从事服务端编程,epoll是绕不开的话题,很多著名的开源软件如redis,nginx等都使用了epoll,公司内部的很多产品也用到了epoll。我个人在工作中也用过epoll做开发,但会用只是第一步,完整地理解它的实现,明白它为什么如此高效,才是我的目标,今天写文记录一下,如果哪里写的不准确,希望大家不吝赐教,帮忙指出来。
最开始,先说一下,最普通的场景---阻塞recv的用法,要先明白epoll为什么如此高效,需要先明白最简单的用法有什么问题。
首先,每个socketfd在内核中,都有以下三个成员:发送缓冲区、接收缓冲区、等待队列。
代码运行到recv的时候,进程陷入内核,如果当前该socketfd对应的接收缓冲区没有数据的话,内核将进程从运行态切换为阻塞态,并将当前进程加入到该socketfd的等待队列中。如果该连接有数据到达网卡,会触发网卡的中断,进入网卡设备的中断服务程序。
网卡设备的中断服务程序中,首先将数据拷贝到对应socketfd的接收缓冲区,然后把socketfd的等待队列中的进程重新放入就绪列表中,进程重新得到执行。
进程重新执行后,当前socketfd的接收缓冲区已经有数据,直接将数据拷贝到用户传入的buff指针中。
这里有2次拷贝。
这种场景的问题:只能同时监听同一个fd。
解决这个问题的方法就是多路复用,历史上先出现的解决方案是select,它的大致原理是:
将fd1,fd2,fd3,一共三个fd,通过select接口传入内核,内核将当前进程同时加入到三个fd的等待队列中。
执行select的时候,陷入内核阻塞,之后的过程和上面的第一种用法类似,数据到达时,内核会拷贝数据到对应fd的接收缓冲区,然后将当前进程放入就绪列表执行。
select通过一次性传入多个fd的方式,实现多路复用,这种方案的问题是:
1. 每次调用select都需要把进程添加进所有fd的等待队列,每次唤起又需要从等待队列中移除,2次遍历,开销比较大;
2. 每次调用select都需要把完整的fd列表传给内核,也有一定的开销;
3. select返回后,需要用户自己遍历所有的fd,看哪个fd有事件发生,这是第三次遍历。
epoll是为了解决select的这三个问题而出现的,epoll的使用如下:
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...)
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
while(1){
int n = epoll_wait(...)
for(接收到数据的socket){
//处理
}
}
在梳理epoll的实现之前,需要了解一下背景,epoll的出现,是为了解决select和poll的问题:
1. 每次调用select或poll,都需要传输完整的监听fds信息到内核;
2. select或poll返回时,只是通知应用层有fd可读,但却没有告诉应用层哪个fd可读,因此还需要应用层主动进行O(n)的遍历,才能拿到具体的fd进行读取操作;
为了解决第一个问题,内核需要在内核中存储需要监听的fd信息,存储的数据结构需要支持快速增删查改,因为用户可能对这些数据进行修改,这部分内核使用了红黑树来作为数据结构。
为了解决第二个问题,内核需要维护就绪状态fd列表,来返回给应用层,让应用层知道哪个fd有事件发生,这部分内核使用了双向链表作为数据结构,目前链表可以理解,但为什么使用双向链表还不是太理解。
下面说下我对epoll实现的理解:epoll由3个系统调用组成,epoll_create,epoll_ctl,epoll_wait。
epoll_create的时候,创建内核中的epollevent对象。
epoll_ctl的时候,内核对需要监听的fd信息(红黑树上的信息)进行操作,有add,del,mod三种命令。
epoll_wait的时候,内核检测epollevent对象中的就绪fd列表中是否有数据,若没有,则阻塞知道超时;若有,则直接返回该fd信息。
最后,这里还有一个疑点,涉及到内核epoll的实现原理:有网络数据到达的时候,内核如何操作,让有数据到来的fd,出现在就绪列表中的呢?
用户通过epoll_ctl添加fd进epollevent时,内核将该epollevent添加到fd的等待队列中。
如果网卡上有数据到来,内核发现fd等待队列中的是一个epollevent,则会把该fd添加到epollevent对象的就绪列表中,然后把网卡数据拷贝到fd的接收缓冲区。
用户调用epoll_wait时,此时epollevent对象的就绪列表中已经有fd了,则不会阻塞,直接返回,此时应用层已经知道了可读的fd,则直接调用recv即可拿到数据了。