Before Lab
在做实验之前,我们需要做以下步骤:
注意一定要合并 lab1
,否则会导致机器无法启动
配置内核启动页表
在 kernel/arch/aarch64/boot/raspi3/init/mmu.c
中配置内核的地址映射,想法是很朴素的,与 xv6
的做法相似,将 va = 0xffff_ff00_0000_0000 + addr
映射到了 pa = addr
的位置,可以引申 xv6
的映射方式:
但 aarch64
方便的一点在于他有两个页表寄存器,risc-v
只有一个 satp
,我们通过如下配置:
如果一开始不会写,没关系,可以看看上面几行里是如何配置用户进程的页表的,我们的做法仅仅是将虚拟地址变为高地址,并配置到内核的页表寄存器中
伙伴系统
请看完银杏书再来写这个实验,否则体验会很差
但在这里还是简单介绍以下伙伴系统。
实际上想法很简单,我们维护一个数组(假定全局只有这一个),这个数组的索引表示阶(i.e. order
),可以看作是物理块的大小(2order),数组的每一项是一个struct
,这个结构体维护了一个链表和链表的长度(或许我们就可以把它当作是一个链表),链表维护了物理内存中大小为 2order 的物理块。
而所谓的伙伴系统,实际上我们可以简化成如下两个函数:
- 操作系统申请了一块大小为
m
的内存,我们首先定阶,找到最适合的一块物理块,如果没有,那么我们向上找,并将大的不断均分,直到找到最合适的,在均分的过程中,我们需要将分离出来的物理块放到对应的链表中去。
- 操作系统需要回收一块大小为
m
的内存,我们首先需要找到这块内存的伙伴(不用担心,这部分已经写好了函数),由于伙伴的大小和这块内存是一样的,所以我们只需要不断向上合并,直到无法合并为止。
当然,还有实现上的很多细节没有说明,但有这个思路后已经不难了。我们首先来实现第一个功能
buddy_get_pages
注意这里实现上的细节:
- 检查当前需要分配的内存是否大于最大的块(检查
order
的大小即可)
- 由于维护了链表,我们需要用到
common/list.h
中的函数,包括 list_add
, list_del
, list_entry
- 找到了合适大小的块(或者需要分裂的块)后,我们通过分裂函数进行修正,保证其是最合适的块
allocated
置为 1
,表面这个块已经被分配
接着,我们开始完善 split_page
,这个函数需要做的事情是很简单的,只需要不断的分裂即可,但需要在分裂时维护链表,我们直接给出实现:
需要注意的细节:
- 首先我们需要检查分裂的块是否被分配了,如果被分配了那么显然暂时不能分配
- 通过
get_buddy_chunk
获得分裂后的块的另一半,将这个 buddy
放到对应的空闲链表中去(记得维护贡献出大物理块的链表)
buddy_free_pages
第二个功能就很简单了,实际上就是第一个的逆向:
注意这一步: page = page < buddy ? page : buddy
我们并不知道找到的伙伴哪个的地址更低,但我们需要保证,我们合并进链表时,一定是低地址在前,换而言之,我们总是把高地址放在低地址后面(这是很显然的事情)
做完这一步后,我们可以输入 make qemu
,如果没有出现 BUG
停顿的话,说明 kmalloc
已经正常工作了(也就是你的伙伴系统已经正确了)
这里有一个奇怪的 bug
,当你做完 buddy system
后,测试似乎不会停下来,我甚至跑了 10 分钟的测试,他都没停下来,但输入 make grade
的话就又正常了,这个 bug
会导致页表管理部分没办法 debug
,只能肉眼差错。
页表管理
一定一定去提前看 page_table.h
和文档后再来做这部分
query_in_pgtbl
这个函数是 trival
的,在 xv6
中也做过这个函数的实现,简单来说,我们通过 get_next_ptp
来找到下一级页表,直到找到最后的 PTE
,然后通过 offset
来获取 pa
但在这里,我们需要注意:
- L1 与 L2 页表可以直接指向物理块,因此我们需要对这部分进行判断
- 记得看
get_next_ptp
的实现还有它的注释,我们需要用到 virt_to_phys
这个函数,如果不看的话就不知道,
请注意高亮位置(可能并不是很亮),这里使用了 return
而非 break
,但注意如果你使用了 break
,它甚至不会报错(除非你做到了最后一个测试点才会报错),我的建议是在这里使用 goto
,就像我在下面做的一样。
(un)map_range_in_pgtbl
以 map_range_in_pgtbl
为例,我们的做法是显然的:
- 找到最后的
PTE
- 将
pa
的偏移量写入这个 PTE
的 PFN
中
- 设置
PTE
的 flags
于是,代码如下:
注意高亮部分的处理即可。
而关于 unmap
的部分,相较于 map
应该更为简单,我们只需要将 is_valid
字段置为 0 即可,如下:
但注意高亮部份为 true
以保证所有条目都会被清除(后续的也类似)
(un)map_range_in_pgtbl_huge
这部分的内容更为简单一些,如果过不去测试可以看看是不是 query
写错了。
做法分三步:
- 分配 1G 的大页,直到不够一个 1G 大页
- 分配 2M 的大页,中止条件同上
- 剩余部分通过
map_range_in_pgtbl
分配 4KB 页表来完成
注意的是我们需要时刻维护 len
这个变量(因为最后的函数需要用到),代码如下:
注意这里定义的宏即可,关于这个宏的定义,请参考此图:
参考此图 block
中的 output address
位置即可, 我们用的还是 4KB 的粒度
实验结果