popen (pipe open) 是一个在类 Unix 系统(如 Linux, macOS)和 Windows 上都存在的标准库函数,它用于启动一个进程来执行一个命令,并创建一个管道(pipe)来连接这个进程。

这个管道使得你的程序可以:
- 向新进程的输入流写入数据(就好像在命令行里用 或
<将数据传给命令)。 - 从新进程的输出流读取数据(就好像在命令行里用 或
>获取命令的输出)。
popen 的核心优势在于它提供了一种简单的方式来执行外部命令并与其进行交互,而不需要像 fork + exec + pipe 那样复杂的系统调用组合。
函数原型
popen 的函数原型如下:
#include <stdio.h> FILE *popen(const char *command, const char *type);
- 参数:
command: 一个字符串,指定了你要执行的命令,这通常是一个 shell 命令(ls -l)。type: 一个字符串,指定了管道的打开模式,决定了你的程序如何与新进程交互。"r"(read): 以只读模式打开管道,你的程序可以读取新进程的标准输出,常用于获取命令的执行结果。popen("ls -l", "r"),你可以读取ls命令列出的文件列表。
"w"(write): 以只写模式打开管道,你的程序可以向新进程的标准输入写入数据。popen("sort > output.txt", "w"),你可以向sort命令写入数据,sort的结果会存入output.txt。
- 返回值:
- 成功时,返回一个指向
FILE流的指针,你可以像使用fopen返回的文件指针一样,使用标准 I/O 函数(如fgets,fscanf,fputs,fprintf)来读写这个流。 - 失败时,返回
NULL,并设置errno来指示错误原因。
- 成功时,返回一个指向
如何使用:一个简单的例子
下面是一个最经典的例子,使用 popen 来执行 ls -l 命令,并读取其输出。

#include <stdio.h>
#include <stdlib.h> // for exit()
int main() {
FILE *fp;
char path[1035]; // 用于存储命令输出的缓冲区
// 使用 "r" 模式执行 "ls -l" 命令
fp = popen("ls -l", "r");
if (fp == NULL) {
printf("Failed to run command\n");
exit(1);
}
// 逐行读取命令的输出
while (fgets(path, sizeof(path), fp) != NULL) {
printf("%s", path);
}
// 关闭管道
pclose(fp);
return 0;
}
代码解释:
fp = popen("ls -l", "r");:执行ls -l命令,并返回一个可读的FILE指针fp,这个指针连接到了ls命令的标准输出。fgets(path, sizeof(path), fp);:从fp指向的流中读取一行,并存入path数组,这行代码会阻塞,直到ls命令输出一行或命令结束。pclose(fp);:非常重要! 这个函数会关闭管道,并等待命令执行完毕,然后返回命令的退出状态码。必须调用pclose来回收资源,否则会导致僵尸进程。
popen 的工作原理
popen 的内部实现可以简化为以下步骤(以 "r" 模式为例):
- 创建管道:创建一个匿名管道,得到两个文件描述符,一个用于读(
pipefd[0]),一个用于写(pipefd[1])。 - 创建子进程:调用
fork()创建一个子进程。 - 子进程操作:
- 子进程关闭管道的读端(
pipefd[0])。 - 子进程将它的标准输出(STDOUT_FILENO)重定向到管道的写端(
pipefd[1])。 - 子进程调用
exec()来执行command字符串中的命令(通常是/bin/sh -c command)。 - 子进程的所有输出都会通过管道发送给父进程。
- 子进程关闭管道的读端(
- 父进程操作:
- 父进程关闭管道的写端(
pipefd[1])。 - 父进程将管道的读端(
pipefd[0])包装成一个FILE*流(fp),并返回给调用者。 - 父进程可以开始从这个流中读取数据,这些数据正是来自子进程的命令输出。
- 父进程关闭管道的写端(
"w" 模式的原理类似,只是方向相反:父进程向流写入数据,子进程从标准读取这些数据。
pclose 函数
pclose 是 popen 的“另一半”,它的原型是:

#include <stdio.h> int pclose(FILE *stream);
- 作用:
- 关闭由
popen创建的FILE*流。 - 等待由
popen启动的子进程(即 shell)执行完毕。 - 返回子进程的终止状态,这个状态和
wait()或system()返回的状态一样,可以使用WEXITSTATUS和WIFEXITED等宏来解析。
- 关闭由
- 注意: 如果不调用
pclose,子进程会变成僵尸进程,占用系统资源。
安全性:Shell 注入风险
popen 的一个主要安全风险是Shell 注入,如果你的 command 字符串来自不可信的输入(比如用户),攻击者可以注入恶意命令。
不安全的例子:
#include <stdio.h>
#include <string.h>
int main() {
char user_input[100];
char command[200];
printf("Enter a filename to list: ");
scanf("%99s", user_input); // 用户输入 "myfile.txt; rm -rf /"
// 危险!直接将用户输入拼接到命令中
sprintf(command, "ls -l %s", user_input);
printf("Executing: %s\n", command);
FILE *fp = popen(command, "r");
if (fp) {
// ... 处理输出 ...
pclose(fp);
}
return 0;
}
如果用户输入 myfile.txt; rm -rf /,那么最终执行的命令是 ls -l myfile.txt; rm -rf /。 是 shell 的命令分隔符,rm -rf / 也会被执行,导致灾难性后果。
如何避免:
- 最佳实践:避免使用
popen,如果只是想执行一个命令并获取其完整输出,并且该命令的参数是固定的或来自可信源,可以使用popen,但如果参数来自用户,应考虑更安全的替代方案,如posix_spawn或exec系列函数(配合fork)。 - 如果必须用,请进行严格的验证和转义,对用户输入进行白名单校验,只允许特定的字符(如字母、数字、下划线、点等),并移除所有 shell 元字符(如 ,
&, , ,>,<, , 等)。
popen vs. system
| 特性 | popen |
system |
|---|---|---|
| 功能 | 执行命令并通过管道进行 I/O(读或写)。 | 执行命令,无法直接获取其输出或输入。 |
| 交互性 | 可以,你的程序可以和命令交换数据。 | 不可以,命令执行是黑盒,程序只能等待它结束。 |
| 返回值 | 返回一个 FILE* 流,通过 pclose 获取命令退出状态。 |
返回命令的退出状态码(通过 shell)。 |
| 典型用例 | 需要解析命令的输出(如 ping, curl),或向命令提供输入(如 sort, bc)。 |
简单地执行一个命令,不关心其输出(如 ls > /dev/null,mkdir mydir)。 |
| 实现 | 内部通常通过 fork + pipe + exec 实现。 |
内部通常通过 fork + exec + wait 实现。 |
popen是一个强大的工具,用于执行外部命令并与之交互,特别是当你需要读取命令的输出时。- 使用
"r"模式来获取命令的输出,使用"w"模式来向命令提供输入。 - 永远不要忘记
pclose(fp),否则会造成资源泄漏(僵尸进程)。 - 警惕 Shell 注入风险,不要将不可信的输入直接拼接到
command字符串中,在处理用户输入时,优先考虑更安全的 API。
