马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
📢博客主页: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 创建句柄
- int epoll_create(int size); // 创建一个 epoll 句柄
复制代码 参数
- size:在早期版本的 Linux 中指定了监听的文件形貌符数量上限,自从 linux2.6.8 之后,这个参数被忽略 ,由于 epoll 主动调解以处置惩罚最大数量的文件形貌符。因此,通报任何大于 0 的值都是可以的,通常利用 1 作为默认值。
返回值
- 乐成时,epoll_create() 返回一个非负的文件形貌符,该形貌分用于后续的 epoll 操纵
- 失败时,返回 -1 并设置 errno 以指示错误范例
留意
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 的利用过程就是三部曲
- 调用 epoll_create 创建一个 epoll 句柄
- 调用 epoll_ctl ,将要监控
的文件形貌符举行注册
- 调用 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 系统调用举行等候,根据返回值实行对应的操纵:
- 返回值为0 :打印超时日记,并退出循环
- 返回值为-1 :打印出错日记,并退出循环
- 返回值大于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() 函数处置惩罚就绪变乱,紧张分为以下两步:
- 从变乱数组中读取 合法fd 和 events
- 判定读变乱是否就绪
- listensock 就绪
- normal sockfd 就绪
- void HandlerEvent(int nums) {
- for (int i = 0; i < nums; ++i) {
- // 1. 从事件数组中读取合法的 fd 和 events
- int 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();
- else
- HandlerIO(fd);
- }
- }
- }
复制代码 3.3.1.6 监听套接字处置惩罚函数 HandlerNewConnection()
- 获取链接
- 获取链接乐成将新的 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()函数的返回值做出不一样的决策,紧张分为以下三种情况:
- 返回值大于0,读取文件形貌符中的数据,并利用 send() 函数做出回应!
- 返回值等于0,读到文件末了,打印客户端退出的日记,关闭文件形貌符,将 epfd 从 epoll 中移除并关闭 fd
- 返回值小于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 工作方式
你妈喊你用饭的例子
你正在吃鸡, 眼看进入了决赛圈, 你妈饭做好了, 喊你用饭的时间有两种方式:
- 假如你妈喊你一次, 你没动, 那么你妈会继续喊你第二次, 第三次…(亲妈,水平触发)
- 假如你妈喊你一次, 你没动, 你妈就不管你了(后妈, 边沿触发)
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 做了一个较为具体的先容,不知道对你有没有资助呢
以为博主写得还不错的三连支持下吧!会继续努力的~
|