Linux内核的等待队列

相信很多写程序的人都写过 socket 的程序。当我们 open 一个 socket 之后,接着去读取这个 socket,如果此时没有任何资料可供读取,那 read 就会 block 住。(这是没有加上 O_NONBLOCK 的情形),直到有资料可读取才会传回来。在 Linux kernel 里有一个数据结构可以帮助我们做到这样的功能。这个数据结构就是这里要跟各位介绍的 wait queue。在 kernel 里,wait_queue 的应用很广,举凡 device driver semaphore 等方面都会使用到 wait_queue 来 implement。所以,它算是 kernel 里蛮基本的一个数据结构。

接下来,我要跟各位介绍一下 wait_queue 的用法,以及用一个例子来说明如何使用 wait_queue。最后,我会带各位去 trace 一下 wait_queue 的原始程序代码,看看 wait_queue 是如何做到的。

首先需要明白Linux在user space和kernel space上的差异。我们知道linux是multi-tasking的环境,同时可以有很多人执行很多的程序。这是从user的观点来看的。如果就kernel的观点来看,是没有所谓的multi-tasking的。在kernel里,只有single-thread。也就是说,如果你的kernel code正在执行,那系统里只有那部分在执行。不会有另一部分的kernel code也在运作。当然,这是指单核的情况下。在windows3.1环境下,每个程序都必须适当的将CPU让给别的程序使用。如果有个程序里面有一个while(1)的话,那保证系统就停在那里了。这种的多任务叫做non-preemptive。它多任务的特性是由各个程序相互合作造成的。在Linux的user space下,则是所谓的Preeemptive,各个preocess喜欢什么就执行什么,就算你在你的程序里加上 while(1); 这一行也不会影响系统的运作。反正时间到了,系统自动就会将你的程序停住,让别的程序去执行。这是在 user space 的情况下,在 kernel 这方面,就跟 Windows 3.1 程序是一样的。在 kernel 里,你必须适当的将 CPU 的执行权释放出来。如果你在 kernel里加入 while(1); 这一行。那系统就会跟 Windows 3.1 一样。卡在那里。当然啦,我是没试过这样去改 kernel,有兴趣的人可以去试试看,如果有不同的结果,请记得告诉我。

假设我们在 kernel 里产生一个 buffer,user 可以经由 read,write 等 system call 来读取或写资料到这个 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 的话,那程序看起来会比较漂亮,而且也比较让人了解,如下所示:

struct wait_queue *wq = NULL; /* global variable */
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的动作应该是在read buffer这方面的程序代码做的。

extern struct wait_queue *wq;
if ( !is_empty ) {
read_from_buffer();
wake_up_interruptible( &wq );
}
....

以 上的程序代码应该要放在 read buffer 这部分的程序代码里,当 buffer 有多余的空间时,我们就呼叫 wake_up_interruptible( &wq ) 来将挂在 wq 上的所有 process 叫醒。请记得,我是说将 wq 上的所有 process 叫醒,所以,如果如果有10个 process 挂在 wq 上的话,那这 10 个都会被叫醒。之后,至于谁先执行。则是要看 schedule 是怎么做的。就是因为这 10 个都会被叫醒。如果 A 先执行,而且万一很不凑巧的,A 又把 buffer 写满了,那其它 9 个 process 要怎么办呢? 所以在 write buffer 的部分,需要用一个 while 来检查 buffer 目前是否满了.如果是的话,那就继续挂在 wq 上面。

上面所谈的就是wait_queue的用法。接下来,再介绍一下wait_queue提供哪些function让我们使用。让我再重申一次。wait_queue应设为global variable,比方叫wq,只要任何的Process想将自己挂在上面,就可以直接呼叫sleep_on等function。要将wq上的process叫醒,只要呼叫wake_up等function就可以了。

据我所知,wait_queue提供4个function可以使用,两个是用来将process加到wait_queue的。

sleep_on( struct wait_queue **wq );
interruptible_sleep_on( struct wait_queue **wq );

另外两个则是将process从wait_queue上叫醒的。

wake_up( struct wait_queue **wq );
wake_up_interruptible( struct wait_queue **wq );

 为什么会有两组呢?有interruptible的那一组是这样子的。当我们去read一个没有资料可供读取的socket时,process会block在那里。如果我们此时按下ctrl+c,那read()就会传回EINTR。像这种的block IO就是使用interruptible_sleep_on()做到的。也就是说,如果你是用interruptible_sleep_on()来讲process放到wait_queue时,如果有人送一个signal给这个process,那它就会自动从wait_quue中醒来。但是如果你使用sleep_on()把process放到wq中的话,那不管你送任何signal给它,它还是不会理你的。除非你是使用wake_up()将它叫醒。

wakeup也有两组,wake_up_interruptible()会将wq中使用interruptible_sleep_on()的process

叫醒。至于wake_ip()则是会将wq中所有的process叫醒,包括使用interruptibal_sleep_on()的process。

在使用wait_queue之前有一点需要特别的小心,呼叫Interruptible_sleep_on()和sleep_on()的function必须要是reentrant。简单的说,reentrant的意思是说此function不会改变任何的global variable,或者是不会depend on任何的global variable,或者是在呼叫interruptible_sleep_on() 或 sleep_on() 之后不会 depend on 任何的 global variable。因为当此function呼叫sleep_on()时,目前的process会被暂停执行。可能另外一个process又会呼叫此function。若之前的process将某些information存在global variable,等它恢复执行时要使用,结果第二个进程来了,又把这个global variable改掉了。等第一个process恢复执行时,放在global variable中的information都变了。产生的结果恐怕就不是我们所能想象了。其实,从process执行执行到此function中所呼叫的function都应该是要reentrant的。不然,很有可能还是会有上述的情形发生。

由于wait_queue是kernel所提供的,所以,这个例子必须要要放到kernel里去执行。我使用的这个例子是一个简单的driver。它会maintain一个buffer,大小是8192bytes。提供read跟write的功能。当buffer中没有资料时,read()会马上传回,也就是说不做block IO。而当write buffer时,如果呼叫write()时,空间已满或写入的资料比buffer大时,就会被block住,直到有人将buffer里的资料读出来为止。在write buffer的程序代码中,我们使用wait_queue来做到block IO的功能。在这里,我会将此driver写成module,方便加载kernel。

第一步:这个driver是一个简单的character device driver。所以,我们先在/dev下产生一个character device。major number我们找一个比较没人使用的,像是54,minor number就用0,接着下一个命令:mknod /dev/buf c 54 0

mknod是用来产生special file的command。/dev/buf表示要产生叫buf的档案,位于/dev下。c 表示它是一个 character device。54 为其 major number,0 则是它的 minor number。

第二步,我们要写一个 module,底下是这个 module 的程序代码:

buf.c
#define MODULE
#include
#include
#include
#include
#include
#define BUF_LEN 8192

int flag; /* when rp = wp,flag = 0 for empty,flag = 1 for
non-empty */
char *wp,*rp;
char buffer[BUF_LEN];
EXPORT_NO_SYMBOLS; /* don't export anything */

static ssize_t buf_read( struct file *filp,char *buf,size_t count,
loff_t *ppos )
{
return count;
}

static ssize_t buf_write( struct file *filp,const char *buf,size_t count,
loff_t *ppos )
{
return count;
}

static int buf_open( struct inode *inode,struct file *filp )
{
MOD_INC_USE_COUNT;
return 0;
}

static int buf_release( struct inode *inode,struct file *filp )
{
MOD_DEC_USE_COUNT;
return 0;
}

static struct file_operations buf_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 */
};

static int buf_init()
{
int result;

flag = 0;
wp = rp = buf;

result = register_chrdev( 54,"buf",&buf_fops );
if ( result < 0 ) {
printk( "<5>buf: cannot get major 54 " );
return result;
}

return 0;
}

static void buf_clean()
{
if ( unregister_chrdev( 54,"buf" ) ) {
printk( "<5>buf: unregister_chrdev error " );
}
}

int init_module( void )
{
return buf_init();
}

void cleanup_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: cannot get major 54 " );
return result;
}
return 0;

init_buf()做的事就是去注册一个character device driver。在注册一个character device driver之前,必须要先准备一个型为file_operations结构的变量,里面包含了一些function pointer。driver的作者必须自己写这些function。并将function address放到这个结构里。如此以来,当user去读取这个device时,kernel才有办法呼叫对应这个driver的function。

register_chrdev() 看名字就大概知道是要注册 character device driver。第一个参数是此 device 的 major number。第二个是它的名字。名字你可以随便取。第三个的参数就是一个 file_operations 变量的地址。init_module() 必须要传回 0,module 才会被加载。

在 cleanup_module() 的部分,我们也是只呼叫 buf_clean() 而已。它做的事是 unregister 的动作。

if ( unregister_chrdev( 54,"buf" ) ) {
printk( "<5>buf: unregister_chrdev error " );
}

也就是将原本记录在 device driver table 上的资料洗掉。第一个参数是 major number。第二个则是此 driver 的名称,这个名字必须要跟 register_chrdev() 中所给的名字一样才行。

现在我们来看看此 driver 所提供的 file_operations 是那些。

static struct file_operations buf_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 */
};

在 此,我们只打算 implement buf_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()。

static int buf_open( struct inode *inode,struct file *filp )
MOD_INC_USE_COUNT;
return 0;
}

buf_open() 做的事很简单。就是将此 module 的 use count 加一。这是为了避免当此 module 正被使用时不会被从 kernel 移除掉。相对应的,在 buf_release() 中,我们应该要将 use count 减一。就像开启档案一样。有 open(),就应该要有对应的 close() 才行。如果 module 的 use count 在不为 0 的话,那此 module 就无法从 kernel 中移除了。

static int buf_release( struct inode *inode,struct file *filp )
{
MOD_DEC_USE_COUNT;
return 0;
}

接下来,我们要看一下buf_read()和buf_write()。

static ssize_t buf_read( struct file *filp,char *buf,size_t count,
loff_t *ppos )
{
return count;
}

static ssize_t buf_write( struct file *filp,const char *buf,
size_t count,loff_t *ppos )
{
return count;
}

在 此,我们都只是回传 user 要求读取或写入的字符数目而已。在此,我要说明一下这些参数的意义。filp 是一个 file 结构的 pointer。也就是指我们在 /dev 下所产生的 buf 档案的 file 结构。当我们呼叫 read() 或 write() 时,必须要给一个 buffer 以及要读写的长度。Buf 指的就是这个 buffer,而 count 指的就是长度。至于 ppos 是表示目前这个档案的 offset 在那里。这个值对普通档案是有用的。也就是跟 lseek() 有关系。由于在这里是一个 drvice。所以 ppos 在此并不会用到。

有一点要小心的是,上面参数buf是一个地址,而且还是一个user space的地址,当kernel呼叫buf_read()时,程序在位于kernel space。所以你不能直接读写资料到buf里。

Makefile
P = buf
OBJ = buf.o
INCLUDE = -I/usr/src/linux/include/linux
CFLAGS = -D__KERNEL__ -DMODVERSIONS -DEXPORT_SYMTAB -O $(INCLUDE)
-include /usr/src/linux/include/linux/modversions.h
CC = gcc

$(P): $(OBJ)
ld -r $(OBJ) -o $(P).o

.c.o:
$(CC) -c $(CFLAGS) $<

clean:
rm -f *.o *~ $(P)

 加 入上面这个 Makefile,打入 make 之后,就会产生一个 buf.o 的档案。利用 insmod 将 buf.o 载到 kernel 里。相信大家应该都用过 /dev/zero 这个 device。去读取这个 device,只会得到空的内容。写资料到这个 device 里也只会石沈大海。现在你可以去比较 buf 和 zero 这两个 device。两者的行为应该很类似才是。

第三步:我们在第二步中实现一个像zero的device driver。我们现在要经由修改它来使用wait_queue。首先我们先加入一个global variable write_wq,并把它设置为NULL。

struct wait_queue *write_wq = NULL;

 然后,在 buf_read() 里,我们要改写成这个样子。

static ssize_t buf_read( struct file *filp,char *buf,size_t count,
loff_t *ppos )
{
int num,nRead;
nRead = 0;
while ( ( wp == rp ) && !flag ) { /* buffer is empty */
return 0;
}

repeate_reading:
if ( rp < wp ) {
num = min( count,( int ) ( wp-rp ) );
}
else {
num = min( count,( int ) ( buffer+BUF_LEN-rp ) );
}
copy_to_user( buf,rp,num );
rp += num;
count -= num;
nRead += num;
if ( rp == ( buffer + BUF_LEN ) )
rp = buffer;
if ( ( rp != wp ) && ( count > 0 ) )
goto repeate_reading;
flag = 0;
wake_up_interruptible( &write_wq );----唤醒被阻塞进程
return nRead;
}

 在 前头我有提到,buf 的地址是属于 user space 的。在 kernel space 中,你不能像普通写到 buffer 里一样直接将资料写到 buf 里,或直接从 buf 里读资料。Linux 里使用 FS 这个 register 来当作 kernel space 和 user space 的切换。所以,如果你想手动的话,可以这样做:

mm_segment_t fs;
fs = get_fs();
set_fs( USER_DS );
write_data_to_buf( buf );
set_fs( fs );

 也 就是先切换到 user space,再写资料到 buf 里。之后记得要切换回来 kernel space。这种自己动手的方法比较麻烦,所以 Linux 提供了几个 function,可以让我们直接在不同的 space 之间做资料的搬移。诚如各位所见,copy_to_user() 就是其中一个。

顾 名思义,copy_to_user() 就是将资料 copy 到 user space 的 buffer 里,也就是从 to 写到 from,n 为要 copy 的 byte 数。相同的,copy_from_user() 就是将资料从 user space 的 from copy 到位于 kernel 的 to 里,长度是 n bytes。在以前的 kernel 里,这两个 function 的前身是 memcpy_tofs() 和 memcpy_fromfs(),不知道为什么到了 kernel 2.2.1之后,名字就被改掉了。至于它们的程序代码有没有更改就不太清楚了。至于到那一版才改的。我没有仔细去查,只知道在 2.0.36 时还没改,到了 2.2.1 就改了。这两个 function 是 macro,都定义在 里。要使用前记得先 include 进来。

相信 buf_read() 的程序代码应当不难了解才对。不知道各位有没有看到,在buf_read() 的后面有一行的程序,就是:

wake_up_interruptible( &write_wq );

write_wq 是我们用来放那些想要写资料到 buffer,但 buffer 已满的 process。这一行的程序会将挂在此 queue 上的 process 叫醒。当 queue 是空的时,也就是当 write_wq 为 NULL 时,wake_up_interruptible() 并不会造成任何的错误。接下来,我们来看看更改后的 buf_write()。

static ssize_t buf_write( struct file *filp,const char *buf,size_t count,loff_t *ppos )
{
int num,nWrite;
nWrite = 0;
while ( ( wp == rp ) && flag ) {
interruptible_sleep_on( &write_wq );
}

repeate_writing:
if ( rp > wp ) {
num = min( count,( int ) ( rp - wp ) );
}
else {
num = min( count,( int ) ( buffer + BUF_LEN - wp ) );
}
copy_from_user( wp,buf,num );
wp += num;
count -= num;
nWrite += num;
if ( wp == ( buffer + BUF_LEN ) ) {
wp = buffer;
}
if ( ( wp != rp ) && ( count > 0 ) ) {
goto repeate_writing;
}
flag = 1;
return nWrite;
}

 我们把 process 丢到 write_wq 的动作放在 buf_write() 里。当 buffer 已满时,就直接将 process 丢到 write_wq 里.

while ( ( wp == rp ) && flag ) {
interruptible_sleep_on( &write_wq );
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值