使用 eBPF 擴充核心

擴充 Berkeley 封包篩選器 (eBPF) 是核心內的虛擬機器,可執行使用者提供的 eBPF 程式,擴充核心功能。這些程式可以連結至核心中的探測器或事件,並用於收集實用的核心統計資料、監控及偵錯。程式會使用 bpf(2) 系統呼叫載入核心,並由使用者以 eBPF 機械指令的二進位大型物件形式提供。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 核心說明文件
追蹤點 將掛鉤 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.csched_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 記憶體剖析。