談談對物理記憶體和虛擬記憶體的理解以及記憶體分配原理,一文徹底搞懂

文章每周持續更新,原創不易,「三連」讓更多人看到是對我最大的肯定。可以微信搜索公眾號「 後端技術學堂 」第一時間閱讀(一般比部落格早更新一到兩篇)

今天繼續來學習Linux記憶體管理,什麼?你更想學時間管理,我不配,抱個西瓜去微博學吧。

img

言歸正傳,上一篇文章 別再說你不懂Linux記憶體管理了,10張圖給你安排的明明白白! 分析了 Linux 記憶體管理機制,如果已經忘了的同學還可以回頭看下,並且也強烈建議先閱讀那一篇再來看這一篇。限於篇幅,上一篇沒有深入學習物理記憶體管理和虛擬記憶體分配,今天就來學習一下。

通過前面的學習我們知道,程式可沒這麼好騙,任你記憶體管理把虛擬地址空間玩出花來,到最後還是要給程式實實在在的物理記憶體,不然程式就要罷工了,所以物理記憶體這麼重要的資源一定要好好管理起來使用(物理記憶體,就是你實實在在的記憶體條),那麼內核是如何管理物理記憶體的呢?

物理記憶體管理

Linux 系統中通過分段和分頁機制,把物理記憶體劃分 4K 大小的記憶體頁 Page(也稱作頁框Page Frame),物理記憶體的分配和回收都是基於記憶體頁進行,把物理記憶體分頁管理的好處大大的。

假如系統請求小塊記憶體,可以預先分配一頁給它,避免了反覆的申請和釋放小塊記憶體帶來頻繁的系統開銷。

假如系統需要大塊記憶體,則可以用多頁記憶體拼湊,而不必要求大塊連續記憶體。你看不管記憶體大小都能收放自如,分頁機制多麼完美的解決方案!

But,理想很豐滿,現實很骨感。如果就直接這樣把記憶體分頁使用,不再加額外的管理還是存在一些問題,下面我們來看下,系統在多次分配和釋放物理頁的時候會遇到哪些問題。

物理頁管理面臨問題

物理記憶體頁分配會出現外部碎片和內部碎片問題,所謂的「內部」和「外部」是針對「頁框內外」而言,一個頁框內的記憶體碎片是內部碎片,多個頁框間的碎片是外部碎片。

外部碎片

當需要分配大塊記憶體的時候,要用好幾頁組合起來才夠,而系統分配物理記憶體頁的時候會盡量分配連續的記憶體頁面,頻繁的分配與回收物理頁導致大量的小塊記憶體夾雜在已分配頁面中間,形成外部碎片,舉個例子:

外部碎片

內部碎片

物理記憶體是按頁來分配的,這樣當實際只需要很小記憶體的時候,也會分配至少是 4K 大小的頁面,而內核中有很多需要以位元組為單位分配記憶體的場景,這樣本來只想要幾個位元組而已卻不得不分配一頁記憶體,除去用掉的位元組剩下的就形成了內部碎片。

內部碎片

頁面管理演算法

方法總比困難多,因為存在上面的這些問題,聰明的程式設計師靈機一動,引入了頁面管理演算法來解決上述的碎片問題。

Buddy(夥伴)分配演算法

Linux 內核引入了夥伴系統演算法(Buddy system),什麼意思呢?就是把相同大小的頁框塊用鏈表串起來,頁框塊就像手拉手的好夥伴,也是這個演算法名字的由來。

具體的,所有的空閑頁框分組為11個塊鏈表,每個塊鏈表分別包含大小為1,2,4,8,16,32,64,128,256,512和1024個連續頁框的頁框塊。最大可以申請1024個連續頁框,對應4MB大小的連續記憶體。

夥伴系統

因為任何正整數都可以由 2^n 的和組成,所以總能找到合適大小的記憶體塊分配出去,減少了外部碎片產生 。

分配實例

比如:我需要申請4個頁框,但是長度為4個連續頁框塊鏈表沒有空閑的頁框塊,夥伴系統會從連續8個頁框塊的鏈表獲取一個,並將其拆分為兩個連續4個頁框塊,取其中一個,另外一個放入連續4個頁框塊的空閑鏈表中。釋放的時候會檢查,釋放的這幾個頁框前後的頁框是否空閑,能否組成下一級長度的塊。

命令查看
[lemon]]# cat /proc/buddyinfo 
Node 0, zone      DMA      1      0      0      0      2      1      1      0      1      1      3 
Node 0, zone    DMA32   3198   4108   4940   4773   4030   2184    891    180     67     32    330 
Node 0, zone   Normal  42438  37404  16035   4386    610    121     22      3      0      0      1 

slab分配器

看到這裡你可能會想,有了夥伴系統這下總可以管理好物理記憶體了吧?不,還不夠,否則就沒有slab分配器什麼事了。

那什麼是slab分配器呢?

一般來說,內核對象的生命周期是這樣的:分配記憶體-初始化-釋放記憶體,內核中有大量的小對象,比如文件描述結構對象、任務描述結構對象,如果按照夥伴系統按頁分配和釋放記憶體,對小對象頻繁的執行「分配記憶體-初始化-釋放記憶體」會非常消耗性能。

夥伴系統分配出去的記憶體還是以頁框為單位,而對於內核的很多場景都是分配小片記憶體,遠用不到一頁記憶體大小的空間。slab分配器,通過將記憶體按使用對象不同再劃分成不同大小的空間,應用於內核對象的快取。

夥伴系統和slab不是二選一的關係,slab 記憶體分配器是對夥伴分配演算法的補充。

大白話說原理

對於每個內核中的相同類型的對象,如:task_struct、file_struct 等需要重複使用的小型內核數據對象,都會有個 slab 快取池,快取住大量常用的「已經初始化」的對象,每當要申請這種類型的對象時,就從快取池的slab 列表中分配一個出去;而當要釋放時,將其重新保存在該列表中,而不是直接返回給夥伴系統,從而避免內部碎片,同時也大大提高了記憶體分配性能。

主要優點
  • slab 記憶體管理基於內核小對象,不用每次都分配一頁記憶體,充分利用記憶體空間,避免內部碎片。
  • slab 對內核中頻繁創建和釋放的小對象做快取,重複利用一些相同的對象,減少記憶體分配次數。
數據結構

slab分配器

kmem_cache 是一個cache_chain 的鏈表組成節點,代表的是一個內核中的相同類型的「對象高速快取」,每個kmem_cache 通常是一段連續的記憶體塊,包含了三種類型的 slabs 鏈表:

  • slabs_full (完全分配的 slab 鏈表)
  • slabs_partial (部分分配的slab 鏈表)
  • slabs_empty ( 沒有被分配對象的slab 鏈表)

kmem_cache 中有個重要的結構體 kmem_list3 包含了以上三個數據結構的聲明。

kmem_list3 內核源碼

slab slab 分配器的最小單位,在實現上一個 slab 有一個或多個連續的物理頁組成(通常只有一頁)。單個slab可以在 slab 鏈表之間移動,例如如果一個「半滿 slabs_partial鏈表」被分配了對象後變滿了,就要從 slabs_partial 中刪除,同時插入到「全滿slabs_full鏈表」中去。內核slab對象的分配過程是這樣的:

  1. 如果 slabs_partial鏈表還有未分配的空間,分配對象,若分配之後變滿,移動 slabslabs_full 鏈表
  2. 如果 slabs_partial鏈表沒有未分配的空間,進入下一步
  3. 如果slabs_empty 鏈表還有未分配的空間,分配對象,同時移動slab進入 slabs_partial鏈表
  4. 如果slabs_empty為空,請求夥伴系統分頁,創建一個新的空閑slab, 按步驟 3 分配對象

slab分配圖解

命令查看

上面說的都是理論,比較抽象,動動手來康康系統中的 slab 吧!你可以通過 cat /proc/slabinfo 命令,實際查看系統中 slab 資訊。

slabinfo查詢

slabtop 實時顯示內核 slab 記憶體快取資訊。

slabtop查詢

slab高速快取的分類

slab高速快取分為兩大類,「通用高速快取」和「專用高速快取」。

通用高速快取

slab分配器中用 kmem_cache 來描述高速快取的結構,它本身也需要 slab 分配器對其進行高速快取。cache_cache 保存著對「高速快取描述符的高速快取」,是一種通用高速快取,保存在cache_chain 鏈表中的第一個元素。

另外,slab 分配器所提供的小塊連續記憶體的分配,也是通用高速快取實現的。通用高速快取所提供的對象具有幾何分布的大小,範圍為32到131072位元組。內核中提供了 kmalloc()kfree() 兩個介面分別進行記憶體的申請和釋放。

專用高速快取

內核為專用高速快取的申請和釋放提供了一套完整的介面,根據所傳入的參數為制定的對象分配slab快取。

專用高速快取的申請和釋放

kmem_cache_create() 用於對一個指定的對象創建高速快取。它從 cache_cache 普通高速快取中為新的專有快取分配一個高速快取描述符,並把這個描述符插入到高速快取描述符形成的 cache_chain 鏈表中。kmem_cache_destory() 用於撤消和從 cache_chain 鏈表上刪除高速快取。

slab的申請和釋放

slab 數據結構在內核中的定義,如下:

slab結構體內核程式碼

kmem_cache_alloc() 在其參數所指定的高速快取中分配一個slab,對應的 kmem_cache_free() 在其參數所指定的高速快取中釋放一個slab。

虛擬記憶體分配

前面討論的都是對物理記憶體的管理,Linux 通過虛擬記憶體管理,欺騙了用戶程式假裝每個程式都有 4G 的虛擬記憶體定址空間(如果這裡不懂我說啥,建議回頭看下 別再說你不懂Linux記憶體管理了,10張圖給你安排的明明白白!)。

所以我們來研究下虛擬記憶體的分配,這裡包括用戶空間虛擬記憶體和內核空間虛擬記憶體。

注意,分配的虛擬記憶體還沒有映射到物理記憶體,只有當訪問申請的虛擬記憶體時,才會發生缺頁異常,再通過上面介紹的夥伴系統和 slab 分配器申請物理記憶體。

用戶空間記憶體分配

malloc

malloc 用於申請用戶空間的虛擬記憶體,當申請小於 128KB 小記憶體的時,malloc使用 sbrk或brk 分配記憶體;當申請大於 128KB 的記憶體時,使用 mmap 函數申請記憶體;

存在問題

由於 brk/sbrk/mmap 屬於系統調用,如果每次申請記憶體都要產生系統調用開銷,cpu 在用戶態和內核態之間頻繁切換,非常影響性能。

而且,堆是從低地址往高地址增長,如果低地址的記憶體沒有被釋放,高地址的記憶體就不能被回收,容易產生記憶體碎片。

解決

因此,malloc採用的是記憶體池的實現方式,先申請一大塊記憶體,然後將記憶體分成不同大小的記憶體塊,然後用戶申請記憶體時,直接從記憶體池中選擇一塊相近的記憶體塊分配出去。
malloc原理

內核空間記憶體分配

在講內核空間記憶體分配之前,先來回顧一下內核地址空間。kmallocvmalloc 分別用於分配不同映射區的虛擬記憶體。

內核空間細分區域.

kmalloc

kmalloc() 分配的虛擬地址範圍在內核空間的「直接記憶體映射區」。

按位元組為單位虛擬記憶體,一般用於分配小塊記憶體,釋放記憶體對應於 kfree ,可以分配連續的物理記憶體。函數原型在 <linux/kmalloc.h> 中聲明,一般情況下在驅動程式中都是調用 kmalloc() 來給數據結構分配記憶體 。

還記得前面說的 slab 嗎?kmalloc 是基於slab 分配器的 ,同樣可以用cat /proc/slabinfo 命令,查看 kmalloc 相關 slab 對象資訊,下面的 kmalloc-8、kmalloc-16 等等就是基於slab分配的 kmalloc 高速快取。

slabinfo-kmalloc

vmalloc

vmalloc 分配的虛擬地址區間,位於 vmalloc_start vmalloc_end 之間的「動態記憶體映射區」。

一般用分配大塊記憶體,釋放記憶體對應於 vfree,分配的虛擬記憶體地址連續,物理地址上不一定連續。函數原型在 <linux/vmalloc.h> 中聲明。一般用在為活動的交換區分配數據結構,為某些 I/O 驅動程式分配緩衝區,或為內核模組分配空間。

下面的圖總結了上述兩種內核空間虛擬記憶體分配方式。
kmalloc_vmalloc圖解

總結一下

這是Linux 記憶體管理系列文章的下篇,強烈建議閱讀過程中有不清楚的同學,先去看看我之前寫的 別再說你不懂Linux記憶體管理了,10張圖給你安排的明明白白!,寫到這裡Linux 記憶體管理專題告一段落,我分享的這些知識很基礎,基礎到日常開發工作幾乎用不上,但我認為每個在Linux下開發人員都應該了解。

我知道有些面試官喜歡在面試的時候考察一下,或多或少反應候選人基礎素養,這兩篇文章的內容也足夠應付面試。還是那句話,Linxu 記憶體管理太複雜,不是一兩篇文章能講的清楚,但至少要有宏觀意識,不至於一問三不知,如果你想深入了解原理,強烈建議從書中並結合內核源碼學習,每天進步一點點,我們的目標是星辰大海。

本文創作過程我也畫了大量的示例圖解,可以作為知識索引,個人感覺看圖還是比看文字更清晰明了,你可以在我公眾號「後端技術學堂」後台回復「記憶體管理」獲取這些圖片的高清原圖。

老規矩,感謝各位的閱讀,文章的目的是分享對知識的理解,技術類文章我都會反覆求證以求最大程度保證準確性,若文中出現明顯紕漏也歡迎指出,我們一起在探討中學習。今天的技術分享就到這裡,我們下期再見。

Reference

《Linux內核設計與實現(原書第3版)》

linux內核slab機制分析 //www.jianshu.com/p/95d68389fbd1

Linux記憶體管理中的slab分配器 //edsionte.com/techblog/archives/4019

Linux slab 分配器剖析 //www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/index.html#table2

Linux內核記憶體管理演算法Buddy和Slab //zhuanlan.zhihu.com/p/36140017

Linux記憶體之Slab //fivezh.github.io/2017/06/25/Linux-slab-info/

malloc 的實現原理 記憶體池 mmap sbrk 鏈表 //zhuanlan.zhihu.com/p/57863097

malloc實現原理 //luodw.cc/2016/02/17/malloc/

glibc記憶體管理那些事兒 //www.jianshu.com/p/2fedeacfa797

原創不易,看到這裡動動手指,各位的「 轉發和點贊」是對我持續創作的最大支援,我們下篇文章再見。

可以微信搜索公眾號「 後端技術學堂 」回復「資料」「1024」有我給你準備的各種編程學習資料。文章每周持續更新,我們下期見!