19、进程控制(二):exec族函数的使用

上一章我们聊了 fork(),它像孙悟空拔毫毛,变出一个一模一样的自己。但很多时候,我们想让新进程去干点别的事——比如在 shell 里执行 ls、grep 这些命令。这时候,就需要 exec 族函数登场了。

exec 族函数,说白了就是「换芯」。它不创建新进程,而是把当前进程的代码段、数据段、堆栈全部替换掉,然后从头执行一个新程序。嗯,进程 ID 不变,但灵魂已经换了。

核心理解:fork() 是分身,exec() 是变身。两者经常配合使用。

19.1 exec 族函数全家福

Linux 提供了 6 个 exec 函数,名字长得像兄弟连。我刚开始学的时候也晕,后来发现规律其实很简单——看后缀字母就行。

函数名 后缀含义 参数形式 搜索路径 环境变量
execl() l = list 参数列表(可变参数) 不搜索,需绝对/相对路径 继承当前环境
execv() v = vector 参数数组 不搜索,需绝对/相对路径 继承当前环境
execle() l + e = list + env 参数列表 + 环境数组 不搜索,需绝对/相对路径 自定义环境
execve() v + e = vector + env 参数数组 + 环境数组 不搜索,需绝对/相对路径 自定义环境
execlp() l + p = list + path 参数列表 搜索 PATH 环境变量 继承当前环境
execvp() v + p = vector + path 参数数组 搜索 PATH 环境变量 继承当前环境

你看,规律其实很简单:l 表示参数用列表传,v 表示用数组传;p 表示搜索 PATH 路径;e 表示可以自定义环境变量。

19.2 核心函数详解

19.2.1 execl() — 参数列表形式

原型:

int execl(const char *path, const char *arg, ... /* (char  *) NULL */);

第一个参数是程序路径,后面跟可变参数列表,最后必须以 NULL 结尾。我在项目中经常用它来执行一些固定路径的工具。

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

int main() {
    printf("我是父进程,PID=%d\n", getpid());
    
    // 执行 /bin/ls,列出当前目录文件
    execl("/bin/ls", "ls", "-l", "-a", NULL);
    
    // 如果 exec 成功,下面这行不会执行
    perror("execl 失败");
    exit(1);
}

注意:第二个参数 "ls" 是 argv[0],也就是程序名。虽然它不影响实际执行哪个程序,但很多程序会用它来显示帮助信息或错误提示。我习惯写成和文件名一致。

19.2.2 execv() — 参数数组形式

原型:

int execv(const char *path, char *const argv[]);

当参数数量不确定时,用数组更灵活。我个人更喜欢 execv,因为可以动态构建参数列表。

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

int main() {
    char *args[] = {"ls", "-l", "-a", NULL};
    
    printf("准备执行 ls...\n");
    execv("/bin/ls", args);
    
    perror("execv 失败");
    exit(1);
}

19.2.3 execlp() 和 execvp() — 带路径搜索

这两个函数带 p 后缀,会自动在 PATH 环境变量中搜索可执行文件。你想想看,平时在 shell 里直接敲 ls 就能运行,靠的就是这个机制。

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

int main() {
    printf("用 execlp 执行 ls...\n");
    
    // 不需要写完整路径,系统会自动在 PATH 中找
    execlp("ls", "ls", "-l", NULL);
    
    perror("execlp 失败");
    exit(1);
}

我曾经踩过的坑:有一次在嵌入式系统上,我用了 execlp() 但没设置 PATH 环境变量,结果程序一直返回 -1。查了半天才发现,那个系统的默认 PATH 是空的。所以用 p 系列函数前,最好确认一下 PATH 是否设置正确。

19.2.4 execle() 和 execve() — 自定义环境变量

带 e 后缀的函数可以传入自定义的环境变量数组。这在需要隔离环境或传递特殊配置时非常有用。

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

int main() {
    char *env[] = {
        "MY_VAR=hello",
        "PATH=/usr/bin:/bin",
        NULL
    };
    
    printf("用自定义环境执行程序...\n");
    execle("/bin/printenv", "printenv", NULL, env);
    
    perror("execle 失败");
    exit(1);
}

19.3 知识体系图

下面这张图帮你理清 exec 族函数的选择逻辑:

exec 族函数选择决策树 需要执行新程序? 知道程序完整路径吗? 是(完整路径) 否(搜索 PATH) 需要自定义环境变量? execl() / execv() 参数列表 / 参数数组 execle() / execve() 自定义环境变量 需要自定义环境变量? execlp() / execvp() 搜索 PATH,继承环境 execle() / execve() 搜索 PATH + 自定义环境 提示:execle() 和 execve() 同时支持完整路径和 PATH 搜索,但需要手动传环境变量

19.4 实战:fork + exec 组合拳

在实际项目中,exec 很少单独使用。最常见的模式是:fork 一个子进程,然后在子进程里 exec 执行新程序。父进程继续做自己的事,子进程去跑别的任务。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    
    if (pid == -1) {
        perror("fork 失败");
        exit(1);
    }
    
    if (pid == 0) {
        // 子进程
        printf("子进程 PID=%d,准备执行 ls...\n", getpid());
        execlp("ls", "ls", "-l", NULL);
        
        // 如果 exec 失败,子进程需要退出
        perror("execlp 失败");
        exit(1);
    } else {
        // 父进程
        printf("父进程 PID=%d,等待子进程结束...\n", getpid());
        int status;
        wait(&status);
        
        if (WIFEXITED(status)) {
            printf("子进程正常退出,返回码=%d\n", WEXITSTATUS(status));
        }
    }
    
    return 0;
}

我个人的习惯:在子进程里 exec 之后,一定要加 exit() 或 _exit()。因为如果 exec 失败,子进程会继续执行后面的代码,这往往不是我们想要的。我曾经见过一个 bug,就是忘了加 exit,结果子进程和父进程一起跑同样的逻辑,数据全乱了。

19.5 常见错误与调试

exec 函数执行失败时返回 -1,并设置 errno。常见的错误码有:

errno 含义 常见原因
ENOENT 文件或路径不存在 路径写错了,或者 PATH 没设置
EACCES 权限不足 文件没有可执行权限
ENOMEM 内存不足 系统资源耗尽
E2BIG 参数列表太长 参数或环境变量太多

我曾经遇到的坑:有一次在嵌入式 Linux 上,我用 execv 执行一个脚本,结果一直返回 ENOENT。后来发现脚本的第一行是 #!/bin/bash,但系统里装的是 busybox,没有 bash。换成 #!/bin/sh 就好了。所以执行脚本时,要注意解释器是否存在。

19.6 总结

exec 族函数是进程控制的精髓之一。记住几个要点:

  • l 和 v 决定参数怎么传——列表还是数组
  • p 决定是否搜索 PATH 路径
  • e 决定是否自定义环境变量
  • exec 成功后不会返回,失败后返回 -1
  • fork + exec 是最经典的进程创建模式

嗯,这些函数看着多,但用熟了之后,你会发现它们其实就是一套工具的不同变体。选哪个,取决于你的场景——路径已知还是未知?参数固定还是动态?环境变量要不要隔离?想清楚了,选起来就顺手了。


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