Linux 内存管理 | 物理内存管理:物理内存、内存碎片、伙伴系统、slab分配器
文章目錄
- 物理內存
- 物理內存分配
- 內存碎片
- 外部碎片
- 內部碎片
- 伙伴系統(buddy system)
- slab分配器
本文舉例為32位Linux
物理內存
在Linux中,內核將物理內存劃分為三個區域。
- DMA內存區域(ZONE_DMA):包含0M~16M之內的內存頁框,可以直接映射到內核空間中的直接映射區
- 普通內存區域(ZONE_NORMAL):包含16MB~896M以上的內存頁框,可以直接映射到內核空間中的直接映射區。與DMA加起來總共896M
- 高端內存區域(ZONE_HIGHMEM):包含896M以上的內存頁框,不可以進行直接映射,可以通過高端內存映射區中的永久內存映射區以及臨時內存映射區(固定內存映射區中的一部分)來對這塊物理內存進行訪問
內存分布如下圖
物理內存分配
內存碎片
在Linux中,通過分段和分頁的機制,將物理內存劃分為4k大小的內存頁(page),并且將頁作為物理內存分配與回收的基本單位。通過分頁機制我們可以靈活的對內存進行管理。
- 如果用戶申請了小塊內存,我們可以直接分配一頁給它,就可以避免因為頻繁的申請、釋放小塊內存而發起的系統調用帶來的消耗。
- 如果用戶申請了大塊內存,我們可以將多個頁框組合成一大塊內存后再進行分配,非常的靈活。
但是,這種直接的內存分配存在著大量的問題,非常容易導致內存碎片的出現,下面就分別介紹內部碎片和外部碎片的情況。
外部碎片
當我們需要分配大塊內存時,操作系統會將連續的頁框組合起來,形成大塊內存,來將其分配給用戶。但是,頻繁的申請和釋放內存頁,就會帶來內存外碎片的問題,如下圖。
假設我們這塊內存塊中有10個頁框,我們一開始先是分配了3個頁框給進程A,而后又分配了5個頁框給進程B。當進程A結束后,其釋放了申請的3個頁框,此時我們剩余空間就是內存塊起始位置的3個頁框,以及末尾位置的2個頁框。
假如此時我們運行了進程C,其需要5個頁框的內存,此時雖然這塊內存中還剩下5個頁框,但是由于我們頻繁的申請和釋放小塊空間導致內存碎片化,因此如果我們想申請5個頁框的空間,只能到其他的內存塊中申請。久而久之,就造成了大量的空間浪費,這也就是內存外碎片的問題。
內部碎片
一開始的時候也說了,由于頁是物理內存分配的基本單位,因此即使我們需求的內存很小,Linux也會至少給我們分配4k的內存頁。
如上圖,倘若我們需求的只有幾個字節,那該內存頁中又有大量的空間未被使用,造成了內存浪費的問題,而如果我們頻繁的進行小塊內存的申請,這種浪費現象就會愈發嚴重,這也就是內存內碎片的問題
為了解決上述的內存碎片問題,Linux中引入了伙伴系統以及slab分配器
要想解決內存外碎片的問題,無非就兩種方法
- 外碎片問題的本質就是空間不連續,所以可以將非連續的空閑頁框映射到連續的虛擬地址空間
- 記錄現存的空閑連續頁框塊的情況,盡量避免為了滿足小塊內存的請求而分割大的空閑塊。
Linux選擇了第二種方法來解決這個問題,即引入伙伴系統算法,來解決內存外碎片的問題。
伙伴系統(buddy system)
什么是伙伴系統算法呢?其實就是把相同大小的連續頁框塊用鏈表串起來,這也頁框之間看起來就像是手拉手的伙伴,這也就是其名字的由來。
伙伴系統將所有的空閑頁框分組為11塊鏈表,每個塊鏈表分別包含大小為1,2,4,8,16,32,64,128,256,512和1024個連續頁框的頁框塊,即2的0~10次方,最大可以申請1024個連續頁框,對應4MB(1024 * 4k)大小的連續內存。每個頁框塊的第一個頁框的物理地址是該塊大小的整數倍。
如下圖
因為任何正整數都可以由 2^n 的和組成,所以我們總能通過拆分與合并,來找到合適大小的內存塊分配出去,減少了外部碎片產生 。
倘若我們需要分配1MB的空間,即256個頁框的塊,我們就會去查找在256個頁框的鏈表中是否存在一個空閑塊,如果沒有,則繼續往下查找更大的鏈表,如查找512個頁框的鏈表。如果存在空閑塊,則將其拆分為兩個256個頁框的塊,一個用來進行分配,另一個則放入256個頁框的鏈表中。
釋放時也同理,它會將多個連續且空閑的頁框塊進行合并為一個更大的頁框塊,放入更大的鏈表中
slab分配器
雖然伙伴系統很好的解決了內存外碎片的問題,但是它還是以頁作為內存分配和釋放的單位,而我們在實際的應用中則是以字節為單位,例如我們要申請2個字節的空間,其還是會向我們分配一頁,也就是4096字節的內存,因此其還是會存在內存內碎片的問題。
為了解決這個問題,slab分配器就應運而生了。其以字節為基本單位,專門用于對小塊內存進行分配。slab分配器并未脫離伙伴系統,而是對伙伴系統的補充,它將伙伴系統分配的大內存進一步細化為小內存分配。
那么它的原理是什么呢?
對于內核對象,生命周期通常是這樣的:分配內存->初始化->釋放內存。而內核中如文件描述符、pcb等小對象又非常多,如果按照伙伴系統按頁分配和釋放內存,不僅存在大量的空間浪費,還會因為頻繁對小對象進行分配-初始化-釋放這些操作而導致性能的消耗。
所以為了解決這個問題,對于內核中這些需要重復使用的小型數據對象,slab通過一個緩存池來緩存這些常用的已初始化的對象。
當我們需要申請這些小對象時,就會直接從緩存池中的slab列表中分配一個出去。而當我們需要釋放時,我們不會將其返回給伙伴系統進行釋放,而是將其重新保存在緩存池的slab列表中。通過這種方法,不僅避免了內存內碎片的問題,還大大的提高了內存分配的性能。
下面就由大到小,來畫出底層的數據結構
kmem_cache是一個cache_chain的鏈表,描述了一個高速緩存,這個緩存可以看做是同類型對象的一種儲備,每個高速緩存包含了一個slab的列表,這通常是一段連續的內存塊,并包含3種類型的slabs鏈表:
- slabs_full(完全分配的slab)
- slabs_partial(部分分配的slab)
- slabs_empty(空slab,或者沒有對象被分配)。
slab是slab分配器的最小單位,在具體實現上一個slab由一個或者多個連續的物理頁組成(通常只有一頁)。單個slab可以在slab鏈表中進行移動,例如一個未滿的slab節點,其原本在slabs_partial鏈表中,如果它由于分配對象而變慢,就需要從原先的slabs_partial中刪除,插入到完全分配的鏈表slabs_full中
這里來具體舉一個例子,來說明上述之間結構的關系。
假設每一個對象就是一瓶牛奶,kmem_cache結構描述的slab緩存池其實就相當于我們家中的牛奶箱,箱中存儲著很多瓶牛奶。
當我們想喝牛奶(分配內存)時,其實就是從牛奶箱中取出一瓶牛奶。由于牛奶箱并非無限,因此總會有拿空的一天,當我們的箱子空了之后,自然就要去商店再買一箱回來。
而商店其實就相當于slabs_partial鏈表,商店中不僅有現貨,它的倉庫中還有大量的牛奶儲備,倉庫即相當于slabs_empty,當我們現貨不夠的時候就會去倉庫中取貨,放到商店中進行售賣。
那如果商店的庫存也用完了呢?那自然就需要去廠家進貨,這里的廠家其實就是我們的伙伴系統,所以結合這個例子,給出內核中slab分配對象的全過程
從上面可以看出,slab分配器的本質其實就是通過將內存按使用對象不同再劃分成不同大小的空間,即對內核對象的緩存操作
總結
以上是生活随笔為你收集整理的Linux 内存管理 | 物理内存管理:物理内存、内存碎片、伙伴系统、slab分配器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 高级数据结构与算法 | LFU缓存机制(
- 下一篇: 趣谈设计模式 | 职责链模式(Chain