IO多路复用技术之epoll源码

4,982 阅读6分钟

epoll 是笔者在学习 Netty 源码时接触到的。当时从 Netty 源码一路看到 Java NIO 的源码,最后挖到底层 c 的 native 方法才了解到 epoll。为此特地参阅了几本关于 Linux 的书籍,随着学习的深入愈发自觉无知,直到发现看书还是不能解决自己的疑惑时就自己查看源码,并且看了看其他服务端开发的大佬关于这方面的文章,虽然很多大佬的文章末尾不写参考来源,这让我觉得这些文章可信度很低,但是还是强迫自己将信将疑地看一遍,毕竟自己能力有限,只能用这些没有参考来源的文章来解释自己的疑惑了,往后能力提升之后会填上这个坑。

版本约束

linux v2.6.12

API 介绍

所有源码分析都应该是从一段示例代码开始的,以常用的 api 作为切入口去窥视其整体机制和原理

示例代码

int main() {
    int fds[] = ...;  // 关心的 socket 数组
    int epfd = epoll_create(...); // 📌 创建 epoll 实例
    // 将关心的 socket 添加到 epoll 中(红黑树等)
    for (int i=0; i < fds.length; i++){
        epoll_ctl(epfd,EPOLL_CTL_ADD, fds[i], ...); // 📌
    }

    // 定义一个结构,用来接收就绪的事件
    struct epoll_event events[MAX_EVENTS];
    while(1){
        // 如果无事件发生,那么进程将阻塞在这里
        // 如果有事件发生,则返回就绪的事件个数,同时事件被存储在 events 中
        int n = epoll_wait(epfd, &events,...); // 📌
        for (int i=0; i < n; i++) {
            // 通过下标取到返回的就绪事件,进行对应的逻辑处理
            new_event = events[i];
        }
    }

    return 0;
}
  • 系统调用 epoll_create() 创建一个 epoll 实例,返回代表该实例的文件描述符
  • 系统调用 epoll_ctl() 操作同 epoll 实例相关联的兴趣列表。通过 epoll_ctl(),我们可以增加新的描述符到列表中,将已有的文件描述符从该列表中移除,以及修改代表文件描述符上事件类型的位掩码
  • 系统调用 epoll_wait() 返回与 epoll 实例相关联的就绪列表中的成员

epoll_create()

系统调用 epoll_create() 创建了一个新的 epoll 实例

#include <sys/epoll.h>
int epoll_create(int size);
  • 参数 size 指定了我们想要通过 epoll 实例来检查的文件描述符个数。该参数并不是一个上限,而是告诉内核应该如何为内部数据结构划分初始大小
  • 作为函数返回值,epoll_create() 返回了代表新创建的 epoll 实例的文件描述符

epoll_ctl()

系统调用 epoll_ctl() 能够修改由 文件描述符 epfd 所代表的 epoll 实例中的 兴趣列表

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
  • 参数 fd 指明了要修改兴趣列表中的哪一个文件描述符的设定。该参数可以是代表管道、FIFO、套接字、POSIX 消息队列、inotify 实例、终端、设备,甚至是另一个 epoll 实例的文件描述符(例如,我们可以为受检查的描述符建立起一种层次关系)。但是,这里 fd 不能作为普通文件或目录的文件描述符
  • 参数 op 用来指定需要执行的操作

epoll_wait()

系统调用 epoll_wait() 返回 epoll 实例中处于就绪态的文件描述符信息。单个 epoll_wait() 调用能返回多个就绪态文件描述符的信息

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
  • 参数 evlist 所指向的结构体数组中返回的是有关就绪态文件描述符的信息
  • 参数 timeout 用来确定 epoll_wait() 的阻塞行为
    • 如果 timeout 等于−1,调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生,或者直到捕获到一个信号为止
    • 如果 timeout 等于 0,执行一次非阻塞式的检查
    • 如果 timeout 大于 0,调用将阻塞至多 timeout 毫秒
  • epoll_event 将在下一小节讲解
  • 调用成功后,epoll_wait() 返回数组 evlist 中的元素个数。如果在 timeout 超时间隔内没有任何文件描述符处于就绪态的话,返回 0。出错时返回−1

epoll_event

struct epoll_event {
	__u32 events;
	__u64 data;
} EPOLL_PACKED;

events 字段上的掩码值:

深入探究 epoll 的语义

文件描述(file description) 表示的是一个打开文件的上下文信息(大小、内容、编码等与文件有关的信息),可以比喻为一个抽屉,这部分内容实际上是由内核来管理的。而用户空间的应用程序如果要操作文件怎么办。就是通过 open() 这样的系统调用向内核请求,然后内核分配给用户空间一个文件描述符(file descriptor)。这个文件描述符可以比喻为抽屉的把手(handle 之所以翻译为“句柄”,这就是原因),有了这个把手(文件描述符),用户就可以操作抽屉(文件描述)里的内容了。但是,一个抽屉可以有多个把手(即文件描述可以对应多个文件描述符),只有当所有的把手(文件描述符)都关闭了,内核就知道此时没有用户空间的程序要用这个抽屉了(文件描述),那么就把它回收

文件描述 实际上是内核中的一个数据结构,而用户空间中的文件描述符只不过是一个整数,epoll 的兴趣列表实际关注的是内核中的数据结构

源码分析

以下为源码执行流程图 (此图并非是高清图,原图是 svg,但是因为掘金不能放 svg,所以强烈建议可以去 语雀 看)

epoll()源码 2022-06-07 12.11.29.excalidraw.png

sys_epoll_create()

此函数是用户空间 epoll_create(2) 的内核部分

函数定义

详细定义请看这里 此处只给出函数名、参数和返回值类型:

asmlinkage long sys_epoll_create(int size){...}

函数流程

  • 创建 fd、inode 和 file
  • 创建并初始化 eventpoll 结构体 ep,并将 ep 放入 file->private,并返回 fd eventpoll 的定义如下

eventpoll 结构体

struct eventpoll {
	/* Protect the this structure access */
	rwlock_t lock;

	/*
	 * This semaphore is used to ensure that files are not removed
	 * while epoll is using them. This is read-held during the event
	 * collection loop and it is write-held during the file cleanup
	 * path, the epoll file exit code and the ctl operations.
	 */
	struct rw_semaphore sem;

	/* 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;

	// 用于存储受监控的fd结构的红黑树树根
	struct rb_root rbr;
};

sys_epoll_ctl()

此函数是用户空间 epoll_ctl(2) 的内核部分

函数定义

详细定义请看这里 此处只给出函数名、参数和返回值类型:

asmlinkage long sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event){...}
  • epfd 是 epoll 的文件描述符
  • fd 是要加入 epoll 的文件描述符

函数流程

  • 根据传入的参数 epfd、fd 分别获取对应的 file 类型的变量 file、tfile
  • 若 tfile 不支持 poll 才能往下走
  • 若 file 不等于 tfile 且 file 是 epoll 文件才能往下走
  • 通过 file 的 private_data 获取 eventpoll 并给 eventpoll 加上写锁
  • 在 eventpoll 的字段 rbr 所代表的红黑树中查找 tfile
  • 根据传入的参数 op 执行相应地对红黑树的操作(添加、删除和修改红黑树节点)

sys_epoll_wait()

此函数是用户空间 epoll_wait(2) 的内核部分

函数定义

详细定义请看这里 此处只给出函数名、参数和返回值类型:

asmlinkage long sys_epoll_wait(int epfd, struct epoll_event __user *events,int maxevents, int timeout){...}
  • epfd 是 epoll 的文件描述符

函数流程

  • 根据传入的参数 epfd 获取对应的 file 类型的变量 file
  • 通过 file 的 private_data 获取 eventpoll
  • 调用 ep_poll()
    • eventpoll 内存储的就绪队列 rdllist 是否为空
      • 不空,进入循环
        • rdllist 不空,跳出循环
        • rdllist 为空,jtimeout 为 0,跳出循环
        • rdllist 为空,jtimeout 不为 0
          • 检查当前进程是否有信号处理,返回不为0表示有信号需要处理,跳出循环
          • 返回为0表示无信号需要处理,睡眠阻塞
      • 为空
        • res 为 0 && rdllist 不空 && 传输到用户空间的事件数为 0 && jtimeout 为 0
          • 去到循环开头处
        • 否则
          • 返回 res

重要数据结构之间的联系

疑问

看完并且整理了源码之后,只能说建立了初步的认识,因为笔者没有调试源码,所以还有很多疑问,如下:

  • 回调执行时机
  • rdlist 里存的是什么
  • wq 里存的是什么
  • rdlist、wq 和红黑树之间的关系
  • rdlist 和红黑树是存在哪里的
  • 水平触发和边缘触发是什么

参考

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!