內存管理

13. 內存管理 html

13.1. 引言 node

Linux對物理內存的描述機制有兩種:UMANUMALinux把物理內存劃分爲三個層次來管理:存儲節點(Node)、管理區(Zone)和頁面 Page)。UMA對應一致存儲結構,它只須要一個Node就能夠描述當前系統中的物理內存,可是NUMA的出現打破了這種平靜,此時須要多個 Node,它們被統必定義爲一個名爲discontig_node_data的數組。爲了和UMA兼容,就將描述UMA存儲結構的描述符 contig_page_data放到該數組的第一個元素中。內核配置選項CONFIG_NUMA決定了當前系統是否支持NUMA機制。此時不管UMA NUMA,它們都是對應到一個類型爲pg_data_t的數組中,便於統一管理。 linux

 71. Node ZonePage的關係 算法

 

上圖描述Linux管理物理內存的三個層次之間的拓撲關係。從圖中能夠看出一個存儲節點由pg_data_t描述,一個UMA系統中只有一個Node,而 NUMA中則能夠存在多個Node。它由CONFIG_NODES_SHIFT配置選項決定,它是CONFIG_NUMA的子選項,因此只有配置了 CONFIG_NUMA,該選項才起做用。UMA狀況下,NODES_SHIFT被定義爲0MAX_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;

  • node_zones:當前節點中包含的最大管理區數。MAX_NR_ZONESinclude/linux/bounds.h定義,該文件是在編譯過程當中根據管理區類型定義中的__MAX_NR_ZONES變量自動生成的。
  • node_zonelists 內存分配器所使用的管理區鏈表數組,MAX_ZONELISTS的值在配置CONFIG_NUMA時爲2,不然爲1。索引爲0的鏈表表示後援 (Fallback)鏈表,也即當該鏈表中的第一個不知足分配內存時,依次嘗試鏈表的其餘管理區。索引爲1,的鏈表則用來針對GFP_THISNODE 內存申請,此時只能申請指定的該鏈表中的管理區。
  • nr_zones:指定當前節點中的管理區數,也即node_zones中實際用到的管理區數。它的取值範圍爲[1, MAX_NR_ZONES]。對於UMA來講,它的值爲1
  • node_mem_map:節點中頁描述符數組首地址。
  • node_page_cgroup
  • bdata:系統引導時用的Bootmem分配器。
  • node_start_pfn:節點中第一個頁框的下標。
  • node_present_pages:節點中的頁面數,不包含孔洞。
  • node_spanned_pages:節點中的頁面總數,包含孔洞。
  • node_id:節點標識符,在節點數組中惟一存在。
  • kswapd_waitkswapd頁換出守護進程使用的等待隊列。
  • kswapd: 指針指向kswaps內核線程的進程描述符。
  • kswapd_max_orderkswapd將要建立的空閒塊大小取對數的值。

注意到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體系結構的兩種 硬件約束:

  • ISA總線的直接內存存取DMA訪問控制器只能對RAM的低16MB尋址。
  • 在具備大容量RAM的現代32位計算機中,因爲線性地址空間的限制,CPU不能直接訪問全部的物理內存。

最後一種限制不只存在於80x86,而存在於全部的體系結構中。爲了應對這兩種限制,Linux把每一個內存節點的物理內存劃分爲多個(一般爲3個)管理區(zone)。在80x86 UMA體系結構中的管理區爲:

  • ZONE_DMA,包含低於16MB的內存頁框。
  • ZONE_NORMAL,包含高於16MB且低於896MB的內存頁框。
  • ZONE_HIGHMEM,包含從896MB開始的內存頁框。

對於ARM來講,ZONE_HIGHMEM被名爲ZONE_MOVABLE的宏取代,而ZONE_DMA也不會僅限於最低的16MB,而可能對應全部的內存區域,此時只有內存節點ZONE_DMA有效,因此ZONE_DMA並不必定名副其實的用來做爲DMA訪問之用。

ZONE_DMAZONE_NORMAL區包含內存的"常規"頁框,經過把它們線性的映射到線性地址的第4 GB0xc0000000-0xcfffffff),內核就能夠直接訪問。相反ZONE_HIGHMEM或者ZONE_MOVABLE區包含的內存頁不 能由內核直接訪問,儘管它們也線性地映射到了線性地址空間的第4GB。每一個內存管理區都有本身的描述符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];

};

  • pages_min,記錄管理區中空閒頁的數目。
  • pages_low,回收頁框使用的下屆,同時也被管理區分配器做爲閾值使用。
  • pages_high,回收頁框使用的上屆,同時也被管理區分配器做爲閾值使用。
  • lowmem_reserve,指明在處理內存不足的臨界狀況下每一個管理區必須保留的頁框數目。
  • pageset,單一頁框的特殊告訴緩存。

在申請內存時,會遇到兩種狀況:若是有足夠的空閒頁可用,請求就會被馬上知足;不然,必須回收一些內存,而且將發出請求的內核控制路徑阻塞,直到有內存被 釋放。不過有些內存請求不能被阻塞。這種狀況發生在處理中斷或在執行臨界區內的代碼時。在這些狀況下,一條內核控制路徑應使用原子內存分配請求 (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_DMAZONE_NORMAL內存管理區的可用頁框數決定,這能夠 經過nr_free_buffer_pages獲取。儘管能夠經過/proc/sys/vm/min_free_kbytes來修改該它的大小,可是 min_free_kbytes的初始值範圍必須是[128K, 64M]。管理區描述符中的pages_min成員存儲了管理區內保留頁框的數目。這個字段與pages_lowpages_high字段一塊兒被用在內 存分配和回收算法中。pages_low字段老是被設爲pages_min的值的5/4,而pages_high則老是被設爲pages_min的值的3 /2。這些值在模塊快初始化module_init調用的init_per_zone_pages_min中被設置。

 27. 頁面分配控制

名稱

大小

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;

13.2. page管理項

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結構來進行管理:記錄分配狀態,分配和回收,互斥以及同步操做。對該結構成員的解釋以下:

  • flag域存放當前頁框的頁標誌,它存儲了體系結構無關的狀態,專門供Linux內核自身使用。該標誌可能的值定義在include/linux/page-flags.h中。
  • 原子計數成員_count則指明瞭當前頁框的引用計數,當該值爲0時,就說明它沒有被使用,此時在新分配內存時它就能夠被使用。內核代碼應該經過page_count來訪問它,而非直接訪問。
  • 原子計數成員_mapcount表示在頁表中有多少頁指向該頁框。在SLUB中它被inuseobjects代替。

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)

 28. 頁標誌宏函數

擴展函數/

用途

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
SETPAGEFLAG
CLEARPAGEFLAG

當於同時擴展了三個宏,也即三個函數

PAGEFLAG_FALSE(uname)

Page##uname

永遠返回0

TESTSCFLAG(uname, lname)

TESTSETFLAG
TESTCLEARFLAG

當於同時擴展了兩個宏,也即兩個函數

SETPAGEFLAG_NOOP(uname)

SetPage##uname

空操做

CLEARPAGEFLAG_NOOP(uname)

ClearPage##unam

空操做

__CLEARPAGEFLAG_NOOP(uname)

__ClearPage##uname

空操做

TESTCLEARFLAG_FALSE(uname)

TestClearPage##uname

永遠返回0

[a] 以上三個宏分別對應test_bitset_bitclear_bit,是原子操做,與它們對應的是有三個開頭
爲下劃線的同名函數__SETPAGEFLAG等與它們相對應,但不是原子操做,這裏再也不列出。

[b] 與此對應也有__PAGEFLAG的宏存在。

 


flags
實際上爲兩部分:標誌區(Flags Area)從最低處向上擴展到第__NR_PAGEFLAGS;字段區(Fields Area)則從最高位向低位擴展。字段區用來實現管理區,內存節點和稀疏內存的映射。

| FIELD | ... | FLAGS |

N-1 ^ 0

(__NR_PAGEFLAGS)

_count引用計數不該被直接引用,內核提供了一系列的內聯函數來操做它,一般它們被定義在include/linux/mm.h中。

 29. 頁引用計數函數

函數名

用途

page_count

讀取引用計數

get_page

引用計數加1

init_page_count

初始化引用計數爲1


_mapcount
_count引用計數相似,不該被直接引用,內核提供了一系列的內聯函數來操做它,它們也被定義在include/linux/mm.h中。

 30. 頁引用計數函數

函數名

用途

reset_page_mapcount

初始化引用計數爲-1[a]

page_mapcount

讀取引用計數並加1的值

page_mapped

該函數根據引用計數值是否大於等於0,判斷該頁框是否被映射。

[a] 沒有初始化爲0是由於atomic_inc_and_testatomic_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指針和複合頁的首頁指針與privatemapping公用存儲空間。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,可是它並老是如此。能夠有如下兩種可能:

  • address_space的實例
  • 當最低位爲1時,指向anon_vma的實例,此時完成匿名頁的逆向映射。

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

13.3. bootmem_free_node

bootmem_init_node函數中,根據struct meminfo參數來初始化內存節點對應的pg_data_t數據結構contig_page_data,而且申請Bootmem機制使用的 bitmap。從該函數使用的參數來看,一個內存節點對應一個struct meminfo的內存塊信息。因此一個內存節點有可能對應多個membank,而這些membank的物理地址多是不連續的,這就是內存孔洞的存在的原 因。contig_page_data成員中的bdata->node_min_pfnbdata->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_nodebootmem_init_node參數相似,它用來初始化特定內存節點的管理區信息。

arch/arm/mm/init.c

static void __init bootmem_free_node(int node, struct meminfo *mi);

  • 儘管局部zone_sizezhole_size聲明爲大小爲MAX_NR_ZONES的數組,可是隻用到了其中的第一個元素。這是因爲ARM Linux採用了UMA方式的內存管理機制。
  • zone_size[0]被賦值爲end_pfn - start_pfn,而後根據zone_size減去meminfo中每一個membank中真正的size獲得內存孔洞的大小zhole_size[0]
  • 經過arch_adjust_zones爲特定架構的系統預留內存。一般用它來爲特定的限制的DMA尋址預留內存,將這些DMA沒法訪問的內存放入zone[1],而DMA對應zone[0],一般DMA能夠尋址全部內存。
  • 最後調用free_area_init_node初始化節點對應的pg_data_t描述符信息,而且爲每一個頁表分配struct page結構。

#define arch_adjust_zones(node,size,holes) do { } while (0)

 72. bootmem_free_node調用流程

 

13.4. free_area_init_node

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的函數參數解釋以下:

  • nid,節點ID號。
  • zones_size,大小爲MAX_NR_ZONES的數組,用來記錄當前內存節點中的內存頁框數,包含孔洞。
  • node_start_pfn,當前內存節點中的起始內存頁框。
  • zholes_size,大小爲MAX_NR_ZONES的數組,用來記錄當前內存節點中的內存孔洞頁框數。

free_area_init_node完成如下功能:

  • 根據參數nid,肯定該節點對應的pgdat,並初始化成員node_id = nid
  • pgdat->node_start_pfn = node_start_pfn
  • 經過calculate_node_totalpages函數,計算pgdat->node_spanned_pages(包含孔洞)pgdat->node_present_pages(不含孔洞)
  • 每個物理頁框對應一個struct page結構,經過alloc_node_mem_map爲全部的物理頁面分配該結構體空間,並將起始頁框地址保存在pgdat->node_mem_map中。
  • 調用free_area_init_core,用來初始化內存管理區zone

13.5. free_area_init_core

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的函數參數解釋以下:

  • pgdat,內存節點對應的pgdat_t類型描述符。
  • zones_size,大小爲MAX_NR_ZONES的數組,用來記錄當前內存節點中的內存頁框數,包含孔洞。
  • zholes_size,大小爲MAX_NR_ZONES的數組,用來記錄當前內存節點中的內存孔洞頁框數。

free_area_init_core針對單個內存節點內的全部管理區進行初始化,並計算管理內存頁所用的struct page數組佔用的memmap_pages

  • 經過pgdat_resize_init函數初始化pgdat自旋鎖成員node_size_lock,它與CONFIG_MEMORY_HOTPLUG(內存熱插拔)有關。
  • 初始化pgdat->nr_zones0
  • 經過init_waitqueue_head函數初始化pgdat->kswapd_wait,它是kswapd頁換出守護進程使用的等待隊列。
  • 初始化pgdat->kswapd_max_order0
  • 經過pgdat_page_cgroup_init函數初始化pgdat->node_page_cgroupNULL,若是沒有打開CONFIG_CGROUP_MEM_RES_CTLR選項,則爲空函數。

接下來遍歷內存節點中的全部管理區,完成如下工做:

  • 計算含有孔洞的頁面總數存入size,同時zone->spanned_pages記錄該值。
  • 計算不含孔洞的頁面總數存入realsize
  • size變量計算頁面數組所佔用的頁面數,存入memmap_pages。之因此不使用realsize,是由於在經過 alloc_node_mem_map函數來分配頁面管理數組時採用的含有孔洞的頁面數,這是爲了管理方便,可是在有大量孔洞的內存節點中,這樣會浪費大 struct page頁面管理結構,因此一般會使能內存的CONFIG_DISCONTIGMEM選項。
  • 若是處理管理區是DMA區,那麼將在realsize中再次爲DMA預留內存。也即realsize再次減去dma_reserve
  • realsize減去頁面映射使用的頁面大小memmap_pages並存入zone->present_pages
  • 經過is_highmem_idx判斷當前內存區是否爲高端內存,若是不是,那麼將realsize計入內核全局統計信息nr_kernel_pages,它描述了內核全部能夠一一映射的頁。
  • realsize計入nr_all_pages,與nr_kernel_pages相似,它還記錄了高端內存頁。
  • 若是定義了CONFIG_NUMA,則初始化管理區中的nodemin_unmapped_pagesmin_slab_pages成員。
  • zone->name賦值,它指向zone_names數組中對應的當前管理區的值
  • 使用spin_lock_init初始化管理區中的locklru_lock自旋鎖。
  • 若是配置了CONFIG_MEMORY_HOTPLUG,那麼初始化自旋鎖span_seqlock。與locklru_lock不一樣,它經過seqlock_init函數完成初始化。
  • 設置prev_priorityDEF_PRIORITY
  • 初始化管理區中的回調指針zone_pgdat,顯然它指向該區所屬的內存節點類型pgdat指針。
  • zone_pcp_init初始化管理區的per-CPU緩存。
  • 初始化lru成員。
  • 初始化recent_rotatedrecent_scanned0
  • 經過函數zap_zone_vm_stats初始化vm_stat成員爲0
  • 初始化flags成員爲0
  • 若是打開了CONFIG_HUGETLB_PAGE_SIZE_VARIABLE選項,則經過pageblock_default_order函數獲取默認值並設置給全局變量pageblock_order,不然默認值爲MAX_ORDER-1。它被用在夥伴系統中。
  • setup_usemap設置pageblock_flagsNULL。若是該區包含的頁框數知足要求,那麼爲pageblock_flags分配內存並初始化爲0pageblock_flags與夥伴系統的碎片遷移算法有關。
  • init_currently_empty_zone初始化夥伴系統的free_area列表。
  • 最後經過memmap_init宏間接引用函數memmap_init_zone將屬於該管理區的全部page數組都設置爲初始默認值。
  • zone_start_pfn記錄下一循環處理的管理區的開始頁框地址。

13.6. memmap_init_zone

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_initmemmap_init_zone顧名思義,它是針對單個管理區對應的page數組來初始化的。

  • size指明瞭管理區的頁幀數,它包含孔洞。
  • nid是當前管理區所屬的內存節點的編號。
  • zone指明瞭當前管理區在內存節點中node_zones數組下標。
  • zone_start_pfn則提供了當前管理區的第一個頁幀的編號。
  • context是爲了指明當前是在系統初始化階段,仍是熱插拔階段對內存管理頁的初始化。它只有兩個值:MEMMAP_EARLYMEMMAP_HOTPLUG

memmap_init_zone依次完成了如下功能:

  • 經過end_pfn = start_pfn + size獲得終止頁幀,而後從start_pfnend_pfn經過循環一次處理它們對應的struct page
  • 若是context指定的系統狀態是MEMMAP_EARLY,則須要判斷當前頁幀是否存在,這是由於內存孔洞的存在。[10]
  • 根據公式page = pfn_to_page(pfn),由頁幀獲得它對應的struct page管理項。
  • pageflags成員的Field Area,它由段區,管理區和節點區三部分組成,分別佔用的位數由SECTIONS_WIDTHZONES_WIDTHNODES_WIDTH分別表 示。set_page_links函數的做用就是分別經過set_page_zoneset_page_nodeset_page_section 數來設置這些字段區。之後就能夠根據這些區域獲取當前頁幀的位置信息。
  • mminit_verify_page_links用來驗證set_page_links設置的信息是否正確。
  • 經過init_page_countpage->_count成員初始化爲1
  • 經過reset_page_mapcountpage->_mapcount成員初始化爲-1
  • 經過由宏定義展開後的函數SetPageReserved設置PG_reserved標記到page->flags中。
  • 設置全部頁面均爲MIGRATE_MOVABLE的。
  • 初始化page->lru
  • 若是配置了WANT_PAGE_VIRTUAL,且不爲高端內存則初始化virtual成員。好比SPARC系統。

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_mapalloc_node_mem_map中被賦值爲node_mem_map,也即管理數組的首地址。

pageflags成員的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 |

*/

13.7. build_all_zonelists

build_all_zonelistsinit/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依次完成如下功能:

  • system_state查看系統的運行狀態,在內核啓動階段,它的值保持爲0,也即SYSTEM_BOOTING,只有在start_kernel 行到最後一個函數rest_init後,纔會進入SYSTEM_RUNNING階段。若是爲內核啓動階段,那麼調用 __build_all_zonelists初始化分配器管理區鏈表,不然掛起系統,顯然內存管理功能初始化出現異常爲致命錯誤。
  • nr_free_pagecache_pages直接調用nr_free_zone_pages來統計系統中全部內存節點中可用的內存頁框數,一般就是對present_pages成員的疊加。
  • 據當前系統中的內存頁框數目,決定是否啓用流動分組(Mobility Grouping)機制,這種機制能夠在分配大內存塊時減小內存碎片。顯然只有內存足夠大時纔會啓用該功能,不然將得不償失。 pageblock_nr_pages其實是一個宏,它表示夥伴系統中的最高階頁塊所能包含的頁面數。

pageblock_orderfree_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調用了一些列函數,調用流程以下所示:

 73. 內存管理區初始化流程圖

 

13.8. __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_zonelistsdummy參數沒有任何意義,而且返回值永遠爲0,這是爲了方便stop_machine對其結果做爲參數引用。build_zonelist_cacheCONFIG_NUMA相關,它用來設置zlcache相關的成員。

static void build_zonelist_cache(pg_data_t *pgdat)

{

    pgdat->node_zonelists[0].zlcache_ptr = NULL;

}

13.9. build_zonelists

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被設置爲NULLzone_idx0,這是用來遍歷時判斷結尾。

13.10. build_zonelists_node

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 - 1nr_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區分配內存。

  • pgdat:當前存儲節點對應的pg_data_t類型描述符。
  • zonelist:當前存儲節點對應的管理區列表,一般它經過就是pgdat->node_zonelists + 0
  • nr_zones:當前開始處理的管理區在存儲節點中的編號,每處理一個管理區那麼該值加1
  • zone_type:指定處理幾個管理區。一般爲MAX_NR_ZONES - 1
  • 返回下一個未處理的管理區的索引值。

build_zonelists_node完成了如下功能,當未開啓CONFIG_NUMA時,依次映射管理區到參考鏈表中。

  • 斷定提供的zone_type參數是否正確,它不該該超過一個管理節點所能包含的最大管理區的個數。
  • populated_zone是一個簡單的宏:!!zone->present_pages,因爲present_pages參數在free_area_init_core 被初始化爲該節點中可用的內存頁數realsize,因此這裏的意圖就是保證當天節點是否已經被初始化。
  • zoneref_set_zone 設置管理節點中的成員zoneref,它記錄當前管理區的地址和在管理區數組node_zones中的索引。顯然在循環中,全部的初始化過的zone都是 依次從node_zones出來,放入_zonerefs的,這個順序是倒序的。build_zonelists_node函數跟是否配置 CONFIG_NUMA相關,啓用該功能將使用另外一個同名函數,此函數將根據Node的各個要素權衡它們在鏈表中的順序。
  • check_highest_zone函數只在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中的參考鏈表中。

相關文章
相關標籤/搜索