1. 为什么要 IO 复用
1.1 IO 操作
在 Linux 下,IO 操作主要就是操作fd,如 socket,文件,事件 fd 等
1.2 IO 操作的进化之路
IO 操作耗时,进而需要在单独线程运行
方案 1:
每个操作单独放一个线程
优点:实现简单
缺点:如果 IO 数量众多,导致多线程数量剧增,效率反而低下
方案 2:
专门找个线程轮询这些 fd 是否可读可写,可读或者可写后再单独创建一个线程执行操作
这就要求单线程轮询,同时需要 fd 的可读可写检查操作为非阻塞
优点:线程减少
缺点:轮询线程要么一直轮询占用 CPU,要么定时轮询,可能导致事件处理延迟
方案 3:
轮询线程监听所有 fd,没有事件轮询线程休眠,有事件轮询唤醒
2. 平台实现方式
1 select
2 pool
3 epoll
这里不详细介绍他们的区别,只说结论 epoll:
比如我们一共有 1000 个 fd 需要监测,当有事件来临时,select 不知道是哪个 fd 来了哪个事件,
进而需要全部遍历,而 epoll 准确的知道是哪个 fd 来了什么事件
3. epoll 使用
1. int epoll_create(int size);
创建一个监控和管理句柄 fd 的池子
size: 表示池子的大小
返回值表示:epool 本身也是个句柄
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
向 epoll 池中,op(添加删除更新)感兴趣的句柄 fd, 以及对应句柄感兴趣的事件
2.1 epfd:epoll 池句柄
2.2 op:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
2.3 fd:感兴趣的文件句柄
2.4 event:对 fd 句柄感兴趣的事件
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可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发
(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket
的话,需要再次把这个socket加入到EPOLL队列里
3.int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生
events:
用户提供的给内核写入事件的集合;
跟 epoll_ctl 传入的最后一个参数类型一样,当有事件发生时,内核内核会把发生的事件复制到 events 中
maxevents:最大的事件值
timeout:超时时间
4. epool 原理
1. 红黑树
每个监听的 fd 都对应一个红黑树节点,时间复杂度为 O(log n)保证效率
每个节点为一个 epitem 数据结构
2. 就绪队列
有事件发生的队列,数据结构为 epitem
从这里可以知道哪个 fd 发生了什么事件