一,epoll的三个系统调用
函数1
int epoll_create(int size);
//创建一个句柄,也要在文件描述符表上占一个位置
//size 可以忽略,存在的原因是有很多程序已经用了size,如果去掉就不能用了。
函数2
int epoll_ctl (int epfd ,int op ,int fd, struct epoll_event * event);
作用:epoll事件注册函数
1,int epfd :表示要对那个epoll对象进行操作,传入上面创建的句柄
2, int op :表示对某个文件描述符进行什么操作,增删改
EPOLL_CTL_ADD:注册新的fd到epfd中。
EPOLL_CTL_MOD:修改已经注册的fd的监听事件。
EPOLL_CTL_DEL:从epfd中删除一个fd。
3, int fd :操作那个文件描述符
4,struct epoll_event * event:
struct epoll_event
{
uint32_t events; //检测的事件
epoll_data_t data;//用户自定义空间
}__EPOLL_PACKED;
typedef union epoll_data
{
void * ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
events 可以是以下⼏个宏的集合:
EPOLLIN : 表⽰对应的⽂件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表⽰对应的⽂件描述符可以写;
EPOLLPRI : 表⽰对应的⽂件描述符有紧急的数据可读 (这⾥应该表⽰有带外数据到来);
EPOLLERR : 表⽰对应的⽂件描述符发⽣错误;
EPOLLHUP : 表⽰对应的⽂件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于⽔平触发(Level Triggered) 来说的.
EPOLLONESHOT:只监听⼀次事件, 当监听完这次事件之后, 还需要继续监听这个socket。
函数3
int epoll_wait(int epfd ,struct epoll_event* events , int maxevents ,int timeout);
函数作用:收集在epoll监控的事件中已经发送的事件
参数events是分配好的epoll_event结构体数组首元素的指针,每个结构体维护了一个文件描述符。
epoll将发生的事件赋值到events数组中,(数组的空间需要我们自己创建)
maxevents 将告诉内核,这个events有多大,就是有几个结构体。但不能大于创建句柄时的参数size
参数timeout 是超时时间,(0会立即返回,-1会永久阻塞)
函数返回值:成功返回对应Io上就绪的文件描述符的个数。返回0 表示超时,返回小于0表示失败
二,epoll的工作原理
1,当使用epoll_create方法创建一个epoll对象时,内核会创建一个epollevent结构体。
struct eventpoll{
....
/*红⿊树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*链表头结点,双链表中则存放着将要通过epoll_wait返回给⽤户的满⾜条件的事件*/
struct list_head rdlist;
....
};
2,每个epoll对象都会有一个对应的epollevent结构体,将通过epoll_ctl方法加入的新的事件加入到结构体的红黑树成员中。
3,这些事件被挂载在红黑树中,如果有重复添加的事件,就可以通过红黑树高效的识别出来,(红黑树的插入效率是log n )。
4,所有添加进epoll中的事件,都会与设备(网卡)驱动程序建立回调关系,也就是说当响应发生时,会自动调用这个回调方法。
5,这个回调⽅法在内核中叫eppollcallback,它会将发⽣的事件添加到rdlist双链表中。
6,当调⽤epoll_wait检查是否有事件发⽣时,只需要检查eventpoll对象中的rdlist双链表中是否有 epitem元素即可。
7,如果rdlist不为空,则把发⽣的事件复制到⽤户态,同时将事件数量返回给⽤户. 这个操作的时间复 杂度是O(1)。
三,epoll优点
1,文件描述符的数目无上限:基于红黑树来管理所有需要监控的文件描述符。(对应 4 )
2,基于事件就绪通知方式:一旦被监听的文件描述符就绪,内核会采用类似于callback的回调机制,
迅速激活这个文件描述符,将就绪的文件描述符加入就绪队列,调用epoll_wait函数获取就绪文件描述符时,
直接拿出内核中就绪队列的元素即可。 这样随着文件描述符的增加,也不会影响判定就绪的性能。
其实就是说,select和poll获取就绪文件描述符时,都要遍历查找文件描述符表,开销很大,但是epoll
已将就绪描述符加入到就绪队列中,只需要从队列中拿出来就行,不用遍历。(对应select 3 )
4,从接口使用角度上讲,不必每次都重新设置要监控的文件描述符,使接口使用方便,
也能够避免频繁的用户态和内核态之间,大量文件描述符的拷贝开销。
其实就是说,select和poll每次都要讲文件描述符表拷贝到内核中,因为内核中并没有存放文件描述符的空间,
所以这两个函数将文件描述符的 增删改 和 等待 没有区分,因为每次循环都需要 增删改 和等待。
但是在创建一个epoll对象时,在内核中就已经为其开辟了一块空间(就是红黑树),用来保存文件描述符表,
所以要进行 增删改 就直接调用 epoll_ctl 函数就行了,在每次的循环中就只需要等待就行了。
这就是为什么,epoll要将 epoll_ctl 和 epoll_wait 函数分开来使用的原因了。(对应select 1, 2)
四,epoll的两种模式
工作模式1:LT水平触发(select ,poll ,epoll默认都是LT)
第一次 epoll-wait 返回读就绪, 如果一次没有将数据读完,那么下次还会触发这个文件描述符的读就绪。
工作模式2:ET边缘触发
要求一次将读就绪的数据读完,也就是第一次没有将数据读完时,下次不会触发这个文件描述符的读就绪。
对比ET和LT:
使用ET能减少epoll的触发次数,但是要求程序员必须在一次响应中就将数据处理完,这无疑增加了代码的复杂度。
但是ET模式,也使得程序员可以操作的空间更加灵活了。
理解ET模式和文件描述符的非阻塞模式结合: