影子頁表淺析
xen爲了讓內存可以爲不同的虛擬機共享,它在虛擬地址到物理地址之間引入了一層中間地址,從而Guest OS看到的是這層中間地址,而不是機器的實際地址,因此Guest OS感覺自己的物理地址是從0開始的,“連續”的地址,然而xen將這層中間地址真正的映射到機器地址卻可以是不連續的,這樣就保證了所有的物理內存可以被任意分配給不同的Guest OS。其關係圖如下:
爲了區分這層中間地址,我們將這層中間地址稱爲物理地址,而機器的實際地址(即沒有虛擬化時的物理地址)稱爲機器地址。
爲了實現物理地址和機器地址的轉換,xen用了兩種頁表:P2M和M2P. P2M是物理地址到機器地址的映射,由於每個Guest OS的物理到機器地址的映射不一樣,因此每個Guest OS都有一個P2M表。M2P表是機器地址到物理地址的映射,它只有一個,由xen來維護。
在全虛擬化中,xen採用了影子頁表機制來實現虛擬地址到機器地址的轉換,如下圖:
傳統的操作系統進行地址轉換時,首先通過通過分段機制將邏輯地址轉爲線性地址,然後再通過頁表機制轉爲機器地址。這裏主要討論頁表機制。在linux中,對於一般的不支持PAE和PSE的32位系統,主要採用了兩級頁表機制,頁目錄表PGD和頁表PT,在這裏我們又稱爲l2級頁表和l1級頁表,如果有更高級別的頁表,比如四級頁表,那麼最高的那層稱爲l4級頁表,剩下的分別爲l3級,l2級和l1級。對於一個32位的線性地址,操作系統通過CR3寄存器找到PGD所在機器頁,然後加上線性地址的最高10位地址,就可以找到PT所在的機器頁,然後再加上線性地址的中間10位地址,就可以獲得線性地址的所對應的機器頁,然後再加上線性地址的最後12位就可以獲得該線性地址最後對應的機器地址。
當操作系統被虛擬化,並採用了影子頁表機制後,Guest OS仍通過上述機制進行尋址,即虛擬機監控器那層對Guest OS是透明的。但是它通過CR3查找PGD時,存儲在CR3寄存器中的值並不是Guest OS所認爲的CR3的值,而是指向影子頁表的Host CR3值。這樣客戶機的頁表維護的是線性地址到物理地址的轉換;而影子頁表維護的是線性地址到相應的機器地址的轉換。在進行地址轉換時,真正使用的是影子頁表。
每一級別客戶頁表中都有相應級別的影子頁表與之對應。比如如果Guest OS有四級頁表l1-l4,那麼影子頁表也有四級l1’-l4’。每一頁客戶機頁表中都有規定數目中的頁表表項,每一個頁表表項中都包含了一個物理地址,該物理地址指向下一級的頁表地址或者指向線性地址對應的最後物理地址,在與該客戶機頁表所對應的影子頁表中,也有相應數目的影子頁表表項,每個頁表表項中包含了一個機器地址。該機器地址指向下一級影子頁表或者指向線性地址對應的最後機器地址。那麼客戶機的每一級頁表怎麼和影子頁表的每一級頁表對應起來呢?有兩種對應關係:一種通過哈希函數來得到,一種通過P2M表來轉換。其中P2M只針對最低級別的頁表,即l1,其他級別的都通過哈希函數來完成。該哈希函數輸入一個Guest OS的頁幀號,以及該頁的類型,就可以獲得相應的影子頁表。對於上圖就是hash(mfn(A), typeof(A)) = mfn(D), hash(mfn(B),typeof(B)) = mfn(E),P2M(mfn(C)) = mfn(F), 其中mfn代表一個頁框的頁幀號。
當利用xm create命令來創建一個domain時,會調用domain_create函數,該函數調用arch_domain_create函數,來根據不同的硬件構架來創建相應的domain。
對於hvm,arch_domain_create會首先調用hvm_domain_initialise,來初始化一個domain所需要的資源。然後調用paging_enable來初始化頁表。該函數會根據CPU是否支持EPT技術來調用不同的函數:
如果支持則調用hap_enabled,否則就調用shdow_enabled函數【1】
int paging_enable(struct domain *d, u32 mode)
{
if ( hap_enabled(d) )
returnhap_enable(d, mode | PG_HAP_enable);
else
returnshadow_enable(d, mode | PG_SH_enable);
}
對於影子頁表,我們關注shadow_enable函數的實現。在shadow_enable函數中首先調用sh_set_allocation來爲影子頁表分配內存頁。實現方式主要是調用alloc_domheap_pages函數。shadow_enable 然後調用p2m_alloc_table來初始化P2M表。最後用函數shadow_hash_alloc來初始化哈希表。
通過分析,可以得出在初始化時所有的影子頁表項都是空。即並沒有建立客戶機的頁表到影子頁表的映射。
當Guest OS 啓動分頁時,影子頁表還是空的。隨着客戶機的運行,xen就會監控Guest OS對內存訪問的操作,一旦發現對應的Guest OS的影子頁表不存在,就會分配一個新的物理頁,用做影子頁表,並利用GuestOS頁表的信息和哈希表或者P2M表來填充影子頁表項的內容。
創建影子頁表的幾個函數如下:
shadow_get_and_create_l1e
shadow_get_and_create_l2e
shadow_get_and_create_l3e
shadow_get_and_create_l4e
對於一個虛擬地址,返回該虛擬地址對應的影子頁表的1,2,3,4級的機器頁和對應的影子頁表項指針。
由於它們的實現原理一樣,這裏介紹shadow_get_and_create_l1e,其他的類似:
首先利用shadow_get_and_create_l2e獲得2級的機器頁和指向1級頁表的指針,如果該頁表項存在,則可以通過該頁表項可以獲得1級頁表的機器頁;如果不存在,則首先利用get_fl1_shadow_status函數通過哈希表來查找該1級別的頁表,如果存在就直接獲得,如果不存在就首先利用sh_make_shadow來創建一個影子頁表,並利用函數l2e_propagate_from_guest根據該創建的影子頁表的機器頁幀號,以及訪問權限創建一個l2頁表項,並利用函數shadow_set_l2e,使其指向該頁。
其中l2e_propagate_from_guest由_sh_propagate實現的_sh_propagate 函數 從客戶機的頁表計算出影子頁表相應的頁表項。它的原型如下:
_sh_propagate(struct vcpu *v,
guest_intpte_t guest_intpte,
mfn_ttarget_mfn,
void *shadow_entry_ptr,
int level,
fetch_type_t ft,
p2m_type_tp2mt)
主要根據客戶機的頁表項guest_intpte來獲取頁表項的訪問權限,比如讀寫權限等,並和機器頁號target_mfn結合,形成影子頁表項。放到shadow_entry_ptr中.
我們一般不直接用_sh_propagate函數,而是用它的封裝函數
l1e_propagate_from_guest
l2e_propagate_from_guest
l3e_propagate_from_guest
l4e_propagate_from_guest
分別構造1,2,3,4級的目錄頁表項。
Xen具體更新影子頁表的方法主要有兩種。第一種是out-of-sync【2】。步驟是:
1. 客戶機修改自己的頁表,發生頁保護異常,陷入到Xen。
2. Xen將客戶機的頁表設置成可寫的,並將影子頁表中對應項所在的頁表中的所有頁表項設置成out-of-sync,即設置成頁不存在。
3. 返回至客戶機中,客戶機重新執行更新頁表操作,更新成功。首先,這時客戶機的頁表和影子頁表中的項已經不同步了,但是隻要這項在使用時,被同步就可以。其次,由於這時要修改的頁表項所在的整個頁表被設置成可寫,所以這時修改此頁表中的其他頁表項,仍然可以正常進行。所以,可能這時整個頁表中的所有頁表項都會不同步。
4. 當需要使用此頁表項進行地址轉換時,由於是使用影子頁表進行實際的地址轉換,而影子頁表中此項被設置成不存在,所以會發生缺頁異常,陷入到Xen。
5. Xen將客戶機中的頁表重新設置成只讀。並按照客戶機中的頁表,更新影子頁表,即將不同步的頁表項,按照客戶機中的物理地址,在影子頁表中更新成機器地址。此時客戶機中的頁表和影子頁表同步。返回至客戶機。
影子頁表的第二種管理方法是emulated write。步驟是:
1. 客戶機修改自己的頁表,發生頁保護異常,陷入到Xen。
2. Xen直接解析客戶機對頁表的更新操作,替客戶機完成更新頁表的操作,並同步更新影子頁表。然後返回客戶機。
Xen涉及到影子頁表的操作主要在下面三種情況:【3】
1. 缺頁中斷
2. 更新CR3
3. INVLPG指令仿真
缺頁中斷:
當xen截取到缺頁異常後,第一步要查找客戶機的頁表以確定指向發生缺頁異常的線性地址對應的物理地址所在的頁表項,該頁表項的[0:11]位表明了該物理地址所對應的頁的被訪問權限,監控程序再根據此權限對照缺頁異常產生的錯誤碼,以確定該缺頁異常是客戶機本身的缺頁異常,還是由於影子頁表與客戶機頁表不一致而產生的錯誤。後面這種錯誤稱之爲影子錯誤。
對於客戶機本身的缺頁異常,xen不作任何處理就直接返回給客戶機。客戶機會解決該缺頁異常。而對於影子錯誤,xen會根據出錯的客戶機頁表項的內容來生成或更新相應的影
子頁表項。
當更新一個影子頁表項時,入口函數爲:
static int sh_page_fault(struct vcpu *v,
unsigned long va,
struct cpu_user_regs *regs),其中va對應客戶機的虛擬地址。
流程如下:
首先要找出該虛擬地址對應的影子頁表的機器頁和偏移,即找出該頁表項所在的影子頁表的位置,然後構建該頁表項,將其填到該頁表項中。核心函數如下:
ptr_sl1e = shadow_get_and_create_l1e(v, &gw, &sl1mfn,ft);
l1e_propagate_from_guest(v, gw.l1e, gmfn, &sl1e, ft, p2mt);
r = shadow_set_l1e(v,ptr_sl1e, sl1e, sl1mfn);
更新CR3:
當進程切換時,就會更新當期的CR3的值,這樣系統的使用的頁表也會發生改變。即不同的進程切換後,頁表也會相應的切換,因此影子頁表也要做相應的切換。一般採取的方法是刪掉所有的影子頁表,使之爲空,然後根據進程的執行,再建立相應的影子頁表。但這樣在性能上開銷比較大,xen採取一些優化措施,仍然允許使用老的影子頁表,但會讓影子頁表與客戶機頁表保持同步。
INVLPG指令仿真:
INVLPG 指令是用來刷新TLB 中的表項。該指令以線性地址爲參數,作用是刷新該線性地址在TLB 中的物理地址,使之無效。這樣當處理器再訪問該線性地址時,就必須遍歷頁表,並重新填充TLB。該指令通常用於修改單一的頁表表項。Xen來截獲客戶機對INVLPG 指令的執行。 當執行這條指令時,影子頁表中相應的頁表項設爲,這樣在GuestOS再次訪問該線性地址時,就會發生不存在的影子錯誤。監控程序會截獲這個影子錯誤,然後會執行上節
中對缺頁異常的處理,重新使影子頁表與當前的客戶機頁表同步。