iT邦幫忙

2025 iThome 鐵人賽

DAY 5
0
自我挑戰組

30 天 hypervisor 入門系列 第 5

Day 05 設計簡易的 VM-exit 處理表

  • 分享至 

  • xImage
  •  

在虛擬化系統裡,Guest 遇到無法直接執行的指令時,處理器會觸發 VM-exit,將控制權交還給 Hypervisor。這意味著我們必須在 Host 端準備一個「分配中心」,負責接手這些事件。
虛擬化是一層監督者,而 VM-exit Handler 就像是這個監督者的硬體抽象層(HAL),它的職責很簡單,當事件發生時,決定該怎麼處理、該如何回應。

表驅動

VM-exit 的本質就是一組編號(exit reason),每個編號都代表一種需要 Hypervisor 介入的事件。最簡單的抽象,就是將這些「事件編號 -> 處理函式」集中成一張表。

這張表就像一個任務觸發中心,告訴 Hypervisor 要如何接手處理這些任務,比如:

  • 遇到 I/O 就調用 handle_io()
  • 遇到 HLT 就調用 handle_hlt()
  • 遇到尚未實作的事件就調用 default_handler(),留下紀錄告訴我們還有哪些事件等待實現

對分派中心而言,它不需要知道邏輯細節,只要查表並轉交即可。

static int default_handler(struct kvm_run *run) {
    fprintf(stderr, "[warn] unhandled exit_reason=%u\n", run->exit_reason);
    return EXIT_HANDLE_ERROR;
}

typedef int (*exit_handler)(struct kvm_run *run);
#define EXIT_CMD_LIST(X) \
    X(KVM_EXIT_IO, handle_io) \
    X(KVM_EXIT_HLT, handle_hlt) \
    X(KVM_EXIT_SHUTDOWN, handle_shutdown) \
    X(KVM_EXIT_MMIO, handle_mmio)
    
#define DECLARE_HANDLER(_, fn) static int fn(struct kvm_run *run); EXIT_CMD_LIST(DECLARE_HANDLER)

static exit_handler handler[KVM_EXIT_XEN] = {
    [0 ... KVM_EXIT_XEN - 1] = default_handler,
#define REGISTER_HANDLER(code, fn) [code] = fn,
    EXIT_CMD_LIST(REGISTER_HANDLER)
#undef REGISTER_HANDLER
};

這樣就完成了簡單的分派骨架。未來只要實作新的 VM-exit,直接在表中註冊對應的處理方法即可;尚未實作的則會自動落到 default_handler,在測試中提醒我們 Guest 執行還缺少哪些功能。

註: [0 ... KVM_EXIT_XEN - 1] 這是 gcc 的擴展語法,上面的寫法等價於把 handler[0] 到 handler[KVM_EXIT_XEN-1] 全部設成 default_handler,而無須逐一寫入。

分配中心

有了處理表之後,主控邏輯就能保持簡單,他不會處理任何任務細節,僅根據 exit_reason 查表並調用對應的函式,並依具返回結果決定要不要繼續跑。

for (;;) {
    if (ioctl(vcpu_fd, KVM_RUN, 0) < 0) {
        if (errno == EINTR) continue;
        perror("KVM_RUN");
        break;
    }
    int rc = handler[run->exit_reason](run);
    if (rc == EXIT_HANDLE_CONTINUE)
        continue;
    if (rc == EXIT_HANDLE_DONE)
        break;
    break; // ERROR or unknown
}

昨天我們先用 switch 完成了一個最小可行的分派器,驗證 VM-exit 能被正確捕捉。這是必經的一步,因為它簡單直接,能幫我們確認流程。
但當事件類型逐漸增加,switch 會變得笨重,修改時也容易牽動整個主控迴圈。因此這裡先將分派邏輯抽離並打表處理。

各式 Handler

有了分配骨架後,接下來的任務就是補齊各種 VM-exit 的處理方法。讓 handler 處理事件並把回報給分派中心。

I/O 事件

最先著手的是 I/O,因為這是最容易驗證的出口。我們把原本 switch-case 的 I/O 處理邏輯封裝成一個獨立函式:

static int handle_io(struct kvm_run *run)
{
    if (run->io.direction != KVM_EXIT_IO_OUT) {
        fprintf(stderr, "[err] unexpected IO direction=%u\n", run->io.direction);
        return EXIT_HANDLE_ERROR;
    }

    unsigned char *data = (unsigned char *)run + run->io.data_offset;
    for (uint32_t i = 0; i < run->io.count; ++i) {
        putchar(data[i]);
    }
    fflush(stdout);
    return EXIT_HANDLE_CONTINUE;
}

目前我們的 I/O 暫不模擬任何裝置,他做的事情只是打印資料。

HLT 與 SHUTDOWN

static int handle_hlt(struct kvm_run *run) {
    (void)run;
    printf("[info] KVM_EXIT_HLT\n");
    return EXIT_HANDLE_DONE;
}

static int handle_shutdown(struct kvm_run *run) {
    (void)run;
    printf("[info] KVM_EXIT_SHUTDOWN\n");
    return EXIT_HANDLE_DONE;
}

目前我們並沒有模擬任何真實裝置,handler 的工作只有一件事:把資料打印到 Host。

HLT 與 SHUTDOWN

接著是是 HLT 與 SHUTDOWN 事件。這代表 Guest 主動結束,Hypervirsor 需要回收控制資源。

static int handle_hlt(struct kvm_run *run) {
    (void)run;
    printf("[info] KVM_EXIT_HLT\n");
    return EXIT_HANDLE_DONE;
}

static int handle_shutdown(struct kvm_run *run) {
    (void)run;
    printf("[info] KVM_EXIT_SHUTDOWN\n");
    return EXIT_HANDLE_DONE;
}

在這裡,我們選擇回傳 EXIT_HANDLE_DONE,讓主控迴圈能識別並結束。這樣一來,測試就可以在「建立 vCPU -> I/O 輸出 -> 中止」,的流程下走通。


上一篇
Day 04 讓 Guest 輸出 HELLO
下一篇
Day 06 中斷事件
系列文
30 天 hypervisor 入門11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言