от PxL(22-02-2007)

рейтинг (52)   [ добре ]  [ зле ]

Printer Friendly Вариант за отпечатване

.::Linux драйвъри::.

.:: Кой? Къде? Защо?
Преди известно време бях много запален да напиша драйвър за ядрото на Linux.Известно ми беше горе-долу как се пише драйвър и как работи такъв, съответно имах и известни познания по C.Общо взето това би трябвало да е изискването за писане на драйвъри за устройства. Като цяло подобни материали не липсват в интернет, но не съм срещал на български.

.:: Нива на комуникация
Както предполагам е известно на повечето Linux потребители, устройствата там са представени като файлове, намират се в /dev часта...Отсъства портовата комуникация...? По-скоро комуникацията с физическите устройства се осъществява на две нива. Реалната комуникация с хардуерното устройството се съществява от т.нар. kernel space ниво, където ядрото и модулите му се грижат да предават информацията от и за физическите устройства към съответстващите им такива представени от файлове. Другата част се нарича user space, където апликациите комуникират с тези файлове.

.:: Kernel space / User space
Като начало можем да кажем накратко, че модулите са приложения, чиято идея е да се разшири и лесно да се добавя функционалност към ядрото. В зависимост от типа на ядрото (в конкретният случай визирам Линукс ядро) модулите могат да се добавят динамично, т.е. не е нужна прекомпилация и рестарт на самото ядро за да бъде добавен нов модул. По-специално ни интересуват т.нар. monolitic тип ядра, тъй като Linux ядрото е такъв тип. При monolitic типовете ядра (за разлика например от microkernel типовете) модулите използват паметта заделена за ядрото. Това означава, че всички глобални данни са видими за всички модули. Именно поради това писането на модули трябва да е много строго стандартизирано и е за предпочитане да се ползват static променливи..
Както споменах по-горе целта на драйвърите в kernel space е да свържат файловете на устройствата в /dev със самит ехардуерни устройства.Модулите в kernel space (LKM - Loadable Kernel Modul) обменят данни с потребителските апликации в user space чрез callback функции в ядрото.Представено визуално това би изглеждало така:

За да не объркам някого или себе си направо ще дам пример за прост kernel модул.

simple.c
#include <linux/module.h>
 
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("Dimiter Todorov Dimitrov");
 MODULE_DESCRIPTION("Simple kernel-space module");
 MODULE_SUPPORTED_DEVICE("testdevice");

Както забелязвате използвам няколко макроса, чрез които определяме лиценз и информация за модула. Дефинирани са в module.h необходимост за всеки един kernel модул (vim /usr/include/linux/module.h).

За да компилираме ще ни трябва елементарен Makefile
Makefile
KDIR:=/lib/modules/$(shell uname -r)/build
 
 obj-m:=simple.o
 
 default:
 $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
 clean:
 $(RM) .*.cmd *.mod.c *.o *.ko -r .tmp* *.symvers
Необходимо е за компилация да се ползва ядрото, в което ще заредите модула. Добавил съм и опция за clean след компилация.

Запазваме и компилираме:
Примерен код
jorko tut $ make; ls
 make -C /lib/modules/2.6.18/build SUBDIRS=/pxl/Devel/tut modules
 make[1]: Entering directory `/usr/src/linux-2.6.18'
 CC [M]  /pxl/Devel/tut/simple.o
 Building modules, stage 2.
 MODPOST
 CC      /pxl/Devel/tut/simple.mod.o
 LD [M]  /pxl/Devel/tut/simple.ko
 make[1]: Leaving directory `/usr/src/linux-2.6.18'
 Makefile        simple.c   simple.mod.c  simple.o
 Module.symvers  simple.ko  simple.mod.o
 jorko tut $

Компилираният модул simple.ko можем да заредим от user space в ядрото, чрез insmod, и да разгледаме с lsmod.
Примерен код
jorko tut # insmod simple.ko
 jorko tut #
 jorko tut # lsmod | grep simple
 simple                  2176  0
 jorko tut #      
Mодула е зареден и работи в kernel space. Можем да го премахнем с rmmod.
Примерен код
jorko tut # rmmod simple
.:: Device файлове
В горният пример създадохме модул, който дефакто не прави нищо освен да се зареди в kernel space. При зареждане на модули в повечето случай е необходимо да извършите операция, например инициализация на устройство, данни и т.н. За целта са предоставени две функций: module_init() и module_exit().

Нека разширим нашият пример:
Примерен код
#include <linux/module.h>
 
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("Dimiter Todorov Dimitrov");
 MODULE_DESCRIPTION("Simple kernel-space module");
 MODULE_SUPPORTED_DEVICE("testdevice");
 
 static int simple_init(void) {
         printk("Simple module sayes: Hello!\n");
         return 0;
 }
 
 static void simple_exit(void) {
         printk("Simple module unloaded\n");
 }
 
 module_init(simple_init);
 module_exit(simple_exit);

Използваме callback функциите предоставени от ядрото, за да изпълним операция при зареждане и премахване на нашият модул. В случая ползвам kernel функцията printk, която запазва системни съобщения в системният лог. За да ги видим можем да ползваме dmesg. По аналогичен начин както първият път зареждам модула и търся за съобщенията в лог-а:
Примерен код
jorko tut # insmod simple.ko
 jorko tut # dmesg | grep Simple
 Simple module sayes: Hello!
 jorko tut # rmmod simple
 jorko tut # dmesg | grep Simple
 Simple module sayes: Hello!
 Simple module unloaded
 jorko tut #

.::Device файлове
Както споменах вече апликациите от user space комуникират с модулите в ядрото чрез файл-ове намиращи се в /dev частта. За всяко устройство предоставено в /dev отговаря съответен модул в ядрото. За да се /назначи/ даден модул към даден файл се използват уникални числа (желателно е те да са уникални, за да не се объркват модулите, но е възможно да се дублират, което би довело до най-различни /неприятни/ последствия). Тези числа са наречени major numbers, чрез тях ядрото знае кой модул за кой файл отговаря.Всяко устройство има и minor number, което не играе роля при ядрото, то е необходимо на самият модул да различава свойте устроства. Важно е да знаем, че тези устройства могат да са два вида: character и block.Главната разликата както можеби се досещате е, че block устройствата предават данните на отделни фиксирани в зависимост от типа на устройството блокове, докато character устройствата нямат фиксиран размер на предаваните данни. Ето един пример:
Примерен код
jorko tut $ ls -la /dev/hda*
 brw-rw---- 1 root disk 3, 0 Feb 18 13:18 /dev/hda
 brw-rw---- 1 root disk 3, 1 Feb 18 13:18 /dev/hda1
 brw-rw---- 1 root disk 3, 2 Feb 18 13:18 /dev/hda2
 brw-rw---- 1 root disk 3, 5 Feb 18 13:18 /dev/hda5
 brw-rw---- 1 root disk 3, 6 Feb 18 13:18 /dev/hda6
 brw-rw---- 1 root disk 3, 7 Feb 18 13:18 /dev/hda7
 brw-rw---- 1 root disk 3, 8 Feb 18 13:18 /dev/hda8

Виждате типа на устройството [color=blue]b[/color]rw-rw----. Както и неговите major и minor номера brw-rw---- 1 root disk [color=red]3[/color], [color=blue]8[/color] Feb 18 13:18 /dev/hda8
Можете да забележите от горният пример, че за тези устройства отговаря един модул, тъй като имат един и същи major номер. Съответно minor номерата са различни, за да може модулът да различава устройствата.
За да знаем кога процес се опитва да чете или пише в дадено устройство е необходимо да ползваме структура наречена file_operations, дефинирана в fs.h.
Примерен код
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 (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
         ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
         ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);
         int (*readdir) (struct file *, void *, filldir_t);
         unsigned int (*poll) (struct file *, struct poll_table_struct *);
         int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
         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 *);
         int (*open) (struct inode *, struct file *);
         int (*flush) (struct file *);
         int (*release) (struct inode *, struct file *);
         int (*fsync) (struct file *, struct dentry *, int datasync);
         int (*aio_fsync) (struct kiocb *, int datasync);
         int (*fasync) (int, struct file *, int);
         int (*lock) (struct file *, int, struct file_lock *);
         ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
         ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
         ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
         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 (*dir_notify)(struct file *filp, unsigned long arg);
         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);
 };
Както сами забелязвате структурата предоставя доста указатели, като повечето от тях в случая няма да ни интересуват, така че не се стряскайте. За целта можем да ползваме GCC разширеният начин за дефиниране на тази структура:
Примерен код
struct file_operations fops = {
         read: device_read,
         write: device_write,
         open: device_open,
         release: device_release
 };

или C99 стандарта:
Примерен код
struct file_operations fops = {
         .read = device_read,
         .write = device_write,
         .open = device_open,
         .release = device_release
 };

Недефинираните членове на структурата ще се дефинират автоматично от gcc като NULL.

В примера, ще използвам коментари директно, за да е по-разбираемо:
Примерен код
#include <linux/module.h> /* definicii na modul fukciite */
 #include <linux/kernel.h> /* Definiciq na printk() i konstanti za neq */
 #include <linux/fs.h> /* Definiciq na file_operations strukturata */
 #include <linux/uaccess.h> /* Za prenos ot user space kym kernel space */
 
 #define BUF_LEN 64 /* maksimalna dyljina na syobshteniqta */
 
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("Dimiter Todorov Dimitrov");
 MODULE_DESCRIPTION("Simple kernel-space module");
 MODULE_SUPPORTED_DEVICE("testdevice");
 
 /* Prototipi na funkciite  */
 static int simple_init(void);
 static void simple_exit(void);
 
 static int simple_open(struct inode *inode, struct file *file);
 static int simple_release(struct inode *inode, struct file *file);
 static ssize_t simple_read(struct file *filp, char *buffer, size_t length, loff_t *offset);
 static ssize_t simple_write(struct file *filp, const char *buff, size_t len, loff_t *off);
 
 
 /* Deklaraciq na init i exit funkciite */
 module_init(simple_init);
 module_exit(simple_exit);
 
 
 /* Deklaraciq na strukturata za rabota s fajlove */
 struct file_operations fops = {
         read: simple_read,
         write: simple_write,
         open: simple_open,
         release: simple_release
 };
 
 /* Shte ni e neobhodim za da razberem kakyv Major nomer shte poluchi ustrojstvoto pri registraciq*/
 static int Major;      // Promenliva za Major nomera daden ni ot qdroto
 static int Device_Open = 0;  // Flag, koito shte polzvame dokato proces izpolzva ustrojstvoto
 static char msg[BUF_LEN]; // Bufer za syobshteniqta
 static char *msgPtr;      // Ukazatel kym bufera za syobshteniq
 
 
 
 /* Init i Exit funkcii */
 static int simple_init(void) {
         Major = register_chrdev(0, "simple", &fops); // Registrirane na ustrojstvoto
         
         if (Major < 0) {
                 printk ("Registering the character device failed with %d\n", Major);
                 return Major;
         }
         
         printk("Simple module loaded into kernel space!\n");
         printk("Simple module has been assigned to Major number %d\n", Major);
         printk("Create device file with: mknod /dev/simple c %d 0\n", Major);
         return 0;
 }
 
 static void simple_exit(void) {
         int ret = unregister_chrdev(Major, "simple");
         if (ret < 0)
         printk("Error in unregister_chrdev: %d\n", ret);
         
         printk("Simple module unloaded successfuly!\n");
 }
 
 
 /* Metodi ot file_operations */
 
 
 /*Izvikva se pri opit za otwarqne na fajlovoto ustrojstvo  */
 static int simple_open(struct inode *inode, struct file *file)
 {
         static int counter = 0;
         if (Device_Open)
         return -EBUSY; // Ustrojstvoto se izpolzva ot drug procesz
         
         Device_Open ++; // Vdigame flag, che ustrojstvoto e zaeto
         sprintf(msg,"You have accessed this device %d times!\n", counter++);
         msgPtr = msg;
         
         return 0;
 }
 
 static int simple_release(struct inode *inode, struct file *file)
 {
         Device_Open --; // Nulirame flaga, za da mojem da priemame i drugi procesi      
         return 0;
 }
 
 
 /* Izvikva se pri opit za chetene ot ustroistvoto */
 static ssize_t simple_read(struct file *filp, char *buffer, size_t length, loff_t *offset)
 {
         int bytes_read = 0; // Broqch
         
         if (*msgPtr == 0)
         return 0;
         
         while(length && *msgPtr)
         {
                 put_user(*(msgPtr++), buffer++); // Funkciqta kopira dannite ot kernel space kym user space
                 
                 length --;
                 bytes_read ++;
         }
         
         return bytes_read; // Vryshtame kato rezultat prochetenite baitove
 }
 
 static ssize_t simple_write(struct file *filp, const char *buff, size_t len, loff_t *off)
 {
         
         printk( "You don`t wanna write to me :)");
         return -EINVAL;
 }

След като заредим модула ще трябва да създадем файл за устройството в /dev с mknod като му зададем Major number според това, какъв Major number ни е определен от ядрото
Примерен код
jorko tut # insmod simple.ko
 jorko tut # dmesg| tail -n 1
 Create device file with: mknod /dev/simple c 253 0
 jorko tut # mknod /dev/simple c 253 0
 jorko tut # cat /dev/simple
 You have accessed this device 0 times!
 jorko tut # cat /dev/simple
 You have accessed this device 1 times!
 jorko tut # cat /dev/simple
 You have accessed this device 2 times!
 jorko tut # cat /dev/simple
 You have accessed this device 3 times!
 jorko tut # rmmod simple
 jorko tut # dmesg| tail -n 1
 Simple module unloaded successfuly
 jorko tut # rm /dev/simple

Вече знаем как да създадем модул и да го асоцираме с user space устройство.
Остана единствено момента, в който реално ще комуникираме с хардуер...

.:: Операции с хардуер
Линукс предоставя няколко функций за операции с хардуера. Преди да се зареди драйвър, подобен на горният той трябва да /резервира/ даден IO порт за ползване.Това се осъществява чрез функцията request_region изискваща номер на порт, обхват ако са повече от 1 входно-изходен порт и име на драйвъра. Преди нея обаче, за да се увери, че някой друг модул не ползва порта драйвъра трябва да извика check_region, изискваща два параметъра: номер на порт и дължина (1 за конкретен порт). След приключване на работа с външното у-во Драйъвъра трябва да освободи порт-а за да е възможно други драйвъри да работят с него, за целта се ползва release_region изискваща отново същите параметри. За комуникация с устройствата се ползват функции от библиотеката io.h (asm/io.h). Двете по-съществени функций за писане и четене от порт са: outb и inb

.:: Хардуерни прекъсвания
За да работи с хардуерни прекъсвания (IRQ - Interrupt Request), драйвърът ползва request_irq и free_irq. Също можем да ползваме две функций за временно прекратяване на прекъсванията и съответно възстановяването им: cli и sti.

.:: DMA
DMA (Direct Memory Access) каналите ползват директно RAM паметта, без да налагат прекъсване на процесорната работа. За работа с DMA се използва библиотеката asm/dma.h. Устройствата ползващи DMA са обикновено прикрепени към дънната платка.

.:: КонклузИонето
Най-добрият начин да разберете нещо е да разгледате готов пример и да тествате.

п.с.: Тази статия е провокирана от основното соло в Sweet child o' mine на Guns 'n' Roses

Референций:
http://www.faqs.org/docs/kernel/
http://kernel.org/


Благодарско на @Angel от http://forums.bgdev.org/ за съветите относно kernel/user space графиката.

Димитър Т. Димитров PxL 2007
http://insecurebg.org/
pxl at insecurebg


--EOF

Съжалявам ако имам неточности,помагайте да ги оправим.


<< | Програмиране графичен интерфейс (GUI) с Lazarus и freepascal >>