IO多路复用与linux epoll

libevent_benchmark

定义:I/O多路复用是指在单线程中同时监视多个文件描述符的状态变化(如可读、可写、异常等),当其中一个或多个文件描述符发生状态变化时,内核会通知应用程序进行相应的处理。

1. epoll 与 select/poll 区别

epoll的实现代码fs/eventpoll.c,其分为三个接口函数:

  • epoll_create: 创建一个epoll实例,返回一个文件描述符。
  • epoll_ctl: 向epoll实例中添加、修改或删除需要监视的文件描述符。
  • epoll_wait: 等待epoll实例中监视的文件描述符发生状态变化,并返回就绪的文件描述符列表。

具体流程分为两个阶段:

eventpoll_epitem

注册阶段(epoll_ctl EPOLL_CTL_ADD):

  1. 为目标 fd 创建 epitem,插入 ep->rbr 红黑树。
  2. 调用 ep_item_poll(epi, &epq.pt, 1),内部通过 ep_ptable_queue_proc 分配一个 eppoll_entry,将 ep_poll_callback 注册为回调函数,并通过 add_wait_queue 将其挂入目标 fd(如 socket)自身的等待队列,完成事件监听挂钩。

等待/触发阶段(epoll_wait):

  1. epoll_wait 调用 ep_poll,先检查 ep->rdllist 是否有就绪事件:若有则直接调用 ep_try_send_events 传递事件并返回;若无则将当前进程加入 ep->wqepoll 实例自身的等待队列),挂起进程等待唤醒。
  2. 当目标 fd(如 socket)收到数据后,内核驱动层(网络栈/设备驱动)调用 wake_up() 唤醒该 fd 自身的等待队列,从而触发挂在其上的回调 ep_poll_callback
  3. ep_poll_callback 将对应 epitem 加入 ep->rdllist(若此时正在向用户空间传输事件,则暂存于 ep->ovflist 溢出链表),然后调用 wake_up(&ep->wq) 唤醒 epoll_wait 中挂起的进程。
  4. 进程被唤醒,ep_poll 调用 ep_send_events,后者先通过 ep_start_scan 调用 list_splice_init(&ep->rdllist, txlist)rdllist 中的 epitem 原子地转移到临时链表 txlist,同时将 ep->ovflistEP_UNACTIVE_PTR 置为 NULL,以接收此传输期间新到的事件。
  5. ep_send_events 遍历 txlist 中每个 epitem,调用 ep_item_poll(内部调用 vfs_poll)获取 fd 上最新的 events(防止使用过时状态),再将 events 与 epoll_event.data 通过 epoll_put_uevent 拷贝到用户空间。LT 模式下若事件仍未处理完,epitem 会被重新加回 ep->rdllist,保证下次 epoll_wait 仍能返回;ET 模式则不重新加入。
  6. ep_done_scanovflist 中传输期间到达的新事件合并回 ep->rdllist,并在 rdllist 非空时再次调用 wake_up(&ep->wq) 通知等待者。

epoll_wait流程如下:

epoll_wait_flowchart

图中涉及两个不同的等待队列:目标 fd(如 socket)自身的等待队列(ep_poll_callback 注册于此,收到数据时被触发);以及 ep->wq(epoll 实例的等待队列,epoll_wait 调用者挂于此处休眠)。两者概念不同,需加以区分。

1.1. epoll 与 poll/select 特点总结

select/poll给定一个fd数组,每次都是从用户态传递到内核态,内核态遍历该数组,填充有事件发生的fd。然后内核态将该数组传回用户态,用户态再次遍历该数组,处理有事件发生的fd。因此,select/poll的时间复杂度为O(n)

epoll只在有事件发生时才通知用户程序,只将就绪的fd返回给用户程序。epoll有如下特点:

  • 红黑树: 内核中epoll_ctl将需要监听的文件描述符(针对网络通信就是套接字)时,保存在红黑树中。添加/删除/索引的时间复杂度为O(log n)
  • rdllist: ep->rdllist是内核存储的就绪事件列表,当有事件发生时比如套接字数据可读,驱动将fd对应的epitem加入到rdllist中(通过fd等待队列上的回调函数ep_poll_callback)。epoll_wait通过ep_start_scanrdllist转移到临时txlist后扫描处理,更新对应的epoll_event数据,并返回给用户程序。

2. ET模式与LT模式区别

LT模式:当fd就绪时,epoll_wait 会一直返回该fd,直到事件被处理。例如,如果一个socket连接有数据可读,epoll_wait 会每次都返回该socket fd,直到数据被完全读取。

ET模式: 当fd从未就绪变为就绪时,epoll_wait 只会返回一次该fd。例如,如果一个socket有数据可读(kernel中缓冲区由空变为非空),epoll_wait 只会在数据第一次到达时返回该套接字,之后即使有更多数据到达,也不会再次返回。

ET的这个特性使得其要求应用程序必须一次性读取所有数据,否则可能会错过后续的数据到达事件,不能实时处理数据。因此,使用ET模式时,必须将套接字设置为非阻塞模式,并在事件处理函数中循环读取数据,直到返回EWOULDBLOCK错误。

以下是ET模式下的事件处理示例:

while (1) {
    ssize_t n = read(fd, buffer, sizeof(buffer));
    if (n == -1) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            break; // 数据读完
        }

        // 处理其他异常情形
    }

    // 处理读取的数据
}

明显的,ET模式相较于LT模式,减少了系统调用次数,提高了性能。

3. 设置socket为非阻塞

Blocking read Non-blocking read
blocking_read nonblocking_read

ET模式下,使用阻塞模式socket,如果数据量较大,需要多次read,最后一次可能没有数据可读,此时read将一直阻塞。使用非阻塞模式socketread返回EWOULDBLOCK即代表数据读完。

4. epoll 调用流程图

graph TD
    A[启动服务器] --> B[创建 epoll 实例]
    B --> C[创建监听套接字]
    C --> D[设置监听套接字为非阻塞]
    D --> E[绑定监听套接字到指定端口]
    E --> F[监听连接请求]
    F --> G[将监听套接字添加到 epoll 实例中]
    G --> H[进入事件循环]
    H --> I[调用 epoll_wait 等待事件发生]
    I --> J{有事件发生吗?}
    J -->|是| K[处理就绪事件]
    J -->|否| I
    K --> L{事件类型}
    L -->|新连接| M[接受新连接]
    M --> N[设置新连接套接字为非阻塞]
    N --> O[将新连接套接字添加到 epoll 实例中]
    L -->|可读事件| P[读取数据]
    P --> Q{读取成功吗?}
    Q -->|是| R[处理读取的数据]
    Q -->|否| S[关闭套接字]
    R --> T[将套接字修改为可写事件]
    L -->|可写事件| U[写入数据]
    U --> V[将套接字修改为可读事件]
    S --> H
    O --> H
    T --> H
    V --> H

5. libEvent

libEvent是一个跨平台的事件通知库,提供了统一的接口来处理不同操作系统上的事件驱动机制(如selectpollepoll等)。它封装了底层的事件处理细节,使得开发者可以更方便地编写高性能的网络应用程序。

libEvent可以处理的事件类型包括(还有很多高级功能没有列出来,比如处理复杂的事件组合、优先级事件等):

  • I/O事件:如套接字可读、可写等。
  • 定时器事件:在指定时间后触发的事件。
  • 信号事件:当特定信号发生时触发的事件,使用evsigsel

libEvent基本使用流程为,首先创建一个事件基础设施(event_base),然后为每个需要监视的事件创建一个事件对象(event),并将其添加到基础设施中。最后,调用event_base_dispatch进入事件循环,等待事件发生并处理。

资源:

参考




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • NVIDIA GPU 架构:SP、SM 与 LSU 工作原理详解
  • al-folio 模板定制修改总结
  • al-folio 部署记录(Ubuntu 24.04)
  • Ubuntu 26.04 安装 Docker 和 Docker Compose
  • C++ Traits