【原創】Linux虛擬化KVM-Qemu分析(三)之KVM原始碼(1)

LoyenWang發表於2020-09-12

背景

  • Read the fucking source code! --By 魯迅
  • A picture is worth a thousand words. --By 高爾基

說明:

  1. KVM版本:5.9.1
  2. QEMU版本:5.0.0
  3. 工具:Source Insight 3.5, Visio
  4. 文章同步在部落格園:https://www.cnblogs.com/LoyenWang/

1. 概述

  • 從本文開始將開始source code的系列分析了;
  • KVM作為核心模組,可以認為是一箇中間層,向上對接使用者的控制,向下對接不同架構的硬體虛擬化支援;
  • 本文主要介紹體系架構初始化部分,以及向上的框架;

2. KVM初始化

  • 貝多芬曾經說過,一旦你找到了程式碼的入口,你就扼住了軟體的咽喉;
  • 我們的故事,從module_init(arm_init)開始,程式碼路徑:arch/arm64/kvm/arm.c

老規矩,先來一張圖(圖片中涉及到的紅色框函式,都是會展開描述的):

  • 核心的功能模組,基本上的套路就是:1)完成模組初始化,向系統註冊;2)響應各類請求,這種請求可能來自使用者態,也可能來自異常響應等;
  • kvm的初始化,在kvm_init中完成,既包含了體系結構相關的初始化設定,也包含了各類回撥函式的設定,資源分配,以及裝置註冊等,只有當初始化完成後,才能響應各類請求,比如建立虛擬機器等;
    1. 回撥函式設定:cpuhp_setup_state_nocall與CPU的熱插拔相關,register_reboot_notifer與系統的重啟相關,register_syscore_ops與系統的休眠喚醒相關,而這幾個模組的回撥函式,最終都會去呼叫體系結構相關的函式去開啟或關閉Hypervisor
    2. 資源分配:kmem_cache_create_usercopykvm_async_pf_init都是建立slab快取,用於核心物件的分配;
    3. kvm_vfio_ops_initVFIO是一個可以安全將裝置I/O、中斷、DMA匯出到使用者空間的框架,後續在將IO虛擬化時再深入分析;
  • 圖片中紅色的兩個函式,是本文分析的內容,其中kvm_arch_init與前文ARMv8硬體虛擬化支援緊密相關,而misc_register與上層操作緊密相關;

2.1 kvm_arch_init

  • It's a big topic, I'll try to put it in a nutshell.
  • 這部分內容,設計ARMv8體系結構,建議先閱讀《Linux虛擬化KVM-Qemu分析(二)之ARMv8虛擬化》
  • 紅色框的函式是需要進一步展開講述的;

  • is_hyp_mode_available用於判斷ARMv8的Hyp模式是否可用,實際是通過判斷__boot_cpu_mode的值來完成,該值是在arch/arm64/kernel/head.S中定義,在啟動階段會設定該值:

  • is_kernel_in_hyp_mode,通過讀取ARMv8的CurrentEL,判斷是否為CurrentEL_EL2
  • ARM架構中,SVE的實現要求VHE也要實現,這個可以從arch/arm64/Kconfig中看到,SVE的模組編譯:depends on !KVM || ARM64_VHESVE(scalable vector extension),是AArch64下一代的SIMD(single instruction multiple data)指令集,用於加速高效能運算。其中SIMD如下:

  • init_common_resources,用於設定IPA的地址範圍,將其限制在系統能支援的實體地址範圍之內。stage 2頁表依賴於stage 1頁表程式碼,需要遵循一個條件:Stage 1的頁表級數 >= Stage 2的頁表級數;

2.1.1 init_hyp_mode

  • 放眼望去,init_hyp_mode解決的問題就是各種對映,最終都會呼叫到__create_hyp_mappings,先來解決這個對映問題:

  • 看過之前記憶體管理子系統的同學,應該熟悉這個頁表對映建立的過程,基本的流程是給定一個虛擬地址區間和實體地址,然後從pgd開始逐級往下去建立對映。ARMv8架構在實際對映過程中,P4D這一級頁表並沒有使用。

讓我們繼續回到init_hyp_mode的正題上來,這個函式完成了PGD頁表的分配,完成了IDMAP程式碼段的對映,完成了其他各種段的對映,完成了異常向量表的對映,等等。此外,再補充幾點內容:

  1. ARMv8異常向量表

  • ARMv8架構的AArch64執行態中,每種EL都有16個entry,分為四類:Synchronous,IRQ,FIQ,SError。以系統啟動時設定hypervisor的異常向量表__hyp_stub_vectors為例:

  • 當從不同的Exception Level觸發異常時,根據執行狀態,去選擇對應的handler處理,比如上圖中只有el1_sync有效,也就是在EL1狀態觸發EL2時跳轉到該函式;
  1. pushsection/popsection
  • init_hyp_mode函式中,完成各種段的對映,段的定義放置在vmlinux.lds.S中,比如hyp.idmap.text

  • 可以通過pushsection/popsection來在目標檔案中來新增一個段,並指定段的屬性,比如"ax"代表可分配和可執行,這個在彙編程式碼中經常用到,比如hyp-init.S中,會將程式碼都放置在hyp.idmap.text中:

  • 除了pushsection/popsection外,通過#define __hyp_text __section(.hyp.text) notrace __noscs的形式也能將程式碼放置在指定的段中;
  1. Hypervisor相關暫存器
  • 講幾個關鍵的相關暫存器:
    1)sctlr_el2(System Control Register):可以用於控制EL2的MMU和Cache相關操作;
    2)ttbr0_el2(Translation Table Base Register 0):用於存放頁表的基地址,上文中提到分配的hyp_pgd就需要設定到該暫存器中;
    3)vbar_el2(Vector Base Address Register):用於存放異常向量表的基地址;

我們需要先明確幾點:

  1. Hyp模式下要執行的程式碼,需要先建立起對映;
  2. 對映IDMAP程式碼段和其他程式碼段,明確這些段中都有哪些函式,這個可以通過pushsection/popsection以及__hyp_text巨集可以看出來;
  3. 最終的目標是需要建立好頁表對映,並安裝好異常向量表;

貌似內容比較零碎,最終的串聯與謎題留在下一小節來解答。

2.1.2 init_subsystems

先看一下函式的呼叫流程:

  • VGICtimer,以及電源管理相關模組在本文中暫且不深入分析了,本節主要關心cpu_hyp_reinit的功能;
  • 綠色框中的函式,會陷入到EL2進行執行;

看圖中有好幾次異常向量表的設定,此外,還有頁表基地址、棧頁的獲取與設定等,結合上一小節的各類對映,是不是已經有點迷糊了,下邊這張圖會將這些內容串聯起來:

  • 在整個異常向量表建立的過程中,涉及到三個向量表:__hyp_stub_vectors__kvm_hyp_init__kvm_call_hyp,這些程式碼都是彙編實現;
  • 在系統啟動過程中(arch/arm64/kernel/head.S),呼叫到el2_setup函式,在該函式中設定了一個臨時的異常向量表,也就是先打一個樁,這個從名字也可以看出來,該異常向量表中僅實現了el2_synchandler處理函式,可以應對兩種異常:1)設定新的異常向量表;2)重置異常向量表,也就是設定回__hyp_stub_vectors
  • kvm初始化時,呼叫了__hyp_set_vectors來設定新的異常向量表:__kvm_hyp_init。這個向量表中只實現了__do_hyp_init的處理函式,也就是隻能用來對Hyp模式進行初始化。上文提到過idmap段,這個程式碼就放置在idmap段,以前分析記憶體管理子系統時也提到過idmap,為什麼需要這個呢?idmap: identity map,也就是實體地址和虛擬地址是一一對映的,防止MMU在使能前後程式碼不能執行;
  • __kvm_call_hyp函式,用於在Hyp模式下執行指定的函式,在cpu_hyp_reinit函式中呼叫了該函式,傳遞的引數包括了新的異常向量表地址,頁表基地址,Hyp的棧地址,per-CPU偏移等,最終會呼叫__do_hyp_init函式完成相應的設定。

到此,頁表和異常向量表的設定算是完成了。

2.2 misc_register

misc_register用於註冊字元裝置驅動,在kvm_init函式中呼叫此函式完成註冊,以便上層應用程式來使用kvm模組

  • 字元裝置的註冊分為三級,分別代表kvm, vm, vcpu,上層最終使用底層的服務都是通過ioctl函式來操作;
  • kvm:代表kvm核心模組,可以通過kvm_dev_ioctl來管理kvm版本資訊,以及vm的建立等;
  • vm:虛擬機器例項,可以通過kvm_vm_ioctl函式來建立vcpu,設定記憶體區間,分配中斷等;
  • vcpu:代表虛擬的CPU,可以通過kvm_vcpu_ioctl來啟動或暫停CPU的執行,設定vcpu的暫存器等;

Qemu的使用為例:

  1. 開啟/dev/kvm裝置檔案;
  2. ioctl(xx, KVM_CREATE_VM, xx)建立虛擬機器物件;
  3. ioctl(xx, KVM_CREATE_VCPU, xx)為虛擬機器建立vcpu物件;
  4. ioctl(xx, KVM_RUN, xx)讓vcpu執行起來;

3. 總結

本文主要從兩個方向來介紹了kvm_init

  1. 底層的體系結構相關的初始化,主要涉及的就是EL2的相關設定,比如各個段的對映,異常向量表的安裝,頁表基地址的設定等,當把這些準備工作做完後,才能在硬體上去支援虛擬化的服務請求;
  2. 字元裝置註冊,設定好各類ioctl的函式,上層應用程式可以通過字元裝置檔案,來操作底層的kvm模組。這部分內容深入的分析,留到後續的文章再展開了;

實際在看程式碼過程中,一度為很多細節絞盡乳汁,對不起,是絞盡腦汁,每有會意,便欣然忘食,一文也無法覆蓋所有內容,草率了。

歡迎關注個人公眾號,不定期更新技術文章。

相關文章