标准输入上等待输入事件

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

int main() {
    fd_set read_fds;        // 读文件描述符集合
    struct timeval timeout; // 超时时间结构体

    int max_fd = 1; // 文件描述符的最大值
    int result;

    while (1) {
        FD_ZERO(&read_fds);           // 清空文件描述符集合
        FD_SET(STDIN_FILENO, &read_fds); // 将标准输入添加到文件描述符集合

        timeout.tv_sec = 5;  // 设置超时时间为5秒
        timeout.tv_usec = 0;

        // 使用 select 函数检查文件描述符集合上是否有可读事件
        result = select(max_fd, &read_fds, NULL, NULL, &timeout);

        if (result == -1) {
            perror("select");
            exit(EXIT_FAILURE);
        } else if (result == 0) {
            // 超时,可以在这里添加相应的处理逻辑
            printf("Timeout occurred!\n");
            fflush(stdout);
        } else {
            // 文件描述符上有可读事件发生
            if (FD_ISSET(STDIN_FILENO, &read_fds)) {
                char buffer[1024];
                // 从标准输入读取数据
                ssize_t bytesRead = read(STDIN_FILENO, buffer, sizeof(buffer));

                if (bytesRead == -1) {
                    perror("read");
                    exit(EXIT_FAILURE);
                } else if (bytesRead == 0) {
                    // EOF,表示输入结束
                    printf("End of file reached. Exiting...\n");
                    break;
                } else {
                    // 处理读取到的数据,这里简单地将其输出
                    printf("Read from stdin: %.*s", (int)bytesRead, buffer);
                    fflush(stdout);
                }
            }
        }
    }

    return 0;
}

select 的局限性与性能分析

  • fd_set 是位图结构,大小固定

    fd_set 在 Linux 下定义为:

    typedef struct {
        __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];   
    } fd_set;
    
    #define __FD_SETSIZE 1024

    这意味着单个 fd_set 最多只能同时监控 1024 个文件描述符

    对于高并发服务器,这个限制非常明显

  • 无法重用 fd_set,每次循环都要重建

    • select 调用会修改传入的 fd_set,使其只保留有事件发生的文件描述符
    • 所以每次循环都必须重新 FD_ZEROFD_SET,增加了额外的开销
  • 用户态与内核态频繁拷贝,性能开销大

    • select 内部需要从用户态拷贝 fd_set 到内核态,再从内核态拷贝结果回用户态
    • 对大量文件描述符的程序,拷贝成本不可忽视。
  • 轮询文件描述符,O(n) 复杂度

    • 内核会遍历 fd_set 中的每个文件描述符,检查是否就绪
    • 随着 fd 数量增加,轮询开销线性增长,影响性能
flowchart

    %% 用户态调用
    A[用户态程序] -->|调用 select/poll/epoll_wait| B[内核态]

    %% 内核判断就绪
    B --> C{文件描述符就绪?}
    C -->|是| D[返回就绪 fd 集合]
    C -->|否| E["阻塞或超时"]
    subgraph select_poll [select / poll 特点]

        F["fd_set / fd数组线性扫描, O(n)"]
        G[每次调用需重建 fd_set / 数组]
        H["用户态 <-> 内核态频繁拷贝"]
    end

    subgraph epoll [epoll 特点]

        I[内核维护就绪事件链表]
        J["只返回有事件的 fd, O(1)"]
        K[适合高并发场景]
    end

    %% 连接节点
    D --> F
    D --> G
    D --> H
    D --> I
    I --> J
    J --> K