首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

eBPF 简史(2)

eBPF 简史(2)

清单 2 BPF                Sample
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <……>
// tcpdump -dd 生成出的伪代码块
// instruction format:
// opcode: 16bits; jt: 8bits; jf: 8bits; k: 32bits
static struct sock_filter code[] = {
    { 0x28, 0, 0, 0x0000000c }, // (000) ldh [12]
    { 0x15, 0, 4, 0x000086dd }, // (001) jeq #0x86dd jt 2 jf 6
    { 0x30, 0, 0, 0x00000014 }, // (002) ldb [20]
    { 0x15, 0, 11, 0x00000006 }, // (003) jeq #0x6 jt 4 jf 15
    { 0x28, 0, 0, 0x00000038 }, // (004) ldh [56]
    { 0x15, 8, 9, 0x00000438 }, // (005) jeq #0x438 jt 14 jf 15
    { 0x15, 0, 8, 0x00000800 }, // (006) jeq #0x800 jt 7 jf 15
    { 0x30, 0, 0, 0x00000017 }, // (007) ldb [23]
    { 0x15, 0, 6, 0x00000006 }, // (008) jeq #0x6 jt 9 jf 15
    { 0x28, 0, 0, 0x00000014 }, // (009) ldh [20]
    { 0x45, 4, 0, 0x00001fff }, // (010) jset #0x1fff jt 15 jf 11
    { 0xb1, 0, 0, 0x0000000e }, // (011) ldxb 4*([14]&0xf)
    { 0x48, 0, 0, 0x00000010 }, // (012) ldh [x + 16]
    { 0x15, 0, 1, 0x00000438 }, // (013) jeq #0x438 jt 14 jf 15
    { 0x6, 0, 0, 0x00040000 }, // (014) ret #262144
    { 0x6, 0, 0, 0x00000000 }, // (015) ret #0
};
int main(int argc, char **argv)
{
    // ……
    struct sock_fprog bpf = { sizeof(code)/sizeof(struct sock_filter), code };
    // ……
    // 1. 创建 raw socket
    s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    // ……
    // 2. 将 socket 绑定给指定的 ethernet dev
    name = argv[1]; // ethernet dev 由 arg 1 传入
    memset(&addr, 0, sizeof(addr));
    addr.sll_ifindex = if_nametoindex(name);
    // ……
    if (bind(s, (struct sockaddr *)&addr, sizeof(addr))) {
        // ……
    }
    // 3. 利用 SO_ATTACH_FILTER 将 bpf 代码块传入内核
    if (setsockopt(s, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf))) {
        // ……
    }
    for (; ;) {
        bytes = recv(s, buf, sizeof(buf), 0); // 4. 利用 recv()获取符合条件的报文
        // ……
        ip_header = (struct iphdr *)(buf + sizeof(struct ether_header));
        inet_ntop(AF_INET, &ip_header->saddr, src_addr_str, sizeof(src_addr_str));
        inet_ntop(AF_INET, &ip_header->daddr, dst_addr_str, sizeof(dst_addr_str));
        printf("IPv%d proto=%d src=%s dst=%s\n",
        ip_header->version, ip_header->protocol, src_addr_str, dst_addr_str);
    }
    return 0;
}




篇幅所限,清单 2 中笔者只列出了部分代码,代码分析也以注释为主。有兴趣的读者可以移步阅读完全版。
由于主要是和过滤报文打交道,内核中(before 3.18)的 BPF 的绝大部分实现都被放在了下,篇幅原因笔者就不对代码进行详述了,文件不长,600                    来行(v2.6),比较浅显易懂,有兴趣的读者可以移步品评一下。值得留意的函数有两个,sk_attach_filter()和sk_run_filter():前者将 filter                伪代码由用户空间复制进内核空间;后者则负责在报文到来时执行伪码解析。
演进:JIT For                BPFBPF 被引入 Linux 之后,除了一些小的性能方面的调整意外,很长一段时间都没有什么动静。直到 3.0                才首次迎来了比较大的革新:在一些特定硬件平台上,BPF 开始有了用于提速的 JIT(Just-In-Time) Compiler。
最先实现 JIT 的是平台,其后包括等一众平台纷纷跟进,到今天 Linux 的主流平台中支持 JIT For BPF 的已经占了绝大多数了。
BPF JIT 的接口还是简单清晰的:各平台的 JIT 编译函数都实现于之中(3.16 之后,开始逐步改为如果 CONFIG_BPF_JIT 被打开,则传入的 BPF                伪代码就会被传入该函数加以编译,编译结果被拿来替换掉默认的处理函数 sk_run_filter()。JIT 的实现不在本文讨论之列,其代码基本位于                arch/<platform>/net 之下,有致力于优化的同学可以尝试学习一下。
打开 BPF 的 JIT 很简单,只要向/proc/sys/net/core/bpf_jit_enable 写入 1                即可;对于有调试需求的开发者而言,如果写入 2 的话,还可以在内核 log 中看到载入 BPF 代码时候 JIT                    生成的优化代码,内核开发者们还提供了一个更加方便的工具,可以将内核 log 中的二进制转换为汇编以便阅读。
JIT Compiler 之后,针对 BPF 的小改进不断:如将 BPF 引入 seccomp(3.4);添加一些 debug 工具如 bpf_asm                和 bpf_dbg(3.14)。不过比较革命性的大动作就要等到 3.17 了,这次的改进被称为 extended BPF,即 eBPF。
进化:extended                BPF自 3.15 伊始,一个套源于 BPF 的全新设计开始逐渐进入人们的视野,并最终(3.17)被添置到了 kernel/bpf                下。这一全新设计最终被命名为了 extended BPF(eBPF):顾名思义,有全面扩充既有 BPF 功能之意;而相对应的,为了后向兼容,传统的                BPF 仍被保留了下来,并被重命名为 classical BPF(cBPF)。
相对于 cBPF,eBPF 带来的改变可谓是革命性的:一方面,它已经为内核追踪(Kernel                Tracing)、应用性能调优/监控、流控(Traffic Control)等领域带来了激动人心的变革;另一方面,在接口的设计以及易用性上,eBPF                也有了较大的改进。
Linux 内核代码的 samples 目录下有大量前人贡献的,这里笔者先挑选其中相对简单的 sockex1 来帮助读者们建立一个 eBPF 的初步印象:
清单 3                sockex1_user.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <…>
// 篇幅所限,清单 3 和 4 都只罗列出部分关键代码,有兴趣一窥全貌的读者可以移步 http://elixir.free-electrons.com/linux/v4.12.6/source/samples/bpf深入学习
int main(int ac, char **argv)
{
    // 1. eBPF 的伪代码位于 sockex1_kern.o 中,这是一个由 llvm 生成的 elf 格式文件,指令集为 bpf;
    snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);
    if (load_bpf_file(filename)) {
        // load_bpf_file()定义于 bpf_load.c,利用 libelf 来解析 sockex1_kern.o
        // 并利用 bpf_load_program 将解析出的伪代码 attach 进内核;
    }
    // 2. 因为 sockex1_kern.o 中 bpf 程序的类型为 BPF_PROG_TYPE_SOCKET_FILTER
    // 所以这里需要用用 SO_ATTACH_BPF 来指明程序的 sk_filter 要挂载到哪一个套接字上
    sock = open_raw_sock("lo");
    assert(setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, prog_fd,
    sizeof(prog_fd[0])) == 0);
    //……
    for (i = 0; i < 5; i++) {
        // 3. 利用 map 机制获取经由 lo 发出的 tcp 报文的总长度
        key = IPPROTO_TCP;
        assert(bpf_map_lookup_elem(map_fd[0], &key, &tcp_cnt) == 0);
        // ……
    }
    return 0;
}




清单 4                sockex1_kern.c
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
#include <……>
// 预先定义好的 map 对象
// 这里要注意好其实 map 是需要由用户空间程序调用 bpf_create_map()进行创建的
// 在这里定义的 map 对象,实际上会在 load_bpf_file()解析 ELF 文件的同时被解析和创建出来
// 这里的 SEC(NAME)宏表示在当前 obj 文件中新增一个段(section)
struct bpf_map_def SEC("maps") my_map = {
    .type = BPF_MAP_TYPE_ARRAY,
    .key_size = sizeof(u32),
    .value_size = sizeof(long),
    .max_entries = 256,
};
SEC("socket1")
int bpf_prog1(struct __sk_buff *skb)
{
    // 这个例子比较简单,仅仅是读取输入报文的包头中的协议位而已
    // 这里的 load_byte 实际指向了 llvm 的 built-in 函数 asm(llvm.bpf.load.byte)
    // 用于生成 eBPF 指令 BPF_LD_ABS 和 BPF_LD_IND
    int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
    long *value;
    // ……
    // 根据 key(&index,注意这是一个指向函数的引用)获取对应的 value
    value = bpf_map_lookup_elem(&my_map, &index);
    if (value)
        __sync_fetch_and_add(value, skb->len); //这里的__sync_fetch_and_add 是 llvm 中的内嵌函数,表示 atomic 加操作
    return 0;
}
// 为了满足 GPL 毒药的需求,所有会注入内核的 BPF 代码都须显式的支持 GPL 协议
char _license[] SEC("license") = "GPL";




对比一下清单 3&4 以及清单 2 的代码片段,很容易看出一些 eBPF 显而易见的革新:
  • 用 C 写成的 BPF 代码(sockex1_kern.o);
  • 基于 map 的内核与用户空间的交互方式;
  • 全新的开发接口;
除此之外,还有一些不那么明显的改进隐藏在内核之中:
  • 全新的伪指令集设计;
  • In-kernel verifier;
由一个文件(net/core/filter.c)进化到一个目录(kernel/bpf),eBPF                的蜕变三言两语间很难交代清楚,下面笔者就先基于上述的几点变化来帮助大家入个门,至于个中细节,就只能靠读者以后自己修行了。
返回列表