微信公众号:二进制人生
专注于嵌入式linux开发。问题或建议,请发邮件至hjhvictory@163.com。
更新:2019/2/12,转载请注明出处。
内存管理系列文章:
内容整理自网络和自己的认知,旨在学习交流,请勿用于商业用途。
linux内存管理(一)开篇介绍
linux内存管理(二)两种内存架构和三种内存模型
linux内存管理(三)内存管理三级结构
linux内存管理(四)分页机制概述
linux内存管理(五)内存源头
linux内存管理(六)分页机制的演进
linux内存管理(七)arm页表机制
linux内存管理(八)页表最初的初始化--从内核启动的第一段代码谈起
linux内存管理(九)内存管理临时机制-memblock
linux内存管理(十)内核空间虚拟内存布局
linux内存管理(十一)
内容目录
前面学习了arm的两级页表机制,今天来学习下linux的虚拟内存布局。知道了总体的布局,我们再逐个去分析具体的建立过程。
在32位的系统上,内核使用第3GB~第4GB的线性地址空间,共1GB大小(当然,前面说了,这个比例是可以由用户配置的,我们以典型的3:1的比例来介绍)。内核将其中的前896MB与物理内存的0~896MB进行直接映射,即线性固定映射,并把它叫做低端内存。将剩余的128M线性地址空间作为访问高于896M的内存的一个窗口,把它叫做高端内存。
引入高端内存映射这样一个概念的主要原因就是我们所安装的内存大于1G时,内核的1G线性地址空间无法建立一个完全的直接映射来触及整个物理内存空间。
这128MB的高端内存划分如下(从high_memory开始):



上图是内核空间1G线性地址的布局,直接映射区为PAGE_OFFSET~PAGE_OFFSET+ 896MB,直接映射的物理地址末尾对应的线性地址保存在high_memory变量中。直接映射区后边有一个8MB的保护区,目的是用来"捕获"对内存的越界访问。然后是非连续内存区,范围从VMALLOC_START~VMALLOC_END,出于同样的原因,每个非连续内存区之间隔着4KB。永久内核映射区从PKMAP_BASE开始,大小为2MB。后边是固定映射区,范围是FIXADDR_START~FIXADDR_TOP。
上面的两个图是网上广为流传的图,实际上和我现在看到的5.2版本的内核有点出入。Pkmap区不是在vmalloc区之后,而是在3G内存下方2M处,vmalloc区之后空着4M的空间,接着就是fixmap区。
对于一个内存只有256M的板子的虚拟地址映射如下:

看下vmalloc区的起始地址和在高端内存区的偏移:
#define VMALLOC_OFFSET (8*1024*1024)
#define VMALLOC_START (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))
#define VMALLOC_END 0xff800000UL
static void * __initdata vmalloc_min =
(void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET);
新的内核代码限制了vmalloc区最小大小240M,从而也限制了低端内存的最大范围。vmalloc_min是为了满足vmalloc区最小大小240M而计算出来的vmalloc起始地址的最小值,等于F0000000。
F0000000 - C0000000 = 768M,也就是说新版本的内核,低端内存最大只能是768M,不再是以前说的896M。
vmalloc_min变量在函数void __init adjust_lowmem_bounds(void)中使用,用于限制低端内存的最大值,high_memory全局变量也是在该函数初始化,从而确定了高端内存的起始地址。
setup_arch--〉adjust_lowmem_bounds
adjust_lowmem_bounds函数从名字就可以知道,它确定了低端内存的上界。低端内存的起始我们都清楚,从内核空间起始即3G开始。前面虽然低端内存的大小为768M,但说的是低端内存的最大大小,实际要根据系统的具体物理内存而定。比如你的板子最大内存就只有256M,那低端内存肯定就只有256M。而adjust_lowmem_bounds函数就是根据实际情况确定低端内存的上界。
我们在前面的文章里提到,系统解析设备树得到了一块块的内存块,将这些内存块交由memblock机制临时管理。memblock以物理地址的形式来管理内存。前面计算的vmalloc_min是指vmalloc区起始地址的最小值,这个地址是虚拟地址。我们需要把它转换成物理地址才能和memblock管理的物理内存同等计算。转换很简单:
vmalloc_limit = (u64)(uintptr_t)vmalloc_min - PAGE_OFFSET + PHYS_OFFSET;
PAGE_OFFSET等于3G,是指内核空间起始地址,也就是低端内存起始地址,PHYS_OFFSET是指物理地址起始偏移。
u64 vmalloc_limit;
struct memblock_region *reg;
phys_addr_t lowmem_limit = 0;
for_each_memblock(memory, reg) {//遍历每一个内存块
phys_addr_t block_start = reg->base;
phys_addr_t block_end = reg->base + reg->size;
if (reg->base < vmalloc_limit) {
if (block_end > lowmem_limit)
/*
* Compare as u64 to ensure vmalloc_limit does
* not get truncated. block_end should always
* fit in phys_addr_t so there should be no
* issue with assignment.
*/
lowmem_limit = min_t(u64,
vmalloc_limit,
block_end);
...
}
}
arm_lowmem_limit = lowmem_limit;
high_memory = __va(arm_lowmem_limit - 1) + 1;
__va用于将物理地址转为虚拟地址,低端内存虚拟地址转物理地址很简单,经过简单的线性计算即可:
static inline unsigned long __phys_to_virt(phys_addr_t x)
{
return x - PHYS_OFFSET + PAGE_OFFSET;
}
...
#define __pa(x) __virt_to_phys((unsigned long)(x))
high_memory是一个全局void *指针,记录高端内存起始位置,可以看到通过该函数确定了低端内存的上限,也即高端内存的起始位置。
后面会详细介绍低端内存建立映射的过程,以及高端内存的三种访问方式:非连续内存区映射(vmalloc),永久内核映射(pkmap),临时内核映射(fixmap)。
每天进步一点点……
图 二进制人生公众号





