当前位置: 首页 > news >正文

Linux进程控制(2)

Linux进程控制(2)

📟作者主页:慢热的陕西人

🌴专栏链接:Linux

📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言

本博客主要内容讲解了进程等待收尾内容和进程的程序替换,以及进程程序替换的原理,进程程序替换的7个重要接口

文章目录

  • Linux进程控制(2)
    • 1.进程等待(续)
    • 2.进程程序替换
      • 2.1 程序替换是如何完成的---单线程版
      • 2.2程序替换的原理
      • 2.3引入多进程,使用所有程序替换的接口
        • 熟悉所有的替换程序接口(7个)

1.进程等待(续)

我们稍微改造一下,之前进程等待的时候,父进程不要阻塞等待的代码,让父进程真正的去运行一些任务。

我们采用函数回调的方式,让父进程在等待子进程的时候也可以去运行自己的一些任务!

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>#define TASK_NUM 10//预设一批任务
void sync_disk()
{printf("这是一个刷新数据的任务\n");
}void sync_log()
{printf("这是一个同步日志的任务\n");
}void net_send()
{printf("这是一个网络发送的任务\n");
}                                                                                                                                                            //保存相关的任务
typedef void (*func_t)(); //定义了一个函数指针类型
func_t orther_task[TASK_NUM] = {NULL}; //装载任务
int Load_Task(func_t fuc)
{int i = 0;for(; i < TASK_NUM; ++i){if(orther_task[i] == NULL) break;}if(TASK_NUM == i) return -1;else orther_task[i] = fuc;return 0;
}//初始化函数指针数组
void Init_Task()
{for(int i = 0; i < TASK_NUM; ++i) orther_task[i] = NULL;Load_Task(sync_disk);  Load_Task(sync_log);Load_Task(net_send);
}void Run_Task()
{for(int i = 0; i < TASK_NUM; ++i){if(orther_task[i] == NULL) continue;else orther_task[i]();}
}int main()
{pid_t id = fork();if(id == 0){//子进程int cnt = 5;while(cnt){printf("我是子进程,我还活着呢,我还有%dS,我的pid:%d,我的ppid:%d\n", cnt--, getpid(), getppid());sleep(1);}exit(0);}Init_Task();while(1){int status = 0;pid_t ret_id = waitpid(id, &status, WNOHANG);// 夯住了if(ret_id < 0){printf("waitpid_error\n");}else if(ret_id == 0){Run_Task();                                                                                                                                          sleep(1);continue;}else{printf("我是父进程,我等待成功了,我的pid:%d,我的ppid:%d, ret_id: %d, child exit code: %d, child exit signal:%d\n",getpid(), getppid(), ret_id, (status >> 8)&0xFF, status & 0x7F);exit(0);}sleep(1); }return 0;
}

运行结果:

image-20231109151934831

继续改进,我们之前获取进程退出码的时候是使用(status >> 8)& 0xFF的方式来进行获取的,那么实际上C库也给我们提供了两个宏来帮助我们获取进程的退出码:

status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

我们用这两个宏来优化一下我们等待成功,也就是子进程结束的时候的代码:

      else                                                                                       {                                                                                            //等待成功                                                                                   if(WIFEXITED(status))                                                       {                                                                                         //正常退出                                                                                 printf("wait success, child exit code :%d", WEXITSTATUS(status));                           }                                                                                             else {                                                                                             //异常退出                                                                                printf("wait success, child exit signal :%d", status & 0x7F);    }            exit(0);    }   

正常退出:

image-20231109153300831

异常退出:我们尝试在父进程等待的时候杀掉子进程:

image-20231109153505131

2.进程程序替换

我们为什么需要创建子进程?为了让子进程帮我执行特定的任务;

①让子进程执行父进程的一部分代码;

②如果子进程指向一段全新的代码呢?这时候我们就需要进程的程序替换!

也是为什么需要进程的程序替换。

2.1 程序替换是如何完成的—单线程版

代码:

#include<stdio.h>    
#include<stdlib.h>    
#include<unistd.h>    int main()    
{    printf("begin......\n");    printf("begin......\n");    printf("begin......\n");    printf("begin......\n");    printf("begin......\n");    execl("/bin/ls", "ls", "-a", "-l", NULL);                                                           printf("end........\n");    printf("end........\n");    printf("end........\n");    printf("end........\n");    printf("end........\n");    return 0;    
}  

运行结果:

那么我们可以看到这个进程运行了开始的begin....,然后运行了ls,但是后面的end...却不见了。

这是因为发生了进程的程序的替换,简要的原理就是,操作系统通过提供的地址/bin/ls从磁盘中拿出ls然后选到指定的文件ls,在输入一些参数-a, -l,以NULL表示结束。

image-20231109162541803

2.2程序替换的原理

操作系统不动当前进程的内核数据结构,而是去磁盘内部拿到要替换的数据和代码,将我们当前进程的数据和代码替换掉。

当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。

所以进程的程序替换,是没有创建新的进程的。

image-20231109163349868

①站在进程的角度

操作系统,帮我们在磁盘内部找到我们要替换的数据和代码,替换进程的数据和代码。

②站在程序的角度

这个程序被加载了,所以我们也称execl这类函数为加载器

我们在回到一开始,为什么我们程序后面的end.....却没有打印出来?

image-20231109164919209

原因是当我们加载程序替换的时候,新的数据和代码就进入了进程,当前进程后续没有没运行的代码就成为了老代码,直接被替换了,没有机会执行了。

所以进程的程序替换是整体替换,而不是局部替换

所以我们接下来引入多进程的程序替换

2.3引入多进程,使用所有程序替换的接口

例程:

#include<stdio.h>    
#include<stdlib.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<sys/wait.h>    int main()    
{    pid_t id = fork();    if(id == 0)    {    //child    printf("我是子进程:%d", getpid());    execl("/bin/ls", "ls", "-a", "-l", NULL);    }    sleep(2);    //father    waitpid(id, NULL, 0);    printf("我是父进程:%d\n", getpid());                                                                                                                      return 0;    
}

运行结果:

我们看到execl之后父进程的内容也被运行了

因为进程的独立性,所以进程的程序替换只会影响调用程序替换的子进程

子进程加载新程序的时候,是需要进行程序替换的,发生写时拷贝(子进程执行的可是全新的代码啊,新的代码,所以代码区也可以发生写时拷贝)

image-20231109172035019

那么对于execl这类加载函数,它有没有返回值呢?

答案是分情况:

①替换成功是没有返回值的

②替换失败是有返回值的-1

原因是:假设替换替换成功了,那么我们该进程中的代码和数据,都会被替换成新的代码和数据,那么我们之前的返回值也就不复存在了,并且我们也不需要返回值了。

替换失败的情况下,进程之前的代码和数据还是存在的,那么我们的返回值也是存在的,从而可以返回。

所以我们调用了加载函数之后我们不用去判断它是否加载成功,只需要在函数后面返回异常即可。

失败的例程:

#include<stdio.h>    
#include<stdlib.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<sys/wait.h>    int main()    
{    pid_t id = fork();    if(id == 0)    {    //child    printf("我是子进程:%d\n", getpid());                                                                                                                    execl("/bin/lsss", "lsss", "-a", "-l", NULL);    exit(1);    }    sleep(2);    //father    int status;    waitpid(id,&status, 0);    printf("我是父进程:%d, child exit signal:%d\n", getpid(), WEXITSTATUS(status));    return 0;    
} 

image-20231109173257987

熟悉所有的替换程序接口(7个)

int execl(const char *path, const char *arg, ...);

例如:execl("/bin/ls", "ls", "-a", "-l", NULL);

l代表list;

path:路径,也就是告诉操作系统,你要用来替换的程序,在磁盘的哪个路径,例程里的/bin/ls

arg:文件,是你要用来替换的文件名,例程里面的ls

其中的...是可变参数列表,例程中的那些参数-a , -l

其中最后我们要特别的输入一个NULL参数,告诉函数参数结束了;

int execv(const char *path, char *const argv[]);

相比较第一个函数的差别就是,第一个函数要求我们一个一个的去传参数,而第二个要求我们直接用数组的形式去传,但是原则也是一样的,数组的最后一个元素也要置成NULL

我们在子进程内部调用的时候是这样的:

先创建一个数组,将这些参数一个一个放进去,再传给execv即可

那么其实v就是vector就是数组的意思;

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

运行结果:

image-20231109175259752

int execlp(const char *file, const char *arg, ...);

p:当我们指定执行程序的时候,只需要指定程序名即可,系统会自动在**环境变量PATH**中查找。

也就是说我们要用于替换的程序,必须在环境变量PATH中,或者说我们在环境变量PATH中设置过;

 execlp("ls", "ls", "-a", "-l", NULL);    

image-20231109180335737

那么其中的两个ls是不一样的,一个是文件名,一个是参数。

int execvp(const char *file, char *const argv[]);

v:表示参数以数组的形式传入;

p:表示在环境变量PATH中去寻找用于替换的文件;

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

运行结果:

image-20231109181025149

int execle(const char *path, const char *arg, ..., char * const envp[]);

envp[]:叫做自定义环境变量,当我们不想使用系统默认的环境变量的时候,这个时候我们就传递一个envp

比如我们现在要让我们的调用exec目录下的ortherproc来替换myproc的子进程的后续代码

image-20231109182618129

先用execl尝试一下:

 execl("./exec/ortherproc", "ortherproc", NULL);  

运行结果:

image-20231109182932018

换成的动态的效果再看看:

动态效果

下来我们尝试用execle来实现一下:

proc.c

   char* const envp[] =                                                                                                             {                                                                                                                                "MYENVP=UCanCMe!",    NULL    };    execle("./exec/ortherproc", "ortherproc",envp);  

ortherproc.cc

for(int i = 0; i < 5; ++i)      
{      cout << "我是另一个程序,我的PID是 :" << getpid() << endl;      cout << "MYENVP: " << (getenv("MYENVP")==NULL ? "NULL" : getenv("MYENVP")) << endl;           cout << "PATH: " << (getenv("PATH") == NULL ? "NULL" : getenv("PATH")) << endl;      sleep(1);                                                                     
}     

运行结果:

我们看到自定环境变量打印出来了,但是操作系统内部的环境变量却不见了,所以我们可以得到一个结论:

自定义环境变量覆盖了,默认的环境变量;

image-20231109193230574

我们传默认的环境变量试试:

extern char ** environ;execle("./exec/ortherproc", "ortherproc",NULL , environ);  

运行结果:

image-20231109193621091

那么如果我们两个都要呢,那么有一个接口putenv给我们提供了一个将自定义环境变量追加到进程的默认环境变量的方法:接下来我们尝试一下

   putenv("MYENVP=UCanCMe");    execle("./exec/ortherproc", "ortherproc", NULL, environ);  

运行结果:

image-20231109201734620

插播一段

我们知道环境变量具有全局属性,可以被子进程继承下去,那么操作系统是怎么办到的?

只需要用execle的最后一个参数传过去即可!

那么我们是不是不需要putenv也能实现两个都能被子进程读取到呢?

我们直接把自定义的环境变量exportbash中试试:

[mi@lavm-5wklnbmaja lesson6]$ export MYENVP=UCanCMe
[mi@lavm-5wklnbmaja lesson6]$ echo $MYENVP
UCanCMe

运行结果:

我们发现是可行的,自定义环境变量-----> bash ----->父进程------>子进程

image-20231109202438896

int execvpe(const char *file, char *const argv[], char *const envp[]);

p:不需要指定路径,只要在环境变量内部即可;

v:参数以数组的形式传入;

e:环境变量数组传入;

使用方法都与上面的类似。

int execve(const char *filename, char *const argv[], char *const envp[]);

这个接口也不用过多介绍了,使用方法都是一样的。

那么我们需要注意的是,在linux的man手册中将区域六个接口都放在了3号手册,唯独这个却放在了2号手册。

其实操作系统只给我们提供了一个程序替换的接口execve,剩下的几个接口都是由这个接口封装出来的。

image-20231109204020146

并且我们程序替换的时候不仅可以替换C语言的,甚至其他的语言都可以替换,我上面的例子也做到了用C++替换,因为这些代码都是交给操作系统来处理的而不是编译器,所以不论是什么语言都是可以替换的!

到这本篇博客的内容就到此结束了。
如果觉得本篇博客内容对你有所帮助的话,可以点赞,收藏,顺便关注一下!
如果文章内容有错误,欢迎在评论区指正

在这里插入图片描述

相关文章:

Linux进程控制(2)

Linux进程控制(2) &#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;Linux &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 本博客主要内容讲解了进程等待收尾内容和进程的程序…...

Android Glide transform旋转rotate圆图CircleCrop,Kotlin

Android Glide transform旋转rotate圆图CircleCrop&#xff0c;Kotlin import android.graphics.Bitmap import android.os.Bundle import android.util.Log import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import com.bumptech.glide.load…...

如何让群晖Audio Station公开共享的本地音频公网可访问?

文章目录 1. 本教程使用环境&#xff1a;2. 制作音频分享链接3. 制作永久固定音频分享链接&#xff1a; 之前文章我详细介绍了如何在公网环境下使用pc和移动端访问群晖Audio Station&#xff1a; 公网访问群晖audiostation听歌 - cpolar 极点云 群晖套件不仅能读写本地文件&a…...

生态环境领域基于R语言piecewiseSEM结构方程模型

结构方程模型&#xff08;Sructural Equation Modeling&#xff0c;SEM&#xff09;可分析系统内变量间的相互关系&#xff0c;并通过图形化方式清晰展示系统中多变量因果关系网&#xff0c;具有强大的数据分析功能和广泛的适用性&#xff0c;是近年来生态、进化、环境、地学、…...

spring boot+netty 搭建MQTT broken

一、项目结构 二、安装依赖 <!-- netty包 --><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.75.Final</version></dependency><!-- 常用JSON工具包 --><…...

从零开始搭建React+TypeScript+webpack开发环境-使用iconfont构建图标库

创建iconfont项目 进入iconfont官网&#xff0c;完成注册流程&#xff0c;即可创建项目。 无法访问iconfont可尝试将电脑dns改为阿里云镜像223.5.5.5和223.6.6.6 添加图标 在图标库里选择图标&#xff0c;加入购物车 将图标添加到之前创建的项目中 生成代码 将代码配置到项目…...

微服务之初始微服务

文章目录 一、服务架构演变1.单体架构2.分布式架构 二、认识微服务三、总结四、微服务技术对比五、SpringCloud注意 一、服务架构演变 1.单体架构 单体架构&#xff1a;将业务的所有功能集中在一个项目中开发&#xff0c;打成一个包部署。 优点&#xff1a; 架构简单部署成本…...

大口径智能水表支持最高水流量是多少?

随着科技的不断发展&#xff0c;我国城市化进程的加快&#xff0c;水资源管理日益受到重视。作为一种先进的用水计量设备&#xff0c;大口径智能水表凭借其高精度、低误差、远程抄表等优点&#xff0c;在市场上备受青睐。那么接下来&#xff0c;小编就来为大家详细的介绍一下大…...

在Spring Boot中使用MyBatis访问数据库

MyBatis&#xff0c;这个对各位使用Java开发的开发者来说还是蛮重要的&#xff0c;我相信诸位在企业开发项目的时候&#xff0c;大多数采用的是Mybatis。使用MyBatis帮助我们解决各种问题&#xff0c;实际上这篇文章&#xff0c;基本上默认为可以跳过的一篇&#xff0c;但是为了…...

懒羊羊闲话2

前言&#xff1a; 笔者谈不上是某个领域的高手&#xff0c;也不是大厂的某个神秘高手&#xff0c;一直游离于小型公司&#xff0c;写下这篇文章献给那些无法接触到好的学习环境&#xff0c;苦恼自己原地踏步的coder。 1、如何快速熟悉某个行业 作为一个编码多年的程序员&#…...

多路转接(上)——select

目录 一、select接口 1.认识select系统调用 2.对各个参数的认识 二、编写select服务器 1.两个工具类 2.网络套接字封装 3.服务器类编写 4.源文件编写 5.运行 一、select接口 1.认识select系统调用 int select(int nfds, fd_set readfds, fd_set writefds, fd_set ex…...

基于SSM的图书管理借阅系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…...

Python的内存优化

在Python中&#xff0c;内存管理和优化是一个复杂的话题&#xff0c;因为它涉及到Python解释器的内部机制&#xff0c;特别是Python的垃圾收集和内存分配策略。Python通过自动垃圾收集机制管理内存&#xff0c;主要包括引用计数和标记-清除算法。 Python内存管理机制&#xff…...

蓝桥杯-回文日期[Java]

目录: 学习目标&#xff1a; 学习内容&#xff1a; 学习时间&#xff1a; 题目&#xff1a; 题目描述: 输入描述: 输出描述: 输入输出样例: 示例 1: 运行限制: 题解: 思路: 学习目标&#xff1a; 刷蓝桥杯题库日记 学习内容&#xff1a; 编号498题目回文日期难度…...

acwing算法基础之搜索与图论--树与图的遍历

目录 1 基础知识2 模板3 工程化 1 基础知识 树和图的存储&#xff1a;邻接矩阵、邻接表。 树和图的遍历&#xff1a;dfs、bfs。 2 模板 树是一种特殊的图&#xff08;即&#xff0c;无环连通图&#xff09;&#xff0c;与图的存储方式相同。 对于无向图中的边ab&#xff0c;…...

前端uniapp请求真是案例(带源码)

目录 案例一案例二最后 案例一 <template><view class"box"><!-- <view class"title-back" click"backPrivious"><</view> --><!-- <view class"title-back" click"backPrivious"…...

MySQL -- mysql connect

MySQL – mysql connect 文章目录 MySQL -- mysql connect一、Connector/C 使用1.环境安装2.尝试链接mysql client 二、MySQL接口1.初始化2.链接数据库3.下发mysql命令4.获取执行结果5.关闭mysql链接6.在C语言中连接MySQL 三、MySQL图形化界面推荐 使用C接口库来进行连接 一、…...

如何用AI帮你下载安卓源码

以Android 11源码下载流程图如下所示&#xff1a; 1. 安装Git和Repo工具 2. 创建一个工作目录 3. 初始化仓库并下载源码 4. 切换到指定的分支 5. 编译源码 具体步骤如下&#xff1a; 安装Git和Repo工具&#xff1a;在Linux或Mac上&#xff0c;可以通过终端运行以下命令安装Gi…...

第三章:人工智能深度学习教程-基础神经网络(第三节-Tensorflow 中的多层感知器学习)

在本文中&#xff0c;我们将了解多层感知器的概念及其使用 TensorFlow 库在 Python 中的实现。 多层感知器 多层感知也称为MLP。它是完全连接的密集层&#xff0c;可将任何输入维度转换为所需的维度。多层感知是具有多个层的神经网络。为了创建神经网络&#xff0c;我们将神…...

Python的版本如何查询?

要查询Python的版本&#xff0c;可以使用以下方法之一&#xff1a; 1.在命令行中使用python --version命令。这会显示安装在计算机上的Python解释器的版本号。 # Author : 小红牛 # 微信公众号&#xff1a;wdPython2.在Python脚本中使用import sys语句&#xff0c;然后打印sy…...

conda相比python好处

Conda 作为 Python 的环境和包管理工具&#xff0c;相比原生 Python 生态&#xff08;如 pip 虚拟环境&#xff09;有许多独特优势&#xff0c;尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处&#xff1a; 一、一站式环境管理&#xff1a…...

Appium+python自动化(十六)- ADB命令

简介 Android 调试桥(adb)是多种用途的工具&#xff0c;该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具&#xff0c;其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利&#xff0c;如安装和调试…...

《基于Apache Flink的流处理》笔记

思维导图 1-3 章 4-7章 8-11 章 参考资料 源码&#xff1a; https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...

用docker来安装部署freeswitch记录

今天刚才测试一个callcenter的项目&#xff0c;所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

LeetCode - 199. 二叉树的右视图

题目 199. 二叉树的右视图 - 力扣&#xff08;LeetCode&#xff09; 思路 右视图是指从树的右侧看&#xff0c;对于每一层&#xff0c;只能看到该层最右边的节点。实现思路是&#xff1a; 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...

安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖

在Vuzix M400 AR智能眼镜的助力下&#xff0c;卢森堡罗伯特舒曼医院&#xff08;the Robert Schuman Hospitals, HRS&#xff09;凭借在无菌制剂生产流程中引入增强现实技术&#xff08;AR&#xff09;创新项目&#xff0c;荣获了2024年6月7日由卢森堡医院药剂师协会&#xff0…...

CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝

目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为&#xff1a;一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...

免费数学几何作图web平台

光锐软件免费数学工具&#xff0c;maths,数学制图&#xff0c;数学作图&#xff0c;几何作图&#xff0c;几何&#xff0c;AR开发,AR教育,增强现实,软件公司,XR,MR,VR,虚拟仿真,虚拟现实,混合现实,教育科技产品,职业模拟培训,高保真VR场景,结构互动课件,元宇宙http://xaglare.c…...

实战三:开发网页端界面完成黑白视频转为彩色视频

​一、需求描述 设计一个简单的视频上色应用&#xff0c;用户可以通过网页界面上传黑白视频&#xff0c;系统会自动将其转换为彩色视频。整个过程对用户来说非常简单直观&#xff0c;不需要了解技术细节。 效果图 ​二、实现思路 总体思路&#xff1a; 用户通过Gradio界面上…...