Bu yazı daha önce saltokunur.org e-dergisinde yayınlanmıştır.
Herkese merhabalar. Geçen ay başladığımız Linux sürücüsü yazma serüvenimizin ikinci yazısıyla yine beraberiz (TV programı girişi gibi oldu :) Yazının birinci kısmında Linux çekirdeğine giriş yapmış ve sürücü yazarken işimize yarayacak çeşitli konulara (sistem çağrıları gibi) değinmiştik. Bu ay bu konulara kaldığımız yerden devam ediyoruz.
Kesmeler
İşletim sisteminin donanımdaki değişiklikleri ya da ondan gelen sinyalleri algılaması (örneğin klavyeden bir tuşa basılması) için iki genel yöntem bulunur. Bunlardan birincisi polling denilen CPU’nun sürekli donanımda bir değişiklik var mı diye onu kontrol etmesidir. Ama bu yöntem sürekli işlemciyi meşgul edeceği için terk edilmiştir. İkinci ve en çok kullanılan bir yöntem ise donanımın CPU’ya kesme göndermesidir.
Mesela bilgisayarınızda Half-Life oynuyorsunuz. İşlemci siz oyunu oynarken o sırada ekranda görülmesi gereken nesnelerinin durumlarını hesaplamakla meşguldür. Patlamalar, kurşunların hedefte zarara yok açması gibi... Birden klavyeden W-A-S-D tuşlarına basarak konumunuzu değiştirdiniz. Artık ekranda farklı cisimlerin gözükmesi gerekir. İşte bu noktada klavye CPU’ya yaptığı işi KESME'sini ve klavyeden basılan tuşlara göre oyuncunun yeni konumunu hesaplaması ve ekranda gösterilecek yeni nesnelerin durumlarını hesaplamasını söyler.
Kesmeler temelde önce kesme yönetici (interrupt controller)’ ne oradan da CPU’ya gönderilen elektrik sinyalleridir. İşlemci bu sinyalleri alır ve işlemek üzere işletim sistemine gönderir. İşletim sistemi tarafında bir Interrupt’a cevap verilmek üzere çalıştırılacak olan kodların bulunduğu fonksiyona Interrupt Handler denir.
İşletim sistemi de Interrupt Handler ‘leri çağırarak bu kesmelere cevap verir. İşletim sistemi tarafından her donanıma farklı bir kesme numarası verilir. Böylece gelen kesmenin hangi donanım tarafından üretildiği belirlenir. Kesme numarasıyla birlikte bunlara Interrupt Request (IRQ) denir.
Günümüz PC’leri iç mimarilerinde donanımlar arası haberleşme için PCI veri yolunu kullanır. PCI veri yolunda IRQ’lar dinamik olarak belirlenebilir. Bu da tak ve kullan cihazların sistemin yeniden başlatılmasına gerek kalmadan hemen kullanılmasını sağlar.
Çekirdek tarafında bir IRQ’ya cevap olarak çalışan fonksiyonlara Interrupt Handler(IH) denir. Biz de sürücümüzü yazarken, donanımdan gelen isteklere cevap vermek üzere bir IH yazacağız. IH'lar normal C fonksiyonlarıdır. Çekirdek bu fonksiyonları kesmelere cevap olarak çağırır. Bir IH donanımdan gelen isteğe göre istenilen işi yapmalı ve donanıma işin sonucuna göre bir cevap vermelidir.
Bir IH'yi Sisteme Kaydetmek
Bir IH’nin donanımdan gelen isteklere cevap vermesi için IRQ ile IH’nin birbirine bağlanması gerekir. Bu request_irq() fonksiyonu ile yapılır. Bu fonksiyondaki ikinci parametre olan IH, birinci parametre olan kesmeyi işleyecek şekilde sisteme kaydedilir. Bundan sonra irq numaralı kesme geldiğinde ikinci parametredeki fonksiyon çağrılacaktır.
int request_irq(int irq, irqreturn_t (*handler) (int, void *, struct pt_regs *), long flags, char *devname, void *dev_id)
Burada ilk parametre gelen kesmenin numarası, ikinci parametre bu kesmeyi işleyecek IH’yi işaret eden bir fonksiyona göstericisi, flag parametresi çeşitli ayarlar için kullanılan bayrak değişkeni, dördüncü parametre cihazın adı, beşinci parametre cihazı gösteren işaretçidir.
IH Yazmak
Bir IH fonksiyonu şu prototipe uyar:
static irqreturn_t int_handler(int irq, void *dev_id, struct pt_regs *regs)
Burada irq kesme numarası, dev_id ise donanımı gösteren bir işaretçidir. Bu fonksiyonun içinde donanımdan gelen kesmelere cevap olacak işlemlerin yapılması gerekir. Örneğin kullanıcı klavyeden ‘A’ tuşuna bastıysa sisteme ‘A’ tuşuna basıldı sinyalinin verilmesi ve buna cevap bir işlem yapılmalıdır.
Resim 6.1 de donanımdan gelen bir kesmenin nasıl işlendiği görülüyor.
Modül Yazmak
Yavaş yavaş sürücü yazımına geçmek üzereyiz. Ancak bahsetmemiz gereken bir kaç şey daha var. Daha önce de belirttiğimiz gibi biz sürücümüzü modül olarak yazacağız ve çekirdeğe sistem çalışırken dinamik olarak yükleyeceğiz. Şimdi bu konuya bakalım.
Modüller sistem çalışırken dinamik olarak çekirdeğe yüklenebilen çekirdek programcıklarıdır. Bu özelliklerine bakarak modülleri çekirdek yamaları olarak da nitelendirebiliriz. Basit bir modül tanımı aşağıda görülmektedir. Her modülde modül ilk yüklendiğinde çekirdek tarafından çağırılan bir init fonksiyonu ve modül sistemden kaldırıldığında çağırılan bir exit fonksiyonu tanımlanmış olmalıdır. En alttaki module_init ve module_exit makroları ise bu fonksiyonları çekirdeğe bildirir. MODULE_LICENSE satırı dikkatinizi çekmiştir. Bu makro ile de modülün hangi lisans ile lisanslandığı belirtilir. Lisansın niye özellikle belirtildiği merak etmiş olabilirsiniz. Linux GPL ile dağıtıldığı için onu kullanan bütün kodların (buna modüller de dâhil) GPL ile dağıtılması gereklidir. Bu modül derlenirken açık kaynaktan başka bir lisans kullanılmışsa kod derleme sırasında hata verecektir. Açık kaynakçılar lisanslamayı bu kadar ciddi takip ediyorlar.
#include/* Needed by all modules */ #include /* Needed for KERN_INFO */ MODULE_LICENSE("GPL"); int __init init_module(void) { printk(KERN_INFO " Merhaba Kernel Programlama \n"); return 0; } void __exit exit_module(void) { printk(KERN_INFO " Güle güle Kernel Programlama \n"); } module_init(init_module); module_exit(exit_module);
Bir sürücü yazılırken yapılması gereken init fonksiyonunun içinde sürücüyü sisteme kaydetmek ve gerekli ilk yükleme işlemlerini yapmak, exit fonksiyonunun içinde ise daha önceden sürücüye tahsis edilen sistem kaynaklarını sisteme geri vermektir.
Modülü Derlemek
Daha önce anlatıldığı gibi linux-source ve linux-headers paketlerini kurduysanız, basit bir makefile yazıp modül derleme işlemini kolayca halledebilirsiniz. İçinizden makefile da ne diye soruyorsanız, makefile Linux'ta C derleyicileri için çeşitli derleme parametrelerinin ve komutlarının toplu olarak bulunduğu bir dosyadır. Derleyici bu dosyadaki ayarlara göre kaynak kodun derlemesini yapar. Biz de böylece komut satırından onlarca satır komut girmek zorunda kalmayız.
Bu da hello modülümüzü derleyecek makefile dosyasının içeriği:
ifneq ($(KERNELRELEASE),) obj-m := hello.o else KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules Endif
Bu makefile’da shell’e bazı komutlar gönderilip (shell uname -r), Linux başlık dosyalarının nerede olduğu bulunuyor ve buna göre kaynak kodumuzu derleniyor. Bu dosyadaki modülle ilgili en önemli komut;
obj-m :=hello.o
Derleyici hello.c’yi derledikten sonra oluşan hello.o dosyasından hello.ko biçiminde bir modül oluşturacaktır.
Bu makefile ile modülümüzü derledikten sonra elimizde hello.ko modülü oluşur. insmod programı modülü çekirdeğe yüklemek için kullanılır.
insmod hello.ko
Bu komut çağırılır çağrılmaz, modülün init fonksiyonu çağrılır ve komut satırında printk ile yazdırdığımız mesaj gözükür. Bazı dağıtımlarda bu çıktılar komut satırı yerine sistem loglarına basılabilir. /var/log/syslog dosyasına bakabilirsiniz.
rmmod ise modülü sistemden kaldırmak için kullanılır.
rmmod hello.ko komutunu çalıştırdığınızda modül sistemden kaldırılır ve modülün exit fonksiyonu çağrılır.
Donanım Çeşitleri
Donanımlar bilgisayarla haberleşme yöntemlerine göre 3'e ayrılır.
char: char donanımlar giriş-çıkış portlarına elektrik sinyalleri halinde tekli sırada, stream (akış) şeklinde veri gönderen cihazlardır. Klavye, fare, yazıcı, projeksiyon cihazı bunlara örnek verilebilir. Örneğin klavyeden basılan tuşlar bir akış halinde sırayla bilgisayara gönderilir. Bu cihazlar bilgisayara interrupt verisi gönderir.
block: içindeki verilere tek bir akıştan sabit hızda değil de, donanımın farklı konumlarından aynı anda rastgele ve farklı hızlarda erişilen donanımlardır. En iyi örnek hard disk gibi depolama birimleridir. Bu cihazlar ise bilgisayarla haberleşmek için bulk verisi gönderir.
network: ağ iletişimi için kullanılan, aynı anda hem okuma hem yazma yapabilen ve işlemleri ağın trafiğine ve topolojiye göre belirli bir sürede yapmak zorunda olan donanımlardır. Kablosuz ağ kartları örnek verilebilir. Bu cihazlar bilgisayara tty verisi gönderir.
Bilgisayarla haberleşme biçimleri farklı olduğu için, bu 3 farklı cihaz türünün de sürücüsü yazılırken farklı yollar izlenir. Açıklamalardan da anlayabileceğiniz sürücüsünü yazacağımız fare char tipli bir donanımdır ve iletişim için bilgisayara interrupt gönderir. Bu yüzden biz sürücü yazmayı sadece char donanımlara göre anlatacağız ve sadece interrupt verisi işleyen bir sürücü yazacağız.
USB Cihazlar
USB sürücüsü yazmaya çalıştığımız için bu noktada, USB’nin hangi türe girdiğini merak etmiş olabilirsiniz. Cevap E Hepsi :)
Usb arayüzü genel amaçlı bir arayüz olduğu için yukarıda sayılan türdeki cihazların hepsi (fare, usb hard disk, kablosuz ağ kartı) bu porta takılabilir. Bu yüzden USB sürücüsü yazma diğer arayüzlere sürücü yazmaktan daha karmaşıktır. USB sürücülerin bir sorunu da şudur ki, bir bilgisayarda genelde birden fazla USB girişi vardır. Bu yüzden USB portuna takılan bir cihazın bilgisayarın I/O portları üzerindeki adresi sabit değildir.
PCI Sistem İletişim Yolu
Güncel PC mimarisi donanımlar arası haberleşme için PCI veri yolunu kullanır. I/O portları bilgisayarın içinde hep PCI veri yollarına bağlıdır. Linux’ta lspci komutu bütün PCI cihazlarının listesini verir. Aşağıdaki komut satırı çıktısından bütün I/O portlarının ve bunların bağlı olduğu microcontroller’ların PCI veri yoluna bağlı olduğu görülür.
hamza@hamza-laptop:~$ lspci 00:00.0 Host bridge: Intel Corporation Mobile 945GM/PM/GMS, 943/940GML and 945GT Express Memory Controller Hub (rev 03) 00:01.0 PCI bridge: Intel Corporation Mobile 945GM/PM/GMS, 943/940GML and 945GT Express PCI Express Root Port (rev 03) 00:1b.0 Audio device: Intel Corporation 82801G (ICH7 Family) High Definition Audio Controller (rev 02) 00:1c.0 PCI bridge: Intel Corporation 82801G (ICH7 Family) PCI Express Port 1 (rev 02) 00:1c.1 PCI bridge: Intel Corporation 82801G (ICH7 Family) PCI Express Port 2 (rev 02) 00:1c.2 PCI bridge: Intel Corporation 82801G (ICH7 Family) PCI Express Port 3 (rev 02) 00:1d.0 USB Controller: Intel Corporation 82801G (ICH7 Family) USB UHCI Controller #1 (rev 02) 00:1d.1 USB Controller: Intel Corporation 82801G (ICH7 Family) USB UHCI Controller #2 (rev 02) 00:1d.2 USB Controller: Intel Corporation 82801G (ICH7 Family) USB UHCI Controller #3 (rev 02) 00:1d.3 USB Controller: Intel Corporation 82801G (ICH7 Family) USB UHCI Controller #4 (rev 02) 00:1d.7 USB Controller: Intel Corporation 82801G (ICH7 Family) USB2 EHCI Controller (rev 02) 00:1e.0 PCI bridge: Intel Corporation 82801 Mobile PCI Bridge (rev e2) 00:1f.0 ISA bridge: Intel Corporation 82801GBM (ICH7-M) LPC Interface Bridge (rev 02) 00:1f.2 IDE interface: Intel Corporation 82801GBM/GHM (ICH7 Family) SATA IDE Controller (rev 02) 00:1f.3 SMBus: Intel Corporation 82801G (ICH7 Family) SMBus Controller (rev 02) 01:00.0 VGA compatible controller: nVidia Corporation G73 [GeForce Go 7600] (rev a1) 05:00.0 Network controller: Intel Corporation PRO/Wireless 3945ABG [Golan] Network Connection (rev 02) 07:06.0 CardBus bridge: Texas Instruments PCIxx12 Cardbus Controller 07:06.1 FireWire (IEEE 1394): Texas Instruments PCIxx12 OHCI Compliant IEEE 1394 Host Controller 07:06.2 Mass storage controller: Texas Instruments 5-in-1 Multimedia Card Reader (SD/MMC/MS/MS PRO/xD) 07:06.3 SD Host controller: Texas Instruments PCIxx12 SDA Standard Compliant SD Host Controller 07:08.0 Ethernet controller: Intel Corporation PRO/100 VE Network Connection (rev 02)
Sürücüler Donanımlarla Nasıl Eşleştirilir?
Bilmiyorum daha önce hiç aklınıza geldi mi ama (benim bu konuyu araştırmadan önce hiç aklıma gelmemişti) sürücülerin donanımlarla nasıl eşleştirildiğini hiç merak ettiniz mi? Soruyu daha açık bir şekilde sorarsam: Sistemimizde kullandığımız bütün cihazlar için sürücüler yüklenmiş durumda. Cihazımızı PC’mize bağlayınca hiç sorunsuz şekilde o cihaza ait sürücü çekirdek tarafından otomatik olarak yükleniyor ve cihazın isteklerine cevap vermek üzere çalıştırılıyor. Peki, çekirdek o kadar sürücü arasından hangisinin hangi cihaza (örneğin fare) ait olduğunu nereden anlıyor?
Bunun için bir PCI cihazın içindeki kontrol registerlarına bakmamız gerekiyor.
PCI Cihazların Kontrol Registerları
Yukarıdaki fotoğrafta bir PCI cihazın ayar registerları görülüyor. Bu registerlarda cihazla ilgili çeşitli özellikler (Üretici numarası, cihaz numarası, cihazın sınıfı, adres hatları ile ilgili bilgiler, IRQ numarası…) saklanıyor.
Buradaki registerlardan Vendor ID, PCI cihazlarının standardizasyonunu yapan kuruluş tarafından cihazın üreticisine verilmiş bir üretici numarasıdır. Bu numaranın verilmesindeki mantığın aynısı ağ kartlarına MAC numarası verilmesinde de uygulanır.
Device ID ise, o cihaza üreticisi tarafından verilmiş cihaz numarasıdır. Vendor ID ile birleşince dünyadaki bütün PCI cihazların benzersiz bir ürün numarası olmuş olur. Cihazlara sürücü yazılırken de bu numaralar sürücülerin içine koyulur. Böylece çekirdek sürücüde belirtilen numaralı cihaz sisteme bağlandığında, bağlanan cihazla ilişkilendireceği sürücüyü bu numaralar bakarak belirler.
Cihaz sürücüleri sadece belli bir cihazı çalıştıracak şekilde özel olarak yazıldığı gibi, belli bir tür cihazı çalıştıracak şekilde, genel sürücüler de yazılabilir. Buna USB Fare veya Klavye sürücüleri örnek verilebilir. Bilgisayar dünyasında bu tür genel cihazların yapıları standarttır. Böylece, örneğin bir USB Fare bağlandığında bu genel sürücü yüklenerek, cihaza özel bir sürücüye gerek kalman çalıştırılabilir (Zaten sürücüsünü yazacağımız fare bizim yazacağımız sürücü olmadan da çalışıyor. Ancak USB standardı dışındaki ekstra tuşları çalışmıyor. Biz bu yüzden bu cihaza özel sürücü yazacağız). Peki çekirdek bağlanan cihazın bir fare olduğunu ve ona özel sürücü değil de genel fare sürücüsünü yüklemesi gerektiğini nerden anlıyor? Bunu da yine yukarıdaki registerlara bakarak anlıyor. Şekilde Class Code registerı cihazın hangi tür bir cihaz olduğunu belirtiyor.
USB Portu
USB portunda kullanıcıdan gelen istekleri donanıma ulaştırırken, isteğin geçtiği katmanlar şekilde görülüyor. Buna göre, gelen istek USB cihazı hangi tür bir cihaz ise (char, network, block) o türe göre farklı işlemlerden geçip önce sürücüye geçiyor, sürücüden isteklerin yorumlandığı USB Core’a, oradan da ilgili cihaza donanımsal olarak bağlı olan USB Host Controller (ki isteğin hangi adresteki cihaza gideceğini controller biliyor) üzerinden donanıma erişiliyor.
Buraya kadar anlattığım her şey genel bilgisayar mimarisi için geçerlidir. USB portu söz konusu olunca anlatılan birçok şey farklı isimlendirilme ya da bakış açılarına tabii tutuluyor. Örneğin;
USB portundan gelen kesmelere özel olarak URB (USB Request Block) deniyor. Biz sürücümüzde bu URB’leri alıp içindeki veriye göre sistemimizde farklı tepkiler oluşmasını sağlayacağız.
Tabii IRQ’dan URB ‘ye olan değişim sadece isimde olmuyor. URB’lere cevap verecek IH’ler ve bunların sisteme kaydı işlemi normalden farklı hale geliyor. Sürücüyü yazarken ayrıca inceleyeceğiz ama şimdilik şunu söyleyebilirim ki, bu işlem usb_fill_int_urb() isminde daha özel bir fonksiyonla yapılıyor. Burada fonksiyon adında geçen int bu fonksiyonun interrupt yani kesme ile çalışan bir cihaza ait URB’lere cevap vereceğini belirtiyor.
PCI cihazlarınkine benzer şekilde, ancak USB’lere özel daha ayrıntılı bilgiler listeleyen bir komutumuz var: lsusb.
lsusb komutu bilgisayara bağlı bütün USB cihazlarını tüm detayına kadar listeler.
hamza@hamza-pardus ~ $ lsusb Bus 001 Device 004: ID 058f:6387 Alcor Micro Corp. Transcend JetFlash Flash Drive Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Bus 005 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub Bus 004 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub Bus 002 Device 002: ID 0930:0508 Toshiba Corp. Integrated Bluetooth HCI Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub Bus 003 Device 002: ID 09da:021f A4 Tech Co., Ltd Bus 003 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bu çıktıda : ile ayrılmış hex numaraların birincisi cihazın Vendor ID’si, ikincisi ise Device ID’sidir. İşte biz bu numaraları sürücünün içinde belirteceğiz. Yani çekirdeğe “bu numaralı cihazlar sisteme bağlandığında bu sürücüyü yükle diyeceğiz.” Buna göre sürücüsü yazmaya çalıştığımız cihazın Vendor ID: 09da
Device ID’si: 021f
Lsusb komutu çeşitli parametrelerle çalıştırıldığında cihazla ilgili tüm bilgileri listeler. Cihaz adı, üreticisi, numarası, iletişim şekli, türü, gücü, tipi, tamponu…
Usb Interface, Usb Endpoint
Bilgisayarın USB cihazların takıldığı USB portlarına özel bir adlandırmayla USB Interface denir. Bu USB Interface’lerin mantıksal karşılılıkları USB Endpoint’lerdir. USB Endpoint’ler, karşılık geldikleri USB Interface’ler hakkında bazı bilgileri bize sağlar. Bu bilgilerden bizim işimize yarayacak olan, cihazın Vendor ve Product ID’si, cihazın tipi (char, block, network), tamponunun boyutu, türü bilgileridir.
USB Potu Bit Haritası
USB Portu 64 bitlik bir porttur. USB’den haberleşen cihazlar 64 bitlik bir veri yolu kullanır. Yani her USB isteği, sinyali, URB’si sisteme 64 bitlik veri gönderir. Aşağıdaki şekilde bir USB Fare için bit haritası görülüyor. Bu haritaya göre en anlamlı 8 bit temel tuşlar için, sonraki byte’lar ise sırayla X, Y eksenlerinde ve tekerin hareketi için kullanılır.
Sürücü yazarken bizim yapacağımız URB deki bu 64 biti teker teker alıp hangi tuşa karşılık geliyorsa Linux çekirdeğine kayıt ettirmek olacak. Böylece fareden bir tuşa basıldığında ve bu tuşun bit sinyali USB Controller’a geldiğinde, Linux Çekirdeği hangi tuşa basıldığını bilebilecektir.
Sayısal örnek vermek gerekirse;
10000000-00000000-00000000-0000000-00000000-00000000-0000000-00000000 : Sol Tuş 01000000-00000000-00000000-0000000-00000000-00000000-0000000-00000000 : Sağ Tuş 00100000-00000000-00000000-0000000-00000000-00000000-0000000-00000000 : Orta Tuş
Yazarken kendimi kaptırmışım. Çok uzun bir yazı olmuş. Bu aylık veda vakti geldi. Sürücüyü yazmak için öğrenmemiz gereken bilgiler bu kadardı. Bir sonraki yazıda artık sürücümüzü yazmaya başlayacağız.
Sağlıcakla Kalın.
Kaynaklar
Linux Device Drivers, Robert Corbert, O’Reilly, 2005
Linux Kernel Development, Robert Love, Novell Press, 2005
Güzel bir yazı olmuş teşekkürler
YanıtlaSil