菜鸟科技网

popen执行命令时如何获取返回结果?

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

popen执行命令时如何获取返回结果?-图1
(图片来源网络,侵删)

这个管道使得你的程序可以:

  1. 向新进程的输入流写入数据(就好像在命令行里用 或 < 将数据传给命令)。
  2. 从新进程的输出流读取数据(就好像在命令行里用 或 > 获取命令的输出)。

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 命令,并读取其输出。

popen执行命令时如何获取返回结果?-图2
(图片来源网络,侵删)
#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;
}

代码解释:

  1. fp = popen("ls -l", "r");:执行 ls -l 命令,并返回一个可读的 FILE 指针 fp,这个指针连接到了 ls 命令的标准输出。
  2. fgets(path, sizeof(path), fp);:从 fp 指向的流中读取一行,并存入 path 数组,这行代码会阻塞,直到 ls 命令输出一行或命令结束。
  3. pclose(fp);非常重要! 这个函数会关闭管道,并等待命令执行完毕,然后返回命令的退出状态码。必须调用 pclose 来回收资源,否则会导致僵尸进程

popen 的工作原理

popen 的内部实现可以简化为以下步骤(以 "r" 模式为例):

  1. 创建管道:创建一个匿名管道,得到两个文件描述符,一个用于读(pipefd[0]),一个用于写(pipefd[1])。
  2. 创建子进程:调用 fork() 创建一个子进程。
  3. 子进程操作
    • 子进程关闭管道的读端(pipefd[0])。
    • 子进程将它的标准输出(STDOUT_FILENO)重定向到管道的写端(pipefd[1])。
    • 子进程调用 exec() 来执行 command 字符串中的命令(通常是 /bin/sh -c command)。
    • 子进程的所有输出都会通过管道发送给父进程。
  4. 父进程操作
    • 父进程关闭管道的写端(pipefd[1])。
    • 父进程将管道的读端(pipefd[0])包装成一个 FILE* 流(fp),并返回给调用者。
    • 父进程可以开始从这个流中读取数据,这些数据正是来自子进程的命令输出。

"w" 模式的原理类似,只是方向相反:父进程向流写入数据,子进程从标准读取这些数据。


pclose 函数

pclosepopen 的“另一半”,它的原型是:

popen执行命令时如何获取返回结果?-图3
(图片来源网络,侵删)
#include <stdio.h>
int pclose(FILE *stream);
  • 作用:
    1. 关闭由 popen 创建的 FILE* 流。
    2. 等待由 popen 启动的子进程(即 shell)执行完毕。
    3. 返回子进程的终止状态,这个状态和 wait()system() 返回的状态一样,可以使用 WEXITSTATUSWIFEXITED 等宏来解析。
  • 注意: 如果不调用 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 / 也会被执行,导致灾难性后果。

如何避免:

  1. 最佳实践:避免使用 popen,如果只是想执行一个命令并获取其完整输出,并且该命令的参数是固定的或来自可信源,可以使用 popen,但如果参数来自用户,应考虑更安全的替代方案,如 posix_spawnexec 系列函数(配合 fork)。
  2. 如果必须用,请进行严格的验证和转义,对用户输入进行白名单校验,只允许特定的字符(如字母、数字、下划线、点等),并移除所有 shell 元字符(如 , &, , , >, <, , 等)。

popen vs. system

特性 popen system
功能 执行命令并通过管道进行 I/O(读或写)。 执行命令,无法直接获取其输出或输入
交互性 可以,你的程序可以和命令交换数据。 不可以,命令执行是黑盒,程序只能等待它结束。
返回值 返回一个 FILE* 流,通过 pclose 获取命令退出状态。 返回命令的退出状态码(通过 shell)。
典型用例 需要解析命令的输出(如 ping, curl),或向命令提供输入(如 sort, bc)。 简单地执行一个命令,不关心其输出(如 ls > /dev/nullmkdir mydir)。
实现 内部通常通过 fork + pipe + exec 实现。 内部通常通过 fork + exec + wait 实现。

  • popen 是一个强大的工具,用于执行外部命令并与之交互,特别是当你需要读取命令的输出时。
  • 使用 "r" 模式来获取命令的输出,使用 "w" 模式来向命令提供输入。
  • 永远不要忘记 pclose(fp),否则会造成资源泄漏(僵尸进程)。
  • 警惕 Shell 注入风险,不要将不可信的输入直接拼接到 command 字符串中,在处理用户输入时,优先考虑更安全的 API。
分享:
扫描分享到社交APP
上一篇
下一篇