Ubuntu Weekly Recipe

第555回いま、あらためてudev

皆さん、ホットプラグってますか!? 猫も杓子もUSB端子なこのご時世、いろんなデバイスをPCに繋いだり取り外したりしていることと思います。そこで今回はデバイスの自動認識やデバイス名の設定など、システムの裏方として大活躍しているudevの基本を紹介します。

動的デバイス管理ツールudev

udevとはsystemdの別名です。

すみません、言い過ぎました。Ubuntuで使われているudevは、systemdの一部として提供されるデバイスの認識に関わるデーモンでありツールです。もともとは独立したソフトウェアでしたが、2012年にsystemdと同じソースツリーから提供されるようになりました。ただしデーモンプロセスとしてはsystemd(PID=1)から独立しています[1]⁠。

udevは具体的には次のような流れでデバイスを認識し、設定された作業を行います。

  • カーネルが追加・削除されたデバイスを認識する
  • ueventによってudevに変更内容が通知される
  • udevは通知された内容をもとに事前に設定されたルールに従って処理する

たとえばPCIやUSB、I2Cなどのデバイスバスのドライバーは、バスに何かが接続されたらACPIやDevice Treeといった事前に設定されたデバイス情報や、接続されたデバイスに汎用的にアクセスできるベンダー名・デバイス名などから通知する情報をとりまとめます。udevはその情報を元に必要に応じてカーネルモジュールをロードし、そのモジュールなどのドライバーからさらにデバイス固有の情報が通知されます。

udev自身がルールに従って主体的に情報を収集し、後続のルールに通知することもあります。そのようなルールの流れを経て、udevやそこから呼び出されるツールたちは次のような機能を実現するのです。

  • 適切なモジュールの自動ロード
  • ストレージ接続・切断時のマウント・アンマウント
  • 各種デバイスファイルの権限の変更
  • デバイス認識に伴うスクリプトの実行
  • デバイスファイルやシンボリックリンクの作成
  • サービスの起動・停止

特に任意のファイル名やシンボリックリンクの作成は、デバイスの認識順に依存しない「永続的なデバイス名」の命名に役立っています。

udevのルールと変更方法

udevがインストールされたシステムにおいて、ユーザー側にもっとも影響するのが「ルールファイル」です。ルールファイルの記述方法を把握しておくと、任意のデバイスを接続したときの挙動をコントロールできます。

udevのルールファイルは以下のいずれかのディレクトリに保存されています。

  • /etc/udev/rules.d:システムの管理者が作成・変更することを想定したルールファイルの保存場所
  • /run/udev/rules.d:一時的に作成されるルールファイルの保存場所
  • /lib/udev/rules.d:パッケージ等システムが提供するルールファイルの保存場所

上記ディレクトリのうち「.rules」を拡張子として持つファイルのみをルールファイルとして扱います。

複数のディレクトリで同じファイル名だった場合は、⁠/etc、/run、/lib」の順で優先されます。つまり「/lib」以下の任意のルールファイルを無効化したい場合、⁠/etc」以下に同名のファイルを「/dev/null」へのシンボリックリンクにすれば良いのです[2]⁠。

モジュール自動ロードのルールファイル

具体的なルールファイルの中身を見てみましょう。⁠/lib/udev/rules.d/80-drivers.rules」は必要なカーネルモジュールをロードする、udevが提供するルールファイルです。

# do not edit this file, it will be overwritten on update

ACTION!="add", GOTO="drivers_end"

ENV{MODALIAS}=="?*", RUN{builtin}+="kmod load $env{MODALIAS}"
SUBSYSTEM=="tifm", ENV{TIFM_CARD_TYPE}=="SD", RUN{builtin}+="kmod load tifm_sd"
SUBSYSTEM=="tifm", ENV{TIFM_CARD_TYPE}=="MS", RUN{builtin}+="kmod load tifm_ms"
SUBSYSTEM=="memstick", RUN{builtin}+="kmod load ms_block mspro_block"
SUBSYSTEM=="i2o", RUN{builtin}+="kmod load i2o_block"
SUBSYSTEM=="module", KERNEL=="parport_pc", RUN{builtin}+="kmod load ppdev"
KERNEL=="mtd*ro", ENV{MTD_FTL}=="smartmedia", RUN{builtin}+="kmod load sm_ftl"

LABEL="drivers_end"

ルールファイルはキーバリューの変数をベースにした判定システムです。変数にはカーネルからueventと渡されるものだけでなく、前のルールで設定された値もあります。また「#」で始まる行や空行は無視されます。

変数には「マッチングに使える変数」「値を代入できる変数」の2種類が存在します。マッチング変数の判定をカンマで連結することで、すべてにマッチする場合のみ、変数に値が代入されます。変数によっては代入された値によって特定の処理を実行します。

ルールは1行ずつ処理されていきます。上記ファイルの内容を順番に見ていきましょう。

ACTIONとGOTO変数

ACTION!="add", GOTO="drivers_end"

上記は「ACTIONがaddでなければ、GOTOで指定されたdrivers_endラベルにジャンプする」です。前者がマッチングに使われ、後者に値を代入しています。

「ACTION」は代表的なマッチング変数の一つです。この変数はカーネル内部で次のいずれかがセットされます。

  • add:デバイスの追加
  • remove:デバイスの除去
  • change:デバイスの状態変更
  • move:デバイスの親子関係や名前の変更
  • online:デバイスがオンラインになった
  • offline:デバイスがオフラインになった
  • bind:ドライバーとデバイスが紐付けられた
  • unbind:ドライバーとデバイスの関連付けを切断

大抵のルールはaddとremoveのみを考慮しています。それ以外については必要になったときに調べると良いでしょう。

「GOTO」は制御構造を作るときに使われます。特定の条件のとき(今回はデバイスが追加されたとき)のみルールを適用したい場合に、条件にマッチしなければGOTOでスキップするのに用います。また、1行のうちカンマで区切られたマッチング変数は「AND」で連結されます。もし「OR」のような制御をしたい場合は、GOTOが必要になります。

ENVとRUN変数

前項のACTIONとGOTOのおかげで、これ以降はデバイスが追加されたときのみ反映されるルールとなります。最初は、モジュールのロードを行う部分です。

ENV{MODALIAS}=="?*", RUN{builtin}+="kmod load $env{MODALIAS}"

ENVはカーネルドライバーの中や他のルールで設定されるデバイスプロパティです。連想配列的にアクセス可能なので、上記の前半は「ENVのMODALIASに何か値が設定されていたとき」となります。このように右辺のダブルクオートの中はシェルグロブのような簡易的なパターンを記述可能です。

RUNは追加したコマンドを実行する変数です。⁠kmod load $env{MODALIAS}」が実行されるということですね。⁠+=」とすることでこれまで他のルールが追加した値に追記することになります。ちなみにRUNが評価される(内部のコマンドが実行される)のは、すべてのルールが評価されたあとです。

「builtin」はudevコマンド内部のビルドインコマンドを実行することを意味します。実はシステムには/bin/kmodが存在しますが、この/bin/kmodにはloadサブコマンドが存在しません。udevのビルトインコマンドであるkmodにのみloadオプションが存在するのです。

変数の値の中で他の変数を参照できます。上記の例だと「$env{MODALIAS}」の部分ですね。他にもいくつかの参照変数が存在しますので、詳しいことはudevのmanページを参照してください。

MODALIASには「acpi:(PNP ID⁠⁠」や「i2c:foo⁠⁠、⁠usb:v(ベンダーID)p(プロダクトID)...」といった文字列が渡されます。これらの文字列と「/lib/modules/(カーネルバージョン)/modules.alias」の内容から、適切なモジュールがロードされるというわけです。

SUBSYSTEMとKERNEL、LABEL変数

ここまでの基本をおさえておくと、あとは直感的に読めるようになります。

SUBSYSTEM=="tifm", ENV{TIFM_CARD_TYPE}=="SD", RUN{builtin}+="kmod load tifm_sd"
SUBSYSTEM=="tifm", ENV{TIFM_CARD_TYPE}=="MS", RUN{builtin}+="kmod load tifm_ms"
SUBSYSTEM=="memstick", RUN{builtin}+="kmod load ms_block mspro_block"
SUBSYSTEM=="i2o", RUN{builtin}+="kmod load i2o_block"
SUBSYSTEM=="module", KERNEL=="parport_pc", RUN{builtin}+="kmod load ppdev"
KERNEL=="mtd*ro", ENV{MTD_FTL}=="smartmedia", RUN{builtin}+="kmod load sm_ftl"

SUBSYSTEMはそのデバイスが所属しているサブシステムです。⁠tifm」はTexas InstrumentsのFlash Mediaコントローラー向けのサブシステムで、接続されたメディアの種別によってロードするモジュールを変えているようです。⁠memstick」はソニーのメモリースティックですね。

「i2o」Intelligent Input/Output (I2O)なるデバイス向けのようですが、Linux Kernel 4.2あたりでで削除されたので今となってはもう意味はありません。

「module」はカーネルモジュール全般の操作のためのサブシステムです。その先のKERNEL変数は、そのデバイスのカーネル上の名前(⁠⁠sda1」とか「eth0」とか)を表します。おそらく読者の大抵のPCでは、parport_pcモジュールとppdevモジュールがロードされるのではないでしょうか。最後はMTDデバイスが「SmartMedia/xD FTL」デバイスだったらsm_ftlモジュールをロードしています。

SUBSYSTEM変数とは別にSUBSYSTEMS変数もあります。後者の変数には、そのデバイスがつながっているサブシステムのさらに祖先のサブシステムがすべて記録されています。

ファイルの最後はLABEL変数です。GOTOはLABEL変数がセットされている値と一致する場所までジャンプします。

LABEL="drivers_end"

アクセス権の変更

udevでルールを記述する場合の代表的な例がアクセス権限の変更です。

USBなどのデバイスをホットプラグしたものの、rootでしか読み書きできないデバイスにsudo経由で実行したという経験がある読者もいることでしょう。udevを使うと、新規に生成されたデバイスのアクセス権限を変更できます。

たとえばUSBシリアルコンソールデバイスを接続したときを考えてみましょう。一般的なUSBシリアルコンソールデバイスなら接続すると同時に「/dev/ttyUSB0」が作られます。このデバイスファイルはオーナーがroot、グループがdialoutで、パーミッションが「0660」です。つまりrootかdialoutグループに所属するユーザーしか読み書きできません。

$ ls -l /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 0  2月  3 19:48 /dev/ttyUSB0

そして一般ユーザーは、初期設定だとdialoutグループには所属していません。もし/dev/ttyUSB0を読み書きしたいなら、次のようにdialoutグループに所属する必要があります。

$ sudo adduser $USER dialout

ちなみにグループ変更の設定を反映するためには、一度ログインしなおしてください。

さて、実はシリアルコンソールデバイスに関してはすでにルールファイル「/lib/udev/rules.d/60-serial.rules」が存在します。今回の説明に関連しそうなところを抜粋すると次のような内容です。

ACTION=="remove", GOTO="serial_end"
SUBSYSTEM!="tty", GOTO="serial_end"

SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb"
SUBSYSTEMS=="usb-serial", ENV{.ID_PORT}="$attr{port_number}"
SUBSYSTEMS=="usb", ENV{ID_USB_INTERFACE_NUM}="$attr{bInterfaceNumber}"

LABEL="serial_end"

最初の2行からこのルールは切断されたとき以外でなおかつサブシステムがttyのときのみ適用されることがわかります。またusbサブシステムやusb-serialサブシステムの下に接続されている場合は、各種デバイスプロパティが追加設定されることもわかります。

ポイントはIMPORT{builtin}の部分です。IMPORT変数は指定した値を「実行し、その結果をデバイスプロパティとして取り込む」変数です。builtinはRUNと同様に、udevのビルトインコマンドを意味します。usb_idはUSBのベンダーIDやモデルIDなどを出力します。

hwdbはもう少し複雑です。実はシステム上には「/lib/udev/hwdb.d/」などにハードウェア情報を格納するデータベースが存在します。このデータベースにはベンダーIDに紐づく実際のベンダー名などの汎用的な情報やタッチパッドの座標設定のようなデバイス固有の情報などがテキスト形式で保存されているのです。hwdbを使えば、ハードウェア固有の雑多な情報をudevのコードの外に記述できるというわけです。

ちなみに次のようにsysfs上のパスを指定することで、実際に得られる値を参照できます。

$ udevadm test-builtin usb_id /sys/class/tty/ttyUSB0
calling: test-builtin
Load module index
Parsed configuration file /lib/systemd/network/99-default.link
Created link configuration context.
/sys/devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1:1.0: if_class 255 protocol 0
ID_VENDOR=FTDI
ID_VENDOR_ENC=FTDI
ID_VENDOR_ID=0403
ID_MODEL=TTL232R-3V3
ID_MODEL_ENC=TTL232R-3V3
ID_MODEL_ID=6001
ID_REVISION=0600
ID_SERIAL=FTDI_TTL232R-3V3_FTGC0U6E
ID_SERIAL_SHORT=FTGC0U6E
ID_TYPE=generic
ID_BUS=usb
ID_USB_INTERFACES=:ffffff:
ID_USB_INTERFACE_NUM=00
ID_USB_DRIVER=ftdi_sio
Unload module index
Unloaded link configuration context.

さてシリアルコンソールデバイスに関する諸々の設定は「60-serial.rules」で設定されていることがわかりました。そこで追加の設定は「/etc/udev/rules.d/65-serial.rules」に保存することにしましょう。アクセス権限に関する設定は次の3つです。

  • OWNER:所有者の設定
  • GROUP:所有グループの設定
  • MODE:パーミッションの設定

上記3つをどう設定するかはケースバイケースでしょう。よく例としてあがるのが、GROUPを使いたいユーザーが所属しているグループに設定し、パーミッションを「0660」にする設定です。特にシリアルコンソールデバイスとしてdialoutグループが使われることが多いようです。

そういえば前に出力していた/dev/ttyUSB0は、dialoutグループになっていましたね。話をひっくり返すようですが、実は上記の設定はすでに「/lib/udev/rules.d/50-udev-default.rules」に存在します。

SUBSYSTEM=="tty", KERNEL=="ptmx", GROUP="tty", MODE="0666"
SUBSYSTEM=="tty", KERNEL=="tty", GROUP="tty", MODE="0666"
SUBSYSTEM=="tty", KERNEL=="tty[0-9]*", GROUP="tty", MODE="0620"
SUBSYSTEM=="tty", KERNEL=="sclp_line[0-9]*", GROUP="tty", MODE="0620"
SUBSYSTEM=="tty", KERNEL=="ttysclp[0-9]*", GROUP="tty", MODE="0620"
SUBSYSTEM=="tty", KERNEL=="3270/tty[0-9]*", GROUP="tty", MODE="0620"
SUBSYSTEM=="vc", KERNEL=="vcs*|vcsa*", GROUP="tty"
KERNEL=="tty[A-Z]*[0-9]|ttymxc[0-9]*|pppox[0-9]*|ircomm[0-9]*|noz[0-9]*|rfcomm[0-9]*", GROUP="dialout"

この最後の行が効いていたために、ttyUSB0のグループが変更されていたのです。それに対してMODEはtty0などしか変更されないようになっています。というわけで、MODEだけ誰でもアクセスできるように変更してみましょう[3]⁠。

設定例はいくつかあります。まずシンプルにカーネル上のファイル名に合わせて一括変換する方法です。

ACTION=="remove", GOTO="serial_end"
SUBSYSTEM!="tty", GOTO="serial_end"
KERNEL=="ttyUSB[0-9]*", MODE="0666"
LABEL="serial_end"

ttyUSBxすべてではなく、特定のUSBデバイスのみ設定を変更したいなら、次のようにベンダーIDなどでマッチングルールを記述すると良いでしょう。

ACTION=="remove", GOTO="serial_end"
SUBSYSTEM!="tty", GOTO="serial_end"
ENV{ID_VENDOR_ID}=="0403", ENV{ID_MODEL_ID}=="6001", MODE="0666"
LABEL="serial_end"

上記ではベンダーIDとモデルIDで絞っています。これらの値はたとえばlsusbなどで確認できます。

$ lsusb
(中略)
Bus 003 Device 004: ID 0403:6001 Future Technology Devices International, Ltd FT232 USB-Serial (UART) IC

FTDIのチップが載った一般的なUSBシリアルコンソールケーブルです。ルールを追加したら、udevにルールセットの再読込を指示します。

$ sudo udevadm control --reload

さらにUSBシリアルコンソールを接続し直しましょう。

$ ls -l /dev/ttyUSB0
crw-rw-rw- 1 root dialout 188, 0  2月  3 20:11 /dev/ttyUSB0

これでシリアルコンソールを使うために、screenやminicomやkermitをユーザー権限で実行できるようになりました。

デバイスに対するシンボリックリンクの追加

もう一つの例が、特定のデバイスファイルに対するシンボリックリンクの追加です。

「/dev」以下のデバイスファイルは、一般的にはカーネルが認識順に付けた名前で作られます。たとえば最初に認識したSATAやUSBマスストレージの名前は「/dev/sda」ですし、次に認識すると「/dev/sdb」と名付けられます。ノートPCにも組み込まれることが増えてきたMMCブロックデバイスは「/dev/mmcblk0」⁠/dev/mmcblk1」と増えていきますし、NMVeならコントローラーごとに「/dev/nvme0」⁠/dev/nmve1」となり、さらにコントローラーのノードは「/dev/nvme0n1」などの数字が付けられます。

ポイントは「認識順」であることです。何がしかの理由で起動ごとに認識順が入れ替わってしまう場合、このデバイスファイル名は起動ごとに変わってしまうことになります。

そこで必要になるのが起動ごとに固有の名前になる「永続的なデバイス名」です。たとえば最近のUbuntuだと、ネットワークインターフェース名が「eth0」のような名前から、次のような固有の名前に変更されました。

  • enp0s25:Ethernet(en⁠⁠、PCIのBus 0、Slot 25(p0s25)
  • enx001de1570857:Ethernet(en⁠⁠、MACアドレス(xMACADDR)
  • wlp3s0:Wireless LAN(wl⁠⁠、PCIのBus 3、Slot 0(p3s0)

他にもACPIテーブル上にデバイスインデックスが付いていたら「eno1」になります。このようにPCIバスのどこに接続されているかやMACアドレスがなんであるかなどの、固有の情報を元に名前が付けれるのです。

実はすでに既存のルールで、デバイス固有の情報に基づいたシンボリックリンクが「/dev/サブシステム/by-タイプ/」以下に作られていいます。たとえば先程のシリアルコンソールデバイスだと次のようなシンボリックリンクが作られているのです。

$ ls -l /dev/serial/by-id/
lrwxrwxrwx 1 root root 13  2月  3 20:11 usb-FTDI_TTL232R-3V3_FTGC0U6E-if00-port0 -> ../../ttyUSB0
$ ls -l /dev/serial/by-path/
lrwxrwxrwx 1 root root 13  2月  3 20:11 pci-0000:00:14.0-usb-0:1:1.0-port0 -> ../../ttyUSB0

ただし若干長いですね。そこで「/dev/ftdi」をシンボリックリンクに追加してみましょう。先程作成した「/etc/udev/rules.d/65-serial.rules」を次のように変更します。

ACTION=="remove", GOTO="serial_end"
SUBSYSTEM!="tty", GOTO="serial_end"
ENV{ID_VENDOR_ID}=="0403", ENV{ID_MODEL_ID}=="6001", MODE="0666", SYMLINK+="ftdi"
LABEL="serial_end"

SYMLINK変数にパス名を追加すると、⁠/dev/」以下のその名前でシンボリックリンクを作ってくれます。他のルールがSYMLINK変数を設定しているため、⁠+=」で値を追加しています。このようにSYMLINKには複数の値を入れておくと、その数だけシンボリックリンクを作ってくれるのです。

設定をリロードした上で、デバイスを再度つなげてみましょう。次のようにシンボリックリンクが作られていたら成功です。

$ ls -l /dev/ftdi
lrwxrwxrwx 1 root root 7  2月  3 20:21 /dev/ftdi -> ttyUSB0

さらにデバイスを取り外すと、シンボリックリンクが消えていることでしょう。

このようにudevのルールファイルを書けば、デバイス認識時の挙動を細かく設定できます。特にUSBやThunderbolt、Bluetoothなどのホットプラガブルなデバイスを使うときに効力を発揮することでしょう。より詳細に設定するために、まずはudevのmanページを参照してください。udevadmなどの便利ツールもありますが、これについてはまた別の機会にお話することにしましょう。

おすすめ記事

記事・ニュース一覧