BPF数据传递的桥梁——BPF Map(一)


Docker 技术鼻祖系列

对 ebpf 技术感兴趣的同学可以订阅作者的博客主题:

https://davidlovezoe.club/wordpress/archives/tag/ebpf

这是一个实战系列文章,它是eBPF 学习计划[1]里面的应用场景之网络部分,终极目标是源码级别学习云原生网络方案 Cilium(声明:下文提到的 BPF 字样是泛指,包括 cBPF 和 eBPF)。本篇文章从源码级别学习 BPF Map 使用场景和工作原理,祝大家阅读愉快。

TL;DR

文章涉及的实验环境和代码可以到这个 git repo 获取:https://github.com/nevermosby/linux-bpf-learning/tree/master/bpf/bpf-maps[2]

为什么需要 BPF Map

通过消息传递来触发程序中的行为是软件工程中广泛使用的技术。一个程序可以通过发送消息来修改另一个程序的行为,这也允许这些程序之间通过这个方式来传递信息。关于 BPF 最吸引人的一个方面,就是运行在内核上的程序可以在运行时使用消息传递相互通信,我称之为 「communication on air」。而 BPF Map 就是用户空间和内核空间之间的数据交换、信息传递的桥梁。

BPF Map 是什么

BPF Map 本质上是以「键/值」方式存储在内核中的数据结构,它们可以被任何知道它们的 BPF 程序访问。在内核空间的程序创建 BPF Map 并返回对应的文件描述符,在用户空间运行的程序就可以通过这个文件描述符来访问并操作 BPF Map,这就是为什么 BPF Map 在 BPF 世界中是桥梁的存在了。

BPF Map 类型

根据申请内存方式的不同,BPF Map 有很多种类型(具体的列表可以看之前的博客文章[3]),常用的类型是BPF_MAP_TYPE_HASHBPF_MAP_TYPE_ARRAY,它们背后的内存管理方式跟我们熟悉的哈希表和数组基本一致,此外还有包括BPF_MAP_TYPE_PROG_ARRAYBPF_MAP_TYPE_PERF_EVENT_ARRAY等 10 余种 Map 类型,具体可以查看之前的博文[4]。随着多 CPU 架构的成熟发展,BPF Map 也引入了 per-cpu 类型,如BPF_MAP_TYPE_PERCPU_HASHBPF_MAP_TYPE_PERCPU_ARRAY等,当你使用这种类型的 BPF Map 时,每个 CPU 都会存储并看到它自己的 Map 数据,从属于不同 CPU 之间的数据是互相隔离的,这样做的好处是,在进行查找和聚合操作时更加高效,性能更好,尤其是你的 BPF 程序主要是在做收集时间序列型数据,如流量数据或指标等。

如何创建 BPF Map

原生版 BPF Map 创建方式

union bpf_attr my_map_attr {
  .map_type = BPF_MAP_TYPE_ARRAY,
  .key_size = sizeof(int),
  .value_size = sizeof(int),
  .max_entries = 1024,
  .map_flags = BPF_F_NO_PREALLOC,
};

int fd = bpf(BPF_MAP_CREATE, &my_map_attr, sizeof(my_map_attr));

上面是原生创建 BPF Map 的代码片段,最初创建 BPF Map 的方式都是通过 bpf 系统调用函数(上述代码第 9 行),传入的第一个参数是BPF_MAP_CREATE,它是创建 BPF Map 系统调用的代号,第二参数是指定将要创建 Map 的属性,第三个参数是这个 Map 配置的大小。因此创建 Map 之前首先要声明一个 BPF Map(上述代码的第 1-7 行),其中有四大要素:

  • Map 类型(map_type),就是上文提到的各种 Map 类型

  • Map 的键大小(key_size),以字节为单位

  • Map 的值大小(value_size),以字节为单位

  • Map 的元素最大容量(max_entries),个数为单位 可以去到内核代码里看到相关定义[5]

union bpf_attr {
 struct { /* anonymous struct used by BPF_MAP_CREATE command */
  __u32 map_type; /* one of enum bpf_map_type */
  __u32 key_size; /* size of key in bytes */
  __u32 value_size; /* size of value in bytes */
  __u32 max_entries; /* max number of entries in a map */
  __u32 map_flags; /* BPF_MAP_CREATE related
      * flags defined above.
      */
  __u32 inner_map_fd; /* fd pointing to the inner map */
  __u32 numa_node; /* numa node (effective only if
      * BPF_F_NUMA_NODE is set).
      */
  char map_name[BPF_OBJ_NAME_LEN];
 };

除了四大要素之后,还有一些高级选项,其中map_flags作为可以调整 Map 创建行为的参数,使用频率也是越来越高。

创建 BPF Map 的返回值

上文示例代码的第 9 行中变量fd是创建 BPF Map 系统调用的返回值。正常情况下它的值是被成功创建出来的 BPF Map 的文件描述符(file descriptor)。非正常情况下,也就是系统调用失败,BPF Map 没有被成功创建出来,返回值为 -1,可以通过系统全局错误变量errno获取具体的错误信息,一般创建失败有 3 种原因:

  • BPF Map 定义的属性不正确,此时 errno 变量值为EINVAL

  • 当前用户没有足够权限执行相关操作,此时 errno 变量值为EPERM

  • 没有足够的内存空间来保存新创建 BPF Map,此时 errno 变量值为ENOMEN

简化版 BPF Map 创建方式

相对于直接使用 bpf 系统调用函数来创建 BPF Map,在实际场景中常用的是一个简化版:

struct bpf_map_def SEC("maps") my_bpf_map = {
 .type      = BPF_MAP_TYPE_HASH,
 .key_size  = sizeof(int),
 .value_size  = sizeof(int),
 .max_entries = 100,
 .map_flags  = BPF_F_NO_PREALLOC,
};

这个简化版看起来就是一个 BPF Map 声明,它是如何做到声明即创建的呢?关键点就是SEC("maps"),学名 ELF 惯例格式(ELF convention),它的工作原理是这样的:

  1. 声明 ELF Section 属性 SEC("maps") (之前的博文[6]里有对 Section 作用的描述)

  2. 内核代码`bpf_load.c`[7]respect 目标文件中所有 Section 信息,它会扫描目标文件里定义的 Section,其中就有用来创建 BPF Map 的SEC("maps"),我们可以到相关代码[8]里看到说明:

// https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.h#L41
/* parses elf file compiled by llvm .c->.o
 * . parses 'maps' p and creates maps via BPF syscall // 就是这里
 * . parses 'license' p and passes it to syscall
 * . parses elf relocations for BPF maps and adjusts BPF_LD_IMM64 insns by
 *   storing map_fd into insn->imm and marking such insns as BPF_PSEUDO_MAP_FD
 * . loads eBPF programs via BPF syscall
 *
 * One ELF file can contain multiple BPF programs which will be loaded
 * and their FDs stored stored in prog_fd array
 *
 * returns zero on success
 */
int load_bpf_file(char *path);
  1. `bpf_load.c`[9]扫描到SEC("maps")后,对 BPF Map 相关的操作是由`load_maps`[10]函数完成,其中的`bpf_create_map_node()`[11]和`bpf_create_map_in_map_node()`[12]就是创建 BPF Map 的关键函数,它们背后都是调用了定义在内核代码tools/lib/bpf/bpf.c[13]中的方法,而这个方法[14]就是使用上文提到的BPF_MAP_CREATE命令进行的系统调用。

  2. 最后在编译程序时,通过添加bpf_load.o作为依赖库,并合并为最终的可执行文件中,这样在程序运行起来时,就可以通过声明SEC("maps")即可完成创建 BPF Map 的行为了。从上面梳理的过程可以看到,这个简化版虽然使用了“语法糖”,但最后还是会去使用 bpf()函数完成系统调用。

如何操作 BPF Map

BPF Map 也有自己的 CRUD,除了bpf_map_create是创建 BPF Map 操作之外,下面列出了其他主要操作,

  • bpf_map_lookup_elem(map, key)函数,通过 key 查询 BPF Map,得到对应 value

  • bpf_map_update_elem(map, key, value, options)函数,通过 key-value 更新 BPF Map,如果这个 key 不存在,也可以作为新的元素插入到 BPF Map 中去

  • bpf_map_get_next_key(map, lookup_key, next_key)函数,这个函数可以用来遍历 BPF Map,下文有具体的介绍。这里只是简单罗列了操作 BPF Map 的函数名称,想具体了解函数使用场景的可以看这本书「linux-observability-with-bpf」[15]的第 3 章节,这本书也有了中文版——「Linux 内核观测技术 BPF」[16],作为国内第一部关于 BPF 的技术书籍,感兴趣的都去支持下哦。

开发 BPF Map 示例程序

功能设计

还记得上一篇如何调试 BPF 程序的博文[17]么,我们是通过bpf_trace_printk函数打印日志来调试程序,但是这个只能在内核空间使用的 helper 函数,其本身有很多限制,包括输出参数的个数有限、类型有限等。现在我们就可以借助 BPF Map 来完成相同的功能,即在内核空间收集网络包信息(源地址和目标地址),在用户空间展示这些信息,完成期待的程序「调试」工作。

代码设计

代码主要分两个部分:

  1. 一个是运行在内核空间的程序,主要功能为创建出定制版 BPF Map,收集目标信息并存储至 BPF Map 中。

  2. 另一个是运行在用户空间的程序,主要功能为读取上面内核空间创建出的 BPF Map 里的数据,并进行格式化展示,以演示 BPF Map 在两者之间进行数据传递。请注意,该程序的编译运行是基于 Linux 内核代码中 BPF 示例环境,如果你还不熟悉,可以参考这篇博文[18]。下面首先介绍运行在内核空间的示例代码:

#define KBUILD_MODNAME "foo"
#include <uapi/linux/bpf.h>
#include <uapi/linux/if_ether.h>
#include <uapi/linux/if_packet.h>
#include <uapi/linux/if_vlan.h>
#include <uapi/linux/ip.h>
#include <uapi/linux/in.h>
#include <uapi/linux/tcp.h>
#include <uapi/linux/udp.h>
#include "bpf_helpers.h"
#include "bpf_endian.h"
#include "xdp_ip_tracker_common.h"

#define bpf_printk(fmt, ...)                       \
    ({                                             \
        char ____fmt[] = fmt;                      \
        bpf_trace_printk(____fmt, sizeof(____fmt), \
                         ##__VA_ARGS__);           \
    })

struct bpf_map_def SEC("maps") tracker_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(struct pair),
    .value_size = sizeof(struct stats),
    .max_entries = 2048,
};

static __always_inline bool parse_and_track(bool is_rx, void *data_begin, void *data_end, struct pair *pair)
{
    struct ethhdr *eth = data_begin;

    if ((void *)(eth + 1) > data_end)
        return false;

    if (eth->h_proto == bpf_htons(ETH_P_IP))
    {
        struct iphdr *iph = (struct iphdr *)(eth + 1);
        if ((void *)(iph + 1) > data_end)
            return false;

        pair->src_ip = is_rx ? iph->daddr : iph->saddr;
        pair->dest_ip = is_rx ? iph->saddr : iph->daddr;

        // update the map for track
        struct stats *stats, newstats = {0, 0, 0, 0};
        long long bytes = data_end - data_begin;

        stats = bpf_map_lookup_elem(&tracker_map, pair);
        if (stats)
        {
            if (is_rx)
            {
                stats->rx_cnt++;
                stats->rx_bytes += bytes;
            }
            else
            {
                stats->tx_cnt++;
                stats->tx_bytes += bytes;
            }
        }
        else
        {
            if (is_rx)
            {
                newstats.rx_cnt = 1;
                newstats.rx_bytes = bytes;
            }
            else
            {
                newstats.tx_cnt = 1;
                newstats.tx_bytes = bytes;
            }
            bpf_map_update_elem(&tracker_map, pair, &newstats, BPF_NOEXIST);
        }
        return true;
    }
    return false;
}

SEC("xdp_ip_tracker")
int _xdp_ip_tracker(struct xdp_md *ctx)
{
    // the struct to store the ip address as the keys of bpf map
    struct pair pair;

    bpf_printk("starting xdp ip tracker...\n");

    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    // pass if the network packet is not ipv4
    if (!parse_and_track(true, data, data_end, &pair))
        return XDP_PASS;

    return XDP_DROP;
}

char _license[] SEC("license") = "GPL";

接下来是运行在用户空间的示例代码:

#include <linux/bpf.h>
#include <linux/if_link.h>
#include <assert.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <arpa/inet.h>
#include <netinet/ether.h>
#include <unistd.h>
#include <time.h>
#include "bpf_load.h"
#include <bpf/bpf.h>
#include "bpf_util.h"
#include "xdp_ip_tracker_common.h"

static int ifindex = 6; // target network interface to attach, you can find it via `ip a`
static __u32 xdp_flags = 0;

// unlink the xdp program and exit
static void int_exit(int sig)
{
    printf("stopping\n");
    set_link_xdp_fd(ifindex, -1, xdp_flags);
    exit(0);
}

// An XDP program which track packets with IP address
// Usage: ./xdp_ip_tracker
int main(int argc, char **argv)
{
    char *filename = "xdp_ip_tracker_kern.o";
    // change limits
    struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
    if (setrlimit(RLIMIT_MEMLOCK, &r))
    {
        perror("setrlimit(RLIMIT_MEMLOCK, RLIM_INFINITY)");
        return 1;
    }

    // load the kernel bpf object file
    if (load_bpf_file(filename))
    {
        printf("error - bpf_log_buf: %s", bpf_log_buf);
        return 1;
    }

    // confirm the bpf prog fd is available
    if (!prog_fd[0])
    {
        printf("load_bpf_file: %s\n", strerror(errno));
        return 1;
    }

    // add signal handlers
    signal(SIGINT, int_exit);
    signal(SIGTERM, int_exit);

    // link the xdp program to the network interface
    if (set_link_xdp_fd(ifindex, prog_fd[0], xdp_flags) < 0)
    {
        printf("link set xdp fd failed\n");
        return 1;
    }

    int result;
    struct pair next_key, lookup_key = {0, 0};
    struct stats value = {};
    while (1)
    {
        sleep(2);
        // retrieve the bpf map of statistics
        while (bpf_map_get_next_key(map_fd[0], &lookup_key, &next_key) != -1)
        {
            //printf("The local ip of next key in the map is: '%d'\n", next_key.src_ip);
            //printf("The remote ip of next key in the map is: '%d'\n", next_key.dest_ip);
            struct in_addr local = {next_key.src_ip};
            struct in_addr remote = {next_key.dest_ip};
            printf("The local ip of next key in the map is: '%s'\n", inet_ntoa(local));
            printf("The remote ip of next key in the map is: '%s'\n", inet_ntoa(remote));

            // get the value via the key
            // TODO: change to assert
            // assert(bpf_map_lookup_elem(map_fd[0], &next_key, &value) == 0)
            result = bpf_map_lookup_elem(map_fd[0], &next_key, &value);
            if (result == 0)
            {
                // print the value
                printf("rx_cnt value read from the map: '%llu'\n", value.rx_cnt);
                printf("rx_bytes value read from the map: '%llu'\n", value.rx_bytes);
            }
            else
            {
                printf("Failed to read value from the map: %d (%s)\n", result, strerror(errno));
            }
            lookup_key = next_key;
            printf("\n\n");
        }
        printf("start a new loop...\n");
        // reset the lookup key for a fresh start
        lookup_key.src_ip = 0;
        lookup_key.dest_ip = 0;
    }

    printf("end\n");
    // unlink the xdp program
    set_link_xdp_fd(ifindex, -1, xdp_flags);
    return 0;
}

分析代码

我们先来看运行在内核空间的 BPF 程序代码重点内容:

  • 通过SEC("maps")声明并创建了一个名为tracker_map 的 BPF Map,它的类型是BPF_MAP_TYPE_HASH,它的 key 和 value 都是自定义的 struct,定义在了xdp_ip_tracker_common.h头文件中,具体如下所示:

  // define the struct for the key of bpf map
  struct pair {
  **u32 src_ip;
  **u32 dest_ip;
  };

       // define the struct for the value of bpf map
       struct stats {
         __u64 tx_cnt; // the sending request count
         __u64 rx_cnt; // the received request count
         __u64 tx_bytes; // the sending request bytes
         __u64 rx_bytes; // the sending received bytes
       };

你可以基于自定义struct,实现灵活的数据结构设计,比起之前使用bpf_trace_printk函数只能使用单一的数据类型,will make you smile again.

  • 函数parse_and_track是对网络包进行分析和过滤,把源地址和目的地址联合起来作为 BPF Map 的 key,把当前网络包的大小以 byte 单位记录下来,并联合网络包计数器作为 BPF Map 的 value。对于连续的网络包,如果生成的 key 已经存在,就把 value 累加,否则就新增一对 key-value 存入 BPF Map 中。其中通过bpf_map_lookup_elem()函数来查找元素,bpf_map_update_elem()函数来新增元素。

我们再来看运行在用户空间程序代码的重点内容:

  • 用户空间的代码跟一般看到的 C 程序的结构是一样的,都是有 main 函数作为入口。基本流程是,通过`load_bpf_file()`[19]函数(本质就是用BPF_PROG_LOAD命令进行系统调用)加载对应内核空间的 BPF 程序编译出来的**.o**文件,这种通过编程加载 BPF 程序的方式,和我们之前通过命令行工具的方式相比,更具灵活性,适合实际场景中的产品分发。

  • 加载完 BPF 程序之后,使用`set_link_xdp_fd()`[20]函数 attach 到目标 hook 上,看函数名就知道了,这是 XDP network hook。它接受的两个主要的参数是:- ifindex,这个是目标网卡的序号(可以通过ip a查看),我这里填写的是 6,它是对应了一个 docker 容器的 veth 虚拟网络设备;- prog_fd[0],这个是 BPF 程序加载到内存后生成的文件描述符 fd。

  • 有两个神奇的变量prog_fdmap_fd得说明下:- 它们都是定义在`bpf_load.c`[21]的全局变量;- prog_fd是一个数组,在加载内核空间 BPF 程序时,一旦 fd 生成后,就添加到这个数组中[22]去;- map_fd也是一个数组,在运行上文提到的`load_maps()`[23]函数时,一旦完成创建 BPF Map 系统调用生成 fd 后,同样会添加到这个数组中[24]去。因此在 bpf sample 文件夹下的程序可以直接使用这两个变量,作为对于 BPF 程序和 BPF Map 的引用。

  • 从 XX 行开始是一个无限循环,里面是每 2 秒获取一下目标 BPF Map 的数据。获取的逻辑是通过bpf_map_get_next_key(map_fd[0], &lookup_key, &next_key)函数,map_fd[0]是你的目标 BPF Map;lookup_key是需要查找的 BPF Map 目标 key,这个参数是要主动传入的,而next_key是这个目标 key 相邻的下一个 key,这个参数是被动赋值的。如果你想从头开始遍历 BPF Map,就可以通过传入一个一定不存在的 key 作为lookup_key,然后next_key会被自动赋值为 BPF Map 中第一个 key,key 知道了,对应的 value 也就可以被读取了,直到bpf_map_get_next_key()返回为-1,即next_key没有可以被赋值的了,遍历也就完成了,这个函数工作起来是不是像一个 iterator。通过上面两层循环,不停遍历 BPF Map 并打印里面的内容,一旦有新的网络包进来,也能及时获取到相关信息。

  • 还有一段非常陌生的代码,如下所示:

struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
if (setrlimit(RLIMIT_MEMLOCK, &r))
{
   perror("setrlimit(RLIMIT_MEMLOCK, RLIM_INFINITY)");
   return 1;
}
  • 这里有一个struct叫`rlimit`[25],全称是resource limit,顾名思义,它是控制应用进程能使用资源的限额。

  • 常量RLIM_INFINITY看起来就是无限的意思,因此第一行代码就是定义了一个没有上限的资源配额。

  • 第二行代码使用了函数setrlimit(),传入的第一个参数是一个资源规格名称——RLIMIT_MEMLOCK,即内存;第二个参数是刚才定义的无限资源配额,可以猜出这行代码的意思就是为内存资源配置了无限配额,即没有内存上限。

  • 为什么要把内存限制放开呢?因为操作系统在不同的CPU架构,对于应用进程能使用的内存限制是不统一的,而不同的BPF程序需要使用到的内存资源也是可变的,比如你的BPF Map申请了很大的max_entries,那么这个BPF程序一定会使用不少的内存。因此为了成功运行BPF程序,就把对于内存的限制放开成无限了。

想了解更多关于rlimit的信息,可以看这篇博文[26]

示例程序的运行效果

视频地址:https://www.youtube.com/embed/jiKmbSZh6jU

BPF Map背后的fd分析

在Unix/Linux的世界,一切皆是文件,BPF Map也不例外。从上文看到我们是可以通过文件描述符fd来访问BPF Map内的数据,因此BPF Map创建是遵循Linux文件创建的过程。实现 BPF_MAP_CREATE 系统调用命令的函数是 map_create()[27],即创建BPF Map的核心函数:

// https://elixir.bootlin.com/linux/v4.15/source/kernel/bpf/syscall.c#L383
static int map_create(union bpf_attr *attr)
{
 int numa_node = bpf_map_attr_numa_node(attr);
 struct bpf_map *map;
 int f_flags;
 int err;

    err = CHECK_ATTR(BPF_MAP_CREATE);
    if (err)
     return -EINVAL;

    f_flags = bpf_get_file_flag(attr->map_flags);
    if (f_flags < 0)
     return f_flags;

    if (numa_node != NUMA_NO_NODE &&
        ((unsigned int)numa_node >= nr_node_ids ||
         !node_online(numa_node)))
     return -EINVAL;

    /* find map type and init map: hashtable vs rbtree vs bloom vs ... */
    map = find_and_alloc_map(attr);
    if (IS_ERR(map))
     return PTR_ERR(map);

    err = bpf_obj_name_cpy(map->name, attr->map_name);
    if (err)
     goto free_map_nouncharge;

    atomic_set(&map->refcnt, 1);
    atomic_set(&map->usercnt, 1);

    err = security_bpf_map_alloc(map);
    if (err)
     goto free_map_nouncharge;

    err = bpf_map_charge_memlock(map);
    if (err)
     goto free_map_sec;

    err = bpf_map_alloc_id(map);
    if (err)
     goto free_map;

    // assign a fd for bpf map
    err = bpf_map_new_fd(map, f_flags);
    if (err < 0) {
     /* failed to allocate fd.
      * bpf_map_put() is needed because the above
      * bpf_map_alloc_id() has published the map
      * to the userspace and the userspace may
      * have refcnt-ed it through BPF_MAP_GET_FD_BY_ID.
      */
     bpf_map_put(map);
     return err;
    }

    trace_bpf_map_create(map, err);
    return err;

free_map:
bpf_map_uncharge_memlock(map);
free_map_sec:
security_bpf_map_free(map);
free_map_nouncharge:
map->ops->map_free(map);
return err;
}

其中`bpf_map_new_fd()`[28]函数就是用来为BPF Map分配fd的,下面是其函数主体:

// https://elixir.bootlin.com/linux/v4.15/source/kernel/bpf/syscall.c#L327
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_getfd - creates a new file instance by hooking it up to an
 *                    anonymous inode, and a dentry that describe the "class"
 *                    of the file
 *
 * @name:    [in]    name of the "class" of the new file
 * @fops:    [in]    file operations for the new file
 * @priv:    [in]    private data for the new file (will be file's private_data)
 * @flags:   [in]    flags
 *
 * Creates a new file by hooking it on a single inode. This is useful for files
 * that do not need to have a full-fledged inode in order to operate correctly.
 * All the files created with anon_inode_getfd() will share a single inode,
 * hence saving memory and avoiding code duplication for the file/inode/dentry
 * setup.  Returns new descriptor or an error code.
 */
 return anon_inode_getfd("bpf-map", &bpf_map_fops, map,
    flags | O_CLOEXEC);
}

要说的是anon_inode_getfd()这个函数,它不是一般的分配 fd 的方式,是一种特殊的匿名(anonymous)方式,它的 inode 没有被绑定到磁盘上的某个文件,而是仅仅在内存里。一旦 fd 关闭后,对应的内存空间就会被释放,相关数据,即我们的 BPF Map 也就被删除了。它的 comment doc 写得非常好,详细大家可以自行了解。也可以通过lsofcat /proc/[pid]/fd命令看到 BPF Map 作为anon_inode的效果(其实普通的 BPF 程序也是这个 type):

查看正在使用的 BPF Map

如果想看当前操作系统上面是否有正在使用 BPF Map,可以使用 BPF 社区大力推荐的命令行工具——BPFtool[29],它是专门用来查看 BPF 程序和 BPF Map 的命令行工具,并且可以对它们做一些简单操作。BPFtool 源码[30]被维护在 Linux 内核代码里(可见其重要性),因此一般都是通过 make 命令自行编译出可执行文件,操作起来并不麻烦,如下所示:

cd linux-source-code/tools
make -C  bpf/bpftool/
cd bpf/bpftool/
// the output is a binary named as `bpftool`
./bpftool [prog|map]

需要注意的是,不同内核版本下的 BPFtool 代码有所差异,其功能也不一样,一般来说高版本内核下的 BPFtool 功能更多,也是向下兼容的。我使用的就是在 5.6.6 内核版本下编译出来的 BPFtool,并且在内核版本是 4.15.0 操作系统上运行顺畅。

使用 BPFtool 查看 BPF Map 信息

接下来给大家简单演示如何使用 bpftool 查看 BPF Map 信息,主要用两个命令进行查看:

# command #1, list all the bpf map in the current node
# you can find map id, map type, map name, key type, value type, the number of max entry and memory allocation in the output
> bpftool map
29: hash  name tracker_map  flags 0x0
 key 8B  value 32B  max_entries 2048  memlock 217088B


# command #2, show the bpf map details including keys and value in hex-format
# the map id can be found in the output of command #1
# you can also find the element number
> bpftool map dump id [map id]
key:
c0 a8 3a 01 ac 11 00 02
value:
00 00 00 00 00 00 00 00  0a 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  e4 02 00 00 00 00 00 00
key:
ac 11 00 01 ac 11 00 02
value:
00 00 00 00 00 00 00 00  07 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  06 02 00 00 00 00 00 00
Found 2 elements

以下演示两个场景的 BPF Map 信息:

  • 当前的实验环境(ubuntu 18.04 with kernel 4.15.0),正在运行上文的示例程序:

  > root@bpf-ubuntubox:~# ./hbpftool map
  2: lpm_trie flags 0x1
  key 8B value 8B max_entries 1 memlock 4096B
  3: lpm_trie flags 0x1
  key 20B value 8B max_entries 1 memlock 4096B
  4: lpm_trie flags 0x1
  key 8B value 8B max_entries 1 memlock 4096B
  5: lpm_trie flags 0x1
  key 20B value 8B max_entries 1 memlock 4096B
  6: lpm_trie flags 0x1
  key 8B value 8B max_entries 1 memlock 4096B
  7: lpm_trie flags 0x1
  key 20B value 8B max_entries 1 memlock 4096B
  29: hash name tracker_map flags 0x0
  key 8B value 32B max_entries 2048 memlock 217088B

  > root@bpf-ubuntubox:~# ./hbpftool map dump id 29
  key:
  c0 a8 3a 01 ac 11 00 02
  value:
  00 00 00 00 00 00 00 00  0a 00 00 00 00 00 00 00
  00 00 00 00 00 00 00 00  e4 02 00 00 00 00 00 00
  key:
  ac 11 00 01 ac 11 00 02
  value:
  00 00 00 00 00 00 00 00  07 00 00 00 00 00 00 00
  00 00 00 00 00 00 00 00  06 02 00 00 00 00 00 00
  Found 2 elements

注:这个环境的BPFTool命名为hbpftool,因为它来自于更高版本的内核,区别于当前内核版本下的bpftool

  • 一个部署了 Cilium 网络方案的 kubernetes 工作节点

  > root@k8s-cilium-w1:~# ./bpftool map
  2: lpm_trie flags 0x1
  key 8B value 8B max_entries 1 memlock 4096B
  3: lpm_trie flags 0x1
  key 20B value 8B max_entries 1 memlock 4096B
  4: lpm_trie flags 0x1
  key 8B value 8B max_entries 1 memlock 4096B
  5: lpm_trie flags 0x1
  key 20B value 8B max_entries 1 memlock 4096B
  6: lpm_trie flags 0x1
  key 8B value 8B max_entries 1 memlock 4096B
  7: lpm_trie flags 0x1
  key 20B value 8B max_entries 1 memlock 4096B
  37: hash flags 0x1
  key 20B value 48B max_entries 65535 memlock 8916992B
  38: lpm_trie flags 0x1
  key 24B value 12B max_entries 512000 memlock 36868096B
  39: percpu_hash flags 0x1
  key 8B value 16B max_entries 65536 memlock 7340032B
  40: hash flags 0x1
  key 20B value 20B max_entries 65536 memlock 7344128B
  41: hash flags 0x1
  key 12B value 12B max_entries 65536 memlock 6295552B
  42: hash flags 0x1
  key 2B value 8B max_entries 65536 memlock 5246976B
  43: hash flags 0x1
  key 2B value 6B max_entries 65536 memlock 5246976B
  46: prog_array flags 0x0
  key 4B value 4B max_entries 65535 memlock 528384B
  owner_prog_type sched_cls owner jited
  47: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  48: array flags 0x0
  key 4B value 1B max_entries 1 memlock 4096B
  49: perf_event_array flags 0x0
  key 4B value 4B max_entries 2 memlock 4096B
  50: perf_event_array flags 0x0
  key 4B value 4B max_entries 2 memlock 4096B
  51: lru_hash flags 0x0
  key 14B value 56B max_entries 524288 memlock 71307264B
  52: lru_hash flags 0x0
  key 14B value 56B max_entries 262144 memlock 35655680B
  53: lru_hash flags 0x0
  key 14B value 40B max_entries 524288 memlock 62918656B
  54: lru_hash flags 0x0
  key 4B value 8B max_entries 524288 memlock 41947136B
  55: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  56: lru_hash flags 0x0
  key 16B value 8B max_entries 262144 memlock 23072768B
  57: lru_hash flags 0x0
  key 32B value 20B max_entries 262144 memlock 31461376B
  58: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  59: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  60: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  61: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  62: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  63: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  64: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  65: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  66: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  67: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  68: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  69: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  70: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  71: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  72: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  73: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  74: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  75: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  76: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  77: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  78: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  79: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  80: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  81: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  82: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  83: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  84: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  85: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  86: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  87: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  88: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited
  89: hash flags 0x1
  key 8B value 24B max_entries 16384 memlock 1576960B
  90: prog_array flags 0x0
  key 4B value 4B max_entries 22 memlock 4096B
  owner_prog_type sched_cls owner jited

  > root@k8s-cilium-w1:~# ./bpftool map dump id 90
  key: 01 00 00 00  value: 4e 01 00 00
  key: 06 00 00 00  value: 51 01 00 00
  key: 0f 00 00 00  value: 4f 01 00 00
  key: 11 00 00 00  value: 50 01 00 00
  Found 4 elements

可以看到Cilium创建了很多BPF Map,主要是hashprog_array两个类型,存储了大量信息用来实现kube-proxy组件的功能,待深入学习后会单独写系列文章出来。

BPFtool 其实功能非常丰富,后面还会使用到它,大家感兴趣的可以去这里[31]看看更多功能介绍。

彩蛋

讲了上面的 BPF Map 背后原理,可以看到只要能知晓 BPF Map 的 fd 就能访问里面的数据,那么其他非 BPF 程序是否也能访问 BPF Map?答案是肯定的,这次的彩蛋,就给大家介绍一个来自于腾讯云团队的使用 IPVS 模块操作 BPF Map 的例子,大致技术是这样的:

当前 Kubernetes Service 的主流方案采用 IPVS 和 iptables 的混合模式,IPVS 实现后端 pod 之间负载均衡,而由于 IPVS 没有实现 SNAT 功能,就使用了 iptables 辅助实现 SNAT。这种组合性能并不理想,原因在于 iptables 完成了很多通用功能,对于单纯使用 SNAT 而言显得过于复杂,造成性能损耗。为了解决这个问题,腾讯云通过 eBPF 扩展了 IPVS 的功能,新方案用 eBPF 程序实现 SNAT 功能,并修改 IPVS 内核源码与 eBPF 密切配合,实现了 FULL-NAT 的负载均衡器。详细的内容可以查看这篇文章[32]。在腾讯云的技术实现细节中,就有通过 IPVS 模块向 BPF Map 插入数据的逻辑,其关键代码如下所示:

// https://github.com/Tencent/TencentOS-kernel/blob/master/net/netfilter/ipvs/ip_vs_bpf_proc.c#L56
struct bpf_map  *conntrack_map;


// https://github.com/Tencent/TencentOS-kernel/blob/master/net/netfilter/ipvs/ip_vs_bpf_proc.c#L402
static ssize_t ip_vs_bpf_write(struct file *file,
          const char __user *ubuf,
          size_t count,
          loff_t *ppos)
{
 int err = 0;
 struct bpf_map *map = NULL;
 struct bpf_prog *prog1 = NULL;
 struct bpf_prog *prog2 = NULL;
 const char delim[2] = ":";
 char ids[5][20];
 char *token;
 int tag, pid;
 unsigned int mapid, progid1, progid2;
 char buf[100];
 int i = 0;
 char *s = buf;
...

 /* singleton:conntrack_map is assigned once,
  * and be nulled in module exit
  */
 if (conntrack_map) {
  pr_err("%s %d conntrack_map exists\n",
         __func__, __LINE__);
  return -EEXIST;
 }
...

 err = bpf_conntrack_map_get(pid, mapid,
        (long long)(resolve_addrs.bpf_map_fops),
        &map);
 if (err != 0 || !map) {
  pr_err("%s acquire bpf_map failed\n", __func__);
  return -EINVAL;
 }
...

 bpf_map_fd = mapid;
...
    // got the target bpf map via fd
 conntrack_map = map;
...
}

// https://github.com/Tencent/TencentOS-kernel/blob/master/net/netfilter/ipvs/ip_vs_bpf_proc.c#L306
static int bpf_conntrack_map_get(int pid,
     unsigned int fd,
     unsigned long long addr,
     struct bpf_map **map)
{
 struct files_struct *files;
 struct task_struct *task;
 struct file *file;
 int err = 0;

...
 file = fcheck_files(files, fd);
...
 *map = file->private_data;
 if (*map)
  bpf_map_inc2(*map);
...
}

简单说明下ip_vs_bpf_proc.c文件:

  • ip_vs_bpf_proc.c这个文件开头声明了一个bpf_map结构(来自内核 bpf 体系)的变量conntrack_map,这个就是将来被 IPVS 操作的 BPF Map 对象。

  • 同样是在ip_vs_bpf_proc.c这个文件里,函数ip_vs_bpf_write()内调用了bpf_conntrack_map_get()方法,而它的入参之一mapid在这里就是 BPF Map 的 fd,这个方法内容就是通过 fd 找到了背后相关数据,并序列化为bpf_map结构的对象,最终赋值给conntrack_map,完成目标 BPF Map 的获取。然后在 IPVS 模块创建新 connection 时,对上面conntrack_map插入了必要的元素,关键代码如下所示:

// https://github.com/Tencent/TencentOS-kernel/blob/master/net/netfilter/ipvs/ip_vs_conn.c#L1098
static bool ip_vs_conn_new_bpf(struct ip_vs_dest *dest,
          unsigned int flags,
          const struct ip_vs_conn_param *p,
          int *skip)
{
 int i;
 struct ip_vs_service *svc;
 struct bpf_lb_conn_key key = {};
 struct bpf_lb_conn_key reply_key = {};
 struct bpf_lb_conn_value value = {};
 struct bpf_lb_conn_value reply_value = {};
 struct bpf_lb_conn_value *v;
 int inserted = 0;
 struct bpf_map *map;

...
 if (!bpf_mode_on)
  return true;
...
 // conntrack_map is the same one
 map = conntrack_map;
...
 lip = alloc_localip();
 reply_key.sip = key.dip;
 reply_key.sport = key.dport;
 reply_key.dip = lip;
 reply_key.dport = key.sport;
 reply_key.proto = p->protocol;
 reply_key.vip = 0;
 reply_key.vport = 0;
 reply_key.pad = 0;

 atomic_set(&reply_value.ref, 0);
 reply_value.sip = key.dip;
 reply_value.sport = key.dport;
 reply_value.dip = key.sip;
 reply_value.dport = key.sport;
 reply_value.proto = p->protocol;
...
 if (likely(!map->ops->map_lookup_elem(map, &reply_key))) {
  if (likely(map->ops->map_update_elem(map,
            &reply_key,
            &reply_value,
            BPF_ANY) == 0)) {
    /* the common case! break the loop */
    inserted = 1;
    nf_conntrack_single_unlock(&reply_key,
          map->key_size);
    break;
   }
   /* if lookup ok, shall insert ok since lock is held!*/
   pr_err("map insert key failed\n");

   BPF_STAT_INC(p->ipvs, BPF_NEW_INSERT);
   nf_conntrack_single_unlock(&reply_key, map->key_size);
   return false;
  }
...
}

在上面的代码里使用了map_lookup_elem()map_update_elem()两个操作 BPF Map 的方法——先查询后更新(插入新的元素),完成对 BPF Map 的操作。

是不是很有趣?

下篇预告——如何持久化 BPF Map 数据

上面讲到了 BPF Map 生命周期是跟着它的 fd,当它所在的 BPF 程序运行结束后,fd 也就关闭了,Map 数据就没了?

如果开发者觉得 BPF Map 数据很重要,希望持久保留,该怎么办呢?社区大佬听到开发者的心声,大手一挥,实现了 BPF Map 数据持久化。详情请听下回分解。

参考资料

[1]

eBPF 学习计划: https://davidlovezoe.club/ebpf-learning

[2]

https://github.com/nevermosby/linux-bpf-learning/tree/master/bpf/bpf-maps: https://github.com/nevermosby/linux-bpf-learning/tree/master/bpf/bpf-maps

[3]

之前的博客文章: https://davidlovezoe.club/wordpress/archives/867#eBPF_%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84

[4]

之前的博文: https://davidlovezoe.club/wordpress/archives/867#eBPF_%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84

[5]

定义: https://elixir.bootlin.com/linux/v4.15/source/include/uapi/linux/bpf.h#L228

[6]

博文: https://davidlovezoe.club/wordpress/archives/937#%E8%AE%BE%E8%AE%A1%E4%BD%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AAXDP%E7%A8%8B%E5%BA%8F

[7]

bpf_load.c: https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.c

[8]

相关代码: https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.h#L41

[9]

bpf_load.c: https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.c

[10]

load_maps: https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.c#L212

[11]

bpf_create_map_node(): https://elixir.bootlin.com/linux/v4.15/source/tools/lib/bpf/bpf.c#L62

[12]

bpf_create_map_in_map_node(): https://elixir.bootlin.com/linux/v4.15/source/tools/lib/bpf/bpf.c#L101

[13]

tools/lib/bpf/bpf.c: https://elixir.bootlin.com/linux/v4.15/source/tools/lib/bpf/bpf.c

[14]

这个方法: https://elixir.bootlin.com/linux/v4.15/source/tools/lib/bpf/bpf.c#L83

[15]

「linux-observability-with-bpf」: https://sysdig.com/resources/papers/linux-observability-with-bpf/

[16]

中文版——「Linux 内核观测技术 BPF」: https://item.jd.com/12939760.html

[17]

如何调试 BPF 程序的博文: https://davidlovezoe.club/bpf-debug-101

[18]

这篇博文: https://davidlovezoe.club/compile-bpf-examples

[19]

load_bpf_file(): https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.c#L606

[20]

set_link_xdp_fd(): https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.c#L698

[21]

bpf_load.c: https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.c#L38

[22]

添加到这个数组中: https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.c#L111

[23]

load_maps(): https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.c#L212

[24]

添加到这个数组中: https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.c#L242

[25]

rlimit: https://elixir.bootlin.com/linux/v4.15/source/include/uapi/linux/resource.h#L43

[26]

博文: https://blog.csdn.net/liangkwok/article/details/6413158

[27]

map_create(): https://elixir.bootlin.com/linux/v4.15/source/kernel/bpf/syscall.c#L383

[28]

bpf_map_new_fd(): https://elixir.bootlin.com/linux/v4.15/source/kernel/bpf/syscall.c#L327

[29]

BPFtool: https://elixir.bootlin.com/linux/v4.15/source/tools/bpf/bpftool/Documentation/bpftool.rst

[30]

BPFtool 源码: https://elixir.bootlin.com/linux/v4.15/source/tools/bpf/bpftool

[31]

这里: https://docs.cilium.io/en/v1.8/bpf/#bpftool

[32]

这篇文章: https://www.kubernetes.org.cn/7944.html


你可能还喜欢

点击下方图片即可阅读

卧槽!竟然可以直接白嫖 Github Action 的 2C7G 服务器。。

云原生是一种信仰 ????

码关注公众号

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!

点击 "阅读原文" 获取更好的阅读体验!

❤️给个「在看」,是对我最大的支持❤️
  • 11
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值