• Epoll:让IO多路复用变得有趣


    Epoll 是 Linux 系统中高性能的 I/O 事件通知机制,通过它可以实现对大量文件描述符的高效监控,适用于构建高并发的网络服务器。

    epoll介绍

    在Linux中,epoll是一种高效的I/O多路复用机制,用于监视多个文件描述符(通常是套接字)的I/O事件。它相对于传统的 select 和 poll 具有更高的效率和扩展性,因此在处理大规模并发连接时被广泛应用。

    主要特点

    • 事件驱动: epoll 采用事件驱动的方式工作,当文件描述符就绪时会触发事件通知,而不需要像 select 和 poll 那样需要遍历所有文件描述符进行轮询。
    • 高效的数据结构: epoll 内部使用了红黑树和哈希表等数据结构,能够快速地插入、删除和查找文件描述符,因此在增加大量文件描述符时性能下降较少。
    • 支持大量文件描述符: epoll 能够支持大量的文件描述符,且随着文件描述符数量的增加,其性能不会线性下降,这使得它非常适合于高并发的网络编程。
    • 水平触发和边缘触发: epoll 提供了两种工作模式,即水平触发(Level-Triggered,简称 LT)和边缘触发(Edge-Triggered,简称 ET)。在 LT 模式下,只要文件描述符就绪就会通知,而在 ET 模式下,只有当文件描述符的状态变化时才会通知。

    epoll与poll、select区别

    效率和扩展性

    • select 和 poll: 在调用时需要将文件描述符集合从用户态拷贝到内核态,造成了性能损耗。而且随着文件描述符数量的增加,它们的性能会线性下降,对于大规模并发连接的情况表现不佳。
    • epoll: 使用事件驱动的方式工作,能够高效地监视大量的文件描述符而不会因文件描述符数量增加导致性能下降,因此在处理大规模并发连接时具有更高的效率和扩展性。

    文件描述符数量限制

    • select: 通常限制为 1024 个文件描述符(32位机器默认为1024,64位默认为2048),因为使用固定大小的位图来表示文件描述符状态,当文件描述符数量较大时会带来额外的开销。
    • poll: 文件描述符数量的限制取决于系统的配置。
    • epoll: 没有明显的文件描述符数量限制,能够支持非常大的文件描述符数量,因为它内部使用红黑树和哈希表等数据结构,能够快速地处理大量的文件描述符。

    工作模式

    • select 和 poll: 只提供了一种工作模式,即水平触发(Level-Triggered),无法区分文件描述符的状态变化,只要文件描述符就绪就会通知。
    • epoll: 提供了两种工作模式,即水平触发和边缘触发(Edge-Triggered)。在边缘触发模式下,只有当文件描述符的状态变化时才会通知,能够更精确地控制事件通知的时机。

    小结

    poll模型的效率不如epoll,一般在fd数量比较多,但某段时间内,就绪事件fd数量较少的情况下,epoll才会体现出它的优势,也就是说socket连接数量较大时而活跃连接较少时epoll模型更高效。

    epoll常用函数

    在 Linux 下,epoll 提供了一些常用的函数来实现 I/O 多路复用,以下是几个常用的 epoll 函数及其功能介绍:

    epoll_create

    int epoll_create(int size);
    
    • 1
    • 功能:创建一个 epoll 实例,返回一个文件描述符,用于标识该 epoll 实例。
    • 参数:size 表示 epoll 实例中能够同时处理的文件描述符数量,该参数在 Linux 2.6.8 之后已经被忽略,可以传入任意大于 0 的值。
    • 返回值:成功时返回一个新的文件描述符,失败时返回 -1。

    epoll_ctl

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    
    • 1
    • 功能:向 epoll 实例中注册或修改事件。
    • 参数
      • epfd:epoll 实例的文件描述符。
      • op:表示对事件进行的操作,可以是 EPOLL_CTL_ADD(添加新的文件描述符)、EPOLL_CTL_MOD(修改已经注册的文件描述符)、EPOLL_CTL_DEL(从 epoll 实例中删除文件描述符)。当取值是EPOLL_CTL_DEL,第四个参数event忽略不计,可以设置为NULL。
      • fd:需要注册或修改事件的文件描述符。
      • event:指向 epoll_event 结构体的指针,描述了需要注册的事件类型和文件描述符相关的数据。
    • 返回值:成功时返回 0,失败时返回 -1。

    epoll_event结构体如下:

    struct epoll_event {
        uint32_t     events; // 事件类型,需要检测的fd事件,取值与poll函数一样
        epoll_data_t data; // 用户自定义数据
    };
    
    typedef union epoll_data {
        void    *ptr; // 指向用户数据的指针
        int      fd;  // 文件描述符
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    events:用来描述文件描述符上注册的事件类型,可以是以下的一个或多个值的组合:

    • EPOLLIN:表示对应的文件描述符可以读(包括对端 Socket 正常关闭)。
    • EPOLLOUT:表示对应的文件描述符可以写。
    • EPOLLRDHUP:表示对应的文件描述符被对端关闭,或者对端关闭了写操作。
    • EPOLLPRI:表示对应的文件描述符有紧急数据可读。
    • EPOLLERR:表示对应的文件描述符发生错误。
    • EPOLLHUP:表示对应的文件描述符被挂起。
    • EPOLLET:设置边缘触发模式(Edge-Triggered),默认为水平触发模式(Level-Triggered)。

    epoll_wait

    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    
    • 1
    • 功能:等待文件描述符上的 I/O 事件,并将就绪的文件描述符和事件信息存储到 events 数组中。
    • 参数
      • epfd:epoll 实例的文件描述符。
      • events:用于存储就绪事件的 epoll_event 结构体数组。
      • maxevents:events 数组的大小,表示最多能够存储多少个就绪事件。
      • timeout:超时时间,单位为毫秒,-1 表示永久阻塞直到有事件发生,0 表示立即返回,其他正数表示等待指定的毫秒数。
    • 返回值:返回就绪事件的数量,失败时返回 -1。

    实例

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define MAX_EVENTS 10
    #define PORT 8888
    
    int main() {
        int server_fd, client_fd, epoll_fd, nfds, n;
        struct epoll_event event, events[MAX_EVENTS];
        struct sockaddr_in server_addr, client_addr;
        socklen_t client_len = sizeof(client_addr);
    
        // 创建 socket
        if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(EXIT_FAILURE);
        }
    
        // 设置 server 地址信息
        memset(&server_addr, 0, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        server_addr.sin_port = htons(PORT);
    
        // 绑定 server 地址
        if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
            perror("bind");
            exit(EXIT_FAILURE);
        }
    
        // 监听连接
        if (listen(server_fd, 5) == -1) {
            perror("listen");
            exit(EXIT_FAILURE);
        }
    
        // 创建 epoll 实例
        if ((epoll_fd = epoll_create1(0)) == -1) {
            perror("epoll_create1");
            exit(EXIT_FAILURE);
        }
    
        // 将 server_fd 添加到 epoll 实例中
        event.events = EPOLLIN;
        event.data.fd = server_fd;
        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
            perror("epoll_ctl: server_fd");
            exit(EXIT_FAILURE);
        }
    
        while (1) {
            nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
            if (nfds == -1) {
                perror("epoll_wait");
                exit(EXIT_FAILURE);
            }
    
            for (n = 0; n < nfds; ++n) {
                if (events[n].data.fd == server_fd) {  // 有新连接
                    client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
                    if (client_fd == -1) {
                        perror("accept");
                        exit(EXIT_FAILURE);
                    }
                    // 将新的 client_fd 添加到 epoll 实例中
                    event.events = EPOLLIN;
                    event.data.fd = client_fd;
                    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
                        perror("epoll_ctl: client_fd");
                        exit(EXIT_FAILURE);
                    }
                } else {  // 有数据可读
                    char buffer[1024];
                    int bytes_recv = recv(events[n].data.fd, buffer, sizeof(buffer), 0);
                    if (bytes_recv <= 0) {  // 客户端断开连接
                        close(events[n].data.fd);
                    } else {
                        // 回显收到的消息
                        send(events[n].data.fd, buffer, bytes_recv, 0);
                    }
                }
            }
        }
    
        close(server_fd);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93

    参考链接:

    • https://blog.csdn.net/m0_67392182/article/details/124487784
    • https://www.zhihu.com/tardis/bd/art/159135478?source_id=1001
    • https://blog.51cto.com/morris131/6216788
    • https://www.cnblogs.com/xuewangkai/p/11158576.html
  • 相关阅读:
    Java中的抽象类和接口
    Java的String
    运放电路中何时加入偏置电流补偿电阻-运算放大器
    【Python】实现M行N列的矩阵转置
    【元宇宙欧米说】听兔迷兔如何从虚拟到现实创造潮玩新时代
    XTU-OJ 1171-coins
    论文阅读笔记:Instance-Aware Dynamic Neural Network Quantization
    专用短程通讯(DSRC)技术介绍
    Leetcode2-AddTwoNumbers
    haas506 2.0开发教程-sntp(仅支持2.2以上版本)
  • 原文地址:https://blog.csdn.net/baidu_33256174/article/details/134456230