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

IO多路复用:select、poll、epoll的底层区别

1. select

工作原理:
  • select 使用一个固定大小的数组来存储需要监控的文件描述符。这个数组的大小通常是限制在 FD_SETSIZE(通常为 1024)内。
  • 当调用 select 时,操作系统会检查每个文件描述符的状态,确定哪些描述符准备好进行 I/O 操作。
  • select 是阻塞的,即如果没有文件描述符准备好,它会阻塞调用线程直到有文件描述符变为可用状态,或达到超时。
优缺点:
  • 优点
    • 简单易用,适合于小规模的文件描述符监控。
  • 缺点
    • 不支持超过 FD_SETSIZE 的文件描述符。
    • 每次调用都需要将文件描述符数组复制到内核空间,性能开销大。
    • 每次调用时都要遍历整个文件描述符集合,效率低下,尤其是在大量文件描述符时。

2. poll

工作原理:
  • poll 使用一个可变大小的数组(pollfd 结构体数组)来存储需要监控的文件描述符。与 select 不同,poll 不再受到 FD_SETSIZE 的限制。
  • 调用 poll 时,它会检查 pollfd 数组中每个描述符的状态,并返回准备好 I/O 操作的文件描述符数量。
  • poll 也是阻塞的。
优缺点:
  • 优点
    • 不受 FD_SETSIZE 的限制,可以处理任意数量的文件描述符。
    • 结构更灵活,能够更容易地扩展。
  • 缺点
    • 仍然需要在每次调用时遍历整个数组,性能问题仍然存在。
    • 每次调用时,文件描述符状态的更新需要复制数据到内核,性能开销依旧。

3. epoll

工作原理:
  • epoll 是为了解决 select 和 poll 的一些性能瓶颈而设计的。它使用一个文件描述符来表示一个 epoll 实例。
  • epoll 通过 epoll_ctl 添加和删除文件描述符,而不是在每次调用中传递整个文件描述符数组。它维护了一个内核中的事件表。
  • epoll_wait 只返回准备好 I/O 操作的文件描述符,而不是遍历所有描述符。
  • 可以选择边缘触发(Edge Triggered)和水平触发(Level Triggered)模式。
  • 在水平触发模式下,当某个文件描述符(FD)变为可读、可写或发生异常时,epoll_wait 会返回该文件描述符的事件。只要文件描述符的状态满足条件(如可读),即使后续调用 epoll_wait 也会继续返回该文件描述符,直到应用程序将所有数据读出或写入完成。应用程序可以多次读取数据,确保数据不会丢失。适合需要确保数据完整处理的场景。
  • 在边缘触发模式下,当文件描述符的状态发生变化(例如从不可读变为可读)时,epoll_wait 会返回该文件描述符。一旦状态变化通知后,只有在后续状态再次变化时(如读入数据后再次变为可读),才会再次触发通知。如果没有读取所有可用数据,可能会错过后续的事件通知。适合高性能和高并发的场景。
优缺点:
  • 优点
    • 高效:适用于大量并发连接,尤其是在高并发的场景下,性能显著提高。
    • 不需要在每次调用时复制文件描述符的状态,减少了开销。
    • 只返回准备好的描述符,避免了遍历所有描述符的开销。
  • 缺点
    • API 比较复杂,使用上相对 select 和 poll 难度较高。
    • 只在 Linux 系统中可用,移植性较差。

总结

特性selectpollepoll
文件描述符限制有 (FD_SETSIZE)没有没有
数据结构固定大小的位图可变大小的结构体数组内核维护的事件表
调用性能O(n),每次遍历O(n),每次遍历O(1),只返回活跃的文件描述符
内存复制每次调用都需要每次调用都需要仅在添加/删除时需要
触发模式水平触发水平触发支持水平触发和边缘触发
适用场景小型应用中型应用大型高并发应用
  • select:简单但有文件描述符数量限制,性能在高并发情况下不佳。
  • poll:解决了 select 的限制,但性能依然受到遍历数组的影响。
  • epoll:最适合高并发场景,提供更高的性能和更灵活的接口,适合处理大量文件描述符。

代码示例

每个示例都创建了一个简单的 TCP 服务器,接收客户端的连接请求并处理数据。

1. 使用 select

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/select.h>

#define PORT 8080
#define MAX_CLIENTS 10

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};

    // 创建 socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置 socket 选项
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 绑定
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    bind(server_fd, (struct sockaddr *)&address, sizeof(address));

    // 监听
    listen(server_fd, MAX_CLIENTS);

    fd_set read_fds;
    int max_sd;

    while (true) {
        // 清空 fd_set
        FD_ZERO(&read_fds);
        FD_SET(server_fd, &read_fds);
        max_sd = server_fd;

        // 添加客户端 sockets
        for (int i = 0; i < MAX_CLIENTS; i++) {
            if (FD_ISSET(i, &read_fds) && i > 0) {
                FD_SET(i, &read_fds);
                if (i > max_sd) max_sd = i;
            }
        }

        // 调用 select
        int activity = select(max_sd + 1, &read_fds, nullptr, nullptr, nullptr);
        if ((activity < 0) && (errno != EINTR)) {
            std::cerr << "select error" << std::endl;
        }

        // 处理新连接
        if (FD_ISSET(server_fd, &read_fds)) {
            if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            std::cout << "New connection: " << new_socket << std::endl;
            FD_SET(new_socket, &read_fds);
        }

        // 处理客户端请求
        for (int i = 0; i < MAX_CLIENTS; i++) {
            if (FD_ISSET(i, &read_fds)) {
                int valread = read(i, buffer, 1024);
                if (valread == 0) {
                    // 客户端断开连接
                    std::cout << "Client disconnected: " << i << std::endl;
                    close(i);
                    FD_CLR(i, &read_fds);
                } else {
                    buffer[valread] = '\0';
                    std::cout << "Received from client " << i << ": " << buffer << std::endl;
                    send(i, buffer, valread, 0); // 回送数据
                }
            }
        }
    }

    return 0;
}

2. 使用 poll

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <poll.h>

#define PORT 8080
#define MAX_CLIENTS 10

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};

    // 创建 socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置 socket 选项
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 绑定
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    bind(server_fd, (struct sockaddr *)&address, sizeof(address));

    // 监听
    listen(server_fd, MAX_CLIENTS);

    struct pollfd fds[MAX_CLIENTS + 1];
    fds[0].fd = server_fd;
    fds[0].events = POLLIN;

    for (int i = 1; i <= MAX_CLIENTS; i++) {
        fds[i].fd = -1; // 初始化客户端 sockets
    }

    while (true) {
        int activity = poll(fds, MAX_CLIENTS + 1, -1); // 等待无限期

        if ((activity < 0) && (errno != EINTR)) {
            std::cerr << "poll error" << std::endl;
        }

        // 处理新连接
        if (fds[0].revents & POLLIN) {
            if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            std::cout << "New connection: " << new_socket << std::endl;

            for (int i = 1; i <= MAX_CLIENTS; i++) {
                if (fds[i].fd == -1) {
                    fds[i].fd = new_socket;
                    fds[i].events = POLLIN;
                    break;
                }
            }
        }

        // 处理客户端请求
        for (int i = 1; i <= MAX_CLIENTS; i++) {
            if (fds[i].fd > 0 && (fds[i].revents & POLLIN)) {
                int valread = read(fds[i].fd, buffer, 1024);
                if (valread == 0) {
                    // 客户端断开连接
                    std::cout << "Client disconnected: " << fds[i].fd << std::endl;
                    close(fds[i].fd);
                    fds[i].fd = -1; // 标记为无效
                } else {
                    buffer[valread] = '\0';
                    std::cout << "Received from client " << fds[i].fd << ": " << buffer << std::endl;
                    send(fds[i].fd, buffer, valread, 0); // 回送数据
                }
            }
        }
    }

    return 0;
}

3. 使用 epoll

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#define PORT 8080
#define MAX_CLIENTS 10

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};

    // 创建 socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置 socket 选项
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 绑定
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    bind(server_fd, (struct sockaddr *)&address, sizeof(address));

    // 监听
    listen(server_fd, MAX_CLIENTS);

    // 创建 epoll 实例
    int epoll_fd = epoll_create1(0);
    struct epoll_event event, events[MAX_CLIENTS];

    // 添加 server socket 到 epoll
    event.events = EPOLLIN;
    event.data.fd = server_fd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);

    while (true) {
        int num_events = epoll_wait(epoll_fd, events, MAX_CLIENTS, -1);
        for (int i = 0; i < num_events; i++) {
            if (events[i].data.fd == server_fd) {
                // 处理新连接
                if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
                    perror("accept");
                    exit(EXIT_FAILURE);
                }
                std::cout << "New connection: " << new_socket << std::endl;

                // 将新连接的 socket 添加到 epoll
                event.events = EPOLLIN;
                event.data.fd = new_socket;
                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event);
            } else {
                // 处理客户端请求
                int valread = read(events[i].data.fd, buffer, 1024);
                if (valread == 0) {
                    // 客户端断开连接
                    std::cout << "Client disconnected: " << events[i].data.fd << std::endl;
                    close(events[i].data.fd);
                } else {
                    buffer[valread] = '\0';
                    std::cout << "Received from client " << events[i].data.fd << ": " << buffer << std::endl;
                    send(events[i].data.fd, buffer, valread, 0); // 回送数据
                }
            }
        }
    }

    return 0;


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

相关文章:

  • 003 Qt_信号和槽-上
  • FPGA图像处理之均值滤波
  • react子应用嵌入qiankun微前端后,多层抽屉drawer getContainer={false}挂载在当前位置后抽屉不在停靠在窗口的最边上
  • HarmonyOS NEXT开发之ArkTS自定义组件学习笔记
  • 全桥LLC谐振变换器概述及MATLAB仿真
  • LeetCode刷题日记之贪心算法(二)
  • 【汇编语言】寄存器(内存访问)(七)—— CPU提供的栈机制
  • python从0快速上手(十二)高级特性2
  • 如何接受Date范围的数据
  • Chrome DevTools 三: Performance 性能面板扩展—— 性能优化
  • Python | basemap空间绘图 | cartopy | geoviews
  • ABAP:创建/修改客户的银行信息
  • 深度学习模型训练的主要流程(不定时更新中)
  • javaScript逆向怎样做
  • 【赵渝强老师】Oracle的联机重做日志文件与数据写入过程
  • 使用正则解决SQL注入问题
  • 对于六上前二单元的一些感想
  • [Hbase]一 HBase基础
  • 【论文速看】DL最新进展20241019-人脸识别、多模态
  • 使用Radzen Blazor组件库开发的基于ABP框架炫酷UI主题