socket的多路复用,指的是单个线程或进程中可以同时处理多个socket的 I/O 事件,可以提高整体效率和资源利用率,常见的多路复用机制包括select、poll和epoll。
以放羊为例:同步阻塞的方式相当于一个人放羊只可以管理一只,epoll的存在相当于一只“牧羊犬”,将大量羊管理起来,如果某只羊有什么需要,牧羊犬就通知给人,完成相应的处理。
epoll的工作机制:使用两个系统调用来操作,epoll_create
创建一个epoll 实例,epoll_ctl
增加、修改或删除要控制的文件描述符,epoll_wait
则是用于等待事件的发生。
编程过程中,建议使用epoll:
- epoll效率高,基于事件通知的方式解决轮询带来的性能瓶颈,处理大量文件描述符效率高。
- 不受描述符数量限制,select有文件描述符数量上限(通常 1024),epoll无限制。
- 内存拷贝少:epoll的系统调用仅在需要数据时进行内存拷贝,减少了系统开销。
select每次调用时都要重新传递所有文件描述符集合,并进行内存拷贝;
poll则需要传递整个文件描述符数组。
epoll每次只传递发生的事件,不需要传递所有文件描述符。 - 支持边沿触发:相比于select和poll的水平触发(LT),epoll还支持边沿触发(ET)。
LT 是默认的触发模式,处理器只要发现事件有未处理的数据就会再次通知;
ET 更高效,它只在状态变化(例如从无数据到有数据)时通知一次,开发难度稍大但可减少系统调用次数。
epoll核心原理
同步阻塞:服务端等待用户端的数据到达,无用户到达就一直阻塞。
多路复用(同时监听很多 TCP 连接等待数据就绪,复用的是进程):epoll_wait
同时监听百万连接的数据是否到达,这些连接上只要有数据到达,它就不会进入阻塞状态;无数据到达则阻塞。
多路复用相比于同步阻塞的优点:
- 减少进程上下文切换的开销。
- 让 CPU 缓存命中率更高,执行效率也更高:(CPU 硬件的原因)CPU 处理一个进程是有 L1、L2、L3 缓存的,会缓存内存的数据,长时间运行某一个进程,该进程所需的资源在缓存中被命中的概率越大,CPU 执行效率也会更高!
epoll三个关键函数实现原理
epoll_create
:创建epoll的内核对象struct eventpoll
,与用户进程打开的文件句柄列表关联起来。
epollwait
等待队列:存阻塞的进程,等待内核唤醒。- 就绪描述符队列:存socket连接描述符(就绪的描述符)。
- 红黑树:
struct epitem
理解为一个连接,交给底层管理。
epoll_ctl
:将连接添加到eventpoll
对象中,底层添加至红黑树中。epitem
类似于“粘合剂”,可以关联到相应的socket,也可以关联到红黑树。
epoll_wait
:等待就绪的事件发生。
- 就绪队列中有ready的socket:
epoll_wait
取走处理,用户进程不会被阻塞。 - 就绪队列中没有ready的socket:定义等待队列,告诉内核自己需要“睡眠”,如果就绪队列中有socket准备好了,内核将自己“唤醒”,然后主动让出 CPU。
数据到达处理
- 保存数据到socket接收队列
- 就绪回调:
ep_poll_callback
。
- 将当前
epitem
添加到eventpoll
的就绪队列中! - 查看
eventpoll
的的等待队列上是否有在等待,如果有,就将其唤醒!
百万用户高并发
设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接,而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?
在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完成后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。
epoll的原理及优势:epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用B+树,磁盘IO消耗低,效率很高),将select/poll的调用分为如下三步:
- 调用
epoll_create
建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)。 - 调用
epoll_ctl
向epoll对象中添加这100万个连接的套接字。 - 调用
epoll_wait
收集发生的事件的fd资源。
因此,为实现如上场景,在进程启动时建立一个epoll对象,让后在需要的时候向这个epoll对象添加 / 删除连接。epoll_wait
的效率非常高,在调用时,并未复制100万个连接的句柄数据,内核也无需遍历全部连接!
1 | struct eventpoll{ |