1 今日内容(分页机制初始化)
在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制.
在分页机制完成后, 才会开始初始化系统的内存数据结构(包括内存节点数据和内存区域), 并在随后初始化buddy伙伴系统来接管内存管理的工作
2 分页机制初始化
arm64架构下, 内核在start_kernel()
->setup_arch()
中通过arm64_memblock_init( )
完成了memblock的初始化之后, 接着通过setup_arch()
->paging_init()
开始初始化分页机制
paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响
2.1 虚拟地址空间(以x86_32位系统为例)
因此在仔细考察其实现之前,很重要的一点是解释该函数的目的
在x86_32系统上内核通常将总的4GB可用虚拟地址空间按3:1的比例划分给用户空间和内核空间, 虚拟地址空间的低端3GB
用于用户状态应用程序, 而高端的1GB则专用于内核. 尽管在分配内核的虚拟地址空间时, 当前系统上下文是不相干的, 但每个进程都有自身特定的地址空间.
这些划分主要的动机如下所示
- 在用户应用程序的执行切换到核心态时(这总是会发生,例如在使用系统调用或发生周期性的时钟中断时),内核必须装载在一个可靠的环境中。因此有必要将地址空间的一部分分配给内核专用.
- 物理内存页则映射到内核地址空间的起始处,以便内核直接访问,而无需复杂的页表操作.
如果所有物理内存页都映射到用户空间进程能访问的地址空间中, 如果在系统上有几个应用程序在运行, 将导致严重的安全问题. 每个应用程序都能够读取和修改其他进程在物理内存中的内存区. 显然必须不惜任何代价防止这种情况出现.
虽然用于用户层进程的虚拟地址部分随进程切换而改变,但是内核部分总是相同的
出于内存保护等一系列的考虑, 内核将整个进程的虚拟运行空间划分为内核虚拟运行空间和内核虚拟运行空间
按3:1的比例划分地址空间, 只是约略反映了内核中的情况,内核地址空间作为内核的常驻虚拟地址空间, 自身又分为各个段
地址空间的第一段用于将系统的所有物理内存页映射到内核的虚拟地址空间中。由于内核地址空间从偏移量0xC0000000开始,即经常提到的3 GiB,每个虚拟地址x都对应于物理地址x—0xC0000000,因此这是一个简单的线性平移。
直接映射区域从0xC0000000到high_memory地址,high_memory准确的数值稍后讨论。第1章提到过,这种方案有一问题。由于内核的虚拟地址空间只有1 GiB,最多只能映射1 GiB物理内存。IA-32系统(没有PAE)最大的内存配置可以达到4 GiB,引出的一个问题是,如何处理剩下的内存?
这里有个坏消息。如果物理内存超过896 MiB,则内核无法直接映射全部物理内存。该值甚至比此前提到的最大限制1 GiB还小,因为内核必须保留地址空间最后的128 MiB用于其他目的,我会稍后解释。将这128 MiB加上直接映射的896 MiB内存,则得到内核虚拟地址空间的总数为1 024 MiB = 1GiB。内核使用两个经常使用的缩写normal和highmem,来区分是否可以直接映射的页帧。
内核地址空间的最后128 MiB用于何种用途呢?如图3-15所示,该部分有3个用途。
虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配。该机制通常用于用户过程,内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上,在内核需要物理内存时,就可能出现可用空间不连续的情况。此类情况,主要出现在动态加载模块时
持久映射用于将高端内存域中的非持久页映射到内核中
固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由
选择。它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间
的关联可以自行定义,关联建立后内核总是会注意到的
同样我们的用户空间, 也被划分为几个段, 包括从高地址到低地址分别为 :
栈 |
局部变量, 函数参数, 返回地址等 |
堆 |
动态分配的内存 |
BSS段 |
未初始化或初值为0的全局变量和静态局部变量 |
数据段 |
一初始化且初值非0的全局变量和静态局部变量 |
代码段 |
可执行代码, 字符串面值, 只读变量 |
2.2 paging_init初始化分页机制
paging_init函数定义在arch/arm64/mm/mmu.c?v=4.7, line 538
/*
* paging_init() sets up the page tables, initialises the zone memory
* maps and sets up the zero page.
*/
void __init paging_init(void)
{
phys_addr_t pgd_phys = early_pgtable_alloc();
pgd_t *pgd = pgd_set_fixmap(pgd_phys);
map_kernel(pgd);
map_mem(pgd);
/*
* We want to reuse the original swapper_pg_dir so we don't have to
* communicate the new address to non-coherent secondaries in
* secondary_entry, and so cpu_switch_mm can generate the address with
* adrp+add rather than a load from some global variable.
*
* To do this we need to go via a temporary pgd.
*/
cpu_replace_ttbr1(__va(pgd_phys));
memcpy(swapper_pg_dir, pgd, PAGE_SIZE);
cpu_replace_ttbr1(swapper_pg_dir);
pgd_clear_fixmap();
memblock_free(pgd_phys, PAGE_SIZE);
/*
* We only reuse the PGD from the swapper_pg_dir, not the pud + pmd
* allocated with it.
*/
memblock_free(__pa(swapper_pg_dir) + PAGE_SIZE,
SWAPPER_DIR_SIZE - PAGE_SIZE);
}
启动期间的内存管理之pagging_init初始化分页机制--Linux内存管理(十四)