epoll的使用与源码分析
什么是epoll?
epoll是linux中IO多路复用的一种机制,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。IO多路复用的方法,处理有select,poll,epoll。
epoll是为处理大批量句柄而作了改进的poll,被认为Linux下性能最好的多路I/O就绪通知方法。
- epoll的相关系统调用
epoll只有epoll_create , epoll_ctl , epoll_wait 3个系统调用。#创建epoll fd int epoll_create(int size); #添加epoll需要监听的事件 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); #收集在epoll监控的事件中已经发送的事件 #当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符 #而是一个代表就绪描述符数量的值, #你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可, int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- epoll的工作原理
-
select/poll 每次调用都要传递所要监控的所有fd给系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。
而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。
- select/poll一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。
-
当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。
这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd(socket)插入到准备就绪list链表里了。
-
epoll比select 和 poll 好在哪里?
-
支持打开大数目的socket描述符(FD)不同
a. select 方法是通过开辟一个数组来存储和维护fd,这个数组的长度是2048,也就是说select支持的fd是有限的。
b. poll 是通过链表来维护fd的,所有数量上面没有限制。
-
IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数。
-
不需要在用户空间和内核空间之间频繁的拷贝fd
-
epoll源码分析
epoll相关的内核代码在fs/eventpoll.c文件中,下面分别分析epoll_create、epoll_ctl和epoll_wait三个函数在内核中的实现。
# epoll_create用于创建一个epoll的句柄
# 其在内核的系统实现如下:
SYSCALL_DEFINE1(epoll_create, int, size)
{
if (size <= 0)
return -EINVAL;
return sys_epoll_create1(0);
//我们在调用epoll_create时,
//传入的size参数,仅仅是用来判断是否小于等于0
//之后再也没有其他用处。
}
感觉这个人写得更好: https://www.jianshu.com/p/aa486512e989
- Epoll的2种工作方式 — 水平触发(LT)和边缘触发(ET)
epoll 的默认模式是水平触发模式,但是也可以设置为边缘触发模式,在边缘触发模式的效率更高。
具体的区别在于:
如上图所示,0表示文件还没有准备好,1表示文件描述符已经准备好了。
如果在水平模式下面,有一个文件描述符从0变成1,那么表示的是这个描述符已经准备就绪了,可以对它进行io操作了。如果你不作任何操作,内核还是会继续通知你 的,所以,这种模式编程出错误可能性要小一点。
在边缘模式下面,如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。
源码
epoll使用的源码放在了Github上 :
https://github.com/GreenGitHuber/code_something/tree/master/epoll%2Breactor%E6%A8%A1%E5%BC%8F%E7%9A%84%E6%9C%8D%E5%8A%A1%E5%99%A8