Linux操作系统下的poll和select
select()函数的作用
系统调用select和poll的后端实现,用这两个系统调用来查询设备是否可读写,或是否处于某种状态。如果poll为空,则驱动设备会被认为即可读又可写,返回值是一个状态掩码 如何使用select()函数? select()函数的接口主要是建立在一种叫'fd_set'类型的基础上。它('fd_set') 是一组文件描述符(fd)的集合。由于fd_set类型的长度在不同平台上不同,因此应该用一组标准的宏定义来处理此类变量:- fd_set set;
- FD_ZERO(&set); /* 将set清零 */
- FD_SET(fd, &set);/* 将fd加入set */
- FD_CLR(fd, &set);/* 将fd从set中清除 */
- FD_ISSET(fd, &set); /* 如果fd在set中则真 */
在过去,一个fd_set通常只能包含少于等于32个文件描述符,因为fd_set其实只用了一个int的比特矢量来实现,在大多数情况下,检查 fd_set能包括任意值的文件描述符是系统的责任,但确定你的fd_set到底能放多少有时你应该检查/修改宏FD_SETSIZE的值。*这个值是系统相关的*,同时检查你的系统中的select() 的man手册。有一些系统对多于1024个文件描述符的支持有问题。[译者注: Linux操作系统就是这样的系统!你会发现sizeof(fd_set)的结果是128(*8 = FD_SETSIZE=1024) 尽管很少你会遇到这种情况。]
select的基本接口十分简单:
- int select(int nfds, fd_set *readset, fd_set *writeset,
- fd_set *exceptset, struct timeval *timeout);
其中:
nfds
需要检查的文件描述符个数,数值应该比是三组fd_set中最大数 更大,而不是实际文件描述符的总数。 readset 用来检查可读性的一组文件描述符。 writeset 用来检查可写性的一组文件描述符。 exceptset 用来检查意外状态的文件描述符。(注:错误并不是意外状态) timeout NULL指针代表无限等待,否则是指向timeval结构的指针,代表最 长等待时间。(如果其中tv_sec和tv_usec都等于0, 则文件描述符 的状态不被影响,但函数并不挂起) 函数将返回响应操作的对应操作文件描述符的总数,且三组数据均在恰当位置被修改,只有响应操作的那一些没有修改。接着应该用FD_ISSET宏来查找返回的文件描述符组。这里是一个简单的测试单个文件描述符可读性的例子:
- int isready(int fd)
- {
- int rc;
- fd_set fds;
- struct timeval tv;
- FD_ZERO(&fds);
- FD_SET(fd,&fds);
- // tvtv.tv_sec = tv.tv_usec = 0;
- //rc = select(fd+1, &fds, NULL, NULL, &tv);
- rc = select(fd+1, &fds, NULL, NULL, NULL);
- if (rc < 0)
- return -1;
- return FD_ISSET(fd,&fds) ? 1 : 0;
- }
当然如果我们把NULL指针作为fd_set传入的话,这就表示我们对这种操作的发生不感兴趣,但select() 还是会等待直到其发生或者超过等待时间。
[译者注:在Linux操作系统中,timeout指的是程序在非sleep状态中度过的时间,而不是实际上过去的时间,这就会引起和非Linux操作系统平台移植上的时间不等问题。移植问题还包括在System V风格中select()在函数退出前会把timeout设为未定义的 NULL状态,而在BSD中则不是这样, Linux操作系统在这点上遵从System V,因此在重复利用timeout指针问题上也应该注意。]
Linux操作系统下select调用的过程:
1.用户层应用程序调用select(),底层调用poll()) 2.核心层调用sys_select() ------> do_select() 最终调用文件描述符fd对应的struct file类型变量的struct file_operations *f_op的poll函数。 poll指向的函数返回当前可否读写的信息。 1)如果当前可读写,返回读写信息。 2)如果当前不可读写,则阻塞进程,并等待驱动程序唤醒,重新调用poll函数,或超时返回。 3.驱动需要实现poll函数。 当驱动发现有数据可以读写时,通知核心层,核心层重新调用poll指向的函数查询信息。 poll_wait(filp,&wait_q,wait) // 此处将当前进程加入到等待队列中,但并不阻塞 在中断中使用wake_up_interruptible(&wait_q)唤醒等待队列**************************************************************************
static int buttons_poll(struct file *file, struct poll_table_struct *wait)
{ unsigned int mask = 0;//添加等待队列到等待队列表中(poll_table)
poll_wait(file, &button_waitq, wait);if(ev_press)
{ //标识数据可以获得 mask |= POLLIN | POLLRDNORM; }return mask;
}简单应用程序例子
#include <stdio.h>
#include <stdlib.h> #include <errno.h>int main(int argc, char **argv)
{ int fd; int key_status[4];//以阻塞方式打开设备文件,非阻塞时flags=O_NONBLOCK
fd = open("/dev/my2440_buttons", 0);if(fd < 0)
{ printf("Open Buttons Device Faild!\n"); exit(1); }while(1)
{ int i; int ret; fd_set rds; FD_ZERO(&rds); FD_SET(fd, &rds); //应用程序进行轮询,查询是否可对设备进行访问 ret = select(fd + 1, &rds, NULL, NULL, NULL); if(ret < 0) { printf("Read Buttons Device Faild!\n"); exit(1); } if(ret == 0) { printf("Read Buttons Device Timeout!\n"); } else if(FD_ISSET(fd, &rds)) { //读设备 ret = read(fd, key_status, sizeof(key_status));if(ret != sizeof(key_status))
{ if(errno != EAGAIN) { printf("Read Button Device Faild!\n"); }continue;
} else { for(i = 0; i < 6; i++) { //对应驱动中按键的状态,为0即按键被按下 if(key_status[i] == 0) { printf("Key%d DOWN\n", i + 1); } } } } }close(fd);
return 0;
}
***********************************************************************
在Linux驱动程序中,我们可以使用等待队列(wait queue)来实现阻塞操作。wait queue很早就作为一个基本的功能单位出现在Linux内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。等待队列可以用来同步对系统资源的访问,上节中所讲述Linux信号量在内核中也是由等待队列来实现的。
下面我们以"globalfifo"为例,来说明linux下poll/select及阻塞与非阻塞操作的实现过程."globalfifo"可以被多个进程打开,但是每次只有当一个进程写入了一个数据之后本进程或其它进程才可以读取该数据,否则一直阻塞。 select和 poll的本质一样,前者在BSD Unix中引入,后者在System V中引入。poll和select用于查询设备的状态,以便用户程序获知是否能对设备进行非阻塞的访问,它们都需要设备驱动程序中的poll函数支持。 驱动程序中poll函数中最主要用到的一个API是poll_wait,其原型如下: void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait); poll_wait函数所做的工作是把当前进程添加到wait参数指定的等待列表(poll_table)中。 下面我们给出globalfifo的poll函数: static unsigned int globalfifo_poll(struct file *filp, poll_table *wait) { unsigned int mask = 0; struct globalfifo_dev *dev = filp->private_data; /*获得设备结构体指针*/ down(&dev->sem); //添加等待队列dev->r_wait/dev->w_wait到poll_table wait所在的结构体的poll_table_entry中,然后添加等待队列项(指向当前进程)到等待队列头dev->r_wait/dev->w_wait中 poll_wait(filp, &dev->r_wait, wait); poll_wait(filp, &dev->w_wait, wait); /*fifo非空*/ if (dev->current_len != 0) { mask |= POLLIN | POLLRDNORM; /*标示数据可获得*/ } /*fifo非满*/ if (dev->current_len != GLOBALFIFO_SIZE) { mask |= POLLOUT | POLLWRNORM; /*标示数据可写入*/ } up(&dev->sem); return mask; }
poll_wait内核实现代码如下:
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;//添加wait_queue_head_t到entry init_waitqueue_entry(&entry->wait, current); add_wait_queue(wait_address, &entry->wait);//添加等待队列项到等待队列头 } poll_wait函数并不阻塞,真正的阻塞动作是上层的select/poll函数中完成的。 select/poll会在 一个循环中对每个需要监听的设备调用它们自己的poll支持函数以使得当前进程被加入各个设备的等待列表。若当前没有任何被监听的设备就绪,则内核进行调度(调用schedule)让出cpu进入阻塞状态,schedule返回时将再次循环检测是否有操作可以进行,如此反复;否则,若有任意一个设备就绪, select/poll都立即返回。以select(2.6.28内核)为例代码如下: 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) { if (copy_from_user(&tv, tvp, sizeof(tv))) return -EFAULT; to = &end_time; 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); return ret; } poll/select内核调用过程如下:select()->core_sys_select()->do_select():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(); retval = max_select_fd(n, fds); rcu_read_unlock(); if (retval < 0) return retval; n = retval; poll_initwait(&table);//初始化结构体,主要是初始化poll_wait的回调函数为__pollwait wait = &; if (end_time && !end_time->tv_sec && !end_time->tv_nsec) { wait = NULL; timed_out = 1; } if (end_time && !timed_out) slack = estimate_accuracy(end_time); retval = 0; for (;;) { unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp; set_current_state(TASK_INTERRUPTIBLE);//设置当前进程状态为TASK_INTERRUPTIBLE inp = fds->in; outp = fds->out; exp = fds->ex; rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex; 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; in = *inp++; out = *outp++; ex = *exp++; all_bits = in | out | ex; if (all_bits == 0) { i += __NFDBITS; continue; } for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) { int fput_needed; if (i >= n) break; if (!(bit & all_bits)) continue; file = fget_light(i, &fput_needed); if (file) { f_op = file->f_op; mask = DEFAULT_POLLMASK; if (f_op && f_op->poll) mask = (*f_op->poll)(file, retval ? NULL : wait);//调用poll_wait处理过程,即把驱动中等待队列头增加到poll_wqueues中的entry中,并把指向当前里程的等待队列项增加到等待队列头中。每一个等待队列头占用一个entry fput_light(file, fput_needed); if ((mask & POLLIN_SET) && (in & bit)) { res_in |= bit; retval++; } if ((mask & POLLOUT_SET) && (out & bit)) { res_out |= bit; retval++; } if ((mask & POLLEX_SET) && (ex & bit)) { res_ex |= bit; retval++; } } } if (res_in) *rinp = res_in; if (res_out) *routp = res_out; if (res_ex) *rexp = res_ex; cond_resched();//增加抢占点,调度其它进程,当前里程进入睡眠 } wait = NULL; if (retval || timed_out || signal_pending(current)) break; if (table.error) { retval = table.error; break; } /* * 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; } if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS)) timed_out = 1; } __set_current_state(TASK_RUNNING); poll_freewait(&table);//从等待队列头中删除poll_wait中添加的等待队列,并释放资源 return retval; } 从以上代码可以看出,当一个想从设备读数据的进程调用select,则内核调用poll_wait,把含当前进程的等待队列项添加到设备的等待列队上,然后简单的把当前里程设置为睡眠。直到有其它进程写设备,数据可获取,调用wake up,唤醒当前进程。当前进程继续执行,再次调用poll后,有数据可读返回。 下面再看globalfifo的write过程,源代码如下: /*globalfifo写操作*/ static ssize_t globalfifo_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { struct globalfifo_dev *dev = filp->private_data; //获得设备结构体指针 int ret; DECLARE_WAITQUEUE(wait, current); //定义等待队列 down(&dev->sem); //获取信号量 add_wait_queue(&dev->w_wait, &wait); //进入写等待队列头 /* 等待FIFO非满 */ if (dev->current_len == GLOBALFIFO_SIZE) { if (filp->f_flags &O_NONBLOCK) //如果是非阻塞访问 { ret = - EAGAIN; goto out; } __set_current_state(TASK_INTERRUPTIBLE); //改变进程状态为睡眠 up(&dev->sem); schedule(); //调度其他进程执行 if (signal_pending(current)) //如果是因为信号唤醒 { ret = - ERESTARTSYS; goto out2; } down(&dev->sem); //获得信号量 } /*从用户空间拷贝到内核空间*/ if (count > GLOBALFIFO_SIZE - dev->current_len) count = GLOBALFIFO_SIZE - dev->current_len; if (copy_from_user(dev->mem + dev->current_len, buf, count)) { ret = - EFAULT; goto out; } else { dev->current_len += count; printk(KERN_INFO "written %d bytes(s),current_len:%dn", count, dev ->current_len); wake_up_interruptible(&dev->r_wait); //唤醒读等待队列,睡眠在读队列上的里程被唤醒。 ret = count; } out: up(&dev->sem); //释放信号量 out2:remove_wait_queue(&dev->w_wait, &wait); //从附属的等待队列头移除 set_current_state(TASK_RUNNING); return ret; } 读函数的阻塞机制同上,这里就不再详述。