使用 eBPF 擴充核心

擴充 Berkeley 封包篩選器 (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 also defines 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 將 Hook PROGFUNC 連結至追蹤點。PROGNAME 必須採用 SUBSYSTEM/EVENT 格式。舉例來說,如要將函式附加至排程器內容切換事件,追蹤點區段會是 SEC("tracepoint/sched/sched_switch"),其中 sched 是追蹤子系統的名稱,sched_switch 則是追蹤事件的名稱。如要進一步瞭解追蹤點,請參閱追蹤事件核心說明文件。
skfilter 程式會做為網路通訊端篩選器。
schedcls 程式會做為網路流量分類器。
cgroupskb、cgroupsock 每當 CGroup 中的程序建立 AF_INET 或 AF_INET6 通訊端時,程式就會執行。

如要查看其他類型,請參閱 Loader 原始碼。

舉例來說,下列 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");

當程式使用核心提供的 BPF 輔助函式時,LICENSE 巨集會用來驗證程式是否與核心授權相容。以字串形式指定程式的授權名稱,例如 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 程式庫包含 android::BpfMap 類別,可利用 C++ 範本,根據相關對應項目的鍵和值類型,將 BpfMap 例項化。上述程式碼範例示範如何使用 BpfMap,其中鍵和值為整數。整數也可以是任意結構。

因此,範本化的 BpfMap 類別可讓您定義適合特定地圖的自訂 BpfMap 物件。接著,您可以使用自訂產生的函式存取對應,這些函式可感知類型,因此程式碼更簡潔。

如要進一步瞭解 BpfMap,請參閱 Android 來源。

偵錯

在啟動期間,系統會記錄多則與 BPF 載入相關的訊息。如果載入程序因任何原因失敗,logcat 會提供詳細的記錄訊息。依 bpf 篩選 logcat 記錄,即可列印載入期間的所有訊息和任何詳細錯誤,例如 eBPF 驗證工具錯誤。

Android 中的 eBPF 範例

AOSP 中的下列程式提供使用 eBPF 的其他範例:

  • Android 中的網路 Daemon (netd) 會使用 netd eBPF C 程式,執行各種作業,例如插座篩選和統計資料收集。如要瞭解這項程式的使用方式,請查看 eBPF 流量監控工具來源。

  • time_in_state eBPF C 程式會計算 Android 應用程式在不同 CPU 頻率下花費的時間,用於計算耗電量。

  • 在 Android 12 中,gpu_mem eBPF C 程式會追蹤每個程序和整個系統的 GPU 記憶體用量。這個程式用於 GPU 記憶體剖析。

如要進一步瞭解如何使用 eBPF 收集 statsd 的低負荷核心記憶體管理和電源延遲直方圖,請參閱「使用 eBPF 收集核心遙測資料」。