1. 程式人生 > >Linux記憶體管理-linuxnote03

Linux記憶體管理-linuxnote03

linuxnote01linuxnote02中學習了Linux中程序執行緒的描述,並學習了Linux中如何進行程序管理,學習理解的過程中,我們有收穫也有疑惑,比如什麼是程序的地址空間這個地址空間存在哪兒,什麼是核心棧,頁表是什麼?等等的問題,其中一些問題都是與Linux中記憶體管理相關,因此linuxnote03開始學習Linux記憶體管理相關的知識。本篇及後續所有的Linux學習都基於”LINUX核心修煉之道任橋偉著“和”Linux核心設計與實現“這兩本書,作為一個Linux的一個初學者,非常感激前人留下的寶貴經驗。)

記憶體管理概述

來源https://blog.csdn.net/y0608122008/article/details/78925909

記憶體是CPU能夠訪問的大容量高速儲存區域,是Linux核心所管理的重要資源之一。初識記憶體這個概念的時候,感覺非常模糊,外加自身一些理解的RAM,ROM,eMMC,UFS,快閃記憶體等等相關的概念名詞,也許這篇文章”

淺談eMMC,SSD,UFS“可以初步解答我們的一些疑惑。結合上圖,本文所介紹的就是中間區域的記憶體在Linux中管理所涉及到的相關知識。

Page頁&Zone區

頁,看到這個名詞,很容易聯想到我們日常所用的書籍,由於對實體記憶體結構方面的缺乏,暫且就將用到的實體記憶體比作書籍,實體記憶體實實在在,在Linux MMU(記憶體管理單元)記憶體管理中以頁為最小單位管理實體記憶體,就好比一本書由很多頁組成。目前Linux中1page=4kb。Linux核心中以struct page結構體描述一個物理頁,我們一定記住該結構體僅與物理頁相關,與後續所說的虛擬頁無關。該結構體只用來描述一個物理頁是否空閒,表明該頁是否空閒,以及擁有者。

Linux的記憶體結構可以很好的支援NUMA的伺服器。NUMA下對於SMP系統,每一個CPU對應一個物理儲存,一個實體記憶體結點就稱為Node,而通常的單機系統為UMA就是一個Node。每個Node下實體記憶體分成幾個Zone,Zone再對物理葉Page就行管理,因此Linux用了Node->Zone->Page三層結構來描述實體記憶體。(本文涉及的一些關鍵名詞請參考”Linux中關鍵名字解釋-linuxnote00“中的解釋)由於硬體和體系結構的原因,一些在特定地址範圍內的物理頁在使用時有所限制,為此Linux一般將實體記憶體劃分三個Zone:ZONE_DMA(0~16MB)DMA記憶體分配的區域,ZONE_NORMAL(16~896MB)正常對映的記憶體區域,ZONE_HIGHMEM(896~)高階記憶體區域,其中的頁不能永久對映到核心地址空間,只做動態對映。核心中使用struct pglist_data pg_data_t來描述每個Node的記憶體,使用結構體struct zone來描述Zone。

疑惑點整理:核心空間是什麼?

最基礎的獲取記憶體頁的方法!

核心函式(以頁為單位分配記憶體):

struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)//定義於linux/gfp.h

該函式分配2^order(1<<order)個連續的物理頁,並返回一個指向地一個物理頁page結構體的指標。如果出錯,就返回null。使用如下函式把指定頁轉化成它的邏輯地址:

void *page_address(const struct page *page)//定義於linux/mm.h

返回一個指標,指向給定物理頁當前所在的邏輯地址。如果不需要用到結構體struct page,我們可以使用如下函式直接獲取一個紙指向第一個頁的邏輯地址:

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)//定義於mm/page_alloc.c

該函式與alloc_pages作用相同,不同的是alloc_pages返回的是指向第一個物理頁結構體的指標,後者是指向第一個頁的邏輯地址。對應這兩個函式的簡化如下:

alloc_page(gfp_t gfp_mask)//定義於linux/gfp.h
__get_free_page(gfp_t gfp_mask)//定義於linux/gfp.h

這兩個與之前的區別在於後面的引數order=0,意思只獲取一頁。

以上分配方法中分配的物理頁都是包含一些隨機的垃圾資訊,因為未對page結構體中的資訊做任何初始化,那麼我們就可以使用

unsigned long get_zeroed_page(gfp_t gfp_mask)//定義於mm/page_alloc.c
{
	return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}

其實該函式也就對gfp_mask增加一個引數而已,使用該方法,可以將頁的所有資訊都填充為0,一般如果分配的頁是給使用者空間的時候,會非常有用。

有分配就有釋放!

當不需要頁的時候,我們需要釋放頁,對應的函式如下:

void __free_pages(struct page *page, unsigned int order)
{
	if (put_page_testzero(page)) {
		if (order == 0)
			free_hot_cold_page(page, false);
		else
			__free_pages_ok(page, order);
	}
}

void free_pages(unsigned long addr, unsigned int order)
{
	if (addr != 0) {
		VM_BUG_ON(!virt_addr_valid((void *)addr));
		__free_pages(virt_to_page((void *)addr), order);
	}
}

#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)

釋放頁的時候,要謹慎,只能釋放我們分配的對應頁。如果引數中傳遞了錯誤的struct page或地址 ,用了錯誤的order,很有可能導致系統崩潰。

基於基礎分配釋放記憶體方法的包裝

kmalloc,該方法定義與slab.h中。

static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
	if (__builtin_constant_p(size)) {
		if (size > KMALLOC_MAX_CACHE_SIZE)
			return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
		if (!(flags & GFP_DMA)) {
			int index = kmalloc_index(size);

			if (!index)
				return ZERO_SIZE_PTR;

			return kmem_cache_alloc_trace(kmalloc_caches[index],
					flags, size);
		}
#endif
	}
	return __kmalloc(size, flags);
}

在前面最基礎的分配記憶體的方式中,我們介紹了,前面分配記憶體的方式,都是基於物理頁的,以頁為單位,並且分配的物理頁在實體地址上也連續的,對應的邏輯地址上也是連續的。kmalloc是以位元組為單位獲取一塊核心記憶體。kmalloc返回一個指向記憶體塊的指標,其記憶體塊至少要有size大小,分配的記憶體區在物理上都是連續的,出錯時返回null。一般都會分配成功,除非記憶體不足時,因此在呼叫完kmalloc之後,必須檢查返回是否為null,並做適當的處理。kmalloc的另一端就是kfree。kfree函式釋放有kmalloc分配出來的記憶體,否則會出錯。以下就是一個正確使用kmalloc和kfree的例子:

struct gfm_demo *test;
test = kmalloc(sizeof(test), gfp_mask);
if (!p)
kfree(test);

vmalloc,vmalloc分配的記憶體邏輯地址是連續的,但實體地址可以不是連續的。vmalloc只能確保邏輯獲得記憶體在虛擬地址空間內是連續的,它通過分配非連續的實體記憶體塊,再”修正“頁表,把記憶體對映到邏輯地址空間的連續區域中,來保證在虛擬地址空間內是連續性。vmalloc定義於vmalloc.c中,對應的釋放記憶體函式是vfree

void *vmalloc(unsigned long size)
{
	return __vmalloc_node_flags(size, NUMA_NO_NODE,
				    GFP_KERNEL | __GFP_HIGHMEM);
}

gfp_t gfp_mask標記

一般核心分配釋放記憶體時,程式碼中最多的還是使用kmalloc和vmalloc,當然Linux核心中還是很多對基礎方法的包裝,不管包裝成什麼樣,其原理是一致的。使用這些方法的時候,我們都會使用一個標記gfp_t gfp_mask,這個標記按其作用可以分為三類:行為修飾符,區修飾符和型別。行為修飾符表示核心應該如何分配所需的記憶體,在某些特定的情況下,我們只能使用某些特定的方法進行記憶體分配,比如中斷程式就要求核心在分配記憶體分配的時候不能睡眠(因為中斷處理程式不能被重新排程)。區修飾符表示從哪兒分配記憶體,在介面的介紹中,我們說Linux是通過Node->Zone->Page進行管理記憶體的,Linux核心將記憶體分成不同的幾個區,區修飾符就是指明到底從這些區中的哪一個區進行分配。型別則是行為和區的一個組合,將各種可能用到的組合歸納為不同的型別,簡化使用過程。

通過上述的學習,我們知道了什麼,有什麼疑惑?

通過上述的介紹,我們瞭解了本文所介紹的是基於實體記憶體條記憶體的管理方法,就是文章唯一圖片所顯示的記憶體構成中的最中間的那種型別的記憶體。我們初步瞭解,Linux核心是通過Node->Zone->Page三層結構來管理記憶體的,Page對應實實在在的物理頁,Zone是對實體記憶體的進一步劃分,以滿足不同的需求,而Node在不同的架構和體系中都是不一樣的,UMA中SMP系統每個CPU通過匯流排連線到一塊物理連續的記憶體上,該系統中只有一個Node,NUMA中,每個CPU都對應一個Node節點,意思就是每個CPU都有自己的記憶體。掌握了最基本的分配釋放記憶體的函式方法,當然Linux核心中還提供很多分配和釋放的方法,往往每一種分配和釋放的函式方法都是相對應的。如果我們想獲取連續的物理頁時,我們可以使用最基礎的分配方法或者kmalloc。如果你對物理結構上的頁是否連續並沒有要求,你可以使用vmalloc,它獲取的是邏輯地址連續的記憶體,vmalloc通過修正頁表來保證邏輯地址的連續性。我們可以配合不同的gfp_t gfp_mask標記,進行不同型別的記憶體分配方式,以及規定在不同的區內進行記憶體分配。前面介紹程序的時候,我們介紹了程序排程的時候,區分不同的程序,對應呼叫不同的排程器,那麼在記憶體管理的時候以此類推,我們是否存在不同的記憶體分配器呢?(不同的分配器其實就是不同的策略,不同的演算法)當然學習過程中,存在疑惑都是正常,是否和博主一樣,對文章中提及的頁表這一名詞,一直存在疑問?它到底是什麼?接下來我們就慢慢解決這些個思考和疑惑。

記憶體分配的優化之Buddy管理演算法

Buddy演算法是一種經典的記憶體管理演算法,其作用是減少儲存空間中的空洞和碎片,增加利用率。Buddy把記憶體中所有的頁面按照2的冪次放進行分塊管理,分配的時候如果沒有找到相應大小的塊,就把大的塊分成小塊,釋放的時候,回收的塊跟相鄰的空閒夥伴塊又能合併成大塊。這就是Buddy的命名的由來。具體的來說,Buddy把所有的物理空閒頁分成了11個塊連結串列,每個塊連結串列分別包含了大小1,2,4,8,16,32,64,128,256,512和1024個連續的頁。因此限制了最大申請的記憶體為1024頁及對應4M的連續記憶體塊。每個塊的第一個頁的實體地址是該塊大小的整數倍。如,大小為16個頁的塊,其起始地址是16*4096的倍數。直接介紹的struct zone結構體中的free_area域,他是一個數組大小為11,記錄了實體記憶體上對應大小的空閒塊。如下所示

free area[MAX_ORDER]//MAX_ORDER=11                           物理頁面
                                                                .
├─────────┬                                                     .
│         │                                                     .
│ 10      │                                                     .
├─────────├                                                     .
│         │                                                     .
│ 9       │                                                     .
├─────────├                                                     .
│         │                                                     .
│ 8       │                                                     .
├─────────├                                                     .
│         │                                                     .
│ 7       │                                                     .
├─────────├                                                     .
│         │                                                     .
│ 6       │                                                     .
├─────────├                                                     .
│         │                                                     .
│ 5       │                                                     .
├─────────├                                                     .
│         │                                                     .
│ 4       │                                                     .
├─────────├                                                     .
│         │                                                     .
│ 3       │                                                 ├─────────├
├─────────├                                             ────│   4     │
│         │       ├────────────────────────────────────│    ├─────────├
│ 2       │       │                                    └────│   3     │
├─────────├       ├─────┬       ├─────┬                     ├─────────├
│         │──────>│  3  │──────>│  1  │                  ───│   2     │
│ 1       │       │ page│       │ page│─────────────────│   ├─────────├
│         │<──────└─────├ <─────└─────├                 └───│   1     │
│         │                                                 ├─────────├ 
├─────────├                                                 │   0     │               
│         │       ├─────┬───────────────────────────────────├─────────├
│ 0       │──────>│  0  │
│         │       │ page│
│         │<──────└─────├  
├─────────├

陣列0指向所有空閒的單頁記憶體連結串列,陣列1指向所有空閒的2頁記憶體連結串列,依次往上。當需要分配一個頁的記憶體的時候,直接從陣列0中獲取到0號page,如果分配一個頁面的時候不存在0號page,則從陣列1中拿到兩個頁面大小的記憶體,分裂成兩個頁面大小為1頁的記憶體塊,放於陣列0中。同樣分配n個(n<11)頁面的時候,以此類推。反過來,釋放的時候,我們需要合併一些空閒的記憶體頁。因為都是二分的,所以每個頁塊只有一個buddy,相關索引計算如下:

__find_buddy_index(unsigned long page_idx, unsigned int order)
{
	return page_idx ^ (1 << order);
}

假如頁面1是空閒的,計算出它的buddy頁塊是頁面0,也是空閒的,則進行合併得到一個order=1(即兩個頁面組成的頁塊)的頁塊。夥伴演算法管理的記憶體管理的記憶體適合大塊的記憶體,但如果對小記憶體管理,如幾個幾十幾百個位元組的空間。如直接給一個頁則導致記憶體空間的浪費。linux針對小記憶體的管理,提供了另一個專門的分配器,slab分配器。

疑惑點整理:若干,雖然瞭解了buddy演算法的思想,但對其實現細節,未能完全掌握,計劃後續寫一個詳細介紹該演算法的文章。

slab/slob/slub分配器

在linux的不斷優化過程中,目前Linux提供配置一組slab/slob/slub的選擇,不同的分配器在不同的應用場景下有著不同的應用。我們這裡簡單的瞭解一下slab分配器的基本原理,slab分配器申請了一系列的連續實體記憶體,該記憶體儲存在系統的快取記憶體當中,當需要小記憶體從這些快取記憶體中直接申請。slab分配器並不直接釋放已分配的內容,而是等到系統的核心執行緒週期性的掃描快取記憶體並釋放slab的內容。

疑惑點整理:slab/slob/slub分配器,上同buddy演算法,後續專門分析

程序地址空間,是時候解決一下之前的疑惑了!

以32bit的Linux為例,Linux所能管理的虛擬記憶體最大為2^32(4G=1024*1024*1024*4),Linux將3-4G(0xC0000000~0xFFFFFFFF)的空間分配給核心,將0-3G(0x00000000~0xBFFFFFFF)的空間分配給使用者。因此3-4G的空間稱為核心空間,0~3G的空間稱為使用者空間。核心空間是所有程序共享的,系統只存在一份,而使用者空間每個程序都有各自獨立的3G虛擬地址空間。不管是核心空間還是使用者空間,他們都是通過一定的方法將虛擬記憶體的地址對映到對應的實實在在的實體記憶體頁之上,目前採用的技術就是分頁。MMU通過對應的頁表將虛擬地址轉化成實體地址。在每個程序的虛擬地址空間內,程序根據不同的作用,將地址範圍分成了不同的記憶體區域,包括程式碼段,資料段,BSS段,堆,棧……資料段中包括了所有靜態分配的資料空間,即全域性變數和所有申明為static的區域性變數。BSS段則是一些為初始化的全域性變數,之前的程序管理文章中,我們介紹過Linux核心用task_struct結構體用來描述程序,在task_struct中對應也有程序地址空間資訊的域,Linux核心使用struct mm_struct結構體來描述一個程序的地址空間資訊,具體我們來看下這個結構體:(刪除一些資訊)

struct mm_struct {
	struct vm_area_struct *mmap;		/* list of VMAs */
	struct rb_root mm_rb;
	u32 vmacache_seqnum;                   /* per-thread vmacache */
#ifdef CONFIG_MMU
	unsigned long (*get_unmapped_area) (struct file *filp,
				unsigned long addr, unsigned long len,
				unsigned long pgoff, unsigned long flags);
#endif
	unsigned long mmap_base;		/* base of mmap area */
	unsigned long mmap_legacy_base;         /* base of mmap area in bottom-up allocations */
	unsigned long task_size;		/* size of task vm space */
	unsigned long highest_vm_end;		/* highest vma end address */
	pgd_t * pgd;
	atomic_t mm_users;			/* How many users with user space? */
	atomic_t mm_count;			/* How many references to "struct mm_struct" (users count as 1) */
	atomic_long_t nr_ptes;			/* PTE page table pages */
#if CONFIG_PGTABLE_LEVELS > 2
	atomic_long_t nr_pmds;			/* PMD page table pages */
#endif
	int map_count;				/* number of VMAs */

	spinlock_t page_table_lock;		/* Protects page tables and some counters */
	struct rw_semaphore mmap_sem;

	struct list_head mmlist;		/* List of maybe swapped mm's.	These are globally strung
						 * together off init_mm.mmlist, and are protected
						 * by mmlist_lock
						 */


	unsigned long hiwater_rss;	/* High-watermark of RSS usage */
	unsigned long hiwater_vm;	/* High-water virtual memory usage */

	unsigned long total_vm;		/* Total pages mapped */
	unsigned long locked_vm;	/* Pages that have PG_mlocked set */
	unsigned long pinned_vm;	/* Refcount permanently increased */
	unsigned long shared_vm;	/* Shared pages (files) */
	unsigned long exec_vm;		/* VM_EXEC & ~VM_WRITE */
	unsigned long stack_vm;		/* VM_GROWSUP/DOWN */
	unsigned long def_flags;
	unsigned long start_code, end_code, start_data, end_data;
	unsigned long start_brk, brk, start_stack;
	unsigned long arg_start, arg_end, env_start, env_end;

	unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
};

 我們暫且先關注這幾個欄位:mmap和mm_rb都是用來描述程序地址空間內的區域塊(如下使用pmap某個程序號和cat 對應程序下的maps輸出資訊,每一行資訊都與一個vm_area_struct結構體對應,代表一個記憶體區域),不同是mmap用的是連結串列形式表示,mm_rb用的是紅黑樹表示,如此空間冗餘目的還是以空間換取時間,提高效率,連結串列可以高效率的遍歷,紅黑樹則方便查詢。pgd則對應分頁,頁表相關對映資訊。

pmap 7636
7636:   adb logcat -b all
000055655826f000   2212K r-x-- adb
0000556558498000     92K r---- adb
00005565584af000      4K rw--- adb
00005565584b0000     12K rw---   [ anon ]
000055655a2ef000    132K rw---   [ anon ]
00007f725379b000   1792K r-x-- libc-2.23.so
00007f725395b000   2048K ----- libc-2.23.so
00007f7253b5b000     16K r---- libc-2.23.so
00007f7253b5f000      8K rw--- libc-2.23.so
00007f7253b61000     16K rw---   [ anon ]
00007f7253b65000     88K r-x-- libgcc_s.so.1
00007f7253b7b000   2044K ----- libgcc_s.so.1
00007f7253d7a000      4K rw--- libgcc_s.so.1
00007f7253d7b000     28K r-x-- librt-2.23.so
00007f7253d82000   2044K ----- librt-2.23.so
00007f7253f81000      4K r---- librt-2.23.so
00007f7253f82000      4K rw--- librt-2.23.so
00007f7253f83000   1056K r-x-- libm-2.23.so
00007f725408b000   2044K ----- libm-2.23.so
00007f725428a000      4K r---- libm-2.23.so
00007f725428b000      4K rw--- libm-2.23.so
00007f725428c000     96K r-x-- libpthread-2.23.so
00007f72542a4000   2044K ----- libpthread-2.23.so
00007f72544a3000      4K r---- libpthread-2.23.so
00007f72544a4000      4K rw--- libpthread-2.23.so
00007f72544a5000     16K rw---   [ anon ]
00007f72544a9000     12K r-x-- libdl-2.23.so
00007f72544ac000   2044K ----- libdl-2.23.so
00007f72546ab000      4K r---- libdl-2.23.so
00007f72546ac000      4K rw--- libdl-2.23.so
00007f72546ad000    152K r-x-- ld-2.23.so
00007f72548af000     20K rw---   [ anon ]
00007f72548d2000      4K r---- ld-2.23.so
00007f72548d3000      4K rw--- ld-2.23.so
00007f72548d4000      4K rw---   [ anon ]
00007ffc7add9000    136K rw---   [ stack ]
00007ffc7adfb000     12K r----   [ anon ]
00007ffc7adfe000      8K r-x--   [ anon ]
ffffffffff600000      4K r-x--   [ anon ]

pmap資訊分為四行:區域其實地址,區域大小,區域屬性,記憶體區域對映的檔案。部分資訊解釋:[anon]一般表示匿名的記憶體對映,指的是動態生成的內容所佔的記憶體,比如堆。[stack]表示棧區域。一般認為具有rx屬性的是程式的程式碼段,具有w可能是資料段,BSS段等等。觀察每段記憶體區域的大小,不免會發現,其大小都是實體記憶體頁大小(4k)的整數倍。

cat maps 
55655826f000-556558498000 r-xp 00000000 fc:00 20467712                   /usr/bin/adb
556558498000-5565584af000 r--p 00228000 fc:00 20467712                   /usr/bin/adb
5565584af000-5565584b0000 rw-p 0023f000 fc:00 20467712                   /usr/bin/adb
5565584b0000-5565584b3000 rw-p 00000000 00:00 0 
55655a2ef000-55655a310000 rw-p 00000000 00:00 0                          [heap]
7f725379b000-7f725395b000 r-xp 00000000 fc:00 19005614                   /lib/x86_64-linux-gnu/libc-2.23.so
7f725395b000-7f7253b5b000 ---p 001c0000 fc:00 19005614                   /lib/x86_64-linux-gnu/libc-2.23.so
7f7253b5b000-7f7253b5f000 r--p 001c0000 fc:00 19005614                   /lib/x86_64-linux-gnu/libc-2.23.so
7f7253b5f000-7f7253b61000 rw-p 001c4000 fc:00 19005614                   /lib/x86_64-linux-gnu/libc-2.23.so
7f7253b61000-7f7253b65000 rw-p 00000000 00:00 0 
7f7253b65000-7f7253b7b000 r-xp 00000000 fc:00 19005505                   /lib/x86_64-linux-gnu/libgcc_s.so.1
7f7253b7b000-7f7253d7a000 ---p 00016000 fc:00 19005505                   /lib/x86_64-linux-gnu/libgcc_s.so.1
7f7253d7a000-7f7253d7b000 rw-p 00015000 fc:00 19005505                   /lib/x86_64-linux-gnu/libgcc_s.so.1
7f7253d7b000-7f7253d82000 r-xp 00000000 fc:00 19005637                   /lib/x86_64-linux-gnu/librt-2.23.so
7f7253d82000-7f7253f81000 ---p 00007000 fc:00 19005637                   /lib/x86_64-linux-gnu/librt-2.23.so
7f7253f81000-7f7253f82000 r--p 00006000 fc:00 19005637                   /lib/x86_64-linux-gnu/librt-2.23.so
7f7253f82000-7f7253f83000 rw-p 00007000 fc:00 19005637                   /lib/x86_64-linux-gnu/librt-2.23.so
7f7253f83000-7f725408b000 r-xp 00000000 fc:00 19005605                   /lib/x86_64-linux-gnu/libm-2.23.so
7f725408b000-7f725428a000 ---p 00108000 fc:00 19005605                   /lib/x86_64-linux-gnu/libm-2.23.so
7f725428a000-7f725428b000 r--p 00107000 fc:00 19005605                   /lib/x86_64-linux-gnu/libm-2.23.so
7f725428b000-7f725428c000 rw-p 00108000 fc:00 19005605                   /lib/x86_64-linux-gnu/libm-2.23.so
7f725428c000-7f72542a4000 r-xp 00000000 fc:00 19005612                   /lib/x86_64-linux-gnu/libpthread-2.23.so
7f72542a4000-7f72544a3000 ---p 00018000 fc:00 19005612                   /lib/x86_64-linux-gnu/libpthread-2.23.so
7f72544a3000-7f72544a4000 r--p 00017000 fc:00 19005612                   /lib/x86_64-linux-gnu/libpthread-2.23.so
7f72544a4000-7f72544a5000 rw-p 00018000 fc:00 19005612                   /lib/x86_64-linux-gnu/libpthread-2.23.so
7f72544a5000-7f72544a9000 rw-p 00000000 00:00 0 
7f72544a9000-7f72544ac000 r-xp 00000000 fc:00 19005616                   /lib/x86_64-linux-gnu/libdl-2.23.so
7f72544ac000-7f72546ab000 ---p 00003000 fc:00 19005616                   /lib/x86_64-linux-gnu/libdl-2.23.so
7f72546ab000-7f72546ac000 r--p 00002000 fc:00 19005616                   /lib/x86_64-linux-gnu/libdl-2.23.so
7f72546ac000-7f72546ad000 rw-p 00003000 fc:00 19005616                   /lib/x86_64-linux-gnu/libdl-2.23.so
7f72546ad000-7f72546d3000 r-xp 00000000 fc:00 19005610                   /lib/x86_64-linux-gnu/ld-2.23.so
7f72548af000-7f72548b4000 rw-p 00000000 00:00 0 
7f72548d2000-7f72548d3000 r--p 00025000 fc:00 19005610                   /lib/x86_64-linux-gnu/ld-2.23.so
7f72548d3000-7f72548d4000 rw-p 00026000 fc:00 19005610                   /lib/x86_64-linux-gnu/ld-2.23.so
7f72548d4000-7f72548d5000 rw-p 00000000 00:00 0 
7ffc7add9000-7ffc7adfb000 rw-p 00000000 00:00 0                          [stack]
7ffc7adfb000-7ffc7adfe000 r--p 00000000 00:00 0                          [vvar]
7ffc7adfe000-7ffc7ae00000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

 pmap命令和程序maps資訊都是一樣的,都能檢視程序地址空間記憶體區域的對映情況。比如:第一行pmap命令獲取的資訊2212k=(556558498000 -  55655826f000)這是十六進位制數字= 93893761269760−93893759004672 對應十進位制= 2265088 b。雖然我們很直觀的瞭解了程序地址空間的組成,但是這些記憶體區域是如何建立起來的呢?答案就是通過mmap系統呼叫,mmap系統呼叫的最終目的是將程序所對應的程式檔案的程式碼段,資料段等資訊(這些資訊在程式編譯時就已經確定)對映到使用者程序的虛擬地址空間。執行完mmap,僅僅是分配了虛擬記憶體而已,並沒有分配對應的物理頁面對應的頁表也沒有建立。當實際訪問新對映的頁面時,如果沒有查詢到相應的物理頁面,就會丟擲一個缺頁異常,使用者程序陷入核心態,進行頁表建立和實體記憶體的分配。

疑惑點整理:mmap系統呼叫的時機?頁表如何建立?

寫在最後

通過本篇的學習,我們最起碼掌握了Linux記憶體管理的一些基本的知識,瞭解了Linux中一些基本的分配記憶體的方法,我們知道了,對於一些大塊記憶體,我們使用buddy演算法進行分配回收管理,在分配小塊記憶體的時候(比如程序需要很多的幾百位元組小塊記憶體,我們總不能為了它們每個分配1page),為了避免記憶體浪費,Linux又增加了slab/slub/slob分配器具專門針對這些小塊記憶體的分配回收,學習過程中我們也解決了前面一直困惑我們的問題,程序的地址空間,頁表。當然疑惑也在所難免,本準備一下子將記憶體相關的知識全部學習一下,但實際遇到很多問題,比如buddy演算法的細節實現,slab/slob/slub分配器如何分配記憶體和buddy之間是否存在一定的關係?Linux通過fork+exec的技術就行程序建立,我們知道這些都是在核心態下完成的,exec就是完成的父子程序可執行程式的替換,那麼在mmap系統呼叫的時機在哪裡……長路漫漫,來日方長。

補充:資料結構解釋(持續更新)

結構體
struct page 核心對每一個物理頁使用page結構體來表示
struct zone 核心把系統的頁劃分為區
struct vm_struct 表示的虛擬地址是給核心使用的,地址空間範圍是(3G + 896M + 8M) ~ 4G(一般小於4G),
struct vmap_area 與vm_struct相似,只不過進行一層連結串列和rb樹的包裝,便於遍歷和查詢
struct mm_struct 用於描述一個程序的地址空間資訊的記憶體描述符,地址空間範圍是0~3G
struct vm_area_struct 程序地址空間裡的記憶體區域塊用該結構體描述,通常稱為VMA,表示的是一塊連續的虛擬地址空間區域