select(poll)分析背景: 我们编写驱动程序的时候,通常需要告诉上层应用程序设备的状态:是否可读写。 应用程序可以直接通过read/write系统调用(阻塞和非阻塞模式)进入内核态驱动程序,那么驱动程序的read/write至少需要支持阻塞和非阻塞模式的读写: 上层阻塞模式调用read时,如果设备驱动的read_buffer中没有数据可以供上层读取,就让该进程睡眠(阻塞)直到有数据才被唤醒,然后取到数据返回用户态;如果是非阻塞模式调用read时,read_buffer中没有数据直接返回0。 上层阻塞模式调用write时,如果设备驱动的write_buffer已满没有空间写入数据,就让该进程睡眠(阻塞)直到write_buffer中有空间可以写入数据才被唤醒,然后将数据写入write_buffer后返回用户态;如果是非阻塞模式调用write时,write_buffer中没有空间直接返回0。 如果一个应用程序需要同时访问多个硬件设备,此时就不能使用阻塞模式打开设备了,否则整个进程可能就阻塞在了某个硬件上。这种情况可以采用如下异步的方式:有数据可读返回数据个数,没有返回0或者-1,应用程序每隔一段时间就对依次各个设备进行读取,对单个设备来说有数据就返回数据,没有就返回0,当所有设备遍历完之后,继续睡眠,时间间隔到再唤醒继续这样的动作。这样进程反复睡眠唤醒对系统效率影响很大。 对于这个问题,linux提供了另外一个方法,select和poll的方式: 这种方式下,应用程序将轮询进程所有设备文件描述符的工作交给了操作系统,这样应用程序中只需阻塞在select或者poll系统调用中,返回后,肯定至少有一个设备文件描述符的状态有发生改变,要不然该应用进程的阻塞是不会退出的。 本次研究主要问题集中在: select和poll系统调用在应用程序中如何使用? select和poll系统调用中如何实现同时监测多个设备文件描述符? ---------------------------------------- select()和poll()系统调用的本质一样,前者在BSD UNIX中引入的,后者在System V中引入的。
一、select 应用程序中最广泛用到的是BSD UNIX中引入的select()系统调用,其原型如下: int select(int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
select的第一个参数maxfdp是文件描述符集中要被检测的数目,这个值必须至少比待检测的最大文件描述符大1; 参数readfds指定了需要被读监测的文件描述符集;参数writefds指定了需要被写监测的文件描述符集;而参数errorfds指定了可能出现异常情况的文件描述符集。
timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout时间后若没有文件描述符准备好则返回。 struct timeval数据结构的定义如下: struct timeval { int tv_sec; //秒单位 int tv_usec; //微秒单位 };
timeout取不同的值,该调用就表现不同的性质: 1.timeout为0,调用立即返回; 2.timeout为NULL,select()调用就阻塞,直到知道有文件描述符就绪;(当有文件描述符就绪时,会向这个函数发送信号,以唤醒此函数。) 3.timeout为正整数,就是一般的定时器。
select的返回值有如下情况: 1.正常情况下返回就绪的文件描述符个数; 2.经过了timeout时长后仍无设备准备好,返回值为0; 3.如果select被某个信号中断,它将返回-1并设置errno为EINTR。 4.如果出错,返回-1并设置相应的errno。
select()函数的接口主要是建立在一种叫fd_set结构体的基础上。这个结构体是一组文件描述符(fd)的集合。因为fd_set类型的长度在不同平台上是不同的,此应该用一组标准的宏定义来处理这个类变量。 我们来了解fd_set这个结构的定义: typedef struct { unsigned long fds_bits [__FDSET_LONGS]; } __kernel_fd_set;
#define __NFDBITS (8 * sizeof(unsigned long)) //32 #define __FD_SETSIZE 1024// 每个进程能打开的文件描述符的上限,可以更改 #define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS)// 32
typedef __kernel_fd_set fd_set;
对用fd_set定义的readfds、writefds、errorfds操作集进行操作最好使用封装的通用宏来处理: FD_ZERO(fd_set *set);//将文件描述符集fd_set中的值置0,如此以来对应所有位都被设置为0; FD_SET(int fd,fd_set *set);//将一个文件描述符加入文件描述集中; FD_CLR(int fd,fd_set *set)//将一个文件描述符从文件描述符集中清除; FD_ISSET(int fd,fd_set *set)//判断文件描述符是否被置位。
下面是一个典型的程序片段: FD_ZERO(&readset); FD_SET(fd,&readset); select(fd+1,&readset,NULL,NULL,NULL); if(FD_ISSET(fd,readset){……} 需要注意的是每次调用select之前都需要重新设定fd_set集合。
二、poll 函数原型 #include <poll.h> =0超时; = -1表示出错。
第一个参数是一个pollfd结构体数组,其中包括了你想测试的文件描述符和事件, 事件由结构中事件域events来确定,调用后实际发生的时间将被填写在结构体的revents域。 struct pollfd { int fd; /* 文件描述符 */ short events; /* 等待的事件 */ short revents; /* 实际发生了的事件 */ }; 等待事件的掩码: POLLIN 普通或优先级带数据可读 POLLRDNORM普通数据可读 POLLRDBAND优先级带数据可读 POLLPRI 高优先级数据可读
POLLOUT 普通或优先级带数据可写 POLLWRNORM普通数据可写 POLLWRBAND 优先级带数据可写
POLLERR发生错误 POLLHUP发生挂起 POLLVAL 描述字不是一个打开的文件 第一部分为处理输入的四个常值,第二部分是处理输出的三个常值,第三部分是处理错误的三个常值。 poll处理三个级别的数据,普通normal,优先级带priority band,高优先级high priority,这些都是出于流的实现。
第二个参数nfds用来指定第一个参数数组元素个数。
第三个参数指定poll函数在返回前等待多长时间,单位为毫秒。当等待时间为0时,poll()函数立即返回,为-1则使poll()一直阻塞直到一个指定事件发生。
如果没有事件发生,revents会被清空,所以你不必多此一举。
例子如下: int poll_two_normal(int fd1,int fd2) { struct pollfd poll_list[2]; int retval;
poll_list[0].fd = fd1; poll_list[1].fd = fd2; poll_list[0].events = POLLIN|POLLPRI; poll_list[1].events = POLLIN|POLLPRI;
while(1) { retval = poll(poll_list,(unsigned long)2,-1); /* retval 总是大于0或为-1,因为我们在阻塞中工作 */
if(retval < 0) { fprintf(stderr,"poll错误: %s/n",strerror(errno)); return -1; }
if(((poll_list[0].revents&POLLHUP) == POLLHUP) || ((poll_list[0].revents&POLLERR) == POLLERR) || ((poll_list[0].revents&POLLNVAL) == POLLNVAL) || ((poll_list[1].revents&POLLHUP) == POLLHUP) || ((poll_list[1].revents&POLLERR) == POLLERR) || ((poll_list[1].revents&POLLNVAL) == POLLNVAL)) return 0;
if((poll_list[0].revents&POLLIN) == POLLIN) handle(poll_list[0].fd,NORMAL_DATA); if((poll_list[0].revents&POLLPRI) == POLLPRI) handle(poll_list[0].fd,HIPRI_DATA); if((poll_list[1].revents&POLLIN) == POLLIN) handle(poll_list[1].fd,NORMAL_DATA); if((poll_list[1].revents&POLLPRI) == POLLPRI) handle(poll_list[1].fd,HIPRI_DATA); } } -------------------------------------------- 上层要能使用select()和poll()系统调用来监测某个设备文件描述符,那么就必须实现这个设备驱动程序中struct file_operation结构体的poll函数,为什么? 因为这两个系统调用最终都会调用驱动程序中的poll函数来初始化一个等待队列项, 然后将其加入到驱动程序中的等待队列头,这样就可以在硬件可读写的时候wake up这个等待队列头,然后等待(可以是多个)同一个硬件设备可读写事件的进程都将被唤醒。 (这个等待队列头可以包含多个等待队列项,这些不同的等待队列项是由不同的应用程序调用select或者poll来监测同一个硬件设备的时候调用file_operation的poll函数初始化填充的)。 下面就以select系统调用分析具体实现,源码路径:fs/select.c。
一、 select()系统调用代码走读 调用顺序如下:sys_select() à core_sys_select() à do_select() à fop->poll()
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp, fd_set __user *, exp, struct timeval __user *, tvp) { struct timespec end_time, *to = NULL; struct timeval tv; int ret;
if (tvp) {// 如果超时值非NULL if (copy_from_user(&tv, tvp, sizeof(tv))) // 从用户空间取数据到内核空间 return -EFAULT;
to = &end_time; // 得到timespec格式的未来超时时间 if (poll_select_set_timeout(to, tv.tv_sec + (tv.tv_usec / USEC_PER_SEC), (tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC)) return -EINVAL; }
ret = core_sys_select(n, inp, outp, exp, to); // 关键函数 ret = poll_select_copy_remaining(&end_time, tvp, 1, ret); /*如果有超时值, 并拷贝离超时时刻还剩的时间到用户空间的timeval中*/
return ret; // 返回就绪的文件描述符的个数 }
================================================================== core_sys_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; /** typedef struct { unsigned long *in, *out, *ex; unsigned long *res_in, *res_out, *res_ex; } fd_set_bits; 这个结构体中定义的全是指针,这些指针都是用来指向描述符集合的。 **/ void *bits; int ret, max_fds; unsigned int size; struct fdtable *fdt; /* Allocate small arguments on the stack to save memory and be faster */ long stack_fds[SELECT_STACK_ALLOC/sizeof(long)]; // 256/32 = 8, stack中分配的空间 /** @ include/linux/poll.h #define FRONTEND_STACK_ALLOC 256 #define SELECT_STACK_ALLOC FRONTEND_STACK_ALLOC **/
ret = -EINVAL; if (n < 0) goto out_nofds;
/* max_fds can increase, so grab it once to avoid race */ rcu_read_lock(); fdt = files_fdtable(current->files); // RCU ref, 获取当前进程的文件描述符表 max_fds = fdt->max_fds; rcu_read_unlock(); if (n > max_fds)// 如果传入的n大于当前进程最大的文件描述符,给予修正 n = max_fds;
/* * We need 6 bitmaps (in/out/ex for both incoming and outgoing), * since we used fdset we need to allocate memory in units of * long-words. */ size = FDS_BYTES(n); // 以一个文件描述符占一bit来计算,传递进来的这些fd_set需要用掉多少个字 bits = stack_fds; if (size > sizeof(stack_fds) / 6) { // 除6,为什么?因为每个文件描述符需要6个bitmaps /* Not enough space in on-stack array; must use kmalloc */ ret = -ENOMEM; bits = kmalloc(6 * size, GFP_KERNEL); // stack中分配的太小,直接kmalloc if (!bits) goto out_nofds; } // 这里就可以明显看出struct fd_set_bits结构体的用处了。 fds.in = bits; fds.out = bits + size; fds.ex = bits + 2*size; fds.res_in = bits + 3*size; fds.res_out = bits + 4*size; fds.res_ex = bits + 5*size; // get_fd_set仅仅调用copy_from_user从用户空间拷贝了fd_set 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); // 对这些存放返回状态的字段清0 zero_fd_set(n, fds.res_out); zero_fd_set(n, fds.res_ex);
ret = do_select(n, &fds, end_time); // 关键函数,完成主要的工作
if (ret < 0) // 有错误 goto out; if (!ret) { // 超时返回,无设备就绪 ret = -ERESTARTNOHAND; if (signal_pending(current)) goto out; ret = 0; }
// 把结果集,拷贝回用户空间 if (set_fd_set(n, inp, fds.res_in) || set_fd_set(n, outp, fds.res_out) || set_fd_set(n, exp, fds.res_ex)) ret = -EFAULT;
out: if (bits != stack_fds) kfree(bits); // 如果有申请空间,那么释放fds对应的空间 out_nofds: return ret; // 返回就绪的文件描述符的个数 }
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ do_select()函数解析:
int do_select(int n, fd_set_bits *fds, struct timespec *end_time) { ktime_t expire, *to = NULL; struct poll_wqueues table; poll_table *wait; int retval, i, timed_out = 0; unsigned long slack = 0;
rcu_read_lock(); // 根据已经设置好的fd位图检查用户打开的fd, 要求对应fd必须打开, 并且返回 // 最大的fd。 retval = max_select_fd(n, fds); rcu_read_unlock();
if (retval < 0) return retval; n = retval;
// 一些重要的初始化: // poll_wqueues.poll_table.qproc函数指针初始化,该函数是驱动程序中poll函数实 // 现中必须要调用的poll_wait()中使用的函数。 poll_initwait(&table); wait = &table.pt; if (end_time && !end_time->tv_sec && !end_time->tv_nsec) { wait = NULL; timed_out = 1; // 如果系统调用带进来的超时时间为0,那么设置 // timed_out = 1,表示不阻塞,直接返回。 }
if (end_time && !timed_out) slack = estimate_accuracy(end_time); // 超时时间转换
retval = 0; for (;;) { unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
inp = fds->in; outp = fds->out; exp = fds->ex; rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
// 所有n个fd的循环 for (i = 0; i < n; ++rinp, ++routp, ++rexp) { unsigned long in, out, ex, all_bits, bit = 1, mask, j; unsigned long res_in = 0, res_out = 0, res_ex = 0; const struct file_operations *f_op = NULL; struct file *file = NULL;
// 先取出当前循环周期中的32个文件描述符对应的bitmaps in = *inp++; out = *outp++; ex = *exp++; all_bits = in | out | ex; // 组合一下,有的fd可能只监测读,或者写, // 或者e rr,或者同时都监测 if (all_bits == 0) { // 这32个描述符没有任何状态被监测,就跳入 // 下一个32个fd的循环中 i += __NFDBITS; //每32个文件描述符一个循环,正好一个long型数 continue; }
// 本次32个fd的循环中有需要监测的状态存在 for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {// 初始bit = 1 int fput_needed; if (i >= n) // i用来检测是否超出了最大待监测的fd break; if (!(bit & all_bits)) continue; // bit每次循环后左移一位的作用在这里,用来 // 跳过没有状态监测的fd file = fget_light(i, &fput_needed); // 得到file结构指针,并增加 // 引用计数字段f_count if (file) { // 如果file存在 f_op = file->f_op; mask = DEFAULT_POLLMASK; if (f_op && f_op->poll) { wait_key_set(wait, in, out, bit);// 设置当前fd待监测 // 的事件掩码 mask = (*f_op->poll)(file, wait); /* 调用驱动程序中的poll函数,以evdev驱动中的 evdev_poll()为例该函数会调用函数poll_wait(file, &evdev->wait, wait),继续调用__pollwait()回调来分配一个poll_table_entry结构体,该结构体有一个内嵌的等待队列项,设置好wake时调用的回调函数后将其添加到驱动程序中的等待队列头中。 */ } fput_light(file, fput_needed); // 释放file结构指针,实际就是减小他的一个引用 计数字段f_count。
// mask是每一个fop->poll()程序返回的设备状态掩码。 if ((mask & POLLIN_SET) && (in & bit)) { res_in |= bit; // fd对应的设备可读 retval++; wait = NULL; // 后续有用,避免重复执行__pollwait() } if ((mask & POLLOUT_SET) && (out & bit)) { res_out |= bit; // fd对应的设备可写 retval++; wait = NULL; } if ((mask & POLLEX_SET) && (ex & bit)) { res_ex |= bit; retval++; wait = NULL; } } } // 根据poll的结果写回到输出位图里,返回给上级函数 if (res_in) *rinp = res_in; if (res_out) *routp = res_out; if (res_ex) *rexp = res_ex; /* 这里的目的纯粹是为了增加一个抢占点。 在支持抢占式调度的内核中(定义了CONFIG_PREEMPT), cond_resched是空操作。 */ cond_resched(); } wait = NULL; // 后续有用,避免重复执行__pollwait() if (retval || timed_out || signal_pending(current)) break; if (table.error) { retval = table.error; break; } /*跳出这个大循环的条件有: 有设备就绪或有异常(retval!=0), 超时(timed_out = 1), 或者有中止信号出现*/
/* * If this is the first loop and we have a timeout * given, then we convert to ktime_t and set the to * pointer to the expiry value. */ if (end_time && !to) { expire = timespec_to_ktime(*end_time); to = &expire; }
// 第一次循环中,当前用户进程从这里进入休眠, // 上面传下来的超时时间只是为了用在睡眠超时这里而已 // 超时,poll_schedule_timeout()返回0;被唤醒时返回-EINTR if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, to, slack)) timed_out = 1; /* 超时后,将其设置成1,方便后面退出循环返回到上层 */ }
// 清理各个驱动程序的等待队列头,同时释放掉所有空出来 // 的page页(poll_table_entry) poll_freewait(&table);
return retval; // 返回就绪的文件描述符的个数 } ------------------------------------ 二、重要结构体之间关系 比较重要的结构体由四个:struct poll_wqueues、struct poll_table_page、struct poll_table_entry、struct poll_table_struct,这小节重点讨论前三个,后面一个留到后面小节。
2.1、结构体关系 每一个调用select()系统调用的应用进程都会存在一个struct poll_weueues结构体,用来统一辅佐实现这个进程中所有待监测的fd的轮询工作,后面所有的工作和都这个结构体有关,所以它非常重要。 struct poll_wqueues { poll_table pt; struct poll_table_page *table; struct task_struct *polling_task; //保存当前调用select的用户进程struct task_struct结构体 int triggered; // 当前用户进程被唤醒后置成1,以免该进程接着进睡眠 int error; // 错误码 int inline_index; // 数组inline_entries的引用下标 struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES]; }; 实际上结构体poll_wqueues内嵌的poll_table_entry数组inline_entries[] 的大小是有限: #define MAX_STACK_ALLOC 832 #define FRONTEND_STACK_ALLOC 256 #define WQUEUES_STACK_ALLOC (MAX_STACK_ALLOC - FRONTEND_STACK_ALLOC) #define N_INLINE_POLL_ENTRIES (WQUEUES_STACK_ALLOC / sizeof(struct poll_table_entry)) 如果空间不够用,后续会动态申请物理内存页以链表的形式挂载poll_wqueues.table上统一管理。接下来的两个结构体就和这项内容密切相关: struct poll_table_page { // 申请的物理页都会将起始地址强制转换成该结构体指针 struct poll_table_page * next; // 指向下一个申请的物理页 struct poll_table_entry * entry; // 指向entries[]中首个待分配(空的) poll_table_entry地址 struct poll_table_entry entries[0]; // 该page页后面剩余的空间都是待分配的 // poll_table_entry结构体 }; 对每一个fd调用fop->poll() à poll_wait() à __pollwait()都会先从poll_wqueues. inline_entries[]中分配一个poll_table_entry结构体,直到该数组用完才会分配物理页挂在链表指针poll_wqueues.table上然后才会分配一个poll_table_entry结构体。具体用来做什么?这里先简单说说,__pollwait()函数调用时需要3个参数,第一个是特定fd对应的file结构体指针,第二个就是特定fd对应的硬件驱动程序中的等待队列头指针,第3个是调用select()的应用进程中poll_wqueues结构体的poll_table项(该进程监测的所有fd调用fop->poll函数都用这一个poll_table结构体)。 struct poll_table_entry { struct file *filp; // 指向特定fd对应的file结构体; unsigned long key; // 等待特定fd对应硬件设备的事件掩码,如POLLIN、 // POLLOUT、POLLERR; wait_queue_t wait; // 代表调用select()的应用进程,等待在fd对应设备的特定事件 // (读或者写)的等待队列头上,的等待队列项; wait_queue_head_t *wait_address; // 设备驱动程序中特定事件的等待队列头; }; 总结一下几点: 1. 特定的硬件设备驱动程序的事件等待队列头是有限个数的,通常是有读事件和写事件的等待队列头; 2. 而一个调用了select()的应用进程只存在一个poll_wqueues结构体; 3. 该应用程序可以有多个fd在进行同时监测其各自的事件发生,但该应用进程中每一个fd有多少个poll_table_entry存在,那就取决于fd对应的驱动程序中有几个事件等待队列头了,也就是说,通常驱动程序的poll函数中需要对每一个事件的等待队列头调用poll_wait()函数。比如,如果有读写两个等待队列头,那么就在这个应用进程中存在两个poll_table_entry结构体,在这两个事件的等待队列头中分别将两个等待队列项加入; 4. 如果有多个应用进程使用selcet()方式同时在访问同一个硬件设备,此时硬件驱动程序中加入等待队列头中的等待队列项对每一个应用程序来说都是相同数量的(一个事件等待队列头一个,数量取决于事件等待队列头的个数)。
2.2、注意项 对于第3点中,如果驱动程序中有多个事件等待队列头,那么在这种情况下,写设备驱动程序时就要特别小心了,特别是设备有事件就绪然后唤醒等待队列头中所有应用进程的时候需要使用另外的宏,唤醒使用的宏和函数源码见include/linux/wait.h: 在这之前看一看__pollwait()函数中填充poll_table_entry结构体时注册的唤醒回调函数pollwake()。 static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key) { struct poll_table_entry *entry;
entry = container_of(wait, struct poll_table_entry, wait); // 取得poll_table_entry结构体指针 if (key && !((unsigned long)key & entry->key)) /*这里的条件判断至关重要,避免应用进程被误唤醒,什么意思?*/ return 0; return __pollwake(wait, mode, sync, key); } 到底什么情况下会出现误唤醒呢?当然是有先决条件的。 驱动程序中存在多个事件的等待队列头,并且应用程序中只监测了该硬件的某几项事件,比如,驱动中有读写等待队里头,但应用程序中只有在监测读事件的发生。这种情况下,写驱动程序时候,如果唤醒函数用法不当,就会引起误唤醒的情况。 先来看一看我们熟知的一些唤醒函数吧! #define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL) #define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL) void __wake_up(wait_queue_head_t *q, unsigned int mode, int nr, void *key); 注意到这个key了吗?通常我们调用唤醒函数时key为NULL,很容易看出,如果我们在这种情况下,使用上面两种唤醒函数,那么上面红色字体的判断条件一直都会是假,那么也就是说,只要设备的几类事件之一有发生,不管应用程序中是否对其有监测,都会在这里顺利通过将应用程序唤醒,唤醒后,重新调用一遍fop->poll(注意:第一次和第二次调用该函数时少做了一件事,后面代码详解)函数,得到设备事件掩码。假如恰好在这次唤醒后的一轮调用fop->poll()函数的循环中,没有其他硬件设备就绪,那么可想而知,从源码上看,do_select()会直接返回0。 // mask是每一个fop->poll()程序返回的设备状态掩码。 if ((mask & POLLIN_SET) && (in & bit)) { res_in |= bit; // fd对应的设备可读 retval++; wait = NULL; // 后续有用,避免重复执行__pollwait() } (in & bit)这个条件就是用来确认用户程序有没有让你监测该事件的, 如果没有retval仍然是0,基于前面的假设,那么do_select()返回给上层的也是0。那又假如应用程序中调用select()的时候没有传入超时值,那岂不是和事实不相符合吗?没有传递超时值,那么select()函数会一直阻塞直到至少有1个fd的状态就绪。 所以在这种情况下,设备驱动中唤醒函数需要用另外的一组: #define wake_up_poll(x, m) / __wake_up(x, TASK_NORMAL, 1, (void *) (m)) #define wake_up_interruptible_poll(x, m) / __wake_up(x, TASK_INTERRUPTIBLE, 1, (void *) (m)) 这里的m值,应该和设备发生的事件相符合。设置poll_table_entry结构体的key项的函数是: #define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR) #define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR) #define POLLEX_SET (POLLPRI) static inline void wait_key_set(poll_table *wait, unsigned long in, unsigned long out, unsigned long bit) { if (wait) { wait->key = POLLEX_SET; if (in & bit) wait->key |= POLLIN_SET; if (out & bit) wait->key |= POLLOUT_SET; } } 这里的m值,可以参考上面的宏来设置,注意传递的不是key的指针,而就是其值本身,只不过在wake_up_poll()到pollwake()的传递过程中是将其转换成指针的。 如果唤醒函数使用后面一组的话,再加上合理设置key值,我相信pollwake()函数中的if一定会严格把关,不让应用程序没有监测的事件唤醒应用进程,从而避免了发生误唤醒。 -------------------------------------------------- 三、讨论几个细节
3.1、fop->poll() fop->poll()函数就是file_operations结构体中的poll函数指针项,该函数相信很多人都知道怎么写,网上大把的文章介绍其模板,但是为什么要那么写,而且它做了什么具体的事情?本小节来揭开其神秘面纱,先贴一个模板上来。 static unsigned int XXX_poll(struct file *filp, poll_table *wait) { unsigned int mask = 0; struct XXX_dev *dev = filp->private_data; ... poll_wait(filp, &dev->r_wait, wait); poll_wait(filp ,&dev->w_wait, wait);
if(...)//读就绪 { mask |= POLLIN | POLLRDNORM; } if(...)//写就绪 { mask |= POLLOUT | POLLRDNORM; } .. return mask; } poll_wait()只因有wait字样,经常给人误会,以为它会停在这里等,也就是常说的阻塞。不过我们反过来想想,要是同一个应用进程同时监测多个fd,那么没一个fd调用xxx_poll的时候都阻塞在这里,那和不使用select()又有何区别呢?都会阻塞在当个硬件上而耽误了被的设备就绪事件的读取。 其实,这个poll_wait()函数所做的工作挺简单,就是添加一个等待等待队列项到poll_wait ()函数传递进去的第二个参数,其代表的是驱动程序中的特定事件的等待队列头。 下面以字符设备evdev为例,文件drivers/input/evdev.c。 static unsigned int evdev_poll(struct file *file, poll_table *wait) { struct evdev_client *client = file->private_data; struct evdev *evdev = client->evdev;
poll_wait(file, &evdev->wait, wait); return ((client->head == client->tail) ? 0 : (POLLIN | POLLRDNORM)) | (evdev->exist ? 0 : (POLLHUP | POLLERR)); } 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); } 其中wait_address是驱动程序需要提供的等待队列头,来容纳后续等待该硬件设备就绪的进程对应的等待队列项。关键结构体poll_table, 这个结构体名字也取的不好,什么table?其实其中没有table的一丁点概念,容易让人误解呀!
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *); typedef struct poll_table_struct { poll_queue_proc qproc; unsigned long key; } poll_table; fop->poll()函数的poll_table参数是从哪里传进来的?好生阅读过代码就可以发现,do_select()函数中存在一个结构体struct poll_wqueues,其内嵌了一个poll_table的结构体,所以在后面的大循环中依次调用各个fd的fop->poll()传递的poll_table参数都是poll_wqueues.poll_table。 poll_table结构体的定义其实蛮简单,就一个函数指针,一个key值。这个函数指针在整个select过程中一直不变,而key则会根据不同的fd的监测要求而变化。 qproc函数初始化在函数do_select()àpoll_initwait()àinit_poll_funcptr(&pwq->pt, __pollwait)中实现,回调函数就是__pollwait()。 int do_select(int n, fd_set_bits *fds, struct timespec *end_time) { struct poll_wqueues table; … poll_initwait(&table); … } void poll_initwait(struct poll_wqueues *pwq) { init_poll_funcptr(&pwq->pt, __pollwait); … } static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc) { pt->qproc = qproc; pt->key = ~0UL; /* all events enabled */ }
/* Add a new entry */ 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; get_file(filp); entry->filp = filp; // 保存对应的file结构体 entry->wait_address = wait_address; // 保存来自设备驱动程序的等待队列头 entry->key = p->key; // 保存对该fd关心的事件掩码 init_waitqueue_func_entry(&entry->wait, pollwake); // 初始化等待队列项,pollwake是唤醒该等待队列项时候调用的函数 entry->wait.private = pwq; // 将poll_wqueues作为该等待队列项的私有数据,后面使用 add_wait_queue(wait_address, &entry->wait); // 将该等待队列项添加到从驱动程序中传递过来的等待队列头中去。 } 该函数首先通过container_of宏来得到结构体poll_wqueues的地址,然后调用poll_get_entry()函数来获得一个poll_table_entry结构体,这个结构体是用来连接驱动和应用进程的关键结构体,其实联系很简单,这个结构体中内嵌了一个等待队列项wait_queue_t,和一个等待队列头 wait_queue_head_t,它就是驱动程序中定义的等待队列头,应用进程就是在这里保存了每一个硬件设备驱动程序中的等待队列头(当然每一个fd都有一个poll_table_entry结构体)。 很容易想到的是,如果这个设备在别的应用程序中也有使用,又恰好别的应用进程中也是用select()来访问该硬件设备,那么在另外一个应用进程的同一个地方也会调用同样的函数来初始化一个poll_table_entry结构体,然后将这个结构体中内嵌的等待队列项添加到同一份驱动程序的等待队列头中。此后,如果设备就绪了,那么驱动程序中将会唤醒这个对于等待队列头中所有的等待队列项(也就是等待在该设备上的所有应用进程,所有等待的应用进程将会得到同一份数据)。 上面红色字体的语句保存了一个应用程序select一个fd的硬件设备时候的最全的信息,方便在设备就绪的时候容易得到对应的数据。这里的entry->key值就是为了防止第二节中描述的误唤醒而准备的。设置这个key值的地方在函数do_select()中。如下: if (file) { f_op = file->f_op; mask = DEFAULT_POLLMASK; if (f_op && f_op->poll) { wait_key_set(wait, in, out, bit); // 见第二节 mask = (*f_op->poll)(file, wait); } }
fop->poll()函数的返回值都是有规定的,例如函数evdev_poll()中的返回值: return ((client->head == client->tail) ? 0 : (POLLIN | POLLRDNORM)) | (evdev->exist ? 0 : (POLLHUP | POLLERR)); 会根据驱动程序中特定的buffer队列标志,来返回设备状态。这里的判断条件是读循环buffer的头尾指针是否相等:client->head == client->tail。
3.2、poll_wait()函数在select()睡眠前后调用的差异 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); } 这里有一个if条件判断,如果驱动程序中没有提供等待队列头wait_address,那么将不会往下执行p->qproc(__pollwait()),也就是不会将代表当前应用进程的等待队列项添加进驱动程序中对应的等待队列头中。也就是说,如果应用程序恰好用select来监测这个fd的这个等待队列头对应的事件时,是永远也得不到这个设备的就绪或者错误状态的。 如果select()中调用fop->poll()时传递进来的poll_table是NULL,通常情况下,只要在应用层传递进来的超时时间结构体值不为0,哪怕这个结构体指针你传递NULL,那么在函数do_select()中第一次睡眠之前的那次所有fd的大循环中调用fop->poll()函数传递的poll_table是绝对不会为NULL的,但是第一次睡眠唤醒之后的又一次所有fd的大循环中再次调用fop->poll()函数时,此时传递的poll_table是NULL,可想而知,这一次只是检查fop->poll()的返回状态值而已。最后如果从上层调用select时传递的超时值结构体赋值成0,那么do_select()函数的只会调用一次所有fd的大循环,之后不再进入睡眠,直接返回0给上层,基本上这种情况是没有得到任何有用的状态。 为了避免应用进程被唤醒之后再次调用pollwait()的时候重复地调用函数__pollwait(),那么在传递poll_table结构体指针的时候,在睡眠之前保证其为有效地址,而在唤醒之后保证传入的poll_table地址是NULL,因为在唤醒之后,再次调用fop->poll()的作用只是为了再次检查设备的事件状态而已。具体详见代码。
3.3、唤醒应用进程 第二节中已经讨论过驱动程序唤醒进程的一点注意项,但这里再次介绍睡眠唤醒的整个流程。 睡眠是调用函数poll_schedule_timeout()来实现: int poll_schedule_timeout(struct poll_wqueues *pwq, int state, ktime_t *expires, unsigned long slack) { int rc = -EINTR;
set_current_state(state); if (!pwq->triggered) // 这个triggered在什么时候被置1的呢?只要有一个fd // 对应的设备将当前应用进程唤醒后将会把它设置成1 rc = schedule_hrtimeout_range(expires, slack, HRTIMER_MODE_ABS); __set_current_state(TASK_RUNNING);
set_mb(pwq->triggered, 0); return rc; } 唤醒的话会调用函数pollwake(): static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key) { struct poll_table_entry *entry;
entry = container_of(wait, struct poll_table_entry, wait); if (key && !((unsigned long)key & entry->key)) return 0; return __pollwake(wait, mode, sync, key); } static int __pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key) { struct poll_wqueues *pwq = wait->private; DECLARE_WAITQUEUE(dummy_wait, pwq->polling_task);
/* * Although this function is called under waitqueue lock, LOCK * doesn't imply write barrier and the users expect write * barrier semantics on wakeup functions. The following * smp_wmb() is equivalent to smp_wmb() in try_to_wake_up() * and is paired with set_mb() in poll_schedule_timeout. */ smp_wmb(); pwq->triggered = 1; // select()用户进程只要有被唤醒过,就不可能再次进入睡眠,因为这个标志在睡眠的时候有用
return default_wake_function(&dummy_wait, mode, sync, key); // 默认通用的唤醒函数 }
参考网址: 1. http://blogold./u2/60011/showart_1334783.html http://yuanbor.blog.163.com/blog/static/56674620201051134748647/ http://www.cnblogs.com/hanyan225/archive/2010/10/13/1850497.html http://hi.baidu.com/operationsystem/blog/item/208eab9821da8f0e6f068cea.html 2. fs/select.c drivers/input/evdev.c include/linux/poll.h include/linux/wait.h kernel/wait.c ------------------------------------------- |
|