我想有件事要先提及的是 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();
以上的程序代码应该要放在 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 的:
我现在来解释一下为什么会有两组。有 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 的。不然,很有可能还是会有上述的情形发生.
第一步,这个 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 方面的东西.
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 结构的变量,file_operations 里包含了一些 function pointer。driver 的作者必须自己写这些 function。并将 function address 放到这个结构里。如此一来,当 user 去读取这个 device 时,kernel 才有办法去呼叫对应这个 driver 的 function。其实,简要来讲。character device driver 就是这么一个 file_operations 结构的变量。file_operations 定义在 这个档案里。它的 prototype 在 kernel 2.2.1 与以前的版本有些微的差异,这点是需要注意的地方。
register_chrdev() 看名字就大概知道是要注册 character device driver。第一个参数是此 device 的 major number。第二个是它的名字。名字你可以随便取。第三个的参数就是一个 file_operations 变量的地址。init_module() 必须要传回 0,module 才会被加载。
第三步,我们在第二步中 implement 一个像 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 的切换。所以,如果你想手动的话,可以这样做:
顾名思义,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 进来。
从这里可以看出,两个 macro 几乎是一样的,差别只在于传给 __wake_up() 中的一个 flag 有所差异而已。其实,wake_up() 传给 __wake_up() 的是 TASK_UNINTERRUPTIBLE|TASK_INTERRUPTIBLE,意思是说它会将 wait_queue list 中 process->state 是 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 的所有 process 叫醒。而 wake_up_interruptible() 则只将 state是 TASK_INTERRUPTIBLE 的叫醒.
void __wake_up(struct wait_queue **q,unsigned int mode) { struct wait_queue *next; read_lock(&waitqueue_lock); if (q && (next = *q)) { struct wait_queue *head; head = WAIT_QUEUE_HEAD(q); while (next != head) { struct task_struct *p = next->task; next = next->next; if (p->state & mode) wake_up_process(p); } } read_unlock(&waitqueue_lock); }
在 wake up 的过程中,我们不需要设定 write lock,但是仍要设定 read lock,这是为了避免有人在我们读取 wait_queue 时去写 wait_queue list 的内容,造成 inconsistent。在这段程序代码中,是去 transverse 整个 list,如果 process 的 state 跟 mode 有吻合,则呼叫 wake_up_process() 将它叫醒。
在此,runqueue_lock 也是一个 spin lock,kernel 依然在此设一个 critical section 以方便更改 run queue。Run queue 是用来放可以执行的 process 用的。在放入 run queue 之前,会先将 process 的 state 设为 TASK_RUNNING。