深入浅出epoll:高效网络编程的关键

系统 Linux
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。

在如今这个数字化时代,网络应用的规模与日俱增,从大型电商平台到热门社交软件,每天都要处理海量的用户请求。对于服务器来说,如何高效地应对高并发场景,成为了一个关键问题。这就好比一家热闹的餐厅,用餐高峰时,大量顾客涌入,服务员需要巧妙地安排座位、上菜和结账,才能让餐厅顺利运转。而在服务器的世界里,epoll 就像是一位经验丰富的 “服务员”,它能帮助服务器高效地管理众多的网络连接,实现性能的飞跃。

也许你在开发网络应用时,正为服务器性能不佳而烦恼,或者你对 Linux 系统下的高性能 I/O 机制充满好奇。不管怎样,今天我们就一起踏上这场从零基础开始学习 epoll 的奇妙之旅,深入了解它的工作原理、使用方法以及如何通过它让你的服务器性能倍增,开启网络编程的新世界。

一、epoll简介

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

1.1epoll 概述

epoll 可是 Linux 下多路复用 I/O 接口的 “超级增强版”,专为应对高并发而生。与传统的 select 和 poll 相比,那优势可不是一星半点。select 在处理大量并发连接时,就像个没头苍蝇,每次都得把所有文件描述符集合一股脑从用户态拷贝到内核态,开销巨大,而且还得在内核里线性遍历这些描述符,看看哪个 “有事”,效率低得感人,关键它还有个致命弱点,默认最多只能处理 1024 个文件描述符,稍微多点连接就应付不来。

poll 虽说在一些方面改进了 select,比如不需要计算最大文件描述符加一的大小,对大批文件描述符处理速度稍快,基于链表存储没了最大连接数限制,但本质上还是得遍历所有描述符找就绪的,大量无谓的遍历让它在高并发下也力不从心。

epoll 就不一样了,它采用全新的设计理念。当创建一个 epoll 实例后,在内核中有个精心构建的数据结构,像是用红黑树来高效管理所有要监听的文件描述符,添加、删除操作那叫一个快,时间复杂度仅 O (log n);还有个就绪列表,通常用双向链表实现,专门存放已经就绪、有事件发生的文件描述符。

当调用 epoll_wait 时,压根不用像 select、poll 那样大海捞针般遍历所有描述符,只需瞅瞅这个就绪列表就行,轻松定位到 “有事” 的连接,大大节省了 CPU 时间。就好比在一个大型仓库里找几件特定物品,select 和 poll 是逐个货架、逐件货物查看,epoll 则是有个智能清单,直接指引到目标货物所在货架,效率高下立判。这使得 epoll 在面对海量并发连接时,系统资源开销小,响应迅速,成为众多高性能网络应用的坚实后盾。

epoll除了提供select/poll那种IO事件的电平触发 (Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。Linux2.6内核中对/dev/epoll设备的访问的封装(system epoll)。这个使我们开发网络应用程序更加简单,并且更加高效。

1.2为什么要使用epoll?

同样,我们在linux系统下,影响效率的依然是I/O操作,linux提供给我们select/poll/epoll等多路复用I/O方式(kqueue暂时没研究过),为什么我们对epoll情有独钟呢?原因如下:

⑴文件描述符数量的对比

epoll并没有fd(文件描述符)的上限,它只跟系统内存有关,我的2G的ubuntu下查看是20480个,轻松支持20W个fd。可使用如下命令查看:

cat /proc/sys/fs/file-max

再来看select/poll,有一个限定的fd的数量,linux/posix_types.h头文件中

#define __FD_SETSIZE    1024

⑵效率对比

当然了,你可以修改上述值,然后重新编译内核,然后再次写代码,这也是没问题的,不过我先说说select/poll的机制,估计你马上会作废上面修改枚举值的想法。

select/poll会因为监听fd的数量而导致效率低下,因为它是轮询所有fd,有数据就处理,没数据就跳过,所以fd的数量会降低效率;而epoll只处理就绪的fd,它有一个就绪设备的队列,每次只轮询该队列的数据,然后进行处理。

⑶内存处理方式对比

不管是哪种I/O机制,都无法避免fd在操作过程中拷贝的问题,而epoll使用了mmap(是指文件/对象的内存映射,被映射到多个内存页上),所以同一块内存就可以避免这个问题。

btw:TCP/IP协议栈使用内存池管理sk_buff结构,你还可以通过修改内存池pool的大小,毕竟linux支持各种微调内核。

二、深入探究epoll底层原理

2.1epoll 的设计思路

epoll 是在 select 出现 N 多年后才被发明的,是 select 和 poll 的增强版本。epoll 通过以下一些措施来改进效率。

措施一:功能分离

select 低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。每次调用 select 都需要这两步操作,然而大多数应用场景中,需要监视的 socket 相对固定,并不需要每次都修改。epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见的,效率就能得到提升。

相比 select,epoll 拆分了功能;为方便理解后续的内容,我们再来看看 epoll 的用法。如下的代码中,先用 epoll_create 创建一个 epoll 对象 epfd,再通过 epoll_ctl 将需要监视的 socket 添加到 epfd 中,最后调用 epoll_wait 等待数据。

int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的 socket 添加到 epfd 中 while(1){
    int n = epoll_wait(...)
    for(接收到数据的 socket){
        //处理
    }
}

功能分离,使得 epoll 有了优化的可能。

措施二:就绪列表

select 低效的另一个原因在于程序不知道哪些 socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 socket,就能避免遍历。

2.2epoll 实现过程

从网卡接收数据说起

在计算机的硬件世界里,网络数据的接收就像是一场精密的接力赛。想象一下,计算机由 CPU、内存、网卡等关键部件组成,就像一个高效运转的工厂。当网卡收到网线传来的数据时,它就像是接到了接力棒的运动员,迅速行动起来。通过一系列复杂的硬件电路传输,包括 DMA(直接内存访问)传输和 IO 通路选择等技术,网卡最终将数据写入内存中的特定地址。这个过程就好比把货物准确无误地存放到仓库的指定位置 。

当网卡完成数据写入后,它会向 CPU 发出一个中断信号。这个中断信号就像是一声响亮的哨声,告诉 CPU:“嘿,有新数据来了,快来处理!”CPU 在接收到这个信号后,会立即暂停当前正在执行的程序,转而执行相应的中断处理程序。在网络数据接收的场景中,这个中断程序主要有两个重要任务:一是将网络数据写入到对应 socket 的接收缓冲区中,就像把货物从仓库的临时存放点搬运到专门的存储区域;二是唤醒此 socket 阻塞队列上的进程,将其重新放入到工作队列中,让这些进程能够继续处理数据。

进程阻塞与资源占用

在网络编程中,进程阻塞是一个常见的概念。当一个进程执行到像Recv这样的系统调用时,如果此时没有数据可读,进程就会进入阻塞状态。这就好比一个人在等待快递送达,在快递还没到之前,他只能暂时停下手中其他的事情,处于等待状态。

从操作系统进程调度的角度来看,操作系统为了支持多任务处理,会把进程分为 “运行” 和 “等待” 等不同状态。运行状态的进程就像是正在舞台上表演的演员,能够获得 CPU 的使用权,正在执行代码;而等待状态的进程则像是在后台候场的演员,当程序执行到Recv时,进程就会从运行状态变为等待状态,进入 socket 的等待队列中。在这个队列中,进程不会占用 CPU 资源,因为 CPU 此时会去执行其他处于运行状态的进程的程序。只有当 socket 接收到数据后,操作系统才会将该 socket 等待队列上的进程重新放回到工作队列中,让它重新变为运行状态,继续执行代码 。

传统 I/O 模型的困境

在早期的网络编程中,单进程单 socket 模型是一种简单直接的方式。服务端需要管理多个客户端连接,但recv()系统调用却只能监视单个 socket。这就好比一个人要同时照顾多个孩子,但每次却只能关注一个孩子,效率极低。为了管理多个客户端连接,就需要多开进程或线程,每个进程维护一个 socket 套接字。在没有网络数据时,进程阻塞在recv()系统调用上,当网络数据到达时,操作系统唤醒对应 socket 等待队列上的进程。然而,这种方式面临着巨大的系统开销,每个线程的栈空间通常为 8M,系统的内存资源有限,1K 个线程就需要消耗 8G 内存,显然不可能无限制地多开线程。而且,进程、线程间的频繁切换也会带来较大的开销。

为了解决单进程单 socket 模型的弊端,select模型应运而生。select的设计思想很直接,就是使用一个进程监视多个 socket。预先传入一个 socket 列表,如果列表中的 socket 都没有数据,则进程继续挂起;直到有一个或以上的 socket 接收到网络数据,再唤醒进程。假设进程 A 同时监听sock1、sock2、sock3,在调用select之后,操作系统会把进程 A 分别加入到这三个 socket 的等待队列中。当任何一个 socket 上收到数据时,中断程序将唤起进程,将其从所有的 socket 对象的等待队列中移除,并插入到就绪队列中。此时,进程 A 被唤醒后,它知道所检测的 socket 列表中至少有个 socket 已经接收到数据了。然而,程序并不知道 socket 列表中的哪些 socket 上收到数据,因此在用户空间内需要对 socket 列表再做一次遍历。

select虽然在一定程度上解决了多 socket 监视的问题,但它也存在明显的缺点。每次调用select都需要将进程加入到所有 socket 对象的等待队列中,每次唤醒进程又要将进程从所有 socket 对象的等待队列中移除,这里涉及到对 socket 列表的两次遍历,而且每次都要将整个 fds 列表传递给内核,有一定的开销。正因为遍历操作开销大,出于效率的考量,select规定了默认只能监视 1024 个 socket。

三、epoll实现原理

3.1epoll 操作

epoll 在 linux 内核中申请了一个简易的文件系统,把原先的一个 select 或者 poll 调用分为了三个部分:调用 epoll_create 建立一个 epoll 对象(在 epoll 文件系统中给这个句柄分配资源)、调用 epoll_ctl 向 epoll 对象中添加连接的套接字、调用 epoll_wait 收集发生事件的连接。

这样只需要在进程启动的时候建立一个 epoll 对象,并在需要的时候向它添加或者删除连接就可以了,因此,在实际收集的时候,epoll_wait 的效率会非常高,因为调用的时候只是传递了发生 IO 事件的连接。

3.2epoll 实现

我们以 linux 内核 2.6 为例,说明一下 epoll 是如何高效的处理事件的。当某一个进程调用 epoll_create 方法的时候,Linux 内核会创建一个 eventpoll 结构体,这个结构体中有两个重要的成员。

  • 第一个是 rb_root rbr,这是红黑树的根节点,存储着所有添加到 epoll 中的事件,也就是这个 epoll 监控的事件。
  • 第二个是 list_head rdllist 这是一个双向链表,保存着将要通过 epoll_wait 返回给用户的、满足条件的事件。

每一个 epoll 对象都有一个独立的 eventpoll 结构体,这个结构体会在内核空间中创造独立的内存,用于存储使用 epoll_ctl 方法向 epoll 对象中添加进来的事件。这些事件都会挂到 rbr 红黑树中,这样就能够高效的识别重复添加的节点。

所有添加到 epoll 中的事件都会与设备(如网卡等)驱动程序建立回调关系,也就是说,相应的事件发生时会调用这里的方法。这个回调方法在内核中叫做 ep_poll_callback,它把这样的事件放到 rdllist 双向链表中。在 epoll 中,对于每一个事件都会建立一个 epitem 结构体。

当调用 epoll_wait 检查是否有发生事件的连接时,只需要检查 eventpoll 对象中的 rdllist 双向链表中是否有 epitem 元素,如果 rdllist 链表不为空,则把这里的事件复制到用户态内存中的同时,将事件数量返回给用户。通过这种方法,epoll_wait 的效率非常高。epoll-ctl 在向 epoll 对象中添加、修改、删除事件时,从 rbr 红黑树中查找事件也非常快。这样,epoll 就能够轻易的处理百万级的并发连接。

⑴pollable

首先,linux 的 file 有个 pollable 的概念,只有 pollable 的 file 才可以加入到 epoll 和 select 中。一个 file 是 pollable 的当且仅当其定义了 file->f_op->poll。file->f_op->poll 的形式如下:

__poll_t poll(struct file *fp, poll_table *wait)

不同类型的 file 实现不同,但做的事情都差不多:

  • 通过 fp 拿到其对应的 waitqueue
  • 通过 wait 拿到外部设置的 callback[[1]]
  • 执行 callback(fp, waitqueue, wait),在 callback 中会将另外一个 callback2[[2]] 注册到 waitqueue[[3]]中,此后 fp 有触发事件就会调用 callback2

waitqueue 是事件驱动的,与驱动程序密切相关,简单来说 poll 函数在 file 的触发队列中注册了个 callback, 有事件发生时就调用callback。感兴趣可以根据文后 [[4]] 的提示看看 socket 的 poll 实现

了解了 pollable 我们看看 epoll 的三个系统调用 epoll_create,,epoll_ctl,,epoll_wait:

(2)epoll_create 函数实现

epoll_create函数是 epoll 机制的入口,它的主要作用是创建一个新的 epoll 实例,并返回一个对应的文件描述符。在 Linux 内核中,epoll_create系统调用最终会调用到sys_epoll_create1函数,下面是简化后的代码实现逻辑分析:

SYSCALL_DEFINE1(epoll_create1, int, flags) {
    int error, fd;
    struct eventpoll *ep = NULL;
    struct file *file;

    // 检查flags参数的有效性
    if (flags & ~EPOLL_CLOEXEC)
        return -EINVAL;

    // 分配一个eventpoll结构体
    error = ep_alloc(&ep);
    if (error < 0)
        return error;

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

    // 创建一个文件对象,并将eventpoll结构体关联到文件的private_data字段
    file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));
    if (IS_ERR(file)) {
        error = PTR_ERR(file);
        goto out_free_fd;
    }

    // 将文件对象关联到eventpoll结构体
    ep->file = file;
    // 将文件描述符安装到进程的文件描述符表中
    fd_install(fd, file);

    return fd;

out_free_fd:
    put_unused_fd(fd);
out_free_ep:
    ep_free(ep);
    return error;
}
  • 参数检查:首先检查传入的flags参数,确保其值是有效的。EPOLL_CLOEXEC标志用于设置文件描述符在执行exec系列函数时是否关闭,如果flags中包含其他非法标志,则返回-EINVAL错误。
  • 分配 eventpoll 结构体:调用ep_alloc函数分配一个struct eventpoll结构体,用于表示新创建的 epoll 实例。这个结构体是 epoll 机制的核心数据结构,包含了红黑树、就绪链表、等待队列等重要成员。
  • 获取文件描述符:使用get_unused_fd_flags函数获取一个未被使用的文件描述符fd,并设置其标志位。O_RDWR表示该文件描述符可读可写,O_CLOEXEC标志根据传入的flags参数进行设置。
  • 创建文件对象并关联 eventpoll:通过anon_inode_getfile函数创建一个匿名 inode 文件对象,并将eventpoll_fops(这是一个struct file_operations结构体,定义了对 epoll 文件的操作方法)和ep(即前面分配的eventpoll结构体)关联到这个文件对象上。这样,通过返回的文件描述符fd就可以访问到对应的eventpoll实例。
  • 安装文件描述符:调用fd_install函数将获取到的文件描述符fd安装到当前进程的文件描述符表中,使其可以被进程使用。
  • 错误处理:如果在上述任何步骤中发生错误,会通过goto语句跳转到相应的错误处理标签,释放已经分配的资源,如文件描述符和eventpoll结构体。

通过以上步骤,epoll_create函数成功创建了一个 epoll 实例,并返回一个文件描述符,后续可以通过这个文件描述符对 epoll 实例进行操作,如添加、删除或修改被监听的文件描述符,以及等待事件的发生。

(3)epoll_ctl 函数实现

epoll_ctl函数用于对 epoll 实例进行控制,它可以添加(EPOLL_CTL_ADD)、修改(EPOLL_CTL_MOD)或删除(EPOLL_CTL_DEL)要监听的文件描述符及其相关事件。下面我们详细分析其在内核中的实现逻辑。

首先,epoll_ctl系统调用的内核实现函数为sys_epoll_ctl,其简化后的代码结构如下:

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

    // 判断操作类型是否为删除操作,如果不是则从用户空间拷贝事件数据
    if (ep_op_has_event(op) && copy_from_user(&epds, event, sizeof(struct epoll_event)))
        return -EFAULT;

    // 获取epoll实例对应的文件描述符和eventpoll结构体
    f = fdget(epfd);
    if (!f.file)
        return -EBADF;
    ep = f.file->private_data;

    // 获取要操作的文件描述符对应的文件结构
    tf = fdget(fd);
    if (!tf.file) {
        error = -EBADF;
        goto error_tgt_fput;
    }

    // 根据操作类型进行不同的处理
    switch (op) {
    case EPOLL_CTL_ADD:
        // 检查是否已存在该文件描述符的epitem,若存在则返回错误
        epi = ep_find(ep, tf.file, fd);
        if (epi) {
            error = -EEXIST;
            goto error_tgt_fput;
        }
        // 添加文件描述符到epoll实例中
        error = ep_insert(ep, &epds, tf.file, fd, 0);
        break;
    case EPOLL_CTL_MOD:
        // 查找要修改的文件描述符对应的epitem,若不存在则返回错误
        epi = ep_find(ep, tf.file, fd);
        if (!epi) {
            error = -ENOENT;
            goto error_tgt_fput;
        }
        // 修改epitem的事件信息
        error = ep_modify(ep, epi, &epds);
        break;
    case EPOLL_CTL_DEL:
        // 查找要删除的文件描述符对应的epitem,若不存在则返回错误
        epi = ep_find(ep, tf.file, fd);
        if (!epi) {
            error = -ENOENT;
            goto error_tgt_fput;
        }
        // 从epoll实例中删除文件描述符
        error = ep_delete(ep, epi);
        break;
    default:
        error = -EINVAL;
        goto error_tgt_fput;
    }

error_tgt_fput:
    // 释放文件描述符相关资源
    fdput(tf);
error_fput:
    fdput(f);
    return error;
}
  • 参数处理与检查:首先判断操作类型op是否为删除操作(EPOLL_CTL_DEL),如果不是,则从用户空间将event数据拷贝到内核空间的epds中。接着通过fdget函数获取epfd对应的文件描述符和eventpoll结构体,以及fd对应的文件结构。如果获取失败,返回-EBADF错误。
  • 添加操作(EPOLL_CTL_ADD):调用ep_find函数在 epoll 实例的红黑树中查找是否已经存在该文件描述符对应的epitem。如果存在,返回-EEXIST错误;否则,调用ep_insert函数将文件描述符添加到 epoll 实例中。在ep_insert函数中,会为新的文件描述符创建一个epitem结构体,将其插入到红黑树中,并注册回调函数,以便在文件描述符状态改变时能够通知 epoll。
  • 修改操作(EPOLL_CTL_MOD):同样通过ep_find函数查找要修改的文件描述符对应的epitem。若不存在,返回-ENOENT错误;若存在,则调用ep_modify函数修改epitem的事件信息。
  • 删除操作(EPOLL_CTL_DEL):使用ep_find函数查找要删除的文件描述符对应的epitem。若不存在,返回-ENOENT错误;若存在,调用ep_delete函数从 epoll 实例的红黑树和其他相关数据结构中删除该epitem,并释放相关资源。
  • 错误处理与资源释放:在函数执行过程中,如果发生任何错误,会跳转到相应的错误处理标签,释放获取的文件描述符相关资源,最后返回错误码。

通过以上步骤,epoll_ctl函数能够根据不同的操作类型,对 epoll 实例中的文件描述符及其事件进行准确的管理和操作,确保 epoll 机制能够正确地监听和处理各种 I/O 事件。

(4)epoll_wait 函数实现

epoll_wait函数是 epoll 机制中用于等待事件发生的核心函数。它会阻塞调用线程,直到有被监听的文件描述符上发生了事件,或者等待超时。下面我们深入分析其内核实现逻辑。

epoll_wait系统调用在内核中的实现函数是sys_epoll_wait,简化后的代码结构如下:

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events, int, maxevents, int, timeout) {
    struct fd f;
    struct eventpoll *ep;
    int error, res;
    wait_queue_entry_t wait;
    long flags;

    // 获取epoll实例对应的文件描述符和eventpoll结构体
    f = fdget(epfd);
    if (!f.file)
        return -EBADF;
    ep = f.file->private_data;

    // 初始化等待队列项
    init_waitqueue_entry(&wait, current);
    wait.flags |= WQ_FLAG_EXCLUSIVE;

    // 将当前线程添加到epoll的等待队列中
    add_wait_queue_exclusive(&ep->wq, &wait);

    for (;;) {
        // 尝试获取就绪事件
        res = ep_poll(ep, events, maxevents, timeout);

        // 如果获取到事件或者超时,或者被信号中断,则退出循环
        if (res || signal_pending(current) || (timeout &&!ep_events_available(ep)))
            break;

        // 否则,将当前线程设置为可中断睡眠状态,等待事件发生
        __set_current_state(TASK_INTERRUPTIBLE);
        // 释放CPU资源,等待被唤醒
        schedule();
    }

    // 将当前线程从等待队列中移除
    remove_wait_queue(&ep->wq, &wait);
    // 设置当前线程为运行状态
    __set_current_state(TASK_RUNNING);

    // 释放文件描述符相关资源
    fdput(f);

    // 根据不同情况返回结果
    if (res == -EINTR && signal_pending(current))
        return -EINTR;
    return res;
}
  • 参数检查与资源获取:首先通过fdget函数获取epfd对应的文件描述符和eventpoll结构体。如果获取失败,返回-EBADF错误。
  • 等待队列设置:初始化一个等待队列项wait,并将其添加到epoll实例的等待队列ep->wq中。等待队列用于在没有事件发生时,将调用线程阻塞,直到有事件发生或者等待超时。
  • 事件获取循环:进入一个无限循环,在循环中调用ep_poll函数尝试获取就绪事件。ep_poll函数会检查eventpoll的就绪链表rdllist,如果链表不为空,说明有就绪事件,将这些事件复制到用户空间的events数组中,并返回就绪事件的数量;如果链表为空,且timeout不为 0,表示设置了等待超时,则会根据timeout的值进行等待,直到有事件发生或者超时;如果timeout为 - 1,表示无限等待,直到有事件发生。
  • 线程状态管理与调度:如果在等待过程中没有获取到事件,且没有被信号中断,将当前线程设置为可中断睡眠状态(TASK_INTERRUPTIBLE),然后调用schedule函数释放 CPU 资源,让其他线程有机会运行。当有事件发生或者等待超时,或者线程被信号中断时,会从schedule函数返回,继续执行后续代码。
  • 等待队列移除与结果返回:将当前线程从等待队列中移除,并将线程状态设置为运行状态(TASK_RUNNING)。最后,根据不同情况返回结果。如果在等待过程中被信号中断,且有信号待处理,返回-EINTR错误;否则,返回获取到的就绪事件数量。

在ep_poll函数中,还会涉及到对水平触发(LT)和边缘触发(ET)两种模式的处理。对于水平触发模式,只要文件描述符上有未处理的事件,就会一直触发;而对于边缘触发模式,只有当文件描述符的状态发生变化时才会触发。在处理就绪事件时,会根据不同的触发模式,决定是否将文件描述符重新添加到就绪链表中,以确保事件能够被正确处理。

通过以上步骤,epoll_wait函数实现了高效的事件等待机制,能够在高并发场景下,准确地通知应用程序哪些文件描述符上发生了事件,从而提高了程序的性能和响应速度。

epoll 的原理基本上就这些,还有很多细节如红黑树在哪里用,怎样实现 level-triggered 和 edge-triggered... 我还没看。

PS. 普通文件不是 pollable 的,详情请看 epoll_does_not_work_with_file

3.3epoll 工作模式

①水平触发(LT):持续通知的 “贴心管家”

水平触发(LT)可是 epoll 的默认工作模式,就像一位贴心管家,时刻关注着文件描述符的状态。当某个文件描述符处于就绪状态,比如有数据可读或者可写,内核就会通知应用程序。要是应用程序这次没处理完数据,或者没来得及处理,别担心,下次调用 epoll_wait 时,内核依旧会不厌其烦地再次通知,直到数据被处理完或者缓冲区里没数据可读、可写了为止。

举个例子,在处理 HTTP 报文时,数据可能是一段段陆续到达的。使用 LT 模式,只要缓冲区还有没读完的报文片段,每次 epoll_wait 都会把对应的文件描述符事件返回,让应用程序可以分次从容地解析报文,不用担心错过任何数据,大大降低了编程复杂度,对新手程序员那是相当友好,就像有个老师在旁边,不停提醒你还有作业没做完呢。

②边缘触发(ET):高效灵敏的 “情报员”

边缘触发(ET)模式则像一位高效灵敏的情报员,奉行 “只报新事” 原则。只有在文件描述符的状态发生改变时,比如从无数据变为有数据可读,或者从不可写变为可写,内核才会触发事件通知应用程序。一旦通知了,它就默认你知晓此事,后续除非状态再次改变,否则不会重复通知。这意味着应用程序得打起十二分精神,在收到通知后,必须立刻、马上处理数据,而且要尽可能把当前就绪的数据一次性处理完。

比如说读取大型文件,使用 ET 模式,一旦检测到文件描述符可读,就得赶紧用 while 循环一股脑把数据全读完,不然下次 epoll_wait 可不会再提醒你还有剩余数据。要是读数据时遇到 EAGAIN 或 EWOULDBLOCK 错误,那就说明这次数据真读完了。这种模式虽然编程难度稍高,需要精细处理数据,但减少了不必要的唤醒次数,系统开销小,在追求极致性能的场景下,那可是 “利器”,能让数据如闪电般高效流转。

当然,在 LT 模式下开发基于 epoll 的应用要简单一些,不太容易出错,而在 ET 模式下事件发生时,如果没有彻底地将缓冲区的数据处理完,则会导致缓冲区的用户请求得不到响应。注意,默认情况下 Nginx 采用 ET 模式使用 epoll 的。

四、epoll内核源码详解

网上很多博客说epoll使用了共享内存,这个是完全错误的 ,可以阅读源码,会发现完全没有使用共享内存的任何api,而是 使用了copy_from_user跟__put_user进行内核跟用户虚拟空间数据交互。

/*
 *  fs/eventpoll.c (Efficient event retrieval implementation)
 *  Copyright (C) 2001,...,2009	 Davide Libenzi
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  Davide Libenzi <davidel@xmailserver.org>
 *
 */
/*
 * 在深入了解epoll的实现之前, 先来了解内核的3个方面.
 * 1. 等待队列 waitqueue
 * 我们简单解释一下等待队列:
 * 队列头(wait_queue_head_t)往往是资源生产者,
 * 队列成员(wait_queue_t)往往是资源消费者,
 * 当头的资源ready后, 会逐个执行每个成员指定的回调函数,
 * 来通知它们资源已经ready了, 等待队列大致就这个意思.
 * 2. 内核的poll机制
 * 被Poll的fd, 必须在实现上支持内核的Poll技术,
 * 比如fd是某个字符设备,或者是个socket, 它必须实现
 * file_operations中的poll操作, 给自己分配有一个等待队列头.
 * 主动poll fd的某个进程必须分配一个等待队列成员, 添加到
 * fd的对待队列里面去, 并指定资源ready时的回调函数.
 * 用socket做例子, 它必须有实现一个poll操作, 这个Poll是
 * 发起轮询的代码必须主动调用的, 该函数中必须调用poll_wait(),
 * poll_wait会将发起者作为等待队列成员加入到socket的等待队列中去.
 * 这样socket发生状态变化时可以通过队列头逐个通知所有关心它的进程.
 * 这一点必须很清楚的理解, 否则会想不明白epoll是如何
 * 得知fd的状态发生变化的.
 * 3. epollfd本身也是个fd, 所以它本身也可以被epoll,
 * 可以猜测一下它是不是可以无限嵌套epoll下去... 
 *
 * epoll基本上就是使用了上面的1,2点来完成.
 * 可见epoll本身并没有给内核引入什么特别复杂或者高深的技术,
 * 只不过是已有功能的重新组合, 达到了超过select的效果.
 */
/* 
 * 相关的其它内核知识:
 * 1. fd我们知道是文件描述符, 在内核态, 与之对应的是struct file结构,
 * 可以看作是内核态的文件描述符.
 * 2. spinlock, 自旋锁, 必须要非常小心使用的锁,
 * 尤其是调用spin_lock_irqsave()的时候, 中断关闭, 不会发生进程调度,
 * 被保护的资源其它CPU也无法访问. 这个锁是很强力的, 所以只能锁一些
 * 非常轻量级的操作.
 * 3. 引用计数在内核中是非常重要的概念,
 * 内核代码里面经常有些release, free释放资源的函数几乎不加任何锁,
 * 这是因为这些函数往往是在对象的引用计数变成0时被调用,
 * 既然没有进程在使用在这些对象, 自然也不需要加锁.
 * struct file 是持有引用计数的.
 */
/* --- epoll相关的数据结构 --- */
/*
 * This structure is stored inside the "private_data" member of the file
 * structure and rapresent the main data sructure for the eventpoll
 * interface.
 */
/* 每创建一个epollfd, 内核就会分配一个eventpoll与之对应, 可以说是
 * 内核态的epollfd. */
struct eventpoll {
	/* Protect the this structure access */
	spinlock_t lock;
	/*
	 * 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.
	 */
	/* 添加, 修改或者删除监听fd的时候, 以及epoll_wait返回, 向用户空间
	 * 传递数据时都会持有这个互斥锁, 所以在用户空间可以放心的在多个线程
	 * 中同时执行epoll相关的操作, 内核级已经做了保护. */
	struct mutex mtx;
	/* Wait queue used by sys_epoll_wait() */
	/* 调用epoll_wait()时, 我们就是"睡"在了这个等待队列上... */
	wait_queue_head_t wq;
	/* Wait queue used by file->poll() */
	/* 这个用于epollfd本事被poll的时候... */
	wait_queue_head_t poll_wait;
	/* List of ready file descriptors */
	/* 所有已经ready的epitem都在这个链表里面 */
	struct list_head rdllist;
	/* RB tree root used to store monitored fd structs */
	/* 所有要监听的epitem都在这里 */
	struct rb_root rbr;
	/*
		这是一个单链表链接着所有的struct epitem当event转移到用户空间时
	 */
	 * This is a single linked list that chains all the "struct epitem" that
	 * happened while transfering ready events to userspace w/out
	 * holding ->lock.
	 */
	struct epitem *ovflist;
	/* The user that created the eventpoll descriptor */
	/* 这里保存了一些用户变量, 比如fd监听数量的最大值等等 */
	struct user_struct *user;
};
/*
 * Each file descriptor added to the eventpoll interface will
 * have an entry of this type linked to the "rbr" RB tree.
 */
/* epitem 表示一个被监听的fd */
struct epitem {
	/* RB tree node used to link this structure to the eventpoll RB tree */
	/* rb_node, 当使用epoll_ctl()将一批fds加入到某个epollfd时, 内核会分配
	 * 一批的epitem与fds们对应, 而且它们以rb_tree的形式组织起来, tree的root
	 * 保存在epollfd, 也就是struct eventpoll中. 
	 * 在这里使用rb_tree的原因我认为是提高查找,插入以及删除的速度.
	 * rb_tree对以上3个操作都具有O(lgN)的时间复杂度 */
	struct rb_node rbn;
	/* List header used to link this structure to the eventpoll ready list */
	/* 链表节点, 所有已经ready的epitem都会被链到eventpoll的rdllist中 */
	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 */
	/* epitem对应的fd和struct file */
	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 */
	/* 当前epitem属于哪个eventpoll */
	struct eventpoll *ep;
	/* List header used to link this item to the "struct file" items list */
	struct list_head fllink;
	/* The structure that describe the interested events and the source fd */
	/* 当前的epitem关系哪些events, 这个数据是调用epoll_ctl时从用户态传递过来 */
	struct epoll_event event;
};
struct epoll_filefd {
	struct file *file;
	int fd;
};
/* poll所用到的钩子Wait structure used by the poll hooks */
struct eppoll_entry {
	/* List header used to link this structure to the "struct epitem" */
	struct list_head llink;
	/* The "base" pointer is set to the container "struct epitem" */
	struct epitem *base;
	/*
	 * Wait queue item that will be linked to the target file wait
	 * queue head.
	 */
	wait_queue_t wait;
	/* The wait queue head that linked the "wait" wait queue item */
	wait_queue_head_t *whead;
};
/* Wrapper struct used by poll queueing */
struct ep_pqueue {
	poll_table pt;
	struct epitem *epi;
};
/* Used by the ep_send_events() function as callback private data */
struct ep_send_events_data {
	int maxevents;
	struct epoll_event __user *events;
};

/* --- 代码注释 --- */
/* 你没看错, 这就是epoll_create()的真身, 基本啥也不干直接调用epoll_create1了,
 * 另外你也可以发现, size这个参数其实是没有任何用处的... */
SYSCALL_DEFINE1(epoll_create, int, size)
{
        if (size <= 0)
                return -EINVAL;
        return sys_epoll_create1(0);
}
/* 这才是真正的epoll_create啊~~ */
SYSCALL_DEFINE1(epoll_create1, int, flags)
{
	int error;
	struct eventpoll *ep = NULL;//主描述符
	/* Check the EPOLL_* constant for consistency.  */
	/* 这句没啥用处... */
	BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);
	/* 对于epoll来讲, 目前唯一有效的FLAG就是CLOEXEC */
	if (flags & ~EPOLL_CLOEXEC)
		return -EINVAL;
	/*
	 * Create the internal data structure ("struct eventpoll").
	 */
	/* 分配一个struct eventpoll, 分配和初始化细节我们随后深聊~ */
	error = ep_alloc(&ep);
	if (error < 0)
		return error;
	/*
	 * Creates all the items needed to setup an eventpoll file. That is,
	 * a file structure and a free file descriptor.
	 */
	/* 这里是创建一个匿名fd, 说起来就话长了...长话短说:
	 * epollfd本身并不存在一个真正的文件与之对应, 所以内核需要创建一个
	 * "虚拟"的文件, 并为之分配真正的struct file结构, 而且有真正的fd.
	 * 这里2个参数比较关键:
	 * eventpoll_fops, fops就是file operations, 就是当你对这个文件(这里是虚拟的)进行操作(比如读)时,
	 * fops里面的函数指针指向真正的操作实现, 类似C++里面虚函数和子类的概念.
	 * epoll只实现了poll和release(就是close)操作, 其它文件系统操作都有VFS全权处理了.
	 * ep, ep就是struct epollevent, 它会作为一个私有数据保存在struct file的private指针里面.
	 * 其实说白了, 就是为了能通过fd找到struct file, 通过struct file能找到eventpoll结构.
	 * 如果懂一点Linux下字符设备驱动开发, 这里应该是很好理解的,
	 * 推荐阅读 <Linux device driver 3rd>
	 */
	error = anon_inode_getfd("[eventpoll]", &eventpoll_fops, ep,
				 O_RDWR | (flags & O_CLOEXEC));
	if (error < 0)
		ep_free(ep);
	return error;
}
/* 
* 创建好epollfd后, 接下来我们要往里面添加fd咯
* 来看epoll_ctl
* epfd 就是epollfd
* op ADD,MOD,DEL
* fd 需要监听的描述符
* event 我们关心的events
*/
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
		struct epoll_event __user *, event)
{
	int error;
	struct file *file, *tfile;
	struct eventpoll *ep;
	struct epitem *epi;
	struct epoll_event epds;
	error = -EFAULT;
	/* 
	 * 错误处理以及从用户空间将epoll_event结构copy到内核空间.
	 */
	if (ep_op_has_event(op) &&
	    copy_from_user(&epds, event, sizeof(struct epoll_event)))
		goto error_return;
	/* Get the "struct file *" for the eventpoll file */
	/* 取得struct file结构, epfd既然是真正的fd, 那么内核空间
	 * 就会有与之对于的一个struct file结构
	 * 这个结构在epoll_create1()中, 由函数anon_inode_getfd()分配 */
	error = -EBADF;
	file = fget(epfd);
	if (!file)
		goto error_return;
	/* Get the "struct file *" for the target file */
	/* 我们需要监听的fd, 它当然也有个struct file结构, 上下2个不要搞混了哦 */
	tfile = fget(fd);
	if (!tfile)
		goto error_fput;
	/* The target file descriptor must support poll */
	error = -EPERM;
	/* 如果监听的文件不支持poll, 那就没辙了.
	 * 你知道什么情况下, 文件会不支持poll吗?
	 */
	if (!tfile->f_op || !tfile->f_op->poll)
		goto error_tgt_fput;
	/*
	 * We have to check that the file structure underneath the file descriptor
	 * the user passed to us _is_ an eventpoll file. And also we do not permit
	 * adding an epoll file descriptor inside itself.
	 */
	error = -EINVAL;
	/* epoll不能自己监听自己... */
	if (file == tfile || !is_file_epoll(file))
		goto error_tgt_fput;
	/*
	 * At this point it is safe to assume that the "private_data" contains
	 * our own data structure.
	 */
	/* 取到我们的eventpoll结构, 来自与epoll_create1()中的分配 */
	ep = file->private_data;
	/* 接下来的操作有可能修改数据结构内容, 锁之~ */
	mutex_lock(&ep->mtx);
	/*
	 * Try to lookup the file inside our RB tree, Since we grabbed "mtx"
	 * above, we can be sure to be able to use the item looked up by
	 * ep_find() till we release the mutex.
	 */
	/* 对于每一个监听的fd, 内核都有分配一个epitem结构,
	 * 而且我们也知道, epoll是不允许重复添加fd的,
	 * 所以我们首先查找该fd是不是已经存在了.
	 * ep_find()其实就是RBTREE查找, 跟C++STL的map差不多一回事, O(lgn)的时间复杂度.
	 */
	epi = ep_find(ep, tfile, fd);
	error = -EINVAL;
	switch (op) {
		/* 首先我们关心添加 */
	case EPOLL_CTL_ADD:
		if (!epi) {
			/* 之前的find没有找到有效的epitem, 证明是第一次插入, 接受!
			 * 这里我们可以知道, POLLERR和POLLHUP事件内核总是会关心的
			 * */
			epds.events |= POLLERR | POLLHUP;
			/* rbtree插入, 详情见ep_insert()的分析
			 * 其实我觉得这里有insert的话, 之前的find应该
			 * 是可以省掉的... */
			error = ep_insert(ep, &epds, tfile, fd);
		} 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) {
			epds.events |= POLLERR | POLLHUP;
			error = ep_modify(ep, epi, &epds);
		} else
			error = -ENOENT;
		break;
	}
	mutex_unlock(&ep->mtx);
error_tgt_fput:
	fput(tfile);
error_fput:
	fput(file);
error_return:
	return error;
}
/* 分配一个eventpoll结构 */
static int ep_alloc(struct eventpoll **pep)
{
	int error;
	struct user_struct *user;
	struct eventpoll *ep;
	/* 获取当前用户的一些信息, 比如是不是root啦, 最大监听fd数目啦 */
	user = get_current_user();
	error = -ENOMEM;
	ep = kzalloc(sizeof(*ep), GFP_KERNEL);
	if (unlikely(!ep))
		goto free_uid;
	/* 这些都是初始化啦 */
	spin_lock_init(&ep->lock);
	mutex_init(&ep->mtx);
	init_waitqueue_head(&ep->wq);//初始化自己睡在的等待队列
	init_waitqueue_head(&ep->poll_wait);//初始化
	INIT_LIST_HEAD(&ep->rdllist);//初始化就绪链表
	ep->rbr = RB_ROOT;
	ep->ovflist = EP_UNACTIVE_PTR;
	ep->user = user;
	*pep = ep;
	return 0;
free_uid:
	free_uid(user);
	return error;
}
/*
 * Must be called with "mtx" held.
 */
/* 
 * ep_insert()在epoll_ctl()中被调用, 完成往epollfd里面添加一个监听fd的工作
 * tfile是fd在内核态的struct file结构
 */
static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
		     struct file *tfile, int fd)
{
	int error, revents, pwake = 0;
	unsigned long flags;
	struct epitem *epi;
	struct ep_pqueue epq;
	/* 查看是否达到当前用户的最大监听数 */
	if (unlikely(atomic_read(&ep->user->epoll_watches) >=
		     max_user_watches))
		return -ENOSPC;
	/* 从著名的slab中分配一个epitem */
	if (!(epi = kmem_***_alloc(epi_***, GFP_KERNEL)))
		return -ENOMEM;
	/* Item initialization follow here ... */
	/* 这些都是相关成员的初始化... */
	INIT_LIST_HEAD(&epi->rdllink);
	INIT_LIST_HEAD(&epi->fllink);
	INIT_LIST_HEAD(&epi->pwqlist);
	epi->ep = ep;
	/* 这里保存了我们需要监听的文件fd和它的file结构 */
	ep_set_ffd(&epi->ffd, tfile, fd);
	epi->event = *event;
	epi->nwait = 0;
	/* 这个指针的初值不是NULL哦... */
	epi->next = EP_UNACTIVE_PTR;
	/* Initialize the poll table using the queue callback */
	/* 好, 我们终于要进入到poll的正题了 */
	epq.epi = epi;
	/* 初始化一个poll_table
	 * 其实就是指定调用poll_wait(注意不是epoll_wait!!!)时的回调函数,和我们关心哪些events,
	 * ep_ptable_queue_proc()就是我们的回调啦, 初值是所有event都关心 */
	init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
	/*
	 * Attach the item to the poll hooks and get current event bits.
	 * We can safely use the file* here because its usage count has
	 * been increased by the caller of this function. Note that after
	 * this operation completes, the poll callback can start hitting
	 * the new item.
	 */
	/* 这一部很关键, 也比较难懂, 完全是内核的poll机制导致的...
	 * 首先, f_op->poll()一般来说只是个wrapper, 它会调用真正的poll实现,
	 * 拿UDP的socket来举例, 这里就是这样的调用流程: f_op->poll(), sock_poll(),
	 * udp_poll(), datagram_poll(), sock_poll_wait(), 最后调用到我们上面指定的
	 * ep_ptable_queue_proc()这个回调函数...(好深的调用路径...).
	 * 完成这一步, 我们的epitem就跟这个socket关联起来了, 当它有状态变化时,
	 * 会通过ep_poll_callback()来通知.
	 * 最后, 这个函数还会查询当前的fd是不是已经有啥event已经ready了, 有的话
	 * 会将event返回. */
	revents = tfile->f_op->poll(tfile, &epq.pt);
	/*
	 * We have to check if something went wrong during the poll wait queue
	 * install process. Namely an allocation for a wait queue failed due
	 * high memory pressure.
	 */
	error = -ENOMEM;
	if (epi->nwait < 0)
		goto error_unregister;
	/* Add the current item to the list of active epoll hook for this file */
	/* 这个就是每个文件会将所有监听自己的epitem链起来 */
	spin_lock(&tfile->f_lock);
	list_add_tail(&epi->fllink, &tfile->f_ep_links);
	spin_unlock(&tfile->f_lock);
	/*
	 * Add the current item to the RB tree. All RB tree operations are
	 * protected by "mtx", and ep_insert() is called with "mtx" held.
	 */
	/* 都搞定后, 将epitem插入到对应的eventpoll中去 */
	ep_rbtree_insert(ep, epi);
	/* We have to drop the new item inside our item list to keep track of it */
	spin_lock_irqsave(&ep->lock, flags);
	/* If the file is already "ready" we drop it inside the ready list */
	/* 到达这里后, 如果我们监听的fd已经有事件发生, 那就要处理一下 */
	if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
		/* 将当前的epitem加入到ready list中去 */
		list_add_tail(&epi->rdllink, &ep->rdllist);
		/* Notify waiting tasks that events are available */
		/* 谁在epoll_wait, 就唤醒它... */
		if (waitqueue_active(&ep->wq))
			wake_up_locked(&ep->wq);
		/* 谁在epoll当前的epollfd, 也唤醒它... */
		if (waitqueue_active(&ep->poll_wait))
			pwake++;
	}
	spin_unlock_irqrestore(&ep->lock, flags);
	atomic_inc(&ep->user->epoll_watches);
	/* We have to call this outside the lock */
	if (pwake)
		ep_poll_safewake(&ep->poll_wait);
	return 0;
error_unregister:
	ep_unregister_pollwait(ep, epi);
	/*
	 * We need to do this because an event could have been arrived on some
	 * allocated wait queue. Note that we don't care about the ep->ovflist
	 * list, since that is used/cleaned only inside a section bound by "mtx".
	 * And ep_insert() is called with "mtx" held.
	 */
	spin_lock_irqsave(&ep->lock, flags);
	if (ep_is_linked(&epi->rdllink))
		list_del_init(&epi->rdllink);
	spin_unlock_irqrestore(&ep->lock, flags);
	kmem_***_free(epi_***, epi);
	return error;
}
/*
 * This is the callback that is used to add our wait queue to the
 * target file wakeup lists.
 */
/* 
 * 该函数在调用f_op->poll()时会被调用.
 * 也就是epoll主动poll某个fd时, 用来将epitem与指定的fd关联起来的.
 * 关联的办法就是使用等待队列(waitqueue)
 */
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;
	if (epi->nwait >= 0 && (pwq = kmem_***_alloc(pwq_***, GFP_KERNEL))) {
		/* 初始化等待队列, 指定ep_poll_callback为唤醒时的回调函数,
		 * 当我们监听的fd发生状态改变时, 也就是队列头被唤醒时,
		 * 指定的回调函数将会被调用. */
		init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
		pwq->whead = whead;
		pwq->base = epi;
		/* 将刚分配的等待队列成员加入到头中, 头是由fd持有的 */
		add_wait_queue(whead, &pwq->wait);
		list_add_tail(&pwq->llink, &epi->pwqlist);
		/* nwait记录了当前epitem加入到了多少个等待队列中,
		 * 我认为这个值最大也只会是1... */
		epi->nwait++;
	} else {
		/* We have to signal that an error occurred */
		epi->nwait = -1;
	}
}
/*
 * This is the callback that is passed to the wait queue wakeup
 * machanism. It is called by the stored file descriptors when they
 * have events to report.
 */
/* 
 * 这个是关键性的回调函数, 当我们监听的fd发生状态改变时, 它会被调用.
 * 参数key被当作一个unsigned long整数使用, 携带的是events.
 */
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
	int pwake = 0;
	unsigned long flags;
	struct epitem *epi = ep_item_from_wait(wait);//从等待队列获取epitem.需要知道哪个进程挂载到这个设备
	struct eventpoll *ep = epi->ep;//获取
	spin_lock_irqsave(&ep->lock, flags);
	/*
	 * If the event mask does not contain any poll(2) event, we consider the
	 * descriptor to be disabled. This condition is likely the effect of the
	 * EPOLLONESHOT bit that disables the descriptor when an event is received,
	 * until the next EPOLL_CTL_MOD will be issued.
	 */
	if (!(epi->event.events & ~EP_PRIVATE_BITS))
		goto out_unlock;
	/*
	 * Check the events coming with the callback. At this stage, not
	 * every device reports the events in the "key" parameter of the
	 * callback. We need to be able to handle both cases here, hence the
	 * test for "key" != NULL before the event match test.
	 */
	/* 没有我们关心的event... */
	if (key && !((unsigned long) key & epi->event.events))
		goto out_unlock;
	/*
	 * If we are trasfering events to userspace, we can hold no locks
	 * (because we're accessing user memory, and because of linux f_op->poll()
	 * semantics). All the events that happens during that period of time are
	 * chained in ep->ovflist and requeued later on.
	 */
	/* 
	 * 这里看起来可能有点费解, 其实干的事情比较简单:
	 * 如果该callback被调用的同时, epoll_wait()已经返回了,
	 * 也就是说, 此刻应用程序有可能已经在循环获取events,
	 * 这种情况下, 内核将此刻发生event的epitem用一个单独的链表
	 * 链起来, 不发给应用程序, 也不丢弃, 而是在下一次epoll_wait
	 * 时返回给用户.
	 */
	if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
		if (epi->next == EP_UNACTIVE_PTR) {
			epi->next = ep->ovflist;
			ep->ovflist = epi;
		}
		goto out_unlock;
	}
	/* If this file is already in the ready list we exit soon */
	/* 将当前的epitem放入ready list */
	if (!ep_is_linked(&epi->rdllink))
		list_add_tail(&epi->rdllink, &ep->rdllist);
	/*
	 * Wake up ( if active ) both the eventpoll wait list and the ->poll()
	 * wait list.
	 */
	/* 唤醒epoll_wait... */
	if (waitqueue_active(&ep->wq))
		wake_up_locked(&ep->wq);
	/* 如果epollfd也在被poll, 那就唤醒队列里面的所有成员. */
	if (waitqueue_active(&ep->poll_wait))
		pwake++;
out_unlock:
	spin_unlock_irqrestore(&ep->lock, flags);
	/* We have to call this outside the lock */
	if (pwake)
		ep_poll_safewake(&ep->poll_wait);
	return 1;
}
/*
 * Implement the event wait interface for the eventpoll file. It is the kernel
 * part of the user space epoll_wait(2).
 */
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
		int, maxevents, int, timeout)
{
	int error;
	struct file *file;
	struct eventpoll *ep;
	/* The maximum number of event must be greater than zero */
	if (maxevents <= 0 || maxevents > EP_MAX_EVENTS)
		return -EINVAL;
	/* Verify that the area passed by the user is writeable */
	/* 这个地方有必要说明一下:
	 * 内核对应用程序采取的策略是"绝对不信任",
	 * 所以内核跟应用程序之间的数据交互大都是copy, 不允许(也时候也是不能...)指针引用.
	 * epoll_wait()需要内核返回数据给用户空间, 内存由用户程序提供,
	 * 所以内核会用一些手段来验证这一段内存空间是不是有效的.
	 */
	if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event))) {
		error = -EFAULT;
		goto error_return;
	}
	/* Get the "struct file *" for the eventpoll file */
	error = -EBADF;
	/* 获取epollfd的struct file, epollfd也是文件嘛 */
	file = fget(epfd);
	if (!file)
		goto error_return;
	/*
	 * We have to check that the file structure underneath the fd
	 * the user passed to us _is_ an eventpoll file.
	 */
	error = -EINVAL;
	/* 检查一下它是不是一个真正的epollfd... */
	if (!is_file_epoll(file))
		goto error_fput;
	/*
	 * At this point it is safe to assume that the "private_data" contains
	 * our own data structure.
	 */
	/* 获取eventpoll结构 */
	ep = file->private_data;
	/* Time to fish for events ... */
	/* OK, 睡觉, 等待事件到来~~ */
	error = ep_poll(ep, events, maxevents, timeout);
error_fput:
	fput(file);
error_return:
	return error;
}
/* 这个函数真正将执行epoll_wait的进程带入睡眠状态... */
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
		   int maxevents, long timeout)
{
	int res, eavail;
	unsigned long flags;
	long jtimeout;
	wait_queue_t wait;//等待队列
	/*
	 * Calculate the timeout by checking for the "infinite" value (-1)
	 * and the overflow condition. The passed timeout is in milliseconds,
	 * that why (t * HZ) / 1000.
	 */
	/* 计算睡觉时间, 毫秒要转换为HZ */
	jtimeout = (timeout < 0 || timeout >= EP_MAX_MSTIMEO) ?
		MAX_SCHEDULE_TIMEOUT : (timeout * HZ + 999) / 1000;
retry:
	spin_lock_irqsave(&ep->lock, flags);
	res = 0;
	/* 如果ready list不为空, 就不睡了, 直接干活... */
	if (list_empty(&ep->rdllist)) {
		/*
		 * We don't have any available event to return to the caller.
		 * We need to sleep here, and we will be wake up by
		 * ep_poll_callback() when events will become available.
		 */
		/* OK, 初始化一个等待队列, 准备直接把自己挂起,
		 * 注意current是一个宏, 代表当前进程 */
		init_waitqueue_entry(&wait, current);//初始化等待队列,wait表示当前进程
		__add_wait_queue_exclusive(&ep->wq, &wait);//挂载到ep结构的等待队列
		for (;;) {
			/*
			 * We don't want to sleep if the ep_poll_callback() sends us
			 * a wakeup in between. That's why we set the task state
			 * to TASK_INTERRUPTIBLE before doing the checks.
			 */
			/* 将当前进程设置位睡眠, 但是可以被信号唤醒的状态,
			 * 注意这个设置是"将来时", 我们此刻还没睡! */
			set_current_state(TASK_INTERRUPTIBLE);
			/* 如果这个时候, ready list里面有成员了,
			 * 或者睡眠时间已经过了, 就直接不睡了... */
			if (!list_empty(&ep->rdllist) || !jtimeout)
				break;
			/* 如果有信号产生, 也起床... */
			if (signal_pending(current)) {
				res = -EINTR;
				break;
			}
			/* 啥事都没有,解锁, 睡觉... */
			spin_unlock_irqrestore(&ep->lock, flags);
			/* jtimeout这个时间后, 会被唤醒,
			 * ep_poll_callback()如果此时被调用,
			 * 那么我们就会直接被唤醒, 不用等时间了... 
			 * 再次强调一下ep_poll_callback()的调用时机是由被监听的fd
			 * 的具体实现, 比如socket或者某个设备驱动来决定的,
			 * 因为等待队列头是他们持有的, epoll和当前进程
			 * 只是单纯的等待...
			 **/
			jtimeout = schedule_timeout(jtimeout);//睡觉
			spin_lock_irqsave(&ep->lock, flags);
		}
		__remove_wait_queue(&ep->wq, &wait);
		/* OK 我们醒来了... */
		set_current_state(TASK_RUNNING);
	}
	/* Is it worth to try to dig for events ? */
	eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;
	spin_unlock_irqrestore(&ep->lock, flags);
	/*
	 * Try to transfer events to user space. In case we get 0 events and
	 * there's still timeout left over, we go trying again in search of
	 * more luck.
	 */
	/* 如果一切正常, 有event发生, 就开始准备数据copy给用户空间了... */
	if (!res && eavail &&
	    !(res = ep_send_events(ep, events, maxevents)) && jtimeout)
		goto retry;
	return res;
}
/* 这个简单, 我们直奔下一个... */
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;
	return ep_scan_ready_list(ep, ep_send_events_proc, &esed);
}
/**
 * ep_scan_ready_list - Scans the ready list in a way that makes possible for
 *                      the scan code, to call f_op->poll(). Also allows for
 *                      O(NumReady) performance.
 *
 * @ep: Pointer to the epoll private data structure.
 * @sproc: Pointer to the scan callback.
 * @priv: Private opaque data passed to the @sproc callback.
 *
 * Returns: The same integer error code returned by the @sproc callback.
 */
static int ep_scan_ready_list(struct eventpoll *ep,
			      int (*sproc)(struct eventpoll *,
					   struct list_head *, void *),
			      void *priv)
{
	int error, pwake = 0;
	unsigned long flags;
	struct epitem *epi, *nepi;
	LIST_HEAD(txlist);
	/*
	 * We need to lock this because we could be hit by
	 * eventpoll_release_file() and epoll_ctl().
	 */
	mutex_lock(&ep->mtx);
	/*
	 * Steal the ready list, and re-init the original one to the
	 * empty list. Also, set ep->ovflist to NULL so that events
	 * happening while looping w/out locks, are not lost. We cannot
	 * have the poll callback to queue directly on ep->rdllist,
	 * because we want the "sproc" callback to be able to do it
	 * in a lockless way.
	 */
	spin_lock_irqsave(&ep->lock, flags);
	/* 这一步要注意, 首先, 所有监听到events的epitem都链到rdllist上了,
	 * 但是这一步之后, 所有的epitem都转移到了txlist上, 而rdllist被清空了,
	 * 要注意哦, rdllist已经被清空了! */
	list_splice_init(&ep->rdllist, &txlist);
	/* ovflist, 在ep_poll_callback()里面我解释过, 此时此刻我们不希望
	 * 有新的event加入到ready list中了, 保存后下次再处理... */
	ep->ovflist = NULL;
	spin_unlock_irqrestore(&ep->lock, flags);
	/*
	 * Now call the callback function.
	 */
	/* 在这个回调函数里面处理每个epitem
	 * sproc 就是 ep_send_events_proc, 下面会注释到. */
	error = (*sproc)(ep, &txlist, priv);
	spin_lock_irqsave(&ep->lock, flags);
	/*
	 * During the time we spent inside the "sproc" callback, some
	 * other events might have been queued by the poll callback.
	 * We re-insert them inside the main ready-list here.
	 */
	/* 现在我们来处理ovflist, 这些epitem都是我们在传递数据给用户空间时
	 * 监听到了事件. */
	for (nepi = ep->ovflist; (epi = nepi) != NULL;
	     nepi = epi->next, epi->next = EP_UNACTIVE_PTR) {
		/*
		 * We need to check if the item is already in the list.
		 * During the "sproc" callback execution time, items are
		 * queued into ->ovflist but the "txlist" might already
		 * contain them, and the list_splice() below takes care of them.
		 */
		/* 将这些直接放入readylist */
		if (!ep_is_linked(&epi->rdllink))
			list_add_tail(&epi->rdllink, &ep->rdllist);
	}
	/*
	 * We need to set back ep->ovflist to EP_UNACTIVE_PTR, so that after
	 * releasing the lock, events will be queued in the normal way inside
	 * ep->rdllist.
	 */
	ep->ovflist = EP_UNACTIVE_PTR;
	/*
	 * Quickly re-inject items left on "txlist".
	 */
	/* 上一次没有处理完的epitem, 重新插入到ready list */
	list_splice(&txlist, &ep->rdllist);
	/* ready list不为空, 直接唤醒... */
	if (!list_empty(&ep->rdllist)) {
		/*
		 * Wake up (if active) both the eventpoll wait list and
		 * the ->poll() wait list (delayed after we release the lock).
		 */
		if (waitqueue_active(&ep->wq))
			wake_up_locked(&ep->wq);
		if (waitqueue_active(&ep->poll_wait))
			pwake++;
	}
	spin_unlock_irqrestore(&ep->lock, flags);
	mutex_unlock(&ep->mtx);
	/* We have to call this outside the lock */
	if (pwake)
		ep_poll_safewake(&ep->poll_wait);
	return error;
}
/* 该函数作为callbakc在ep_scan_ready_list()中被调用
 * head是一个链表, 包含了已经ready的epitem,
 * 这个不是eventpoll里面的ready list, 而是上面函数中的txlist.
 */
static int ep_send_events_proc(struct eventpoll *ep, struct list_head *head,
			       void *priv)
{
	struct ep_send_events_data *esed = priv;
	int eventcnt;
	unsigned int revents;
	struct epitem *epi;
	struct epoll_event __user *uevent;
	/*
	 * We can loop without lock because we are passed a task private list.
	 * Items cannot vanish during the loop because ep_scan_ready_list() is
	 * holding "mtx" during this call.
	 */
	/* 扫描整个链表... */
	for (eventcnt = 0, uevent = esed->events;
	     !list_empty(head) && eventcnt < esed->maxevents;) {
		/* 取出第一个成员 */
		epi = list_first_entry(head, struct epitem, rdllink);
		/* 然后从链表里面移除 */
		list_del_init(&epi->rdllink);
		/* 读取events, 
		 * 注意events我们ep_poll_callback()里面已经取过一次了, 为啥还要再取?
		 * 1. 我们当然希望能拿到此刻的最新数据, events是会变的~
		 * 2. 不是所有的poll实现, 都通过等待队列传递了events, 有可能某些驱动压根没传
		 * 必须主动去读取. */
		revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) &
			epi->event.events;
		if (revents) {
			/* 将当前的事件和用户传入的数据都copy给用户空间,
			 * 就是epoll_wait()后应用程序能读到的那一堆数据. */
			if (__put_user(revents, &uevent->events) ||
			    __put_user(epi->event.data, &uevent->data)) {
				list_add(&epi->rdllink, head);
				return eventcnt ? eventcnt : -EFAULT;
			}
			eventcnt++;
			uevent++;
			if (epi->event.events & EPOLLONESHOT)
				epi->event.events &= EP_PRIVATE_BITS;
			else if (!(epi->event.events & EPOLLET)) {
				/* 嘿嘿, EPOLLET和非ET的区别就在这一步之差呀~
				 * 如果是ET, epitem是不会再进入到readly list,
				 * 除非fd再次发生了状态改变, ep_poll_callback被调用.
				 * 如果是非ET, 不管你还有没有有效的事件或者数据,
				 * 都会被重新插入到ready list, 再下一次epoll_wait
				 * 时, 会立即返回, 并通知给用户空间. 当然如果这个
				 * 被监听的fds确实没事件也没数据了, epoll_wait会返回一个0,
				 * 空转一次.
				 */
				list_add_tail(&epi->rdllink, &ep->rdllist);
			}
		}
	}
	return eventcnt;
}
/* ep_free在epollfd被close时调用,
 * 释放一些资源而已, 比较简单 */
static void ep_free(struct eventpoll *ep)
{
	struct rb_node *rbp;
	struct epitem *epi;
	/* We need to release all tasks waiting for these file */
	if (waitqueue_active(&ep->poll_wait))
		ep_poll_safewake(&ep->poll_wait);
	/*
	 * We need to lock this because we could be hit by
	 * eventpoll_release_file() while we're freeing the "struct eventpoll".
	 * We do not need to hold "ep->mtx" here because the epoll file
	 * is on the way to be removed and no one has references to it
	 * anymore. The only hit might come from eventpoll_release_file() but
	 * holding "epmutex" is sufficent here.
	 */
	mutex_lock(&epmutex);
	/*
	 * Walks through the whole tree by unregistering poll callbacks.
	 */
	for (rbp = rb_first(&ep->rbr); rbp; rbp = rb_next(rbp)) {
		epi = rb_entry(rbp, struct epitem, rbn);
		ep_unregister_pollwait(ep, epi);
	}
	/*
	 * Walks through the whole tree by freeing each "struct epitem". At this
	 * point we are sure no poll callbacks will be lingering around, and also by
	 * holding "epmutex" we can be sure that no file cleanup code will hit
	 * us during this operation. So we can avoid the lock on "ep->lock".
	 */
	/* 之所以在关闭epollfd之前不需要调用epoll_ctl移除已经添加的fd,
	 * 是因为这里已经做了... */
	while ((rbp = rb_first(&ep->rbr)) != NULL) {
		epi = rb_entry(rbp, struct epitem, rbn);
		ep_remove(ep, epi);
	}
	mutex_unlock(&epmutex);
	mutex_destroy(&ep->mtx);
	free_uid(ep->user);
	kfree(ep);
}
/* File callbacks that implement the eventpoll file behaviour */
static const struct file_operations eventpoll_fops = {
	.release	= ep_eventpoll_release,
	.poll		= ep_eventpoll_poll
};
/* Fast test to see if the file is an evenpoll file */
static inline int is_file_epoll(struct file *f)
{
	return f->f_op == &eventpoll_fops;
}
/* OK, eventpoll我认为比较重要的函数都注释完了... */

4.1epoll_create

从slab缓存中创建一个eventpoll对象,并且创建一个匿名的fd跟fd对应的file对象, 而eventpoll对象保存在struct file结构的private指针中,并且返回, 该fd对应的file operations只是实现了poll跟release操作。

创建eventpoll对象的初始化操作,获取当前用户信息,是不是root,最大监听fd数目等并且保存到eventpoll对象中 初始化等待队列,初始化就绪链表,初始化红黑树的头结点。

4.2epoll_ctl操作

将epoll_event结构拷贝到内核空间中,并且判断加入的fd是否支持poll结构(epoll,poll,selectI/O多路复用必须支持poll操作),并且从epfd->file->privatedata获取event_poll对象,根据op区分是添加删除还是修改, 首先在eventpoll结构中的红黑树查找是否已经存在了相对应的fd,没找到就支持插入操作,否则报重复的错误,相对应的修改,删除比较简单就不啰嗦了。

插入操作时,会创建一个与fd对应的epitem结构,并且初始化相关成员,比如保存监听的fd跟file结构之类的,重要的是指定了调用poll_wait时的回调函数用于数据就绪时唤醒进程,(其内部,初始化设备的等待队列,将该进程注册到等待队列)完成这一步, 我们的epitem就跟这个socket关联起来了, 当它有状态变化时, 会通过ep_poll_callback()来通知,最后调用加入的fd的file operation->poll函数(最后会调用poll_wait操作)用于完成注册操作,最后将epitem结构添加到红黑树中。

4.3epoll_wait操作

计算睡眠时间(如果有),判断eventpoll对象的链表是否为空,不为空那就干活不睡明.并且初始化一个等待队列,把自己挂上去,设置自己的进程状态,为可睡眠状态.判断是否有信号到来(有的话直接被中断醒来,),如果啥事都没有那就调用schedule_timeout进行睡眠,如果超时或者被唤醒,首先从自己初始化的等待队列删除,然后开始拷贝资源给用户空间了,拷贝资源则是先把就绪事件链表转移到中间链表,然后挨个遍历拷贝到用户空间, 并且挨个判断其是否为水平触发,是的话再次插入到就绪链表。

五、epoll常见问题解答

5.1使用 epoll 时的注意事项

在实际使用 epoll 时,有几个关键的注意事项需要牢记。首先是参数设置方面,epoll_wait的timeout参数需要谨慎设置。如果设置为 0,epoll_wait会立即返回,不会阻塞,这适用于需要快速轮询事件的场景,但可能会增加 CPU 的使用率。如果设置为 - 1,则表示无限期阻塞,直到有事件发生,这在需要长时间等待事件的场景中比较适用,但如果处理不当,可能会导致程序在无事件时一直阻塞。

文件描述符管理也至关重要。当添加文件描述符到 epoll 实例时,要确保文件描述符的有效性和合法性。例如,在添加 socket 时,要确保 socket 已经正确创建和绑定。同时,要注意及时删除不再需要监听的文件描述符,避免资源浪费和潜在的错误。在使用完 epoll 实例后,要及时关闭对应的文件描述符,防止文件描述符泄漏。比如在一个高并发的网络服务器中,如果不断地创建 epoll 实例而不关闭,可能会导致文件描述符耗尽,影响服务器的正常运行。

在设置 socket 为非阻塞模式时,要注意处理EAGAIN错误。在边缘触发(ET)模式下,当 socket 可读或可写时,需要一次性读取或写入尽可能多的数据,直到返回EAGAIN错误。否则,可能会导致数据处理不及时或丢失事件通知。

5.2常见错误及解决方法

一个 epoll fd 可以当作一个普通的 fd 被另一个 epoll 监听吗?

可以

这种使用方式,自己没见过具体的使用场景,但是 epoll 是支持这种用法的。

epoll 最常用的方式是监听普通的 socket fd,比如建立了 100 条 tcp 连接,然后把每个连接加入到 epoll 中,通过 epoll 进行监听,如下图所示:

图片图片

epoll 也是用一个 fd 来表示,epoll fd 也可以当做一个普通的 fd 加入到另外一个 epoll 里边进行监听。如下图所示,有 200 个 tcp 连接,其中第 1 到 100 个被 epoll1 监听,第 101 到 200 个被 epoll2 监听,然后 epoll1 和 epoll 2 再被 epoll 3 监听,这样用户只监听 epoll3 来同时监听 200 个连接。

当一个对象实现了 struct file_operations 中的 poll() 方法时,这个对象就可以加入到 epoll 中监听,否则无法使用 epoll 监听。如下是 epoll 实现的 struct_operations,其中实现了 poll 方法,所以 epoll 也可以当做一个普通 fd 被另一个 epoll 监听。

/* File callbacks that implement the eventpoll file behaviour */
static const struct file_operations eventpoll_fops = {
#ifdef CONFIG_PROC_FS
	.show_fdinfo	= ep_show_fdinfo,
#endif
	.release	= ep_eventpoll_release,
	.poll		= ep_eventpoll_poll,
	.llseek		= noop_llseek,
};

图片图片

这种使用方式下,代码也需要分两级来进行:

event_num = epoll_wait(epoll3, events, MAX_EVENT, -1);
for (int i = 0; i < event_num; i++ ) {
    if (events[i].data.fd == epoll1) {
        epoll_wait(epoll1, events, MACX_EVENT, -1);
        ....
    } else if (events[i].data.fd == epoll2) {
        epoll_wait(epoll1, events, MACX_EVENT, -1);
        ....
    }
}

通过代码来理解:

(1) epoll1, epoll2 加入到 epoll3 和普通的 fd1 加入到 epoll1,代码上有什么区别

无论是普通的 fd 还是 epoll fd, 需要被 epoll 监听的时候,均是向这个 fd 的 poll_wait 队列入队一个元素,即注册回调函数 ep_poll_callback(),当描述符中有事件的时候,则会调用这个回调函数进行事件通知。

回调注册在 ep_item_poll() 函数中完成,普通 fd 通过 vfs_poll() 完成,epoll fd 通过 __ep_eventpoll_poll() 完成。

static __poll_t ep_item_poll(const struct epitem *epi, poll_table *pt,
                 int depth)
{
    struct file *file = epi->ffd.file;
    __poll_t res;
 
    pt->_key = epi->event.events;
    if (!is_file_epoll(file))
        res = vfs_poll(file, pt);
    else
        res = __ep_eventpoll_poll(file, pt, depth);
    return res & epi->event.events;

(2) 当 fd1 上有事件的时候,是怎么通知到 epoll3 的

epoll3 被通知到有事件,需要经过两步骤:

第一步:fd1 上有事件,通知 epoll1

调用栈如下所示,底层协议栈收到数据之后通过 ep_poll_callback() 通知 epoll

0xffffffff8b9281e0 : ep_poll_callback+0x0/0x2c0 [kernel]
0xffffffff8b7027fa : __wake_up_common+0x7a/0x190 [kernel]
0xffffffff8b70298c : __wake_up_common_lock+0x7c/0xc0 [kernel]
0xffffffff8bd1cf07 : sock_def_readable+0x37/0x60 [kernel]
0xffffffff8bdd7243 : tcp_data_queue+0x233/0xb60 [kernel]
0xffffffff8bdd7d90 : tcp_rcv_established+0x220/0x620 [kernel]
0xffffffff8bde3c8a : tcp_v4_do_rcv+0x12a/0x1e0 [kernel]
0xffffffff8bde5b2d : tcp_v4_rcv+0xb3d/0xc40 [kernel]
0xffffffff8bdbafc9 : ip_local_deliver_finish+0x69/0x210 [kernel]
0xffffffff8bdbb2eb : ip_local_deliver+0x6b/0xe0 [kernel]
0xffffffff8bdbb5db : ip_rcv+0x27b/0x36a [kernel]
0xffffffff8bd3ebe1 : __netif_receive_skb_core+0xb41/0xc40 [kernel]
0xffffffff8bd3ed7d : netif_receive_skb_internal+0x3d/0xb0 [kernel]
0xffffffff8bd3f67a : napi_gro_receive+0xba/0xe0 [kernel]

第二步:epoll1 上有事件,通知 epoll3

这个通知在 ep_poll_callback 中发起,由上文可知,epoll1 加入到 epoll3 的时候,epoll3 在 epoll1 的 poll_wait 中入队了一个元素,所以这个时候,发现 epoll1 的 poll_wait 有元素,则通过 ep_poll_safe_wake() 调用 epoll3 的回调,同样最终会调到 ep_poll_callback() 将事件挂到 epoll 的就绪队列中,这样 epoll3 就能监听到事件了。

static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{
   ... 
   if (waitqueue_active(&ep->poll_wait))
    pwake++;
    ...
    if (pwake)
    ep_poll_safewake(ep, epi);
 
    ...
}

(3) 深度限制

由上边的分析,可以知道,当一个 epoll 被另外一个 epoll 监听的时候,事件上报路径会增加一级。epoll 中在向 epoll 添加需要被监听的描述符时,会对事件上报路径深度进行检查,如果检查不通过,则会添加失败。

引起上报深度过长的情况由两种:

  • ① 循环路径,epoll1 监听 epoll2, 同时 epoll2 监听 epoll1
  • ② 路径过长,如下边右边这张图,epoll1 被 epoll2 监听,epoll2 被 epoll3 监听 ...,epoll5 被 epoll6 监听。

图片图片

代码验证:

如下代码能验证以上两种情况,将 注释 1 和 注释 2 同时打开,能验证循环监听的情况; 将 注释 1 打开,注释 2 不打开,能验证监听路径太长的情况。

#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
 
#define SERVER_IP     ("0.0.0.0")
#define SERVER_PORT   (12345)
#define MAX_LISTENQ   (32)
#define MAX_EVENT     (128)
 
int create_tcp_server() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        printf("create socket error: %s\n", strerror(errno));
        return -1;
    }
 
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); /**< 0.0.0.0 all local ip */
    server_addr.sin_port = htons(SERVER_PORT);
 
    if (bind(listen_fd,(struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        printf("bind[%s:%d] error.\n", SERVER_IP, SERVER_PORT);
        return -1;
    }
 
    if (listen(listen_fd, MAX_LISTENQ) < 0) {
        printf("listen error.\n");
        return -1;
    }
 
    return listen_fd;
}
 
int main() {
    int ret = -1;
    int sock_fd = -1;
    int accetp_fd = -1;
    int event_num = -1;
    int fd1 = -1;
 
    struct sockaddr_in client_addr;
    struct sockaddr_in server_addr;
    socklen_t client = sizeof(struct sockaddr_in);
 
    struct epoll_event ev;
    struct epoll_event events[MAX_EVENT];
 
    fd1 = create_tcp_server();
    if (fd1 < 0) {
        printf("create server error\n");
        return -1;
    }
 
    int epoll1 = epoll_create(MAX_EVENT);
    if (epoll1 <= 0) {
        printf("cteare epoll1 failed, error: %s\n", strerror(errno));
        return -1;
    }
 
    int epoll2 = epoll_create(MAX_EVENT);
    if (epoll2 <= 0) {
        printf("cteare epoll2 failed, error: %s\n", strerror(errno));
        return -1;
    }
 
    int epoll3 = epoll_create(MAX_EVENT);
    if (epoll3 <= 0) {
        printf("cteare epoll3 failed, error: %s\n", strerror(errno));
        return -1;
    }
 
    int epoll4 = epoll_create(MAX_EVENT);
    if (epoll4 <= 0) {
        printf("cteare epoll4 failed, error: %s\n", strerror(errno));
        return -1;
    }
 
    int epoll5 = epoll_create(MAX_EVENT);
    if (epoll5 <= 0) {
        printf("cteare epoll5 failed, error: %s\n", strerror(errno));
        return -1;
    }
 
    int epoll6 = epoll_create(MAX_EVENT);
    if (epoll6 <= 0) {
        printf("cteare epoll6 failed, error: %s\n", strerror(errno));
        return -1;
    }
 
    ev.data.fd = fd1;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll1, EPOLL_CTL_ADD, fd1, &ev) < 0) {
        printf("add fd1 to epoll1 error: %s\n", strerror(errno));
        return -1;
    }
    
    // 注释 1
    /* 
    ev.data.fd = epoll1;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll2, EPOLL_CTL_ADD, epoll1, &ev) < 0) {
        printf("add epoll1 to epoll2 error: %s\n", strerror(errno));
        return -1;
    }
    */
    
    // 注释 2
    /*
    ev.data.fd = epoll2;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll1, EPOLL_CTL_ADD, epoll2, &ev) < 0) {
        printf("add epoll2 to epoll1 error: %s\n", strerror(errno));
        return -1;
    }
    */
 
    ev.data.fd = epoll2;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll3, EPOLL_CTL_ADD, epoll2, &ev) < 0) {
        printf("add epoll2 to epoll3 error: %s\n", strerror(errno));
        return -1;
    }
 
    ev.data.fd = epoll3;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll4, EPOLL_CTL_ADD, epoll3, &ev) < 0) {
        printf("add epoll3 to epoll4 error: %s\n", strerror(errno));
        return -1;
    }
    
    ev.data.fd = epoll4;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll5, EPOLL_CTL_ADD, epoll4, &ev) < 0) {
        printf("add epoll4 to epoll5 error: %s\n", strerror(errno));
        return -1;
    }
 
    ev.data.fd = epoll5;
    ev.events = EPOLLIN;
    if (ret = epoll_ctl(epoll6, EPOLL_CTL_ADD, epoll5, &ev) < 0) {
        printf("add epoll5 to epoll6 error: %s, ret: %d, epoll5: %d, epoll6: %d\n", strerror(errno), ret, epoll5, epoll6);
        return -1;
    }
 
    return 0;
}

循环路径报错信息以及代码位置:

图片图片

int do_epoll_ctl(int epfd, int op, int fd, struct epoll_event *epds,
         bool nonblock)
{
 
    error = epoll_mutex_lock(&ep->mtx, 0, nonblock);
    if (error)
        goto error_tgt_fput;
    if (op == EPOLL_CTL_ADD) {
        if (READ_ONCE(f.file->f_ep) || ep->gen == loop_check_gen ||
            is_file_epoll(tf.file)) {
            mutex_unlock(&ep->mtx);
            error = epoll_mutex_lock(&epmutex, 0, nonblock);
            if (error)
                goto error_tgt_fput;
            loop_check_gen++;
            full_check = 1;
            if (is_file_epoll(tf.file)) {
                tep = tf.file->private_data;
                error = -ELOOP; // 循环路径报错
                if (ep_loop_check(ep, tep) != 0)
                    goto error_tgt_fput;
            }
            error = epoll_mutex_lock(&ep->mtx, 0, nonblock);
            if (error)
                goto error_tgt_fput;
        }
    }
    return error;
}

路径太长报错信息及报错位置:

图片图片

static int ep_insert(struct eventpoll *ep, const struct epoll_event *event,
             struct file *tfile, int fd, int full_check)
{
    /* now check if we've created too many backpaths */
    if (unlikely(full_check && reverse_path_check())) {
        ep_remove(ep, epi);
        return -EINVAL;
    }
    return 0;
}

水平触发和边沿触发的区别。

通俗来讲,水平触发,只要有数据,epoll_wait() 就会一直返回;边沿触发,如果数据没处理完,并且这个 fd 没有新的事件,那么再次 epoll_wait() 的时候也不会有事件上来。

水平触发还是边沿触发,影响的是接收事件的行为。

(1)举个例子

假如一个 tcp socket 一次收到了 1MByte 的数据,但是 epoll_wait() 返回之后,用户一次只读取 1KB 的数据,读完之后再进行 epoll_wait() 监听事件,示例代码如下所示:

for (;;) {
        event_num = epoll_wait(epoll_fd, events, 1, -1);
        ret = recv(tcp_fd, buff, 1024, 0);
        if (ret <= 0) {
            break;
       }
    }

如果是水平触发,那么每次 epoll_wait() 都能返回这个 tcp_fd 的事件,共返回 (1MB / 1KB)= 1024 次事件;如果是边沿触发,那么只有第一次返回了 tcp_fd 的事件,后边的 epoll_wait() 也不返回了。

(2)代码

从代码实现来理解边沿触发和水平触发之间的区别。

用户在调用 epoll_wait() 之后,如果有事件,最终会调用 ep_send_events() 来做事件的转移,

主要工作是将事件从 rdllist 中转移到用户的 events 数组。

static int ep_send_events(struct eventpoll *ep,
              struct epoll_event __user *events, int maxevents)
{
    ep_start_scan(ep, &txlist);
    list_for_each_entry_safe(epi, tmp, &txlist, rdllink)
        revents = ep_item_poll(epi, &pt, 1);
        if (!revents)
            continue;
 
        events = epoll_put_uevent(revents, epi->event.data, events);
    }
    res++;
    if (epi->event.events & EPOLLONESHOT)
        epi->event.events &= EP_PRIVATE_BITS;
    else if (!(epi->event.events & EPOLLET)) { // 如果不是边沿触发,则将 epitem 重新加回就绪链表
        list_add_tail(&epi->rdllink, &ep->rdllist);
        ep_pm_stay_awake(epi);
    }
    return res;
}

对于从 rdllist 中获取的 epitem,首先调用 ep_item_poll() 确定有没有事件,以及事件的具体类型,如果有事件则件通过 epoll_put_uevent() 将事件保存存到用户的 event 数组中。

之后会判断是否为边沿触发,如果不是边沿触发,即水平触发,还会把这个 epitem 加入到 rdllist 的尾部,这样下次 epoll_wait() 的时候还会 poll 这个 fd,还会对这个 fd 进行 poll 操作,如果有事件还会向用户返回。

以上边举的例子来看,第 1 到 1024 次调用 epoll_wait() 都会返回事件,第 1025 次调用 epoll_wait() 的时候,由于数据都已经读取完毕,所以通过 ep_item_poll() 发现没有事件,这次当然也不会向用户返回事件,同时也不会把 epitem 返回 rdllist 了。

(3)实验

server:

#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
 
#define SERVER_IP     ("0.0.0.0")
#define SERVER_PORT   (12345)
#define MAX_LISTENQ   (32)
#define MAX_EVENT     (128)
 
int create_tcp_server() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        printf("create socket error: %s\n", strerror(errno));
        return -1;
    }
 
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); /**< 0.0.0.0 all local ip */
    server_addr.sin_port = htons(SERVER_PORT);
 
    if (bind(listen_fd,(struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        printf("bind[%s:%d] error.\n", SERVER_IP, SERVER_PORT);
        return -1;
    }
 
    if (listen(listen_fd, MAX_LISTENQ) < 0) {
        printf("listen error.\n");
        return -1;
    }
 
    return listen_fd;
}
 
int main() {
    int ret = -1;
    int sock_fd = -1;
    int accetp_fd = -1;
    int event_num = -1;
    int epoll_fd  = -1;
    int listen_fd = -1;
 
    struct sockaddr_in client_addr;
    struct sockaddr_in server_addr;
    socklen_t client = sizeof(struct sockaddr_in);
 
    struct epoll_event ev;
    struct epoll_event events[MAX_EVENT];
 
    listen_fd = create_tcp_server();
    if (listen_fd < 0) {
        printf("create server error\n");
        return -1;
    }
 
    epoll_fd = epoll_create(MAX_EVENT);
    if (epoll_fd <= 0) {
        printf("cteare epoll failed, error: %s\n", strerror(errno));
        return -1;
    }
 
    ev.data.fd = listen_fd;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) {
        printf("add listen fd to epoll error.\n");
        return -1;
    }
 
    char buff[1024] = {'\0'};
    int seq = 0;
    for (;;) {
        printf("epoll wait:\n");
        event_num = epoll_wait(epoll_fd, events, MAX_EVENT, -1);
        printf("events num:%d\n", event_num);
        for (int i = 0; i < event_num; i++ ) {
            if (events[i].events & EPOLLIN == 0) {
                /* 只监听 EPOLLIN 事件 */
                printf("fd %d error, cloase it, event[0x%x].\n", events[i].data.fd, events[i].events);
                close(events[i].data.fd);
                return -1;
            }
            
            if (events[i].data.fd == listen_fd) {
                accetp_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client);
                if(accetp_fd < 0) {
                    printf("accept error.\n");
                    return -1;
                }
 
                ev.data.fd = accetp_fd;
                ev.events = EPOLLIN; // 水平触发,默认触发方式
                // ev.events = EPOLLIN | EPOLLET; // 边沿触发,需要通过标记 EPOLLET 来指定
                if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, accetp_fd, &ev) < 0) {
                    printf("add fd to epoll error.\n");
                    return -1;
                }
                sleep(2); // 之所以等 2 秒,是为了让客户端发送 1M 的数据发送完毕
            } else {
                ret = recv(accetp_fd, buff, 1024, 0);
                if (ret <= 0) {
                    printf("recv error.\n");
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, accetp_fd, &ev);
                    close(accetp_fd);
                    events[i].data.fd = -1;
                    return -1;
                }
                printf("seq: %d, recv len: %d\n", seq, ret);
                seq++;
            }
        }
    }
 
    close(listen_fd);
}

client:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
#define SERVER_PORT   (12345)
#define SERVER_IP     "127.0.0.1"
#define MAX_EVENT     (128)
#define MAX_BUFSIZE   (512)
 
int main(int argc,char *argv[]) {
    int sock_fd;
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(sock_fd < 0) {
        printf("create socket error.\n");
        return -1;
    }
 
    struct sockaddr_in addr_serv;
    memset(&addr_serv, 0, sizeof(addr_serv));
 
    addr_serv.sin_family = AF_INET;
    addr_serv.sin_port =  htons(SERVER_PORT);
    addr_serv.sin_addr.s_addr = inet_addr(SERVER_IP);
 
    if(connect(sock_fd, (struct sockaddr *)&addr_serv,sizeof(struct sockaddr)) < 0){
        printf("connect error.\n");
        return -1;
    }
    char send_buff[1024 * 1024] = "epoll test!";
    int send_total_count = 0;
    int send_tmp_count = 0;
    while (send_total_count < 1024 * 1024) {
        send_tmp_count = send(sock_fd, send_buff, 1024 * 1024, 0);
        send_total_count += send_tmp_count;
    }
    printf("send 1MB success\n");
 
    close(sock_fd);
    return 0;
}

水平触发测试结果:

计数从 0 开始,到 1023 结束,共接收 1024 次数据,共 1MB

图片图片

边沿触发测试结果:第一次接受 1024 的数据之后,后边 epoll_wait() 不会返回事件。

图片图片

为什么当发送返回失败时才需要监听 EPOLLOUT 事件:

(1)接收数据和发送数据的区别: 一个被动,一个主动

使用一个连接(tcp socket) 无非就是做两件事情,接收数据和发送数据,

对于接收数据来说,接收方是被动的,只有对端的数据到达本端之后,才会产生 EPOLLIN 事件,之后进行实际数据的接收才是有意义的;

而对于发送来说,发送端是主动的,不是被动的,也就是说只要发送缓冲区有充足的空间,那么发送端就可以发送数据,而不需要有 EPOLLOUT 事件才能发送数据。

所以对于发送数据来说,并不需要一开始就监听 EPOLLOUT 事件,只需要等到这次发送失败(实际发送的数据长度小于要发送的数据长度), 才需要监听发送事件。而一旦 EPOLLOUT 事件上来之后,就需要清除这个事件,不需要一直监听,否则的话每次 epoll wait,只要发送侧有缓冲区可用,都会返回可写事件。

(2)tcp 上报 EPOLLOUT 事件的判断条件

static inline bool __sk_stream_is_writeable(const struct sock *sk, int wake)
{
        // 条件1:剩余空间大于发送缓冲区最大值的 1/3
        // 条件2:沒有发送出去的要小于 lowwat 的临界值
    return sk_stream_wspace(sk) >= sk_stream_min_wspace(sk) &&
           __sk_stream_memory_free(sk, wake);
}

epoll 相对于 select, poll 有什么区别:

(1)epoll 在内核态和用户态,只需要遍历有事件的 fd,不用全部遍历

这个是 epoll 和 select, poll 最明显的别,也是 epoll 最明显的优势。

假如要监听 500 个 fd,而现在只有 10 个 fd 有事件,对于 select 和 poll 来说,当调用 select(), poll() 之后,内核需要把 500 个 fd 遍历一遍,然后返回,返回之后,用户处理事件的时候,仍然要把所有 fd 都遍历一遍,同时处理有事件的 fd。

对于 epoll 来说,当调用 epoll_wait() 之后,在内核态只需要遍历 10 个文件描述符,返回之后,在用户态也只需要处理 10 个事件即可,不需要遍历全部的 500 个描述符。

图片图片

由此可见,select 和 poll 适用于 fd 个数比较少,或者 fd 个数比较多并且大多数 fd 都比较活跃的场景;而 epoll 适用于 fd 个数比较多,但是活跃 fd 占比不大的场景。

(2)监听的文件个数限制

① select 只能监听 1024 个文件描述符(0 ~ 1023)

select 能监听的 fd 的个数是有限制的,1024 个,并且这 1024 个 fd 的范围也是有限制的,只能是 0~ 1023, 假如你创建了一个 fd 是 2000, 但是被 select 监听的 fd 只有 10 个(没有超过数量限制),那么这个 2000 的 fd 也是不能被 select 监听。因为 fd_set 在内核是一个 bitmap, 用 bitmap 的位来表示一个 fd,所以能表示的 fd 的范围就是 0 ~ 1023。

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
 
#define __FD_SETSIZE    1024
typedef struct {
    unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

② poll 和 epoll 本身没有对文件按描述符个数的限制。

(3)目标事件和实际事件分离

① select 目标事件和实际事件没有分离

select 每次调用之前,都需要将自己要监听的读事件,写事件,错误事件分别设置到对应的 fd_set 中,内核处理的时候,如果要监听的事件,当前没有,就会把对应位置清除,返回给用户态。所以对于 select 来说,用户要监听的事件,以及用户返回的事件,都放到 fd_set 里边,共用这块数据空间。这就导致每次调用 select 时,都需要重新设置一边 fd_set。

② poll 和 epoll 实现了分离

poll 使用一个结构体 strucy poll_fd 来表示一个 被监听的 fd,并且目标事件和实际事件通过 events 和 reevents 来表示,这样对于用户来说不需要每次调用 poll() 之前都设置一遍目标事件。

struct pollfd {
    int fd;
    short int events;
    short int revents;
};

epoll 通过一个专门的 api epoll_ctl 来管理要监听的目标事件,epoll_wait() 的时候直接获取数据事件。

(4)系统调用个数的区别

select 和 poll 均是只有一个 系统调用,这样使用简单,但是每次都要传递要监听的 fd 以及监听的目标事件,导致在用户态和内核态传递的数据量较大;

epoll 通过三个 api 来实现,epoll_create(), epoll_ctl(), epoll_wait()。epoll 本身也占用一个 fd 资源,select,poll 不存在这种情况,但是 epoll 通过这种方式,将控制面和数据面完全分离,要监听哪些 fd,以及每个 fd 要监听的目标事件,完全在内核进行管理,这就减少了 epoll_wait() 时在用户态和内核态传输的数据量。

5.3epoll 惊群问题剖析

epoll 惊群问题是在多线程或多进程环境下使用 epoll 时可能遇到的一个问题。其现象是当有新的连接请求到达时,多个阻塞在epoll_wait上的线程或进程会同时被唤醒,但实际上只有一个线程或进程能够成功处理accept事件,其他线程或进程会收到EAGAIN错误。这就好比多个服务员在等待顾客进门,当有顾客进门时,所有服务员都冲过去迎接,但只有一个服务员能真正为顾客服务,其他服务员则白跑一趟。

产生这种问题的原因在于,当有新的连接请求到达时,操作系统会唤醒所有阻塞在相关 socket 上的线程或进程,而无法区分哪个线程或进程应该处理这个连接。在早期的 Linux 内核中,这种情况尤为明显。不过,现代的 Linux 内核已经对 accept 惊群问题进行了一定的优化,当有新的连接进入到 accept 队列的时候,内核会唤醒且仅唤醒一个进程来处理,通过添加WQ_FLAG_EXCLUSIVE标记,告诉内核进行排他性的唤醒,即唤醒一个进程后即退出唤醒的过程 。

但对于 epoll_wait 的惊群问题,在一些场景下仍然存在。例如,当多个进程共享同一个 epoll 实例,并且都在epoll_wait上阻塞时,就可能出现惊群现象。为了避免和处理惊群问题,在多线程环境下,可以只让一个线程进行epoll_wait监听,当有新的连接请求进来之后,由这个线程调用accept建立新的连接,然后交给其他工作线程处理后续的数据读写请求。在多进程环境下,像 Nginx 采用了全局互斥锁的方式,每个子进程在进行epoll_wait前先获取锁,只有获取到锁的子进程才能进行epoll_wait监听,这样可以避免多个子进程同时被唤醒,从而解决惊群问题。

六、epoll使用实例:TCP服务器处理多个客户端请求

6.1epoll创建

int epoll_create(int size); //监听个数

6.2epoll事件设置

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

第一个参数epfd是epoll_create()的返回值。

第二个参数op表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的fd。

第四个参数是告诉内核需要监听什么事。

struct epoll_event结构如下:

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

events可以是以下几个宏的集合:

  • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT:表示对应的文件描述符可以写;
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  • EPOLLERR:表示对应的文件描述符发生错误;
  • EPOLLHUP:表示对应的文件描述符被挂断;
  • EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

6.3epoll监听

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
  • 等待事件的产生,类似于select()调用。
  • 参数events用来从内核得到事件的集合,
  • maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
  • 参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。
  • 该函数返回需要处理的事件数目,如返回0表示已超时。

5.4编程实例测试

本次测试在上篇Unix域socket通信代码的基础上进行修改,只使用TCP方式的socket通信进行测试。上篇的测试代码,服务端接收到一个客户端的连接后,就仅对该客户端进行服务,没有再接收其它客户端的处理逻辑,本篇要实现的,就是一个服务端,能够接收多个客户端的数据。

编程之前,先来看下要实现的程序结构,其中黄色的部分为本篇在上篇例程的基础上,需要增加的部分:

图片图片


①为socket服务端增加epoll监听功能,TCP服务端的代码修改后如下,主要的修改在listen之后,创建一个epoll,然后把服务端的socketfd加入epoll进行监听:

当有新的客户端请求连接时,服务端的socketfd会收到事件,进而epoll会收到服务端socketfd的EPOLLIN事件,此时可以让服务端接受客户端的请求,并把创建的客户端fd也加入到epoll进行监听

当客户端连接成功并被epoll监听后,客户端再发消息过来,epoll就会收到对应客户端fd的EPOLLIN事件,此时可以让服务端读取客户端的消息

#define LISTEN_MAX     5
#define EPOLL_FDSIZE   LISTEN_MAX
#define EPOLL_EVENTS   20
#define CLIENT_NUM     3

void EpollAddEvent(int epollfd, int fd, int event)
{
    PRINT("epollfd:%d add fd:%d(event:%d)\n", epollfd, fd, event);
    struct epoll_event ev;
    ev.events = event;
    ev.data.fd = fd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
}

void TcpServerThread()
{
	//------------socket
	int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
	if (sockfd < 0)
	{
		PRINT("create socket fail\n");
		return;
	}
	PRINT("create socketfd:%d\n", sockfd);

	struct sockaddr_un addr;
	memset (&addr, 0, sizeof(addr));
	addr.sun_family = AF_UNIX;
	strcpy(addr.sun_path, UNIX_TCP_SOCKET_ADDR);

	//------------bind
	if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)))
	{
		PRINT("bind fail\n");
		return;
	}
	PRINT("bind ok\n");

	//------------listen
	if (listen(sockfd, LISTEN_MAX))
	{
		PRINT("listen fail\n");
		return;
	}
	PRINT("listen ok\n");

	//------------epoll---------------
	int epollfd = epoll_create(EPOLL_FDSIZE);
	if (epollfd < 0)
	{
		PRINT("epoll create fail\n");
		return;
	}
	PRINT("epoll create fd:%d\n", epollfd);

	EpollAddEvent(epollfd, sockfd, EPOLLIN);

	struct epoll_event events[EPOLL_EVENTS];
	while(1)
	{
		PRINT("epoll wait...\n");
		int num = epoll_wait(epollfd, events, EPOLL_EVENTS, -1);
		PRINT("epoll wait done, num:%d\n", num);
		for (int i = 0;i < num;i++)
		{
			int fd = events[i].data.fd;
			if (EPOLLIN == events[i].events)
			{
				//接受客户端的连接请求
				if (fd == sockfd)
				{
					//------------accept
					int clientfd = accept(sockfd, NULL, NULL);
					if (clientfd == -1)
					{
						PRINT("accpet error\n");
					}
					else
					{
						PRINT("=====> accept new clientfd:%d\n", clientfd);
						
						EpollAddEvent(epollfd, clientfd, EPOLLIN);
					}
				}
				//读取客户端发来的数据
				else
				{
					char buf[BUF_SIZE] = {0};
					//------------recv
					size_t size = recv(fd, buf, BUF_SIZE, 0);
					//size = read(clientfd, buf, BUF_SIZE);
					if (size > 0)
					{
						PRINT("recv from clientfd:%d, msg:%s\n", fd, buf);
					}
				}
			}
		}
	}

	PRINT("end\n");
}

⑵启动多个客户端进行测试,修改主程序,创建多个客户端线程,产生多个客户端,去连接同一个服务端,来测试epoll监听多个事件的功能。

int main()
{
	unlink(UNIX_TCP_SOCKET_ADDR);

	//创建一个服务端
	thread thServer(TcpServerThread);

	//创建多个客户端
	thread thClinet[CLIENT_NUM];
	for (int i=0; i<CLIENT_NUM; i++)
	{
		thClinet[i] = thread(TcpClientThread);
		sleep(1);
	}

	while(1)
	{
		sleep(5);
	}
}

本例中,CLIENT_NUM为3,使用3个客户端来测试epoll功能。

⑶测试结果,在Ubuntu上编译运行,程序运行时的打印如下:

[TcpServerThread] create socketfd:3
[TcpServerThread] bind ok
[TcpClientThread] create socketfd:4
[TcpServerThread] listen ok
[TcpServerThread] epoll create fd:5
[EpollAddEvent] epollfd:5 add fd:3(event:1)
[TcpServerThread] epoll wait...
[TcpClientThread] create socketfd:6
[TcpClientThread] connect ok
[TcpServerThread] epoll wait done, num:1
[TcpServerThread] =====> accept new clientfd:7
[EpollAddEvent] epollfd:5 add fd:7(event:1)
[TcpServerThread] epoll wait...
[TcpServerThread] epoll wait done, num:1
[TcpServerThread] recv from clientfd:7, msg:helloTCP(fd:4)1
[TcpServerThread] epoll wait...
[TcpClientThread] create socketfd:8
[TcpClientThread] connect ok
[TcpServerThread] epoll wait done, num:2
[TcpServerThread] recv from clientfd:7, msg:helloTCP(fd:4)2
[TcpServerThread] =====> accept new clientfd:9
[EpollAddEvent] epollfd:5 add fd:9(event:1)
[TcpServerThread] epoll wait...
[TcpServerThread] epoll wait done, num:1
[TcpServerThread] recv from clientfd:9, msg:helloTCP(fd:6)3
[TcpServerThread] epoll wait...
[TcpClientThread] connect ok
[TcpServerThread] epoll wait done, num:3
[TcpServerThread] =====> accept new clientfd:10
[EpollAddEvent] epollfd:5 add fd:10(event:1)
[TcpServerThread] recv from clientfd:7, msg:helloTCP(fd:4)5
[TcpServerThread] recv from clientfd:9, msg:helloTCP(fd:6)6
[TcpServerThread] epoll wait...
[TcpServerThread] epoll wait done, num:1
[TcpServerThread] recv from clientfd:10, msg:helloTCP(fd:8)4
[TcpServerThread] epoll wait...
[TcpServerThread] epoll wait done, num:3
[TcpServerThread] recv from clientfd:10, msg:helloTCP(fd:8)7
[TcpServerThread] recv from clientfd:7, msg:helloTCP(fd:4)8
[TcpServerThread] recv from clientfd:9, msg:helloTCP(fd:6)9
[TcpServerThread] epoll wait...
[TcpServerThread] epoll wait done, num:1
[TcpServerThread] recv from clientfd:7, msg:helloTCP(fd:4)10
[TcpServerThread] epoll wait...
[TcpServerThread] epoll wait done, num:2
[TcpServerThread] recv from clientfd:9, msg:helloTCP(fd:6)12
[TcpServerThread] recv from clientfd:10, msg:helloTCP(fd:8)11
[TcpServerThread] epoll wait...
[TcpServerThread] epoll wait done, num:3
[TcpServerThread] recv from clientfd:10, msg:helloTCP(fd:8)14
[TcpServerThread] recv from clientfd:7, msg:helloTCP(fd:4)13
[TcpServerThread] recv from clientfd:9, msg:helloTCP(fd:6)15
[TcpServerThread] epoll wait...
[TcpServerThread] epoll wait done, num:3
[TcpServerThread] recv from clientfd:10, msg:helloTCP(fd:8)16
[TcpServerThread] recv from clientfd:7, msg:helloTCP(fd:4)17
[TcpServerThread] recv from clientfd:9, msg:helloTCP(fd:6)18
[TcpServerThread] epoll wait...

对结果标注一下,更容易理解程序运行过程:

图片图片

可以看到,服务端依次接受了3个客户端的连接请求,然后可以接收3个客户端发来的数据。

责任编辑:武晓燕 来源: 深度Linux
相关推荐

2023-12-04 13:22:00

JavaScript异步编程

2022-10-31 09:00:24

Promise数组参数

2021-03-16 08:54:35

AQSAbstractQueJava

2011-07-04 10:39:57

Web

2019-02-13 16:22:53

网络虚拟化大二层

2009-11-30 16:46:29

学习Linux

2019-11-11 14:51:19

Java数据结构Properties

2022-12-02 09:13:28

SeataAT模式

2019-01-07 15:29:07

HadoopYarn架构调度器

2021-07-20 15:20:02

FlatBuffers阿里云Java

2012-05-21 10:06:26

FrameworkCocoa

2017-07-02 18:04:53

块加密算法AES算法

2022-09-26 09:01:15

语言数据JavaScript

2017-11-24 11:10:39

神经网络卷积神经网络全连接神经网络

2023-03-20 09:48:23

ReactJSX

2019-11-14 09:53:30

Set集合存储

2009-12-25 15:49:43

Linux rescu

2018-11-09 16:24:25

物联网云计算云系统

2022-01-11 07:52:22

CSS 技巧代码重构

2021-04-27 08:54:43

ConcurrentH数据结构JDK8
点赞
收藏

51CTO技术栈公众号