腾讯二面:说说 epoll 为什么快?我说因为 O(m),面试官摇头了

今天,我们就从Linux内核源码的角度,彻底搞懂这个问题的本质。读完这篇文章,你会明白:

  • epoll到底快在哪里?(用数据说话)
  • 内核用了哪些黑科技?(红黑树+就绪链表+回调机制)
  • 和select/poll的本质区别是什么?

一、性能对比:数据不会骗人

1. 一组震撼的benchmark数据

先看一组来自《The Linux Programming Interface》书中的性能测试数据(单位:毫秒):

监控操作次数selectpollepoll
100.73 ms0.61 ms0.41 ms
1003.0 ms2.9 ms0.42 ms
1,00035 ms35 ms0.53 ms
10,000930 ms990 ms0.66 ms

看到差距了吗?10,000次操作时,epoll比select快1400倍!

这不是魔法,而是算法复杂度的碾压:

  • select/poll: O(n) — 线性遍历所有fd
  • epoll: O(m) — 只处理 m 个就绪的fd(通常m << n)

但很多人只记住了这个结论,却不知道内核是怎么做到O(m)的。

💡 注意:这里的”监控操作次数”是指调用select/poll/epoll_wait的次数。在实际高并发场景中,假设你有10,000个并发连接,每次调用select需要遍历所有10,000个fd,而epoll只处理就绪的那几个,性能差距会更加明显。

2. 为什么要先提select/poll?

要理解epoll的优势,必须先知道select/poll的痛点。

select/poll的工作流程(简化版):复制

用户态                     内核态
  |                          |
  |--- select(fds, 1024) --->| 
  |                          | 1. 把1024个fd从用户态拷贝到内核态
  |                          | 2. 遍历这1024个fd,挨个检查是否就绪
  |                          | 3. 把就绪的fd拷贝回用户态
  |<--- 返回就绪fd数量 ------|
  |                          |
  | 4. 用户态再遍历1024个fd,找出哪些真正就绪1.2.3.4.5.6.7.8.9.

问题出在哪?

  • 问题1:每次调用都要全量拷贝1024个fd,每次select调用都要从用户态→内核态拷贝一遍。
  • 问题2:内核要全量遍历假设你监听10000个连接,但实际只有3个就绪,内核还是要遍历全部10000个。
  • 问题3:用户态还要再遍历一遍内核只告诉你”有3个fd就绪”,但不告诉你是哪3个,用户态要再遍历10000个去找。

这就是O(n)复杂度的根源。

二、epoll的三大核心机制(重点)

epoll之所以能做到O(1),靠的是三大黑科技:

  • 红黑树 — 管理所有被监听的fd
  • 就绪链表 — 只存储就绪的fd
  • 回调机制 — fd就绪时自动加入链表

我们一个个拆解。

三、黑科技1:红黑树 — 高效管理海量fd

1. 内核数据结构揭秘

当你调用epoll_create()时,内核会创建一个eventpoll对象(位于fs/eventpoll.c):复制

struct eventpoll {
    spinlock_t lock;           // 自旋锁,保护就绪链表
    struct mutex mtx;          // 互斥锁,保护红黑树
    wait_queue_head_t wq;      // 等待队列(epoll_wait阻塞在这)
    struct rb_root_cached rbr; // 红黑树根节点 ⭐
    struct list_head rdllist;  // 就绪链表 ⭐⭐
    ...
};1.2.3.4.5.6.7.8.

重点看这两个字段:

  • rbr — 红黑树,存储所有被监听的fd
  • rdllist — 双向链表,存储已就绪的fd

2. 红黑树的作用

Q: 为什么用红黑树?

因为需要频繁的增删查操作:

  • epoll_ctl(ADD) — 添加fd
  • epoll_ctl(DEL) — 删除fd
  • epoll_ctl(MOD) — 修改fd
  • 内核内部查找fd是否存在

红黑树的性能:

  • 查找:O(log n)
  • 插入:O(log n)
  • 删除:O(log n)

对比数组/链表:

  • 数组查找:O(log n) 二分查找,但插入删除O(n)
  • 链表查找:O(n),插入删除O(1)

红黑树在增删查场景下综合最优。

3. 红黑树的节点:epitem

每个被监听的fd在红黑树中对应一个epitem节点:复制

struct epitem {
    struct rb_node rbn;        // 红黑树节点
    struct list_head rdllink;  // 链入就绪链表的节点
    struct epoll_filefd ffd;   // 被监听的文件描述符
    struct eventpoll *ep;      // 所属的epoll对象
    struct epoll_event event;  // 用户注册的事件(EPOLLIN/EPOLLOUT等)
    struct list_head pwqlist;  // poll等待队列链表 ⭐
    ...
};1.2.3.4.5.6.7.8.9.

关键字段:

  • rbn — 挂在红黑树上的节点
  • rdllink — 当fd就绪时,通过这个字段链入就绪链表
  • pwqlist — 这是实现回调的关键(后面详讲)

4. 红黑树的比较函数

内核如何判断两个epitem的大小?看比较函数ep_cmp_ffd():复制

static int ep_cmp_ffd(struct epoll_filefd *p1, struct epoll_filefd *p2)
{
    // 先比较 file 指针地址
    if (p1->file > p2->file)
        return 1;
    if (p1->file < p2->file)
        return -1;
    // 地址相同,再比较 fd
    return p1->fd - p2->fd;
}1.2.3.4.5.6.7.8.9.10.

为什么先比较file指针?

因为fd只是一个整数索引,可能被重用。而struct file指针在文件未关闭前是唯一的。

5. 图解:红黑树存储结构

复制

eventpoll
                   ┌──────────┐
                   │   lock   │
                   │   mtx    │
                   │   wq     │
                   │   rbr ──→│ 红黑树根节点
                   │ rdllist  │
                   └──────────┘
                        │
                        ↓
                   红黑树(管理所有fd)

[epitem:fd=5]

/ \ [epitem:fd=3] [epitem:fd=8] / \ / \ [fd=1] [fd=4] [fd=7] [fd=10] 每个epitem节点包含: – fd信息(文件描述符) – 用户关心的事件(EPOLLIN/EPOLLOUT) – 回调队列 pwqlist

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

添加fd时(epoll_ctl ADD):

  • 创建epitem节点
  • 调用ep_rbtree_insert()插入红黑树 — O(log n)
  • 注册回调函数到fd的等待队列

删除fd时(epoll_ctl DEL):

  • 调用ep_find()在红黑树中查找 — O(log n)
  • 从红黑树删除 — O(log n)
  • 清理回调队列

四、黑科技2:就绪链表 — 只关心就绪的fd

1. 就绪链表的设计思想

核心思想:不要遍历全部fd,只处理就绪的。

epoll维护了一个双向链表rdllist,专门存储已就绪的fd。复制

struct eventpoll {
    ...
    struct list_head rdllist;  // 就绪链表 ⭐
    ...
};1.2.3.4.5.

当fd就绪时(比如socket收到数据):

  • 内核触发该fd的回调函数 ep_poll_callback()
  • 回调函数把对应的epitem加入rdllist

当调用epoll_wait时:

  • 直接遍历rdllist(只有就绪的fd)
  • 把就绪事件拷贝到用户空间
  • 返回就绪fd数量

2. 图解:就绪链表的工作流程

复制

步骤1:初始状态,红黑树有10000个fd,就绪链表为空

    eventpoll                红黑树(10000个fd)
   ┌──────────┐               ●  ●  ●  ●  ●
   │ rdllist  │ → NULL       ●  ●  ●  ●  ●
   └──────────┘              ●  ●  ●  ●  ●

步骤2:fd=5 和 fd=8 的socket收到数据,触发回调

    内核网络层
       ↓ 数据到达
    [socket fd=5] → 触发回调 → ep_poll_callback()
       ↓
    把 epitem(fd=5) 加入 rdllist

    [socket fd=8] → 触发回调 → ep_poll_callback()
       ↓  
    把 epitem(fd=8) 加入 rdllist

步骤3:就绪链表变成这样

    eventpoll
   ┌──────────┐
   │ rdllist  │ → [epitem:fd=5] ⇄ [epitem:fd=8]
   └──────────┘

步骤4:用户调用 epoll_wait()

    只遍历 rdllist(2个fd),不遍历红黑树(10000个fd)

    返回给用户:
    events[0].fd = 5, events[0].events = EPOLLIN
    events[1].fd = 8, events[1].events = EPOLLIN1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.

关键点:

  • 不管有多少fd被监听,epoll_wait只处理就绪的那几个
  • 复杂度:O(就绪fd数量),接近O(1)

3. 为什么rdllist是双向链表?

Q: 为什么不用单链表或数组?

因为需要频繁的插入和删除:

  • 当fd就绪时 → 插入链表尾部(O(1))
  • 当epoll_wait处理后 → 可能需要把fd从链表移除(O(1))
  • 当fd被epoll_ctl(DEL)时 → 需要从链表移除(O(1))

双向链表的删除操作是O(1),单链表需要O(n)找前驱节点。

五、黑科技3:回调机制 — 自动加入就绪链表

1. 最核心的问题:fd怎么知道自己就绪了?

这是整个epoll机制最精妙的部分!

当你调用epoll_ctl(ADD, fd)添加一个socket时,内核做了什么?

关键步骤(源码位于ep_insert()函数):复制

static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
                     struct file *tfile, int fd)
{
    struct epitem *epi;
    struct ep_pqueue epq;
    
    // 1. 分配 epitem 节点
    epi = kmem_cache_alloc(epi_cache, GFP_KERNEL);
    
    // 2. 初始化 epitem
    INIT_LIST_HEAD(&epi->rdllink);   // 初始化就绪链表节点
    INIT_LIST_HEAD(&epi->pwqlist);   // 初始化poll等待队列链表
    epi->ep = ep;
    epi->event = *event;
    
    // 3. 关键:注册回调函数 ⭐⭐⭐
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
    
    // 4. 调用fd的poll函数,建立回调关系
    revents = ep_item_poll(epi, &epq.pt);
    
    // 5. 插入红黑树
    ep_rbtree_insert(ep, epi);
    
    // 6. 如果fd当前已经就绪,加入就绪链表
    if (revents && !ep_is_linked(&epi->rdllink)) {
        list_add_tail(&epi->rdllink, &ep->rdllist);
        if (waitqueue_active(&ep->wq))
            wake_up(&ep->wq);  // 唤醒阻塞在epoll_wait的进程
    }
    
    return0;
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.

2. 回调函数:ep_poll_callback()

核心中的核心:当socket收到数据时,内核会调用这个函数。复制

static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, 
                            int sync, void *key)
{
    struct epitem *epi = ep_item_from_wait(wait);
    struct eventpoll *ep = epi->ep;
    
    spin_lock_irqsave(&ep->lock, flags);
    
    // 1. 检查是否是我们关心的事件
    if (key && !((unsignedlong)key & epi->event.events))
        goto out_unlock;
    
    // 2. 如果epitem还没在就绪链表中,就加入 ⭐
    if (!ep_is_linked(&epi->rdllink)) {
        list_add_tail(&epi->rdllink, &ep->rdllist);
    }
    
    // 3. 唤醒阻塞在 epoll_wait 的进程
    if (waitqueue_active(&ep->wq))
        wake_up(&ep->wq);
    
out_unlock:
    spin_unlock_irqrestore(&ep->lock, flags);
    return1;
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.

这个函数的作用:

  • 当socket收到数据时,被内核自动调用
  • 把对应的epitem加入就绪链表rdllist
  • 唤醒阻塞在epoll_wait的进程

3. 回调是怎么建立的?

关键函数:ep_ptable_queue_proc()复制

static void ep_ptable_queue_proc(struct file *file, 
                                 wait_queue_head_t *whead,
                                 poll_table *pt)
{
    struct epitem *epi = ep_item_from_epqueue(pt);
    struct eppoll_entry *pwq;
    
    // 1. 分配一个poll等待队列项
    pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL);
    
    // 2. 设置回调函数为 ep_poll_callback
    init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
    
    // 3. 把这个等待队列项加入到 socket 的等待队列
    add_wait_queue(whead, &pwq->wait);
    
    // 4. 记录到 epitem 的 pwqlist
    pwq->next = epi->pwqlist;
    epi->pwqlist = pwq;
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.

建立回调的过程:复制

1. 用户调用 epoll_ctl(ADD, socket_fd)
    ↓
2. 内核创建 epitem,调用 ep_insert()
    ↓
3. ep_insert() 调用 socket->ops->poll()
    ↓
4. socket的poll函数会调用 poll_wait()
    ↓
5. poll_wait() 最终调用我们注册的 ep_ptable_queue_proc()
    ↓
6. ep_ptable_queue_proc() 创建等待队列项,设置回调为 ep_poll_callback
    ↓
7. 把等待队列项加入到 socket 的等待队列

完成!现在socket收到数据时,会自动触发 ep_poll_callback()1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.

4. 图解:回调机制完整流程

复制

用户态                    内核态                     网卡/驱动
  |                         |                          |
  |-- epoll_ctl(ADD,fd) --> |                          |
  |                         |                          |
  |                    创建epitem                      |
  |                    注册回调到socket                |
  |                         |                          |
  |                    eventpoll                       |
  |                   ┌──────────┐                     |
  |                   │ rdllist  │ → NULL              |
  |                   └──────────┘                     |
  |                         ↓                          |
  |                    红黑树:[epitem:fd=5]           |
  |                         ↓                          |
  |                    pwqlist → [eppoll_entry]        |
  |                                    ↓               |
  |                            wait_queue_entry        |
  |                            回调=ep_poll_callback   |
  |                                    ↓               |
  |                            socket的等待队列         |
  |                                                    |
  |                         等待数据...                |
  |                         |                          |
  |                         |          <----数据到达---|
  |                         |                          |
  |                    内核网络层                       |
  |                    唤醒socket等待队列               |
  |                         ↓                          |
  |                    执行回调:ep_poll_callback       |
  |                         ↓                          |
  |                    把epitem加入rdllist ⭐          |
  |                         ↓                          |
  |                    唤醒epoll_wait进程               |
  |                         |                          |
  |<--- 返回就绪fd ---------|                          |
  |                                                    |1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.

六、epoll_wait的实现:只看就绪链表

1. epoll_wait做了什么?

复制

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
                   int maxevents, long timeout)
{
    int res = 0;
    
    // 1. 检查就绪链表是否为空
    if (!ep_events_available(ep)) {
        // 2. 如果为空,把当前进程加入等待队列
        init_waitqueue_entry(&wait, current);
        __add_wait_queue_exclusive(&ep->wq, &wait);
        
        // 3. 睡眠,等待被唤醒(或超时)
        for (;;) {
            set_current_state(TASK_INTERRUPTIBLE);
            if (ep_events_available(ep) || timeout <= 0)
                break;
            timeout = schedule_hrtimeout_range(timeout, ...);
        }
        
        // 4. 被唤醒后,移除等待队列
        __remove_wait_queue(&ep->wq, &wait);
    }
    
    // 5. 扫描就绪链表,拷贝到用户空间 ⭐
    if (ep_events_available(ep))
        res = ep_send_events(ep, events, maxevents);
    
    return res;
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.

2. 关键函数:ep_send_events()

复制

static int ep_send_events(struct eventpoll *ep,
                          struct epoll_event __user *events,
                          int maxevents)
{
    struct ep_send_events_data esed;
    
    esed.maxevents = maxevents;
    esed.events = events;
    
    // 扫描就绪链表 rdllist
    return ep_scan_ready_list(ep, ep_send_events_proc, &esed);
}

static int ep_send_events_proc(struct eventpoll *ep, struct list_head *head,
                                void *priv)
{
    struct ep_send_events_data *esed = priv;
    struct epitem *epi;
    int eventcnt = 0;
    
    // 遍历就绪链表
    list_for_each_entry_safe(epi, tmp, head, rdllink) {
        // 再次检查是否真的就绪(可能已经被处理)
        revents = ep_item_poll(epi, NULL);
        
        if (revents) {
            // 拷贝到用户空间
            if (__put_user(revents, &uevent->events) ||
                __put_user(epi->event.data, &uevent->data)) {
                return eventcnt ? eventcnt : -EFAULT;
            }
            eventcnt++;
            uevent++;
            
            // LT模式:保留在链表中
            // ET模式:从链表移除
            if (!(epi->event.events & EPOLLET)) {
                list_add_tail(&epi->rdllink, &ep->rdllist);
            }
        }
    }
    
    return eventcnt;
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.

关键点:

  • 只遍历rdllist就绪链表,不遍历红黑树
  • 复杂度:O(就绪fd数量)

3. LT vs ET 模式的本质区别

水平触发(LT,Level Triggered):

  • fd就绪后,会一直保留在就绪链表中
  • epoll_wait会反复通知,直到数据被读完

边缘触发(ET,Edge Triggered):

  • fd就绪后,只加入就绪链表一次
  • epoll_wait只通知一次,必须一次性读完所有数据

实现上的区别:复制

// ep_send_events_proc 中
if (!(epi->event.events & EPOLLET)) {
    // LT模式:把epitem重新加回就绪链表
    list_add_tail(&epi->rdllink, &ep->rdllist);
} else {
    // ET模式:从就绪链表移除
    ep_pm_stay_awake(epi);
    list_del_init(&epi->rdllink);
}1.2.3.4.5.6.7.8.9.

为什么ET模式性能更好?

  • LT模式:每次epoll_wait都可能返回同一个fd(如果数据没读完)
  • ET模式:只返回一次,减少无谓的epoll_wait调用

七、完整流程图:从添加fd到返回就绪事件

复制

┌─────────────────────────────────────────────────────────────┐
│                  用户态调用 epoll_ctl(ADD, fd)               │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 内核态:ep_insert()                                          │
│  1. 创建 epitem 节点                                         │
│  2. 调用 fd->ops->poll(),注册回调 ep_poll_callback          │
│  3. 插入红黑树                                               │
│  4. 如果fd当前就绪,加入就绪链表                              │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 数据结构状态                                                 │
│                                                             │
│  eventpoll                                                  │
│  ┌──────────┐           红黑树                              │
│  │   rbr    │────→      [epitem:fd=5]                      │
│  │ rdllist  │────→      NULL (空链表)                       │
│  └──────────┘                                               │
│                                                             │
│  socket(fd=5)                                               │
│  ┌──────────┐                                               │
│  │等待队列   │────→ [eppoll_entry(回调=ep_poll_callback)]   │
│  └──────────┘                                               │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 网卡收到数据,触发硬件中断                                     │
│  ↓                                                           │
│ 内核网络层处理                                                │
│  ↓                                                           │
│ 唤醒 socket 等待队列                                          │
│  ↓                                                           │
│ 执行回调:ep_poll_callback()                                 │
│  1. 检查事件类型是否匹配                                       │
│  2. 把 epitem(fd=5) 加入就绪链表 rdllist                      │
│  3. 唤醒阻塞在 epoll_wait 的进程                              │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 数据结构状态                                                 │
│                                                             │
│  eventpoll                                                  │
│  ┌──────────┐                                               │
│  │ rdllist  │────→ [epitem:fd=5] ⭐ (新加入)                │
│  └──────────┘                                               │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 用户态调用 epoll_wait()                                       │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 内核态:ep_poll()                                             │
│  1. 检查 rdllist 是否为空                                     │
│     - 不为空:直接进入步骤4                                    │
│     - 为空:进入步骤2                                         │
│  2. 把当前进程加入 ep->wq 等待队列                             │
│  3. 进程睡眠,等待被唤醒或超时                                 │
│  4. 调用 ep_send_events(),扫描就绪链表                       │
│  5. 把就绪事件拷贝到用户空间                                   │
│  6. 返回就绪fd数量                                            │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 用户态收到就绪事件                                            │
│  events[0].fd = 5                                           │
│  events[0].events = EPOLLIN                                 │
└─────────────────────────────────────────────────────────────┘1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.

八、性能对比总结:为什么epoll是O(1)

1. select/poll vs epoll 的本质区别

维度select/pollepoll
数据结构数组红黑树 + 就绪链表
fd管理每次调用都要传递全部fdfd在内核中持久化存储
查找就绪fd遍历全部fd只遍历就绪链表
通知方式主动轮询事件驱动回调
内存拷贝每次调用都拷贝只在添加/删除时拷贝
时间复杂度O(n)O(1) ~ O(就绪fd数)
fd数量限制select有1024限制理论上无限制(受系统资源限制)

2. epoll高性能的三大支柱

(1) 红黑树 — 高效管理海量fd

  • 添加/删除/查找:O(log n)
  • 避免了select/poll的全量遍历

(2) 就绪链表 — 只关心就绪的fd

  • epoll_wait只遍历就绪链表
  • 不管监听10个还是10万个fd,只处理就绪的那几个

(3) 回调机制 — 事件驱动,自动加入链表

  • fd就绪时,内核自动把epitem加入就绪链表
  • 避免了主动轮询的开销

3. 一个直观的类比

select/poll 就像:复制

你在超市找3个成熟的苹果,
店员让你把10000个苹果全搬出来挨个检查,
每次购物都得重复这个过程。1.2.3.

epoll 就像:复制

你告诉店员要监控这10000个苹果,
店员给每个苹果装了传感器,
苹果成熟时会自动发通知,
你只需要去取走成熟的那3个。1.2.3.4.

九、进阶:epoll的其他优化细节

1. 惊群问题的优化

惊群(Thundering Herd):多个进程/线程监听同一个epoll实例,当事件到来时,内核会唤醒所有等待的进程,但只有一个能处理,其他白白浪费CPU。

解决方案:Linux 4.5引入了EPOLLEXCLUSIVE标志。复制

// 只唤醒一个进程
ev.events = EPOLLIN | EPOLLEXCLUSIVE;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);1.2.3.

内核实现(在ep_poll()中):复制

// 添加进程到等待队列时
__add_wait_queue_exclusive(&ep->wq, &wait);  // 注意 exclusive1.2.

原理:等待队列的独占标志,确保只唤醒一个进程。

2. overflow list的优化

问题:在epoll_wait扫描就绪链表时,如果又有新事件到来怎么办?

解决方案:使用overflow list(ep->ovflist)。复制

// ep_send_events() 中
ep->ovflist = NULL;  // 激活overflow list

// 扫描rdllist的同时,新到的事件加入ovflist

// 扫描完成后,把ovflist的事件合并回rdllist1.2.3.4.5.6.

作用:避免在扫描过程中持有锁,提高并发性能。

3. oneshot模式

EPOLLONESHOT:事件触发一次后自动从就绪链表移除,避免重复触发。复制

ev.events = EPOLLIN | EPOLLONESHOT;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);1.2.

应用场景:多线程环境,确保同一个fd不会被多个线程同时处理。

十、实战:手写一个mini epoll服务器

理论讲完了,来点实战。下面是一个最精简的epoll服务器示例:复制

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

#define MAX_EVENTS 10
#define PORT 8888

// 设置非阻塞
void setnonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    int listen_fd, conn_fd, epoll_fd, nfds;
    struct epoll_event ev, events[MAX_EVENTS];
    struct sockaddr_in addr;
    
    // 1. 创建监听socket
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    setnonblocking(listen_fd);
    
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    
    bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(listen_fd, 128);
    
    // 2. 创建epoll实例
    epoll_fd = epoll_create1(0);
    
    // 3. 添加监听socket到epoll
    ev.events = EPOLLIN | EPOLLET;  // ET模式
    ev.data.fd = listen_fd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
    
    // 4. 事件循环
    while (1) {
        // 等待事件(阻塞)
        nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        
        // 处理就绪事件
        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == listen_fd) {
                // 新连接
                while ((conn_fd = accept(listen_fd, NULL, NULL)) > 0) {
                    setnonblocking(conn_fd);
                    ev.events = EPOLLIN | EPOLLET;
                    ev.data.fd = conn_fd;
                    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev);
                }
            } else {
                // 数据到达,ET模式必须一次性读完
                char buf[1024];
                int n;
                while ((n = read(events[i].data.fd, buf, sizeof(buf))) > 0) {
                    write(events[i].data.fd, buf, n);  // echo回去
                }
                if (n == 0) {
                    // 连接关闭
                    close(events[i].data.fd);
                }
            }
        }
    }
    
    close(listen_fd);
    close(epoll_fd);
    return0;
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.

关键点:

  • ET模式配合非阻塞socket
  • accept和read都要循环调用,直到返回EAGAIN
  • epoll_wait只处理就绪的fd

十一、常见面试追问

(1) Q1: epoll的缺点是什么?

A:

  • 只支持Linux,不可移植(FreeBSD用kqueue,Windows用IOCP)
  • 不支持普通文件fd,只支持socket、pipe等
  • ET模式编程复杂,容易出bug(必须一次性读完)
  • 小规模连接场景不如poll,因为有额外的红黑树操作开销

(2) Q2: epoll适合什么场景?

A:

  • ✅ 高并发长连接场景(如IM服务器、游戏服务器)
  • ✅ 大量连接,但活跃连接少(如反向代理)
  • ✅ 需要精确控制事件的场景(支持ET/LT模式)
  • ❌ 不适合连接数少的场景(select/poll更简单)
  • ❌ 不适合需要跨平台的场景

(3) Q3: epoll和io_uring的区别?

A:

  • epoll:事件通知机制,告诉你”fd可读/可写了”,还需要用户态调用read/write
  • io_uring:真正的异步IO,提交读写请求到内核队列,内核完成后通知你
  • 性能:io_uring更强,但需要Linux 5.1+,且编程更复杂

(4) Q4: 为什么Nginx用epoll而不是io_uring?

A:

  • io_uring是Linux 5.1引入的,太新,生产环境支持度不够
  • epoll已经足够快,能满足绝大多数场景
  • epoll更成熟稳定,坑已经填完了
  • 未来Nginx可能会支持io_uring作为可选项

十二、总结:面试怎么回答

当面试官问”epoll为什么性能高”时,你可以这样回答:

epoll的高性能源于三大核心机制:

  • 红黑树管理fd所有被监听的fd存储在内核的红黑树中,添加/删除/查找都是O(log n),避免了select/poll每次调用都要传递全量fd数组。
  • 就绪链表内核维护一个就绪链表(rdllist),只有就绪的fd才会被加入。epoll_wait只遍历这个链表,而不是遍历所有fd,所以复杂度接近O(1)。
  • 回调机制当fd就绪时(比如socket收到数据),内核会自动调用ep_poll_callback,把对应的epitem加入就绪链表。这是事件驱动的,不需要主动轮询。

本质区别:

  • select/poll是主动轮询:每次都遍历所有fd
  • epoll是被动通知:fd就绪时主动加入就绪链表

这就是为什么10万并发时,epoll比select快4000倍的原因。

如果面试官继续追问细节,你就可以展开讲红黑树、回调注册、LT/ET模式等。

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容