本篇介紹
在使用ebpf時,如何在用戶態和內核態傳遞數據呢? 本篇介紹一個方法,就是使用map.
map 的基礎操作
創建
用戶態和內核態均可以創建map, 最直接的方法使用bpf系統調用, 不過這個api 并不是posix api, 因此需要通過syscall才行, 如下就是libbpf中的調用方式:
static inline int sys_bpf(enum bpf_cmd cmd, union bpf_attr *attr,
unsigned int size)
{
return syscall(__NR_bpf, cmd, attr, size);
}
在bpf.h 中也提供了一個接口, 如下
LIBBPF_API int bpf_map_create(enum bpf_map_type map_type,
const char *map_name,
__u32 key_size,
__u32 value_size,
__u32 max_entries,
const struct bpf_map_create_opts *opts);
這兒需要注意下,以前的接口名字是bpf_create_map, 甚至一些bpf學習材料也是這個名字, 后來修改成了bpf_map_create, 同時接口參數也變了, 在寫代碼時需要按照在對應libbpf 庫的bpf.h中進行確認.
bpf_map_type 支持的類型如下:
enum 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_XSKMAP,
BPF_MAP_TYPE_SOCKHASH,
BPF_MAP_TYPE_CGROUP_STORAGE,
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
BPF_MAP_TYPE_QUEUE,
BPF_MAP_TYPE_STACK,
BPF_MAP_TYPE_SK_STORAGE,
BPF_MAP_TYPE_DEVMAP_HASH,
BPF_MAP_TYPE_STRUCT_OPS,
BPF_MAP_TYPE_RINGBUF,
BPF_MAP_TYPE_INODE_STORAGE,
BPF_MAP_TYPE_TASK_STORAGE,
BPF_MAP_TYPE_BLOOM_FILTER,
};
map_name名字中不能包含空格,出現空格會被報錯誤22. opts 用不到的話,寫成NULL就行.
綜合上述信息,創建map的例子如下:
int fd = bpf_map_create(BPF_MAP_TYPE_HASH, "mymap", sizeof(int), sizeof(int), 100, NULL);
更新
在用戶態和內核態都可以更新map,對應的方法都是bpf_map_update_elem, 不過內核和用戶態的參數會不一樣, 因為內核可以直接訪問map結構,而用戶態只能通過fd的形式. 這兒我們先看下用戶態的函數簽名:
LIBBPF_API int bpf_map_update_elem(int fd, const void *key, const void *value,
__u64 flags);
這兒的flags有幾種取值:
* **BPF_ANY**
* Create a new element or update an existing element.
* **BPF_NOEXIST**
* Create a new element only if it did not exist.
* **BPF_EXIST**
* Update an existing element.
* **BPF_F_LOCK**
* Update a spin_lock-ed map element.
查詢與刪除
map同樣也支持內核和用戶態查詢, 方法也一樣,區別也是內核是直接訪問map結構,用戶態是訪問fd,函數如下:
LIBBPF_API int bpf_map_lookup_elem(int fd, const void *key, void *value);
刪除也同理,用戶態函數如下:
LIBBPF_API int bpf_map_delete_elem(int fd, const void *key);
到了這兒用一個demo演示下map的crud操作, 代碼如下
#include <stdio.h>
#include <linux/bpf.h>
//#include <bpf/bpf_helpers.h>
#include <bpf/bpf.h>
#include <string.h>
#include <errno.h>
#ifdef SEC
#undef SEC
#endif
#define SEC(NAME) __attribute__((section(NAME), used))
int main() {
int fd = bpf_map_create(BPF_MAP_TYPE_HASH, "mymap", sizeof(int), sizeof(int), 100, NULL);
int key, value, result;
key = 1, value = 2;
result = bpf_map_update_elem(fd, &key, &value, BPF_ANY);
printf("map update result %d with bpf any, %s\n", result, strerror(errno));
result = bpf_map_update_elem(fd, &key, &value, BPF_EXIST);
printf("map update result %d with %d, %s\n", result, value, strerror(errno));
result = bpf_map_lookup_elem(fd, &key, &value);
printf("map lookup %d value is %d, %s\n", result, value, strerror(errno));
result = bpf_map_delete_elem(fd, &key);
printf("map delete %d, %s\n", result, strerror(errno));
return 0;
}
這次就按照普通c代碼方式編譯就行,不需要編譯成bpf了.運行效果如下:
shanks@shanks-ThinkPad-T460s:~/Documents/01code/ebpf/chapter02$ sudo ./a.out
map update result 0 with bpf any, Success
map update result 0 with 2, Success
map lookup 0 value is 2, Success
map delete 0, Success
遍歷
bpf也支持遍歷map, 不過這個操作只在用戶態支持. 另外bpf的遍歷和我們常見的遍歷不太一樣,比如c++的map,我們遍歷的話,就是挨個k遍歷就行,如果在遍歷的時候伴隨著刪除操作,就需要格外小心,防止迭代器無效.
而bpf就不用擔心這個問題,技巧就在函數簽名中:
LIBBPF_API int bpf_map_get_next_key(int fd, const void *key, void *next_key);
第一個key是開始遍歷的k,如果指定一個在map中不存在的k,就會從頭遍歷. next_key 是下一個將要遍歷的k,如果在遍歷的時候被刪除了,就會導致再次從頭遍歷. 通過這個機制就不怕遍歷的時候刪除了.
我們用一個demo看下:
#include <stdio.h>
#include <linux/bpf.h>
//#include <bpf/bpf_helpers.h>
#include <bpf/bpf.h>
#include <string.h>
#include <errno.h>
#ifdef SEC
#undef SEC
#endif
#define SEC(NAME) __attribute__((section(NAME), used))
int main() {
int fd = bpf_map_create(BPF_MAP_TYPE_HASH, "mymap", sizeof(int), sizeof(int), 100, NULL);
int key, value;
for (key = 1; key < 5; key ++) {
value = key + 100;
bpf_map_update_elem(fd, &key, &value, BPF_NOEXIST);
printf("insert key %d, value %d\n", key, value);
}
key = -1;
value = -1;
int next_key = 0;
while(bpf_map_get_next_key(fd, &key, &next_key) == 0) {
bpf_map_lookup_elem(fd, &key, &value);
printf("key is %d, value is %d, next_key is %d\n", key, value, next_key);
if (next_key == 4) {
bpf_map_delete_elem(fd, &next_key);
}
key = next_key;
}
return 0;
}
輸出如下:
insert key 1, value 101
insert key 2, value 102
insert key 3, value 103
insert key 4, value 104
key is -1, value is -1, next_key is 2
key is 2, value is 102, next_key is 3
key is 3, value is 103, next_key is 4
key is 4, value is 103, next_key is 2
key is 2, value is 102, next_key is 3
key is 3, value is 103, next_key is 1
查找并刪除
如果需要查找某個值,查找到后就刪除這個值,通過前面介紹的方法也可以實現,bpf中也提供了一個方法可以一下子做到,函數如下:
LIBBPF_API int bpf_map_lookup_and_delete_elem(int fd, const void *key,
void *value);
BPF map 類型介紹
BPF_MAP_TYPE_HASH: 支持動態增刪改查
BPF_MAP_TYPE_ARRAY: 類似與數組,key 就是固定的索引,大小不能修改,也就是不支持刪除,支持修改值
BPF_MAP_TYPE_PROG_ARRAY:用來突破bpf虛擬機指令限制的方法, 在map中存放加載指令的fd.
BPF_MAP_TYPE_PERF_EVENT_ARRAY:本質上是一個ringbuffer,用來讓內核給用戶態傳遞trace數據
Per-CPU Hash Maps:和BPF_MAP_TYPE_HASH類似,差別在于是per cpu變量,也就是每個cpu一個.
Per-CPU Array Maps:類似與BPF_MAP_TYPE_ARRAY
BPF_MAP_TYPE_STACK_TRACE:存儲運行進程的堆棧數據
BPF_MAP_TYPE_CGROUP_ARRAY:用來記錄cgroup節點fd,方便共享cgroup信息
BPF_MAP_TYPE_LRU_HASH and BPF_MAP_TYPE_LRU_PERCPU_HASH: 支持LRU容量管控的hash,不過percpu并不是每個cpu一個hash,而是每個cpu一個lru cache,這樣可以讓所有cpu 最常用的值得到保留
BPF_MAP_TYPE_LPM_TRIE:按照LPM(longest prefix match)算法查找最匹配的key
BPF_MAP_TYPE_ARRAY_OF_MAPS and BPF_MAP_TYPE_HASH_OF_MAPS: 支持存放map的map,不過最多就一層,不支持存放map的map的map
BPF_MAP_TYPE_DEVMAP:存放網絡設備的引用,用來網絡包重定向
BPF_MAP_TYPE_CPUMAP: 也是重來網絡包重定向的,不過存放的是cpu的引用
BPF_MAP_TYPE_XSKMAP:也是網絡包重定向,存放的是socket的引用
BPF_MAP_TYPE_SOCKMAP and BPF_MAP_TYPE_SOCKHASH:也是用來存放打開socket的應用,實現網絡包重定向
BPF_MAP_TYPE_CGROUP_STORAGE and BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE:不同cgroup的邏輯之間協作場景
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY: 存儲可重用的socket引用
BPF_MAP_TYPE_QUEUE:就是一個queue,此時key為0,只存儲value就行
BPF_MAP_TYPE_STACK:就是一個stack,此時key也是0,只存儲value就行
BPF虛擬文件系統
用前面的介紹我們可以看到,bpf的map創建成功后返回的是fd,那應該會在文件系統中存在對應的路徑,實際上也是. BPF也支持持久化map,這樣即使創建這個map的進程退出了,map信息也可以繼續保留.
接下來我們就用demo演示下:
準備2個代碼,一個負責存,一個負責讀.
存map的代碼如下:
#include <stdio.h>
#include <linux/bpf.h>
#include <bpf/bpf.h>
#include <string.h>
#include <errno.h>
static const char *file_path = "/sys/fs/bpf/mapfile";
int main() {
int fd = bpf_map_create(BPF_MAP_TYPE_HASH, "mymap", sizeof(int), sizeof(int), 100, NULL);
printf("bpf map create result %d\n", fd);
int key, value;
for (key = 1; key < 5; key ++) {
value = key + 100;
bpf_map_update_elem(fd, &key, &value, BPF_NOEXIST);
printf("insert key %d, value %d\n", key, value);
}
int pinned = bpf_obj_pin(fd, file_path);
printf("pin ojb at %s, result is %d\n", file_path, pinned);
return 0;
}
執行后,結果如下:
bpf map create result 3
insert key 1, value 101
insert key 2, value 102
insert key 3, value 103
insert key 4, value 104
pin ojb at /sys/fs/bpf/mapfile, result is 0
查看對應的路徑,也可以看到存在期望的文件節點:
root@shanks-ThinkPad-T460s:/sys/fs/bpf# ls -al |grep map
-rw------- 1 root root 0 8月 31 18:39 mapfile
接下來再看下讀的代碼:
#include <stdio.h>
#include <linux/bpf.h>
#include <bpf/bpf.h>
#include <string.h>
#include <errno.h>
static const char *file_path = "/sys/fs/bpf/mapfile";
int main() {
int fd = bpf_obj_get(file_path);
printf("bpf obj get %s, fd %d\n", file_path, fd);
int key, value;
key = -1;
value = -1;
int next_key = 0;
while(bpf_map_get_next_key(fd, &key, &next_key) == 0) {
bpf_map_lookup_elem(fd, &key, &value);
printf("key is %d, value is %d, next_key is %d\n", key, value, next_key);
key = next_key;
}
bpf_map_lookup_elem(fd, &key, &value);
printf("key is %d, value is %d, next_key is %d\n", key, value, next_key);
return 0;
}
執行的結果如下:
bpf obj get /sys/fs/bpf/mapfile, fd 3
key is -1, value is -1, next_key is 3
key is 3, value is 103, next_key is 4
key is 4, value is 104, next_key is 2
key is 2, value is 102, next_key is 1
key is 1, value is 101, next_key is 1