 |
от PxL(22-02-2007)
рейтинг (51)
[ добре ]
[ зле ]
Вариант за отпечатване
.::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 |
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
jorko tut
jorko tut
simple 2176 0
jorko tut |
Mодула е зареден и работи в kernel space. Можем да го премахнем с rmmod.
.:: Device файлове
В горният пример създадохме модул, който дефакто не прави нищо освен да се зареди в kernel space. При зареждане на модули в повечето случай е необходимо да извършите операция, например инициализация на устройство, данни и т.н. За целта са предоставени две функций: module_init() и module_exit().
Нека разширим нашият пример:
Примерен код |
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
jorko tut
Simple module sayes: Hello!
jorko tut
jorko tut
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.
В примера, ще използвам коментари директно, за да е по-разбираемо:
Примерен код |
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Dimiter Todorov Dimitrov");
MODULE_DESCRIPTION("Simple kernel-space module");
MODULE_SUPPORTED_DEVICE("testdevice");
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);
module_init(simple_init);
module_exit(simple_exit);
struct file_operations fops = {
read: simple_read,
write: simple_write,
open: simple_open,
release: simple_release
};
static int Major;
static int Device_Open = 0;
static char msg[BUF_LEN];
static char *msgPtr;
static int simple_init(void) {
Major = register_chrdev(0, "simple", &fops);
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");
}
static int simple_open(struct inode *inode, struct file *file)
{
static int counter = 0;
if (Device_Open)
return -EBUSY;
Device_Open ++;
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 --;
return 0;
}
static ssize_t simple_read(struct file *filp, char *buffer, size_t length, loff_t *offset)
{
int bytes_read = 0;
if (*msgPtr == 0)
return 0;
while(length && *msgPtr)
{
put_user(*(msgPtr++), buffer++);
length --;
bytes_read ++;
}
return bytes_read;
}
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
jorko tut
Create device file with: mknod /dev/simple c 253 0
jorko tut
jorko tut
You have accessed this device 0 times!
jorko tut
You have accessed this device 1 times!
jorko tut
You have accessed this device 2 times!
jorko tut
You have accessed this device 3 times!
jorko tut
jorko tut
Simple module unloaded successfuly
jorko tut |
Вече знаем как да създадем модул и да го асоцираме с 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 >>
|
 |