I/O复用之select函数:
select函数:该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
在红帽linux下用manpage看select,select给我们的形式如下:
int select{int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout};
返回值:就绪描述符的数目,超时返回0,出错返回-1。
(1)第一个参数nfds指定测试的描述字个数,它的值是待测试的最大描述字加1。
(2)中间的三个参数readfds,writefds和exceptfds指定我们要内核测试读,写和异常的条件的描述字。如果对于其中的一个不感兴趣,就可以把它设置为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可以通过四个宏来进行设置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd,fd_set *fdset); //将一个给定的文件描述符加入集合中
void FD_CLR(int fd,fd_set *fdset); //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd,fd_set *fdset); //检查集合中指定的文件描述符是否可以写
目前支持的异常条件只有两个:
1.某个套接字的待外数据的到达;
2.某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息。
(3) 其中timeval是一个结构体,用于指定这段时间的秒数和微妙数,manpage中显示如下:
struct timeval{
long tv_sec; /*seconds*/
long tv_usec; /*microseconds*/
}
and
struct timespec{
long tv_sec; /*seconds*/
long tv_nsec; /*nanoseconds*/
}
timeval这个参素有三种可能:
1.永远等待下去。仅在有一个描述符准备好I/O时才返回,为此,我们把该参数设置为空指针,即timeout == NULL。也就是说,如果捕获到一个信号则中断此无限期等待,当所指定的描述符中的一个已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR。
2.等待固定的时间。在有一个描述字准备好I/O时返回,但不超过timeout参数所指的timeval结构中所指定的秒数和微妙数。即timeout→tv_sec != 0 || timeout→tv_usec != 0。也就是说,当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回。如果在超市到期时还没有一个描述符准备好,则返回值是0。与第一种情况一样,这种等待可能被捕捉到的信号中断。
3.根本不等待。检查描述字后立即返回,这称为轮询。为了实现这一点,参数timeout必须指向结构timeval,且定时器的值(由结构timeval指定的秒数和微秒数)必须为0。即timeout→tv_sec == 0 && timeout→tv_usec == 0。测试所有指定的描述符并立即返回。这是轮询系统找到多个描述符状态而不阻塞select函数的方法。
在前两者情况的等待中,如果进程捕获了一个信号并从信号处理程序返回,那么等待一般是被中断的。
虽然结构体timeval为我们指定了一个微秒级的分辨率,但内核支持的分辨率却要粗糙的多。参数timeout前的限定词const表示它返回时不会被select修改。
select函数的调用过程:
1.使用copy_from_user从用户空间拷贝fd_set到内核空间
2.注册回调函数_pollwait
3.遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用tcp_poll,udp_poll或者datagram_poll)
4.以tcp为例,其核心实现就是_pollwait,也就是上面注册的回调函数。
5._pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备由不同的等待队列,对于tcp_poll来说,其等待队列是sk→sk_sleep,(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设别接受一条消息(网络设备)或填写文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这是current就被唤醒了
6.poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
7.如果遍历完所有的fd,还没有返回一个可以读写的mask掩码,则会调用schedule_timeout,schedule_timeout是调用select进程(也就是current)进入睡眠,当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程,如果超过一定的超时时间(schedule_timeout指定),还是没有人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
8.把fd_set从内核空间拷贝到用户空间。
select的几大缺点:
1.每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
2.同时每次使用select都需要在内核遍历传递进来的所有的fd,这个开销在fd很多时也很大。
3.select支持的文件描述符数量态小,默认为1024,这里所说的小仅仅指的时select与poll和epoll相比,
在限定描述符的数量上,1024个描述符其实已经比较大了,但是epoll不限制描述符的数量。
select的三个可能的返回值:
1.返回-1表示出错,这个是可能发生的,例如,在所指定的描述符一个都没有准备好是捕捉到一个信号。在此种情况下,一个描述符集都不修改。
2.返回值为0表示没有描述符准备好,若指定的描述符一个都没有准备好,指定的时间就过了,那么就会发生这种情况,此时,所有的描述符集都置为0.
3.一个正返回值说明了已经准备好的的描述符数。该值是3个描述符集中已准备好的描述符数之和,所以如果同一描述符已经准备好读和写,那么在返回值中会对其计数两次。
准备好的描述符的意思为:
1.若对读集中的一个描述符进行的read操作不会阻塞,则认为此描述符是准备好的。
2.若对写集中的一个描述符进行的write操作不会阻塞,则认为此描述符是准备好的。
3.若对异常条件集中的一个描述符有一个未决异常条件,则认为此描述符是准备好的。现在,异常条件包括:在网络连接上到达带外的数据,或者在处于数据包模式的伪终端上发生了某些条件。
4.对于读,写和异常条件,普通文件的文件描述符总是返回准备好。
一个描述符阻塞是否并不影响select是否阻塞,理解这一点很重要。
以下内容为借鉴网上的文章:
http://linux.chinaunix.net/techdoc/net/2009/05/03/1109887.shtml
select需要驱动程序的支持,驱动程序实现fops内的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源 可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。
下面我们分两个过程来分析select:
1. select的睡眠过程
支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的BLOCK或NONBLOCK操作。当应用程序通过设 备驱动访问该设备时(默认为BLOCK操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠一段时间,等 到有数据可读/写时再将该进程唤醒。
select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。下面我们看看select睡眠的详细过程。
select会循环遍历它所监测的fd_set(一组文件描述符(fd)的集合)内的所有文件描述符对应的驱动程序的poll函数。驱动程序提供的 poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select 当前资源哪些可用。当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用(即没有一个文件可供操 作),则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout)继续往下执行。