最近突然顿悟,很有必要养成写博文的习惯。其一,相关知识忘记了可以自己查看复习,而不必到处去百度找半天,其二,做IT必须有所积累,不断巩固知识,才可以提高工作效率。
本篇文章通过select应用层调用和select驱动实现两个部分来分析如何使用select机制。
一、应用层如何使用select函数
int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout)
功能说明:
用来监视多个文件描述符的状态变化。当程序运行到这个函数,如果timeout=0不会阻塞继续往下执行,如果timeout>0则程序阻塞,这时跳出阻塞有两个办法:1、超时后跳出 2、在timeout时间内检测到文件描述符有一个或者多个发生状态变化时立即跳出。
如果在timeout指定时间内,需要监视的描述符没有事件发生则函数超时返回0; 如果失败,则返回-1,错误原因存于 errno;需要监视的描述符有事件发生则函数返回1。
参数说明:
maxfd:需要监视的最大的文件描述符值+1。
rdset: 需要检测的可读文件描述符的集合。
wrset: 需要检测的可写文件描述符的集合。
exset: 需要检测的异常文件描述符的集合。
timeout:超时时间,其类型是struct timeval *,即一个struct timeval结构的变量的指针,所以我们在程序里要申明一个struct timeval rto;然后把变量rto的地址&rto传递给select函数。struct timeval结构如下:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
fd_set是一组文件描述字(fd)的集合,它用一位来表示一个fd,对于fd_set类型通过下面四个宏来操作:
FD_ZERO(fd_set *fdset)
功能说明:将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。
FD_SET(int fd,fd_set *fdset)
功能说明:用于在文件描述符集合中增加一个新的文件描述符。
FD_CLR(int fd,fd_set *fdset)
功能说明:用于在文件描述符集合中删除一个文件描述符。
FD_ISSET(int fd,fd_set *fdset)
功能说明:检查fd_set联系的文件描述符fd是否可读写,>0表示可读写。
下面给出一个简单的实例:
int main() { int ret, socketfd_connected, msg_recv_len; struct timeval rto; fd_set socket_read_fds; char msg_recv[1024]; while(1) { /*每次循环都要重新赋值,不能放在大循环外面。可能有人会问,rto是一个全局变量 ,在大循环前对它赋值一次不就可以了吗?这个估计是因为调用select函数会对它清零 ,所以每次循环都要重新赋值*/ rto.tv_sec = 5; rto.tv_usec = 0; //超时5秒 //清空文件描述符集socket_read_fds FD_ZERO(&socket_read_fds); //把已经建立好连接的socketfd_connected加入到文件描述符集socket_read_fds FD_SET(socketfd_connected,&socket_read_fds); /*进入超时等待消息,如果监测多个文件描述符,则只要检测到其中任何一个描述符可 以读写,该函数都会立刻跳出*/ ret = select(socketfd_connected+1,&socket_read_fds, NULL, NULL, &rto); if(ret == -1)//错误 { if(EINTR == errno) continue; return 1; } if(ret == 0) // select 超时 { continue; } //判断socketfd_connected是否可读,如果可读,便调用recv进行读取 if(FD_ISSET(socketfd_connected,&socket_read_fds)) msg_recv_len = recv(socketfd_connected, msg_recv, 1024, 0); } }
到此select函数的应用层调用介绍完了。
二、select函数的驱动实现
用户空间函数对应的内核空间函数
用户空间 |
内核空间(驱动) |
open |
open |
close |
release |
read |
read |
write |
write |
ioctl |
ioctl |
lseek |
lseek |
select |
poll |
我们在驱动中实现poll函数,只需要做两件事:
1. 使用poll_wait()将等待队列添加到poll_table中。
2. 返回描述设备是否可读或可写的掩码。
这里给出本人所写的一个简单的实例:
static unsigned int button_poll(struct file *file, struct poll_table_struct *wait) { unsigned int mask = 0; //添加等待队列到等待队列表中(poll_table) poll_wait(file, &button_waitq_poll, wait); if(ev_press_poll) { ev_press_poll = 0; //标识数据可以获得 mask |= POLLIN | POLLRDNORM; } return mask; }
注意:该mask是返回给do_select函数的,不是返回给用户空间的。(do_select是用户空间中select函数的底层调用,后面会分析这个函数)。mask到底有哪些值呢?请看下表:
标志 |
含义 |
POLLIN |
如果设备无阻塞的读,返回该值 |
POLLRDNORM |
数据已经准备好了,可以读了,就返回该值。通常的做法是返回POLLIN | POLLRDNORM |
POLLRDBAND |
如果可以从设备读出外带数据,就返回该值。它只可以在Linux内核的某些网络代码中使用,通常不用在设备驱动程序中。 |
POLLPRI |
如果可以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select把带外数据当做异常处理。 |
POLLHUP |
当读设备的进程到达文件尾时,驱动程序必须返回该值,依照select的功能描述,调用select的进程被告知进程是可读的。 |
POLLERR |
如果设备发生错误就返回该值 |
POLLOUT |
如果设备可以无阻塞的写 ,就返回该值。 |
POLLWRNORM |
设备以及准备好了,可以写了,就返回该值。通常的做法是返回POLLOUT|POLLWRNORM |
POLLWRBAND |
与POLLRDBAND类似(一个读,一个写啦) |
可以看出,驱动程序中实现的poll函数并没有阻塞,那应用层调用select函数时,是怎么发生阻塞的呢?还有,当调用select函数时,驱动程序中的poll函数是怎么被调用的呢?问题全在do_select函数。do_select函数是select系统调用所对应的内核函数,它完成了select函数的功能。打开内核代码,找到do_select函数,通读之,可以发现:
do_select中有个for(;;)大循环,里面还有一个长这样子for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1)的循环,这个循环会把文件描述符集轮询一边,调用各个文件描述符对应驱动中的poll函数,并通过poll函数返回的mask的值判断对应的文件描述符是否可读或者可写,如果否,则调用函数poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,to, slack)将进程阻塞,接下来等待驱动空间将对应的等待队列唤醒(若唤醒,说明了某个文件描述符的状态改变了),唤醒后for(;;)大循环又执行一次,也就是for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1)循环又执行一次,这时至少有一个文件描述符的mask是可以或者可写的,那么使用break跳出循环,也就跳出select函数的阻塞了。若在timeout时间内文件描述符集都没有状态改变,则超时,也使用break跳出循环,跳出select函数的阻塞。
到这里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); wait = &table.pt; 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; 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) { wait_key_set(wait, in, out, bit); mask = (*f_op->poll)(file, wait); } fput_light(file, fput_needed); if ((mask & POLLIN_SET) && (in & bit)) { res_in |= bit; retval++; wait = NULL; } if ((mask & POLLOUT_SET) && (out & bit)) { res_out |= bit; retval++; wait = NULL; } if ((mask & POLLEX_SET) && (ex & bit)) { res_ex |= bit; retval++; wait = NULL; } } } 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 (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, to, slack)) timed_out = 1; } poll_freewait(&table); return retval; }