1.探测系统物理内存布局
当 ucore 被启动之后,最重要的事情就是知道还有多少内存可用,其基本方法是通过BIOS中断调用来帮助完成的。其中BIOS中断调用必须在实模式下进行,所以在bootloader进入保护模式前完成这部分工作相对比较合适。这些部分由boot/bootasm.S中从probe_memory处到finish_probe处的代码部分完成。
通过BIOS中断获取内存可调用参数为e820h的INT 15h BIOS中断。并且把 e820 映 射结构保存在物理地址0x8000处。具体实现详见boot/bootasm.S
https://chyyuu.gitbooks.io/ucore_os_docs/content/lab2/lab2_3_3_2_search_phymem_layout.html
知识结构及对应联系如下:
详细请看:(https://chyyuu.gitbooks.io/ucore_os_docs/content/lab2/lab2_3_5_probe_phymem_methods.html)
bootasm.S新增的物理内存探测部分
probe_memory:
//对0x8000处的32位单元清零,即给位于0x8000处的
//struct e820map的成员变量nr_map清零
movl $0, 0x8000
xorl %ebx, %ebx
//表示设置调用INT 15h BIOS中断后,BIOS返回的映射地址描述符的起始地址
movw $0x8004, %di
start_probe:
movl $0xE820, %eax // INT 15的中断调用参数
movl $20, %ecx // 设置地址描述符的大小为20字节,其大小等于系统内存映射地址描述符的大小
movl $SMAP, %edx // 设置edx为534D4150h (即4个ASCII字符“SMAP”),这是一个约定
//调用int 0x15中断,要求BIOS返回一个用地址范围描述符表示的内存段信息
int $0x15
//如果eflags的CF位为0,则表示还有内存段需要探测
jnc cont
//探测有问题,结束探测
movw $12345, 0x8000
jmp finish_probe
cont:
//设置下一个BIOS返回的映射地址描述符的起始地址
addw $20, %di
//递增struct e820map的成员变量nr_map
incl 0x8000
//如果INT0x15返回的ebx为零,表示探测结束,否则继续探测
cmpl $0, %ebx
jnz start_probe
finish_probe:
2.以页为单位管理物理内存
在获得可用物理内存范围后,系统需要建立相应的数据结构来管理以物理页(按4KB对齐,且大小为4KB的物理内存单元)为最小单位的整个物理内存,以配合后续涉及的分页管理机制。每个物理页可以用一个 Page数据结构来表示。由于一个物理页需要占用一个Page结构的空间,Page结构在设计时须尽可能小,以减少对内存的占用。Page的定义在kern/mm/memlayout.h中。以页为单位的物理内存分配管理的实现在kern/default_pmm.[ch]。
https://chyyuu.gitbooks.io/ucore_os_docs/content/lab2/lab2_3_3_3_phymem_pagelevel.html
既然我们要以页作为最小单位来管理物理内存,首先我们要为每个页分配一个数据结构来记录相关信息,便于后续的管理。我们也把这些信息存放在内存的一块特定区域。
Page结构
Page结构体的定义(kern/mm/memlayout.h):
/* *
* struct Page - Page descriptor structures. Each Page describes one
* physical page. In kern/mm/pmm.h, you can find lots of useful functions
* that convert Page to other data types, such as phyical address.
* */
struct Page {
// 页帧的 引用计数
int ref;
// 页帧的状态
// bit 0: 此页是否被保留(reserved)
// bit 1: 此页是否可以被分配(free?)
uint32_t flags;
// 记录连续空闲页的数量 只在第一页进行设置
unsigned int property;
// 用于将所有的页帧串在一个双向链表中 这个地方很有趣 直接将 Page 这个结构体加入链表中会有点浪费空间 因此在 Page 中设置一个链表的结点 将其结点加入到链表中 还原的方法是将 链表中的 page_link 的地址 减去它所在的结构体中的偏移 就得到了 Page 的起始地址
list_entry_t page_link;
};
有关page_link,我们到后面再详细分析
现在我们有每个页的结构,为了分配内存,我们需要知道现在有哪些页是空闲(free)的,所以需要一个数据结构来记录那些没被使用的页(能被分配的页)。也就是free_area_t.
free_area_t结构
free_area-t结构体的定义(kern/mm/memlayout.h):
/* free_area_t - maintains a doubly linked list to record free (unused) pages */
typedef struct {
list_entry_t free_list; // the list header
unsigned int nr_free; // # of free pages in this free list
} free_area_t;
结构体中有两个成员变量:
free_list: 一个双向循环链表的指针,这个双向链表记录了没被使用的页。
nr_free: 现在链表中有多少个页(也就是现在能被分配的页)
list_entry_t的定义(lib/list.h):
struct list_entry {
struct list_entry *prev, *next;
};
typedef struct list_entry list_entry_t;
其实就是一个简单的双向链表节点
这里需要注意一点,其实链表节点不是直接指向Page结构,而是指向Page结构中的page_link成员变量。
为什么要这样做,请参考(https://chyyuu.gitbooks.io/ucore_os_docs/content/lab0/lab0_2_6_2_1_linked_list.html).
这样以free_area_t结构的数据为双向循环链表的链表头指针,以Page结构的数据为双向循环链表的链表节点,就可以形成一个完整的双向循环链表,如下图所示:
结构体建立完之后,通过page_init()来遍历之前探测到的物理内存,把已经占用的内存设为reserved,把没能被分配的内存放到free page list 里。
page_init()函数:
/* page_init -
detect physical memory space, reserve already used memory,
then use pmm->init_memmap to create free page list
*/
static void
page_init(void) {
struct e820map *memmap = (struct e820map *)(0x8000 + KERNBASE);
uint64_t maxpa = 0;
cprintf("e820map:\n");
int i;
for (i = 0; i < memmap->nr_map; i ++) {
uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;
cprintf(" memory: %08llx, [%08llx, %08llx], type = %d.\n",
memmap->map[i].size, begin, end - 1, memmap->map[i].type);
if (memmap->map[i].type == E820_ARM) {
if (maxpa < end && begin < KMEMSIZE) {
maxpa = end;
}
}
}
if (maxpa > KMEMSIZE) {
maxpa = KMEMSIZE;
}
extern char end[];
npage = maxpa / PGSIZE;
pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);
for (i = 0; i < npage; i ++) {
SetPageReserved(pages + i);
}
uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);
for (i = 0; i < memmap->nr_map; i ++) {
uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;
if (memmap->map[i].type == E820_ARM) {
if (begin < freemem) {
begin = freemem;
}
if (end > KMEMSIZE) {
end = KMEMSIZE;
}
if (begin < end) {
begin = ROUNDUP(begin, PGSIZE);
end = ROUNDDOWN(end, PGSIZE);
if (begin < end) {
init_memmap(pa2page(begin), (end - begin) / PGSIZE);
}
}
}
}
}
最后,我们需要建立一个物理内存页管理器框架。
3.物理内存页分配算法实现
简单来说,当上层需要请求物理内存时,我们需要有个管理器能够分配物理内存页给上层。为此,管理器需要维护一个数据结构,which 能实时保存当前可供分配的物理内存页。这数据结构就是上面的free_area 双向循环链表。最开始,所有的物理页对应的Page结构都在这链表中。
pmm_manager结构
物理内存页管理器框架pmm_manager,这个数据结构定义了实现内存分配算法的关键函数指针
struct pmm_manager {
const char *name; //物理内存页管理器的名字
void (*init)(void); //初始化内存管理器
void (*init_memmap)(struct Page *base, size_t n); //初始化管理空闲内存页的数据结构
struct Page *(*alloc_pages)(size_t n); //分配n个物理内存页
void (*free_pages)(struct Page *base, size_t n); //释放n个物理内存页
size_t (*nr_free_pages)(void); //返回当前剩余的空闲页数
void (*check)(void); //用于检测分配/释放实现是否正确的辅助函数
};
接下来,就是manager结构体下面成员函数的实现
default_init_memap()
/*对一个连续的、可被分配的内存块(包含n页)进行初始化:
1.对这一块内存的每一页:
设p->flags:
PG_reserved位(bit 0):设为0 未被保留
PG_property位(bit 1):设为1 还未被分配
设p->property为0
设p->ref为0
2.将这一页插入free_list
3.nr_free加上新加入的n
4.把第一块的property设为n
*/
static void
default_init_memmap(struct Page *base, size_t n) {
assert(n > 0);
struct Page *p = base;
for (; p != base + n; p ++) {
assert(PageReserved(p));
p->flags = 0;
SetPageProperty(p);
p->property = 0;
set_page_ref(p, 0);
list_add_before(&free_list, &(p->page_link));
}
nr_free += n;
//first page
base->property = n;
}
default_alloc_pages()
/*为空间请求分配指定大小(n)的空间
1.遍历free_list找出第一个大于等于n的内存块
2.找到对应内存块后(其实是找到free_list中第一个property >= n 的页p), 遍历free_list,对这n个页进行设置:
PG_reserved位(bit 0):设为1 已被占用
PG_property位(bit 1): 设为0 已被分配
3.将这n个页从free_list中删除
4.将free_list的原第n+1个页(我们刚刚分配的块的下一页)的property置为p->property - n, 设置为新块
5.nr_free减去分配的n
6.返回分配的内存块的头页p
*/
static struct Page *
default_alloc_pages(size_t n) {
assert(n > 0);
if (n > nr_free) {
return NULL;
}
list_entry_t *le, *len;
le = &free_list;
while((le=list_next(le)) != &free_list) {
struct Page *p = le2page(le, page_link);
if(p->property >= n){
ClearPageProperty(p);
SetPageReserved(p);
int i;
for(i=0;i<n;i++){
len = list_next(le);
struct Page *pp = le2page(le, page_link);
SetPageReserved(pp);
ClearPageProperty(pp);
list_del(le);
le = len;
}
if(p->property>n){
(le2page(le,page_link))->property = p->property - n;
}
nr_free -= n;
return p;
}
}
return NULL;
}
default_free_pages()
/*将指定页开始,连续n页的内存块释放,重新插入free_list
1.遍历free_list,找到插入点。(第一个地址大于base的页p的位置)
2.将从base开始的n页逐个插入
3.对这一块内存的每一页:
设p->flags:
PG_reserved位(bit 0):设为0 未被保留
PG_property位(bit 1):设为1 还未被分配
设p->property为0
设p->ref为0
4.判断是否能合并
A.若能向前合并(base + n == p)
将base->property += p->property
p->property =0
B.若能向后合并(base - 1 == )
向前遍历找到上一块的头页,合并
5.nr_free加上n
*/
static void
default_free_pages(struct Page *base, size_t n) {
assert(n > 0);
assert(PageReserved(base));
list_entry_t *le = &free_list;
struct Page * p;
while((le=list_next(le)) != &free_list) {
p = le2page(le, page_link);
if(p>base){
break;
}
}
//list_add_before(le, base->page_link);
for(p=base;p<base+n;p++){
list_add_before(le, &(p->page_link));
}
base->flags = 0;
set_page_ref(base, 0);
ClearPageProperty(base);
SetPageProperty(base);
base->property = n;
p = le2page(le,page_link) ;
if( base+n == p ){
base->property += p->property;
p->property = 0;
}
le = list_prev(&(base->page_link));
p = le2page(le, page_link);
if(le!=&free_list && p==base-1){
while(le!=&free_list){
if(p->property){
p->property += base->property;
base->property = 0;
break;
}
le = list_prev(le);
p = le2page(le,page_link);
}
}
nr_free += n;
return ;
}
4.实现分页机制
get_pte()
//get_pte - get pte and return the kernel virtual address of this pte for la
// - if the PT contians this pte didn't exist, alloc a page for PT
pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
int i = PDX(la); // la的对应PDE项在PD中的索引
uintptr_t pa;
pde_t cpde = *(pgdir + i); // la的对应PDE
if (!(cpde & PTE_P)){ // 当cpde not present
struct Page *page;
if (!create || (page = alloc_page()) == NULL) {
return NULL;
}
set_page_ref(page, 1);
pa = page2pa(page); // 得到分配页面(新建的PT所在页)对应的物理地址
memset(KDDR(pa), 0, PGSIZE);
cpde = pa |PTE_U | PTE_W | PTE_A; // 用新建PT的物理地址更新pde的内容
// PDE_ADDR(cpde): 通过pde获得对应的PT的位置
return (pte_t*)(KDDR(PDE_ADDR(cpde)) + PTX(la));
}
page_remove_pte()
//page_remove_pte - free an Page sturct which is related linear address la
// - and clean(invalidate) pte which is related linear address la
//note: PT is changed, so the TLB need to be invalidate
// pgdir: the kernel virtual base address of PDT
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
if ((*ptep)&PTE_P){
struct Page *page = pte2page(*ptep);
page_ref_dec(page);
if (!(page->ref)){
free_page(page);
}
ptep = 0;
tlb_invalidate(pgdir, la);
}
}
pmm.c
void
pmm_init(void) {
//We need to alloc/free the physical memory (granularity is 4KB or other size).
//So a framework of physical memory manager (struct pmm_manager)is defined in pmm.h
//First we should init a physical memory manager(pmm) based on the framework.
//Then pmm can alloc/free the physical memory.
//Now the first_fit/best_fit/worst_fit/buddy_system pmm are available.
init_pmm_manager();
// detect physical memory space, reserve already used memory,
// then use pmm->init_memmap to create free page list
page_init();
//use pmm->check to verify the correctness of the alloc/free function in a pmm
check_alloc_page();
// create boot_pgdir, an initial page directory(Page Directory Table, PDT)
boot_pgdir = boot_alloc_page();
memset(boot_pgdir, 0, PGSIZE);
boot_cr3 = PADDR(boot_pgdir);
check_pgdir();
static_assert(KERNBASE % PTSIZE == 0 && KERNTOP % PTSIZE == 0);
// recursively insert boot_pgdir in itself
// to form a virtual page table at virtual address VPT
boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W;
// map all physical memory to linear memory with base linear addr KERNBASE
//linear_addr KERNBASE~KERNBASE+KMEMSIZE = phy_addr 0~KMEMSIZE
//But shouldn't use this map until enable_paging() & gdt_init() finished.
boot_map_segment(boot_pgdir, KERNBASE, KMEMSIZE, 0, PTE_W);
//temporary map:
//virtual_addr 3G~3G+4M = linear_addr 0~4M = linear_addr 3G~3G+4M = phy_addr 0~4M
boot_pgdir[0] = boot_pgdir[PDX(KERNBASE)];
enable_paging();
//reload gdt(third time,the last time) to map all physical memory
//virtual_addr 0~4G=liear_addr 0~4G
//then set kernel stack(ss:esp) in TSS, setup TSS in gdt, load TSS
gdt_init();
//disable the map of virtual_addr 0~4M
boot_pgdir[0] = 0;
//now the basic virtual memory map(see memalyout.h) is established.
//check the correctness of the basic virtual memory map.
check_boot_pgdir();
print_pgdir();
}
至今为止ucore的物理内存的使用情况分析
1.加电之前:
0XFFFF 0000 ~ 0XFFFF FFFF: ROM占用
我们知道,80386把ROM给编址在了物理内存最高端的64KB区域。Intel又通过某种映射机制,将存在ROM里的BIOS代码映射至物理内存1MB的最高端64KB区域,即0XF0000~0X100000这块区域。至于为什么要映射至这里,其实是为了向8086CPU兼容,详细请看(https://mp.csdn.net/editor/html/114376926)。
0X000F 0000 ~ 0X0010 0000: 从ROM映射的BIOS代码
2.装载bootloader:
BIOS做完计算机硬件自检和初始化后,会选择一个启动设备(例如软盘、硬盘、光盘等),并且读取该设备的第一扇区(即主引导扇区或启动扇区)到内存一个特定的地址0x7c00处,然后CPU控制权会转移到那个地址继续执行。至此BIOS的初始化工作做完了,进一步的工作交给了ucore的bootloader。
我们不用关心BIOS是怎么将bootloader加载进内存中的,我们只需要知道bootloader被BIOS加载至物理内存的0x7c00处。那bootloader是在哪个位置结束的呢?注意,BIOS是读取了一个扇区(512 bytes)的内容进入内存的,而这块内容就是bootloader,所以bootloader在内存中占据了512 bytes的大小,即 B的大小,所以我们可以认为这块内存的结束位置是0x7c00 + 0x100 = 0x7d00。
0x0000 7c00 ~ 0x0000 7d00: bootloader占用
bootloader包括bootasm.S与bootmain.c两部分,为了让bootmain.c运行,bootasm.S需要初始化一块堆栈供bootmain.c使用。我们可以看到bootasm.S有这样一段代码
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
call bootmain
通过将栈底寄存器ebp置0,栈顶寄存器esp置为start代码段的初位置(bootasm.S的首位置),即0x7c00,我们开辟了一块从物理内存最低位置0x0到0x7c00的堆栈区。
而这块堆栈区同时也作为之后装载的ucore的可用堆栈区。
0x0000 0000 ~ 0x0000 7c00: 分配给ucore的堆栈
3.装载ucore
当bootmain.c接手后,它就开始将ucore装载进物理内存中。
我们来看bootmain.c的装载过程
#define SECTSIZE 512
#define ELFHDR ((struct elfhdr *)0x10000)
/* bootmain - the entry of bootloader */
void
bootmain(void) {
// read the 1st page off disk
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// is this a valid ELF?
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// load each program segment (ignores ph flags)
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// call the entry point from the ELF header
// note: does not return
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
程序首先将一页大小的内容从硬盘读出,放入物理内存0x10000处(此时逻辑地址等于物理地址)。而这一页的内容,就是ucore(KERNEL)的ELF header。
所以从0x0001 0000处开始的、一页大小的物理内存空间存放的是ucore内核的ELF header。
0x0001 0000 ~ 0x0001 1000: KERNEL的ELF header
有了ELF header,就能通过e_phoff定位到硬盘文件中Program header的位置,就可以通过Program header提供的信息将KERNEL的各个段载入至指定的物理内存地址(再次强调,现在的 逻辑地址 = 线性地址 = 物理地址,其实program header里存储的还是逻辑(虚拟)地址。
注意,readseg的第一个参数值为(ph->p_va & 0x00FF FFFF)。这表明,对program header里指定的每一段的载入地址(为虚拟地址),只取他们的前24位的值作为物理地址载入内存。
我们用readelf看看kernel的program header
可以看到kernel被分为三段,分别对应TEXT, DATA和BSS段。
经过前面对VirtAddr的处理,代码段被载入至物理内存的0x0010 0000处,数据段被载入至0x0011 5000处。
由此可看出,这里ucore内核的虚拟地址(或可直接称为线性地址),与物理地址存在着这样一个映射关系:
Virtul Address = Linear Address = Logical Address + 0xC000 0000
bootmain.c将kernel全部载入了内存后,就把控制权交给了kernel。而kernel的大小我们也能用readelf得出
0x0010 0000 ~ end: ucore kernel
其中,end为kernel.ld文件中定义的bss段结束地址。