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

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

  1. 把 sys.exit(1) 改成 sys.exit(3):看本机与 Linux 的差值是不是 256 倍。
  2. 在 Linux 上跑一次 kill -9 子进程:看 WIFSIGNALED 那条分支。
  3. 给 ffmpeg 加 -nostdin 与不加各跑一次后台任务:看有没有被 SIGTTIN 挂起。

常见误区

  1. 直接用 system() 的返回值当退出码打印。Linux 上是 256 倍,看着像大错。
  2. 省掉 WIFEXITED 判断。被信号杀掉时拿到的是无意义的数。
  3. 后台跑不加 -nostdin。进程会被 SIGTTIN 挂起,看着像卡死。
自测三问(点开对答案) 1. `system()` 与 `fork+exec` 最本质的区别是什么? —— 前者多起一个 /bin/sh,命令行被解析两次。 2. 为什么读管道前父进程必须 close 写端? —— 不关的话写端引用还在,读端永远等不到 EOF。 3. `-nostdin` 解决的是哪一类问题? —— 后台运行时 ffmpeg 去读终端被挂起,以及和父进程抢 stdin。

相关笔记