select 需要驱动程序的支持,驱动程序实现fops内的 poll 函数 。 select 通过每个设备文件对应的 poll 函数提供的信息判断当前是否有资源可用 ( 如可读或写 ) ,如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执 行。
下面我们分两个过程来分析 select :
1. select 的睡眠过程
支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读 / 写等待队列用于支持上层 ( 用户层 ) 所需的 BLOCK 或 NONBLOCK 操作。当应用程序通过设备驱 动访问该设备时 ( 默 认为 BLOCK 操 作 ) ,若该设备当 前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读 / 写等待队列让其睡眠一段时间,等到有数据可读 / 写时再将该进程唤醒。
select 就是巧妙的利用等待队列机制让用户进程适当在没有资源可读 / 写时睡眠,有资源可读 / 写时唤醒。下面我们看看 select 睡眠的详细过程。
select 会循环遍历它所监测的 fd_set 内的所有文件描述符对应的驱动程序的 poll 函数。驱动程序提供的 poll 函数首先会将调用 select 的用户进程插入到该设备驱动对应资源的等待队列 ( 如读 / 写等待队列 ) ,然后返回一个 bitmask 告诉 select 当前资源哪些可用。当 select 循环遍历完所有 fd_set 内指定的文件描述符对应的 poll 函数后,如果没有一个资源可用 ( 即没有一个文件可供操作 ) ,则 select 让该进程睡眠,一直等到有资源可 用为止,进程被唤醒 ( 或者 timeout) 继续往下执行。
下面分析一下代码是如何实现的。
select 的调用 path 如下: sys_select -> core_sys_select -> do_select
其中最重要的函数是 do_select, 最主要的工作是在这里 , 前面两个函数主要做一些准备工作。 do_select 定义如下:
2. select 的唤醒过程
前面介绍了 select 会循环遍历它所监测的 fd_set 内的所有文件描述符对应的驱动 程序的 poll 函 数。驱动程序提供的 poll 函数首先会将调用 select 的用户进程插入到该设备驱动对应资源的等待队列 ( 如读 / 写等待队列 ) ,然后返回一个 bitmask 告诉 select 当前资源哪些可用。
一个典型的驱动程序 poll 函数实现如下:
( 摘 自《 Linux Device Drivers – ThirdEdition 》 Page 165)
将用户进程插入驱动的等待队列是通过poll_wait做的。Poll_wait定义如下:
这里的p->qproc在do_select内poll_initwait(&table)被初始化为__pollwait,如下:
__pollwait定义如下:
通过 init_waitqueue_entry 初始化一个等待队列项,这个等待队列项关联的进程即当前调用 select 的进程。然后将这个等待队列项插 入等待队列 wait_address 。 Wait_address 即在驱动 poll 函数内调用 poll_wait(filp, &dev->inq, wait); 时传入的该驱动的 &dev->inq 或者 &dev->outq 等待队列。
注 : 关于等待队列的工作原理可以参考下面这篇文档 :
http://blog.chinaunix.net/u2/60011/showart_1334657.html
到这里我们明白了 select 如何当前进程插入所有所监测的 fd_set 关联的驱动内的等待队列,那进 程究竟是何时让出 CPU 进入睡眠状态的呢?
进入睡眠状态是在 do_select 内调用 schedule_timeout(__timeout) 实现的。当 select 遍历完 fd_set 内的所有设备文件,发现没有文件可操作时 ( 即 retval=0), 则调用 schedule_timeout(__timeout) 进入睡眠状态。
唤醒该进程的过程通常是在所监测文件的设备驱动内实现的, 驱动程序维护了针对自身资源读写的等待队列。当设备驱动发现自身资源变为可读写并且有进程睡眠在该资源的等待队列上时,就会唤醒这个资源等待队列上的进 程。
举个例子,比如内核的 8250 uart driver:
Uart 是使用的 Tty 层维护的两个等待队列 , 分别对应于读和写 : (uart 是 tty 设备的一种 )
struct tty_struct {
……
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
……
}
当 uart 设备接收到数据,会调用 tty_flip_buffer_push(tty); 将收到的数据 push 到 tty 层的 buffer 。
然后查看是否有进程睡眠的读等待队列上,如果有则唤醒该等 待会列。
过程如下:
serial8250_interrupt -> serial8250_handle_port -> receive_chars -> tty_flip_buffer_push ->
flush_to_ldisc -> disc->receive_buf
在 disc->receive_buf 函数内:
if (waitqueue_active(&tty->read_wait)) // 若有进程阻塞在 read_wait 上则唤醒
wake_up_interruptible(&tty->read_wait);
到这里明白了 select 进程被唤醒的过程。由于该进程是 阻塞在所有监测的文件对应的设备等待队列上的,因此在 timeout 时间内,只要任意个设备变为可操作,都会立即唤醒该进程,从而继续往下执行。这就实现了 select 的当有一个文件描述符可操作时 就立即唤醒执行的基本原理。
Referece:
1. Linux Device Drivers – ThirdEdition
2. 内核等待队列机制原理分析
http://blog.chinaunix.net/u2/60011/showart_1334657.html
3. Kernel code : Linux 2.6.18_pro500 - Montavista