You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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事件——管道中还有未读数据,但子进程已经关闭了写端。你的代码逻辑会:

  1. 先处理POLLIN,读取剩余数据
  2. 循环继续检查revents,此时POLLHUP仍存在,进入else分支执行close
  3. 但后续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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 22:22:08