[十月往昔]——Linux内核中的内存管理浅谈
为什么要叫做“十月往昔”呢?是为了纪念我的原博客。
不知道为什么,突然想来一个新的开始——而那个博客存活至今刚好十个月,也有十个月里的文档。
十月往昔,总有一些觉得珍贵的,所以搬迁到这里来。
而这篇文章是在09.04.20-09.04.21里写的。
Jason Lee
————————————–cut-line
1。基本框架(此处主要谈页式内存管理)
4G是一个比较敏感的字眼,早些日子,大多数机器(或者说操作系统)支持的内存上限都是这个数字。为什么呢?
之所以说是早些日子,因为现在64位的计算机已经很多了,而对于32位的计算机而言,页式管理是这么进行的,逻辑地址格式如下:
0 -11位:页内偏移OFFSET
12-21位:页面表偏移PT
22-31位:页面目录偏移PGD
寻址过程如下:
1)操作系统从寄存器CR3获得当前页面目录指针(基地址);
2)基地址+页面目录偏移->页面表指针(基地址);
3)页面表指针+页面表偏移->内存页基址;
4)内存页基址+页内偏移->具体物理内存单元。
显然,12位的页内偏移可以寻址4K,所以一张内存页为4K;而总共可寻内存为4G=2^10
* 2^10 * 2^12;因此在32位机器上内存上限一般为4G。
而操作系统是需要支持不同的平台的,比如32位,比如64位等。所以,linux统一使用页式三层映射:PGD-PMD-PT-OFFSET。
PAE是地址扩充功能(Physical Address Extension)的缩写,如果将内存管理设置为PAE模式,这时候就需要三层映射了。
三层映射架构是如何实现双层映射的?linux在暗地里“弄虚作假”了一番,有点类似领导让linux给三层映射一个重要位置,但是在32位计算机的地盘里就“阳奉阴违”了,只给三层映射一个有名无权的虚职。那么这个虚职是怎么实现的呢?
首先,开启了PAE模式的计算机是真切需要三层映射的,所以它不会给三层映射虚职,而是需要三层映射机制去做实事的;而32位计算机如果没有开启PAE模式,那么它是不需要三层映射的,双层映射是它更喜欢的。所以,首先是判断什么情况下给三层映射虚职——
从第一段的注释说明我们可以知道Linux x86
的页式映射机制在编译时可以选择使用传统的双层映射和新的
PAE 模式下的三层映射。而从接下来的代码可以知道,如果对
CONFIG_X86_PAE进行了预处理,即开启了
PAE 模式,那么就使用
pgtable-3level.h ,并且对
X86 PAE caches 进行初始化,而如果没有,则包含
pgtable-2level.h ,即使用双层映射。
pgtable-2level.h实现的双层映射:
从11
行到14
行的注释我们可以知道这里并没有让PMD
实际存在。
PGDIR_SHIFT 是
PGD 的偏移量——这里的偏移量是指位于
32 位中的几位,显然是
22 位,即第
23 位。而
PTRS_PER_PGD是
pointers per PGD,即每个
PGD
位段能表示的指针。这里是 1024
,显然需要 10
位,那么 PGD
就是从位 22
到位 31
,即第 23
位到第 32
位。
于是很显然我们可以了解到PMD
在这里是虚设的,挂了个虚职。因为
PTRS_PER_PMD 为
1 ,那么占用的是
0 位,因为
2^0 = 1 。
到这里,我们知道什么人的地盘上给三层映射挂虚职,怎么设置这个虚职的。而三层映射如果真干起了实事,本质其实和双层映射差不多,只不过多了几个位而已。
————————————–cut-line
1.数据结构和函数
众所周知,linux
下有许多与
ANSI C 不同的数据类型,比如
pid_t ;这些类型实际上是通过一层或者若干层的
typedef 定义而实现的,这样做的一个主要原因是为了可移植性的实现,而这样做的影响是看类型即可以很直观地知道用于何处,比如
pid_t
显然是一个进程 id
的类型;另外一个影响便是,编译内核需要使用相应的 gcc
编译器。
那么,在内存管理(1)
中提到的
PGD 、
PMD 、
PT 等是什么呢?在
include/asm-i386/page.h 中有如下代码:
在开启了PAE
模式的情况下,
pgd_t 、
pmd_t 都是长整形变量,而
pte_t 分为
pte_low 和
pte_high 两个部分。
PTE 是指
page table entry ,即某个具体的页表项,指向一张具体的内存页。但是一个内存页并不需要
32位全部使用,因为每张内存页大小都为
4KB
,所以从地址 0
开始,每间隔 4KB
为一张内存页。所以,内存页的首地址的低 12
位都为 0
,我们只需要高 20
位来指向一个内存页基址,低 12
位用来设置页面状态和权限。另外,还有一个宏用来读取 pte_t
类型的成员。
而没有开启PAE
模式的情况如下:
有了PMD
等结构后就有地方存储地址信息了,那么如何获取这些信息呢?见如下几个宏:
54
行到 56
行是读取成员变量的宏,而 58
行到 61
行则是进行类型转换。这里出现了一个 pgprot
,展开为 page protection
,页面保护。 pgprot
对应着上文提到的页面状态和权限,从而实现页面的保护机制:
具体的pgprot_t
在 /include/asm-i386/pgtable.h
中定义:
显然,pgprot_t
的位设置都是在低
12 位,而
PTE 的指针部分是高
20 位,共同构成了
32 位。那么,二者是如何构成
32 位的页面表表项呢?我们自然而然想到了
20 位左移
12 位再与
pgprot_t 的低
12 位相或,在
pgtable.h 中是由宏
mk_pte 来完成的:
而我们自然又遇到了__mk_pte
。那么
__mk_pte 是什么呢?在
/include/asm-i386/pgtable-2level.h中它一个宏:
以上为63
行单行。而在/include/asm-i386/page.h
中对 PAGE_SHIFT
进行了宏定义:
所以实现的是将内存页面编号左移12
位再与保护字段pgprot
相或得到了
pte 页面表项。另外在上述中出现了
__pte() ,它的原型为: 58#define __pte(x) ((pte_t) { (x) } ),即进行类型转换。而
pgprot_val(pgprot)
的原型为: 56#define pgprot_val(x) ((x).pgprot),与
52typedef struct { unsigned long pgprot; } pgprot_t;相对应则易知是获得某个
pgprot_t
类型变量的成员变量 pgprot
。
最后就剩下一个mem_map
了。我们先来了解一下
/include/linux/mm.h 中的
page 结构。
首先,先看一段前置说明:
简略说下,就是page
结构是与物理内存页相联系的,从而进行状态跟踪;其次,最经常访问的结构体内的成员字段应该保持在
16 位或者更大的单条缓冲线上——显然,这样有利于高速访问。接着来看page
结构体的定义:
当我们看到最后一行(182
行)的时候会有种恍然大悟的感觉—— mem_map_t
。于是我们就会联想
mem_map 是这么一个类型的变量。
实际上,mem_map
是一个全局变量(目前为止是),而且是一个指向
page 结构数组的指针;系统在初始化时根据物理内存的大小创建该数组。每一个数组元素都对应一张物理内存页。从软件方面来讲,页面表项的高
20 位是物理页面的编号,即
mem_map
数组的索引下标,通过该下标可以访问到与物理页面对应的page
结构。而从硬件方面来讲,页面表项的高 20
位再与 12
个 0
结合则构成了 32
位,即每张物理页面的基址。
mem_map
映射着全部的物理内存页,而其本身则分为不同的区,比如 ZONE_DMA、
ZONE_NORMAL和
ZONE_HIGHMEM等。其中
ZONE_DMA
是供 DMA
使用的; ZONE_HIGHMEM
是用于处理物理地址超过 1G
的存储空间。
事实上,三个管理区是这么分配的:0
~ 16MB
分配给
ZONE_DMA ,
16 ~896MB
分配给
ZONE_NORMAL ,最后,
896MB 以上的分配给
ZONE_HIGHMEM 。那么,为什么要这么分配呢?这是由于某些硬件只能特定地访问
0 ~
16MB来执行
DMA
模式;有些机器的配置使得物理内存页面无法总是保持被内核地址映射,这时需要使用ZONE_HIGHMEM
进行动态映射;而其余的就是可以被正常映射的。
那么,为什么这里是896MB
呢,而不是上文提的
1GB ?这是由于内核不仅为
highmem 预留了空间,也为
fixmap 和
vmalloc 预留了虚存空间。
OK
,那内核中的虚拟地址是什么?虚拟地址其实就是逻辑地址——与物理地址相对应。
我们不妨来看看物理地址和内核中虚拟地址在内核空间的关系:
pa
表示 physical address
,即物理地址,而 va
表示虚拟地址 virtual address
。这里,我们不得不去看看 __PAGE_OFFSET
:
前置注释有一堆,而宏定义只有一行。在32
位机器上,通过linux
内核的页式映射可以实现
4GB 的逻辑地址(虚拟地址)。而在
4G 字节中,
0xC0000000 到
0xFFFFFFFF 的这
1G 最高的逻辑地址用于内核本身,称之为“内核空间”;而较低的
3G 字节空间为用户空间。注意,这里的是虚的、逻辑地址。
于是我们知道了__PAGE_OFFSET
是用户空间和内核空间在虚地址上的分界。然而,物理地址始终是从
0×00000000开始的;所以对于内核空间来说,
pa 与
va 就相差了一个
PAGE_OFFSET 。而同时,
PAGE_OFFSET 也代表着用户空间的上限。
到这里,我们了解了内核空间只能“线性映射”1GB“
的物理地址,如果没有
ZONE_HIGHMEM 来管理高于
1GB 的物理地址,那么这些内存就会浪费掉了。于是系统初始化时预留了
128MB的虚存来用于将来可能的映射。以上是对于
x86
体系结构而言,对于其它体系,物理内存可以全部被映射, ZONE_HIGHMEM
为空。
现在回到内存管理区。/include/linux/mmzone.h
中有如下数据结构用于管理区:
(代码有点长,分段来看)
这里的前置注释说明了三个管理区的分布。
由注释我们可以知道这是用来控制SMP
使用的,仅允许单
CPU 工作。
而free_pages
表示着该区目前拥有的空闲页数。
由前置注释可知这是为了保留一些低端内存。我们在这里又遇到了一个新的数据类型:
这里free_area[MAX_ORDER]
是一组队列,用于分配不连续的内存块。队列的实现是通过
free_area_t类型中的成员
struct list_head free_list ,可参加
list.h 。
一些管理区信息如下:
112
表示的是该管理区所在的存储节点; 113
显然是一张内存映射表; 114
是该管理区的物理起始地址,而 115
表示的是在 mem_map
中的起始下标。显然这些都可以直接从变量名看出来。
120
表示的是管理区的名字, 121
表示的是管理区的大小, 122
表示的是管理区实用大小。
当多CPU
引入之后,
NUMA(Non-Uniform Memory Architecture)结构体系出现了,即非匀质存储结构。于是,每个
CPU
都有自己的物理地址,并且有一个公共的物存模块。这样有时候会出现CPU
请求的内存块无法在自己管辖的物理地址模块获得,也不能手伸太长去其它
CPU管理的模块,那么就需要到公共模块请求。同时,新的物理页面管理机制也进行了修正。
在NUMA
下,我们称
CPU 请求的一片连续物理内存页为
node (节点)。而且,此时的
mem_map 不再是全局变量,而是从属于具体节点;管理区也不再高高在上,也是被节点所拥有,每个存储节点至少有两个管理区。从而在
zone_struct
上便有了 pglist_data
数据结构,在 /include/linux/mmzone.h
定义:
首先看看158
行 struct page *node_mem_map
,由于每个节点有一片的内存页,这里的
node_mem_map 便是用来映射表示它们的(
page 结构数组);接着看首行,
155 行
zone_t node_zones[MAX_NR_ZONES]是该节点所拥有的管理区,同时在
zone_struct
也有一行 struct pglist_data *zone_pgdat
,指向所属节点pglist_data
数据结构。
————————————–cut-line –以上数据结构用于物理内存页面管理
–2009-04-20
晚
————————————–cut-line
(续)数据结构和函数
现在开始接触的是用于虚存管理的数据结构和函数。
通常,一个进程所需要使用的虚存空间是离散的各个区间,而区间的数据结构是/include/linux/mm.h中定义的:
45
行是定义了一个指向 mm_struct
结构体的指针,该结构体稍后了解。 vm_start
和 vm_end
是这一段 vm_area
的开始和结束位置,然而 vm_end
是该 vm_area
之后的第一个地址,不属于本 vm_area
。
51
行定义了一个指向 vm_area_struct
结构体的指针 vm_next
。这是由于进程使用的区间是离散的,所以各个区间需要形成链表来保持联系,这里的
vm_next 便是指向下一片
vm_area 的;该链表是按地址排序的。
53
行的 pgprot_t vm_page_prot
显然是本 vm_area
的保护信息, pgprot_t
在之前有谈过。
54
行的 vm_flags
是本 vm_area
的标志,如下:
80 ~83
行分别表示页是否可以被读、写、执行和共享。
85 ~88
行表示可以对
80 ~83
行的标志进行设置。
95
行表示该页含可执行代码。
96
行表示该页被锁。
其它标志均有注释。
在这里一般会有个疑惑,一个vm_area可能包含很多个内存页,为什么只有一个
vm_page_prot
和vm_flags呢?这是因为同一片
vm_area
的所有页面都必须保持相同的保护信息和状态标志。
现在回到vm_area_struct
。
56
行是 rb_node_t vm_rb; rb_node_t
是红黑树 (red-black tree)
节点类型。红黑树的结构如下:
之所以使用红黑树是因为使用链表搜索的话每次都要从头开始,会影响效率。
63 ~64
行为共享内存中的前后区间:
显然可见vm_ops
是一个指针,可以执行操作函数作用在该
vm_area 上。其中
open 和
close 用于打开、关闭虚存空间。而当请求页面不在内存中调用
nopage。
vm_area_struct后面的成员都有注释。
————————————–cut-line
在了解vm_area_struct
的开始,我们提到了
mm_struct 。
207
行的 mmap
指向虚存区间链表。
208
行是指向红黑树。
209
行的 mmap_cache
指向最后一次使用的虚存区间,因为虚存区间有若干个内存页,下一次请求的内存页很可能还在该区间。
210
行的 pgd