【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 等待子进程。
相关推荐
艺杯羹3 小时前
1Panel 装好后 ufw 仍是 inactive:一次服务器安全盲区复盘
服务器·数据库·安全·网络安全·系统架构·实战·1panel
一条破秋裤3 小时前
【1114】【并发控制】【笔记】
linux
Star(up up up)4 小时前
Redis Windows 安装教程:使用Windows 的 Redis 压缩包
redis·笔记·学习·程序人生·云原生·学习方法·改行学it
2035去旅行5 小时前
ADB的底层原理
linux·adb·嵌入式·arm·soc
武子康5 小时前
单卡 A6000 跑 Qwen-Drive:怎样留下可比较的第一条基线?
linux·人工智能·深度学习
女神下凡5 小时前
芯参谋(107):杰理AC_中科蓝讯_开发指南
linux·服务器·网络·单片机·嵌入式硬件
落羽的落羽5 小时前
【C++】C++17的主要特性
linux·服务器·开发语言·c++·算法·机器学习
乌暮7 小时前
Spring AOP 与 Spring 事务:用大白话彻底理解原理与实践
java·数据库·spring boot·后端·sql·学习·spring
爱和冰阔落10 小时前
【Linux网络】TCP 半包粘包怎么解决?从长度报头、JSON 拆包到网络计算器完整实战
linux·运维·网络·tcp/ip
蒸蒸yyyyzwd11 小时前
从邓明学习的后端笔记
笔记·学习