Linux Input Subsystemの使い方

ると

はじめに

この文章はなにか

Linuxでユーザーからの入力を処理するInput Subsystemの説明と、その使い方を書いた非公式な文章です。

主に使う側の視点で書かれています。ドライバの開発者には向いていないかもしれません。

注意

この文章の筆者はLinuxの開発者ではなく、内容には間違いが含まれる場合があります。

Input Subsystemは使い方を誤るとキーロガーが仕掛けられるなど、システムのセキュリティを低下させる恐れがあります。Input Subsystemを利用するプログラムが悪意のあるユーザの攻撃を受けないように注意してください。

サンプルファイル

このページで使っているサンプルファイルを予めダウンロードしておいてください。

Input Subsystemとは何か

Input Subsystemで何ができるのか

Input Subsystemを使うと次のようなことができます。

Input Subsystemの概要

Input Subsystemとは、USBやPS/2などの低水準なドライバと、ユーザーの使うプログラムの橋渡しをするLinuxカーネルの一部です。

ユーザーとのやりとりはデバイスファイルの読み書きによって行なわれます。

必要なソフトウェア

Input Subsystemを使うにはバージョン2.2以降のカーネルが必要となります。

ただ、Linux 2.4以前のInput SubsystemはLinux 2.6のものととやや異なっており、2.6用に書いたコードはそのままでは動きません(運が良ければ動くこともある)。また、Linux 2.4のInput SubsystemはPS/2マウスが扱えなかったり、標準ドライバでの処理を抑制できないなど制約があります。この文章では主にLinux 2.6のInput Subsystemについて説明します。

また、Linux 2.6でもInput SubsystemやUser Inputといったカーネルモジュールが有効にされている必要があります。/dev/input/event[0-9]や/dev/uinputといったファイルがあることを確認してください。

Input Subsystemの使い方

イベントデバイスファイル

/dev/inputというディレクトリを見てみてください。event0とかevent1といったファイルがあるはずです。このファイルがInput Subsystemを使う上で中心となるイベントデバイスファイルです。

このファイル1つ1つが1つのキーボードやマウスなどのデバイスに対応しています。

これらのファイルから読み込めばデバイスからの入力を取得できますし、これらのファイルに書き込めば入力を捏造できます。

イベントデバイスファイルを読んでみる

では、ちょっと覗いてみましょう。hexdump /dev/input/event0などを実行してみてください。一般ユーザーでは見られないかもしれません。そしてキーボードやマウスを操作してみてください。入力に反応して何やら(内容はめちゃくちゃに見えても)出力が得られるはずです。別のファイルを覗いてみると別のデバイスからの出力が得られます。

この出力の形式は/usr/include/linux/input.hで次のように定められています。


struct input_event {
        struct timeval time;
        __u16 type;
        __u16 code;
        __s32 value;
};

それぞれの意味は次のようになります。詳しくは/usr/include/linux/input.hを見てください。

time
入力のあった時間
type
入力のタイプ。キー入力なのか、マウスなどのの動きなのかなど。代表的な値は次のとおり
EV_KEY
キーボードやマウスボタンなどのキー入力
EV_REL
マウスの動きやマウスホイールやジョグダイヤルなどの相対的な動き
EV_ABS
ジョイスティックの傾きやタッチパネルの入力やアクセルペダルなどの絶対的な動き
code
どのキーが入力されたかなど。代表的な値は次のとおり
KEY_SPACE
スペースキー
KEY_MUTE
ミュートキー
BTN_LEFT
マウスの左ボタン
BTN_GEAR_DOWN
ホイールコントローラーのギアダウン
REL_X
マウス等のx軸方向の動き
ABS_X
タッチパネル等のx軸方向の位置、ジョイスティックのx軸方向の傾き
ABS_GAS
ホイールコントローラーのアクセルペダル
注意: 例えば、エスケープキーを表すKEY_ESCとマウスなどのy軸方向の動きを表すREL_Yは同じ0x01という値を持っているのでtypeを見てどちらなのか判断する必要があります。
value
キーが押されたのか離したのか、マウスなどがどのぐらい動いたのかなど、typeによって次のような意味を持つ
type == EV_KEYの場合
キーを押したときは1、離したときは0、オートリピートによる入力があったときは2
type == EV_RELの場合
動いた量
type == EV_ABSの場合
現在の値

これを使うと、イベントデバイスファイルを開いて、input_event構造体を読み込んで、その値に反応してさまざまな処理をするプログラムを書くことができます。

例として、最近のキーボードに付いている「ミュート」「音量を上げる」「音量を下げる」ボタンに反応して音量を調整するプログラムを作ってみます。ただし音量を調節するプログラムとしてmuteというコマンドとaumixというコマンドがインストールされていると仮定します。

例: 音量調整プログラム

#include <stdio.h>
#include <stdlib.h>
#include <linux/input.h>
#include <unistd.h>

int main(void)
{
  for (;;) {
    struct input_event event;

    if (read(0, &event, sizeof(event)) != sizeof(event)) {
      exit(EXIT_FAILURE);
    }
    switch(event.type) {
      case EV_KEY:
        switch(event.code) {
          case KEY_MUTE:
            if (event.value == 1) {
              system("mute");
            }
            break;
          case KEY_VOLUMEDOWN:
            if (event.value != 0) {
              system("aumix -v-");
            }
            break;
          case KEY_VOLUMEUP:
            if (event.value != 0) {
              system("aumix -v+");
            }
            break;
          default:
            break;
        }
        break;
      default:
        break;
    }
  }
}

これをvolme.cとして保存し、gcc volme.c -o volmeとしてコンパイルします。そして./volme < /dev/input/event0とします。ただし、event0はキーボードに対応するイベントデバイスファイルとします。

プログラムは単純で、readして、switchしているだけですね。

「音量を上げる」「音量を下げる」ボタンの場合はevent.valueが1の場合も2の場合も反応するようにしてあるので、ボタンを押しっぱなしにすればどんどん音量を変更できます。

サンプルファイルのdump_event.cはイベントデバイスファイルに関する情報とイベントを人間が読める形式で出力します。デバイスについて調べたいときに使うといいでしょう。

イベントデバイスファイルに書いてみる

こんどはさきほどの逆です。イベントデバイスファイルにinput_event構造体を書き込んでみましょう。

例: イベントの生成

#include <stdio.h>
#include <stdlib.h>
#include <linux/input.h>
#include <sys/time.h>

void write_key_event(int code, int value, int fd)
{
  struct input_event key_event;
  
  gettimeofday(&key_event.time, NULL);
  key_event.type = EV_KEY;
  key_event.code = code;
  key_event.value = value;
  write(fd, &key_event, sizeof(key_event));
}

int main(void)
{
  write_key_event(KEY_A, 1, 1);
  write_key_event(KEY_A, 0, 1);
  write_key_event(KEY_B, 1, 1);
  write_key_event(KEY_B, 0, 1);
  write_key_event(KEY_C, 1, 1);
  write_key_event(KEY_C, 0, 1);

  write_key_event(KEY_LEFTSHIFT, 1, 1);
  write_key_event(KEY_A, 1, 1);
  write_key_event(KEY_A, 0, 1);
  write_key_event(KEY_B, 1, 1);
  write_key_event(KEY_B, 0, 1);
  write_key_event(KEY_C, 1, 1);
  write_key_event(KEY_C, 0, 1);
  write_key_event(KEY_LEFTSHIFT, 0, 1);

  exit(EXIT_SUCCESS);
}

これをwrite.cとして保存し、gcc write.c -o writeとしてコンパイルします。そして./write > /dev/input/event0とします。ただし、event0はキーボードに対応するイベントデバイスファイルとします。するとabcABCというのがキーボードから入力されたように処理されるはずです。

コードでは「Aを押す」「Aを離す」……「shiftを押す」「Aを押す」「Aを離す」……「shiftを離す」というイベントを発生させています。

ところで、このプログラムの出力をマウスに対応するイベントデバイスファイルに書き込むとどうなるでしょうか。イベントは無視されてしまいます。同様にマウス関連のイベントをキーボードに対応するイベントデバイスファイルに書き込んだ場合も無視されてしまいます。また、ホイールの無いマウスに対してホイールイベントを書き込んでも無視されてしまいます。しかし、これではマウスを繋いでいないシステムでマウスをエミュレートするといったことができなくなってしまいます。

そこで次で紹介するUser Inputモジュールを利用します。

User Inputモジュール

User Inputモジュールを使うとユーザーが仮想的なデバイスを作成できます。つまりマウスを繋げてないシステムで仮想的なマウスのイベントデバイスファイルを作成できます。

User Inputモジュールを使うには、uinputデバイスファイル(普通/dev/uinput)に対してどのようなデバイスを作りたいかという情報を書き込んだあと、ioctl(ui_fd, UI_DEV_CREATE, NULL);を実行します。

具体的にどのように使うかはサンプルファイルのhook_main.cを参照してください(TODO: 詳しく書く)。

ここで、例えば仮想的なキーボードデバイスを作ったとしてもすぐにLinuxのコンソールなどイベントが渡されるようになるわけではありません。デバイスはどのようなイベントを発生する可能性があるかという情報を持っているのですが、Linuxの標準キーボードドライバはこれを見て一定の条件を満したものだけイベントをコンソールなどに渡します。そのため、デバイスを作るときに、Linuxの標準ドライバにキーボードやマウスとして認識されるためには次のようなイベントを出力すると宣言する必要があります。

キーボード
KEY_RESERVEDからBTN_MISCの内から最低1つ、もしくはEV_SNDのいずれか
マウス

REL_X, REL_YおよびBTN_LEFT

または、REL_WHEEL

または、ABS_X, ABS_YおよびBTN_TOUCH

または、ABS_X, ABS_Y, ABS_PRESSURE, ABS_TOOL_WIDTHおよびBTN_TTOL_FINGER

つまり、マウスとして認識させるにはioctl(ui_fd, UI_DEV_CREATE, NULL);をする前に次のようにします。

ioctl(ui_fd, UI_SET_EVBIT, EV_REL);
ioctl(ui_fd, UI_SET_RELBIT, REL_X);
ioctl(ui_fd, UI_SET_RELBIT, REL_Y);

ioctl(ui_fd, UI_SET_EVBIT, EV_KEY);
ioctl(ui_fd, UI_SET_KEYBIT, BTN_LEFT);

ここでui_fdはuinputデバイスのファイルデスクリプタとします。

# ただし、Linuxのマウスの標準ドライバ(mousedev.c)はPS/2マウスをエミュレートして/dev/input/mouse*や/dev/input/miceを作るだけであり、しかも最近のXは/dev/input/mouse*や/dev/input/miceではなくイベントデバイスファイルを直接読み込むため、あまり意味はないかもしれません。

標準ドライバで処理しないようにする

ここまでのことを応用すれば入力を簡単に自由に加工できそうです。しかし、CapsLockキーをControlキーにしようと考えて、イベントデバイスファイルからイベントを読み込んでCapsLockキーのイベントが来た時にControlキーのイベントを出力するようにしても、CapsLockキーとControlキーの両方が押されたことになってしまいます。これは標準ドライバがCapsLockキーを通常通り処理してしまうからです。

つまり、標準ドライバ(というか自分以外のプログラム)が処理を行わないようにする必要があります。そのためにはイベントデバイスファイルのファイルデスクリプタに対して次のような操作を行い、デバイスを占有するようにします。

ioctl(fd, EVIOCGRAB, 1);

元に戻すときは0を指定します。

ioctl(fd, EVIOCGRAB, 0);

ただし、grabしてしまうとそのイベントデバイスファイルに書き込んでも標準ドライバで扱われなくなってしまうので、イベントを出力するときはUser Inputモジュールを使わないといけません。

Input Subsystemの内部動作

TODO

入力をカスタマイズするときのTips

Xで水平スクロール その1

# 最近のXはイベントデバイスファイルを直接読めるのでこの節は必要無い?

以下ではX Window SystemはX.org X11を使っているものと仮定します。

Input Subsystemを使ってばりばりカスタマイズしようとするとXでつまづくことがあります。

X.org X11 6.8.2現在、Xは/dev/input/event[0-9]を直接読んではくれません。次のバージョンでは読めるらしいのですが、それまでは/dev/input/mice等を使ってPS/2プロトコルで通信してやる必要があります。

/dev/input/mouse[0-9]はデバイスがマウスと認識されれば自動で作られ、PS/2プロトコルへの変換も自動でやってくれるので作成に関しては気にする必要はありません。

しかし、/dev/input/mouse[0-9]でのやりとりにはPS/2プロトコル(正確にはImPS/2プロトコル)が使われるので次のような制約があります。

そのためXで水平ホイールを使うにはちょっとした工夫が必要です。

そもそもXにはホイールのイベントというものはありません。そこでその代りに多くのアプリケーションではボタン4, 5をホイールの上下、6, 7を水平ホイールの左右として扱っています。

そこで、xorg.conf上でホイールをボタン4, 5に変換して、元々のボタン4, 5をボタン6, 7に変換して、水平スクロールを発生させたいときにはボタン4, 5のイベントを発生させれば良いように思えるのですが、xorg.confではホイールからボタンへの変換はできても、ボタンを別のボタンに変換できません。

そこで、ボタン4, 5はそのままで、ホイールにボタン6, 7を割り当て、イベントを生成するときに、REL_WHEELの代りにBTN_SIDE, BTN_EXTRAを、REL_HWHEELの代りにREL_WHEELを使うというややこしいことをしなければなりません。

この場合のxorg.confは次のようになります。

Section "InputDevice"
          Identifier  "Mouse0"
          Driver      "mouse"
          Option      "Protocol" "auto"
          Option      "Device" "/dev/input/mice"
          Option      "Buttons" "7"
          Option      "ZAxisMapping" "6 7"
EndSection

ただし、この場合通常のホイールマウスなどを併用しようとしても正常に動かないので注意が必要です。

Xで水平スクロール その2

もう1つの方法として、Xのドライバが標準で持っているホイールのエミュレーションを使うという手があります。

この場合xorg.confは次のようになります。

Section "InputDevice"
          Identifier  "Mouse0"
          Driver      "mouse"
          Option      "Protocol" "auto"
          Option      "Device" "/dev/input/mice"
          Option      "Buttons" "7"
          Option      "EmulateWheel" "true"
          Option      "EmulateWheelButton" "4"
          Option      "EmulateWheelInertia" "50"
          Option      "XAxisMapping" "6 7"
          Option      "YAxisMapping" "4 5"
EndSection

こうすると、ボタン4を押しながらマウスを動かすことで垂直/水平ホイールを行うことができます。ボタン4が無いマウスの場合はキーボードの使っていないキーをInput SubsystemでBTN_SIDEに変換してやるといいでしょう。

ホイールの速度はEmulateWheelInertiaで指定します。何ピクセル分マウスを動かしたときにホイールイベントを発生させるかという形で指定します。つまり小さくすると速く、大きくすると遅くなります。

udev

Input Subsystemを使っているとき、デバイスファイルと実際のデバイスの対応が簡単に分ると便利です。また、実際のデバイスと/dev/input/event[0-9]や/dev/input/mouse[0-9]との対応はちょくちょく変わるのでこれを固定できれば便利です。そこでudevを使って/dev/input以下のデバイスファイルのファイル名を制御してやります。

udev自体の導入方法は別のドキュメントを参照してください。ここでは設定方法のみを示します。

まず、udevinfo -a -p /sys/class/input/event4のようにして実際のデバイスを判別するための値を調べます。SYSFS{idProduct}やSYSFS{idVendor}が適当でしょう。SYSFS{product}もわかりやすくておすすめです。

そして/etc/udev/rules.d/10-local.rulesに次のように書きます。

BUS="usb", KERNEL="event*", SYSFS{product}="Kensington USB/PS2 Orbit", NAME="input/%k", SYMLINK="input/event_orbit"
BUS="usb", KERNEL="mouse*", SYSFS{product}="Kensington USB/PS2 Orbit", NAME="input/%k", SYMLINK="input/mouse_orbit"

BUS="usb"とかSYFS{product}="..."とかSYMLINK="..."の部分は適宜書き換えてください。こうすると/dev/input/mouse_orbitというシンボリックリンクが作られ、常にOrbit(註: トラックボールの商品名)に対応する/dev/input/mouse[0-9]を指すようになります。event_orbitも同様にOrbitのイベントデバイスファイルを指すようになります。

ただし、User Inputモジュールで作った仮想的なデバイスについては判別するための情報をudevinfoを使っても得られないため上手くいかないので注意が必要です。

付録: サンプルにあるhook_main.cの使い方

hook_main.cはuinputでデバイスを作った後、Input Subsystemからの入力をフックし、加工してuinputで作ったデバイスに出力するというプログラムのテンプレートとなるファイルです。

別ファイルでいくつかの関数や変数を定義し、hook_main.cと一緒にコンパイルすることで利用できます。

使う時にはhook_main.hをインクルードし、次の関数およびグローバル変数を定義します。

char *name
プログラムの名前。長さはUINPUT_MAX_NAME_SIZEまで。
char *version_information
バージョン情報および著作権情報を表す文字列。
char *description
プログラムの簡単な説明。
void set_event_bits(int fd)
uinputで作成するデバイスがどのイベントを生成するか宣言する。fdはuinputデバイスファイルのファイルデスクリプタ。ioctl(fd, UI_SET_EVBIT, EV_REL);ioctl(fd, UI_SET_RELBIT, REL_WHEEL);等とする。
void *init_event_handler(int number_of_event_device_files, int event_fds[])
プログラムの初期化を自由に行う。event_fdsにはプログラムのコマンドライン引数で指定されたイベントデバイスファイルのファイルデスクリプタが、コマンドライン引数で指定された順番で入る。number_of_event_device_filesはその数。返り値として任意のポインタを返してよい。その値はhandle_event関数にuser_dataとして渡される。
void finalize_event_hander(void *user_data)
プログラムの後処理を行う。user_dataはinit_event_handlerで返された値。
void handle_event(int fd, struct input_event event, void *user_data)
イベントの処理を行う。fdはイベントの起きたイベントデバイスファイルのファイルデスクリプタ。eventはこのイベントを表すinput_event構造体。user_dataはinit_event_handlerで返した値。

また、次の関数が利用可能です。

void send_event(int type, int code, int value)
イベントを出力する。type, code, valueはそれぞれinput_event構造体の対応するフィールドの値。
void report_scroll(int dy)
ホイールイベントを出力する。dyには動かす量を示す。

hook_main.cを使ったプログラムの例として次のようなものがアーカイブに入っています。これらは筆者が個人的に使っていたものなのでそのままでは使いづらいかと思いますが、改造の元にしてください。

toggle_scroll.c
第一引数にマウスのイベントデバイスファイル、第二引数にキーボードのイベントデバイスファイルを指定すると、無変換でマウスのBTN_SIDEを出力し、さらに無変換+マウスの左ボタンでHomeキー、無変換+マウスの右ボタンでEndキーを出力する。xorg.confでEmulateWheelをすることで、無変換+マウス(というかトラックボール)でスクロールを行うことを意図している。昔は自前で垂直ホイールイベントを出力していたためその名残のコードも残っている。
gamepad_mouse.c
USBゲームパッドでfirefoxを使うというコンセプトで作られたもの。2つのアナログスティックのあるゲームパッドを使う。左スティックを回すことで垂直スクロール、右スティックでカーソル移動を行う。その他ボタンにキーストロークが登録されている。

参考にした文献やコード

更新履歴

2005年10月17日(月)
初版公開
2006年7月19日(水)
わかりづらいところを微妙に直す。
2012年1月25日(水)
わかりづらいところを直す。