网上大神整理的linux内存管理图镇楼

起因是面试中面试官提到linux高端内存以及低端内存,对linux内核内存管理这方面了解较少。因此本文全面阐述linux内存,就是上图
Linux内核地址空间划分
通常32位Linux内核虚拟地址空间划分03G为用户空间,34G为内核空间(注意,内核可以使用的线性地址只有1G)。注意这里是32位内核地址空间划分,64位内核地址空间划分是不同的。

高端内存以及低端内存
计算机内存寻址过程中我们将页框作为内存分配的基本单元,一般现代32位计算机页框大小为4kb,此外内核必须记录每个页框的状态,如记录该页框保存的是内核代码还是用户代码,因此linux用page这一描述符来保存页框的状态信息,所有的page都存放在mem_map数组中以便管理。
但是由于计算机发展历史以及硬件体系结构的限制,导致
(1)ISA总线的直接内存存取(DMA)处理器只能对随机存储器(RAM)的前16MB寻址。
(2)具有大容量的RAM的32位计算机中,CPU不能直接访问所有的物理内存!
因此linux将物理内存划分为三个管理区
(1)ZONE_DMA:包含低于16M的内存页框,只有它能够作用于DMA;
(2)ZONE_NORMAL:包含高于16M且低于896M的内存页框;
(3)ZONE_HIGHMEM:包含从896M开始高于896M的内存页框
由于在32位系统中,线性地址空间是4G,其中规定34G的范围是内核空间(1G),03G是用户空间(3G)。如果把这1G的内核线性地址空间全部拿来直接一一映射物理内存的话,在内核态的所有进程(线程)能使用的物理内存总共最多只有1G,很显然,如果你有4G内存,3G都不能用来做内核空间,太浪费了!因此就出现了高端内存以及低端内存。
ZONE_DMA和ZONE_NORMAL我们一般称之为低端内存(896MB),ZONE_HIGHMEM称之为高端内存。即从0xc0000000往高地址空间896mb内存为低端内存,再往上至0xffffffff为高端内存(如下图红色字体所示)。高、低端内存的分类主要在于区分物理内存地址是否可以直接映射到内核线性地址空间中。问题来了,高、低端内存和内核的线性空间的映射是什么样的呢?

低端内存映射
低端内存映射比较简单,这一部分虚拟地址与物理内存中对应的地址只差一个固定偏移量(3G),如果内存物理地址空间从0x00000000地址编址,那么这个固定偏移量就是PAGE_OFFSET=0xC0000000。逻辑地址与物理地址对应的关系为物理地址 = 逻辑地址 – 0xC0000000,如逻辑地址0xc0000003对应的物理地址为0×3,0xc0000004对应的物理地址为0×4。
即1G中的前896M,这部分内核线性空间与物理内存的0~896M一一映射(注意这里内核的虚拟地址在“高端”,但是它映射的物理内存地址在低端。)
高端内存映射
正如前文所说,高端内存解决的问题是突破1gb内核空间的限制,达到更多的内存空间可以映射为内核空间。类似于一种临时借一段地址空间,建立临时地址映射,用完后释放,达到这段地址空间可以循环使用,访问所有物理内存。 如下图高端内存和用户内存都通过页表寻址。

根据应用目标不同,高端内存区分vmalloc区、永久映射区(可持久化映射区)和固定映射区(临时映射区)。
vmalloc区
vmalloc映射区时高端内存的主要部分,该区间的头部与内核线性映射空间(即低端内存)之间有一个8MB的隔离区,尾部与后续的可持久映射区有一个4KB的隔离区。

vmalloc映射区的映射方式与用户空间完全相同,内核可以通过调用函数vmalloc()在这个区域获得内存。这个函数的功能相当于用户空间的malloc(),所提供的内存空间在虚拟地址上连续(注意,不保证物理地址连续)。
可持久化映射区
该区允许内核建立高端页框到内核地址空间的长期映射
内核专门为此留出一块线性空间,从PKMAP_BASE开始,用于映射高端内存,就是可持久内核映射区。在可持久内核映射区,可通过调用函数kmap()在物理页框与内核虚拟页之间建立长期映射。这个空间通常为4MB,最多能映射1024个页框,数量较为稀少,所以为了加强页框的周转,应及时调用函数kunmap()将不再使用的物理页框释放。
临时映射区
内核在 FIXADDR_START 到 FIXADDR_TOP 之间保留了一些线性空间用于特殊需求。这个空间称为“固定映射空间”,在这个空间中,有一部分用于高端内存的临时映射。因为在这个区域所获得的内存空间没有所保护,故所获得的内存必须及时使用;否则一旦有新的请求,该页框上的内容就会被覆盖,所以这个区域叫做临时映射区。linux中通过 kmap_atomic() 可实现临时映射。
linux内存分配api及相关底层调用
网上找到的一张图片

由上图可知
由malloc、fork等系统调用和kmalloc、vmalloc申请得到虚拟内存。
在我们使用该内存的时候,产生请页异常(kmalloc除外)
从空闲的页框分配物理内存,和虚拟地址建立映射。
malloc
malloc分配的是用户的内存
kmalloc和vmalloc
kmalloc和vmalloc是分配的是内核的内存。
如上文提到的vmalloc区,函数vmalloc()在vmalloc分配区分配内存,可获得虚拟地址连续,但并不保证其物理页框连续的较大内存。 与物理空间的内存分配函数malloc()有所区别,vmalloc()分配的物理页不会被交换出去。函数vmalloc()的原型如下:
1 | void *vmalloc(unsigned long size) |
其中,参数size为所请求内存的大小,返回值为所获得内存虚拟地址指针。
kmalloc()是内核另一个常用的内核分配函数,它可以分配一段未清零的连续物理内存页,返回值为直接映射地址。由kmalloc()可分配的内存最大不能超过32页。其优点是分配速度快,缺点是不能分配大于128KB的内存页(出于跨平台考虑)。
kmalloc、vmalloc这两个函数所分配的内存都处于内核空间,即从3GB~4GB;但位置不同,kmalloc()分配的内存处于3GB~high_memory(ZONE_DMA、ZONE_NORMAL)之间,而vmalloc()分配的内存在VMALLOC_START~4GB(ZONE_HIGHMEM)之间,也就是非连续内存区。一般情况下在驱动程序中都是调用kmalloc()来给数据结构分配内存,而vmalloc()用在为活动的交换区分配数据结构,为某些I/O驱动程序分配缓冲区,或为模块分配空间。

kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续。
kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大
vmalloc比kmalloc要慢
alloc_pages()
与上述在虚拟空间分配内存的函数不同,alloc_pages()是在物理内存空间分配物理页框的函数 ,其原型如下:
1 | static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order) |
其中,参数order表示所分配页框的数目,该数目为2^order。order的最大值由include/Linux/Mmzone.h文件中的宏MAX_ORDER决定。参数gfp_mask为说明内存页框分配方式及使用场合。函数返回值为页框块的第一个页框page结构的地址。
kmap()
kmap()是一个映射函数,它可以将一个物理页框映射到内核空间的可持久映射区。这种映射类似于内核ZONE_NORMAL的固定映射,但虚拟地址与物理地址的偏移不一定是PAGE_OFFSET。由于内核可持久映射区的容量有限(总共只有4MB),因此当内存使用完毕后,应该立即释放。
函数kmap()的函数原型如下:
1 | void *kmap(struct page *page) |