操作系統就用一張大表管理內存?
今天我們不聊具體內存管理的算法,我們就來看看,操作系統用什麼樣的一張表,達到了管理內存的效果。
我們以 Linux 0.11 源碼爲例,發現進入內核的 main 函數後不久,有這樣一坨代碼。
void main(void) {
...
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end &= 0xfffff000;
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;
mem_init(main_memory_start,memory_end);
...
}
除了最後一行外,前面的那一大坨的作用很簡單。
其實就只是針對不同的內存大小,設置不同的邊界值罷了,爲了理解它,我們完全沒必要考慮這麼周全,就假設總內存一共就 8M 大小吧。
那麼如果內存爲 8M 大小,memory_end 就是
8 * 1024 * 1024
也就只會走倒數第二個分支,那麼 buffer_memory_end 就爲
2 * 1024 * 1024
那麼 main_memory_start 也爲
2 * 1024 * 1024
你仔細看看代碼邏輯,看是不是這樣?
當然,你不願意細想也沒關係,上述代碼執行後,就是如下效果而已。
你看,其實就是定了三個箭頭所指向的地址的三個邊界變量。具體主內存區是如何管理和分配的,要看 mem_init 裏做了什麼。
void main(void) {
...
mem_init(main_memory_start, memory_end);
...
}
而緩衝區是如何管理和分配的,就要看再後面的 buffer_init 裏幹了什麼。
void main(void) {
...
buffer_init(buffer_memory_end);
...
}
不過我們今天只看,主內存是如何管理的,很簡單,放輕鬆。
進入 mem_init 函數。
#define LOW_MEM 0x100000
#define PAGING_MEMORY (15*1024*1024)
#define PAGING_PAGES (PAGING_MEMORY>>12)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
#define USED 100
static long HIGH_MEMORY = 0;
static unsigned char mem_map[PAGING_PAGES] = { 0, };
// start_mem = 2 * 1024 * 1024
// end_mem = 8 * 1024 * 1024
void mem_init(long start_mem, long end_mem)
{
int i;
HIGH_MEMORY = end_mem;
for (i=0 ; i<PAGING_PAGES ; i++)
mem_map[i] = USED;
i = MAP_NR(start_mem);
end_mem -= start_mem;
end_mem >>= 12;
while (end_mem-->0)
mem_map[i++]=0;
}
發現也沒幾行,而且並沒有更深的方法調用,看來是個好欺負的方法。
仔細一看這個方法,其實折騰來折騰去,就是給一個 mem_map 數組的各個位置上賦了值,而且顯示全部賦值爲 USED 也就是 100,然後對其中一部分又賦值爲了 0。
賦值爲 100 的部分就是 USED,也就表示內存被佔用,如果再具體說是佔用了 100 次,這個之後再說。剩下賦值爲 0 的部分就表示未被使用,也即使用次數爲零。
是不是很簡單?就是準備了一個表,記錄了哪些內存被佔用了,哪些內存沒被佔用。這就是所謂的 “管理”,並沒有那麼神乎其神。
那接下來自然有兩個問題,每個元素表示佔用和未佔用,這個表示的範圍是多大?初始化時哪些地方是佔用的,哪些地方又是未佔用的?
還是一張圖就看明白了,我們仍然假設內存總共只有 8M。
可以看出,初始化完成後,其實就是 mem_map 這個數組的每個元素都代表一個 4K 內存是否空閒(準確說是使用次數)。
4K 內存通常叫做 1 頁內存,而這種管理方式叫分頁管理,就是把內存分成一頁一頁(4K)的單位去管理。
1M 以下的內存這個數組乾脆沒有記錄,這裏的內存是無需管理的,或者換個說法是無權管理的,也就是沒有權利申請和釋放,因爲這個區域是內核代碼所在的地方,不能被 “污染”。
1M 到 2M 這個區間是緩衝區,2M 是緩衝區的末端,緩衝區的開始在哪裏之後再說,這些地方不是主內存區域,因此直接標記爲 USED,產生的效果就是無法再被分配了。
2M 以上的空間是主內存區域,而主內存目前沒有任何程序申請,所以初始化時統統都是零,未來等着應用程序去申請和釋放這裏的內存資源。
那應用程序如何申請內存呢?我們本講不展開,不過我們簡單展望一下,看看申請內存的過程中,是如何使用 mem_map 這個結構的。
在 memory.c 文件中有個函數 get_free_page(),用於在主內存區中申請一頁空閒內存頁,並返回物理內存頁的起始地址。
比如我們在 fork 子進程的時候,會調用 copy_process 函數來複制進程的結構信息,其中有一個步驟就是要申請一頁內存,用於存放進程結構信息 task_struct。
int copy_process(...) {
struct task_struct *p;
...
p = (struct task_struct *) get_free_page();
...
}
我們看 get_free_page 的具體實現,是內聯彙編代碼,看不懂不要緊,注意它裏面就有 mem_map 結構的使用。
unsigned long get_free_page(void) {
register unsigned long __res asm("ax");
__asm__(
"std ; repne ; scasb\n\t"
"jne 1f\n\t"
"movb $1,1(%%edi)\n\t"
"sall $12,%%ecx\n\t"
"addl %2,%%ecx\n\t"
"movl %%ecx,%%edx\n\t"
"movl $1024,%%ecx\n\t"
"leal 4092(%%edx),%%edi\n\t"
"rep ; stosl\n\t"
"movl %%edx,%%eax\n"
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map + PAGING_PAGES-1)
:"di","cx","dx");
return __res;
}
就是選擇 mem_map 中首個空閒頁面,並標記爲已使用。
好了,本講就這麼多,只是填寫了一張大表而已,簡單吧?之後的內存申請與釋放等騷操作,統統是跟着張大表 mem_map 打交道而已,你一定要記住它哦。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/_rTmjHIDCV9ADiJlfo5B3g