一篇文章学会进程替换

进程替换是什么

fork之后,父子进程各自执行父进程的代码的一部分,父子代码共享,数据写时拷贝各自一份。
但是,如果子进程不想执行父进程的代码,就想执行一个全新的代码呢?
这就需要用到 进程程序替换

所谓的程序替换,就是某进程通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中
从而达到 去执行其他程序的目的
下面的图解释了进程程序替换的基本过程

当然,上面的替换过程是 用操作系统的相关接口即可完成

两个问题

  1. 进程替换有没有创建新的子进程 ?
    没有!因为这里仅仅是加载,重新建立映射关系,进程的pcb、优先级、状态这种内核结构根本没有发生变化,因此并没有创建新进程
  2. 如何理解所谓的将程序 放入 内存中?
    这个过程就是加载, 加载也有加载器(就像链接有链接器一样),操作系统帮你把数据从一个硬件(磁盘)搬到另一个硬件(内存),这个过程并不是从硬件层面上加载,而是操作系统提供了相关接口的!因此所谓的exec系列函数,本质就是如何加载程序的函数

基本操作

最简单的execl函数

execl函数
int execl(const char* path,const char* arg, ...)

首先要找到程序,path就是程序所在的路径 + 目标文件名。 注意path既可以是相对路径,也可以是绝对路径。
其中 ... 表示的是: 可变参数列表,即可以传入多个不定个数参数

除了path之外,后面的一串参数怎么传入呢? 这一串参数表示你找到的这个程序,你想要怎么执行他 👇
在命令行上怎么执行,就在这里参数一个一个怎么填
比如: ls -a -l 这条命令, 在这里就是分别传入/usr/bin/lsls-a-l 这四个参数 注意,都是字符串形式!
如果不知道ls所在的位置,就可以利用 which ls找到其所在位置
但是,必须注意的是,最后一个参数必须以 NULL结尾!表示参数传递完毕
即传入:/usr/bin/lsls-a-lNULL

当然上面这样运行的ls命令是没有颜色的, 如果需要颜色,就可以在加一个参数: --color=auto

int main(){
  cout <<"当前进程的开始代码"<<endl;     //成功打印
  
  execl("usr/bin/ls","ls","-l","-a","--color=auto",NULL);
  
  cout <<"当前进程的结束代码"<<endl;     //不打印
}

为什么 main函数中的 执行完execl函数之后,后面代码不打印呢??
因为,execl是程序替换,调用该函数成功之后,会将当前进程的所有代码和数据都进行替换,包括已经执行的和没有执行的!
执行完execl,代码和数据都换成了 usr/bin/ls的代码和数据,原来的代码都没了,去哪里执行呢?

excel返回值:

  • 调用成功:没有返回值
  • 调用失败:返回-1
    因为excel本身也属于原本进程的代码,替换成功之后,原本进程的代码和数据被替换掉,即excel本身和其返回值自然也被替换掉了
    所以execl的调用:
  • 调用成功: 代码和数据全部被替换,后续所有代码,全都不会被执行!
  • 调用失败:(比如调用不存在的命令),代码和数据不会被替换,会继续执行后续代码。

因此excel根本不需要函数返回值的判断,直接在excel后面加一个exit(1)即可,如果excel执行失败,后续代码就会执行,因此直接异常退出即可!

调用execl函数有两种方式:

  1. 不创建子进程
    不创建子进程就是在本进程中调用execl(上面那种),然后本进程的代码和数据被替换为目标进程的代码和数据,该进程后面的代码就无法执行了。但是,如果在本进程进行替换,那么本进程后面的代码就失效了,因此常常使用创建子进程的方式让其进行程序替换!
  2. 创建子进程
    让子进程去执行其他的代码,把子进程的程序做替换,子进程的程序和代码发生变化不会影响到父进程(独立性),子进程完成任务之后进行回收即可,父进程可以继续执行后面的代码并做收尾工作。

为什么要采用创建子进程的方式? 这里有一个通俗的例子:
如果我这样做:

while(1)
{
	1. 显示一个提示行:root@localhost#
	2. 获取用户输入的字符串,fgets,scanf 。 -> ls -a -l
	3. 对字符串进行解析
	4. 然后利用execl()进行程序替换,执行得到的字符串对应的程序
}

那么上面这段伪代码,得到的是什么? – 其实就是一个简单的shell

因此如果不创建子进程,替换的进程只能是父进程
如果创建了子进程:

  • 替换的进程就是子进程,而不影响父进程
  • 另一方面我们想让父进程聚焦在:读取数据、解析数据、指派进程执行代码的功能

注意
加载新程序之前,父子进程的数据和代码的关系: 代码共享,数据写时拷贝
当子进程加载新程序的时候,子进程要把自己的代码和数据进行替换,但是子进程的代码和数据都是继承自父进程。
数据进行替换的时候发生写时拷贝,可以做到。
但是,代码是和父进程共享的。 如果要把新的代码替换子进程的代码,即对代码进行写入。
也就意味着,必须将父子代码分离! 如果不分离,替换之后立马会影响父进程! 因此代码也会发生写时拷贝
这样,父子进程在代码和数据上就彻底分开了(虽然曾经并不冲突)

所以一般而言,父子进程代码数据共享,数据写时拷贝
但是在程序替换这种场景,代码和数据都要写时拷贝

其他函数

execv

int execv(const char* path, char* const argv[])
execl可以想象成是 exec + list,即后面的 l 我们看作list。 即他的参数像list的节点一样,一个一个往后跟
而execv可以看作 exec + vector,所以第二个参数开始传入参数的时候,把要传入的命令构建出一个 指针数组 (char*argv[]
当然同样需要以NULL结尾。 和execl其实没有本质区别,只是参数传递的方式有所不同。

char* const argv[] = {"ls","-a","-l",NULL};
execv("/usr/bin/ls",argv);

execlp

int execlp(const char* file,const char* arg, ...)
这个函数相对于execl来说,名字多了一个p,第一个参数从path变成file 也就是只需要给文件名,不需要给文件路径
但是要执行程序,必须先找到程序。 如果不带路径,如何找到程序呢?
— 利用环境变量可以找到

因此,execlp会自己在系统环境变量PATH中进行查找,不用告诉他要执行的程序在哪里。
(除此之外,带p的函数一般就是会自动去PATH中查找程序)

//用法:
execlp("ls","ls","-a","-l",NULL);

这里不免会产生一个问题
execlp("ls","ls","-a","-l",NULL) , 第一个已经传递了一个"ls", 为什么还要传递一个 “ls” ??

因为这里的两个ls表示的意义并不一样, 第一个 ls 表示 文件名

  1. 第一个ls 表示文件名,要执行谁,是为了找到程序
  2. 第二个ls 表示如何执行文件,即程序能找到,要传递什么选项

多知道一些

程序替换所用的后面的一系列参数,非常像main函数的命令行参数,他也是以NULL结尾的。
所以目标程序才能获得这些参数来执行
实际上,就是exec系列函数,把参数传递给 目标要替换程序的命令行参数
在这个例子里, 就是 "ls" "-a" "-l" NULL 这些参数,通过execlp函数,传递给 ls这个可执行程序的命令行参数来执行程序。

execvp

int execvp(const char*file , char* const argv[]
这个函数就要对比execv函数来看了,因为带了p,所以就是会自己在环境变量PATH中查找,其他的用法和execv一样

```c
char* const argv[] = {char*)"ls",
  (char*)"-a",
  (char*)"-l",
  NULL
};
execvp("ls",argv);

为什么是 char* const类型 而不是 const char*? 或者 const char* const 或者char*
看笔者这一片文章:👉戳我


如何执行其他程序自己写的C、C++程序

我想用我写的程序A,调用我写的程序B,如何实现?
以一个代码作为例子:
mycmd.cpp:

//mycmd.cpp
#include<iostream>
#include<string.h>
#include<stdlib.h>
using namespace std;
// 假设只有 mycmd -a/-b/-c 选项
// argc为命令行参数数量, argv是传递的命令行参数
int main(int argc,char* argv[]){
  
  if(argc!=2) {
    cout <<"can not execute"<<endl;// 如果一个选项都不传递,就无法执行
    exit(1);
  }
  

  //如果传递的第一个参数是a,就执行这个
  if(strcmp(argv[1],"-a") == 0){
    cout <<"hello a!"<<endl;
  }
  //如果传递的第一个参数是b
  else if(strcmp(argv[1],"-b") == 0){
    cout <<"hello b!"<<endl;
  }
  else{
    cout <<"default"<<endl; 
  }
  return 0;
}

exec.cpp:

#include<iostream>
using namespace std;
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
const char* path = "./mycmd";//相对路径
const char* path1= "/home/ky/process_replace/execute_other_cpp/mycmd"; //绝对路径
int main(){

  pid_t id = fork();
  //子进程进行程序替换
  if(id == 0){
    cout <<"子进程开始运行"<<endl;
    execl(path,"mycmd","-a",NULL); //传递mycmd.cpp生成的可执行程序 mycmd
    exit(1);
  }
  //父进程阻塞等待
  else{
    cout <<"父进程开始运行,pid:  " << getpid() <<endl;
    int status = 0;
    pid_t ret = waitpid(-1,&status,0);

    //等待子进程成功,子进程退出
    if(ret > 0){
      cout<<"wait success! the exit code is:"<<WEXITSTATUS(status) <<endl;
    }

  }
  return 0;
}

这里因为要执行另一个cpp程序即:mycmd
因此要用到mycmd.cpp编译好的mycmd可执行文件,因此可以利用makefile同时编译两个cpp文件 makefile一次形成两个可执行

如何执行其他语言的程序,如python

方法1:利用程序替换

因为相比于C/C++这种纯编译性语言,python、shell、java、javac这样的语言都是有对应的解释器的。解释器就是一个程序,python test.py 其实就是把test.py作为一个参数的形式,传递给python解释器这个可执行程序,然后再python程序内部直接去执行解释这个文件。
如下面是一个test.py文件

#! /usr/bin/python3.6
print("hello python")

# [root]$ python test.py   -> 命令行执行py文件

python test.py就对应ls -al
用到exec系列函数,那么path就对应的python解释器的文件路径,而后面就是在命令行执行的参数

# 子进程中:
execlp("python","python","test.py",NULL);

下面是一个test.sh即shell脚本

#! /usr/bin/bash
echo "hello shell"

执行 shell test.sh

用到exec系列函数,同理:

execl("/usr/bin/ bash","bash","test.sh",NULL); 

补充知识
这里注意到 #! /usr/bin/bash 这样的注释,是linux下对于脚本语言指定特定的脚本解释器。
linux系统下脚本特殊注释指定解释器

方法2:利用更改执行权限

以python文件为例,可以给文件添加执行权限, 然后利用./test.py即可运行
chmod +x test.py : 给test.py添加x权限
这样在执行这个python脚本,实际上它是会自动找到python的解释器,直接执行代码
这样如果带了可执行权限,就等价于:

execlp("./test.py","test.py",NULL)

也就是说,本身exec系列函数就可以执行任何程序(因为是系统调用)
注意,必须在test.py即脚本程序中指定解释器,不然就会出现下面的错误:


execle

int execle(const char* path, const char* arg, ... , char* const envp[]
对于基础的exec, l表示想怎么执行这个程序,就把程序一个一个的传递,所以第二个参数是一个可变参数列表
对于e表示环境变量,但是并没有带p,因此就要带全路径path,e则带了一个char* const envp[] 的参数,这是一个环境变量。即,自己主动传递环境变量。
因为最终目标要替换的进程的程序路径、参数选项、环境变量,是由execle这个系统接口,传给要替换的程序的main函数的
execle的使用场景:
execle用在需要传递环境变量的场景,如果期望替换后的程序需要使用原进程(或者原进程的父进程)的环境变量,那么可以使用execle函数传递环境变量

环境变量具有全局属性,可以被子进程继承下去
环境变量是一个指针数组,并且是kv模型, 每个元素都"key=value"
父进程的环境变量,子进程可以直接用

示例: exec调用mycmd, mycmd获取指定的环境变量,exec程序替换时传递环境变量给mycmd

// mycmd.cpp,获取 MY_VAR这个环境变量
#include<iostream>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
using namespace std;
// 假设只有 mycmd -a/-b/-c 选项
// argc为命令行参数数量, argv是传递的命令行参数
int main(int argc,char* argv[]){
  
  if(argc!=2) {
    cout <<"can not execute"<<endl;// 如果一个选项都不传递,就无法执行
    exit(1);
  }
  
  //获取环境变量
  printf("获取环境变量:%s\n",getenv("MY_VAR"));

  //如果传递的第一个参数是a,就执行这个
  if(strcmp(argv[1],"-a") == 0){
    cout <<"hello a!"<<endl;
  }
  //如果传递的第一个参数是b
  else if(strcmp(argv[1],"-b") == 0){
    cout <<"hello b!"<<endl;
  }
  else{
    cout <<"default"<<endl; 
  }
  return 0;
}

//exec.cpp ,调用execle函数,进程替换的同时,传递环境变量
#include<iostream>
using namespace std;
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
const char* path = "./mycmd";//相对路径
int main(){

  pid_t id = fork();
  // 定义环境变量
   char* const env[]={
    (char*)"MY_VAR=Process replace",
    (char*)"mode=easy",
    NULL 
  };

  //子进程进行程序替换
  if(id == 0){
    cout <<"子进程开始运行"<<endl;
   
    execle(path,"mycmd","-a",NULL,env); //进程替换
    
    exit(1);
  }

  //父进程阻塞等待
  else{
    cout <<"父进程开始运行,pid:  " << getpid() <<endl;
    int status = 0;
    pid_t ret = waitpid(-1,&status,0);

    //等待子进程成功,子进程退出
    if(ret > 0){
      cout<<"wait success! the exit code is:"<<WEXITSTATUS(status) <<endl;
    }
  }
  return 0;
}

execvpe

int execvpe(const char *file, char *const argv[],char *const envp[]);
p表示程序会在环境变量中找
v表示命令行参数的传递不是采用可变参数列表,而是采用一个char*数组来接受。简单

char* const env[]={
	(char*)"MY_VAR=123456789",
	NULL
};
char* const argv[]={(char*)"mycmd",(char*)"-a",NULL};
execvpe("mycmd",argv,env); //自动在PATH中找mycmd

前提是:要先把mycmd所在的文件夹导入到系统的环境变量PATH中才可以,因为这个mycmd是你自己写的程序。

echo $PATH  #输出当前环境变量
export PATH=$PATH:.  # 配置环境变量为之前的环境变量 + 当前目录

# : 是环境变量的分隔符

不用担心,这是更改的用户环境变量,退出窗口下次登录又回到默认了

总结

exec* 系列函数: 功能实际上就是加载器的底层接口,用这样的接口实际上就可以把任何程序加载进来。

区分execve和其他函数

  1. execl
  2. execlp
  3. execle
  4. execv
  5. execvp
  6. execvpe

上面这六个,严格意义上来讲,都不是系统直接提供给我们的!准确的说他们是C库函数,而系统直接给我们提供的类似程序替换的接口其实只有一个,而是 execve

int execve(const char* filename,char* const argv[],char* const envp[])
这个函数是系统内核可调用函数,即系统调用
如何验证呢? 上面的6个使用man 3查询,而execve是用man 2查询,这点就可以看出

注意:这里的execve没有带p,因此filename也是需要提供全路径的
当然在程序里也可以直接使用execve这个系统调用,而不使用上面的C库函数

但上面的6个接口,底层实际上都调用的 execve这个函数,他们会把接收到的参数做合并和其他处理,最后处理成符合 execve 接口参数的要求。

之所以要提供这些封装,是因为应用程序替换接口时的场景不一样,有时候知道程序的路径,有时候只知道程序名,有时候参数是一个个可以传,有时候必须使用数组,有时候要带环境变量,有时候不用带。
封装这些的最终目的就是满足上层调用的不同场景。

为什么要进行程序替换(总结)

一定和应用场景有关,我们有时候,必须让子进程来执行新的程序!!

  • 60
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值