eBPF によるカーネルの拡張

Extended Berkeley Packet Filter (eBPF) は、ユーザーが提供した eBPF プログラムを実行してカーネル機能を拡張するカーネル内仮想マシンです。これらのプログラムは、カーネル内のプローブまたはイベントにフックして、有用なカーネル統計の収集、監視、およびデバッグに使用できます。プログラムは、 bpf(2)システムコールを使用してカーネルにロードされ、eBPF マシン命令のバイナリ BLOB としてユーザーによって提供されます。 Android ビルド システムでは、このドキュメントで説明されている単純なビルド ファイル構文を使用して、C プログラムを eBPF にコンパイルすることがサポートされています。

eBPF の内部構造とアーキテクチャの詳細については、Brendan Gregg の eBPF ページを参照してください。

Android には、eBPF ローダーと、起動時に eBPF プログラムをロードするライブラリが含まれています。

Android BPF ローダー

Android の起動中に、 /system/etc/bpf/にあるすべての eBPF プログラムがロードされます。これらのプログラムは、C プログラムから Android ビルド システムによって構築されたバイナリ オブジェクトであり、Android ソース ツリー内のAndroid.bpファイルが付随しています。ビルド システムは生成されたオブジェクトを/system/etc/bpfに保存し、それらのオブジェクトはシステム イメージの一部になります。

Android eBPF C プログラムの形式

eBPF C プログラムは次の形式である必要があります。

#include <bpf_helpers.h>

/* Define one or more maps in the maps section, for example
 * define a map of type array int -> uint32_t, with 10 entries
 */
DEFINE_BPF_MAP(name_of_my_map, ARRAY, int, uint32_t, 10);

/* this will also define type-safe accessors:
 *   value * bpf_name_of_my_map_lookup_elem(&key);
 *   int bpf_name_of_my_map_update_elem(&key, &value, flags);
 *   int bpf_name_of_my_map_delete_elem(&key);
 * as such it is heavily suggested to use lowercase *_map names.
 * Also note that due to compiler deficiencies you cannot use a type
 * of 'struct foo' but must instead use just 'foo'.  As such structs
 * must not be defined as 'struct foo {}' and must instead be
 * 'typedef struct {} foo'.
 */

DEFINE_BPF_PROG("PROGTYPE/PROGNAME", AID_*, AID_*, PROGFUNC)(..args..) {
   <body-of-code
    ... read or write to MY_MAPNAME
    ... do other things
   >
}

LICENSE("GPL"); // or other license

どこ:

  • name_of_my_map 、マップ変数の名前です。この名前は、作成するマップのタイプとパラメータを BPF ローダーに通知します。この構造体定義は、組み込まれているbpf_helpers.hヘッダーによって提供されます。
  • PROGTYPE/PROGNAMEプログラムの種類とプログラム名を表します。プログラムの種類は、次の表にリストされているもののいずれかになります。プログラムの種類がリストされていない場合、そのプログラムには厳密な命名規則がありません。名前は、プログラムを接続するプロセスに知られていれば十分です。

  • PROGFUNC 、コンパイル時に結果のファイルのセクションに配置される関数です。

kプローブkprobe インフラストラクチャを使用してPROGFUNCカーネル命令にフックします。 PROGNAME kprobe されるカーネル関数の名前である必要があります。 kprobe の詳細については、kprobe カーネルのドキュメントを参照してください。
トレースポイントPROGFUNCトレースポイントにフックします。 PROGNAME SUBSYSTEM/EVENTの形式である必要があります。たとえば、スケジューラ コンテキスト スイッチ イベントに関数を付加するためのトレースポイント セクションはSEC("tracepoint/sched/sched_switch")になります。ここで、 schedはトレース サブシステムの名前、 sched_switchはトレース イベントの名前です。トレースポイントの詳細については、トレース イベント カーネルのドキュメントを確認してください。
スクフィルタープログラムはネットワークソケットフィルターとして機能します。
スケジュールプログラムはネットワーク トラフィック分類子として機能します。
cgroupskb、cgroupsock CGroup 内のプロセスが AF_INET または AF_INET6 ソケットを作成するたびに、プログラムが実行されます。

追加のタイプは、ローダーのソース コードにあります。

たとえば、次のmyschedtp.cプログラムは、特定の CPU で実行された最新のタスク PID に関する情報を追加します。このプログラムは、マップを作成し、 sched:sched_switchトレース イベントに付加できるtp_sched_switch関数を定義することで目的を達成します。詳細については、 「トレースポイントへのプログラムの接続」を参照してください。

#include <linux/bpf.h>
#include <stdbool.h>
#include <stdint.h>
#include <bpf_helpers.h>

DEFINE_BPF_MAP(cpu_pid_map, ARRAY, int, uint32_t, 1024);

struct switch_args {
    unsigned long long ignore;
    char prev_comm[16];
    int prev_pid;
    int prev_prio;
    long long prev_state;
    char next_comm[16];
    int next_pid;
    int next_prio;
};

DEFINE_BPF_PROG("tracepoint/sched/sched_switch", AID_ROOT, AID_SYSTEM, tp_sched_switch)
(struct switch_args *args) {
    int key;
    uint32_t val;

    key = bpf_get_smp_processor_id();
    val = args->next_pid;

    bpf_cpu_pid_map_update_elem(&key, &val, BPF_ANY);
    return 1; // return 1 to avoid blocking simpleperf from receiving events
}

LICENSE("GPL");

LICENSE マクロは、プログラムがカーネルによって提供される BPF ヘルパー関数を使用するときに、プログラムがカーネルのライセンスと互換性があるかどうかを確認するために使用されます。プログラムのライセンスの名前を文字列形式で指定します ( LICENSE("GPL")LICENSE("Apache 2.0")

Android.bp ファイルの形式

Android ビルド システムで eBPF .cプログラムをビルドするには、プロジェクトのAndroid.bpファイルにエントリを作成する必要があります。たとえば、 bpf_test.cという名前の eBPF C プログラムをビルドするには、プロジェクトのAndroid.bpファイルに次のエントリを作成します。

bpf {
    name: "bpf_test.o",
    srcs: ["bpf_test.c"],
    cflags: [
        "-Wall",
        "-Werror",
    ],
}

このエントリは C プログラムをコンパイルし、オブジェクト/system/etc/bpf/bpf_test.oを生成します。起動時に、Android システムはbpf_test.oプログラムをカーネルに自動的にロードします。

sysfsで利用可能なファイル

起動中に、Android システムは/system/etc/bpf/からすべての eBPF オブジェクトを自動的にロードし、プログラムに必要なマップを作成し、ロードされたプログラムとそのマップを BPF ファイル システムに固定します。これらのファイルは、eBPF プログラムとのさらなる対話やマップの読み取りに使用できます。このセクションでは、これらのファイルの命名規則と sysfs 内のファイルの場所について説明します。

次のファイルが作成され、固定されます。

  • ロードされたプログラムについて、 PROGNAMEがプログラムの名前、 FILENAMEが eBPF C ファイルの名前であると仮定すると、Android ローダーは各プログラムを作成し、 /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAMEに固定します。

    たとえば、 myschedtp.c内の前述のsched_switchトレースポイントの例では、プログラム ファイルが作成され、 /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switchに固定されます。

  • 作成されたマップについては、 MAPNAMEがマップの名前、 FILENAMEが eBPF C ファイルの名前であると仮定すると、Android ローダーは各マップを作成し、 /sys/fs/bpf/map_FILENAME_MAPNAMEに固定します。

    たとえば、 myschedtp.c内の前述のsched_switchトレースポイントの例では、マップ ファイルが作成され、 /sys/fs/bpf/map_myschedtp_cpu_pid_mapに固定されます。

  • Android BPF ライブラリのbpf_obj_get()は、固定された/sys/fs/bpfファイルからファイル記述子を返します。このファイル記述子は、マップの読み取りやトレースポイントへのプログラムの添付など、さらなる操作に使用できます。

Android BPF ライブラリ

Android BPF ライブラリはlibbpf_android.soという名前で、システム イメージの一部です。このライブラリは、マップの作成と読み取り、プローブ、トレースポイント、およびパフォーマンス バッファーの作成に必要な低レベルの eBPF 機能をユーザーに提供します。

トレースポイントへのプログラムの接続

トレースポイント プログラムは起動時に自動的にロードされます。ロード後、次の手順を使用してトレースポイント プログラムをアクティブにする必要があります。

  1. bpf_obj_get()を呼び出して、固定されたファイルの場所からプログラムfdを取得します。詳細については、 「 sysfs で利用可能なファイル」を参照してください。
  2. BPF ライブラリのbpf_attach_tracepoint()を呼び出し、プログラムfdとトレースポイント名を渡します。

次のコード サンプルは、前のmyschedtp.cソース ファイルで定義されているsched_switchトレースポイントを接続する方法を示しています (エラー チェックは示されていません)。

  char *tp_prog_path = "/sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch";
  char *tp_map_path = "/sys/fs/bpf/map_myschedtp_cpu_pid";

  // Attach tracepoint and wait for 4 seconds
  int mProgFd = bpf_obj_get(tp_prog_path);
  int mMapFd = bpf_obj_get(tp_map_path);
  int ret = bpf_attach_tracepoint(mProgFd, "sched", "sched_switch");
  sleep(4);

  // Read the map to find the last PID that ran on CPU 0
  android::bpf::BpfMap<int, int> myMap(mMapFd);
  printf("last PID running on CPU %d is %d\n", 0, myMap.readValue(0));

地図から読み取る

BPF マップは、任意の複雑なキーと値の構造または型をサポートします。 Android BPF ライブラリには、C++ テンプレートを使用して、問題のマップのキーと値の型に基づいてBpfMapをインスタンス化するandroid::BpfMapクラスが含まれています。前のコード サンプルは、キーと値を整数として持つBpfMapの使用を示しています。整数は任意の構造にすることもできます。

したがって、テンプレート化されたBpfMapクラスを使用すると、特定のマップに適したカスタムBpfMapオブジェクトを簡単に定義できます。その後、型を認識するカスタム生成関数を使用してマップにアクセスできるため、よりクリーンなコードが得られます。

BpfMapの詳細については、 Android のソースを参照してください。

デバッグの問題

ブート時に、BPF ロードに関連するいくつかのメッセージがログに記録されます。何らかの理由でロードプロセスが失敗した場合、詳細なログメッセージが logcat に提供されます。 logcat ログを「bpf」でフィルタリングすると、すべてのメッセージとロード時の詳細なエラー (eBPF 検証エラーなど) が出力されます。

Android での eBPF の例

AOSP の次のプログラムは、eBPF の使用例をさらに示しています。

  • netd eBPF C プログラムは、ソケット フィルタリングや統計収集などのさまざまな目的で、Android のネットワーク デーモン (netd) によって使用されます。このプログラムがどのように使用されているかを確認するには、 eBPF トラフィック モニターのソースを確認してください。

  • time_in_state eBPF C プログラムは、 Android アプリがさまざまな CPU 周波数で費やす時間を計算し、電力の計算に使用されます。

  • Android 12 では、 gpu_mem eBPF C プログラムは、各プロセスおよびシステム全体の合計 GPU メモリ使用量を追跡します。このプログラムは GPU メモリ プロファイリングに使用されます。