Почему 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
или подобный механизм, который блокирует дескриптор файла, чтобы он стал "доступным" (больше данных для чтения или готово принять данные для записи).
Смотрите этот вопрос для вдохновения.