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

news2024/9/24 15:25:24

四、进程程序替换

在这里插入图片描述

  • 之前用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程序,我们想让父进程聚焦在读取命令,解析命令,指派进程执行程序的功能上!

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STL中的神秘“指针”:迭代器

&#x1f680;write in front&#x1f680; &#x1f4dc;所属专栏&#xff1a;C学习 &#x1f6f0;️博客主页&#xff1a;睿睿的博客主页 &#x1f6f0;️代码仓库&#xff1a;&#x1f389;VS2022_C语言仓库 &#x1f3a1;您的点赞、关注、收藏、评论&#xff0c;是对我最大…

【Linux】- RPM 与 YUM

RPM 与 YUM 1.1 rpm 包的管理1.2 rpm 包的简单查询指令1.3 rpm 包的其它查询指令&#xff1a;1.4 卸载 rpm 包&#xff1a;2.1 安装 rpm 包3.1 yum3.2 yum 的基本指令3.3 安装指定的 yum 包3.4 yum 应用实例&#xff1a; 1.1 rpm 包的管理 介绍 rpm 用于互联网下载包的打包及安…

SDN系统方法 | 9. 接入网

随着互联网和数据中心流量的爆炸式增长&#xff0c;SDN已经逐步取代静态路由交换设备成为构建网络的主流方式&#xff0c;本系列是免费电子书《Software-Defined Networks: A Systems Approach》的中文版&#xff0c;完整介绍了SDN的概念、原理、架构和实现方式。原文: Softwar…

【Spring Boot丨序列化、反序列化】

序列化、反序列化 概述Jackson 序列化和反序列化简介自定义序列化器注册外部序列化程序&#xff1a; 指定类的 Json 序列化、反序列化 主页传送门&#xff1a;&#x1f4c0; 传送 概述 序列化是将对象转换为字节序列的过程&#xff0c;而反序列化则是将字节序列恢复为对象的过…

用于系统监控及进程管理python库之psutil

前言 对于一个job级别应用再进行测试的过程中&#xff0c;不可避免测试该服务的一些性能&#xff0c;比如占有cpu的使用量&#xff0c;使用的memory的大小等&#xff0c;比较简单的方式是在服务中起一个并行的线程&#xff0c;每隔一段时间打印这些关注量的大小&#xff0c;之后…

【二分答案】CF1661 C

Problem - C - Codeforces 题意&#xff1a; 思路&#xff1a; 在check的时候&#xff0c;我们要尽量用算贡献的思想&#xff0c;并且大胆贪心 Code&#xff1a; #include <bits/stdc.h>#define int long longusing namespace std;const int mxn3e510; const int mxe3…

MySQL基础扎实——列对比运算符是什么

词义解释 在MySQL中&#xff0c;用于进行列对比的运算符主要有以下几种&#xff0c;其实就是逻辑运算符号&#xff1a; 等号&#xff08;&#xff09;&#xff1a;用于判断两个列是否相等&#xff0c;例如&#xff1a;column_name value。 不等号&#xff08;<>或!&am…

Verilog语法学习——边沿检测

边沿检测 代码 module edge_detection_p(input sys_clk,input sys_rst_n,input signal_in,output edge_detected );//存储上一个时钟周期的输入信号reg signal_in_prev;always (posedge sys_clk or negedge sys_rst_n) beginif(!sys_rst_n)signal_in_prev < 0;else…

95. Python基础教程:异常处理try...except语句

【目录】 文章目录 1. try...except语法解析2. 程序异常3. except的4种使用方式3.1 单独的except3.2 except 异常名称3.3 except 异常类型 as 别名3.4 except (异常类型1,异常类型2) as 别名 4. 总结 【正文】 1. try…except语法解析 try[traɪ]&#xff1a;尝试。 except[…

【QT】Day4

1> 思维导图 2> 手动完成服务器的实现&#xff0c;并具体程序要注释清楚 widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTcpServer> //服务器类 #include <QTcpSocket> //客户端类 #include <QMessageBox> //…

综合能源系统(4)——综合能源系统建模方法

综合能源系统关键技术与典型案例  何泽家&#xff0c;李德智主编 本文主要从物理、信息、价值三个方面介绍综合能源系统关键技术&#xff0c;如图3-1所示。 物理方面&#xff1a;主要包括综合能源系统建模分析技术、规划设计(配置)技术、优化控制技术、运行维护技术和综合评…

mybatisPlus之逻辑删除解读

目录 为什么会有逻辑删除 逻辑删除基本介绍 逻辑删除的使用 局部使用 全局使用 为什么会有逻辑删除 在我们对数据进行增删查改的时候&#xff0c;对于删除操作来说&#xff0c;我们思考一个问题&#xff0c;在实际开发中我们真的会将数据完成从数据库中删除掉么&#xff1f…

二叉搜索树(二叉排序树)

文章目录 基本概念基本操作实现分析插入数据查找数据删除数据遍历数据 源码 基本概念 二叉搜索树也叫搜索二叉树、二叉排序树、排序二叉树。是一种对查找和排序都有用的特殊二叉树。 二叉搜索树&#xff08;Binary Search Tree&#xff0c;简称BST&#xff09; 如何构建一颗二叉…

【使用维纳滤波进行信号分离】基于维纳-霍普夫方程的信号分离或去噪维纳滤波器估计(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【Windows】WDS中如何跳过语言选择以及身份验证

WDS&#xff08;Windows Deployment Services&#xff09;是微软的一项网络服务&#xff0c;用于快速和方便地部署Windows操作系统到多台计算机上。它提供了一种自动化的方式来安装、配置和管理操作系统映像&#xff0c;使企业能够快速部署和更新大量的计算机系统。网上有很多W…

二叉搜索树的本质

引言 打算写写树形数据结构&#xff1a;二叉查找树、红黑树、跳表和 B 树。这些数据结构都是为了解决同一个基本问题&#xff1a;如何快速地对一个大集合执行增删改查。 本篇是第一篇&#xff0c;讲讲搜索树的基础&#xff1a;二叉搜索树。 基本问题 如何在一千万个手机号中…

设计模式-中介者模式在Java中使用示例-客户信息管理

场景 欲开发客户信息管理窗口界面&#xff0c;界面组件之间存在较为复杂的交互关系&#xff1a;如果删除一个客户&#xff0c; 要在客户列表(List)中删掉对应的项&#xff0c;客户选择组合框(ComboBox)中客户名称也将减少一个&#xff1b; 如果增加一个客户信息&#xff0c;…

SpringBoot2.7集成Swagger3.0和knife4j实现API接口文档开发

1. 概述 Swagger 3 是一个用于描述、构建和测试 RESTful Web 服务的开源工具集。它提供了一种简单而强大的方式来定义和文档化 API 接口&#xff0c;同时还具备自动生成客户端代码和服务器存根代码的功能。 Knife4j是一个集Swagger2 和 OpenAPI3为一体的增强解决方案&#xff…

DNSPod十问秦勇:为什么医疗AI最爱眼科?

本期嘉宾 秦勇 鹰瞳科技Airdoc COO 秦勇&#xff0c;鹰瞳科技&#xff08;Airdoc&#xff09;COO&#xff0c;中国人工智能学会智慧医疗专委会委员&#xff0c;长期从事医疗人工智能方向的工作&#xff0c;拥有丰厚的项目经验、管理经验和技术积累&#xff0c;参与多个医院人…

掌握Python的X篇_13+14_Python条件语句实例:判断闰年、成绩评定

前面学习了条件语句以及调试的基本技巧&#xff0c;本篇介绍两个与条件语句有关的实例&#xff0c;对前面的知识又深刻认识。 文章目录 1. 判断闰年1.1 版本11.2 版本21.3 一行代码太长的处理方法 2. 根据成绩评级 1. 判断闰年 用户输入年份&#xff0c;判断该年份是否为闰年…