popen 是什么?
popen (pipe open) 是 C 标准库中的一个函数,它用于创建一个管道,从而调用一个 shell 命令,并可以与该命令进行通信。

与 system() 函数不同,system() 只能执行命令并等待其结束,而无法获取命令的输出或输入。popen 则提供了一个单向的数据流:要么从子进程读取("r"模式),要么向子进程写入("w"模式)。
函数原型
#include <stdio.h> FILE *popen(const char *command, const char *type);
-
command: 一个字符串,代表你想要在 shell 中执行的命令。"ls -l"或"grep 'error' logfile.txt"。 -
type: 一个字符串,指定了管道的打开模式:"r"(read): 以只读模式打开管道。popen会返回一个指向子进程标准输出 的文件指针,你可以用fread,fgets,fscanf等标准 I/O 函数来读取子命令的输出。"w"(write): 以只写模式打开管道。popen会返回一个指向子进程标准输入 的文件指针,你可以用fwrite,fputs,fprintf等标准 I/O 函数来向子命令输入数据。
-
返回值:
(图片来源网络,侵删)- 成功时,返回一个与
FILE关联的流指针,就像fopen一样。 - 失败时,返回
NULL。
- 成功时,返回一个与
如何获取子进程的标准输出(stdout)
这是你最关心的问题,要获取子进程的标准输出,你需要使用 "r" 模式。
工作流程如下:
- 调用
popen: 使用popen(command, "r")启动一个子进程来执行你的命令,这个子进程的stdout会被重定向到一个管道。 - 获取文件指针:
popen返回一个FILE*指针,这个指针指向管道的“读”端。 - 读取数据: 使用标准 I/O 函数(如
fgets)从这个FILE*指针读取数据,这些数据实际上就是子进程命令的输出。 - 关闭管道: 使用
pclose()函数关闭管道。pclose会等待子进程执行完毕,并返回子进程的退出状态。这一点非常重要,popen必须用pclose来关闭,而不是fclose。
代码示例:读取 ls -l 的输出
这是一个非常经典的例子,它演示了如何执行 ls -l 命令并逐行打印其输出。
#include <stdio.h>
#include <stdlib.h> // for exit()
int main() {
FILE *fp;
char path[1035]; // 用于存储命令输出的缓冲区
// 1. 使用 "r" 模式执行命令,获取其 stdout
// "ls -l" 是要执行的命令
fp = popen("ls -l", "r");
if (fp == NULL) {
printf("Failed to run command\n");
exit(1);
}
// 2. 从文件指针 fp 中逐行读取输出
// fgets 会从管道中读取一行数据到 path 缓冲区
printf("--- Output from 'ls -l' command ---\n");
while (fgets(path, sizeof(path), fp) != NULL) {
printf("%s", path); // 打印读取到的一行
}
// 3. 关闭管道,并获取子进程的退出状态
int status = pclose(fp);
if (status == -1) {
// pclose 调用失败
perror("pclose failed");
} else {
// 检查子进程的退出状态
// WIFEXITED: 如果子进程正常终止,返回非零值
// WEXITSTATUS: WIFEXITED 为真,返回子进程的退出码
if (WIFEXITED(status)) {
printf("\nCommand exited with status: %d\n", WEXITSTATUS(status));
} else {
printf("\nCommand did not exit normally.\n");
}
}
return 0;
}
代码解释:
-
fp = popen("ls -l", "r");- 系统会启动一个 shell(
/bin/sh),执行ls -l命令。 ls -l的标准输出不再显示在终端上,而是被重定向到一个管道。popen返回一个指向该管道读端的FILE*指针fp。
- 系统会启动一个 shell(
-
while (fgets(path, sizeof(path), fp) != NULL)fgets是标准库函数,通常用于从文件读取一行,它被用来从管道fp中读取数据。- 当
ls -l命令产生输出时,fgets就会从管道中读取这些数据。 - 当
ls -l命令执行完毕并关闭其标准输出时,fgets会返回NULL,循环结束。
-
pclose(fp);- 这个调用会做两件事:
- 关闭管道的文件指针
fp。 - 等待
ls -l这个子进程完全结束。
- 关闭管道的文件指针
pclose的返回值包含了子进程的退出状态,我们需要使用WIFEXITED和WEXITSTATUS这两个宏(来自<sys/wait.h>)来解析它。
- 这个调用会做两件事:
安全注意事项:popen 与 system 的安全漏洞
popen 和 system 一样,都存在一个严重的安全漏洞:命令注入。
如果你将用户输入直接拼接到 command 字符串中,恶意用户可以插入自己的命令。
不安全的例子:
#include <stdio.h>
#include <string.h>
void list_files(const char *user_input) {
char command[256];
// 拼接命令,非常危险!
sprintf(command, "ls -l %s", user_input);
FILE *fp = popen(command, "r");
// ... 处理输出 ...
pclose(fp);
}
int main() {
// 如果用户输入 "; rm -rf /",会发生什么?
list_files("my_dir; rm -rf /");
return 0;
}
在上面的例子中,user_input 是 "; rm -rf /"。popen 执行的命令变成了:
ls -l my_dir; rm -rf /
这会先列出 my_dir 的内容,然后执行 rm -rf /,这是一个灾难性的命令。
如何避免?
最佳实践是永远不要将用户提供的、未经处理的字符串作为 popen 的 command 参数,如果必须执行动态命令,请使用 exec 系列函数(如 execlp, execvp)并手动处理参数列表,这样可以避免 shell 的解析和注入。
| 特性 | popen(command, "r") |
|---|---|
| 目的 | 执行 shell 命令并读取其标准输出。 |
| 工作方式 | 创建一个管道,子进程的 stdout 连接到管道的写端,父进程通过返回的 FILE* 指针从管道的读端读取数据。 |
| 核心函数 | popen(), fgets(), pclose() |
| 优点 | 简单易用,能无缝集成到标准 I/O 流中。 |
| 缺点 | 存在命令注入风险,需谨慎处理用户输入。 是一个同步操作, pclose 会阻塞父进程,直到子进程结束。 |
| 关闭方式 | 必须使用 pclose(fp),它会等待子进程结束并释放资源。 |
当你需要在 C 程序中捕获另一个命令的输出时,popen 是一个非常方便的工具,但请务必牢记其安全陷阱。
