Linux下C程序检测FFmpeg管道EOF:read阻塞无法退出的求助
问题
在Linux环境下编写C程序,需要捕获FFmpeg的输出数据并转发。通过自定义的popen2实现调用FFmpeg,目前可以通过read()从管道获取数据,但当FFmpeg停止运行时,read()会一直阻塞,无法检测到EOF。
已知read()检测EOF需要管道两端都关闭,因此当检测到FFmpeg崩溃或被终止时,关闭了管道的读端,但程序仍卡在read()中。
请问假设哪里有误?如何正确检测管道另一端(如FFmpeg)停止发送数据?
相关代码
#include <stdlib.h> #include <string.h> #include <signal.h> #include <unistd.h> #include <pthread.h> #include <ctype.h> #include <fcntl.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/wait.h> #include "helper.h" #define READ 0 #define WRITE 1 FILE *sink_stream; int ffmpeg_sink; pid_t popen2_2(const char **command, int *infp, int *outfp); void* pthread_sink_ffmpeg(void *arg); pid_t popen2_2(const char **command, int *infp, int *outfp) { int p_stdin[2], p_stdout[2]; pid_t pid; int devNull = open("/dev/null", O_WRONLY); if (pipe(p_stdin) != 0 || pipe(p_stdout) != 0) return -1; pid = fork(); if (pid < 0) return pid; else if (pid == 0) //first fork { close(p_stdin[WRITE]); dup2(p_stdin[READ], READ); close(p_stdout[READ]); dup2(p_stdout[WRITE], WRITE); //dup2(devNull,2); //pipes stderr to /dev/null.... execvp(*command, command); _exit(1); } if (infp == NULL) { close(p_stdin[WRITE]); close(p_stdin[READ]); } else { *infp = p_stdin[WRITE]; } if (outfp == NULL) { close(p_stdout[WRITE]); close(p_stdout[READ]); } else { *outfp = p_stdout[READ]; } return pid; } void* pthread_sink_ffmpeg(void *arg) { char *pbuffer; int length, res; pbuffer = malloc(4096); while(1) { /* Wait for input */ dbgfprintf("Waiting for input..."); while( (length = read(ffmpeg_sink, pbuffer, 1024)) ) { fprintf(stderr, "Read [ %d ]...\r", length); } /* RIP */ dbgfprintf("Done for now.."); } free(pbuffer); } int main( void ) { int wstatus; pid_t child_pid, w; pthread_t t_ffmpeg_source; const char *command_ffmpeg[] = {"ffmpeg", "-hide_banner", "-re", "-i", "http://relay.ah.fm/192k", "-c:a", "libfdk_aac", "-profile:a", "aac_he_v2", "-b:a", "128k", "-f", "adts", "pipe:1", NULL}; child_pid = popen2_2(command_ffmpeg, NULL, &ffmpeg_sink); dbgfprintf("Started ffmpeg with pid [ %d ]...", child_pid); pthread_create(&t_ffmpeg_source, NULL, pthread_sink_ffmpeg, NULL); do { w = waitpid(child_pid, &wstatus, WUNTRACED | WCONTINUED); } while (!WIFEXITED(wstatus) && !WIFSIGNALED(wstatus)); dbgfprintf("ffmpeg terminated..."); close(ffmpeg_sink); pthread_join(t_ffmpeg_source, NULL); }
尝试过的另一种读取方式
sink_stream = fdopen(ffmpeg_sink, "r"); while( !feof(sink_stream) ) { length = fread(pbuffer, 1, 4096, sink_stream); fprintf(stderr, "Read [ %d ]...\r", length); }
结果相同:能读取数据,但FFmpeg终止后无法退出fread()。
解答
你的假设错误点
关闭读端无法唤醒阻塞的
read():当read()已经处于阻塞状态时,关闭当前进程的读端文件描述符不会让read()返回——read()持有的是文件描述符的引用,关闭其他引用不影响正在进行的系统调用。只有当管道的写端全部关闭时,read()才会返回0(EOF)。FFmpeg进程终止后,写端可能未完全关闭:在你的
popen2_2实现中,父进程没有关闭管道写端的多余引用!当outfp不为NULL时,父进程只保留了p_stdout[READ],但p_stdout[WRITE]并没有关闭——这意味着管道的写端还有父进程的一个引用存在,即使FFmpeg子进程终止,写端也没有完全关闭,所以read()会一直阻塞。
正确的解决步骤
修复
popen2_2中的管道引用泄漏
在popen2_2函数中,当outfp不为NULL时,必须关闭p_stdout[WRITE],因为这个写端只属于子进程,父进程不需要持有:if (outfp == NULL) { close(p_stdout[WRITE]); close(p_stdout[READ]); } else { *outfp = p_stdout[READ]; close(p_stdout[WRITE]); // 新增这一行,关闭父进程持有的写端引用 }修正
read()的循环逻辑
原代码的循环条件错误,read()返回0表示EOF,返回-1表示错误(比如被信号中断),正确的处理逻辑应该是:while(1) { dbgfprintf("Waiting for input..."); length = read(ffmpeg_sink, pbuffer, 1024); if (length == 0) { // EOF,子进程已终止且写端关闭 dbgfprintf("Got EOF, exiting read loop.."); break; } else if (length == -1) { // 处理错误,EINTR表示被信号中断,可重试;其他错误直接退出 if (errno == EINTR) continue; perror("read failed"); break; } fprintf(stderr, "Read [ %d ]...\r", length); }可选:用信号或非阻塞IO优化响应速度
如果需要在子进程终止时立刻唤醒阻塞的read(),可以:- 给读端文件描述符设置非阻塞标志,结合
select/poll监听可读事件 - 在
main函数检测到子进程终止后,调用pthread_kill给读取线程发送信号,让read()因信号中断返回,再处理退出逻辑
- 给读端文件描述符设置非阻塞标志,结合
内容的提问来源于stack exchange,提问作者ZKJohan

