在虛擬化系統裡,Guest 遇到無法直接執行的指令時,處理器會觸發 VM-exit,將控制權交還給 Hypervisor。這意味著我們必須在 Host 端準備一個「分配中心」,負責接手這些事件。
虛擬化是一層監督者,而 VM-exit Handler 就像是這個監督者的硬體抽象層(HAL),它的職責很簡單,當事件發生時,決定該怎麼處理、該如何回應。
VM-exit 的本質就是一組編號(exit reason),每個編號都代表一種需要 Hypervisor 介入的事件。最簡單的抽象,就是將這些「事件編號 -> 處理函式」集中成一張表。
這張表就像一個任務觸發中心,告訴 Hypervisor 要如何接手處理這些任務,比如:
對分派中心而言,它不需要知道邏輯細節,只要查表並轉交即可。
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 會變得笨重,修改時也容易牽動整個主控迴圈。因此這裡先將分派邏輯抽離並打表處理。
有了分配骨架後,接下來的任務就是補齊各種 VM-exit 的處理方法。讓 handler 處理事件並把回報給分派中心。
最先著手的是 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 暫不模擬任何裝置,他做的事情只是打印資料。
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 事件。這代表 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 輸出 -> 中止」,的流程下走通。