ベンダー モジュールのガイドライン

ベンダー モジュールの堅牢性と信頼性を高めるために、以下のガイドラインを使用してください。多くのガイドラインに従うことで、モジュールの正しいロード順序と、ドライバによるデバイスのプローブ順序を決めることが容易になります。

モジュールには、ライブラリまたはドライバがあります。

  • ライブラリ モジュールは、他のモジュールが使用する API を提供するライブラリです。この種類のモジュールは通常、ハードウェア固有ではありません。ライブラリ モジュールの例としては、AES 暗号化モジュール、モジュールとしてコンパイルされた remoteproc フレームワーク、ログバッファ モジュールなどがあります。module_init() 内のモジュール コードは、データ構造をセットアップするために実行されますが、他のコードは、外部モジュールによるトリガーがない限り実行されません。

  • ドライバ モジュールは、特定の種類のデバイスをプローブまたはバインドするドライバです。この種類のモジュールは、ハードウェア固有です。ドライバ モジュールの例として、UART、PCIe、動画エンコーダといったハードウェアがあります。ドライバ モジュールは、関連するデバイスがシステムに存在する場合にのみアクティブになります。

    • デバイスが存在しない場合、実行されるモジュール コードは、ドライバのコア フレームワークにドライバを登録する module_init() のコードのみとなります。

    • デバイスが存在し、ドライバがそのデバイスをプローブまたはバインドできた場合、他のモジュール コードも実行される可能性があります。

モジュールの init/exit を正しく使用する

ドライバ モジュールは、module_init() でドライバを登録し、module_exit() でドライバの登録を解除する必要があります。こうした制限を適用する簡単な方法は、module_init()*_initcall()module_exit() のマクロの直接使用を回避できるラッパーマクロを使用することです。

  • アンロード可能なモジュールの場合は、module_subsystem_driver() を使用します(例: module_platform_driver()module_i2c_driver()module_pci_driver())。

  • アンロード不可能なモジュールの場合は、builtin_subsystem_driver() を使用します(例: builtin_platform_driver()builtin_i2c_driver()builtin_pci_driver())。

一部のドライバ モジュールは、複数のドライバを登録するために module_init()module_exit() を使用しています。module_init()module_exit() を使用して複数のドライバを登録するドライバ モジュールの場合、ドライバをできるだけひとつにまとめるようにしてください。たとえば、個別のドライバを登録せずに、デバイスの compatible 文字列や aux データを使って区別することもできます。または、ドライバ モジュールを 2 つのモジュールに分割することもできます。

init 関数と exit 関数の例外

ライブラリ モジュールは、ドライバを登録しません。また、データ構造、ワークキュー、カーネル スレッドのセットアップに必要となる場合があるため、module_init()module_exit() に関する制約の対象外となります。

MODULE_DEVICE_TABLE マクロを使用する

ドライバ モジュールには、MODULE_DEVICE_TABLE マクロを含める必要があります。これにより、モジュールのロード前に、ユーザー空間からドライバ モジュールでサポートされているデバイスを確認できます。Android では、このデータを使用して、システムに存在しないデバイスのモジュールをロードしないなど、モジュールのロードを最適化できます。このマクロの使用例については、アップストリーム コードを参照してください。

前方宣言されたデータ型による CRC 不一致を回避する

前方宣言されたデータ型の内容にアクセスするために、ヘッダー ファイルをインクルードしないでください。ヘッダー ファイル(header-A.h)で定義されている一部の構造体や共用体などのデータ型は、別のヘッダー ファイル(header-B.h)で前方宣言されている場合があります(典型的な目的は、そのデータ型へのポインタを使用するためです)。このコードパターンは、カーネルがそのデータ構造を header-B.h のユーザーには非公開にしようとしていることを意味します。

header-B.h のユーザーは、前方宣言されたデータ構造の内部に直接アクセスするために header-A.h をインクルードしてはなりません。それを行った場合、別のカーネル(たとえば GKI カーネルなど)がモジュールをロードしようとしたときに、CONFIG_MODVERSIONS で CRC の不一致の問題(ABI コンプライアンスに関する問題)が発生します。

たとえば、struct fwnode_handleinclude/linux/fwnode.h で定義されている一方で、include/linux/device.hstruct fwnode_handle; と前方宣言されているのは、カーネルで include/linux/device.h のユーザーに対して struct fwnode_handle の詳細を非公開にしようとしているためです。このケースでは、struct fwnode_handle のメンバーにアクセスするためにモジュールに #include <linux/fwnode.h> を追加しないでください。このようなヘッダー ファイルをインクルードする必要があるということは、その設計パターンには問題があるということです。

コアカーネル構造体に直接アクセスしない

コアカーネル データ構造体への直接のアクセスや、同構造体の直接の変更は、メモリリーク、クラッシュ、今後のカーネル リリースとの互換性の破壊など、望ましくない動作につながる可能性があります。データ構造体が次のいずれかの条件を満たす場合、それはコアカーネル データ構造体です。

  • データ構造体が KERNEL-DIR/include/ で定義されている。たとえば、struct devicestruct dev_links_info です。include/linux/soc で定義されているデータ構造体は例外です。

  • データ構造体がモジュールによって割り当てまたは初期化されているが、カーネルによってエクスポートされている関数への入力として、(構造体内のポインタで)間接的に渡す、または直接渡すことで、カーネルに公開されている。たとえば、cpufreq ドライバ モジュールは、struct cpufreq_driver を初期化してから、入力として cpufreq_register_driver() に渡しています。この時点以降、cpufreq_register_driver() を呼び出すとカーネルに struct cpufreq_driver が公開されるため、cpufreq ドライバ モジュールで struct cpufreq_driver を直接変更しないでください。

  • データ構造体がモジュールによって初期化されていない。たとえば、regulator_register() から返される struct regulator_dev などです。

コアカーネル データ構造体にアクセスする場合は、カーネルによってエクスポートされている関数を使用するか、明示的にベンダーフックに入力として渡されたパラメータを使用してください。コアカーネル データ構造体の一部を変更するための API またはベンダーフックがない場合、それは意図的なものである可能性が高いため、モジュールからそのデータ構造体を変更しないでください。たとえば、struct devicestruct device.links 内のフィールドは変更しないでください。

  • device.devres_head を変更するには、devm_clk_get()devm_regulator_get()devm_kzalloc() などの devm_*() 関数を使用します。

  • struct device.links 内のフィールドを変更するには、device_link_add()device_link_del() などのデバイスリンク API を使用します。

compatible プロパティを使用してデバイスツリー ノードを解析しない

デバイスツリー(DT)ノードに compatible プロパティがある場合、struct device は自動的に割り当てられるか、親 DT ノードで of_platform_populate() が呼び出されたときに(通常は親デバイスのデバイス ドライバによって)割り当てられます。デフォルトの想定(スケジューラのために早い段階で初期化される一部のデバイスを除く)では、compatible プロパティを持つ DT ノードには struct device と一致するデバイス ドライバがあります。その他の例外はすべて、すでにアップストリーム コードで処理されています。

さらに、fw_devlink(以前の of_devlink)は、compatible プロパティを持つ DT ノードを、ドライバによってプローブされた struct device が割り当てられたデバイスとみなします。DT ノードに compatible プロパティがあっても、割り当てられた struct device がプローブされていない場合、fw_devlink がそのコンシューマ デバイスがプローブするのをブロックしたか、そのサプライヤー デバイスに対する sync_state() の呼び出しをブロックした可能性があります。

ドライバで、of_find_*() 関数(of_find_node_by_name()of_find_compatible_node() など)を使用して compatible プロパティを持つ DT ノードを直接検出し、その DT ノードを解析している場合は、デバイスのプローブまたは compatible プロパティの削除ができるデバイス ドライバを作成してモジュールを修正してください(アップストリームにしていない場合のみ可能)。別の手段について話し合うには、Android カーネルチーム(kernel-team@android.com)に連絡し、そのユースケースの正当性を伝えてください。

サプライヤーの検索には DT phandle を使用する

可能な限り、DT の phandle(DT ノードへの参照/ポインタ)を使用してサプライヤーを参照してください。標準の DT バインディングと phandle を使用してサプライヤーを参照すると、fw_devlink(以前の of_devlink)が実行時に DT を解析してデバイス間の依存関係を自動的に判断できるようになります。こうすることで、カーネルが自動的に正しい順序でデバイスをプローブできるようになり、モジュール ロードの順序決定や MODULE_SOFTDEP() が不要になります。

従来のシナリオ(ARM カーネルで DT がサポートされていない)

ARM カーネルに DT サポートが追加される前は、タッチデバイスなどのコンシューマは、グローバルに一意の文字列を使用してレギュレータなどのサプライヤーを検索していました。たとえば、ACME PMIC ドライバは、複数のレギュレータ(acme-pmic-ldo1 から acme-pmic-ldo10 までなど)の登録またはアドバタイズを行うことができ、タッチドライバは、regulator_get(dev, "acme-pmic-ldo10") を使用してレギュレータを検索できます。しかし、別のボードでは LDO8 がタッチデバイスのサプライヤーであるため、同じタッチドライバであっても、タッチデバイスが使用されているボードごとに、レギュレータを検索するための適切な文字列を判別する必要が生じ、システムが煩雑になります。

現在のシナリオ(ARM カーネルで DT がサポートされている)

ARM カーネルで DT サポートが追加された後は、コンシューマは、phandle を使用してサプライヤーのデバイスツリー ノードを参照することにより、DT 内でサプライヤーを特定できます。コンシューマでは、供給元ではなく、使用目的に基づいてリソースに名前を付けることができます。たとえば、前の例のタッチドライバでは、regulator_get(dev, "core")regulator_get(dev, "sensor") を使用して、タッチデバイスのコアとセンサーに対するサプライヤーを取得できます。このようなデバイスに関連付けられた DT は、次のコードサンプルのようなものとなります。

touch-device {
    compatible = "fizz,touch";
    ...
    core-supply = <&acme_pmic_ldo4>;
    sensor-supply = <&acme_pmic_ldo10>;
};

acme-pmic {
    compatible = "acme,super-pmic";
    ...
    acme_pmic_ldo4: ldo4 {
        ...
    };
    ...
    acme_pmic_ldo10: ldo10 {
        ...
    };
};

問題が残されたままの場合

古いカーネルから移植された一部のドライバでは、DT の旧来の動作が含まれている場合があります。以前のスキームで問題のあった部分が残っており、簡素化のための新しいスキームにも適用されています。このようなドライバでは、コンシューマ ドライバがデバイス固有の DT プロパティを使用して検索に使用する文字列を読み取る一方、サプライヤーは別のサプライヤー固有のプロパティを使用してサプライヤーのリソース登録に使用する名前を定義します。その結果、コンシューマとサプライヤーは、サプライヤーの検索に文字列を使うという、以前と同じ古いスキームを使い続けます。このように問題が残されたままの場合は、次のようになります。

  • タッチドライバは、次のようなコードを使用します。

    str = of_property_read(np, "fizz,core-regulator");
    core_reg = regulator_get(dev, str);
    str = of_property_read(np, "fizz,sensor-regulator");
    sensor_reg = regulator_get(dev, str);
    
  • DT は、次のようなコードを使用します。

    touch-device {
      compatible = "fizz,touch";
      ...
      fizz,core-regulator = "acme-pmic-ldo4";
      fizz,sensor-regulator = "acme-pmic-ldo4";
    };
    acme-pmic {
      compatible = "acme,super-pmic";
      ...
      ldo4 {
        regulator-name = "acme-pmic-ldo4"
        ...
      };
      ...
      acme_pmic_ldo10: ldo10 {
        ...
        regulator-name = "acme-pmic-ldo10"
      };
    };
    

フレームワーク API のエラーを変更しない

フレームワーク API(regulatorclocksirqgpiophysextcon など)は、-EPROBE_DEFER をエラー戻り値として返すことで、デバイスがプローブしようとしているが、現時点ではプローブできず、後でカーネルにプローブの再試行を行ってもらう必要があることを示します。このような場合にデバイスの .probe() 関数が想定どおりに失敗するように、エラー値の置き換えや再マッピングは行わないでください。エラー値の置き換えや再マッピングを行うと、-EPROBE_DEFER が抜け落ちて、デバイスがプローブされなくなる可能性があります。

devm_*() API バリアントを使用する

デバイスが devm_*() API を使用してリソースを取得すると、デバイスがプローブに失敗するか、プローブが成功して後でバインドが解除された場合に、カーネルによってリソースが自動的に解放されます。この機能により、devm_*() で取得したリソースを解放するための goto ジャンプが不要になり、ドライバのバインド解除処理が簡単になるため、probe() 関数のエラー処理コードが簡潔になります。

デバイス ドライバのバインド解除の処理を行う

デバイス ドライバのバインド解除は意図的に行ってください。未定義は禁止を意味しないため、バインド解除を未定義のままにしないでください。デバイス ドライバのバインド解除は、完全に実装するか、あるいは明示的に無効にする必要があります。

デバイス ドライバのバインド解除を実装する

デバイス ドライバのバインド解除を完全に実装する場合は、デバイス ドライバのバインド解除を適切に行い、メモリリーク、リソースリーク、セキュリティの問題を回避します。ドライバの probe() 関数を呼び出すとデバイスをドライバにバインドでき、ドライバの remove() 関数を呼び出すとデバイスをバインド解除できます。remove() 関数が存在しない場合でも、カーネルはデバイスをバインド解除できます。ドライバコアは、ドライバをデバイスからバインド解除する際に、ドライバはクリーンアップ処理を必要としていないと想定しています。次の条件が両方とも満たされている場合、デバイスからバインド解除されるドライバで明示的なクリーンアップ処理を行う必要はありません。

  • ドライバの probe() 関数によって取得されるすべてのリソースは、devm_*() API で取得される。

  • ハードウェア デバイスが、シャットダウンや休止のシーケンスを必要としない。

この場合、ドライバコアが、devm_*() API で取得したすべてのリソースの解放を処理します。どちらかの条件が満たされない場合、ドライバはデバイスからバインド解除されるときにクリーンアップを行う必要があります(リソースを解放し、ハードウェアをシャットダウンまたは休止する)。デバイスがドライバ モジュールを適切にバインド解除できるようにするには、次のいずれかを行います。

  • ハードウェアがシャットダウンや休止のシーケンスを必要としない場合は、devm_*() API を使用してリソースを取得するようにデバイス モジュールを変更します。

  • remove() ドライバ オペレーションを probe() 関数と同じ構造体に実装し、remove() 関数を使用してクリーンアップ処理を行います。

デバイスとドライバのバインド解除を明示的に無効にする(非推奨)

デバイスとドライバのバインド解除を明示的に無効にする場合は、バインド解除を禁止し、さらに、モジュールのアンロードを禁止する必要があります。

  • バインド解除を禁止するには、ドライバの struct device_driversuppress_bind_attrs フラグを true に設定します。この設定により、bind ファイルと unbind ファイルがドライバの sysfs ディレクトリに現れなくなります。unbind ファイルを使用すると、ドライバをそのデバイスからバインド解除することを、ユーザー空間からトリガーできるようになります。

  • モジュールのアンロードを禁止するには、lsmod でモジュールが [permanent] とマークされるようにします。module_exit() または module_XXX_driver() を使用していない場合、モジュールは [permanent] とマークされます。

プローブ関数内からファームウェアをロードしない

ドライバがフラッシュや永続ストレージをベースとしたファイル システムをマウントする前にプローブを行うと、ファームウェアにアクセスできない可能性があるため、.probe() 関数内からファームウェアをロードしないでください。この場合、request_firmware*() API が長時間ブロックされてエラーとなり、起動処理が不必要に長くなる可能性があります。その代わりに、クライアントがデバイスの使用を開始するまでファームウェアのロードを先延ばしにします。たとえば、ディスプレイ ドライバは、ディスプレイ デバイスが開かれたときにファームウェアをロードできます。

.probe() を使用してファームウェアを読み込んでも問題ない場合があります。たとえば、ファームウェアを機能させる必要があるが、デバイスはユーザー空間に公開されないクロック ドライバの場合などです。他にも適切なユースケースが考えられます。

非同期プローブを実装する

今後のリリースで Android に追加される可能性がある機能強化(起動時間を短縮するための並列モジュールのロードや並列デバイスのプローブなど)を活用できるように、非同期プローブをサポートし、それを使用してください。ドライバ モジュールで非同期プローブを使用しない場合、そのような最適化の効果が低下する可能性があります。

ドライバが非同期プローブをサポートし、それを優先するとマークするには、ドライバの struct device_driver メンバーに probe_type フィールドを設定します。次の例では、プラットフォーム ドライバで、このようなサポートを有効にしています。

static struct platform_driver acme_driver = {
        .probe          = acme_probe,
        ...
        .driver         = {
                .name   = "acme",
                ...
                .probe_type = PROBE_PREFER_ASYNCHRONOUS,
        },
};

非同期プローブでドライバを動作させるために、特別なコードは必要ありません。ただし、非同期プローブをサポートする際は、次の点に注意してください。

  • 以前にプローブされた依存関係に関して前提を置かない。直接的または間接的(ほとんどのフレームワーク呼び出し)にチェックを行い、1 つ以上のサプライヤーの準備ができていない場合は -EPROBE_DEFER を返します。

  • 親デバイスのプローブ関数に子デバイスを追加する場合、子デバイスが直ちにプローブされることを前提としない。

  • プローブが失敗した場合、適切なエラー処理を行い、クリーンアップする(devm_*() API バリアントを使用するをご覧ください)。

MODULE_SOFTDEP を使用してデバイス プローブの順序付けをしない

MODULE_SOFTDEP() 関数は、デバイス プローブの順序を保証する信頼できる方法ではなく、次の理由から使用すべきでありません。

  • 遅延プローブ。モジュールがロードされたとき、いずれかのサプライヤーの準備ができていないために、デバイス プローブが先延ばしにされる可能性があります。このため、モジュール ロードの順序とデバイス プローブの順序が一致しなくなる可能性があります。

  • 1 つのドライバに多数のデバイス。1 つのドライバ モジュールで特定のデバイスタイプを管理できます。システムに、デバイスタイプのインスタンスが複数あり、それぞれのプローブ順序の要件が異なっている場合、モジュールのロード順序を使用しても、それらの要件を満たすことはできません。

  • 非同期プローブ。非同期プローブを行うドライバ モジュールは、モジュールがロードされても、すぐにはデバイスをプローブしません。並列スレッドがデバイス プローブを処理するため、モジュール ロードの順序とデバイス プローブの順序が一致しなくなる可能性があります。たとえば、I2C main ドライバ モジュールが非同期プローブを行い、タッチドライバ モジュールが I2C バス上の PMIC に依存する場合は、タッチドライバと PMIC ドライバが正しい順序でロードされても、PMIC ドライバのプローブ前にタッチドライバのプローブが行われない可能性があります。

MODULE_SOFTDEP() 関数を使用しているドライバ モジュールがある場合、この関数を使用しないように修正してください。修正をサポートするために、Android チームは、MODULE_SOFTDEP() を使用せずにカーネルが順序付けの問題に対処できるようにする変更をアップストリームで行っています。具体的には、fw_devlink を使用してプローブの順序を保証し、(デバイスのすべてのコンシューマがプローブを行った後に)sync_state() コールバックを使用して必要なタスクを実行することができます。

設定には #ifdef でなく #if IS_ENABLED() を使用する

#ifdef CONFIG_XXX ではなく #if IS_ENABLED(CONFIG_XXX) を使用して、今後設定ファイルがトライステート設定に変更されても、#if ブロック内のコードがコンパイルされるようにします。相違点は次のとおりです。

  • #if IS_ENABLED(CONFIG_XXX) は、CONFIG_XXX がモジュール(=m)または組み込み(=y)に設定されている場合に true と評価されます。

  • #ifdef CONFIG_XXX は、CONFIG_XXX が組み込み(=y)に設定されている場合は true と評価されますが、CONFIG_XXX がモジュール(=m)に設定されている場合はそうなりません。これは、モジュールに設定が行われているか、設定が無効になっているときに確実に同じことを行う場合にのみ使用します。

条件付きコンパイルに正しいマクロを使用する

CONFIG_XXX がモジュール(=m)に設定されている場合、ビルドシステムは自動的に CONFIG_XXX_MODULE を定義します。ドライバが CONFIG_XXX によって制御されており、モジュールとしてコンパイルされているかどうかを確認するには、以下のガイドラインを使用してください。

  • ドライバの C ファイル(またはヘッダー ファイル以外のソースファイル)では、#ifdef CONFIG_XXX_MODULE を使用しません。これは必要以上に制限が強く、設定の名前が CONFIG_XYZ に変更されると正しく動作しなくなります。モジュールにコンパイルされるヘッダー以外のソースファイルの場合、ビルドシステムはそのファイルのスコープに MODULE を自動的に定義します。したがって、C ファイル(またはヘッダー以外のソースファイル)がモジュールの一部としてコンパイルされているかどうかを確認するには、#ifdef MODULE を使用します(CONFIG_ 接頭辞は付けません)。

  • ヘッダー ファイルの場合、バイナリに直接コンパイルされるのではなく、C ファイル(または他のソースファイル)の一部としてコンパイルされるため、同じ確認がさらに複雑になります。ヘッダー ファイルには次のルールを使用します。

    • #ifdef MODULE を使用するヘッダー ファイルの場合は、どのソースファイルに使用されているかによって結果が変わります。つまり、同じビルド内の同じヘッダー ファイルでも、どの部分のコードがコンパイルされるかはソースファイルごとに異なる可能性があります(モジュールか組み込み、または無効か)。これは、組み込みコードでの展開方法とモジュールでの展開方法が、マクロで別になるように定義する必要がある場合に便利です。

    • 特定の CONFIG_XXX がモジュールに設定されている場合にコンパイルする必要があるコードを含んだヘッダー ファイルの場合(それをインクルードしているソースファイルがモジュールかどうかに関係なく)、そのヘッダー ファイルでは #ifdef CONFIG_XXX_MODULE を使用する必要があります。