使用 eBPF 擴充核心

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

您可以在載入器原始碼中找到其他類型。

舉例來說,下列 myschedtp.c 程式會新增特定 CPU 上執行的最新工作 PID 相關資訊。這個程式會建立地圖並定義 tp_sched_switch 函式,以便附加至 sched: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 記憶體分析。