select
select的实现思路很直接。假如程序同时监视sock1、sock2和sock3三个socket,那么在调用select之后,操作系统把进程A分别加入这三个socket的等待队列中(前文说过,放的其实是进程A的引用),当任何一个socket收到数据后,中断程序将唤起进程(所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面)
每次调用selec都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次
poll
select 单个进程所能打开的最大连接数由FD_SETSIZE宏定义,默认1024,不超过2^32或2^64
poll本质上和select没有区别,但是它没有最大连接数的限制,因为它是基于链表来存储的
epoll 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接
epoll
select低效的原因之一是将"维护等待队列"和“阻塞进程"两个步骤合二为一。如下图所示,每次调用select都需要这两步操作,然而大多数应用场景中、需要监视的socket相对固定,并不需要每次都修改。epoll将这两个操作分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程。显而易见的,效率就能得到提升
select低效的另一个原因在于程序不知道哪些socket收到数据,只能一个个遍历。如果内核维护一个"就绪列表",引用收到数据的socket,就能避免遍历。如下图所示,计算机共有三个socket,收到数据的sock2和sock3被rdlist(就绪列表)所引用。当进程被唤醒后,只要获取rdlist的内容,就能够知道哪些socket收到数据。
执行流程
-
创建epoll对象
- 当某个进程调用epoll_create方法时,内核会创建一个eventpool对象 。eventpool对象也是文件系统中的一员,和socket一样,它也会有等待队列。
-
维护监视列表
- 用epollctl添加或删除所要监听的socket
-
接收数据
- 当socket收到数据后,中断程序会给eventpoll的“就绪列表”添加socket 引用
- 当程序执行epoll_wait时,如果rdlist已经引用了socket,那么epoll_wait直接返回,如果rdlist为空,阻塞进程。
-
阻塞和唤醒进程
- 如果进程A运行到了epoll_wait语句,内核会将进程A放入event_poll的等待队列中,阻塞进程。
- 当socket接收到数据,中断程序一方面修改rdlist,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态。也因为rdist的存在,进程A可以知道哪些socket发生了变化。
data structure
-
每一个epol对象都有一个独立的eventpol结构体,
-
通过epoll_ctl方法向epol对象中添加进来的事件挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的 插入/查找/删除 时间效率是lgn,其中n为树的高度)
- 用户态操作 epoll的监视文件时、需要增、删、改、查等动作有着比较高的效率。尤其是当 epoll监视的文件数量达到百万级的时候,选用不同的数据结构带来的效率差异可能非常大。
-
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系、也就是说、当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫epoll_callback,它会将发生的事件添加到rdlist双向链表中。
- 当网卡收到数据包会触发一个中断,中断处理函数再回调epoll_callback 将这个 fd 添加至 rdlist 中
在连接数多,只有少量连接活跃时,epoll性能好
但是在连接数少并目连接都十分活跃的情况下、select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调