Linux 内存管理窥探(1):内存规划与分布

懂水的小伙伴都知道 Linux 地址组成有虚拟地址和物理地址,早在单片机时代,咱们都是直接访问物理内存,以及统一编址的外设,使用的都是实地址模式,也就是物理地址。物理地址,就是实实在在的物理上的内存地址。后面随着计算机的发展,为了更好的分层以及更好的管理应用程序,出现了虚拟地址的概念,虚拟地址是什么意思呢?虚拟地址不是实实在在的物理地址,但是他和实际的物理地址有一个对应关系。由于多道程序的出现,每个程序使用的都是 0~3G 的地址空间的虚拟地址空间,但是对于每一个进程,虚拟地址在实际意义上都会去通过一个映射关系,映射到物理地址上。

这样做有什么好处呢?

1. 每个进程看到的地址空间都一样(虚的),互不干扰。

2. 每个进程都可以用到比实际物理地址大的地址空间(通过换页技术)。

 

4G 虚拟地址空间

这里的 4G 的地址空间是怎么得来的呢?因为在 32bits 的处理器系统上,系统地址总线最大的寻址范围就是 4G,也就是 2 的 32 次幂等于 4G,这个是和 CPU 的架构绑定的,无需多说。

在这 4G 的虚拟地址空间里,又分成了两段:

1. 内核使用的部分

2. 应用程序使用的部分

 

其中的 3G~4G 的空间中,是内核的地址空间,每个进程的这部分都不可用于应用程序,所以能够使用的是 0 ~ 3GB

 

1. 用户空间分布

针对 0 ~ 3GB 的用户空间来说,用户的程序分为 .data,.text,.bss,stack,heap 这几个区域,这些区域在 3G 的空间分布为:

程序段(Text):程序代码在内存中的映射,存放函数体的二进制代码。

初始化过的数据(Data):在程序运行初已经对变量进行初始化的数据。

未初始化过的数据(BSS):在程序运行初未对变量进行初始化的数据。

栈 (Stack):存储局部、临时变量,函数调用时,存储函数的返回指针,用于控制函数的调用和返回。在程序块开始时自动分配内存,结束时自动释放内存,其操作方式类似于数据结构中的栈。

堆 (Heap):存储动态内存分配,需要程序员手工分配,手工释放.注意它与数据结构中的堆是两回事,分配方式类似于链表。

 

从上面的图可以看到,每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的,最高的1GB字节虚拟内核空间则为所有进程以及内核所共享。内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。

不管是内核空间还是用户空间,它们都处于虚拟空间中(因为 CPU 开启了 MMU)。 虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000),另外,使用虚拟地址可以很好的保护内核空间被用户空间破坏,虚拟地址到物理地址转换过程有操作系统和CPU共同完成(操作系统为CPU设置好页表,CPU通过MMU单元进行地址转换。

注意:几乎每个进程的虚拟地址空间中各段的分布都与上图完全一致,这就给远程发掘程序漏洞的人打开了方便之门。一个发掘过程往往需要引用绝对内存地址:栈地址,库函数地址等。远程攻击者必须依赖地址空间分布的一致性,来探索出这些地址。如果让他们猜个正着,那么有人就会被整了。因此,地址空间的随机排布方式便逐渐流行起来,Linux通过对栈、内存映射段、堆的起始地址加上随机的偏移量来打乱布局。但不幸的是,32位地址空间相当紧凑,这给随机化所留下的空间不大,削弱了这种技巧的效果。

 

2. 内核空间分布

对于 3G ~4G 的内核空间的布局关系为:

内核又将3~4G的虚拟地址空间,划分为如下几个部分: 

896MB又可以细分为ZONE_DMA和ZONE_NORMAL区域。 

1. 低端内存(ZONE_DMA):3G-3G+16M 用于DMA __pa线性映射

2. 普通内存(ZONE_NORMAL):3G+16M-3G+896M __pa线性映射 (若物理内存<896M,则分界点就在3G+实际内存) 

3. 高端内存(ZONE_HIGHMEM):3G+896-4G 采用动态的分配方式

ZONE_DMA + ZONE_NORMAL 属于直接映射区:虚拟地址=3G+物理地址 或 物理地址=虚拟地址-3G,从该区域分配内存不会触发页表操作来建立映射关系。

ZONE_HIGHMEM属于动态映射区:128M虚拟地址空间可以动态映射到(X-896)M(其中X位物理内存大小)的物理内存,从该区域分配内存需要更新页表来建立映射关系,vmalloc就是从该区域申请内存,所以分配速度较慢。

直接映射区的作用是为了保证能够申请到物理地址上连续的内存区域,因为动态映射区,会产生内存碎片,导致系统启动一段时间后,想要成功申请到大量的连续的物理内存,非常困难,但是动态映射区带来了很高的灵活性(比如动态建立映射,缺页时才去加载物理页)

 

为什么要有高端内存区?

试想一下,如果系统的物理内容小于 1 GB 那么内核可以访问到所有的物理内存,那么如果大于了这个数呢?比如 2 GB 的物理内存。内核是使用高端内存来动态进行映射,以便访问更多的物理内存。

当内核想访问高于896MB物理地址内存时,从0xF8000000 ~ 0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完后归还。这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存

 

映射到”内核动态映射空间”(noncontiguous memory allocation)
这种方式很简单,因为通过 vmalloc() ,在”内核动态映射空间”申请内存的时候,就可能从高端内存获得页面(参看 vmalloc 的实现),因此说高端内存有可能映射到”内核动态映射空间”中。

持久内核映射(permanent kernel mapping)
如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间?
内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START ,用于映射高端内存。在 2.6内核上,这个地址范围是 4G-8M 到 4G-4M 之间。这个空间起叫”内核永久映射空间”或者”永久内核映射空间”。这个空间和其它空间使用同样的页目录表,对于内核来说,就是 swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。通常情况下,这个空间是 4M 大小,因此仅仅需要一个页表即可,内核通过来 pkmap_page_table 寻找这个页表。通过 kmap(),可以把一个 page 映射到这个空间来。由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,及应该时从这个空间释放掉(也就是解除映射关系),通过 kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。

临时映射(temporary kernel mapping)
内核在 FIXADDR_START 到 FIXADDR_TOP 之间保留了一些线性空间用于特殊需求。这个空间称为”固定映射空间”在这个空间中,有一部分用于高端内存的临时映射。

 

注意:目前现实中,64位Linux内核不存在高端内存,因为64位内核可以支持超过512GB内存。若机器安装的物理内存超过内核地址空间范围,就会存在高端内存