Tuesday, January 27, 2009

一千零一夜之 keyboard in console

這個系列是讀書筆記,作者可能沒有跟主題有關的開發經驗。

按下或放開按鍵的時候,鍵盤會產生一段長度不等的 scancode,這段 scancode 會被 keyboard driver (像是 drivers/input/keyboard/atkbd.c) 處理,轉換成類型為 EV_MSC 與 EV_KEY 的 input event。EV_MSC 事件的內容是 scancode 本身,與硬體直接相關且不容易處理,除非有特殊需求,通常我們比較關心的是 EV_KEY input event。想要處理這些事件的其它子系統,可以向 input layer 註冊 input handler。例如 evdev 會註冊一個 input handler 讓 userspace 可以從 /dev/input/eventX 直接取得鍵盤事件。kernel 的 console driver 也會向 input layer 註冊一個 handler 來取得鍵盤事件,這是我們這次想要比較深入了解的項目。

console 跟鍵盤的交互作用可以在 drivers/input/char/keyboard.c 看到。在 console 底下,鍵盤有四種模式,

#define VC_XLATE 0 /* translate keycodes using keymap */
#define VC_MEDIUMRAW 1 /* medium raw (keycode) mode */
#define VC_RAW 2 /* raw (scancode) mode */
#define VC_UNICODE 3 /* Unicode mode */

可以利用 kbd_mode 命令來切換。前面有提到 keyboard driver 會送出 EV_MSC 與 EV_KEY 兩種 input event,其中前者送出的 scancode 被用來實作 VC_RAW 模式;後者送出的 keycode,則可以用來實作 VC_MEDIUMRAW 模式。剩下的兩種模式都是 keycode 的再加工。

scancode 到 keycode 的轉換透過了 keyboard driver 定義的 keycode table 完成。在 atkbd.c 裡,如果忽略掉其它細節,它只是簡單的

keycode = atkbd->keycode[code];

一行程式碼。有些多媒體鍵盤會有一些按鍵的 scancode 不在預設的 keycode table 裡,當這些按鍵被按下或放開的時候,kernel 會印出類似

atkbd.c: Unknown key pressed (translated set 2, code 0x1e on isa0060/serio0).
atkbd.c: Use 'setkeycodes 1e <keycode>' to make it known.

的訊息。依照這段訊息提示的方法去做

# setkeycodes <scancode> <keycode>

就可以新增一筆對映的資料。setkeycodes 與 getkeycodes 命令即是用來設定或取得目前的 keycode table。值得注意的是,大部份的 keycode 有對應的 symbolic name,例如 keycode 30 的 symbolic name 是 KEY_A,keycode 48 的是 KEY_B,keycode 46 的是 KEY_C 等 (見 /usr/include/linux/input.h)。這些 symbolic name 是照美式鍵盤定義而來,拿到其它語言的鍵盤上並沒有意義。

console 下 keyboard 預設的模式是 VC_XLATE 或 VC_UNICODE。從 input layer 拿到 keycode 後,console 會呼叫 kbd_event 函數做處理。在這兩個模式下,keycode 會被轉換成 keysym

key_map = key_maps[shift_final];
...
keysym = key_map[keycode];

然後 queue 到 console buffer。stdin 接到 console 的程式即是由這邊取得使用者的輸入。這個動作會經過 line discipline,不過我們這邊不去討論。

一個 keysym 是兩個 byte,分別可以由 KTYP(keysym) 跟 KVAL(keysym) 取得它的 type 與 value。當 type 小於 0xf0 時,整個 keysym 代表一個 UCS2 的 codepoint,keysym 會被轉成 UTF-8 放進 console buffer;當 type >= 0xf0 時,與 type 相關的 handler 被呼叫,其 prototype 為

typedef void (k_handler_fn)(struct vc_data *vc, unsigned char value,
char up_flag);

其中 value 就是剛剛看到的 KVAL(keysym)。有些 handler 會把處理過的 value 寫入 console buffer,有些則不會,這樣設計的目的以例子來看最容易理解。我們知道 shift 按著的時候,所有的字母大小寫會互換。當 shift 的按下時,鍵盤控制器送出中斷,keyboard driver 收到後把 scancode 讀出並轉成 keycode,送出 input event。console 收到 event 後利用 keymap 把 keycode 轉成 keysym。shift 的 keysym type 是 0xf7,value 是 0x00,送到對應的 handler 中

if (up_flag) {
if (shift_down[value])
shift_down[value]--;
} else
shift_down[value]++;

if (shift_down[value])
shift_state |= (1 << value);
else
shift_state &= ~(1 << value);

可以發現 shift_state 的第一個 bit 被設為 1。shift_state 是 keyboard.c 裡的 file-scope 變數,當任何一個按鍵再被按下時,kbd_event 裡這一段程式碼

shift_final = (shift_state | kbd->slockstate) ^ kbd->lockstate;
key_map = key_maps[shift_final];

選取了不同的 keymap,導致或許不一樣的 keysym 被送出,本來是小寫的英文字母也因而變成大寫。

在 kernel 內部實作,console 使用的是 UCS2 編碼。不管是使用者列印到 console 的字串,或者是 keysym 的 value 都會先重新編碼成 UCS2 再做進一步處理。在 VC_UNICODE 模式底下,UCS2 會以 UTF-8 形式被送進 console buffer;在 VC_XLATE 模式,則會嘗試再編碼回原來的 value 送到 console buffer。

相對於 keymap 複雜的機制,它的操作卻非常簡單。在 userspace 可以利用 loadkeys 與 dumpkeys 命令來操作 keymaps

# loadkeys de # 使用德式鍵盤 layout
# loadkeys us # 使用美式鍵盤 layout

進一步的資訊可以參考 keymaps(5)。

這篇文章我們看到了鍵盤在 console 的運作。如果換到 X(org) server,情形有點不同,而且也因驅動程式而異。例如 xf86-input-keyboard 雖然是從 console 取得鍵盤事件,但它會把 console 設為 VC_RAW 模式,自行處理 scancode。而 xf86-input-evdev 則是直接從 /dev/input/eventX 取得鍵盤事件。以後有機會也可以看看 X(org) 下的鍵盤運作。

No comments: