1. 程式人生 > >作業系統原理:動態記憶體分配

作業系統原理:動態記憶體分配

動態記憶體分配背後的機制深刻的體現了電腦科學中的這句名言:

All problem in CS can be solved by another level of indirection. — Butler Lampson

使用者層

malloc的實現

malloc的底層呼叫sbrk和mmap

malloc是C語言標準庫函式,是在使用者層實現的。在Linux裡,malloc編譯好,是在run-time的動態庫so中,通過標準庫標頭檔案把api宣告給呼叫者。malloc

reallocfree是C語言的使用者層的標準庫函式,底層呼叫的是mmapsbrk函式。大致如下圖:
這裡寫圖片描述
malloc 底層呼叫的 sbrk 和 mmap. 申請記憶體小的時候用sbrk,增量擴充套件heap段;申請記憶體大的時候是調 mmap,程序重新開一塊VMA(後面會介紹)。這些都是可配置的,malloc 的預設觸發值是128k,可以看標準的原始碼。

/* malloc.c */
#ifndef DEFAULT_MMAP_THRESHOLD_MIN
#define DEFAULT_MMAP_THRESHOLD_MIN (128 * 1024)
#endif

#ifndef DEFAULT_MMAP_THRESHOLD
#define DEFAULT_MMAP_THRESHOLD DEFAULT_MMAP_THRESHOLD_MIN #endif

malloc 一般實現所用的資料結構 – 雙向連結串列

本質上,malloc就是管理一塊連續的可讀寫的程序虛擬記憶體。作為一個記憶體分配器,要做到: (1)最大化吞吐效率; (2)最大化記憶體利用率。同時不可避免的就會有所限制,即申請過的記憶體塊不能被修改和移動。管理申請的記憶體塊,在具體實現,上要考慮下面幾點:

  • 組織:如何記錄空閒塊;
  • 選擇:如何選擇一個合適的空閒塊來作為一個新分配的記憶體塊;
  • 分割:如何處理一個空閒快分配過記憶體後剩下的部分;
  • 合併:如何處理一個剛剛被釋放的塊;

下面介紹兩種malloc實現常用的資料結構:

隱含連結串列方式 – 隱含連結串列方式即在每一塊空閒或被分配的記憶體塊中使用一個字的空間來儲存此塊大小資訊和標記是否被佔用。根據記憶體塊大小資訊可以索引到下一個記憶體塊,這樣就形成了一個隱含連結串列,通過查表進行記憶體分配。優點是簡單,缺點就是慢,需要遍歷所有。meta-data如圖:
這裡寫圖片描述
顯示空閒連結串列 – 顯示空閒連結串列的方法,和隱含連結串列方式相似,唯一不同就是在空閒記憶體塊中增加兩個指標,指向前後的空閒記憶體塊。相比顯示連結串列,就是分配時只需要順序遍歷空閒塊。meta-data如圖:
這裡寫圖片描述
還有一些其他的方法不一一介紹。

核心層

brk系統呼叫的實現原理

Linux kernel通過 VMA : vm_area_struct來分塊管理程序地址空間。每個程序的程序控制塊 task_struct 裡都有一個 mm_struct指向了程序可使用的所有的VMA。sys_brk 作業系統呼叫本質就是調整這些VMA, 是mmap家族的一個特例,sbrk則是glibc針對brk的基礎上實現的。詳細可以看Linux kernel和glibc的的原始碼。

/* 大致的結構 */
struct mm_struct {
         struct vm_area_struct * mmap;  /* 指向虛擬區間(VMA)連結串列 */
         rb_root_t mm_rb;         /* 指向red_black樹 */
         struct vm_area_struct * mmap_cache;     /* 指向最近找到的虛擬區間*/
         pgd_t * pgd;             /* 指向程序的頁目錄 */
         atomic_t mm_users;                   /* 使用者空間中的有多少使用者*/
         atomic_t mm_count;               /* 對"struct mm_struct"有多少引用*/
         int map_count;                        /* 虛擬區間的個數*/
         struct rw_semaphore mmap_sem;
         spinlock_t page_table_lock;        /* 保護任務頁表和 mm->rss */
         struct list_head mmlist;            /*所有活動(active)mm的連結串列 */
         unsigned long start_code, end_code, start_data, end_data;
         unsigned long start_brk, brk, start_stack; /*堆疊相關*/
         unsigned long arg_start, arg_end, env_start, env_end;
         unsigned long rss, total_vm, locked_vm;
         unsigned long def_flags;
         unsigned long cpu_vm_mask;
         unsigned long swap_address;
         unsigned dumpable:1;
         /* Architecture-specific MM context */
         mm_context_t context;
};

下面是一個 mm_struct 管理程序記憶體的示意圖:
這裡寫圖片描述

Linux程序VMA的管理 - 紅黑樹

每個VMA包括VMA的起始和結束地址, 訪問許可權等. 其中的 vm_file 欄位表示了該區域對映的檔案(如果有的話)。有些不對映檔案的VMA是匿名的,比如的heap, stack都分別對應於一個單獨的匿名的VMA. 程序的VMA存放在一個List和一個rb_tree中, 該List根據VMA的起始地址排序。紅黑樹結構是為了加快查詢速度,快速查詢某一地址是否在程序的某一個VMA中. 通過命令讀取/proc/pid/maps檔案檢視程序的記憶體對映, 這個實現也是通過查存放VMA的List打印出來的。
這裡寫圖片描述

物理頁記憶體管理-夥伴演算法

作業系統是按頁來管理實體記憶體的。夥伴演算法每次只能分配2的冪次頁的空間,比如一次分配1頁,2頁,4頁,…, 1024頁等。夥伴的定義:

  1. 兩個塊大小相同;
  2. 兩個塊地址連續;
  3. 兩個塊必須是同一個大塊中分離出來的;

分配記憶體:

  1. 尋找大小合適的記憶體塊(大於等於所需大小並且最接近2的冪,比如需要27,實際分配32)。如果找到了,分配給應用程式,沒有執行下一步;
  2. 對半分離出高於所需大小的空閒記憶體塊;
  3. 如果分到最低限度,分配這個大小;
  4. 回到步驟1,尋找合適大小的塊;
  5. 重複該步驟直到一個合適的塊;

釋放記憶體:

  1. 釋放該記憶體塊;
  2. 尋找相鄰的塊,看其是否釋放了;
  3. 如果相鄰塊也釋放了,合併這兩個塊,重複上述步驟直到遇上未釋放的相鄰塊,或者達到最高上限(即所有記憶體都釋放了);

參考