このページでは、Android の Extended Berkeley Packet Filter(eBPF)カーネル テレメトリー フレームワークの概要について説明します。フレームワーク アーキテクチャ、組み込み指標、新しいカーネル指標を追加する方法について説明します。
Android には、オーバーヘッドの少ないカーネル指標を収集し、それらの指標を statsd などのユーザー空間サービスに公開するための、統合された eBPF カーネル テレメトリ フレームワークが含まれています。ポータブル eBPF プログラムをカーネル トレースポイントと kprobe に接続することで、フレームワークは Generic Kernel Image(GKI)を変更したり、カーネル モジュール インターフェース(KMI)を壊したりすることなく、高頻度のカーネル イベントをその場で集約し、コンパクトなヒストグラムまたはカウンタをエクスポートします。
カーネル テレメトリーの従来のアプローチでは、カスタム procfs ノードまたは sysfs ノード、ツリー外のカーネル パッチ、ベンダーフックに依存しており、カーネル、SELinux、Java Native Interface(JNI)、ユーザー空間サービス全体でカスタム配管が必要になります。eBPF カーネル テレメトリー フレームワークは、Android 共通カーネル(ACK)全体でこのパイプラインを標準化し、次の利点を提供します。
- カーネル ABI と KMI のチャーンを回避する: カーネル ソースコードの変更、カーネル アプリケーション バイナリ インターフェース(ABI)の変更、
TRACE_ANDROID_VH_*などの Android ベンダーフックの追加を行うことなく、eBPF プログラムを既存のカーネル トレースポイントまたは kprobe にアタッチします。デバイス メーカーとシステム オン チップ(SoC)パートナーは、新しいテレメトリー ベンダーフックを追加する代わりに、または既存のベンダーフックから移行するために、eBPF テレメトリーを使用できます。 - カーネル バージョン間の移植性を維持: BPF Compile Once - Run Everywhere(CO-RE)と BPF Type Format(BTF)を使用することで、単一のコンパイル済み eBPF プログラムが、
android14-6.1以降の GKI バージョンにおける内部カーネル構造レイアウトの変更に自動的に適応します。 - システムヘルスのオーバーヘッドを最小限に抑える: 高頻度のカーネル イベントをソースで直接フィルタリングして集約し、コンパクトな 32 バケットの 2 進対数(
log2)ヒストグラムに変換します。通常、BPF マップあたり 1 KB 未満です。ユーザー空間は、1 つのBPF_MAP_LOOKUP_BATCHバッチ syscall を使用して、集計されたマップエントリを 1 日に 1 回取得します。 - SELinux と JNI の統合を簡素化:
/sys/fs/bpf/kernelstats/の下にテレメトリー マップを固定して、ディレクトリ レベルのfs_bpf_kernelstatsSELinux タイプ継承を適用します。また、BTF メタデータを使用して JNI の実行時に構造体メンバーのオフセットを解決し、新しい指標を追加する際にカスタム C++ 解析コードを不要にします。
Android が eBPF プログラムとマップを読み込んで固定する方法について詳しくは、eBPF によるカーネルの拡張をご覧ください。
アーキテクチャの概要
eBPF カーネル テレメトリー パイプラインは、一般的なインフラストラクチャ コンポーネントを指標固有の定義から分離するため、新しいカーネル指標を追加する際に必要なコード変更は最小限に抑えられます。次の表に、パイプラインのコンポーネントを示します。
| コンポーネント | パイプラインにおける役割 | スコープ |
|---|---|---|
BPF ローダ、bpfloader | 実行中のカーネルと互換性がある場合は、起動時に /system/etc/bpf/kernelstats/ からコンパイル済みの eBPF プログラムを読み込み、マップを /sys/fs/bpf/kernelstats/ に固定します。無効またはサポートされていない場合は、読み込みをスキップします。 | 汎用 |
SELinux ポリシー、fs_bpf_kernelstats | ディレクトリレベルの SELinux ラベルを /sys/fs/bpf/kernelstats/ に適用し、system_server が指標ごとの SELinux ポリシー更新なしでテレメトリー マップを読み取れるようにします。 | 汎用 |
system/bpfprogs/telemetry/ の eBPF プログラム | カーネル トレースポイントまたは kprobe にアタッチし、イベントをフィルタして、指標を BPF マップに集約します。 | 指標ごとにカスタム |
| JNI BTF リーダー | コンパイルされた BPF Executable and Linkable Format(ELF)バイナリから BTF メタデータを実行時に解析し、構造体メンバーのオフセットをキャッシュに保存して、プリミティブ Java 配列へのバッチマップ読み取りを実行します。 | 汎用 |
statsd のユーザースペース プラー | 累積ヒストグラム マップを毎日スケジュールに従って取得し、キャッシュに保存された履歴との差分を計算して、厳密に型指定された StatsEvent プルアトムを構築します。 | アトムごとに 1 つの case エントリを持つ汎用ヘルパー |
| テレメトリー スキーマ | 強く型付けされた statsd pull atom protobuf と、サーバーサイドのヒストグラムまたは値指標の集計を定義します。 | 指標ごとにカスタム |
ユーザー空間の使用量モデル
カーネル テレメトリ指標を設計する際は、指標が累積状態を表すか、順序付けられたイベント シーケンスを表すかに応じて、2 つのユーザー空間使用量モデルから選択します。次の表は、2 つのモデルを比較したものです。
| プロパティ | アキュムレータ | ストリーム |
|---|---|---|
| 定義 | ステートフルで単調増加する累積カウンタまたはヒストグラム | タイムスタンプ付きのカーネル イベントの順序付きシーケンス |
| アクセス パターン | ユーザースペースは、statsd を介して 24 時間に 1 回など、オンデマンドでポーリングします。 | ユーザー空間は、イベントのドロップを回避するために、リングバッファから継続的に消費します。 |
| 一般的な BPF マップタイプ | BPF_MAP_TYPE_HASH、BPF_MAP_TYPE_LRU_HASH、BPF_MAP_TYPE_PERCPU_HASH、BPF_MAP_TYPE_LRU_PERCPU_HASH | BPF_MAP_TYPE_RINGBUF |
| 同時実行モデル | カーネルマップのルックアップはアトミックです。複数のリーダーがユーザー空間のロックなしでポーリングできます。 | 破壊的な読み取りセマンティクスでは、コンシューマにディスパッチする単一のリーダー スレッドが必要 |
| サンプル ユースケース | メモリ管理レイテンシ ヒストグラム、ダイレクト メモリアクセス バッファ(DMA-BUF)割り当てサイズ、ウェイクアップ ソース イテレータ | フィルタされたスケジューラ トレース イベント、高周波サブシステム電力トレース |
組み込みのカーネル テレメトリー指標
Android には、GKI 6.1 以降のカーネル(android14-6.1、android15-6.6、android16-6.12、android17-6.18 など)で動作する eBPF カーネル テレメトリー プログラムが組み込まれています。各レイテンシ指標とサイズ指標では、24 時間のウィンドウで集計された 32 バケットの指数(log2)ヒストグラムが使用されます。次の表に、組み込みのカーネル テレメトリー指標を示します。
| 指標アトム | サブシステム | 説明 | ディメンション キー |
|---|---|---|---|
KswapdLatency | メモリ管理 | カーネル スワップ デーモン、kswapd、ウェイク トゥ スリープの実行レイテンシ(マイクロ秒) | 不均一メモリアクセス(NUMA)node_id |
DirectReclaimLatency | メモリ管理 | 同期直接メモリ再利用のレイテンシ(マイクロ秒単位) | プロセス メモリ不足(OOM)oom_score_adj 状態 |
CompactionDuration | メモリ管理 | システム全体のメモリ圧縮の実行時間(マイクロ秒単位)。以前の圧縮ベンダーフックを置き換えます。 | システム全体 |
CmaAllocationLatency | メモリ管理 | 連続メモリ アロケータ(CMA)のページ割り当てレイテンシ(マイクロ秒単位) | CMA ヒープ名 |
DmabufRequestSizes | メモリ管理 | DMA-BUF 割り当てリクエストのサイズ(バイト単位) | エクスポータのヒープ名 |
SuspendResumeLatency | 電源管理 | カーネルの一時停止と再開の各ステージのレイテンシ(マイクロ秒) | 一時停止アクション ステージ |
カーネル テレメトリー指標を追加する
eBPF カーネル テレメトリー パイプラインに新しいアキュムレータ指標を追加する手順は次のとおりです。
system/bpfprogs/telemetry/の下に eBPF プログラムを記述し、バイナリを/system/etc/bpf/kernelstats/にビルドします。- 汎用 JNI BTF リーダーの BTF 構造体のレイアウトの互換性を検証します。
statsdに pull atom を登録します。- pull アトム protobuf スキーマを定義します。
以降のセクションでは、DirectReclaimLatency(directReclaim.c)を例として使用します。
eBPF プログラムを作成する
system/bpfprogs/telemetry/ で、<bpf_telemetry.h> を含めて、マップキーと出力アキュムレータ マップを定義します。マップのメモリ使用量を最小限に抑え、プライバシーを保護するために、未加工の文字列ではなく、BPF マップキーで整数識別子または列挙型を使用します。
#include <android_bpf_defs.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf_telemetry.h>
#include <stdint.h>
struct direct_reclaim_key {
uint32_t process_state;
};
#define DIRECT_RECLAIM_START_MAP_ENTRIES 128
#define DIRECT_RECLAIM_OUTPUT_MAP_ENTRIES 8
// Temporary map to store start time. Key: pid (thread_id), Value: start_time_ns
DEFINE_BPF_MAP_GRO(direct_reclaim_start_map, HASH, uint32_t, uint64_t,
DIRECT_RECLAIM_START_MAP_ENTRIES, AID_SYSTEM);
// Output map. Key: direct_reclaim_key, Value: accumulator_value (32 log2 buckets)
DEFINE_BPF_MAP_GRO(direct_reclaim_latency_map, HASH, struct direct_reclaim_key,
struct accumulator_value, DIRECT_RECLAIM_OUTPUT_MAP_ENTRIES,
AID_SYSTEM);
トレースポイント ハンドラで、エントリの開始タイムスタンプを記録し、GET_LATENCY_BUCKET を使用して終了時に log2 レイテンシ バケットを計算し、INCREMENT_ACCUMULATOR を使用してアキュムレータ マップ エントリをインクリメントします。
DEFINE_BPF_PROG("tp_btf/mm_vmscan_direct_reclaim_begin", AID_ROOT, AID_SYSTEM,
tracepoint_vmscan_mm_vmscan_direct_reclaim_begin)(
uint64_t* args) {
(void)args;
uint32_t pid = (uint32_t)bpf_get_current_pid_tgid();
SAVE_FIRST_START_TIME(direct_reclaim_start_map, &pid);
return 0;
}
DEFINE_BPF_PROG("tp_btf/mm_vmscan_direct_reclaim_end", AID_ROOT, AID_SYSTEM,
tracepoint_vmscan_mm_vmscan_direct_reclaim_end)(
uint64_t* args) {
(void)args;
uint32_t pid = (uint32_t)bpf_get_current_pid_tgid();
uint32_t bucket = GET_LATENCY_BUCKET(direct_reclaim_start_map, &pid);
if (bucket == BPF_TELEMETRY_INVALID_BUCKET) return 0;
struct task_struct* task = (struct task_struct*)bpf_get_current_task();
int oom_score_adj = BPF_CORE_READ(task, signal, oom_score_adj);
enum process_state state = get_process_state(oom_score_adj);
struct direct_reclaim_key key = {.process_state = (uint32_t)state};
INCREMENT_ACCUMULATOR(direct_reclaim_latency_map, &key, bucket);
return 0;
}
BTF 構造体レイアウトの互換性を検証する
マップのキーと値の構造体が標準の整数、列挙型、固定配列型を使用している限り、新しいヒストグラム マップを追加するときにカスタム JNI C++ コードを記述する必要はありません。BPF マップ エントリをコンパイル時にハードコードされた C 構造体にキャストする代わりに、汎用 JNI リーダーは実行時に 3 つのフェーズで構造体レイアウトを動的に解決します。
- ELF からマップへの関連付け: JNI レイヤは、
libbpfbtf__parse関数を使用して、/system/etc/bpf/kernelstats/directReclaim.bpfなどのディスク上のコンパイル済み BPF ELF ファイルから BTF メタデータを直接解析します。 - BTF 型の解決: 最初のアクセス時に、リーダーはマップのキーと値の C 構造体の構造表現を構築してキャッシュに保存し、構造体メンバー、整数と列挙型のバイト幅、配列要素のサイズをベースポインタから計算します。
- バッチ ルックアップとシリアル化:
statsdがマップをプルすると、JNI レイヤはsys_bpfsyscall に単一のBPF_MAP_LOOKUP_BATCHコマンドを発行して、すべてのハッシュ エントリを一括で読み取り、解決されたキー ID とバケット配列をSetLongArrayRegionJNI 関数を使用してプリミティブ Javalong[]配列に直接コピーします。
将来のリリースで BPF C 構造体が変更された場合、再コンパイルされた BTF セクションが自動的に更新され、JNI リーダーが実行時に適応するため、JNI の再コンパイルは不要です。
pull アトムを statsd に登録
StatsPullAtomService.java では、汎用 pullKernelTelemetry ヘルパーが固定された BPF マップを読み取り、前のプルでキャッシュに保存されたカウントに対するバケットの差分を計算し、StatsEvent エントリを出力します。新しいカーネル テレメトリー指標を追加するには、次の 3 つの小さな追加のみが必要です。
// 1. Dispatch the atom tag to pullKernelTelemetry() in onPullAtomLocked():
case FrameworkStatsLog.DIRECT_RECLAIM_LATENCY:
return pullKernelTelemetry(atomTag, data);
// 2. Add the atom tag to the registered atoms list in registerKernelTelemetry():
int[] atoms = {
FrameworkStatsLog.DIRECT_RECLAIM_LATENCY,
// ...
};
// 3. Map the atom tag to its pinned BPF map path:
case FrameworkStatsLog.DIRECT_RECLAIM_LATENCY ->
"/sys/fs/bpf/kernelstats/map_directReclaim_direct_reclaim_latency_map";
pull アトムの protobuf を定義する
BPF マップで生成されるディメンション キーと 32 バケットの log2 ヒストグラム配列に一致するように、frameworks/proto_logging/ で厳密に型指定されたプルアトム メッセージを定義します。
message DirectReclaimLatency {
enum ProcessState {
PROCESS_STATE_UNSPECIFIED = 0;
STATE_NATIVE = 1;
STATE_SYSTEM = 2;
STATE_PERSISTENT = 3;
STATE_FOREGROUND = 4;
STATE_VISIBLE = 5;
STATE_PERCEPTIBLE = 6;
STATE_SERVICE_BACKGROUND = 7;
STATE_CACHED = 8;
}
optional ProcessState process_state = 1;
repeated int32 duration_log2_buckets_micros = 2;
}