hnu操作系统-lab8
HNU OSLab 8
实验目的
理解和实现操作系统中的内存管理机制
理解分页原理:学习操作系统中如何使用分页技术来管理内存,包括虚拟地址到物理地址的转换过程。
页表的创建和管理:编写代码来创建和管理页表,这是实现分页内存管理的核心部分。
内存映射配置:通过定义内存映射区域(mmu_mmap_region_s结构体数组),设置不同
区域的内存属性,如缓存共享、设备属性等。)MMU 寄存器配置:学习并配置 MMU 的相关寄存器,如 TCR(Translation Control Register)、
TTBR (Translation Table Base Register)、MAIR(Memory Attribute Indirection Register)等。
缓存和 TLB 的刷新:理解并实现在内存映射更新后刷新数据缓存、指令缓存和 TLB 的过
程。
调试和测试:通过调试确保 MMU 正确启动并运行,测试不同的内存访问以验证分页机制的正确性。
前序知识
Armv8的地址转换
TTBR0指向整个虚拟空间下半部分通常用于应用程序的空间,TTBR1指向虚拟空间的上半部分通常用于内核的空间。其中TTBR0除了在EL1中存在外,也在EL2 and EL3中存在,但TTBR1只在EL1中存在。
TTBR0_ELn 和 TTBR1_ELn 是页表基地址寄存器,地址转换的过程如下所示。
单级页表地址转换机制说明
(基于64KB粒度 & 42位虚拟地址)
1. 页表基址选择
- TTBR1选择条件:当虚拟地址高位
VA[63:42]
全为1时,使用寄存器TTBR1
作为一级页表基址。 - TTBR0选择条件:当
VA[63:42]
全为0时,使用寄存器TTBR0
作为一级页表基址。
2. 页表结构与索引
- 页表规模:一级页表包含8,192个64位页表项(PTE)。
- 索引方式:通过虚拟地址的
VA[41:29]
(13位)索引一级页表项。
3. 页表项检查
MMU会验证目标页表项的以下属性:
- 有效性(Valid)
- 访问权限(Memory Access Permission)
若检查通过,则继续转换流程。
4. 大页映射(512MB块描述符)
- 物理地址生成:
- 高19位:取自页表项的
[47:29]
→ 对应PA[47:29]
。 - 低29位:直接使用虚拟地址的
VA[28:0]
→ 对应PA[28:0]
。
- 高19位:取自页表项的
- 输出结果:最终生成48位物理地址
PA[47:0]
,并附带页表项中的附加属性(如内存属性、权限等)。
5. 单级转换的局限性
- 地址空间划分粗糙:仅支持512MB大页,无法实现细粒度(如4KB页)的内存管理。
- 扩展方案:实际系统中通常采用多级页表(如二级页表),通过一级页表项指向二级页表,从而支持更灵活的地址空间划分。
MMU管理实现
新建 src/bsp/mmu.c 文件
头文件与全局变量
#include "prt_typedef.h" // 基础类型定义(如U64/U32)
#include "prt_module.h" // 模块相关定义
#include "prt_errno.h" // 错误码定义
#include "mmu.h" // MMU相关宏和结构体
#include "prt_task.h" // 任务管理相关
extern U64 g_mmu_page_begin; // 页表内存起始地址(链接脚本定义) extern U64 g_mmu_page_end; // 页表内存结束地址
内存映射配置
static mmu_mmap_region_s g_mem_map_info[] = {
{
.virt = 0x0, // 虚拟地址起点
.phys = 0x0, // 物理地址起点
.size = 0x40000000, // 映射大小1GB
.max_level = 0x2, // 最大页表级别(0-3)
.attrs = MMU_ATTR_DEVICE_NGNRNE | MMU_ACCESS_RWX, // 设备内存属性(非缓存、可读写执行)
}, {
.virt = 0x40000000, // 第二段映射:虚拟地址0x40000000
.phys = 0x40000000, // 映射到相同物理地址
.size = 0x40000000, // 1GB大小
.max_level = 0x2,
.attrs = MMU_ATTR_CACHE_SHARE | MMU_ACCESS_RWX, // 缓存内存属性(可读写执行)
}
};
- 作用:定义两段内存映射区域:
- 设备内存:0x0-0x40000000,非缓存。
- 普通内存:0x40000000-0x80000000,带缓存。
- 这两段内存映射区域的存在,是为了将设备内存和普通内存分离,对他们设置不同的属性,确保了内存访问的正确性和系统的性能优化。
MMU控制结构体
`static mmu_ctrl_s g_mmu_ctrl = { 0 }; // 全局MMU控制状态 结构体在mmu.h中定义
TCR寄存器配置
根据内存映射配置(g_mem_map_info
)和页表粒度(4K/64K),计算并返回 TCR 寄存器值,同时可选地返回物理地址位数(ips
)和虚拟地址位数(va_bits
)。TRC寄存器用于控制内存映射和地址转换的参数。static U64 mmu_get_tcr(U32 *pips, U32 *pva_bits)
{
==//计算最大虚拟地址==
U64 max_addr = 0;
U64 ips, va_bits;
U64 tcr;
U32 i;
U32 mmu_table_num = sizeof(g_mem_map_info) / sizeof(mmu_mmap_region_s);
// 根据g_mem_map_info表计算所使用的虚拟地址的最大值
for (i = 0; i < mmu_table_num; ++i) {
max_addr = MAX(max_addr, g_mem_map_info[i].virt + g_mem_map_info[i].size);
}
[!NOTE]
遍历g_mem_map_info
数组,找到所有内存映射区域中最大的虚拟地址(virt + size
)。若g_mem_map_info
定义了两段 1GB 的映射(0x0 和 0x40000000),则max_addr = 0x80000000
。
==// 依据虚拟地址最大值(max_addr)计算虚拟地址所需的位数,==
// 实际上应该分别计算物理地址的ips和虚拟地址的va_bits,而不是如下同时进行。
//如果最大虚拟地址大于44位,需要5级物理地址和48位虚拟地址
if (max_addr > (1ULL << MMU_BITS_44)) {
ips = MMU_PHY_ADDR_LEVEL_5;
//5级物理地址
va_bits = MMU_BITS_48;
//48位虚拟地址
} else if (max_addr > (1ULL << MMU_BITS_42)) {
ips = MMU_PHY_ADDR_LEVEL_4;
//
va_bits = MMU_BITS_44;
} else if (max_addr > (1ULL << MMU_BITS_40)) {
ips = MMU_PHY_ADDR_LEVEL_3;
va_bits = MMU_BITS_42;
} else if (max_addr > (1ULL << MMU_BITS_36)) {
ips = MMU_PHY_ADDR_LEVEL_2;
va_bits = MMU_BITS_40;
} else if (max_addr > (1ULL << MMU_BITS_32)) {
ips = MMU_PHY_ADDR_LEVEL_1;
va_bits = MMU_BITS_36;
} else {
ips = MMU_PHY_ADDR_LEVEL_0;
va_bits = MMU_BITS_32;
}
==//构建Translation Control Register寄存器的值,tcr可控制TTBR0_EL1和TTBR1_EL1的影响==
tcr = TCR_EL1_RSVD | TCR_IPS(ips);
//初始化tcr 保留位+物理地址位数
==//页表粒度配置==
if (g_mmu_ctrl.granule == MMU_GRANULE_4K) {
tcr |= TCR_TG0_4K | TCR_SHARED_INNER | TCR_ORGN_WBWA | TCR_IRGN_WBWA;
//页表粒度 共享内存属性,读写设置(写回,写分配)
} else {
tcr |= TCR_TG0_64K | TCR_SHARED_INNER | TCR_ORGN_WBWA | TCR_IRGN_WBWA;
}
[!NOTE]
-
TCR_TG0_4K
/TCR_TG0_64K
:设置页表粒度(4KB 或 64KB)。-
TCR_SHARED_INNER
:共享内存属性(Inner Shareable)。-
TCR_ORGN_WBWA
/TCR_IRGN_WBWA
:
- ORGN(Outer Cacheability):Write-Back, Write-Allocate。
- IRGN(Inner Cacheability):Write-Back, Write-Allocate。
// 地址空间范围配置
`tcr |= TCR_T0SZ(va_bits); // Memory region 2^(64-T0SZ)
[!NOTE]
TCR_T0SZ(va_bits)
:
- 计算方式:
T0SZ = 64 - va_bits
。- 作用:定义 TTBR0_EL1 管理的地址空间范围为
2^(64 - T0SZ)
。
// 选择性的返回输出参数ips/va_bits
if (pips != NULL) {
*pips = ips;
}
if (pva_bits != NULL) {
*pva_bits = va_bits;
}
return tcr;
}
获取页表类型
static U32 mmu_get_pte_type(U64 const *pte)
{
return (U32)(*pte & PTE_TYPE_MASK);//提取页表类型
}
根据页表项级别计算当个页表项表示的范围(位数)
static U32 mmu_level2shift(U32 level)
{
//计算某页表项覆盖的地址范围位数(4K/64K粒度不同)
if (g_mmu_ctrl.granule == MMU_GRANULE_4K) {
return (U32)(MMU_BITS_12 + MMU_BITS_9 * (MMU_LEVEL_3 - level));
} else {
return (U32)(MMU_BITS_16 + MMU_BITS_13 * (MMU_LEVEL_3 - level));
}
}
根据虚拟地址找到对于级别的页表项
根据虚拟地址 addr
和页表级别 level
,从顶级页表开始逐级向下查找,返回目标级别的页表项指针。若中间遇到无效项或块描述符(Block Descriptor),则提前终止并返回 NULL
。
static U64 *mmu_find_pte(U64 addr, U32 level)
{
U64 *pte = NULL;
U64 idx;
U32 i;
if (level < g_mmu_ctrl.start_level) {
return NULL;
}
pte = (U64 *)g_mmu_ctrl.tlb_addr;//初始化为顶级页表的指针
// 从顶级页表开始,直到找到所需level级别的页表项或返回NULL
for (i = g_mmu_ctrl.start_level; i < MMU_LEVEL_MAX; ++i) {
// 依据级别i计算页表项在页表中的索引idx
if (g_mmu_ctrl.granule == MMU_GRANULE_4K) {
idx = (addr >> mmu_level2shift(i)) & 0x1FF;//使用9位索引
} else {
idx = (addr >> mmu_level2shift(i)) & 0x1FFF;//使用13位索引
}
// 找到对应的页表项
pte += idx;
// 如果是需要level级别的页表项则返回
if (i == level) {
return pte;
}
// 从顶级页表开始找,
// 找到当前级别页表项不是有效的(无效或是block entry)直接返回NULL
if (mmu_get_pte_type(pte) != PTE_TYPE_TABLE) {
return NULL;
}
// 不是所需级别但pte指向有效,依据页表粒度准备访问下级页表
if (g_mmu_ctrl.granule == MMU_GRANULE_4K) {
pte = (U64 *)(*pte & PTE_TABLE_ADDR_MARK_4K);
} else {
pte = (U64 *)(*pte & PTE_TABLE_ADDR_MARK_64K);
}
}
return NULL;
}
根据页表粒度在页表区域新建一个页表,返回页表起始位置
static U64 *mmu_create_table(void)
{
U32 pt_len;
U64 *new_table = (U64 *)g_mmu_ctrl.tlb_fillptr;
//确定页表长度
if (g_mmu_ctrl.granule == MMU_GRANULE_4K) {
pt_len = MAX_PTE_ENTRIES_4K * sizeof(U64);
} else {
pt_len = MAX_PTE_ENTRIES_64K * sizeof(U64);
}
// 根据页表粒度在页表区域新建一个页表(4K或64K)
g_mmu_ctrl.tlb_fillptr += pt_len;
if (g_mmu_ctrl.tlb_fillptr - g_mmu_ctrl.tlb_addr > g_mmu_ctrl.tlb_size) {
return NULL;
}
// 初始化页表全为0,因此该页表所有的页表项初始都是无效页表项PTE_TYPE_FAULT
// (void)memset_s((void *)new_table, MAX_PTE_ENTRIES_64K * sizeof(U64), 0, pt_len);
U64 *tmp = new_table;
for(int i = 0; i < pt_len; i+=sizeof(U64)){
*tmp = 0;
tmp++;
}
return new_table;
}
将一个页表项指向一个新的页表
1 |
|
- 作用:设置页表项为中间表描述符,指向下级页表。
- 关键点:
PTE_TYPE_TABLE
:标记该描述符类型为页表(非块或页)。(U64)table
:下级页表的物理地址(低48位有效,需对齐)。
依据mmu_mmap_region_s填充pte
1 |
|
- 逻辑分支:
- 上级页表项(
level < max_level
):创建下级页表并链接。 - 最后一级(L3):设置为页描述符(4KB/64KB页)。
- 中间级别:设置为块描述符(如1GB/2MB大页)。
- 上级页表项(
- 参数:
map
:内存映射配置(属性、最大级别等)。phys
:当前级别的物理地址基址。
依据 mmu_mmap_region_s 的定义,生成 mmu 映射
1 |
|
- 作用:按
map
配置逐级构建页表映射。 - 关键流程:
- 从
start_level
开始,逐级查找或创建页表项。
2. 如果为上级页表项且pte指向无效,新建下级页表且pte指向该新建的页表
3. 如果为最低页表项或到达设定级别页表项,直接设置页表项的值 - 更新地址和映射大小
- 从
mmu寄存器配置
通过内联汇编直接操作 ARMv8-A 的系统寄存器,完成以下操作:
- 设置页表基址寄存器(TTBR0_EL1)。
- 配置地址转换控制寄存器(TCR_EL1)。
- 定义内存属性寄存器(MAIR_EL1)。
- 插入内存屏障指令,确保配置顺序性和一致性。
1 |
|
MMU初始化核心函数
static U32 mmu_setup_pgtables(mmu_mmap_region_s *mem_map, U32 mem_region_num, U64 tlb_addr, U64 tlb_len, U32 granule)
{
U32 i;
U32 ret;
U64 tcr;
U64 *new_table = NULL;
//初始化页表控制结构
g_mmu_ctrl.tlb_addr = tlb_addr; // 页表内存池起始地址
g_mmu_ctrl.tlb_size = tlb_len; // 页表内存池大小
g_mmu_ctrl.tlb_fillptr = tlb_addr; // 当前页表分配指针(初始指向起始地址)
g_mmu_ctrl.granule = granule; // 设置页表粒度(4K/64K)
g_mmu_ctrl.start_level = 0; // 临时初始化起始级别(后续计算)
//获取tcr寄存器和虚拟地址位数
tcr = mmu_get_tcr(NULL, &g_mmu_ctrl.va_bits);
// 依据页表粒度和虚拟地址位数计算地址转换起始级别
if (g_mmu_ctrl.granule == MMU_GRANULE_4K) {
if (g_mmu_ctrl.va_bits < MMU_BITS_39) {
g_mmu_ctrl.start_level = MMU_LEVEL_1;
} else {
g_mmu_ctrl.start_level = MMU_LEVEL_0;
}
} else {
if (g_mmu_ctrl.va_bits <= MMU_BITS_36) {
g_mmu_ctrl.start_level = MMU_LEVEL_2;
} else {
g_mmu_ctrl.start_level = MMU_LEVEL_1;
return 3;
}
}
// 创建一个顶级页表,不一定是L0
new_table = mmu_create_table();
if (new_table == NULL) {
return 1;//若内存池耗尽,则返回1
}
//构建内存映射
for (i = 0; i < mem_region_num; ++i) {
//根据mem_map的配置逐级填充页表项
ret = mmu_add_map(&mem_map[i]);
if (ret) {
return ret;
}
}
//配置mmu寄存器:ttbr0_el1,tcr_el1,mair_el1
mmu_set_ttbr_tcr_mair(g_mmu_ctrl.tlb_addr, tcr, MEMORY_ATTRIBUTES);
return 0;
}
[!流程图]
开始
│
├─ 初始化页表内存池参数
│
├─ 计算TCR和VA位数
│
├─ 确定起始级别(L0/L1/L2)
│ ├─ 4K粒度:VA<39 → L1,否则L0
│ └─ 64K粒度:VA≤36 → L2,否则报错
│
├─ 创建顶级页表
│
├─ 遍历mem_map构建映射
│ ├─ 逐级填充页表项
│ └─ 失败则终止
│
├─ 配置TTBR0/TCR/MAIR
│
└─ 返回成功(0)或错误码(1/2/3)
mmu初始化的封装函数
1 |
|
mmu初始化的调用接口
1 |
|
新建 src/bsp/mmu.h, 该文件可从 这里 下载
关键部分解析
mair 寄存器编码
1 |
|
预定义 5 种内存类型(如 MT_DEVICE_NGNRNE
非缓存设备内存,MT_NORMAL
缓存内存)
页表项属性标记
- 权限控制:
PTE_BLOCK_AP_R/RW
(读/读写)、PTE_BLOCK_PXN/UXN
(执行权限)。 - 缓存策略:
PTE_BLOCK_MEMTYPE(MT_NORMAL)
配合PTE_BLOCK_INNER_SHARE
定义缓存共享域。
页表描述符格式
类型标识
1 |
|
地址对齐掩码
1 |
|
地址转换控制(tcr寄存器)
1 |
|
多级页表管理
级别定义
1 |
|
内存映射配置
1 |
|
mmu控制状态
1 |
|
内存映射区域控制
1 |
|
新建 src/bsp/cache_asm.S, 该文件可从 这里 下载
用于处理缓存和tlb(translation lookaside buffer)的操作
1.os_asm_dcache_level:这是一个内部函数,用于清空指定级别的数据缓存。它通过设置 CSS
(Cache Size Selection)寄存器选择指定的缓存级别,并循环遍历每个缓存行,逐行清空缓存。
2.os_asm_dcache_all:这个函数用于清空所有数据缓存。它会循环遍历系统中的每个缓存级别,并调用 os_asm_dcache_level 函数来清空每个级别的缓存。
3.os_asm_invalidate_dcache_all:这个函数用于使所有数据缓存无效。它通过将os_asm_dcache_all 函数的参数设置为0x1,实现清空所有数据缓存。
4.os_asm_flush_dcache_all:这个函数用于刷新所有数据缓存。与清空缓存不同,刷新缓存会将缓存中的数据写回到主存。它通过将 os_asm_dcache_all函数的参数设置为 Ox0,实现刷新所有数据缓存。
5.os_asm_clean_dcache_all:这个函数用于清洁所有数据缓存。清洁缓存会将缓存中的脏数据写回主存,并将缓存行置为无效状态。它通过将os_asm_dcache_all 函数的参数设置为0x2,实现清洁所有数据缓存。
6.os_asm_invalidate_icache_all:这个函数用于使所有指令缓存无效。它通过调用 ic ialluis 汇编指令来实现,该指令用于使指令缓存失效。
7.os_asm_invalidate_tlb_all:这个函数用于使 TLB(Translation Lookaside Buffer)无效。它通过调用 tlbi vmalle1 汇编指令来实现,该指令用于使 TLB 中的所有条目无效。
启用mmu
start.S 中在 B OsEnterMain 之前启用 MMU
将新建文件加入构建系统
调试确保已经启动mmu
在start.S里打上合适的断点,可以看到,逐步调试到mmu_init后
跳转到mmu.c中,对mmu进行一系列的初始化操作
我们可以看到有关寄存器成功被初始化
mmu成功启动^-^
作业
- 在printf.c中修改宏定义
- 在hwi.init.C中修改宏定义
- 启动ttbr1,确保mmu寄存器被正确初始化
- 选择页表颗粒度4kb,则高位的f不用更改
- 根据宏定义修改映射信息
- 修改get_tcr函数并在mmu.h中 添加相关宏定义
- 修改mmu_setup_pgtables 为ttbr0 ttbr1创建独立页表
- 修改mmu_set_ttbr_tcr_mair
- 程序运行成功