我将详细介绍这三种方法,并提供完整的代码示例和讲解。
system() 函数 (最简单)
这是最简单、最直接的方法,但它也是最不灵活、最不安全的方法。
原理
system() 函数会启动一个 shell (通常是 /bin/sh),然后由这个 shell 来解析并执行你传入的命令字符串,这相当于你在终端里输入命令然后按回车。
函数原型
#include <stdlib.h> int system(const char *command);
返回值
command是NULL,则当 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,因此更安全、更高效。
原理
这个过程通常分为两步:
fork(): 创建一个子进程,父进程继续执行,子进程是父进程的一个副本。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 重定向。
示例代码 (执行命令并获取输出)
这个例子比前两个复杂,因为它完整地展示了如何创建管道、fork、exec 和 dup2。
#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 下执行命令的标准方式。
