文章出處

《30天自制操作系統》筆記(07)——內存管理

進度回顧

上一篇中處理掉了絕大部分與CPU配置相關的東西。本篇介紹內存管理的思路算法

現在想想,從軟件工程師的角度看,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
匯編版本的memtest_sub

知道了內存容量,就可以進行管理了。

關閉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就行了,現在不是深究算法的時候。

請查看下一篇《《30天自制操作系統》筆記(08)——疊加窗口刷新》


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()