Lab2光就实验而言并不难, 但实验外的东西仍是很值得研究的。指导书上也说了,Lab1和Lab2对于初次接触这门课的同窗来讲是一道坎,只要搞懂了这两Lab的代码,接下来的其余Lab就会相对容易不少。因此除了作实验,我还大体地阅读了每一部分的代码。经过阅读代码,对系统内存的探测,内存物理页的管理, 不一样阶段的地址映射等有了更进一步的理解,下面就先从系统内存的探测开始。算法
在咱们分配物理内存空间前,咱们必需要获取物理内存空间的信息 - 好比哪些地址空间可使用,哪些地址空间不能使用等。在本实验中, 咱们经过向INT 15h中断传入e820h参数来探测物理内存空间的信息(除了这种方法外,咱们还可使用其余的方法,具体如何使用这些方法请自行网上搜索)。
下面咱们来看一下ucore中物理内存空间的信息:session
e820map: memory: 0009fc00, [00000000, 0009fbff], type = 1. memory: 00000400, [0009fc00, 0009ffff], type = 2. memory: 00010000, [000f0000, 000fffff], type = 2. memory: 07ee0000, [00100000, 07fdffff], type = 1. memory: 00020000, [07fe0000, 07ffffff], type = 2. memory: 00040000, [fffc0000, ffffffff], type = 2.
这里的type是物理内存空间的类型,1是可使用的物理内存空间, 2是不能使用的物理内存空间。注意, 2中的"不能使用"指的是这些地址不能映射到物理内存上, 但它们能够映射到ROM或者映射到其余设备,好比各类外设等。ide
除了这两种类型,还有几种其余类型,只是在这个实验中咱们并无使用:函数
type = 3: ACPI Reclaim Memory (usable by OS after reading ACPI tables) type = 4: ACPI NVS Memory (OS is required to save this memory between NVS sessions) type = other: not defined yet -- treat as Reserved
要使用这种方法来探测物理内存空间,咱们必须将系统置于实模式下。所以, 咱们在bootloader中添加了物理内存空间探测的功能。 这种方法获取的物理内存空间的信息是用内存映射地址描述符(Address Range Descriptor)来表示的,一个内存映射地址描述符占20B,其具体描述以下:源码分析
00h 8字节 base address #系统内存块基地址 08h 8字节 length in bytes #系统内存大小 10h 4字节 type of address range #内存类型
每探测到一块物理内存空间, 其对应的内存映射地址描述符就会被写入咱们指定的内存空间(能够理解为是内存映射地址描述符表)。 当完成物理内存空间的探测后, 咱们就能够经过这个表来了解物理内存空间的分布状况了。ui
下面咱们来看看INT 15h中断是如何进行物理内存空间的探测:this
/* memlayout.h */ struct e820map { int nr_map; struct { long long addr; long long size; long type; } map[E820MAX]; }; /* bootasm.S */ probe_memory: /* 在0x8000处存放struct e820map, 并清除e820map中的nr_map */ movl $0, 0x8000 xorl %ebx, %ebx /* 0x8004处将用于存放第一个内存映射地址描述符 */ movw $0x8004, %di start_probe: /* 传入0xe820做为INT 15h中断的参数 */ movl $0xE820, %eax /* 内存映射地址描述符的大小 */ movl $20, %ecx movl $SMAP, %edx /* 调用INT 15h中断 */ int $0x15 /* 若是eflags的CF位为0,则表示还有内存段须要探测 */ jnc cont movw $12345, 0x8000 jmp finish_probe cont: /* 设置下一个内存映射地址描述符的起始地址 */ addw $20, %di /* e820map中的nr_map加1 */ incl 0x8000 /* 若是还有内存段须要探测则继续探测, 不然结束探测 */ cmpl $0, %ebx jnz start_probe finish_probe:
从上面代码能够看出,要实现物理内存空间的探测,大致上只须要3步:spa
设置一个存放内存映射地址描述符的物理地址(这里是0x8000)操作系统
将e820做为参数传递给INT 15h中断设计
经过检测eflags的CF位来判断探测是否结束。若是没有结束, 设置存放下一个内存映射地址描述符的物理地址,而后跳到步骤2;若是结束,则程序结束
当咱们在bootloader中完成对物理内存空间的探测后, 咱们就能够根据获得的信息来对可用的内存空间进行管理。在ucore中, 咱们将物理内存空间按照页的大小(4KB)进行管理, 页的信息用Page这个结构体来保存。下面是Page在Lab2中的具体描述:
struct Page { int ref; // page frame's reference counter uint32_t flags; // array of flags that describe the status of the page frame unsigned int property; // the num of free block, used in first fit pm manager list_entry_t page_link; // free list link };
咱们下面来看看程序是如何初始化物理内存空间的页信息的。
物理内存空间的初始化能够分为如下4步:
根据物理内存空间探测的结果, 找到最后一个可用空间的结束地址(或者Kernel的结束地址,选一个小的) 根据这个结束地址计算出整个可用的物理内存空间一共有多少个页。
找到Kernel的结束地址(end),这个地址是在kernel.ld中定义的, 咱们从这个地址所在的下一个页开始(pages)写入系统页的信息(将全部的Page写入这个地址)
从pages开始,将全部页的信息的flag都设置为reserved(不可用)
找到free页的开始地址, 并初始化全部free页的信息(free页就是除了kernel和页信息外的可用空间,初始化的过程会reset flag中的reserved位)
上面这几部中提到了不少地址空间, 下面我用一幅图来讲明:
end指的就是BSS的结束处;pages指的是BSS结束处 - 空闲内存空间的起始地址;free页是从空闲内存空间的起始地址 - 实际物理内存空间结束地址。
有了这幅图,这些地址就很容易理解了。
pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);
uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);
init_memmap(pa2page(begin), (end - begin) / PGSIZE);
从pages开始保存了全部物理页的信息(严格来说, 在pages处保存的npage个页的信息并不必定是全部的物理页信息,它还包括各类外设地址,ROM地址等。不过由于它包含了全部可用free页的信息,咱们就可使用pages来找到任何free页的信息)。 那如何将free页的信息和free页联系起来呢?很简单, 咱们用地址的物理页号(pa的高20bit)做为index来定位free页的信息。 由于pages处保存了系统中的第一个物理页的页信息,只要咱们知道某个页的物理地址, 咱们就能够很容易的找到它的页号(pa >> 12)。 有了页号,咱们就能够经过pages[页号]来定位其页的信息了。在本lab中, 获取页的信息是由 pa2page()
来完成的。
在初始化free页的信息时, 咱们只将连续多个free页中第一个页的信息连入free_list中, 而且只将这个页的property设置为连续多个free页的个数。 其余全部页的信息咱们只是简单的设置为0。
这个lab中最重要的一个知识点就是内存的段页式管理。 下图是段页式内存管理的示意图:
咱们能够看到,在这种模式下,逻辑地址先经过段机制转化成线性地址, 而后经过两种页表(页目录和页表)来实现线性地址到物理地址的转换。 有一点须要注意,在页目录和页表中存放的地址都是物理地址。
下面是页目录表表项:
下面是页表表项:
在X86系统中,页目录表的起始物理地址存放在cr3 寄存器中, 这个地址必须是一个页对齐的地址,也就是低 12 位必须为0。在ucore 用boot_cr3(mm/pmm.c)记录这个值。
在ucore中,线性地址的的高10位做为页目录表的索引,以后的10位做为页表的的索引,因此页目录表和页表中各有1024个项,每一个项占4B,因此页目录表和页表恰好能够用一个物理的页来存放。
在这个实验中,咱们在4个不一样的阶段使用了四种不一样的地址映射, 下面我就分别介绍这4种地址映射。
这一阶段是从bootasm.S的start到entry.S的kern_entry前,这个阶段很简单, 和lab1同样(这时的GDT中每一个段的起始地址都是0x00000000而且此时kernel尚未载入)。
virt addr = linear addr = phy addr
这个阶段就是从entry.S的kern_entry到pmm.c的enable_paging()。 这个阶段就比较复杂了,咱们先来看bootmain.c这个文件:
#define ELFHDR ((struct elfhdr *)0x10000) // scratch space
bootmain.c中的函数被调用时候还处于第一阶段, 因此从上面这个宏定义咱们能够知道kernel是被放在物理地址为0x10000的内存空间。咱们再来看看连接文件kernel.ld,
/* Load the kernel at this address: "." means the current address */ . = 0xC0100000;
链接文件将kernel连接到了0xC0100000(这是Higher Half Kernel, 具体参考Higher Half Kernel),这个地址是kernel的虚拟地址。 因为此时系统还只是采用段式映射,若是咱们仍是使用
virt addr = linear addr = phy addr
的话,咱们根本不能访问到正确的内存空间,好比要访问虚拟地址0xC0100000, 其物理地址应该在0x00100000,而在这种映射下, 咱们却访问了0xC0100000的物理地址。所以, 为了让虚拟地址和物理地址能匹配,咱们必需要从新设计GDT。
在entry.S中,咱们从新设计了GDT,其形式以下:
#define REALLOC(x) (x - KERNBASE) lgdt REALLOC(__gdtdesc) ... __gdt: SEG_NULL SEG_ASM(STA_X | STA_R, - KERNBASE, 0xFFFFFFFF) # code segment SEG_ASM(STA_W, - KERNBASE, 0xFFFFFFFF) # data segment __gdtdesc: .word 0x17 # sizeof(__gdt) - 1 .long REALLOC(__gdt)
能够看到,此时段的起始地址由0变成了-KERNBASE。所以,在这个阶段, 地址映射关系为:
virt addr - 0xC0000000 = linear addr = phy addr
这里须要注意两个个地方,第一,lgdt载入的是线性地址,因此用.long REALLOC(__gdt)将GDT的虚拟地址转换成了线性地址;第二,由于在载入GDT前,映射关系仍是
virt addr = linear addr = phy addr
因此经过REALLOC(__gdtdesc)来将__gdtdesc的虚拟地址转换为物理地址,这样,lgdt才能真正的找到GDT存储的地方。
这个阶段是从kmm.c的enable_paging()到kmm.c的gdt_init()。 这个阶段是最复杂的阶段,咱们开启了页机制, 而且在boot_map_segment()中将线性地址按照以下规则进行映射:
linear addr - 0xC0000000 = phy addr
这就致使此时虚拟地址,线性地址和物理地址之间的关系以下:
virt addr = linear addr + 0xC0000000 = phy addr + 2 * 0xC0000000
这确定是错误的,由于咱们根本不能经过虚拟地址获取正确的物理地址, 咱们能够继续用以前例子。咱们仍是要访问虚拟地址0xC0100000, 则其线性地址就是0x00100000,而后经过页映射后的物理地址是0x80100000。 咱们原本是要访问0x00100000,却访问了0x80100000, 所以咱们须要想办法来解决这个问题,即要让映射仍是:
virt addr - 0xC0000000 = linear addr = phy addr
这个和第一阶段到第二阶段的转变相似,都是须要调整映射关系。为了解决这个问题, ucore使用了一个小技巧:
在boot_map_segment()中, 线性地址0xC0000000-0xC0400000(4MB)对应的物理地址是0x00000000-0x00400000(4MB)。若是咱们还想使用虚拟地址0xC0000000来映射物理地址0x00000000, 也就是线性地址0x00000000来映射物理地址0x00000000,咱们能够这么作:
在开启页映射机制前, 将页目录表中的第0项和第0b1100_0000_00设置为相同的映射(boot_pgdir[0] = boot_pgdir[PDX(KERNBASE)]),这样,当虚拟地址是0xC0000000时, 其对应的线性地址就是0x00000000, 而后经过页表能够知道其物理地址也是0x00000000。
举个例子,好比enable_paging()后应该运行gdt_init(),gdt_init()的虚拟地址是0xC01033CF,那么其对应的线性地址就是0x001033CF,它将映射到页目录表的第一项。而且这个线性地址和0xC01033CF最终是指向同一个物理页,它们对应的物理地址是0x001033CF。而根据gdt_init()的虚拟地址和连接地址可知,gdt_init()的物理地址就是0x001033CF,所以经过这种地址变换后,咱们能够正确的取到以后的指令。
由于ucore在当前lab下的大小是小于4MB的,所以这么作以后, 咱们依然能够按照阶段二的映射方式运行ucore。若是ucore的大小大于了4MB, 咱们只需按一样的方法设置页目录表的第1,2,3...项。
这一阶段开始于kmm.c的gdt_init()。gdt_init()从新设置GDT, 新的GDT又将段的起始地址变为了0. 调整后, 地址的映射关系终于由
virt addr = linear addr + 0xC0000000 = phy addr + 2 * 0xC0000000
变回了
virt addr = linear addr = phy addr + 0xC0000000
同时,咱们把目录表中的第0项设置为0,这样就把以前的这种映射关系解除了。经过这几个步骤的转换, 咱们终于在开启页映射机制后将映射关系设置正确了。
在这个实验中,为了方便快速地访问页目录项和页表项,用了一个小技巧 - 也就是自映射。若是用常规方法的话,要访问一个页表项,必须先使用虚拟地址访问到对应的页目录项,而后经过其中的页表地址找到页表,最后再经过虚拟地址的页表项部分找到咱们须要的页表项。这个过程是比较繁琐的,为了方便的访问这些表项,咱们使用了自映射。咱们下面经过代码来看看什么是自映射以及如何使用自映射来快速查找表项的内容。
首先,咱们将页一个目录项的值设置为页目录表的物理地址,这么作的做用就是当咱们使用的虚拟地址,高10位为1111 1010 11b时,它对应的页表就是页目录表,这也就实现了自映射:
/* VPT = 0xFAC00000 = 1111 1010 11 | 00 0000 0000 | 0000 0000 0000b 注意,这个地址是在ucore有效地址以外的地址(有效地址从0xC0000000到0xF8000000) */ boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W;
这样以后,咱们来看看如何使用这种自映射。咱们还定义了一个地址VPD:
VPD = 0xFAFEB000 = 1111 1010 11 | 11 1110 1011 | 0000 0000 0000b
经过VPD和VPT,咱们就能方便的访问页目录项和页表项了。下面给一个例子说明如何获取0xF8000000所在的页目录项和页表项:
首先,咱们思考下直接经过VPD = 1111 1010 11 | 11 1110 1011 | 0000 0000 0000b这个地址咱们能访问到什么?在页目录表中经过1111 1010 11能够找到boot_pgdir[PDX(VPT)]这项,这项又直接指回了boot_pgdir,此时咱们将页目录表当成了页表。咱们此时再用第二个11 1110 1011,仍是找到boot_pgdir[PDX(VPT)]这项,它仍是指回boot_pgdir。也就是说,VPD这个地址最终就是指回了页目录表,而且咱们能够经过它的最后12bit来访问页目录表。0xF8000000地址对应的页目录项是1111 1000 00b,咱们只要将这个值放在VPD的后12bit就好了(由于12bit是大于10bit的,所以咱们必定能找到须要访问的页目录项),也就是说咱们经过
VPD + 0xF8000000 >> 22
就能够得到0xF8000000对应的页目录项。若是懂了如何获取页目录项,再来看如何获取页表项就很简单了。首先,咱们根据VPT能够访问到页目录表,这个页目录表一样也是VPT对应的页表,经过VPT的低22位咱们就能够像访问页表同样的访问页目录表。0xF8000000的高20位是1111 1000 0000 0000 0000b,用这个地址咱们就能够经过页目录表找到它对应的页表项了。这里我以为指导书上说的不对,若是要能访问一个非4MB对齐的地址,不能直接使用
VPT + addr >> 12
而要用
VPT + [addr_31_22 : 00 : addr_21_12]
好比一个高20位地址是1111 1000 11|00 0000 0011b,那么要用在VPT中,1111 1000 11要放在VPT的21_12位,用于找到页目录表项,从而找到页表,剩下的00 0000 0003就要用来在页表中找页表项。由于VPT中的低22位为0,若是直接使用addr >> 22的话,那么1111 1000 11|00 0000 0011b就变成了0011 1110 00| 1100 0000 0011b,这样的话,用于查找页目录项和页表项的索引就不对了,因此我以为应该是我说的那种转换方法。也便是1111 1000 11|00 0000 0011b变成了1111 1000 11|0000 0000 0011b,这样才和以前的是对应的。若是要访问的地址是4MB对齐的,那么就能够直接用VPT + addr >> 12了。
这个练习是实现first-fit连续物理内存分配算法。难度不大,主要经过实现两个函数 default_alloc_pages(size_t n)
和 default_free_pages(struct Page *base, size_t n)
。 下面是这两个函数的代码:
/* default_pmm.c */ static struct Page * default_alloc_pages(size_t n) { assert(n > 0); if (n > nr_free) { return NULL; } struct Page *page = NULL; list_entry_t *le = &free_list; /* find the fist memory block that is larger or equal to n pages */ while ((le = list_next(le)) != &free_list) { struct Page *p = le2page(le, page_link); if (p->property >= n) { page = p; break; } } if (page != NULL) { /* if the memory block is larger than n pages, we need to divide this * memory block to two pieces and add the second piece to the free_list. * Item in free list should be sorted by address */ if (page->property > n) { struct Page *p = page + n; p->property = page->property - n; list_add_after(&(page->page_link), &(p->page_link)); } /* cleanup Page information and remove it from free_list */ for (int i = 0; i < n; i++) ClearPageProperty(page + i); page->property = 0; list_del(&(page->page_link)); nr_free -= n; } return page; }
default_alloc_pages(size_t n)
会返回分配的物理页对应的页信息,根据页信息,咱们能够经过计算它的index(page - pages)来获取物理页的物理页号。以后根据各类转换,咱们就能知道物理页的物理地址和虚拟地址。
/* default_pmm.c */ static void default_free_pages(struct Page *base, size_t n) { struct Page *prev, *next; assert(n > 0); struct Page *p = base; for (; p != base + n; p ++) { /* !PageProperty(p) checks two things: * 1. whether base belongs to free or allocated pages. If page is allocated, Property flag * is set to 0; If page is free, Property flag is set to 1. * 2. whether base + n across the boundary of base memory block. * The Property flag of allocated page is set to 0, is one page's Property flag is set to 1, * base + n must across the boundary. */ assert(!PageReserved(p) && !PageProperty(p)); p->flags = 0; set_page_ref(p, 0); } base->property = n; list_entry_t *le = list_next(&free_list); /* find the first Page in free_list that address is larger than base */ while (le != &free_list) { struct Page *p = le2page(le, page_link); if (p > base) break; le = list_next(le); } /* there are two cases here: * 1. free_list is not empty * 2. we can find a Page in free_list that address is larger than base */ if (le != &free_list) { next = le2page(le, page_link); /* if we can combine base and next memory spaces, just do it. But we should not insert base to free_list here. * We will deal with this later */ if (base + n == next) { base->property += next->property; next->property = 0; list_del(&(next->page_link)); } /* if base's address is smaller than the first Page's address, we just insert base to free_list */ if (le->prev == &free_list) { list_add_after(&(free_list), &(base->page_link)); } else { prev = le2page(le->prev, page_link); /* if we can combine base and previous memory spaces, just do it. In this case, we do not need * to insert base to free_list */ if (prev + prev->property == base) { prev->property += base->property; base->property = 0; } /* if we can not combine base and previous memory spaces, no matter base can combine next memory space or not, * we just insert base to free_list */ else { list_add_after(&(prev->page_link), &(base->page_link)); } } } /* there are two cases here: * 1. free_list is empty * 2. we can not find a Page in free_list that address is larger than base * In these two cases, we only need to set base page's Property flag to 1 and insert * it to free_list */ else { list_add_before(&(free_list), &(base->page_link)); } for (int i = 0; i < n; i++) SetPageProperty(base + i); nr_free += n; }
default_free_pages(struct Page *base, size_t n)
将根据传入的Page address来释放n page大小的内存空间。该函数会判断Page address是不是allocated的,也会判断是否base + n会跨界(由allocated到free的内存空间)。若是输入的Page address合法,则会将新的Page插入到free_list中的合适位置(free_list是按照Page地址由低向高排序的)。
有一点须要注意,在本first-fit连续物理内存分配算法中,对于任何allocated后的Page,Property flag都为0;任何free的Page,Property flag都为1。
对于allocated后的Pages,第一个Page的property在这里是被清零了的,若是ucore要求只能用第一个Page来free Pages,那么allocate时,第一个Page的property就不该该清零。咱们在free Page时要用property来判断Page是否是第一个Page。
若是ucore规定free须要free掉整个Page块,那么咱们还须要检测第一个Page的property是否和要free的page数相等。
上面这几点在Lab2中并不能肯定,若是以后Lab有说明,或者出现错误,咱们须要从新修改这些地方。
这个练习是实现寻找虚拟地址对应的页表项。
/* pmm.c */ pte_t * get_pte(pde_t *pgdir, uintptr_t la, bool create) { pte_t *pt_addr; struct Page *p; uintptr_t *page_la; if (pgdir[(PDX(la))] & PTE_P) { pt_addr = (pte_t *)(KADDR(pgdir[(PDX(la))] & 0XFFFFF000)); return &pt_addr[(PTX(la))]; } else { if (create) { p = alloc_page(); if (p == NULL) { cprintf("boot_alloc_page failed.\n"); return NULL; } p->ref = 1; page_la = KADDR(page2pa(p)); memset(page_la, 0x0, PGSIZE); pgdir[(PDX(la))] = ((page2pa(p)) & 0xFFFFF000) | (pgdir[(PDX(la))] & 0x00000FFF); pgdir[(PDX(la))] = pgdir[(PDX(la))] | PTE_P | PTE_W | PTE_U; return &page_la[PTX(la)]; } else { return NULL; } } }
这个代码很简单, 但有几个地方仍是须要注意下。
首先,最重要的一点就是要明白页目录和页表中存储的都是物理地址。因此当咱们从页目录中获取页表的物理地址后,咱们须要使用KADDR()将其转换成虚拟地址。以后就能够用这个虚拟地址直接访问对应的页表了。
第二, *, &, memset()
等操做的都是虚拟地址。注意不要将物理或者线性地址用于这些操做(假设线性地址和虚拟地址不同)。
第三,alloc_page()获取的是物理page对应的Page结构体,而不是咱们须要的物理page。经过一系列变化(page2pa()),咱们能够根据获取的Page结构体获得与之对应的物理page的物理地址,以后咱们就能得到它的虚拟地址。
这个练习是实现释放某虚地址所在的页并取消对应二级页表项的映射。这个练习比Task2还要简单,我就直接贴出代码了。
/* pmm.c */ static inline void page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) { pte_t *pt_addr; struct Page *p; uintptr_t *page_la; if ((pgdir[(PDX(la))] & PTE_P) && (*ptep & PTE_P)) { p = pte2page(*ptep); page_ref_dec(p); if (p->ref == 0) free_page(p); *ptep = 0; tlb_invalidate(pgdir, la); } else { cprintf("This pte is empty!\n"); } }