BPF_MAP_TYPE_HASH,带 PERCPU 和 LRU 变体

注意

  • BPF_MAP_TYPE_HASH 在内核版本 3.19 中引入

  • BPF_MAP_TYPE_PERCPU_HASH 在版本 4.6 中引入

  • BPF_MAP_TYPE_LRU_HASHBPF_MAP_TYPE_LRU_PERCPU_HASH 都在版本 4.10 中引入

BPF_MAP_TYPE_HASHBPF_MAP_TYPE_PERCPU_HASH 提供通用哈希映射存储。键和值都可以是结构体,允许复合键和值。

内核负责分配和释放键/值对,直到您指定的最大条目限制。哈希映射默认使用哈希表元素的预分配。BPF_F_NO_PREALLOC 标志可用于在内存开销过大时禁用预分配。

BPF_MAP_TYPE_PERCPU_HASH 为每个 CPU 提供一个单独的值槽。每个 CPU 的值内部存储在一个数组中。

BPF_MAP_TYPE_LRU_HASHBPF_MAP_TYPE_LRU_PERCPU_HASH 变体为其各自的哈希表添加了 LRU 语义。当哈希表达到容量时,LRU 哈希将自动逐出最近最少使用的条目。LRU 哈希维护一个内部 LRU 列表,用于选择要逐出的元素。此内部 LRU 列表在 CPU 之间共享,但在调用 bpf_map_create 时,可以使用 BPF_F_NO_COMMON_LRU 标志请求每个 CPU 的 LRU 列表。下表概述了 LRU 映射的属性,具体取决于映射类型和用于创建映射的标志。

标志

BPF_MAP_TYPE_LRU_HASH

BPF_MAP_TYPE_LRU_PERCPU_HASH

BPF_F_NO_COMMON_LRU

每个 CPU 的 LRU,全局映射

每个 CPU 的 LRU,每个 CPU 的映射

!BPF_F_NO_COMMON_LRU

全局 LRU,全局映射

全局 LRU,每个 CPU 的映射

用法

内核 BPF

bpf_map_update_elem()

long bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)

可以使用 bpf_map_update_elem() 辅助函数添加或更新哈希条目。此辅助函数原子地替换现有元素。flags 参数可用于控制更新行为

  • BPF_ANY 将创建新元素或更新现有元素

  • BPF_NOEXIST 仅在元素不存在时创建新元素

  • BPF_EXIST 将更新现有元素

bpf_map_update_elem() 成功时返回 0,失败时返回负错误码。

bpf_map_lookup_elem()

void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)

可以使用 bpf_map_lookup_elem() 辅助函数检索哈希条目。此辅助函数返回指向与 key 关联的值的指针,如果未找到条目,则返回 NULL

bpf_map_delete_elem()

long bpf_map_delete_elem(struct bpf_map *map, const void *key)

可以使用 bpf_map_delete_elem() 辅助函数删除哈希条目。此辅助函数成功时返回 0,失败时返回负错误码。

每个 CPU 的哈希

对于 BPF_MAP_TYPE_PERCPU_HASHBPF_MAP_TYPE_LRU_PERCPU_HASHbpf_map_update_elem()bpf_map_lookup_elem() 辅助函数自动访问当前 CPU 的哈希槽。

bpf_map_lookup_percpu_elem()

void *bpf_map_lookup_percpu_elem(struct bpf_map *map, const void *key, u32 cpu)

可以使用 bpf_map_lookup_percpu_elem() 辅助函数在特定 CPU 的哈希槽中查找值。返回与 cpu 上的 key 关联的值,如果未找到条目或 cpu 无效,则返回 NULL

并发

存储在 BPF_MAP_TYPE_HASH 中的值可以由在不同 CPU 上运行的程序并发访问。自内核版本 5.1 起,BPF 基础设施提供了 struct bpf_spin_lock 来同步访问。参见 tools/testing/selftests/bpf/progs/test_spin_lock.c

用户空间

bpf_map_get_next_key()

int bpf_map_get_next_key(int fd, const void *cur_key, void *next_key)

在用户空间中,可以使用 libbpf 的 bpf_map_get_next_key() 函数遍历哈希的键。通过调用 bpf_map_get_next_key() 并将 cur_key 设置为 NULL,可以获取第一个键。后续调用将获取当前键的下一个键。bpf_map_get_next_key() 成功时返回 0,如果 cur_key 是哈希中的最后一个键则返回 -ENOENT,失败时返回负错误码。

请注意,如果 cur_key 被删除,则 bpf_map_get_next_key() 将返回哈希表中的第一个键,这是不希望的。如果键删除与 bpf_map_get_next_key() 混用,建议使用批量查找。

示例

请参阅 tools/testing/selftests/bpf 目录以获取功能示例。下面的代码片段演示了 API 用法。

此示例演示如何声明一个带有结构体键和结构体值的 LRU 哈希。

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

struct key {
    __u32 srcip;
};

struct value {
    __u64 packets;
    __u64 bytes;
};

struct {
        __uint(type, BPF_MAP_TYPE_LRU_HASH);
        __uint(max_entries, 32);
        __type(key, struct key);
        __type(value, struct value);
} packet_stats SEC(".maps");

此示例演示如何使用原子指令创建或更新哈希值

static void update_stats(__u32 srcip, int bytes)
{
        struct key key = {
                .srcip = srcip,
        };
        struct value *value = bpf_map_lookup_elem(&packet_stats, &key);

        if (value) {
                __sync_fetch_and_add(&value->packets, 1);
                __sync_fetch_and_add(&value->bytes, bytes);
        } else {
                struct value newval = { 1, bytes };

                bpf_map_update_elem(&packet_stats, &key, &newval, BPF_NOEXIST);
        }
}

用户空间遍历上述声明的映射元素

#include <bpf/libbpf.h>
#include <bpf/bpf.h>

static void walk_hash_elements(int map_fd)
{
        struct key *cur_key = NULL;
        struct key next_key;
        struct value value;
        int err;

        for (;;) {
                err = bpf_map_get_next_key(map_fd, cur_key, &next_key);
                if (err)
                        break;

                bpf_map_lookup_elem(map_fd, &next_key, &value);

                // Use key and value here

                cur_key = &next_key;
        }
}

内部实现

本文档的此部分面向 Linux 开发人员,描述了映射实现中不被视为稳定 ABI 的方面。以下细节可能会在内核的未来版本中发生变化。

BPF_MAP_TYPE_LRU_HASH 及其变体

更新 LRU 映射中的元素时,当映射容量达到上限时,可能会触发逐出行为。更新算法尝试了各种步骤来强制执行 LRU 属性,这些步骤对涉及以下操作尝试的其他 CPU 产生了越来越大的影响

  • 尝试使用 CPU 本地状态批量操作

  • 尝试从全局列表中获取 target_free 空闲节点

  • 尝试从全局列表中拉取任何节点并将其从哈希映射中移除

  • 尝试从任何 CPU 的列表中拉取任何节点并将其从哈希映射中移除

一次从全局列表借用的节点数量,target_free,取决于映射的大小。更大的批量大小会减少锁竞争,但也可能耗尽全局结构。该值在映射初始化时计算,通过将所有 CPU 的总保留限制在映射大小的一半来避免耗尽,最小值为单个元素,最大预算为每次 128 个。

此算法在下图中有视觉描述。有关相应操作的完整解释,请参阅 commit 3a08c2fd7634 (“bpf: LRU List”) 中的描述

Diagram outlining the LRU eviction steps taken during map update.

BPF_MAP_TYPE_LRU_HASH 及其变体在映射更新期间的 LRU 哈希逐出。有关内核函数名称代码引用,请参阅点文件源。

映射更新从右上角的椭圆形“begin bpf_map_update()”开始,通过图表向下推进,最终结果可能是成功更新或带有各种错误代码的失败。右上角的图例提供了特定操作可能涉及的锁的指示符。这旨在作为推理映射争用如何影响更新操作的视觉提示,尽管映射类型和标志可能会根据上表中描述的逻辑影响这些锁的实际争用。例如,如果映射是使用类型 BPF_MAP_TYPE_LRU_PERCPU_HASH 和标志 BPF_F_NO_COMMON_LRU 创建的,则所有映射属性都将是每个 CPU 的。