创建BPF映射的最直接方法是使用bpf系统调用。如果该系统调用的第一个参数设置为BPF_MAP_CREATE,则表示创建一个新的映射。该调用将返回与创建映射相关的文件描述符。bpf系统调用的第二个参数是BPF映射的设置,如下所示:
union bpf_attr { struct { __u32 map_type; /* one of the values from bpf_map_type */ __u32 key_size; /* size of the keys, in bytes */ __u32 value_size; /* size of the values, in bytes */ __u32 max_entries; /* maximum number of entries in the map */ __u32 map_flags; /* flags to modify how we create the map */ }; }
bpf系统调用的第三个参数是设置属性的大小。
如下代码创建一个键和值为无符号整数的哈希表映射:
union bpf_attr my_map { .map_type = BPF_MAP_TYPE_HASH, .key_size = sizeof(int), .value_size = sizeof(int), .max_entries = 100, .map_flags = BPF_F_NO_PREALLOC, }; int fd = bpf(BPF_MAP_CREATE, &my_map, sizeof(my_map));
如果系统调用失败,内核返回-1。失败有三种原因,通过errno来进行区分。如果属性无效,内核将errno变量设置为EINVAL。如果用户没有足够的权限执行操作,内核将errno变量设置为EPERM。最后,如果没有足够的内存保存映射,内核将errno变量设置为ENOMEM。
本章后续将通过示例演示如何使用BPF映射执行一些高级操作。我们先介绍使用更直接的方法创建任何类型的映射。
内核包括一些约定和帮助函数,用于生成和使用BPF映射。使用这些约定比直接执行系统调用更为常用,因为约定的方式更具可读性且更易于遵循。值得注意的是,这些约定即使运行在内核中,底层仍然是通过bpf系统调用来创建映射。如果你事先不知道需要哪种映射类型,那么直接使用bpf系统调用会更加方便。
帮助函数bpf_map_create封装了我们上面使用的代码,可以容易地按需初始化映射。现在,我们只需要一行代码就可以实现上述映射的创建:
int fd; fd = bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(int), sizeof(int), 100, BPF_F_NO_PREALOC);
如果你知道程序将使用的映射类型,也可以预定义映射。这有助于预先对程序使用的映射获取更直观的理解:
struct bpf_map_def SEC("maps") my_map = { .type = BPF_MAP_TYPE_HASH, .key_size = sizeof(int), .value_size = sizeof(int), .max_entries = 100, .map_flags = BPF_F_NO_PREALLOC, };
这种方式使用section属性来定义映射,本示例中为SEC("maps")。这个宏告诉内核该结构是BPF映射,并告诉内核创建相应的映射。
你可能已经注意到,在新的示例中没有看到与映射相关联的文件描述符。这里,内核使用map_data全局变量来保存BPF程序映射信息。这个变量是数组结构,按照程序中指定映射的顺序进行排序。例如,如果上面的映射是程序中的第一个映射,那么该映射的文件描述符可以从数组的第一个元素中获得:
fd = map_data[0].fd;
你也可以从该数组中访问映射的名称和定义。这些信息对于调试和跟踪有时很有用。
映射初始化后,你就可以开始使用它们在内核和用户空间之间传递消息。现在,让我们看看如何使用映射存储数据。