爲了支持NUMA模型,也即CPU對不一樣內存單元的訪問時間可能不一樣,此時系統的物理內存被劃分爲幾個節點(node), 一個node對應一個內存簇bank,即每一個內存簇被認爲是一個節點node
pg_data_t
的實例. 系統中每一個節點被連接到一個以NULL結尾的pgdat_list
鏈表中,而其中的每一個節點利用pg_data_tnode_next
字段連接到下一節.而對於PC這種UMA結構的機器來講, 只使用了一個成爲contig_page_data
的靜態pg_data_t結構.下面咱們就來詳解講講內存管理域的內容zonelinux
NUMA結構下, 每一個處理器CPU與一個本地內存直接相連, 而不一樣處理器以前則經過總線進行進一步的鏈接, 所以相對於任何一個CPU訪問本地內存的速度比訪問遠程內存的速度要快, 而Linux爲了兼容NUMAJ結構, 把物理內存相依照CPU的不一樣node分紅簇, 一個CPU-node對應一個本地內存pgdata_t.ios
這樣已經很好的表示物理內存了, 在一個理想的計算機系統中, 一個頁框就是一個內存的分配單元, 可用於任何事情:存放內核數據, 用戶數據和緩衝磁盤數據等等. 任何種類的數據頁均可以存放在任頁框中, 沒有任何限制.算法
可是Linux內核又把各個物理內存節點分紅個不一樣的管理區域zone, 這是爲何呢?c#
由於實際的計算機體系結構有硬件的諸多限制, 這限制了頁框可使用的方式. 尤爲是, Linux內核必須處理80x86體系結構的兩種硬件約束.數組
所以Linux內核對不一樣區域的內存須要採用不一樣的管理方式和映射方式, 所以內核將物理地址或者成用zone_t表示的不一樣地址區域緩存
前面咱們說了因爲硬件的一些約束, 低端的一些地址被用於DMA, 而在實際內存大小超過了內核所能使用的現行地址的時候, 一些高地址處的物理地址不能簡單持久的直接映射到內核空間. 所以內核將內存的節點node分紅了不一樣的內存區域方便管理和映射.數據結構
Linux使用enum zone_type來標記內核所支持的全部內存區域架構
zone_type結構定義在include/linux/mmzone.h, 其基本信息以下所示併發
enum zone_type { #ifdef CONFIG_ZONE_DMA ZONE_DMA, #endif #ifdef CONFIG_ZONE_DMA32 ZONE_DMA32, #endif ZONE_NORMAL, #ifdef CONFIG_HIGHMEM ZONE_HIGHMEM, #endif ZONE_MOVABLE, #ifdef CONFIG_ZONE_DEVICE ZONE_DEVICE, #endif __MAX_NR_ZONES };
不一樣的管理區的用途是不同的,ZONE_DMA類型的內存區域在物理內存的低端,主要是ISA設備只能用低端的地址作DMA操做。ZONE_NORMAL類型的內存區域直接被內核映射到線性地址空間上面的區域(line address space),ZONE_HIGHMEM將保留給系統使用,是系統中預留的可用內存空間,不能被內核直接映射。
在內存中,每一個簇所對應的node又被分紅的稱爲管理區(zone)的塊,它們各自描述在內存中的範圍。一個管理區(zone)由struct zone結構體來描述,在linux-2.4.37以前的內核中是用typedef struct zone_struct zone_t
數據結構來描述)
管理區的類型用zone_type表示, 有以下幾種
管理內存域 | 描述 |
---|---|
ZONE_DMA | 標記了適合DMA的內存域. 該區域的長度依賴於處理器類型. 這是因爲古老的ISA設備強加的邊界. 可是爲了兼容性, 現代的計算機也可能受此影響 |
ZONE_DMA32 | 標記了使用32位地址字可尋址, 適合DMA的內存域. 顯然, 只有在53位系統中ZONE_DMA32才和ZONE_DMA有區別, 在32位系統中, 本區域是空的, 即長度爲0MB, 在Alpha和AMD64系統上, 該內存的長度多是從0到4GB |
ZONE_NORMAL | 標記了可直接映射到內存段的普通內存域. 這是在全部體系結構上保證會存在的惟一內存區域, 但沒法保證該地址範圍對應了實際的物理地址. 例如, 若是AMD64系統只有兩2G內存, 那麼全部的內存都屬於ZONE_DMA32範圍, 而ZONE_NORMAL則爲空 |
ZONE_HIGHMEM | 標記了超出內核虛擬地址空間的物理內存段, 所以這段地址不能被內核直接映射 |
ZONE_MOVABLE | 內核定義了一個僞內存域ZONE_MOVABLE, 在防止物理內存碎片的機制memory migration中須要使用該內存域. 供防止物理內存碎片的極導致用 |
ZONE_DEVICE | 爲支持熱插拔設備而分配的Non Volatile Memory非易失性內存 |
MAX_NR_ZONES | 充當結束標記, 在內核中想要迭代系統中全部內存域, 會用到該常亮 |
根據編譯時候的配置, 可能無需考慮某些內存域. 如在64位系統中, 並不須要高端內存, 由於AM64的linux採用4級頁表,支持的最大物理內存爲64TB, 對於虛擬地址空間的劃分,將0x0000,0000,0000,0000 – 0x0000,7fff,ffff,f000這128T地址用於用戶空間;而0xffff,8000,0000,0000以上的128T爲系統空間地址, 這遠大於當前咱們系統中的內存空間, 所以全部的物理地址均可以直接映射到內核中, 不須要高端內存的特殊映射. 能夠參見Documentation/x86/x86_64/mm.txt
ZONE_MOVABLE和ZONE_DEVICE實際上是和其餘的ZONE的用途有異,
關於ZONE_DEVICE, 具體的信息能夠參見[ATCH v2 3/9mm: ZONE_DEVICE for 「device memory」
While pmem is usable as a block device or via DAX mappings to userspace
there are several usage scenarios that can not target pmem due to its
lack of struct page coverage. In preparation for 「hot plugging」 pmem
into the vmemmap add ZONE_DEVICE as a new zone to tag these pages
separately from the ones that are subject to standard page allocations.
Importantly 「device memory」 can be removed at will by userspace
unbinding the driver of the device.
對於x86機器,管理區(內存區域)類型以下分佈
類型 | 區域 |
---|---|
ZONE_DMA | 0~15MB |
ZONE_NORMAL | 16MB~895MB |
ZONE_HIGHMEM | 896MB~物理內存結束 |
而因爲32位系統中, Linux內核虛擬地址空間只有1G, 而0~895M這個986MB被用於DMA和直接映射, 剩餘的物理內存被成爲高端內存. 那內核是如何藉助剩餘128MB高端內存地址空間是如何實現訪問能夠全部物理內存?
當內核想訪問高於896MB物理地址內存時,從0xF8000000 ~ 0xFFFFFFFF地址空間範圍內找一段相應大小空閒的邏輯地址空間,借用一會。借用這段邏輯地址空間,創建映射到想訪問的那段物理內存(即填充內核PTE頁面表),臨時用一會,用完後歸還。這樣別人也能夠借用這段地址空間訪問其餘物理內存,實現了使用有限的地址空間,訪問全部全部物理內存
關於高端內存的內容, 咱們後面會專門抽出一章進行講解
所以, 傳統和X86_32位系統中, 前16M劃分給ZONE_DMA, 該區域包含的頁框能夠由老式的基於ISAS的設備經過DMA使用」直接內存訪問(DMA)」, ZONE_DMA和ZONE_NORMAL區域包含了內存的常規頁框, 經過把他們線性的映射到現行地址的第4個GB, 內核就能夠直接進行訪問, 相反ZONE_HIGHME包含的內存頁不能由內核直接訪問, 儘管他們也線性地映射到了現行地址空間的第4個GB. 在64位體系結構中, 線性地址空間的大小遠遠好過了系統的實際物理地址, 內核可知直接將全部的物理內存映射到線性地址空間, 所以64位體系結構上ZONE_HIGHMEM區域老是空的.
一個管理區(zone)由
struct zone
結構體來描述(linux-3.8~目前linux4.5),而在linux-2.4.37以前的內核中是用struct zone_struct
數據結構來描述), 他們都經過typedef被重定義爲zone_t類型
zone對象用於跟蹤諸如頁面使用狀況的統計數, 空閒區域信息和鎖信息
裏面保存着內存使用狀態信息,如page使用統計, 未使用的內存區域,互斥訪問的鎖(LOCKS)等.
struct zone在linux/mmzone.h中定義, 在linux-4.7的內核中可使用include/linux/mmzone.h來查看其定義
struct zone { /* Read-mostly fields */ /* zone watermarks, access with *_wmark_pages(zone) macros */ unsigned long watermark[NR_WMARK]; unsigned long nr_reserved_highatomic; /* * We don't know if the memory that we're going to allocate will be * freeable or/and it will be released eventually, so to avoid totally * wasting several GB of ram we must reserve some of the lower zone * memory (otherwise we risk to run OOM on the lower zones despite * there being tons of freeable ram on the higher zones). This array is * recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl * changes. * 分別爲各類內存域指定了若干頁 * 用於一些不管如何都不能失敗的關鍵性內存分配。 */ long lowmem_reserve[MAX_NR_ZONES]; #ifdef CONFIG_NUMA int node; #endif /* * The target ratio of ACTIVE_ANON to INACTIVE_ANON pages on * this zone's LRU. Maintained by the pageout code. * 不活動頁的比例, * 接着是一些不多使用或者大部分狀況下是隻讀的字段: * wait_table wait_table_hash_nr_entries wait_table_bits * 造成等待列隊,能夠等待某一頁可供進程使用 */ unsigned int inactive_ratio; /* 指向這個zone所在的pglist_data對象 */ struct pglist_data *zone_pgdat; /*/這個數組用於實現每一個CPU的熱/冷頁幀列表。內核使用這些列表來保存可用於知足實現的「新鮮」頁。但冷熱頁幀對應的高速緩存狀態不一樣:有些頁幀極可能在高速緩存中,所以能夠快速訪問,故稱之爲熱的;未緩存的頁幀與此相對,稱之爲冷的。*/ struct per_cpu_pageset __percpu *pageset; /* * This is a per-zone reserve of pages that are not available * to userspace allocations. * 每一個區域保留的不能被用戶空間分配的頁面數目 */ unsigned long totalreserve_pages; #ifndef CONFIG_SPARSEMEM /* * Flags for a pageblock_nr_pages block. See pageblock-flags.h. * In SPARSEMEM, this map is stored in struct mem_section */ unsigned long *pageblock_flags; #endif /* CONFIG_SPARSEMEM */ #ifdef CONFIG_NUMA /* * zone reclaim becomes active if more unmapped pages exist. */ unsigned long min_unmapped_pages; unsigned long min_slab_pages; #endif /* CONFIG_NUMA */ /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT * 只內存域的第一個頁幀 */ unsigned long zone_start_pfn; /* * spanned_pages is the total pages spanned by the zone, including * holes, which is calculated as: * spanned_pages = zone_end_pfn - zone_start_pfn; * * present_pages is physical pages existing within the zone, which * is calculated as: * present_pages = spanned_pages - absent_pages(pages in holes); * * managed_pages is present pages managed by the buddy system, which * is calculated as (reserved_pages includes pages allocated by the * bootmem allocator): * managed_pages = present_pages - reserved_pages; * * So present_pages may be used by memory hotplug or memory power * management logic to figure out unmanaged pages by checking * (present_pages - managed_pages). And managed_pages should be used * by page allocator and vm scanner to calculate all kinds of watermarks * and thresholds. * * Locking rules: * * zone_start_pfn and spanned_pages are protected by span_seqlock. * It is a seqlock because it has to be read outside of zone->lock, * and it is done in the main allocator path. But, it is written * quite infrequently. * * The span_seq lock is declared along with zone->lock because it is * frequently read in proximity to zone->lock. It's good to * give them a chance of being in the same cacheline. * * Write access to present_pages at runtime should be protected by * mem_hotplug_begin/end(). Any reader who can't tolerant drift of * present_pages should get_online_mems() to get a stable value. * * Read access to managed_pages should be safe because it's unsigned * long. Write access to zone->managed_pages and totalram_pages are * protected by managed_page_count_lock at runtime. Idealy only * adjust_managed_page_count() should be used instead of directly * touching zone->managed_pages and totalram_pages. */ unsigned long managed_pages; unsigned long spanned_pages; /* 總頁數,包含空洞 */ unsigned long present_pages; /* 可用頁數,不包哈空洞 */ /* 指向管理區的傳統名字, "DMA", "NROMAL"或"HIGHMEM" */ const char *name; #ifdef CONFIG_MEMORY_ISOLATION /* * Number of isolated pageblock. It is used to solve incorrect * freepage counting problem due to racy retrieving migratetype * of pageblock. Protected by zone->lock. */ unsigned long nr_isolate_pageblock; #endif #ifdef CONFIG_MEMORY_HOTPLUG /* see spanned/present_pages for more description */ seqlock_t span_seqlock; #endif /* * wait_table -- the array holding the hash table * wait_table_hash_nr_entries -- the size of the hash table array * wait_table_bits -- wait_table_size == (1 << wait_table_bits) * * The purpose of all these is to keep track of the people * waiting for a page to become available and make them * runnable again when possible. The trouble is that this * consumes a lot of space, especially when so few things * wait on pages at a given time. So instead of using * per-page waitqueues, we use a waitqueue hash table. * * The bucket discipline is to sleep on the same queue when * colliding and wake all in that wait queue when removing. * When something wakes, it must check to be sure its page is * truly available, a la thundering herd. The cost of a * collision is great, but given the expected load of the * table, they should be so rare as to be outweighed by the * benefits from the saved space. * * __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the * primary users of these fields, and in mm/page_alloc.c * free_area_init_core() performs the initialization of them. */ /* 進程等待隊列的散列表, 這些進程正在等待管理區中的某頁 */ wait_queue_head_t *wait_table; /* 等待隊列散列表中的調度實體數目 */ unsigned long wait_table_hash_nr_entries; /* 等待隊列散列表數組大小, 值爲2^order */ unsigned long wait_table_bits; ZONE_PADDING(_pad1_) /* free areas of different sizes 頁面使用狀態的信息,以每一個bit標識對應的page是否能夠分配 是用於夥伴系統的,每一個數組元素指向對應階也表的數組開頭 如下是供頁幀回收掃描器(page reclaim scanner)訪問的字段 scanner會跟據頁幀的活動狀況對內存域中使用的頁進行編目 若是頁幀被頻繁訪問,則是活動的,相反則是不活動的, 在須要換出頁幀時,這樣的信息是很重要的: */ struct free_area free_area[MAX_ORDER]; /* zone flags, see below 描述當前內存的狀態, 參見下面的enum zone_flags結構 */ unsigned long flags; /* Write-intensive fields used from the page allocator, 保存該描述符的自旋鎖 */ spinlock_t lock; ZONE_PADDING(_pad2_) /* Write-intensive fields used by page reclaim */ /* Fields commonly accessed by the page reclaim scanner */ spinlock_t lru_lock; /* LRU(最近最少使用算法)活動以及非活動鏈表使用的自旋鎖 */ struct lruvec lruvec; /* * When free pages are below this point, additional steps are taken * when reading the number of free pages to avoid per-cpu counter * drift allowing watermarks to be breached * 在空閒頁的數目少於這個點percpu_drift_mark的時候 * 當讀取和空閒頁數同樣的內存頁時,系統會採起額外的工做, * 防止單CPU頁數漂移,從而致使水印被破壞。 */ unsigned long percpu_drift_mark; #if defined CONFIG_COMPACTION || defined CONFIG_CMA /* pfn where compaction free scanner should start */ unsigned long compact_cached_free_pfn; /* pfn where async and sync compaction migration scanner should start */ unsigned long compact_cached_migrate_pfn[2]; #endif #ifdef CONFIG_COMPACTION /* * On compaction failure, 1<<compact_defer_shift compactions * are skipped before trying again. The number attempted since * last failure is tracked with compact_considered. */ unsigned int compact_considered; unsigned int compact_defer_shift; int compact_order_failed; #endif #if defined CONFIG_COMPACTION || defined CONFIG_CMA /* Set to true when the PG_migrate_skip bits should be cleared */ bool compact_blockskip_flush; #endif bool contiguous; ZONE_PADDING(_pad3_) /* Zone statistics 內存域的統計信息, 參見後面的enum zone_stat_item結構 */ atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; } ____cacheline_internodealigned_in_smp;
字段 | 描述 |
---|---|
watermark | 每一個 zone 在系統啓動時會計算出 3 個水位值, 分別爲 WMAKR_MIN, WMARK_LOW, WMARK_HIGH 水位, 這在頁面分配器和 kswapd 頁面回收中會用到 |
lowmem_reserve[MAX_NR_ZONES] | zone 中預留的內存, 爲了防止一些代碼必須運行在低地址區域,因此事先保留一些低地址區域的內存 |
pageset | page管理的數據結構對象,內部有一個page的列表(list)來管理。每一個CPU維護一個page list,避免自旋鎖的衝突。這個數組的大小和NR_CPUS(CPU的數量)有關,這個值是編譯的時候肯定的 |
lock | 對zone併發訪問的保護的自旋鎖 |
free_area[MAX_ORDER] | 頁面使用狀態的信息,以每一個bit標識對應的page是否能夠分配 |
lru_lock | LRU(最近最少使用算法)的自旋鎖 |
wait_table | 待一個page釋放的等待隊列哈希表。它會被wait_on_page(),unlock_page()函數使用. 用哈希表,而不用一個等待隊列的緣由,防止進程長期等待資源 |
wait_table_hash_nr_entries | 哈希表中的等待隊列的數量 |
zone_pgdat | 指向這個zone所在的pglist_data對象 |
zone_start_pfn | 和node_start_pfn的含義同樣。這個成員是用於表示zone中的開始那個page在物理內存中的位置的present_pages, spanned_pages: 和node中的相似的成員含義同樣 |
name | zone的名字,字符串表示: 「DMA」,」Normal」 和」HighMem」 |
totalreserve_pages | 每一個區域保留的不能被用戶空間分配的頁面數目 |
ZONE_PADDING | 因爲自旋鎖頻繁的被使用,所以爲了性能上的考慮,將某些成員對齊到cache line中,有助於提升執行的性能。使用這個宏,能夠肯定zone->lock,zone->lru_lock,zone->pageset這些成員使用不一樣的cache line. |
managed_pages | zone 中被夥伴系統管理的頁面數量 |
spanned_pages | zone 中包含的頁面數量 |
present_pages | zone 中實際管理的頁面數量. 對一些體系結構來講, 其值和 spanned_pages 相等 |
lruvec | LRU 鏈表集合 |
vm_stat | zone 計數 |
該結構比較特殊的地方是它由ZONE_PADDING分隔的幾個部分. 這是由於堆zone結構的訪問很是頻繁.在多處理器系統中, 一般會有不一樣的CPU試圖同時訪問結構成員. 所以使用鎖能夠防止他們彼此干擾, 避免錯誤和不一致的問題. 因爲內核堆該結構的訪問很是頻繁, 所以會常常性地獲取該結構的兩個自旋鎖zone->lock
和zone->lru_lock
因爲
struct zone
結構常常被訪問到, 所以這個數據結構要求以L1 Cache
對齊. 另外, 這裏的ZONE_PADDING( )
讓zone->lock
和zone_lru_lock
這兩個很熱門的鎖能夠分佈在不一樣的Cahe Line
中.一個內存node
節點最多也就幾個zone
, 所以 zone 數據結構不須要像 struct page 同樣關心數據結構的大小,
那麼數據保存在CPU高速緩存中, 那麼會處理得更快速. 高速緩衝分爲行, 每一行負責不一樣的內存區.內核使用ZONE_PADDING
宏生成」填充」字段添加到結構中, 以確保每一個自旋鎖處於自身的緩存行中
ZONE_PADDING
宏定義在include/linux/mmzone.h?v4.7, line 105
/* * 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
內核還用了____cacheline_internodealigned_in_smp
,來實現最優的高速緩存行對其方式.
#if !defined(____cacheline_internodealigned_in_smp) #if defined(CONFIG_SMP) #define ____cacheline_internodealigned_in_smp \ __attribute__((__aligned__(1 << (INTERNODE_CACHE_SHIFT)))) #else #define ____cacheline_internodealigned_in_smp #endif #endif
Zone的管理調度的一些參數watermarks水印, 水存量很小(MIN)進水量,水存量達到一個標準(LOW)減少進水量,當快要滿(HIGH)的時候,可能就關閉了進水口
WMARK_LOW
, WMARK_LOW
, WMARK_HIGH
就是這個標準
enum zone_watermarks { WMARK_MIN, WMARK_LOW, WMARK_HIGH, NR_WMARK }; #define min_wmark_pages(z) (z->watermark[WMARK_MIN]) #define low_wmark_pages(z) (z->watermark[WMARK_LOW]) #define high_wmark_pages(z) (z->watermark[WMARK_HIGH])
在linux-2.4中, zone結構中使用以下方式表示水印, 參照include/linux/mmzone.h?v=2.4.37, line 171
typedef struct zone_watermarks_s { unsigned long min, low, high; } zone_watermarks_t; typedef struct zone_struct { zone_watermarks_t watermarks[MAX_NR_ZONES];
在Linux-2.6.x中標準是直接經過成員pages_min, pages_low and pages_high定義在zone結構體中的, 參照include/linux/mmzone.h?v=2.6.24, line 214
當系統中可用內存不多的時候,系統進程kswapd被喚醒, 開始回收釋放page, 水印這些參數(WMARK_MIN, WMARK_LOW, WMARK_HIGH)影響着這個代碼的行爲
每一個zone有三個水平標準:watermark[WMARK_MIN], watermark[WMARK_LOW], watermark[WMARK_HIGH],幫助肯定zone中內存分配使用的壓力狀態
標準 | 描述 |
---|---|
watermark[WMARK_MIN] | 當空閒頁面的數量達到page_min所標定的數量的時候, 說明頁面數很是緊張, 分配頁面的動做和kswapd線程同步運行. WMARK_MIN所表示的page的數量值,是在內存初始化的過程當中調用free_area_init_core中計算的。這個數值是根據zone中的page的數量除以一個>1的係數來肯定的。一般是這樣初始化的ZoneSizeInPages/12 |
watermark[WMARK_LOW] | 當空閒頁面的數量達到WMARK_LOW所標定的數量的時候,說明頁面剛開始緊張, 則kswapd線程將被喚醒,並開始釋放回收頁面 |
watermark[WMARK_HIGH] | 當空閒頁面的數量達到page_high所標定的數量的時候, 說明內存頁面數充足, 不須要回收, kswapd線程將從新休眠,一般這個數值是page_min的3倍 |
內存管理域zone_t結構中的flags字段描述了內存域的當前狀態
// http://lxr.free-electrons.com/source/include/linux/mmzone.h#L475 struct zone { /* zone flags, see below */ unsigned long flags; }
它容許使用的標識用enum zone_flags
標識, 該枚舉標識定義在include/linux/mmzone.h?v4.7, line 525, 以下所示
enum zone_flags { ZONE_RECLAIM_LOCKED, /* prevents concurrent reclaim */ ZONE_OOM_LOCKED, /* zone is in OOM killer zonelist 內存域可被回收*/ ZONE_CONGESTED, /* zone has many dirty pages backed by * a congested BDI */ ZONE_DIRTY, /* reclaim scanning has recently found * many dirty file pages at the tail * of the LRU. */ ZONE_WRITEBACK, /* reclaim scanning has recently found * many pages under writeback */ ZONE_FAIR_DEPLETED, /* fair zone policy batch depleted */ };
flag標識 | 描述 |
---|---|
ZONE_RECLAIM_LOCKED | 防止併發回收, 在SMP上系統, 多個CPU可能試圖併發的回收億i個內存域. ZONE_RECLAIM_LCOKED標誌可防止這種狀況: 若是一個CPU在回收某個內存域, 則設置該標識. 這防止了其餘CPU的嘗試 |
ZONE_OOM_LOCKED | 用於某種不走運的狀況: 若是進程消耗了大量的內存, 導致必要的操做都沒法完成, 那麼內核會使徒殺死消耗內存最多的進程, 以獲取更多的空閒頁, 該標誌能夠放置多個CPU同時進行這種操做 |
ZONE_CONGESTED | 標識當前區域中有不少髒頁 |
ZONE_DIRTY | 用於標識最近的一次頁面掃描中, LRU算法發現了不少髒的頁面 |
ZONE_WRITEBACK | 最近的回收掃描發現有不少頁在寫回 |
ZONE_FAIR_DEPLETED | 公平區策略耗盡(沒懂) |
內存域struct zone的vm_stat維護了大量有關該內存域的統計信息. 因爲其中維護的大部分信息曲面沒有多大意義
// http://lxr.free-electrons.com/source/include/linux/mmzone.h#L522 struct zone { atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; }
vm_stat的統計信息由enum zone_stat_item
枚舉變量標識, 定義在include/linux/mmzone.h?v=4.7, line 110
enum zone_stat_item { /* First 128 byte cacheline (assuming 64 bit words) */ NR_FREE_PAGES, NR_ALLOC_BATCH, NR_LRU_BASE, NR_INACTIVE_ANON = NR_LRU_BASE, /* must match order of LRU_[IN]ACTIVE */ NR_ACTIVE_ANON, /* " " " " " */ NR_INACTIVE_FILE, /* " " " " " */ NR_ACTIVE_FILE, /* " " " " " */ NR_UNEVICTABLE, /* " " " " " */ NR_MLOCK, /* mlock()ed pages found and moved off LRU */ NR_ANON_PAGES, /* Mapped anonymous pages */ NR_FILE_MAPPED, /* pagecache pages mapped into pagetables. only modified from process context */ NR_FILE_PAGES, NR_FILE_DIRTY, NR_WRITEBACK, NR_SLAB_RECLAIMABLE, NR_SLAB_UNRECLAIMABLE, NR_PAGETABLE, /* used for pagetables */ NR_KERNEL_STACK, /* Second 128 byte cacheline */ NR_UNSTABLE_NFS, /* NFS unstable pages */ NR_BOUNCE, NR_VMSCAN_WRITE, NR_VMSCAN_IMMEDIATE, /* Prioritise for reclaim when writeback ends */ NR_WRITEBACK_TEMP, /* Writeback using temporary buffers */ NR_ISOLATED_ANON, /* Temporary isolated pages from anon lru */ NR_ISOLATED_FILE, /* Temporary isolated pages from file lru */ NR_SHMEM, /* shmem pages (included tmpfs/GEM pages) */ NR_DIRTIED, /* page dirtyings since bootup */ NR_WRITTEN, /* page writings since bootup */ NR_PAGES_SCANNED, /* pages scanned since last reclaim */ #ifdef CONFIG_NUMA NUMA_HIT, /* allocated in intended node */ NUMA_MISS, /* allocated in non intended node */ NUMA_FOREIGN, /* was intended here, hit elsewhere */ NUMA_INTERLEAVE_HIT, /* interleaver preferred this zone */ NUMA_LOCAL, /* allocation from local node */ NUMA_OTHER, /* allocation from other node */ #endif WORKINGSET_REFAULT, WORKINGSET_ACTIVATE, WORKINGSET_NODERECLAIM, NR_ANON_TRANSPARENT_HUGEPAGES, NR_FREE_CMA_PAGES, NR_VM_ZONE_STAT_ITEMS };
內核提供了不少方式來獲取當前內存域的狀態信息, 這些函數大多定義在include/linux/vmstat.h?v=4.7
struct zone中實現了一個等待隊列, 可用於等待某一頁的進程, 內核將進程排成一個列隊, 等待某些條件. 在條件變成真時, 內核會通知進程恢復工做.
struct zone { wait_queue_head_t *wait_table; unsigned long wait_table_hash_nr_entries; unsigned long wait_table_bits; }
字段 | 描述 |
---|---|
wait_table | 待一個page釋放的等待隊列哈希表。它會被wait_on_page(),unlock_page()函數使用. 用哈希表,而不用一個等待隊列的緣由,防止進程長期等待資源 |
wait_table_hash_nr_entries | 哈希表中的等待隊列的數量 |
wait_table_bits | 等待隊列散列表數組大小, wait_table_size == (1 << wait_table_bits) |
當對一個page作I/O操做的時候,I/O操做須要被鎖住,防止不正確的數據被訪問。進程在訪問page前,wait_on_page_locked函數,使進程加入一個等待隊列
訪問完成後,UnlockPage函數解鎖其餘進程對page的訪問。其餘正在等待隊列中的進程被喚醒。每一個page均可以有一個等待隊列,可是太多的分離的等待隊列使得花費太多的內存訪問週期。替代的解決方法,就是將全部的隊列放在struct zone數據結構中
也能夠有一種可能,就是struct zone中只有一個隊列,可是這就意味着,當一個page unlock的時候,訪問這個zone裏內存page的全部休眠的進程將都被喚醒,這樣就會出現擁堵(thundering herd)的問題。創建一個哈希表管理多個等待隊列,能解決這個問題,zone->wait_table
就是這個哈希表。哈希表的方法可能仍是會形成一些進程沒必要要的喚醒。可是這種事情發生的機率不是很頻繁的。下面這個圖就是進程及等待隊列的運行關係:
等待隊列的哈希表的分配和創建在free_area_init_core
函數中進行。哈希表的表項的數量在wait_table_size() 函數中計算,而且保持在zone->wait_table_size成員中。最大4096個等待隊列。最小是NoPages / PAGES_PER_WAITQUEUE的2次方,NoPages是zone管理的page的數量,PAGES_PER_WAITQUEUE被定義256
zone->wait_table_bits
用於計算:根據page 地址獲得須要使用的等待隊列在哈希表中的索引的算法因子. page_waitqueue()函數負責返回zone中page所對應等待隊列。它用一個基於struct page虛擬地址的簡單的乘法哈希算法來肯定等待隊列的.
page_waitqueue()函數用GOLDEN_RATIO_PRIME的地址和「右移zone→wait_table_bits一個索引值」的一個乘積來肯定等待隊列在哈希表中的索引的。
Zone的初始化, 在kernel page table經過paging_init()函數徹底創建起z來之後,zone被初始化。下面章節將描述這個。固然不一樣的體系結構這個過程確定也是不同的,但它們的目的倒是相同的:肯定什麼參數須要傳遞給free_area_init()函數(對於UMA體系結構)或者free_area_init_node()函數(對於NUMA體系結構)。這裏省略掉NUMA體系結構的說明。
free_area_init()函數的參數:
unsigned long *zones_sizes: 系統中每一個zone所管理的page的數量的數組。這個時候,還沒能肯定zone中那些page是能夠分配使用的(free)。這個信息知道boot memory allocator完成以前還沒法知道。
內核常常請求和釋放單個頁框. 爲了提高性能, 每一個內存管理區都定義了一個每CPU(Per-CPU)的頁面高速緩存. 全部」每CPU高速緩存」包含一些預先分配的頁框, 他們被定義知足本地CPU發出的單一內存請求.
struct zone
的pageset成員用於實現冷熱分配器(hot-n-cold allocator)
struct zone { struct per_cpu_pageset __percpu *pageset; };
內核說頁面是熱的, 意味着頁面已經加載到CPU的高速緩存, 與在內存中的頁相比, 其數據訪問速度更快. 相反, 冷頁則再也不高速緩存中. 在多處理器系統上每一個CPU都有一個或者多個告訴緩存. 各個CPU的管理必須是獨立的.
儘管內存域可能屬於一個特定的NUMA結點, 於是關聯到某個特定的CPU。 但其餘CPU的告訴緩存仍然能夠包含該內存域中的頁面. 最終的效果是, 每一個處理器均可以訪問系統中的全部頁, 儘管速度不一樣. 於是, 特定於內存域的數據結構不只要考慮到所屬NUMA結點相關的CPU, 還必須照顧到系統中其餘的CPU.
pageset是一個指針, 其容量與系統可以容納的CPU的數目的最大值相同.
數組元素類型爲per_cpu_pageset, 定義在include/linux/mmzone.h?v4.7, line 254 以下所示
struct per_cpu_pageset { struct per_cpu_pages pcp; #ifdef CONFIG_NUMA s8 expire; #endif #ifdef CONFIG_SMP s8 stat_threshold; s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS]; #endif };
該結構由一個per_cpu_pages pcp變量組成, 該數據結構定義以下, 位於include/linux/mmzone.h?v4.7, line 245
struct per_cpu_pages { int count; /* number of pages in the list 列表中的頁數 */ int high; /* high watermark, emptying needed 頁數上限水印, 在須要的狀況清空列表 */ int batch; /* chunk size for buddy add/remove, 添加/刪除多頁塊的時候, 塊的大小 */ /* Lists of pages, one per migrate type stored on the pcp-lists 頁的鏈表*/ struct list_head lists[MIGRATE_PCPTYPES]; };
字段 | 描述 |
---|---|
count | 記錄了與該列表相關的頁的數目 |
high | 是一個水印. 若是count的值超過了high, 則代表列表中的頁太多了 |
batch | 若是可能, CPU的高速緩存不是用單個頁來填充的, 而是歐諾個多個頁組成的塊, batch做爲每次添加/刪除頁時多個頁組成的塊大小的一個參考值 |
list | 一個雙鏈表, 保存了當前CPU的冷頁或熱頁, 可以使用內核的標準方法處理 |
在內核中只有一個子系統會積極的嘗試爲任何對象維護per-cpu上的list鏈表, 這個子系統就是slab分配器.
struct zone中經過zone_start_pfn成員標記了內存管理區的頁面地址.
而後內核也經過一些全局變量標記了物理內存所在頁面的偏移, 這些變量定義在mm/nobootmem.c?v4.7, line 31
unsigned long max_low_pfn; unsigned long min_low_pfn; unsigned long max_pfn; unsigned long long max_possible_pfn;
PFN是物理內存以Page爲單位的偏移量
變量 | 描述 |
---|---|
max_low_pfn | x86中,max_low_pfn變量是由find_max_low_pfn函數計算而且初始化的,它被初始化成ZONE_NORMAL的最後一個page的位置。這個位置是kernel直接訪問的物理內存, 也是關係到kernel/userspace經過「PAGE_OFFSET宏」把線性地址內存空間分開的內存地址位置 |
min_low_pfn | 系統可用的第一個pfn是min_low_pfn變量, 開始與_end標號的後面, 也就是kernel結束的地方.在文件mm/bootmem.c中對這個變量做初始化 |
max_pfn | 系統可用的最後一個PFN是max_pfn變量, 這個變量的初始化徹底依賴與硬件的體系結構. |
max_possible_pfn |
x86的系統中, find_max_pfn函數經過讀取e820表得到最高的page frame的數值, 一樣在文件mm/bootmem.c中對這個變量做初始化。e820表是由BIOS建立的
This is the physical memory directly accessible by the kernel and is related to the kernel/userspace split in the linear address space marked by PAGE OFFSET.
我理解爲這段地址kernel能夠直接訪問,能夠經過PAGE_OFFSET宏直接將kernel所用的虛擬地址轉換成物理地址的區段。在文件mm/bootmem.c中對這個變量做初始化。在內存比較小的系統中max_pfn和max_low_pfn的值相同
min_low_pfn, max_pfn和max_low_pfn這3個值,也要用於對高端內存(high memory)的起止位置的計算。在arch/i386/mm/init.c文件中會對相似的highstart_pfn和highend_pfn變量做初始化。這些變量用於對高端內存頁面的分配。後面將描述。
內核在初始化內存管理區時, 首先創建管理區表zone_table. 參見mm/page_alloc.c?v=2.4.37, line 38
/* * * The zone_table array is used to look up the address of the * struct zone corresponding to a given zone number (ZONE_DMA, * ZONE_NORMAL, or ZONE_HIGHMEM). */ zone_t *zone_table[MAX_NR_ZONES*MAX_NR_NODES]; EXPORT_SYMBOL(zone_table);
MAX_NR_ZONES是一個節點中所能包容納的管理區的最大數, 如3個, 定義在include/linux/mmzone.h?v=2.4.37, line 25, 與zone區域的類型(ZONE_DMA
, ZONE_NORMAL
, ZONE_HIGHMEM
)定義在一塊兒. 固然這時候咱們這些標識都是經過宏的方式來實現的, 而不是現在的枚舉類型
MAX_NR_NODES是能夠存在的節點的最大數.
函數EXPORT_SYMBOL使得內核的變量或者函數能夠被載入的模塊(好比咱們的驅動模塊)所訪問.
該表處理起來就像一個多維數組, 在函數free_area_init_core中, 一個節點的全部頁面都會被初始化.
當前結點與系統中其餘結點的內存域以前存在一種等級次序
咱們考慮一個例子, 其中內核想要分配高端內存.
內核定義了內存的一個層次結構, 首先試圖分配」廉價的」內存. 若是失敗, 則根據訪問速度和容量, 逐漸嘗試分配」更昂貴的」內存.
高端內存是最廉價的, 由於內核沒有任何部份依賴於從該內存域分配的內存. 若是高端內存域用盡, 對內核沒有任何反作用, 這也是優先分配高端內存的緣由.
其次是普通內存域, 這種狀況有所不一樣. 許多內核數據結構必須保存在該內存域, 而不能放置到高端內存域.
所以若是普通內存徹底用盡, 那麼內核會面臨緊急狀況. 因此只要高端內存域的內存沒有用盡, 都不會從普通內存域分配內存.
最昂貴的是DMA內存域, 由於它用於外設和系統之間的數據傳輸. 所以從該內存域分配內存是最後一招.
內核還針對當前內存結點的備選結點, 定義了一個等級次序. 這有助於在當前結點全部內存域的內存都用盡時, 肯定一個備選結點
內核使用pg_data_t中的zonelist數組, 來表示所描述的層次結構.
typedef struct pglist_data { struct zonelist node_zonelists[MAX_ZONELISTS]; /* ...... */ }pg_data_t;
關於該結構zonelist的全部相關信息定義include/linux/mmzone.h?v=4.7, line 568, 咱們下面慢慢來說.
node_zonelists數組對每種可能的內存域類型, 都配置了一個獨立的數組項.
該數組項的大小MAX_ZONELISTS用一個匿名的枚舉常量定義, 定義在include/linux/mmzone.h?v=4.7, line 571
enum { ZONELIST_FALLBACK, /* zonelist with fallback */ #ifdef CONFIG_NUMA /* * The NUMA zonelists are doubled because we need zonelists that * restrict the allocations to a single node for __GFP_THISNODE. */ ZONELIST_NOFALLBACK, /* zonelist without fallback (__GFP_THISNODE) */ #endif MAX_ZONELISTS };
咱們會發如今UMA結構下, 數組大小MAX_ZONELISTS = 1, 由於只有一個內存結點, zonelist中只會存儲一個ZONELIST_FALLBACK類型的結構, 可是NUMA下須要多餘的ZONELIST_NOFALLBACK用以表示當前結點的信息
pg_data_t->node_zonelists數組項用struct zonelis結構體定義, 該結構包含了類型爲struct zoneref的一個備用列表因爲該備用列表必須包括全部結點的全部內存域,所以由MAX_NUMNODES * MAX_NZ_ZONES項組成,外加一個用於標記列表結束的空指針
struct zonelist結構的定義在include/linux/mmzone.h?v=4.7, line 606
/* * One allocation request operates on a zonelist. A zonelist * is a list of zones, the first one is the 'goal' of the * allocation, the other zones are fallback zones, in decreasing * priority. * * To speed the reading of the zonelist, the zonerefs contain the zone index * of the entry being read. Helper functions to access information given * a struct zoneref are * * zonelist_zone() - Return the struct zone * for an entry in _zonerefs * zonelist_zone_idx() - Return the index of the zone for an entry * zonelist_node_idx() - Return the index of the node for an entry */ struct zonelist { struct zoneref _zo
而struct zoneref結構的定義以下include/linux/mmzone.h?v=4.7, line 583
/* * This struct contains information about a zone in a zonelist. It is stored * here to avoid dereferences into large structures and lookups of tables */ struct zoneref { struct zone *zone; /* Pointer to actual zone */ int zone_idx; /* zone_idx(zoneref->zone) */ };
那麼咱們內核是如何組織在zonelist中組織內存域的呢?
NUMA系統中存在多個節點, 每一個節點對應一個struct pglist_data結構, 每一個結點中能夠包含多個zone, 如: ZONE_DMA, ZONE_NORMAL, 這樣就產生幾種排列順序, 以2個節點2個zone爲例(zone從高到低排列, ZONE_DMA0表示節點0的ZONE_DMA,其它相似).
Node方式, 按節點順序依次排列,先排列本地節點的全部zone,再排列其它節點的全部zone。
Zone方式, 按zone類型從高到低依次排列各節點的同相類型zone
可經過啓動參數」numa_zonelist_order」來配置zonelist order,內核定義了3種配置, 這些順序定義在mm/page_alloc.c?v=4.7, line 4551
// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4551 #define ZONELIST_ORDER_DEFAULT 0 /* 智能選擇Node或Zone方式 */ #define ZONELIST_ORDER_NODE 1 /* 對應Node方式 */ #define ZONELIST_ORDER_ZONE 2 /* 對應Zone方式 */
注意
在非NUMA系統中(好比UMA), 因爲只有一個內存結點, 所以ZONELIST_ORDER_ZONE和ZONELIST_ORDER_NODE選項會配置相同的內存域排列方式, 所以, 只有NUMA能夠配置這幾個參數
全局的current_zonelist_order
變量標識了系統中的當前使用的內存域排列方式, 默認配置爲ZONELIST_ORDER_DEFAULT
, 參見mm/page_alloc.c?v=4.7, line 4564
宏 | zonelist_order_name宏 | 排列方式 | 描述 |
---|---|---|---|
ZONELIST_ORDER_DEFAULT | Default | 由系統智能選擇Node或Zone方式 | |
ZONELIST_ORDER_NODE | Node | Node方式 | 按節點順序依次排列,先排列本地節點的全部zone,再排列其它節點的全部zone |
ZONELIST_ORDER_ZONE | Zone | Zone方式 | 按zone類型從高到低依次排列各節點的同相類型zone |
內核經過build_all_zonelists初始化了內存結點的zonelists域
首先內核經過set_zonelist_order函數設置了zonelist_order,以下所示, 參見mm/page_alloc.c?v=4.7, line 5031
創建備用層次結構的任務委託給build_zonelists, 該函數爲每一個NUMA結點都建立了相應的數據結構. 它須要指向相關的pg_data_t實例的指針做爲參數
在linux中,內核也不是對全部物理內存都一視同仁,內核而是把頁分爲不一樣的區, 使用區來對具備類似特性的頁進行分組.
Linux必須處理以下兩種硬件存在缺陷而引發的內存尋址問題:
爲了解決這些制約條件,Linux使用了三種區:
而爲了兼容一些設備的熱插拔支持以及內存碎片化的處理, 內核也引入一些邏輯上的內存區.
須要說明的是,區的劃分沒有任何物理意義, 只不過是內核爲了管理頁而採起的一種邏輯上的分組. 儘管某些分配可能須要從特定的區中得到頁, 但這並非說, 某種用途的內存必定要從對應的區來獲取,若是這種可供分配的資源不夠用了,內核就會佔用其餘可用去的內存.
下表給出每一個區及其在X86上所佔的列表