標籤:des style blog http color 使用 strong 資料
記憶體管理頁
核心把物理頁作為記憶體管理的基本單位;記憶體管理單元(MMU,管理記憶體並把虛擬位址轉換為物理地址)通常以頁為單位進行處理。MMU以頁大小為單位來管理系統中的頁表。從虛擬記憶體的角度看,頁就是最小單位。
32位系統:頁大小4KB
64位系統:頁大小8KB
在支援4KB頁大小並有1GB實體記憶體的機器上,實體記憶體會被劃分為262144個頁。核心用 struct page 結構表示系統中的每個物理頁。
struct page {
page_flags_t flags; /* 表示頁的狀態,每一位表示一種狀態*/
atomic_t _count; /* 存放頁的引用計數,0代表沒有被引用 */
atomic_t _mapcount;
unsigned long private;
strcut address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual; /* 頁在虛擬記憶體中的地址,動態映射物理頁 */
}
下面,我們來解釋下其中的重要欄位。
flags:這個欄位用於存放頁的狀態。這些狀態包括頁是不是髒的,是不是被鎖定在記憶體中等。 flag 的每一位單獨表示一種狀態,所以,它至少可以同時表示出32種不同的狀態。
_count:這個欄位存放頁的使用計數,也就是這個頁被引用了多少次。很奇怪,技術值變為 -1 時,就說明當前核心並沒有引用這一頁,於是,在新的分配中就可以使用它,注意,這個欄位使用的是 -1 代表未使用,而不是 0 。
virtual:這個欄位是頁的虛擬位址。
mapping:這個域指向和這個頁關聯的address_space 對象。
private:這個根據名字就可以看得出,它指向私人資料。
核心通過這樣的資料結構管理系統中所有的頁,因為核心需要知道一個頁是否空閑,誰有擁有這個頁。擁有者可能是:使用者空間進程、動態分配的核心資料、靜態核心代碼、頁快取等等。系統中每一個物理頁都要分配這樣一個結構體,進行記憶體管理。
區
由於硬體的限制,核心並不能對所有的頁一視同仁。Linux必須處理如下兩種由於硬體存在缺陷而引起的記憶體定址問題:
1)一些硬體只能用某些特定的記憶體位址來執行DMA(直接記憶體存取)。
2)一些體繫結構其記憶體的物理定址範圍比虛擬定址範圍大得多。這樣,就有一些記憶體不能永久地映射到核心空間上。
由於存在這種限制,核心把具有相似特性的頁劃分為不同的區(ZONE):
1)ZONE_DMA——這個區包含的頁能用來執行DMA操作。
2)ZONE_NORMAL——這個區包含的都是能正常地映射網頁。
3)ZONE_DMA32——同上,不過只能被32位裝置訪問
4)ZONE_HIGHMEM——這個區包含“高端記憶體”,其中的頁並能不永久地映射到核心地址空間。
Linux把系統的頁劃分為區,形成不同的記憶體池,這樣就可以根據用途進行分配。注意,區的劃分沒有任何物理意義,這隻是核心為了管理頁而採取的一種邏輯上的分組。用於DMA的記憶體必須從ZONE_DMA中進行分配,但是一般用途的記憶體卻既能從ZONE_DMA分配,也能從ZONE_NORMAL分配。
獲得頁
核心提供了一種請求記憶體的底層機制,並提供了對它進行訪問的幾個介面。所有這些介面都以頁為單位分配記憶體,定義於<linux/gfp.h>。最核心的函數是:
structpage *alloc_pages( unsigned int gfp_mask, unsigned int order );
該函數分配 2order 個連續的物理頁,並返回一個指向第一頁的 page 結構體指標,如果出錯就返回NULL。
void*page_address( struct page *page );
把給定的頁轉換成它的邏輯地址。如果無須用到 struct page,可以調用:
unsignedlong __get_free_pages( unsigned int gfp_mask, unsigned int order );
這個函數與alloc_pages 作用相同,不過它直接返回所請求的第一個頁的邏輯地址。因為頁是連續的,因此其他頁也會緊隨其後。
如果只需要一頁,可以用以下兩個函數:
structpage *alloc_page( unsigned int gfp_mask );
unsignedlong _get_free_page( unsigned int gfp_mask );
如果需要讓返回頁的內容全為0,可以使用下面這個函數
unsignedlong get_zeroed_page(unsigned int gfp_mask );
方法 |
描述 |
alloc_page(gfp_mask) |
只分配一頁,返回指向頁結構的指標 |
alloc_pages(gfp_mask, order) |
分配 2^order 個頁,返回指向第一頁頁結構的指標 |
__get_free_page(gfp_mask) |
只分配一頁,返回指向其邏輯地址的指標 |
__get_free_pages(gfp_mask, order) |
分配 2^order 個頁,返回指向第一頁邏輯地址的指標 |
get_zeroed_page(gfp_mask) |
只分配一頁,讓其內容填充為0,返回指向其邏輯地址的指標 |
當不再需要頁時可以使用以下函數來釋放它。
void__free_pages( struct page *page, unsigned int order );
voidfree_pages( unsigned long addr, unsigned int order );
voidfree_page( unsigned long addr );
釋放頁時要謹慎,只能釋放屬於你的頁。傳遞了錯誤的 struct page 或地址,用了錯誤的 order 值都可能導致系統崩潰。請記住,核心是完全依賴自己的。
kmalloc()
kmalloc 與 malloc 一族函數非常類似,只不過它多了一個 flags 參數。kmalloc在<linux/slab.h>中聲明:
void*kmalloc( size_t size, int flags );
這個函數返回一個指向記憶體塊的指標,其記憶體塊至少要有 size 大小。所分配的記憶體正在物理上是連續的。在出錯時,它返回 NULL。除非沒有足夠的記憶體可用,否則核心總能分配成功。在對 kmalloc 調用之後,你必須檢查返回的是不是 NULL,如果是,要適當地處理錯誤。
在低級頁分配函數還是 kmalloc 中,都用到了gfp_mask(分配器標誌)。這些標誌可分為三類:行為修飾符、區修飾符及類型。
1)行為修飾符表示核心應當如何分配所需的記憶體。在某些特定情況下,只能使用某些特定的方法分配記憶體。例如,中斷處理常式就要求核心在分配記憶體的過程中不能睡眠(因為中斷處理常式不能被重新調度)。
2)區修飾符指明到底從哪一區中進行分配。
3)類型標誌組合了行為修飾符和區修飾符,將各種可能用到的組合歸納為不同類型,簡化了修飾符的使用。
kmalloc 的另一端就是 kfree,kfree聲明於<linux/slab.h>中
voidkfree( constvoid *ptr );
kfree 函數釋放由 kmalloc分配出來的記憶體塊。調用 kfree( NULL ) 是安全的。
vmalloc()
vmalloc 的工作方式是類似於 kmalloc,只不過前者分配的記憶體虛擬位址是連續的,而物理地址則無需連續。這也是使用者空間分配函數的工作方式:由malloc()返回的頁在進程的虛擬位址空間內是連續的,但是這並不保證他們在物理RAM中也是連續的。kmalloc()函數確保頁在物理地址上是連續。vmalloc函數值確保在虛擬位址空間內是連續的。它通過分配非連續的實體記憶體塊,在修訂頁表,把記憶體映射到邏輯地址空間的連續地區中,就能做到這點。
大多數情況下,只有硬體裝置需要得到物理地址連續的記憶體,因為硬體裝置存在記憶體管理單元以外,它根本不理解什麼是虛擬位址。儘管僅僅在某些情況下才需要物理上連續的記憶體塊,但是很多核心都有kmalloc()來擷取記憶體,而不是vmalloc()。這主要出於效能方面的考慮。vmalloc()函數為了把物理上不連續的頁轉換成虛擬位址空間上連續的頁,必須專門建立頁表項。糟糕的是,通過vmalloc()獲得的頁必須一個一個地進行映射。因為這些原因,一般是在為了獲得大塊記憶體時,例如當模組被動態插入核心時,就把模組裝載到由vmalloc()分配的記憶體上。
void *vmalloc(unsigned long size)
該函數返回一個指標,指向邏輯上連續的一塊記憶體,其大小至少為size。在發生錯誤時,函數返回NULL。函數可能睡眠,因此麼不能從中斷上下文中進行調用,也不能從其他不允許阻塞的情況下進行調用。
釋放通過vfree()函數
void vfree(const void *addr)
slab層
為了便於資料的頻繁分配和回收,Linux核心提供了slab層(也就是所謂的slab分配器)。slab分配器扮演了通用資料結構緩衝層的角色。
slab層把不同的對象劃分為快取,其中每個快取組中存放的都是不同類型的資料結構對象。例如,一個快取用於存放進程描述符,另一個快取用於存放i節點。這些快取又被劃分為slab,slab由一個或多個物理上連續的頁組成。一般情況下,slab也就僅僅由一頁組成。每個快取可以由多個slab組成。
每個slab都包含一些對象成員,這裡的對象指的是被緩衝的資料結構。每個slab處於三種狀態之一:滿、部分滿或空。當核心的某一部分需要一個對象時,就要由slab分配了,首先考慮的是部分滿的slab,如果不存在部分滿的slab則去空的slab分配,如果也不存在空的slab,則核心需要申請頁重新分配快取。描述了快取、slab及對象之間的關係,來自http://www.cnblogs.com/wang_yb/archive/2013/05/23/3095907.html
整個slab層的原理如下:
1.可以在記憶體中建立各種對象的快取(比如進程描述相關的結構 task_struct的快取)
2.除了針對特定對象的快取以外,也有通用對象的快取
3.每個快取中包含多個 slab,slab用於管理緩衝的對象
4.slab中包含多個緩衝的對象,物理上由一頁或多個連續的頁組成
每個快取都是用kmem_cache_s 結構來表示。這個結構包含三個鏈表 slabs_full,slabs_partial和 slabs_empty,均存放在 kmem_lists 結構內。這些鏈表包含快取中的所有slab。slab描述符 structslab 用來描述每個slab:
struct slab {
struct list_head list; /* 滿、部分滿或空鏈表 */
unsigned long colouroff; /* slab 著色的位移量 */
void *s_mem; /* 在 slab 中的第一個對象 */
unsigned int inuse; /* 已指派的對象數 */
kmem_bufctl_t tree; /* 第一個空間對象(如果有的話) */
};
slab分配器的介面
主要有四個
1. 快取的建立
struct kmem_cache * kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *))
2. 從快取中指派至
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
3. 釋放對象,返回給原先的slab
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
4.快取的銷毀
void kmem_cache_destroy(struct kmem_cache *cachep)
slab解決記憶體片段
記憶體片段存在的方式有兩種:a.內部片段 b.外部片段
內部片段的產生:因為所有的記憶體配置必須起始於可被 4、8或 16 整除(視處理器體繫結構而定)的地址或者因為MMU的分頁機制的限制,決定記憶體配置演算法僅能把預定大小的記憶體塊分配給客戶。假設當某個客戶請求一個 43 位元組的記憶體塊時,因為沒有適合大小的記憶體,所以它可能會獲得 44位元組、48位元組等稍大一點的位元組,因此由所需大小四捨五入而產生的多餘空間就叫內部片段。
外部片段的產生: 頻繁的分配與回收物理頁面會導致大量的、連續且小的頁面塊夾雜在已指派的頁面中間,就會產生外部片段。假設有一塊一共有100個單位的連續空閑記憶體空間,範圍是0~99。如果你從中申請一塊記憶體,如10個單位,那麼申請出來的記憶體塊就為0~9區間。這時候你繼續申請一塊記憶體,比如說5個單位大,第二塊得到的記憶體塊就應該為10~14區間。如果你把第一塊記憶體塊釋放,然後再申請一塊大於10個單位的記憶體塊,比如說20個單位。因為剛被釋放的記憶體塊不能滿足新的請求,所以只能從15開始分配出20個單位的記憶體塊。現在整個記憶體空間的狀態是0~9空閑,10~14被佔用,15~24被佔用,25~99空閑。其中0~9就是一個記憶體片段了。如果10~14一直被佔用,而以後申請的空間都大於10個單位,那麼0~9就永遠用不上了,變成外部片段。
解決方案:
slab機制,因為slab預先分配了特定資料結構大小的記憶體,所以沒有內部片段或者外部片段。
slab與傳統記憶體管理員模式比較:
與傳統的記憶體管理員模式相比, slab 緩衝分配器提供了很多優點。首先,核心通常依賴於對小對象的分配,它們會在系統生命週期內進行無數次分配。slab 緩衝分配器通過對類似大小的對象進行緩衝而提供這種功能,從而避免了常見的片段問題。slab 分配器還支援通用對象的初始化,從而避免了為同一目而對一個對象重複進行初始化。最後,slab 分配器還可以支援硬體緩衝對齊和著色,這防止錯誤的共用(兩個或兩個對象儘管位於不同的記憶體位址,但映射到相同的告訴緩衝行),這可以提高效能,但以增加記憶體浪費為代價。
在棧上的靜態分配
核心棧大小固定。我們在進程時要注意節省棧資源,要控制函數內的局部變數,盡量不要出現大型數組或大型結構體。尤其對於核心棧,一旦造成溢出,就會影響到核心資料(如thread_info)。所以應當優先考慮動態分配。另外一個進程的核心棧和中斷棧是分開的,這樣可以減輕核心棧的負擔(一個核心棧只佔1頁或2頁)。
高端記憶體的映射
因為32位的處理器能夠定址達到4GB。一旦這些頁被分配,就必須映射到核心的虛擬記憶體空間上。
高於896MB的所有實體記憶體的範圍大都是高端記憶體,它不會永久或自動的映射到核心虛擬位址空間。
核心地址的虛擬記憶體大小為1G,其中0-896M的記憶體與實體記憶體一一映射,即線性映射。而896MB~1024MB的虛擬記憶體如果也與實體記憶體線性映射,那麼核心態只能使用1G的實體記憶體,即使實體記憶體大於1G(比如4G),這樣的話就沒有充分利用實體記憶體了。所以核心虛擬記憶體中的896MB~1024MB與高端記憶體不會一一映射。具體的映射方式如下:
當核心態需要訪問高端實體記憶體時,在核心虛擬記憶體空間中的896-1024MB找一段相應大小閒置邏輯地址空間,借用一會。借用這段邏輯地址空間,建立映射到想要訪問的那段實體記憶體,臨時用一會,用完後歸還。這樣當進程後面又需要訪問其他的高端實體記憶體時,仍然可以用這段邏輯地址空間。
高端記憶體的最基本思想:在核心虛擬空間896MB~1024MB的記憶體中借一段地址空間,建立與高端實體記憶體的臨時地址映射,用完後釋放虛擬空間,達到這段虛擬位址空間可以迴圈使用,訪問所有實體記憶體。
高端記憶體映射有三種方式:
1、映射到“核心動態映射空間”
這種方式很簡單,因為通過 vmalloc() ,在”核心動態映射空間“申請記憶體的時候,就可能從高端記憶體獲得頁面(參看 vmalloc 的實現),因此說高端記憶體有可能映射到”核心動態映射空間“ 中。
2、永久核心映射
如果是通過alloc_page() 獲得了高端記憶體對應的 page,如何給它找個線性空間?
核心專門為此留出一塊線性空間,從 PKMAP_BASE 到 FIXADDR_START ,用於映射高端記憶體。在 2.4 核心上,這個位址範圍是 4G-8M 到 4G-4M 之間。這個空間起叫“核心永久映射空間”或者“永久核心映射空間”。這個空間和其它空間使用同樣的頁目錄表,對於核心來說,就是 swapper_pg_dir,對普通進程來說,通過 CR3 寄存器指向。通常情況下,這個空間是 4M 大小,因此僅僅需要一個頁表即可,核心通過來 pkmap_page_table 尋找這個頁表。
3、臨時映射
當必須建立一個映射而當前的上下文又不能睡眠時,核心提供了臨時映射(也就是原子映射)。有一組保留的映射,他們可以存放新建立的臨時映射。核心可以原子地把高端記憶體中的一個頁映射到某個保留的映射中。因此,臨時映射可以用在不能睡眠的地方,比如中斷處理常式中,因為擷取映射時絕不會阻塞。
每個CPU資料
SMP環境下加鎖過多的話,會嚴重影響並行的效率,如果是自旋鎖的話,還會浪費其他CPU的執行時間。所以核心中才有了按CPU分配資料的介面。按CPU分配資料之後,每個CPU自己的資料不會被其他CPU訪問,雖然浪費了一點記憶體,但是會使系統更加的簡潔高效。
按CPU來分配資料主要有2個優點:
1.最直接的效果就是減少了對資料的鎖,提高了系統的效能
2.由於每個CPU有自己的資料,所以處理器切換時可以大大減少緩衝失效的幾率。因為如果一個處理器操作某個資料,而這個資料在另一個處理器的緩衝中時,那麼存放這個資料的那個處理器必須清理或重新整理自己的緩衝。持續的緩衝失效成為緩衝抖動,對系統效能影響很大。