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

无法修改子进程管道缓冲区大小:非阻塞读取tcpdump输出问题

问题描述

我希望运行tcpdump持续5秒并获取其stdout输出。问题在于,当我使用非阻塞模式读取时,必须等待缓冲区填满至4096字节才能获取到tcpdump的stdout内容。我已多次搜索并尝试多种方法,但仍未解决问题。请指出我的错误所在,以及如何在非阻塞模式下读取子进程的stdout,无需等待缓冲区填满?

附当前代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>

#define PIPE_READ 0
#define PIPE_WRITE 1

// 将文件描述符设置为非阻塞模式
void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL");
        exit(1);
    }
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl F_SETFL");
        exit(1);
    }
}

int main() {
    int stdout_pipe[2], stderr_pipe[2];
    pid_t pid;

    // 创建两个管道:一个用于stdout,一个用于stderr
    if (pipe(stdout_pipe) == -1) {
        perror("pipe stdout");
        exit(1);
    }
    if (pipe(stderr_pipe) == -1) {
        perror("pipe stderr");
        exit(1);
    }

    // 分叉进程
    pid = fork();

    if (pid == -1) {
        // 分叉进程出错
        perror("fork");
        exit(1);
    }

    if (pid == 0) {  // 子进程
        // 关闭子进程中未使用的管道端
        close(stdout_pipe[PIPE_READ]);
        close(stderr_pipe[PIPE_READ]);

        // 将stdout和stderr设置为无缓冲模式
        setvbuf(stdout, NULL, _IONBF, 0);  // 无缓冲stdout
        setvbuf(stderr, NULL, _IONBF, 0);  // 无缓冲stderr

        // 将stdout和stderr重定向到管道
        dup2(stdout_pipe[PIPE_WRITE], STDOUT_FILENO);
        dup2(stderr_pipe[PIPE_WRITE], STDERR_FILENO);

        // 关闭管道写入端,因为已完成重定向
        close(stdout_pipe[PIPE_WRITE]);
        close(stderr_pipe[PIPE_WRITE]);

        // 子进程运行tcpdump(示例)
        execlp("timeout", "timeout", "5s", "tcpdump", "src", "port", "41972", (char *)NULL);

        // 如果execlp执行失败
        perror("execlp");
        exit(1);
    } else {  // 父进程
        char buffer[256];
        int status;
        ssize_t nbytes;

        // 关闭父进程中未使用的管道端
        close(stdout_pipe[PIPE_WRITE]);
        close(stderr_pipe[PIPE_WRITE]);

        // 将两个管道文件描述符设置为非阻塞模式
        set_nonblocking(stdout_pipe[PIPE_READ]);
        set_nonblocking(stderr_pipe[PIPE_READ]);

        // 父进程以非阻塞模式从管道读取内容
        printf("Parent is reading from child's stdout:\n");
        while (1) {
            // 尝试从stdout管道读取
            nbytes = read(stdout_pipe[PIPE_READ], buffer, sizeof(buffer) - 1);
            if (nbytes > 0) {
                buffer[nbytes] = '\0';  // 字符串末尾添加空字符
                printf("%s", buffer);
            } else if (nbytes == 0) {
                // 到达EOF(子进程已关闭其管道端)
                break;
            } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
                // 处理其他错误
                perror("read stdout");
                break;
            }

            // 尝试从stderr管道读取
            nbytes = read(stderr_pipe[PIPE_READ], buffer, sizeof(buffer) - 1);
            if (nbytes > 0) {
                buffer[nbytes] = '\0';  // 字符串末尾添加空字符
                printf("%s", buffer);
            } else if (nbytes == 0) {
                // 到达EOF(子进程已关闭其管道端)
                break;
            } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
                // 处理其他错误
                perror("read stderr");
                break;
            }

            // 短暂休眠以避免死循环(可按需调整)
            usleep(100000); // 休眠100毫秒
        }

        // 等待子进程退出
        waitpid(pid, &status, 0);

        // 关闭父进程中的管道读取端
        close(stdout_pipe[PIPE_READ]);
        close(stderr_pipe[PIPE_READ]);
    }

    return 0;
}

错误分析与解决方案

核心错误原因

  1. setvbuf调用时机错误:你在子进程中先调用setvbuf设置无缓冲,之后才用dup2重定向stdout/stderr。setvbuf是针对原有的stdout流生效的,重定向后新的文件描述符对应的流并没有被设置为无缓冲,等于白做。
  2. tcpdump本身的缓冲策略:默认情况下,当stdout指向管道时,tcpdump会使用全缓冲(4096字节),不会实时输出内容,必须等缓冲区满才会写入管道。

解决步骤与修改后的代码

修改后的代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>

#define PIPE_READ 0
#define PIPE_WRITE 1

void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL");
        exit(1);
    }
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl F_SETFL");
        exit(1);
    }
}

int main() {
    int stdout_pipe[2], stderr_pipe[2];
    pid_t pid;
    int stdout_eof = 0, stderr_eof = 0;

    if (pipe(stdout_pipe) == -1) {
        perror("pipe stdout");
        exit(1);
    }
    if (pipe(stderr_pipe) == -1) {
        perror("pipe stderr");
        exit(1);
    }

    pid = fork();

    if (pid == -1) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {  // 子进程
        close(stdout_pipe[PIPE_READ]);
        close(stderr_pipe[PIPE_READ]);

        // 先重定向,再设置缓冲
        dup2(stdout_pipe[PIPE_WRITE], STDOUT_FILENO);
        dup2(stderr_pipe[PIPE_WRITE], STDERR_FILENO);

        close(stdout_pipe[PIPE_WRITE]);
        close(stderr_pipe[PIPE_WRITE]);

        // 重定向后设置stdout和stderr为无缓冲
        setvbuf(stdout, NULL, _IONBF, 0);
        setvbuf(stderr, NULL, _IONBF, 0);

        // 添加-l参数让tcpdump强制行缓冲输出
        execlp("timeout", "timeout", "5s", "tcpdump", "-l", "src", "port", "41972", (char *)NULL);

        perror("execlp");
        exit(1);
    } else {  // 父进程
        char buffer[256];
        int status;
        ssize_t nbytes;

        close(stdout_pipe[PIPE_WRITE]);
        close(stderr_pipe[PIPE_WRITE]);

        set_nonblocking(stdout_pipe[PIPE_READ]);
        set_nonblocking(stderr_pipe[PIPE_READ]);

        printf("Parent is reading from child's stdout:\n");
        while (!stdout_eof || !stderr_eof) {
            // 读取stdout
            if (!stdout_eof) {
                nbytes = read(stdout_pipe[PIPE_READ], buffer, sizeof(buffer) - 1);
                if (nbytes > 0) {
                    buffer[nbytes] = '\0';
                    printf("%s", buffer);
                } else if (nbytes == 0) {
                    stdout_eof = 1;
                } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
                    perror("read stdout");
                    stdout_eof = 1;
                }
            }

            // 读取stderr
            if (!stderr_eof) {
                nbytes = read(stderr_pipe[PIPE_READ], buffer, sizeof(buffer) - 1);
                if (nbytes > 0) {
                    buffer[nbytes] = '\0';
                    printf("%s", buffer);
                } else if (nbytes == 0) {
                    stderr_eof = 1;
                } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
                    perror("read stderr");
                    stderr_eof = 1;
                }
            }

            // 非阻塞检查子进程是否退出,避免无限等待
            if (waitpid(pid, &status, WNOHANG) > 0) {
                // 子进程退出后,读取剩余的输出数据
                while (!stdout_eof || !stderr_eof) {
                    if (!stdout_eof) {
                        nbytes = read(stdout_pipe[PIPE_READ], buffer, sizeof(buffer) - 1);
                        if (nbytes > 0) {
                            buffer[nbytes] = '\0';
                            printf("%s", buffer);
                        } else if (nbytes == 0) {
                            stdout_eof = 1;
                        } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
                            perror("read stdout after child exit");
                            stdout_eof = 1;
                        }
                    }
                    if (!stderr_eof) {
                        nbytes = read(stderr_pipe[PIPE_READ], buffer, sizeof(buffer) - 1);
                        if (nbytes > 0) {
                            buffer[nbytes] = '\0';
                            printf("%s", buffer);
                        } else if (nbytes == 0) {
                            stderr_eof = 1;
                        } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
                            perror("read stderr after child exit");
                            stderr_eof = 1;
                        }
                    }
                }
                break;
            }

            usleep(100000);
        }

        close(stdout_pipe[PIPE_READ]);
        close(stderr_pipe[PIPE_READ]);
    }

    return 0;
}

关键修改说明

  1. 给tcpdump添加-l参数:这是最核心的修改,该参数强制tcpdump使用行缓冲模式,每捕获到一条数据包就立即输出一行内容,不再等待缓冲区填满。
  2. 调整setvbuf调用顺序:先完成dup2重定向,再设置流的缓冲模式,确保设置对重定向后的stdout/stderr生效。
  3. 优化父进程循环逻辑:
    • 增加stdout_eof和stderr_eof标记,只有当两个管道都到达EOF时才退出循环,避免提前终止读取。
    • 加入waitpid的非阻塞检查,子进程退出后继续读取剩余的输出数据,防止内容丢失。

内容的提问来源于stack exchange,提问作者Mustafa Chelik

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 00:19:51