kswapd と lmkd のインタラクション

このページでは、Android での kswapd と lmkd の相互作用について説明し、これらがどのように連携してメモリ圧迫を管理するかを示します。

背景

Android では、システムがメモリ不足になったときにメモリを再利用する主なメカニズムが 2 つあります。

  1. kswapd: クリーンなファイルバック ページを強制排除するか、匿名ページを ZRAM にスワップすることで、ページを再利用する標準の Linux カーネル デーモン。
  2. lmkd: Android のユーザー空間のローメモリ キラーデーモン。kswapd がメモリ需要に対応できず、システム負荷が増加すると(PSI で通知)、lmkd が介入してプロセスを強制終了し、より大きなメモリチャンクを解放します。

kswapd と lmkd は、2 つの補完的なメモリ再利用システムです。どちらも同じ目標(空きメモリの作成)を達成するために機能しますが、手段が異なります。比較対照すると

  • 回復可能と破壊的: kswapd は、ストレージから読み戻すことができるメモリページをドロップするか、メモリページを他のメモリ パーティションまたはオンデマンドで読み戻すことができるストレージにスワップします。lmkd はプロセスを強制終了し、不揮発性の状態を破棄します。
  • 再利用帯域幅: kswapd は、一定量の空きメモリを再利用するために lmkd よりも多くの CPU を消費し、そのメモリの再利用に時間がかかります。lmkd は、比較的少ない労力で短時間に大量のメモリを再利用できます。モバイル デバイスでは、CPU の負荷がレイテンシだけでなく、バッテリー消費にも影響することに注意してください。

OOM スコアバケット

Android は、ユーザーにとっての重要度に基づいて、プロセスにメモリ不足(OOM)スコア調整(oom_score_adj)を割り当てます。スコアの高いプロセスから強制終了されます。

一般的なバケットには、次のものがあります。

  • キャッシュに保存済み(スコア >= 900): 現在は必要ないが、再起動を高速化するために保持されているプロセス。
  • Service(スコア 500): 開始されたサービスをホストするプロセス。
  • フォアグラウンド サービス(スコア 200): フォアグラウンド サービスをホストするプロセス。
  • フォアグラウンド(スコア 0): ユーザーが現在操作しているアプリ。

特集: PSI と LMKD のトリガー

PSI とは

プレッシャー ストール情報(PSI)は、リソース不足を測定する標準的な方法を提供する Linux カーネル機能です。CPU、メモリ、I/O の不足によりタスクが遅延した時間を追跡します。

メモリ PSI: 「some」と「full」

メモリ PSI は、メモリ不足による遅延(ページ再利用や再フォールトの待機など)を特に追跡します。次の 2 つの指標が提供されます。

  • some: 少なくとも 1 つのタスクがメモリ上でストールした時間の割合。これは、メモリ不足がパフォーマンスに影響しているものの、一部の処理は引き続き進行していることを示します。
  • full: アイドル状態でないすべてのタスクが同時に停止した時間の割合。これは、システムがメモリを待機して完全にブロックされている重大な状態を示します。

PSI が LMKD をトリガーする仕組み

ユーザースペースの lmkd デーモンは、PSI しきい値を超えたときに通知を受け取るようにカーネルに登録します(/proc/pressure/memory をモニタリングします)。システムでメモリ不足が発生すると(たとえば、full のストールが構成された時間を超えると)、カーネルは lmkd を起動します。lmkd は、システムの状態を評価し、OOM スコアとサイズに基づいて、プロセスを強制終了するかどうか、およびどのプロセスを強制終了するかを決定します。

LMKD のチューニング

LMKD の動作は、システム プロパティを使用して調整できます。

デバイスのチューニング値の確認

接続されたデバイスの現在の LMKD チューニング値を確認するには、次のコマンドを実行します。

adb shell getprop | grep lmk

一般的な LMKD プロパティ

LMKD の調整に使用される主なプロパティを次に示します(完全なリストについては、Android のドキュメントをご覧ください)。

  • ro.lmk.use_psi: メモリ プレッシャーの検出に PSI モニターを使用するには、true に設定します(Android 10 以降のデフォルト)。
  • ro.lmk.psi_partial_stall_ms: 強制終了をトリガーする some(部分的な)ストール時間のしきい値(ミリ秒単位)。デフォルトは、通常のデバイスでは 70 ミリ秒、RAM の少ないデバイスでは 200 ミリ秒です。
  • ro.lmk.psi_complete_stall_ms: 強制終了をトリガーする full のストール時間のしきい値(ミリ秒単位、デフォルトは 700 ミリ秒)。
  • ro.lmk.kill_heaviest_task: true の場合、LMKD は同じ OOM バケット内の候補の中で最もメモリを使用しているプロセスを強制終了します。

テストデバイス(Google Pixel 8 Pro)の関連するチューニング値

(注: Android 15 を搭載したハイエンド デバイスの一般的なデフォルト)

  • ro.lmk.use_psi: true
  • ro.lmk.psi_partial_stall_ms: 70(標準)
  • ro.lmk.psi_complete_stall_ms: 700(標準)
  • ro.lmk.kill_heaviest_task: true

ZRAM とスワップの検証

ZRAM の容量は、スワップが満杯になる速度と LMKD による強制終了のトリガーに大きく影響します。デバイスの ZRAM とスワップのステータスを確認するには、次のコマンドを使用します。

# Check ZRAM sizing and usage
adb shell free -m

# Check swap partition details
adb shell cat /proc/swaps

実践的な演習: kswapd と lmkd のトリガー

この演習では、KswapdLmkdLab サンプルアプリを使用して、さまざまな OOM バケットにプロセスを作成し、メインプロセスでメモリを消費して kswapd と lmkd をトリガーします。

このアプリは、サブプロセスを自動的に生成し、メモリを割り当てて、LMKD の魅力的なターゲットになるように構成されています。

1. サブプロセスを作成する

このアプリは、起動時に次のサブプロセスを自動的に生成します。

  • キャッシュに保存されたプロセス(oom_score_adj >= 900)
  • サービス プロセス(oom_score_adj 500)
  • フォアグラウンド サービス プロセス(oom_score_adj 200)

ADB を使用して、プロセスとその OOM スコアを確認します。

adb shell dumpsys activity processes | grep com.android.kswapdlmkdlab

出力スニペットの例:

    Proc #  0: fg       T/A/TOP  LCMNFUATI  t: 0 18959:com.android.kswapdlmkdlab/u0a358 (top-activity)
    Proc #  1: prcp     b/ /FGS  -------TI  t: 0 19050:com.android.kswapdlmkdlab:foreground/u0a358 (fg-service)
    Proc #  2: svc      b/ /SVC  -------TI  t: 0 19077:com.android.kswapdlmkdlab:service/u0a358 (started-services)
    Proc #  3: cch      b/ /CACC -------TI  t: 0 19014:com.android.kswapdlmkdlab:cached/u0a358 (cch-client-act)

プロセス状態の略語は、次のとおりです。

  • fg: フォアグラウンド(OOM スコア 0)
  • prcp: 知覚可能(フォアグラウンド サービス、OOM スコア 200)
  • svc: サービスを開始しました(OOM スコア 500)
  • cch: キャッシュに保存されたプロセス(OOM スコア 900)

2. メモリ負荷をトリガーする

アプリは、メインプロセスでメモリの割り当てを自動的に開始します。RAM 容量の大きいデバイスでテストを効果的に行うため、アプリはサブプロセスにメモリを割り当てる(例: 各 500 MB)よう指示し、LMKD のターゲットとして適したものにします。

サブプロセスの終了をモニタリングする

アプリは、IBinder.linkToDeath メカニズムを使用して、これらのサブプロセスの終了をモニタリングします。

  1. メインプロセスは、bindService を使用してターゲット プロセスのサービスにバインドします。
  2. 接続すると、サービスを表す IBinder オブジェクトを取得します。
  3. バインダーで linkToDeath を呼び出して、IBinder.DeathRecipient 実装を登録します。
  4. ターゲット プロセスが終了すると(LMKD によって強制終了された場合など)、システムは binderDied() メソッドを呼び出し、アプリが終了イベントを非同期で記録できるようにします。

このバインディングによってターゲット プロセスの優先度が上がらないように(演習の目的が損なわれないように)、アプリは Context.BIND_WAIVE_PRIORITY フラグを使用します。

サンプルアプリの動作

以下は、Google Pixel 8 Pro(合計 RAM 容量は約 12 GB)でテスト実行が完了した後のアプリのスクリーンショットです。

テスト実行完了後のサンプルアプリのスクリーンショット。
図 1. Google Pixel 8 Pro でテスト実行が完了した後のサンプルアプリ。

Perfetto トレース分析

インタラクションをキャプチャするには、次の Perfetto 構成を使用して、スケジューリング、メモリ カウンタ、LMK による強制終了、圧力ストール情報(PSI)をキャプチャします。

Perfetto の構成

スケジューリング、メモリ カウンタ、LMK による強制終了、プレッシャーのストールに関する情報(PSI)を指定して Perfetto を実行し、インタラクションをキャプチャします。

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/kswapd_lmkd.perfetto-trace <<EOF
buffers: { size_kb: 131072 }
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "lowmemorykiller/lowmemory_kill"
            ftrace_events: "vmscan/mm_vmscan_kswapd_wake"
            ftrace_events: "vmscan/mm_vmscan_kswapd_sleep"
            ftrace_events: "psi/psi_event"
        }
    }
}
duration_ms: 180000
EOF

トレースを確認する

次のスクリーンショットは、:cached(最も強制終了しやすい)プロセスが強制終了される直前から、:foreground(最も強制終了しにくい)プロセスが強制終了されるまでの期間を示すトレースから取得したものです。

kswapd と lmkd のアクティビティを示す Perfetto UI のスクリーンショット。
図 2. kswapd の再利用と lmkd の強制終了を示す Perfetto トレース。

MemFree はすでにかなり低く、常に圧力がかかっていることがわかります。kswapd は常に動作しており、kswapdlmkdlab プロセス(メインプロセスとサブプロセス)すべてから、そしておそらくこのスクリーンショットに表示されていない他のプロセスから、mem.rss.file ページをドロップして mem.rss.anon を mem.swap に移動することで、圧力を軽減しています。同時に、kswapdlmkdlab アプリはメモリの割り当てを継続し、kswapd が再利用できるよりも速くメモリを消費します。

psi.mem が十分な期間にわたって高くなっていることからわかるように、圧力が過度に高くなると、lmkd が起動して一部のプロセスを強制終了します。lmkd が OOM スコアを尊重し、最初に kswapdlmkdlab:cached、次に :service、次に :foreground を強制終了していることがわかります。

イベントのタイムラインを確認し、次の点について考えてみましょう。

  • kswapd と lmkd が互いに補完する方法: kswapd はメモリ不足に最初に応答しますが、kswapd の再利用帯域幅が割り当て需要を満たしていない場合は lmkd が役立ちます。
  • メモリが解放される速度: kswapd で費やされた CPU 時間と、mem.rss.file のドロップまたは mem.rss.anon から mem.swap へのスワップによって再利用されるメモリを観察します。これを、lmkd での短時間のアクティビティのバーストと比較します。このバーストにより、大量のプロセス プライベート メモリが解放されます。

クエリ

Perfetto UI(Query SQL)でこれらのクエリを実行して、トレースからデータを抽出します。

1. kswapd アクティビティ

カーネルのスワップ デーモンがアクティブで実行中だった日時を確認するには、次のコマンドを実行します。

SELECT ts, dur, state
FROM thread_state
JOIN thread USING (utid)
WHERE thread.name LIKE 'kswapd%'
  AND state = 'Running'
LIMIT 5;

出力例:

ts dur state
30317633052969 159180 実行中
30317633398347 3999186 実行中
30317638448111 1847941 実行中
30317641455354 1031006 実行中
30317643272615 888387 実行中

2. LMKD による強制終了

LMKD は、ローメモリ キルを instant テーブルに書き込みます。すべての LMKD 強制終了をクエリするには:

SELECT ts, name
FROM instant
WHERE name LIKE 'lmk,%'
ORDER BY ts ASC;

イベント名の形式は lmk,<pid>,<kill_reason>,<oom_score_adj>,<min_oom_score>,<max_thrashing> です。

  • kill_reason: lmkd がプロセスを強制終了した理由を示す整数(system/memory/lmkd/statslog.h で定義):
    • 0: PRESSURE_AFTER_KILL
    • 1: NOT_RESPONDING
    • 2: LOW_SWAP_AND_THRASHING(ZRAM スワップ領域が極端に少なく、スラッシングが発生した)
    • 3: LOW_MEM_AND_SWAP(システムの空きメモリと ZRAM スワップの両方が不足している)
    • 4: LOW_MEM_AND_THRASHING
    • 5: DIRECT_RECL_AND_THRASHING
    • 6: LOW_MEM_AND_SWAP_UTIL
    • 7: LOW_FILECACHE_AFTER_THRASHING
    • 8: LOW_MEM
    • 9: DIRECT_RECL_STUCK
  • max_thrashing: 強制終了時の最大ページ スラッシング率。

出力例:

ts name
30326148679967 lmk,2308,3,900,201,0
30330439763710 lmk,2309,3,500,201,0
30334985999837 lmk,2310,2,200,0,307

可視化

データを可視化するには、次の gnuplot スクリプトを使用して、システムの空きメモリ、kswapd アクティビティ、lmkd による強制終了、PSI を単一の時系列にプロットします。このスクリプトは、Android ビルドの最上位ディレクトリから実行されることを前提としています。

出力プロットの例

次のプロットは、テスト実行を表すサンプルデータを使用して、上記のスクリプトで生成されたものです。

空きメモリ、kswapd アクティビティ、PSI、lmkd による強制終了のプロット。
図 3. 空きメモリ、kswapd、PSI、lmkd の強制終了を示すメモリ負荷プロット。

プロットの説明:

このプロットは、ストレステスト中のさまざまなメモリ管理コンポーネント間の動的な相互作用を示しています。

  1. 空きメモリの減少: メインアプリがメモリを割り当てると、システムの空きメモリ(紫色の線)が着実に減少します。
  2. kswapd の有効化: 空きメモリが特定のウォーターマークを下回ると、kswapd が起動し(青色のインパルス)、ZRAM にスワップしてメモリを回収します。これは断続的なスパイクとして表示されます。
  3. PSI の急増: kswapd が高い割り当て率に対応できなくなると、スレッドがメモリ割り当てでストールし始め、PSI(Pressure Stall Information)(緑色の線)が急増します。
  4. LMKD による強制終了: PSI がクリティカルなレベルに達し(たとえば、概念的に ro.lmk.psi_complete_stall_ms しきい値を超えた場合)、空きメモリが極端に少なくなると、lmkd が介入して優先度の低いプロセスを強制終了し(時間 30 と 45 のオレンジ色のインパルス)、大量のメモリを解放します。強制終了の直後に空きメモリが増加し、PSI が低下していることに注目してください。これは、強制終了が圧力の軽減に効果的であることを示しています。________________________________________________________________________________

← メモリの再利用: 削除とスワップ | ↑ 上へ | メモリページを OS に戻す →