1. 程式人生 > >Linux內存描述之內存節點node--Linux內存管理(二)

Linux內存描述之內存節點node--Linux內存管理(二)

不一定 狀態 不同的 所有結點 結構 area ron 還需 ini

1 內存節點node

1.1 為什麽要用node來描述內存

這點前面是說的很明白了, NUMA結構下, 每個處理器CPU與一個本地內存直接相連, 而不同處理器之前則通過總線進行進一步的連接, 因此相對於任何一個CPU訪問本地內存的速度比訪問遠程內存的速度要快

Linux適用於各種不同的體系結構, 而不同體系結構在內存管理方面的差別很大. 因此linux內核需要用一種體系結構無關的方式來表示內存.

因此linux內核把物理內存按照CPU節點劃分為不同的node, 每個node作為某個cpu結點的本地內存, 而作為其他CPU節點的遠程內存, 而UMA結構下, 則任務系統中只存在一個內存node, 這樣對於UMA結構來說, 內核把內存當成只有一個內存node節點的偽NUMA

1.2 內存結點的概念

CPU被劃分為多個節點(node), 內存則被分簇, 每個CPU對應一個本地物理內存, 即一個CPU-node對應一個內存簇bank,即每個內存簇被認為是一個節點

系統的物理內存被劃分為幾個節點(node), 一個node對應一個內存簇bank,即每個內存簇被認為是一個節點

內存被劃分為結點. 每個節點關聯到系統中的一個處理器, 內核中表示為pg_data_t的實例. 系統中每個節點被鏈接到一個以NULL結尾的pgdat_list鏈表中<而其中的每個節點利用pg_data_tnode_next字段鏈接到下一節.而對於PC這種UMA結構的機器來說, 只使用了一個成為contig_page_data

的靜態pg_data_t結構.

內存中的每個節點都是由pg_data_t描述,而pg_data_t由struct pglist_data定義而來, 該數據結構定義在include/linux/mmzone.h, line 615

在分配一個頁面時, Linux采用節點局部分配的策略, 從最靠近運行中的CPU的節點分配內存, 由於進程往往是在同一個CPU上運行, 因此從當前節點得到的內存很可能被用到

1.3 pg_data_t描述內存節點

表示node的數據結構為typedef struct pglist_data pg_data_t, 這個結構定義在include/linux/mmzone.h, line 615中,結構體的內容如下:

/*
 * The pg_data_t structure is used in machines with CONFIG_DISCONTIGMEM
 * (mostly NUMA machines?) to denote a higher-level memory zone than the
 * zone denotes.
 *
 * On NUMA machines, each NUMA node would have a pg_data_t to describe
 * it's memory layout.
 *
 * Memory statistics and page replacement data structures are maintained on a
 * per-zone basis.
 */
struct bootmem_data;
typedef struct pglist_data {
    /*  包含了結點中各內存域的數據結構 , 可能的區域類型用zone_type表示*/
    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;      /*  指向page實例數組的指針,用於描述結點的所有物理內存頁,它包含了結點中所有內存域的頁。    */
#ifdef CONFIG_PAGE_EXTENSION
    struct page_ext *node_page_ext;
#endif
#endif
#ifndef CONFIG_NO_BOOTMEM
       /*  在系統啟動boot期間,內存管理子系統初始化之前,
       內核頁需要使用內存(另外,還需要保留部分內存用於初始化內存管理子系統)
       為解決這個問題,內核使用了自舉內存分配器 
       此結構用於這個階段的內存管理  */
    struct bootmem_data *bdata;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
    /*
     * Must be held any time you expect node_start_pfn, node_present_pages
     * or node_spanned_pages stay constant.  Holding this will also
     * guarantee that any pfn_valid() stays that way.
     *
     * pgdat_resize_lock() and pgdat_resize_unlock() are provided to
     * manipulate node_size_lock without checking for CONFIG_MEMORY_HOTPLUG.
     *
     * Nests above zone->lock and zone->span_seqlock
     * 當系統支持內存熱插撥時,用於保護本結構中的與節點大小相關的字段。
     * 哪調用node_start_pfn,node_present_pages,node_spanned_pages相關的代碼時,需要使用該鎖。
     */
    spinlock_t node_size_lock;
#endif
    /* /*起始頁面幀號,指出該節點在全局mem_map中的偏移
    系統中所有的頁幀是依次編號的,每個頁幀的號碼都是全局唯一的(不只是結點內唯一)  */
    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;        /*  全局結點ID,系統中的NUMA結點都從0開始編號  */
    wait_queue_head_t kswapd_wait;      /*  交換守護進程的等待隊列,
    在將頁幀換出結點時會用到。後面的文章會詳細討論。    */
    wait_queue_head_t pfmemalloc_wait;
    struct task_struct *kswapd;     /* Protected by  mem_hotplug_begin/end() 指向負責該結點的交換守護進程的task_struct。   */
    int kswapd_max_order;                       /*  定義需要釋放的區域的長度  */
    enum zone_type classzone_idx;

#ifdef CONFIG_COMPACTION
    int kcompactd_max_order;
    enum zone_type kcompactd_classzone_idx;
    wait_queue_head_t kcompactd_wait;
    struct task_struct *kcompactd;
#endif

#ifdef CONFIG_NUMA_BALANCING
    /* Lock serializing the migrate rate limiting window */
    spinlock_t numabalancing_migrate_lock;

    /* Rate limiting time interval */
    unsigned long numabalancing_migrate_next_window;

    /* Number of pages migrated during the rate limiting time interval */
    unsigned long numabalancing_migrate_nr_pages;
#endif

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
    /*
     * If memory initialisation on large machines is deferred then this
     * is the first PFN that needs to be initialised.
     */
    unsigned long first_deferred_pfn;
#endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */

#ifdef CONFIG_TRANSPARENT_HUGEPAGE
    spinlock_t split_queue_lock;
    struct list_head split_queue;
    unsigned long split_queue_len;
#endif
} pg_data_t;
字段 描述
node_zones 每個Node劃分為不同的zone,分別為ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM
node_zonelists 這個是備用節點及其內存域的列表,當當前節點的內存不夠分配時,會選取訪問代價最低的內存進行分配。分配內存操作時的區域順序,當調用free_area_init_core()時,由mm/page_alloc.c文件中的build_zonelists()函數設置
nr_zones 當前節點中不同內存域zone的數量,1到3個之間。並不是所有的node都有3個zone的,比如一個CPU簇就可能沒有ZONE_DMA區域
node_mem_map node中的第一個page,它可以指向mem_map中的任何一個page,指向page實例數組的指針,用於描述該節點所擁有的的物理內存頁,它包含了該頁面所有的內存頁,被放置在全局mem_map數組中
bdata 這個僅用於引導程序boot 的內存分配,內存在啟動時,也需要使用內存,在這裏內存使用了自舉內存分配器,這裏bdata是指向內存自舉分配器的數據結構的實例
node_start_pfn pfn是page frame number的縮寫。這個成員是用於表示node中的開始那個page在物理內存中的位置的。是當前NUMA節點的第一個頁幀的編號,系統中所有的頁幀是依次進行編號的,這個字段代表的是當前節點的頁幀的起始值,對於UMA系統,只有一個節點,所以該值總是0
node_present_pages node中的真正可以使用的page數量
node_present_pages node中的真正可以使用的page數量
node_spanned_pages 該節點以頁幀為單位的總長度,這個不等於前面的node_present_pages,因為這裏面包含空洞內存
node_id node的NODE ID 當前節點在系統中的編號,從0開始
kswapd_wait node的等待隊列,交換守護列隊進程的等待列表
kswapd_max_order 需要釋放的區域的長度,以頁階為單位
classzone_idx 這個字段暫時沒弄明白,不過其中的zone_type是對ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGH,ZONE_MOVABLE,__MAX_NR_ZONES的枚舉

1.4 結點的內存管理域

typedef struct pglist_data {
    /*  包含了結點中各內存域的數據結構 , 可能的區域類型用zone_type表示*/
    struct zone node_zones[MAX_NR_ZONES];
    /*  指點了備用結點及其內存域的列表,以便在當前結點沒有可用空間時,在備用結點分配內存   */
    struct zonelist node_zonelists[MAX_ZONELISTS];
    int nr_zones;                                   /*  保存結點中不同內存域的數目    */

} pg_data_t;
  • node_zones[MAX_NR_ZONES]數組保存了節點中各個內存域的數據結構,

  • 而node_zonelist則指定了備用節點以及其內存域的列表, 以便在當前結點沒有可用空間時, 在備用節點分配內存.

  • nr_zones存儲了結點中不同內存域的數目

1.5 結點的內存頁面

typedef struct pglist_data
{
    struct page *node_mem_map;      /*  指向page實例數組的指針,用於描述結點的所有物理內存頁,它包含了結點中所有內存域的頁。    */

    /* /*起始頁面幀號,指出該節點在全局mem_map中的偏移
    系統中所有的頁幀是依次編號的,每個頁幀的號碼都是全局唯一的(不只是結點內唯一)  */
    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;        /*  全局結點ID,系統中的NUMA結點都從0開始編號  */
} pg_data_t;
  • 其中node_mem_map是指向頁面page實例數組的指針, 用於描述結點的所有物理內存頁. 它包含了結點中所有內存域的頁.

  • node_start_pfn是該NUMA結點的第一個頁幀的邏輯編號. 系統中所有的節點的頁幀是一次編號的, 每個頁幀的編號是全局唯一的. node_start_pfn在UMA系統中總是0, 因為系統中只有一個內存結點, 因此其第一個頁幀編號總是0.

  • node_present_pages指定了結點中頁幀的數目, 而node_spanned_pages則給出了該結點以頁幀為單位計算的長度. 二者的值不一定相同, 因為結點中可能有一些空洞, 並不對應真正的頁幀.

1.6 交換守護進程

typedef struct pglist_data
{
    wait_queue_head_t kswapd_wait;      /*  交換守護進程的等待隊列,
    在將頁幀換出結點時會用到。後面的文章會詳細討論。    */
    wait_queue_head_t pfmemalloc_wait;
    struct task_struct *kswapd;     /* Protected by  mem_hotplug_begin/end() 指向負責該結點的交換守護進程的task_struct。   */
};
  • kswapd指向了負責將該結點的交換守護進程的task_struct. 在將頁幀換出結點時會喚醒該進程.

  • kswap_wait是交換守護進程(swap daemon)的等待隊列

  • 而kswapd_max_order用於頁交換子系統的實現, 用來定義需要釋放的區域的長度.

2 結點狀態

2.1 結點狀態標識node_states

內核用enum node_state變量標記了內存結點所有可能的狀態信息, 其定義在include/linux/nodemask.h?v=4.7, line 381

enum node_states {
    N_POSSIBLE,         /* The node could become online at some point 
                         結點在某個時候可能變成聯機*/
    N_ONLINE,           /* The node is online 
                        節點是聯機的*/
    N_NORMAL_MEMORY,    /* The node has regular memory
                            結點是普通內存域 */
#ifdef CONFIG_HIGHMEM
    N_HIGH_MEMORY,      /* The node has regular or high memory 
                           結點是普通或者高端內存域*/
#else
    N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
#ifdef CONFIG_MOVABLE_NODE
    N_MEMORY,           /* The node has memory(regular, high, movable) */
#else
    N_MEMORY = N_HIGH_MEMORY,
#endif
    N_CPU,      /* The node has one or more cpus */
    NR_NODE_STATES
};
狀態 描述
N_POSSIBLE 結點在某個時候可能變成聯機
N_ONLINE 節點是聯機的
N_NORMAL_MEMORY 結點是普通內存域
N_HIGH_MEMORY 結點是普通或者高端內存域
N_MEMORY 結點是普通,高端內存或者MOVEABLE域
N_CPU 結點有一個或多個CPU

其中N_POSSIBLE, N_ONLINE和N_CPU用於CPU和內存的熱插拔.

對內存管理有必要的標誌是N_HIGH_MEMORY和N_NORMAL_MEMORY, 如果結點有普通或高端內存則使用N_HIGH_MEMORY, 僅當結點沒有高端內存時才設置N_NORMAL_MEMORY

N_NORMAL_MEMORY,    /* The node has regular memory
                            結點是普通內存域 */
#ifdef CONFIG_HIGHMEM
    N_HIGH_MEMORY,      /* The node has regular or high memory 
                           結點是高端內存域*/
#else
    /*  沒有高端內存域, 仍設置N_NORMAL_MEMORY  */
    N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif

同樣ZONE_MOVABLE內存域同樣用類似的方法設置, 僅當系統中存在ZONE_MOVABLE內存域內存域(配置了CONFIG_MOVABLE_NODE參數)時, N_MEMORY才被設定, 否則則被設定成N_HIGH_MEMORY, 而N_HIGH_MEMORY設定與否同樣依賴於參數CONFIG_HIGHMEM的設定

#ifdef CONFIG_MOVABLE_NODE
    N_MEMORY,           /* The node has memory(regular, high, movable) */
#else
    N_MEMORY = N_HIGH_MEMORY,
#endif

2.2 結點狀態設置函數

內核提供了輔助函數來設置或者清楚位域活特定結點的一個比特位

static inline int node_state(int node, enum node_states state)
static inline void node_set_state(int node, enum node_states state)
static inline void node_clear_state(int node, enum node_states state)
static inline int num_node_state(enum node_states state)

此外宏for_each_node_state(__node, __state)用來叠代處於特定狀態的所有結點,

#define for_each_node_state(__node, __state)         for_each_node_mask((__node), node_states[__state])

而for_each_online_node(node)則負責叠代所有的活動結點.

如果內核編譯只支持當個結點(即使用平坦內存模型), 則沒有結點位圖, 上述操作該位圖的函數則變成空操作, 其定義形式如下, 參見include/linux/nodemask.h?v=4.7, line 406

參見內核

#if MAX_NUMNODES > 1
    /*   some real function  */
#else
    /*  some NULL function  */
#endif

3 查找內存結點

node_id作為全局節點id。 系統中的NUMA結點都是從0開始編號的

3.1 linux-2.4中的實現

pgdat_next指針域和pgdat_list內存結點鏈表

而對於NUMA結構的系統中, 在linux-2.4.x之前的內核中所有的節點,內存結點pg_data_t都有一個next指針域pgdat_next指向下一個內存結點. 這樣一來系統中所有結點都通過單鏈表pgdat_list鏈接起來, 其末尾是一個NULL指針標記.

這些節點都放在該鏈表中,均由函數init_bootmem_core()初始化結點

那麽內核提供了宏函數for_each_pgdat(pgdat)來遍歷node節點, 其只需要沿著node_next以此便立即可, 參照include/linux/mmzone.h?v=2.4.37, line 187

/**
 * for_each_pgdat - helper macro to iterate over nodes
 * @pgdat - pg_data_t * variable
 * Meant to help with common loops of the form
 * pgdat = pgdat_list;
 * while(pgdat) {
 *      ...
 *      pgdat = pgdat->node_next;
 * }
 */
#define for_each_pgdat(pgdat)         for (pgdat = pgdat_list; pgdat; pgdat = pgdat->node_next)

3.2 linux-3.x~4.x的實現

node_data內存節點數組

在新的linux3.x~linux4.x的內核中,內核移除了pg_data_t的pgdat_next之指針域, 同時也刪除了pgdat_list鏈表, 參見Remove pgdat listRemove pgdat list ver.2

但是定義了一個大小為MAX_NUMNODES類型為pg_data_t數組node_data,數組的大小根據CONFIG_NODES_SHIFT的配置決定. 對於UMA來說,NODES_SHIFT為0,所以MAX_NUMNODES的值為1.

for_each_online_pgdat遍歷所有的內存結點

內核提供了for_each_online_pgdatfor_each_online_pgdat(pgdat)來遍歷節點

/**
 * for_each_online_pgdat - helper macro to iterate over all online nodes
 * @pgdat - pointer to a pg_data_t variable
 */
#define for_each_online_pgdat(pgdat)                            for (pgdat = first_online_pgdat();                           pgdat;                                                  pgdat = next_online_pgdat(pgdat))

其中first_online_pgdat可以查找到系統中第一個內存節點的pg_data_t信息, next_online_pgdat則查找下一個內存節點.

下面我們來看看first_online_pgdat和next_online_pgdat是怎麽實現的.

first_online_node和next_online_node返回結點編號

由於沒了next指針域pgdat_next和全局node鏈表pgdat_list, 因而內核提供了first_online_node指向第一個內存結點, 而通過next_online_node來查找其下一個結點, 他們是通過狀態node_states的位圖來查找結點信息的, 定義在include/linux/nodemask.h?v4.7, line 432

//  http://lxr.free-electrons.com/source/include/linux/nodemask.h?v4.7#L432
#define first_online_node       first_node(node_states[N_ONLINE])
#define first_memory_node       first_node(node_states[N_MEMORY])
static inline int next_online_node(int nid)
{
    return next_node(nid, node_states[N_ONLINE]);
}

first_online_node和next_online_node返回所查找的node結點的編號, 而有了編號, 我們直接去node_data數組中按照編號進行索引即可去除對應的pg_data_t的信息.內核提供了NODE_DATA(node_id)宏函數來按照編號來查找對應的結點, 它的工作其實其實就是從node_data數組中進行索引

NODE_DATA(node_id)查找編號node_id的結點pg_data_t信息

移除了pg_data_t->pgdat_next指針域. 但是所有的node都存儲在node_data數組中, 內核提供了函數NODE_DATA直接通過node編號索引節點pg_data_t信息, 參見NODE_DATA的定義

extern struct pglist_data *node_data[];
#define NODE_DATA(nid)          (node_data[(nid)])

在UMA結構的機器中, 只有一個node結點即contig_page_data, 此時NODE_DATA直接指向了全局的contig_page_data, 而與node的編號nid無關, 參照include/linux/mmzone.h?v=4.7, line 858, 其中全局唯一的內存node結點contig_page_data定義在mm/nobootmem.c?v=4.7, line 27, linux-2.4.37

#ifndef CONFIG_NEED_MULTIPLE_NODES
extern struct pglist_data contig_page_data;
#define NODE_DATA(nid)          (&contig_page_data)
#define NODE_MEM_MAP(nid)       mem_map
else
/*  ......  */
#endif

first_online_pgdat和next_online_pgdat返回結點的pg_data_t

  • 首先通過first_online_node和next_online_node找到節點的編號
  • 然後通過NODE_DATA(node_id)查找到對應編號的結點的pg_data_t信息
struct pglist_data *first_online_pgdat(void)
{
        return NODE_DATA(first_online_node);
}

struct pglist_data *next_online_pgdat(struct pglist_data *pgdat)
{
    int nid = next_online_node(pgdat->node_id);

    if (nid == MAX_NUMNODES)
        return NULL;
    return NODE_DATA(nid);
}

Linux內存描述之內存節點node--Linux內存管理(二)