使用 eBPF 擴充內核

擴展伯克利資料包過濾器 (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是追蹤事件的名稱。有關跟踪點的更多信息,請查看跟踪事件內核文檔
斯克過濾器程式充當網路套接字過濾器。
時間表程式充當網路流量分類器。
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 eBPF C 程式由 Android 中的網路守護程式 (netd) 用於各種目的,例如套接字過濾和統計資訊收集。若要了解如何使用程序,請檢查eBPF 流量監控來源。

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

  • 在 Android 12 中, gpu_mem eBPF C 程式追蹤每個進程和整個系統的 GPU 記憶體總使用量。該程式用於 GPU 記憶體分析。