Почему popen2() зависает между вызовами записи и чтения?

Я пытаюсь интегрировать использование samtools в программу Си. Это приложение считывает данные в двоичном формате, называемом BAM, например, из stdin:

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

(Я понимаю, что это бесполезное использование cat, но я просто показываю, как байты файла BAM могут быть переданы samtools в командной строке. Эти байты могут поступать из других процессов верхнего уровня.)

В рамках программы на C я хотел бы написать куски unsigned char байтов в samtools двоичный, одновременно захватывая стандартный вывод из samtools после того, как он обрабатывает эти байты.

Потому что я не могу использовать popen() чтобы одновременно писать и читать из процесса, я изучил использование общедоступных реализаций popen2(), который, кажется, написан, чтобы поддержать это.

Я написал следующий тестовый код, который пытается write() 4 байт кусков байта файла BAM, расположенного в том же каталоге, samtools процесс. Тогда read()с байтов из вывода samtools в строковый буфер, напечатанный со стандартной ошибкой:

#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Вот ссылка на двоичный файл, который я использую для тестирования. Но любой файл 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.

Если я close() infp переменная сразу после write() вызовите, затем цикл проходит еще одну итерацию перед зависанием:

...
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() что будет работать лучше для этой задачи?

2 ответа

В качестве альтернативы pipeпочему бы не общаться с samtools через socket? Проверка 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,

Эта проблема не имеет ничего общего с конкретной реализацией popen2, Также обратите внимание, что в OS X popen позволяет открыть двунаправленный канал, это может быть верно и для других систем BSD. Если это должно быть переносимым, вам нужно проверить конфигурацию для popen разрешает двунаправленные каналы (или что-то эквивалентное проверке конфигурации).

Вам нужно переключить трубы в неблокирующий режим и переключаться между read а также write звонки в бесконечном цикле. Такой цикл, чтобы не тратить процессор при samtools процесс занят, нужно использовать select, poll или подобный механизм, который блокирует дескриптор файла, чтобы он стал "доступным" (больше данных для чтения или готово принять данные для записи).

Смотрите этот вопрос для вдохновения.

Другие вопросы по тегам