內存管理之5:物理內存管理中的抽象

date: 2014-09-16 19:09node

在軟件設計時,咱們通常要從需求中提取出抽象(類或者數據結構),而後圍繞這些抽象設計相關的算法。內存管理天然也不能例外,這兩節咱們來看看爲了管理爲了物理內存以及整個虛存空間,linux提取哪些抽象,提取這些抽象背後的動機是什麼?這些抽象之間的關聯是什麼?linux

注:本文展現的結構體定義來自2.6.24版本的內核。算法

1 最頂層——節點 Node

Node(內存節點)是由於NUMA的出現而產生的抽象。NUMA(Non Uniform Memory Access)即非一致性內存訪問,主要出如今大型機上。參考下圖,在這種系統中,每一個CPU都有本地內存,但也能夠經過總線訪問其餘CPU的本地內存;總線上還有一個公共的內存模塊,各CPU均可以經過系統總線訪問。顯然,對某個具體的CPU來講,其訪問本地內存的速度是最快的,而經過總線訪問其餘CPU本地內存的速度要慢,並且還可能面臨着競爭。可見,在這樣的系統中,雖然內存的地址是連續的,但「質地」卻不均勻,所以咱們須要將這些質地不均勻的內存分開管理,每個內存模塊稱之爲一個內存節點Node。數組

UMA-NUMA

與NUMA相對的是UMA即一致性內存訪問,在這種系統下,全部處理器對內存的訪問具備相同的速度,有就是說整個內存質地是均勻的,所以只對應一個內存節點Node。傳統的計算機中採用的就是UMA結構,本文主要分析UMA結構。緩存

Node對應的數據結構爲pglist_data,其定義以下(對定義進行了簡化,去掉宏開關中的內容):安全

<include/linux/mmzone.h>
    typedef struct pglist_data {
    	struct zone node_zones[MAX_NR_ZONES];
    	struct zonelist node_zonelists[MAX_ZONELISTS];
    	int nr_zones;
    	struct page *node_mem_map;
    	struct bootmem_data *bdata;		   /*自舉內存分配器*/
    	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;

給你一整塊地,你該怎麼打理?固然是將整塊地劃分紅不一樣的區域,某個區域用來蓋房子,某個區域用來種農做物,其餘的區域用來種經濟做物。這裏也同樣,內存節點將整個內存再細分爲區域Zone,結構體成員node_zones就表示內存節點所劃分的內存區域,而nr_zones指示所分區域的數量。通常分爲三個區:ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM,每一個區域都有其特殊用途。ZONE_DMA在低端地址處,該區域主要是爲ISA(Industry Standard Architecture)設備而設置;ZONE_NORMAL區域的物理內存經過某個固定的偏移映射到內核的虛擬地址空間;若是內存空間超過1G,則還會有ZONE_HIGHMEM,設置該區域的目的主要是爲了使得內核能夠訪問1G之外的內存空間。在X86架構下,區域的劃分通常以下:數據結構

  • ZONE_DMA First —— 16MiB of memory
  • ZONE_NORMAL —— 16MiB - 896MiB
  • ZONE_HIGHMEM —— 896 MiB - End

當須要申請一個內存頁Page時,alloc_page的調用者通常會指定從哪一個區域分配,但若是該區域內存不夠該怎辦,這時就須要指定「備選」區域,能夠有多個備選區,優先級從高到底排列,表示當指定區域的內存不足時,優先從第一個備選區分配,若是第一個備選區仍然內存不足,則嘗試第二個備選區,依次類推,這就是所謂的分配策略。結構體成員node_zonelists就表示這種策略組。架構

一個內存節點最終管理的仍是物理內存的page,成員node_mem_map是一個page類型的指針,指向該內存節點全部的page數組。app

2 中間層——管理區Zone

在介紹節點Node時,咱們已經介紹了管理區zone的概念,管理區分爲以下幾種類型:async

enum zone_type {
    #ifdef CONFIG_ZONE_DMA
    	/* DMA內存域,通常爲16M,用於ISA */
    	ZONE_DMA,
    #endif
    #ifdef CONFIG_ZONE_DMA32
    	/* 在64位系統中,使用32位地址尋址、適合DMA的內存域 */
    	ZONE_DMA32,
    #endif
    	/* 可直接映射到內核地址空間的內存區域 */
    	ZONE_NORMAL,
    #ifdef CONFIG_HIGHMEM
    	/* 內核不能直接映射的內存域 */
    	ZONE_HIGHMEM,
    #endif
    	/* 可移動內存區域 */
    	ZONE_MOVABLE,
    	MAX_NR_ZONES
    };

管理區對應的結構體爲zone,定義在同一文件中。

/* 內存節點的內存域 */
    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];
    	spinlock_t		lock;
    	struct free_area	free_area[MAX_ORDER];
    	ZONE_PADDING(_pad1_)
    
    	/* Fields commonly accessed by the page reclaim scanner */
    	spinlock_t		lru_lock;	
    	struct list_head	active_list;
    	struct list_head	inactive_list;
    	unsigned long		nr_scan_active;
    	unsigned long		nr_scan_inactive;
    	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];
    	int prev_priority;
    	ZONE_PADDING(_pad2_)
        
    	/* Rarely used or read-mostly fields */
    	wait_queue_head_t	* wait_table;
    	unsigned long		wait_table_hash_nr_entries;
    	unsigned long		wait_table_bits;
    	struct pglist_data	*zone_pgdat;  /* 該內存域所屬的節點 */
    	/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
    	unsigned long		zone_start_pfn;  /* 該內存域第一個頁幀號 */
    	unsigned long		spanned_pages;  /* total size, including holes */
    	unsigned long		present_pages;  /* amount of memory (excluding holes) */
    
    	/*
    	 * rarely used fields:
    	 */	
    	const char		*name;  /* 內存域名稱 */
    } ____cacheline_internodealigned_in_smp;

2.1 padding

首先,該結構體被ZONE_PADDING「分割」成三部分,宏ZONE_PADDING的定義在同一個文件中:

/*
     * zone->lock and zone->lru_lock are two of the hottest locks in the kernel.
     * So add a wild amount of padding here to ensure that they fall into separate
     * cachelines.  There are very few zone structures in the machine, so space
     * consumption is not a concern here.
     */
    #if defined(CONFIG_SMP)
    struct zone_padding {
    	char x[0];
    } ____cacheline_internodealigned_in_smp;
    #define ZONE_PADDING(name)	struct zone_padding name;
    #else
    #define ZONE_PADDING(name)
    #endif

可見在SMP壞境下,ZONE_PADDING 定義了一個zone_padding類型的變量name;而zone_padding自己被定義成一個柔性數組,數組具體的大小會在編譯時決定,以保證此前的的部分按照cache line(通常爲16個字節)對齊。

爲何要按cache line進行對齊呢?ZONE_PADDING的註釋說得很清楚了:由於zone結構體會被頻繁訪問到,在多核環境下,會有多個CPU同時訪問同一個結構體,爲了不彼此間的干擾,必須對每次訪問進行加鎖。那麼是否是要對整個zone加鎖呢?一把鎖鎖住zone中全部的成員?這應該不是最有效的方法。咱們知道zone結構體很大,訪問zone最頻繁的有兩個兩個場景,一是頁面分配,二是頁面回收,這兩種場景各自訪問結構體zone中不一樣的成員。若是一把鎖鎖住zone中全部成員的話,當一個cpu正在處理頁面分配的事情,另外一個cpu想要進行頁面回收的處理,卻由於第一個cpu鎖住了zone中全部成員而只好等待。第二個CPU明知道此時能夠「安全的」進行回收處理(由於第一個CPU不會訪問與回收處理相關的成員)卻也只能乾着急。可見,咱們應該將鎖再細分。zone中定義了兩把鎖,lock以及lru_lock,lock與頁面分配有關,lru_lock與頁面回收有關。定義了兩把鎖,天然,每把鎖所控制的成員最好跟鎖呆在一塊兒,最好是跟鎖在同一個cache line中。當CPU對某個鎖進行上鎖處理時,CPU會將鎖從內存加載到cache中,由於是以cache line爲單位進行加載,因此與鎖緊挨着的成員也被加載到同一cache line中。CPU上完鎖想要訪問相關的成員時,這些成員已經在cache line中了。

增長padding,會額外佔用一些字節,但內核中zone結構體的實例並不會太多(UMA下只有三個),相比效率的提高,損失這點空間是值得的。

2.2 水位值

一個區域所包含的內存時有限的,若是任憑頁面分配程序「予取予求」的話,內存總有耗盡的時刻,等到耗盡時再作補救的話爲時已晚。因此咱們須要未雨綢繆,這內存耗盡以前實施補救措施,也就是喚醒守護進程kswapd來回收一些內存頁面。三個水位值pages_min、pages_low、pages_high與守護進程kswapd的「互動」以下圖:

zone的水位

關於lowmem_reserve成員,爲了不某些區域的內存分配殆盡(好比ZONE_NORMAL)而另外一些區域(好比ZONE_ HIGHMEM)卻內存充沛,「旱的旱死澇的澇死」,每一個區域都預留一些「壓箱底」的內存。

2.3 空閒內存

區域中到底有多少可用內存呢?由成員free_area來表示,這是一個struct free_area類型的數組,數組的尺寸MAX_ORDER的定義也在本文件中。若是沒有定義CONFIG_FORCE_MAX_ZONEORDER宏的話,MAX_ORDER的值將爲11。

/* Free memory management - zoned buddy allocator.  */
    #ifndef CONFIG_FORCE_MAX_ZONEORDER
    #define MAX_ORDER 11
    #else
    #define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
    #endif

爲何要將可用內存用一個數組來表示呢?這與夥伴系統有關。在內核中,內存管理的工做由夥伴系統來承擔,夥伴系統分配的都是2^n個連續的頁面。假定某個系統共有60個物理頁,系統運行一段時間後,物理頁面的分配情形以下:

分配60個頁面

左側的地址空間散佈着空閒頁,儘管空閒頁超過25%,但夥伴系統可以分配的連續頁面最大爲一頁,這就是內存碎片。注意這裏的內存碎片只對內核有意義,這種碎片對用戶空間沒有影響,由於用戶空間直接訪問的是虛擬地址,經過頁表映射到物理地址,即便物理空間不連續,其上的虛擬地址能夠是連續的。

若是咱們將空閒內存根據其連續頁面的大小按照2的n次冪來進行分組,n的取值在[0, MAX_ORDER]閉區間中,則能夠有效的減小內存碎片。若是須要分配連續8個頁面,則到n=3的組中去找空閒內存,找到了則分配連續的8個頁面;若是本組中已經沒有空閒頁面,則去更高一級n=4的組中去找空閒內存,該組中的空閒內存塊都是連續的16個頁面,若是該組中有空閒內存,則將空閒內存塊一分爲二,分紅兩個連續頁面爲8的空閒塊,一塊分配出去,一塊併入n=3的組中。在釋放內存頁時,假定釋放的內存爲連續的8個頁面,則將其納入n=3的空閒頁面組中,若是發現該組中某兩個內存款互爲夥伴,便可將這兩個塊合併成一個連續16個頁面的內存塊,並納入到n=4的組中。這就是夥伴系統的工做過程。

struct free_area的定義在同一個文件中:

#define MIGRATE_UNMOVABLE     0
    #define MIGRATE_RECLAIMABLE   1
    #define MIGRATE_MOVABLE       2
    #define MIGRATE_RESERVE       3
    #define MIGRATE_ISOLATE       4 /* can't allocate from here */
    #define MIGRATE_TYPES         5
    
    struct free_area {
    	struct list_head	free_list[MIGRATE_TYPES];
    	unsigned long		nr_free;
    };

結構體free_area定義了一組隊列頭,這樣,一個管理區的空閒內存就經過兩個層次來管理:

Zone的空閒內存

空閒內存已經按2的n次冪來進行了分組,對每一個組內的內存塊爲什麼還要再分組?爲了解釋這個問題,咱們須要知道內核將已經分配的頁面按照可移動性分爲下列三種:

  • MIGRATE_UNMOVABLE,不可移動頁。頁幀在內存中的位置固定,不能夠移動,內核分配的大部分頁面都屬於該類型。
  • MIGRATE_RECLAIMABLE,可回收頁。頁幀不能直接移動,但能夠刪除。其內容能夠從某些源從新生成。文件映射對應的內存屬於該類。
  • MIGRATE_MOVABLE,可移動頁,頁幀能夠隨意移動,屬於用戶空間程序的頁屬於此類,頁幀移動後,只須要更新相應的頁表便可,用戶空間的應用並不會感受到這些移動。

假定某個連續32頁的內存塊中,散落着不可移動頁(深顏色部分)和可回收頁(白色部分)。不可移動頁已經被分出去5頁(斜線部分)。

不區分可移動性

此時,雖然可回收頁有16頁,但最多可分4個連續的頁。但若是咱們將不可移動頁和可回收頁分組,以下圖,則對於可回收頁,最多可分連續的16頁,減小了內存碎片。

區分可移動性

可見,將內存按照可移動性分組,能夠有效減小內存碎片。這就是在2.6.24版本的內核引入的「反碎片」技術的基礎。

結構體free_area中的成員nr_free表示該區域空閒頁面的總數。

讀者可能會有疑問,前面說過內存碎片是對內核而言的,而內核中主要又使用不可移動頁面。即便將內存頁按移動性分組,可這對解決內核的碎片化又有什麼幫助呢?內核仍然在不可移動頁上分配,不可移動頁上的碎片化並無改善呀?你的想法是正確的,這裏的反碎片技術實際上是對用戶空間程序有益的。大多數現代CPU都提供了使用巨型頁的可能,這種巨型頁比普通頁大不少,這對內存密集型的應用有好處。用戶空間經過頁表映射訪問內存,使用更大的頁(這個頁內的地址固然是連續的),則「地址轉換/查找緩衝區」只需處理較少的項,而且能夠下降TLB緩存(爲了加快頁面映射的速度,並非每次都從內存中訪問頁面映射目錄和頁面映射表,而是將它們裝入高速緩存中緩衝起來,這部分高速緩存稱爲TLB,即「地址轉換/查找緩衝區」(Translation Lookaside Buffers))失效的可能性。而分配巨型頁,就須要更多的連續內存頁。用戶空間的應用程序通常在可移動或者可回收內存頁上分配,將內存按移動性分組後,咱們就能夠利用這種移動性,將小塊空閒內存合併成大塊空閒內存,給分配巨型頁提供了可能。

若是內核沒法知足針對某種移動性的分配請求,會怎麼樣?這與咱們在Node定義時討論的備選區的問題相似,內核的解決辦法也是提供一個備選列表。其定義以下,其表達的意圖不言自明。

<mm/page_alloc.c>
    static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = {
        [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,
                                    MIGRATE_RESERVE },
        [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,
                                    MIGRATE_RESERVE },
        [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE,
                                    MIGRATE_RESERVE },
	    [MIGRATE_RESERVE]     = { MIGRATE_RESERVE,     MIGRATE_RESERVE,
                                    MIGRATE_RESERVE }, /* Never used */
    };

zone中其餘的成員的含義請參考註釋。

3 最底層——物理頁幀Page

物理頁幀是物理內存的最小管理單元,其對應的抽象爲page,結構體定義以下:

struct page {
    	/*用於描述頁的屬性。如PG_locked */
    	unsigned long flags;
    	/* 使用計數,表示內核中引用該頁的次數 */
    	atomic_t _count;		/* Usage count, see below. */
    	union {
    		/*
    		 * 若是該頁映射到用戶空間虛擬地址上,則表示映射到頁表pte的次數 
    		 * 每增長一個使用者,計數器加1
    		 */
    		atomic_t _mapcount;
    		/* 若是是內核slab使用的頁,則表示其中的slab對象數目 */
    		unsigned int inuse;/* SLUB: Nr of objects */
    	};
    	union {
    	    struct {
           /*
    		 * 私有數據指針。
    		 * 若是頁位於交換緩存,則指向存儲swp_entry_t結構
    		 */
    		unsigned long private;
    		struct address_space *mapping; 
    	    };
    	    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; /*用於頁面換出的鏈表,可能連接到活動頁鏈表和不活動頁鏈表 */
    
    #if defined(WANT_PAGE_VIRTUAL)
    	void *virtual;			/* Kernel virtual address (NULL if not kmapped, 
                                   ie. highmem) */
    #endif 
    };
  • 因爲一個page可能經過頁表映射到了用戶的虛擬地址空間中,也可能在內核中被slab分配器管理,因此結構體的定義中有不少的聯合union。slab分配器的工做過程比較複雜,留待之後分析。後面的講解將跳過slab分配相關的內容。
  • mapping爲address_space類型的指針,在頁面換入換出時會詳細講到。
  • 當頁面的內容來自一個文件時(好比mmap映射,或者時從交換文件中換入),index表明着該頁面在文件中的序號;當頁面的內容換出到交換設備上,但還保留着內容做爲緩衝時,index指向了頁面的去向。

爲了便於後文中的情景分析,這裏將2.4版本內核中page結構體的定義展現以下:

/*
        * Try to keep the most commonly accessed fields in single cache lines
        * here (16 bytes or greater). This ordering should be particularly
        * beneficial on 32bit processors.
        *
        * The first line is data used in page cache lookup, the second line
        * is used for linear searches (eg. clock algorithm scans).
        */
    typedef struct page {
        struct list_head list;
        struct address_space *mapping;
        unsigned long index;
        struct page *next_hash;
        atomic_t count;
        unsigned long flags; /* atomic flags, some possibly updated asynchronously */
        struct list_head lru;
        unsigned long age;
        wait_queue_head_t wait;
        struct page **pprev_hash;
        struct buffer_head * buffers;
        void *virtual; /* nonNULL if kmapped */
        struct zone_struct *zone;
     } mem_map_t

該版本的實現中,page有兩個成員next_hash 和pprev_hash,該成員用來將page鏈到某個哈希表中。

page就像是物理頁幀的戶口,每個物理頁幀都對應一個page結構。若是一個物理頁面沒有對應的page結構,它就成了「黑戶」了,系統就沒法看到它,也就沒法對它進行管理。系統在初始化時,會根據物理內存的大小創建一個全局的page結構體數組mem_map,做爲物理頁面的「戶口簿」,裏面的每個元素表明一個物理的頁幀,而數組的下標就是物理page的邏輯編號。

物理內存管理中的這三層抽象,示意以下:

物理內存管理三層抽象

相關文章
相關標籤/搜索