日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

xv6 内存管理

發布時間:2023/12/9 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 xv6 内存管理 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前文講述了 xv6 的啟動過程,本文接著講述 xv6 內存管理的部分,直接來看。

  • 公眾號:Rand_cs

啟動部分完善

前文只是介紹了啟動的過程,但是各類函數之間的調用,地址的變換,內存布局的變化并沒有詳細說明明,本節來完善。

BIOS

還是從 BIOS 開始,入口點是 0xffff00xffff00xffff0,是一跳轉指令 jmpf000:e05bjmp \ \ f000:e05bjmp??f000:e05b,然后開始執行 BIOS 的代碼,內存低 1M 的頂部 64KB 都是分配給 BIOS 的,所以此時內存布局為:

bootblock

xv6 沒有實際的 MBR,bootasm.S 和 bootmain.c 兩文件聯合在一起編譯成二進制文件 bootblock 放在磁盤最開始的那個扇區,然后被 BIOS 加載到 0x7c000x7c000x7c00 處,從 0x7c000x7c000x7c00 處開始執行

此時內存布局為:

bootmain.c

bootmain 加載內核,來看看是怎么加載的,加載到哪兒。

elf = (struct elfhdr*)0x10000; readseg((uchar*)elf, 4096, 0); //從磁盤讀4096字節到物理地址 0x10000

這里 readseg 函數的意思是從磁盤的 1 扇區讀取 4096字節到物理地址 0x10000 處。內核文件在磁盤的扇區 1 ,注意這里雖然參數傳的是 0,但是函數內部加了 1,所以是從扇區 1 讀取的。這個函數后面講述磁盤再詳述,這里知道作用就行。

0x10000 有什么意義?再來看一眼內存低 1M 的布局圖:

所以沒什么特殊意義,就是找了一塊空閑地兒,來存放內核的開始的 4096 字節。

那這 4096 字節有什么用?這就加載內核了?當然不是,xv6 的內核有 200 多 KB,開始的 4096 字節只是包括了 elf 文件的一些頭部信息:

這是從我虛擬機上截的圖,使用 readelf -h kernel 命令來查看內核的 elf 頭信息,從截圖上可知程序頭的相對 elf 文件開始的偏移量為 52 字節,有 3 個程序頭,每個 32 字節,所以這 4096 字節至少包括內核的 elf 頭和程序頭表,而這是我們加載內核正需要的信息。

此時內存中的布局:

運行 bootmain.c 的時候是將 0x7c00 以下作為棧使用,根據內存低 1M 布局圖可以看出,0x7c00 以下有大約 30K 的空閑空間可用,這段代碼很少,??臻g用不了多少,30K 太足夠了,不會有什么問題。

下面就開始正式加載內核了,加載到哪兒是一個問題,這就需要程序頭中記載的信息了:

ph = (struct proghdr*)((uchar*)elf + elf->phoff); //第一個程序段的位置eph = ph + elf->phnum;for(; ph < eph; ph++){pa = (uchar*)ph->paddr;readseg(pa, ph->filesz, ph->off); //從ph->off所在的扇區讀取ph->filesz字節到物理地址paif(ph->memsz > ph->filesz)stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz); //調用 stosb 將段的剩余部分置零

上下結合來看得知將內核加載到了物理地址的 0x100000 處。

此時的內存布局:

end 為內核末尾地址,不同版本有稍許不同,可以在 kernel.sym 文件中查找,也可以直接讀取 elf 的程序頭,根據 PhysAddr+MemSizePhysAddr + MemSizePhysAddr+MemSize 計算出來。

前面都是在未開啟分頁機制下運行,涉及到的地址都是實際的物理地址,從 bootmain.c 中 跳到 entry.S 就開啟分頁機制,分頁必然要建立頁表,涉及到內存管理,下面一一來看:

臨時頁表

xv6 在啟動的時候建立了一個臨時頁表,在 main.c 文件的最后部分:

pde_t entrypgdir[NPDENTRIES] = {// 將虛擬地址的[0,4M)映射到物理地址[0,4M)[0] = (0) | PTE_P | PTE_W | PTE_PS,// 將虛擬地址[800 0000,800 0000+40 0000)映射到[0,4M)[KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS, };

xv6 定義虛擬地址 0x800 0000 以上為內核部分,虛擬地址空間和物理地址空間具體怎么映射的后面建立正式的頁表時候再說。

為啥要將虛擬地址不同的兩部分映射到相同的物理地址?這需要看 entrypgdir 用在什么地方,entrypgdir 定義在 main.c 中,用在 entry.S 文件中。啟動那篇文章說過,entry.S 主要就是開啟分頁機制。本身代碼是在物理地址低 4M 內,必須保證分頁機制前的線性地址與分頁機制的虛擬地址對應的物理地址一致,也就是必須使開啟分頁機制和跳到高地址之間的指令能夠正確執行

頁面大小擴展

entry.S 代碼里面有這么幾句指令:

# Turn on page size extension for 4Mbyte pages # 開啟頁面大小擴展,每頁 4 M movl %cr4, %eax orl $(CR4_PSE), %eax movl %eax, %cr4

將 CR4 寄存器的 PSE 位置 1,以及設置頁目錄項的 PS 位,便可以設置每頁的大小為 4M,但是此時對虛擬地址的解析有了變化,如果使用二級頁表的話,我們是將虛擬地址的高 10 位作為頁目錄的索引,得到一級頁表的物理地址,將中 10 位作為頁表的索引,得到頁框的物理地址,再加上后面 12 位的偏移地址得到最終目標的物理地址。示意圖如下:

如果是使用一級頁表的話,將虛擬地址的前 20 位作為頁表的索引,得到頁框的物理地址,加上后面 12 位的索引得到最終目標的物理地址,示意圖如下:

但如果是開啟頁面大小擴展,有點類似與一級頁表,但又有所不同,它是將虛擬地址的高 10 位作為頁表的索引,得到頁框的物理地址,加上低 22 位的偏移量得到最終目標的物理地址,示意圖如下:

所以這就解釋了為什么 entrypgdir 簡簡單單的兩項,兩條語句就映射了 4M 的地址空間。那為什么要使用頁面大小擴展呢?我合理的猜測下:就是簡單方便,語句少,想想如果使用二級頁表,頁面大小不進行擴展只有 4K 的情況要怎么映射,兩部分地址空間,得有兩個頁目錄項,對應兩個一級頁表,4M 有 1024 個 4K,得有 1024 個頁表項。雖然 4M 沒有全用,不用全映射,但是總的來說使用頁面大小擴展之后更加簡單方便。

內存管理

建立正式頁表之前先來看看 xv6 是如何對內存進行組織管理的,任何一個操作系統都需要對內存進行管理,將內存以某種方式組織起來,用的時候可以分配,不再使用的時候回收。組織方式常見的有鏈式和位圖,xv6 里面是用鏈表的形式將空閑空間給組織起來,相關代碼在 kalloc.c 文件中,我們來具體分析一下:

首先定義了兩個結構體:

struct run {struct run *next; };struct {struct spinlock lock;int use_lock;struct run *freelist; } kmem;

這兩個結構體什么意思,有什么用?看個圖就明白了:

所以 kmem 就像個內存分配器,這個 freelist 就是這片空閑頁鏈表的鏈頭,分配內存的時候就將它先分配出去,然后每頁里面有一個指針,指向下一個空閑頁。有了這個了解之后來看具體的實現代碼:

char* kalloc(void) {struct run *r; //聲明run結構體指針if(kmem.use_lock) //加鎖acquire(&kmem.lock);r = kmem.freelist; //第一個空閑頁地址賦給rif(r)kmem.freelist = r->next; //鏈頭移動到下一頁,相當于把鏈頭給分配出去了if(kmem.use_lock) //釋放鎖release(&kmem.lock);return (char*)r; //返回第一個空閑頁的地址 }

代碼很簡單,就是加鎖,取鏈頭地址,鏈頭移到下一個空閑頁,釋放鎖,返回取到的鏈頭地址。

void kfree(char *v) //釋放頁v {struct run *r;//這個頁應該在這些范圍內且邊界為4K的倍數if((uint)v % PGSIZE || v < end || V2P(v) >= PHYSTOP) panic("kfree");// Fill with junk to catch dangling refs.memset(v, 1, PGSIZE); //將這個頁填充無用信息,全置為1if(kmem.use_lock) //取鎖acquire(&kmem.lock);r = (struct run*)v; //頭插法將這個頁放在鏈首r->next = kmem.freelist;kmem.freelist = r; if(kmem.use_lock) //釋放鎖release(&kmem.lock); }

基本上是 kalloc 的逆操作,先檢查要釋放的頁合理與否,然后填充無效信息,再取鎖,使用頭插法將這個頁放在鏈首,釋放鎖。從這看出這應該是用的頭插法。

void freerange(void *vstart, void *vend) //連續釋放vstart到vend之間的頁 {char *p;p = (char*)PGROUNDUP((uint)vstart);for(; p + PGSIZE <= (char*)vend; p += PGSIZE)kfree(p); }

還有兩個函數 kinit1,kinit2 是上述 freerange 函數的封裝:

void kinit1(void *vstart, void *vend) //kinit1(end, P2V(4*1024*1024)); {initlock(&kmem.lock, "kmem");kmem.use_lock = 0;freerange(vstart, vend); }void kinit2(void *vstart, void *vend) //kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); {freerange(vstart, vend);kmem.use_lock = 1; }

它倆是在 main.c 的 main() 函數中被調用,調用的參數也已經注釋在后邊。調用這兩個函數就是初始化內存,將內存一頁一頁的使用頭插法鏈在一起。

內核頁表

要了解其他幾個參數還需要先來了解 xv6 的虛擬地址空間和實際的物理地址空間的映射關系,這也有相應的結構體表示:

#define EXTMEM 0x100000 // Start of extended memory #define PHYSTOP 0xE000000 // Top physical memory#define DEVSPACE 0xFE000000 // 一些設備的地址,比如apic的一些寄存器// Key addresses for address space layout (see kmap in vm.c for layout) #define KERNBASE 0x80000000 // 內核的起始虛擬地址 #define KERNLINK (KERNBASE+EXTMEM) // 內核文件的鏈接地址#define V2P(a) (((uint) (a)) - KERNBASE) //內核虛擬地址轉物理地址 #define P2V(a) ((void *)(((char *) (a)) + KERNBASE)) //物理地址轉內核虛擬地址static struct kmap { void *virt;uint phys_start;uint phys_end;int perm; } kmap[] = {{ (void*)KERNBASE, 0, EXTMEM, PTE_W}, // I/O space{ (void*)KERNLINK, V2P(KERNLINK), V2P(data), 0}, // kern text+rodata{ (void*)data, V2P(data), PHYSTOP, PTE_W}, // kern data+memory{ (void*)DEVSPACE, DEVSPACE, 0, PTE_W}, // more devices };

上面那一坨就是說明虛擬地址空間內核部分到物理內存的映射關系,看起來可能很麻雜,做了一張表格和圖:

所以從這張圖可以看出,內核部分的虛擬地址空間和物理地址空間就是一一對應的,只是相差了 0x8000 0000,所以這就是為什么簡單的宏 V2P,P2V 就可以實現虛擬地址物理地址之間的轉換,當然這只是內核部分才行。用戶態部分的我們還沒有涉及,用戶態下的虛擬地址到物理地址之間的轉換就必須要使用頁表了,相關部分在進程我們再詳述。

再者也可以看出 xv6 并沒有使用全部的 4G 地址空間,有很大一部分都沒有使用,除開這部分所有的物理內存實際都映射到內核中去了,那用戶部分呢?用戶部分是通過頁表映射到了物理地址空間的空閑部分,這部分物理地址空間又可以通過 P2V 映射到內核部分去,是不是很繞,后面講述進程的時候慢慢說這部分。

另外關于設備部分是直接映射的,是真的一一對應,虛擬地址和物理地址一樣,這部分地址空間是分配給一些設別的,比如 APIC 的一些寄存器,詳見:

實現上述的映射得建立相應的頁表,來看相關代碼:

#define PDX(va) (((uint)(va) >> PDXSHIFT) & 0x3FF) //高10位 #define PTX(va) (((uint)(va) >> PTXSHIFT) & 0x3FF) //中10位 #define PGADDR(d, t, o) ((uint)((d) << PDXSHIFT | (t) << PTXSHIFT | (o))) //d為高10位,t為中10位,o為低12位,將他們組合成虛擬地址static pte_t * walkpgdir(pde_t *pgdir, const void *va, int alloc) //根據虛擬地址 va 返回相應的頁表項地址 {pde_t *pde; //頁目錄項地址pte_t *pgtab; //一級頁表地址pde = &pgdir[PDX(va)]; //va取高12位->頁目錄項if(*pde & PTE_P){ //若一級頁表存在pgtab = (pte_t*)P2V(PTE_ADDR(*pde)); //取一級頁表的物理地址,轉化成虛擬地址} else {if(!alloc || (pgtab = (pte_t*)kalloc()) == 0) //否則分配一頁出來做頁表return 0;// Make sure all those PTE_P bits are zero.memset(pgtab, 0, PGSIZE); //初始化置0*pde = V2P(pgtab) | PTE_P | PTE_W | PTE_U; //將新分配出來的以及頁表記錄在頁目錄中}return &pgtab[PTX(va)]; //va取中10位->頁表項 } static int mappages(pde_t *pgdir, void *va, uint size, uint pa, int perm) {char *a, *last;pte_t *pte;a = (char*)PGROUNDDOWN((uint)va); //虛擬地址va以4K為單位的下邊界last = (char*)PGROUNDDOWN(((uint)va) + size - 1); //偏移量,所以減1for(;;){if((pte = walkpgdir(pgdir, a, 1)) == 0) //獲取地址a的頁表項地址return -1;if(*pte & PTE_P) //如果該頁本來就存在panic("remap");*pte = pa | perm | PTE_P; //填寫地址a相應的頁表項if(a == last) //映射完了退出循環break;a += PGSIZE;pa += PGSIZE;}return 0; }

mappages 映射虛擬地址 va 到物理地址 pa,映射大小為 size,實現方式將相應的頁表項填進 pgdir 指向的頁表中去。總的來說分為兩步,調用 walkpgdir 獲取虛擬地址相應的頁表項,然后將物理地址屬性位填進這個頁表項。這就是映射一頁的操作,重復這個操作映射從 va 開始的 size 大小區域。

現在有了內核映射的要求和實現方法,可以建立內核正式的頁表了:

#define NELEM(x) (sizeof(x)/sizeof((x)[0])) //x有多少項 pde_t* setupkvm(void) //建立內核頁表 {pde_t *pgdir;struct kmap *k;if((pgdir = (pde_t*)kalloc()) == 0) //分配一頁作為頁目錄表return 0; memset(pgdir, 0, PGSIZE); //頁目錄表置0if (P2V(PHYSTOP) > (void*)DEVSPACE) //PHYSTOP的地址不能高于DEVSPACEpanic("PHYSTOP too high");for(k = kmap; k < &kmap[NELEM(kmap)]; k++) //映射4項,循環4次if(mappages(pgdir, k->virt, k->phys_end - k->phys_start, (uint)k->phys_start, k->perm) < 0) {freevm(pgdir);return 0;}return pgdir; }

setupkvm() 相當于 mappages() 的封裝,它循環四次,將 kmap 給出的信息當作參數傳給 mappages,映射相應的地址空間。

注意 kmap 最后一項的 phys_end 為0,kmap 結構體中聲明的物理地址都是無符號數,所以最后一項k->phys_end - k->phys_start,如此計算也是沒有問題的,對于數值問題有疑惑的請看我這篇文章:

建好頁表就該切換頁表,就是將頁表的及地址賦給 CR3,看下面對 setupkvm() 封裝的函數:

pde_t *kpgdir; void kvmalloc(void) {kpgdir = setupkvm(); //建立頁表switchkvm(); //切換頁表 } void switchkvm(void) {lcr3(V2P(kpgdir)); //加載內核頁表到cr3寄存器,cr3存放的是頁目錄物理地址 }

kpgdir 是個全局變量,為內核頁表的地址,kvmalloc() 調用 setupkvm() 建立頁表,返回的頁表地址賦給 kpgdir,然后調用 switchkvm() 切換成內核頁表,也就是將 kpgdir 的物理地址加載到 CR3 寄存器。

頁表的事完成之后,內核完全運行在高地址之上了,相應的一些結構的地址也得切換到高地址上面去,比如說 GDTR 中存放的 GDT 地址和界限。最開始的 GDT 是在 bootasm.S 文件建立的,放在物理地址值的低 1M,后來分頁機制開啟之后使用的臨時頁表,映射了虛擬地址空間低 4M 和 內核之上的低 4M 到物理地址空間的低 4M,所以 GDTR 中的地址沒問題,CPU 能夠找到 GDT。但是切換成正式頁表之后不再映射虛擬地址空間的低地址部分,低地址部分是給用戶態用的,內核都處于高地址,所以 GDTR 中的地址不再有效。況且 GDT 還需要重新建立正式的 GDT,所以有了如下的 seginit():

void seginit(void) //設置內核用戶的代碼段和數據段 {struct cpu *c;c = &cpus[cpuid()]; //獲取當前CPU//建立段描述符,內核態用戶態的代碼段和數據段 c->gdt[SEG_KCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, 0);c->gdt[SEG_KDATA] = SEG(STA_W, 0, 0xffffffff, 0);c->gdt[SEG_UCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, DPL_USER);c->gdt[SEG_UDATA] = SEG(STA_W, 0, 0xffffffff, DPL_USER);lgdt(c->gdt, sizeof(c->gdt)); //加載到GDTR }

每個 CPU 有自己的結構,cpus 這個結構體數組本身位于內核,內核現已運行在高地址,GDT 放在 CPU 結構體中,那么也就相當于放在了高地址上。設置好段描述符,建立好 GDT 之后,便將 GDT 的新地址和界限寫進 GDTR 寄存器中去。

上述講述了內核頁表的過程,有了這全局的認識之后,來解決上述遺留的一些問題:

  • 為什么要分兩次初始化內存:kinit1() 和 kinit2()
  • 為什么 kinit2() 必須在 startothers() 之后

解決這兩個問題,我們要來看看 xv6 的設計思路,當然只是看和內存相關比較緊密的部分:

最開始內核加載到物理地址 0x10 0000 處,xv6 內核很小,整個內核只有 200 多 K。內核一開始就先運行 entry.S 的代碼,開啟分頁機制,分頁當然得有頁表,為簡單方便將頁面大小擴展到了 4M,制作了一個啟動時用的臨時頁表,映射了低 4M 的內存。entry.S 代碼運行完之后跳到 main() 中去。

int main(void) {kinit1(end, P2V(4*1024*1024)); // phys page allocatorkvmalloc(); // kernel page table/*********/seginit(); // segment descriptors/*********/startothers(); // start other processorskinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()/*********/ }

首先就是初始化內核結束點到 4M 之間的內存,kinit1() 使用的地址是虛擬地址,此時的頁表只映射了低 4M,所以傳的參數為 end 到 P2V(4*1024*1024)。

初始化了 end 到 4M 之間的內存區域之后就可以構建正式的內核頁表映射更多的地址空間,所以緊接著調用了 kvmalloc() 建立內核部分的頁表。

原本內核在低地址,由于分頁機制的開啟,內核跑到高地址上面去了,需要改變一些寄存器中記錄的值,比如記錄 GDT 地址和界限的 GDTR 寄存器,所以有了 seginit() 重新初始化 GDT,然后將 GDT 的虛擬地址和界限寫到 GDTR 中去。

現在已經建立了正式的內核頁表,映射了整個內核部分,有更多的虛擬地址空間可用,所以可以初始化更多的內存了,因此有了 kinit2(),初始化的區域是 4M 到 PHYSTOP,這個宏定義可以在一定范圍內改變,從這個宏定義可以看出,xv6 實際并沒有用到 32 位全部的 4G 空間。

那為什么 kinit2() 必須在 startothers() 后面呢?原因就在于其他 CPU 啟動的時候也是用的那張臨時頁表,只映射了物理地址的低 4M, kinit2() 的初始化內存是用頭插法依次鏈接在頭部的,如果先執行 kinit2() 的話,那么在執行 startothers() 時候給 APs 分配內存的時候就會先分配高處的內存,而這些內存的地址臨時頁表是沒有映射的,就會引發錯誤,所以 kinit2() 必須在 startothers() 之后。

至于其他 APs 的啟動,大都重復 BSP 的過程,只不過 APs 的啟動代碼放在了 0x7000 處,其他的基本一樣就不再贅述了。

本文講述了 xv6 的內存管理部分,完善了啟動過程中的內存布局變化,但也只涉及了內核部分,用戶部分將和進程結合在一起敘述。好啦本文就到這里,有什么錯誤還請批評指正,也歡迎大家來同我討論交流。

  • 公眾號:Rand_cs

找了一個相關資料的網站,有各種手冊:

bochs: The Open Source IA-32 Emulation Project (Tech Specs) (sourceforge.io)

總結

以上是生活随笔為你收集整理的xv6 内存管理的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。