Linux使用者空間記憶體管理

oscarwin發表於2018-09-26

虛擬記憶體

每個程式都有自己獨立的地址空間,程式的資料儲存在這個地址空間中,使得程式之間相互不影響。以32位x86的Linux系統為例,其地址空間範圍是0x00000000-0xFFFFFFFF,也就是4G。這4G並不是真實的4G實體記憶體,設想一下,如果每個程式都佔用4G的實體記憶體,那即使再大的記憶體條也抗住不這樣的消耗。因此,現代作業系統都採用了虛擬記憶體的設計。虛擬記憶體和實體記憶體都按照一定的大小分成很多頁,比如每頁是4k的話,4G的虛擬記憶體就可以分成1048576頁。虛擬記憶體的頁通過一張對映關係表與實際實體記憶體的頁建立對應關係,如下圖所示。

圖1

4G的虛擬地址空間並非都被使用,很少有程式會用滿虛擬地址空間,對於使用了的虛擬地址也並不是全部都對映到實體記憶體。實際上只有一部分對映到實體記憶體,一部分對映到磁碟的交換區(swap分割槽),這樣就大大減少了單個程式對實體記憶體的佔用。

程式訪問虛擬地址空間的某個地址的過程是這樣的:程式訪問某個地址,首先根據這個地址計算得到分頁,然後查詢對映關係表,如果該分頁對映到了實體記憶體幀上,則從實體記憶體中的資料取出返回;如果該分頁沒有對映到實體記憶體幀上,則將該分頁的資料從磁碟載入到記憶體中,並更新對映關係表。之所以可以採用這種方式來設計有兩個前提原因:

(1)剛被訪問過的資料,很有可能被再次訪問;

(2)二八原則——80%的時間都在訪問20%的資料;

Linux記憶體的分佈

Linux的記憶體管理將這4GB的地址分為兩個部分,一部分是核心空間的記憶體,一部分是使用者空間的記憶體,核心空間佔據3G-4G範圍的地址,使用者空間佔據0G-3G範圍的地址。核心空間是作業系統核心使用記憶體,使用者空間就是執行在作業系統上的程式可以使用的記憶體。

Linux使用者空間記憶體管理

Linux核心空間

Linux核心空間是Linux核心使用的地址空間,Linux核心總是駐留在記憶體中,使用者級程式不能訪問核心的地址空間。之前所提到的虛擬記憶體的頁表與實體記憶體的對映關係表就儲存在核心的地址空間裡。核心空間的記憶體分配非常複雜,本文主要討論使用者空間的記憶體分配。

Linux使用者空間

Linux使用者地址空間從低位到高位的順序可以分為:文字段(Text Segment)、初始化資料段(Data Segment)、未初始化資料段(Bss Segment)、堆(Heap)、棧(Stack)和環境變數區(Environment variables)

文字段

使用者空間的最低位是文字段,包含了程式執行的機器碼。文字段具有隻讀屬性,防止程式意外修改了指令導致程式出錯。而且對於多程式的程式,可以共享同一份程式程式碼,這樣減少了對實體記憶體的佔用。

但是文字段並不是從0x0000 0000開始的,而是從0x0804 8000開始。0x0804 8000以下的地址是保留區,程式是不能去訪問該地址段的資料,因此C語言中將為空的指標指向0。

初始化資料段

文字段上面就是初始化的資料段,資料段包含顯示初始化的全域性變數和靜態變數。當程式被載入到記憶體中時,從可執行檔案中讀取這些資料的值,並載入到記憶體。因此,可執行檔案中需要儲存這些變數的值。

Bss

Bss段包含未初始化的全域性變數和靜態變數,還包含顯示初始化為0的全域性變數(根據編譯器的實現)。當程式被載入到記憶體中時,這一段記憶體就會被初始化為0。可執行檔案中只需要儲存這一段記憶體的起始地址就行,因此減小了可執行檔案的大小。

堆從下自上增長(根據實現),用於動態分配記憶體。堆的頂端成為program break,可以通過brk和sbrk函式調整堆頂的位置。c語言通過malloc函式實現動態記憶體分配,通過free釋放分配的記憶體,後面會詳細描述這兩個函式的實現。堆上的記憶體通過一個雙向連結串列進行維護,連結串列的每個節點儲存這塊記憶體的大小是否可用等資訊。在堆上分配記憶體可能會導致以下問題: (1)分配的記憶體,沒有釋放,就會導致記憶體洩漏; (2)頻繁的分配小塊的記憶體有可能導致堆上都是剩餘的小塊的記憶體,這稱為記憶體碎片;

棧是一個動態增長和收縮的段,棧是自頂向下增長。棧由棧幀組成,每呼叫一個函式,系統會為每個當前呼叫的函式分配一個棧幀,棧幀從儲存了引數的實參,以及函式中使用的區域性變數,當函式返回時,該函式的棧幀就會彈出,函式中的區域性變數因此也就被銷燬了。

環境變數

在棧上面還有一小段空間,這段空間裡儲存的是環境變數和命令列引數,環境變數和命令列引數都是指向字串的陣列argv和environ。

malloc和free實現

動態記憶體的分配是通過維護一個雙向連結串列來實現,每個節點儲存該記憶體塊的大小的使用情況。malloc的分配有多種演算法,比如首次適配原則,最優適配原則等。我們這裡採用首次適配原則。實際上free函式,當堆頂有大塊的記憶體時,會通過sbrk函式降低堆頂的地址,我們這裡並不做處理。

malloc和free函式

/* my_malloc.h */
#ifndef _MY_MALLOC_H_
#define _MY_MALLOC_H_

#include <unistd.h>
#include <stdlib.h>
#include <stdbool.h>

//儲存每個記憶體塊的資訊
typedef struct _MEM_CONTROL_BLOCK_
{
    unsigned int uiBlockSize;      //當前塊的大小
    unsigned int uiPrevBlockSize;  //前一個記憶體塊的大小
    bool bIsAvalible;              //該記憶體塊是否已經被分配記憶體
} MemControlBlock;

#define INIT_BLOCK_SIZE        (0x21000)          //初始化堆的大小
#define MEM_CONTROL_BLOCK_SIZE (sizeof(MemControlBlock))

void* g_pMallocStartAddr;     //維護堆底地址
void* g_pMallocEndAddr;       //維護堆頂地址

//初始化堆段
void malloc_init()
{
    g_pMallocStartAddr = sbrk(INIT_BLOCK_SIZE);
    g_pMallocEndAddr = g_pMallocStartAddr + INIT_BLOCK_SIZE;

    //初始化時堆只有一個記憶體塊
    MemControlBlock* pFirstBlock;
    pFirstBlock = (MemControlBlock*)g_pMallocStartAddr;
    pFirstBlock->bIsAvalible = 1;
    pFirstBlock->uiBlockSize = INIT_BLOCK_SIZE;
    pFirstBlock->uiPrevBlockSize = 0;
}

void* my_malloc(unsigned int uiMallocSize)
{
    static bool bIsInit = false;
    if(!bIsInit)
    {
        malloc_init();
        bIsInit = true;
    }

    void* pCurAddr = g_pMallocStartAddr;
    MemControlBlock* pCurBlock = NULL;
    MemControlBlock* pLeaveBlock = NULL;
    void* pRetAddr = NULL;

    //判斷是否到了堆頂
    while (pCurAddr < g_pMallocEndAddr)
    {
        pCurBlock = (MemControlBlock*)pCurAddr;
        if (pCurBlock->bIsAvalible)
        {
            //判斷該塊可用的記憶體大小是否滿足分配的需求
            if (pCurBlock->uiBlockSize - MEM_CONTROL_BLOCK_SIZE >= uiMallocSize)
            {
                //該塊分配空間後剩餘的空間是否足夠分配一個控制塊,如果不能則把該塊全部分配了
                if ((pCurBlock->uiBlockSize - MEM_CONTROL_BLOCK_SIZE) <= (uiMallocSize + MEM_CONTROL_BLOCK_SIZE))
                {
                    pCurBlock->bIsAvalible = 0;
                    pRetAddr = pCurAddr;
                    break;
                }
                else
                {
                    //分配記憶體,並將剩餘的空間獨立成一個塊
                    pLeaveBlock = (MemControlBlock*)(pCurAddr + MEM_CONTROL_BLOCK_SIZE + uiMallocSize);
                    pLeaveBlock->bIsAvalible = 1;
                    pLeaveBlock->uiBlockSize = pCurBlock->uiBlockSize - MEM_CONTROL_BLOCK_SIZE - uiMallocSize;
                    pLeaveBlock->uiPrevBlockSize = MEM_CONTROL_BLOCK_SIZE + uiMallocSize;

                    pCurBlock->bIsAvalible = 0;
                    pCurBlock->uiBlockSize = MEM_CONTROL_BLOCK_SIZE + uiMallocSize;

                    pRetAddr = pCurAddr;
                    break;
                }
            }
            else
            {
                pCurAddr += pCurBlock->uiBlockSize;
                continue;
            }
        }
        else
        {
            pCurAddr += pCurBlock->uiBlockSize;
            continue;
        }
    }

    //已有的塊中找不到合適的塊,則通過sbrk函式增加堆頂地址
    if (!pRetAddr)
    {
        unsigned int uiAppendMemSize = uiMallocSize + MEM_CONTROL_BLOCK_SIZE;
        unsigned int uiPrevBlockSize = pCurBlock->uiBlockSize;
        if(*((int*)sbrk(uiAppendMemSize)) == -1)
        {
            return NULL;
        }
        g_pMallocEndAddr = g_pMallocEndAddr + uiAppendMemSize;
        pCurBlock = (MemControlBlock*)pCurAddr;
        pCurBlock->bIsAvalible = 0;
        pCurBlock->uiBlockSize = uiAppendMemSize;
        pCurBlock->uiPrevBlockSize = uiPrevBlockSize;

        pRetAddr = pCurAddr;
    }

    return pRetAddr + MEM_CONTROL_BLOCK_SIZE;
}

void my_free(void* pFreeAddr)
{
    if (pFreeAddr == NULL)
    {
        return;
    }

    MemControlBlock* pCurBlock = (MemControlBlock*)(pFreeAddr - MEM_CONTROL_BLOCK_SIZE);
    MemControlBlock* pPrevBlock = (MemControlBlock*)(pFreeAddr - MEM_CONTROL_BLOCK_SIZE - pCurBlock->uiPrevBlockSize);
    MemControlBlock* pNextBlock = (MemControlBlock*)(pFreeAddr - MEM_CONTROL_BLOCK_SIZE + pCurBlock->uiBlockSize);
    if (pCurBlock->bIsAvalible == 0)
    {
        pCurBlock->bIsAvalible = 1;

        //判斷前一個記憶體塊是否可用
        if (pCurBlock->uiPrevBlockSize != 0 && pPrevBlock->bIsAvalible)
        {
            pPrevBlock->uiBlockSize += pCurBlock->uiBlockSize;

            if((void*)pNextBlock < g_pMallocEndAddr)
            {
                pNextBlock->uiPrevBlockSize = pPrevBlock->uiBlockSize;
            }
        }
    }

    return;
}

#endif //_MY_MALLOC_H_
複製程式碼

測試程式

這個測試程式就是迴圈在堆上動態分配記憶體,然後釋放記憶體,可以選擇釋放起始塊的位置,也可以選擇間隔的塊數量。

/*test_malloc.c*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "my_malloc.h"

#define MAX_ALLOCS 1000

int main(int argc, char const *argv[])
{
    if(argc < 3 || strcmp(argv[1], "--help") == 0)
    {
        printf("Usage: %s num_allocs block_size [step [min [max]]]\n", argv[1]);
        return -1;
    }

    int iNumAllocs = atoi(argv[1]);
    int iBlockSize = atoi(argv[2]);
    if(iBlockSize > MAX_ALLOCS)
    {
        printf("Out range of the max mallocs.\n");
    }

    int iStep, iMin, iMax;
    iStep = (argc > 3) ? atoi(argv[3]) : 1;
    iMin = (argc > 4) ? atoi(argv[4]) : 1;
    iMax = (argc > 5) ? atoi(argv[5]) : iNumAllocs;

    printf("Initial program break: %10p\n", sbrk(0));

    void* pArr[iNumAllocs];
    memset(pArr, 0, sizeof(pArr));
    for(int i = 0; i < iNumAllocs; ++i)
    {
        pArr[i] = my_malloc(iBlockSize);
        if(pArr[i] == NULL)
        {
            printf("malloc failed.\n");
            return -1;
        }
        printf("After malloc, program break: %10p\n", sbrk(0));
    }

    printf("After alloc, program break: %10p\n", sbrk(0));

    for (int i = iMin; i <= iMax; i += iStep)
    {
        my_free(pArr[i - 1]);
    }

    printf("After free, program break: %10p\n", sbrk(0));

    return 0;
}
複製程式碼

測試結果

$:./a.out 10 100 2 
Initial program break:   0xa13000
After malloc, program break:   0xa34000
After malloc, program break:   0xa34000
After malloc, program break:   0xa34000
After malloc, program break:   0xa34000
After malloc, program break:   0xa34000
After malloc, program break:   0xa34000
After malloc, program break:   0xa34000
After malloc, program break:   0xa34000
After malloc, program break:   0xa34000
After malloc, program break:   0xa34000
After alloc, program break:   0xa34000
After free, program break:   0xa34000複製程式碼

相關文章