处理Socket与SIGALRM时select()持续中断进入无限循环的原因?
首先,咱们来拆解你遇到的两个核心问题:为什么FD_ISSET在select被信号中断后返回true,以及为什么会陷入无限循环。
1. 为什么FD_ISSET会误报socket就绪?
当select被信号中断并返回EINTR时,POSIX标准明确规定此时fd_set的内容是未定义的。也就是说,内核不会更新fd_set的状态——它还是你调用select之前用FD_SET设置的样子。你的代码每次循环都会把master_sockfd加入readfds,所以当select被中断返回时,fd_set里的master_sockfd位仍然是1,导致FD_ISSET错误地返回true,但这并不代表socket真的有可读事件。
2. 为什么会陷入无限循环?
你的测试步骤是先发送SIGALRM,再用nc连接。此时master_sockfd确实有连接请求等待处理,但第一次select被信号中断,没有处理这个请求。当你重新进入循环调用select时,本应立即返回并处理连接,但你看到的却是无限的EINTR错误,这大概率是因为:
- 你可能误操作发送了多次
SIGALRM信号(比如命令重复执行),导致每次select都被新的信号中断; - 更关键的是,你的代码在
EINTR分支里错误地检查了FD_ISSET,虽然这个检查结果不可信,但你还是continue进入下一次循环。如果此时socket确实有可读事件,select本应正常返回,但如果信号持续到来,就会不断触发EINTR,形成循环。
另外,还有一个隐藏的坑:你在信号处理函数里调用了printf,这是不可重入函数,在信号上下文里调用可能导致未定义行为(比如破坏全局状态、修改errno等),虽然这不一定是本次循环的直接原因,但也是需要修正的问题。
解决办法
针对这些问题,你可以做以下调整:
方法一:忽略EINTR时的fd_set,直接重启select
当select返回EINTR时,不要去检查fd_set的状态,直接重新进入循环调用select即可,因为此时的fd_set结果不可信。修改后的代码分支如下:
else if (errno == EINTR) { perror("select() "); // 不要检查FD_ISSET,直接continue重启select continue; }
方法二:使用sigaction设置信号自动重启系统调用
如果你希望系统调用被信号中断后自动重启,而不是返回EINTR,可以用sigaction代替signal(),并设置SA_RESTART标志。这样select会在信号处理函数返回后自动继续等待,不需要你手动重启:
#include <signal.h> #include <unistd.h> void handle_alarm(int sig) { // 改用write替代printf,避免不可重入函数 const char msg[] = "handling alarm\n"; write(STDOUT_FILENO, msg, sizeof(msg)-1); } int main() { // ... 其他代码 ... struct sigaction sa; sa.sa_handler = handle_alarm; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; // 自动重启被中断的系统调用 sigaction(SIGALRM, &sa, NULL); // ... 循环代码 ... }
方法三:避免在信号处理函数中调用不可重入函数
像printf、malloc这类函数都是不可重入的,在信号处理函数中调用可能导致数据损坏或其他异常。建议改用write这类可重入的系统调用输出信息。
总结
你的核心问题是误解了select返回EINTR时fd_set的状态,错误地进行了FD_ISSET检查。同时,信号处理函数中的不可重入调用也存在潜在风险。通过上述调整,应该能解决无限循环的问题,正确处理信号和socket连接。
内容的提问来源于stack exchange,提问作者snailsuperperson

