前言
在閱讀Android底層源碼,特別是關(guān)于Linux內(nèi)核的代碼時候,如果對Linux內(nèi)核整體上沒有一定的認知,閱讀起來一定很幸苦,本文就總結(jié)一下Linux內(nèi)核內(nèi)存管理系統(tǒng)的總覽闷哆,之后有時間會把這些總覽分為一個個詳細的知識點一一解析其源碼
可以在本文下討論http://www.reibang.com/p/82b4454697ce
正文
為了作為Binder驅(qū)動一文的補充也好,還是為了下一篇的Ashmem的解析鋪墊也好单起,本文繼續(xù)補充一下Linux內(nèi)存系統(tǒng)相關(guān)知識抱怔。很多人可能會分不清Linux的虛擬內(nèi)存和物理內(nèi)存什么時候使用,也弄不清虛擬內(nèi)存為什么叫做虛擬嘀倒,具體和物理內(nèi)存有什么區(qū)別屈留。也不清楚虛擬內(nèi)存為什么可以有4g大小甚至超過內(nèi)存卡中內(nèi)存大小局冰,這是怎么做到的?接下來灌危,我們來討論一下康二。
我們先不用管內(nèi)存中的邏輯地址,線性地址勇蝙,物理地址沫勿。首先在Linux中把內(nèi)存分為2大類,一個是物理內(nèi)存味混,虛擬內(nèi)存产雹。
顧名思義,物理內(nèi)存是指真實的物理設(shè)備中的地址翁锡,虛擬內(nèi)存是一個虛擬的概念概念蔓挖,經(jīng)過內(nèi)核屏蔽后面向應(yīng)用的內(nèi)存,同一個物理地址可能通過映射在不同的進程顯示出來的虛擬地址是不同的盗誊。
因此时甚,我們想要弄清楚Linux的內(nèi)存管理,需要了解三個方面:
- 1.物理內(nèi)存管理
- 2.虛擬內(nèi)存管理
- 3.物理內(nèi)存是如何映射到虛擬內(nèi)存的哈踱,而映射又分為用戶態(tài)的映射以及內(nèi)核態(tài)的映射。
那么Linux內(nèi)核什么使用物理內(nèi)存梨熙,什么時候虛擬內(nèi)存呢开镣?記住雖然內(nèi)核擁有直接訪問物理內(nèi)存的權(quán)限,但是它不會這么做咽扇。換句話說邪财,任何時候訪問都是虛擬內(nèi)存,只有進行物理內(nèi)存綁定和分配的時候會使用物理內(nèi)存质欲。
虛擬內(nèi)存的劃分
首先我們要知道一個進程會使用什么內(nèi)容需要放在內(nèi)存中:
- 1.代碼需要放在內(nèi)存
- 2.全局變量
- 3.常量字符串
- 4.函數(shù)棧
- 5.堆
接下來是內(nèi)核 - 6.內(nèi)核代碼也需要放在內(nèi)存
- 7.內(nèi)核也有全局變量
- 8.每一個進程都需要task_struct
- 9.每一個進程都需要內(nèi)核棧
- 10.內(nèi)核同樣需要動態(tài)分配內(nèi)存
為了保證這些數(shù)據(jù)都有充足的空間存放树埠,在32位中就有2^32也就是4G的虛擬內(nèi)存分配,為了更好的劃分出哪一塊是交給內(nèi)核嘶伟,哪一塊是交給用戶操作的怎憋,分為了內(nèi)核態(tài)以及用戶態(tài)。
同樣進程為了可以正常運行九昧,進程會為上面10點劃分內(nèi)存绊袋,這里就從進程的低位開始說起虛擬內(nèi)存的布局:
- 1.Text Segment 存放二進制可執(zhí)行代碼
- 2.Data Segment 存放靜態(tài)常量
- 3.BSS Segment 存放為初始化的靜態(tài)常量
- 4.堆(Heap)段 堆是往高地址增長的,用來動態(tài)分配內(nèi)存
- 5.Memory Mapping Segment 這一段是用來把文件映射進內(nèi)存铸鹰,如加載so庫
- 6.棧(Stack)段 主線程的函數(shù)調(diào)用的函數(shù)棧
同樣的內(nèi)核中也有一樣的結(jié)構(gòu).
虛擬內(nèi)存劃分邏輯
了解了虛擬內(nèi)存布局之后癌别,我們想要進一步的理解的物理內(nèi)存到虛擬內(nèi)存的映射方式,就要重新復(fù)習(xí)一下mmap那一節(jié)和大家聊過的把虛擬空間分為多段保存蹋笼,這里簡單回顧一下展姐,大致分為段式躁垛,頁式,段頁式管理這些虛擬內(nèi)存圾笨。
段式管理
段式的原理圖如下:
能看到這里面的原理很直觀缤苫,段式管理分為2部分,段選擇子和段內(nèi)偏移量墅拭。段選擇子中最重要的是段號活玲,段號作為段表的索引.段表中存放著基地址,段界限谍婉,特權(quán)等級舒憾。段內(nèi)偏移量應(yīng)該在0到段界限之間,當我們嘗試著找物理地址穗熬,就可以通過如下公式尋找:
物理地址 = 基地址 + 段內(nèi)偏移量
DPL特權(quán)等級是指訪問這一段地址的權(quán)限镀迂,如DPL為3則是用戶態(tài)可以訪問,0則是內(nèi)核態(tài)
頁式管理
Linux更加偏向使用頁式管理對內(nèi)存進行分頁唤蔗,讓虛擬內(nèi)存轉(zhuǎn)化為物理內(nèi)存探遵。這種方式有一種特殊處理方式,當一塊內(nèi)存長時間不用但是沒有釋放則會暫時寫入到硬盤上妓柜,叫做換出箱季。一旦需要,再加載進來叫做換入棍掐。這樣就可以依靠硬盤大于內(nèi)存卡數(shù)十倍的空間充分的利用物理設(shè)備藏雏,擴大可用物理內(nèi)存,提高物理內(nèi)存利用率作煌。
一般控制Linux每一頁的大小由PAGE_SIZE的宏控制的掘殴,大小默認設(shè)置為4kb。原理圖如下:
虛擬地址分為2部分粟誓,頁號和頁內(nèi)偏移奏寨。頁號作為頁表的索引,頁表包含著物理頁每一頁所在物理地址的基地址鹰服,就能依照如下的公式計算:
頁基地址 + 頁內(nèi)偏移 = 物理地址
但是這樣能找到的地址太少了病瞳,如果需要4G的內(nèi)存,一頁4kb获诈,那就要1M的頁表仍源。這樣太消耗內(nèi)存了。因此Linux做了一個中間鍵舔涎,名為頁目錄笼踩,如果能通過目錄再去查找就能消耗更小的內(nèi)存。一個頁目錄的頁表項有1k個亡嫌,每一項只用4個字節(jié)大小嚎于,那頁表只需要1k個掘而。這樣就能極大利用更多的物理空間
把整個流程畫出圖就是如下:
task_struct中的設(shè)計
文件:/include/linux/sched.h
在代表進程結(jié)構(gòu)體的task_struct下有這么一個結(jié)構(gòu)體,象征著進程中所管理的虛擬內(nèi)存于购。
struct mm_struct *mm;
這個結(jié)構(gòu)體通過task_size把進程中的虛擬內(nèi)存一分為二袍睡,task_size默認是3G,也就是用戶態(tài)虛擬內(nèi)存的大小
unsigned long task_size; /* size of task vm space */
用戶態(tài)的布局
我們接下來看看mm_struct用戶態(tài)中關(guān)鍵的屬性
unsigned long mmap_base; /* 虛擬地址空間中用于內(nèi)存映射的起始地址*/
unsigned long total_vm; /*總共映射的頁的總數(shù)*/
unsigned long locked_vm; /* 被鎖定不能換出的頁 */
unsigned long pinned_vm; /* 不能換出也不能移動的頁 */
unsigned long data_vm; /* 存放數(shù)據(jù)的頁的數(shù)目*/
unsigned long exec_vm; /* 存放可執(zhí)行文件頁的數(shù)目 */
unsigned long stack_vm; /* 棧所占用頁的數(shù)據(jù) */
unsigned long start_code, end_code,/*可執(zhí)行代碼的起點和結(jié)束位置*/ start_data, end_data/*已經(jīng)初始化數(shù)據(jù)開始位置和結(jié)束位置*/;
unsigned long start_brk/*堆起始的位置*/, brk/*堆當前結(jié)束的位置*/, start_stack/*棧起始的位置肋僧,棧的結(jié)束位置在寄存器的棧頂指針中*/;
unsigned long arg_start, arg_end,/*參數(shù)列表的位置*/ env_start, env_end/**環(huán)境變量的位置/;
還有其他的重要數(shù)據(jù)結(jié)構(gòu)
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
還記得我之前在Binder映射聊過的vm_area_struct嗎斑胜?這個數(shù)據(jù)結(jié)構(gòu)代表著用戶空間的虛擬內(nèi)存,vm_struct則代表內(nèi)核空間的虛擬內(nèi)存.所有的用戶態(tài)的虛擬內(nèi)存都會鏈接到mmap和mm_rb.紅黑樹的出現(xiàn)就為了讓查找效率在O(logn)之內(nèi)嫌吠。
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start; /* 用戶空間的起點地址 */
unsigned long vm_end; /* 用戶空間的 結(jié)束地址*/
/* 把這一串區(qū)域鏈接到task_struct的鏈表上 */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb/*把當前的虛擬內(nèi)存區(qū)域掛載在task_struct紅黑樹上*/;
struct mm_struct *vm_mm; /* The address space we belong to. */
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* 匿名映射區(qū)域 */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops ;/*操縱該虛擬內(nèi)存的操作方法*/
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
} __randomize_layout;
vm_area_struct什么時候把這些屬性和真實的地址的數(shù)據(jù)起來呢止潘。其實是在exec運行加載二進制文件的時候調(diào)用load_elf_binary完成的。
- 在這里面設(shè)置了mmap_base映射區(qū)域的起始地址辫诅;
- 棧的vm_area_struct凭戴,這里面設(shè)置了arg_start指向棧底;start_stack也同時賦值炕矮。
- elf_map將elf中的代碼加載到內(nèi)存
- set_brk設(shè)置了堆的vm_area_struct么夫,設(shè)置了startbrk以及brk同時在堆頂
- load_elf_interp 將依賴的so映射到內(nèi)存
一旦映射完畢之后,我們想要修改映射肤视,一般就是指malloc档痪,free之類的方式申請堆內(nèi)存或者執(zhí)行函數(shù)加深函數(shù)棧等「炙蹋可以分為2種情況:
- 1.函數(shù)調(diào)用修改棧頂指針
- 2.malloc申請堆內(nèi)空間钞它,要么申請小空間就直接修改brk的指向,要么直接執(zhí)行mmap做大內(nèi)存的映射殊鞭。
malloc的核心原理就是如果在一頁內(nèi)足以分配直接修改brk指針,如果超過一頁則會從vm_area_struct紅黑樹中查找能否是否有相應(yīng)連續(xù)的空間并且合并不能合并則申請一個vm_area_struct掛載在紅黑樹和鏈表尼桶。
內(nèi)核態(tài)布局
原理圖如下:
記住整個內(nèi)核態(tài)的虛擬內(nèi)存和物理內(nèi)存在不同的進程之間其實共用的是一份操灿。在32位內(nèi)核態(tài)中一般分配1G的大小。其中896M是直接映射區(qū)泵督,所謂的直接映射區(qū)就是物理內(nèi)存和虛擬內(nèi)存有很簡單的映射關(guān)系趾盐,其實就是如下:
物理地址 = 虛擬地址 - 3G
在著896M中,前1M已經(jīng)被系統(tǒng)啟動占用小腊,從1M開始加載內(nèi)核代碼救鲤,然后是全局變量,BBS等秩冈,其實和上面那一副虛擬內(nèi)存劃分圖幾乎一致本缠。
因為大部分的操作都在3G到3G+896M之間,這里會產(chǎn)生一種錯覺入问,內(nèi)核會直接操作物理內(nèi)存而不是虛擬內(nèi)存丹锹。然而這是錯誤的稀颁,內(nèi)核有這么一個權(quán)限,但是它不會這么做的楣黍。
896M之上還有固定映射匾灶,持久映射,vmalloc租漂,8M的空余阶女。這些全部稱為高端內(nèi)存。高端內(nèi)存是物理內(nèi)存的概念哩治,除了內(nèi)存管理模塊有機會直接操作物理內(nèi)存其他模塊還是通過虛擬內(nèi)存的映射找物理內(nèi)存秃踩。因此接下來高端內(nèi)存也的操作其實也是通過虛擬內(nèi)存操作的。
剩下的虛擬地址分為如下功能:
- 1.8M的空出
- 2.vmalloc的區(qū)稱為內(nèi)核動態(tài)映射區(qū)锚扎。一旦內(nèi)核使用vmalloc申請內(nèi)存吞瞪,就是這個區(qū)間,這個區(qū)間是從分配的物理內(nèi)存1.5G開始驾孔,需要使用頁表記錄這些內(nèi)存和物理頁之間的映射關(guān)系
- 3.持久映射區(qū)芍秆,當使用alloc_page的時候,在物理內(nèi)存的高端內(nèi)存中獲取到struct page結(jié)構(gòu)體可以通過kmap映射到該區(qū)域
- 4.固定映射區(qū)域翠勉,用于滿足系統(tǒng)初期啟動時候特殊需求
- 5.可以通過kmap_atomic實現(xiàn)臨時的內(nèi)核映射妖啥。當我們通過mmap映射把虛擬內(nèi)存到物理內(nèi)存之后,我們需要把虛擬內(nèi)存中的數(shù)據(jù)寫入到物理內(nèi)存中就需要kmap_atomic做一個臨時映射对碌,當寫入完畢荆虱,就解除映射。
物理內(nèi)存的布局
物理內(nèi)存的組織方式
如果物理內(nèi)存是連續(xù)的朽们,也是連續(xù)的怀读,就稱為是平坦內(nèi)存模型。在使用這種模型下骑脱,CPU有很多菜枷,在總線的一側(cè)。所有的內(nèi)存組成一大片內(nèi)存叁丧,在總線另一側(cè)啤誊。cpu想要訪問就需要越過總線,這種方式稱為SMP拥娄,即對稱多處理器模式蚊锹。
后面就有了一種高級的模式NUMA,非一致內(nèi)存訪問稚瘾。這種模式下牡昆,內(nèi)存不是一整塊,每個CPU都有自己的本地內(nèi)存孟抗,之后CPU就能直接回到訪問內(nèi)存不需要過總線迁杨,每一個CPU和內(nèi)存被稱為一個NUMA節(jié)點钻心。當本地內(nèi)存不足的時候就會嘗試的獲取其他NUMA節(jié)點申請。
這里需要指出的是铅协,NUMA 往往是非連續(xù)內(nèi)存模型捷沸。而非連續(xù)內(nèi)存模型不一定就是 NUMA,有時候一大片內(nèi)存的情況下狐史,也會有物理內(nèi)存地址不連續(xù)的情況痒给。
再后來內(nèi)存繼續(xù)支持熱插拔。
這里指講解NUMA的模式骏全。在這種模式下苍柏,有三個數(shù)據(jù)結(jié)構(gòu)需要關(guān)注,節(jié)點姜贡,區(qū)域以及頁试吁。
節(jié)點
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];//區(qū)域數(shù)組
struct zonelist node_zonelists[MAX_ZONELISTS];//備用節(jié)點和它區(qū)域的情況
int nr_zones;//當前節(jié)點區(qū)域數(shù)目
struct page *node_mem_map;//節(jié)點里面所有頁
unsigned long node_start_pfn;//這個節(jié)點的起始頁號
unsigned long node_present_pages; /* 這個節(jié)點包含的不連續(xù)物理內(nèi)存地址的頁面數(shù)*/
unsigned long node_spanned_pages; /* 真正可用的物理內(nèi)存頁數(shù) */
int node_id;//節(jié)點id
......
} pg_data_t;
node_spanned_pages真正可用的物理內(nèi)存頁數(shù)是因為有的頁是作為間隔空洞的作用,并不是用來存儲楼咳。
每個節(jié)點分為一個個區(qū)域zone熄捍,在node_zones數(shù)組中有不同類型的區(qū)域如下:
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,//直接內(nèi)存存取,DMA模式允許CPU下發(fā)指令把事情交給外設(shè)完成
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,// 直接映射區(qū)母怜,從物理內(nèi)存到虛擬內(nèi)存的映射區(qū)域
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,//高端內(nèi)存余耽,超過896M之上的地方
#endif
ZONE_MOVABLE,//可移動區(qū)域,通過劃分可移動不可移動區(qū)域苹熏,避免內(nèi)存碎片化
__MAX_NR_ZONES
};
區(qū)域
struct zone {
......
struct pglist_data *zone_pgdat;
struct per_cpu_pageset __percpu *pageset;//區(qū)分冷熱頁
unsigned long zone_start_pfn;//zone的第一頁
/*
* 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;//被伙伴系統(tǒng)管理的頁
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_
這里面涉及到了冷熱頁,什么是冷熱頁轨域?CPU訪問存儲在高速緩存的頁比直接訪問內(nèi)存速度快多了袱耽,存在高速緩存的頁叫做熱頁,沒有叫做冷頁干发。每一個CPU都有一個自己的高速緩存扛邑。
頁
頁都知道是結(jié)構(gòu)體page,其實page的使用大致分為2種铐然。
- 1.用這一整頁和虛擬內(nèi)存內(nèi)存建立映射關(guān)系稱為匿名頁,用這一整頁和文件關(guān)聯(lián)恶座,再和虛擬內(nèi)存建立關(guān)系叫做內(nèi)存映射文件搀暑。常規(guī)mmap和Binder就是匿名頁,Ashmem和shmem則是內(nèi)存映射文件跨琳。
在這個過程有一個關(guān)鍵的結(jié)構(gòu)體address_space自点。這個結(jié)構(gòu)體用于內(nèi)存映射。如果是匿名頁最低位為1脉让,如果是內(nèi)存映射文件最低位為0.
- 2.僅分配小內(nèi)存桂敛。如剛開始申請一個task_struct的時候不需要這么大的空間功炮,為了滿足這種快速的小內(nèi)存申請,Linux中有slab allocator技術(shù)术唬,用于分配slab的一小塊內(nèi)存薪伏。基本原理就是從內(nèi)存管理系統(tǒng)中申請一整頁粗仓,劃分多個小塊內(nèi)存用于分配嫁怀,并且用復(fù)雜的數(shù)據(jù)結(jié)構(gòu)保存。
當然還有一種叫做slob的分配機制借浊,一般用于嵌入式系統(tǒng)塘淑。最后看看page的結(jié)構(gòu)體
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 */
};
......
}
頁的分配 伙伴系統(tǒng)
物理頁的分配是有一種叫做伙伴系統(tǒng)的方式分配出合理大小的物理頁,其實這個原理在Binder的時候已經(jīng)聊過了蚂斤,這里看看它的原理圖:
調(diào)用伙伴系統(tǒng)分配物理頁存捺,一般是使用alloc_pages方法進行分配。其核心原理就是當向內(nèi)核請求分配 (2^(i-1) 曙蒸,2^i] 數(shù)目的頁塊時捌治,按照 2^i 頁塊請求處理。如果對應(yīng)的頁塊鏈表中沒有空閑頁塊逸爵,那我們就在更大的頁塊鏈表中去找具滴。當分配的頁塊中有多余的頁時,伙伴系統(tǒng)會根據(jù)多余的頁塊大小插入到對應(yīng)的空閑頁塊鏈表中师倔。
比如我們要申請一個128頁的大小的頁塊构韵,嘗試的超著128頁對應(yīng)的鏈表中有沒有空閑,有則取出趋艘,沒有則往更加高一層疲恢,也就是256大小頁塊區(qū)域查找。找到有空閑瓷胧,則把256拆成2部分显拳,128和128,其中一個128拿去用搓萧,另一個128掛載128頁項的空閑鏈表中杂数。
整個物理頁體系如下管理
小物理內(nèi)存申請
slab中小內(nèi)存大致上是如此劃分,每一個小內(nèi)存都有指針指向下一個小內(nèi)存瘸洛,方便增加刪除揍移。
分配緩存塊的時候,分為兩種情況反肋,一種是kmem_cache_cpu快通道那伐,一種是kmem_cache_node普通通道。
每一次都會從kmem_cache_cpu快通道申請,freelist(2次機會)和partial(1次機會)兩個內(nèi)存鏈表發(fā)現(xiàn)沒有足夠的內(nèi)存罕邀,接著才從普通通道申請畅形,也是從freelist(2次機會)和partial(1次機會)中查找內(nèi)存。
頁面的換入換出
頁面的換入換出本質(zhì)上就是為了實現(xiàn)虛擬內(nèi)存很大诉探,而物理內(nèi)存沒有這么多的問題日熬。如果一段內(nèi)存長時間不用了,就會把物理內(nèi)存記錄在硬盤阵具,把活躍的內(nèi)存交給活躍進程碍遍。
頁面的換入換出的時機:
- 1.申請一頁,最終會調(diào)用shrink_node阳液,頁面的換入換出是以節(jié)點為單位換入換出
- 2.當系統(tǒng)內(nèi)存緊張時候怕敬,內(nèi)核線程 kswapd開始換入換出內(nèi)存頁,最后也是調(diào)用shrink_node
內(nèi)存也分為2中帘皿,匿名內(nèi)存东跪,內(nèi)存映射文件。他們都有兩個列表active活躍列表和inactive非活躍列表鹰溜。換入換出也是選擇從非活躍中獲取這個LRU最不活躍的列表進行換入換出虽填。
物理內(nèi)存到虛擬內(nèi)存的映射
這個模塊分為兩部分來介紹,一個是用戶態(tài)的內(nèi)存映射曹动,一個是內(nèi)核態(tài)的內(nèi)存映射斋日。
用戶態(tài)的內(nèi)存映射
這里涉及到了mmap的源碼,有興趣可以去Binder系列下文過一遍源碼:
Android 重學(xué)系列 Binder驅(qū)動的初始化 映射原理
其實在這個過程vm_area_struct結(jié)構(gòu)體是從slab中申請出來的墓陈,并且會嘗試這合并vm_area_struct結(jié)構(gòu)體恶守,最后會掛載到mm_struct 的紅黑樹。
對于內(nèi)存映射文件贡必,還會多處理一些邏輯兔港。對于打開的文件有結(jié)構(gòu)體file表示,里面有一個address_space的成員變量:
struct address_space {
struct inode *host; /* owner: inode, block_device */
......
struct rb_root i_mmap; /* tree of private and shared mappings */
......
const struct address_space_operations *a_ops; /* methods */
......
}
static void __vma_link_file(struct vm_area_struct *vma)
{
struct file *file;
file = vma->vm_file;
if (file) {
struct address_space *mapping = file->f_mapping;
vma_interval_tree_insert(vma, &mapping->i_mmap);
}
所映射的內(nèi)存將不會直接掛載到mm_struct中仔拟,而是掛載在這個結(jié)構(gòu)體的i_mmap紅黑樹中衫樊。這個方式可以在shmem中看到。
到這些方式都只是在邏輯層面上讓虛擬內(nèi)存結(jié)構(gòu)體和文件關(guān)聯(lián)起來利花,并沒有具體申請物理頁科侈。因為物理頁很珍貴,需要用時獲取炒事。
那么一般情況下兑徘,什么時候才開始申請物理頁。一般是嘗試著操作這一段虛擬內(nèi)存的時候發(fā)現(xiàn)沒有映射物理頁羡洛,就發(fā)出了缺頁中斷,系統(tǒng)就會申請物理頁,而方法就是do_page_fault欲侮。
這個過程完成了如下步驟:
- 1.判斷缺頁是否在內(nèi)核崭闲。是在內(nèi)核則調(diào)用vmalloc_fault,這樣就能把vmallo區(qū)中的建立起物理頁和虛擬內(nèi)存的關(guān)系
- 2.如果是用戶態(tài)則調(diào)用handle_mm_fault映射。
- 3.handle_mm_fault建立起了pgd,p4d,pud,pmd,pte線性地址的五個組成部分威蕉。如果不看p4d刁俭。這里實際上就是我在Binder中聊過的頁管理的組成部分,pgd全局目錄韧涨,pud上層目錄牍戚,pmd中間目錄,pte直接頁表項虑粥。最后還有一個offset如孝。
- 4.調(diào)用handle_pte_fault。在這里面分為三種情況娩贷。
情況一:如果新的頁表項第晰,這個頁是一個匿名頁,要映射到某一段物理內(nèi)存中彬祖,就會調(diào)用do_anonymous_page茁瘦,最后會調(diào)用alloc_zeroed_user_highpage_movable通過伙伴系統(tǒng)分配物理頁,最后塞進頁表中储笑。
情況二:映射到文件甜熔,就會調(diào)用__do_fault,其中調(diào)用了vm_operations_struct的fault的操作方法突倍。在ext4文件系統(tǒng)中腔稀,如果文件已經(jīng)有物理內(nèi)存作為緩存直接獲取對應(yīng)的page,預(yù)讀里面的數(shù)據(jù)赘方;沒有則分配一個物理頁添加到lru表烧颖,調(diào)用address_space中readPage的方法讀取數(shù)據(jù)到內(nèi)存。
情況三:do_swap_page換入換出窄陡。首先查找swap文件有沒有緩存炕淮。沒有則通過swapin_readahead把數(shù)據(jù)從文件讀取出來加載到內(nèi)存,并生成頁目錄插入頁表中跳夭。
還有一種名為TLB的方式涂圆,稱為塊表。專門做地址映射的硬件設(shè)備币叹。不再內(nèi)存中可存儲的數(shù)據(jù)比內(nèi)存少润歉,但是比內(nèi)存快【备В可以當成頁表的Cache踩衩。查找方式如下。
內(nèi)核態(tài)的內(nèi)存映射
實際上在fork等方式生成新的進程時候,會初始化mm_struct的pgd全局目錄項驱富,其中調(diào)用了pgd_ctor方法锚赤,而這個方法拷貝了swapper_pg_dir的引用,而這個引用就是內(nèi)核態(tài)中的最頂級的全局目錄頁表褐鸥。一個進程內(nèi)存分為用戶態(tài)和內(nèi)核態(tài)线脚,當然內(nèi)存頁也分為用戶態(tài)和內(nèi)核態(tài)的頁表。
當進程調(diào)度的時候叫榕,內(nèi)存會進行切換浑侥,會調(diào)用load_new_mm_cr3方法。這是指cr3寄存器晰绎,這個寄存器保存著當前進程的全局目錄頁表寓落。如果進程要訪問虛擬內(nèi)存,會從cr3寄存器獲取pgd地址寒匙,通過這個地址找到后面的頁表查找真正的物理內(nèi)存數(shù)據(jù)零如。
初始化內(nèi)核態(tài)的頁表本質(zhì)上還是要看swapper_pg_dir這個屬性是怎么初始化的。原理如圖:
vmalloc 和 kmap_atomic 原理
用戶態(tài)通過malloc申請內(nèi)存大的內(nèi)存實際上調(diào)用的是mmap锄弱。
vmalloc也是十分相似考蕾,最后會調(diào)用__vmalloc_node_range,在vmalloc區(qū)進行邏輯上的關(guān)聯(lián)会宪。
kmap_atomic臨時映射肖卧,如果是32位有高端地址則調(diào)用set_pte通過內(nèi)核頁表進行映射。
kmap_atomic發(fā)現(xiàn)沒有頁表掸鹅,則會創(chuàng)建頁表進行映射塞帐。vmalloc則是借助缺頁中斷進行分配物理頁。
總結(jié)原理圖如下: