使用 eBPF 擴充核心

擴充的柏克利封包篩選器 (eBPF) 是核心虛擬機器 執行使用者提供的 eBPF 程式來擴充核心功能。這些程式可連結至核心中的探針或事件,用於收集有用的核心統計資料、監控和偵錯。程式 透過 bpf(2) syscall 載入核心中,並由使用者提供 視為 eBPF 機器指示的二進位 blobAndroid 建構系統支援使用本文所述的簡單建構檔案語法,將 C 程式編譯為 eBPF。

如要進一步瞭解 eBPF 內部和架構,請參閱 Brendan Gregg 的 eBPF 頁面

Android 提供可載入 eBPF 的 eBPF 載入器和程式庫 啟動程式

Android BPF 載入器

在 Android 啟動期間,系統會載入位於 /system/etc/bpf/ 的所有 eBPF 程式。這些程式是 Android 建構系統建構的二進位物件 並附上 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 核心說明文件 kaskes
追蹤點 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");

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/,建立程式所需的地圖,並固定載入的地圖 且該程式會對應至 BPF 檔案系統。這些檔案可用於進一步與 eBPF 程式互動或讀取地圖。這個區段 說明瞭為這些檔案命名時,應遵循的命名慣例 sysfs。

系統會建立並固定下列檔案:

  • 針對任何已載入的程式,假設 PROGNAME 是程式名稱, FILENAME 是 eBPF C 檔案的名稱,Android 載入器會建立和 會在 /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME 固定每個節目。

    例如,若為前一個 sched_switch 追蹤記錄點範例, myschedtp.c,程式檔案建立並固定至 /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 程式庫包含使用 C++ 的 android::BpfMap 類別 範本,根據下列項目的鍵和值類型將 BpfMap 執行個體化 使用地圖。先前的程式碼範例示範如何搭配使用 BpfMap 與索引鍵 以及整數值整數也可以是任意結構。

因此,範本化的 BpfMap 類別可讓您定義自訂 BpfMap 適用於特定地圖的物件。這樣一來,您就可以使用 自訂產生的函式,是類型感知的函式,產生的程式碼會更加簡潔。

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

偵錯問題

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

Android 中的 eBPF 範例

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

  • netd eBPF C 計畫 Android 的網路 Daemon (netd) 為各種用途 做為通訊端篩選和統計資料收集。如要瞭解這項程式的使用方式,請查看 eBPF 流量監控器來源。

  • time_in_state eBPF C 計畫 會計算 Android 應用程式在不同時間停留的時間 CPU 頻率,用來計算功率。

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