date: 2014-09-16 19:09node
在軟件設計時,咱們通常要從需求中提取出抽象(類或者數據結構),而後圍繞這些抽象設計相關的算法。內存管理天然也不能例外,這兩節咱們來看看爲了管理爲了物理內存以及整個虛存空間,linux提取哪些抽象,提取這些抽象背後的動機是什麼?這些抽象之間的關聯是什麼?linux
注:本文展現的結構體定義來自2.6.24版本的內核。算法
Node(內存節點)是由於NUMA的出現而產生的抽象。NUMA(Non Uniform Memory Access)即非一致性內存訪問,主要出如今大型機上。參考下圖,在這種系統中,每一個CPU都有本地內存,但也能夠經過總線訪問其餘CPU的本地內存;總線上還有一個公共的內存模塊,各CPU均可以經過系統總線訪問。顯然,對某個具體的CPU來講,其訪問本地內存的速度是最快的,而經過總線訪問其餘CPU本地內存的速度要慢,並且還可能面臨着競爭。可見,在這樣的系統中,雖然內存的地址是連續的,但「質地」卻不均勻,所以咱們須要將這些質地不均勻的內存分開管理,每個內存模塊稱之爲一個內存節點Node。數組
與NUMA相對的是UMA即一致性內存訪問,在這種系統下,全部處理器對內存的訪問具備相同的速度,有就是說整個內存質地是均勻的,所以只對應一個內存節點Node。傳統的計算機中採用的就是UMA結構,本文主要分析UMA結構。緩存
Node對應的數據結構爲pglist_data,其定義以下(對定義進行了簡化,去掉宏開關中的內容):安全
<include/linux/mmzone.h> typedef struct pglist_data { struct zone node_zones[MAX_NR_ZONES]; struct zonelist node_zonelists[MAX_ZONELISTS]; int nr_zones; struct page *node_mem_map; struct bootmem_data *bdata; /*自舉內存分配器*/ unsigned long node_start_pfn; /*該節點的其實頁幀邏輯編號,全局惟一*/ unsigned long node_present_pages; /* total number of physical pages */ unsigned long node_spanned_pages; /* total size of physical page range, including holes */ int node_id; wait_queue_head_t kswapd_wait; struct task_struct *kswapd; int kswapd_max_order; } pg_data_t;
給你一整塊地,你該怎麼打理?固然是將整塊地劃分紅不一樣的區域,某個區域用來蓋房子,某個區域用來種農做物,其餘的區域用來種經濟做物。這裏也同樣,內存節點將整個內存再細分爲區域Zone,結構體成員node_zones就表示內存節點所劃分的內存區域,而nr_zones指示所分區域的數量。通常分爲三個區:ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM,每一個區域都有其特殊用途。ZONE_DMA在低端地址處,該區域主要是爲ISA(Industry Standard Architecture)設備而設置;ZONE_NORMAL區域的物理內存經過某個固定的偏移映射到內核的虛擬地址空間;若是內存空間超過1G,則還會有ZONE_HIGHMEM,設置該區域的目的主要是爲了使得內核能夠訪問1G之外的內存空間。在X86架構下,區域的劃分通常以下:數據結構
當須要申請一個內存頁Page時,alloc_page的調用者通常會指定從哪一個區域分配,但若是該區域內存不夠該怎辦,這時就須要指定「備選」區域,能夠有多個備選區,優先級從高到底排列,表示當指定區域的內存不足時,優先從第一個備選區分配,若是第一個備選區仍然內存不足,則嘗試第二個備選區,依次類推,這就是所謂的分配策略。結構體成員node_zonelists就表示這種策略組。架構
一個內存節點最終管理的仍是物理內存的page,成員node_mem_map是一個page類型的指針,指向該內存節點全部的page數組。app
在介紹節點Node時,咱們已經介紹了管理區zone的概念,管理區分爲以下幾種類型:async
enum zone_type { #ifdef CONFIG_ZONE_DMA /* DMA內存域,通常爲16M,用於ISA */ ZONE_DMA, #endif #ifdef CONFIG_ZONE_DMA32 /* 在64位系統中,使用32位地址尋址、適合DMA的內存域 */ ZONE_DMA32, #endif /* 可直接映射到內核地址空間的內存區域 */ ZONE_NORMAL, #ifdef CONFIG_HIGHMEM /* 內核不能直接映射的內存域 */ ZONE_HIGHMEM, #endif /* 可移動內存區域 */ ZONE_MOVABLE, MAX_NR_ZONES };
管理區對應的結構體爲zone,定義在同一文件中。
/* 內存節點的內存域 */ struct zone { /* Fields commonly accessed by the page allocator */ /* 水線相關字段 */ unsigned long pages_min, pages_low, pages_high; unsigned long lowmem_reserve[MAX_NR_ZONES]; struct per_cpu_pageset pageset[NR_CPUS]; spinlock_t lock; struct free_area free_area[MAX_ORDER]; ZONE_PADDING(_pad1_) /* Fields commonly accessed by the page reclaim scanner */ spinlock_t lru_lock; struct list_head active_list; struct list_head inactive_list; unsigned long nr_scan_active; unsigned long nr_scan_inactive; unsigned long pages_scanned; /* since last reclaim */ unsigned long flags; /* zone flags, see below */ /* Zone statistics */ atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; int prev_priority; ZONE_PADDING(_pad2_) /* Rarely used or read-mostly fields */ wait_queue_head_t * wait_table; unsigned long wait_table_hash_nr_entries; unsigned long wait_table_bits; struct pglist_data *zone_pgdat; /* 該內存域所屬的節點 */ /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */ unsigned long zone_start_pfn; /* 該內存域第一個頁幀號 */ unsigned long spanned_pages; /* total size, including holes */ unsigned long present_pages; /* amount of memory (excluding holes) */ /* * rarely used fields: */ const char *name; /* 內存域名稱 */ } ____cacheline_internodealigned_in_smp;
首先,該結構體被ZONE_PADDING「分割」成三部分,宏ZONE_PADDING的定義在同一個文件中:
/* * zone->lock and zone->lru_lock are two of the hottest locks in the kernel. * So add a wild amount of padding here to ensure that they fall into separate * cachelines. There are very few zone structures in the machine, so space * consumption is not a concern here. */ #if defined(CONFIG_SMP) struct zone_padding { char x[0]; } ____cacheline_internodealigned_in_smp; #define ZONE_PADDING(name) struct zone_padding name; #else #define ZONE_PADDING(name) #endif
可見在SMP壞境下,ZONE_PADDING 定義了一個zone_padding類型的變量name;而zone_padding自己被定義成一個柔性數組,數組具體的大小會在編譯時決定,以保證此前的的部分按照cache line(通常爲16個字節)對齊。
爲何要按cache line進行對齊呢?ZONE_PADDING的註釋說得很清楚了:由於zone結構體會被頻繁訪問到,在多核環境下,會有多個CPU同時訪問同一個結構體,爲了不彼此間的干擾,必須對每次訪問進行加鎖。那麼是否是要對整個zone加鎖呢?一把鎖鎖住zone中全部的成員?這應該不是最有效的方法。咱們知道zone結構體很大,訪問zone最頻繁的有兩個兩個場景,一是頁面分配,二是頁面回收,這兩種場景各自訪問結構體zone中不一樣的成員。若是一把鎖鎖住zone中全部成員的話,當一個cpu正在處理頁面分配的事情,另外一個cpu想要進行頁面回收的處理,卻由於第一個cpu鎖住了zone中全部成員而只好等待。第二個CPU明知道此時能夠「安全的」進行回收處理(由於第一個CPU不會訪問與回收處理相關的成員)卻也只能乾着急。可見,咱們應該將鎖再細分。zone中定義了兩把鎖,lock以及lru_lock,lock與頁面分配有關,lru_lock與頁面回收有關。定義了兩把鎖,天然,每把鎖所控制的成員最好跟鎖呆在一塊兒,最好是跟鎖在同一個cache line中。當CPU對某個鎖進行上鎖處理時,CPU會將鎖從內存加載到cache中,由於是以cache line爲單位進行加載,因此與鎖緊挨着的成員也被加載到同一cache line中。CPU上完鎖想要訪問相關的成員時,這些成員已經在cache line中了。
增長padding,會額外佔用一些字節,但內核中zone結構體的實例並不會太多(UMA下只有三個),相比效率的提高,損失這點空間是值得的。
一個區域所包含的內存時有限的,若是任憑頁面分配程序「予取予求」的話,內存總有耗盡的時刻,等到耗盡時再作補救的話爲時已晚。因此咱們須要未雨綢繆,這內存耗盡以前實施補救措施,也就是喚醒守護進程kswapd來回收一些內存頁面。三個水位值pages_min、pages_low、pages_high與守護進程kswapd的「互動」以下圖:
關於lowmem_reserve成員,爲了不某些區域的內存分配殆盡(好比ZONE_NORMAL)而另外一些區域(好比ZONE_ HIGHMEM)卻內存充沛,「旱的旱死澇的澇死」,每一個區域都預留一些「壓箱底」的內存。
區域中到底有多少可用內存呢?由成員free_area來表示,這是一個struct free_area類型的數組,數組的尺寸MAX_ORDER的定義也在本文件中。若是沒有定義CONFIG_FORCE_MAX_ZONEORDER宏的話,MAX_ORDER的值將爲11。
/* Free memory management - zoned buddy allocator. */ #ifndef CONFIG_FORCE_MAX_ZONEORDER #define MAX_ORDER 11 #else #define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER #endif
爲何要將可用內存用一個數組來表示呢?這與夥伴系統有關。在內核中,內存管理的工做由夥伴系統來承擔,夥伴系統分配的都是2^n個連續的頁面。假定某個系統共有60個物理頁,系統運行一段時間後,物理頁面的分配情形以下:
左側的地址空間散佈着空閒頁,儘管空閒頁超過25%,但夥伴系統可以分配的連續頁面最大爲一頁,這就是內存碎片。注意這裏的內存碎片只對內核有意義,這種碎片對用戶空間沒有影響,由於用戶空間直接訪問的是虛擬地址,經過頁表映射到物理地址,即便物理空間不連續,其上的虛擬地址能夠是連續的。
若是咱們將空閒內存根據其連續頁面的大小按照2的n次冪來進行分組,n的取值在[0, MAX_ORDER]閉區間中,則能夠有效的減小內存碎片。若是須要分配連續8個頁面,則到n=3的組中去找空閒內存,找到了則分配連續的8個頁面;若是本組中已經沒有空閒頁面,則去更高一級n=4的組中去找空閒內存,該組中的空閒內存塊都是連續的16個頁面,若是該組中有空閒內存,則將空閒內存塊一分爲二,分紅兩個連續頁面爲8的空閒塊,一塊分配出去,一塊併入n=3的組中。在釋放內存頁時,假定釋放的內存爲連續的8個頁面,則將其納入n=3的空閒頁面組中,若是發現該組中某兩個內存款互爲夥伴,便可將這兩個塊合併成一個連續16個頁面的內存塊,並納入到n=4的組中。這就是夥伴系統的工做過程。
struct free_area的定義在同一個文件中:
#define MIGRATE_UNMOVABLE 0 #define MIGRATE_RECLAIMABLE 1 #define MIGRATE_MOVABLE 2 #define MIGRATE_RESERVE 3 #define MIGRATE_ISOLATE 4 /* can't allocate from here */ #define MIGRATE_TYPES 5 struct free_area { struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free; };
結構體free_area定義了一組隊列頭,這樣,一個管理區的空閒內存就經過兩個層次來管理:
空閒內存已經按2的n次冪來進行了分組,對每一個組內的內存塊爲什麼還要再分組?爲了解釋這個問題,咱們須要知道內核將已經分配的頁面按照可移動性分爲下列三種:
假定某個連續32頁的內存塊中,散落着不可移動頁(深顏色部分)和可回收頁(白色部分)。不可移動頁已經被分出去5頁(斜線部分)。
此時,雖然可回收頁有16頁,但最多可分4個連續的頁。但若是咱們將不可移動頁和可回收頁分組,以下圖,則對於可回收頁,最多可分連續的16頁,減小了內存碎片。
可見,將內存按照可移動性分組,能夠有效減小內存碎片。這就是在2.6.24版本的內核引入的「反碎片」技術的基礎。
結構體free_area中的成員nr_free表示該區域空閒頁面的總數。
讀者可能會有疑問,前面說過內存碎片是對內核而言的,而內核中主要又使用不可移動頁面。即便將內存頁按移動性分組,可這對解決內核的碎片化又有什麼幫助呢?內核仍然在不可移動頁上分配,不可移動頁上的碎片化並無改善呀?你的想法是正確的,這裏的反碎片技術實際上是對用戶空間程序有益的。大多數現代CPU都提供了使用巨型頁的可能,這種巨型頁比普通頁大不少,這對內存密集型的應用有好處。用戶空間經過頁表映射訪問內存,使用更大的頁(這個頁內的地址固然是連續的),則「地址轉換/查找緩衝區」只需處理較少的項,而且能夠下降TLB緩存(爲了加快頁面映射的速度,並非每次都從內存中訪問頁面映射目錄和頁面映射表,而是將它們裝入高速緩存中緩衝起來,這部分高速緩存稱爲TLB,即「地址轉換/查找緩衝區」(Translation Lookaside Buffers))失效的可能性。而分配巨型頁,就須要更多的連續內存頁。用戶空間的應用程序通常在可移動或者可回收內存頁上分配,將內存按移動性分組後,咱們就能夠利用這種移動性,將小塊空閒內存合併成大塊空閒內存,給分配巨型頁提供了可能。
若是內核沒法知足針對某種移動性的分配請求,會怎麼樣?這與咱們在Node定義時討論的備選區的問題相似,內核的解決辦法也是提供一個備選列表。其定義以下,其表達的意圖不言自明。
<mm/page_alloc.c> static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = { [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE }, [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE }, [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE }, [MIGRATE_RESERVE] = { MIGRATE_RESERVE, MIGRATE_RESERVE, MIGRATE_RESERVE }, /* Never used */ };
zone中其餘的成員的含義請參考註釋。
物理頁幀是物理內存的最小管理單元,其對應的抽象爲page,結構體定義以下:
struct page { /*用於描述頁的屬性。如PG_locked */ unsigned long flags; /* 使用計數,表示內核中引用該頁的次數 */ atomic_t _count; /* Usage count, see below. */ union { /* * 若是該頁映射到用戶空間虛擬地址上,則表示映射到頁表pte的次數 * 每增長一個使用者,計數器加1 */ atomic_t _mapcount; /* 若是是內核slab使用的頁,則表示其中的slab對象數目 */ unsigned int inuse;/* SLUB: Nr of objects */ }; union { struct { /* * 私有數據指針。 * 若是頁位於交換緩存,則指向存儲swp_entry_t結構 */ unsigned long private; struct address_space *mapping; }; struct kmem_cache *slab; /* SLUB: Pointer to slab */ struct page *first_page; /* Compound tail pages */ }; union { pgoff_t index; /* Our offset within mapping. */ void *freelist; /* SLUB: freelist req. slab lock */ }; struct list_head lru; /*用於頁面換出的鏈表,可能連接到活動頁鏈表和不活動頁鏈表 */ #if defined(WANT_PAGE_VIRTUAL) void *virtual; /* Kernel virtual address (NULL if not kmapped, ie. highmem) */ #endif };
爲了便於後文中的情景分析,這裏將2.4版本內核中page結構體的定義展現以下:
/* * Try to keep the most commonly accessed fields in single cache lines * here (16 bytes or greater). This ordering should be particularly * beneficial on 32bit processors. * * The first line is data used in page cache lookup, the second line * is used for linear searches (eg. clock algorithm scans). */ typedef struct page { struct list_head list; struct address_space *mapping; unsigned long index; struct page *next_hash; atomic_t count; unsigned long flags; /* atomic flags, some possibly updated asynchronously */ struct list_head lru; unsigned long age; wait_queue_head_t wait; struct page **pprev_hash; struct buffer_head * buffers; void *virtual; /* nonNULL if kmapped */ struct zone_struct *zone; } mem_map_t
該版本的實現中,page有兩個成員next_hash 和pprev_hash,該成員用來將page鏈到某個哈希表中。
page就像是物理頁幀的戶口,每個物理頁幀都對應一個page結構。若是一個物理頁面沒有對應的page結構,它就成了「黑戶」了,系統就沒法看到它,也就沒法對它進行管理。系統在初始化時,會根據物理內存的大小創建一個全局的page結構體數組mem_map,做爲物理頁面的「戶口簿」,裏面的每個元素表明一個物理的頁幀,而數組的下標就是物理page的邏輯編號。
物理內存管理中的這三層抽象,示意以下: