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

Linux高级IO之poll与epoll

文章目录

    • poll
      • 使用
    • epoll
      • 系统调用
    • epoll的工作原理
      • 红黑树
      • 队列
    • epoll的工作模式
      • 水平触发
      • 边缘触发
    • Reactor设计模式
      • 工作原理
      • epoll Reactor设计模式的简单示例

poll和epoll都是多路转接的调用,但是epoll实在过于优秀了,一般也都是用epoll的,除此之外还着使用时还蕴含着Reactor设计模式的思想

poll

poll几乎是解决了select的痛点问题的,就像c和c with class一样

使用

poll的函数原型和数据结构长这样

#include <poll.h>

int poll(struct pollfd *fd, nfds_t nfds, int tiemout);

struct pollfd{
    int fd;			// 文件描述符
    short events;	// 时间类型
    short revents;	// 实际发生的时间
};
  • fds:只想一个pollfd结构体数组的指针,每一个结构体都在监视一个文件描述符
  • nfds:文件描述符的数量
  • timeout:与select相同,只是直接使用int作为类型,单位是毫秒

epoll

epoll是为了处理大量句柄而做了改进的poll,在实际中运用的最多的也是这个

系统调用

这个系统调用是用于创建一个epoll模型的

这个系统调用是用于设置监听的文件描述符和事件

最后一个epoll_event是这样的

struct epoll_event{
    uint32_t events;
    epoll_data_t data;
};

events表示事件发生时要做的操作,也就是需要监听的事件

data则是监听对应的文件描述符

events也是一个位图,使用宏来表示事件

  • EPOLLIN:表示对应的文件描述符可以读
  • EPOLLOUT:表示对应的文件描述符可以写
  • EPOLLRI:表示对应的文件描述符有紧急数据可读
  • EPOLLRR:表示文件描述符出错
  • EPOLLHUP:表示文件描述符被挂断
  • EPOLLET:表示设为边缘出发模式
  • EPOLLONESHOT:只监听一次事件,如果需要继续监听,需要重新加入EPOLL队列中

epoll_wait是用于等待是否就绪

下面是一个简单的epoll示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#define MAX_EVENTS 10
#define TIMEOUT 5000  // 5秒

int main() {
    int epfd = epoll_create1(0);
    if (epfd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }

    // 假设我们要监听标准输入(fd = 0)
    struct epoll_event ev;
    ev.events = EPOLLIN;  // 监听读事件
    ev.data.fd = STDIN_FILENO;

    if (epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev) == -1) {
        perror("epoll_ctl: stdin");
        close(epfd);
        exit(EXIT_FAILURE);
    }

    struct epoll_event events[MAX_EVENTS];
    int nfds;

    while (1) {
        nfds = epoll_wait(epfd, events, MAX_EVENTS, TIMEOUT);
        if (nfds == -1) {
            if (errno == EINTR)
                continue;  // 被信号中断,重新调用
            perror("epoll_wait");
            break;
        }

        if (nfds == 0) {
            printf("等待超时,没有事件发生。\n");
            continue;
        }

        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == STDIN_FILENO) {
                if (events[i].events & EPOLLIN) {
                    char buffer[1024];
                    ssize_t count = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
                    if (count == -1) {
                        perror("read");
                    } else if (count == 0) {
                        printf("标准输入关闭。\n");
                        close(epfd);
                        exit(EXIT_SUCCESS);
                    } else {
                        buffer[count] = '\0';
                        printf("读取到数据: %s", buffer);
                    }
                }
            }
            // 这里可以处理其他文件描述符的事件
        }
    }

    close(epfd);
    return 0;
}

epoll的工作原理

epoll的内核中使用了两个数据结构来管理文件描述符,分别是一个红黑树,一个队列

红黑树

红黑树主要用于存放所有正在监视的文件描述符,当我们使用epoll_ctl设置关心的事件,就是在这个红黑树上进行增删改

队列

队列存放的是就绪事件的文件描述符,也就是epoll_wait所等待的就绪的文件描述符

用户使用的门槛其实降低了很多,只需要设置监视,然后获取结果,不需要对fd和event进行管理

每一个epoll对象都有一个独立的eventpoll结构体,用来存放通过ctl向epoll对象添加的事件

事件会放在红黑树,因此插入的时间是O(lg n)

添加到epoll到事件会与设备建立回调关系,当事件发生,调用这个回调方法

这个回调方法会将发生的事件放在rdlist双联白哦

而每一个事件都对应着一个epitem结构体

里面是这样的

struct epitem{
    struct rb_node rbn; 		// 红黑树节点
    struct list_head rdllink; 	// 双向链表节点
    struct epoll_filefd ffd; 	// 事件句柄信息
    struct eventpoll *ep;		// 只想其所属的eventpoll对象
    struct epoll_event event;	// 期待发生的事件类型
};

当epoll_wait检查事件是否发生时,只需要查看epitem的rdlist是否为空就绪,这个事件复杂度就只有O(1)

epoll的工作模式

epoll有两种工作模式

一种是水平触发LT,另一种是边缘触发ET

水平触发

水平触发就可以理解为阻塞的触发,如果事件发生了,那就会一直进行等待并且通知

epoll的默认工作模式就是水平触发,当epoll检测到事件就绪时,可以不立即进行处理,或者仅处理一部分,一直到缓冲区的所有数据都被处理完,才不会立即返回,支持阻塞和非阻塞

但是这样做的代价很高,因为不能处理返回其他事情

边缘触发

边缘触发就是类似于非阻塞的情况,事件发生时,只通知一次,爱拿不拿,不保证数据依然还在,你只有一次处理机会

ET的性能会比LT高很多,而Nginx默认采用的就是ET模式

但是ET只支持非阻塞

Reactor设计模式

reactor是一种用于处理事件的设计模式,核心思想就是将事件和处理分开

用一个事件的循环来监控多个IO事件,当事件发生时,reactor会调用相应的处理器(回调函数)来处理这些事件

工作原理

一般就是分成四个逻辑

  1. 注册事件,将事件源和处理器注册到事件循环中
  2. 等待事件,事件循环持续监控事件源,等待事件发生
  3. 分发事件,事件发生,事件循环调用处理器
  4. 处理事件,处理器执行具体到业务逻辑

应用场景,一般就是高并发服务器(HTTP服务器),图形用户界面,网络通信

epoll Reactor设计模式的简单示例

#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#include <unordered_map>

const int MAX_EVENTS = 10;

// 事件处理器基类
class EventHandler {
public:
    virtual void handleEvent(uint32_t events) = 0;
};

// Reactor类
class Reactor {
public:
    Reactor() {
        epoll_fd = epoll_create1(0);
        if (epoll_fd == -1) {
            std::cerr << "Failed to create epoll file descriptor" << std::endl;
            exit(EXIT_FAILURE);
        }
    }

    ~Reactor() {
        close(epoll_fd);
    }

    // 注册事件处理器
    void registerHandler(int fd, EventHandler* handler, uint32_t events) {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = fd;

        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
            std::cerr << "Failed to add file descriptor to epoll" << std::endl;
            exit(EXIT_FAILURE);
        }

        handlers[fd] = handler;
    }

    // 运行事件循环
    void run() {
        struct epoll_event events[MAX_EVENTS];
        while (true) {
            int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
            if (n == -1) {
                std::cerr << "epoll_wait failed" << std::endl;
                exit(EXIT_FAILURE);
            }

            for (int i = 0; i < n; ++i) {
                int fd = events[i].data.fd;
                uint32_t event_types = events[i].events;
                if (handlers.count(fd)) {
                    handlers[fd]->handleEvent(event_types);
                }
            }
        }
    }

private:
    int epoll_fd;
    std::unordered_map<int, EventHandler*> handlers;
};

// 自定义事件处理器
class MyEventHandler : public EventHandler {
public:
    void handleEvent(uint32_t events) override {
        if (events & EPOLLIN) {
            char buffer[1024];
            ssize_t count = read(STDIN_FILENO, buffer, sizeof(buffer));
            if (count == -1) {
                std::cerr << "Read error" << std::endl;
            } else if (count == 0) {
                std::cout << "EOF" << std::endl;
            } else {
                std::cout << "Read: " << std::string(buffer, count) << std::endl;
            }
        }
    }
};

int main() {
    Reactor reactor;
    MyEventHandler handler;

    // 将标准输入(STDIN_FILENO)注册到epoll中,监听读事件(EPOLLIN)
    reactor.registerHandler(STDIN_FILENO, &handler, EPOLLIN);

    // 开始事件循环
    reactor.run();

    return 0;
}

http://www.kler.cn/news/328495.html

相关文章:

  • 基于Springboot+微信小程序 的高校社团管理小程序(含源码+数据库+lw)
  • TypeScript 算法手册【插入排序】
  • 搜维尔科技:SenseGlove DK1触觉反馈手套,远程操作机器人任务,保证你工作时的安全
  • js无法获取执行的线程号(Thread ID)
  • 【Golang】关于Go语言中的包
  • 超分服务的分量保存
  • Gateway和VirtualService
  • 代码随想录算法训练营day44
  • PostgreSQL 数据库语法学习:深入理解 `JOIN` 操作
  • 【AI基础】pytorch lightning 基础学习
  • 【JavaEE初阶】深入解析死锁的产生和避免以及内存不可见问题
  • 药品识别与分类系统源码分享
  • 【Transformer】长距离依赖
  • 微信小程序中的 `<block>` 元素:高效渲染与结构清晰的利器
  • 初识C语言(五)
  • 鸿蒙开发(NEXT/API 12)【硬件(传感器开发)】传感器服务
  • Unity 2D RPG Kit 学习笔记
  • 滚雪球学Oracle[8.1讲]:高级主题与未来趋势
  • vite 快速入门指南
  • Flask+微信小程序实现Login+Profile
  • python-ds:Python 中的数据结构库(适用于面试的数据结构和算法合集)
  • 眼镜识别数据集类别和数量已经在文档中说明,训练集和验证集共2200,g是眼镜,ng是没有眼镜。
  • 可视化图表与源代码显示配置项及页面的动态调整功能分析
  • 9、论文阅读:无监督的感知驱动深水下图像增强
  • Arduino UNO R3自学笔记6 之 Arduino引脚(IO)功能介绍
  • 电笔有用吗
  • 【PostgreSQL 】入门篇——支持的各种数据类型介绍,包括整数、浮点数、字符串、日期、JSON、数组等
  • 2024双十一有什么值得买?分享五款优质好物提高幸福感!
  • GPT对话知识库——bootloader是什么?ymodel协议是什么?
  • `git restore` 和 `git checkout` 用于丢弃工作区的改动, `git switch` 和 `git checkout` 用来切换分支