程序员内功修炼系列:10 张图解谈 Linux 物理内存和虚拟内存
來源 |?后端技術學堂
責編 | Carol
封圖 | CSDN 付費下載于視覺中國
我們都知道,程序可沒這么好騙,任你內存管理把虛擬地址空間玩出花來,到最后還是要給程序實實在在的物理內存,不然程序就要罷工了。
所以物理內存這么重要的資源一定要好好管理起來使用(物理內存,就是你實實在在的內存條),那么內核是如何管理物理內存的呢?
物理內存管理
在Linux系統中通過分段和分頁機制,把物理內存劃分 4K 大小的內存頁 Page(也稱作頁框Page Frame),物理內存的分配和回收都是基于內存頁進行,把物理內存分頁管理的好處大大的。
假如系統請求小塊內存,可以預先分配一頁給它,避免了反復的申請和釋放小塊內存帶來頻繁的系統開銷。
假如系統需要大塊內存,則可以用多頁內存拼湊,而不必要求大塊連續內存。你看不管內存大小都能收放自如,分頁機制多么完美的解決方案!
But,理想很豐滿,現實很骨感。如果就直接這樣把內存分頁使用,不再加額外的管理還是存在一些問題,下面我們來看下,系統在多次分配和釋放物理頁的時候會遇到哪些問題。
1、物理頁管理面臨問題
物理內存頁分配會出現外部碎片和內部碎片問題,所謂的「內部」和「外部」是針對「頁框內外」而言,一個頁框內的內存碎片是內部碎片,多個頁框間的碎片是外部碎片。
2、外部碎片
當需要分配大塊內存的時候,要用好幾頁組合起來才夠,而系統分配物理內存頁的時候會盡量分配連續的內存頁面,頻繁的分配與回收物理頁導致大量的小塊內存夾雜在已分配頁面中間,形成外部碎片,舉個例子:
外部碎片
3、內部碎片
物理內存是按頁來分配的,這樣當實際只需要很小內存的時候,也會分配至少是 4K 大小的頁面,而內核中有很多需要以字節為單位分配內存的場景,這樣本來只想要幾個字節而已卻不得不分配一頁內存,除去用掉的字節剩下的就形成了內部碎片。
內部碎片
4、頁面管理算法
方法總比困難多,因為存在上面的這些問題,聰明的程序員靈機一動,引入了頁面管理算法來解決上述的碎片問題。
5、Buddy(伙伴)分配算法
Linux 內核引入了伙伴系統算法(Buddy system),什么意思呢?就是把相同大小的頁框塊用鏈表串起來,頁框塊就像手拉手的好伙伴,也是這個算法名字的由來。
具體的,所有的空閑頁框分組為11個塊鏈表,每個塊鏈表分別包含大小為1,2,4,8,16,32,64,128,256,512和1024個連續頁框的頁框塊。最大可以申請1024個連續頁框,對應4MB大小的連續內存。
伙伴系統
因為任何正整數都可以由 2^n 的和組成,所以總能找到合適大小的內存塊分配出去,減少了外部碎片產生 。
6、分配實例
比如:我需要申請4個頁框,但是長度為4個連續頁框塊鏈表沒有空閑的頁框塊,伙伴系統會從連續8個頁框塊的鏈表獲取一個,并將其拆分為兩個連續4個頁框塊,取其中一個,另外一個放入連續4個頁框塊的空閑鏈表中。釋放的時候會檢查,釋放的這幾個頁框前后的頁框是否空閑,能否組成下一級長度的塊。
7、命令查看
[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 br8、slab分配器
看到這里你可能會想,有了伙伴系統這下總可以管理好物理內存了吧?不,還不夠,否則就沒有slab分配器什么事了。
9、那什么是slab分配器呢?
一般來說,內核對象的生命周期是這樣的:分配內存-初始化-釋放內存,內核中有大量的小對象,比如文件描述結構對象、任務描述結構對象,如果按照伙伴系統按頁分配和釋放內存,對小對象頻繁的執行「分配內存-初始化-釋放內存」會非常消耗性能。
伙伴系統分配出去的內存還是以頁框為單位,而對于內核的很多場景都是分配小片內存,遠用不到一頁內存大小的空間。slab分配器,「通過將內存按使用對象不同再劃分成不同大小的空間」,應用于內核對象的緩存。
伙伴系統和slab不是二選一的關系,slab 內存分配器是對伙伴分配算法的補充。
10、大白話說原理
對于每個內核中的相同類型的對象,如:task_struct、file_struct 等需要重復使用的小型內核數據對象,都會有個 slab 緩存池,緩存住大量常用的「已經初始化」的對象,每當要申請這種類型的對象時,就從緩存池的slab 列表中分配一個出去;而當要釋放時,將其重新保存在該列表中,而不是直接返回給伙伴系統,從而避免內部碎片,同時也大大提高了內存分配性能。
11、主要優點
slab 內存管理基于內核小對象,不用每次都分配一頁內存,充分利用內存空間,避免內部碎片。
slab 對內核中頻繁創建和釋放的小對象做緩存,重復利用一些相同的對象,減少內存分配次數。
12、數據結構
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對象的分配過程是這樣的:
如果slabs_partial鏈表還有未分配的空間,分配對象,若分配之后變滿,移動?slab?到slabs_full?鏈表
如果slabs_partial鏈表沒有未分配的空間,進入下一步
如果slabs_empty?鏈表還有未分配的空間,分配對象,同時移動slab進入slabs_partial鏈表
如果slabs_empty為空,請求伙伴系統分頁,創建一個新的空閑slab, 按步驟 3 分配對象
slab分配圖解
13、命令查看
上面說的都是理論,比較抽象,動動手來康康系統中的 slab 吧!你可以通過 cat /proc/slabinfo 命令,實際查看系統中slab 信息。
slabinfo查詢
slabtop ?實時顯示內核 slab 內存緩存信息。
slabtop查詢
14、slab高速緩存的分類
slab高速緩存分為兩大類,「通用高速緩存」和「專用高速緩存」。
15、通用高速緩存
slab分配器中用 kmem_cache 來描述高速緩存的結構,它本身也需要 slab 分配器對其進行高速緩存。cache_cache 保存著對「高速緩存描述符的高速緩存」,是一種通用高速緩存,保存在cache_chain 鏈表中的第一個元素。
另外,slab 分配器所提供的小塊連續內存的分配,也是通用高速緩存實現的。通用高速緩存所提供的對象具有幾何分布的大小,范圍為32到131072字節。內核中提供了 kmalloc()和 kfree()?兩個接口分別進行內存的申請和釋放。
16、專用高速緩存
內核為專用高速緩存的申請和釋放提供了一套完整的接口,根據所傳入的參數為指定的對象分配slab緩存。
17、專用高速緩存的申請和釋放
kmem_cache_create() 用于對一個指定的對象創建高速緩存。它從 cache_cache 普通高速緩存中為新的專有緩存分配一個高速緩存描述符,并把這個描述符插入到高速緩存描述符形成的 cache_chain 鏈表中。kmem_cache_destory() 用于撤消和從 cache_chain 鏈表上刪除高速緩存。
18、slab的申請和釋放
slab 數據結構在內核中的定義,如下:
20、slab結構體內核代碼
kmem_cache_alloc() 在其參數所指定的高速緩存中分配一個slab,對應的 kmem_cache_free() 在其參數所指定的高速緩存中釋放一個slab。
虛擬內存分配
前面討論的都是對物理內存的管理,Linux 通過虛擬內存管理,欺騙了用戶程序假裝每個程序都有 4G 的虛擬內存尋址空間。
所以我們來研究下虛擬內存的分配,這里包括用戶空間虛擬內存和內核空間虛擬內存。
注意,分配的虛擬內存還沒有映射到物理內存,只有當訪問申請的虛擬內存時,才會發生缺頁異常,再通過上面介紹的伙伴系統和 slab 分配器申請物理內存。
1、用戶空間內存分配
malloc
malloc 用于申請用戶空間的虛擬內存,當申請小于 128KB 小內存的時,malloc使用 ?sbrk或brk 分配內存;當申請大于 128KB 的內存時,使用 mmap 函數申請內存;
存在問題
由于 brk/sbrk/mmap 屬于系統調用,如果每次申請內存都要產生系統調用開銷,cpu在用戶態和內核態之間頻繁切換,非常影響性能。
而且,堆是從低地址往高地址增長,如果低地址的內存沒有被釋放,高地址的內存就不能被回收,容易產生內存碎片。
解決
因此,malloc采用的是內存池的實現方式,先申請一大塊內存,然后將內存分成不同大小的內存塊,然后用戶申請內存時,直接從內存池中選擇一塊相近的內存塊分配出去。
2、內核空間內存分配
在講內核空間內存分配之前,先來回顧一下內核地址空間。kmalloc 和 vmalloc 分別用于分配不同映射區的虛擬內存,看這張上次畫的圖:
內核空間細分區域
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 驅動程序分配緩沖區,或為內核模塊分配空間。
下面的圖總結了上述兩種內核空間虛擬內存分配方式。
總結一下
作者分享的這些知識很基礎,基礎到日常開發工作幾乎用不上,但我認為每個在Linux下開發人員都應該了解。
我知道有些面試官喜歡在面試的時候考察一下,或多或少反應候選人基礎素養,這兩篇文章的內容也足夠應付面試。還是那句話,Linxu 內存管理太復雜,不是一兩篇文章能講的清楚,但至少要有宏觀意識,不至于一問三不知,如果你想深入了解原理,強烈建議從書中并結合內核源碼學習,每天進步一點點,我們的目標是星辰大海。
本文創作過程我也畫了大量的示例圖解,可以作為知識索引,個人感覺看圖還是比看文字更清晰明了。
如果您對本文有什么想說的,也歡迎在評論區告訴我們,一起探討學習!
推薦閱讀
一文帶你認識keepalived,再帶你通關LVS+Keepalived!
那個分分鐘處理 10 億節點圖計算的 Plato,現在怎么樣了?
“谷歌殺手”發明者,科學天才 Wolfram
數據庫激蕩 40 年,深入解析 PostgreSQL、NewSQL 演進歷程
超詳細!一文告訴你 SparkStreaming 如何整合 Kafka !附代碼可實踐
5分鐘!就能學會以太坊 JSON API 基礎知識!
真香,朕在看了!
總結
以上是生活随笔為你收集整理的程序员内功修炼系列:10 张图解谈 Linux 物理内存和虚拟内存的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 果断拿下4000万美元D轮融资,Ranc
- 下一篇: linux 其他常用命令