【Linux】进程信号 -- 信号产生 | 系统调用、硬件、软件的信号发送

news2024/11/22 22:11:13

  • 信号的旧识引入
    • 信号引入
    • signal调用
  • 系统调用向目标进程发送信号
    • 模拟实现一个kill命令
    • raise给自己发送任意信号
    • abort给自己发送指定信号(6)SIGABRT
  • 硬件异常产生信号
    • 除0异常
    • 野指针访问异常
  • 软件条件产生信号
    • 拓展
  • 总结思考
  • 进程退出时核心转储问题
  • 小实验

信号的旧识引入

kill -l是一个在LinuxUnix系统中使用的命令,用于列出可用的信号列表。

在Linux和Unix系统中,进程可以通过发送信号来与其他进程或操作系统交互。kill 命令可以向指定的进程发送一个特定的信号,以便对其进行控制,例如终止进程或重新启动进程等。

kill -l 命令会列出可用的信号列表,每个信号都有一个唯一的数字编号和一个名称。输出结果通常是一个由数字和名称组成的列表,例如:

[AMY@VM-12-15-centos ~]$ kill -l
 1) SIGHUP	 	2) SIGINT	 	3) SIGQUIT	 	4) SIGILL	 	5) SIGTRAP
 6) SIGABRT	 	7) SIGBUS	 	8) SIGFPE	 	9) SIGKILL		10) SIGUSR1
11) SIGSEGV		12) SIGUSR2		13) SIGPIPE		14) SIGALRM		15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD		18) SIGCONT		19) SIGSTOP		20) SIGTSTP
21) SIGTTIN		22) SIGTTOU		23) SIGURG		24) SIGXCPU		25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF		28) SIGWINCH	29) SIGIO		30) SIGPWR
31) SIGSYS		34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

我们可以发现里面数字没有0、32 、33:
1~31普通信号
34~64实时信号

进程信号的认识:信号是给进程发的,比如kill -9 (pid)
信号相关总结:

  • 进程是如何识别信号的?认识+动作
  • 进程本身是被程序员编写的属性和逻辑的集合—程序员编码完成的
  • 当进程收到信号的时候,进程可能正在执行更重要的代码,所以信号不一定会被立即处理
  • 进程本身必须要有对于信号的保存能力
  • 进程在处理信号(信号被捕捉)的时候,一般有三种动作(默认,自定义,忽略)

如果一个信号是发给进程的,而进程要保存,那么应该保存在哪里? task struct(PCB)
如何保存呢?是否收到了指定的信号1~31

struct task_struct
{
	...
	unsigned int signal;
	...
}

在这里插入图片描述
32位:
比特位的位置,代表信号编号。
比特位的内容,代表是否收到该信号,0没有1有

发送信号的本质:修改PCB中的信号位图

PCB是内核维护的数据结构,PCB的管理者是OS,那么谁有权力修改PCB的内容呢?是OS,所以无论我们学习多少种发送信号的方法,本质上都是通过OS向目标进程发送信号。那么OS一定需要提供发送信号的相关系统调用,我们也可以推测出kill命令,底层肯定是调用了对应的系统调用


信号引入

我们常常使用CTRL+c来终止一个正在运行的进程,本质上,这个CTRL+c是一个组合键,OS将其解释成为2号信号2) SIGINT

我们可以使用:

man 7 signal

原始POSIX.1-1990标准中描述的信号:
在这里插入图片描述

signal调用

man 2 signal

在这里插入图片描述
函数原型:

sighandler_t signal(int signum, sighandler_t handler);

案例使用:

#include <iostream>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
    std::cout << "进程捕捉到了一个信号,信号编号是: " << signo << std::endl;
}
int main()
{
    // 这里是signal函数的调用,并不是handler的调用
    /// 仅仅是设置了对2号信号的捕捉方法,并不代表该方法被调用了
    // 一般这个方法不会执行,除非收到对应的信号!
    signal(2, handler);

    while(true)
    {
        std::cout << "我是一个进程: " << getpid() << std::endl;
        sleep(1);
    }
}

直接执行我们并没有执行handler(),因为没有传递这个信号
在这里插入图片描述
我们在执行的时候按CTRL+c就会执行handler()
在这里插入图片描述
为什么我们现在的CTRL+c不能够终止进程呢?是因为我们将默认动作改为自定义动作而去执行handler()了,所以我们不能终止,需要终止我们在handler()函数最后加上exit就行。当然我们还可以使用kill -9 (pid)去杀掉这个进程


系统调用向目标进程发送信号

模拟实现一个kill命令

先写了一个将来会一直运行的程序mytest.cpp,用来进行后续的命令测试

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    while(true)
    {
        std::cout << "我是一个正在运行的进程,pid: " << getpid() << std::endl;
        sleep(1);
    }
}

我们使用man手册查看kill的系统调用
在这里插入图片描述

然后我们实现读取键盘部分代码mysignal.cpp

#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;

static void Usage(const string &proc)
{
    std::cout << "\nUsage: " << proc << " pid signo\n" << std::endl;
}
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    pid_t pid = atoi(argv[1]);
    int signo = atoi(argv[2]);
    int n = kill(pid,signo);
    if(n != 0)
    {
        perror("kill");
    }
    return 0;
}

在这里插入图片描述

在这里插入图片描述
从上述过程我们可得:我们使用自己的调用实现了kill命令,我们平时使用的kill命令其实也就是封装的系统调用kill

kill()可以向任意进程发送任意信号

raise给自己发送任意信号

在这里插入图片描述

int main(int argc, char* argv[])
{
    int cnt=0;
    while(true)
    {
        cnt++;
        cout<<"cnt:"<<cnt<<endl;
        if(cnt>=3)
        {
            raise(9);
        }
    }
    return 0}

在这里插入图片描述
函数原型:

int raise(int sig);

只需要传入信号值,就可以直接发送该信号

abort给自己发送指定信号(6)SIGABRT

在这里插入图片描述

int cnt=0;
while(true)
{
    cnt++;
    cout<<"cnt:"<<cnt<<endl;
    if(cnt>=3)
    {
        abort();
        //raise(9);
    }
}

在这里插入图片描述
前三个调用总结:

  • kill()可以想任意进程发送任意信号
  • raise()给自己 发送 任意信号 等同:kill(getpid(), 任意信号)
  • abort()给自己 发送 指定的信号SIGABRT 等同:kill(getpid(), SIGABRT)

关于信号处理的行为的理解:有很多的情况,进程收到大部分的信号,默认处理动作都是终止进程
信号的意义:信号的不同,代表不同的事件,但是对事件发生之后的处理动作可以一样!


硬件异常产生信号

除0异常

我们对一个常数进行除0运算:
在这里插入图片描述
我们捕捉这个信号:

void catchSig(int signo)
{
    std::cout << "获取到一个信号,信号编号是: " << signo << std::endl;
}
int main(int argc, char* argv[])
{

    signal(SIGFPE, catchSig);
    while (true)
    {
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int a = 10;
        a /= 0;
    }
    return 0;
}

在这里插入图片描述
虽然我们将signal(SIGFPE, catchSig);是写在循环之前,但是当我们进行除0以后,它还是会运行,这是因为:signal()在这里是注册一种未来的方法,当某种条件成立后就自动调用这个注册好的方法

我们还发现这个捕捉到信号SIGFPE后,它就一直运行catchSig()打印
在这里插入图片描述
除0异常解释:
在这里插入图片描述

由上述输出结果我们可以知道:收到信号不一定会引起进程退出,没有退出有可能还会被调度。而CPU内部的寄存器只有一份,但是寄存器里面的内容,属于当前进程上下文。一旦出现异常,我们有没有能力或者动作去修正这个问题呢?答案是没有,比如溢出标记位被置1了,我们有没有能力去更改为0呢?我们肯定不能,因为状态寄存器是由CPU自己维护的,用户没有办法也没有权力去更改。

当进程被切换的时候,就有无数次状态寄存器被保存和恢复的过程,所以每一次恢复的时候,就让OS识别到了CPU内部的状态寄存器中的溢出标志位是1

野指针访问异常

while(true)
{
    std::cout << "我在运行中...." << std::endl;
    sleep(1);
    int *p = nullptr;
    *p=10;
}

在这里插入图片描述

void catchSig(int signo)
{
    std::cout << "获取到一个信号,信号编号是: " << signo << std::endl;
}
int main(int argc, char *argv[])
{

    signal(11, catchSig);

    int *p = nullptr;
    *p = 10;
    while (true)
    {
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
由上述的操作结果我们可知:野指针访问属于是(11) SIGSEGV,且跟除0的捕捉结果是一样的,也是一直调用catchSig()
在这里插入图片描述
MMU位于CPU内部,在ARM32中,MMU主要完成虚拟地址到物理地址的映射,并且能够控制内存的访问权限,而页表是实现上述功能的主要手段。页表又分为一级页表、二级页表,在ARM64中甚至还有三级页表。上图这样画只是为了更形象。

while循环的代码不会被使用因为:访问野指针并触发SIGSEGV信号时,程序已经进入了异常状态。当catchSig函数被调用时,异常状态并没有被完全处理,因此程序进入了一个不稳定的状态。一旦catchSig函数返回,程序会尝试继续执行之前的指令,但由于进程仍处于异常状态,又会再次触发SIGSEGV信号。这将导致catchSig函数被不断地调用。


软件条件产生信号

管道通信举例:
在这里插入图片描述
当读端关闭,写端却一直在写的情况时候,OS会发送(13) SIGPIPE信号来让写端关闭,这中情况就是由软件条件触发产生信号。

定时器软件条件:
alarm():设定闹钟
在这里插入图片描述

int main(int argc, char *argv[])
{
    alarm(1);
    int cnt = 0;
    while (true)
    {
        cout << "cnt:" << cnt << endl;
        cnt++;
    }
    return 0;
}

输出结果:
在这里插入图片描述
我们更改一下代码:

int cnt = 0;
void catchSig(int signo)
{
    std::cout << "获取到一个信号,信号编号是: " << signo << "  cnt:" << cnt << std::endl;
}
int main(int argc, char *argv[])
{
    signal(SIGALRM, catchSig);
    alarm(1);
    while (true)
    {
        cnt++;
    }

在这里插入图片描述
我们首先可以的个结论:IO确实很慢,都是1秒钟,一直打印cnt的那个明显累加次数很少(我使用的是云服务器,这些运行的结果还要通过网络传递给我,所以会更慢,一般使用虚拟机会快很多)

我们这里跟之前的输出结果不一样,这里只打印了一次,说明alarm只发送一次信号

拓展

为什么你说设置“闹钟”是软件条件呢?
任意一个进程,都可以通过alarm系统调用在内核中设置闹钟,OS内可能会存在着很多的闹钟,那么操作系统要不要管理这些闹钟呢?要→先描述再组织

举例:操作系统中有很多管理闹钟的方法比如堆等
描述:

struct alarm
{
    uint64_t when; // 未来的超时时间int type
    // 闹钟类型,一次性的,还是周期性
    task_struct *p;
    struct alarm *next;
}

组织:
在这里插入图片描述
OS会周期性的检测这些闹钟,如果curr_timestamp > alarm.when,超时了OS发送SIGALARM -> alarm.p;


总结思考

关于信号发送的问题:

  • 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
    OS是进程的管理者,也只有OS有权利去操作

  • 信号的处理是否是立即处理的?
    在合适的时候

  • 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
    是的,保存在PCB

  • 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
    我们应该知道,比如红灯快要亮起,但还没有,此时我们知不知道红灯亮起的时候该怎么办?答案是知道。信号还没有产生但是我们应该知道信号产生该做什么处理,这个工作由程序员系统中代码默认体现。

  • 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
    就是OS直接修改目标进程中PCB存放信号的位图

进程退出时核心转储问题

在这里插入图片描述
同样都是越界,为什么右边的越界报错了?因为数组是只分配了10的空间,但是不代表整个函数的栈帧结构只有那么多,所以你即使越界了,但是你还是在有效栈区里所以就没报错,除非你访问了一个完全不属于你的空间,比如系统的某些空间,这时候OS就会识别出来。(不同编译器检查越界方式也可能不同,比如vs2019就是采用抽检)

在这里插入图片描述

Term正常结束,Core除了终止还要做其他工作

在云服务器上,默认如果进程是core退出的,我们暂时看不到明显的现象,如果想看到:

[AMY@VM-12-15-centos lesson_18]$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7260
max locked memory       (kbytes, -l) unlimited
max memory size         (kbytes, -m) unlimited
open files                      (-n) 100001
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7260
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

云服务器默认关闭了core file size选项:
在这里插入图片描述
如何打开这个选项呢?
在这里插入图片描述
在这里插入图片描述
我们现在还多了一个文件core.15199
在这里插入图片描述
(core dumped)核心转储︰当进程出现异常的时候,我们将进程在对应的时刻,在内存中的有效数据,转储到磁盘中–核心转储!而core.1519915199是引起core问题的进程的pid

进程在运行时出异常,以前的终止就是直接结束,而现在打开了core选项进程在终止时会多做一个工作,就是把进程中一些有效的二进制数据给dumped转储到磁盘当中,这个就叫核心转储,形成的这个临时文件以core命名,后缀为该进程的pid

为什么要有这个呢?
正常我们程序崩溃了,我们最想知道的是为什么崩溃?在哪里崩溃?而核心转储可方便我们调试(Linux调试我们需要带上 -g选项)
在这里插入图片描述
gdb上下文之中,输入core-file core.(pid值)

Term不可以核心转储,Core可以核心转储


小实验

如果我们将所有的信号全部捕捉,那么还能不能杀死这个进程呢?

void catchSig(int signo)
{
    std::cout << "获取到一个信号,信号编号是: " << signo << std::endl;
}
int main(int argc, char *argv[])
{
    for(int signo = 1; signo <= 31; signo++)
    {
        signal(signo, catchSig);
    }
    while(true) 
    {
        cout << "我在运行: " << getpid() <<endl;
        sleep(3);
    }
    return 0;
}

在这里插入图片描述
在这里插入图片描述
我们将所有信号全部捕捉,别的确实无法终止进程,但是kill -9可以,这也是OS的设置,OS禁止捕捉9号进程,即使你捕捉了也无法生效,它是管理员进程。否则如果真出现恶意程序捕捉所有信号,那岂不是真不能终止。


如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀

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

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

相关文章

手动实现 Tomcat 底层机制+ 自己设Servlet 问题分析

文章目录 手动实现 Tomcat 底层机制 自己设Servlet问题分析完成小案例运行效果 此项目用maven至于怎么配置在下一篇文章创建cal.htmlCalServlet.java# 实现步骤 web.xmlWebUtils 问题:Tomcat 整体架构分析测试分析&#xff1a;抓包情况 手动实现 Tomcat 底层机制 自己设Servlet…

ALPAGASUS : TRAINING A BETTER ALPACA WITH FEWER DATA♢

ALPAGASUS : TRAINING A BETTER ALPACA WITH FEWER DATA♢ IntroductionMethod参考 Introduction 本文证明了数据的质量的重要性要大于数量。作者通过与GPT交互的方法过滤了Alpaca52k的数据&#xff0c;剩下9k&#xff0c;对二者分别微调&#xff0c;通过实验对比&#xff0c;…

软件测试行业的困境和迷局

中国的软件测试虽然起点较高&#xff0c;但是软件测试的发展似乎没有想象中那么顺利。 其实每个行业除了有自身领域外&#xff0c;还有属于自己的“生态系统”。属于软件测试的生态系统主要包括后备软件测试人员、软件开发人员和软件管理决策者。后备软件测试人员是软件测试的…

获取gitlab上项目列表过程及脚本

一、使用Gitlab API查询项目列表 1、首先获取访问令牌&#xff1a;在Gitlab上生成一个访问令牌&#xff0c;以便能够使用API进行身份验证。可以在GitLab的用户设置中创建一个访问令牌。 2、使用curl发送GET请求的命令&#xff1a; curl --header "PRIVATE-TOKEN: <you…

若依字典使用

若依字典使用 此文章使用的若依是大于3.7.0版本的 JS文件配置 main.js中引入全局变量和方法 import DictData from /components/DictData DictData.install()DictData.js配置 可以从DictData.js中看出在install方法中调用了字典查询接口&#xff0c;在install方法中可以做…

Atom配置Java开发环境

第1步&#xff1a; 从Oracle网站下载安装最新的Java开发包&#xff08;JDK&#xff09; 将JDK添加到环境变量中 参考链接&#xff1a;传送门1&#xff08;外网&#xff09;&#xff0c; 传送门2&#xff08;国内&#xff09; 第2步&#xff1a;现在要在Atom编辑器上运行Jav…

【C++修炼之路】list 模拟实现

&#x1f451;作者主页&#xff1a;安 度 因 &#x1f3e0;学习社区&#xff1a;StackFrame &#x1f4d6;专栏链接&#xff1a;C修炼之路 文章目录 一、读源码二、成员三、默认成员函数1、构造2、析构3、拷贝构造4、赋值重载 四、迭代器五、其他接口 如果无聊的话&#xff0c;…

Pytorch自动求导机制详解

目录 1. 自动求导 1.1 梯度计算 1.1.1 一阶导数 1.1.2 二阶导数 1.1.3 向量 1.2 线性回归实战 1. 自动求导 在深度学习中&#xff0c;我们通常需要训练一个模型来最小化损失函数。这个过程可以通过梯度下降等优化算法来实现。梯度是函数在某一点上的变化率&#xff0c;可以告…

vue代码格式化,Prettier - Code formatter格式化规则文件

vue2&#xff0c;vue3格式化代码使用方法&#xff1a; 1、新建文件名&#xff1a; .prettierrc.cjs&#xff0c;里面放上下面的代码片段&#xff0c;直接粘贴即可 2、把 .prettierrc.cjs文件放在项目的根目录中 // prettier的默认配置文件 module.exports {// 一行最多 100 …

Final Cut Pro中文新手教程 (52绿幕抠图)FCPX透明通道基础使用方法

今天小编为大家分享的是FCPX透明通道基础教程&#xff0c;究竟什么是透明通道呢&#xff1f;透明通道就是一个阿尔法(alpha)通道&#xff0c;也叫做通明阿尔法通道。只要带有alpha的图片或者视频&#xff0c;他们的背景就是透明的只会显示他们的形状和内容。这种技术经常应用在…

VLAN :虚拟局域网

目录 VLAN&#xff1a;虚拟局域网 VLAN种类&#xff1a; 接口分配链路类型 接口划分VLAN 跨网段的通讯 VLAN&#xff1a;虚拟局域网 LAN &#xff1a;局域网 MAN&#xff1a;城域网 WAN&#xff1a;广域网 1.一个VLAN相当于一个广播域 VLAN&#xff1a;通过路由器和交换机…

OpenCv之图像形态学

目录 一、形态学 二、图像全局二值化 三、自适应阈值二值化 四、腐蚀操作 五、获取形态学卷积核 六、膨胀操作 七、开运算 八、闭运算 一、形态学 定义: 指一系列处理图像形状特征的图像处理技术形态学的基本思想是利用一种特殊的结构元(本质上就是卷积核)来测量或提取输…

数据结构--图的基本操作

数据结构–图的基本操作 使用的存储模式&#xff1a; 图的基本操作&#xff1a; • Adjacent(G,x,y)&#xff1a;判断图G是否存在边<x, y>或(x, y)。 • Neighbors(G,x)&#xff1a;列出图G中与结点x邻接的边。 • InsertVertex(G,x)&#xff1a;在图G中插入顶点x。 • …

VSCode 注释后光标快速定位下一行

VSCode默认用 Ctrl / 注释一行时&#xff0c;光标停留在该行中。下面介绍如何注释后&#xff0c;光标会自动移动到下一行。 1.【View】 ->【Extensions】->【查找并安装Multi-command 扩展】 2.【File 】 -> 【Preferences 】->【Keyboard Shortcuts】&#xff08…

怎样优雅地增删查改(八):按用户关系查询

文章目录 原理实现正向用户关系反向用户关系 使用测试 用户关系&#xff08;Relation&#xff09;是描述业务系统中人员与人员之间的关系&#xff0c;如&#xff1a;签约、关注&#xff0c;或者朋友关系。 之前我们在扩展身份管理模块的时候&#xff0c;已经实现了用户关系管理…

Spark(30):Spark性能调优之常规性能调优

目录 0. 相关文章链接 1. 最优资源配置 2. RDD优化 2.1. RDD复用 2.2. RDD持久化 2.3. RDD尽可能早的 filter 操作 3. 并行度调节 4. 广播大变量 5. Kryo序列化 6. 调节本地化等待时长 0. 相关文章链接 Spark文章汇总 1. 最优资源配置 Spark 性能调优的第一步&…

9.Ceph部署

文章目录 Ceph部署前期环境准备实验部署软件安装部署Ceph集群部署mon节点部署OSD存储节点部署mgr节点开启监控模块管理pool Ceph部署 前期环境准备 主机名public网络cluster网络角色admin192.168.242.69admin(管理节点)node01192.168.242.66192.168.242.100.11mon、mgr、osdn…

【Elemnt-UI——el-popover点击出现多个弹框】

效果图 解决 :append-to-body"false"添加这个属性就可以了 <el-popoverv-model"item.contextmenuVisible"placement"bottom-end":append-to-body"false"trigger"click":visible-arrow"false"hide"item.…

[PCIE体系结构导读]PCIE总结(一)

什么是PCIE PCIe Peripheral Component Interconnect express 快速外部组件互联 高速串行计算机扩展总线标准 处理器系统的局部总线 连接外部设备 高速、低时延支持热插拔可靠扩展性好复杂度高点对点串行连接 附一个博主写的总结文章&#xff0c;非常好 《PCI EXPRESS体系结…

如何在服务器下载coco数据集

如果是需要在服务器上进行跑实验&#xff0c;可以直接通过wget下载链接 那么如何确定下载地址呢&#xff1f; 例如&#xff1a;先找到coco的数据集地址http://cocodataset.org 然后可以看到 可以下载一个 2017 Train/Val annotations[241MB]&#xff0c;在下载内容中可以看到…