內存尋址

內存尋址


內存地址


使用用80x86微處理器時,必須區分以下三種不同的地址

  1. 邏輯地址包含在機器語言指令中用來指定一個操作數或一條指令的地址。這種尋址有80x86著名的分段結構中得到表現,它促使MS-DOSwindows程序員把程序分成若干段。每個邏輯地址都是由一個段和偏移量組成,偏移量指明瞭從段開始的地方到實際地址之間的距離。

  2. 線性地址是一個32位的無符號整數,可以用來表示高達4GB的地址,也就是,高達4294967296個內存單元。線性地址通常用十六進制表示,值的範圍從0x000000000xffffffff

  3. 物理地址用於內存芯片級內存單元尋址。它們與從微處理器的地址引腳發送到內存總線上的電信號相對應。物理地址同32位或36位無符號整數表示。


內存控制單元通過一種爲分段單元的硬件電路把一個邏輯地址轉換成線性地址;接着,第二個稱爲分頁單元的硬件電路把線性地址轉換成物理地址。(見圖21)


在多處理器系統中,所有CPU都共享同一內存;這意味着RAM芯片可以由獨立的CPU併發地訪問。因爲在RAM芯片上讀或寫操作必須串行地執行,因此一種所謂內存仲裁器的硬件電路插在總線和每個RAM芯片之間。其作用是如果其中一個RAM芯片空閒,就准予一個CPU訪問,如果該芯片忙於爲另一個處理器提出的請求服務,就廷遲這個CPU訪問。即使在單處理器上也使用內存仲裁器,因爲單處理器系統中包括一個叫做DMA控制器的特殊處理器,而DMA控制器與CPU併發操作。在多處理器系統的情況下,因爲仲裁器有多個輸入端口,所以其結構更加複雜。例如,雙pentium在每個芯片的入口維持一個兩端口仲裁器,並在試圖使用公用總線前請求兩個CPU交換同步信息。從編程觀點看,因爲仲裁器由硬件電路管理,因此它是隱藏的。


硬件中的分段


80286模型開始,Intel微處理器以兩種不同的方式執行地址轉換,這兩種方式分別稱爲實模式和保護模式。實模式存在主要原因是要維持處理器與早期模型兼容,並讓操作系統自舉。


段選擇符和段寄存器


一個邏輯地址由兩部分組成:一個段標識符和一個指定段內相對地址的偏移量。段標識符是一個16位長的字段,稱爲段選擇符如圖2-2所示,而偏移量是一個32位長的字段。


爲了快速方便地找到段選擇符,處理器提供段寄存器,段寄存器的唯一目的是存放段選擇符。這些段寄存器稱爲cs,ss,ds,es,fsgs。儘管只有6個段寄存器,但程序可以把同一個段寄存器用於不同的目的,方法是先將其值保存在內存中,用完後再恢復


6個寄存器中3個有專門的用途:

cs代碼段寄存器,指向包含程序指令的段。

Ss棧段寄存器,指向包含當前程序棧的段。

Ds數據段寄存器,指向包含靜態數據或者全局數據段。


其它3個段寄存器作一般用途,可以指向任意的數據段。


cs寄存器還有一個很重要的功能:它含有一個兩位的字段,用以指明CPU的當前特權級。值爲0代表最高優先級,而值爲3代表最低優先級。Linux只有0級和3級,分別稱之爲內核態和用戶態。


段描述符


每個段由一個8字節的段段描述符表示,它描述了段的特徵。段描述符放在全局描述符表或局部描述符表中。


通常只定義一個GDT,而每個進程除了存放在GDT中的段之外如果還需要創建附加的段,就可以有自己的LDTGDT在主存中的地址和大小存放在gdtr控制寄存器中,當前正被使用的LDT地址和大小放在ldtr控制寄存器中。


2-3闡明瞭段描述符的格式:表2-1解釋了圖中各個字段的含義


有幾種不同類型的段以及和它們對應的段描述符。下面列出了Linux中被廣泛使用的類型:


代碼段描述符

表示這個段描述符代表一個代碼段,它可以放在GDTLDT中。該描述符置S標誌爲1

數據段描述符

表示這個段描述符代表一個數據段,它可以放在GDTLDT中,該描述符置S標誌爲1

任務狀態段描述符

表示這個段描述符代表一個任務狀態段,也就是說這個段用於保存處理器寄存器的內容。它只能出現在GDT中,根據相應的進程是否正CPU上運行,其Type字段的值分別爲119。這個描述符的S標誌置爲0

局部描述符表描述符

表示這個段描述符代表一個包含LDT的段,它只出現在GDT中。相應的Type字段的值加2S標誌置爲0


快速訪問段描述符


邏輯地址由16位段選擇符和32位偏移量組成,段寄存器僅僅存放段選擇符。


爲了加速邏輯地址到線性地址的轉換,80x86處理提供一種附加的非編程的寄存器,供6個可編程的段寄存器使用。每一個非編程的寄存器含有8個字節的段描述符,由相應的段寄存器中的段選擇符來指定。每當一個段選擇符被裝入段寄存器時,相應的段描述符由內存到對應的非編程CPU寄存器。從那時起,針對那個寄存器的邏輯地址轉換就可以不訪問主存中的GDTLDT,處理器只需直接引用存放段描述符中CPU寄存器即可。僅當段寄存器的內容改變時,纔有必要訪問GDTLDT(2-4)


由於一個段描述符是8字節長,因此它在GDTLDT內的相對地址是由段選擇符的最高13位值乘以8得到的。例如:如果GDT0x00020000且由段選擇符所指定的索引號爲2,那麼相應的段描述符地址是0x00020000+ (2 * 8),


GDT第一項總是設爲0。這就確保空段選擇符的邏輯地址會被認爲是無效的,因此引起一個處理器異常。能夠保存在GDT中的段描述符的最大數目是8191


分段單元


2-5詳細顯示了一個邏輯地址是怎樣轉換成相應的線性地址的。分段單元執行以下操作:

  1. 先檢查段選擇符的TI字段,以決定段描述符保存在哪一個描述表中。TI字段指明描述符是在GDT中還是在激活的LDT中。

  2. 從段選擇符的index字段計算段描述符的地址,index字段的值乘以8,這個結果與gdtrldtr寄存器中的內容相加。

  3. 把邏輯地址的偏移量與段描述符Base字段的值相加就得了線性地址。


請注意,有了與段寄存器相關的不可編程寄存器,只有當段寄存器的內容被改變時才需要執行前兩個操作。


Linux中的分段


80x86微處理器中的分段鼓勵程序員把他們的程序化成邏輯上相關的實體,例如子程序或者全局與局部數據區。然而,Linux以很有限的方式使用分段。實際上,分段和分頁在某種程度上有點多餘,因爲它們都可以劃分進程的物理空間:分段可以給每一個進程分配不同的線性地址空間,而分頁可以把同一線性地址空間映射到不同的物理空間。與分段相比,Linux更喜歡使用分頁方式,因爲:


  1. 當所有進程使用相同的段寄存器值時,內存管理變得更簡單,也就是說它們能共享同樣的一組線性地址。

  2. Linux設計目標之一是可以把它移植到絕大多數流行的處理器平臺上。然而,RISC體系結構對分段的支持很有限。


2.6版的Linux只有在80x86結構下才需要使用分段。


運行在用戶態的所有Linux進程都使用一對相同的段來對指令和數據尋址。這個兩個段就是所謂的用戶代碼段和用戶數據段。類似地,運行在內核態的所有Linux進程都使用一個相同的段對指令和數據尋址:它們分別叫做內核代碼段和內核數據段。表2-3顯示了這四個重要段的段描述字段的值。


相應的段選擇符由宏__USER_CS,__USER_DS,__KERNEL_CS,__KERNEL_DS分別定義。例如,爲了對內核代碼段尋址,內核只需要把__KERNEL_CS宏產生的值裝進cs段寄存器即可。


注意,與段相關的線性地址從0開始,達到2321的尋址限長。這就意味着在用戶態或內核態下的所有進程可以使用相同的邏輯地址。


所有段都從0x00000000開始,這可以得出另一個重要結論,那就是在Linux下邏輯地址與線性地址是一致的,即邏輯地址的偏移量字段的值與相應的線性地址的值總是一致的。


如前所述,CPU的當前特權級反映了進程是在用戶態還是內核態,並由存放在cs寄存器中的段選擇符的RPL字段指定。只要當前特權級被改變,一些段寄存器必須相應地更新。例如,當CPL3時,ds寄存器必須含有用戶數據段的段描述符,而當CPL=0時,ds寄存器必須含有內核數據段的段選擇符。


類似的情況也出現在ss寄存器中。當CPL3時,它必須指向一個用戶數據段中用戶棧,當CPL0時,它必須指向內核段中的一個內核棧。當從用戶態切換到內核態時,Linux總是確保ss寄存器裝有內核數據段的段的段選擇符。


當對指向指令或者數據結構的指針進行保存時,內核根本不需要爲其設置邏輯地址的段選擇符,因爲cs寄存器就含有當前的段選擇符。例如,當內核調用一個函數時,它執行一條call彙編語言指令,該指令僅指定其邏輯地址的偏移部分,而段選擇符不用設置,它已經隱含在cs寄存器中了,因爲“在內核態執行”的段只有一種,叫做代碼段,由宏__KERNEL_CS定義,所以只要當CPU切換到內核態時將__KERNEL_CS裝載入cs就足夠了。同樣的首也適用於指向內核數據結構的指針。


除了剛纔描述的4個段以外,Linux還使用了其它幾個專門的段。


LinuxGDT


在單處理器系統中只有一個GDT,而在多處理器系統中每個CPU對應一個GDT。所有的GDT都存放在cpu_gdt_table數組中,而所有GDT的地址和它們的大小被存放在cpu_gdt_descr數組中。


2-6GDT的佈局示意圖。每個GDT包含18個段描述符和14個空的,未使用的,或保留的項,插入未使用的項的目的是爲了使經常一起訪問的描述符能夠處於同一個32字節的硬件高速緩存行中。


每一個GDT中包含的18個段描述符指向下列的段:

  1. 用戶態和內核態下的代碼段和數據段共4

  2. 任務狀態段,每個處理器有1。每個TSS相應的線性地址空間都是內核數據段相應線性地址空間的一個小子集。所有的任務狀態段都順序地存放在init_tss數組中;值得特別說明的是,第NCPUTSS描述符的Base字段指向init_tssN個元素。G標誌被清0,而Limit字段置爲0xeb,因爲TSS段是236字節長。Type字段置爲911,且DPL置爲0,因爲不允許用戶態下的進程訪問TSS段。

  3. 1個包括缺省局部描述符表的段,這個段通常是被所有進程共享的段

  4. 3個局部線程存儲段:這種機制允許多線程應用程序使用最多3個局部於線程的數據段。系統調用set_thread_area()get_thread_area()分別下在執行的進程創建和撤銷一個TLS段。

  5. 與高級電源管理相關的3個段:由於BIOS代碼使用段,所以當LinuxAPM驅動程序調用BIOS函數來獲取或者設置APM設備的狀態時,就可以使用自定義的代碼段和數據段。

  6. 與支持即敀插即用功能的BIOS服務程序相關的5個段:在前一種情況下,就像前述與AMP相關的3個段的情況一樣,由於BIOS例程使用段,所以當LinuxPnP設備驅動程序調用BIOS函數來檢測PnP設備使用的資源時,就可以使用自定義的代碼段和數據段。

  7. 被內核用來處理“雙重錯誤”異常的特殊TSS段。


如前所述,系統中每個處理器都有一個GDT副本。除了少數幾種情況以外,所有GDT的副本都存放相同的表項。首先,每個處理器都有它自己的TSS段,因此其對應的GDT項不同。其次,GDT中只有少數項可能依賴於CPU正在執行的進程。最後,在一些情況下,處理器可能臨時修改GDT副本里的某個項。


LinuxLDT


大多數用戶態下的Linux程序不使用局部描述符表,這樣內核就定義了一個缺省的LDT供大多數進程共享。缺省的局部描述符表存放在default_ldt數組中。它包含5個項,但內核僅僅有效地使用了其中的兩個項:用於iBCS執行文件的調用門和Solaris/x86可執行文件的調用門。調用門是80x86微處理器提供的一種機制,用於在調用預定義函數時改變CPU的特權段。


在一些情況下,進程仍然需要創建自己的局部描述符表。這對有些應用程序很有用,像wine那樣的程序,它們執行面向段的微軟windows應用程序。modify_ldt()系統調用允許進程創建自己的局部描述符。


任何被modify_ldt()創建的自定義局部描述符表仍然需要它自己的段。當處理器開始執行擁有自定義局部描述符表的進程時,該CPUGDT副本中的LDT表項相應地就被修改了。


用戶態下的程序同樣也利用modify_ldt()來分配新的段,但內核從不使用這些段,它也不需要了解相應的段描述符,因爲這些段描述符被包含在進程自定義的局部描述符表中了。


硬件中的分頁


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


爲了效率起見,線性地址被分成以固定長度爲單位的組,稱爲頁。頁內部連續的線性地址被映射到連續的物理地址中。這樣,內核可以指定一個頁的物理地址和其存取權限,而不用指定頁所包含的全部線性地址的存取權限。


分頁單元把所有的RAM分成固定長度的頁框。每一個頁框包含一個頁,也就是一個頁框的長度與一個頁的長度一致。頁框是主存的一部分,因此也是一個存儲區域。區分一頁和一個頁框是很重要的,前者是一個數據塊,可以存放任何頁框或磁盤中。


把線性地址映射到物理地址的數據結構稱爲頁表。頁表存放在主存中,並在啓用分頁單元之前由內核對頁表進行適當的初始化。


80386開始,所有的80x86處理器都支持分頁,它通過設置cr0寄存器的PG標誌啓用。當PG0,線性地址就被解釋成物理地址。


常規分頁


80386起,Intel處理器的分頁單元處理器4KB的頁。


32位的線性地址被分成3個域:


Directory(目錄)

最高10

Table(頁表)

中間10

Offset(偏移量)

最低12


線性地址的轉換分兩步完成,每一步都基於一種轉換表,第一種轉換表稱爲頁目錄表,第二種轉換表稱爲頁表。


使用這種二級模式的目的在於減少每個進程頁表所需RAM的數量。如果使用簡單的一級頁表,那將需要高達220次方個表頁來表示每個進程的頁表,即使一個進程並不使用那個範圍內的所有地址。二級模式通過只爲進程實際使用的那些虛擬內存區請求頁表來減少內存容量。


每個活動進程必須有一個分配給它的頁目錄。不過,沒有必要很快爲進程的所有頁表都分配RAM。只有在進程實際需要一個頁表時纔給該頁表分配RAM會更爲有效率。


正在使用的頁目錄的物理地址存放在控制寄存器cr3中。線性地址內的Directory字段決定頁目錄中的目錄項,而目錄項指向適當的頁表。地址的Table字段依次又決定頁表中的表項,而表項含有頁所在頁框的物理地址。Offset字段決定頁框的相對位置(如圖2-7)。由於它是12位長,幫每一頁含有4096字節的數據。


Directory字段和Table字段都是10位長,因此頁目錄和頁表都可以多達1024項。那麼一個頁目錄可以尋址到高達102410242092232次方個存儲單元,這和你對32位地址所期望的一樣。


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


Present標誌

如果被置爲1,所指的頁就在主存中;如果爲0,則這一頁不在主存中,些時這個表項剩餘的位可由操作系統用於自己的目的。如果執行一個地址轉換所需要頁表項目錄項中present標誌被清0,那麼分頁單元就把該線性地址存放在cr2中,併產生14號異常:缺頁異常。


包含頁框物理地址的最高20位的字段

由於每一個頁框有4KB的容量,它的物理地址必須是4096的倍數,因此物理地址的最低12總是爲0。如果這個字段指向一個頁目錄,相應的頁框就包含有一個頁表;如果它指向一個頁表。相應的頁框就含有一頁數據。


Accessed標誌

每當分佈單元對相應頁框進行尋址時就設置這個標誌。當選中的頁被交換出去時,這一標誌就可以由操作系統使用。分頁單元從來不重置這個標誌,而是必須由操作系統去做。


Dirty標誌

只應用於頁表項中,每當對一個頁框進行寫操作時就設置這個標誌。與Accessed標誌一樣,當選中的頁被交換出去時,這一標誌就可以由操作系統使用。分頁單元從來不智重置這個標誌。


Read/Write標誌

含有頁或頁表的存取權限。

User/Supervisor標誌

含有訪問頁或頁表所需的特權級。

PCDPWT標誌

控制硬件高速緩存處理頁或頁表的方式。

PageSize標誌

只應用於頁目錄項。如果設置爲1,則頁目錄項指的是2MB4MB的頁框。


擴展分頁

pentium模型開始,80x86微處理器引入了擴展分頁,它允許頁框大小爲4MB而不是4KB。擴展分頁用於把大段連續的線性地址轉換成相應的物理地址,在這種情況下,內核可以不用中間頁表進行地址轉換。


正如前面所述,通過設置頁目錄項的PageSize標誌啓用擴展分頁功能。在這種情況下,分頁單元把32位線性地址分成兩個字段:

Directory

最高10

offset

剩下22


擴展分頁與正常分頁的頁目錄基本相同,除了:

  1. PageSize標誌必須被設置

  2. 20位物理地址字段只有最高10位是有意義的。這是因爲每一個物理地址都是在以4MB爲邊界的地方開始的,故這個地址的最低22位爲0

通過設置cr4處理器寄存器的PSE標誌能使擴展分頁和正常分頁共存。


硬件保護方案


分頁單元和分段單元的保護方案不同。儘管80x86處理器允許一個段使用4種可能的特權級別,但與頁和頁表相關的特權級只有兩個,因爲特權由前面“常規分頁”一節中所提到的User/Ssupervisor標誌所控制。若這個標誌爲0,只有當CPL小於3時才能對頁尋址;若該標誌爲1,則總能對頁尋址。


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


常規分頁舉例


假定內核已給一個正在運行的進程分配的線性地址空間範圍是0x200000000x2003ffff。這個空間正好是64頁組成。我們不必關心包含這些頁的頁框的物理地址,事實上,其中的一些頁甚至可能不在主存中。只關注頁表項中剩餘的字段。


讓我們從分配給進程的線性地址的最高10開始。這兩個地址都以2開頭後面跟着0,因此高10有相同的值即0x080或十進制128。因此,這兩個地址的directory字段都指向進程頁目錄的第129項。相應的目錄項中必須包含分配給該進程的頁表的物理地址(2-9)。如果沒有給這個進程分配其它的線性地址,則頁目錄的其它1023項都填爲0


中間的10位的值範圍從00x03f。因而只有頁表的前64個表項是有意義的,其它960個表項都填0


假設進程需要讀線性地址0x20021406中的字節。這個地址由分頁單元按下面的方法處理:

  1. Directory字段的0x80用於選擇頁目錄的第0x80項,此目錄項指向和該進程的頁相關的頁表。

  2. Table字段0x21用於選擇頁表的第0x21表項,此表項指向包含所需頁的頁框。

  3. 最後,offset字段0x406用於在目標頁框中讀偏移量爲0x406中的字節。


如果頁表第0x21表項的present標誌爲0,則此頁不在主存中,在這種情況下,分頁單元在線性地址轉換的同時產生一缺頁異常。無論何時,當進程試圖訪問限定在0x200000000x2003ffff範圍之外的線性地址是,都將產生一個缺頁異常,因爲這些頁表項都填充了0,特別是它們的present標誌都被清0


物理地址擴展分頁機制


處理器所支持的RAM容量受連接到地址總線上的地址管腳數限制。早期Intel處理器從80386Pentium使用32位物理地址。從理論上講,這樣的的系統上可以安裝高達4GBRAM,而實際上,由於用戶進程線性地址空間的需要,內核不能直接對1GB以上的RAM進行尋址。


然而,大型服務器需要大於4GBRAM來同時運行數以千計的進程,近幾年這對Intel造成了壓力,所以必須擴展3280x86結構所支持的RAM容量。


Intel通過在它的處理器上把管腳數從32增加到36已經滿足了這些需求。從PentiumPro開始。Intel所有處理器現在尋址能力達64GB。不過,只有引入一種的分頁機制把32位線性地址轉換到36位物理地址才能使用所增加的物理地址。


pentiumPro處理器開始,Intel引入一種叫做物理地址擴展的機制。另外一種叫做頁大小擴展的機制在Pentium3處理器中引入,但是Linux並沒有使用這種機制。


通過設置cr4控制寄存器中的物理地址擴展標誌激活PAE。頁目錄中的頁大小標誌ps啓用大尺寸頁。


Intel爲了支持PAE已經改變了分頁機制。


  1. 64GBRAM被分爲了224次方個頁框,頁表項的物理地址字段從20位擴展到24位。因爲PAE頁表項必須包含12個標誌位和24個物理地址。總數之和爲36。頁表項大小從32位變成64們增加了一倍。結果,一個4KB的頁表包含512個表頁不是1024個表項。

  2. 引入一個叫做頁目錄指針表的頁表新級別,它由464位表項組成。

  3. cr3控制寄存器包含一個27位的頁目錄指針表基地址字段。因爲PDPT存放在RAM的前4GB中,並在32字節的倍數上對齊,因此27位足以表示這種表的基地址。

  4. 當把線性地址映射到4KB的頁時,32位線性地址按下列方式解釋:

    cr3

    指向一個PDPT

    3130

    指向PDPT4個項中的一個

    2921

    指向嶡目錄中512個項中的一個

    2012

      指向頁表中512項中一個

110

4KB頁中的偏移量

總之,一旦cr3被設置,就可能尋址高達4GBRAM,如果我們希望對更多的RAM尋址,就必須在cr3中放置一個新值,或改變PDPT的內容。然而,使用PAE的主要問題是線必地址仍然是32位長。


硬件高速緩存


當今的微處理器時鐘頻率接近幾個GHZ,而動態RAM芯片的存取時間是時鐘週期的數百倍。這意味着,當從RAM中取操作數或向RAM中存放結果這樣的指令執行是,CPU可能等待很長時間。


爲了縮小CPURAM之間的速度不匹配,引入了硬件高速緩存內存。硬件高速緩存基於著名的局部性原理,該原理既適用程序結構和適用數據結構。這表明由於程序的循環結構及相關數組可以組織成線性數組,最近最常用的相鄰在最近的將來又被用到的可能性極大。因此,引入小而快的內存來存放最近最常使用的代碼和數據變得很有意義。爲此,80x86體系結構中引入了一個叫行的新單元。行由幾十個連續的字節組成,它們以脈衝突發模式在慢速DRAM和快速的用來實現高速緩存的片上靜態RAM之間傳送,用來實現高速緩存。


高速緩存再被細分爲行的子集。在一種極端的情況下,高速緩存可以是直接映射的,這時主存中的一個行總是存放在高速緩存中完全相同的位置。在另一種極端情況下,高速緩存是充分關聯的,這意味着主存中的任意一個行可以存放在高速緩存中的任意位置。但是大多數高速緩存在某種程序上是N路相關聯的,意味着主存中的任意一個行可以存放在高速緩存N行中的任意一行中。例如,內存中的一個行可以存放到一個2路組關聯高速緩存兩個不同的行中。


轉換後援緩衝器


除了通過硬件高速緩存之外,80x86處理器還包含了另一個稱爲轉換後援緩衝器的高速緩存用於加快線性地址的轉換。當一個線性地址第一次使用時,通過慢速訪問RAM中的頁表計算出相應的物理地址。同時,物理地址被存放在一個TLB表項中,以便以後對同一個線性地址的引用可以快速地行到轉換。


Linux中的分頁


Linux採用了一種同時適用於32位和64位系統的普通分頁模型。正像前面“64位系統中的分頁”一節所解釋的那樣,兩級頁表對32位系統來說已經足夠了,但64位系統需要更多數量的分頁級別。直到2.6.10版本,Linux採用三級分頁模型。從2.6.11版本開始,採用了四級分頁模型。圖2-12中提示的4種頁表分別被爲:

  1. 頁全局目錄

  2. 頁上級目錄

  3. 頁中間目錄

  4. 頁表


頁全局目錄包含若干頁上級目錄的地址,頁上級目錄又依次包含若干頁中間目錄的地址,而頁中間目錄又包含若干頁表的地址。每一個頁表項指向一個頁框。線性地址因此被分成五個部分。圖2-12沒有顯示位數,因爲每一部分的大小與具體的計算機體系結構有關。


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


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


最後,64位系統使用三級還是四級分頁取決於硬件對線性地址的位的劃分(見表24)


Linux的進程處理很大程序上依賴於分頁。事實上,線性地址到物理地址的自動轉換使下面的設計目錄就得可行:

  1. 給每一個進程分配一塊不同的物理地址空間,這確保了可以有效地防止尋址錯誤。

  2. 區別頁和頁框之不同。這就允許放在某個頁框中的一個頁,然後保存到磁盤上,以後重新裝入這同一頁時又可以被裝在不同的頁框中。這就是虛擬內存機制的基本要素。


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


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


線性地址字段


下列宏簡化了頁表處理:


PAGE_SHIFT

指定Offset字段的位數;當用於80x86處理時,它產生的值爲12。由於頁內所有地址都必須能放到Offset字段中。因此80x86系統的頁的大小是4096個字節。PAGE_SHIFT的值爲12,可以看作以2爲低的頁大小的對數。這個宏由PAGE_SIZE使用以返回頁的大小。最後,PAGE_MASK宏產生的值爲0xfffff000,用以屏蔽offset字段的所有位。


PMD_SHIFT


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

PAE被禁用時,PDM_SHIFT產生的什爲22PMD_SIZE產生的值爲4MBPMD_MASK產生的值爲0xffc00000。相反,當PAE被激活時,PMD_SIZE產生的值爲2MPMD_MASK產生的值爲0xffe00000


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


PUD_SHIFT


指定頁上級目錄項能映射的區域大小的對數。PUD_SIZE宏用於計算頁全局目錄中的一個單獨項所能映射的區域大小。PUD_MASK宏用於屏蔽Offset字段、Table字段。MiddleAir字段等。

80x86處理上,PUD_SHIFT總是等價於PMD_SHIFT,而PUD_SIZE則等於4M2M


PGDIR_SHIFT


確定頁全局目錄能映射的區域大小的對象。PGDIR_SIZE宏用於計算頁全局目錄中一個單獨表項能映射區域的大小。PGDIR_MASK宏用於屏蔽OffsetTable,等一些字段。

PAE被禁止時,PGDIR_SHIFT產生的值爲22PGDIR_SIZE產生的值爲4M,以及PGDIR_MASK產生的值爲0xffc00000。相反,當PAE被激活時,PGDIR_SHIFT產生的值爲30PGDIR_SIZE產生的值爲1GB


PTRS_PER_PTEPTRS_PER_PMDPTRS_PER_PUDPTRS_PER_PGD


用於計算頁表,頁中間目錄,頁上級目錄和頁全局目錄表中表項的個數。當PAE被禁止時,它們產生的值爲別爲1024111024。當PAE被激活時,產生的值分別爲51251214


頁表處理


pte_t,pmd_t,pud_tpgd_t分別描述頁表項、頁中間目錄項、頁上級目錄和頁全局目錄項的格式。當PAE被激活時它們都是64位的數據類型。否則都是32位數據類型,它表示與一個單獨表項相關的保護標誌。


五個類型轉換宏(__pte,__pmd,__pud,__pgd__pgprot)把一個無符號整數轉換成所需要類型。另外的五個類型轉換成(pte_val,pmd_val,pud_val,pgd_valpgprot_val)執行相反的轉換,即把上面提到的四種特殊的類型轉換成一個無符號整數。


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

  1. 如果相應的表項值爲0,那麼,宏pte_none,pmd_none,pud_nonepgd_none產生的值爲1,否則產生的值爲0

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

  3. set_pte,set_pmd,set_pudset_pgd向一個頁表項中寫入指定的值。set_pte_atomicset_pte的作用相同,但是當PAE被激活時它同樣能保證64位的值被原子地寫入。

  4. 如果AB兩個頁表項指向同一個頁並且指定相同的訪問優先級,那麼pte_same(A,B)返回1,否則返回0

  5. 如果頁中間目錄項e指向一個大型頁,那麼pmd_large返回1,否則返回0


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

  1. 頁不在主存中

  2. 頁只允許讀訪問

  3. Acessed或者Dirty被清除。


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


如果一個頁表項的Present標誌或者pagesize標誌等於1,則pte_present宏產生的值爲1,否則爲0。前面講過頁表項的Pagesize標誌對微處理器的分頁單元來講沒有意義,然而,對於當前在主存中卻又沒有讀、寫或執行權限的頁,內核將其PresentPageSize分別標記爲01。這樣,任何試圖對此類頁的訪問都會引起一個缺頁異常。因爲頁的present標誌清0,而內核可能通過檢查Pagesize的值來檢測到產生異常並不是因爲缺頁。


如果相應表項的present標誌等於1,也就是說,如果對應的頁或頁表被載入主存,pmd_present宏產生的值爲1pud_presetn宏和pgd_present宏產生的值總爲1


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


26列出的另一組函數用於設置頁表項中和標誌的值。


27對頁表項進行操作,它們把一個頁地址和一組保護標誌組合成頁表項,或者執行相反的操作,從一個頁表項中提出頁地址。


當使用兩級頁表時,創建或刪除一個頁中間目錄項是不重要的。頁中間目錄僅含有一個指向下屬頁表的目錄項。所以,頁中間目錄項只是頁全局目錄中的一項而已。然而當處理頁表時,創建一個頁表可能很複雜,因爲包含頁表項的那個頁表可能就不存在。在這樣的情況下,有必要分配一個新頁框,把它填寫爲0,並把這個表項加入。


如果PAE被激活,內核使用三級頁表。當內核創建一個新的頁全局目錄時,同時也分配四個相應的頁中間目錄;只有當父頁全局目錄被釋放時,這四個頁中間目錄才以釋放。


當使用兩級或三級分頁時,頁上級目錄項總是被映射爲頁全局目錄中的一個單獨項。


28中列出的函數描述是針對80x86體系結構的。


物理內存佈局


在初始化階段,內核必須建立一個物理地址映射來指定哪些物理地址範圍對內核可用而哪些不可用。

內核將下列頁框記爲保留:

  1. 在不可用的物理地址範圍內的頁框

  2. 含有內核代碼和已初始化的數據結構的頁框


保留頁框的頁絕不能被動態分配或交換到磁盤上。


一般來說,Linux內核安裝在RAM中物理地址0x00100000開始地方,也就是說,從第二個MB開始。所需頁框總數依賴於內核的配置方案:典型的配置所得到的內核可以被安裝在小於3MRAM中。


爲什麼內核沒有安裝在RAM第一個MB開始的地方呢?因爲PC體系結構有幾個獨特的地方必須考慮到。劎例如:

  1. 頁框0BIOS使用,存放加電自檢期間檢查到的系統硬件配置。

  2. 物理地址從0x000a00000x000fffff的範圍通常留給BIOS例程,並且映射ISA圖形卡上的內部內存。這個區域就是所有IMB兼容PC上從640KB1MB之間著名的洞

  3. 第一個MB內的其它頁框可能由特定計算機模型保留。


在啓動過程的早期階段,內核詢問BIOS並瞭解物理內存的大小。在新近的教育處機中,內核也調用BIOS過程建立一組物理地址範圍和其對應的內存類型。


隨後,內核執行machine_specific_memory_setup()函數,該函數建立物理地址映(見表2-9)射。當然,如果這張表是可獲取的,那是內核在BIOS列表的基礎上構建的;否則,內核按保守的缺省設置構建這張表:從0x9f0x100號的所有頁框都標記爲保留。


2-9顯示了具有128MRAM計算機的典型配置。從0x07ff00000x07ff2fff的物理地址範圍中存有加電自檢階段由BIOS寫入的系統硬件設備信息;在初始化階段,內核把這些信息拷貝到一個合適的內核數據結構中,然後認爲這些頁框是可用的。相反,從0x07ff30000x07ffffff的物理地址範圍被映射到硬件設備的ROM芯片。從0xffff0000開始的物理地址範圍標記爲保留,因爲它由硬件映射到BIOSROM芯片。注意BIOS也許不提供一些物理地址範圍的信息。


內核可能不會見到BIOS報告的所有物理內存:例如,如果未使用PAE支持來編譯,即使有更大的物理內存可供使用,內核也只能尋址4GB大小的RAMsetup_memory()函數在machine_specific_memory_setup()執行後被調用:它分析物理內存區域表並初始化一些變量業描述內核的物理內存佈局,這些變量如表2-10所示。


爲了避免把內核裝入一組不連續的頁框裏面,Linux更願意跳過RAM的第一個MB。明確地說,LinuxPC體系結構未保留的頁框來動態存放所分配的頁。


2-13顯示Linux怎樣填充前3MBRAM。假定內核需要小於3MBRAM


符號_text對應於物理地址0x00100000,表示內核代碼第一個字節的地址。內核代碼的結束位置由另外一個類似的符號_etext表示。內核數據分爲兩組:初始化過的數據和和沒有初始化的數據。初始化過的數據在_etext後開始,在_edata處結束。緊接着是未初始化的數據並以_end結束。


進程的頁表


進程的線性地址空間分成兩部分:


  1. 0x000000000xbfffffff的線性地址,無論進程運行在用戶態還是在內核態都可以尋址。

  2. 0xc00000000xffffffff的線性地址,只有內核態的進程才能尋址。


當進程運行在用戶態時,它產生的線性地址小於0xc0000000;當進程運行在內核態時,它執行內核代碼,所產生的地址大於等於0xc0000000。但是,在一些情況下,內核爲了檢索或存放數據必須訪問用戶態線性地址空間。


PAGE_OFFSET產生的值爲0xc0000000,這就是進程在線性地址空間中的偏移量,也是內核生存空間的開始之處。


頁全局目錄的第一部分表項映射的線性地址小於0xc0000000,具體大小依賴於特定進程。


內核頁表


內核維持着一組自己使用的頁表,駐留在所謂的主內核頁全局目錄中。系統初始化後,這組頁表還從未被任何進程或任何內核線程直接使用;更確切地說,主內核頁全局目錄的最高目錄項部分作爲參考模型,爲系統中每個普通進程對應的頁全局目錄項提供參考模型。


內核初始化自己的頁表。這個過程分爲兩個階段。事實上,內核映像剛剛被裝入內存後,CPU仍然運行於實模式,所以分頁功能沒有被啓用。


第一個階段,內核創建一個有限的地址空間,包括內核的代碼段和數據段、初始頁表和用於存放動態數據結構的共128KB大小的空間。這個最小限度的地址空間僅夠內核狀入RAM和對初始化的核心數據結構。


第二個階段,內核充分利用剩餘的RAM並適當地建立分頁表。


臨時內核頁表


臨時頁全局目錄是在內核編譯過程中靜態地初始化的,而臨時頁表是由startup_32()彙編語言函數初始化的。


臨時頁全局目錄放在swapper_pg_dir變量中。臨時頁表在pg0變量處開始存放,緊接在內核未初始化的數據段後面。爲簡單起見,我們假定內核使用的段、臨時頁表和128KB的內存範圍能容納於RAM8MB空間裏。爲了映射RAM8MB的空間,需要用到兩個頁表。


分頁第一個階段的目錄是允許在實模式下和保護模式下都能很容易地對這8MB尋址。因些,內核必須創建一個映射,把從0x000000000x007fffff的線性地址和從0xc00000000xc07fffff的線性地址映射到從0x000000000x007fffff的物理地址。換句話說,內核在初始化的第一個階段,可以通過與物理地址相同的線性地址或者通過從0xc0000000開始的8MB線性地址對RAM的前8MB進行尋址


內核通過把swapper_pg_dir所有項都填充爲0來創建期望的映射,不過,010x3000x301這四項除外;後兩項包含了從0xc00000000xc07fffff間的所有線性地址。010x3000x301按以下方式初始化:

  1. 0項和0x300項的地址字段置爲pg0的物理地址,而1項和0x301項的地址字段置爲緊隨pg0後的頁框的物理地址。

  2. 把這四個項中的PresentReand/WriteUser/Supervisor標誌置位。

  3. 把這四個項中的AccessedDirytPCDPWDPageSize標誌清0

彙編語言函數startup_32()也啓用分頁單元,通過向cr3控制寄存器裝入swpper_pg_dir的地址及設置cr0控制寄存器的PG標誌來達到這一目的。下面是等價的代碼片段:


RAM小於896MB時的最終內核頁表


由內核頁表所提供的最終映射必須把從0xc0000000開始的線性地址轉化爲從0開始的物理地址。


__pa用於把從PAGE_OFFSET開始的線性地址轉換成相應的物理地址,而宏__va做相反的轉化。


主內核頁全局目錄仍然保存在swapper_pg_dir變量中。它由page_init()函數初始化。該函數進行如下操作:

  1. 調用pagetable_init()適當地建立頁表項。

  2. swapper_pg_dir的物理地址寫cr3控制寄存器中。

  3. 如果CPU支持PAE並且如果內核編譯時支持PAE,則將cr4控制寄存器的PAE標誌置位。

  4. 調用__flush_tlb_all()使TLB的所有項無效。

pagetable_init()執行的操作既依賴於現有RAM的容量,也依賴於CPU模型。計算機有小於896MBRAM32位物理地址足以對所有可用RAM進行尋址,因而沒有必要激活PAE機制。


我們假定CPU是支持4MB頁和“全局”TLB表項的最新80x86微處理器。注意如果頁全局目錄項對應的是0xc0000000之上的線性地址,則把所有這些項的User/Supervisor標誌清0。由此拒絕用戶態進程訪問內核地址空間。還要注意PageSize被置位使得內核可能通過使用大型而對RAM進行尋址。


startup_32()函數創建的物理內存前8MB的恆等映射用來完成內核的初始化階段。當這種映射不再必要是,內核調用zap_low_mappings()函數清除對應的頁表項。


RAM大小在896MB4096MB之間時的最終內核頁表


在這種情況下,並把RAM全部映射到內核地址空間。Linux在初始化階段可以做的最好的事把一個具有896MBRAM窗口映射到內核線性空間。如果一個程序需要對現在RAM的其餘部分尋址,尋就必須把某些其它的線性地址間隔映射到所需的RAM。這意味着修改一些頁表的值。


RAM大於4096MB時的最終內核頁表


如果RAM大於4GB計算機的內核頁表初始化;更確切地說,要處理以下發生的情況:

  1. CPU模型支持物理地址擴展

  2. RAM容量大於4GB

  3. 內核以PAE支持來編譯

儘管PAE處理36位物理地址,但是線性地址依然是32位地址,如前所述,Linux映射一個896MBRAM窗口到內核線性地址空間,剩餘RAM留着不映射,並由動態重映射來處理。


而全局目錄中的前三項與用戶線性地址空間相對應,內核用一個空頁的地址對這三項進行初始化。第四項用頁中間目錄中的前448項用RAM896MB的物理地址填充。


注意,支持PAE的所有CPU模型也支持大型2MB頁和全局頁。正如前一種情況一樣,只要可能,Linux使用大型頁來減少頁表數。


然後頁全局目錄的第四項被拷貝到第一項中,這樣好爲線性地址空間的前896MB中的低物理內存映射做鏡像。爲了完成對SMP系統的初始化,這個映射是必需的:當這個映射不再必要時,內核通過調用zap_low_mapping()函數來清除對應的頁表項。


固定映射的線性地址


我們看到內核線性地址第四個GB的初始部分映射系統的物理內存。但是,至少128M的線性地址總是留作他用,因爲內核使用這些線性地址實現非連續內存分配和固定映射的線性地址。


非連續內存分配僅僅是動態分配和釋放內存頁的一種特殊方式。


固定映射的線性地址基本上是一種類似於0xffffc000這樣的常量線性地址,其對應的物理地址不必等於線性地址送去0xc0000000,而是可以以任意方式建立。因此,每個固定映射的線性地址都映射一個物理內存的頁框。其實主是使用固定映射的線性地址來代替指針變量,因爲這些變量的值從不改變。


固定映射的線性地址概念上類似於對RAM896MB映射的線性地址。不過,固定映射的線性地址可以映射任何物理地址,而由第4GB初始化的線性地址所建立的映射是線性的。


就指針變量而言,固定映射的線性地址更有效。事實上間接引用一個立即常量地址要多一次內存訪問。此外,在間接引用一個指針變量之前對其值進行檢查是一個良好的編程習慣。


每個固定映射的線性地址都定義於enumfixed_address數據結構中的整型索引來表示:


每個固定映射的線性地址都存放在線性地址第四個GB的低端。fix_to_virt()函數計算從給定索引開始的常量線性地址:


假定某個內核函數調用fix_to_virt(FIX_IO_APIC_BASE_0).因爲該函數聲明爲“inline”,所以C編譯程序不調用fix_to_virt(),而是僅僅把它的代碼插入到調用函數中。此外,運行時從不對這個索引值執行檢查。事實上,FIX_IO_APIC_BASE_0是個等於3的常量,因此編譯程序可以去掉if語句,因爲它的條件在編譯時爲假。相反,如果條件爲址,或者fix_to_virt()參數不是一個常量,則編譯程序在連接階段產生一個錯誤。


爲了把一個物理地址與固定映射的線性地址關聯起來,內核使用set_fixma(idx,phys)set_fixmap_nocache(idx,phys)宏。這兩個函數都把fix_to_virt(idx)線性地址對應一個頁表項初始化爲物理地址phys;不過,第二個函數也把頁表項的PCD標誌置位,因此,當訪問這個頁框中的數據時禁用硬件高速緩存。反過來,clear_fixmap(idx)用來撤消固定映射線性地址idx和物理地址之間的連接。


處理硬件高速緩存和TLB


內存尋址的最後一個主題是關於內核如果使用硬件高速緩存來達到最佳效果。硬件高速緩存和轉換後援緩衝器在提高現代計算機體系結構的性能上扮演着重要的角色。採用一些技術來減少高速緩存和TLB的未命中次數。


處理硬件高速緩存


如前所述,硬件高速緩存是通過高速緩存行尋址的。L1_CACHE_BYTES宏產生以字節爲單位的高速緩存行的大小。


爲了使高速緩存的命中率達到最優化,內核在下列決策中考慮體系結構:

  1. 一個數據結構中最常使用的字段放在該數據結構內的低偏移部分,以便它們能夠處於高速緩存的同一行中。

  2. 當爲一大組數據結構分配空間時,內核試圖把它們都存放在內存中,以便所有高速緩存行按同一方式使用。

80x86微處理器自動處理高速緩存的同步,所以應用於這種處理器的Linux內核並不處理任何硬件高速緩存的刷新。不過內核卻爲不能同步高速緩存的處理器提供了高速緩存刷新接口。


處理TLB


處理器不能自動同步它們自己的TLB高速緩存,因爲決定線性地址和物理地址之間映射何時不再有效的是內核,而不是硬件。


Linux2.6提供了幾種在合適時機應當運用的TLB刷新方法,這取決於頁表更換的類型。


儘管普通Linux內核提供了豐富的TLB方法,但通常每個微處理器都提供了更受限制的一組使TLB無效的彙編語言指令。在這個方面,一個更爲靈活的硬件平臺就是SunUltraSPARC.與之相比,Intel微處理器只提供了兩種使TLB無效的技術:

  1. 在向cr3寄存器寫入值時所有Pentium處理器自動刷新相對於非全局頁的TLB表項。

  2. pentiumPro及以後的處理器中,invlpg彙編語言指令使映射指定線性地址的單個TLB表項無效。

2-12列出了採用這種硬件技術的 Linux宏;這些宏是實現獨立於系統的方法的基本要點。


注意表2-12中沒有flush_tlb_pgtables方法:在80x86系統中,當頁表與父頁表解除鏈接時什麼也不需要做,所以實現這個方法的函數爲空。


獨立於體系結構的使TLB無效的方法非常簡單地擴展到了多處理器系統,在一個CPU上運行的函數發送一個處理器間中斷給其它的CPU來強制它們執行適當的函數使TLB無效。


一般來說,任何進程切換都會暗示着更換活動頁表集。相對於過期頁表,本地TLB表項必須被刷新:這個過程在內核把新頁全局目錄的地址寫入cr3控制寄存器時會自動完成。不過內核在下列情況下將避免TLB被刷新:

  1. 當兩個使用相同頁表集的普通進程之間執行進程切換時。

  2. 當在一個普通進程一個內核線程執行進程切換時。事實上,內核線程並不擁用自己的頁表集;更確切地說,它們使用剛在CPU上執行過的普通進程的頁表集。

除了進程切換以外,還有其它幾種情況下內核需要刷新TLB中的一些表項。例如,當內核爲某個用戶態進程分配頁框並將它的物理地址存入頁表項,它必須刷新與相應線性地址對應的任何本地TLB表項。在多處理器系統中,如果有多個CPU在使用相同的頁表集,那麼內核還必須刷新這些CPU上使用相同頁表的TLB表項。


爲了避免多處理器系統上無用的TLB刷新,內核使用一種叫做懶惰TLB模式的技術。其基本思想是,如果幾個CPU正在使用相同的頁表,而且必須對這些CPU上的一個TLB表項刷新,那麼,在一些情況下,正在運行內核線程的那些CPU上的刷新就可以廷遲。


事實上,每個內核線程並不擁有自己的頁表集;更確切的說,它使用一個普通進程的頁表集。不過,沒有必要使用一個用戶態線性地址對應的TLB表項無效,因爲內核線程不訪問內核態地址空間。


當某個CPU開始運行一個內核線程時,內核把它置爲懶惰TLB模式。當發出清除TLB表項的請求時,處理懶惰TLB模式的每個CPU都不刷新相應的表項;但是,CPU記住它的當前進程正運行在一組頁表上,而這組頁表的TLB表項對用戶態地址是無效的。只要處於懶惰TLB模式的CPU用一個不同的頁表集切換到一個普通進程,硬件就自動刷新TLB表項,同時內核把CPU設置爲非懶惰TLB模式。然而,如果處理懶惰TLB模式的CPU切換到進程與剛纔運行的內核擁有相同的頁表集,那麼,任何使TLB無效的廷遲操作必須由內核有效地實施;這種使TLB無效的“懶惰”操作可以通過刷新CPU的所有非全局TLB項來有效地獲取。


爲了實現懶惰TLB模式,需要一些額外的數據結構。cpu_tlbstate變量是一個具有NR_CPUS個結構的靜態數組,這個結構有兩個字段,一個是指向當前進程內存描述符的active_mm字段,一個是具有兩個狀態值的state字段:TLBSTATE_OKTLBSTATE_LYZY。此外,每個內存描述符中包含一個cpu_vm_mask字段,該字段存放的是CPU下標;只有當內存描述符屬於當前運行的一個進程時這個字段纔有意義。


當一個CPU開始執行內核線程是,內核把該CPUcpu_tlbstate元素的state字段置爲TLBSTATE_LAZY;此外,活動內存描述符的cpu_vm_mask字段存放系統中所有CPU的下標。對於與給定頁表集相關的所有CPUTLB表項,當另外一個CPU想使這些表項我效時,該CPU就把一個處理器間中斷髮送給下標處於對應內存描述符的cpu_vm_mask字段中的那些CPU


CPU接受到一個與TLB刷新相關的處理器中斷,並驗證它影響了當前進程的頁表集時,它就檢查它的cpu_tlbstate元素的state字段是否等於TLBSTATE_LAZY。如果等於,內核就拒絕使TLB表項無效,並從內存描述符的cpu_vm_mask字段刪除該CPU下標。這有兩種結果:


  1. 只要CPU還處於懶惰TLB模式,它將不接受其它與TLB刷新相關的處理器間中斷。

  2. 如果CPU切換到另一個進程,而這個進程與剛被替換的內核線程使用相同的頁表集。那麼內核調用__flush_tlb()使該CPU的所有非全局TLB表項有效。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章