在先前的實作中,我們已經建立了 host_io 模組,並且透過 epoll 以統一監聽各檔案描述符的事件,同時透過分發對應的 callback,讓 Host 模擬各種 I/O 設備的事件觸發,並透過拉 GSI 來實現 Guest 可感知的事件。
而今天我會以 stdin(鍵盤輸入)作為例子,讓 host_io 可即時捕捉鍵盤輸入,並把調用 callback 處理。
終端機的輸入模式預設是 Canonical 模式,示只有當使用者按下 enter 之後,程式才會讀取到一整行資料。而對於我們的需求是希望每個按鍵按下就立即觸發事件。為此我們需要調整一些屬性。
為了達成即時效果,可用 termios 以修改終端行為,其流程如下:
static int configure_terminal(struct host_input *input)
{
struct termios attr;
if (tcgetattr(STDIN_FILENO, &attr) < 0)
return -1;
input->original_termios = attr;
input->termios_valid = 1;
attr.c_lflag &= (unsigned int)~(ICANON | ECHO);
attr.c_lflag |= ISIG;
attr.c_cc[VMIN] = 0;
attr.c_cc[VTIME] = 0;
if (tcsetattr(STDIN_FILENO, TCSANOW, &attr) < 0)
return -1;
return 0;
}
tcgetattr 可取得當前終端設定,我們將此設定保存到 input->original_termios,可在程式結束或出錯時還原終端環境。
而 c_lflag,可用於控制中端輸入行為。我們關閉了 ICANON 和 ECHO。關閉 ICANON 後每輸入一個字元就立即生效,無需等待 enter;而關閉 echo 則避免輸入直接顯示在螢幕上,影響觀察 Guest 輸出。
同時,我們需要 ISIG,這將使 Ctrl + C、Ctrl + Z 的控制訊號仍然會被處理,因此即便程式出錯我們仍可以透過 Ctrl + C 快速終止程式。
將 VMIN 和 VTIME 設定為 0,程式在讀取輸入時會採取 non-blocking 策略,因此事件可隨時檢查而不會被 read 卡住。
而 epoll 觸發事件後會交給第一層 callback 事件分法處理以取得事件。
static void host_input_handle_ready(struct host_input *input)
{
unsigned char buffer[64];
for (;;) {
ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer));
if (n > 0) {
input->callback(buffer, (size_t)n, input->opaque);
continue;
}
if (n == 0)
break;
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
perror("host_input read");
break;
}
}
當 epoll 監控到事件觸發就會透過 callback 調用到 stdin 的處理函式 host_input_handle_ready,此函ˋ將轉發到註冊再 host_io_handle 的處理方法 input->callback。
static void host_keyboard_callback(const unsigned char *data, size_t len, void *opaque)
{
struct vm_runtime *vm = opaque;
if (!vm || !vm->keyboard_buf)
return;
for (size_t i = 0; i < len; ++i) {
vm->keyboard_buf[0] = data[i];
if (irq_manager_pulse(&vm->irq_mgr, 1) < 0)
perror("irq_manager_pulse keyboard");
}
}
最終 input->callback 則連接到 host_keyboard_callback 並調用 irq_manager_pulse 設定 GSI 以讓 Guest 感知到中斷。