FUSE パススルー

Android 12 は FUSE パススルーをサポートしています。これにより、FUSE のオーバーヘッドを最小限に抑えて、下位のファイル システムに直接アクセスする場合と同等のパフォーマンスを実現できます。FUSE パススルーは、android12-5.4android12-5.10android-mainline(テスト専用)の各カーネルでサポートされています。そのため、この機能がサポートされるかどうかは、デバイスで使用しているカーネルと、デバイスで稼働している Android のバージョンによって決まります。

  • Android 11 から Android 12 にアップグレードするデバイスでは、FUSE パススルーをサポートできません。Android 11 デバイスのカーネルは固定されており、FUSE パススルーの変更を追加して正式にアップグレードされたカーネルに移行することができないからです。

  • Android 12 でリリースされるデバイスでは、公式カーネルを使用することで FUSE パススルーをサポートできます。また、FUSE パススルーを実装する Android フレームワーク コードが MediaProvider メインライン モジュールに埋め込まれ、自動的にアップグレードされます。MediaProvider をメインライン モジュールとして実装していないデバイス(Android Go デバイスなど)でも、MediaProvider の変更にアクセスできます。これは、MediaProvider の変更が一般公開されているためです。

FUSE と SDCardFS の比較

FUSE(File system in Userspace)は、カーネル(FUSE ドライバ)によって、FUSE ファイル システムで実行される操作をユーザー空間プログラム(FUSE デーモン)にアウトソーシングして実行させるメカニズムです。Android 11 では、SDCardFS のサポートが終了し、FUSE がストレージ エミュレーションのデフォルトのソリューションとなりました。この変更の一環として、ファイル アクセスのインターセプト、追加のセキュリティとプライバシー機能の適用、ランタイム時のファイル操作を行う Android 独自の FUSE デーモンを実装しました。

FUSE は、ページや属性などのキャッシュ可能な情報を処理する際には良好に機能しますが、外部ストレージにアクセスするときにパフォーマンスの低下が生じます。特にミッドエンドおよびローエンド デバイスでこの傾向は顕著です。パフォーマンスの低下(軽量で、カーネルに完全に実装されている下位のファイル システムへの直接アクセスと比べた場合)が発生する原因は、FUSE ファイル システムの実装で連携する一連のコンポーネントと、FUSE ドライバと FUSE デーモン間の通信におけるカーネル空間からユーザー空間への複数のスイッチです。

このようなパフォーマンスの低下を軽減するためには、アプリでスプライシングを使用してデータコピーを減らし、ContentProvider API を使用して、下位のファイル システムのファイルに直接アクセスしてください。これらの最適化やその他の最適化を行っても、FUSE を使用した場合、下位のファイル システムに直接アクセスする場合に比べて、読み取り / 書き込み操作の帯域幅が減少する可能性があります。特に、ランダム読み取り操作では、キャッシュ保存や先読みが有効ではないため、帯域幅が減少する可能性があります。また、従来の /sdcard/ パスを介してストレージに直接アクセスするアプリでは、パフォーマンスは引き続き大きく低下します。特に IO 集約型の操作を実行する場合にこの傾向が顕著です。

SDcardFS のユーザー空間のリクエスト

SDcardFS を使用すると、カーネルからのユーザー空間の呼び出しが排除され、FUSE のストレージ エミュレーションと権限チェックが高速化されます。ユーザー空間のリクエストは、ユーザー空間 → VFS → sdcardfs → VFS → ext4 → ページ キャッシュ/ストレージのパスで行われます。

FUSE パススルー SDcardFS

図 1. SDcardFS のユーザー空間のリクエスト

FUSE のユーザー空間のリクエスト

FUSE は当初、ストレージ エミュレーションを有効にして、アプリが内部ストレージと外部 SD カードのいずれかを透過的に使用できるようにするために用いられていました。FUSE を使用すると、各ユーザー空間のリクエストがユーザー空間 → VFS → FUSE ドライバ → FUSE デーモン → VFS → ext4 → ページ キャッシュ/ストレージのパスで行われるため、一定のオーバーヘッドが生じます。

FUSE パススルー FUSE

図 2. FUSE のユーザー空間のリクエスト

FUSE のパススルー リクエスト {#fuse-passthrough-requests}

ほとんどのファイル アクセス許可はファイルを開いたときにチェックされ、ファイルの読み書きのときにも追加の権限チェックが行われます。一部のケースでは、ファイルを開いたときに、リクエスト元のアプリがリクエスト対象のファイルへの完全アクセス権を持っていることが判明するため、システムは読み取りおよび書き込みリクエストを FUSE ドライバから FUSE デーモンに転送する必要はありません(この操作はデータを別の場所に移動するだけであるため)。

FUSE パススルーでは、ファイルを開くリクエストを処理する FUSE デーモンは、操作が許可されていることと、後続のすべての読み取りと書き込みリクエストが下位のファイル システムに直接転送できることを FUSE ドライバに通知します。これにより、ユーザー空間 FUSE デーモンが FUSE ドライバ リクエストに応答するのを待機することで発生する余計なオーバーヘッドを回避できます。

FUSE のリクエストと FUSE のパススルー リクエストの比較を以下に示します。

FUSE パススルーの比較

図 3. FUSE リクエストと FUSE パススルー リクエストの比較

アプリが FUSE ファイル システムにアクセスすると、次の操作が発生します。

  1. FUSE ドライバは、リクエストを処理してキューに登録し、FUSE デーモンに渡します。この FUSE デーモンは、/dev/fuse ファイル(FUSE デーモンは読み取りをブロックされています)にある特定の接続インスタンスを通じて FUSE ファイル システムを処理します。

  2. FUSE デーモンは、ファイルを開くリクエストを受信すると、そのファイルに対して FUSE パススルーを利用できるかどうかを判断します。FUSE パススルーが利用でき場合、デーモンは次の処理を行います。

    1. このリクエストを FUSE ドライバに通知する。

    2. FUSE_DEV_IOC_PASSTHROUGH_OPEN ioctl を使用して、そのファイルに対して FUSE パススルーを有効にする。これは、開いた /dev/fuse のファイル記述子で実行する必要があります。

  3. ioctl は、以下を含むデータ構造を(パラメータとして)受け取ります。

    • パススルー機能のターゲットである下位ファイル システムのファイルのファイル記述子。

    • 現在処理されている FUSE リクエストの一意の識別子(開くか、作成して開く必要があります)。

    • 空白のままにしておき、将来の実装に使用できる追加のフィールド。

  4. ioctl に成功すると、FUSE デーモンがファイルを開くリクエストを完了して、FUSE ドライバが FUSE デーモンの応答を処理し、下位のファイル システムのファイルへの参照がカーネル内の FUSE ファイルに追加されます。アプリが FUSE ファイルに対する読み取り / 書き込み操作をリクエストすると、FUSE ドライバは下位のファイル システムのファイルへの参照を使用できるかどうかを確認します。

    • 参照を使用できる場合、ドライバは、下位のファイル システムのファイルをターゲットとする同じパラメータの新しい仮想ファイル システム(VFS)リクエストを作成します。

    • 参照を使用できない場合、ドライバは FUSE デーモンにリクエストを転送します。

上記の操作は、汎用ファイルに対する読み取り / 書き込み操作と読み取り反復 / 書き込み反復操作、およびメモリにマッピングされたファイルに対する読み取り / 書き込み操作について発生します。特定のファイルに対する FUSE パススルーは、そのファイルが閉じられるまで存在します。

FUSE パススルーの実装

Android 12 搭載デバイスで FUSE パススルーを有効にするには、次の行をターゲット デバイスの $ANDROID_BUILD_TOP/device/…/device.mk ファイルに追加します。

# Use FUSE passthrough
PRODUCT_PRODUCT_PROPERTIES += \
    persist.sys.fuse.passthrough.enable=true

FUSE パススルーを無効にするには、上記の設定変更を省略するか、persist.sys.fuse.passthrough.enablefalse に設定します。FUSE パススルーを有効にしていた場合、無効にするとデバイス FUSE パススルーを使用できなくなりますが、デバイスは引き続き機能します。

デバイスをフラッシュせずに FUSE パススルーを有効または無効にするには、ADB コマンドを使用してシステム プロパティを変更します。たとえば、次のようになります。

adb root
adb shell setprop persist.sys.fuse.passthrough.enable {true,false}
adb reboot

詳細については、リファレンス実装をご覧ください。

FUSE パススルーの検証

MediaProvider が FUSE パススルーを使用していることを検証するには、logcat でデバッグ メッセージを確認します。例:

adb logcat FuseDaemon:V \*:S
--------- beginning of main
03-02 12:09:57.833  3499  3773 I FuseDaemon: Using FUSE passthrough
03-02 12:09:57.833  3499  3773 I FuseDaemon: Starting fuse...

ログの FuseDaemon: Using FUSE passthrough エントリから、FUSE パススルーが使用されていることがわかります。

Android 12 CTS には CtsStorageTest が含まれています。これには FUSE パススルーをトリガーするテストが含まれます。テストを手動で実行するには、次のように atest を使用します。

atest CtsStorageTest