IO多路复用
所谓的I/O多路复用,就是可以监控多个socket上的IO请求。允许多个socket在可读或可写准备好时,应用能被通知到,这样应用就可以一次非阻塞的处理多个socket相关的IO请求。
IO多路复用的有三种实现方式:
Select
I/O复用模型早期用select实现。
int select (int n,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
select 函数可以实现IO多路复用。但是select在大规模网络环境下有如下的缺点:
- 可监控的socket有限(内部用数组保存)
- 要遍历所有的fd集合来确定是否相关事件
- 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
其有如下特点:
1. 监控的socket没有限制(内部用链表,当socket数量多时,性能下降明显)
2. 和select一样,要遍历所有的socket来检查相关事件的产生
3.epoll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd
epoll
epoll是目前使用最广泛的IO多路复用机制。它克服了select 和 epoll 的一些缺点。
- 其内部使用了 红黑树和链表机制,通过红黑树高效管理大量fd
- 通过内核的回调机制,直接把产生事件的fd添加到内部的链表中。从而不需要遍历所有的fd,直接返回发生fd
- 支持水平触发(Level Triger)和边沿触发(Edge Trigger)两种模式
水平触发指的是:当有事件发生时,如果应用没有完该IO,系统会不断的产生相关事件提醒应用去处理。
边沿触发指的是:当有事件发生时,只产生一次通知事件,需要应用一次性把该事件处理完。
在大规模高性能的网络编程中,一般都采用边沿触发的模式。
Epoll的使用流程如下
- 调用函数 epoll_create()系统调用。此调用返回一个fd 。
- epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
- epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。
IO 多路复用的模式
两种I/O多路复用模式:Reactor和Proactor
Reactor
Reactor 用于同步IO
对于linux, epoll 是同步操作,所以只能用Reactor模式, 其核心在于:当通过epoll触发事件后,只是表明数据就绪, 需要自己去在socket上接收数据。
Proactor
Proactor用于异步IO
对于Windows, IOCP是异步操作。当由事件触发时,其数据已经从socket上接收完毕并拷贝到应用的缓存区中,数据接收已经完成,通知应用可以使用该数据。
IO 多路复用多线程模型
Half-sync/Half-async模型
本模型主要实现如下:
- 有一个专用的独立线程(事件监听线程)调用epoll_wait 函数来监听网络IO事件
- 线程池(工作线程)用于处理网络IO事件 : 每个线程会有一个事件处理队列。
- 事件监听线程获取到 IO事件后,选择一个线程,把事件投递到该线程的处理队列,由该线程后续处理。
这里关键的一点是:如果选择一个线程?一般根据 socket 的 fd 来 hash映射到线程池中的线程。这里特别要避免的是:同一个socket不能有多个线程处理,只能由单个线程处理。
如图所示,系统有一个监听线程,一般为主线程 main_loop 调用 epoll_wait 来获取并产生事件,根据socket的 fd 的 hash算法来调度到相应的 线程,把事件投递到线程对应的队列中。工作线程负责处理具体的事件。
这个模型的优点是结构清晰,实现比较直观。 但也有如下的 不足:
- 生产事件的线程(main_loop线程) 和 消费事件的线程(工作者线程)访问同一个队列会有锁的互斥和线程的切换。
- main_loop是同步的,如果有线程的队列满,会阻塞main_loop线程,导致其它线程临时没有事件可消费。
Leader/Follower
当Leader监听到socket事件后:处理模式
1)指定一个Follower为新的Leader负责监听socket事件,自己成为Follower去处理事件
2)指定Follower 去完成相应的事件,自己仍然是Leader
由于Leader自己监听IO事件并处理客户请求,该模式不需要在线程间传递额外数据,也无需像半同步/半反应堆模式那样在线程间同步对请求队列的访问。
ceph Async 模型
在Ceph Async模型里,一个Worker类对应一个工作线程和一个事件中心EventCenter。 每个socket对应的AsyncConnection在创建时根据负载均衡绑定到对应的Worker中,以后都由该Worker处理该AsyncConnection上的所有的读写事件。
如图所示,在Ceph Async模型里,没有单独的main_loop线程,每个工作线程都是独立的,其循环处理如下:
- epoll_wait 等待事件
- 处理获取到的所有IO事件
- 处理所有时间相关的事件
- 处理外部事件
在这个模型中,消除了Half-sync/half-async的 队列互斥访问和 线程切换的问题。 本模型的优点本质上是利用了操作系统的事件队列,而没有自己去处理事件队列。