【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 表示自动搜 PATHe 表示自定义环境变量。
  4. 简易 shell 的核心逻辑fork 创建子进程 → exec 替换程序 → wait 等待子进程。
相关推荐
小+不通文墨1 小时前
把树莓派外接的DHT11接收的温湿度发送到emqx上
经验分享·笔记·嵌入式硬件·学习·树莓派
bush42 小时前
嵌入式linux学习记录四
linux·运维·学习
峥嵘life2 小时前
Android 蓝牙设备连接广播详解-2026
android·python·学习
lihao lihao3 小时前
软硬链接
linux·运维·服务器
TOWE technology3 小时前
智能安防监控系统如何做好防雷?——视频信号SPD综合应用方案解析
运维·服务器·防雷产品·信号保护·信号防雷·spd
楼田莉子3 小时前
Docker学习:Docker介绍及其架构介绍
运维·后端·学习·docker·容器·架构
雪度娃娃3 小时前
存储器层次结构——磁盘硬盘存储
服务器·网络·数据库·计算机组成原理
YY&DS3 小时前
Qt 嵌入 CEF 在 Linux 下必须设置 `QT_XCB_GL_INTEGRATION=xcb_egl才能加载网页
linux·开发语言·qt
辰风沐阳3 小时前
ThinkPHP8.1 + think-swoole 4.1 使用指南(保姆级教程)
linux·后端·swoole