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 仍可能被使用,但性能较差。
相关推荐
AC赳赳老秦36 分钟前
OpenClaw+Power Apps 实战:自动生成 Power Apps 应用、连接 Excel 数据源
大数据·开发语言·python·serverless·excel·deepseek·openclaw
提笔了无痕39 分钟前
如何用Go实现整套RAG流程
开发语言·后端·golang
(Charon)41 分钟前
【C++ 面试高频基础:指针、引用、const、static、new/delete 总结】
java·开发语言
一只齐刘海的猫42 分钟前
【Leetcode】找到字符串中所有字母异位词
算法·leetcode·职场和发展
海清河晏1111 小时前
数据结构 | 八大排序
数据结构·算法·排序算法
2601_961875241 小时前
法考考试时间安排及科目|时间表|资料已整理
开发语言·c#·inverted-index·suffix-tree·sstable·r-tree·lsm-tree
AI科技星1 小时前
数术工坊第八卷:算力革命
c语言·开发语言·网络·量子计算·agi
geovindu2 小时前
go: Generators Pattern
开发语言·后端·设计模式·golang·生成器模式
IronMurphy2 小时前
【算法五十七】146. LRU 缓存
算法·缓存
凌波粒3 小时前
LeetCode--108.将有序数组转换为二叉搜索树(二叉树)
算法·leetcode·职场和发展