Linux之进程概念

news2024/11/18 11:44:13

 9efbcbc3d25747719da38c01b3fa9b4f.gif

                                                      作者主页:     作者主页

                                                      本篇博客专栏:Linux专栏

                                                      创作时间 :2024年9月28日

9efbcbc3d25747719da38c01b3fa9b4f.gif

基本概念:

进程说白了其实就是一个程序的执行实例,正在执行的程序。

内核层面来说,就是一个担当分配资源(CPU时间,内存)的实体

写过代码的都知道,当你的代码进行编译链接之后就会形成一个可执行的程序了,这个程序本质上是一个文件,是放在磁盘上的。当我们双击这个程序让他运行起来之后,本质上是让这个程序加载到内存当中去了,因为只有加载到内存当中去CPU才能对他进行逐语句执行,而一旦将这个程序加载到内存之后,我们就不应该叫他程序了,严格意义上应该称他为进程。

描述进程-PCB

系统中可以同时存在大量的进程,当我们使用ps aux命令时便可以看见此时存在的所有进程

当我们电脑开机时,打开的第一个程序其实就是操作系统(即操作系统是第一个加载到系统中的),我们都知道操作系统是管理工作的,其中一个就是进程管理,那么我们电脑上这么多的进程,操作系统是如何进行管理的呢?

这时我想首先告诉大家一个六字真言:就是先描述,再组织,操作系统管理也是如此,操作系统作为管理者是不需要直接和被管理者进行沟通的,当一个进程出现时,操作系统会直接对其进行描述,然后对他的管理其实就是对其描述信息的管理,进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,这个就是PCB

操作系统将每一个进程进行描述,形成一个个的进程控制块(PCB),并且通过双链表的形式将他们连接起来。

这样,操作系统只要拿到这个双链表的头指针就可以对这个双链表进行管理,这样操作系统对各个进程的管理就变成了对双链表的管理。

例如我们创建一个进程时首先就是将改进程的代码和数据加载到内存,然后操作系统对此进程进行描述形成对应的进程控制块(PCB),并将这个PCB插入到双链表中,想要退出这个进程时就直接从双链表当中删去这个节点(PCB)即可。这样一来,操作系统对于进程的管理就变成了对一个双链表的增删查改。

task_struct --PCB的一种

进程控制块(PCB)是描述进程的,再c++中我们称之为面向对象,而在c语言当中我们称之为结构体,因为Linux是用C语言写的,当然PCB也是用c语言写的了,也就是用结构体来实现的。

  • PCB实际上是对进程控制块的统称,在Linux中描述进程的结构体叫做task_struct
  • task_struct是Linux中的一种数据结构,他会被装载到RAM(内存)里并包含进程的信息

task_struct 内容分类

task_struct 就是Linux中的进程控制块,它包含着以下的一些信息。

  • 标识符:描述本进程的唯一标识符,用来区别其他标识符
  • 状态:任务状态,退出代码,推出信号等
  • 优先级:相对于其他进程的优先级
  • 程序计数器:程序中即将被执行的下一条指令的地址
  • 内存地址:包含程序代码和进程相关的指针,还有和其他进程共享的内存块中的指针
  • 上下文数据:进程执行时处理器的寄存器中的数据
  • I/O状态信息:包含显示的I/O请求等
  • 记账信息:可能包含处理器时间的总和等
  • 其他信息

查看进程

通过系统目录来查看信息:

在根目录下有一个名为proc的系统文件夹,其中包含了大量进程信息,其中一些子目录的名字为数字

这些数字其实是一某一进程的PID,对应的文件夹中记录中对应进程的各种信息,想要查看直接输入ls /proc/对应的数字 即可

通过ps命令查看:

ps与对应的指令想叠加,便可以显示对应的进程的信息 

 ps aux | head -1 && ps aux | grep proc | grep -v grep

通过系统调用获得对应的进程的PID和PPID

通过系统调用函数,getpid和getppid两个函数分别可以获得对应进程的PID和PPID

当我们执行该程序之后,这个程序会不停的执行下去

我们通过ps命令得到对应进程的PID和PPID,就可以发现和getpid与getppid得到的值是一模一样的

通过系统调用创建进程-fork初始

fork函数创建子程序

加入fork函数之后,运行结果如下:

循环打印两行数据,这两行数据分别为:第一行是该进程的PID和PPID,第二行是fork创建的子进程的PID和PPID,我们可以发现fork创建的子进程的PPID是子进程的PID,说明这两个进程的关系为父子关系。

同样,操作系统也会为这个新建的进程创建PCB。

我们知道加载到内存中的数据和代码是属于父进程的,那么子进程的数据和代码又是从哪里来的呢?

我们可以看到,fork之前的代码是父进程自己执行的,之后的代码是父子进程都执行

需要注意的是,虽然父子进程共享代码了,但是其实是各自开辟空间(采用写时拷贝);

小贴士:使用fork函数创建子进程后就有了两个进程,这两个进程的调度顺序是不确定的,取决于操作系统调度算法具体的实现。

fork返回值:

1.如果fork函数创建子进程成功,那么在父进程中返回父进程的pid,子进程的返回0

2.如果创建失败,那么父进程的返回-1.

Linux运行状态:

一个进程都创建而产生到因撤销而销毁的整个生命期间,有时占有处理器执行,有时虽然可以运行但是分不到处理器。有时虽然有空闲处理器但是由于待某个时间的发生而无法执行,这一切就说明进程和程序有区别,进程是活动的且状态变化的,所以叫做进程

这里我们具体谈一下进程中的一些状态:

Linux源代码中对于一些状态的定义:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char *task_state_array[] = {
	"R (running)",       /*  0*/
    "S (sleeping)",      /*  1*/
    "D (disk sleep)",    /*  2*/
    "T (stopped)",       /*  4*/
    "T (tracing stop)",  /*  8*/
    "Z (zombie)",        /* 16*/
    "X (dead)"           /* 32*/
};

运行状态——R

一个程序处于运行状态,并不一定一定处于运行当中,一个进程处于R状态,表示处于运行当中或者处于运行队列(runqueue)当中,所有会存在多个进程同时处于R状态。

浅睡眠状态——S

一个进程处于浅睡眠状态,也叫可中断睡眠状态,意味着等待某件事情的完成,处于浅睡眠状态的进程随时可以被唤醒,也可以随时被杀掉

当我们在一个进程中加入sleep(100),意思就是在这里休息一百秒,此时编译运行之后就会出现浅睡眠状态。

深睡眠状态——D

一个进程处于深度睡眠状态,表示这个进程不可以被杀掉,即便是操作系统也是不可以的,只有这个进程自动唤醒才可以恢复,该进程也被称为不可中睡眠状态。

例如:某一进程对磁盘进行写入操作时,再写入期间,就会处于D状态,是无法被杀掉的,因为该进程必须磁盘回复是否写入成功,以做出相应的回应

暂停状态——T

在Linux中,我们可以通过向进程发送SIGSTOP信号使进程进入暂停状态(T),发送SIGCONT可以让处于暂停状态的进程继续运行。

僵尸状态——Z

当一个进程将要退出的时候,在系统层面,这个系统曾经申请的资源并不会被马上释放,而是暂时存储一段时间,以供操作系统或者其父进程进行读取,如果信息一直未被读取,则相关数据会一直存在,不会被释放掉的,如果一个进程等待着数据被读取,那么我们就说他正处在僵尸状态。

僵尸状态时应该存在的,因为我们调用一个程序时,调用方是应该知道这个完成情况的,所以僵尸状态必须是要存在的,以便后续的相关操作。

例如我们在编写程序时都会在最后写一个return 0,它的作用就是告诉操作系统这个程序顺利完成结束了。

在Linux中,我们可以通过echo $命令来获取最近的一次进程的退出码

死亡状态——X

死亡状态只是一个返回状态,当一个进程的信息被退出之后,该进程申请的资源会立即被释放,所以你不会在进程状态中看到死亡状态。

僵尸进程

前面我们已经说过僵尸状态的概念,相信大家也有了一个大致的了解,而处于僵尸状态的进程,就被称为僵尸进程。

例如,对于下面的代码,当程序执行五次之后,子进程便会退出,但是父进程不知道它退出了,那么此时子进程就处于僵尸状态。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0){ //child
		int count = 5;
		while(count){
			printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("child quit...\n");
		exit(1);
	}
	else if(id > 0){ //father
		while(1){
			printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
	}
	else{ //fork error
	}
	return 0;
} 

运行该代码之后,我们可以通过下面这个简单的脚本进行监控

while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done

运行之后我们发现:当子进程退出之后,子进程就会编程僵尸状态。

僵尸进程的危害:

  1. 僵尸进程的退出状态会一直维持下去,因为它需要告诉父进程执行的相应的结果信息。但是父进程一直在不停的执行,所以子进程就一直处于僵尸状态
  2. 僵尸进程的信息会一直存在与task_struct(PCB)中,所以PCB就需要一直去维护
  3. 若一个父进程创建了多个子进程,并且不对其进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占据内存
  4. 僵尸进程越来越多,申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说僵尸进程会造成内存泄漏

孤儿进程:

孤儿进程是指在操作系统中,其父进程已经结束(正常或异常终止),但该进程本身还在继续运行的进程。当一个进程创建子进程后,如果父进程在子进程结束之前就已经退出,那么子进程就会成为孤儿进程。例如,一个父进程启动了一个子进程,之后父进程由于某种原因(如完成了它的任务或者遇到了错误而终止)退出,此时子进程就变成了孤儿进程。

对于以下代码,父进程执行五次后退出,子进程就变成了孤儿进程:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0){ //child
		int count = 5;
		while(1){
			printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);
			sleep(1);
		}
	}
	else if(id > 0){ //father
		int count = 5;
		while(count){
			printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("father quit...\n");
		exit(0);
	}
	else{ //fork error
	}
	return 0;
} 

观察代码运行,会发现父进程退出后,子进程的PPID变成了1,这就说明他被一号进程领养了

进程优先级:


基本概念:

什么是进程优先级?优先级实际上就是进程获取某些资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序,就是指进程的优先权,优先权高的进程具有优先执行的权力

为什么要有进程优先级?

进程优先级存在的主要原因就是我们的资源是有限的,就像我们的电脑,一般都是单CPU的,一个CPU一次只能执行一个进程,而进程有多个,所以需要存在进程优先级,确定获取CPU资源的先后顺序。

查看系统进程

在Linux中输入ps -l可以看到以下的东西:

我列出其中重要的几个信息:

  • UID:代表着执行者的身份
  • PID:代表这个进程的代号
  • PPID:代表着这个进程由哪一个进程发展而来,即父进程的代号
  • PRI:代表这个进程的可被执行的优先级,值越小越早被执行
  • NI:代表着这个进程的nice值

PRI和NI

  • PRI代表着进程的优先级,这个值越小进程的优先级越高
  • NI代表着nice值,其表示进程可被执行的优先级的修正数值
  • PRI(new)=PRI(old)+NI;
  • 若NI值为负值,那么PRI变小,优先级变高
  • 调整进程的优先级,在Linux下就是调整NI,即nice值
  • NI的范围是-20-19,也就是说进程优先级一共分为四十个级别

注意:在Linux系统中,PRI默认的值是80,也就是PRI=80+NI。

当我们创建一个进程之后,我们可以通过ps -al查看进程的优先级。

通过top命令修改进程优先级

这里的top命令其实就相当于Windows中的任务管理器,可以调整进程优先级。

使用top命令后按r,然后输入要调整的进程的PID,然后调整后的nice值即可

注意:要想将nice调整为负值,需要加上sudo提升权限

通过renice调整进程优先级

输入 renice +更改后的nice +PID即可

四个重要概念:

竞争性:由于只有一个CPU,所以资源有限,会出现资源竞争,为了高效完成任务,合理分配CPU资源,所以会出现进程优先级

独立性:多进程之间运行需要独享各种资源,运行期间互不打扰

并发:即多个进程在一个进程下采用进程切换的方式,在一段时间段内,让多个进程都得以共同推进,称之为并发

并行:多个进程在多个CPU下同时进行

环境变量:

基本概念

环境变量一般是指在操作系统中指定操作系统运行环境的一些参数

常见环境变量

  • PATH:指定命令的搜索路径
  • HOME:指定用户的主工作目录
  • SHELL:当前shell,他的值一般是/bin/bash

查看当前环境变量的方法

使用echo来查看:

echo $NAME //NAME为待查看的环境的名称

测试PATH

大家有没有想过这样一个问题:为什么执行ls命令时不用带./,而在执行我们自己的可执行程序时就必须要带上?

容易理解的是,我们在执行一个程序的时候,必须要先找到他在哪里,既然不带ls就可以执行ls,说明操作系统可以找到他,而系统找不到我们的可执行程序,必须带上./来说明他在我们的当前目录下。

而系统就是通过环境变量PATH来找到ls的,查看环境变量PATH,可以看到下面内容:

可以看到很多路径,这些路径通过冒号隔开,然后执行ls命令时,系统会从左到右开始寻找ls命令

而ls命令确实存在与这些路径中的某个路径下面。

那我们可不可以让自己的可执行程序不带./就执行呢

两个方法:

  • 一个就是在系统默认的路径下创建这个程序,然后生成可执行程序或者将这个可执行程序拷贝到PATH的某一个路径下面
  • 将可执行程序所在的路径导入到PATH这个路径下面

部分环境变量说明:

set:显示本地定义的shell变量和环境变量

unset:清楚环境变量

通过代码来获取环境变量:

你知道main函数其实是由参数的嘛?

我们平常情况不会使用他,所以基本不会写出来。

在这里我们可以看到,调用main函数时向其传递了三个参数。

我们先来说说前两个参数

我们在Linux下写下这个程序并运行:

                                                                                                                                                                                 
#include <stdio.h>
int main(int argc,char *argv[])
{
for(int i=0;i<argc;i++)
{
printf("argv[%d]:%s\n",i,argv[i]);                                                                                                                                                                 
 }                                  
                                    
  return 0;                        
 }                                    
                                     
                  

执行结果如下:

现在我们来说说main函数中的前两个参数,其中的第二个参数是一个字符指针数组,数组中第一个字符指针存储的是可执行程序的位置,其余字符指针存储的是所给的若干选项。最后一个指针为空,而前面那个第一个参数就代表着字符指针数组中有效元素的个数

下面我们来写一个简单的代码,这个代码运行起来之后会根据你所给的选项的不同给出不同的提示语句:

现在我们来说说main的第三个参数:

main的第三个参数实际上是接受的环境变量表

可通过他获得系统的环境变量:

通过系统调用获取环境变量:

程序地址空间:

下面我们来验证一下:

最后:

十分感谢你可以耐着性子把它读完和我可以坚持写到这里,送几句话,对你,也对我:

1.一个冷知识:
屏蔽力是一个人最顶级的能力,任何消耗你的人和事,多看一眼都是你的不对。

2.你不用变得很外向,内向挺好的,但需要你发言的时候,一定要勇敢。
正所谓:君子可内敛不可懦弱,面不公可起而论之。

3.成年人的世界,只筛选,不教育。

4.自律不是6点起床,7点准时学习,而是不管别人怎么说怎么看,你也会坚持去做,绝不打乱自己的节奏,是一种自我的恒心。

5.你开始炫耀自己,往往都是灾难的开始,就像老子在《道德经》里写到:光而不耀,静水流深。

最后如果觉得我写的还不错,请不要忘记点赞✌,收藏✌,加关注✌哦(。・ω・。)

愿我们一起加油,奔向更美好的未来,愿我们从懵懵懂懂的一枚菜鸟逐渐成为大佬。加油,为自己点赞!

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

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

相关文章

如何去编写一个好的单元测试,通义灵码是如何快速生成单元测试?

本文首先讲述了什么是单元测试、单元测试的价值、一个好的单元测试所具备的原则&#xff0c;进而引入如何去编写一个好的单元测试&#xff0c;通义灵码是如何快速生成单元测试的。 通义灵码插件下载安装&#xff1a;通义灵码_智能编码助手_AI编程-阿里云 目录 什么是单元测试&…

CS 工作笔记:SmartEdit 里创建的是 CMS Component

下图是在 SmartEdit 里创建的 cms Component&#xff0c;在 Back-Office 里的截图&#xff1a; SAP Commerce Cloud 的 CMS Component 是其内容管理系统 (CMS) 的核心组成部分&#xff0c;它提供了对在线商店或平台内容的灵活管理。通过这些组件&#xff0c;用户能够在不涉及复…

MinIO使用客户端进行桶和对象的管理

MinIO使用客户端进行桶和对象的管理 minio安装完成后&#xff0c;除了自带的webui管理界面&#xff0c;还可以使用官方配套的客户端mc进行管理。除此之外&#xff0c;还可以使用第三方客户端s3browser也可以完成对象和桶的生命周期管理。 1. 官方客户端mc MinIO客户端 mc 命…

【STM32】TCP/IP通信协议--LWIP内存管理

五、LWIP内存管理 1.什么是内存管理&#xff1f; &#xff08;1&#xff09;内存管理&#xff0c;是指软件运行时对计算机内存资源的分配的使用的技术&#xff0c;其主要目的是如何高效、快速的分配&#xff0c;并且在适当的时候释放和回收内存资源&#xff08;就比如C语言当…

只申请一块sizeofimage的内存能否实现PE文件的拉伸

不能,别试了,浪费时间. 从最后一个节复制,也会被覆盖 BOOL StrechFileBuffer(__in char* m_fileName, __inout char** LPImageBuffer) {FILE* file (fopen(m_fileName, "rb"));if (file NULL){printf("error :%d", GetLastError());return FALSE;}// 从文…

【HyperWorks入门教程】HyperWorks的shrink warp meshing

在HyperWorks中&#xff0c;针对某些具有复杂几何特征的零部件的网格剖分&#xff0c;Altair HyperMesh 向用户提供了一种名为 Shrink Warp Meshing 的技术&#xff0c;快捷高效地完成有限元模型前处理工作。例如在车辆碰撞分析研究中&#xff0c;用户可以使用 Shrink Warp Mes…

3GPP链路级仿真-Link-Level Simulator for 5G Localization

文章目录 II. SYSTEM ARCHITECTURE AND CAPABILITIESA. System Architecture III. KEY COMPONENTSA. Transmission Models of the Positioning SignalsB. Dedicated Wireless Channel Model IV. APPLICATION CASESA. Two-Dimensional Mobile Terminal Localization仿真工作流程…

Spring+Mybatis IOC + AOP + 开启事务 模板

萌新小白刚入行Java 框架的可以试着自己拿着这个代码改一改,看看能不能运行成功 第一步:创建maven项目,在pom.xml文件中引入以下需要用到的jar包 <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance&…

matlab2019b-2024b knnclassify无法识别的问题(亲测,已解决)

matlab2019a-2024b 已经移除了knnclassify分类&#xff0c;修改了名称和功能&#xff0c;如果你还想使用它&#xff0c;就必须在2018版本以前的旧版本中找相关的工具箱&#xff08;这是免费的哦&#xff0c;如果官网下载 需要付费&#xff09;。 这里本人从2014a中分离出的工具…

Docker启动 Redis提示:Can‘t initialize Background Jobg

问题说明: 在使用docker启动redis失败&#xff0c;但是查看容器日志&#xff0c;除了提示 Fatal:Cant initialize Background Jobg&#xff0c;没有其他错误信息。经过长时间查找资料及试错&#xff0c;现记录下可能的产生原因及解决方案&#xff0c;以便以后参考。 产生原因&…

【数据结构与算法】Z算法(扩展KMP)(C++和Python写法)

Z算法&#xff08;扩展KMP&#xff09; 文章目录 Z算法&#xff08;扩展KMP&#xff09;朴素求法线性求法力扣类型题变种题&#xff1a;[3303. 第一个几乎相等子字符串的下标](https://leetcode.cn/problems/find-the-occurrence-of-first-almost-equal-substring/) 所谓Z算法&…

[题解] Codeforces Round 976 (Div. 2) A ~ E

A. Find Minimum Operations 签到. void solve() {int n, k;cin >> n >> k;if (k 1) {cout << n << endl;return;}int ans 0;while (n) {ans n % k;n / k;}cout << ans << endl; }B. Brightness Begins 打表发现, 翻转完后的序列为: 0…

基于SSM+小程序的流浪动物领养管理系统(救助1)(源码+sql脚本+视频导入教程+文档)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 本系统功能为信息发布管理、领养记录管理、动物小圈管理、求助日报管理等。本系统的使用角色为管理员和用户&#xff0c;用户可以发布自己捡到的流浪动物、求领养信息、申请领养&#xff…

从零开发操作系统

没有操作系统 要考虑放到什么位置 org 07c00h 我用nasm&#xff08;汇编编译&#xff09; 放到7c00处 ibm兼容机 AX发生变化 -寄存器 不可能做存储 内存- 代码段数据段 if else --指令 代码 int a -数据段 必须告诉计算机代码段从哪里开始 改变cs寄存器里面的值可以改变推进寄…

【MYSQL】MYSQL约束

约束是在创建表的时候用的 1、概念 约束英文&#xff1a;constraint 约束实际上就是表中数据的限制条件 2、作用 表在设计的时候加入约束的目的就是为了保证表中的记录完整性和有效性&#xff0c;比如&#xff1a;用户表有些列的值&#xff08;手机号&#xff09;不能为空…

STM32自动下载电路分享及注意事项

文章目录 简介ISP下载启动配置 USB转串口芯片CH340C手动isp下载自动isp下载RTS、DTR电平变化分析注意事项 简介 在嵌入式开发中&#xff0c;使用STM32下载程序&#xff0c;可以通过仿真器下载&#xff0c;也可以通过串口下载。在stm32串口下载时&#xff0c;我们需要手动配置启…

gradle的入门及kotlin的了解

gradle项目创建方式 1.idea springboot initalizer 2.命令行 gradle目录结构 gradle命令 gradle wrapper 一个解决不同项目需要不同版本gradle的问题 比如&#xff0c;对方电脑没用安装gradle 对方电脑安装了gradle&#xff0c;但是版本太旧了 于是&#xff0c;在项目根目…

Qt 学习第十一天:QTableWidget 的使用

一、创建QTableWidget对象&#xff0c;设置大小&#xff0c;在窗口的位置 //创建tablewidgetQTableWidget *table new QTableWidget(this);table->resize(550, 300);table->move(100, 100); //移动 二、设置表头 //设置表头QStringList headerList; //定义headerList…

Webpack 特性探讨:CDN、分包、Tree Shaking 与热更新

文章目录 前言包准备CDN 集成代码分包Tree Shaking原理实现条件&#xff1a;解决 treeShaking 无效方案&#xff1a;示例代码&#xff1a; 热更新&#xff08;HMR&#xff09; 前言 Webpack 作为现代前端开发中的核心构建工具&#xff0c;提供了丰富的特性来帮助开发者优化和打…

63.5 注意力提示_by《李沐:动手学深度学习v2》pytorch版

系列文章目录 文章目录 系列文章目录注意力提示生物学中的注意力提示查询、键和值注意力的可视化使用 show_heatmaps 显示注意力权重代码示例 代码解析结果 小结练习 注意力提示 &#x1f3f7;sec_attention-cues 感谢读者对本书的关注&#xff0c;因为读者的注意力是一种稀缺…