X86_64 CR3控制寄存器詳解

CR3寄存器目前博客主要能查找到的內容都比較簡單,例如

控制寄存器 cr0,cr2,cr3》等博客,只對CR3寄存器進行了簡單的介紹:

狀態和控制寄存器組除了EFLAGS、EIP ,還有四個32位的控制寄存器,它們是CR0,CR1,CR2和CR3

CR3含有存放頁目錄表頁面的物理地址,因此CR3也被稱爲PDBR。因爲頁目錄表頁面是頁對齊的,所以該寄存器只有高20位是有效的。而低12位保留供更高級處理器使用,因此在往CR3中加載一個新值時低12位必須設置爲0。

使用MOV指令加載CR3時具有讓頁高速緩衝無效的副作用。爲了減少地址轉換所要求的總線週期數量,最近訪問的頁目錄和頁表會被存放在處理器的頁高速緩衝器件中,該緩衝器件被稱爲轉換查找緩衝區(Translation Lookaside Buffer,TLB)。只有當TLB中不包含要求的頁表項時纔會使用額外的總線週期從內存中讀取頁表項。

即使CR0中的PG位處於復位狀態(PG=0),我們也能先加載CR3。以允許對分頁機制進行初始化。當切換任務時,CR3的內容也會隨之改變。但是如果新任務的CR3值與原任務的一樣,處理器就無需刷新頁高速緩衝。這樣共享頁表的任務可以執行得更快。

本文深入挖掘一下CR3寄存器的相關信息,瞭解MMU、TLB、操作系統與CR3寄存器的交互

一、CR3寄存器

對於64位機,CR3寄存器也從32位變成了64位,它的主要功能還是用來存放頁目錄表物理內存基地址,每當進程切換時,Linux就會把下一個將要運行進程的頁目錄表物理內存基地址等信息存放到CR3寄存器中。

圖片來源(http://ilinuxkernel.com/?p=606

二、CR3寄存器與TLB

關於CR3寄存器與TLB找到了兩個大佬的博文,對於TLB裏面的信息描述得很清楚,可以直接移步~

1、《TLB原理》

地址:https://zhuanlan.zhihu.com/p/108425561?utm_source=wechat_timeline

這個文章對於TLB的原理描述地很清晰,TLB只能使用虛擬地址來做tag,那麼是否會出現TLB別名問題以及TLB歧義問題都進行了分析。文中指出了TLB不存在別名問題,但是存在TLB歧義,解決TLB歧義最簡單的方法就是進程切換之後使整個TLB無效,這會導致性能損失,所以文中提出了儘可能避免TLB flush的方法,例如在TLB中添加一項ASID(Address Space ID)的匹配之類的。這篇文章很有參考價值,內容也比較新~

2、《進程切換分析(2):TLB處理》

地址:http://www.wowotech.net/process_management/context-switch-tlb.html

這個文章也提出了《TLB原理》這個文章中的一些問題,但是沒有上一篇文章那麼細緻,它重點關注了進程切換的時候,TLB的一些處理。

在《深入理解LINUX內核》一書中指出,Intel微處理器只提供了兩種使TLB無效的技術:

  • 在向CR3寄存器寫入值時所有Pentium處理器自動刷新相對於非全局頁的TLB表項;
  • 在Pentium Pro及之後的處理器中,invlpg彙編語言指令使映射指定虛擬地址的單個TLB表項無效。

深入理解LINUX內核基於2.6版本內核,本文將基於4.15版本內核對相關內容進行探究,所以上面的說法可能有些已經過時,需要我們自己看內核代碼來分析。對於本節給出的文章2《進程切換分析(2):TLB處理》,它指出x86平臺上,在進程切換的時候,軟件不需要顯示的調用tlb flush函數,在switch_mm函數中會用next task中的mm->pgd加載CR3寄存器,這時候load cr3的動作會導致本cpu中的local tlb entry被全部flush掉。在x86支持PCID(X86術語,相當與ARM的ASID)的情況下會怎樣呢?也會在load cr3的時候flush掉所有的本地CPU上的 local tlb entry嗎?其實在linux中,由於TLB shootdown,普通的linux並不支持PCID,因此,對於x86的進程地址空間切換,它就是會有flush local tlb entry這樣的side effect。

但是對於切換到內核線程時,不需要進行TLB flush,這裏涉及到enter_lazy_tlb函數。

三、CR3寄存器與操作系統

CR3寄存器的改變與操作系統的關聯主要是由於進程切換,每當進程切換時,CR3的內容需要被操作系統修改。

先了解一下進程切換的具體內容,從本質上說,每個進程切換由兩部分組成:

1、切換頁全局目錄以安裝一個新的地址空間

2、切換內核態堆棧和硬件上下文,因爲硬件上下文提供了內核執行新進程所需要的所有信息,包括CPU寄存器

context_switch切換到新的MM,同時修改新進程寄存器的狀態。

先看代碼(基於Linux-4.15 x86)。在代碼中,如果next task是內核線程,我們並不會執行switch_mm(該函數會引起tlb flush的動作),而是調用enter_lazy_tlb進入lazy tlb mode。在x86架構下,代碼如下:

 

這段代碼的意思就是如果要切入的next task是一個內核線程(next->mm == NULL )的話,那麼可以通過enter_lazy_tlb函數標記本cpu上的next task進入lazy TLB mode。

entry_lazy_tlb不是本文重點,修改CR3寄存器狀態是本文的重點~

在context_switch函數中,switch_mm_irqs_off(進行頁切換,如更新cr3寄存器)是一個重點

從上面的代碼(linux-4.15版本)中,可以看出還是有ASID這個概念的(與上一節文章2有一定區別了)。

switch_mm_irqs_off函數比較重要,裏面內容還是挺多的。

從這裏可以看出來在x86中還是有asid這個概念,

如果是一個新進程,那麼定義了新的asid:new_asid

獲取new_asid,進入到該函數查看該函數內容

裏面用到了next->context,先查看一下這個context到底是什麼內容再做具體分析

context是mm_context_t類型,繼續查看mm_context_t的內容

從上可以看到ctx_id唯一標識了一個mm_struct(內存描述符),ctx_id永不會被重複使用,如果ctx_id爲0則表示這個ctx_id無效。

tlb_gen: 任何需要爲此mm執行任何類型的TLB刷新的代碼都將首先對其頁表進行更改,然後遞增tlb_gen,然後刷新。這使低層級刷新代碼可以跟蹤需要刷新的內容。

回到函數choose_new_asid函數,

如果next進程的進程描述符id在系統中(個人感覺就是如果ctx_id能夠對應上,那麼說明asid就已經在TLB中,只需要把new_asid賦予它原來的asid即可,也就是不用重新分配,有的話就直接返回)

如果沒有,那麼就需要分配一個asid,分配asid的代碼應該如下:

其中TLB_NR_DYN_ASIDS等於6

this_cpu_add_return()函數開始就看不太懂了~接下來回到switch_mm_irqs_off函數。

這段代碼應該是ASID分配完了,所以需要flush,然後把新的asid的相關內容添加進來,如果不需要flush,那麼直接寫cr3寄存器,將next的內容填入cr3。

上面是修改cr3的代碼,write_cr3調用的是native_read_cr3函數~

繼續看上面的代碼,invalidate_user_asid函數如下,對於一個給定的ASID,flush它對應的user ASID

然後根據上面的代碼根據pgdir和new_asid構建新的cr3的內容,下面的兩個函數定義在arch、x86/include/asm/tlbflush.h中

如果支持PCID,那麼cr3就由pgd和asid組成,否則就是pgd

最後寫入cr3~

現在的疑問是這個asid是怎麼分配的,它與pid的關係,以及它被保存在哪裏?

先了解兩個結構體tlb_context與tlb_state:

1、struct mm_struct *loaded_mm:每當中斷打開時,cpu_tlbstate.loaded_mm應與CR3匹配。這意味着它可能與current-> active_mm不匹配,即使我們已經切換回swapper_pg_dir,當我們處於懶惰TLB模式時,它將包含先前的用戶mm。

2、bool invalidate_other:如果設置將頁面表更改爲需要使所有上下文(也就是PCID / ASID)無效的方式。這告訴我們在下一個上下文切換上使所有未加載的ctxs []無效。當前ctx在運行時保持最新狀態,不需要無效。

3、unsigned short user_pcid_flush_mask:包含TLB_NR_DYN_ASIDS + 1位的掩碼,用於指示相應的用戶PCID(https://blog.csdn.net/jus3ve/article/details/79544927下次切換時需要刷新; 參見SWITCH_TO_USER_CR3

4、struct tlb_context ctxs[TLB_NR_DYN_ASIDS]:

1)這是TLB中可能存在的所有上下文的列表。我們使用的每個ASID都有一個,而ASID(CPU稱爲PCID)是ctxt的索引(但是很遺憾TLB_NR_DYN_ASIDS等於6)。

2)對於每個上下文,ctx_id表示TLB用戶條目來自哪裏(來自哪個進程地址空間mm_struct)。 作爲不變式,TLB永遠不會包含過時的條目,例如該mm到達列表中的tlb_gen時。

3) 明確地說,這意味着TLB代碼在不更新tlb_gen的情況下刷新TLB是合法的。 由於paravirt remote flushes,這種情況可能會發生(至少目前如此)。

4) 注意:context 0有點特殊,因爲初始化代碼的各個位也使用它。 這很好-不知道PCID的代碼最終將無害地刷新context 0。

5、struct tlb_context {

    u64 ctx_id;

    u64 tlb_gen;

};

cpu_tlbstate暫未在內核代碼中找到相關定義,根據資料來看(書籍《深入理解Linux內核》,與https://wenku.baidu.com/view/0dabe0be453610661fd9f41f.html)大概可以知道cpu_tlbstate是內核中一個由tlb_state結構組成的全局數組,數組的大小就是cpu的個數。

操作系統讀寫cr3接口

四、總結

從代碼中可以看出,x86在linux-4.15版本還是有關於ASID的代碼。

 

在這裏面也有一個判斷,如果系統支持X86_FEATURE_PCID,那麼對CR3寄存器寫入的時候也會將ASID寫入CR3(寫入CR3的低12位),也就是儘可能避免TLB的flush~ 。

但是從ASID分配來看似乎支持的最大ASID爲6,這個有一定的疑問,是不是因爲ASID多了之後TLB不好管理之類的,如果有大佬知曉還請評論解答一下~

TLB在判斷一個虛擬地址是否命中時,根據ASID來看是否是對應進程的,ASID被分配完了之後需要flush tlb,然後新進程切換進來又重新分配ASID。cpu_tlbstate裏面綁定ASID與進程mm_struct的綁定,讓TLB中的一個ASID只對應一個mm_struct。

 

簡單梳理一下,如有錯誤,還望各位大佬評論指正~

 

 

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