在poll()循环中调用close()是否合理?子进程大量输出时触发错误
问题描述
我用fork()/exec()/poll()实现了一段捕获子进程stdout和stderr的程序,整体运行正常,但当子进程输出大量内容时,执行以下代码会偶发“close: Bad file descriptor”错误(10次运行约5次出现):
if (close(pfds[j].fd) == -1) perror("close");
请问是否应该在poll()循环中保留这个close()调用?
完整代码如下:
int exec(char* argv1) { int pipefd_out[2], pipefd_err[2]; pipe(pipefd_out); pipe(pipefd_err); pid_t child_pid = fork(); if (child_pid == 0) { close(pipefd_out[0]); close(pipefd_err[0]); dup2(pipefd_out[1], STDOUT_FILENO); dup2(pipefd_err[1], STDERR_FILENO); // Prepared a few possible cases, to demo different behaviors if (atoi(argv1) == 0) { execl("./sub.out", "./sub.out", NULL); } else if (atoi(argv1) == 1) { const char *const args[] = {"./sub.out", "segfault", NULL}; execv(args[0], args); } else if (atoi(argv1) == 2) { const char *const args[] = {"./sub.out", "flooding", NULL}; execv(args[0], args); } else if (atoi(argv1) == 3) { const char *const args[] = {"/bin/ls", "-l", "/tmp/", NULL}; execv(args[0], args); } else { const char *const args[] = {"/bin/ls", "-l", "/path/that/definitely/does/not/exist/", NULL}; execv(args[0], args); } perror("execl()/execv()"); _exit(EXIT_FAILURE); } close(pipefd_out[1]); close(pipefd_err[1]); struct pollfd pfds[] = { { pipefd_out[0], POLLIN, 0 }, { pipefd_err[0], POLLIN, 0 }, }; int nfds = sizeof(pfds) / sizeof(struct pollfd); int num_open_fds = nfds; while (num_open_fds > 0) { int ready = poll(pfds, nfds, -1); if (ready == -1) perror("poll()"); for (int j = 0; j < nfds; j++) { if (pfds[j].revents != 0) { char buf[4096] = {0}; if (pfds[j].revents & POLLIN) { ssize_t s = read(pfds[j].fd, buf, sizeof(buf)-1); if (s == -1) perror("read()"); if (j == 0) { printf("<stdout>%s</stdout>\n", buf); } else { printf("<stderr>%s</stderr>\n", buf); } fflush(stdout); } else { /* POLLERR | POLLHUP */ if (close(pfds[j].fd) == -1) perror("close"); num_open_fds--; } } } } int status; waitpid(child_pid, &status, 0); if (WIFEXITED(status)) { printf("Child process exited normally, rc: %d\n", WEXITSTATUS(status)); } else { printf("Child process exited unexpectedly\n"); } return EXIT_SUCCESS; } int main(int argc, char** argv) { if (argc != 2) { printf("Usage: %s <0|1|2|3|4>\n", argv[0]); return EXIT_FAILURE; } return exec(argv[1]); }
分析与解决方案
错误根源
偶发的EBADF错误,核心是同一个文件描述符被重复关闭。当子进程输出大量数据时,poll()可能同时返回POLLIN和POLLHUP事件——管道中还有未读数据,但子进程已经关闭了写端。你的代码逻辑会:
- 先处理
POLLIN,读取剩余数据 - 循环继续检查
revents,此时POLLHUP仍存在,进入else分支执行close - 但后续
poll会再次触发该fd的POLLHUP(因为你没标记fd已关闭,也没从pfds中移除),导致重复关闭已失效的fd,触发错误。
是否保留close()?
必须保留,但要修复重复关闭的问题。关闭管道读端是必要的:
- 不关闭会导致管道资源无法及时释放
- 后续系统复用文件描述符时,可能引发意外问题
修复方案
关闭fd后,标记该fd已失效,避免后续循环重复处理:
else { /* POLLERR | POLLHUP */ if (pfds[j].fd != -1) { // 先检查fd是否有效 if (close(pfds[j].fd) == -1) perror("close"); pfds[j].fd = -1; // 标记为已关闭,poll会忽略该fd num_open_fds--; } }
额外优化点
1. 处理read()返回EOF的情况
当read()返回0时,说明子进程已关闭写端,此时也需要关闭读端并标记:
ssize_t s = read(pfds[j].fd, buf, sizeof(buf)-1); if (s == -1) { perror("read()"); } else if (s == 0) { // 读到EOF,关闭读端 if (pfds[j].fd != -1) { if (close(pfds[j].fd) == -1) perror("close"); pfds[j].fd = -1; num_open_fds--; } } else { buf[s] = '\0'; // 确保字符串以空字符结尾,避免乱码 if (j == 0) { printf("<stdout>%s</stdout>\n", buf); } else { printf("<stderr>%s</stderr>\n", buf); } fflush(stdout); }
2. 处理poll()被信号打断的情况
poll()返回-1时,可能是被系统信号打断(比如SIGCHLD),此时应该重新调用poll()而非直接退出:
int ready = poll(pfds, nfds, -1); if (ready == -1) { if (errno == EINTR) continue; // 被信号打断,重新执行poll perror("poll()"); break; }
内容的提问来源于stack exchange,提问作者D.J. Elkind
相关产品推荐
相关产品推荐

