Part A 物理頁管理
Exercise1
補全在kern/pmap.c下的幾個函數(shù)涯贞。
boot_alloc()
mem_init() (only up to the call to check_page_free_list(1))
page_init()
page_alloc()
page_free()
boot_alloc()
在JOS中枪狂,一開始的物理內存布局如下圖所示
虛擬內存布局
在代碼中,所有的變量的地址都是虛擬地址宋渔,JOS中虛擬地址到物理地址的轉換很簡單:
虛擬地址 = 物理地址 + KERNBASE(0xF0000000)
boot_alloc()中的end代表的就是內核代碼的最上端州疾,即內核代碼的末尾,此位置往上的物理內存都可以分配皇拣。當申請n字節(jié)大小空間的內存時严蓖,將當前nextfree保存在result當做函數(shù)返回值,然后將其向后移動ROUNDUP(n, PGSIZE)氧急,此時[result, nextfree)的空間就分配出來了颗胡。因為是按頁管理內存,所以分配的內存大小需要頁對齊态蒂。
static void *
boot_alloc(uint32_t n)
{
static char *nextfree; // virtual address of next byte of free memory
char *result;
// end: bss段的末尾杭措,正好是kernel的末尾的指針,第一個未使用的虛擬地址
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}
// cprintf("nextfree: %x\n", nextfree);
// LAB 2: Your code here.
// 返回的是上一次的地址钾恢,然后再將nextfree往后移動手素,相當于分配空間
result = nextfree;
if(n != 0) {
nextfree = ROUNDUP(nextfree + n, PGSIZE);
}
return result;
}
mem_init() 只需要完成到調用check_page_free_list(1)之前
在內核代碼中每個物理頁都由一個PageInfo的數(shù)據(jù)結構來標識鸳址,一共有npages個物理頁。所有的PageInfo組成一個pages數(shù)組泉懦。所以在mem_init需要先對pages結構進行物理內存分配稿黍。
之后所出現(xiàn)的物理頁其實是指PageInfo,代碼中對物理頁的操作其實都是操作PageInfo這個結構
struct PageInfo {
// Next page on the free list.
struct PageInfo *pp_link;
// pp_ref is the count of pointers (usually in page table entries)
// to this page, for pages allocated using page_alloc.
// Pages allocated at boot time using pmap.c's
// boot_alloc do not have valid reference count fields.
uint16_t pp_ref;
};
void
mem_init(void)
{
...
// Allocate an array of npages 'struct PageInfo's and store it in 'pages'.
// The kernel uses this array to keep track of physical pages: for
// each physical page, there is a corresponding struct PageInfo in this
// array. 'npages' is the number of physical pages in memory. Use memset
// to initialize all fields of each struct PageInfo to 0.
// Your code goes here:
// npages: 還有多少頁物理內存崩哩,每個頁都要有一個PageInfo
pages = (struct PageInfo*)boot_alloc(sizeof(struct PageInfo) * npages);
// cprintf("npages: %d\n", npages);
memset(pages, 0, npages * sizeof(struct PageInfo));
//////////////////////////////////////////////////////////////////////
...
}
page_init()
分配完內存后自然就要對數(shù)據(jù)結構進行初始化巡球,即將物理內存中的每一頁都與pageInfo關聯(lián),其中分為可用頁和不可用頁邓嘹。物理內存頁到pages數(shù)組下標的映射關系為: 地址/PGSIZE(4k)酣栈。根據(jù)提示可知,一共有兩大塊空閑物理內存塊汹押。[1, npages_basemem)矿筝。第二塊就是從內核代碼往后,這個地址可以用boot_alloc(0)取到棚贾,即分配了pages內存之后的地址窖维。但這個地址在代碼中是虛擬地址,所以需要將其轉換成物理地址妙痹,可以用PADDR()宏來轉換铸史。所以第二塊范圍就是[PADDR(boot_alloc(0)/PGSIZE),npages)怯伊。找出這些空閑頁后需要用page_free_list鏈表串起來琳轿。方便后續(xù)內存分配。
void
page_init(void)
{
// The example code here marks all physical pages as free.
// However this is not truly the case. What memory is free?
// 1) Mark physical page 0 as in use.
// This way we preserve the real-mode IDT and BIOS structures
// in case we ever need them. (Currently we don't, but...)
// 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)
// is free.
// 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must
// never be allocated.
// 4) Then extended memory [EXTPHYSMEM, ...).
// Some of it is in use, some is free. Where is the kernel
// in physical memory? Which pages are already in use for
// page tables and other data structures?
//
// Change the code to reflect this.
// NB: DO NOT actually touch the physical memory corresponding to
// free pages!
size_t i;
for (i = 1; i < npages_basemem; i++) {
pages[i].pp_ref = 0;
// 構成一個鏈表
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
size_t first_free_page = (size_t)PADDR(boot_alloc(0))/PGSIZE;
// cprintf("npages_basemem: %d\n first_free_page: %d\n", npages_basemem, first_free_page);
for (i = first_free_page; i < npages; i++) {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
pages[0].pp_ref = 1;
pages[0].pp_link = NULL;
for(i = npages_basemem; i < first_free_page; i++) {
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
}
page_alloc()
空閑物理頁的分配耿芹。在空閑物理頁鏈表中取出一個物理頁即可利赋。返回的是PageInfo*,這個怎么與物理內存中的物理頁對應呢猩系?
- 注意: 兩個指針相減,結果并不是兩個指針數(shù)值上的差中燥,而是把這個差除以指針指向類型的大小的結果寇甸。
可以用page2pa(PageInfo*)的宏,因為pages數(shù)組是連續(xù)的物理內存疗涉,所以直接將PageInfo* pp 的地址減去pages就可以知道在數(shù)組中的下標是多少拿霉。在乘以4K就可以得到物理地址了: (pp-pages) << PGSHIFT。PGSHIFT = 1<<12 = 4096 = 4k咱扣。
struct PageInfo *
page_alloc(int alloc_flags)
{
// Fill this function in
if (page_free_list == NULL) {
return NULL;
}
struct PageInfo* pageInfo = page_free_list;
page_free_list = page_free_list->pp_link;
pageInfo->pp_link = NULL;
if (alloc_flags & ALLOC_ZERO) {
// 內核虛擬地址空間映射到物理地址空間绽淘,直接減去kernbase
memset(page2kva(pageInfo), 0, PGSIZE);
// cprintf("page2kva(pageInfo): %x %x %d\n", pageInfo, page2kva(pageInfo), PGSIZE);
}
return pageInfo;
}
page_free()
這個比較簡單。頁面釋放闹伪,將物理頁重新插入到page_free_list中沪铭。前提是要保證該頁面沒有被引用壮池,并且也不在空閑鏈表中。
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.
// cprintf("pp->pp_ref: %d pp->pp_link: %d\n", pp->pp_ref, pp->pp_link == NULL);
assert(pp->pp_ref == 0 && pp->pp_link == NULL);
pp->pp_link = page_free_list;
page_free_list = pp;
}
Part B 虛擬內存
Exercise2
閱讀Intel 80386 Reference Manual的第5第6章杀怠。
在x86結構下椰憋,使用的是分段分頁機制,虛擬地址轉換為物理地址需要中間還需要經(jīng)歷線性地址(分段的過程)赔退。
在JOS中,虛擬地址=線性地址硕旗,為什么呢窗骑?因為在boot/boot.S中把所有的段地址都設置成了0 到0xffffffff,即段基址都等于0漆枚,相當于0+offset创译,所以就沒有分段的效果了。這樣我們就可以專注于實現(xiàn)分頁機制了浪读。
Exercise3
使用qemu-debug下的xp命令查看物理地址的內容昔榴。因為gdb只能獲取到虛擬地址,所以需要使用qemu的下的debug模式才能查看物理地址碘橘。
我覺得直接在程序中用cprintf也可以查看互订。
Exercise4
補全kern/pmap.c下的這些函數(shù),實現(xiàn)頁表管理痘拆。
pgdir_walk()
boot_map_region()
page_lookup()
page_remove()
page_insert()
在補全這些函數(shù)之前仰禽,需要先明白一個圖的含義。JOS采用的是二級頁表機制纺蛆,主要由五個元素組成吐葵,頁目錄表-頁目錄項(PDE, page diretory entry),頁表-頁表項(PTE, page table entry)桥氏,物理頁温峭。PDE和PTE存儲的都是地址。
其中一個頁目錄項對應一個頁表字支,一個頁表項對應一個物理頁凤藏。頁目錄表的地址存儲在CR3寄存器中。
pgdir_walk()
根據(jù)(頁目錄表堕伪,虛擬地址揖庄,創(chuàng)建標志)找到該虛擬地址所對應的物理頁的虛擬地址。
通過PDX獲得va的頁目錄項在頁目錄表中的偏移取得PDE欠雌,如果該PDE所指向的PT是空的話且create == 1蹄梢,那就創(chuàng)建一個頁目錄表,即申請一頁的物理內存富俄。并設置為用戶可讀可寫禁炒。然后再根據(jù)PTX獲得va在頁表項在頁表中的偏移獲取PTE而咆,返回此PTE的地址。
PTE_ADDR(*pde)的作用是去掉后面的權限位齐苛。
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
int pde_index = PDX(va);
int pte_index = PTX(va);
pde_t *pde = &pgdir[pde_index];
if (!(*pde & PTE_P)) {
if (create) {
struct PageInfo *page = page_alloc(ALLOC_ZERO);
if (!page) return NULL;
page->pp_ref++;
*pde = page2pa(page) | PTE_P | PTE_U | PTE_W;
} else {
return NULL;
}
}
pte_t *p = (pte_t *) KADDR(PTE_ADDR(*pde));
return &p[pte_index];
}
boot_map_region()
之前的pgdir_walk是取到頁表項翘盖,但頁表項還未真正的映射到物理頁上,此函數(shù)將從va開始的大小為size的地址按頁從物理地址pa開始映射凹蜂。相當于對頁表項賦值馍驯。
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
// 頁表項映射到物理地址, 頁表本身需要物理地址存儲,pgdir_walk得到頁表存儲的虛擬地址
size_t i;
for (i = 0; i < size; va += PGSIZE, pa += PGSIZE, i += PGSIZE) {
pte_t* pte = pgdir_walk(pgdir, (void*)va, 1);
// 不需要 *pte & PTE_P
if (pte == NULL) {
panic("error");
}
*pte = pa | perm | PTE_P;
}
}
page_lookup()
返回頁表項所對應的物理頁的虛擬地址玛痊,并把頁表項存儲在pte_store中绍绘。**pte_store二級指針相當于傳入指針的引用永脓。
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
pte_t* pte = pgdir_walk(pgdir, va, 0);
if (pte == NULL || !(*pte & PTE_P)) {
return NULL;
}
if (pte_store) {
*pte_store = pte;
}
return (struct PageInfo*)pa2page(PTE_ADDR(*pte));
}
page_remove()
清空頁表項對應的物理頁,并把物理頁引用減減牡借。
void
page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
pte_t* pte_store;
struct PageInfo* pp = page_lookup(pgdir, va, &pte_store);
if(pp == NULL || !(*pte_store & PTE_P))
return;
page_decref(pp);
*pte_store = 0;
tlb_invalidate(pgdir, va);
}
page_insert()
給頁表項賦值一個物理頁酵使。
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
pte_t* pte = pgdir_walk(pgdir, va, 1);
if (pte == NULL) {
return -E_NO_MEM;
}
pp->pp_ref++;
if (*pte & PTE_P) {
page_remove(pgdir, va);
}
*pte = page2pa(pp) | perm | PTE_P;
// cprintf("page_insert: %x\n", *pte);
return 0;
}
Part 3 內核地址空間
JOS的內核空間為[UTOP, KERNBASE),一共為256MB对省。
填充完整mem_init()蝗拿,將虛擬內核地址空間映射到物理地址上。
- [UPAGES, UPAGES+PTSIZE)這段空間是pages數(shù)組的空間蒿涎,將其映射到PADDR(pages)上哀托。
- [KSTACKTOP-KSTKSIZE, KSTACKTOP)是內核棧的空間,將其映射到PADDR(bootstack)
- [KERNBASE, 2^32-1)劳秋,其中32位系統(tǒng)無法計算 2^32仓手,但 2^32-1 == -KERNBASE。這段地址從物理地址0開始映射玻淑。
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
PTE_U);
boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
boot_map_region(kern_pgdir, KERNBASE, -KERNBASE, 0, PTE_W);