I/O 多路转接epoll
目录
epoll初识
epoll的相关系统调用
epoll工作原理
1.epoll 的整体模型
2. 事件到达后的回调流程
回调机制的本质
epoll 底层的“回调队列(ready list + 等待队列)
就绪队列是如何被填充的(push)
2.1 订阅阶段(epoll_ctl ADD/MOD)
2.2 事件到达(底层唤醒)
就绪队列是如何被消费的(pop)
三个队列不要混淆
1. ep->rdllist(就绪队列,epoll 私有)
2. ep->wq(epoll 等待队列,系统通用机制)
3.底层文件的等待队列(struct file->f_op->poll_wait 注册的 wait_queue_head_t)
epoll服务器
epoll的优点
epoll工作方式
epoll初识
epoll也是系统提供的一个多路转接接口。
- epoll系统调用也可以让我们的程序同时监视多个文件描述符上的事件是否就绪,与select和poll的定位是一样的,适用场景也相同。
- epoll在命名上比poll多了一个e,这个e可以理解成是extend,epoll就是为了同时处理大量文件描述符而改进的poll。
- epoll在2.5.44内核中被引进,它几乎具备了select和poll的所有优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll的相关系统调用
epoll有三个相关的系统调用,分别是epoll_create、epoll_ctl和epoll_wait。
epoll_create
epoll_create函数用于创建一个epoll模型,该函数的函数原型如下:
int epoll_create(int size);
参数说明:
- size:自从Linux2.6.8之后,size参数是被忽略的,但size的值必须设置为大于0的值。
返回值说明:
- epoll模型创建成功返回其对应的文件描述符,否则返回-1,同时错误码会被设置。
注意: 当不再使用时,必须调用close函数关闭epoll模型对应的文件描述符,当所有引用epoll实例的文件描述符都已关闭时,内核将销毁该实例并释放相关资源。
epoll_ctl
epoll_ctl函数用于向指定的epoll模型中注册事件,该函数的函数原型如下:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
- epfd:指定的epoll模型。
- op:表示具体的动作,用三个宏来表示。
- fd:需要监视的文件描述符。
- event:需要监视该文件描述符上的哪些事件。
第二个参数op的取值有以下三种:
- EPOLL_CTL_ADD:注册新的文件描述符到指定的epoll模型中。
- EPOLL_CTL_MOD:修改已经注册的文件描述符的监听事件。
- EPOLL_CTL_DEL:从epoll模型中删除指定的文件描述符。
返回值说明:
函数调用成功返回0,调用失败返回-1,同时错误码会被设置。
第四个参数对应的struct epoll_event结构如下:
struct epoll_event结构中有两个成员,第一个成员events表示的是需要监视的事件,第二个成员data是一个联合体结构,一般选择使用该结构当中的fd,表示需要监听的文件描述符。
events的常用取值如下:
- EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。
- EPOLLOUT:表示对应的文件描述符可以写。
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)。
- EPOLLERR:表示对应的文件描述符发送错误。
- EPOLLHUP:表示对应的文件描述符被挂断,即对端将文件描述符关闭了。
- EPOLLET:将epoll的工作方式设置为边缘触发(Edge Triggered)模式。
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听该文件描述符的话,需要重新将该文件描述符添加到epoll模型中
epoll_wait函数
epoll_wait函数用于收集监视的事件中已经就绪的事件,该函数的函数原型如下:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数说明:
- epfd:指定的epoll模型。
- events:内核会将已经就绪的事件拷贝到events数组当中(events不能是空指针,内核只负责将就绪事件拷贝到该数组中,不会帮我们在用户态中分配内存)。
- maxevents:events数组中的元素个数,该值不能大于创建epoll模型时传入的size值。
- timeout:表示epoll_wait函数的超时时间,单位是毫秒(ms)。
参数timeout的取值:
- -1:epoll_wait调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
- 0:epoll_wait调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,epoll_wait检测后都会立即返回。
- 特定的时间值:epoll_wait调用后在直到的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后epoll_wait进行超时返回。
返回值说明:
- 如果函数调用成功,则返回有事件就绪的文件描述符个数。
- 如果timeout时间耗尽,则返回0。
- 如果函数调用失败,则返回-1,同时错误码会被设置。
epoll_wait调用失败时,错误码可能被设置为:
- EBADF:传入的epoll模型对应的文件描述符无效。
- EFAULT:events指向的数组空间无法通过写入权限访问。
- EINTR:此调用被信号所中断。
- EINVAL:epfd不是一个epoll模型对应的文件描述符,或传入的maxevents值小于等于0。
epoll工作原理
红黑树+就绪队列+回调机制
epoll模型=红黑树+就绪队列+回调机制
1.epoll 的整体模型
-
epoll 内部用红黑树(rb-tree)管理所有被监控的文件描述符(fd)。
-
每个节点(
struct rb_node
)记录:-
int fd
—— 要监控的文件描述符; -
uint32_t events
—— 关心的事件,如EPOLLIN/EPOLLOUT
; -
uint32_t revents
—— 实际触发的事件; -
link
指向就绪队列。
-
-
-
红黑树的作用:快速查找 / 增加 / 删除某个 fd 的注册信息,复杂度 O(log n)。
-
就绪队列(ready list):一个双向链表,保存“已经有事件发生”的 fd。
OS自动检测fd就绪”“自动进行回调”就是指内核检测到事件后,把对应节点加入就绪队列。
epoll 的数据结构设计
在 epoll
内核实现中,核心有两个数据结构:
-
红黑树(rbtree)
-
用来存储所有用户关心的 监控目标文件描述符(fd)。
-
这是一个查找树,保证
O(log n)
的插入/删除/查找效率。 -
每个注册的 fd 都会对应一个节点插入红黑树。
-
树的作用是“监控集合”,表示哪些 fd 已经被 epoll 关注。
-
-
就绪队列(ready list)
-
用来保存真正触发了事件的 fd。
-
就绪队列是一个链表(双向链表),插入/删除复杂度是
O(1)
。 -
作用是“结果集合”,表示哪些 fd 已经发生了 I/O 事件,可以立刻返回给用户。
-
“节点即属于树又属于就绪队列”
其实这里容易混淆,内核实现并不是直接把“同一个节点”放在两个容器里,而是:
-
红黑树节点(表示 fd 被监控)在
epoll
创建时就存在。 -
当该 fd 触发事件时,
ep_poll_callback()
会把对应的 红黑树节点的指针 挂到就绪队列里。
换句话说:
👉 红黑树负责管理 “是否被关注”。
👉 就绪队列负责管理 “是否触发事件”。
👉 节点本身的数据结构设计里有双重信息:它既能链接到红黑树,也能链接到就绪队列(通过内核里常见的嵌入式链表/结构体复用手法)。
2. 事件到达后的回调流程
1.用户注册关注事件
-
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev)
-
内核在红黑树中插入一个
rb_node
,记录 fd 和用户想监听的事件(ev.events
)。
2.内核检测 I/O 状态变化
-
网络协议栈或驱动层,当某个 fd 上有数据可读、可写或异常时,触发底层的回调(callback/hook)。
-
这个回调会找到该 fd 对应的
rb_node
。
3.激活红黑树节点,挂入就绪队列
-
内核把
revents
填上实际触发的事件(比如EPOLLIN
),并把这个节点挂到 epoll 的就绪队列尾部。 -
就绪队列本质上是一个链表,OS 负责维护,用户态不用关心。
4.用户调用 epoll_wait()
-
epoll_wait()
会扫描就绪队列,把里面的 fd 和事件拷贝到用户空间数组返回。 -
返回后,就绪队列上被取出的节点会被清空/复用,等待下次事件。
回调机制的本质
epoll 底层的“回调队列(ready list + 等待队列)
拆开到内核对象与关键路径,帮你看清楚它到底怎么“挂入、唤醒、取出”的。以下基于主线 Linux(5.x/6.x 代整体一致,字段名可能略有差异,但机制相同)
涉及到的核心数据结构(简化版)
struct eventpoll {struct mutex mtx; // 用户态操作互斥spinlock_t lock; // 快速路径自旋锁struct list_head rdllist; // ✅ 回调“就绪队列”:已就绪 epitem 链表wait_queue_head_t wq; // ✅ epoll_wait() 线程在此睡眠/被唤醒struct rb_root_cached rbr; // 监控集合:所有 fd 的 epitem(红黑树)
};struct epitem {struct eventpoll *ep; // 所属的 ep 实例struct file *file; // 被监控的底层 filestruct list_head rdllink; // ✅ 若就绪则挂到 ep->rdlliststruct list_head pwqlist; // 订阅的底层“等待队列(Wait Queue)”列表unsigned int eventmask; // 用户关心的 eventsunsigned int revents; // 实际触发的 eventsbool ready; // 是否已在 rdllist
};struct eppoll_entry { // “订阅关系”的一次绑定struct epitem *epi;struct wait_queue_entry wait; // wait.func = ep_poll_callbackstruct list_head llink; // 挂到 epi->pwqlist
};
关键点:
-
rdllist 就是 epoll 的“就绪队列”,放的是
epitem
; -
wq
是 epoll_wait 的睡眠队列,当 rdllist 非空或超时到达时把等待线程唤醒; -
pwqlist
存放与底层驱动/协议栈 wait queue 的订阅(eppoll_entry
回调注册对象),它们负责在事件发生时调用 ep 的回调函数。
就绪队列是如何被填充的(push)
2.1 订阅阶段(epoll_ctl ADD/MOD)
- 用户
epoll_ctl(ADD)
:创建epitem
,放入ep->rbr
(红黑树)。 - 调用底层文件的
->poll()
(TCP、TTY、磁盘……)以“注册等待” ->poll()
在看到 epoll 提供的queue_proc
(ep_ptable_queue_proc)
后,会把eppoll_entry
绑到该文件的 wait queue 上:
- epoll 提供的
queue_proc
实现是ep_ptable_queue_proc,作用是
分配一个 eppoll_entry -
设置
wait.func = ep_poll_callback
;(回调函数) -
同时把
eppoll_entry
加入epi->pwqlist
,以便后续解除订阅/清理。
含义:epoll 把自己的回调函数“挂”在了底层事件源的等待队列上。
->poll()(TCP、TTY、磁盘……)以“注册等待?
在 Linux 内核里,一切都是文件。
无论是 TCP 套接字、TTY 终端,还是磁盘文件,用户空间拿到的都是一个 fd。
每个 fd
在内核中对应一个 struct file
,里面有一套操作函数表 struct file_operations f_op
,包括:
-
.read
-
.write
-
.poll
-
……
👉 其中 .poll
就是和 I/O 多路复用(select/poll/epoll
)打交道的函数。
epoll_ctl(ADD) 时会做什么?
当你把一个 fd
加入 epoll(epoll_ctl(ADD)
)时,内核会调用:
file->f_op->poll(file, ep_pt, &poll_table)
这里的 file->f_op->poll()
就是 底层设备驱动提供的 poll 方法。
不同设备类型有不同实现:
-
TCP socket →
tcp_poll()
-
TTY 终端 →
tty_poll()
-
磁盘文件 →
vfs_poll()
-
……
“注册等待”是什么意思?
调用 .poll()
的时候,会传入一个 poll_table 参数,里面封装了 poll_wait()
函数。
驱动在实现 .poll()
时,如果发现这个文件可能会阻塞(比如 socket 的接收缓冲区现在是空的),就会调用:
poll_wait(file, &socket->wait_queue, poll_table);
这一步做了两件事:
1.把 epoll 的 eppoll_entry(含 ep_poll_callback)挂到底层文件的等待队列上。
-
等待队列就是
wait_queue_head_t
,是 Linux 通用的睡眠/唤醒机制。 -
相当于对设备说:“以后你有数据了,记得叫我一声”。
2.返回当前文件的就绪状态(掩码)。
-
如果现在已经可读,就会立刻返回
POLLIN
。 -
如果不可读,那就等以后事件发生,由等待队列回调通知。
为什么叫“注册等待”?
-
这个过程并不是去真正“等待”,而是 告诉底层驱动:“我(epoll)要等你这个 fd,有变化请通过等待队列回调通知我”。
-
就像去餐厅点菜,
->poll()
就是和服务员打招呼:“我点的菜做好了请叫我”。 -
epoll 自己不会傻傻去轮询所有 fd,而是把回调注册到各个 fd 的等待队列里,真正的事件发生时由底层主动唤醒
2.2 事件到达(底层唤醒)
当网络协议栈/驱动检测到 fd 可读/可写/异常:
-
底层调用
wake_up()
→ 依次执行队列中每个wait_queue_entry.func
; -
对 epoll 来说,执行的是
ep_poll_callback()
。它做两件事(在ep->lock
保护下):-
读取底层
->poll()
的返回掩码,计算与epi->eventmask
的交集,写入epi->revents
; -
如果
epi
还不在就绪队列,把epi->rdllink
挂到ep->rdllist
尾部、置epi->ready=true
;
-
-
回调末尾 唤醒
ep->wq
,让epoll_wait()
里的线程被调度运行。
这一步就是“回调队列被填充”的真正来源:底层 wait queue → ep_poll_callback → rdllist。
就绪队列是如何被消费的(pop)
当用户线程调用 epoll_wait()
:
-
若
rdllist
为空 → 线程睡到ep->wq
(或到超时)。 -
被唤醒或本就非空时,进入
ep_send_events()
/ep_scan_ready_list()
:-
在
ep->lock
下,从rdllist
取若干epitem
; -
根据触发模式(LT/ET、
EPOLLONESHOT
)决定:-
LT(level-triggered):若条件仍满足,可继续留在 rdllist 或稍后再次加入;
-
ET(edge-triggered):从 rdllist 删除并清
epi->ready
,需要用户把数据读空,下一次“边沿”才会再回调; -
ONESHOT:清除
epi->eventmask
中可触发位,使其暂停触发,直到epoll_ctl(MOD)
重新 armed。
-
-
把 (
fd
,epi->revents
) 拷给用户态数组返回。
-
这一步是“就绪队列的消费”。消费完成就决定是否“复位/重挂”。
三个队列不要混淆
1. ep->rdllist(就绪队列,epoll 私有)
-
归属:属于 epoll 实例 (
struct eventpoll
) 内部的数据结构。 -
作用:保存触发了事件的 epitem(即某个 fd 的 epoll 节点)。
-
范围:这是 epoll 独有的,不是系统通用队列。
-
用户可见性:
epoll_wait()
返回的事件就是从这里取出来的。
👉 可以把它看作 epoll 的事件缓存池。
2. ep->wq(epoll 等待队列,系统通用机制)
-
归属:这是 Linux 内核的 等待队列(wait queue)机制,但在 epoll 中被用来保存“等待事件的进程”。
-
作用:当
epoll_wait()
被调用,而rdllist
为空时,调用进程会挂到ep->wq
里阻塞。 -
范围:这是内核通用的等待队列,用于进程睡眠/唤醒,不是 epoll 特有。
👉 可以看作 谁在等 epoll 的结果。
3.底层文件的等待队列(struct file->f_op->poll_wait 注册的 wait_queue_head_t)
-
归属:属于具体的 底层设备/文件(比如 socket、管道、字符设备)。
-
作用:当文件状态(可读/可写/异常)发生变化时,会通过这个等待队列唤醒挂上去的“等待者”。
-
在 epoll 中的用法:
-
当 epoll 关注某个 fd 时,会调用
poll_wait()
,把 epoll 的回调函数 挂到底层文件的等待队列上。 -
一旦底层文件有事件,内核就会调用回调 → 把 epitem 放进
rdllist
→ 唤醒ep->wq
。
-
👉 可以看作 谁在等 fd 的变化。
三个队列的关系:
┌─────────────────────────────── 用户态 ───────────────────────────────┐
│ │
│ epoll_ctl(ADD fd, events) epoll_wait() │
│ ────────────────────────┐ ┌───────────────┐ │
│ │ │ │ │
└────────────────────────────┼───────────┼───────────────┼────────────┘│ │ │▼ ▼ ▼
┌────────────────────────────┴───────────┴───────────────┴────────────┐
│ 内核(epoll 核心) │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ struct eventpoll │ │
│ │ │ │
│ │ rbr ──▶ [红黑树] 所有已注册的 fd │ │
│ │ │ │
│ │ rdllist ──▶ [就绪队列] │◀───────────┐ │
│ │ ↑ │ │ │
│ │ │ (ep_poll_callback) │ │ │
│ │ wq ──▶ [epoll_wait 睡眠队列] │ │ │
│ └─────────────────────────────────────────┘ │ │
│ │ │
└───────────────────────────────────────────────────────────────────┘ ││(1) 注册 fd → 在底层驱动处挂回调 ││
┌────────────────────────── 底层文件/驱动/协议栈 ──────────────────────┐
│ │
│ ┌───────────────┐ ┌───────────────────────────────┐ │
│ │ fd (socket) │ │ wait_queue_head_t (底层等待队列)│ │
│ └───────────────┘ └───────────────────────────────┘ │
│ │ 注册回调 (ep_ptable_queue_proc) │
│ ▼ │
│ 数据到达/状态变化 → wake_up() │
│ │ │
│ └── 调用 ep_poll_callback() │
│ │ │
│ ├─ 设置 epitem->revents │
│ └─ 把 epitem 加到 ep->rdllist (就绪队列) │
│ 并唤醒 ep->wq(睡眠的 epoll_wait 线程) │
└─────────────────────────────────────────────────────────────────────┘
epoll服务器
为了简单演示一下epoll的使用方式,这里我们也实现一个简单的epoll服务器,该服务器也只是读取客户端发来的数据并进行打印
EpollServer类
class EpollServer
{const static int size = 64;const static int defaultfd = -1;public:EpollServer(int port) : _listensock(std::make_unique<TcpSocket>()), _isrunning(false), _epfd(defaultfd){// 1. 创建listensocket_listensock->BuildTcpSocketMethod(port); // 3// 2. 创建epoll模型_epfd = epoll_create(256);if (_epfd < 0){LOG(LogLevel::FATAL) << "epoll_create error";exit(EPOLL_CREATE_ERR);}LOG(LogLevel::INFO) << "epoll_create success: " << _epfd; // 4// 3. 将listensocket设置到内核中!struct epoll_event ev; // 有没有设置到内核中,有没有rb_tree中新增节点??没有!!ev.events = EPOLLIN;ev.data.fd = _listensock->Fd(); // TODO : 这里未来是维护的是用户的数据,常见的是fd// ev.data.ptr = _listensock.get();int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->Fd(), &ev);if (n < 0){LOG(LogLevel::FATAL) << "add listensockfd failed";exit(EPOLL_CTL_ERR);}}void Start(){int timeout = -1;_isrunning = true;while (_isrunning){// 能不能直接accept呢??不能!应该干什么?int n = epoll_wait(_epfd, _revs, size, timeout);switch (n){case 0:LOG(LogLevel::DEBUG) << "timeout...";break;case -1:LOG(LogLevel::ERROR) << "epoll error";break;default:Dispatcher(n);break;}}_isrunning = false;}// 事件派发器void Dispatcher(int rnum){LOG(LogLevel::DEBUG) << "event ready ..."; // LT: 水平触发模式--epoll默认for (int i = 0; i < rnum; i++){// epoll也要循环处理就绪事件--这是应该的,本来就有可能有多个fd就绪!int sockfd = _revs[i].data.fd;uint32_t revent = _revs[i].events;if (revent & EPOLLIN){ // 读事件就绪// listensockfd ready? normal socfd ready??if (sockfd == _listensock->Fd()){// 读事件就绪 && 新连接到来Accepter();}else{// 读事件就绪 && 普通socket可读Recver(sockfd);}}// if(_revs[i].events & EPOLLOUT)// {// 写事件就绪// }}}// 链接管理器void Accepter(){InetAddr client;// 新连接到来 --- 至少有一个连接到来 --- accept一次 --- 绝对不会阻塞int sockfd = _listensock->Accept(&client); // accept会不会阻塞? 0 or 1if (sockfd >= 0){// 获取新链接到来成功, 然后呢??能不能直接// read/recv(), sockfd是否读就绪,我们不清楚// 只有谁最清楚,未来sockfd上是否有事件就绪?select!// 将新的sockfd,托管给select!// 如何托管? 将新的fd放入辅助数组!LOG(LogLevel::INFO) << "get a new link, sockfd: "<< sockfd << ", client is: " << client.StringAddr();// 能不能直接recv??? 不能!!!// 将新的sockfd添加到内核!struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = sockfd;int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);if (n < 0){LOG(LogLevel::WARNING) << "add listensockfd failed";}else{LOG(LogLevel::INFO) << "epoll_ctl add sockfd success: " << sockfd;}}}// IO处理器void Recver(int sockfd){char buffer[1024];// 我在这里读取的时候,会不会阻塞? 本次读取,不会被阻塞ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); // recv写的时候有bug吗?if (n > 0){buffer[n] = 0;std::cout << "client say@ " << buffer << std::endl;}else if (n == 0){LOG(LogLevel::INFO) << "clien quit...";// 2. 从epoll中移除fd的关心 && 关闭fd -- 细节:epoll_ctl: 只能移除合法fd -- 先移除,在关闭!!int m = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);if(m > 0){LOG(LogLevel::INFO) << "epoll_ctl remove sockfd success: " << sockfd;}close(sockfd);}else{LOG(LogLevel::ERROR) << "recv error";// 2. 关闭fdint ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);if(ret > 0){LOG(LogLevel::INFO) << "epoll_ctl remove sockfd success: " << sockfd;}close(sockfd);}}void Stop(){_isrunning = false;}~EpollServer(){_listensock->Close();if (_epfd > 0)close(_epfd);}private:std::unique_ptr<Socket> _listensock;bool _isrunning;int _epfd;struct epoll_event _revs[size];
};
epoll的优点
- 接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效。
- 数据拷贝轻量:只在新增监视事件的时候调用epoll_ctl将数据从用户拷贝到内核,而select和poll每次都需要重新将需要监视的事件从用户拷贝到内核。此外,调用epoll_wait获取就绪事件时,只会拷贝就绪的事件,不会进行不必要的拷贝操作。
- 事件回调机制:避免操作系统主动轮询检测事件就绪,而是采用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中。调用epoll_wait时直接访问就绪队列就知道哪些文件描述符已经就绪,检测是否有文件描述符就绪的时间复杂度是O ( 1 ) O(1)O(1),因为本质只需要判断就绪队列是否为空即可。
- 没有数量限制:监视的文件描述符数目无上限,只要内存允许,就可以一直向红黑树当中新增节点。
注意:
- 有人说epoll中使用了内存映射机制,内核可以直接将底层就绪队列通过mmap的方式映射到用户态,此时用户就可以直接读取到内核中就绪队列当中的数据,避免了内存拷贝的额外性能开销。
- 这种说法是错误的,实际操作系统并没有做任何映射机制,因为操作系统是不相信任何人的,操作系统不会让用户进程直接访问到内核的数据的,用户只能通过系统调用来获取内核的数据。
- 因此用户要获取内核当中的数据,势必还是需要将内核的数据拷贝到用户空间。
与select和poll的不同之处
- 在使用select和poll时,都需要借助第三方数组来维护历史上的文件描述符以及需要监视的事件,这个第三方数组是由用户自己维护的,对该数组的增删改操作都需要用户自己来进行。
- 而使用epoll时,不需要用户自己维护所谓的第三方数组,epoll底层的红黑树就充当了这个第三方数组的功能,并且该红黑树的增删改操作都是由内核维护的,用户只需要调用epoll_ctl让内核对该红黑树进行对应的操作即可。
- 在使用多路转接接口时,数据流都有两个方向,一个是用户告知内核,一个是内核告知用户。select和poll将这两件事情都交给了同一个函数来完成,而epoll在接口层面上就将这两件事进行了分离,epoll通过调用epoll_ctl完成用户告知内核,通过调用epoll_wait完成内核告知用户
epoll工作方式
epoll有两种工作方式,分别是水平触发工作模式和边缘触发工作模式。
水平触发(LT,Level Triggered
- 只要底层有事件就绪,epoll就会一直通知用户。
- 就像数字电路当中的高电平触发一样,只要一直处于高电平,则会一直触发。
epoll默认状态下就是LT工作模式。
- 由于在LT工作模式下,只要底层有事件就绪就会一直通知用户,因此当epoll检测到底层读事件就绪时,可以不立即进行处理,或者只处理一部分,因为只要底层数据没有处理完,下一次epoll还会通知用户事件就绪。
- select和poll其实就是工作是LT模式下的。
- 支持阻塞读写和非阻塞读写。
边缘触发(ET,Edge Triggered)
- 只有底层就绪事件数量由无到有或由有到多发生变化的时候,epoll才会通知用户。
- 就像数字电路当中的上升沿触发一样,只有当电平由低变高的那一瞬间才会触发。
如果要将epoll改为ET工作模式,则需要在添加事件时设置EPOLLET选项。
- 由于在ET工作模式下,只有底层就绪事件无到有或由有到多发生变化的时候才会通知用户,因此当epoll检测到底层读事件就绪时,必须立即进行处理,而且必须全部处理完毕,因为有可能此后底层再也没有事件就绪,那么epoll就再也不会通知用户进行事件处理,此时没有处理完的数据就相当于丢失了。
- ET工作模式下epoll通知用户的次数一般比LT少,因此ET的性能一般比LT性能更高,Nginx就是默认采用ET模式使用epoll的。
- 只支持非阻塞的读写。
为什么ET模式工作在非阻塞的环境下?
当ET底层缓冲区有数据后,ET只会通知用户进程一次,逼着用户一次要把缓冲区的数据读完,但是用户并不知道缓冲区的数据大小,只能进行循环读取recv,一旦底层缓冲区的数据被读完后,再调用recv如果是阻塞模式就被阻塞了,服务器就会被挂起