当前位置: 首页 > news >正文

i/o复用函数的使用——epoll

i/o复用函数的使用——epoll

目录

一、epoll

1.1 概念

1.2函数原型

1.2.1 创建

1.2.2. 控制(增删改)

1.2.3. 等待就绪

1.2.4 epoll_event 结构体

二、i/o方法并发处理客户端

2.1 基于 Epoll 水平触发下的TCP服务器实现

2.2 cli.c客户端

2.3 基于 Epoll 边缘触发下的TCP服务器实现

三、 epoll工作方式

​编辑3.1 水平触发LT

3.2 边缘触发ET

3.3 ET和LT对比


一、epoll

1.1 概念

epoll是linux特有的。epoll(Event Poll)是Linux内核提供的I/O多路复用接口,它用于监视多个文件描述符(FD),以确定它们是否处于就绪状态(即可以进行非阻塞的读或写操作)。epollselectpoll系统调用的替代品,提供了更高的效率和可扩展性,特别是在处理大量文件描述符时。

1.2函数原型

1.2.1 创建

 epoll_create(int size)

  • 功能:创建一个新的 epoll 实例,用于管理一组文件描述符的事件。

  • 参数

    • size:建议要监视的文件描述符数量,这个参数是可选的,大多数情况下可以设置为 0,让内核自行决定实例大小。

  • 返回值:成功时返回新创建的 epoll 实例的文件描述符;失败时返回 -1,并设置 errno 以指示错误原因。

1.2.2. 控制(增删改)

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

  • 功能:控制 epoll 实例,注册、修改或删除文件描述符的事件。

  • 参数

    • epfd:由 epoll_create 返回的 epoll 实例的文件描述符。

    • op:操作类型,可以是:

      • EPOLL_CTL_ADD:添加一个新的文件描述符到 epoll 实例。

      • EPOLL_CTL_MOD:修改已注册的文件描述符的事件。

      • EPOLL_CTL_DEL:从 epoll 实例中删除文件描述符。

    • fd:要注册、修改或删除的文件描述符。

    • event:指向 epoll_event 结构的指针,指定文件描述符的事件类型和用户数据。

  • 返回值:成功时返回 0;失败时返回 -1,并设置 errno 以指示错误原因。

1.2.3. 等待就绪

epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

  • 功能:等待 epoll 实例中的一个或多个事件变得就绪。

  • 参数

    • epfd:由 epoll_create 返回的 epoll 实例的文件描述符。

    • events:指向 epoll_event 结构数组的指针,用于存储就绪事件的信息。

    • maxeventsevents 数组中的最大就绪事件数。

    • timeout:等待事件的最长时间(毫秒)。如果设置为 -1,则无限期等待;如果设置为 0,则立即返回。

  • 返回值成功时返回就绪事件的数量;失败时返回 -1,并设置 errno 以指示错误原因。=0超时

1.2.4 epoll_event 结构体

struct epoll_event event;
event.events = EPOLLIN | EPOLLOUT; // 我们感兴趣的是读和写事件
event.data.fd = fd; // 将文件描述符存储在 data.fd 中
  • events:指定文件描述符的事件类型,可以是以下事件的组合:

    • EPOLLIN:可读事件。

    • EPOLLOUT:可写事件。

    • EPOLLERR:发生错误。

    • EPOLLHUP:挂起事件。

    • EPOLLRDHUP:远程对端关闭连接。

  • data:用户定义的数据,通常用于识别与文件描述符关联的特定对象或状态。

二、i/o方法并发处理客户端

2.1 基于 Epoll 水平触发下的TCP服务器实现

epoll.c服务器

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>#define MAXFD  10 // 定义最大的文件描述符数量为10// 初始化套接字的函数声明
int socket_init();// 将文件描述符添加到epoll监听队列中
void epoll_add(int epfd, int fd) {struct epoll_event ev;ev.events = EPOLLIN; // 设置监听读事件ev.data.fd = fd; // 设置文件描述符if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1) { // 添加到epoll队列printf("epoll add err\n");}return;
}// 从epoll监听队列中删除文件描述符
void epoll_del(int epfd, int fd) {if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1) { // 删除文件描述符printf("epoll del err\n");}
}// 接受客户端连接
void accept_client(int epfd, int sockfd) {int c = accept(sockfd, NULL, NULL); // 接受连接if (c < 0) {return; // 如果接受失败,返回}printf("accept c=%d\n", c); // 打印客户端文件描述符epoll_add(epfd, c); // 将客户端文件描述符添加到epoll监听队列中
}// 接收客户端数据
void recv_data(int epfd, int c) {char buff[128] = {0};int n = recv(c, buff, 127, 0); // 接收数据if (n <= 0) {epoll_del(epfd, c); // 从epoll中删除文件描述符close(c); // 关闭文件描述符printf("client close\n"); // 打印关闭信息return;}printf("read(%d)=%s\n", c, buff); // 打印接收到的数据send(c, "ok", 2, 0); // 发送确认信息
}int main() {int sockfd = socket_init(); // 初始化套接字if (-1 == sockfd) {exit(1); // 如果初始化失败,退出程序}int epfd = epoll_create(MAXFD); // 创建epoll实例if (-1 == epfd) {exit(1); // 如果创建失败,退出程序}epoll_add(epfd, sockfd); // 将监听套接字添加到epoll监听队列struct epoll_event evs[MAXFD]; // 定义epoll事件数组while (1) {int n = epoll_wait(epfd, evs, MAXFD, 5000); // 等待事件,超时时间为5秒if (-1 == n) {printf("epoll wait err\n"); // 如果等待失败,打印错误信息continue;}else if (0 == n) {printf("time out\n"); // 如果超时,打印超时信息continue;}else { // 如果有事件就绪for (int i = 0; i < n; i++) {int fd = evs[i].data.fd; // 获取文件描述符if (fd == -1) {continue; // 如果文件描述符无效,跳过}if (evs[i].events & EPOLLIN) { // 如果有读事件if (fd == sockfd) {accept_client(epfd, sockfd); // 接受新的客户端连接}else {recv_data(epfd, fd); // 接收数据}}//if (evs[i].events & EPOLLOUT) // 如果有写事件(这里没有处理)}}}
}int socket_init() {int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字if (-1 == sockfd) {return -1; // 如果创建失败,返回-1}struct sockaddr_in saddr; // 定义 sockaddr_in 结构体memset(&saddr, 0, sizeof(saddr)); // 清空 sockaddr_in 结构体saddr.sin_family = AF_INET; // 设置为IPv4地址族saddr.sin_port = htons(6000); // 设置端口号为6000saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置IP地址为127.0.0.1int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)); // 绑定地址到套接字if (-1 == res) {printf("bind err\n"); // 如果绑定失败,打印错误信息return -1;}res = listen(sockfd, 5); // 开始监听,允许5个连接排队if (-1 == res) {return -1; // 如果监听失败,返回-1}return sockfd; // 返回监听套接字的文件描述符
}

这段代码实现了一个使用 `epoll` 进行 I/O 多路复用的 TCP 服务器。下面是代码的主要组成部分及其功能的详细解释:

1. 包含头文件
- 这些头文件包含了程序中使用的各种函数和数据结构的定义,例如网络编程、内存操作、字符串处理等。

2. 宏定义
#define MAXFD 10
- `MAXFD` 定义了最大的文件描述符数量,用于限制 `epoll` 实例中可以同时监听的文件描述符的最大数量。

3. 套接字初始化函数 `socket_init`
int socket_init() {
    // 创建套接字、绑定地址和监听的代码
}
- 这个函数负责创建一个新的 TCP 套接字,绑定到本地地址 `127.0.0.1` 的 `6000` 端口,并开始监听连接请求。

4. `epoll_add` 和 `epoll_del` 函数
void epoll_add(int epfd, int fd) {
    // 将文件描述符添加到epoll实例中
}

void epoll_del(int epfd, int fd) {
    // 从epoll实例中删除文件描述符
}
- `epoll_add` 函数用于将一个新的文件描述符添加到 `epoll` 实例中,并指定我们感兴趣的事件类型(如读事件)。
- `epoll_del` 函数用于从 `epoll` 实例中删除一个文件描述符,通常在关闭套接字时调用。

5. 接受客户端连接 `accept_client`
void accept_client(int epfd, int sockfd) {
    // 接受新的客户端连接并将其添加到epoll实例中
}
 这个函数处理新的客户端连接请求,接受连接后将新的套接字添加到 `epoll` 实例中以监听其事件。

6. 接收数据 `recv_data`
void recv_data(int epfd, int c) {
    // 从客户端接收数据,并发送确认响应
}
 这个函数从客户端套接字接收数据,打印接收到的数据,并发送一个简单的响应回客户端。

7. 主函数 `main`
int main() {
    // 初始化套接字,创建epoll实例,添加监听套接字到epoll实例中
    // 进入事件循环,等待和处理事件
}
- 主函数初始化服务器套接字和 `epoll` 实例,然后进入一个无限循环,使用 `epoll_wait` 等待事件的发生,并根据事件类型调用相应的处理函数。

 8. `epoll_create`, `epoll_ctl`, 和 `epoll_wait` 函数
- `epoll_create` 用于创建一个新的 `epoll` 实例。
- `epoll_ctl` 用于向 `epoll` 实例添加、修改或删除文件描述符。
- `epoll_wait` 用于等待 `epoll` 实例中的一个或多个文件描述符就绪,并返回就绪的文件描述符列表。

这段代码通过 `epoll` 机制有效地管理多个客户端连接和数据交换,适用于需要处理大量并发连接的场景。

2.2 cli.c客户端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>int main()
{int sockfd = socket(AF_INET,SOCK_STREAM,0);if( sockfd == -1 ){exit(1);}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("127.0.0.1");int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));if ( res == -1 ){printf("connect err\n");exit(1);}while( 1 ){printf("input\n");char buff[128] = {0};fgets(buff,128,stdin);if( strncmp(buff,"end",3) == 0 ){break;}send(sockfd,buff,strlen(buff)-1,0);memset(buff,0,128);recv(sockfd,buff,127,0);printf("read:%s\n",buff);}close(sockfd);exit(0);
}

2.3 基于 Epoll 边缘触发下的TCP服务器实现

epoll_et.c 代码

#include <stdio.h>      // 标准输入输出头文件,用于 I/O 操作,如 printf
#include <stdlib.h>     // 标准库头文件,提供通用工具函数,如 exit
#include <unistd.h>     // POSIX 操作系统API,提供对POSIX操作系统的访问,如 read, write
#include <string.h>      // 字符串处理头文件,提供字符串操作函数,如 memset
#include <sys/socket.h>   // 套接字头文件,提供伯克利网络接口,如 socket, bind
#include <arpa/inet.h>    // 提供网络字节处理函数,如 inet_addr
#include <netinet/in.h>  // 提供网络字节处理函数,如 htons
#include <sys/epoll.h>    // epoll 头文件,提供 epoll 系统调用,用于 I/O 多路复用
#include <errno.h>      // 错误号头文件,提供错误号常量,如 EAGAIN, EWOULDBLOCK
#include <fcntl.h>      // 文件控制头文件,提供对文件描述符的操作函数,如 fcntl#define MAXFD  10   // 定义最大的文件描述符数量为10// 初始化套接字的函数声明
int socket_init();// 设置非阻塞模式的函数
void setnonblock(int fd) {int fl = fcntl(fd, F_GETFL);  // 获取文件描述符的当前标志int sfl = fl | O_NONBLOCK;  // 设置非阻塞标志if (fcntl(fd, F_SETFL, sfl) == -1)  // 修改文件描述符的标志{printf("fcntl err\n");  // 错误处理}
}// 将文件描述符添加到epoll实例的函数
void epoll_add(int epfd, int fd) {struct epoll_event ev;  // epoll_event 结构体,用于定义感兴趣的事件ev.events = EPOLLIN | EPOLLET;  // 设置感兴趣的事件为读事件和边缘触发ev.data.fd = fd;  // 设置文件描述符setnonblock(fd);  // 设置非阻塞模式if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)  // 添加文件描述符到epoll实例{printf("epoll add err\n");  // 错误处理}return;
}// 从epoll实例中删除文件描述符的函数
void epoll_del(int epfd, int fd) {if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1)  // 删除文件描述符{printf("epoll del err\n");}
}// 接受客户端连接请求的函数
void accept_client(int epfd, int sockfd) {int c = accept(sockfd, NULL, NULL);  // 接受新的客户端连接if (c < 0){return;  // 错误处理}printf("accept c=%d\n", c);  // 打印客户端文件描述符epoll_add(epfd, c);  // 将客户端文件描述符添加到epoll实例中
}// 接收客户端数据的函数
void recv_data(int epfd, int c) {while (1)  // 循环接收数据,直到发生错误或连接关闭{char buff[128] = {0};int n = recv(c, buff, 127, 0);  // 接收数据if (-1 == n)  // 接收错误{if (errno == EAGAIN || errno == EWOULDBLOCK)  // 非阻塞模式下,操作会返回-1,错误号EAGAIN或EWOULDBLOCK{send(c, "ok", 2, 0);  // 发送确认信息}else{printf("recv err\n");  // 错误处理}break;  // 跳出循环}else if (0 == n)  // 连接关闭{epoll_del(epfd, c);  // 从epoll实例中删除文件描述符close(c);  // 关闭文件描述符printf("client close\n");  // 打印关闭信息break;  // 跳出循环}else{printf("read(%d)=%s\n", c, buff);  // 打印接收到的数据}}
}// 主函数
int main() {int sockfd = socket_init();  // 初始化套接字if (-1 == sockfd){exit(1);  // 初始化失败,退出程序}int epfd = epoll_create(MAXFD);  // 创建epoll实例if (-1 == epfd){exit(1);  // 创建失败,退出程序}epoll_add(epfd, sockfd);  // 将监听套接字添加到epoll实例中struct epoll_event evs[MAXFD];  // 定义epoll_event数组,用于存储就绪事件while (1)  // 无限循环,等待事件{int n = epoll_wait(epfd, evs, MAXFD, 5000);  // 等待就绪事件,超时时间为5秒printf("---\n");if (-1 == n)  // 错误处理{printf("epoll wait err\n");continue;}else if (0 == n)  // 超时处理{printf("time out\n");continue;}else  // 有事件就绪{for (int i = 0; i < n; i++)  // 遍历就绪事件{int fd = evs[i].data.fd;  // 获取文件描述符if (fd == -1){continue;  // 文件描述符无效,跳过}if (evs[i].events & EPOLLIN)  // 检查是否有读事件{if (fd == sockfd)  // 如果是监听套接字{accept_client(epfd, sockfd);  // 接受新的客户端连接}else{recv_data(epfd, fd);  // 接收数据}}//if (evs[i].events & EPOLLOUT) // 如果有写事件(这里没有处理)}}}
}// 初始化套接字的函数定义
int socket_init() {int sockfd = socket(AF_INET, SOCK_STREAM, 0);  // 创建TCP套接字if (-1 == sockfd){return -1;  // 创建失败,返回-1}struct sockaddr_in saddr;  // sockaddr_in 结构体,用于存储网络地址信息memset(&saddr, 0, sizeof(saddr));  // 清空结构体saddr.sin_family = AF_INET;  // 设置地址族为IPv4saddr.sin_port = htons(6000);  // 设置端口号为6000saddr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 设置IP地址为127.0.0.1int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));  // 绑定地址到套接字if (-1 == res){printf("bind err\n");  // 绑定失败,打印错误信息return -1;}res = listen(sockfd, 5);  // 开始监听,允许5个连接排队if (-1 == res){return -1;  // 监听失败,返回-1}return sockfd;  // 返回监听套接字的文件描述符
}

这段代码是一个使用 epoll 实现的 TCP 服务器程序,采用边缘触发(ET)模式。它能监听本地的 6000 端口,接收客户端的连接请求,并且可以接收客户端发送的数据。一旦客户端断开连接,程序会相应地进行处理。

两段代码的主要区别在于处理接收数据时的逻辑 

三、 epoll工作方式


3.1 水平触发LT

epoll默认情况下就是LT

当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分
假设只读了1K数据, 缓冲区中还剩1K数据, 在第二次调epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪. 直到缓冲区上所有的数据都被处理完, epoll_wait 不会立刻返回
支持阻塞读写和非阻塞读写

3.2 边缘触发ET

将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式

当epoll检测到socket上事件就绪时, 必须立刻处理 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候,epoll_wait 不会再返回了
ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会。只支持非阻塞的读写


3.3 ET和LT对比

select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET
LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是一次响应就绪过程中就把所有的数据都处理完
ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll
使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞.

http://www.xdnf.cn/news/199135.html

相关文章:

  • jclasslib 与 BinEd 结合的二进制分析技术指南
  • 【计算机系统结构】第四章
  • 利用EMQX实现单片机和PyQt的数据MQTT互联
  • 数据库系统概论|第三章:关系数据库标准语言SQL—课程笔记6
  • 计算机基础—(九道题)
  • 云上玩转DeepSeek系列之六:DeepSeek云端加速版发布,具备超高推理性能
  • AI图片跳舞生成视频,animate X本地部署。
  • 2025系统架构师---论企业集成平台的技术与应用
  • 永磁同步电机控制算法-反馈线性化滑模控制
  • Telephony VoiceMail
  • 数据库基础与核心操作:从概念到实战的全面解析
  • 嵌入式多功能浏览器系统设计详解
  • 使用双端队列deque模拟栈stack
  • 获得ecovadis徽章资格标准是什么?ecovadis评估失败的风险
  • sortablejs + antd-menu 拖拽出重复菜单
  • 【个人理解】MCP server和client二者各自的角色以及发挥的作用
  • 【TS入门笔记4---装饰器】
  • DPanel 一款更适合国人的 Docker 管理工具
  • linux 使用nginx部署vue、react项目
  • 结合大语言模型的机械臂抓取操作学习
  • Python 中支持函数式编程的 operator 与 functools 包
  • 第一节:Linux系统简介
  • Android显示学习笔记本
  • 打造即插即用的企业级云原生平台——KubeSphere 4.1 扩展组件在生产环境的价值全解
  • 解决跨域实现方案
  • 用vite动态导入vue的路由配置
  • 本地部署Qwen-7B实战 vLLM加速推理
  • 网络协议之为什么要分层
  • 论文分享 | 基于区块链和签名的去中心化跨域认证方案
  • 受限字符+环境变量RCE