內(nèi)存管理是Linux內(nèi)核最復雜的組件。內(nèi)存管理包括虛擬內(nèi)存機制和物理內(nèi)存管理。這篇說說物理內(nèi)存管理的一些要點。
物理內(nèi)存地址空間和虛擬內(nèi)存地址空間
說到虛擬內(nèi)存的時候我們知道虛擬內(nèi)存地址空間分為兩部分:內(nèi)核地址空間和用戶進程地址空間。這兩個地址空間都使用虛擬地址,也就是說程序使用的都是虛擬地址。從虛擬地址映射到實際物理地址時有所區(qū)別:
1. 內(nèi)核使用物理內(nèi)存時可以直接通過虛擬地址和內(nèi)核地址空間的起始值的偏移量來計算得到實際物理內(nèi)存的地址
2. 進程使用物理內(nèi)存必須通過頁表結(jié)構(gòu)進行轉(zhuǎn)換
對應內(nèi)核地址空間來說,
1. 如果內(nèi)核虛擬地址空間 > 物理內(nèi)存地址空間時, 那么物理內(nèi)存地址空間可以全部映射到內(nèi)核虛擬地址空間。在目前64位機器的情況下基本都是這種情況,由于硬件的限制,可用的物理內(nèi)存遠小于可用的內(nèi)核虛擬地址空間
2. 如果內(nèi)核虛擬地址空間 < 物理內(nèi)存地址空間時,物理內(nèi)存地址空間的一部分映射到內(nèi)核虛擬地址空間,剩余的物理內(nèi)存地址空間被稱為高端內(nèi)存(high memory),內(nèi)核會采取額外的映射機制來訪問這些高端內(nèi)存。這種情況在之前的32位機器上是常態(tài),在32位機器下,內(nèi)核地址空間和用戶進程地址空間的比例默認為1:3,也就是說4G的虛擬地址空間,內(nèi)核地址空間占1G,而可用的物理內(nèi)存可達到4G,內(nèi)核地址空間還必須預留一部分作為內(nèi)核運行使用,所以可用的內(nèi)核地址空間對物理內(nèi)存地址空間的映射為896MB,剩余的物理內(nèi)存地址空間都是高端內(nèi)存
對于64位機器來說,虛擬內(nèi)存地址空間遠大于可用的物理內(nèi)存地址空間,所以虛擬內(nèi)存地址空間劃分成內(nèi)核地址空間和用戶進程地址空間時就比32位機器的地址空間富裕很多,整個64位虛擬地址空間分為上下半部和中間禁用區(qū)三部分,可以看到虛擬地址的第0到46位都是任意設(shè)置的,而47位到63位對于內(nèi)核地址空間來說都是0,對于用戶進程地址空間都是1。這種虛擬地址稱為規(guī)范的地址,其他的都是不規(guī)范的地址。所以在64位機器來說內(nèi)核虛擬地址空間和用戶進程虛擬地址空間都是2的47次。
下圖是更細節(jié)的描述,內(nèi)核的虛擬內(nèi)存地址空間的起始部分是對物理內(nèi)次地址空間的一致性映射,然后是vmalloc區(qū)域,然后是給內(nèi)核使用的其他內(nèi)存區(qū)域
之前說虛擬內(nèi)存的時候已經(jīng)說了頁表的結(jié)構(gòu),再來看一下頁表和一個虛擬地址的映射關(guān)系
1. 對于64位機器,4KB的頁大小來說,Offset表示該地址在頁內(nèi)的偏移量,所以O(shè)ffset 的長度=12, 2的12次方正好是4KB
2. PGD,PUD,PMD,PTE各位的長度都是9,也就是每級頁表的頁表項都是512個
3. 這樣總共64位的虛擬地址長度使用了48位,已經(jīng)遠大于目前可用的物理內(nèi)存地址空間
而頁表結(jié)構(gòu)如下,每級頁表實際就是一個一個的數(shù)組,數(shù)組項的長度是64位的,上級頁表的頁表項存放著下級頁表的物理內(nèi)存地址,而最后的PTE頁表項存放的就是實際物理內(nèi)存頁的頁號。有了物理內(nèi)存頁的頁號我們就可以找到對應物理內(nèi)存頁。物理內(nèi)存地址空間就是按照物理頁長度劃分的一個數(shù)組。PTE頁表項既然存放的是物理內(nèi)存頁號,那么肯定不需要64位長度,剩下的位可以用作標志位來提供額外的功能,比如讀寫執(zhí)行等權(quán)限控制,是否是臟頁,是否被交換到了交換區(qū)等等。
物理內(nèi)存的頁組織成了唯一一個數(shù)組,就是我們上面說的mem_map結(jié)構(gòu),而虛擬內(nèi)存的頁卻采用了頁表結(jié)構(gòu),組織成了多級數(shù)組結(jié)構(gòu),這樣的目的主要就是減少頁表的長度。虛擬內(nèi)存當然也可以和物理內(nèi)存一樣只使用一個唯一的數(shù)組,但是虛擬內(nèi)存地址空間有2的64次方這么大,如果使用固定長度的數(shù)組來表示,那么有大量的數(shù)組項是空的,因為物理內(nèi)存沒這么大。每個進程要維護自己的頁表,這樣虛擬內(nèi)存也使用一個數(shù)組來表示,就造成了極大的內(nèi)存浪費,因為存儲頁表是需要物理內(nèi)存的。
采用了頁表這樣的多級數(shù)組結(jié)構(gòu)后,可以按需分配數(shù)組,使用多少物理內(nèi)存就分配多少虛擬地址空間的頁表項,這樣極大地壓縮了頁表長度,節(jié)省了物理內(nèi)存。
關(guān)于頁表,需要記住的是用戶進程的虛擬地址是和這個地址對應的頁表數(shù)組索引可以互相轉(zhuǎn)化計算。
物理內(nèi)存數(shù)據(jù)結(jié)構(gòu)
Linux要支持NUMA架構(gòu)和非NUMA架構(gòu)等多種硬件體系架構(gòu),在NUMA架構(gòu)下,每個CPU獨享一個本地內(nèi)存,在非NUMA架構(gòu)比如SMP架構(gòu)下,多個CPU共享一個物理內(nèi)存,Linux對物理內(nèi)存的管理必須要適用這多種架構(gòu)。
所以Linux對物理內(nèi)存的管理分為幾個層次
1. 最頂層是結(jié)點,在內(nèi)核中是pg_data_t結(jié)構(gòu)的實例
2. 每個結(jié)點又最多分為3個內(nèi)存域,比如上面說的不可以直接映射的高端內(nèi)存,可以直接映射的普通內(nèi)存,已經(jīng)給DMA用的內(nèi)存域
3. 每個內(nèi)存域都關(guān)聯(lián)了一個mem_map結(jié)構(gòu)的數(shù)組,數(shù)組項都是page實例。page結(jié)構(gòu)表示物理內(nèi)存頁幀,是最重要的物理內(nèi)存的表示結(jié)構(gòu)。相當于把整個內(nèi)存域的物理內(nèi)存地址空間按頁大小(4KB-2MB)劃分成一個數(shù)組,這個數(shù)組就是mem_map,數(shù)組項就是page。得到了page結(jié)構(gòu),就得到了物理內(nèi)存頁幀的位置,物理內(nèi)存頁幀的狀態(tài)。
page的結(jié)構(gòu)如下,在計算機底層知識拾遺(六)理解頁緩存page cache和地址空間address_space 這篇中說到地址空間address_space時也說到了page的結(jié)構(gòu)
1. flags表示該物理內(nèi)存頁幀的狀態(tài)
2. _count表示該頁被引用的計數(shù)器
3. private指針當在PagePrivate標志下,用來指向該頁緩存對應的底層buffer cache的buffer_head鏈表指針,當設(shè)置了PageSwapCache的時候,表示這個頁對應的頁交換區(qū)的位置信息
4. mapping指針當作為文件的頁緩存時,指向該文件inode對應的address_space地址空間。如果頁是匿名內(nèi)存,即沒有后備文件,那么指向anon_vma匿名區(qū)域?qū)ο?/p>
5. virtual 指向這個物理內(nèi)存地址對應的虛擬內(nèi)存地址
物理內(nèi)存分配
Linux內(nèi)核在機器啟動時被加載到物理內(nèi)存,內(nèi)核鏡像在物理內(nèi)存的存儲結(jié)構(gòu)如下所示
1. 第一個物理頁幀4KB的空間是預留給BIOS使用的
2. 接下來的640KB區(qū)域預留未使用,原因是緊鄰該區(qū)域的后面一塊空間給加載ROM使用,ROM是不可寫的,所以如果這640KB給內(nèi)核使用,必須保證內(nèi)核小于640KB
3. Linux內(nèi)核實際從1MB之后的連續(xù)內(nèi)存開始,依次是內(nèi)核代碼,內(nèi)核數(shù)據(jù)等
可以通過查看/proc/iomem來查看物理內(nèi)存實際的分配情況,可以看到,內(nèi)核代碼是用第1MB物理地址開始的
當內(nèi)核初始化完成之后,對物理內(nèi)存的管理由伙伴系統(tǒng)承擔,下面看看伙伴系統(tǒng)的基本原理。
上面說了每個結(jié)點的內(nèi)存分為3個域,域采用zone數(shù)據(jù)結(jié)構(gòu),包含一個free_area數(shù)組。數(shù)組的下標是階order。階是伙伴系統(tǒng)的一個重要術(shù)語,描述了內(nèi)存分配的數(shù)量單元。內(nèi)存塊的長度是2^order,階的范圍是0-MAX_ORDER。MAX_ORDER默認是11,也就是說一次分配可以請求的最大頁數(shù)是2^11 = 2048個頁
free_area結(jié)構(gòu)包含了一個free_list鏈表。free_list是用于連接空閑頁的頁鏈表,頁鏈表包含大小相同的連續(xù)內(nèi)存區(qū)。比如free_area[0]表示0階的空閑區(qū)域,它的頁鏈表包含的都是連續(xù)的空閑單頁。free_area[1]表示1階的空閑區(qū)域,它的頁鏈表包含的是連續(xù)的空閑雙頁
free_area數(shù)組和free_list鏈表的組成如下圖所示。 free_list里面大小相同的單元叫做伙伴,伙伴之間不需要連續(xù),采用鏈表相聯(lián)。
系統(tǒng)當前伙伴系統(tǒng)信息可以通過cat /proc/buddyinfo查看
伙伴系統(tǒng)的特點是簡單高效,只使用了雙鏈表結(jié)構(gòu),它可以高效的分配連續(xù)的內(nèi)存區(qū)域。對于用戶進程來說,物理內(nèi)存碎片的問題影響還不大,因為用戶進程利用頁表來訪問物理內(nèi)存,只要虛擬地址是連續(xù)的即可,vmalloc可以支持非連續(xù)的物理內(nèi)存。但是對于內(nèi)核來說,它直接映射了物理內(nèi)存地址空間,如果物理內(nèi)存碎片多,那么影響內(nèi)核的內(nèi)存分配。比如下面這個結(jié)構(gòu),雖然只有4頁被分配了,但是對伙伴系統(tǒng)來說,它能分配的最大連續(xù)頁只能是8頁,因為分配的頁數(shù)都是2的冪,雖然有連續(xù)的15個頁,但是伙伴系統(tǒng)最大只能分配8頁
為了解決內(nèi)存碎片的問題,內(nèi)核采用了反碎片的設(shè)計,試圖從最初開始盡可能防止碎片。
內(nèi)核將已分配的頁分為三種類型
1. 不可移動頁 Unmovable。 內(nèi)核中的已分配頁基本屬于這個類型,不能移動位置
2. 可回收頁 Reclaimable。 不能直接移動,但是可以刪除,其內(nèi)容可以重新再生,比如映射到文件的內(nèi)存,可以重新讀取文件內(nèi)容來加載頁
3. 可移動頁 Movable,用戶空間的以分配頁基本屬于這個類型,因為用戶空間利用頁表訪問物理內(nèi)存,可以通過復制頁到新的位置,然后更新頁表項來實現(xiàn)
反碎片的設(shè)計就是將相同移動性的頁分到一組。我們可以看到free_area里面的free_list鏈表實際是按照移動性MIGRATE_TYPES分組的。
需要注意的是伙伴系統(tǒng)是內(nèi)核用來分配大塊內(nèi)存的,它只能分配2的冪的頁。slab分配器是內(nèi)核用來分配細粒度的內(nèi)存分配器。它們于C語言庫的malloc這種可以按照字節(jié)大小來分配內(nèi)存的分配器不同?;锇橄到y(tǒng)分配器和slab分配器是最底層的內(nèi)核級內(nèi)存分配器,其他語言機的內(nèi)存分配器都是基于它們來工作的
伙伴系統(tǒng)分配器的幾個API:
alloc_pages(mask, orders)分配2^order頁的物理內(nèi)存并返回一個頁page結(jié)構(gòu),作為分配的內(nèi)存的起始頁
get_zeroed_page(mask)分配一頁并返回page實例,頁對應的內(nèi)存全部填充為0
__get_free_page(mask, order)返回分配內(nèi)存塊的虛擬地址,而不是頁page實例。
free_page(struct page*)用于將page實例對應的物理內(nèi)存頁返回給內(nèi)存管理子系統(tǒng),參數(shù)是page實例
__free_page(address)也是釋放內(nèi)存給內(nèi)存管理子系統(tǒng),區(qū)別是傳遞參數(shù)是虛擬地址
關(guān)于內(nèi)存分配系統(tǒng),需要注意的是 所有的內(nèi)存分配系統(tǒng)都是在初始化時預先獲得了一塊物理內(nèi)存,在這個物理內(nèi)存區(qū)域內(nèi)再進行分配,釋放內(nèi)存實際上就是把分配的內(nèi)存返還給內(nèi)存分配器。
1. 比如內(nèi)核的伙伴系統(tǒng)分配器,內(nèi)核在初始化時會語言確定各個內(nèi)存域的上下邊界,然后初始化各個內(nèi)存域的各種數(shù)據(jù)結(jié)構(gòu),比如page實例,然后把這個內(nèi)存域交給伙伴系統(tǒng)分配器。
2. 比如Java的內(nèi)存管理,也是在Java進程啟動的時候指定了各個內(nèi)存區(qū)域的大小,確定邊界,然后在Java進程啟動時就預先向內(nèi)核申請了Java進程管理的大部分內(nèi)存。Java的內(nèi)存分配和垃圾回收都是在這塊預先分配的內(nèi)存區(qū)域進行的。
vmalloc
內(nèi)核使用vmalloc在虛擬內(nèi)存地址空間分配連續(xù)的虛擬內(nèi)存地址,這些連續(xù)的虛擬內(nèi)存地址可以映射到不連續(xù)的物理內(nèi)存頁,從而利用內(nèi)存碎片。內(nèi)核的虛擬內(nèi)存地址空間有一塊專門的空間是給vmalloc使用的,每個vmalloc區(qū)域之后用空洞隔開,防止錯誤的虛擬地址引用。
每個用vmalloc函數(shù)創(chuàng)建的vmalloc區(qū)域?qū)ο蠖紝獌?nèi)核的一個vm_struct結(jié)構(gòu)體
1. addr表示這個vmalloc區(qū)域的起始虛擬地址,size表示這個vmalloc區(qū)域的長度,這兩個參數(shù)就確定了一個vmalloc區(qū)域的邊界
2. flags標志位表示了一組vmalloc區(qū)域的標志
3. pages是一個page數(shù)組,表示這個vmalloc區(qū)域?qū)膶嶋H物理內(nèi)存地址,可以是不連續(xù)的。nr_pages表示這個pages數(shù)組的長度
4. next指針指向下一個vmalloc區(qū)域?qū)ο螅械膙malloc區(qū)域?qū)ο蠼M成一個單鏈表結(jié)構(gòu)
vmalloc區(qū)域?qū)ο蠛臀锢韮?nèi)存的映射關(guān)系如下
slab分配器
伙伴系統(tǒng)分配器用來分配以頁為大小的大內(nèi)存空間,內(nèi)核同樣需要按字節(jié)大小等細粒度分配內(nèi)存空間的分配器,slab分配器就是這樣一個分配器
1. 分配細粒度的內(nèi)存空間,是內(nèi)核的kmallocAPI的底層實現(xiàn)。slab分配器對計算機高速緩存影響小,原因是不會每次都分配頁,減少頁表的操作。而伙伴系統(tǒng)分配器每次都要更新頁表,而更新頁表會影響高速緩存和TLB單元
2. 用作內(nèi)核的緩存,緩存內(nèi)核創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)的對象實例
slab分配器和伙伴系統(tǒng)的關(guān)系
聯(lián)系客服