• <noscript id="e0iig"><kbd id="e0iig"></kbd></noscript>
  • <td id="e0iig"></td>
  • <option id="e0iig"></option>
  • <noscript id="e0iig"><source id="e0iig"></source></noscript>
  • Linux內存管理:分頁機制

    標簽: 【Linux內核】

    Linux內存管理:內存描述之內存節點node

    Linux內存管理:內存描述之內存區域zone

    Linux內存管理:內存描述之內存頁面page

    Linux內存管理:內存描述之高端內存

    Linux內存管理:分頁機制

    內存管理:Linux Memory Management:MMU、段、分頁、PAE、Cache、TLB

    目錄

    1 分頁機制

    1.1 為什么使用多級頁表來完成映射

    1.2 32位系統中2級頁表

    1.3 64位系統中的分頁

    1.4 Linux中的分頁

    2 頁表

    3 Linux分頁機制的演變

    3.1 Linux的頁表實現

    3.2 Linux最初的二級頁表

    3.3 Linux的三級頁表

    3.4 Linux的四級頁表

    4 頁式管理

    4.1 分段機制存在的問題

    4.2 分頁存儲的基本內容

    5 分頁機制支持

    5.1 硬件分頁支持

    5.2 常規的32bit分頁

    5.3 物理地址擴展(PAE)分頁機制和擴展分頁(PSE)

    5.4 64位系統中的分頁

    5.5 硬件保護方案

    6 部分總結

    6.1 為什么使用多級頁表

    6.2 x86_32兩級頁表結構

    6.3 線性地址到物理地址的轉換

    6.4 擴展分頁

    6.5 頁面高速緩存

    7 linux的分頁機制

    7.1 四級分頁機制

    7.3 不同架構的分頁機制

    7.4 為什么linux熱衷:分頁>分段

    8 linux中頁表處理數據結構

    8.1 頁表類型定義pgd_t、pmd_t、pud_t和pte_t

    8.1.1 pteval_t,pmdval_t,pudval_t,pgdval_t

    8.1.2 pgd_t、pmd_t、pud_t和pte_t

    8.1.4 xxx_val和__xxx

    8.2 頁表描述宏

    8.2.1 PAGE宏–頁表(Page Table)

    8.2.2 PMD-Page Middle Directory (頁目錄)

    8.2.3 PUD_SHIFT-頁上級目錄(Page Upper Directory)

    8.2.4 PGDIR_SHIFT-頁全局目錄(Page Global Directory)

    8.3 頁表處理函數

    8.3.1 查詢頁表項中任意一個標志的當前值

    8.3.2 設置頁表項中各標志的值

    8.3.3 宏函數-把一個頁地址和一組保護標志組合成頁表項,或者執行相反的操作

    8.3.4 簡化頁表項的創建和撤消

    9 線性地址轉換

    9.1 分頁模式下的的線性地址轉換

    9.1.1 頁表

    9.1.2 頁表項

    9.1.3 地址轉換過程

    9.2 Linux中通過4級頁表訪問物理內存

    相關鏈接


     

    1 分頁機制


    前面我們講解了操作系統段式存儲管理的主要內容.

    • 32位,在保護方式下,其能夠訪問的線性地址空間可達4GB,而且允許幾乎不受存儲空間限制的虛擬存儲器程序。虛擬存儲器地址空間能夠可達64TB。它還提供了復雜的存儲管理和硬件輔助的保護機構和增加了支持多任務操作系統的特別優化的指令。實際上,64TB的虛擬地址空間是有磁盤等外部存儲器的支持下實現的。在編寫程序是可以放在磁盤存儲器上,但在執行時,必須把程序加載到物理存儲器中。而存儲器管理就是要將46位虛擬地址變換成32位物理地址。

    • 將程序分成不同的段進行管理,我們編程訪問內存地址時,訪問的其實是操作系統抽象給我們的虛擬地址,通過段基址:段偏移的方式訪問內存虛擬地址,極大了簡化了程序員的編程結構。

    • 通過硬件和操作系統的段式管理機制,實模式下通過段基址左移四位+段內偏移,保護模式下通過段選擇子select從段描述符GDT/LDT表中獲取到段描述符,然后對基地址和段偏移處理,將虛擬地址轉換為線性地址。

    如果沒有采用存儲器分頁管理機制,那么我們得到的線性地址就直接對應與物理地址,否則,則需要將線性地址轉換為物理地址。

    從80386開始,所有的80x86處理器都支持分頁,它通過設置CR0寄存器的PG標志啟用分頁。當PG=0時,線性地址就被解釋成物理地址。

    在虛擬內存中,頁表是個映射表的概念, 即從進程能理解的線性地址(linear address)映射到存儲器上的物理地址(phisical address).

    很顯然,這個頁表是需要常駐內存的東西, 以應對頻繁的查詢映射需要(實際上,現代支持VM的處理器都有一個叫TLB的硬件級頁表緩存部件,本文不討論)。

     

    1.1 為什么使用多級頁表來完成映射


    但是為什么要使用多級頁表來完成映射呢?

    用來將虛擬地址映射到物理地址的數據結構稱為頁表, 實現兩個地址空間的關聯最容易的方式是使用數組, 對虛擬地址空間中的每一頁, 都分配一個數組項. 該數組指向與之關聯的頁幀, 但這會引發一個問題, 例如, IA-32體系結構使用4KB大小的頁, 在虛擬地址空間為4GB的前提下, 則需要包含100萬項的頁表. 這個問題在64位體系結構下, 情況會更加糟糕. 而每個進程都需要自身的頁表, 這回導致系統中大量的所有內存都用來保存頁表.

    設想一個典型的32位的X86系統,它的虛擬內存用戶空間(user space)大小為3G, 并且典型的一個頁表項(page table entry, pte)大小為4 bytes,每一個頁(page)大小為4k bytes。那么這3G空間一共有(3G/4k=)786432個頁面,每個頁面需要一個pte來保存映射信息,這樣一共需要786432個pte!

    如何存儲這些信息呢?一個直觀的做法是用數組來存儲,這樣每個頁能存儲(4k/4=)1K個,這樣一共需要(786432/1k=)768個連續的物理頁面(phsical page)。而且,這只是一個進程,如果要存放所有N個進程,這個數目還要乘上N! 這是個巨大的數目,哪怕內存能提供這樣數量的空間,要找到連續768個連續的物理頁面在系統運行一段時間后碎片化的情況下,也是不現實的。

    為減少頁表的大小并容許忽略不需要的區域, 計算機體系結構的涉及會將虛擬地址分成多個部分. 同時虛擬地址空間的大部分們區域都沒有使用, 因而頁沒有關聯到頁幀, 那么就可以使用功能相同但內存用量少的多的模型: 多級頁表

    但是新的問題來了, 到底采用幾級頁表合適呢?

     

    1.2 32位系統中2級頁表


    從80386開始, intel處理器的分頁單元是4KB的頁, 32位的地址空間被分為3部分

    單元描述
    頁目錄表Directory最高10位
    頁中間表Table中間10位
    頁內偏移最低12位

    即頁表被劃分為頁目錄表Directory和頁中間表Tabl兩個部分

    此種情況下, 線性地址的轉換分為兩步完成.

    • 第一步, 基于兩級轉換表(頁目錄表和頁中間表), 最終查找到地址所在的頁幀

    • 第二步, 基于偏移, 在所在的頁幀中查找到對應偏移的物理地址

    使用這種二級頁表可以有效的減少每個進程頁表所需的RAM的數量. 如果使用簡單的一級頁表, 那將需要高達220220個頁表, 假設每項4B, 則共需要占用220?4B=4MB220?4B=4MB的RAM來表示每個進程的頁表. 當然我們并不需要映射所有的線性地址空間(32位機器上線性地址空間為4GB), 內核通常只為進程實際使用的那些虛擬內存區請求頁表來減少內存使用量.

     

    1.3 64位系統中的分頁


    正常來說, 對于32位的系統兩級頁表已經足夠了, 但是對于64位系統的計算機, 這遠遠不夠.

    首先假設一個大小為4KB的標準頁. 因為1KB覆蓋210210個地址的范圍, 4KB覆蓋212212個地址, 所以offset字段需要12位.

    這樣線性地址空間就剩下64-12=52位分配給頁中間表Table和頁目錄表Directory. 如果我們現在決定僅僅使用64位中的48位來尋址(這個限制其實已經足夠了, 2^48=256TB, 即可達到256TB的尋址空間). 剩下的48-12=36位被分配給Table和Directory字段. 即使我們現在決定位兩個字段各預留18位, 那么每個進程的頁目錄和頁表都包含218218個項, 即超過256000個項.

    基于這個原因, 所有64位處理器的硬件分頁系統都使用了額外的分頁級別. 使用的級別取決于處理器的類型

    平臺名稱頁大小尋址所使用的位數分頁級別數線性地址分級
    alpha8KB43310 + 10 + 10 + 13
    ia644KB3939 + 9 + 9 + 12
    ppc644KB41310 + 10 + 9 + 12
    sh644KB41310 + 10 + 9 + 12
    x86_644KB4849 + 9 + 9 + 9 + 12

    1.4 Linux中的分頁


    層次話的頁表用于支持對大地址空間快速, 高效的管理. 因此linux內核堆頁表進行了分級.

    前面我們提到過, 對于32位系統中, 兩級頁表已經足夠了. 但是64位更多數量的分頁級別.

    為了同時支持適用于32位和64位的系統, Linux采用了通用的分頁模型. 在Linux-2.6.10版本中, Linux采用了三級分頁模型. 而從2.6.11開始普遍采用了四級分頁模型.

    目前的內核的內存管理總是嘉定使用四級頁表, 而不管底層處理器是否如此.

    單元描述
    頁全局目錄Page GlobalDirectory
    頁上級目錄Page Upper Directory
    頁中間目錄Page Middle Directory
    頁表Page Table
    頁內偏移Page Offset

    Linux不同于其他的操作系統, 它把計算機分成獨立層(體系結構無關)/依賴層(體系結構相關)兩個層次. 對于頁面的映射和管理也是如此. 頁表管理分為兩個部分, 第一個部分依賴于體系結構, 第二個部分是體系結構無關的. 所有數據結構幾乎都定義在特定體系結構的文件中. 這些數據結構的定義可以在頭文件arch/對應體系/include/asm/page.harch/對應體系/include/asm/pgtable.h中找到. 但是對于AMD64和IA-32已經統一為一個體系結構. 但是在處理頁表方面仍然有很多的區別, 因為相關的定義分為兩個不同的文件arch/x86/include/asm/page_32.harch/x86/include/asm/page_64.h, 類似的也有pgtable_xx.h .

     

    2 頁表


    Linux內核通過四級頁表將虛擬內存空間分為5個部分(4個頁表項用于選擇頁, 1個索引用來表示頁內的偏移). 各個體系結構不僅地址長度不同, 而且地址字拆分的方式也不一定相同. 因此內核使用了宏用于將地址分解為各個分量.

    其他內容請參照博主的另外兩篇博客, 我就不羅嗦了

    深入理解計算機系統-之-內存尋址(五)–頁式存儲管理, 詳細講解了傳統的頁式存儲管理機制

    深入理解計算機系統-之-內存尋址(六)–linux中的分頁機制, 詳細的講解了Linux內核分頁機制的實現機制

     

    3 Linux分頁機制的演變


    3.1 Linux的頁表實現


    由于程序存在局部化特征, 這意味著在特定的時間內只有部分內存會被頻繁訪問,具體點,進程空間中的text段(即程序代碼), 堆, 共享庫,棧都是固定在進程空間的某個特定部分,這樣導致進程空間其實是非常稀疏的, 于是,從硬件層面開始,頁表的實現就是采用分級頁表的方式,Linux內核當然也這么做。所謂分級簡單說就是,把整個進程空間分成區塊,區塊下面可以再細分,這樣在內存中只要常駐某個區塊的頁表即可,這樣可以大量節省內存。

     

    3.2 Linux最初的二級頁表


    Linux最初是在一臺i386機器上開發的,這種機器是典型的32位X86架構,支持兩級頁表

    一個32位虛擬地址如上圖劃分。當在進行地址轉換時,

    結合在CR3寄存器中存放的頁目錄(page directory, PGD)的這一頁的物理地址,再加上從虛擬地址中抽出高10位叫做頁目錄表項(內核也稱這為pgd)的部分作為偏移, 即定位到可以描述該地址的pgd;

    從該pgd中可以獲取可以描述該地址的頁表的物理地址,再加上從虛擬地址中抽取中間10位作為偏移, 即定位到可以描述該地址的pte;

    在這個pte中即可獲取該地址對應的頁的物理地址, 加上從虛擬地址中抽取的最后12位,即形成該頁的頁內偏移, 即可最終完成從虛擬地址到物理地址的轉換。
    從上述過程中,可以看出,對虛擬地址的分級解析過程,實際上就是不斷深入頁表層次,逐漸定位到最終地址的過程,所以這一過程被叫做page talbe walk。

    至于這種做法為什么能節省內存,舉個更簡單的例子更容易明白。比如要記錄16個球場的使用情況,每張紙能記錄4個場地的情況。采用4+4+4+4,共4張紙即可記錄,但問題是球場使用得很少,有時候一整張紙記錄的4個球場都沒人使用。于是,采用4 x 4方案,即把16個球場分為4組,同樣每張紙剛好能記錄4組情況。這樣,使用一張紙A來記錄4個分組球場情況,當某個球場在使用時,只要額外使用多一張紙B來記錄該球場,同時,在A上記錄”某球場由紙B在記錄”即可。這樣在大部分球場使用很少的情況下,只要很少的紙即困記錄,當有球場被使用,有需要再用額外的紙來記錄,當不用就擦除。這里一個很重要的前提就是:局部性。

     

    3.3 Linux的三級頁表


    當X86引入物理地址擴展(Pisycal Addrress Extension, PAE)后,可以支持大于4G的物理內存(36位),但虛擬地址依然是32位,原先的頁表項不適用,它實際多4 bytes被擴充到8 bytes,這意味著,每一頁現在能存放的pte數目從1024變成512了(4k/8)。相應地,頁表層級發生了變化,Linus新增加了一個層級,叫做頁中間目錄(page middle directory, PMD), 變成:

    字段描述位數
    cr3指向一個PDPTcrs寄存器存儲
    PGD指向PDPT中4個項中的一個位31~30
    PMD指向頁目錄中512項中的一個位29~21
    PTE指向頁表中512項中的一個位20~12
    page offset4KB頁中的偏移位11~0

    實際的page table walk依然類似,只不過多了一級

    現在就同時存在2級頁表和3級頁表,在代碼管理上肯定不方便。巧妙的是,Linux采取了一種抽象方法:所有架構全部使用3級頁表: 即PGD -> PMD -> PTE。那只使用2級頁表(如非PAE的X86)怎么辦?

    辦法是針對使用2級頁表的架構,把PMD抽象掉,即虛設一個PMD表項。這樣在page table walk過程中,PGD本直接指向PTE的,現在不了,指向一個虛擬的PMD,然后再由PMD指向PTE。這種抽象保持了代碼結構的統一。

     

    3.4 Linux的四級頁表


    硬件在發展,3級頁表很快又捉襟見肘了,原因是64位CPU出現了, 比如X86_64, 它的硬件是實實在在支持4級頁表的。它支持48位的虛擬地址空間1。如下:

    字段描述位數
    PML4指向一個PDPT位47~39
    PGD指向PDPT中4個項中的一個位38~30
    PMD指向頁目錄中512項中的一個位29~21
    PTE指向頁表中512項中的一個位20~12
    page offset4KB頁中的偏移位11~0

    Linux內核針為使用原來的3級列表(PGD->PMD->PTE),做了折衷。即采用一個唯一的,共享的頂級層次,叫PML4[2]。這個PML4沒有編碼在地址中,這樣就能套用原來的3級列表方案了。不過代價就是,由于只有唯一的PML4, 尋址空間被局限在(239=)512G, 而本來PML4段有9位, 可以支持512個PML4表項的。現在為了使用3級列表方案,只能限制使用一個, 512G的空間很快就又不夠用了,解決方案呼之欲出。

    在2004年10月,當時的X86_64架構代碼的維護者Andi Kleen提交了一個叫做4level page tables for Linux的PATCH系列,為Linux內核帶來了4級頁表的支持。在他的解決方案中,不出意料地,按照X86_64規范,新增了一個PML4的層級, 在這種解決方案中,X86_64擁一個有512條目的PML4, 512條目的PGD, 512條目的PMD, 512條目的PTE。對于仍使用3級目錄的架構來說,它們依然擁有一個虛擬的PML4,相關的代碼會在編譯時被優化掉。 這樣,就把Linux內核的3級列表擴充為4級列表。這系列PATCH工作得不錯,不久被納入Andrew Morton的-mm樹接受測試。

    不出意外的話,它將在v2.6.11版本中釋出。但是,另一個知名開發者Nick Piggin提出了一些看法,他認為Andi的Patch很不錯,不過他認為最好還是把PGD作為第一級目錄,把新增加的層次放在中間,并給出了他自己的Patch:alternate 4-level page tables patches。Andi更想保持自己的PATCH, 他認為Nick不過是玩了改名的游戲,而且他的PATCH經過測試很穩定,快被合并到主線了,不宜再折騰。

    不過Linus卻表達了對Nick Piggin的支持,理由是Nick的做法conceptually least intrusive。畢竟作為Linux的扛把子,穩定對于Linus來說意義重大。

    最終,不意外地,最后Nick Piggin的PATCH在v2.6.11版本中被合并入主線。在這種方案中,4級頁表分別是:PGD -> PUD -> PMD -> PTE。

     

    4 頁式管理


    4.1 分段機制存在的問題


    分段,是指將程序所需要的內存空間大小的虛擬空間,通過映射機制映射到某個物理地址空間(映射的操作由硬件完成)。分段映射機制解決了之前操作系統存在的兩個問題:

    • (1)地址空間沒有隔離

    • (2)程序運行的地址不確定

    不過分段方法存在一個嚴重的問題:內存的使用效率低。

    分段的內存映射單位是整個程序;如果內存不足,被換入換出到磁盤的空間都是整個程序的所需空間,這會造成大量的磁盤訪問操作,并且嚴重降低了運行速度.

    事實上,很多時候程序運行所需要的數據只是很小的一部分,加入到內存的數據大小可能會很小,并沒有必要整體的寫入和寫出.

    分頁機制解決了上面分段方法所存在的一個內存使用效率問題;其核心思想是系統為程序執行文件中的第x頁分配了內存中的第y頁,同時y頁會添加到進程虛擬空間地址的映射表中(頁表),這樣程序就可以通過映射訪問到內存頁y了。

     

    4.2 分頁存儲的基本內容


    分頁的基本方法是將地址空間人為地等分成某一個固定大小的頁;每一頁大小由硬件來決定,或者是由操作系統來決定(如果硬件支持多種大小的頁)。目前,以大小為4KB的分頁是絕大多數PC操作系統的選擇.

    • 邏輯空間等分為頁;并從0開始編號

    • 內存空間等分為塊,與頁面大小相同;從0開始編號

    • 分配內存時,以塊為單位將進程中的若干個頁分別裝入

    關于進程分頁. 當我們把進程的虛擬地址空間按頁來分割,常用的數據和代碼會被裝在到內存;暫時沒用到的是數據和代碼則保存在磁盤中,需要用到的時候,再從磁盤中加載到內存中即可.

    這里需要了解三個概念:

    1. 虛擬頁(VP, Virtual Page),虛擬空間中的頁;

    2. 物理頁(PP, Physical Page),物理內存中的頁;

    3. 磁盤頁(DP, Disk Page),磁盤中的頁。

    虛擬內存的實現需要硬件的支持,從Virtual Address到Physical Address的映射,通過一個叫MMU(Memory Mangement Unit)的部件來完成

     

    5 分頁機制支持


    5.1 硬件分頁支持


    分頁單元(paging unit)把線性地址轉換成物理地址。其中的一個關鍵任務就是把所請求的訪問類型與線性地址的訪問權限相比較,如果這次內存訪問是無效的,就產生一個缺頁異常。

    • :為了更高效和更經濟的管理內存,線性地址被分為以固定長度為單位的組,成為頁。頁內部連續的線性地址空間被映射到連續的物理地址中。這樣,內核可以指定一個頁的物理地址和對應的存取權限,而不用指定全部線性地址的存取權限。這里說頁,同時指一組線性地址以及這組地址包含的數據

    • 頁框:分頁單元把所有的 RAM 分成固定長度的頁框(page frame)(有時叫做物理頁)。每一個頁框包含一個頁(page),也就是說一個頁框的長度與一個頁的長度一致。頁框是主存的一部分,因此也是一個存儲區域。區分一頁和一個頁框是很重要的,前者只是一個數據塊,可以存放在任何頁框或磁盤中。

    • 頁表:把線性地址映射到物理地址的數據結構稱為頁表(page table)。頁表存放在主存中,并在啟用分頁單元之前必須由內核對頁表進行適當的初始化。

     

    5.2 常規的32bit分頁


    常規4KB分頁,32位的線性地址被分成3個域

    Directory(目錄)Table(頁表)Offset(偏移量)
    最高10位中間10位最低12位

     

     

    線性地址的轉換分為兩步完成,每一步都基于一種轉換表,第一種轉換表稱為頁目錄表(page directory),第二種轉換表稱為頁表(page table)。

    為什么需要兩級呢?
    目的在于減少每個進程頁表所需的 RAM 的數量。如果使用簡單的一級頁表,將需要高達220220 個表項來表示每個進程的頁表,即時一個進程并不使用所有的地址,二級模式通過職位進程實際使用的那些虛擬內存區請求頁表來減少內存容量。

    每個活動的進程必須有一個頁目錄,但是卻沒有必要馬上為所有進程的所有頁表都分配 RAM,只有在實際需要一個頁表時候才給該頁表分配 RAM。

    頁目錄項和頁表項有同樣的結構,每項都包含下面的字段:

    字段描述
    Present標志如果被置為1,所指的頁(或頁表)就在主存中;如果該標志為0,則這一頁不在主存中,此時這個表項剩余的位可由操作系統用于自己的目的。如果執行一個地址轉換所需的頁表項或頁目錄項中Present標志被清0,那么分頁單元就把該線性地址轉換所需的頁表項或頁目錄項中Present標志被清0,那么分頁單元就把該線性地址存放在控制寄存器cr2中,并產生14號異常:缺頁異常。
     包含頁框物理地址最高20位的字段。由于每一個頁框有4KB的容量,它的物理地址必須是4096的倍數,因此物理地址的最低12位總為0.如果這個字段指向一個頁目錄,相應的頁框就含有一個頁表;如果它指向一個頁表,相應的頁框就含有一頁數據
    Accessed標志每當分頁單元對相應頁框進行尋址時就設置這個標志。當選中的頁被交換出去時,這一標志就可以由操作系統使用。分頁單元從來不重置這個標志,而是必須由操作系統去做
    Dirty標志只應用于頁表項中。每當對一個頁框進行寫操作時就設置這個標志。與Accessed標志一樣,當選中的頁被交換出去時,這一標志就可以由操作系統使用。分頁單元從來不重置這個標志,而是必須由操作系統去做。
    Read/Write標志含有頁或頁表的存取權限(Read/Write或Read)。
    User/Supervisor標志含有訪問頁或頁表所需的特權級。
    PCD和PWT標志控制硬件高速緩存處理頁或頁表的方式。
    Page Size標志只應用于頁目錄項。如果設置為1,則頁目錄項指的是2MB或4MB頁框。
    Global標志只應用于頁表項。這個標志是在Pentium Pro中引入的,用來防止常用頁從TLB高速緩存中刷新出去。只有在cr4寄存器的頁全局啟用(Page Global Enable, PGE)標志置位時這個標志才起作用。

    正在使用的頁目錄的物理地址存放在控制寄存器CR3中。

    了解了以上結構之后,我們看看如何從線性地址轉換到物理地址的 :

    • 線性地址中的 Directory 字段決定頁目錄中的目錄項,目錄項指向適當的頁表

    • 線性地址中的 Table 字段又決定頁表的頁表項,頁表項含有頁所在頁框的物理地址

    • 線性地址中的 Offset 地段決定了頁框內的相對位置,由于 offset 為 12 為,所以一頁含有 4096 字節的數據

    Directory字段和Table字段都是10位長,因此頁目錄和頁表都可以多達1024項。那么一個頁目錄可以尋址到高達1024*1024*4096=232個存儲單元,這和32位地址所期望的一樣。

    5.3 物理地址擴展(PAE)分頁機制和擴展分頁(PSE)


    處理器所支持的RAM容易受到連接到地址總線上的地址管腳樹限制. 早期Intel處理器從80386到Pentium使用32位物理地址.

    從理論上講, 這樣的系統可以使用高達2^32=4GB的RAM, 而實際上, 由于用戶進程現行地址空間的需要, 4GB的虛擬地址按照1:3的比例劃分給內核虛擬地址空間和進程虛擬地址空間. 則內核只能直接對1GB的線性地址空間進行尋址.

    然而, 大型服務器需要大于4GB的RAM來同時運行數以錢計的進程, 所以必須擴展32位80x86架構所支持的RAM容量.

    Intel通過在它的處理器上把管腳數從32增加到36滿足這樣的需要, 從Pentinum Pro開始, Intel所有處理器的尋址能力可達到2^36=64GB, 但是只有引入一種新的分頁機制才能把32位現行地址轉換為36位物理地址才能使用所增加的物理地址.

    從Pentinum Pro處理器開始, Intel引入一種叫做物理地址擴展(Physical Address Extension, PAE)的機制.

    從Pentium模型開始,80x86微處理器引入了擴展分頁(externded paging),也叫頁大小擴展[Page Size Extension], 它允許頁框大小為4MB而不是4KB。擴展分頁用于把大段連續的線性地址轉換成相應的物理地址,在這種情況下,內核可以不用中間頁表進行地址轉換,從而節省內存并保留TLB項。

    但是Linux并沒有采用這種機制

    正如前面所述,通過設置頁目錄項的Page Size標志啟用擴展分頁功能。在這種情況下,分頁單元把32位線性地址分成兩個字段:
    Directory:最高10位。
    Offfset:其余22位。

    擴展分頁和正常分頁的頁目錄項基本相同,除了
    * Page Size標志必須被設置。
    * 20位物理地址字段只有最高10位是有意義的。這是因為每一個物理地址都是在以4MB為邊界的地方開始的,故這個地址的最低22位為0。

    通過設置cr4處理器寄存器的PSE標志能使擴展分頁與常規分頁共存

    Intel為了支持PAE改變了分頁機制

    • 64GB的RAM被分成了2^24個頁框, 頁表項的物理地址字段從20位擴展到了24位. 因為PAE頁表項必須包含12個標志位和24個物理地址位, 總數之和為36, 頁表項大小從32位擴展到了64位, 結果, 一個4KB的頁表項包含512個表項而不是1024個表項

    • 引入一個頁目錄指針表(Page Directory Pointer Table, PDPT)的頁表新級別, 它由4個64位表項組成.

    • cr3控制寄存器包含一個27位的頁目錄指針表(PDPT)基地址字段. 因為PDPT存放在RAM的前4GB中, 并在32字節(2^5)的倍數上對其, 因此27位足以表示這種表的基地址

    • 當把線性地址映射到4KB的頁時(頁目錄項中的PS標準清0), 32位線性地址將按照如下方式解釋

    字段描述位數
    cr3指向一個PDPTcrs寄存器存儲
    PGD指向PDPT中4個項中的一個位31~30
    PMD指向頁目錄中512項中的一個位29~21
    PTE指向頁表中512項中的一個位20~12
    page offset4KB頁中的偏移位11~0

    * 當把現行地址映射到2MB的頁時(頁目錄項中的PS標志置為1), 32位線性地址按照如下方式解釋

    字段描述位數
    cr3指向一個PDPTcrs寄存器存儲
    PGD指向PDPT中4個項中的一個位31~30
    PMD指向頁目錄中512項中的一個位29~21
    page offset2MB頁中的偏移位20~0

    總之, 一旦cr3被設置, 就可能尋址高達4GB RAM, 如果我們期望堆更多的RAM進行尋址, 就必須在cr3中放置一個新值, 或改變PDPT的內容.

    但是PAE的主要問題是線性地址仍然是32位長, 這就需要內核黑客用同一線性地址映射不同的RAM區. 很顯然, PAE并沒有擴大進程的線性地址空間, 因為它只處理物理地址. 此外, 只有內核能夠修改進程的頁表, 所以在用戶態下運行的程序不可能使用大于4GB的物理地址空間. 另一方面, PAE允許內核使用容量高達64GB的RAM, 從而顯著的增加系統中的進程數目

     

    5.4 64位系統中的分頁


    32位處理器普遍采用兩級分頁。然而兩級分頁并不適用于采用64位系統的計算機。

    原因如下 :

    首先假設一個大小為4KB的標準頁,4KB覆蓋2^12個地址,所以offset字段是12位。如果我們現在決定僅僅使用64位中的48位來尋址(這個限制仍然能是我們自在地擁有256TB的尋址空間!),剩下的48-12=36位被分配給Table和Directory字段。如果我們決定為兩個字段個預留18位,那么每個進程的頁目錄和頁表都含有2^18個項,即超過256000個項。

    由于這個原因,所有64位處理器的硬件分頁系統都使用了額外的分頁級別。使用的級別數量取決于處理器的類型。

    平臺名稱頁大小尋址使用位數分頁級別線性地址分級
    alpha8KB43310+10+10+13
    ia644KB3939+9+9+12
    ppc644KB41310+10+9+12
    x86_644KB4849+9+9+9+12

    注:ia64是intel的一門高端技術,不與x86_64系統兼容
    IA-32e Paging機制下線性地址映射到4KB的頁

     

    5.5 硬件保護方案

    與頁和頁表相關的特權級只有兩個,因為特權由前面“常規分頁”一節中所提到的User/Supervisor標志所控制。若這個標志為0,只有當CPL小于3(這意味著對于Linux而言,處理器處于內核態)時才能對頁尋址;若該標志為1,則總能對頁尋址。

    此外,與段的3種存取權限(讀,寫,執行)不同的是,頁的存取權限只有兩種(讀,寫)。如果頁目錄項或頁表項的Read/Write標志等于0,說明相應的頁表或頁是只讀的,否則是可讀寫的。

     

    6 部分總結


    80386 使用4K字節大小的頁。每一頁都有4K字節長,并在4K字節的邊界上對齊,即每一頁的起始地址都能被4K整除。因此,80386把4G字節的線性地址空間,劃分為1G個頁面,每頁有4K字節大小。分頁機制通過把線性地址空間中的頁,重新定位到物理地址空間來進行管理,因為每個頁面的整個4K字節作為一個單位進行映射,并且每個頁面都對齊4K字節的邊界,因此,線性地址的低12位經過分頁機制直接地作為物理地址的低12位使用。

     

    6.1 為什么使用多級頁表


    假設每個進程都占用了4G的線性地址空間,頁表共含1M個表項,每個表項占4個字節,那么每個進程的頁表要占據4M的內存空間。為了節省頁表占用的空間,我們使用兩級頁表。每個進程都會被分配一個頁目錄,但是只有被實際使用頁表才會被分配到內存里面。一級頁表需要一次分配所有頁表空間,兩級頁表則可以在需要的時候再分配頁表空間。

     

    6.2 x86_32兩級頁表結構


    兩級表結構的第一級稱為頁目錄,存儲在一個4K字節的頁面中。頁目錄表共有1K個表項,每個表項為4個字節,并指向第二級表。線性地址的最高10位(即位31~位32)用來產生第一級的索引,由索引得到的表項中,指定并選擇了1K個二級表中的一個表。

    兩級表結構的第二級稱為頁表,也剛好存儲在一個4K字節的頁面中,包含1K個字節的表項,每個表項包含一個頁的物理基地址。第二級頁表由線性地址的中間10 位(即位21~位12)進行索引,以獲得包含頁的物理地址的頁表項,這個物理地址的高20位與線性地址的低12位形成了最后的物理地址,也就是頁轉化過程輸出的物理地址。

    • 第31~12位是20位頁表地址,由于頁表地址的低12位總為0,所以用高20位指出32位頁表地址就可以了。因此,一個頁目錄最多包含1024個頁表地址。

    • 第0位是存在位,如果P=1,表示頁表地址指向的該頁在內存中,如果P=0,表示不在內存中。

    • 第1位是讀/寫位,第2位是用戶/管理員位,這兩位為頁目錄項提供硬件保護。當特權級為3的進程要想訪問頁面時,需要通過頁保護檢查,而特權級為0的進程就可以繞過頁保護。

    • 第3位是PWT(Page Write-Through)位,表示是否采用寫透方式,寫透方式就是既寫內存(RAM)也寫高速緩存,該位為1表示采用寫透方式

    • 第4位是PCD(Page Cache Disable)位,表示是否啟用高速緩存,該位為1表示啟用高速緩存。

    • 第5位是訪問位,當對頁目錄項進行訪問時,A位=1。

    • 第7位是Page Size標志,只適用于頁目錄項。如果置為1,頁目錄項指的是4MB的頁面,請看后面的擴展分頁。

    • 第9~11位由操作系統專用,Linux也沒有做特殊之用。

    80386的每個頁目錄項指向一個頁表,頁表最多含有1024個頁面項,每項4個字節,包含頁面的起始地址和有關該頁面的信息。頁面的起始地址也是4K的整數倍,所以頁面的低12位也留作它用.

    第31~12位是20位物理頁面地址,除第6位外第0~5位及9~11位的用途和頁目錄項一樣,第6位是頁面項獨有的,當對涉及的頁面進行寫操作時,D位被置1.

    4GB的內存只有一個頁目錄,它最多有1024個頁目錄項,每個頁目錄項又含有1024個頁面項,因此,內存一共可以分成1024×1024=1M個頁面。由于每個頁面為4K個字節,所以,存儲器的大小正好最多為4GB.

     

    6.3 線性地址到物理地址的轉換


    • 1.CR3包含著頁目錄的起始地址,用32位線性地址的最高10位A31~A22作為頁目錄的頁目錄項的索引,將它乘以4,與CR3中的頁目錄的起始地址相加,形成相應頁表的地址。

    • 2.從指定的地址中取出32位頁目錄項,它的低12位為0,這32位是頁表的起始地址。用32位線性地址中的A21~A12位作為頁表中的頁面的索引,將它乘以4,與頁表的起始地址相加,形成32位頁面地址。

    • 3.將A11~A0作為相對于頁面地址的偏移量,與32位頁面地址相加,形成32位物理地址。

     

    6.4 擴展分頁


    從奔騰處理器開始,Intel微處理器引進了擴展分頁,它允許頁的大小為4MB

    在擴展分頁的情況下,分頁機制把32位線性地址分成兩個域:最高10位的目錄域和其余22位的偏移量。

     

    6.5 頁面高速緩存


    由于在分頁情況下,每次存儲器訪問都要存取兩級頁表,這就大大降低了訪問速度。所以,為了提高速度,在386中設置一個最近存取頁面的高速緩存硬件機制,它 自動保持32項處理器最近使用的頁面地址,因此,可以覆蓋128K字節的存儲器地址。當進行存儲器訪問時,先檢查要訪問的頁面是否在高速緩存中,如果在, 就不必經過兩級訪問了,如果不在,再進行兩級訪問。平均來說,頁面高速緩存大約有98%的命中率,也就是說每次訪問存儲器時,只有2%的情況必須訪問兩級分頁機構。這就大大加快了速度.

     

    7 linux的分頁機制


    7.1 四級分頁機制


    前面我們提到Linux內核僅使用了較少的分段機制,但是卻對分頁機制的依賴性很強,其使用一種適合32位和64位結構的通用分頁模型,該模型使用四級分頁機制,即

    • 頁全局目錄(Page Global Directory)
    • 頁上級目錄(Page Upper Directory)
    • 頁中間目錄(Page Middle Directory)
    • 頁表(Page Table)
    • 頁全局目錄包含若干頁上級目錄的地址;
    • 頁上級目錄又依次包含若干頁中間目錄的地址;
    • 而頁中間目錄又包含若干頁表的地址;
    • 每一個頁表項指向一個頁框。
      因此線性地址因此被分成五個部分,而每一部分的大小與具體的計算機體系結構有關。

    7.3 不同架構的分頁機制


    對于不同的體系結構,Linux采用的四級頁表目錄的大小有所不同:對于i386而言,僅采用二級頁表,即頁上層目錄和頁中層目錄長度為0;對于啟用PAE的i386,采用了三級頁表,即頁上層目錄長度為0;對于64位體系結構,可以采用三級或四級頁表,具體選擇由硬件決定。

    對于沒有啟用物理地址擴展的32位系統,兩級頁表已經足夠了。從本質上說Linux通過使“頁上級目錄”位和“頁中間目錄”位全為0,徹底取消了頁上級目錄和頁中間目錄字段。不過,頁上級目錄和頁中間目錄在指針序列中的位置被保留,以便同樣的代碼在32位系統和64位系統下都能使用。內核為頁上級目錄和頁中間目錄保留了一個位置,這是通過把它們的頁目錄項數設置為1,并把這兩個目錄項映射到頁全局目錄的一個合適的目錄項而實現的。

    啟用了物理地址擴展的32 位系統使用了三級頁表。Linux 的頁全局目錄對應80x86 的頁目錄指針表(PDPT),取消了頁上級目錄,頁中間目錄對應80x86的頁目錄,Linux的頁表對應80x86的頁表。

    最終,64位系統使用三級還是四級分頁取決于硬件對線性地址的位的劃分。

     

    7.4 為什么linux熱衷:分頁>分段


    那么,為什么Linux是如此地熱衷使用分頁技術而對分段機制表現得那么地冷淡呢,因為Linux的進程處理很大程度上依賴于分頁。事實上,線性地址到物理地址的自動轉換使下面的設計目標變得可行:

    • 給每一個進程分配一塊不同的物理地址空間,這確保了可以有效地防止尋址錯誤。
    • 區別頁(即一組數據)和頁框(即主存中的物理地址)之不同。這就允許存放在某個頁框中的一個頁,然后保存到磁盤上,以后重新裝入這同一頁時又被裝在不同的頁框中。這就是虛擬內存機制的基本要素。

    每一個進程有它自己的頁全局目錄和自己的頁表集。當發生進程切換時,Linux把cr3控制寄存器的內容保存在前一個執行進程的描述符中,然后把下一個要執行進程的描述符的值裝入cr3寄存器中。因此,當新進程重新開始在CPU上執行時,分頁單元指向一組正確的頁表。

    把線性地址映射到物理地址雖然有點復雜,但現在已經成了一種機械式的任務。

     

    8 linux中頁表處理數據結構


    8.1 頁表類型定義pgd_t、pmd_t、pud_t和pte_t


    Linux分別采用pgd_t、pmd_t、pud_t和pte_t四種數據結構來表示頁全局目錄項、頁上級目錄項、頁中間目錄項和頁表項。這四種 數據結構本質上都是無符號長整型unsigned long

    Linux為了更嚴格數據類型檢查,將無符號長整型unsigned long分別封裝成四種不同的頁表項。如果不采用這種方法,那么一個無符號長整型數據可以傳入任何一個與四種頁表相關的函數或宏中,這將大大降低程序的健壯性。

    pgprot_t是另一個64位(PAE**時)或32位(PAE禁用時)的數據類型,它表示與一個單獨表項相關的保護標志。

    首先我們查看一下子這些類型是如何定義的

     

    8.1.1 pteval_t,pmdval_t,pudval_t,pgdval_t


    參照arch/x86/include/asm/pgtable_64_types.h

    #ifndef __ASSEMBLY__
    #include <linux/types.h>
    
    /*
     * These are used to make use of C type-checking..
     */
    typedef unsigned long   pteval_t;
    typedef unsigned long   pmdval_t;
    typedef unsigned long   pudval_t;
    typedef unsigned long   pgdval_t;
    typedef unsigned long   pgprotval_t;
    
    typedef struct { pteval_t pte; } pte_t;
    
    #endif  /* !__ASSEMBLY__ */

    8.1.2 pgd_t、pmd_t、pud_t和pte_t


    參照 /arch/x86/include/asm/pgtable_types.h

    typedef struct { pgdval_t pgd; } pgd_t;
    
    static inline pgd_t native_make_pgd(pgdval_t val)
    {
            return (pgd_t) { val };
    }
    
    static inline pgdval_t native_pgd_val(pgd_t pgd)
    {
            return pgd.pgd;
    }
    
    static inline pgdval_t pgd_flags(pgd_t pgd)
    {
            return native_pgd_val(pgd) & PTE_FLAGS_MASK;
    }
    
    #if CONFIG_PGTABLE_LEVELS > 3
    typedef struct { pudval_t pud; } pud_t;
    
    static inline pud_t native_make_pud(pmdval_t val)
    {
            return (pud_t) { val };
    }
    
    static inline pudval_t native_pud_val(pud_t pud)
    {
            return pud.pud;
    }
    #else
    #include <asm-generic/pgtable-nopud.h>
    
    static inline pudval_t native_pud_val(pud_t pud)
    {
            return native_pgd_val(pud.pgd);
    }
    #endif
    
    #if CONFIG_PGTABLE_LEVELS > 2
    typedef struct { pmdval_t pmd; } pmd_t;
    
    static inline pmd_t native_make_pmd(pmdval_t val)
    {
            return (pmd_t) { val };
    }
    
    static inline pmdval_t native_pmd_val(pmd_t pmd)
    {
            return pmd.pmd;
    }
    #else
    #include <asm-generic/pgtable-nopmd.h>
    
    static inline pmdval_t native_pmd_val(pmd_t pmd)
    {
            return native_pgd_val(pmd.pud.pgd);
    }
    #endif
    
    static inline pudval_t pud_pfn_mask(pud_t pud)
    {
            if (native_pud_val(pud) & _PAGE_PSE)
                    return PHYSICAL_PUD_PAGE_MASK;
            else
                    return PTE_PFN_MASK;
    }
    
    static inline pudval_t pud_flags_mask(pud_t pud)
    {
            return ~pud_pfn_mask(pud);
    }
    
    static inline pudval_t pud_flags(pud_t pud)
    {
            return native_pud_val(pud) & pud_flags_mask(pud);
    }
    
    static inline pmdval_t pmd_pfn_mask(pmd_t pmd)
    {
            if (native_pmd_val(pmd) & _PAGE_PSE)
                    return PHYSICAL_PMD_PAGE_MASK;
            else
                    return PTE_PFN_MASK;
    }
    
    static inline pmdval_t pmd_flags_mask(pmd_t pmd)
    {
            return ~pmd_pfn_mask(pmd);
    }
    
    static inline pmdval_t pmd_flags(pmd_t pmd)
    {
            return native_pmd_val(pmd) & pmd_flags_mask(pmd);
    }
    
    static inline pte_t native_make_pte(pteval_t val)
    {
            return (pte_t) { .pte = val };
    }
    
    static inline pteval_t native_pte_val(pte_t pte)
    {
            return pte.pte;
    }
    
    static inline pteval_t pte_flags(pte_t pte)
    {
            return native_pte_val(pte) & PTE_FLAGS_MASK;
    }

    8.1.4 xxx_val和__xxx


    參照/arch/x86/include/asm/pgtable.h

    五個類型轉換宏(_ pte、_ pmd、_ pud、_ pgd和__ pgprot)把一個無符號整數轉換成所需的類型。

    另外的五個類型轉換宏(pte_val,pmd_val, pud_val, pgd_val和pgprot_val)執行相反的轉換,即把上面提到的四種特殊的類型轉換成一個無符號整數。

    #define pgd_val(x)      native_pgd_val(x)
    #define __pgd(x)        native_make_pgd(x)
    
    #ifndef __PAGETABLE_PUD_FOLDED
    #define pud_val(x)      native_pud_val(x)
    #define __pud(x)        native_make_pud(x)
    #endif
    
    #ifndef __PAGETABLE_PMD_FOLDED
    #define pmd_val(x)      native_pmd_val(x)
    #define __pmd(x)        native_make_pmd(x)
    #endif
    
    #define pte_val(x)      native_pte_val(x)
    #define __pte(x)        native_make_pte(x)

    這里需要區別指向頁表項的指針和頁表項所代表的數據。以pgd_t類型為例子,如果已知一個pgd_t類型的指針pgd,那么通過pgd_val(*pgd)即可獲得該頁表項(也就是一個無符號長整型數據),這里利用了面向對象的思想。

     

    8.2 頁表描述宏


    參照arch/x86/include/asm/pgtable_64
    linux中使用下列宏簡化了頁表處理,對于每一級頁表都使用有以下三個關鍵描述宏:

    宏字段描述
    XXX_SHIFT指定Offset字段的位數
    XXX_SIZE頁的大小
    XXX_MASK用以屏蔽Offset字段的所有位。

    我們的四級頁表,對應的宏分別由PAGE,PMD,PUD,PGDIR

    宏字段前綴描述
    PGDIR頁全局目錄(Page Global Directory)
    PUD頁上級目錄(Page Upper Directory)
    PMD頁中間目錄(Page Middle Directory)
    PAGE頁表(Page Table)

    8.2.1 PAGE宏–頁表(Page Table)


    字段描述
    PAGE_SHIFT指定Offset字段的位數
    PAGE_SIZE頁的大小
    PAGE_MASK用以屏蔽Offset字段的所有位。

    定義如下,在/arch/x86/include/asm/page_types.h 文件中

    /* PAGE_SHIFT determines the page size */
     #define PAGE_SHIFT      12
     #define PAGE_SIZE       (_AC(1,UL) << PAGE_SHIFT)
     #define PAGE_MASK       (~(PAGE_SIZE-1))

    當用于80x86處理器時,PAGE_SHIFT返回的值為12。
    由于頁內所有地址都必須放在Offset字段, 因此80x86系統的頁的大小PAGE_SIZE是212=4096212=4096字節。
    PAGE_MASK宏產生的值為0xfffff000,用以屏蔽Offset字段的所有位。

    8.2.2 PMD-Page Middle Directory (頁目錄)


    字段描述
    PMD_SHIFT指定線性地址的Offset和Table字段的總位數;換句話說,是頁中間目錄項可以映射的區域大小的對數
    PMD_SIZE用于計算由頁中間目錄的一個單獨表項所映射的區域大小,也就是一個頁表的大小
    PMD_MASK用于屏蔽Offset字段與Table字段的所有位

    當PAE 被禁用時,PMD_SHIFT 產生的值為22(來自Offset 的12 位加上來自Table 的10 位),
    PMD_SIZE 產生的值為222 或 4 MB,
    PMD_MASK產生的值為 0xffc00000。

    相反,當PAE被**時
    PMD_SHIFT 產生的值為21 (來自Offset的12位加上來自Table的9位),
    PMD_SIZE 產生的值為221221 或2 MB
    PMD_MASK產生的值為 0xffe00000。

    大型頁不使用最后一級頁表,所以產生大型頁尺寸的LARGE_PAGE_SIZE 宏等于PMD_SIZE(2PMD_SHIFT),而在大型頁地址中用于屏蔽Offset字段和Table字段的所有位的LARGE_PAGE_MASK宏,就等于PMD_MASK。

    8.2.3 PUD_SHIFT-頁上級目錄(Page Upper Directory)


    字段描述
    PUD_SHIFT確定頁上級目錄項能映射的區域大小的位數
    PUD_SIZE用于計算頁全局目錄中的一個單獨表項所能映射的區域大小。
    PUD_MASK用于屏蔽Offset字段,Table字段,Middle Air字段和Upper Air字段的所有位

    在80x86處理器上,PUD_SHIFT總是等價于PMD_SHIFT,而PUD_SIZE則等于4MB或2MB。

    8.2.4 PGDIR_SHIFT-頁全局目錄(Page Global Directory)


    字段描述
    PGDIR_SHIFT確定頁全局頁目錄項能映射的區域大小的位數
    PGDIR_SIZE用于計算頁全局目錄中一個單獨表項所能映射區域的大小
    PGDIR_MASK用于屏蔽Offset, Table,Middle Air及Upper Air的所有位

    當PAE 被禁止時
    PGDIR_SHIFT 產生的值為22(與PMD_SHIFT 和PUD_SHIFT 產生的值相同),
    PGDIR_SIZE 產生的值為 222 或 4 MB,
    PGDIR_MASK 產生的值為 0xffc00000。

    相反,當PAE被**時
    PGDIR_SHIFT 產生的值為30 (12 位Offset 加 9 位Table再加 9位 Middle Air),
    PGDIR_SIZE 產生的值為230 或 1 GB
    PGDIR_MASK產生的值為0xc0000000

    PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD以及PTRS_PER_PGD

    用于計算頁表、頁中間目錄、頁上級目錄和頁全局目錄表中表項的個數。當PAE被禁止時,它們產生的值分別為1024,1,1和1024。當PAE被**時,產生的值分別為512,512,1和4。

     

    8.3 頁表處理函數


    [注意]
    以下內容主要參見 深入理解linux內核第二章內存尋址中頁表處理

    內核還提供了許多宏和函數用于讀或修改頁表表項:

    • 如果相應的表項值為0,那么,宏pte_none、pmd_none、pud_none和 pgd_none產生的值為1,否則產生的值為0。

    • 宏pte_clear、pmd_clear、pud_clear和 pgd_clear清除相應頁表的一個表項,由此禁止進程使用由該頁表項映射的線性地址。ptep_get_and_clear( )函數清除一個頁表項并返回前一個值。

    • set_pte,set_pmd,set_pud和set_pgd向一個頁表項中寫入指定的值。set_pte_atomic與set_pte作用相同,但是當PAE被**時它同樣能保證64位的值能被原子地寫入。

    • 如果a和b兩個頁表項指向同一頁并且指定相同訪問優先級,pte_same(a,b)返回1,否則返回0。

    • 如果頁中間目錄項指向一個大型頁(2MB或4MB),pmd_large(e)返回1,否則返回0。

    宏pmd_bad由函數使用并通過輸入參數傳遞來檢查頁中間目錄項。如果目錄項指向一個不能使用的頁表,也就是說,如果至少出現以下條件中的一個,則這個宏產生的值為1:

    • 頁不在主存中(Present標志被清除)。

    • 頁只允許讀訪問(Read/Write標志被清除)。

    • Acessed或者Dirty位被清除(對于每個現有的頁表,Linux總是
      強制設置這些標志)。

    pud_bad宏和pgd_bad宏總是產生0。沒有定義pte_bad宏,因為頁表項引用一個不在主存中的頁,一個不可寫的頁或一個根本無法訪問的頁都是合法的。

    如果一個頁表項的Present標志或者Page Size標志等于1,則pte_present宏產生的值為1,否則為0。

    前面講過頁表項的Page Size標志對微處理器的分頁部件來講沒有意義,然而,對于當前在主存中卻又沒有讀、寫或執行權限的頁,內核將其Present和Page Size分別標記為0和1。

    這樣,任何試圖對此類頁的訪問都會引起一個缺頁異常,因為頁的Present標志被清0,而內核可以通過檢查Page Size的值來檢測到產生異常并不是因為缺頁。

    如果相應表項的Present標志等于1,也就是說,如果對應的頁或頁表被裝載入主存,pmd_present宏產生的值為1。pud_present宏和pgd_present宏產生的值總是1。

     

    8.3.1 查詢頁表項中任意一個標志的當前值


    下表中列出的函數用來查詢頁表項中任意一個標志的當前值;除了pte_file()外,其他函數只有在pte_present返回1的時候,才能正常返回頁表項中任意一個標志。

    函數名稱說明
    pte_user( )讀 User/Supervisor 標志
    pte_read( )讀 User/Supervisor 標志(表示 80x86 處理器上的頁不受讀的保護)
    pte_write( )讀 Read/Write 標志
    pte_exec( )讀 User/Supervisor 標志( 80x86 處理器上的頁不受代碼執行的保護)
    pte_dirty( )讀 Dirty 標志
    pte_young( )讀 Accessed 標志
    pte_file( )讀 Dirty 標志(當 Present 標志被清除而 Dirty 標志被設置時,頁屬于一個非線性磁盤文件映射)

    8.3.2 設置頁表項中各標志的值


    下表列出的另一組函數用于設置頁表項中各標志的值

    函數名稱說明
    mk_pte_huge( )設置頁表項中的 Page Size 和 Present 標志
    pte_wrprotect( )清除 Read/Write 標志
    pte_rdprotect( )清除 User/Supervisor 標志
    pte_exprotect( )清除 User/Supervisor 標志
    pte_mkwrite( )設置 Read/Write 標志
    pte_mkread( )設置 User/Supervisor 標志
    pte_mkexec( )設置 User/Supervisor 標志
    pte_mkclean( )清除 Dirty 標志
    pte_mkdirty( )設置 Dirty 標志
    pte_mkold( )清除 Accessed 標志(把此頁標記為未訪問)
    pte_mkyoung( )設置 Accessed 標志(把此頁標記為訪問過)
    pte_modify(p,v)把頁表項 p 的所有訪問權限設置為指定的值
    ptep_set_wrprotect()與 pte_wrprotect( ) 類似,但作用于指向頁表項的指針
    ptep_set_access_flags( )如果 Dirty 標志被設置為 1 則將頁的訪問權設置為指定的值,并調用flush_tlb_page() 函數
    ptep_mkdirty()與 pte_mkdirty( ) 類似,但作用于指向頁表項的指針。
    ptep_test_and_clear_dirty( )與 pte_mkclean( ) 類似,但作用于指向頁表項的指針并返回 Dirty 標志的舊值
    ptep_test_and_clear_young( )與 pte_mkold( ) 類似,但作用于指向頁表項的指針并返回 Accessed標志的舊值

    8.3.3 宏函數-把一個頁地址和一組保護標志組合成頁表項,或者執行相反的操作


    現在,我們來討論下表中列出的宏,它們把一個頁地址和一組保護標志組合成頁表項,或者執行相反的操作,從一個頁表項中提取出頁地址。請注意這其中的一些宏對頁的引用是通過 “頁描述符”的線性地址,而不是通過該頁本身的線性地址。

    宏名稱說明
    pgd_index(addr)找到線性地址 addr 對應的的目錄項在頁全局目錄中的索引(相對位置)
    pgd_offset(mm, addr)接收內存描述符地址 mm 和線性地址 addr 作為參數。這個宏產生地址addr 在頁全局目錄中相應表項的線性地址;通過內存描述符 mm 內的一個指針可以找到這個頁全局目錄
    pgd_offset_k(addr)產生主內核頁全局目錄中的某個項的線性地址,該項對應于地址 addr
    pgd_page(pgd)通過頁全局目錄項 pgd 產生頁上級目錄所在頁框的頁描述符地址。在兩級或三級分頁系統中,該宏等價于 pud_page() ,后者應用于頁上級目錄項
    pud_offset(pgd, addr)參數為指向頁全局目錄項的指針 pgd 和線性地址 addr 。這個宏產生頁上級目錄中目錄項 addr 對應的線性地址。在兩級或三級分頁系統中,該宏產生 pgd ,即一個頁全局目錄項的地址
    pud_page(pud)通過頁上級目錄項 pud 產生相應的頁中間目錄的線性地址。在兩級分頁系統中,該宏等價于 pmd_page() ,后者應用于頁中間目錄項
    pmd_index(addr)產生線性地址 addr 在頁中間目錄中所對應目錄項的索引(相對位置)
    pmd_offset(pud, addr)接收指向頁上級目錄項的指針 pud 和線性地址 addr 作為參數。這個宏產生目錄項 addr 在頁中間目錄中的偏移地址。在兩級或三級分頁系統中,它產生 pud ,即頁全局目錄項的地址
    pmd_page(pmd)通過頁中間目錄項 pmd 產生相應頁表的頁描述符地址。在兩級或三級分頁系統中, pmd 實際上是頁全局目錄中的一項
    mk_pte(p,prot)接收頁描述符地址 p 和一組訪問權限 prot 作為參數,并創建相應的頁表項
    pte_index(addr)產生線性地址 addr 對應的表項在頁表中的索引(相對位置)
    pte_offset_kernel(dir,addr)線性地址 addr 在頁中間目錄 dir 中有一個對應的項,該宏就產生這個對應項,即頁表的線性地址。另外,該宏只在主內核頁表上使用
    pte_offset_map(dir, addr)接收指向一個頁中間目錄項的指針 dir 和線性地址 addr 作為參數,它產生與線性地址 addr 相對應的頁表項的線性地址。如果頁表被保存在高端存儲器中,那么內核建立一個臨時內核映射,并用 pte_unmap 對它進行釋放。 pte_offset_map_nested 宏和 pte_unmap_nested 宏是相同的,但它們使用不同的臨時內核映射
    pte_page( x )返回頁表項 x 所引用頁的描述符地址
    pte_to_pgoff( pte )從一個頁表項的 pte 字段內容中提取出文件偏移量,這個偏移量對應著一個非線性文件內存映射所在的頁
    pgoff_to_pte(offset )為非線性文件內存映射所在的頁創建對應頁表項的內容

    8.3.4 簡化頁表項的創建和撤消


    下面我們羅列最后一組函數來簡化頁表項的創建和撤消。當使用兩級頁表時,創建或刪除一個頁中間目錄項是不重要的。如本節前部分所述,頁中間目錄僅含有一個指向下屬頁表的目錄項。所以,頁中間目錄項只是頁全局目錄中的一項而已。然而當處理頁表時,創建一個頁表項可能很復雜,因為包含頁表項的那個頁表可能就不存在。在這樣的情況下,有必要分配一個新頁框,把它填寫為 0 ,并把這個表項加入。

    如果 PAE 被**,內核使用三級頁表。當內核創建一個新的頁全局目錄時,同時也分配四個相應的頁中間目錄;只有當父頁全局目錄被釋放時,這四個頁中間目錄才得以釋放。當使用兩級或三級分頁時,頁上級目錄項總是被映射為頁全局目錄中的一個單獨項。與以往一樣,下表中列出的函數描述是針對 80x86 構架的。

    函數名稱說明
    pgd_alloc( mm )分配一個新的頁全局目錄。如果 PAE 被**,它還分配三個對應用戶態線性地址的子頁中間目錄。參數 mm( 內存描述符的地址 )在 80x86 構架上被忽略
    pgd_free( pgd)釋放頁全局目錄中地址為 pgd 的項。如果 PAE 被**,它還將釋放用戶態線性地址對應的三個頁中間目錄
    pud_alloc(mm, pgd, addr)在兩級或三級分頁系統下,這個函數什么也不做:它僅僅返回頁全局目錄項 pgd 的線性地址
    pud_free(x)在兩級或三級分頁系統下,這個宏什么也不做
    pmd_alloc(mm, pud, addr)定義這個函數以使普通三級分頁系統可以為線性地址 addr 分配一個新的頁中間目錄。如果 PAE 未被**,這個函數只是返回輸入參數 pud 的值,也就是說,返回頁全局目錄中目錄項的地址。如果 PAE 被**,該函數返回線性地址 addr 對應的頁中間目錄項的線性地址。參數 mm 被忽略
    pmd_free(x)該函數什么也不做,因為頁中間目錄的分配和釋放是隨同它們的父全局目錄一同進行的
    pte_alloc_map(mm, pmd, addr)接收頁中間目錄項的地址 pmd 和線性地址 addr 作為參數,并返回與 addr 對應的頁表項的地址。如果頁中間目錄項為空,該函數通過調用函數 pte_alloc_one( ) 分配一個新頁表。如果分配了一個新頁表, addr 對應的項就被創建,同時 User/Supervisor 標志被設置為 1 。如果頁表被保存在高端內存,則內核建立一個臨時內核映射,并用 pte_unmap 對它進行釋放
    pte_alloc_kernel(mm, pmd, addr)如果與地址 addr 相關的頁中間目錄項 pmd 為空,該函數分配一個新頁表。然后返回與 addr 相關的頁表項的線性地址。該函數僅被主內核頁表使用
    pte_free(pte)釋放與頁描述符指針 pte 相關的頁表
    pte_free_kernel(pte)等價于 pte_free( ) ,但由主內核頁表使用
    clear_page_range(mmu, start,end)從線性地址 start 到 end 通過反復釋放頁表和清除頁中間目錄項來清除進程頁表的內容

    9 線性地址轉換


    9.1 分頁模式下的的線性地址轉換


    線性地址、頁表和頁表項線性地址不管系統采用多少級分頁模型,線性地址本質上都是索引+偏移量的形式,甚至你可以將整個線性地址看作N+1個索引的組合,N是系統采用的分頁級數。在四級分頁模型下,線性地址被分為5部分,如下圖:

    在線性地址中,每個頁表索引即代表線性地址在對應級別的頁表中中關聯的頁表項。正是這種索引與頁表項的對應關系形成了整個頁表映射機制。

    9.1.1 頁表


    多個頁表項的集合則為頁表,一個頁表內的所有頁表項是連續存放的。頁表本質上是一堆數據,因此也是以頁為單位存放在主存中的。因此,在虛擬地址轉化物理物理地址的過程中,每訪問一級頁表就會訪問一次內存。

    9.1.2 頁表項


    頁表項從四種頁表項的數據結構可以看出,每個頁表項其實就是一個無符號長整型數據。每個頁表項分兩大類信息:頁框基地址和頁的屬性信息。在x86-32體系結構中,每個頁表項的結構圖如下:

    這個圖是一個通用模型,其中頁表項的前20位是物理頁的基地址。由于32位的系統采用4kb大小的 頁,因此每個頁表項的后12位均為0。內核將后12位充分利用,每個位都表示對應虛擬頁的相關屬性。

    不管是那一級的頁表,它的功能就是建立虛擬地址和物理地址之間的映射關系,一個頁和一個頁框之間的映射關系體現在頁表項中。上圖中的物理頁基地址是 個抽象的說明,如果當前的頁表項位于頁全局目錄中,這個物理頁基址是指頁上級目錄所在物理頁的基地址;如果當前頁表項位于頁表中,這個物理頁基地址是指最 終要訪問數據所在物理頁的基地址。

    9.1.3 地址轉換過程


    地址轉換過程有了上述的基本知識,就很好理解四級頁表模式下如何將虛擬地址轉化為邏輯地址了。基本過程如下:

    • 1.從CR3寄存器中讀取頁目錄所在物理頁面的基址(即所謂的頁目錄基址),從線性地址的第一部分獲取頁目錄項的索引,兩者相加得到頁目錄項的物理地址。

    • 2.第一次讀取內存得到pgd_t結構的目錄項,從中取出物理頁基址取出(具體位數與平臺相關,如果是32系統,則為20位),即頁上級頁目錄的物理基地址。

    • 3.從線性地址的第二部分中取出頁上級目錄項的索引,與頁上級目錄基地址相加得到頁上級目錄項的物理地址。

    • 4.第二次讀取內存得到pud_t結構的目錄項,從中取出頁中間目錄的物理基地址。

    • 5.從線性地址的第三部分中取出頁中間目錄項的索引,與頁中間目錄基址相加得到頁中間目錄項的物理地址。

    • 6.第三次讀取內存得到pmd_t結構的目錄項,從中取出頁表的物理基地址。

    • 7.從線性地址的第四部分中取出頁表項的索引,與頁表基址相加得到頁表項的物理地址。

    • 8.第四次讀取內存得到pte_t結構的目錄項,從中取出物理頁的基地址。

    • 9.從線性地址的第五部分中取出物理頁內偏移量,與物理頁基址相加得到最終的物理地址。

    • 10.第五次讀取內存得到最終要訪問的數據。

    整個過程是比較機械的,每次轉換先獲取物理頁基地址,再從線性地址中獲取索引,合成物理地址后再訪問內存。不管是頁表還是要訪問的數據都是以頁為單 位存放在主存中的,因此每次訪問內存時都要先獲得基址,再通過索引(或偏移)在頁內訪問數據,因此可以將線性地址看作是若干個索引的集合。

    9.2 Linux中通過4級頁表訪問物理內存


    linux中每個進程有它自己的PGD( Page Global Directory),它是一個物理頁,并包含一個pgd_t數組。

    進程的pgd_t數據見 task_struct -> mm_struct -> pgd_t * pgd;

    PTEs, PMDs和PGDs分別由pte_t, pmd_t 和pgd_t來描述。為了存儲保護位,pgprot_t被定義,它擁有相關的flags并經常被存儲在page table entry低位(lower bits),其具體的存儲方式依賴于CPU架構。

    前面我們講了頁表處理的大多數函數信息,在上面我們又講了線性地址如何轉換為物理地址,其實就是不斷索引的過程。

    通過如下幾個函數,不斷向下索引,就可以從進程的頁表中搜索特定地址對應的頁面對象

    宏函數說明
    pgd_offset根據當前虛擬地址和當前進程的mm_struct獲取pgd項
    pud_offset參數為指向頁全局目錄項的指針 pgd 和線性地址 addr 。這個宏產生頁上級目錄中目錄項 addr 對應的線性地址。在兩級或三級分頁系統中,該宏產生 pgd ,即一個頁全局目錄項的地址
    pmd_offset根據通過pgd_offset獲取的pgd 項和虛擬地址,獲取相關的pmd項(即pte表的起始地址)
    pte_offset根據通過pmd_offset獲取的pmd項和虛擬地址,獲取相關的pte項(即物理頁的起始地址)

    根據虛擬地址獲取物理頁的示例代碼詳見mm/memory.c中的函數follow_page

    不同的版本可能有所不同,早起內核中存在follow_page,而后來的內核中被follow_page_mask替代,目前最新的發布4.4中為查找到此函數

    我們從早期的linux-3.8的源代碼中, 截取的代碼如下

    /**
     * follow_page - look up a page descriptor from a user-virtual address
     * @vma: vm_area_struct mapping @address
     * @address: virtual address to look up
     * @flags: flags modifying lookup behaviour
     *
     * @flags can have FOLL_ flags set, defined in <linux/mm.h>
     *
     * Returns the mapped (struct page *), %NULL if no mapping exists, or
     * an error pointer if there is a mapping to something not represented
     * by a page descriptor (see also vm_normal_page()).
     */
    struct page *follow_page(struct vm_area_struct *vma, unsigned long address,
                unsigned int flags)
    {
        pgd_t *pgd;
        pud_t *pud;
        pmd_t *pmd;
        pte_t *ptep, pte;
        spinlock_t *ptl;
        struct page *page;
        struct mm_struct *mm = vma->vm_mm;
    
        page = follow_huge_addr(mm, address, flags & FOLL_WRITE);
        if (!IS_ERR(page)) {
            BUG_ON(flags & FOLL_GET);
            goto out;
        }
    
        page = NULL;
        pgd = pgd_offset(mm, address);
        if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
            goto no_page_table;
    
        pud = pud_offset(pgd, address);
        if (pud_none(*pud))
            goto no_page_table;
        if (pud_huge(*pud) && vma->vm_flags & VM_HUGETLB) {
            BUG_ON(flags & FOLL_GET);
            page = follow_huge_pud(mm, address, pud, flags & FOLL_WRITE);
            goto out;
        }
        if (unlikely(pud_bad(*pud)))
            goto no_page_table;
    
        pmd = pmd_offset(pud, address);
        if (pmd_none(*pmd))
            goto no_page_table;
        if (pmd_huge(*pmd) && vma->vm_flags & VM_HUGETLB) {
            BUG_ON(flags & FOLL_GET);
            page = follow_huge_pmd(mm, address, pmd, flags & FOLL_WRITE);
            goto out;
        }
        if (pmd_trans_huge(*pmd)) {
            if (flags & FOLL_SPLIT) {
                split_huge_page_pmd(mm, pmd);
                goto split_fallthrough;
            }
            spin_lock(&mm->page_table_lock);
            if (likely(pmd_trans_huge(*pmd))) {
                if (unlikely(pmd_trans_splitting(*pmd))) {
                    spin_unlock(&mm->page_table_lock);
                    wait_split_huge_page(vma->anon_vma, pmd);
                } else {
                    page = follow_trans_huge_pmd(mm, address,
                                     pmd, flags);
                    spin_unlock(&mm->page_table_lock);
                    goto out;
                }
            } else
                spin_unlock(&mm->page_table_lock);
            /* fall through */
        }
    split_fallthrough:
        if (unlikely(pmd_bad(*pmd)))
            goto no_page_table;
    
        ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
    
        pte = *ptep;
        if (!pte_present(pte))
            goto no_page;
        if ((flags & FOLL_WRITE) && !pte_write(pte))
            goto unlock;
    
        page = vm_normal_page(vma, address, pte);
        if (unlikely(!page)) {
            if ((flags & FOLL_DUMP) ||
                !is_zero_pfn(pte_pfn(pte)))
                goto bad_page;
            page = pte_page(pte);
        }
    
        if (flags & FOLL_GET)
            get_page(page);
        if (flags & FOLL_TOUCH) {
            if ((flags & FOLL_WRITE) &&
                !pte_dirty(pte) && !PageDirty(page))
                set_page_dirty(page);
            /*
             * pte_mkyoung() would be more correct here, but atomic care
             * is needed to avoid losing the dirty bit: it is easier to use
             * mark_page_accessed().
             */
            mark_page_accessed(page);
        }
        if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
            /*
             * The preliminary mapping check is mainly to avoid the
             * pointless overhead of lock_page on the ZERO_PAGE
             * which might bounce very badly if there is contention.
             *
             * If the page is already locked, we don't need to
             * handle it now - vmscan will handle it later if and
             * when it attempts to reclaim the page.
             */
            if (page->mapping && trylock_page(page)) {
                lru_add_drain();  /* push cached pages to LRU */
                /*
                 * Because we lock page here and migration is
                 * blocked by the pte's page reference, we need
                 * only check for file-cache page truncation.
                 */
                if (page->mapping)
                    mlock_vma_page(page);
                unlock_page(page);
            }
        }
    unlock:
        pte_unmap_unlock(ptep, ptl);
    out:
        return page;
    
    bad_page:
        pte_unmap_unlock(ptep, ptl);
        return ERR_PTR(-EFAULT);
    
    no_page:
        pte_unmap_unlock(ptep, ptl);
        if (!pte_none(pte))
            return page;
    
    no_page_table:
        /*
         * When core dumping an enormous anonymous area that nobody
         * has touched so far, we don't want to allocate unnecessary pages or
         * page tables.  Return error instead of NULL to skip handle_mm_fault,
         * then get_dump_page() will return NULL to leave a hole in the dump.
         * But we can only make this optimization where a hole would surely
         * be zero-filled if handle_mm_fault() actually did handle it.
         */
        if ((flags & FOLL_DUMP) &&
            (!vma->vm_ops || !vma->vm_ops->fault))
            return ERR_PTR(-EFAULT);
        return page;
    }

    以上代碼可以精簡為

    unsigned long v2p(int pid unsigned long va)
    {
            unsigned long pa = 0;
            struct task_struct *pcb_tmp = NULL;
            pgd_t *pgd_tmp = NULL;
            pud_t *pud_tmp = NULL;
            pmd_t *pmd_tmp = NULL;
            pte_t *pte_tmp = NULL;
    
            printk(KERN_INFO"PAGE_OFFSET = 0x%lx\n",PAGE_OFFSET);
            printk(KERN_INFO"PGDIR_SHIFT = %d\n",PGDIR_SHIFT);
            printk(KERN_INFO"PUD_SHIFT = %d\n",PUD_SHIFT);
            printk(KERN_INFO"PMD_SHIFT = %d\n",PMD_SHIFT);
            printk(KERN_INFO"PAGE_SHIFT = %d\n",PAGE_SHIFT);
    
            printk(KERN_INFO"PTRS_PER_PGD = %d\n",PTRS_PER_PGD);
            printk(KERN_INFO"PTRS_PER_PUD = %d\n",PTRS_PER_PUD);
            printk(KERN_INFO"PTRS_PER_PMD = %d\n",PTRS_PER_PMD);
            printk(KERN_INFO"PTRS_PER_PTE = %d\n",PTRS_PER_PTE);
    
            printk(KERN_INFO"PAGE_MASK = 0x%lx\n",PAGE_MASK);
    
            //if(!(pcb_tmp = find_task_by_pid(pid)))
            if(!(pcb_tmp = findTaskByPid(pid)))
            {
                    printk(KERN_INFO"Can't find the task %d .\n",pid);
                    return 0;
            }
            printk(KERN_INFO"pgd = 0x%p\n",pcb_tmp->mm->pgd);
    
            /* 判斷給出的地址va是否合法(va&lt;vm_end)*/
            if(!find_vma(pcb_tmp->mm,va))
            {
                    printk(KERN_INFO"virt_addr 0x%lx not available.\n",va);
                    return 0;
            }
    
            pgd_tmp = pgd_offset(pcb_tmp->mm,va);
            printk(KERN_INFO"pgd_tmp = 0x%p\n",pgd_tmp);
            printk(KERN_INFO"pgd_val(*pgd_tmp) = 0x%lx\n",pgd_val(*pgd_tmp));
            if(pgd_none(*pgd_tmp))
            {
                    printk(KERN_INFO"Not mapped in pgd.\n");
                    return 0;
            }
    
            pud_tmp = pud_offset(pgd_tmp,va);
            printk(KERN_INFO"pud_tmp = 0x%p\n",pud_tmp);
            printk(KERN_INFO"pud_val(*pud_tmp) = 0x%lx\n",pud_val(*pud_tmp));
            if(pud_none(*pud_tmp))
            {
                    printk(KERN_INFO"Not mapped in pud.\n");
                    return 0;
            }
    
            pmd_tmp = pmd_offset(pud_tmp,va);
            printk(KERN_INFO"pmd_tmp = 0x%p\n",pmd_tmp);
            printk(KERN_INFO"pmd_val(*pmd_tmp) = 0x%lx\n",pmd_val(*pmd_tmp));
            if(pmd_none(*pmd_tmp))
            {
                    printk(KERN_INFO"Not mapped in pmd.\n");
                    return 0;
            }
    
            /*在這里,把原來的pte_offset_map()改成了pte_offset_kernel*/
            pte_tmp = pte_offset_kernel(pmd_tmp,va);
    
            printk(KERN_INFO"pte_tmp = 0x%p\n",pte_tmp);
            printk(KERN_INFO"pte_val(*pte_tmp) = 0x%lx\n",pte_val(*pte_tmp));
            if(pte_none(*pte_tmp))
            {
                    printk(KERN_INFO"Not mapped in pte.\n");
                    return 0;
            }
            if(!pte_present(*pte_tmp)){
                    printk(KERN_INFO"pte not in RAM.\n");
                    return 0;
            }
    
            pa = (pte_val(*pte_tmp) & PAGE_MASK) | (va & ~PAGE_MASK);
            printk(KERN_INFO"virt_addr 0x%lx in RAM is 0x%lx t .\n",va,pa);
            printk(KERN_INFO"contect in 0x%lx is 0x%lx\n", pa, *(unsigned long *)((char *)pa + PAGE_OFFSET)
    }

     

    相關鏈接


    我對linux內核四級分頁理解

    Linux內核4級頁表的演進

    Linux內存 之 頁表

    內存管理(四) 頁表數據結構

    Linux內存管理之我見(二)-頁表、頁式內存管理機制

    Linux分頁機制之概述--Linux內存管理(六)

    Linux分頁機制之分頁機制的演變--Linux內存管理(七)

    Linux分頁機制之分頁機制的實現詳解--Linux內存管理(八)

    版權聲明:本文為Rong_Toa原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
    本文鏈接:https://blog.csdn.net/Rong_Toa/article/details/109586581

    智能推薦

    解決Pyinstaller打包numpy和pandas庫文件過大問題

    解決Pyinstaller壓縮numpy和pandas庫文件過大問題 文件包類型和網上的方法 Windows下docker的安裝 在docker下實現打包     今天是2021年的第一天,先祝各位小伙伴現年快樂哈。最近因為做了一個項目,需要打包文件,文件中包含了numpy和pandas庫,結果打包出來幾百行的代碼居然要900m,人都傻了,翻遍了全網找解決方...

    【混沌工程】基于ChaosBlade實現網絡故障模擬

    一、前言 很久之前曾基于linux內核自帶的TC和netem模擬一些公網中遇到的極端情況(延遲、丟包、重復、損壞和亂序等),驗證了我們傳輸程序的健壯性! 具體細節可見這篇老博客: https://blog.csdn.net/u013128262/article/details/84784663 最近在復現kafka生產端一個timeout異常場景時,發現之前方案時因為內核和OS版本問題有些差異而無...

    使用FileZilla連接時超時,無法連接到服務器

    用FileZilla連接服務器時,顯示錯誤: 解決方法: 檢查基本的內容 主機是否寫錯 端口是否自定義,默認21 檢查用戶名和密碼是否錯誤 如果連接的是公司內網 使用ping命令,測試一下是否能收到數據 收不到則需要開啟VPN,再ping,看是否能接收數據(請老鐵們用自己最合適的方法解決) 如果開啟VPN后能接收數據,則可以連接一下服務器,如果不行(怎么可能不行),則跳轉3并依次嘗試 開啟VPN后...

    反匯編:結構體拷貝傳參

    一、結構體拷貝傳參 二、引用和常引用傳參 三、大結構體做形參/數組拷貝...

    猜你喜歡

    羊城杯web(eazyphp)

    題目環境 file_put_contents() file_put_contents() 函數把一個字符串寫入文件中 構造payload 在 url 中傳參 蟻劍連接拿到flag...

    37種傳感器(十七)之有水銀開關模塊+Stduino Nano&UNO

    37種傳感器(十七)之有水銀開關模塊+Stduino Nano&UNO 本文轉載自:http://www.stduino.com/forum.php?mod=viewthread&tid=41&extra=page%3D1= 關鍵詞: 51、stm32、arduino、stduino單片機、stduino UNO&Nano、水銀開關模塊 說明: 水銀,即金屬&ldq...

    CentOS7設置Nginx開機自啟

    目錄 在/etc/init.d文件下創建nginx文件 修改nginx文件路徑 設置文件執行權限 將nginx服務加入到chkconfig中 設置開啟啟動 啟動nginx服務 查看是否設置成功 在/etc/init.d文件下創建nginx文件 vim /etc/init.d/nginx 添加以下腳本內容 修改nginx文件路徑 nginx=”/usr/local/nginx/sbin/...

    363. Max Sum of Rectangle No Larger Than K

    363. Max Sum of Rectangle No Larger Than K 最大矩形和不超過k 題目: 給定一個矩陣matrix和一個整數k,求元素加和不超過k的最大子矩形。 解題思路: 動態規劃。 我們使用暴力搜索。 定義sum[i][j]為矩形(0,0)-(i,j)的和;然后我們遍歷這個矩形中的所有的子矩形,計算它與k的大小,并更新結果。 代碼:...

    精品国产乱码久久久久久蜜桃不卡