デバイスドライバモデルとカーネルモジュールの仕組み
ホットプラグでデバイスが勝手に使えるのも、modprobe 一発で機能が増えるのも、device・driver・bus の三角形と参照カウントが支えています。sysfs の階層からモジュールのシンボル解決まで内側を解き明かします。
- 1.Linux のドライバモデルは device・driver・bus の三者を struct で結び、bus の match 関数でドライバとデバイスを突き合わせて probe を呼ぶ。この対応関係が sysfs に階層として現れる。
- 2.ホットプラグはカーネルが uevent を発行し、udev がそれを受けてデバイスノード作成やモジュール自動ロードを行う。modalias とエイリアスの照合で必要なモジュールが特定される。
- 3.カーネルモジュールはロード時に未解決シンボルをカーネルのシンボルテーブルへ解決し、参照カウント(module refcount)が 0 になるまでアンロードできない。EXPORT_SYMBOL が外部公開の境界を決める。
ドライバモデルは device・driver・bus の三角形
Linux カーネルがあらゆる種類のハードウェアを統一的に扱えるのは、デバイス・ドライバ・バスを別々のオブジェクトとして表現し、それらを結びつける「統一ドライバモデル(unified device model)」があるからです。核心は3つの構造体です。
struct device:ハードウェア(または論理デバイス)1つを表す実体。struct device_driver:そのデバイスを操作するコード(ドライバ)を表す。struct bus_type:device と driver を仲介する「接続規格」。PCI・USB・I2C・platform などがそれぞれ1つの bus。
ポイントは、device と driver が直接結びつかない ことです。両者は必ず bus を介して対応づけられます。bus が持つ match 関数が「この driver はこの device を担当できるか」を判定し、合致すると bus の probe(最終的に driver の probe)が呼ばれてドライバがデバイスを初期化します。逆にデバイスが消えると remove が呼ばれます。
同じ USB ドライバが、ポートに挿された個体が違っても再利用できるのは、driver(コード)と device(個体)が分離されているからです。bus を挟むことで、PCI と USB のように接続規格ごとに「どう一致を判定するか」「どう列挙するか」を差し替えられます。新しいバス規格を足すときも、既存の device/driver 抽象には手を入れずに済みます。
マッチングの実体:id_table と modalias
bus の match 関数が何を見るかは規格ごとに違いますが、定番は ID テーブルの照合 です。PCI なら (vendor, device) の組、USB なら (idVendor, idProduct) やクラスコード。各ドライバは自分が対応する ID の一覧を id_table として持ち、デバイス側の ID と突き合わせます。
この ID 情報はモジュールのバイナリにも MODULE_DEVICE_TABLE マクロ経由で埋め込まれ、ビルド時に modalias という文字列パターンへ変換されます。たとえば PCI デバイスなら次のような形式です。
pci:v00008086d000010D3sv*sd*bc*sc*i*
└ vendor 0x8086, device 0x10D3, それ以下はワイルドカード
カーネルは検出したデバイスごとに、その素性を表す modalias 文字列(/sys/.../modalias で読める)を作ります。あとはこのデバイス由来の modalias とモジュール由来のエイリアスパターンを照合すれば、どのモジュールを読み込めばよいかが一意に決まります。これがホットプラグ時の自動ロードの土台になります。
sysfs:オブジェクトの階層をファイルで見せる
ドライバモデルの内部状態は、/sys にマウントされる sysfs を通して可視化されます。sysfs は実在のデータを持たない仮想ファイルシステムで、VFS 抽象化レイヤ の上に構築され、カーネル内のオブジェクトとディレクトリ/ファイルを1対1に対応づけます。
中核にあるのが kobject です。すべての device・driver・bus は内部に kobject を埋め込んでおり、これが sysfs のディレクトリ1つに対応します。kobject 同士の親子関係がそのまま sysfs の階層になり、属性(attribute)が個々のファイルになります。
| sysfs パス | 表すもの | 内側のオブジェクト |
|---|---|---|
| /sys/devices/ | デバイスの物理的な接続ツリー(唯一の実体) | struct device の kobject |
| /sys/bus/<bus>/devices/ | バスごとのデバイス一覧(symlink) | device への参照 |
| /sys/bus/<bus>/drivers/ | バスに登録されたドライバ一覧 | device_driver |
| /sys/class/<class>/ | 機能カテゴリ別の論理ビュー(net, block 等) | device への symlink |
重要なのは、実体は /sys/devices/ の物理ツリーただ1つ で、/sys/bus/ や /sys/class/ はそこへの symlink による「別の見え方」だという点です。同じデバイスを「どのバスに繋がっているか」「何の機能を提供するか」という複数の軸から引けるよう、ビューを多重化しているわけです。kobject には参照カウントが組み込まれており、誰かが参照している間はオブジェクトが解放されない(=sysfs エントリが消えない)ことが保証されます。
ホットプラグと udev:uevent からデバイスノードまで
デバイスの抜き挿し(ホットプラグ)に追従する仕組みが uevent と udev です。流れはこうです。
- デバイスが現れる/消えると、カーネルは該当
kobjectについてuevent(add/remove/changeなどのアクション + 環境変数群)を生成する。 - この uevent が netlink ソケット(カーネル⇔ユーザー空間のメッセージ経路)を通じてユーザー空間へ届く。
- ユーザー空間の
udevd(systemd-udevd)が uevent を受け取り、/etc/udev/rules.dなどのルールに従って処理する。
udev が行う代表的な仕事は2つです。第一に、/dev 配下の デバイスノード(/dev/sda のようなキャラクタ/ブロック特殊ファイル)を、カーネルが割り当てた (major, minor) 番号に合わせて作成・命名すること。第二に、uevent に含まれる MODALIAS を見て、対応するモジュールを modprobe で 自動ロード することです。
キャラクタ/ブロックデバイスは (major, minor) の番号対で識別されます。major はドライバ(種別)、minor は同じドライバが扱う個々のデバイスを区別します。アプリが /dev/sda を open すると、VFS はこの番号からドライバの file_operations を引き、read/write を該当ドライバへ橋渡しします。番号とドライバの結びつきは register_chrdev 系の登録で作られます。
ブート初期にはまだ udev が動いていないため、カーネルは devtmpfs を使って最低限のデバイスノードを自前で作ります。その後 udev が起動し、ルールに基づいた整備(永続的な命名やシンボリックリンク付与)を引き継ぎます。起動プロセス で initramfs 内の udev がルートデバイスを認識できるのは、この連携のおかげです。
カーネルモジュール:ロードとシンボル解決
ドライバの多くは、起動時に常駐させるのではなく カーネルモジュール(.ko)として必要時にロードされます。モジュールは独立した ELF オブジェクトで、ロード時にカーネル本体や他のモジュールが公開するシンボルを参照します。ここで 動的リンク と似た「シンボル解決」が、カーネル空間で起こります。
insmod/modprobe が init_module(2)(実際は finit_module)でモジュールをカーネルに渡すと、カーネルは次を行います。
- ELF を検証してメモリ領域へ配置し、署名と
vermagic(コンパイル時のカーネルバージョン・構成)の整合を確認する。署名強制が有効なら不正署名はここで弾かれる。 - モジュール内の未解決シンボルを、カーネルのシンボルテーブル(
ksymtab)から探して**再配置(relocation)**する。見つからないシンボルが1つでもあればロードは失敗(Unknown symbolエラー)。CONFIG_MODVERSIONS有効時は、シンボルごとの CRC を突き合わせて ABI 不一致も検出する。 - モジュールの
init関数(module_initで登録)を呼ぶ。多くの場合ここでドライバ登録(pci_register_driver等)が走り、ドライバモデルへ組み込まれる。
モジュールが外部に公開する関数・変数は EXPORT_SYMBOL(または GPL 限定の EXPORT_SYMBOL_GPL)で明示されたものだけです。これがカーネル内 API の境界線で、export されていない内部関数は他モジュールから解決できません。
EXPORT_SYMBOL_GPL で公開されたシンボルは、MODULE_LICENSE が GPL 互換と宣言されたモジュールからしか解決できません。プロプライエタリなドライバが GPL-only シンボルを使おうとするとロード時に拒否されます。これはライセンス境界を技術的に強制する仕掛けで、単なる慣習ではなくカーネルが実際にチェックしています。
依存関係と参照カウント:安全なアンロード
モジュール同士には依存があります。あるモジュールが別モジュールの export シンボルを使えば、前者は後者に依存します。この依存グラフは depmod が modules.dep に書き出し、modprobe はそれを読んで依存モジュールを正しい順序で先にロードします(insmod は単体ロードのみで依存解決しません)。
アンロードの安全性を支えるのが モジュール参照カウント(module refcount) です。原理はこうです。
- あるモジュールが提供する資源を誰かが使い始めると、
try_module_get()でそのモジュールの refcount が増える。 - 使い終わると
module_put()で減る。 - refcount が 0 でない限り、
rmmodは失敗する(使用中のモジュールは外せない)。
たとえばファイルシステムドライバのモジュールは、そのファイルシステムが1つでもマウントされている間は refcount が正になり、アンロードできません。lsmod の出力にある "Used by" 列は、まさにこの参照状況を表しています。さらに B が A に依存している(A のシンボルを使う)場合、B がロードされている間は A の refcount も保たれ、A を先に外すことはできません。
ドライバが try_module_get/module_put の対を誤ると、深刻なバグになります。put し忘れれば refcount が下がらず永久にアンロード不能になり、逆に使用中なのに get し損ねれば、利用中のモジュールが rmmod されコードが消えた領域へジャンプしてカーネルがクラッシュします(use-after-free)。割り込みハンドラを登録するドライバが、ハンドラ実行中にアンロードされない保証もこの参照カウントが担います(関連: 割り込みの top/bottom half)。
押さえどころは「device・driver・bus の三角形と、bus の match → probe」という対応の流れ。sysfs は「kobject の階層を VFS 上にファイルとして写したもので、実体は /sys/devices、bus/class は symlink ビュー」。ホットプラグは「カーネルが uevent を netlink で発行 → udev が受けてノード作成と modalias によるモジュール自動ロード」。モジュールは「ロード時に未解決シンボルを ksymtab で解決、EXPORT_SYMBOL が公開境界、refcount が 0 でないとアンロード不可」。この4点を一息で言えれば十分です。
まとめ
Linux のデバイスドライバモデルは、device(個体)・driver(コード)・bus(仲介規格)を分離し、bus の match 関数で対応づけて probe を呼ぶ という設計に貫かれています。その対応関係は kobject の階層として sysfs に写し出され、/sys/devices の物理ツリーを bus/class の symlink で多視点に見せます。ホットプラグでは、カーネルが uevent を netlink 経由で投げ、ユーザー空間の udev がデバイスノード作成と modalias によるモジュール自動ロードを担います。そして機能を運ぶカーネルモジュールは、ロード時に シンボル解決(EXPORT_SYMBOL が公開境界)を行い、参照カウントが 0 になるまで安全にアンロードできない——この一連の仕組みが、無数のハードウェアを統一的かつ動的に扱う土台になっています。
OS Article
デバイスドライバモデルとカーネルモジュールの仕組みを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
デバイスドライバ
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
ホットプラグはカーネルが uevent を発行し、udev がそれを受けてデバイスノード作成やモジュール自動ロードを行う。modalias とエイリアスの照合で必要なモジュールが特定される。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「デバイスドライバ / カーネルモジュール」に近いか確認する。
- 強みである「Linux のドライバモデルは device・driver・bus の三者を struct で結び、bus の match 関数でドライバとデバイスを突き合わせて probe を呼ぶ。この対応関係が sysfs に階層として現れる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。