日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

物理內(nèi)存的組織方式

前面咱們講虛擬內(nèi)存,涉及物理內(nèi)存的映射的時候,我們總是把內(nèi)存想象成它是由連續(xù)的一頁一頁地塊組成的。我們可以從 0 開始對物理頁編號,這樣每個物理頁都會有個頁號。

由于物理地址是連續(xù)的,頁也是連續(xù)的,每個頁大小也是一樣的。因而對于任何一個地址,只要直接除一下每頁的大小,很容易直接算出在哪一頁。每個頁面有一個結(jié)構(gòu) struct page 表示,這個結(jié)構(gòu)也是放在一個數(shù)組里面,這樣根據(jù)頁號,很容易通過下標找到相應(yīng)的 struct page 結(jié)構(gòu)。

如果是這樣,整個物理內(nèi)存的布局就非常簡單、易管理,這就是最經(jīng)典的平坦內(nèi)存模型(Flat Memory Model)。

我們講 x86 的工作模式的時候,講過 CPU 是通過總線去訪問內(nèi)存的,這就是最經(jīng)典的內(nèi)存使用方式。

物理內(nèi)存的組織方式:每個頁面都有一個結(jié)構(gòu),每一個都有節(jié)點

 

在這種模式下,CPU 也會有多個,在總線的一側(cè)。所有的內(nèi)存條組成一大片內(nèi)存,在總線的另一側(cè),所有的 CPU 訪問內(nèi)存都要過總線,而且距離都是一樣的,這種模式稱為SMP(Symmetric multiprocessing),即對稱多處理器。當然,它也有一個顯著的缺點,就是總線會成為瓶頸,因為數(shù)據(jù)都要走它。

物理內(nèi)存的組織方式:每個頁面都有一個結(jié)構(gòu),每一個都有節(jié)點

 

為了提高性能和可擴展性,后來有了一種更高級的模式,NUMA(Non-uniform memory access),非一致內(nèi)存訪問。在這種模式下,內(nèi)存不是一整塊。每個 CPU 都有自己的本地內(nèi)存,CPU 訪問本地內(nèi)存不用過總線,因而速度要快很多,每個 CPU 和內(nèi)存在一起,成為一個 NUMA 節(jié)點。但是,在本地內(nèi)存不足的情況下,每個 CPU 都可以去另外的 NUMA 節(jié)點申請內(nèi)存,這個時候訪問延時就會比較長。

這樣,內(nèi)存被分成了多個節(jié)點,每個節(jié)點再被分成一個一個的頁面。由于頁需要全局唯一定位,頁還是需要有全局唯一的頁號的。但是由于物理內(nèi)存不是連起來的了,頁號也就不再連續(xù)了。于是內(nèi)存模型就變成了非連續(xù)內(nèi)存模型,管理起來就復雜一些。

這里需要指出的是,NUMA 往往是非連續(xù)內(nèi)存模型。而非連續(xù)內(nèi)存模型不一定就是 NUMA,有時候一大片內(nèi)存的情況下,也會有物理內(nèi)存地址不連續(xù)的情況。

后來內(nèi)存技術(shù)牛了,可以支持熱插拔了。這個時候,不連續(xù)成為常態(tài),于是就有了稀疏的內(nèi)存模型。

更多l(xiāng)inux內(nèi)核視頻教程文檔資料免費領(lǐng)取后臺私信【內(nèi)核】自行獲取。

物理內(nèi)存的組織方式:每個頁面都有一個結(jié)構(gòu),每一個都有節(jié)點

 

內(nèi)核學習網(wǎng)站:

Linux內(nèi)核源碼/內(nèi)存調(diào)優(yōu)/文件系統(tǒng)/進程管理/設(shè)備驅(qū)動/網(wǎng)絡(luò)協(xié)議棧-學習視頻教程-騰訊課堂

NUMA節(jié)點

我們主要解析當前的主流場景,NUMA 方式。我們首先要能夠表示 NUMA 節(jié)點的概念,于是有了下面這個結(jié)構(gòu) typedef struct pglist_data pg_data_t,它里面有以下的成員變量:

  • 每一個節(jié)點都有自己的 ID:node_id;
  • node_mem_map 就是這個節(jié)點的 struct page 數(shù)組,用于描述這個節(jié)點里面的所有的頁;
  • node_start_pfn 是這個節(jié)點的起始頁號;
  • node_spanned_pages 是這個節(jié)點中包含不連續(xù)的物理內(nèi)存地址的頁面數(shù);
  • node_present_pages 是真正可用的物理頁面的數(shù)目。

例如,64M 物理內(nèi)存隔著一個 4M 的空洞,然后是另外的 64M 物理內(nèi)存。這樣換算成頁面數(shù)目就是,16K 個頁面隔著 1K 打開頁面,然后是另外 16K 個頁面。這種情況下,node_spanned_pages 就是 33K 個頁面,node_present_pages 就是 32K 個頁面。

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;

每一個節(jié)點分成一個個區(qū)域 zone,放在數(shù)組 node_zones 里面。這個數(shù)組的大小為 MAX_NR_ZONES。我們來看區(qū)域的定義。

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(Direct Memory Access,直接內(nèi)存存取)的內(nèi)存。DMA 是這樣一種機制:要把外設(shè)的數(shù)據(jù)讀入內(nèi)存或把內(nèi)存的數(shù)據(jù)傳送到外設(shè),原來都要通過 CPU 控制完成,但是這會占用空間 CPU,影響 CPU 處理其他事情,所以有了 DMA 模式。CPU 只需向 DMA 控制器下達指令,讓 DMA 控制器來處理數(shù)據(jù)的傳送,數(shù)據(jù)傳送完畢后再把信息反饋給 CPU,這樣就可以解放 CPU。

對于 64 位系統(tǒng),有兩個 DMA 區(qū)域。除了上面說的 ZONE_DMA,還有 ZONE_DMA32。在這里你大概能理解 DMA 的原理就可以,不必糾結(jié),我們后面會講 DMA 的機制。

ZONE_NORMAL 是直接映射區(qū),就是上一節(jié)講的,從物理內(nèi)存到虛擬內(nèi)存的內(nèi)核區(qū)域,通過加上一個常量直接映射。

ZONE_HIGHMEM 是高端內(nèi)存區(qū),就是上一節(jié)講的,對于 32 位系統(tǒng)來說超過 896M 的地方,對于 64 未必要有的一段區(qū)域。

ZONE_MOVABLE 是可移動區(qū)域,通過將物理內(nèi)存劃分為可移動分配區(qū)域和不可移動分配區(qū)域來避免內(nèi)存碎片。

這里你需要注意一下,我們剛才對于區(qū)域的劃分,都是針對物理內(nèi)存的。

nr_zones 表示當前節(jié)點的區(qū)域的數(shù)量。node_zonelists 是備用節(jié)點和它的內(nèi)存區(qū)域的情況。前面講 NUMA 的時候,我們講了 CPU 訪問內(nèi)存,本節(jié)點速度最快,但是如果本節(jié)點內(nèi)存不夠怎么辦,還是需要去其他節(jié)點進行分配。畢竟,就算在備用節(jié)點里面選擇,慢了點也比沒有強。

既然整個內(nèi)存被分成了多個節(jié)點,那 pglist_data 應(yīng)該放在一個數(shù)組里面。每個節(jié)點一項,就像下面代碼里面一樣:

struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;

區(qū)域

到這里,我們把內(nèi)存分成了節(jié)點,把節(jié)點分成了區(qū)域。接下來我們來看,一個區(qū)域里面是如何組織的。

表示區(qū)域的數(shù)據(jù)結(jié)構(gòu) 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 里面,zone_start_pfn 表示屬于這個 zone 的第一個頁。

如果我們仔細看代碼的注釋,可以看到,spanned_pages = zone_end_pfn - zone_start_pfn,也即 spanned_pages 指的是不管中間有沒有物理內(nèi)存空洞,反正就是最后的頁號減去起始的頁號。

present_pages = spanned_pages - absent_pages(pages in holes),也即 present_pages 是這個 zone 在物理內(nèi)存中真實存在的所有 page 數(shù)目。

managed_pages = present_pages - reserved_pages,也即 managed_pages 是這個 zone 被伙伴系統(tǒng)管理的所有的 page 數(shù)目,伙伴系統(tǒng)的工作機制我們后面會講。

per_cpu_pageset 用于區(qū)分冷熱頁。什么叫冷熱頁呢?咱們講 x86 體系結(jié)構(gòu)的時候講過,為了讓 CPU 快速訪問段描述符,在 CPU 里面有段描述符緩存。CPU 訪問這個緩存的速度比內(nèi)存快得多。同樣對于頁面來講,也是這樣的。如果一個頁被加載到 CPU 高速緩存里面,這就是一個熱頁(Hot Page),CPU 讀起來速度會快很多,如果沒有就是冷頁(Cold Page)。由于每個 CPU 都有自己的高速緩存,因而 per_cpu_pageset 也是每個 CPU 一個。

了解了區(qū)域 zone,接下來我們就到了組成物理內(nèi)存的基本單位,頁的數(shù)據(jù)結(jié)構(gòu) struct page。這是一個特別復雜的結(jié)構(gòu),里面有很多的 union,union 結(jié)構(gòu)是在 C 語言中被用于同一塊內(nèi)存根據(jù)情況保存不同類型數(shù)據(jù)的一種方式。這里之所以用了 union,是因為一個物理頁面使用模式有多種。

**第一種模式,要用就用一整頁。**這一整頁的內(nèi)存,或者直接和虛擬地址空間建立映射關(guān)系,我們把這種稱為匿名頁(Anonymous Page)。或者用于關(guān)聯(lián)一個文件,然后再和虛擬地址空間建立映射關(guān)系,這樣的文件,我們稱為內(nèi)存映射文件(Memory-mApped File)。

如果某一頁是這種使用模式,則會使用 union 中的以下變量:

  • struct address_space *mapping 就是用于內(nèi)存映射,如果是匿名頁,最低位為 1;如果是映射文件,最低位為 0;
  • pgoff_t index 是在映射區(qū)的偏移量;
  • atomic_t _mapcount,每個進程都有自己的頁表,這里指有多少個頁表項指向了這個頁;
  • struct list_head lru 表示這一頁應(yīng)該在一個鏈表上,例如這個頁面被換出,就在換出頁的鏈表中;
  • compound 相關(guān)的變量用于復合頁(Compound Page),就是將物理上連續(xù)的兩個或多個頁看成一個獨立的大頁。
  •  

**第二種模式,僅需分配小塊內(nèi)存。**有時候,我們不需要一下子分配這么多的內(nèi)存,例如分配一個 task_struct 結(jié)構(gòu),只需要分配小塊的內(nèi)存,去存儲這個進程描述結(jié)構(gòu)的對象。為了滿足對這種小內(nèi)存塊的需要,Linux 系統(tǒng)采用了一種被稱為slab allocator的技術(shù),用于分配稱為 slab 的一小塊內(nèi)存。它的基本原理是從內(nèi)存管理模塊申請一整塊頁,然后劃分成多個小塊的存儲池,用復雜的隊列來維護這些小塊的狀態(tài)(狀態(tài)包括:被分配了 / 被放回池子 / 應(yīng)該被回收)。

也正是因為 slab allocator 對于隊列的維護過于復雜,后來就有了一種不使用隊列的分配器 slub allocator,后面我們會解析這個分配器。但是你會發(fā)現(xiàn),它里面還是用了很多 slab 的字眼,因為它保留了 slab 的用戶接口,可以看成 slab allocator 的另一種實現(xiàn)。

還有一種小塊內(nèi)存的分配器稱為slob,非常簡單,主要使用在小型的嵌入式系統(tǒng)。

如果某一頁是用于分割成一小塊一小塊的內(nèi)存進行分配的使用模式,則會使用 union 中的以下變量:

  • s_mem 是已經(jīng)分配了正在使用的 slab 的第一個對象;
  • freelist 是池子中的空閑對象;
  • rcu_head 是需要釋放的列表。
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 */
    	};
    ......
    }

頁的分配

好了,前面我們講了物理內(nèi)存的組織,從節(jié)點到區(qū)域到頁到小塊。接下來,我們來看物理內(nèi)存的分配。

對于要分配比較大的內(nèi)存,例如到分配頁級別的,可以使用伙伴系統(tǒng)(Buddy System)。

Linux 中的內(nèi)存管理的“頁”大小為 4KB。把所有的空閑頁分組為 11 個頁塊鏈表,每個塊鏈表分別包含很多個大小的頁塊,有 1、2、4、8、16、32、64、128、256、512 和 1024 個連續(xù)頁的頁塊。最大可以申請 1024 個連續(xù)頁,對應(yīng) 4MB 大小的連續(xù)內(nèi)存。每個頁塊的第一個頁的物理地址是該頁塊大小的整數(shù)倍。

物理內(nèi)存的組織方式:每個頁面都有一個結(jié)構(gòu),每一個都有節(jié)點

 

第 i 個頁塊鏈表中,頁塊中頁的數(shù)目為 2^i。

在 struct zone 里面有以下的定義:

struct free_area	free_area[MAX_ORDER];

MAX_ORDER 就是指數(shù)。

#define MAX_ORDER 11

當向內(nèi)核請求分配 (2^(i-1),2^i] 數(shù)目的頁塊時,按照 2^i 頁塊請求處理。如果對應(yīng)的頁塊鏈表中沒有空閑頁塊,那我們就在更大的頁塊鏈表中去找。當分配的頁塊中有多余的頁時,伙伴系統(tǒng)會根據(jù)多余的頁塊大小插入到對應(yīng)的空閑頁塊鏈表中。

例如,要請求一個 128 個頁的頁塊時,先檢查 128 個頁的頁塊鏈表是否有空閑塊。如果沒有,則查 256 個頁的頁塊鏈表;如果有空閑塊的話,則將 256 個頁的頁塊分成兩份,一份使用,一份插入 128 個頁的頁塊鏈表中。如果還是沒有,就查 512 個頁的頁塊鏈表;如果有的話,就分裂為 128、128、256 三個頁塊,一個 128 的使用,剩余兩個插入對應(yīng)頁塊鏈表。

上面這個過程,我們可以在分配頁的函數(shù) 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;
}

alloc_pages 會調(diào)用 alloc_pages_current,這里面的注釋比較容易看懂了,gfp 表示希望在哪個區(qū)域中分配這個內(nèi)存:

  • GFP_USER 用于分配一個頁映射到用戶進程的虛擬地址空間,并且希望直接被內(nèi)核或者硬件訪問,主要用于一個用戶進程希望通過內(nèi)存映射的方式,訪問某些硬件的緩存,例如顯卡緩存;
  • GFP_KERNEL 用于內(nèi)核中分配頁,主要分配 ZONE_NORMAL 區(qū)域,也即直接映射區(qū);
  • GFP_HIGHMEM,顧名思義就是主要分配高端區(qū)域的內(nèi)存。

另一個參數(shù) order,就是表示分配 2 的 order 次方個頁。

接下來調(diào)用 __alloc_pages_nodemask。這是伙伴系統(tǒng)的核心方法。它會調(diào)用 get_page_from_freelist。這里面的邏輯也很容易理解,就是在一個循環(huán)中先看當前節(jié)點的 zone。如果找不到空閑頁,則再看備用節(jié)點的 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,都有伙伴系統(tǒng)維護的各種大小的隊列,就像上面伙伴系統(tǒng)原理里講的那樣。這里調(diào)用 rmqueue 就很好理解了,就是找到合適大小的那個隊列,把頁面取下來。

接下來的調(diào)用鏈是 rmqueue->__rmqueue->__rmqueue_smallest。在這里,我們能清楚看到伙伴系統(tǒng)的邏輯。

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

從當前的 order,也即指數(shù)開始,在伙伴系統(tǒng)的 free_area 找 2^order 大小的頁塊。如果鏈表的第一個不為空,就找到了;如果為空,就到更大的 order 的頁塊鏈表里面去找。找到以后,除了將頁塊從鏈表中取下來,我們還要把多余的的部分放到其他頁塊鏈表里面。expand 就是干這個事情的。area–就是伙伴系統(tǒng)那個表里面的前一項,前一項里面的頁塊大小是當前項的頁塊大小除以 2,size 右移一位也就是除以 2,list_add 就是加到鏈表上,nr_free++ 就是計數(shù)加 1。

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);
	}
}

分享到:
標簽:物理 內(nèi)存
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨大挑戰(zhàn)2018-06-03

數(shù)獨一種數(shù)學游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數(shù)有氧達人2018-06-03

記錄運動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定