主頁 > 作業系統 > 【原創】Linux虛擬化KVM-Qemu分析(四)之CPU虛擬化(2)

【原創】Linux虛擬化KVM-Qemu分析(四)之CPU虛擬化(2)

2020-10-11 14:22:08 作業系統

背景

  • 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. 概述

  • 本文圍繞ARMv8 CPU的虛擬化展開;
  • 本文會結合Qemu + KVM的代碼分析,捋清楚上層到底層的脈絡;
  • 本文會提供一個Sample Code,用于類比Qemu和KVM的關系,總而言之,大同小異,大題小做,大道至簡,大功告成,大恩不言謝;

先來兩段前戲,

1.1 CPU作業原理

AI的世界,程式的執行不再冰冷,CPU對a.out說,hello啊,world已經ok啦,下來return吧!

既然要說CPU的虛擬化,那就先簡要介紹一下CPU的作業原理:

  • CPU的根本任務是執行指令,我們常說的取指-譯碼-執行-訪存-寫回,就是典型的指令Pipeline操作;
  • 從CPU的功能出發,可以簡要分成三個邏輯模塊:
    1. Control Unit:CPU的指揮中心,協調資料的移動;
    2. ALU:運算單元,執行CPU內部所有的計算;
    3. Register:暫存器和Cache,都算是CPU內部的存盤單元,其中暫存器可用于存盤需要被譯碼和執行的指令、資料、地址等;
  • CPU從記憶體中讀取指令進行譯碼并執行,執行的程序中需要去訪問記憶體中的資料,CPU內部的暫存器可以暫存中間的指令和資料等資訊,通常說的CPU的context指的就是CPU暫存器值;

在硬體支持虛擬化之前,Qemu純軟體虛擬化方案,是通過tcg(tiny code generator)的方式來進行指令翻譯,翻譯成Host處理器架構的指令來執行,硬體虛擬化技術,是讓虛擬機能直接執行在Host CPU上,讓Host CPU直接來執行虛擬機,結合CPU的實際作業原理,應該怎么來理解呢?來張圖:

  • CPU通過pc暫存器獲取下一條執行指令,進行取指譯碼執行等操作,因此給定CPU一個Context,自然就能控制其執行某些代碼;
  • CPU的虛擬化,最終目標讓虛擬機執行在CPU上,無非也是要進行CPU的Context切換,控制CPU去執行對應的代碼,下文會進一步闡述;

既然都講CPU了,那就捎帶介紹下ARMv8的暫存器吧:

  1. 通用暫存器:

  • 圖中描述的是EL3以下,AArch32AArch64暫存器對應關系;
  • AArch64中,總共31個通用暫存器,64bit的為X0-X30,32bit的為W0-W30;
  1. 特殊用途暫存器:

  • 這些特殊用途的暫存器,主要分為三種:1)存放例外回傳地址的ELR_ELx;2)各個EL的堆疊指標SP_ELx;3)CPU的狀態相關暫存器;
  1. CPU的狀態PSTATE

  • CPU的狀態在AArch32時是通過CPSR來獲取,在AArch64中,使用PSTATEPSTATE不是一個暫存器,它表示的是保存當前CPU狀態資訊的一組暫存器或一些標志資訊的統稱;

好了,ARMv8的介紹該打住了,否則要跑偏了,,,

1.2 guest模式

  • Linux系統有兩種執行模式:kernel模式與user模式,為了支持虛擬化功能的CPU,KVM向Linux內核提供了guest模式,用于執行虛擬機系統非I/O的代碼;
  • user模式,對應的是用戶態執行,Qemu程式就執行在user模式下,并回圈監聽是否有I/O需要模擬處理;
  • kernel模式,運行kvm模塊代碼,負責將CPU切換到VM的執行,其中包含了背景關系的load/restore;
  • guest模式,本地運行VM的非I/O代碼,在某些例外情況下會退出該模式,Host OS開始接管;

好了啦,前戲結束,開始直奔主題吧,

2. 流程分析

不管你說啥,我上來就是一句中國萬歲,對不起,跑題了,我上來就是一張Qemu初始化流程圖:

  • 看過Qemu源代碼的人可能都有種感覺,一開始看好像摸不到門框,這圖簡要畫了下關鍵模塊的流程;
  • Qemu的源代碼,后續的文章會詳細介紹,本文只focus在vcpu相關部分;

除了找到了qemu_init_vcpu的入口,這張圖好像跟本文的vcpu的虛擬化關系不是很大,不管了,就算是給后續的Qemu分析打個廣告吧,

2.1 vcpu的創建

2.1.1 qemu中vcpu創建

  • Qemu初始化流程圖中,找到了qemu_init_vcpu的入口,順著這個qemu_init_vcpu就能找到與底層KVM模塊互動的程序;
  • Qemu中為每個vcpu創建了一個執行緒,操作設備節點來創建和初始化vcpu;

所以,接力棒甩到了KVM內核模塊,

2.1.2 kvm中vcpu創建

來一張前文的圖:

  • 前文中分析過,系統在初始化的時候會注冊字符設備驅動,設定好了各類操作函式集,等待用戶層的ioctl來進行控制;
  • Qemu中設定KVM_CREATE_VCPU,將觸發kvm_vm_ioctl_create_vcpu的執行,完成vcpu的創建作業;

  • 在底層中進行vcpu的創建作業,主要是分配一個kvm_vcpu結構,并且對該結構中的欄位進行初始化;
  • 其中有一個用于與應用層進行通信的資料結構struct kvm_run,分配一頁記憶體,應用層會呼叫mmap來進行映射,并且會從該結構中獲取到虛擬機的退出原因;
  • kvm_arch_vcpu_create主要完成體系架構相關的初始化,包括timer,pmu,vgic等;
  • create_hyp_mappingskvm_vcpu結構體建立映射,以便在Hypervisor模式下能訪問該結構;
  • create_vcpu_fd注冊了kvm_vcpu_fops操作函式集,針對vcpu進行操作,Qemu中設定KVM_ARM_VCPU_INIT,將觸發kvm_arch_vcpu_ioctl_vcpu_init的執行,完成的作業主要是vcpu的核心暫存器,系統暫存器等的reset操作,此外還包含了上層設定下來的值,放置在struct kvm_vcpu_init中;

2.2 vcpu的執行

2.2.1 qemu中vcpu的執行

  • Qemu中為每一個vcpu創建一個用戶執行緒,完成了vcpu的初始化后,便進入了vcpu的運行,而這是通過kvm_cpu_exec函式來完成的;
  • kvm_cpu_exec函式中,呼叫kvm_vcpu_ioctl(,KVM_RUN,)來讓底層的物理CPU進行運行,并且監測VM的退出,而這個退出原因就是存在放在kvm_run->exit_reason中,也就是上文中提到過的應用層與底層互動的機制;

2.2.2 kvm中vcpu的執行

用戶層通過KVM_RUN命令,將觸發KVM模塊中kvm_arch_vcpu_ioctl_run函式的執行:

  • vcpu最終是要放置在物理CPU上執行的,很顯然,我們需要進行context的切換:保存好Host的Context,并切換到Guest的Context去執行,最終在退出時再恢復回Host的Context;
  • __guest_enter函式完成最終的context切換,進入Guest的執行,當Guest退出時,fixup_guest_exit將會處理exit_code,判斷是否繼續回傳Guest執行;
  • 當最終Guest退出到Host時,Host呼叫handle_exit來處理例外退出,根據kvm_get_exit_handler去查詢例外處理函式表對應的處理函式,最終進行執行處理;

3. Sample Code

  • 上文已經將Qemu+KVM的CPU的虛擬化大概的輪廓已經介紹了,方方面面,問題不大;
  • 來一段Sample Code類比Qemu和KVM的關系,在Ubuntu16.04系統上進行測驗;

簡要介紹一下:

  1. tiny_kernel.S,相當于Qemu中運行的Guest OS,完成的功能很簡單,沒錯,就是Hello, world列印;
  2. tiny_qemu.c,相當于Qemu,用于加載Guest到vCPU上運行,最終通過kvm放到物理CPU上運行;

魯迅在1921年的時候,說過這么一句話:Talk is cheap, show me the code

  • tiny_kernel.S
start:
/* Hello */
mov     $0x48, %al
outb    %al, $0xf1
mov     $0x65, %al
outb    %al, $0xf1
mov     $0x6c, %al
outb    %al, $0xf1
mov     $0x6c, %al
outb    %al, $0xf1
mov     $0x6f, %al
outb    %al, $0xf1
mov     $0x2c, %al
outb    %al, $0xf1

/* world */
mov     $0x77, %al
outb    %al, $0xf1
mov     $0x6f, %al
outb    %al, $0xf1
mov     $0x72, %al
outb    %al, $0xf1
mov     $0x6c, %al
outb    %al, $0xf1
mov     $0x64, %al
outb    %al, $0xf1

mov     $0x0a, %al
outb    %al, $0xf1

hlt
  • tiny_qemu.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/kvm.h>
#include <sys/mman.h>

#define KVM_DEV     "/dev/kvm"
#define TINY_KERNEL_FILE    "./tiny_kernel.bin"
#define PAGE_SIZE  0x1000

int main(void)
{
    int kvm_fd;
    int vm_fd;
    int vcpu_fd;
    int tiny_kernel_fd;
    int ret;
    int mmap_size;
    
    struct kvm_sregs sregs;
    struct kvm_regs regs;
    struct kvm_userspace_memory_region mem;
    struct kvm_run *kvm_run;
    void *userspace_addr;

    /* open kvm device */
    kvm_fd = open(KVM_DEV, O_RDWR);
    assert(kvm_fd > 0);

    /* create VM */
    vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0);
    assert(vm_fd >= 0);

    /* create VCPU */
    vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);
    assert(vcpu_fd >= 0);

    /* open tiny_kernel binary file */
    tiny_kernel_fd = open(TINY_KERNEL_FILE, O_RDONLY);
    assert(tiny_kernel_fd > 0);
    /* map 4K into memory */
    userspace_addr = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    assert(userspace_addr > 0);
    /* read tiny_kernel binary into the memory */
    ret = read(tiny_kernel_fd, userspace_addr, PAGE_SIZE);
    assert(ret >= 0);

    /* set user memory region */
    mem.slot = 0;
    mem.flags = 0;
    mem.guest_phys_addr = 0;
    mem.memory_size = PAGE_SIZE;
    mem.userspace_addr = (unsigned long)userspace_addr;
    ret = ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &mem);
    assert(ret >= 0);

    /* get kvm_run */
    mmap_size = ioctl(kvm_fd, KVM_GET_VCPU_MMAP_SIZE, NULL);
    assert(mmap_size >= 0);
    kvm_run = (struct kvm_run *)mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpu_fd, 0);
    assert(kvm_run >= 0);

    /* set cpu registers */
    ret = ioctl(vcpu_fd, KVM_GET_SREGS, &sregs);
    assert(ret >= 0);
    sregs.cs.base = 0;
    sregs.cs.selector = 0;
    ret = ioctl(vcpu_fd, KVM_SET_SREGS, &sregs);
    memset(&regs, 0, sizeof(struct kvm_regs));
    regs.rip = 0;
    ret = ioctl(vcpu_fd, KVM_SET_REGS, &regs);
    assert(ret >= 0);

    /* vcpu run */
    while (1) {
        ret = ioctl(vcpu_fd, KVM_RUN, NULL);
        assert(ret >= 0);

        switch(kvm_run->exit_reason) {
            case KVM_EXIT_HLT:
                printf("----KVM EXIT HLT----\n");
                close(kvm_fd);
                close(tiny_kernel_fd);
                return 0;
            case KVM_EXIT_IO:
                putchar(*(((char *)kvm_run) + kvm_run->io.data_offset));
                break;
            default:
                printf("Unknow exit reason: %d\n", kvm_run->exit_reason);
                break;
        }
    }

    return 0;
}

為了表明我沒有騙人,上一張在Ubuntu16.04的虛擬機上運行的結果圖吧:

草草收工吧,

4. 參考

ARMv8-A Architecture Overview
ARMv8 Techinology Preview
Arm Architecture Reference Manual, Armv8, for Armv8-A architecture profile
 Virtual lockstep for fault tolerance and architectural vulnerability analysis

歡迎關注個人公眾號,不定期分享技術文章:

轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/167643.html

標籤:其他

上一篇:linux服務器間ssh埠轉發

下一篇:WSL2 bug

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • CA和證書

    1、在 CentOS7 中使用 gpg 創建 RSA 非對稱密鑰對 gpg --gen-key #Centos上生成公鑰/密鑰對(存放在家目錄.gnupg/) 2、將 CentOS7 匯出的公鑰,拷貝到 CentOS8 中,在 CentOS8 中使用 CentOS7 的公鑰加密一個檔案 gpg -a ......

    uj5u.com 2020-09-10 00:09:53 more
  • Kubernetes K8S之資源控制器Job和CronJob詳解

    Kubernetes的資源控制器Job和CronJob詳解與示例 ......

    uj5u.com 2020-09-10 00:10:45 more
  • VMware下安裝CentOS

    VMware下安裝CentOS 一、軟硬體準備 1 Centos鏡像準備 1.1 CentOS鏡像下載地址 下載地址 1.2 CentOS鏡像下載程序 點擊下載地址進入如下圖的網站,選擇需要下載的版本,這里選擇的是Centos8,點擊如圖所示。 決定選擇Centos8后,選擇想要的鏡像源進行下載,此 ......

    uj5u.com 2020-09-10 00:12:10 more
  • 如何使用Grep命令查找多個字串

    如何使用Grep 命令查找多個字串 大家好,我是良許! 今天向大家介紹一個非常有用的技巧,那就是使用 grep 命令查找多個字串。 簡單介紹一下,grep 命令可以理解為是一個功能強大的命令列工具,可以用它在一個或多個輸入檔案中搜索與正則運算式相匹配的文本,然后再將每個匹配的文本用標準輸出的格式 ......

    uj5u.com 2020-09-10 00:12:28 more
  • git配置http代理

    git配置http代理 經常遇到克隆 github 慢的問題,這里記錄一下幾種配置 git 代理的方法,解決 clone github 過慢。 目錄 git配置代理 git單獨配置github代理 git配置全域代理 配置終端環境變數 git配置代理 主要使用 git config 命令 git單獨 ......

    uj5u.com 2020-09-10 00:12:33 more
  • Linux npm install 裝包時提示Error EACCES permission denied解

    npm install 裝包時提示Error EACCES permission denied解決辦法 ......

    uj5u.com 2020-09-10 00:12:53 more
  • Centos 7下安裝nginx,使用yum install nginx,提示沒有可用的軟體包

    Centos 7下安裝nginx,使用yum install nginx,提示沒有可用的軟體包。 18 (flaskApi) [root@67 flaskDemo]# yum -y install nginx 19 已加載插件:fastestmirror, langpacks 20 Loading ......

    uj5u.com 2020-09-10 00:13:13 more
  • Linux查看服務器暴力破解ssh IP

    在公網的服務器上經常遇到別人爆破你服務器的22埠,用來挖礦或者干其他嘿嘿嘿的事情~ 這種情況下正確的做法是: 修改默認ssh的22埠 使用設定密鑰登錄或者白名單ip登錄 建議服務器密碼為復雜密碼 創建普通用戶登錄服務器(root權限過大) 建立堡壘機,實作統一管理服務器 統計爆破IP [root ......

    uj5u.com 2020-09-10 00:13:17 more
  • CentOS 7系統常見快捷鍵操作方式

    Linux系統中一些常見的快捷方式,可有效提高操作效率,在某些時刻也能避免操作失誤帶來的問題。 ......

    uj5u.com 2020-09-10 00:13:31 more
  • CentOS 7作業系統目錄結構介紹

    作業系統存在著大量的資料檔案資訊,相應檔案資訊會存在于系統相應目錄中,為了更好的管理資料資訊,會將系統進行一些目錄規劃,不同目錄存放不同的資源。 ......

    uj5u.com 2020-09-10 00:13:35 more
最新发布
  • vim的常用命令

    Vim的6種基本模式 1. 普通模式在普通模式中,用的編輯器命令,比如移動游標,洗掉文本等等。這也是Vim啟動后的默認模式。這正好和許多新用戶期待的操作方式相反(大多數編輯器默認模式為插入模式)。 2. 插入模式在這個模式中,大多數按鍵都會向文本緩沖中插入文本。大多數新用戶希望文本編輯器編輯程序中一 ......

    uj5u.com 2023-04-20 08:43:21 more
  • vim的常用命令

    Vim的6種基本模式 1. 普通模式在普通模式中,用的編輯器命令,比如移動游標,洗掉文本等等。這也是Vim啟動后的默認模式。這正好和許多新用戶期待的操作方式相反(大多數編輯器默認模式為插入模式)。 2. 插入模式在這個模式中,大多數按鍵都會向文本緩沖中插入文本。大多數新用戶希望文本編輯器編輯程序中一 ......

    uj5u.com 2023-04-20 08:42:36 more
  • docker學習

    ###Docker概述 真實專案部署環境可能非常復雜,傳統發布專案一個只需要一個jar包,運行環境需要單獨部署。而通過Docker可將jar包和相關環境(如jdk,redis,Hadoop...)等打包到docker鏡像里,將鏡像發布到Docker倉庫,部署時下載發布的鏡像,直接運行發布的鏡像即可。 ......

    uj5u.com 2023-04-19 09:26:53 more
  • 設定Windows主機的瀏覽器為wls2的默認瀏覽器

    這里以Chrome為例。 1. 準備作業 wsl是可以使用Windows主機上安裝的exe程式,出于安全考慮,默認情況下改功能是無法使用。要使用的話,終端需要以管理員權限啟動。 我這里以Windows Terminal為例,介紹如何默認使用管理員權限打開終端,具體操作如下圖所示: 2. 操作 wsl ......

    uj5u.com 2023-04-19 09:25:49 more
  • docker學習

    ###Docker概述 真實專案部署環境可能非常復雜,傳統發布專案一個只需要一個jar包,運行環境需要單獨部署。而通過Docker可將jar包和相關環境(如jdk,redis,Hadoop...)等打包到docker鏡像里,將鏡像發布到Docker倉庫,部署時下載發布的鏡像,直接運行發布的鏡像即可。 ......

    uj5u.com 2023-04-19 09:19:04 more
  • Linux學習筆記

    IP地址和主機名 IP地址 ifconfig可以用來查詢本機的IP地址,如果不能使用,可以通過install net-tools安裝。 Centos系統下ens33表示主網卡;inet后表示IP地址;lo表示本地回環網卡; 127.0.0.1表示代指本機;0.0.0.0可以用于代指本機,同時在放行設 ......

    uj5u.com 2023-04-18 06:52:01 more
  • 解決linux系統的kdump服務無法啟動的問題

    問題:專案麒麟系統服務器的kdump服務無法啟動,沒有相關日志無法定位問題。 1、查看服務狀態是關閉的,重啟系統也無法啟動 systemctl status kdump 2、修改grub引數,修改“crashkernel”為“512M(有的機器數值太大太小都會導致報錯,建議從128M開始試,或者加個 ......

    uj5u.com 2023-04-12 09:59:50 more
  • 解決linux系統的kdump服務無法啟動的問題

    問題:專案麒麟系統服務器的kdump服務無法啟動,沒有相關日志無法定位問題。 1、查看服務狀態是關閉的,重啟系統也無法啟動 systemctl status kdump 2、修改grub引數,修改“crashkernel”為“512M(有的機器數值太大太小都會導致報錯,建議從128M開始試,或者加個 ......

    uj5u.com 2023-04-12 09:59:01 more
  • 你是不是暴露了?

    作者:袁首京 原創文章,轉載時請保留此宣告,并給出原文連接。 如果您是計算機相關從業人員,那么應該經歷不止一次網路安全專項檢查了,你肯定是收到過資訊系統技術檢測報告,要求你加強風險監測,確保你提供的系統服務堅實可靠了。 沒檢測到問題還好,檢測到問題的話,有些處理起來還是挺麻煩的,尤其是線上正在運行的 ......

    uj5u.com 2023-04-05 16:52:56 more
  • 細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實作

    1. 前文回顧 在之前的幾篇記憶體管理系列文章中,筆者帶大家從宏觀角度完整地梳理了一遍 Linux 記憶體分配的整個鏈路,本文的主題依然是記憶體分配,這一次我們會從微觀的角度來探秘一下 Linux 內核中用于零散小記憶體塊分配的記憶體池 —— slab 分配器。 在本小節中,筆者還是按照以往的風格先帶大家簡單 ......

    uj5u.com 2023-04-05 16:44:11 more