PCIe と USB ドライバモデル

このページの目次

PCIe と USB はどちらもデバイスを列挙できるが、ドライバへの制御方法は異なる。PCI ドライバは通常、BAR レジスタを直接操作し、DMA キューを構築する。USB インターフェースドライバはリクエストを USB コアに送信し、ホストコントローラがバストランザクションを実行する。両者に共通する難しさは、デバイスとリクエストが非同期に消失する可能性があること、リソースは最後のユーザーが退出した後にのみ解放されるべきであるということである。

1. PCI:設定空間、BAR、DMA は別物

設定空間は、デバイスの識別子、機能、リソースウィンドウ情報を提供する。システムはトポロジをスキャンし、利用可能なリソースを保持または割り当てる。ファームウェアは、ルートブリッジや設定空間ウィンドウなどのプラットフォーム情報を記述できる場合もある。完全なアドレスは 0000:03:00.0 のような形式であり、複数のドメインにまたがる場合、bus:device.function だけでは一意に特定できないことがある。

インターフェース用途混同すべきでない対象
設定空間識別子、機能、BAR 設定通常デバイスの業務用レジスタ
BAR リソースレジスタまたはデバイスメモリの MMIO / I/O ウィンドウメインメモリの DMA バッファ
DMA アドレスデバイスがメインメモリを読み書きする際に使用するアドレスCPU の仮想ポインタ

通常のエンドポイント設定ヘッダーには 6 つの BAR スロットがあり、64 ビット BAR は隣接する 2 つのスロットを使用する。ブリッジのレイアウトはこれとは異なる。したがって、カーネルの pci_dev.resource を固定された 6 項目として偽装し、それに基づいてすべてのリソースをトラバースしてはならない。pci_resource_start/len/flags() などの API を通じてリソースを解釈し、適切なマッピングおよび I/O アクセサを使用すること。

pci_iomap() などのマッピングが返す __iomem アドレスは、通常の RAM のように自由に間接参照したり memcpy したりしてはならない。ioread32() / iowrite32() などの、デバイスプロトコルに一致するアクセス方法を使用すること。MMIO の順序や、posted write の完了要件については、ハードウェアおよびプラットフォームのルールを参照すること。

2. probe 時のリソース依存関係

典型的な初期化には、デバイスの有効化、必要な BAR の要求、ハードウェアが実際にサポートする DMA マスクの設定、レジスタのマッピング、キューと割込みの割り当て、そして外部インターフェースの登録が含まれる。具体的な順序は、ハードウェアのリセット、ファームウェアの読み込み、および割込みのマスク要件の影響を受けるため、汎用的な呼び出しリストをそのまま実行可能なドライバとして扱うことはできない。

バスマスターを有効にすると、デバイスがトランザクションを開始できるようになる。これは、バッファとデバイス状態の準備が整った段階で処理すべきである。外部にインターフェースを公開する前に、エラー戻り値のパスでは、取得したリソースを逆順で取り消せるようにする必要がある。新しいドライバでは、古いバージョンの pci_driver の suspend/resume フィールドをそのままコピーするのではなく、dev_pm_ops を通じて電源管理を表現するのが一般的である。

PCI ドキュメント は初期化とリソースインターフェースについて説明している。実装時には対象となるカーネルヘッダーファイルと照合し、managed/unmanaged リソースの解放責任を整合させること。

3. MSI/MSI-X:要求した数が返された数ではない

ベクターの予算:返された数に基づいて後続のリソースを構築する

MSI/MSI-X は、デバイスが発行するメッセージを使用して割込みをトリガする。MSI-X テーブルは最大 2048 項目をサポートするが、デバイスの実装、プラットフォーム、および利用可能なリソースによって実際の数が制限されることがある。1 から 16 個のベクターを要求しても、1、4、または 8 個しか返されない場合がある。

以下は初期化段階の部分的な例であり、完全なドライバではない。

int nvec = pci_alloc_irq_vectors(pdev, 1, 16, PCI_IRQ_MSIX);
if (nvec < 0)
    return nvec;

/* [0, nvec) の範囲のみをトラバースし、その後、処理関数を個別にインストールしてエラーを確認する。 */
for (int i = 0; i < nvec; ++i) {
    int irq = pci_irq_vector(pdev, i);
    dev_dbg(&pdev->dev, "vector %d uses IRQ %d\n", i, irq);
}

i はデバイスのベクターインデックス、irq は Linux IRQ 番号である。これらは互いに交換してはならない。ベクターの数は、キュー数や CPU 数とも一致しない。複数のキューが割込みを共有できる場合があり、親和性はルーティングを決定するものであり、特定のワーカースレッドへの恒久的なバインドではない。

クリーンアップ時は、デバイスが関連する割込みをさらに生成しないようにしてから、処理関数を適切に同期させ、使用した API の規約に従って処理関数とベクターを解放する。一部の managed PCI インターフェースには自動クリーンアップ機能があるため、無条件で手動解放を行うことはできない。MSI ドライバガイド を参照のこと。

8 つのキューに対して 3 つのベクターしか取得できない場合、どのように割り当てるか

デバイスが複数の完了キューを 1 つの MSI-X ベクターにルーティングできると仮定する。ここでは明確なドライバ戦略を採用する。ベクターが 2 つ以上ある場合は、インデックス 0 を管理イベント用に確保し、残りのベクターをデータキューにローテーションで割り当てる。ベクターが 1 つしかない場合は、管理イベントとすべてのデータキューがインデックス 0 を共有する。このようなルーティングがサポートされているかどうかは、ハードウェアによって確認されなければならず、OS が返す数から推測してはならない。

実際のベクター数管理イベントデータキュー 0~7 に対応するベクターインデックス
100、0、0、0、0、0、0、0
301、2、1、2、1、2、1、2
501、2、3、4、1、2、3、4
def route_queues(queue_count, linux_irqs):
    if queue_count < 0 or not linux_irqs or len(set(linux_irqs)) != len(linux_irqs):
        raise ValueError("有効なキュー数と重複のないベクター IRQ マッピングが必要です")
    nvec = len(linux_irqs)
    indices = [0 if nvec == 1 else 1 + q % (nvec - 1)
               for q in range(queue_count)]
    groups = {i: [] for i in range(nvec)}
    for q, index in enumerate(indices):
        groups[index].append(q)
    return indices, groups

indices, groups = route_queues(8, [48, 113, 207])
assert indices == [1, 2, 1, 2, 1, 2, 1, 2]
assert groups == {0: [], 1: [0, 2, 4, 6], 2: [1, 3, 5, 7]}
for nvec in (1, 3, 5):
    for count in (0, 1, 8, 17):
        indices, groups = route_queues(count, list(range(100, 100 + nvec)))
        assert sorted(q for group in groups.values() for q in group) == list(range(count))
        assert all(0 <= i < nvec for i in indices)
assert route_queues(8, [48])[0] == [0] * 8

ここで IRQ 113 の処理関数は、キュー 0、2、4、6 の完了状態を確認する必要がある。「割込みが 1 回発生した」ことを「リクエストがちょうど 1 つ完了した」としてはならない。割込みの統合、複数の完了エントリ、およびキューが引き続き作業を生成している場合、デバイスキュープロトコルに従って消費位置を進める必要がある。1 つのデバイスベクターを共有することは、従来の INTx の共有ラインモードを使用することとは異なり、2 つの「共有」は別々に理解すべきである。

初期化の失敗に対しても、実際に構築されたリソースに基づいてロールバックする必要がある。3 つのベクターを割り当てたが、3 番目の request_irq() が失敗した場合、インストール済みの最初の 2 つの処理関数のみを解放し、その後ベクターを解放する。キューと DMA バッファは、それぞれ取得した順序に従って処理される。通常運用に入ると、ハード割込みの同期に加え、処理関数が派生させた作業を排空する必要がある。そうしないと、IRQ の解放が、作業項目がキューにアクセスしなくなったことを証明するものではない。

4. USB:ドライバは通常インターフェースにバインドされる

1 つの USB デバイスは複数の構成を提供できる。選択された構成には複数のインターフェースが含まれ、インターフェースには代替設定(alternate settings)があり、各設定がエンドポイントのグループを記述する。カメラのオーディオ機能とビデオ機能は、異なるインターフェースドライバによって処理されることがある。

一般的な usb_driver.probe() は usb_interface * を受け取る。ドライバは、バインドされたインターフェースの現在設定されているエンドポイントを調べるべきであり、「最初のエンドポイントが常に bulk IN である」と仮定してはならない。エンドポイント 0 はデバイスのデフォルト制御エンドポイントであり、各インターフェース固有のエンドポイントとして適用してはならない。

転送タイプ適したタスクサービス特性
Control標準リクエストおよび設定コマンドsetup などのフェーズを含む
Bulkストレージおよび大量データ信頼性の高い転送。固定帯域幅/レイテンシの保証なし
Interruptキーボードなどの少量の周期的データホストがサービス間隔に従ってスケジューリング
Isochronousオーディオ/ビデオストリームタイムリーなサービス。エラーは通常再送信しない

ここで言う interrupt は、周辺デバイスが任意の時点で CPU を直接割り込むことを意味するものではない。USB バス転送はホストによってスケジューリングされ、HCD の CPU 割込みは別の層のメカニズムである。ポート接続の検出は、USB の速度とプロトコルによって異なり、D+ のレベル変化として一律に解釈できるものではない。

5. URB は非同期リクエストであり、データがすでに送信されたことの証明ではない

USB リクエスト:リクエストの受け入れと関数の返却は同時ではない ドライバが URB とバッファを準備 エンドポイント、方向、長さ、完了コールバックを設定 1 コアがリクエストを受け入れ、在途状態に入る コアが在途リクエストを管理し、ドライバはリソースの寿命を保持 2 HCD が実行し、完了を報告 成功、エラー、キャンセルのいずれでも完了ステータスが生成される 3 完了コールバックが status / actual_length を確認 回収、待機中のスレッドのウェイクアップ、または許可条件下での再送信 4 送信失敗は呼び出し側で処理する。送信成功は、転送が完了したことを意味しない。
usb_submit_urb の結果所有権と後続の責任
エラーを返すリクエストはコアに正常に引き渡されず、呼び出し側がエラーとリソースを処理
0 を返すリクエストが受け入れられ、バッファとコンテキストは完了まで存続しなければならない

送信前に、エンドポイント、バッファ、およびコールバックコンテキストを準備する。送信成功は、デバイスがデータをすでに使用し終わったことを意味しない。

送信成功したリクエストはすぐに完了する可能性があり、コールバックは usb_submit_urb() が返る前に発生することさえある。したがって、コールバックが使用するコンテキストは、送信インターフェースを呼び出す前に準備されていなければならない。図の「リクエストの受け入れ」は送信呼び出しの間に発生するため、関数の返却をコールバック開始のバリアとして扱ってはならない。

コールバックへの参照を先に取得し、リクエストを送信する

ドライバ固有のリクエストオブジェクトと URB 自体は別のオブジェクトである。USB コアは URB を管理するが、任意の urb->context ポインタに対して自動的に参照を取得するわけではない。以下に、1 回のリクエストにおけるペアリング方法を示す。呼び出し側はまずプライベートオブジェクトへの参照を保持し、送信前に完了パス用に 1 つ分を予約する。送信成功後、この参照はコールバックによって解放される。送信失敗でこの完了コールバックが発生しない場合、参照は呼び出し側によって取り消される。

コールバックが先に完了しても遅くても、送信前に参照を予約する必要がある 早期完了 予約:2 コールバック:1 呼び出し側の退出:0 遅延完了 予約:2 呼び出し側の退出:1 コールバック:0 数字はドライバ固有オブジェクトの参照数。送信失敗時は、呼び出し側がコールバックの予約を取り消す。
class Request:
    def __init__(self):
        self.refs = 1
        self.ready = True
        self.callback_owned = False
        self.completions = 0
        self.freed = False

    def put(self):
        assert self.refs > 0 and not self.freed
        self.refs -= 1
        self.freed = self.refs == 0

    def reserve_callback(self):
        assert self.ready and not self.freed and not self.callback_owned
        self.refs += 1
        self.callback_owned = True

    def finish(self):
        assert self.ready and self.callback_owned and not self.freed
        self.completions += 1
        self.callback_owned = False
        self.put()

    def rollback_failed_submit(self):
        assert self.callback_owned and self.completions == 0
        self.callback_owned = False
        self.put()


def one_request(mode):
    r = Request()
    r.reserve_callback()                 # submit に入る前に実行する必要がある
    if mode == "failure":
        result = -1                      # 失敗した送信では、この完了コールバックは発生しない
    else:
        result = 0
        if mode == "early":
            r.finish()                   # 送信関数の返却より前にコールバックが完了
    if result:
        r.rollback_failed_submit()
    assert not r.freed                    # 呼び出し側自身の参照はまだ有効
    r.put()                              # 呼び出し側が今回のアクセスを終了
    if mode == "late":
        assert not r.freed                # コールバックの参照がオブジェクトを保護し続ける
        r.finish()
    assert r.freed and r.refs == 0
    assert r.completions == (mode != "failure")

for mode in ("failure", "early", "late"):
    one_request(mode)

このモデルは、1 回の送信におけるプライベートオブジェクトの寿命のみを追跡する。実際の並行コードでは、適切なアトミック参照、状態同期、および独立した URB/バッファ管理が必要である。コールバックが再送信を行う場合、新しいリクエストに対して完全な引き継ぎを構築する必要があり、旧リクエストの完了をセッション全体の終了と誤認してはならない。submitted = true を個別に保存し、送信の返却後にのみ参照を増やす方法は、早期完了パスではすでに遅すぎる。

ステータス成功、バイトの完全性、ビジネスロジックの完全性は別々に判断する

以下では、bulk IN などの非等時転送について議論する。リクエストが 4096 バイトを読み取り、成功して 512 バイトを返すことは、合法なショートリード(短縮読み取り)となり得る。もし上位プロトコルが固定された 4096 バイトのレコードを要求している場合、この USB の成功がレコードの完全性を意味するわけではない。逆に、1 つの URB が複数のビジネスレコードを運ぶこともあり、レコードの境界はプロトコルパーサによって維持される必要がある。

完了結果確定できる事実上位層が決定すべきこと
status=0, actual_length=4096今回実際に 4096 バイトを受信コンテンツ、チェックサム、およびレコード境界が有効か
status=0, actual_length=512ショートリードをエラーとして扱わず、512 バイトを受信1 つの短い応答か、それとも継続して連結する必要があるか
status がエラー, actual_length=512エラー発生前に部分的なデータが存在した可能性破棄するか、接頭辞を保持してプロトコルを再同期するか
actual_length=0今回データバイトなしプロトコルが空の応答を許可しているか。ファイルの EOF と直接同一視してはならない

非等時読み取りでは、URB_SHORT_NOT_OK を使用してショートリードをエラーとして報告させることができるが、これは残りのバイトを補完するものでも、ビジネスレコードを検証するものでもない。等時転送の場合は、iso_frame_desc のステータスと長さを個別に確認する必要があり、URB の全体ステータスだけですべてのフレームが成功したと推論してはならない。フィールドの意味については USB Host Side API を参照のこと。

完了時は以下の順序で確認する:

  1. urb->status を使用して結果を判断する。
  2. actual_length を使用して実際の長さを判断する。成功しても、必ずしも要求された長さと一致するわけではない。
  3. 再送信が必要な場合は、まずデバイスが切断、クローズ、またはサスペンド状態に入っていないことを確認する。

コールバックはスリープ不可能なコンテキストで実行されるため、複雑なブロッキング処理は適切なワークメカニズムに委ねる。そうしないと、終了プロセス中に転送が再起動する可能性がある。

  • usb_unlink_urb() は非同期キャンセルを要求するが、これによってコールバックによってまだアクセスされる可能性のあるオブジェクトを即座に解放してはならない。
  • usb_kill_urb() は、リクエストの退出と完了コールバックの終了を待つ。呼び出し位置ではスリープが許可されており、かつコールバックが必要とするロックを保持していない必要がある。
  • USB アンカーは URB のグループを追跡でき、グループ化された在途リクエストの停止に便利であるが、ドライバ自身のオープンファイル、ワークアイテム、および参照の管理は依然として必要である。

インターフェースルールとキャンセルセマンティクスについては URB ドキュメント を参照のこと。

リクエストが飛行中(実行中)またはコールバック実行中にバッファの解放を試みる。リクエストのキャンセル、コールバックの退出、解放の資格がそれぞれ表示される。さらにコールバックが必要とするロックを追加し、同期待機がなぜデッドロックする可能性があるかを観察する。

図を準備しています
リクエストキャンセル後、誰がバッファを使用する可能性があるか?

usb_unlink_urb はリクエストのキャンセルであり、完了コールバックの終了に代わるものではない。同期 kill はリクエストとコールバックの退出を待ち、待機中はコールバックが必要とするロックを保持してはならない。

6. ホットプラグと電源管理の両方で、まずリクエストを停止する必要がある

disconnect/remove では、オフラインとしてマークし、新しいリクエストを阻止してから、非同期リクエストを取り消し、コールバックを待ち、機能インターフェースを登録解除し、リソースを解放する必要がある。ユーザーがデバイスのファイルをまだ保持している可能性がある。後続の操作では、解放済みのプライベートデータにアクセスするのではなく、適切なエラーが表示されるべきである。物理デバイスがすでに拔けている場合、クリーンアップを完了するために「レジスタに再書き込みすれば必ず成功する」という前提に依存することはできない。

送信参照:アイドルサスペンドとシステムサスペンドは別々に処理する

runtime PM はアイドル期間中のサスペンドとレジュームを処理し、システムサスペンドはマシン全体の状態を調整する。I/O を送信する前に、適切な PM 使用参照を取得し、失敗時は戻り値を処理する。転送が完了したら、規約に従って参照を解放する。USB の自動サスペンドには、インターフェースドライバとコアの連携が必要であり、PCI でさえもレジューム後にデバイス状態を再構築する必要がある場合がある。電源管理 を参照のこと。

続きを読む

  • デバイスモデル:検出、マッチング、およびオブジェクト参照。
  • DMA API:PCI キューの背後にあるアドレスとバッファの引き継ぎ。
  • カーネルソースのエントリポイント:drivers/pci/、drivers/usb/core/、include/linux/usb.h。実際のドライバのエラーロールバックと remove パスは、probe と合わせて読むべきである。