epoll
epoll的解释:当文件描述符的内核缓冲区非空的时候,发出可读信号进行通知,当写缓冲区不满的时候,发出可写信号通知的机制。
- 创建文件描述符
#include<sys/epoll.h>
int epoll_create(int size)
// size 表示要创建这个内核事件表的大小
- 操作内核事件表
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event))
fd:要操作的文件描述符
op:操作方法
event:指定事件
struct epoll_event{
__uint32_t events; // epoll事件
epoll_data_t data; // 用户数据
}
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
void *ptr:一个指针,可用于存储任何用户指定的数据类型的地址。
int fd:一个整数,通常用于存储文件描述符。指定事件所从属的目标文件描述符
uint32_t u32:一个32位无符号整数,用于存储用户指定的32位整数。
uint64_t u64:一个64位无符号整数,用于存储用户指定的64位整数
- 在超时事件内等待一组文件描述符上的事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
// -------------------------------------
epfd:epoll 实例的文件描述符。
events:用于存储发生事件的 struct epoll_event 结构体数组。
maxevents:events 数组的大小,即最多返回多少个事件。
timeout:等待事件的超时时间,单位是毫秒。传递 -1 表示永久等待,传递 0 表示立即返回,传递正整数表示等待指定的毫秒数。
epoll_wait如果监听到事件,则将事件表中的所有就绪事件从内核事件表(由epfd指定)复制到由events指向的数组中
epoll对文件描述符的操作方式
ET:边缘触发(注册EPOLLET事件时)
而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait 调用将不再向应用程序通知这一事件。可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数, 因此效率要比LT模式高。
- ET是一次事件只会触发一次
- socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
- socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
- 仅在状态变化时触发事件
LT:电平触发(默认工作模式)
对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait 还会再次向应用程序通告此事件,直到该事件被处理。
- LT是一次事件会触发多次
- socket接收缓冲区不为空 有数据可读 读事件一直触发
- socket发送缓冲区不满 可以继续写入数据 写事件一直触发
EPOLLONESHONT事件
一个线程读取一个socket上的数据,读取后处理但是socket上又有新的数据,然后会有另一个线程来读,就会造成两个线程读取一个socket
对注册该事件的fd,操作系统最多触发其注册的一个可读,可写或者异常事件,只触发一次。一旦socket被某个线程处理完毕,该线程就会立刻重置socket上的该事件,则下次可读时又会触发该事件。
代码案例
服务端功能 : 将接收的客户端的数据逆转发送给所有连接的客户端,采用ET模式,及对fd注册EPOLLET事件,不注册的话默认采用LT,同样的在注册事件是可以注册EPOLLONESHOT事件。
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cassert>
#include<cstring>
#include<algorithm>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>
constexpr int MAX_EVENTS = 10;
int main(){
// 1. 创建服务器Socket
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 设置端口复用
int opt = 1;
if(setsockopt(server_socket, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)) == -1){
perror("socket opt failed");
exit(EXIT_FAILURE);
}
// 3. 设置服务器端口号和ip
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
// 4. 绑定ip和端口号
if(bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1){
perror("binding failed");
exit(EXIT_FAILURE);
}
// 5. 监听
if(listen(server_socket, 5) == -1){
perror("listening failed");
exit(EXIT_FAILURE);
}
// 6. epoll创建内核事件表
int epoll_fd = epoll_create1(0);
if(epoll_fd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
std::vector<int> clientSockets; // 存放所有客户端的socket
// 7. 添加服务器Socket到epoll对象中, 并设置为边缘触发方式(只有epoll独有)
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
event.data.fd = server_socket;
// 8. 将该事件注册到该内核事件表
if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket, &event) == -1){
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
while(true){
struct epoll_event events[MAX_EVENTS];
int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // 监听是否有事件
if (num_events == -1) {
perror("epoll_wait failed");
exit(EXIT_FAILURE);
}
// 遍历到达的事件(和poll,select的差别)
for(int i = 0; i < num_events; i++){
// 新的客户端连接
if(events[i].data.fd == server_socket){
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_len);
if (client_socket == -1) {
perror("Accept failed");
exit(EXIT_FAILURE);
}
std::cout << "New connection accepted from [" << inet_ntoa(client_addr.sin_addr)<<"]"<< std::endl;
// 将新的客户端Socket添加到 epoll对象(并设置ET模式)
event.events = EPOLLIN | EPOLLET;
event.data.fd = client_socket;
if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_socket, &event) == -1){
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
clientSockets.push_back(client_socket);
}
else{
// 读取客户端数据
char buffer[2048];
ssize_t byteRead = read(events[i].data.fd, buffer, sizeof(buffer)-1);
if(byteRead <= 0){
// 客户端断开连接
if(epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, nullptr) == -1){
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
close(events[i].data.fd);
}
else{
buffer[byteRead] = '\0';
std::cout<<"receive from ["<<events[i].data.fd<<" ] data is : "<<buffer<<std::endl;
// 发送数据给所有的客户端
std::string s(buffer);
std::reverse(s.begin(), s.end());
for(const auto &client : clientSockets){
std::cout<<"["<<client<<"] send data"<<std::endl;
send(client, s.data(), s.size(), 0);
}
}
}
}
}
}
客户端
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include<cassert>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main(){
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd != -1);
struct sockaddr_in saddr;
memset(&saddr,0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port=htons(8080);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd,(struct sockaddr*)&saddr, sizeof(saddr));
assert(res != -1);
while(true){
std::cout<<"input you want to send message ...."<<std::endl;
std::string msg;
std::getline(std::cin, msg);
if(msg == "end") break;
send(sockfd, msg.data(), msg.size(), 0); // recv的msg最大就是send的msg的大小
recv(sockfd, msg.data(), msg.size(), 0);
std::cout<<"receive data is : "<<msg<<std::endl;
}
close(sockfd);
}
三种IO多路复用区别
这 3 组 系 统 调 用 都 能 同 时 监 听 多 个 文 件 描 述 符 ,它 们 将 等 待 由 timeout 参 数 指 定 的 超 时 时 间 , 直 到 一 个 或 者 多 个 文 件 描 述 符 上 有 事 件 发 生 时 返 回 , 返 回 值 是 就 绪 的 文 件 描 述 符 的 数 量 , 返 回 0 表 示 没 有 事 件 发 生 。
我 们 从 事 件 集 、 最 大 支 持 文 件 描 述 符 数 、 工 作 模 式 和 具 体 实 现 等 四 个 方 面 进 一 步 比 较 它 们 的 异 同 , 以 明 确 在 实 际 应 用 中 应 该 选 择 使 用 哪 个 ( 或 哪 些 )。
事件集
这 3 组 函 数 都 通 过 某 种 结 构 体 变 量 来 告 诉 内 核 监 听 哪 些 文 件 描 述 符 上 的 哪 些 事 件 , 并 使 用 该 结 构 体 类 型 的 参 数 来 获 取 内 核 处 理 的 结 果 ,
select 的 参 数 类 型 fd_set 没 有 将 文 件 描 述 符 和 事 件 绑 定 , 它 仅 仅 是 一 个 文 件 描 述 符 集 合, 因 此 select 需 要 提 供 3 个 这 种 类 型 的 参 数 来 分 别 传 人 和 输 出 可 读 、 可 写 及 异 常 等 事 件 。 这 一 方 面 使 得 select 不 能 处 理 更 多 类 型 的 事 件 , 另 一 方 面 由 于 内 核 对 fd_set 集 合 的 在 线 修 改 , 应 用 程 序 下 次 调 用 select 前 不 得 不 重 置 这 3 个 fd 集 合 。
poll 的 参 数 类 型 pollfd 则 多 少 “ 聪 明 ” 一 些 。 它 把 文 件 描 述 符 和 事 件 都 定 义 其 中 , 任 何 事 件 都 被 统 一 处 理, 从 而 使 得 编 程 接 口 简 洁 得 多 。 并 且 内 核 每 次 修 改 的 是 pollfd 结 构 体 的 revents 成 员 , 而 events 成 员 保 持 不 变, 因 此 下 次 调 用 poll时 应 用 程 序 无 须 重 置 pollfd 类 型 的 事 件 集 参 数 ,由 于 每 次 和 poll 调 用 都 返 回 整 个 用 户 注 册 的 事 件 集 合 ( 其 中 包 括 就 绪 的 和 未 就 绪 的 ) , 所 以 应 用 程 序 索 引 就 绪 文 件 描 述 符 的 时 间 复 杂 度 为 0 (n)。
epoll 则 采 用 与 和 poll 完 全 不 同 的 方 式 来 管 理 用 户 注 册 的 事 件 。 它 在 内 核 中 维 护 一 个 事 件 表 , 并 提 供 了 一 个 独 立 的 系 统 调 用 epoll_ctl 来 控 制 往 其 中 添 加 、 删 除 、 修 改 事 件 。 这 样 , 每 次 epoll_ wait 调 用 都 直 接 从 该 内 核 事 件 表 中 取 得 用 户 注 册 的 事 件 , 而 无 须 反 复 从 用 户 空 间 读 入 这 些 事 件 ,epoll_wait 系 统 调 用 的 events 参 数 仅 用 来 返 回 就 绪 的 事 件 , 这 使 得 应 用 程 序 索 引 就 绪 文 件 描 述 符 的 时 间 复 杂 度 达 到 O(1) 。
最 大 支 持 文 件 描 述 符 数
poll 和 epoll_wait 分 别 用 nfds 和 maxevents 参 数 指 定 最 多 监 听 多 少 个 文 件 描 述 符 和 事 件 。 这 两 个 数 值 都 能 达 到 系 统 允 许 打 开 的 最 大 文 件 描 述 符 数 目 。 即 65535 (cat/proc/sys/fs/file- max)。而 select 允 许 监 听 的 最 大 文 件 描 述 符 数 量 通 常 有 限 制(一般是1024) 。 虽 然 用 户 可 以 修 改 这 个 限 制 , 但 这 可 能 导 致 不 可 预 期 的 后 果 。
工 作 模 式
select和poll都 只 能 工 作 在 相 对 低 效 的 LT 模 式 , 而 epoll则 可 以 工 作 在 ET 高 效 模 式 。 并 且 epoll 还 支 持 EPOLLONESHOT 事 件 。 该 事 件 能 进 一 步 减 少 可 读 、 可 写 和 异 常 等 事 件 被 触 发 的 次 数
具 体 实 现
从 实 现 原 理 上 来 说 , select 和 poll 采 用 的 都 是 轮 询 的 方 式 , 即 每 次 调 用 都 要 扫 描 整 个 注 册 文 件 描 述 符 集 合 , 并 将 其 中 就 绪 的 文 件 描 述 符 返 回 给 用 户 程 序 , 因 此 它 们 检 测 就 绪 事 件 的 算 法 的 时 间 复 杂 度 是 O (n)
epoll_wait 则 不 同 , 它 采 用 的 是 回 调 的 方 式 。 内 核 检 测 到 就 绪 的 文 件 描 述 符 时 , 将 触 发 回 调 数 ,回 调 函 数 就 将 该 文 件 描 述 符 上 对 应 的 事 件 插 人 内 核 就 绪 事 件 队 列 。 内 核 最 后 在 适 当 的 时 机 将 该 就 绪 事 件 队 列 中 的 内 容 拷 贝 到 用 户 空 间 。 因 此 epoll_wait 无 须 轮 询 整 个 文 件 描 述 符 集 合 来 检 测 哪 些 事 件 已 经 就 绪 , 其 算 法 时 间 复 杂 度 是 O(1)。 但 是 , 当 活 动 连 接 比 较 多 的 时 候 , epoll_watt 的 效 率 未 必 比 select 和 poll 高 , 因 为 此 时 回 调 函 数 被 触 发 得 过 于 頻 繁 。 所 以 epoll_wait 适 用 于 连 接 数 量 多 , 但 活 动 连 接 较 少 的 情 况 。