BPF是最近Linux内核畛域热门的技术。传统的BPF指的是tcpdump命令用于过滤网络包的工具,当初BPF曾经失去极大的扩大,不再是Berkeley Packet Filter的缩写对应的简略的网络包过滤工具。 从Kernel 4.9之后,BPF曾经成为一个欠缺的内核扩大工具,BPF在内核里运行一个sandbox,用于执行BPF的字节码(bytecode), 在执行BPF程序前,BPF的查看器会对BPF程序的字节码进行安全检查(比方,指针要先判断不为空后再拜访,代码里不能有循环,等等),以保障BPF程序不会导致系统解体,因为BPF程序执行时是在内核态。 因而,BPF能够很平安地在内核态执行用户编写的程序,而且有平安保障,这比编写内核模块平安太多了。 正是因为BPF能保障平安,并运行在内核态,能够大大简化很多以前很简单的事件,目前BPF曾经利用于性能剖析、网络、平安、驱动、区块链等等畛域。

曾经有很多文章介绍BPF在内核性能剖析(Kernel tracing)方面的利用。内核有各种定义的tracepoint用于动态tracing,也能够采纳动静tracing来跟踪内核函数调用。 用BPF进行内核tracing,开销小而且性能好,因为BPF程序是运行在内核态,不须要把采集到的数据再传回用户态解决,而是间接在内核态实现数据采集和解决,而后把处理结果传回用户态用于展现。 对于BPF用于内核tracing方面不再赘述,本文专一于利用BPF进行用户态利用性能剖析(Userspace tracing)方面。

用户态tracing

在进行用户态tracing前,要在程序里定义tracepoints。这里次要介绍Userland Statically Defined Tracepoints(USDT)。

因为USDT依赖systemtap-sdt-dev包,先要装置依赖包,以Ubuntu为例,运行sudo apt install systemtap-sdt-dev进行装置。 上面的示例test-server.c给出如何应用宏DTRACE_PROBE1在用户程序里定义tracepoint:

#include <sys/sdt.h>#include <unistd.h>int main(int argc, char **argv){    int idx = 0;    while(1) {        idx++;        // 自定义的tracepoint        DTRACE_PROBE1(test_grp, test_idx, idx);        sleep(1);    }    return 0;}

下面的例子,用宏DTRACE_PROBE1定义了一个组名为test_grp、名称为test_idx的用户态tracepoint。该tracepoint只有一个参数,该参数是一个递增的整数变量。 如果要定义有两个或更多参数的tracepoint,要用DTRACE_PROBE2、DTRACE_PROBE3,以此类推。如果tracepoint不带参数,则用DTRACE_PROBE来定义。 用gcc命令编译下面的程序gcc -g -fno-omit-frame-pointer -O0 test-server.c -o test-server,失去可执行的二进制程序test-server。

USDT的原理

用宏DTRACE_PROBE1定义的tracepoint在编译实现的二进制里对应CPU指令nop操作,能够用gdb看下二进制文件test-server对应的汇编:

$ gdb test-server......Reading symbols from test-server...(gdb) disas main <<== gdb命令用于查看二进制文件的汇编Dump of assembler code for function main:   0x0000000000001149 <+0>: endbr64   0x000000000000114d <+4>: push   %rbp   0x000000000000114e <+5>: mov    %rsp,%rbp   0x0000000000001151 <+8>: sub    $0x20,%rsp   0x0000000000001155 <+12>: mov    %edi,-0x14(%rbp)   0x0000000000001158 <+15>: mov    %rsi,-0x20(%rbp)   0x000000000000115c <+19>: movl   $0x0,-0x4(%rbp)   0x0000000000001163 <+26>: addl   $0x1,-0x4(%rbp)   0x0000000000001167 <+30>: nop <<= tracepoint对应的nop指令   0x0000000000001168 <+31>: mov    $0x1,%edi   0x000000000000116d <+36>: callq  0x1050 <sleep@plt>   0x0000000000001172 <+41>: jmp    0x1163 <main+26>End of assembler dump.

从下面的汇编能够看出,nop操作只是放空一个CPU cycle,这个代价非常低,所以程序里定义USDT对性能的影响能够疏忽。

在运行时,如果用BPF对test-server进行tracing(具体tracing的细节下文会讲),再用sudo gdb查看运行时的程序对应的汇编,会发现nop操作被替换成int3指令了:

$ sudo gdb -p $(pidof test-server)......(gdb) disas mainDump of assembler code for function main:   0x00005583ee599149 <+0>: endbr64   0x00005583ee59914d <+4>: push   %rbp   0x00005583ee59914e <+5>: mov    %rsp,%rbp   0x00005583ee599151 <+8>: sub    $0x20,%rsp   0x00005583ee599155 <+12>: mov    %edi,-0x14(%rbp)   0x00005583ee599158 <+15>: mov    %rsi,-0x20(%rbp)   0x00005583ee59915c <+19>: movl   $0x0,-0x4(%rbp)   0x00005583ee599163 <+26>: addl   $0x1,-0x4(%rbp)   0x00005583ee599167 <+30>: int3 <<== tracepoint对应的int3指令   0x00005583ee599168 <+31>: mov    $0x1,%edi   0x00005583ee59916d <+36>: callq  0x5583ee599050 <sleep@plt>   0x00005583ee599172 <+41>: jmp    0x5583ee599163 <main+26>End of assembler dump.

从下面gdb展现的运行时test-server的汇编,发现nop指令被替换为int3指令,该指令是设置断点,用于转向执行BPF等tracing工具的指令,诸如采集tracepoint数据等等。 所以USDT是从指令层面进行tracing,对程序运行时性能影响很小。

用bpftrace命令进行tracing

这里先介绍bpftrace命令,这个命令不须要写BPF程序,只用写脚本的形式来实现tracing。在Ubuntu上运行sudo apt-get install -y bpftrace来装置bpftrace。 运行如下bpftrace命令,必须用sudo权限:

$ sudo bpftrace \    -e 'usdt:/home/pwang/usdt_test/test-server:test_grp:test_idx \        { printf("%d\n", arg0); }' \    -p $(pidof test-server)Attaching 1 probe...136513661367136813691370^C

上述bpftrace命令采集到test-server的名为test_idx的tracepoint,并打印出该tracepoint的第一个参数的值。 该bpftrace命令蕴含如下几个组成部分:

-e的含意是运行前面跟着的脚本,该脚本蕴含两局部:

1、第一局部usdt:/home/pwang/usdt_test/test-server:test_grp:test_idx是用冒号分隔的四段:

  • usdt是tracing类型;
  • /home/pwang/usdt_test/test-server是被trace的程序的绝对路径;
  • test_grp是tracepoint的组名,也能够省略不写;
  • test_idx是tracepoint的名称。

2、第二局部{ printf("%d\n", arg0); }是打印该tracepoint的第一个参数的值;

-p是指定被trace的过程ID。

用BCC工具编写BPF程序进行tracing

BCC是用于编写BPF程序的开发框架和编译器。目前BPF程序次要用C语言来写,然而不反对C的全副语法,比方不反对循环。 BCC调用LLVM把BPF的C代码转成BPF的字节码,而后通过BPF查看器的测验,测验通过的BPF字节码胜利加载到sandbox里运行。 BCC还反对用Python代码来跟运行在sandbox里的BPF程序进行交互,比方拿到BPF的tracing后果并展现。 在Ubuntu上运行sudo apt-get install -y bpfcc-tools来装置BCC。

上面是一段BCC的Python脚本,内嵌了BPF的C代码:

#! /usr/bin/python3from bcc import BPF, USDTimport sys// 内嵌的BPF的C代码bpf_src = """int trace_udst(struct pt_regs *ctx) {    u32 idx;    bpf_usdt_readarg(1, ctx, &idx);    bpf_trace_printk("test_idx=%d tracepoint cachted\\n", idx);    return 0;};"""// 指定USDT的tracepoint,并关联BPF程序里的函数procid = int(sys.argv[1])u = USDT(pid=procid)u.enable_probe(probe="test_idx", fn_name="trace_udst")// 加载BPF程序并打印输出后果b = BPF(text=bpf_src, usdt_contexts=[u])print("Start USDT tracing")b.trace_print()

下面的BCC代码分成几个局部:

1、第一局部是内嵌的BPF的C代码,用字符串的形式定义给bpf_src变量。bpf_src里的C代码定义了trace_udst函数,该函数的输出参数是抓取到的tracepoint上下文,该函数做两件事:

  • bpf_usdt_readarg(1, ctx, &idx);用于从tracepoint上下文中读取tracepoint的第一个参数的值;
  • bpf_trace_printk("test_idx=%d tracepoint cachted\n", idx);打印输出后果,打印的后果会发送到一个管道,管道的门路是/sys/kernel/debug/tracing/trace_pipe;

2、第二局部是指定USDT的tracepoint,并关联BPF程序以实现tracing:

  • u = USDT(pid=procid)生成USDT对象;
  • u.enable_probe(probe="test_idx", fn_name="trace_udst")指定tracepoint为text_idx,并指定BPF程序中的trace_udst函数为trace到该point的执行操作;

3、第三局部是加载BPF程序并从管道中取出输入后果并打印到STDOUT:

  • b = BPF(text=bpf_src, usdt_contexts=[u])生成BPF对象,并关联USDT对象,加载BPF程序到sandbox进行测验和运行;
  • b.trace_print()打印每次BPF程序trace到text_idx的输入后果。

上面是该BCC代码的执行后果,留神该脚本用python3来运行:

$ sudo ./ebpf_hello_usdt.py $(pidof test-server)Start USDT tracingb'     test-server-27125 [000] .... 26587.633948: 0: test_idx=2958 tracepoint cachted'b'     test-server-27125 [000] .... 26588.634090: 0: test_idx=2959 tracepoint cachted'b'     test-server-27125 [000] .... 26589.634237: 0: test_idx=2960 tracepoint cachted'b'     test-server-27125 [000] .... 26590.634386: 0: test_idx=2961 tracepoint cachted'b'     test-server-27125 [000] .... 26591.634563: 0: test_idx=2962 tracepoint cachted'b'     test-server-27125 [000] .... 26592.634714: 0: test_idx=2963 tracepoint cachted'b'     test-server-27125 [000] .... 26593.634870: 0: test_idx=2964 tracepoint cachted'

能够看到BPF程序胜利trace到递增的text_idx的值。

BPF是十分弱小的内核扩大工具,有了BPF能够做很多以前很简单或做不到的事件。此外,有了BCC,编写BPF程序的复杂度大大降低,用BCC能够写出十分弱小的内核扩大利用,而且安全性和性能有保障。 业内人士普遍认为,有了BPF之后,Linux的内核会越来越成为微内核,很多在内核里或用内核扩大来做的事件能够让用户自行编写BPF程序来实现。

作者 | 王璞