Linux下 进程控制(二) —— 进程程序替换

欢迎来到我的频道 【点击跳转专栏】

码云链接 【点此转跳】

文章目录

  • [0. fork常规用法回顾](#0. fork常规用法回顾)
  • [1. 进程替换原理](#1. 进程替换原理)
  • [2. 替换函数的接口](#2. 替换函数的接口)
    • [2.1 execl](#2.1 execl)
    • [2.2 execlp](#2.2 execlp)
    • [2.3 execv](#2.3 execv)
      • [2.3.1 加餐:深度解析 char *const argv[]和const char* argv[]](#2.3.1 加餐:深度解析 char const argv[]和const char argv[])
    • [2.4 execvp](#2.4 execvp)
    • [2.5 execvpe](#2.5 execvpe)
      • [2.5.1 putenv](#2.5.1 putenv)
      • [2.5.2 结论](#2.5.2 结论)
    • [2.6 execle](#2.6 execle)
    • [2.7 execve(系统调用)](#2.7 execve(系统调用))
      • [2.7.1 execve 与其他函数的对比](#2.7.1 execve 与其他函数的对比)
  • [3. 自主shell](#3. 自主shell)

0. fork常规用法回顾

我们之前使用fork创建子进程大多是一个父进程希望复制自己,使父子进程同时执行不同的代码段。但是更多⼀个进程要执⾏⼀个不同的程序。例如⼦进程从fork返回后,调⽤exec函数!使用进程的程序替换来完成这个功能!

1. 进程替换原理

先看现象:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>

int main() 
{
  printf("我变成了一个进程:%d\n",getpid());
  
 //执行另一个程序的代码
 execl("/usr/bin/ls","ls","-a","-l",NULL);//程序替换函数

  
  printf("我的代码运行中....\n");
  printf("我的代码运行中....\n");
  printf("我的代码运行中....\n");
  printf("我的代码运行中....\n");
  printf("我的代码运行中....\n");
}

我么发现execl后面的printf直接就不执行了,程序之间变成了执行ls -l -a,这就是 进程的程序替换


接下来 我们来讲原理层面:

  1. 进程替换后,有没有创建新的进程?

其实这个答案很简单,进程替换的本质其实是 在物理内存将原本的代码和数据段替换成ls的,而我们看是否创建新进程的关键是看它的pid,而pid存放在PCB中,而PCB是没有受到影响的,所以并没有创建新的进程。


  1. 那么进程替换的本质是什么?

进程替换的本质就是把代码和数据拷贝到内存中,然后对应页表的映射关系会发生变化,同时虚拟内存也会根据替换的代码和数据变化程度,进行相应的大小调整


  1. bash的工作原理

结合上面的知识 我们知道程序在运行前必须要加载到内存中,众所周知,我们的代码或者是ls等指令,他们的父进程都是bash!那么bash真实创建子进程(新程序)的方式其实也不难想象:bashfork一个子进程,然后再通过excl将新的程序从硬盘加载到内存中!


  1. 那么谁有权限把硬盘的数据加载到内存中?

答案是 操作系统!所以OS 必须提供对应系统调用来完成这个工作,所以也不难得知,execl就是 封装了系统调用的一个函数!


  1. 那么程序替换可不可以把shell、java、python这样的语言给调用起来呢?

其实这跟语言没关系,程序替换的本质 是替换的可执行程序 而不是替换的源文件!

2. 替换函数的接口

关于替换函数的接口,一共有7个,一个在2号手册中是系统调用;另外6个是在三号手册中的库函数:

二号手册单独的:

cpp 复制代码
#include <unistd.h>
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[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);

程序替换函数,部分参数确实可以省略 但是本身不复杂 所以不建议省略!

如果所有exec*的返回值是什么?

如果替换成功 则无返回值,替换失败返回-1 同时errno被设置!


那么什么时候会替换失败呢?

很简单 你路径写错 选项写错都会失败

但是我们一般不需要通过返回值写判断

因为一旦替换失败 这个函数就会继续往前走 我们直接exit()退出即可!

2.1 execl

c 复制代码
#include <unistd.h>

int execl(const char *path, const char *arg, ... /* (char *) NULL */);
  • path : 这是一个字符串,指定了要执行程序的完整路径 。例如,要执行 ls 命令,必须写成 "/bin/ls"execl 不会自动在 PATH 环境变量中搜索。
  • arg : 这是传递给新程序的命令行参数列表。
    1. 第一个参数通常是程序名本身
    2. 后续参数是具体的命令行选项。
    3. 参数列表必须以 (char *)NULL 结尾,以标识参数结束。

  1. 进程替换,而非创建execl 不会创建新的进程。它只是在当前进程的地址空间里加载新程序,覆盖掉旧的程序内容。因此,调用 execl 前后,进程 ID (PID) 保持不变
  2. 成功则不返回 :如果 execl 调用成功,当前进程的程序代码就被完全替换,新程序将从它的 main 函数开始执行。因此,execl 成功时没有返回值,其后的代码不会被执行。
  3. 失败则返回 :只有当调用失败时(例如文件路径错误或权限不足),execl 才会返回 -1,并设置 errno 变量来指示错误类型。

  • l (list) :表示它使用可变参数列表的方式来传递命令行参数

如果用图画 类似是这样的!

cpp 复制代码
[ "/bin/ls" ] -> [ "ls" ] -> [ "-l" ] -> [ "-a" ] -> [ NULL ]

当你的程序运行起来,CPU 真正执行 execl 时,操作系统会把这一串参数收集起来,打包成一个指针数组(也就是 char *argv[]

cpp 复制代码
// 内存中的真实结构
char *argv[] = {
    "/bin/ls",  // argv[0]
    "ls",       // argv[1]
    "-l",       // argv[2]
    "-a",       // argv[3]
    NULL        // argv[4] (结束标志)
};

这才是它真实的物理长相:一个连续的内存块,里面存了一堆地址。

作为程序员,你在写代码时,是一个一个列出来的(List them out)。你不需要自己先创建一个数组,而是直接罗列参数。这种"罗列"的行为,很像是在列清单(List),所以我们叫它是链表存储

execl 的"链表"只是一个逻辑上的概念,指的是你罗列参数的方式。在计算机内存的深处,它们最终都会变成整齐的数组

2.2 execlp

execlp 是 Linux/Unix 系统编程中 exec 函数族的一员。它的核心功能是用一个新的程序替换当前进程的映像。

它与 execl 最大的区别在于:execlp 会自动在 PATH 环境变量中查找可执行文件,因此你不需要提供程序的完整路径。

c 复制代码
#include <unistd.h>

int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
  • file : 这是要执行的程序名称execlp 会根据 PATH 环境变量自动在指定的目录中搜索这个文件。例如,你可以直接写 "ls",而不必写成 "/bin/ls"。(也就是说它会在环境变量表中的PATH变量里面的路径里面去找)
  • arg : 这是传递给新程序的命令行参数列表。
    1. 第一个参数通常是程序名本身 (即 file 参数)。
    2. 后续参数是具体的命令行选项。
    3. 参数列表必须以 (char *)NULL 结尾,以标识参数结束。
  1. 自动搜索路径 (p for PATH) :这是 execlp 的标志性特点。它会像 Shell 一样,在 PATH 变量定义的目录列表中查找可执行文件。
  2. 进程替换 :和 execl 一样,execlp 不会创建新进程,而是将当前进程的代码、数据、堆栈等完全替换为新程序的内容。进程 ID (PID) 保持不变。
  3. 成功则不返回 :如果调用成功,当前进程的程序就被新程序完全覆盖,execlp 之后的代码将不会被执行。
  4. 失败则返回 :只有当调用失败时(例如在 PATH 中找不到文件),execlp 才会返回 -1,并设置 errno 变量来指示错误类型。
cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>

int main()
{
  printf("我变成了一个进程:%d\n",getpid());
  pid_t id=fork();
 if(id==0)
 {
        sleep(2);
       printf("下面代码都是子进程在执行:\n");
      //执行另一个程序的代码
      execlp("ls","ls","-a","-l","-n",NULL);
      // -n 就是把用户信息变成数字
       exit(1);
 }
int status=0;
pid_t rid=waitpid(id,&status,0);
if(rid>0)
 {
          printf("wait success, exit code: %d\n", WEXITSTATUS(status));
 }
}

我们发现在没有相对路径的情况下 依然成功执行了代码:


有个问题:

execlp("ls","ls","-a","-l","-n",NULL);

这里有两个ls重复吗?

不重复 第一个ls表示你要执行谁 第二个ls表示你要怎么执行!


execlp("ls",,"-l","-n",NULL); 当然这么写也是对的 但是这是一种省略写法没必要记!反而容易出错!!

2.3 execv

c 复制代码
#include <unistd.h>

int execv(const char *pathname, char *const argv[]);
  1. pathname :
    • 这是一个字符串,指向要执行的程序的完整路径
    • 例如:"/bin/ls""/usr/bin/python3"。它不会像 Shell 那样自动在 PATH 环境变量中搜索。
  2. argv :
    • 这是一个字符串指针数组,用于向新程序传递命令行参数
    • 这个数组必须以 NULL 指针结尾
    • 按照惯例,数组的第一个元素 argv[0] 应该是程序本身的名称。
  • 成功时不会返回 。当前进程的映像被新程序替换,从新程序的 main 函数开始执行。
  • 失败时 :返回 -1,并设置全局变量 errno 来指示具体的错误原因(如文件不存在、权限不足等)。

  • v vs l : execv 使用一个指针数组来传递参数,而 execl 则是将参数一个一个列出来(以链表的形式)。

execv 是构建 Shell、容器技术(如 Docker)等复杂系统的基础机制之一。

特性 execl (List) execv (Vector)
你写代码的样子 execl(path, arg1, arg2, NULL) execv(path, argv_array)
形象比喻 报菜名:红烧肉、鱼香肉丝、米饭,没了。 递菜单:给,这是一张写好的菜单(数组)。
内存里的真身 最终还是会被组装成数组 本来就是数组
适用场景 参数固定,写死在代码里很方便。 参数数量不确定(比如 Shell 接收用户输入),必须用数组。

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

int main()
{
  printf("我变成了一个进程:%d\n",getpid());
  pid_t id=fork();
 if(id==0)
 {
        sleep(2);
       printf("下面代码都是子进程在执行:\n");
      //执行另一个程序的代码
      char* argv[]={
              (char*)"ls",
              (char*)"-a",
              (char*)"-l",
              NULL
      };
      execv("/usr/bin/ls",argv);
      // -n 就是把用户信息变成数字
       exit(1);
 }
int status=0;
pid_t rid=waitpid(id,&status,0);
if(rid>0)
 {
          printf("wait success, exit code: %d\n", WEXITSTATUS(status));
 }
}

上图:这么写会报警 那是因为"ls"是字符串常量!

强转一下就没事了!
效果:


那么这个argv传给了谁?

本质就是传给了"ls"main中的命令行参数!!


2.3.1 加餐:深度解析 char const argv[]和const char argv[]

这两个类型的核心区别在于:const 修饰的位置不同,导致"不可变"的对象也不同

简单来说:

  • char *const argv[]:指针本身是常量(不能换地址),但指向的内容可以改。
  • const char* argv[]:指向的内容是常量(不能改字符),但指针本身可以换地址。

char *const argv[] (execv 的用法)

为什么 execv 要这样设计?

execv 需要读取你传入的参数数组来启动新程序。

  1. 防止篡改指针 :操作系统需要确保你传给它的参数列表结构是稳定的。如果允许 execv 内部修改 argv[1] 的指向,可能会导致逻辑混乱(比如把参数 "-l" 偷偷换成了 "hello")。
  2. 允许修改内容:在某些底层实现或特殊场景下,被调用的程序可能需要临时修改参数内容的缓冲区(虽然不常见,但 C 语言保留了这种可能性)。

代码演示:

c 复制代码
char *args[] = {"ls", "-l", NULL}; 

// ✅ 合法:修改内容
args[0][0] = 'L'; // 把 "ls" 变成 "Ls",这是允许的

// ❌ 非法:修改指针指向
args[1] = "-a"; // 编译报错!因为 args 里的指针是 const 的

  1. const char* argv[] (只读数据)

这通常用于处理字符串常量

  • 含义argv 数组里的元素是指向常量字符的指针。
  • 你可以做什么 :你可以让指针指向别的地方(例如 argv[1] = other_string)。
  • 你不能做什么 :你绝对不能修改字符串的内容(例如不能把 "hello" 改成 "hallo")。

代码演示:

c 复制代码
const char *args[] = {"ls", "-l", NULL};

// ❌ 非法:修改内容
args[0][0] = 'L'; // 运行时错误(段错误)!因为 "ls" 在只读内存区

// ✅ 合法:修改指针指向
args[1] = "-a"; // 这是允许的,指针本身不是 const

总结对比表

特性 char *const argv[] const char* argv[]
Const 修饰谁 修饰指针本身 (指针的值即指向的地址) 修饰指向的内容 (字符串字符)
能改字符串内容吗 (如 args[0][0] = 'A') 不能 (会导致崩溃)
能换指针指向吗 不能 (如 args[1] = "x")

2.4 execvp

c 复制代码
#include <unistd.h>

int execvp(const char *file, char *const argv[]);
  1. file :
    • 这是要执行的文件名 (例如 "ls""gcc")。
    • 关键点 :你不需要写 /bin/ls,只需要写 lsexecvp 会自动去 PATH 环境变量里找 ls 在哪里。
  2. argv :
    • 这是一个字符串指针数组,用于传递命令行参数。
    • 关键点 :数组的最后一个元素必须是 NULL
    • 按照惯例,argv[0] 应该是程序名本身。

返回值

  • 成功时不返回 。当前进程被新程序完全覆盖,从新程序的 main 函数开始执行。
  • 失败时 :返回 -1,并设置 errno 错误码(例如文件没找到、权限不足)。
特性 execv execvp
路径搜索 不搜索 。必须提供完整路径(如 /bin/ls)。 自动搜索 。只需提供文件名(如 ls),系统去 PATH 找。
灵活性 较低。如果程序换了安装位置,代码就得改。 。只要环境变量配对了,程序在哪都能找到。
典型场景 执行系统内部固定路径的工具。 Shell 实现、执行用户输入的命令(因为用户通常只输命令名)。

2.5 execvpe

c 复制代码
int execvpe(const char *file, char *const argv[], char *const envp[]);
  • file: 要执行的程序文件名。execvpe 会根据 PATH 环境变量在指定的目录中搜索该文件。
  • argv: 传递给新程序的命令行参数数组。这个数组的最后一个元素必须是空指针 NULL
  • envp: 传递给新程序的环境变量数组。这个数组的最后一个元素也必须是空指针 NULL
  • 成功时:函数不会返回,因为当前进程的映像已经被新程序完全替换。
  • 失败时 :函数返回 -1,并设置全局变量 errno 来指示错误原因。

环境变量
execvpeexecvp 的主要区别在于,execvpe 允许你通过 envp 参数为新程序指定一套全新的环境变量,而不是继承调用进程的环境变量。

  1. 当你想使用父进程自带的环境变量

可以通过 extern char** environ 直接传递environ


  1. 当你想使用自己的环境变量

你可以自己写一个 myenv[]然后传递进函数!而我们传递自己环境变量的本质,其实就是对系统环境变量做的一个覆盖!


这是一个在 Linux 环境下使用 execvpe 的简单示例:

这是一个我们自己写的程序 othercmd

cpp 复制代码
#include <iostream>
#include <stdio.h>

int main(int argc, char *argv[], char *env[])
{
    for(int i = 0; i < argc; i++)
    {
        printf("argv[%d]: %s\n", i, argv[i]);
    }
    std::cout << "\r\n";
    for(int j = 0; env[j]; j++)
    {
        printf("env[%d]->%s\n", j, env[j]);
    }


    std::cout << "我是自己的C++程序!" << std::endl;
    return 0;
}

效果如下:


1 . 使用自己的环境变量:

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

int main()
{
  printf("我变成了一个进程:%d\n",getpid());
  pid_t id=fork();
 if(id==0)
 {
        sleep(2);
       printf("下面代码都是子进程在执行:\n");
      //执行另一个程序的代码
      char* argv[]={
              (char*)"othercmd",
              (char*)"-a",
              (char*)"-b",
              NULL
      };

      char *env[]={
             (char*)"PATH=/home/fcy/linux_code/lesson22"
                     ,NULL
      };
      extern char** environ;
      execvpe("othercmd",argv,env);
      exit(1);
 }
int status=0;
pid_t rid=waitpid(id,&status,0);
if(rid>0)
 {
          printf("wait success, exit code: %d\n", WEXITSTATUS(status));
  }
  }                                                                                               

运行结果:

2.5.1 putenv

当你又想使用系统的环境变量表,并且添加一些新的环境变量,可以使用putenv函数!

putenv 是一个用于动态修改或添加当前进程环境变量的 C 语言标准库函数。它定义在 <stdlib.h> 头文件中。

c 复制代码
int putenv(char *string);
  • string: 一个格式为 "name=value" 的字符串。
    • name: 要设置或修改的环境变量名。
    • value: 环境变量的新值。
  • 成功时:返回 0。
  • 失败时 :返回 -1,并设置全局变量 errno 来指示错误原因(例如 ENOMEM 表示内存不足)。

  1. 作用范围
    putenv 函数仅影响调用它的当前进程 的环境变量。它不会修改父进程或操作系统的全局环境设置。由当前进程通过 fork 创建的子进程会继承修改后的环境变量。
  2. 内存管理 (关键)
    putenv 函数不会复制 你传入的字符串,而是直接将这个字符串指针添加到环境变量数组中。这意味着:
    • 你不能传入一个栈上的局部数组,因为函数返回后该内存会失效。
    • 如果你后续修改了这个字符串的内容,环境变量的值也会随之改变。
    • 你不能释放这个字符串的内存,否则环境变量会指向一块无效内存。
    • 推荐做法 :通常建议使用更安全的 setenv 函数来代替,因为它会复制字符串,避免了这些内存管理陷阱。
  3. 删除环境变量
    删除环境变量的方法在不同系统上存在差异:
    • 在 Linux/Unix 上putenv 通常不用于删除变量,应使用 unsetenv 函数。

案例:

cpp 复制代码
//把这一块代码进行些修改
      extern char** environ;
      putenv((char*)"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/fcy/linux_code/lesson22");
      execvpe("othercmd",argv,envion);

效果:

2.5.2 结论

结合上面的内容,其实我们可以得到一个结论:命令行参数表和环境变量表,都是父进程通过 exec* 给你传递的!

2.6 execle

c 复制代码
#include <unistd.h>

int execle(const char *path, const char *arg0, ..., char * const envp[]);
  1. path : 要执行的程序的完整路径 。与 execlpexecvp 不同,execle 不会PATH 环境变量中搜索程序,所以你必须提供准确的路径(如 "/bin/ls""./myapp")。
  2. arg0, ... : 这是传递给新程序的命令行参数列表。
    • arg0 通常是程序名本身。
    • 后续参数是具体的命令行选项。
    • 这个列表必须 以一个空指针 NULL 结尾。
  3. envp : 这是一个指向字符串数组的指针,数组中的每个字符串都是一个 "KEY=VALUE" 格式的环境变量。这个数组也必须NULL 结尾。新程序将使用这个全新的环境,而不是继承调用进程的环境。
  • 成功时 :函数不会返回。因为当前进程的代码、数据、堆栈等已被新程序完全覆盖。
  • 失败时 :函数返回 -1,并设置全局变量 errno 来指示错误原因(如 ENOENT 文件未找到,EACCES 权限不足等)。

2.7 execve(系统调用)

execve 是 Linux/Unix 系统中最基础、最核心 的系统调用,它是整个 exec 函数族(包括 execl, execvp, execle 等)的底层实现基础

如果说 execlexecvp 是为了方便程序员使用的"高级封装",那么 execve 就是直接和操作系统内核打交道的"原生接口"。

c 复制代码
#include <unistd.h>

int execve(const char *filename, char *const argv[], char *const envp[]);
  1. filename :
    • 要执行的程序的完整路径 (例如 "/bin/ls""./myapp")。
    • 注意execve 不会execlp 那样去 PATH 环境变量里查找文件,你必须指定确切的路径。
  2. argv :
    • 传递给新程序的命令行参数数组。
    • argv[0] 通常是程序名本身。
    • 数组必须NULL 指针结尾。
  3. envp :
    • 传递给新程序的环境变量数组(例如 {"PATH=/bin", "HOME=/home/user", NULL})。
    • 数组必须NULL 指针结尾。
    • 新程序将使用这个全新的环境,不会继承当前进程的环境变量
  • 成功时不会返回。当前进程的代码段、数据段、堆栈等会被新程序完全覆盖("夺舍")。
  • 失败时 :返回 -1,并设置 errno 错误码(如 ENOENT 文件未找到,EACCES 权限不足)。

2.7.1 execve 与其他函数的对比

execve 是系统调用,其他函数(如 execl, execvp)都是 C 库函数,它们最终都会调用 execve 来完成任务。

函数 参数形式 是否自动搜索 PATH 是否自定义环境 底层实现
execve 数组 (v) 系统调用
execl 列表 (l) 调用 execve
execle 列表 (l) 调用 execve
execlp 列表 (l) 是 (p) 调用 execve
execv 数组 (v) 调用 execve
execvp 数组 (v) 是 (p) 调用 execve

⚠️:在底层封装中,如果没有传环境变量,在execve中默认传environ,这也就是为什么 分装的6个库函数在使用手册中 都与entern char** environ高度绑定的原因!

总结:

如果你需要完全控制 新程序的环境变量(例如为了安全或隔离),或者你正在编写底层的系统代码,直接使用 execve 是最佳选择。如果你只是想在脚本中简单运行一个命令,使用 库函数

3. 自主shell

当然光说 肯定是空洞的 我们还是得写一个自主shell 来好好感受一下程序替换的魅力!!篇幅原因 请期待

相关推荐
m0_7381207214 小时前
应急响应(重点)——记一次某公司流量应急溯源分析(附带下载链接)
服务器·前端·数据库·安全·web安全·网络安全
中科三方15 小时前
输入域名后无法访问?教你快速区分域名解析问题与服务器问题
运维·服务器
internet Boy15 小时前
桌面运维面试常见问题及标准答案(完整版)
运维
用户23678298016815 小时前
Linux find 命令深度解析:从递归遍历到性能优化的完整实现
linux
ascarl201016 小时前
Linux.do 帖子整理:AI 调用 Chrome DevTools 调试前端页面
linux·前端·人工智能
Slow菜鸟16 小时前
Docker 学习篇(三)| Docker安装指南(Linux版)
linux·学习·docker
liuluyang53016 小时前
linux kernel CONFIG_KCMP解析
linux·运维·服务器
Koma_zhe16 小时前
【Ansible开源自动化运维工具】别再手动装监控了,Ansible能让上百台机器同时搞定Node Exporter(1)
运维·开源·ansible
daad77717 小时前
记录一次上下文切换次数的统计
服务器·c++·算法