一 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个数。
二 ebpf.o解析map
我们仍然以libbpf-bootstrap为开发框架进行说明。
一个完整的ebpf程序一般由两个部分组成,一个是ebpf部分(prog、map的定义等等),另外一个是用户态部分(解析ebpf部分的.o文件,根据解析的结果创建map、attach prog,与ebpf进行数据交换等等)。
下面我们就开始分析libbpf-bootstrap中maps的创建流程。
2.1 编译生成ebpf.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 解析ebpf.o中"maps"和".maps" section
从2.1我们了解到libbpf-bootstrap已经提供了一个基本的框架skeleton,并提起了btf.o目标文件的内容;接下来就能够通过skeleton提供的libbpf库的封装函数从ebpf.o中解析我们前面在ebpf.c中创建的"maps"或".maps" 。
这一步是通过<name>_bpf__open()函数来完成的,对于前面的minimal的例子,就是minimal_bpf__open。这个函数是对libbpf库函数的封装,简单列举一下这个里面对于map的解析流程。
2.2.1 遍历ebpf.o中的所有sec找到SEC("maps")和SEC(".maps")这两类section
找到名字为"maps"或者".maps"的section,并将它们的idx分别存放到obj->efile.maps_shndx和obj->efile.btf_maps_shndx中
2.2.2 解析SEC("maps")和SEC(".maps")中的数据并将各个map结构内容提取到struct bpf_map
调用btf_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来进行的。
三 在内核创建ebpf map对象
这里所指的创建ebpf map对象是指在内核中创建。上一节用户态程序调用<name>_bpf__open()函数(底层调用了libbpf中的接口函数)对ebpf.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数组的具体定义如下:
1 2 3 4 5 6 7 8 | tatic 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指针:
truct 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对象进行交互。
四 总结
event buffer对应的map的创建过程如下:
1) 解析ebpf.o中的SEC("maps")和SEC(".maps"),并提取各个maps数据结构作为后续系统调用参数
与其他maps不同的是,针对这种类型的map会默认将map的max_entry设置为系统cpu个数nr_cpus.
2) 调用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系统调用的返回值返回到用户态
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!