select和poll之间的性能对比

poll 通过 revents 字段直接标记事件,而 select 需要遍历三个独立的位图。这种设计差异直接导致了性能上的差距。以下是详细对比:


1. poll 的事件检测:单次遍历 + 直接标记

(1)pollfd 结构体

cpp 复制代码
struct pollfd {
    int fd;         // 文件描述符
    short events;   // 用户关心的事件(如 POLLIN、POLLOUT)
    short revents;  // 内核填充的实际发生的事件
};
  • events :用户设置的监听事件掩码(如 POLLIN | POLLHUP)。
  • revents :由内核填充,表示该 fd 上实际发生的事件(如 POLLIN | POLLERR)。

(2)内核处理逻辑

  1. 用户调用

    cpp 复制代码
    struct pollfd fds[1000];
    for (int i = 0; i < 1000; i++) {
        fds[i].fd = ...;
        fds[i].events = POLLIN; // 只关心可读事件
    }
    poll(fds, 1000, timeout);
  2. 内核行为

    • 单次遍历 :内核直接遍历 fds 数组(1000 次)。
    • 直接标记
      • 对每个 fds[i],内核检查 fd 的状态。
      • 如果事件发生(如可读),则将 revents 设置为对应标志(如 POLLIN)。
      • 无需额外遍历或位操作
  3. 用户检查

    cpp 复制代码
    for (int i = 0; i < 1000; i++) {
        if (fds[i].revents & POLLIN) {
            // fd 可读
        }
    }

(3)优势

  • 高效 :内核只需遍历一次数组,且直接填充 revents,无冗余操作。
  • 灵活revents 可以同时标记多个事件(如 POLLIN | POLLHUP)。

2. select 的事件检测:三次遍历 + 位图操作

(1)select 的参数

cpp 复制代码
int select(
    int nfds,          // 最大 fd + 1
    fd_set *readfds,   // 可读事件位图
    fd_set *writefds,  // 可写事件位图
    fd_set *exceptfds, // 异常事件位图
    struct timeval *timeout
);
  • 三个位图:分别对应读、写、异常事件。
  • nfds :强制内核遍历 0nfds-1 的所有 fd,即使未被监听。

(2)内核处理逻辑

  1. 用户调用

    cpp 复制代码
    fd_set read_fds, write_fds;
    FD_ZERO(&read_fds);
    FD_ZERO(&write_fds);
    for (int fd = 3; fd <= 1002; fd++) {
        FD_SET(fd, &read_fds);  // 监听 1000 个 fd 的可读事件
    }
    select(1003, &read_fds, NULL, NULL, NULL);
  2. 内核行为

    • 拷贝位图 :将 read_fdswrite_fdsexcept_fds 从用户空间拷贝到内核。
    • 三次遍历
      1. 遍历 read_fds
        • 对每个 fd(从 01002):
          • 如果 FD_ISSET(fd, &read_fds) 为真,检查是否可读。
          • 如果可读,保留该 fd 在位图中(内核可能直接修改位图)。
      2. 遍历 write_fds(同理)。
      3. 遍历 except_fds(同理)。
    • 无效操作
      • 即使 fd=0,1,2 未被设置,内核仍会检查它们(共 3 次无效操作)。
      • 如果 fd 数量更多(如 10,000),无效操作会线性增加。
  3. 用户检查

    cpp 复制代码
    for (int fd = 3; fd <= 1002; fd++) {
        if (FD_ISSET(fd, &read_fds)) {
            // fd 可读
        }
    }

(3)劣势

  • 低效
    • 内核需要三次独立遍历(读、写、异常),且每次遍历都可能涉及全局扫描。
    • 每次遍历需通过 FD_ISSET 位操作检查 fd 是否被监听。
  • 局限性
    • FD_SETSIZE 限制(通常 1024),无法直接支持大量 fd。
    • 位图操作在 fd 稀疏时(如跳过前 1000 个 fd)浪费大量计算。

3. 直观对比:poll vs select 的事件检测

步骤 poll select
事件标记方式 revents 字段直接填充 三个独立位图(读、写、异常)
内核遍历次数 1 次(遍历 pollfd 数组) 3 次(遍历三个位图)
无效 fd 检查 无(仅检查用户提供的 fd) 有(必须检查 0nfds-1
位图操作 每次检查需 FD_ISSET 位操作
支持 fd 数量 无限制(仅受系统资源限制) FD_SETSIZE 限制(通常 1024)

4. 为什么 poll 更高效?

  1. 单次遍历 vs 三次遍历
    • poll 只需遍历用户提供的 fd 列表一次,而 select 需要遍历三个位图各一次。
  2. 直接标记 vs 位图操作
    • pollrevents 直接填充事件标志,无需位操作。
    • select 需通过 FD_ISSET 检查位图,额外开销大。
  3. 无效操作优化
    • poll 完全跳过未监听的 fd,而 select 必须检查所有 fd(即使未被监听)。

5. 代码示例对比

(1)poll 检测可读事件

cpp 复制代码
struct pollfd fds[1000];
for (int i = 0; i < 1000; i++) {
    fds[i].fd = 3 + i;
    fds[i].events = POLLIN;
}
poll(fds, 1000, timeout);

for (int i = 0; i < 1000; i++) {
    if (fds[i].revents & POLLIN) {
        printf("fd %d is readable\n", fds[i].fd);
    }
}

(2)select 检测可读事件

cpp 复制代码
fd_set read_fds;
FD_ZERO(&read_fds);
for (int fd = 3; fd <= 1002; fd++) {
    FD_SET(fd, &read_fds);
}
select(1003, &read_fds, NULL, NULL, timeout);

for (int fd = 3; fd <= 1002; fd++) {
    if (FD_ISSET(fd, &read_fds)) {
        printf("fd %d is readable\n", fd);
    }
}

6. 总结

  • pollrevents 设计
    通过单次遍历和直接标记事件,避免了 select 的三次遍历和位图操作,因此性能更高。
  • select 的局限性
    全局扫描和位图操作使其在处理大量 fd 时效率低下,且受 FD_SETSIZE 限制。
  • 适用场景
    • 高并发(大量 fd)时,优先使用 poll(或更现代的 epoll/kqueue)。
    • 低并发或遗留代码中,select 仍可能被使用,但性能较差。
相关推荐
笨蛋不要掉眼泪1 小时前
Java虚拟机:堆的参数配置
java·开发语言·jvm
冰暮流星1 小时前
javascript之date对象
开发语言·javascript·ecmascript
闪电悠米1 小时前
力扣hot100-54.螺旋矩阵-模拟边界控制详解
算法·leetcode·矩阵
变量未定义~1 小时前
虚拟节点-星石传送阵(4星)、强连通分量
数据结构·算法
Wang's Blog1 小时前
Go-Zero项目开发17: IM私聊功能实现与消息存储设计
开发语言·后端·golang
Ulyanov2 小时前
Python雷达电子对抗仿真引擎(一):打破单体瓶颈,构建微服务与ECS架构的顶层设计
开发语言·python·微服务·云原生·架构·雷达电子对抗
IT方大同2 小时前
C语言分支与循环语句
c语言·开发语言·算法
长不胖的路人甲2 小时前
斐波那契查找Java 实现 + 完整思路
java·开发语言
待磨的钝刨2 小时前
深入理解主成分分析(PCA)
人工智能·线性代数·算法·机器学习
星恒随风2 小时前
C++ 继承进阶:默认成员函数、多继承、虚继承与组合设计
开发语言·c++·笔记·学习