操作系统7(虚拟内存)

1 介绍
在之前,虚拟内存就写过一篇:https://blog.csdn.net/fanged/article/details/158703959。不过这一篇只是写了虚拟内存在ARM上的硬件支持,并没有写在操作系统中的应用和实现。这次就结合这个部分来写写。
首先还是回顾一下之前接触的虚拟内存的知识:
初始化
核心作用是Set up page translation tables and enable virtual memory,也就是创建映射表和使能。具体是在mmu_init中创建了paging数组,里面的内容就是PDE和PTE。之后将这个数组写入ttbr0_el1和ttbr1_el1寄存器(分别对应用户空间和内核),然后使能页表转换。
应用
应用的流程之前写过,这里直接引用一下即可。
1 启动:将 paging 数组(根页表)的物理地址加载到寄存器TTBR0。
2 索引(VPN):当访问虚拟地址时,MMU取出VA的高位作为索引,去查找页表。
3 查找(PDE/PTE):
如果查到的是 PDE(L1/L2 描述符),MMU 获得下一级表的地址,继续查。
如果查到的是 PTE(L3 描述符),MMU 获得PPN。
4 合成:MMU将PPN和虚拟地址的低12 位(Offset)拼接,得到最终能从内存条上拿数据的物理地址。
5 缓存(TLB):MMU把这次查到的结果存入TLB,下次访问同样的地址就不用再查页表了。
本质就是给了一个虚拟地址,如何转换成物理地址的流程。
2 代码分析
示例代码来自:https://github.com/s-matyukevich/raspberry-pi-os/tree/master/src/lesson06
这一篇也是这个系列目前最后的一篇。其实后面还有很多内容,比如文件系统,驱动等等。但是很遗憾,作者烂尾了。。。虽然还遗憾,但是对我还好,很多知识点都写过了。
这篇相对于lesson5,台阶没那么大,新增内容要相对少一些。根据上一节的预习,我们知道,MMU分为初始化和应用两个部分。那么就根据这两个部分来阅读。
2.1 内核层页表
直接在启动的boot.S里面,降级到el1就开始。
.globl _start
_start:
mrs x0, mpidr_el1
and x0, x0,#0xFF // Check processor id
cbz x0, master // Hang for all non-primary CPU
b proc_hang
proc_hang:
b proc_hang
master:
ldr x0, =SCTLR_VALUE_MMU_DISABLED
msr sctlr_el1, x0
ldr x0, =HCR_VALUE
msr hcr_el2, x0
ldr x0, =SCR_VALUE
msr scr_el3, x0
ldr x0, =SPSR_VALUE
msr spsr_el3, x0
adr x0, el1_entry
msr elr_el3, x0
eret
el1_entry:
adr x0, bss_begin
adr x1, bss_end
sub x1, x1, x0
bl memzero
bl __create_page_tables
2.1.1 创建表
调用__create_page_tables创建页表。整个代码如下:
.macro create_pgd_entry, tbl, virt, tmp1, tmp2
create_table_entry \tbl, \virt, PGD_SHIFT, \tmp1, \tmp2
create_table_entry \tbl, \virt, PUD_SHIFT, \tmp1, \tmp2
.endm
.macro create_table_entry, tbl, virt, shift, tmp1, tmp2
lsr \tmp1, \virt, #\shift
and \tmp1, \tmp1, #PTRS_PER_TABLE - 1 // table index
add \tmp2, \tbl, #PAGE_SIZE
orr \tmp2, \tmp2, #MM_TYPE_PAGE_TABLE
str \tmp2, [\tbl, \tmp1, lsl #3]
add \tbl, \tbl, #PAGE_SIZE // next level table page
.endm
.macro create_block_map, tbl, phys, start, end, flags, tmp1
lsr \start, \start, #SECTION_SHIFT
and \start, \start, #PTRS_PER_TABLE - 1 // table index
lsr \end, \end, #SECTION_SHIFT
and \end, \end, #PTRS_PER_TABLE - 1 // table end index
lsr \phys, \phys, #SECTION_SHIFT
mov \tmp1, #\flags
orr \phys, \tmp1, \phys, lsl #SECTION_SHIFT // table entry
9999: str \phys, [\tbl, \start, lsl #3] // store the entry
add \start, \start, #1 // next entry
add \phys, \phys, #SECTION_SIZE // next block
cmp \start, \end
b.ls 9999b
.endm
__create_page_tables:
mov x29, x30 // save return address
adrp x0, pg_dir
mov x1, #PG_DIR_SIZE
bl memzero
adrp x0, pg_dir
mov x1, #VA_START
create_pgd_entry x0, x1, x2, x3
/* Mapping kernel and init stack*/
mov x1, xzr // start mapping from physical offset 0
mov x2, #VA_START // first virtual address
ldr x3, =(VA_START + DEVICE_BASE - SECTION_SIZE) // last virtual address
create_block_map x0, x1, x2, x3, MMU_FLAGS, x4
/* Mapping device memory*/
mov x1, #DEVICE_BASE // start mapping from device base address
ldr x2, =(VA_START + DEVICE_BASE) // first virtual address
ldr x3, =(VA_START + PHYS_MEMORY_SIZE - SECTION_SIZE) // last virtual address
create_block_map x0, x1, x2, x3, MMU_DEVICE_FLAGS, x4
mov x30, x29 // restore return address
ret
这里使用了嵌套宏来构建 ARM64 的多级页表(PGD -> PUD -> PMD/Section),可以看到,和之前的教程还是不同,之前是PDE/PTE。这个源自 x86(32位)架构或者是比较简化的二级页表教程。
区别:
32位系统 (PDE/PTE):地址空间只有 2^32 (4GB)。使用两级页表(10+10+12 位拆分)就足以覆盖全范围。
64位系统 (PGD/PUD/PMD/PTE):理论地址空间高达 2^64。即便 ARM64 实际通常只使用39位或48位地址线,两级页表也完全不够用。如果只用两级,单张页表会变得极其巨大(几百 GB),这在物理内存里根本存不下。
不过在这里的内核态代码中,也进行了简化。在应用层中的普通映射:PGD -> PUD -> PMD -> PTE (一般4KB)。
虚拟地址到物理page映射完整的示意如下:

不过这里内核层代码直接是段映射 (Block Mapping):PGD -> PUD -> PMD (直接指向 2MB 物理块)。简化了一级。
入口是__create_page_tables
这里首先用到的是pg_dir,这个是在link.ld里面定义的。基本上是在整个分区的最后。
SECTIONS
{
. = 0xffff000000000000;
.text.boot : { *(.text.boot) }
. = ALIGN(0x00001000);
user_begin = .;
.text.user : { build/user* (.text) }
.rodata.user : { build/user* (.rodata) }
.data.user : { build/user* (.data) }
.bss.user : { build/user* (.bss) }
user_end = .;
.text : { *(.text) }
.rodata : { *(.rodata) }
.data : { *(.data) }
. = ALIGN(0x8);
bss_begin = .;
.bss : { *(.bss*) }
bss_end = .;
. = ALIGN(0x00001000);
pg_dir = .;
.data.pgd : { . += (3 * (1 << 12)); }
}
VA_START是一个宏,0xffff000000000000,在内核中,就是将物理地址做一个填充,就成为虚拟地址。将pg_dir和VA_START作为参数传给create_pgd_entry。
.macro create_pgd_entry, tbl, virt, tmp1, tmp2
create_table_entry \tbl, \virt, PGD_SHIFT, \tmp1, \tmp2
create_table_entry \tbl, \virt, PUD_SHIFT, \tmp1, \tmp2
.endm
可以看到,这里是用的create_table_entry创建。
.macro create_table_entry, tbl, virt, shift, tmp1, tmp2
lsr \tmp1, \virt, #\shift
and \tmp1, \tmp1, #PTRS_PER_TABLE - 1 // table index
add \tmp2, \tbl, #PAGE_SIZE
orr \tmp2, \tmp2, #MM_TYPE_PAGE_TABLE
str \tmp2, [\tbl, \tmp1, lsl #3]
add \tbl, \tbl, #PAGE_SIZE // next level table page
.endm
这里的页表都是使用汇编创建,不过据说linux这些还是用的C创建。
上面涉及到的宏定义如下:
#define PAGE_SHIFT 12
#define TABLE_SHIFT 9
#define PGD_SHIFT PAGE_SHIFT + 3*TABLE_SHIFT
#define PUD_SHIFT PAGE_SHIFT + 2*TABLE_SHIFT
#define PMD_SHIFT PAGE_SHIFT + TABLE_SHIFT
#define PTRS_PER_TABLE (1 << TABLE_SHIFT)
这里的PAGE_SHIFT 12,表示了一个页面的大小,2的12次方, 4096,即标准的4KB页面。ABLE_SHIFT 9,表示每一级页表包含的条目。这里就是2的9次方,也就是512个。
PMD_SHIFT (Level 2),这里是12+9=21,也就是2MB,表示了一个PMD的空间。PUD_SHIFT (Level 1)是30,也就是1GB,表示一个PUD表示1GB空间。最后的PGD_SHIFT (Level 0)是39,表示512GB,表示整个虚拟地址空间有效位是 39 位(即所谓的39-bit VA)。
这里创建了一个表的基本结构:
| 物理地址 | 页表层级 (Level) | 存放内容 |
| pg_dir + 0 | PGD (Level 0) | 包含一个指向 PUD 的指针 (描述符) |
| pg_dir + 4KB | PUD (Level 1) | 包含一个指向 PMD 的指针 (描述符) |
| pg_dir + 8KB | PMD (Level 2) | 目前为空(等待后续 create_block_map 填入物理地址) |
2.1.2 填充内容
使用的create_block_map填充PMD的内容。该函数将一段连续的虚拟地址映射到连续的物理地址上。这里使用的是 Section Mapping(段映射),即 2MB 的大页。代码如下:
/* Mapping kernel and init stack*/
mov x1, xzr // start mapping from physical offset 0
mov x2, #VA_START // first virtual address
ldr x3, =(VA_START + DEVICE_BASE - SECTION_SIZE) // last virtual address
create_block_map x0, x1, x2, x3, MMU_FLAGS, x4/* Mapping device memory*/
mov x1, #DEVICE_BASE // start mapping from device base address
ldr x2, =(VA_START + DEVICE_BASE) // first virtual address
ldr x3, =(VA_START + PHYS_MEMORY_SIZE - SECTION_SIZE) // last virtual address
create_block_map x0, x1, x2, x3, MMU_DEVICE_FLAGS, x4.macro create_block_map, tbl, phys, start, end, flags, tmp1
lsr \start, \start, #SECTION_SHIFT
and \start, \start, #PTRS_PER_TABLE - 1 // table index
lsr \end, \end, #SECTION_SHIFT
and \end, \end, #PTRS_PER_TABLE - 1 // table end index
lsr \phys, \phys, #SECTION_SHIFT
mov \tmp1, #\flags
orr \phys, \tmp1, \phys, lsl #SECTION_SHIFT // table entry
9999: str \phys, [\tbl, \start, lsl #3] // store the entry
add \start, \start, #1 // next entry
add \phys, \phys, #SECTION_SIZE // next block
cmp \start, \end
b.ls 9999b
.endm
大概含义就是:
1 计算起始和结束索引。
2 构造 Entry(物理地址 + 属性位 flags)。
3 循环填充页表项,直到覆盖从 start 到 end 的范围。也就是填充完整个PMD。
填充完成后的表格如下:
| 物理地址 (PA) | 逻辑名称 | 填入的内容 (Entry 内容) | 指向哪里 |
| 0x1000 | PGD (L0) | `0x2000 | MM_TYPE_PAGE_TABLE` |
| 0x2000 | PUD (L1) | `0x3000 | MM_TYPE_PAGE_TABLE` |
| 0x3000 | PMD (L2) | `0x000000 | MMU_FLAGS` |
| 0x3008 | PMD (L2) | `0x200000 | MMU_FLAGS` |
| ... | ... | ... | ... |
| 0x3xxx | PMD (L2) | `0x3F000000 | MMU_DEVICE_FLAGS` |
最后将pg_dir写入ttbr1_el1寄存器,之后内核态的查表,就是查的上面创建的表。
至于应用层的表创建,是set_pgd函数完成,这个下面介绍。。。
2.2 应用层页表
首先还是创建内核进程copy_process。
和lesson在内存管理商的第一个区别,之前的childregs->sp = stack + PAGE_SIZE;p->stack = stack;换成了copy_virt_memory(p);实现如下:
int copy_virt_memory(struct task_struct *dst) {
struct task_struct* src = current;
for (int i = 0; i < src->mm.user_pages_count; i++) {
unsigned long kernel_va = allocate_user_page(dst, src->mm.user_pages[i].virt_addr);
if( kernel_va == 0) {
return -1;
}
memcpy(kernel_va, src->mm.user_pages[i].virt_addr, PAGE_SIZE);
}
return 0;
}
这里就是遍历父进程的页表,然后重新申请,最后将内容拷贝过来。
在真正的 Linux 内核中,这里通常会使用 写时复制 (Copy-on-Write, COW) 技术。
fork时不拷贝内容,只把页表设为只读并共享物理页。只有当有人想写数据时,硬件触发异常,内核才在那一刻进行真正的memcpy。
之后就是将内核进程切换到用户进程,使用move_to_user_mode实现。
int move_to_user_mode(unsigned long start, unsigned long size, unsigned long pc)
{
struct pt_regs *regs = task_pt_regs(current);
regs->pstate = PSR_MODE_EL0t;
regs->pc = pc;
regs->sp = 2 * PAGE_SIZE;
unsigned long code_page = allocate_user_page(current, 0);
if (code_page == 0) {
return -1;
}
memcpy(code_page, start, size);
set_pgd(current->mm.pgd);
return 0;
}
重要的首先还是分配了用户页allocate_user_page。
unsigned long allocate_user_page(struct task_struct *task, unsigned long va) {
unsigned long page = get_free_page();
if (page == 0) {
return 0;
}
map_page(task, va, page);
return page + VA_START;
}
这里做的就是创建了用户空间的页表。使用map_page:
void map_page(struct task_struct *task, unsigned long va, unsigned long page){
unsigned long pgd;
if (!task->mm.pgd) {
task->mm.pgd = get_free_page();
task->mm.kernel_pages[++task->mm.kernel_pages_count] = task->mm.pgd;
}
pgd = task->mm.pgd;
int new_table;
unsigned long pud = map_table((unsigned long *)(pgd + VA_START), PGD_SHIFT, va, &new_table);
if (new_table) {
task->mm.kernel_pages[++task->mm.kernel_pages_count] = pud;
}
unsigned long pmd = map_table((unsigned long *)(pud + VA_START) , PUD_SHIFT, va, &new_table);
if (new_table) {
task->mm.kernel_pages[++task->mm.kernel_pages_count] = pmd;
}
unsigned long pte = map_table((unsigned long *)(pmd + VA_START), PMD_SHIFT, va, &new_table);
if (new_table) {
task->mm.kernel_pages[++task->mm.kernel_pages_count] = pte;
}
map_table_entry((unsigned long *)(pte + VA_START), va, page);
struct user_page p = {page, va};
task->mm.user_pages[task->mm.user_pages_count++] = p;
}
unsigned long map_table(unsigned long *table, unsigned long shift, unsigned long va, int* new_table) {
unsigned long index = va >> shift;
index = index & (PTRS_PER_TABLE - 1);
if (!table[index]){
*new_table = 1;
unsigned long next_level_table = get_free_page();
unsigned long entry = next_level_table | MM_TYPE_PAGE_TABLE;
table[index] = entry;
return next_level_table;
} else {
*new_table = 0;
}
return table[index] & PAGE_MASK;
}
这里的基本功能和内核层的汇编功能一致。但是这里实现的是Page Mapping(4KB 页)。完整实现了 PGD -> PUD -> PMD -> PTE。
这里的pgd + VA_START,就是把物理地址pgd,通过偏移转换成虚拟地址。这个也仅限在内核中的操作。
最后通过set_pgd将页表地址设置到寄存器ttbr0_el1。
set_pgd:
msr ttbr0_el1, x0
tlbi vmalle1is
DSB ISH // ensure completion of TLB invalidation
isb
ret
假设get_free_page的地址是0x100000,此时生成的表结构如下:
| 物理地址 (PA) | 表类型 | 条目索引 (Index) | 条目内容 (Entry Value) | 含义解释 |
| 0x100000 | PGD | [0] |
`0x101000 | 0x3` |
| 0x101000 | PUD | [0] |
`0x102000 | 0x3` |
| 0x102000 | PMD | [0] |
`0x103000 | 0x3` |
| 0x103000 | PTE | [0] |
`0x400000 | FLAGS` |
2.3 应用
运行时,整体流程和lesson5没有太大区别。在内核中创建一个user_process,之后将它切换到用户态。
void user_process()
{
call_sys_write("User process\n\r");
int pid = call_sys_fork();
if (pid < 0) {
call_sys_write("Error during fork\n\r");
call_sys_exit();
return;
}
if (pid == 0){
loop("abcde");
} else {
loop("12345");
}
}
在这里,首先打印User process,之后fork了这个进程,然后循环打印abcde和12345 。
3 代码运行
输出:
Kernel process started. EL 1
User process
12345123451234512abcdeabcdeabcdeabcdeab34512345123451234cdeabcdeabcdeabcdeabcd51234

打印的速度比之前慢很多。
4 调试
4.1 内核页表创建
在代码分配内存的地方增加了printf,但是对于内核态的页表的打印,只能写函数去读取,内存地址是根据log填的,因为运行时已经开启了MMU,所以增加了0xffff000000000000偏移。具体的函数内容如下:
void dump_page_tables() {
unsigned long ttbr1;
asm volatile("mrs %0, ttbr1_el1" : "=r" (ttbr1));
printf("TTBR1_Low: %u\n", (unsigned int)ttbr1);
unsigned long manual_pgd_va = 0x84000 + 0xffff000000000000;
unsigned long *manual_ptr = (unsigned long *)manual_pgd_va;
// 打印前 8 个条目(看看 0x3 属性位)
for (int i = 0; i < 1; i++) {
unsigned int low = (unsigned int)(manual_ptr[i] & 0xFFFFFFFF);
printf("PGD Index [%d] Low_32: %u\r\n", i, low);
}
unsigned long *pud_ptr = (unsigned long *)(0x85000 + 0xffff000000000000);
unsigned int pud_low = (unsigned int)(pud_ptr[0] & 0xFFFFFFFF);
printf("PUD Index [0] Low_32: %u\r\n", pud_low);
unsigned long *pmd_ptr = (unsigned long *)(0x86000 + 0xffff000000000000);
printf("PMD Index [0] Low_32: %u\r\n", (unsigned int)(pmd_ptr[0] & 0xFFFFFFFF));
for (int i = 1; i < 512; i++) {
if (pmd_ptr[i] != 0) {
//前面几百个条目是有规律递增的
printf("PMD Index [%d] Low_32: %u\r\n",i, (unsigned int)(pmd_ptr[i] & 0xFFFFFFFF));
}
}
}
内核页表打印结果如下:
dump_page_tables
TTBR1_Low: 540672 (0x84000)
PGD Index [0] Low_32: 544771 (0x85003)
PUD Index [0] Low_32: 548867(0x86003)
PMD Index [0] Low_32: 1029(0x405)
PMD Index [1] Low_32: 2098181
PMD Index [2] Low_32: 4195333
PMD Index [3] Low_32: 6292485
PMD Index [4] Low_32: 8389637
PMD Index [5] Low_32: 10486789
PMD Index [6] Low_32: 12583941
PMD Index [7] Low_32: 14681093
PMD Index [8] Low_32: 16778245
PMD Index [9] Low_32: 18875397...
生成的页表结构如下,不过这里只有一个PGD,一个PUD,PMD是有一组,后面没有PTE了,直接接的2MB的block。

页表的定义如下:

根据打印的内核页表信息和上面的页表结构,整理结果如下:
| 步骤 | 寄存器/条目 | 物理地址 | 关键值 (Hex) | 动作 |
| 起点 | TTBR1_EL1 |
0x84000 |
- | 告诉 CPU 去 528KB 处找 PGD |
| 第一级 | PGD[0] |
0x84000 |
0x85003 |
发现 0x3,跳往 PUD (0x85000) |
| 第二级 | PUD[0] |
0x85000 |
0x86003 |
发现 0x3,跳往 PMD (0x86000) |
| 第三级别 | PMD[0] |
0x86000 |
0x405 |
映射成功! 对应物理地址 0x0 |
PMD[1] |
0x200405 |
这里PGD的0x3就是属性,0x3也就是11,最后两个一个是Present,一个是Read/Write。表示该页存在于内存中 (Present) 且用户拥有读写权限 (Writable)。硬件看到这最后两位,知道这是一个 Table Descriptor(指向下一级页表)。
最后的各种flag含义如下:
| 十六进制 | 二进制 | 含义 |
0x1 |
0001 |
存在 (Present),但只读。 |
0x3 |
0011 |
存在 (Present),可读可写。 |
0x5 |
0101 |
存在,只读,且已访问 (Accessed)(位 2 是 User/Supervisor 位)。 |
0x7 |
0111 |
存在,可读写,且用户态可访问。 |
至于PDE,flag定义和上面也是一样。
最后的1029,也就是0x405,PMD(Level 2)的 Block Descriptor(块描述符)中,低位 (Bit 0 - 11):被保留用来存放属性(Attributes)和类型(Type)。高位 (Bit 12 - 47):才是真正的输出物理地址(Output Address)
0x400是Bit 10 (AF - Access Flag):访问标志位。表示这个内存已经访问过。最低两位是 01,表示这是一个block。
整体的LOG显示有个问题,作者源代码里面带的printf无法完整打印0xffff000000000000这种64位的地址,应该是只打印最后32位。
4.2 应用层页表创建
...
copy_process page 4194304
Kernel process started. EL 1
allocate_user_page page 4198400
11111111111111
map_page task->mm.pgd 4202496
map_table next_level_table 4206592
map_page pud 4206592
map_table next_level_table 4210688
map_page pmd 4210688
map_table next_level_table 4214784
map_page pte 4214784
move_to_user_mode code_page 4198400
map_page task->mm.pgd 4202496
map_page pud 4206592
map_page pmd 4210688
map_page pte 4214784
User process
copy_process page 4222976
allocate_user_page page 4227072
11111111111111
map_page task->mm.pgd 4231168
map_table next_level_table 4235264
map_page pud 4235264
map_table next_level_table 4239360
map_page pmd 4239360
map_table next_level_table 4243456
map_page pte 4243456
copy_virt_memory kernel_va 4227072
allocate_user_page page 4247552
map_page task->mm.pgd 4231168
map_page pud 4235264
map_page pmd 4239360
map_page pte 4243456
copy_virt_memory kernel_va 4247552
12345123451234512abcdeabcdeabcdeabcdeab3451234512345123cdeabcde
代码中首先调用的是copy_process里面的分类内核页,
copy_process page 4194304: 内核分配了一个 4KB 页面(物理地址 4194304,即 0x400000)作为 task_struct 结构体。
之后是将内核的进程切换到用户层。move_to_user_mode(begin, end - begin, process - begin);
在这里增加打印,是这样的、Kernel process move_to_user_mode. begin 4096 end 4398 process 4188。这里对应的是link的位置。
SECTIONS
{
. = 0xffff000000000000;
.text.boot : { *(.text.boot) }
. = ALIGN(0x00001000);
user_begin = .;
.text.user : { build/user* (.text) }
.rodata.user : { build/user* (.rodata) }
.data.user : { build/user* (.data) }
.bss.user : { build/user* (.bss) }
user_end = .;
.text : { *(.text) }
.rodata : { *(.rodata) }
.data : { *(.data) }
. = ALIGN(0x8);
bss_begin = .;
.bss : { *(.bss*) }
bss_end = .;
. = ALIGN(0x00001000);
pg_dir = .;
.data.pgd : { . += (3 * (1 << 12)); }
}
在这里,就是把 .text.user : { build/user* (.text) } .rodata.user : { build/user* (.rodata) } .data.user : { build/user* (.data) } .bss.user : { build/user* (.bss) }这些全部拷贝到了用户层的页表中。之后将user_process的起始位置传给pc寄存器。可以看到,此时的process 4188是位于begin4096和end4398之间。
之后调用allocate_user_page page 4198400: 为用户代码分配了第一块物理内存页(0x401000)。
之后就类似内核中的页表,开始创建用户层页表。对应的代码如下:
if (!task->mm.pgd) {
task->mm.pgd = get_free_page();
task->mm.kernel_pages[++task->mm.kernel_pages_count] = task->mm.pgd;
}
pgd = task->mm.pgd;
int new_table;
unsigned long pud = map_table((unsigned long *)(pgd + VA_START), PGD_SHIFT, va, &new_table);
if (new_table) {
task->mm.kernel_pages[++task->mm.kernel_pages_count] = pud;
}
unsigned long pmd = map_table((unsigned long *)(pud + VA_START) , PUD_SHIFT, va, &new_table);
if (new_table) {
task->mm.kernel_pages[++task->mm.kernel_pages_count] = pmd;
}
unsigned long pte = map_table((unsigned long *)(pmd + VA_START), PMD_SHIFT, va, &new_table);
if (new_table) {
task->mm.kernel_pages[++task->mm.kernel_pages_count] = pte;
}
map_table_entry((unsigned long *)(pte + VA_START), va, page);
可以看到,用户层这里比起内核页表的区别是多了一层PTE。PTE中再去映射4KB的页。
-
map_page task->mm.pgd 4202496: 分配 PGD 根表4202496(0x402000)。 -
map_table 4206592: 分配 PUD 表4206592(0x403000)。 -
map_table 4210688: 分配 PMD 表4210688(0x404000)。 -
map_table 4214784: 分配 PTE 表4214784(0x405000)。
最后有一个函数去创建映射page页。
map_table_entry((unsigned long *)(pte + VA_START), va, page);
void map_table_entry(unsigned long *pte, unsigned long va, unsigned long pa) {
unsigned long index = va >> PAGE_SHIFT;
index = index & (PTRS_PER_TABLE - 1);
unsigned long entry = pa | MMU_PTE_FLAGS;
pte[index] = entry;
}
这里是填充PTE页表项,这里只有一个,就是PTE[0]=0x401447(4199495)。
这里的entry = pa | MMU_PTE_FLAGS,去掉低三位,此时是0x401000,也就是在函数中分配的物理page。0x447是标志位,也就是0100 0100 0111,最后的111在上面说过,表示存在,可读写,且用户态可访问。
也就是说,此时创建了0x0000000000000000对应到0x401000的映射。
之后又调用了map_page,触发了缺页do_mem_abort。原因是作者特意在move_to_user_mode这里使用了2倍pagesize,后面会触发中断。
int move_to_user_mode(unsigned long start, unsigned long size, unsigned long pc)
{
struct pt_regs *regs = task_pt_regs(current);
regs->pstate = PSR_MODE_EL0t;
regs->pc = pc;
regs->sp = 2 * PAGE_SIZE;
unsigned long code_page = allocate_user_page(current, 0);
if (code_page == 0) {
return -1;
}
memcpy(code_page, start, size);
set_pgd(current->mm.pgd);
return 0;
}
处理缺页中断的函数如下:
int do_mem_abort(unsigned long addr, unsigned long esr) {
printf("do_mem_abort\r\n");
unsigned long dfs = (esr & 0b111111);
if ((dfs & 0b111100) == 0b100) {
unsigned long page = get_free_page();
if (page == 0) {
return -1;
}
map_page(current, addr & PAGE_MASK, page);
ind++;
if (ind > 2){
return -1;
}
return 0;
}
return -1;
}
这里首先判断是不是缺页0b100,之后用ind避免死循环。
整个页表没有重建,只是在最后PTE中增加了一项。PTE[1]=0x406447(4219975)。
根据上面的分析,447是标志位,这里的物理页是0x406000。也就说虚拟地址0x0000000000001000对应到了0x406000。
之后的代码是在用户态fork第二个进程。如下:
void user_process()
{
call_sys_write("User process\n\r");
int pid = call_sys_fork();
if (pid < 0) {
call_sys_write("Error during fork\n\r");
call_sys_exit();
return;
}
if (pid == 0){
loop("abcde");
} else {
loop("12345");
}
}
在fork的时候,如果发现是用户层状态下,此时会调用copy_virt_memory,复制自身的物理页和页表。
int copy_virt_memory(struct task_struct *dst) {
struct task_struct* src = current;
for (int i = 0; i < src->mm.user_pages_count; i++) {
unsigned long kernel_va = allocate_user_page(dst, src->mm.user_pages[i].virt_addr);
if( kernel_va == 0) {
return -1;
}
memcpy(kernel_va, src->mm.user_pages[i].virt_addr, PAGE_SIZE);
}
return 0;
}
在这里又会调用allocate_user_page,触发map_page,这个流程和上面一致,就不多写了。
因为src->mm.user_pages_count此时已经是2了,就是由上面缺页多分配了一个,所以整体流程是循环了两次。两个过程中PGD(0x409000),PUD(0x40A000),PMD(0x40B000),PTE(0x40C000)都完全一致,只是在PTE中增加了一个条目。第二个进程中PTE中两个内容如下(0x408447)4228167和(0x40D447)4248647,也就是0x408000和0x40D000。
| 调用批次 | 目标虚拟地址 (VA) | 目标物理页 (PA) | 操作性质 |
| 第一波 | 0x0 (代码段) |
0x408000 | 开荒:创建了全套页表,填入了第一个物理页。 |
| 第二波 | 0x1000 (栈/异常页) |
0x40D000 | 填坑:发现路都通了,直接在现成的PTE表里填入第二个物理页。 |
到现在整个流程就结束了。创建了两个用户态的进程,并且使用虚拟地址。
4.3 实验
在user进程中增加一个栈上的变量data,打印地址。在子进程中修改data,看看两个进程中的变量data是不是独立的。
char data = 'A';
int pid = call_sys_fork();
if (pid == 0)
call_sys_write("Son process ");
else
call_sys_write("father process ");
char buf[20] = {0};
hex_to_str((unsigned long)buf, buf);
call_sys_write(buf);
if (pid == 0) {
data = 'C'; // 子进程修改自己物理页里的内容
while(1) {
char msg[2] = {data, '\0'};
call_sys_write(msg); // 应该一直打 'C'
user_delay(1000000);
}
} else {
user_delay(500000); // 稍微等子进程改完
while(1) {
char msg[20] = {data, '\0'};
call_sys_write(msg); // 应该一直打 'A'
user_delay(1000000);
}
}
}
得到的log如下:
Kernel process started. EL 1
allocate_user_page page 4198400
11111111111111
map_page task->mm.pgd 4202496
map_table next_level_table 4206592
map_table table[index] 0 4206595
map_page pud 4206592
map_table next_level_table 4210688
map_table table[index] 0 4210691
map_page pmd 4210688
map_table next_level_table 4214784
map_table table[index] 32 4214787
map_page pte 4214784
map_table_entry index 0 entry4199495
allocate_user_page page 4218880
map_page task->mm.pgd 4202496
map_page pud 4206592
map_page pmd 4210688
map_table next_level_table 4222976
map_table table[index] 0 4222979
map_page pte 4222976
map_table_entry index 0 entry4219975
move_to_user_mode code_page 4218880
do_mem_abort
do_mem_abort page 4227072
map_page task->mm.pgd 4202496
map_page pud 4206592
map_page pmd 4210688
map_page pte 4222976
map_table_entry index 1 entry4228167
User process
copy_process page 4231168
allocate_user_page page 4235264
11111111111111
map_page task->mm.pgd 4239360
map_table next_level_table 4243456
map_table table[index] 0 4243459
map_page pud 4243456
map_table next_level_table 4247552
map_table table[index] 0 4247555
map_page pmd 4247552
map_table next_level_table 4251648
map_table table[index] 32 4251651
map_page pte 4251648
map_table_entry index 0 entry4236359
copy_virt_memory kernel_va 4235264
allocate_user_page page 4255744
map_page task->mm.pgd 4239360
map_page pud 4243456
map_page pmd 4247552
map_table next_level_table 4259840
map_table table[index] 0 4259843
map_page pte 4259840
map_table_entry index 0 entry4256839
copy_virt_memory kernel_va 4255744
allocate_user_page page 4263936
map_page task->mm.pgd 4239360
map_page pud 4243456
map_page pmd 4247552
map_page pte 4259840
map_table_entry index 1 entry4265031
copy_virt_memory kernel_va 4263936
father process 0x0000000000001FE0
AAAAAAAAAAAAAAAAAAAA_schedule
Son process 0x0000000000001FE0
CCCCCCCCCCCCCCCCCCCCCC_schedule
_schedule
AAAAAAAAAAAAAAAAAAAAAA_schedule
CCCCCCCCCCCCCCCCCCCCCC_schedule
_schedule
AAAAAAAAAAAAAAAAAAAAAA_schedule
CCCCCCCCCCCCCCCCCCCCCC_schedule
_schedule
AAAAAAAAAAAAAT
分析log可以看到,父子进程的buf地址都是0x0000000000001FE0。但是对应的物理地址却不同。在子进程中修改了data,并不会影响到父进程。
| 进程 | 虚拟地址 (完全一样) | 独有物理页基址 | 最终 buf 物理地址 |
|---|---|---|---|
| 父进程 | 0x4000FE0 |
4239360 | 4239360/ 0x40B000 |
| 子进程 | 0x4000FE0 |
4263936 | 4251648 / 0x411000 |
这部分看着简单,实际代码调了好几天了,稍微一点没对,就是一个报错,大概这样的IRQ_INVALID_EL0_32, ESR: 82000007, address: 1fe0,都要把我搞得麻木了。
好了,整个流程,大概就是这样。。。
参考:
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)