使用 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 will also define 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 基礎結構將PROGFUNC掛鉤到內核指令。 PROGNAME必須是被 kprobed 的內核函數的名稱。有關 kprobe 的更多信息,請參閱kprobe 內核文檔
跟踪點PROGFUNC掛鉤到跟踪點。 PROGNAME必須採用SUBSYSTEM/EVENT格式。例如,用於將函數附加到調度程序上下文切換事件的跟踪點部分將是SEC("tracepoint/sched/sched_switch") ,其中sched是跟踪子系統的名稱, sched_switch是跟踪事件的名稱。查看跟踪事件內核文檔以獲取有關跟踪點的更多信息。
skfilter程序用作網絡套接字過濾器。
時間表程序用作網絡流量分類器。
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 功能。

將程序附加到跟踪點

Tracepoint 程序會在啟動時自動加載。加載後,必須使用以下步驟激活跟踪點程序:

  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 的其他示例:

  • netd C 程序由 Android 中的網絡守護程序 (netd) 用於各種目的,例如套接字過濾和統計信息收集。要了解如何使用此程序,請查看eBPF 流量監控源。

  • time_in_state eBPF C 程序計算 Android 應用程序在不同 CPU 頻率上花費的時間量,用於計算功率。

  • 在 Android 12 中, gpu_mem eBPF C 程序跟踪每個進程和整個系統的總 GPU 內存使用情況。該程序用於 GPU 內存分析。