select、poll、epoll

95 阅读4分钟

生动有趣地聊聊 selectpollepoll 的 I/O 多路复用 🌟

Hello,小伙伴们!今天我们来聊聊计算机网络编程中的三位“超级英雄”——selectpollepoll!它们在处理多个 I/O 操作时各有千秋,一起来看看它们各自的绝活吧!


🧐 什么是 I/O 多路复用?

I/O 多路复用就是让一个程序同时处理多个 I/O 操作的“魔法技能”!通过这个技能,程序可以在一个线程里高效地处理多个连接。下面我们就来认识一下这三位“超级英雄”!


🐱‍👤 select:老牌经典英雄

🧩 数据结构

  • 位图select 使用固定大小的位图(bitmap),由三个 fd_set 组成,分别表示读、写和异常的文件描述符集合。
  • 大小限制select 的文件描述符数量有限,通常是 FD_SETSIZE(默认 1024)。

🎯 用法示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>

int main() {
    fd_set read_fds;
    struct timeval timeout;
    int fd1 = 0; // 标准输入
    int fd2 = 3; // 假设另一个文件描述符,如 socket

    timeout.tv_sec = 5; // 5 秒超时
    timeout.tv_usec = 0;

    FD_ZERO(&read_fds); // 清除集合
    FD_SET(fd1, &read_fds); // 添加文件描述符
    FD_SET(fd2, &read_fds);

    int max_fd = (fd1 > fd2) ? fd1 : fd2;

    int result = select(max_fd + 1, &read_fds, NULL, NULL, &timeout);

    if (result == -1) {
        perror("select");
        exit(EXIT_FAILURE);
    } else if (result == 0) {
        printf("超时了,没有数据到来!\n");
    } else {
        if (FD_ISSET(fd1, &read_fds)) {
            printf("标准输入有数据可读!\n");
        }
        if (FD_ISSET(fd2, &read_fds)) {
            printf("文件描述符 3 有数据可读!\n");
        }
    }

    return 0;
}

🧐 特点

  • 每次调用都需要拷贝文件描述符集到内核空间,并在返回时将结果拷贝回用户空间。
  • 性能:遍历所有文件描述符,复杂度是 O(n)。

🐱‍🚀 poll:进阶高手

🧩 数据结构

  • pollfd 数组poll 使用 pollfd 数组,每个 pollfd 结构包含文件描述符和事件掩码。
  • 大小限制:没有固定限制,可以处理任意数量的文件描述符。

🎯 用法示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>

#define TIMEOUT 5000 // 超时时间,毫秒

int main() {
    struct pollfd fds[2];
    int ret;

    fds[0].fd = STDIN_FILENO;
    fds[0].events = POLLIN;

    fds[1].fd = 3; // 假设另一个文件描述符
    fds[1].events = POLLIN;

    ret = poll(fds, 2, TIMEOUT);

    if (ret == -1) {
        perror("poll");
        exit(EXIT_FAILURE);
    } else if (ret == 0) {
        printf("超时了,没有数据到来!\n");
    } else {
        if (fds[0].revents & POLLIN) {
            printf("标准输入有数据可读!\n");
        }
        if (fds[1].revents & POLLIN) {
            printf("文件描述符 3 有数据可读!\n");
        }
    }

    return 0;
}

🧐 特点

  • 每次调用都需要拷贝 pollfd 数组到内核空间,并在返回时将结果拷贝回用户空间。
  • 性能:遍历 pollfd 数组,复杂度也是 O(n)。

🐱‍🏍 epoll:高效王者

🧩 数据结构

  • 内核事件表epoll 使用内核事件表(红黑树和链表)。
  • 大小限制:没有固定大小限制,理论上可以支持上百万个并发连接。

🎯 用法示例

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

#define MAX_EVENTS 10

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

    struct epoll_event event;
    struct epoll_event events[MAX_EVENTS];

    event.events = EPOLLIN;
    event.data.fd = STDIN_FILENO;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, &event) == -1) {
        perror("epoll_ctl");
        exit(EXIT_FAILURE);
    }

    int num_fds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
    if (num_fds == -1) {
        perror("epoll_wait");
        exit(EXIT_FAILURE);
    }

    for (int i = 0; i < num_fds; i++) {
        if (events[i].data.fd == STDIN_FILENO) {
            printf("标准输入有数据可读!\n");
        }
    }

    close(epoll_fd);
    return 0;
}

🧐 特点

  • 只在需要时拷贝文件描述符到内核空间(调用 epoll_ctl 时),减少每次调用的开销。
  • 性能:事件驱动,只处理发生事件的文件描述符,复杂度接近 O(1)。

🥳 结论大比拼

特性selectpollepoll
数据结构固定大小的位图pollfd 数组内核事件表
大小限制1024(默认)无固定限制无固定限制
内存拷贝每次调用都需要每次调用都需要仅在 epoll_ctl 时需要
时间复杂度O(n)O(n)O(1)
处理过程遍历所有文件描述符遍历所有文件描述符事件驱动,只处理发生事件的文件描述符
并发能力小规模并发中等规模并发大规模并发

epoll 是处理大规模并发连接的王者,适用于高性能网络服务器。而 selectpoll 则适用于较小或中等规模的并发连接。


希望这个有趣的讲解能帮助你更好地理解 selectpollepoll!快来用这些“超级英雄”提升你的编程技能吧!🌟💪