0%

I/O 多路复用

为什么需要 I/O 多路复用

大部分应用都可以使用阻塞 I/O 模型就足够完成任务了。但是,有些应用需要满足以下的条件:

  • 使用非阻塞的方式检查文件描述符是否可以执行 I/O 操作。
  • 可以同时检查多个文件描述符。

我们可以使用非阻塞 I/O 或者多进程多线程的方式满足以上需求,但是又会带来新的问题:

  • 如果文件描述符很多,非阻塞 I/O 需要不停轮询每个文件描述符,造成 CPU 的浪费。
  • 如果每个文件描述符都创建一个新进程执行 I/O 操作,会带来开销过于昂贵的问题,包括创建进程、维护进程、父子进程间通信。
  • 多线程的方法虽然会占用较少的资源,但是正确地编写线程间通信代码是一项非常复杂的工作。

我们可以使用 I/O 多路复用技术解决上面提到的问题。

什么是 I/O 多路复用

I/O 多路复用允许同时监听多个文件描述符,找出任意一个文件描述符是否可以执行 I/O 操作。

I/O 多路本质上是一种同步操作。因为真正的 I/O 操作仍然是阻塞的。

I/O 多路复用在网络编程中有以下的应用场景:

  • 客户处理多个描述符(通常是网络套接字和交互式输入);
  • 客户同时处理多个套接字;
  • TCP 服务器既要处理监听套接字,又要处理已连接套接字;
  • 服务器需要同时处理不同协议的套接字(比如同时处理 TCP 连接和 UDP 连接)。

APIs: select/poll/epoll

Linux 环境下提供了三组 API 用于使用多路复用技术,分别是 select、poll、epoll。

select

1
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

每个参数有以下的含义:

  • readfds、writefds、exceptfds 都是指向文件集合的指针;
  • nfds 必须设置为三个描述符集合中的最大值加 1;
  • timeout 设置 select 的阻塞行为,如果指定为 NULL,select 会一直阻塞。

成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0。

select 存在以下的缺陷:

  • 每次调用,程序需要拷贝一份包含所有指定的文件描述符的数据结构到内核中。当检查大量文件描述符时,拷贝操作将会占用大量的 CPU 时间。
  • 每次调用,内核必须检查所有的文件描述符,是否处于就绪状态。如果文件描述符过多,该操作会消耗大量时间。
  • 程序必须检查返回的数据结构中的每个文件描述符,如果文件描述符过多,这个操作会耗费大量的时间。
  • select 使用的数据结构 fd_set 对于被检查的文件描述符有一个上限(FD_SETSIZE),在 Linux 下的默认值是 1024。

poll

1
int poll (struct pollfd *fds, unsigned int nfds, int timeout);

poll 的功能和 select 类似,不过 poll并没有设置被检查文件描述符的最大值

epoll

epoll 是 Linux 2.6 版本中提供的新的 API,是 select 和 poll 的升级版。epoll 使用 一个文件描述符同时监听多个描述符。每当注册新的描述符时,epoll 都会把它拷贝到一个内核数据结构中,可以避免每次监听时都要把所有监听文件描述符拷贝到内核中的时间开销。

epoll 提供了三个方法:epoll_create、epoll_ctl、epoll_wait。

epoll_create 创建一个 epoll 句柄。size 表示 epoll 支持的最大描述符个数,但是这个参数在某些 Linux 实现中已经没有意义。

1
int epoll_create(int size);

epoll_ctl 执行对 epoll 文件描述符集合的操作。

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

每个参数有以下的含义:

  • epfd:创建 epoll 对象时分配的文件描述符;
  • op:对文件描述符集合执行的操作。epoll 定义了以下三种操作:
    • EPOLL_CTL_ADD:添加文件描述符;
    • EPOLL_CTL_MID:修改文件描述符监听的事件;
    • EPOLL_CTL_DEL:删除文件描述符。
  • fd:需要监听的文件描述符;
  • event:需要监听的事件。

epoll_wait 等待时间的产生,并返回所有已就绪时间的信息。

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

和 select/poll 不同,epoll 采用类似于事件回调的方式获取已经发生的事件信息。当注册一个文件描述符(调用 epoll_ctl 时),内核将相应的设备和文件描述符建立回调关系,一旦事件就绪,内核就会调用相应的回调方法,epoll_wait 就可以收到通知,然后处理所有已经就绪的时间。

对比

epoll 的优势

  • 没有监听文件描述符数量的限制;
  • 只有当注册需要监听的文件描述符时,才会把 fd 拷贝到内核中。在调用 epoll_wait 时不会重复拷贝 fd。
  • 采用类似于事件回调的机制,不需要内核依次检查每个文件描述符相应事件是否就绪。当一个事件就绪时,注册的回调函数会将事件添加到就绪列表中,内核不需要一个一个检查文件描述符上的时间是否就绪。因此,epoll 性能不会随着监听 fd 数量的上升而下降。

使用场景

select 的 timeout 参数为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景。同时,select 可移植性强,如果有跨平台的需求可以考虑 select。

poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。

epoll 用于只在 Linux 上运行,并且并发连接数很高但是活跃连接比例不高的场景,因此更适用于长连接场景。如果连接数量不多,拷贝的文件描述符不多,不能体现出 epoll 的优势。如果活跃连接数多,变化频繁,并且连接都是短暂的,也不适用于 epoll。因为 epoll 中的所有描述符都存储在内核中,每次需要对描述符的状态改变都需要通过 epoll_ctl 进行系统调用,频繁系统调用降低效率。

参考