我們接著上篇文章《一步一圖帶你深入理解 Linux 虛擬內(nèi)存管理(上)》 繼續(xù):
7. 內(nèi)核虛擬內(nèi)存空間
現(xiàn)在我們已經(jīng)知道了進程虛擬內(nèi)存空間在內(nèi)核中的布局以及管理,那么內(nèi)核態(tài)的虛擬內(nèi)存空間又是什么樣子的呢腿箩?本小節(jié)筆者就帶大家來一層一層地拆開這個黑盒子豪直。
之前在介紹進程虛擬內(nèi)存空間的時候,筆者提到不同進程之間的虛擬內(nèi)存空間是相互隔離的度秘,彼此之間相互獨立顶伞,相互感知不到其他進程的存在。使得進程以為自己擁有所有的內(nèi)存資源剑梳。
而內(nèi)核態(tài)虛擬內(nèi)存空間是所有進程共享的唆貌,不同進程進入內(nèi)核態(tài)之后看到的虛擬內(nèi)存空間全部是一樣的。
什么意思呢垢乙?比如上圖中的進程 a锨咙,進程 b,進程 c 分別在各自的用戶態(tài)虛擬內(nèi)存空間中訪問虛擬地址 x 追逮。由于進程之間的用戶態(tài)虛擬內(nèi)存空間是相互隔離相互獨立的酪刀,雖然在進程a,進程b钮孵,進程c 訪問的都是虛擬地址 x 但是看到的內(nèi)容卻是不一樣的(背后可能映射到不同的物理內(nèi)存中)骂倘。
但是當進程 a,進程 b巴席,進程 c 進入到內(nèi)核態(tài)之后情況就不一樣了历涝,由于內(nèi)核虛擬內(nèi)存空間是各個進程共享的,所以它們在內(nèi)核空間中看到的內(nèi)容全部是一樣的漾唉,比如進程 a荧库,進程 b,進程 c 在內(nèi)核態(tài)都去訪問虛擬地址 y赵刑。這時它們看到的內(nèi)容就是一樣的了分衫。
這里筆者和大家澄清一個經(jīng)常被誤解的概念:由于內(nèi)核會涉及到物理內(nèi)存的管理,所以很多人會想當然地認為只要進入了內(nèi)核態(tài)就開始使用物理地址了般此,這就大錯特錯了蚪战,千萬不要這樣理解,進程進入內(nèi)核態(tài)之后使用的仍然是虛擬內(nèi)存地址铐懊,只不過在內(nèi)核中使用的虛擬內(nèi)存地址被限制在了內(nèi)核態(tài)虛擬內(nèi)存空間范圍中屎勘,這也是本小節(jié)筆者要為大家介紹的主題。
在清楚了這個基本概念之后居扒,下面筆者分別從 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
文件中通過 TASK_SIZE 將進程虛擬內(nèi)存空間和內(nèi)核虛擬內(nèi)存空間分割開來喜喂。
/*
* User space process size: 3GB (default).
*/
#define TASK_SIZE __PAGE_OFFSET
__PAGE_OFFSET 的值在 32 位系統(tǒng)下為 0xC000 000
在 32 位體系結(jié)構(gòu)下進程用戶態(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)存空間的布局情況腻异。
7.1.1 直接映射區(qū)
在總共大小 1G 的內(nèi)核虛擬內(nèi)存空間中,位于最前邊有一塊 896M 大小的區(qū)域这揣,我們稱之為直接映射區(qū)或者線性映射區(qū)悔常,地址范圍為 3G -- 3G + 896m 。
之所以這塊 896M 大小的區(qū)域稱為直接映射區(qū)或者線性映射區(qū)给赞,是因為這塊連續(xù)的虛擬內(nèi)存地址會映射到 0 - 896M 這塊連續(xù)的物理內(nèi)存上机打。
也就是說 3G -- 3G + 896m 這塊 896M 大小的虛擬內(nèi)存會直接映射到 0 - 896M 這塊 896M 大小的物理內(nèi)存上,這塊區(qū)域中的虛擬內(nèi)存地址直接減去 0xC000 0000 (3G) 就得到了物理內(nèi)存地址片迅。所以我們稱這塊區(qū)域為直接映射區(qū)残邀。
為了方便為大家解釋,我們假設(shè)現(xiàn)在機器上的物理內(nèi)存為 4G 大小
雖然這塊區(qū)域中的虛擬地址是直接映射到物理地址上柑蛇,但是內(nèi)核在訪問這段區(qū)域的時候還是走的虛擬內(nèi)存地址芥挣,內(nèi)核也會為這塊空間建立映射頁表。關(guān)于頁表的概念筆者后續(xù)會為大家詳細講解耻台,這里大家只需要簡單理解為頁表保存了虛擬地址到物理地址的映射關(guān)系即可空免。
大家這里只需要記得內(nèi)核態(tài)虛擬內(nèi)存空間的前 896M 區(qū)域是直接映射到物理內(nèi)存中的前 896M 區(qū)域中的,直接映射區(qū)中的映射關(guān)系是一比一映射盆耽。映射關(guān)系是固定的不會改變鼓蜒。
明白了這個關(guān)系之后,我們接下來就看一下這塊直接映射區(qū)域在物理內(nèi)存中究竟存的是什么內(nèi)容~~~
在這段 896M 大小的物理內(nèi)存中征字,前 1M 已經(jīng)在系統(tǒng)啟動的時候被系統(tǒng)占用,1M 之后的物理內(nèi)存存放的是內(nèi)核代碼段娇豫,數(shù)據(jù)段匙姜,BSS 段(這些信息起初存放在 ELF格式的二進制文件中,在系統(tǒng)啟動的時候被加載進內(nèi)存)冯痢。
我們可以通過
cat /proc/iomem
命令查看具體物理內(nèi)存布局情況氮昧。
當我們使用 fork 系統(tǒng)調(diào)用創(chuàng)建進程的時候,內(nèi)核會創(chuàng)建一系列進程相關(guān)的描述符浦楣,比如之前提到的進程的核心數(shù)據(jù)結(jié)構(gòu) task_struct袖肥,進程的內(nèi)存空間描述符 mm_struct,以及虛擬內(nèi)存區(qū)域描述符 vm_area_struct 等振劳。
這些進程相關(guān)的數(shù)據(jù)結(jié)構(gòu)也會存放在物理內(nèi)存前 896M 的這段區(qū)域中椎组,當然也會被直接映射至內(nèi)核態(tài)虛擬內(nèi)存空間中的 3G -- 3G + 896m 這段直接映射區(qū)域中。
當進程被創(chuàng)建完畢之后历恐,在內(nèi)核運行的過程中寸癌,會涉及內(nèi)核棧的分配专筷,內(nèi)核會為每個進程分配一個固定大小的內(nèi)核棧(一般是兩個頁大小,依賴具體的體系結(jié)構(gòu))蒸苇,每個進程的整個調(diào)用鏈必須放在自己的內(nèi)核棧中磷蛹,內(nèi)核棧也是分配在直接映射區(qū)。
與進程用戶空間中的棧不同的是溪烤,內(nèi)核棧容量小而且是固定的味咳,用戶空間中的棧容量大而且可以動態(tài)擴展。內(nèi)核棧的溢出危害非常巨大檬嘀,它會直接悄無聲息的覆蓋相鄰內(nèi)存區(qū)域中的數(shù)據(jù)槽驶,破壞數(shù)據(jù)。
通過以上內(nèi)容的介紹我們了解到內(nèi)核虛擬內(nèi)存空間最前邊的這段 896M 大小的直接映射區(qū)如何與物理內(nèi)存進行映射關(guān)聯(lián)枪眉,并且清楚了直接映射區(qū)主要用來存放哪些內(nèi)容捺檬。
寫到這里,筆者覺得還是有必要再次從功能劃分的角度為大家介紹下這塊直接映射區(qū)域贸铜。
我們都知道內(nèi)核對物理內(nèi)存的管理都是以頁為最小單位來管理的堡纬,每頁默認 4K 大小,理想狀況下任何種類的數(shù)據(jù)頁都可以存放在任何頁框中蒿秦,沒有什么限制烤镐。比如:存放內(nèi)核數(shù)據(jù),用戶數(shù)據(jù)棍鳖,緩沖磁盤數(shù)據(jù)等炮叶。
但是實際的計算機體系結(jié)構(gòu)受到硬件方面的限制制約,間接導致限制了頁框的使用方式渡处。
比如在 X86 體系結(jié)構(gòu)下镜悉,ISA 總線的 DMA (直接內(nèi)存存取)控制器医瘫,只能對內(nèi)存的前16M 進行尋址侣肄,這就導致了 ISA 設(shè)備不能在整個 32 位地址空間中執(zhí)行 DMA,只能使用物理內(nèi)存的前 16M 進行 DMA 操作醇份。
因此直接映射區(qū)的前 16M 專門讓內(nèi)核用來為 DMA 分配內(nèi)存稼锅,這塊 16M 大小的內(nèi)存區(qū)域我們稱之為 ZONE_DMA。
用于 DMA 的內(nèi)存必須從 ZONE_DMA 區(qū)域中分配僚纷。
而直接映射區(qū)中剩下的部分也就是從 16M 到 896M(不包含 896M)這段區(qū)域矩距,我們稱之為 ZONE_NORMAL。從字面意義上我們可以了解到怖竭,這塊區(qū)域包含的就是正常的頁框(使用沒有任何限制)锥债。
ZONE_NORMAL 由于也是屬于直接映射區(qū)的一部分,對應(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)核針對物理內(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ū)域為 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 的大小总棵,這樣一來內(nèi)核剩余可用的虛擬內(nèi)存空間就變?yōu)榱?1G - 896M = 128M。
顯然物理內(nèi)存中 3200M 大小的 ZONE_HIGHMEM 區(qū)域無法繼續(xù)通過直接映射的方式映射到這 128M 大小的虛擬內(nèi)存空間中改含。
這樣一來物理內(nèi)存中的 ZONE_HIGHMEM 區(qū)域就只能采用動態(tài)映射的方式映射到 128M 大小的內(nèi)核虛擬內(nèi)存空間中情龄,也就是說只能動態(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)存空洞盗扇〉豢希空洞范圍為:high_memory 到 VMALLOC_START 。
VMALLOC_START 定義在內(nèi)核源碼 /arch/x86/include/asm/pgtable_32_areas.h
文件中:
#define VMALLOC_OFFSET (8 * 1024 * 1024)
#define VMALLOC_START ((unsigned long)high_memory + VMALLOC_OFFSET)
7.1.3 vmalloc 動態(tài)映射區(qū)
接下來 VMALLOC_START 到 VMALLOC_END 之間的這塊區(qū)域成為動態(tài)映射區(qū)疗隶。采用動態(tài)映射的方式映射物理內(nèi)存中的高端內(nèi)存佑笋。
#ifdef CONFIG_HIGHMEM
# define VMALLOC_END (PKMAP_BASE - 2 * PAGE_SIZE)
#else
# define VMALLOC_END (LDT_BASE_ADDR - 2 * PAGE_SIZE)
#endif
和用戶態(tài)進程使用 malloc 申請內(nèi)存一樣,在這塊動態(tài)映射區(qū)內(nèi)核是使用 vmalloc 進行內(nèi)存分配斑鼻。由于之前介紹的動態(tài)映射的原因蒋纬,vmalloc 分配的內(nèi)存在虛擬內(nèi)存上是連續(xù)的,但是物理內(nèi)存是不連續(xù)的卵沉。通過頁表來建立物理內(nèi)存與虛擬內(nèi)存之間的映射關(guān)系,從而可以將不連續(xù)的物理內(nèi)存映射到連續(xù)的虛擬內(nèi)存上法牲。
由于 vmalloc 獲得的物理內(nèi)存頁是不連續(xù)的史汗,因此它只能將這些物理內(nèi)存頁一個一個地進行映射,在性能開銷上會比直接映射大得多拒垃。
關(guān)于 vmalloc 分配內(nèi)存的相關(guān)實現(xiàn)原理停撞,筆者會在后面的文章中為大家講解,這里大家只需要明白它在哪塊虛擬內(nèi)存區(qū)域中活動即可。
7.1.4 永久映射區(qū)
而在 PKMAP_BASE 到 FIXADDR_START 之間的這段空間稱為永久映射區(qū)戈毒。在內(nèi)核的這段虛擬地址空間中允許建立與物理高端內(nèi)存的長期映射關(guān)系艰猬。比如內(nèi)核通過 alloc_pages() 函數(shù)在物理內(nèi)存的高端內(nèi)存中申請獲取到的物理內(nèi)存頁,這些物理內(nèi)存頁可以通過調(diào)用 kmap 映射到永久映射區(qū)中埋市。
LAST_PKMAP 表示永久映射區(qū)可以映射的頁數(shù)限制冠桃。
#define PKMAP_BASE \
((LDT_BASE_ADDR - PAGE_SIZE) & PMD_MASK)
#define LAST_PKMAP 1024
8.1.5 固定映射區(qū)
內(nèi)核虛擬內(nèi)存空間中的下一個區(qū)域為固定映射區(qū),區(qū)域范圍為:FIXADDR_START 到 FIXADDR_TOP道宅。
FIXADDR_START 和 FIXADDR_TOP 定義在內(nèi)核源碼 /arch/x86/include/asm/fixmap.h
文件中:
#define FIXADDR_START (FIXADDR_TOP - FIXADDR_SIZE)
extern unsigned long __FIXADDR_TOP; // 0xFFFF F000
#define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)
在內(nèi)核虛擬內(nèi)存空間的直接映射區(qū)中食听,直接映射區(qū)中的虛擬內(nèi)存地址與物理內(nèi)存前 896M 的空間的映射關(guān)系都是預(yù)設(shè)好的,一比一映射污茵。
在固定映射區(qū)中的虛擬內(nèi)存地址可以自由映射到物理內(nèi)存的高端地址上樱报,但是與動態(tài)映射區(qū)以及永久映射區(qū)不同的是,在固定映射區(qū)中虛擬地址是固定的泞当,而被映射的物理地址是可以改變的迹蛤。也就是說,有些虛擬地址在編譯的時候就固定下來了襟士,是在內(nèi)核啟動過程中被確定的盗飒,而這些虛擬地址對應(yīng)的物理地址不是固定的。采用固定虛擬地址的好處是它相當于一個指針常量(常量的值在編譯時確定)敌蜂,指向物理地址箩兽,如果虛擬地址不固定,則相當于一個指針變量章喉。
那為什么會有固定映射這個概念呢 ? 比如:在內(nèi)核的啟動過程中汗贫,有些模塊需要使用虛擬內(nèi)存并映射到指定的物理地址上,而且這些模塊也沒有辦法等待完整的內(nèi)存管理模塊初始化之后再進行地址映射秸脱。因此落包,內(nèi)核固定分配了一些虛擬地址,這些地址有固定的用途摊唇,使用該地址的模塊在初始化的時候咐蝇,將這些固定分配的虛擬地址映射到指定的物理地址上去。
7.1.6 臨時映射區(qū)
在內(nèi)核虛擬內(nèi)存空間中的最后一塊區(qū)域為臨時映射區(qū)巷查,那么這塊臨時映射區(qū)是用來干什么的呢有序?
筆者在之前文章 《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫本質(zhì)》 的 “ 12.3 iov_iter_copy_from_user_atomic ” 小節(jié)中介紹在 Buffered IO 模式下進行文件寫入的時候,在下圖中的第四步岛请,內(nèi)核會調(diào)用 iov_iter_copy_from_user_atomic 函數(shù)將用戶空間緩沖區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)拷貝到 page cache 中旭寿。
但是內(nèi)核又不能直接進行拷貝,因為此時從 page cache 中取出的緩存頁 page 是物理地址崇败,而在內(nèi)核中是不能夠直接操作物理地址的盅称,只能操作虛擬地址肩祥。
那怎么辦呢?所以就需要使用 kmap_atomic 將緩存頁臨時映射到內(nèi)核空間的一段虛擬地址上缩膝,這段虛擬地址就位于內(nèi)核虛擬內(nèi)存空間中的臨時映射區(qū)上混狠,然后將用戶空間緩存區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)通過這段映射的虛擬地址拷貝到 page cache 中的相應(yīng)緩存頁中。這時文件的寫入操作就已經(jīng)完成了疾层。
由于是臨時映射将饺,所以在拷貝完成之后,調(diào)用 kunmap_atomic 將這段映射再解除掉云芦。
size_t iov_iter_copy_from_user_atomic(struct page *page,
struct iov_iter *i, unsigned long offset, size_t bytes)
{
// 將緩存頁臨時映射到內(nèi)核虛擬地址空間的臨時映射區(qū)中
char *kaddr = kmap_atomic(page),
*p = kaddr + offset;
// 將用戶緩存區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)拷貝到文件緩存頁中
iterate_all_kinds(i, bytes, v,
copyin((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len),
memcpy_from_page((p += v.bv_len) - v.bv_len, v.bv_page,
v.bv_offset, v.bv_len),
memcpy((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len)
)
// 解除內(nèi)核虛擬地址空間與緩存頁之間的臨時映射俯逾,這里映射只是為了臨時拷貝數(shù)據(jù)用
kunmap_atomic(kaddr);
return bytes;
}
7.1.7 32位體系結(jié)構(gòu)下 Linux 虛擬內(nèi)存空間整體布局
到現(xiàn)在為止,整個內(nèi)核虛擬內(nèi)存空間在 32 位體系下的布局舅逸,筆者就為大家詳細介紹完畢了桌肴,我們再次結(jié)合前邊《4.1 32 位機器上進程虛擬內(nèi)存空間分布》小節(jié)中介紹的進程虛擬內(nèi)存空間和本小節(jié)介紹的內(nèi)核虛擬內(nèi)存空間來整體回顧下 32 位體系結(jié)構(gòu) Linux 的整個虛擬內(nèi)存空間的布局:
7.2 64 位體系內(nèi)核虛擬內(nèi)存空間布局
內(nèi)核虛擬內(nèi)存空間在 32 位體系下只有 1G 大小,實在太小了琉历,因此需要精細化的管理坠七,于是按照功能分類劃分除了很多內(nèi)核虛擬內(nèi)存區(qū)域,這樣就顯得非常復雜旗笔。
到了 64 位體系下彪置,內(nèi)核虛擬內(nèi)存空間的布局和管理就變得容易多了,因為進程虛擬內(nèi)存空間和內(nèi)核虛擬內(nèi)存空間各自占用 128T 的虛擬內(nèi)存蝇恶,實在是太大了拳魁,我們可以在這里邊隨意翱翔,隨意揮霍撮弧。
因此在 64 位體系下的內(nèi)核虛擬內(nèi)存空間與物理內(nèi)存的映射就變得非常簡單潘懊,由于虛擬內(nèi)存空間足夠的大县耽,即便是內(nèi)核要訪問全部的物理內(nèi)存玲献,直接映射就可以了窘茁,不在需要用到《7.1.2 ZONE_HIGHMEM 高端內(nèi)存》小節(jié)中介紹的高端內(nèi)存那種動態(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
文件中通過 TASK_SIZE 將進程虛擬內(nèi)存空間和內(nèi)核虛擬內(nèi)存空間分割開來惋鸥。
#define TASK_SIZE (test_thread_flag(TIF_ADDR32) ? \
IA32_PAGE_OFFSET : TASK_SIZE_MAX)
#define TASK_SIZE_MAX task_size_max()
#define task_size_max() ((_AC(1,UL) << __VIRTUAL_MASK_SHIFT) - PAGE_SIZE)
#define __VIRTUAL_MASK_SHIFT 47
64 位系統(tǒng)中的 TASK_SIZE 為 0x00007FFFFFFFF000
在 64 位系統(tǒng)中游沿,只使用了其中的低 48 位來表示虛擬內(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 開始到 0xFFFF 8800 0000 0000 這段地址空間是一個 8T 大小的內(nèi)存空洞區(qū)域。
緊著著 8T 大小的內(nèi)存空洞下一個區(qū)域就是 64T 大小的直接映射區(qū)炊汤。這個區(qū)域中的虛擬內(nèi)存地址減去 PAGE_OFFSET 就直接得到了物理內(nèi)存地址正驻。
PAGE_OFFSET 變量定義在 /arch/x86/include/asm/page_64_types.h
文件中:
#define __PAGE_OFFSET_BASE _AC(0xffff880000000000, UL)
#define __PAGE_OFFSET __PAGE_OFFSET_BASE
從圖中 VMALLOC_START 到 VMALLOC_END 的這段區(qū)域是 32T 大小的 vmalloc 映射區(qū),這里類似用戶空間中的堆抢腐,內(nèi)核在這里使用 vmalloc 系統(tǒng)調(diào)用申請內(nèi)存姑曙。
VMALLOC_START 和 VMALLOC_END 變量定義在 /arch/x86/include/asm/pgtable_64_types.h
文件中:
#define __VMALLOC_BASE_L4 0xffffc90000000000UL
#define VMEMMAP_START __VMEMMAP_BASE_L4
#define VMALLOC_END (VMALLOC_START + (VMALLOC_SIZE_TB << 40) - 1)
從 VMEMMAP_START 開始是 1T 大小的虛擬內(nèi)存映射區(qū),用于存放物理頁面的描述符 struct page 結(jié)構(gòu)用來表示物理內(nèi)存頁迈倍。
VMEMMAP_START 變量定義在 /arch/x86/include/asm/pgtable_64_types.h
文件中:
#define __VMEMMAP_BASE_L4 0xffffea0000000000UL
# define VMEMMAP_START __VMEMMAP_BASE_L4
從 __START_KERNEL_map 開始是大小為 512M 的區(qū)域用于存放內(nèi)核代碼段伤靠、全局變量、BSS 等啼染。這里對應(yīng)到物理內(nèi)存開始的位置宴合,減去 __START_KERNEL_map 就能得到物理內(nèi)存的地址。這里和直接映射區(qū)有點像迹鹅,但是不矛盾卦洽,因為直接映射區(qū)之前有 8T 的空洞區(qū)域,早就過了內(nèi)核代碼在物理內(nèi)存中加載的位置斜棚。
__START_KERNEL_map 變量定義在 /arch/x86/include/asm/page_64_types.h
文件中:
#define __START_KERNEL_map _AC(0xffffffff80000000, UL)
7.2.1 64位體系結(jié)構(gòu)下 Linux 虛擬內(nèi)存空間整體布局
到現(xiàn)在為止阀蒂,整個內(nèi)核虛擬內(nèi)存空間在 64 位體系下的布局筆者就為大家詳細介紹完畢了,我們再次結(jié)合前邊《4.2 64 位機器上進程虛擬內(nèi)存空間分布》小節(jié)介紹的進程虛擬內(nèi)存空間和本小節(jié)介紹的內(nèi)核虛擬內(nèi)存空間來整體回顧下 64 位體系結(jié)構(gòu) Linux 的整個虛擬內(nèi)存空間的布局:
8. 到底什么是物理內(nèi)存地址
聊完了虛擬內(nèi)存弟蚀,我們接著聊一下物理內(nèi)存蚤霞,我們平時所稱的內(nèi)存也叫隨機訪問存儲器( random-access memory )也叫 RAM 。而 RAM 分為兩類:
- 一類是靜態(tài) RAM(
SRAM
)义钉,這類 SRAM 用于 CPU 高速緩存 L1Cache昧绣,L2Cache,L3Cache捶闸。其特點是訪問速度快夜畴,訪問速度為 1 - 30 個時鐘周期,但是容量小鉴嗤,造價高斩启。
- 另一類則是動態(tài) RAM (
DRAM
),這類 DRAM 用于我們常說的主存上醉锅,其特點的是訪問速度慢(相對高速緩存)兔簇,訪問速度為 50 - 200 個時鐘周期,但是容量大硬耍,造價便宜些(相對高速緩存)垄琐。
內(nèi)存由一個一個的存儲器模塊(memory module)組成,它們插在主板的擴展槽上经柴。常見的存儲器模塊通常以 64 位為單位( 8 個字節(jié))傳輸數(shù)據(jù)到存儲控制器上或者從存儲控制器傳出數(shù)據(jù)狸窘。
如圖所示內(nèi)存條上黑色的元器件就是存儲器模塊(memory module)。多個存儲器模塊連接到存儲控制器上坯认,就聚合成了主存翻擒。
而 DRAM 芯片就包裝在存儲器模塊中氓涣,每個存儲器模塊中包含 8 個 DRAM 芯片,依次編號為 0 - 7 陋气。
而每一個 DRAM 芯片的存儲結(jié)構(gòu)是一個二維矩陣劳吠,二維矩陣中存儲的元素我們稱為超單元(supercell),每個 supercell 大小為一個字節(jié)(8 bit)巩趁。每個 supercell 都由一個坐標地址(i痒玩,j)。
i 表示二維矩陣中的行地址议慰,在計算機中行地址稱為 RAS (row access strobe蠢古,行訪問選通脈沖)。
j 表示二維矩陣中的列地址别凹,在計算機中列地址稱為 CAS (column access strobe,列訪問選通脈沖)草讶。
下圖中的 supercell 的 RAS = 2,CAS = 2炉菲。
DRAM 芯片中的信息通過引腳流入流出 DRAM 芯片到涂。每個引腳攜帶 1 bit的信號。
圖中 DRAM 芯片包含了兩個地址引腳( addr
)颁督,因為我們要通過 RAS践啄,CAS 來定位要獲取的 supercell 。還有 8 個數(shù)據(jù)引腳(data
)沉御,因為 DRAM 芯片的 IO 單位為一個字節(jié)(8 bit)屿讽,所以需要 8 個 data 引腳從 DRAM 芯片傳入傳出數(shù)據(jù)。
注意這里只是為了解釋地址引腳和數(shù)據(jù)引腳的概念吠裆,實際硬件中的引腳數(shù)量是不一定的伐谈。
8.1 DRAM 芯片的訪問
我們現(xiàn)在就以讀取上圖中坐標地址為(2,2)的 supercell 為例试疙,來說明訪問 DRAM 芯片的過程诵棵。
首先存儲控制器將行地址 RAS = 2 通過地址引腳發(fā)送給 DRAM 芯片。
DRAM 芯片根據(jù) RAS = 2 將二維矩陣中的第二行的全部內(nèi)容拷貝到內(nèi)部行緩沖區(qū)中祝旷。
接下來存儲控制器會通過地址引腳發(fā)送 CAS = 2 到 DRAM 芯片中履澳。
DRAM芯片從內(nèi)部行緩沖區(qū)中根據(jù) CAS = 2 拷貝出第二列的 supercell 并通過數(shù)據(jù)引腳發(fā)送給存儲控制器。
DRAM 芯片的 IO 單位為一個 supercell 怀跛,也就是一個字節(jié)(8 bit)距贷。
8.2 CPU 如何讀寫主存
前邊我們介紹了內(nèi)存的物理結(jié)構(gòu),以及如何訪問內(nèi)存中的 DRAM 芯片獲取 supercell 中存儲的數(shù)據(jù)(一個字節(jié))吻谋。本小節(jié)我們來介紹下 CPU 是如何訪問內(nèi)存的:
CPU 與內(nèi)存之間的數(shù)據(jù)交互是通過總線(bus)完成的忠蝗,而數(shù)據(jù)在總線上的傳送是通過一系列的步驟完成的,這些步驟稱為總線事務(wù)(bus transaction)漓拾。
其中數(shù)據(jù)從內(nèi)存?zhèn)魉偷?CPU 稱之為讀事務(wù)(read transaction)阁最,數(shù)據(jù)從 CPU 傳送到內(nèi)存稱之為寫事務(wù)(write transaction)戒祠。
總線上傳輸?shù)男盘柊ǎ旱刂沸盘枺瑪?shù)據(jù)信號速种,控制信號得哆。其中控制總線上傳輸?shù)目刂菩盘柨梢酝绞聞?wù),并能夠標識出當前正在被執(zhí)行的事務(wù)信息:
- 當前這個事務(wù)是到內(nèi)存的哟旗?還是到磁盤的?或者是到其他 IO 設(shè)備的栋操?
- 這個事務(wù)是讀還是寫闸餐?
- 總線上傳輸?shù)牡刂沸盘枺ㄎ锢韮?nèi)存地址),還是數(shù)據(jù)信號(數(shù)據(jù))矾芙?舍沙。
這里大家需要注意總線上傳輸?shù)牡刂肪鶠槲锢韮?nèi)存地址。比如:在 MESI 緩存一致性協(xié)議中當 CPU core0 修改字段 a 的值時剔宪,其他 CPU 核心會在總線上嗅探字段 a 的物理內(nèi)存地址拂铡,如果嗅探到總線上出現(xiàn)字段 a 的物理內(nèi)存地址,說明有人在修改字段 a葱绒,這樣其他 CPU 核心就會失效字段 a 所在的 cache line 感帅。
如上圖所示,其中系統(tǒng)總線是連接 CPU 與 IO bridge 的地淀,存儲總線是來連接 IO bridge 和主存的失球。
IO bridge 負責將系統(tǒng)總線上的電子信號轉(zhuǎn)換成存儲總線上的電子信號。IO bridge 也會將系統(tǒng)總線和存儲總線連接到IO總線(磁盤等IO設(shè)備)上帮毁。這里我們看到 IO bridge 其實起的作用就是轉(zhuǎn)換不同總線上的電子信號实苞。
8.3 CPU 從內(nèi)存讀取數(shù)據(jù)過程
假設(shè) CPU 現(xiàn)在需要將物理內(nèi)存地址為 A 的內(nèi)容加載到寄存器中進行運算。
大家需要注意的是 CPU 只會訪問虛擬內(nèi)存烈疚,在操作總線之前黔牵,需要把虛擬內(nèi)存地址轉(zhuǎn)換為物理內(nèi)存地址,總線上傳輸?shù)亩际俏锢韮?nèi)存地址爷肝,這里省略了虛擬內(nèi)存地址到物理內(nèi)存地址的轉(zhuǎn)換過程猾浦,這部分內(nèi)容筆者會在后續(xù)文章的相關(guān)章節(jié)詳細為大家講解,這里我們聚焦如果通過物理內(nèi)存地址讀取內(nèi)存數(shù)據(jù)灯抛。
首先 CPU 芯片中的總線接口會在總線上發(fā)起讀事務(wù)(read transaction)跃巡。 該讀事務(wù)分為以下步驟進行:
CPU 將物理內(nèi)存地址 A 放到系統(tǒng)總線上。隨后 IO bridge 將信號傳遞到存儲總線上牧愁。
主存感受到存儲總線上的地址信號并通過存儲控制器將存儲總線上的物理內(nèi)存地址 A 讀取出來素邪。
存儲控制器通過物理內(nèi)存地址 A 定位到具體的存儲器模塊,從 DRAM 芯片中取出物理內(nèi)存地址 A 對應(yīng)的數(shù)據(jù) X猪半。
存儲控制器將讀取到的數(shù)據(jù) X 放到存儲總線上兔朦,隨后 IO bridge 將存儲總線上的數(shù)據(jù)信號轉(zhuǎn)換為系統(tǒng)總線上的數(shù)據(jù)信號偷线,然后繼續(xù)沿著系統(tǒng)總線傳遞。
CPU 芯片感受到系統(tǒng)總線上的數(shù)據(jù)信號沽甥,將數(shù)據(jù)從系統(tǒng)總線上讀取出來并拷貝到寄存器中声邦。
以上就是 CPU 讀取內(nèi)存數(shù)據(jù)到寄存器中的完整過程。
但是其中還涉及到一個重要的過程摆舟,這里我們還是需要攤開來介紹一下亥曹,那就是存儲控制器如何通過物理內(nèi)存地址 A 從主存中讀取出對應(yīng)的數(shù)據(jù) X 的?
接下來我們結(jié)合前邊介紹的內(nèi)存結(jié)構(gòu)以及從 DRAM 芯片讀取數(shù)據(jù)的過程恨诱,來總體介紹下如何從主存中讀取數(shù)據(jù)媳瞪。
8.4 如何根據(jù)物理內(nèi)存地址從主存中讀取數(shù)據(jù)
前邊介紹到,當主存中的存儲控制器感受到了存儲總線上的地址信號時照宝,會將內(nèi)存地址從存儲總線上讀取出來蛇受。
隨后會通過內(nèi)存地址定位到具體的存儲器模塊。還記得內(nèi)存結(jié)構(gòu)中的存儲器模塊嗎 厕鹃?
而每個存儲器模塊中包含了 8 個 DRAM 芯片兢仰,編號從 0 - 7 。
存儲控制器會將物理內(nèi)存地址轉(zhuǎn)換為 DRAM 芯片中 supercell 在二維矩陣中的坐標地址(RAS剂碴,CAS)把将。并將這個坐標地址發(fā)送給對應(yīng)的存儲器模塊。隨后存儲器模塊會將 RAS 和 CAS 廣播到存儲器模塊中的所有 DRAM 芯片忆矛。依次通過 (RAS秸弛,CAS) 從 DRAM0 到 DRAM7 讀取到相應(yīng)的 supercell 。
我們知道一個 supercell 存儲了一個字節(jié)( 8 bit ) 數(shù)據(jù)洪碳,這里我們從 DRAM0 到 DRAM7 依次讀取到了 8 個 supercell 也就是 8 個字節(jié)递览,然后將這 8 個字節(jié)返回給存儲控制器,由存儲控制器將數(shù)據(jù)放到存儲總線上瞳腌。
CPU 總是以 word size 為單位從內(nèi)存中讀取數(shù)據(jù)绞铃,在 64 位處理器中的 word size 為 8 個字節(jié)。64 位的內(nèi)存每次只能吞吐 8 個字節(jié)嫂侍。
CPU 每次會向內(nèi)存讀寫一個 cache line 大小的數(shù)據(jù)( 64 個字節(jié))儿捧,但是內(nèi)存一次只能吞吐 8 個字節(jié)。
所以在物理內(nèi)存地址對應(yīng)的存儲器模塊中挑宠,DRAM0 芯片存儲第一個低位字節(jié)( supercell )菲盾,DRAM1 芯片存儲第二個字節(jié),......依次類推 DRAM7 芯片存儲最后一個高位字節(jié)各淀。
由于存儲器模塊中這種由 8 個 DRAM 芯片組成的物理存儲結(jié)構(gòu)的限制懒鉴,內(nèi)存讀取數(shù)據(jù)只能是按照物理內(nèi)存地址,8 個字節(jié) 8 個字節(jié)地順序讀取數(shù)據(jù)。所以說內(nèi)存一次讀取和寫入的單位是 8 個字節(jié)临谱。
而且在程序員眼里連續(xù)的物理內(nèi)存地址實際上在物理上是不連續(xù)的璃俗。因為這連續(xù)的 8 個字節(jié)其實是存儲于不同的 DRAM 芯片上的。每個 DRAM 芯片存儲一個字節(jié)(supercell)
8.5 CPU 向內(nèi)存寫入數(shù)據(jù)過程
我們現(xiàn)在假設(shè) CPU 要將寄存器中的數(shù)據(jù) X 寫到物理內(nèi)存地址 A 中悉默。同樣的道理城豁,CPU 芯片中的總線接口會向總線發(fā)起寫事務(wù)(write transaction)。寫事務(wù)步驟如下:
CPU 將要寫入的物理內(nèi)存地址 A 放入系統(tǒng)總線上抄课。
通過 IO bridge 的信號轉(zhuǎn)換唱星,將物理內(nèi)存地址 A 傳遞到存儲總線上。
存儲控制器感受到存儲總線上的地址信號跟磨,將物理內(nèi)存地址 A 從存儲總線上讀取出來间聊,并等待數(shù)據(jù)的到達。
CPU 將寄存器中的數(shù)據(jù)拷貝到系統(tǒng)總線上吱晒,通過 IO bridge 的信號轉(zhuǎn)換,將數(shù)據(jù)傳遞到存儲總線上沦童。
存儲控制器感受到存儲總線上的數(shù)據(jù)信號仑濒,將數(shù)據(jù)從存儲總線上讀取出來。
存儲控制器通過內(nèi)存地址 A 定位到具體的存儲器模塊偷遗,最后將數(shù)據(jù)寫入存儲器模塊中的 8 個 DRAM 芯片中墩瞳。
總結(jié)
本文我們從虛擬內(nèi)存地址開始聊起,一直到物理內(nèi)存地址結(jié)束氏豌,包含的信息量還是比較大的喉酌。首先筆者通過一個進程的運行實例為大家引出了內(nèi)核引入虛擬內(nèi)存空間的目的及其需要解決的問題。
在我們有了虛擬內(nèi)存空間的概念之后泵喘,筆者又近一步為大家介紹了內(nèi)核如何劃分用戶態(tài)虛擬內(nèi)存空間和內(nèi)核態(tài)虛擬內(nèi)存空間泪电,并在次基礎(chǔ)之上分別從 32 位體系結(jié)構(gòu)和 64 位體系結(jié)構(gòu)的角度詳細闡述了 Linux 虛擬內(nèi)存空間的整體布局分布。
我們可以通過
cat /proc/pid/maps
或者pmap pid
命令來查看進程用戶態(tài)虛擬內(nèi)存空間的實際分布纪铺。還可以通過
cat /proc/iomem
命令來查看進程內(nèi)核態(tài)虛擬內(nèi)存空間的的實際分布相速。
在我們清楚了 Linux 虛擬內(nèi)存空間的整體布局分布之后,筆者又介紹了 Linux 內(nèi)核如何對分布在虛擬內(nèi)存空間中的各個虛擬內(nèi)存區(qū)域進行管理鲜锚,以及每個虛擬內(nèi)存區(qū)域的作用突诬。在這個過程中還介紹了相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu),近一步從內(nèi)核源碼實現(xiàn)角度加深大家對虛擬內(nèi)存空間的理解芜繁。
最后筆者介紹了物理內(nèi)存的結(jié)構(gòu)旺隙,以及 CPU 如何通過物理內(nèi)存地址來讀寫內(nèi)存中的數(shù)據(jù)。這里筆者需要特地再次強調(diào)的是 CPU 只會訪問虛擬內(nèi)存地址骏令,只不過在操作總線之前蔬捷,通過一個地址轉(zhuǎn)換硬件將虛擬內(nèi)存地址轉(zhuǎn)換為物理內(nèi)存地址,然后將物理內(nèi)存地址作為地址信號放在總線上傳輸榔袋,由于地址轉(zhuǎn)換的內(nèi)容和本文主旨無關(guān)抠刺,考慮到文章的篇幅以及復雜性塔淤,筆者就沒有過多的介紹。
好了速妖,本文的全部內(nèi)容到這里就結(jié)束了高蜂,感謝大家的收看,我們下篇文章見~~~