Linux的内存管理可谓是学好Linux的必经之路,也是Linux的关键知识点,有人说打通了内存管理的知识,也就打通了Linux的任督二脉,这一点不夸张。有人问网上有很多Linux内存管理的内容,为什么还要看你这一篇,这正是我写此文的原因,网上碎片化的相关知识点大都是东拼西凑,先不说正确性与否,就连基本的逻辑都没有搞清楚,我可以负责任的说Linux内存管理只需要看此文一篇就可以让你入Linux内核的大门,省去你东找西找的时间,让你形成内存管理知识的闭环。
文章比较长,做好准备,深呼吸,让我们一起打开Linux内核的大门!
如果文章实在理解不透彻的同学,这里整理了2个相关视频讲解:
Linux内核其他知识点内容讲解学习地址:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈
Linux内存管理之CPU访问内存的过程
我喜欢用图的方式来说明问题,简单直接:
蓝色部分是cpu,灰色部分是内存,白色部分就是cpu访问内存的过程,也是地址转换的过程。在解释地址转换的本质前我们先理解下几个概念:
-
TLB:MMU工作的过程就是查询页表的过程。如果把页表放在内存中查询的时候开销太大,因此为了提高查找效率,专门用一小片访问更快的区域存放地址转换条目。(当页表内容有变化的时候,需要清除TLB,以防止地址映射出错。)
-
Caches:cpu和内存之间的缓存机制,用于提高访问速率,armv8架构的话上图的caches其实是L2 Cache,这里就不做进一步解释了。
虚拟地址转换为物理地址的本质
我们知道内核中的寻址空间大小是由CONFIG_ARM64_VA_BITS控制的,这里以48位为例,ARMv8中,Kernel Space的页表基地址存放在TTBR1_EL1寄存器中,User Space页表基地址存放在TTBR0_EL0寄存器中,其中内核地址空间的高位为全1,(0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF),用户地址空间的高位为全0,(0x00000000_00000000 ~ 0x0000FFFF_FFFFFFFF)
有了宏观概念,下面我们以内核态寻址过程为例看下是如何把虚拟地址转换为物理地址的。
我们知道linux采用了分页机制,通常采用四级页表,页全局目录(PGD),页上级目录(PUD),页中间目录(PMD),页表(PTE)。如下:
-
从CR3寄存器中读取页目录所在物理页面的基址(即所谓的页目录基址),从线性地址的第一部分获取页目录项的索引,两者相加得到页目录项的物理地址。
-
第一次读取内存得到pgd_t结构的目录项,从中取出物理页基址取出,即页上级页目录的物理基地址。
-
从线性地址的第二部分中取出页上级目录项的索引,与页上级目录基地址相加得到页上级目录项的物理地址。
-
第二次读取内存得到pud_t结构的目录项,从中取出页中间目录的物理基地址。
-
从线性地址的第三部分中取出页中间目录项的索引,与页中间目录基址相加得到页中间目录项的物理地址。
-
第三次读取内存得到pmd_t结构的目录项,从中取出页表的物理基地址。
-
从线性地址的第四部分中取出页表项的索引,与页表基址相加得到页表项的物理地址。
-
第四次读取内存得到pte_t结构的目录项,从中取出物理页的基地址。
-
从线性地址的第五部分中取出物理页内偏移量,与物理页基址相加得到最终的物理地址。
-
第五次读取内存得到最终要访问的数据。
整个过程是比较机械的,每次转换先获取物理页基地址,再从线性地址中获取索引,合成物理地址后再访问内存。不管是页表还是要访问的数据都是以页为单位存放在主存中的,因此每次访问内存时都要先获得基址,再通过索引(或偏移)在页内访问数据,因此可以将线性地址看作是若干个索引的集合。
Linux内存初始化
有了armv8架构访问内存的理解,我们来看下linux在内存这块的初始化就更容易理解了。
创建启动页表:
在汇编代码阶段的head.S文件中,负责创建映射关系的函数是create_page_tables。create_page_tables函数负责identity mapping和kernel image mapping。
-
identity map:是指把idmap_text区域的物理地址映射到相等的虚拟地址上,这种映射完成后,其虚拟地址等于物理地址。idmap_text区域都是一些打开MMU相关的代码。
-
kernel image map:将kernel运行需要的地址(kernel txt、rodata、data、bss等等)进行映射。
arch/arm64/kernel/head.S:
ENTRY(stext)
bl preserve_boot_args
bl el2_setup // Drop to EL1, w0=cpu_boot_mode
adrp x23, __PHYS_OFFSET
and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0
bl set_cpu_boot_mode_flag
bl __create_page_tables
/*
* The following calls CPU setup code, see arch/arm64/mm/proc.S for
* details.
* On return, the CPU will be ready for the MMU to be turned on and
* the TCR will have been set.
*/
bl __cpu_setup // initialise processor
b __primary_switch
ENDPROC(stext)
__create_page_tables主要执行的就是identity map和kernel image map:
__create_page_tables:
......
create_pgd_entry x0, x3, x5, x6
mov x5, x3 // __pa(__idmap_text_start)
adr_l x6, __idmap_text_end // __pa(__idmap_text_end)
create_block_map x0, x7, x3, x5, x6
/*
* Map the kernel image (starting with PHYS_OFFSET).
*/
adrp x0, swapper_pg_dir
mov_q x5, KIMAGE_VADDR + TEXT_OFFSET // compile time __va(_text)
add x5, x5, x23 // add KASLR displacement
create_pgd_entry x0, x5, x3, x6
adrp x6, _end // runtime __pa(_end)
adrp x3, _text // runtime __pa(_text)
sub x6, x6, x3 // _end - _text
add x6, x6, x5 // runtime __va(_end)
create_block_map x0, x7, x3, x5, x6
......
其中调用create_pgd_entry进行PGD及所有中间level(PUD, PMD)页表的创建,调用create_block_map进行PTE页表的映射。关于四级页表的关系如下图所示,这里就不进一步解释了。
汇编结束后的内存映射关系如下图所示:
等内存初始化后就可以进入真正的内存管理了,初始化我总结了一下,大体分为四步:
-
物理内存进系统前
-
用memblock模块来对内存进行管理
-
页表映射
-
zone初始化
Linux是如何组织物理内存的?
-
node 目前计算机系统有两种体系结构:
-
非一致性内存访问 NUMA(Non-Uniform Memory Access)意思是内存被划分为各个node,访问一个node花费的时间取决于CPU离这个node的距离。每一个cpu内部有一个本地的node,访问本地node时间比访问其他node的速度快
-
一致性内存访问 UMA(Uniform Memory Access)也可以称为SMP(Symmetric Multi-Process)对称多处理器。意思是所有的处理器访问内存花费的时间是一样的。也可以理解整个内存只有一个node。
-
zone
ZONE的意思是把整个物理内存划分为几个区域,每个区域有特殊的含义
-
page
代表一个物理页,在内核中一个物理页用一个struct page表示。
-
page frame
为了描述一个物理page,内核使用struct page结构来表示一个物理页。假设一个page的大小是4K的,内核会将整个物理内存分割成一个一个4K大小的物理页,而4K大小物理页的区域我们称为page frame
-
page frame num(pfn)
pfn是对每个page frame的编号。故物理地址和pfn的关系是:
物理地址>>PAGE_SHIFT = pfn
-
pfn和page的关系
内核中支持了好几个内存模型: