萬(wàn)字帶你深入理解 Linux 虛擬內(nèi)存管理(下)
接上文:萬(wàn)字帶你深入理解 Linux 虛擬內(nèi)存管理(上)
6. 程序編譯后的二進(jìn)制文件如何映射到虛擬內(nèi)存空間中
經(jīng)過(guò)前邊這么多小節(jié)的內(nèi)容介紹,現(xiàn)在我們已經(jīng)熟悉了進(jìn)程虛擬內(nèi)存空間的布局,以及內(nèi)核如何管理這些虛擬內(nèi)存區(qū)域,并對(duì)進(jìn)程的虛擬內(nèi)存空間有了一個(gè)完整全面的認(rèn)識(shí)。
現(xiàn)在我們?cè)賮?lái)回到最初的起點(diǎn),進(jìn)程的虛擬內(nèi)存空間 mm_struct 以及這些虛擬內(nèi)存區(qū)域 vm_area_struct 是如何被創(chuàng)建并初始化的呢?

在 《3. 進(jìn)程虛擬內(nèi)存空間》小節(jié)中,我們介紹進(jìn)程的虛擬內(nèi)存空間時(shí)提到,我們寫(xiě)的程序代碼編譯之后會(huì)生成一個(gè) ELF 格式的二進(jìn)制文件,這個(gè)二進(jìn)制文件中包含了程序運(yùn)行時(shí)所需要的元信息,比如程序的機(jī)器碼,程序中的全局變量以及靜態(tài)變量等。
這個(gè) ELF 格式的二進(jìn)制文件中的布局和我們前邊講的虛擬內(nèi)存空間中的布局類似,也是一段一段的,每一段包含了不同的元數(shù)據(jù)。
磁盤(pán)文件中的段我們叫做 Section,內(nèi)存中的段我們叫做 Segment,也就是內(nèi)存區(qū)域。
磁盤(pán)文件中的這些 Section 會(huì)在進(jìn)程運(yùn)行之前加載到內(nèi)存中并映射到內(nèi)存中的 Segment。通常是多個(gè) Section 映射到一個(gè) Segment。
比如磁盤(pán)文件中的 .text,.rodata 等一些只讀的 Section,會(huì)被映射到內(nèi)存的一個(gè)只讀可執(zhí)行的 Segment 里(代碼段)。而 .data,.bss 等一些可讀寫(xiě)的 Section,則會(huì)被映射到內(nèi)存的一個(gè)具有讀寫(xiě)權(quán)限的 Segment 里(數(shù)據(jù)段,BSS 段)。
那么這些 ELF 格式的二進(jìn)制文件中的 Section 是如何加載并映射進(jìn)虛擬內(nèi)存空間的呢?
內(nèi)核中完成這個(gè)映射過(guò)程的函數(shù)是 load_elf_binary ,這個(gè)函數(shù)的作用很大,加載內(nèi)核的是它,啟動(dòng)第一個(gè)用戶態(tài)進(jìn)程 init 的是它,fork 完了以后,調(diào)用 exec 運(yùn)行一個(gè)二進(jìn)制程序的也是它。當(dāng) exec 運(yùn)行一個(gè)二進(jìn)制程序的時(shí)候,除了解析 ELF 的格式之外,另外一個(gè)重要的事情就是建立上述提到的內(nèi)存映射。
setup_new_exec 設(shè)置虛擬內(nèi)存空間中的內(nèi)存映射區(qū)域起始地址 mmap_base
setup_arg_pages 創(chuàng)建并初始化棧對(duì)應(yīng)的 vm_area_struct 結(jié)構(gòu)。置 mm->start_stack 就是棧的起始地址也就是棧底,并將 mm->arg_start 是指向棧底的。
elf_map 將 ELF 格式的二進(jìn)制文件中.text ,.data,.bss 部分映射到虛擬內(nèi)存空間中的代碼段,數(shù)據(jù)段,BSS 段中。
set_brk 創(chuàng)建并初始化堆對(duì)應(yīng)的的 vm_area_struct 結(jié)構(gòu),設(shè)置?
current->mm->start_brk = current->mm->brk
,設(shè)置堆的起始地址 start_brk,結(jié)束地址 brk。 起初兩者相等表示堆是空的。load_elf_interp 將進(jìn)程依賴的動(dòng)態(tài)鏈接庫(kù) .so 文件映射到虛擬內(nèi)存空間中的內(nèi)存映射區(qū)域
初始化內(nèi)存描述符 mm_struct
7. 內(nèi)核虛擬內(nèi)存空間
現(xiàn)在我們已經(jīng)知道了進(jìn)程虛擬內(nèi)存空間在內(nèi)核中的布局以及管理,那么內(nèi)核態(tài)的虛擬內(nèi)存空間又是什么樣子的呢?本小節(jié)筆者就帶大家來(lái)一層一層地拆開(kāi)這個(gè)黑盒子。
之前在介紹進(jìn)程虛擬內(nèi)存空間的時(shí)候,筆者提到不同進(jìn)程之間的虛擬內(nèi)存空間是相互隔離的,彼此之間相互獨(dú)立,相互感知不到其他進(jìn)程的存在。使得進(jìn)程以為自己擁有所有的內(nèi)存資源。

而內(nèi)核態(tài)虛擬內(nèi)存空間是所有進(jìn)程共享的,不同進(jìn)程進(jìn)入內(nèi)核態(tài)之后看到的虛擬內(nèi)存空間全部是一樣的。
什么意思呢?比如上圖中的進(jìn)程 a,進(jìn)程 b,進(jìn)程 c 分別在各自的用戶態(tài)虛擬內(nèi)存空間中訪問(wèn)虛擬地址 x 。由于進(jìn)程之間的用戶態(tài)虛擬內(nèi)存空間是相互隔離相互獨(dú)立的,雖然在進(jìn)程a,進(jìn)程b,進(jìn)程c 訪問(wèn)的都是虛擬地址 x 但是看到的內(nèi)容卻是不一樣的(背后可能映射到不同的物理內(nèi)存中)。
但是當(dāng)進(jìn)程 a,進(jìn)程 b,進(jìn)程 c 進(jìn)入到內(nèi)核態(tài)之后情況就不一樣了,由于內(nèi)核虛擬內(nèi)存空間是各個(gè)進(jìn)程共享的,所以它們?cè)趦?nèi)核空間中看到的內(nèi)容全部是一樣的,比如進(jìn)程 a,進(jìn)程 b,進(jìn)程 c 在內(nèi)核態(tài)都去訪問(wèn)虛擬地址 y。這時(shí)它們看到的內(nèi)容就是一樣的了。
這里筆者和大家澄清一個(gè)經(jīng)常被誤解的概念:由于內(nèi)核會(huì)涉及到物理內(nèi)存的管理,所以很多人會(huì)想當(dāng)然地認(rèn)為只要進(jìn)入了內(nèi)核態(tài)就開(kāi)始使用物理地址了,這就大錯(cuò)特錯(cuò)了,千萬(wàn)不要這樣理解,進(jìn)程進(jìn)入內(nèi)核態(tài)之后使用的仍然是虛擬內(nèi)存地址,只不過(guò)在內(nèi)核中使用的虛擬內(nèi)存地址被限制在了內(nèi)核態(tài)虛擬內(nèi)存空間范圍中,這也是本小節(jié)筆者要為大家介紹的主題。
在清楚了這個(gè)基本概念之后,下面筆者分別從 32 位體系 和 64 位體系下為大家介紹內(nèi)核態(tài)虛擬內(nèi)存空間的布局。
7.1 32 位體系內(nèi)核虛擬內(nèi)存空間布局
在前邊《5.1 內(nèi)核如何劃分用戶態(tài)和內(nèi)核態(tài)虛擬內(nèi)存空間》小節(jié)中我們提到,內(nèi)核在?/arch/x86/include/asm/page_32_types.h
?文件中通過(guò) TASK_SIZE 將進(jìn)程虛擬內(nèi)存空間和內(nèi)核虛擬內(nèi)存空間分割開(kāi)來(lái)。
__PAGE_OFFSET 的值在 32 位系統(tǒng)下為 0xC000 000

在 32 位體系結(jié)構(gòu)下進(jìn)程用戶態(tài)虛擬內(nèi)存空間為 3 GB,虛擬內(nèi)存地址范圍為:0x0000 0000 - 0xC000 000 。內(nèi)核態(tài)虛擬內(nèi)存空間為 1 GB,虛擬內(nèi)存地址范圍為:0xC000 000 - 0xFFFF FFFF。
本小節(jié)我們主要關(guān)注 0xC000 000 - 0xFFFF FFFF 這段虛擬內(nèi)存地址區(qū)域也就是內(nèi)核虛擬內(nèi)存空間的布局情況。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺(jué)得比較好的學(xué)習(xí)書(shū)籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書(shū)、實(shí)戰(zhàn)項(xiàng)目及代碼)? ? ??


7.1.1 直接映射區(qū)
在總共大小 1G 的內(nèi)核虛擬內(nèi)存空間中,位于最前邊有一塊 896M 大小的區(qū)域,我們稱之為直接映射區(qū)或者線性映射區(qū),地址范圍為 3G -- 3G + 896m 。
之所以這塊 896M 大小的區(qū)域稱為直接映射區(qū)或者線性映射區(qū),是因?yàn)檫@塊連續(xù)的虛擬內(nèi)存地址會(huì)映射到 0 - 896M 這塊連續(xù)的物理內(nèi)存上。
也就是說(shuō) 3G -- 3G + 896m 這塊 896M 大小的虛擬內(nèi)存會(huì)直接映射到 0 - 896M 這塊 896M 大小的物理內(nèi)存上,這塊區(qū)域中的虛擬內(nèi)存地址直接減去 0xC000 0000 (3G) 就得到了物理內(nèi)存地址。所以我們稱這塊區(qū)域?yàn)橹苯佑成鋮^(qū)。
為了方便為大家解釋,我們假設(shè)現(xiàn)在機(jī)器上的物理內(nèi)存為 4G 大小

雖然這塊區(qū)域中的虛擬地址是直接映射到物理地址上,但是內(nèi)核在訪問(wèn)這段區(qū)域的時(shí)候還是走的虛擬內(nèi)存地址,內(nèi)核也會(huì)為這塊空間建立映射頁(yè)表。關(guān)于頁(yè)表的概念筆者后續(xù)會(huì)為大家詳細(xì)講解,這里大家只需要簡(jiǎn)單理解為頁(yè)表保存了虛擬地址到物理地址的映射關(guān)系即可。
大家這里只需要記得內(nèi)核態(tài)虛擬內(nèi)存空間的前 896M 區(qū)域是直接映射到物理內(nèi)存中的前 896M 區(qū)域中的,直接映射區(qū)中的映射關(guān)系是一比一映射。映射關(guān)系是固定的不會(huì)改變。
明白了這個(gè)關(guān)系之后,我們接下來(lái)就看一下這塊直接映射區(qū)域在物理內(nèi)存中究竟存的是什么內(nèi)容~~~
在這段 896M 大小的物理內(nèi)存中,前 1M 已經(jīng)在系統(tǒng)啟動(dòng)的時(shí)候被系統(tǒng)占用,1M 之后的物理內(nèi)存存放的是內(nèi)核代碼段,數(shù)據(jù)段,BSS 段(這些信息起初存放在 ELF格式的二進(jìn)制文件中,在系統(tǒng)啟動(dòng)的時(shí)候被加載進(jìn)內(nèi)存)。
我們可以通過(guò)?
cat /proc/iomem
?命令查看具體物理內(nèi)存布局情況。
當(dāng)我們使用 fork 系統(tǒng)調(diào)用創(chuàng)建進(jìn)程的時(shí)候,內(nèi)核會(huì)創(chuàng)建一系列進(jìn)程相關(guān)的描述符,比如之前提到的進(jìn)程的核心數(shù)據(jù)結(jié)構(gòu) task_struct,進(jìn)程的內(nèi)存空間描述符 mm_struct,以及虛擬內(nèi)存區(qū)域描述符 vm_area_struct 等。
這些進(jìn)程相關(guān)的數(shù)據(jù)結(jié)構(gòu)也會(huì)存放在物理內(nèi)存前 896M 的這段區(qū)域中,當(dāng)然也會(huì)被直接映射至內(nèi)核態(tài)虛擬內(nèi)存空間中的 3G -- 3G + 896m 這段直接映射區(qū)域中。

當(dāng)進(jìn)程被創(chuàng)建完畢之后,在內(nèi)核運(yùn)行的過(guò)程中,會(huì)涉及內(nèi)核棧的分配,內(nèi)核會(huì)為每個(gè)進(jìn)程分配一個(gè)固定大小的內(nèi)核棧(一般是兩個(gè)頁(yè)大小,依賴具體的體系結(jié)構(gòu)),每個(gè)進(jìn)程的整個(gè)調(diào)用鏈必須放在自己的內(nèi)核棧中,內(nèi)核棧也是分配在直接映射區(qū)。
與進(jìn)程用戶空間中的棧不同的是,內(nèi)核棧容量小而且是固定的,用戶空間中的棧容量大而且可以動(dòng)態(tài)擴(kuò)展。內(nèi)核棧的溢出危害非常巨大,它會(huì)直接悄無(wú)聲息的覆蓋相鄰內(nèi)存區(qū)域中的數(shù)據(jù),破壞數(shù)據(jù)。
通過(guò)以上內(nèi)容的介紹我們了解到內(nèi)核虛擬內(nèi)存空間最前邊的這段 896M 大小的直接映射區(qū)如何與物理內(nèi)存進(jìn)行映射關(guān)聯(lián),并且清楚了直接映射區(qū)主要用來(lái)存放哪些內(nèi)容。
寫(xiě)到這里,筆者覺(jué)得還是有必要再次從功能劃分的角度為大家介紹下這塊直接映射區(qū)域。
我們都知道內(nèi)核對(duì)物理內(nèi)存的管理都是以頁(yè)為最小單位來(lái)管理的,每頁(yè)默認(rèn) 4K 大小,理想狀況下任何種類的數(shù)據(jù)頁(yè)都可以存放在任何頁(yè)框中,沒(méi)有什么限制。比如:存放內(nèi)核數(shù)據(jù),用戶數(shù)據(jù),緩沖磁盤(pán)數(shù)據(jù)等。
但是實(shí)際的計(jì)算機(jī)體系結(jié)構(gòu)受到硬件方面的限制制約,間接導(dǎo)致限制了頁(yè)框的使用方式。
比如在 X86 體系結(jié)構(gòu)下,ISA 總線的 DMA (直接內(nèi)存存?。┛刂破?,只能對(duì)內(nèi)存的前16M 進(jìn)行尋址,這就導(dǎo)致了 ISA 設(shè)備不能在整個(gè) 32 位地址空間中執(zhí)行 DMA,只能使用物理內(nèi)存的前 16M 進(jìn)行 DMA 操作。
因此直接映射區(qū)的前 16M 專門(mén)讓內(nèi)核用來(lái)為 DMA 分配內(nèi)存,這塊 16M 大小的內(nèi)存區(qū)域我們稱之為 ZONE_DMA。
用于 DMA 的內(nèi)存必須從 ZONE_DMA 區(qū)域中分配。
而直接映射區(qū)中剩下的部分也就是從 16M 到 896M(不包含 896M)這段區(qū)域,我們稱之為 ZONE_NORMAL。從字面意義上我們可以了解到,這塊區(qū)域包含的就是正常的頁(yè)框(使用沒(méi)有任何限制)。
ZONE_NORMAL 由于也是屬于直接映射區(qū)的一部分,對(duì)應(yīng)的物理內(nèi)存 16M 到 896M 這段區(qū)域也是被直接映射至內(nèi)核態(tài)虛擬內(nèi)存空間中的 3G + 16M 到 3G + 896M 這段虛擬內(nèi)存上。

注意這里的 ZONE_DMA 和 ZONE_NORMAL 是內(nèi)核針對(duì)物理內(nèi)存區(qū)域的劃分。
現(xiàn)在物理內(nèi)存中的前 896M 的區(qū)域也就是前邊介紹的 ZONE_DMA 和 ZONE_NORMAL 區(qū)域到內(nèi)核虛擬內(nèi)存空間的映射筆者就為大家介紹完了,它們都是采用直接映射的方式,一比一就行映射。
7.1.2 ?ZONE_HIGHMEM 高端內(nèi)存
而物理內(nèi)存 896M 以上的區(qū)域被內(nèi)核劃分為 ZONE_HIGHMEM 區(qū)域,我們稱之為高端內(nèi)存。
本例中我們的物理內(nèi)存假設(shè)為 4G,高端內(nèi)存區(qū)域?yàn)?4G - 896M = 3200M,那么這塊 3200M 大小的 ZONE_HIGHMEM 區(qū)域該如何映射到內(nèi)核虛擬內(nèi)存空間中呢?
由于內(nèi)核虛擬內(nèi)存空間中的前 896M 虛擬內(nèi)存已經(jīng)被直接映射區(qū)所占用,而在 32 體系結(jié)構(gòu)下內(nèi)核虛擬內(nèi)存空間總共也就 1G 的大小,這樣一來(lái)內(nèi)核剩余可用的虛擬內(nèi)存空間就變?yōu)榱?1G - 896M = 128M。
顯然物理內(nèi)存中 3200M 大小的 ZONE_HIGHMEM 區(qū)域無(wú)法繼續(xù)通過(guò)直接映射的方式映射到這 128M 大小的虛擬內(nèi)存空間中。
這樣一來(lái)物理內(nèi)存中的 ZONE_HIGHMEM 區(qū)域就只能采用動(dòng)態(tài)映射的方式映射到 128M 大小的內(nèi)核虛擬內(nèi)存空間中,也就是說(shuō)只能動(dòng)態(tài)的一部分一部分的分批映射,先映射正在使用的這部分,使用完畢解除映射,接著映射其他部分。
知道了 ZONE_HIGHMEM 區(qū)域的映射原理,我們接著往下看這 128M 大小的內(nèi)核虛擬內(nèi)存空間究竟是如何布局的?

內(nèi)核虛擬內(nèi)存空間中的 3G + 896M 這塊地址在內(nèi)核中定義為 high_memory,high_memory 往上有一段 8M 大小的內(nèi)存空洞??斩捶秶鸀椋篽igh_memory 到 ?VMALLOC_START 。
VMALLOC_START 定義在內(nèi)核源碼?/arch/x86/include/asm/pgtable_32_areas.h
?文件中:
7.1.3 vmalloc 動(dòng)態(tài)映射區(qū)
接下來(lái) VMALLOC_START 到 VMALLOC_END 之間的這塊區(qū)域成為動(dòng)態(tài)映射區(qū)。采用動(dòng)態(tài)映射的方式映射物理內(nèi)存中的高端內(nèi)存。

和用戶態(tài)進(jìn)程使用 malloc 申請(qǐng)內(nèi)存一樣,在這塊動(dòng)態(tài)映射區(qū)內(nèi)核是使用 vmalloc 進(jìn)行內(nèi)存分配。由于之前介紹的動(dòng)態(tài)映射的原因,vmalloc 分配的內(nèi)存在虛擬內(nèi)存上是連續(xù)的,但是物理內(nèi)存是不連續(xù)的。通過(guò)頁(yè)表來(lái)建立物理內(nèi)存與虛擬內(nèi)存之間的映射關(guān)系,從而可以將不連續(xù)的物理內(nèi)存映射到連續(xù)的虛擬內(nèi)存上。
由于 vmalloc 獲得的物理內(nèi)存頁(yè)是不連續(xù)的,因此它只能將這些物理內(nèi)存頁(yè)一個(gè)一個(gè)地進(jìn)行映射,在性能開(kāi)銷上會(huì)比直接映射大得多。
關(guān)于 vmalloc 分配內(nèi)存的相關(guān)實(shí)現(xiàn)原理,筆者會(huì)在后面的文章中為大家講解,這里大家只需要明白它在哪塊虛擬內(nèi)存區(qū)域中活動(dòng)即可。
7.1.4 永久映射區(qū)

而在 PKMAP_BASE 到 FIXADDR_START 之間的這段空間稱為永久映射區(qū)。在內(nèi)核的這段虛擬地址空間中允許建立與物理高端內(nèi)存的長(zhǎng)期映射關(guān)系。比如內(nèi)核通過(guò) alloc_pages() 函數(shù)在物理內(nèi)存的高端內(nèi)存中申請(qǐng)獲取到的物理內(nèi)存頁(yè),這些物理內(nèi)存頁(yè)可以通過(guò)調(diào)用 kmap 映射到永久映射區(qū)中。
LAST_PKMAP 表示永久映射區(qū)可以映射的頁(yè)數(shù)限制。
8.1.5 固定映射區(qū)

內(nèi)核虛擬內(nèi)存空間中的下一個(gè)區(qū)域?yàn)楣潭ㄓ成鋮^(qū),區(qū)域范圍為:FIXADDR_START 到 FIXADDR_TOP。
FIXADDR_START 和 FIXADDR_TOP 定義在內(nèi)核源碼?/arch/x86/include/asm/fixmap.h
?文件中:
在內(nèi)核虛擬內(nèi)存空間的直接映射區(qū)中,直接映射區(qū)中的虛擬內(nèi)存地址與物理內(nèi)存前 896M 的空間的映射關(guān)系都是預(yù)設(shè)好的,一比一映射。
在固定映射區(qū)中的虛擬內(nèi)存地址可以自由映射到物理內(nèi)存的高端地址上,但是與動(dòng)態(tài)映射區(qū)以及永久映射區(qū)不同的是,在固定映射區(qū)中虛擬地址是固定的,而被映射的物理地址是可以改變的。也就是說(shuō),有些虛擬地址在編譯的時(shí)候就固定下來(lái)了,是在內(nèi)核啟動(dòng)過(guò)程中被確定的,而這些虛擬地址對(duì)應(yīng)的物理地址不是固定的。采用固定虛擬地址的好處是它相當(dāng)于一個(gè)指針常量(常量的值在編譯時(shí)確定),指向物理地址,如果虛擬地址不固定,則相當(dāng)于一個(gè)指針變量。
那為什么會(huì)有固定映射這個(gè)概念呢 ? ?比如:在內(nèi)核的啟動(dòng)過(guò)程中,有些模塊需要使用虛擬內(nèi)存并映射到指定的物理地址上,而且這些模塊也沒(méi)有辦法等待完整的內(nèi)存管理模塊初始化之后再進(jìn)行地址映射。因此,內(nèi)核固定分配了一些虛擬地址,這些地址有固定的用途,使用該地址的模塊在初始化的時(shí)候,將這些固定分配的虛擬地址映射到指定的物理地址上去。
7.1.6 ?臨時(shí)映射區(qū)
在內(nèi)核虛擬內(nèi)存空間中的最后一塊區(qū)域?yàn)榕R時(shí)映射區(qū),那么這塊臨時(shí)映射區(qū)是用來(lái)干什么的呢?

筆者在之前文章?《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫(xiě)本質(zhì)》?的 “ 12.3 iov_iter_copy_from_user_atomic ” 小節(jié)中介紹在 Buffered IO 模式下進(jìn)行文件寫(xiě)入的時(shí)候,在下圖中的第四步,內(nèi)核會(huì)調(diào)用 iov_iter_copy_from_user_atomic 函數(shù)將用戶空間緩沖區(qū) DirectByteBuffer 中的待寫(xiě)入數(shù)據(jù)拷貝到 page cache 中。

但是內(nèi)核又不能直接進(jìn)行拷貝,因?yàn)榇藭r(shí)從 page cache 中取出的緩存頁(yè) page 是物理地址,而在內(nèi)核中是不能夠直接操作物理地址的,只能操作虛擬地址。
那怎么辦呢?所以就需要使用 kmap_atomic 將緩存頁(yè)臨時(shí)映射到內(nèi)核空間的一段虛擬地址上,這段虛擬地址就位于內(nèi)核虛擬內(nèi)存空間中的臨時(shí)映射區(qū)上,然后將用戶空間緩存區(qū) DirectByteBuffer 中的待寫(xiě)入數(shù)據(jù)通過(guò)這段映射的虛擬地址拷貝到 page cache 中的相應(yīng)緩存頁(yè)中。這時(shí)文件的寫(xiě)入操作就已經(jīng)完成了。
由于是臨時(shí)映射,所以在拷貝完成之后,調(diào)用 kunmap_atomic 將這段映射再解除掉。
7.1.7 32位體系結(jié)構(gòu)下 Linux 虛擬內(nèi)存空間整體布局
到現(xiàn)在為止,整個(gè)內(nèi)核虛擬內(nèi)存空間在 32 位體系下的布局,筆者就為大家詳細(xì)介紹完畢了,我們?cè)俅谓Y(jié)合前邊《4.1 32 位機(jī)器上進(jìn)程虛擬內(nèi)存空間分布》小節(jié)中介紹的進(jìn)程虛擬內(nèi)存空間和本小節(jié)介紹的內(nèi)核虛擬內(nèi)存空間來(lái)整體回顧下 32 位體系結(jié)構(gòu) Linux 的整個(gè)虛擬內(nèi)存空間的布局:

7.2 64 位體系內(nèi)核虛擬內(nèi)存空間布局
內(nèi)核虛擬內(nèi)存空間在 32 位體系下只有 1G 大小,實(shí)在太小了,因此需要精細(xì)化的管理,于是按照功能分類劃分除了很多內(nèi)核虛擬內(nèi)存區(qū)域,這樣就顯得非常復(fù)雜。
到了 64 位體系下,內(nèi)核虛擬內(nèi)存空間的布局和管理就變得容易多了,因?yàn)檫M(jìn)程虛擬內(nèi)存空間和內(nèi)核虛擬內(nèi)存空間各自占用 128T 的虛擬內(nèi)存,實(shí)在是太大了,我們可以在這里邊隨意翱翔,隨意揮霍。
因此在 64 位體系下的內(nèi)核虛擬內(nèi)存空間與物理內(nèi)存的映射就變得非常簡(jiǎn)單,由于虛擬內(nèi)存空間足夠的大,即便是內(nèi)核要訪問(wèn)全部的物理內(nèi)存,直接映射就可以了,不在需要用到《7.1.2 ZONE_HIGHMEM 高端內(nèi)存》小節(jié)中介紹的高端內(nèi)存那種動(dòng)態(tài)映射方式。
在前邊《5.1 內(nèi)核如何劃分用戶態(tài)和內(nèi)核態(tài)虛擬內(nèi)存空間》小節(jié)中我們提到,內(nèi)核在?/arch/x86/include/asm/page_64_types.h
?文件中通過(guò) TASK_SIZE 將進(jìn)程虛擬內(nèi)存空間和內(nèi)核虛擬內(nèi)存空間分割開(kāi)來(lái)。
64 位系統(tǒng)中的 TASK_SIZE 為 0x00007FFFFFFFF000

在 64 位系統(tǒng)中,只使用了其中的低 48 位來(lái)表示虛擬內(nèi)存地址。其中用戶態(tài)虛擬內(nèi)存空間為低 128 T,虛擬內(nèi)存地址范圍為:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 。
內(nèi)核態(tài)虛擬內(nèi)存空間為高 128 T,虛擬內(nèi)存地址范圍為:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 。
本小節(jié)我們主要關(guān)注 0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 這段內(nèi)核虛擬內(nèi)存空間的布局情況。

64 位內(nèi)核虛擬內(nèi)存空間從 0xFFFF 8000 0000 0000 開(kāi)始到 0xFFFF 8800 0000 0000 這段地址空間是一個(gè) 8T 大小的內(nèi)存空洞區(qū)域。
緊著著 8T 大小的內(nèi)存空洞下一個(gè)區(qū)域就是 64T 大小的直接映射區(qū)。這個(gè)區(qū)域中的虛擬內(nèi)存地址減去 PAGE_OFFSET 就直接得到了物理內(nèi)存地址。
PAGE_OFFSET 變量定義在?/arch/x86/include/asm/page_64_types.h
?文件中:
從圖中 VMALLOC_START 到 VMALLOC_END 的這段區(qū)域是 32T 大小的 vmalloc 映射區(qū),這里類似用戶空間中的堆,內(nèi)核在這里使用 vmalloc 系統(tǒng)調(diào)用申請(qǐng)內(nèi)存。
VMALLOC_START 和 ?VMALLOC_END 變量定義在?/arch/x86/include/asm/pgtable_64_types.h
?文件中:
從 VMEMMAP_START 開(kāi)始是 1T 大小的虛擬內(nèi)存映射區(qū),用于存放物理頁(yè)面的描述符 struct page 結(jié)構(gòu)用來(lái)表示物理內(nèi)存頁(yè)。
VMEMMAP_START 變量定義在?/arch/x86/include/asm/pgtable_64_types.h
?文件中:
從 __START_KERNEL_map 開(kāi)始是大小為 512M 的區(qū)域用于存放內(nèi)核代碼段、全局變量、BSS 等。這里對(duì)應(yīng)到物理內(nèi)存開(kāi)始的位置,減去 __START_KERNEL_map 就能得到物理內(nèi)存的地址。這里和直接映射區(qū)有點(diǎn)像,但是不矛盾,因?yàn)橹苯佑成鋮^(qū)之前有 8T 的空洞區(qū)域,早就過(guò)了內(nèi)核代碼在物理內(nèi)存中加載的位置。
__START_KERNEL_map 變量定義在?/arch/x86/include/asm/page_64_types.h
?文件中:
7.2.1 64位體系結(jié)構(gòu)下 Linux 虛擬內(nèi)存空間整體布局
到現(xiàn)在為止,整個(gè)內(nèi)核虛擬內(nèi)存空間在 64 位體系下的布局筆者就為大家詳細(xì)介紹完畢了,我們?cè)俅谓Y(jié)合前邊《4.2 64 位機(jī)器上進(jìn)程虛擬內(nèi)存空間分布》小節(jié)介紹的進(jìn)程虛擬內(nèi)存空間和本小節(jié)介紹的內(nèi)核虛擬內(nèi)存空間來(lái)整體回顧下 64 位體系結(jié)構(gòu) Linux 的整個(gè)虛擬內(nèi)存空間的布局:

8. 到底什么是物理內(nèi)存地址
聊完了虛擬內(nèi)存,我們接著聊一下物理內(nèi)存,我們平時(shí)所稱的內(nèi)存也叫隨機(jī)訪問(wèn)存儲(chǔ)器( random-access memory )也叫 RAM 。而 RAM 分為兩類:
一類是靜態(tài) RAM(?
SRAM
?),這類 SRAM 用于 CPU 高速緩存 L1Cache,L2Cache,L3Cache。其特點(diǎn)是訪問(wèn)速度快,訪問(wèn)速度為 1 - 30 個(gè)時(shí)鐘周期,但是容量小,造價(jià)高。

另一類則是動(dòng)態(tài) RAM (?
DRAM
?),這類 DRAM 用于我們常說(shuō)的主存上,其特點(diǎn)的是訪問(wèn)速度慢(相對(duì)高速緩存),訪問(wèn)速度為 50 - 200 個(gè)時(shí)鐘周期,但是容量大,造價(jià)便宜些(相對(duì)高速緩存)。
內(nèi)存由一個(gè)一個(gè)的存儲(chǔ)器模塊(memory module)組成,它們插在主板的擴(kuò)展槽上。常見(jiàn)的存儲(chǔ)器模塊通常以 64 位為單位( 8 個(gè)字節(jié))傳輸數(shù)據(jù)到存儲(chǔ)控制器上或者從存儲(chǔ)控制器傳出數(shù)據(jù)。

如圖所示內(nèi)存條上黑色的元器件就是存儲(chǔ)器模塊(memory module)。多個(gè)存儲(chǔ)器模塊連接到存儲(chǔ)控制器上,就聚合成了主存。

而 DRAM 芯片就包裝在存儲(chǔ)器模塊中,每個(gè)存儲(chǔ)器模塊中包含 8 個(gè) DRAM 芯片,依次編號(hào)為 0 - 7 。

而每一個(gè) DRAM 芯片的存儲(chǔ)結(jié)構(gòu)是一個(gè)二維矩陣,二維矩陣中存儲(chǔ)的元素我們稱為超單元(supercell),每個(gè) supercell 大小為一個(gè)字節(jié)(8 bit)。每個(gè) supercell 都由一個(gè)坐標(biāo)地址(i,j)。
i 表示二維矩陣中的行地址,在計(jì)算機(jī)中行地址稱為 RAS (row access strobe,行訪問(wèn)選通脈沖)。 j 表示二維矩陣中的列地址,在計(jì)算機(jī)中列地址稱為 CAS (column access strobe,列訪問(wèn)選通脈沖)。
下圖中的 supercell 的 RAS = 2,CAS = 2。

DRAM 芯片中的信息通過(guò)引腳流入流出 DRAM 芯片。每個(gè)引腳攜帶 1 bit的信號(hào)。
圖中 DRAM 芯片包含了兩個(gè)地址引腳(?addr
?),因?yàn)槲覀円ㄟ^(guò) RAS,CAS 來(lái)定位要獲取的 supercell 。還有 8 個(gè)數(shù)據(jù)引腳(data
),因?yàn)?DRAM 芯片的 IO 單位為一個(gè)字節(jié)(8 bit),所以需要 8 個(gè) data 引腳從 DRAM 芯片傳入傳出數(shù)據(jù)。
注意這里只是為了解釋地址引腳和數(shù)據(jù)引腳的概念,實(shí)際硬件中的引腳數(shù)量是不一定的。
8.1 DRAM 芯片的訪問(wèn)
我們現(xiàn)在就以讀取上圖中坐標(biāo)地址為(2,2)的 supercell 為例,來(lái)說(shuō)明訪問(wèn) DRAM 芯片的過(guò)程。

首先存儲(chǔ)控制器將行地址 RAS = 2 通過(guò)地址引腳發(fā)送給 DRAM 芯片。
DRAM 芯片根據(jù) RAS = 2 將二維矩陣中的第二行的全部?jī)?nèi)容拷貝到內(nèi)部行緩沖區(qū)中。
接下來(lái)存儲(chǔ)控制器會(huì)通過(guò)地址引腳發(fā)送 CAS = 2 到 DRAM 芯片中。
DRAM芯片從內(nèi)部行緩沖區(qū)中根據(jù) CAS = 2 拷貝出第二列的 supercell 并通過(guò)數(shù)據(jù)引腳發(fā)送給存儲(chǔ)控制器。
DRAM 芯片的 IO 單位為一個(gè) supercell ,也就是一個(gè)字節(jié)(8 bit)。
8.2 CPU 如何讀寫(xiě)主存
前邊我們介紹了內(nèi)存的物理結(jié)構(gòu),以及如何訪問(wèn)內(nèi)存中的 DRAM 芯片獲取 supercell 中存儲(chǔ)的數(shù)據(jù)(一個(gè)字節(jié))。本小節(jié)我們來(lái)介紹下 CPU 是如何訪問(wèn)內(nèi)存的:

CPU 與內(nèi)存之間的數(shù)據(jù)交互是通過(guò)總線(bus)完成的,而數(shù)據(jù)在總線上的傳送是通過(guò)一系列的步驟完成的,這些步驟稱為總線事務(wù)(bus transaction)。
其中數(shù)據(jù)從內(nèi)存?zhèn)魉偷?CPU 稱之為讀事務(wù)(read transaction),數(shù)據(jù)從 CPU 傳送到內(nèi)存稱之為寫(xiě)事務(wù)(write transaction)。
總線上傳輸?shù)男盘?hào)包括:地址信號(hào),數(shù)據(jù)信號(hào),控制信號(hào)。其中控制總線上傳輸?shù)目刂菩盘?hào)可以同步事務(wù),并能夠標(biāo)識(shí)出當(dāng)前正在被執(zhí)行的事務(wù)信息:
當(dāng)前這個(gè)事務(wù)是到內(nèi)存的?還是到磁盤(pán)的?或者是到其他 IO 設(shè)備的?
這個(gè)事務(wù)是讀還是寫(xiě)?
總線上傳輸?shù)牡刂沸盘?hào)(物理內(nèi)存地址),還是數(shù)據(jù)信號(hào)(數(shù)據(jù))?。
這里大家需要注意總線上傳輸?shù)牡刂肪鶠槲锢韮?nèi)存地址。比如:在 MESI 緩存一致性協(xié)議中當(dāng) CPU core0 修改字段 a 的值時(shí),其他 CPU 核心會(huì)在總線上嗅探字段 a 的物理內(nèi)存地址,如果嗅探到總線上出現(xiàn)字段 a 的物理內(nèi)存地址,說(shuō)明有人在修改字段 a,這樣其他 CPU 核心就會(huì)失效字段 a 所在的 cache line 。
如上圖所示,其中系統(tǒng)總線是連接 CPU 與 IO bridge 的,存儲(chǔ)總線是來(lái)連接 IO bridge 和主存的。
IO bridge 負(fù)責(zé)將系統(tǒng)總線上的電子信號(hào)轉(zhuǎn)換成存儲(chǔ)總線上的電子信號(hào)。IO bridge 也會(huì)將系統(tǒng)總線和存儲(chǔ)總線連接到IO總線(磁盤(pán)等IO設(shè)備)上。這里我們看到 IO bridge 其實(shí)起的作用就是轉(zhuǎn)換不同總線上的電子信號(hào)。
8.3 CPU 從內(nèi)存讀取數(shù)據(jù)過(guò)程
假設(shè) CPU 現(xiàn)在需要將物理內(nèi)存地址為 A 的內(nèi)容加載到寄存器中進(jìn)行運(yùn)算。
大家需要注意的是 CPU 只會(huì)訪問(wèn)虛擬內(nèi)存,在操作總線之前,需要把虛擬內(nèi)存地址轉(zhuǎn)換為物理內(nèi)存地址,總線上傳輸?shù)亩际俏锢韮?nèi)存地址,這里省略了虛擬內(nèi)存地址到物理內(nèi)存地址的轉(zhuǎn)換過(guò)程,這部分內(nèi)容筆者會(huì)在后續(xù)文章的相關(guān)章節(jié)詳細(xì)為大家講解,這里我們聚焦如果通過(guò)物理內(nèi)存地址讀取內(nèi)存數(shù)據(jù)。

首先 CPU 芯片中的總線接口會(huì)在總線上發(fā)起讀事務(wù)(read transaction)。 該讀事務(wù)分為以下步驟進(jìn)行:
CPU 將物理內(nèi)存地址 A 放到系統(tǒng)總線上。隨后 IO bridge 將信號(hào)傳遞到存儲(chǔ)總線上。
主存感受到存儲(chǔ)總線上的地址信號(hào)并通過(guò)存儲(chǔ)控制器將存儲(chǔ)總線上的物理內(nèi)存地址 A 讀取出來(lái)。
存儲(chǔ)控制器通過(guò)物理內(nèi)存地址 A 定位到具體的存儲(chǔ)器模塊,從 DRAM 芯片中取出物理內(nèi)存地址 A 對(duì)應(yīng)的數(shù)據(jù) X。
存儲(chǔ)控制器將讀取到的數(shù)據(jù) X 放到存儲(chǔ)總線上,隨后 IO bridge 將存儲(chǔ)總線上的數(shù)據(jù)信號(hào)轉(zhuǎn)換為系統(tǒng)總線上的數(shù)據(jù)信號(hào),然后繼續(xù)沿著系統(tǒng)總線傳遞。
CPU 芯片感受到系統(tǒng)總線上的數(shù)據(jù)信號(hào),將數(shù)據(jù)從系統(tǒng)總線上讀取出來(lái)并拷貝到寄存器中。
以上就是 CPU 讀取內(nèi)存數(shù)據(jù)到寄存器中的完整過(guò)程。
但是其中還涉及到一個(gè)重要的過(guò)程,這里我們還是需要攤開(kāi)來(lái)介紹一下,那就是存儲(chǔ)控制器如何通過(guò)物理內(nèi)存地址 A 從主存中讀取出對(duì)應(yīng)的數(shù)據(jù) X 的?
接下來(lái)我們結(jié)合前邊介紹的內(nèi)存結(jié)構(gòu)以及從 DRAM 芯片讀取數(shù)據(jù)的過(guò)程,來(lái)總體介紹下如何從主存中讀取數(shù)據(jù)。
8.4 如何根據(jù)物理內(nèi)存地址從主存中讀取數(shù)據(jù)
前邊介紹到,當(dāng)主存中的存儲(chǔ)控制器感受到了存儲(chǔ)總線上的地址信號(hào)時(shí),會(huì)將內(nèi)存地址從存儲(chǔ)總線上讀取出來(lái)。
隨后會(huì)通過(guò)內(nèi)存地址定位到具體的存儲(chǔ)器模塊。還記得內(nèi)存結(jié)構(gòu)中的存儲(chǔ)器模塊嗎 ?

而每個(gè)存儲(chǔ)器模塊中包含了 8 個(gè) DRAM 芯片,編號(hào)從 0 - 7 。

存儲(chǔ)控制器會(huì)將物理內(nèi)存地址轉(zhuǎn)換為 DRAM 芯片中 supercell 在二維矩陣中的坐標(biāo)地址(RAS,CAS)。并將這個(gè)坐標(biāo)地址發(fā)送給對(duì)應(yīng)的存儲(chǔ)器模塊。隨后存儲(chǔ)器模塊會(huì)將 RAS 和 CAS 廣播到存儲(chǔ)器模塊中的所有 DRAM 芯片。依次通過(guò) (RAS,CAS) 從 DRAM0 到 DRAM7 讀取到相應(yīng)的 supercell 。

我們知道一個(gè) supercell 存儲(chǔ)了一個(gè)字節(jié)( 8 bit ) 數(shù)據(jù),這里我們從 DRAM0 到 DRAM7 依次讀取到了 8 個(gè) supercell 也就是 8 個(gè)字節(jié),然后將這 8 個(gè)字節(jié)返回給存儲(chǔ)控制器,由存儲(chǔ)控制器將數(shù)據(jù)放到存儲(chǔ)總線上。
CPU 總是以 word size 為單位從內(nèi)存中讀取數(shù)據(jù),在 64 位處理器中的 word size 為 8 個(gè)字節(jié)。64 位的內(nèi)存每次只能吞吐 8 個(gè)字節(jié)。
CPU 每次會(huì)向內(nèi)存讀寫(xiě)一個(gè) cache line 大小的數(shù)據(jù)( 64 個(gè)字節(jié)),但是內(nèi)存一次只能吞吐 8 個(gè)字節(jié)。
所以在物理內(nèi)存地址對(duì)應(yīng)的存儲(chǔ)器模塊中,DRAM0 芯片存儲(chǔ)第一個(gè)低位字節(jié)( supercell ),DRAM1 芯片存儲(chǔ)第二個(gè)字節(jié),......依次類推 DRAM7 芯片存儲(chǔ)最后一個(gè)高位字節(jié)。

由于存儲(chǔ)器模塊中這種由 8 個(gè) DRAM 芯片組成的物理存儲(chǔ)結(jié)構(gòu)的限制,內(nèi)存讀取數(shù)據(jù)只能是按照物理內(nèi)存地址,8 個(gè)字節(jié) 8 個(gè)字節(jié)地順序讀取數(shù)據(jù)。所以說(shuō)內(nèi)存一次讀取和寫(xiě)入的單位是 8 個(gè)字節(jié)。

而且在程序員眼里連續(xù)的物理內(nèi)存地址實(shí)際上在物理上是不連續(xù)的。因?yàn)檫@連續(xù)的 8 個(gè)字節(jié)其實(shí)是存儲(chǔ)于不同的 DRAM 芯片上的。每個(gè) DRAM 芯片存儲(chǔ)一個(gè)字節(jié)(supercell)
8.5 CPU 向內(nèi)存寫(xiě)入數(shù)據(jù)過(guò)程
我們現(xiàn)在假設(shè) CPU 要將寄存器中的數(shù)據(jù) X 寫(xiě)到物理內(nèi)存地址 A 中。同樣的道理,CPU 芯片中的總線接口會(huì)向總線發(fā)起寫(xiě)事務(wù)(write transaction)。寫(xiě)事務(wù)步驟如下:
CPU 將要寫(xiě)入的物理內(nèi)存地址 A 放入系統(tǒng)總線上。
通過(guò) IO bridge 的信號(hào)轉(zhuǎn)換,將物理內(nèi)存地址 A 傳遞到存儲(chǔ)總線上。
存儲(chǔ)控制器感受到存儲(chǔ)總線上的地址信號(hào),將物理內(nèi)存地址 A 從存儲(chǔ)總線上讀取出來(lái),并等待數(shù)據(jù)的到達(dá)。
CPU 將寄存器中的數(shù)據(jù)拷貝到系統(tǒng)總線上,通過(guò) IO bridge 的信號(hào)轉(zhuǎn)換,將數(shù)據(jù)傳遞到存儲(chǔ)總線上。
存儲(chǔ)控制器感受到存儲(chǔ)總線上的數(shù)據(jù)信號(hào),將數(shù)據(jù)從存儲(chǔ)總線上讀取出來(lái)。
存儲(chǔ)控制器通過(guò)內(nèi)存地址 A 定位到具體的存儲(chǔ)器模塊,最后將數(shù)據(jù)寫(xiě)入存儲(chǔ)器模塊中的 8 個(gè) DRAM 芯片中。
總結(jié)
本文我們從虛擬內(nèi)存地址開(kāi)始聊起,一直到物理內(nèi)存地址結(jié)束,包含的信息量還是比較大的。首先筆者通過(guò)一個(gè)進(jìn)程的運(yùn)行實(shí)例為大家引出了內(nèi)核引入虛擬內(nèi)存空間的目的及其需要解決的問(wèn)題。
在我們有了虛擬內(nèi)存空間的概念之后,筆者又近一步為大家介紹了內(nèi)核如何劃分用戶態(tài)虛擬內(nèi)存空間和內(nèi)核態(tài)虛擬內(nèi)存空間,并在次基礎(chǔ)之上分別從 32 位體系結(jié)構(gòu)和 64 位體系結(jié)構(gòu)的角度詳細(xì)闡述了 Linux 虛擬內(nèi)存空間的整體布局分布。
我們可以通過(guò)?
cat /proc/pid/maps
?或者?pmap pid
?命令來(lái)查看進(jìn)程用戶態(tài)虛擬內(nèi)存空間的實(shí)際分布。還可以通過(guò)?
cat /proc/iomem
?命令來(lái)查看進(jìn)程內(nèi)核態(tài)虛擬內(nèi)存空間的的實(shí)際分布。
在我們清楚了 ?Linux 虛擬內(nèi)存空間的整體布局分布之后,筆者又介紹了 Linux 內(nèi)核如何對(duì)分布在虛擬內(nèi)存空間中的各個(gè)虛擬內(nèi)存區(qū)域進(jìn)行管理,以及每個(gè)虛擬內(nèi)存區(qū)域的作用。在這個(gè)過(guò)程中還介紹了相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu),近一步從內(nèi)核源碼實(shí)現(xiàn)角度加深大家對(duì)虛擬內(nèi)存空間的理解。
最后筆者介紹了物理內(nèi)存的結(jié)構(gòu),以及 CPU 如何通過(guò)物理內(nèi)存地址來(lái)讀寫(xiě)內(nèi)存中的數(shù)據(jù)。這里筆者需要特地再次強(qiáng)調(diào)的是 CPU 只會(huì)訪問(wèn)虛擬內(nèi)存地址,只不過(guò)在操作總線之前,通過(guò)一個(gè)地址轉(zhuǎn)換硬件將虛擬內(nèi)存地址轉(zhuǎn)換為物理內(nèi)存地址,然后將物理內(nèi)存地址作為地址信號(hào)放在總線上傳輸,由于地址轉(zhuǎn)換的內(nèi)容和本文主旨無(wú)關(guān),考慮到文章的篇幅以及復(fù)雜性,筆者就沒(méi)有過(guò)多的介紹。
原文作者:bin的技術(shù)小屋
