一、內(nèi)存管理架構(gòu)
二、虛擬地址空間布局架構(gòu)
三、物理內(nèi)存體系架構(gòu)
四、內(nèi)存結(jié)構(gòu)
五、內(nèi)存模型
六、虛擬地址和物理地址的轉(zhuǎn)換
七、內(nèi)存映射原理分析
一、內(nèi)存管理架構(gòu)
內(nèi)存管理子系統(tǒng)架構(gòu)可以分為:用戶空間、內(nèi)核空間及硬件部分3個(gè)層面,具體結(jié)構(gòu)如下所示:
1、用戶空間:應(yīng)用程序使用malloc()申請(qǐng)內(nèi)存資源/free()釋放內(nèi)存資源。
2、內(nèi)核空間:內(nèi)核總是駐留在內(nèi)存中,是操作系統(tǒng)的一部分。內(nèi)核空間為內(nèi)核保留,不允許應(yīng)用程序讀寫該區(qū)域的內(nèi)容或直接調(diào)用內(nèi)核代碼定義的函數(shù)。
3、硬件:處理器包含一個(gè)內(nèi)存管理單元(Memory Management Uint,MMU)的部件,負(fù)責(zé)把虛擬地址轉(zhuǎn)換為物理地址。
二、虛擬地址空間布局架構(gòu)
上面的用戶空間和內(nèi)核空間所指的都是虛擬地址,物理地址沒(méi)有用戶和內(nèi)核之分。每個(gè)項(xiàng)目的物理地址對(duì)于進(jìn)程不可見(jiàn),誰(shuí)也不能直接訪問(wèn)這個(gè)物理地址。操作系統(tǒng)會(huì)給進(jìn)程分配一個(gè)虛擬地址。所有進(jìn)程看到的這個(gè)地址都是一樣的,里面的內(nèi)存都是從 0 開(kāi)始編號(hào)。所有進(jìn)程共享內(nèi)核虛擬地址空間,每個(gè)進(jìn)程有獨(dú)立的用戶虛擬地址空間,同一個(gè)線程組的用戶線程共享用戶虛擬地址空間,內(nèi)核線程沒(méi)有用戶虛擬地址空間。
在程序里面,指令寫入的地址是虛擬地址。例如,位置為 10M 的內(nèi)存區(qū)域,操作系統(tǒng)會(huì)提供一種機(jī)制,將不同進(jìn)程的虛擬地址和不同內(nèi)存的物理地址映射起來(lái)。當(dāng)程序要訪問(wèn)虛擬地址的時(shí)候,由內(nèi)核的數(shù)據(jù)結(jié)構(gòu)進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換成不同的物理地址,這樣不同的進(jìn)程運(yùn)行的時(shí)候,寫入的是不同的物理地址,這樣就不會(huì)沖突了。
32位處理器使用32位虛擬地址,而64位處理器卻不是使用64位虛擬地址。因?yàn)槟壳皯?yīng)用程序沒(méi)有那么大的內(nèi)存需求,所以ARM64和X86_64處理器不支持完全的64位虛擬地址,而是使用了48位。我們計(jì)算一下,如果是 32 位,有 2^32 = 4G 的內(nèi)存空間都是我的,不管內(nèi)存是不是真的有 4G。如果是 64 位,在 ARM64和X86_64 下面,其實(shí)只使用了 48 位,那也挺恐怖的。48 位地址長(zhǎng)度也就是對(duì)應(yīng)了 256TB 的地址空間。我都沒(méi)怎么見(jiàn)過(guò) 256T 的硬盤,別說(shuō)是內(nèi)存了。
這么大的虛擬空間一切二,一部分用來(lái)放內(nèi)核的東西,稱為內(nèi)核空間,一部分用來(lái)放進(jìn)程的東西,稱為用戶空間。用戶空間其實(shí)包含以下幾個(gè)區(qū)域,我們最低位開(kāi)始排起:1.代碼段,數(shù)據(jù)段,未初始化的數(shù)據(jù)段(bss) 2.存放動(dòng)態(tài)生成數(shù)據(jù)的堆,堆是往高地址增長(zhǎng)的 3.動(dòng)態(tài)庫(kù)的代碼段,數(shù)據(jù)段和未初始化的數(shù)據(jù)段(bss) 4.存放局部變量和實(shí)現(xiàn)函數(shù)調(diào)用的棧 5.把文件映射到虛擬地址空間的內(nèi)存映射區(qū) 6.存放在棧底的環(huán)境變量和參數(shù)字符串
三、物理內(nèi)存體系架構(gòu)
目前多處理器系統(tǒng)有兩種體系結(jié)構(gòu):
1)一致內(nèi)存訪問(wèn)(Uniform Memory Access,UMA),所有處理器訪問(wèn)內(nèi)存花費(fèi)的時(shí)間是相同。
這種結(jié)構(gòu)的CPU 是通過(guò)一條通用總線連接到北橋,北橋中的內(nèi)存控制器鏈接著內(nèi)存。這種設(shè)計(jì)中,瓶頸馬上出現(xiàn)了。第一個(gè)瓶頸與設(shè)備對(duì)RAM 的訪問(wèn)有關(guān)。早期,所有設(shè)備之間的通信都需要經(jīng)過(guò) CPU,結(jié)果嚴(yán)重影響了整個(gè)系統(tǒng)的性能。為了解決這個(gè)問(wèn)題,有些設(shè)備加入了直接內(nèi)存訪問(wèn)(DMA)的能力。DMA 允許設(shè)備在北橋的幫助下,無(wú)需 CPU 的干涉,直接讀寫 RAM。到了今天,所有高性能的設(shè)備都可以使用 DMA。雖然 DMA 大大降低了 CPU 的負(fù)擔(dān),卻占用了北橋的帶寬,與 CPU 形成了爭(zhēng)用。所以現(xiàn)在很少使用了。
2)非一致內(nèi)存訪問(wèn)(Non-Unit Memory Access,NUMA):指內(nèi)存被劃分成多個(gè)內(nèi)存節(jié)點(diǎn)的多處理器系統(tǒng),訪問(wèn)一個(gè)內(nèi)存節(jié)點(diǎn)花費(fèi)的時(shí)間取決于處理器和內(nèi)存節(jié)點(diǎn)的距離。
采用這樣的架構(gòu),系統(tǒng)里有幾個(gè)處理器,就可以有幾個(gè)內(nèi)存庫(kù)。系統(tǒng)仍然要讓所有內(nèi)存能被所有處理器所訪問(wèn),導(dǎo)致內(nèi)存不再是統(tǒng)一的資源。處理器能以正常的速度訪問(wèn)本地內(nèi)存(連接到該處理器的內(nèi)存)。但它訪問(wèn)其它處理器的內(nèi)存時(shí),卻需要使用處理器之間的互聯(lián)通道。使用下面的命令可以查看內(nèi)存架構(gòu):
jian@ubuntu:~$ numactl -H
available: 1 nodes (0)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 7953 MB
node 0 free: 5900 MB
node distances:
node 0
0: 10
如上面所示,我們只有一個(gè)內(nèi)存節(jié)點(diǎn),我們這個(gè)8個(gè)cpu核心都是使用這一個(gè)內(nèi)存節(jié)點(diǎn),所以我們可以認(rèn)為我們是UMA架構(gòu)的。
四、內(nèi)存結(jié)構(gòu)
由于現(xiàn)在我接觸的基本都是使用UMA的結(jié)構(gòu),所以下面說(shuō)的都是這種:內(nèi)存管理子系統(tǒng)使用節(jié)點(diǎn)(node),區(qū)域(zone)、頁(yè)(page)三級(jí)結(jié)構(gòu)描述物理內(nèi)存。節(jié)點(diǎn)是基于哪個(gè)cpu,一般多少核的cpu就有多少個(gè)節(jié)點(diǎn)node;zone是每個(gè)cpu,也就是每個(gè)節(jié)點(diǎn)會(huì)把內(nèi)存分為高端內(nèi)存,低端內(nèi)存,DMA區(qū)域等等的內(nèi)存區(qū)域;頁(yè)就是物理內(nèi)存的最小單位了,也是虛擬內(nèi)存映射到物理內(nèi)存的最小單位。最后,在NUMA內(nèi)存架構(gòu)中, Linux定義了一個(gè) pglist_data 的結(jié)構(gòu)體來(lái)管理所有的內(nèi)存節(jié)點(diǎn).
- 內(nèi)存節(jié)點(diǎn)(node)
在NUMA體系的內(nèi)存節(jié)點(diǎn)是根據(jù)處理器和內(nèi)存的距離劃分的,而在具有不連續(xù)內(nèi)存的NUMA系統(tǒng)中,表示比區(qū)域的級(jí)別更高的內(nèi)存區(qū)域,根據(jù)物理地址是否連續(xù)劃分,每塊物理地址連續(xù)的內(nèi)存是一個(gè)內(nèi)存節(jié)點(diǎn)。內(nèi)存節(jié)點(diǎn)結(jié)構(gòu)體在linux內(nèi)核include/linux/mmzone.h文件中,
/*
* On NUMA machines, each NUMA node would have a pg_data_t to describe
* it's memory layout. On UMA machines there is a single pglist_data which
* describes the whole memory.
*
* Memory statistics and page replacement data structures are maintained on a
* per-zone basis.
*/
struct bootmem_data;
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];//內(nèi)存區(qū)域數(shù)組
struct zonelist node_zonelists[MAX_ZONELISTS];//備用區(qū)域數(shù)組
int nr_zones;//該節(jié)點(diǎn)包含的內(nèi)存區(qū)域數(shù)量
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
struct page *node_mem_map;//指向物理頁(yè)描述符數(shù)組
#ifdef CONFIG_PAGE_EXTENSION
struct page_ext *node_page_ext;//頁(yè)的擴(kuò)展屬性
#endif
#endif
#ifndef CONFIG_NO_BOOTMEM
struct bootmem_data *bdata;//早期內(nèi)存管理器
......
} pg_data_t;
- 內(nèi)存區(qū)域(zone)
每一個(gè)節(jié)點(diǎn)分成一個(gè)個(gè)區(qū)域 zone,放在數(shù)組 node_zones 里面。這個(gè)數(shù)組的大小為 MAX_NR_ZONES。我們來(lái)看區(qū)域的定義。內(nèi)存節(jié)點(diǎn)被劃分為內(nèi)存區(qū)域,內(nèi)存區(qū)域結(jié)構(gòu)體在linux內(nèi)核include/linux/mmzone.h文件中
struct zone {
unsigned long watermark[NR_WMARK];
unsigned long nr_reserved_highatomic;
long lowmem_reserve[MAX_NR_ZONES];
#ifdef CONFIG_NUMA
int node;
#endif
struct pglist_data *zone_pgdat;
struct per_cpu_pageset __percpu *pageset;
#ifndef CONFIG_SPARSEMEM
unsigned long *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */
unsigned long zone_start_pfn;
unsigned long managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
const char *name;
#ifdef CONFIG_MEMORY_ISOLATION
unsigned long nr_isolate_pageblock;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
seqlock_t span_seqlock;
#endif
int initialized;
ZONE_PADDING(_pad1_)
struct free_area free_area[MAX_ORDER];//內(nèi)存區(qū)域數(shù)組,用于伙伴分配器進(jìn)行頁(yè)分配
unsigned long flags;//內(nèi)存區(qū)域的屬性,定義在下面
spinlock_t lock;
ZONE_PADDING(_pad2_)
unsigned long percpu_drift_mark;
#ifdef CONFIG_COMPACTION
unsigned int compact_considered;
unsigned int compact_defer_shift;
int compact_order_failed;
#endif
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
bool compact_blockskip_flush;
#endif
bool contiguous;
ZONE_PADDING(_pad3_)
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
atomic_long_t vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;
//struct zone 的 flags 參數(shù)
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,
#ifdef CONFIG_ZONE_DEVICE
ZONE_DEVICE,
#endif
__MAX_NR_ZONES
};
ZONE_DMA 是指可用于作 DMA(Direct Memory Access,直接內(nèi)存存?。┑膬?nèi)存。DMA 是這樣一種機(jī)制:要把外設(shè)的數(shù)據(jù)讀入內(nèi)存或把內(nèi)存的數(shù)據(jù)傳送到外設(shè),原來(lái)都要通過(guò) CPU 控制完成,但是這會(huì)占用 CPU,影響 CPU 處理其他事情,所以有了 DMA 模式。CPU 只需向 DMA 控制器下達(dá)指令,讓 DMA 控制器來(lái)處理數(shù)據(jù)的傳送,數(shù)據(jù)傳送完畢再把信息反饋給 CPU,這樣就可以解放 CPU。對(duì)于 64 位系統(tǒng),有兩個(gè) DMA 區(qū)域。除了上面說(shuō)的 ZONE_DMA,還有 ZONE_DMA32。在這里你大概理解 DMA 的原理就可以,不必糾結(jié),我們后面會(huì)講 DMA 的機(jī)制。ZONE_NORMAL 是直接映射區(qū),就是上一節(jié)講的,從物理內(nèi)存到虛擬內(nèi)存的內(nèi)核區(qū)域,通過(guò)加上一個(gè)常量直接映射。ZONE_HIGHMEM 是高端內(nèi)存區(qū),就是上一節(jié)講的,對(duì)于 32 位系統(tǒng)來(lái)說(shuō)超過(guò) 896M 的地方,對(duì)于 64 位沒(méi)必要有的一段區(qū)域。
ZONE_MOVABLE 是可移動(dòng)區(qū)域,通過(guò)將物理內(nèi)存劃分為可移動(dòng)分配區(qū)域和不可移動(dòng)分配區(qū)域來(lái)避免內(nèi)存碎片。這里你需要注意一下,我們剛才對(duì)于區(qū)域的劃分,都是針對(duì)物理內(nèi)存的。
- 內(nèi)存頁(yè)(page)
了解了區(qū)域 zone,接下來(lái)我們就到了組成物理內(nèi)存的基本單位,頁(yè)的數(shù)據(jù)結(jié)構(gòu) struct page。這是一個(gè)特別復(fù)雜的結(jié)構(gòu),里面有很多的 union,union 結(jié)構(gòu)是在 C 語(yǔ)言中被用于同一塊內(nèi)存根據(jù)情況保存不同類型數(shù)據(jù)的一種方式。這里之所以用了 union,是因?yàn)橐粋€(gè)物理頁(yè)面使用模式有兩種。第一種模式,僅需分配小塊內(nèi)存,Linux 系統(tǒng)采用了一種被稱為 slab allocator的技術(shù),下一節(jié)會(huì)講。第二種模式,要用就用一整頁(yè)。這一整頁(yè)的內(nèi)存,或者直接和虛擬地址空間建立映射關(guān)系,我們把這種稱為匿名頁(yè)(Anonymous Page)?;蛘哂糜陉P(guān)聯(lián)一個(gè)文件,然后再和虛擬地址空間建立映射關(guān)系,這樣的文件,我們稱為內(nèi)存映射文件(Memory-mapped File)。每個(gè)物理頁(yè)對(duì)應(yīng)一個(gè)page結(jié)構(gòu)體,稱為頁(yè)描述符,內(nèi)存節(jié)點(diǎn)的pglist_data實(shí)例的成員node_mem_map指向該內(nèi)存節(jié)點(diǎn)包含的所有物理頁(yè)的頁(yè)描述符組成的數(shù)組。內(nèi)存區(qū)域結(jié)構(gòu)體在linux內(nèi)核include/linux/mm_types.h文件中
struct page {
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
......
} _struct_page_alignment;
其結(jié)構(gòu)大概如下圖所示:
五、內(nèi)存模型
內(nèi)存模型是其實(shí)就是從cpu的角度看,其物理內(nèi)存的分布情況,在linux kernel中,使用什么的方式來(lái)管理這些物理內(nèi)存。內(nèi)存管理子系統(tǒng)支持3種內(nèi)存模型:1)平坦內(nèi)存(Flat Memory):內(nèi)存的物理地址空間是連續(xù)的,沒(méi)有空洞。如果從系統(tǒng)中任意一個(gè)processor的角度來(lái)看,當(dāng)它訪問(wèn)物理內(nèi)存的時(shí)候,物理地址空間是一個(gè)連續(xù)的,沒(méi)有空洞的地址空間,那么這種計(jì)算機(jī)系統(tǒng)的內(nèi)存模型就是Flat memory。這種內(nèi)存模型下,物理內(nèi)存的管理比較簡(jiǎn)單,每一個(gè)物理頁(yè)幀都會(huì)有一個(gè)page數(shù)據(jù)結(jié)構(gòu)來(lái)抽象,因此系統(tǒng)中存在一個(gè)struct page的數(shù)組(mem_map),每一個(gè)數(shù)組條目指向一個(gè)實(shí)際的物理頁(yè)幀(page frame)。在flat memory的情況下,PFN(page frame number)和mem_map數(shù)組index的關(guān)系是線性的(有一個(gè)固定偏移,如果內(nèi)存對(duì)應(yīng)的物理地址等于0,那么PFN就是數(shù)組index)。因此從PFN到對(duì)應(yīng)的page數(shù)據(jù)結(jié)構(gòu)是非常容易的,反之亦然,具體可以參考page_to_pfn和pfn_to_page的定義。此外,對(duì)于flat memory model,節(jié)點(diǎn)(struct pglist_data)只有一個(gè)(為了和Discontiguous Memory Model采用同樣的機(jī)制)。需要強(qiáng)調(diào)的是struct page所占用的內(nèi)存位于直接映射(directly mapped)區(qū)間,因此操作系統(tǒng)不需要再為其建立page table。
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))
2)不連續(xù)內(nèi)存(Discontiguous Memory):內(nèi)存的物理地址空間存在空洞,這種模型可以高效地處理空洞。如果cpu在訪問(wèn)物理內(nèi)存的時(shí)候,其地址空間有一些空洞,是不連續(xù)的,那么這種計(jì)算機(jī)系統(tǒng)的內(nèi)存模型就是Discontiguous memory。一般而言,NUMA架構(gòu)的計(jì)算機(jī)系統(tǒng)的memory model都是選擇Discontiguous Memory,不過(guò),這兩個(gè)概念其實(shí)是不同的。NUMA強(qiáng)調(diào)的是memory和processor的位置關(guān)系,和內(nèi)存模型其實(shí)是沒(méi)有關(guān)系的,只不過(guò),由于同一node上的memory和processor有更緊密的耦合關(guān)系(訪問(wèn)更快),因此需要多個(gè)node來(lái)管理。Discontiguous memory本質(zhì)上是flat memory內(nèi)存模型的擴(kuò)展,整個(gè)物理內(nèi)存的address space大部分是成片的大塊內(nèi)存,中間會(huì)有一些空洞,每一個(gè)成片的memory address space屬于一個(gè)node(如果局限在一個(gè)node內(nèi)部,其內(nèi)存模型是flat memory)。因此,這種內(nèi)存模型下,節(jié)點(diǎn)數(shù)據(jù)(struct pglist_data)有多個(gè),宏定義NODE_DATA可以得到指定節(jié)點(diǎn)的struct pglist_data。而,每個(gè)節(jié)點(diǎn)管理的物理內(nèi)存保存在struct pglist_data 數(shù)據(jù)結(jié)構(gòu)的node_mem_map成員中(概念類似flat memory中的mem_map)。這時(shí)候,從PFN轉(zhuǎn)換到具體的struct page會(huì)稍微復(fù)雜一點(diǎn),我們首先要從PFN得到node ID,然后根據(jù)這個(gè)ID找到對(duì)于的pglist_data 數(shù)據(jù)結(jié)構(gòu),也就找到了對(duì)應(yīng)的page數(shù)組,之后的方法就類似flat memory了。
#define __pfn_to_page(pfn)
({ unsigned long __pfn = (pfn);
unsigned long __nid = arch_pfn_to_nid(__pfn);
NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);
})
3)稀疏內(nèi)存(Space Memory):內(nèi)存的物理地址空間存在空洞,如果要支持內(nèi)存熱插拔,只能選擇稀疏內(nèi)存模型。Memory model也是一個(gè)演進(jìn)過(guò)程,剛開(kāi)始的時(shí)候,使用flat memory去抽象一個(gè)連續(xù)的內(nèi)存地址空間(mem_maps[]),出現(xiàn)NUMA之后,整個(gè)不連續(xù)的內(nèi)存空間被分成若干個(gè)node,每個(gè)node上是連續(xù)的內(nèi)存地址空間,也就是說(shuō),原來(lái)的單一的一個(gè)mem_maps[]變成了若干個(gè)mem_maps[]了。一切看起來(lái)已經(jīng)完美了,但是memory hotplug的出現(xiàn)讓原來(lái)完美的設(shè)計(jì)變得不完美了,因?yàn)榧幢闶且粋€(gè)node中的mem_maps[]也有可能是不連續(xù)了。
其實(shí),在出現(xiàn)了sparse memory之后,Discontiguous memory內(nèi)存模型已經(jīng)不是那么重要了,按理說(shuō)sparse memory最終可以替代Discontiguous memory的,這個(gè)替代過(guò)程正在進(jìn)行中,4.4的內(nèi)核仍然是有3中內(nèi)存模型可以選擇。為什么說(shuō)sparse memory最終可以替代Discontiguous memory呢?實(shí)際上在sparse memory內(nèi)存模型下,連續(xù)的地址空間按照SECTION(例如1G)被分成了一段一段的,其中每一section都是hotplug的,因此sparse memory下,內(nèi)存地址空間可以被切分的更細(xì),支持更離散的Discontiguous memory。
此外,在sparse memory沒(méi)有出現(xiàn)之前,NUMA和Discontiguous memory總是剪不斷,理還亂的關(guān)系:NUMA并沒(méi)有規(guī)定其內(nèi)存的連續(xù)性,而Discontiguous memory系統(tǒng)也并非一定是NUMA系統(tǒng),但是這兩種配置都是multi node的。有了sparse memory之后,我們終于可以把內(nèi)存的連續(xù)性和NUMA的概念剝離開(kāi)來(lái):一個(gè)NUMA系統(tǒng)可以是flat memory,也可以是sparse memory,而一個(gè)sparse memory系統(tǒng)可以是NUMA,也可以是UMA的。對(duì)于經(jīng)典的sparse memory模型,一個(gè)section的struct page數(shù)組所占用的內(nèi)存來(lái)自directly mapped區(qū)域,頁(yè)表在初始化的時(shí)候就建立好了,分配了page frame也就是分配了虛擬地址。
但是,對(duì)于SPARSEMEM_VMEMMAP而言,虛擬地址一開(kāi)始就分配好了,是vmemmap開(kāi)始的一段連續(xù)的虛擬地址空間,每一個(gè)page都有一個(gè)對(duì)應(yīng)的struct page,當(dāng)然,只有虛擬地址,沒(méi)有物理地址。因此,當(dāng)一個(gè)section被發(fā)現(xiàn)后,可以立刻找到對(duì)應(yīng)的struct page的虛擬地址,當(dāng)然,還需要分配一個(gè)物理的page frame,然后建立頁(yè)表什么的,因此,對(duì)于這種sparse memory,開(kāi)銷會(huì)稍微大一些(多了個(gè)建立映射的過(guò)程)。
#define __pfn_to_page(pfn) (vmemmap + (pfn))
#define vmemmap ((struct page *)VMEMMAP_START -
SECTION_ALIGN_DOWN(memstart_addr >> PAGE_SHIFT))
六、虛擬地址和物理地址的轉(zhuǎn)換
cpu讀寫指令和數(shù)據(jù)都需要用到內(nèi)存,而我們程序操作的都是虛擬內(nèi)存,大家覺(jué)得,為什么系統(tǒng)設(shè)計(jì)者要引入虛擬地址呢?設(shè)想一下,如果一臺(tái)計(jì)算機(jī)的內(nèi)存中只運(yùn)行一個(gè)程序 A,因?yàn)槌绦?A 的地址在鏈接時(shí)就可以確定,例如從內(nèi)存地址 0x8000 開(kāi)始,每次運(yùn)行程序 A 都裝入內(nèi)存 0x8000 地址處開(kāi)始運(yùn)行,沒(méi)有其它程序干擾?,F(xiàn)在改變一下,內(nèi)存中又放一道程序 B,程序 A 和程序 B 各自運(yùn)行一秒鐘,如此循環(huán),直到其中之一結(jié)束。這個(gè)新場(chǎng)景下就會(huì)產(chǎn)生一些問(wèn)題,當(dāng)然這里我們只關(guān)心內(nèi)存相關(guān)的這幾個(gè)核心問(wèn)題。
- 誰(shuí)來(lái)保證程序 A 跟程序 B 沒(méi)有內(nèi)存地址的沖突?換句話說(shuō),就是程序 A、B 各自放在什么內(nèi)存地址,這個(gè)問(wèn)題是由 A、B 程序協(xié)商,還是由操作系統(tǒng)決定。怎樣保證程序 A 跟程序 B 不會(huì)互相讀寫各自的內(nèi)存空間?這個(gè)問(wèn)題相對(duì)簡(jiǎn)單,用保護(hù)模式就能解決。如何解決內(nèi)存容量問(wèn)題?程序 A 和程序 B,在不斷開(kāi)發(fā)迭代中程序代碼占用的空間會(huì)越來(lái)越大,導(dǎo)致內(nèi)存裝不下。還要考慮一個(gè)擴(kuò)展后的復(fù)雜情況,如果不只程序 A、B,還可能有程序 C、D、E、F、G……它們分別由不同的公司開(kāi)發(fā),而每臺(tái)計(jì)算機(jī)的內(nèi)存容量不同。這時(shí)候,又對(duì)我們的內(nèi)存方案有怎樣的影響呢?
想完美地解決以上最核心的 4 個(gè)問(wèn)題,一個(gè)較好的方案是:讓所有的程序都各自享有一個(gè)從 0 開(kāi)始到最大地址的空間,這個(gè)地址空間是獨(dú)立的,是該程序私有的,其它程序既看不到,也不能訪問(wèn)該地址空間,這個(gè)地址空間和其它程序無(wú)關(guān),和具體的計(jì)算機(jī)也無(wú)關(guān)。事實(shí)上,計(jì)算機(jī)科學(xué)家們?cè)缇瓦@么做了,這個(gè)方案就是虛擬地址。虛擬地址正如其名,這個(gè)地址是虛擬的,自然而然地和具體環(huán)境進(jìn)行了解耦,這個(gè)環(huán)境包括系統(tǒng)軟件環(huán)境和硬件環(huán)境。
事實(shí)上,所有的應(yīng)用程序開(kāi)始的部分都是這樣的。這正是因?yàn)槊總€(gè)應(yīng)用程序的虛擬地址空間都是相同且獨(dú)立的。那么這個(gè)地址是由誰(shuí)產(chǎn)生的呢?答案是鏈接器,其實(shí)我們開(kāi)發(fā)軟件經(jīng)過(guò)編譯步驟后,就需要鏈接成可執(zhí)行文件才可以運(yùn)行,而鏈接器的主要工作就是把多個(gè)代碼模塊組裝在一起,并解決模塊之間的引用,即處理程序代碼間的地址引用,形成程序運(yùn)行的靜態(tài)內(nèi)存空間視圖。只不過(guò)這個(gè)地址是虛擬而統(tǒng)一的,而根據(jù)操作系統(tǒng)的不同,這個(gè)虛擬地址空間的定義也許不同,應(yīng)用軟件開(kāi)發(fā)人員無(wú)需關(guān)心,由開(kāi)發(fā)工具鏈給自動(dòng)處理了。由于這虛擬地址是獨(dú)立且統(tǒng)一的,所以各個(gè)公司開(kāi)發(fā)的各個(gè)應(yīng)用完全不用擔(dān)心自己的內(nèi)存空間被占用和改寫。物理地址雖然虛擬地址解決了很多問(wèn)題,但是虛擬地址只是邏輯上存在的地址,無(wú)法作用于硬件電路的,程序裝進(jìn)內(nèi)存中想要執(zhí)行,就需要和內(nèi)存打交道,從內(nèi)存中取得指令和數(shù)據(jù)。而內(nèi)存只認(rèn)一種地址,那就是物理地址。
什么是物理地址呢?物理地址在邏輯上也是一個(gè)數(shù)據(jù),只不過(guò)這個(gè)數(shù)據(jù)會(huì)被地址譯碼器等電子器件變成電子信號(hào),放在地址總線上,地址總線電子信號(hào)的各種組合就可以選擇到內(nèi)存的儲(chǔ)存單元了。但是地址總線上的信號(hào)(即物理地址),也可以選擇到別的設(shè)備中的儲(chǔ)存單元,如顯卡中的顯存、I/O 設(shè)備中的寄存器、網(wǎng)卡上的網(wǎng)絡(luò)幀緩存器。不過(guò)如果不做特別說(shuō)明,我們說(shuō)的物理地址就是指選擇內(nèi)存單元的地址。虛擬地址到物理地址的轉(zhuǎn)換明白了虛擬地址和物理地址之后,我們發(fā)現(xiàn)虛擬地址必須轉(zhuǎn)換成物理地址,這樣程序才能正常執(zhí)行。要轉(zhuǎn)換就必須要轉(zhuǎn)換機(jī)構(gòu),它相當(dāng)于一個(gè)函數(shù):p=f(v),輸入虛擬地址 v,輸出物理地址 p。那么要怎么實(shí)現(xiàn)這個(gè)函數(shù)呢?用軟件方式實(shí)現(xiàn)太低效,用硬件實(shí)現(xiàn)沒(méi)有靈活性,最終就用了軟硬件結(jié)合的方式實(shí)現(xiàn),它就是 MMU(內(nèi)存管理單元)。MMU 可以接受軟件給出的地址對(duì)應(yīng)關(guān)系數(shù)據(jù),進(jìn)行地址轉(zhuǎn)換。MMU一個(gè)工具,我們通過(guò)mmu去讀取地址關(guān)系轉(zhuǎn)化表,再根據(jù)虛擬地址空間地址找到物理地址所在區(qū)域,可以看圖:
下面我們不妨想一想地址關(guān)系轉(zhuǎn)換表的實(shí)現(xiàn). 如果在地址關(guān)系轉(zhuǎn)換表中,這樣來(lái)存放:一個(gè)虛擬地址對(duì)應(yīng)一個(gè)物理地址。那么問(wèn)題來(lái)了,32 位地址空間下,4GB 虛擬地址的地址關(guān)系轉(zhuǎn)換表就會(huì)把整個(gè) 32 位物理地址空間用完,這顯然不行。
系統(tǒng)設(shè)計(jì)者最后采用一個(gè)這樣的方案,即把虛擬地址空間和物理地址空間都分成同等大小的塊,也稱為頁(yè),按照虛擬頁(yè)和物理頁(yè)進(jìn)行轉(zhuǎn)換。根據(jù)軟件配置不同,這個(gè)頁(yè)的大小可以設(shè)置為 4KB、2MB、4MB、1GB,這樣就進(jìn)入了現(xiàn)代內(nèi)存管理模式——分頁(yè)模型。于是mmu的功能就是這樣的了:
結(jié)合圖片可以看出,一個(gè)虛擬頁(yè)可以對(duì)應(yīng)到一個(gè)物理頁(yè),由于頁(yè)大小一經(jīng)配置就是固定的,所以在地址關(guān)系轉(zhuǎn)換表中,只要存放虛擬頁(yè)地址對(duì)應(yīng)的物理頁(yè)地址就行了。MMU 頁(yè)表現(xiàn)在我們開(kāi)始研究地址關(guān)系轉(zhuǎn)換表,其實(shí)它有個(gè)更加專業(yè)的名字——頁(yè)表。它描述了虛擬地址到物理地址的轉(zhuǎn)換關(guān)系,也可以說(shuō)是虛擬頁(yè)到物理頁(yè)的映射關(guān)系,所以稱為頁(yè)表。
為了增加靈活性和節(jié)約物理內(nèi)存空間(因?yàn)轫?yè)表是放在物理內(nèi)存中的),所以頁(yè)表中并不存放虛擬地址和物理地址的對(duì)應(yīng)關(guān)系,只存放物理頁(yè)面的地址,MMU 以虛擬地址為索引去查表返回物理頁(yè)面地址,而且頁(yè)表是分級(jí)的,總體分為三個(gè)部分:一個(gè)頂級(jí)頁(yè)目錄,多個(gè)中級(jí)頁(yè)目錄,最后才是頁(yè)表。.
上圖中 CR3 就是 CPU 的一個(gè)的寄存器,MMU 就是根據(jù)這個(gè)寄存器找到頁(yè)目錄的。所以,每個(gè)進(jìn)程都有一個(gè)頁(yè)表基地址,我們每次切換進(jìn)程都會(huì)把當(dāng)前cpu寄存器的值入棧,這叫環(huán)境保護(hù),等cpu再次切換回來(lái)的時(shí)候出棧,恢復(fù)cpu寄存器大值,這叫環(huán)境恢復(fù)。
七、內(nèi)存映射原理分析
內(nèi)存映射即在進(jìn)程的虛擬地址空間中創(chuàng)建一個(gè)映射,分為兩種:(1)文件映射:文件支持的內(nèi)存映射,把文件的一個(gè)區(qū)間映射到進(jìn)程的虛擬地址空間, 數(shù)據(jù)源是存儲(chǔ)設(shè)備上的文件。(2)匿名映射:沒(méi)有文件支持的內(nèi)存映射,把物理內(nèi)存映射到進(jìn)程的虛擬地址空間, 沒(méi)有數(shù)據(jù)源。
創(chuàng)建內(nèi)存映射時(shí),在進(jìn)程的用戶虛擬地址空間中分配一個(gè)虛擬內(nèi)存區(qū)域。內(nèi)核采用延遲分配物理內(nèi)存的策略,在進(jìn)程第一次訪問(wèn)虛擬頁(yè)的時(shí)候,產(chǎn)生缺頁(yè)異常。如果是文件映射,那么分配物理頁(yè),把文件指定區(qū)間的數(shù)據(jù)讀到物理頁(yè)中,然后在頁(yè)表中把虛擬頁(yè)映射到物理頁(yè)。如果是匿名映射,就分配物理頁(yè),然后在頁(yè)表中把虛擬頁(yè)映射到物理頁(yè)。我們的內(nèi)存空間是分成一段段的,這叫分段機(jī)制。分段機(jī)制下的虛擬地址由兩部分組成,段選擇子和段內(nèi)偏移量。段選擇子就保存在咱們前面講過(guò)的段寄存器里面。段選擇子里面最重要的是段號(hào),用作段表的索引。段表里面保存的是這個(gè)段的基地址、段的界限和特權(quán)等級(jí)等。
虛擬地址中的段內(nèi)偏移量應(yīng)該位于 0 和段界限之間。如果段內(nèi)偏移量是合法的,就將段基地址加上段內(nèi)偏移量得到物理內(nèi)存地址。其實(shí) Linux 傾向于另外一種從虛擬地址到物理地址的轉(zhuǎn)換方式,稱為分頁(yè)(Paging)。對(duì)于物理內(nèi)存,操作系統(tǒng)把它分成一塊一塊大小相同的頁(yè),這樣更方便管理,例如有的內(nèi)存頁(yè)面長(zhǎng)時(shí)間不用了,可以暫時(shí)寫到硬盤上,稱為換出。一旦需要的時(shí)候,再加載進(jìn)來(lái),叫做換入。這樣可以擴(kuò)大可用物理內(nèi)存的大小,提高物理內(nèi)存的利用率。這個(gè)換入和換出都是以頁(yè)為單位的。頁(yè)面的大小一般為 4KB。為了能夠定位和訪問(wèn)每個(gè)頁(yè),需要有個(gè)頁(yè)表,保存每個(gè)頁(yè)的起始地址,再加上在頁(yè)內(nèi)的偏移量,組成線性地址,就能對(duì)于內(nèi)存中的每個(gè)位置進(jìn)行訪問(wèn)了。
虛擬地址分為兩部分,頁(yè)號(hào)和頁(yè)內(nèi)偏移。頁(yè)號(hào)作為頁(yè)表的索引,頁(yè)表包含物理頁(yè)每頁(yè)所在物理內(nèi)存的基地址。這個(gè)基地址與頁(yè)內(nèi)偏移的組合就形成了物理內(nèi)存地址。32 位環(huán)境下,虛擬地址空間共 4GB。如果分成 4KB 一個(gè)頁(yè),那就是 1M 個(gè)頁(yè)。每個(gè)頁(yè)表項(xiàng)需要 4 個(gè)字節(jié)來(lái)存儲(chǔ),那么整個(gè) 4GB 空間的映射就需要 4MB 的內(nèi)存來(lái)存儲(chǔ)映射表。如果每個(gè)進(jìn)程都有自己的映射表,100 個(gè)進(jìn)程就需要 400MB 的內(nèi)存。對(duì)于內(nèi)核來(lái)講,有點(diǎn)大了 。
頁(yè)表中所有頁(yè)表項(xiàng)必須提前建好,并且要求是連續(xù)的。如果不連續(xù),就沒(méi)有辦法通過(guò)虛擬地址里面的頁(yè)號(hào)找到對(duì)應(yīng)的頁(yè)表項(xiàng)了。那怎么辦呢?我們可以試著將頁(yè)表再分頁(yè),4G 的空間需要 4M 的頁(yè)表來(lái)存儲(chǔ)映射。我們把這 4M 分成 1K(1024)個(gè) 4K,每個(gè) 4K 又能放在一頁(yè)里面,這樣 1K 個(gè) 4K 就是 1K 個(gè)頁(yè),這 1K 個(gè)頁(yè)也需要一個(gè)表進(jìn)行管理,我們稱為頁(yè)目錄表,這個(gè)頁(yè)目錄表里面有 1K 項(xiàng),每項(xiàng) 4 個(gè)字節(jié),頁(yè)目錄表大小也是 4K。頁(yè)目錄有 1K 項(xiàng),用 10 位就可以表示訪問(wèn)頁(yè)目錄的哪一項(xiàng)。這一項(xiàng)其實(shí)對(duì)應(yīng)的是一整頁(yè)的頁(yè)表項(xiàng),也即 4K 的頁(yè)表項(xiàng)。每個(gè)頁(yè)表項(xiàng)也是 4 個(gè)字節(jié),因而一整頁(yè)的頁(yè)表項(xiàng)是 1K 個(gè)。再用 10 位就可以表示訪問(wèn)頁(yè)表項(xiàng)的哪一項(xiàng),頁(yè)表項(xiàng)中的一項(xiàng)對(duì)應(yīng)的就是一個(gè)頁(yè),是存放數(shù)據(jù)的頁(yè),這個(gè)頁(yè)的大小是 4K,用 12 位可以定位這個(gè)頁(yè)內(nèi)的任何一個(gè)位置。這樣加起來(lái)正好 32 位,也就是用前 10 位定位到頁(yè)目錄表中的一項(xiàng)。將這一項(xiàng)對(duì)應(yīng)的頁(yè)表取出來(lái)共 1k 項(xiàng),再用中間 10 位定位到頁(yè)表中的一項(xiàng),將這一項(xiàng)對(duì)應(yīng)的存放數(shù)據(jù)的頁(yè)取出來(lái),再用最后 12 位定位到頁(yè)中的具體位置訪問(wèn)數(shù)據(jù)。如下圖所示:
你可能會(huì)問(wèn),如果這樣的話,映射 4GB 地址空間就需要 4MB+4KB 的內(nèi)存,這樣不是更大了嗎?當(dāng)然如果頁(yè)是滿的,當(dāng)時(shí)是更大了,但是,我們往往不會(huì)為一個(gè)進(jìn)程分配那么多內(nèi)存。比如說(shuō),上面圖中,我們假設(shè)只給這個(gè)進(jìn)程分配了一個(gè)數(shù)據(jù)頁(yè)。如果只使用頁(yè)表,也需要完整的 1M 個(gè)頁(yè)表項(xiàng)共 4M 的內(nèi)存,但是如果使用了頁(yè)目錄,頁(yè)目錄需要 1K 個(gè)全部分配,占用內(nèi)存 4K,但是里面只有一項(xiàng)使用了。到了頁(yè)表項(xiàng),只需要分配能夠管理那個(gè)數(shù)據(jù)頁(yè)的頁(yè)表項(xiàng)頁(yè)就可以了,也就是說(shuō),最多 4K,這樣內(nèi)存就節(jié)省多了。當(dāng)然對(duì)于 64 位的系統(tǒng),兩級(jí)肯定不夠了,就變成了四級(jí)目錄,分別是全局頁(yè)目錄項(xiàng) PGD(Page Global Directory)、上層頁(yè)目錄項(xiàng) PUD(Page Upper Directory)、中間頁(yè)目錄項(xiàng) PMD(Page Middle Directory)和頁(yè)表項(xiàng) PTE(Page Table Entry)。