I/O复用

oneNeko 于 2022-05-23 发布

I/O复用使得程序能同时监听多个文件描述符,这对提高程序性能至关重要。

注意:I/O复用本身是阻塞的,当有多个文件描述符就绪,将只能按顺序处理文件描述符,串行处理。要实现并发就得使用多进程或多线程等手段

I/O复用函数

I/O复用有select、poll、epoll三种系统调用,这三种系统调用都能同时监听多个文件描述符。他们将等待由timeout参数指定的超时时间,直到一个或多个文件描述符上由事件发生时返回。

这三组函数都通过某种结构体变量来告诉内核监听哪些文件描述符上的哪些事件,并使用该结构体类型的参数来获取内核处理的结果。

系统调用 select poll epoll
事件集合 用户通过传入3个参数分别传入感兴趣的可读、可写、异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪时间。这使得用户每次调用select都要重置这3个参数 统一处理所有事件类型,因此只需一个事件集参数。用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd.revents反馈其中就绪的事件 内核通过一个事件表直接管理用户的所有事件。epoll_wait系统调用的参数events禁用来反馈就绪的事件
索引就绪文件描述符的时间复杂度 O(n) O(n) O(1)
最大支持文件描述符数 一般用最大限制 65535 65535
工作模式 LT LT LT、ET
内核实现 轮询 轮询 回调

注意:select在高连接数高事件数时效率可能会比epoll高

epoll 函数

epoll使用一组函数来完成任务,其次epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需像select和poll那样每次调用都要重复传入文件描述符或事件集。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个时间表。这个文件描述符使用epoll_create函数

epoll_create

#include <sys/epoll.h>
int epoll_create(int size); // size参数并不起作用,只是给内核一个提示,告诉他事件表需要多大

epoll_ctl

epoll_ctl函数用来操作epoll的内核事件表:

#include <sys/epoll.h>
int epoll_ctl(int epfd,in op,int fd,struct epoll_event *event);

fd参数是要操作的文件描述符,op参数则指定操作类型:

event参数指定事件,epoll_event的定义如下:

struct epoll_event{
    __uint32_t events;
    epoll_data_t data;
}

typedef union epoll_data{
    void* ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
}epoll_data_t;

其中events描述事件类型:

epoll_data_t是一个联合体,四个成员中使用最多的是fd,指定事件所属的目标文件描述符

epoll_wait

epoll_wait在一段超时时间内等待一组文件描述符上的事件

#include <sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event* events,int macevents,int timeout);

函数成功时返回就绪的文件描述符的个数,失败是返回-1并设置errno
参数:

LT和ET模式

LT模式是默认工作模式,此种模式下epoll相当于一个效率较高的poll。当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,引用可以不立即处理该事件,这样等到程序下一次调用epoll_wait时,epoll_wait还会继续通知该事件,直到该事件被处理

当往epoll内核时间表中注册一个文件描述符上的EPOLLET时间时,epoll将以ET模式来操作该文件描述符。当epoll_wait检测到有事件发生,并将此事件通知应用程序后,应用程序必须立即处理,因为后续将不再向应用通知。

ET模式很大程度上降低了同一个epoll事件被触发的次数,因此效率要比LT模式高。

但即便是ET模式,一个socket上的某个事件还是可能在并发中被触发多次,这是不期望出现的结果。因此,可以注册EPOLLONESHOT事件来实现只触发一次。EPOLLONESHOT最多触发在其上注册的一个可读、可写或者异常事件,除非使用epoll_ctl重置该文件描述符上注册的EPOLLONESHOT事件

参考

《Linux高性能服务器编程》第九章