execve系统调用及示例

好的,我们继续按照您的列表顺序,介绍下一个函数是 execve


1. 函数介绍

execve 是 Linux 系统编程中一组被称为 exec 函数族的核心成员之一。它的功能是用一个新的程序镜像(program image)

你可以把 execve 想象成彻底的身份转变

  • 你是一个人(当前运行的进程)。
  • 你决定彻底改变自己,变成另一个人(一个全新的程序)。
  • 你喝下了一瓶神奇药水(调用 execve)。
  • 瞬间,你的外表、记忆、技能、思维方式全部变成了那个人的(新的程序代码、数据、堆栈)。
  • 不再是原来的你,而是完全变成了新程序的实例。
  • 你的身份(PID)可能保持不变,但你的“灵魂”(程序代码)已经彻底替换。

execve(以及整个 exec 函数族)是实现程序执行的根本机制。当你在 shell 中输入命令(如 lsgrepgcc)并按回车时,shell 实际上是通过 fork 创建一个子进程,然后在子进程中调用 execve 来运行你指定的程序。


2. 函数原型

#include <unistd.h> // 必需

int execve(const char *pathname, char *const argv[], char *const envp[]);

3. 功能

  • 替换进程镜像: 用由 pathname 指定的新程序的镜像完全替换调用 execve 的当前进程的镜像。
  • 加载新程序: 内核会加载 pathname 指定的可执行文件。
  • 初始化新程序: 内核会为新程序分配内存,将程序代码和数据加载到内存中,初始化堆栈,并设置程序计数器(PC)指向程序的入口点(通常是 main 函数)。
  • 传递参数和环境: 将 argv 指定的命令行参数和 envp 指定的环境变量传递给新程序。
  • 开始执行: 从新程序的入口点开始执行新程序。

4. 参数

  • const char *pathname: 这是一个指向以空字符结尾的字符串的指针,该字符串包含了要执行的新程序的路径名
    • 这个路径名必须指向一个有效的、可执行的文件。
    • 它可以是绝对路径(如 /bin/ls)或相对路径(如 ./my_program)。
  • char *const argv[]: 这是一个指针数组,数组中的每个元素都是一个指向以空字符结尾的字符串的指针。这些字符串构成了传递给新程序的命令行参数
    • 惯例argv[0] 通常是程序的名字(或调用它的名字)。
    • 结尾: 数组的最后一个元素必须是 NULL,以标记参数列表的结束。
    • 例如:char *args[] = { "ls", "-l", "/home", NULL };
  • char *const envp[]: 这也是一个指针数组,数组中的每个元素都是一个指向以空字符结尾的字符串的指针。这些字符串定义了新程序的环境变量
    • 格式: 每个字符串的格式通常是 NAME=VALUE
    • 结尾: 数组的最后一个元素必须是 NULL,以标记环境变量列表的结束。
    • 例如:char *env_vars[] = { "HOME=/home/user", "PATH=/usr/bin:/bin", NULL };
    • 获取当前环境: 在 C 程序中,可以通过全局变量 extern char **environ; 来访问当前进程的环境变量列表。如果你想让新程序继承当前进程的所有环境变量,可以将 environ 作为 envp 参数传递。

5. 返回值

  • 成功时execve 永远不会返回。如果调用成功,当前进程的镜像就被新程序完全替代,执行从新程序的入口点开始。
  • 失败时: 如果 execve 调用失败(例如,文件不存在、权限不足、文件不是有效的可执行格式等),它会返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EACCES 权限不足,ENOENT 文件不存在,EINVAL 文件格式无效等)。

关键理解点execve 的成功调用是**“不归之路”**。一旦成功,调用 execve 的代码就不再存在了。


6. 相似函数,或关联函数

execve 是 exec 函数族中最底层、最通用的函数。其他 exec 函数都是基于 execve 或与其紧密相关的变体:

  • execlint execl(const char *path, const char *arg, ..., (char *)NULL);
    • 参数以列表(list)形式传递,而不是数组。
    • 最后一个参数必须是 (char *)NULL
    • 使用当前进程的 environ 作为环境。
  • execlpint execlp(const char *file, const char *arg, ..., (char *)NULL);
    • 与 execl 类似,但会在 PATH 环境变量指定的目录中搜索可执行文件。
  • execleint execle(const char *path, const char *arg, ..., (char *)NULL, char *const envp[]);
    • 与 execl 类似,但允许指定自定义的环境变量数组 envp
  • execvint execv(const char *path, char *const argv[]);
    • 参数以数组(vector)形式传递。
    • 使用当前进程的 environ 作为环境。
  • execvpint execvp(const char *file, char *const argv[]);
    • 与 execv 类似,但会在 PATH 环境变量指定的目录中搜索可执行文件。
  • execvpeint execvpe(const char *file, char *const argv[], char *const envp[]); (GNU 扩展)
    • 与 execvp 类似,但允许指定自定义的环境变量数组 envp

7. 示例代码

示例 1:使用 execve 执行 /bin/ls

这个例子演示了如何使用最底层的 execve 函数来执行 /bin/ls -l /tmp 命令。

// execve_ls.c
#include <unistd.h> // execve
#include <stdio.h>  // perror, printf
#include <stdlib.h> // exit

// 全局变量 environ,指向当前进程的环境变量数组
extern char **environ;

int main() {
    // 1. 定义要执行的程序路径
    char *pathname = "/bin/ls";

    // 2. 定义命令行参数数组 (argv)
    // 注意: argv[0] 通常是程序名,数组必须以 NULL 结尾
    char *argv[] = { "ls", "-l", "/tmp", NULL };

    // 3. 定义环境变量数组 (envp)
    // 为了简化,我们让新程序继承当前进程的所有环境变量
    // 通过传递全局变量 environ
    char **envp = environ; // 或者可以构造一个自定义的 envp 数组

    printf("About to execute: %s %s %s\n", argv[0], argv[1], argv[2]);

    // --- 关键: 调用 execve ---
    // 如果成功,execve 永远不会返回
    execve(pathname, argv, envp);

    // --- 如果代码执行到这里,说明 execve 失败了 ---
    perror("execve failed");
    // 打印错误信息后,程序继续执行下面的代码

    printf("This line will only be printed if execve fails.\n");
    exit(EXIT_FAILURE); // 因此,如果 execve 失败,应该显式退出
}

代码解释:

  1. 定义要执行的程序的完整路径 pathname ("/bin/ls")。
  2. 定义命令行参数数组 argv。它是一个 char * 数组。
    • argv[0] 设置为 "ls"(程序名)。
    • argv[1] 设置为 "-l"(第一个参数)。
    • argv[2] 设置为 "/tmp"(第二个参数)。
    • 关键: 数组的最后一个元素必须是 NULL,以标记参数列表结束。
  3. 定义环境变量数组 envp。这里为了简化,直接使用了全局变量 environ,它指向当前进程的环境变量列表,从而使新程序继承所有环境变量。
  4. 调用 execve(pathname, argv, envp)
  5. 关键: 如果 execve 成功,它会用 ls 程序替换当前进程,ls 程序开始执行,并且永远不会返回到 execve 之后的代码。
  6. 关键: 如果 execve 失败(例如,/bin/ls 文件不存在或不可执行),它会返回 -1,并设置 errno
  7. 因此,execve 之后的代码只有在失败时才会执行。这里打印错误信息并调用 exit(EXIT_FAILURE) 退出程序。

示例 2:使用 execve 执行自定义程序并传递自定义环境变量

这个例子演示了如何执行一个自定义程序,并向其传递一组自定义的环境变量。

// execve_custom.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

// 假设你有一个简单的 C 程序 my_program.c 如下,并已编译为 my_program:
/*
// my_program.c
#include <stdio.h>
#include <stdlib.h> // getenv

int main(int argc, char *argv[], char *envp[]) {
    printf("--- My Custom Program Started ---\n");
    printf("Arguments received (argc=%d):\n", argc);
    for (int i = 0; i < argc; ++i) {
        printf("  argv[%d]: %s\n", i, argv[i]);
    }

    // 打印特定的环境变量
    char *my_env = getenv("MY_CUSTOM_ENV");
    char *lang_env = getenv("LANG");
    printf("\nEnvironment variables:\n");
    printf("  MY_CUSTOM_ENV: %s\n", my_env ? my_env : "(not set)");
    printf("  LANG: %s\n", lang_env ? lang_env : "(not set)");

    printf("--- My Custom Program Finished ---\n");
    return 42;
}
*/

int main() {
    char *pathname = "./my_program"; // 假设 my_program 在当前目录

    // 1. 定义命令行参数
    char *argv[] = { "my_program_alias", "arg1", "arg2 with spaces", NULL };

    // 2. 定义自定义环境变量
    // 注意:数组必须以 NULL 结尾
    char *envp[] = {
        "MY_CUSTOM_ENV=Hello_From_Execve",
        "LANG=C",
        "PATH=/usr/local/bin:/usr/bin:/bin", // 覆盖 PATH
        NULL
    };

    printf("Parent process preparing to execve '%s' with custom environment.\n", pathname);

    // --- 关键: 调用 execve 并传递自定义环境 ---
    execve(pathname, argv, envp);

    // --- 如果执行到这里,说明 execve 失败 ---
    perror("execve failed");
    printf("Failed to execute '%s'. Make sure it exists and is executable.\n", pathname);
    exit(EXIT_FAILURE);
}

如何测试:

  1. 首先,创建并编译 my_program.c# 创建 my_program.c (内容如上注释所示) gcc -o my_program my_program.c chmod +x my_program # 确保可执行
  2. 编译并运行 execve_custom.cgcc -o execve_custom execve_custom.c ./execve_custom

代码解释:

  1. 定义要执行的程序路径 pathname ("./my_program")。
  2. 定义命令行参数 argv,包括一个别名和两个参数。
  3. 关键: 定义一个自定义的环境变量数组 envp
    • 它包含三个环境变量:MY_CUSTOM_ENVLANGPATH
    • 重要: 数组以 NULL 结尾。
  4. 调用 execve(pathname, argv, envp)
  5. 如果成功,my_program 将被执行,并接收 argv 和 envp 中定义的参数和环境变量。
  6. my_program 会打印接收到的参数和特定的环境变量值,证明 execve 正确传递了它们。
  7. my_program 执行完毕后(返回 42),整个进程(包括 execve_custom)就结束了。

示例 3:fork + execve 经典范式

这个例子演示了 Unix/Linux 系统编程中最经典、最常用的模式:fork 创建子进程,然后在子进程中调用 execve 执行新程序。

// fork_execve.c
#include <sys/socket.h> // fork, wait
#include <sys/wait.h>   // wait
#include <unistd.h>     // execve, fork
#include <stdio.h>      // perror, printf
#include <stdlib.h>     // exit

extern char **environ;

int main() {
    pid_t pid;
    char *pathname = "/bin/date"; // 执行 date 命令
    char *argv[] = { "date", "+%Y-%m-%d %H:%M:%S", NULL };
    char **envp = environ;

    printf("Parent process (PID: %d) is about to fork.\n", getpid());

    // 1. 创建子进程
    pid = fork();

    if (pid == -1) {
        perror("fork failed");
        exit(EXIT_FAILURE);

    } else if (pid == 0) {
        // --- 子进程 ---
        printf("Child process (PID: %d) created.\n", getpid());

        // 2. 在子进程中调用 execve 执行新程序
        printf("Child (PID: %d) is about to execve '%s'.\n", getpid(), pathname);
        execve(pathname, argv, envp);

        // --- 如果代码执行到这里,说明 execve 失败 ---
        perror("execve failed in child");
        printf("Child process (PID: %d) exiting due to execve failure.\n", getpid());
        // 子进程失败时应使用 _exit,而不是 exit
        _exit(EXIT_FAILURE);

    } else {
        // --- 父进程 ---
        printf("Parent process (PID: %d) created child (PID: %d).\n", getpid(), pid);

        // 3. 父进程等待子进程结束
        int status;
        printf("Parent (PID: %d) is waiting for child (PID: %d) to finish...\n", getpid(), pid);

        if (waitpid(pid, &status, 0) == -1) {
            perror("waitpid failed");
            exit(EXIT_FAILURE);
        }

        // 4. 检查子进程的退出状态
        if (WIFEXITED(status)) {
            int exit_code = WEXITSTATUS(status);
            printf("Parent: Child (PID: %d) exited normally with status %d.\n", pid, exit_code);
        } else if (WIFSIGNALED(status)) {
            int sig = WTERMSIG(status);
            printf("Parent: Child (PID: %d) was terminated by signal %d.\n", pid, sig);
        } else {
            printf("Parent: Child (PID: %d) did not exit normally.\n", pid);
        }

        printf("Parent process (PID: %d) finished.\n", getpid());
    }

    return 0;
}

代码解释:

  1. 定义要执行的程序路径 (/bin/date) 和参数 (date +%Y-%m-%d %H:%M:%S)。
  2. 调用 fork() 创建子进程。
  3. 在子进程中 (pid == 0):
    • 调用 execve(pathname, argv, envp) 执行 date 命令。
    • 如果 execve 成功,子进程从此处消失,date 命令开始执行。
    • 如果 execve 失败,打印错误信息并调用 _exit(EXIT_FAILURE) 退出子进程。强调: 在 fork 的子进程中,失败时应使用 _exit 而非 exit
  4. 在父进程中 (pid > 0):
    • 打印信息。
    • 调用 waitpid(pid, &status, 0) 等待特定的子进程 (pid) 结束。
    • waitpid 返回后,检查子进程的退出状态 status
      • WIFEXITED(status): 检查子进程是否正常退出(通过 exit 或 return)。
      • WEXITSTATUS(status): 获取子进程的退出码。
      • WIFSIGNALED(status): 检查子进程是否被信号终止。
      • WTERMSIG(status): 获取终止子进程的信号编号。
    • 根据退出状态打印相应信息。
    • 父进程结束。

重要提示与注意事项:

  1. 永不返回execve 成功时永远不会返回。这是其最根本的特性。
  2. 失败处理execve 失败时返回 -1。必须检查返回值并处理错误,因为程序会继续执行 execve 之后的代码。
  3. _exit vs exit: 在 fork 之后的子进程中,如果 execve 失败并需要退出,应调用 _exit() 而不是 exit()。因为 exit() 会执行一些清理工作(如调用 atexit 注册的函数、刷新 stdio 缓冲区),这在子进程中可能导致意外行为(例如,缓冲区被刷新两次)。
  4. 参数和环境数组argv 和 envp 数组必须以 NULL 指针结尾。忘记 NULL 会导致未定义行为。
  5. argv[0]: 按惯例,argv[0] 应该是程序的名字。虽然可以是任意字符串,但很多程序会使用它来确定自己的行为。
  6. 环境变量envp 数组定义了新程序的完整环境。它不会自动继承父进程的环境,除非你显式地传递 environ
  7. PATH 搜索execve 不会在 PATH 环境变量中搜索可执行文件。它要求 pathname 是一个完整的路径。如果需要 PATH 搜索功能,应使用 execvp 或 execvpe
  8. 权限: 调用进程必须对 pathname 指定的文件具有执行权限。
  9. 文件描述符execve 不会关闭当前进程中打开的文件描述符(除非它们设置了 FD_CLOEXEC 标志)。新程序会继承这些文件描述符。
  10. exec 函数族选择:
    • 需要最精确控制(指定完整路径、自定义环境):使用 execve
    • 需要 PATH 搜索:使用 execvp 或 execvpe
    • 参数较少且希望列表形式:使用 execl 或 execlp
    • 一般推荐:execv 或 execvp,因为它们使用数组形式,更灵活且不易出错。

总结:

execve 是 Linux 系统中执行新程序的核心机制。它通过完全替换当前进程的内存镜像来启动一个新的程序。理解其参数(路径、参数数组、环境数组)和永不返回的特性对于掌握进程执行和 Unix/Linux 编程范式至关重要。它通常与 fork 结合使用,形成创建并运行新进程的经典模式。虽然有更高级的 exec 函数变体,但 execve 是它们的基础。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注