第八章 內存管理
本章通過三部分內容描述內核給自己動態分配內存:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 頁框處理?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 內存區管理
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 非連續內存分配
頁框管理 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
Linux采用4KB頁框大小作為標準的內存分配單元,基于以下兩個原因:
1.由分頁單元引發的缺頁異常很容易得到解釋,或者時由于請求的頁存在但不允許進程對齊訪問,或者是由于請求的頁不存在。在第二種情況下,內存分配器必須找到一個4KB的空閑頁框,并將其分配
2.雖然4KB和4MB都是磁盤塊大小的倍數,但是在絕大數情況下,當主存和磁盤之間傳輸小塊數據時更有效
頁描述符
頁框的狀態信息保存在一個類型為page的頁描述符中
所有的頁描述符保存在mem_map數組中,每個頁描述符長度為32字節
virt_to_page(addr)宏產生線性地址addr對應的頁描述符地址
pfn_to_page(pfn)宏產生與頁框號p f n對應的頁描述符地址
頁描述符(page)兩個重要的字段:
_count:頁框的引用計數
? ? ? ? ? ? ? ?如果為-1,則該頁框空閑,可以被分配
? ? ? ? ? ? ? ?如果大于等于0,則說明該頁框被分配給了一個或者多個進程,或者用于存放一些內核數據結構
? ? ? ? ? ? ? ?page_count( )函數返回_count +1的值,也就是該頁使用者的數目?
flags:包含多達32個用來描述頁框狀態的標志
非一致內存訪問(None-Uniform Memory Access,NUMA)
非一致內存訪問:同一個cpu對不同內存單元的訪問可能需要的時間不一樣
節點(node):物理內存被分為好幾個節點
? ? ? ? ? ? ? ? ? ? ? ? ? ? 在一個單獨的節點中,任一給定的CPU訪問頁面的時間都是相同的
節點描述度:每個節點都有一個類型為pg_data_t的描述符
? ? ? ? ? ? ? ? ? ? ? ? 所有節點的描述符都存放在一個單鏈表中,它的第一個元素由pgdat_list變量指向
由于80x86體系結構支持一致內存訪問(Uniform Memory Acess,UMA),所以并不真正需要NUMA的支持。但是,Linux還是使用了節點,不過時單獨一個節點。
在80x86結構中,把物理內存分組在一個單獨的節點中可能沒有用處,但是,這種方式有助于內核代碼的處理更具有移植性。
管理區(zone):每個節點又被分為好幾個管理區
內存管理區
Linux內核必須處理80X86體系結構的兩種硬件限制:
? ? ? ? ? ?1.ISA總線的直接內存存取(DMA)處理器有一個嚴格的限制:它們只對RAM的前16MB尋址
? ? ? ? ? ?2.在具有大容量RAM的現代32位計算機中,CPU不能直接訪問所有的物理內存,因為線性地址太小了
為了應對這兩種限制,Linux 2.6把每個內存節點的內存劃分為3個管理區(zone):
ZONE_DMA:包含低于16MB的內存頁框
ZONE_NORMAL:包含高于16MB且低于896MB的內存頁框
ZONE_HIGHMEM:包含高于從896MB開始高于896MB的內存頁框
在64位體系結構上ZONE_HIFHMEM區總是空的
每個內存管理區都自己的描述符
每個頁描述符都有到內存節點和到節點管理區(包含相應頁框)的鏈接
page_zone( )函數接收一個頁描述符的地址作為它的參數,它讀取頁描述符的flags的最高位,然后通過查看zone_table數組來確定相應管理區描述符的地址
2015.10.19.21.40
保留的頁框池
內核為原子內存分配請求保留了一個頁框池,只有在內存不足時才使用
min_free_kbytes:保留內存的數量,以KB為單位
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 大小取決于ZONE_DMA和ZONE_NORMAL內存管理區的頁框數目
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?不能小于128也不能大于65536
計算公式:保留池大小^2=16*直接映射內存 ?保留池單位(KB) ? ? ? ? ? ? ? ? ? ? ? ? ?
分區頁框分配器 ? ? ? ? ? ? ? ? ? ?
被稱作分區頁框分配器(zoned page frame allocator)的內核子系統,處理對連續頁框組的內存分配請求?
請求和釋放頁框
6個請求分配的函數和宏:
alloc_pages(gfp_mask,order):用這個函數請求2^order個頁框
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?它返回第一個頁框的頁描述符的線性地址,或者分配失敗,返回NULL
alloc_page(gfp_mask):用于獲得一個單獨頁框的宏;它擴展為:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? alloc_pages(gfp_mask, 0)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?它返回所分配頁框的頁描述符的線性地址
_ _get_free_pages(gfp_mask,order):該函數類似于alloc_pages(),但它返回第一個頁框的線性地址
_ _get_free_page(gfp_mask):用于獲得一個單獨頁框的宏;它擴展為:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?_ _get_free_pages(gfp_mask)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?返回所獲取頁框的線性地址
get_zeroed_page(gfp_mask):函數用來獲取填滿0的頁框;它調用:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?alloc_pages(gfp_mask | _ _GFP_ZERO,0)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?然后返回所獲取頁框的線性地址
_ _get_dma_pages(gfp_mask,order):用這個宏獲取適用于DMA的頁框,它擴展為:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?_ _get_free_pages(gfp_mask | _ _GFP_DMA,order)
4個釋放頁框的函數和宏:
_ _free_pages(page,order):該函數先檢查page指向的頁描述符的指針,如果該頁框未被保留(PG_reserved標志為0),就把描述符的count字段減1。如果count值為0,就假定從與page對應的頁框開始的2^order個連續的頁框不再使用
free_pages(addr,order):該函數類似于_ _free_pages(page,order),但是它接受的參數為要釋放的第一個頁框的線性地址addr
_ _free_page(page):這個宏釋放page所指頁描述符對應的頁框;它擴展為:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?_ _free_pages(addr,0)
free_page(addr):該函數釋放線性地址為addr的頁框。它擴展為:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?free_pages(addr,0) ? ? ? ? ??
總結:對于分配函數或者宏,alloc_*的返回頁框的頁描述符的線性地址
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? *get*的返回頁框的線性地址
? ? ? ? ? ? 對于釋放函數或者宏,_ _*的以頁框的頁描述符的線性地址為參數
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?非_ _開頭的以頁框的線性地址為參數 ? ? ??
2015.10.19
高端內存頁框的內核映射
物理內存896MB以上的部分稱為高端內存,這些內存的頁框并不映射在內核線性地址的第四個GB,所以內核不能直接訪問它們。
32位系統內核訪問高端內存的方法:
1.高端內存頁框的分配只能通過alloc_pages()函數和它的快捷函數alloc_page()函數。這些函數返回第一個分配頁框的線性地址。
2.沒有線性地址的頁框不能被內核訪問。因此,內核線性地址空間的最高128M的一部分專門用于映射高端內存頁框。
內核采用三種不同的機制將頁框映射到高端內存:永久內核映射
臨時內核映射 非連續內存分配
建立永久內核映射可能堵塞當前進程;這發生在空閑頁表項不存在時,也就是在高端內存上沒有頁表項可以用作頁框的‘窗口’時。因此永久內核映射不能用于中斷處理程序和可延遲函數。
建立臨時內核映射絕不會要求堵塞當前進程;不過它的缺點是只有很少的臨時內核映射可以同時建立起來。
永久內核映射
永久內核映射允許建立高端頁框到內核地址空間的長期映射。它們使用主內核頁表中一個專門的頁表,其地址存放在pkmap_page_table變量中。
LAST_PKMAP:頁表中的表項數
PKMAP_BASE:該頁表相對應的線性首地址
pkmap_count數組:包含LAST_PKMAP個計數器,pkmap_page_table中的每一項都有
計數器為0,對應的頁表項沒有映射任何高端內存,并且可用
計數器為1,對應的頁表項映射了相應的高端內存,但是它不可用
計數器為n,相應的頁表項映射了一個高端頁框,這意味這正好有n-1個內核成分正在使用這個頁框為了記錄高端內存頁框與永久內核映射的線性地址之間的聯系,內核使用了page_address_htable散列表。
page_address_htable包含一個page_address_map數據結構,該結構還包含執行頁框描述符的指針和頁框的線性地址。
page_address()函數用于返回頁框的線性地址,參數是一個指向頁描述符的指針。
兩種情況:
1.如果頁框不在高端內存上,則線性地址肯定存在并且通過計算頁框下標,然后轉換成物理地址,最后通過物理地址得到相應的線性地址
2.如果頁框在高端內存中,該函數就到page_address_htable散列表中查找。如果在散列表中找到頁框,page_address()就返回它的線性地址。
臨時內核映射
臨時內核映射比永久內核映射的實現要簡單;此外,他們可以用在中斷處理程序和可延遲函數的內部,因為它們從不阻塞當前進程。
每個CPU都有它自己的包含13個窗口的集合,它們用enum
km_type數據結構表示。每個符號標識了窗口的線性地址。
內核必須確保同一窗口永不會被兩個不同的控制路徑同時使用。因此,km_type結構的每一個符號只能由一種內核成分使用,并以該成分命名。最后一個符號KM_TYPE_NR本身不表示一個線性地址,但由每個CPU用來產生不同的可用窗口
2015.10.20.23.28
伙伴系統
從本質上來說,避免內存外碎片的方法有兩種:
1.利用分頁單元把一組不連續的物理內存映射到連續的線性地址
2.開發一種適當的技術來記錄現存的空閑連續頁框塊的情況,以盡量避免為滿足對小塊的請求而分割大的空閑塊
Linux采用著名的伙伴系統(buddy
system)算法來解決外碎片問題。
把所有的空閑頁框分組為11個塊鏈表,每個塊鏈表分別包含大小為1,2,4,8,16,32,64,128,256,512和1024個連續的頁框。
每個塊的第一個頁框的物理地址是該塊大小的整數倍。
滿足以下三個條件的兩個塊稱為伙伴:
1.兩個塊具有相同的大小,記作b
2.它們的物理地址是連續的
3.第一塊的第一個頁框物理地址是2*b*2^12
數據結構
Linux?2.6為每個管理區使用不同的伙伴系統
1.管理區描述符的zone_mem_map和size字段:
zone_mem_map:指向管理區的第一個頁描述符的指針
size:管理區中頁框的個數
2.包含11個元素,元素類型為free_area的一個數組,每個元素對應一種塊的大小:
free_list字段:這個雙向鏈表集中了大小為2^k頁的空閑塊對應的頁描述符更精確地說,該鏈表包含每個空閑頁塊的起始頁框的描述符
頁描述符的lru:保存了指向鏈表中相鄰元素的指針(p?r e和next)
nr_free:它指定了大小為2^k的空閑塊的個數
頁描述符的private:一個2^k的空閑塊的起始頁框的頁描述符的private置為order?即為k
分配塊和釋放塊
分配塊:_?_rmqueue()函數,參數為管理區描述符的地址和order
釋放塊:_?_free_pages_bulk()函數,參數為被釋放塊中所包含的第一個頁描述符的地址(page)
管理區描述符的地址(zone),塊大小的對數(order)
每CPU頁框高速緩存
內核經常請求和釋放單個頁框,為了提高系統性能,每個內存管理區定義了一個“每CPU”頁框高速緩存。
實際上,這里為每個CPU提供了兩個高速緩存:一個熱高速緩存和冷高速緩存
如果內核或者用戶態進程在剛分配到頁框后就立即向頁框寫,那么從熱高速緩存中獲取頁框就對系統性能有利。
如果頁框將要被DMA操作填充,那么從冷高速緩存中獲取頁框比較有利。
實現每CPU頁框高速緩存的主要數據結構是存放在內存管理區描述符的pageset字段中的一個per_cpu_pageset數組數據結構。該數組包含為每個CPU提供的一個元素;這個元素依次有兩個per_cpu_pages描述符組成,一個熱高速緩存和一個冷高速緩存
通過每CPU頁框高速緩存分配頁框
函數buffered_rmqueue():該函數在制定的內存管理區中分配頁框。
參數:內存管理區描述符的地址,請求的內存大小的對數order,以及分配標志gfp_flags
釋放頁框到每CPU頁框高速緩存
free_hot_page()和free_cold_page():都是對free_hot_cold_page()函數的簡單封裝,接受的參數為將要釋放的頁框的描述符地址page和cold標志(指定是熱高速緩存還是冷高速緩存)
管理區分配器
管理區分配器是內核頁框分配器的前端。該成分必須分配一個包含足夠多空閑頁框的內存管理區,它滿足以下幾個目標:
1.它應該保護保留的頁框池
2.當內存不足且允許阻塞當前進程時,它應當出發頁框回收算法;一旦某些頁框被釋放,管理區分配器將再次嘗試分配
3.如果可能它應當保存小而珍貴的ZONE_DMA內存管理區
對一組連續頁框的每次請求實質上時通過alloc_pages()宏來處理的。接著,這個宏又再次調用__alloc_pages()函數,這個函數是管理區分配器的核心。它接受3個參數:
1.gfp_mask:在內存分配請求中指定的標志
2.order:將要分配的一組連續頁框數量的對數
3.zonelist:指向zonelist數據結構的指針,該數據結構按優先次序少廟了適用于內存分配的內存管理區
分區頁框分配器-->管理區頁框分配器
釋放一組頁框
在前面的“分區頁框分配器”一節描述的迎來釋放頁框的所有內核宏和函數都依賴于_
_free_pages函數。
它接受的參數為將要釋放的第一個頁框的頁描述符的地址(page)和將要釋放的一組頁框的數量的對數(order)。
內存區管理
伙伴系統算法采用頁框作為基本內存區,這適合于大塊內存的請求,但對小內存區的請求,比如幾十字節或幾百字節?顯然,如果為了存放很少的字節而給它分配一個整頁框,這顯然是一種浪費
slab分配器
高速緩存被劃分為多個slab,每個slab由一個或者多個連續的頁框組成,這些頁框包含已分配的對象,也包含空閑的對象。
高速緩存描述符
每個高速緩存都由kmap_cache_t類型的數據來描述,稱為高速緩存描述符。
slab描述符
高速緩存中的每個slab都有自己的類型為slab_t的描述符,稱為slab描述符
slab描述符可以放在兩個地方:
外部slab描述符:存放在slab外部,位于cache_sizes指向的一個不適合ISADMA的普通高速緩存中
內部slab描述符:存放在slab內部,位于分配給slab的第一個頁框的起始地址
普通和專用高速緩存
高速緩存被分為兩種類型:
普通高速緩存:只由slab分配器用于自己的目的
專用高速緩存:有內核的其余部分使用
普通高速緩存是:
1.第一個高速緩存叫做kmem_cache,包含由內核使用的其余高速緩存的高速緩存描述符。cache_cache變量包含第一個高速緩存的高速緩存描述符。
2.另外一些高速緩存包含用作普通用途的內存區。內存區大小的范圍一般包含13個集合分區的內存區。一個叫做malloc_sizes的表分別指向26個高速緩存描述符,與其相關的內存大小為32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536和131072字節。對于每中大小,都有兩個高速緩存:一個適用于ISADMA分配,另一個適用于常規分配
在系統初始化期間調用kmem_cache_init()和kmem_cache_sizes_init()來建立普通高速映射。
2015.10.21.23:36
slab分配器與分區分頁分配器的接口
當slab分配器創建新的slab時,它依靠分區分配器來獲得一組連續的空閑頁框。
它調用kmem_getpages()函數:
void* kmem_getpages(kmem_cache_t *cachep,int?flags)
參數:cachep高速緩存描述符
flags說明如何分配請求頁框
在相反的操作中,通過調用kmem_freepages()函數可以釋放給slab的頁框
給高速緩存分配slab
一個新創建的高速緩存沒有包含任何slab,因此沒有空閑對象。只有當滿足以下兩個條件都為真時,才給高速緩存分配slab:
1.已發出分配新對象的請求
2.高速緩存不包含任何空閑對象
從高速緩存中釋放slab
在下列兩種條件下才能撤銷slab:
1.slab高速緩存中有太多的空閑對象
2.被周期性調用的定時器函數確定是否有完全未使用的slab能被釋放
內存分配層次:分區頁框分配器--->管理器分配器------>slab分配器------>slab對象
對象描述符
每個對象都有類型為kmem_bufctl_t的描述符。對象描述符存放在一個數組中,位于相應的slab描述符之后。因此,對象描述符也有兩種可能的存放方式:
內部對象描述符:存放在slab的外面,位于slabp_cache字段指向的一個普通高速緩存中
外部對象描述符:存放在slab的內部,正好位于描述符所描述的對象之前
數組中的第一個對象描述符描述slab中的第一個對象,依次類推。對象描述符只不過是一個無符號整數,只有在對象空閑時才有效。它包含的是下一個空閑對象在slab中的下標,因此實現了slab空閑對象的一個簡單鏈表。空閑對象鏈表中的最后一個元素的對象描述符用常規值BUFCTL_END(0xffff)標記。
對齊內存中的對象
slab分配器所管理的對象可以在內存中進行對齊,頁就是說,存放它們的內存單元的起始物理地址是一個給定常量的倍數,通常是2。這個常量就叫對齊因子(alignment
factor)。
通常情況下,如果內存單元的物理地址是字對齊的,那么微機內存單元的存取會非常塊。
當創建一個新的slab高速緩存時,就可以讓它所包含的對象在第一級硬件高速緩存中對齊。為了做到這點,設置SLAB_HWCACHE_ALIGN高速緩存描述符標志。kmem_cache_create()函數按如下方式處理請求:
1.如果對象的大小大于高速緩存行(cacheline)的一半,就在RAM中根據L1_CACHE_BYTES的倍數(也就是行的開始)對齊對象
2.否則,對象的大小就是L1_CACHE_BYTES的因子取整。這可以保證一個小對象不會橫跨兩個高速緩存行。
顯然,slab分配器在這里所做的事情就是以內存空間換取訪問時間,即通過人為地增加對象的大小來獲得較好的高速緩存性能,因此也引起額外的內碎片。
slab著色
同一硬件高速緩存行可以映射RAM中很多不同的塊。而相同大小的對象傾向于存放在高速緩存內相同的偏移量處。在不同的slab內具有相同偏移量的對象最終可能映射在同一高速緩存中。高速緩存硬件可能因此而花費內存周期在同一高速緩存行與RAM內存單元之間來來往往傳送兩個對象,而其他的高速緩存行并未充分使用。slab分配器通過一種叫做slab著色(slab
coloring)的策略,盡量降低高速緩存的這種不愉快行為:把叫做顏色(color)的不同隨機數分配給slab。
在slab內放置對象有很多可能的方式。方式的選擇取決于對下列變量所作的決定:
num:可以在slab中存放的對象的個數(其值在高速緩存描述符的num字段中)
osize:對象的大小,包括對齊的字節
dsize:slab描述符的大小加上所有對象描述符的大小,就等于硬件高速緩存行大小的最小倍數。如果slab描述符和對象描述符都存放在外部,那么這個值就為0
free:在slab內未使用字節(沒有分配給任一對象的字節)的個數
一個slab中的總字節長度可以表示為如下表達式:
slab的長度=(num * osize) + dsize + free
free總是小于osize,因為否則的話,就有可能把另外的對象放在slab內。
slab分配器利用空閑未用的字節free來對slab著色。
可用顏色的個數為free/aln(aln對齊因子)。因此,第一個顏色為0,最后一個顏色為
(free/aln– 1)。
如果用顏色col對一個slab著色,那么第一個對象的偏移量(相對于slab的起始位置)就等于
col* aln +? dsize字節。著色本質上導致把slab上的一些空閑區域從末尾移到開始。如下圖:
只有當free足夠大時,著色才起作用。
空閑slab對象的本地高速緩存
slab分配器的每個高速緩存包含一個稱作slab本地高速緩存的數據結構(每個CPU都有一個),該結構由一個指向被時方對象的小指針數組組成。slab對象的大多數分配和釋放只影響本地數組,只有在本地數組上溢或者下溢才更新slab數據結構。
高速緩存描述符的array字段(struct?array_cache*[],即指針數組)是一組指向array_cache數據結構的指針,系統中的每個CPU對應于一個。array_cache結構的字段如下:
本地高速緩存描述符并不包含本地高速緩存本身的地址;事實上,它正好位于描述符之后。當然本地高速緩存描述符存放的是指向已釋放對象的指針,而不是對象本身。
內存池
內存池和保留頁框池的區別:
保留的頁框池:用于滿足中斷處理程序或者內部臨界區發出的原子內存分配請求
內存池:是動態內存的儲備,只能被特定的內核成分(即池的擁有者使用)使用
內存池描述符為mempool_t。
非連續內存管理區
把內存區映射到一組連續的頁框是最好的選擇,這樣會充分利用高速緩存并獲得較低的平均訪問時間。不過,如果對內存區的請求不是很頻繁,那么通過連續的線性地址來訪問非連續的頁框這樣一種分配模式就會很有意義。這種模式的主要優點是避免了外碎片,而缺點是打亂了內核頁表。
Linux非連續內存的應用:
1.為活動的交換區分配數據結構
2.為模塊分配空間,或者給某些I/O驅動程序分配緩沖區
3.非連續內存分配還提供了一種使用高端內存的方法
非連續內存區的線性地址
第四GB線性地址的使用情況:
1.內存區的開始部分包含的是對896MB
RAM進行映射的線性地址;直接映射的物理內存末尾所對應的線性地址保存在high_memory變量中
2.內存區的結尾部分包含的是對固定映射的線性地址
3.從PKMAP_BASE開始,我們查找用于高端內存頁框的永久映射的線性地址
4.其余線性地址可以用于非連續內存區。在物理內存映射的莫為與第一個內存之間插入一個大小為8MB(宏為VMALLOC_OFFSET)的安全區,目的是為了“捕獲”對內存的越界訪問。
如下圖:從PAGE_OFFSET開始的線性地址空間
非連續內存區的描述符
每個非連續內存區都對應這一個類型vm_struct的描述符,下圖列出了它的字段:
2015.10.23