这两个系统调用已经被 epoll 全面超越。但理解它们——知道为什么该退场——有助于理解 I/O 多路复用的演进。
一、先看图
flowchart LR
subgraph select
FDS[fd_set<br/>位图 1024 bit] --> SCAN1[内核遍历<br/>每个 bit]
SCAN1 --> COPY1[copy_to_user<br/>修改后的 fd_set]
end
subgraph poll
POLLFD[struct pollfd 数组] --> SCAN2[内核遍历<br/>每个 pollfd]
SCAN2 --> COPY2[copy_to_user<br/>设置 revents]
end
subgraph epoll
ADD[epoll_ctl 一次] --> CB[回调驱动]
CB --> READY[就绪链表<br/>O(就绪数)]
end
classDef old fill:#f0883e22,stroke:#f0883e,color:#adbac7;
classDef new fill:#3fb95022,stroke:#3fb950,color:#adbac7;
class FDS,SCAN1,COPY1,POLLFD,SCAN2,COPY2 old
class ADD,CB,READY new
二、select
2.1 API
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
int n = select(sockfd + 1, &readfds, NULL, NULL, &timeout);
if (FD_ISSET(sockfd, &readfds)) { /* 可读 */ }2.2 问题
- fd_set 上限:
FD_SETSIZE默认 1024。fd >= 1024 → 未定义行为 - 每次调用:copy 整个 fd_set 到内核 → 内核遍历每个 bit → copy 回来
- O(n):n 是 maxfd,不是就绪 fd 数
- timeout 被修改(Linux 特有):返回后 timeout 变成剩余时间
2.3 内核实现
// fs/select.c
int core_sys_select(int n, fd_set __user *inp, ...)
{
// copy_from_user → 逐 fd poll → 无就绪则 schedule_timeout → 再试
}三、poll
3.1 API
struct pollfd fds[2];
fds[0].fd = sockfd;
fds[0].events = POLLIN;
int n = poll(fds, 2, timeout_ms);
if (fds[0].revents & POLLIN) { /* 可读 */ }3.2 改进
- 无 FD_SETSIZE 限制(数组长度自定义)
- 输入(events)和输出(revents)分离 → 不用每次重建
3.3 仍有的问题
- 每次调用:copy 整个 pollfd 数组到内核
- O(n):内核遍历每个 pollfd
- 万级 fd → 每次 poll 都是线性扫描
四、ppoll 与 pselect
4.1 信号安全
// 原子地设置 signal mask + poll
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *tmo_p, const sigset_t *sigmask);解决 select/poll 与 signal 的竞争:
// 错误模式
sigprocmask(SIG_BLOCK, &mask, &oldmask);
// ← 信号可能在这里到达
select(...); // 永远等待
// 正确模式
pselect(..., &mask); // 原子操作五、性能对比
| 方案 | 复杂度 | fd 上限 | 每次 syscall 开销 |
|---|---|---|---|
| select | O(maxfd) | 1024 | copy fd_set × 2 |
| poll | O(nfds) | 无硬限制 | copy pollfd[] × 2 |
| epoll | O(就绪数) | 无硬限制 | 无需 copy fd 集合 |
10K fd 场景:select/poll 每次 syscall 处理 10K fd;epoll 只处理就绪的几十个。
六、还有哪些代码该用 select/poll?
6.1 可以用的场景
- 跨平台代码(POSIX 要求 select/poll,epoll 是 Linux 专有)
- fd 数量极少(< 10)→ 开销可忽略
- 嵌入式系统无 epoll(某些 RTOS)
6.2 不该用的场景
- 服务器 → epoll
- 高并发 → epoll 或 io_uring
- 新 Linux 代码 → epoll
七、实现细节
7.1 do_select(select 内核路径)
// fs/select.c
static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{
for (;;) {
for (i = 0; i < n; i++) {
mask = vfs_poll(f.file, wait); // 逐 fd 检查
}
if (retval || timed_out || signal_pending(current))
break;
poll_schedule_timeout(wait, ...);
}
}7.2 do_poll(poll 内核路径)
类似,遍历 pollfd 数组。
八、历史
- 1983:select(4.2BSD)
- 1986:poll(SVR3)
- 2002:epoll(Linux 2.5.44)
- 2019:io_uring(Linux 5.1)
从 O(n) 到 O(就绪数) 到零 syscall——20 年的演进。
九、观察
strace -e select,poll,ppoll,pselect6 <command>
# 查看哪些进程还在用 select/poll
bpftrace -e 'tracepoint:syscalls:sys_enter_select { @[comm] = count(); }'十、小结
- select:fd_set 位图、1024 限制、O(maxfd)
- poll:pollfd 数组、无硬限制、仍 O(n)
- 都是每次 syscall copy + 遍历全量 → 高并发性能差
- 新代码用 epoll;跨平台/极少 fd 场景保留
参考文献
man 2 select、man 2 pollfs/select.c- Stevens & Rago, “Advanced Programming in the UNIX Environment.” ch14
- Banga et al., “Scalable Kernel Performance for Internet Servers Under Realistic Loads.” USENIX 1999
工具
straceltracebpftrace
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。
【操作系统百科】用户态分配器
glibc malloc、tcmalloc、jemalloc、mimalloc 各有哲学。本文讲 arena、thread cache、size class、madvise 返还策略、碎片与 RSS 膨胀、如何根据负载选分配器。