操作系統就用一張大表管理內存?

今天我們不聊具體內存管理的算法,我們就來看看,操作系統用什麼樣的一張表,達到了管理內存的效果。

我們以 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=; 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