eBPF によるカーネルの拡張

Extended Berkeley Packet Filter(eBPF)は、ユーザー指定の eBPF プログラムを実行してカーネル機能を拡張するカーネル内仮想マシンです。このようなプログラムは、カーネル内のプローブまたはイベントにフックして、カーネルに関する有用な統計情報の収集、モニタリング、デバッグに使用できます。プログラムは bpf(2) syscall を使用してカーネルに読み込まれ、eBPF マシン命令のバイナリ blob としてユーザーから提供されます。Android ビルドシステムは、このドキュメントで説明するシンプルなビルドファイル構文を使用して C プログラムを eBPF にコンパイルする処理をサポートしています。

eBPF の内部とアーキテクチャの詳細については、Brendan Gregg 氏の eBPF のページをご覧ください。

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

Android BPF ローダ

Android の起動時に、/system/etc/bpf/ にあるすべての eBPF プログラムが読み込まれます。これらのプログラムは Android ビルドシステムによって C プログラムからビルドされるバイナリ オブジェクトであり、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 は、生成されるファイルのセクションにコンパイル時に配置される関数です。

kprobe kprobe インフラストラクチャを使用して、PROGFUNC をカーネルの命令でフックします。PROGNAME は、kprobe されているカーネル関数の名前にする必要があります。kprobe の詳細については、kprobe のカーネル ドキュメントをご覧ください。
tracepoint PROGFUNC をトレースポイントでフックします。PROGNAMESUBSYSTEM/EVENT の形式にする必要があります。たとえば、スケジューラのコンテキスト スイッチ イベントに関数をアタッチするトレースポイント セクションは SEC("tracepoint/sched/sched_switch") になります。ここで、sched はトレース サブシステムの名前、sched_switch はトレース イベントの名前です。トレースポイントの詳細については、トレース イベントに関するカーネル ドキュメントをご覧ください。
skfilter プログラムは、ネットワーク ソケット フィルタとして機能します。
schedcls プログラムは、ネットワーク トラフィック分類子として機能します。
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 での場所について説明します。

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

  • 読み込まれたすべてのプログラムに対して、Android ローダは各プログラムを /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME に作成し固定します。ここで、PROGNAME はプログラムの名前、FILENAME は eBPF C ファイルの名前です。

    たとえば、先の例で示した myschedtp.csched_switch トレースポイントの場合、プログラム ファイルは /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch に作成され固定されます。

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

    たとえば、先の例で示した myschedtp.csched_switch トレースポイントの場合、マップファイルは /sys/fs/bpf/map_myschedtp_cpu_pid_map に作成され固定されます。

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

Android BPF ライブラリ

Android BPF ライブラリは libbpf_android.so という名前を持ち、システム イメージに含まれています。このライブラリは、マップの作成と読み取り、およびプローブ、トレースポイント、perf バッファの作成に必要な低レベルの 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 の例

eBPF のその他の使用例としては、AOSP の以下のプログラムがあります。

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

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

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