6.1 前言
前面的章节介绍了dummyxv6内核的很多基础模块,本章要介绍的是内核的物理内存管理机制。物理内存,即是设备所持有的实际的内存,内核对物理内存进行统一管理,可以方便后续实现进程逻辑的时候的内存分配和回收,因此本章涉及的内容非常重要。现在将逐个章节开始介绍。
6.2 加载内核文件
第一章已经介绍了dummyxv6的编译流程,最后会生成一个kernel的可执行文件,这个kernel包含了dummyxv6的内核逻辑。kernel存到计算机磁盘后,启动计算机时,会加载kernel到内存中。kernel.ld脚本指明了kernel的起始物理内存地址是0x80000000,其十进制值为2G。这里需要注意的是,实际上没有哪个设备会从物理地址为2G值的位置开始加载内核的,这样会造成极大的内存浪费。实际上官方xv6的x86版本,是从物理地址0x0的位置开始加载内核的,riscv版本从之所以从0x80000000开始,笔者认为这是为了便于学生理解MMIO。因为设备访问均是通过特定地址段来实现,绝大多数设备的寄存器的地址段都位于0x80000000之下。这种方式可以避免做复杂的映射处理。
kernel被加载到内存之后,这部分内存是固定被占用的,它不能再被分配作他用。kernel在内存中的起始地址,被视作为kernel base,从kernel base开始到物理地址上限值这一段内存,就是设备所能提供的实际的物理内存。在dummyxv6中,实际物理内存的大小为128M。
6.3 物理内存范围
物理内存空间包含了kernel加载到内存后的部分,正如图1所示的那样,蓝色区域是内核可以管理的物理内存空间,它将被按页拆分,并存入一个加锁的单向链表当中。
图1
6.4 物理内存初始化
第一章就介绍过kernel的内存布局,kernel的指令和数据所占用的内存结束于一个end变量,本章新增了kalloc.h|c和string.h|c文件,kalloc.c定义了这个end变量。
// kernel/kalloc.c
...
extern char end[];
...这个变量的地址在kernel.ld中被赋值,标志着kernel指令和数据占用内存的上限值。现在来复习一下kernel.ld脚本,代码第36行,将end变量赋值为.bss段的结束地址。ld脚本中的“.”代表当前地址。
// kernel/kernel.ld
1 OUTPUT_ARCH("riscv")
2 ENTRY(_entry)
3
4 SECTIONS {
5 /* ensure that _entry is starting at 0x80000000 */
6 . = 0x80000000;
7
8 .text : {
9 *(.text .text.*)
10 . = ALIGN(0x1000);
11
12 PROVIDE(etext = .);
13 }
14
15 .rodata : {
16 . = ALIGN(16);
17 *(.srodata .srodata.*)
18 . = ALIGN(16);
19 *(.rodata .rodata.*)
20 }
21
22 .data : {
23 . = ALIGN(16);
24 *(.sdata .sdata.*)
25 . = ALIGN(16);
26 *(.rodata .rodata.*)
27 }
28
29 .bss : {
30 . = ALIGN(16);
31 *(.sbss .sbss.*)
32 . = ALIGN(16);
33 *(.bss .bss.*)
34 }
35
36 PROVIDE(end = .);
37 }在图1中地址超过end,小于MAX_MEM_LIMIT的内存段,属于要被管理的物理内存。物理内存初始化做的事情,就是将位于这两条地址边界之间的内存,按页分段,并且存放到一个单向链表中管理起来。这样操作系统在创建用户进程时,就可以到这个链表中获取物理内存页,分配给用户进程。在dummyxv6中,一个数据页是4KB,数据页的起始地址会被记录在run的数据结构之中:
// kernel/kalloc.c
...
1 struct run {
2 struct run* next;
3 };
4
5 struct {
6 struct spinlock lock;
7 struct run *freelist;
8 } kmem;
...在64位系统中,数据页的头8个字节记录着链表中,下一个数据页的起始地址。链表头部的地址,记录在一个全局变量kmem的freelist字段中。初始化的逻辑很简单,首先获得end变量向上按页对齐的地址,然后从这里开始,每隔4KB切分数据页,并将它们组成链表。完成初始化的结果,如图2所示。
// kernel/kalloc.c
...
1 void freerange(void *vstart, void *vend) {
2 char *p;
3 p = (char *)PROUNDUP((uint64_t)vstart);
4 for (; p + PGSIZE <= (char *)vend; p += PGSIZE)
5 kfree(p);
6 }
7
8 void kinit() {
9 initlock(&kmem.lock, "kmem");
10 kmem.freelist = 0;
11
12 freerange(end, (void *)MAX_MEM_LIMIT);
13 }
...
图2
6.5 开辟内存
本节将介绍kernel开辟物理内存的逻辑,在上一节中,读者们已经了解到了初始化后,物理内存的逻辑关系。当kernel要从这个freelist链表中获取一个物理页时,他的逻辑如下所示:
// kernel/kalloc.c
...
1 void *kalloc() {
2 acquire(&kmem.lock);
3 struct run *r = kmem.freelist;
4 if (r)
5 kmem.freelist = r->next;
6 release(&kmem.lock);
7
8 memset((char *)r, 5, PGSIZE); // fill with junk
9
10 return (void *)r;
11 }
...这里的逻辑很简单,就是acquire全局变量kmem的锁变量之后,从freelist中取出一个物理内存,然后填充初始化数据后返回。这里为什么要填充一个初始化数据呢?回顾一下图2,从链表中取出的物理内存页,此时还包含着链表中,下一个数据页的地址,填充初始化数据可以抹掉这些信息。此外,还可以根据内存页中的填充值,判定其当前处于alloc还是free状态。还有一个需要注意的点则是,一个物理内存页被一个用户进程使用完,归还kernel之后,可能会被另一个用户进程使用,因此对数据页进行填充也可以保护用户进程的隐私数据。
6.6 释放内存
释放物理内存的逻辑也很简单,即是填充一个指定的值之后,加锁塞回到freelist的头部。
// kernel/kalloc.c
...
1 void kfree(void *v) {
2 memset(v, 1, PGSIZE); // fill with junk
3
4 struct run *r = (struct run *)v;
5
6 acquire(&kmem.lock);
7 r->next = kmem.freelist;
8 kmem.freelist = r;
9 release(&kmem.lock);
10 }
...6.7 测试
到这里,基本就完成kernel物理内存管理的介绍了,接下来是对它进行测试。测试用例添加在main.c文件之中,如下代码所示,CPU0负责初始化物理内存,在完成物理内存初始化之后,其他核心同时开始从物理内存链表中开辟一个物理内存页,并且填充“hello world”,最后再打印。
// kernel/main.c
...
1 void main() {
2 if (cpuid() == 0) {
3 uartinit();
4 printfinit();
5
6 printf("dummyxv6 booting!\n");
7 printf("start to initialize the kernel.\n");
8
9 kinit();
10
11 printf("CPU%d initialize success.\n", cpuid());
12 is_initialized = 1;
13
14 __sync_synchronize();
15 }
16 else {
17 while (is_initialized == 0);
18
19 printf("CPU%d starts initializing.\n", cpuid());
20
21 // test kalloc
22 void *p = kalloc();
23 strcpy(p, "hello, world!\n");
24 printf("CPU%d: %s", cpuid(), p);
25 kfree(p);
26
27 printf("Initialization of CPU%d complete.\n", cpuid());
28 }
29}上述代码运行的结果如下所示:
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
dummyxv6 booting!
start to initialize the kernel.
CPU0 initialize success.
CPU1 starts initializing.
CPU2 starts initializing.
CPU2: hello, world!
CPU1: hello, world!
Initialization of CPU2 complete.
Initialization of CPU1 complete.6.8 结束语
本章介绍了dummyxv6的物理内存管理模块,包括物理内存是什么,物理内存的初始化、物理内存开辟和物理内存释放,为后续开发推进打下必要的基础。