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 程序會在啟動時自動加載。加載後,必須使用以下步驟激活跟踪點程序:
- 調用
bpf_obj_get()
從固定文件的位置獲取程序fd
。有關詳細信息,請參閱sysfs 中可用的文件。 - 調用 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 內存分析。