[原] KVM 虛擬化原理探究(2)— QEMU啓動過程

KVM 虛擬化原理探究— QEMU啓動過程

標籤(空格分隔): KVMgit


虛擬機啓動過程

第一步,獲取到kvm句柄
kvmfd = open("/dev/kvm", O_RDWR);
第二步,建立虛擬機,獲取到虛擬機句柄。
vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0);
第三步,爲虛擬機映射內存,還有其餘的PCI,信號處理的初始化。
ioctl(kvmfd, KVM_SET_USER_MEMORY_REGION, &mem);
第四步,將虛擬機鏡像映射到內存,至關於物理機的boot過程,把鏡像映射到內存。
第五步,建立vCPU,併爲vCPU分配內存空間。
ioctl(kvmfd, KVM_CREATE_VCPU, vcpuid);
vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
第五步,建立vCPU個數的線程並運行虛擬機。
ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0);
第六步,線程進入循環,並捕獲虛擬機退出緣由,作相應的處理。
這裏的退出並不必定是虛擬機關機,虛擬機若是遇到IO操做,訪問硬件設備,缺頁中斷等都會退出執行,退出執行能夠理解爲將CPU執行上下文返回到QEMU。
open("/dev/kvm")
ioctl(KVM_CREATE_VM)
ioctl(KVM_CREATE_VCPU)
for (;;) {
     ioctl(KVM_RUN)
     switch (exit_reason) {
     case KVM_EXIT_IO:  /* ... */
     case KVM_EXIT_HLT: /* ... */
     }
}

關於KVM_CREATE_VM參數的描述,建立的VM是沒有cpu和內存的,須要QEMU進程利用mmap系統調用映射一塊內存給VM的描述符,其實也就是給VM建立內存的過程。github

KVM ioctl接口文檔api

先來一個KVM API開胃菜

下面是一個KVM的簡單demo,其目的在於加載 code 並使用KVM運行起來.
這是一個at&t的8086彙編,.code16表示他是一個16位的,固然直接運行是運行不起來的,爲了讓他運行起來,咱們能夠用KVM提供的API,將這個程序看作一個最簡單的操做系統,讓其運行起來。
這個彙編的做用是輸出al寄存器的值到0x3f8端口。對於x86架構來講,經過IN/OUT指令訪問。PC架構一共有65536個8bit的I/O端口,組成64KI/O地址空間,編號從0~0xFFFF。連續兩個8bit的端口能夠組成一個16bit的端口,連續4個組成一個32bit的端口。I/O地址空間和CPU的物理地址空間是兩個不一樣的概念,例如I/O地址空間爲64K,一個32bit的CPU物理地址空間是4G。
最終程序理想的輸出應該是,al,bl的值後面KVM初始化的時候有賦值。
4\n (並不直接輸出\n,而是換了一行),hlt 指令表示虛擬機退出數組

.globl _start
    .code16
_start:
    mov $0x3f8, %dx
    add %bl, %al
    add $'0', %al
    out %al, (%dx)
    mov $'\n', %al
    out %al, (%dx)
    hlt

咱們編譯一下這個彙編,獲得一個 Bin.bin 的二進制文件架構

as -32 bin.S -o bin.o
ld -m elf_i386 --oformat binary -N -e _start -Ttext 0x10000 -o Bin.bin bin.o

查看一下二進制格式函數

➜  demo1 hexdump -C bin.bin
00000000  ba f8 03 00 d8 04 30 ee  b0 0a ee f4              |......0.....|
0000000c
對應了下面的code數組,這樣直接加載字節碼就不須要再從文件加載了
    const uint8_t code[] = {
        0xba, 0xf8, 0x03, /* mov $0x3f8, %dx */
        0x00, 0xd8,       /* add %bl, %al */
        0x04, '0',        /* add $'0', %al */
        0xee,             /* out %al, (%dx) */
        0xb0, '\n',       /* mov $'\n', %al */
        0xee,             /* out %al, (%dx) */
        0xf4,             /* hlt */
    };
#include <err.h>
#include <fcntl.h>
#include <linux/kvm.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>

int main(void)
{
    int kvm, vmfd, vcpufd, ret;
    const uint8_t code[] = {
        0xba, 0xf8, 0x03, /* mov $0x3f8, %dx */
        0x00, 0xd8,       /* add %bl, %al */
        0x04, '0',        /* add $'0', %al */
        0xee,             /* out %al, (%dx) */
        0xb0, '\n',       /* mov $'\n', %al */
        0xee,             /* out %al, (%dx) */
        0xf4,             /* hlt */
    };
    uint8_t *mem;
    struct kvm_sregs sregs;
    size_t mmap_size;
    struct kvm_run *run;
    
    // 獲取 kvm 句柄
    kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);
    if (kvm == -1)
        err(1, "/dev/kvm");

    // 確保是正確的 API 版本
    ret = ioctl(kvm, KVM_GET_API_VERSION, NULL);
    if (ret == -1)
        err(1, "KVM_GET_API_VERSION");
    if (ret != 12)
        errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);
    
    // 建立一虛擬機
    vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long)0);
    if (vmfd == -1)
        err(1, "KVM_CREATE_VM");
    
    // 爲這個虛擬機申請內存,並將代碼(鏡像)加載到虛擬機內存中
    mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (!mem)
        err(1, "allocating guest memory");
    memcpy(mem, code, sizeof(code));

    // 爲何從 0x1000 開始呢,由於頁表空間的前4K是留給頁表目錄
    struct kvm_userspace_memory_region region = {
        .slot = 0,
        .guest_phys_addr = 0x1000,
        .memory_size = 0x1000,
        .userspace_addr = (uint64_t)mem,
    };
    // 設置 KVM 的內存區域
    ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &region);
    if (ret == -1)
        err(1, "KVM_SET_USER_MEMORY_REGION");
    
    // 建立虛擬CPU
    vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);
    if (vcpufd == -1)
        err(1, "KVM_CREATE_VCPU");

    // 獲取 KVM 運行時結構的大小
    ret = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL);
    if (ret == -1)
        err(1, "KVM_GET_VCPU_MMAP_SIZE");
    mmap_size = ret;
    if (mmap_size < sizeof(*run))
        errx(1, "KVM_GET_VCPU_MMAP_SIZE unexpectedly small");
    // 將 kvm run 與 vcpu 作關聯,這樣可以獲取到kvm的運行時信息
    run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
    if (!run)
        err(1, "mmap vcpu");

    // 獲取特殊寄存器
    ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs);
    if (ret == -1)
        err(1, "KVM_GET_SREGS");
    // 設置代碼段爲從地址0處開始,咱們的代碼被加載到了0x0000的起始位置
    sregs.cs.base = 0;
    sregs.cs.selector = 0;
    // KVM_SET_SREGS 設置特殊寄存器
    ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs);
    if (ret == -1)
        err(1, "KVM_SET_SREGS");

    
    // 設置代碼的入口地址,至關於32位main函數的地址,這裏16位彙編都是由0x1000處開始。
    // 若是是正式的鏡像,那麼rip的值應該是相似引導扇區加載進來的指令
    struct kvm_regs regs = {
        .rip = 0x1000,
        .rax = 2,    // 設置 ax 寄存器初始值爲 2
        .rbx = 2,    // 同理
        .rflags = 0x2,   // 初始化flags寄存器,x86架構下須要設置,不然會粗錯
    };
    ret = ioctl(vcpufd, KVM_SET_REGS, &regs);
    if (ret == -1)
        err(1, "KVM_SET_REGS");

    // 開始運行虛擬機,若是是qemu-kvm,會用一個線程來執行這個vCPU,並加載指令
    while (1) {
        // 開始運行虛擬機
        ret = ioctl(vcpufd, KVM_RUN, NULL);
        if (ret == -1)
            err(1, "KVM_RUN");
        // 獲取虛擬機退出緣由
        switch (run->exit_reason) {
        case KVM_EXIT_HLT:
            puts("KVM_EXIT_HLT");
            return 0;
        // 彙編調用了 out 指令,vmx 模式下不容許執行這個操做,因此
        // 將操做權切換到了宿主機,切換的時候會將上下文保存到VMCS寄存器
        // 後面CPU虛擬化會講到這部分
        // 由於虛擬機的內存宿主機可以直接讀取到,因此直接在宿主機上獲取到
        // 虛擬機的輸出(out指令),這也是後面PCI設備虛擬化的一個基礎,DMA模式的PCI設備
        case KVM_EXIT_IO:
            if (run->io.direction == KVM_EXIT_IO_OUT && run->io.size == 1 && run->io.port == 0x3f8 && run->io.count == 1)
                putchar(*(((char *)run) + run->io.data_offset));
            else
                errx(1, "unhandled KVM_EXIT_IO");
            break;
        case KVM_EXIT_FAIL_ENTRY:
            errx(1, "KVM_EXIT_FAIL_ENTRY: hardware_entry_failure_reason = 0x%llx",
                 (unsigned long long)run->fail_entry.hardware_entry_failure_reason);
        case KVM_EXIT_INTERNAL_ERROR:
            errx(1, "KVM_EXIT_INTERNAL_ERROR: suberror = 0x%x", run->internal.suberror);
        default:
            errx(1, "exit_reason = 0x%x", run->exit_reason);
        }
    }
}

編譯並運行這個demooop

gcc -g demo.c -o demo
➜  demo1 ./demo
4
KVM_EXIT_HLT

另一個簡單的QEMU emulator demo

IBM的徐同窗有作過介紹,在此基礎上我再詳細介紹一下qemu-kvm的啓動過程。ui

.globl _start
    .code16
_start:
    xorw %ax, %ax   # 將 ax 寄存器清零

loop1:
    out %ax, $0x10  # 像 0x10 的端口輸出 ax 的內容,at&t彙編的操做數和Intel的相反。
    inc %ax         # ax 值加一
    jmp loop1       # 繼續循環

這個彙編的做用就是一直不停的向0x10端口輸出一字節的值。spa

從main函數開始提及

int main(int argc, char **argv) {
    int ret = 0;
    // 初始化kvm結構體
    struct kvm *kvm = kvm_init();

    if (kvm == NULL) {
        fprintf(stderr, "kvm init fauilt\n");
        return -1;
    }
    
    // 建立VM,並分配內存空間
    if (kvm_create_vm(kvm, RAM_SIZE) < 0) {
        fprintf(stderr, "create vm fault\n");
        return -1;
    }
    
    // 加載鏡像
    load_binary(kvm);

    // only support one vcpu now
    kvm->vcpu_number = 1;
    // 建立執行現場
    kvm->vcpus = kvm_init_vcpu(kvm, 0, kvm_cpu_thread);
    
    // 啓動虛擬機
    kvm_run_vm(kvm);

    kvm_clean_vm(kvm);
    kvm_clean_vcpu(kvm->vcpus);
    kvm_clean(kvm);
}

第一步,調用kvm_init() 初始化了 kvm 結構體。先來看看怎麼定義一個簡單的kvm。

struct kvm {
   int dev_fd;              // /dev/kvm 的句柄
   int vm_fd;               // GUEST 的句柄
   __u64 ram_size;          // GUEST 的內存大小
   __u64 ram_start;         // GUEST 的內存起始地址,
                            // 這個地址是qemu emulator經過mmap映射的地址
   
   int kvm_version;         
   struct kvm_userspace_memory_region mem; // slot 內存結構,由用戶空間填充、
                                           // 容許對guest的地址作分段。將多個slot組成線性地址

   struct vcpu *vcpus;      // vcpu 數組
   int vcpu_number;         // vcpu 個數
};

初始化 kvm 結構體。

struct kvm *kvm_init(void) {
    struct kvm *kvm = malloc(sizeof(struct kvm));
    kvm->dev_fd = open(KVM_DEVICE, O_RDWR);  // 打開 /dev/kvm 獲取 kvm 句柄

    if (kvm->dev_fd < 0) {
        perror("open kvm device fault: ");
        return NULL;
    }

    kvm->kvm_version = ioctl(kvm->dev_fd, KVM_GET_API_VERSION, 0);  // 獲取 kvm API 版本

    return kvm;
}

第二步+第三步,建立虛擬機,獲取到虛擬機句柄,併爲其分配內存。

int kvm_create_vm(struct kvm *kvm, int ram_size) {
    int ret = 0;
    // 調用 KVM_CREATE_KVM 接口獲取 vm 句柄
    kvm->vm_fd = ioctl(kvm->dev_fd, KVM_CREATE_VM, 0);

    if (kvm->vm_fd < 0) {
        perror("can not create vm");
        return -1;
    }

    // 爲 kvm 分配內存。經過系統調用.
    kvm->ram_size = ram_size;
    kvm->ram_start =  (__u64)mmap(NULL, kvm->ram_size, 
                PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, 
                -1, 0);

    if ((void *)kvm->ram_start == MAP_FAILED) {
        perror("can not mmap ram");
        return -1;
    }
    
    // kvm->mem 結構須要初始化後傳遞給 KVM_SET_USER_MEMORY_REGION 接口
    // 只有一個內存槽
    kvm->mem.slot = 0;
    // guest 物理內存起始地址
    kvm->mem.guest_phys_addr = 0;
    // 虛擬機內存大小
    kvm->mem.memory_size = kvm->ram_size;
    // 虛擬機內存在host上的用戶空間地址,這裏就是綁定內存給guest
    kvm->mem.userspace_addr = kvm->ram_start;
    
    // 調用 KVM_SET_USER_MEMORY_REGION 爲虛擬機分配內存。
    ret = ioctl(kvm->vm_fd, KVM_SET_USER_MEMORY_REGION, &(kvm->mem));

    if (ret < 0) {
        perror("can not set user memory region");
        return ret;
    }
    return ret;
}

接下來就是load_binary把二進制文件load到虛擬機的內存中來,在第一個demo中咱們是直接把字節碼放到了內存中,這裏模擬鏡像加載步驟,把二進制文件加載到內存中。

void load_binary(struct kvm *kvm) {
    int fd = open(BINARY_FILE, O_RDONLY);  // 打開這個二進制文件(鏡像)

    if (fd < 0) {
        fprintf(stderr, "can not open binary file\n");
        exit(1);
    }

    int ret = 0;
    char *p = (char *)kvm->ram_start;

    while(1) {
        ret = read(fd, p, 4096);           // 將鏡像內容加載到虛擬機的內存中
        if (ret <= 0) {
            break;
        }
        printf("read size: %d", ret);
        p += ret;
    }
}

加載完鏡像後,須要初始化vCPU,以便可以運行鏡像內容

struct vcpu {
    int vcpu_id;                 // vCPU id,vCPU
    int vcpu_fd;                 // vCPU 句柄
    pthread_t vcpu_thread;       // vCPU 線程句柄
    struct kvm_run *kvm_run;     // KVM 運行時結構,也能夠看作是上下文
    int kvm_run_mmap_size;       // 運行時結構大小
    struct kvm_regs regs;        // vCPU的寄存器
    struct kvm_sregs sregs;      // vCPU的特殊寄存器
    void *(*vcpu_thread_func)(void *);  // 線程執行函數
};

struct vcpu *kvm_init_vcpu(struct kvm *kvm, int vcpu_id, void *(*fn)(void *)) {
    // 申請vcpu結構
    struct vcpu *vcpu = malloc(sizeof(struct vcpu));
    // 只有一個 vCPU,因此這裏只初始化一個
    vcpu->vcpu_id = 0;
    // 調用 KVM_CREATE_VCPU 獲取 vCPU 句柄,並關聯到kvm->vm_fd(由KVM_CREATE_VM返回)
    vcpu->vcpu_fd = ioctl(kvm->vm_fd, KVM_CREATE_VCPU, vcpu->vcpu_id);

    if (vcpu->vcpu_fd < 0) {
        perror("can not create vcpu");
        return NULL;
    }
    
    // 獲取KVM運行時結構大小
    vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);

    if (vcpu->kvm_run_mmap_size < 0) {
        perror("can not get vcpu mmsize");
        return NULL;
    }

    printf("%d\n", vcpu->kvm_run_mmap_size);
    // 將 vcpu_fd 的內存映射給 vcpu->kvm_run結構。至關於一個關聯操做
    // 以便可以在虛擬機退出的時候獲取到vCPU的返回值等信息
    vcpu->kvm_run = mmap(NULL, vcpu->kvm_run_mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpu->vcpu_fd, 0);

    if (vcpu->kvm_run == MAP_FAILED) {
        perror("can not mmap kvm_run");
        return NULL;
    }
    
    // 設置線程執行函數
    vcpu->vcpu_thread_func = fn;
    return vcpu;
}

最後一步,以上工做就緒後,啓動虛擬機。

void kvm_run_vm(struct kvm *kvm) {
    int i = 0;

    for (i = 0; i < kvm->vcpu_number; i++) {
        // 啓動線程執行 vcpu_thread_func 並將 kvm 結構做爲參數傳遞給線程
        if (pthread_create(&(kvm->vcpus->vcpu_thread), (const pthread_attr_t *)NULL, kvm->vcpus[i].vcpu_thread_func, kvm) != 0) {
            perror("can not create kvm thread");
            exit(1);
        }
    }

    pthread_join(kvm->vcpus->vcpu_thread, NULL);
}

啓動虛擬機其實就是建立線程,並執行相應的線程回調函數。
線程回調函數在kvm_init_vcpu的時候傳入

void *kvm_cpu_thread(void *data) {
    // 獲取參數
    struct kvm *kvm = (struct kvm *)data;
    int ret = 0;
    // 設置KVM的參數
    kvm_reset_vcpu(kvm->vcpus);

    while (1) {
        printf("KVM start run\n");
        // 啓動虛擬機,此時的虛擬機已經有內存和CPU了,能夠運行起來了。
        ret = ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0);
    
        if (ret < 0) {
            fprintf(stderr, "KVM_RUN failed\n");
            exit(1);
        }
        
        // 前文 kvm_init_vcpu 函數中,將 kvm_run 關聯了 vCPU 結構的內存
        // 因此這裏虛擬機退出的時候,能夠獲取到 exit_reason,虛擬機退出緣由
        switch (kvm->vcpus->kvm_run->exit_reason) {
        case KVM_EXIT_UNKNOWN:
            printf("KVM_EXIT_UNKNOWN\n");
            break;
        case KVM_EXIT_DEBUG:
            printf("KVM_EXIT_DEBUG\n");
            break;
        // 虛擬機執行了IO操做,虛擬機模式下的CPU會暫停虛擬機並
        // 把執行權交給emulator
        case KVM_EXIT_IO:
            printf("KVM_EXIT_IO\n");
            printf("out port: %d, data: %d\n", 
                kvm->vcpus->kvm_run->io.port,  
                *(int *)((char *)(kvm->vcpus->kvm_run) + kvm->vcpus->kvm_run->io.data_offset)
                );
            sleep(1);
            break;
        // 虛擬機執行了memory map IO操做
        case KVM_EXIT_MMIO:
            printf("KVM_EXIT_MMIO\n");
            break;
        case KVM_EXIT_INTR:
            printf("KVM_EXIT_INTR\n");
            break;
        case KVM_EXIT_SHUTDOWN:
            printf("KVM_EXIT_SHUTDOWN\n");
            goto exit_kvm;
            break;
        default:
            printf("KVM PANIC\n");
            goto exit_kvm;
        }
    }

exit_kvm:
    return 0;
}

void kvm_reset_vcpu (struct vcpu *vcpu) {
    if (ioctl(vcpu->vcpu_fd, KVM_GET_SREGS, &(vcpu->sregs)) < 0) {
        perror("can not get sregs\n");
        exit(1);
    }
    // #define CODE_START 0x1000
    /* sregs 結構體
        x86
        struct kvm_sregs {
            struct kvm_segment cs, ds, es, fs, gs, ss;
            struct kvm_segment tr, ldt;
            struct kvm_dtable gdt, idt;
            __u64 cr0, cr2, cr3, cr4, cr8;
            __u64 efer;
            __u64 apic_base;
            __u64 interrupt_bitmap[(KVM_NR_INTERRUPTS + 63) / 64];
        };
    */
    // cs 爲code start寄存器,存放了程序的起始地址
    vcpu->sregs.cs.selector = CODE_START;
    vcpu->sregs.cs.base = CODE_START * 16;
    // ss 爲堆棧寄存器,存放了堆棧的起始位置
    vcpu->sregs.ss.selector = CODE_START;
    vcpu->sregs.ss.base = CODE_START * 16;
    // ds 爲數據段寄存器,存放了數據開始地址
    vcpu->sregs.ds.selector = CODE_START;
    vcpu->sregs.ds.base = CODE_START *16;
    // es 爲附加段寄存器
    vcpu->sregs.es.selector = CODE_START;
    vcpu->sregs.es.base = CODE_START * 16;
    // fs, gs 一樣爲段寄存器
    vcpu->sregs.fs.selector = CODE_START;
    vcpu->sregs.fs.base = CODE_START * 16;
    vcpu->sregs.gs.selector = CODE_START;
    
    // 爲vCPU設置以上寄存器的值
    if (ioctl(vcpu->vcpu_fd, KVM_SET_SREGS, &vcpu->sregs) < 0) {
        perror("can not set sregs");
        exit(1);
    }
    
    // 設置寄存器標誌位
    vcpu->regs.rflags = 0x0000000000000002ULL;
    // rip 表示了程序的起始指針,地址爲 0x0000000
    // 在加載鏡像的時候,咱們直接將binary讀取到了虛擬機的內存起始位
    // 因此虛擬機開始的時候會直接運行binary
    vcpu->regs.rip = 0;
    // rsp 爲堆棧頂
    vcpu->regs.rsp = 0xffffffff;
    // rbp 爲堆棧底部
    vcpu->regs.rbp= 0;

    if (ioctl(vcpu->vcpu_fd, KVM_SET_REGS, &(vcpu->regs)) < 0) {
        perror("KVM SET REGS\n");
        exit(1);
    }
}

運行一下結果,能夠看到當虛擬機執行了指令 out %ax, $0x10 的時候,會引發虛擬機的退出,這是CPU虛擬化裏面將要介紹的特殊機制。
宿主機獲取到虛擬機退出的緣由後,獲取相應的輸出。這裏的步驟就相似於IO虛擬化,直接讀取IO模塊的內存,並輸出結果。

➜  kvmsample git:(master) ✗ ./kvmsample
read size: 712288
KVM start run
KVM_EXIT_IO
out port: 16, data: 0
KVM start run
KVM_EXIT_IO
out port: 16, data: 1
KVM start run
KVM_EXIT_IO
out port: 16, data: 2
KVM start run
KVM_EXIT_IO
out port: 16, data: 3
KVM start run
KVM_EXIT_IO
out port: 16, data: 4
...

總結

虛擬機的啓動過程基本上能夠這麼總結:
建立kvm句柄->建立vm->分配內存->加載鏡像到內存->啓動線程執行KVM_RUN。從這個虛擬機的demo能夠看出,虛擬機的內存是由宿主機經過mmap調用映射給虛擬機的,而vCPU是宿主機的一個線程,這個線程經過設置相應的vCPU的寄存器指定了虛擬機的程序加載地址後,開始運行虛擬機的指令,當虛擬機執行了IO操做後,CPU捕獲到中斷並把執行權又交回給宿主機。

固然真實的qemu-kvm比這個複雜的多,包括設置不少IO設備的MMIO,設置信號處理等。

下一篇將介紹CPU虛擬化相關知識。

相關文章
相關標籤/搜索