参考文献:《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition》

1、MMU

1.1 背景

  早期的内存是比较小的,一般是几十k,不过相应的程序也是比较小的,这时程序可以直接加载到内存中运行。后来为了支持多个程序的并行,内存中出现了固定分区,在编译阶段将不同程序,划分在不同的内存区域上。这种方式存在不少问题:一是内存分区大小与程序大小要匹配,二是地址空间无法动态的增长。于是内存动态分区思想便诞生了,内存上线划出一块区域给操作系统,然后剩余的内存空间给用户进程使用,这样用户程序所使用的内存空间,跟随程序大小及数目进行变动。

  不论是静态分析,还是动态分区,都存在一些问题:进程地址空间安全问题、内存使用效率低。为了能够让多程序安全、高效地并行运行,物理内存中需要存放多个程序的代码及数据,这时虚拟内存便诞生了。不得不说虚拟内存是一个伟大的发明,一方面它让每个程序认为自己是独自、连续的使用内存,另一方面,每个程序之间的内存形成了安全隔离,避免程序破坏彼此的内存。

  后来随着软件的快速发展,一个程序的大小变得很大,这时物理内存大小跟不上程序大小增加的速度。这样便不能将整个程序加载到物理内存中,一是物理内存没有这么大,二是如果将整个程序加载到内存,为了多程序并行,就需要将大量的数据及代码换入、换出,这导致程序运行效率低下。虚拟内存并没解决高效使用内存的问题,好在程序运行遵循时间、空间局部性原理,进而出现了分页机制。分页机制从根本上解决了高效使用物理内存的问题。每次只需要将几页的代码、数据从磁盘中加载到内存,程序就能正常运行。当程序运行的过程中,需要新的代码、数据会产生缺页异常,这些代码、数据就会从磁盘加载到内存,然后程序从异常恢复正常运行。

  对于支持虚拟内存,分页机制的系统,处理器直接寻址虚拟地址,这个地址不会直接发给内存控制器,而是先发给内存管理单元(Memory Manager Unit,MMU)。MMU 就是负责将虚拟地址转换和翻译成物理地址的一个硬件模块,其实 MMU 所做的事,完全可以通过 CPU 来实现。为啥还要一个 MMU 硬件模块呢?就是为了提升虚拟地址到物理地址转换的速度,减少转换所消耗的时间。MMU 包含两个模块TLB(Translation Lookaside Buffer)和TWU(Table Walk Unit)。TLB 是一个高速缓存,用于缓存页表转换的结果,从而缩短页表查询的时间。TWU 是一个页表遍历模块,页表是由操作系统维护在物理内存中,但是页表的遍历查询是由 TWU 完成的,这样减少对 CPU 资源的消耗。

在这里插入图片描述

  虚拟内存及分页机制的出现,解决了进程地址空间安全性的问题和内存使用效率低的问题,但是也引入了系统性能变差的问题。本来 CPU 可以直接通过访存执行程序,但是现在引入了虚拟地址到物理地址的转换。MMU 硬件模块的出现,就是为了解决这个性能问题。因此,几 G 运行内存的电脑,可以并行运行几十G的多程序,让你在听歌的同时,能够并行处理编辑文档,下载电影,收发邮件等。

1.2 主要功能

  • 地址翻译
    • 在用户访问内存时,将用户访问的虚拟地址翻译为实际的物理地址,以便 CPU 对实际的物理地址进行访问。
  • 访问权限控制
    • 可以对一些虚拟地址进行访问权限控制,以便于对用户程序的访问权限和范围进行管理,如代码段一般设置为只读,如果有用户程序对代码段进行写操作,系统会触发异常。
  • 引申的物理内存管理
    • 对系统的物理内存资源进行管理,为用户程序提供物理内存的申请、释放等操作接口。

2、Translation tables

2.1 Linux 中的页表

  Linux 内核通过多级页表管理虚拟地址到物理地址的转换。不同处理器架构可根据需求选择页表级数,主要组件包括:

缩写全称作用描述典型x86_64位偏移
PGDPage Global Directory顶级页表结构47-39位
P4DPage 4th-level Directory第四级目录(4.11新增)未固定
PUDPage Upper Directory上层目录38-30位
PMDPage Middle Directory中间目录29-21位
PTEPage Table Entry最终页表项指向物理页帧 20-12位

Linux 4.11 之前:

  • 最大支持4级页表(PGD→PUD→PMD→PTE)

实际使用级数由 CPU 架构决定

  • 4级:PGD+PUD+PMD+PTE(如x86_64常规4KB页)
  • 3级:PGD+PMD+PTE
  • 2级:PGD+PTE (常见的 ARMv7)

Linux 4.11 及之后:

  • 引入P4D 级(位于 PGD 和 PUD 之间),扩展至 5 级页表
  • 新增级数应对48位以上地址空间(如 5 级分页应对 57 位地址)

注意:
PGD、PUD、PMD、PTE 这些,都是 Linux 中的称呼。在不同 CPU 架构手册中,叫法可能各有不同。例如,在 ARMv7 中,对于只支持二级页表的情况下,PGD 称之为 First-level table,PTE 称之为 Second-level table。

  在下面的文章中,为了符合 ARM 手册,将 translation table,翻译成转换表,可以理解成,就是页表的含义。

2.2 ARMv7 中的页表

ARMv7-A 定义了两种转换表格式 :

Short-descriptor format

这是一种基础格式,是未包含 大物理地址扩展(LPAE) 的实现中唯一支持的格式。它在转换表中使用了 32 位的描述符条目,并提供了:

  • 提供两级地址转换
  • 32-bit 输入地址
  • 输出地址可高达 40-bit
  • 通过使用 supersections 实现 32 位以上地址的支持,最小内存单位为 16MB
  • 支持无访问域、客户端域和管理器域
  • 32-bit 页表项

Long-descriptor format

这是一种 可选格式。大型物理地址扩展增加了对这种格式的支持。它在转化表中使用了64位的描述符条目,并提供了:

  • 最多三级地址查找
  • 第二级转换时,输入地址可以高达40位
  • 输出地址高达40位
  • 4KB assignment granularity across the entire PA range.
  • 不支持域,所有存储区都被视为在客户域中
  • 64-bit 位页表项
  • 固定 4kB 的表大小,除非输入地址被截断

  本篇文章,皆以常见的 Short-descriptor format 为例进行讲解。关于 Long-descriptor format ,有兴趣的可以自行阅读 ARM 手册相关内容。

转换表格式,其实就是页表格式。所谓的 descriptor,描述的就是各级 页表 的 页表项

3. Short-descriptor format

3.1 Short-descriptor translation table format descriptors

  短描述符转换表格式支持以内存段(memory sections)或内存页(memory pages)为基础的内存映射

  • Spersections: 16MB 的内存块(memory block)
  • Sections: 1MB 的内存块
  • Large pages: 64KB 的内存块
  • Small pages: 4KB 的内存块

这其中,除了 Small pages,其余仅使用单个 TLB 表项映射大区域内存。

Short-descriptor format 中是否支持 Spersections 是 implementation DEFINED

当使用 Short-descriptor translation table format 时,内存中保留两级转换表:

First-level table
一级表存储一级描述符,每个描述符包含:

  • 段(Section)和超级段(Supersection)的基地址与转换属性
  • 大页(Large page)或小页(Small page)的转换属性及指向二级表的指针

Second-level tables
二级表存储二级描述符,包含:

  • 大页(Large page)或小页(Small page)基地址与转换属性

在短描述符格式(Short-descriptor format)中,二级表可称为页表(Page tables)

注:每个二级表需占用 1KB 内存空间。一条页表项 4 字节,共有 2^8 个页表项
First-level table 在 Linux 中又叫做一级页表,PGD
Second-level tables 在 Linux 中又叫做二级页表,PTE

转换表中的描述符通常分为以下类型:

  • 无效条目/故障条目(Invalid or fault entry)
  • 页表条目(Page table entry):指向下一级翻译表
  • 页条目/段条目(Page or section entry):定义内存访问属性
  • 保留格式(Reserved format)
    在这里插入图片描述

从上图也可以看到,Large pages、 Small pages 才支持二级页表。Spersections 和 Sections 只支持一级页表

3.1.1 一级转换表

在这里插入图片描述
最后两位 bits[1:0] 决定描述符类型

  • 0b00, Invalid
  • 0b01, Page table
  • 0b10, Section or Supersection
  • 0b11, Section or Supersection, if the implementation supports the PXN attribute
  • 0b11, Reserved, UNK/SBZP, if the implementation does not support the PXN attribute

举个例子,当操作系统全部使用 Section 作为一级页表中的最小寻址单位时,共 12 位地址表示。因为 Section 不涉及到二级页表,所以页表项最多为 212。而页内偏移为 [19:0] ,所以页大小为 220。综上,可以表示的最大虚拟空间大小为:212 * 220

3.1.2 二级转换表

在这里插入图片描述
最后两位 bits[1:0] 决定描述符类型

  • 0b00, Invalid
  • 0b01, Large page
  • 0b1x, Small page

举个例子,使用 Small Page 作为二级页表的最小寻址单位时,共 20 位地址表示,所能表示的最大虚拟地址范围就是 220* 212 = 4GB。页内偏移为 [11:0] ,所以页大小为 212

3.2 Short-descriptor translation table format descriptors 中的内存属性

  把内存属性单独拿出一个章节来讲,是因为内存属性很重要.。

  • TEX[2:0], C, B
    • 内存区域属性位
    • 详见第 5 章节
  • XN bit
    • 全局不可执行。如果执行,会触发 Permission fault
  • PXN bit, when supported
    • Privileged Execute-Never,特权模式不可执行。在支持的情况下,PXN 位决定处理器在 PL1 特权级时是否可执行该内存区域的代码。如果执行,会触发 Permission fault
  • NS bit
    • NS = Non-Secure,只有在 启用了 TrustZone 安全扩展 的系统中才有意义
    • 只会出现在一级页表项中
    • 详见 3.4 章节
  • Domain
    • Domains 是一种粗粒度的内存访问控制机制,属于 ARMv7 MMU 的一部分
    • 详见 3.3 章节
  • AP[2], AP[1:0]
    • 访问权限位
    • 详见 3.5 章节
  • S bit
    • 可共享位。确定寻址区域是否为可共享内存
  • nG bit
    • 非全局位。确定如何在 TLB 中标记转换
  • Bit[18], when bits[1:0] indicate a Section or Supersection descriptor
    • 0 Descriptor is for a Section.
    • 1 Descriptor is for a Supersection

3.3 Domains

在 ARMv7 的 VMSA 中,仅存在于 Short-descriptor format:

  • 总共有 16 个 Domains(编号 0 ~ 15)
  • 每个 Section/Page 在页表中被指定属于哪个 Domain(用 4-bit 域表示)(不支持 Supersections)
  • 仅有一级页表条目有 Domains 域,二级转换表条目从父级一级页表条目继承域设置
  • CPU 中有一个 DACR(Domain Access Control Register),用于配置每个 Domain 的访问权限

DACR 是一个 32-bit 寄存器,每两个 bit 控制一个 domain 的访问权限。每个域的访问权限可以设置为:
在这里插入图片描述

含义解释
0b00No access访问该 domain 的内存会导致 fault
0b01Client根据页表中的 AP bits 进行访问权限检查(标准权限检查方式)
0b11Manager不检查访问权限,直接允许访问(相当于绕过 AP 位)
0b10保留(未使用)访问该 domain 的内存会导致 fault

✅ 工作原理总结

访问内存时,ARM 的 MMU 进行如下步骤:

  • 使用页表将虚拟地址翻译为物理地址,并获得该页属于哪个 Domain(通过页表项的 bits[8:5])
  • 查 DACR 寄存器,查看该 Domain 的访问权限设定
  • 根据权限设定决定行为:
    • No access → 抛出异常
    • Client → 继续检查页表中的 AP(Access Permission)位
    • Manager → 直接允许访问(忽略 AP 位)

Domains 域现在不常用了。粒度太粗、管理复杂,ARMv8+ 已废弃 Domain 概念,使用更加细粒度的 EL 权限模型、权限表(PTE) 来管理访问权限。
在 ARMv7 的 VMSA 中,Domains 是 MMU 机制的一部分,不能绕过或关闭。通常可以将所有 domain 设置为 Client,配合合理的页表 AP

3.4 Control of Secure or Non-secure memory access

  在 ARMv7 架构中,针对安全(Secure)与非安全(Non-secure)PL1&0 阶段的第 1 阶段地址转换,其转换表基址寄存器(TTBR0、TTBR1)及转换表基址控制寄存器(TTBCR)均采用安全与非安全 双 bank 设计。处理器执行内存访问时所处的安全状态(Security state)将自动选择对应版本的寄存器组。

  当 CPU 当前处于 Secure 状态(比如运行 Secure OS 或 TrustZone secure world 中的代码)时:页表中设置的 NS 位会影响这个访问是访问到:

  • Secure memory(NS=0)
  • 还是 Non-Secure memory(NS=1)

  当 CPU 当前处于 Non-secure 状态时(比如运行 Linux 等普通 OS):页表中设置的 NS 位根本不起作用,会被忽略。因为 Non-secure world 不能访问 Secure memory。

3.5 Memory access control

  在转换表描述符中,访问权限位(Access permission bits)用于控制对相应内存区域的访问。短描述符转换表格式(Short-descriptor translation table format)支持两种定义访问权限的方式:

  • 三比特模式(AP[2:0]):
    • 通过 AP[2:0] 三个比特位定义访问权限。
  • 两比特模式(AP[2:1] + 访问标志位):
    • 通过 AP[2:1] 两个比特位定义访问权限,
    • AP[0] 可作为访问标志(Access flag)使用。

系统控制寄存器 SCTLR.AFE 用于选择访问权限的配置模式:

  • 若将该位置 1(启用访问标志功能),则自动选择 AP[2:1] 定义访问权限的模式。

在这里插入图片描述
在这里插入图片描述

3.6 转换表的工作流程

以 Small page 为例:

  • 首先输入的虚拟地址,可以大致分为 3 部分
    • 一级页表偏移
    • 二级页表偏移
    • 页内偏移
  • 根据输入地址的范围(也就是 N 的取值,这一块可以先阅读第 4 章节),找到页表基址(TTBR0 or TTBR1)
  • 根据 页表基址 + 一级页表偏移,找到一级页表的 “页表项描述符”
    • 页表基址,实际上就是 PGD 表的基址;一级页表偏移指的就是 PGD 表中的某个页表项
  • 根据上一步找到 “一级页表项描述符” 中包含的二级页表的基址[31:10] + 二级页表偏移,找到二级页表的 “页表项描述符”
    • 二级页表的基址,实际上就是 PTE 表的基址;二级页表偏移指的就是 PTE 表中的某个页表项
  • 在二级页表项描述符中,就包含了最终的物理地址的 [31:12],再加上 页内偏移,最终就找到了物理地址
    • 二级页表项描述符,指的就是 PTE 表中的某个页表项

在这里插入图片描述

4、关于页表基址 TTBR

   ARMv7 中,有两个存放页表(一级页表)基地址的寄存器,TTBR0 和 TTBR1。那 MMU 进行地址翻译(translation table walk)的时候到底是选择哪一个寄存器的值作为基地址呢?

   那么这个时候就需要用到 TTBCR 寄存器,这个寄存器的格式如下图:
在这里插入图片描述

其中最后 [2-0] 位(值为N)决定了用 TTBR0 还是 TTBR1,手册里面已经说的很清楚了,这里直接贴出来。

在这里插入图片描述

  • 当 N == 0 是,使用 TTBR0 作为基址。关闭/禁用 TTBR1 作为基址
  • 当 N>0 时,如果给的虚拟地址 [31:32-N] 位都是 0,那么用 TTBR0,一旦这其中的某一位是 1 了,就用 TTBR1。
    • 又因为 N 的取值只有 0 到 7,所以每个 N 的取值都对应了一个用 TTBR0 或者 TTBR1 的分界线。
    • 比如当 N=2 时,如果给的虚拟地址第 [31:30] 位为 0,即不超过 0x40000000,那么就用 TTBR0,一旦等于或者超过 0x40000000 那么就用 TTBR1。

下面还有一张 N 值不同时,对应的地址分界线图:
在这里插入图片描述
在这里插入图片描述

理论上,一个页表基址寄存器也是能正常工作,但两个可以带来更好的性能和系统隔离性

为什么需要两个基址寄存器?

✅ 好处 1:用户态和内核态分离

  • 操作系统常常希望:
    • 用户进程只能访问用户空间
    • 内核可以访问全部空间
  • 有了两个页表:
    • 切换进程时,只需切换 TTBR0(用户空间部分)
    • TTBR1(内核空间页表)不变,提升性能并确保安全

✅ 好处 2:性能优化

  • 避免频繁清空整个 TLB(Translation Lookaside Buffer):
    • 进程切换时只换 TTBR0 → 用户空间换了
    • TTBR1 → 内核页表不变,TLB 中内核页表项仍有效

✅ 好处 3:内核共享

  • 所有进程共用一套内核映射(TTBR1)
  • 每个进程拥有自己独立的用户映射(TTBR0)

5、关于内存属性

  除输出地址外,指向内存页或区域的页表项通常还包含用于定义目标内存属性字段(Memory Region Attribute Fields),用于控制:

  • 内存类型(如Normal或Device内存)
  • 缓存访问策略(如Write-Back、Write-Through)
  • 可共享性(Shareable),即多核间的一致性(Coherency)

为深入理解内存属性字段的配置机制,需区分以下两种模式:

  • Short-descriptor translation table format (无 TEX 重映射)
    • 直接通过TEX[2:0]、C(Cacheable)、B(Bufferable)位组合定义内存行为,适用于大多数标准场景
  • Short-descriptor translation table format (启用 TEX 重映射)
    • 通过 CP15 寄存器重映射 TEX 位语义,支持更灵活的缓存策略定制,常见于定制化硬件设计

5.1 without TEX remap

  使用 Short-descriptor translation table formats 时,可以使用 SCTLR.TRE 禁用 TEX 重映射。

在这里插入图片描述
在这里插入图片描述
(without TEX remap 场景下的的 Shareability 与 S 位)

转换表项(Translation Table Entry)中还包含一个 S 位(Shareable位),其作用如下:

  • 若表项指向设备内存(Device)或强序内存(Strongly-ordered):
    • 该位被忽略(无论S=0或1均不生效)
  • 若表项指向普通内存(Normal Memory):
    • 该位决定内存区域的可共享性(Shareability):
      • S=0:该普通内存区域为非可共享(Non-shareable)。
      • S=1:该普通内存区域为可共享(Shareable)

在这里插入图片描述

5.2 with TEX remap

  当使用短描述符转换表格式(Short-descriptor translation table formats)时,若将系统控制寄存器 SCTLR.TRE 置为1,则启用 TEX 重映射(TEX Remap)功能。在此配置下:

  • 软件配置要求
    • 定义转换表的软件必须编程设置 PRRR(Primary Region Remap Register)和 NMRR(Normal Memory Remap Register),以指定 7 种可能的内存区域属性
    • 转换表描述符中的 TEX[0]、C(Cacheable)和 B(Bufferable)位通过索引 PRRRNMRR 来定义内存区域属性。
    • 硬件不会使用 TEX[2:1] 位
  • TEX 重映射生效时的行为
    • 对于 TEX[0]、C 和 B 位的 8 种可能组合中的 7 种,其内存区域属性由 PRRRNMRR 的字段定义(如本节所述)。
    • 第 8 种组合的含义由具体实现定义(IMPLEMENTATION DEFINED)。
    • PRRR 中的 4 个比特位用于定义该区域是否可共享(Shareable)
  • 寄存器字段映射关系
    • 对于转换表项中TEX[0]、C 和 B 位的所有可能编码组合,表 B3-12 列出了 PRRRNMRR 寄存器中描述内存区域属性的对应字段。

在这里插入图片描述

   乍一看,有点迷糊,这块不太好理解。总结来说,在 without TEX remap 的场景下,在 PTE 中直接写 TEX[2:0]、B、C 对应的 bit 位,就可以控制页面的内存区域属性。但是在 with TEX remap 情况下,TEX、B、C 的值不再直接表示区域内存属性。所有可能的区域内存属性的值,都在 PRRRNMRR 寄存器中。而 TEX、B、C 三个 bit 组成的 8 种组合,就是在这两个寄存器中的偏移。

PRRR, Primary Region Remap Register, VMSA

在这里插入图片描述
所有 TEX[0]、B、C 可能的组合如下:对应的 n 是几,使用的就是 NOS(n) + TR(n) 的组合
在这里插入图片描述
关于 PRRR 寄存器,以下是详细解释:
在这里插入图片描述

  • NOS 位是控制内存区域的可共享属性。
  • NS1, bit[19]
  • NS0, bit[18]
  • DS1, bit[17]
  • DS0, bit[16]

   这些位控制的是 Normal memory 和 Device memory 的共享性(shareability)属性,也就是说,它们不是控制缓存方式,而是控制 是否是 shareable 类型。

名称控制的情况含义(bit 值)
NS0 (bit 18)页表中 S=0 且是 Normal memory0 ➜ Non-shareable
1 ➜ Shareable
NS1 (bit 19)页表中 S=1 且是 Normal memory0 ➜ Non-shareable
1 ➜ Shareable
NS0 (bit 18)类似逻辑,作用于 Device memory,S=0/S=1 映射

在这里插入图片描述

  • TR 位用于控制内存类型

NMRR, Normal Memory Remap Register, VMSA

关于 NMRR 寄存器这里不再详细解释了,有兴趣的可以自行研究。
在这里插入图片描述

6、Linux 下的页表映射

  上面讲的都是理论,我们对应到 Linux 源码中,看看内存区域属性是如何配置的。我们查看下 ARM 架构下的 ioremap 的实现。

arch\arm\mm\ioremap.c:

void __iomem *ioremap(resource_size_t res_cookie, size_t size)
{return arch_ioremap_caller(res_cookie, size, MT_DEVICE,__builtin_return_address(0));
}
EXPORT_SYMBOL(ioremap);void __iomem *ioremap_cache(resource_size_t res_cookie, size_t size)
{return arch_ioremap_caller(res_cookie, size, MT_DEVICE_CACHED,__builtin_return_address(0));
}
EXPORT_SYMBOL(ioremap_cache);void __iomem *ioremap_wc(resource_size_t res_cookie, size_t size)
{return arch_ioremap_caller(res_cookie, size, MT_DEVICE_WC,__builtin_return_address(0));
}

  关于 MT_DEVICEMT_DEVICE_CACHEDMT_DEVICE_WC,就是内存区域属性,最终会被翻译成我们在第 5 章节所讲的 TEX、B、C 的值写到 PTE 页表项中。

arch\arm\mm\mmu.c

static struct mem_type mem_types[] __ro_after_init = {[MT_DEVICE] = {		  /* Strongly ordered / ARMv6 shared device */.prot_pte	= PROT_PTE_DEVICE | L_PTE_MT_DEV_SHARED |L_PTE_SHARED,.prot_l1	= PMD_TYPE_TABLE,.prot_sect	= PROT_SECT_DEVICE | PMD_SECT_S,.domain		= DOMAIN_IO,},[MT_DEVICE_NONSHARED] = { /* ARMv6 non-shared device */.prot_pte	= PROT_PTE_DEVICE | L_PTE_MT_DEV_NONSHARED,.prot_l1	= PMD_TYPE_TABLE,.prot_sect	= PROT_SECT_DEVICE,.domain		= DOMAIN_IO,},[MT_DEVICE_CACHED] = {	  /* ioremap_cache */.prot_pte	= PROT_PTE_DEVICE | L_PTE_MT_DEV_CACHED,.prot_l1	= PMD_TYPE_TABLE,.prot_sect	= PROT_SECT_DEVICE | PMD_SECT_WB,.domain		= DOMAIN_IO,},[MT_DEVICE_WC] = {	/* ioremap_wc */.prot_pte	= PROT_PTE_DEVICE | L_PTE_MT_DEV_WC,.prot_l1	= PMD_TYPE_TABLE,.prot_sect	= PROT_SECT_DEVICE,.domain		= DOMAIN_IO,},............
}

上表中提到的 L_PTE_MT_DEV_SHAREDL_PTE_SHARED 页表项 PTE 相关的宏,定义在 pgtable-2level.h 头文件中:

arch\arm\include\asm\pgtable-2level.h

/** These are the memory types, defined to be compatible with* pre-ARMv6 CPUs cacheable and bufferable bits: n/a,n/a,C,B* ARMv6+ without TEX remapping, they are a table index.* ARMv6+ with TEX remapping, they correspond to n/a,TEX(0),C,B** MT type		Pre-ARMv6	ARMv6+ type / cacheable status* UNCACHED		Uncached	Strongly ordered* BUFFERABLE		Bufferable	Normal memory / non-cacheable* WRITETHROUGH		Writethrough	Normal memory / write through* WRITEBACK		Writeback	Normal memory / write back, read alloc* MINICACHE		Minicache	N/A* WRITEALLOC		Writeback	Normal memory / write back, write alloc* DEV_SHARED		Uncached	Device memory (shared)* DEV_NONSHARED	Uncached	Device memory (non-shared)* DEV_WC		Bufferable	Normal memory / non-cacheable* DEV_CACHED		Writeback	Normal memory / write back, read alloc* VECTORS		Variable	Normal memory / variable** All normal memory mappings have the following properties:* - reads can be repeated with no side effects* - repeated reads return the last value written* - reads can fetch additional locations without side effects* - writes can be repeated (in certain cases) with no side effects* - writes can be merged before accessing the target* - unaligned accesses can be supported** All device mappings have the following properties:* - no access speculation* - no repetition (eg, on return from an exception)* - number, order and size of accesses are maintained* - unaligned accesses are "unpredictable"*/
#define L_PTE_MT_UNCACHED	(_AT(pteval_t, 0x00) << 2)	/* 0000 */
#define L_PTE_MT_BUFFERABLE	(_AT(pteval_t, 0x01) << 2)	/* 0001 */
#define L_PTE_MT_WRITETHROUGH	(_AT(pteval_t, 0x02) << 2)	/* 0010 */
#define L_PTE_MT_WRITEBACK	(_AT(pteval_t, 0x03) << 2)	/* 0011 */
#define L_PTE_MT_MINICACHE	(_AT(pteval_t, 0x06) << 2)	/* 0110 (sa1100, xscale) */
#define L_PTE_MT_WRITEALLOC	(_AT(pteval_t, 0x07) << 2)	/* 0111 */
#define L_PTE_MT_DEV_SHARED	(_AT(pteval_t, 0x04) << 2)	/* 0100 */
#define L_PTE_MT_DEV_NONSHARED	(_AT(pteval_t, 0x0c) << 2)	/* 1100 */
#define L_PTE_MT_DEV_WC		(_AT(pteval_t, 0x09) << 2)	/* 1001 */
#define L_PTE_MT_DEV_CACHED	(_AT(pteval_t, 0x0b) << 2)	/* 1011 */
#define L_PTE_MT_VECTORS	(_AT(pteval_t, 0x0f) << 2)	/* 1111 */
#define L_PTE_MT_MASK		(_AT(pteval_t, 0x0f) << 2)

对于 ARMv7 来说,我们只需要关注上表的后三位。分别对应的就是 TEX[0]、C、B。

6.1 关于 PRRR 和 NMRR 寄存器的值

arch\arm\mm\proc-v7-2level.S

	/** Memory region attributes with SCTLR.TRE=1**   n = TEX[0],C,B*   TR = PRRR[2n+1:2n]		- memory type*   IR = NMRR[2n+1:2n]		- inner cacheable property*   OR = NMRR[2n+17:2n+16]	- outer cacheable property**			n	TR	IR	OR*   UNCACHED		000	00*   BUFFERABLE		001	10	00	00*   WRITETHROUGH	010	10	10	10*   WRITEBACK		011	10	11	11*   reserved		110*   WRITEALLOC		111	10	01	01*   DEV_SHARED		100	01*   DEV_NONSHARED	100	01*   DEV_WC		001	10*   DEV_CACHED		011	10** Other attributes:**   DS0 = PRRR[16] = 0		- device shareable property*   DS1 = PRRR[17] = 1		- device shareable property*   NS0 = PRRR[18] = 0		- normal shareable property*   NS1 = PRRR[19] = 1		- normal shareable property*   NOS = PRRR[24+n] = 1	- not outer shareable*/
.equ	PRRR,	0xff0a81a8        // 初始化了 PRRR 寄存器的值
.equ	NMRR,	0x40e040e0        // 初始化了 NMRR 寄存器的值

对应的,将 PRRR 翻译结果如下:

寄存器偏移对应值属性
TR000Strongly-ordered
TR110Normal memory
TR210Normal memory
TR310Normal memory
TR401Device
TR500Strongly-ordered
TR600Strongly-ordered
TR710Normal memory
寄存器偏移对应值属性
NOS01Memory region is Inner Shareable
NOS11Memory region is Inner Shareable
NOS21Memory region is Inner Shareable
NOS31Memory region is Inner Shareable
NOS41Memory region is Inner Shareable
NOS51Memory region is Inner Shareable
NOS61Memory region is Inner Shareable
NOS71Memory region is Inner Shareable

NMRR 翻译结果如下:

寄存器偏移对应值属性
OR000Region is Non-cacheable
OR100Region is Non-cacheable
OR201Region is Write-Back, Write-Allocate
OR311Region is Write-Back, no Write-Allocate
OR400Region is Non-cacheable
OR500Region is Non-cacheable
OR600Region is Non-cacheable
OR701Region is Write-Back, Write-Allocate

6.2 案例

函数调用关系链:

ioremap+-> arch_ioremap_caller+-> __arm_ioremap_pfn_caller+-> get_mem_type+-> ioremap_page_range+-> kmsan_ioremap_page_range+->__vmap_pages_range_noflush+->vmap_range_noflush+->vmap_p4d_range+->vmap_pud_range+->vmap_pmd_range+->vmap_pte_range+->set_pte_at+->cpu_v7_set_pte_ext

总结来说,在 ARMv7 架构下,调用 ioremap 接口,默认会将地址映射成 MT_DEVICE 属性。

void __iomem *ioremap(resource_size_t res_cookie, size_t size)
{return arch_ioremap_caller(res_cookie, size, MT_DEVICE,__builtin_return_address(0));
}
EXPORT_SYMBOL(ioremap);

在调用 get_mem_type 接口时,会将 MT_DEVICE 转换成架构相关的内存区域属性相关的宏 :
PROT_PTE_DEVICE | L_PTE_MT_DEV_SHARED | L_PTE_SHARED

static struct mem_type mem_types[] __ro_after_init = {[MT_DEVICE] = {		  /* Strongly ordered / ARMv6 shared device */.prot_pte	= PROT_PTE_DEVICE | L_PTE_MT_DEV_SHARED |L_PTE_SHARED,.prot_l1	= PMD_TYPE_TABLE,.prot_sect	= PROT_SECT_DEVICE | PMD_SECT_S,.domain		= DOMAIN_IO,},............
}const struct mem_type *get_mem_type(unsigned int type)
{return type < ARRAY_SIZE(mem_types) ? &mem_types[type] : NULL;
}

而这其中,和硬件相关的就是 PROT_PTE_DEVICE | L_PTE_MT_DEV_SHARED

#define L_PTE_SHARED		(_AT(pteval_t, 1) << 10)	/* shared(v6), coherent(xsc3) */#define L_PTE_MT_DEV_SHARED	(_AT(pteval_t, 0x04) << 2)	/* 0100 */

L_PTE_MT_DEV_SHARED 宏为例,介绍如何通过这个宏,去控制内存区域属性的。

  • 对应的 TEX[0]、C、B,组成的 index 值为 0x4
  • 对应 RRRNMRR 寄存器中,使用的就是
    • NOS4 + TR4 + OR4 + IR4 的组合、

最终,会调用 set_pte_at 接口,将这些宏设置到 PTE 页表项中。
该函数的三个入参:

  • r0: pte 页表项的地址
  • r1: Linux 版二级页表项的内容(也就是 PROT_PTE_DEVICE | L_PTE_MT_DEV_SHARED | L_PTE_SHARED 这些值的组合)
  • r2: 通常是 0,除非需要特殊设置
  • 输出 r3:硬件页表项(详情见 6.3 章节)
ENTRY(cpu_v7_set_pte_ext)
#ifdef CONFIG_MMUstr	r1, [r0]			@ linux version //把 r1(linux 的 PTE)存入 r0 地址bic	r3, r1, #0x000003f0         //先清除 r1 的 bit[9:4](通常为 memory type / cache属性)bic	r3, r3, #PTE_TYPE_MASK      //然后再清除最低 2 位(bit[1:0],即页表项类型 Small Page / Section)orr	r3, r3, r2					//将扩展属性(ext)合并进 r3orr	r3, r3, #PTE_EXT_AP0 | 2    //设置 AP0 位(Access permission)和最低两位为 0b10,表示 Small Pagetst	r1, #1 << 4                 //如果原始 PTE 的 bit4 被置位,则设置硬件页表项的 TEX=1orrne	r3, r3, #PTE_EXT_TEX(1)eor	r1, r1, #L_PTE_DIRTYtst	r1, #L_PTE_RDONLY | L_PTE_DIRTYorrne	r3, r3, #PTE_EXT_APX	//根据页表中的只读和脏页标志,设置 APX(Access Permission Extension)位tst	r1, #L_PTE_USERorrne	r3, r3, #PTE_EXT_AP1	//如果页表设置了 user-mode 访问权限,设置 AP1(即允许用户空间访问)tst	r1, #L_PTE_XNorrne	r3, r3, #PTE_EXT_XN		//设置执行禁止位(XN)tst	r1, #L_PTE_YOUNGtstne	r1, #L_PTE_VALIDeorne	r1, r1, #L_PTE_NONEtstne	r1, #L_PTE_NONEmoveq	r3, #0					//这段逻辑是做 PTE 的“有效性”判断:如果不是 Young 或 Valid,或者被标记为 None(无效项),则把 r3 清零(页表项无效)ARM(	str	r3, [r0, #2048]! )		//真正的“硬件页表项”是写在 r0 + 2048 字节偏移处!THUMB(	add	r0, r0, #2048 )THUMB(	str	r3, [r0] )ALT_SMP(W(nop))ALT_UP (mcr	p15, 0, r0, c7, c10, 1)		@ flush_pte
#endifbx	lr
ENDPROC(cpu_v7_set_pte_ext)

6.3 硬件页表项和软件页表项

  在 ARM 架构下,尤其是 Linux 运行于 ARMv6/v7 等 MMU 支持的平台时,页表(Page Table)是内存管理的核心组件。为了兼顾内核的抽象管理与硬件 MMU 的访问需求,Linux 使用了“双版本页表”的设计思路,即:

  • 软件版本(Software Version)
  • 硬件版本(Hardware Version)

  这两者虽共享数据结构和逻辑关联,但服务于不同的对象和目的,理解它们的区别对于调试页表异常、实现页表扩展、或阅读内核代码都至关重要。

软件版本页表(Software Version)

软件版本是 Linux 内核自己维护和使用的页表项,它主要用于:

  • 内核自身管理的权限与状态位(如 Dirty、Accessed、Writeback)
  • Linux 的抽象内存属性(如 L_PTE_DIRTY, L_PTE_YOUNG, L_PTE_RDONLY 等)
  • 与用户空间接口(如 mprotect, mmap)之间的语义协同

这些软件位通常嵌入在标准页表项结构中(如 pteval_t),但并不一定直接被 ARM MMU 硬件识别。在 ARM Linux 中,这些位通过宏定义编码进 PTE 中,仅用于 Linux 内核逻辑判断,硬件则会忽略它们

软件页表项位通常保存在:

  • pte_t 类型的结构体中
  • 页表内存中的 [r0] 位置(见 cpu_v7_set_pte_ext 中第一条 str r1, [r0])

硬件版本页表(Hardware Version)

硬件版本是写入到 ARM MMU 实际访问的页表内存地址 中的内容。它必须符合 ARM 架构定义的格式,包含如下内容:

  • 页表项类型(small page, section, supersection 等)
  • 权限位(AP[2:0], APX)
  • 缓存控制位(TEX[2:0], C, B)
  • 共享属性(S, nG 等)
  • 执行权限(XN)

其实就是我们上面所讲到的一些 ARM 寄存器相关的知识

在执行页表设置时,Linux 会将软件版本的页表项翻译成硬件格式并写入对应物理地址供 MMU 使用。

硬件页表项最终保存在:

  • 页表物理页中,通常偏移 +2048 字节
  • 通过 cpu_v7_set_pte_ext 函数中 str r3, [r0, #2048]! 写入

arch\arm\include\asm\pgtable-2level.h 中的宏,对于硬件版本和软件版本有很好的体现:

/** "Linux" PTE definitions.** We keep two sets of PTEs - the hardware and the linux version.* This allows greater flexibility in the way we map the Linux bits* onto the hardware tables, and allows us to have YOUNG and DIRTY* bits.** The PTE table pointer refers to the hardware entries; the "Linux"* entries are stored 1024 bytes below.*/
#define L_PTE_VALID		(_AT(pteval_t, 1) << 0)		/* Valid */
#define L_PTE_PRESENT		(_AT(pteval_t, 1) << 0)
#define L_PTE_YOUNG		(_AT(pteval_t, 1) << 1)
#define L_PTE_DIRTY		(_AT(pteval_t, 1) << 6)
#define L_PTE_RDONLY		(_AT(pteval_t, 1) << 7)
#define L_PTE_USER		(_AT(pteval_t, 1) << 8)
#define L_PTE_XN		(_AT(pteval_t, 1) << 9)
#define L_PTE_SHARED		(_AT(pteval_t, 1) << 10)	/* shared(v6), coherent(xsc3) */
#define L_PTE_NONE		(_AT(pteval_t, 1) << 11)//以下都是硬件版本,上面都讲过
/** These are the memory types, defined to be compatible with* pre-ARMv6 CPUs cacheable and bufferable bits: n/a,n/a,C,B* ARMv6+ without TEX remapping, they are a table index.* ARMv6+ with TEX remapping, they correspond to n/a,TEX(0),C,B** MT type		Pre-ARMv6	ARMv6+ type / cacheable status* UNCACHED		Uncached	Strongly ordered* BUFFERABLE		Bufferable	Normal memory / non-cacheable* WRITETHROUGH		Writethrough	Normal memory / write through* WRITEBACK		Writeback	Normal memory / write back, read alloc* MINICACHE		Minicache	N/A* WRITEALLOC		Writeback	Normal memory / write back, write alloc* DEV_SHARED		Uncached	Device memory (shared)* DEV_NONSHARED	Uncached	Device memory (non-shared)* DEV_WC		Bufferable	Normal memory / non-cacheable* DEV_CACHED		Writeback	Normal memory / write back, read alloc* VECTORS		Variable	Normal memory / variable** All normal memory mappings have the following properties:* - reads can be repeated with no side effects* - repeated reads return the last value written* - reads can fetch additional locations without side effects* - writes can be repeated (in certain cases) with no side effects* - writes can be merged before accessing the target* - unaligned accesses can be supported** All device mappings have the following properties:* - no access speculation* - no repetition (eg, on return from an exception)* - number, order and size of accesses are maintained* - unaligned accesses are "unpredictable"*/
#define L_PTE_MT_UNCACHED	(_AT(pteval_t, 0x00) << 2)	/* 0000 */
#define L_PTE_MT_BUFFERABLE	(_AT(pteval_t, 0x01) << 2)	/* 0001 */
#define L_PTE_MT_WRITETHROUGH	(_AT(pteval_t, 0x02) << 2)	/* 0010 */
#define L_PTE_MT_WRITEBACK	(_AT(pteval_t, 0x03) << 2)	/* 0011 */
#define L_PTE_MT_MINICACHE	(_AT(pteval_t, 0x06) << 2)	/* 0110 (sa1100, xscale) */
#define L_PTE_MT_WRITEALLOC	(_AT(pteval_t, 0x07) << 2)	/* 0111 */
#define L_PTE_MT_DEV_SHARED	(_AT(pteval_t, 0x04) << 2)	/* 0100 */
#define L_PTE_MT_DEV_NONSHARED	(_AT(pteval_t, 0x0c) << 2)	/* 1100 */
#define L_PTE_MT_DEV_WC		(_AT(pteval_t, 0x09) << 2)	/* 1001 */
#define L_PTE_MT_DEV_CACHED	(_AT(pteval_t, 0x0b) << 2)	/* 1011 */
#define L_PTE_MT_VECTORS	(_AT(pteval_t, 0x0f) << 2)	/* 1111 */
#define L_PTE_MT_MASK		(_AT(pteval_t, 0x0f) << 2)

为什么需要两个版本?

  • 可扩展性:软件页表项可以携带更多信息,如“是否访问过”、“是否脏”等,硬件页表项不一定有足够位支持这些信息
  • 兼容性:Linux 在不同架构上运行时,仍可以通过抽象的软件表示层实现统一逻辑
  • 性能调优:部分页表标志如 young/old, dirty 可由软件策略控制,而不依赖硬件

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

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

相关文章

Github 贪吃蛇 主页设置

自动化脚本顶部元信息触发条件&#xff08;on:&#xff09;作业&#xff08;jobs:&#xff09;步骤&#xff08;steps:&#xff09;1. 生成 SVG2. 推送到 output 分支Commit & Push在 README 里引用参考&#xff1a;https://github.com/Platane/Platane/tree/master 首先写…

关于Spring RestTemplate

​ 一、概述RestTemplate 是 Spring Framework 提供的一个同步 HTTP 客户端工具&#xff0c;用于简化与 RESTful API 的交互。它封装了底层 HTTP 通信细节&#xff0c;提供了统一的 API 来发送各种 HTTP 请求&#xff08;GET、POST、PUT、DELETE 等&#xff09;&#xff0c;并自…

异步解决一切问题 |消息队列 |减少嵌套 |hadoop |rabbitmq |postsql

设计准则“为什么要考虑这个问题”The forward logic is only about 10% of your code, everything else is 90%.主流逻辑 10%保障扩容和稳健的代码设计90%同步代码就是绑在一个绳上的蚂蚱异步就是实现了解耦这个异步或许有点类似于--一些分布式数据的处理 设计如何实现的呢?…

Spring AI 项目实战(十八):Spring Boot + AI + Vue3 + OSS + DashScope 实现高效语音识别系统(附完整源码)

系列文章 序号 文章名称 1 Spring AI 项目实战(一):Spring AI 核心模块入门 2 Spring AI 项目实战(二):Spring Boot + AI + DeepSeek 深度实战(附完整源码) 3 Spring AI 项目实战(三):Spring Boot + AI + DeepSeek 打造智能客服系统(附完整源码) 4

指针数组和数组指针的应用案例

1. 指针数组应用&#xff1a;查找最长字符串用指针数组存储若干字符串&#xff0c;编写函数找出其中最长的字符串&#xff08;若有多个&#xff0c;返回第一个&#xff09;。#include <stdio.h> #include <string.h>// 函数原型&#xff1a;找出最长字符串 const c…

MCU进入低功耗模式前的引脚处理原则和方法 --> 以最小化低功耗电流

在MCU进入低功耗模式(如Sleep, Stop, Standby, Deep Sleep等)前,精心处理每一个GPIO引脚的状态是最大限度降低功耗电流的关键一步。悬空或配置不当的引脚是导致“漏电”的常见原因。以下是处理引脚以达到最小低功耗电流的原则和方法: 📌 核心原则 避免浮空输入: 浮空(…

张 关于大语言模型(LLM)置信度研究的经典与前沿论文 :温度缩放;语义熵;自一致性;事实与反思;检索增强;黑盒引导;

关于大语言模型(LLM)置信度研究的经典与前沿论文 :温度缩放;语义熵;自一致性;事实与反思;检索增强;黑盒引导; 目录 关于大语言模型(LLM)置信度研究的经典与前沿论文 :温度缩放;语义熵;自一致性;事实与反思;检索增强;黑盒引导; 一、校准方法:让模型概率更贴近真实正确…

ICT测试原理之--什么是假短

ICT测试原理之–什么是假短 文章目录ICT测试原理之--什么是假短一、假短的由来防止假短二、无法检测的短路示例解决无法检测的短路调试短路文件调试意外断路调试意外短路三、调试假短报告短路和断路报告假短报告短路设备/引脚功能性短路测试功能性短路测试的语法一、假短的由来…

三种深度学习模型(LSTM、CNN-LSTM、贝叶斯优化的CNN-LSTM/BO-CNN-LSTM)对北半球光伏数据进行时间序列预测

代码功能 该代码实现了一个光伏发电量预测系统&#xff0c;采用三种深度学习模型&#xff08;LSTM、CNN-LSTM、贝叶斯优化的CNN-LSTM&#xff09;对北半球光伏数据进行时间序列预测&#xff0c;并通过多维度评估指标和可视化对比模型性能。 算法步骤 1. 数据预处理 数据导入&am…

Typecho+阿里云CDN完整配置:防止DDoS攻击与IP暴露

文章目录 Typecho使用阿里云CDN保护网站真实IP地址的完整指南 背景与问题分析 技术选型与方案设计 详细实施步骤 第一步:阿里云CDN基础配置 第二步:DNS解析设置 第三步:源站服务器防护配置 Nginx服务器配置 防火墙配置(以Ubuntu为例) 第四步:Typecho配置调整 高级防护措施…

[硬件]运算放大器对相位噪声的影响与设计提示

运算放大器对相位噪声的影响与设计提示 文章目录运算放大器对相位噪声的影响与设计提示运放影响位噪声的主要因素如何最小化运放对相位噪声的影响总结运算放大器是常用的模拟电路元器件&#xff0c;通常用于放大信号&#xff0c;增强驱动。但是当使用运放放大一个信号时&#x…

github jekyll+chirpy主题搭建博客

github jekyllchirpy主题搭建博客 标签&#xff1a;后端、blog、jekyll 全文链接 本文简要介绍了如何基于 GitHub Pages、Jekyll 及 Chirpy 主题搭建个人博客的流程和注意事项。 主要内容 GitHub Pages 站点简介 可免费搭建个人博客&#xff0c;支持自定义域名&#xff0c;适…

Flutter状态管理篇之ValueNotifier(三)

目录 前言 一、ValueNotifier 概述 二、ValueNotifier 的实现原理 1.类定义 1.类定义 2.关键字段 3.关键方法 1.构造函数 2.getter:value 3.setter:value: 4.toString 2.继承自ChangeNotifier的机制 3.ValueListenable 接口 三、ValueNotifier 的用法 1.基本用法…

Ubuntu togo 系统安装指南

制作一个 “Ubuntu To Go” 系统&#xff08;也就是一个可以随身携带、在不同电脑上启动并拥有持久化存储的U盘系统&#xff09;是解决你问题的完美方案。 这样一来&#xff0c;你就可以&#xff1a; 不改动你现有的电脑系统 (保留你的Ubuntu 20.04 或 Windows)。拥有一个完整…

Python爬虫实战:研究pefile库相关技术

一、引言 可执行文件(Portable Executable,PE)是 Windows 操作系统中最常见的文件格式,包括.exe、.dll、.sys 等多种类型。对 PE 文件的分析在软件逆向工程、恶意软件检测、系统安全研究等领域具有重要意义。传统的 PE 文件分析主要依赖手动操作和专业工具,效率较低且对分…

盟接之桥说制造:差异化定位与效率竞争的双轮驱动

在当今竞争日益激烈的商业环境中&#xff0c;企业如何在市场中脱颖而出&#xff0c;既避免陷入同质化的价格战&#xff0c;又能够通过效率提升实现可持续发展&#xff0c;是每一个经营者必须思考的问题。本文将围绕“差异化”与“效率竞争”两大核心战略展开分析&#xff0c;探…

Vue基础(前端教程①-路由)

项目结构src/├── router/│ └── index.js # 路由配置├── components/│ ├── Home.vue # 首页组件│ ├── About.vue # 关于页组件│ └── Contact.vue # 联系页组件├── App.vue # 根组件&#xff08;含导航栏&…

驾驭 Spring Boot 事件机制:8 个内置事件 + 自定义扩展实战

驾驭 Spring Boot 事件机制&#xff1a;8 个内置事件 自定义扩展实战在 Spring Boot 应用的完整生命周期中&#xff0c;框架为我们预埋了 8 个关键事件&#xff08;Application-level & Context-level&#xff09;。 理解并善用这些事件&#xff0c;可以在“不侵入框架、…

【kafka4源码学习系列】kafka4总体架构介绍

二 kafka架构介绍学习一个系统之前很重要的一点就是先了解这个系统整体的架构&#xff0c;这能够使我们对整个系统有个总体的认识&#xff0c;清楚地知道这个系统有什么能力。这不仅帮助我们学习时快速定位到我们想要的内容&#xff0c;还能避免我们学习过程中在庞大的系统中迷…

java内存图

java内存图java文件运行流程程序的内存空间认识虚拟机栈程序的执行流程认识堆java的类与对象的关系java文件运行流程 有这样的一份 java 文件 在该目录下的终端运行 javac Hello.java 命令&#xff0c;会生成 Hello.class 文件&#xff0c;内容如下&#xff1a; Hello.java 打…