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

【Linux网络】I/O多路转接技术 - epoll

📢博客主页:https://blog.csdn.net/2301_779549673
📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨

在这里插入图片描述

在这里插入图片描述

文章目录

  • 🏳️‍🌈一、epoll 概念
  • 🏳️‍🌈二、epoll 相关调用
    • 2.1 epoll_create 创建句柄
    • 2.2 epoll_ctl 执行事件
    • 2.3 epoll_wait 阻塞调用
  • 🏳️‍🌈三、epoll 工作原理
    • 3.1 数据到达主机
    • 3.2 epoll 工作原理
    • 3.3 模拟演示
      • 3.3.1 EpollServer 类
        • 3.3.1.1 基本结构
        • 3.3.1.2 构造函数、析构函数
        • 3.3.1.3 初始化函数 InitServer()
        • 3.3.1.4 循环函数 Loop()
        • 3.3.1.5 处理就绪事件函数 HandlerEvent()
        • 3.3.1.6 监听套接字处理函数 HandlerNewConnection()
        • 3.3.1.7 普通套接字处理函数 HandlerIO()
      • 3.3.2 EpollServer.cpp 主函数
      • 3.3.3 运行结果
  • 🏳️‍🌈四、epoll 的优点(与 select 的缺点对应)
  • 🏳️‍🌈五、epoll 工作方式
  • 🏳️‍🌈六、对比 LT 和 ET
  • 🏳️‍🌈七、理解 ET 模式和 非阻塞文件描述符
  • 🏳️‍🌈八、ET 模式的典型陷阱​
  • 🏳️‍🌈九、epoll 的使用场景
  • 👥总结


🏳️‍🌈一、epoll 概念

  • 按照 man 手册的说法: 是为处理大批量句柄而作了改进的 poll.
  • 它是在 2.5.44 内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
  • 几乎具备了之前所说的一切优点,被公认为 Linux2.6 下性能最好的多路 I/O 就绪通知方法.

作用:等待多个fd,等待fd上面的新事件就绪,通知程序员,事件已经就绪,可以进行IO拷贝了!
定位:只负责进行等,等就绪事件派发!

🏳️‍🌈二、epoll 相关调用

2.1 epoll_create 创建句柄

int epoll_create(int size);     // 创建一个 epoll 句柄

参数

  • size:在早期版本的 Linux 中指定了监听的文件描述符数量上限,自从 linux2.6.8 之后,这个参数被忽略因为 epoll 自动调整以处理最大数量的文件描述符因此,传递任何大于 0 的值都是可以的,通常使用 1 作为默认值。

返回值

  • 成功时,epoll_create() 返回一个非负的文件描述符,该描述分用于后续的 epoll 操作
  • 失败时,返回 -1 并设置 errno 以指示错误类型

注意

  • 用完之后,必须调用 close 关闭

2.2 epoll_ctl 执行事件

 // 注册、注销、修改 epoll 事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  

参数:

  • epfd: 由 epoll_create() 或 epoll_createl() 返回的 epoll 文件描述符
  • op:要执行的操作,可以是以下三个值之一
    • EPOLL_CTL_ADD:向 epoll 实例中添加一个新的文件描述符
    • EPOLL_CTL_DEL:从 epoll 实例中删除一个文件描述符
    • EPOLL_CTL_MOD修改一个已经存在于 epoll 实例中的文件描述符的监听事件
  • fd要添加、删除或修改的文件描述符
  • event执行一个 epoll_event 结构体的指针该结构体指定了要监听的事件类型和数据。对于 EPOLL_CTL_DEL 操作,这个参数可以是 nullptr,因为删除操作不需要直到事件类型

返回值:

  • 成功时,epoll_ctl() 返回 0
  • 失败时,返回 -1 并设置 errno 以指示错误类型

epoll_event 结构体

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 */
} __EPOLL_PACKED;

events 可以是以下几个宏的集合:

  • EPOLLIN : 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);
  • EPOLLOUT : 表示对应的文件描述符可以写;
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
  • EPOLLERR : 表示对应的文件描述符发生错误;
  • EPOLLHUP : 表示对应的文件描述符被挂断;
  • EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
  • EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里.

在这里插入图片描述
在这里插入图片描述

2.3 epoll_wait 阻塞调用

// 阻塞调用线程,直到有至少一个文件描述符上的事件变得就绪,或者超时发生
int epopll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

参数

  • epfd: 由 epoll_create() 或 epoll_createl() 返回的 epoll 文件描述符
  • events:指向一个 epoll_event 结构体数组的指针,该数组用于存储返回的事件信息
  • maxevents:events 数组的大小,即最多可以返回的事件数量
  • timeout等待事件的超时事件(毫秒)
    • 如果为 -1,则 无限期阻塞,直到有事件发生
    • 如果为 0 ,则 立即返回

返回值

  • 成功时,epoll_wait() 返回就绪事件的数量,这些事件被村粗在 events 数组中
  • 失败时,返回 -1 并设置 errno 以指示错误类型

🏳️‍🌈三、epoll 工作原理

3.1 数据到达主机

  • 数据到达主机的原理 涉及多个层级和协议的协同工作。通过逐层封装、转发和接收处理,数据能够准确地从源主机传输到目的主机
  • 硬件中断时由硬件设备发出的信号,用于通知计算机系统发生了某个事件,需要系统进行处理。这些硬件设备可以是磁盘、网卡、键盘、时钟等

在这里插入图片描述

3.2 epoll 工作原理

在这里插入图片描述

  • 当某一进程调用 epoll create 方法时,Linux 内核会创建一个 eventpoll 结构体,这个结构体中有两个成员epoll 的使用方式密切相关.
struct eventpoll{// 红黑树的根节点,这棵树中存储着所有添加到 epoll 中的需要监控的时间struct rb_root rbr;// 双链表中则存放着将要通过 epoll_wait 返回给用户的满足条件的事件struct list_head rdlist;
};
  • 每一个 epoll 对象都有一个独立的 eventpoll 结构体用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件
  • 这些事件就会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效地是识别出来(红黑树地插入时间效率是 lgn,其中 n 为树地高度)
  • >所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系</font,也就是说,当相应地事件发生时会调用这个回调方法
  • 这个回调方法在内核中叫 ep_poll_callback,它将发生的事件添加到 rdlist 双链表中
  • epoll 中,对于每一个事件,都会建立一个 epitem 结构体
struct epitem{struct rb_node rbn;         // 红黑树节点struct list_head rdlink;    // 双向链表节点struct epoll_filefd ffd;    // 事件句柄信息struct eventpoll* ep;       // 指向其所属地 eventpoll 对象struct epoll_event event;   // 其弟啊发生地事件类型
};
  • 当调用 epoll_wait 检查是否有事件发生时,只需要检查 eventpoll 对象中 rdlist 双链表中是否有 epitem 元素即可
  • 如果 rdlist 不会空,则把发生的事件复制到用户态,同时将事件数量返回给用户,这个操作的时间复杂度是 O(1)

总结一下, epoll 的使用过程就是三部曲

  1. 调用 epoll_create 创建一个 epoll 句柄
  2. 调用 epoll_ctl ,将要监控的文件描述符进行注册
  3. 调用 epoll_wait 等待文件描述符就绪

在这里插入图片描述

3.3 模拟演示

3.3.1 EpollServer 类

3.3.1.1 基本结构

EpollServer 类的成员变量包括 端口号,listen套接字,epfd(epoll_create()函数的返回值),接收事件的数组
成员函数与 PollServer 类基本一致!

class EpollServer{const static int gsize = 128;const static int gnum = 1024;public:EpollServer(uint16_t port);void InitServer();void Loop();~EpollServer();private:uint16_t _port;SockPtr _lostensock;int _epfd;struct epoll_event _events[gnum];
};
3.3.1.2 构造函数、析构函数

构造函数 初始化端口号,根据端口号创建监听套接字对象 以及 创建epoll句柄
析构函数 关闭 epfd(合法的前提下) 和 listensock

EpollServer(uint16_t port): _port(port), _listensock(std::unique_ptr<TcpSocket>()) {_listensock->BuildListenSocket(_port);_epfd = ::epoll_create(gsize);if (_epfd < 0) {LOG(LogLevel::FATAL) << "epoll_create error: " << errno;exit(1);}LOG(LogLevel::INFO) << "epoll_create success. epfd: " << _epfd;
}
~EpollServer() {if (_epfd >= 0)::close(_epfd);_listensock->Close();
}
3.3.1.3 初始化函数 InitServer()

InitServer() 函数使用系统调用(epoll_ctl),将 listensock 添加到 epoll

void InitServer() {struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = _listensock->Sockfd();// 将监听套接字添加到 epoll 中int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->Sockfd(), &ev);if (n < 0) {LOG(LogLevel::FATAL) << "epoll_ctl errno\n";exit(2);}LOG(LogLevel::INFO) << "epoll_ctl success, listensockfd: "<< _listensock->Sockfd();
}
3.3.1.4 循环函数 Loop()

Loop() 函数调用 epoll_wait 系统调用进行等待,根据返回值执行对应的操作:

  1. 返回值为0 :打印超时日志,并退出循环
  2. 返回值为-1 :打印出错日志,并退出循环
  3. 返回值大于0 :打印事件发生日志,并处理合法事件
void Loop() {int timeout = 1000;while (true) {int n = ::epoll_wait(_epfd, _events, gnum, timeout);switch (n) {case 0:LOG(LogLevel::INFO) << "epoll timeout";break;case -1:LOG(LogLevel::ERROR) << "epoll_wait error: " << errno;break;default:LOG(LogLevel::INFO) << "haved event happened, nums : " << n;HandlerEvent(n);break;}}
}
3.3.1.5 处理就绪事件函数 HandlerEvent()

HandlerEvent() 函数处理就绪事件,主要分为以下两步:

  1. 从事件数组中读取 合法fdevents
  2. 判断读事件是否就绪
    • listensock 就绪
    • normal sockfd 就绪
void HandlerEvent(int nums) {for (int i = 0; i < nums; ++i) {// 1. 从事件数组中读取合法的 fd 和 eventsint fd = _events[i].data.fd;uint32_t events = _events[i].events;LOG(LogLevel::INFO) << "上面有事件就绪了,具体事件是:" << fd << " "<< std::to_string(events).c_str();// 2. 判断是监听套接字就绪还是其他套接字就绪if (events & EPOLLIN) {if (fd == _listensock->Sockfd())HandlerNewConnection();elseHandlerIO(fd);}}
}
3.3.1.6 监听套接字处理函数 HandlerNewConnection()
  1. 获取链接
  2. 获取链接成功将新的 fd读事件 添加到 epoll(使用epoll_ctl系统调用)
void HandlerNewConnection() {InetAddr client;int sockfd = _listensock->Accepter(&client);if (sockfd < 0) {LOG(LogLevel::ERROR) << "accept error: " << errno;return;}LOG(LogLevel::INFO) << "get a new connection from "<< client.AddrStr().c_str() << ", sockfd : " << sockfd;struct epoll_event ev;ev.data.fd = sockfd;ev.events = EPOLLIN;::epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);LOG(LogLevel::INFO) << "epoll_ctl success, sockfd: " << sockfd;
}
3.3.1.7 普通套接字处理函数 HandlerIO()

HandlerIO() 函数处理普通fd情况,直接读取文件描述符中的数据,根据recv()函数的返回值做出不一样的决策,主要分为以下三种情况:

  1. 返回值大于0,读取文件描述符中的数据,并使用 send() 函数做出回应!
  2. 返回值等于0,读到文件结尾,打印客户端退出的日志,关闭文件描述符,将 epfd 从 epoll 中移除并关闭 fd
  3. 返回值小于0,读取文件错误,打印接受失败的日志,然后同上!
void HandlerIO(int fd) {char buffer[1024];ssize_t n = ::recv(fd, buffer, sizeof(buffer) - 1, 0);if (n > 0) {buffer[n] = 0;std::cout << buffer;std::string content = "<html><body><h1>hello linux</h1></body></html>";std::string echo_str = "HTTP/1.0 200 OK\r\n";echo_str += "Content-Type: text/html\r\n";echo_str +="Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";echo_str += content;::send(fd, echo_str.c_str(), echo_str.size(), 0);} else if (n == 0) {LOG(LogLevel::DEBUG) << "client " << fd << " closed";// 1. 从 epoll 中移除,从 epoll 中移除 fd,这个必须是健康 合法的// fd,否则会移除出错::epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);// 2. 关闭fd::close(fd);} else {LOG(LogLevel::ERROR) << "recv error: " << errno;// 1. 从 epoll 中移除,从 epoll 中移除 fd,这个必须是健康 合法的// fd,否则会移除出错::epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);// 2. 关闭fd::close(fd);}
}

3.3.2 EpollServer.cpp 主函数

根据主函数反向实现类和成员函数

#include "EpollServer.hpp"int main(int argc, char* argv[]){if(argc != 2){std::cerr << "Usage: " << argv[0] << " locak-port" << std::endl; }uint16_t port = std::stoi(argv[1]);std::unique_ptr<EpollServer> svr = std::make_unique<EpollServer>(port);svr->InitServer();svr->Loop();return 0;
}

3.3.3 运行结果

在这里插入图片描述

🏳️‍🌈四、epoll 的优点(与 select 的缺点对应)

  • 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
  • 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而 select/poll 都是每次循环都要进行拷贝)
  • 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中 , epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度 O(1). 即使文件描述符数目很多, 效率也不会受到影响.
  • 没有数量限制: 文件描述符数目无上限.

网上有些博客说, epoll 中使用了内存映射机制

  • 内存映射机制: 内核直接将就绪队列通过 mmap 的方式映射到用户态. 避免了拷贝内存这样的额外性能开销.
  • 这种说法是不准确的. 我们定义的 struct epoll_event 是我们在用户空间中分配好的内存. 势必还是需要将内核的数据拷贝到这个用户空间的内存中的.

🏳️‍🌈五、epoll 工作方式

你妈喊你吃饭的例子

你正在吃鸡, 眼看进入了决赛圈, 你妈饭做好了, 喊你吃饭的时候有两种方式:

  1. 如果你妈喊你一次, 你没动, 那么你妈会继续喊你第二次, 第三次…(亲妈,水平触发)
  2. 如果你妈喊你一次, 你没动, 你妈就不管你了(后妈, 边缘触发)

epoll 有 2 种工作方式 - 水平触发(LT)边缘触发(ET)

假如有这样一个例子:

  • 我们已经把一个 tcp socket 添加到 epoll 描述符
  • 这个时候 socket 的另一端被写入了 2KB 的数据
  • 调用 epoll_wait,并且它会返回. 说明它已经准备好读取操作
  • 然后调用 read, 只读取了 1KB 的数据
  • 继续调用 epoll_wait…

水平触发 Level Triggered 工作模式

  • epoll 默认状态下就是 LT 工作模式.
  • epoll 检测到 socket 上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
  • 如上面的例子, 由于只读了 1K 数据, 缓冲区中还剩 1K 数据, 在第二次调用epoll_wait 时, epoll_wait 仍然会立刻返回并通知 socket 读事件就绪.
  • 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
  • 支持 阻塞读写非阻塞读写

边缘触发 Edge Triggered 工作模式

  • 如果我们在第 1 步将 socket 添加到 epoll 描述符的时候使用了 EPOLLET 标志, epoll 进入 ET 工作模式.
  • epoll 检测到 socket 上事件就绪时, 必须立刻处理.
  • 如上面的例子, 虽然只读了 1K 的数据, 缓冲区还剩 1K 的数据, 在第二次调用epoll_wait 的时候, epoll_wait 不会再返回了.
  • 也就是说, ET 模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
  • ET 的性能比 LT 性能更高( epoll_wait 返回的次数少了很多). Nginx 默认采用ET 模式使用 epoll.
  • 只支持 非阻塞 的读写

selectpoll 其实也是工作在 LT 模式下.
epoll 既可以支持 LT, 也可以支持 ET.

🏳️‍🌈六、对比 LT 和 ET

  1. LTepoll 的默认行为.
  2. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿 一次响应就绪过程中就把所有的数据都处理完
    • 相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.
  3. 另一方面, ET 的代码复杂程度更高了.
  4. ET 的通知效率更高
  5. ET可能给对方一个更大的接受窗口增加IO效率 – 即ET的IO效率更高

🏳️‍🌈七、理解 ET 模式和 非阻塞文件描述符

使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是 “工程实践” 上的要求,逻辑如下:

在这里插入图片描述
有一个问题:LT也可以设置非阻塞,LT我也可以循环读取完毕啊,为什么要有ET呢?

最简单的理解,ET是被强制要求非阻塞的,但是LT可以是阻塞也可以是非阻塞!

假设这样的场景: 服务器接收到一个 10k 的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第二个 10k 请求

在这里插入图片描述

如果服务端写的代码是阻塞式的 read, 并且一次只 read 1k 数据的话(read 不能保证一次就把所有的数据都读出来, 参考 man 手册的说明, 可能被信号打断), 剩下的 9k 数据就会待在缓冲区中.

在这里插入图片描述

此时由于 epoll 是 ET 模式, 并不会认为文件描述符读就绪. epoll_wait 就不会再次返回. 剩下的 9k 数据会一直在缓冲区中. 直到下一次客户端再给服务器写数据.epoll_wait 才能返回

但是问题来了

  • 服务器只读到 1k 个数据, 要 10k 读完才会给客户端返回响应数据.
  • 客户端要读到服务器的响应, 才会发送下一个请求
  • 客户端发送了下一个请求, epoll_wait 才会返回, 才能去读缓冲区中剩余的数据.

在这里插入图片描述

所以, 为了解决上述问题(阻塞 read 不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区, 保证一定能把完整的请求都读出来.

而如果是 LT 没这个问题. 只要缓冲区中的数据没读完, 就能够让 epoll_wait 返回文件描述符读就绪.

🏳️‍🌈八、ET 模式的典型陷阱​

  1. ​事件丢失​
    • 若未在 ET 模式下一次性处理完所有数据,剩余数据不会再次触发 EPOLLIN,导致数据滞留。
    • 解决:必须循环读取直到 EAGAIN。
  2. 饥饿问题​
    • 若某个 fd 持续有数据到达,可能独占事件循环,导致其他 fd 得不到处理。
    • ​解决: 设置处理上限(如每次最多读取 10 次),或使用 EPOLLONESHOT 标志。
  3. 错误处理遗漏​
    • 未处理 EPOLLERR 或 EPOLLHUP,导致程序无法感知连接异常。
    • ​解决: 始终优先检查错误事件:

🏳️‍🌈九、epoll 的使用场景

epoll 的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll 的性能可能适得其反.

  • 对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用 epoll.

例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网 APP 的入口服务器,这样的服务器就很适合 epoll.

如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用 epoll 就并不合适. 具体要根据需求和场景特点来决定使用哪种 IO 模型.


👥总结

本篇博文对 【Linux网络】I/O多路转接技术 - epoll 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

相关文章:

  • epoll函数
  • 【Shell 脚本编程】详细指南:第四章 - 循环结构(for、while、until) 深度解析
  • 60常用控件_QSpinBox的使用
  • 排序算法——冒泡排序
  • C语言学习之动态内存的管理
  • 交我算使用保姆教程:在计算中心利用singularity容器训练深度学习模型
  • caffe适配cudnn9.6.0(ai修改代码踩坑)
  • synchronized与Lock深度对比
  • 随机森林实战:从原理到垃圾邮件分类
  • Windows下Python3脚本传到Linux下./example.py执行失败
  • AdaBoost算法详解:原理、实现与应用指南
  • 极简GIT使用
  • 补4月30日
  • 常见电源的解释说明
  • C#泛型集合深度解析(九):掌握System.Collections.Generic的核心精髓
  • RTOS接口-Semaphores
  • ADG网络故障恢复演练
  • 实现了一个基于寄存器操作STM32F103C8t6的工程, 并实现对PA1,PA2接LED正极的点灯操作
  • 如何提升个人的稳定性?
  • 蓝桥杯比赛
  • 燕子矶:物流网络中的闪亮节点|劳动者的书信②
  • 美“群聊泄密门”始作俑者沃尔兹将离职
  • 经济日报社论:书写新征程上奋斗华章
  • 山西太原小区爆炸事故已造成17人受伤
  • 结婚这件事,年轻人到底怎么想的?
  • 陕西省副省长窦敬丽已任宁夏回族自治区党委常委、统战部部长