写在开篇
浅析epoll的实现原理。
recv
先看下只监听一个socket的程序流程:1
2
3
4
5
6
7
8
9
10
11
12//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);
//绑定
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...);
//将数据打印出来
printf(...)
这是一段最基础的网络编程代码,先新建socket对象,依次调用bind、listen与accept,最后调用recv接收数据。recv是个阻塞方法,当程序运行到recv时,它会一直等待,直到接收到数据才往下执行。
下图的计算机中运行着A、B与C三个进程,其中进程A执行着上述基础网络程序,一开始,这3个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行。
创建socket
当进程A执行到创建socket的语句时,操作系统会创建一个由文件系统管理的socket对象。这个socket对象包含了发送缓冲区、接收缓冲区与等待队列等成员。
阻塞进程
当程序执行到recv时,操作系统会将进程A从工作队列移动到该socket的等待队列中。
唤醒进程
当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列。
那我们接下来要讨论的问题是如何同时监视多个socket的数据?
select
下面是selec的用法:1
2
3
4
5
6
7
8
9
10
11
12int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...);
int fds[] = 存放需要监听的socket;
while(1){
int n = select(..., fds, ...)
for(int i=0; i < fds.count; i++){
if(FD_ISSET(fds[i], ...)){
//fds[i]的数据处理
}
}
}
先准备一个数组fds,让fds存放着所有需要监视的socket。然后调用select,如果fds中的所有socket都没有数据,select会阻塞,直到有一个socket接收到数据,select返回,唤醒进程。
select的实现思路很直接,假如程序同时监视如下图的sock1、sock2和sock3三个socket,那么在调用select之后,操作系统把进程A分别加入这三个socket的等待队列中。
假如sock2接收到了数据,中断程序唤起进程A。
所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面。
将进程A从所有等待队列中移除,再加入到工作队列里面。
经由这些步骤,当进程A被唤醒后,它知道至少有一个socket接收了数据。程序只需遍历一遍socket列表,就可以得到就绪的socket。
这种简单方式行之有效,在几乎所有操作系统都有对应的实现。
但是简单的方法往往有缺点,主要是:
- 每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
- 进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。
epoll
先看下epoll的程序流程:1
2
3
4
5
6
7
8
9
10
11
12
13int 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将select中的“维护等待队列”和“阻塞进程”这两个操作分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程。
每次调用select都需要这两步操作,然而大多数应用场景中,需要监视的socket相对固定,并不需要每次都修改。epoll将这两个操作分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程。显而易见地,效率就能得到提升。
我们接着讲epoll的工作流程:
创建 epoll 对象
当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象。而eventpoll维护着一个等待队列和一个就绪列表(rdlist)。
维护监视列表
创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket。
如果通过epoll_ctl添加sock1、sock2和sock3的监视,内核会将eventpoll添加到这三个socket的等待队列中。
阻塞进程
假设计算机中正在运行进程A和进程B,在某时刻进程A运行到了epoll_wait语句。如下图所示,内核会将进程A放入eventpoll的等待队列中,阻塞进程。
唤醒进程
当socket收到数据后,中断程序会操作eventpoll对象,而不是直接操作进程。
当socket接收到数据,中断程序一方面修改就绪列表,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态(如下图)。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。
小结
epoll在select和poll的基础上引入了eventpoll作为中间层,使用了先进的数据结构,是一种高效的多路复用技术。这里也以表格形式简单对比一下select、poll与epoll结束此文。
整理自:https://my.oschina.net/editorial-story/blog/3052308#comments