Linux內存管理第二章 -- Describing Physical Memory

Linux內存管理第二章 – Describing Physical Memory

首先來描述幾個名詞:

  • NUMA:Non-Uniform Memory Access即內存非一致性訪問.在很多巨星機上內存被分成了多個區域,CPU訪問不同的內存區域耗費的時間是不同的.但在同一區域內,CPU訪問這些內存的時間是一致的。
  • UMA:Uniform Memory Access即內存一致性訪問,即所CPU訪問所有內存的耗時都是一致的。
  • node:NUMA架構下的每一個內存區域稱作是node,node用struct pglist_data來描述。所有的node在Linux內核中用一個全局鏈表pgdat_list關聯起來,pg_data_t->pgdat_next指向下一個區域。
  • zone:每一個node下面的內存又被劃分爲多個小區域,每個區域稱作是zone.zone一般分爲三個:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM.Linux中使用struct zone來描述一個zone。
  • page:Linux內核對每一個物理內存頁框的描述。
    node和zone的關係如下圖:
    zone

Nodes

Linux內核中每一個node由struct pglist_data來進行描述。其定義如下<include/linux/mmzone.h>,下面來介紹下主要成員的作用。

typedef struct pglist_data {
	struct zone node_zones[MAX_NR_ZONES];
	struct zonelist node_zonelists[GFP_ZONETYPES];
	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;
	struct pglist_data *pgdat_next;
	wait_queue_head_t       kswapd_wait;
	struct task_struct *kswapd;
} pg_data_t;
  • node_zones:當前node中的zones:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM
  • node_zonelists:該數組決定了當前node中分配物理內存時,選擇zone的順序.該數組的初始化由mm/page_alloc.c中的build_zonelists()來完成。build_zonelists()由free_area_init_core()調用觸發。一般狀況下當從ZONE_HIGHMEM中分配內存失敗時,會再嘗試從ZONE_NORMAL或者ZONE_DMA中分配。
  • node_mem_map:該指針表示當前node中的物理頁框數字中的第一個頁框描述符struct page的地址。在內核中有些地方也可以直接使用全局數組mem_map來訪問獲取node的第一個page地址,實現node_mem_map相同的功能。
  • node_start_paddr:當前node的物理地址的起始地址。該字段類型是一個unsigned long型,有的時候可能會超長,一個比較好的頒發是在此字段中中記錄物理頁框號(Page Frame Number,PFN)。PFN = page_phys_addr >> PAGE_SHIFT
  • node_start_mapnr:當前node中起始物理地址在全局數組mem_map中的偏移。
  • node_size:當前node中page的總數。
    系統中維護的所有node都在一個全局列表pgdat_list中。該list由init_bootmem_core()函數初始化。

Zones

Linux內核中每一個zone由struct zone結構來描述。該結構主要使用來統計page的使用和釋放信息。其定義如下,接下來再看下重點成員:

struct zone {
	spinlock_t		lock;
	unsigned long		free_pages;
	unsigned long		pages_min, pages_low, pages_high;
	unsigned long		protection[MAX_NR_ZONES];
	ZONE_PADDING(_pad1_)
	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		nr_active;
	unsigned long		nr_inactive;
	int			all_unreclaimable; /* All pages pinned */
	unsigned long		pages_scanned;	   /* since last reclaim */
	ZONE_PADDING(_pad2_)
	int temp_priority;
	int prev_priority;
	struct free_area	free_area[MAX_ORDER];
	wait_queue_head_t	* wait_table;
	unsigned long		wait_table_size;
	unsigned long		wait_table_bits;
	ZONE_PADDING(_pad3_)
	struct per_cpu_pageset	pageset[NR_CPUS];
	struct pglist_data	*zone_pgdat;
	struct page		*zone_mem_map;
	/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
	unsigned long		zone_start_pfn;
	char			*name;
	unsigned long		spanned_pages;	/* total size, including holes */
	unsigned long		present_pages;	/* amount of memory (excluding holes) */
};
  • free_pages:當前zone中free page的總個數
  • pages_min,pages_low,page_high:該zone中的水位線,具體在下一小節進行說明。
  • free_area:buddy allocator使用的free area bitmaps
  • wait_table:等待某個page被釋放的進程的等待隊列鏈表。這個隊列對於wait_on_page()和unlock_page()函數來說非常重要。
  • wait_table_size:wait_table中等待隊列的個數。
  • wait_table_bits:wait_table_size == (1 << wait_table_bits)
  • zone_pgdat:指向當前zone所屬的node
  • zone_mem_map:當前zone中的第一個page的地址,該page輸入全局數組mem_map
  • size:當前zone中pages 的個數

Zone初始化

當內核的頁表有page_init()建立好之後,就開會初始化zone。每種硬件架構執行zone初始化的時候可能稍有不同,但是最終目標是相同的。每種架構初始化zone的流程中的最終目標是決定傳入什麼參數值到UMA架構的初始化函數free_area_init()或者NUMA架構的初始化函數free_area_init_node()中。下面來看下參數的含義:

void __init free_area_init_node(int nid, struct pglist_data *pgdat,
								unsigned long *zones_size, unsigned long node_start_pfn,
								unsigned long *zholes_size)
{
	............
}
  • nid:要被初始化的zone所屬的node的邏輯標識符。
  • pgdat:要被初始化成zone的node
  • zones_size:包含每個zone的page的個數的數組
  • node_start_pfn:當前node的起始PFN
  • zholes_size:包含每個zone中memory holes的個數的數組。
    無論是free_area_init()還是free_area_init_node()都會調用free_area_init_core()來真正初始化每個struct zone中的成員。在做zone初始化的時候,系統無法知道每個zone中有多少可用的page,這些信息知道boot memory allocator退休之後纔會知道。

mem_map初始化

mem_map是在系統啓動階段被創建。在NUMA系統中,全局數組mem_map被當做是一個虛擬的數組其起始位置爲PAGE_OFFSET(一般就是指虛擬地址3G的位置)。系統中的每個node初始化的過程中調用free_area_init_node()來爲這個虛擬數組分配填充mem_map的一部分內容。而在UMA系統中,因爲只有一個node,所以其node爲全局變量contig_page_data,而全局變量mem_map就是這個contig_page_data中的ode_mem_map。
對於UMA架構來說,核心函數free_area_core()將爲當前初始化的node分配一個本地的lmem_map,lmem_map是從boot memory allocator中使用alloc_bootmem_node()來進行分配。該新分配的lmem_map將會成爲全局的mem_map。但NUMA系統中與此有稍微不同。
對於NUMA架構來說,核心函數free_area_core()將從node節點自己的內存中分配lmem_map,而全部變量mem_map不會顯式的分配,而是將其設定爲PAGE_OFFSET的虛擬地址上,其被當做是一個虛擬的數組。而lmem_map被存儲在node中的node_mem_map,pg_data_t->node_mem_map,其在虛擬數組mem_map的某個位置上。當node和zone被初始化完整之後,mem_map將會被當成一個真正的數組了。

Pages

page定義

系統中的每一個物理頁框都有一個相應地struct page,其用來持續跟蹤物理頁框的狀態。其定義如下:

struct page {
	page_flags_t flags;		/* Atomic flags, some possibly updated asynchronously */
	atomic_t _count;		/* Usage count, see below. */
	atomic_t _mapcount;		/* Count of ptes mapped in mms,to show when page is mapped & limit reverse map searches. 
	unsigned long private;	/* Mapping-private opaque data:usually used for buffer_heads if PagePrivate set;
	                        /* used for swp_entry_t if PageSwapCache */
	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.*/
	pgoff_t index;			/* Our offset within mapping. */
	struct list_head lru;/* Pageout list, eg. active_list protected by zone->lru_lock !*/
	/* On machines where all RAM is mapped into kernel address space,
	 * we can simply calculate the virtual address. On machines with
	 * highmem some memory is mapped into kernel virtual memory
	 * dynamically, so we need a place to store that address.
	 * Note that this field could be 16 bits on x86 ... ;)
	 *
	 * Architectures with slow multiplication can define
	 * WANT_PAGE_VIRTUAL in asm/page.h 
  */
#if defined(WANT_PAGE_VIRTUAL)
	void *virtual;	/* Kernel virtual address (NULL if not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
};
  • mapping:當文件或者設備有內存映射,那麼文件的inode中有一個成員address_space,如果該page屬於這個文件那麼mapping字段將指向這個address_space。
  • lru:根據page的替換策略,active_list或者inactive_list中的pages都可能被換出。該字段是active_list或者inacvtive_list的頭。
  • virtual:通常來講只有ZONE_NORMAL中的物理內存纔會被內核直接映射到虛擬地址空間,而ZONE_HIGHMEM中的物理內存需要調用kmap()來講物理地址轉成虛擬地址,通常只有固定數目的pages被kmap()轉換,如果當前page被kmap映射了,則virtual字段記錄它的虛擬地址。
  • index:mapping中偏移。
  • flags:該字段表示描述page狀態的標誌位
  • count:當前page正在被使用的次數。當count爲0時,該page可能被釋放。只要大於0,那麼當前被一個或者多個進程或者內核在使用中。
bit名稱 描述
PG_active 當一個page在avtive_list LRU中時,該bit被置位。從active_list LRU中被刪除時,該bit被清零。該bit標記page當前的熱度
PG_arch_1 該bit位依據不同的架構而不同的page狀態位。通常當一個page第一次整體進入page cache的時候該位被清零
PG_checked 僅僅被Ext2文件系統使用
PG_dirty 該標記位1時表示該page需要被刷新到磁盤上。磁盤文件對應的page被寫後不會立即刷新到磁盤,該bit爲來保證dirty page在刷新之前不被釋放
PG_error 在磁盤IO過程中發生錯誤,該bit爲被置位
PG_fs_1 該bit位保留給文件系統自己用。當前只有NFS用該bit位來表示當前page是否同遠端server同步
PG_highmem 高端內存的pages不能被kernel直接映射,因此在mem_init()的時候high memory page該bit位被置位
PG_launder 系統一般會先將該bit置位然後再調用writepage()函數,當系統想換出一個page,在掃描時如果發現一個page的該bit爲被置位且PG_locked被置位,則系統會等待IO完成。
PG_locked 當進行磁盤IO時該bit爲必須置位,IO完成,該bit爲清零
PG_lru 如果一個page在active_list或者inactive_list中時,該bit爲置位
PG_referenced 當一個page被映射並且通過映射或者哈希表有訪問,則該bit爲置位,其主要用作LRU list更新過程中。
PG_slab 如果page正在被slab allocator使用,該bit爲置位
PG_skip 某些架構中用該bit爲來標記跳過虛擬地址空間中沒有對應物理內存的部分
PG_unused 該標記位目前沒有使用
PG_uptodate 當一個page從磁盤中讀入內容時沒有報錯,該bit爲置位

映射 Pages 到 Zones

在kernel2.4中,struct page中有個struct zone的指針,用來表示page輸入哪個zone,這是一種相當浪費的行爲。因爲當有幾千個page的時候,這一個小小的指針也會耗費很多的memory。在kernel2.6中,struct page中的zone字段被刪除了,取而代之的是使用page->flags中的高ZONE_SHIFT(x86下是8)位來決定page屬於哪個zone。
首先創建一個zone_table,mm/page_alloc.c:

/*
 * Used by page_zone() to look up the address of the struct zone whose
 * id is encoded in the upper bits of page->flags
 */
struct zone *zone_table[1 << (ZONES_SHIFT + NODES_SHIFT)];
EXPORT_SYMBOL(zone_table);

然後調用set_page_zone設置zone的ID。

static inline void set_page_zone(struct page *page, unsigned long nodezone_num)
{
	page->flags &= ~(~0UL << NODEZONE_SHIFT);
	page->flags |= nodezone_num << NODEZONE_SHIFT;
}

高端內存–High Memory

爲什麼要支持高端內存?

因爲kernel所能使用的虛擬地址空間是有限的(一般只ZONE_NORMAL)。所以kernel需要支持High Memory的概念。在x86 32位系統中,對於高端內存來說有兩個閾值:4GB,64GB
4GB:因爲物理地址是32位,最大4GB。所以虛擬地址也是32位即虛擬地址空間是0 ~~ 4GB。由於內核只能使用虛擬地址3G ~ 4G這一個G的虛擬地址,而這一個G的虛擬地址大部分被kernel從ZONE_NORMAL和ZONE_DMA直接線性映射所佔用,如果系統想要訪問更多的物理內存就得從ZONE_HIGMEM中獲取物理頁框再調用kmap()將其映射到虛擬地址空間,從而讓高端物理內存可以。
64GB:Intel發明了PAE技術(Page Address Extension)讓32位系統可以使用更多的物理內存。即 2362^{36} = 64GB
PAE理論上讓處理器可以訪問64GB物理內存,但是實際上32位Linux系統中的進程無法訪問那麼多物理內存因爲虛擬地址空間仍然是4GB。
再者來說,PAE不允許內核自己使用如此多的內存。一個struct page佔用44字節,而struct page所佔用的的虛擬內從空間是在內核可用的虛擬地址空間內。因此描述1GB的物理內存,所需要的struct page就要佔用11MB的虛擬地址空間,16GB的物理內存描述就要佔用176MB的虛擬地址空間,這會使得內核所使用的虛擬地址空間更加緊張。
因此在32位系統中要訪問更多的物理內存,就得內核就得支持高端內存的概念。
所謂高端內存就是指將內核現行映射完成之後還有多餘的物理內存,通過動態映射的方式將物理地址轉換爲虛擬地址,從而使得這部分物理地址可用。這也僅僅是臨時可用,一個進程也不能一次malloc 3G以上的內存使用。

高端內存具體如何使用?

例如socket中的buffer就是使用高端內存,比如網絡上來了大量數據,內核先調用kmap()將一些page映射成虛擬地址,然後將網絡數據copy到物理內存,然後調用kunmap()解除這段虛擬地址到物理地址的映射,此時內核記錄的是struct page的地址來描述這些buff在哪裏。
當上層應用要讀取socket的數據的時候,kernel再次調用kmap()將之前存放網絡數據的page再次映射成虛擬地址,讓上層應用將這些數據copy出去。

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