说明
eBPF
中常见的map
有:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20enum bpf_map_type
{
BPF_MAP_TYPE_UNSPEC,
BPF_MAP_TYPE_HASH,
BPF_MAP_TYPE_ARRAY,
BPF_MAP_TYPE_PROG_ARRAY,
BPF_MAP_TYPE_PERF_EVENT_ARRAY,
BPF_MAP_TYPE_PERCPU_HASH,
BPF_MAP_TYPE_PERCPU_ARRAY,
BPF_MAP_TYPE_STACK_TRACE,
BPF_MAP_TYPE_CGROUP_ARRAY,
BPF_MAP_TYPE_LRU_HASH,
BPF_MAP_TYPE_LRU_PERCPU_HASH,
BPF_MAP_TYPE_LPM_TRIE,
BPF_MAP_TYPE_ARRAY_OF_MAPS,
BPF_MAP_TYPE_HASH_OF_MAPS,
BPF_MAP_TYPE_DEVMAP,
BPF_MAP_TYPE_SOCKMAP,
BPF_MAP_TYPE_CPUMAP,
};
通过前面的示例代码和相关学习,目前接触到类型有:BPF_MAP_TYPE_PERF_EVENT_ARRAY
,BPF_MAP_TYPE_PERCPU_ARRAY
,这两种类型主要都是在perf_heap
相关程序中接触到的. 具体代码参考:perf_event_map
BPF_MAP_TYPE_PERF_EVENT_ARRAY
BPF_MAP_TYPE_PERF_EVENT_ARRAY
是eBPF(Extended Berkeley Packet Filter)中的一种特殊类型的映射(map)。它用于在eBPF程序和用户空间之间传递性能事件(perf events)。
示例程序如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20struct bpf_map_def SEC("maps/my_map") my_map = {
.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
.key_size = sizeof(int),
.value_size = sizeof(u32),
.max_entries = 1024,
};
struct data_t {
u32 pid;
};
SEC("kprobe/vfs_mkdir")
int kprobe_vfs_mkdir(void *ctx)
{
bpf_printk("mkdir_perf_event (vfs hook point)%u\n",bpf_get_current_pid_tgid());
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
bpf_perf_event_output(ctx, &my_map, BPF_F_CURRENT_CPU, &data, sizeof(data));
return 0;
};
其中:1
2
3
4
5
6struct bpf_map_def SEC("maps/my_map") my_map = {
.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
.key_size = sizeof(int),
.value_size = sizeof(u32),
.max_entries = 1024,
};
这段代码定义了一个名为my_map的eBPF映射,其类型为BPF_MAP_TYPE_PERF_EVENT_ARRAY。该映射用于存储性能事件的数据。它的键大小为sizeof(int),值大小为sizeof(u32),最大条目数为1024。
示例代码的整个逻辑也非常的简单。在vfs_mkdir系统调用的钩子函数中,将当前进程的PID作为性能事件的数据发送到my_map映射中。这样,用户空间程序可以通过读取my_map映射来获取性能事件数据,并进行进一步的分析和处理。
BPF_MAP_TYPE_PERCPU_ARRAY
BPF_MAP_TYPE_PERCPU_ARRAY
是eBPF(Extended Berkeley Packet Filter)中的一种特殊类型的映射(map)。它用于在eBPF程序中创建一个数组,每个CPU都有自己的副本,可以在不同的CPU上并发地进行读写操作。BPF_MAP_TYPE_PERCPU_ARRAY
是一种用于多核系统的映射类型,它提供了一种在不同CPU上进行并发读写的机制,避免了竞争条件和锁的使用。
示例代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29struct bpf_map_def SEC("maps/heap") heap = {
.type = BPF_MAP_TYPE_PERCPU_ARRAY,
.key_size = sizeof(int),
.value_size = sizeof(u32),
.max_entries = 1,
};
struct data_t {
u32 pid;
};
struct bpf_map_def SEC("maps/perf_map") perf_map = {
.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
.key_size = sizeof(int),
.value_size = sizeof(u32),
.max_entries = 1024,
};
SEC("kprobe/vfs_mkdir")
int kprobe_vfs_mkdir(void *ctx)
{
bpf_printk("mkdir_perf_event (vfs hook point)%u\n", bpf_get_current_pid_tgid());
int zero = 0;
struct data_t *data = bpf_map_lookup_elem(&heap, &zero);
if (!data) {
return 0;
}
data->pid = bpf_get_current_pid_tgid();
bpf_perf_event_output(ctx, &perf_map, BPF_F_CURRENT_CPU, data, sizeof(*data));
return 0;
}
向比较BPF_MAP_TYPE_PERF_EVENT_ARRAY
的代码,在本例中存在一处不一样的代码,struct data_t *data = bpf_map_lookup_elem(&heap, &zero);
。bpf_map_lookup_elem
主要在堆上申请空间,bpf_map_lookup_elem
的作用是查找heap映射中键为zero的元素。因为heap是一个BPF_MAP_TYPE_PERCPU_ARRAY类型的映射,所以每个CPU都有一个与键zero关联的元素。这个元素的类型是data_t,它是一个结构体,包含一个u32类型的成员pid。如果成功返回了,那么就说明成功申请了这段内存。接下来,就可以将当前进程的PID作为性能事件的数据发送到perf_map映射中。这样,用户空间程序可以通过读取perf_map映射来获取性能事件数据,并通过读取heap映射来获取每个CPU上的数据。
这段代码的含义同样也很简单,在vfs_mkdir系统调用的钩子函数中,将当前进程的PID作为性能事件的数据发送到perf_map映射中。同时,使用heap映射来存储每个CPU的数据,以便在不同CPU上进行并发读写操作。用户空间程序可以通过读取perf_map映射来获取性能事件数据,并通过读取heap映射来获取每个CPU上的数据。
BPF_MAP_TYPE_HASH
BPF_MAP_TYPE_HASH
一般就是用于内核态和用户态之间传输数据. 这个目前是最为常见。可以参见示例代码:map_rewite1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29struct bpf_map_def SEC("maps/cache") cache = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(u32),
.value_size = sizeof(u32),
.max_entries = 10,
};
SEC("kprobe/vfs_mkdir")
int kprobe_vfs_mkdir(void *ctx)
{
u32 key = 1;
u32 *value;
bpf_printk("map rewrite,mkdir (vfs hook point)\n");
value = bpf_map_lookup_elem(&cache, &key);
if (value) {
u32 new_value = 100;
bpf_printk("Value found in cache: %u\n", *value);
bpf_map_update_elem(&cache, &key, &new_value, BPF_ANY);
value = bpf_map_lookup_elem(&cache, &key);
if (value) {
bpf_printk("Value updated in cache: %u\n", *value);
} else {
bpf_printk("Failed to update value in cache\n");
}
} else {
bpf_printk("Value not found in cache\n");
}
return 0;
};
BPF_MAP_TYPE_LRU_HASH
普通 hash map 的问题是有大小限制,超过最大数量后无法再插入了。LRU map 可以避 免这个问题,如果 map 满了,再插入时它会自动将最久未被使用(least recently used)的 entry 从 map 中移除。
参考Hades
中的代码.1
2
3
4
5
6
7
8
9
10
BPF_MAP(_name, BPF_MAP_TYPE_LRU_HASH, _key_type, _value_type, _max_entries)
typedef struct net_ctx {
int fd;
sa_family_t sa_family;
int addr;
} net_ctx_t;
BPF_LRU_HASH(connect_cache, u64, net_ctx_t, 4096);
其中BPF_LRU_HASH(connect_cache, u64, net_ctx_t, 4096)
就等价于:1
2
3
4
5
6struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, u64);
__type(value, struct net_ctx);
__uint(max_entries, 4096);
} connect_cache SEC(".maps");
转换成为这种熟悉的格式,就很方便理解了.接下来看有关BPF_MAP_TYPE_LRU_HASH
类型的具体使用.
BPF_MAP_TYPE_HASH_OF_MAPS
内核态
map-in-map:第一个 map 内的元素是指向另一个 map 的指针。还是以实际的例子来作说明。map_in_map1
2
3
4
5
6
7
8
9
10
11
12struct bpf_map_def SEC("maps/InnerM") InnerM = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(u32),
.value_size = sizeof(u32),
.max_entries = 10,
};
struct bpf_map_def SEC("maps/OuterM") OuterM = {
.type = BPF_MAP_TYPE_HASH_OF_MAPS,
.key_size = sizeof(u32),
.max_entries = 10,
};
分别定义了两个MAP
类型的结构体,其中InnerM
结构体的类型是BPF_MAP_TYPE_HASH
,作为内层MAP
。OuterM
结构体的类型是BPF_MAP_TYPE_HASH_OF_MAPS
,作为外层MAP
。InnerM
结构体的key_size
和value_size
都是sizeof(u32)
,max_entries
是10。OuterM
结构体的key_size
是sizeof(u32)
,max_entries
是10。
接下来看具体的使用和交互。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20u32 key = 1;
u32 value = 42;
u32 newKey = 3;
u32 newValue = 3;
void *inner_map = bpf_map_lookup_elem(&OuterM, &key);
if (inner_map == NULL) {
bpf_printk("map lookup failed\n");
return 0;
}
bpf_printk("map rewrite,mkdir (vfs hook point)\n");
int result = bpf_map_update_elem(inner_map, &newKey, &newValue, BPF_ANY);
if (result == 0) {
bpf_printk("add new key-value pair\n");
}
result = bpf_map_update_elem(inner_map, &key, &value, BPF_ANY);
if (result == 0) {
bpf_printk("rewrite key-value pair\n");
}
return 0;
在钩子函数kprobe_vfs_mkdir
中,通过调用bpf_map_lookup_elem
函数,可以获取OuterM映射中键为1的条目的指针,并将其赋值给inner_map变量。bpf_map_update_elem(inner_map, &newKey, &newValue, BPF_ANY)
,如果对应的KEY不存在,就是新建。如果存在就是更新为newValue
。在本例中因为newKey
不在inner_map
就是更新。bpf_map_update_elem(inner_map, &key, &value, BPF_ANY)
,在本例中,因为key
已经存在,所以inner_map
就是更新。
用户态
初始化Map
对象,初始化Manager
和之前一样没有差别。不过初始化BPF_MAP_TYPE_HASH
和BPF_MAP_TYPE_HASH_OF_MAPS
类型存在差别。
初始化Map
具体代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37m := &manager.Manager{
Probes: []*manager.Probe{
{
UID: "KprobeVFSMkdir",
Section: "kprobe/vfs_mkdir",
EbpfFuncName: "kprobe_vfs_mkdir",
AttachToFuncName: "vfs_mkdir",
},
},
Maps: []*manager.Map{
{
Name: "InnerM",
Contents: []ebpf.MapKV{
{Key: uint32(1), Value: uint32(1)},
{Key: uint32(2), Value: uint32(2)},
},
},
},
}
options := manager.Options{
MapSpecEditors: map[string]manager.MapSpecEditor{
"OuterM": {
InnerMap: &ebpf.MapSpec{
Name: "InnerM",
Type: ebpf.Hash,
KeySize: 4,
ValueSize: 4,
MaxEntries: 10,
Flags: 0,
},
EditorFlag: manager.EditInnerMap,
},
},
}
err := m.InitWithOptions(bytes.NewReader(_bytecode), options)
InnerM
的定义是直接在定义Manager
时定义的:1
2
3
4
5
6
7
8
9Maps: []*manager.Map{
{
Name: "InnerM",
Contents: []ebpf.MapKV{
{Key: uint32(1), Value: uint32(1)},
{Key: uint32(2), Value: uint32(2)},
},
},
},
定义的类型是ebpf.MapKV
,并且初始化了两个键值对。这两个键值对会在InnerM
初始化时插入到InnerM
中。OuterM
的定义是通过MapSpecEditors
定义的:1
2
3
4
5
6
7
8
9
10
11
12
13MapSpecEditors: map[string]manager.MapSpecEditor{
"OuterM": {
InnerMap: &ebpf.MapSpec{
Name: "InnerM",
Type: ebpf.Hash,
KeySize: 4,
ValueSize: 4,
MaxEntries: 10,
Flags: 0,
},
EditorFlag: manager.EditInnerMap,
},
},
通过InnerMap
定义了内部Map类型和名称。EditorFlag
定义了编辑标志,这里是manager.EditInnerMap
,表示编辑内部Map。这样在初始化Manager
时,就会自动创建OuterM
和InnerM
两个Map,并将InnerM
插入到OuterM
中。
读取Map
通过GetMap
的方式就可以读取定义的BPF_MAP_TYPE_HASH
类型的Map
了。1
2
3
4
5
6
7
8
9
10
11
12sharedCache, found, err := m.GetMap("InnerM")
if err != nil || !found {
fmt.Println(fmt.Errorf("error:%v, %s", err, "couldn't find shared_cache1 in m1"))
}
// Iterate over the map
entries := sharedCache.Iterate()
var key, val uint32
for entries.Next(&key, &val) {
// Order of keys is non-deterministic due to randomized map seed
fmt.Printf("%v contains %v at key %v\n", sharedCache, val, key)
}
实际运行输出的是如下的结果,符合预期。(因为这两个值是在初始化时插入的)1
2Hash(InnerM)#3 contains 2 at key 2
Hash(InnerM)#3 contains 1 at key 1
接下来分析OuterM
的加载。1
2
3
4
5router := manager.MapRoute{RoutingMapName: "OuterM", Key: uint32(1), Map: sharedCache}
if err := m.UpdateMapRoutes(router); err != nil {
fmt.Println("update error", err)
return
}
通过MapRoute
定义了OuterM
和InnerM
关联关系。OuterM
中的键为1对应的值就是sharedCache
,然后通过m.UpdateMapRoutes(router)
执行更新操作。
通过这种关联,所以当我们在内核态中的代码void *inner_map = bpf_map_lookup_elem(&OuterM, &key)
才会成功执行。
更新Map
当我们在用户态触发了vfs_mkdir
事件之后,就会执行下面的代码:
bpf_map_update_elem(inner_map, &newKey, &newValue, BPF_ANY);
,内层Map
会新增加一个key=3,value=3
的键值对bpf_map_update_elem(inner_map, &key, &value, BPF_ANY)
,内存Map
会更新key=1
的值为value=42
实际运行的结果如下:1
2
3
4Generating events to trigger the probes ...
Hash(InnerM)#3 contains 2 at key 2
Hash(InnerM)#3 contains 3 at key 3
Hash(InnerM)#3 contains 42 at key 1
符合代码执行预期。
这种map嵌套map的方式,并且内存map还可以保存复杂的数据结构时,可以更好地用与保存各种复杂规则。在字节开源的 vArmor-ebpf 中就是使用这种方式来保存各种规则的。有关其中具体的规则应用可以参考之前写的文章,字节vArmor客户端代码解读
总结
按照在 bpf_map_type
中的定义还存在很多其他类型的Map,但是就目前我所观察到的,目前使用的比较多的就是上述的几种类型。上面这几种常见的类型也基本上可以满足场景需求。
参考
http://arthurchiao.art/blog/bpf-advanced-notes-2-zh/
https://elixir.bootlin.com/linux/latest/source/samples/bpf/test_map_in_map.bpf.c