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"); } }