在应用程序中可以通过select或poll系统调用来查询多个文件描述符。它们都允许进程一直阻塞,直到一个或多个文件描述符成为就绪状态或者在调用时指定一个超时时间。poll执行的任务同select很类似,两者的主要区别在于如何指定待检查的文件描述符,通常select的应用更广泛一些。
select调用的原型为:
#include <sys/time.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中nfds表示需要检测的最大文件描述符加1。readfs、writefds、exceptfds分别是select要查询的读写和异常处理文件描述符的集合。timeout是超时时间。
在使用select之前我们首先需要确定其参数。
首先要确定各个文件描述符的集合。数据类型fd_set已位掩码的形式来实现的,在使用的过程中我们不需要知道其内部细节内核为我们提供了下面的一些宏来操作文件描述符的集合:
#include <sys/select.h>
void FD_ZERO(fd_set *fdset); //将fdset所指向的集合清空
void FD_SET(int fd, fd_set *fdset);//将文件描述符fd加入fdset所指向的集合中
void FD_CLR(int fd, fd_set *fdset);//将文件描述符fd从fdset所指向的集合中移除
void FD_ISSET(int fd, fd_set *fdset);//判断fd是否在fdset所指向的集合中
参数timeout决定select的阻塞行为,当改参数指定为NULL时select会一直阻塞下去直到一个或多个文件描述符成为就绪状态。当timeout指向一个timeval结构是,timeout将是select阻塞等待的超时时间。timeval结构体的定义如下:
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
如果结构体timeval的两个域都为0的话,select将不阻塞应用程序中的select最终会导致执行驱动程序中的poll函数。驱动程序中poll函数的原型是:
unsigned int (*poll) (struct file *fp, struct poll_table *wait);
其中参数struct poll_table *wait是一个等待队列表,由被轮询等待数据的设备驱动程序所拥有。在驱动程序中我们的poll函数主要要实现两个功能:
1、 将驱动程序中可引起设备状态变化的等待队列头加到struct poll_table *wait中,这一步我们可以使用poll_wait()来实现。
2、 返回一个描述设备能进行哪些操作的掩码。
其中常用的操作标识码有:
POLLIN:设备可以无阻塞地读取
POLLRDNORM:数据准备就绪,可以读取(一个可读设备返回POLLIN| POLLRDNORM)。
POLLOUT:设备可以无阻塞地写入
POLLWRNORM:同POLLOUT意义一样(一个可写设备将返回POLLOUT| POLLWRNORM)。
就写驱动程序而言,我们只需要实现上面的功能就可以了,在驱动程序中一个简单的poll函数模板如下:
static unsigned int xxx_poll (struct file *fp, struct poll_table *wait)
{
unsigned int mask = 0;
/*将驱动中的read_wait 等待队列加入wait队列表中*/
poll_wait(fp, &read_wait, wait);
/*返回可进行操作的掩码*/
if (readable)
mask |= POLLIN | POLLRDNORM; /*可读*/
return mask;
}
poll_wait()的定义在linux/poll.h中:
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && wait_address)
p->qproc(filp, wait_address, p);
}
它只是简单地调用了poll_table中的qproc函数,在我们目前的应用中这个函数实际是__pollwait(),这个函数定义在fs/select.c文件中:
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
poll_table *p)
{
struct poll_table_entry *entry = poll_get_entry(p);
if (!entry)
return;
get_file(filp);
entry->filp = filp;
entry->wait_address = wait_address;
init_waitqueue_entry(&entry->wait, current);
add_wait_queue(wait_address, &entry->wait);
}
通过上面的代码可以知道poll_wait的作用只是将驱动程序的等待队列,加到一个等待队列列表中,这样当条件满足的时候就可以在驱动程序中唤醒相应的等待队列来唤醒进程了。注意,poll_wait并不会引起进程休眠。
那系统是如何将__pollwait()函数同wait->qproc联系起来的呢,要回答这个问题我们必须首先了解在应用程序中调用select是如何一步一步调用到我们驱动程序的poll函数的。
在应用程序作用调用select最终会依次调用内核中sys_select()->core_sys_select()->do_select(),这些函数都定义在fs/select.c文件中。先来看一看do_select()的核心代码:
int do_select(int n, fd_set_bits *fds, s64 *timeout)
{
struct poll_wqueues table;
poll_table *wait;
int retval, i;
...
/*初始化table,在其中会将__pollwait()函数赋值给table.pt的成员qproc。*/
poll_initwait(&table);
/*wait只是table的一个成员pt*/
wait = &table.pt;
...
for (;;) {
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
long __timeout;
/*将当前进程状态设置为TASK_INTERRUPTIBLE */
set_current_state(TASK_INTERRUPTIBLE);
...
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
...
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
...
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll)
/*调用驱动程序中的poll函数*/
mask = (*f_op->poll)(file, retval ? NULL : wait);
...
}
...
}
...
}
wait = NULL;
/*如果可进行一项IO操作|时间到|收到信号 则跳出循环唤醒进程*/
if (retval || !*timeout || signal_pending(current))
break;
...
/*调度其他进程执行*/
__timeout = schedule_timeout(__timeout);
if (*timeout >= 0)
*timeout += __timeout;
}
/*将进程状态设为TASK_RUNNING */
__set_current_state(TASK_RUNNING);
poll_freewait(&table);
return retval;
}
在do_select()中会遍历调用select所设置的所有的文件描述符所指向文件的驱动程序的poll函数,取出可操作的动作的mask。如果没有任何可操作的I/O则会让当前进程休眠,通过下面的语句:
__timeout = schedule_timeout(__timeout);
如果某个文件的驱动程序将进程唤醒,则会继续循环从新执行各个文件驱动程序的poll函数,并取出可进行I/O动作的mask。当代码再次运行到这一句是会从新判断条件并跳出循环:
/*如果可进行一项IO操作|时间到|收到信号 则跳出循环唤醒进程*/
if (retval || !*timeout || signal_pending(current))
break;
然后将进程状态设为TASK_RUNNING,do_select()返回,直至我们的系统调用select()返回。
至此,Linux的整个轮询机制的流程就已经完了。