linux之 内存管理(1)-armv8 内核启动页表建立过程
一、内核启动时,页表映射有哪些?
Linux初始化过程,会依次建立如下页表映射:
1.恒等映射:页表基地址idmap_pg_dir;
2.粗粒度内核镜像映射:页表基地址init_pg_dir;
3.fixmap映射:页表基地址为init_pg_dir;
4.内核主表映射,包含细粒度内核镜像映射和线性映射,页表基地址为swapper_pg_dir;
5.用户空间页表映射:页表基地址task->mm->pgd;
二、恒等映射
在我们将uboot和kernel image烧写到ram后,uboot完成相关硬件的初始化后,最后跳转到kernel的入口函数(假设地址为0x40000000,那么就会将PC设置为0x40000000),此时,由于MMU尚未开启,所以程序一直运行在物理地址空间,也就是PC的值,也就是指令地址,都是物理地址。
一旦MMU开始后,地址就会经过MMU转换为物理地址,也就是所有地址就会被认为是虚拟地址,但是,现代处理器大多支持多级流水线,处理器会提前预取多条指令到流水线中,当打开MMU时,CPU已经预取多条指令到流水线中,并且这些指令都是用物理地址预取的;MMU开启后,将以虚拟地址访问,这样继续访问流水线中预取的指令(按物理地址预取,比如0x40000488,这个地址就会被认为是虚拟地址),就很容易出错。由此,便引出了恒等映射,所谓恒等,就是将虚拟地址映射到完全相等的物理地址上去(比如前面地址0x40000488映射到物理地址还是0x40000488),这样就解决了这个问题。当然这个映射的区域是很小的,恒等映射完成后,启动MMU,CPU就开启虚拟地址访问阶段。
简单看下启动部分的汇编代码(head.S)
SYM_CODE_START(primary_entry) //入口函数
bl preserve_boot_args // 保存启动参数到boot_args数组
bl el2_setup // w0=cpu_boot_mode,切换到EL1模式
adrp x23, __PHYS_OFFSET
and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0
bl set_cpu_boot_mode_flag //设置__boot_cpu_mode全局变量
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 // 为打开MMU做准备
b __primary_switch //启动MMU,最后跳转到start_kernel(内核C语言部分)
SYM_CODE_END(primary_entry)
其中__create_page_tables函数的功能就是创建恒等映射和内核映像映射页表,这里先看创建恒等映射页表。
2.1 __create_page_tables
/*
* Create the identity mapping.
*/
adrp x0, idmap_pg_dir
adrp x3, __idmap_text_start // __pa(__idmap_text_start)
mov x5, #VA_BITS_MIN
1:
adr_l x6, vabits_actual
str x5, [x6]
dmb sy
dc ivac, x6 // Invalidate potentially stale cache line
adrp x5, __idmap_text_end
clz x5, x5
cmp x5, TCR_T0SZ(VA_BITS) // default T0SZ small enough?
b.ge 1f // .. then skip VA range extension
adr_l x6, idmap_t0sz
str x5, [x6]
dmb sy
dc ivac, x6 // Invalidate potentially stale cache line
mov x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT)
str_l x4, idmap_ptrs_per_pgd, x5
1:
ldr_l x4, idmap_ptrs_per_pgd
mov x5, x3 // __pa(__idmap_text_start)
adr_l x6, __idmap_text_end // __pa(__idmap_text_end)
map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14
这段汇编的作用的就是创建恒等映射页表,在调用map_memory的时候,各个参数的值分别如下:
x0:idmap_pg_dir
x3:idmap_text_start
x6:idmap_text_end
x7:SWAPPER_MM_MMUFLAGS
x4:PTRS_PER_PGD
map_memory
其它的作为输出。再看下宏map_memory的定义
.macro map_memory, tbl, rtbl, vstart, vend, flags, phys, pgds, istart, iend, tmp, count, sv
add \rtbl, \tbl, #PAGE_SIZE
mov \sv, \rtbl
mov \count, #0
compute_indices \vstart, \vend, #PGDIR_SHIFT, \pgds, \istart, \iend, \count
populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
mov \tbl, \sv
mov \sv, \rtbl
#if SWAPPER_PGTABLE_LEVELS > 3
compute_indices \vstart, \vend, #PUD_SHIFT, #PTRS_PER_PUD, \istart, \iend, \count
populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
mov \tbl, \sv
mov \sv, \rtbl
#endif
#if SWAPPER_PGTABLE_LEVELS > 2
compute_indices \vstart, \vend, #SWAPPER_TABLE_SHIFT, #PTRS_PER_PMD, \istart, \iend, \count
populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
mov \tbl, \sv
#endif
compute_indices \vstart, \vend, #SWAPPER_BLOCK_SHIFT, #PTRS_PER_PTE, \istart, \iend, \count
bic \count, \phys, #SWAPPER_BLOCK_SIZE - 1
populate_entries \tbl, \count, \istart, \iend, \flags, #SWAPPER_BLOCK_SIZE, \tmp
.endm
宏各个参数的含义如下:
tbl:页表起始地址,页表基地址
rtbl:下一级页表起始地址,一般是tbl + PAGE_SIZE
vstart:要映射的虚拟地址的起始地址
vend:要映射的虚拟地址的结束地址
flags:最后一级页表的属性
phys:要映射的物理地址
pgds:PGD页表的个数
首先用compute_indices计算虚拟地址对应的pgd页表的索引,然后用populate_entries填充相关页表项。
compute_indices
.macro compute_indices, vstart, vend, shift, ptrs, istart, iend, count
lsr \iend, \vend, \shift
mov \istart, \ptrs
sub \istart, \istart, #1
and \iend, \iend, \istart // iend = (vend >> shift) & (ptrs - 1)
mov \istart, \ptrs
mul \istart, \istart, \count
add \iend, \iend, \istart // iend += (count - 1) * ptrs
// our entries span multiple tables
lsr \istart, \vstart, \shift
mov \count, \ptrs
sub \count, \count, #1
and \istart, \istart, \count
sub \count, \iend, \istart
.endm
各个入参含义:
vstart:起始虚拟地址
vend:结束虚拟地址
shift:各级页表在虚拟地址中的偏移
ptrs:页表项的个数
istart:vstart索引值
iend:vend索引值
populate_entries
.macro populate_entries, tbl, rtbl, index, eindex, flags, inc, tmp1
.Lpe\@: phys_to_pte \tmp1, \rtbl
orr \tmp1, \tmp1, \flags // tmp1 = table entry
str \tmp1, [\tbl, \index, lsl #3]
add \rtbl, \rtbl, \inc // rtbl = pa next level
add \index, \index, #1
cmp \index, \eindex
b.ls .Lpe\@
.endm
各参数含义:
tbl:页表基地址
rtbl:下级页表基地址
index:写入页表的起始索引
flags:页表属性
三、粗粒度内核镜像映射
一般情况下,物理地址都是低端地址(不会超过256T),所以,恒等映射的时候,其实是将用户空间的一段地址与物理地址建立了映射,所以,MMU启动后,idmap_pg_dir会写入TTBR0;然内核链接的地址都是高端地址(0xFFFF_xxxx_xxxx_xxxx),它的页表基地址需要写入TTBR1,所以还需要创建一张表来映射整个内核镜像。 这张表的页表基地址是init_pg_dir。
/*
* Map the kernel image (starting with PHYS_OFFSET).
*/
adrp x0, init_pg_dir
mov_q x5, KIMAGE_VADDR // compile time __va(_text)
add x5, x5, x23 // add KASLR displacement
mov x4, PTRS_PER_PGD
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)
map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14
这里还是调用map_memory宏,前面已经看过。内核镜像的映射后如下图:
这里有几个点需要注意:
(1).idmap.text本身也是属于内核镜像的一部分,所以,这部分,其实是映射了两次;
(2)两张页表idmap_pg_dir和init_pg_dir,也都在内核镜像当中,从vmlinux.lds.S能够看到其定义:
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
idmap_pg_end = .;
...
. = ALIGN(PAGE_SIZE);
init_pg_dir = .;
. += INIT_DIR_SIZE;
init_pg_end = .;
(3) 细心的人可能会注意到上面那张映射的图好像有问题,明明是4级映射,怎么只有PGD,PUD和PMD三张表,这是因为目前我们项目的内核使能了section map机制,在这种机制下,idmap和init_pg_dir的创建会比实际设置值减少一级,将最后一级页表取消,所以最后一级的页表每一项能够映射2M,整张表的映射大小达到了1G,这样,一张表就能映射整个内核了。
这个最后一级能够映射2M,和整张表可以映射1G,对于不懂虚拟地址转换成物理地址的人,可能是一团浆糊,下面简单介绍一下,为什么?
第一、64位地址,四级页表,每个页表4k,就是一个page,能存储512个entry,每个entry 占用8字节,这是最常见的情况。
内核将一个进程的内存映射表建立好之后,在该进程被调度运行的时候,会将PGD的物理地址放置到MMU的页表基地址寄存器中,在X86_64架构下,该寄存器为CR3,ARM64架构下,该寄存器为ttbr0_el1和ttbr1_el1,接下来的寻址过程中,就不需要linux来干预了,MMU会通过PGD-PUD-PMD-PTE-PAGE-OFFSET这个过程,根据虚拟地址,找到其对应的物理地址。
第二、拆分一下,64位的地址,MMU如何计算虚拟地址 得出物理地址;
- ttbr0_el1寄存器中保存着页表的基地址,也就是存放PGD页表起始的物理地址,在内核中是申请了一个page,共4096字节,每个表项占8字节,可以存512个表项;要取哪个表项,需要根据虚拟地址 39-47 的PGD的索引,就可以得到下一级PUD的页表基地址;
- 39-47bit为PGD的索引,该索引可以找到PUD的页基地址,2的9次方,共512个表项,每个表项占8字节,共4096字节。
- 30-38bit为PUG的索引,该索引可以找到PMD的页基地址,2的9次方,共512个表项,每个表项占8字节,共4096字节;
- 21-29bit为PMD的索引,该索引可以找到PTE的页基地址,2的9次方,共512个表项,每个表项占8字节,共4096字节;
- 12-20bit为PTE的索引,该索引可以找到物理内存页面的基地址,2的9次方,共512个表项,每个表项占8字节,共4096字节;
- 0-11bit为页内偏移地址,根据页基地址+偏移量找到对应的物理内存;
第三、上面使用section map 建立页表到PMD,可以映射2M,是 如何计算?
假如 21-29bit 的索引值计算出来为A,根据A,找到PTE的页基地址,PTE有512个表项,每个表项代表一个页面基地址,每个页4K大小,可以得出公式:
2M = 512* 4K
第四、1G 的大小是如何计算出?
固定PGD 的索引值,得出固定的PUG索引值,可以得到PMD 的基地址,PMD的基地址开始保存512个表项,一个PMD的表项可以有2M的物理地址范围,那么就是
1G = 512* 2M
所以init_pg_dir 的映射表,一整张表就能映射整个内核 是指一个PMD ,即一个PMD的页,512个表项*2M。
四、fixmap映射
为什么需要fixmap? 内核启动流程,首先由uboot将kernel image和dtb拷贝到内存中,并且将dtb物理地址告诉内核。内核启动后,在mmu已经启动(之后soc只能通过虚拟地址访问内存),paging_init还没完成调用之前,内核启动过程需要访问解析dtb,此时就需要将虚拟地址和物理地址进行映射。这就是fixmap机制的产生原因。
fixmap的区域是在编译阶段就确定好的,在fixmap.h能够看到代码定义
enum fixed_addresses {
FIX_HOLE,
#define FIX_FDT_SIZE (MAX_FDT_SIZE + SZ_2M)
FIX_FDT_END,
FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1,
FIX_EARLYCON_MEM_BASE,
FIX_TEXT_POKE0,
#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
FIX_ENTRY_TRAMP_DATA,
FIX_ENTRY_TRAMP_TEXT,
#define TRAMP_VALIAS (__fix_to_virt(FIX_ENTRY_TRAMP_TEXT))
#endif /* CONFIG_UNMAP_KERNEL_AT_EL0 */
__end_of_permanent_fixed_addresses,
#define NR_FIX_BTMAPS (SZ_256K / PAGE_SIZE)
#define FIX_BTMAPS_SLOTS 7
#define TOTAL_FIX_BTMAPS (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,
FIX_PTE,
FIX_PMD,
FIX_PUD,
FIX_PGD,
__end_of_fixed_addresses
};
再看下fixmap的虚拟地址是怎么计算的:
#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))
这里面的x就对应上面的枚举值,所以两个相差为1的枚举间隔的内存大小刚好是一页,即4K。
fixmap的映射大致可以分为三类:固定映射,动态映射,查找映射。
固定映射就是在内核启动过程中,用于分配给指定物理地址设定的映射关系,持续在系统启动到关机的整个生命周期;
动态映射就是在内核启动过程中,或启动完成后,动态地给模块分配虚拟地址,模块退出后释放该虚拟内存,即过程中动态建立映射关系;
查找映射,用于 paing_init,通过 pgd_set_fixmap 给页目录表的全局表项 swapper_pg_dir 做虚拟内存映射,映射到具体的 pte 的表项,然后后续就可以根据这个页表项所映射的内存空间建立模块的映射关系。
处理流程
映射框架建立:early_fixmap_init
void __init early_fixmap_init(void)
{
pgd_t *pgdp;
pud_t *pudp;
pmd_t *pmdp;
unsigned long addr = FIXADDR_START; //(1)
pgdp = pgd_offset_k(addr); // (2)
__p4d_populate(pgdp, __pa_symbol(bm_pud), PUD_TYPE_TABLE); // (3)
pudp = fixmap_pud(addr); // (4)
__pud_populate(pudp, __pa_symbol(bm_pmd), PMD_TYPE_TABLE); //(5)
pmdp = fixmap_pmd(addr); //(6)
__pmd_populate(pmdp, __pa_symbol(bm_pte), PMD_TYPE_TABLE); //(7)
}
(1)拿到FIXMAP的起始地址;
(2)这里获取addr地址对应pgd全局页表中的entry,这个pgd全局页表就是init_pg_dir全局页表;
#define pgd_offset_k(address) pgd_offset(&init_mm, (address))
#define INIT_MM_CONTEXT(name) .pgd = init_pg_dir,
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
...
INIT_MM_CONTEXT(init_mm)
}
完成后如下图所示
early_ioremap_setup
void __init early_ioremap_setup(void)
{
int i;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
}
这个比较简单,就是将FIX_BTMAP_BEGIN ~ FIX_BTMAP_END的虚拟内存做初始化,依序将各个slots的起始地址填入slot_virt数组中,目的是为后续使用这部分虚拟内存的时候,只需要调用这个数组对应的数组项即可,这段虚拟内存,既被称为临时映射虚拟内存区域。
dtb映射
内核初始化需要读取dtb,dtb在烧写的时候会烧写到物理内存中的一块区域,现在开启MMU后,需要访问的话,就必须要先建立起映射。 dtb的虚拟地址空间为4M。由于linux规定dtb的size需要小于2M,理论上用一个2M的虚拟地址空间即可完成映射。但是建立dtb页表需要使用section map,即其最后一级页表会直接指向2M block为边界的物理地址。此时若dtb的位置横跨物理地址2M边界,就需要为上下两个2M block都创建页表才能访问完整的image,正是基于这个考虑,此处内核为dtb保留了4M的虚拟地址空间。 当前fixmap的映射情况如上面图6所示,注意此时bm_pte中还没有填充,若要访问fixmap的虚拟地址,首先要填充bm_pte。
dtb映射的函数如下:
void *__init fixmap_remap_fdt(phys_addr_t dt_phys, int *size, pgprot_t prot) //(1)
{
const u64 dt_virt_base = __fix_to_virt(FIX_FDT); //(2)
int offset;
void *dt_virt;
offset = dt_phys % SWAPPER_BLOCK_SIZE;
dt_virt = (void *)dt_virt_base + offset;
create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE),
dt_virt_base, SWAPPER_BLOCK_SIZE, prot); //(3)
return dt_virt;
}
(1)入参dt_phys就是dtb的物理起始地址;
(2)获取fixmap中FIX_FDT的虚拟起始地址;
(3)调用建立映射函数create_mapping_noalloc。
create_mapping_noalloc函数主要调用了__create_pgd_mapping
static void __create_pgd_mapping(pgd_t *pgdir, phys_addr_t phys,
unsigned long virt, phys_addr_t size,
pgprot_t prot,
phys_addr_t (*pgtable_alloc)(int),
int flags)
{
unsigned long addr, end, next;
pgd_t *pgdp = pgd_offset_pgd(pgdir, virt);
phys &= PAGE_MASK;
addr = virt & PAGE_MASK;
end = PAGE_ALIGN(virt + size);
do {
next = pgd_addr_end(addr, end); // (1)
alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc,
flags); //(2)
phys += next - addr;
} while (pgdp++, addr = next, addr != end);
}
由于fixmap大小只有4M,所以共享同一个pgd项,这里获取的pgdp也就是图6中的pgdp。 循环就是在根据每一个pgd项创建pud页表
static void alloc_init_pud(pgd_t *pgdp, unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot,
phys_addr_t (*pgtable_alloc)(int),
int flags)
{
unsigned long next;
pud_t *pudp;
p4d_t *p4dp = p4d_offset(pgdp, addr);
p4d_t p4d = READ_ONCE(*p4dp);
pudp = pud_set_fixmap_offset(p4dp, addr); //(1)
do {
pud_t old_pud = READ_ONCE(*pudp);
next = pud_addr_end(addr, end);
if (use_1G_block(addr, next, phys) &&
(flags & NO_BLOCK_MAPPINGS) == 0) {
pud_set_huge(pudp, phys, prot);
BUG_ON(!pgattr_change_is_safe(pud_val(old_pud),
READ_ONCE(pud_val(*pudp))));
} else {
alloc_init_cont_pmd(pudp, addr, next, phys, prot,
pgtable_alloc, flags);
BUG_ON(pud_val(old_pud) != 0 &&
pud_val(old_pud) != READ_ONCE(pud_val(*pudp)));
}
phys += next - addr;
} while (pudp++, addr = next, addr != end);
pud_clear_fixmap();
}
这里有个关键点需要注意,就是对pudp的访问,首先看(1),pudp获得是什么
void __set_fixmap(enum fixed_addresses idx,
phys_addr_t phys, pgprot_t flags)
{
unsigned long addr = __fix_to_virt(idx); //(2)
pte_t *ptep;
ptep = fixmap_pte(addr); //(3)
if (pgprot_val(flags)) {
set_pte(ptep, pfn_pte(phys >> PAGE_SHIFT, flags)); //(4)
} else {
pte_clear(&init_mm, addr, ptep);
flush_tlb_kernel_range(addr, addr+PAGE_SIZE);
}
}
#define __set_fixmap_offset(idx, phys, flags) \
({ \
unsigned long ________addr; \
__set_fixmap(idx, phys, flags); \
________addr = fix_to_virt(idx) + ((phys) & (PAGE_SIZE - 1)); \
________addr; //(5) \
})
#define set_fixmap_offset(idx, phys) \
__set_fixmap_offset(idx, phys, FIXMAP_PAGE_NORMAL)
#define pud_offset_phys(dir, addr) (p4d_page_paddr(READ_ONCE(*(dir))) + pud_index(addr) * sizeof(pud_t)) //(1)
#define pud_set_fixmap(addr) ((pud_t *)set_fixmap_offset(FIX_PUD, addr))
#define pud_set_fixmap_offset(p4d, addr) pud_set_fixmap(pud_offset_phys(p4d, addr))
(1)计算出FIX_FDT对应的pud项地址,也就是图7中的entryU的物理地址
(2)获得FIX_PUD的虚拟地址
(3)计算FIX_PUD的虚拟地址对应的pte项
(4)将entryU的物理地址写入对应的pte项
(5)返回一个地址,这个地址是fixmap中FIX_PUD中的一个地址,在FIX_PUD中的偏移对应的是图7中entryU在bm_pud中的偏移
返回的地址就是图中的pudp,我们知道这是一个虚拟地址,如果页表没有相应的映射,访问是会失败的。在early_fixmap_init函数中,只是建立了一个映射的框架,并没有填充pte,所以直接访问pudp会失败。刚刚的过程就是在为其建立映射。
现在再来看访问地址pudp,mmu是怎么映射的,首先FIXMAP共享同一个pgd项和pud项,再看FIXMAP_START和FIX_PUD也属于同一个pmd项(一个pmd项对应2M空间,FIXMAP_START和FIX_PUD在同一个2M范围内),而刚刚又为pudp填充了对应的pte项,所以,这个地址是能够映射成功的,并且是将FIX_PUD与bm_pud建立起了映射。
同样对FIX_PMD建立映射后如图8所示
static void init_pmd(pud_t *pudp, unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot,
phys_addr_t (*pgtable_alloc)(int), int flags)
{
unsigned long next;
pmd_t *pmdp;
pmdp = pmd_set_fixmap_offset(pudp, addr); //(1)
do {
pmd_t old_pmd = READ_ONCE(*pmdp);
next = pmd_addr_end(addr, end);
if (((addr | next | phys) & ~SECTION_MASK) == 0 &&
(flags & NO_BLOCK_MAPPINGS) == 0) {
pmd_set_huge(pmdp, phys, prot); //(2)
} else {
alloc_init_cont_pte(pmdp, addr, next, phys, prot,
pgtable_alloc, flags);
}
phys += next - addr;
} while (pmdp++, addr = next, addr != end);
pmd_clear_fixmap(); //(3)
}
上述代码完成(1)后,就如图8所示;
(2)dtb的映射使能了2M的block映射,也就是最后一级为pmd,且其相应的entry会设置为2M对齐的物理地址与prot组合的值,所以会进入到这个分支; 完成后如图9所示,pte中设置的实际值上面的图中没有说明清楚,这里完善了下
(3)清FIX_PMD对应的pte项,因为最后一级已经设置在了pmd,所以pte项也就不再需要了,同样FIX_PUD对应的pte项也不需要了;完成后如图10所示
到这里就完成了对dtb的映射,就可以正常读取dtb的信息了,dtb中就包含了内存信息,地址范围,大小等等。然后就会初始化物理页面分配器,即初始化伙伴系统;有了物理页面分配器,内核主页表就可以建立动态映射页表。
五、内核主页表建立
内核主表的建立包含了细粒度内核镜像映射,线性映射。 主要在函数paging_init中完成
void __init paging_init(void)
{
pgd_t *pgdp = pgd_set_fixmap(__pa_symbol(swapper_pg_dir)); //(1)
map_kernel(pgdp); //内核细粒度映射
map_mem(pgdp); //线性映射
pgd_clear_fixmap();
cpu_replace_ttbr1(lm_alias(swapper_pg_dir));
init_mm.pgd = swapper_pg_dir;
memblock_free(__pa_symbol(init_pg_dir),
__pa_symbol(init_pg_end) - __pa_symbol(init_pg_dir));
memblock_allow_resize();
}
细粒度内核镜像映射
细粒度映射就是为内核的每个段建立页表
static void __init map_kernel(pgd_t *pgdp)
{
static struct vm_struct vmlinux_text, vmlinux_rodata, vmlinux_inittext,
vmlinux_initdata, vmlinux_data;
pgprot_t text_prot = rodata_enabled ? PAGE_KERNEL_ROX : PAGE_KERNEL_EXEC;
if (arm64_early_this_cpu_has_bti())
text_prot = __pgprot_modify(text_prot, PTE_GP, PTE_GP);
map_kernel_segment(pgdp, _text, _etext, text_prot, &vmlinux_text, 0,
VM_NO_GUARD);
map_kernel_segment(pgdp, __start_rodata, __inittext_begin, PAGE_KERNEL,
&vmlinux_rodata, NO_CONT_MAPPINGS, VM_NO_GUARD);
map_kernel_segment(pgdp, __inittext_begin, __inittext_end, text_prot,
&vmlinux_inittext, 0, VM_NO_GUARD);
map_kernel_segment(pgdp, __initdata_begin, __initdata_end, PAGE_KERNEL,
&vmlinux_initdata, 0, VM_NO_GUARD);
map_kernel_segment(pgdp, _data, _end, PAGE_KERNEL, &vmlinux_data, 0, 0);
if (!READ_ONCE(pgd_val(*pgd_offset_pgd(pgdp, FIXADDR_START)))) {
set_pgd(pgd_offset_pgd(pgdp, FIXADDR_START),
READ_ONCE(*pgd_offset_k(FIXADDR_START)));
} else if (CONFIG_PGTABLE_LEVELS > 3) {
pgd_t *bm_pgdp;
p4d_t *bm_p4dp;
pud_t *bm_pudp;
BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));
bm_pgdp = pgd_offset_pgd(pgdp, FIXADDR_START);
bm_p4dp = p4d_offset(bm_pgdp, FIXADDR_START);
bm_pudp = pud_set_fixmap_offset(bm_p4dp, FIXADDR_START);
pud_populate(&init_mm, bm_pudp, lm_alias(bm_pmd));
pud_clear_fixmap();
} else {
BUG();
}
kasan_copy_shadow(pgdp);
}
看函数map_kernel_segment
static void __init map_kernel_segment(pgd_t *pgdp, void *va_start, void *va_end,
pgprot_t prot, struct vm_struct *vma,
int flags, unsigned long vm_flags)
{
phys_addr_t pa_start = __pa_symbol(va_start);
unsigned long size = va_end - va_start;
__create_pgd_mapping(pgdp, pa_start, (unsigned long)va_start, size, prot,
early_pgtable_alloc, flags);
if (!(vm_flags & VM_NO_GUARD))
size += PAGE_SIZE;
vma->addr = va_start;
vma->phys_addr = pa_start;
vma->size = size;
vma->flags = VM_MAP | vm_flags;
vma->caller = __builtin_return_address(0);
vm_area_add_early(vma);
}
页表建立过程不再细说,这里在建立之前页表如上面图10所示 paging_init的第一步是为swapper_pg_dir的pgd表建立映射,也就是FIX_PGDswapper_pg_dir的pgd表建立映射,与swapper_pg_dir是完成内核映射的页表基地址。 注意此时pgdp已经指向了swapper_pg_dir,所以后面的pud页表、pmd页和pte表都需要动态创建。
线性映射
内核镜像映射仅仅是对内核镜像空间做了映射,为了方便内核自由访问所有物理内存,内核还做了一个线性映射。
static void __init map_mem(pgd_t *pgdp)
{
phys_addr_t kernel_start = __pa_symbol(_text);
phys_addr_t kernel_end = __pa_symbol(__init_begin);
phys_addr_t start, end;
int flags = 0;
u64 i;
if (rodata_full || debug_pagealloc_enabled())
flags = NO_BLOCK_MAPPINGS | NO_CONT_MAPPINGS;
memblock_mark_nomap(kernel_start, kernel_end - kernel_start);
/* map all the memory banks */
for_each_mem_range(i, &start, &end) {
if (start >= end)
break;
__map_memblock(pgdp, start, end, PAGE_KERNEL_TAGGED, flags);
}
__map_memblock(pgdp, kernel_start, kernel_end,
PAGE_KERNEL, NO_CONT_MAPPINGS);
memblock_clear_nomap(kernel_start, kernel_end - kernel_start);
}
核心就是__map_memblock
static void __init __map_memblock(pgd_t *pgdp, phys_addr_t start,
phys_addr_t end, pgprot_t prot, int flags)
{
__create_pgd_mapping(pgdp, start, __phys_to_virt(start), end - start,
prot, early_pgtable_alloc, flags);
}
具体的建立过程和内核镜像建立过程类似,完成映射后,基本如下图所示
完成映射后,将init_mm.pgd设置成swapper_pg_dir,同时将TTBR1寄存器改写成swapper_pg_dir,自此,后面内核的页表基地址就是swapper_pg_dir了。
六、用户空间页表创建
每个进程都有自己的用户空间,所以每个进程都有自己独立的页表。用户进程创建页表发生在三个时候:创建进程时,缺页异常时,进程切换时。
进程创建时
第一步,分配pgd物理页面 调用顺序copy_process->copy_mm->dup_mm->mm_init->mm_alloc_pgd
static inline int mm_alloc_pgd(struct mm_struct *mm)
{
mm->pgd = pgd_alloc(mm);
if (unlikely(!mm->pgd))
return -ENOMEM;
return 0;
}
pgd_t *pgd_alloc(struct mm_struct *mm)
{
gfp_t gfp = GFP_PGTABLE_USER;
if (PGD_SIZE == PAGE_SIZE)
return (pgd_t *)__get_free_page(gfp);
else
return kmem_cache_alloc(pgd_cache, gfp);
}
第二步,拷贝父进程页表,依次拷贝vma 调用路径 copy_process->copy_mm->dup_mm->dup_mmap->copy_page_range
int
copy_page_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma)
{
pgd_t *src_pgd, *dst_pgd;
unsigned long next;
unsigned long addr = src_vma->vm_start;
unsigned long end = src_vma->vm_end;
struct mm_struct *dst_mm = dst_vma->vm_mm;
struct mm_struct *src_mm = src_vma->vm_mm;
struct mmu_notifier_range range;
int ret;
ret = 0;
dst_pgd = pgd_offset(dst_mm, addr);
src_pgd = pgd_offset(src_mm, addr);
do {
next = pgd_addr_end(addr, end);
if (pgd_none_or_clear_bad(src_pgd))
continue;
if (unlikely(copy_p4d_range(dst_vma, src_vma, dst_pgd, src_pgd,
addr, next))) {
ret = -ENOMEM;
break;
}
} while (dst_pgd++, src_pgd++, addr = next, addr != end);
return ret;
}
copy_p4d_range里依次拷贝pud,pmd和pte。
缺页异常
页表拷贝完成后,等进程发生写时拷贝,触发缺页异常,在异常服务处理里真正分配物理页面。
static vm_fault_t wp_page_copy(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct mm_struct *mm = vma->vm_mm;
struct page *old_page = vmf->page;
struct page *new_page = NULL;
pte_t entry;
int page_copied = 0;
struct mmu_notifier_range range;
if (is_zero_pfn(pte_pfn(vmf->orig_pte))) {
new_page = alloc_zeroed_user_highpage_movable(vma,
vmf->address);
if (!new_page)
goto oom;
} else {
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,
vmf->address); //分配一个新的物理页面,并把old_page复制到new_page
if (!new_page)
goto oom;
if (!cow_user_page(new_page, old_page, vmf)) {
put_page(new_page);
if (old_page)
put_page(old_page);
return 0;
}
}
if (mem_cgroup_charge(new_page, mm, GFP_KERNEL))
goto oom_free_new;
cgroup_throttle_swaprate(new_page, GFP_KERNEL);
__SetPageUptodate(new_page);
mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma, mm,
vmf->address & PAGE_MASK,
(vmf->address & PAGE_MASK) + PAGE_SIZE);
mmu_notifier_invalidate_range_start(&range);
vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);
if (likely(pte_same(*vmf->pte, vmf->orig_pte))) {
if (old_page) {
if (!PageAnon(old_page)) {
dec_mm_counter_fast(mm,
mm_counter_file(old_page));
inc_mm_counter_fast(mm, MM_ANONPAGES);
}
} else {
inc_mm_counter_fast(mm, MM_ANONPAGES);
}
flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
entry = mk_pte(new_page, vma->vm_page_prot);
entry = pte_sw_mkyoung(entry);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
ptep_clear_flush_notify(vma, vmf->address, vmf->pte);
page_add_new_anon_rmap(new_page, vma, vmf->address, false);
lru_cache_add_inactive_or_unevictable(new_page, vma);
set_pte_at_notify(mm, vmf->address, vmf->pte, entry);
update_mmu_cache(vma, vmf->address, vmf->pte);
if (old_page) {
page_remove_rmap(old_page, false);
}
new_page = old_page;
page_copied = 1;
} else {
update_mmu_tlb(vma, vmf->address, vmf->pte);
}
if (new_page)
put_page(new_page);
pte_unmap_unlock(vmf->pte, vmf->ptl);
mmu_notifier_invalidate_range_only_end(&range);
if (old_page) {
if (page_copied && (vma->vm_flags & VM_LOCKED)) {
lock_page(old_page); /* LRU manipulation */
if (PageMlocked(old_page))
munlock_vma_page(old_page);
unlock_page(old_page);
}
put_page(old_page);
}
return page_copied ? VM_FAULT_WRITE : 0;
oom_free_new:
put_page(new_page);
oom:
if (old_page)
put_page(old_page);
return VM_FAULT_OOM;
}
进程切换
主要完成两件事
(1)设置进程的
ASID到TTBR1_EL1
(2)设置mm->pgd到TTBR0_EL1完成地址空间切换
调用路径:context_switch->switch_mm_irqs_off->switch_mm->__switch_mm->check_and_switch_context->cpu_switch_mm->cpu_do_switch_mm
void cpu_do_switch_mm(phys_addr_t pgd_phys, struct mm_struct *mm)
{
unsigned long ttbr1 = read_sysreg(ttbr1_el1);
unsigned long asid = ASID(mm);
unsigned long ttbr0 = phys_to_ttbr(pgd_phys);
/* Skip CNP for the reserved ASID */
if (system_supports_cnp() && asid)
ttbr0 |= TTBR_CNP_BIT;
/* SW PAN needs a copy of the ASID in TTBR0 for entry */
if (IS_ENABLED(CONFIG_ARM64_SW_TTBR0_PAN))
ttbr0 |= FIELD_PREP(TTBR_ASID_MASK, asid);
/* Set ASID in TTBR1 since TCR.A1 is set */
ttbr1 &= ~TTBR_ASID_MASK;
ttbr1 |= FIELD_PREP(TTBR_ASID_MASK, asid);
write_sysreg(ttbr1, ttbr1_el1); //(1)
isb();
write_sysreg(ttbr0, ttbr0_el1); //(2)
isb();
post_ttbr_update_workaround();
}
(1) ASID写到TTBR1_EL1;
(2) 新进程页表基地址pgd,填入ttbr0_el1