一、exec族函数:让进程“变身”的核心机制

进程执行这个话题,说白了就是“怎么让一个程序去跑另一个程序”。

我刚开始学Linux编程时,总觉得进程创建就是fork(),后来才发现——fork只是生了个孩子,exec才是让孩子换身衣服去干别的事。

exec族函数的核心作用:用新程序替换当前进程的代码段、数据段、堆和栈。进程PID不变,但干的事完全变了。

关键理解:exec不创建新进程,它只是让当前进程“改头换面”。

1.1 exec族函数全家福

这六个函数长得像,但各有脾气。我按命名规律给你拆开讲:

函数名 路径查找 参数传递 环境变量
execl 需绝对/相对路径 列表形式(可变参数) 继承当前环境
execlp 通过PATH环境变量查找 列表形式 继承当前环境
execle 需绝对/相对路径 列表形式 自定义环境变量
execv 需绝对/相对路径 数组形式(argv) 继承当前环境
execvp 通过PATH查找 数组形式 继承当前环境
execve 需绝对/相对路径 数组形式 自定义环境变量

命名规律其实很好记:带p的通过PATH找,带e的可以传环境变量,带v的用数组传参,带l的用列表传参

1.2 execl:最基础的exec

先看个最简单的例子:

#include <unistd.h>
#include <stdio.h>

int main() {
    printf("执行前:PID = %d\n", getpid());
    
    // 执行/bin/ls,参数列表以NULL结尾
    execl("/bin/ls", "ls", "-l", "/home", NULL);
    
    // 如果exec成功,下面这行不会执行
    printf("如果看到这行,说明exec失败了\n");
    return 1;
}

注意那个NULL——它告诉函数“参数结束了”。我见过新手漏掉这个,结果程序直接崩溃。

重要:exec函数如果成功,不会返回。如果返回了,一定是出错了(返回-1)。

1.3 execlp:偷懒神器

execlp的好处是——你不用写完整路径。它会自动去PATH环境变量里找:

// 不用写/bin/ls,直接写ls就行
execlp("ls", "ls", "-l", NULL);

我个人习惯在写脚本工具时用execlp,省得纠结路径问题。但生产环境里,我建议还是用完整路径,避免PATH被篡改导致安全问题。

1.4 execle:环境变量自己说了算

这个函数带个e,意思是你可以给新程序定制环境变量:

char *env[] = {
    "PATH=/usr/bin",
    "HOME=/tmp",
    "MY_VAR=hello",
    NULL
};

execle("/bin/bash", "bash", "-c", "echo $MY_VAR", NULL, env);

我在项目中遇到过一个问题:子进程继承了父进程的环境变量,里面有个敏感信息泄露了。后来改用execle,只传必要的环境变量,问题就解决了。

1.5 execv和execvp:数组传参更灵活

当参数数量不确定时,用数组比列表方便:

char *args[] = {"ls", "-l", "/home", NULL};
execv("/bin/ls", args);
// 或者
execvp("ls", args);

execvp和execlp一样,会去PATH里找。区别只是传参方式不同。

二、fork+exec组合:真正的进程创建

单独用exec,只是替换当前进程。但实际开发中,我们通常需要父进程继续运行,子进程去干别的事

这就是fork+exec组合的由来。

pid_t pid = fork();

if (pid == 0) {
    // 子进程
    execl("/bin/ls", "ls", "-l", NULL);
    // 如果exec失败,必须退出
    exit(1);
} else if (pid > 0) {
    // 父进程
    wait(NULL);  // 等待子进程结束
    printf("子进程执行完毕\n");
} else {
    perror("fork失败");
}

这个模式太常见了——shell执行命令、服务器处理请求、任务调度……本质上都是fork+exec。

我的经验:fork之后,子进程一定要检查exec的返回值。如果exec失败,子进程必须exit(),否则它会继续执行父进程的代码,造成混乱。

三、system函数:偷懒但危险

system函数是fork+exec+wait的封装。你传一个字符串,它帮你执行:

system("ls -l /home");

看起来很方便对吧?但我要泼盆冷水——system函数有安全隐患

我曾经在代码里写过:

char cmd[256];
sprintf(cmd, "rm -rf %s", user_input);
system(cmd);

结果用户输入了; rm -rf /……嗯,你懂的。从那以后,我再也不敢在生产环境用system了。

避坑指南:

  • system会阻塞父进程,直到命令执行完
  • system会继承父进程的信号处理方式
  • 如果用户输入可控,绝对不要用system
  • 优先用fork+exec,自己控制参数

四、知识体系总览

下面这张图,把exec族函数、fork+exec、system的关系理清楚了:

进程执行:exec族函数与进程替换 exec族函数 参数传递方式 列表形式 (l) 数组形式 (v) 路径查找方式 完整路径 PATH查找 (p) 环境变量处理 继承环境 自定义 (e) fork + exec 组合 父进程继续,子进程替换 system 函数 fork+exec+wait 封装 ⚠ 核心要点 exec成功不返回 | 子进程exec失败必须exit | 优先用fork+exec替代system 带p用PATH查找 | 带e自定义环境 | 带v用数组 | 带l用列表

五、实战中的选择建议

说了这么多,到底该用哪个?我总结几条经验:

  • 写简单脚本工具:用execlp或execvp,省事
  • 生产环境服务:用execl或execv,指定完整路径
  • 需要隔离环境变量:用execle或execve
  • 执行用户输入的命令:绝对不要用system,自己写fork+exec
  • 临时调试:system("command") 凑合用,但别提交到代码库

我的习惯:我一般用execvp,因为它既支持PATH查找,又用数组传参,灵活性最好。只有在需要自定义环境变量时,才换成execve。

嗯,exec族函数就这些内容。记住一句话:exec是让进程“变身”,fork+exec是“生个孩子让他去变身”,system是“偷懒但可能出事”

下一节我们会讲进程间通信,到时候这些exec的知识还会用上。


公众号:蓝海资料掘金营,微信deep3321