【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 等待子进程。
相关推荐
qetfw1 小时前
Debian Chrony 时间同步、RAID5 与 LUKS 磁盘加密配置
linux·debian
每天题库2 小时前
架子工脚手架工题库:安全操作考点与模拟题练习指南
学习·安全·考试·题库·考证
yangmu32032 小时前
Codex 提示词优化教程:用四要素把模糊需求变成可执行任务
人工智能·学习
东城居士3 小时前
Linux驱动阻塞与非阻塞访问的理解
linux·嵌入式系统
无敌贵点大王3 小时前
RTThread学习记录13——关于要使用一个外设,RTThread与cubemx到底要怎么配合?
c语言·stm32·学习·rtthread
..Dauntless..3 小时前
【Linux】进程地址空间初步理解
linux·运维·服务器
Julien20043 小时前
Docker 网络(一)
linux·运维·服务器·ssh·学习方法
杨云龙UP4 小时前
一次数据库查询缓慢故障复盘:大表数据增长、SQL全表扫描导致系统响应异常
linux·运维·服务器·数据库·sql·mysql
喜欢打篮球的普通人5 小时前
MiniMind 学习笔记(十二):Pretrain 实操——从版本梳理到 8GB 显卡上的真实训练
人工智能·笔记·学习
PellyKoo5 小时前
【linux运维】ubuntu+samba 用户组独占目录权限配置踩坑记
linux·运维·ubuntu