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 来好好感受一下程序替换的魅力!!篇幅原因 请期待

相关推荐
blackorbird2 小时前
AI工作流自动化平台n8n正被大规模网络武器化
运维·网络·人工智能·自动化
浮尘笔记2 小时前
Java Snowy 框架生产环境安全部署全流程(服务器篇)
java·运维·服务器·开发语言·后端
web守墓人2 小时前
【linux】Mubuntu v1.0.10更新日志
linux·运维·服务器
不怕犯错,就怕不做2 小时前
(Hisilicon)笔试题:嵌入式Linux C语言GPIO中断与按键消抖(转载)
linux·驱动开发·嵌入式硬件
我科绝伦(Huanhuan Zhou)2 小时前
Oracle BBED 工具部署全流程:Linux 64位环境实操指南
linux·数据库·oracle
ONLYOFFICE2 小时前
11款Linux PDF编辑工具横评|开源、免费为主
linux·pdf·onlyoffice
赵钰老师2 小时前
最新Hermes Agent 技能封装与科研自动化:以 Meta-Analysis 为例-实现从文献检索到绘图的一站式工作流
运维·chatgpt·自动化·ai编程·ai写作
.小小陈.2 小时前
深度拆解 Linux 进程间通信(IPC):从管道到 System V 全链路详解
linux·服务器·网络·学习
落羽的落羽2 小时前
【Linux系统】深入线程:多线程的互斥与同步原理,封装实现两种生产者消费者模型
java·linux·运维·服务器·c++·人工智能·python