一篇文章学会进程替换

进程替换是什么

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 接口参数的要求。

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/528660.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MybatisPlus分页插件的使用

目录 &#x1f9c2;1.添加mybatisPlus依赖 &#x1f953;2.添加配置类 &#x1f32d;3.添加分页接口 &#x1f37f;4. 添加实现类 &#x1f95e;5.测试 1.添加mybatisPlus依赖 <!--mybatisPlus--><dependency><groupId>com.baomidou</groupId>&l…

linux中查看占用端口的进程方法

1、netstat -tlnp | grep 端口号 netstat -tlnp|grep 3306 其中&#xff1a;95115是进程号&#xff0c;mysqld是进程名称 2、ss -ltnp | grep 端口号 注意&#xff1a;-tlnp和-ltnp ss -ltnp|grep 3306其中&#xff1a;mysqld是进程名称&#xff0c;95115是进程id 3、lsof…

【Python 基础知识课程】Python的第一个程序

Python 简介 Python 是一种功能强大且用途广泛的编程语言&#xff0c;广泛用于数据科学、Web 开发、自动化等高需求领域。 幸运的是&#xff0c;对于初学者来说&#xff0c;它也是一种很好的学习语言&#xff0c;因为Python代码更容易阅读和编写。它的简单性使其成为初学者的完…

R语言绘图 | 散点小提琴图

原文链接&#xff1a;R语言绘图 | 散点小提琴图 本期教程 写在前面 本期的图形来自发表在Nature期刊中的文章&#xff0c;这样的基础图形在日常分析中使用频率较高。 获得本期教程数据及代码&#xff0c;后台回复关键词&#xff1a;20240405 绘图 设置路径 setwd("You…

我是如何从功能测试成功转岗测试开发的?记录下我的面试经验

由于这段时间我面试了很多家公司&#xff0c;也经历了之前公司的不愉快。所以我想写一篇文章来分享一下自己的面试体会。希望能对我在之后的工作或者面试中有一些帮助&#xff0c;也希望能帮助到正在找工作的你。 一 找工作 壹&#xff0f; 我们总是草率地进入一个自己不了解…

岩土工程监测振弦采集仪在隧道工程中的监测与应用

岩土工程监测振弦采集仪在隧道工程中的监测与应用 岩土工程监测是隧道工程的重要环节之一&#xff0c;而振弦采集仪作为岩土工程监测中的关键设备之一&#xff0c;在隧道工程中的应用十分重要。本文将从振弦采集仪的基本原理、在隧道工程中的监测与应用以及其优点和局限性等方…

面试(01)————JVM篇,最大白话的一集,常见概念的讲解以及GC监控调优等等

一、JDK体系结构图 二、JVM整体架构 三、JVM组成 3.1、JVM内存区域的执行底层原理 ​编辑 3.1.1、程序计数器 3.1.2、堆栈关系的发现 3.1.3、方法去和堆的关系 3.1.4、堆&#xff08;重点&#xff09; 3.1.4.1、可达性分析算法 3.1、内存泄漏测试以及堆区的GC监控 3.…

【论文解读】大模型事实性调查(上)

一、简要介绍 本调查探讨了大型语言模型&#xff08;llm&#xff09;中的事实性的关键问题。随着llm在不同领域的应用&#xff0c;其输出的可靠性和准确性变得至关重要。论文将“事实性问题”定义为llm产生与既定事实不一致的内容的概率。论文首先深入研究了这些不准确性的含义…

kali基础渗透学习,永恒之蓝,木马实战

简介 kali的学习本质是在linux上对一些攻击软件的使用&#xff0c;只是学习的初期 先在终端切换到root用户&#xff0c;以便于有些工具对权限的要求 下载链接 镜像源kali 攻击流程 公网信息搜集 寻找漏洞&#xff0c;突破口&#xff0c;以进入内网 进入内网&#xff0c…

GD32F470_SHT20温湿度传感器模块/数字型温湿度测量模块 I2C通讯小体积模块

2.24 SHT20温湿度传感器 由瑞士Sensirion推出的 SHT20数字温湿度传感器&#xff0c;基于领先世界的CMOSens 数字传感技术&#xff0c;具有极高的可靠性和卓越的长期稳定性。全量程标定&#xff0c;两线数字接口&#xff0c;可与单片机直接相连&#xff0c;大大缩短研发时间、…

物联网数据服务平台

随着物联网技术的迅猛发展&#xff0c;海量数据的产生和应用成为推动工业数字化转型的核心动力。在这个数据为王的时代&#xff0c;如何高效地收集、处理、分析并应用这些数据&#xff0c;成为了企业关注的焦点。物联网数据服务平台应运而生&#xff0c;为企业提供了全面、高效…

考研数学|打基础看张宇《30讲》还是武忠祥《基础篇》?

这题我会啊&#xff0c;基础阶段我还是推荐张宇老师 因为张宇老师和武忠祥老师的实力都很厉害&#xff0c;最主要的区别就是讲课的风格。我比较喜欢张宇老师的讲课风格&#xff0c;比较幽默风趣&#xff0c;能够调动课堂氛围了和学生思维。这一点在考研初期&#xff0c;帮助我…

区块链与数字身份:探索Facebook的新尝试

在数字化时代&#xff0c;随着区块链技术的崛起&#xff0c;数字身份成为了一个备受关注的话题。作为全球最大的社交媒体平台之一&#xff0c;Facebook一直在探索如何利用区块链技术来改善数字身份管理和用户数据安全。本文将深入探讨Facebook在这一领域的新尝试&#xff0c;探…

AI大模型基石:文字与数字的起源与演变

AI大模型基石&#xff1a;文字与数字的起源与演变 1、文字 1.1、起源 我们的祖先在还没有发明文字和语言之前就已经开始使用“咿咿呀呀”的声音来传播信息了&#xff0c;比如在野外活动遇到危险&#xff0c;然后发出“咿咿呀呀”的声音来提醒同伴小心&#xff0c;同伴在接收到…

u-tabs徽标改颜色并随着鼠标点击而改色

在uview官网中没找到改色的api&#xff0c;然后就查看源码&#xff0c;发现通过修改源码能实现上图效果&#xff0c;本次组件用的uview 2x版本 修改文件u-tabs文件&#xff0c;然后把依赖文件带过来&#xff0c;如图下&#xff1a; 然后修改my_tabs.vue文件&#xff08;即原u-…

一站式解读多模态——Transformer、Embedding、主流模型与通用任务实战(下)

本文章由飞桨星河社区开发者高宏伟贡献。高宏伟&#xff0c;飞桨开发者技术专家&#xff08;PPDE&#xff09;&#xff0c;飞桨领航团团长&#xff0c;长期在自媒体领域分享AI技术知识&#xff0c;博客粉丝9w&#xff0c;飞桨星河社区ID为GoAI 。分享分为上下两期&#xff0c;本…

Windows下的ASLR保护机制详解及其绕过

写在前面&#xff1a; 本篇博客为本人原创&#xff0c;但非首发&#xff0c;首发在先知社区 原文链接&#xff1a; https://xz.aliyun.com/t/13924?time__1311mqmxnQ0%3DqGwx2DBqDTlpzeG%3DKT8qQTID&alichlgrefhttps%3A%2F%2Fxz.aliyun.com%2Fu%2F74789各位师傅有兴趣的…

设计模式总结-抽象工厂模式

抽象工厂模式 模式动机模式定义模式结构模式分析模式实例与解析实例一&#xff1a;电器工厂 模式动机 在工厂方法模式中具体工厂负责生产具体的产品&#xff0c;每一个具体工厂对应一种具体产品&#xff0c;工厂方法也具有唯一性&#xff0c;一般情况下&#xff0c;一个具体工…

4.8QT

将按钮3&#xff0c;基于qt4版本连接实现点击按钮3&#xff0c;实现关闭窗口。 widget.cpp #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget), btn3(new QPushButton(this)) {ui->s…

D1084 5A低压差电压调整器应用方案,内含电流限制和热保护功能,防止任何过载时产生过高的结温。

1、 概述&#xff1a; D1084是一款具有5A输出能力、低压差为1.5V的三端稳压器。输出电压可通过电位器调节或1.5V, 1.8V, 3.3V三个固定电压版。内含电流限制和热保护功能&#xff0c;防止任何过载时产生过高的结温。D1084系列电路有标准TO-220、TO-263和TO-252封装形式。 2、 典…