第六章 IO复用:select和poll函数
1. 概述
I/O复用使用场合:
-
- 当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须要使用I/O复用;
- 一个客户同时处理多个套接字(比较少见);
- 如果一个TCP服务器既要处理监听套接字,又要处理已连接的套接字,一般要使用I/O复用;
- 如果一个服务器要处理多个服务或者多个协议,一般要使用I/O复用
另外I/O复用并非只限于网络编程,还有许多重要程序也会用到这项技术。
2. I/O模型
- 阻塞式I/O模型
- 非阻塞式I/O模型
- I/O复用模型
- 信号驱动I/O模型
- 异步I/O模型
3. select函数
1)函数作用:允许进程指示内核等待多个事件中的一个发生, 并只在有一个或多个事件发生,或通过定时唤醒它。(前面说的同时处理socket描述符和等待用户输入就符合这个情况)(Berkeley的实现允许任何描述符的I/O复用)
2)函数定义
#include <sys/time.h> #include <sys/select.h> int select (int maxfdp1, fd_set *readset, fd_set *writeset, fd_set exceptset, const struct timeval *timeout);
struct timeval
{
long tv_sec;
long tv_usec;
}
参数介绍:
timeout:告知内核等待所指定的描述符中任何一个就绪的时间。其有三种可能:
-
- 永远等下去,此时值设置为NULL;
- 等一段固定的时间。即等待时间不超过所指定的timeout时间值;
- 根本不等待。此时称为轮询,timeout结构总的秒、微妙都设置为0;
exceptset:目前支持的异常条件只有两个
-
- 某个套接字的带外数据到达(24章讨论);
- 某个已置为分组模式的伪终端,存在可以从其主终端读取的控制状态信息(本书不涉及)
readset,writeset:我们要让内核读和写的描述符;
maxfdp1: 指定待测描述符的个数,具体值从0开始到maxfdp1-1(FD_SETSIZE常值为fd_set描述符的总数)。
返回值:若有就绪描述符则为其个数,超时返回0,出错为-1.
select可以作为定时器,此时中间三个描述符集设置为NULL,这个定时器比sleep还有精确,sleep以秒为单位,其以微妙为单位。
3)fd_set类型的相关操作
fd_set rset; //注意新定义变量一定要用FD_ZERO初始化,其自动分配的值不可意料,会导致不可意料的后果。 void FD_ZERO(fd_set *fdset); //initialize the set: all bits off void FD_SET(int fd, fd_set *fdset); //turn on the bit for fd in fdset void FD_CLR(int fd, fd_set *fdset); //turn off the bits for fd in fdset int FD_ISSET(int fd, fd_set *fdset); //is the bit for fd on in fdset
4) 描述符的就绪条件
-
- 有数据可读(读);
- 关闭连接的读一半(TCP收到FIN)(读);
- 给监听套接口准备好新连接(读);
- 有待处理的错误(读写)
- 有可用的写空间(写);
- 关闭连接写一半(写);
- tcp带外数据(异常)
5)select的最大描述符数:不同系统可能不同,使用大描述符集时,要考虑移植性。
6)例子:第五章中说到的同时处理fputs、read的情况
void str_cli_1(FILE *fp, int sockfd) { char sendline[MAXLINE]; char readline[MAXLINE]; fd_set rset; FD_ZERO(&rset); while(1) { FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); int maxfdp1 = MAX(fileno(fp), sockfd) + 1; if( select(maxfdp1, &rset, NULL, NULL, NULL) <=0 ) { printf("%s:%d select error\n", __FUNCTION__, __LINE__); return; } if( FD_ISSET(sockfd, &rset) ) { if( read(sockfd, readline, MAXLINE) == 0) { printf("%s:%d recv data error\n", __FUNCTION__, __LINE__); return; } fputs(readline,stdout); } if( FD_ISSET(fileno(fp), &rset) ) { if( NULL == fgets(sendline, MAXLINE, fp) ) { printf("%s:%d fgets error\n", __FUNCTION__, __LINE__); return; } if( write(sockfd, sendline, strlen(sendline)) != strlen(sendline) ) { printf("%s:%d send data error\n", __FUNCTION__, __LINE__); return ; } } } }
例子中有了select当服务器进程退出后,客户端就不会阻塞于fgets函数,客户端收到FIN后read会马上返回错误。(此例子还有问题)
4. 批量输入
如果客户端持续向服务器发送数据,发送完数据后客户端马上调用close关闭连接。此时客户端可能仍有请求去往服务器的路上,或是仍有服务器的应答在返回客户段的路上。这样如果调用close全关闭就会造成问题。我们需要一个半关闭TCP的一种方法。即下面将说到的shutdown。
混合使用select和stdio比较容易发生错误,要非常小心。
5. shutdown函数
1)close函数的限制
-
- close把描述符的引用计数减一,要到计数为0时才关闭套接字。shutdown可以不管引用计数就激发TCP的正常连接终止。
- close终止读和写两个方向的数据传送。但是有时我们需要告知对方我们已经把数据发送完毕,即使我们还有数据需要接收。
2)shutdown函数定义
#include <sys/socket.h> //返回值:成功0,出错-1 int shutdown(int sockfd, int howto);
howto参数值说明:
-
- SHUT_RD:关闭连接的读这一半,套接字没有数据可以接收了,而且套接字接收缓冲区中现有的数据将被丢弃;
- SHUT_WR:关闭连接的写这一半,对于TCP这叫半关闭,当前留在套接字发送缓冲区的数据将被发送完,紧跟TCP正常的连接终止系列。
- SHUT_RW:关闭整个连接。
6. 修正3中最后面的str_cli_1例子
void str_cli_2(FILE *fp, int sockfd) { char sendline[MAXLINE]; char readline[MAXLINE]; fd_set rset; FD_ZERO(&rset); int stdineof = 0; while(1) { if(stdineof == 0) { FD_SET(fileno(fp), &rset); } FD_SET(sockfd, &rset); int maxfdp1 = MAX(fileno(fp), sockfd) + 1; if( select(maxfdp1, &rset, NULL, NULL, NULL) <=0 ) { printf("%s:%d select error\n", __FUNCTION__, __LINE__); return; } if( FD_ISSET(sockfd, &rset) ) { int rn = 0; if( (rn = read(sockfd, readline, MAXLINE)) == 0) { if(stdineof == 1) { return; } printf("%s:%d recv data error\n", __FUNCTION__, __LINE__); return; } write(fileno(stdout), readline, rn); } if( FD_ISSET(fileno(fp), &rset) ) { int rn = 0; if( (rn = read(fileno(fp), sendline, MAXLINE)) == 0 ) { stdineof = 1; shutdown(sockfd, SHUT_WR); //send fin FD_CLR(fileno(fp), &rset); printf("%s:%d read fileno(fp) error\n", __FUNCTION__, __LINE__); continue; } if( write(sockfd, sendline, rn) != rn ) { printf("%s:%d send data error\n", __FUNCTION__, __LINE__); return ; } } } }
7. 使用select的TCP服务器程序
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <signal.h> #include <sys/socket.h> #include <sys/select.h> #include <arpa/inet.h> #include <netinet/in.h> #define SRV_PORT 8888 #define MAXLINE 4096 void str_echo(int fd); void sig_chld(int signo); int main(int argc, char **argv) { int listenfd = socket(AF_INET, SOCK_STREAM, 0); if(listenfd < 0) { perror("create socket error."); } struct sockaddr_in srvaddr; bzero(&srvaddr, sizeof(srvaddr)); srvaddr.sin_family = AF_INET; srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); srvaddr.sin_port = htons(SRV_PORT); if(bind(listenfd, (struct sockaddr*)&srvaddr, sizeof(srvaddr)) < 0) { perror("bind error."); } if(listen(listenfd, 1023) < 0) { perror("listen error."); } int maxfd = listenfd; int maxi = -1; int client[FD_SETSIZE]; int i=0; for(i=0; i<FD_SETSIZE; i++) { client[i] = -1; } fd_set rset, allset; FD_ZERO(&allset); FD_SET(listenfd, &allset); struct sockaddr_in cliaddr; for(; ;) { rset = allset; int nready = select(maxfd+1, &rset, NULL, NULL, NULL); if(FD_ISSET(listenfd, &rset)) { socklen_t clilen = sizeof(cliaddr); int connfd; if((connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen)) < 0) { if(errno == EINTR) { continue; } else { perror("accept error."); } } for(i=0; i < FD_SETSIZE; i++) { if(client[i] < 0) { client[i] = connfd; break; } } if(i == FD_SETSIZE) { printf("too many connect."); exit(-1); } FD_SET(connfd, &allset); maxfd = maxfd > connfd ? maxfd : connfd; maxi = i > maxi ? i : maxi; if(--nready <= 0) { continue; } } //get client data for(i=0; i <= maxi; ++i) { if(client[i] < 0) { continue; } if(FD_ISSET(client[i], &rset)) { int byteN; char buf[MAXLINE]; if( (byteN = read(client[i], buf, MAXLINE)) == 0) { close(client[i]); FD_CLR(client[i], &allset); client[i] = -1; } else { write(client[i], buf, byteN); if(--nready <= 0) { break; } } } } } return 0; }
8. pselect函数
#include <sys/select.h> int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
此函数比select函数多了timespec和sigmask-不细说了
9. poll函数
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */ //入参
short revents; /* returned events */ //出参
};
说明:与select提供的功能累世,处理流设备时能提供额外的信息。不限定最大描述符数量。