はじめに

ここではキーボード入力に対してOSがどういう処理を行っているかを読解します。OSはlinux-2.6.26.3を対象とします*1。注目するのは以下の点です。

  • キーボードが押されたときのハンドリング
  • デバイスファイルからの読み込み

なお、プラットフォームはx86、キーボードはPS/2接続とします。

前提知識、というか深く突っ込まないこと

以下のものは多分こんな風に動くんだろうなぁということで実際にどう処理しているかには踏み込みません。

  • モジュールの初期化、ドライバの初期化順序
  • ファイル操作の実装
  • 遅延処理とか
  • いろんなデータ構造

キーボード入力の処理(ハードウェア寄り)

キーボードの処理をやっていそうなコードを探すとdrivers/input/keyboardにatkbd.cが見つかります。dmesg見ても確かにこれが使われてるっぽいです。

$ dmesg
...
input: AT Translated Set 2 keyboard as /class/input/input0

module_init()ではatkbd_initが指定されているので見てみるとserio_register_driver()が呼び出されています。

次にdrivers/input/serio/serio.cを見るとロード時にkseriodスレッドを起動しています。スレッドではserio_event_listにエントリが入っていたらserio_handle_event()を呼び出すということをやっています。 というわけでserio_event_listにエントリを追加しているところを探すと、

serio_queue_event()
↑serio_rescan()
↑serio_interrupt()

というフローがあるようですが、serio_interrupt()では、

if (likely(serio->drv)) {
    ret = serio->drv->interrupt(serio, data, dfl);
} else if (!dfl && serio->registered) {
    serio_rescan(serio);
    ret = IRQ_HANDLED;
}

となっているのでドライバが設定されていればserio_event_listには格納されずinterrupt関数が直接呼ばれているようです。ちなみにlikelyについてはpsを読むに書いているので参照してください。

さて次にserio_interrupt()が誰に呼ばれるかですがgrepしてみるとi8042.cのi8042_interrupt()が見つかります。i8042_interruptはrequest_irq()の引数に渡されているのでキーボードが押されて割り込みが発生すると呼ばれるようです。

atkbd.cに戻って、serio_interrupt()から呼ばれるatkbd_interrupt()を眺めると変換とかをいろいろやっていますが一番重要なのは以下の一行でしょう。

input_event(dev, EV_KEY, keycode, value);

というわけでキーボードが押されたことによって発生した割り込みは入力イベントとしてポストされるようです。次節に続く。

キーボード入力の処理(ソフトウェア寄り、トップハーフ部分)

drivers/input/input.cを見ると

input_event()
→input_handle_event(type=EV_KEY)
→input_pass_event()

と処理が進んでいます。で、input_handlerに処理を渡しています。当てがなくなってしまったので、"find . | grep keyboard"でひっかけたdrivers/char/keyboard.cを眺めて見たところ、kbd_init()で、

error = input_register_handler(&kbd_handler);

としているのでこのファイルがキーボードの入力イベントを処理しているようです。

というわけでevent関数として設定されているkbd_event()→kbd_keycode()を眺めると、各キー入力に対してハンドラが呼ばれてるっぽいということがわかります。key_mapsでgrepをかけたところ、drivers/char/defkeymap.c_shippedがひっかかりました。

ushort *key_maps[MAX_NR_KEYMAPS] = {
	plain_map, shift_map, altgr_map, NULL,
	ctrl_map, shift_ctrl_map, NULL, NULL,
	alt_map, NULL, NULL, NULL,
	ctrl_alt_map, NULL
};

u_short plain_map[NR_KEYS] = {
	0xf200,	0xf01b,	0xf031,	0xf032,	0xf033,	0xf034,	0xf035,	0xf036,
	...
};

実際の入力を考えないとわからないので'1'を入力したとしましょう。多分、plain_mapの3番目の0xf031が選択されるのでしょう。というわけでk_handlerを見るとk_selfが実行されることがわかります。

k_self()→k_unicode()と進んで、キーボードがユニコードモードかで分岐してますがどちらにしろ最後はput_queue()に行き着いています。

static void put_queue(struct vc_data *vc, int ch)
{
	struct tty_struct *tty = vc->vc_tty;

	if (tty) {
		tty_insert_flip_char(tty, ch, 0);
		con_schedule_flip(tty);
	}
}

tty_insert_flip_char()はinclude/linux/tty_flip.hに書かれています。

static inline int tty_insert_flip_char(struct tty_struct *tty,
					unsigned char ch, char flag)
{
	struct tty_buffer *tb = tty->buf.tail;
	if (tb && tb->used < tb->size) {
		tb->flag_buf_ptr[tb->used] = flag;
		tb->char_buf_ptr[tb->used++] = ch;
		return 1;
	}
	return tty_insert_flip_string_flags(tty, &ch, &flag, 1);
}

con_schedule_flip()はinclude/linux/kbd_kern.hに書かれています。

static inline void con_schedule_flip(struct tty_struct *t)
{
	unsigned long flags;
	spin_lock_irqsave(&t->buf.lock, flags);
	if (t->buf.tail != NULL)
		t->buf.tail->commit = t->buf.tail->used;
	spin_unlock_irqrestore(&t->buf.lock, flags);
	schedule_delayed_work(&t->buf.work, 0);
}

というわけで、以降の処理は遅延実行されるみたいです。

キーボード入力の処理(ボトムハーフ部分)

ttyの処理をやってそうなコードを探すとdrivers/char/tty_io.cが見つかります。その中でtty_struct.buf.workを初期化しているコードを探すとinitialize_tty_struct()で初期化されていました。

static void initialize_tty_struct(struct tty_struct *tty)
{
	...
	INIT_DELAYED_WORK(&tty->buf.work, flush_to_ldisc);

flush_to_ldiscに進む前にinitialize_tty_structが呼ばれるまでを確認します。まず呼び出し階層は、

initialize_tty_struct()
↑init_dev()
↑tty_open()

となっています。次に、tty_openはtty_fopsのopenとして指定されています。

static const struct file_operations tty_fops = {
	.open		= tty_open,
	...
};

でもって、tty_fopsは/dev/ttyに関連付けられています。

static int __init tty_init(void)
{
	cdev_init(&tty_cdev, &tty_fops);
	if (cdev_add(&tty_cdev, MKDEV(TTYAUX_MAJOR, 0), 1) ||
	    register_chrdev_region(MKDEV(TTYAUX_MAJOR, 0), 1, "/dev/tty") < 0)
		panic("Couldn't register /dev/tty driver\n");
	device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 0), "tty");

それではflush_to_ldiscを眺めます。重要なのは、

disc->receive_buf(tty, char_buf, flag_buf, count);

の一行です。で、ldiscって何が設定されてるの?というと、

static void initialize_tty_struct(struct tty_struct *tty)
{
	memset(tty, 0, sizeof(struct tty_struct));
	tty->magic = TTY_MAGIC;
	tty_ldisc_assign(tty, tty_ldisc_get(N_TTY));
void __init console_init(void)
{
	/* Setup the default TTY line discipline. */
	(void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY);

というわけでtty_ldisc_N_TTYが設定されているようです。tty_ldisc_N_TTYはdrivers/char/n_tty.cにあります。receive_bufに設定されているn_tty_receive_bufを見ると、いろいろやってますが、ttyのバッファに入力文字が格納されるということでいいでしょう。

キーボード入力の読み込み

さて、ここまで読めばもう大体わかりますが、デバイスファイルからの読み込みは、

tty_read() [drivers/char/tty_io.c]
→read_chan() [drivers/char/n_tty.c]

と進んでバッファに格納された値が読み込まれるようです。なんかread_chan()かなり長いんですけど本質的なことはそんなところでしょう。

おわりに

今回はキーボード入力をLinuxがどのように処理しているか見てきました。わかったこととしては以下のことがあります。

  • ハードウェア的な入力とソフトウェア的な入力というレイヤーになっている
  • ttyバッファへの格納はボトムハーフで行われている

とりあえずこれで、GUIからトップダウンに眺めてきたキーボード入力の旅は終わりです。それではみなさんもよいコードリーディングを。


*1 すでに2.6.28が出ていますが、眺め始めたのが2.6.26.3だったため、2.6.26.3を対象にします。まあ、キーボード周りは変わらないでしょう

トップ   編集 凍結解除 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2009-01-24 (土) 13:11:36 (5570d)