bccPython开发者教程(译:bccPythonDeveloperTutorial)翻译⾃:github/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md
bcc Python Developer Tutorial
这个教程主要⽬的是展⽰如何使⽤python来进⾏bcc⼯具开发和编程。教程主要分为两个部分:可观察性和⽹络。
⽂中的代码⽚段均都来⾃于bcc:代码⽚段的licenses见bcc中具体⽂件。
也可参考bcc开发者⼿册以及end-users⼯具教程: 。此外bcc还开放有lua接⼝。
Observability
"可观察性"教程包含17个课程和46个要学习列举的事项。
Lesson 1. Hello World
我们通过运⾏这个例⼦来开启我们的学习之旅。在⼀个终端运⾏这个脚本,同时在另外⼀个终端运⾏⼀些命令(例如"ls")。正常的预期是在新任务运⾏时打印"Hello, World!",如果结果不符合预期,说明还有⼀些东西没有准备好:参考。
# ./examples/hello_world.py
bash-13364 [002] d... 24573433.052937: : Hello, World!
bash-13364 [003] d... 24573436.642808: : Hello, World!
[...]
下⾯是hello_world.py:
from bcc import BPF
BPF(text='int kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World!\\n"); return 0; }').trace_print()
在这⾥我们有6样需要学习的东西:
1. text='...':定义⼀个inline BPF 程序. 这个程序使⽤C语⾔编码风格。
2. kprobe__sys_clone():这是通过kprobe进⾏内核函数动态跟踪的快捷⽅法。如果C语⾔函数名称以"kprobe__"作为前缀,则函数名其
余部分则表⽰将要被跟踪的内核函数接⼝(名),在我们这⾥的场景中就是跟踪内核函数sys_clone().
3. void *ctx: ctx本来是具体类型的参数,但是由于我们这⾥没有使⽤这个参数,因此就将其写成void *类型。
4. bpf_trace_printk():⼀种将信息输出到trace_pipe(/sys/kernel/debug/tracing/trace_pipe)简单机制。在⼀些简单⽤例中这样使⽤没有问
题, but它也有⼀些限制:最多3 参数;第⼀个参数必须是%s(即字符串);同时trace_pipe在内核中全局共享,so 其他并⾏使⽤trace_pipe的程序有可能会将trace_pipe的输出扰乱。⼀个更好的⽅式是通过BPF_PERF_OUTPUT(), 稍后将会讲到。
5. return 0;:必须这样,返回0 (如果要知道why, 参考 #139 github/iovisor/bcc/issues/139)。
6. .trace_print(): bcc提供的⼀个功能⽤以读取trace_pipe的内容输出到终端。
Lesson 2. sys_sync()
这⼀课我们要写⼀个跟踪sys_sync()内核函数的程序。这个程序会在sys_sync()函数被调⽤时在终端打
印"sys_sync() called" 。程序写好运⾏起来,并在另外⼀个终端运⾏sync命令来进⾏测试。Lesson 1中的hello_world.py 程序基本上不⽤怎么修改就够⽤。
不过,在Lesson 1的基础上再增加⼀条:在跟踪程序运⾏时会在第⼀条打印输出"Tracing sys_sync()... Ctrl-C to end." 。提⽰:it's just Python。
Lesson 3. hello_fields.py
这个程序在. ⽰例输出如下 (命令运⾏在另外⼀个终端):
# ./examples/tracing/hello_fields.py
TIME(s) COMM PID MESSAGE
24585001.174885999 sshd 1432 Hello, World!
24585001.195710000 sshd 15780 Hello, World!
under开头的单词24585001.991976000 systemd-udevd 484 Hello, World!
24585002.276147000 bash 15787 Hello, World!
代码如下:
from bcc import BPF
# define BPF program
prog = """
int hello(void *ctx) {
bpf_trace_printk("Hello, World!\\n");
return 0;
}
"""
# load BPF program
b = BPF(text=prog)
b.attach_kprobe(_syscall_fnname("clone"), fn_name="hello")
# header
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "MESSAGE"))
# format output
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
continue
print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))
这个程序与hello_world.py相似也是通过sys_clone()来跟踪新任务,但是添加了⼀些新的学习事项:
1. prog =: 这⼀次我们将C程序定义为了变量,后续通过引⽤这个变量的⽅式来使⽤。如果你想根据命令⾏参数来进⾏⼀些字符串替换,
这种⽅式就很有⽤。
2. hello(): 我们定义了⼀个C语⾔函数⽽⾮kprobe__ 快捷⽅式,稍后我们将会引⽤这个函数。所有声明在BPF程序中的C函数在跟踪函数
的kprobe会被执⾏,因⽽这⾥的C函数需要⼀个pt_reg* ctx类型的⾸参。如果你想定义⼀些helper函数,但是⼜不希望这些函数在probe时就执⾏,那么需要将这些helper函数定义为static inline 这样编译器可以将其编译为inlined属性;有时候也许你需要使⽤_always_inline 函数属性来实现这⼀效果。
3. b.attach_kprobe(_syscall_fnname("clone"), fn_name="hello"):为内核的clone系统调⽤函数添加⼀个kprobe点,这样实
施后在clone()函数的kprobe会执⾏我们定义的hello() 函数。也可以多次调⽤attach_kprobe() 函数在需要跟踪的内核函数的kprobe点插⼊你定义的kprobe跟踪函数。
4. b.trace_fields():从trace_pipe返回⼀组固定字段。类似于trace_print()这个函数⼀般只在调试时使⽤,如果在正式发布的⼯具中应该使
⽤BPF_PERF_OUTPUT()来代替。
Lesson 4. sync_timing.py
还记得吗,系统管理员层在reboot机器前在终端上连敲了三次sync命令来让第⼀次sync同步执⾏完成? 后来有⼈觉得sync;sync;sync这种把它们放在⼀⾏运⾏的操作简直是666,甚⾄最终都成为了⾏业惯例,尽管违背了初衷!
接下来的这个列⼦⽤以记录do_sync被频繁调⽤的有都快,如果调⽤间隔⼩于⼀秒,则将两次被调⽤的时间间隔打印出来。这样
sync;sync;sync⼀串命令将会输出第2次和第3次的调⽤间隔。
# ./examples/tracing/sync_timing.py
Tracing for quick Ctrl-C to end
At time0.00 s: multiple syncs detected, last95 ms ago
At time0.10 s: multiple syncs detected, last96 ms ago
这个程序在:
from__future__import print_function
from bcc import BPF
# load BPF program
b = BPF(text="""
#include <uapi/linux/ptrace.h>
BPF_HASH(last);
int do_trace(struct pt_regs *ctx) {
u64 ts, *tsp, delta, key = 0;
// attempt to read stored timestamp
tsp = last.lookup(&key);
if (tsp != NULL) {
delta = bpf_ktime_get_ns() - *tsp;
if (delta < 1000000000) {
// output if time is less than 1 second
bpf_trace_printk("%d\\n", delta / 1000000);
}
last.delete(&key);
linux内核开发招聘}
// update stored timestamp
ts = bpf_ktime_get_ns();
last.update(&key, &ts);
return 0;
}
""")
b.attach_kprobe(_syscall_fnname("sync"), fn_name="do_trace")
print("Tracing for quick Ctrl-C to end")
# format output
start = 0
while 1:
(task, pid, cpu, flags, ts, ms) = b.trace_fields()
if start == 0:
start = ts
ts = ts - start
print("At time %.2f s: multiple syncs detected, last %s ms ago" % (ts, ms))
这⼀课我们要学习如下知识:
1. bpf_ktime_get_ns(): 返回当前时间戳,以纳秒为单位。
2. BPF_HASH(last): 创建⼀个名字为"last"的BPF map对象,其本质上是⼀个hash表。我们没有指定任何参数,因⽽这⾥对map中的key
和value都默认为u64类型。
3. key = 0: 在这个hash map中我们仅存放⼀对key/value,且key硬编码为0。
4. last.lookup(&key): 在hash中通过key查元素,如果查到则返回key对应的value指针,否则返回NULL。这⾥⼊参传递的是key地
址。
5. if (tsp != NULL) {: 内核中的verifier 要求在引⽤⼀个返回⾃map lookup的value指针前必须进⾏NULL
指针检查。
6. last.delete(&key): 从hash中删除key。由于⽼版本kenrel存在bug因⽽要求在.update()后需要这样做,不过这个bug已经在4.8.10后已经
fixed。
7. last.update(&key, &ts): 在hash map中将ts与key进⾏关联,这会覆盖之前的键值对中key对应的value,这⾥是记录时间戳。Lesson 5. sync_count.py
对上⼀节的sync_timing.py 程序进⾏修改,把内核中sync系统调⽤的次数(both fast and slow)记录下来,并在打印中输出。这个调⽤次数count可在已有的hash map中新增⼀个key来记录。
Lesson 6. disksnoop.py
看看程序⼀些新鲜的玩法吧,下⾯是这个程序的输出:
# ./disksnoop.py
TIME(s) T BYTES LAT(ms)
16458043.436012 W 4096 3.13
16458043.437326 W 4096 4.44
16458044.126545 R 409642.82
16458044.129872 R 4096 3.24
[...]
部分代码⽚段如下:
[...]
REQ_WRITE = 1 # from include/linux/blk_types.h
# load BPF program
b = BPF(text="""
#include <uapi/linux/ptrace.h>
#include <linux/blkdev.h>
BPF_HASH(start, struct request *);
void trace_start(struct pt_regs *ctx, struct request *req) {
// stash start timestamp by request ptr
u64 ts = bpf_ktime_get_ns();
start.update(&req, &ts);
}
void trace_completion(struct pt_regs *ctx, struct request *req) {
u64 *tsp, delta;
tsp = start.lookup(&req);
if (tsp != 0) {
delta = bpf_ktime_get_ns() - *tsp;
bpf_trace_printk("%d %x %d\\n", req->__data_len,
req->cmd_flags, delta / 1000);
start.delete(&req);
}
}
""")
b.attach_kprobe(event="blk_start_request", fn_name="trace_start")
b.attach_kprobe(event="blk_mq_start_request", fn_name="trace_start")
b.attach_kprobe(event="blk_account_io_done", fn_name="trace_completion")
[...]
这节我们要学习如下知识:
1. REQ_WRITE: 我们在这个python程序中定义了⼀个内核中已有的常量,因为稍后我们将会使⽤到它。如果我们在BPF中使⽤直接
REQ_WRITE即使不定义也应该不会有问题,但前提是要使⽤正确的头⽂件#includes。
2. trace_start(struct pt_regs *ctx, struct request *req): 这个函数稍后会被attached到kprobes中。Kprobe函数中的⾸参是struct pt_regs
*ctx,这个参数⽤以提供BPF现场和上下⽂寄存器;第⼆个是被kprobe跟踪的内核函数的实际参数。我们将trace_start()函数attach到blk_start_request()内核函数,⽽这个内核函数的第⼀个参数就是struct request *类型。
3. start.update(&req, &ts): 这⾥我们使⽤request 结构指针作为我们hash map的键值key。 What? ⽤指针做key?哈哈,这个在traceing不
⾜为奇。结构体指针在hash map中被证明是很理想的键值,因为他们是独⼀⽆⼆的:两个结构体(对象)不可能有相同的指针(地址)。
(但是要注意内存被释放后指针被重复使⽤的情况)。因此这⾥我们将时间戳timestamp与描述磁盘IO的结构体request struct(指针)进⾏key/value配对使⽤,这样我们就可以对其进⾏计时。通常有两种键值可⽤来与时间戳配对存放:结构体指针和线程IDs (for timing function entry to return).
4. req->__data_len:这⾥引⽤struct request的成员。详情请翻阅内核源码中这个结构的定义以及它有哪些成员。bcc⼯具实际上将这些
表达式重写为了⼀系列bpf_probe_read_kernel() 调⽤。有时候bcc⽆法处理⼀些复杂的引⽤,此时需要直接调⽤
bpf_probe_read_kernel()。
这个程序⾮常有意思,如果你能够理解这⾥所有的代码,你就会解许多重要的基础知识。⽬前我们仍然使⽤的是bpf_trace_printk()函数,让我们接下来继续改进它吧 !
Lesson 7. hello_perf_output.py
好了,接下来我们不再⽤前⾯的bpf_trace_printk(),⽽是使⽤BPF_PERF_OUTPUT() 接⼝这才是正确的打开⽅式。这也意味着我们⽆法再欢快⽽⾃由的通过 trace_field()获取到PID和timestamp这些成员字段了,我们不得不⾃⾷其⼒直接取到他们。⽤例的输出如下:
# ./hello_perf_output.py
TIME(s) COMM PID MESSAGE
0.000000000 bash 22986 Hello, perf_output!
0.021080275 systemd-udevd 484 Hello, perf_output!
0.021359520 systemd-udevd 484 Hello, perf_output!
0.021590610 systemd-udevd 484 Hello, perf_output!
[...]
代码取⾃:
from bcc import BPF
# define BPF program
prog = """
#include <linux/sched.h>
// define output data structure in C
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_PERF_OUTPUT(events);
int hello(struct pt_regs *ctx) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&datam, sizeof(datam));
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
"""
# load BPF program
windowsperlb = BPF(text=prog)
b.attach_kprobe(_syscall_fnname("clone"), fn_name="hello")
# header
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "MESSAGE"))
# process event
start = 0
def print_event(cpu, data, size):
global start
event = b["events"].event(data)
if start == 0:
start = event.ts
time_s = (float(event.ts - start)) / 1000000000
print("%-18.9f %-16s %-6d %s" % (time_s, eventm, event.pid,
"Hello, perf_output!"))
# loop with callback to print_event
js可以写大型游戏吗b["events"].open_perf_buffer(print_event)
while 1:
b.perf_buffer_poll()
本节需要学习:
1. struct data_t:这是⼀个我们⾃⼰定义的C语⾔结构体,⽤于内核向⽤户态传递数据。
2. BPF_PERF_OUTPUT(events):将我们的输出通道命名为"events"。
3. struct data_t data = {};:创建⼀个空的data_t struct结构体对象,其成员在后续填充。
4. bpf_get_current_pid_tgid():返回值的低32位保存当前任务的线程(在Linux中内核视⾓的PID实际上是⽤户态的线程ID),⾼32位保存
线程组ID(也就是⽤户态视⾓的进程PID)。如果⽤u32类型对这个值进⾏强转,⾼32位将会被截断discard。我们应该⽤ PID 还是 TGID 呢? 对于多线程应⽤来说线程组中的TGID都是相同的,因此如果你想要区分的是不同的线程,那么就使⽤PID。究竟使⽤PID还是TGID实际上,这是⼀个与⽤户期望有关的问题。
5. bpf_get_current_comm():将当前任务的名字(字符串)放到第⼀个⼊参中指针所指向的内存中。
6. events.perf_submit():提交event以便⽤户态通过perf ring buffer读取perf数据。
7. def print_event():⾃定义的Python函数⽤以处理event stream读取的events信息。
8. b["events"].event(data):以⼀个python对象的⽅式返回events信息,这个python对象是从前⾯C语⾔声明中⾃动⽣成的。
9. b["events"].open_perf_buffer(print_event):将Python函数print_event 与events stream关联起来。
10. while 1: b.perf_buffer_poll():polling等待perf 事件。
Lesson 8. sync_perf_output.py
使⽤BPF_PERF_OUTPUT对上⼀节的sync_timing.py进⾏重构。
Lesson 9. bitehist.py
下⾯这个⼯具以直⽅图⽅式记录disk I/O⼤⼩,⽰例输出如下:
# ./bitehist.py
< Hit Ctrl-C to end.
^C
kbytes : count distribution
0 -> 1 : 3 | |
2 ->
3 : 0 | |
4 -> 7 : 211 |********** |
8 -> 15 : 0 | |
16 -> 31 : 0 | |
32 -> 63 : 0 | |
64 -> 127 : 1 | |
128 -> 255 : 800 |**************************************|
下⾯在来⾃:
from__future__import print_function
from bcc import BPFpython基础代码大全加翻译
from time import sleep
# load BPF program
b = BPF(text="""
#include <uapi/linux/ptrace.h>
#include <linux/blkdev.h>
BPF_HISTOGRAM(dist);
int kprobe__blk_account_io_done(struct pt_regs *ctx, struct request *req)
{
dist.increment(bpf_log2l(req->__data_len / 1024));
return 0;
}
""")
# header
print(" Hit Ctrl-C to end.")
# trace until Ctrl-C
try:
sleep(99999999)
except KeyboardInterrupt:
print()
# output
b["dist"].print_log2_hist("kbytes")
让我们回顾⼀下前⾯的课程:
kprobe__:以这个为前缀开表达式中后⾯的字符串表⽰要安装kprobe钩⼦的内核函数。
struct pt_regs *ctx, struct request *req:kprobe钩⼦函数的参数。参数ctx存着寄存器和BPF的上下⽂;参数req是被跟踪内核函数(这⾥是blk_account_io_done())第⼀个参数。
req->__data_len:对参数成员进⾏引⽤。google翻译翻译
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论