epoll的作用

epoll 是 Linux 内核提供的一种 I/O 多路复用 机制。它的核心作用是:让单个进程(或线程)能够同时监听大量文件描述符(如 socket、管道、文件等),并在这些描述符就绪(可读、可写、出错)时得到通知,从而高效地处理 I/O 事件

epoll的工作原理

1、epoll 的三个核心数据结构

当调用 epoll_create() 时,内核会创建一个 struct eventpoll 对象,它主要包含两个核心成员:

成员 类型 作用
rbr 红黑树根节点 存放所有被监听的文件描述符及其关注的事件(epoll_event)。查找、插入、删除都是 O(log n)。
rdlist 双向链表 存放已经就绪(有事件发生)的文件描述符。epoll_wait 直接从这里取数据,O(1) 返回。
wq 等待队列 当进程调用 epoll_wait 且当前没有就绪事件时,进程会挂载到这里,直到被唤醒。
2、事件注册:epoll_ctl 做了什么

调用 epoll_ctl(EPOLL_CTL_ADD, fd, &ev) 时:

  1. 检查 fd 是否已经在红黑树中(避免重复添加)。

  2. 如果没有,就创建一个 epitem 结构(代表一个被监听的事件节点),包含:

    • 指向目标文件对象 struct file * 的指针

    • 用户关注的事件类型(EPOLLINEPOLLOUT 等)

    • 回调函数指针(关键)

  3. 将这个 epitem 插入到红黑树 rbr 中。

  4. 最重要的一步:向目标文件(如 socket)的等待队列中添加一个回调入口。这个回调函数就是 ep_poll_callback

至此,内核已经“告诉”文件系统:当这个 fd 上发生任何事件(数据到达、写缓冲区空闲等),请立即调用 ep_poll_callback

3、事件发生:回调驱动

当某个被监听的 fd 上有事件发生时(比如网卡收到数据,TCP 栈把数据放入 socket 接收缓冲区):

  1. 文件系统(如 socket 的 tcp_data_ready 函数)会调用之前注册的 ep_poll_callback

  2. 这个回调函数会做三件事:

    • 将对应的 epitem 添加到 eventpoll 的 就绪链表 rdlist 中(如果还没在链表里)。

    • 如果当前有进程正阻塞在 epoll_wait 上(即挂在 wq 等待队列中),则唤醒该进程。

  3. 整个过程不涉及轮询,完全由事件驱动。

4、获取就绪事件:epoll_wait 如何工作

调用 epoll_wait(events, maxevents, timeout) 时:

  1. 检查 rdlist 是否为空。

    • 如果不为空:将 rdlist 中的事件复制到用户提供的 events 数组,并返回事件个数。复制后可以选择是否从链表中移除(取决于触发模式)。

    • 如果为空:将当前进程加入 wq 等待队列,然后调用 schedule_timeout() 让出 CPU。当有事件触发回调时,进程会被唤醒,重新检查 rdlist

  2. 边缘触发(ET)与水平触发(LT)的区别:

    • LT(水平触发,默认):内核在返回事件后,不会从 rdlist 中删除该 epitem,只要文件描述符依然处于就绪状态,下次 epoll_wait 还会再次返回它。

    • ET(边缘触发):内核在返回事件后立即从 rdlist 中删除该 epitem,应用程序必须一次性处理完所有数据,否则不会再次通知。

一.重要数据结构

struct eventpoll{
    //...
    struct rb_root rbr;    //红黑树,管理epoll的新事件
    struct list_head rdlist;    //就绪列表,保存epoll_wait返回满足条件的事件
};

//一个连接的io事件
struct epitem {
struct rb_node rbn;            //红黑树节点
struct list_head rdllist;      //双向链表节点
struct epoll_filefd ffd;       //事件句柄信息
struct eventpoll *ep;          //指向所属的eventpo11对象
struct epoll_event event;      //注册的事件类型
};

struct epoll_event {
    uint32_t     events;   //感兴趣的事件掩码(EPOLLIN、EPOLLOUT 等)
    epoll_data_t data;     //用户数据,可以是 int 或 void* 等 
};

typedef union epoll_data {
    void    *ptr;   // 可以指向任意用户数据(如连接对象)
    int      fd;    // 直接存放文件描述符
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

二.主要函数

1.epoll_creat系统调用
int epoll_creat(int flags);
  • 功能:创建一个 epoll 实例,返回一个文件描述符(epfd),后续操作都通过它进行。

  • 参数 flags

    • 0:默认行为(兼容旧版 epoll_create(int size),size 参数已忽略)。

    • EPOLL_CLOEXEC:当进程执行 exec 系列函数时,自动关闭这个 epoll 文件描述符,防止子进程继承。

  • 返回值:成功返回非负整数(epfd);失败返回 -1,并设置 errno

2.epoll_ctl系统调用
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
  • 功能:向 epoll 实例中添加、修改或删除要监控的文件描述符。

  • 参数详解

参数 含义
epfd epoll_create1() 返回的 epoll 实例文件描述符
op 操作类型:EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除)
fd 要监控的文件描述符(如 socket)
event 指向 struct epoll_event 的指针,描述感兴趣的事件及用户数据。当 op 为 EPOLL_CTL_DEL 时可为 NULL

常用 events 标志

标志 含义
EPOLLIN 可读事件(socket 接收缓冲区有数据,或监听 socket 有新连接)
EPOLLOUT 可写事件(socket 发送缓冲区有空闲,可以发送数据)
EPOLLRDHUP 对端关闭连接(TCP 连接被对端关闭,某些内核版本支持)
EPOLLERR 错误事件(如连接被重置)
EPOLLHUP 挂起事件(对端挂断,通常与 EPOLLERR 一起出现)
EPOLLET 边缘触发模式(Edge Triggered),默认是水平触发(Level Triggered)
EPOLLONESHOT 一次性触发,事件发生后自动移除,需重新添加才能再次监控
3. epoll_wait()

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • 功能:等待 epoll 实例上的事件发生,返回就绪的事件列表。

  • 参数详解

参数 含义
epfd epoll 实例文件描述符
events 输出参数,指向 struct epoll_event 数组,用于接收就绪的事件
maxevents events 数组的大小(最多返回多少个事件)
timeout 超时时间(毫秒)。-1 表示阻塞直到有事件;0 表示立即返回(非阻塞);>0 表示等待指定毫秒数后超时返回 0
  • 返回值

    • 成功:返回就绪的事件个数(0 表示超时)。

    • 失败:返回 -1,设置 errno

三、触发模式详解

水平触发(Level Triggered, LT,默认)
  • 只要文件描述符处于可读/可写状态,epoll_wait 就会一直返回该事件。

  • 优点:编程简单,不易遗漏数据。

  • 缺点:可能多次唤醒,效率略低。

边缘触发(Edge Triggered, ET,高性能推荐)
  • 仅当文件描述符的状态发生变化(从无数据变为有数据,或发送缓冲区从满变为不满)时,才返回一次事件。

  • 必须以非阻塞方式循环读/写,直到系统调用返回 EAGAIN 或 EWOULDBLOCK

  • 优点:减少系统调用次数,效率更高。

  • 缺点:编程复杂,必须一次处理完所有数据。

设置 ET 模式
ev.events = EPOLLIN | EPOLLET; // 添加 EPOLLET 标志

四.扩展

fcntl 函数族

  • fcntl(fd, F_GETFL, 0):获取文件描述符 fd 当前的状态标志(如是否非阻塞)。

  • fcntl(fd, F_SETFL, flags):设置文件描述符的状态标志。

  • O_NONBLOCK:非阻塞标志。当文件描述符被设置为非阻塞时,recv/send 等操作不会阻塞进程,如果没有数据可读或缓冲区已满,会立即返回 -1 并设置 errno 为 EAGAIN 或 EWOULDBLOCK

EAGAIN 和 EWOULDBLOCK

  • 在使用非阻塞 socket 时,如果 recv 没有数据可读,或 send 的发送缓冲区已满,系统调用会返回 -1,并设置 errno 为 EAGAIN 或 EWOULDBLOCK(这两个值在 Linux 上通常相同)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐