MIT JOS学习笔记02:kernel 01(2016.10.28)
未经许可谢绝以任何形式对本文内容进行转载!
在文章开头不得不说的是,因为这部分的代码需要仔细理清的东西太多,所以导致这篇分析显得很啰嗦,还请谅解。
我们在上一篇文章已经分析了Boot Loader的功能,现在我们来分析由Boot Loader加载到内存里的kernel。从MAKEFILE文件可以看出kernel由以下几部分代码组成(注:这里给出的列表是进行lab2时的代码,即比lab1多了pmap.c等文件,由lab1更新到lab2需要用到Git,具体操作仅贴出参考链接:http://www.xuebuyuan.com/1498170.html):
KERN_SRCFILES := kern/entry.S \ kern/entrypgdir.c \ kern/init.c \ kern/console.c \ kern/monitor.c \ kern/pmap.c \ kern/env.c \ kern/kclock.c \ kern/picirq.c \ kern/printf.c \ kern/trap.c \ kern/trapentry.S \ kern/sched.c \ kern/syscall.c \ kern/kdebug.c \ lib/printfmt.c \ lib/readline.c \ lib/string.c
因为kernel的代码相对复杂,我们直接从程序执行的流程来对kernel进行分析。
kernel的入口处在entry.S中。这个文件同样是AT&T汇编格式,其中定义了.text节和.data节。在.text的开头,作者用.align伪指令让代码按4字节对齐,同时用.long伪指令写入了MULTIBOOT_HEADER_MAGIC和MULTIBOOT_HEADER_FLAGS两个常量,并计算了它们的校验和。这之后,用RELOC()宏定义了一个外部符号_start,这个符号是链接器(Linker)的一个导出符号,指向kernel入口在内存中实际的物理地址(因为在kernel刚加载的时候我们尚未建立虚拟内存到物理内存的映射,需要我们手动计算kernel的入口,否则无法加载)。紧接着就是kernel实际上执行的第一句代码:
1 movw $0x1234,0x472 # warm boot
这句代码的作用是向0x472写入0x1234,告知BIOS下次为热引导(这句语句在kernel.asm中显示的虚拟地址为:0xf010 0000,但实际上查询内核信息显示的kernel虚拟的入口地址是:0xf010 000c,这个问题想不通,可能以后补上)。
之后就是我们在monitor中查到的kernel的入口地址。首先,kernel用RELOC()宏计算了页目录的入口物理地址(接下来请注意区分虚拟地址和物理地址,只有虚拟地址对程序才有意义,程序不能直接对物理地址进行修改,二者的联系后面会提到),并把这个物理地址赋给了CR3控制寄存器,这个寄存器保存的是我们将在虚拟内存管理(二级页表)中使用的页目录(一级页表)的入口物理地址。注意,这里同样是因为目前没有虚拟地址映射,所以调用RELOC()宏手动计算出需要的物理地址。接着我们借助CR0的PE、PG和WP位开启CPU的页式管理模式(CR0的使用见上一篇文章),但是值得注意的是,这个时候我们还没有建立任何对内存的页式管理机制,这个机制需要我们在后面自己实现。
随后是标号为relocated的一段代码:
1 relocated: 2 3 # Clear the frame pointer register (EBP) 4 # so that once we get into debugging C code, 5 # stack backtraces will be terminated properly. 6 movl $0x0,%ebp # nuke frame pointer 7 8 # Set the stack pointer 9 movl $(bootstacktop),%esp
这段代码直接用0覆盖了原有的ebp(简单粗暴,难怪注释用了“nuke”...),然后把bootstacktop标号指向的虚拟地址赋给了esp。这个bootstacktop是什么?在entry.S的.data节给出了定义:
1 .data 2 ################################################################### 3 # boot stack 4 ################################################################### 5 .p2align PGSHIFT # force page alignment 6 .globl bootstack 7 bootstack: 8 .space KSTKSIZE 9 .globl bootstacktop 10 bootstacktop:
其中,.p2align让该节的起始虚拟地址向后移动至2^PGSHIFT的倍数(这个值在kernel.asm中可以找到为:0xf011 4000,有兴趣可以记一下这个值,后面会再看到),.globl定义了一个名为bootstack的全局符号,这个全局符号指向一片由.space伪指令定义的、大小为KSTKSIZE、被0填充的数据区,这片数据区被作为kernel所使用的栈,而栈顶就是由.globl定义的全局符号bootstacktop。
这段关于堆栈初始化的代码结束后,kernel使用call指令调用了init.c中的i386_init()函数:
1 void 2 i386_init(void) 3 { 4 extern char edata[], end[]; 5 6 // Before doing anything else, complete the ELF loading process. 7 // Clear the uninitialized global data (BSS) section of our program. 8 // This ensures that all static/global variables start out zero. 9 memset(edata, 0, end - edata); 10 11 // Initialize the console. 12 // Can't call cprintf until after we do this! 13 cons_init(); 14 15 cprintf("6828 decimal is %o octal!\n", 6828); 16 17 // Lab 2 memory management initialization functions 18 mem_init(); 19 20 // Drop into the kernel monitor. 21 while (1) 22 monitor(NULL); 23 }
i386_init()的任务是:
(1)用0初始化edata和end指针之间的一片内存区域。其中,edata和end符号是链接器(Linker)的导出符号,edata指向.data节的末尾,end指向.bss节的末尾(尽管我们没有定义.bss节)。实际上,这片被初始化的区域正好是kernel程序的.bss节,目的是为了保证所有以static和global声明的变量初始值为0。
(2)初始化控制台。这部分放在以后介绍。
(3)内存初始化。通过调用pmap.c中的mem_init()函数实现内存的初始化。这部分是实现基于二级页表的虚拟内存管理的关键,也是lab2中要求实现的部分,下面会重点介绍。
(4)启用对kernel的monitor。monitor的代码放在以后介绍。值得注意的是,由于这里是死循环,理论上i386_init()不应该返回(否则转到entry.S里spin标号指向的死循环)。
现在我们来介绍下所有mem_init()会直接或者间接调用到的函数,在介绍的过程中会穿插分析mem_init()的功能。
这里先给出这些函数的函数头:
1 //用于实现页式内存管理的函数 2 static void i386_detect_memory(void); //检测机器的内存信息,主要是初始化npages(物理内存中的页数) 3 //和npages_basemem(640K基本内存中的页数) 4 static void * boot_alloc(uint32_t n); //最底层的内存分配函数,需要实现 5 void page_init(void); //初始化页面数组(page[],即页表)和空闲页面链表,需要实现 6 struct Page * page_alloc(int alloc_flags); //分配一张物理页面,需要实现 7 void page_free(struct Page *pp); //释放一张已经分配的物理页面,需要实现 8 void page_decref(struct Page* pp); //减少页面的引用数 9 pte_t * pgdir_walk(pde_t *pgdir, const void *va, int create); //查找页表(page[])中虚拟地址va对应的入口地址(对应项的高20 10 //bit)或在页表中分配对应的页面(通过占用va对应页表项),需要 11 //实现 12 int page_insert(pde_t *pgdir, struct Page *pp, void *va, int perm); //将物理页pp映射到虚拟地址va上,需要实现 13 struct Page * page_lookup(pde_t *pgdir, void *va, pte_t **pte_store); //查找虚拟地址va对应的物理页pp,需要实现 14 void page_remove(pde_t *pgdir, void *va); //解除虚拟地址va对应的物理页pp到va的映射,需要实现 15 void tlb_invalidate(pde_t *pgdir, void *va); //无效化虚拟地址va对应的TLB入口 16 17 //JOS自带的检查函数 18 static void check_page_free_list(bool only_low_memory); 19 static void check_page_alloc(void); 20 static void check_kern_pgdir(void); 21 static physaddr_t check_va2pa(pde_t *pgdir, uintptr_t va); 22 static void check_page(void); 23 static void check_page_installed_pgdir(void);
整个过程中的虚拟内存的划分如下(复制自“inc/memlayout.h”):
1 /* 2 * Virtual memory map: Permissions 3 * kernel/user 4 * 5 * 4 Gig --------> +------------------------------+ 6 * | | RW/-- 7 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 * : . : 9 * : . : 10 * : . : 11 * |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| RW/-- 12 * | | RW/-- 13 * | Remapped Physical Memory | RW/-- 14 * | | RW/-- 15 * KERNBASE -----> +------------------------------+ 0xf0000000 16 * | Empty Memory (*) | --/-- PTSIZE 17 * KSTACKTOP ----> +------------------------------+ 0xefc00000 --+ 18 * | Kernel Stack | RW/-- KSTKSIZE | 19 * | - - - - - - - - - - - - - - -| PTSIZE 20 * | Invalid Memory (*) | --/-- | 21 * ULIM ------> +------------------------------+ 0xef800000 --+ 22 * | Cur. Page Table (User R-) | R-/R- PTSIZE 23 * UVPT ----> +------------------------------+ 0xef400000 24 * | RO PAGES | R-/R- PTSIZE 25 * UPAGES ----> +------------------------------+ 0xef000000 26 * | RO ENVS | R-/R- PTSIZE 27 * UTOP,UENVS ------> +------------------------------+ 0xeec00000 28 * UXSTACKTOP -/ | User Exception Stack | RW/RW PGSIZE 29 * +------------------------------+ 0xeebff000 30 * | Empty Memory (*) | --/-- PGSIZE 31 * USTACKTOP ---> +------------------------------+ 0xeebfe000 32 * | Normal User Stack | RW/RW PGSIZE 33 * +------------------------------+ 0xeebfd000 34 * | | 35 * | | 36 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 37 * . . 38 * . . 39 * . . 40 * |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| 41 * | Program Data & Heap | 42 * UTEXT --------> +------------------------------+ 0x00800000 43 * PFTEMP -------> | Empty Memory (*) | PTSIZE 44 * | | 45 * UTEMP --------> +------------------------------+ 0x00400000 --+ 46 * | Empty Memory (*) | | 47 * | - - - - - - - - - - - - - - -| | 48 * | User STAB Data (optional) | PTSIZE 49 * USTABDATA ----> +------------------------------+ 0x00200000 | 50 * | Empty Memory (*) | | 51 * 0 ------------> +------------------------------+ --+ 52 * 53 * (*) Note: The kernel ensures that "Invalid Memory" (ULIM) is *never* 54 * mapped. "Empty Memory" is normally unmapped, but user programs may 55 * map pages there if desired. JOS user programs map pages temporarily 56 * at UTEMP. 57 */
现在对上述列出的所有需要实现的函数进行分析:
(1)static void * boot_alloc(uint32_t n)
简单来说,这个函数需要实现的功能是:分配物理上连续的n个字节的空间,并把这片连续空间的起始地址(虚拟地址)作为返回值返回。从pmap.c中已经实现的部分代码来看,boot_alloc()维护了一个静态的指针nextfree,这个指针指向下一片空闲的内存块的起始地址。当nextfree第一次被使用时,该指针指向end符号“按PGSIZE个字节向上对齐(取整)”的地址(这也是为什么我们返回的地址是虚拟地址的一个原因,因为链接器导出的end符号指向的地址就是虚拟地址;另一个原因之前提到了,对我们的C程序而言,只有虚拟地址才有意义,毕竟不能直接访问物理内存)。实质上,这之后对连续内存的分配都是靠指针在内存中的移动完成的。因为nextfree指向下一块空闲的内存块的起始地址,所以这个地址就是我们这次分配得到的空间的起始地址。但是我们需要考虑另外一个问题,这块分配出去的空间应该有多大?显然,这块分配出去的空间至少要有n个字节大小,又因为我们的分配是以页为单位进行的,所以这块空间的大小应该是“n按PGSIZE向上对齐(取整)”这么多个字节。另外值得注意的一点是:nextfree是向内存的高地址方向增长的,因为nextfree向低地址方向增长会覆盖掉kernel程序的.data节和.text节。
具体实现的代码如下:
1 static void * 2 boot_alloc(uint32_t n) 3 { 4 static char *nextfree; // virtual address of next byte of free memory 5 char *result; 6 7 // Initialize nextfree if this is the first time. 8 // 'end' is a magic symbol automatically generated by the linker, 9 // which points to the end of the kernel's bss segment: 10 // the first virtual address that the linker did *not* assign 11 // to any kernel code or global variables. 12 if (!nextfree) { 13 extern char end[]; 14 nextfree = ROUNDUP((char *) end, PGSIZE); 15 } 16 17 // Allocate a chunk large enough to hold 'n' bytes, then update 18 // nextfree. Make sure nextfree is kept aligned 19 // to a multiple of PGSIZE. 20 // 21 // LAB 2: Your code here. 22 if (n > 0) { 23 result = nextfree; 24 nextfree += ROUNDUP(n, PGSIZE); 25 } 26 else if (n == 0) 27 result = nextfree; 28 else 29 result = NULL; 30 31 //test output 32 //cprintf(">> boot_alloc() was called! Entry(virtual address) of new page is: %x\n", (int)result); 33 34 return result; //result here is an virtual address 35 }
(2)void page_init(void)
在分析这个函数的功能之前,我们先看看JOS的设计者在mem_init()的开头做了什么工作。
首先,调用i386_detect_memory()初始化了两个全局变量:npages(物理内存中的页数)和npages_basemem(640K基本内存中的页数)。
接着,通过下面的两行语句用0初始化了一片大小为一页(4096bytes)的、用于存放页目录的内存,然后把这片内存的起始地址(虚拟地址)赋值给了存放页目录入口地址的全局变量kern_pgdir:
1 kern_pgdir = (pde_t *) boot_alloc(PGSIZE); 2 memset(kern_pgdir, 0, PGSIZE);
然后,将UVPT(这是一个虚拟地址)对应的页目录项(kern_pgdir[PDX(UVPT)])设置为kern_pgdir[](即页目录)的入口地址(注意,这里是物理地址,因为使用了PADDR宏),并通过标志位的组合设置了访问权限(这里有点没看懂,JOS的设计者在注释中说是递归,个人感觉是在kern_pgdir的物理地址和UVPT这一虚拟地址建立映射,可能以后再具体解释这句代码):
1 kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
最后,在上面给出的mem_init()代码的第33行处,JOS的设计者要求我们用boot_alloc()给页表pages[]分配空间(注意pages指向虚拟地址),实现如下:
1 pages = (struct Page *)boot_alloc(npages * sizeof(struct Page));
上面已经介绍了boot_alloc()的实现本质上只是移动了指针,然后返回该片内存区域的入口地址(虚拟地址,如上所述),最后通知调用该函数的调用者分配成功。在这里我们将boot_alloc()中声明的静态指针nextfree向高地址移动了页表需要的大小,即npages * sizeof(struct Page)。分配的这块空间是用来存放物理页对应的每个Page结构(二级页表中的页表),也就是说,实际内存中的每个页和这个数组里的每个Page结构是一一对应的。
现在我们可以来分析page_init()的实现了。JOS设计者在一开始就将pages[]数组中的每个Page结构通过指针串成了一条单向链表,用page_free_list这一指针指向链表头(该指针负责后面每个Page结构的分配)。但是这样的实现显然是有问题的,因为这条链表中有一些Page结构对应的物理页框是不能被分配的(比如BIOS和kernel占用的那部分物理内存所在的页框),所以必须从链表上把这些Page结构拆下来。那么现在的问题是怎么定位这些需要拆下来的Page结构在pages[]数组中的位置?要解决这一问题,我们必须要能够根据一个物理地址定位这个地址在内存中位于第几页。在“kern/pmap.h”中,JOS的设计者为我们提供了这类的函数和宏,其中pa2page()就能够根据给出的物理地址计算出该地址处于第几页中,利用这个函数我们可以修改上述的单向链表。注意,我们修改前这条单向链表的结构是这样的(实际上这就是数组pages[],只是数组中每一项又通过指针相连):
根据JOS设计者的要求,第0页必须标记为使用中,不能用于分配,因为这一页是分配给实模式IDT和BIOS的。为了把第0页对应的Page结构从链表上拆下来,我们只要把第1页对应的Page结构中的指针域改写为NULL即可。另外,还有两块内存也是不可分配的。用物理地址的区间来描述就是:
1. 为IO预留的空间[IOPHYSMEM, EXTPHYSMEM)
2. kernel使用的空间[0x0010 0000, end - KERNBASE + PGSIZE + npages*sizeof(struct Page))。
这些值是怎么来的?这里给出解释:
1. 为IO预留的空间是JOS设计者在“inc/memlayout.h”中定义的,IOPHYSMEM=0x000A 0000,EXTPHYSMEM = 0x0010 0000。这两个地址都是物理地址。
2. 这里注意到,EXTPHYSMEM = 0x0010 0000,而kernel的起始物理地址也是这个值(要验证的话请打开kernel.asm,就像之前提到的,可以看到kernel的链接地址是0xf010 0000,这个地址是虚拟地址,再减去KERNBASE得到kernel的物理地址,就是这个值;或者通过ELFHDR宏来看,这个正是我们在上一篇分析Boot Loader中用来载入kernel的物理地址)。也就是说,JOS中IO使用的内存和kernel使用的内存是相连的。那么kernel使用的内存到哪里为止?这时我们就需要借助之前使用过的end符号。上面我们分析过了,在mem_init()的开头,我们用boot_alloc()初始化了一张页目录(kern_pgdir[],大小为PGSIZE,即一页。之所以大小是一页是因为32bit计算机中每个地址都是32bit,即4bytes,而页目录有2^10 = 1024项,每项保存的是一个地址和标志位的组合结果。4*1024 = PGSIZE)和一个与物理页框一一对应的数组(pages[],其中有npages项,总的大小显然是npages*sizeof(struct Page))。boot_alloc()分配内存的过程是指针向后移动的过程,所以kernel的末尾就应该是end + PGSIZE + npages*sizeof(struct Page)。但是,有一点需要注意,end符号的指向的地址是虚拟地址。所以要计算kernel末尾的物理地址还要把这个值减去KERNBASE,最终得到上面的那个物理地址区间。
综上所述,[IOPHYSMEM, end - KERNBASE + PGSIZE + npages*sizeof(struct Page))这一区间内的物理内存是不可分配的。我们用pa2page()函数把这片物理内存的区间换算成pages[]中下标的区间,而下标位于这个区间中的数组项都是应该从链表中被拆除的。区间的头和尾可以这么计算:
1 struct Page *ppg_start = pa2page((physaddr_t)IOPHYSMEM); //at low *physical* address 2 struct Page *ppg_end = pa2page((physaddr_t)((end - KERNBASE) + PGSIZE + sizeof(struct Page)*npages)); //at high *physical* address
但是需要注意,ppg_start和ppg_end对应的Page结构也是不可分配的(因为它们是区间的头和尾),所以我们可以让ppg_start和ppg_end分别自减和自加,然后直接把得到的两个Page结构连接起来,就能把中间的一串不可分配的Page结构从page_free_list链表中拆下。用图来说明就是这样:
具体实现的代码如下:
1 void 2 page_init(void) 3 { 4 // The example code here marks all physical pages as free. 5 // However this is not truly the case. What memory is free? 6 // 1) Mark physical page 0 as in use. 7 // This way we preserve the real-mode IDT and BIOS structures 8 // in case we ever need them. (Currently we don't, but...) 9 // 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE) 10 // is free. 11 // 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must 12 // never be allocated. 13 // 4) Then extended memory [EXTPHYSMEM, ...). 14 // Some of it is in use, some is free. Where is the kernel 15 // in physical memory? Which pages are already in use for 16 // page tables and other data structures? 17 // 18 // Change the code to reflect this. 19 // NB: DO NOT actually touch the physical memory corresponding to 20 // free pages! 21 22 size_t i; 23 for (i = 0; i < npages; i++) { 24 pages[i].pp_ref = 0; 25 pages[i].pp_link = page_free_list; //save old node 26 page_free_list = &pages[i]; //point to new node 27 } //growth of page_free_list 28 29 //remove physical page 0 from page_free_list 30 pages[1].pp_link = NULL; 31 32 //remove continuous pages from page_free_list 33 extern char end[]; //this is an *virtual* address 34 struct Page *ppg_start = pa2page((physaddr_t)IOPHYSMEM); //at low *physical* address 35 struct Page *ppg_end = pa2page((physaddr_t)((end - KERNBASE) + PGSIZE + sizeof(struct Page)*npages)); //at high *physical* address 36 37 //test output 38 //cprintf(">> ppg_start: %x\tppg_end: %x\n", (int)ppg_start, (int)ppg_end); 39 40 ppg_start--; ppg_end++; 41 ppg_end->pp_link = ppg_start; 42 }
(3)struct Page * page_alloc(int alloc_flags)
这个函数没什么太难的地方。前面已经提到,page_free_list是kernel维护的空闲物理页框对应的Page结构的链表,分配一个空闲物理页框只需要从这个链表上拆下来一个Page结构返回就可以了(时间复杂度为O(1))。唯一需要注意的是,如果alloc_flags要求我们用0把分配的物理页框初始化,调用memset()函数时必须传递的是虚拟地址,因为程序不能直接访问物理地址。
具体实现的代码如下:
1 struct Page * 2 page_alloc(int alloc_flags) 3 { 4 // Fill this function in 5 6 //test output 7 //cprintf(">> page_alloc() was called!\n"); 8 9 if (page_free_list == NULL) 10 return NULL; 11 12 struct Page *result = page_free_list; 13 page_free_list = page_free_list->pp_link; 14 15 if (alloc_flags & ALLOC_ZERO) 16 memset(page2kva(result), 0, PGSIZE); 17 18 return result; 19 }
(4)void page_free(struct Page *pp)
这个函数和上面的page_alloc()差不多,只是把一个Page结构装回page_free_list而已。
具体实现的代码如下:
1 void 2 page_free(struct Page *pp) 3 { 4 // Fill this function in 5 6 //test output 7 //cprintf(">> page_free() was called!\n"); 8 9 pp->pp_link = page_free_list; 10 page_free_list = pp; 11 }
对kernel分析的第一篇就先到这里。