eBPF perf event Map 的創建
1. perf buffer 專用 map 簡介
在 eBPF perf buffer 的使用 一文中,我們簡單介紹了在使用 perf buffer 時會先定義一個 BPF_MAP_TYPE_PERF_EVENT_ARRAY 類型的 map,如下所示:
/* BPF perfbuf map */
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(int));
__uint(value_size, sizeof(u32));
} my_map SEC(".maps");
這個 map 的 key_size 爲 sizeof(int),而 value_size 爲 sizeof(u32)-- 但這並非是真正保存在 map 中的值情況,因爲這是一種 "FD_ARRAY",map 裏面存放的其實是自定義的數據類型 size,這點後面會講到。
另外,BPF_MAP_TYPE_PERF_EVENT_ARRAY 類型的 map 也不需要指定 max_entries,因爲在 libbpf 中默認會將 max_entries 設置爲系統中 cpu 個數。
2. 解析 xx.bpf.o 文件
我們仍然以 libbpf-bootstrap 爲開發框架進行說明。
一個完整的 ebpf 程序一般由兩個部分組成,一個是 bpf 部分 (prog、map 以及 SEC 定義等等,這部分一般經過 clang 編譯爲 xx.bpf.o),另外一個是用戶態部分 (解析 bpf 部分的. o 文件,根據解析的結果創建 map、attach prog,與 ebpf 進行數據交互等等)。
下面我們就開始分析 libbpf-bootstrap 中 maps 的創建流程。
2.1 編譯生成 xx.bpf.o 和 skeleton
既然使用 libbpf-bootstrap 框架,我們就用框架裏面 example 目錄下的 minimal 這個 demo 來舉例:
ebpf程序的編譯:
clang -O2 -g -Wall -target bpf -c minimal.bpf.c -o minimal.bpf.o
注意:需要確保 clang 版本在 10.0 以上
利用 bpftool 生成 skeleton:
bpftool gen skeleton minimal.bpf.o > minimal.skel.h
生成的 skeleton 提取了 bpf.o 目標文件中的 maps 和 prog 信息構造出方便開發的數據結構,同時還對 libbpf 庫函數進行了包裝,方便開發者使用。在 skeleton 生成後可以通過如下幾個簡單的步驟來完成用戶態程序的開發:
<name>_bpf__open() – creates and opens BPF application;
<name>_bpf__load() – instantiates, loads, and verifies BPF application parts;
<name>_bpf__attach() – attaches all auto-attachable BPF programs (it’s optional, you can have more control by using libbpf APIs directly);
<name>_bpf__destroy() – detaches all BPF programs and frees up all used resources.
參考:Building BPF applications with libbpf-bootstrap
2.2 解析 xx.bpf.o 中 "maps" 和 ".maps" section
從 2.1 我們瞭解到 libbpf-bootstrap 已經提供了一個基本的框架 skeleton,並提取了 bpf.o 目標文件的內容;接下來就能夠通過 libbpf 庫的封裝函數從 xx.bpf.o 中解析我們前面在 xx.bpf.c 中創建的 "maps" 或 ".maps" 。
這一步是通過_bpf__open() 函數來完成的,對於前面的 minimal 的例子,就是 minimal_bpf__open。這個函數是對 libbpf 庫函數的封裝,簡單列舉一下對於 map 的解析流程:
-
遍歷 xx.bpf.o 中的所有 sec 找到 SEC("maps") 和 SEC(".maps") 這兩類 section, 找到名字爲 "maps" 或者 ".maps" 的 section,並將它們的 idx 分別存放到 obj->efile.maps_shndx 和 obj->efile.btf_maps_shndx 中;
-
解析 SEC("maps") 和 SEC(".maps") 中的數據並將各個 map 結構內容提取到 struct bpf_map;
調用 bpf_object__init_user_maps 來解析 "maps" section,這個 section 中存放了所有定義在 SEC("maps") 的數據結構:struct bpf_map_def;
調用 bpf_object__init_user_btf_maps 來解析 ".maps" section,這個 section 中存放了所有定義在 SEC(".maps") 的數據結構,將這些數據結構都提取到 struct bpf_map 結構中,後續 map 在內核中的對象創建時都是基 struc bpf_map 來進行的。
3. 在內核創建 ebpf map 對象
這裏所指的創建 ebpf map 對象是指在內核中創建。上一節用戶態程序調用_bpf__open()函數 (底層調用了 libbpf 中的接口函數) 對 xx.bpf.o 文件進行了解析,並提取了 SEC("maps")和 SEC(".maps")兩個 section 中申明的 map 數據結構。
真正要讓 ebpf map 運行起來,承接起用戶態和內核態數據交互等等方面的功能,還需要在內核中創建一個 map 對象。我們來看一下整個創建流程。
3.1 構建系統調用參數
這一步會枚舉前面解析的 struct bpf_map_def 結構,然後通過 bpf 系統調用在內核中創建 map 對象。
首先,對於 BPF_MAP_TYPE_PERF_EVENT_ARRAY 類型的 maps 要特殊一些,在定義時一般不用指定 max_entries;不指定的話 libbpf 會調用接口函數 map_set_def_max_entries 中會默認將 max_entries 設置爲 nr_cpus。
我們再來回顧一下它的定義,只定義了 map type、key_size, value_size:
/* BPF perfbuf map */
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(int));
__uint(value_size, sizeof(u32));
} my_map SEC(".maps");
接着,調用 bpf_object__create_map 函數將 struct bpf_map_def 信息提取到 union bpf_attr attr 中;bpf_attr 聯合體中集成了 bpf 系統調用創建 map 所需要的係數,包括:key_size, value_size, map_type,max_entries 等等。
最後,調用 syscall(__NR_bpf, BPF_MAP_CREATE, attr, attr_size) 系統調用進入內核。
3.2 bpf BPF_MAP_CREATE 系統調用
syscall__NR_bpf, BPF_MAP_CREATE) 在內核中進入如下流程:
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
err = map_create(&attr);
這裏的 map_create() 函數就是 map 創建的核心,部分核心代碼如下:
map = find_and_alloc_map(attr);
type = array_index_nospec(type, ARRAY_SIZE(bpf_map_types));
ops = bpf_map_types[type];
map = ops->map_alloc(attr); //創建struct bpf_map *map對象
err = bpf_obj_name_cpy(map->name, attr->map_name);
bpf_map_init_memlock(map) //更新user->locked_vm並檢查RLIMIT_MEMLOCK
bpf_map_new_fd(map, f_flags) //爲map創建anon_inode
下面會對這裏的這些代碼依次展開。
3.3 find_and_alloc_map() 創建 map 對象 struct bpf_map
這個步驟是藉助內核函數 find_and_alloc_map() 來完成的。
3.3.1 根據 map 類型獲取對應的 map ops
首先根據 map 的類型 attr->map_type 找到這種 map 類型對應的 struct bpf_map_ops *ops; 系統中所有 map 類型對應的 bpf_map_ops 都存放在 struct bpf_map_ops * const bpf_map_types[] 這個數組中。
根據 map 的類型,經過處理後得到 bpf_map_types[] 數組的 index,即可得到對應的 struct bpf_map_ops * 指針。對於 BPF_MAP_TYPE_PERF_EVENT_ARRAY 類型的 map,其對應的 bpf_map_ops 是 perf_event_array_map_ops。
這個 bpf_map_types 數組的具體定義如下:
static const struct bpf_map_ops * const bpf_map_types[] = {
#define BPF_PROG_TYPE(_id, _ops)
#define BPF_MAP_TYPE(_id, _ops) \
[_id] = &_ops,
#include <linux/bpf_types.h>
#undef BPF_PROG_TYPE
#undef BPF_MAP_TYPE
};
3.3.2 創建 struct bpf_map 結構
3.3.2.1 尋找 ops
在 2.1 獲取到 map 的 struct bpf_map_ops 指針 ops 後,內核通過 ops->map_alloc(attr) 來分配 map 在內核中的對象 struct bpf_map 結構,而 BPF_MAP_TYPE_PERF_EVENT_ARRAY 類型的 ops 實際上就是 perf_event_array_map_ops,其定義如下:
const struct bpf_map_ops perf_event_array_map_ops = {
.map_alloc_check = fd_array_map_alloc_check,
.map_alloc = array_map_alloc, /* map_alloc分配map內存 */
.map_free = fd_array_map_free,
.map_get_next_key = array_map_get_next_key,
.map_lookup_elem = fd_array_map_lookup_elem,
.map_delete_elem = fd_array_map_delete_elem,
.map_fd_get_ptr = perf_event_fd_array_get_ptr,
.map_fd_put_ptr = perf_event_fd_array_put_ptr,
.map_release = perf_event_fd_array_release,
.map_check_btf = map_check_no_btf,
};
因此 BPF_MAP_TYPE_PERF_EVENT_ARRAY 類型的 map,ops->map_alloc(attr) 實際調用的是 array_map_alloc() 函數。
3.3.2.2 分配 struct bpf_map
首先將 value size 進行 8 字節對齊,本來在定義時的 size 爲 4 字節,但是實際存儲到內核中的 map 對象的 value size 是 8 字節。
elem_size = round_up(attr->value_size, 8);
3.3.2.3 計算 map 需要的內存空間
對於 BPF_MAP_TYPE_PERF_EVENT_ARRAY 類型的 map,所需要的內存空間如下:
array_size = sizeof(*array); /*struct bpf_array結構的size*/
array_size += (u64) max_entries * elem_size; /*max_entries個value的空間*/
其中 struct bpf_array 是專門爲 array 類型的 map 設計的一個數據結構,array 類型 map 的 value 就存放在此結構中;同時裏面內置了 bpf_map 結構,因此一旦有了 bpf_map 就能夠獲取到 bpf_array 指針:
struct bpf_array {
struct bpf_map map; /*內置了bpf_map對象*/
u32 elem_size;
u32 index_mask;
/* 'ownership' of prog_array is claimed by the first program that
* is going to use this map or by the first program which FD is stored
* in the map to make sure that all callers and callees have the same
* prog_type and JITed flag
*/
enum bpf_prog_type owner_prog_type;
bool owner_jited;
union { /* map元素存放在此 */
char value[0] __aligned(8);
void *ptrs[0] __aligned(8);
void __percpu *pptrs[0] __aligned(8);
};
};
3.3.2.4 分配內存
這一步是調用 array = bpf_map_area_alloc(array_size, numa_node) 來完成的。
參數 array_size 是 struct bpf_array 的 size 和 map 中元素的 size 之和,這裏會通過 kmalloc 或者 vmalloc 來分配內存,分配完成後返回的地址既是 bpf_array 地址,也是 bpf_map 的地址。
3.4 bpf_map 初始化
在 3.3 分配了 bpf_array 和 bpf_map,這裏進行簡單的初始化:
/* find_and_alloc_map 完成*/
map->ops = ops; /* */
map->map_type = type;
bpf_obj_name_cpy(map->name, attr->map_name) /* 拷貝map name */
3.5 創建 aone inode
bpf map create 系統調用最終返回的是一個文件句柄 fd;因此還需要創建一個連接用戶態和內核態的連接橋樑,這裏是通過 anon inode 來實現的。
/* 返回map對應的文件句柄 */
int bpf_map_new_fd(struct bpf_map *map, int flags)
{
int ret;
ret = security_bpf_map(map, OPEN_FMODE(flags));
if (ret < 0)
return ret;
/* 創建一個anon inode,裏面關聯了bpf_map ,並返回fd */
return anon_inode_getfd("bpf-map", &bpf_map_fops, map,
flags | O_CLOEXEC);
}
anon_inode_getfd 的流程大致如下:
1. get_unused_fd_flags 尋找一個空閒fd;
2, anon_inode_getfile 分配struct file結構,同時將bpf_map指針存放到file->private_data;
3. 通過fd_install(fd, file)將fd與file關聯;
4. 返回fd到用戶態。
這樣後續,用戶態就可以通過 fd 來與 map 對象進行交互。
4. 總結
perf buffer 對應的 map 的創建過程如下:
-
解析 ebpf.o 中的 SEC("maps") 和 SEC(".maps"),並提取各個 maps 數據結構作爲後續系統調用參數
與其他 maps 不同的是,針對這種類型的 map 會默認將 map 的 max_entry 設置爲系統 cpu 個數 nr_cpus.
-
調用 syscall(__NR_bpf, BPF_MAP_CREATE, attr, attr_size) 系統調用在內核創建 maps 對象
創建 strcut bpf_array 結構,裏面內置了 struct bpf_map 對象,並未爲 map 的 value 分配內存,這部分內存放在 bpf_array 中,爲 bpf_map 對象創建一個 anon inode,並將 bpf_map 對象關聯到 file->private_data,最後將這個 file 對應的 fd 作爲 bpf sysacall 系統調用的返回值返回到用戶態。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qN30SOVxnf6czFM_oW8bhg