select系统调用与其他描述符混用未触发stdin就绪检测问题
问题:select函数为何出现异常分支不触发的情况?
以下是一段C++代码:
int main() { fd_set readfds; fd_set writefds; FD_ZERO(&readfds); FD_ZERO(&writefds); FD_SET(0, &readfds); int fd = open("random_path", O_WRONLY); FD_SET(fd, &writefds); struct timeval tv; tv.tv_sec = 10; tv.tv_usec = 0; int r = 0; string s; int i = 0; // Running loop 5 times while (i<5) { r = select(fd+1, &readfds, &writefds, NULL, &tv); cout<<"r is "<<r<<endl; if (r == -1) { cout<<"error"<<endl; exit(1); } if (FD_ISSET(0, &readfds)) { cout<<"reading from stdin"<<endl; cin>>s; cout<<"s is "<<s<<endl; } if (FD_ISSET(fd, &writefds)) { cout<<"writing to file"<<endl; write(fd, s.c_str(), s.length()); } cout<<"sleeping"<<endl; sleep(5); i += 1; } }
运行该代码后,输出如下:
r is 1 writing to file sleeping random text to set stdin fd r is 1 writing to file sleeping
可见代码从未进入FD_ISSET(0, &readfds)分支,未打印"reading from stdin"。但将select第一个参数改为小于fd的值(如1或2)时,可正常进入该分支,输出如下:
qqq r is 1 reading from stdin s is qqq sleeping
请问为何会出现此异常现象?
原因分析
核心问题出在select函数的第一个参数nfds的定义以及文件描述符集合fd_set在select调用后的状态变化上:
- 首先明确
nfds的作用:它指定了select需要检查的文件描述符范围——所有编号小于nfds的描述符都会被纳入检查。当你传入fd+1时,select会检查从0到fd的所有描述符。 - 你通过
open得到的fd编号必然大于1(0、1、2是标准输入、输出、错误的固定描述符),而普通文件的写描述符几乎总是处于就绪状态(因为操作系统对普通文件的写操作不会阻塞)。 - 最关键的错误:select会修改传入的
fd_set集合——它会清空集合中未就绪的描述符位,只保留就绪的。你的代码在循环中没有重新初始化readfds和writefds:第一次调用select后,标准输入(0号)未就绪,readfds里的0号位被清空;而fd的写位因为一直就绪被保留。 - 后续循环调用select时,因为
nfds是fd+1,select会优先检测到fd的写就绪状态,直接返回1,进入写分支。此时readfds里的0号位已经被之前的select清空,即便后续你在标准输入输入内容,也不会触发FD_ISSET(0, &readfds)的判断。 - 当你把
nfds改成小于fd的值(比如1)时,select只会检查编号小于1的描述符(也就是只有0号)。这时候,即使readfds之前被修改,当你输入内容让0号就绪时,select会标记它,触发读分支;同时因为不检查fd的写状态,不会每次都直接返回写就绪的结果。
总结你代码里的两个关键错误:
- 没有在每次循环调用select前,重新用
FD_ZERO和FD_SET初始化fd_set集合,导致后续循环的集合状态错误。 - 普通文件的写描述符持续就绪,当
nfds包含它时,会抢占select的返回结果,掩盖了标准输入的就绪检查。
正确的做法是,在每次循环的开头重新初始化readfds、writefds以及timeval结构体(因为select也会修改timeout参数)。
内容的提问来源于stack exchange,提问作者Shivanshu Arora
相关产品推荐
相关产品推荐

