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 (its 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 的解析流程:

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 的創建過程如下:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qN30SOVxnf6czFM_oW8bhg