操作系統就用一張大表管理內存?
今天我們不聊具體內存管理的算法,我們就來看看,操作系統用什么樣的一張表,達到了管理內存的效果。
我們以 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 打交道而已,你一定要記住它哦。