🎬 第十二章 各种开发语言调用FFmpeg命令行
📄 一句话概括

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 秒实验

  1. 把第三段的 exit(3) 改成 exit(0):看 close() 返回 0 还是别的。
  2. 把 2>&1 去掉、把 stderr 重定向到文件:看另一种「两路分开」的土办法。
  3. 让子进程写 300 KB 再 read:验证「popen 不卡,是因为你一直在读」。

常见误区

  1. 忘了 pclose()。留下的不只是 FILE*,还有一个僵尸进程。
  2. 以为 pclose 返回的就是 ffmpeg 的退出码。Linux 上要 >> 8。
  3. 用 popen 又想要超时。它没有这个能力,卡住就是卡住。
自测三问(点开对答案) 1. `popen(cmd, "r")` 内部起了几个进程? —— 两个:一个 /bin/sh 加一个 ffmpeg。 2. 为什么加了 `2>&1` 之后顺序会乱? —— 两路缓冲策略不同,合并的瞬间先后关系就丢了。 3. `popen` 最容易踩的性能问题是什么? —— 其实没有性能问题;坑是「起了不读」导致的阻塞。

相关笔记