【Linux 服务器开发】一文看懂 Epoll 实现的 IO 多路复用(IO Multiplexing)

0x00 什么是 Select

在了解 epoll 前,需要先了解 Select 是什么及其作用场景。

1. Select 从哪来

在网络通信中,需要持续监听 Socket 是否接收到了网络数据,从而将处于阻塞状态的进程重新恢复运行。

其中 OS 可以使用端口号或相关的索引映射方式判断 Socket 对应的网络数据。

而对于服务器而言,其往往需要监听并管理多个源连接,故而引出了对 Socket 缓冲列表的处理。

为了解决监听多个连接的问题,Select 应运而生:当 OS 得到了将要维护的一个 Socket 连接的列表后,进行检测,若列表中的所有 Socket 都没有数据,则挂起进程;直到有一个 Socket 收到数据,此时 OS 将会唤醒进程。

也即 Select 使用的是轮询的工作原理,轮询每个客户端文件描述符(fd),查看其是否存在数据并决定是否处理。

2. Select 的工作流程

从 OS 的角度来看,当 Select 被系统调用后,OS 会将监听进程加入到所有 Socket 的等待队列中去,当某个 Socket 接收到数据后,中断程序将会唤起该监听进程,也即将该进程从所有 Socket 的等待队列中移除,再将进程恢复至 OS 的工作队列中去。

此时 OS 会获知至少有一个 Socket 收到了网络数据,再遍历一次Socket 缓冲列表,即可得到该活跃的 Socket 了。

3. Select 的缺点

综合上述原理与工作流程,不难看出 Select 存在着一些亟待解决的缺点。

(1)效率较低

由于网络连接时间只有部分的 Socket 处于活跃状态,而 Select 每次调用都会线性扫描 Socket 缓冲列表,从而导致了效率的下降。

(2)开销较大

每次调用 Select 都要传递所要监控的所有客户端文件描述符给 Select 系统调用,也即每次调用都要将 fd 列表从用户态拷贝到内核态,当 fd 数量较大时,会显著地降低效率且增大开销。

(3)单个进程监视的文件描述符上限为 1024

由于 Select 需要将所有客户端文件描述符从用户态拷贝到内核态,故而从性能上看,若 fd 列表很庞大,用户态和内核态之间数据复制将产生巨大的花销,所以 Select 限制文件描述符上限为1024.

#define FD_SETSIZE 1024
#define NFDBITS (8 * sizeof(unsigned long))
#define __FDSET_LONGS (FD_SETSIZE/NFDBITS)

typedef struct {
  unsigned long fds_bits[__FDSET_LONGS];
} fd_set;
  • 相较于 Select,Poll(Select 的改进版) 通过使用链表存储文件描述符,取代了 fd_set ,解决了上限的问题,但其他缺点依旧没有解决。

0x01 什么是 Epoll

为了解决 Select 存在的种种缺点而提出了 epoll.

epoll(eventpoll)在Linux内核中实现了 IO多路复用(IO Multiplexing)。

也即在一个操作中,同时监听多个 IO 源,在其中一个或多个 IO 源可用时返回,然后对其进行操作。

epoll 常应用于 Linux 下高并发服务型程序,如在大量并发连接中只有少部分连接活跃的情况下,能显著地提高程序的 CPU 利用率。

1. 相较于 Select 如何做出改善

由于 Select 中进行线性遍历扫描的操作效率较低,故而在 epoll 中维护一个连接活跃列表(就绪列表)双向链表(对应 fd 在该列表中的快速添加与删除),来记录接收到了数据的连接,只对活跃的 Socket 进行操作,就能避免遍历操作。

当连接获取到了网络数据,进程将被唤醒,此时只需要获取连接活跃列表中的数据即可判定是哪一个或多个连接收到了数据。

2. Epoll 工作流程

可以从其主要涉及的三个函数 epoll_create()epoll_ctl()epoll_wait() 来展开理解 epoll 的工作流程。

(1)int epoll_create(int size);

该函数用于创建一个 epoll 的句柄,创建成功会返回一个 epfd.

当某个进程调用 epoll_create() 时,内核会创建一个 epoll 对象,其包含了类似于 Socket 的等待队列与前文提到的连接活跃列表

(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

创建了 epoll 对象后,就要对该对象进行控制与操作。调用 epoll_ctl() 以对 epfd 中的 fd 进行添加、修改与删除的操作。

此时若通过 epoll_ctl 添加了对若干个 Socket 的监听,内核会将该 epfd 相对应的 epoll 对象添加到这些 Socket 的等待队列中去。

进行了这种操作后,再当 Socket 收到网络数据,中断程序会对 epoll 对象进行操作,向其连接活跃列表添加 Socket 引用(链表的插入)。故使得数据接收不直接影响进程,只需要修改连接活跃列表即可。而不再需要遍历所有连接,也就提升了效率。

(3)int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

参数 events 指代从内核得到的事件集合,参数 maxevents 指代内核获取的 events 大小,参数 timeout 指代超时时间。epoll_wait 返回需要处理的事件数目,若返回值为 0 ,表示已超时。

当进程运行到 epoll_wait 语句时,内核会将该进程放入 epoll 的等待队列中,实现对进程的阻塞。

当 Socket 收到网络数据,中断程序还会唤醒对应 epoll 等待队列中的进程,让该进程进入运行状态,此时该进程可以从 epoll 的连接活跃列表中获知,收到信息的 Socket 是哪些。

即使要监控大量的连接,通常也只会存在一定数量的、较少的活跃连接,因此,epoll_wait 仅需要从内核态拷贝较少的句柄到用户态,从而避免产生过多的开销。

3. Epoll 的结构

epoll_create 生成的 epoll 对象会在内核内存中创建一个红黑树的索引结构,用于存储 epoll_ctl 传来的 fd(对应相应的 Socket),从而便捷地对 fd 进行增加、删除、修改与查找。

注:由于 OS 有许多待保存的数据,故而链表实现的连接活跃列表并非直接引用 Socket,而是通过 Epitem 间接引用,红黑树的节点也是 Epitem 对象

4. Epoll 的工作模式

epoll 的工作模式分为 LT(Level-Triggered,水平触发)ET(Edge-Triggered,边缘触发)两种。

在 LT 模式下,若某句柄上的事件没有一次处理完,则会在以后每次调用 epoll_wait 时返回该句柄。

而 ET 模式仅在第一次调用 epoll_wait 时会产生返回。

典型地,nginx 使用的工作模式为 ET ,redis 使用的工作模式为 LT.

0x02 Select 与 Epoll 的比较

下表概括了Select 与 Epoll 之间各自的特点。

selectepoll
性能连接数量越高效率越低,当并发连接的数量极大时性能较低,开销较大连接数的增减基本不影响效率的改变,当并发连接的数量极大时仍能保持较好的性能与开销
连接上限连接上限由 FD_SETSIZE 决定,默认为1024连接数无上限,由内存大小决定
工作模式LTLT/ET
处理机制线性轮询回调 callback

在某些情况下,epoll 不一定优于 select
当处于连接数量较少,且连接的 IO 均很繁忙的情况下,此时 select 的性能更优

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值