vs使用未初始化的内存怎么解决_MIT 6.828:实现操作系统 | Lab 2 Part 1:内核内存分配器

29dfe0bbb45ad1b84dd4133d8497b6b4.png
本文使用 Zhihu On VSCode 创作并发布

本文为本人学习6.828的Lab笔记,对应Lab2 Part1的内容,接续上一篇笔记:

anarion:MIT 6.828:实现操作系统 | Lab1:看看内核​zhuanlan.zhihu.com
30d9f59b17734b530839a69fe63bb4dd.png

如果你没有阅读上一篇,请务必阅读后再开始本文阅读。其它Lab笔记在专栏持续更新:

MIT 6.828 实现操作系统​zhuanlan.zhihu.com
baab8583280c25530a4386c7fc83e7da.png

本文md文档源码链接:AnBlogs

这个part的主要内容是初始化内核的内存分配器,下一个part关注虚拟内存映射等等方面的内容。如果你对内核分配器不感兴趣,可以直接跳过本文,阅读下一篇对应Part 2的内容:

anarion:MIT 6.828:实现操作系统 | Lab 2 Part 2:内核内存映射​zhuanlan.zhihu.com
30d9f59b17734b530839a69fe63bb4dd.png

相比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.hkern/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这一行之上进行的操作汇总如下。

  1. 直接调用硬件查看可以使用的内存大小,也就是函数i386_detect_memory
  2. 创建一个内核初始化时的page目录,并设置权限。
  3. 创建用于管理page的数组,初始化page分配器组件。
  4. 测试page分配器组件。

需要我们写的函数有:

  1. boot_allocpage未初始化时的分配器。
  2. page_init, page_alloc, page_freepage分配器组件。
  3. 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个成员代表内存中第ipage。故物理地址数组索引很方便相换算。初始化时,形成一个链表,所有可分配的page都以struct PageInfo的形式存在于链表上。要通过分配器拿到一个page,也就是读取链表开头的节点,这个节点就对应一个page

初始化函数page_init将所有的pp_link初始化指向与自己相邻的PageInfo,如下:

d4b3552416000dd5bdbb4adcf9f9beb7.png
free list

这样初始化的操作是在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_allocfree 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-0x100000BIOS映射区,在这之前又是ROM映射区,这段空间不能使用,不能被分配器分配出去。查看讲义,我们知道,地址0xa0000-0x100000ROM, BIOSIO使用的内存,不可以被分配,初始化时应排除这部分空间。在文件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_allocpage为单位分配,这样得到的地址是一个page的首地址,这个page的索引可以轻易获得:

i = PADDR(boot_alloc(0)) / PGSIZE;

最后分配得到的应该如下图所示,其中basemem部分省略了指针指向。

f58564d534c9c7c23ad92406b90726ab.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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值