Linux内核IO多路复用epoll机制简介


前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。

内核IO多路复用epoll机制简介

1. epoll 核心设计思想与数据结构

传统 select/poll 的根本缺陷在于:每次调用都需要将包含大量文件描述符(FD) combat 集合的数组从用户态拷贝到内核态,并且内核需要线性遍历整个集合来检查就绪状态,时间复杂度为 O ( N ) O(N) O(N)

epoll 采用了空间换时间以及事件驱动的设计思想。它在内核中维护了一个持久化的上下文,将“事件注册”与“事件等待”分离开来。

epoll 的底层数据结构主要依赖于红黑树(Red-Black Tree)双向链表(Doubly Linked List)

  • 红黑树:用于高效管理(增删改查)用户注册的所有 FD,保证了操作的时间复杂度为 O ( log ⁡ N ) O(\log N) O(logN)
  • 就绪链表:用于存放当前已经有事件触发的 FD。当进程调用 epoll_wait 时,内核只需直接检查该链表是否为空,时间复杂度为 O ( 1 ) O(1) O(1)

核心结构体定义

在 Linux 内核源码 fs/eventpoll.c 中,最关键的结构体是 struct eventpollstruct epitem

// 代表一个 epoll 实例的内核对象
struct eventpoll {
    /* 保护此结构体的自旋锁 */
    spinlock_t lock;

    /* 互斥锁,用于在修改 epoll 结构时进行同步 */
    struct mutex mtx;

    /* 等待队列,用于存放调用 epoll_wait 陷入阻塞的进程 */
    wait_queue_head_t wq;

    /* 用于 epoll 自身作为被监听对象(嵌套 epoll)时的等待队列 */
    wait_queue_head_t poll_wait;

    /* 就绪双向链表,存放所有就绪事件的 epitem */
    struct list_head rdllist;

    /* 红黑树根节点,用于管理所有被监听的 file */
    struct rb_root_cached rbr;

    /* 这是一个单链表,保存着执行完 epoll_wait 准备向用户空间复制,
       但由于某些原因(如 LT 模式)需要重新放回 rdllist 的节点 */
    struct epitem *ovflist;

    /* 属性:创建该实例的用户信息 */
    struct user_struct *user;

    /* 对应的 file 结构体指针 */
    struct file *file;
    // ... 省略部分统计与优化字段
};

每个被监听的文件描述符在内核中都会对应一个 struct epitem 结构体:

// 代表红黑树中的一个节点(即一个被监听的 FD)
struct epitem {
    /* 红黑树节点,用于将当前结构挂载到 eventpoll 的 rbr 红黑树中 */
    struct rb_node rbn;

    /* 双向链表节点,用于将当前结构挂载到 eventpoll 的 rdllist 就绪链表中 */
    struct list_head rdllink;

    /* 处于下一级单链表 ovflist 的指针 */
    struct epitem *next;

    /* 包含被监听的 fd 和对应的 file 结构体指针 */
    struct epoll_filefd ffd;

    /* 轮询等待队列的个数 */
    int nwait;

    /* 包含当前项的等待队列项列表 */
    struct list_head pwqlist;

    /* 指向所属的 eventpoll 对象 */
    struct eventpoll *ep;

    /* 处于循环链表中的节点 */
    struct list_head fllink;

    /* 产生事件时的回调函数绑定的侦听器 */
    struct wakeup_source __rcu *ws;

    /* 用户注册的目标事件类型及私有数据 */
    struct epoll_event event;
};


2. 三大核心系统调用源码深度剖析

epoll 体系由 epoll_createepoll_ctlepoll_wait 三个系统调用组成。以下结合内核源码分析其执行流程。

2.1 epoll_create / epoll_create1

epoll_create 核心任务是分配 struct eventpoll 内存,并创建一个匿名的 file 结构体,将其与一个未使用的文件描述符(fd)绑定,最后返回该 fd 给用户。这样设计可以使用户像操作普通文件一样操作 epoll(如 close)。

内核入口函数通常为 SYSCALL_DEFINE1(epoll_create1, int, flags),其内部调用 do_epoll_create

static int do_epoll_create(int flags) {
    int error;
    struct eventpoll *ep = NULL;
    struct file *file;
    int fd;

    /* 检查非法 flags */
    if (flags & ~EPOLL_CLOEXEC)
        return -EINVAL;

    /* 1. 分配 eventpoll 内存并初始化各数据结构 */
    error = ep_alloc(&ep);
    if (error < 0)
        return error;

    /* 2. 获取一个未使用的文件描述符 fd */
    fd = get_unused_fd_flags(O_CLOEXEC | (flags & EPOLL_CLOEXEC));
    if (fd < 0) {
        error = fd;
        goto out_free_ep;
    }

    /* 3. 创建匿名匿名文件,绑定 eventpoll_fops 操作集 */
    /* 用户操作这个 fd 时的 read/write 等重定向到 epoll 的特有操作 */
    file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
                 O_RDWR | (flags & EPOLL_CLOEXEC));
    if (IS_ERR(file)) {
        error = PTR_ERR(file);
        goto out_put_fd;
    }
    
    ep->file = file;
    /* 4. 将 fd 与 file 建立映射,放入当前进程的 fd 数组中 */
    fd_install(fd, file);
    return fd;

out_put_fd:
    put_unused_fd(fd);
out_free_ep:
    ep_free(ep);
    return error;
}

2.2 epoll_ctl (重点:事件控制与回调注册)

epoll_ctl 负责对红黑树进行 ADDMODDEL 操作。其中 EPOLL_CTL_ADD 是最核心、最复杂的逻辑,它实现了底层驱动事件与 epoll 回调的对接。

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
        struct epoll_event __user *, event) {
    struct error_check; 
    struct fd f, tf;
    struct eventpoll *ep;
    struct epitem *epi;
    struct epoll_event epds;

    /* 从用户空间拷贝监听事件配置 */
    if (ep_op_has_event(op) && copy_from_user(&epds, event, sizeof(epds)))
        return -EFAULT;

    f = fdget(epfd); // 获取 epoll 本身的 file
    tf = fdget(fd);  // 获取目标被监听文件的 file

    ep = f.file->private_data;

    mutex_lock_nested(&ep->mtx, 0);

    /* 在红黑树中查找该 fd 是否已经存在 */
    epi = ep_find(ep, tf.file, fd);

    switch (op) {
    case EPOLL_CTL_ADD:
        if (!epi) {
            epds.events |= EPOLLERR | EPOLLHUP; // 默认监听错误事件
            /* 核心函数:向红黑树插入节点,并挂载回调 */
            error = ep_insert(ep, &epds, tf.file, fd, full_check);
        } else {
            error = -EEXIST;
        }
        break;
    case EPOLL_CTL_DEL:
        if (epi)
            error = ep_remove(ep, epi); // 从红黑树及就绪链表移除
        else
            error = -ENOENT;
        break;
    case EPOLL_CTL_MOD:
        if (epi) {
            /* 修改已有节点的监听事件 */
            error = ep_modify(ep, epi, &epds);
        } else {
            error = -ENOENT;
        }
        break;
    }
    mutex_unlock(&ep->mtx);
    // ... 省略资源释放
    return error;
}

深入分析 ep_insert 机制

ep_insert 内部通过调用目标文件的 poll 机制(针对网络套接字通常是 sock_poll,底层调用 tcp_poll),将自定义的回调函数注册到驱动的等待队列中。

static int ep_insert(struct eventpoll *ep, const struct epoll_event *event,
             struct file *tfile, int fd, int full_check) {
    int error;
    struct epitem *epi;
    struct ep_pqueue epq;

    /* 1. 分配并初始化 epitem 节点 */
    if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
        return -ENOMEM;

    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;

    /* 2. 初始化 ep_pqueue 适配器,设置回调包装函数 */
    epq.epi = epi;
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

    /* 3. 内部引发虚函数调用:tfile->f_op->poll() */
    /* 该调用会触发 ep_ptable_queue_proc,从而将 ep_poll_callback 绑定到设备驱动 */
    revents = ep_item_poll(epi, &epq.pt, 1);

    /* 4. 将 epitem 插入到 eventpoll 的红黑树中 */
    ep_rbtree_insert(ep, epi);

    /* 5. 如果当前设备在注册时就已经有就绪事件,直接将其放入就绪链表,并唤醒可能在等待的进程 */
    if ((revents & event->events) && !ep_is_linked(epi)) {
        list_add_tail(&epi->rdllink, &ep->rdllist);
        if (waitqueue_active(&ep->wq))
            wake_up(&ep->wq);
    }
    return 0;
}

ep_item_poll 的链路中,驱动程序通过调用 poll_wait 触发了先前设置的 ep_ptable_queue_proc

/* 驱动的 poll 机制回调该函数来建立等待项 */
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
                 poll_table *pt) {
    struct epitem *epi = ep_cb_from_pt(pt)->epi;
    struct eppoll_entry *pwq;

    if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
        /* 初始化真正的驱动等待队列项,将其核心回调函数设置为 ep_poll_callback */
        init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
        pwq->whead = whead;
        pwq->base = epi;
        
        /* 将等待项挂载到具体设备(如 Socket 接收缓冲区)的等待队列中 */
        add_wait_queue(whead, &pwq->wait);
        list_add_tail(&pwq->llink, &epi->pwqlist);
        epi->nwait++;
    } else {
        epi->nwait = -1;
    }
}

2.3 epoll_wait

epoll_wait 检查就绪链表 rdllist 是否有节点。如果没有,当前进程将让出 CPU,进入睡眠状态,直到被回调函数唤醒或超时。

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
        int, maxevents, int, timeout) {
    // ... 基础安全检查
    return do_epoll_wait(epfd, events, maxevents, timeout);
}

static int do_epoll_wait(int epfd, struct epoll_event __user *events,
             int maxevents, int timeout) {
    struct fd f;
    struct eventpoll *ep;

    f = fdget(epfd);
    ep = f.file->private_data;

    /* 调用 ep_poll 获取就绪事件 */
    error = ep_poll(ep, events, maxevents, timeout);
    return error;
}

ep_poll 实现逻辑
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
           int maxevents, long timeout) {
    int res = 0, eavail;
    unsigned long flags;
    wait_queue_entry_t wait;

    /* 1. 检查当前是否有可用就绪事件 */
    if (list_empty(&ep->rdllist)) {
        /* 没有事件,初始化一个等待队列项,关联当前进程 current */
        init_waitqueue_entry(&wait, current);
        /* 挂载到 eventpoll 的等待队列 wq 中 */
        __add_wait_queue_exclusive(&ep->wq, &wait);

        for (;;) {
            /* 将进程状态设置为可中断睡眠(TASK_INTERRUPTIBLE) */
            set_current_state(TASK_INTERRUPTIBLE);

            /* 再次检查是否有就绪事件或被信号中断 */
            if (!list_empty(&ep->rdllist) || !timeout)
                break;
            if (signal_pending(current)) {
                res = -EINTR;
                break;
            }

            /* 让出 CPU,进入睡眠,等待 timeout 或被驱动回调唤醒 */
            if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS)) {
                timeout = 0; // 超时
                break;
            }
        }
        /* 被唤醒后,将进程状态恢复为运行,并移出等待队列 */
        __remove_wait_queue(&ep->wq, &wait);
        __set_current_state(TASK_RUNNING);
    }

    /* 2. 检查是否有就绪事件需要拷贝给用户空间 */
    eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_NON_BLOCK;
    if (eavail && res == 0) {
        /* 核心函数:将事件数据从内核空间拷贝到用户空间的 events 数组 */
        res = ep_send_events(ep, events, maxevents);
    }
    return res;
}


3. 关键驱动机制:ep_poll_callback 的触发流程

当网卡收到数据包时,硬件中断触发驱动程序。驱动程序将数据解析并放入对应的 socket 缓冲区。此时,驱动会调用原本挂载在 socket->sk_sleep 队列上的唤醒函数,这个唤醒函数就是我们在 epoll_ctl 中注册的 ep_poll_callback

以下是 ep_poll_callback 的核心源码:

static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key) {
    int campgrounds;
    unsigned long flags;
    /* 通过 wait 针脚反向查找到对应的 eppoll_entry,进而拿到 epitem */
    struct epitem *epi = ep_item_from_wait(wait);
    struct eventpoll *ep = epi->ep;
    __poll_t pollflags = key_to_pollflags(key);

    spin_lock_irqsave(&ep->lock, flags);

    /* 1. 检查驱动传来的事件掩码是否与用户注册的事件匹配 */
    if (pollflags && !(pollflags & epi->event.events))
        goto out_unlock;

    /* 2. 判断当前 epitem 是否已经存在于就绪链表中,若存在则直接跳过防止重复添加 */
    if (ep_is_linked(epi))
        goto check_pages;

    /* 3. 核心步骤:将就绪的 epitem 添加到 eventpoll 的就绪双向链表末尾 */
    list_add_tail(&epi->rdllink, &ep->rdllist);

check_pages:
    /* 4. 唤醒在 epoll_wait 中阻塞等待的进程 */
    if (waitqueue_active(&ep->wq)) {
        wake_up(&ep->wq);
    }

out_unlock:
    spin_unlock_irqrestore(&ep->lock, flags);
    return 1;
}


4. LT(水平触发)与 ET(边缘触发)内核实现差异

LT(Level Triggered)和 ET(Edge Triggered)的本质区别在于:**当一次 epoll_wait 结束后,未处理完(或未完全读写完)的就绪 FD 是否会被自动重新放回 rdllist**

这一逻辑在 ep_send_events_proc(由 ep_send_events 调用)中实现:

static __initialize int ep_send_events_proc(void *priv, void *cookie, int call_nests) {
    struct ep_send_events_data *esed = priv;
    struct eventpoll *ep = esed->ep;
    struct epitem *epi;
    struct epoll_event __user *uevent = esed->events;
    struct list_head txlist;

    /* 1. 将 rdllist 上的所有节点转移到一个临时链表 txlist 中,清空 rdllist */
    list_splice_init(&ep->rdllist, &txlist);

    /* 2. 遍历临时链表 txlist */
    while (!list_empty(&txlist) && esed->res < esed->maxevents) {
        epi = list_first_entry(&txlist, struct epitem, rdllink);

        /* 将其从临时链表移除 */
        list_del_init(&epi->rdllink);

        /* 再次调用驱动的 poll 查看当前事件是否真实有效 */
        revents = ep_item_poll(epi, &pt, 1);
        revents &= epi->event.events;

        if (revents) {
            /* 将事件拷贝到用户空间 */
            if (__put_user(revents, &uevent->events) ||
                __put_user(epi->event.data, &uevent->data)) {
                /* 拷贝失败则将节点重新放回原就绪链表 */
                list_add(&epi->rdllink, &ep->rdllist);
                return -EFAULT;
            }
            esed->res++;
            uevent++;

            /* 3. 【核心区别所在】判断触发模式 */
            if (epi->event.events & EPOLLET) {
                /* ET 模式:既然已经通知了用户,就不再做任何操作 */
                /* 这个节点不会被放回 rdllist,直到下一次设备状态改变触发回调 */
            } else {
                /* LT 模式(默认):只要事件依然有效,就重新将节点放回 eventpoll 的 rdllist 中 */
                list_add_tail(&epi->rdllink, &ep->rdllist);
            }
        }
    }
    
    /* 4. 如果还有未处理完的节点(例如 maxevents 小于实际就绪数),全部放回 rdllist */
    if (!list_empty(&txlist)) {
        list_splice(&txlist, &ep->rdllist);
    }
    return 0;
}

机制对比总结

特性 / 行为水平触发 (LT)边缘触发 (ET)
内核实现本质事件复制到用户态后,若 FD 仍有事件,内核将其重新放回 rdllist事件复制到用户态后,内核直接将其从 rdllist 移除,不再放回。
用户态下一次 epoll_wait如果缓冲区还有数据未读完,epoll_wait立即返回,不会阻塞。除非有新数据到达触发中断,否则即使缓冲区有剩余数据,epoll_wait 也会保持阻塞
推荐编程模式支持阻塞与非阻塞 I/O,容错率高。必须搭配非阻塞 I/O(Non-blocking I/O),且必须循环 read/write 直到返回 EAGAIN
内核上下文切换开销相对较高,因为未读完的 FD 会反复触发 epoll_wait 返回。极低,每个事件仅通知一次,对高并发大连接场景性能更优。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值