non-blocking I/O Multiplexing + poll/epoll 的正确使用
2017-07-14 13:44
295 查看
1、首先来回顾下poll / epoll 函数的原型
#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 */
};
#include <sys/epoll.h>
int epoll_create(int size); //size 并不代表能够容纳的事件个数
int epoll_create1(int flags); // EPOLL_CLOEXEC
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);
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
具体的参数介绍参考以前的文章。
2、关于SIGPIPE 信号的产生和处理
如果客户端关闭套接字close,而服务器调用一次write, 服务器会接收一个RST segment(tcp传输层)
如果服务器端再次调用了write,这个时候就会产生SIGPIPE信号,默认终止进程。可以在程序中直接忽略掉,如 signal(SIGPIPE, SIG_IGN);
3、TIME_WAIT 状态对 服务器的影响
如果服务器端 主动断开连接(先于client 调用close),服务器端就会进入TIME_WAIT 状态。应尽可能在服务器端避免TIME_WAIT 状态,因为它会在一定时间内hold住一些内核资源。协议设计上,应该让客户端主动断开连接,这样就把TIME_WAIT状态分散到大量的客户端。如果客户端不活跃了,一些不客户端不断开连接,这样就会占用服务器端的连接资源。服务器端也要踢掉不活跃的连接close。
4、使用 C++ erase 的注意点
![](https://img-blog.csdn.net/20131009204055453?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvam51X3NpbWJh/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
即erase 返回的是下一个元素的iterator
5、新的accept4 系统调用
accept - accept a connection on a socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
可以使用accept4 这个新的系统调用,多了一个flags 参数,可以设置以下两个标志:
SOCK_NONBLOCK Set the O_NONBLOCK file status flag on the new open file description. Using this flag saves extra calls to fcntl(2) to achieve
the same result.
SOCK_CLOEXEC Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of the O_CLOEXEC flag in open(2)
for reasons why this may be useful.
注意,这两个标志是设置accept 回来的conn 标志的,当然也可以使用fcntl (F_SETFL / F_SETFD) 设置,但少了两次系统调用,可以稍微提高点性能。
7、poll 的处理流程和存在的问题
![](https://img-blog.csdn.net/20131009204620171?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvam51X3NpbWJh/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
存在的问题和解决办法:
(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 基本类似
![](https://img-blog.csdn.net/20131009215046312?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvam51X3NpbWJh/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
LT 电平触发(高电平触发):
EPOLLIN 事件
内核中的某个socket接收缓冲区 为空 低电平
内核中的某个socket接收缓冲区 不为空 高电平
EPOLLOUT 事件
内核中的某个socket发送缓冲区 不满 高电平
内核中的某个socket发送缓冲区 满 低电平
注:只要第一次write没写完整,则下次调用write直接把数据添加到应用层缓冲区OutBuffer,等待EPOLLOUT事件。
如果采用Level-Triggered,那什么时候关注EPOLLOUT事件?会不会造成busy-loop(忙等待)?
Edge-Triggered:
![](https://img-blog.csdn.net/20131009215126218?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvam51X3NpbWJh/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
ET 边沿触发:
低电平-》高电平 触发
推荐epoll使用LT模式的原因:
与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 才退出。代码示例如下:
C++ Code
10、accept(2)返回EMFILE的处理(文件描述符已经用完)
(1)、调高进程文件描述符数目
(2)、死等
(3)、退出程序
(4)、关闭监听套接字。那什么时候重新打开呢?
(5)、如果是epoll模型,可以改用edge trigger。问题是如果漏掉了一次accept(2),程序再也不会收到新连接(没有状态变化)
(6)、准备一个空闲的文件描述符。遇到这种情况,先关闭这个空闲文件,获得一个文件描述符名额;再accept(2)拿到socket连接的文件描述符;随后立刻close(2),这样就优雅地断开了与客户端的连接;最后重新打开空闲文件,把“坑”填上,以备再次出现这种情况时使用。
如下面的代码片段:
C++ Code
#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 */
};
#include <sys/epoll.h>
int epoll_create(int size); //size 并不代表能够容纳的事件个数
int epoll_create1(int flags); // EPOLL_CLOEXEC
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);
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
具体的参数介绍参考以前的文章。
2、关于SIGPIPE 信号的产生和处理
如果客户端关闭套接字close,而服务器调用一次write, 服务器会接收一个RST segment(tcp传输层)
如果服务器端再次调用了write,这个时候就会产生SIGPIPE信号,默认终止进程。可以在程序中直接忽略掉,如 signal(SIGPIPE, SIG_IGN);
3、TIME_WAIT 状态对 服务器的影响
如果服务器端 主动断开连接(先于client 调用close),服务器端就会进入TIME_WAIT 状态。应尽可能在服务器端避免TIME_WAIT 状态,因为它会在一定时间内hold住一些内核资源。协议设计上,应该让客户端主动断开连接,这样就把TIME_WAIT状态分散到大量的客户端。如果客户端不活跃了,一些不客户端不断开连接,这样就会占用服务器端的连接资源。服务器端也要踢掉不活跃的连接close。
4、使用 C++ erase 的注意点
即erase 返回的是下一个元素的iterator
5、新的accept4 系统调用
accept - accept a connection on a socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
可以使用accept4 这个新的系统调用,多了一个flags 参数,可以设置以下两个标志:
SOCK_NONBLOCK Set the O_NONBLOCK file status flag on the new open file description. Using this flag saves extra calls to fcntl(2) to achieve
the same result.
SOCK_CLOEXEC Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of the O_CLOEXEC flag in open(2)
for reasons why this may be useful.
注意,这两个标志是设置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 基本类似
LT 电平触发(高电平触发):
EPOLLIN 事件
内核中的某个socket接收缓冲区 为空 低电平
内核中的某个socket接收缓冲区 不为空 高电平
EPOLLOUT 事件
内核中的某个socket发送缓冲区 不满 高电平
内核中的某个socket发送缓冲区 满 低电平
注:只要第一次write没写完整,则下次调用write直接把数据添加到应用层缓冲区OutBuffer,等待EPOLLOUT事件。
如果采用Level-Triggered,那什么时候关注EPOLLOUT事件?会不会造成busy-loop(忙等待)?
Edge-Triggered:
ET 边沿触发:
低电平-》高电平 触发
推荐epoll使用LT模式的原因:
与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 才退出。代码示例如下:
C++ Code
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 事件 } |
(1)、调高进程文件描述符数目
(2)、死等
(3)、退出程序
(4)、关闭监听套接字。那什么时候重新打开呢?
(5)、如果是epoll模型,可以改用edge trigger。问题是如果漏掉了一次accept(2),程序再也不会收到新连接(没有状态变化)
(6)、准备一个空闲的文件描述符。遇到这种情况,先关闭这个空闲文件,获得一个文件描述符名额;再accept(2)拿到socket连接的文件描述符;随后立刻close(2),这样就优雅地断开了与客户端的连接;最后重新打开空闲文件,把“坑”填上,以备再次出现这种情况时使用。
如下面的代码片段:
C++ Code
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"); } |
相关文章推荐
- 浅谈 non-blocking I/O Multiplexing + poll/epoll 的正确使用
- 浅谈 non-blocking I/O Multiplexing + poll/epoll 的正确使用
- non-blocking I/O Multiplexing + poll/epoll 的正确使用
- 浅谈 non-blocking I/O Multiplexing + poll/epoll 的正确使用
- Python: 使用select函数编写nonblocking TCP/IP socket程序
- 浅谈 non-blocking I/O Multiplexing + poll/epoll 的正确使用
- I/O Multiplexing & poll
- 一个完整的稳定的,单线程的,有正确的收发功能的使用epoll的socket服务程序
- Linux下I/O多路复用select, poll, epoll 三种模型的Python使用
- select,poll和epoll使用
- Linux系统编程——I/O多路复用select、poll、epoll的区别使用
- select,poll和epoll使用
- Python-Select/Poll/Epoll使用
- linux的IO复用技术:select、poll、epoll的区别以及epoll的原理和使用
- Linux系统编程——I/O多路复用select、poll、epoll的区别使用
- 一个完整的稳定的,单线程的,有正确的收发功能的使用epoll的socket服务程序
- select、poll、epoll使用小结
- Linux系统编程——I/O多路复用select、poll、epoll的区别使用
- I/O多路复用select、poll、epoll的区别使用
- linux下select,poll,epoll的使用与重点分析