在Linux系统中,实现IO多路复用的机制有select、poll、epoll,其中epoll性能表现最为优秀。这里的复用指的是对进程的复用。
我们先从一个使用示例出发,接着探究epoll如何在底层做了支持。epoll使用示例:
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Code to set up listening socket, 'listen_sock',
(socket(), bind(), listen()) omitted. */
epollfd = epoll_create1(0);
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock,
(struct sockaddr *) &addr, &addrlen);
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd);
}
}
}
上面是官方手册里给出的一个使用示例,简单做一下解读:
要使用epoll,我们首先要有一个用来监听(接收新连接)的socket,listen_sock, 这个socket一般通过socket()、bind()、listen()等一系列系统调用得到。
epoll_create函数的size参数在Linux 2.6.8之后已经没有用了,但是必须大于0。epoll_create1函数对epoll_create进行了扩展。当flags=0,即当调用epoll_create1(0)时,
等同于调用epoll_create。flags传入其他值(如flags = EPOLL_CLOEXEC)时,会有不同的功能。
epoll_create函数创建了一个epoll对象,返回一个指向epoll对象的文件描述符。这个文件描述符(示例代码中的epollfd)很重要,是使用者与epoll机制交互的重要数据。
增加、修改、删除epoll对象的interest list,为目标文件描述符fd执行op操作,返回0表示成功,-1表示失败。op可以为以下值之一:
EPOLL_CTL_ADD
在epoll的interest list中增加一个条目,这个条目包含目标文件描述符fd和在event参数中的一些设置。
EPOLL_CTL_MOD
修改interest list中fd的设置置。
EPOLL_CTL_DEL
将fd从interest list中移除,此时event参数没有用。
结构体epoll_event的定义如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
epoll_event的events成员,可以为以下值之一:
EPOLLIN
EPOLLOUT
EPOLLRDHUP
EPOLLPRI
EPOLLERR
EPOLLHUP
EPOLLET
EPOLLONESHOT
EPOLLWAKEUP
EPOLLEXCLUSIVE
这些事件就不详细介绍了,具体可以参考epoll_ctl的官方手册。
等待epfd指向的epoll对象上的events事件发生。epoll_wait会阻塞调用进程直到以下事件发生:
文件描述符投递了一个事件过来
调用被一个signal handler中断
超时
timeout=-1时,epoll_wait会一直等待;timeout=0时,epoll_wait立即返回。
当accept之后,服务端进程会创建一个socket出来,用于与客户端通信,然后把socket放到当前进程的打开文件列表中。socket定义在include/linux/net.h中,
struct socket {
socket_state state;
short type;
unsigned long flags;
struct file *file;
struct sock *sk;
const struct proto_ops *ops;
struct socket_wq wq;
};
socket的源码非常复杂,这里我们只关注跟epoll机制紧密相关的一些数据结构。如下图:
可以看到,accept后,会生成一个socket,这个socket关联了一个等待队列和数据ready时的回调函数(sock_def_readable)。
当用户进程调用了epoll_create后,内核会创建一个struct eventpoll的内核对象,同样放到当前进程的打开文件列表中。eventpoll定义在fs/eventpoll.c中,
struct eventpoll {
struct mutex mtx;
/* Wait queue used by sys_epoll_wait() */
wait_queue_head_t wq;
/* Wait queue used by file->poll() */
wait_queue_head_t poll_wait;
/* List of ready file descriptors */
struct list_head rdllist;
/* Lock which protects rdllist and ovflist */
rwlock_t lock;
/* RB tree root used to store monitored fd structs */
struct rb_root_cached rbr;
/**省略 */
}
epoll_create主要是进行eventpoll对象的初始化工作,这其中就包含了wq、rdllist、rbr成员。
epoll_ctl是epoll机制的关键实现。直观的来理解,epoll_ctl执行完成后,内核中epoll相关的对象得以逐步串联起来。epoll_ctl完成的功能主要有:
下面这张图是笔者阅读源码后总结的,描述了epoll_ctl执行的一些关键动作。可能画的有些随意,如果读者能从中获取到以下两个信息:
那么,就能比较容易的理解epoll_ctl的功能。
如果读者觉得上面这张图过于复杂和不好理解,下面这张概念性的图也描述了epoll_ctl的功能。