多路复用技术
2025-06-28 22:18:10

socket的多路复用,指的是单个线程或进程中可以同时处理多个socket的 I/O 事件,可以提高整体效率和资源利用率,常见的多路复用机制包括select、poll和epoll。

以放羊为例:同步阻塞的方式相当于一个人放羊只可以管理一只,epoll的存在相当于一只“牧羊犬”,将大量羊管理起来,如果某只羊有什么需要,牧羊犬就通知给人,完成相应的处理。

epoll的工作机制:使用两个系统调用来操作,epoll_create创建一个epoll 实例,epoll_ctl增加、修改或删除要控制的文件描述符,epoll_wait则是用于等待事件的发生。

编程过程中,建议使用epoll:

  1. epoll效率高,基于事件通知的方式解决轮询带来的性能瓶颈,处理大量文件描述符效率高。
  2. 不受描述符数量限制,select有文件描述符数量上限(通常 1024),epoll无限制。
  3. 内存拷贝少:epoll的系统调用仅在需要数据时进行内存拷贝,减少了系统开销。
    select每次调用时都要重新传递所有文件描述符集合,并进行内存拷贝;
    poll则需要传递整个文件描述符数组。
    epoll每次只传递发生的事件,不需要传递所有文件描述符。
  4. 支持边沿触发:相比于select和poll的水平触发(LT),epoll还支持边沿触发(ET)。
    LT 是默认的触发模式,处理器只要发现事件有未处理的数据就会再次通知
    ET 更高效,它只在状态变化(例如从无数据到有数据)时通知一次,开发难度稍大但可减少系统调用次数。

epoll核心原理

同步阻塞:服务端等待用户端的数据到达,无用户到达就一直阻塞。

多路复用(同时监听很多 TCP 连接等待数据就绪,复用的是进程):epoll_wait同时监听百万连接的数据是否到达,这些连接上只要有数据到达,它就不会进入阻塞状态;无数据到达则阻塞。

多路复用相比于同步阻塞的优点:

  1. 减少进程上下文切换的开销
  2. 让 CPU 缓存命中率更高,执行效率也更高:(CPU 硬件的原因)CPU 处理一个进程是有 L1、L2、L3 缓存的,会缓存内存的数据,长时间运行某一个进程,该进程所需的资源在缓存中被命中的概率越大,CPU 执行效率也会更高!

epoll三个关键函数实现原理

  1. epoll_create:创建epoll的内核对象struct eventpoll,与用户进程打开的文件句柄列表关联起来。

  • epollwait等待队列:存阻塞的进程,等待内核唤醒。
  • 就绪描述符队列:存socket连接描述符(就绪的描述符)。
  • 红黑树:struct epitem理解为一个连接,交给底层管理。
  1. epoll_ctl:将连接添加到eventpoll对象中,底层添加至红黑树中。
    epitem类似于“粘合剂”,可以关联到相应的socket,也可以关联到红黑树。

  1. epoll_wait:等待就绪的事件发生。

  • 就绪队列中有ready的socket:epoll_wait取走处理,用户进程不会被阻塞。
  • 就绪队列中没有ready的socket:定义等待队列,告诉内核自己需要“睡眠”,如果就绪队列中有socket准备好了,内核将自己“唤醒”,然后主动让出 CPU。

数据到达处理

  1. 保存数据到socket接收队列

  1. 就绪回调:ep_poll_callback

  • 将当前epitem添加到eventpoll的就绪队列中
  • 查看eventpoll的的等待队列上是否有在等待,如果有,就将其唤醒

百万用户高并发

设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接,而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?

在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完成后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

epoll的原理及优势:epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用B+树,磁盘IO消耗低,效率很高),将select/poll的调用分为如下三步:

  1. 调用epoll_create建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)。
  2. 调用epoll_ctl向epoll对象中添加这100万个连接的套接字。
  3. 调用epoll_wait收集发生的事件的fd资源。

因此,为实现如上场景,在进程启动时建立一个epoll对象,让后在需要的时候向这个epoll对象添加 / 删除连接。epoll_wait的效率非常高,在调用时,并未复制100万个连接的句柄数据,内核也无需遍历全部连接!

1
2
3
4
5
6
7
8
9
10
struct eventpoll{
/
*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*
/
struct rb_root rbr;
/
*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*
/
struct list_head rdlist;
};
Prev
2025-06-28 22:18:10
Next