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

Linux 中的poll、select和epoll有什么区别?

pollselect 是Linux 系统中用于多路复用 I/O 的系统调用,它们允许一个程序同时监视多个文件描述符,以便在任何一个文件描述符准备好进行 I/O 操作时得到通知。

一、select

select 是一种较早的 I/O 多路复用机制,具有以下特点:

  1. 接口

    • select 使用三个文件描述符集合(读、写、异常)来监视文件描述符的状态。

    • 函数签名为:

      int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
      
    • nfds 是需要监视的文件描述符的数量(通常是最大文件描述符加一)。

    • fd_set 是一个位图结构,用于表示文件描述符集合。

    • timeout 指定等待事件发生的超时时间。

  2. 限制

    • select 的文件描述符数量受到 FD_SETSIZE 的限制,通常为 1024。这意味着它不适合处理非常大量的并发连接。
  3. 效率

    • 每次调用 select 都需要重新初始化文件描述符集合,因此在处理大量文件描述符时效率较低。
  4. 可移植性

    • select 是 POSIX 标准的一部分,因此在许多操作系统上都可用。

select 示例:同时监视多个文件描述符的 I/O 事件

这个例子展示了如何使用 select 来监视两个文件描述符:标准输入(通常是终端输入)和一个网络套接字。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd, newsockfd;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t clilen;
    char buffer[BUFFER_SIZE];
    fd_set readfds;
    int maxfd;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 初始化服务器地址结构
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // 绑定套接字
    if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(sockfd, 5) < 0) {
        perror("listen failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 接受一个连接
    clilen = sizeof(cliaddr);
    newsockfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
    if (newsockfd < 0) {
        perror("accept failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    while (1) {
        // 清空文件描述符集合
        FD_ZERO(&readfds);

        // 将标准输入和新套接字添加到集合中
        FD_SET(STDIN_FILENO, &readfds);
        FD_SET(newsockfd, &readfds);
        maxfd = (STDIN_FILENO > newsockfd) ? STDIN_FILENO : newsockfd;

        // 使用 select 监视
        if (select(maxfd + 1, &readfds, NULL, NULL, NULL) < 0) {
            perror("select error");
            break;
        }

        // 检查标准输入是否有数据
        if (FD_ISSET(STDIN_FILENO, &readfds)) {
            if (fgets(buffer, BUFFER_SIZE, stdin) != NULL) {
                printf("Input from stdin: %s", buffer);
            }
        }

        // 检查套接字是否有数据
        if (FD_ISSET(newsockfd, &readfds)) {
            int n = read(newsockfd, buffer, BUFFER_SIZE);
            if (n <= 0) {
                printf("Client disconnected\n");
                break;
            }
            buffer[n] = '\0';
            printf("Received from client: %s", buffer);
        }
    }

    close(newsockfd);
    close(sockfd);
    return 0;
}

二、poll

pollselect 的改进版本,提供了一些更灵活的特性:

  1. 接口

    • poll 使用一个结构体数组来监视文件描述符的状态。

    • 函数签名为:

      int poll(struct pollfd *fds, nfds_t nfds, int timeout);
      
    • pollfd 结构体包含文件描述符及其感兴趣的事件。

    • timeout 以毫秒为单位,指定等待事件发生的超时时间。

  2. 无数量限制

    • poll 不受 FD_SETSIZE 限制,因此可以监视的文件描述符数量仅受系统资源的限制。
  3. 效率

    • 由于 poll 使用数组而不是位图,每次调用不需要重新初始化整个集合,但仍然需要扫描整个数组,因此在处理非常大量文件描述符时效率也受到限制。
  4. 事件类型

    • poll 可以监视更多类型的事件,如挂起和优先级数据。

poll 示例:监视标准输入和一个网络套接字

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd, newsockfd;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t clilen;
    char buffer[BUFFER_SIZE];
    struct pollfd fds[2];

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 初始化服务器地址结构
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // 绑定套接字
    if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(sockfd, 5) < 0) {
        perror("listen failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 接受一个连接
    clilen = sizeof(cliaddr);
    newsockfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
    if (newsockfd < 0) {
        perror("accept failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 设置 poll 文件描述符
    fds[0].fd = STDIN_FILENO;
    fds[0].events = POLLIN;
    fds[1].fd = newsockfd;
    fds[1].events = POLLIN;

    while (1) {
        // 使用 poll 监视
        int ret = poll(fds, 2, -1);
        if (ret < 0) {
            perror("poll error");
            break;
        }

        // 检查标准输入是否有数据
        if (fds[0].revents & POLLIN) {
            if (fgets(buffer, BUFFER_SIZE, stdin) != NULL) {
                printf("Input from stdin: %s", buffer);
            }
        }

        // 检查套接字是否有数据
        if (fds[1].revents & POLLIN) {
            int n = read(newsockfd, buffer, BUFFER_SIZE);
            if (n <= 0) {
                printf("Client disconnected\n");
                break;
            }
            buffer[n] = '\0';
            printf("Received from client: %s", buffer);
        }
    }

    close(newsockfd);
    close(sockfd);
    return 0;
}

三、epoll

epoll 是 Linux 内核提供的一种高效的 I/O 事件通知机制,用于处理大量文件描述符的事件。它是 pollselect 系统调用的改进版本,专为解决在处理大量并发连接时的性能问题而设计

epoll 的主要特点包括:

  1. 高效性:与 selectpoll 不同,epoll 不需要在每次调用时重新传递所有的文件描述符集。它通过一个文件描述符来管理感兴趣的事件,减少了内核和用户空间之间的数据拷贝。
  2. 水平触发和边缘触发epoll 支持两种触发模式。水平触发(Level-Triggered)模式类似于 selectpoll 的工作方式,而边缘触发(Edge-Triggered)模式则更加高效,但也更复杂,需要更仔细的事件处理。
  3. 支持大规模连接epoll 能够更好地处理大规模并发连接,因此非常适合用在高性能服务器应用中,如网络服务器或代理服务器。
  4. 事件注册和监听分离:通过 epoll_ctl 可以动态地添加、修改或删除监听的文件描述符,而 epoll_wait 用于等待事件的发生。

使用 epoll 通常涉及三个主要的系统调用:

  • epoll_create:创建一个 epoll 实例。
  • epoll_ctl:注册、修改或删除感兴趣的事件。
  • epoll_wait:等待事件的发生并获取事件列表。

epoll 是 Linux 特有的 I/O 多路复用机制,专为处理大量并发连接而设计。与 selectpoll 相比,epoll 更高效,因为它在内核中维护一个事件表,避免了每次调用都需要重新传递完整的文件描述符集合。

应用示例:监视标准输入和一个网络套接字。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 1024
#define MAX_EVENTS 10

int main() {
    int sockfd, newsockfd, epollfd;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t clilen;
    char buffer[BUFFER_SIZE];
    struct epoll_event ev, events[MAX_EVENTS];
    int nfds;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 初始化服务器地址结构
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // 绑定套接字
    if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(sockfd, 5) < 0) {
        perror("listen failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 创建 epoll 实例
    epollfd = epoll_create1(0);
    if (epollfd == -1) {
        perror("epoll_create1 failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 将监听套接字添加到 epoll 实例中
    ev.events = EPOLLIN;
    ev.data.fd = sockfd;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
        perror("epoll_ctl: sockfd");
        close(sockfd);
        close(epollfd);
        exit(EXIT_FAILURE);
    }

    // 将标准输入添加到 epoll 实例中
    ev.events = EPOLLIN;
    ev.data.fd = STDIN_FILENO;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev) == -1) {
        perror("epoll_ctl: stdin");
        close(sockfd);
        close(epollfd);
        exit(EXIT_FAILURE);
    }

    while (1) {
        // 等待事件发生
        nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait failed");
            break;
        }

        for (int n = 0; n < nfds; ++n) {
            if (events[n].data.fd == sockfd) {
                // 处理新连接
                clilen = sizeof(cliaddr);
                newsockfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
                if (newsockfd == -1) {
                    perror("accept failed");
                    continue;
                }

                // 将新连接添加到 epoll 实例中
                ev.events = EPOLLIN;
                ev.data.fd = newsockfd;
                if (epoll_ctl(epollfd, EPOLL_CTL_ADD, newsockfd, &ev) == -1) {
                    perror("epoll_ctl: newsockfd");
                    close(newsockfd);
                    continue;
                }

            } else if (events[n].data.fd == STDIN_FILENO) {
                // 处理标准输入
                if (fgets(buffer, BUFFER_SIZE, stdin) != NULL) {
                    printf("Input from stdin: %s", buffer);
                }

            } else {
                // 处理来自客户端的数据
                int n = read(events[n].data.fd, buffer, BUFFER_SIZE);
                if (n <= 0) {
                    if (n == 0) {
                        printf("Client disconnected\n");
                    } else {
                        perror("read error");
                    }
                    close(events[n].data.fd);
                } else {
                    buffer[n] = '\0';
                    printf("Received from client: %s", buffer);
                }
            }
        }
    }

    close(sockfd);
    close(epollfd);
    return 0;
}

解释

  1. 创建套接字:首先创建一个 TCP 套接字并绑定到指定端口,然后开始监听连接。
  2. 创建 epoll 实例:使用 epoll_create1 创建一个新的 epoll 实例。
  3. 注册文件描述符:将监听套接字和标准输入文件描述符注册到 epoll 实例中,以便监视它们的可读事件。
  4. 事件循环:使用 epoll_wait 等待事件发生。当有事件发生时,检查是哪种事件并进行相应处理:
    • 如果是监听套接字有事件,表示有新连接到达,使用 accept 接受连接并将新套接字注册到 epoll 实例中。
    • 如果是标准输入有事件,读取输入并处理。
    • 如果是客户端套接字有事件,读取数据并处理。
  5. 清理:在程序结束时关闭所有打开的文件描述符。

四、总结

poll和select机制
  • 用户空间到内核空间的拷贝:每次调用 selectpoll 时,用户必须将所有需要监视的文件描述符列表传递给内核。这意味着每次调用都需要将这些数据从用户空间复制到内核空间,这在监视大量文件描述符时效率较低。
  • 线性扫描:内核需要扫描所有文件描述符以确定哪些文件描述符有事件发生。这种线性扫描在文件描述符数量很大时效率不高。
fd_set: fd1, fd2, ..., fdN
检查fd1, fd2, ..., fdN
返回结果: fdX, fdY
用户空间
内核空间
内核空间
用户空间
epoll机制
  • 内核维护事件表epoll 通过 epoll_create 创建一个 epoll 实例,这个实例在内核中维护一个事件表。这个表记录了所有已经注册的文件描述符及其感兴趣的事件类型。
  • 增量更新:通过 epoll_ctl,用户可以增量地添加、修改或删除文件描述符及其感兴趣的事件。这意味着用户只需要在文件描述符集合发生变化时进行更新,而不是每次等待事件时都传递整个集合。
  • 事件驱动epoll_wait 返回的不是所有文件描述符的状态,而是已经准备好进行 I/O 操作的文件描述符列表。这使得 epoll 在处理大量文件描述符时更加高效,因为它只返回有事件的文件描述符。
epoll_create
epoll_ctl: 添加/修改fd
epoll_wait
返回有事件的fd
用户空间
内核空间: 事件表
内核空间
用户空间

select/poll 中,每次都需要将完整的文件描述符集合传递给内核,而 epoll 通过维护一个事件表,只在有事件发生时通知用户空间,从而提高了效率


http://www.kler.cn/a/519861.html

相关文章:

  • R语言学习笔记之高效数据操作
  • 【测试】UI自动化测试
  • springboot3 集成 knife4j(接口文档)
  • 从零到全栈开发
  • Docker快速部署高效照片管理系统LibrePhotos搭建私有云相册
  • PHP防伪溯源一体化管理系统小程序
  • 【学习笔记】计算机网络(二)
  • 第29章 xUnit框架下的测试模式详解
  • 1、云计算
  • 什么是区块链
  • 单链表算法实战:解锁数据结构核心谜题——链表的回文结构
  • Leecode刷题C语言之完成所有交易的初始最少钱数
  • Rust 中的结构体使用指南
  • 積分方程與簡單的泛函分析8.具連續對稱核的非齊次第II類弗雷德霍姆積分算子方程
  • 【矩阵二分】力扣378. 有序矩阵中第 K 小的元素
  • 10 Hyperledger Fabric 介绍
  • 个性化的语言模型构建思路
  • 洛谷 P5709:Apples Prologue / 苹果和虫子
  • 2025年前端技术革新趋势
  • Leetcode求职题目(21)
  • 适合 C# 开发者的 Semantic Kernel 入门:用 AI 赋能你的 .NET 应用
  • 【由浅入深认识Maven】第1部分 maven简介与核心概念
  • 回溯算法学习记录及习题集合
  • JavaScript常见面试问题解答
  • 代码随想录训练营第五十六天| 108.冗余连接 109.冗余连接II
  • 2024年蓝桥杯真题C/C++/java组部分真题解析(一)