13. 內存管理 html
Linux對物理內存的描述機制有兩種:UMA和NUMA。Linux把物理內存劃分爲三個層次來管理:存儲節點(Node)、管理區(Zone)和頁面 (Page)。UMA對應一致存儲結構,它只須要一個Node就能夠描述當前系統中的物理內存,可是NUMA的出現打破了這種平靜,此時須要多個 Node,它們被統必定義爲一個名爲discontig_node_data的數組。爲了和UMA兼容,就將描述UMA存儲結構的描述符 contig_page_data放到該數組的第一個元素中。內核配置選項CONFIG_NUMA決定了當前系統是否支持NUMA機制。此時不管UMA還 是NUMA,它們都是對應到一個類型爲pg_data_t的數組中,便於統一管理。 linux
上圖描述Linux管理物理內存的三個層次之間的拓撲關係。從圖中能夠看出一個存儲節點由pg_data_t描述,一個UMA系統中只有一個Node,而 在NUMA中則能夠存在多個Node。它由CONFIG_NODES_SHIFT配置選項決定,它是CONFIG_NUMA的子選項,因此只有配置了 CONFIG_NUMA,該選項才起做用。UMA狀況下,NODES_SHIFT被定義爲0,MAX_NUMNODES也即爲1。 數組
include/linux/numa.h 緩存
#ifdef CONFIG_NODES_SHIFT 數據結構
#define NODES_SHIFT CONFIG_NODES_SHIFT 架構
#else app
#define NODES_SHIFT 0 async
#endif
#define MAX_NUMNODES (1 << NODES_SHIFT)
這裏主要介紹UMA機制。contig_page_data被定義以下:
mm/page_alloc.c
struct pglist_data __refdata contig_page_data = { .bdata = &bootmem_node_data[0] };
EXPORT_SYMBOL(contig_page_data);
struct pglist_data便是pg_data_t的原型。瞭解pg_data_t中的結構成員對於瞭解內存管理是必經之路:
enum zone_type {
ZONE_DMA,
ZONE_NORMAL,
ZONE_MOVABLE,
......
__MAX_NR_ZONES
};
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
struct page *node_mem_map;
#ifdef CONFIG_CGROUP_MEM_RES_CTLR
struct page_cgroup *node_page_cgroup;
#endif
#endif
struct bootmem_data *bdata;
...... /* for CONFIG_MEMORY_HOTPLUG */
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;
注意到zonelist中的_zonerefs元素,它用來實現分配器分配內存時候的管理區後援功能。MAX_ZONES_PER_ZONELIST被定 義爲全部節點中包含的最多管理區的和並加上1,加1的目的是在後援鏈表中,能夠檢測是否遍歷到最後一個節點了,若是是說明申請失敗。
/* Maximum number of zones on a zonelist */
#define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES * MAX_NR_ZONES)
struct zonelist {
struct zonelist_cache *zlcache_ptr; // NULL or &zlcache
struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
#ifdef CONFIG_NUMA
struct zonelist_cache zlcache; // optional ...
#endif
};
節點中的管理區都在free_area_init_core函數中初始化。調用關係以下所示:
start_kernel->setup_arch->paging_init->bootmem_init->bootmem_free_node->free_area_init_node->free_area_init_core
在理想的計算機體系結構中,一個物理頁框就是一個內存存儲單元,可用於任何事情:存放內核數據和用戶數據,磁盤緩衝數據等。熱河中磊的數據頁均可以存放在 任何頁框中,沒有什麼限制。可是,實際的計算機體系結構有硬件的制約,這制約頁框可使用的方式。尤爲是Linux內核必須處理80x86體系結構的兩種 硬件約束:
最後一種限制不只存在於80x86,而存在於全部的體系結構中。爲了應對這兩種限制,Linux把每一個內存節點的物理內存劃分爲多個(一般爲3個)管理區(zone)。在80x86 UMA體系結構中的管理區爲:
對於ARM來講,ZONE_HIGHMEM被名爲ZONE_MOVABLE的宏取代,而ZONE_DMA也不會僅限於最低的16MB,而可能對應全部的內存區域,此時只有內存節點ZONE_DMA有效,因此ZONE_DMA並不必定名副其實的用來做爲DMA訪問之用。
ZONE_DMA和ZONE_NORMAL區包含內存的"常規"頁框,經過把它們線性的映射到線性地址的第4個 GB(0xc0000000-0xcfffffff),內核就能夠直接訪問。相反ZONE_HIGHMEM或者ZONE_MOVABLE區包含的內存頁不 能由內核直接訪問,儘管它們也線性地映射到了線性地址空間的第4個GB。每一個內存管理區都有本身的描述符struct zone。它用來保存管理區的跟蹤信息:內存使用統計,空閒區,鎖定區等。
include/linux/mmzone.h
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];
struct free_area free_area[MAX_ORDER];
ZONE_PADDING(_pad1_)
/* Fields commonly accessed by the page reclaim scanner */
spinlock_t lru_lock;
struct {
struct list_head list;
unsigned long nr_scan;
} lru[NR_LRU_LISTS];
unsigned long recent_rotated[2];
unsigned long recent_scanned[2];
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];
};
在申請內存時,會遇到兩種狀況:若是有足夠的空閒頁可用,請求就會被馬上知足;不然,必須回收一些內存,而且將發出請求的內核控制路徑阻塞,直到有內存被 釋放。不過有些內存請求不能被阻塞。這種狀況發生在處理中斷或在執行臨界區內的代碼時。在這些狀況下,一條內核控制路徑應使用原子內存分配請求 (GFP_ATOMIC)。原子請求從不被阻塞;若是沒有足夠的空閒頁,則僅僅是分配失敗而已。
內核爲了儘量保證一個原子內存分配請求成功,它爲原子內存分配請求保留了一個頁框池,只有在內存不足時才使用。保留內存的數量存放在min_free_kbytes變量中,單位爲KB。
mm/page_alloc.c
int min_free_kbytes = 1024;
.....
/* min_free_kbytes = sqrt(lowmem_kbytes * 16); */
lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10);
min_free_kbytes = int_sqrt(lowmem_kbytes * 16);
min_free_kbytes由當前直接映射區的物理內存數量決定。也即ZONE_DMA和ZONE_NORMAL內存管理區的可用頁框數決定,這能夠 經過nr_free_buffer_pages獲取。儘管能夠經過/proc/sys/vm/min_free_kbytes來修改該它的大小,可是 min_free_kbytes的初始值範圍必須是[128K, 64M]。管理區描述符中的pages_min成員存儲了管理區內保留頁框的數目。這個字段與pages_low和pages_high字段一塊兒被用在內 存分配和回收算法中。pages_low字段老是被設爲pages_min的值的5/4,而pages_high則老是被設爲pages_min的值的3 /2。這些值在模塊快初始化module_init調用的init_per_zone_pages_min中被設置。
名稱 |
大小 |
pages_min |
min_free_kbytes >> (PAGE_SHIFT - 10) |
pages_low |
pages_min * 5 / 4 |
pages_high |
pages_min * 3 / 2 |
free_area_init_core中對管理區初始化的代碼部分以下,後續章節將對該函數進一步分析。
zone->spanned_pages = size;
zone->present_pages = realsize;
zone->name = zone_names[j];
spin_lock_init(&zone->lock);
spin_lock_init(&zone->lru_lock);
zone_seqlock_init(zone);
zone->zone_pgdat = pgdat;
zone->prev_priority = DEF_PRIORITY;
zone_pcp_init(zone);
for_each_lru(l) {
INIT_LIST_HEAD(&zone->lru[l].list);
zone->lru[l].nr_scan = 0;
}
zone->recent_rotated[0] = 0;
zone->recent_rotated[1] = 0;
zone->recent_scanned[0] = 0;
zone->recent_scanned[1] = 0;
zap_zone_vm_stats(zone);
zone->flags = 0;
struct page {
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
atomic_t _count; /* Usage count, see below. */
union {
atomic_t _mapcount; /* Count of ptes mapped in mms,
* to show when page is mapped
* & limit reverse map searches.
*/
struct { /* SLUB */
u16 inuse;
u16 objects;
};
};
每個物理頁框都須要一個對應的page結構來進行管理:記錄分配狀態,分配和回收,互斥以及同步操做。對該結構成員的解釋以下:
include/linux/page-flags.h
enum pageflags {
PG_locked, /* Page is locked. Don't touch. */
PG_error,
PG_referenced,
PG_uptodate,
PG_dirty,
PG_lru,
PG_active,
......
__NR_PAGEFLAGS,
......
}
以上是頁標誌位的可能取值,一般不該該直接使用這些標誌位,而應該內核預約義好的宏,它們在相同的頭文件中被定義,可是它們是被間接定義的,也即經過##連字符來統一對它們進行定義。
#define TESTPAGEFLAG(uname, lname) \
static inline int Page##uname(struct page *page) \
{ return test_bit(PG_##lname, &page->flags); }
......
TESTPAGEFLAG(Locked, locked)
PAGEFLAG(Error, error)
PAGEFLAG(Referenced, referenced) TESTCLEARFLAG(Referenced, referenced)
宏 |
擴展函數/宏 |
用途 |
TESTPAGEFLAG(uname, lname) |
Page##uname |
測試PG_##lname位 |
SETPAGEFLAG(uname, lname) |
SetPage##uname |
設置PG_##lname位 |
CLEARPAGEFLAG(uname, lname)[a] |
ClearPage##uname |
清除PG_##lname位 |
TESTSETFLAG(uname, lname) |
TestSetPage##uname |
測試並設置PG_##lname |
TESTCLEARFLAG(uname, lname) |
TestClearPage##uname |
測試並清除PG_##lname |
PAGEFLAG(uname, lname)[b] |
TESTPAGEFLAG |
當於同時擴展了三個宏,也即三個函數 |
PAGEFLAG_FALSE(uname) |
Page##uname |
永遠返回0 |
TESTSCFLAG(uname, lname) |
TESTSETFLAG |
當於同時擴展了兩個宏,也即兩個函數 |
SETPAGEFLAG_NOOP(uname) |
SetPage##uname |
空操做 |
CLEARPAGEFLAG_NOOP(uname) |
ClearPage##unam |
空操做 |
__CLEARPAGEFLAG_NOOP(uname) |
__ClearPage##uname |
空操做 |
TESTCLEARFLAG_FALSE(uname) |
TestClearPage##uname |
永遠返回0 |
[a] 以上三個宏分別對應test_bit,set_bit和clear_bit,是原子操做,與它們對應的是有三個開頭 [b] 與此對應也有__PAGEFLAG的宏存在。 |
flags實際上爲兩部分:標誌區(Flags Area)從最低處向上擴展到第__NR_PAGEFLAGS位;字段區(Fields Area)則從最高位向低位擴展。字段區用來實現管理區,內存節點和稀疏內存的映射。
| FIELD | ... | FLAGS |
N-1 ^ 0
(__NR_PAGEFLAGS)
_count引用計數不該被直接引用,內核提供了一系列的內聯函數來操做它,一般它們被定義在include/linux/mm.h中。
函數名 |
用途 |
page_count |
讀取引用計數 |
get_page |
引用計數加1 |
init_page_count |
初始化引用計數爲1 |
_mapcount與_count引用計數相似,不該被直接引用,內核提供了一系列的內聯函數來操做它,它們也被定義在include/linux/mm.h中。
函數名 |
用途 |
reset_page_mapcount |
初始化引用計數爲-1[a] |
page_mapcount |
讀取引用計數並加1的值 |
page_mapped |
該函數根據引用計數值是否大於等於0,判斷該頁框是否被映射。 |
[a] 沒有初始化爲0是由於atomic_inc_and_test和atomic_add_negative的操做,對該引用計數的加減是由這兩個函數完成的。 |
union {
struct {
unsigned long private; /* Mapping-private opaque data:
* usually used for buffer_heads
* if PagePrivate set; used for
* swp_entry_t if PageSwapCache;
* indicates order in the buddy
* system if PG_buddy is set.
*/
struct address_space *mapping; /* If low bit clear, points to
* inode address_space, or NULL.
* If page mapped as anonymous
* memory, low bit is set, and
* it points to anon_vma object:
* see PAGE_MAPPING_ANON below.
*/
};
#if USE_SPLIT_PTLOCKS
spinlock_t ptl;
#endif
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; /* Pageout list, eg. active_list
因爲內核引入了不少的分配機制,以往間簡單的page結構變得愈來愈複雜。爲了無縫引入slub分配器來分配小於1個頁面的內存,這裏使用共用體將 slab指針和複合頁的首頁指針與private和mapping公用存儲空間。private的用途與flags標誌位息息相關。若是設置了 PG_private,那麼它被用於buffer_heads;若是設置了PG_swapcache,那麼用於swp_entry_t;若是設置了 PG_buddy,則用於夥伴系統中的階(Order)。
內核能夠將多個相鄰的頁框合併爲複合頁(Compound Page)。分組中的第一個也成爲首頁(Head Page),而全部其他各頁叫作尾頁(Tail Page)。全部尾頁對應的管理page數據結構都將first_page指向首頁。
mapping指定了頁框所在的地址空間。index是頁框在mapping映射內部的偏移量。mapping指針一般是對齊到sizeof(long)的,這保證它的最低位爲0,可是它並老是如此。能夠有如下兩種可能:
lru是一個表頭,用於在各類量表上維護該頁框,以便將它按不一樣類別分組,最重要的就是zone->lru_lock保護的活動頁框(active_list)和不活動頁框。
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
};
WANT_PAGE_VIRTUAL是由是否須要高端內存決定的,virtual用於尋址高端內存區域中的頁框,存儲該頁的虛擬地址。有些時候高端內存並不映射到任何實際的物理地址頁框上,此時它的值爲NULL。
在bootmem_init_node函數中,根據struct meminfo參數來初始化內存節點對應的pg_data_t數據結構contig_page_data,而且申請Bootmem機制使用的 bitmap。從該函數使用的參數來看,一個內存節點對應一個struct meminfo的內存塊信息。因此一個內存節點有可能對應多個membank,而這些membank的物理地址多是不連續的,這就是內存孔洞的存在的原 因。contig_page_data成員中的bdata->node_min_pfn和bdata->node_low_pfn參數分別記 錄了全部內存塊中的最低物理頁框地址和最高物理頁框地址。
arch/arm/include/asm/setup.h
struct membank {
unsigned long start;
unsigned long size;
int node;
};
struct meminfo {
int nr_banks;
struct membank bank[NR_BANKS];
};
static unsigned long __init bootmem_init_node(int node, struct meminfo *mi);
bootmem_free_node與bootmem_init_node參數相似,它用來初始化特定內存節點的管理區信息。
arch/arm/mm/init.c
static void __init bootmem_free_node(int node, struct meminfo *mi);
#define arch_adjust_zones(node,size,holes) do { } while (0)
mm/page_alloc.c
void __paginginit free_area_init_node(int nid, unsigned long *zones_size,
unsigned long node_start_pfn, unsigned long *zholes_size)
free_area_init_node的函數參數解釋以下:
free_area_init_node完成如下功能:
mm/page_alloc.c
/*
* Set up the zone data structures:
* - mark all pages reserved
* - mark all memory queues empty
* - clear the memory bitmaps
*/
static void __paginginit free_area_init_core(struct pglist_data *pgdat,
unsigned long *zones_size, unsigned long *zholes_size);
free_area_init_core的函數參數解釋以下:
free_area_init_core針對單個內存節點內的全部管理區進行初始化,並計算管理內存頁所用的struct page數組佔用的memmap_pages。
接下來遍歷內存節點中的全部管理區,完成如下工做:
memmap_init_zone函數初始化每一個管理區中的頁幀對應的page數組。
mm/page_alloc.c
#ifndef __HAVE_ARCH_MEMMAP_INIT
#define memmap_init(size, nid, zone, start_pfn) \
memmap_init_zone((size), (nid), (zone), (start_pfn), MEMMAP_EARLY)
#endif
void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
unsigned long start_pfn, enum memmap_context context);
mem_init_zone經過memmap_init宏實現調用,因爲一些特定的架構,系統公共的memmap初始化函數沒法知足需求,好比IA64。 此時在特定架構的代碼中會定義memmap_init。memmap_init_zone顧名思義,它是針對單個管理區對應的page數組來初始化的。
memmap_init_zone依次完成了如下功能:
include/asm-generic/memory_model.h
#if defined(CONFIG_FLATMEM)
#ifndef ARCH_PFN_OFFSET
#define ARCH_PFN_OFFSET (0UL)
#endif
......
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page) ((unsigned long)((page) - mem_map) + \
ARCH_PFN_OFFSET)
......
#ifdef CONFIG_OUT_OF_LINE_PFN_TO_PAGE
struct page;
/* this is useful when inlined pfn_to_page is too big */
extern struct page *pfn_to_page(unsigned long pfn);
extern unsigned long page_to_pfn(struct page *page);
#else
#define page_to_pfn __page_to_pfn
#define pfn_to_page __pfn_to_page
#endif /* CONFIG_OUT_OF_LINE_PFN_TO_PAGE */
在struct page管理數組是線性分佈的時候,pfn_to_page被統必定義爲__pfn_to_page。平坦內存中的ARCH_PFN_OFFSET被定義 爲0,而mem_map在alloc_node_mem_map中被賦值爲node_mem_map,也即管理數組的首地址。
page中flags成員的Field Area由三部分組成,它們從高地址位該是依次分佈。段區只有在配置了CONFIG_SPARSEMEM時纔有可能存在。
/* .....
*
* No sparsemem or sparsemem vmemmap: | NODE | ZONE | ... | FLAGS |
* classic sparse with space for node:| SECTION | NODE | ZONE | ... | FLAGS |
* classic sparse no space for node: | SECTION | ZONE | ... | FLAGS |
*/
build_all_zonelists在init/main.c中的start_kernel中被調用,它用來初始化內存分配器使用的存儲節點中的管理區鏈表。
include/linux/kernel.h
extern enum system_states {
SYSTEM_BOOTING,
SYSTEM_RUNNING,
SYSTEM_HALT,
SYSTEM_POWER_OFF,
SYSTEM_RESTART,
SYSTEM_SUSPEND_DISK,
} system_state;
mm/page_alloc.c
void build_all_zonelists(void)
{
set_zonelist_order();
if (system_state == SYSTEM_BOOTING) {
__build_all_zonelists(NULL);
mminit_verify_zonelist();
cpuset_init_current_mems_allowed();
} else {
/* we have to stop all cpus to guarantee there is no user
of zonelist */
stop_machine(__build_all_zonelists, NULL, NULL);
/* cpuset refresh routine should be here */
}
vm_total_pages = nr_free_pagecache_pages();
if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES))
page_group_by_mobility_disabled = 1;
else
page_group_by_mobility_disabled = 0;
printk("Built %i zonelists in %s order, mobility grouping %s. "
"Total pages: %ld\n",
num_online_nodes(),
zonelist_order_name[current_zonelist_order],
page_group_by_mobility_disabled ? "off" : "on",
vm_total_pages);
}
build_all_zonelists依次完成如下功能:
pageblock_order在free_area_init_core中被初始化爲夥伴系統中的最高階,pageblock_nr_pages * MIGRATE_TYPES的用於在於能夠確保當前內存能夠知足最高階鏈表中至少有一個可分配節點的存在。
include/linux/pageblock-flags.h
#define pageblock_nr_pages (1UL << pageblock_order)
一個擁有256MB的內存開發板上的管理區參考鏈表信息以下:
Built 1 zonelists in Zone order, mobility grouping on. Total pages: 65024
build_all_zonelists調用了一些列函數,調用流程以下所示:
/* return values int ....just for stop_machine() */
static int __build_all_zonelists(void *dummy)
{
int nid;
for_each_online_node(nid) {
pg_data_t *pgdat = NODE_DATA(nid);
build_zonelists(pgdat);
build_zonelist_cache(pgdat);
}
return 0;
}
__build_all_zonelists的dummy參數沒有任何意義,而且返回值永遠爲0,這是爲了方便stop_machine對其結果做爲參數引用。build_zonelist_cache與CONFIG_NUMA相關,它用來設置zlcache相關的成員。
static void build_zonelist_cache(pg_data_t *pgdat)
{
pgdat->node_zonelists[0].zlcache_ptr = NULL;
}
static void build_zonelists(pg_data_t *pgdat)
{
int node, local_node;
enum zone_type j;
struct zonelist *zonelist;
local_node = pgdat->node_id;
zonelist = &pgdat->node_zonelists[0];
j = build_zonelists_node(pgdat, zonelist, 0, MAX_NR_ZONES - 1);
for (node = local_node + 1; node < MAX_NUMNODES; node++) {
if (!node_online(node))
continue;
j = build_zonelists_node(NODE_DATA(node), zonelist, j,
MAX_NR_ZONES - 1);
}
for (node = 0; node < local_node; node++) {
if (!node_online(node))
continue;
j = build_zonelists_node(NODE_DATA(node), zonelist, j,
MAX_NR_ZONES - 1);
}
zonelist->_zonerefs[j].zone = NULL;
zonelist->_zonerefs[j].zone_idx = 0;
}
注意到最後一個_zonerefs元素的zone被設置爲NULL,zone_idx爲0,這是用來遍歷時判斷結尾。
static int build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist,
int nr_zones, enum zone_type zone_type)
{
struct zone *zone;
BUG_ON(zone_type >= MAX_NR_ZONES);
zone_type++;
do {
zone_type--;
zone = pgdat->node_zones + zone_type;
if (populated_zone(zone)) {
zoneref_set_zone(zone,
&zonelist->_zonerefs[nr_zones++]);
check_highest_zone(zone_type);
}
} while (zone_type);
return nr_zones;
}
build_zonelists_node針對單個存儲節點初始化分配器的管理區列表。zone_type用來指明當前處理的管理區的個數,一般它就是 MAX_NR_ZONES - 1。nr_zones則指明瞭將被填充的_zonerefs管理區備用鏈表的索引,一般該索引以前的成員已經按內存優先級進行了賦值。循環中 zone_type一直遞減,而nr_zones索引一直遞減,因此在UMA系統中,編號越大的管理區類型優先級越高,將被最早掛載到備用鏈表。一般 ZONE_HIGHMEM的優先級最高,而ZONE_DMA的優先級最低。因此分配器在分配內存的時候一般在ZONE_HIGHMEM中分配。 populated_zone函數用來確保該區的可用內存頁(present_pages)有效,也即大於0。在內存少於896MB的系統 上,ZONE_HIGHMEM的有效內存頁就爲0,此時只有到ZONE_NORMAL或者ZONE_DMA區分配內存。
build_zonelists_node完成了如下功能,當未開啓CONFIG_NUMA時,依次映射管理區到參考鏈表中。
static void zoneref_set_zone(struct zone *zone, struct zoneref *zoneref)
{
zoneref->zone = zone;
zoneref->zone_idx = zone_idx(zone);
}
注意:zoneref同時定義zone的地址和索引,看起來畫蛇添足,這是由於在NUMA中,可能將另外一個Node中的zone加入本Node中的參考鏈表中。