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)内核处理逻辑
-
用户调用 :
cppstruct pollfd fds[1000]; for (int i = 0; i < 1000; i++) { fds[i].fd = ...; fds[i].events = POLLIN; // 只关心可读事件 } poll(fds, 1000, timeout); -
内核行为 :
- 单次遍历 :内核直接遍历
fds数组(1000 次)。 - 直接标记 :
- 对每个
fds[i],内核检查fd的状态。 - 如果事件发生(如可读),则将
revents设置为对应标志(如POLLIN)。 - 无需额外遍历或位操作。
- 对每个
- 单次遍历 :内核直接遍历
-
用户检查 :
cppfor (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:强制内核遍历0到nfds-1的所有 fd,即使未被监听。
(2)内核处理逻辑
-
用户调用 :
cppfd_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); -
内核行为 :
- 拷贝位图 :将
read_fds、write_fds、except_fds从用户空间拷贝到内核。 - 三次遍历 :
- 遍历
read_fds:- 对每个
fd(从0到1002):- 如果
FD_ISSET(fd, &read_fds)为真,检查是否可读。 - 如果可读,保留该 fd 在位图中(内核可能直接修改位图)。
- 如果
- 对每个
- 遍历
write_fds(同理)。 - 遍历
except_fds(同理)。
- 遍历
- 无效操作 :
- 即使
fd=0,1,2未被设置,内核仍会检查它们(共 3 次无效操作)。 - 如果 fd 数量更多(如 10,000),无效操作会线性增加。
- 即使
- 拷贝位图 :将
-
用户检查 :
cppfor (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) | 有(必须检查 0 到 nfds-1) |
| 位图操作 | 无 | 每次检查需 FD_ISSET 位操作 |
| 支持 fd 数量 | 无限制(仅受系统资源限制) | 受 FD_SETSIZE 限制(通常 1024) |
4. 为什么 poll 更高效?
- 单次遍历 vs 三次遍历 :
poll只需遍历用户提供的 fd 列表一次,而select需要遍历三个位图各一次。
- 直接标记 vs 位图操作 :
poll的revents直接填充事件标志,无需位操作。select需通过FD_ISSET检查位图,额外开销大。
- 无效操作优化 :
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. 总结
poll的revents设计 :
通过单次遍历和直接标记事件,避免了select的三次遍历和位图操作,因此性能更高。select的局限性 :
全局扫描和位图操作使其在处理大量 fd 时效率低下,且受FD_SETSIZE限制。- 适用场景 :
- 高并发(大量 fd)时,优先使用
poll(或更现代的epoll/kqueue)。 - 低并发或遗留代码中,
select仍可能被使用,但性能较差。
- 高并发(大量 fd)时,优先使用