popen 就是「起个子进程 + 接一根管子」,两行代码换来的便利, 代价是四件它做不到的事。认清这四条,就知道什么时候该换掉它。
上一节把它列为「三条路」之一,这一节单独拆开讲。 因为它是 C 标准库里最常用的一个,也是最容易被误用的一个。
一、它到底做了什么
popen(cmd, "r") 做了三件事:起一个 shell、把它的一路输出接到管道、
把管道包成一个 FILE* 返回给你。
所以它本质上是 system() 加一根管子。
"r" 是「我要读它的输出」,"w" 是「我要写它的输入」——
两个方向只能选一个,这是它的第一个限制。
最后必须 pclose():它等子进程结束、回收资源、
顺便把「子进程怎么结束的」这个信息带回来。
忘了 pclose() 不只是泄漏一个 FILE*,还会留下一个僵尸进程。
二、第一件做不到:两路分开收
popen 只给你一根管子。
ffmpeg 的 stdout 与 stderr 是两根,你只能选一根接过来。
默认接的是一路,另一路直接进父进程的终端——
换句话说,ffmpeg 的报错在你程序里一行都看不到。
这是 popen 最容易被忽略的一处:程序「跑成功了」,
但你想打印日志时发现只有媒体数据,没有一行诊断信息。
想拿 stderr,只能在命令行末尾加 2>&1,把两路并成一路。
代价是顺序不能当真——本机实测,合并之后 ERR 会跑到 OUT 前面。
因为两根管子的缓冲策略不同,合并的那一刻先后关系就丢了。
三、第二件做不到:给一个干净的退出码
pclose() 返回的是「子进程结束状态」,跟 system() 是同一套。
Linux 上它是 16 位打包值,高 8 位才是退出码;Windows 上直接就是退出码。
本机实测:子进程 exit 3,close() 拿到 3。
而 Linux 上同样代码拿到 768。
于是「pclose(p) == 3」只在 Windows 成立,
「pclose(p) >> 8 == 3」只在 Linux 成立。
想两边都对,只有两条路:#ifdef 分开写,或者干脆别用 popen。
这也解释了为什么跨平台项目里,popen 通常被一层薄封装包住——
把平台差异关在一个函数里,业务代码只看到「成功/失败」。
四、第三件做不到:超时
popen 期间的等待是阻塞的。
ffmpeg 卡住(等一个连不上的地址、读一个坏文件),你的 read() 就跟着卡住,
它没有超时参数,也没有办法从外部打断。
这一点在脚本里很致命:一个卡住的转码任务能把整条流水线堵死。
要超时,只能换成 fork + exec 再自己 poll/select,
或者起一个看门狗进程到点 kill。
顺带说一句:popen 起的那个 shell 也会挡在中间。
你 kill 掉的往往是 pclose 对应的 pid,
而 ffmpeg 是 shell 的子进程,可能还活着——留下一份孤儿进程。
五、第四件做不到:它其实不慢
很多人以为 popen 因为「只读一路」所以会卡,其实不会。
popen 本身没有任何缓冲,你 read() 一次,子进程就能多写一次。
本机实测:让子进程写 300 KB,一路 read() 读完,
七百多毫秒收全,一个字节都没丢。
真正会卡的是「起了进程却不读」。
上一节 Qt 那个死锁,本质就是把「等」和「读」分开了;
popen 的接口设计恰好逼着你「读」和「等」一起做,反而不会踩这个坑。
所以它的问题不在性能,在能力边界。
六、什么时候该换掉它
四条限制合起来是一句话:popen 适合「跑一条命令、把输出读回来」这种简单活。
你的需求 还能用 popen 吗
跑一下,成不成无所谓 可以 要把日志读回来存起来 可以,记得 2>&1 要区分 stdout 与 stderr 不行,换 fork+exec 要有超时 不行,换 fork+exec 要边跑边处理进度 可以,但要一直 read
它不难用,是它只能干这么点事。 判断标准很简单:把上面那张表对一遍, 落在「不行」那两行的,就是该换掉它的时候。
七、动手 Demo:只连一路、返回值含义,两件事一次看清
下面用 os.popen 复现 C 里 popen 的行为,因为两者是同一个东西。
先看「只连一路」丢掉了什么,再看 close() 返回的是什么。
import os, sys
PY = sys.executable
CH = "import sys; sys.stdout.write('OUT\\n'); sys.stderr.write('ERR\\n')"
f = os.popen(PY + " -c \"" + CH + "\"")
print("只有一路:", repr(f.read()))
f.close()
f = os.popen(PY + " -c \"" + CH + "\" 2>&1")
print("合并两路:", repr(f.read()))
f.close()
f = os.popen(PY + " -c \"import sys; sys.exit(3)\"")
f.read()
print("exit 3 之后 close() 返回:", f.close())
实跑输出:
--------------------------------------------------------------------
12.1.6 popen 与 pclose
--------------------------------------------------------------------
**① `popen` 只连一路,另一路默认丢掉**
popen 怎么开的 读到的内容
-------------------------------------------------------------
popen(cmd, "r") OUT
popen(cmd + " 2>&1", "r") OUT ERR
**默认只拿到 stdout,stderr 直接进了父进程的终端——**
**换句话说,ffmpeg 的错误信息在你程序里【一行都看不到】。**
**注意第二行:`ERR` 跑到了 `OUT` 前面。**
**两个流的缓冲策略不一样,合并之后顺序就不能再当真——**
**所以日志看着「前后颠倒」,不是 ffmpeg 乱打,是合并的代价。**
**② `pclose` 返回的那 8 位,两个平台含义不同**
子进程 exit 3 之后 本机拿到的值 按 Linux 的读法
--------------------------------------------------------------------
pclose / close 3 768,要 >> 8
**本机(Windows)拿到的就是 3,一点没打包。**
**Linux 上同样的代码会拿到 768——高 8 位才是退出码。**
**于是「`if (pclose(p) == 3)`」这种写法只在 Windows 成立;**
**而「`if (pclose(p) >> 8 == 3)`」只在 Linux 成立。**
**想两边都对,只能 `#ifdef`,或者干脆别用 `popen`。**
**③ `popen` 不会卡,是因为你一直在读它**
做法 结果
------------------------------------------------------------
popen 后一直 read() 读完 300000 字节,822 毫秒
起了进程却不读 输出一过 4 KB 就永久停住
**`popen` 本身没有任何缓冲,它就是一个管道加一个 `FILE*`。**
**你 `read()` 一次,子进程就能多写一次——所以「边读边等」在这条路上是天然的。**
**上一节 Qt 那个死锁,恰好就是因为它把「等」和「读」分开了。**
**④ 用它之前先认下这四个限制**
限制 后果
----------------------------------------------------------------
只能单向 要么读它输出,要么给它喂输入,不能同时
没有超时 它卡住,你也跟着卡住,没有 API 能打断
隐式起 shell 路径里的空格、通配符、`$` 又多一层解析
两路不能分开收 stderr 想要,只能 `2>&1` 混进来
**四条合起来就是一句话:`popen` 适合「跑一条命令、把它的输出读回来」这种简单活。**
**一旦需要超时、需要两路分开、需要边跑边处理,就该换 `fork + exec` 或平台 API 了。**
**它不难用,是它只能干这么点事。**
第一段只读到 OUT,ERR 直接漏到终端上了。
第二段两路都在,但 ERR 跑到了 OUT 前面——顺序不能当真。
第三段是本机拿到 3;Linux 上同一行会拿到 768。
30 秒实验
- 把第三段的
exit(3)改成exit(0):看close()返回 0 还是别的。 - 把
2>&1去掉、把 stderr 重定向到文件:看另一种「两路分开」的土办法。 - 让子进程写 300 KB 再 read:验证「popen 不卡,是因为你一直在读」。
常见误区
- 忘了
pclose()。留下的不只是FILE*,还有一个僵尸进程。 - 以为
pclose返回的就是 ffmpeg 的退出码。Linux 上要>> 8。 - 用
popen又想要超时。它没有这个能力,卡住就是卡住。
自测三问(点开对答案)
1. `popen(cmd, "r")` 内部起了几个进程? —— 两个:一个 /bin/sh 加一个 ffmpeg。 2. 为什么加了 `2>&1` 之后顺序会乱? —— 两路缓冲策略不同,合并的瞬间先后关系就丢了。 3. `popen` 最容易踩的性能问题是什么? —— 其实没有性能问题;坑是「起了不读」导致的阻塞。- ☐ 12.1.6 完——下一节 12.2 Java 调用 FFmpeg 命令行