memory-cnblog

trashwin發表於2024-04-18

linux虛擬記憶體系統

程序的虛擬記憶體
使用者區分段:程式碼段、資料段、堆、共享庫、棧
核心區:存放程序資訊,PID\程式計數器、開啟檔案列表、task和mm(描述虛擬記憶體)結構等

Linux載入程序時(exec系列系統呼叫)會為該地址空間每個段分配VMA,VMA資料結構(vm_area_struct)會描述該段的虛擬空間的開始地址,結束地址,同時會描述該VMA背後對映的檔名、對映空間所在檔案的偏移量和大小(file指標指向它的backend硬碟檔案)。但是Linux核心不給該空間分配實體記憶體,所以此時的頁表項是空的。程序分配的虛擬記憶體只有在實際寫入資料時,才會變為實體記憶體,否則不佔用空間,因此雖然程序地址空間位4G,但記憶體可以同時容納多個程序。在記憶體不夠時,會發生記憶體交換,將不常用的程序的記憶體交換到快速磁碟上,從而騰出空間給其他程序使用。

在翻譯虛擬記憶體時,如果發生缺頁,則系統要判斷:

  • 地址是否合法?處理程式會將地址和mm_struct的mmap成員指標指向的每個區域vm_area_struct的start和end對比,從而判斷是否合法,不合法就會報錯segment fault
  • 記憶體訪問是否合法?讀寫或者執行許可權判斷

共享物件不要求虛擬地址相同,但實體地址相同,這是任何程序的讀寫對其他程序都是可見的,也會反映到磁碟上。
私有物件採用寫時複製

從記憶體角度看系統呼叫

  • fork,建立子程序,包括mm_struct(描述虛擬記憶體當前狀態)、區域結構和頁表,分配pid,並把兩個程序的頁面都標記為只讀,每個段標記為寫時複製。
  • execve,刪除使用者區,對映私有區域(程式碼,資料,堆疊),對映共享區域(如標準c庫),設定pc。

顯式分配器

  • malloc,分配至少為引數大小的記憶體塊(包含邊界對齊),返回指向該塊的指標。malloc可以使用mmap和sbrk(使用核心的brk指標擴充套件或者收縮堆)等函式來分配記憶體
  • free,釋放malloc分配的記憶體塊,引數是malloc返回的指標

記憶體分配器實現(來自csapp)

  • 隱式空閒連結串列:見csapp 9.9.12,即使用頭部和腳部(邊界標記)來記錄塊大小和是否空閒(腳部方便合併),中間用來儲存有效載荷,但這樣空間浪費,且分配時間是塊總數的線性時間。有一種邊界標記的最佳化方法,把上一個塊的已分配位存放在當前塊中多出來的低位(以書中為例,有3位用於標記,但實際只用了1位),這樣上一個塊的腳部就可以存放資料。但是空閒塊仍然需要腳部,因為合併時要知道上一個塊的大小。

  • 顯式空閒連結串列:將空閒塊組織為顯式資料結構--連結串列,由於空閒塊不需要存資料,所以指標可以放在空閒塊主體裡

  • 分離空閒連結串列:維護多個空閒連結串列,其中每個連結串列中的塊有大致相等的大小。分配器維護著一個空閒連結串列陣列,每個大小類一個空閒連結串列,按照大小的升序排列。當分配器需要一個塊時,它就搜尋相應的空閒連結串列。如果不能找到合適的塊與之匹配,它就搜尋下一個連結串列,以此類推。

    • 簡單分離儲存:每個大小類的空閒連結串列包含大小相等的塊,為了分配一個給定大小的塊,我們檢査相應的空閒連結串列。如果連結串列非空,我們簡單地分配其中第一塊的全部。空閒塊是不會分割以滿足分配請求的。如果連結串列為空,分配器就向作業系統請求一個固定大小的額外記憶體片(通常是頁大小的整數倍), 將這個片分成大小相等的塊,並將這些塊連結起來形成新的空閒連結串列。要釋放一個塊,分配器只要簡單將這個塊插人到相應的空閒連結串列的前部。缺點是會帶來內部碎片,即分配的塊比請求的塊大,但是分配器不會將它們分割以滿足其他請求。
    • 分離適配:每個連結串列包含潛在的大小不同的塊,為了分配一個塊,必須確定請求的大小類,並且對適當的空閒連結串列做首次適配,査找一個合適的塊。如果找到了一個,那麼就(可選地)分割它,並將剩餘的部分插入到適當的空閒連結串列中。如果找不到合適的塊,那麼就搜尋下一個更大的大小類的空閒連結串列。如此重複,直到找到一個合適的塊。如果空閒連結串列中沒有合適的塊,那麼就向作業系統請求額外的堆記憶體,從這個新的堆記憶體中分配出一個塊,將剩餘部分放置在適當的大小類中。要釋放一個塊,執行合併,並將結果放置到相應的空閒連結串列中。既減少了搜尋時間,也提高了記憶體效率。書上說GNU malloc採用這種方法?
    • 夥伴系統:是分離適配的一種特例,其中每個大小類都是2的冪,請求塊大小向上舍人到最接近的2的冪。初始時只有一個大塊,分配時找到第一個大小足夠的塊,如果正好相等,就分配;否則遞迴二分這個塊,直到大小相等,而其餘半塊(夥伴)加入到相應的空閒連結串列。釋放塊時,與空閒的夥伴合併,直到遇到已分配的夥伴。優點是合併和分配快,缺點是可能導致內部碎片,但對於特定的分配需求,比如分配空間大小為2的冪,是很好的選擇。
  • glibc實現為代表的ptmalloc,隱藏頭風格,

下面詳細介紹malloc方法

ptmalloc

分配器處在使用者程式和核心之間,它響應使用者的分配請求,向作業系統申請記憶體,然後將其返回給使用者程式,為了保持高效的分配,分配器一般都會預先分配一塊大於使用者請求的記憶體,並透過某種演算法管理這塊記憶體,來滿足使用者的記憶體分配要求,使用者釋放掉的記憶體也並不是立即就返回給作業系統,相反,分配器會管理這些被釋放掉的空閒空間,以應對使用者以後的記憶體分配要求。因此,分配器不但要管理已分配的記憶體塊,還需要管理空閒的記憶體塊,分配器會首先在空閒空間中尋找一塊合適的記憶體給使用者,在空閒空間中找不到的情況下才分配一塊新的記憶體。

ptmalloc 中使用一個 chunk 來表示記憶體塊,無論是分配的還是空閒的。
chunk 的結構如下:

struct malloc_chunk {
  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;
  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

size除了包括大小外,還包括了chuck是否使用等位資訊。

  • P: PREV_INUSE. P=0則前一個chunk空閒,P=1則前一個chunk被分配。
  • M: IS_MMAPPED. M=1為mmap對映區域分配,M=0為heap區域分配
  • A: NON_MAIN_ARENA. A=0為主分配區分配,A=1為非主分配區分配。

links只有在空閒時有用,被分配時也就不需要了,可以用於存放資料。
fd_nextsize 指向下一個比當前 chunk 大小大的第一個空閒 chunk , bk_nextszie 指向前一個比當前 chunk 大小小的第一個空閒 chunk。被分配時存放資料。加快在large bin中查詢最近匹配的空閒chunk

  • 使用中的chunk

  • 空閒chunk

將相似大小的chunk用雙向連結串列連結起來,這樣一個連結串列被稱為一個bin,基於chunk的大小,有下列幾種可用bins:

  • fast
  • unsorted
  • small
  • large

使用bins陣列儲存large bin之外的所有bins,共有128個。使用fastbinsY陣列儲存fast bins,共有10個。

fast bins

對應的bins記錄在陣列fastbinsY中,共有10個大小不同的bins,每個chunk的大小為 16-64B。最後釋放的 chunk 被連結到連結串列的頭部,而申請 chunk 是從連結串列頭部開始查詢。fast bins 中的 chunk 不會被合併,當使用者釋放一塊不大於max_fast(預設值64B)的chunk的時候,會預設會被放到fast bins上。,當需要給使用者分配的 chunk 小於或等於 max_fast 時,malloc 首先會到fast bins上尋找是否有合適的chunk。

unsorted bin

是第1個bin,大小無嚴格限制。當使用者釋放的記憶體大於max_fast或者fast bins合併後的chunk都會首先進入unsorted bin上。在進行 malloc 操作的時候,如果在 fast bins 中沒有找到合適的 chunk,則 ptmalloc 會先在 unsorted bin 中查詢合適的空閒 chunk,如果沒有合適的bin,ptmalloc會將unsorted bin上的chunk放入bins上,然後才查詢 bins。
具體遍歷過程如下:

如果只有一個chunk並且大於待分配的chunk,則進行切割,並且剩餘的chunk繼續放入 unsorted bins
如果有大小和待分配chunk相等的,則返回,並從unsorted bins刪除;
如果某一chunk大小 屬於small bins的範圍,則放入small bins的頭部;
如果某一chunk大小 屬於large bins的範圍,則找到合適的位置放入。

small bins

small bin是第2-64個bin,大小為16-512B同一個small bin中的chunk具有相同的大小。兩個相鄰的small bin中的chunk大小相差8bytes。small bins 中的 chunk 按照最近使用順序進行排列(FIFO),最後釋放的 chunk 被連結到連結串列的頭部,而申請 chunk 是從連結串列尾部開始查詢。可以透過 chunk 的大小計算出它所在的 small bin 的索引,即最優適配,這樣就可以快速定位到 small bin。
當空閒的 chunk 被連結到 bin 中的時候,ptmalloc 會把表示該 chunk 是否處於使用中的標誌 P 設為 0(這個標誌實際上處在下一個 chunk 中),同時 ptmalloc 還會檢查它前後的 chunk 是否也是空閒的,如果是的話,ptmalloc 會首先把它們合併為一個大的 chunk,然後將合併後的 chunk 放到 unstored bin 中。

large bins

大小大於等於512位元組的chunk被稱為large chunk,儲存在bins的第65-128個bin中,large bins中的每一個bin分別包含了一個給定範圍內的chunk(大小類),其中的chunk按大小遞減排序,大小相同則按照最近使用時間排列。使用最優適配,多餘部分分割後加入到unsorted bin中。合併方法和small bins相同。

特殊情況

  • top chunk。top chunk相當於分配區的頂部空閒記憶體,當bins上都不能滿足記憶體分配要求的時候,就會來top chunk上分配。
    • 當top chunk大小比使用者所請求大小還大的時候,top chunk會分為兩個部分:User chunk(使用者請求大小)和Remainder chunk(剩餘大小)。其中Remainder chunk成為新的top chunk。
    • 當top chunk大小小於使用者所請求的大小時,top chunk就透過sbrk(main arena)或mmap(thread arena)系統呼叫來擴容。
  • mmaped chunk。當分配的記憶體非常大(大於分配閥值,預設128K)的時候,需要被mmap對映,則會放到mmaped chunk上,當釋放mmaped chunk上的記憶體的時候會直接交還給作業系統。
  • last remainder chunk。當需要分配一個small chunk,但在small bins中找不到合適的chunk,如果last remainder chunk的大小大於所需要的small chunk大小,last remainder chunk被分裂成兩個chunk,其中一個chunk返回給使用者,另一個chunk變成新的last remainder chunk。

ptmalloc參考連結