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程序来实现。
作者 | 王璞