epoll模型详解
epoll_wait和poll的区别
- epoll_wait 函数调用完之后,我们可以直接在 event 参数中拿到所有有事件就绪的 fd,直接处理即可(event 参数仅仅是个出参);
- 而 poll 函数的事件集合调用前后数量都未改变,只不过调用前我们通过 pollfd 结构体的 events 字段设置待检测事件,调用后我们需要通过 pollfd 结构体的 revents 字段去检测就绪的事件( 参数 fds 既是入参也是出参)。
- 一般在 fd 数量比较多,但某段时间内,就绪事件 fd 数量较少的情况下,epoll_wait 才会体现出它的优势,也就是说 socket 连接数量较大时而活跃连接较少时 epoll 模型更高效。
函数签名
#include <sys/epoll.h>
//创建epoll句柄
int epoll_create(int size);// 返回一个epoll句柄
//参数size在Linux2.6以后不在使用,但是必须设置为一个>0的数
成功返回一个非负的epollfd句柄,失败返回-1
// 将待检测事件的句柄绑定到epollfd上
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
参数一:epollfd
参数二:操作选项:取值有 EPOLL_CTL_ADD、EPOLL_CTL_MOD 和 EPOLL_CTL_DEL,分别表示向epollfd
上添加、修改和移除一个其他 fd(socketfd),当取值为EPOLL_CTL_DEL时,第四个参数忽略不计,
可以设置为NULL
参数三:被操作fd,socketfd
参数四:结构体地址
成功返回0,失败返回-1
//检测句柄事件
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
参数一:epollfd
参数二:结构体输出参数,调用成功后,events 中存放的是与就绪事件相关 epoll_event 结构体数组
参数三:参数二数组中元素的最大个数
参数四:超时时间,单位微秒
成功返回有件事的fd数量,失败返回-1,返回0表示超时
epoll_event结构体:
struct epoll_event
{
uint32_t events; /* 需要检测的 fd 事件,取值与 poll 函数一样 */
epoll_data_t data; /* 用户自定义数据 */ data联合体对象,64位系统上,8字节大小
};
typedef union epoll_data //
{
void* ptr;
int fd; // 这里设置fd
uint32_t u32;
uint64_t u64;
} epoll_data_t;
关于联合体,可以存储不同的数据类型,但是只能同时存储其中一种的数据类型,任何两个成员不会同时生效,
节省空间
tip:联合体所占空间字节数,必须满足能容纳最大字段的字节,并且可以被所有成员字节整除
水平模式和边缘模式
水平模式(默认模式)
一个事件只要有,就会一直触发
- socket可读事件在水平模式触发条件
- socket上没有数据->socket上有数据
- socket上处于有数据状态
- socket可写事件在水平模式触发条件
- socket可写->socket可写
socket不可可写->socket可写
- socket可写->socket可写
边缘模式(需要添加事件宏EPOLLET)
只有一个事件从无到有才会触发
- socket触发可写
- socket不可写->可写
- socket触发可读
- socket上无数据->socekt有数据
- socket上又新来一次数据
关于socket读写事件状态
- 对于写事件:
- 正常情况下,网络通信双方,socket是一直可写,所谓socket可写就是TCP缓冲区足够大,允许你把数据发出去。
- 所以如果是水平模式注册了写事件,就会一直触发。一般对于发数据都建议先发,发不完再注册写事件,发完了取消写事件。
- 对于边缘模式,一般也是先发数据,如果数据没发完,就注册可写时间,数据发完了不需要取消可写时间。
- 对于读事件:
- 考虑到边缘模式只会触发一次,所以注册的读事件触发之后,一般建议一次性收完,通过while死循环直到recv返回EWOULDBLOCK错误码,表示没有数据可收了。
- 而对于水平模式,注册了读事件,如果socket缓冲区一直有数据,会一直触发,所以水平模式可以按需收取字节数,收不完没事。
- 关于水平模式和边缘模式效率问题
- 一般没有所谓的效率高低,只是根据两个模式特点,编码方式不同,需要根据业务来定;
- 对于写事件
- 水平模式编码简单,但是触发次数多;
- 边缘模式编码复杂,触发次数少。
- 对于读事件:
- 边缘模式触发次数少,就需要while循环不断去收,如果对端不停发数据,一个线程的边缘模式就会卡在一个socket的收数据循环,从而无法处理其他socket上的事件。
- 水平模式不会丢失数据或者信息,如果数据没读完,内核会一直上报,虽然每次读数据都会系统调用一次,但是可以照顾多个连接的公平性(实时性),而且水平模式的写法可以跨平台。
epoll底层数据结构(以后要修改)
- 通过epoll_create创建一个epoll文件描述符的时候,会创建一个eventpoll对象,这个对象有两个重要的成员变量,一个是指向红黑树的根节点,一个指向就绪双向链表的跟节点。
- 红黑树里面存放的是所有挂载到epollfd上的所要监测的socketfd信息,双向链表存储的是事件就绪的sockefd信息。
- epoll_ctl会通过传入的op操作,操作红黑树上相关的socketfd的节点信息(新增、修改、删除)。
- epoll_wait是判断双向链表里面有没有就绪节点,如果有信息就会取出来填充到epoll_waitd的传入参数中。如果双向链表没有信息,epoll_wait会阻塞设定的超时时间,如果这段时间有事件就绪,内核会将发送事件的socketfd信息插入到双向链表中,并且被epoll_wait填充到传出参数()中。
优点
- 相比select没有连接数限制;
- 效率提升,应用程序不需要轮询,不会随着句柄数量增加而降低效率,也就是只关心活跃的连接,epoll_wait返回的从内核携带出来的只有发送了事件的结构体;
- 每次调用epoll_wait的时候,不想poll和select传入一个结构体到内核中,而是通过传入的epoll句柄复用内核中的epoll实例结构体,减小了系统开销。
- epoll底层有个红黑树和双向链表,当有事件发生了,内核可以很快的遍历红黑树o(logn),并将相应的socket节点信息添加到双向链表中,不想poll和select的是循环遍历O(n)。
小总结:
- LT 模式下,读事件触发后,可以按需收取想要的字节数,不用把本次接收到的数据收取干净(即不用循环到 recv 或者 read 函数返回 -1,错误码为 EWOULDBLOCK 或 EAGAIN);ET 模式下,读事件必须把数据收取干净,因为你不一定有下一次机会再收取数据了,即使有机会,也可能存在上次没读完的数据没有及时处理,造成客户端响应延迟。
- LT 模式下,不需要写事件一定要及时移除,避免不必要的触发,浪费 CPU 资源;ET 模式下,写事件触发后,如果还需要下一次的写事件触发来驱动任务(例如发上次剩余的数据),你需要继续注册一次检测可写事件。