使用Perl的open3先读后写子进程时程序挂起的原因
现有如下C程序:
... puts("What is your name?"); scanf("%s", name); puts("Thank You!"); ...
该程序会询问姓名、接收输入、输出“Thank You!”后退出。
我编写了一个Perl程序,通过IPC::Open3自动向该C程序写入姓名并接收输出,先写后读的版本运行正常:
use strict; use IPC::Open3; my $bin = './myniceprogram'; my $pid = open3(\*CHLD_IN, \*CHLD_OUT, \*CHLD_ERR, "$bin") or die "open3() failed $!"; my $r; print CHLD_IN "A"x10 . "\n"; $r = <CHLD_OUT>; print "$r"; $r = <CHLD_OUT>; print "$r"; waitpid $pid, 0;
输出为:
What is your name? Thank You!
但当调整为先读取C程序的第一行输出(“What is your name?”)再写入时,Perl程序出现挂起:
use strict; use IPC::Open3; my $bin = './myniceprogram'; my $pid = open3(\*CHLD_IN, \*CHLD_OUT, \*CHLD_ERR, "$bin") or die "open3() failed $!"; my $r; $r = <CHLD_OUT>; print "$r"; print CHLD_IN "A"x10 . "\n"; $r = <CHLD_OUT>; print "$r"; waitpid $pid, 0;
通过strace查看发现程序卡在read调用:
[myuser@myhost mydir]$ strace perl test2.pl ... close(6) = 0 close(8) = 0 read(5,
请问这是什么原因?
核心问题是C程序的标准输出缓冲机制:
- 当C程序的stdout连接到终端时,默认是行缓冲模式,
puts输出的内容会因为换行符立即刷新到终端。 - 但当stdout被重定向到管道(比如
IPC::Open3创建的管道)时,stdout会切换为全缓冲模式。此时puts("What is your name?")的内容会被存在缓冲区里,不会立即写入管道。
先写后读的版本能运行,是因为Perl先把输入发给C程序,C程序执行scanf读取输入后,后续的puts("Thank You!")可能触发了缓冲区刷新(或者程序退出时自动刷新),导致之前的"What is your name?"和"Thank You!"一起被输出到管道,Perl后续读取时能拿到内容。
而先读后写的版本中,Perl一开始就尝试从CHLD_OUT读取内容,但C程序的输出还在缓冲区里没刷到管道,所以Perl的read调用会一直阻塞等待数据,导致程序挂起。
解决办法
可以从以下几个方向解决:
修改C程序,强制刷新缓冲区:
在puts("What is your name?")之后加上fflush(stdout);,强制把缓冲区内容写入管道:puts("What is your name?"); fflush(stdout); // 新增这行 scanf("%s", name); puts("Thank You!");修改C程序的stdout缓冲模式:
在程序开头用setvbuf(stdout, NULL, _IOLBF, 0);把stdout设置为行缓冲模式,这样即使输出到管道,换行符也会触发缓冲区刷新。在Perl中使用非阻塞IO或多路复用:
比如用select同时监听CHLD_OUT和CHLD_ERR,避免单线程阻塞在读取操作上,不过这种方式相对复杂。
内容的提问来源于stack exchange,提问作者yomol777

