Linux內存管理(二)物理內存管理(上)

Linux內存管理

Linux內存管理(一)Linux進程空間管理

Linux內存管理(二)物理內存管理(上)

Linux內存管理(三)物理內存管理(下)

Linux內存管理(四)用戶態內存映射

Linux內存管理(五)內核態內存映射

Linux內存管理(二)物理內存管理(上)

一、物理內存的組織形式

由於物理內存是連續的,頁也是連續的,每個頁的大小一樣,從0開始給每個頁編號,每個頁用struct page表示,存放在一個大數組裏。因此對於任何一個地址,只要除以頁的大小,就可以得到對應頁的編號,根據下標就可以找到對應的struct page結構,這種模型是最經典的平坦內存模型

所有的CPU總是通過總線去訪問內存,這是最經典的內存使用方法,它可以使用平坦內存模型來管理內存

img

在這種模式下,所有的CPU都在總線的一側,所有的內存組成一大塊內存在總線的另外一側,CPU訪問內存都需要通過總線訪問,而且距離都是一樣的,這種模式稱爲SMP(Symmetric multiprocessing),即爲對稱多處理器。這種模式有一個顯著的缺點,就是每個CPU訪問內存都需要通過總線,那麼總線就會成爲瓶頸

img

爲了提高性能,有了一種更加高級的模式,NUMA(Non-uniform memory access),非一致內存訪問。這種模式下,內存不是組成連續的一大塊,而是每個CPU都有自己的一塊內存,CPU訪問內存不需要經過總線,所以速度上會更快,每個CPU和內存組成一個NUMA節點。但是在本地內存不足的情況下,每個CPU會去其他NUMA節點申請內存,此時內存的訪問時間就比較長

這樣內存被分爲多個節點,每個節點都分成一個一個的頁。由於頁是全局唯一定位的,所以每個頁都需要有一個全局唯一的頁號。但是由於物理內存不再是連續的,所以頁號也不是連續的,於是內存模型就變成了非連續內存模型,管理起來就會比較複雜

二、節點

下面解析當前主流場景,NUMA方式

爲了表示一個NUMA節點,內核定義了struct pglist_struct這樣一個結構體,如下

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;
	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;
......
} pg_data_t;

  • 每個節點都有自己的ID:node_id
  • node_mem_map是這個節點struct page數組,用於描述這個節點所有的頁
  • node_start_pfn是這個節點的起始頁號
  • node_spanned_pages是整個物理內存包含的頁數目(包括空洞)
  • node_present_pages是真正可用的物理頁數目

例如:64M物理內存隔着4M的空洞,然後再是另外的64M,換算成頁數目,分別是16K、1K、16K。那麼node_spanned_pages就是33K,node_spanned_pages就是32K

每個節點被分爲一個一個的區域zone,存放在node_zones數組中,數組的大小爲MAX_NR_ZONES,定義如下

enum zone_type {
#ifdef CONFIG_ZONE_DMA
	ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
	ZONE_DMA32,
#endif
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
	__MAX_NR_ZONES
};

這裏說明以下,這些分區都是對物理內存的說明

  • ZONE_DMA:用作DMA的物理內存
  • ZONE_DMA32:對於64位CPU,還有這個DMA區域
  • ZONE_NORMAL:就是直接映射區
  • ZONE_MOVABLE:可移動區域,通過將物理內存劃分爲可移動分配區域和不可移動分配區域來避免內存碎片
  • __MAX_NR_ZONES:內存區域的數量

內核中有一個數組用來存放節點

struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;

三、區域

到這裏,將內存分爲節點,將節點分爲區域,下面來看一看區域的定義

區域是zone結構體表示

struct zone {
......
	struct pglist_data	*zone_pgdat;
	struct per_cpu_pageset __percpu *pageset;


	unsigned long		zone_start_pfn;


	/*
	 * spanned_pages is the total pages spanned by the zone, including
	 * holes, which is calculated as:
	 * 	spanned_pages = zone_end_pfn - zone_start_pfn;
	 *
	 * present_pages is physical pages existing within the zone, which
	 * is calculated as:
	 *	present_pages = spanned_pages - absent_pages(pages in holes);
	 *
	 * managed_pages is present pages managed by the buddy system, which
	 * is calculated as (reserved_pages includes pages allocated by the
	 * bootmem allocator):
	 *	managed_pages = present_pages - reserved_pages;
	 *
	 */
	unsigned long		managed_pages;
	unsigned long		spanned_pages;
	unsigned long		present_pages;


	const char		*name;
......
	/* free areas of different sizes */
	struct free_area	free_area[MAX_ORDER];


	/* zone flags, see below */
	unsigned long		flags;


	/* Primarily protects free_area */
	spinlock_t		lock;
......
} ____cacheline_internodealigned_in_

  • zone_start_pfn:表示這個屬於這個zone的第一個頁
  • spanned_pages:註釋裏有spanned_pages = zone_end_pfn - zone_start_pfn,表示spanned_pages就是結束頁面減去起始頁面的頁面數,不管中間是否存在空洞
  • present_pages:註釋裏有spanned_pages - absent_pages(pages in holes),表示減去空洞後的頁面數
  • managed_pages:註釋中有managed_pages = present_pages - reserved_pages,表示這個zone中被夥伴系統管理的所有的page數目
  • per_cpu_pageset:用於區分冷熱頁。什麼是冷熱頁?指的是一個頁是否被加載進CPU的高速緩存中

四、頁

在瞭解區域後,再來看組成物理內存最基本的單位,頁的數組結構使用struct page表示。這個結構體定義非常的複雜,因爲支持多種使用模式,所以定義了許多union

    struct page {
    	unsigned long flags;
    	union {
    		struct address_space *mapping;	
    		void *s_mem;			/* slab first object */
    		atomic_t compound_mapcount;	/* first tail page */
    	};
    	union {
    		pgoff_t index;		/* Our offset within mapping. */
    		void *freelist;		/* sl[aou]b first free object */
    	};
    	union {
    		unsigned counters;
    		struct {
    			union {
    				atomic_t _mapcount;
    				unsigned int active;		/* SLAB */
    				struct {			/* SLUB */
    					unsigned inuse:16;
    					unsigned objects:15;
    					unsigned frozen:1;
    				};
    				int units;			/* SLOB */
    			};
    			atomic_t _refcount;
    		};
    	};
    	union {
    		struct list_head lru;	/* Pageout list	 */
    		struct dev_pagemap *pgmap; 
    		struct {		/* slub per cpu partial pages */
    			struct page *next;	/* Next partial slab */
    			int pages;	/* Nr of partial slabs left */
    			int pobjects;	/* Approximate # of objects */
    		};
    		struct rcu_head rcu_head;
    		struct {
    			unsigned long compound_head; /* If bit zero is set */
    			unsigned int compound_dtor;
    			unsigned int compound_order;
    		};
    	};
    	union {
    		unsigned long private;
    		struct kmem_cache *slab_cache;	/* SL[AU]B: Pointer to slab */
    	};
    ......
    }

第一模式

要用就使用一整頁。這一整頁的內存要麼直接跟虛擬地址建立映射關係,這中稱爲匿名頁(Anonymous Page)。或者關聯一個文件,然後再跟虛擬地址建立映射

如果某一頁使用此模式,那麼union就使用以下的結構

  • struct address_space *mapping 就是用於內存映射,如果是匿名頁,最低位爲1;如果是映射文件,最低位爲0
  • pgoff_t index 是映射區的偏移量
  • atomic_t _mapcount 每個進程都有自己的頁表,這個變量是指有多少個頁表映射到這個物理頁
  • struct list_head lru 表示這一頁應該在鏈表上,如果這一頁被換出,那麼就應該在換出頁的鏈表中
  • compound相關的變量用於複合頁,就是將物理上連續的兩個或者多個看成一個獨立的大頁

第二種模式

僅需要分配一小塊內存,並不需要一整頁。爲了滿足這種需求,Linux系統採用了一種被稱爲slab allocator的技術,用於分配slab中的一小塊內存。它的基本工作原理是申請一整塊頁,然後劃分成許多小塊的存儲池,用複雜的隊列來維護這些小塊的狀態(被分配了 / 被放回池子了 / 應該被回收)

slab對於隊列的維護過於複雜,後來出現了一種不使用隊列的分配器slub allocator,它保留的slab的用戶接口,可以把它看作是slab的另一種實現

還有一種小塊內存的分配器slob

如果某一頁被切分爲一小塊一小塊,那麼union中就會使用以下結構

  • s_mem 是已經分配了正在使用的slab的第一個對象
  • freelist 是池子的空閒對象
  • rcu_head 是需要釋放的列表

五、頁的分配

前面講了物理內存的組織,從NUMA節點到區域到頁再到小塊。接下倆看物理內存的分配

對於要分配比較大的內存,例如分配頁級別的,可以使用夥伴系統(Buddy System)

Linux內存管理的頁大小爲4K。把所有空閒的頁分組爲11個頁塊鏈表,每個鏈表管理相應大小的頁塊,有1、2、4、8、16、32、64、128、256、512、1024個連續頁的頁塊。最大可以申請1024個連續的頁,對應4M大小的連續內存。每個頁塊第一頁的起始地址是該頁塊大小的整數倍

img

在 struct zone 裏面有以下的定義

struct free_area	free_area[MAX_ORDER];

MAX_ORDER表示2的指數

#define MAX_ORDER 11

當申請的頁塊大小介於free_area中兩個頁塊大小之間時,會選取更大的一個頁塊大小,或者如果對應的大小沒有空閒的頁塊,那麼也會分配一個更大的頁塊。在得到一個更大的頁塊後,會將其進行拆分,然後將空閒的頁塊繼續插入到對應頁塊大小的鏈表中

例如申請一個128個頁的頁塊,如果沒有,那麼就找256,然後一直如此,直到能夠找到。如果找到的是256個頁的頁塊。那麼就會將其拆分爲128和128個頁大小的頁塊,然後將一個空閒的頁塊添加到128對應的頁塊鏈表中

對於這些內容,可以在 alloc_pages 函數中找到定義

static inline struct page *
alloc_pages(gfp_t gfp_mask, unsigned int order)
{
	return alloc_pages_current(gfp_mask, order);
}


/**
 * 	alloc_pages_current - Allocate pages.
 *
 *	@gfp:
 *		%GFP_USER   user allocation,
 *      	%GFP_KERNEL kernel allocation,
 *      	%GFP_HIGHMEM highmem allocation,
 *      	%GFP_FS     don't call back into a file system.
 *      	%GFP_ATOMIC don't sleep.
 *	@order: Power of two of allocation size in pages. 0 is a single page.
 *
 *	Allocate a page from the kernel page pool.  When not in
 *	interrupt context and apply the current process NUMA policy.
 *	Returns NULL when no page can be allocated.
 */
struct page *alloc_pages_current(gfp_t gfp, unsigned order)
{
	struct mempolicy *pol = &default_policy;
	struct page *page;
......
	page = __alloc_pages_nodemask(gfp, order,
				policy_node(gfp, pol, numa_node_id()),
				policy_nodemask(gfp, pol));
......
	return page;
}

  • order:表示分配2的指數個頁的頁塊
  • gfp:分配標誌,表示要分配那麼區域的物理頁
    • GFP_USER 表示分配一個頁映射到用戶虛擬地址空間,並且希望直接被內核或者硬件訪問,主要用於一個用戶進程希望通過內存映射的方式,訪問硬件緩存(如顯卡緩存)
    • GFP_KERNEL 用於內核中分配頁,主要分配 ZONE_NORMAL 區域的內存
    • GFP_HIGHMEM 用於分配高端區域的物理內存

接下來調用 get_page_from_freelist,這是夥伴系統的核心。它會先循環查找對應節點的zone,如果找不到,那麼就看備用節點的zone

static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
						const struct alloc_context *ac)
{
......
	for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx, ac->nodemask) {
		struct page *page;
......
		page = rmqueue(ac->preferred_zoneref->zone, zone, order,
				gfp_mask, alloc_flags, ac->migratetype);
......
}

每一個zone,都有夥伴系統維護的各種大小的隊列

rmqueue 就是找到合適大小的隊列,然後將頁塊取下來

最終會調用到 __rmqueue_smallest

static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
						int migratetype)
{
	unsigned int current_order;
	struct free_area *area;
	struct page *page;


	/* Find a page of the appropriate size in the preferred list */
	for (current_order = order; current_order < MAX_ORDER; ++current_order) {
		area = &(zone->free_area[current_order]);
		page = list_first_entry_or_null(&area->free_list[migratetype],
							struct page, lru);
		if (!page)
			continue;
		list_del(&page->lru);
		rmv_page_order(page);
		area->nr_free--;
		expand(zone, page, order, current_order, area, migratetype);
		set_pcppage_migratetype(page, migratetype);
		return page;
	}


	return NULL;

從指定的區域中,按照當前指定的指數開始查找,如果找不到,那麼就到更大的指數查找。除了將頁塊從鏈表取下,還要將多餘的頁面插入到合適的鏈表中,expand 就是完成這個工作

static inline void expand(struct zone *zone, struct page *page,
	int low, int high, struct free_area *area,
	int migratetype)
{
	unsigned long size = 1 << high;


	while (high > low) {
		area--;
		high--;
		size >>= 1;
......
		list_add(&page[size].lru, &area->free_list[migratetype]);
		area->nr_free++;
		set_page_order(&page[size], high);
	}
}

六、總結

如果有多個CPU,就會有多個NUMA節點。每個節點使用 struct pglist_data 表示,存放在一個數組中

每個節點分爲多個區域,每個區域使用 struct zone 表示,也存放在一個數組中

每個區域分爲多個頁,空閒頁存放在 struct free_area 中,使用夥伴系統進行管理和分配

每一頁都是使用 struct page 表示

img

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章