IO多路复用——poll
1、基本知识
poll是Linux中的字符设备驱动中的一个函数。Linux 2.5.44版本后,poll被epoll取代。和select实现的功能差不多,poll的作用是把当前的文件指针挂到等待队列。
poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
poll优点
(1)poll() 不要求开发者计算最大文件描述符加一的大小。
(2)poll() 在应付大数目的文件描述符的时候速度更快,相比于select。
(3)它没有最大连接数的限制,原因是它是基于链表来存储的。
(4)在调用函数时,只需要对参数进行一次设置就好了
poll缺点
(1)大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
(2)与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符,这样会使性能下降
(3)同时连接的大量客户端在一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降
elect优点:
目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点
select缺点:
(1)每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大,同时每次调用 select() 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大。
(2)单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低
(3)select函数在每次调用之前都要对参数进行重新设定,这样做比较麻烦,而且会降低性能
2、poll函数
函数格式如下所示:
# include <poll.h> int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
pollfd结构体定义如下:
struct pollfd { int fd; /* 文件描述符 */ short events; /* 等待的事件 */ short revents; /* 实际发生了的事件 */ } ;
每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。合法的事件如下:
POLLIN 有数据可读。
POLLRDNORM 有普通数据可读。
POLLRDBAND 有优先数据可读。
POLLPRI 有紧迫数据可读。
POLLOUT 写数据不会导致阻塞。
POLLWRNORM 写普通数据不会导致阻塞。
POLLWRBAND 写优先数据不会导致阻塞。
POLLMSGSIGPOLL 消息可用。
此外,revents域中还可能返回下列事件:
POLLER 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起事件。
POLLNVAL 指定的文件描述符非法。
这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。
使用poll()和select()不一样,你不需要显式地请求异常情况报告。
POLLIN | POLLPRI等价于select()的读事件,POLLOUT |POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。
timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
返回值和错误代码
成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
EBADF 一个或多个结构体中指定的文件描述符无效。
EFAULTfds 指针指向的地址超出进程的地址空间。
EINTR 请求的事件之前产生一个信号,调用可以重新发起。
EINVALnfds 参数超出PLIMIT_NOFILE值。
ENOMEM 可用内存不足,无法完成请求。
3、测出程序
编写一个echo server程序,功能是客户端向服务器发送信息,服务器接收输出并原样发送回给客户端,客户端接收到输出到终端。
服务器端程序如下:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <netinet/in.h> #include <sys/socket.h> #include <poll.h> #include <unistd.h> #include <sys/types.h> #define IPADDRESS "127.0.0.1" #define PORT 8787 #define MAXLINE 1024 #define LISTENQ 5 #define OPEN_MAX 1000 #define INFTIM -1 //函数声明 //创建套接字并进行绑定 static int socket_bind(const char* ip,int port); //IO多路复用poll static void do_poll(int listenfd); //处理多个连接 static void handle_connection(struct pollfd *connfds,int num); int main(int argc,char *argv[]) { int listenfd,connfd,sockfd; struct sockaddr_in cliaddr; socklen_t cliaddrlen; listenfd = socket_bind(IPADDRESS,PORT); listen(listenfd,LISTENQ); do_poll(listenfd); return 0; } static int socket_bind(const char* ip,int port) { int listenfd; struct sockaddr_in servaddr; listenfd = socket(AF_INET,SOCK_STREAM,0); if (listenfd == -1) { perror("socket error:"); exit(1); } bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET,ip,&servaddr.sin_addr); servaddr.sin_port = htons(port); if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1) { perror("bind error: "); exit(1); } return listenfd; } static void do_poll(int listenfd) { int connfd,sockfd; struct sockaddr_in cliaddr; socklen_t cliaddrlen; struct pollfd clientfds[OPEN_MAX]; int maxi; int i; int nready; //添加监听描述符 clientfds[0].fd = listenfd; clientfds[0].events = POLLIN; //初始化客户连接描述符 for (i = 1;i < OPEN_MAX;i++) clientfds[i].fd = -1; maxi = 0; //循环处理 for ( ; ; ) { //获取可用描述符的个数 nready = poll(clientfds,maxi+1,INFTIM); if (nready == -1) { perror("poll error:"); exit(1); } //测试监听描述符是否准备好 if (clientfds[0].revents & POLLIN) { cliaddrlen = sizeof(cliaddr); //接受新的连接 if ((connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen)) == -1) { if (errno == EINTR) continue; else { perror("accept error:"); exit(1); } } fprintf(stdout,"accept a new client: %s:%d\n", inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port); //将新的连接描述符添加到数组中 for (i = 1;i < OPEN_MAX;i++) { if (clientfds[i].fd < 0) { clientfds[i].fd = connfd; break; } } if (i == OPEN_MAX) { fprintf(stderr,"too many clients.\n"); exit(1); } //将新的描述符添加到读描述符集合中 clientfds[i].events = POLLIN; //记录客户连接套接字的个数 maxi = (i > maxi ? i : maxi); if (--nready <= 0) continue; } //处理客户连接 handle_connection(clientfds,maxi); } } static void handle_connection(struct pollfd *connfds,int num) { int i,n; char buf[MAXLINE]; memset(buf,0,MAXLINE); for (i = 1;i <= num;i++) { if (connfds[i].fd < 0) continue; //测试客户描述符是否准备好 if (connfds[i].revents & POLLIN) { //接收客户端发送的信息 n = read(connfds[i].fd,buf,MAXLINE); if (n == 0) { close(connfds[i].fd); connfds[i].fd = -1; continue; } // printf("read msg is: "); write(STDOUT_FILENO,buf,n); //向客户端发送buf write(connfds[i].fd,buf,n); } } }
客户端代码如下所示:
#include <netinet/in.h> #include <sys/socket.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <poll.h> #include <time.h> #include <unistd.h> #include <sys/types.h> #define MAXLINE 1024 #define IPADDRESS "127.0.0.1" #define SERV_PORT 8787 #define max(a,b) (a > b) ? a : b static void handle_connection(int sockfd); int main(int argc,char *argv[]) { int sockfd; struct sockaddr_in servaddr; sockfd = socket(AF_INET,SOCK_STREAM,0); bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr); connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr)); //处理连接描述符 handle_connection(sockfd); return 0; } static void handle_connection(int sockfd) { char sendline[MAXLINE],recvline[MAXLINE]; int maxfdp,stdineof; struct pollfd pfds[2]; int n; //添加连接描述符 pfds[0].fd = sockfd; pfds[0].events = POLLIN; //添加标准输入描述符 pfds[1].fd = STDIN_FILENO; pfds[1].events = POLLIN; for (; ;) { poll(pfds,2,-1); if (pfds[0].revents & POLLIN) { n = read(sockfd,recvline,MAXLINE); if (n == 0) { fprintf(stderr,"client: server is closed.\n"); close(sockfd); } write(STDOUT_FILENO,recvline,n); } //测试标准输入是否准备好 if (pfds[1].revents & POLLIN) { n = read(STDIN_FILENO,sendline,MAXLINE); if (n == 0) { shutdown(sockfd,SHUT_WR); continue; } write(sockfd,sendline,n); } } }
4、程序测试结果