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

次のガイドラインを使用して、ベンダーモジュールの堅牢性と信頼性を高めます。多くのガイドラインに従うと、正しいモジュールのロード順序と、ドライバーがデバイスをプローブする必要がある順序を簡単に決定できるようになります。

モジュールは、ライブラリまたはドライバにすることができます。

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

  • ドライバーモジュールは、特定のタイプのデバイスをプローブまたはバインドするドライバーです。このようなモジュールはハードウェア固有です。ドライバモジュールの例には、UART、PCIe、およびビデオエンコーダハードウェアが含まれます。ドライバモジュールは、関連するデバイスがシステムに存在する場合にのみアクティブになります。

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

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

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

ドライバーモジュールは、module_init()でドライバーを登録し、 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_init()およびmodule_exit() )を使用します。 module_init()およびmodule_exit()を使用して複数のドライバーを登録するドライバーモジュールの場合は、ドライバーを1つのドライバーに結合してみてください。たとえば、個別のドライバーを登録する代わりに、 compatibleのある文字列またはデバイスのAuxデータを使用して区別することができます。または、ドライバモジュールを2つのモジュールに分割することもできます。

初期化および終了機能の例外

ライブラリモジュールはドライバーを登録せず、データ構造、ワークキュー、またはカーネルスレッドを設定するためにこれらの関数が必要になる可能性があるため、module_init module_init()およびmodule_exit() )の制限が免除されます。

MODULE_DEVICE_TABLEマクロを使用する

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

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

前方宣言されたデータ型を可視化するためにヘッダーファイルをインクルードしないでください。ヘッダーファイル( header-Ah )で定義された一部の構造体、共用体、およびその他のデータ型は、通常それらのデータ型へのポインターを使用する別のヘッダーファイル( header-Bh )で前方宣言できます。このコードパターンは、カーネルが意図的にデータ構造をheader-Bhのユーザーに対してプライベートに保とうとしていることを意味します。

header-Bhのユーザーは、これらの前方宣言されたデータ構造の内部に直接アクセスするために、 header-Ahを含めるべきではありません。これを行うと、別のカーネル(GKIカーネルなど)がモジュールをロードしようとしたときに、 CONFIG_MODVERSIONS CRCの不一致の問題(ABIコンプライアンスの問題が発生します)が発生します。

たとえば、 struct fwnode_handleinclude/linux/fwnode.hで定義されていますが、 struct fwnode_handle;カーネルがstruct fwnode_handleの詳細をinclude/linux/device.h /linux / device.hのユーザーから非公開にしようとしているため、 include/linux/device.hにあります。このシナリオでは、モジュールに#include <linux/fwnode.h>を追加して、 struct fwnode_handleのメンバーにアクセスしないでください。このようなヘッダーファイルをインクルードする必要があるデザインは、デザインパターンが悪いことを示しています。

コアカーネル構造に直接アクセスしないでください

コアカーネルデータ構造に直接アクセスまたは変更すると、メモリリーク、クラッシュ、将来のカーネルリリースとの互換性の低下などの望ましくない動作が発生する可能性があります。データ構造は、次のいずれかの条件を満たす場合のコアカーネルデータ構造です。

  • データ構造は、 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 deviceまたはstruct device.links内のフィールドは変更しないでください。

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

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

互換性のあるプロパティを持つデバイスツリーノードを解析しないでください

デバイスツリー(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ハンドルを使用してサプライヤーを検索する

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

レガシーシナリオ(ARMカーネルでのDTサポートなし)

以前は、DTサポートがARMカーネルに追加される前は、タッチデバイスなどのコンシューマーは、グローバルに一意の文字列を使用してレギュレーターなどのサプライヤーを検索していました。たとえば、ACME PMICドライバーは複数のレギュレーター( acme-pmic-ldo1からacme-pmic-ldo10など)を登録またはアドバタイズでき、タッチドライバーはregulator_get(dev, "acme-pmic-ldo10")を使用してレギュレーターを検索できます。 。ただし、別のボードでは、LDO8がタッチデバイスを提供する可能性があり、同じタッチドライバーが、タッチデバイスが使用されている各ボードのレギュレーターの正しいルックアップ文字列を決定する必要がある面倒なシステムを作成します。

現在のシナリオ(ARMカーネルでのDTサポート)

DTサポートがARMカーネルに追加された後、コンシューマーは、 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エラーを変更しないでください

regulatorclocksirqgpiophysextconなどのフレームワークAPIは、エラー戻り値として-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マスタードライバーモジュールが非同期プロービングを実行し、タッチドライバーモジュールがI2Cバス上にあるPMICに依存している場合、タッチドライバーとPMICドライバーが正しい順序でロードされていても、タッチドライバーのプローブが以前に試行される可能性があります。 PMICドライバープローブ。

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

構成には、#ifdefの代わりに#if IS_ENABLED()を使用します

#ifdefCONFIG_XXXの代わりに#ifdef CONFIG_XXX #if IS_ENABLED(CONFIG_XXX)を使用して、構成が将来トライステート構成に変更された場合でも、 #ifブロック内のコードがコンパイルされ続けるようにします。違いは次のとおりです。

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

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

条件付きコンパイルには正しいマクロを使用してください

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

  • ドライバーのCファイル(またはヘッダーファイルではないソースファイル)では、 #ifdef CONFIG_XXX_MODULEを使用しないでください。これは不必要に制限され、構成の名前がCONFIG_XYZに変更されると壊れます。モジュールにコンパイルされたヘッダー以外のソースファイルの場合、ビルドシステムはそのファイルのスコープに対してMODULEを自動的に定義します。したがって、Cファイル(またはヘッダー以外のソースファイル)がモジュールの一部としてコンパイルされているかどうかを確認するには、 #ifdef MODULECONFIG_プレフィックスなし)を使用します。

  • ヘッダーファイルでは、ヘッダーファイルが直接バイナリにコンパイルされるのではなく、Cファイル(または他のソースファイル)の一部としてコンパイルされるため、同じチェックがより巧妙になります。ヘッダーファイルには次のルールを使用します。

    • #ifdef MODULEを使用するヘッダーファイルの場合、結果は、それを使用しているソースファイルに基づいて変化します。これは、同じビルド内の同じヘッダーファイルで、コードのさまざまな部分をさまざまなソースファイル用にコンパイルできることを意味します(モジュールと組み込みまたは無効)。これは、組み込みコードの場合は1つの方法で展開し、モジュールの場合は別の方法で展開する必要があるマクロを定義する場合に役立ちます。

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