MMU与Cache

CPU通过地址来访问内存中的单元,地址有虚拟地址物理地址之分,如果CPU没有MMU(Memory Management Unit,内存管理单元),或者有MMU但没有启用,CPU核在取指令或访问内存时发出的地址将直接传到CPU芯片的外部地址引脚上,直接被内存芯片(以下称为物理内存,以便与虚拟内存区分)接收,这称为物理地址(Physical Address,以下简称PA)

无MMU时,CPU直接通过物理地址访问

Memory Management Unit,内存管理单元,主要负责将虚拟地址(Virtual Address,以下简称VA)转换为物理地址。

CPU发出的虚拟地址到MMU,而MMU将这个地址翻译成物理地址发到CPU芯片的外部地址引脚(内存芯片地址引脚)上,进行内存的访问。

MMU转换

MMU将虚拟地址映射到物理地址是以(Page)为单位的,对于32位CPU通常一页为4K。例如,虚拟地址0xb700 1000~0xb700 1fff是一个页,可能被MMU映射到物理地址0x2000~0x2fff,物理内存中的一个物理页面也称为一个页帧(Page Frame)。

虚拟内存管理

现代操作系统充分利用MMU提供的VA到PA的映射机制来做内存管理,以下称为虚拟内存管理(Virtual Memory Management)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
↪ =>$ps
PID TTY TIME CMD
12795 pts/0 00:00:00 bash
12825 pts/0 00:00:00 ps

↪ =>$pmap 12795
12795: bash
00005642d3bea000 180K r---- bash
00005642d3c17000 708K r-x-- bash
00005642d3cc8000 220K r---- bash
00005642d3cff000 16K r---- bash
00005642d3d03000 36K rw--- bash
00005642d3d0c000 40K rw--- [ anon ]
00005642d57c1000 1568K rw--- [ anon ]
00007f308a4be000 8644K r---- locale-archive
00007f308ad2f000 12K rw--- [ anon ]
00007f308ad32000 148K r---- libc-2.31.so
00007f308ad57000 1504K r-x-- libc-2.31.so
00007f308aecf000 296K r---- libc-2.31.so
00007f308af19000 4K ----- libc-2.31.so
00007f308af1a000 12K r---- libc-2.31.so
00007f308af1d000 12K rw--- libc-2.31.so
00007f308af20000 16K rw--- [ anon ]
00007f308af24000 4K r---- libdl-2.31.so
00007f308af25000 8K r-x-- libdl-2.31.so
00007f308af27000 4K r---- libdl-2.31.so
00007f308af28000 4K r---- libdl-2.31.so
00007f308af29000 4K rw--- libdl-2.31.so
...

↪ =>$readelf -S /usr/bin/bash
There are 30 section headers, starting at offset 0x120758:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.propert NOTE 0000000000000338 00000338
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.gnu.build-i NOTE 0000000000000358 00000358
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000037c 0000037c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a0
0000000000004aac 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 0000000000004e50 00004e50
000000000000e418 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000013268 00013268
0000000000009740 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 000000000001c9a8 0001c9a8
0000000000001302 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 000000000001dcb0 0001dcb0
00000000000000d0 0000000000000000 A 7 3 8
[10] .rela.dyn RELA 000000000001dd80 0001dd80
000000000000dc80 0000000000000018 A 6 0 8
[11] .rela.plt RELA 000000000002ba00 0002ba00
0000000000001470 0000000000000018 AI 6 25 8
[12] .init PROGBITS 000000000002d000 0002d000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 000000000002d020 0002d020
0000000000000db0 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 000000000002ddd0 0002ddd0
0000000000000030 0000000000000010 AX 0 0 16
[15] .plt.sec PROGBITS 000000000002de00 0002de00
0000000000000da0 0000000000000010 AX 0 0 16
[16] .text PROGBITS 000000000002eba0 0002eba0
00000000000aeb55 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 00000000000dd6f8 000dd6f8
000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 00000000000de000 000de000
000000000001a094 0000000000000000 A 0 0 32
[19] .eh_frame_hdr PROGBITS 00000000000f8094 000f8094
00000000000044dc 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 00000000000fc570 000fc570
0000000000017c28 0000000000000000 A 0 0 8
[21] .init_array INIT_ARRAY 0000000000115cf0 00114cf0
0000000000000008 0000000000000008 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000115cf8 00114cf8
0000000000000008 0000000000000008 WA 0 0 8
[23] .data.rel.ro PROGBITS 0000000000115d00 00114d00
00000000000028f0 0000000000000000 WA 0 0 32
[24] .dynamic DYNAMIC 00000000001185f0 001175f0
0000000000000210 0000000000000010 WA 7 0 8
[25] .got PROGBITS 0000000000118800 00117800
00000000000007e8 0000000000000008 WA 0 0 8
[26] .data PROGBITS 0000000000119000 00118000
0000000000008604 0000000000000000 WA 0 0 32
[27] .bss NOBITS 0000000000121620 00120604
0000000000009c78 0000000000000000 WA 0 0 32
[28] .gnu_debuglink PROGBITS 0000000000000000 00120604
0000000000000034 0000000000000000 0 0 4
[29] .shstrtab STRTAB 0000000000000000 00120638
000000000000011d 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

在64位CPU上0x0000 0000 0000 0000 ~ 0x0000 ffff ffff ffff地址范围为用户空间地址。
bash进程的虚拟地址空间

  • 0x00005642d3bea000开始的180K,表示init段
  • 0x00005642d3c17000开始的708K,表示text段,因为权限是r-x--
  • 0x00005642d57c1000开始的1563K,表手,因为权限也是rw---,但是没有对应任何磁盘文件,而是用[ anon ](anonymous,匿名)来表示

以上进程中的地址与程序各段的对应关系,可以通过地址长度与段的长度及相应权限确定

arm64_memory

为什么需要虚拟内存管理呢?

  • 让每个进程有独立的地址空间是引入虚拟内存管理的最主要目的

    所谓独立的地址空间是指,不同进程中的同一个VA被MMU映射到不同的PA,并且在某一个进程中访问任何地址都不可能访问到另外一个进程的数据,这样使得任何一个进程由于程序BUG或恶意代码所导致的非法内存访问都不会意外改写其它进程的数据,不会影响其它进程的运行,从而保证了整个系统的稳定性。另一方面,每个进程都认为自己独占4GB的地址空间,编写程序会比较方便,不必为每个进程分配一个地址范围,而是每个进程都可以使用一个完整的地址空间中的任何地址。

  • 引入VA到PA的映射也会给分配和释放内存带来方便

    物理上不连续的空间可以映射为逻辑上连续的虚拟地址空间。比如要malloc一块很大的内存空间,而物理内存虽然有足够的空闲内存,却没有足够大的连续空闲内存,这时就可以分配多个不连续的物理页面,而映射为连续的虚拟地址范围

  • 一个系统如果同时运行着很多进程,为各进程分配的内存之和可能会大于实际可用的物理内存,虚拟内存管理使得这种情况下各进程仍然能够正常运行

    各进程分配的只不过是虚拟内存的页,这个页的内容可以映射到物理内存的页帧,也可以临时保存到磁盘上而不占用物理内存的页帧,磁盘上这一部分称为交换设备(Swap Device),可能是一个磁盘分区,也可能是一个磁盘文件。当物理内存不够时将物理内存中不常用的页帧临时保存到磁盘上,而当用到这些页帧时再从磁盘加载回内存,这称为换页(Paging)因此:系统中可分配的内存总量 = 物理内存的大小 + 交换设备的大小

  • 虚拟内存管理可以控制物理页面的访问权限

    物理内存本身是不限制访问的,任何地址都可以读写,而操作系统要求实现各种不同的访问权限,在先前的例子中我们已经看到,代码段要求是rx的,数据段要求是rw的,用户进程不能访问属于内核的地址空间,这些都是操作系统和MMU配合实现的

虚拟地址与物理地址之间的关系——页表

arm64_vm_pm_translation

页表用于建立用户进程的虚拟地址空间和系统物理内存(内存、页帧)之间的关联。

页表用于向每个进程提供一致的虚拟地址空间。应用程序看到的地址空间是一个连续的内存区。该表也将虚拟内存页映射到物理内存,因而支持共享内存的实现(几个进程同时共享的内存),还可以在不额外增加物理内存的情况下,将页换出到块设备来增加有效的可用内存空间。

CPU访问内存时的硬件操作顺序

arm_cpu_mem

  1. CPU核(图中的“ARM”框)发出VA请求读数据,TLB(Translation Lookaside Buffer)接收到该地址。TLB是MMU中的一块高速缓存(也是一种Cache),它缓存最近查找过的VA对应的页表项,如果TLB里缓存了当前VA的页表项就不必做Translation Table Walk了,否则去物理内存中读出页表项保存在TLB中,TLB缓存可以减少访问物理内存的次数。

  2. 页表项中不仅保存着物理页面的基地址,还保存着权限位和是否允许Cache的标志。MMU首先检查权限位,如果没有访问权限,就引发一个异常给CPU核。然后检查是否允许Cache,如果允许Cache就启用Cache和CPU核互操作,图中的“C, B bits”可以理解为直写和回写线,后面再详细解释这两个位的作用。

  3. 如果不允许Cache,则直接发出PA从物理内存中读取数据到CPU核

  4. 如果允许Cache,则以VA为索引到Cache中查找是否缓存了要读取的数据,如果Cache中已经缓存了该数据(称为Cache Hit)则直接返回给CPU核,如果Cache中没有缓存该数据(称为Cache Miss),则发出PA从物理内存中读取数据并缓存到Cache中,同时返回给CPU核。然而Cache并不是只取CPU核所要的数据,而是把相邻的数据都取上来缓存,这称为一个Cache Line

TLB

Translation Lookaside Buffer (TLB)是MMU中最近访问的页面翻译的缓存。 对于处理器执行的每个内存访问,MMU都会检查翻译是否缓存在TLB中。 如果请求的地址转换在TLB内导致命中,则地址转换立即可用。

每个TLB条目通常不仅包含物理地址虚拟地址,还包含内存类型缓存策略访问权限地址空间ID (ASID)虚拟机ID(VMID)等属性。 如果 TLB 不包含处理器发出的虚拟地址的有效转换,称为TLB未命中,则执行外部转换表遍历或查找。 MMU中的专用硬件使其能够读取内存中的转换表。 如果转换表遍历不会导致页面错误,则新加载的转换可以缓存在TLB中以供可能的重用。TLB的确切结构因ARM处理器的实现而异。

参考

  • DEN0024A_v8_architecture_PG.pdf