IO epoll
概述
epoll 全称 eventpoll,是 linux 内核实现IO多路转接/复用(IO multiplexing)的一个实现。
IO多路转接的意思是在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,
然后对其的进行读写操作。epoll是select和poll的升级版,相较于这两个前辈,epoll改进了工作方式,因此它更加高效。
- 对于待检测集合select和poll是基于线性方式处理的,epoll是基于红黑树来管理待检测集合的。
- select和poll每次都会线性扫描整个待检测集合,集合越大速度越慢,epoll使用的是回调机制,效率高,处理效率也不会随着检测集合的变大而下降
- select和poll工作过程中存在内核/用户空间数据的频繁拷贝问题,在epoll中内核和用户区使用的是共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
- 程序猿需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,通过epoll可以直接得到已就绪的文件描述符集合,无需再次检测
- 使用epoll没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制
操作函数
#include <sys/epoll.h>
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
select/poll低效的原因之一是将“添加/维护待检测任务”和“阻塞进程/线程”两个步骤合二为一。
每次调用select都需要这两步操作,然而大多数应用场景中,需要监视的socket个数相对固定,并不需要每次都修改。
epoll将这两个操作分开,先用epoll_ctl()维护等待队列,再调用epoll_wait()阻塞进程(解耦)。
通过下图的对比显而易见,epoll的效率得到了提升。
create
epoll_create()函数的作用是创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。
int epoll_create(int size);
- 函数参数 size:在Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数值就可以了。
- 函数返回值:
失败:返回-1
成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的epoll实例了
ctl
epoll_ctl()函数的作用是管理红黑树实例上的节点,可以进行添加、删除、修改操作。
// 联合体, 多个变量共用同一块内存
typedef union epoll_data {
void *ptr;
int fd; // 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数参数:
- epfd:epoll_create() 函数的返回值,通过这个参数找到epoll实例
- op:这是一个枚举值,控制通过该函数执行什么操作
- EPOLL_CTL_ADD:往epoll模型中添加新的节点
- EPOLL_CTL_MOD:修改epoll模型中已经存在的节点
- EPOLL_CTL_DEL:删除epoll模型中的指定的节点
- fd:文件描述符,即要添加/修改/删除的文件描述符
- event:epoll事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件
- events:委托epoll检测的事件
- EPOLLIN:读事件, 接收数据, 检测读缓冲区,如果有数据该文件描述符就绪
- EPOLLOUT:写事件, 发送数据, 检测写缓冲区,如果可写该文件描述符就绪
- EPOLLERR:异常事件
- data:用户数据变量,这是一个联合体类型,通常情况下使用里边的fd成员,用于存储待检测的文件描述符的值,在调用epoll_wait()函数的时候这个值会被传出。
- events:委托epoll检测的事件
函数返回值:
- 失败:返回-1
- 成功:返回0
wait
函数的作用是检测创建的epoll实例中有没有就绪的文件描述符。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
函数参数:
- epfd:epoll_create() 函数的返回值, 通过这个参数找到epoll实例
- events:传出参数, 这是一个结构体数组的地址, 里边存储了已就绪的文件描述符的信息
- maxevents:修饰第二个参数, 结构体数组的容量(元素个数)
- timeout:如果检测的epoll实例中没有已就绪的文件描述符,该函数阻塞的时长, 单位ms 毫秒
- 0:函数不阻塞,不管epoll实例中有没有就绪的文件描述符,函数被调用后都直接返回
- 大于0:如果epoll实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
- -1:函数一直阻塞,直到epoll实例中有已就绪的文件描述符之后才解除阻塞
函数返回值:
- 成功:
- 等于0:函数是阻塞被强制解除了, 没有检测到满足条件的文件描述符
- 大于0:检测到的已就绪的文件描述符的总个数
- 失败:返回-1
示例代码
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
int main()
{
printf("%s 向你问好!\n", "IOepoll");
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
return -1;
}
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (sockaddr*)&saddr, sizeof(saddr));
if (ret == -1)
{
perror("bind error");
return -1;
}
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen error");
return -1;
}
//创建epoll示例
int epfd = epoll_create(1);
if (epfd == -1)
{
perror("epoll_create error");
return -1;
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(evs[0]);
while (1)
{
int num = epoll_wait(epfd, evs, size, -1);
printf("num = %d \n", num);
for (int i = 0; i < num; i++)
{
int fd = evs[i].data.fd;
if (fd == lfd)
{
int cfd = accept(fd, NULL, NULL);
//struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = cfd;
//这一步会做拷贝,所以说ev用一个也是可以的
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
}
else//通信的描述符
{
char buf[1024];
int len = recv(fd, buf, sizeof(buf), 0);
if (len == -1)
{
perror("recv error");
return -1;
}
else if (len == 0)
{
printf("client disconnect...\n");
//先删除再做操作
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
}
printf("read buf = %s \n", buf);
for (int i = 0; i < len; i++)
{
buf[i] = toupper(buf[i]);
}
printf("after buf = %s \n", buf);
ret = send(fd, buf, strlen(buf) + 1, 0);
if (ret == -1)
{
perror("send error");
return -1;
}
}
}
}
close(lfd);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!