【问题标题】:Why does popen2() hang between write and read calls?为什么 popen2() 在 write 和 read 调用之间挂起?
【发布时间】:2025-12-19 13:55:07
【问题描述】:

我正在尝试将 samtools 的使用集成到 C 程序中。此application 以称为 BAM 的二进制格式读取数据,例如来自stdin

$ cat foo.bam | samtools view -h -
...

(我意识到这是对 cat 的无用使用,但我只是展示了如何在命令行上将 BAM 文件的字节通过管道传输到 samtools。这些字节可能来自其他上游进程。)

在 C 程序中,我想将 unsigned char 字节块写入 samtools 二进制文件,同时在处理这些字节后从 samtools 捕获标准输出。

因为我不能使用popen() 同时对进程进行读写,所以我研究了使用popen2() 的公开可用实现,它似乎是为了支持这一点而编写的。

我编写了以下测试代码,它尝试将位于同一目录中的 BAM 文件的 write() 4 kB 块字节分配给 samtools 进程。然后它从samtools 的输出中将read()s 个字节放入行缓冲区,打印到标准错误:

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

#define READ 0
#define WRITE 1

pid_t popen2(const char *command, int *infp, int *outfp)
{
    int p_stdin[2], p_stdout[2];
    pid_t pid;

    if (pipe(p_stdin) != 0 || pipe(p_stdout) != 0)
        return -1;

    pid = fork();

    if (pid < 0)
        return pid;
    else if (pid == 0)
    {
        close(p_stdin[WRITE]);
        dup2(p_stdin[READ], READ);
        close(p_stdout[READ]);
        dup2(p_stdout[WRITE], WRITE);

        execl("/bin/sh", "sh", "-c", command, NULL);
        perror("execl");
        exit(1);
    }

    if (infp == NULL)
        close(p_stdin[WRITE]);
    else
        *infp = p_stdin[WRITE];

    if (outfp == NULL)
        close(p_stdout[READ]);
    else
        *outfp = p_stdout[READ];

    return pid;
}

int main(int argc, char **argv)
{
    int infp, outfp;

    /* set up samtools to read from stdin */
    if (popen2("samtools view -h -", &infp, &outfp) <= 0) {
        printf("Unable to exec samtools\n");
        exit(1);
    }

    const char *fn = "foo.bam";
    FILE *fp = NULL;
    fp = fopen(fn, "r");
    if (!fp)
        exit(-1);
    unsigned char buf[4096];
    char line_buf[65536] = {0};
    while(1) {
        size_t n_bytes = fread(buf, sizeof(buf[0]), sizeof(buf), fp);
        fprintf(stderr, "read\t-> %08zu bytes from fp\n", n_bytes);
        write(infp, buf, n_bytes);
        fprintf(stderr, "wrote\t-> %08zu bytes to samtools process\n", n_bytes);
        read(outfp, line_buf, sizeof(line_buf));
        fprintf(stderr, "output\t-> \n%s\n", line_buf);
        memset(line_buf, '\0', sizeof(line_buf));
        if (feof(fp) || ferror(fp)) {
            break;
        }
    }
    return 0;
}

(对于foo.bam 的本地副本,这是我用于测试的二进制文件的link。但任何 BAM 文件都可以用于测试目的。)

编译:

$ cc -Wall test_bam.c -o test_bam

问题是程序在write() 调用后挂起:

$ ./test_bam
read    -> 00004096 bytes from fp
wrote   -> 00004096 bytes to samtools process
[bam_header_read] EOF marker is absent. The input is probably truncated.

如果我在write() 调用之后立即close() infp 变量,则循环在挂起之前再进行一次迭代:

...
write(infp, buf, n_bytes);
close(infp); /* <---------- added after the write() call */
fprintf(stderr, "wrote\t-> %08zu bytes to samtools process\n", n_bytes);
...

使用close() 声明:

$ ./test_bam
read    -> 00004096 bytes from fp
wrote   -> 00004096 bytes to samtools process
[bam_header_read] EOF marker is absent. The input is probably truncated.
[main_samview] truncated file.
output  -> 
@HD VN:1.0 SO:coordinate
@SQ SN:seq1 LN:5000
@SQ SN:seq2 LN:5000
@CO Example of SAM/BAM file format.

read    -> 00004096 bytes from fp
wrote   -> 00004096 bytes to samtools process

通过此更改,如果我在命令行上运行samtools,我会得到一些我希望得到的输出,但如前所述,该过程再次挂起。

如何使用popen2() 将数据以块的形式写入和读取到内部缓冲区?如果无法做到这一点,是否有替代 popen2() 更适合此任务的方法?

【问题讨论】:

    标签: c popen samtools


    【解决方案1】:

    作为pipe 的替代方案,为什么不通过socketsamtools 通信?检查samtools 源,文件knetfile.c 表明samtools 有可用的套接字通信:

    #include "knetfile.h"
    
    /* In winsock.h, the type of a socket is SOCKET, which is: "typedef
    * u_int SOCKET". An invalid SOCKET is: "(SOCKET)(~0)", or signed
    * integer -1. In knetfile.c, I use "int" for socket type
    * throughout. This should be improved to avoid confusion.
    *
    * In Linux/Mac, recv() and read() do almost the same thing. You can see
    * in the header file that netread() is simply an alias of read(). In
    * Windows, however, they are different and using recv() is mandatory.
    */
    

    这可能提供比使用pipe2 更好的选择。

    【讨论】:

    • 我认为这需要将 samtools 头文件和库导入到这个程序中。我看不到在命令行上运行此二进制文件以将其置于某种可以通过套接字接受连接的守护程序模式的方法。我会寻找一种通过文件流来做到这一点的方法,这样我就可以将二进制文件用作依赖项。
    • 很公平。我看到了套接字的可用性,并认为我会提供它作为解决方案。
    • 对不起,如果这听起来像是我忘恩负义——我真的不是!我很感激所有的建议。我只是想澄清我要解决的问题。
    【解决方案2】:

    这个问题与popen2的具体实现无关。另请注意,在 OS X 上,popen 允许您打开双向管道,这在其他 BSD 系统上可能也是如此。如果这是可移植的,您需要配置检查 popen 是否允许双向管道(或等效于配置检查)。

    您需要将管道切换到非阻塞模式,并在无限循环中交替调用readwrite。这样的循环,为了在samtools进程繁忙时不浪费CPU,需要使用selectpoll或类似的机制来阻止文件描述符变为“可用”(读取更多数据,或准备接受数据写入)。

    请参阅this question 以获得一些灵感。

    【讨论】:

    • 我肯定在寻找便携式解决方案。我不确定popen 是否会起作用,如果规格与描述的一样。
    • @AlexReynolds 事实上的可移植解决方案是使用popen2(如果平台提供),或者使用popen(如果它在给定平台上是双向的),或者使用您自己的popen2(如果这些都不是)工作。然后你切换到非阻塞处理,在宇宙中一切都很好:) 配置检查是必须的,在某些化身中。您可以简单地描述所有支持的平台并使用静态配置头来检测平台并激活正确的实现。这就是许多只有头文件的 C++ 库所做的事情,并且像 sqlite 合并一样“编译”C 库。
    • 您知道如何实现与我原来使用popen2() 的问题相关的阻塞机制吗?看起来在写描述符上设置一个非阻塞标志会有所帮助,但有些数据会丢失。您链接到的问题在这里没有提供太多帮助。我感谢任何细节。
    • @AlexReynolds “有些数据丢失了” - 嗯?如何?为什么?您需要做的就是编写第二段所说的内容。有几十行。不是非阻塞标志“有帮助”,不使用非阻塞访问或多线程就不可能做你想做的事情。
    最近更新 更多