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,將在這篇文章中介紹):

Linux中的物理記憶體管理 [二]

只有低位的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的初始化

介紹完了這些核心的資料結構,來看看它們是怎麼被使用的。

Linux中的物理記憶體管理 [二]

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個函式都會獲得

2^{order}

個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的介紹,請看下文。

原創文章,轉載請註明出處。