內存管理是Linux內核中很是重要的部分,今天和你們一塊兒學習一下。html
當咱們要學習一個新知識點時,比較好的過程是先理解出現這個技術點的 背景緣由,同期其餘解決方案,新技術點解決了什麼問題以及它存在哪些不足和改進之處,這樣整個學習過程是 閉環 的,我的以爲這是個很好的學習思路。linux
凡事都是相通的,計算機學科的一些問題在現實生活中均可以找到原型,因此我以爲計算機科學家大部分都是善於觀察生活並總結概括的。人類社會就是一臺複雜的機器,其中充滿了機制和規則,因此有時候跳進代碼海洋不如先回到生活之中,尋找原型再探究代碼,可能理解會更深入。git
linux內存管理卷帙浩繁,本文只能層層遞進地帶你領略冰山輪廓,經過本文你將瞭解到如下內容:github
老子的著名觀點是無爲而治,簡單說就是不過多幹預而充分依靠自覺就能夠有條不紊地運做,理想是美好的,現實是殘酷的。面試
在linux系統中若是以一種原始簡單的方式管理內存是存在一些問題的,咱們來看幾個場景。算法
假如如今有ABC三個進程運行在linux的內存空間,設定os給進程A分配的地址空間是0-20M 進程B地址空間30-80M,進程C地址空間90-120M,如圖:緩存
在某些時候程序空間的訪問可能出現問題,好比進程A訪問了屬於進程B的空間,進程B訪問了屬於進程C的空間,甚至修改了空間的值,這樣就會形成混亂和錯誤,因此實際中是不容許這種狀況發生的。數據結構
機器的內存是有限資源,而進程數量是沒法肯定的,若是在某些時候已經啓動的進程佔據了全部內存空間,此時就沒法啓動新進程了,由於沒有新內存可分配了,可是咱們觀察到已經啓動的進程有時候是在睡大覺,也就是給了內存也不用,這樣效率確實是有點低,因此咱們須要一個管理員把不用的內存倒騰出來,另外連續內存實在是很珍貴,不少時候咱們無法有效及時地分配連續內存,所以虛擬化和離散化可能會有效提升內存的使用率。工具
因爲程序運行時的位置時不肯定的,咱們在定位問題、調試代碼、編譯執行時都會存在不少問題,咱們但願每一個進程有一致且完整的地址空間,一樣的起始位置放置了堆、棧以及代碼段等,從而簡化編譯和執行過程當中的 linker 連接器、loader 加載器的使用。post
爲了解決上述的一些問題,linux系統引入了虛擬空間的概念,虛擬化的出現和硬件有密不可分的聯繫,能夠說是軟硬件組合的結果,虛擬地址空間就是在程序和物理空間所增長的中間層,這也是內存管理的重點。
磁盤 disk 做爲一種大容量的存儲也做爲"內存"的一部分參與程序的運行,內存管理系統會將不經常使用非活躍內存進行頁面換出,能夠認爲內存是磁盤的緩存,內存中保留了活躍的數據,從而間接擴展了有限的物理內存空間,這部分空間稱爲虛擬內存是相對於物理內存而言的。
本文並不深刻地將分段管理內存和分頁管理內存,由於將這些細節的優秀文章不少,感興趣的使用搜索引擎一鍵即達。
段頁機制也不是一蹴而就的,經歷了單純物理分段、單純分頁、單純邏輯分段等階段,最終演進出來了分段和分頁結合的內存管理方式,段頁結合得到了分段和分頁的優點也避免了單一模式的弊端,是一種比較好的管理模式。
本文對於段頁管理機制只想通俗地說明一些概念,段頁管理機制是分段式管理和分頁式管理的組合,段式管理是邏輯上的管理方式,分頁管理是偏物理上的管理方式。
計算機裏面的一些技術和實現均可以在現實生活中找到縮影,所謂藝術和科技源自生活大概就是這個意思吧。
舉個栗子:
在進行居民戶籍管理時都會有區縣市的概念,可是實際上並無這種實體,都是邏輯上的,增長了這些行政單位以後可讓地址管理更加直接。
對於咱們居民來講惟一的實體就是本身的房子住所,這是物理上的單位,是真實存在的,這也是最基本的單位。
對比linux段頁時管理來講,段是邏輯上的單位至關於區縣市的概念,頁是物理上的單位至關於小區/房屋的概念,這樣就方便不少。
多級頁表也很好理解,總的物理內存假若有4GB,頁大小爲4KB,那麼就總共有2^20個頁,數量仍是很是大的,這樣編號來創建索引尋址比較不方便,因此引入多級頁表,來減小存儲便於管理。
段頁機制加持下的邏輯地址和物理地址的映射關係簡圖,也就是虛擬地址到物理地址的對應關係:
內存管理單元( MMU Memory Management Unit )是硬件層組件,主要提供將虛擬地址映射爲物理地址。
MMU 的工做流程:CPU 生成邏輯地址交給分段單元,分段單元進行處理將邏輯地址轉換爲線性地址,再線性地址交給分頁單元,分頁單元根據頁表映射轉換內存物理地址,其中可能出現缺頁中斷。
缺頁中斷( Page Fault )是隻當軟件試圖訪問一個虛擬地址時,通過段頁轉換爲物理地址以後,此時發現該頁並無在內存中,這時 cpu 就會報出中斷,再進行相關虛擬內存的調入工做或者分配工做,若是出現異常也可能直接中斷。
前面說的段頁管理機制算是虛擬空間的部分,然而linux內存管理的另一個重要部分就是物理內存的管理了,也就是如何分配和回收物理內存,這就涉及到一些內存分配算法和分配器。
分配器和分配算法就像公司財務,內存就像公司資金,如何把資金合理使用是財務的本職工做,如何把物理內存合理使用是分配器的份內之事。
若是咱們不知道內存碎片是什麼,試想一下咱們常說的碎片化的時間,也就是那些雖然空閒可是沒有被利用的時間,其實內存也是如此。
不管是時間仍是內存被碎片化以後都沒法被有效利用,所以合理管理減小碎片對咱們來講是相當重要的,這也是物理內存分配算法和分配器的研究重點。
按照碎片的位置和產生緣由,內存碎片分爲外部碎片和內部碎片,咱們看下這兩種碎片的直觀展現:
從圖中能夠知道,外部碎片是進程與進程間未分配的內存空間,外部碎片的出現和進程頻繁的分配和釋放內存有直接關係,這個很好理解,模擬一下分配不一樣空間的進程不一樣時間釋放就能夠看到外部碎片的產生了。
內部碎片主要由於分配器粒度問題以及一些地址限制致使實際分配的內存大於所需內存,這樣在進程內部就會出現內存空洞。
雖然虛擬地址讓進程使用的內存在物理內存上是離散的,可是不少時候進程須要必定量連續物理內存,若是大量碎片存在,就會形成沒法啓動進程的問題,如圖Process7須要一塊連續的物理內存卻沒法被分配:
linux將物理內存按照頁來劃分,內存頁的大小在不一樣的軟硬件中可能不同,linux內核設置爲4KB,有的內核可能更大也可能更小,當時不一樣的大小在實際中都是有考量的,就像麪包同樣有大有小,並非整齊劃一的。
在內核中爲了創建對物理內存頁page的使用狀況的監控,會有struct page這樣的數據結構來記錄頁的位置地址/使用狀況等,至關於內核對內存頁管理的一本帳目。
linux系統有內核態和用戶態之分,內核態申請內存就馬上知足而且認爲這個請求必定是合理的。然而用戶態申請內存的請求,老是儘可能延後分配物理內存,因此用戶態進程是先得到一個虛擬內存區,在運行時經過缺頁異常得到一塊真正的物理內存,咱們執行 malloc 時獲取的只是虛擬內存而已,並非真實的物理內存,也是這個緣由形成的。
第一次聽到這個算法名稱就很好奇爲何叫夥伴系統?讓咱們來一塊兒揭祕。
夥伴系統算法是解決外部碎片的有力工具,簡單來講它針對頻繁請求和釋放不一樣大小的一組連續頁框的場景,創建一套管理機制來高效的分配和回收資源,下降外部碎片。
第一種思路:把已經存在的外部碎片經過新的技術把這些非連續的空閒內存映射到連續的線性空間,其實至關於沒有去下降外部碎片的產生而是治理型方案,可是這種方案在真實須要連續物理內存時是無效的。
第二種思路:把這些小的空閒的不連續內存記錄在案,若是有新的分配需求就從中搜索合適的將空閒內存分配出去,這樣就避免了在新的區域進行分配內存,有種變廢爲寶的感受,其實這樣場景也很熟悉當你想吃一包餅乾時,你媽媽確定會說先把以前剩下一半沒吃完的吃掉,不要先開新的了。
基於一些其餘方面的考量,linux內核選擇了第二種思路來解決外部碎片。
在夥伴系統中把大小相同且物理地址連續的兩塊內存區域稱爲夥伴,連續地址的要求實際上是比較苛刻了,可是這也是算法的關鍵,由於這樣的兩塊內存區域能夠合併成一塊更大的區域。
夥伴系統將不一樣大小的連續物理頁框進行管理,在申請時從最接近的頁框大小進行分配,剩餘的進行新的拆解,並將有夥伴關係的內存會進行合併成爲大的頁框。
夥伴系統維護了 n=0~10 共 11 個塊鏈表,每一個塊鏈表分別包含了大小爲 2^n 個連續的物理頁。當 n=10 時即 1024 個 4KB 頁對應 4MB 大小的連續物理內存塊,這裏的 n稱爲 order,在夥伴系統中 order爲0~10,也就是最小的是 4KB,最大的內存塊是4MB,這些相同大小的物理塊組成雙向鏈表進行管理,如圖展現了 order=0 和 order=2 的兩個雙向鏈表的狀況:
申請內存過程:假設請求一個頁框塊,夥伴系統算法先在 order=0 的鏈表中檢查是否有空閒塊可分配。若是沒有則查找下一個更大的塊,在 order=1 的鏈表中找一個空閒塊,鏈表中存在就把2個頁框拆分,1個頁框分配出去1 個頁框加入到 order=0的鏈表中。若是 order=1 的鏈表中仍未找到空閒塊,就繼續向更大的order搜索,若是找到進行拆分處理,若是最終至 order=10 的鏈表也沒有空閒塊,則算法報錯。
合併內存過程:合併內存的過程是夥伴算法中夥伴塊的體現,算法把兩個塊具備相同大小 A且它們物理地址連續的內存合併爲一個大小爲 2A 的單獨塊。夥伴算法是自底向上迭代合併的,其實這個過程和 leveldb 中 sst 的合併過程很類似,區別在於夥伴算法要求內存塊是連續的,這個過程也體現了夥伴系統對於大塊內存的友好。
從夥伴系統的介紹能夠知道其分配的最小單位是 4KB 的頁框,這對於一些頻繁申請的小到幾十字節的內存來講仍是十分浪費的,因此咱們須要更細粒度的分配器,這就是slab分配器。
slab分配器並非和夥伴系統分立的,而是創建在夥伴系統之上,能夠看做是夥伴系統的二級分銷商,更加靠近用戶側,可是slab分配器由於更靠近使用方,所以在結構實現上比夥伴系統更加複雜,本文只能簡單歸納。
我的感受slab分配器的亮點包括:最小粒度爲對象和內存惰性歸還。
Linux 所使用的 slab 分配器的基礎是 Jeff Bonwick 爲 SunOS 操做系統首次引入的一種算法。Jeff 的分配器是圍繞對象緩存進行的。在內核中,會爲有限的對象集(例如文件描述符和其餘常見結構)分配大量內存。Jeff 發現對內核中普通對象進行初始化所需的時間超過了對其進行分配和釋放所需的時間。所以他的結論是不該該將內存釋放回一個全局的內存池,而是將內存保持爲針對特定初始化的狀態。
from 《linux slab 分配器剖析》
slab採用對象做爲最小單位的理論基礎就在於初始化一個結構的時間可能超過了分配和釋放的時間。
slab分配器能夠看做是一種內存預分配機制,就像超市會把經常使用的物品放到你們更容易找到的位置,事先把這些對象準備好申請時就能夠馬上分配出去了。
對象是從 slab 中進行分配和釋放的,每一個kmem_cache的slab列表是存在狀態遷移的,可是被回收的部分slab並不會馬上歸還給夥伴系統,而且在分配時會優先分配最近被釋放的對象,目的是利用cpu緩存的局部性原理,能夠看出來slab分配器的細節作的很足,可是爲了實現這一套複雜的邏輯,就要維護多個隊列會比夥伴系統更復雜。
slab的內容相比buddy更復雜,本文再也不展開。
linux內存管理的東西確實很是多,本文也只有5k字而且沒有源碼,因此只能是淺談。
從工程角度來講,本文也只是拋磚引玉的做用,因此這篇文章並非深刻的探討內存管理,對此我也表示歉意。
讓個人好朋友sy審稿了一下,他說看着寫了不少但又好像啥也沒寫,其實我跟他的感覺同樣,可能最近寫文章沒感受,和上篇快手面試題差很少,寫完以後有點迷茫。
因此最後仍是那句話,本文只是淺談,深刻理解仍是須要去看內核書籍,沒有捷徑。