Node
介紹完了page和zone,沿著自底向上的順序,最後就是表示node的結構體
pglist_data
了。
typedef
struct
pglist_data
{
int
nr_zones
;
struct
zone
node_zones
[
MAX_NR_ZONES
];
struct
zonelist
node_zonelists
[
MAX_ZONELIST
];
unsigned
long
node_size
;
struct
page
*
node_mem_map
;
int
node_id
;
unsigned
long
node_start_paddr
;
struct
pglist_data
*
node_next
;
spinlock_t
lru_lock
;
。。。
}
pg_data_t
;
nr_zones
表示這個node含有多少個zones,
node_zones
[]則是一個包含各個zone結構體的陣列。
node_zonelists
[]包含了2個zonelist,一個是由本node的zones組成,另一個是由從本node分配不到記憶體時可選的備用zones組成,相當於是選擇了一個退路,所以叫fallback。
enum
{
ZONELIST_FALLBACK
,
/* zonelist with fallback */
#ifdef CONFIG_NUMA
ZONELIST_NOFALLBACK
,
/* zonelist without fallback (__GFP_THISNODE) */
#endif
MAX_ZONELISTS
};
如果能從指定的目標node獲得記憶體,則為NUMA hit;只能從備選node中獲取,則為NUMA miss,可透過“/sys/devices/system/node/node*/numastat”檢視。
node_size
是指這個node含有多少個page frames,
node_mem_map
指向node中所有struct page構成的mem_map陣列。
node_id
是這個node的邏輯ID,也就是在NUMA系統中的編號。現在Linux中的記憶體分配函式區分不同的node,靠的就是這個node_id,類似於檔案描述符fd。
node_start_paddr
(在2。6核心中被換成了node_start_pfn)是該node的起始物理地址。
node_next
指向由多個node構成的NUMA單向連結串列pgdat_list中的下一個節點。如果是UMA系統,只有一個node,則node_start_pfn為0,node_next為NULL。
你看,表示node的結構體pglist_data透過“node_zones”包含了它的下一級,也就是表示zone的結構體zone_struct,zone_struct又透過“zone_pgdat”指向包含它的node。zone_struct中“zone_mem_map”指向它的下一級,也就是表示page frame的struct page,按理struct page中也應該有一個元素是指向包含它的zone,可是好像沒看到對不對?
並不是在那個省略號裡,而是……就在struct page的“flags”裡,它用flags的高8位儲存了它所屬的zone和node。這是透過page flags找到page所屬的node的方法:
static
inline
int
page_to_nid
(
const
struct
page
*
page
)
{
struct
page
*
p
=
(
struct
page
*
)
page
;
return
(
PF_POISONED_CHECK
(
p
)
->
flags
>>
NODES_PGSHIFT
)
&
NODES_MASK
;
}
這是透過page flags找到page所屬的zone的:
static
inline
enum
zone_type
page_zonenum
(
const
struct
page
*
page
)
{
return
(
page
-
>
flags
>>
ZONES_PGSHIFT
)
&
ZONES_MASK
;
}
static
inline
struct
zone
*
page_zone
(
const
struct
page
*
page
)
{
return
&
NODE_DATA
(
page_to_nid
(
page
))
-
>
node_zones
[
page_zonenum
(
page
)];
}
事實上,page flags完整的組織是這樣的(在NODE和ZONE前面,還有一個SECTION,將在這篇文章中介紹):
只有低位的bits才是上文提到的那些表示page frame屬性和狀態的標誌位。在32位系統中,只有32個bits的flags的資源已經非常緊張了。然而,為了避免在struct page中單獨開闢記憶體空間引發反對的聲浪,ZONE, NODE和SECTION這三個傢伙還硬擠進了flags裡,佔去flags那麼多寶貴的bits,以至於現在要再新增一個標誌位都變得極其困難。
對,就是加小小的一個bit,也會在Linux社群遭到一眾的抵制,可以說是“錙銖必較”。這裡儼然已經成為了kernel王國中地價最貴的地方,就像紐約的曼哈頓一樣,真正的寸土寸金。
其實擠進來的這三個傢伙也不容易,寄人籬下,一共只分到了8個bits,能表達的zone, node和section的數目也是著實有限的。好在如果是64位系統的話,flags可以是64個bits,所以如果有的特性一定要新加標誌位的話,那就請移步64位的世界吧,32位的世界實在是沒法滿足您了。
關於這個問題的討論可參考2009年的這篇文章和2019年的這篇文章,十年過去了,面對這片兵家必爭之地,大家還在削尖了腦袋往裡面鑽。
Node和Zone的初始化
介紹完了這些核心的資料結構,來看看它們是怎麼被使用的。
free_area_init_nodes()遍歷系統中所有的nodes,呼叫free_area_init_node()依次初始化各個node。
for_each_online_node
(
nid
)
{
free_area_init_node
(
nid
,
pgdat
,
NULL
,
find_min_pfn_for_node
(
nid
),
NULL
);
}
free_area_init_core()則遍歷node內的所有zones並依次初始化。
for
(
j
=
0
;
j
<
MAX_NR_ZONES
;
j
++
)
{
struct
zone
*
zone
=
pgdat
-
>
node_zones
+
j
;
size
=
zone_spanned_pages_in_node
(
nid
,
j
,
zones_size
);
realsize
=
size
-
zone_absent_pages_in_node
(
nid
,
j
,
zholes_size
);
}
size就是上文介紹的strut zone裡的spanned_pages,realsize就是present_pages。
獲取物理記憶體
Linux為獲取page frame提供了兩個基本函式:
struct
page
*
alloc_pages
(
gfp_t
gfp_mask
,
unsigned
int
order
)
unsigned
long
__get_free_pages
(
gfp_t
gfp_mask
,
unsigned
int
order
)
兩者的引數是一模一樣的,區別體現在返回值上,alloc_pages()返回的是指向第一個page的struct page的指標,__get_free_pages()返回的是第一個page對映後的虛擬地址。其實__get_free_pages()就是比alloc_pages()多了一個地址轉換的工作,因為CPU直接使用的是虛擬地址,這樣做也是為了給呼叫者提供更大的方便。
unsigned
long
__get_free_pages
(
gfp_t
gfp_mask
,
unsigned
int
order
)
{
page
=
alloc_pages
(
gfp_mask
,
order
);
if
(
page
!
=
NULL
)
return
(
unsigned
long
)
page_address
(
page
);
}
關於__get_free_pages()的返回值用unsigned long是否合適,可參考這篇文章的討論。呼叫這2個函式都會獲得
個page frames,這些pages在物理地址上是連續的。之所以要求是2的n次方,這是由底層的buddy分配機制決定的。order需大於或等於0,如果只需要一個page,可設定order為0,也可以直接呼叫現成的alloc_page(gfp_mask)。
那誰會放著可以一口氣分配多個page frames的alloc_pages()不用,而是去呼叫alloc_page()一個個的分呢?那就是前面介紹到的vmalloc()啦,因為vmalloc()分配的物理記憶體可能是不連續的,所以不能直接使用alloc_pages()。
for
(
i
=
0
;
i
<
area
-
>
nr_pages
;
i
++
)
{
struct
page
*
page
;
if
(
node
==
NUMA_NO_NODE
)
page
=
alloc_page
(
alloc_mask
);
else
page
=
alloc_pages_node
(
node
,
alloc_mask
,
order
);
}
kmalloc(size, flags)則是按位元組分配,根據核心4。19的程式碼(/include/linux/slab。h), 如果申請的空間比較小,那麼kmalloc將使用slab分配器,否則將透過alloc_pages()直接使用buddy分配器。對於slub分配器,劃分的界限是2個pages大小。
void
*
kmalloc
(
size_t
size
,
gfp_t
flags
)
{
if
(
size
>
KMALLOC_MAX_CACHE_SIZE
)
return
kmalloc_large
(
size
,
flags
);
。。。
}
不管是直接呼叫alloc_pages()還是使用kmalloc(),都有用到一組限定了從何處獲取以及如何獲取空閒物理記憶體的
GFP
(Get Free Page)標誌位。關於GFP的介紹,請看下文。
原創文章,轉載請註明出處。