【Linux】进程程序替换 + 模拟实现简易shell

前言

上一节我们介绍了 **进程终止**和 **进程等待**等一系列问题,并做了相应的验证,本章将继续对进程控制进行介绍,重点学习进程程序替换,并进行相应验证,在此基础上,自己模拟实现一个shell,该shell能够实现执行命令操作。。

目录

    • 1.进程程序替换
      • 1.1为什么要做进程程序替换?
      • 1.2 进程程序替换的原理
      • 1.3 六个exec替换函数
        • 1.3.1 execl函数:
        • 1.3.2 execv 函数:
        • 1.3.3 execlp函数:
        • 1.3.4 execvp函数:
        • 1.3.5 execle函数:
        • 1.3.6 execve函数:
      • 1.4 实现简易版shell
        • 1.4.1 Shell 内建命令等问题的解决

1.进程程序替换

概念引入:

将可执行程序加载到内存,并且重新调整 子进程的页表映射,使之指向新的进程的代码和数据段,这种过程就叫做程序替换

  • 用fork创建子进程后执行的是和父进程相同的程序,因为代码共享
  • 但,如果我们想让创建出来的子进程,执行全新的程序呢,此时就需要用到进程的程序替换
  • 进程程序替换就相当于一个加载器的角色

1.1为什么要做进程程序替换?

原因:

  • 原因是我们想让我们的子进程执行一个全新的程序。
  • 不同语言写的功能(比如python shell) 互相调用,这就是为什么要有程序替换的原因。

我们一般在服务器设计(Linux编程)的时候,往往需要子进程干两件种类事情

  • 1.让子进程执行父进程的代码片段(代码共享)
  • 2.让子进程执行磁盘中一个全新的程序(使用shell脚本, 想让客户端执行对应的程序,通过我们的进程,执行其他人写的进程代码等等)

1.2 进程程序替换的原理

程序替换的原理:

  • 将磁盘中的待执行的程序,加载入内存结构
  • 重新建立页表映射,谁执行程序替换,就重新建立子进程的映射关系

效果:让我们的父进程和子进程彻底分离,并让子进程执行一个全新的程序!

![

](https://i-blog.csdnimg.cn/direct/f4c725a0e19742b0831dec30e48d9525.png)

在这里插入图片描述
调整子进程的页表,让其不再与父进程代码和数据有任何关系,而是指向自己的代码和自己的数据区

当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

总结:

  • 说白了就是让fork创建子进程,不想让子进程执行父进程代码片段。
  • 我们想让子进程执行磁盘当中全新的程序,而且我们没有创建新的进程。
  • 因为子进程的内核数据结构基本没变,只是重新建立了虚拟到物理的映射关系罢了

1.3 六个exec替换函数

程序替换的是子进程:(重点)
进程替换永远影响的是进程的本身,子进程的替换永远不会影响父进程,因为进程具有独立性
重新建立的是页表映射但并不影响内核数据结构的具体情况.

其实有六种以exec开头的函数,统称exec函数:

#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 execve(const char *path, char *const argv[], char *const envp[]);

函数解释

  • 这些函数如果调用成功,则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值(注意:此函数调用失败,才有返回值

函数命名理解

  • (list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量
    在这里插入图片描述
1.3.1 execl函数:
int execl(const char *path, const char *arg, ...);
path:这个是路径,可执行程序的路径
arg: 可变参数,可以传多个参数,参数要以列表形式写,比如(1s -1 -a) ,就要写成"ls",“-l”,“-a”,NULL, 
注意:最后必须是NULL结尾,表示参数传递完毕。

代码演示:

#include <stdio.h>
#include <unistd.h>int main()
{//让我的程序执行系统上的: ls -a -i这样的一个命令printf("我是一个进程,我的pid是 : %d\n", getpid());//int ret = execl("/usr/bin/ls", "ls", "-l", "-a", NULL); //带选项//替换失败的情况int ret = execl("/usr/bin/lsssss", "ls", "-l", "-a", NULL); //带选项printf("我执行完毕了,我的pid : %d, ret = %d\n", getpid(), ret);return 0;
}
  • 一旦替换成功,是将当前进程的代码和数据全部替换了!!
  • 前一个printf被执行是因为程序替换并没有执行。
1.3.2 execv 函数:
int execv(const char *path, char *const argv[]); %%
实现的功能和execl一模一样。
path:这个是路径,可执行程序的路径
argv[]: 和execl的唯一区别就是传参方式的不一样,这个要传入数组har* const argv_[] = {"ls","-l","-a","-i", NULL};
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main()
{printf("我是父进程,我的pid是:%d\n", getpid());pid_t id = fork();if(id == 0){//子进程//我们要子进程执行全新的程序,以前我们是子进程执行父进程的代码片段printf("我是子进程,我的pid是:%d\n", getpid());//char* const argv_[] = {//    (char*)"ls",//    (char*)"-l",//    (char*)"-a",//    (char*)"-i",//    NULL//};char* const argv_[] = {(char*)"top",NULL };//execv("/usr/bin/ls", argv_);execv("/usr/bin/top", argv_);   }//一定是父进程int status = 0;int ret = waitpid(id, &status, 0);if(ret == id){sleep(2);printf("进程等待成功!\n");}return 0;
}
1.3.3 execlp函数:
int execlp(const char *file, const char *arg, ...);
file: 你想执行程序的名字,注意:命名中带P的,可以不用带可执行程序的路径。
arg:以列表形式传参

代码演示:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>//有时候不想让父进程做一件事,只想让子进程做一件事
//将进程创建引入进来int main()
{printf("我是父进程,我的pid是:%d\n", getpid());pid_t id = fork();if(id == 0){printf("我是子进程,我的pid是:%d\n", getpid());execlp("ls", "ls", "-a", "-l", "-i", NULL);//这里出现了两个ls,含义一样吗?-- 不一样!//第一个参数是供系统去找要执行谁的指令,后面一坨是表示如何执行该指令exit(100); //只要执行了exit,就意味着,execl系列的函数失败了 -- 进程替换失败了}//一定是父进程int status = 0;int ret = waitpid(id, &status, 0);%0代表阻塞等待if(ret == id){sleep(2);printf("wait success, ret : %d, 我所等待子进程的退出码: %d, 退出信号是: %d\n", ret, (status >> 8) & 0xFF, status & 0x7F);}return 0;
}
1.3.4 execvp函数:

作用和execIp是一样的,只不过传参形式不一样。。

int execvp(const char *file, char *const argv[]);
file: 你想执行程序的名字,注意:命名中带P的,可以不用带可执行程序的路径。
arg:以数组形式传参

代码演示:

    char* const argv_[] = {(char*)"top",NULL };execvp("ls", argv_ );
1.3.5 execle函数:

多了一个参数,是环境变量。

int execle(const char *path, const char *arg, ..., char * const envp[]);
path:这个是路径,可执行程序的路径
arg: 可变参数,可以传多个参数,参数要以列表形式写,比如(1s -1 -a) ,就要写成"ls",“-l”,“-a”,NULL, 
注意:最后必须是NULL结尾,表示参数传递完毕。envp:环境变量

代码演示:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main()
{//环境变量的指针声明extern char** environ;printf("我是父进程,我的pid是:%d\n", getpid());pid_t id = fork();if(id == 0){ printf("我是子进程,我的pid是:%d\n", getpid());//我们来手动导入一个环境变量char* const env_[] = {(char*)"MYPATH=You Can See Me!!",NULL };//e: 添加环境变量给目标进程,是覆盖式的!//execle("./mycmd", "mycmd", NULL, env_);//execle("/usr/bin/ls", "ls", NULL, env_);execle("./mycmd", "mycmd", NULL, environ);exit(100); //只要执行了exit,就意味着,execl系列的函数失败了 -- 进程替换失败了}//一定是父进程int status = 0;int ret = waitpid(id, &status, 0);if(ret == id){sleep(2);printf("进程等待成功!\n");}return 0;
}

使用execle()添加环境变量给目标进程,是覆盖式的!会把原来的environ环境变量都改掉
所以环境变量只剩下MYPATHT

正确做法是:将全部环境变量传过去,将environ传过去。

在这里插入图片描述

补充重点1:

  • 子进程会继承父进程的环境变量的,当父进程调用fork()创建子进程时,子进程会继承父进程的所有环境变量。
  • 当子进程调用execlp()等函数执行其他程序时,子进程也会继承父进程的环境变量.
  • 如果需要在子进程中更改环境变量,可以使用setenv()或putenv()等函数进行更改.
  • 更改的环境变量只会影响当前进程和它的子进程,并不会影响父进程或其他进程的环境变量

补充重点2:
ls 是一个常见的系统命令, 它通常位于系统的某个标准路径(如 /bin 或 /usr/bin),即使 PATH 为空,execlp() 会检查这些标准路径,找到 ls 的可执行文件并执行它。

  1. 如果PATH环境变量为空,execlp()函数会无法在环境变量中查找可执行文件的路径。但是,execle()函数会检查一些默认路径,例如/bin、/usr/bin等,来查找可执行文件。因此,即使PATH为空execle()函数也可能会在这些默认路径中找到可执行文件并执行它

  2. 但是,如果在默认路径中也找不到可执行文件,则execle()函数会执行失败,并将errno设置为ENOENT,表示无法找到可执行文件。因此,如果需要执行特定路径下的可执行文件,最好使用execv()或execve()等函数,并指定可执行文件的完整路径。这样可以避免依赖PATH环境变量来查找可执行文件的路径。

1.3.6 execve函数:
int execve(const char *path, char *const argv[], char *const envp[]);

有了上面的execle()函数的讲解,理解就不复杂了,只是第二个参数传的不同,这里传的是一个指针数组。
在这里插入图片描述

为什么有那么多的接口?

  • 目的是:适配应用场景。
  • 其实上述函数都是对系统调用接口的封装。

1.4 实现简易版shell

只要我们懂得了程序替换的原理,会用程序替换的接口,就很好理解:

  • shell本身执行起来就是个死循环
  • shell创建子进程,将子进程给替换掉就ok了

要写一个shell,需要循环以下过程

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父等待子进程退出(wait)
#include <stdio.h>
#include <string.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>#define SEP " "
#define MAX_CMD 1024
#define SIZE 128char command_line[MAX_CMD];
char* command_args[SIZE];char env_buffer[NUM];extern char** environ;//对应上层的内建命令
int ChangeDir(const char* new_path)
{chdir(new_path);return 0;//调用成功
}void PutEnvInMyShell(char* new_env)
{putenv(new_env);
}int main()
{//shell本质就是一个死循环while(1){//不关心获取这些属性的接口,搜索一下都有//1.显示提示符printf("[用户名@我的主机名 当前目录]# ");fflush(stdout);//2.获取用户输入memset(command_line, '\0', sizeof(command_line)); //初始化//从键盘获取,标准输入,stdin,获取到的是C风格的字符串(stdio.h结尾的),'\0'结尾fgets(command_line, NUM, stdin);command_line[strlen(command_line) - 1] = '\0';//清空\n回车//printf("%s\n", command_line);//3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分 -- 因为这些参数一定得以列表或者数组方式传递给程序替换接口//shell必须切分,因为必须调用execl函数//将第一个字符串地址用0号下标指向,第二个字符串地址用1号下标指向 command_args[0] = strtok(command_line, SEP);int index = 1;//给ls命令添加颜色: 如果提取出来的程序名是ls -- 1下标设置成改颜色的if(strcmp(command_args[0], "ls") == 0) command_args[index++] = (char*)"--color=auto";//strtok截取成功返回字符串起始地址//截取失败,返回NULLwhile(command_args[index++] = strtok(NULL, SEP));//for debug//int i = 0;//for(i = 0; i < index; i++)//{//    printf("%d : %s\n", i, command_args[i]);//}//4.TODO -- 编写后面的逻辑,内建命令(由父Shell自己实现的自己调用的一个函数)if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL){//让调用方进行路径切换,父进程ChangeDir(command_args[1]);continue;}//走到这里一定是将命令行参数解析完了,包括命令 + 选项//将环境变量的信息导入在了父进程的上下文当中if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL){//环境变量列表(是个指针数组,每个元素是个指针指向一个环境变量)//我们传的是一个字符串首地址,但是环境变量的内容还是我们自己维护的//目前,环境变量信息在comman_line,会被清空,那么环境变量当然就没有了//所以此处我们需要自己保存一下环境变量的内容strcpy(env_buffer, command_args[1]);PutEnvInMyShell(env_buffer);//PutEnvInMyShell(command_args[1]);//MYENV=112233continue;}//5.创建进程Fork,执行//如果自己直接程序替换的话,就把自己写的shell给替换了pid_t id = fork();if(id == 0){//子进程//6.程序替换//execvpe(command_args[0], command_args, environ);execvp(command_args[0], command_args);exit(1);//执行到这里,子进程一定替换失败了}int status = 0;pid_t ret = waitpid(id, &status, 0);if(ret > 0){printf("等待子进程成功: sig: %d, code: %d\n", status & 0x007F, (status & 0xFF00) >> 8);}}//end whilereturn 0;
}
1.4.1 Shell 内建命令等问题的解决
  • cd命令的处理
    在命令行中使用cd指令,会跳转路径,如果使用绝对命令,就不行了

  • 一个进程也存在对应路径,进程对应的路径可以理解成:当进程启动的时候,在那个路径启动,这个进程所在的路径就是当前进程所启动的路径。

  • 如果我们不对cd进行特殊处理,则子进程路径切换后,并不影响父进程的路径,会发现命令路径还是没变化。

所以,对应这个命令,就不能用程序替换的方式,来执行一些特殊的命令了。
而是,在父进程中将一些命令,单独处理。

重点:

程序替换影响的是子进程和父进程没关系,子进程一 跑就完了,曾经所有的操作就没有意义,路径切换就没意义了,所以我们要让父进程的路径发生变化。
如果有些行为,是必须让父进程shell执行的,不想让子进程执行,绝对不能创建子进程!只能是父进程自己实现对应的代码!

正确做法:
使用系统中,更改工作目录的函数。
在这里插入图片描述

 if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL){//让调用方进行路径切换,父进程ChangeDir(command_args[1]);continue;}
  • 内建命令:
    由shell自己执行的命令,我们称之为内建(内置bind- in)命令。

  • export的处理:
    导入环境变量:

  • export不是一个可执行程序和cd,ls,cat等指令不同。

  • export是一个shell内置命令,用于设置环境变量。它并不是一个可执行程序,而是由shell解释器直接执行的命令

所以,在使用execvp进行程序替换的时候,是不能替换成功的!
在这里插入图片描述
注意:

  • 环境变量是属于系统的数据,子进程在执行程序替换时,当前进程的环境变量数据,不会被替换掉,而且是以父进程为模版继承下来的
  • 所以才会让父进程以内建命令的方式putenv,子进程就能直接获取了.
  • 环境变量会被子进程继承下去,所以他会有全局属性。
  • 必须,Export放在父进程的内建命令中实现,因为放在子进程中,父进程内的环境变量,就没有改变,是有问题的。。

尾声
看到这里,相信大家对这个Linux有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦

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

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

相关文章

Redis分布式锁-Redisson可重入锁原理的个人见解。

记录Redisson可重入锁的个人见解。 文章目录 前言一、什么叫做锁的重入&#xff1f;二、Redisson可重入锁原理 前言 ⁣⁣⁣⁣ ⁣⁣⁣⁣ 之前在写项目的时候&#xff0c;注意到Redisson可重入锁的一个问题&#xff0c;随即在网上搜索其对应的资料&#xff0c;下面就记录一下个…

昇思25天学习打卡营第14天 | ShuffleNet图像分类

昇思25天学习打卡营第14天 | ShuffleNet图像分类 文章目录 昇思25天学习打卡营第14天 | ShuffleNet图像分类ShuffleNetPointwise Group ConvolutionChannel ShuffleShuffleNet模块网络构建 模型训练与评估数据集训练模型评估模型预测 总结打卡 ShuffleNet ShuffleNetV1是旷世科…

鸿蒙实训笔记

第一天 #初始化一个新的NPM项目(根据提示操作) npm init #安装TSC、TSLint和NodeJS的类型声明 npm install -s typescript tslint types/node 在根目录中新建一个名为tsconfig.json的文件&#xff0c;然后在代码编辑器中打开&#xff0c;写入下述内容&#xff1a; {"co…

MATLAB激光通信和-积消息传递算法(Python图形模型算法)模拟调制

&#x1f3af;要点 &#x1f3af;概率论和图论数学形式和图结构 | &#x1f3af;数学形式、图结构和代码验证贝叶斯分类器算法&#xff1a;&#x1f58a;多类型&#xff1a;朴素贝叶斯&#xff0c;求和朴素贝叶斯、高斯朴素贝叶斯、树增强贝叶斯、贝叶斯网络增强贝叶斯和半朴素…

网络层重点协议—IP协议

在复杂的网络环境中确定一个合适的路径 协议头格式如下&#xff1a; 4位版本号(version) 指定协议的版本&#xff08;IPV4-4,IPV6-6&#xff09; 4位首部长度(header length) IP头部的长度是多少个32bit&#xff0c;也就是length*4的字节数。4bit表示最大的数字是15&#x…

【密码学】密码学数学基础:群的定义

一、群的定义 在密码学中&#xff0c;群&#xff08;Group&#xff09;的概念是从抽象代数借用来的&#xff0c;它是一种数学结构&#xff0c;通常用于描述具有特定性质的运算集合。 群的定义 群定义中的几个关键要素&#xff1a; 集合&#xff1a;首先&#xff0c;群是由一系…

AutoMQ 中的元数据管理

本文所述 AutoMQ 的元数据管理机制均基于 AutoMQ Release 1.1.0 版本 [1]。 01 前言 AutoMQ 作为新一代基于云原生理念重新设计的 Apache Kafka 发行版&#xff0c;其底层存储从传统的本地磁盘替换成了以对象存储为主的共享存储服务。对象存储为 AutoMQ 带来可观成本优势的…

draggable 实现一个简单的拖拽

拖拽区域代码 <draggable v-if="activeFirstIndex !== 8" :list="showResourseList" :group="{ name: resources, pull: clone, put: false }" :sort="false" :multiple="false" :move="onMove1" @end="…

【JavaScript 算法】冒泡排序:简单有效的排序方法

&#x1f525; 个人主页&#xff1a;空白诗 文章目录 一、算法原理二、算法实现三、应用场景四、优化与扩展五、总结 冒泡排序&#xff08;Bubble Sort&#xff09;是一种基础的排序算法&#xff0c;通过重复地遍历要排序的数列&#xff0c;一次比较两个元素&#xff0c;如果它…

【香橙派 AIpro测评:探索高效图片分类项目实战】

前言 最近入手了一块香橙派 AIpro开发板&#xff0c;在使用中被它的强大深深震撼&#xff0c;有感而发写下这篇文章。 本文旨在深入探讨OrangePi AIpro的各项性能&#xff0c;从硬件配置、软件兼容性到实际应用案例&#xff0c;全方位解析这款设备如何在开源社区中脱颖而出&am…

案例 | 人大金仓助力山西政务服务核心业务系统实现全栈国产化升级改造

近日&#xff0c;人大金仓支撑山西涉企政策服务平台、政务服务热线联动平台、政务网、办件中心等近30个政务核心系统完成全栈国产化升级改造&#xff0c;推进全省通办、跨省通办、综合业务受理、智能审批、一件事一次办等业务的数字化办结进程&#xff0c;为我国数字政务服务提…

数据结构(Java):LinkedList集合Stack集合

1、集合类LinkedList 1.1 什么是LinkedList LinkedList的底层是一个双向链表的结构&#xff08;故不支持随机访问&#xff09;&#xff1a; 在LinkedList中&#xff0c;定义了first和last&#xff0c;分别指向链表的首节点和尾结点。 每个节点中有一个成员用来存储数据&…

构建高效智能标准化仓库

在快节奏的现代商业环境中&#xff0c;仓库作为供应链的核心枢纽&#xff0c;其运营效率与管理水平直接影响着企业的整体竞争力。一个“高效智能标准化的仓库”&#xff0c;不仅是货物有序存储的空间&#xff0c;更是降本增效、提升客户满意度的关键所在。 在传统工厂管理模式下…

AI Agent 开发综合指南

本文介绍了 ReAct 模式以改进功能&#xff0c;并演示了如何从头开始创建 AI 代理。它涵盖了测试、调试和优化 AI 代理&#xff0c;以及工具、库、环境设置和实施。本教程为用户提供了创建有效 AI 代理所需的技能&#xff0c;无论他们是开发人员还是爱好者。 NSDT工具推荐&#…

【Linux】01.Linux 的常见指令

1. ls 指令 语法&#xff1a;ls [选项] [目录名或文件名] 功能&#xff1a;对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xff0c;将列出文件名以及其他信息 常用选项&#xff1a; -a&#xff1a;列出当前目录下的所有文件&#xff0c;包含隐藏文件…

从 Pandas 到 Polars 十八:数据科学 2025,对未来几年内数据科学领域发展的预测或展望

我在2021年底开始使用Polars和DuckDB。我立刻意识到这些库很快就会成为数据科学生态系统的核心。自那时起&#xff0c;这些库的受欢迎程度呈指数级增长。 在这篇文章中&#xff0c;我做出了一些关于未来几年数据科学领域的发展方向和原因的预测。 这篇文章旨在检验我的预测能力…

开始Linux之路

人生得一知己足矣&#xff0c;斯世当以同怀视之。——鲁迅 Linux操作系统简单操作指令 1、ls指令2、pwd命令3、cd指令4、mkdir指令(重要)5、whoami命令6、创建一个普通用户7、重新认识指令8、which指令9、alias命令10、touch指令11、rmdir指令 及 rm指令(重要)12、man指令(重要…

记录自己Ubuntu加Nvidia驱动从入门到入土的一天

前言 记录一下自己这波澜壮阔的一天&#xff0c;遇到了很多问题&#xff0c;解决了很多问题&#xff0c;但是还有很多问题&#xff0c;终于在晚上的零点彻底放弃&#xff0c;重启windows。 安装乌班图 1.安装虚拟机 我开始什么操作系统的基础都没有&#xff0c;网上随便搜了…

JDBC基础 -获取连接的方式、结果集、批处理、事务处理、连接池、Apache-DBUtils

文章目录 概述快速入门(增删改)获取数据库的五种方式方式一&#xff1a;获取Driver实现类对象方式二&#xff1a;反射方式三&#xff1a;使用DriverManager代替Driver方式四&#xff1a;Class.forName自动完成注册驱动&#xff08;推荐&#xff09;方式五&#xff1a;使用prope…

请你谈谈:BeanDefinition类作为Spring Bean的建模对象,与BeanFactoryPostProcessor之间的羁绊

那么&#xff0c;我们如何理解Spring Bean的建模对象呢&#xff1f;简而言之&#xff0c;它是指用于描述和配置Bean实例化过程的模型对象。有人可能会提出疑问&#xff0c;既然只需要Class&#xff08;类&#xff09;就可以实例化一个对象&#xff0c;Class作为类的元数据&…