【Linux进程】进程控制(下) {进程程序替换:程序替换的工作原理,程序替换函数exec*,简单的命令行解释器}

四、进程程序替换

在这里插入图片描述

  • 之前用fork创建子进程后,父子进程执行同一个程序的不同代码段。

  • 如何使子进程执行另一个不同的程序呢?子进程需要进行程序替换!

  • 程序替换,就是通过特定的接口,将磁盘上一个全新的程序(包括代码和数据)加载到调用进程的地址空间中。


4.1 程序替换的原理

在这里插入图片描述

在进行程序替换时,操作系统会将新程序的代码和数据加载到调用进程的地址空间中。这个过程通常包括以下几个步骤:

  1. 加载数据:操作系统会将新程序的代码和数据从磁盘或其他存储介质中读取到内存中。这些代码和数据会被加载到物理内存中的合适位置。

  2. 调整进程地址空间(重定位):由于新的程序可能与原有程序的地址空间不同,因此需要进行地址重定位。操作系统会根据新程序的要求,将程序中的地址引用进行调整,使其指向正确的物理内存位置。

  3. 更新页表:操作系统会更新进程的页表,以映射新程序的代码和数据所在的物理内存页。这样,进程就可以通过虚拟内存地址访问到正确的物理内存。

  4. 清理旧程序:操作系统会释放原有程序占用的物理内存页,以便为新程序腾出空间。这些旧的物理内存页会被标记为可重用,以供其他进程使用。

总的来说,程序替换是通过加载新程序的代码和数据到物理内存中,并进行地址重定位和页表更新来完成的。这样,进程就可以执行新的程序了。


4.2 程序替换函数

  • 子进程往往要调用exec*函数进行程序替换以执行另一个程序,exec*函数是加载器的底层调用接口。
  • 当进程调用exec*函数时,该进程用户空间的代码和数据完全被新程序替换,从新程序的启动例程开始执行。
  • 调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变。

下面是exec*系统调用:

在这里插入图片描述


4.2.1 execl函数

参数:

  1. path:待替换程序的所在路径+文件名
  2. arg, …:命令行参数,以字符串的形式一个个传入,最后以NULL结尾(标识参数传递完毕)。

返回值:

  1. exec函数只有发生错误失败时才会返回,返回值是-1。

  2. exec函数一旦调用成功,后续的所有代码都不会执行,也根本不需要有返回值。

提示:

  1. execl拆解速记:exec(execute) - l(list)
  2. 命令行参数表和环境变量表都是以NULL结尾,用于表示结束。

测试代码:

#include <iostream>    
#include <unistd.h>    
using namespace std;    
    
int main(){    
  cout << "当前进程的开始代码!" << endl;    
  execl("/usr/bin/ls", "ls", "-a", "-l", "--color=auto", NULL);                                                                  
  cout << "当前进程的结束代码!" << endl; //程序被替换,所以不会被打印   
}    

测试结果:

在这里插入图片描述


4.2.2 execv函数

与execl比较,只是第二个参数argv不同。

argv:命令行参数表(字符指针数组),数组元素同样要以NULL结尾。

其他特性和execl一模一样

execv拆解速记:exec(execute) - v(vector)

测试代码:

#include <iostream>          
#include <unistd.h>    
#include <sys/wait.h>    
using namespace std;    
    
int main(){    
  pid_t id = fork();                                           
  if(id == 0)                                                        
  {                                                              
    //子进程执行流        
    cout << "I'm child process! child_pid:" << getpid() << endl;    
    char *const argv[] = {(char*)"ls", (char*)"-a", (char*)"-l", (char*)"--color=auto", NULL};    
    execv("/usr/bin/ls", argv);                                                                   
  }                                
  else if(id > 0)         
  {                                
    //父进程执行流                                                  
    int status = 0;                                                                               
    pid_t ret = waitpid(id, &status, 0);    
    if(ret > 0)                             
    {              
      if(WIFEXITED(status))                    
      {                                                              
        cout << "子进程正常退出!child_pid:" << ret;    
        cout << " exit_code:" << WEXITSTATUS(status) << endl;    
      }
      else{
        cout << "子进程崩溃!child_pid:" << ret;
        cout << " exit_signal:" << (status & 0x7F) << endl;
      }                                                                                                                          
    }
    else if(ret == -1)
    {
      cout << "等待子进程失败!" << endl;
    }
  }
  else{
    perror("子进程创建失败!");
    return 1;
  }
}

测试结果:

在这里插入图片描述


4.2.3 execlp函数

与execl比较,只是第一个参数file不同。

file:待替换的程序名,该程序的所在路径必须在PATH环境变量中。

其他特性和execl一模一样

execlp拆解速记:exec(execute) - l(list) - p(PATH)

测试代码:

//......  
  if(id == 0)    
  {    
    //子进程执行流    
    cout << "I'm child process! child_pid:" << getpid() << endl;    
    execlp("ls", "ls", "-a", "-l", NULL);    
//...... 

运行结果:

在这里插入图片描述


当然也可以执行我们自己编写的代码

可以使用绝对路径或相对路径:

  1. 绝对路径:execv("/home/zty/code/Linux/20230721/test", argv);
  2. 相对路径:execv("./test", argv);

测试代码:这里测试的是之前写过的一个接收命令行选项参数的简单程序

测试结果:

在这里插入图片描述

补充内容:makefile一次性构建多个可执行程序

.PHONY:all    
all:myproc test    

myproc:myproc.cc    
g++ $^ -o $@ -std=c++11    
test:test.cc    
g++ $^ -o $@ -std=c++11    

.PHONY:clean    
clean:    
rm -f myproc      

甚至还可以执行其他语言编写的程序

shell脚本

#! /usr/bin/bash 

echo "hello shell!"

运行命令:bash test.sh

将进程替换为shell程序:execlp("bash", "bash", "test.sh", NULL);

python脚本

#! /usr/bin/python3.6

print("hello Python/n")

运行命令:python test.py

将进程替换为Python程序:execlp("python", "python", "test.py", NULL);

如果python脚本文件具有可执行权限:execlp("./test.py", "test.py", NULL);

注意:

  1. shell,Python,Java等编程语言拥有自己的解释器,通过解释器可以直接运行所编写的程序,无需编译生成可执行程序。
  2. 可以给脚本文件加上可执行权限,然后直接./test.py运行程序。实际仍然是通过解释器执行程序的。

4.2.4 execle函数

与execl相比,新增了第三个参数envp

envp:环境变量表(字符指针数组),数组元素同样要以NULL结尾。

其他特性和execl一模一样

execle拆解速记:exec(execute) - l(list) - e(environment)

测试代码:

//......  
  if(id == 0)    
  {    
    //子进程执行流    
    cout << "I'm child process! child_pid:" << getpid() << endl;    
    char *const _env[] = {"MYENV=122", NULL}; //设置MYENV环境变量
    execle("./test", "test", "-e", "NULL", _env);//test -e选项打印MYENV环境变量
//...... 

测试结果:

在这里插入图片描述

提示:环境变量具有全局属性,是因为可以通过类似于execle的系统调用,将父进程环境变量表传递给子进程。


4.2.5 execve函数

在这里插入图片描述

execve是一个系统调用函数,用于在Linux系统中执行一个新的程序。它的参数解释如下:

  1. const char *filename:要执行的程序的路径。可以是绝对路径,也可以是相对路径。

  2. char *const argv[]:一个字符串数组,用于传递给新程序的命令行参数。数组的最后一个元素必须为NULL,表示参数列表的结束。

  3. char *const envp[]:一个字符串数组,用于传递给新程序的环境变量。数组的最后一个元素必须为NULL,表示环境变量列表的结束。如果envp为NULL,则新程序将继承当前进程的环境变量。

execve函数的返回值是一个整数,如果执行成功,它不会返回,而是直接在当前进程中加载并执行新程序。如果发生错误,返回值为-1,并设置errno来指示具体的错误类型。

注意:

  1. execve函数会替换当前进程的代码和数据,将其替换为新程序的代码和数据。因此,execve函数之后的代码将不会被执行。
  2. 如果希在执行新程序后继续执行其他操作,可以使用fork函数创建一个子进程,在子进程中调用execve函数,而在父进程中继续执行其他操作。

execve是真正的系统调用,而上面的6个接口实际上是系统提供的基本封装。 他们会将接收到的参数进行合并处理,最后底层还是会调用execve:

在这里插入图片描述

在这里插入图片描述
总结exec*函数的命名方式:

  1. +l/v:命令行参数以可变参数列表或者指针数组传入
  2. +p:是否在环境变量PATH中查找程序路径
  3. +e:是否自己维护环境变量表

4.3 简单的命令行解释器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

char cmd_line[1024]; //用于接收存储整条命令
char* cmd_param[32]; //将整条命令拆解成一个个参数
char env_buffer[64]; //环境变量缓冲区

//shell运行原理:父进程接收并解析命令,创建子进程执行命令,父进程等待。
int main(){
  //0.命令行解释器是常驻内存进程,不退出
  while(1)
  {
    //1.打印提示信息:[root@localhost myshell]#
    printf("[root@localhost myshell]# ");
    fflush(stdout); //[1]
    //sleep(1);
    
    //2.获取用户输入(包括指令和选项):"ls -a -l -i"
    memset(cmd_line, '\0', sizeof(cmd_line));    
    if(fgets(cmd_line, sizeof(cmd_line), stdin) == NULL) continue; //[2]
    if(strcmp(cmd_line, "\n") == 0) continue; //[2]
    cmd_line[strlen(cmd_line)-1] = '\0'; //[3]
    //printf("%s\n", cmd_line);
   
    //3.命令行字符串解析:"ls -a -l" --> "ls" "-a" "-l"
    cmd_param[0] = strtok(cmd_line, " "); //[4]
    int i = 1;
    while(cmd_param[i++] = strtok(NULL, " "));
    //for(int i = 0; cmd_param[i]; ++i)
    //{
    //  printf("%s ",cmd_param[i]);
    //}
    //printf("\n");
    
	//4.内置命令:让父进程(shell)自己执行的指令,又叫内建命令
    //内建命令本身就是shell中的一个函数调用
    if(strcmp(cmd_param[0], "cd") == 0) //cd指令切换父进程(shell)工作目录
    {
      if(cmd_param[1] != NULL)
      chdir(cmd_param[1]); //[5]
      continue;
    }
    if(strcmp(cmd_param[0], "export") == 0) //export指令导出环境变量,使其可被子进程继承
    {    
      if(cmd_param[1] != NULL)    
      {    
        strcpy(env_buffer, cmd_param[1]); //[6]
        putenv(env_buffer);  //[7] 
      }    
      continue;    
    }    
	//5.创建子进程执行命令:
    int id = fork();
    if(id == 0)
    {
      printf("I'm child process! pid:%d ppid:%d\n", getpid(), getppid());
      execvp(cmd_param[0], cmd_param);
      exit(1);
    }
	
	//6.父进程(shell)等待子进程,获取退出状态,回收资源
    int status = 0;
    int ret = waitpid(-1, &status, 0); //阻塞等待
    if(ret > 0)
    {
      if(WIFEXITED(status))
      {
        //正常退出返回退出码
		printf("normal exit! child_pid:%d exit_code:%d\n", ret, WEXITSTATUS(status));
      }
      else
      {
		//异常退出返回退出信号
        printf("abnormal exit! child_pid:%d exit_signal:%d\n", ret, status&0x7F);
      }
    }
    else if(ret < 0)
    {
      printf("Waiting failed!\n");
    }
  }
}

设计流程:

  1. 命令行解释器是常驻内存进程,不退出
  2. 打印提示信息:[root@localhost myshell]#
  3. 获取用户输入(包括指令和选项):“ls -a -l -i”
  4. 命令行字符串解析:“ls -a -l” --> “ls” “-a” “-l”
  5. 内置命令:让父进程(shell)自己执行的指令,又叫内建命令,内建命令本身就是shell中的一个函数调用
  6. 创建子进程执行命令
  7. 父进程(shell)等待子进程,获取退出状态,回收资源

解释:

  • [1] 由于打印的提示信息不带’\n’,所以缓冲区中的数据不会自动刷新到显示器,需要手动fflush(stdout)强制刷新,使其立马显示在屏幕上。
  • [2] 如果没有获取到任何字符或者获取失败直接continue;需要注意的是’\n’也会被读取,如果只读取到’\n’也要continue;
  • [3] 需要将获取到的最后一个换行符替换为’\0’,否则会被当做命令的一部分处理。
  • [4] C库函数strtok的用法:C 库函数 – strtok() | 菜鸟教程 (runoob.com)
  • [5] strtok工作原理:将指定的分隔符替换为’\0’,将原字符串分割。依次返回子串的首字符地址。
  • [6] Linux系统调用chdir:
    • 功能:用于改变当前进程的工作目录;这意味着后续的文件操作(如打开文件、读写文件)将在新的当前目录下进行。
    • 头文件:<unistd.h>
    • 参数:切换的路径
    • 返回值:成功返回0,失败返回-1
  • [7] cmd_param保存的是cmd_line中各命令行参数的首字符地址。memset会将cmd_line清空,导致环境变量丢失。因此需要将环境变量拷贝到缓冲区。
  • [8] Linux系统调用putenv:
    • 功能:用于设置环境变量
    • 头文件:<stdlib.h>
    • 参数:环境变量键值对字符串
    • 返回值:设置成功返回0,失败返回非0值

提示:

  1. shell执行的命令通常有两种:
    • 外部命令:第三方提供的在磁盘中有具体二进制文件的可执行程序(由子进程执行),如:ls,ps,pwd,我们编写的程序
    • 内置命令:shell内部自己实现的方法,由父进程(shell)自己执行,这些命令就是要影响shell本身,如:cd,export
  2. 进程的环境变量会被子进程继承,并且子进程进行程序替换并不会替换环境变量相关的内容
  3. shell的环境变量是从哪里来的?
    • 最初的环境变量是保存在配置文件(shell脚本)中的。shell启动的时候,通过读取配置文件,获得起始环境变量。

为什么要程序替换?

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

为什么要创建子进程执行程序?

为了不影响父进程,如上面的shell程序,我们想让父进程聚焦在读取命令,解析命令,指派进程执行程序的功能上!

如果不创建子进程,我们就只能替换父进程程序了

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

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

相关文章

OBS 迁移--华为云

一、创建迁移i任务 1. 登录管理控制台。 2. 单击管理控制台左上角的 在下拉框中选择区域。 3. 单击“ 服务列表 ”&#xff0c;选择“ 迁移 > 对象存储迁移服务 OMS ”&#xff0c;进入“ 对象存储迁移服务 ”页面。 4. 单击页面右上角“ 创建迁移任务 ”。 5. 仔细阅读…

Verilog语法学习——LV4_移位运算与乘法

LV4_移位运算与乘法 题目来源于牛客网 [牛客网在线编程_Verilog篇_Verilog快速入门 (nowcoder.com)](https://www.nowcoder.com/exam/oj?page1&tabVerilog篇&topicId301) 题目 题目描述&#xff1a; 已知d为一个8位数&#xff0c;请在每个时钟周期分别输出该数乘1/…

Python爬取IP归属地信息及各个地区天气信息

一、实现样式 二、核心点 1、语言&#xff1a;Python、HTML&#xff0c;CSS 2、python web框架 Flask 3、三方库&#xff1a;requests、xpath 4、爬取网站&#xff1a;https://ip138.com/ 5、文档结构 三、代码 ipquery.py import requests from lxml import etree # 请求…

了解Unity编辑器之组件篇Miscellaneous(九)

一、Aim Constraint&#xff1a;是一种动画约束&#xff0c;用于使一个对象朝向另一个对象或一个指定的矢量方向 Activate按钮&#xff1a;用于激活或停用Aim Constraint。当Aim Constraint处于激活状态时&#xff0c;其约束效果将应用于目标对象。 Zero按钮&#xff1a;用于将…

Spring Boot单元测试入门指南

Spring Boot单元测试入门指南 JUnit是一个成熟和广泛应用的Java单元测试框架&#xff0c;它提供了丰富的功能和灵活的扩展机制&#xff0c;可以帮助开发人员编写高质量的单元测试。通过JUnit&#xff0c;开发人员可以更加自信地进行重构、维护和改进代码&#xff0c;同时提高代…

Skin Shader 使用自动生成的Thickness

Unity2023.2的版本&#xff0c;Thickness 自动化生成&#xff0c;今天测试了一把&#xff0c;确实不错。 1.Render 设置 在Project Settings->Graphics->HDRP Global Settings中 Frame Setting->Rendering->Compute Thickness 打开 2.Layer设置 2.1添加Layer&…

如何提高自己的软件测试水平之bug定位

同学们在面试投简历的时候会经常看到人家公司JD上写的要求之一&#xff0c;如下&#xff1a; 这句话大家不要以为随便写写的&#xff0c;在我工作的十几年过程中起码见过10个以上试用期没过的公司新人&#xff0c;公司在衡量一个测试工程师是否专业的标准之一就是&#xff1a;…

Kotlin基础(八):泛型

前言 本文主要讲解kotlin泛型&#xff0c;主要包括泛型基础&#xff0c;类型变异&#xff0c;类型投射&#xff0c;星号投射&#xff0c;泛型函数&#xff0c;泛型约束&#xff0c;泛型在Android中的使用。 Kotlin文章列表 Kotlin文章列表: 点击此处跳转查看 目录 1.1 泛型基…

opencv-28 自适应阈值处理-cv2.adaptiveThreshold()

什么是自适应阈值处理? 对于色彩均衡的图像&#xff0c;直接使用一个阈值就能完成对图像的阈值化处理。但是&#xff0c;有时图像的色彩是不均衡的&#xff0c;此时如果只使用一个阈值&#xff0c;就无法得到清晰有效的阈值分割结果图像。 有一种改进的阈值处理技术&#xff…

Mybatis-Plus学习笔记,包含mybatis-plus基本使用,各种插件使用等等

&#x1f600;&#x1f600;&#x1f600;创作不易&#xff0c;各位看官点赞收藏. 文章目录 Mybatis-Plus笔记1、简介2、Mybatis-Plus Demo 程序3、Mybatis-Plus 常见注解4、Mybatis-Plus 条件构造器 Wrapper5、Mybatis-Plus 插件5.1、乐观锁插件5.2、分页插件5.3、逻辑删除插件…

OJ练习第145题——并行课程 III

力扣链接&#xff1a;2050. 并行课程 III 题目描述 给你一个整数 n &#xff0c;表示有 n 节课&#xff0c;课程编号从 1 到 n 。同时给你一个二维整数数组 relations &#xff0c;其中 relations[j] [prevCoursej, nextCoursej] &#xff0c;表示课程 prevCoursej 必须在课…

Python(四十七)列表对象的创建

❤️ 专栏简介&#xff1a;本专栏记录了我个人从零开始学习Python编程的过程。在这个专栏中&#xff0c;我将分享我在学习Python的过程中的学习笔记、学习路线以及各个知识点。 ☀️ 专栏适用人群 &#xff1a;本专栏适用于希望学习Python编程的初学者和有一定编程基础的人。无…

脚手架(vue-cli)的安装详细教程

首先要下载node.js 下载 | Node.js 中文网 (nodejs.cn)https://nodejs.cn/download/ 大家根据自己的系统来选择哪个&#xff0c;我是Windows系统&#xff0c;所以选择红色箭头所指的安装包去安装&#xff01;&#xff01;&#xff01; 接下来双击安装&#xff01;&#xff01;…

面试之CurrentHashMap的底层原理

首先回答HashMap的底层原理? HashMap是数组链表组成。数字组是HashMap的主体&#xff0c;链表则是主要为了解决哈希冲突而存在的。要将key 存储到&#xff08;put&#xff09;HashMap中&#xff0c;key类型实现必须计算hashcode方法&#xff0c;默认这个方法是对象的地址。接…

【深度学习笔记】Softmax 回归

本专栏是网易云课堂人工智能课程《神经网络与深度学习》的学习笔记&#xff0c;视频由网易云课堂与 deeplearning.ai 联合出品&#xff0c;主讲人是吴恩达 Andrew Ng 教授。感兴趣的网友可以观看网易云课堂的视频进行深入学习&#xff0c;视频的链接如下&#xff1a; 神经网络和…

C++数据结构笔记(11)二叉树的#号创建法及计算叶子节点数

首先分享一段计算叶子节点数目的代码&#xff0c;如下图&#xff1a; 不难发现&#xff0c;上面的二叉树叶子节点数目为4。我们可以采用递归的方式&#xff0c;每当一个结点既没有左结点又没有右节点时&#xff0c;即可算为一个叶子结点。 int num0; //全局变量&#xff0c;代…

会议OA系统会议管理模块开发思路(layui搭建)

目录 一.为什么要进行开发 1.开发目的 2.项目流程 A.发起会议请求过程 1.首先实现我们的多选下拉框功能&#xff01; 2.时间组件功能&#xff0c;并且提交我们新增加的会议内容 3.在进行发起会议编码时遇到的问题&#xff0c;BUG 3.1.有点时候js访问不到路径 3.2在增加…

Mac/win开发快捷键、vs插件、库源码、开发中的专业名词

目录 触控板手势&#xff08;2/3指&#xff09; 鼠标右键 快捷键 鼠标选择后shift⬅️→改变选择 mac command⬅️&#xff1a;删除←边的全部内容 commadtab显示下栏 commandshiftz向后撤回 commandc/v复制粘贴 command ⬅️→回到行首/末 commandshift3/4截图 飞…

C语言每日一题:4.消失的数字+数字在升序数组中出现的次数+整数转换

消失的数字&#xff1a; 思路1&#xff1a;排序遍历 1.使用qsort排序数组判断当前数值1是否是数组下一个元素的数值。 2.如果是一直循环注意数组越界&#xff0c;如果不是那么当前的数组的数值1就是消失的数。 3.存在0——n的数字是第n个数没有了。循环过程中从头到尾也找不到这…

适用于虚拟环境的免费企业备份软件

多年来&#xff0c;许多行业严重依赖物理服务器提供计算资源——你可以想象到巨大的服务器机房和笨重的服务器的场景。 然而&#xff0c;随着业务快速增长&#xff0c;许多组织发现物理服务器已经无法有效利用计算资源。因此&#xff0c;为了节省成本&#xff0c;引入了虚拟服…