epoll底层原理
epoll作为linux下高性能网络服务器的必备技术至关重要,java NIO、nginx、redis、skynet和大部分游戏服务器都使用了这一多路复用技术。
epoll是select和poll的增强版本。
epoll的三个方法:
epoll_create: 内核会创建一个eventpoll对象(专用的文件描述符,也就是程序中epfd所代表的对象)
eventpoll对象也是文件系统中的一员,和socket一样,它也会有等待队列。epoll_ctl: 添加待监控的socket
如果通过spoll_ctl添加sock1、sock2和sock3的监视,内核会将三个socket添加到eventpoll监听队列epoll_wait: 阻塞等待
进程A运行到了epoll_wait语句之后,进程A会等到eventpoll的等待队列。
epoll相对于select和poll的优化措施:
优化措施一:
- 功能分离: 进程到等待队列,进程阻塞
select低效的原因之一是将“维护等待队列”和“阻塞进程”两个步揍合二为一。
大多数的应用场景中,需要监视的socket数量相对固定,并不需要每次都修改。epoll将这两个操作分开,首先调用epoll_ctl维护监听队列,在调用epoll_wait阻塞进程。
优化措施二:
- 引入了就绪队列rdlist
select低效的另一个原因在于程序不知道那些socket收到数据,只能一个一个遍历。如果内核维护一个“就绪队列”rdlist,引用收到数据的socket,就能避免遍历。
下面代码中,先用epoll_create创建一个epoll对象epfd,再通过epoll_ctl将需要监听的socket添加到epfd的专用监听队列中,最后调用epoll_wait等待数据,返回rdlist列表中的就绪socket。
int epfd=epoll_create(...);
epoll_ctl(epfd,...)//第一步:将所需要监听的socket添加到epfd中监听队列
while(1){
int n=epoll_wait(...); //第二步:阻塞进程等待事件
for(接收到数据的socket){
//处理
}
}
假设计算机正在运行进程A和进程B,在某时刻进程A运行到了epoll_wait语句。内核会将进程A放入到eventpoll监听队列中,阻塞进程。
当socket接收到数据,中断程序会做两个工作
- 一方面修改rdlist
- 另一方面唤醒eventpoll监听队列中的进程,进程A再次进入运行状态。
也正因为rdlist的存在,进程A可以知道哪些socket发生了变化。
epoll的三大步骤:
Epoll第一步epoll_create:
当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象(epfd文件描述符)
Epoll的第二步epoll_ctl:
可以用epoll_ctl添加和删除所要监听的socket。
eg:如果通过epoll_ctl添加sock1、sock2和sock3的监视,内核会将eventpoll添加到这三个socket的监视队列中。
当socket收到数据后,中断程序会操作eventpoll的就绪队列rdlist,而不是直接操作读取数据的进程(如进程A)。当socket2、socket3收到数据后,中断程序让这两个socket进入rdlist。
Epoll的第三步:epoll_wait:
当程序执行到epoll_wait时,如果rdlist非空则返回,如果rdlist为空则阻塞进程。
当socket接收到数据,中断程序一方面将其插入rdlist,另一方面唤醒eventpoll中的进程,进程A再次进入内核的工作队列,进程A进入运行状态。
Linux epoll API:epoll_create
#index
int epoll_create(int size) //创建一个epoll的句柄,自从linux2.6.8之后,size参数是被忽略的。
需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,如果查看/proc/进程id/fd/ ,能够看到这个fd的,所以在使用完epoll后,必须调用close ()关闭,否则可能导致fd被耗尽。
cd /proc/进程id/fd/
ls -l
lrwx--- 1 root root 64 Nov 21 09:44 133 -> /dev/sda1
lrwx--- 1 root root 64 Nov 21 09:44 134 -> /dev/sdb1
lrwx--- 1 root root 64 Nov 21 09:44 136 -> /dev/sdb1
lrwx--- 1 root root 64 Nov 21 09:44 137 -> socket:[232460]
lrwx--- 1 root root 64 Nov 21 09:44 138 -> socket:[7326842]
lrwx--- 1 root root 64 Nov 21 09:44 139 -> socket:[7341066]
soket后面的一串数字是socket的inode号
Linnux epoll API:epoll_ctl:
epoll的事件注册函数,先注册要监听的事件类型
#include
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
- 第一个参数是epoll_create()的返回值 epollfd的句柄epfd。
- 第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
- 第三个参数是需要监听的fd,比如说socket A、socket B、socket C。
- 第四个参数是告诉内核需要监听什么事,使用epoll_event结构
struct epoll_event结构如下:
struct epoll_event{
__uint32_t events;/*Epoll events*/
epoll_data_t data;/*User data variable*/
}
events的事件类型(events可以是以下几个宏的集合)
- EPOLLIN ∶表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
- EPOLLOUT:表示对应的文件描述符可以写;
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
- EPOLLERR:表示对应的文件描述符发生错误;
- EPOLLHUP:表示对应的文件描述符被挂断;
- EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
epoll_event结构data成员变量:
是一个union类型的成员变量,类型定义如下
typedef union epoll_data{
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
union如果该结构对象属于动态存储类型,其成员具有不确定、灵活的初始值
Linux epoll API:epoll_wait:
等到epoll事件中已经发送的事件,类似于select()调用。
#include
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
- 第二个参数events用来从内核得到事件的集合。epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。
- 第三个参数maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
- 第四个参数参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
eventpoll的数据结构
eventpoll的rbr是一颗红黑树,存放所有的socket;rdlist是一个双向链表,存放已经发生IO事件的socket;等待队列为poll_wait,进程A放在poll_wait里;
拓展知识:
红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N)),效率较好。eventpoll的rbr等待队列是一颗红黑树,监听所有socket的IO事件。
(10亿数据红黑树可以进行不到30次比较就可以查到目标)