xxx

130-012625

Gelelim device driver yani aygıt sürücü kavramına. Şimdiye kadar biraz kernel modüllerine baktık ama bizim modüllerimiz user moddan erişilmez durumdadır.

Device driver’ın bir tanımı da user moddan erişilebilen, kullanılabilen kernel modüldür diye yapılmaktadır. Ama bu tanım bence biraz eksiktir. Bir device driver’ın adı üstünde biraz da gerçek bir cihazla ilişkisi olması gerekir. Elbette LDD3’te de örnek verildiği gibi hayali cihazlar da yaratılabilir.

Her kernel modül bir device driver değildir ama her device driver bir kernel modüldür. (statik derlenmediyse ?)

Aygıt sürücüler, open() fonksiyonu ile bir dosya gibi açılırlar. Buradan bir dosya betimleyici elde edilir. Bu betimleyici read(), write(), lseek(), close() gibi fonksiyonlarla kullanılabilir. Aygıt sürücülere ilişkin bir dosya betimleyicisi böyle bir fonksiyonla kullanıldığında aslında sürücü içerisindeki belirlenen fonksiyonlar çağrılır. Ayrıca ioctl() POSIX fonksiyonu ile (o da sys_ioctl() isimli sistem fonksiyonunu çağırmaktadır) sürücü içerisindeki foo(), bar() gibi “rastgele” fonksiyonları da çağırabiliyoruz. Aslında bir nevi user moddan, kernel modda “istediğimiz” fonksiyonları kullanabiliyoruz. Böylece prosesimiz normalde user modda yapamayacağı şeyleri kernel modda yapar hale geliyor. Aygıt sürücü ile user moddaki proses arasındaki veri transferi sanki bir dosya varmış gibi yapılabilmektedir. Driver’ın fonksiyonlarını çağırmak arka planda aslında mekanizma olarak bir sistem çağrısı yapmaya benzemektedir.

131-000030


Peki aygıt sürücüyü open() ile açıyorsak, biz yol ifadesi olarak ne vereceğiz? Yani “nereyi açacaklar?”

Device file yani aygıt dosyası olan dizin girişleri var. Bunlar gerçek disk dosyası değiller ama arka planda inode elemanı tutuluyor kernel tarafından bunlar için. O inode elemanı aslında aygıt sürücüyü gösteriyor. Tipik olarak /dev altındaki dosyalar böyle dosyalardır.

ls ile listelediğimiz zaman c character device, b ise block device demektir.

Aygıt sürücüler bu iki tipe ayrılmaktadır. Karakter aygıt sürücüleri daha sık kullanılmaktadır. Blok aygıt sürücüleri tipik olarak disk gibi medyalara erişmek için kullanılır. Bu sürücülerde aktarım blok blok yapılır. Bu işlemlerin daha etkin gerçekleştirilmesi için çekirdeğin buffer cache ya da page cache gibi alt sistemleri de devreye sokulur. Karakter aygıt sürücüleri de blok blok aktarım yapMAyan aygıtlar için kullanılır. Seri port sürücüleri karakter aygıtı olarak çalışırken RAM disk oluşturan sürücüler blok aygıtı sürücüleridir. Karakter aygıtı sürücüsü yazmak blok aygıtına göre daha kolaydır.

Major ve Minor Numara

Aygıt dosyalarında major ve minör numara diye bir kavram vardır.

Örnek:

$ ls -lah /dev/tty
crw-rw-rw- 1 root tty 5, 0 Dec 27 14:42 /dev/tty

Burada c bir character device olduğunu göstermektedir. Major numara 5, minör numara ise 0 dır.

Dosya ile ilgili bilgiler stat(), fstat(), lstat() gibi POSIX fonksiyonları ile elde edilmektedir. struct stat yapısının dev_t türünden st_rdev elemanı eğer dosya bir aygıt dosyası ise dosyanın major ve minör numaralarını belirtir. <sys/stat.h> dosyasındaki S_ISCHR ve S_ISBLK makroları ile ilgili dosyanın bir aygıt dosyası olup olmadığını öğrenebiliriz. [1] [2]

Genelde major numara onun türünü anlatırken minör numara da onun aynı türden hangi cihaz olduğunu gösterir. Mesela bilgisayarımızda iki seri port varsa tipik olarak major numaraları aynı, minörleri farklı olacaktır.

Biz device driver yazarken diyoruz ki: Bu sürücünün major numarası şu minörü de bu olsun. Sonra aynı numaralarda bir aygıt dosyası oluşturuyoruz. open() ile açınca bizim driver’a gidiyor. Yani biz bu numaraları bir adres olarak da düşünebiliriz. Ama aynı major ve minor numaraya sahip iki farklı aygıt dosyası Linux üzerinde oluşturulabilir, partikte pek görmesek de. Bu durumda yapılan işlemler, yani o dosyaya yapılan sistem çağrıları, aynı sürücüye gidecektir.

mknod() Fonksiyonu

Biz user tarafta open() ile disk dosyası, mkfifo() de FIFO dosyası oluşturabiliyoruz. Benzer şekilde aygıt dosyaları için de ayrı bir fonksiyon vardır. mknod() isimli POSIX fonksiyonu da aygıt dosyası oluşturmak için kullanılır. [3] Linux’ta doğrudan aslında sys_mknod() isimli syscall cağrılır bu fonksiyon tarafından.

#include <sys/stat.h>

int mknod(const char *path, mode_t mode, dev_t dev);

Birinci parametre aygıt dosyasının yol ifadesi, ikincisi erişim haklarını ve üçüncüsü de major ve minör numarayı belirtmektedir. Major ve minör numara, dev_t türü içerisinde beraber belirtilir.

İkinci izin parametresi ile beraber aslında dosyanın türünü de belirtmeliyiz. Karakter mi, blok mu, FIFO mu vs. mknod() fonksiyonunu aslında mkfifo() yerine de kullanabiliriz.

Aşağıdaki sembolik sabitlerden biri OR ile girilebilir:

S_IFCHR  //(Karakter Aygıt Sürücüsü)
S_IFBLK  //(Blok Aygıt Sürücüsü)

Belirttiğimiz gibi diğer dosya türlerini de oluşturabiliriz, her ne kadar bu amaçla kullanmayacak olsak da. Bu durumda üçüncü parametre, dev_t, anlamsız olmaktadır ve kullanılmaz.

Girilebilecek diğer değerler:

S_IFREG  //(Disk dosyası yaratmak için)
S_IFIFO  //(İsimli boru dosyası yaratmak için)
S_IFSOCK //(UNIX domain soket dosyası yaratmak için)

Fonksiyon başarı durumda 0, başarısızlık durumunda -1 döner ve errno set edilir.

mknodat() Fonksiyonu

Bu fonksiyonun da at li (🐴) versiyonu vardır.

#include <fcntl.h>

int mknodat(int fd, const char *path, mode_t mode, dev_t dev);

Bildiğimiz at’li versiyonlar gibidir. Fonksiyon ilgili dizine ait dosya betimleyicisini ve göreli yol ifadesini parametre olarak alır. O dizinden göreli biçimde yol ifadesi oluşturulur. Birinci parametreye AT_FDCWD gibi özel bir değer geçersek fonksiyon at’siz gibi çalışır. Benzer şekilde ikinci parametre mutlak yol ifadesi ise, /xx, bu durumda birinci parametredeki dizin yine kullanılmaz.


Her iki fonksiyon da prosesin umask değerini dikkate alır.


İlerleyen kısımlarda kod yazacağız ama önce mknod isimli komuta bakalım.

mknod Komutu

mknod isimli bir de shell fonksiyonu vardır. Shell komutlarını man 1 den görebiliriz:

https://man7.org/linux/man-pages/man1/mknod.1.html

Böylece direk shell’den de böyle bir dosya yaratabiliriz. Örnek:

$ sudo mknod device c 123 321

$ ls -lah device
crw-r--r-- 1 root root 123, 321 Dec 27 19:13 device

Bu sayede device isminde istediğimiz major ve minör numaralarda bir aygıt dosyası yarattık, türü de character, c. Peki sudo ya ihtiyacımız var mı? Önce rm device ile dosyamızı silip sudo olmadan tekrar yaratmaya çalışalım. rm için sudo yetkisi gerekmiyor.

$ mknod device c 123 321
mknod: device: Operation not permitted

Olmadı. O zaman strace ile bir bakalım.

$ strace mknod device c 123 321
...
mknodat(AT_FDCWD, "device", S_IFCHR|0666, makedev(0x7b, 0x141)) = -1 EPERM (Operation not permitted)
...

mknodat() bir sistem çağrısı, sys call. EPERM açıklamasına bakalım.

EPERM  mode requested creation of something other than a regular
      file, FIFO (named pipe), or UNIX domain socket, and the
      caller is not privileged (Linux: does not have the
      CAP_MKNOD capability); also returned if the filesystem
      containing path does not support the type of node
      requested.

Yani root olmamız gerekiyormuş, teyit etmiş olduk.

Yardımcı Fonksiyon ve Makrolar

User Space

Major ve minör numaraların dev_t içerisinde beraber tutulduğunu söylemiştik. Bunlar için user space’te sunulan çeşitli yardımcı araçlar vardır.

makedev() fonksiyonu, major ve minör numaradan dev_t üretmemizi sağlar.

#include <sys/sysmacros.h>

dev_t makedev(unsigned int maj, unsigned int min);

unsigned int major(dev_t dev);
unsigned int minor(dev_t dev);

Benzer şekilde yukarıda prototipleri gösterilen major() ve minor() isimli fonksiyonlar da dev_t türünden bir nesneden major ve minör numaraları “ayıklamamızı” sağlar.

Aslında major ve minör numaralar, dev_t nin belli bitlerinde bulunmaktadır. Fakat hangi bitlerinde bulunduğu sistemden sisteme değişeceği için bizim her zaman bu fonksiyonları kullanmamız gerekir.

Kernel Space

Kernel space/mode yani modül/driver kısmında ise aynı işler için tanımlanmış makrolar vardır.

 7#define MINORBITS	20
 8#define MINORMASK	((1U << MINORBITS) - 1)
 9
10#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
11#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
12#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))

Gösterilen MAJOR(), MINOR() ve MKDEV() makroları aynı amaçla kullanılır. Görüldüğü üzere aslında düşük 20-bit minör numarayı tutarken geri kalan bitler, güncel durumda dev_t nin 32-bittir yani üst 12-bit, major numarayı tutar. Ama hem user hem kernel tarafında bu bilgiyi doğrudan kullanmamamız gerekir.

18typedef u32 __kernel_dev_t;
19
20typedef __kernel_fd_set		fd_set;
21typedef __kernel_dev_t		dev_t;

mymknod Programımız

Şimdi POSIX fonksiyonu olan mknod() a geri dönüp mymknod isminde kendi mknod programımızı yazalım.

Yapılacaklar

Yazı henüz bitmemiştir. 131-011945