> IO Portlarının Kullanımı: CPU ile RAM arasında veri transferi aslında tamamen elektriksel düzeyde 1'lerle 0'larla gerçekleşmektedir. CPU'nun adres uçları (address bus) RAM'in adres uçlarına bağlanır. Bu adres uçları RAM'den ransfer edilecek bilginin fiziksel adresini belirtmek için kullanılmaktadır CPU'nun veri uçları (data bus) uçları ise bilginin alınıp gönderilmesinde kullanılmaktadır. İşlemin okuma mı yazma mı olduğu genellikle R/W biçiminde isimlendirilen ayrı bir control ucuyla yapılmaktadır. Örneğin, 32 bit Intel işlemcilerinde MOV EAX, [XXXXXXXX] komutu RAM'deki XXXXXXXX adresinden başlayan 4 byte bilginin CPU içerisindeki EAX yazmacına çekileceği anlamına gelmektedir. Bu makine komutu işletilirken CPU önce erişilecek adres olan XXXXXXXX adresini adres uçlarına elektriksel işaret olarak kodlar. RAM bu adresi alır, bu adresten başlayan 4 byte'lık bilgiyi veri uçlarına elektirksel olarak kodlar. CPU'da bu uçlardan bilgiyi yine elektirksel olarak alır ve EAX yazmacına yerleştirir. CPU'nun adres uçları RAM'in adres uçlarına, CPU'nun veri uçları ise RAM'in veri uçlarına bağlıdır. Tranfer yönü R/W ucuyla belirlenmektedir. Tabii CPU'lar bugün DRAM belleklerden daha hızlıdır. Dolayısıyla CPU RAM'den yanıt gelene kadar beklemektedir (wait state). Bir bilisayar sisteminde yalnızca Merkezi İşlemci (CPU) değil aynı zamanda yerel birtakım olaylardan sorumlu yardımcı işlemciler de vardır. Bu yardımcı işlemcilere genellikle "controller (denetleyici)" denilmektedir. Örneğin klasik PC mimarisinde, "Kesme Denetleyicisi (Intel 8250-PIC)", "Klavye Denetleyicisi (Intel 8042-KC)", "UART denetleyicisi (Intel 8250/NS 16550-UART)" gibi pek çok işlemci vardır. Bu işlemcilere komutlar tıpkı CPU/RAM haberleşmesinde olduğu gibi elektriksel düzeyde CPU'nun adres ve veri uçları yoluyla gönderilmekte ve bu işlemcilerden bilgiler yine tıpkı RAM'de olduğu gibi adres ve veri uçları yoluyla alınmaktadır. Yani CPU'nun adres ve veri uçları yalnızca RAM'e değil yardımcı işlemcilere de bağlıdır. Pekiyi bu durumda CPU RAM'e erişirken aynı zamanda yardımcı işlemcilere de erişmez mi? İşte CPU'ların genellikle IO/Mem biçiminde isimlendirilen bir uçları daha vardır. Bu ucun 5V ya da 0V olması erişimin RAM'e mi yoksa yardımcı işlemciye mi yapılacağını belirtir. Yardımcı işlemcileri tasarlayanlar bu uca bakarak bilginin RAM'e değil kendilerine geldiğini anlayabilirler. Nomral RAM erişimlerine ilişkin MOV ya da LOAD/STORE makine komutlarında bu IO/Mem ucu "mem" biçiminde aktive edilir. Ancak bazı IN, OUT gibi komutlarda bu uç "IO" biçiminde aktive edilmektedir. Bu durumda yardımcı işlemcilere erişmek için MOV, LOAD/STORE komutları değil genellikle IN, OUT biçiminde isimlendirilen komutları kullanılmaktadır. Ancak bazı yardımcı işlemciler bu "IO/Mem" ucu tam tersine "Mem" olarak aktive edildiğinde de işlevini yapacak biçimde konfigüre edilmiş olabilir. Bu durumda bu işlemcilere biz IN, OUT komutlarıyla değil RAM'e erişiyormuş gibi MOV, LOAD/STORE komutlarıyla erişiriz. İşte bu tekniğe "Memory Mapped IO" denilmektedir. Memory Mapped IO yardımcı işlemcilere sanki RAM'miş gibi erişme anlamına gelir. Bunun da sistem programcısı için önemli avantajları vardır. Sistem programcısı bu sayede göstericileri kullanarak bu işlemcilere erişebilmektedir. Normal "IO/Mem" ucu "IO" biçiminde aktive edilerek erişimlere "Port-Mapped IO" da denilmektedir. Bazı mimarilerde her iki teknik de yoğun kullanılmaktadır. Ancak bazı mimarilerde "memory mapped io" tekniği daha yoğun kullanılabilmektedir. Memory mapped IO tekniği kullanılırken artık RAM'in ilgili adresteki kısmına erişilemez ya da bu erişimin bir anlamı kalmaz. Yani adeta bu teknikte sanki RAM'in bir bölümü çıkartılmış onun yerine ilgili işlemci oraya takılmış gibi bir etki oluşmaktadır. Örneğin memory mapped IO bilgisayar sistemlerinde grafik kartları tarafından yoğun olarak kullanılmaktadır. Grafik kartlarını tasarlayanlar kartın üzerindeki RAM'in (bu ana RAM değil) içeriğini belli periyotlarla ekrana göndermektedir. Programcı da C'de göstericileri kullanarak belli adrese yazma yapma yoluyla ekrana belirli şeylerin çıkmasını sağlayabilmektedir. Pekiyi yardımcı işlemcileri biribirnden ayıran şey nedir? İşte CPU'nun adres uçları bu yardımcı işlemciler tarafından özel bazı değerlerde ise dikkate alınmaktadır. Yani nasıl RAM'deki byte'ların adresleri varsa yardımcı işlemcilerin de birer donanımsal adresleri vardır. Bu adreslere genellikle "port numaraları" da denilmektedir. Yardımcı işlemcilerin port numaraları donanım mimarisini tasarlayanlar tarafından donanımsal olarak önceden belirlenmiştir. Ancak modern sistemlerde programlama yoluyla değiştirilebilen port adresleri de söz konusu olmaktadır. PC mimarisinde programlanabilen port numarasına sahip olan işlemcilere "plug and play (PnP)" işlemciler de denilmektedir. O halde bizim bir yardımcı işlemciyi programlayabilmemiz için şu bilgileri edinmiş olmamız gerekmektedir: -> Yardımcı işlemci "port mapped IO" mu yoksa "memory mapped IO" mu kullanmaktadır? -> Yardımcı işlemcinin port numaraları (ya da memory mapped io söz konusu ise belek adresleri) nedir? -> Bu yardımcı işlemcinin hangi portuna (memory mapped io söz konusu ise hangi adrese) hangi değerler gönderildiğinde bu işlemci ne yapacaktır? -> İşlemci bize bilgi verecekse hangi bunu portu okuyarak (memory mapped IO söz konusu ise hangi adresi okuyarak) vermektedir. Verilen bilginin biçimi nedir? Yardımcı işlemciler yalnızca bilgisayar donanımın içerisinde donanımsal olarak çivilenmiş bir biçimde bulunmayabilirler. Bazı bilgisayar sistemlerinde (örneğin masasüstü PC'lerde) genişleme yuvaları vardır. Bu genişleme yuvaları CPU'nun adres ve veri yoluna erişebilmektedir. Bu genişleme yuvaları için kart tasarlayan tasarımcılar kartlarının üzerinde yardımcı işlemcileri bulundurabilirler. Böylece ilgili kart takıldığında sanki sisteme yeni bir yardımcı işlemci takılmış gibi etki oluşmaktadır. CPU'ya neden "merkezi (central)" işlemci denildiğini artık anlayabilirsiniz. Bilgisayar sistemlerinde kendi yerel işlemlerinden sorumlu pek çok yardımcı işlemci olabilir. Ancak bunların hepsini elektriksel olarak CPU programlamaktadır. Bu nedenle CPU'ya merkezi işlemci denilmiştir. Tabii CPU aslında bizim yazdığımız programları çalıştırır. Yani yardımcı işlemcileri de sonuç olarak biz programlamış oluruz. Pekiyi yardımcı işlemcileri programlarken onlara tek hamlede kaç byte bilgi gönderip onlardan kaç byte bilgi okuyabiliriz? İşte bazı işlemciler (özellikle eskiden tasarlanmış olanlar) byte düzeyinde programlanmaktadır. Bazıları ise WORD düzeyinde bazıları ise DWORD düzeyinde programlanabilmektedir. O halde bizim bir haberleşme portuna 1 byte, 2 byte, 4 byte gönderip alabilmemiz gerekir. Pekiyi bir yardımcı işlemci kernel modda programlanabiliyorsa user mode programlar bu yardımcı işlemciyi nasıl kullanmaktadır? İşte tipik olarak user mode programlar ioctl işlemleriyle aygıt sürücünün kodlarını çalıştırırlar. Aygıt sürücüler de bu kodlarda ilgili yarmcı işlemciye komutlar yollayabilir. Bazen read/write işlemleri de bu amaçla kullanılabilmektedir. Tabii karmaşık yardımcı işlemciler için aygıt sürücüleri yazanlar faydalaı işlemlerin daha kolay yapılabilmesi için daha yüksek seviyeli fonksiyonları bir API kütüphanesi yoluyla sağlayabilmektedir. İşlemcilerin IN, OUT gibi makine komutları "özel (privileged)" komutlardır. Bunlar user mode'dan kullanılırsa işlemci koruma mekanizması gereği bir içsel kesme oluşturur, işletim sistemi de bu kesme kodunda prosesi sonlandırır. Dolayısıyla bu komutları kullanarak donanım aygıtlarıyla konuşabilmek için kernel mod aygıt sürücü yazmak gerekir. Aygıtlara erişmekte kullanılan komutlar CPU mimarisine göre değişebildiğnden Linux çekirdeğinde bunlar için ortak arayüze sahip inline fonksiyonlar bulundurulmuştur. Bu fonksiyonlar şunlardır: #include unsigned char inb(int addr); unsigned short inw(int addr); unsigned int inl(int addr); void outb(unsigned char b, int addr); void outw(unsigned short b, int addr); void outl(unsigned int b, int addr); inb portlarından 1 byte, inw 2 byte, inl 4 byte okumak için kullanılmaktadır. Benzer biçimde haaberleşme portlarına outb 1 byte, outw 2 byte, outl 4 byte, göndermek için kullanılmaktadır. Bazı mimarilerde bir bellek adresinden başlayarak belli bir sayıda byte'ı belli bir porta gönderen ve belli bir prottan yapılan okumaları belli bir adresten itibaren belleğe yerleştiren özel makine makumtları vardır. Bu komutlara string komutları denilmektedir. (Intel'de string komutları yalnızca IO işlemleri ile ilgili değildir.) İşte bu komutlara sahip mimarilerde bu string komutlarıyla IN, OUT yapan çekirdek fonksiyonları da bulundurulmıştur: #include void insb(unsigned long addr, void *buffer, unsigned int count); void insw(unsigned long addr, void *buffer, unsigned int count); void insl(unsigned long addr, void *buffer, unsigned int count); void outsb(unsigned long addr, const void *buffer, unsigned int count); void outsw(unsigned long addr, const void *buffer, unsigned int count); void outsl(unsigned long addr, const void *buffer, unsigned int count); insb, insw ve insl sırasıyla 1 byte 2 byte ve 4 byte'lık sitring fonksiyonlarıdır. Bu fonksiyonlar birinci parametresiyle belirtilen port numarasından 1, 2 ya da 4 byte'lık bilgileri ikinci parametresinde belirtilen adresten itibaren belleğe yerleştirirler. Bu işlemi de count kere tekrar ederler. Yani bu fonksiyonlar porttan count defa okuma yapıp okunanları buffer ile belirtilen adresten itibaren belleğe yerleştirmektedir. outsb, outsw ve outsl fonksiyonları ise bu işlemin tam tersini yapmaktadır. Yani bellekte bir adresten başlayarak count tane byte'ı birinci parametresiyle belirtilen port'a yerleştirmektedir. Bazı sistemlerde aygıtlar yavaş kalabilmektedir. Yani bus çok hızlı aygıt yavaş ise o aygıt port'larına peşi sıra bilgiler gönderilip alınırken sorunlar oluşabilmektedir. Bunun için bilgiyi porta gönderdikten ya da bilgiyi port'tan aldıktan sonra kısa bir süre bekleme yapmak gerekebilir. İşte bu nedenle yukarıdaki fonksiyonların bekleme yapan p'li (pause) versiyonları da bulundurulmuştur. #include unsigned char inb_p(int addr); unsigned short inw_p(int addr); unsigned int inl_p(int addr); void outb_p(unsigned char b, int addr); void outw_p(unsigned short b, int addr); void outl_p(unsigned int b, int addr); Linux'ta user moddan haberleşme portlarına IN ve OUT yapmak için basit bir aygıt sürücüsü de bulundurulmuştur. Bu aygıt sürücüsüne "/dev/port" aygıt dosyasıyla erişilebilir. Tabii user mod programların bu biçimde her defasında kernel moda geçerek In/OUT yapması verimsiz bir yöntemdir. Ancak yine de basit uygulamalar için faydalı kullanımlar söz konusu olabilmektedir. Bu aygıt sürücü dosya gibi açıkdıktan sonra sanki her dosya offset'i bir haberleşme portuymuş gibi işlem görmektedir. Yine okuma yazma sırasında dosya göstericisi ilerletilmekte dolayısıyla başka porta konumlandırılmaktadır. Aşağıdaki örnekte 0x60 numaralı porttan "/dev/port" aygıt sürücüsü yoluyla 1 byte okunmaktadır. * Örnek 1, /* app.c*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; int result; unsigned char ch; if ((fd = open("/dev/port", O_RDWR)) == -1) exit_sys("open"); if (lseek(fd, 0x60, SEEK_SET) == -1) exit_sys("lseek"); if ((result = read(fd, &ch, 1)) == -1) exit_sys("read"); printf("%02X\n", ch); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Bir haberleşme portu ile çalışmadan önce o portun boşta olup olmadığını belirlemek gerekebilir. Çünkü başka aygıtların kullandığı port'lara erişmek sorunlara yol açabilmektedir. Tabii eğer biz ilgili port'un kullanılmasının bir soruna yol açmayacağından emin isek başkalarının kullandığı kullandığı port'ları doğrudan kullanabiliriz. Çekirdek bu bakımdan bir kontrol yapmamaktadır. Kullanmadan önce bir portun başkaları tarafından kullanılıp kullanılmadığının sorgulanması için request_region isimli çekirdek fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include struct resource *request_region(unsigned long first, unsigned long n, const char *name); Fonksiyonun birinci parametresi kullanılmak istenen port numarasının başlangıç numarasını, ikinci parametresi ilgili port numarasından itibaren ardışıl kaç port numarasının kullanılacağını, üçüncü parametresi ise "/proc/ioports" dosyasında görüntülenecek ismi belirtmektedir. Fonksiyon başarı durumunda portları betimleyen resource isimli yapının başlangıç adresine başarısızlık durumunda NULL adrese geri dönmektedir. request_region fonksiyonu ile tahsis edilen port umaraları release_region foksiyonu ile serbest bırakılmalıdır: #include void release_region(unsigned long start, unsigned long n); Yukarıda da belirttiğimiz gibi portların kullanılması için bu biçimde tahsisat yapma zorunluluğu yoktur. Ancak programcı programlanabilir IO portları söz konusu olduğunda ilgili port numaralarını başkalarının kullanmadığından emin olmak için bu yöntemi izlemelidir. PC'lerdeki klavye denetleyicisinin (klavye içerisindeki değil PC tarafındaki denetleyicinin (orijinali Intel 8042)) 60H ve 64H numaralı iki port'u vardır. 60H portu hem okunabilir hem de yazılabilir durumdadır. 60H portu 1 byte olarak okunduğunda son basılan ya da çekilen tuşun klavye scan kodu elde edilmektedir. Yukarıda dabelirttiğimiz gibi klavye terminolojisinde uşa basılırken oluşturulan scan koduna "make code", parmak tuştan çekildiğinde oluşturulan scan koduna ise "break code" denilmektedir. Klavye içerisindeki işlemcinin (keyboard encoder) break code olarak önce bir F0 byte sonra da make code byte'ını gönderdiğini belirtmiştik. İşte PC içerisindeki klavye denetleyicisi bu break kodu aldığında bunu iki byte olarak değil yüksek anlamlı biti 1 olan byte olarak saklamaktadır. Böylece biz 60H port'unu okuduğumuzda onun yüksek anlamlı bitine bakarak okuduğumuz scan kodunun make code mu yoksa break code mu olduğunu anlayabiliriz. Klavye denetleyicisinin 60H portuna gönderilen 1 byte değere "keyboard encoder command" denilmektedir. Bu 1 byte'lık komut klavye denetleyicisi tarafından klavye içerisindeki işlemciye gönderilir. Ancak bu 1 byte'tan sonra bazı komutlar parametre almaktadır. Parametreler de komutton sonra 1 byte olarak aynı port yoluyla iletilmektedir. Aşağıdaki örnekte klavyeden tuşlara basıldığında basılan ve çekilen tuşların make ve break code'ları yazdırılmaktadır. * Örnek 1, /* irq-driver.c */ #include #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 unsigned char g_keymap[128] = { [30] = 'A', [31] = 'S', [32] = 'D', [33] = 'F', }; 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) { unsigned char code; char *code_type; code = inb(0x60); code_type = code & 0x80 ? "Break code: " : "Make code: "; if (g_keymap[code & 0x7F]) printk(KERN_INFO "%s %c (%02X)\n", code_type, g_keymap[code & 0x7F], code); else printk(KERN_INFO "%s %02X\n", code_type, code); 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); Kesme kodları bazen bilgiyi bir kaynaktan alıp (örneğin network kartından, seri porttan, klavye denetleyicisinden) onu bir yere (genellikle bir kuyruk sistemi) yerleştirip, uyuyan thread'leri uyandırmaktır. Örneğin bir thread'in klavyeden bir tuşa basılana kadar bekleyeceğini düşünelim. Bu durumda thread işletim sistemi tarafından bir bekleme kuyruğuna alınır. Klavyeden bir tuşa basıldığında oluşan IRQ içerisinde bu bekleme kuyruğunda bekleyen thread'ler uyandırılır. Aşağıda örnekte aygıt sürücüye aygıt sürücünün bir IOCTL komutunda thread bloke edilmiştir. Sonra klavyeden bir tuşa basıldığında thread uykudan uyandırılıp basılmış olan tuşuna scan kodu IOCTL kodu tarafından thread'e verilmiştir. * Örnek 1, /* irq-driver.h */ #ifndef IRQDRIVER_H_ #define IRQDRIVER_H_ #include #define KEYBOARD_MAGIC 'k' #define IOC_GETKEY _IOR(KEYBOARD_MAGIC, 0, int) #endif /* irq-driver.c */ #include #include #include #include #include #include #include #include "irq-driver.h" MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Character Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static irqreturn_t keyboard_irq_handler(int irq, void *dev_id); static long keyboard_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_file_ops = { .owner = THIS_MODULE, .unlocked_ioctl = keyboard_ioctl, }; static DECLARE_WAIT_QUEUE_HEAD(g_wq); static int g_key; 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) { int key; if (g_key != 0) return IRQ_NONE; key = inb(0x60); if (key & 0x80) return IRQ_NONE; g_key = key; wake_up_all(&g_wq); return IRQ_HANDLED; } static long keyboard_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case IOC_GETKEY: g_key = 0; if (wait_event_interruptible(g_wq, g_key != 0)) return -ERESTARTSYS; if (copy_to_user((void *)arg, &g_key, sizeof(int)) != 0) return -EFAULT; return g_key; default: return -ENOTTY; } } 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); # 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 -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* app.c */ #include #include #include #include #include #include "irq-driver.h" void exit_sys(const char *msg); int main(void) { int fd; int key; if ((fd = open("irq-driver", O_RDONLY)) == -1) exit_sys("open"); if (ioctl(fd, IOC_GETKEY, &key) == -1) exit_sys("ioctl"); printf("Scan code: %d (%02x)\n", key, key); close(fd); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aşağıdaki örnekte IOC_SETLIGHTS ioctl komutu ile 8042 klavye denetleyicine komut gönderme yoluyla klavye ışıkları yakılıp söndürülmektedir. Klavyede üç ışıklı tuş vardır: Caps-Lock, Num-Lock ve Scroll_Lock. Bu ışıkları yakıp söndürebilmek için önce 60h portuna 0xED komutu gönderilir. Sonra yine 60h portuna ışıkların durumunu belirten 1 byte gönderilir. Bu byte'ın düşük anlamlı 3 biti sırasıyla Scroll-Lock, Num-Lock ve Caps-Lock tuşlarının ışıklarını belirtmektedir: 7 6 5 4 3 CL NL SL x x x x x x x x 60h portuna komut göndermeden önce 64h portundan elde edilen değerin 2 numaralı bitinin 0 olması gerekmektedir. Ayrıntılı bilgi için http://www.brokenthorn.com/Resources/OSDev19.html sayfasını inceleyebilirsiniz. Aşağıdaki aygıt sürücüde IOC_SETLIGHTS IOCTK komutunda klavye ışıklarının yakılıp söndürülmesi sağlanmıştır. Burada "app.c" isimli user mode program bir komut satırı argümanı almış ve o komut satırı argümanınındaki sayıyı yukarıda alattığımız gibi klavye denetleyicisine göndermiştir. Artık pek çok klavyede Scroll Lock ve Num Lock tuşlarının ışıkları bulunmamaktadır. Programın Caps Lock ışığını yakmasını istiyorsanız 4 argümanıyla (CL bitinin 2 numaralı bit olduğuna dikkat ediniz) söndürmek istiyorsanız 0 argümanıyla çalıştırabilirsiniz. Örneğin: $ ./app 4 $ ./app 0 * Örnek 1, /* irq-driver.h */ #ifndef IRQDRIVER_H_ #define IRQDRIVER_H_ #include #define KEYBOARD_MAGIC 'k' #define IOC_GETKEY _IOR(KEYBOARD_MAGIC, 0, int) #endif /* irq-driver.c */ #include #include #include #include #include #include #include #include "irq-driver.h" MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Character Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static irqreturn_t keyboard_irq_handler(int irq, void *dev_id); static long keyboard_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_file_ops = { .owner = THIS_MODULE, .unlocked_ioctl = keyboard_ioctl, }; #ifndef IRQDRIVER_H_ #define IRQDRIVER_H_ #include #define KEYBOARD_MAGIC 'k' #define IOC_GETKEY _IOR(KEYBOARD_MAGIC, 0, int) #endif static DECLARE_WAIT_QUEUE_HEAD(g_wq); static int g_key; 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) { int key; if (g_key != 0) return IRQ_NONE; key = inb(0x60); if (key & 0x80) return IRQ_NONE; g_key = key; wake_up_all(&g_wq); return IRQ_HANDLED; } static long keyboard_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case IOC_GETKEY: g_key = 0; if (wait_event_interruptible(g_wq, g_key != 0)) return -ERESTARTSYS; if (copy_to_user((void *)arg, &g_key, sizeof(int)) != 0) return -EFAULT; return g_key; case IOC_SETLIGHTS: while ((inb(0x64) & 2) != 0) ; outb(0xED, 0x60); while ((inb(0x64) & 2) != 0) ; outb(arg, 0x60); break; default: return -ENOTTY; } return 0; } 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); # 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 -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* app.c */ #include #include #include #include #include #include "irq-driver.h" #define CAPS_LOCK 0x04 #define NUM_LOCK 0x02 #define SCROLL_LOCK 0x04 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; int key; int keycode; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } keycode = atoi(argv[1]); if ((fd = open("irq-driver", O_RDONLY)) == -1) exit_sys("open"); if (ioctl(fd, IOC_SETLIGHTS, keycode) == -1) exit_sys("ioctl"); close(fd); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Derleyiciler ve işlemciler tarafından yapılan önemli bir optimizasyon temasına "komutların yer değiştirilmesi (instruction reordering)" denilmektedir. Bu optimizasyon derleyici tarafından da bizzat işlemcinin kendisi tarafından da yapılabilmektedir. Burada biribirlerini normal bir durumda etkilemeyecek iki ya da daha fazla ayrı makine komutunun yerleri daha hızlı çalışma sağlamak için değiştirilmektedir. Bu tür yer değiştirmeler normal user mod programlarda hiçbir davranış değişiklğine yol açmazlar. Ancak işletim sistemi ve aygıt sürücü kodlarında ve özellikle IO portlarına erişim söz konusu olduğunda bu optimizasyon olumsuz yan etkilere yol açabilmektedir. Örneğin birbirleriyle alakasız iki adrese yazma yapılması durumunda yazma komutlarının yer değiştirmesi işlemcinin bu işleri daha hızlı yapabilmesine yol açabilmektedir. Fakat IO portları ve memory mapped IO söz konusu olduğunda bu sıralama değişikliği istenmeyen olumsuz sonuçlar doğurabilmektedir. İşte bu yer değiştirmeyi ortadan kaldırmak için "bariyer (barrier)" koyma yöntemi uygulanmaktadır. Derleyici ve işlemci bariyerin yukarısıyla aşağısını yer değiştirmemektedir. Bariyer fonksiyonları şunlardır: #include void rmb(void); void wmb(void); void mb(oid); rmb fonksiyonun aşağısındaki kodlar yukarısındaki okuma işlemleri yapıldıktan sonra yapılırlar. wmb fonksiyonun ise yukarısındaki yazma işlemleri yapıldıktan sonra aşağıdaki işlemler yapılırlar. mb fonksiyonu ise hem okuma hem yazma için yukarıdaki ve aşağıaki kodları birbirlerinden ayırmaktadır. Örneğin PORT1 portuna yazma yapıldıktan sonra PORT2 portuna yazma yapılacak olsun. Şöyle bir bariyer kullanmalıyız: outb(cmd1, port1); wmb(); outb(cmd2, port2); Memory Mapped IO işlemi pek çok mimaride normal göstericilerle yapılabilmektedir. Yani aslında bu mimarilerde "memory mapped IO" için özel kernel fonksiyonlarının kullanılmasına gerek yoktur. Ancak bazı mimarilerde memory mapped io işlemi için özel bazı işlemlerin de yapılması gerekebilmektedir. Bu nedenle bu işlemlerin taşınabilir yapılabilmesi için özel kernel fonksiyonlarının kullanılması tavsiye edilir. Tıpkı normal IO işlemlerinde olduğu gibi memory mapped IO için de iki farklı aygıt aynı adres bölgesini kullanmasın diye bir registration işlemi söz konusudur. Bu işlemler request_mem_region ve release_mem_region fonksiyonlarıyla yapılmaktadır: #include struct resource *request_mem_region(unsigned long start, unsigned long len, const char *name); Fonksiyonun birinci parametresi başlangıç bellek adresini, ikinci parametresi alanın uzunluğunu belitmektedir. Üçüncü parametre ise "/proc/iomem" dosyasında görüntülenecek isimdir. Fonksiyon başarı durumunda resource isimli bir yapı nesnesinin adresine, başarısızlık durumunda NULL adrese geri dönmektedir. Daha önce rgister ettirilmiş olan bellek bölgesini serbest bırakmak için (yani register ettirilmemiş hale getirmek için) release_mem_region fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include void release_mem_region(unsigned long start, unsigned long len); Fonksiyonun birinci parametresi başlangıç bellek adresini, ikinci parametresi ise uzunluğu belirtmektedir. Aşağıdaki fonksiyonlar addr ile belirtilen bellek adresinden 1 byte, 2 byte ve 4 byte okurlar. #include unsigned int ioread8(void *addr); unsigned int ioread16(void *addr); unsigned int ioread32(void *addr); Aşağıdaki fonksiyonlar ise addr ile belirtilen bellek adresine 1 byte, 2 byte ve 4 byte bilgi yazmaktadır: #include void iowrite8(u8 value, void *addr); void iowrite16(u16 value, void *addr); void iowrite32(u32 value, void *addr); Yukarıdaki fonksiyonların rep'li versiyonları da vardır: #include void ioread8_rep(void *addr, void *buf, unsigned long count); void ioread16_rep(void *addr, void *buf, unsigned long count); void ioread32_rep(void *addr, void *buf, unsigned long count); void iowrite8_rep(void *addr, const void *buf, unsigned long count); void iowrite16_rep(void *addr, const void *buf, unsigned long count); void iowrite32_rep(void *addr, const void *buf, unsigned long count); Bu fonksiyonlar memmory mapped IO adresinden belli bir adrese belli miktarda (count parametresi) byte, word ya da dword transfer etmektedir. Tıpkı memcpy fonksiyonunda olduğu gibi memory mapped IO adresi ile bellek arasında blok kopyalaması yapan iki fonksiyon bulunmaktadır: #include void memcpy_fromio(void *dest, const void *source, unsigned int count); void memcpy_toio(void *dest, const void *source, unsigned int count); Belli bir memory mapped IP adresine belli bir byte'ı n defa dolduran fonksiyon da şöyledir: #include void memset_io(void *dest, u8 value, unsigned int count); Bu fonksiyonu memset fonksiyonuna benzetebilirsiniz. Aygıtın kullandığı bellek adresi genellikle fiziksel adrestir. Örneğin aygıt 0xFFFF8000 gibi bir adresi kullanıyorsa bu genellikle fiziksel anlamına gelir. Halbuki aygıt sürücünün bu fiziksel adrese erişebilmesi için bu dönüşümü yapacak sayfa tablosu girişlerin olması gerekir. İşte bu girişleri elde edebilmek için şu fonksiyonlar bulundurulmuştur: #include void *ioremap(unsigned long physical_address, unsigned long size); Bu fonksiyon fiziksel adrese erişebilmek için gereken sayfa tablosu adresini bize verir. Gerçi genellikle fiziksel RAM daha önceden de belirtildiği gibi Linux'ta sanal belleğin PAGE_OFFSET ile belirtilen kısmından başlanarak map edilmiştir. Bu işlemi geri almak için iounmap fonksiyonu kullanılmaktadır: #include void iounmap(void *addr);