IO过程以及IO多路复用
操作系统上的IO就是用户空间和内核空间数据的交互,因此IO操作通常包含以下步骤
- 等待网络数据到达网卡(读就绪)
- 读取到内核缓冲区
- 从内核缓冲区复制数据到用户空间
- 从用户空间读数据
所谓的多路复用其实复用的只是线程
系统调用 | select | poll | epoll |
---|---|---|---|
事件集合 | 传入感兴趣的可读、可写以及异常事件,内核通过对这些参数修改来反馈其中的就绪事件,这使得用户每次调用select都要重置这3个参数 | 统一处理所有事件类型。通过pollfd.events传入所有事件,内核通过pollfd.revents反馈其中就绪事件 | 内核通过一个事件表接管用户所有事件,无需反复传入 |
索引就绪文件符时间复杂度 | O(n) | O(n) | O(1) |
最大支持文件描述符数 | 有最大值限制 | 没有最大值限制 | 没有最大值限制 |
工作模式 | LT | LT | ET |
内核实现与工作效率 | 轮询方式检测就绪事件O(n) | 轮询方式检测就绪事件O(n) | 回调方式检测就绪事件O(1) |
工作模式
在上表中出现的LT、ET两种模式,LT指的是Level Trigger,ET指的是Edge Trigger
- LT: 只要fd一直保持就绪,就会一直提醒用户程序操作
- ET: 仅会提示一次,直到下次数据流入前都不会提示
select
select的思想是预先传递一个socket列表,若列表中的socket都没有数据就挂起进程,直到有一个socket收到数据,唤醒进程
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
具体流程如下
- 对于进程A同时监听sock1、sock2、sock3,那么调用A后就会将A加入这3个socket的等待队列中
- 当任何一个socket收到数据后,就会将进程A从所有的等待队列中移除,加入到工作队列中
也就是说当进程A被唤醒后,至少知道有一个socket接收了数据。程序只需要遍历一遍socket列表,就可以得到就绪的socket
但每次调用select都要将进程加入所有要监听的socket等待队列,每次唤醒都要从所有队列移除,而且要知道哪个socket收到了数据还要再遍历一次,这效率就很低。
epoll
epoll在select基础上改良引入了两项措施
- 将维护等待队列和堵塞进程功能分离
- 采用就绪列表储存进程就绪socket
大概流程是epoll维护了储存需要监听socket的红黑树,以及就绪socket链表。当有socket数据过来时,加入该socket到就绪socket链表,然后进程遍历对应的就绪socket链表
linux epoll相关方法
// 内核生成一个epoll实例数据结构,并返回一个文件描述符。这个特定的描述符是epoll实例的句柄,接下来的两个接口以它为中心(即epfd形式参数)。
// size 表示最大监视的文件描述符数量。后续版本弃用
int epoll_create(int size);
// 添加正在监听的描述符,或者将其从红黑树中移除,或者修改要监听的文件描述符的最大值,尽管在Linux的后续版本中已经弃用了(另外,不要为大小传递0,否则会报告无效参数错误)。
// op参数描述了操作的类型。
// EPOLL_CTL_ADD:向需要监控的兴趣列表添加一个描述符
// EPOLL_CTL_DEL:从兴趣列表删除一个描述符
// EPOLL_CTL_MOD: 从兴趣列表修改一个描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
typedef union epoll_data {
void *ptr; // 指向自定义数据
int fd; // 注册的文件描述符
uint32_t u32;
uint64_t u64;
}epoll_data_t;
struct epoll_event {
// 描述epoll事件。常见epoll事件如下
// EPOLLIN: 描述符处于可读状态
// EPOLLOUT: 描述符处于可写状态
// EPOLLET:设置epoll事件通知方式为ET
// Epollonshot:第一次通知,之后不再监控
// EPOLLHUP:本地描述符生成一个挂起事件,即默认的监控事件
// EPOLLRDHUP:相反的描述符生成一个待决事件
// EPOLLPRI:由带外数据触发
// EPOLLERR:当描述符生成错误时触发,默认的检测事件
uint32_t event;
epoll_data_t data
}
// 阻塞等待注册事件的发生,返回事件数量,并将激活事件写入events数组(events大小与maxevents一致)
// timeout -1: 意味着调用将一直堵塞,直到文件描述符进入就绪状态,或者在返回之前捕获到信号
// 0 用于非堵塞状态检测描述符是否处于就绪状态
// >0 代表调用将持续最多超时事件,如果在此期间就绪或者捕获信号,则返回,否则直接超时
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
Ref
- https://www.sobyte.net/post/2022-04/epoll-efficiently/
- https://stackoverflow.com/questions/9162712/what-is-the-purpose-of-epolls-edge-triggered-option