- 1. epoll 核心设计思想与数据结构
- 2. 三大核心系统调用源码深度剖析
- 2.1 epoll_create / epoll_create1
- 2.2 epoll_ctl (重点:事件控制与回调注册)
- 2.3 epoll_wait
- 3. 关键驱动机制:`ep_poll_callback` 的触发流程
- 4. LT(水平触发)与 ET(边缘触发)内核实现差异
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
内核IO多路复用epoll机制简介
1. epoll 核心设计思想与数据结构
传统 select/poll 的根本缺陷在于:每次调用都需要将包含大量文件描述符(FD) combat 集合的数组从用户态拷贝到内核态,并且内核需要线性遍历整个集合来检查就绪状态,时间复杂度为 O(N)。
epoll 采用了空间换时间以及事件驱动的设计思想。它在内核中维护了一个持久化的上下文,将“事件注册”与“事件等待”分离开来。
epoll 的底层数据结构主要依赖于红黑树(Red-Black Tree)和双向链表(Doubly Linked List):
- 红黑树:用于高效管理(增删改查)用户注册的所有 FD,保证了操作的时间复杂度为 O(logN)。
- 就绪链表:用于存放当前已经有事件触发的 FD。当进程调用
epoll_wait 时,内核只需直接检查该链表是否为空,时间复杂度为 O(1)。
核心结构体定义
在 Linux 内核源码 fs/eventpoll.c 中,最关键的结构体是 struct eventpoll 和 struct epitem。
// 代表一个 epoll 实例的内核对象
struct eventpoll {
/* 保护此结构体的自旋锁 */
spinlock_t lock;
/* 互斥锁,用于在修改 epoll 结构时进行同步 */
struct mutex mtx;
/* 等待队列,用于存放调用 epoll_wait 陷入阻塞的进程 */
wait_queue_head_t wq;
/* 用于 epoll 自身作为被监听对象(嵌套 epoll)时的等待队列 */
wait_queue_head_t poll_wait;
/* 就绪双向链表,存放所有就绪事件的 epitem */
struct list_head rdllist;
/* 红黑树根节点,用于管理所有被监听的 file */
struct rb_root_cached rbr;
/* 这是一个单链表,保存着执行完 epoll_wait 准备向用户空间复制,
但由于某些原因(如 LT 模式)需要重新放回 rdllist 的节点 */
struct epitem *ovflist;
/* 属性:创建该实例的用户信息 */
struct user_struct *user;
/* 对应的 file 结构体指针 */
struct file *file;
// ... 省略部分统计与优化字段
};
每个被监听的文件描述符在内核中都会对应一个 struct epitem 结构体:
// 代表红黑树中的一个节点(即一个被监听的 FD)
struct epitem {
/* 红黑树节点,用于将当前结构挂载到 eventpoll 的 rbr 红黑树中 */
struct rb_node rbn;
/* 双向链表节点,用于将当前结构挂载到 eventpoll 的 rdllist 就绪链表中 */
struct list_head rdllink;
/* 处于下一级单链表 ovflist 的指针 */
struct epitem *next;
/* 包含被监听的 fd 和对应的 file 结构体指针 */
struct epoll_filefd ffd;
/* 轮询等待队列的个数 */
int nwait;
/* 包含当前项的等待队列项列表 */
struct list_head pwqlist;
/* 指向所属的 eventpoll 对象 */
struct eventpoll *ep;
/* 处于循环链表中的节点 */
struct list_head fllink;
/* 产生事件时的回调函数绑定的侦听器 */
struct wakeup_source __rcu *ws;
/* 用户注册的目标事件类型及私有数据 */
struct epoll_event event;
};
2. 三大核心系统调用源码深度剖析
epoll 体系由 epoll_create、epoll_ctl、epoll_wait 三个系统调用组成。以下结合内核源码分析其执行流程。
2.1 epoll_create / epoll_create1
epoll_create 核心任务是分配 struct eventpoll 内存,并创建一个匿名的 file 结构体,将其与一个未使用的文件描述符(fd)绑定,最后返回该 fd 给用户。这样设计可以使用户像操作普通文件一样操作 epoll(如 close)。
内核入口函数通常为 SYSCALL_DEFINE1(epoll_create1, int, flags),其内部调用 do_epoll_create:
static int do_epoll_create(int flags) {
int error;
struct eventpoll *ep = NULL;
struct file *file;
int fd;
/* 检查非法 flags */
if (flags & ~EPOLL_CLOEXEC)
return -EINVAL;
/* 1. 分配 eventpoll 内存并初始化各数据结构 */
error = ep_alloc(&ep);
if (error < 0)
return error;
/* 2. 获取一个未使用的文件描述符 fd */
fd = get_unused_fd_flags(O_CLOEXEC | (flags & EPOLL_CLOEXEC));
if (fd < 0) {
error = fd;
goto out_free_ep;
}
/* 3. 创建匿名匿名文件,绑定 eventpoll_fops 操作集 */
/* 用户操作这个 fd 时的 read/write 等重定向到 epoll 的特有操作 */
file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
O_RDWR | (flags & EPOLL_CLOEXEC));
if (IS_ERR(file)) {
error = PTR_ERR(file);
goto out_put_fd;
}
ep->file = file;
/* 4. 将 fd 与 file 建立映射,放入当前进程的 fd 数组中 */
fd_install(fd, file);
return fd;
out_put_fd:
put_unused_fd(fd);
out_free_ep:
ep_free(ep);
return error;
}
2.2 epoll_ctl (重点:事件控制与回调注册)
epoll_ctl 负责对红黑树进行 ADD、MOD、DEL 操作。其中 EPOLL_CTL_ADD 是最核心、最复杂的逻辑,它实现了底层驱动事件与 epoll 回调的对接。
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
struct epoll_event __user *, event) {
struct error_check;
struct fd f, tf;
struct eventpoll *ep;
struct epitem *epi;
struct epoll_event epds;
/* 从用户空间拷贝监听事件配置 */
if (ep_op_has_event(op) && copy_from_user(&epds, event, sizeof(epds)))
return -EFAULT;
f = fdget(epfd); // 获取 epoll 本身的 file
tf = fdget(fd); // 获取目标被监听文件的 file
ep = f.file->private_data;
mutex_lock_nested(&ep->mtx, 0);
/* 在红黑树中查找该 fd 是否已经存在 */
epi = ep_find(ep, tf.file, fd);
switch (op) {
case EPOLL_CTL_ADD:
if (!epi) {
epds.events |= EPOLLERR | EPOLLHUP; // 默认监听错误事件
/* 核心函数:向红黑树插入节点,并挂载回调 */
error = ep_insert(ep, &epds, tf.file, fd, full_check);
} else {
error = -EEXIST;
}
break;
case EPOLL_CTL_DEL:
if (epi)
error = ep_remove(ep, epi); // 从红黑树及就绪链表移除
else
error = -ENOENT;
break;
case EPOLL_CTL_MOD:
if (epi) {
/* 修改已有节点的监听事件 */
error = ep_modify(ep, epi, &epds);
} else {
error = -ENOENT;
}
break;
}
mutex_unlock(&ep->mtx);
// ... 省略资源释放
return error;
}
深入分析 ep_insert 机制
ep_insert 内部通过调用目标文件的 poll 机制(针对网络套接字通常是 sock_poll,底层调用 tcp_poll),将自定义的回调函数注册到驱动的等待队列中。
static int ep_insert(struct eventpoll *ep, const struct epoll_event *event,
struct file *tfile, int fd, int full_check) {
int error;
struct epitem *epi;
struct ep_pqueue epq;
/* 1. 分配并初始化 epitem 节点 */
if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
return -ENOMEM;
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;
ep_set_ffd(&epi->ffd, tfile, fd);
epi->event = *event;
/* 2. 初始化 ep_pqueue 适配器,设置回调包装函数 */
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
/* 3. 内部引发虚函数调用:tfile->f_op->poll() */
/* 该调用会触发 ep_ptable_queue_proc,从而将 ep_poll_callback 绑定到设备驱动 */
revents = ep_item_poll(epi, &epq.pt, 1);
/* 4. 将 epitem 插入到 eventpoll 的红黑树中 */
ep_rbtree_insert(ep, epi);
/* 5. 如果当前设备在注册时就已经有就绪事件,直接将其放入就绪链表,并唤醒可能在等待的进程 */
if ((revents & event->events) && !ep_is_linked(epi)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
if (waitqueue_active(&ep->wq))
wake_up(&ep->wq);
}
return 0;
}
在 ep_item_poll 的链路中,驱动程序通过调用 poll_wait 触发了先前设置的 ep_ptable_queue_proc:
/* 驱动的 poll 机制回调该函数来建立等待项 */
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
poll_table *pt) {
struct epitem *epi = ep_cb_from_pt(pt)->epi;
struct eppoll_entry *pwq;
if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
/* 初始化真正的驱动等待队列项,将其核心回调函数设置为 ep_poll_callback */
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
pwq->whead = whead;
pwq->base = epi;
/* 将等待项挂载到具体设备(如 Socket 接收缓冲区)的等待队列中 */
add_wait_queue(whead, &pwq->wait);
list_add_tail(&pwq->llink, &epi->pwqlist);
epi->nwait++;
} else {
epi->nwait = -1;
}
}
2.3 epoll_wait
epoll_wait 检查就绪链表 rdllist 是否有节点。如果没有,当前进程将让出 CPU,进入睡眠状态,直到被回调函数唤醒或超时。
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
int, maxevents, int, timeout) {
// ... 基础安全检查
return do_epoll_wait(epfd, events, maxevents, timeout);
}
static int do_epoll_wait(int epfd, struct epoll_event __user *events,
int maxevents, int timeout) {
struct fd f;
struct eventpoll *ep;
f = fdget(epfd);
ep = f.file->private_data;
/* 调用 ep_poll 获取就绪事件 */
error = ep_poll(ep, events, maxevents, timeout);
return error;
}
ep_poll 实现逻辑
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents, long timeout) {
int res = 0, eavail;
unsigned long flags;
wait_queue_entry_t wait;
/* 1. 检查当前是否有可用就绪事件 */
if (list_empty(&ep->rdllist)) {
/* 没有事件,初始化一个等待队列项,关联当前进程 current */
init_waitqueue_entry(&wait, current);
/* 挂载到 eventpoll 的等待队列 wq 中 */
__add_wait_queue_exclusive(&ep->wq, &wait);
for (;;) {
/* 将进程状态设置为可中断睡眠(TASK_INTERRUPTIBLE) */
set_current_state(TASK_INTERRUPTIBLE);
/* 再次检查是否有就绪事件或被信号中断 */
if (!list_empty(&ep->rdllist) || !timeout)
break;
if (signal_pending(current)) {
res = -EINTR;
break;
}
/* 让出 CPU,进入睡眠,等待 timeout 或被驱动回调唤醒 */
if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS)) {
timeout = 0; // 超时
break;
}
}
/* 被唤醒后,将进程状态恢复为运行,并移出等待队列 */
__remove_wait_queue(&ep->wq, &wait);
__set_current_state(TASK_RUNNING);
}
/* 2. 检查是否有就绪事件需要拷贝给用户空间 */
eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_NON_BLOCK;
if (eavail && res == 0) {
/* 核心函数:将事件数据从内核空间拷贝到用户空间的 events 数组 */
res = ep_send_events(ep, events, maxevents);
}
return res;
}
3. 关键驱动机制:ep_poll_callback 的触发流程
当网卡收到数据包时,硬件中断触发驱动程序。驱动程序将数据解析并放入对应的 socket 缓冲区。此时,驱动会调用原本挂载在 socket->sk_sleep 队列上的唤醒函数,这个唤醒函数就是我们在 epoll_ctl 中注册的 ep_poll_callback。
以下是 ep_poll_callback 的核心源码:
static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key) {
int campgrounds;
unsigned long flags;
/* 通过 wait 针脚反向查找到对应的 eppoll_entry,进而拿到 epitem */
struct epitem *epi = ep_item_from_wait(wait);
struct eventpoll *ep = epi->ep;
__poll_t pollflags = key_to_pollflags(key);
spin_lock_irqsave(&ep->lock, flags);
/* 1. 检查驱动传来的事件掩码是否与用户注册的事件匹配 */
if (pollflags && !(pollflags & epi->event.events))
goto out_unlock;
/* 2. 判断当前 epitem 是否已经存在于就绪链表中,若存在则直接跳过防止重复添加 */
if (ep_is_linked(epi))
goto check_pages;
/* 3. 核心步骤:将就绪的 epitem 添加到 eventpoll 的就绪双向链表末尾 */
list_add_tail(&epi->rdllink, &ep->rdllist);
check_pages:
/* 4. 唤醒在 epoll_wait 中阻塞等待的进程 */
if (waitqueue_active(&ep->wq)) {
wake_up(&ep->wq);
}
out_unlock:
spin_unlock_irqrestore(&ep->lock, flags);
return 1;
}
4. LT(水平触发)与 ET(边缘触发)内核实现差异
LT(Level Triggered)和 ET(Edge Triggered)的本质区别在于:**当一次 epoll_wait 结束后,未处理完(或未完全读写完)的就绪 FD 是否会被自动重新放回 rdllist**。
这一逻辑在 ep_send_events_proc(由 ep_send_events 调用)中实现:
static __initialize int ep_send_events_proc(void *priv, void *cookie, int call_nests) {
struct ep_send_events_data *esed = priv;
struct eventpoll *ep = esed->ep;
struct epitem *epi;
struct epoll_event __user *uevent = esed->events;
struct list_head txlist;
/* 1. 将 rdllist 上的所有节点转移到一个临时链表 txlist 中,清空 rdllist */
list_splice_init(&ep->rdllist, &txlist);
/* 2. 遍历临时链表 txlist */
while (!list_empty(&txlist) && esed->res < esed->maxevents) {
epi = list_first_entry(&txlist, struct epitem, rdllink);
/* 将其从临时链表移除 */
list_del_init(&epi->rdllink);
/* 再次调用驱动的 poll 查看当前事件是否真实有效 */
revents = ep_item_poll(epi, &pt, 1);
revents &= epi->event.events;
if (revents) {
/* 将事件拷贝到用户空间 */
if (__put_user(revents, &uevent->events) ||
__put_user(epi->event.data, &uevent->data)) {
/* 拷贝失败则将节点重新放回原就绪链表 */
list_add(&epi->rdllink, &ep->rdllist);
return -EFAULT;
}
esed->res++;
uevent++;
/* 3. 【核心区别所在】判断触发模式 */
if (epi->event.events & EPOLLET) {
/* ET 模式:既然已经通知了用户,就不再做任何操作 */
/* 这个节点不会被放回 rdllist,直到下一次设备状态改变触发回调 */
} else {
/* LT 模式(默认):只要事件依然有效,就重新将节点放回 eventpoll 的 rdllist 中 */
list_add_tail(&epi->rdllink, &ep->rdllist);
}
}
}
/* 4. 如果还有未处理完的节点(例如 maxevents 小于实际就绪数),全部放回 rdllist */
if (!list_empty(&txlist)) {
list_splice(&txlist, &ep->rdllist);
}
return 0;
}
机制对比总结
| | |
|---|
| 内核实现本质 | 事件复制到用户态后,若 FD 仍有事件,内核将其重新放回rdllist。 | 事件复制到用户态后,内核直接将其从rdllist 移除,不再放回。 |
用户态下一次 epoll_wait | 如果缓冲区还有数据未读完,epoll_wait 会立即返回,不会阻塞。 | 除非有新数据到达触发中断,否则即使缓冲区有剩余数据,epoll_wait 也会保持阻塞。 |
| 推荐编程模式 | | 必须搭配非阻塞 I/O(Non-blocking I/O),且必须循环 read/write 直到返回 EAGAIN。 |
| 内核上下文切换开销 | 相对较高,因为未读完的 FD 会反复触发 epoll_wait 返回。 | 极低,每个事件仅通知一次,对高并发大连接场景性能更优。 |