BCC Python开发教程&常用BCC工具(二)

Lesson 4. sync_timing.py

还记得吗,系统管理员层在reboot机器前在终端上连敲了三次sync命令来让第一次sync同步执行完成? 后来有人觉得sync;sync;sync这种把它们放在一行运行的操作简直是666,甚至最终都成为了行业惯例,尽管违背了初衷! 

接下来的这个列子用以记录do_sync被频繁调用的有都快,如果调用间隔小于一秒,则将两次被调用的时间间隔打印出来。这样sync;sync;sync一串命令将会输出第2次和第3次的调用间隔。

 

代码如下:

#!/usr/bin/python
#
# sync_timing.py Trace time between syncs.
# For Linux, uses BCC, eBPF. Embedded C.
#
# Written as a basic example of tracing time between events.
#
# Copyright 2016 Netflix, Inc.
# Licensed under the Apache License, Version 2.0 (the "License")

from __future__ import print_function
from bcc import BPF
from bcc.utils import printb

# 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);
}

// update stored timestamp
ts = bpf_ktime_get_ns();
last.update(&key, &ts);
return 0;
}
""")

b.attach_kprobe(event=b.get_syscall_fnname("sync"), fn_name="do_trace")
print("Tracing for quick sync's... Ctrl-C to end")

# format output
start = 0
while 1:
try:
(task, pid, cpu, flags, ts, ms) = b.trace_fields()
if start == 0:
start = ts
ts = ts - start
printb(b"At time %.2f s: multiple syncs detected, last %s ms ago" % (ts, ms))
except KeyboardInterrupt:
exit()

 这一课我们要学习如下知识:

  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来记录。

int do_trace(struct pt_regs *ctx) { 
	u64 curct=1,ts, *tsp, delta, key = 0,key1 = 1; 
	u64 *ctp; // attempt to read stored timestamp 
	tsp = last.lookup(&key); 
	ctp = count.lookup(&key1); //获取保存的地址,一个key对应一个变量地址 
	if (NULL != ctp){ //如果存在则累加,并更新 curct = *ctp+1; 
		//注意:此处不能使用*curct++,可能是因为内核数据的原因,只能通过count.update更新 
		count.update(&key1, &curct); }else{ count.update(&key1, &curct);//如果不存在直接保存当前值1 
	} if (tsp != NULL) { 
		delta = bpf_ktime_get_ns() - *tsp; 
		if (delta < 1000000000) { // output if time is less than 1 second 
			bpf_trace_printk("count:%d,%d\\n", curct,delta / 1000000); //打印count 
		} last.delete(&key); 
	} // update stored timestamp 
	ts = bpf_ktime_get_ns(); 
	last.update(&key, &ts); 
	return 0; 
} 

"""int do_trace(struct pt_regs *ctx) { u64 curct=1,ts, *tsp, delta, key = 0,key1 = 1; u64 *ctp; // attempt to read stored timestamp tsp = last.lookup(&key); ctp = count.lookup(&key1); //获取保存的地址,一个key对应一个变量地址 if (NULL != ctp){ //如果存在则累加,并更新 curct = *ctp+1; //注意:此处不能使用*curct++,可能是因为内核数据的原因,只能通过count.update更新 count.update(&key1, &curct); }else{ count.update(&key1, &curct);//如果不存在直接保存当前值1 } if (tsp != NULL) { delta = bpf_ktime_get_ns() - *tsp; if (delta < 1000000000) { // output if time is less than 1 second bpf_trace_printk("count:%d,%d\\n", curct,delta / 1000000); //打印count } last.delete(&key); } // update stored timestamp ts = bpf_ktime_get_ns(); last.update(&key, &ts); return 0; } """)

Lesson 6. disksnoop.py

代码如下:

#!/usr/bin/python
#
# disksnoop.py Trace block device I/O: basic version of iosnoop.
# For Linux, uses BCC, eBPF. Embedded C.
#
# Written as a basic example of tracing latency.
#
# Copyright (c) 2015 Brendan Gregg.
# Licensed under the Apache License, Version 2.0 (the "License")
#
# 11-Aug-2015 Brendan Gregg Created this.

from __future__ import print_function
from bcc import BPF
from bcc.utils import printb

REQ_WRITE = 1 # from include/linux/blk_types.h

# load BPF program
b = BPF(text="""
#include <uapi/linux/ptrace.h>
#include <linux/blk-mq.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);
}
}
""")

if BPF.get_kprobe_functions(b'blk_start_request'):
b.attach_kprobe(event="blk_start_request", fn_name="trace_start")
b.attach_kprobe(event="blk_mq_start_request", fn_name="trace_start")
if BPF.get_kprobe_functions(b'__blk_account_io_done'):
b.attach_kprobe(event="__blk_account_io_done", fn_name="trace_completion")
else:
b.attach_kprobe(event="blk_account_io_done", fn_name="trace_completion")

# header
print("%-18s %-2s %-7s %8s" % ("TIME(s)", "T", "BYTES", "LAT(ms)"))

# format output
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
(bytes_s, bflags_s, us_s) = msg.split()

if int(bflags_s, 16) & REQ_WRITE:
type_s = b"W"
elif bytes_s == "0": # see blk_fill_rwbs() for logic
type_s = b"M"
else:
type_s = b"R"
ms = float(int(us_s, 10)) / 1000

printb(b"%-18.9f %-2s %-7s %8.2f" % (ts, type_s, bytes_s, ms))
except KeyboardInterrupt:
exit()

  这节我们要学习如下知识:

  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这些成员字段了,我们不得不自食其力直接取到他们。用例的输出如下:

 

代码如下:

#!/usr/bin/python
#
# This is a Hello World example that uses BPF_PERF_OUTPUT.

from bcc import BPF
from bcc.utils import printb

# 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(&data.comm, sizeof(data.comm));

events.perf_submit(ctx, &data, sizeof(data));

return 0;
}
"""

# load BPF program
b = BPF(text=prog)
b.attach_kprobe(event=b.get_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
printb(b"%-18.9f %-16s %-6d %s" % (time_s, event.comm, event.pid,
b"Hello, perf_output!"))

# loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()

 本节需要学习:

  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进行重构。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值