Linux 下 select 测试函数

报表:

bash 复制代码
=======================================================================
 Linux select() 返回值 / errno 对照表
=======================================================================
 FD_SETSIZE     = 1024
 sizeof(fd_set) = 128 字节
 sizeof(long)   = 8 字节

 已确保 RLIMIT_NOFILE >= 2048

-----------------------------------------------------------------------
 场景 A:缓冲区足够大(堆分配覆盖 maxfd),fd 打开且可读
-----------------------------------------------------------------------
  select(nfds=1023, fd=1022 open & readable)               | ret =   1 | errno = 0   (Success)
  select(nfds=1024, fd=1023 open & readable)               | ret =   1 | errno = 0   (Success)
  select(nfds=1025, fd=1024 open & readable)               | ret =   1 | errno = 0   (Success)
  select(nfds=1026, fd=1025 open & readable)               | ret =   1 | errno = 0   (Success)

-----------------------------------------------------------------------
 场景 B:缓冲区只有 128 字节,nfds = fd+1 (fd >= 1024)
         用 mmap + guard page 让内核越界读立刻失败
-----------------------------------------------------------------------
  select(nfds=1025) with only 128B buffer at page end      | ret =  -1 | errno = 14  (Bad address)
  select(nfds=1026) with only 128B buffer at page end      | ret =  -1 | errno = 14  (Bad address)

-----------------------------------------------------------------------
 场景 C:fd_set 里放了一个已关闭的 fd
-----------------------------------------------------------------------
  select(nfds=4) with closed fd=3 in set                   | ret =  -1 | errno = 9   (Bad file descriptor)

-----------------------------------------------------------------------
 场景 D:nfds 非法
-----------------------------------------------------------------------
  select(nfds=-1, NULL, NULL, NULL, {0,0})                 | ret =  -1 | errno = 22  (Invalid argument)
  select(nfds=0,  NULL, NULL, NULL, {0,0})                 | ret =   0 | errno = 0   (Success)

-----------------------------------------------------------------------
 场景 E:所有 fd_set 位为 0,nfds 取 1024 / 1025 / 1026
-----------------------------------------------------------------------
  select(nfds=1024, all-zero, {0,0})                       | ret =   0 | errno = 0   (Success)
  select(nfds=1025, all-zero, {0,0})                       | ret =   0 | errno = 0   (Success)
  select(nfds=1026, all-zero, {0,0})                       | ret =   0 | errno = 0   (Success)

-----------------------------------------------------------------------
 场景 F:nfds=1026,fd_set 里只有 fd=1025 就绪,超时 500ms
-----------------------------------------------------------------------
  select(nfds=1026, fd=1025 ready, 500ms)                  | ret =   1 | errno = 0   (Success)

=======================================================================
 结论
-----------------------------------------------------------------------
 A) 缓冲区够大、nfds = maxfd+1 时,fd = 1024 / 1025 完全正常:
    ret = 就绪 fd 数,errno = 0。不会返回 EINVAL 或 EBADF。

 B) 缓冲区只有标准 128 字节但 nfds > 1024 时,内核拷贝 fd_set
    越界读,若缓冲区紧邻 guard page -> ret = -1, errno = EFAULT(14)。

 C) fd_set 中含已关闭的 fd -> ret = -1, errno = EBADF(9)。

 D) nfds < 0 -> ret = -1, errno = EINVAL(22)。
    nfds = 0 且所有位为 0 -> ret = 0。

 E) 所有 fd_set 位为 0 时,nfds 即使传 1024/1025/1026,
    只要缓冲区够大,select 直接按 timeout 超时返回 0,errno = 0。

 F) 标准 fd_set 下 FD_SET(1024, &set) / FD_SET(1025, &set)
    是用户态越界写,属于未定义行为,不会产生 errno。
=======================================================================

代码:

cpp 复制代码
// select_errno_demo.cpp
//
// 专门测试 Linux select() 在各种边界下返回什么、errno 是什么。
//
// 编译:
//   g++ -std=c++17 -O0 -g -Wall -Wextra -o select_errno_demo select_errno_demo.cpp
// 运行:
//   ./select_errno_demo
//
//#define _GNU_SOURCE
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cerrno>
#include <cstdint>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/resource.h>

/* ===================================================================== */
/*                              工具函数                                  */
/* ===================================================================== */

static bool raise_nofile(int need)
{
    struct rlimit rl;
    if (::getrlimit(RLIMIT_NOFILE, &rl) != 0) return false;
    if (static_cast<int>(rl.rlim_cur) >= need) return true;

    rlim_t want = static_cast<rlim_t>(need);
    if (rl.rlim_max != RLIM_INFINITY && want > rl.rlim_max)
        want = rl.rlim_max;
    rl.rlim_cur = want;
    if (::setrlimit(RLIMIT_NOFILE, &rl) != 0) return false;

    if (::getrlimit(RLIMIT_NOFILE, &rl) != 0) return false;
    return static_cast<int>(rl.rlim_cur) >= need;
}

/* 统一格式打印结果,方便对照 */
static void line(const char *desc, int ret, int err)
{
    if (ret < 0) {
        std::printf("  %-56s | ret = %3d | errno = %-3d (%s)\n",
                    desc, ret, err, std::strerror(err));
    } else {
        std::printf("  %-56s | ret = %3d | errno = %-3d (%s)\n",
                    desc, ret, err, err ? std::strerror(err) : "Success");
    }
}

/* 创建一个编号 >= min_fd 且立刻"可读"的 fd */
static int make_readable_fd_at_least(int min_fd, int *wfd_out)
{
    int pfd[2];
    if (::pipe(pfd) < 0) { std::perror("pipe"); std::exit(EXIT_FAILURE); }

    int rfd = ::fcntl(pfd[0], F_DUPFD, min_fd);
    if (rfd < 0) { std::perror("fcntl(F_DUPFD)"); std::exit(EXIT_FAILURE); }
    ::close(pfd[0]);

    char c = 'x';
    if (::write(pfd[1], &c, 1) != 1) { std::perror("write"); std::exit(EXIT_FAILURE); }

    *wfd_out = pfd[1];
    return rfd;
}

/* 精确拿到编号为 target 的 fd;失败返回 -1 */
static int dupfd_exact(int fd, int target)
{
    if (target < 0) return -1;
    int got = ::fcntl(fd, F_DUPFD, target);
    if (got < 0) return -1;
    if (got != target) { ::close(got); return -1; }
    return got;
}

/* 在堆上分配一个足以覆盖 [0, maxfd] 的 fd_set 缓冲区 */
static fd_set *fdset_alloc(int maxfd)
{
    const size_t bits  = static_cast<size_t>(maxfd) + 1;
    const size_t bytes = (bits + 7) / 8;
    size_t nsets = (bytes + sizeof(fd_set) - 1) / sizeof(fd_set);
    if (nsets == 0) nsets = 1;

    fd_set *p = static_cast<fd_set *>(std::calloc(nsets, sizeof(fd_set)));
    if (!p) { std::perror("calloc"); std::exit(EXIT_FAILURE); }
    return p;
}

/* ===================================================================== */
/*                               主程序                                   */
/* ===================================================================== */

int main(void)
{
    std::printf("=======================================================================\n");
    std::printf(" Linux select() 返回值 / errno 对照表\n");
    std::printf("=======================================================================\n");
    std::printf(" FD_SETSIZE     = %d\n", FD_SETSIZE);
    std::printf(" sizeof(fd_set) = %zu 字节\n", sizeof(fd_set));
    std::printf(" sizeof(long)   = %zu 字节\n\n", sizeof(long));

    if (!raise_nofile(2048)) {
        std::printf(" [警告] 无法把 RLIMIT_NOFILE 提升到 2048\n");
    } else {
        std::printf(" 已确保 RLIMIT_NOFILE >= 2048\n\n");
    }

    /* ------------------------------------------------------------------ */
    /* 准备 fd:1022 / 1023 / 1024 / 1025,均打开且可读                    */
    /* ------------------------------------------------------------------ */
    struct FdPair { int wanted; int rfd; int wfd; };
    FdPair fds[] = {
        { 1022, -1, -1 },
        { 1023, -1, -1 },
        { 1024, -1, -1 },
        { 1025, -1, -1 },
    };
    const int N = static_cast<int>(sizeof(fds) / sizeof(fds[0]));

    for (int i = 0; i < N; ++i) {
        int w = -1;
        int r = make_readable_fd_at_least(fds[i].wanted, &w);
        if (r != fds[i].wanted) {
            int exact = dupfd_exact(r, fds[i].wanted);
            if (exact != fds[i].wanted) {
                std::printf(" [错误] 无法把 fd 精确调到 %d\n", fds[i].wanted);
                ::close(r); ::close(w); fds[i].rfd = -1; continue;
            }
            ::close(r); r = exact;
        }
        fds[i].rfd = r;
        fds[i].wfd = w;
    }

    struct timeval tv;

    /* ================================================================== */
    /* 场景 A:缓冲区足够大,fd 打开且可读                                  */
    /* ================================================================== */
    std::printf("-----------------------------------------------------------------------\n");
    std::printf(" 场景 A:缓冲区足够大(堆分配覆盖 maxfd),fd 打开且可读\n");
    std::printf("-----------------------------------------------------------------------\n");

    for (int i = 0; i < N; ++i) {
        if (fds[i].rfd < 0) continue;
        const int fd = fds[i].rfd;

        fd_set *rfds = fdset_alloc(fd);
        FD_SET(fd, rfds);

        tv.tv_sec = 0; tv.tv_usec = 0;
        errno = 0;
        const int ret = ::select(fd + 1, rfds, nullptr, nullptr, &tv);
        const int e = errno;

        char desc[128];
        std::snprintf(desc, sizeof(desc),
                      "select(nfds=%d, fd=%d open & readable)", fd + 1, fd);
        line(desc, ret, e);

        std::free(rfds);
    }
    std::printf("\n");

    /* ================================================================== */
    /* 场景 B:缓冲区只有 sizeof(fd_set),nfds 却 = fd+1                    */
    /*         内核 copy_from_user 越界读 -> EFAULT                          */
    /* ================================================================== */
    std::printf("-----------------------------------------------------------------------\n");
    std::printf(" 场景 B:缓冲区只有 %zu 字节,nfds = fd+1 (fd >= 1024)\n",
                sizeof(fd_set));
    std::printf("         用 mmap + guard page 让内核越界读立刻失败\n");
    std::printf("-----------------------------------------------------------------------\n");

    {
        const long page = ::sysconf(_SC_PAGESIZE);
        char *base = static_cast<char *>(
            ::mmap(nullptr, static_cast<size_t>(page) * 2,
                   PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
        if (base == MAP_FAILED) { std::perror("mmap"); return 1; }
        if (::mprotect(base + page, static_cast<size_t>(page), PROT_NONE) < 0) {
            std::perror("mprotect"); return 1;
        }

        /* 把标准 fd_set 紧贴在 guard page 之前,保证越界读立刻 EFAULT */
        fd_set *rfds = reinterpret_cast<fd_set *>(base + page - sizeof(fd_set));
        std::memset(rfds, 0, sizeof(fd_set));

        for (int i = 0; i < N; ++i) {
            if (fds[i].rfd < 0) continue;
            const int fd = fds[i].rfd;
            if (fd < FD_SETSIZE) continue;   /* 只测 fd >= 1024 */

            tv.tv_sec = 0; tv.tv_usec = 0;
            errno = 0;
            const int ret = ::select(fd + 1, rfds, nullptr, nullptr, &tv);
            const int e = errno;

            char desc[128];
            std::snprintf(desc, sizeof(desc),
                          "select(nfds=%d) with only %zuB buffer at page end",
                          fd + 1, sizeof(fd_set));
            line(desc, ret, e);
        }

        ::munmap(base, static_cast<size_t>(page) * 2);
    }
    std::printf("\n");

    /* ================================================================== */
    /* 场景 C:fd_set 里放了一个已经关闭的 fd -> EBADF                      */
    /* ================================================================== */
    std::printf("-----------------------------------------------------------------------\n");
    std::printf(" 场景 C:fd_set 里放了一个已关闭的 fd\n");
    std::printf("-----------------------------------------------------------------------\n");

    {
        int pfd[2];
        if (::pipe(pfd) < 0) { std::perror("pipe"); return 1; }
        int closed_fd = pfd[0];
        ::close(pfd[0]);
        ::close(pfd[1]);

        fd_set *rfds = fdset_alloc(closed_fd);
        FD_SET(closed_fd, rfds);

        tv.tv_sec = 0; tv.tv_usec = 0;
        errno = 0;
        const int ret = ::select(closed_fd + 1, rfds, nullptr, nullptr, &tv);
        const int e = errno;

        char desc[128];
        std::snprintf(desc, sizeof(desc),
                      "select(nfds=%d) with closed fd=%d in set",
                      closed_fd + 1, closed_fd);
        line(desc, ret, e);

        std::free(rfds);
    }
    std::printf("\n");

    /* ================================================================== */
    /* 场景 D:nfds 非法                                                    */
    /* ================================================================== */
    std::printf("-----------------------------------------------------------------------\n");
    std::printf(" 场景 D:nfds 非法\n");
    std::printf("-----------------------------------------------------------------------\n");

    {
        tv.tv_sec = 0; tv.tv_usec = 0;

        errno = 0;
        int ret = ::select(-1, nullptr, nullptr, nullptr, &tv);
        line("select(nfds=-1, NULL, NULL, NULL, {0,0})", ret, errno);

        errno = 0;
        ret = ::select(0, nullptr, nullptr, nullptr, &tv);
        line("select(nfds=0,  NULL, NULL, NULL, {0,0})", ret, errno);
    }
    std::printf("\n");

    /* ================================================================== */
    /* 场景 E:nfds 合法,但所有位为 0 -> 超时返回 0                         */
    /* ================================================================== */
    std::printf("-----------------------------------------------------------------------\n");
    std::printf(" 场景 E:所有 fd_set 位为 0,nfds 取 1024 / 1025 / 1026\n");
    std::printf("-----------------------------------------------------------------------\n");

    {
        const int nfds_list[] = { 1024, 1025, 1026 };
        for (int nfds : nfds_list) {
            fd_set *rfds = fdset_alloc(nfds - 1);

            tv.tv_sec = 0; tv.tv_usec = 0;
            errno = 0;
            const int ret = ::select(nfds, rfds, nullptr, nullptr, &tv);
            const int e = errno;

            char desc[128];
            std::snprintf(desc, sizeof(desc),
                          "select(nfds=%d, all-zero, {0,0})", nfds);
            line(desc, ret, e);

            std::free(rfds);
        }
    }
    std::printf("\n");

    /* ================================================================== */
    /* 场景 F:nfds=1026,fd_set 里只有 fd=1025 就绪,超时 500ms            */
    /* ================================================================== */
    std::printf("-----------------------------------------------------------------------\n");
    std::printf(" 场景 F:nfds=1026,fd_set 里只有 fd=1025 就绪,超时 500ms\n");
    std::printf("-----------------------------------------------------------------------\n");

    {
        if (fds[3].rfd >= 0) {
            const int fd = fds[3].rfd;   /* 1025 */

            fd_set *rfds = fdset_alloc(fd);
            FD_SET(fd, rfds);

            tv.tv_sec = 0; tv.tv_usec = 500000;
            errno = 0;
            const int ret = ::select(fd + 1, rfds, nullptr, nullptr, &tv);
            const int e = errno;

            char desc[128];
            std::snprintf(desc, sizeof(desc),
                          "select(nfds=%d, fd=%d ready, 500ms)", fd + 1, fd);
            line(desc, ret, e);

            std::free(rfds);
        }
    }
    std::printf("\n");

    /* ---------- 收尾 ---------- */
    for (int i = 0; i < N; ++i) {
        if (fds[i].rfd >= 0) ::close(fds[i].rfd);
        if (fds[i].wfd >= 0) ::close(fds[i].wfd);
    }

    std::printf("=======================================================================\n");
    std::printf(" 结论\n");
    std::printf("-----------------------------------------------------------------------\n");
    std::printf(" A) 缓冲区够大、nfds = maxfd+1 时,fd = 1024 / 1025 完全正常:\n");
    std::printf("    ret = 就绪 fd 数,errno = 0。不会返回 EINVAL 或 EBADF。\n");
    std::printf("\n");
    std::printf(" B) 缓冲区只有标准 128 字节但 nfds > 1024 时,内核拷贝 fd_set\n");
    std::printf("    越界读,若缓冲区紧邻 guard page -> ret = -1, errno = EFAULT(14)。\n");
    std::printf("\n");
    std::printf(" C) fd_set 中含已关闭的 fd -> ret = -1, errno = EBADF(9)。\n");
    std::printf("\n");
    std::printf(" D) nfds < 0 -> ret = -1, errno = EINVAL(22)。\n");
    std::printf("    nfds = 0 且所有位为 0 -> ret = 0。\n");
    std::printf("\n");
    std::printf(" E) 所有 fd_set 位为 0 时,nfds 即使传 1024/1025/1026,\n");
    std::printf("    只要缓冲区够大,select 直接按 timeout 超时返回 0,errno = 0。\n");
    std::printf("\n");
    std::printf(" F) 标准 fd_set 下 FD_SET(1024, &set) / FD_SET(1025, &set)\n");
    std::printf("    是用户态越界写,属于未定义行为,不会产生 errno。\n");
    std::printf("=======================================================================\n");

    return 0;
}
相关推荐
夜雪一千1 小时前
Python 生成模拟地址数据:Faker之外的备选库
网络·windows·python
DongQiShanRen1 小时前
玄龙(上):TICK 主循环——意识心跳怎么跳
linux·jvm·数据库·人工智能·数据挖掘·rust
小此方1 小时前
Linux网络(二十一):深入理解 TCP 异常处理:网线断开、Keepalive 保活机制与 Linux 内核传输层协议源码剖析
linux·网络·tcp/ip
pride.li2 小时前
Python 安装
linux·python·ubuntu
数据库小学妹2 小时前
存算分离到底分离了什么?四条架构变化与选型判断
数据库·数据库架构·云原生数据库·缓存一致性·存算分离
FPGA小徐2 小时前
【一生一芯 / PA】异常响应机制:RISC-V 中 ecall → mtvec → mret 的完整代
开发语言·数据库·c#
笔墨登场说说2 小时前
centos 8 使用 kafka_2.13-4.3.1.tgz 安装kafka
linux·kafka·centos
北京盛世宏博2 小时前
工业环境监测选型:Modbus TCP以太网温湿度传感器部署、调试、避坑全指南
开发语言·网络·php
luo_guibin2 小时前
Linux系统下OpenSSL升级全流程,新旧版本切换
linux·运维·服务器