网络编程:epoll
原理
select 的几个缺点:
1)每次调用select,都需要把fd集合从用户空间拷贝到内核空间,这个开销在fd很多时会很大
2)每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也会很大
3)select支持的文件描述符数量太小了,默认是1024
在调用接口上,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数:epoll_create、epoll_ctl和epoll_wait。epoll_create是创建一个epoll句柄,epoll_ctl是注册要监听的事件类型,epoll_wait是等待事件的产生。
对于第一个缺点,epoll的解决方案在epoll_ctl函数中。 每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍,并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者,就会调用这个回调函数,而这个 回调函数会把就绪的fd加入一个就绪链表。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(就绪链表是否为空)。
对于第三个缺点,epoll没有这个限制,它所 支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,具体可以cat /proc/sys/fs/file-max查看,在1GB内存的机器上大约是10万左右。
epoll的回调机制:
/*
* This is the callback that is used to add our wait queue to the
* target file wakeup lists.
*/
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_cache_alloc(pwq_cache, GFP_KERNEL))) {
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
pwq->whead = whead;
pwq->base = epi;
add_wait_queue(whead, &pwq->wait);
list_add_tail(&pwq->llink, &epi->pwqlist);
epi->nwait++;
} else {
/* We have to signal that an error occurred */
epi->nwait = -1;
}
}
其中init_waitqueue_func_entry的实现如下:
static inline void init_waitqueue_func_entry(wait_queue_t *q,
wait_queue_func_t func)
{
q->flags = 0;
q->private = NULL;
q->func = func;
}
可以看到,总体上和select实现是类似的,只不过它是创建了一个epoll_entry结构pwq,pwq->wait的func成员被设置成了回调函数ep_poll_callback(而不是default_wake_function,所以这里并不会有唤醒操作而只是执行回调函数),private成员被设置成了NULL。最后把pwq->wait链入到whead中(也就是设备等待队列中)。这样,当设备等待队列中的进程被唤醒时,就会调用ep_poll_callback了。
epoll的流程:
当epoll_wait时,它会判断就绪链表中有没有就绪的fd,如果没有,则把current进程加入到一个等待队列(file->private_data->wq)中,并在一个while(1)循环中判断就绪队列是否为空,并结合schedule_timeout实现睡一会。如果current进程在睡眠中,设备就绪了,就会调用回调函数。在回调函数中,会把就绪的fd放到就绪链表,并唤醒等待队列(file->private_data->wq)中的current进程,这样epoll_wait又能继续执行下去了。
API
epoll 不仅提供了默认的 level-triggered(条件触发)机制,还提供了性能更为强劲的 edge-triggered(边缘触发)机制
使用 epoll 进行网络程序的编写,需要三个步骤,分别是 epoll_create
,epoll_ctl
和 epoll_wait
。
- epoll_create:用于创建一个epoll实例
int epoll_create(int size);
int epoll_create1(int flags);
返回值: 若成功返回一个大于0的值,表示epoll实例;若返回-1表示出错
size参数
:用来告知内核期望监控的文件描述字大小,然后内核使用这部分的信息来初始化内核数据结构。现在,对size设置为一个大于0的整数就 可以
flags参数
:输入flags为0,则和epoll_create一样,内核自动忽略
- epoll_ctl :往这个epoll实例中添加删除监控的事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
返回值: 若成功返回0;若返回-1表示出错
第一个参数epfd
:调用epoll_create创建的epoll实例描述字,可简单理解为epoll的句柄
第二个参数op
:表示增加/删除一个监控事件,有三个选项可供选择:
- EPOLL_CTL_ADD: 向 epoll 实例注册文件描述符对应的事件;
- EPOLL_CTL_DEL:向 epoll 实例删除文件描述符对应的事件;
- EPOLL_CTL_MOD: 修改文件描述符对应的事件。
第三个参数fd
:注册的事件的文字描述符,比如一个监听套接字
第四个参数event
:表示注册的事件类型,并且可以在这个结构体里设置用户需要的数据,其中最为常见的是使用联合结构里的fd字段,表示事件所对应的文件描述符
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 */
};
重点看一下这几种事件类型:
- EPOLLIN:表示对应的文件描述字可以读;
- EPOLLOUT:表示对应的文件描述字可以写;
- EPOLLRDHUP:表示套接字的一端已经关闭,或者半关闭;
- EPOLLHUP:表示对应的文件描述字被挂起;
- EPOLLET:设置为 edge-triggered,默认为 level-triggered。
- epoll_wait:调用者进程被挂起,在等待内核I/O事件的分发
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
返回值: 成功返回的是一个大于0的数,表示事件的个数;返回0表示的是超时时间到;若出错返回-1.
第一个参数epfd
是 epoll 实例描述字,也就是 epoll 句柄。第二个参数events
返回给用户空间需要处理的 I/O 事件,这是一个数组,数组的大小由 epoll_wait 的返回值决定,这个数组的每个元素都是一个需要待处理的 I/O 事件,其中 events 表示具体的事件类型,事件类型取值和 epoll_ctl 可设置的值一样,这个 epoll_event 结构体里的 data 值就是在 epoll_ctl 那里设置的 data,也就是用户空间和内核空间调用时需要的数据。第三个参数maxevents
是一个大于 0 的整数,表示 epoll_wait 可以返回的最大事件值。第四个参数timeout
是 epoll_wait 阻塞调用的超时值,如果这个值设置为 -1,表示不超时;如果设置为 0 则立即返回,即使没有任何 I/O 事件发生
实践
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#define SERV_PORT 43211
#define LISTENQ 1024
#define MAXEVENTS 128
char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
void make_nonblocking(int fd)
{
fcntl(fd, F_SETFL, O_NONBLOCK);
}
int tcp_nonblocking_server_listen(int port)
{
int listen_fd;
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
make_nonblocking(listen_fd);
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
int on = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int rt1 = bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if(rt1 < 0)
{
perror("bind error\n");
return -1;
}
int rt2 = listen(listen_fd, LISTENQ);
if(rt2 < 0)
{
perror("listen failed");
return -1;
}
signal(SIGPIPE, SIG_IGN);
return listen_fd;
}
int main(int argc, char *argv[])
{
int listen_fd, socket_fd;
int n, i;
int efd;
struct epoll_event event;
struct epoll_event *events;
listen_fd = tcp_nonblocking_server_listen(SERV_PORT);
//为epoll创建实例
efd = epoll_create1(0);
if(efd == -1)
{
perror("epoll create failed");
return -1;
}
event.data.fd = listen_fd;
event.events = EPOLLIN | EPOLLET;
// 调用epoll_ctl将监听字对应的I/O事件进行注册,有新的连接建立,就可以感知,采用edge-triggered边缘触发
if(epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1)
{
perror("epoll_ctl add listen fd failed");
return -1;
}
// Buffer where events are returned
events = calloc(MAXEVENTS, sizeof(event));
while(1)
{
// 调用epoll_wait函数分发I/O事件,当epoll_wait成功返回后,通过遍历返回的event数组,就可知道发生的I/O事件
n = epoll_wait(efd, events, MAXEVENTS, -1);
printf("epoll_waite wakeup\n");
for(i = 0; i < n; i++)
{
if((events[i].events & EPOLLERR) ||
(events[i].events & EPOLLHUP) ||
(!events[i].events & EPOLLIN))
{
fprintf(stderr, "epoll error\n");
close(events[i].data.fd);
continue;
}
else if(listen_fd == events[i].data.fd)
{
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
if(fd < 0)
{
perror("accept failed");
return -1;
}
else
{
// accept建立连接,并将该连接设置为非阻塞,在调用epoll_ctl把已连接套接字对应的可读事件
// 注册到epoll实例中,这里使用了event_data里面的fd字段,将连接套接字存储器中
make_nonblocking(fd);
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;// edge-triggered
if(epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1)
{
perror("epoll_ctl add connection fd failed");
return -1;
}
}
continue;
}
else
{
socket_fd = events[i].data.fd;
printf("get event on socket fd == %d\n",socket_fd);
while(1)
{
char buf[512];
if((n = read(socket_fd, buf, sizeof(buf))) < 0)
{
if(errno != EAGAIN)
{
perror("read error");
close(socket_fd);
}
break;
}
else if(n == 0)
{
close(socket_fd);
break;
}
else
{
for(i = 0;i < n; ++i)
{
buf[i] = rot13_char(buf[i]);
}
if(write(socket_fd, buf, n) < 0)
{
perror("write error");
return -1;
}
}
}
}
}
}
free(events);
close(listen_fd);
return 0;
}
运行结果
边缘触发vs水平触发
条件触发的意思是只要满足事件的条件,比如有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。
一般认为,边缘触发的效率比条件触发的效率要高
边缘触发:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#define SERV_PORT 43211
#define LISTENQ 1024
#define MAXEVENTS 128
char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
void make_nonblocking(int fd)
{
fcntl(fd, F_SETFL, O_NONBLOCK);
}
int tcp_nonblocking_server_listen(int port)
{
int listen_fd;
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
make_nonblocking(listen_fd);
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
int on = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int rt1 = bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if(rt1 < 0)
{
perror("bind error\n");
return -1;
}
int rt2 = listen(listen_fd, LISTENQ);
if(rt2 < 0)
{
perror("listen failed");
return -1;
}
signal(SIGPIPE, SIG_IGN);
return listen_fd;
}
int main(int argc, char *argv[])
{
int listen_fd, socket_fd;
int n, i;
int efd;
struct epoll_event event;
struct epoll_event *events;
listen_fd = tcp_nonblocking_server_listen(SERV_PORT);
efd = epoll_create1(0);
if(efd == -1)
{
perror("epoll create failed");
return -1;
}
event.data.fd = listen_fd;
event.events = EPOLLIN | EPOLLET;
if(epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1)
{
perror("epoll_ctl add listen fd failed");
return -1;
}
// Buffer where events are returned
events = calloc(MAXEVENTS, sizeof(event));
while(1)
{
n = epoll_wait(efd, events, MAXEVENTS, -1);
printf("epoll_waite wakeup\n");
for(i = 0; i < n; i++)
{
if((events[i].events & EPOLLERR) ||
(events[i].events & EPOLLHUP) ||
(!events[i].events & EPOLLIN))
{
fprintf(stderr, "epoll error\n");
close(events[i].data.fd);
continue;
}
else if(listen_fd == events[i].data.fd)
{
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
if(fd < 0)
{
perror("accept failed");
return -1;
}
else
{
make_nonblocking(fd);
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;// edge-triggered
if(epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1)
{
perror("epoll_ctl add connection fd failed");
return -1;
}
}
continue;
}
else
{
socket_fd = events[i].data.fd;
printf("get event on socket fd == %d\n",socket_fd);
}
}
}
free(events);
close(listen_fd);
return 0;
}
执行效果:
可发现,边缘触发情况下,开启这个服务器程序,用 telnet 连接上,输入一些字符,我们看到,服务器端只从 epoll_wait 中苏醒过一次,就是第一次有数据可读的时候。
水平触发:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#define SERV_PORT 43211
#define LISTENQ 1024
#define MAXEVENTS 128
char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
void make_nonblocking(int fd)
{
fcntl(fd, F_SETFL, O_NONBLOCK);
}
int tcp_nonblocking_server_listen(int port)
{
int listen_fd;
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
make_nonblocking(listen_fd);
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
int on = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int rt1 = bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if(rt1 < 0)
{
perror("bind error\n");
return -1;
}
int rt2 = listen(listen_fd, LISTENQ);
if(rt2 < 0)
{
perror("listen failed");
return -1;
}
signal(SIGPIPE, SIG_IGN);
return listen_fd;
}
int main(int argc, char *argv[])
{
int listen_fd, socket_fd;
int n, i;
int efd;
struct epoll_event event;
struct epoll_event *events;
listen_fd = tcp_nonblocking_server_listen(SERV_PORT);
efd = epoll_create1(0);
if(efd == -1)
{
perror("epoll create failed");
return -1;
}
event.data.fd = listen_fd;
event.events = EPOLLIN | EPOLLET;
if(epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1)
{
perror("epoll_ctl add listen fd failed");
return -1;
}
// Buffer where events are returned
events = calloc(MAXEVENTS, sizeof(event));
while(1)
{
n = epoll_wait(efd, events, MAXEVENTS, -1);
printf("epoll_waite wakeup\n");
for(i = 0; i < n; i++)
{
if((events[i].events & EPOLLERR) ||
(events[i].events & EPOLLHUP) ||
(!events[i].events & EPOLLIN))
{
fprintf(stderr, "epoll error\n");
close(events[i].data.fd);
continue;
}
else if(listen_fd == events[i].data.fd)
{
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
if(fd < 0)
{
perror("accept failed");
return -1;
}
else
{
make_nonblocking(fd);
event.data.fd = fd;
event.events = EPOLLIN;// level-triggered
if(epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1)
{
perror("epoll_ctl add connection fd failed");
return -1;
}
}
continue;
}
else
{
socket_fd = events[i].data.fd;
printf("get event on socket fd == %d\n",socket_fd);
}
}
}
free(events);
close(listen_fd);
return 0;
}
效果:
然后按照同样的步骤来一次,观察服务器端,可看到,服务器端不断地从 epoll_wait 中苏醒,告诉我们有数据需要读取。
小结
epoll 通过改进的接口设计,避免了用户态 - 内核态频繁的数据拷贝,大大提高了系统性能。在使用 epoll 的时候,我们一定要理解条件触发和边缘触发两种模式。条件触发的意思是只要满足事件的条件,比如有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。