使用select复用匿名管道与其他文件描述符时的无限读取问题
解决select监听匿名管道时无限触发读取事件的问题
这个问题我之前排查过好几次,核心是你没处理管道读端遇到EOF的场景,咱们来一步步理清楚问题出在哪,以及怎么修复:
问题根源分析
当子进程写完管道数据并关闭写端后,父进程的管道读端(pfd[0])在读完所有数据后,后续的read()调用会直接返回0(这是管道写端关闭的标志,也就是EOF)。但你的代码里既没有判断这个返回值,也没有把pfd[0]从master文件描述符集合中移除,导致select()会一直认为这个fd是可读的,进而无限触发读取逻辑,形成死循环。
另外还有个小问题:你把close(pfd[1]);放在了每次管道读取的分支里,这完全没必要——父进程从一开始就不需要写管道,应该在fork之后立刻关闭父进程的写端。
修正后的代码
#include <iostream> #include <stdlib.h> #include <algorithm> #include <stdio.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <string.h> using namespace std; int main(){ fd_set master; // master file descriptor list fd_set read_fds; // temp file descriptor list for select() FD_ZERO(&master); // clear the master and temp sets FD_ZERO(&read_fds); int fdmax; // maximum file descriptor number int pfd[2]; if(pipe(pfd)!=0) { cout<<"Unable to create a pipe.\n"; exit(1); }; FD_SET(0, &master); FD_SET(pfd[0],&master); fdmax = max(pfd[0], 0); // 确保fdmax是当前最大的fd值 pid_t pid = fork(); if(pid > 0){ // Parent // 父进程不需要写管道,立刻关闭写端 close(pfd[1]); for (;;){ read_fds = master; // 复制master集合到临时集合 if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) { perror("select"); exit(4); } for(int i = 0; i <= fdmax; i++) { if (FD_ISSET(i, &read_fds)) { int n; char buff[200]; if (i == pfd[0]){ n = read(pfd[0], buff, sizeof(buff)-1); // 留一个位置给字符串终止符 if (n == 0) { // 管道写端已关闭,移除该fd并关闭读端 FD_CLR(pfd[0], &master); close(pfd[0]); // 如果当前关闭的是最大fd,更新fdmax if (pfd[0] == fdmax) { fdmax = -1; for(int j=0; j <= pfd[0]; j++){ if(FD_ISSET(j, &master) && j > fdmax){ fdmax = j; } } } cout<<"Pipe closed, stop listening to it\n"; continue; } else if (n < 0) { perror("read pipe"); FD_CLR(pfd[0], &master); close(pfd[0]); continue; } buff[n] = 0; cout<<"Read from pipe:"<<buff<<endl; } else if(i == 0){ n = read(0, buff, sizeof(buff)-1); if (n == 0) { // stdin关闭,移除并关闭fd FD_CLR(0, &master); close(0); if (0 == fdmax) { fdmax = -1; for(int j=0; j <= 0; j++){ if(FD_ISSET(j, &master) && j > fdmax){ fdmax = j; } } } cout<<"Stdin closed\n"; continue; } else if (n < 0) { perror("read stdin"); FD_CLR(0, &master); close(0); continue; } buff[n] = 0; cout<<"Read from std:"<<buff<<endl; } } } // 没有需要监听的fd时退出循环 if (fdmax == -1) { cout<<"No more fds to listen, exit\n"; break; } } } else if(pid == 0){ // Child usleep(50000); char buff[200] = "This is a simple sample."; close(pfd[0]); // 子进程不需要读管道,关闭读端 write(pfd[1], buff, strlen(buff)); // 只写入有效字符串内容,避免多余空字符 close(pfd[1]); exit(0); } else { perror("fork"); exit(2); } return 0; }
关键修改点说明
- 父进程fork后立刻关闭管道写端
pfd[1],避免资源泄漏 - 严格处理
read()的返回值:- 当
n == 0时,说明对应fd的写端已关闭,将该fd从master集合中移除并关闭,同时更新fdmax(如果该fd是当前最大值) - 当
n < 0时,处理读错误场景,同样移除并关闭fd
- 当
- 子进程写管道时,用
strlen(buff)代替sizeof(buff),避免写入数组中多余的空字符 - 增加
fdmax的动态更新逻辑,确保select()的参数始终正确 - 当没有需要监听的fd时主动退出循环,避免无意义的空转
内容的提问来源于stack exchange,提问作者geradism
相关产品推荐
相关产品推荐

