デバイスモデル

このページの目次

デバイスが sysfs に現れていても、ドライバがバインドされていないことがあります。また、ドライバのアンバインド後、デバイスオブジェクトがまだ参照されている可能性があります。デバイスモデルを読む際には、⁠検出、マッチング、機能登録、そして最終的な解放の各段階に焦点を当てて確認する必要があります。

確認目標読むべき箇所
オブジェクトと probe第 1~2 節
参照と managed リソース第 3 節
ユーザ空間のビューと読み取り専用実験第 4~5 節

1. 4つのオブジェクトがそれぞれ解決する問題

オブジェクト問われる問題例
deviceこれはどのデバイスか、親デバイスは誰か、どのようなリソースがあるか?1つのPCI function、1つのUSB interface
device_driverそれを操作できるのはどの実装か?PCIネットワークカード用ドライバ、USBシリアル用ドライバ
bus_typeデバイスとドライバをどのように発見し、マッチングさせ、整理するか?PCI、USB、platform
classユーザはどのような機能の観点からデバイスを探すか?net、block、tty

バスとクラスは異なる視点です。PCIネットワークカードはまずPCIバス上のデバイスであり、そのネットワークインターフェースはネットワークサブシステムを通じて機能オブジェクトを提供します。/sys/class/net/eth0 を単なるPCIデバイスオブジェクトの別名として扱うことはできません。1つの物理デバイスが複数の機能オブジェクトを公開することもあります。

kobject はより低レベルな命名、階層、参照カウントの基盤であり、device はこれを内包しています。kset は関連する kobject を整理し、イベント管理にも参加します。これらはバスモデルを代替するドライバインターフェースのセットではありません。通常、既存のサブシステムが提供する device API を使用すべきであり、ドライバのために独自に kobject ツリーを構築すべきではありません。

カーネル構造体はバージョンによって変化します。include/linux/device.h、include/linux/kobject.h を読む際には、メンバの役割に注目してください。kobject.sd は kernfs ノードを指しており、VFS の dentry ではありません。オブジェクトの最終的な解放は release コールバックとそのコンテナ管理に依存しており、参照カウントがゼロになっても、外側の構造体をどのように解放すべきかを自動的に推測することはできません。

2. ハードウェア情報から probe へ

発見、バインド、解放は3つの異なる段階 デバイス登録 列挙またはファームウェア記述 バスマッチング ID / compatible / HID ドライバ登録 ビルトインまたはモジュールロード probe:リソースの取得と初期化 依存関係が未準備の場合はプローブを遅延可能 デバイスがサブシステムで利用可能に remove:停止とアンバインド 最後の参照解放でオブジェクト解放 アンバインドは即時解放を意味しません。sysfs で見えるからといって、デバイスが利用可能であるとは限りません。

デバイスが先に登録されても、ドライバが先に登録されても、マッチングをトリガーできます。バスがマッチングルールを担当します。PCI では通常 vendor/device/class が使用され、USB ではデバイスまたはインターフェース記述子によるマッチングが可能です。platform ドライバはファームウェアマッチングテーブルなどの経路でマッチングできます。

match は候補ドライバがデバイスをサポートする可能性があることを示すだけです。probe ではさらに、クロック、電源、レジスタ、割り込みなどのリソースを取得し、ハードウェアを初期化し、機能サブシステムに登録する必要があります。どの段階でも失敗した場合、実行中の半初期化状態のデバイスを残してはいけません。

regulator、clock、またはその他のサプライヤがまだ準備できていない場合は、-EPROBE_DEFER を返すことができます。コアは後で条件が満たされたときに再試行します。これは依存関係が一時的に不足していることを示すものであり、永久的にサポートされていないハードウェアやエラーパラメータを隠すために使用してはいけません。デバイスのリンクはサプライヤ/コンシューマ関係を表現し、プローブと電源管理の順序に影響を与えます。

遅延プローブは初期化の再試行であり、半分の初期化の継続ではない

センサーのコンシューマが列挙されているが、それが参照する電源レギュレータのドライバがまだ登録を完了していないとします。最初の probe でプライベートコンテキストが構築されていた場合、レギュレータの取得時に -EPROBE_DEFER が返されることがあります。この probe では、現在の試行で確保されたリソースを退出してクリーンアップする必要があります。依存関係の変化によって再度プローブが行われる際には、一貫した未バインドの状態から再開する必要があります。

時点コンシューマの状態本次試行のリソース
列挙済み、未プローブデバイスは存在するが未バインド成功したドライバインスタンスなし
最初の probe でサプライヤ取得失敗EPROBE_DEFER を返し、バインドはまだ完了していない本次の managed リソースを回収し、手動リソースも処理する必要がある
サプライヤが後で準備完了後続のプローブパスをトリガー可能以前のプライベートバッファがまだ有効であると仮定してはならない
後続の probe 成功ドライババインドと機能登録を完了新しい試行のリソースはアンバインドまで保持される

したがって、probe ログが繰り返し表示されるのは依存関係の再試行である可能性があり、必ずしもデバイスが複数回列挙されたわけではありません。逆に、誤ったファームウェアパラメータやサポートされていないデバイスを一律に EPROBE_DEFER に書き換えると、実際のエラーが永遠に依存関係が不足しているように見えてしまいます。ドライバはリソースAPIのエラー意味を保持し、デバイスリンク、遅延理由、および元の戻り値を組み合わせて特定する必要があります。初期化の順序が偶然成立するように固定の sleep 時間を追加してはいけません。

ファームウェア記述とバス列挙の連携

  • Device Tree:アドレス、割り込み、クロック、デバイス接続などの情報を記述し、compatible は互換性のあるインターフェースを示します。ARM、RISC-V で一般的ですが、これらのアーキテクチャに固有の形式ではありません。
  • ACPI:デバイスID、リソース、電源管理方法などを提供します。x86 で一般的ですが、arm64 サーバーでも ACPI が使用されます。
  • PCI/USB 列挙⁠:バスプロトコルを通じてデバイスを見つけ、ID を読み取ります。PCI ルートブリッジ、設定空間アクセス方法、割り込みルーティングなどは、ACPI や DT に依存する可能性があり、「自己列挙であれば完全にファームウェア記述を必要としない」と一般化することはできません。

platform bus は、これらのバスプロトコルで自己列挙できないオンチップデバイスに一般的に使用されます。platform device はデバイスモデル内の実際のオブジェクトであり、ハードウェアが模擬されていることを意味するものではありません。DT ノードがすべて platform device に変換されるわけではなく、ノードが存在するバスがインスタンス化方法を決定します。

3. オブジェクトの寿命とドライババインドの寿命

get_device() / put_device() はデバイスの参照を管理します。device_unregister() は登録を取り消し、登録者の参照を解放します。まだ参照が残っている場合、オブジェクトは存続し続け、最後の put のみで最終的な解放に入ります。

境界終了するのは何か
removeドライバサービスとバインド関係
releaseデバイスオブジェクト自体のメモリ寿命
参照が残っているオブジェクトのメモリが存続し続けることを示すだけ

参照は、取り外されたハードウェアをオンラインに戻すものでもなく、レジスタにアクセスできることを保証するものでもありません。

device_register() には初期化と追加のステップが含まれます。登録に失敗した場合でも、呼び出し元は通常、初期化によって取得した参照を返すために put_device() が必要であり、デバイスモデル管理に渡されたオブジェクトを安易に kfree() してはいけません。具体的なコンテナの割り当てと release ルールは、所属するサブシステムに従うべきです。

struct device を守っても、ドライバのプライベートデータまでは守れない

登録者がデバイスの参照を保持し、非同期利用者がさらに参照を取得しているとします。ドライバのアンバインド時、devm によるプライベートメモリは解放される可能性がありますが、非同期利用者が保持している device 参照によりデバイスオブジェクト自体は存続し続けます。非同期利用者が古い drvdata ポインタを保存し、その後で間接参照を行うと、すでに解放されたドライバデータにアクセスしてしまう可能性があります。

参照はデバイスオブジェクトを保護するが、アンバインドされたドライバリソースの寿命を延ばすことはできない バインド中 アンバインド完了 登録取消 最後の put 参照 2 プライベートデータ有効 参照 2 プライベートデータ解放済み 参照 1 オブジェクトは存続中 参照 0 release 実行 古い drvdata ポインタは、device 参照を保持し続けていても有効になり続けるわけではない これは登録者と外部保持者のみを示しており、カーネル内部の他の一時参照は含んでいません。

以下では、登録者と1つの外部保持者の参照のみをカウントし、カーネル内部のすべての一時参照を再現しません。状態モデルは、登録、バインド、プライベートリソース、オブジェクト解放を分離しており、どの保証がどのステップで終了するかを観察しやすくしています。

class Device:
    def __init__(self):
        self.refs = 1  # 初期化によって取得した参照。登録者が返却する責任を持つ
        self.registered = False
        self.bound = False
        self.private_live = False
        self.releases = 0

    def get_from_live_reference(self):
        if self.refs == 0:
            raise RuntimeError("解放済みのオブジェクトを復活させることはできない")
        self.refs += 1

    def put(self):
        assert self.refs > 0
        self.refs -= 1
        if self.refs == 0:
            assert not self.registered and not self.bound
            self.releases += 1

    def add_and_bind(self):
        assert self.refs > 0 and not self.registered
        self.registered = self.bound = self.private_live = True

    def unbind(self):
        assert self.bound
        self.bound = False
        self.private_live = False

    def unregister(self):
        assert self.registered
        if self.bound:
            self.unbind()
        self.registered = False
        self.put()

    def io(self):
        if not self.bound or not self.private_live:
            raise RuntimeError("オブジェクト参照は、ドライバがまだサービスを提供できることを意味しない")
        return "ok"

dev = Device()
dev.add_and_bind()
dev.get_from_live_reference()
assert dev.refs == 2 and dev.io() == "ok"
dev.unbind()
assert dev.registered and dev.refs == 2 and not dev.private_live
try:
    dev.io()
except RuntimeError:
    pass
else:
    raise AssertionError("アンバインド後、参照に依存してドライバにアクセスすることはできない")
dev.unregister()
assert dev.refs == 1 and dev.releases == 0
dev.put()  # 最後の外部参照を返却
assert dev.refs == 0 and dev.releases == 1
try:
    dev.get_from_live_reference()
except RuntimeError:
    pass
else:
    raise AssertionError("終了したライフサイクルを get で再開することはできない")

failed_add = Device()  # 初期化は完了したが、追加ステップで失敗
failed_add.put()
assert failed_add.releases == 1

実際の get_device() は参照操作のみを行い、呼び出し元に代わって、すでに無効になっている可能性のある生ポインタから安全にオブジェクトを取り戻すことはありません。最初に参照を取得するには、ロックで保護された検索、既存の参照、または所属サブシステムが規定する取得プロトコルに依存する必要があります。すでに取得した参照はオブジェクトのメモリ寿命を保護しますが、アンバインドの同期やプライベート状態自体の参照ルールを代替するものではありません。

ローカルカーネルの実装では、drivers/base/core.c の device_unregister で、まず device_del、次に put_device が行われるのを見ることができます。アンバインドのクリーンアップは drivers/base/dd.c に含まれており、devres_release_all と drvdata のクリアが含まれます。2つのパスの責任は異なるため、ドライバ設計では非同期利用者がどのレベルのオブジェクトを保持しているかを明確にする必要があります。デバイスオブジェクトのライフサイクル、devres 解放セマンティクス

devres はリソース解放の順序を管理する

devm_kzalloc() などの managed API はリソースをデバイスにアタッチし、プローブの失敗またはアンバインド時には通常、逆順でクリーンアップされます。これはエラー分岐を減らしますが、すべての非同期アクティビティを自動的に解決するわけではありません。

  1. 新しいリクエストの進入を防ぐ。
  2. ハードウェアプロトコルに従って DMA を停止し、割り込みソースを無効にする。
  3. 実行中のワーク、コールバック、およびリクエストを待機またはキャンセルする。
  4. これらのパスがまだアクセスしているリソースを解放する。

待機操作中には、完了コールバックに必要なロックを保持してはいけません。managed と unmanaged API は併用できますが、どのリソースが誰によって解放されるかを明確にし、手動解放後に再度自動解放されないようにする必要があります。デバイスのライフサイクル と devres ドキュメント を参照してください。

呼び出し元の参照を取得し、ドライバのバインド解除またはデバイスの登録解除を行う。device オブジェクトが生きている間に、どのドライバリソースがすでに無効になっているかを観察する。参照カウントがデバイスのアクセス可能性のスイッチではない。

図を準備しています
デバイスオブジェクトは存続しているが、managed リソースもまだ存在するか?

device の参照寿命がドライババインド寿命より長いことは合法な状態である。get_device はオブジェクトのメモリを保護するが、アンバインドされた devres やオフラインになったハードウェアまでは保護しない。

4. sysfs、devtmpfs、udev の分担

/sys/devices はデバイスの階層を表示します。/sys/bus/.../devices と /sys/class/... は他の検索ビューを提供し、しばしばシンボリックリンクを含みます。sysfs ファイルはカーネルがエクスポートした属性インターフェースであり、通常のディスクファイルではなく、またすべてのファイルが device_attribute に対応しているわけではありません。

デバイス属性の show では通常 sysfs_emit() が出力に使用され、store では kstrto*() が入力の解析に使用されます。属性には適切なロック、権限、およびデバイス状態のチェックが必要です。安定してエクスポートされた後は、ユーザ空間 ABI も考慮する必要があり、単位や意味を安易に変更してはいけません。

デバイス番号を持ち、対応するメカニズムを使用して登録されたデバイスに対して、devtmpfs はカーネルによって基本デバイスノードを作成できます⁠。udev は uevent を受信し、属性を読み取り、ルールに基づいて権限を設定し、安定したシンボリックリンクを作成し、modalias に応じてモジュールロードをトリガーします。ビルトインドライバにはモジュールロードは不要であり、また多くのデバイスには /dev ノードが存在しません。例えば、ネットワークインターフェースは通常、ソケットと netlink を介して使用されます。

期待されるデバイスが見えない場合次の証拠層
発見されたかバス列挙と device オブジェクト
バインドされたかドライバ関連付け、マッチング、および probe 結果
機能を提供できるかサブシステム機能オブジェクトの登録
名前/アクセスが正しいかユーザ空間の命名、権限、およびルール

/dev 名が欠落しているだけでは、probe が実行されていないことを直接証明するものではありません。

5. PCI デバイスの読み取り専用確認

以下では sysfs のみを読み取り、デバイスのアンバインドやリソースの変更は行いません。ホットプラグにより、トラバーサル中にファイルが消える可能性があるため、読み取り例外を処理しています。

from pathlib import Path

for dev in sorted(Path('/sys/bus/pci/devices').glob('*'))[:8]:
    try:
        vendor = (dev / 'vendor').read_text().strip()
        product = (dev / 'device').read_text().strip()
        link = dev / 'driver'
        driver = link.resolve(strict=True).name if link.is_symlink() else '(unbound)'
        print(dev.name, vendor, product, driver)
    except OSError as exc:
        print(dev.name, type(exc).__name__)

例えば、名前 0000:03:00.0 には PCI domain、bus、device、function が含まれています。driver リンクがないのは、現在バインドされていないことを示すだけで、その理由についてはマッチングテーブル、モジュール、プローブログ、およびデバイス状態を組み合わせて分析する必要があります。

続きを読む