使用 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 程式。這些程式是 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 程式可做為網路通訊 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 記憶體分析。