《30天自制操作系統》筆記(07)——內存管理
進度回顧
上一篇中處理掉了絕大部分與CPU配置相關的東西。本篇介紹內存管理的思路和算法。
現在想想,從軟件工程師的角度看,CPU也只是一個軟件而已:它的功能就是加載指令、執行指令和響應中斷,而響應中斷也是在加載指令、執行指令。就像火車沿著一條環形鐵軌前進;當中斷發生時,就好像鐵軌岔口處變軌了,火車就順著另一條軌跡走了;走完之后又繞回來重新開始。決定CPU是否變軌的,就是CPU里的特定寄存器。
這是題外話,就此為止。
什么是內存管理
假設內存大小是128MB,應用程序A暫時需要100KB,畫面控制需要1.2MB……。
像這樣,操作系統有時要分配一定大小的內存,用完后又要收回。因此,必須管理好哪些內存空閑可用,哪些正在被占用。這就是內存管理。
內存管理的兩個任務,一是內存分配,一是內存釋放。
如何獲取內存容量
檢查內存容量的方法
要管理內存,首先得知道操作系統所在的這個計算機內存有多大。檢查內存容量的方法很簡單,就是從要檢查的起始位置到最后位置依次寫入一個數值(例如0xaa55aa55),然后按位反轉,檢查是否正確地完成了反轉(變成0x55aa55aa);然后再次反轉,檢查是否正確地完成了反轉。如果反轉結果都是正確的,說明這個地址的內存是存在的;否則就說明內存的最大地址就到此為止了。其代碼如下。
1 unsigned int memtest_sub(unsigned int start, unsigned int end) 2 { 3 unsigned int i, *p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa; 4 for (i = start; i <= end; i += 0x1000) { 5 p = (unsigned int *) (i + 0xffc); 6 old = *p; /* 先記住修改前的值 */ 7 *p = pat0; /* 試寫 */ 8 *p ^= 0xffffffff; /* 反轉 */ 9 if (*p != pat1) { /* 檢查反轉結果 */ 10 not_memory: 11 *p = old; 12 break; 13 } 14 *p ^= 0xffffffff; /* 再次反轉 */ 15 if (*p != pat0) { /* 檢查是否恢復 */ 16 goto not_memory; 17 } 18 *p = old; /* 恢復為修改前的值 */ 19 } 20 return i; 21 }
但直接用C語言來寫這個函數的話,C編譯器會把它優化成這個樣子。
1 unsigned int memtest_sub(unsigned int start, unsigned int end) 2 { 3 unsigned int i, *p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa; 4 for (i = start; i <= end; i += 0x1000) { 5 //全部被優化掉 了 6 } 7 return i; 8 }
C編譯器不會理睬"內存到頭了"這種事情,它只在應用程序的層面看問題。所以它認為for循環里的if判定都是必然為真(或為假)的,認為沒有被其它代碼使用的變量都是沒用的。所以它就干脆把這些"沒用的"代碼刪掉了。
為了解決這個問題,還是用匯編語言來寫這個memtest_sub函數好了。代碼如下。

1 _memtest_sub: ; unsigned int memtest_sub(unsigned int start, unsigned int end) 2 PUSH EDI ; (由于還要使用EBX, ESI, EDI) 3 PUSH ESI 4 PUSH EBX 5 MOV ESI,0xaa55aa55 ; pat0 = 0xaa55aa55; 6 MOV EDI,0x55aa55aa ; pat1 = 0x55aa55aa; 7 MOV EAX,[ESP+12+4] ; i = start; 8 mts_loop: 9 MOV EBX,EAX 10 ADD EBX,0xffc ; p = i + 0xffc; 11 MOV EDX,[EBX] ; old = *p; 12 MOV [EBX],ESI ; *p = pat0; 13 XOR DWORD [EBX],0xffffffff ; *p ^= 0xffffffff; 14 CMP EDI,[EBX] ; if (*p != pat1) goto fin; 15 JNE mts_fin 16 XOR DWORD [EBX],0xffffffff ; *p ^= 0xffffffff; 17 CMP ESI,[EBX] ; if (*p != pat0) goto fin; 18 JNE mts_fin 19 MOV [EBX],EDX ; *p = old; 20 ADD EAX,0x1000 ; i += 0x1000; 21 CMP EAX,[ESP+12+8] ; if (i <= end) goto mts_loop; 22 JBE mts_loop 23 POP EBX 24 POP ESI 25 POP EDI 26 RET 27 mts_fin: 28 MOV [EBX],EDX ; *p = old; 29 POP EBX 30 POP ESI 31 POP EDI 32 RET
知道了內存容量,就可以進行管理了。
關閉CPU高速緩存
486以上的CPU是有高速緩存(cache)的。CPU每次訪問內存,都要將所訪問的地址和內容存入cache,也就是存放成這樣"18號地址的值是54"。如果下次要用18號地址的內容,CPU就不需要再讀內存了(速度慢),而是直接從cache中取得18號地址的內容(速度快)。
如果開啟著CPU高速緩存(cache),上述的檢測代碼就不會正常工作。因為寫入一個內存地址,然后立即讀出,這樣的操作符合cache到的情況,CPU不會從內存讀,而是直接讀cache到的東西。結果,所有的內存都"正常",檢測代碼就起不到作用了。
1 #define EFLAGS_AC_BIT 0x00040000 2 #define CR0_CACHE_DISABLE 0x60000000 3 4 unsigned int memtest(unsigned int start, unsigned int end) 5 { 6 char flg486 = 0; 7 unsigned int eflg, cr0, i; 8 9 /* 確認CPU是386還是486以上的 */ 10 eflg = io_load_eflags(); 11 eflg |= EFLAGS_AC_BIT; /* AC-bit = 1 */ 12 io_store_eflags(eflg); 13 eflg = io_load_eflags(); 14 if ((eflg & EFLAGS_AC_BIT) != 0) { /* 如果是386,即使設定AC=1,AC的值還會自動回到0 */ 15 flg486 = 1; 16 } 17 eflg &= ~EFLAGS_AC_BIT; /* AC-bit = 0 */ 18 io_store_eflags(eflg); 19 20 if (flg486 != 0) { 21 cr0 = load_cr0(); 22 cr0 |= CR0_CACHE_DISABLE; /* 禁止緩存 */ 23 store_cr0(cr0); 24 } 25 26 i = memtest_sub(start, end); 27 28 if (flg486 != 0) { 29 cr0 = load_cr0(); 30 cr0 &= ~CR0_CACHE_DISABLE; /* 允許緩存 */ 31 store_cr0(cr0); 32 } 33 34 return i; 35 }
內存管理算法
假設內存有128MB(0x08000000字節),以4KB(0x1000字節)為單位進行管理。
最簡單的方法
128MB/4KB=32768。所以我們創建32768字節的區域,寫入0表示對應的內存是空閑的,寫入1表示對應的內存是正在使用的。這個方法的名字我沒有查到。
1 char a[32768]; 2 for (i = 0; i < 1024; i++) { 3 a[i] = 1; //一直到4MB為止,標記為正在使用(BIOS、OS等) 4 } 5 for (i = 1024; i< 32768; i++) { 6 a[i] = 0; //剩下的全部標記為空閑 7 }
比如需要100KB的內存,那么只要從a中找出連續25個標記為0的地方就可以了。
1 //從a[j]到a[j + 24]為止,標記連續為0"; 2 j = 0; 3 再來一次: 4 for (i = 0; i < 25; i++) { 5 if (a[j + i] != 0) { 6 j++; 7 if (j < 32768 - 25) goto 再來一次; 8 "沒有可用內存了"; 9 } 10 } 11 //從j * 0x1000開始的100KB空間得到分配 12 for (i = 0; i < 25; i++) { 13 a[j + i] = 1; 14 }
需要收回這100KB的時候,用地址值/0x1000,計算出j就可以了。
1 j = 0x00123000 / 0x1000; 2 for (i = 0; i < 25; i++) { 3 a[j + i] = 0; 4 }
當然,我們可以用1bit來代替1個char,這樣所需的管理空間就可以省下7/8,使用方法則是一樣的。
列表管理
把類似于"從xxx號地址開始的yyy字節的空間是空閑的"這種信息都存儲到列表里。
1 struct FREEINFO { /* 可用狀況 */ 2 unsigned int addr, size; 3 }; 4 5 struct MEMMAN { /* 內存管理 */ 6 int frees, maxfrees, lostsize, losts; 7 struct FREEINFO free[MEMMAN_FREES]; 8 }; 9 struct MEMMAN memman; 10 memman.frees = 1;//可用狀況list中只有1件 11 memman.free[0].addr = 0x00400000;//從0x00400000號地址開始 12 memman.free[0].size = 0x07c00000;//有124MB可用
比如,如果需要100KB的內存,只要查看memman中free的狀況,從中找到100MB以上的可用空間就行了。
1 for (i = 0; i < memman.frees; i++) { 2 if (memman.free[i].size >= 100 * 1024) { 3 //找到可用空間 4 memman.free[i].addr += 100 * 1024; 5 memman.free[i].size -= 100 * 1024; 6 } 7 }
釋放內存時,增加1條可用信息,frees加1。而且還要看看新釋放出的內存與相鄰的內存能不能連到一起,如果可以,就要歸為1條。
與上文的最簡單的方法相比,這種列表管理的方法,占用內存少,且內存的申請和釋放更迅速。
缺點是釋放內存的代碼比較復雜。另外,如果內存變成零零碎碎的,那么需要的MEMMAN里的數組就會超過1000,這是個問題。如果真發生這種情況,只能將一部分零碎的空閑內存都視作被占用的,然后合并。
1 void memman_init(struct MEMMAN *man) 2 { 3 man->frees = 0; /* 可用信息數目 */ 4 man->maxfrees = 0; /* 用于觀察可用狀況:frees的最大值 */ 5 man->lostsize = 0; /* 釋放失敗的內存的大小總和 */ 6 man->losts = 0; /* 釋放失敗次數 */ 7 return; 8 } 9 10 unsigned int memman_total(struct MEMMAN *man) 11 /* 報告空余內存大小的合計 */ 12 { 13 unsigned int i, t = 0; 14 for (i = 0; i < man->frees; i++) { 15 t += man->free[i].size; 16 } 17 return t; 18 } 19 20 unsigned int memman_alloc(struct MEMMAN *man, unsigned int size) 21 /* 分配 */ 22 { 23 unsigned int i, a; 24 for (i = 0; i < man->frees; i++) { 25 if (man->free[i].size >= size) { 26 /* 找到了足夠大的內存 */ 27 a = man->free[i].addr; 28 man->free[i].addr += size; 29 man->free[i].size -= size; 30 if (man->free[i].size == 0) { 31 /* 如果是free[i]變成了0,就減掉一條可用信息 */ 32 man->frees--; 33 for (; i < man->frees; i++) { 34 man->free[i] = man->free[i + 1]; /* 代入結構體 */ 35 } 36 } 37 return a; 38 } 39 } 40 return 0; /* 沒有可用空間 */ 41 } 42 43 int memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size) 44 /* 釋放 */ 45 { 46 int i, j; 47 /* 為便于歸納內存,將free[]按照addr的順序排列 */ 48 /* 所以,先決定應該放在哪里 */ 49 for (i = 0; i < man->frees; i++) { 50 if (man->free[i].addr > addr) { 51 break; 52 } 53 } 54 /* free[i - 1].addr < addr < free[i].addr */ 55 if (i > 0) { 56 /* 前面有可用內存 */ 57 if (man->free[i - 1].addr + man->free[i - 1].size == addr) { 58 /* 可用與前面的可用內存歸納到一起 */ 59 man->free[i - 1].size += size; 60 if (i < man->frees) { 61 /* 后面也有 */ 62 if (addr + size == man->free[i].addr) { 63 /* 也可以與后面的可用內存歸納到一起 */ 64 man->free[i - 1].size += man->free[i].size; 65 /* man->free[i]刪除 */ 66 /* free[i]變成0后歸納到前面去 */ 67 man->frees--; 68 for (; i < man->frees; i++) { 69 man->free[i] = man->free[i + 1]; /* 結構體賦值 */ 70 } 71 } 72 } 73 return 0; /* 成功完成 */ 74 } 75 } 76 /* 不能與前面的可用空間歸納到一起 */ 77 if (i < man->frees) { 78 /* 后面還有 */ 79 if (addr + size == man->free[i].addr) { 80 /* 可用與后面的內容歸納到一起 */ 81 man->free[i].addr = addr; 82 man->free[i].size += size; 83 return 0; /* 成功完成 */ 84 } 85 } 86 /* 既不能與前面歸納到一起,也不能與后面歸納到一起 */ 87 if (man->frees < MEMMAN_FREES) { 88 /* free[i]之后的,向后移動 */ 89 for (j = man->frees; j > i; j--) { 90 man->free[j] = man->free[j - 1]; 91 } 92 man->frees++; 93 if (man->maxfrees < man->frees) { 94 man->maxfrees = man->frees; /* 更新最大值 */ 95 } 96 man->free[i].addr = addr; 97 man->free[i].size = size; 98 return 0; /* 成功完成 */ 99 } 100 /* 不能往后移動 */ 101 man->losts++; 102 man->lostsize += size; 103 return -1; /* 失敗 */ 104 }
總結
內存管理要結合GDT的設定進行。按段(Segment)設計的GDT,內存就得按段申請和回收。按頁設計的GDT,額我不知道,以后再說。
內存管理需要的預備知識還包括"獲取內存容量"、"禁止/啟用高速緩存"、"數據結構-鏈表"。
內存管理的算法還有很多,本篇只給出了兩種最基本最簡單的,夠做個簡易的OS就行了,現在不是深究算法的時候。
文章列表