> "kernel modules" : Kernel'ın bir parçası gibi işlev gören, herhangi bir koruma engeline takılmayan, kernel modda çalışan özel olarak hazırlanmış modüllere (yani kod parçalarına) Linux dünyasında "kernel modülleri (kernel modules)" denilmektedir. Diğer yandan kernel modülleri eğer kesme gibi bazı mekanizmaları kullanıyorsa ve bir donanım aygıtını yönetme iddiasındaysa bunlara özel olarak "aygıt sürücüleri (device drivers)" da denilmektedir. Nasıl bir masaüstü bilgisayara kart taktığımızda artık o kart donanımın bir parçası haline geliyorsa, "kernel" modülleri ve aygıt sürücüleri de "install" edildiklerinde adeta "kernel" ın bir parçası haqline gelmektedir. Bu yüzdendir ki, -> Her aygıt sürücü bir "kernel" modülüdür ancak her kernel modülü bir aygıt sürücü değildir. Bu nedenle biz yalnızca "kernel modülü" dediğimizde genel olarak aygıt sürücüleri de dahil etmiş olacağız. Biz bu bölümde Linux sistemleri için "kernel" modüllerinin ve aygıt sürücülerinin nasıl yazılacağını ele alacağız. "kernel" modülleri ve aygıt sürücüler genel bir konu değildir. Her işletim sisteminin o sisteme özgü bir aygıt sürücü mimarisi vardır. Hatta bu mimari, işletim sisteminin versiyonundan versiyonuna da değişebilmektedir. Bu nedenle aygıt sürücü yazmak genel bir konu değil, o işletim sistemine hatta işletim sisteminin belirli versiyonlarına özgü bir konudur. "kernel" modüllerinde ve aygıt sürücülerde her türlü fonksiyon kullanılamaz. Bunları yazabilmek için özel başlık dosyalarına ve kütüphanelere gereksinim duyulmaktadır. Bu nedenle ilk yapılacak şey bu başlık dosyalarının ve kütüphanelerin ilgili sisteme yüklenmesidir. Genellikle bir Linux sistemini yüklediğimizde zaten "kernel" modüllerini ve aygıt sürücüleri oluşturabilmek için gereken kütüphaneler ve başlık dosyaları zaten yüklü biçimde bulunmaktadır. Tabii programcı "kernel" kodlarını da kendi makinesine indirmek isteyebilir. Bunun için aşağıdaki komut kullanılabilir: "sudo apt-get install linux-source" Eğer sisteminizde Linux'un kaynak kodları yüklü ise bu kaynak kodlar "/usr/src" dizininde bulunmaktadır. Bu dizindeki "linux-headers-$(uname -r)" dizini, kaynak kodlar yüklü olmasa bile, bulunan bir dizindir ve bu dizin çekirdek modülleri ve aygıt sürücülerin "build edilmeleri" için gereken başlık dosyalarını barındırmaktadır. Benzer biçimde "/lib/modules" isimli dizinde "$(uname -r)" isimli bir dizin vardır. Bu dizin "kernel" modüllerinin "build" edilmesi için gereken bazı kodları ve kütüphaneleri bulundurmaktadır. Özetle "kernel" modülleri ve aygıt sürü yazmak için gerekli olan dizinler şunlardır: -> "linux-headers-$(uname -r)" dizini. Bu dizinde başlık dosyaları bulunmaktadır. -> "/lib/modules/$(uname -r)" dizini. Burada da "kernel" modülün "build" edilmesi için gerekli olan dosyalar ve kütüphaneler bulunmaktadır. Bir "kernel" modülünde biz "user-mod" için yazılmış kodları kullanamayız. Çünkü orası ayrı bir dünyadır. Ayrıca biz "kernel" modüllerinde "kernel" içerisindeki her fonksiyonu kullanamayız. Yalnızca bazı fonksiyonları kullanabiliriz. Bunlara "kernel tarafından export edilmiş fonksiyonlar" denilmektedir. "kernel" tarafından "export" edilmiş fonksiyon kavramının, "sistem fonksiyonu" kavramından, bir ilgisi yoktur. Sistem fonksiyonları "user-mod" dan çağrılmak üzere tasarlanmış ayrı bir grup fonksiyondur. Oysa "kernel" tarafından "export" edilmiş fonksiyonlar "user-mod" dan çağrılamazlar. Yalnızca "kernel" modüllerinden çağrılabilirler. Buradan çıkan sonuç şudur: -> Bir "kernel" modül yazılırken ancak "kernel" ın "export" ettiği fonksiyon ve veriler kullanılabilmektedir. Tabii "kernel" ın kaynak kodları çok büyüktür ancak buradaki kısıtlı sayıda fonksiyon "export" edilmiştir. Benzer biçimde programcının oluşturduğu bir "kernel" modül içerisindeki belli fonksiyonları da programcı "export" edebilir. Bu durumda bu fonksiyonlar da başka "kernel" modüllerinden kullanılabilirler. O halde özetle: -> Kernel modülleri yalnızca kernel içerisindeki export edilmiş fonksiyonları kullanabilirler. -> Kendi "kernel" modülümüzde istediğimiz bir fonksiyonu "export" edebiliriz. Bu durumda bizim "kernel" modülümüz "kernel" ın bir parçası haline geldiğine göre başka "kernel" modüller de bizim "export" ettiğimiz fonksiyonları kullanabilir. Mademki "kernel" modüller işletim sisteminin "kernel" kodlarındaki fonksiyon ve verileri kullanabiliyorlar o zaman "kernel" modüller o anda çalışılan "kernel" ın yapısına da bağlı durumdadırlar. Yukarıda da ifade ettiğimiz gibi işletim sistemlerinde "kernel modül yazmak" ya da "aygıt sürücü yazmak" biçiminde genel bir konu yoktur. Her işletim sisteminin "kernel" modül ve aygıt sürücü mimarisi diğerlerinden farklıdır. Dolayısıyla bu konu spesifik bir işletim sistemi için geçerli olabilecek oldukça platform bağımlı bir konudur. Hatta işletim sistemlerinde bazı versiyonlarda genel aygıt sürücü mimarisi bile değiştirilebilmektedir. Dolayısıyla eski aygıt sürücüler yeni versiyonlarda çalışamamakta yenileri de eski versiyonlarda çalışamamaktadır. "Kernel" modüllerinin ve aygıt sürücülerin yazımı için programcının genel olarak "kernel" yapısını bilmesi gerekmektedir. Çünkü bunları yazarken "kernel" ın içerisindeki "export" edilmiş fonksiyonlar kullanılmaktadır. Linux "kernel" modüller ve aygıt sürücüler hakkında yazılmış birkaç kitap vardır. Bunların en klasik olanı "Linux Device Drivers (3. Edition)" kitabıdır. Bu konudaki resmi dokümanlar "kernel.org" sitesindeki "documentation" kısmında bulunmaktadır. Bir "kernel" modülünü derlemek ve "link" etmek maalesef sanıldığından daha zordur. Her ne kadar "kernel" modüller ELF "object" dosyaları biçimindeyse de bunlarda özel bazı "bölümler (sections)" bulunmaktadır. Dolayısıyla bu modüllerin derlenmesinde özel "gcc" seçenekleri devreye sokulur. "Kernel" modüllerin "link" edilmeleri de bazı kütüphane dosyalarının devreye sokulmasıyla yapılmaktadır. Dolayısıyla bir "kernel" modülün "manuel" biçimde "build edilmesi" için bazı ayrıntılı bilgilere gereksinim duyulmaktadır. İşte "kernel" tasarımcıları bu sıkıcı işlemleri kolaylaştırmak için özel "make dosyaları" düzenlemişlerdir. Programcı bu "make" dosyalarından faydalanarak "build" işlemini çok daha kolay yapabilmektedir. * Örnek 1, "Kernel" modüller için "build" işlemini yapan örnek bir "Makefile" aşağıdaki gibi olabilir: obj-m += generic.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Burada önce "/lib/modules/$(uname -r)/build" dizinindeki "Makefile" çalıştırılmış ondan sonra çalışma bu yazdığımız "make" dosyasından devam ettirilmiştir. Özetle bu make dosyası "generic.c" isimli dosyanın derlenerek "kernel" modül biçiminde "link" edilmesini sağlamaktadır. "Kernel" modül birden fazla kaynak dosyadan oluşturulabilir. Bu durumda ilk satır şöyle oluşturulabilir: "obj-m += a.o b.o c.o... " Ya da bu belirtme işlemi ayrı satır halinde de yapılabilir: "obj-m += a.o" "obj-m += b.o" "obj-m += c.o" ... Bizim oluştuduğumuz Makefile dosyasındaki "all" hedefine dikkat ediniz: "make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules" "make" programının "-C" seçeneği "Makefile" dosyasını aramadan önce bu seçeneğin argümanında belirtilen dizine geçiş yapmaktadır. Dolayısıyla aslında yukarıdaki satırla "/lib/modules/$(shell uname -r)/build" dizinindeki "Makefile" dosyası çalıştırılacaktır. "Make" dosyasının başındaki kısım aslında standart bir "make" yönergesi değildir. Ana "make" dosyası bu dosyayı ele almaktadır. Tipik olarak bir "Makefile" dosyasının içeriği de aşağıdaki gibidir: # Makefile obj-m += generic.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean * Örnek 2, Tabii aslında "make" dosyası parametrik biçimde de oluşturabilmektedir. Bu durumda "make" programı çalıştırılıken bu parametrenin değeri de belirtilmelidir. Örneğin: make file=hellomodule Bu durumda "Makefile" dosyamızın içeriği de aşağıdaki gibi olacaktır: # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Şimdi en basit bir kernel modülü oluştutup bunu bir başlangıç noktası olarak kullanalım. Bu modülümüze "helloworld" ismini verelim. Bu kernel modül aşağıdaki gibi build edilebilir: "$ make file=helloworld" "Build" işlemi bittiğinde "kernel" modül "helloworld.ko" dosyası biçiminde oluşturulacaktır. Burada "ko" uzantısı "kernel object" sözcüklerinden kısaltılmıştır. Bir "kernel" modül, "kernel" ın içerisine "insmod" isimli programla yerleştirilmektedir. Tabii bu programın "sudo" ile "root" önceliğinde çalıştırılması gerekmektedir. Örneğin, "$ sudo insmod helloworld.ko" Artık "kernel" modülümüz "kernel" ın içerisine yerleştirilmiştir. Yani modülümüz adeta "kernel" ın bir parçası gibi işlev görecektir. "Kernel" modüller istenildiği zaman "rmmod" isimli programla "kernel" dan çıkartılabilirler. Bu programın da yine "sudo" ile "root" önceliğinde çalıştırılması gerekir. Örneğin: "$ sudo rmmod helloworld.ko" Aşağıda bu konuya ilişkin bir örnek verilmiştir. * Örnek 1, "make" işlemi şöyle yapılabilir: "$ make file=helloworld" Dosyaların içerikleri ise şöyle olabilir: # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* helloworld.c */ #include #include MODULE_LICENSE("GPL"); int init_module(void) { printk(KERN_INFO "Hello World...\n"); return 0; } void cleanup_module(void) { printk(KERN_INFO "Goodbye World...\n"); } En basit bir kernel modülde aşağıdaki iki temel dosya include edilmelidir: "#include " "#include " Bu iki dosya "/lib/modules/$(uname -r)/build/include" dizini içerisindedir. (Yani "libc" ve POSIX kütüphanelerinin başlık dosyalarının bulunduğu "/usr/include" içerisinde değildir.) Yukarıda kullandığımız "make" dosyası "include" dosyalarının bu dizinde aranmasını sağlamaktadır. Eskiden "kernel" modüllerine modül lisansının eklenmesi zorunlu değildir. Ancak belli bir süreden sonra bu zorunlu hale getirilmiştir. Modül lisansı "MODULE_LICENSE" isimli makro ile belirtilmektedir. Bu makro "" dosyası içerisinde bildirilmiştir. Tipik modül lisansı aşağıdaki gibi "GPL" biçiminde oluşturulabilir: MODULE_LICENSE("GPL"); Bir "kernel" modül yüklendiğinde "kernel" modül içerisinde belirlenmiş olan bir fonksiyon çağrılır (bu fonksiyon C++'taki "constructor" gibi düşünülebilir.) "Default" çağrılacak fonksiyonun ismi `init_module` biiçimindedir. Bu fonksiyonun geri dönüş değeri "int" türdendir ve parametresi yoktur. Fonksiyon başarı durumunda "0" değerine başarısızlık durumunda negatif hata koduna geri dönmelidir. Bu fonksiyon başarısızlıkla geri dönerse modülün yüklenmesinden vazgeçilmektedir. Benzer biçimde bir modül "kernel" alanından boşaltılırken de yine bir fonksiyon çağrılmaktadır. (Bu fonksiyon da C++'taki "destructor" gibi üşünülebilir.) "Default" çağrılacak fonksiyonun ismi "cleanup_module" biçimindedir. Bu fonksiyonun geri dönüş değeri ve parametresi "void" biçimdedir. "Kernel" modüller tıpkı daha önce görmüş olduğumuz "deamon" lar gibi ekrana değil "log" dosyalarına yazarlar. Bunun için "kernel" içindeki "printk" isimli fonksiyon kullanılmaktadır. Yukarıdaki "helloworld" modülünde kullanmış olduğumuz "printk" fonksiyonu "'kernel' ın 'printf' fonksiyonu" gibi düşünülebilir. "printk" fonksiyonun genel kullanımı "printf" fonksiyonu gibidir. "Default" durumda bu fonksiyon mesajların "/var/log/syslog" dosyasına yazdırılması sağlamaktadır. "printk" fonksiyonun prototipi "" dosyası içerisindedir. "printk" fonksiyonun örnek kullanımı şöyledir: printk(KERN_INFO "This is test\n"); Mesajın solundaki "KERN_XXX" biçimindeki makrolar aslında bir "string" açımı yapmaktadır. Dolayısıyla yan yana iki "string" birleştirildiği için mesaj yazısının başında küçük bir önek bulunur. Bu örnek (yani bu makro) mesajın türünü ve aciliyetini belirtmektedir. Tipik "KERN_XXX" makroları şunlardır: "KERN_EMERG" "KERN_ALERT" "KERN_CRIT" "KERN_ERR" "KERN_WARN" "KERN_NOTICE" "KERN_INFO" "KERN_DEBUG" Bu makroların tipik yazım biçimi şöyledir: #define KERN_EMERG "<0>" #define KERN_ALERT "<1>" #define KERN_CRIT "<2>" #define KERN_ERR "<3>" #define KERN_WARNING "<4>" #define KERN_NOTICE "<5>" #define KERN_INFO "<6>" #define KERN_DEBUG "<7>" Ancak bu makrolar da çeşitli "kernel" versiyonlarında değişiklikler yapılabilmektedir. Aslında "KERN_XXX" makroları ile "printk" fonksiyonunu kullanmak yerine "pr_xxx" makroları da kullanılabilir. Şöyle ki: "printk(KERN_INFO "Hello World...\n");" ile "pr_info("Hello World...\n");" tamamen eşdeğerdir. Diğer "pr_xxx" makroları şunlardır: "pr_emerg" "pr_alert" "pr_crit" "pr_err" "pr_warning" "pr_notice" "pr_info" "pr_debug" "printk" fonksiyonun yazdıklarını "/var/log/syslog" dosyasına bakarak görebiliriz. Örneğin: "tail /var/log/syslog" Ya da "dmesg" programı ile de aynı bilgi elde edilebilir. "Kernel" modüller "kernel" ın içerisine yerleştirildiği için "kernel" modüllerde biz "user-mode" daki kütüphaneleri kullanamayız. Örneğin, "kernel" mode içerisinde Standart C fonksiyonlarını ve POSIX fonksiyonlarını kullanamayız. Çünkü Standart C fonksiyonları ve POSIX foksiyonları "user-mode" programlar için oluşturulmuş kütüphanelerin içerisindedir. Biz "kernel" modüllerin içerisinde yalnızca "export edilmiş kernel fonksiyonlarını" kullanabiliriz. "Kernel" modüller içerisinde kullanılabilecek "export" edilmiş "kernel" fonksiyonları "Linux Kernel API" ismi altında "kernel.org" tarafından dokümante edilmiştir. Bu fonksiyonların dokğmantasyonuna aşağıdaki bağlantıdan erişebilirsiniz: https://docs.kernel.org/core-api/kernel-api.html Aslında "init_module" ve "cleanup_module" fonksiyonlarının ismi değiştirilebilir. Fakat bunun için bildirimde bulunmak gerekir. Bildirimde bulunmak için ise "module_init(...)" ve "modeul_exit(...)" makroları kullanılmaktadır. Bu makrolar kaynak kodun herhangi bir yerinde bulundurulabilir. Ancak makro içerisinde belirtilen fonksiyonların daha yukarıda bildirilmiş olması gerekmektedir. Bu makrolar tipik olarak kaynak kodun sonuna yerleştirilmektedir. * Örnek 1, "make" işlemi şöyle yapılabilir: "$ make file=helloworld" # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* helloworld.c */ #include #include int helloworld_init(void) { printk(KERN_INFO "Hello World...\n"); return 0; } void helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); * Örnek 2, Genellikle "kernel" modül içerisindeki global değişkenlerin ve fonksiyonların "internal linkage" yapılması tercih edilmektedir. Bu durum birtakım isim çakışmalarını da engelleyecektir. "make" işlemi şöyle yapılabilir: "$ make file=helloworld" # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* helloworld.c */ #include #include static int helloworld_init(void) { printk(KERN_INFO "Hello World...\n"); return 0; } static void helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); * Örnek 3, "Kernel" modüllerde "init" ve "cleanup" fonksiyonlarında fonksiyon isimlerinin soluna "__init" ve "__exit" makroları getirilebilmektedir. Bu makrolar "" dosyası içerisindedir. Bu dosya da "" dosyası içerisinden "include" edilmiştir. "__init" makrosu ilgili fonksiyonu ELF dosyasının özel bir bölümüne ("section") yerleştirir. Modül yüklendikten sonra bu bölüm "kernel" alanından atılmaktadır. "__exit" makrosu ise "kernel" ın içine gömülmüş modüllerde fonksiyonun dikkate alınmayacağını (dolayısıyla hiç yüklenmeyeceğini) belirtir. Ancak sonradan yüklemelerde bu makronun bir etkisi yoktur. "make" işlemi şöyle yapılabilir: "$ make file=helloworld" # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* helloworld.c */ #include #include static int __init helloworld_init(void) { printk(KERN_INFO "Hello World...\n"); return 0; } static void __exit helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); Nasıl "user-mode" programlarda "main" fonksiyonuna komut satırı argümanları geçirilebiliyorsa benzer biçimde kernel modüllere de argüman (ya da parametre diyebiliriz) geçirilebilmektedir. Bu konuya genel olarak "kernel modül parametreleri" denilmektedir. "Kernel" modüllere parametre geçirme işlemi "insmod" ile modül yüklenirken komut satırında modül isminden sonra "değişken=değer" çiftleriyle yapılmaktadır. Örneğin: "sudo insmod helloworld.ko number=10 msg="\"This is a test\"" values=10,20,30,40,50" Bu örnekte "number" parametresi "int" bir değerden, "msg" parametresi ise bir yazıdan oluşmaktadır. "values" parametresi birden fazla "int" değerden oluşmaktadır. Bu tür parametrelere modülün dizi parametreleri denilmektedir. "Kernel" modüllere geçirilen parametreleri modül içerisinde almak için "module_param" ve "module_param_array" isimli makrolar kullanılır. Bu makrolardan, >> "module_param" makrosunun üç parametresi vardır: "module_param(name, type, perm)" "name" parametresi ilgili değişkenin ismini belirtmektedir. Biz makroyu çağırmadan önce bu isimde bir global değişkeni tanımlamalıyız. Ancak buradaki değişken isminin komut satırında verilen parametre (argüman da diyebiliriz) ismi ile aynı olması gerekmektedir. "type" ilgili parametrenin türünü belirtir. Bu tür şunlardan biri olabilir: "int" "long" "short" "uint" "ulong" "ushort" "charp" "bool" "invbool" Buradaki "charp" "char" türden adresi, "invbool" ise geçirilen argümanın "bool" bakımdan tersini temsil etmektedir. "module_param" makrosunun "perm" parametresi "/sys/modules/" dizininde yaratılacak olan "parameters" dizininin erişim haklarını belirtir. Bu makrolar global alanda herhangi bir yere yerleştirilebilir. * Örnek 1, "kernel" modülümüzde "count" ve "msg" isimli iki parametre olsun. Bunlara ilişkin "module_param" makroları şöyle oluşturumalıdır: int count = 0; char *msg = "Ok"; module_param(count, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); module_param(msg, charp, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); "char *" türünden modül parametresi için makrodaki türün "charp" biçiminde olduğuna dikkat ediniz. Buradaki gösterici "const" olamamaktadır. Bizim bir parametre için "module_param" makrosunu kullanmış olmamız modül yüklenirken bu parametrenin belirtilmesini zorunlu hale getirmemektedir. Bu durumda bu parametreler "default" değerlerde kalacaktır. Yukarıdaki parametreleri "helloworld" modülüne aşağıdaki gibi geçirebiliriz: "$ sudo insmod helloworld.ko count=100 msg="\"this is a test\""" Burada neden iç içe tırnakların kullanıldığını merak edebilirsiniz. Şöyleki; -> "shell" programı üzerinde tırnaklar "boşluklarla ayrılmış olan yazıların tek bir komut satırı argümanı olarak ele alınacağını belirtmektedir. Ancak bizim ayrıca yazısal argümanları modüllere parametre yoluyla aktarırken onları tırnaklalamız gerekir. Bu nedenle iç içe iki tırnak kullanılmıştır. >> "Kernel" modüle birden fazla değer de bir dizi gibi aktarılabilir. Bunun için "module_param_array" makrosu kullanılmaktadır. "module_param_array" makrosu da şöyledir: "module_param_array(name, type, nump, perm)" Makronun birinci ve ikinci parametreleri yine değişken ismi ve türünü belirtir. Tabii buradaki değişken isminin bir dizi ismi olarak girilmesi gerekmektedir. Üçüncü parametre toplam kaç değerin modüle dizi biçiminde aktarıldığını belirten "int" bir nesnenin adresini (ismini değil) alır. Son parametre yine oluşturulacak dosyanın erişim haklarını belirtmektedir. * Örnek 1, static int values[5]; static int size; module_param_array(values, int, &size, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); module_param_array makrosuyla bir diziye değer aktarırken değerlerin virgüllerle ayrılmış bir biçimde girilmesi gerekmektedir. Şöyleki; "sudo insmod helloworld.ko values=1,2,3,4,5" Burada eğer verilen değerler dizinin uzunluğundan fazla olursa zaten modül yüklenmemektedir. Bu örnekte biz girilen değerlerin sayısını "size" nesnesinden alabiliriz. Aşağıdaki örnekte üç parametre komut satırından "kernel" modüle geçirilmiştir. Komut satırındaki isimlerle programın içerisindeki değişken isimlerinin aynı olması gerektiğine dikkat ediniz. Yazıların geçirilmesinde iki tırnaklar kullanılır. Dizi geçirirken yanlışlıkla virgüllerin arasına boşluk karakterleri yerleştirmeyiniz. * Örnek 1, Programu şöyle make yapabilirsiniz: "make file=helloworld" Yüklemeyi şöyle yapabilirsiniz: sudo insmod helloworld.ko count=100 msg="\"this is a test\"" values=1,2,3,4,5 # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* helloworld.c */ #include #include MODULE_LICENSE("GPL"); static int count = 0; static char *msg = "Ok"; static int values[5]; static int size; module_param(count, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); module_param(msg, charp, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); module_param_array(values, int, &size, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); static int __init helloworld_init(void) { int i; printk(KERN_INFO "Hello World...\n"); printk(KERN_INFO "count = %d\n", count); printk(KERN_INFO "msg = %s\n", msg); for (i = 0; i < size; ++i) { printk(KERN_INFO "%d\n", values[i]); } return 0; } static void __exit helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); Modül parametreleri kernel tarafından "/sys/module" içerisindeki modül ismine ilişkin dizinin altındaki "parameters" dizininde dosyalar biçiminde dış dünyaya sunulmaktadır. İşte makrodaki erişim hakları buradaki parametre dosyalarının erişim haklarını belirtmektedir. "Kernel" modül "root" kullanıcısı tarafından yüklendiğine göre bu dosyaların da Kullanıcı ID ve Grup ID değerleri "root" olacaktır. * Örnek 1, Yukarıdaki "helloworld" modülü için bu dosyalar "/sys/module/helloworld/parameters" dizini içerisinde aşağıdaki gibidir: "$ ls -l /sys/module/helloworld/parameters" toplam 0 -rw-r--r-- 1 root root 4096 Mar 22 22:24 count -rw-r--r-- 1 root root 4096 Mar 22 22:24 msg Bu dosyalar doğrudan kernel modüldeki parametre değişkenlerini temsil etmektedir. Yani örneğin biz buradaki "count" dosyasına başka bir değer yazdığımızda "kernel" modülümüzdeki "count" değeri de değişmiş olacaktır. Tabii yukarıdaki erişim haklarıyla biz dosyaya yazma yapamayız. Bu erişim haklarıyla yazma yapabilmemiz için yazmayı yapan programın "root" olması gerekir. Terminalden bu işlem aşağıdaki gibi yapılabilir: $ sudo bash -c "echo 200 > /sys/module/helloworld/parameters/count" Burada işlemi aşağıdaki gibi yapamayacağımıza dikkat ediniz: $ sudo echo 200 > /sys/module/helloworld/parameters/count Çünkü burada her ne kadar "echo" programı "root" önceliğinde çalıştırılıyorsa da dosyayı açan kullanıcı "root" değildir. Linux'ta bir "kernel" modül artık "user-mode" tan kullanılabilir hale getirildiyse buna "aygıt sürücü (device driver)" denilmektedir. Aygıt sürücüler "open" fonksiyonuyla bir dosya gibi açılırlar. Bu açma işleminden bir dosya betimleyicisi elde edilir. Bu dosya betimleyicisi "read", "write", "lseek", "close" gibi fonksiyonlarda kullanılabilir. Aygıt sürücülere ilişkin dosya betimleyicileri bu fonksiyonlarla kullanıldığında aygıt sürücü içerisindeki belirlenen bazı fonksiyonlar çağrılmaktadır. Yani tersten gidersek biz örneğin aygıt sürücümüze ilişkin dosya betimleyicisi ile "read" ve "write" fonksiyonu çağrıldığığımızda aslında aygıt sürücümüzdeki belli fonksiyonlar çalışırılmaktadır. Böylece aygıt sürücü ile "user-mod" arasında veri transferleri yine dosyalarda olduğu gibi dosya fonksiyonlarıyla yapılabilmektedir. Pekiyi "user-mode" tan aygıt sürücümüzdeki herhangi bir fonksiyonu da çağırabilir miyiz? Yanıt evet. Bunun için "ioctl" isimli bir POSIX fonksiyonu (tabii bu POSIX fonksiyonu "sys_ioctl" isimli sistem fonksiyonunu çağırmaktadır) kullanılmaktadır. Aygıt sürücü içerisinde fonksiyonlara birer kod numarası atanır. Sonra ioctl fonksiyonunda bu kod numarası belirtilir. Böylece akış "user-mod" tan "kernel-moda" geçerek belirlenen fonksiyonu kernel modda çalıştıracaktır. Özetle; -> Bir aygıt sürücüsü "kernel-modda" çalışmaktadır. -> Biz aygıt sürücüsünü "open" fonksiyonu ile açıp elde ettiğimiz betimleyici ile "read", "write", "lseek" ve "close" fonksiyonlarını çağırdığımızda aygıt sürücüsü içerisindeki ilgili fonksiyon çalıştırılmaktadır. -> Aygıt sürücüsü içerisindeki herhangi bir fonksiyon ise "user-mode" tan "ioctl" isimli fonksiyonla çalıştırılmaktadır. Tabii "user mode" programlar aygıt sürücü içerisindeki kodları "read", "write", "lseek", "close", "ioctl" gibi fonksiyonlar yoluyla çalıştırdıklarında proses, "user-mode" tan geçici süre "kernel-mode"'a geçer, ilgili kodlar "kernel-mode" da koruma engeline takılmadan çalıştırılır. Fonksiyonların çalışması bittiğinde proses yine "user-mode" da döner. Pekiyi aygıt sürücüleri açmak için "open" fonksiyonunda yol ifadesi olarak (yani dosya ismi olarak) ne verilecektir? İşte aygıt sürücüler dosya sisteminde bir dizin girişiyle temsil edilmektedir. O dizin girişi "open" ile açıldığında aslında o dizin girişinin temsil ettiği aygıt sürücü açılmış olur. Bu biçimdeki aygıt srücüleri temsil eden dizin girişlerine "aygıt dosyaları (device files)" denilmektedir. Aygıt dosyaları diskte bir dosya belirtmemektedir. "Kernel" içerisindeki aygıt sürücüyü temsil eden bir dizin girişi belirtmektedir. Aygıt dosyalarının "i-node" tablosunda bir "i-node" elemanı vardır ancak bu "i-node" elemanı diskte bir yer değil "kernel" da bir aygıt sürücü belirtmektedir. Pekiyi bir aygıt dosyası nasıl yaratılmaktadır ve nasıl bir aygıt sürücüyü temsil eder hale getirilmektedir? İşte her aygıt sürücünün majör ve minör numaraları vardır. Aynı zamanda aygıt dosyalarının da majör ve minör numaraları vardır. Bir aygıt sürücünün majör ve minör numarası bir aygıt dosyasının majör ve minör numarasıyla aynıysa bu durumda o aygıt dosyası o aygıt sürücüyü temsil eder. Aygıt dosyaları özel dosyalardır. Bir dosyanın aygıt dosyası olup olmadığı "ls -l" komutunda dosya türü olarak 'c' (karakter aygıt sürücüsü) ya da 'b' (blok aygıt sürücüsü) ile temsil edilmektedir. Anımsanacağı gibi dosya bilgileri "stat", "fstat", "lstat" fonksiyonlarıyla elde ediliyordu. İşte "struct stat" yapısının "dev_t" türünden "st_rdev" elemanı eğer dosya bir aygıt dosyasıysa dosyanın majör ve minör numaralarını belirtir. Biz de "" dosyasındaki "S_ISCHR" ve "S_ISBLK" makrolarıyla ilgili dosyanın bir aygıt dosyası olup olmadığını öğrenebiliriz. Yukarıda da belirttiğimiz gibi aygıt sürücüler, "karakter aygıt sürücüleri (character device driver)", "blok aygıt sürücüleri (block device driver)" olmak üzere ikiye ayrılmaktadır. Karakter aygıt sürücüleri daha yaygın kullanılmaktadır. Biz kursumuzda önce karakter aygıt sürücülerini sonra blok aygıt sürücülerini ele alacağız. O halde şimdi bizim bir aygıt dosyasını nasıl oluşturacağımızı ve aygıt sürücüye nasıl majör ve minör numara atayacağımızı bilmemiz gerekir. Aygıt dosyaları "mknod" isimli POSIX fonksiyonuyla (bu fonksiyon Linux'ta doğrudan "sys_node" isimli sistem fonksiyonunu çağırmaktadır) ya da komut satırından "mknod" komutuyla (bu komut da "mknod" fonksiyonu ile işlemini yapmaktadır) yaratılabilir. "mknod" fonksiyonun prototipi şöyledir: #include int mknod(const char *pathname, mode_t mode, dev_t dev); Fonksiyonun birinci parametresi yaratılacak aygıt dosyasının yol ifadesini, ikinci parametresi erişim haklarını ve üçüncü parametresi de aygıt dosyasının majör ve minör numaralarını belirtmektedir. Aygıt dosyasının majör ve minör numaraları "dev_t" türünden tek bir değer ile belirtilmektedir. "dev_t" türü POSIX standartlarına göre herhangi bir tamsayı türü olabilmektedir. Biz majör ve minör numaraları user mod programlarda "makedev" isimli makroyla oluştururuz. Bir "dev_t" türünden değerin içerisinden major numarayı almak için major makrosu, minor numarayı almak için ise minor makrosu bulunmaktadır: #include dev_t makedev(unsigned int maj, unsigned int min); unsigned int major(dev_t dev); unsigned int minor(dev_t dev); Yani aslında majör ve minör numaralar "dev_t" türünden bir bir değerin belli bitlerinde bulunmaktadır. Ancak bu numaraların "dev_t" türünden değerin hangi bitlerinde bulunduğu sistemden sisteme değişebileceği için bu makrolar kullanılmaktadır. Ancak "kernel" modda bu makrolar yerine aşağıdakiler kullanılmaktadır: #include MKDEV(major, minor) MAJOR(dev) MINOR(dev) Linux'ta son versiyonlar da dikkate alındığında "dev_t" türü "32-bit" lik işaretsiz bir tamsayı türündendir. Bu "32-bit" in yüksek anlamlı 12 biti majör numarayı, düşük anlamlı 20 biti ise minör numarayı temsil etmektedir. Ancak programcı bu varsayımlarla kodunu düzenlememeli, yukarıda belirtilen makroları kullanmalıdır. "mknod" fonksiyonun ikinci parametresindeki erişim haklarına aygıt dosyasının türünü belirten aşağıdaki sembolik sabitlerden biri de "bit-wise OR" operatörü ile eklenmelidir: S_IFCHR (Karakter aygıt sürücüsü) S_IFBLK (Blok aygıt sürücüsü) Aslında "mknod" fonksiyonu ile Linux sistemlerinde isimli boru dosyaları, UNIX domain soket dosyaları ve hatta normal dosyalar da yaratılabilmektedir. Bu durumda fonksiyonun aygıt numarası belirten üçüncü parametresi fonksiyon tarafından dikkate alınmamaktadır. Bu özel dosyalar için erişim haklarına eklenecek makrolar da şunlardır: S_IFREG (Disk dosyası yaratmak için) S_IFIFO (İsimli boru dosyası yaratmak için) S_IFSOCK (UNIX domain soket dosyası yaratmak için) Aslında "mknod" fonksiyonu aygıt dosyaları yaratmak için kullanılıyor olsa da yukarıda belirttiğimiz özel dosyaları da yaratabilmektedir. Tabii zaten isimli boru dosyasını yaratmak için mkfifo fonksiyonu, normal dosyaları yaratmak için "open" fonksiyonu kullanılabilmektedir. "mknod" fonksiyonu başarı durumunda "0" değerine, başarısızlık durumunda "-1" değerine geri dönmektedir. Ayrıca "mknod" POSIX fonksiyonunun "mknodat" isimli "at" li bir versiyonu da bulunmaktadır: #include int mknodat(int fd, const char *path, mode_t mode, dev_t dev); Bu "at" li versiyon daha önce görmüş dolduğumuz "at" li fonksiyonlar gibi çalışmaktadır. Yani fonksiyon ilgili dizine ilişkin dosya betimleyicisini ve göreli yol ifadesini parametre olarak alır. O dizinden göreli biçimde yol ifadesini oluşturur. Yine fonksiyonun birinci parametresine "AT_FDCWD" özel değeri geçilebilir. Bu durumda fonksiyon "at" siz versiyondaki gibi çalışır. Diğer "at" li fonksiyonlarda olduğu gibi bu fonksiyonun da ikinci parametresindeki yol ifadesi mutlak ise birinci parametresindeki dizin hiç kullanılmamaktadır. "mknod" ve "mknodat" fonksiyonları prosesin "umask" değerini dikkate almaktadır. Bu fonksiyonlarla aygıt dosyası yaratabilmek için (diğer özel dosyalar için gerekmemektedir) prosesin uygun önceliğe sahip olması gerekmektedir. Aşağıdaki aygıt dosyası yaratan "mymknode" isimli bir fonksiyon yazılmıştır. * Örnek 1, Fonksiyonun genel kullanımı şöyledir: ./mymknod [-m ya da --mode ] [c ya da b] Tipik bir çalıştırma şöyle olabilir: $ sudo ./mymknode -m 666 mydriver c 25 0 Programın kodu ise aşağıdaki gibidir: /* mymknod.c */ #include #include #include #include #include #include #include bool ismode_correct(const char *mode); void exit_sys(const char *msg); int main(int argc, char *argv[]) /* ./mymknod [-m ] */ { int m_flag; int err_flag; char *m_arg; int result; int mode; dev_t dev; struct option options[] = { {"mode", required_argument, NULL, 'm'}, {0, 0, 0, 0} }; m_flag = err_flag = 0; opterr = 0; while ((result = getopt_long(argc, argv, "m:", options, NULL)) != -1) { switch (result) { case 'm': m_flag = 1; m_arg = optarg; break; case '?': if (optopt == 'm') fprintf(stderr, "option -m or --mode without argument!...\n"); else if (optopt != 0) fprintf(stderr, "invalid option: -%c\n", optopt); else fprintf(stderr, "invalid long option!...\n"); err_flag = 1; break; } } if (err_flag) exit(EXIT_FAILURE); if (argc - optind != 4) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if (m_flag) { if (!ismode_correct(m_arg)) { fprintf(stderr, "incorrect mode argument!..\n"); exit(EXIT_FAILURE); } sscanf(m_arg, "%o", &mode); } else mode = 0644; if (argv[optind + 1][1] != '\0') { fprintf(stderr, "invalid type argument: %s\n", argv[optind + 1]); exit(EXIT_FAILURE); } if (argv[optind + 1][0] == 'c') mode |= S_IFCHR; else if (argv[optind + 1][0] == 'b') mode |= S_IFBLK; else { fprintf(stderr, "invalid type argument: %s\n", argv[optind + 1]); exit(EXIT_FAILURE); } dev = makedev(atoi(argv[optind + 2]), atoi(argv[optind + 3])); umask(0); if (mknod(argv[optind + 0], mode, dev) == -1) exit_sys("mknod"); return 0; } bool ismode_correct(const char *mode) { if (strlen(mode) > 3) return false; while (*mode != '\0') { if (*mode < '0' || *mode > '7') return false; ++mode; } return true; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aslında yukarıda yazdığımız "mymknod" programının aynısı zaten "mknod" isimli kabuk komutu biçimide bulunmaktadır. Bu komutun genel biçimi şöyledir: sudo mknod [-m ya da --mode Tipik bir çalıştırma şöyle olabilir:: $ sudo mknod devfile c 25 0 Yukarıdaki komut uygulandığında oluşturulan dosya şöyle olacaktır: crw-rw-rw- 1 root root 25, 0 Mar 29 22:05 mydriver Bir "kernel" modülün karakter aygıt sürücüsü haline getirilebilmesi için öncelikle bir aygıt numarasıyla (majör ve minör numara ile) temsil edilip çekirdeğe kaydettirilmesi ("register" ettirlmesi) gerekmektedir. Bu işlem tipik olarak "register_chrdev_region" isimli fonksiyonla yapılır. Fonksiyonun prototpi şöyledir: #include int register_chrdev_region(dev_t from, unsigned count, const char *name) Fonksiyonun birinci parametresi aygıt sürücünün majör ve minör numaralarına ilişkin "dev_t" türünden değeri almaktadır. Bu parametre için argüman genellikle "MKDEV" makrosuyla oluşturulmaktadır. MKDEV makrosu majör ve minör numarayı argüman olarak alıp bundan "dev_t" türünden aygıt numarası oluşturmaktadır. Fonksiyonun ikinci parametresi ilk parametrede belirtilen minör numaradan itibaren kaç minör numaranın kaydettirileceğini belirtmektedir. Örneğin biz "majör=20", "minör=0" dan itibaren "5" minör numarayı kaydettirebiliriz. Fonksiyonun son parametresi "proc" ve "sys" dosya sistemlerindeki görüntülenecek olan aygıt sürücünün ismini belirtmektedir. "Kernel" modüllerin isimleri "kernel" modül dosyasından gelmektedir. Ancak karakter aygıt sürücülerinin isimlerini biz istediğimiz gibi veririz. Tabii her aygıt sürücü bir "kernel" modül biçiminde yazılmak zorundadır. "register_chrdev_region" fonksiyonu başarı durumunda "0" değerine, başarısızlık durumunda negatif "errno" değerine geri döner. "register_chrdev_region" fonksiyonu ile "register" ettirilen majör ve minör numaralar "unregister_chrdev_region" fonksiyonuyla geri bırakılmalıdır. Aksi halde modül "kernel" alanından "rmmod" komutuyla atılsa bile bu aygıt numaraları tahsis edilmiş bir biçimde kalmaya devam etmektedir. "unregister_chrdev_region" fonksiyonun prototipi şöyledir: #include void unregister_chrdev_region (dev_t from, unsigned count); Fonksiyonun birinci parametresi aygıt sürücünün "register" ettirilmiş olan majör ve minör numarasını, ikinci parametresi ise yine o noktadan başlayan kaç minör numaranın "unregister" ettirileceğidir. Bir aygıt sürücü "register_chrdev_region" fonksiyonuyla majör ve minör numarayı "register" ettirdiğinde artık "/proc/devices" dosyasında bu aygıt sürücü için bir satır yaratılmaktadır. Aygıt sürücü "unregister_chrdev_region" fonksiyonuyla yok edildiğinde "/proc/devices" dosyasındaki satır silinmektedir. * Örnek 1, Aşağıdaki örnekte "kernel" modülün "init" fonksiyonunda "register_chrdev_region" fonksiyonu ile "Majör:25", "Minor:1" olacak biçimde bir aygıt numarası "kernel" a kaydettirilmiştir. Bu kayıt modülün "exit" fonksiyonunda "unregister_chrdev_region" fonksiyonu ile silinmiştir. Kernel modülü aşağıdaki gibi derleyebilirsiniz: "$ make file=generic-char-driver" Modülü "install" ettikten sonra "/proc/modules" ve "/proc/devices" dosyalarına bakınız. "proc/devices" dosyasında aygıt sürücünün belirlediğiiz isimle kaydettirildiğini göreceksiniz. Programın kodu ise aşağıdaki gibidir: # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module intitialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!..\n"); return result; } return 0; } static void __exit generic_exit(void) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } module_init(generic_init); module_exit(generic_exit); Bir çekirdek modülü bir aygıt numarasıyla ilişkilendirdikten sonra artık ona gerçek anlamda bir karakter aygıt sürücü kimliği kazandırmak gerekmektedir. Bu işlem struct cdev isimli bir yapının için doldurularak sisteme eklenmesi (yerleştirilmesi) ile yapılır. Linux çekirdeği tüm çekirdek modülleri ve aygıt sürücüleri çeşitli veri yapılarıyla tutmaktadır. Aygıt sürücü yazan programcılar çekirdeğin bu organizasyonunu bilmek zorunda değillerdir. Ancak bazı işlemleri tam gerektiği gibi yapmak zorundadırlar. (Linux çekirdeğinin aygıt sürücü mimarisi oldukça karmaşıktır. Bu konu "Linux Kernel" kursunda ele alınmaktadır.) cdev yapısı aşağıdaki gibi bir yapıdır: struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; }; Bu türden bir yapı nesnesi programcı tarafından global olarak (statik ömürlü olarak) tanımlanabilir ya da alloc_cdev isimli çekirdek fonksiyonuyla çekirdeğin heap sistemi (slab allocator) kullanılarak dinamik bir biçimde tahsis edilebilir. (İşletim sistemlerinin çekirdeğinin ayrı bir heap sistemi vardır. Linux çekirdeğinde spesifik türden nesnelerin hızlı tahsis edilmesi için "slab allocator" denilen bir heap sistemi kullanılmaktadır.) Eğer bu yapı nesnesi programcı tarafından, -> Global bir biçimde tanımlanacaksa yapının elemanlarına ilkdeğer vermek için cdev_init fonksiyonu çağrılmalıdır. cdev_init fonksiyonunun parametrik yapısı şöyledir: #include void cdev_init(struct cdev *cdev, const struct file_operations *fops); Fonksiyonun birinci parametresi ilkdeğer verilecek global cdev nesnesinin adresini alır. İkinci parametre ise file_operations türünden bir yapı nesnesinin adresi almaktadır. file_operations isimli yapı birtakım fonksiyon adreslerinden oluşmaktadır. Yani yapının tüm elemanları birer fonksiyon göstericisidir. Bu yapı user moddaki program tarafından ilgili aygıt dosyası açılıp çeşitli işlemler yapıldığında çağrılacak fonksiyonların adreslerini tutmaktadır. Örneğin user moddaki program open, close, read, write yaptığında çağrılacak fonksiyonlarımızı burada belirtiriz. file_operations yapısı büyük bir yapıdır: struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iopoll)(struct kiocb *kiocb, bool spin); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); unsigned long mmap_supported_flags; int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); int (*fadvise)(struct file *, loff_t, loff_t, int); }; Bu yapının bazı elemanlarına atama yapabiliriz. Şöyleki: struct file_operations g_file_ops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; Yapının owner elemanına THIS_MODULE makrosunun atanması iyi bir tekniktir. Biz burada "aygıt sürücümüz open fonksiyonuyla açıldığında generic_open isimli fonksiyon çağrılsın", aygıt sürücümüz close fonksiyonu ile kapatıldığında "generic_release isimli fonksiyonumuz çağrılsın" demiş olmaktayız. -> cdev_alloc fonksiyonuyla dinamik bir biçimde tahsis edilecekse bu işlem cdev_init ile yapılmaz, çünkü zaten cdev_alloc bu işlemi de yapmaktadır. Fakat yine de programcının bu kez manuel olarak bu yapının bazı elemanlarına değer ataması gerekir. Burada kullanılan cdev_alloc fonksiyonunun parametrik yapısı aşağıdaki gibidir: #include struct cdev *cdev_alloc(void); Fonksiyon başarı durumunda cdev yapısının adresine, başarısızlık durumunda NULL adrese geri dönmektedir. Yukarıda da belirttiğimiz gibi cdev yapısı cdev_alloc ile tahsis edilmişse cdev_init yapılmasına gerek yoktur. Ancak bu durumda programcının manuel olarak yapının owner ve ops elemanlarına değer ataması gerekir. Örneğin: struct cdev *g_cdev; ... if ((gcdev = cdev_alloc()) == NULL) { printk(KERN_ERROR "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_file_ops; Bu iki yoldan biriyle oluşturulmuş olan cdev yapısının en sonunda cdev_add isimli fonksiyonla çekirdek veri yapılarına yerleştirilmeleri gerekir. cdev_add fonksiyonunun prototipi aşağıdaki gibidir: #include int cdev_add(struct cdev *devp, dev_t dev, unsigned count); Fonksiyonun birinci parametresi cdev türünden yapı nesnesinin adresini almaktadır. İkinci parametre aygıt sürücünün majör ve minör numarasını belirtmektedir. Üçüncü parametresi ise ilgili minör numaradan itibaren kaç minör numaranın kullanılacağı belirtir. Fonksiyon başarı durumunda sıfır değerine, başarısızlık durumunda negatif errno değerine geri döner. Örneğin: if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { ... return result; } Tabii aygıt sürücü boşaltılırken bu yerleştirme işlemi cdev_del fonksiyonuyla geri alınmalıdır. cdev_del fonksiyonu, struct cdev yapısı cdev_alloc ile tahsis edilmişse aynı zamanda onu free hale de getirmektedir. cdev_del fonksiyonunun prototipi aşağıdaki gibidir: #include void cdev_del(struct cdev *devp); Fonksiyon parametre olarak cdev yapısının adresini almaktadır. Özetle çekirdek modülümüzün tam bir karakter aygıt sürücüsü haline getirilmesi için şunlar yapılmalıdır: -> struct cdev isimli bir yapı türünden nesne global olarak (statik ömürlü olarak) tanımlanmalı ya da cdev_alloc fonksiyonu ile çekirdeğin heap sistemi içerisinde tahsis edilmelidir. Eğer bu nesne global olarak tanımlanacaksa nesneye cdev_init fonksiyonu ile ilkdeğerleri verilmelidir. Eğer nesne cdev_alloc fonksiyonu ile çekirdeğin heap alanında tahsis edilecekse bu durumda ilkdeğer verme işlemi bu fonksiyon tarafından yapılmaktadır. Ancak programcının yine yapının bazı elemanlarını manuel olarak doldurması gerekmektedir. -> Oluşturulan bu struct cdev nesnesi cdev_add çekirdek fonksiyonu ile çekirdeğe eklenmelidir. -> Çekirdek modülü çekirdek alanından atılırken modülün exit fonksiyonunda cdev_add işleminin geri alınması için cdev_del fonksiyonunun çağrılması gerekmektedir. Buradaki önemli bir nokta şudur: -> cdev_add fonksiyonu cdev nesnesinin içini çekirdekteki uygun veri yapısına kopyalamamaktadır. Bizzat bu nesnenin adresini kullanmaktadır. Yani çekirdek modülü var olduğu sürece bu cdev nesnesinin de yaşıyor olması gerekmektedir. Bu da cdev nesnesinin ve file_operations nesnesinin global biçimde (ya da statik ömürlü biçimde) tanımlanmasını gerektirmektedir. Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, Aşağıda bu işlemlerin yapıldığı örnek bir karakter aygıt sürücüsü verilmiştir. Bu aygıt sürücü majör=25, minör=0 aygıtını kullanmaktadır. Dolayısıyla aşağıdaki programın testi için şöyle bir aygıt dosyasının yaratılmış olması gerekir. Yaratımı sudo mknod mydriver -m=666 c 25 0 gibi yapabilirsiniz. Bu aygıt sürücü insmod ile yüklendiğinde artık biz user mode'da "mydriver" dosyasını açıp kapattığımızda file_operations yapısına yerleştirdiğimiz generic_open ve generic_release fonksiyonları çağrılacaktır. # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } module_init(generic_init); module_exit(generic_exit); /* app.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("mydriver", O_RDONLY)) == -1) exit_sys("open"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Yukarıdaki programda biz cdev nesnesini global olarak tanımladık. Aşağıda nesnenin cdev_alloc fonksiyonu ile dinamik biçimde tahsis edilmesine bir örnek verilmiştir. # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot alloc cdev!...\n"); return result; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } module_init(generic_init); module_exit(generic_exit); /* app.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("mydriver", O_RDONLY)) == -1) exit_sys("open"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Çekirdek kodları ya da aygıt sürücü kodları çoğu zaman çekirdek alanı ile user alanı arasında veri transfer yapmak isterler. Örneğin sys_read sistem fonksiyonu çekirdek alanında elde ettiği bilgileri user alanındaki programcının verdiği adrese kopyalar. Benzer biçimde sys_write fonksiyonu da bunun tersini yapmaktadır. Çekirdek alanı ile user alanı arasında memcpy fonksiyonu ile transfer yapmaya çalışmak uygun değildir. Bunun birkaç nedeni vardır. Bu tür transferlerde kernel mod programcılarının user alanındaki adresin geçerliliğini kontrol etmesi gerekir. Aksi takdirde kernel modda geçersiz bir alana kopyalama yapmak sistemin çökmesine yol açabilmektedir. Ayrıca user alanına ilişkin prosesin sayfa tablosunun bazı bölümleri o anda bellekte olmayabilir (yani swap out yapılmış olabilir). Böyle bir durumda işleme devam etmek çekirdek tasarımı açısından sorun olmaktadır. Eğer böyle bir durum varsa çekirdek kodlarının önce sayfa tablosunu RAM'e geri yükleyip işlemine devam etmesi gerekmektedir. İşte yukarıda açıklanan bazı nedenlerden dolayı çekirdek alanı ile user alanı arasında kopyalama işlemi için özel çekirdek fonksiyonları kullanılmaktadır. Yani biz user mod programları ile kernel modülümüz arasında transferleri özel bazı kernel fonksiyonlarıyla yapmalıyız. Bu amaçla kullanılan çeşitli kernel fonksiyonları ve makroları bulunmaktadır. En temel iki fonksiyon copy_to_user ve copy_from_user fonksiyonlarıdır. Bu fonksiyonların prototipleri şöyledir: #include unsigned long copy_to_user(void *to, const void *from, unsigned len); unsigned long copy_from_user(void *to, const void *from, unsigned len); Fonskiyonların birinci parametreleri kopyalamanın yapılacağı hedef adresi belirtmektedir. Yani copy_to_user için birinci parametre user alanındaki adres, copy_from_user için birinci parametre kernel alanındaki adrestir. İkinci parametre kaynak adresi belirtmektedir. Bu kaynak adres copy_to_user için kernel alanındaki adres, copy_from_user için user alanındaki adrestir. Son parametre transfer edilecek byte sayısını belirtmektedir. Fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda transfer edilemeyen byte sayısına geri dönerler. Kernel mod programcılarının bu fonksiyonlar başarısızken bunu çağıran foksiyonlarını -EFAULT (Bad address) ile geri döndürmesi uygun olur. (Örneğin sys_read ve sys_write fonksiyonlarına biz geçersiz bir user mode adresi verirsek bu sistem fonksiyonları da -EFAULT değeri ile geri dönmektedir. Bu hata kodunun yazısal karşılığı "Bad address" biçimindedir.) Bazen user alanındaki adresin zaten geçerliliği sınanmıştır. Bu durumda yeniden geçerlilik sınaması yapmadan yukarıdaki işlemleri yapan __copy_to_user ve __copy_from_user fonksiyonları kullanılabilir. Bu fonksiyonların parametrik yapıları aynıdır. Bu fonksiyonların yukarıdakilerden tek farkı adres geçerliliğine ilişkin sınama yapmamalarıdır: #include unsigned long __copy_to_user(void *to, const void *from, unsigned len); unsigned long __copy_from_user(void *to, const void *from, unsigned len); Bazı durumlarda programcı 1 byte, 2 byte, 4 byte, 8 byte'lık verileri transfer etmek isteyebilir. Bu küçük miktardaki verilerin transfer edilmesi için daha hızlı çalışan özel iki makro vardır: put_user ve get_user. Bu makroların parametrik yapısı şöyledir: #include put_user(x, ptr); get_user(x, ptr); Burada x aktarılacak nesneyi belirtir. (Bu nesnenin adresini programcı almaz, makro içinde bu işlem yapılmaktadır.) ptr ise transfer adresini belirtmektedir. Aktarım ikinci parametrede belirtilen adresin türünün uzunluğu kadar yapılmaktadır. Başka bir deyişle biz makroya hangi türden nesne verirsek zaten makro o uzunlukta tranfer yapmaktadır. Makrolar başarı durumunda 0, başarısızlık durumunda negatif hata koduna geri dönmektedir. Bu makroların da geçerlilik kontrolü yapmayan __put_user ve __get_user isimli versiyonları vardır: #include __put_user(x, ptr); __get_user(x, ptr); Örneğin biz çekirdek modülümüzdeki 4 byte'lık int bir x nesnesinin içerisindeki bilgiyi puser ile temsil edilen user adresine kopyalamak isteyelim. Bu işlemi şöyle yaparız: int x; int *puser; ... put_user(x, puser); Nihayet user alanındaki adresin geçerliliği de access_ok isimli makroyla sorgulanabilmektedir. Makro şöyledir: #include access_ok(type, addr, size); Buradaki type sınanacak geçerliliğin türünü anlatmaktadır. Okuma geçerliliği için bu parametre VERIFY_READ, yazma geçerliliği için VERIFY_WRITE ve hem okuma hem de yazma geçerliliği için VERIFY_READ|VERIFY_WRITE biçiminde girilmelidir. İkinci parametre geçerliliği sınanacak adresi ve üçüncü parametre de o adresten başlayan alanın uzunluğunu belirtmektedir. Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri dönmektedir. Örneğin biz user alanında puser adresiyle başlayan 100 byte'lık alanın yazma bakımından geçerli bir alan olup olmadığını sınamak isteyelim. Bu sınamayı çekirdek modülümüzde şöyle yapabiliriz: if (access_ok(VERIFY_WRITE, puser, 100)) { // adres geçerli ... } Biz şimdiye kadar aygıt dosyası open ile açıldığında ve close ile kapatıldığında aygıt sürücümüz içerisindeki fonksiyonlarımızın çağrılmasını sağladık. Şimdi de aygıt dosyası üzerinde read ve write fonksiyonları uygulandığında aygıt sürücümüzdeki ilgili fonksiyonların çağrılması üzerinde duracağız. Aygıt sürücümüz için read ve write fonksiyonları aşağıdaki parametrik yapıya uygun olacak biçiminde file_operations yapısına yerleştirilmelidir: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct file_operations g_file_ops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release, .read = generic_read, .write = generic_write, }; Artık aygıt dosyası üzerinde read POSIX fonksiyonu çağrıldığında aygıt sürücümüzdeki generic_read fonksiyonu write POSIX fonksiyonu çağrıldığında aygıt sürücümüzdeki generic_write POSIX fonksiyonu çağrılacaktır. read ve write fonksiyonlarının birinci parametresi açılmış dosyaya ilişkin struct file nesnesinin adresini belirtir. Anımsanacağı gibi bir dosya açıldığında kernel sys_open fonksiyonunda bir dosya nesnesi (struct file) tahsis edip bu dosya nesnesinin adresini dosya betimleyici tablosunda bir slota yerleştirip onun indeksini dosya betimleyicisi olarak geri döndürüyordu. İşte bu read ve write fonksiyonlarının birinci parametreleri bu dosya nesnesinin adresini belirtmektedir. Daha önceden de belirttiğimiz gibi file yapısı içerisinde dosya göstericisinin konumu, dosyanın erişim hakları, referans sayacının değeri, dosyanın açış modu ve açış bayrakları ve başka birtakım bilgiler bulunmaktadır. Linux kernel 2.4.30'daki file yapısı şöyledir: struct file { struct list_head f_list; struct dentry *f_dentry; struct vfsmount *f_vfsmnt; struct file_operations *f_op; atomic_t f_count; unsigned int f_flags; mode_t f_mode; loff_t f_pos; unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; struct fown_struct f_owner; unsigned int f_uid, f_gid; int f_error; size_t f_maxcount; unsigned long f_version; // needed for tty driver, and maybe others void *private_data; // preallocated helper kiobuf to speedup O_DIRECT struct kiobuf *f_iobuf; long f_iobuf_lock; }; Biz burada bilerek sadelik yüzünden eski bir çekirdeğin file yapısını verdik. Yeni çekirdeklerde buna birkaç eleman daha eklenmiştir. Ancak temel elemanlar yine aynıdır. read ve write fonksiyonlarının ikinci parametresi user alanındaki transfer adresini belirtir. Üçüncü parametreler okunacak ya da yazılacak byte miktarını belirtmektedir. Son parametre dosya göstericisinin konumunu belirtir. Ancak bu parametre file yapısı içerisindeki f_pos elemanının adresi değildir. Çekirdek tarafından read ve write fonksiyonları çağrılmadan önce file yapısı içerisindeki f_pos elemanının değeri başka bir nesneye atanıp o nesnenin adresi read ve write fonksiyonlarına geçirilmektedir. read ve write fonksiyonları sonlandığında çekirdek adresini geçirdiği nesnenin değerini file yapısının f_pos elemanına kendisi yerleştirmektedir. Fonksiyon başarı durumunda transfer edilen byte sayısına, başarısızlık durumunda negatif errno değerine geri dönmelidir. Biz aygıt sürücümüz için read ve write fonksiyonlarını yazarken transfer edilen byte miktarı kadar dosya göstericisini kendimizin ilerletmesi gerekir. Bu işlem fonksiyonların son parametresi olan off göstericisinin gösterdiği yerin güncellenmesi ile yapılır. Örneğin n byte transfer edilmiş olsun. Bu durumda dosya göstericisinin konumu aşağıdaki gibi güncellenebilir: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { ... *off += n; return n; } Aygıt sürücüsünüzün read ve write fonksiyonlarında dosya göstericisini konumlandırmak için file yapısının f_pos elemanını güncellemeyiniz. Dosya göstericisinin konumlandırılması her zaman read ve write fonksiyonlarının son parametresi yoluyla yapılmaktadır. Çekirdeğin dosya göstericisini nasıl güncellediğine ilişkin aşağıdaki gibi bir temsili kod örneği verebiliriz: loff_t off; ... off = filp->f_pos; read(filp, buf, size, &off); filp_f_pos = off; Şimdi de bu konuya ilişkin örnekleri inceleyelim: * Örnek 1, Aşağıdaki örnekte aygıt sürücü için read fonksiyonu yazılmıştır. Bu fonksiyon aslında g_buf isimli dizinin içini dosya gibi vermektedir. Ayrıca aşağıdaki aygıt sürücüye read ve write fonksiyonları içi boş bir biçimde yerleştirilmiştir. User mode'dan read ve write yapıldığında aygıt sürücümüzün içerisindeki bu fonksiyonların çalıştığını gözlemleyiniz. # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "generic_read called...\n"); return size; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "generic_write called...\n"); return size; } module_init(generic_init); module_exit(generic_exit); /* app.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[100]; if ((fd = open("mydriver", O_RDWR)) == -1) exit_sys("open"); read(fd, buf, 100); write(fd, buf, 100); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi de aygıt sürücümüzün read fonksiyonunun gerçekten bir dosyadan okuma yapıyormuş gibi davranmasını sağlayalım. Bunun için dosyamızı temsil eden aşağıdaki gibi global bir dizi kullanacağız: static char g_buf[] = "01234567890ABCDEFGH"; Buradaki diziyi sanki bir dosya gibi ele alacağız. Aygıt sürücümüzün read fonksiyonu aşağıdaki gibi olacaktır: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t slen; slen = strlen(g_buf); esize = *off + size > slen ? slen - *off : size; if (copy_to_user(buf, g_buf + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } Burada önce dosya göstericisinin gösterdiği yerden itibaren "size" kadar byte'ın gerçekten dizi içerisinde olup olmadığına bakılmıştır. Eğer "*off + size" değeri bu dizinin uzunluğundan fazlaysa "size" kadar değer değil, "slen - *off" kadar değer okunmuştur. Aygıt sürücülerin read ve write fonksiyonlarında dosya göstericisinin ilerletilmesi programcının sorumluluğundadır. Bu nedenle okuma işlemi yapıldığında dosya göstericisinin konumu aşağıdaki gibi artırılmıştır: *off += size; read fonksiyonunun okunabilen byte sayısına geri döndürüldüğüne dikkat ediniz. copy_to_user fonksiyonu ile tüm byte'lar user alanına kopyalanamamışsa fonksiyon -EFAULT değeri ile geri döndürülmüştür. * Örnek 1, # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static char g_buf[] = "01234567890ABCDEFGH"; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t slen; slen = strlen(g_buf); esize = *off + size > slen ? slen - *off : size; if (copy_to_user(buf, g_buf + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "generic_write called...\n"); return size; } module_init(generic_init); module_exit(generic_exit); /* app.c */ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[1024]; ssize_t result; if ((fd = open("mydriver", O_RDONLY)) == -1) exit_sys("open"); if ((result = read(fd, buf, 3)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: \"%s\"\n", (intmax_t)result, buf); if ((result = read(fd, buf, 5)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: \"%s\"\n", (intmax_t)result, buf); if ((result = read(fd, buf, 30)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: \"%s\"\n", (intmax_t)result, buf); if ((result = read(fd, buf, 30)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: \"%s\"\n", (intmax_t)result, buf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aygıt sürücü için write fonksiyonu da tamamen read fonksiyonuna benzer biçimde yazılmaktadır. write fonksiyonu içerisinde biz user moddaki bilgiyi copy_from_user ya da get_user fonksiyonlarıyla alırız. Yine write fonksiyonu da bir sorun çıktığında -EFAULT değeri ile, başarılı sonlanmada ise yazılan (kernel alanına yazılan) byte miktarı ile geri dönmelidir. Aşağıdaki örnekte aygıt sürücü bellekte oluşturulmuş bir dosya gibi davranmaktadır. Aygıt sürücünün taklit ettiği dosya en fazla 4096 byte olabilmektedir: #define FILE_MEMORY_MAX_SIZE 4096 ... static char g_fmem[FILE_MEMORY_MAX_SIZE]; Ancak buradaki FILE_MEMORY_MAX_SIZE bellek dosyasının maksimum uzunluğunu belirtmektedir. Bellek dosyasının gerçek uzunluğu g_fmem_size nesnesinde tutulmaktadır. Aygıt sürücünün write fonksiyonu aşağıdaki gibi yazılmıştır: static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > FILE_MEMORY_MAX_SIZE ? FILE_MEMORY_MAX_SIZE - *off : size; if (copy_from_user(g_fmem + *off, buf, esize) != 0) return -EFAULT; *off += esize; if (*off > g_fmem_size) g_fmem_size = *off; return esize; } Burada yine dosya göstericisinin gösterdiği yerden itibaren yazılmak istenen byte sayısı FILE_MEMORY_MAX_SIZE değerini aşıyorsa geri kalan miktar kadar yazma yapılmıştır. Burada yine dosya göstericisinin ilerletildiğine dikkat ediniz. Dosya göstericisinin ilerletilmesi her zaman programcının sorumluluğundadır. Aygıt sürücümüzün read fonksiyonu da şöyledir: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > g_fmem_size ? g_fmem_size - *off : size; if (copy_to_user(buf, g_fmem + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } Burada da dosya göstericisinin gösterdiği yerden itibaren okunmak istenen byte sayısının g_fmem_size değerinden büyük olup olmadığına bakılmıştır. Yine dosya göstericisi fonksiyon tarafından güncellenmiştir. Buradaki aygıt sürücüyü test etmek için "app-write.c" ve "app-read.c" isimli iki ayrı programdan faydalanılmıştır. "app-write.c" bellek dosyasına yazma yapmakta, "app-read.c" ise bellek dosyasından okuma yapmaktadır. Bu örnekte bellek dosyasına yazılanların aygıt sürücü çekirdekte bulunduğu sürece kalıcı olduğuna dikkat ediniz. * Örnek 1, # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 #define FILE_MEMORY_MAX_SIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static char g_fmem[FILE_MEMORY_MAX_SIZE]; static size_t g_fmem_size; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > g_fmem_size ? g_fmem_size - *off : size; if (copy_to_user(buf, g_fmem + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > FILE_MEMORY_MAX_SIZE ? FILE_MEMORY_MAX_SIZE - *off : size; if (copy_from_user(g_fmem + *off, buf, esize) != 0) return -EFAULT; *off += esize; if (*off > g_fmem_size) g_fmem_size = *off; return esize; } module_init(generic_init); module_exit(generic_exit); /* app-write.c */ #include #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; ssize_t result; char buf[5000]; if ((fd = open("mydriver", O_WRONLY)) == -1) exit_sys("open"); if ((result = write(fd, "ankara", 6)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if ((result = write(fd, "izmir", 5)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); memset(buf, 'x', 5000); if ((result = write(fd, buf, 5000)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* app-read.c */ #include #include #include #include #include #define BUFFER_SIZE 5 void exit_sys(const char *msg); int main(void) { int fd; ssize_t result; char buf[BUFFER_SIZE + 1]; if ((fd = open("mydriver", O_RDONLY)) == -1) exit_sys("open"); while ((result = read(fd, buf, BUFFER_SIZE)) > 0) { buf[result] = '\0'; printf("%s", buf); fflush(stdout); } putchar('\n'); if (result == -1) exit_sys("read"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } User moddan aygıt dosyası betimleyicisi ile lseek işlemi yapıldığında aygıt sürücünün file_operations yapısı içerisine yerleştirilen llseek fonksiyonu çağrılmaktadır. Fonksiyonun parametrik yapısı şöyledir: static loff_t generic_llseek(struct file *filp, loff_t off, int whence); Fonksiyonun birinci parametresi dosya nesnesini, ikinci parametresi konumlandırılmak istenen offset'i, üçüncü parametresi ise konumlandırmanın nereye göre yapılacağını belirtmektedir. Bu fonksiyonu gerçekleştirirken programcı file yapısı içerisindeki f_pos elemanını güncellemelidir. Tipik olarak programcı whence parametresini switch içerisine alır. Hedeflenen offset'i hesaplar ve en sonunda file yapısının f_pos elemanına bu hedeflenen offset'i yerleştirir. Hedeflenen offset uygun değilse fonksiyon tipik olarak -EINVAL değeriyle geri döndürülür. Eğer konumlandırma offset'i başarılı ise fonksiyon dosya göstericisinin yeni değerine geri dönmelidir. Aşağıda daha önce yapmış olduğumuz bellek dosyası örneğine llseek fonksiyonu da eklenmiştir. Fonksiyon aşağıdaki gibi yazılmıştır: static loff_t generic_llseek(struct file *filp, loff_t off, int whence) { loff_t newpos; switch (whence) { case 0: newpos = off; break; case 1: newpos = filp->f_pos + off; break; case 2: newpos = g_fmem_size + off; break; default: return -EINVAL; } if (newpos < 0 || newpos > g_fmem_size) return -EINVAL; filp->f_pos = newpos; return newpos; } Burada önce whence parametresine bakılarak dosya göstericisinin konumlandırılacağı offset belirlenmiştir. Sonra dosya nesnesinin f_pos elemanı güncellenmiştir. * Örnek 1, # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 #define FILE_MEMORY_MAX_SIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static loff_t generic_llseek(struct file *filp, loff_t off, int whence); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .llseek = generic_llseek, .release = generic_release }; static char g_fmem[FILE_MEMORY_MAX_SIZE]; static size_t g_fmem_size; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > g_fmem_size ? g_fmem_size - *off : size; if (copy_to_user(buf, g_fmem + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > FILE_MEMORY_MAX_SIZE ? FILE_MEMORY_MAX_SIZE - *off : size; if (copy_from_user(g_fmem + *off, buf, esize) != 0) return -EFAULT; *off += esize; if (*off > g_fmem_size) g_fmem_size = *off; return esize; } static loff_t generic_llseek(struct file *filp, loff_t off, int whence) { loff_t newpos; switch (whence) { case 0: newpos = off; break; case 1: newpos = filp->f_pos + off; break; case 2: newpos = g_fmem_size + off; break; default: return -EINVAL; } if (newpos < 0 || newpos > g_fmem_size) return -EINVAL; filp->f_pos = newpos; return newpos; } module_init(generic_init); module_exit(generic_exit); /* app.c */ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; ssize_t result; char buf[4096]; if ((fd = open("mydriver", O_RDWR)) == -1) exit_sys("open"); if ((result = write(fd, "ankara", 6)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if ((result = write(fd, "izmir", 5)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if (lseek(fd, 0, 0) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); if (lseek(fd, -2, 1) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; if (lseek(fd, -2, 2) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Biz şimdiye kadarki örneklerimizde aygıt sürücümüzün majör ve minör numarasını baştan belirledik. Bunun en önemli sakıncası zaten o numaralı bir aygıt sürücünün yüklü olarak bulunuyor olmasıdır. Bu durumda aygıt sürücümüz yüklenemeyecektir. Aslında daha doğru bir strateji tersten gitmektir. Yani önce aygıt sürücümüz içerisinde biz boş bir aygıt numarasını bulup onu kullanabiliriz. Tabii sonra user moddan bu aygıt numarasına ilişkin bir aygıt dosyasını da yaratmamız gerekir. Boş bir aygıt numarasını bize veren alloc_chrdev_region isimli bir kernel fonksiyonu vardır. Fonksiyonun parametrik yapısı şöyledir: int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); Fonksiyonun birinci parametresi boş aygıt numarasının yerleştirileceği dev_t nesnesinin adresini alır. İkinci ve üçüncü parametreler başlangıç minör numarası ve onun sayısını belirtir. Son parametre ise aygıt sürücüsünün "/proc/devices" dosyasında ve "/sys/dev" dizininde görüntülenecek olan ismini belirtmektedir. alloc_chrdev_region fonksiyonu zaten register_chrdev_region fonksiyonunun yaptığını da yapmaktadır. Dolayısıyla bu iki fonksiyondan yalnızca biri kullanılmalıdır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda negatif errno değerine geri döner. Örneğin: dev_t g_dev; ... if ((result = alloc_chrdev_region(&g_dev, 0, 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } Aygıt sürücümüzde alloc_chrdev_region fonksiyonu ile boş bir majör numara numaranın bulunup aygıt sürücümüzün register ettirildiğini düşünelim. Pekiyi biz bu numarayı nasıl bilip bu numaraya uygun aygıt dosyası yaratacağız? İşte bunun için genellikle izlenen yöntem "/proc/devices" dosyasına bakıp oradan majör numarayı alıp aygıt dosyasını yaratmaktır. Tabii bu manuel olarak yapılabilir ancak bir shell script ile otomatize de edilebilir. Aşağıdaki "load" isimli script bu işlemi yapmaktadır: #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module Artık biz bu load script'i ile aygıt sürücümüzü yükleyip aygıt dosyamızı yaratacağız. Bu script'i load ismiyle yazıp aşağıdaki gibi x hakkı vermelisiniz: chmod +x load Çalıştırmayı komut satırı argümanı vererek aşağıdaki gibi yapmalısınız: $ sudo ./load generic-char-driver Burada load script'i çalıştırıldığında hem aygıt sürücü çekirdek alanına yüklenmekte hem de yüklenen aygıt sürücünün majör numarasıyla minör numara 1 olacak biçimde "generic-char-driver" isimli aygıt dosyası yaratılmaktadır. Aygıt sürücünün çekirdek alanından atılması manuel bir biçimde "rmmod" komutuyla yapılabilir. Tabii aynı zamanda bu aygıt sürücü için yaratılan aygıt dosyasının da silinmesi gerekir. Yukarıdaki script'te aygıt dosyası zaten varsa aynı zamanda o silinmektedir. Tabii aygıt dosyasını çekirdek alanından atarak silen ayrı bir "unload" isimli script'i de aşağıdaki gibi yazabiliriz: #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module Tabii yine bu script dosyasının da "x" hakkına sahip olması gerekmektedir: $ chmod +x unload unload Script'ini aşağıdaki gibi çalıştırabilirsiniz: $ sudo ./unload generic-char-driver Aşağıdaki örnekte alloc_chrdev_region fonksiyonuyla hem boş aygıt numarası elde edilip hem de bu aygıt numarası register ettirilmiştir. Yükleme işlemi yukarıdaki "load" scrip'i ile yapılmalıdır. Kernel modülün boşaltılması işlemi manuel olarak ya da "unload" script'i ile yapılabilir. Örneğin: $ sudo ./load generic-char-driver ... $ sudo ./unload generic-char-driver Şimdi de bu konuya ilişkin örneklere bakalım: * Örnek 1, # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #include #define FILE_MEMORY_MAX_SIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static loff_t generic_llseek(struct file *filp, loff_t off, int whence); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .llseek = generic_llseek, .release = generic_release }; static char g_fmem[FILE_MEMORY_MAX_SIZE]; static size_t g_fmem_size; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "generic-char-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > g_fmem_size ? g_fmem_size - *off : size; if (copy_to_user(buf, g_fmem + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > FILE_MEMORY_MAX_SIZE ? FILE_MEMORY_MAX_SIZE - *off : size; if (copy_from_user(g_fmem + *off, buf, esize) != 0) return -EFAULT; *off += esize; if (*off > g_fmem_size) g_fmem_size = *off; return esize; } static loff_t generic_llseek(struct file *filp, loff_t off, int whence) { loff_t newpos; switch (whence) { case 0: newpos = off; break; case 1: newpos = filp->f_pos + off; break; case 2: newpos = g_fmem_size + off; break; default: return -EINVAL; } if (newpos < 0 || newpos > g_fmem_size) return -EINVAL; filp->f_pos = newpos; return newpos; } module_init(generic_init); module_exit(generic_exit); /* load */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* app.c */ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; ssize_t result; char buf[4096]; if ((fd = open("generic-char-driver", O_RDWR)) == -1) exit_sys("open"); if ((result = write(fd, "ankara", 6)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if ((result = write(fd, "izmir", 5)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if (lseek(fd, 0, 0) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); if (lseek(fd, -2, 1) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; if (lseek(fd, -2, 2) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aşağıda gelinen noktaya kadar görülmüş olan konular kullanılarak yazılmış basit bir boru örneği verilmiştir. Bu boru örneğinde bir proses boruyu yazma modunda açar ve prosesin write fonksiyonuyla yazdıkları aygıt sürücü içerisindeki bir FIFO kuyruk sistemine yazılır. Diğer proses de read fonksiyonuyla bu FIFO kuyruk sisteminden kuyruktan okuma yapar. Bu gerçekleştirim orijinal "isimli boru (named pipe)" gerçekleştirimine benziyorsa da ondan farklıdır. Burada yapılan gerçekleştirimin önemli noktaları şunlardır: -> write fonksiyonu borudaki boş alan miktarından daha fazla bilgi yazılmaya çalışılırsa blokeye yol açmaz, yazabildiği kadar byte'ı yazar ve boruya yazabildiği byte sayısına geri döner. Halbuki anımsanacağı gibi orijinal borularda talep edilen miktarın tamamı yazılana kadar write bloke oluşturmaktadır. -> read fonksiyonu boruda hiçbir bilgi yoksa blokeye yol açmaz 0 ile geri döner. Ancak read eğer boruda en az bir byte varsa okuyabildiği kadar byte'ı okuyup okuyabildiği byte sayısına geri döner. -> Aygıt sürücünün read/write fonksiyonlarında hiçbir senkronizasyon uygulanmamıştır. Dolayısıyla eşzamanlı işlemlerde tanımsız davranışlar ortaya çıkabilir. Örneğin iki farklı proses bu boruya yazma yaparsa senkronizasyondan kaynaklanan sorunlar oluşabilir. -> İki proses de boruyu kapatsa bile boru silinmemektedir. Halbuki orijinal borularda prosesler isimli boruyu kapatınca boru içerisindeki tüm bilgiler silinmektedir. -> Bu uygulamada sistem genelinde tek bir boru yaratılmaktadır. Yani bizim boru aygıt sürücümüz tek bir boru üzerinde işlemler yapmaktadır. Halbuki orijinal isimli borularda programcılar birbirinden bağımsız istedikleri kadar çok isimli boru yaratabilmektedir. Aygıt sürücünüzü önce build edip sonra aşağıdaki gibi yüklemelisiniz: $ make file=pipe-dirver $ sudo ./load pipe-driver Buradaki boruyu aygıt sürücüsünü test etmek için "prog1" ve "prog2" isimli iki program yazılmıştır. "prog1" klavyeden alınan yazıları boruya yazmakta, "prog2" ise klavyeden alınan uzunlukta byte'ı borudan okumaktadır. Boruyu test temek için boru uzunluğunu azaltabilirsiniz. Biz örneğimizde boru uzunluğunu 4096 aldık. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 20 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; esize = MIN(g_count, size); if (g_tail < g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail > g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aygıt sürücülerdeki kodlar user mode'dan farklı prosesler tarafından kullanılıyor olabilir. Ayrıca ileride göreceğimiz gibi aygıt sürücüler donanım kesmelerini de kullanıyor olabilir. Dolayısıyla aygıt sürücü kodları eşzamanlı (concurrent) erişime uygun biçimde yazılmalıdır. User mode'daki bir proses aygıt sürücü içerisindeki bir kaynağı kullanıyorken user mode'daki diğer prosesin o kaynağın bozulmaması için diğerini beklemesi gerekebilmektedir. Kernel mode'da aygıt sürücü kodları daha önce user mode'da gördüğümüz senkronizasyon nesnelerini kullanamaz. Çünkü daha önce gördüğümüz senkronizasyon nesneleri user mode'dan kullanılsın diye oluşturulmuştur. Çekirdeğin içerisinde kernel mode'dan kullanılabilecek ayrı senkronizasyon nesneleri bulunmaktadır. Bu bölümde aygıt sürücülerin kernel mode'da kullanabileceği senkronizasyon nesnelerini göreceğiz. Kernel mode için user mode'dakine benzer senkronizasyon nesneleri kullanılmaktadır. Bunların genel çalışma biçimi user mode'dakilere benzemektedir. Kernel mutex mekanizması 2.6 çekirdeğinde çekirdeğe eklenmiştir. Bundan önce mutex işlemleri binary semaphore'larla yapılıyordu. Bu mutex mekanizması user mode'daki mutex mekanizmasına çok benzemektedir. Yine kernel mode'daki mutex'in thread temelinde sahipliği vardır. Bu mutex mekanizması yine thread'i bloke edip sleep kuyruklarında bekletebilmektedir. Kernel mode mutex mekanizmasının tipik gerçekleştirimi şöyledir: -> lock işlemi sırasında işlemcinin maliyetsiz compare/set (compare/exchange) komutlarıyla mutex'in kilitli olup olmadığına bakılır. -> diğer bir işlemcideki proses mutex'i kilitlemişse boşuna bloke olmamak için yine compare/set komutlarıyla biraz spin işlemi yapılır. -> spin işleminden sonuç elde edilemezse bloke oluşturulur. Kernel mode'daki mutex'ler tipik olarak şöyle kullanılmaktadır: -> Mutex nesnesi "mutex" isimli bir yapıyla temsil edilmektedir. Sistem programcısı bu yapı türünden bir nesne yaratır ve ona ilk değerini verir. DEFINE_MUTEX(name) makrosu hem mutex türünden nesneyi tanımlamakta hem de ona ilk değerini vermektedir. Örneğin: #include DEFINE_MUTEX(g_mutex); Burada biz hem g_mutex isminde bir global nesne tanımlamış olduk hem de ona ilk değer vermiş olduk. Aynı işlem önce nesneyi tanımlayıp sonra mutex_init fonksiyonuyla da yapılabilir. Örneğin: struct mutex g_mutex; ... mutex_init(&g_mutex); DEFINE_MUTEX makrosuna nesnenin adresinin verilmediğine dikkat ediniz. Bu makro ve mutex_init fonksiyonunun prototipleri başlık dosyasında bulunmaktadır. Her ne kadar mutex_init bir fonksiyon görünümündeyse de aslında çekirdek kodlarında hem bir makro olarak hem de bir fonksiyon olarak bulunmaktadır. Mevcut Linux çekirdeklerinde fonksiyonların makro gerçekleştirimleri aşağıdaki gibidir: #define DEFINE_MUTEX(mutexname) \ struct mutex mutexname = __MUTEX_INITIALIZER(mutexname) #define mutex_init(mutex) \ do { \ static struct lock_class_key __key; \ \ __mutex_init((mutex), #mutex, &__key); \ } while (0) -> Mutex'i kilitlemek için mutex_lock fonksiyonu kullanılır: void mutex_lock(struct mutex *lock); Mutex'in kilitli olup olmadığı ise mutex_trylock fonksiyonuyla kontrol edilebilir: #include int mutex_trylock(struct mutex *lock); Eğer mutex kilitliyse fonksiyon bloke olmadan 0 değeriyle geri döner. Eğer mutex kilitli değilse mutex kilitlenir ve fonksiyon 1 değeri ile geri döner. Mutex nesnesi mutex_lock ile kilitlenmek istendiğinde bloke oluşursa bu blokeden sinyal yoluyla çıkılamamaktadır. Örneğin mutex_lock ile kernel mode'da biz mutex kilidini alamadığımızdan dolayı bloke oluştuğunu düşünelim. Bu durumda ilgili prosese bir sinyal gelirse ve eğer o sinyal için sinyal fonksiyonu set edilmişse thread uyandırılıp sinyal fonksiyonu çalıştırılmamaktadır. İşte eğer mutex'in kilitli olması nedeniyle bloke oluştuğunda sinyal yoluyla thread'in uyandırılıp sinyal fonksiyonunun çalıştırması isteniyorsa mutex nesnesi mutex_lock ile değil, mutex_lock_interrupible fonksiyonu ile kilitlenmeye çalışılmalıdır. mutex_lock_interruptible fonksiyonunun prototipi şöyledir: #include int mutex_lock_interruptible(struct mutex *lock); Fonksiyon eğer mutex kilidini alarak sonlanırsa 0 değerine, bloke olup sinyal dolayısıyla sonlanırsa -EINTR değerine geri dönmektedir. Programcı bu fonksiyonun -EINTR ile sonlandığını tespit ettiğinde ilgili sistem fonksiyonunun yeniden çalıştırılabilirliğini sağlamak için -ERESTARTSYS ile geri dönebilir. -> Mutex nesnesinin kilidini bırakmak için (unlock etmek için) mutex_unlock fonksiyonu kullanılmaktadır: void mutex_unlock(struct mutex *lock); Bu durumda örneğin tipik olarak aygıt sürücü içerisinde belli bir bölgeyi mutex yoluyla koruma şöyle yapılmaktadır: DEFINE_MUTEX(g_mutex); ... if (mutex_lock_interruptible(&g_mutex) < 0) return -ERESTARTSYS; ... ... ... mutex_unlock(&g_mutex); Aşağıdaki örnekte yukarıdaki boru sürücüsü daha güvenli olacak biçimde mutex nesneleriyle senkronize edilmiştir. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static DEFINE_MUTEX(g_mutex); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (mutex_lock_interruptible(&g_mutex) < 0) return -ERESTARTSYS; esize = MIN(g_count, size); if (g_tail < g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; mutex_unlock(&g_mutex); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (mutex_lock_interruptible(&g_mutex) < 0) return -ERESTARTSYS; esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail > g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; mutex_unlock(&g_mutex); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Kernel'da da user moddakine benzer semaphore nesneleri vardır. Kernel semaphore nesneleri de sayaçlıdır. Yine bunların sayaçları 0'dan büyükse semaphore açık durumdadır, sayaçlar 0 değerinde ise semaphore kapalı durumdadır. Kritik koda girildiğinde yine sayaç 1 eksiltilir. Sayaç 0 olduğunda thread bloke edilir. Yine bloke işleminde biraz spin işlemi yapılıp sonra bloke uygulanmaktadır. Kernel semaphore nesneleri şöyle kullanılmaktadır: -> Semaphore nesnesi struct semaphore isimli bir yapıyla temsil edilmiştir. Bir semaphore nesnesi DEFINE_SEMAPHORE(name) makrosuyla aşağıdaki gibi oluşturulabilir. #define DEFINE_SEMAPHORE(g_sem); Bu biçimde yaratılan semaphore nesnesinin başlangıçta sayaç değeri 1'dir. Yeni çekirdeklerde (v6.4-rc1 ve sonrası) bu makro iki parametreli olarak da kullanılabilmektedir: DEFINE_SEMAPHORE(g_sem, n); Buradaki ikinci parametre semaphore sayacının başlangıçtaki değerini belirtmektedir. Semaphore nesneleri sema_init fonksiyonuyla da yaratılabilmektedir: struct semaphore g_sem; ... sema_init(&g_sem, 1); Fonksiyonun ikinci parametresi başlangıç sayaç numarasıdır. -> Kritik kod "down" ve "up" fonksiyonları arasına alınır. "down" fonksiyonları sayacı bir eksilterek kritik koda giriş yapar. "up" fonksiyonu ise sayacı bir artırmaktadır. Fonksiyonların prototipleri şöyledir: #define void down(struct semaphore *sem); int down_interruptible(struct semaphore *sem); int down_killable(struct semaphore *sem); int down_trylock(struct semaphore *sem); int down_timeout(struct semaphore *sem, long jiffies); void up(struct semaphore *sem); Kritik kod "down" fonksiyonu ile oluşturulduğunda thread bloke olursa sinyal yoluyla uyandırılamamaktadır. Ancak kritik kod "down_interruptible" fonksiyonu ile oluşturulduğunda thread bloke olursa sinyal yoluyla uyandırılabilmektedir. "down_killable" bloke olmuş thread'i yalnızca SIGKILL sinyali geldiğinde blokeden kurtarıp sonlandırabilmektedir. "down_killable" fonksiyonunda eğer thread bloke olursa diğer sinyaller yine blokeyi sonlandıramamaktadır. "down_trylock" yine nesnenin açık olup olmadığına bakmak için kullanılır. Eğer nesne açıksa yine sayaç 1 eksiltilir ve kritik koda girilir. Bu durumda fonksiyon 0 dışı bir değerle geri döner. Nesne kapalıysa fonksiyon bloke olmadan 0 değerine geri döner. "down_timeout" ise en kötü olasılıkla belli miktar "jiffy" zamanı kadar blokeye yol açmaktadır. ("jiffy" kavramı ileride ele alınacaktır.) Fonksiyon zaman aşımı dolduğundan dolayı sonlanmışsa negatif hata koduna, normal bir biçimde sonlanmışsa 0 değerine geri dönmektedir. "down_interruptible" fonksiyonu normal sonlanmada 0 değerine, sinyal yoluyla sonlanmada -ERESTARTSYS değeri ile geri döner. Normal uygulama eğer bu fonksiyonlar -ERESTARTSYS ile geri dönerse aygıt sürücüdeki fonksiyonun da aynı değerle geri döndürülmesidir. Zaten çekirdek bu -ERESTARTSYS geri dönüş değerini aldığında asıl sistem fonksiyonunu eğer sinyal için otomatik restart mekanizması aktif değilse -EINTR değeri ile geri döndürmektedir. Bu da tabii POSIX fonksiyonlarının başarısız olup errno değerini EINTR biçiminde set edilmesine yol açmaktadır. "up" fonksiyonu yukarıda da belirttiğimiz gibi semaphore sayacını 1 artırmaktadır. Kernel semaphore nesneleriyle kritik kod aşağıdaki gibi oluşturulmaktadır: DEFINE_SEMAPHORE(g_sem); ... down_interruptible(&g_sem); ... ... ... up(&g_sem); Yukarıdaki boru örneğinde biz mutex nesnesi yerine binary semaphore nesnesi de kullanabilirdik. Aşağıda aynı örneğin binary semaphore ile gerçekleştirimi görülmektedir. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static DEFINE_SEMAPHORE(g_sem); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem) < 0) return -ERESTARTSYS; esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; up(&g_sem); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem) < 0) return -ERESTARTSYS; esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; up(&g_sem); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } User modda gördüğümüz diğer senkronizasyon nesnelerinin benzerleri de çekirdek içerisinde bulunmaktadır. Örneğin spinlock kullanımına çekirdek kodlarında ve aygıt sürücülerde sıkça rastlanmaktadır. Anımsanacağı gibi spinlock uykuya dalarak değil, (yani bloke olarak değil) meşgul bir döngü içerisinde kilidin açılmasını bekleyen senkronizasyon nesnelerini belirtiyordu. Spinlock nesnelerinin çok işlemcili ya da çekirdekli sistemlerde kullanılabileceğini belirtmiştik. Tek işlemcili ya da çekirdekli sistemlerde spinlock ile kritik kod oluşturmak kötü bir tekniktir. Çünkü kilidi başka bir thread alırsa diğer thread CPU'yu meşgul bir döngüde bekleyecektir. Spinlock nesneleri küçük kod blokları için ve özellikle çok işlemcili ya da çok çekirdekli sistemlerde kullanılması gereken senkronizasyon nesneleridir. Spinlock nesnesinin kilidini alan thread'in bloke olmaması gerekir. Aksi takdirde istenmeyen sonuçlar oluşabilir. Özetle spinlock nesnesinin kilidini alan thread şu durumlara dikkat etmelidir: -> Thread, kilidi uzun süre kapalı tutmamalıdır. -> Thread, kilidi aldıktan sonra bloke olmamalıdır. -> Thread, kilidi aldıktan sonra IRQ (donanım kesmeleri) dolayısıyla kontrolü bırakma konusunda dikkatli olmalıdır. Linux'ta bir thread spinlock kilidini almışsa artık quanta süresi dolsa bile thread'ler arası geçiş kapatılmaktadır. Kernel spinlock nesneleri tipik olarak şöyle kullanılmaktadır: -> Spinlock nesnesi spinlock_t türü ile temsil edilmektedir. Spinlock nesnesinin yaratılması statik düzeyde aşağıdaki gibi yapılabilir: #include spinlock_t g_spinlock = SPIN_LOCK_UNLOCKED; Burada spinlock nesnesi kilit açık bir biçimde oluşturulmuştur. Ancak bu makro yeni çekirdeklerde kaldırılmıştır. Yeni çekirdeklerde DEFINE_SPINLOCK makrosu spinlock nesnesini kapalı olarak oluşturmakta kullanılabilmektedir. Örneğin: DEFINE_SPINLOCK(g_spinlock); Aynı işlem spin_lock_init fonksiyonuyla da yapılabilmektedir: #include void spin_lock_init(spinlock_t *lock); -> Kritik koda giriş için aşağıdaki fonksiyonlar kullanılmaktadır: #include void spin_lock(spinlock_t *lock); void spin_lock_irq(spinlock_t *lock); void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); void spin_lock_bh(spinlock_t *lock); "spin_lock" fonksiyonu klasik spin yapan fonksiyondur. "spin_lock_irq" fonksiyonu o anda çalışılan işlemci ya da çekirdekteki IRQ'ları (yani donanım kesmelerini) kapatarak kilidi almaktadır. Yani biz bu fonksiyonla kilidi almışsak kilidi bırakana kadar donanım kesmeleri oluşmayacaktır. "spin_lock_irqsave" fonksiyonu kritik koda girerken donanım kesmelerini kapatmakla birlikte önceki bir durumu geri yükleme yeteneğine sahiptir. Aslında bu fonksiyonların bazıları makro olarak yazılmıştır. Örneğin "spin_lock_irqsave" aslında bir makrodur. Biz bu fonksiyonun ikinci parametresine nesne adresini geçmemiş olsak da bu bir makro olduğu için aslında ikinci parametrede verdiğimiz nesnenin içerisine IRQ durumlarını yazmaktadır. "spin_lock_bh" fonksiyonu yalnızca yazılım kesmelerini kapatmaktadır. -> Kilidin geri bırakılması için ise aşağıdaki fonksiyonlar kullanılmaktadır: #include void spin_unlock(spinlock_t *lock); void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags); void spin_unlock_irq(spinlock_t *lock); void spin_unlock_bh(spinlock_t *lock); Yukarıdaki lock fonksiyonlarının hepsinin bir unlock karşılığının olduğunu görüyorsunuz. Biz kilidi hangi lock fonksiyonu ile almışsa o unlock fonksiyonu ile bırakmalıyız. Örneğin: spinlock_t g_spinlock = SPIN_LOCK_UNLOCKED; ... spin_lock(&g_spinlock); ... ... ... spin_unlock(&g_spinlock); Ya da örneğin: spinlock_t g_spinlock = SPIN_LOCK_UNLOCKED; ... unsigned long irqstate; ... spin_lock_irqsave(&g_spinlock, irqstate); ... ... ... spin_unlock_irqrestore(&g_spinlock, irqstate); -> Yine kernel spinlock nesnelerinde de try'lı lock fonksiyonları bulunmaktadır: #include int spin_trylock(spinlock_t *lock); int spin_trylock_bh(spinlock_t *lock); Bu fonksiyonlar eğer spinlock kilitliyse spin yapmazlar ve 0 ile geri dönerler. Eğer kilidi alırlarsa sıfır dışı bir değerle geri dönerler. Her ne kadar yukarıdaki boru sürücüsündeki read ve write fonksiyonlarında kuyruğu korumak için spinlock kullanımı uygun değilse de biz yine kullanım biçimini göstermek için aşağıdaki örneği veriyoruz. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; spinlock_t g_spinlock; static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } spin_lock_init(&g_spinlock); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; spin_lock(&g_spinlock); esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; spin_unlock(&g_spinlock); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; spin_lock(&g_spinlock); esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; spin_unlock(&g_spinlock); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Biz daha önce user modda reader/writer lock nesnelerini görmüştük. Bu nesneler birden fazla thread'in kritik koda okuma amaçlı girmesine izin veriyordu. Ancak bir thread kritik koda yazma amaçlı girmişse diğer bir thread'in okuma ya da yazma amaçlı kritik koda girmesine izin vermiyordu. İşte user moddaki reader/write lock nesnelerinin bir benzeri kernel modda reader/writer spinlock nesneleri biçiminde bulunmaktadır. Yine kernel modda da kritik koda okuma amaçlı ya da yazma amaçlı giren fonksiyonlar vardır. reader/writer spinlock nesneleri rwlock_t türüyle temsil edilmektedir. Bunların yaratılması rwlock_init fonksiyonuyla yapılmaktadır: #include void rwlock_init(rwlock_t *lock); reader/writer spinlock nesneleri ile ilgili diğer çekirdek fonksiyonları şunlardır: #include void read_lock(rwlock_t *lock); void read_lock_irqsave(rwlock_t *lock, unsigned long flags); void read_lock_irq(rwlock_t *lock); void read_lock_bh(rwlock_t *lock); void read_unlock(rwlock_t *lock); void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void read_unlock_irq(rwlock_t *lock); void read_unlock_bh(rwlock_t *lock); void write_lock(rwlock_t *lock); void write_lock_irqsave(rwlock_t *lock, unsigned long flags); void write_lock_irq(rwlock_t *lock); void write_lock_bh(rwlock_t *lock); int write_trylock(rwlock_t *lock); void write_unlock(rwlock_t *lock); void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void write_unlock_irq(rwlock_t *lock); void write_unlock_bh(rwlock_t *lock); Nesne read amaçlı lock edilmişse read amaçlı unlock işlemi, write amaçlı lock edilmişse write amaçlı unlock işlemi uygulanmalıdır. Fonksiyonların diğer işlevleri normal spinlock nesnelerinde olduğu gibidir. User moddaki senkronizasyon nesnelerinin benzerlerinin çekirdek içerisinde de bulunduğunu görüyorsunuz. Ancak user moddaki her senkronizasyon nesnesinin bir kernel mod karşılığı yoktur. Örneğin user moddaki "koşul değişkenlerinin (condition variables)" bir kernel mod karşılığı bulunmamaktadır. Ayrıca burada ele almadığımız (belki ileride ele alacağımız) yalnızca çekirdek içerisinde kullanılan birkaç senkronizasyon nesnesi daha bulunmaktadır. Biz user modda çeşitli fonksiyonların çeşitli koşullar altında blokeye yol açtığını belirtmiştik. Bir thread bloke olduğunda thread geçici süre (belli bir koşul sağlanana kadar) ilgili CPU'nun "çalışma kuyruğundan (run queue)" çıkartılır, ismine "bekleme kuyruğu (wait queue)" denilen bir kuyruğa yerleştirilir. Blokeye yol açan koşul ortadan kalktığında ise thread yeniden bekleme kuyruğundan alınarak CPU'nun çalışma kuyruğuna yerleştirilir. Biz şimdiye kadar user modda hep sistem fonksiyonları yoluyla blokelerin oluştuğunu gördük. Ancak kernel moddaki aygıt sürücülerde blokeyi aygıt sürücünün kendisi oluşturmaktadır. * Örnek 1, Biz boru aygıt sürücümüzde read işlemi yapıldığında eğer boruda okunacak hiç bilgi yoksa read işlemini yapan user moddaki thread'i bloke edebiliriz. Boruya bilgi geldiğinde de thread'i yeniden çalışma kuyruğuna yerleştirip blokeyi çözebiliriz. İşte bu bölümde aygıt sürücüde thread'lerin nasıl bloke edileceği ve blokenin nasıl çözüleceği üzerinde duracağız. Mevcut Linux sistemlerinde her CPU ya da çekirdeğin ayrı bir çalışma kuyruğu (run queue) bulunmaktadır. Ancak bir ara O(1) çizelgelemesi ismiyle Linux'ta bu konuda bir değişikliğe gidilmişti. O(1) çizelgelemesi tekniğinde toplam tek bir çalışma kuyruğu bulunuyordu. Hangi CPU ya da çekirdeğe atama yapılacaksa bu tek olan çalışma kuyruğundan thread alınıyordu. O(1) çizelgelemesi Linux'ta kısa bir süre kullanılmıştır. Bunun yerine "CFS (Completely Fair Scheduling)" çizelgeleme sistemine geçilmiştir. Çalışmakta olan bir thread'in bloke olması sırasında thread'in yerleştirileceği tek bir "bekleme kuyruğu (wait queue)" yoktur. Her CPU ya da çekirdek için de ayrı bir bekleme kuyruğu bulundurulmamaktadır. Bekleme kuyrukları ilgili olay temelinde oluşturulmaktadır. Örneğin sleep fonksiyonu dolayısıyla bloke olan thread'ler ayrı bir bekleme kuyruğuna, boru dolayısıyla bloke olan thread'ler ayrı bir bekleme kuyruğuna yerleştirilmektedir. Aygıt sürücüleri yazanlar da kendi olayları için kendi bekleme kuyruklarını yaratmaktadır. Tabii kernel'daki mutex ve semaphore fonksiyonları da aslında kendi içerisinde bir bekleme kuyruğu kullanmaktadır. Çünkü bu fonksiyonlar da blokeye yol açmaktadır. Yukarıda da belirttiğimiz gibi her aygıt sürücü kendi bloke olayları için kendinin kullanacağı bekleme kuyrukları yaratabilmektedir. Çekirdek içerisinde bekleme kuyruklarını yaratan ve yok eden çekirdek fonksiyonları bulunmaktadır. Yine çekirdek içerisinde bir thread'i çalışma kuyruğundan çıkartıp bekleme kuyruğuna yerleştiren, bekleme kuyruğundan çıkartıp çalışma kuyruğuna yerleştiren fonksiyonlar bulunmaktadır. Linux'ta bekleme kuyrukları wait_queue_head_t isimli bir yapıyla temsil edilmektedir. Bir bekleme kuyruğu DECLARE_WAIT_QUEUE_HEAD(name) makrosuyla oluşturulabilir. Örneğin: #include DECLARE_WAIT_QUEUE_HEAD(g_wq); Ya da nesne tanımlanıp init_waitqueue_head fonksiyonuyla da ilk değerlenebilir: #include wait_queue_head_t g_wq; ... init_waitqueue_head(&g_wq); Bir thread'i (yani task_struct nesnesini) çalışma kuyruğundan çıkartıp istenilen bekleme kuyruğuna yerleştirme işlemi wait_event makrolarıyla gerçekleştirilmektedir. Temel wait_event makroları şunlardır: wait_event(wq_head, condition); wait_event_interruptible(wq_head, condition); wait_event_killable(wq_head, condition); wait_event_timeout(wq_head, condition, timeout); wait_event_interruptible_timeout(wq_head, condition, timeout); wait_event_interruptible_exclusive(wq_head, condition); Bunlardan, -> wait_event makrosu thread'i "uninterruptible" biçimde bekleme kuyruğuna yerleştirir. Bu biçimde bloke olmuş thread'lerin blokeleri sinyal dolayısıyla çözülememektedir. -> wait_event_interruptible makrosu ise aynı işlemi "interruptible" olarak yapmaktadır. Yani sinyal geldiğinde thread bekleme kuyruğundan uyandırılır. wait_event_interruptible makrosunun wait_event makrosundan farkı eğer thread uyutulmuşsa, uykudan bir sinyalle uyandırılabilmesidir. Halbuki wait_event ile uykuya dalmış olan thread sinyal oluşsa bile uykudan uyandırılmamaktadır. wait_event_interruptible makrosu eğer sinyal dolayısıyla uyanmışsa -ERESTARTSYS değeri ile, koşul sağlandığından dolayı uyanmışsa 0 değeri ile geri dönmektedir. Örneğin: DECLARE_WAIT_QUEUE_HEAD(g_wq); int g_flag = 0; ... if (wait_event_interruptible(g_wq, g_flag != 0) != 0) return -ERESTARTSYS; Bu tür durumlarda böylesi flag değişkenlerini atomic almak iyi bir tekniktir. Örneğin: DECLARE_WAIT_QUEUE_HEAD(g_wq); atomic_t g_flag = ATOMIC_INIT(0); ... if (wait_event_interruptible(g_wq, atomic_read(&g_flag) != 0) != 0) return -ERESTARTSYS; -> wait_event_killable makrosu yalnızca SIGKILL sinyali için thread'i uyandırmaktadır. Yani bu biçimde bekleme kuyruğuna yerleştirilmiş bir thread'in blokesi sinyal geldiğinde çözülmez, ancak SIGKILL sinyali ile thread yok edilebilir. wait_event_killable ile thread uykuya dalındığında ise yalnızca SIGKILL sinyali ile thread uykudan uyandırılabilmektedir. Tabii programcı wait_event_interruptible makrosunun geri dönüş değerine bakmalı, eğer thread sinyal dolayısıyla uykudan uyandırılmışsa -ERESTARTSYS değeriyle kendi fonksiyonundan geri dönmelidir. -> wait_event_timeout ve wait_event_interruptible_timeout makrolarının wait_event makrolarından farkı thread'i en kötü olasılıkla belli bir jiffy zaman aşımı ile uyandırabilmesidir. Jiffy kavramı izleyen bölümlerde ele alınacaktır. -> wait_event_interruptible_exclusive (bunun interrutible olmayan biçimi yoktur) makrosu Linux çekirdeklerine 2.6'ının belli sürümünden sonra sokulmuştur. Yine bu makroyla birlikte aşağıda ele alınan wake_up_xxx_nr makroları da eklenmiştir. Bir prosesin exclusive olarak wait kuyruğuna yerleştirilmesi onlardan belli sayıda olanların uyandırılabilmesini sağlamaktadır. Makrolardaki "condition (koşul)" parametresi bool bir ifade biçiminde oluşturulmalıdır. Bu ifade ya sıfır olur ya da sıfır dışı bir değer olur. Bu koşul ifadesi "uyanık kalmak için bir koşul" belirtmektedir. Yani bu koşul uyandırma koşulu değildir, uyanık kalma koşuludur. Çünkü bu makrolarda koşula bakılması uyumadan önce ve uyandırılma işleminden sonra yapılmaktadır. Yani önce koşula bakılır. Koşul sağlanmıyorsa thread uyutulur. Thread uyandırıldığında yeniden koşula bakılır. Koşul sağlanmıyorsa yeniden uyutulur. Dolayısıyla uyanma işlemi çekirdek kodlarında tıpkı koşul değişkenlerinde (condition variable) olduğu gibi döngü içerisinde yapılmaktadır. Örneğin: DECLARE_WAIT_QUEUE_HEAD(g_wq); int g_flag = 0; ... wait_event(g_wq, g_flag != 0); Burada koşul g_flag != 0 biçimindedir. wait_event makroları fonksiyon değil makro biçiminde yazıldığı için bu koşul bu haliyle makronun içinde kullanılmaktadır. (Yani ifadenin sonucu değil, kendisi makroda kullanılmaktadır.) Makronun içerisinde önce koşula bakılmakta, bu koşul sağlanıyorsa thread zaten uyutulmamaktadır. Eğer koşul sağlanmıyorsa thread uyutulmaktadır. Thread uykudan uyandırıldığında tıpkı koşul değişkenlerinde olduğu gibi yeniden koşula bakılmakta eğer koşul sağlanmıyorsa thread yeniden uyutulmaktadır. Tabii wait_event makroları o andaki thread'i çizelgeden (yani run kuyruğundan) çıkartıp wait kuyruğuna yerleştirdikten sonra "context switch" işlemini de yapmaktadır. Context switch işlemi sonrasında artık run kuyruğundaki yeni bir thread çalışır. wait_event makrolarının temsili kodunu şöyle düşünebilirsiniz: while (koşul_sağlanmadığı_sürece) { } Bekleme kuyruğunda blokede bekletilen thread wake_up makrolarıyla uyandırılmaktadır. Uyandırılmaktan kastedilen şey thread'in bekleme kuyruğundan çıkartılıp yeniden çalışma kuyruğuna (run queue) yerleştirilmesidir. wait_event makrolarındaki koşula wake_up bakmamaktadır. wake_up makroları yalnızca thread'i bekleme kuyruklarından çalışma kuyruğuna taşımaktadır. Koşula uyandırılmış thread'in kendisi bakmaktadır. Eğer koşul sağlanmıyorsa yeniden uyutulmaktadır. Yani biz koşulu sağlanır duruma getirmeden wake_up işlemi yaparsak thread yeniden uykuya dalacaktır. (Zaten "koşulu sağlayan thread'i uyandırma" işlemi mümkün değildir.) En çok kullanılan wake_up makroları şunlardır: wake_up(wq_head); wake_up_nr(wq_head, nr); wake_up_all(wq_head); wake_up_interruptible(wq_head); wake_up_interruptible_nr(wq_head, nr); wake_up_interruptible_all(wq_head); Bu makroların çalışmasının anlaşılması için bekleme kuyrukları hakkında biraz ayrıntıya girmek gerekir. Bekleme kuyruğunu temsil eden wait_queue_head_t yapısı şöyle bildirilmiştir: struct __wait_queue_head { spinlock_t lock; struct list_head task_list; }; Görüldüğü gibi bu bir bağlı listedir. Bağlı liste spinlock ile korunmaktadır. Bu bağlı liste aşağıdaki yapılardan oluşmaktadır: struct __wait_queue { unsigned int flags; void *private; wait_queue_func_t func; struct list_head task_list; }; Bu yapının ayrıntısına girmeyeceğiz. Ancak yapıdaki flags elemanına dikkat ediniz. Bekleme kuyruğuna yerleştirilen bir thread'in exclusive bekleme yapıp yapmadığı (yani wait_event_intrerruptible_exclusive ile bekleme yapıp yapmadığı) bu flags elemanında saklanmaktadır. Bu wait kuyruğunun bekleyen thread'leri (onların task_struct adreslerini) tutan bir bağlı liste olduğunu varsayabilirsiniz. Yani bekleme kuyrukları aşağıdaki gibi düşünülebilir: T1 ---> T2 ---> T3 ---> T4 ---> T5 ---> T6 ---> T7 ---> T8 ---> NULL Bu thread'lerden bazıları exclusive bekleme yapmış olabilir. Bunları (E) ile belirtelim: T1 ---> T2 ---> T3 ---> T4(E) ---> T5 ---> T6(E) ---> T7 ---> T8(E) ---> NULL Yukarıdaki wake_up makrolarından, -> wake_up makrosu kuyruğun başından itibaren ilk exclusive bekleme yapan thread'e kadar bu thread de dahil olmak üzere tüm thread'leri uyandırmaktadır. Tabii bu thread'lerin hepsi uyandırıldıktan sonra ayrıca koşula da bakmaktadır. Örneğimizde wake_up makrosu çağrıldığında T1, T2, T3 ve T4 thread'leri uyandırılacaktır. Görüldüğü gibi wake_up makrosu aslında 1 tane exclusive thread uyandırmaya çalışmaktadır. Ancak onu uyandırırken kuyruğun önündeki exclusive olmayanları da uyandırmaktadır. -> wake_up_nr makrosu, wake_up makrosu gibi davranır ancak 1 tane değil en fazla nr parametresiyle belirtilen sayıda exclusive thread'i uyandırmaya çalışır. Başka bir deyişle wake_up(g_wq) çağrısı ile wake_up_nr(g_qw, 1) çağrısı aynı anlamdadır. Eğer yukarıdaki örnekte wake_up_nr(g_wq, 2) çağrısını yapmış olsaydık T1, T2, T2, T4, T5, T6 thread'leri uyandırılırdı. Tabii bu thread'lerin uyandırılmış olması wait_event makrolarından çıkılacağı anlamına gelmemektedir. Uyandırma işleminden sonra koşula yeniden bakılmaktadır. -> wake_up_all makrosu bekleme kuyruğundaki tüm exclusive thread'leri ve exclusive olmayan thread'leri yani kısaca tüm thread'leri uyandırmaktadır. Tabii yine uyanan thread'ler koşula bakmaktadır. -> wake_up_interruptible, wake_up_interruptible_nr ve wake_up_interruptible_all makroları interruptible olmayan makrolar gibi çalışmaktadır. Ancak bu makrolar bekleme kuyruğunda yalnızca "interruptible" wait_event fonksiyonlarıyla bekletilmiş thread'lerle ilgilenmektedir. Diğer thread'ler kuyrukta yokmuş gibi davranmaktadır. Şimdi de bu makroları kullandığımız bir örnek verelim: * Örnek 1, Aygıt sürücümüzün read ve write fonksiyonları aşağıdaki gibi olsun: wait_queue_head_t g_wq; atomic_t g_flag; ... static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver read...\n"); atomic_set(&g_flag, 0); if (wait_event_interruptible(g_wq, atomic_read(&g_flag) != 0) != 0) { printk(KERN_INFO "Signal occured..."); return -ERESTARTSYS; } return 0; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver write...\n"); atomic_set(&g_flag, 1); wake_up_interruptible(&g_wq); return 0; } Burada eğer birden fazla thread read yaparsa exclusive olmayan bir biçimde bekleme kuyruğunda bekleyecektir. write işleminde wake_up_interruptible makrosu ile uyandırma yapıldığına dikkat ediniz. Bekleme kuyruğunda exclusive bekleyen thread olmadığına göre burada tüm read yapan thread'ler uyandırılacaktır. Onların koşulları sağlandığı için hepsi read fonksiyonundan çıkacaktır. Şimdi bu read fonksiyonunda exclusive bekleme yapmış olalım: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver read...\n"); atomic_set(&g_flag, 0); if (wait_event_interruptible_exclusive(g_wq, atomic_read(&g_flag) != 0) != 0) { printk(KERN_INFO "Signal occured..."); return -ERESTARTSYS; } return 0; } Artık write fonksiyonunda wake_up makrosu çağrıldığında yalnızca bir tane exclusive bekleme yapan thread uyandırılacağı için read fonksiyonundan yalnızca bir thread çıkacaktır. Test için aşağıdaki kodları kullanabilirsiniz. /* wait-driver.c */ #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Wait-Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wq; static atomic_t g_flag; static int __init generic_init(void) { int result; printk(KERN_INFO "wait-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "wait-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } init_waitqueue_head(&g_wq); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "wait-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "wait-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "wait-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver read...\n"); atomic_set(&g_flag, 0); if (wait_event_interruptible_exclusive(g_wq, atomic_read(&g_flag) != 0) != 0) { printk(KERN_INFO "Signal occured..."); return -ERESTARTSYS; } return 0; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver write...\n"); atomic_set(&g_flag, 1); wake_up_interruptible(&g_wq); return 0; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* wait-test-read.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[32]; ssize_t result; if ((fd = open("wait-driver", O_RDONLY)) == -1) exit_sys("open"); printf("reading begins...\n"); if ((result = read(fd, buf, 32)) == -1) exit_sys("result"); printf("Ok\n"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* wait-test-write.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[32] = {0}; if ((fd = open("wait-driver", O_WRONLY)) == -1) exit_sys("open"); if (write(fd, buf, 32) == -1) exit_sys("write"); printf("Ok\n"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Burada bir noktaya dikkatinizi çekmek istiyoruz. Daha önce görmüş olduğumuz mutex, semaphore, read/write kilitleri gibi senkronizasyon nesnelerinin kendilerinin oluşturduğu bekleme kuyrukları vardır. Bu senkronizasyon nesneleri bloke oluşturmak için kendi bekleme kuyruklarını kullanmaktadır. Şimdi de daha önce yapmış olduğumuz boru örneğimizi gerçek bir boru haline getirelim. Yani eğer boruda en az 1 byte boş alan kalmadıysa read fonksiyonu blokede en az 1 byte okuyana kadar beklesin. Eğer boruda tüm bilgileri yazacak kadar boş yer kalmadıysa bu kez de yazan taraf blokede beklesin. Burada izlenecek temel yöntem aslında kursumuzda "koşul değişkenleri (condition variable)" denilen senkronizasyon nesnelerindeki yöntemin aynısı olmalıdır. Okuyan thread kuyruktaki byte sayısını belirten g_count == 0 olduğu sürece bekleme kuyruğunda beklemelidir. Tabii bizim kuyruk üzerinde işlem yaptığımız kısımları senkronize etmemiz gerekir. Bunu da bir binary semaphore nesnesi ya da mutex nesnesi yapabiliriz. Semaphore nesnesini ve bekleme kuyruğunu aşağdaki gibi yaratabiliriz: static wait_queue_head_t g_wq; DEFINE_SEMAPHORE(g_sem); Okuyan taraf önce semaphore kilidini eline almalı ancak eğer uykuya dalacaksa onu serbest bırakıp uykuya dalmalıdır. Kuyruk üzerinde aynı anda işlemler yapılabiceği için tüm işlemlerin kritik kod içerisinde yapılması uygun olur. O halde read işleminin tipik çatısı şöyle olmalıdır: ... if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); if (wait_event_interruptible(g_wqread, g_count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } // kuyruktan okuma işlemleri up(&g_sem); Burada önce down_interruptible fonksiyonu ile semaphore kilitlenmeye çalışılmıştır. Eğer semaphore zaten kilitliyse semaphore'un kendi bekleme kuyruğunda thread uykuya dalacaktır. Daha sonra g_count değerine bakılmıştır. Eğer g_count değeri 0 ise önce semaphore serbest bırakılıp sonra thread bekleme kuyruğunda uyutulmuştur. Thread bekleme kuyruğundan uyandırıldığında yeniden semaphore kontrolünü ele almaktadır. Tabii eğer birden fazla thread bekleme kuyruğundan uyandırılırsa yalnızca bunlardan biri semaphore kontrolünü ele alacaktır. Tabii bundan sonra kuyruktan bilgiler okunacak ve semaphore kilidi serbest bırakılacaktır. Eğer birden fazla thread bekleme kuyruğundan uyanmışsa bu kez diğer bir thread semaphore kontrolünü ele alacak ve g_count değerine bakacaktır. Yukarıda da belirttiğimiz gibi aslında bu bir "koşul değişkeni" kodu gibidir. Çekirdek içerisinde böyle bir nesne olmadığı için manuel uygulanmıştır. Benzer biçimde write işleminin de çatısı aşağıdaki gibidir: if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } // kuyruğa yazma işlemleri up(&g_sem); Burada benzer işlemler uygulanmıştır. Eğer kuyrukta yazma yapılmak istenen kadar boş alan varsa akış while döngüsünün içerisine girmeyecektir. (Buradaki while koşulunun "PIPE_BUFSIZE - g_count < size" biçiminde olduğuna dikkat ediniz.) Dolayısıyla yazma işlemi kritik kod içerisinde yapılabilecektir. Ancak kuyrukta yeteri kadar yer yoksa semaphore kilidi serbest bırakılıp thread bekleme kuyruğunda bekletilecektir. Çıkışta benzer işlemler yapılmaktadır. Aslında burada spinlock nesneleri de kullanılabilir. Ancak zaten mutex, semaphore ve read/write lock nesneleri kendi içerisinde bir miktar spin yapmaktadır. Bu örnekte semaphore yerine spinlock kullanabilir miyiz? Spinlock için şu durumları gözden geçirmelisiniz: -> Spinlock nesnesinde bekleme CPU zamanı harcanarak meşgul bir döngü içerisinde yapılmaktadır. Dolayısıyla spinlock nesneleri kilidin kısa süreli bırakılacağından emin olunabiliyorsa kullanılmalıdır. -> Spinlock içerisinde sinyal işlemleri bekletilmektedir. Yani spinlock beklemelerinin "interruptible" bir biçimi yoktur. Aslında bu uygulamada spinlock nesneleri de kullanılabilir. Ancak yine de kilitli kalınan kod miktarı dikkate alındığında semaphore nesnesi daha uygun bir seçenektir. Burada yazma işlemleri için "yazma bekleme kuyruğu" ve okuma işlemleri için "okuma bekleme kuyruğu" biçiminde iki bekleme kuyruğu olduğuna dikkat ediniz. Çünkü yazan taraf okuma bekleme kuyruğundaki thread'leri okuyan taraf ise yazma bekleme kuyruğundaki thread'leri uyandırmak isteyecektir. Şimdi de bu anlatılanları işlediğimiz bir örnek verelim: * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); if (wait_event_interruptible(g_wqread, g_count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len; if ((fd = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aslında wait_event fonksiyonları export edilmiş birkaç fonksiyon çağrılarak yazılmıştır. Dolayısıyla wait_event fonksiyonlarını çağırmak yerine programcı daha aşağı seviyeli (zaten wait_event fonksiyonlarının çağırmış olduğu) fonksiyonları çağırabilir. Yani bu işlemi daha aşağı seviyede manuel de yapabilir. Prosesin manuel olarak wait kuyruğuna alınması prepare_to_wait ve prepare_to_wait_exclusive isimli fonksiyonlar tarafından yapılmaktadır: #include void prepare_to_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state); void prepare_to_wait_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state); Bu fonksiyonların birinci parametreleri bekleme kuyruğu nesnesinin adresini almaktadır. İkinci parametreleri bu kuyruğa yerleştirilecek wait_queue_entry nesnesinin adresini almaktadır. Fonksiyonların üçüncü parametreleri TASK_UNINTERRUPTIBLE ya da TASK_INTERRUPTIBLE biçiminde geçilebilir. Bir wait_queue_entry nesnesi şöyle oluşturulabilir: DEFINE_WAIT(wqentry); Ya da açıkça tanımlanıp init_wait makrosuyle ilk değerlenebilir. Örneğin: struct wait_queue_entry wqentry; ... init_wait(&wqentry); DEFINE_WAIT makrosu global tanımlamalarda kullanılamamktadır. Çünkü bu makro küme parantezleri içerisinde sabit ifadesi olmayan ifadeler barındırmaktadır. Ancak makro yerel tanımlamalarda kullanılabilir. Dolayısıyla prepare_to_wait ve prepare_to_wait_exclusive fonksiyonları da aslında bekleme kuruğuna bir wait_queue_enty nesnesi eklemektedir. Yani programcının bunun için yeni bir wait_queue_entry nesnesi oluşturması gerekmektedir. prepare_to_wait_exclusive exclusive uyuma için kullanılmaktadır. prepare_to_wait ve prepare_to_wait_exclusive fonksiyonları şunları yapmaktadır: -> Thread'i çalışma kuyruğundan çıkartıp bekleme kuyruğuna yerleştirir. (Çalışma kuruğunun organizasyonu ve bu işlemin gerçek ayrıntıları biraz karmaşıktır.) -> Thread'in durum bilgisini (task state) state parametresiyle belirtilen duruma çeker. -> prepare_to_wait fonksiyonu kuyruk elemanını eclusive olmaktan çıkartırken, prepare_to_wait_exclusive onu exclusive yapar. Thread'in çalışma kuyruğundan bekleme kuyruğuna aktarılması onun uykuya dalması anlamına gelmemektedir. Programcı artık thread çalışma kuyruğunda olmadığına göre schedule fonksiyonu ile thread'ler arası geçiş (context switch) uygulamalı ve akış kontrolünü başka bir thread'e bırakmalıdır. Zaten thread'in çalışma kuyruğundan çıkartılması artık yeniden çalışma kuyruğuna alınmadıktan sonra uykuda bekletilmesi anlamına gelmektedir. Tabii biz prepare_to_wait ya da prepare_to_wait_exclusive fonksiyonlarını çağırdıktan sonra bir biçimde koşul durumuna bakmalıyız. Eğer koşul sağlanmışsa hiç prosesi uykuya daldırmadan hemen bekleme kuyruğundan çıkarmalıyız. Eğer koşul sağlanmamışsa gerçekten artık schedule fonksiyonuyla "thread'lerarası geçiş" yapmalıyız. Thread'imiz schedule fonksiyonunu çağırdıktan sonra artık uyandırılana kadar bir daha çizelgelenmyecektir. Bu da bizim uykuya dalmamız anlamına gelmektedir. Pekiyi thread'imiz uyandırıldığında nereden çalışmaya devam edecektir? İşte schedule fonksiyonu thread'ler arası geçiş yaparken kalınan yeri thread'e ilişki task_struct yapısının içerisine kaydetmektedir. Kalının yer schudule fonksiyonunun içerisinde bir yerdir. O halde thread'imiz uyandırıldığında schedule fonksiyonunun içerisinden çalışma devam eder. schedule fonksiyonu geri dönecek ve thread akışı devam edecektir. wake_up fonksiyonları thread'i bekleme kuyruklarından çıkartıp çalışma kuyruğuna eklemektedir. Ancak prepare_to_wait ve prepare_to_wait_exclusive fonksiyonları çağrıldıktan sonra eğer koşulun zaten sağlandığı görülürse bu durumda uyandırma wake_up fonksiyonlarıyla yapılmadığı için bekleme kuyruğundan thread'in geri çıkartılması da programcının sorumluluğundadır. Bu işlem finish_wait fonksiyonu ile yapılmaktadır. #include void finish_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry); Bu fonksiyon zaten thread wake_up fonksiyonları tarafından bekleme kuyruğundan çıkartılmışsa herhangi bir işlem yapmamaktadır. Bu durumda manuel uyuma şöyle yapılabilir. DEFINE_WAIT(wqentry); prepare_to_wait(&g_wq, &wqentry, TASK_UNINTERRUPTIBLE); if (!condition) schedule(); finish_wait(&wqentry); Tabii eğer thread INTERRUPTIBLE olarak uyuyorsa schedule fonksiyonundan çıkıldığında sinyal dolayısıyla da çıkılmış olabilir. Bunu anlamak için signal_pending isimli fonksiyon çağrılır. Bu fonksiyon sıfır dışı bir değerle geri dönmüşse uyandırma işleminin sinyal yoluyla yapıldığı anlaşılır. Bu durumda tabii aygıt sürücüdeki fonksiyon bu durumda -ERESTARTSYS ile geri döndürülmelidir. signal_pending fonksiyonunun prototipi şöyledir: #include int signal_pending(struct task_struct *p); Fonksiyon parametre olarak thread'e ilişkin task_struct yapısının adresini parametre olarak almaktadır. Bu durumda INTERRUPTIBLE uyuma aşağıdaki gibi yapılabilir: DEFINE_WAIT(wqentry); prepare_to_wait(&g_wq, &wqentry, TASK_INTERRUPTIBLE); if (!condition) schedule(); if (signal_pending(current)) return -ERESTARTSYS; finish_wait(&wqentry); wake_up makrolarının şunları yaptıpını anımsayınız: -> Wait kuyruğundaki prosesleri çıkartarak run kuyruğuna yerleştirir. -> Prosesin durumunu TASK_RUNNING haline getirir. Aşağıda boru örneğinde manuel uykuya dalma işlemi uygulamıştır: * Örnek 1, /* pipe-driver-manual-wait.c */ #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver-manual-wait")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; DEFINE_WAIT(wqentry); if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); prepare_to_wait(&g_wqread, &wqentry, TASK_INTERRUPTIBLE); schedule(); if (signal_pending(current)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; DEFINE_WAIT(wqentry); if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); prepare_to_wait(&g_wqwrite, &wqentry, TASK_INTERRUPTIBLE); schedule(); if (signal_pending(current)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver-manual-wait", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver-manual-wait", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aslında wait_event fonksiyonları yukarıda açıkladığımız daha aşağı seviyeli fonksiyonlar kullanılarak gerçekleştirilmiştir. Mevcut son Linux çekirdeğinde wait_event_interruptible fonksiyonu şöyle yazılmıştır: #define wait_event_interruptible(wq_head, condition) \ ({ \ int __ret = 0; \ might_sleep(); \ if (!(condition)) \ __ret = __wait_event_interruptible(wq_head, condition); \ __ret; \ }) Burada gcc'nin bileşik ifade de denilen bir eklentisi (extension) kullanılmıştır. Bu makro ayrıntılar göz ardı edilirse __wait_event_interruptible makrosunu çağırmaktadır. Bu makro şöyle tanımlanmıştır: #define __wait_event_interruptible(wq_head, condition) \ ___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0, \ schedule()) Burada ___wait_event makrosunun interruptible olan ve olmayan kodların ortak makrosu olduğu görülmektedir. Bu makro da şöyle tanımlanmıştır: #define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \ ({ \ __label__ __out; \ struct wait_queue_entry __wq_entry; \ long __ret = ret; /* explicit shadow */ \ \ init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \ for (;;) { \ long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state); \ \ if (condition) \ break; \ \ if (___wait_is_interruptible(state) && __int) { \ __ret = __int; \ goto __out; \ } \ \ cmd; \ } \ finish_wait(&wq_head, &__wq_entry); \ __out: __ret; \ }) Aygıt sürücülerimize arzu edersek "blokesiz (nonbloking)" okuma yazma desteği verebiliriz. Tabii bu detseğin verilebilmesi için aygıt sürücünün okuma yazma sırasında bloke oluşturması gerekmektedir. Anımsanacağı gibi blokesiz işlem yapabilmek için open POSIX fonksiyonunda fonksiyonun ikinci parametresine O_NONBLOCK bayrağının eklenmelidir. Normal disk dosyalarında O_NONBLOCK bayrağının bir anlamı yoktur. Ancak boru gibi özel dosyalarda ve aygıt sürücülerde bu bayrak şu anlama gelmektedir: -> Okuma sırasında eğer okunacak bir bilgi yoksa read fonksiyonu bloke oluşturmaz başarısızlıkla geri döner ve errno değeri EAGAIN olarak set edilir. -> Yazma sırasında yazma eylemi meşguliyet yüzünden yapılamıyorsa write fonksiyonu bloke oluşturmaz başarısızlıkla geri döner ve errno değeri yine EAGAIN olarak set edilir. Aygıt sürücü açıldığında open fonksiyonun ikinci parametresi file yapısının (dosya nesnesinin) f_flags elemanına yerleştirilmektedir. Dosya nesnesinin adresinin aygıt sürücüdeki fonksiyonları filp parametresiyle aktarıldığını anımsayınız. Bu durumda biz aygıt dosyasının blokesiz modda açılıp açılmadığını şöyle test edebiliriz: if (filp->f_flags & O_NONBLOCK) { /* blokesiz modda mı açılmış */ /* open fonksiyonunda aygıt O_NONBLOCK bayrağı ile açılmış */ } Aygıt sürücümüz blokesiz modda işlemlere izin vermiyorsa biz bu durumu kontrol etmeyebiliriz. Yani böyle bir aygıt sürücüde programcının aygıt sürücüyğ O_NONBLOCK bayrağını kullanarak açmışsa bu durumu hiç dikkate almayabiliriz. (Örneğin disk dosyalarında blokesiz işlemlerin bir anlamı olmadığı halde Linux çekirdeği disk dosyaları O_NONBLOCK bayrağıyla açıldığında hata ile geri dönmeden bayrağı dikkate almamaktadır.) Eğer bu kontrol yapılmak isteniyorsa aygıt sürücünün açılması sırasında kontrol aygıt sürücünün open fonksiyonund yapılabilir. Bu durumda open fonksiyonunu -EINVAL değeriyşe geri döndürebilirsiniz. Örneğin: static int generic_open(struct inode *inodep, struct file *filp) { if (filp->f_flas & O_NONBLOK) return -EINVAL; return 0; } Pekiyi boru aygıt sürücümüze nasıl blokesiz mod desteği verebiliriz? Aslında bunun için iki şeyi yapmamız gerekir: -> Yazma yapıldığı zaman boruda yazılanları alacak kadar yoksa aygıt sürücümüzün write fonksiyonunu -EAGAIN değeriyşe geri döndürmeliyiz. Örneğin: ... if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } ... -> Okuma yapıldığı zaman eğer boruda hiç bilgi yoksa aygıt sürücümüzün read fonksiyonunu -EAGAIN geri döndürmeliyiz. Örneğin: ... if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqread, g_count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } ... read ve write fonksiyonlarının -EAGAIN değeriyle geri döndürülmeden önce aygıt dosyasının blokesiz modda açlıp açılmadığının kontrol edilmesi gerektiğine dikkat ediniz. Aşaşğıdaki örnekte boru aygıt sürücüsüne blokesiz okuma ve yazma desteği verilmiştir. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqread, g_count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY|O_NONBLOCK)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) if (errno == EAGAIN) { printf("write returns -1 with errno = EAGAIN...\n"); continue; } printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY|O_NONBLOCK)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) if (errno == EAGAIN) { printf("read returns -1 with errno = EAGAIN...\n"); continue; } buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Çekirdek modülleri ve aygıt sürücüler dinamik bellek tahsis etmeye gereksinim duyabilirler. Ancak kernel mod programlar dinamik tahsisatları malloc, calloc ve realloc gibi fonksiyonlarla yapamazlar. Çünkü bu fonksiyonlar user mod programlar tarafından kullanılacak biçimde prosesin bellek alanında tahsisat yapmak için tasarlanmışlardır. Oysa çekirdeğin ayrı bir heap sistemi vardır. Bu nedenle çekirdek modülleri ve aygıt sürücüler çekirdeğin sunduğu fonksiyonlarla çekirdeğin heap alanında tahsisat yapabilirler. Biz de bu bölümde bu fonksiyonlar üzerinde duracağız. Anımsanacağı gibi Linux sistemlerinde proseslerin bellek alanları sayfa tabloları yoluyla izole edilmişti. Ancak çekirdek tüm proseslerin sayfa tablosunda aynı yerde bulunmaktadır. Başka bir deyişle her prosesin sayfa tablosunda çekirdek hep aynı sanal adreslerde bulunmaktadır. Örneğin sys_open sistem fonksiyonuna girildiğinde bu fonksiyonun sanal adresi her proseste aynıdır. 32 Bit linux sistemlerinde proseslerin sanal bellek alanları 3GB User, 1GB Kernel olmak üzere 2 bölüme ayrılmıştır. 64 bit Linux sistemlerinde ise yalnızca sanal bellek alanının 256 TB'si kullanılmaktadır. Bu sistemlerde user alanı için 128 TB, Kernel alanı için de 128 TB yer ayrılmıştır. 32 Bit Linux sistemlerindeki prosesin sanal bellek alanı şöyle gösterilebilir: 00000000 USER ALANI (3 GB) C0000000 KERNEL ALANI (1GB) 64 Bit Linux sistemlerindeki sanal bellek alanı ise kabaca şöyledir: 0000000000000000 USER ALANI (128 TB) 0000800000000000 BOŞ BÖLGE (yaklaşık 16M TB) FFFF800000000000 KERNEL ALANI (128 TB) FFFFFFFFFFFFFFFF Bir sistem fonksiyonunun çağrıldığını düşünelim. İşlemci kernel moda'a otomatik olarak geçirilecektir. Bu durumda sayfa tablosu değişmeyecektir. Pekiyi kernel nasıl tüm fiziksel belleğe erişebilmektedir? İşte 32 bitlik sistemlerde proseslerin sayfa tablolarının son 1GB'yi sayfalandırdığı girişleri tamamen fiziksel belleği eşlemektedir. Başka bir deyişle bu sistemlerde çekirdek alanının başlangıcı olan C0000000 adresi aslında sayfa tablosunda 00000000 fiziksel adresini belirtmektedir. Böylece kernel'ın herhangi bir fiziksel adrese erişmek istediği zaman tek yapacağı şey bu adrese C00000000 değerini toplamaktır. Bu sistemlerde C0000000 adresinden itibaren proseslerin sayfa tabloları zaten fiziksel belleği 0'dan itibaren haritalandırmaktadır. Ancak 32 bit sistemlerde şöyle bir sorun vardır: Sayfa tablosunda C0000000'dan itibaren sayfalar fiziksel belleği haritalandırdığına göre 32 bit sistemlerin maksimum sahip olacağı 4GB fiziksel RAM'in hepsi haritalandırılamamaktadır. İşte Linux tasarımcıları sayfa tablolarında C0000000'dan itibaren fiziksel RAM'in 1GB'sini değil 896MB'sini haritalandırmıştır. Geri kalan 128 MB'lik sayfa tablosu alanı fiziksel RAM'de 896MB'nin ötesine erişmek için değiştirilerek kullanılmaktadır. Yani 32 bit sistemlerde kernel fiziksel RAM'in ilk 896MB'sine doğrudan ancak bunun ötesine sayfa tablosunun son 128 MB'lik bölgesini değiştirerek erişmektedir. 32 bit sistemlerde 896MB'nin ötesine dolaylı biçimde erişildiği için bu bölgeye "high memory zone" denilmektedir. Tabii 64 bit sistemlerde böyle bir problem yoktur. Çünkü bu sistemlerde yine sayfa tablolarının kernel alanı fiziksel RAM'i başından itibaren haritlandırmaktadır. Ancak 128TB'lik alan zaten şimdiki bilgisayarlara takılabilecek fiziksel RAM'in çok ötesindedir. Bu nedenle 64 bit sistemlerde "high memory zone" kavramı yoktur. Çekirdek kodların kernel alanın başlangıcı PAGE_OFFSET makrosuyla belirlenmiştir. Linux çekirdeği için fiziksel RAM temel olarak 3 bölgeye (zone) ayrılmıştır: ZONE_DMA ZONE_NORMAL ZONE_HIGHMEM ZONE_DMA ilgili sistemde disk ile RAM arasında transfer yapan DMA'nın erişebildiği RAM alanıdır. Bazı sistemlerde DMA tüm fiziksel RAM'in her yerine transfer yapamamaktadır. ZONE_NORMAL doğrudan çekirdeğin sayfa tablosu yoluyla haritalandırdığı fiziksel bellek bölgesidir. Intel 32 bit Linux sistemlerinde bu bölge ilk 896 MB'dir. Ancak 64 bit Linux sistemlerinde bu bölge tüm fiziksel RAM'i içermektedir. ZONE_HIGHMEM ise 32 bit sistemlerde çekirdeğin doğrudan haritalandıramadığı sayfa tablosunda değişiklik yapılarak erişilebilen fiziksel RAM alanıdır. 32 Linux sistemlerinde 896 MB'nin yukarısındaki fiziksel RAM ZONE_HIHMEM alanıdır. Yukarıda da belirttiğimiz gibi 64 bit Intel işlemcilerinde ZONE_HIGHMEM biçiminde bir alan yoktur. User mode programlarda kullandığımız malloc fonksiyonunun uyguladığı klasik tahsisat yöntemi "boş alan bağlı liste" denilen yöntemdir. Bu yöntemde yalnızca boş alanlar bir bağlı listede tutulmaktadır. Dolayısıyla malloc gibi bir fonksiyon bu bağlı listede uygun bir elemanı bağlı listeyi dolaşarak bulmaktadır. free fonksiyonu da tahsis edilmiş olan alanı bu boş bağlı listeye eklemektedir. Tabii free fonksiyonu aynı zamanda bağlı listedeki komşu alanları da daha büyük bir boş alan oluşturacak biçimde birleştirmektedir. Ancak bu klasik yöntem çekirdek heap sistemi için çok yavaş kalmaktadır. Bu nedenle çekirdeğin heap sistemi için hızlı çalışan tahsisat algoritmaları kullanılmaktadır. Eğer tahsis edilecek bloklar eşit uzunlukta olursa bu durumda tahsisat işlemi ve geri bırakmak işlemi O(1) karmaşıklıkta yapılabilir. Örneğin heap içerisindeki tüm blokların 16 byte uzunlukta olduğunu düşünelim. Bu durumda 16 byte'lık tahsisat sırasında uygun bir boş alan aramaya gerek kalmaz. Bir dizisi içerisinde boş alanlar tutulabilir. Bu boş alanlardan herhangi biri verilebilir. Tabii uygulamalarda tahsis edilecek alanların büyükleri farklı olmaktadır. İşte BSD ve Linux sistemlerindekullanılan "dilimli tahsisat sistemi (slab allocator)" denilen tahsisat sisteminin anahtar noktası eşit uzunlukta ismine "dilim (slab)" denilen blokların tahsis edilmesidir. Kernel içerisinde çeşitli nesneler için o nesnelerin uzunluğuna ilişkin farklı dilimli tahsisat sistemleri oluşturulmuştur. Örneğin bir proses yaratıldığında task_struct yapısı çekirdeğin heap alanında tahsis edilmektedir. İşte dilimli tahsisat sistemlerinden biri sizeof(struct task_struct) kadar dilimlerdne oluşan sistemdir. Böylece pek çok kernel nesnesi için ayrı dilimli tahisat sistemleri luşturulmuştur. Bunların yanı sıra ayrıca bir de genel kullanım için blok uzunlukları 32, 64, 96, 128, 192, 256 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, ... biçiminde olan farklı dilimli tahsisat sistemleri de bulundurulmuştur. Böylece kernek mod programcısı belli uzunlukta bir alan tahsis etmek istediğinde bu uzunlupa en yakın bu uzunluktan büyük bir dilimli tahsisat sistemini kullanır. Tabii kernel mode programcılar isterse kendi nesneleri için o nesnelerin uzunluğu kadar yeni dilimli tahsisat sistemleri de oluşturabilmektedir. Aslında dilimli tahsisat sisteminin hazırda bulundurduğu dilimler işletim sisteminin sayfa tahsisatı yapan başka bir tahsisat algoritmasından elde edilmektedir. Linux sistemlerinde sayfa temelinde tahsisat yapmak için kullanılan tahsisat sistemine "buddy allocator" denilmektedir. (CSD işletim sisteminde buna "ikiz blok sistemi" denilmektedir.) Çekirdek kodlamasında çekirdek alanında dinamik tahsisat yapmak için kullanılan en genel fonksiyon kmalloc isimli fonksiyondur. Bu fonksiyon aslında parametresiyle belirtilen uzunluğa en yakon önceden yaratılmış olan dilimli tahsisat sisteminden dilim vermektedir (yani blok tahsis etmektedir). Örneğin biz kmalloc fonksiyonu ile 100 byte tahsis etmek istesek 100 byte'lık blokların bulunduğu önceden yaratılmış bir dilimli tahsisat sistemi olmadığ için kmalloc 128 byte'lık bloklara sahip dilimli tahsisat sisteminden bir dilim tahsis ederek bize vermektedir. Tabii bu örnekte 28 byte boşuna tahsis edilmiş olacaktır. Ancak çekirdek tahsisat sisteminin amacı en uygun miktarda belleği tahsis etmek değil talep edilen miktarda belleği hızlı tahsis etmektir. kmalloc fonksiyonu ile tahsis edilen dilimler kfree fonksiyonu ile serbest bırakılmaktadır. Fonksiyonların prototipleri şöyledir: void *kmalloc (size_t size, int flags); void kfree (const void *objp); kmalloc fonksiyonunun birinci parametresi tahsis edilecek byte sayısını belirtir. İkincisi parametresi ise tahsis edilecek alan ve biçim hakkında çeşitli bayrakları içermektedir. Bu ikinci parametre çeşitli sembolik sabitlerden oluşturulmaktadır. Burada önemli birkaç bayrak şunlardır: -> GFP_KERNEL: Kernel alanı içerisinde normal tahsisat yapmak için kullanılır. Bu bayrak en sık bu kullanılan bayraktır. Burada eğer RAM doluysa işletim sistemi prosesi bloke ederek swap işlemi ile yer açabilmektedir. Bu işlem sırasında akış kernel mode'da bekleme kuruklarında bekletilebilir. Tahsisat işlemi ZONE_NORMAL alanından yapılmaktadır. -> GFP_NOWAIT: GFP_KERNEL gibidir. Ancak hazırda bellek yoksa proses uykuya yatılmaz. Fonksiyon başarısız olur. -> GFP_HIGHUSER: 32 bit sistemlerde ZONE_HIHMEM alanından tahssat yapar. -> GFP_DMA: İlgili sistemde DMA'nın erişebildiği fiziksel RAM alanından tahsisat yapar. kmalloc fonksiyonu başarı durumunda tahsis edilen alanın sanal bellek adresiyle başarısızlık durumunda NULL adresle geri dönmektedir. Çekirdek modülleri ve aygıt sürücüler dinamik tahsisat başarısız olursa tipik olarak "-ENOMEM" değerine geri dönmelidir. kfree fonksiyonu ise daha önce kmalloc ile tahsis edilmiş olan alanın başlangıç adresini parametre olarak almaktadır. Aşağıda daha önce yapmış olduğumuz boru aygıt sürücüsündeki kuyruk sistemini kmalloc fonksiyonu ile tahsis edilip kfree fonksiyonu ile serbest bırakılmasına örnek verilmiştir. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); struct QUEUE { unsigned char pipebuf[PIPE_BUFSIZE]; size_t head; size_t tail; size_t count; }; static struct QUEUE *g_queue; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } if ((g_queue = (struct QUEUE *)kmalloc(sizeof(struct QUEUE), GFP_KERNEL)) == NULL) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { kfree(g_queue); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_queue->count == 0) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqread, g_queue->count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_queue->count, size); if (g_queue->tail <= g_queue->head) size1 = MIN(PIPE_BUFSIZE - g_queue->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_queue->pipebuf + g_queue->head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_queue->pipebuf, size2) != 0) return -EFAULT; g_queue->head = (g_queue->head + esize) % PIPE_BUFSIZE; g_queue->count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_queue->count < size) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_queue->count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_queue->count, size); if (g_queue->tail >= g_queue->head) size1 = MIN(PIPE_BUFSIZE - g_queue->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_queue->pipebuf + g_queue->tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_queue->pipebuf, buf + size1, size2) != 0) return -EFAULT; g_queue->tail = (g_queue->tail + esize) % PIPE_BUFSIZE; g_queue->count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len; if ((fd = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Yukarıda da belirttiğimiz gibi istersek genel amaçlı kmalloc fonksiyonunu kullanmak yerine kendimiz tam istediğimiz büyüklükte dilimlere sahip olan yeni bir dilimli tahsisat sistemi yaratıp onu kullanabiliriz. Yeni bir dilimli tahsisat sisteminin yaratılması kmem_cache_create fonksiyonu ile yapılmaktadır. Fonksiyonun prototipi şöyledir: #include struct kmem_cache *kmem_cache_create( const char *name, unsigned int size, int align, slab_flags_t flags, void (*ctor)(void *) ); Fonksiyonun birinci parametresi yeni yaratılacak dilim sisteminin ismini belirtmektedir. Herhangi bir isim verilebilir. İkinci parametre dilimlerin büyüklüğünü belirtmektedir. Üçücnü parametre hizalama değerini belirtir. Bu parametre 0 geçilirse default hizalama kullanılır. Fonksiyonun dördüncü parametresi yaratılacak dilim sistemine ilişkin bazı özelliklerin belirlenmesi için kullanılmaktadır. Buradaki bayrakların önemli birkaç tanesi şöyledir: -> SLAB_NO_REAP: Fiziksel RAM'in dolması nedeniyle kullanılmayan dilimlerin otomatik olarak sisteme iade edileceği anlamına gelir. Uç durumlarda bu bayrak kulanılabilir. -> SLAB_HWCACHE_ALIGN: Bu bayrak özellikle SMP sistemlerinde işlemci ya da çekirdeklerin cache alanları için hizalama yapılmasının sağlamaktadır. Yaratım sırasında bu parametreyi kullanabilirsiniz. -> SLAB_CACHE_DMA: Bu parametre DMA alanında (DMA zone) tahsisat için kullanılmaktadır. Fonksiyonun son parametresi dilim sistemi yaratıldığında çağrılacak callback fonksiyonu belirtmektedir. Bu parametre NULL geçilebilir. Fonksiyon başarı durumunda kmem_cache_create fonksiyonu kmem_cache türünden bir yapı nesnesinin adresiyle başarısızlık durumunda NULL adrese geri dönmektedir. Başarısızlık durumunda aygıt sürücü fonksiyonunun -ENOMEM değeri ile geri döndürülmesi uygundur. Örneğin: if ((g_queue_cachep = kmem_cache_create("pipe-driver-cachep", sizeof(struct QUEUE), 0, SLAB_HWCACHE_ALIGN, NULL)) == NULL) { ... return -ENOMEM; } Yaratılmış olan bir dilim sisteminden tahsisatlar kmem_cache_alloc fonksiyonu ile yapılmaktadır. Fonksiyonun parametresi şöyledir: #include void *kmem_cache_alloc(kmem_cache_t *cache, int flags); Fonksiyonun birinci parametresi yaratılmış olan dilim sisteminin handle değerini, ikinci parametresi yaratım bayraklarını almaktadır. Bu bayraklar kmalloc fonksiyonundaki bayraklarla aynıdır. Yani örneğin bu parametreye GFP_KERNEL geçilebilir. Fonksiyon başarı durumunda tahsis edilen sanal adrese başarısızlık durumunda NULL adrese geri dönmektedir. Bu durumda aygıt sürücüdeki fonksiyonun -ENOMEM değeri ile geri döndürülmesi uygundur. Örneğin: if ((g_queue = (struct QUEUE *)kmem_cache_alloc(g_queue_cachep, GFP_KERNEL)) == NULL) { ... return -ENOMEM; } kmem_cache_alloc fonksiyonu ile tahsis edilen dinamik alan kmem_cache_free fonksiyonu ile serbest bırakılabilir. Fonksiyonun prototipi şöyledir: #include void kmem_cache_free(kmem_cache_t *cache, const void *obj); Fonksiyonun birinci parametresi dilim sisteminin handle değerini, ikincisi parametresi ise serbest bırakılacak dilimin adresini belirtmektedir. Örneğin: kmem_cache_free(g_queue_cachep, g_queue); kmem_cache_create fonksiyonu ile yaratılmış olan dilim sistemi kmem_cache_destroy fonksiyonu ile serbest bırakılabilir. Fonksiyonun prototipi şöyledir. #include int kmem_cache_destroy(kmem_cache_t *cache); Fonksiyon dilim sisteminin handle değerini parametre olarak alır. Başarı durumunda 0 değerine başarısızlık durumunda negatif errno değerine geri döner. Örneğin: kmem_cache_destroy(g_queue_cachep); Pekiyi kmalloc yerine yeni bir dilimli tahsisat sisteminin yaratılması tercih edilmeli midir? Yukarıda da belirttiğimiz gibi kmalloc fonksiyonu da aslında önceden yaratılmış belli uzunluktaki dilim sistemlerinden tahsisat yapmaktadır. Ancak "çok sayıda aynı büyüklükte alanların" tahsis edildiği durumlarda programcının talep ettiği uzunlukta kendi dilim sistemini yaratması tavsiye edilebilir. Bunun dışında genel amaçlı kmalloc fonksiyonu tercih edilebilir. Örneğin boru aygıt sürücümüzde yeni bir dilim sisteminin yaratılmasına hiç gerek yoktur. Ancak bir aşağıda örnek vermek amacıyla boru aygıt sürücünde yeni bir dilim sistemi yarattık. Örneği inceleyiniz. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); struct QUEUE { unsigned char pipebuf[PIPE_BUFSIZE]; size_t head; size_t tail; size_t count; }; static struct QUEUE *g_queue; struct kmem_cache *g_queue_cachep; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } if ((g_queue_cachep = kmem_cache_create("pipe-driver-cachep", sizeof(struct QUEUE), 0, SLAB_HWCACHE_ALIGN, NULL)) == NULL) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } if ((g_queue = (struct QUEUE *)kmem_cache_alloc(g_queue_cachep, GFP_KERNEL)) == NULL) { kmem_cache_destroy(g_queue_cachep); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { kmem_cache_free(g_queue_cachep, g_queue); kmem_cache_destroy(g_queue_cachep); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_queue->count == 0) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqread, g_queue->count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_queue->count, size); if (g_queue->tail <= g_queue->head) size1 = MIN(PIPE_BUFSIZE - g_queue->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_queue->pipebuf + g_queue->head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_queue->pipebuf, size2) != 0) return -EFAULT; g_queue->head = (g_queue->head + esize) % PIPE_BUFSIZE; g_queue->count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_queue->count < size) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_queue->count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_queue->count, size); if (g_queue->tail >= g_queue->head) size1 = MIN(PIPE_BUFSIZE - g_queue->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_queue->pipebuf + g_queue->tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_queue->pipebuf, buf + size1, size2) != 0) return -EFAULT; g_queue->tail = (g_queue->tail + esize) % PIPE_BUFSIZE; g_queue->count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); Aygıt sürücünün majör ve minör numaraları ne anlam ifade etmektedir? Majör numara aygıt sürücünün türünü belirtir. Minör numara ise aynı türden aygıt sürücülerin farklı örneklerini (instance'larını) belirtmektedir. Örneğin biz yukarıdaki "pipe-driver" aygıt sürücümüzün tek bir boruyu değil on farklı boruyu idare etmesini isteyebiliriz. Bu durumda aygıt sürücümüzün bir tane majör numarası ancak 10 tane minör numarası olacaktır. Aygıt sürücülerin majör numaraları aynı ise bunların kodları da aynıdır. O aynı kod birden fazla aygıt için işlev görmektedir. Örneğin seri portu kontrol eden bir aygıt sürücü söz konusu olsun. Ancak bilgissayarımızda dört seri port olsun. İşte bu durumda bu seri porta ilişkin aygıt dosyalarının hepsinin majör numaraları aynıdır. Ancak minör numaraları farklıdır. Ya da örneğin terminal aygıt sürücüsü bir tanedir. Ancak bu aygıt sürücü birden fazla terminali yönetebilmektedir. O halde her terminale ilişkin aygıt dosyasının majör numaraları aynı minör numaraları farklı olacaktır. Örneğin: /dev$ ls -l tty1 tty2 tty3 tty4 tty5 crw--w---- 1 root tty 4, 1 Haz 2 15:05 tty1 crw--w---- 1 root tty 4, 2 Haz 2 15:05 tty2 crw--w---- 1 root tty 4, 3 Haz 2 15:05 tty3 crw--w---- 1 root tty 4, 4 Haz 2 15:05 tty4 crw--w---- 1 root tty 4, 5 Haz 2 15:05 tty5 Pekiyi birden fazla aygıtı yönetecek (yani birden fazla minör numaraya sahip olan) bir aygıt sürücü nasıl yazılabilir? Her şeyden önce birden fazla minör numara kullanan aygıt sürücüleri yazarken dikkatli olmak gerekir. Çünkü tek bir kod birden fazla aynı türden bağımsız aygıtı idare edecektir. Dolayısıyla bu tür durumlarda bazı nesnelerin senkronize edilmesi gerekebilir. Birden fazla minör numara üzerinde çalışacak aygıt sürücüleri tipik olarak şöyle yazılmaktadır: -> Minör numara sayısının aşağıdaki gibi ndevices isimli parametre yoluyla komut satırından aşağıdaki gibi aygıt sürücüye aktarıldığını varsayacağız: #define NDEVICES 10 ... static int ndevices = NDEVICES; module_param(ndevices, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); Programcının majör ve minör numaraları tahsis etmesi gerekir. Yukarıda da yaptığımız gibi majör numara alloc_chrdev_region fonksiyonuyla dinamik olarak belirlenebilmektedir. Bu fonksiyon aynı zamanda belli bir minör numaradan başlayarak n tane minör numarayı da tahsis edebilmektedir. Örneğin: if ((result = alloc_chrdev_region(&g_dev, 0, ndevices, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } Burada 0'ıncı minör numaradan ndevices tane minör numara için aygıt tahsisatı yapılmıştır. -> Her aygıt bir yapıyla temsil edilmelidir. Bunun için N elemanlı bir yapı dizisi yaratabilirsiniz. Bu dizi global düzeyde tanımlanabileceği gibi kmalloc fonksiyonuyla dinamik biçimde de tahsis edilebilir. Oluşturulan bu yapının içerisine struct cdev nesnesi de eklenmelidir. Örneğin: struct PIPE_DEVICE { unsigned char pipebuf[PIPE_BUFSIZE]; size_t head; size_t tail; size_t count; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; static struct PIPE_DEVICE *g_pdevices; ... if ((g_pdevices = (struct PIPE_DEVICE *)kmalloc(sizeof(struct PIPE_DEVICE) * ndevices, GFP_KERNEL)) == NULL) { unregister_chrdev_region(g_dev, ndevices); return -ENOMEM; } Burada görüldüğü gibi her farklı borunun farklı bekleme kuyrukları ve semaphore nesnesi vardır. cdev yapı nesnesinin yapının içerisine yerleştirilmesinin amacı şleride görüleceği gibi bu adresten hareketle yapı nesnesinin adresinin elde edilmesini sağlamaktır. Bunun nasıl yapıldığı izleyen pragraflarda görülecektir. -> N tane minör numaralı aygıt için cdev_add fonksiyonuyla aygıtlar çekirdeğe eklenmelidir. Örneğin: for (i = 0; i < ndevices; ++i) { g_pdevices[i].head = g_pdevices[i].tail = g_pdevices[i].count = 0; sema_init(&g_pdevices[i].sem, 1); init_waitqueue_head(&g_pdevices[i].wqread); init_waitqueue_head(&g_pdevices[i].wqwrite); cdev_init(&g_pdevices[i].cdev, &g_fops); dev = MKDEV(MAJOR(g_dev), i); if ((result = cdev_add(&g_pdevices[i].cdev, dev, 1)) < 0) { for (k = 0; k < i; ++k) cdev_del(&g_pdevices[i].cdev); kfree(g_pdevices); unregister_chrdev_region(dev, ndevices); printk(KERN_ERR "Cannot add device!...\n"); return result; } } Burada yapı dizisininin her elemanındaki elemanlara ilkdeğerleri verilmiştir. Sonra her boru için ayrı bir cdev nesnesi cdev_add fonksiyonu ile eklenmiştir. Eklemelerden biri başarısız olursa daha önce eklenenlerin de cdev_del fonksiyonu ile silindiğine dikkat ediniz. -> Bizim read, write gibi fonksiyonlarında file yapısı türünden adres belirten filp parametre değişkeni yoluyla DEVICE yapısına erişmemiz gerekir. Bu işlem dolaylı bir biçimde şöyle yapılmaktadır: -> Önce aygıt sürücünün open fonksiyonunda programcı inode yapısının i_cdev elemanından hareketle cdev nesnesinin içinde bulunduğu yapı nesnesinin başlangıç adresini container_of makrosuyla elde eder. Çünkü inode yapısının i_cdev elemanı cdev_add fonkisyonuyla eklenen cdev yapı nesnesinin adresini tutmaktadır. -> Programcı device nesnesinin adresini elde ettikten sonra onu file yapısının private_data elemanına yerleştirir. file yapısının private_data elemanı programcının kendisinin isteğe bağlı olarak yerleştirebileceği bilgiler için bulundurulmuştur. Bu işlemler aşağıdaki gibi yapılabilir: static int generic_open(struct inode *inodep, struct file *filp) { struct PIPE_DEVICE *pdevice; pdevice = container_of(inodep->i_cdev, struct PIPE_DEVICE, cdev); filp->private_data = pdevice; printk(KERN_INFO "pipe-driver opened...\n"); return 0; } -> Aygıt sürücünün read ve write fonksiyonları yazılır. -> release (close) işleminde yapılacak birtakım son işlemler varsa yapılır. -> Aygıt sürücünün exit fonksiyonunda yine tüm minör numaralar için cdev_del fonksiyonu çağrılır ve unregister_chrdev_region işlemi yapılır. -> Birden fazla minör numara için çalışacak aygıt sürücülerin birden fazla aygıt dosdyası yaratması gerekir. Yani aygıt sürücüsü kaç minör numarayı destekliyorsa o sayıda aygıt dıosyalarının yaraılması gerekmektedir. Bu da onları yüklemek için kullandığımız load scriptinde değişiklik yapmayı gerektirmektedir. N tane minör numaraya ilişkin aygıt dosyası yaratacak biçimde yeni bir "loadmulti" isimli script aşağıdaki gibi yazılabilir: #!/bin/bash module=$2 mode=666 /sbin/insmod ./${module}.ko ${@:3} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) for ((i = 0; i < $1; ++i)) do rm -f ${module}$i mknod ${module}$i c $major $i chmod $mode ${module}$i done Buradaki "loadmulti" script'i iki komut satırı argümanıyla aşağıdaki örnekteki gibi çalıştırılmalıdır: $ sudo ./loadmulti 10 pipe-driver ndevices=10 Burada "loadmulti" script'i hem aygıt sürücüyü yükleyecek hem de pipe-driver0, pipe-driver1, ..., pipedriver9 biçiminde aygıt dosyalarını yaratacaktır. Aşağıda yaratılmış olan örnek aygıt dosyalarına dikkat ediniz: crw-rw-rw- 1 root root 236, 0 Haz 7 22:09 pipe-driver0 crw-rw-rw- 1 root root 236, 1 Haz 7 22:09 pipe-driver1 crw-rw-rw- 1 root root 236, 2 Haz 7 22:09 pipe-driver2 crw-rw-rw- 1 root root 236, 3 Haz 7 22:09 pipe-driver3 crw-rw-rw- 1 root root 236, 4 Haz 7 22:09 pipe-driver4 crw-rw-rw- 1 root root 236, 5 Haz 7 22:09 pipe-driver5 crw-rw-rw- 1 root root 236, 6 Haz 7 22:09 pipe-driver6 crw-rw-rw- 1 root root 236, 7 Haz 7 22:09 pipe-driver7 crw-rw-rw- 1 root root 236, 8 Haz 7 22:09 pipe-driver8 crw-rw-rw- 1 root root 236, 9 Haz 7 22:09 pipe-driver9 Aygıt dosyalarının majör numaralarının hepsi aynıdır ancak minör numaraları farklıdır. Burada adeta birbirinden bağımsız 10 ayrı boru aygıtı var gibidir. Ancak aslında tek bir aygıt sürücü kodu bulunmaktadır. Tabii bizim benzer biçimde "unload" script'ini de tüm aygıt dosyalarını silecek biçimde düzeltmemiz gerekir. Bunun için "unloadmulti" scritp'ini aşağıdaki yazabiliriz: #!/bin/bash module=$2 mode=666 /sbin/rmmod ./$module.ko || exit 1 for ((i = 0; i < $1; ++i)) do rm -f ${module}$i done Bu scrip'te biz modülü önce çekirdekten sonra da "loadmulti" ile yarattığımız aygıt dosyalarını dosya sisteminden sildik. Script aşağıdaki örnekteki gibi kullanılmalıdır: sudo ./unloadmulti 10 pipe-driver Daha önce yapımış olduğumuz boru aygıt sürücücüsünün 10 farklı minör numarayı destekleyen biçimini aşağıda veriyoruz. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define NDEVICES 10 #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; struct PIPE_DEVICE { unsigned char pipebuf[PIPE_BUFSIZE]; size_t head; size_t tail; size_t count; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; static dev_t g_dev; static struct PIPE_DEVICE *g_pdevices; static int ndevices = NDEVICES; module_param(ndevices, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); static int __init generic_init(void) { int result; dev_t dev; int i, k; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, ndevices, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_pdevices = (struct PIPE_DEVICE *)kmalloc(sizeof(struct PIPE_DEVICE) * ndevices, GFP_KERNEL)) == NULL) { unregister_chrdev_region(g_dev, ndevices); return -ENOMEM; } for (i = 0; i < ndevices; ++i) { g_pdevices[i].head = g_pdevices[i].tail = g_pdevices[i].count = 0; sema_init(&g_pdevices[i].sem, 1); init_waitqueue_head(&g_pdevices[i].wqread); init_waitqueue_head(&g_pdevices[i].wqwrite); cdev_init(&g_pdevices[i].cdev, &g_fops); dev = MKDEV(MAJOR(g_dev), i); if ((result = cdev_add(&g_pdevices[i].cdev, dev, 1)) < 0) { for (k = 0; k < i; ++k) cdev_del(&g_pdevices[i].cdev); kfree(g_pdevices); unregister_chrdev_region(dev, ndevices); printk(KERN_ERR "Cannot add device!...\n"); return result; } } return 0; } static void __exit generic_exit(void) { int i; for (i = 0; i < ndevices; ++i) cdev_del(&g_pdevices[i].cdev); kfree(g_pdevices); unregister_chrdev_region(g_dev, ndevices); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { struct PIPE_DEVICE *pdevice; pdevice = container_of(inodep->i_cdev, struct PIPE_DEVICE, cdev); filp->private_data = pdevice; printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; while (pdevice->count == 0) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqread, pdevice->count > 0)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->count, size); if (pdevice->tail <= pdevice->head) size1 = MIN(PIPE_BUFSIZE - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, pdevice->pipebuf + pdevice->head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, pdevice->pipebuf, size2) != 0) return -EFAULT; pdevice->head = (pdevice->head + esize) % PIPE_BUFSIZE; pdevice->count -= esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - pdevice->count < size) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqwrite, PIPE_BUFSIZE - pdevice->count >= size)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - pdevice->count, size); if (pdevice->tail >= pdevice->head) size1 = MIN(PIPE_BUFSIZE - pdevice->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(pdevice->pipebuf + pdevice->tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(pdevice->pipebuf, buf + size1, size2) != 0) return -EFAULT; pdevice->tail = (pdevice->tail + esize) % PIPE_BUFSIZE; pdevice->count += esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 mode=666 /sbin/insmod ./${module}.ko ${@:3} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) for ((i = 0; i < $1; ++i)) do rm -f ${module}$i mknod ${module}$i c $major $i chmod $mode ${module}$i done /* unloadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 mode=666 /sbin/rmmod ./$module.ko || exit 1 for ((i = 0; i < $1; ++i)) do rm -f ${module}$i done /* prog1.c */ #include #include #include #include #include #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len; if ((fd = open("pipe-driver5", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver5", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aygıt sürücüden bilgi okumak için "read" fonksiyonun, aygıt sürücüye bilgi göndermek için ise "write" fonksiyonun kullanıldığını gördük. Ancak bazen aygıt sürücüye "write" fonksiyonunu kullanmadan bazı bilgilerin gönderilmesi, aygıt sürücüden "read" fonksiyonunu kullanmadan bazı bilgilerin alınması gerekebilmektedir. Bazen hiç bilgi okumadan ve bilgi göndermeden aygıt sürüceden bazı şeyleri yapmasını da isteyebiliriz. Bu tür bazı işlemlerin "read" ve "write" fonksiyonlarıyla yaptırılması mümkün olsa bile kullanışsızdır. Örneğin yukarıdaki boru aygıt sürücümüzde "(pipe-driver)" biz aygıt sürücüden kullandığı "FIFO" alanın uzunluğunu isteyebiliriz ya da bu alanın boyutunu değiştirmek isteyebiliriz. Bu işlemleri "read" ve "write" fonksiyonlarıyla yapmaya çalışsak aygıt sürücümüz sanki boruyu temsil eden kuyruktan okuma yazma yapmak istediğimizi sanacaktır. Tabii yukarıda da belirttiğimiz gibi zorlanırsa bu tür işlemler "read" ve "write" fonksiyonlarıyla yine de yapılabilir. Ancak böyle bir kullanımım mümkün hale getirilmesi ve "user mode" tan kullanılması oldukça zor olacaktır. İşte aygıt sürücüye komut gönderip ondan bilgi almak için genel amaçlı "ioctl" isminde özel bir POSIX fonksiyonu bulundurulmuştur. Linux sistemlerinde "ioctl" fonksiyonu "sys_ioctl" isimli sistem fonksiyonunu çağırmaktadır. "ioctl" fonksiyonunun parametrik yapısı şöyledir: #include int ioctl(int fd, unsigned long request, ...); Fonksiyonun birinci parametresi aygıt sürücüye ilişkin dosya betimleyicisini belirtir. İkinci parametre ileride açıklanacak olan komut kodudur. Programcı aygıt sürücüsünde farklı komutlar için farklı komut kodları (yani numaralar) oluşturur. Sonra bu komut kodlarını "switch" içerisine sokarak hangi numaralı istekte bulunulmuşsa ona yönelik işlemleri yapar. "ioctl" fonksiyonu iki parametreyle ya da üç parametreyle kullanılmaktadır. Yani fonksyonun üçüncü parametresi isteğe bağlıdır. Eğer bir veri transferi söz konusu değilse "ioctl" genellikle iki argümanla çağrılır. Ancak bir veri transferi söz konusu ise "ioctl" üç argümanla çağrılmalıdır. Bu durumda üçüncü argüman "user mode" taki transfer adresini belirtir. Tabii aslında bu üçüncü parametrenin veri transferi ile ilgili olması dolayısıyla da bir adres belirtmesi zorunlu değildir. "ioctl" fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. "errno" uygun biçimde set edilmektedir. "User mode" dan bir program aygıt sürücü için "ioctl" fonksiyonunu çağırdığında akış "user mode" dan kernel moda geçer ve aygıt sürücüdeki "file_operations" yapısının "unlocked_ioctl" elemanında belirtilen fonksiyon çağrılır. Bu fonksiyonun parametrik yapısı şöyle olmalıdır: long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); Fonksiyonun birinci parametresi yine dosya nesnesinin adresini, ikinci parametresei "ioctl" fonksiyonunda kullanılan komut kodunu (yani "ioctl" fonksiyonuna geçirilen ikinci argümanı) ve üçüncü parametresi de ek argümanı (yani "ioctl" fonksiyonuna geçirilen üçüncü argümanı) belirtmektedir. Tabii programcının eğer "ioctl" fonksiyonu iki argümanlı çağrılmışsa bu üçüncü parametreye erişmemesi gerekir. Bu fonksiyon başarı durumunda 0 değerine başarısızlık durumunda negatif hata koduna geri dönmelidir. Fakat bazen programcı doğrudan iletilecek değeri geri dönüş değeri biçiminde oluşturabilir. Bu durumda geri dönüş değeri pozitif değer olabilir. Örneğin: static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); ... static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release, .unlocked_ioctl = generic_ioctl }; "ioctl" işleminde "ioctl" fonksiyonunun ikinci parametresi olan kontrol kodu dört parçanın bit düzeyinde birleştirilmesiyle oluşturulmaktadır. Bu parçaların belli bir uzunlukları vardır. Ancak bu parçalara ilişkin bitlerin 32 bit içerisinde belli pozisyonlara yerleştirilmesini kolaylaştırmak için "_IOC" isimli bir makro bulundurulmuştur. Bu makronun parametreleri şöyledir: _IOC(dir, type, nr, size) Bu makro buradaki parçaları bit düzeyinde birleştirerek bir 4 byte'lık bir tamsayı biçiminde vermektedir. Makronun parametrelerini oluşturan dört parçanın anlamları ve bit uzunlukları şöyledir: -> dir (direction): Bu 2 bitlik bir alandır ([30, 31] bitler). Burada kullanılacak sembolik sabitler "_IOC_NONE", "_IOC_READ", "_IOC_WRITE", "_IOC_READ|_IOC_WRITE" biçimindedir. Buradaki "_IOC_READ" aygıt sürücüden bilgi alınacağını "_IOC_WRITE" ise aygıt sürücüye bilgi gönderileceğini belirtmektedir. Buradaki yön "ioctl" sistem fonksiyonu tarafından dosyanın açış moduyla kontrol edilmemektedir. Örneğin biz buradaki yönü "_IOC_READ|_IOC_WRITE" biçiminde vermiş olsak bile dosyası "O_RDONLY" modunda açıp bu ioctl işlemini yapabiliriz. Eğer programcı böyle bir kontrol yapmak istiyorsa aygıt sürücünün "ioctl" fonksiyonu içerisinde bu kontrolü yapabilir. -> type: Bu 8 bitlik bir alandır ([8, 15] bitleri). Bu alana aygıt sürücüyü yazan istediği herhangi bir byte'ı verebilir. Genellikle bu byte bir akarkter sabiti olarak verilmektedir. Buna "magic number" da denilmektedir. -> nr: Bu 8 bitlik bir alandır ([0, 7] bitleri). Programcı tarafından kontrol koduna verilen sıra numarasını temsil etmektedir. Genellikle aygıt sürücü programcıları 0'dan başlayarak her koda bir numara vermektedir. -> size: Bu 14 bitlik bir alandır ([16:29] bitleri). Bu alan kaç byte'lık bir transferin yapılacağını belirtmektedir. Buradaki "size" değeri aslında çekirdek tarafından kullanılmamaktadır. Dolayısıyla biz 14 bitten daha büyük transferleri de yapabiliriz. Kullanım kolaylığı sağlamak için genellikle "_IOC" makrosu bir sembolik sabit biçiminde define edilir. Örneğin: #define PIPE_MAGIC 'x' #define IOC_PIPE_GETBUFSIZE _IOC(_IOC_READ, PIPE_MAGIC, 0, 4) Aslında "_IOC" makrosundan daha kolay kullanılabilen aşağıdaki makrolar da oluşturulmuştur: #ifndef __KERNEL__ #define _IOC_TYPECHECK(t) (sizeof(t)) #endif #define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size))) #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) Bu makrolarda "_IOC" makrosunun birinci parametresinin artık belirtilmediğine dikkat ediniz. Çünkü makrolar zaten isimlerine göre "_IOC" makrosunun birinci parametresini kendisi oluşturmaktadır. Ayrıca artık uzunluk (size parametresi) byte olarak değil tür olarak belirtilmelidir. Makrolar bu türleri "sizeof" operatörüne kendisi sokmaktadır. Görüldüğü gibi, -> "_IO" makrosu veri transferinin söz konusu olmadığı durumda, -> "_IOR" aygıt sürücüden okuma yapıldığı durumda, -> "_IOW" aygıt sürücüye yazma yapıldığı durumda, -> "_IOWR" ise aygıt sürücüden hem okuma hem de yazma yapıldığı durumlarda kullanılmaktadır. Örneğin: #define PIPE_MAGIC 'x' #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_MAGIC, 0, int) "ioctl" için kontrol kodları hem aygıt sürücünün içerisinden hem de "user mode" dan kullanılacağına göre ortak bir başlık dosyasının oluşturulması uygun olabilir. Burada "ioctl" kontrol kodları bulundurulabilir. Örneğin boru aygıt sürücümüz için "pipe-driver.h" dosyası açağıdaki gibi düzenlenebilir: // pipe-driver.h #ifndef PIPEDRIVER_H_ #define PIPEDRIVER_H_ #include #include #define PIPE_DRIVER_MAGIC 'p' #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_DRIVER_MAGIC, 0, size_t) #endif Aygıt sürücüdeki ioctl fonksiyonunu yazarken iki noktaya dikkat etmek gerekir: -> "ioctl" fonksiyonun üçüncü parametresi "unsigned long" türden olmasına karşın aslında genellikle "user mod" programcısı buraya bir nesnesin adresini geçirmektedir. Dolayısıyla bu transfer adresine aktarım gerekmektedir. Bunun için "copy_to_user", "copy_from_use", "put_user", "get_user" gibi "adresin geçerliliğini sorguladıktan sonra tranfers yapan fonksiyonlar" kullanılabilir. -> "User mod" programcısının olmayan bir komut kodu girmesi durumunda "ioctl" fonksiyonu "-ENOTTY" değeri ile geri döndürülmelidir. Bu tuhaf hata kodu ("TTY", "tele type terminal" sözcüklerinden kısaltmadır) tarihsel bir durumdan kaynaklanmaktadır. Bu hata kodu için "user mode" da "Inappropriate ioctl for device" biçiminde bir hata yazısı elde edilmektedir. User moddaki "ioctl" fonksiyonu başarı durumunda 0 değerine geri döndüğü için aygıt sürücüsündeki "ioctl" fonksiyonu da genel olarak başarı durumunda 0 ile geri döndürülmelidir. Yukarıda da belirttiğmiz gibi olmayan bir "ioctl" kodu için agıt sürücüdeki fonksiyonun "-ENOTTY" ile geri döndrülmesi uygundur. Bazı aygıt sürücülerinde başarı durumunda aygıt sürücüden bilgi "ioctl" fonksiyonunun üçüncü parametresi yoluyla değil geri dönüş değeri yoluyla elde edilmektedir. Bu durumda aygıt sürücüdeki "ioctl" fonksiyonu pozitif değerle de geri döndürülebilir. Ancak bu durum seyrektir. Biz transferin "ioctl" fonksiyonunun üçüncü parametresi yoluyla yapılmasını tavsiye ediyoruz. Aygıt sürücüdeki "ioctl" fonksiyonu tipik olarak bir "switch" deyimi ile gerçekleştirilmektedir. Örneğin, static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case IOC_PIPE_GETBUFSIZE: // ... break; default: return -ENOTTY; } return 0; } Burada "switch" deyiminin "default" bölümünde fonksiyonun "-NOTTY" değeri ile geri döndürüldüğüne dikkat ediniz. Tabii fonksiyon üçüncü parametresi ile belirtilen transfer adresi geçersiz bir adrezse yine "-EFAULT" değeri ile döndürülmelidir. Aşağıdaki örnekte boru yagıt sürücüsüsünün kullandığı boru uzunluğu "IOC_PIPE_GETBUFSIZE" "ioctl" koduyla elde edilip "IOC_PIPE_SETBUFSIZE" fonksiyomıyla değiştirilebilmektedir. Buradaki "IOCTL" kodları şöyle oluşturulmuştur: #define PIPE_DRIVER_MAGIC 'p' #define IOC_PIPE_GETCOUNT _IOR(PIPE_DRIVER_MAGIC, 0, size_t) #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_DRIVER_MAGIC, 1, size_t) #define IOC_PIPE_SETBUFSIZE _IOW(PIPE_DRIVER_MAGIC, 2, size_t) #define IOC_PIPE_PEEK _IOWR(PIPE_DRIVER_MAGIC, 3, struct PIPE_PEEK) Ancak bu örnek için yukarıda vermiş olduğumuz boru aygıt sürücüsünde bazı değişiklikler yaptık. Bu değişiklikler şunlardır: -> Artık borunun uzunluğu "PIPE_DEVICE" yapısının içerisinde tutulmaya başlanmıştır: struct PIPE_DEVICE { unsigned char *pipebuf; size_t head; size_t tail; size_t count; size_t bufsize; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; Buradaki "bufsize" elemanı ilgili borunun uzunluğunu belirtmektedir. Default uzunluk test için kolaylık sağlamak amacıyla yine 10 olarak tutulmuştur. Bu değişiklik sayesinde artık her minör numaraya ilişkin boru uzunluğu farklılaşabilecektir. -> Aygıt sürücümüz içerisindeki "ioctl" fonksiyonu aşağıdaki gibi yazılmıştır: static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct PIPE_DEVICE *pdevice; printk(KERN_INFO "ioctl"); pdevice = (struct PIPE_DEVICE *)filp->private_data; switch (cmd) { case IOC_PIPE_GETCOUNT: return put_user(pdevice->count, (size_t *)arg); case IOC_PIPE_GETBUFSIZE: return put_user(pdevice->bufsize, (size_t *)arg); case IOC_PIPE_SETBUFSIZE: return set_bufsize(pdevice, arg); case IOC_PIPE_PEEK: return read_peek(pdevice, arg); default: return -ENOTTY; } return 0; } Burada gördüğünüz gibi ilgili minör numaradaki borudaki byte sayısı "IOC_PIPE_GETCOUNT", uzunluğu ise "IOC_PIPE_GETBUFSIZE" ioctl kodu ile alınmakta ve bu boru uzunluğu uzunluk "IOC_PIPE_SETBUFSIZE" ioctl kodu ile değiştirilebilmektedir. Boru için kullanılan tampon değiştirilirken eşzamanlı erişimlere dikkat edilmelidir. Çünkü daha önceden de belirttiğimiz gibi aygıt sürücünün içerisindeki fonksiyonlar farklı prosesler tarafından aynı anda çağrılabilmektedir. Bu tür durumlarda daha önce görmüş olduğumuz çekirdek senkronizasyon nesneleri ile işlemlerin senktronize edilmesi gerekmektedir. Örneğimizdeki senkronizasyon "PIPE_DEVICE" yapısının içeriisndeki semaphore nesnesi yoluyla yapılmıştır. "IOC_PIPE_PEEK" "ioctl" kodu borudan atmadan okuma yapmakta kullanılmaktadır. Normal olarak borudan "read" fonksiyonu ile okuma yapıldığında okunanlar borudan atılmaktadır. Ancak bu "ioctl" kodu ile borudan okuma yapıldığında okunanlar borudan atılmamaktadır. Bu "ioctl" kodu için aşağıdaki gibi bir yapı oluşturulmuştur: struct PIPE_PEEK { size_t size; void *buf; }; Yapının "size" elemanı kaç byte "peek" işleminin yapılacağını, "buf" elemanı ise "peek" edilen byte'ların yerleştirileceği adresi belirtmektedir. Tabii boruda mevcut olan byte sayısından daha fazla byte "peek" edilmek istenirse boruda olan kadar byte "peek" edilmektedir. "Peek" edilen byte sayısı aygıt sürücü tarafından yapının size elemanına aktarılmaktadır. Aygıt sürücümüzü yine "loadmulti" script'i ile aşağıdaki gibi yükleyebilirsiniz: $ sudo ./loadmulti 10 pipe-driver ndevices=10 Aygıt sürücünün çekirdekten atılması da yine "unloadmulti" script'i ile yapılabilir: $ sudo ./unloadmulti 10 pipe-driver Aşağıda örneğin tüm modlarını veriyoruz. * Örnek 1, /* pipe-driver.h */ #ifndef PIPEDRIVER_H_ #define PIPEDRIVER_H_ #include #include struct PIPE_PEEK { size_t size; void *buf; }; #define PIPE_DRIVER_MAGIC 'p' #define IOC_PIPE_GETCOUNT _IOR(PIPE_DRIVER_MAGIC, 0, size_t) #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_DRIVER_MAGIC, 1, size_t) #define IOC_PIPE_SETBUFSIZE _IOW(PIPE_DRIVER_MAGIC, 2, size_t) #define IOC_PIPE_PEEK _IOWR(PIPE_DRIVER_MAGIC, 3, struct PIPE_PEEK) #endif /* pipe-driver.c */ #include #include #include #include #include #include #include #include #include "pipe-driver.h" #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define NDEVICES 10 #define DEF_PIPE_BUFSIZE 10 #define MAX_PIPE_BUFSIZE 131072 /* 128K */ MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release, .unlocked_ioctl = generic_ioctl }; struct PIPE_DEVICE { unsigned char *pipebuf; size_t head; size_t tail; size_t count; size_t bufsize; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; static int set_bufsize(struct PIPE_DEVICE *pdevice, unsigned long arg); static int read_peek(struct PIPE_DEVICE *pdevice, unsigned long arg); static dev_t g_dev; static struct PIPE_DEVICE *g_pdevices; static int ndevices = NDEVICES; module_param(ndevices, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); static int __init generic_init(void) { int result; dev_t dev; int i, k; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, ndevices, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_pdevices = (struct PIPE_DEVICE *)kmalloc(sizeof(struct PIPE_DEVICE) * ndevices, GFP_KERNEL)) == NULL) { unregister_chrdev_region(g_dev, ndevices); return -ENOMEM; } for (i = 0; i < ndevices; ++i) { g_pdevices[i].head = g_pdevices[i].tail = g_pdevices[i].count = 0; g_pdevices[i].bufsize = DEF_PIPE_BUFSIZE; sema_init(&g_pdevices[i].sem, 1); init_waitqueue_head(&g_pdevices[i].wqread); init_waitqueue_head(&g_pdevices[i].wqwrite); cdev_init(&g_pdevices[i].cdev, &g_fops); dev = MKDEV(MAJOR(g_dev), i); g_pdevices[i].pipebuf = (char *)kmalloc(DEF_PIPE_BUFSIZE, GFP_KERNEL); result = cdev_add(&g_pdevices[i].cdev, dev, 1); if (g_pdevices[i].pipebuf == NULL || result < 0) { if (g_pdevices[i].pipebuf != NULL) kfree(g_pdevices[i].pipebuf); for (k = 0; k < i; ++k) { cdev_del(&g_pdevices[k].cdev); kfree(g_pdevices[k].pipebuf); } kfree(g_pdevices); unregister_chrdev_region(dev, ndevices); printk(KERN_ERR "Cannot add device!...\n"); return result; } } return 0; } static void __exit generic_exit(void) { int i; for (i = 0; i < ndevices; ++i) cdev_del(&g_pdevices[i].cdev); kfree(g_pdevices); unregister_chrdev_region(g_dev, ndevices); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { struct PIPE_DEVICE *pdevice; pdevice = container_of(inodep->i_cdev, struct PIPE_DEVICE, cdev); filp->private_data = pdevice; printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; while (pdevice->count == 0) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqread, pdevice->count > 0)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->count, size); if (pdevice->tail <= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, pdevice->pipebuf + pdevice->head, size1) != 0) { up(&pdevice->sem); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, pdevice->pipebuf, size2) != 0) { up(&pdevice->sem); return -EFAULT; } pdevice->head = (pdevice->head + esize) % pdevice->bufsize; pdevice->count -= esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; if (size > pdevice->bufsize) size = pdevice->bufsize; while (pdevice->bufsize - pdevice->count < size) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqwrite, pdevice->bufsize - pdevice->count >= size)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->bufsize - pdevice->count, size); if (pdevice->tail >= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(pdevice->pipebuf + pdevice->tail, buf, size1) != 0) { up(&pdevice->sem); return -EFAULT; } if (size2 != 0) if (copy_from_user(pdevice->pipebuf, buf + size1, size2) != 0) { up(&pdevice->sem); return -EFAULT; } pdevice->tail = (pdevice->tail + esize) % pdevice->bufsize; pdevice->count += esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqread); return esize; } static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct PIPE_DEVICE *pdevice; printk(KERN_INFO "ioctl"); pdevice = (struct PIPE_DEVICE *)filp->private_data; switch (cmd) { case IOC_PIPE_GETCOUNT: return put_user(pdevice->count, (size_t *)arg); case IOC_PIPE_GETBUFSIZE: return put_user(pdevice->bufsize, (size_t *)arg); case IOC_PIPE_SETBUFSIZE: return set_bufsize(pdevice, arg); case IOC_PIPE_PEEK: return read_peek(pdevice, arg); default: return -ENOTTY; } return 0; } static int set_bufsize(struct PIPE_DEVICE *pdevice, unsigned long arg) { char *new_pipebuf; size_t size; if (arg > MAX_PIPE_BUFSIZE) return -EINVAL; if (arg <= pdevice->count) return -EINVAL; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; if ((new_pipebuf = (char *)kmalloc(arg, GFP_KERNEL)) == NULL) { up(&pdevice->sem); return -ENOMEM; } if (pdevice->count != 0) { if (pdevice->tail <= pdevice->head) { size = pdevice->bufsize - pdevice->head; memcpy(new_pipebuf, pdevice->pipebuf + pdevice->head, size); memcpy(new_pipebuf + size, pdevice->pipebuf, pdevice->count - size); } else memcpy(new_pipebuf, pdevice->pipebuf + pdevice->head, pdevice->count); } pdevice->head = 0; pdevice->tail = pdevice->count; kfree(pdevice->pipebuf); pdevice->pipebuf = new_pipebuf; pdevice->bufsize = arg; up(&pdevice->sem); return 0; } static int read_peek(struct PIPE_DEVICE *pdevice, unsigned long arg) { size_t esize, size1, size2; struct PIPE_PEEK *userpp = (struct PIPE_PEEK *)arg; struct PIPE_PEEK pp; int status = 0; if (copy_from_user(&pp, userpp, sizeof(struct PIPE_PEEK)) != 0) return -EFAULT; if (pp.size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; esize = MIN(pdevice->count, pp.size); if (pdevice->tail <= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(pp.buf, pdevice->pipebuf + pdevice->head, size1) != 0) { status = -EFAULT; goto EXIT; } if (size2 != 0) if (copy_to_user(pp.buf + size1, pdevice->pipebuf, size2) != 0) { status = -EFAULT; goto EXIT; } if (put_user(esize, &userpp->size) != 0) status = -EFAULT; EXIT: up(&pdevice->sem); return status; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 mode=666 /sbin/insmod ./${module}.ko ${@:3} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) for ((i = 0; i < $1; ++i)) do rm -f ${module}$i mknod ${module}$i c $major $i chmod $mode ${module}$i done /* unloadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 /sbin/rmmod ./$module.ko || exit 1 for ((i = 0; i < $1; ++i)) do rm -f ${module}$i done /* prog1.c */ #include #include #include #include #include #include #include "pipe-driver.h" #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len, bufsize, new_bufsize; if ((fd = open("pipe-driver5", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if (buf[0] == '!') { new_bufsize = atoi(&buf[1]); printf("%zd\n", new_bufsize); if (ioctl(fd, IOC_PIPE_SETBUFSIZE, new_bufsize) == -1) exit_sys("ioctl"); if (ioctl(fd, IOC_PIPE_GETBUFSIZE, &bufsize) == -1) exit_sys("ioctl"); printf("new pipe buffer size is %zu\n", bufsize); } else { len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #include #include "pipe-driver.h" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int count, size; ssize_t result; struct PIPE_PEEK pp; char *peekbuf; if ((pdriver = open("pipe-driver5", O_RDONLY)) == -1) exit_sys("open"); for (;;) { if (ioctl(pdriver, IOC_PIPE_GETCOUNT, &count) == -1) exit_sys("ioctl"); printf("There are (is) %d byte(s) in the pipe\n", count); printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if (size < 0) { pp.size = -size; if ((pp.buf = malloc(-size)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } if (ioctl(pdriver, IOC_PIPE_PEEK, &pp) == -1) exit_sys("ioctl"); peekbuf = (char *)pp.buf; for (size_t i = 0; i < pp.size; ++i) putchar(peekbuf[i]); putchar('\n'); free(pp.buf); } else { if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi de aygıt sürücülerde zamanlama işlemlerinin nasıl yapılacağı üzerinde duracağız. Çekirdeğin zamanlama mekanizması periyodik oluşturulan donanım kesmeleriyle sağlanmaktadır. Bu kesmelere genel olarak "timer kesmeleri" ya da Linux terminolojisinde "jiffy" denilmektedir. Eskiden tek CPU'lu makineler kullanıyordu ve eski işlemcilerde işlemcinin içerisinde periyodik kesme oluşturacak bir mekanizma yoktu. Ancak daha sonraları işlemcilere kendi içerisinde periyodik kesme oluşturabilecek timer devreleri eklendi. Bugün ağırlıklı olarak birden fazla çekirdeğe sahip işlemcileri kullanıyoruz. Bu işlemcilerin içerisindeki çekirdeklerin her birinde o çekirdekte periyodik kesme oluşturan "(local interrupts)" timer devreleri bulunmaktadır. Böylece her çekirdek kendi timer devresiyle "context switch" yapmakta ve proses istatistiklerini güncellemektedir. Bugün PC mimarisinde yerel çekirdeklerin kesme mekanizmaları dışında ayrıca bir de eski sistemlerde zaten var olan IRQ0 hattına bağlı global bir timer devresi de bulunmaktadır. Bu global timer devresi "context switch" yapmak için değil sistem zamanının ilerletilmesi amacıyla kullanılmaktadır. Bugünkü Linux sistemlerinde söz konusu olan bu timer devrelerinin hepsi 1 milisaniye, 4 milisaniye ya da 10 milisaniyeye kurulmaktadır. Eskiden ilk Linux çekirdeklerinde 10 milisaniyelik periyotlar kullanılıyordu. Sonra bilgisayarlar hızlanınca 1 milisaniye periyot yaygın olarak kullanılmaya başlandı. Ancak bugünlerde 4 milisaniye periyotları kullanan çekirdekler de yaygın biçimde bulunmaktadır. Aslında timer frekansı çekirdek konfigüre edilirken kullanıcılar tarafından da değiştirilebilmektedir. (Çekirdek derlenmeden önce çekirdeğin davranışları üzerinde etkili olan parametrelerin belirlenmesi sürecine "çekirdeğin konfigüre" edilmesi denilmektedir.) Şimdi de bu "jiffy" kavramı üzerinde duralım: >> "jiffy" : Global timer kesmelerine (PC mimarisinde IRQ0) ilişkin kesme kodları çekirdek içerisindeki jiffies isimli bir global değişkeni artırmaktadır. Böylece eğer timer kesme periyodu biliniyorsa iki jiffies değeri arasındaki farka bakılarak bir zaman ölçümü mümkün olabilmektedir. Timer frekansı Linux kernel içerisindeki HZ isimli sembolik sabitle belirtilmiştir. Timer periyodu çekirdek konfigüre edilirken değiştirilebilmektedir. Genellikle bu süre 1 ms, 4 ms ya da 10 ms olmaktadır. (Ancak değişik mimarilerde farklı değerlerde olabilir.) Örneğin kursun yapıldığı sanal makinede timer periyodu 4 milisaniye'dir. Bu da saniyede 250 kez timer kesmesinin oluşacağı anlamına gelmektedir. Başka bir deyişle bu makinede HZ sembolik sabiti 250 olarak define edilmiştir. İşte timer kesmesi her oluşturduğunda işletim sisteminin "kesme kodu (interrupt handler)" devereye girip "jiffies" isimli global değişkeni 1 artırmaktadır. Bu jiffies değişkeni "unsigned long" türdendir. Bildindiği gibi "unsigned long" türü 32 bit Linux sistemlerinde 32 bit, 64 bit Linux sistemlerinde 64 bittir. 32 bit Linux sistemlerinde ayrıca jiffies_64 isimli bir değişken daha vardır. Bu değişken hem 32 bit sistemde hem de 64 bit sistemde 64 bitliktir. 32 bit sistemde jiffies değişkeni 32 bit olduğu için bilgisayar uzun süre açık kalırsa taşma (overflow) oluşabilmektedir. Ancak 64 bit sistemlerde taşma mümkün değildir. 32 bit sistemlerde jiffies_64 değeri çekirdek tarafından iki ayrı makine komutuyla güncellenmektedir. Çünkü 32 bit sistemlerde 64 bit değeri belleğe tek hamlede yazmak mümkün değildir. Bu nedenle jiffies_64 değerinin taşma durumunda yanlış okunabilme olasılığı vardır. Hem 32 bit hem 64 bit sistemlerde 64 bitlik jiffies değerini düzgün bir biçimde okuyabilmek için get_jiffies_64 isimli fonksiyon bulundurulmuştur. Fonksiyonun prototipi aşağıdaki gibidir: #include u64 get_jiffies_64(void); Biz 32 bit sistemde de olsak bu fonksiyonla 64 bitlik jiffies değerini düzgün bir biçimde okuyabiliriz. * Örnek 1, Aşağıdaki örnekte çekirdek modülü içerisinde proc dosya sisteminde "jify-module" isimli bir dizin, dizin'in içerisinde de "jiffy" ve "hertz" isimli iki dosya yaratılmıştır. jiffy dosyası okunduğunda o anki jiffies değeri elde edilmektedir. hertz dosyası okunduğunda ise timer frekansı elde edilmektedir. Aygıt sürücüyü aşağıdaki gibi derleyip yükleyebilirsiniz: $ make file=jiffy-module $ sudo insmod jiffy-module.ko Boşaltımı da şöyle yapabilirsiniz: $ sudo rmmod jiffy-driver.ko Programa ilişkin kodlar aşağıdaki gibidir: /* jiffy-module.c */ #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("procfs driver"); static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off); static struct proc_ops g_procops_jiffy = { .proc_read = proc_read_jiffy, }; static struct proc_ops g_procops_hertz = { .proc_read = proc_read_hertz, }; static char g_jiffies_str[32]; static char g_hertz_str[32]; static int __init generic_init(void) { struct proc_dir_entry *pde_dir; if ((pde_dir = proc_mkdir("jiffy-module", NULL)) == NULL) return -ENOMEM; if (proc_create("jiffy", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_jiffy) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("hertz", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_hertz) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("jiffy-module", NULL); printk(KERN_INFO "jiffy-module module exit...\n"); } static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_jiffies_str, "%lu\n", jiffies); left = strlen(g_jiffies_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_jiffies_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "jiffy file read...\n"); return esize; } static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_hertz_str, "%d\n", HZ); left = strlen(g_hertz_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_hertz_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "hertz file read...\n"); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Yukarıda da belirttiğimiz gibi eğer 64 bit sistemde çalışılıyorsa jiffies değerinin taşması (overflow olması) mümkün değildir. Ancak 32 bit sistemlerde timer frekansı 1000 ise 49 günde taşma meydana gelebilmektedir. Aygıt sürücü programcısı bazen geçen zamanı hesaplamak için iki noktada jiffies değerini alıp aradaki farka bakmak isteyebilmektedir. Ancak bu durumda 32 bit sistemlerde "overflow" olasılığının ele alınması gerekir. İşaretli sayıların ikili sistemdeki temsiline dayanarak iki jiffies arasındaki fark aşağıdaki gibi tek bir ifadeyle de hesaplanabilmektedir: unsigned long int prev_jiffies, next_jiffies; ... net_jiffies = (long) next_jiffies - (long) prev_jiffies; Çekirdek içerisinde iki jiffy değerini alarak bunları öncelik sonralık ilişkisi altında karşılaştıran aşağıdaki fonksiyonlar bulunmaktadır: time_after(jiffy1, jiffy2) time_before(jiffy1, jiffy2) time_after_eq(jiffy1, jiffy2) time_before_eq(jiffy1, jiffy2) Bu fonksiyonların hepsi bool bir değere geri dönmektedir. Bu fonksiyonlar 32 bit sistemlerde taşma durumunu da dikkate almaktadır. time_after fonksiyonu birinci parametresiyle belirtilen jiffy değerinin ikinci parametresiyle belirtilen "jiffy" değerinden sonraki bir jiffy değeri olup olmadığını belirlemekte kullanılmaktadır. Diğer fonksiyonlar da bu biçimde birinci parametredeki jiffy değeri ile ikinci parametredeki jiffy değerini karşılaştırmaktadır. Çekirdek içerisinde jiffies değerini çeşitli biçimlere dönüştüren aşağıdaki fonksiyonlar da bulunmaktadır: #include unsigned long msecs_to_jiffies(const unsigned int m); unsigned long usecs_to_jiffies(const unsigned int m); unsigned long usecs_to_jiffies(const unsigned int m); Bu işlemin tersini yapan da üç fonksiyon vardır: #include unsigned int jiffies_to_msecs(const unsigned long j); unsigned int jiffies_to_usecs(const unsigned long j); unsigned int jiffies_to_nsecs(const unsigned long j); Bu fonksiyonlar o andaki aktif HZ değerini dikkate almaktadır. Ayrıca jiffies değerini saniye ve nano saniye biçiminde ayırıp bize struct timespec64 biçiminde bir yapı nesnesi olarak veren jiffies_to_timespec64 isimli bir fonksiyon da vardır. Bunun tersi timespec64_to_jiffies fonksiyonuyla yapılmaktadır. timespec64 yapısı da şöyledir: struct timespec64 { time64_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; Eski çekirdeklerde bu fonksiyonların yerine aşağıdaki fonksiyonlar bulunyordu: #include unsigned long timespec_to_jiffies(struct timespec *value); void jiffies_to_timespec(unsigned long jiffies, struct timespec *value); unsigned long timeval_to_jiffies(struct timeval *value); void jiffies_to_timeval(unsigned long jiffies, struct timeval *value); Aşağıdaki örnekte proc dosya sisteminde "jiffy-module" dizini içerisinde ayrıca "difference" isimli bir dosya da yaratılmıştır. Bu dosya her okunduğunda önceki okumayla aradaki jiffy farkı yazdırılmaktadır. * Örnek 1, /* jiffy-module.c */ #include #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("procfs driver"); static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_difference(struct file *filp, char *buf, size_t size, loff_t *off); static struct proc_ops g_procops_jiffy = { .proc_read = proc_read_jiffy, }; static struct proc_ops g_procops_hertz = { .proc_read = proc_read_hertz, }; static struct proc_ops g_procops_difference = { .proc_read = proc_read_difference, }; static char g_jiffies_str[32]; static char g_hertz_str[32]; static char g_difference_str[512]; static int __init generic_init(void) { struct proc_dir_entry *pde_dir; if ((pde_dir = proc_mkdir("jiffy-module", NULL)) == NULL) return -ENOMEM; if (proc_create("jiffy", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_jiffy) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("hertz", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_hertz) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("difference", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_difference) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("jiffy-module", NULL); printk(KERN_INFO "jiffy-module module exit...\n"); } static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_jiffies_str, "%lu\n", jiffies); left = strlen(g_jiffies_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_jiffies_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "jiffy file read...\n"); return esize; } static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_hertz_str, "%d\n", HZ); left = strlen(g_hertz_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_hertz_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "hertz file read...\n"); return esize; } static ssize_t proc_read_difference(struct file *filp, char *buf, size_t size, loff_t *off) { static unsigned long prev_jiffies; loff_t left, esize; long int net_jiffies; struct timespec64 ts; net_jiffies = (long)jiffies - (long)prev_jiffies; jiffies_to_timespec64(net_jiffies, &ts); sprintf(g_difference_str, "Jiffy difference: %10ld (%ld seconds + %ld nonoseconds)\n", net_jiffies, (long)ts.tv_sec, (long)ts.tv_nsec); left = (loff_t)strlen(g_difference_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_difference_str + *off, esize) != 0) return -EFAULT; *off += esize; } prev_jiffies = jiffies; return (ssize_t)esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Aygıt sürücü içerisinde bazen belli bir süre bekleme yapmak gerekebilmektedir. Biz kursumuzda daha önce user modda bekeleme yapan fonksiyonları görmüştük. Ancak o fonksiyonlar kernel modda kullanılamamtadır. Kermel modda çekirdek içerisindeki olanaklarla bekleme yapılabilmektedir. Eğer bekleme süresi kısa ise bekleme işlemi meşgul bir döngü ile yapılabilir. Örneğin: while (time_before(jiffies, jiffies_target)) schedule(); Burada o anki jiffies değeri hedef jiffies değerinden küçükse schedule fonksiyonu çağrılmıştır. schedule fonksiyonu thread'i uykuya yatırmamaktadır. Yalnızca thread'ler arası geçiş oluşmasına yol açmaktadır. Yani bu fonksiyon uykuya dalmadan CPU'yu bırakmak için kullanılmaktadır. schedule fonksiyonunu çağıran thread çalışma kuyruğunda (run queue) kalmaya devam eder. Yine çalışma sırası ona geldiğinde kaldığı yerden çalışmaya devam eder. Ancak meşgul bir döngü içerisinde schedule işlemi yine önemli bir CPU zamanın harcanmasına yol açmaktadır. Bu nedenle uzun beklelemelerin yukarıdaki gibi yapılması tavsiye edilmemektedir. Uzun beklemelerin uykuya dalarak yapılması gerekir. Uzun beklemeler için bir wait kuyruğu oluşturulup wait_event_timeout ya da wait_event_interrptible_timeout fonksiyonlarıyla koşul 0 yapılarak gerçekleştirilebilir. Ancak bunun için bir wait kuyruğunun oluşturulması gerekir. Bu işlemi zaten kendi içerisinde yapan özel fonksiyonlar vardır. schedule_timeout fonksiyonu belli bir jiffy zamanı geçene kadar thread'i çekirdek tarafından bu amaçla oluşturulmuş olan bir wait kuyruğunda bekletir. signed long schedule_timeout(signed long timeout); Fonksiyon parametre olarak beklenecek jiffy değerini alır. Eğer sinyal dolayısıyla fonksiyon sonlanırsa kalan jiffy sayısına, eğer zaman aşımının dolması nedeniyle fonksiyon sonlanısa 0 değerine geri döner. Fonksiyon başarısız olmamaktadır. Fonksiyonu kullanmadan önce prosesin durum bilgisini set_current_state isimli fonksiyonla değiştirmek gerekir. Değiştirilecek durum TASK_UNINTERRUPTIBLE ya da TASK_INTERRUPTIBLE olabilir. Bu işlem yapılmazsa bekleme gerçekleşmemektedir. Örneğin: set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(jiffies + 5 * HZ); Uzun beklemeyi kendi içerisinde schedule_timeout kullanarak yapan üç yardımcı fonksiyon da vardır: #include void msleep(unsigned int msecs); unsigned long msleep_interruptible(unsigned int msecs); void ssleep(unsigned int secs); Aşağıdaki örnekte "jiffy-module" dizinindeki "sleep" dosyasından okuma yapıldığında (denemeyi cat yapabilirsiniz) 10 saniye bekleme oluşacaktır. * Örnek 1, /* jiffy-module.c */ #include #include #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("procfs driver"); static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_difference(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_sleep(struct file *filp, char *buf, size_t size, loff_t *off); static struct proc_ops g_procops_jiffy = { .proc_read = proc_read_jiffy, }; static struct proc_ops g_procops_hertz = { .proc_read = proc_read_hertz, }; static struct proc_ops g_procops_difference = { .proc_read = proc_read_difference, }; static struct proc_ops g_procops_sleep = { .proc_read = proc_read_sleep, }; static char g_jiffies_str[32]; static char g_hertz_str[32]; static char g_difference_str[512]; static int __init generic_init(void) { struct proc_dir_entry *pde_dir; if ((pde_dir = proc_mkdir("jiffy-module", NULL)) == NULL) return -ENOMEM; if (proc_create("jiffy", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_jiffy) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("hertz", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_hertz) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("difference", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_difference) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("sleep", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_sleep) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("jiffy-module", NULL); printk(KERN_INFO "jiffy-module module exit...\n"); } static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_jiffies_str, "%lu\n", jiffies); left = strlen(g_jiffies_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_jiffies_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "jiffy file read...\n"); return esize; } static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_hertz_str, "%d\n", HZ); left = strlen(g_hertz_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_hertz_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "hertz file read...\n"); return esize; } static ssize_t proc_read_difference(struct file *filp, char *buf, size_t size, loff_t *off) { static unsigned long prev_jiffies; loff_t left, esize; long int net_jiffies; struct timespec64 ts; net_jiffies = (long)jiffies - (long)prev_jiffies; jiffies_to_timespec64(net_jiffies, &ts); sprintf(g_difference_str, "Jiffy difference: %10ld (%ld seconds + %ld nonoseconds)\n", net_jiffies, (long)ts.tv_sec, (long)ts.tv_nsec); left = (loff_t)strlen(g_difference_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_difference_str + *off, esize) != 0) return -EFAULT; *off += esize; } prev_jiffies = jiffies; return (ssize_t)esize; } static ssize_t proc_read_sleep(struct file *filp, char *buf, size_t size, loff_t *off) { /* set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(HZ * 10); */ ssleep(10); return 0; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Aygıt sürücü içerisinde kısa beklemeler gerebilmektedir. Çünkü bazı donanım aygıtlarının programlanabilmesi için bazı beklemelere gereksinim duyulabilmektedir. Kısa beklemeler meşgul döngü yoluyla yani hiç sleep yapılmadan sağlanmaktadır. Ayrıca kısa bekleme yapan fonksiyonlar atomiktir. Atomiklikten kastedilen şey threadler arası geçiş işleminin kapatılmasıdır. Yani kısa bekleme yapan fonksiyonlar threadler arası geçiş işlemini o işlemci için kapatırlar. Bu sırada thread'ler arası geçiş söz konusu olmamaktadır. Ancak donanım kesmeleri bu süre içerisinde oluşabilmektedir. Kısa süreli döngü içerisinde bekleme yapan fonksiyonlar şunlardır: void ndelay(unsigned int nsecs); void udelay(unsigned int usecs); void mdelay(unsigned int msecs); Burada delay nano saniye cinsinden bekleme yapmak için, udelay mikro saniye cinsinden bekeleme yapmak için mdelay ise mili saniye cinsinden bekleme yapmak için kullanılmaktadır. Öte yandan Linux çekirdeklerine belli versiyonyondan sonra bir timer mekanizması da eklenmiştir. Bu sayede aygıt sürücü programcısı belli bir zaman sonra belirlediği bir fonksiyonun çağrılmasını saplayabilmektedir. Bu mekanizmaya "kernel timer" mekanizması denilmektedir. >>> "kernel timer" : Maalesef kernel timer mekanizması da birkaç kere arayüz olarak değiştirilmiştir. Bu mekanizma kullanılıken dikkat edilmesi gereken bir nokta callback fonksiyonun bir proses bağlamında çağrılmadığıdır. Yani callback fonksiyon çağrıldığında biz current makrosu ile o andaki prosese erişemeyiz. O anda çalışan prosesin user alanına kopyalamalar yapamayız. Çünkü callback fonksiyon timer kesmeleri tarafından çağrılmaktadır. Dolayısıyla callback fonksiyon çağrıldığında o anda hangi prosesin çalışmakta olduğu belli değildir. Son Linux çekirdeklerindeki kernel timer kullanımı şöyledir: -> struct timer_list türünden bir yapı nesnesi statik düzeyde tanımlanır ve bu yapı nesnesine ilk değeri verilir. DEFINE_TIMER makrosu ile hem tanımlama hem de ilkdeğer verme işlemi birlikte yapılabilir. Makro şöyledir: #include #define DEFINE_TIMER(_name, _function) Örneğin: DEFINE_TIMER(g_mytimer, timer_proc); Ya da alternatif olarak struct timer_list nesnesi yaratılıp timer_setup makrosuyle de ilkdeğer verilebilir. Makronun parametrik yapısı şöyledir: #include #define timer_setup(timer, callback, flags) Makronun birinci parametresi timer nesnesinin adresini almaktadır. İkinci parametresi çağrılacak fonksiyonun belirtir. flags parametresi 0 geçilebilir. Örneğin: static struct timer_list g_mytimer; timer_setup(&g_mytimer, timer_proc, 0); Buradaki timer fonksiyonun parametrik yapısı şöyle olmalıdır: void timer_proc(struct timer_list *tlisr); -> Tanımlanan struct timer_list nesnesi add_timer fonksiyonu ile bir bağlı listeye yerleştirilir. Bu bağlı liste çekirdeğin içerisinde çekirdek tarafından oluşturulmuş bir listedir. add_timer fonksiyonunun prototipi şöyledir: #include void add_timer(struct timer_list *timer); -> Daha sonra ne zaman fonksiyonun çağrılacağını anlatmak için mod_timer fonksiyonu kullanılır. #include int mod_timer(struct timer_list *timer, unsigned long expires); Buradaki expiry parametresi jiffy türündendir. Bu parametre hedef jiffy değerini içermelidir. (Yani jiffies + gecikme jiffy değeri) -> Timer nesnesinin silinmesi için del_timer ya da del_timer_sync fonksiyonu kullanılmaktadır: #include int del_timer(struct timer_list * timer); int del_timer_sync(struct timer_list * timer); del_timer fonksiyonu eğer timer fonksiyonu o anda başka bir işlemcide çalışıyorsa asenkron biçimde silme yapar. Yani fonksiyon sonlandığında henüz silme gerçekleşmemiş göreli bir süre sonra gerçekleşecek olabilir. Halbuki del_timer_sync fonksiyonu geri dönünce timer silinmesi gerçekleşmiş olur. Eğer timer silinmezse modül çekirdekten atıldığında tüm sistem çökebilir. Normal olarak belirlenen fonksiyon yalnızca 1 kez çağrılmaktadır. Ancak bu fonksiyonun içerisinde yeniden mod_timer çağrılarak çağırma periyodik hale getirilebilir. Aşağıda kernel timer kullanımına basit bir örnek verilmiştir: * Örnek 1, /* timer-module.c */ #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Timmer Module"); static void timer_proc(struct timer_list *tlist); DEFINE_TIMER(g_mytimer, timer_proc); static int __init generic_init(void) { add_timer(&g_mytimer); mod_timer(&g_mytimer, jiffies + msecs_to_jiffies(5000)); printk(KERN_INFO "timer-module module init...\n"); return 0; } static void timer_proc(struct timer_list *tlist) { static int count = 0; if (count == 5) { del_timer(&g_mytimer); count = 0; return; } ++count; printk(KERN_INFO "timer callback (%d)\n", count); mod_timer(&g_mytimer, jiffies + msecs_to_jiffies(5000)); } static void __exit generic_exit(void) { printk(KERN_INFO "timer-module module exit...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Önceki konularda da UNIX/Linux sistemlerinde kernel mode'da çalışan işletim sistemine ait thread'ler olduğundan bahsetmiştik. Bu thread'ler çalışma kuyruğunda (run queue) bulunan ve uykuya dalabilen işletim sisteminin bir parçası durumundaki thread'lerdir. İşletim sistemine ait bu thread'ler çeşitli işlemlerden sorumludurlar. Linux işletim sisteminde kernel thread'ler genellikle "user mode daemon"lar gibi sonu 'd' ile bitecek biçimde isimlendirilmiştir. Ancak çekirdeğe ait olan bu thread'lerin ismi 'k' (kernel'dan geliyor) ile başlatılmıştır. Örneğin "kupdated", "kswapd", "keventd" gibi. İşte aygıt sürücüler de isterlerse arka planda kernel mode'da bir proses gibi çalışan thread'ler yaratabilirler. Ancak bu thread'ler bir proses ile ilişkisiz biçimde çalıştırılmaktadır. Bu nedenle bunlar içerisinde current makrosu, ve copy_to_user ya da copy_from_user gibi fonksiyonlar kullanılamaz. Aygıt sürücü kodlarımız genellikle bir olay olduğunda (örneğin kesme gibi) ya da user mode'dan çağrıldığında (read, write, ioctl gibi) çalıştırılmaktadır. Ancak kernel thread'ler aygıt sürücüye sanki bir programmış gibi kernel mode'da sürekli çalışma imkanı vermektedir. Kernel thread'ler sırasıyla şu adımlarlardan geçilerek kullanılmaktadır: -> Önce kernel thread aygıt sürücü içerisinde yaratılır. Yaratılma modülün init fonksiyonunda yapılabileceği gibi aygıt sürücü ilk kez açıldığında open fonksiyonunda ya da belli bir süre sonra belli bir fonksiyonda da yapılabilmektedir. Kernel thread'ler kthread_create fonksiyonuyla yaratılmaktadır: #include struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char *namefmt); Fonksiyonun birinci parametresi thread akışının başlatılacağı fonksiyonun adresini almaktadır. Bu fonksiyon "void *" türünden parametreye ve int geri dönüş değerine sahip olma zorundadır. Fonksiyonun ikinci parametresi thread fonksiyonuna geçirilecek parametreyi belirtmektedir. Eğer bir kernel thread'e bir parametre geçirilmek istenmiyorsa bu parametre için NULL adres girilebilir. Fonksiyon üçüncü parametresi proc dosya sisteminde (dolayısıyla "ps" komutunda) görüntülenecek ismi belirtir. Fonksiyon başarı durumunda yaratılan thread'in task_struct adresine, başarısızlık durumunda negatif errno değerine geri dönmektedir. Adrese geri dönen diğer kernel fonksiyonlarında olduğu gibi fonksiyonun başarı durumu IS_ERR makrosuyla test edilmelidir. Eğer fonksiyon başarısız olmuşsa negatif errno değeri PTR_ERR makrosuyle elde elde edilebilir. Örneğin: struct task_struct *ts; ts = kthread_create(...); if (IS_ERR(ts)) { printk(KERN_ERROR "cannot create kernel thread!..") return PTR_ERR(ts); } Anımsanacağı gibi Linux sistemlerinde prosesler ve thread'ler task_struct yapısıyla temsil edilmektedir. İşte bu fonksiyon da başarı durumunda çekirdek tarafından yaratılan task_struct nesnesinin adresini bize vermektedir. Kernel thread bu fonksiyonla yaratıldıktan sonra hemen çalışmaz. Onu çalıştırmak için wake_up_process fonksiyonun çağrılması gerekir: #include int wake_up_process(struct task_struct *tsk); Fonksiyon ilgili kernel thread'in task_struct adresini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda negatif errno değerine geri döner. Aslında yukarıdaki işlemi tek hamlede yapan kthread_run isimli bir fonksiyon da vardır: struct task_struct *kthread_run(int (*threadfn)(void *data), void *data, const char *namefmt); -> Kernel thread kthread_stop fonksiyonuyla herhangi bir zaman ya da aygıt sürücü bellekten atılırken yok edilebilir: int kthread_stop(struct task_struct *ts); Fonksiyon thread sonlanana kadar blokeye yol açar. Fonksiyon thread fonksiyonunun exit koduyla (yani thread fonksiyonunun geri dönüş değeri ile) geri dönmektedir. Genellikle programcılar thread fonksiyonlarını başarı durumunda sıfır, başarısızlık durumunda sıfır dışı bir değerle geri döndürmektedir. Burada önemli nokta kthread_stop fonksiyonunun kernel thread'i zorla sonlandırılmadığıdır. Kernel thread'in sonlanması zorla yapılmaz. kthread_stop fonksiyonu bir bayrağı set eder. Kernel thread de tipik olarak bir döngü içerisinde "bu bayrak set edilmiş mi" diye bakar. Eğer bayrak set edilmişse kendini sonlandırır. Kernel thread'in bu bayrağa bakması kthread_should_stop fonksiyonuyla yapılmaktadır. #include bool kthread_should_stop(void); Fonksiyon eğer bu flag set edilmişse sıfır dışı bir değere, set edilmediyse 0 değerine geri dönmektedir. Tipik olarak kernel thread fonksiyonu aşağıdaki gibi bir döngüde yaşamını geçirir: while (!kthread_should_stop()) { ... } Tabii aslında biz kthread_create fonksiyonu ile bir kernel thread yaratmak istediğimizde asıl thread'in başlatıldığı fonksiyon çekirdek içerisindeki bir fonksiyondur. Bizim kthread_create fonksiyonuna verdiğimiz fonksiyon bu fonksiyon tarafından çağrılmaktadır Dolayısıyla bizim fonksiyonumuz bittiğinde akış yine çekirdek içerisindeki asıl fonksiyona döner. O fonksiyonda da yaratılmış thread kaynakları otomatik boşaltılır. Yani biz bir thread yarattığımız zaman onun yok edilmesi thread fonksiyonu bittiğinde otomatik yapılmaktadır. Kernel thread'in kendisini sonşandırması do_exit fonksiyonuyla sağlanabilmektedir. Aslında do_exit fonksiyonu prosesleri sonlandıran sys_exit fonksiyonun doğrudan çağırdığı taban fonksiyondur. #include void do_exit(long code); Fonksiyon thread'in exit kodunu parametre olarak almaktadır. Kernel thread normal biçimde ya da kthread_stop fonksiyonuyla sonlanmışsa artık thread'in tüm kaynakları (task_struct yapısı da) serbest bırakulmaktadır. Dolayısıyla artık ona kthread_stop uygulamamak gerekir. Aşağıdaki örnekte modül initialize edilirken kernel thread yaratılmış, modül yok edilirken kthread_stop ile thread'in sonlanması beklenmiştir. kernel thread içerisinde msleep fonksiyonu ile 1 saniyelik beklemeler yapılmıştır. * Örnek 1, /* kernel-thread-module.c */ #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Kernel Thread Module"); static int kernel_thread_proc(void *param); struct task_struct *g_ts; static int __init generic_init(void) { printk(KERN_INFO "kernel-thread-module init...\n"); g_ts = kthread_run(kernel_thread_proc, NULL, "kmythreadd"); if (IS_ERR(g_ts)) { printk(KERN_ERR "cannot create kernel thread!..\n"); return PTR_ERR(g_ts); } return 0; } static int kernel_thread_proc(void *param) { static int count; printk(KERN_INFO "kernel-thread starts...\n"); while (!kthread_should_stop()) { printk(KERN_INFO "kernel-thread is running: %d\n", count); msleep(1000); ++count; } return 0; } static void __exit generic_exit(void) { int ecode; ecode = kthread_stop(g_ts); printk(KERN_INFO "kernel thread exits with code \"%d\"\n", ecode); printk(KERN_INFO "kernel-thread-module exit...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Şimdi de "interrupt" hususuna değinelim; İşlemcinin çalıştırmakta olduğu koda ara vererek başka bir kodu çalıştırması ve çalıştırma bittikten sonra kaldığı yerden devam etmesi sürecine "kesme (interrupt)" denilmektedir. Kesmeler oluşma biçiminde göre üçe ayrılmaktadır: -> Donanım Kesmeleri (Hardware Interrupts) -> İçsel Kesmeler (Internal Interrupts) -> Yazılım Kesmeleri (Software Interrupts) Kesme denildiğinde akla default olarak donanım kesmeleri gelmektedir. Donanım kesmeleri CPU'nın bir ucunun (genellekle bu uca INT ucu denilmektedir) elektriksel olarak dışsal bir birim tarafından uyarılmasıyla oluşmaktadır. Yani donanım kesmeleri o anda çalışmakta olan koddan bağımsız bir biçimde dış dünyadaki birimler tarafından oluşturulmaktadır. PC terminolojisinde donanım kesmesi oluşturan kaynaklara IRQ da denilmektedir. İçsel kesmeler CPU'nun kendi çalışması sırasında kendisinin oluşturduğu kesmelerdir. Intel bu tür kesmelerin önemli bir bölümünü "fault" olarak isimlendirmektedir. Örneğin fiziksel RAM'de olmayan bir sayfaya erişildiğinde CPU "page fault" denilen içsel kesme oluşturmaktadır. Yazılım kesmeleri ise programcının program kodyla oluşturduğu kesmelerdir. Her türlü CPU'da yazılım kesmesi oluşturulamamaktadır. Bir kesme oluştuğunda çalışıtırılan koda "kesme kodu (interrupt handler)" denilmektedir. Donanım kesmesi oluşturan elektronik birimlerin hepsi doğrudan CPU'nın INT ucuna bağlanmamaktadır. Çünkü bunun pek çok sakıncası vardır. Genellikle bu amaçla bu işe aracılık eden daha akıllı bir işlemciler kullanılmaktadır. Bu işlemcilere genel olarak "kesme denetelecileri (interrupt controllers)" denilmektedir. Bazı mimarilerde kesme denetleyicisi işlemci işlemcinin içerisinde bulunmakıtadır. Bazı mimarilerde ise dışarıda ayrı bir entegre devre olarak bulunmaktadır. Tabii artık pek çok entegre devre "SoC (System on Chip)" adı altında tek bir entegre devrenin içersine yerleşirilmiş durumdadır. Kesme denetleyicilerinin temel işlevi şöyledir: -> Birden fazla donanım biriminin aynı anda kesme oluşturması durumunda kesme denetleyicisi bunları sıraya dizebilmektedir. -> Birden fazla donanım biriminin aynı anda kesme oluşturması durumunda kesme denetleyicisi bunlara öncelik verebilmektedir. -> Belli birimlerden gelen kesme isteklerini kesme denetleyicisi görmezden gelebilmektedir. Buna ilgili IRQ'nun disable edilmesi denilmektedir. -> Kesme denetleyicileri çok çekirdekli donanımlarda kesmenin belli bir çekirdekte çalışttırılabilmesini sağlayabilmektedir. Bugün kullandığımız PC'lerde (laptop ve notebook'lar da dahil olmak üzere) eskiden kesme denetleyicisi olarak bir tane Intel'in 8259 (PIC) denilen entegre devresi kullanılıyordu. Bunun 8 girişi bulunuyordu. Yani bu kesme denetleyicisinin uçları 8 ayrı donanım birimine bağlanabiliyordu. | (INT ucu CPU'ya bağlanır) <8259 (PIC)> | | | | | | | | 0 1 2 3 4 5 6 7 Bu uçlara IRQ uçları deniliyordu ve bu uçlar değişik donanım birimlerine bağlıydı. Böylece bir donanım birimi kesme oluşturmak isterse kesme denetleyicisinin igili ucunu uyarıyordu. Kesme deetleyicisi de CPU'nın INT ucunu uuarıyordu. İlk PC'lerde toplam 8 IRQ vardı. Ancak 80'li yılların ortalarında PC mimarisinde değişikler yapılarak kesme denetleyicisinin sayısı iki'ye yükseltildi. Böylece IRQ uçlarının sayısı da 15'e yükseltilmiş oldu. Intel'in iki 8259 işlemcisini katkat bağlayabilmek için birinci kesme kesme denetleyicisinin (Master PIC) bir ucunun ikinci kesme denetleyicsinin INGT ucuna bağlanması gerekmektedir. İşte PC mimarsinde birinci kesme denetleyicisinin 2 numaralı ucu ikinci kesme denetleyicisine bağlanmıştır. Böylece toplam IRQ'ların sayısı 16 değil 15 olmaktadır. | (INT ucu CPU'ya baplanır) | <8259 (PIC)> <8259 (PIC)> | | X | | | | | | | | | | | | | 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Ancak zamanla 15 IRQ ucu da yetersiz kalmaya başlamıştır. Çok çekirdekli sistemlerde her çekirdeğin (yani CPU'nun) ayrı bir INT ucu vardır. Yani bu çekirdekler diğerlerinden bağımsız kesme alabilmektedir. İşte zamanla Intel'in klasik 8259 kesme denetleyicisi daha gelişmiş olam ve ismine IOAPIC denilen kesme denetleyicisi ile değiştirilmiştir. Bugün kullandığımız Intel tabanlı bilgisayar mimarisinde artık IOAPIC kesme denetleyicileri bulunmaktadır. Bu yeni kesme denetleyicisinin 24 IRQ ucu vardır. IOAPIC birden fazla çekirdeğin bulunduğu durumda tek bir çekirdeğe değil tüm çekirdeklere bağlanmaktadır. Dolayısıyla istenilen bir çekirdekte kesme oluşturabilmektedir. IOAPIC devresinin bazı uçları bazı donanım birimelrine bağlı biçimdedir. Ancak bazı uçları boştadır. Bugün kullanılan ve ismine PCI ya da PCI-X denilen genişleme yuvaalarının bazı uçları bu IOAPIC ile bağlantılıdır. Dolayısıyla genişleme yuvalarına takılan kartlar da IRQ oluşturabilmektedir. Bugün Pentium ve eşdeğer AMD ieşlemcilerinin içerisinde (her çekirdeğin içeisidne) aynı zamanda ismine "Local APIC" denilen bir kesme denetleyicisi de vardır. Bu local APIC iki uca sahiptir. Local APIC içerisinde aynı zamanda bir timer devresi de bulunmaktadır. Bu timer devresi periyodik donanım kesmesi oluşturmak için kullanılmaktadır. Intel ve AMD çekirdeklerinin içerisinde bulunan APIC devresinin en önemli özelliği kesmeleri artık uçlarla değil veri yoluyla (data bus) oluşturabilmesidir. Buu özellik sayesinde hiç işlemcinin INT uyarılmadan çok fazla sayıda kesme sanki belleğe bir değer yazıyormuş gibi oluşturulabilmektedir. Bu tekniğe "Message Signaled Intterpt (MSI)" denilmektedir. Gerçekten de bugün PCI slotlara takılan bazı kartlar kesmeleri doğrudan belli bir çekirdekte MSI kullanarak oluşturmaktadır. O halde kullanığımız Intel taabanlı PC mimarisideki bugünkü durum şöyledir: -> Bazı donanım birimleri built-in biçimde IOAPIC'in uçlarına bağlı durumdadır. Bu uçlar eskiye uyumu korumak için 8259'un uçlarıyla aynı biçimde bağlı gibi IRQ oluşturmaktadır. -> Bazı PCI kartlar slot üzerindeki 4 IRQ hattından (INTA, INTB, INTC, INTD) birini kullanarak kesme oluşturmaktadır. Bu hatlar IOAPIC'in bazı uçlarına bağlıdır. -> Bazı PCI kartlar ise doğurdan modern MSI sistemini kullanarak IOAPIC'i pass geçerek bellek işlemleriyle doğrudan ilgili çekirdekte kesme oluşturabilmektedir. Bir aygıt sürücü programcısı mademki birtakım kartlar için onu işler hale getiren temel yazılımları da yazma iddiasındadır. O halde o kartın kullanacağı kesme için kesme kodlarını (interrupt handlers) yazabilmelidir. Tabii işletim sisteminin aygıt sürücü mimarisinde bu işlemler de özel zernel fonksiyonlarıyla yapılır. Yani kesme kodu yazmanın belli bir kuralı vardır. Pekiyi çok çekirdekli bilgisayar sistemlerinde oluşan bir kesme Intel taabanlı PC mimariisnde hangi çekirdek tarafından işlenmektedir? İşte bugün kullanılan IOAPIC devreleri bu bakımdan şu özelliklere sahiptir: -> Kesme IOAPIC tarafından donanım biriminin istediği bir çekirdekte oluşturulabilir. -> Kesme IOAPIC tarafından en az yüklü çekirdeğe karar verilerek orada oluşturulabilmektedir. -> Kesme IOAPIC tarafından döngüsel bir biçimde (yani sırasıyla her bir çekirdekte) oluşturulabilmektedir. IOAPIC'in en az yüklü işlemciyi bilmesi mümkün değildir. Onu ancak işletim sistemi bilebilir. İşte işlemcilerin Local APIC'leri içerisinde özel bazı yazmaçlar vardır. Aslında IOAPIC bu yazmaçtaki değerlere bakıp en düşüğünü seçmektedir. Bu değerleri de işletim sistemi set eder. İşletim sisteminin yaptığı bu faaliyete "kesme dengeleme (IRQ balancing)" denilmektedir. Linux sistemlerinde bir süredir kesme dengelemesi işletim sisteminin kernel thread'i (irqbalance) tarafından yapılmaktadır. Böylece Linux sistemlerinde aslında donanım kesmeleri her defasında farklı çekirdeklerde çalıştırılıyor olabilir. Pek çok CPU ailesinde donanım kesmelerinin teorik maksimum bir limiti vardır. Örneğin Intel mimarisinde toplam kesme sayısı 256'yı geçememektedir. Yani bu mimaride en fazla 256 farklı kesme oluşturulabilmektedir. Bu mimaride her kesmenin bir numarası vardır. IRQ numarası ile kesme numarasının bir ilgisi yoktur. Biz örneğin PIC ya da IOAPIC'i programlayarak belli bir kesmenin belli bir IRQ için belli numaralı bir kesmenin oluşmasını sağlayabiliriz. Örneğin timer (IRQ-0) için 8 numaralı kesmenin çalışmasını sağlayabiliriz. Pekiyi bir IRQ oluşturulduğunda çekirdek kaç numaralı kesme kodunun çalıştırılacağını nereden anlamaktadır? İşte PIC ya da IOAPIC CPU'nun INT ucunu uyararak kesme oluşturuken veri yolunun ilk 8 ucundan kesme numarasını da CPU'ya bildirmektedir. >>> "Interrupts in Kernel Modules" : Şimdi de aygıt sürücüler içerisinde kesmelerin nasıl ele alınacağı üzerinde duralım. Bir donanım kesmesi oluştuğunda aslında işletim sisteminin kesme kodu (interrupt handler) devreye girmektedir. Ancak işletim sisteminin kesme kodu istek doğrultusunda aygıt sürücülerin içerisindeki fonksiyonları çağırabilmektedir. Farklı aygıt sürücüleri aynı IRQ için istekte bulunabilir. Bu durumda işletim sistemi IRQ oluştuğunda farklı aygıt sürücülerdeki fonksiyonları belli bir düzen içerisinde çağırmaktadır. Aygıt sürücü programcısı bir kesme oluştuğunda aygıt sürücüsünün içerisindeki bir fonksiyonunun çağrılmasını istiyorsa önce onu request_irq isimli kernel fonksiyonuyla register ettirmelidir. #include int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id); Fonksiyonun birinci parametresi IRQ numarasını, ikinci parametresi IRQ oluştuğunda çağrılacak fonksiyonu belirtmektedir. Bu fonksiyonun geri dönüş değeri irqreturn_t türünden parametreleri de sırasıyla int ve "void *" türündendir. Örneğin: irqreturn_t my_irq_handler(int irq, void *dev_id) { ... } Buradaki irqreturn_t türü bir enum türü olarak typedef edilmiştir. Bu enum türünün elemanları şunlardır: enum irqreturn { IRQ_NONE = (0 << 0), IRQ_HANDLED = (1 << 0), IRQ_WAKE_THREAD = (1 << 1), }; typedef enum irqreturn irqreturn_t; request_irq fonksiyonun üçüncü parametresi bazı bayraklardan oluşur. Bu bayrak 0 geçilebilir ya da örneğin IRQF_SHARED geçilebilir. Diğer seçenkler için dokümanlara başvurabilirsiniz. IRQF_SHARED aynı kesmenin birden fazla aygıt sürücü tarafından kullanılabileceği anlamına gelmektedir. (Tabii biz ilk register ettiren değilsek daha önce resgister ettirenlerin bu bayrağı kullanmış olması gerekir. Aksi halde biz de bu bayrağı kullanamayız.) Fonksiyonun dördüncü parametresi "/proc/interrupts" dosyasında görüntülenecek ismi belirtir. Son parametre ise sistem genelinde tek olan bir nesnenin adresi olarak girilmelidir. Aygıt sürücü programcıları bu parametreye tipik olarak aygıt yapısını ya da çağrılacak foksiyonu girerler. Bu parametre IRQ handler fonksiyonuna ikinci parametre olarak geçilmektedir. Fonksiyon başarı durumunda o değerine başarısızlık durumunda negatif hata değerine geri dönmelidir. Örneğin: if ((result = request_irq(1, my_irq_handler, IRQF_SHARED, "my_irq1", NULL)) != 0) { ... return result; } Bir kesme kodu request_irq fonksiyonuyla register ettirilmişse bunun geri alınması free_irq fonksiyonuyla yapılmaktadır: #include const void *free_irq(unsigned int irq, void *dev_id); Fonksiyonun birinci parametresi silinecek irq numarasını, ikinci parametresi irq_reuest fonksiyonuna girilen son parametreyi belirtir. Fonksiyon başarı durumunda aygıt irq_request fonksiyonunda verilen isme, başarısızlık durumunda NULL adrese geri dönmektedir. Geri dönüş değeri bir hata kodu ieçrmemektedir. Normal olarak fonksiyonun ikinci parametresine request_irq fonksiyonunun son parametresiyle aynı değer geçilir. Bu parametrenin neden bu fonksiyona geirildiğinin bazı ayrıntıları vardır. Örneğin: if (free_irq(1, NULL) == NULL) printk(KERN_INFO "cannot free IRQ\n"); Pekiyi IRQ fonksiyonundan (IRQ habdler) hangi değerle geri dönülmelidir. Aslında programcı bu fonksiyondan ya IRQ_NONE değeri ile ya da IRQ_HANDLED değeri ile geri döner. Eğer programcı kesme kodu içerisinde yapmak istediği şeyi yapmışsa fonksiyondan IRQ_HANDLED, yapamamışsa ya da yapmak istememişse fonksiyondan IRQ_NONE değeri ile geri döner. Örneğin: static irqreturn_t my_irq_handler(int irq, void *dev_id) { ... return IRQ_HANDLED; } Donanımsal kesme mekanizmasının tipik örneklerinden biri klavye kullanımıdır. PC klavyesinde bir tuşa basıldığında klavye içerisindeki işlemci (keyboard encoder - Eskiden Intel 8048 ya da Holtek HT82K629B) basılan ya da çekilen tuşun klavyedeki sıra numarasını (buna "scan code" denilmektedir) dış dünyaya seri bir biçimde kodlamaktadır. Bu bilgi bilgisayardaki klavye denetleyicisine (Eskiden Intel 8042) gelir. Klavye denetleyicisi (keyboard controller) bu scan kodu kendi içerisinde bir yazmaçta saklar. PIC ya da IOAPIC'in 1 numaralı ucu klavye denetleyicisine bağlıdır ve bu uçtan IRQ1 kesmesini oluşturulmaktadır. Dolayısıyla biz bir tuşa bastığımızda otomatik olarak basılan tuşa ilişkin klavye scan kodu bilgisayar tarafına iletilir ve IRQ1 kesmesi oluşturulur. IRQ1 kesme kodu birincil olarak işletim sistemi tarafından ele alınmaktadır. İşletim sistemi de bu IRQ oluştuğunda aygıt sürücülerin belirlediği fonksiyonları çağırmaktadır. Klavyede yalnızca bir tuşa basılınca değil parmak tuştan çekildiğinde de yine klavye işlemcisi çekilen tuşun scan kodunu klavye denetleyicisine gönderip IRQ1 kesmesinin oluşmasına yol açmaktadır. Yani hem tuşa basında hem de parmak tuştan çekildiğinde IRQ1 oluşmaktadır. Klavye terminolojisinde parmağın tuşa basılmasıyla gönderilen scan koda "make code", parmağın tuştan çekilmesiyle gönderilen koda ise "break code" denilmektedir. Bugün PC'lerde kullandığımız klavyelerde parmak tuştan çekildiğinde önce PC tarafında bir F0 byte'ı ve sonra da tuşun scan kodu gönderilmektedir. Örneğin parmağımızı "A" tuşuna basıp çekelim şu scan kodlar bilgisayar tarafında gönderilecektir: Ctrl, Shift, Alt, Caps-Lock gibi tuşların diğer tuşlardan bir farkı yoktur. Ctrl+C gibi bir tuşa basıdığı işletim sisteminin tuttuğu flag değişkenlerle tespit edilmektedir. Örneğin biz Ctrl+C tuşlarına basıp çekmiş olalım. Klavye işlemcisi bilgisayar tarafında şu kodları gönderecektir: İşte Ctrl tuşuna basıldığını fark eden işletim sistemi bir flag'i set eder, parmak bu tuştan bırakıldığında flag'i reset eder. Böylece diğer tuşlara basıldığında bu flag'e bakılarak Ctrl tuşu ile bu tuşa basılıp basılmadığı anlaşılmaktadır. Pekiyi biz Linux'ta stdin dosyasından (0 numaralı betimleyici) okuma yaptığımızda neler olmaktadır? İşte aslında işletim sistemi bir tula basıldığında basılan tuşları klavye denetleyicisinde alır ve onları biz kuyruk sisteminde saklar. Terminal aygıt sürücüsü de bu kuyruğa başvurur. Kurukta hiç tuş yoksa thread'i bu amaçla oluşturulmuş bir wait kuruğunda bekletir. Klavyeden tuşa basılınca wait kuruğunda bekleyen thread'leri uyandırır. Yani okuma yapıldığında o anda klavyeden okuma yapılmamaktadır. Kuyruklanmış tuşlar okunmaktadır. Klavyedeki tuşların üzerinde yazan harflerin hiçbir önemi yoktur. Yani İngilizce klavye ile Türkçe klavye aynı tuşlar için aynı scan kodu göndermektedir. Basılan tuşun hangi tuş olduğu aslında dil ayarlarına bakılarak işletim sistemi tarafından anlamlandırılmaktadır. Klavye ile bilgisayar arasındaki iletişim tek yönlü değil çift yönlüdür. Yani klavye denetleyicisi de (PC tarafındaki denetleyici) isterse klavye içerisindeki işlemciye komutlar gönderebilmektedir. Aslında klavye üzerindeki ışıkların yakılması da klavyenin içerisinde tuşa basılınca yapılmamaktadır. Işıklı tuşlara basıldığında gönderilen scan kod klavye denetleyicisi tarafından alınır, eğer bu tuş ışıklı tuşlardan biri ise klavye denetleyicisi klavye işlemcisine "falanca ışığı yak" komutunu göndermektedir. Özetle yeniden ifade edersek klavyedki ışıklar klaye devresi tarafından ilgili tuşlara basılınca yakılmamaktadır. Karşı taraftan emir geldiğinde yakılmaktadır. Tasarımın bu biçimde yapılmış olması çok daha esnek bir kullanım oluşturmaktadır. Aşağıdaki örnekte klavyeden tuşa basıldığında ve çekildiğinde oluşan 1 numaralı IRQ ele alınıp işlenmiştir. * Örnek 1, /* irq-driver.c */ #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Character Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_file_ops = { .owner = THIS_MODULE, }; static irqreturn_t keyboard_irq_handler(int irq, void *dev_id); static int __init generic_init(void) { int result; if ((result = alloc_chrdev_region(&g_dev, 0, 1, "irq-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_file_ops; if ((result = cdev_add(g_cdev, g_dev, 1)) != 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "Cannot add character device driver!...\n"); return result; } if ((result = request_irq(1, keyboard_irq_handler, IRQF_SHARED, "irq-driver", &g_cdev)) != 0) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "interrupt couldn't registered!...\n"); return result; } printk(KERN_INFO "irq-driver init...\n"); return 0; } static irqreturn_t keyboard_irq_handler(int irq, void *dev_id) { static int count = 0; ++count; if (count % 1000 == 0) printk(KERN_INFO "Keyboard IRQ occurred: %d\n", count); return IRQ_HANDLED; } static void __exit generic_exit(void) { free_irq(1, &g_cdev); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "irq-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); Bazen sistem programcısı belli bir IRQ'yu belli süre için disable etmek isteyebilir. Bunun için disable_irq ve enable_irq isimli iki kernel fonksiyonu kullanılmaktadır. Bu fonksiyonlar belli numaralı bir IRQ'yu disable ve enable etmeketdir. Ancak bu fonksiyonlar bu işlemi doğrudan kesme denetleyicisini (PIC ya da IOAPIC) programlayarak yapmamaktadır. void disable_irq(unsigned int irq); void enable_irq(unsigned int irq); Fonksiyonlar IRQ numarasını parametre olarak alır. Şimdi de yukarıda kabaca giriş yaptığımız blok aygıt sürücülere değinelim; Anımsanacağı üzere aygıt sürücülerinin, "karakter aygıt sürücüleri (character device driver)", "blok aygıt sürücüleri (block device driver)" olmak üzere ikiye ayrıldığından ve Karakter Aygıt Sürücülerinin ne olduğundan bahsetmiştik. Şimdi de Blok Aygıt Sürücülerine değineceğiz. Blok aygıt sürücüleri (block device drivers) disk benzeri birimlerden bloklu okuma ve yazma yapabilmek için kullanılan özel aygıt sürücülerdir. Daha önceden de belirttiğimiz gibi disk benzeri birimlerden bir hamlede okunabilecek ya da yazılabilecek bilgi miktarına "sektör" denilmektedir. İşte blok aygıt sürücüleri transferleri byte byte değil blok blok (sektör sektör) yapmaktadır. Örneğin bir diskten 1 byte okuma diye bir şey yoktur. Ya da bir diske 1 yazma diye bir şey yoktur. Diskteki 1 byte değiştirilecekse önce onun bulunduğu sektör RAM' okunur, değişiklik RAM üzerinde yapılır. Sonra o sektör yeniden diske yazılır. Tipik transfer bu adımlardan geçilerek gerçekleştirilmektedir. Bir sektör değişebilse de hemen her zaman 512 byte'tır. Bir Linux sistemini kurduğumuzda "/dev" dizininin altında disklerle işlem yapan aygıt sürücülere yönelik aygıt dosyaşarı da oluşturulmuş durumdadır. Blok aygıt sürücülerine ilişkin aygıt dosyaları "ls -l" komutunda dosya türü olarak 'b' biçiminde görüntülenmektedir. Örneğin: $ ls -l /dev ... brw-rw---- 1 root disk 8, 0 Ağu 7 13:57 sda brw-rw---- 1 root disk 8, 1 Ağu 7 13:57 sda1 brw-rw---- 1 root disk 8, 2 Ağu 7 13:57 sda2 brw-rw---- 1 root disk 8, 3 Ağu 7 13:57 sda3 crw-rw----+ 1 root cdrom 21, 0 Ağu 7 13:57 sg0 crw-rw---- 1 root disk 21, 1 Ağu 7 13:57 sg1 ... Burada sda sygıt dosyası diske bir bütün olarak erişmek için sda1, sda2, sda3 aygıt dosyaları ise diskteki disk bölümlerine (partition) erişmek için kullanılmaktadır. Bu aygıt dosyalarının majör numaralarının aynı olduğuna ancak minör numaralarının farklı olduğuna dikkat ediniz. Biz bir flash belleği USB soketine taktığımızda burada flash belleğe erişmek için gerekli olan aygıt dosyaları otomatik biçimde oluşturulacaktır. İşletim sistemleri bloklu çalışan aygıtlarda erişimi hızlandırmak için ismine "IO çizelgelemesi (IO Scheduling)" denilen bir yöntem uygulamaktadır. Çeşitli prosesler diskten çeşitli sektörleri okumak istediğinde ya da yazmak istediğinde bunlar işletim sistemi tarafından birleşitirlerek disk erişimleri azaltılmaktadır. Yani bu tür transferlerde transfer talep edildiği anda değil biraz bekletilerek (çık kısa bir zaman) gerçekleştirilebilmektedir. Bu tür durumlarda işletim sistemleri ilgi thread'i bloke ederek transfer sonlanana kadar wait kuyruklarında bekletmektedir. Disk sistemi bilgisayar sistemlerinin en yavaş kısmını oluşturmaktadır. SSD diskler bile yazma bakımından RAM'e göre binlerce kat yavaştır. İşte işletim sistemleri aslında ayrık olan birtakım okuma yazma işlemlerini diskte mümkün olduğunca ardışıl hale getirerek disk erişiminden kaynaklanan zaman kaybını minimize etmeye çalışır. Disk sistemlerinde ayrık işlemler yerine peşi sıra blokların tek hamlede okunup yazılması ciddi hız kazancı sağlayabilmektedir. Farklı proseslerin sektör okuma istekleri aslında bazen birbirine yakın bölgelerde gerçekleşit. İşte onların yniden sıralanması gibi faaliyetler IO çizelgeleycisinin önemli görevlerindendir. Blok aygıt sürücüleri bazı bakımlardan karakter aygıt aygıt sürücülerine benzese de bu IO çizelgelemesi yüzünden tasarımsal farklılıklara sahiptir. Karakter aygıt sürücülerinde her read ve write işlemi için bir IO çizelgelemesi yapılmadan aygıt sürücünün fonksiyonları çağrılmaktadır. Çünkü karakter aygıt sürücülerinde neticede disk gibi zaman alıcı birimler ile uğraşılmamaktadır. Ancak blok aygıt sürücülerinde transfer isteği çizelgelenerek bazen gecikmelerle yerine getirilmektedir. Bir blok aygıt sürücüsü oluşturmak için ilk yapılacak işlem tıpkı karakter aygıt sürücülerinde olduğu gibi blok aygıt sürücüsünün bir isim altında aygıt numarası belirtilerek register ettirilmesidir. Bu işlem register_blkdev fonkiyonuyla yapılmaktadır: #include int register_blkdev(unsigned int major, const char *name); Fonksiyonun birinci parametresi aygıtın majör numarasını belirtir. Eğer majör numara olarak 0 geçilirse fonksiyon boş bir majör numarayı kendisi tahsis etmektedir. Fonksiyonun ikinci parametresi ise "/proc/devices" dosyasında görüntülenecek olan ismi belirtmektedir. Fonksiyon başarı durumunda majör numaraya, başarısızlık durumunda negatif error koduna geri dönmektedir. İkinci parametre aygıt sürücünün /proc/devices dosyasında görüntülenecek ismini belirtmektedir. Örneğin: if ((g_major = register_blkdev(0, "generic-blkdev")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } Modül boşaltılırken bu işlemin geri alınması için unregister_blkdev fonksiyonu kullanılmaktadır: #include void unregister_blkdev(unsigned int major, const char *name); Fonksiyonun parametrik yapısı register_blkdev fonksiyonuyla tamamen aynıdır. Örneğin: unregister_blkdev(g_major, "generic-blkdev"); Bizim daha önce kullandığımız "load" script'i karakter aygıt aygıt dosyası yaratıyordu. Halbuki bizim artık blok aygıt dosyaları yaratmamız gerekir. Bunun için "load" ve "unload" script'lerini "loadblk" ve "unloadblk" ismiyle yeniden yazacağız. Tabii aslında "unload" script'inde değiştirilecek bir şey yoktur. Ancak isimsel uyumluluk bakımından biz her iki dosyayı da yeniden yeni isimlerle oluşturacağız: /* loadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unloadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* generic-blkdev-driver.c */ #include #include #include #include MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Block Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static int g_major; static int __init generic_init(void) { int result; if ((g_major = register_blkdev(0, "generic-blkdev")) < 0) { printk(KERN_INFO "cannot alloc block driver!..\n"); return result; } printk(KERN_INFO "generic-block-driver init...\n"); return 0; } static void __exit generic_exit(void) { unregister_blkdev(g_major, "generic-blkdev"); printk(KERN_INFO "generic-block-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unloadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module Blok aygıt sürücüleri için en önemli nesne gendisk isimli nesnesidir. Blok aygıt sürücü çekirdekte bu nesne ile temsil edilmektedir. gendisk nesnesi alloc_disk isimli fonksiyonla (aslında bir makro olarak yazılmıştır) tahsis edilmektedir. Ancak 5'li çekirdeklerle birlikte fonksiyonun ismi blk_alloc_disk biçiminde değiştirilmiştir. Ayrıca aşağıdaki fonksiyonların bir bölümünün bulunduğu dosyası da çekirdeğin 5.18 versiyonunda kaldırılmış buradaki fonksiyonların prototipleri dosyasına taşınmıştır. #include struct gendisk *alloc_disk(int minors); /* eski çekirdek versiyonları bu fonksiyonu kullanıyor */ struct gendisk *blk_alloc_disk(int minors); /* yeni çekirdek versiyonlar bu fonksiyonu kullanıyor */ Fonksiyonlar parametre olarak aygıt sürücünün destekleyeceği minör numara sayısını almaktadır. Geri dönüş değeri de diski temsil eden gendisk isimli yapı nesnesinin adresidir. (Birden fazla minör numaranın söz konusu olduğu durumda geri döndürülen adres aslında bir dizi adresidir.) Fonksiyonlar başarsızlık durumunda NULL adrese geri dönmektedir. Başarısızlık durumunda bu fonksiyonları çağıran aygıt sürücü fonksiyonlarını -ENOMEM değeri ile geri döndürebilirsiniz. Örneğin: static struct gendisk *g_gdisk; if ((g_gdisk = blk_alloc_disk(1)) == NULL) { ... return -NOMEM; } alloc_disk ile elde edilen gendisk nesnesinin içinin doldurulması gerekmektedir. Bu yapının doldurulması gereken elemanları şunlardır: -> Yapının major isimli elemanına aygıt sürücünün majör numarası yerleştirilmelidir. Örneğin: g_gdisk->major = g_major; -> Yapının first_minor elemanına aygıt sürücünün ilk minör numarası yerleştirilmelidir (Tipik olarak 0). Örneğin: g_gdisk->first_minor = 0; -> Yapının flags elemanına duruma göre bazı bayraklar girilebilmektedir. Öreğin ramdisk için bu bayrak GENHD_FL_NO_PART_SCAN biçiminde girilebilir. Örneğin: g_gdisk->flags = GENHD_FL_NO_PART_SCAN; -> Yapının fops elemanına aygıt sürücü açıldığında, kapatıldığında, ioctl işlemi sırasında vs. çağrılacak fonksiyonların bulunduğuğu block_device_operations isimli yapının adresi atanmalıdır. Bu yapı karakter aygıt sürücülerindeki file_operations yapısına benzetilebilir. Yapıının iki önemli elemanı open ve release elemanlarıdır. Burada belirtilen fonksiyonlar aygıt sürücü açıldığında ve her kapatıldığında çağrılmaktadır. Örneğin: static struct block_device_operations g_bops = { /* ... */ }; g_gdisk->fops = &g_bops; -> Yapının queue elemanına programcı tarafından yaratılacak olan request_queue nesnesinin adresi atanmalıdır. request_queue aygıt sürücüden read/write işlemi yapıldığında çekirdeğin IO çizelgeleyici alt sistemi tarafından optimize edilen işlemlerin yerleştirileceği kuyruk sistemidir. Programcı ileride görüleceği üzere bu kuyruk sisteminden istekleri alarak yerine getirir. Maalesef bu kuyruğun yaratılması ve işleme sokulması için gerekli kernel fonksiyonları kernel'ın çeşitli versiyonlarında değiştirilmiştir. Eskiden 4'lü çekirdeklerde request_queue nesnesi oluşturmak için blk_init_queue isimli bir fonksiyon kullnılıyordu. Sonra 5'li çekirdeklerle birlikte request_queue işlemleri üzerinde değişiklikler yapıldı. Biz kursumuzda 5'li çekirdekler kullanıyoruz. Ancak önceki çekirdeklerdeklerde kullanılan fonksiyonlar üzerinde de durmak istiyoruz. Burada önce eski çekirdeklerdeki request queue işlemlerini ele alıp sonra ayrı paragrafta yeni çekirdeklere ilişkin değişiklikler üzerinde duracağız. Eski çekirdeklerde request_queue nesnesi oluşturmak için blk_init_queue isimli bir fonksiyon kullanılıyordu. Fonksiyonun prototipi şöyledi: #include typedef void (request_fn_proc) (struct request_queue *q); struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock); Fonksiyonun birinci parametresi kuyruktan gelen istekleri almakta kullanılan request_fn_proc türünden bir nesnenin adresini almaktadır. İkinci parametre kuyruğu korumak için kullanılan spinlock nesnesini belirtmektedir. Fonksiyonun birinci parametresine geri dönüş değeri void olan parametresi struct gendisk * türünden olan bir fonksiyonun adresi girilmelidir. Bu fonksiyon kuyruk işlemlerini yapmak için bulundurulur. blk_init_queue fonksiyonu başarı durumunda request_queue nesnesinin adresi ile, başarısızlık durumunda NULL adresle geri dönmektedir. Bu durumda çağıran fonksiyonu -ENOMEM değeri geri döndürebilirsiniz. Örneğin: static struct request_queue *g_rq; static spinlock_t g_sl; static void request_proc(struct request_queue *rq) { /* ... */ } ... if ((g_rq = blk_init_queue(request_proc, &g_sl)) == NULL) { ... retur -ENOMEM; } Biz burada elde ettiğimiz request_queue nesnesinin adresini gendisk yapısının queue elemanına atamalıyız. Örneğin: g_gdisk->queue = g_rq; blk_init_queue fonksiyonuyla yaratılan kuyruk nesnesinin kullanım bittikten sonra cleanup_queue fonksiyonuyla boşaltılması gerekmektedir: #include void blk_cleanup_queue(struct request_queue *rq); Fonksiyon parametre olarak request_queue nesneesinin adresini almaktadır. Yeni kuyruk fonksiyonları izleyen paragraflarda ele alınacaktır. -> gendik yapısının içerisine diskin (blok aygıt sürücüsünün temsil ettiği medyanın) kapasitesi set edilmelidir. Bu işlem set_capacity fonksiyonuyla yapılmaktadır. Fonksiyon prototipi şöyledir: #include void set_capacity(struct gendisk *disk, sector_t size); Fonksiyonun birinci parametresi gendisk yapısının adresini, ikinci parametresi aygıtın sektör uzunluğunu almaktadır. (Aslında bu fonksiyon gendisk yapısının ilgili elemanını set etmektedir.) Fonksşyonun ikinci parametresi aygıt sürücünün temsil ettiği aygıtın sektör uzunluğunu almaktadır. Bir sektör 512 byte'tır. Örneğin: set_capacity(g_gdisk, 1000); -> gendisk yapısının disk_name isimli elemanı char türden bir dizi belirtmektedir. Bu diziye disk isminin bulunduğu yazı kopyalanmalıdır. Disk ismi "/sys/block" dizininde görüntülenecek dosya ismini belirtmektedir. Örneğin: strcpy(g_gdisk->disk_name, "myblockdev"); -> Nihayet gendisk yapısının private_data elemanına programcı kendi yapı nesnesinin adresini yerleştirebilir. Örneğin daha önce karakter aygıt sürücülerinde yaptığımız gibi bu private_data elemanına gendisk nesnesinni içinde bulunduğu yapı nesnesinin adresini atayabiliriz. Aşağıdaki örnekte yukarıda anlatılan kısma kadar olan işlemleri içeren bir blok aygıt sürücü örneği verilmiştir. Örneğin: struct BLOCKDEV { spinlock_t sl; struct gendisk *gdisk; struct request_queue *rq; size_t capacity; }; static struct BLOCKDEV g_bdev; ... g_gdisk->private_data = &g_bdev; Tabii buradaki örnekte g_bdev zaten bir nesne oludğu için ona private_data yoluyla erişmeye gerek kalmamaktadır. Ancak aygıt sürücümüz birden fazla minör numarayı destekliyorsa her aygıtın ayrı bir BLOCKDEV yapısı olacağı için ilgili aygıta bu private_data elemanı yoluyla erişebiliriz. blk_alloc_disk fonksiyonu ile elde edilen gendisk nesnesi add_disk fonksiyonu ile sisteme eklenmelidir: #include void add_disk(struct gendisk *disk); /* eski çekirdek versiyonlarındaki prototip */ Bu fonksiyonun geri dönüş değeri 5'li çekirdeklerle birlikte int yapılmıştır: int add_disk(struct gendisk *disk); /* yeni çekirdek versiyonlarındaki prototip */ Fonksiyon başarı durumunda 0 değrine başarısızlık durumunda negatif errno değerine geri dönmektedir. Örneğin: if ((result = add_disk(g_gdisk)) != 0) { ... return result; } alloc_disk ve add_disk fonksiyonuyla tahsis edilen ve sisteme yerleştirilen gendisk nesnesi del_gendisk fonksiyonuyla serbest bırakılmaktadır: #include void del_gendisk(struct gendisk *gdisk); Örneğin: if ((g_gdisk = alloc_disk(1)) == NULL) { ... return -ENOMEM; } if ((result = add_disk(g_bdev->gdisk)) != 0) { ... return result; } ... del_gendisk(g_gdisk); Aşağıda şimdiye kadar gördüğümüz işlemleri yapan basit bir blok aygıt sürücüsü iskeleti verilmiştir. * Örnek 1, Burada iki ayrı program veriyoruz. Birinci program global değişkenler kullanılarak yazılmıştır. İkincisi ise bu global değişkenlerin BLOCKDEV isimli bir yapıya yerleştirilmiş biçimidir. Tabii yukarıda da belirttiğimiz gibi aşağıdaki aygıt sürücü kodlarında eski request_queue fonksiyonları kullanılmıştır. Dolayısıyla bu kodlar 5'li ve sonraki çekirdeklerde derlenmeyecektir. /* generic-blkdev.c */ #include #include #include #include #include #define KERNEL_SECTOR_SIZE 512 #define CAPACITY (KERNEL_SECTOR_SIZE * 1000) MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Block Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static int g_major; static struct gendisk *g_gdisk; static struct block_device_operations g_bops = { //... }; static struct request_queue *g_rq; static struct request_queue *g_rq; static spinlock_t g_sl; typedef void (request_fn_proc) (struct request_queue *q); struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock); static void request_proc(struct request_queue *rq); static int __init generic_init(void) { int result; if ((g_major = register_blkdev(0, "generic-blkdev")) < 0) { printk(KERN_INFO "cannot block driver!..\n"); return result; } if ((g_gdisk = blk_alloc_disk(1)) == NULL) { printk(KERN_ERR "Cannot alloc disk!..\n"); return -ENOMEM; } if ((result = add_disk(g_gdisk)) != 0) { del_gendisk(g_gdisk); printk(KERN_ERR "Cannot add disk!..\n"); return result; } g_gdisk->major = g_major; g_gdisk->first_minor = 0; g_gdisk->flags = GENHD_FL_NO_PART_SCAN; g_gdisk->fops = &g_bops; if ((g_rq = blk_init_queue(request_proc, &g_sl)) == NULL) { del_gendisk(g_gdisk); unregister_blkdev(g_major, "generic-blkdev"); return -ENOMEM; } g_gdisk->queue = g_rq; set_capacity(g_gdisk, CAPACITY); strcpy(g_gdisk->disk_name, "myblockdev"); printk(KERN_INFO "generic-block-driver init...\n"); return 0; } static void request_proc(struct request_queue *rq) { /* ... */ } static void __exit generic_exit(void) { cleanup_queue(g_rq); del_gendisk(g_gdisk); unregister_blkdev(g_major, "generic-blkdev"); printk(KERN_INFO "generic-block-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); /* generic-blkdev.c */ #include #include #include #include #include #define KERNEL_SECTOR_SIZE 512 #define CAPACITY (KERNEL_SECTOR_SIZE * 1000) MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Block Device Driver"); MODULE_AUTHOR("Kaan Aslan"); int g_major = 0; struct BLOCKDEV { spinlock_t sl; struct gendisk *gdisk; struct request_queue *rq; size_t capacity; }; static int generic_open(struct block_device *bdev, fmode_t mode); static void generic_release(struct gendisk *gdisk, fmode_t mode); static void request_proc(struct request_queue *rq); static struct block_device_operations g_devops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; static struct BLOCKDEV *g_bdev; static int __init generic_init(void) { int result = 0; if ((g_major = register_blkdev(g_major, "generic-bdriver")) < 0) { printk(KERN_ERR "Cannot register block driver!...\n"); return g_major; } if ((g_bdev = kmalloc(sizeof(struct BLOCKDEV), GFP_KERNEL)) == NULL) { printk(KERN_ERR "Cannot allocate memory!...\n"); result = -ENOMEM; goto EXIT1; } memset(g_bdev, 0, sizeof(struct BLOCKDEV)); g_bdev->capacity = CAPACITY; spin_lock_init(&g_bdev->sl); if ((g_bdev->rq = blk_init_queue(request_proc, &g_bdev->sl)) == NULL) { printk(KERN_ERR "Canno allocate queue!...\n"); result = -ENOMEM; goto EXIT2; } g_bdev->rq->queuedata = g_bdev; if ((g_bdev->gdisk = alloc_disk(1)) == NULL) { result = -ENOMEM; goto EXIT3; } g_bdev->gdisk->major = g_major; g_bdev->gdisk->first_minor = 0; g_bdev->gdisk->flags = GENHD_FL_NO_PART_SCAN; g_bdev->gdisk->fops = &g_devops; g_bdev->gdisk->queue = g_bdev->rq; set_capacity(g_bdev->gdisk, g_bdev->capacity >> 9); g_bdev->gdisk->private_data = g_bdev; strcpy(g_bdev->gdisk->disk_name, "blockdev"); add_disk(g_bdev->gdisk); printk(KERN_INFO "Module initialized with major number %d...\n", g_major); return result; EXIT3: blk_cleanup_queue(g_bdev->rq); EXIT2: kfree(g_bdev); EXIT1: unregister_blkdev(g_major, "generic-bdriver"); return result; } static int generic_open(struct block_device *bdev, fmode_t mode) { printk(KERN_INFO "device opened...\n"); return 0; } static void generic_release(struct gendisk *gdisk, fmode_t mode) { printk(KERN_INFO "device closed...\n"); } static void request_proc(struct request_queue *rq) { /* ... */ } static void __exit generic_exit(void) { del_gendisk(g_bdev->gdisk); blk_cleanup_queue(g_bdev->rq); kfree(g_bdev); unregister_blkdev(g_major, "generic-bdriver"); printk(KERN_INFO "Goodbye...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += generic.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module b $major 0 /* unloadblk (bu satırı dosyaya kopyalamayınız ) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* sample.c */ #include #include #include #include #include #include "keyboard-ioctl.h" void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("generic-bdriver", O_RDONLY)) == -1) exit_sys("open"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Blok aygıt sürücülerinde yukarıda belirtilen işlemlerden sonra artık transfer işleminin yapılması için bulundurulan fonksiyonun (örneğimizde request_proc) yazılması aşamasına geldik. User mode kodlar tarafından blok transferine yönelik bir istek oluştuğunda (örneğin blok aygıt sürücüsünden bir sektör okunmak istediğinde) çekirdeğin IO çizelgeleyicisi bunları çizelgeleyerek uygun bir zamanda transfer edilebilmesi için bizim belirttiğimiz ve yukarıdaki örnekte ismini request_proc olarak verdiğimiz fonksiyonu çağrımaktadır. Yani örneğimizdeki request_proc bizim tarafımızdan değil çekirdek tarafından callback fonksiyon olarak çağrılmaktadır. Bizim de bu fonksiyon içerisinde kuyruğa bırakılmış blok transfer isteklerini kuyruktan alarak gerçekleştirmemiz gerekir. Örneğin bir blok aygıt sürücüsünü user mode'da open fonksiyonuyla açıp içerisinden 10 byte'ı read fonksiyonuyla okumak isteyelim. Eğer bu aygıt sürücü karakter aygıt sürücüsü olsaydı çekirdek doğrudan aygıt sürücünün read fonksiyonunu çağıracaktı. Aygıt sürücü de istenen 10 byte'ı user mode'daki adrese transfer edecekti. Halbuki blok aygıt sürücüsü durumunda çekirdek aygıt sürücüden 10 byte transfer istemeyecektir. İlgili 10 byte'ın blunduğu bloğun (block ardışıl n sektördür) transferini isteyecektir. User mod programa o bloğun içerisindeki 10 byte'ı vermek kernel'ın görevidir. Blok aygıt sürücülerinden transferler byte düzeyinde değil blok düzeyinde yapılmaktadır. Yani block aygıt sürücülerinden transfer edilecek en küçük birim 1 sektör yani 512 byte'tır. Şüphesiz kernel 10 byte okuma isteğine konu olan yerin aygıttaki sektör numarasını hesap eder ve o sektörden itibaren blok transferi ister. Kernel aygıt sürücünün transfer edeceği blokları bir kuyruk sistemine yerleştirir. Bu kuyruk sistemi request_queue denilen bir yapı ile temsil edilmiştir. Bu kuyruğun içerisindeki kuyruk elemanları request isimli yapı nesnelerinden oluşmaktadır. (Yani request_queue aslında request yapılarının oluşturduğu bir kuyruk sistenidir.) Her request nesnesi kendi içerisinde bio isimli yapı nesnelerinden oluşmaktadır. request nesnesinin içerisindeki bio nesnleri bir bağlı liste biçiminde tutulmaktadır. Her bio yapısının sonunda ise değişken sayıda (yani n tane) bio_vec yapıları bulunmaktadır. İşte transfer işini yapacak fonksiyon transfere ilişkin bilgileri bu bio_vec yapısından elde etmektedir. request_queue: request ---> request ---> request ---> request ... request: bio ---> bio ---> bio ---> bio ---> bio ---> ... bio: bio_vec[N] Buradan da görüldüğü gibi request_queue nesneleri request nesnelerinden, request nesneleri bio nesnelerinden, bio nesneleri de bio_vec nesnelerinden oluşmaktadır. İşte transfer fonksiyonu kernel tarafından çağrıldığında "request_queue içerisindeki request nesnelerine elde edip, bu request nesnelerinin içerisindeki bio nesnelerini elde edip, bio nesneleri içerisindeki bio_vec dizisinde belirtilen transfer bilgilerine ilişkin transfeleri" yapması gerekir. Şüphesiz bu işlem ancak açık ya da örtük iç içe 3 döngü ile yapılabilir. Yani bir döngü request nesnelerini elde etmeli, onun içerisindeki bir döngü bio nesnelerini elde etmeli, onun içerisindeki bir döngü de bio_vec nesleerini elde etmelidir. request_queue içerisindeki request nesnelerinin elde edilmesi birkaç biçimde yapılabilmektedir. Yöntemlerden tipik olanı şöyledir: static void request_proc(struct request_queue *rq) { struct request *rqs; for (;;) { if ((rqs = blk_fetch_request(rq)) == NULL) break; if (blk_rq_is_passthrough(rqs)) { __blk_end_request_all(rqs, -EIO); continue; } ... __blk_end_request_all(rqs, 0); } } Burada blk_fetch_request fonksiyonu kuyruğun hemen başındaki request nesnesini alarak onu kuyruktan siler. Böylece döngü içerisinde tek tek request nesneleri elde dilmiştir. blk_rq_is_passthrough fonksiyonu dosya sistemi ile alakalı olmayan request nesnelerini geçmek için kullanılır. Bazı request nesneleri transferle ilgili değildir. Bunların geçilmesi gerekmektedir. Bir request nesnesi kuyruktan alındıktan sonra akibeti konusunda çekirdeğe bilgi verilmesi gerekmektedir. İşte bu işlem __blk_end_request_all fonksiyonuyla yapılmaktadır. Bu fonksiyonun ikinci parametresi -EIO girilirse bu durum bu işlemin yapılmadığını 0 girilirse bu da bu işlemin başarılı bir biçimde yapıldığını belirtmektedir. Pekiyi çekirdek ne zaman kuyruğa request nesnesi yerleştirmektedir? Şüphesiz çekirdeğin böyle bir nesneyi kuyruğua yerleştirmesi için aygıt üzerinde bir okuma ya da yazma olayının gerçekleşmiş olması gerekir. Bunun tipik yolu aygıt dosyasının open fonksiyonuyla açılıp read ya da write yapılmasıdır. Tabii blok aygıt sürücüsü bir dosya sistemi yani bir disk bölümü haline getirilmiş olabilir. Bu durumda formatlama gibi, mount etme gibi eylemlerde de gizli bir okuma yazma işlemleri söz konusu olmaktadır. Pekiyi çekirdek aygıt sürücü açılıp aşağıdaki gibi iki ayrı read işleminde iki ayrı request nesnesi mi oluşturmaktadır? if ((fd = open("generic-bdriver", O_RDONLY)) == -1) exit_sys("open"); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); Aslında çekirdek burada iki okumanın aynı blok içerisinde kaldığını anladığı için aygıttan yalnızca tek blokluk okuma talep edecektir. Çünkü okunan iki kısım da aynı blok içerisindedir. (Çekirdeğin bu biçimde düzenleme yapan kısmına "IO çizelgeleyicisi (IO scheduler)" denilmektedir.) Eğer bu iki okuma aşağıdaki gibi yapılmış olsaydı bu durumda çekirdek iki farklı blok için iki farklı request nesnesi oluşturacaktı: if ((fd = open("generic-bdriver", O_RDONLY)) == -1) exit_sys("open"); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); lseek(fd, 5000, 0); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); Pekiyi çekirdek aygıt sürücüden kaç byte'lık bir bloğun transferini istemektedir? Bunun bir sektör olması gerektiğini düşünebilirsiniz. Ancak çekirdek sektör küçük olduğu için aygıt sürücüden bir "blok" transfer istemektedir. Bir blok ardışıl n tane sektörden oluşmaktadır (örneğin bir blok 8 sektörden yani 4K'dan oluşabilir) ve bu durum çekirdek konfigürasyonuna bağlıdır. Ancak çekirdek transfer isteklerinde istenen bloğu her zaman sektör olarak ifade etmektedir. Başka bir deyişle çekirdek aygıt sürücüden "şu sektörden itibaren 4096 byte transfer et" gibi bir istekte bulunmaktadır. Şimdi biz request nesnesinin içerisinden bio nesnelerini, bio nesnelerinin içerisinden de bio_vec nesnelerini elde edelim. Pekiyi çekirdek neden transfer bilgilerini doğrudan request nesnelerinin içerisine kodlamak yerine böylesi iç nesneler oluşturmuştur? İşte aslında bir request nesnesi ardışıl sektör transferi ile ilgilidir. Ancak bu ardışıl sektörler read ya da write biçimde olabilir. İşte bu read ve write yönleri o request nesnesinin ayrı bio nesnelerinde kodlanmıştır. Read ve write işlemleri ise aslında birden fazla tampon ile (yani aktarılacak hedef adres ile) ilgili olabilir. Bu bilgiler de bio_vec içerisine kodlanmıştır. Dolayısıyla aslında programcı tüm bio'lar içerisindeki bio_vec'leri dolaşmalıdır. Bir süre önceye kadar request içerisindeki bio nesnelerinin dolaşılması normal kabul ediliyordu. Ancak belli bir çekirdekten sonra bu tavsiye edilmemeye başlanmıştır. Fakat yine de request içerisindeki bio nesneleri şöyle dolaşılabilir: struct bio *bio; ... bio = rqs->bio; for_each_bio(bio) { ... } for_each_bio makrosunun artık doğrudan yukarıdaki biçimde kullanılması önerilmemektedir. Artık bugünlerde bio nesnelerini dolaşmak yerine zaten bio nesnelerini ve onların içerisindeki bio-vec'leri dolaşan (yani içteki iki döngünün işlevini tek başına yapan) rq_for_each_segment isimli makronun kullanılması tavsiye edilmektedir. Bu makro şöyle yzılmıştır: #define rq_for_each_segment(bvl, _rq, _iter) \ __rq_for_each_bio(_iter.bio, _rq) \ bio_for_each_segment(bvl, _iter.bio, _iter.iter) Görüldüğü gibi aslında bu makro request nesnesi içerisindeki bio nesnelerini, bio nesneleri içerisindeki bio_vec nesnelerini dolaşmaktadır. rq_for_each_segment makrosunun birinci parametresi bio_vec tründen nesneyi almaktadır. Makronun ikinci parametresi request nesnesinin adresini almaktadır. Üçüncü parametre ise dolaşım sırasında kullanılacak bir iteratör nesnesidir. Bu nesne req_iterator isimli bir yapı türünden olmalıdır. Bu durumda transfer fonksiyonun yeni durumu aşağıdaki gibi olacaktır: static void request_proc(struct request_queue *rq) { struct request *rqs; struct bio_vec biov; struct req_iterator iterator; struct BLOCKDEV *bdev = (struct BLOCKDEV *) rq->queuedata; for (;;) { if ((rqs = blk_fetch_request(rq)) == NULL) break; if (blk_rq_is_passthrough(rqs)) { __blk_end_request_all(rqs, -EIO); continue; } rq_for_each_segment(biov, rqs, iterator) { /* biov kullanılarak transfer yapılır */ } __blk_end_request_all(rqs, 0); printk(KERN_INFO "new request object\n"); } } Gerçek transfer bilgileri bio_vec yapılarının içerisinde olduğuna göre acaba bu yapı nasıldır? İşte bu yapı şöyle bildirilmiştir: struct bio_vec { struct page *bv_page; unsigned int bv_len; unsigned int bv_offset; }; Yapının bv_page elemanı transferin yapılacağı adresi belirtmektedir. Çekirdek ismine eskiden "buffer cache" denilen daha sonra "page cache" denilen bir cache veri yapısı kullanmaktadır. İşletim sistemi read/write işlemlerinde önce bu "page cache" e başvurmakta eğer ilgili blok cache'te varsa buradan almaktadır. Eğer ilgili blok cache'te yoksa blok aygıt sürücüsünden transfer istemektedir. Blok aygıt sürücüleri karakter aygıt sürücülerinin yaptığı gibi transferleri user alanına kopyalamamaktadır. Blok aygıt sürücüleri transferi çekirdek tarafından tahsis edilen cache'teki sayfalara yapar. Çekirdek o sayfalardaki bilgiyi user alanına transfer eder. Ancak bu sayfa adresinin özellikle 32 bit Linux sistemlerinde sayfa tablosunda girişi olmayabilir. Programcının bu girişi oluşturması gerekmektedir. Bu girişi oluşturmak için kmap_atomic isimli fonksiyon ve girişi boşaltmak için ise kunmap_atomic isimli fonksiyon kullanılmaktadır. Yapının ikinci elemanı olan bv_len transfer edilecek byte sayısını barındırmaktadır. (Bu elemanda transfer edilecek sektör sayısı değil doğrudan byte sayısı bulunmaktadır.) Nihayet yapının üçüncü elemanı olan bv_offset birinci elemanında belirtilen adresten uzaklığı tutmaktadır. Yani aslında gerçek transfer adresi bv_page değildir. bv_page + bv_offset'tir. Bu yapıda en önemli bilgi olan transferin söz konusu olduğu sektör bilgisi yoktur. İşte aslında transfere konu olan sektör numarası bio yapısının içerisindedir. Her ne kadar biz yukarıdaki makroda bio yapısına erişemiyor olsak da buna dolaylı olarak iterator nesnesi yoluyla "iterator.iter.bi_sect" ifadesi ile erişilebilmektedir. Bu durumda rq_for_each_segment fonksiyonu içerisinde transfer tipik olarak şöyle yapılmaktadır: rq_for_each_segment(biov, rqs, iterator) { sector_t sector = iterator.iter.bi_sector; char *buf = (char *)kmap_atomic(biov.bv_page); size_t len = biov.bv_len; int direction = bio_data_dir(iterator.bio); transfer_block(bdev, sector, buf + biov.bv_offset, len, direction); kunmap_atomic(buf); } Burada transferin yapılacağı sektörün numarası bio_vec içerisinde bulunmadığından bu bilgi iterator yolu ile "iterator.iter.bi_sector" ifadesiyle alınmıştır. Transfer adresi olan buf adresi biov.bv_page adresinden biov.bv_offset kadar ileridedir. Yine transferin yönü doğrudan bio_vec yapısının içerisinde değildir. Bu yön bilgisinin bio_data_dir makrosuyla iterator yoluyla alındığına dikkat ediniz. (Aslında yön bilgisi bio yapısının içerisindedir. Bu makro onu buradan almaktadır.) Örneğimizde transferin transfer_block isimli fonksiyonla yapldığını görüyorsunuz. Bu fonksiyon bizim tarafımızdan yazılan gerçek transferin yapıldığı fonksiyondur. Aygıt sürücünün en önemli bilgilerinin bizim tarafımızdan oluşturulan BLOCKDEV isimli yapıda tutulduğunu anımsayınız. Her ne kadar bu yapı nesnesinin adresi global g_bdev göstericisinde tutuluyor olsa da örneğimizde requeat_queue nesnesinin queuedata elemanından çekilerek alınıp transfer_block fonksiyonuna verilmiştir. Bu durum yukarıdaki örnek için gereksiz olsa da birden fazla minör numarayla çalışıldığı durumda aygıt bilgilerinin yerinin belirlebilmesi için genel bir çözüm oluşturmak amacıyla transfer_block fonksiyonuna aktarılmıştır. Aygıt bilgilerinin request_queue yapısının quedata elemanına nesne tahsis edildikten sonra yerleştirildiğini anımsayınız. Buradaki bizim tarafımızdan yazılması gereken transfer_block fonksiyonun parametrik yapısı şöyle olabilir: static void transfer_block(struct BLOCKDEV *bdev, sector_t sector, char *buf, size_t len, int direction); Buradaki direction değeri READ (0) ve WRITE (1) sembolik sabitleriyle define edilmiştir. Aşağıda RAMDISK 5'li çekirdekler öncesinde kullabileceğiniz block aygıt sürücüsünün tüm kodlarını görüyorsunuz. * Örnek 1, /* ramdisk-driver.c */ #include #include #include #include #include #define KERNEL_SECTOR_SIZE 512 #define CAPACITY (KERNEL_SECTOR_SIZE * 1000) MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Ramdisk Device Driver"); MODULE_AUTHOR("Kaan Aslan"); int g_major = 0; struct BLOCKDEV { spinlock_t sl; struct gendisk *gdisk; struct request_queue *rq; size_t capacity; void *data; }; static int generic_open(struct block_device *bdev, fmode_t mode); static void generic_release(struct gendisk *gdisk, fmode_t mode); static void request_proc(struct request_queue *rq); static void transfer_block(struct BLOCKDEV *bdev, sector_t sector, char *buf, size_t len, int direction); static struct block_device_operations g_devops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; static struct BLOCKDEV *g_bdev; static int __init generic_init(void) { int result = 0; if ((g_major = register_blkdev(g_major, "generic-bdriver")) < 0) { printk(KERN_ERR "Cannot register block driver!...\n"); return g_major; } if ((g_bdev = kmalloc(sizeof(struct BLOCKDEV), GFP_KERNEL)) == NULL) { printk(KERN_ERR "Cannot allocate memory!...\n"); result = -ENOMEM; goto EXIT1; } memset(g_bdev, 0, sizeof(struct BLOCKDEV)); g_bdev->capacity = CAPACITY; if ((g_bdev->data = vmalloc(CAPACITY)) == NULL) { printk(KERN_ERR "Cannot allocate memory!...\n"); result = -ENOMEM; goto EXIT2; } spin_lock_init(&g_bdev->sl); if ((g_bdev->rq = blk_init_queue(request_proc, &g_bdev->sl)) == NULL) { printk(KERN_ERR "Canno allocate queue!...\n"); result = -ENOMEM; goto EXIT3; } g_bdev->rq->queuedata = g_bdev; if ((g_bdev->gdisk = alloc_disk(1)) == NULL) { result = -ENOMEM; goto EXIT4; } g_bdev->gdisk->major = g_major; g_bdev->gdisk->first_minor = 0; g_bdev->gdisk->flags = GENHD_FL_NO_PART_SCAN; g_bdev->gdisk->fops = &g_devops; g_bdev->gdisk->queue = g_bdev->rq; set_capacity(g_bdev->gdisk, g_bdev->capacity >> 9); g_bdev->gdisk->private_data = g_bdev; strcpy(g_bdev->gdisk->disk_name, "blockdev"); add_disk(g_bdev->gdisk); printk(KERN_INFO "Module initialized with major number %d...\n", g_major); return result; EXIT4: blk_cleanup_queue(g_bdev->rq); EXIT3: vfree(g_bdev->data); EXIT2: kfree(g_bdev); EXIT1: unregister_blkdev(g_major, "generic-bdriver"); return result; } static int generic_open(struct block_device *bdev, fmode_t mode) { printk(KERN_INFO "device opened...\n"); return 0; } static void generic_release(struct gendisk *gdisk, fmode_t mode) { printk(KERN_INFO "device closed...\n"); } static void request_proc(struct request_queue *rq) { struct request *rqs; struct bio_vec biov; struct req_iterator iterator; struct BLOCKDEV *bdev = (struct BLOCKDEV *)rq->queuedata; for (;;) { if ((rqs = blk_fetch_request(rq)) == NULL) break; if (blk_rq_is_passthrough(rqs)) { __blk_end_request_all(rqs, -EIO); continue; } rq_for_each_segment(biov, rqs, iterator) { sector_t sector = iterator.iter.bi_sector; char *buf = (char *)kmap_atomic(biov.bv_page); size_t len = biov.bv_len; int direction = bio_data_dir(iterator.bio); transfer_block(bdev, sector, buf + biov.bv_offset, len, direction); kunmap_atomic(buf); } __blk_end_request_all(rqs, 0); } } static void transfer_block(struct BLOCKDEV *bdev, sector_t sector, char *buf, size_t len, int direction) { if (direction == READ) memcpy(buf, (char *)bdev->data + sector * KERNEL_SECTOR_SIZE, len); else memcpy((char *)bdev->data + sector * KERNEL_SECTOR_SIZE, buf, len); } static void __exit generic_exit(void) { del_gendisk(g_bdev->gdisk); blk_cleanup_queue(g_bdev->rq); vfree(g_bdev->data); kfree(g_bdev); unregister_blkdev(g_major, "generic-bdriver"); printk(KERN_INFO "Goodbye...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += generic.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module b $major 0 /* unloadblk (bu satırı dosyaya kopyalamayınız ) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module Yukarıdaki örneklerdeki request kuyruk yapısı Linux'un 5'ten önceki çekirdeklerine özgüdür. Maalesef blok aygıt sürücülerinin içsel yapısı birkaç kere değiştirilmiştir. Burada yeni çekirdeklerdeki request kuyruk yapısı üzerinde duracağız. Yeni çekirdeklerde bu kuyruk yapısı şöyle kullanılmaktadır: -> Programcı blk_mq_tag_set isimli yapı türünden bir yapı nesnesi oluşturur. Bu yapı nesnesi blok yagıt sürücüsünün bilgilerinin tutulacağı yapının bir elemanı olarak alınabilir. Ya da global bir değişken olarak alınabilir. #include struct BLOCKDEV { spinlock_t sl; struct gendisk *gdisk; struct blk_mq_tag_set ts; struct request_queue *rq; size_t capacity; void *data; }; static struct BLOCKDEV *g_bdev; -> Bu blk_mq_tag_set yapısının içi aşağıdaki gibi doldurulur: g_bdev->ts.ops = &g_mqops; g_bdev->ts.nr_hw_queues = 1; g_bdev->ts.queue_depth = 128; g_bdev->ts.numa_node = NUMA_NO_NODE; g_bdev->ts.cmd_size = 0; g_bdev->ts.flags = BLK_MQ_F_SHOULD_MERGE; g_bdev->ts.driver_data = &g_bdev; Buradaki elemanlar çekirdeğin blok aygıt sürücü mimarisiyle ilgilidir. Biz burada tipik değerler kullandık. blk_mq_tag_set yapısının ops elemanı transfer isteklerini yerine getiren ana fonksiyonun adresini almaktadır. Bu fonksiyonun parametrik yapısı şöyle olmalıdır: static blk_status_t request_proc(struct blk_mq_hw_ctx *ctx, const struct blk_mq_queue_data *data); Yapının driver_data elemanına bu fonksiyon içerisinde eişilebilecek nesnenin adresi girilmelidir. Örneğimizde bu elemana g_bdev nesnesinin adresini girdik. Tabii aslında bu nesne global olduğu için zaten bu nesneye her yerden erişilebilmektedir. Ancak birden fazla minör numaranın desteklendiği durumda buraya ilgili minör numaraya ilişkin aygıt bilgisi girilebilir. -> İçi doldurulan blk_mq_tag_set nesnesi blk_mq_alloc_tag_set fonksiyonu ile tahsis edilip set edilmelidir. Fonksiyonun prototipi şöyledir: #include int blk_mq_alloc_tag_set(struct blk_mq_tag_set *set); Fonksiyon blk_mq_tag_set yapı nesnesinin adresini alır. Başarı durumunda 0 değerine başarısızlık durumunda negatif errno değerine geri döner. Yukarıdaki blok aygıt sürücüsü bir dosya sistemi ile formatlanarak sanki bir disk bölümüymüş gibi de kullanılabilir. Bunun için önce aygıt sürücünün idare ettiği alanın formatlanması gerekir. Formatlama işlemi mkfs isili utility programla yapılmaktadır: $ sudo mkfs -t ext2 generic-bdriver Burada -t seçeneği volümün hangi dosya sistemi ile formatlanacağını belirtmektedir. Formatlama aslında volümdeki bazı sektörlerde özel meta alanlarının yaratılması anlamına gelmektedir. Dolayısıyla mkfs komutu aslında ilgili aygıt sürücüyü açıp onun bazı sektörlerine bazı bilgileri yazmaktadır. Formatlama işleminden sonra artık blok aygıt sürücüsünün mount edilmesi gerekmektedir. mount işlemi bir dosya sisteminin dizin ağacının belli bir dizinine monte edilmesi anlamına gelmektedir. Dolayısıyla mount komutunda kullanıcı block aygıt sürücüsünü ve mount edilecek dizini girmektedir. Örneğin: $ sudo mount generic-bdriver /mnt/myblock Burada mount noktası (mount point) /ment dizinin altında myblock isimli dizindir. Bu dizinin kullanıcı tarafından önceden mkdir komutu ile yaratılması gerekir. Tabii mount noktalarının /mnt dizinin altında bulndurulması gibi zorunluluk yoktur. Mount noktasına ilişkin dizinin içinin boş olması da gerekmez. Fakat mount işleminden sonra artık o dizinin altı görünmez. Dosya sisteminin kök dizini o dizin olacak biçimde dosya sistemi ağaca monte edilmiş olur. mount komutu aslında mount isimli bir sistem fonksiyonu çağrılarak gerçekleştirilmektedir. Yani aslında bu işlem programlama yoluyla da yapılabilmektedir. Aygıt sürücümüzü mount ettikten sonra artık onu unmount etmeden rmmod komutuyla boşaltamayız. mount edilen dosya sistemi umount komutuyla eski haline getirilmektedir. Örneğin: $ sudo umount /mnt/myblock umount komutunun komutu argümanının mount noktasını belirten dizin olduğuna dikkat ediniz. Tabii aslında umount komutu da işlemini umount isimli sistem fonksiyonuyla yapmaktadır. > Hatırlatıcı Notlar: >> Bir kernel modülü yazarken o modül le ilgili önemli bazı belirlemeler "modül makroları" denilen "MODULE_XXXX" biçimindeki makrolarla yapılmaktadır. Her ne kadar bu modül makrolarının bulundurulması zorunlu değilse de şiddetle tavsiye edilmektedir. En önemli üç makronun tipik kullanımı şöyledir: MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); Modül lisansı herhangi bir "open-source" lisans olabilir. Tipik olarak "GPL" tercih edilmektedir. "MODULE_AUTHOR" makrosu ile modülün yazarı belirtilir. "MODULE_DESCRPTION" modülün ne iş yaprığına yönelik kısa bir başlık yazısı içermektedir. Bu makrolar global alanda herhangi bir yere yerleştirilebilmektedir. >> Tıpkı user modda olduğu gibi aygıt sürücülerde de basit atama, artırma, eksiltme gibi işlemlerin atomic yapılmasını sağlayan özel fonksiyonlar vardır. Aslında bu işlemler thread'ler konusunda görmüş olduğumuz gcc'nin built-in atomic fonksiyonlarıyla yapılabilir. Ancak çekirdek içerisindeki fonksiyonların kullanılması uyum bakımından daha uygundur. Bu fonksiyonların hepsi nesneyi atomic_t türü biçiminde istemektedir. Bu aslında içerisinde yalnızca int bir nesne olan bir yapı türüdür. Bu yapı nesnesinin içerisindeki değeri alan atomic_read isimli bir fonksiyon da vardır. Atomic fonksiyonların bazıları şunlardır: #include atomic_set atomic_add atomic_sub atomic_inc atomic_dec ... Bu fonksiyonların hepsinin atomic_t türünden nesnenin adresini alan bir parametresi vardır. atomic_set fonksiyonunun ikinci parametresi set edilecek değeri almaktadır. Yukarıda da belirttiğimiz gibi atomic_t türü aslında int bir elemana sahip bir yapı biçimindedir. atomic_t türünden bir değişkene ilkdeğer vermek için ATOMIC_INIT makrosu da kullanılabilir. Örneğin: atomic_t g_count = ATOMIC_INIT(0); Yukarıda da belirttiğimiz gibi atomic_t nesnesi içerisindeki değeri atomic_read makrosuyla elde edebiliriz. Örneğin: val = atomic_read(&g_count); Bit işlemlerine yönelik atomik işlemler de yapılabilmektedir: void set_bit(nr, void *addr); void clear_bit(nr, void *addr); void change_bit(nr, void *addr); test_bit(nr, void *addr); int test_and_set_bit(nr, void *addr); int test_and_clear_bit(nr, void *addr); int test_and_change_bit(nr, void *addr); >> Biz aygıt sürücü kodumuzda o anda quanta süresini bırakıp çizelgeleyicinin kendi algortimasına göre sıradaki thread'i çizelgelemesini sağlayabiliriz. Bunun schedule isimli fonksiyon kullanılmaktadır. Bu fonksiyon bloke oluşturmamaktadır. Yalnızca thread'ler arası geçiş (context switch) oluşturmaktadır. Fonksiyon herhangi bir parametre almamaktadır: #include void schedule(void); >> Biz kernel mode'da kod yazarken belli bir fiziksel adrese erişmek istersek onun sanal adresini bulmamız gerekir. Bu işin manuel yapılması yerine bunun için __va isimli makro kullanılmaktadır. Biz bu makroya bir fiziksel adres veririz o da bize o fiziksel adrese erişmek için gereken sanal adresi verir. Benzer biçimde bir sanal adresin fiziksel RAM karşılığını bulmak için de __pa makrosu kullanılmaktadır. Biz bu makroya sanal adresi veririz o da bize o sanal adresin aslında RAM'deki hangi fiziksel adres olduğunu verir. __va makrosu parametre olarak unsigned long biçiminde fiziksel adresi alır, o fiziksel adrese erişmek için gerekli olan sanal adresi void * türünden bize verir. __pa makrosu bunun tam tersini yapmaktadır. Bu makro bizden unsigned long biçiminde sanal adresi alır. O sanal adrese sayfa tablo tablosunda karşı gelen fiziksel adresi bize verir. Kernel mode'da RAM'in her yerine erişebildiğimize ve bu konuda bizi engelleyen hiçbir mekanizmanın olmadığına dikkat ediniz. >> Belli bir anda yüklenmiş olan modüller "/proc/modules" dosyasından elde edilebilir. Bu dosya bir "text" dosyadır. Dosyanın her satırında bir "kernel" modülün bilgisi vardır. Örneğin: "$ cat /proc/modules" helloworld 16384 0 - Live 0x0000000000000000 (OE) vmw_vsock_vmci_transport 32768 2 - Live 0x0000000000000000 vsock 40960 3 vmw_vsock_vmci_transport, Live 0x0000000000000000 snd_ens1371 28672 2 - Live 0x0000000000000000 snd_ac97_codec 131072 1 snd_ens1371, Live 0x0000000000000000 gameport 20480 1 snd_ens1371, Live 0x0000000000000000 ac97_bus 16384 1 snd_ac97_codec, Live 0x0000000000000000 binfmt_misc 24576 1 - Live 0x0000000000000000 intel_rapl_msr 20480 0 - Live 0x0000000000000000 .... Aslında yüklü modüllerin bilgileri "lsmod" isimli bir yardımcı programla da görüntülenebilmektedir. Tabii "lsmod" aslında "/proc/modules" dosyasını okuyup onu daha anlaşılır biçimde görüntülemektedir. >> "insmod" ile yüklediğimiz her modül için "/sys/module" dizinin içerisinde ismi modül ismiyle aynı olan bir dizin yaratılmaktadır. "/proc/modules" dosyası ile bu dizini karıştırmayınız. "/proc/modules" dosyasının satırları yüklü olan modüllerin isimlerini ve bazı temel bilgilerini tutmaktadır. Modüllere ilikin asıl önemli bilgiler "kernel" tarafından "/sys/modules" dizininde tutulmaktadır. "sys" dosya sistemi de "proc" dosya sistemi gibi "kernel" tarafından bellek üzerinde oluşturulan ve içeriği "kernel" tarafından güncellenen bir dosya sistemidir. Örneğin "helloworld.ko" modülünü yükledikten sonra bu dizinin içeriği şöyle görüntülenmektedir: "$ ls /sys/module/helloworld -l" toplam 0 -r--r--r-- 1 root root 4096 Mar 22 21:25 coresize drwxr-xr-x 2 root root 0 Mar 22 21:25 holders -r--r--r-- 1 root root 4096 Mar 22 21:25 initsize -r--r--r-- 1 root root 4096 Mar 22 21:25 initstate drwxr-xr-x 2 root root 0 Mar 22 21:25 notes -r--r--r-- 1 root root 4096 Mar 22 21:25 refcnt drwxr-xr-x 2 root root 0 Mar 22 21:25 sections -r--r--r-- 1 root root 4096 Mar 22 21:25 srcversion -r--r--r-- 1 root root 4096 Mar 22 21:25 taint --w------- 1 root root 4096 Mar 22 21:22 uevent