乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      探索 Linux 內(nèi)存模型

       waston 2012-08-03

      探索 Linux 內(nèi)存模型

      理解 Linux 使用的內(nèi)存模型是從更大程度上掌握 Linux 設(shè)計和實現(xiàn)的第一步,因此本文將概述 Linux 內(nèi)存模型和管理。

      Linux 使用的是單一整體式結(jié)構(gòu) (Monolithic),其中定義了一組原語或系統(tǒng)調(diào)用以實現(xiàn)操作系統(tǒng)的服務(wù),例如在幾個模塊中以超級模式運(yùn)行的進(jìn)程管理、并發(fā)控制和內(nèi)存管理服務(wù)。盡管出于兼容性考慮,Linux 依然將段控制單元模型 (segment control unit model) 保持一種符號表示,但實際上已經(jīng)很少使用這種模型了。

      與內(nèi)存管理有關(guān)的主要問題有:

      • 虛擬內(nèi)存的管理,這是介于應(yīng)用程序請求與物理內(nèi)存之間的一個邏輯層。
      • 物理內(nèi)存的管理。
      • 內(nèi)核虛擬內(nèi)存的管理/內(nèi)核內(nèi)存分配器,這是一個用來滿足對內(nèi)存的請求的組件。這種對內(nèi)存的請求可能來自于內(nèi)核,也可能來自于用戶。
      • 虛擬地址空間的管理。
      • 交換和緩存。

      本文探討了以下問題,可以幫助您從操作系統(tǒng)中內(nèi)存管理的角度來理解 Linux 的內(nèi)幕:

      • 段控制單元模型,通常專用于 Linux
      • 分頁模型,通常專用于 Linux
      • 物理內(nèi)存方面的知識

      雖然本文并沒有詳細(xì)介紹 Linux 內(nèi)核管理內(nèi)存的方法,但是介紹了有關(guān)整個內(nèi)存模型的知識以及系統(tǒng)的尋址方式,這些介紹可為您進(jìn)一步的學(xué)習(xí)提供一個框架。本文重點(diǎn)介紹的是 x86 架構(gòu),但本文中的知識對于其他硬件實現(xiàn)同樣適用。

      x86 內(nèi)存架構(gòu)

      在 x86 架構(gòu)中,內(nèi)存被劃分成 3 種類型的地址:

      • 邏輯地址 (logical address) 是存儲位置的地址,它可能直接對應(yīng)于一個物理位置,也可能不直接對應(yīng)于一個物理位置。邏輯地址通常在請求控制器中的信息時使用。
      • 線性地址 (linear address) (或稱為 平面地址空間)是從 0 開始進(jìn)行尋址的內(nèi)存。之后的每個字節(jié)都可順序使用下一數(shù)字來引用(0、1、2、3 等),直到內(nèi)存末尾為止。這就是大部分非 Intel CPU 的尋址方式。Intel? 架構(gòu)使用了分段的地址空間,其中內(nèi)存被劃分成 64KB 的段,有一個段寄存器總是指向當(dāng)前正在尋址的段的基址。這種架構(gòu)中的 32 位模式被視為平面地址空間,不過它也使用了段。
      • 物理地址 (physical address) 是使用物理地址總線中的位表示的地址。物理地址可能與邏輯地址不同,內(nèi)存管理單元可以將邏輯地址轉(zhuǎn)換成物理地址。

      CPU 使用兩種單元將邏輯地址轉(zhuǎn)換成物理地址。第一種稱為分段單元 (segmented unit),另外一種稱為分頁單元 (paging unit)。


      圖 1. 轉(zhuǎn)換地址空間使用的兩種單元
      轉(zhuǎn)換地址空間使用的兩種單元

      下面讓我們來介紹一下段控制單元模型。

      段控制單元模型概述

      這種分段模型背后的基本思想是將內(nèi)存分段管理。從本質(zhì)上來說,每個段就是自己的地址空間。段由兩個元素構(gòu)成:

      • 基址 (base address) 包含某個物理內(nèi)存位置的地址
      • 長度值 (length value) 指定該段的長度

      分段地址還包括兩個組件 —— 段選擇器 (segment selector)段內(nèi)偏移量 (offset into the segment)。段選擇器指定了要使用的段(即基址和長度值),而段內(nèi)偏移量組件則指定了實際內(nèi)存位置相對于基址的偏移量。實際內(nèi)存位置的物理地址就是這個基址值與偏移量之和。如果偏移量超過了段的長度,系統(tǒng)就會生成一個保護(hù)違例錯誤。

      上述內(nèi)容可小結(jié)如下:

      分段單元可以表示成 -> 段: 偏移量 模型
      也也可表示成 -> 段標(biāo)識符: 偏移量
      

      每個段都是一個 16 位的字段,稱為段標(biāo)識符 (segment identifier)段選擇器 (segment selector)。x86 硬件包括幾個可編程的寄存器,稱為 段寄存器 (segment register),段選擇器保存于其中。這些寄存器為 cs(代碼段)、ds(數(shù)據(jù)段)和 ss(堆棧段)。每個段標(biāo)識符都代表一個使用 64 位(8 個字節(jié))的段描述符 (segment descriptor) 表示的段。這些段描述符可以存儲在一個 GDT(全局描述符表,global descriptor table)中,也可以存儲在一個 LDT(本地描述符表,local descriptor table)中。


      圖 2. 段描述符和段寄存器的相互關(guān)系
      段描述符和段寄存器的相互關(guān)系

      每次將段選擇器加載到段寄存器中時,對應(yīng)的段描述符都會從內(nèi)存加載到相匹配的不可編程 CPU 寄存器中。每個段描述符長 8 個字節(jié),表示內(nèi)存中的一個段。這些都存儲到 LDT 或 GDT 中。段描述符條目中包含一個指針和一個 20 位的值(Limit 字段),前者指向由 Base 字段表示的相關(guān)段中的第一個字節(jié),后者表示內(nèi)存中段的大小。

      其他某些字段還包含一些特殊屬性,例如優(yōu)先級和段的類型(csds)。段的類型是由一個 4 位的 Type 字段表示的。

      由于我們使用了不可編程寄存器,因此在將邏輯地址轉(zhuǎn)換成線性地址時不引用 GDT 或 LDT。這樣可以加快內(nèi)存地址的轉(zhuǎn)換速度。

      段選擇器包含以下內(nèi)容:

      • 一個 13 位的索引,用來標(biāo)識 GDT 或 LDT 中包含的對應(yīng)段描述符條目
      • TI (Table Indicator) 標(biāo)志指定段描述符是在 GDT 中還是在 LDT 中,如果該值是 0,段描述符就在 GDT 中;如果該值是 1,段描述符就在 LDT 中。
      • RPL (request privilege level) 定義了在將對應(yīng)的段選擇器加載到段寄存器中時 CPU 的當(dāng)前特權(quán)級別。

      由于一個段描述符的大小是 8 個字節(jié),因此它在 GDT 或 LDT 中的相對地址可以這樣計算:段選擇器的高 13 位乘以 8。例如,如果 GDT 存儲在地址 0x00020000 處,而段選擇器的 Index 域是 2,那么對應(yīng)的段描述符的地址就等于 (2*8) + 0x00020000。GDT 中可以存儲的段描述符的總數(shù)等于 (2^13 - 1),即 8191。

      圖 3 展示了從邏輯地址獲得線性地址。


      圖 3. 從邏輯地址獲得線性地址
      從邏輯地址獲得線性地址

      那么這在 Linux 環(huán)境下有什么不同呢?

      Linux 中的段控制單元

      Linux 對這個模型稍微進(jìn)行了修改。我注意到 Linux 以一種受限的方法來使用這種分段模型(主要是出于兼容性方面的考慮)。

      在 Linux 中,所有的段寄存器都指向相同的段地址范圍 —— 換言之,每個段寄存器都使用相同的線性地址。這使 Linux 所用的段描述符數(shù)量受限,從而可將所有描述符都保存在 GDT 之中。這種模型有兩個優(yōu)點(diǎn):

      • 當(dāng)所有的進(jìn)程都使用相同的段寄存器值時(當(dāng)它們共享相同的線性地址空間時),內(nèi)存管理更為簡單。
      • 在大部分架構(gòu)上都可以實現(xiàn)可移植性。某些 RISC 處理器也可通過這種受限的方式支持分段。

      圖 4 展示了對模型的修改。


      圖 4. 在 Linux 中,段寄存器指向相同的地址集
      在 Linux 中,段寄存器指向相同的地址集

      段描述符

      Linux 使用以下段描述符:

      • 內(nèi)核代碼段
      • 內(nèi)核數(shù)據(jù)段
      • 用戶代碼段
      • 用戶數(shù)據(jù)段
      • TSS 段
      • 默認(rèn) LDT 段

      下面詳細(xì)介紹這些段寄存器。

      GDT 中的內(nèi)核代碼段 (kernel code segment) 描述符中的值如下:

      • Base = 0x00000000
      • Limit = 0xffffffff (2^32 -1) = 4GB
      • G(粒度標(biāo)志)= 1,表示段的大小是以頁為單位表示的
      • S = 1,表示普通代碼或數(shù)據(jù)段
      • Type = 0xa,表示可以讀取或執(zhí)行的代碼段
      • DPL 值 = 0,表示內(nèi)核模式

      與這個段相關(guān)的線性地址是 4 GB,S = 1 和 type = 0xa 表示代碼段。選擇器在 cs 寄存器中。Linux 中用來訪問這個段選擇器的宏是 _KERNEL_CS。

      內(nèi)核數(shù)據(jù)段 (kernel data segment) 描述符的值與內(nèi)核代碼段的值類似,惟一不同的就是 Type 字段值為 2。這表示此段為數(shù)據(jù)段,選擇器存儲在 ds 寄存器中。Linux 中用來訪問這個段選擇器的宏是 _KERNEL_DS。

      用戶代碼段 (user code segment) 由處于用戶模式中的所有進(jìn)程共享。存儲在 GDT 中的對應(yīng)段描述符的值如下:

      • Base = 0x00000000
      • Limit = 0xffffffff
      • G = 1
      • S = 1
      • Type = 0xa,表示可以讀取和執(zhí)行的代碼段
      • DPL = 3,表示用戶模式

      在 Linux 中,我們可以通過 _USER_CS 宏來訪問此段選擇器。

      用戶數(shù)據(jù)段 (user data segment) 描述符中,惟一不同的字段就是 Type,它被設(shè)置為 2,表示將此數(shù)據(jù)段定義為可讀取和寫入。Linux 中用來訪問此段選擇器的宏是 _USER_DS。

      除了這些段描述符之外,GDT 還包含了另外兩個用于每個創(chuàng)建的進(jìn)程的段描述符 —— TSS 和 LDT 段。

      每個 TSS 段 (TSS segment) 描述符都代表一個不同的進(jìn)程。TSS 中保存了每個 CPU 的硬件上下文信息,它有助于有效地切換上下文。例如,在 U->K 模式的切換中,x86 CPU 就是從 TSS 中獲取內(nèi)核模式堆棧的地址。

      每個進(jìn)程都有自己在 GDT 中存儲的對應(yīng)進(jìn)程的 TSS 描述符。這些描述符的值如下:

      • Base = &tss (對應(yīng)進(jìn)程描述符的 TSS 字段的地址;例如 &tss_struct)這是在 Linux 內(nèi)核的 schedule.h 文件中定義的
      • Limit = 0xeb (TSS 段的大小是 236 字節(jié))
      • Type = 9 或 11
      • DPL = 0。用戶模式不能訪問 TSS。G 標(biāo)志被清除

      所有進(jìn)程共享默認(rèn) LDT 段。默認(rèn)情況下,其中會包含一個空的段描述符。這個默認(rèn) LDT 段描述符存儲在 GDT 中。Linux 所生成的 LDT 的大小是 24 個字節(jié)。默認(rèn)有 3 個條目:

      LDT[0] = 空
      LDT[1] = 用戶代碼段
      LDT[2] = 用戶數(shù)據(jù)/堆棧段描述符
      

      計算任務(wù)

      要計算 GDT 中最多可以存儲多少條目,必須先理解 NR_TASKS(這個變量決定了 Linux 可支持的并發(fā)進(jìn)程數(shù) —— 內(nèi)核源代碼中的默認(rèn)值是 512,最多允許有 256 個到同一實例的并發(fā)連接)。

      GDT 中可存儲的條目總數(shù)可通過以下公式確定:

      GDT 中的條目數(shù) = 12 + 2 * NR_TASKS。
      正如前所述,GDT 可以保存的條目數(shù) = 2^13 -1 = 8192。
      

      在這 8192 個段描述符中,Linux 要使用 6 個段描述符,另外還有 4 個描述符將用于 APM 特性(高級電源管理特性),在 GDT 中還有 4 個條目保留未用。因此,GDT 中的條目數(shù)等于 8192 - 14,也就是 8180。

      任何情況下,GDT 中的條目數(shù) 8180,因此:

      2 * NR_TASKS = 8180
      NR_TASKS = 8180/2 = 4090

      (為什么使用 2 * NR_TASKS?因為對于所創(chuàng)建的每個進(jìn)程,都不僅要加載一個 TSS 描述符 —— 用來維護(hù)上下文切換的內(nèi)容,另外還要加載一個 LDT 描述符。)

      這種 x86 架構(gòu)中進(jìn)程數(shù)量的限制是 Linux 2.2 中的一個組件,但自 2.4 版的內(nèi)核開始,這個問題已經(jīng)不存在了,部分原因是使用了硬件上下文切換(這不可避免地要使用 TSS),并將其替換為進(jìn)程切換。

      接下來,讓我們了解一下分頁模型。

      分頁模型概述

      分頁單元負(fù)責(zé)將線性地址轉(zhuǎn)換成物理地址(請參見圖 1)。線性地址會被分組成頁的形式。這些線性地址實際上都是連續(xù)的 —— 分頁單元將這些連續(xù)的內(nèi)存映射成對應(yīng)的連續(xù)物理地址范圍(稱為 頁框)。注意,分頁單元會直觀地將 RAM 劃分成固定大小的頁框。

      正因如此,分頁具有以下優(yōu)點(diǎn):

      • 為一個頁定義的訪問權(quán)限中保存了構(gòu)成該頁的整組線性地址的權(quán)限
      • 頁的大小等于頁框的大小

      將這些頁映射成頁框的數(shù)據(jù)結(jié)構(gòu)稱為頁表 (page table)。頁表存儲在主存儲器中,可由內(nèi)核在啟用分頁單元之前對其進(jìn)行恰當(dāng)?shù)某跏蓟D 5 展示了頁表。


      圖 5. 頁表將頁轉(zhuǎn)換成頁框
      頁表將頁轉(zhuǎn)換成頁框

      注意,上圖 Page1 中包含的地址集正好與 Page Frame1 中包含的地址集匹配。

      在 Linux 中,分頁單元的使用多于分段單元。前面介紹 Linux 分段模型時已提到,每個分段描述符都使用相同的地址集進(jìn)行線性尋址,從而盡可能降低使用分段單元將邏輯地址轉(zhuǎn)換成線性地址的需要。通過更多地使用分頁單元 而非分段單元,Linux 可以極大地促進(jìn)內(nèi)存管理及其在不同硬件平臺之間的可移植性。

      分頁過程中使用的字段

      下面讓我們來介紹一下用于在 x86 架構(gòu)中指定分頁的字段,這些字段有助于在 Linux 中實現(xiàn)分頁功能。分頁單元進(jìn)入作為分段單元輸出結(jié)果的線性字段,然后進(jìn)一步將其劃分成以下 3 個字段:

      • Directory 以 10 MSB 表示(Most Significant Bit,也就是二進(jìn)制數(shù)字中值最大的位的位置 —— MSB 有時稱為最左位)。
      • Table 以中間的 10 位表示。
      • Offset 以 12 LSB 表示。(Least Significant Bit,也就是二進(jìn)制整數(shù)中給定單元值的位的位置,即確定這個數(shù)字是奇數(shù)還是偶數(shù)。LSB 有時稱為最右位。這與數(shù)字權(quán)重最輕的數(shù)字類似,它是最右邊位置處的數(shù)字。)

      線性地址到對應(yīng)物理位置的轉(zhuǎn)換的過程包含兩個步驟。第一步使用了一個稱為頁目錄 (Page Directory) 的轉(zhuǎn)換表(從頁目錄轉(zhuǎn)換成頁表),第二步使用了一個稱為頁表 (Page Table) 的轉(zhuǎn)換表(即頁表加偏移量再加頁框)。圖 6 展示了此過程。


      圖 6. 分頁字段
      分頁字段

      開始時,首先將頁目錄的物理地址加載到 cr3 寄存器中。線性地址中的 Directory 字段確定頁目錄中指向恰當(dāng)?shù)捻摫項l目。Table 字段中的地址確定包含頁的頁框物理地址所在頁表中的條目。Offset 字段確定了頁框中的相對位置。由于 Offset 字段為 12 位,因此每個頁中都包含有 4 KB 數(shù)據(jù)。

      下面小結(jié)物理地址的計算:

      1. cr3 + Page Directory (10 MSB) = 指向 table_base
      2. table_base + Page Table (10 中間位) = 指向 page_base
      3. page_base + Offset = 物理地址 (獲得頁框)

      由于 Page Directory 字段和 Page Table 段都是 10 位,因此其可尋址上限為 1024*1024 KB,Offset 可尋址的范圍最大為 2^12(4096 字節(jié))。因此,頁目錄的可尋址上限為 1024*1024*4096(等于 2^32 個內(nèi)存單元,即 4 GB)。因此在 x86 架構(gòu)上,總可尋址上限是 4 GB。

      擴(kuò)展分頁

      擴(kuò)展分頁是通過刪除頁表轉(zhuǎn)換表實現(xiàn)的;此后線性地址的劃分即可在頁目錄 (10 MSB) 和偏移量 (22 LSB) 之間完成了。

      22 LSB 構(gòu)成了頁框的 4 MB 邊界(2^22)。擴(kuò)展分頁可以與普通的分頁模型一起使用,并可用于將大型的連續(xù)線性地址映射為對應(yīng)的物理地址。操作系統(tǒng)中刪除頁表以提供擴(kuò)展頁表。這可以通過設(shè)置 PSE (page size extension) 實現(xiàn)。

      36 位的 PSE 擴(kuò)展了 36 位的物理地址,可以支持 4 MB 頁,同時維護(hù)一個 4 字節(jié)的頁目錄條目,這樣就可以提供一種對超過 4 GB 的物理內(nèi)存進(jìn)行尋址的方法,而不需要對操作系統(tǒng)進(jìn)行太大的修改。這種方法對于按需分頁來說具有一些實際的限制。

      Linux 中的分頁模型

      雖然 Linux 中的分頁與普通的分頁類似,但是 x86 架構(gòu)引入了一種三級頁表機(jī)制,包括:

      • 頁全局目錄 (Page Global Directory),即 pgd,是多級頁表的抽象最高層。每一級的頁表都處理不同大小的內(nèi)存 —— 這個全局目錄可以處理 4 MB 的區(qū)域。每項都指向一個更小目錄的低級表,因此 pgd 就是一個頁表目錄。當(dāng)代碼遍歷這個結(jié)構(gòu)時(有些驅(qū)動程序就要這樣做),就稱為是在“遍歷”頁表。
      • 頁中間目錄 (Page Middle Directory),即 pmd,是頁表的中間層。在 x86 架構(gòu)上,pmd 在硬件中并不存在,但是在內(nèi)核代碼中它是與 pgd 合并在一起的。
      • 頁表條目 (Page Table Entry),即 pte,是頁表的最低層,它直接處理頁(參看 PAGE_SIZE),該值包含某頁的物理地址,還包含了說明該條目是否有效及相關(guān)頁是否在物理內(nèi)存中的位。

      為了支持大內(nèi)存區(qū)域,Linux 也采用了這種三級分頁機(jī)制。在不需要為大內(nèi)存區(qū)域時,即可將 pmd 定義成“1”,返回兩級分頁機(jī)制。

      分頁級別是在編譯時進(jìn)行優(yōu)化的,我們可以通過啟用或禁用中間目錄來啟用兩級和三級分頁(使用相同的代碼)。32 位處理器使用的是 pmd 分頁,而 64 位處理器使用的是 pgd 分頁。


      圖 7. 三級分頁
      三級分頁

      如您所知,在 64 位處理器中:

      • 21 MSB 保留未用
      • 13 LSB 由頁面偏移量表示
      • 其余的 30 位分為:
        • 10 位用于頁表
        • 10 位用于頁全局目錄
        • 10 位用于頁中間目錄

      我們可以從架構(gòu)中看到,實際上使用了 43 位進(jìn)行尋址。因此在 64 位處理器中,可以有效使用的內(nèi)存是 2 的 43 次方。

      每個進(jìn)程都有自己的頁目錄和頁表。為了引用一個包含實際用戶數(shù)據(jù)的頁框,操作系統(tǒng)(在 x86 架構(gòu)上)首先將 pgd 加載到 cr3 寄存器中。Linux 將 cr3 寄存器的內(nèi)容存儲到 TSS 段中。此后只要在 CPU 上執(zhí)行新進(jìn)程,就從 TSS 段中將另外一個值加載到 cr3 寄存器中。從而使分頁單元引用一組正確的頁表。

      pgd 表中的每一條目都指向一個頁框,其中中包含了一組 pmd 條目;pdm 表中的每個條目又指向一個頁框,其中包含一組 pte 條目;pde 表中的每個條目再指向一個頁框,其中包含的是用戶數(shù)據(jù)。如果正在查找的頁已轉(zhuǎn)出,那么就會在 pte 表中存儲一個交換條目,(在缺頁的情況下)以定位將哪個頁框重新加載到內(nèi)存中。

      圖 8 說明我們連續(xù)為各級頁表添加偏移量來映射對應(yīng)的頁框條目。我們通過進(jìn)入作為分段單元輸出的線性地址,再劃分該地址來獲得偏移量。要將線性地址劃分成對應(yīng)的 每個頁表元素,需要在內(nèi)核中使用不同的宏。本文不詳細(xì)介紹這些宏,下面我們通過圖 8 來簡單看一下線性地址的劃分方式。


      圖 8. 具有不同地址長度的線性地址
      具有不同地址長度的線性地址

      預(yù)留頁框

      Linux 為內(nèi)核代碼和數(shù)據(jù)結(jié)構(gòu)預(yù)留了幾個頁框。這些頁永遠(yuǎn)不會 被轉(zhuǎn)出到磁盤上。從 0x0 到 0xc0000000 (PAGE_OFFSET) 的線性地址可由用戶代碼和內(nèi)核代碼進(jìn)行引用。從 PAGE_OFFSET 到 0xffffffff 的線性地址只能由內(nèi)核代碼進(jìn)行訪問。

      這意味著在 4 GB 的內(nèi)存空間中,只有 3 GB 可以用于用戶應(yīng)用程序。

      如何啟用分頁

      Linux 進(jìn)程使用的分頁機(jī)制包括兩個階段:

      • 在啟動時,系統(tǒng)為 8 MB 的物理內(nèi)存設(shè)置頁表。
      • 然后,第二個階段完成對其余物理地址的映射。

      在啟動階段,startup_32() 調(diào)用負(fù)責(zé)對分頁機(jī)制進(jìn)行初始化。這是在 arch/i386/kernel/head.S 文件中實現(xiàn)的。這 8 MB 的映射發(fā)生在 PAGE_OFFSET 之上的地址中。這種初始化是通過一個靜態(tài)定義的編譯時數(shù)組 (swapper_pg_dir) 開始的。在編譯時它被放到一個特定的地址(0x00101000)。

      這種操作為在代碼中靜態(tài)定義的兩個頁 —— pg0pg1 —— 建立頁表。這些頁框的大小默認(rèn)為 4 KB,除非我們設(shè)置了頁大小擴(kuò)展位(有關(guān) PSE 的更多內(nèi)容,請參閱 擴(kuò)展分頁 一節(jié))。這個全局?jǐn)?shù)組所指向的數(shù)據(jù)地址存儲在 cr3 寄存器中,我認(rèn)為這是為 Linux 進(jìn)程設(shè)置分頁單元的第一階段。其余的頁項是在第二階段中完成的。

      第二階段由方法調(diào)用 paging_init() 來完成。

      在 32 位的 x86 架構(gòu)上,RAM 映射到 PAGE_OFFSET 和由 4GB 上限 (0xFFFFFFFF) 表示的地址之間。這意味著大約有 1 GB 的 RAM 可以在 Linux 啟動時進(jìn)行映射,這種操作是默認(rèn)進(jìn)行的。然而,如果有人設(shè)置了 HIGHMEM_CONFIG,那么就可以將超過 1 GB 的內(nèi)存映射到內(nèi)核上 —— 切記這是一種臨時的安排??梢酝ㄟ^調(diào)用 kmap() 實現(xiàn)。

      物理內(nèi)存區(qū)域

      我已經(jīng)向您展示了(32 位架構(gòu)上的) Linux 內(nèi)核按照 3:1 的比率來劃分虛擬內(nèi)存:3 GB 的虛擬內(nèi)存用于用戶空間,1 GB 的內(nèi)存用于內(nèi)核空間。內(nèi)核代碼及其數(shù)據(jù)結(jié)構(gòu)都必須位于這 1 GB 的地址空間中,但是對于此地址空間而言,更大的消費(fèi)者是物理地址的虛擬映射。

      之所以出現(xiàn)這種問題,是因為若一段內(nèi)存沒有映射到自己的地址空間中,那么內(nèi)核就不能操作這段內(nèi)存。因此,內(nèi)核可以處理的最大內(nèi)存總量就是可以映射到內(nèi)核的 虛擬地址空間減去需要映射到內(nèi)核代碼本身上的空間。結(jié)果,一個基于 x86 的 Linux 系統(tǒng)最大可以使用略低于 1 GB 的物理內(nèi)存。

      為了迎合大量用戶的需要,支持更多內(nèi)存、提高性能,并建立一種獨(dú)立于架構(gòu)的內(nèi)存描述方法,Linux 內(nèi)存模型就必須進(jìn)行改進(jìn)。為了實現(xiàn)這些目標(biāo),新模型將內(nèi)存劃分成分配給每個 CPU 的空間。每個空間都稱為一個 節(jié)點(diǎn);每個節(jié)點(diǎn)都被劃分成一些 區(qū)域。區(qū)域(表示內(nèi)存中的范圍)可以進(jìn)一步劃分為以下類型:

      • ZONE_DMA(0-16 MB):包含 ISA/PCI 設(shè)備需要的低端物理內(nèi)存區(qū)域中的內(nèi)存范圍。
      • ZONE_NORMAL(16-896 MB):由內(nèi)核直接映射到高端范圍的物理內(nèi)存的內(nèi)存范圍。所有的內(nèi)核操作都只能使用這個內(nèi)存區(qū)域來進(jìn)行,因此這是對性能至關(guān)重要的區(qū)域。
      • ZONE_HIGHMEM(896 MB 以及更高的內(nèi)存):系統(tǒng)中內(nèi)核不能映像到的其他可用內(nèi)存。

      節(jié)點(diǎn)的概念在內(nèi)核中是使用 struct pglist_data 結(jié)構(gòu)來實現(xiàn)的。區(qū)域是使用 struct zone_struct 結(jié)構(gòu)來描述的。物理頁框是使用 struct Page 結(jié)構(gòu)來表示的,所有這些 Struct 都保存在全局結(jié)構(gòu)數(shù)組 struct mem_map 中,這個數(shù)組存儲在 NORMAL_ZONE 的開頭。節(jié)點(diǎn)、區(qū)域和頁框之間的基本關(guān)系如圖 9 所示。


      圖 9. 節(jié)點(diǎn)、區(qū)域和頁框之間的關(guān)系
      節(jié)點(diǎn)、區(qū)域和頁框之間的關(guān)系

      當(dāng)實現(xiàn)了對 Pentium II 的虛擬內(nèi)存擴(kuò)展的支持(在 32 位系統(tǒng)上使用 PAE —— Physical Address Extension —— 可以訪問 64 GB 的內(nèi)存)和對 4 GB 的物理內(nèi)存(同樣是在 32 位系統(tǒng)上)的支持時,高端內(nèi)存區(qū)域就會出現(xiàn)在內(nèi)核內(nèi)存管理中了。這是在 x86 和 SPARC 平臺上引用的一個概念。通常這 4 GB 的內(nèi)存可以通過使用 kmap()ZONE_HIGHMEM 映射到 ZONE_NORMAL 來進(jìn)行訪問。請注意在 32 位的架構(gòu)上使用超過 16 GB 的內(nèi)存是不明智的,即使啟用了 PAE 也是如此。

      (PAE 是 Intel 提供的內(nèi)存地址擴(kuò)展機(jī)制,它通過在宿主操作系統(tǒng)中使用 Address Windowing Extensions API 為應(yīng)用程序提供支持,從而讓處理器將可以用來尋址物理內(nèi)存的位數(shù)從 32 位擴(kuò)展為 36 位。)

      這個物理內(nèi)存區(qū)域的管理是通過一個 區(qū)域分配器(zone allocator) 實現(xiàn)的。它負(fù)責(zé)將內(nèi)存劃分為很多區(qū)域;它可以將每個區(qū)域作為一個分配單元使用。每個特定的分配請求都利用了一組區(qū)域,內(nèi)核可以從這些位置按照從高到低的順序來進(jìn)行分配。

      例如:

      • 對于某個用戶頁面的請求可以首先從“普通”區(qū)域中來滿足(ZONE_NORMAL);
      • 如果失敗,就從 ZONE_HIGHMEM 開始嘗試;
      • 如果這也失敗了,就從 ZONE_DMA 開始嘗試。

      這種分配的區(qū)域列表依次包括 ZONE_NORMALZONE_HIGHMEMZONE_DMA 區(qū)域。另一方面,對于 DMA 頁的請求可能只能從 DMA 區(qū)域中得到滿足,因此這種請求的區(qū)域列表就只包含 DMA 區(qū)域。

      結(jié)束語

      內(nèi)存管理是一組非常龐大、復(fù)雜且耗時的任務(wù),也是一個非常難以實現(xiàn)的任務(wù),因為我們需要精雕細(xì)琢出一個模型,設(shè)計好系統(tǒng)如何在真實的多程序的環(huán)境中進(jìn)行操 作,這是一項非常艱難的工作。諸如調(diào)度、分頁行為和多進(jìn)程的交互組件都向我們提出了相當(dāng)難度的挑戰(zhàn)。我希望本文可以幫助您了解接受 Linux 內(nèi)存管理挑戰(zhàn)所需要的一些基本知識,并為您提供一個起點(diǎn)。


      2.6 內(nèi)核中內(nèi)存管理的改進(jìn)

      反向映射

      在 Linux 內(nèi)存管理器中,頁表保持對進(jìn)程使用的內(nèi)存物理頁的追蹤,它們將虛擬頁映射到物理頁。這些頁中有一些可能不是長時間使用,它們應(yīng)該被交換出去。不過,在它們 可以被交換出去之前,必須找到映射那個頁的每一個進(jìn)程,這樣那些進(jìn)程中相應(yīng)頁的頁表條目才可以被更新。在 Linux 2.4 內(nèi)核中,這是一項令人生畏的任務(wù),因為為了確定某個頁是否被某個進(jìn)程映射,必須遍歷每個進(jìn)程的頁表。隨著在系統(tǒng)中運(yùn)行的進(jìn)程數(shù)量的增加,將這些頁交換出去 的工作量也會增加。

      反向映射,或者說是 RMAP,就是為解決此問題而在 2.5 內(nèi)核中實現(xiàn)的。反向映射提供了一個發(fā)現(xiàn)哪些進(jìn)程正在使用給定的內(nèi)存物理頁的機(jī)制。不再是遍歷每個進(jìn)程的頁表,內(nèi)存管理器現(xiàn)在為每一個物理頁建立了一個鏈表,包含了指向當(dāng)前映射那個頁的每一個進(jìn)程的頁表條目(page-table entries, PTE)的指針。這個鏈表叫做 PTE 鏈。PTE 鏈極大地提高了找到那些映射某個頁的進(jìn)程的速度,如圖 1 所示。


      圖 1. 2.6 中的反向映射
      2.6 中的反向映射

      當(dāng)然,沒有什么是免費(fèi)的:用反向映射獲得性能提高也要付出代價。反向映射最重要、明顯的代價是,它帶來了一些內(nèi)存開銷。不得不用一些內(nèi)存來保持對所有那些反向映射的追蹤。PTE 鏈的每一個條目使用 4 個字節(jié)來存儲指向頁表條目的指針,用另外 4 個字節(jié)來存儲指向鏈的下一個條目的指針。這些內(nèi)存必須使用低端內(nèi)存,而這在 32 位硬件上有點(diǎn)不夠用。有時這可以優(yōu)化到只使用一個條目而不使用鏈表。這種方法叫做 p頁直接方法(page-direct approach)。如果只有一個到這個頁的映射,那么可以用一個叫做“direct”的指針來代替鏈表。只有在某個頁只是由一個惟一的進(jìn)程映射時才可以進(jìn)行這種優(yōu)化。如果稍后這個頁被另一個進(jìn)程所映射,它將不得不再去使用 PTE 鏈。一個標(biāo)記設(shè)置用來告訴內(nèi)存管理器什么時候這種優(yōu)化對一個給定的頁有效。

      反向映射還帶來了一些其他的復(fù)雜性。當(dāng)頁被一個進(jìn)程映射時,必須為所有那些頁建立反向映射。同樣,當(dāng)一個進(jìn)程釋放對頁的映射時,相應(yīng)的 映射也必須都刪除掉。這在退出時尤其常見。所有這些操作都必須在鎖定情況下進(jìn)行。對那些執(zhí)行很多派生和退出的應(yīng)用程序來說,這可能會非常浪費(fèi)并且增加很多 開銷。

      盡管有一些折衷,但可以證明反向映射是對 Linux 內(nèi)存管理器的一個頗有價值的修改。通過這一途徑,查找定位映射某個頁的進(jìn)程這一嚴(yán)重瓶頸被最小化為只需要一個簡單的操作。當(dāng)大型應(yīng)用程序向內(nèi)核請求大量內(nèi) 存和多個進(jìn)程共享內(nèi)存時,反向映射幫助系統(tǒng)繼續(xù)有效地運(yùn)行和擴(kuò)展。當(dāng)前還有更多對反向映射的改進(jìn)正在研究中,可能會出現(xiàn)在未來的 Linux 內(nèi)核版本中。

      大內(nèi)存頁

      典型地,內(nèi)存管理器在 x86 系統(tǒng)上處理的內(nèi)存頁為 4 KB。實際的頁大小是與體系結(jié)構(gòu)相關(guān)的。對大部分用途來說,內(nèi)存管理器以這樣大小的頁來管理內(nèi)存是最有效的。不過,有一些應(yīng)用程序要使用特別多的內(nèi)存。大 型數(shù)據(jù)庫就是其中一個常見的例子。由于每個頁都要由每個進(jìn)程映射,必須創(chuàng)建頁表條目來將虛擬地址映射到物理地址。如果您的一個進(jìn)程要使用 4KB 的頁來映射 1 GB 內(nèi)存,這將用到 262,144 個頁表條目來保持對那些頁的追蹤。如果每個頁表條目消耗 8 個字節(jié),那些每映射 1 GB 內(nèi)存需要 2 MB 的開銷。這本身就已經(jīng)是非??捎^的開銷了,不過,如果有多個進(jìn)程共享那些內(nèi)存時,問題會變得更嚴(yán)重。在這種情況下,每個映射到同一塊 1 GB 內(nèi)存的進(jìn)程將為頁表條目付出自己 2 MB 的代價。如果有足夠多的進(jìn)程,內(nèi)存在開銷上的浪費(fèi)可能會超過應(yīng)用程序請求使用的內(nèi)存數(shù)量。

      解決這一問題的一個方法是使用更大的頁。大部分新的處理器都支持至少一個小的和一個大的內(nèi)存頁大小。在 x86 上,大內(nèi)存頁的大小是 4 MB,或者,在物理地址擴(kuò)展(PAE)打開的系統(tǒng)上是 2 MB。假定在前面的中使用頁大小為 4 MB 的大內(nèi)存頁,同樣 1 GB 內(nèi)存只用 256 個頁表條目就可以映射,而不需要 262,144 個。這樣開銷從 2 MB 變?yōu)?2,048 個字節(jié)。

      大內(nèi)存頁的使用還可以通過減少 變換索引緩沖(translation lookaside buffer, TLB)的失敗次數(shù)來提高性能。TLB 是一種頁表的高速緩存,讓那些在表中列出的頁可以更快地進(jìn)行虛擬地址到物理地址的轉(zhuǎn)換。大內(nèi)存頁可以用更少的實際頁來提供更多的內(nèi)存,相當(dāng)于較小的頁大小,使用的大內(nèi)存頁越多,就有越多的內(nèi)存可以通過 TLB 引用。

      在高端內(nèi)存中存儲頁表條目

      在 32 位機(jī)器上頁表通常只可以存儲在低端內(nèi)存中。低端內(nèi)存只限于物理內(nèi)存的前 896 MB,同時還要滿足內(nèi)核其余的大部分要求。在應(yīng)用程序使用了大量進(jìn)程并映射了大量內(nèi)存的情況下,低端內(nèi)存可能很快就不夠用了。

      現(xiàn)在,在 2.6 內(nèi)核中有一個配置選項叫做 Highmem PTE,讓頁表條目可以存放在高端內(nèi)存中,釋放出更多的低端內(nèi)存區(qū)域給那些必須放在這里的其他內(nèi)核數(shù)據(jù)結(jié)構(gòu)。作為代價,使用這些頁表條目的進(jìn)程會稍微慢一些。不過,對于那些在大量進(jìn)程在運(yùn)行的系統(tǒng)來說,將頁表存儲到高端內(nèi)存中可以在低端內(nèi)存區(qū)域擠出更多的內(nèi)存。

      圖 2. 內(nèi)存區(qū)域
      內(nèi)存區(qū)域

        本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
        轉(zhuǎn)藏 分享 獻(xiàn)花(0

        0條評論

        發(fā)表

        請遵守用戶 評論公約

        類似文章 更多