操作系统之内存管理

news2024/11/18 19:47:08

文章目录

  • 一、 虚拟内存
  • 二、内存分段
  • 三、内存分页
      • 多级页表
      • TLB
  • 四、段页式内存管理
  • 五、Linux 内存管理


一、 虚拟内存

单片机是没有操作系统的,所以每次写完代码,都需要借助工具把程序烧录进去,这样程序才能跑起来。
另外,单片机的 CPU 是直接操作内存的物理地址
在这里插入图片描述
在这种情况下,要想在内存中同时运行两个程序是不可能的。如果第⼀个程序在 2000 的位置写入⼀个新
的值,将会擦掉第二个程序存放在相同位置上的所有内容,所以同时运行两个程序是根本行不通的,这两
个程序会立刻崩溃。

操作系统是如何解决这个问题呢?
这里关键的问题是这两个程序都引用了绝对物理地址,而这正是我们最需要避免的。
我们可以把进程所使用的地址隔离开来,即让操作系统为每个进程分配独立的⼀套虚拟地址,人人都有,大家自己玩自己的地址就行,互不干涉。但是有个前提每个进程都不能访问物理地址,至于虚拟地址最终怎么落到物理内存里,对进程来说是透明的,操作系统已经把这些都安排的明明白白了。
在这里插入图片描述
操作系统会提供⼀种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。
如果程序要访问虚拟地址的时候,由操作系统转换成不同的物理地址,这样不同的进程运的时候,写入的是不同的物理地址,这样就不会冲突了。

于是,这里就引出了两种地址的概念:

  • 我们程序所使用的内存地址叫做虚拟内存地址(Virtual Memory Address)
  • 实际存在硬件里面的空间地址叫物理内存地址(Physical Memory Address)。

操作系统引入了虚拟内存,进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关
系,来转换变成物理地址,然后再通过物理地址访问内存,如下图所示:
在这里插入图片描述

操作系统是如何管理虚拟地址与物理地址之间的关系?
主要有两种方式,分别是内存分段和内存分页,分段是比较早提出的,我们先来看看内存分段。

二、内存分段

程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属
性的,所以就用分段(Segmentation)的形式把这些段分离出来

分段机制下,虚拟地址和物理地址是如何映射的?
分段机制下的虚拟地址由两部分组成,段选择因子段内偏移量
在这里插入图片描述

  • 段选择因子就保存在段寄存器里面。段选择因子里面最重要的是段号,用作段表的索引段表里面保存的是这个段的基地址、段的界限和特权等级等。
  • 虚拟地址中的段内偏移量应该位于 0 和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。

在上面,知道了虚拟地址是通过段表与物理地址进行映射的,分段机制会把程序的虚拟地址分成 4 个段,
每个段在段表中有⼀个项,在这⼀项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址,
如下图:
在这里插入图片描述
如果要访问段 3 中偏移量 500 的虚拟地址,我们可以计算出物理地址为,段 3 基地址 7000 + 偏移量 500
= 7500。
分段的办法很好,解决了程序本身不需要关心具体的物理内存地址的问题,但它也有⼀些不足之处:

  • 第⼀个就是内存碎片的问题。
  • 第二个就是内存交换的效率低的问题。

接下来,说说为什么会有这两个问题。
我们先来看看,分段为什么会产生内存碎片的问题
我们来看看这样⼀个例子。假设有 1G 的物理内存,用户执行了多个程序,其中:

  • 游戏占用了 512MB 内存
  • 浏览器占用了 128MB 内存
  • 音乐占用了 256 MB 内存。

这个时候,如果我们关闭了浏览器,则空闲内存还有 1024 - 512 - 256 = 256MB。
如果这个 256MB 不是连续的,被分成了两段 128 MB 内存,这就会导致没有空间再打开⼀个 200MB 的程
序。
在这里插入图片描述
针对上面两种内存碎片的问题,解决的方式会有所不同。
解决外部内存碎片的问题就是内存交换
可以把音乐程序占用的那 256MB 内存写到硬盘上,然后再从硬盘上读回来到内存里。不过再读回的时
候,我们不能装载回原来的位置,而是紧紧跟着那已经被占用了的 512MB 内存后面。这样就能空缺出连
续的 256MB 空间,于是新的 200MB 程序就可以装载进来。
这个内存交换空间,在 Linux 系统里,也就是我们常看到的 Swap 空间,这块空间是从硬盘划分出来的,用于内存与硬盘的空间交换。

再来看看,分段为什么会导致内存交换效率低的问题?
对于多进程的系统来说,用分段的方式,内存碎片是很容易产生的,产生了内存碎片,那不得不重新
Swap 内存区域,这个过程会产生性能瓶颈
因为硬盘的访问速度要比内存慢太多了,每⼀次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。
为了解决内存分段的内存碎片和内存交换效率低的问题,就出现了内存分页。

三、内存分页

分段的好处就是能产生连续的内存空间,但是会出现内存碎片和内存交换的空间太大的问题。
要解决这些问题,那么就要想出能少出现⼀些内存碎片的办法。另外,当需要进行内存交换的时候,让需
交换写入或者从磁盘装载的数据更少⼀点,这样就可以解决问题了。这个办法,也就是内存分页
分页是把整个虚拟和物理内存空间切成⼀段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,
我们叫页(Page)。在 Linux 下,每⼀页的大小为 4KB 。

虚拟地址与物理地址之间通过页表来映射,如下图:
在这里插入图片描述
页表是存储在内存里的内存管理单元 (MMU) 就做将虚拟内存地址转换成物理地址的工作。
而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内
存、更新进程页表,最后再返回用户空间,恢复进程的运行。

分页是怎么解决分段的内存碎片、内存交换效率低的问题?
由于内存空间都是预先划分好的,也就不会像分段会产生间隙非常小的内存,这正是分段会产生内存碎片
的原因。而采用了分页,那么释放的内存都是以页为单位释放的,也就不会产生无法给进程使用的小内存。

如果内存空间不够,操作系统会把其他正在运行的进程中的最近没被使用的内存页面给释放掉,也就
是暂时写在硬盘上,称为换出(Swap Out)。⼀旦需要的时候,再加载进来,称为换入(Swap In)。所
以,⼀次性写入磁盘的也只有少数的⼀个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。
在这里插入图片描述
更进⼀步地,分页的方 式使得我们在加载程序的时候,不再需要⼀次性都把程序加载到物理内存中。我们
完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在
程序运行中,需要用到对应虚拟内页里面的指令和数据时,再加载到物理内存里面去。

分页机制下,虚拟地址和物理地址是如何映射的?
在分页机制下,虚拟地址分为两部分,页号页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址,见下图。
在这里插入图片描述
总结⼀下,对于⼀个内存地址转换,其实就是这样三个步骤:

  • 把虚拟内存地址,切分成页号和偏移量;
  • 根据页号,从页表里面,查询对应的物理页号;
  • 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。

下面举个例子,虚拟内存中的页通过页表映射为了物理内存中的页,如下图:
在这里插入图片描述
这看起来似乎没什么毛病,但是放到实际中操作系统,这种简单的分页是肯定是会有问题的。
简单的分页有什么缺陷吗?
有空间上的缺陷。
因为操作系统是可以同时运行非常多的进程的,那这不就意味着页表会非常的庞大。
在 32 位的环境下,虚拟地址空间共有 4GB,假设⼀个页的大小是 4KB(2^12),那么就需要大约 100 万
(2^20) 个页,每个页表项需要 4 个字节大小来存储,那么整个 4GB 空间的映射就需要有 4MB
的内存来存储页表。
这 4MB大小的页表,看起来也不是很大。但是要知道每个进程都是有自己的虚拟地址空间的,也就说都有
自己的页表。

那么, 100 个进程的话,就需要 400MB 的内存来存储页表,这是非常大的内存了,更别说 64 位的环
境了。

多级页表

要解决上面的问题,就需要采用⼀种叫作多级页表(Multi-Level Page Table)的解决方案。
在前面我们知道了,对于单页表的实现方式,在 32 位和页大小4KB 的环境下,⼀个进程的页表需要装
下 100 多万个页表项,并且每个页表项是占用4 字节大小的,于是相当于每个页表需占用4MB 大小的空间。
我们把这个 100 多万个页表项的单级页表再分页,将页表(⼀级页表)分为 1024 个页表(⼆级页表),每个表(⼆级页表)中包含 1024 个页表项,形成⼆级分页。如下图所示:
在这里插入图片描述
你可能会问,分了二级表,映射 4GB 地址空间就需要 4KB(⼀级页表)+ 4MB(二级页表)的内存,这样
占用空间不是更大了吗?

当然如果 4GB 的虚拟地址全部都映射到了物理内存上的话,二级分页占用空间确实是更大了,但是,我们
往往不会为⼀个进程分配那么多内存。
其实我们应该换个角度来看问题,还记得计算机组成原理里面无处不在的局部性原理么?
每个进程都有 4GB 的虚拟地址空间,而显然对于大多数程序来说,其使用到的空间远未达到 4GB,因为
会存在部分对应的页表项都是空的,根本没有分配,对于已分配的页表项,如果存在最近一定时间未访问
的页表,在物理内存紧张的情况下,操作系统会将页面换出到硬盘,也就是说不会占用物理内存。

如果使用了⼆级分页,⼀级页表就可以覆盖整个 4GB 虚拟地址空间,但如果某个⼀级页表的页表项没有被
用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。做个简单的计
算, 假设只有 20% 的⼀级页表项被用到了,那么页表占用的内存空间就只有 4KB(⼀级页表) + 20% *
4MB(二级页表)= 0.804MB ,
这对比单级页表的 4MB 是不是⼀个巨大的节约?
那么为什么不分级的页表就做不到这样节约内存呢?我们从页表的性质来看,保存在内存中的页表承担的
职责是将虚拟地址翻译成物理地址
。假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作
了。所以页表⼀定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页表项来映射,而二级分
页则只需要 1024 个页表项 (此时⼀级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)
我们把二级分页再推广到多级页表,就会发现页表占用的内存空间更少了,这⼀切都要归功于对局部性原
理的充分应用。

对于 64 位的系统,两级分页肯定不够了,就变成了四级目录,分别是:

  • 全局页目录项 PGD(Page Global Directory);
  • 上层页目录项 PUD(Page Upper Directory);
  • 中间页目录项 PMD(Page Middle Directory);
  • 页表项 PTE(Page Table Entry);

在这里插入图片描述

TLB

多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降
低了这俩地址转换的速度,也就是带来了时间上的开销。
程序是有局部性的,即在⼀段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的
存储空间也局限于某个内存区域。
在这里插入图片描述
我们就可以利用这一特性,把最常访问的几个页表项存储到访问速度更快的硬件,于是计算机科学家们,
就在 CPU 芯片中,加⼊了⼀个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB
(Translation Lookaside Buffer) ,通常称为页表缓存、转址旁路缓存、快表等。
在这里插入图片描述
在 CPU 芯片里面,封装了内存管理单元(Memory Management Unit)芯片,它用来完成地址转换和 TLB
的访问与交互。
有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。
TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个。

四、段页式内存管理

内存分段和内存分页并不是对立的,它们是可以组合起来在同一个系统中使用的,那么组合起来后,通常
称为段页式内存管理
在这里插入图片描述
段页式内存管理实现的方式:

  • 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
  • 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;

这样,地址结构就由段号、段内页号和页内位移三部分组成。
用于段页式地址变换的数据结构是每⼀个程序⼀张段表每个段又建立⼀张页表段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号,如图所示:
在这里插入图片描述
段页式地址变换中要得到物理地址须经过三次内存访问

  • 第一次访问段表,得到页表起始地址;
  • 第二次访问页表,得到物理页号;
  • 第三次将物理页号与页内位移组合,得到物理地址。

可用软、硬件相结合的方法实现段页式地址变换,这样虽然增加了硬件成本和系统开销,但提高了内存的
利用率。

五、Linux 内存管理

那么,Linux 操作系统采用了哪种方式来管理内存呢?
在回答这个问题前,我们得先看看 Intel 处理器的发展历史。
早期 Intel 的处理器从 80286 开始使用的是段式内存管理。但是很快发现,光有段式内存管理而没有页式内存管理是不够的,这会使它的 X86 系列会失去市场的竞争力。因此,在不久以后的 80386 中就实现了对页式内存管理。也就是说,80386 除了完成并完善从 80286 开始的段式内存管理的同时还实现了页式内存管理

但是这个 80386 的页式内存管理设计时,没有绕开段式内存管理,而是建立在段式内存管理的基础上,这
就意味着,页式内存管理的作用是在由段式内存管理所映射而成的地址上再加上⼀层地址映射。

Linux 内存主要采用的是页式内存管理,但同时也不可避免地涉及了段机制。

Linux 系统中的每个段都是从 0 地址开始的整个 4GB 虚拟空间(32 位环境下),也就是所有的段的起始
地址都是⼀样的
。这意味着,Linux 系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的
地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于
访问控制和内存保护。

我们再来瞧⼀瞧,Linux 的虚拟地址空间是如何分布的?
在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址
空间的范围也不同。比如最常见的 32 位和 64 位系统,如下所示:
在这里插入图片描述
通过这里可以看出:

  • 32 位系统的内核空间占用1G ,位于最高处,剩下的 3G 是用户空间;
  • 64 位系统的内核空间和用户空间都是 128T ,分别占据整个内存空间的最高和最低处,剩下的中
    间部分是未定义的。

再来说说,内核空间与用户空间的区别:

  • 进程在用户态时,只能访问用户空间内存;
  • 只有进入内核态后,才可以访问内核空间的内存;

虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内
。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。
在这里插入图片描述
接下来,进⼀步了解虚拟空间的划分情况,用户空间和内核空间划分的方式是不同的,内核空间的分布情
况就不多说了。
我们看看用户空间分布的情况,以 32 位系统为例,我画了一张图来表示它们的关系:
在这里插入图片描述
通过这张图你可以看到,用户空间内存,从低到高分别是 7 种不同的内存段:

  • 程序文件段,包括二进制可执行代码;
  • 已初始化数据段,包括静态常量;
  • 未初始化数据段,包括未初始化的静态变量;
  • 堆段,包括动态分配的内存,从低地址开始向上增长;
  • 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关);
  • 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,⼀般是 8 MB 。当然系统也提供
    了参数,以便我们自定义大小;

在这 7 个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用C 标准库的 malloc() 或者
mmap() ,就可以分别在堆和文件映射段动态分配内存。

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

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

相关文章

基于单片机的出租车计价器设计

✅作者简介:嵌入式领域优质创作者,博客专家 ✨个人主页:咸鱼弟 🔥 系列专栏:单片机设计专栏 📃推荐一款求职面试、刷题神器👉注册免费刷题 一、主要功能 本课程设计所设计的出租车计价器的主要功…

python数据结构 操作指南(列表、元组、字典、集合)

数据结构是在python程序运行中必不可少的一部分,在深度学习应用,更是常用来存储模型输出的信息,小编在深度学习开发中过程中,发现有时候我们需要对数据结构中存储的信息,进行复合的条件的筛选,并返回特定的…

python高阶函数心得笔记,python高阶函数知识

递归函数 <1>什么是递归函数 通过前面的学习知道一个函数可以调用其他函数。 如果一个函数在内部不调用其它的函数&#xff0c;而是自己本身的话&#xff0c;这个函数就是递归函数。 <2>递归函数的作用 举个例子&#xff0c;我们来计算阶乘 n! 1 * 2 * 3 * .…

python 绘制BA图, 绘制Bland-Altman, 两个连续变量的一致性分析

一致性分析 分析数据一致性时常用的方法如下&#xff1a; 方法数据类型ICC组内相关系数定量或者定类Kappda一致性系数定类&#xff08;分级&#xff09;Bland-Altman图&#xff08;BA图&#xff09;定量数据 Bland-Altman 常用于生物医学研究论文中评价 两种连续变量测量方法…

MySQL MVCC工作原理

之前的文章中我们讲到&#xff0c;MySQL事务的隔离级别有四种分别是&#xff1a;read uncommitted、read committed、repeatable read和serializable。现在InnoDB下默认的存储引擎是repeatable read&#xff0c;之前也提过在repeatable read下MySQL是通过MVCC来解决幻读的问题。…

Dubbo服务调用扩展点Filter的介绍与使用

扩展点介绍 如上图所示&#xff0c;从服务调用的角度来看&#xff0c;Dubbo 在链路中提供了丰富的扩展点&#xff0c;覆盖了负载均衡方式、选址前后的拦截器、服务端处理拦截器等。 简单来说 Dubbo 发起远程调用的时候&#xff0c;主要工作流程可以分为消费端和服务端两个部分。…

Linux网络--------http协议

文章目录URL---网址对http协议的宏观认识http协议的请求方法http响应的状态码最简单的http协议服务器关于http协议的一些概念性知识URL—网址 首先&#xff0c;http协议是应用层协议&#xff0c; 是超文本传输协议。 urlencode : 转码 urldecode &#xff1a; 解码 将 ---- …

python自学入门(打卡十)2022-11-22

Pytest与Unittest区别 参考资料&#xff1a;https://blog.csdn.net/qq_33385691/article/details/112004487 pytest用例规则 文件名以test_.py文件和test.py 以test_开头的函数 以Test开头的类&#xff0c;test_开头的方法&#xff0c;并且不能带有__init_ 方法 所有的包pake…

极智AI | 昇腾开发环境搭建 CANN MindStudio (无坑版)

欢迎关注我的公众号 [极智视界]&#xff0c;获取我的更多笔记分享 大家好&#xff0c;我是极智视界&#xff0c;本文介绍一下 昇腾开发环境搭建 CANN & MindStudio&#xff0c;没有坑。 本文介绍的方法适用于&#xff1a; 系统&#xff1a;ubuntu18.04 (注&#xff1a;[ce…

5个常见的JavaScript内存错误

JavaScript 不提供任何内存管理操作。相反&#xff0c;内存由 JavaScript VM 通过内存回收过程管理&#xff0c;该过程称为垃圾收集。 既然我们不能强制的垃圾回收&#xff0c;那我们怎么知道它能正常工作&#xff1f;我们对它又了解多少呢&#xff1f; 脚本执行在此过程中暂停…

计算机毕业设计之java+ssm某地区精准扶贫网站

项目介绍 本精准扶贫网站管理系统主要包括系统用户管理模块、捐赠信息管理模块、投诉信息管理、扶贫资讯管理、登录模块、和退出模块等多个模块,系统采用了jsp的mvc框架,SSM(springMvcspringMybatis)框架进行开发,本系统使用mysql&#xff0c;独立运行,不依附于其他系统&#…

Redis数据类型之set

文章目录setⅠ. 基础操作Ⅱ. 随机数据Ⅲ. 交、并、差Ⅳ. 应用场景Ⅴ. 注意事项提示&#xff1a;以下是本篇文章正文内容&#xff0c;Redis系列学习将会持续更新 set ● 数据存储需求&#xff1a;存储大量的数据&#xff0c;在查询方面提供更高的效率。 ● 需要的存储结构&#…

家乡主题网页设计代码 旅游主题网页设计 html静态网页设计制作 dw静态网页成品模板素材网页 web前端网页设计与制作 div静态网页设计

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

详细解读Spring Boot中@Import三种使用方式

需要注意的是&#xff1a;ImportSelector、ImportBeanDefinitionRegistrar这两个接口都必须依赖于Import一起使用&#xff0c;而Import可以单独使用。Import是一个非常有用的注解&#xff0c;它的长处在于你可以通过配置来控制是否注入该Bean&#xff0c;也可以通过条件来控制注…

CNN卷积神经网络

&#xff08;声明&#xff1a;本文章是在学习他人视频的学习笔记&#xff0c;图片出处均来自该up主&#xff0c;侵权删 up主链接&#xff1a;同济子豪兄的个人空间_哔哩哔哩_bilibili&#xff09; 卷积神经网络就像一个黑箱&#xff0c;有输入和输出&#xff0c;输入是一个图像…

Spring 中更加简单的 “存储“ 和 “读取“ 对象

目录 1. 更加简单的存储对象 1.1 配置扫描路径 1.2 使用五大类注解存储 bean 对象 1.2.1 五大类注解之间的关系 1.2.2 关于 bean 的命名规则 1.3 使用方法注解存储 bean 对象 1.3.1 bean 的重命名 2. 更加简单的获取对象 (DI) 2.1 属性注入 2.1.1 属性注入优缺点分析 …

三、图片的几何变换

目录一、图片缩放1 - 等比缩放2 - 最近领域插值3 - 双线性插值4 - 矩阵缩放二、图片剪切与位移1 - 图片剪切2 - 图片位移三、图片镜像四、图片仿射变换五、图片旋转一、图片缩放 1 - 等比缩放 # 1 load 2 info 3 resize 4 check import cv2img cv2.imread(image0.jpg, 1) im…

软件工程详细知识点复习(上)

文章目录一、软件工程概述1、软件与软件危机2、软件工程二、可行性研究三、需求分析四、概要设计五、详细设计一、软件工程概述 1、软件与软件危机 软件程序数据文档 1、软件危机的主要表现 软件不能满足用户需求软件开发成本严重超标&#xff0c;开发周期大大超过规定日期…

网络设备安装上线,你要知道的10个步骤

大家好&#xff0c;我是技福的小咖老师。在网络工程中设备的安装工作必不可少&#xff0c;你平时都是按哪些步骤完成的&#xff1f;今天给大家总结一下最常见的10个步骤。 安装流程 网络设备安装流程图 安装环境要求 1► 安装场景 为确保设备的正常运行&#xff0c;延长设备…

C. Infected Tree(思维+DFS)

Problem - 1689C - Codeforces Byteland是一片美丽的土地&#xff0c;因其美丽的树木而闻名。 米沙发现了一棵有n个顶点的二叉树&#xff0c;编号从1到n。二叉树是一个无环连接的双向图&#xff0c;包含n个顶点和n-1条边。每个顶点的度数最多为3&#xff0c;而根是数字为1的顶…