Linux epoll 深度解析:关键要素与应用概览

13小时前发布 gsjqwyl
1 0 0

Linux epoll的深入剖析:关键要点与应用总览

什么是epoll?

epoll是自Linux 2.6版本开始,由Linux内核提供的一种高效I/O事件通知机制,用于解决传统select和poll在处理大量并发连接时存在的遍历效率低下、最大数量限制以及数据频繁拷贝等问题。它能够对多种文件描述符(诸如socket、管道、eventfd、timerfd等)上的I/O事件进行监听。

核心特性

高效的事件驱动模型

  • 时间复杂度优势:无论监控多少个文件描述符(FD),epoll进行事件检测的时间复杂度几乎稳定在O(1),而select和poll的时间复杂度为O(n)。
  • 仅返回活跃的FD:与select和poll需要遍历所有FD不同,epoll只会把发生事件的FD返回,从而减少了无效遍历。

支持大并发连接

  • 单个进程能够轻松管理数十万乃至百万级别的并发连接,像Nginx、Redis就是借助epoll实现高并发的。
  • 采用红黑树(RB – tree)来存储FD,使得查找、插入、删除操作都十分高效。

边缘触发与水平触发模式

  • 水平触发(LT,默认模式):只要FD处于可读或可写状态,epoll就会持续向应用程序发送通知,这与poll的机制类似。
  • 边缘触发(ET):仅在FD状态发生变化时才通知一次,这种模式效率更高,但需要正确处理,否则可能会丢失事件。

epoll的使用方法

epoll_create

int epoll_create(int size);

其作用是创建一个epoll实例,得到用于后续对epoll进行所有调用的epoll文件描述符。当不再需要时,调用close()进行关闭,当所有引用该epoll实例的文件描述符都关闭后,内核会销毁该实例并释放相关资源以供重用。返回值方面,成功时返回文件描述符,出错时返回 – 1并设置errno。

epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

该函数用于对要监听的fd进行添加、修改或删除操作。通过对由epfd引用的epoll实例执行控制操作,对目标文件描述符fd执行op操作。event参数描述了与文件描述符fd相关联的对象。

op参数

op 功能 说明
EPOLL_CTL_ADD 将新的fd添加到epoll实例中 如果fd已存在则返回EEXIST
EPOLL_CTL_MOD 修改fd的监听事件 如果fd不存在则返回ENOENT
EPOLL_CTL_DEL 从epoll实例中移除fd event参数可以为NULL

事件

头文件eventpoll.h中的宏定义:

#define EPOLLIN     (__force __poll_t)0x00000001
#define EPOLLOUT    (__force __poll_t)0x00000004
#define EPOLLERR    (__force __poll_t)0x00000008
#define EPOLLHUP    (__force __poll_t)0x00000010
#define EPOLLRDHUP  (__force __poll_t)0x00002000
#define EPOLLEXCLUSIVE  ((__force __poll_t)(1U << 28))
#define EPOLLET     ((__force __poll_t)(1U << 31))

epoll_event结构体定义:

typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
};

epoll的事件类型是在epoll_event.events中设置的,epoll会依据这些事件类型来判断是否将fd加入活跃队列,最终由epoll_wait()返回。

宏定义含义

宏定义 含义
EPOLLIN 表示对应的文件描述符可以进行读操作(recv/read)
EPOLLOUT 表示对应的文件描述符可以进行写操作(send/write)
EPOLLRDHUP 表示对方关闭了写端或者处于半关闭状态(对TCP很有用)
EPOLLERR 表示对应的文件描述符发生错误
EPOLLHUP 表示对应的文件描述符被挂断(对方断开连接)
EPOLLET 边缘触发(Edge Trigger)
EPOLLONESHOT 事件只触发一次,触发后自动从epoll中移除

返回值:成功时返回文件描述符,出错时返回 – 1并设置errno。

epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

该函数用于等待由epfd指向的epoll实例上的事件。events指向的内存区域将包含调用者可用的事件。epoll_wait()最多返回maxevents个事件,maxevents参数必须大于0。

timeout参数指定epoll_wait()阻塞的最小毫秒数。将timeout指定为 – 1会使epoll_wait()无限期阻塞,指定为0会使epoll_wait()立即返回,即便没有可用事件。

返回值:成功时返回已就绪的I/O文件描述符数量;如果在请求的timeout毫秒数内没有文件描述符就绪,则返回0;出错时返回 – 1并设置errno。

核心数据结构

eventpoll

eventpoll是epoll的控制中心,它用红黑树来管理监听的fd,rdllist是触发事件队列,wq是epoll_wait阻塞队列,poll_wait供file->poll使用,通过锁来保护多核并发。事件触发依靠回调,回调会唤醒epoll_wait,事件会被放到rdllist中。

/*
 * This structure is stored inside the "private_data" member of the file
 * structure and represents the main data structure for the eventpoll
 * interface.
 */
struct eventpoll {
    /*
     * This mutex is used to ensure that files are not removed
     * while epoll is using them. This is held during the event
     * collection loop, the file cleanup path, the epoll file exit
     * code and the ctl operations.
     */
    struct mutex mtx;

    /* Wait queue used by sys_epoll_wait() */
    wait_queue_head_t wq;

    /* Wait queue used by file->poll() */
    wait_queue_head_t poll_wait;

    /* List of ready file descriptors */
    struct list_head rdllist;

    /* Lock which protects rdllist and ovflist */
    rwlock_t lock;

    /* RB tree root used to store monitored fd structs */
    struct rb_root_cached rbr;

    /*
     * This is a single linked list that chains all the "struct epitem" that
     * happened while transferring ready events to userspace w/out
     * holding ->lock.
     */
    struct epitem *ovflist;

    /* wakeup_source used when ep_scan_ready_list is running */
    struct wakeup_source *ws;

    /* The user that created the eventpoll descriptor */
    struct user_struct *user;

    struct file *file;

    /* used to optimize loop detection check */
    int visited;
    struct list_head visited_list_link;

#ifdef CONFIG_NET_RX_BUSY_POLL
    /* used to track busy poll napi_id */
    unsigned int napi_id;
#endif
};

成员描述

  • mtx:全局互斥锁,确保在事件收集、关闭fd、epoll_ctl操作时epoll的一致性。
  • wq:epoll_wait阻塞队列,没有事件时,调用epoll_wait的进程会挂在这个队列上。
  • poll_wait:供file->poll()使用的等待队列,内核中poll实现底层回调时会用到。
  • rdllist:就绪事件链表,fd触发事件时,内核会把对应epitem放到这里,供epoll_wait返回给用户。
  • ovflist:单链表,当rdllist被锁定遍历,向用户空间发送数据时,rdllist不允许被修改,新触发的就绪epitem会被ovflist串联起来,等待rdllist处理完后,再将ovflist数据写入rdllist。具体可查看ep_scan_ready_list逻辑。
  • user:拥有这个epoll的用户,用于内核权限检查、资源限制等。
  • lock:锁,保护rdllist和ovflist。
  • rbr:红黑树根节点,用来管理所有注册到epoll的fd(epitem)。
  • file:eventpoll对应的文件结构,Linux中一切皆文件,用vfs管理数据。
  • napi_id:应用于中断缓解技术。

epitem

epitem是epoll中管理单个fd事件状态的核心单元,它挂在红黑树上的rbn,就绪事件挂在rdllist,等待队列挂在pwqlist,将用户关心的事件拷贝到event里,容器指针是ep,fd信息是ffd,这样能高效组织并高效唤醒。

/*
 * Each file descriptor added to the eventpoll interface will
 * have an entry of this type linked to the "rbr" RB tree.
 * Avoid increasing the size of this struct, there can be many thousands
 * of these on a server and we do not want this to take another cache line.
 */
struct epitem {
    union {
        /* RB tree node links this structure to the eventpoll RB tree */
        struct rb_node rbn;
        /* Used to free the struct epitem */
        struct rcu_head rcu;
    };

    /* List header used to link this structure to the eventpoll ready list */
    struct list_head rdllink;

    /*
     * Works together "struct eventpoll"->ovflist in keeping the
     * single linked chain of items.
     */
    struct epitem *next;

    /* The file descriptor information this item refers to */
    struct epoll_filefd ffd;

    /* Number of active wait queue attached to poll operations */
    int nwait;

    /* List containing poll wait queues */
    struct list_head pwqlist;

    /* The "container" of this item */
    struct eventpoll *ep;

    /* List header used to link this item to the "struct file" items list */
    struct list_head fllink;

    /* wakeup_source used when EPOLLWAKEUP is set */
    struct wakeup_source __rcu *ws;

    /* The structure that describe the interested events and the source fd */
    struct epoll_event event;
};

成员描述

  • rbn:挂在eventpoll->rbr红黑树上。
  • rcu:需要释放epitem时用RCU延迟删除。
  • rdllink:链接到eventpoll->rdllist,表示就绪事件。fd有事件触发时,内核会把它放到rdllist里。
  • next:用于维护eventpoll->ovflist单向链表的next指针,防止拷贝事件到用户空间时遗漏新事件。
  • ffd:记录节点对应的fd和file文件信息。
  • nwait:等待队列个数。
  • pwqlist:等待事件回调队列。数据进入网卡时,底层中断会执行ep_poll_callback。
  • ep:eventpoll指针,epitem关联eventpoll。
  • fllink:epoll文件链表结点,与epoll文件链表进行关联file.f_ep_links。参考fs.h, struct file结构。
  • ws:EPOLLWAKEUP模式下使用。
  • event:用户关注的事件。

epoll的网络服务器流程图

初始化阶段

  • socket():创建server_fd
  • bind():绑定IP和端口
  • fcntl():设置非阻塞模式(O_NONBLOCK)
  • listen():监听端口,准备接收连接

epoll创建与注册

epoll_fd = epoll_create()
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, EPOLLIN)
  • epoll_create():创建epoll实例,得到epoll_fd
  • epoll_ctl(EPOLL_CTL_ADD):将server_fd加入epoll_fd,监听EPOLLIN(监听是否有新连接)

事件循环

epoll_wait(epoll_fd, events, ...)
  • epoll_wait():阻塞等待事件,若有事件触发(新连接/客户端读写事件),返回事件数组

处理新连接(server_fd可读)

if (server_fd == events[i].data.fd) {
    client_fd = accept(server_fd);  // 非阻塞accept
    fcntl(client_fd, O_NONBLOCK);  // 设置非阻塞
    epoll_ctl(EPOLL_CTL_ADD, client_fd, EPOLLIN);  // 监控client_fd
}
  • accept()接收新连接,得到client_fd
  • 设置client_fd非阻塞
  • epoll_ctl(EPOLL_CTL_ADD),监听client_fdEPOLLIN

处理客户端数据(client_fd可读)

if (client_fd 有 EPOLLIN 事件) {
    ret = read(client_fd, buf, size);  // 非阻塞read
    if (ret > 0) {
        // 正常读取数据,处理业务逻辑
    } else if (ret == 0 || (ret == -1 && errno != EAGAIN)) {
        // 客户端关闭连接或出错
        close(client_fd);
        epoll_ctl(EPOLL_CTL_DEL, client_fd);  // 移除监控
    }
    // EAGAIN 表示数据未就绪,继续等待
}
  • 如果EPOLLIN:说明client_fd有数据可读,调用read()
  • ret > 0:正常读取数据,进行业务处理。
  • ret == 0:客户端关闭连接,关闭client_fd并从epoll移除。
  • ret == -1 && errno == EAGAIN:数据未就绪(非阻塞模式),继续等待。

处理客户端写入(EPOLLOUT事件)

if (需要向 client_fd 写入数据) {
    epoll_ctl(EPOLL_CTL_MOD, client_fd, EPOLLOUT);  // 关注可写事件
}

if (client_fd 有 EPOLLOUT 事件) {
    ret = write(client_fd, buf, size);  // 非阻塞write
    if (ret == -1 && errno == EAGAIN) {
        // 缓冲区满,稍后重试
    } else if (ret >= 0) {
        // 写入成功,恢复监控 EPOLLIN
        epoll_ctl(EPOLL_CTL_MOD, client_fd, EPOLLIN);
    } else {
        // 写入失败,关闭连接
        close(client_fd);
        epoll_ctl(EPOLL_CTL_DEL, client_fd);
    }
}
  • 如果EPOLLOUT:说明client_fd可写,调用write()
  • ret >= 0:写入成功,恢复监控EPOLLIN
  • ret == -1 && errno == EAGAIN:缓冲区满,稍后重试。

关闭连接

  • close()关闭server_fd和所有client_fd,结束服务

TCP + epoll流程图

这是一张基于Linux 5.0.1内核下,关于epoll的网络编程以及TCP连接建立相关的流程图,涵盖了从客户端连接请求到服务器端处理的多个环节,下面简单介绍流程:

epoll_create

  • 创建eventpoll结构体,初始化红黑树rbr和就绪队列rdlist
  • 返回epoll_fd

epoll_ctl

  • EPOLL_CTL_ADD把目标fd包装成epitem节点
  • 挂到eventpoll->rbr红黑树里
  • 建立sock->sk_wq的回调ep_poll_callback

③ 监听事件队列

  • epitem挂到socket->wq->wait_queue_head
  • 事件触发时由回调唤醒挂在wait_queue上的epitem

epoll_wait

  • 把当前线程挂到eventpoll->wq上,进入TASK_INTERRUPTIBLE
  • 调用schedule()让出CPU等待事件

⑤ 网络事件到达(TCP三次握手)

  • 驱动收到网卡中断,触发tcp_v4_rcv
  • 调用sock_def_wakeup唤醒wait_queue上的epitem

ep_poll_callback

  • 将就绪的epitem节点挂到eventpoll->rdlist
  • 设置wake_up_flag唤醒`epoll
© 版权声明

相关文章

暂无评论

暂无评论...