操作系統(tǒng)物理內(nèi)存的組織形式
由于物理地址是連續(xù)的,頁也是連續(xù)的,每個(gè)頁大小也是一樣的。因而對于任何一個(gè)地址,只要直接除一下每頁的大小,很容易直接算出在哪一頁。每個(gè)頁有一個(gè)結(jié)構(gòu) struct page 表示,這個(gè)結(jié)構(gòu)也是放在一個(gè)數(shù)組里面,這樣根據(jù)頁號(hào),很容易通過下標(biāo)找到相應(yīng)的 struct page 結(jié)構(gòu)。
如果是這樣,整個(gè)物理內(nèi)存的布局就非常簡單、易管理,這就是最經(jīng)典的平坦內(nèi)存模型(Flat Memory Model)。
在這種模式下,CPU 也會(huì)有多個(gè),在總線的一側(cè)。所有的內(nèi)存條組成一大片內(nèi)存,在總線的另一側(cè),所有的 CPU 訪問內(nèi)存都要過總線,而且距離都是一樣的,這種模式稱為 SMP(Symmetric multiprocessing),即對稱多處理器。當(dāng)然,它也有一個(gè)顯著的缺點(diǎn),就是總線會(huì)成為瓶頸,因?yàn)閿?shù)據(jù)都要走它。
為了提高性能和可擴(kuò)展性,后來有了一種更高級(jí)的模式,NUMA(Non-uniform memory access),非一致內(nèi)存訪問。在這種模式下,內(nèi)存不是一整塊。每個(gè) CPU 都有自己的本地內(nèi)存,CPU 訪問本地內(nèi)存不用過總線,因而速度要快很多,每個(gè) CPU 和內(nèi)存在一起,稱為一個(gè) NUMA 節(jié)點(diǎn)。但是,在本地內(nèi)存不足的情況下,每個(gè) CPU 都可以去另外的 NUMA 節(jié)點(diǎn)申請內(nèi)存,這個(gè)時(shí)候訪問延時(shí)就會(huì)比較長。
這樣,內(nèi)存被分成了多個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)再被分成一個(gè)一個(gè)的頁面。由于頁需要全局唯一定位,頁還是需要有全局唯一的頁號(hào)的。但是由于物理內(nèi)存不是連起來的了,頁號(hào)也就不再連續(xù)了。于是內(nèi)存模型就變成了非連續(xù)內(nèi)存模型,管理起來就復(fù)雜一些。
這里需要指出的是,NUMA 往往是非連續(xù)內(nèi)存模型。而非連續(xù)內(nèi)存模型不一定就是 NUMA,有時(shí)候一大片內(nèi)存的情況下,也會(huì)有物理內(nèi)存地址不連續(xù)的情況。
當(dāng)前的主流場景,NUMA 方式。我們首先要能夠表示 NUMA 節(jié)點(diǎn)的概念,于是有了下面這個(gè)結(jié)構(gòu) typedef struct pglist_data pg_data_t,它里面有以下的成員變量:
每一個(gè)節(jié)點(diǎn)都有自己的 ID:node_id;
node_mem_map 就是這個(gè)節(jié)點(diǎn)的 struct page 數(shù)組,用于描述這個(gè)節(jié)點(diǎn)里面的所有的頁;
node_start_pfn 是這個(gè)節(jié)點(diǎn)的起始頁號(hào);
node_spanned_pages 是這個(gè)節(jié)點(diǎn)中包含不連續(xù)的物理內(nèi)存地址的頁面數(shù);
node_present_pages 是真正可用的物理頁面的數(shù)目。
ZONE_DMA 是指可用于作 DMA(Direct Memory Access,直接內(nèi)存存?。┑膬?nèi)存。DMA 是這樣一種機(jī)制:要把外設(shè)的數(shù)據(jù)讀入內(nèi)存或把內(nèi)存的數(shù)據(jù)傳送到外設(shè),原來都要通過 CPU 控制完成,但是這會(huì)占用 CPU,影響 CPU 處理其他事情,所以有了 DMA 模式。CPU 只需向 DMA 控制器下達(dá)指令,讓 DMA 控制器來處理數(shù)據(jù)的傳送,數(shù)據(jù)傳送完畢再把信息反饋給 CPU,這樣就可以解放 CPU。
對于 64 位系統(tǒng),有兩個(gè) DMA 區(qū)域。除了上面說的 ZONE_DMA,還有 ZONE_DMA32。在這里你大概理解 DMA 的原理就可以,不必糾結(jié),我們后面會(huì)講 DMA 的機(jī)制。
ZONE_NORMAL 是直接映射區(qū),就是上一節(jié)講的,從物理內(nèi)存到虛擬內(nèi)存的內(nèi)核區(qū)域,通過加上一個(gè)常量直接映射。
ZONE_HIGHMEM 是高端內(nèi)存區(qū),就是上一節(jié)講的,對于 32 位系統(tǒng)來說超過 896M 的地方,對于 64 位沒必要有的一段區(qū)域。
ZONE_MOVABLE 是可移動(dòng)區(qū)域,通過將物理內(nèi)存劃分為可移動(dòng)分配區(qū)域和不可移動(dòng)分配區(qū)域來避免內(nèi)存碎片。
為了讓 CPU 快速訪問段描述符,在 CPU 里面有段描述符緩存。CPU 訪問這個(gè)緩存的速度比內(nèi)存快得多。同樣對于頁面來講,也是這樣的。如果一個(gè)頁被加載到 CPU 高速緩存里面,這就是一個(gè)熱頁(Hot Page),CPU 讀起來速度會(huì)快很多,如果沒有就是冷頁(Cold Page)。由于每個(gè) CPU 都有自己的高速緩存,因而 per_cpu_pageset 也是每個(gè) CPU 一個(gè)。
物理內(nèi)存的基本單位,頁的數(shù)據(jù)結(jié)構(gòu) struct page。這是一個(gè)特別復(fù)雜的結(jié)構(gòu),里面有很多的 union,union 結(jié)構(gòu)是在 C 語言中被用于同一塊內(nèi)存根據(jù)情況保存不同類型數(shù)據(jù)的一種方式。這里之所以用了 union,是因?yàn)橐粋€(gè)物理頁面使用模式有多種。
第一種模式,要用就用一整頁。這一整頁的內(nèi)存,或者直接和虛擬地址空間建立映射關(guān)系,我們把這種稱為匿名頁(Anonymous Page)。或者用于關(guān)聯(lián)一個(gè)文件,然后再和虛擬地址空間建立映射關(guān)系,這樣的文件,我們稱為內(nèi)存映射文件(Memory-mapped File)。
如果某一頁是這種使用模式,則會(huì)使用 union 中的以下變量:
struct address_space *mapping 就是用于內(nèi)存映射,如果是匿名頁,最低位為 1;如果是映射文件,最低位為 0;
pgoff_t index 是在映射區(qū)的偏移量;
atomic_t _mapcount,每個(gè)進(jìn)程都有自己的頁表,這里指有多少個(gè)頁表項(xiàng)指向了這個(gè)頁;
struct list_head lru 表示這一頁應(yīng)該在一個(gè)鏈表上,例如這個(gè)頁面被換出,就在換出頁的鏈表中;
compound 相關(guān)的變量用于復(fù)合頁(Compound Page),就是將物理上連續(xù)的兩個(gè)或多個(gè)頁看成一個(gè)獨(dú)立的大頁。
第二種模式,僅需分配小塊內(nèi)存。有時(shí)候,我們不需要一下子分配這么多的內(nèi)存,例如分配一個(gè) task_struct 結(jié)構(gòu),只需要分配小塊的內(nèi)存,去存儲(chǔ)這個(gè)進(jìn)程描述結(jié)構(gòu)的對象。為了滿足對這種小內(nèi)存塊的需要,Linux 系統(tǒng)采用了一種被稱為 slab allocator 的技術(shù),用于分配稱為 slab 的一小塊內(nèi)存。它的基本原理是從內(nèi)存管理模塊申請一整塊頁,然后劃分成多個(gè)小塊的存儲(chǔ)池,用復(fù)雜的隊(duì)列來維護(hù)這些小塊的狀態(tài)(狀態(tài)包括:被分配了 / 被放回池子 / 應(yīng)該被回收)。
對于要分配比較大的內(nèi)存,例如到分配頁級(jí)別的,可以使用伙伴系統(tǒng)(Buddy System)。
Linux 中的內(nèi)存管理的“頁”大小為 4KB。把所有的空閑頁分組為 11 個(gè)頁塊鏈表,每個(gè)塊鏈表分別包含很多個(gè)大小的頁塊,有 1、2、4、8、16、32、64、128、256、512 和 1024 個(gè)連續(xù)頁的頁塊。最大可以申請 1024 個(gè)連續(xù)頁,對應(yīng) 4MB 大小的連續(xù)內(nèi)存。每個(gè)頁塊的第一個(gè)頁的物理地址是該頁塊大小的整數(shù)倍。
如果有多個(gè) CPU,那就有多個(gè)節(jié)點(diǎn)。每個(gè)節(jié)點(diǎn)用 struct pglist_data 表示,放在一個(gè)數(shù)組里面。
每個(gè)節(jié)點(diǎn)分為多個(gè)區(qū)域,每個(gè)區(qū)域用 struct zone 表示,也放在一個(gè)數(shù)組里面。每個(gè)區(qū)域分為多個(gè)頁。
為了方便分配,空閑頁放在 struct free_area 里面,使用伙伴系統(tǒng)進(jìn)行管理和分配,每一頁用 struct page 表示。