1. 概述
1.1 什么是 epoll?
epoll 是 Linux 内核提供的一种高效的 I/O 事件通知机制,用于监控多个文件描述符(file descriptors)的 I/O 事件。
1.2 epoll 的优势
| 特性 | select/poll | epoll |
|---|---|---|
| 时间复杂度 | O(n) | O(1) |
| 文件描述符限制 | 1024 (select) / 无限制 (poll) | 无限制 |
| 效率 | 每次调用都要传递所有 fd | 只返回就绪的 fd |
| 适用场景 | 少量 fd | 大量 fd |
1.3 epoll 系统调用族
c
// 创建 epoll 实例
int epoll_create(int size);
int epoll_create1(int flags);
// 控制 epoll 实例
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 等待事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);
2. epoll 架构设计
2.1 整体架构
perl
用户空间 内核空间
┌─────────────┐ ┌─────────────┐
│ │ │ │
│ 应用程序 │ │ epoll 核心 │
│ │ │ │
│ epoll_fd │◄────────────────►│ eventpoll │
│ │ │ │
│ file_fd1 │ │ epitem1 │
│ file_fd2 │ │ epitem2 │
│ file_fd3 │ │ epitem3 │
│ │ │ │
└─────────────┘ └─────────────┘
│ │
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ 文件系统 │ │ 设备驱动 │
│ (socket, │ │ (网络, │
│ pipe, │ │ 串口等) │
│ etc.) │ │ │
└─────────────┘ └─────────────┘
2.2 核心数据结构
2.2.1 eventpoll - epoll 实例
c
// fs/eventpoll.c
struct eventpoll {
// 保护 epoll 实例的自旋锁
spinlock_t lock;
// 等待队列:等待 epoll_wait 的进程
wait_queue_head_t wq;
// 等待队列:等待文件描述符就绪的进程
wait_queue_head_t poll_wait;
// 就绪事件列表(双向链表)
struct list_head rdllist;
// 红黑树根:存储所有监控的文件描述符
struct rb_root_cached rbr;
// 就绪事件的数量
u32 rdllist_count;
// 唤醒回调函数
struct wakeup_source *ws;
// 用户空间事件数组的指针
struct epitem *ovflist;
// epoll 文件描述符
struct file *file;
// 用于避免循环检测
struct user_struct *user;
// 事件循环嵌套深度
int nested_has_wakeup;
};
2.2.2 epitem - 监控的文件描述符项
c
// fs/eventpoll.c
struct epitem {
// 红黑树节点
union {
struct rb_node rbn; // 用于红黑树
struct rcu_head rcu; // 用于 RCU 释放
};
// 就绪列表节点
struct list_head rdllink; // 链接到 rdllist
// epoll 实例
struct eventpoll *ep;
// 监控的文件描述符
struct epoll_filefd ffd;
// 用户空间事件结构
struct epoll_event event;
// 等待队列项
struct wait_queue_entry wq;
// 文件描述符的等待队列
wait_queue_head_t *whead;
};
2.2.3 epoll_event - 用户空间事件结构
c
// include/uapi/linux/eventpoll.h
struct epoll_event {
__u32 events; // 事件类型:
// EPOLLIN - 可读
// EPOLLOUT - 可写
// EPOLLERR - 错误
// EPOLLHUP - 挂起
// EPOLLET - 边缘触发
// EPOLLONESHOT - 一次性
// EPOLLRDHUP - 对端关闭
__u64 data; // 用户数据(可以是 fd、指针等)
};
2.3 数据结构关系图
scss
eventpoll (epoll 实例)
├── rbr (红黑树根)
│ ├── epitem1 (fd1)
│ ├── epitem2 (fd2)
│ └── epitem3 (fd3)
│
└── rdllist (就绪列表)
├── epitem1 (fd1 就绪)
└── epitem3 (fd3 就绪)
3. epoll_create / epoll_create1
3.1 系统调用定义
c
// fs/eventpoll.c
SYSCALL_DEFINE1(epoll_create, int, size)
{
if (size <= 0)
return -EINVAL;
return sys_epoll_create1(0);
}
SYSCALL_DEFINE1(epoll_create1, int, flags)
{
int error, fd;
struct eventpoll *ep = NULL;
struct file *file;
// 1. 检查标志
if (flags & ~EPOLL_CLOEXEC)
return -EINVAL;
// 2. 分配 eventpoll 结构
error = ep_alloc(&ep);
if (error < 0)
return error;
// 3. 获取未使用的文件描述符
fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));
if (fd < 0) {
error = fd;
goto out_free_ep;
}
// 4. 创建文件结构
file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
O_RDWR | (flags & O_CLOEXEC));
if (IS_ERR(file)) {
error = PTR_ERR(file);
goto out_free_fd;
}
// 5. 初始化 eventpoll
ep->file = file;
fd_install(fd, file);
return fd;
out_free_fd:
put_unused_fd(fd);
out_free_ep:
ep_free(ep);
return error;
}
3.2 ep_alloc - 分配 eventpoll
c
// fs/eventpoll.c
static int ep_alloc(struct eventpoll **pep)
{
int error;
struct user_struct *user;
struct eventpoll *ep;
// 1. 分配 eventpoll 结构
ep = kzalloc(sizeof(*ep), GFP_KERNEL);
if (unlikely(!ep))
return -ENOMEM;
// 2. 初始化自旋锁
spin_lock_init(&ep->lock);
// 3. 初始化等待队列
init_waitqueue_head(&ep->wq);
init_waitqueue_head(&ep->poll_wait);
// 4. 初始化就绪列表
INIT_LIST_HEAD(&ep->rdllist);
ep->rdllist_count = 0;
// 5. 初始化红黑树
ep->rbr = RB_ROOT_CACHED;
// 6. 初始化其他字段
ep->ovflist = EP_UNACTIVE_PTR;
ep->user = get_current_user();
*pep = ep;
return 0;
}
3.3 创建流程
scss
用户空间调用 epoll_create1(0)
↓
进入内核 sys_epoll_create1()
↓
分配 eventpoll 结构
↓
初始化等待队列、红黑树、就绪列表
↓
创建匿名 inode 文件
↓
分配文件描述符
↓
返回文件描述符给用户空间
4. epoll_ctl
4.1 系统调用定义
c
// fs/eventpoll.c
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
struct epoll_event __user *, event)
{
struct epoll_event epds;
// 1. 复制用户空间事件结构
if (ep_op_has_event(op) &&
copy_from_user(&epds, event, sizeof(struct epoll_event)))
return -EFAULT;
// 2. 执行操作
return do_epoll_ctl(epfd, op, fd, &epds, false);
}
4.2 do_epoll_ctl - 核心实现
c
// fs/eventpoll.c
int do_epoll_ctl(int epfd, int op, int fd, struct epoll_event *epds,
bool nonblock)
{
int error;
int full_check = 0;
struct fd f, tf;
struct eventpoll *ep;
struct epitem *epi;
struct epoll_event epds;
struct eventpoll *tep = NULL;
// 1. 获取 epoll 文件描述符
f = fdget(epfd);
if (!f.file)
return -EBADF;
// 2. 检查是否是 epoll 文件
if (!is_file_epoll(f.file)) {
error = -EINVAL;
goto error_tgt_fput;
}
// 3. 获取目标文件描述符
tf = fdget(fd);
if (!tf.file) {
error = -EBADF;
goto error_tgt_fput;
}
// 4. 获取 eventpoll 结构
ep = f.file->private_data;
// 5. 根据操作类型执行
switch (op) {
case EPOLL_CTL_ADD:
// 添加文件描述符到 epoll
error = ep_insert(ep, &epds, tf.file, fd, full_check);
break;
case EPOLL_CTL_DEL:
// 从 epoll 删除文件描述符
error = ep_remove(ep, tf.file, fd);
break;
case EPOLL_CTL_MOD:
// 修改文件描述符的事件
error = ep_modify(ep, tf.file, fd, &epds);
break;
default:
error = -EINVAL;
break;
}
error_tgt_fput:
if (full_check)
clear_tfile_check_list();
fdput(tf);
error_tgt_fput:
fdput(f);
return error;
}
4.3 ep_insert - 添加文件描述符
c
// fs/eventpoll.c
static int ep_insert(struct eventpoll *ep,
const struct epoll_event *event,
struct file *tfile, int fd, int full_check)
{
int error, pwake = 0;
__poll_t revents;
long user_watches;
struct epitem *epi;
struct ep_pqueue epq;
// 1. 检查用户限制
user_watches = atomic_long_read(&ep->user->epoll_watches);
if (unlikely(user_watches >= max_user_watches))
return -ENOSPC;
// 2. 分配 epitem
if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
return -ENOMEM;
// 3. 初始化 epitem
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;
epi->nwait = 0;
epi->next = EP_UNACTIVE_PTR;
// 4. 初始化等待队列项
ep_setup_wait(&epq, epi);
// 5. 将 epitem 插入红黑树
spin_lock(&ep->lock);
ep_rbtree_insert(ep, epi);
spin_unlock(&ep->lock);
// 6. 注册回调函数到文件描述符
revents = ep_item_poll(epi, &epq.pt, 1);
// 7. 如果文件已经就绪,添加到就绪列表
if (revents && !ep_is_linked(&epi->rdllink)) {
list_add_tail(&epi->rdllist, &ep->rdllist);
ep_pm_stay_awake_rcu(epi);
// 如果有进程在等待,唤醒它
if (waitqueue_active(&ep->wq))
wake_up(&ep->wq);
}
// 8. 增加用户计数
atomic_long_inc(&ep->user->epoll_watches);
return 0;
}
4.4 ep_item_poll - 注册回调
c
// fs/eventpoll.c
static __poll_t ep_item_poll(const struct epitem *epi, poll_table *pt,
int depth)
{
struct file *file = epi->ffd.file;
__poll_t res;
// 1. 设置等待队列项
pt->_key = epi->event.events;
// 2. 调用文件的 poll 方法
res = vfs_poll(file, pt);
// 3. 如果使用边缘触发,只返回新的事件
if (epi->event.events & EPOLLET) {
res &= epi->event.events | EPOLLONESHOT | EPOLLRDHUP;
}
return res;
}
4.5 回调机制
当文件描述符就绪时,会调用回调函数:
c
// fs/eventpoll.c
static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{
int pwake = 0;
unsigned long flags;
struct epitem *epi = ep_item_from_wait(wait);
struct eventpoll *ep = epi->ep;
__poll_t pollflags = key_to_poll(key);
int ewake = 0;
// 1. 获取锁
spin_lock_irqsave(&ep->lock, flags);
// 2. 检查事件是否匹配
if (!(epi->event.events & ~EPOLLEXCLUSIVE_BITS))
goto out_unlock;
// 3. 检查是否已经在就绪列表中
if (ep_is_linked(&epi->rdllink))
goto out_unlock;
// 4. 添加到就绪列表
list_add_tail(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake_rcu(epi);
// 5. 如果有进程在等待,唤醒它
if (waitqueue_active(&ep->wq)) {
if ((epi->event.events & EPOLLEXCLUSIVE) &&
!(pollflags & POLLFREE))
__add_wait_queue_exclusive(&ep->wq, &epi->wq);
else
wake_up(&ep->wq);
}
if (waitqueue_active(&ep->poll_wait))
pwake++;
out_unlock:
spin_unlock_irqrestore(&ep->lock, flags);
if (pwake)
ep_poll_safewake(&ep->poll_wait);
return 1;
}
5. epoll_wait / epoll_pwait
5.1 系统调用定义
c
// fs/eventpoll.c
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
int, maxevents, int, timeout)
{
return do_epoll_wait(epfd, events, maxevents, timeout);
}
SYSCALL_DEFINE6(epoll_pwait, int, epfd, struct epoll_event __user *, events,
int, maxevents, int, timeout, const sigset_t __user *, sigmask,
size_t, sigsetsize)
{
sigset_t ksigmask, sigsaved;
// 1. 设置信号掩码
if (sigmask) {
if (copy_from_user(&ksigmask, sigmask, sizeof(ksigmask)))
return -EFAULT;
sigdelsetmask(&ksigmask, sigmask(SIGKILL) | sigmask(SIGSTOP));
sigprocmask(SIG_SETMASK, &ksigmask, &sigsaved);
}
// 2. 调用 epoll_wait
int error = do_epoll_wait(epfd, events, maxevents, timeout);
// 3. 恢复信号掩码
if (sigmask)
sigprocmask(SIG_SETMASK, &sigsaved, NULL);
return error;
}
5.2 do_epoll_wait - 核心实现
c
// fs/eventpoll.c
static int do_epoll_wait(int epfd, struct epoll_event __user *events,
int maxevents, int timeout)
{
int error;
struct fd f;
struct eventpoll *ep;
// 1. 参数检查
if (maxevents <= 0 || maxevents > EP_MAX_EVENTS)
return -EINVAL;
// 2. 获取 epoll 文件描述符
f = fdget(epfd);
if (!f.file)
return -EBADF;
// 3. 检查是否是 epoll 文件
if (!is_file_epoll(f.file)) {
error = -EINVAL;
goto error_fput;
}
// 4. 获取 eventpoll 结构
ep = f.file->private_data;
// 5. 等待事件
error = ep_poll(ep, events, maxevents, timeout);
error_fput:
fdput(f);
return error;
}
5.3 ep_poll - 等待事件的核心函数
c
// fs/eventpoll.c
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents, long timeout)
{
int res = 0, eavail, timed_out = 0;
unsigned long flags;
u64 slack = 0;
wait_queue_entry_t wait;
ktime_t expires, *to = NULL;
// 1. 计算超时时间
if (timeout > 0) {
struct timespec64 end_time = ep_set_mstimeout(timeout);
slack = select_estimate_accuracy(&end_time);
to = &expires;
*to = timespec64_to_ktime(end_time);
} else if (timeout == 0) {
// 非阻塞模式
timed_out = 1;
}
// 2. 进入循环
fetch_events:
spin_lock_irqsave(&ep->lock, flags);
// 3. 检查是否有就绪事件
eavail = ep_events_available(ep);
if (eavail)
goto send_events;
// 4. 如果没有就绪事件,准备等待
if (!eavail) {
// 初始化等待队列项
init_waitqueue_entry(&wait, current);
__add_wait_queue_exclusive(&ep->wq, &wait);
// 循环等待
for (;;) {
// 5. 设置进程状态为可中断睡眠
set_current_state(TASK_INTERRUPTIBLE);
// 6. 再次检查是否有就绪事件
eavail = ep_events_available(ep);
if (eavail)
break;
// 7. 检查是否有信号待处理
if (signal_pending(current)) {
res = -EINTR;
break;
}
// 8. 检查超时
if (timeout != -1) {
unsigned long expires = ep_set_mstimeout(timeout);
if (time_after_eq(jiffies, expires)) {
timed_out = 1;
break;
}
}
// 9. 释放锁并进入睡眠
spin_unlock_irqrestore(&ep->lock, flags);
// ========== 关键点:进程在这里进入睡眠 ==========
// 如果 timeout 很大(如 30000ms),进程会在这里等待很长时间
// 在这期间,即使收到冻结信号,进程也不会返回
// 只有超时或事件到来时,进程才会被唤醒
if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS)) {
timed_out = 1;
}
// 10. 被唤醒后重新获取锁
spin_lock_irqsave(&ep->lock, flags);
}
// 11. 从等待队列移除
__remove_wait_queue(&ep->wq, &wait);
__set_current_state(TASK_RUNNING);
}
send_events:
// 12. 检查是否需要发送事件
if (!res && eavail &&
!(res = ep_send_events(ep, events, maxevents)) && !timed_out)
goto fetch_events;
spin_unlock_irqrestore(&ep->lock, flags);
return res;
}
5.4 ep_events_available - 检查就绪事件
c
// fs/eventpoll.c
static inline int ep_events_available(struct eventpoll *ep)
{
return !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;
}
5.5 ep_send_events - 发送事件到用户空间
c
// fs/eventpoll.c
static int ep_send_events(struct eventpoll *ep,
struct epoll_event __user *events, int maxevents)
{
struct epitem *epi, *tmp;
LIST_HEAD(txlist);
int res = 0;
// 1. 获取就绪列表
mutex_lock(&ep->mtx);
ep_start_scan(ep, &txlist);
mutex_unlock(&ep->mtx);
// 2. 遍历就绪列表
list_for_each_entry_safe(epi, tmp, &txlist, rdllink) {
struct epoll_event event;
// 3. 获取事件
event = epi->event;
// 4. 如果是边缘触发,从就绪列表移除
if (epi->event.events & EPOLLET) {
list_del_init(&epi->rdllink);
}
// 5. 复制到用户空间
if (__put_user(event.events, &events[res].events) ||
__put_user(event.data, &events[res].data)) {
list_add(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake(epi);
if (!res)
res = -EFAULT;
break;
}
res++;
if (res >= maxevents)
break;
}
// 6. 将剩余项放回就绪列表
ep_done_scan(ep, &txlist);
return res;
}
5.6 关键点:schedule_hrtimeout_range
c
// kernel/time/hrtimer.c
long __sched schedule_hrtimeout_range(ktime_t *expires, u64 delta,
const enum hrtimer_mode mode)
{
struct hrtimer_sleeper t;
// 1. 设置高精度定时器
hrtimer_init_sleeper_on_stack(&t, CLOCK_MONOTONIC, mode);
hrtimer_set_expires_range_ns(&t.timer, *expires, delta);
// 2. 进入睡眠
do {
set_current_state(TASK_INTERRUPTIBLE);
hrtimer_sleeper_start_expires(&t, mode);
if (likely(t.task))
schedule(); // ← 进程在这里被调度出去
hrtimer_cancel(&t.timer);
mode = HRTIMER_MODE_ABS;
} while (t.task && !signal_pending(current));
__set_current_state(TASK_RUNNING);
destroy_hrtimer_on_stack(&t.timer);
return 0;
}
关键问题:
- 如果
timeout很大(如 30000ms),schedule_hrtimeout_range会设置一个很长的定时器 - 进程会进入
TASK_INTERRUPTIBLE状态,等待定时器到期或事件到来 - 即使收到冻结信号,进程也不会立即返回
- 只有定时器到期或事件到来时,进程才会被唤醒
6. 内核实现详解
6.1 等待队列机制
epoll 使用等待队列来管理等待事件的进程:
c
// include/linux/wait.h
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
struct wait_queue_entry {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head entry;
};
工作流程:
- 进程调用
epoll_wait - 如果没有就绪事件,进程被添加到
ep->wq等待队列 - 当文件描述符就绪时,回调函数
ep_poll_callback被调用 - 回调函数唤醒等待队列中的进程
- 进程被唤醒,检查就绪事件并返回
6.2 红黑树管理
epoll 使用红黑树来高效管理监控的文件描述符:
c
// fs/eventpoll.c
static void ep_rbtree_insert(struct eventpoll *ep, struct epitem *epi)
{
int kcmp;
struct rb_node **rbp, *parent;
struct epitem *epic;
bool leftmost = true;
rbp = &ep->rbr.rb_root.rb_node;
parent = NULL;
while (*rbp) {
parent = *rbp;
epic = rb_entry(parent, struct epitem, rbn);
kcmp = ep_cmp_ffd(&epi->ffd, &epic->ffd);
if (kcmp > 0) {
rbp = &parent->rb_right;
leftmost = false;
} else
rbp = &parent->rb_left;
}
rb_link_node(&epi->rbn, parent, rbp);
rb_insert_color_cached(&epi->rbn, &ep->rbr, leftmost);
}
优势:
- 插入、删除、查找的时间复杂度都是 O(log n)
- 适合管理大量文件描述符
6.3 就绪列表管理
就绪列表是一个双向链表,存储所有就绪的文件描述符:
c
// fs/eventpoll.c
// 添加到就绪列表
list_add_tail(&epi->rdllink, &ep->rdllist);
// 从就绪列表移除
list_del_init(&epi->rdllink);
优势:
- O(1) 时间复杂度的添加和移除
- 只返回就绪的文件描述符,不需要遍历所有 fd
7. 与进程冻结的关系
7.1 问题场景
c
// 问题代码
int nfds = epoll_wait(epfd, events, maxevents, 30000); // 30秒超时
时间线:
ini
T=0s: 进程调用 epoll_wait(..., 30000)
↓
T=0s: 进入内核 do_epoll_wait()
↓
T=0s: 调用 ep_poll()
↓
T=0s: 检查就绪事件:没有
↓
T=0s: 调用 schedule_hrtimeout_range(..., 30000ms)
↓
T=0s: 设置高精度定时器,30秒后到期
↓
T=0s: 进程进入 TASK_INTERRUPTIBLE 状态
↓
T=0s: 调用 schedule(),进程被调度出去
↓
T=5s: 系统尝试挂起,发送冻结信号
↓
T=5s: 冻结信号唤醒进程
↓
T=5s: 进程检查:有事件吗?没有
↓
T=5s: 进程检查:超时了吗?没有(还有25秒)
↓
T=5s: 进程检查:有信号吗?有,但定时器还没到期
↓
T=5s: 进程继续等待定时器到期 ← 问题!
↓
T=30s: 定时器到期,进程被唤醒
↓
T=30s: 系统调用返回,检查冻结条件
↓
T=30s: 发现需要冻结,进入 __refrigerator()
↓
T=30s: 进程成功冻结(但已经晚了25秒!)
7.2 为什么信号不能立即中断?
虽然 epoll_wait 是可中断的系统调用(使用 TASK_INTERRUPTIBLE),但问题在于:
- 信号会唤醒进程:冻结信号会唤醒进程
- 但进程会继续等待:如果定时器还没到期,进程会重新进入睡眠
- 只有定时器到期或事件到来才会返回:进程只有在这些情况下才会从系统调用返回
7.3 修复方案
c
// 修复后的代码
int running = 1;
while (running) {
// 使用短超时(1秒)
int nfds = epoll_wait(epfd, events, maxevents, 1000);
if (nfds > 0) {
// 处理事件
handle_events(events, nfds);
} else if (nfds == 0) {
// 超时,检查是否需要退出
if (should_exit()) {
running = 0;
}
} else {
// 错误处理
if (errno == EINTR) {
// 被信号中断,继续循环
continue;
}
// 其他错误处理
}
}
修复后的时间线:
ini
T=0s: 进程调用 epoll_wait(..., 1000)
↓
T=1s: epoll_wait 超时返回(正常情况)
↓
T=1s: 系统调用返回,检查冻结条件
↓
T=1s: 发现不需要冻结,继续循环
↓
T=5s: 系统尝试挂起,发送冻结信号
↓
T=5s: 进程正在准备调用 epoll_wait
↓
T=5s: 进程调用 epoll_wait(..., 1000)
↓
T=5s: 进入内核,检查 TIF_SIGPENDING 标志
↓
T=5s: 发现需要冻结,epoll_wait 可能提前返回
↓
T=5s: 系统调用返回,检查冻结条件
↓
T=5s: 发现需要冻结,进入 __refrigerator()
↓
T=5s: 进程成功冻结!← 只用了5秒,而不是30秒!
8. 性能对比
8.1 select vs poll vs epoll
| 操作 | select | poll | epoll |
|---|---|---|---|
| 添加 fd | O(n) | O(n) | O(log n) |
| 等待事件 | O(n) | O(n) | O(1) |
| 返回就绪 fd | O(n) | O(n) | O(1) |
| fd 限制 | 1024 | 无限制 | 无限制 |
8.2 实际性能测试
yaml
测试场景:监控 1000 个文件描述符,其中 100 个就绪
select: ~10ms
poll: ~10ms
epoll: ~0.1ms ← 快 100 倍!
9. 最佳实践
9.1 使用短超时
c
// ✅ 正确:使用短超时
int nfds = epoll_wait(epfd, events, maxevents, 1000); // 1秒
// ❌ 错误:使用长超时
int nfds = epoll_wait(epfd, events, maxevents, 30000); // 30秒
9.2 处理 EINTR
c
int nfds;
do {
nfds = epoll_wait(epfd, events, maxevents, timeout);
} while (nfds == -1 && errno == EINTR);
if (nfds > 0) {
// 处理事件
}
9.3 使用边缘触发(ET)模式
c
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
优势:
- 减少事件通知次数
- 提高性能
9.4 使用 epoll_pwait 处理信号
c
sigset_t sigmask;
sigemptyset(&sigmask);
sigaddset(&sigmask, SIGTERM);
int nfds = epoll_pwait(epfd, events, maxevents, timeout, &sigmask);
10. 总结
10.1 关键点
- epoll 是高效的 I/O 事件通知机制
- 使用红黑树管理文件描述符,O(log n) 复杂度
- 使用就绪列表返回事件,O(1) 复杂度
- 使用等待队列管理等待的进程
- 长时间超时会导致进程冻结问题
10.2 与进程冻结的关系
- 问题:长时间超时(如 30 秒)会导致进程无法及时响应冻结请求
- 原因 :进程在
schedule_hrtimeout_range中等待,即使收到信号也不会立即返回 - 解决:使用短超时(如 1 秒),让系统调用频繁返回,及时检查冻结条件
10.3 最佳实践
- 使用短超时(1 秒或更短)
- 正确处理 EINTR
- 使用边缘触发模式提高性能
- 使用 epoll_pwait 处理信号
文档结束