网络编程: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 的时候,我们一定要理解条件触发和边缘触发两种模式。条件触发的意思是只要满足事件的条件,比如有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。
网络编程:epoll的更多相关文章
- UNIX网络编程——epoll 的accept , read, write(重要)
在一个非阻塞的socket上调用read/write函数,返回EAGAIN或者EWOULDBLOCK(注:EAGAIN就是EWOULDBLOCK). 从字面上看,意思是: EAGAIN: 再试一次 E ...
- UNIX网络编程——epoll 系列函数简介、与select、poll 的区别
前面博客<<UNIX环境高级编程--epoll函数使用详解>>有关于epoll函数的讲解. 一.epoll 系列函数简介 #include <sys/epoll.h> ...
- Socket网络编程--epoll小结
以前使用的用于I/O多路复用为了方便就使用select函数,但select这个函数是有缺陷的.因为它所支持的并发连接数是有限的(一般小于1024),因为用户处理的数组是使用硬编码的.这个最大值为FD_ ...
- Linux 网络编程->epoll<-LT/ET模式整理(~相逢何必曾相识~)
今天自己整理一下epoll,网上有很多经典的介绍,看了很多~收藏了很多~还是整理一下做个积累, 自己的东西好找~ 1. epoll 模型简介 epoll 是Linux I/O 多路复用接口 selec ...
- UNIX网络编程——epoll的 et,lt关注点
epoll模型有两种工作模式,ET和LT两种模式下都有一些细节值得注意,以下是一些思考: 一.ET模式下 Q1:调用accept时,到底TCP完成队列里有多少个已经建立好的连接? 这 ...
- UNIX环境高级编程——epoll函数使用详解
epoll - I/O event notification facility 在linux的网络编程中,很长的时间都在使用select来做事件触发.在linux新的内核中,有了一种替换它的机制,就是 ...
- python网络编程——IO多路复用之epoll
1.内核EPOLL模型讲解 此部分参考http://blog.csdn.net/mango_song/article/details/42643971博文并整理 首先我们来定义流的概念,一个流 ...
- Linux 网络编程八(epoll应用--大并发处理)
//头文件 pub.h #ifndef _vsucess #define _vsucess #ifdef __cplusplus extern "C" { #endif //服务器 ...
- Python网络编程(4)——异步编程select & epoll
在SocketServer模块的学习中,我们了解了多线程和多进程简单Server的实现,使用多线程.多进程技术的服务端为每一个新的client连接创建一个新的进/线程,当client数量较多时,这种技 ...
- python 网络编程 IO多路复用之epoll
python网络编程——IO多路复用之epoll 1.内核EPOLL模型讲解 此部分参考http://blog.csdn.net/mango_song/article/details/4264 ...
随机推荐
- 启动U盘制作-小白保姆式超详细刷机教程
疑难解答加微信机器人,给它发:进群,会拉你进入八米交流群 机器人微信号:bamibot 简洁版教程访问:https://bbs.8miyun.cn 一.准备工作 需要用到的工具: 1.一台Window ...
- MySQL - [02] 常用SQL
题记部分 一.连接MySQL服务器 1.常规连接方式 # 连接本地mysql服务器 mysql -u 用户名 -p # 连接到指定mysql服务器,回车执行该命令之后需要输入密码 mysql -h 主 ...
- clickhouse--表引擎
表引擎 表引擎(即表的类型)决定了: 1)数据的存储方式和位置,写到哪里以及从哪里读取数据 2)支持哪些查询以及如何支持. 3)并发数据访问. 4)索引的使用(如果存在). 5)是否可以执行多线程请求 ...
- DeepSeek 不太稳定?那就搭建自己的 DeepSeek 服务
概述 DeepSeek-R1 发布 DeepSeek 在 2025 年给我们送来一份惊喜,1 月 20 号正式发布第一代推理大模型 DeepSeek-R1.这个模型在数学推理.代码生成和复杂问题解决等 ...
- API方式开发AI应用的三点总结
1. 编程式prompt 让 AI 具备类似程序的运行逻辑.把大模型当CLR使用.与传统的角色扮演提示prompt相比,此方式所需的tokens数量更少,且输出结果的准确性更高 .示例如下: 2. 语 ...
- 使用Node.js打造交互式脚手架,简化模板下载与项目创建
在上一篇文章中,我们探讨了如何构建一个通用的脚手架框架.今天,我们将在此基础上进一步扩展脚手架的功能,赋予它下载项目模板的能力. 通常情况下,我们可以将项目模板发布到 npm 上,或者在公司内部利用私 ...
- 少样本学习实战:Few-Shot Prompt设计
让AI用最少样本学会"举一反三" 想象一下,你要教一个外星人认识地球上的动物.如果只给它看三张哈士奇的照片,它可能会认为所有四条腿的动物都叫"哈士奇".这就是A ...
- 【Python&Hypermesh】ABAQUS导入网格,并在Part内保留SET
在Hypermesh定义好set,划分好网格以后,可以导出为INP.然后在ABAQUS导入inp,就可以得到网格.但是这样倒进来的网格一般有两个问题: 网格全在一个部件里,原来定义好的Set会出现在装 ...
- 如何避免VMware平台ESXi主机CPU使用率的“坑”?
https://mp.weixin.qq.com/s?__biz=MjM5NTk0MTM1Mw==&mid=2650636818&idx=1&sn=c43f3a3146092f ...
- Next.js中间件权限绕过漏洞分析(CVE-2025-29927)
本文代码版本为next.js-15.2.2 本篇文章首发在先知社区:https://xz.aliyun.com/news/17403 一.漏洞概述 CVE-2025-29927是Next.js框架中存 ...