I/O复用

I/O复用使得程序能同时监听多个文件描述符,这对提高程序的性能至关重要。通常网络程序在以下情况需要使用I/O复用技术:

  • 客户端程序要同时处理多个socket。
  • 客户端程序要同时处理用户输入和网络连接
  • TCP服务器要同时处理监听socket和连接socket。这是I/O复用使用最多的场合
  • 服务器要同时处理TCP请求和UDP请求
  • 服务器要同时监听多个端口,或者处理多种服务

Linux下实现I/O复用的系统调用主要有:select、poll、epoll

1、select 系统调用

select 系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件

#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
  • nfds 参数指定监听的文件描述符的总数
  • readfds, writefds, exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合

2、poll系统调用

poll系统调用和select类似,也是指定时间内轮询一定数据的文件描述符,以测试其中是否有就绪者

#include <poll.h>
int poll( struct pollfd* fds, nfds_t nfds, int timeout);
  • fds 参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件
  • nfds参数指定被监听事件集合fds的大小
  • timeout 参数指定poll的超时值单位是毫秒。当timeout 为-1时,poll调用将永远阻塞,直到某个事件发生;为0时,poll调用立即返回

3、epoll系列系统调用

epoll是Linux特有的I/O复用函数。它在实现和使用上与select、poll有很大的差异。首先,epoll使用一组函数来完成任务,而不是单个函数。其实,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或者事件集。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。

假设一个场合:100万用户同时与一个进程保持着TCP连接,而每个时刻只有几十个或者几百个TCP连接是活跃(接收TCP包),也就是说每个时刻,进程只需要处理100万连接中一小部分连接。

select与poll是直接把100万连接的套接字传给操作系统(用户态内存到内核态内存大量复制),而内核寻址也将是巨大的资源浪费。epoll不会这么做,而是在内核中申请一个简易的文件系统,把原先一个select或者poll调用分成3个部分:调用epoll_create建立一个epoll对象,调用epoll_ctl向epoll对象添加100万连接的套接字、调用epoll_wait收集发生事件的连接。这样只需要在进程启动建立1个epoll对象,并在需要的时候向它添加或者删除连接就可以了,因此,在实际收集事件时,epoll_wait的效率就非常高,因为调用epoll_wait时并没向它传递这100万个连接,内核也不需要去遍历全部的连接。

select 、poll和epll 的区别

系统调用

select

poll

epoll

事件集合

用户通过3个参数分别传入感兴趣的可读、可写、异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这3个参数

统一处理所有事件类型,因此只需一个事件集参数。用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd.events反馈其中就绪的事件

内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait时,无须反复传入用户感兴趣的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件

应用程序索引就绪文件描述符的时间复杂度

O(n)

O(n)

O(1)

最大支持文件描述符数

一般有最大值限制

65535

65535

工作模式

LT

LT

支持ET高效模式

内核实现和工作效率

采用轮询方式来检测就绪事件, O(n)

轮询方式来检测, O(n)

采用回调方式来检测就绪事件, O(1)