![29dfe0bbb45ad1b84dd4133d8497b6b4.png](https://i-blog.csdnimg.cn/blog_migrate/61456f59cb7c14e3149b5a0613c94ae6.jpeg)
本文使用 Zhihu On VSCode 创作并发布
本文为本人学习6.828的Lab笔记,对应Lab2 Part1
的内容,接续上一篇笔记:
![30d9f59b17734b530839a69fe63bb4dd.png](https://i-blog.csdnimg.cn/blog_migrate/e26af353c0f5c5bbda56aee345ab163e.jpeg)
如果你没有阅读上一篇,请务必阅读后再开始本文阅读。其它Lab笔记在专栏持续更新:
MIT 6.828 实现操作系统zhuanlan.zhihu.com![baab8583280c25530a4386c7fc83e7da.png](https://i-blog.csdnimg.cn/blog_migrate/56ca3842cbe5ffc7798be1488e84c9d4.jpeg)
本文md文档源码链接:AnBlogs
这个part
的主要内容是初始化内核的内存分配器,下一个part
关注虚拟内存映射等等方面的内容。如果你对内核分配器不感兴趣,可以直接跳过本文,阅读下一篇对应Part 2
的内容:
![30d9f59b17734b530839a69fe63bb4dd.png](https://i-blog.csdnimg.cn/blog_migrate/e26af353c0f5c5bbda56aee345ab163e.jpeg)
相比Lab 1
,这个Lab难度更大,更加复杂,也是所有Lab中的基础,课程安排中给这个Lab安排了两周的时间。故我用3篇文章写这个Lab,分别对应Part 1, 2, 3
,以求清晰有条理。本文对应Part 1
,处理内核的内存管理组件。
Lab 2讲义:https://pdos.csail.mit.edu/6.828/2018/labs/lab2/
在开始之前,按照讲义中要求,通过git
拿到这个Lab的代码资源。在将Lab1
中代码commit
之后,依次执行以下代码:
% git pull
% git checkout -b lab2 origin/lab2
% git merge lab1
其中,最后一步可能需要手动调整文件,解决冲突。若IDE
配置有关的文件有冲突,务必解决,否则IDE
部分功能受到影响。
如果取得代码操作正确,应该多了这些文件:
inc/memlayout.h
kern/pmap.c
kern/pmap.h
kern/kclock.h
kern/kclock.c
那么我们开始吧。
回顾:未初始化完成的内存映射
在Lab 1
中,我们做了一个虚拟内存映射,将0xf0000000-0xf0400000
映射到物理地址0x00000000-00400000
,总共大小为4MB
。如果访问任何超出这个范围的虚拟地址,CPU
都会出错。
在之后写代码时,代码中的地址都是虚拟地址,翻译成物理地址的过程是硬件实现的,我们不应该想着如何直接操作物理地址。但是,有时将地址转化物理地址可以方便一些操作,在文件inc/memlayout.h
和kern/pmap.h
中提供了一些宏和函数,方便我们做这样的地址换算。
首先提供了宏KERNBASE
,注释说所有物理地址都被映射到这里,值为0xf0000000
,正是我们映射的地址。所谓所有,就是已经映射过的地址,不包括还没映射的地址。
宏函数KADDR
调用了函数_kaddr
,将物理地址转化成内核地址,或称虚拟地址,也就是在物理地址的数值上加上了KERNBAE
。此时的“所有”物理地址,范围还很小,因为其它的内存映射还没有建立,故可以这样简单地操作。其它内存映射建立之后,物理地址转化为虚拟地址的过程将很复杂。
相应的反向过程将虚拟地址转化为物理地址,宏函数PADDR
做了这样的事情。也就是在输入的虚拟地址上减去KERNBASE
,非常简单。
Part 1任务总览
Lab 2 Part 1
让我们完成内核内存初始化,而用户区User Level
内存初始化在后面的part
中完成。
初始化操作集中在文件kern/pmap.c
的函数mem_init
中,在内核初始化函数i386_init
中调用。在这个part
中,我们开始写这个函数以及它将调用的函数,只需要写到check_page_alloc
函数的调用之前即可。check_page_alloc
这一行之上进行的操作汇总如下。
- 直接调用硬件查看可以使用的内存大小,也就是函数
i386_detect_memory
。 - 创建一个内核初始化时的
page
目录,并设置权限。 - 创建用于管理
page
的数组,初始化page
分配器组件。 - 测试
page
分配器组件。
需要我们写的函数有:
boot_alloc
,page
未初始化时的分配器。page_init, page_alloc, page_free
,page
分配器组件。mem_init
,总的内存初始化函数。
分配器的运行过程十分简单,但是初始化十分困难。因为讲义中没有统一的材料清晰说明内核内存布局设计,需要我们通过代码探索。后文中的内存布局部分,就是我探索的结果。写出来十分简短,但是探索的过程非常困难。建议你先自己看看代码,揣摩内核的内存分布,再来看看我的结果,体会这个过程也很重要。
完成分配器之后,我们的目标是让虚拟地址有基础。进程需要更多内存,向内核发出请求,内核利用分配器,将一个由分配器决定的物理地址和由进程决定的虚拟地址关联到一起,称为映射。这是后面的Lab
的内容,本文只关心分配,不关心任何形式的映射。
两个内存分配器
有两个分配器,一个是正式的Page
分配器,在之后的所有情况下我们都使用这个。另一个是在Page
分配器初始化完成之前使用的,更加原始、简单。
在page
分配器初始化完成之前,内核在初始化的过程中使用boot_alloc
函数分配内存,也可称为boot
分配器。这个分配器非常原始,在page
分配器初始化完成后,务必不可调用boot_alloc
分配内存,以免出现莫名其妙的错误。
以下只大概介绍分配器的简单实现思想和使用,page
分配器的初始化需要考虑很多,在后面单独介绍。
page
分配器
Page
分配器操作内存是以page
为单位的(废话),之后几乎所有管理内存的机制都是以page
为单位。page
就是将所有的内存地址分成长度相同的一个个区块,每个的长度都是4096Bytes
。所有可以分配的内存都注册到一个链表中,通过分配器,可以方便地拿到一个未分配的page
。
内存管理组件维护一个链表,称为free list
,这个链表将所有未分配的page
连起来。需要分配内存时,将链表头部对应的page
返回,并将链表头部更新为链表中的下一个元素。
在inc/memlayout.h
中定义了这样的结构体:
struct PageInfo {
// Next page on the free list.
struct PageInfo *pp_link;
uint16_t pp_ref;
};
其中的指针pp_link
就是链表中常用的next
指针。
要了解此类内存管理的方法,可以看侯捷老师的课程:C++内存管理
创建了一个struct PageInfo
的数组,数组中第i
个成员代表内存中第i
个page
。故物理地址和数组索引很方便相换算。初始化时,形成一个链表,所有可分配的page
都以struct PageInfo
的形式存在于链表上。要通过分配器拿到一个page
,也就是读取链表开头的节点,这个节点就对应一个page
。
初始化函数page_init
将所有的pp_link
初始化指向与自己相邻的PageInfo
,如下:
![d4b3552416000dd5bdbb4adcf9f9beb7.png](https://i-blog.csdnimg.cn/blog_migrate/0439d62441abc0cfa8e27831527fa388.png)
这样初始化的操作是在kern/pmap.c
中完成的,具体的过程后文讲解。大概来说,初始化就是拉了这样一个链表,并且将指针page_free_list
指向链表的开头。分配内存时,若读取page_free_list
指针得到NULL
,则说明分配器已经给完了它能够管理的内存,再也给不出来了。
分配器组件的函数都是在操作PageInfo
指针,也就是pages
数组中的元素,而不是直接操作每个page
的地址。如分配函数page_alloc
返回的是一个PageInfo
,释放page
的函数page_free
接受的也是一个PageInfo
指针。将这个指针和pages
数组开头地址做差,可以得到这个PageInfo
在数组中的索引,也就可以换算出相应物理地址。
在文件kern/pmap.h
中,已经写好了一个函数page2kva
,接受一个PageInfo
指针,返回得到相应page
的虚拟地址。我们可以直接使用这个函数进行换算,这样得到的是虚拟地址,要得到物理地址,还需要在此基础上将地址的数值减去0xf0000000
,宏PADDR
做了这件事情。
内核的其他代码通过函数page_alloc
从free list
取出一个page
,返回当前page_free_list
指针,并零page_free_list
指针指向原链表中的下一个元素。
讲义中要求我们实现文件kern/pmap.c
中的函数page_alloc
,如下:
struct PageInfo *
page_alloc(int alloc_flags)
{
// Fill this function in
// Here begins my code
// out of memory
if (page_free_list == NULL) {
// no changes made so far of course
return NULL;
}
struct PageInfo *target = page_free_list;
page_free_list = page_free_list->pp_link; // update free list pointer
target->pp_link = NULL; // set to NULL according to notes
char *space_head = page2kva(target); // extract kernel virtual memory
if (alloc_flags & ALLOC_ZERO) {
// zero the page according to flags
memset(space_head, 0, PGSIZE);
}
return target;
}
要释放一个page
,也就是将这个page
放回链表。将page_free_list
指针指向这个PageInfo
结构体,并设置这个结构体的pp_link
为之前的page_free_list
指针。放回链表的这个page
也就变成了free list
的开头。
讲义中要求我们实现文件kern/pmap.c
中的函数page_free
,如下:
void
page_free(struct PageInfo *pp)
{
// Fill this function in
// Hint: You may want to panic if pp->pp_ref is nonzero or
// pp->pp_link is not NULL.
if (pp->pp_ref != 0 || pp->pp_link != NULL) {
panic("Page double free or freeing a referenced page...n");
}
pp->pp_link = page_free_list;
page_free_list = pp;
}
这都是非常简单的链表操作。关于这个链表的初始化,后文再解释。
前page
分配器boot_alloc
page
分配组件完成初始化之前,使用boot_alloc
函数分配内存,pages
数组就是这个函数分配的。
函数接受一个参数,代表要多少字节内存。函数将这个字节数上调到page
大小的边界,也就是调整为离这个字节数最近的4096的整数倍,以求每次分配都是以page
为单位的。这个分配器只能在page
分配器初始化完成之前使用,之后一律使用page
分配器。
实现非常简单,如下:
static void *
boot_alloc(uint32_t n)
{
static char *nextfree; // virtual address of next byte of free memory
char *result;
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}
// special case according to notes
if (n == 0) {
return nextfree;
}
// note before update
result = nextfree;
nextfree = ROUNDUP(n, PGSIZE) + nextfree;
// out of memory panic
if (nextfree > (char *)0xf0400000) {
panic("boot_alloc: out of memory, nothing changed, returning NULL...n");
nextfree = result; // reset static data
return NULL;
}
return result;
}
第一次调用这个函数时,必须初始化nextfree
指针。这个初始化也很简单,确定了内核本身在内存中的位置后,让boot_alloc
函数在内核所占空间的内存之后的第一个page
开始分配。表现为代码,就是从连接器中拿到内核的最后一个字节的地址end
,将这个指针的数值上调到4096的整数倍。
这个end
指针是连接器产生的,可以看连接配置文件kern/kernel.ld
的53行左右,end
指向内核的最后一个字节的下一个字节。
内核内存布局和分配器初始化
这里正式讲解page
分配器的初始化,也就是page_init
函数的实现,正确初始化之后的分配器才可以正确使用page_alloc, page_free
等函数。要知道分配器如何初始化,就要理解内核内存的布局Layout
。
获得物理内存信息
在初始化内存组件的函数mem_init
中,首先调用了函数i386_detect_memory
获得了内存硬件信息。追踪一下这个函数的调用,底层实现在kern/kclock.c
中,通过一系列汇编指令向硬件要信息。汇编指令如何执行的,我们暂且不关心。
最终得到的内存信息是两个整数npages, npages_basemem
,分别代表现有内存的page
个数,以及在拓展内存之前的page
个数。这些属于原始硬件信息,获得这个信息是为了确定一段IO
映射区的位置。
接着研究现有内存布局。
内存布局
你最好先自己通过代码探索一下内存布局的设计,而不是直接来看我的结果。看代码固然很繁琐,我在这里花了非常多时间才完全搞明白,但是有锻炼才有提升,这也是我们写操作系统的目的。
如果你已经自己操作过、思考过了,那么请看我的结果。
在文件kern/memlayout.h
中,有一个虚拟内存的布局示意图,这个示意图主要描绘用户区内存分配,而不是指出物理内存分布,故我们暂时不细看它。地址0xf0000000
以上的区域,也就是我们现在已经映射的区域,是我们关心的区域。宏KERNBASE
就是0xf0000000
,同时这个地址也是内核栈的开端。以下为了讲述方便,所有地址都是物理内存。
初始化的重要一步是弄清楚哪些物理地址可以分配,哪些不可以。这也就是弄清楚内存布局的意义所在。
模仿这个布局示意图,我画了一个从KERNBASE
开始的内存布局图,是我对内存布局的理解,下面的描述可以对照着这个图阅读。
/*
* . .
* . Managable Space .
* . .
pages ends 0x158000 -->+------------------------------+
* | |
* . .
* . pages management array .
* . .
* | |
* pages 0x118000 -> +------------------------------+
* | Kernel is here |
* EXT 0x100000 -> +------------------------------+
* | |
* | IO Hole |
* | |
* BASEMEM 0xa0000 -> +------------------------------+
* | Basic Managable Space |
* KERNBASE -----> +------------------------------+
*/
我们从KERNBASE
开始想起。回顾Lab 1
我们知道,内存0xf0000-0x100000
是BIOS
映射区,在这之前又是ROM
映射区,这段空间不能使用,不能被分配器分配出去。查看讲义,我们知道,地址0xa0000-0x100000
是ROM, BIOS
等IO
使用的内存,不可以被分配,初始化时应排除这部分空间。在文件inc/memlayout.h
中,宏IOPHYSMEM
定义了这段IO
段内存的开头。
在IOPHYSMEM
之前还有一些内存没有分配,这部分内存是可以使用的。函数i386_detect_memory
得到的npages_basemem
就是这一段的长度,初始化page
分配器时应该包含这一段。可以验证一下,npages_basemem
的值为160,这么多个page
总的大小为160 * 4096 = 655360 = 0xa0000
,确实是IOPHYSMEM
!
从0x100000
开始以上的内存就是内核,可以回顾Lab 1
中探索内核结构的结果,内核的.text
区的虚拟地址为0xf0100000
,物理地址正是0x100000
。文件inc/memlayout.h
中定义的宏EXTPHYSMEM
就是0x100000
,意思是BIOS
以上的内存,称为拓展区,其上限由RAM硬件大小决定。
如果你不记得内核的装载方式,可以使用指令objdump -h obj/kern/kernel
查看。
% obj/kern/kernel: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00002a4d f0100000 00100000 00001000 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rodata 00000bd0 f0102a60 00102a60 00003a60 2**5
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .stab 000050d1 f0103630 00103630 00004630 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .stabstr 00001bc3 f0108701 00108701 00009701 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .data 00009300 f010b000 0010b000 0000c000 2**12
CONTENTS, ALLOC, LOAD, DATA
5 .got 00000008 f0114300 00114300 00015300 2**2
CONTENTS, ALLOC, LOAD, DATA
6 .got.plt 0000000c f0114308 00114308 00015308 2**2
CONTENTS, ALLOC, LOAD, DATA
7 .data.rel.local 00001000 f0115000 00115000 00016000 2**12
CONTENTS, ALLOC, LOAD, DATA
8 .data.rel.ro.local 00000060 f0116000 00116000 00017000 2**5
CONTENTS, ALLOC, LOAD, DATA
9 .bss 00000681 f0116060 00116060 00017060 2**5
CONTENTS, ALLOC, LOAD, DATA
10 .comment 00000012 00000000 00000000 000176e1 2**0
CONTENTS, READONLY
内核占用了拓展区的开头,这些空间不应该被分配器管辖,不应该初始化到链表上。在初始化page
分配器之前,调用了几次boot_alloc
,这是内核运行时重要数据,他们占用的空间也不应该被分配器管辖。
分配器应该管辖最后一次调用boot_alloc
分配的空间之后的空间,这个空间开头的地址可以直接通过boot_alloc(0)
得到。
剩余的内存可以自由使用,分配器初始化是应该把链表拉到剩余的空间去。
分配器初始化
初始化就是拉链表,并注意排除不应该纳入分配器管辖的空间。总结上面对内存布局的研究,纳入分配器管辖的总共有两部分,分别是basemem
部分,也就是0x0-0xa0000
,和boot_alloc
最后分配的空间的后面的部分,排除了内核,和一些boot_alloc
取得的空间。
boot_alloc
即将分配的空间可以给函数传0直接得到,这是函数的特殊处理。由于boot_alloc
以page
为单位分配,这样得到的地址是一个page
的首地址,这个page
的索引可以轻易获得:
i = PADDR(boot_alloc(0)) / PGSIZE;
最后分配得到的应该如下图所示,其中basemem
部分省略了指针指向。
![f58564d534c9c7c23ad92406b90726ab.png](https://i-blog.csdnimg.cn/blog_migrate/ae15b62bdfffb3385d2b87e175422415.png)
完整实现如下:
void
page_init(void)
{
// Don't mark reference count!
pages[0].pp_ref = 0;
pages[0].pp_link = NULL;
page_free_list = &pages[0];
// base memory
size_t i = 1;
for (; i < npages_basemem; i++) {
pages[i].pp_ref = 0; // Don't mark reference count!
// connect the previous page
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
// extended pages after kernel
i = PADDR(boot_alloc(0)) / PGSIZE;
for (; i < npages; ++i) {
pages[i].pp_ref = 0; // Don't mark reference count!
// connect the previous page
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
// the 0-indexed page must exists
if (npages > 0) {
// first page marked used
pages[1].pp_link = NULL;
}
}
到这里就完成了Part 1
。