【Linux学习】Linux中的进程程序替换

大家好,我是程序员小青蛙,今天介绍进程程序替换。

一、什么是进程程序替换?

核心定义

用 fork() 创建子进程后,子进程默认和父进程执行相同的程序。如果想让子进程执行一个全新的程序 ,就需要调用 exec 系列函数,完成进程程序替换。

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

关键特点

  1. 不创建新进程 :调用 exec 前后,进程的 pid 不会改变。
  2. 替换用户空间:新程序的代码段、数据段会完全覆盖原进程的用户空间,栈、堆也会被重置。
  3. 成功不返回 :exec 调用成功后,原程序的后续代码不会再执行;调用失败时返回 -1。

二、exec 系列函数详解

Linux 提供了 6 个以 exec 开头的函数,统称为 exec 函数族。

1. 函数原型

复制代码
#include <unistd.h>
// l: list(可变参数列表)
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);

// v: vector(参数数组)
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。

如果调用出错则返回-1

所以exec函数只有出错的返回值而没有成功的返回值。

2. 命名规律(记忆技巧)

字母 含义 示例
l list:参数以可变列表 形式传递,以 NULL 结尾 execl("/bin/ls", "ls", "-l", NULL)
v vector:参数以数组 形式传递,数组最后一个元素为 NULL execv("/bin/ls", argv)
p path:自动搜索 PATH 环境变量,无需写全路径 execlp("ls", "ls", "-l", NULL)
e env:自定义环境变量,需手动传入 envp 数组 execle("/bin/ls", "ls", "-l", NULL, envp)

3. 函数对比表

函数名 参数格式 是否带路径 是否使用当前环境变量
execl 列表 必须写全路径 是
execlp 列表 可写程序名,自动搜 PATH 是
execle 列表 必须写全路径 否,需手动传入 envp
execv 数组 必须写全路径 是
execvp 数组 可写程序名,自动搜 PATH 是
execve 数组 必须写全路径 否,需手动传入 envp

4. 底层关系

只有 execve 是真正的系统调用,其他 5 个函数都是对 execve 的封装,以提供更灵活的调用方式。


三、exec 函数使用示例

1. execl 示例(全路径 + 列表传参)

复制代码
#include <unistd.h>
int main() {
    // 执行 ls -l 命令,需要写全路径
    execl("/bin/ls", "ls", "-l", NULL);
    // 只有调用失败才会执行这里
    perror("execl");
    return 1;
}

2. execlp 示例(自动搜 PATH + 列表传参)

复制代码
#include <unistd.h>
int main() {
    // 无需写全路径,自动在 PATH 中搜索 ls
    execlp("ls", "ls", "-l", NULL);
    perror("execlp");
    return 1;
}

3. execv 示例(全路径 + 数组传参)

复制代码
#include <unistd.h>
int main() {
    char *argv[] = {"ls", "-l", NULL};
    execv("/bin/ls", argv);
    perror("execv");
    return 1;
}
cpp 复制代码
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
//带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
//带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
//带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
//带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}

事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。


四、综合应用:简易 Shell 实现

fork + exec + wait 是实现 shell 的核心逻辑。

核心流程

  1. 获取命令行:读取用户输入的命令。
  2. 解析命令 :将命令字符串分割成 argv 数组。
  3. 创建子进程 :调用 fork() 创建子进程。
  4. 程序替换 :子进程调用 execvp 执行命令。
  5. 等待子进程 :父进程调用 waitpid 等待子进程退出。

完整代码

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

#define MAX_CMD 1024
char command[MAX_CMD];

// 1. 读取用户输入
int do_face() {
    memset(command, 0x00, MAX_CMD);
    printf("myshell$ ");
    fflush(stdout);
    if (scanf("%[^\n]%*c", command) == 0) {
        getchar();
        return -1;
    }
    return 0;
}

// 2. 解析命令为 argv 数组
char **do_parse(char *buff) {
    int argc = 0;
    static char *argv[32];
    char *ptr = buff;

    while (*ptr != '\0') {
        if (!isspace(*ptr)) {
            argv[argc++] = ptr;
            while (*ptr != '\0' && !isspace(*ptr)) ptr++;
        } else {
            while (*ptr != '\0' && isspace(*ptr)) ptr++;
            *ptr = '\0';
        }
    }
    argv[argc] = NULL;
    return argv;
}

// 3. 执行命令
int do_exec(char *buff) {
    char **argv = do_parse(buff);
    if (argv[0] == NULL) return -1;

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程:程序替换
        execvp(argv[0], argv);
        perror("execvp");
        exit(1);
    } else {
        // 父进程:等待子进程退出
        waitpid(pid, NULL, 0);
    }
    return 0;
}

int main() {
    while (1) {
        if (do_face() < 0) continue;
        do_exec(command);
    }
    return 0;
}

思考函数和进程之间的相似性

exec/exit就像call/return

一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。

这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间。如下图

一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。


五、关键知识点总结

  1. 程序替换的本质 :用新程序的代码和数据,覆盖进程原有的用户空间,进程 pid 不变。
  2. exec 函数的核心特点 :成功不返回,失败返回 -1。
  3. 命名规律 :l/v 表示参数传递方式,p 表示自动搜 PATH,e 表示自定义环境变量。
  4. 简易 shell 的核心逻辑 :fork 创建子进程 → exec 替换程序 → wait 等待子进程。
相关推荐
Lsetea35 分钟前
curl指定--tlsv1.2仍连接TLS1.3:最低版本与tls-max排查
linux·https·ssl证书·curl·tls
Lsetea41 分钟前
curl访问HTTPS报47 Too many redirects:重定向循环与max-redirs排查
linux·http·https·ssl证书·curl
OnlyEasyCode1 小时前
IIS开放端口转发后端
运维·服务器
知识分享小能手1 小时前
C学习教程,从入门到精通,C语言学生信息管理系统 — 知识点详解(18)
c语言·学习·1024程序员节
破芽1 小时前
learn-claude-code-s05-s11-学习笔记
笔记·学习
pipixia200332 小时前
C++ 面试高频:namespace 的 6 个知识点,从 using 到匿名命名空间
c语言·学习·算法·嵌入式·命名空间·c++面试·匿名命名空间
每天题库2 小时前
汽车维修工等级考试题库 理论实操核心考点一次梳理清
学习·安全·考试·题库·考证
9624562 小时前
一次 Dell PowerEdge R750xs 启动故障复盘:从 iDRAC 带外管理到 Linux 多内核回退与 initramfs 重建
linux·运维·服务器
神也畏惧菠萝2 小时前
想要学习Agent应用开发?:2026 AI Agent开发全栈通关路线
学习·langgraph·mcp协议·agent开发·rag架构·prompt高级工程