IO 多路复用

IO 多路复用是一种同步IO模型:

  • IO:网络I/O。
  • 多路:多个文件句柄。
  • 复用:一个线程可以监视多个文件句柄。一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作,没有文件句柄就绪就会阻塞应用程序,交出CPU。

最大优势是减少系统开销,不必创建过多的进程/线程。

IO 多路复用机制要想高效使用,一般还需要把 socket 设置成「非阻塞」模式:

socket 没有数据可读/可写时,应用层去 read/write socket 也不会阻塞住(内核会返回指定错误,应用层可继续重试),这样应用层就可以去处理其它业务逻辑,不会阻塞影响性能。

select

select会阻塞住监视3类:writefds(写)、readfds(读)、和exceptfds(异常)文件描述符,等有数据、可读、可写、出异常或超时、就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的IO读写操作。

优点:

  • 几乎在所有的平台上支持,跨平台支持性好

缺点:

  • 由于是采用 轮询 方式全盘扫描,会随着文件描述符FD数量增多而性能下降。
  • 监视/就绪涉及到用户态到内核态之间拷贝 ,并进行遍历。
  • 默认单个进程能监听的各种文件描述符 限制 是1024个,可修改宏定义,但是效率仍然慢。

poll

基本原理与select一致,也是 轮询+遍历,但是它没有最大连接数的限制,原因是它的fd集合是基于链表来存储的.

epoll

通过 epoll_ctl注册fd,一旦fd就绪就会通过 callback回调机制来激活对应fd,进行相关的io操作。epoll之所以高性能是得益于它的三个函数:

  • epoll_create: 系统启动时,在Linux内核里面申请一个 B+树 结构文件系统,返回epoll对象,也是一个fd
  • epoll_ctl: 每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数
  • epoll_wait: 轮训所有的callback集合,并完成对应的IO操作

优点:

  • 没fd这个限制,所支持的fd上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄
  • 效率提高,使用 回调 通知而不是轮询的方式,不会随着fd数目的增加效率下降
  • 内核和用户空间mmap同一块内存实现(mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间)

综合比较

相同点:select,poll,epoll本质上都是 同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

不同点:

select poll epoll
操作方式 遍历 遍历 回调
数据结构 bitmap 数组 红黑树
最大连接数 1024(x86)或 2048(x64) 无上限 无上限
最大文件描述符数 一般有最大值限制 65535 65535
fd就绪 从内核空间拷贝到用户空间 从内核空间拷贝到用户空间 触发回调不拷贝
工作模式 LT LT 支持ET高效模式
工作效率 线性遍历 O(n) 线性遍历 O(n) 事件通知方式 O(1)

思考题

在 Redis 事件驱动框架代码中,分别使用了 select 和 epoll 两种机制。

为什么 Redis 没有使用 poll 这一机制?

首先,select 并不是只有 Linux 才支持的,Windows 平台也支持。而 Redis 针对不同操作系统,会选择不同的 IO 多路复用机制来封装事件驱动框架。因为 epoll 性能优于 select 和 poll,所以 Linux 平台下,Redis 直接会选择 epoll。而 Windows 不支持 epoll 和 poll,所以会用 select 模型。

彦祖老师 wechat