一个 Broken pipe 引发的血案

最近遇到一个这样的问题:ffmpeg 解码出 yuv 输出至管道,我写的程序从管道读取 yuv。

e.g:

1
D:\huang_xuezhong\build_win32_VDNAGen>ffmpeg -i test.mkv -c:v rawvideo -s 320x240 -f rawvideo - | my_tool -o output

就上面这行命令,在 Linuxosx 下面运行都正常,唯独在 windows 下面,ffmpeg 报错 av_interleaved_write_frame(): Broken pipe :

怎么会出错了呢?当时我的表情是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Output #0, rawvideo, to 'pipe:':
Metadata:
encoder : Lavf56.4.101
Stream #0:0: Video: rawvideo (I420 / 0x30323449), yuv420p, 320x240 [SAR 120:
91 DAR 160:91], q=2-31, 200 kb/s, 24 fps, 24 tbn, 24 tbc (default)
Metadata:
encoder : Lavc56.1.100 rawvideo
Stream mapping:
Stream #0:0 -> #0:0 (h264 (native) -> rawvideo (native))
Press [q] to stop, [?] for help
processing yuv complete
av_interleaved_write_frame(): Broken pipe
frame= 1 fps=0.0 q=0.0 Lsize= 112kB time=00:00:00.04 bitrate=22118.2kbits/s
video:112kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing o
verhead: 0.000000%
Conversion failed!

在我的程序中,my_toolstdin 中每次尝试读取一帧大小的 yuv 文件,若不足,则继续读取,直到满一帧,然后处理这一帧。代码逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
int process_yuv (int jpg_width, int jpg_height) 
{
int ret = 0;

FILE *yuv_fp = NULL;
unsigned char * yuv_buf = NULL;
int frame_size = 0;
int count = 0;
int try_cnt = 0;

frame_size = jpg_width * jpg_height * 3 / 2;
va_log (vfp_log, "a frame size:%d\n", frame_size);

yuv_fp = stdin;

yuv_buf = (unsigned char *) aligned_malloc_int(
sizeof(char) * (cf->jpg_width + 1) * (cf->jpg_height + 1) * 3, 128);

if (!yuv_buf) {
fprintf (stderr, "malloc yuv buf error\n");
goto end;
}

memset (yuv_buf, 0, frame_size);
while (!feof (yuv_fp)) {

try_cnt++;
va_log (vfp_log, "try_cnt is %d\n", try_cnt);

//MAX_TRY_TIMES = 1000
if (try_cnt > MAX_TRY_TIMES) {
va_log (vfp_log, "try time out\n");
break;
}

count = fread (yuv_buf + last_pos, 1, frame_size - last_pos, yuv_fp);
if (last_pos + count < frame_size) {
va_log (vfp_log, "already read yuv: %d, this time:%d\n", last_pos + count, count);
last_pos += count;
continue;
}

// some_personal_work ();

memset (yuv_buf, 0, frame_size);
last_pos = 0;
try_cnt = 0;
}

fprintf (stderr, "processing yuv complete\n");

end:
if (yuv_buf) {
aligned_free_int (yuv_buf);
}

return ret;
}

windows 下出错,打出的 log 十分诡异,读取一段内容后就读不出东西了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2016/04/05 15:20:38: a frame size:115200
2016/04/05 15:20:38: try_cnt is 1
2016/04/05 15:20:38: already read yuv: 49365, this time:49365
2016/04/05 15:20:38: try_cnt is 2
2016/04/05 15:20:38: already read yuv: 49365, this time:0
2016/04/05 15:20:38: try_cnt is 3
2016/04/05 15:20:38: already read yuv: 49365, this time:0
2016/04/05 15:20:38: try_cnt is 4
2016/04/05 15:20:38: already read yuv: 49365, this time:0
2016/04/05 15:20:38: try_cnt is 5
2016/04/05 15:20:38: already read yuv: 49365, this time:0
2016/04/05 15:20:38: try_cnt is 6
2016/04/05 15:20:38: already read yuv: 49365, this time:0
2016/04/05 15:20:38: try_cnt is 7
2016/04/05 15:20:38: already read yuv: 49365, this time:0
2016/04/05 15:20:38: try_cnt is 8
2016/04/05 15:20:38: already read yuv: 49365, this time:0
2016/04/05 15:20:38: try_cnt is 9
2016/04/05 15:20:38: already read yuv: 49365, this time:0
2016/04/05 15:20:38: try_cnt is 10
2016/04/05 15:20:38: already read yuv: 49365, this time:0
2016/04/05 15:20:38: try_cnt is 11
...
...
2016/04/05 15:20:38: already read yuv: 49365, this time:0
2016/04/05 15:20:38: try_cnt is 1000
2016/04/05 15:20:38: already read yuv: 49365, this time:0
2016/04/05 15:20:38: try_cnt is 1001
2016/04/05 15:20:38: try time out

表面和真相

google 一下有关 Broken pipe 的信息:

  • 这是个系统错误,字面意思是“管道破裂”。
  • 触发原因是该管道的读端被关闭,而写端还尝试往管道里面写,从而系统异常退出。
  • 经常发生在 socket 关闭之后(或者其他的描述符关闭之后)的 write 操作中。
  • 发生此错误时,进程将收到 SIGPIPE 信号,默认动作是进程终止。

回过头去,细看下 my_tool 的这段代码,莫非在 windows 下面,从管道里面读取一段内容后,由于某种原因,feof 条件就为真了吗?导致整个程序执行完毕,管道读端早于写端关闭。

那么,问题来了,是什么导致 feof 了呢?往这个方向搜索下相关资料后,折腾一番,终于找到原因了, 原来是ASCII0x1A 在作怪:

Unix 系统中,stdinstdoutstderr 默认都是以二进制模式打开的,众所周知,用二进制模式打开一个文件的时候,文件本身的内容和你编写程序时读到的内容完全相同。但是在 windows 下面,stdinstdoutstderr 默认都是以文本模式打开的,这就意味着它会对部分读到的特殊字符进行转义,比如 \r\n(0x0D0x0A) 转义成 \n(0x0A) 。另外,千万别忽略 0x1A 字符(也称Ctrl+Z(^z)) ,除了 EOF,它也被系统认为是文件结束符。

因此,极有可能,Broken pipe 的原因是:windows 以文本模式从 stdin 中读取 yuv 内容,如果 yuv 中含有 0x1A 字符时, 系统认为已到达文件尾,从而退出 while 循环,结束程序,管道读端关闭,而写端 ffmpeg 还在解码,往管道写……

为了验证在 windows 中以文本方式读取文件时,中途读到0x1A 导致 feof() 条件为真,写个小程序测试下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#incldue <stdio.h>
int main(void)
{
int i;
unsigned char c;
FILE *fp;
fp = fopen ("test.dat", "w");
fprintf(fp, "abc%c def",0x1A);
fclose (fp);

fp = fopen("test.dat", "r");
for(i=0; i<=7; ++i) {
fread(&c, sizeof(char), 1, fp);
printf("%02X feof=%d\n", c, feof(fp));
}
fclose (fp);
return 0;
}

运行的结果是:

1
2
3
4
5
6
7
8
61 feof=0
62 feof=0
63 feof=0
63 feof=16
63 feof=16
63 feof=16
63 feof=16
63 feof=16

从以上结果可见,在读到第四个字符 0x1A 的时候,feof 为真了。这种现象目前只发现存在于 windows 中,unix 中没有。Surprised!

一个表面上看似 Broken pipe 的错误,引发它的最初缘由居然是 windows 的特立独行,NND,又被这奇葩的 windows 坑了一把。

经验教训

由于 ffmpeg 解码出来的 yuv 是用二进制模式写出的,当然,你读也要用二进制模式。在 windows 下,手动设置 stdin 的读取方式为二进制模式。

1
2
# include <fcntl.h>
setmode (fileno(stdin), O_BINARY);

从 这个 Broken pipe 引发的血案,可以得出两条经验教训:

  • 二进制模式写出的文件,要用二进制模式读,同理,文本模式写的文件,要用文本模式读,不然,出了问题,系统可不会为你负责。
  • 千万记住, windows 默认以 文本模式 打开文件。
彦祖老师 wechat