epoll_create, epoll_wait, epoll_ctl, epoll_pwait, epoll_create1系统调用

系统调用号

  • epoll_create: 213
  • epoll_wait: 232
  • epoll_ctl: 233
  • epoll_pwait: 281
  • epoll_create1: 291

函数原型

内核接口

asmlinkage long sys_epoll_create(int size);
asmlinkage long sys_epoll_wait(int epfd, struct epoll_event __user *events, int maxevents, int timeout);
asmlinkage long sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event);
asmlinkage long sys_epoll_pwait(int epfd, struct epoll_event __user *events, int maxevents, int timeout, const sigset_t __user *sigmask, size_t sigsetsize);
asmlinkage long sys_epoll_create1(int flags);

glibc封装

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);
int epoll_create1(int flags);

简介

selectpoll一样,epoll机制也是为了实现IO多路复用。其使用方法更先进,内部实现也更高效。

我们可以理解为,Linux内核为了实现epoll机制,在内核空间维护了一个数据结构,称为epoll实例。其包含两个集合,一个是由用户感兴趣的文件描述符与相应的事件组成,另一个是由触发了相应事件的文件描述符与相应的事件组成。

我们的整体步骤是

  1. 创建一个epoll实例
  2. 对于epoll实例,将我们感兴趣的文件描述符与相应的事件添加到集合中
  3. 从触发事件的集合中提取相应的文件描述符

使用

epoll_createepoll_create1

其函数签名为

int epoll_create(int size);
int epoll_create1(int flags);

这两个系统调用就是在内核空间创建一个epoll实例,返回该实例的文件描述符。

对于epoll_createsize参数是被忽略的,但其必须大于0。

epoll_create1epoll_create的加强版。如果flags为0,则其行为与epoll_create一致。此外,flags还可以加入EPOLL_CLOEXEC标志位,和open中的O_CLOEXEC标志位功能一致,具体请看相应的描述

epoll_createepoll_create1创建的文件描述符,也就是epoll实例对应的文件描述符也应在程序结束前使用close关闭。但我们应当注意,正如open中描述的,如果使用了dup或者fork等会复制文件描述符的操作,我们将会有多个文件描述符指向Linux内核空间中的epoll实例。只有所有的指向该epoll实例的文件描述符都被关闭,其内核空间中的资源才会被释放。

epoll_ctl

其函数签名为

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

其可以看作epoll机制中较为核心的系统调用。

总的来说,该系统调用接受的四个参数的意义为:

  • epfd

    epoll实例的文件描述符

  • op

    希望进行的操作

  • fd

    感兴趣的文件描述符

  • events

    对应于该文件描述符,感兴趣的事件

首先,epfd的意义很简单,就是我们调用epoll_createepoll_create1返回的epoll实例的文件描述符。

op是我们希望进行的操作,包括:

  • EPOLL_CTL_ADD

    向epoll实例的用户感兴趣的集合中增添元素,文件描述符由fd给出,感兴趣的事件由event给出

  • EPOLL_CTL_MOD

    修改epoll实例的用户感兴趣的集合中的元素。希望修改的元素的文件描述符由fd给出,修改后的事件由event给出

  • EPOLL_CTL_DEL

    删除epoll实例的用户感兴趣的集合中的元素。希望删除的元素的文件描述符由fd给出,event变量将被忽略,可以为NULL

fdevents就是这个系统调用的核心参数。

fd可以看作epoll_ctl阶段,在epoll实例中,用于区分用户感兴趣的集合中的不同元素的方法。这是因为,我们的增加、修改、删除操作,都是基于fd来选择相应的元素的。

这里有一点需要我们考虑。我们考虑以下情况:

我们通过dupfork等复制文件描述符的操作,创造了fd1fd2这两个文件描述符,但是其都指向同一个文件描述。如果我们将fd1fd2都加入epoll实例的用户感兴趣的集合,同时其对应的用户感兴趣的事件是不同的。然后,我们使用close关闭fd1。但由于文件描述没有被释放,在我们使用epoll_wait获取触发了的事件时,仍然会有fd1对应的事件报告出来。

因此,只有指向同一文件描述的所有文件描述符都被关闭,在epoll实例的用户感兴趣的集合中才会删除所有相应的元素。所以,我们在使用close关闭某个被加入epoll实例的文件描述符之前,记得要使用EPOLL_CTL_DEL操作先删除相应的元素。

event参数的类型是struct epoll_event的指针。结合我们之后将到的epoll_wait,这个参数的作用是标记相应的事件。当我们把这个参数传递给epoll_ctl时,这个参数表明我们关心的事件。随后我们使用epoll_wait同样会获得这个类型的实例,其表明触发的事件。

struct epoll_event的定义为

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

首先来讲讲data字段。epoll_data_t的定义为

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

这个字段对epoll实例来说并不会起到实际的用途。当我们把data作为一个字段,放在某个用户感兴趣的epoll_event事件中,传入epoll_ctl函数,那么内核就会记录这个data。当相应的事件触发之后,用户使用epoll_wait等获得相应的事件,此时data会不经修改地出现在返回的事件中。

这个data常见的用途是,由于epoll_wait等方法获得事件时,无法直接获得该事件对应的文件描述符,所以我们在使用epoll_ctl时,将文件描述符作为data即可。随后在epoll_wait获得的事件中,取其data字段,获得相应的fd

events字段是epoll的核心。events字段是一个位掩码,其主要包含以下几种标志位:

  • EPOLLET

    epoll处理的事件有两种模式:边沿触发(edge-triggered)与水平触发(level-triggered)。

    考虑以下情形:

    1. 我们向epoll实例注册一个文件描述符rfd,其代表某个管道的读端。我们关心其是否已经可读
    2. 从写端往管道里写了2KB数据
    3. 我们使用epoll_wait获得触发了的事件,其中包括我们事先注册的rfd
    4. 我们从rfd读取了1KB数据
    5. 我们再次使用epoll_wait

    如果是边沿触发模式,那么epoll只会在第3步的epoll_wait中给出rfd被触发的事件,第5步则不会给出相应的事件;如果是水平触发,那么epoll在第3步和第5步都会给出相应的事件。

    也就是说,只有在相应的文件描述符状态发生变化,从别的状态变成我们感兴趣的状态时,“边沿触发”才会给出我们相应的事件;只要相应的文件描述符处于感兴趣的状态时,“水平触发”就会给出我们相应的事件。

    因此,当我们使用边沿触发模式时,我们的readwrite操作不能只使用一次,因为之后相关的事件就不会被触发,也就不能读取或写入完整的数据了。我们应当在循环中使用readwrite,直到其返回错误EAGAIN(详见open)。同时由于EAGAIN错误只有在使用O_NONBLOCK标志位打开文件时才会出现,所以我们在使用边沿触发时要注意两点:

    • 使用非阻塞的文件描述符
    • readwrite要一直读取或写入到返回EAGAIN错误

    如果我们的events包含标志位EPOLLET,则该事件是边沿触发模式;如果不包含该标志位,则该事件是水平触发模式。

    该标志位只可包含在传递给epoll_ctlevents中,不会出现在epoll_wait等返回的events中。

  • EPOLLIN

    文件描述符已经可以被读取

  • EPOLLOUT

    文件描述符已经可以被写入

  • EPOLLRDHUP

    流套接字的对端关闭连接

  • EPOLLPRI

    select中的其他条件,poll中的POLLPRI

  • EPOLLERR

    出现错误。

    该标志位可能出现在epoll_wait等返回的events中,epoll默认关注这样的状态,因此并没有必要包含在传递给epoll_ctlevents

  • EPOLLHUP

    相关的文件描述符处于挂起状态。

    该标志位可能出现在epoll_wait等返回的events中,epoll默认关注这样的状态,因此并没有必要包含在传递给epoll_ctlevents

  • EPOLLONESHOT

    如果包含该标志位,该事件被触发,被epoll_wait等返回,那么该事件对应的文件描述符将不再会有别的事件被epoll实例关注。也就是说,如果该文件描述符别的事件出现了,epoll实例并不会返回相应的结果。

    如果要再次接受相应的事件,就应在epoll_ctl中使用EPOLL_CTL_MOD,给该事件新的事件掩码。

    在一个多线程程序中,如果我们在一个循环中调用epoll,每次获得一个触发的事件,就开启一个新的线程去处理,那么有可能某个状态没有改变,但是导致某个事件被多次触发,从而使得我们有多个线程去处理同一个文件描述符的状态。因此我们可以使用EPOLLONESHOT标志位来避免这种事。

    该标志位只可包含在传递给epoll_ctlevents中,不会出现在epoll_wait等返回的events中。

epoll_waitepoll_pwait

这两个系统调用的函数签名为

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);

epoll_waitepoll_pwait就是从epoll实例的准备好的集合中,获取相应的事件。其参数包括:

  • epfd为epoll实例的文件描述符。
  • events为一个数组,其元素类型为struct epoll_event,长度为maxevents
  • timeout为超时参数。

对于events参数,epoll实例会从准备好的集合中,选取至多maxevents个事件放入该数组中。

对于timeout参数,其精度为毫秒,如果其为-1,则epoll_wait将无限等待;如果其为0,则epoll_wait将立即返回。

epoll_wait的返回值为准备好的文件描述符的个数。

pselectppoll类似,epoll_pwait就是加上了信号掩码的epoll_wait

因此,如果使用epoll来实现我们在selectpoll中提出的方案三,其方法为:

void process_fds(int *fds, int nfd) {
    int epfd = epoll_create1(0);
    for (int i = 0; i < nfd; i++) {
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = fds[i];
        epoll_ctl(epfd, EPOLL_CTL_ADD, fds[i], &ev);
    }
    struct epoll_event *events = (struct epoll_event *)malloc(nfd * sizeof(struct epoll_event));
    while (1) {
        int ready_nfd = epoll_wait(epfd, events, nfd, -1);
        if (ready_nfd <= 0) {
            break;
        }
        for (int i = 0; i < ready_nfd; i++) {
            if (events[i].events & EPOLLIN) {
                char buf[64];
                read(events[i].data.fd, buf, 64);
                process_read_content(buf);
            }
        }
    }
}

实现

epoll的实现位于Linux内核的fs/eventpoll.c文件中。

在epoll的实现中,有两个结构体最为关键:struct eventpollstruct epitem

struct eventpoll就是内核中的epoll实例的结构体,而struct epitem就是一个文件描述符与它相关的事件组成的结构体,也就是epoll实例的两个集合的元素。

它们的部分字段如下:

struct eventpoll:

struct eventpoll {
	/* 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;

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

	struct list_head visited_list_link;
};

struct epitem:

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;
};

首先是struct rb_root_cached rbr这个字段。这就是epoll实例中用于存储用户感兴趣的事件的结构。它是一个红黑树,其包含的元素可以看作我们的struct epitem(其字段rbn就是表示在这棵红黑树的节点)。当我们使用epoll_create创建一个epoll实例时,这棵红黑树被初始化。当我们使用epoll_ctl去操作感兴趣的集合时,我们实际上就是增添、修改、删除这棵红黑树的元素。

这里值得注意的是,我们之前提到,在epoll_ctl阶段,可以把文件描述符看作集合的键,在我们操作这个集合的时候,通过这个键来区分不同的元素。但实际并不是这样。

以这棵红黑树的插入为例,其实现为ep_rbtree_insert:

static void ep_rbtree_insert(struct eventpoll *ep, struct epitem *epi)
{
	int kcmp;
	struct rb_node **p = &ep->rbr.rb_root.rb_node, *parent = NULL;
	struct epitem *epic;
	bool leftmost = true;

	while (*p) {
		parent = *p;
		epic = rb_entry(parent, struct epitem, rbn);
		kcmp = ep_cmp_ffd(&epi->ffd, &epic->ffd);
		if (kcmp > 0) {
			p = &parent->rb_right;
			leftmost = false;
		} else
			p = &parent->rb_left;
	}
	rb_link_node(&epi->rbn, parent, p);
	rb_insert_color_cached(&epi->rbn, &ep->rbr, leftmost);
}

可以看到在11行,通过调用ep_cmp_ffd来判断是否两个元素相同。首先我们来看看struct epitemffd字段,其类型为struct epoll_filefd:

struct epoll_filefd {
	struct file *file;
	int fd;
} __packed;

ep_cmp_ffd的实现为

static inline int ep_cmp_ffd(struct epoll_filefd *p1, struct epoll_filefd *p2)
{
	return (p1->file > p2->file ? +1:
	        (p1->file < p2->file ? -1 : p1->fd - p2->fd));
}

因此我们可以看到,内核是同时使用文件描述与文件描述符作为这棵红黑树的键的。

如果我们想通过epoll_ctl增加一个我们感兴趣的元素,我们做的核心实际上是增加了一个回调函数。首先我们需要知道,在include/linux/poll.h中,我们对某个文件的poll操作,其最终是这样的情形:

/* 
 * structures and helpers for f_op->poll implementations
 */
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);

/*
 * Do not touch the structure directly, use the access functions
 * poll_does_not_wait() and poll_requested_events() instead.
 */
typedef struct poll_table_struct {
	poll_queue_proc _qproc;
	__poll_t _key;
} poll_table;

static inline __poll_t vfs_poll(struct file *file, struct poll_table_struct *pt)
{
	if (unlikely(!file->f_op->poll))
		return DEFAULT_POLLMASK;
	return file->f_op->poll(file, pt);
}

我们使用vfs_poll之后,会把filept传入其对应的实现中。而ptstruct poll_table_struct的指针,其中,_key字段是一个掩码,表明哪些事件是用户关注的;poll_queue_proc _qproc是一个函数指针。当出现了_key中的事件时,会自动触发这个回调函数。

当我们使用epoll_ctl去创建新的红黑树节点时,有一步为

/* Initialize the poll table using the queue callback */
epq.epi = epi;
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.
 */
revents = ep_item_poll(epi, &epq.pt, 1);

这就是我们设置相应回调函数的地方。epq的类型是struct ep_pqueue,其定义为

struct ep_pqueue {
	poll_table pt;
	struct epitem *epi;
};

也就是说把poll_table封装了一层。我们通过init_poll_funcptr设置了epqpoll_table,然后通过ep_item_poll把这个poll_table传入了最终的vfs_poll函数中。

在epoll中,poll_table_key字段,也就是用户感兴趣的事件是全部事件,epoll会从触发的事件中筛选出用户感兴趣的事件。回调函数则是ep_ptable_queue_proc,其设置了回调函数ep_poll_callback

TODO:增加更多的描述。参考资料: