Linux 上调用外部程序有三条路:system、fork+exec、popen。 它们最实际的区别是「多几个进程」和「退出码要不要移位」。
前面几节都在 Windows 上打转,这一节换到 Linux。 好消息是引号问题没有了——数组天然安全; 坏消息是多出两件新事:system() 的返回值,和 stdin 的归属。
一、三种起法,先看它们各起了几个进程
system() 与 popen() 都要先起一个 /bin/sh,再由它起 ffmpeg。
fork + exec 是直接换壳,中间没有 shell。
起法 进程数 能不能收输出
system() 2 个 不能 fork + exec 1 个 能,自己接管道 popen() 2 个 只能收一路
多一个 shell 不只是多占几 MB 内存。
它意味着你的命令行又要被解析一遍:空格、通配符、$变量 都会被它先处理。
你要的就是这个行为(比如想用通配符),那就用它;
你要的是「把这段字符串原样交给 ffmpeg」,那就别用。
二、退出码:system() 给的是一包,不是一个数
Linux 的 system() 返回的是 waitpid 那种「等待状态」:
低 8 位放终止信号,高 8 位放退出码。
所以子进程 exit 1 之后,你拿到的是 256。
子进程 拿到的值 怎么读
exit 0 0 正常 exit 1 256 WEXITSTATUS → 1 被信号 9 杀掉 9 WIFSIGNALED 为真
注意第三行:被信号杀掉时,WEXITSTATUS 拿到的是无意义的数。
所以正确的读法永远是两步:先 WIFEXITED 判断是不是正常退出,
再 WEXITSTATUS 取退出码。
省掉第一步的代码,会在 ffmpeg 被 OOM 杀掉时给出一个莫名其妙的「错误码 9」。
三、-nostdin:一个只跟后台有关的问题
ffmpeg 默认开着 stdin 交互:按 q 退出、按 +/- 调音量。
这是给人坐在终端前用的。
问题出在「它被放到后台」的时候。
后台进程读终端会被系统发 SIGTTIN 直接挂起,
表现就是「脚本跑一半不动了,进程还在,CPU 为零」。
-nostdin 干的事就是:别读,我不需要交互。
另一层风险是「吃掉父进程的输入」。
如果 ffmpeg 的 stdin 继承自你的程序,而你的程序又在读同一个输入,
两边就会抢——谁先读到算谁的。
本机实测:给子进程喂一段 y,它读走了;换成 DEVNULL,它读到空。
所以封装里写死 stdin=DEVNULL(Python)或把 stdin 指向 /dev/null(C),
是比 -nostdin 更彻底的一道保险。
四、fork + exec 的骨架
这是 Linux 上最干净的写法,也是唯一能精确控制两个管道的写法:
int fd[2]; pipe(fd); // fd[0] 读,fd[1] 写
pid_t pid = fork();
if (pid == 0) { // 子进程
dup2(fd[1], STDOUT_FILENO); // 把 stdout 接到管道
dup2(fd[1], STDERR_FILENO); // 两路合一,省得读两次
close(fd[0]); close(fd[1]);
execv("/usr/bin/ffmpeg", argv); // argv 是数组,不要引号
_exit(127); // 只有 exec 失败才到这儿
}
close(fd[1]); // 父进程必须关掉写端
char buf[4096]; ssize_t n;
while ((n = read(fd[0], buf, sizeof(buf))) > 0) log.append(buf, n);
close(fd[0]);
int status = 0; waitpid(pid, &status, 0);
int rc = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
两处容易漏:父进程必须 close(fd[1]),否则读端永远等不到 EOF,
read 会一直挂着;子进程 dup2 之后也要关掉原始的两个 fd,
不然管道的引用计数不归零,同样等不到 EOF。
这两处漏一个,症状都是「ffmpeg 明明退出了,我的循环还在转」。
五、该用哪一个
要快点把活干完、不关心输出 → system() 够用,记得解 wait status。
要拿输出、要控制管道 → fork + exec,代码长但清楚。
只想读一段输出、不需要超时 → popen 最省事(见下一节)。
三条路都能把 ffmpeg 跑起来,差别在「出错时你能看到多少」。
system() 出错只给你一个数字,fork+exec 出错你能拿到完整 stderr。
调外部程序最贵的从来不是写代码,是出问题时的排查时间。
六、动手 Demo:返回值与 stdin:两处只有平台才会暴露的差别
本机是 Windows,system() 直接返回退出码;Linux 上同样的代码返回 768/256。
下面两段分别测「返回值」和「stdin 归谁」,都是能直接看到的现象。
import os, subprocess, sys
PY = sys.executable
print("exit 0 ->", os.system(PY + " -c \"import sys; sys.exit(0)\""))
print("exit 1 ->", os.system(PY + " -c \"import sys; sys.exit(1)\""))
RD = "import sys; print('读到:', repr(sys.stdin.readline()))"
a = subprocess.run([PY, "-c", RD], input="y\n",
capture_output=True, text=True)
b = subprocess.run([PY, "-c", RD], stdin=subprocess.DEVNULL,
capture_output=True, text=True)
print(a.stdout.strip())
print(b.stdout.strip())
实跑输出:
--------------------------------------------------------------------
12.1.5 Linux 系统下 C++ 调用 FFmpeg 命令行
--------------------------------------------------------------------
**① 三种起法,差别在「多几个进程」和「拿得到什么」**
起法 多起几个进程 输出 退出码
----------------------------------------------------------------------------------
system() 两个:sh 加 ffmpeg 直接打到终端 要解 wait status
fork + exec 一个:ffmpeg 要自己接管道 waitpid 直接给
popen 两个:sh 加 ffmpeg 只能收一路 pclose 里藏着
**`system()` 和 `popen()` 都要先起一个 `/bin/sh`,这是它们和 `exec` 的根本区别。**
**多这一层,多出两个后果:多一个进程,多一次 shell 解析。**
**在 Linux 上这两个后果都不大(shell 解析本来就是你要的),**
**但它决定了你【拿不到】退出码的原值。**
**② 同一行代码,两个平台的返回值不是一回事**
子进程怎么结束 本机 os.system 返回 Linux 上会返回
--------------------------------------------------------------------------
exit 0 0 0
exit 1 1 256(要 >> 8 才是 1)
**Windows 上 `system()` 直接给你退出码,Linux 上给你的是「等待状态」——**
**一个 16 位的打包值,低 8 位放信号、高 8 位放退出码。**
**所以 Linux 上必须写 `WEXITSTATUS(status)` 才拿到 1。**
**注意 `if (system(cmd) != 0)` 这种写法在 Linux 上是能判成败的——**
**256 也是非 0,看着没问题。麻烦出在「想区分」的时候:**
**要知道到底是 1 还是 2,就必须移位;而移位的代码搬到 Windows 上,**
**退出码 1 会被算成 0,于是「失败」被当成「成功」,一声不响地往下跑。**
**③ 后台跑为什么一定要 `-nostdin`**
**先看一件很基础的事:子进程读 stdin,会把父进程留给它的东西吃掉。**
stdin 怎么给的 子进程读到什么
-------------------------------------------------------------
喂一段 "y\n" 读到: 'y\n'
接 DEVNULL 读到: ''
**ffmpeg 默认是【开着】stdin 交互的**(按 `q` 退出、按 `+`/`-` 调音量),**
**这是给人坐在终端前用的。**
**可一旦你把它放到后台(shell 里加 `&`、或者由程序起在后台),**
**它还会去读终端——后台进程读终端会被系统发 `SIGTTIN` 直接挂起,**
**表现就是「推流脚本跑一半不动了」。**
**`-nostdin` 干的事就是:别读,我不用交互。**
**它不是优化项,是【后台运行的必需项】。**
**④ `fork + exec` 的骨架(Linux,编译不了,写给你抄)**
pid_t pid = fork();
if (pid == 0) {
// 子进程:直接换壳成 ffmpeg,不经过 shell
execv("/usr/bin/ffmpeg", argv); // argv 是 char* 数组,空格天然安全
_exit(127); // exec 失败才会走到这儿
}
int status = 0;
waitpid(pid, &status, 0);
int rc = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
**三处值得记住:`execv` 不收命令行字符串,只收数组——所以引号问题根本不存在;**
**`_exit(127)` 是约定(127 表示「命令没找到」,跟 shell 的报错码对齐);**
**`WIFEXITED` 要先判断「是不是正常退出」,否则子进程被信号杀掉时**
**`WEXITSTATUS` 拿到的数是没意义的。**
前两行在本机是 0 和 1;换到 Linux 上是 0 和 256。 后两行说明 stdin 归谁:喂进去就被读走,接 DEVNULL 就读到空。
-nostdin 就是在 ffmpeg 那一侧做同样的事。
30 秒实验
- 把
sys.exit(1)改成sys.exit(3):看本机与 Linux 的差值是不是 256 倍。 - 在 Linux 上跑一次
kill -9子进程:看WIFSIGNALED那条分支。 - 给 ffmpeg 加
-nostdin与不加各跑一次后台任务:看有没有被 SIGTTIN 挂起。
常见误区
- 直接用
system()的返回值当退出码打印。Linux 上是 256 倍,看着像大错。 - 省掉
WIFEXITED判断。被信号杀掉时拿到的是无意义的数。 - 后台跑不加
-nostdin。进程会被 SIGTTIN 挂起,看着像卡死。
自测三问(点开对答案)
1. `system()` 与 `fork+exec` 最本质的区别是什么? —— 前者多起一个 /bin/sh,命令行被解析两次。 2. 为什么读管道前父进程必须 close 写端? —— 不关的话写端引用还在,读端永远等不到 EOF。 3. `-nostdin` 解决的是哪一类问题? —— 后台运行时 ffmpeg 去读终端被挂起,以及和父进程抢 stdin。- ☐ 12.1.5 完——下一节 12.1.6 popen 与 pclose