select与poll是比较常用的多路IO复用,在处理高并发情况下使用的更多的是epoll。这篇文章主要介绍select与poll的内核实现,而epoll将在下一次的文章中介绍。
一、先谈谈文件操作函数与IO多路复用的驱动实现
1、文件操作函数
我们知道在linux(unix)系统中,一切皆是文件。而在linux环境编程中,我们要读写或操作一个文件时,我们首先需要通过open
函数打开该文件获取到文件的描述符,文件描述符是一个无符号整形的数值。无论我们是打开磁盘上的文件,亦或者是打开设备还是网络套接字,最终返回给我们的都是一个文件描述符,这也恰是我喜爱linux的原因之一。
当我们使用open函数时,返回给我们的那个文件描述符其实只是一个索引,这个索引指向当前进程拥有的文件指针数组。在内核中,进程往往使用task_struct数据结构表示,其中的成员files
就是当前进程打开文件信息,files
中的fd_array
就是打开的文件指针数组,而open
函数返回给我们的就是指向该数组中我们打开文件的那个指针。
打开的文件在内存中被抽象成file
结构,而对于文件的读写等操作实现将会调用该结构中的file_operations
成员,file_operations
是由多个成员是函数指针的结构体,其中用户空间的read
最终将会调用它的read
函数,write
将会调用它的write
函数,等等,这里我们需要着重注意的是其中的poll函数。具体内容如下。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*open) (struct inode *, struct file *);
...
}
我们想在用户空间对一个文件使用多路I/O复用,那么我们需要实现该文件驱动的poll
函数。对于一般的磁盘文件而言,这些函数都已在文件系统驱动中实现,而对于自己定义的设备,我们需要自己实现poll
函数。
2、I/O多路复用的驱动实现
poll
函数的原型如下,无论是调用select
、poll
还是epoll
,最终都会调用该函数。
unsigned int (*poll) (struct file *filp, poll_table *wait);
其中filp
是当前文件指针,而wait
是内核中处理poll等待队列的结构。要实现poll函数,我们只要先声明当前设备的读写等状态的等待队列,然后将该等待队列和filp
、wait
传入到下面这个函数中即可,当设备状态改变时可唤醒对应的等待队列。具体内核中如何实现后文将会介绍。
void poll_wait (struct file *, wait_queue_head_t *, poll_table *);
二、select的内核实现
select
函数的实现在内核源码select.c文件中。其中当我们调用select函数时,首先调用的是系统调用SYSCALL_DEFINE5
函数,而该函数调用的是core_sys_select
函数。
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
fd_set __user *, exp, struct timeval __user *, tvp)
{
ret = core_sys_select(n, inp, outp, exp, to);
return ret;
}
在core_sys_select
中,将select传入的文件描述符集合统一存入了fds
变量中,然后调用do_select
函数。
int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, struct timespec *end_time)
{
fd_set_bits fds;
if ((ret = get_fd_set(n, inp, fds.in)) ||
(ret = get_fd_set(n, outp, fds.out)) ||
(ret = get_fd_set(n, exp, fds.ex)))
goto out;
zero_fd_set(n, fds.res_in);
zero_fd_set(n, fds.res_out);
zero_fd_set(n, fds.res_ex);
ret = do_select(n, &fds, end_time);
return ret;
}
在do_select
函数中,是这些描述符处理的地方。这里先使用poll_initwait
主要是对上文的poll_wait
函数进行赋值,然后遍历每一个描述符,调用文件驱动中的poll
函数。
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
/*这个函数时初始化poll_wait函数的*/
poll_initwait(&table);
/*遍历每一个描述符*/
for (;;) {
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {
struct fd f;
f = fdget(i);
if (f.file) {
/*调用驱动中实现的poll函数*/
f_op = f.file->f_op;
mask = DEFAULT_POLLMASK;
if (f_op->poll) {
mask = (*f_op->poll)(f.file, wait);
}
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit;
retval++;
wait->_qproc = NULL;
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit;
retval++;
wait->_qproc = NULL;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
wait->_qproc = NULL;
}
/* got something, stop busy polling */
if (retval) {
can_busy_loop = false;
busy_flag = 0;
} else if (busy_flag & mask)
can_busy_loop = true;
}
}
}
wait->_qproc = NULL;
/*如果有一个可读就跳出循环*/
if (retval || timed_out || signal_pending(current))
break;
/*设置定时器睡眠*/
if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, to, slack))
timed_out = 1;
}
poll_freewait(&table);
return retval;
}
poll_initwait函数主要是将其中的pwq->pt
赋值为__pollwait
,而pwq->pt
我们发现在poll_wait
函数中被使用,即poll_wait
最终调用的就是__pollwait
。
至此我们可以看到,在我们调用select
函数时,最终调用的是设备驱动中的poll
函数。poll
函数中为设备的读写等状态分别声明了等待队列,并将等待队列传递给了__pollwait
函数,而__pollwait
函数把当前进程加入等待队列中,然后就去睡眠了。当超时或者被唤醒时便会查看是否有描述符状态发生改变。
void poll_initwait(struct poll_wqueues *pwq)
{
init_poll_funcptr(&pwq->pt, __pollwait);
pwq->polling_task = current;
pwq->triggered = 0;
pwq->error = 0;
pwq->table = NULL;
pwq->inline_index = 0;
}
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && p->_qproc && wait_address)
p->_qproc(filp, wait_address, p);
}
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
poll_table *p)
{
struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);
struct poll_table_entry *entry = poll_get_entry(pwq);
if (!entry)
return;
entry->filp = get_file(filp);
entry->wait_address = wait_address;
entry->key = p->_key;
init_waitqueue_func_entry(&entry->wait, pollwake);
entry->wait.private = pwq;
add_wait_queue(wait_address, &entry->wait);
}
三、poll的内核实现
poll
系统调用的实现与select
相似。首先poll
函数直接调用的是SYSCALL_DEFINE3
函数,在改函数中直接调用的是do_sys_poll
函数。
SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds,
int, timeout_msecs)
{
ret = do_sys_poll(ufds, nfds, to);
return ret;
}
do_sys_poll
可分为三个部分,第一个部分是将用户空间传来的描述符集合拷贝到内核空间,第二部分与select
函数类似给poll_wait
函数赋值,最后调用do_poll
函数对描述符集合进行操作。
int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
struct timespec *end_time)
{
/*1.将描述符添加到工作链表中,这里的工作链表的第一个节点为head,内存较小,如果head不够存储才会继续申请大块的内存*/
/*ken: POLL_STACK_ALLOC=256*/
long stack_pps[POLL_STACK_ALLOC/sizeof(long)];
struct poll_list *const head = (struct poll_list *)stack_pps;
struct poll_list *walk = head;
unsigned long todo = nfds;
len = min_t(unsigned int, nfds, N_STACK_PPS);
for (;;) {
walk->next = NULL;
walk->len = len;
if (!len)
break;
/*将用户空间中的描述符一部分拷贝到工作链表结点中,循环拷贝,直到拷贝完为止*/
if (copy_from_user(walk->entries, ufds + nfds-todo,
sizeof(struct pollfd) * walk->len))
goto out_fds;
todo -= walk->len;
if (!todo)
break;
/*如果剩余的描述符需要的空间比POLLFD_PER_PAGE小则申请需要的空间即可,否则申请POLLFD_PER_PAGE*/
len = min(todo, POLLFD_PER_PAGE);
size = sizeof(struct poll_list) + sizeof(struct pollfd) * len;
walk = walk->next = kmalloc(size, GFP_KERNEL);
if (!walk) {
err = -ENOMEM;
goto out_fds;
}
}
/*2.初始化poll_wait函数*/
poll_initwait(&table);
/*3.调用do_poll函数*/
fdcount = do_poll(nfds, head, &table, end_time);
return err;
}
do_poll
函数比较简单,遍历工作链表中的描述符并调用do_pollfd
函数,do_pollfd
函数最终调用了驱动中的poll
函数将改进程加入到该设备的等待队列中,其余操作与select类似。
static int do_poll(unsigned int nfds, struct poll_list *list,
struct poll_wqueues *wait, struct timespec *end_time)
{
for (;;) {
/*遍历所有的文件描述符*/
for (walk = list; walk != NULL; walk = walk->next) {
pfd = walk->entries;
pfd_end = pfd + walk->len;
for (; pfd != pfd_end; pfd++) {
/*这里需要注意,当返回有效描述符时,就把pt->_qproc赋值
为NULL,即poll_wait函数为NULL,是因为已经存在有效的
描述符了,可以返回不必等待了,不需要把剩下的描述符都加
入等待队列中
*/
if (do_pollfd(pfd, pt, &can_busy_loop, busy_flag)) {
count++;
pt->_qproc = NULL;
/* found something, stop busy polling */
busy_flag = 0;
can_busy_loop = false;
}
}
}
/*
* All waiters have already been registered, so don't provide
* a poll_table->_qproc to them on the next loop iteration.
*/
pt->_qproc = NULL;
if (!count) {
count = wait->error;
if (signal_pending(current))
count = -EINTR;
}
if (count || timed_out)
break;
/*进程进行睡眠*/
if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack))
timed_out = 1;
}
return count;
}
static inline unsigned int do_pollfd()
{
unsigned int mask;
int fd;
mask = 0;
fd = pollfd->fd;
if (fd >= 0) {
struct fd f = fdget(fd);
mask = POLLNVAL;
if (f.file) {
mask = DEFAULT_POLLMASK;
if (f.file->f_op->poll) {
pwait->_key = pollfd->events|POLLERR|POLLHUP;
pwait->_key |= busy_flag;
/*ken: 将其加入描述符的等待队列中*/
mask = f.file->f_op->poll(f.file, pwait);
if (mask & busy_flag)
*can_busy_poll = true;
}
/* Mask out unneeded events. */
mask &= pollfd->events | POLLERR | POLLHUP;
fdput(f);
}
}
pollfd->revents = mask;
return mask;