上一篇介绍了select的基本用法,接着来学习一下poll和epoll的基本用法。首先来看poll:
#include <sys/poll.h> int poll (struct pollfd *fds, unsigned int nfds, int timeout);
poll() 采用了struct pollfd 结构数组来保存关心的文件描述符,而不是像select一样使用三个fd_set ,pollfd结构体定义如下:
struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ };
每一个pollfd结构体指定了一个被监视的文件描述符,fds数组中可以存放多个pollfd结构,而且数量不会像select的FD_SETSIZE一样被限制在1024或者2048 。数组中每个pollfd结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,系统调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。我们可以设置如下事件:
POLLIN:有数据可读。
POLLRDNORM:有普通数据可读。
POLLRDBAND:有优先数据可读。
POLLPRI:有紧迫数据可读。
------------------------------------------------------------
POLLOUT:写数据不会导致阻塞。
POLLWRNORM:写普通数据不会导致阻塞。
POLLWRBAND:写优先数据不会导致阻塞。
此外,revents域中还可能返回下列事件:
POLLERR:指定的文件描述符发生错误。
POLLHUP:指定的文件描述符挂起事件。
POLLNVAL:指定的文件描述符非法。
注意:只能作为描述字的返回结果存储在revents中,而不能作为测试条件用于events中。
其中POLLIN | POLLPRI等价于select()的读事件,POLLOUT | POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM | POLLRDBAND,而POLLOUT则等价于POLLWRNORM。假如,要同时监视一个文件描述符是否可读和可写,我们可以设置events为POLLIN | POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。
timeout参数指定等待的毫秒数,无论I/O是否准备好,超时时间一到poll都会返回。timeout指定为负数值表示无限超时,UNPv1 中使用的INFTIM 宏貌似现在已经废弃,因此如果要设置无限等待,直接将timeout赋值为-1;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。
成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1。
//pollEcho.cpp #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/types.h> #include <vector> #include <string.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <poll.h> #include <stropts.h> #include <netdb.h> #define PORT 1314 #define MAX_LINE_LEN 1024 int main() { struct sockaddr_in cli_addr, server_addr; socklen_t addr_len; int one,flags,nrcv,nwrite,nready; int listenfd,connfd; char buf[MAX_LINE_LEN],addr_str[INET_ADDRSTRLEN]; std::vector<struct pollfd> pollfdArray; struct pollfd pfd; bzero(&server_addr, sizeof server_addr); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); listenfd = socket(AF_INET, SOCK_STREAM, 0); if( listenfd < 0) { printf("listen error: %s \n", strerror(errno)); exit(1); } one = 1; setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR, &one, sizeof one); flags = fcntl(listenfd,F_GETFL,0); fcntl(listenfd, F_SETFL, flags | O_NONBLOCK); if(bind(listenfd,reinterpret_cast<struct sockaddr *>(&server_addr),sizeof(server_addr)) < 0) { printf("bind error: %s \n", strerror(errno)); exit(1); } listen(listenfd, 100); pfd.fd = listenfd; pfd.events = POLLIN; pollfdArray.push_back(pfd); while(1) { nready = poll(&(*pollfdArray.begin()), pollfdArray.size(), -1); if( nready < 0) { printf("poll error: %s \n", strerror(errno)); } if( pollfdArray[0].revents & POLLIN) { addr_len = sizeof cli_addr; connfd = accept(listenfd, reinterpret_cast<struct sockaddr *>(&cli_addr), &addr_len); if( connfd < 0) { if( errno != ECONNABORTED || errno != EWOULDBLOCK || errno != EINTR) { printf("accept error: %s \n", strerror(errno)); continue; } } printf("recieve from : %s at port %d\n", inet_ntop(AF_INET,&cli_addr.sin_addr,addr_str,INET_ADDRSTRLEN),cli_addr.sin_port); flags = fcntl(connfd, F_GETFL, 0); fcntl(connfd,F_SETFL, flags | O_NONBLOCK); bzero(&pfd, sizeof pfd); pfd.fd = connfd; pfd.events = POLLIN; pollfdArray.push_back(pfd); if(--nready < 0) { continue; } } for( unsigned int i = 1; i < pollfdArray.size(); i++) // i from 1 not 0 { pfd = pollfdArray[i]; if(pfd.revents & (POLLIN | POLLERR)) { memset(buf, 0, MAX_LINE_LEN); if( (nrcv = read(pfd.fd, buf, MAX_LINE_LEN)) < 0) { if(errno != EWOULDBLOCK || errno != EAGAIN || errno != EINTR) { printf("read error: %s\n",strerror(errno)); } } else if( 0 == nrcv) { close(pfd.fd); pollfdArray.erase(pollfdArray.begin() + i); } else { printf("nrcv: %s\n",buf); nwrite = write(pfd.fd, buf, nrcv); if( nwrite < 0) { if(errno != EAGAIN || errno != EWOULDBLOCK) printf("write error: %s\n",strerror(errno)); } printf("nwrite = %d\n",nwrite); } } } } return 0; }
以上代码操作的文件描述符都设置成为了非阻塞的状态,这也是为了更好的配合I/O multiplexing 的执行,试想如果read 或者 write 阻塞在某个描述符上,I/O multiplexing 就失去了真正的意义了,因为此时select/poll 函数就无法处理其它描述符产生的事件了。但是只要设置为非阻塞就够了吗? 这显然是还不够的,后面会专门写一篇文章对非阻塞的I/O multiplexing 进行完善。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它不会复用文件描述符集合来传递结果而迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll的使用与select/poll不同,它是由一组系统调用组成:
#include<sys/epoll.h> int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
第一个函数 epoll_create() 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,其实size参数内核不会用到,只是开发者自己提醒自己的一个标记。epoll对监听的描述符数目没有限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大,在我的机器上这个值为:149197.
第二个函数 epoll_ctl() 是epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD: 修改已经注册的fd监听事件;
EPOLL_CTL_DEL: 从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
其中epoll_data_t 结构如下:
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t;
注意这是一个union 结构。
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
这里介绍一下边沿触发和水平触发(epoll默认使用水平触发):
LT 电平触发(高电平触发):
EPOLLIN 事件
内核中的某个socket接收缓冲区 为空 低电平
内核中的某个socket接收缓冲区 不为空 高电平
EPOLLOUT 事件
内核中的某个socket发送缓冲区 不满 高电平
内核中的某个socket发送缓冲区 满 低电平
ET 边沿触发:
低电平 -> 高电平 触发
高电平 -> 低电平 触发
推荐使用默认的水平触发。
第三个函数epoll_wait() 等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,返回的事件都保存在该events数组中,需要通过判断该数组中各个元素的状态来决定该如何处理,该maxevents告之内核这个events数组的大小。该函数返回需要处理的事件数目,如返回0表示已超时。
#include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/types.h> #include <vector> #include <string.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <sys/epoll.h> using namespace std; #define PORT 1314 #define MAX_LINE_LEN 1024 #define EPOLL_EVENTS 1024 int main() { struct sockaddr_in cli_addr, server_addr; socklen_t addr_len; int one,flags,nrcv,nwrite,nready; int listenfd,epollfd,connfd; char buf[MAX_LINE_LEN],addr_str[INET_ADDRSTRLEN]; struct epoll_event ev; std::vector<struct epoll_event> eventsArray(16); bzero(&server_addr, sizeof server_addr); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); listenfd = socket(AF_INET, SOCK_STREAM, 0); if( listenfd < 0) { perror("socket open error! \n"); exit(1); } one = 1; setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR, &one, sizeof one); flags = fcntl(listenfd,F_GETFL,0); fcntl(listenfd, F_SETFL, flags | O_NONBLOCK); if(bind(listenfd,reinterpret_cast<struct sockaddr *>(&server_addr),sizeof(server_addr)) < 0) { perror("Bind error! \n"); exit(1); } listen(listenfd, 100); epollfd = epoll_create(EPOLL_EVENTS); if(epollfd < 0) { printf("epoll_create error: %s \n",strerror(errno)); exit(1); } ev.events = EPOLLIN; ev.data.fd = listenfd; if(epoll_ctl(epollfd, EPOLL_CTL_ADD,listenfd,&ev) < 0) { printf("register listenfd failed: %s",strerror(errno)); exit(1); } while(1) { nready = epoll_wait(epollfd,&(*eventsArray.begin()),static_cast<int>(eventsArray.size()),-1); if(nready < 0) { printf("epoll_wait error: %s \n",strerror(errno)); } for( int i = 0; i < nready; i++) { if(eventsArray[i].data.fd == listenfd) { addr_len = sizeof cli_addr; connfd = accept(listenfd, reinterpret_cast<struct sockaddr *>(&cli_addr),&addr_len); if( connfd < 0) { if( errno != ECONNABORTED || errno != EWOULDBLOCK || errno != EINTR) { printf("accept socket aborted: %s \n",strerror(errno)); continue; } } flags = fcntl(connfd, F_GETFL, 0); fcntl(connfd,F_SETFL, flags | O_NONBLOCK); ev.events = EPOLLIN; ev.data.fd = connfd; if(epoll_ctl(epollfd,EPOLL_CTL_ADD,connfd,&ev) < 0) { printf("epoll add error: %s",strerror(errno)); } printf("recieve from : %s at port %d\n", inet_ntop(AF_INET,&cli_addr.sin_addr,addr_str,INET_ADDRSTRLEN),cli_addr.sin_port); if(--nready < 0) { continue; } } else { ev = eventsArray[i]; printf("fd = %d \n",ev.data.fd); memset(buf,0,MAX_LINE_LEN); if( (nrcv = read(ev.data.fd, buf, MAX_LINE_LEN)) < 0) { if(errno != EWOULDBLOCK || errno != EAGAIN || errno != EINTR) { printf("read error: %s\n",strerror(errno)); } } else if( 0 == nrcv) { close(ev.data.fd); printf("close: %d fd \n",ev.data.fd); eventsArray.erase(eventsArray.begin() + i); } else { printf("nrcv, content: %s\n",buf); nwrite = write(ev.data.fd, buf, nrcv); if( nwrite < 0) { if(errno != EAGAIN || errno != EWOULDBLOCK) printf("write error: %s\n",strerror(errno)); } printf("nwrite = %d\n",nwrite); } } } } return 0; }
客户端的测试代码还是可以用前一篇文章提到的:nc localhost 1314 的方式来测试。
I/O multiplexing 有三个方式可以完成,这三种方式的优劣和适用场合不同,后面会专门分析。