このページでは、Android での kswapd と lmkd の相互作用について説明し、これらがどのように連携してメモリ圧迫を管理するかを示します。
背景
Android では、システムがメモリ不足になったときにメモリを再利用する主なメカニズムが 2 つあります。
kswapd: クリーンなファイルバック ページを強制排除するか、匿名ページを ZRAM にスワップすることで、ページを再利用する標準の Linux カーネル デーモン。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:truero.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 メカニズムを使用して、これらのサブプロセスの終了をモニタリングします。
- メインプロセスは、
bindServiceを使用してターゲット プロセスのサービスにバインドします。 - 接続すると、サービスを表す
IBinderオブジェクトを取得します。 - バインダーで
linkToDeathを呼び出して、IBinder.DeathRecipient実装を登録します。 - ターゲット プロセスが終了すると(LMKD によって強制終了された場合など)、システムは
binderDied()メソッドを呼び出し、アプリが終了イベントを非同期で記録できるようにします。
このバインディングによってターゲット プロセスの優先度が上がらないように(演習の目的が損なわれないように)、アプリは Context.BIND_WAIVE_PRIORITY フラグを使用します。
サンプルアプリの動作
以下は、Google Pixel 8 Pro(合計 RAM 容量は約 12 GB)でテスト実行が完了した後のアプリのスクリーンショットです。
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(最も強制終了しにくい)プロセスが強制終了されるまでの期間を示すトレースから取得したものです。
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_KILL1:NOT_RESPONDING2:LOW_SWAP_AND_THRASHING(ZRAM スワップ領域が極端に少なく、スラッシングが発生した)3:LOW_MEM_AND_SWAP(システムの空きメモリと ZRAM スワップの両方が不足している)4:LOW_MEM_AND_THRASHING5:DIRECT_RECL_AND_THRASHING6:LOW_MEM_AND_SWAP_UTIL7:LOW_FILECACHE_AFTER_THRASHING8:LOW_MEM9: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 の有効化: 空きメモリが特定のウォーターマークを下回ると、
kswapdが起動し(青色のインパルス)、ZRAM にスワップしてメモリを回収します。これは断続的なスパイクとして表示されます。 - PSI の急増:
kswapdが高い割り当て率に対応できなくなると、スレッドがメモリ割り当てでストールし始め、PSI(Pressure Stall Information)(緑色の線)が急増します。 - LMKD による強制終了: PSI がクリティカルなレベルに達し(たとえば、概念的に
ro.lmk.psi_complete_stall_msしきい値を超えた場合)、空きメモリが極端に少なくなると、lmkdが介入して優先度の低いプロセスを強制終了し(時間 30 と 45 のオレンジ色のインパルス)、大量のメモリを解放します。強制終了の直後に空きメモリが増加し、PSI が低下していることに注目してください。これは、強制終了が圧力の軽減に効果的であることを示しています。________________________________________________________________________________
← メモリの再利用: 削除とスワップ | ↑ 上へ | メモリページを OS に戻す →