在前面的文章中曾经粗略讲过poll,那时是用阻塞IO实现,在发送和接收数据量都较小情况下和网络状况良好的情况下是基本没有问题的,read 不会只接收部分数据,write 也不会一直阻塞。但实际上poll IO复用经常是跟非阻塞IO一起使用的,想想如果现在内核接收缓冲区一点数据没有,read 阻塞了,或者内核发送缓冲区不够空间存放数据,write 阻塞了,那整个事件循环就会延迟响应,比如现在又有一个新连接connect上来了,也不能很快回到循环去accept 它。
在前面的文章中也曾粗略讲过epoll,使用的是ET 边沿触发模式,每次accept 返回需要将conn 设置为非阻塞,ET模式可能存在的问题是有可能只读取了部分数据,剩下的epoll_wait 就再也不会返回可读事件了。
这篇文章来谈谈如何正确使用non-blocking I/O Multiplexing + poll/epoll。
1、首先来回顾下poll / epoll 函数的原型
注意,这两个标志是设置accept 回来的conn 标志的,当然也可以使用fcntl (F_SETFL / F_SETFD) 设置,但少了两次系统调用,可以稍微提高点性能。
7、poll 的处理流程和存在的问题
存在的问题和解决办法:
(1)、read 可能一次并没有把connfd 所对应的接收缓冲区(内核)的数据都读完(粘包问题),那么connfd 下次仍然是活跃的
应该把读到的数据保存在connfd 的应用层接收缓冲区,每次都追加在末尾。需要处理协议以区分每条消息的边界
(2)、write 可能一次并不能把所有数据都写到发送缓冲区(内核),所以应该有一个应用层发送缓冲区,将未发送完的数据添加到应用层发送缓冲区,关注connfd 的POLLOUT 事件。POLLOUT事件到来,则取出应用层发送缓冲区数据发送write,如果应用层发送缓冲区数据发送完毕,则取消关注POLLOUT事件。
POLLOUT 事件触发条件:connfd的发送缓冲区(内核)不满(可以容纳数据)
注:connfd 的接收缓冲区(内核)数据被接收后会被清空,当发出数据段后接收到对方的ACK段后,发送缓冲区(内核)数据段会被清空。write只是将应用层发送缓冲区数据拷贝到connfd 对应的内核发送缓冲区就返回;read 只是从connfd对应的内核接收缓冲区数据拷贝到应用层接收缓冲区就返回。
9、epoll 的两种模式处理流程和存在的问题
Level-Triggered //跟poll 基本类似
内核中的某个socket接收 缓冲区 为空 低电平内核中的某个socket接收缓冲区 不为空 高电平
内核中的某个socket发送缓冲区 不满 高电平内核中的某个socket发送缓冲区 满 低电平
Edge-Triggered:
与poll兼容
LT模式不会发生漏掉事件的BUG,但POLLOUT事件不能一开始就关注,否则会出现busy loop(即暂时还没有数据需要写入,但一旦连接建立,内核发送缓冲区为空会一直触发POLLOUT事件),而应该在write无法完全写入内核缓冲区的时候才关注,将未写入内核缓冲区的数据添加到应用层output buffer,直到应用层output buffer写完,停止关注POLLOUT事件。
读写的时候不必等候EAGAIN,可以节省系统调用次数,降低延迟。(注:如果用ET模式,读的时候读到EAGAIN,写的时候直到output buffer写完或者写到EAGAIN)
注:在使用 ET 模式时,可以写得更严谨,即将 listenfd 设置为非阻塞,如果accpet 调用有返回,除了建立当前这个连接外,不能马上就回到 epoll_wait ,还需要继续循环accpet,直到返回-1 且errno == EAGAIN 才退出。代码示例如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
if(ev.events & EPOLLIN)
{ do { struct sockaddr_in stSockAddr; socklen_t iSockAddrSize = sizeof(sockaddr_in); int iRetCode = accept(listenfd, ( struct sockaddr *) &stSockAddr, iSockAddrSize); if (iRetCode > 0) { // ...建立连接 // 添加事件关注 } else { //直到发生EAGAIN才不继续accept if(errno == EAGAIN) { break; } } } while( true); // ... 其他 EPOLLIN 事件 } |
10、accept(2)返回EMFILE的处理(文件描述符已经用完)
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
int idlefd = open(
"/dev/null", O_RDONLY | O_CLOEXEC);
connfd = accept4(listenfd, ( struct sockaddr *)&peeraddr, &peerlen, SOCK_NONBLOCK | SOCK_CLOEXEC); /* if (connfd == -1) ERR_EXIT("accept4"); */ if (connfd == - 1) { if (errno == EMFILE) { close(idlefd); idlefd = accept(listenfd, NULL, NULL); close(idlefd); idlefd = open( "/dev/null", O_RDONLY | O_CLOEXEC); continue; } else ERR_EXIT( "accept4"); } |