转自:https://www.jianshu.com/p/ef1e93e9d65b
在 嵌入式Linux 开发中,往往会听到 MMU 这个词,但大多数情况下并不会去了解它,因为操作系统已经做好了关于 MMU 的一切操作,我们只需要在操作系统的框架下直接使用即可。但了解 MMU 有助于帮助我们理解操作系统,理解进程等,让我们对 嵌入式Linux 的理解上升一个层次。本文将简单地讲述一下关于 MMU 的基本信息。
注意:本文将按照ARMv7的二级页表映射进行讲述
MMU 全称为 Memory Management Unit,即 内存管理单元。在 带有MMU的嵌入式Linux 中,CPU 访问的地址都是 虚拟地址,而 MMU 负责将程序中 代码或数据 的 虚拟地址 翻译为 物理地址,以便程序访问内存。
在执行操作时,MMU 会自动转换 CPU发出的虚拟地址,无法人工进行操作,只需要配置好 MMU 相关属性即可。
虚拟地址 是在 编译和链接 时定义的,可以简单地理解为 由链接器和链接器脚本 指定虚拟地址。
除了 翻译虚拟地址,MMU 还可以配置 内存区域 的各项配置,如内存区域的访问权限,内存区域是否使能cache等功能。
总结 MMU 的功能,如下:
看到 MMU 的相关文章时,总会提及几个概念如 页,页框(页帧),页表,页表项,TLB等等,下面我们逐个拆分来讲述。
MMU 管理 虚拟地址空间 时,是按照 页 为单位来进行管理。在 ARMv7 的 MMU ,页大小 一共有 16M(Super Section)、1M(Section) 、64K(Large Page) 4K(Page)。页大小 可以通过 协处理器CP15 进行配置,越小的页意味着内存的颗粒度越小,内存使用时的浪费会越小,但也意味着使用的TLB行越多。越大的也内存的颗粒度月大,内存的使用浪费也可能月大,但使用的TLB行越少。比如只需要申请 7K 大小的 物理内存,如果使用 7K大小 的内存,我们可以分配 2 个 4K页,如果分配 64K的大页,则浪费的空间就比较大。
因为 虚拟地址空间 需要有所对应的 物理地址,这样才能在 虚拟地址 中存储数据。所以 MMU 管理 物理地址空间 时,按照 页帧 为单位进行管理。其大小分为 64K 和 4K。一段 虚拟地址空间 有可能存在着多个 页,这些 页 对应着多个 页帧。
按照笔者理解,页 和 页帧 是 不同地址空间下的关于内存空间大小的概念。
MMU 在进行 地址转换 时,需要一些信息,存放这些信息的就是 页表。每个 页表 的最小单位就是 页表项。
页表 存储在 物理地址空间 中,且一个 页表项 对应着一个 页。
在 切换页表 时,通过将 页表的物理首地址 设置到 协处理器CP15 中的 TTBR寄存器(Translation Table Base Register)。此后 MMU 会通过该地址自动去 物理地址空间 中找到对应的 页表,从而完成 虚拟地址到物理地址的映射。
在不考虑 TLB 和 多级页表 的情况下,可以简单地如下图所示:
页表及页表项
TLB 全程为 Translation Lookaside Buffer,即 旁路转换缓冲。它是 MMU 的专属 全相联cache,用于临时存放 虚拟地址到物理地址映射 所需要的信息。
下面按照步骤说明 TLB 的作用:
值得注意的是:ARM架构的TLB只存储有效的页表项,对于无效的页表项TLB并不会存储
TLB 由许多 TLB行 组成,如下图所示:
TLB
一般情况下,切换 进程 时会切换 页表,因为随着进程的切换, 虚拟地址 到 物理地址 的映射已经改变。此时需要 清理TLB(即无效化TLB中的数据) 来保持 TLB一致性。清理TLB 一般通过 协处理器CP15 来完成,在 Linux内核 中,有 flush_tlb_all() 和 flush_tlb_range() 函数来完成该工作。
如下图所示:
MMU组成
MMU 的工作流程可以总结为下面 2 种情况:
ARMv7 下的 MMU 具有 2级页表,分为 1级页表 和 2级页表。
1级页表 也称 主页表 和 段页表,下面简称 L1页表。它将 4GB 的地址空间划分为 4096 个 1MB 大小的 段,每个段的地址为 32bit。所以 1级页表 拥有 4096 个 32bit 的 页表项。
L1页表 使用了 短描述符页表(Short-descriptor translation table),其 页表项 具有以下特征:
在前面说了 TTBR寄存器 是存放 页表物理地址 的寄存器,需要注意的是:存放在TTBR寄存器的地址需要16KB对齐
一级页表项 一共有 4种 格式,如下图所示:
一级页表项格式
每种格式都由 物理地址部分+属性部分 组成,可以直接在图中看出 物理地址部分 的示意,这里不多赘述。各种格式的含义如下:
下面简单说下各个字段的含义:
以 1MB段 举例,假设 L1页表 的物理地址为 0x12300000,现在有一个虚拟地址 0x00100000。其转换过程如图所示:
查表过程
转换过程
值得注意的是:例子中,高12位一共是4096个页表项,那么4096x4一共是16384字节的大小,因为每个页表项是32位。所以4096个页表项需要16K大小的内存来存储页表。也是因为如此,每个虚拟地址的高12bit都需要乘以4.
下图为例子的完整转换过程,其余类型的 页表项 转换过程类似、
L1转换完整过程
2级页表 一共有 256 个 4字节大小 的 页表项,总共占据 1KB大小 的内存空间。L2页表 的大部分内容与 L1页表 类似,相同部分下文将不再赘述
二级页表项 一共有 3种 格式,如下图所示:
二级页表项
每种格式与 L1页表项 一样由 物理地址部分+属性部分 组成,可以直接在图中看出 物理地址部分 的示意,格式如下:
2级页表项 具有以下特征:
属性字段 的含义请参考 1级页表 章节。
L2页表 的转换过程与 L1页表 的转换过程一脉相承。以 4KB 为例子,如下图所示:
image.png
由上图可以看出其转换步骤如下:
结合 L1页表 的完整转化过程如下图所示:
image.png
每个 内存区域 都有自己的权限,不符合访问权限的 内存访问 都会引发 异常。如果是 数据访问 则引发 数据异常。如果是 指令访问,且该指令在执行前没有被 flush,将引发 预取指异常。
引发的 异常原因 将会被设置在 CP15 的 the fault address and fault status registers
内存区域权限 由 AP、APX 和 Domain(域) 共同控制,如下:
访问权限组合表
需要注意的是:内存区域控制以域控制为主,页表项的AP/APX字段为次。ARMv7不建议使用域进行控制,所以建议把DACR寄存器设置为用户模式
ARM架构 实现了 3种内存类型,每种类型都是 互斥的,如下:
每种 类型 的细节如下图所示:
image.png
需要注意的是,Device类型的Shareable内存区域现在已经被弃用
内存区域类型 可以通过 TEX字段、C字段 和 B字段 来进行设置,如下图所示
image.png
值得注意的是:按照笔者理解,inner cache是L1 cache,而outer cache是指在L1cache下面的cache,比如L2cache
操作系统 会为 每个进程 分配一个 页表,该 页表 使用 物理地址 存储。当 进程 使用类似 malloc 等需要 映射代码或数据 的操作时,操作系统 会在随后马上 修改页表 以加入新的 物理内存。当进程完成退出时,内核会将相关的页表项删除掉,以便分配给新的进程。
在操作系统中, 多进程 是一种常态。那么多进程 的情况下,每次 切换进程 都需要进行 TLB清理。这样会导致切换的效率变低。
为了解决问题,TLB 引入了 ASID(Address Space ID) 。ASID 的范围是 0-255。
ASID 由操作系统分配,当前进程的ASID值 被写在 ASID寄存器(使用CP15 c3访问)。TLB 在更新 页表项 时也会将 ASID 写入 TLB。
如果设置了如果 当前进程的ASID,那么 MMU 在查找 TLB 时, 只会查找 TLB 中具有 相同ASID值 的 TLB行。且在切换进程是,TLB 中被设置了 ASID 的 TLB行 不会被清理掉,当下次切换回来的时候还在。所以ASID 的出现使得切换进程时不需要清理 TLB 中的所有数据,可以大大减少 切换开销。
具体可以看参考链接《多核MMU和ASID管理逻辑》
前面讲了 TTBR寄存器 是用于存放 页表基地址,在 ARmv7 中一共有 2个 这样的寄存器,分别是 TTBR0 和 TTBR1。
那么这里提出一个问题:在进行 Translation Table walking 的时候,选择哪个TTBR寄存器,又如何选择?
在 ARMv7 中,有一个寄存器为 TTBCR(TTB Control Register),即TTB控制寄存器。TTBCR寄存器 可以被设置为 0-7 这几个值。
在进行 地址映射 时, MMU 会根据 TTBCR寄存器 中的值查看 虚拟地址 是高位地址,根据 高位地址 选择对应的 TTBR寄存器。
举个例子,假设 TTBCR寄存器 被设置为 4,则 MMU 会检查 虚拟地址 的 高4bit,如果 高4bit 都为 0,则此时选择 TTBR0。
需要注意的是:TTBCR被设置为 0 时,默认选择 TTBR0。
下面我们看看使用和不使用 TTBR1 带来的影响。
本小节简单地讲述一下 Linux 进行 MMU切换 时的代码片段。以 ARMv7单核CPU 为例子。
根据笔者的理解,其调用图谱如下:
->switch_mm ->check_and_switch_context ->cpu_switch_mm(processor.switch_mm) ->cpu_v7_switch_mm
笔者会将简单的说明注释在代码中,不进行另外的说明。
/* arch/arm/include/asm/mmu_context.h */ static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk) { #ifdef CONFIG_MMU unsigned int cpu = smp_processor_id(); /* * __sync_icache_dcache doesn't broadcast the I-cache invalidation, * so check for possible thread migration and invalidate the I-cache * if we're new to this CPU. */ /* 这里应该是说进程如果调度到新的CPU,则需要将该CPU的cache给清理掉 */ if (cache_ops_need_broadcast() && !cpumask_empty(mm_cpumask(next)) && !cpumask_test_cpu(cpu, mm_cpumask(next))) __flush_icache_all(); if (!cpumask_test_and_set_cpu(cpu, mm_cpumask(next)) || prev != next) { /* 如果调度的进程不是本进程,则执行check_and_switch_context */ check_and_switch_context(next, tsk); if (cache_is_vivt()) cpumask_clear_cpu(cpu, mm_cpumask(prev)); } #endif } static inline void check_and_switch_context(struct mm_struct *mm, struct task_struct *tsk) { if (unlikely(mm->context.vmalloc_seq != init_mm.context.vmalloc_seq)) __check_vmalloc_seq(mm); if (irqs_disabled()) /* * cpu_switch_mm() needs to flush the VIVT caches. To avoid * high interrupt latencies, defer the call and continue * running with the old mm. Since we only support UP systems * on non-ASID CPUs, the old mm will remain valid until the * finish_arch_post_lock_switch() call. */ mm->context.switch_pending = 1; else /* 使用该函数进行MMU切换页表 */ cpu_switch_mm(mm->pgd, mm); } /* arch/arm/include/asm/proc-fns.h */ /* 根据笔者找的代码,cpu_switch_mm 应该直接调用了processor.switch_mm */ #define cpu_do_switch_mm processor.switch_mm #define cpu_switch_mm(pgd,mm) cpu_do_switch_mm(virt_to_phys(pgd),mm)
processor.switch_mm 是一个 回调函数,根据笔者找到的资料,应该是指向 ** arch/arm/mm** 目录下的一些列 MMU 操作代码。这里以 proc-v7-2level.S(即ARMv7 2级页表) 进行说明
/* arch/arm/mm/proc-v7-2level.S */ /* 根据APCS,传入的参数是存放在寄存器 r0和r1 */ ENTRY(cpu_v7_switch_mm) #ifdef CONFIG_MMU mmid r1, r1 @ get mm->context.id ALT_SMP(orr r0, r0, #TTB_FLAGS_SMP) ALT_UP(orr r0, r0, #TTB_FLAGS_UP) #ifdef CONFIG_PID_IN_CONTEXTIDR mrc p15, 0, r2, c13, c0, 1 @ read current context ID lsr r2, r2, #8 @ extract the PID bfi r1, r2, #8, #24 @ insert into new context ID #endif #ifdef CONFIG_ARM_ERRATA_754322 dsb #endif mcr p15, 0, r1, c13, c0, 1 @ set context ID isb /* 在这里,将r0所指向的页表基地址设置到TTBR0中,完成页表的切换 */ mcr p15, 0, r0, c2, c0, 0 @ set TTB 0 isb #endif bx lr ENDPROC(cpu_v7_switch_mm)
《ARM Cortex-A Series Programmer’s Guide》
《Cortex-A7 MPCore Technical Reference Manual》
《多核MMU和ASID管理逻辑》
TLB的作用及工作过程
MMU和cache详解(TLB机制)
inux-kernel – Linux内核ARM转换表库(TTB0和TTB1)
ARM TTBR0,TTBR1寄存器与ARM32页表复制
选择使用TTBR0或TTBR1做为translation table base地址寄存器
TLB中ASID和nG bit的关系
ASID
Linux arm 进程切换
ARM-LINUX的进程切换