select、pselect、poll和epoll函数是unix中具有I/O复用的函数。什么是I/O复用?为什么要有I/O复用?以及在什么场合下使用I/O复用?既然都具有I/O复用的功能,那这几个函数又有什么样的区别与联系呢?在下面我会一一解释。
请看下面一段客户端代码:
1 #include “unp.h” 2 3 int main(int argc, char **argv) 4 { 5 int listenfd, connfd; 6 pid_t childpid; 7 socklen_t clilen; 8 struct sockaddr_in cliaddr, servaddr; 9 void sig_chld(int); 10 11 listenfd = Socket(AF_INET, SOCK_STREAM, 0); 12 13 bzero(&servaddr, sizeof(servaddr)); 14 servaddr.sin_family = AF_INET; 15 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 16 servaddr.sin_port = htons(SERV_PORT); 17 18 Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); 19 20 Listen(listenfd, LISTENQ); 21 22 Signal(SIGCHLD, sig_chld); /* must call waitpid() */ 23 24 for ( ; ; ) 25 { 26 clilen = sizeof(cliaddr); 27 if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) 28 { 29 if (errno == EINTR) 30 continue; /* back to for() */ 31 else 32 err_sys(“accept error”); 33 } 34 35 if ( (childpid = Fork()) == 0) 36 { /* child process */ 37 Close(listenfd); /* close listening socket */ 38 str_echo(connfd); /* process the request */ 39 exit(0); 40 } 41 42 Close(connfd); /* parent closes connected socket */ 43 } 44 }
从以上代码,我们可以知道,这个TCP客户端在同时处理两个输入:标准输入和TCP套接口。现在假设我们遇到了以下问题:
客户阻塞于(标准输入上的)fgets调用期间,服务器进程会被杀死。服务器TCP虽然正确地给客户TCP发送一个FIN,但是既然客户进程阻塞于从标准输入读入的过程,它直到从套接口读时才会看到这个文件结束符(可能已经过了很长时间)。我们需要有这样的能力:如果一个或者多个I/O条件满足(例如:输入已准备好,或者描述字可以承接更多的输出)时,我们就被通知到。这个能力称为I/O复用,是由函数select、poll、epoll支持的。
一般输入操作有两个不同的阶段:
1. 等待数据准备好;
2. 从内核到进程拷贝数据。
1. select函数
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
- maxfdp1:表示要被测试的最大描述字加1(从参数maxfdp1命名我们可以略知一二);
- readset、writeset、exceptset: 指定我们要让内核测试读、写和异常条件的描述字;
那么在设计上有一个问题,如何为这三个参数的每一个指定一个或多个描述字值。函数select使用描述字集,它一般是一个整数数组,每个整数中的每一位对应一个描述字。例如,用32位整数数组,那么数组的第一个元素的第一个元素对应于描述字0到31,数组的第二个元素对应于描述字32到63,以此类推。
每一个描述字是如何与fd_set集合中的每一个一一对应的呢?就是用到了下面的四个与select函数相关的宏:
FD_ZERO(fd_set *fdset):清空fdset与所有描述字的联系。
FD_SET(int fd, fd_set *fdset):建立描述字fd与fdset的联系。
FD_CLR(int fd, fd_set *fdset):清除描述字fd与fdset的联系。
FD_ISSET(int fd, fd_set *fdset):检查fdset联系的描述字fd是否可读写,当>0表示可读写。
- timeout: 它告诉内核花多长时间等待一组指定的描述字中的任一个准备好。
有三种可能:
a. 永远等待下去:仅在有一个描述字准备好I/O时才返回,为此,我们将参数timeout设置为空指针;
b. 等待固定时间:在有一个描述字准备好I/O时返回,但不超过由timeout参数所指定的时间。若超过时间则直接返回;
c. 根本不等待:检查描述字后立即返回,这称为轮询(polling)。为了实现这一点,参数timeout必须指向结构timeval,且定时器的值(由结构timeval指定的秒数和微秒数)必须为0
在前两者情况的等待中,如果进程捕获了一个信号并从信号处理程序返回,那么等待一般被中断。
中间三个参数readset,wirteset和exceptset指定我们要让内核测试读写和异常条件所需的描述字,参数maxfdp1说明了被测试的描述符的个数,它的值是要被测试的最大的描述符加1.