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

[复制链接]
发表于 2025-9-19 01:37:12 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
📢博客主页:https://blog.csdn.net/2301_779549673
📢博客堆栈:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢接待点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢将来很长,值得我们努力奔赴更精美的生存✨
  





🏳️‍🌈一、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 创建句柄

  1. int epoll_create(int size);     // 创建一个 epoll 句柄
复制代码
参数


  • size:在早期版本的 Linux 中指定了监听的文件形貌符数量上限,自从 linux2.6.8 之后,这个参数被忽略 ,由于 epoll 主动调解以处置惩罚最大数量的文件形貌符。因此,通报任何大于 0 的值都是可以的,通常利用 1 作为默认值。
返回值


  • 乐成时,epoll_create() 返回一个非负的文件形貌符,该形貌分用于后续的 epoll 操纵
  • 失败时,返回 -1 并设置 errno 以指示错误范例
留意


  • 用完之后,必须调用 close 关闭
2.2 epoll_ctl 实行变乱

  1. // 注册、注销、修改 epoll 事件
  2. 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 结构体
  1. typedef union epoll_data
  2. {
  3.   void *ptr;
  4.   int fd;
  5.   uint32_t u32;
  6.   uint64_t u64;
  7. } epoll_data_t;
  8. struct epoll_event
  9. {
  10.   uint32_t events;        /* Epoll events */
  11.   epoll_data_t data;        /* User data variable */
  12. } __EPOLL_PACKED;
复制代码
events 可以是以下几个宏的聚集:


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


2.3 epoll_wait 壅闭调用

  1. // 阻塞调用线程,直到有至少一个文件描述符上的事件变得就绪,或者超时发生
  2. 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 的利用方式密切相关.
  1. struct eventpoll{
  2.     // 红黑树的根节点,这棵树中存储着所有添加到 epoll 中的需要监控监控的时间
  3.     struct rb_root rbr;
  4.     // 双链表中则存放着将要通过 epoll_wait 返回给用户的满足条件的事件
  5.     struct list_head rdlist;
  6. };
复制代码


  • 每一个 epoll 对象都有一个独立的 eventpoll 结构体,用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的变乱
  • 这些变乱就会挂载在红黑树中,云云,重复添加的变乱就可以通过红黑树而高效地是辨认出来(红黑树地插入时间服从是 lgn,此中 n 为树地高度)
  • 而>所有添加到 epoll 中的变乱都会与装备(网卡)驱动程序创建回调关系</font,也就是说,当相应地变乱发生时会调用这个回调方法
  • 这个回调方法在内核中叫 ep_poll_callback,它将发生的变乱添加到 rdlist 双链表中
  • 在 epoll 中,对于每一个变乱,都会创建一个 epitem 结构体
  1. struct epitem{
  2.     struct rb_node rbn;         // 红黑树节点
  3.     struct list_head rdlink;    // 双向链表节点
  4.     struct epoll_filefd ffd;    // 事件句柄信息
  5.     struct eventpoll* ep;       // 指向其所属地 eventpoll 对象
  6.     struct epoll_event event;   // 其弟啊发生地事件类型
  7. };
复制代码


  • 当调用 epoll_wait 查抄是否有变乱发生时,只需要查抄 eventpoll 对象中 rdlist 双链表中是否有 epitem 元素即可
  • 假如 rdlist 不会空,则把发生的变乱复制到用户态,同时将变乱数量返回给用户,这个操纵的时间复杂度是 O(1)
总结一下, epoll 的利用过程就是三部曲

  • 调用 epoll_create 创建一个 epoll 句柄
  • 调用 epoll_ctl ,将要监控监控的文件形貌符举行注册
  • 调用 epoll_wait 等候文件形貌符就绪

3.3 模仿演示

3.3.1 EpollServer 类

3.3.1.1 根本结构

   EpollServer 类的成员变量包罗 端标语,listen套接字,epfd(epoll_create()函数的返回值),汲取变乱的数组
成员函数与 PollServer 类根本划一!
  1. class EpollServer{
  2.     const static int gsize = 128;
  3.     const static int gnum = 1024;
  4.     public:
  5.         EpollServer(uint16_t port);
  6.         void InitServer();
  7.         void Loop();
  8.         ~EpollServer();
  9.     private:
  10.         uint16_t _port;
  11.         SockPtr _lostensock;
  12.         int _epfd;
  13.         struct epoll_event _events[gnum];
  14. };
复制代码
3.3.1.2 构造函数、析构函数

   构造函数 初始化端标语,根据端标语创建监听套接字对象 以及 创建epoll句柄
析构函数 关闭 epfd(合法的条件下) 和 listensock
  1. EpollServer(uint16_t port)
  2.     : _port(port), _listensock(std::unique_ptr<TcpSocket>()) {
  3.     _listensock->BuildListenSocket(_port);
  4.     _epfd = ::epoll_create(gsize);
  5.     if (_epfd < 0) {
  6.         LOG(LogLevel::FATAL) << "epoll_create error: " << errno;
  7.         exit(1);
  8.     }
  9.     LOG(LogLevel::INFO) << "epoll_create success. epfd: " << _epfd;
  10. }
  11. ~EpollServer() {
  12.     if (_epfd >= 0)
  13.         ::close(_epfd);
  14.     _listensock->Close();
  15. }
复制代码
3.3.1.3 初始化函数 InitServer()

   InitServer() 函数利用系统调用(epoll_ctl),将 listensock 添加到 epoll
  1. void InitServer() {
  2.     struct epoll_event ev;
  3.     ev.events = EPOLLIN;
  4.     ev.data.fd = _listensock->Sockfd();
  5.     // 将监听套接字添加到 epoll 中
  6.     int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->Sockfd(), &ev);
  7.     if (n < 0) {
  8.         LOG(LogLevel::FATAL) << "epoll_ctl errno\n";
  9.         exit(2);
  10.     }
  11.     LOG(LogLevel::INFO) << "epoll_ctl success, listensockfd: "
  12.                         << _listensock->Sockfd();
  13. }
复制代码
3.3.1.4 循环函数 Loop()

   Loop() 函数调用 epoll_wait 系统调用举行等候,根据返回值实行对应的操纵:
  

  • 返回值为0 :打印超时日记,并退出循环
  • 返回值为-1 :打印出错日记,并退出循环
  • 返回值大于0 :打印变乱发生日记,并处置惩罚合法变乱
  1. void Loop() {
  2.     int timeout = 1000;
  3.     while (true) {
  4.         int n = ::epoll_wait(_epfd, _events, gnum, timeout);
  5.         switch (n) {
  6.         case 0:
  7.             LOG(LogLevel::INFO) << "epoll timeout";
  8.             break;
  9.         case -1:
  10.             LOG(LogLevel::ERROR) << "epoll_wait error: " << errno;
  11.             break;
  12.         default:
  13.             LOG(LogLevel::INFO) << "haved event happened, nums : " << n;
  14.             HandlerEvent(n);
  15.             break;
  16.         }
  17.     }
  18. }
复制代码
3.3.1.5 处置惩罚就绪变乱函数 HandlerEvent()

   HandlerEvent() 函数处置惩罚就绪变乱,紧张分为以下两步:
  

  • 从变乱数组中读取 合法fd 和 events
  • 判定读变乱是否就绪

    • listensock 就绪
    • normal sockfd 就绪

  1. void HandlerEvent(int nums) {
  2.     for (int i = 0; i < nums; ++i) {
  3.         // 1. 从事件数组中读取合法的 fd 和 events
  4.         int fd = _events[i].data.fd;
  5.         uint32_t events = _events[i].events;
  6.         LOG(LogLevel::INFO) << "上面有事件就绪了,具体事件是:" << fd << " "
  7.                             << std::to_string(events).c_str();
  8.         // 2. 判断是监听套接字就绪还是其他套接字就绪
  9.         if (events & EPOLLIN) {
  10.             if (fd == _listensock->Sockfd())
  11.                 HandlerNewConnection();
  12.             else
  13.                 HandlerIO(fd);
  14.         }
  15.     }
  16. }
复制代码
3.3.1.6 监听套接字处置惩罚函数 HandlerNewConnection()

   

  • 获取链接
  • 获取链接乐成将新的 fd 和 读变乱 添加到 epoll 中(利用epoll_ctl系统调用)
  1. void HandlerNewConnection() {
  2.     InetAddr client;
  3.     int sockfd = _listensock->Accepter(&client);
  4.     if (sockfd < 0) {
  5.         LOG(LogLevel::ERROR) << "accept error: " << errno;
  6.         return;
  7.     }
  8.     LOG(LogLevel::INFO) << "get a new connection from "
  9.                         << client.AddrStr().c_str() << ", sockfd : " << sockfd;
  10.     struct epoll_event ev;
  11.     ev.data.fd = sockfd;
  12.     ev.events = EPOLLIN;
  13.     ::epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
  14.     LOG(LogLevel::INFO) << "epoll_ctl success, sockfd: " << sockfd;
  15. }
复制代码
3.3.1.7 平凡套接字处置惩罚函数 HandlerIO()

   HandlerIO() 函数处置惩罚平凡fd情况,直接读取文件形貌符中的数据,根据recv()函数的返回值做出不一样的决策,紧张分为以下三种情况:
  

  • 返回值大于0,读取文件形貌符中的数据,并利用 send() 函数做出回应!
  • 返回值等于0,读到文件末了,打印客户端退出的日记,关闭文件形貌符,将 epfd 从 epoll 中移除并关闭 fd
  • 返回值小于0,读取文件错误,打印接受失败的日记,然后同上!
  1. void HandlerIO(int fd) {
  2.     char buffer[1024];
  3.     ssize_t n = ::recv(fd, buffer, sizeof(buffer) - 1, 0);
  4.     if (n > 0) {
  5.         buffer[n] = 0;
  6.         std::cout << buffer;
  7.         std::string content = "<html><body><h1>hello linux</h1></body></html>";
  8.         std::string echo_str = "HTTP/1.0 200 OK\r\n";
  9.         echo_str += "Content-Type: text/html\r\n";
  10.         echo_str +=
  11.             "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";
  12.         echo_str += content;
  13.         ::send(fd, echo_str.c_str(), echo_str.size(), 0);
  14.     } else if (n == 0) {
  15.         LOG(LogLevel::DEBUG) << "client " << fd << " closed";
  16.         // 1. 从 epoll 中移除,从 epoll 中移除 fd,这个必须是健康 合法的
  17.         // fd,否则会移除出错
  18.         ::epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);
  19.         // 2. 关闭fd
  20.         ::close(fd);
  21.     } else {
  22.         LOG(LogLevel::ERROR) << "recv error: " << errno;
  23.         // 1. 从 epoll 中移除,从 epoll 中移除 fd,这个必须是健康 合法的
  24.         // fd,否则会移除出错
  25.         ::epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);
  26.         // 2. 关闭fd
  27.         ::close(fd);
  28.     }
  29. }
复制代码
3.3.2 EpollServer.cpp 主函数

   根据主函数反向实现类和成员函数
  1. #include "EpollServer.hpp"
  2. int main(int argc, char* argv[]){
  3.     if(argc != 2){
  4.         std::cerr << "Usage: " << argv[0] << " locak-port" << std::endl;
  5.     }
  6.     uint16_t port = std::stoi(argv[1]);
  7.     std::unique_ptr<EpollServer> svr = std::make_unique<EpollServer>(port);
  8.     svr->InitServer();
  9.     svr->Loop();
  10.     return 0;
  11. }
复制代码
3.3.3 运行结果


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



  • 接口利用方便: 固然拆分成了三个函数, 但是反而利用起来更方便高效. 不需要每次循环都设置关注的文件形貌符, 也做到了输入输出参数分离开
  • 数据拷贝轻量: 只在符合的时间调用 EPOLL_CTL_ADD 将文件形貌符结构拷贝到内核中, 这个操纵并不频繁(而 select/poll 都是每次循环都要举行拷贝)
  • 变乱回调机制: 制止利用遍历, 而是利用回调函数的方式, 迁就绪的文件形貌符结构参加到就绪队列中 , epoll_wait 返回直接访问就绪队列就知道哪些文件形貌符就绪. 这个操纵时间复杂度 O(1). 纵然文件形貌符数量许多, 服从也不会受到影响.
  • 没有数量限制: 文件形貌符数量无上限.
网上有些博客说, epoll 中利用了内存映射机制


  • 内存映射机制: 内核直接迁就绪队列通过 mmap 的方式映射到用户态. 制止了拷贝内存如许的额外性能开销.
  • 这种说法是禁绝确的. 我们界说的 struct epoll_event 是我们在用户空间中分配好的内存. 势必照旧需要将内核的数据拷贝到这个用户空间的内存中的.
🏳️‍🌈五、epoll 工作方式

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

  • 假如你妈喊你一次, 你没动, 那么你妈会继续喊你第二次, 第三次…(亲妈,水平触发)
  • 假如你妈喊你一次, 你没动, 你妈就不管你了(后妈, 边沿触发)
  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.
  • 只支持 非壅闭 的读写
select 和 poll 着实也是工作在 LT 模式下.
epoll 既可以支持 LT, 也可以支持 ET.
🏳️‍🌈六、对比 LT 和 ET


  • LT 是 epoll 的默认举动.
  • 利用 ET 能够镌汰 epoll 触发的次数. 但是代价就是逼迫着程序猿 一次相应就绪过程中就把所有的数据都处置惩罚完

    • 相当于一个文件形貌符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下假如也能做到每次就绪的文件形貌符都立刻处置惩罚, 不让这个就绪被重复提示的话, 着实性能也是一样的.

  • 另一方面, ET 的代码复杂水平更高了.
  • ET 的关照服从更高
  • 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 模式的典型陷阱​


  • ​变乱丢失​

    • 若未在 ET 模式下一次性处置惩罚完所有数据,剩余数据不会再次触发 EPOLLIN,导致数据滞留。
    • 办理:必须循环读取直到 EAGAIN。

  • ​饥饿题目​

    • 若某个 fd 连续有数据到达,大概独占变乱循环,导致其他 fd 得不到处置惩罚。
    • ​办理: 设置处置惩罚上限(如每次最多读取 10 次),或利用 EPOLLONESHOT 标志。

  • ​错误处置惩罚遗漏​

    • 未处置惩罚 EPOLLERR 或 EPOLLHUP,导致程序无法感知毗连非常。
    • ​办理: 始终优先查抄错误变乱:

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

epoll 的高性能, 是有肯定的特定场景的. 假如场景选择的不适宜, epoll 的性能大概适得其反.


  • 对于多毗连, 且多毗连中只有一部门毗连比力生动时, 比力得当利用 epoll.
比方, 典型的一个需要处置惩罚上万个客户端的服务器, 比方各种互联网 APP 的入口服务器,如许的服务器就很得当 epoll.
假如只是系统内部, 服务器和服务器之间举行通讯, 只有少数的几个毗连, 这种情况下用 epoll 就并不符合. 具体要根据需求和场景特点来决定利用哪种 IO 模子.

👥总结

本篇博文对 【Linux网络】I/O多路转接技能 - epoll 做了一个较为具体的先容,不知道对你有没有资助呢
以为博主写得还不错的三连支持下吧!会继续努力的~
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表