Linux版本:v6.0
處理器架構:ARMv8
KVM品種:NVHE
前言
在Linux kernel 5.10週期,KVM ARM開發者們為了為google pkvm做準備,在code base許多地方做了翻修,這篇所介紹的EL2 per cpu變數也是其中一項。而因為打算討論的內容有點多,所以預期分成兩個部份:
定義及存取
per cpu變數初始化
除了這些之外還有很多相關的內容,例如barriers, preemption, interrupts等和per cpu變數相關的議題,但我目前並不是很熟悉所以無法多做討論,有興趣的讀者可以自行上網查詢資料。
Per CPU 變數
Per CPU變數簡而言之就是每一個CPU核心各有一份的變數,程式存取per cpu變數時會依照當下的CPU核心去存取對應的變數,不會互相干擾。
Linux kernel的per cpu變數實作機制網路上已經有許多很好的資料可以參考,這篇主要會聚焦在KVM ARM為了EL2環境所量身訂做的per cpu變數實作機制。
實作機制
定義
EL2的per cpu變數API其實是和一般的一樣,先看如何定義per cpu變數:
1 | // file: arch/arm64/kvm/hyp/nvhe/switch.c |
macro展開之後會變成:
1 | __attribute__((section(".data..percpu" ""))) __typeof__(unsigned long) kvm_hyp_vector; |
這個結果也和一般的per cpu變數一樣,難道EL2使用的per cpu變數和EL1 kernel使用的都放在同一個section裡面嗎? 答案是否定的,KVM EL2的檔案會由一個特別的linker script來鏈結,也就是arch/arm64/kvm/hyp/nvhe/hyp.lds
,這個linker script會藉由把EL2的sections都改名,來把kernel和hypervisor的sections都分開,以下是用defconfig
編譯產生的hyp.lds
hyp.lds
不會在源碼中出現,因為hyp.lds
是從同個directory的hyp.lds.S
在編譯時預處理所產生的
觀察一下hyp.lds
:
1 | SECTIONS { |
可以看到上面例子的kvm_hyp_vector
原本在.data..percpu
section裡,用hyp.lds
鏈結之後就會變到.hyp.data..percpu
了
存取
接著看一個存取的例子:
1 | // file: arch/arm64/kvm/hyp/nvhe/switch.c |
this_cpu_ptr(&var)
會回傳屬於當前cpu那份叫做var的per cpu變數的位址
這行程式也使用了非常多macro,展開之後如下(不用仔細看):
1 | do { |
其中write_sysreg()
和hcr_el2
的部份是目前不關心的,刪減之後剩下:
1 | // macro expansion of this_cpu_ptr(&kvm_init_params): |
從以上分析可以看出per cpu變數的操作基本上就是去拿一個base pointer,然後加上一個隨cpu變化的offset,去拿到屬於此cpu的位址。
__hyp_my_cpu_offset()
的實作如下:
1 | static inline unsigned long __hyp_my_cpu_offset(void) |
而去取得各個per cpu的offset就是去讀tpidr_el2
這個系統暫存器。tpidr_el2
就是一個專門給軟體使用的暫存器(當然每個CPU核心各有一個),KVM ARM這裡就拿來放per cpu變數的offset。
最後來看一個在組合語言中使用per cpu變數的例子:
1 | // file: arch/arm64/kvm/hyp/nvhe/host.S |
這也是一個macro,效果是把一個在其他地方定義的per cpu struct kvm_host_data kvm_host_data
的位址放到x0
,x1
則是暫時使用的暫存器,macro展開如下:
1 | // 用兩個指令把 kvm_host_data 的位址用pc relative的方式讀到x0 |
作法和C code一樣是使用tpidr_el2
來當作offset,合情合理。