linux c语言 执行命令

99ANYc3cd6
预计阅读时长 22 分钟
位置: 首页 C语言 正文

我将详细介绍这三种方法,并提供完整的代码示例和讲解。

system() 函数 (最简单)

这是最简单、最直接的方法,但它也是最不灵活、最不安全的方法。

原理

system() 函数会启动一个 shell (通常是 /bin/sh),然后由这个 shell 来解析并执行你传入的命令字符串,这相当于你在终端里输入命令然后按回车。

函数原型

#include <stdlib.h>
int system(const char *command);

返回值

  • commandNULL,则当 shell 可用时返回非零值,不可用时返回 0。
  • command 不是 NULL,则返回值由 shell 执行的命令决定:
    • shell 无法启动,返回 -1。
    • 否则,返回 shell 执行命令后的退出状态。

注意: system() 的返回值不是命令的输出结果,而是命令执行是否成功的状态码,如果你想获取命令的输出,system() 无法直接做到。

优点

  • 简单易用:一行代码就能执行命令。
  • 功能强大:可以直接使用 shell 的所有特性,如管道 、重定向 >、通配符 等。

缺点

  • 不安全:如果命令字符串来自用户输入,容易引发命令注入漏洞,用户输入 "; rm -rf /" 会导致灾难性后果。
  • 效率低:需要创建一个额外的 shell 进程,开销较大。
  • 无法获取输出:无法直接获取命令的标准输出或标准错误。

示例代码

#include <stdio.h>
#include <stdlib.h> // system() 函数的头文件
int main() {
    printf("Executing 'ls -l' using system()...\n");
    // 执行一个简单的 ls 命令
    int ret = system("ls -l");
    if (ret == -1) {
        perror("Failed to execute command");
    } else {
        // WIFEXITED 和 WEXITSTATUS 是宏,用于解析 system() 的返回值
        if (WIFEXITED(ret)) {
            printf("Command exited with status: %d\n", WEXITSTATUS(ret));
        }
    }
    // 执行一个带管道的复杂命令
    printf("\nExecuting 'ps aux | grep bash' using system()...\n");
    system("ps aux | grep bash");
    return 0;
}

popen() 函数 (推荐,用于获取输出)

当你需要执行一个命令并读取它的输出结果时,popen() 是最佳选择。

原理

popen() 函数会创建一个管道,然后启动一个 shell 来执行命令,它会返回一个文件指针(FILE*),你可以像操作普通文件一样对这个指针进行 fread()(读取命令输出)或 fwrite()(向命令输入)。

函数原型

#include <stdio.h>
FILE *popen(const char *command, const char *type);
  • command: 要执行的命令字符串。
  • type: 打开模式。
    • "r": 只读,命令的输出会通过管道返回给你,你可以用 fread 读取。
    • "w": 只写,你可以用 fwrite 向管道写入数据,这些数据会成为命令的标准输入。

执行完毕后,必须用 pclose() 关闭管道,并回收子进程资源。

优点

  • 可以获取输出:这是 system() 无法做到的。
  • 相对简单:比 fork + exec 系列函数简单得多。

缺点

  • 仍然依赖 shell:和 system() 一样,它也会启动一个 shell,因此也存在命令注入风险,并且效率不是最高的。
  • 同步阻塞popen() 是一个阻塞函数,它会等待命令执行完毕。

示例代码 (读取命令输出)

#include <stdio.h>
#include <stdlib.h> // popen(), pclose()
#include <string.h> // strlen()
#define BUFFER_SIZE 128
int main() {
    printf("Executing 'ls -l' using popen() and reading output...\n");
    FILE *pipe = popen("ls -l", "r"); // 以只读模式打开管道
    if (pipe == NULL) {
        perror("popen failed");
        return 1;
    }
    char buffer[BUFFER_SIZE];
    // 使用 fgets 从管道中逐行读取命令的输出
    while (fgets(buffer, BUFFER_SIZE, pipe) != NULL) {
        // 去掉末尾的换行符
        buffer[strcspn(buffer, "\n")] = 0;
        printf("Read: %s\n", buffer);
    }
    // 关闭管道,并获取命令的退出状态
    int status = pclose(pipe);
    if (status == -1) {
        perror("pclose failed");
    } else {
        printf("Command exited with status: %d\n", WEXITSTATUS(status));
    }
    return 0;
}

fork() + exec() 系列函数 (最强大、最灵活)

这是最底层、最强大、也是最复杂的方法,它不依赖于 shell,因此更安全、更高效。

原理

这个过程通常分为两步:

  1. fork(): 创建一个子进程,父进程继续执行,子进程是父进程的一个副本。
  2. exec(): 在子进程中调用 exec 系列函数(如 execlp, execvp)。exec 会用新的程序完全替换掉子进程的当前映像,从而执行你指定的命令。

常用 exec 函数

  • execlp(const char *file, const char *arg, ..., NULL): 参数以列表形式传入,p 表示会在 PATH 环境变量中搜索程序。
  • execvp(const char *file, char *const argv[]): 参数以数组(向量)形式传入,p 同样表示搜索 PATH
  • execv(const char *path, char *const argv[]): 参数以数组形式传入,但 path 必须是完整路径。

优点

  • 高效:不启动 shell,直接加载并执行程序,开销最小。
  • 安全:直接将命令和参数分开传递,避免了 shell 解析带来的注入风险。
  • 灵活:可以精确控制子进程的输入、输出和错误流(通过 dup2 等函数)。
  • 功能强大:可以构建复杂的父子进程通信模型。

缺点

  • 复杂:代码量多,需要手动处理进程创建、等待、信号、I/O 重定向等。
  • 需要手动处理输出:如果你想获取命令的输出,需要自己创建管道,并在父子进程间进行 I/O 重定向。

示例代码 (执行命令并获取输出)

这个例子比前两个复杂,因为它完整地展示了如何创建管道、forkexecdup2

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>   // fork(), pipe(), dup2(), close()
#include <sys/wait.h> // waitpid()
#include <string.h>
#define BUFFER_SIZE 128
int main() {
    int pipefd[2];
    pid_t pid;
    char buffer[BUFFER_SIZE];
    // 1. 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe failed");
        exit(EXIT_FAILURE);
    }
    // 2. 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    }
    if (pid == 0) { // --- 子进程 ---
        // 关闭管道的读端,子进程只写
        close(pipefd[0]);
        // 将标准输出 (stdout) 重定向到管道的写端
        dup2(pipefd[1], STDOUT_FILENO);
        // 关闭原始的管道写端描述符,因为它已经被 dup2 复制了
        close(pipefd[1]);
        // 执行命令
        // 使用 execvp,它会自动在 PATH 中查找 "ls"
        // 参数必须以 NULL 
        char *argv[] = {"ls", "-l", NULL};
        if (execvp("ls", argv) == -1) {
            perror("execvp failed");
            exit(EXIT_FAILURE);
        }
        // execvp 成功,它不会返回到这里
        exit(EXIT_SUCCESS);
    } else { // --- 父进程 ---
        // 关闭管道的写端,父进程只读
        close(pipefd[1]);
        // 3. 从管道中读取子进程的输出
        printf("Parent reading output from child process:\n");
        ssize_t nbytes;
        while ((nbytes = read(pipefd[0], buffer, BUFFER_SIZE - 1)) > 0) {
            buffer[nbytes] = '\0'; // 确保字符串正确终止
            printf("%s", buffer);
        }
        // 4. 等待子进程结束
        int status;
        waitpid(pid, &status, 0);
        if (WIFEXITED(status)) {
            printf("\nChild process exited with status: %d\n", WEXITSTATUS(status));
        }
        // 关闭管道的读端
        close(pipefd[0]);
    }
    return 0;
}

总结与对比

特性 system() popen() fork() + exec()
易用性 极高
安全性 (命令注入) (命令注入)
效率 (启动 shell) (启动 shell)
获取输出 不能 (但需自己实现)
灵活性 (支持所有 shell 特性) 中等 极高 (完全控制)
适用场景 快速执行简单命令,不关心输出,不涉及用户输入。 需要获取命令输出的脚本或工具。 高性能、高安全性的服务器程序、需要精细控制子进程行为的场景。

如何选择?

  • 如果你只是想快速执行一个命令,并且不关心它的输出,比如日志记录、定时任务等,使用 system() 最方便。
  • 如果你需要执行一个命令并获取它的返回结果,比如获取系统信息、调用其他脚本并处理其输出,使用 popen() 是最简单直接的选择。
  • 如果你在编写高性能、高安全性的服务端程序,或者需要构建复杂的进程间通信,或者要避免 shell 带来的开销和安全风险,那么你必须使用 fork() + exec() 系列,这是专业 C 程序员在 Linux 下执行命令的标准方式。
-- 展开阅读全文 --
头像
dede后台密码忘了怎么找回?
« 上一篇 今天
Python与C语言混合编程如何高效实现?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]