理解 heap --- 實現一個簡單的 malloc

歡迎分享,微博 老和山小范 ,博客 wsfdl.com

理解 Heap

  high address   +---------------+
                 |               |
                 |     Stack     |
                 |               |
                 +---------------+
                 |       |       |
                 |       v       |
                 |               |
                 |               |
                 +---------------+
                 |      Mmap     |
                 +---------------+
                 |               |
                 |               |
                 |       ^       |
                 |       |       |
                 +---------------+
                 |               |
                 |     Heap      |
                 |               |
                 +---------------+
                 |     Data      | 
                 +---------------+
                 |     Code      |
  low address    +---------------+

上圖是 Linux 進程的地址空間,從低位到高位地址分別爲:

  • Code Segment: 程序的代碼,CPU 執行的指令部分,共享只讀。
  • Data Segment: 可細分爲初始化數據段和未初始化數據段,常用於存儲全局變量等。
  • Stack: 函數以及自動變量(未加 static 的自動變量又稱爲局部變量)。
  • Mmap: mmap 調用分配的地址空間。
  • Heap: 動態分配內存,如 malloc() 分配的內存。

本文主要講解 heap,從上圖可知,進程的堆是一段連續的空間,它分爲三個區域:

  • Mapped region: 該區域的空間已經在物理地址上分配,可以直接被程序使用。
  • Unmapped region: 該區域的空間未在物理地址上分配,需分配後纔可以使用。
  • Unusable region: 不可使用的地址空間,超出 rlimit 的空間都是不可使用的,不同的硬件和操作系統下,rlimit 可能各不相同。

其中 break 和 rlimit 是三個區域的分界線:

  • break: mapped region 和 unmapped region 的分界線,可調用 sbrk(0) 返回當前的 break。
  • rlimit: 堆能分配的最大地址空間,getrlimit(2) 可獲取 rlimit。
  • start of heap: 堆的起始地址,當堆上從未開闢空間時,sbrk(0) 返回的就是堆的起始地址,也可在 /proc/{proc_id}/mapping 獲取堆的起始地址。
                         Heap

   high address  +-------------------+
                 |                   |
                 |  Unusable Region  |
                 |                   |
                 +-------------------+  <--- rlimit
                 |                   |
                 |                   |
                 |  Unmapped Region  |
                 |                   |
                 |                   |
                 +-------------------+  <--- break
                 |                   |
                 |   Mapped Region   |
                 |                   |
   low address   +-------------------+  <--- start of heap

根據運行的需要,程序可以向堆動態的申請和釋放空間,當程序所需要的空間超過 mapped region 時,就需要向操作系統申請擴展堆的空間,即把 break 移動到更高的地址,linux 提供了兩個系統調用用於調整堆的大小。

除此以外,當程序申請較大(比如大於 128 KB)的空間時,linux 還提供了 mmap() 系統調用分配空間,嚴格來講,mmap 分配的地址空間並不屬於 heap,它介於 heap 和 stack 之間。

當程序不再需要某塊內存時,通常使用 free() 釋放它,釋放這塊內存時,並不會告知操作系統,它由運行庫(runtime library)管理起來,並標誌爲空閒狀態,我們用資源池表示這些處於空閒狀態的內存塊的集合。當程序再次申請空間時,運行庫首先搜索資源池,如果找到滿足要求的內存塊,則直接將這塊內存供給程序使用,如果資源池沒有滿足要求的內存塊,則調用 sbrk() 向操作系統分配內存。採用資源池的方式管理堆空間,避免每次分配和釋放內存時都需要告知操作系統,減少了系統調用的開銷。如何按需分配、高效的管理堆空間,這就是堆分配算法。它通常由運行庫實現,malloc 和 free 等組成了堆的分配算法的一種實現。

                                                system call

   +-----------+               +--------------+             +------------+
   |           |    malloc     |   Runtime    |    sbrk     |            |
   | Programme |   -------->   |   library    |  -------->  |     OS     |
   |           |    free       |              |     brk     |            |
   +-----------+               +--------------+             +------------+

實現一個簡單的 malloc

本節主要介紹如何實現一個簡單的 malloc,malloc 屬於運行庫的一個函數,它管理着已向操作系統申請好的堆空間。比如,它必須記錄 mapped region 裏有多少空間,哪些塊已經被分配,哪些塊處於空閒狀態,以及它們的地址和大小信息。管理這些塊的方法有多種,比如鏈表、位圖等。本節採用列表的方式管理這些內存塊:

                     Mapped Region

   high address  +-------------------+  <--- break
                 |                   |
                 |      Block N      |
                 |                   |
                 +-------------------+
                 |  Block N metadata |
                 +-------------------+
                 |                   |
                 |      ......       |
                 |                   |
                 +-------------------+  
                 |                   |
                 |      Block 1      |
                 |                   |
                 +-------------------+
                 |  Block 1 metadata |
   low address   +-------------------+  <--- start of heap

如上圖所示,每塊內存包含兩部分,block 和 block metadata,其中 block 用於存放程序的數據,block metadata 用於描述該 block 的基本信息,如大小、狀態等:

                     Block Metadata

                 +-------------------+ 
                 |       size        |
                 +-------------------+
                 |       free        |
                 +-------------------+ 
                 |       next        |
                 +-------------------+
                 |       prev        |
                 +-------------------+  

綜上,一個 malloc 的樣例爲:

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

#define BLOCK_SIZE sizeof(struct block_meta)

void *heap_start_addr = NULL;

struct block_meta {
    struct block_meta *prev;
    struct block_meta *next;
    int free;
    size_t size;
};

void get_heap_start_addr(){
    if(!heap_start_addr)
        heap_start_addr = sbrk(0);
}

void *get_last_block(){
    struct block_meta *blk_meta;

    blk_meta = heap_start_addr;
    while(blk_meta->next){
        blk_meta = blk_meta->next;
    }
    return blk_meta;
}

void *find_block(size_t size){
    struct block_meta *blk_meta;

    blk_meta = heap_start_addr;
    while(blk_meta){
        if(blk_meta->free)
            if(blk_meta->size >= size)
                break;
        blk_meta = blk_meta->next;
    }
    return blk_meta;
}

void *extend_heap(void *prev, size_t size){
    void *cur_brk;
    struct block_meta *cur_blk_meta, *prev_blk_meta;

    cur_brk = sbrk(size + BLOCK_SIZE);
    if(!cur_brk){
        return NULL;
    }
    else{
        cur_blk_meta = cur_brk;
        cur_blk_meta->free = 0;
        cur_blk_meta->size = size;
        cur_blk_meta->prev = prev;
        cur_blk_meta->next = NULL;

        if(prev){
            prev_blk_meta = prev;
            prev_blk_meta->next = cur_blk_meta;
        }

        return cur_brk + BLOCK_SIZE;
    }
}

void split_block(struct block_meta *blk_meta, size_t size){
    struct block_meta *free_blk_meta;

    if(size + BLOCK_SIZE < blk_meta->size){
        free_blk_meta = blk_meta + size + BLOCK_SIZE;
        free_blk_meta->prev = blk_meta;
        free_blk_meta->next = blk_meta->next;
        free_blk_meta->size = blk_meta->size - size - BLOCK_SIZE;
        free_blk_meta->free = 1;

        blk_meta->size = size;
        blk_meta->free = 0;
        blk_meta->next = free_blk_meta;
    }
    else{
        blk_meta->free = 0;
    }
}

void *malloc(size_t size) {
    void *split_blk, *cur_brk, *ptr;
    struct block_meta *last_blk;

    if(!size)
        return NULL;

    // align with 8 bytes.
    if(size & 0x7){
        size = ((size >> 3) + 1) << 3
    }

    get_heap_start_addr();
    cur_brk = sbrk(0);
    if(heap_start_addr == cur_brk){
        // First malloc, just extend the brk.
        ptr = extend_heap(NULL, size);
        if(ptr)
            return ptr;
        else
            return NULL;
    }
    else{
        split_blk = find_block(size);
        if(split_blk){
            // There is a free block, and we split it.
            split_block(split_blk, size);
            ptr = split_blk + BLOCK_SIZE;
            return ptr;
        }
        else{
            // No block is valid, extend the heap.
            last_blk = get_last_block();
            ptr = extend_heap(last_blk, size);
            if(ptr)
                return ptr;
            else
                return NULL;
        }
    }
}

類似的,free 的實現如下:

void free(void *ptr) {
    struct block_meta *cur_blk_meta, *prev_blk_meta, *next_blk_meta;

    if(!ptr)
        return ;

    cur_blk_meta = ptr - BLOCK_SIZE;
    cur_blk_meta->free = 1;

    next_blk_meta = cur_blk_meta->next;
    if(next_blk_meta){
        if(next_blk_meta->free){
            cur_blk_meta->next = next_blk_meta->next;
            cur_blk_meta->size += next_blk_meta->size + BLOCK_SIZE;
        }
    }

    prev_blk_meta = cur_blk_meta->prev;
    if(prev_blk_meta){
        if(prev_blk_meta->free){
            prev_blk_meta->next = cur_blk_meta->next;
            prev_blk_meta->size += cur_blk_meta->size + BLOCK_SIZE;
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章