相信很多写程序的人都写过 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 也在运作。当然,这是指 single processor 的情况下,如果是 SMP 的话,那我就不清楚了。我想很多人都在 Windows 3.1 下写过程序,在那种环境下写程序,每一个程序都必须适当的将 CPU 让给别的程序使用。如果有个程序里面有一个
while (1);
的话,那保证系统就停在那里了。这种的多任务叫做 non-preemptive。它多任务的特性是由各个程序相互合作而造成的。在 Linux 的 user space 下,则是所谓的 preemptive,各个 process 喜欢执行什么就执行什么,就算你在你的程序里加上 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_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不会改变任何的 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,大小是 8192 bytes。提供 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。有关 character device driver 的写法。有机会我再跟各位介绍,由于这次是讲 wait_queue,所以,就不再多提 driver 方面的东西.
第二步,我们要写一个 module,底下是这个 module 的程序代码:
buf.c
#define MODULE
#include
#include
#include
#include
#include
#define BUF_LEN 8192