Linux内核驱动开发与等待队列机制实例.docx
《Linux内核驱动开发与等待队列机制实例.docx》由会员分享,可在线阅读,更多相关《Linux内核驱动开发与等待队列机制实例.docx(18页珍藏版)》请在冰豆网上搜索。
Linux内核驱动开发与等待队列机制实例
相信很多写程序的人都写过socket的程序。
当我们open一个socket之后,接着去读取这个socket,如果此时没有任何资料可供读取,那read就会block住。
(这是没有加上O_NONBLOCK的情形),直到有资料可读取才会传回来。
在Linuxkernel里有一个数据结构可以帮助我们做到这样的功能。
这个数据结构就是这里要跟各位介绍的waitqueue。
在kernel里,wait_queue的应用很广,举凡devicedriversemaphore等方面都会使用到wait_queue来implement。
所以,它算是kernel里蛮基本的一个数据结构。
接下来,我要跟各位介绍一下wait_queue的用法,以及用一个例子来说明如何使用wait_queue。
最后,我会带各位去trace一下wait_queue的原始程序代码,看看wait_queue是如何做到的。
我想有件事要先提及的是Linux在userspace跟在kernelspace上的差异。
我们知道Linux是multi-tasking的环境,同时可以有很多人执行很多的程序。
这是从user的观点来看的。
如果就kernel的观点来看,是没有所谓的multi-tasking的。
在kernel里,只有single-thread。
也就是说,如果你的kernelcode正在执行,那系统里只有那部分在执行。
不会有另一部分的kernelcode也在运作。
当然,这是指singleprocessor的情况下,如果是SMP的话,那我就不清楚了。
我想很多人都在Windows3.1下写过程序,在那种环境下写程序,每一个程序都必须适当的将CPU让给别的程序使用。
如果有个程序里面有一个
while
(1);
的话,那保证系统就停在那里了。
这种的多任务叫做non-preemptive。
它多任务的特性是由各个程序相互合作而造成的。
在Linux的userspace下,则是所谓的preemptive,各个process喜欢执行什么就执行什么,就算你在你的程序里加上while
(1);这一行也不会影响系统的运作。
反正时间到了,系统自动就会将你的程序停住,让别的程序去执行。
这是在userspace的情况下,在kernel这方面,就跟Windows3.1程序是一样的。
在kernel里,你必须适当的将CPU的执行权释放出来。
如果你在kernel里加入while
(1);这一行。
那系统就会跟Windows3.1一样。
卡在那里。
当然啦,我是没试过这样去改kernel,有兴趣的人可以去试试看,如果有不同的结果,请记得告诉我。
假设我们在kernel里产生一个buffer,user可以经由read,write等systemcall来读取或写资料到这个buffer里。
如果有一个user写资料到buffer时,此时buffer已经满了。
那请问你要如何去处理这种情形呢?
第一种,传给user一个错误讯息,说buffer已经满了,不能再写入。
第二种,将user的要求block住,等有人将buffer内容读走,留出空位时,再让user写入资料。
但问题来了,你要怎么将user的要求block住。
难道你要用
while(is_full);
write_to_buffer;
这样的程序代码吗?
想想看,如果你这样做会发生什么事?
第一,kernel会一直在这个while里执行。
第二个,如果kernel一直在这个while里执行,表示它没有办法去maintain系统的运作。
那此时系统就相当于当掉了。
在这里is_full是一个变量,当然,你可以让is_full是一个function,在这个function里会去做别的事让kernel可以运作,那系统就不会当。
这是一个方式。
但是,如果我们使用wait_queue的话,那程序看起来会比较漂亮,而且也比较让人了解,如下所示:
structwait_queue*wq=NULL;/*globalvariable*/
while(is_full){
interruptible_sleep_on(&wq);
}
write_to_buffer();
interruptible_sleep_on(&wq)是用来将目前的process,也就是要求写资料到buffer的process放到wq这个wait_queue里。
在interruptible_sleep_on里,则是最后会呼叫schedule()来做schedule的动作,也就是去找另一个process来执行以维持系统的运作。
当执行完interruptible_sleep_on之后,要求write的process就会被block住。
那什么时候会恢复执行呢?
这个process之所以会被block住是因为buffer的空间满了,无法写入。
但是如果有人将buffer的资料读取掉,则buffer就有空间可以让人写入。
所以,有关于叫醒process的动作应该是在readbuffer这方面的程序代码做的。
externstructwait_queue*wq;
if(!
is_empty){
read_from_buffer();
wake_up_interruptible(&wq);
}
....
以上的程序代码应该要放在readbuffer这部分的程序代码里,当buffer有多余的空间时,我们就呼叫wake_up_interruptible(&wq)来将挂在wq上的所有process叫醒。
请记得,我是说将wq上的所有process叫醒,所以,如果如果有10个process挂在wq上的话,那这10个都会被叫醒。
之后,至于谁先执行。
则是要看schedule是怎么做的。
就是因为这10个都会被叫醒。
如果A先执行,而且万一很不凑巧的,A又把buffer写满了,那其它9个process要怎么办呢?
所以在writebuffer的部分,需要用一个while来检查buffer目前是否满了.如果是的话,那就继续挂在wq上面.
上面所谈的就是wait_queue的用法。
很简单不是吗?
接下来,我会再介绍一下wait_queue提供那些function让我们使用。
让我再重申一次。
wait_queue应设为globalvariable,比方叫wq,只要任何的process想将自己挂在上面,就可以直接叫呼叫sleep_on等function。
要将wq上的process叫醒。
只要呼叫wake_up等function就可以了.
就我所知,wait_queue提供4个function可以使用,两个是用来将process加到wait_queue的:
sleep_on(structwait_queue**wq);
interruptible_sleep_on(structwait_queue**wq);
另外两个则是将process从wait_queue上叫醒的。
wake_up(structwait_queue**wq);
wake_up_interruptible(structwait_queue**wq);
我现在来解释一下为什么会有两组。
有interruptible的那一组是这样子的。
当我们去read一个没有资料可供读取的socket时,process会block在那里。
如果我们此时按下Ctrl+C,那read()就会传回EINTR。
像这种的blockIO就是使用interruptible_sleep_on()做到的。
也就是说,如果你是用interruptible_sleep_on()来将process放到wait_queue时,如果有人送一个signal给这个process,那它就会自动从wait_queue中醒来。
但是如果你是用sleep_on()把process放到wq中的话,那不管你送任何的signal给它,它还是不会理你的。
除非你是使用wake_up()将它叫醒。
sleep有两组。
wake_up也有两组。
wake_up_interruptible()会将wq中使用interruptible_sleep_on()的process叫醒。
至于wake_up()则是会将wq中所有的process叫醒。
包括使用interruptible_sleep_on()的process。
在使用wait_queue之前有一点需要特别的小心,呼叫interruptible_sleep_on()以及sleep_on()的function必须要是reentrant。
简单的说,reentrant的意思是说此function不会改变任何的globalvariable,或者是不会dependon任何的globalvariable,或者是在呼叫interruptible_sleep_on()或sleep_on()之后不会dependon任何的globalvariable。
因为当此function呼叫sleep_on()时,目前的process会被暂停执行。
可能另一个process又会呼叫此function。
若之前的process将某些information存在globalvariable,等它恢复执行时要使用,结果第二行程进来了,又把这个globalvariable改掉了。
等第一个process恢复执行时,放在globalvariable中的information都变了。
产生的结果恐怕就不是我们所能想象了。
其实,从process执行指令到此function中所呼叫的function都应该是要reentrant的。
不然,很有可能还是会有上述的情形发生.
由于wait_queue是kernel所提供的,所以,这个例子必须要放到kernel里去执行。
我使用的这个例子是一个简单的driver。
它会maintain一个buffer,大小是8192bytes。
提供read跟write的功能。
当buffer中没有资料时,read()会马上传回,也就是不做blockIO。
而当writebuffer时,如果呼叫write()时,空间已满或写入的资料比buffer大时,就会被block住,直到有人将buffer里的资料读出来为止。
在writebuffer的程序代码中,我们使用wait_queue来做到blockIO的功能。
在这里,我会将此driver写成module,方便加载kernel。
第一步,这个driver是一个简单的characterdevicedriver。
所以,我们先在/dev下产生一个characterdevice。
majornumber我们找一个比较没人使用的,像是54,minornumber就用0。
接着下一个命令.
mknod/dev/bufc540
mknod是用来产生specialfile的command。
/dev/buf表示要产生叫buf的档案,位于/dev下。
c表示它是一个characterdevice。
54为其majornumber,0则是它的minornumber。
有关characterdevicedriver的写法。
有机会我再跟各位介绍,由于这次是讲wait_queue,所以,就不再多提driver方面的东西.
第二步,我们要写一个module,底下是这个module的程序代码:
buf.c
#defineMODULE
#include
#include
#include
#include
#include
#defineBUF_LEN8192
intflag;/*whenrp=wp,flag=0forempty,flag=1for
non-empty*/
char*wp,*rp;
charbuffer[BUF_LEN];
EXPORT_NO_SYMBOLS;/*don'texportanything*/
staticssize_tbuf_read(structfile*filp,char*buf,size_tcount,
loff_t*ppos)
{
returncount;
}
staticssize_tbuf_write(structfile*filp,constchar*buf,size_tcount,
loff_t*ppos)
{
returncount;
}
staticintbuf_open(structinode*inode,structfile*filp)
{
MOD_INC_USE_COUNT;
return0;
}
staticintbuf_release(structinode*inode,structfile*filp)
{
MOD_DEC_USE_COUNT;
return0;
}
staticstructfile_operationsbuf_fops={
NULL,/*lseek*/
buf_read,
buf_write,
NULL,/*readdir*/
NULL,/*poll*/
NULL,/*ioctl*/
NULL,/*mmap*/
buf_open,/*open*/
NULL,/*flush*/
buf_release,/*release*/
NULL,/*fsync*/
NULL,/*fasync*/
NULL,/*check_media_change*/
NULL,/*revalidate*/
NULL/*lock*/
};
staticintbuf_init()
{
intresult;
flag=0;
wp=rp=buf;
result=register_chrdev(54,"buf",&buf_fops);
if(result<0){
printk("<5>buf:
cannotgetmajor54\n");
returnresult;
}
return0;
}
staticvoidbuf_clean()
{
if(unregister_chrdev(54,"buf")){
printk("<5>buf:
unregister_chrdeverror\n");
}
}
intinit_module(void)
{
returnbuf_init();
}
voidcleanup_module(void)
{
buf_clean();
}
有关module的写法,请各位自行参考其它的文件,最重要的是要有init_module()和cleanup_module()这两个function。
我在这两个function里分别做initialize和finalize的动作。
现在分别解释一下。
在init_module()里,只有呼叫buf_init()而己。
其实,也可以将buf_init()的code写到init_module()里。
只是我觉得这样比较好而已。
flag=0;
wp=rp=buf;
result=register_chrdev(54,"buf",&buf_fops);
if(result<0){
printk("<5>buf:
cannotgetmajor54\n");
returnresult;
}
return0;
init_buf()做的事就是去注册一个characterdevicedriver。
在注册一个characterdevicedriver之前,必须要先准备一个型别为file_operations结构的变量,file_operations里包含了一些functionpointer。
driver的作者必须自己写这些function。
并将functionaddress放到这个结构里。
如此一来,当user去读取这个device时,kernel才有办法去呼叫对应这个driver的function。
其实,简要来讲。
characterdevicedriver就是这么一个file_operations结构的变量。
file_operations定义在这个档案里。
它的prototype在kernel2.2.1与以前的版本有些微的差异,这点是需要注意的地方。
register_chrdev()看名字就大概知道是要注册characterdevicedriver。
第一个参数是此device的majornumber。
第二个是它的名字。
名字你可以随便取。
第三个的参数就是一个file_operations变量的地址。
init_module()必须要传回0,module才会被加载。
在cleanup_module()的部分,我们也是只呼叫buf_clean()而已。
它做的事是unregister的动作。
if(unregister_chrdev(54,"buf")){
printk("<5>buf:
unregister_chrdeverror\n");
}
也就是将原本记录在devicedrivertable上的资料洗掉。
第一个参数是majornumber。
第二个则是此driver的名称,这个名字必须要跟register_chrdev()中所给的名字一样才行。
现在我们来看看此driver所提供的file_operations是那些。
staticstructfile_operationsbuf_fops={
NULL,/*lseek*/
buf_read,
buf_write,
NULL,/*readdir*/
NULL,/*poll*/
NULL,/*ioctl*/
NULL,/*mmap*/
buf_open,/*open*/
NULL,/*flush*/
buf_release,/*release*/
NULL,/*fsync*/
NULL,/*fasync*/
NULL,/*check_media_change*/
NULL,/*revalidate*/
NULL/*lock*/
};
在此,我们只打算implementbuf_read(),buf_write(),buf_open,和buf_release()等function而已。
当user对这个device呼叫open()的时候,buf_open()会在最后被kernel呼叫。
相同的,当呼叫close(),read(),和write()时,buf_release(),buf_read(),和buf_write()也都会分别被呼叫。
首先,我们先来看看buf_open()。
staticintbuf_open(structinode*inode,structfile*filp)
MOD_INC_USE_COUNT;
return0;
}
buf_open()做的事很简单。
就是将此module的usecount加一。
这是为了避免当此module正被使用时不会被从kernel移除掉。
相对应的,在buf_release()中,我们应该要将usecount减一。
就像开启档案一样。
有open(),就应该要有对应的close()才行。
如果module的usecount在不为0的话,那此module就无法从kernel中移除了。
staticintbuf_release(structinode*inode,structfile*filp)
{
MOD_DEC_USE_COUNT;
return0;
}
接下来,我们要看一下buf_read()和buf_write()。
staticssize_tbuf_read(structfile*filp,char*buf,size_tcount,
loff_t*ppos)
{
returncount;
}
staticssize_tbuf_write(structfile*filp,constchar*buf,
size_tcount,loff_t*ppos)
{
returncount;
}
在此,我们都只是回传user要求读取或写入的字符数目而已。
在此,我要说明一下这些参数的意义。
filp是一个file结构的pointer。
也就是指我们在/dev下所产生的buf档案的file结构。
当我们呼叫read()或write()时,必须要给一个buffer以及要读写的长度。
Buf指的就是这个buffer,而count指的就是长度。
至于ppos是表示目前这个档案的offset在那里。
这个值对普通档案是有用的。
也就是跟lseek()有关系。
由于在这里是一个drvice。
所以ppos在此并不会用到。
有一点要小心的是,上面参数buf是一个地址,而且还是一个userspace的地址,当kernel呼叫buf_read()时,程序在位于kernelspace。
所以你不能直接读写资料到buf里。
必须先切换FS这个register才行。
Makefile
P=buf
OBJ=buf.o
INCLUDE=-I/usr/src/linux/include/linux
CFLAGS=-D__KERNEL__-DMODVERSIONS-DEXPORT_SYMTAB-O$(INCLUDE)\
-i