# Appendix C
這篇主要紀錄在 python 使用 BCC 的使用方法。在 [BPF Performance Tools: Linux System and Application Observability](http://www.brendangregg.com/bpf-performance-tools-book.html) 一書中,放在 Appendix C。而主要參考的資料是 BCC repo 中的[文件](https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md)與[教學](https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md)。使用 BCC 搭配 python 的好處是比 `bpftrace` 有彈性很多,比如 BPF 紀錄的資料可以直接用 matplotlib 之類的函式庫繪圖,或是搭配 python 內建的 [subprocess](https://docs.python.org/2/library/subprocess.html) 模組自動化整個測試等等。
# Outline
如果是在 python 搭配 BCC 使用,那麼整個程式大致上會有兩個部份:
1. 要注入 BPF 執行的物件(C 語言寫成)
2. 接收回傳的資料進行, 並進行可能的後處理(python 寫成)
所以如果去 bcc repo 的教學,大多數的程式架構會看起來會像下面這樣:
```python=
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 != 0) {
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")
# 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))
```
一開始建立 `BPF` 物件中,給的那個 `text` 字串就是要注入 BPF 的物件。而在`attach_*` 函數去把 probe 跟相應的 handler 進行綁定,並且做資料的後處理。
## C
這一部份的程式,是要編成 BPF byte code 的部份。而這些東西畢竟是要在一個放在核心的虛擬機中執行,所以自然會有一些限制。比如:
1. 不能直接呼叫 kernel function,只能使用 BPF 提供的輔助函數。這些函數的格式都是 `bpf_<function_name>` ,比如說 `bpf_ktime_get_ns()`, `bpf_trace_printk()` 等等。更多 BPF 提供的輔助函數可以去[文件](https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#data)找。
2. 資料必須儲存在 BPF 定義的資料結構 :不能擅自使用如整數或陣列這種資料型態,而是要某些 BPF 專用的資料結構。這些 BPF 專屬的資料型態在文件中統稱 map (這在 `bpftrace` 的 script 對應到大概是 `@[]` 的語法)。最常見的大概是 `BPF_HASH`,而更多資料型態可以去文件中的 [map](https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#maps) 可以查到資訊。
3. 若想要讀取核心相關的的資料結構,要先用 [`bpf_probe_read()`](https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#1-bpf_probe_read) 函數搬到 BPF Stack 中。
4. 不能有迴圈
5. 更多限制可以看[這裡](https://stackoverflow.com/questions/57688344/what-is-not-allowed-in-restricted-c-for-ebpf)的整理。
## python
主要使用的是 `bcc` 中的 `BPF` 這個 class。最重要的動作是把 event source 及其觸發時所對應到的動作綁定。而綁定的方法又有兩種:一種是用 `attach_*` 名稱的函數:
```python=
from bcc import BPF
prog="""
int probe_handler(void *arg)
{
/* ... */
}
int ret_handler(void *arg)
{
/* ... */
}
"""
b = BPF(text=prog);
b.attach_kprobe(event='fibdrv_read', fn_name="probe_handler")
b.attach_kretprobe(event='fibdrv_read', fn_name="ret_handler")
```
另外一種是明確地用如 `kprobe__`, `kretprobe__` + 想追蹤的函數名稱:
```python=
from bcc import BPF
prog="""
int kprob__fib_read(void *arg)
{
/* ... */
}
int kretprobe__fib_read(void *arg)
{
/* ... */
}
"""
b = BPF(text=prog);
```
下面舉例簡單說明。
# 綁定 kprobe
以下示範幾個使用的例子。以 [fibdrv](https://github.com/sysprog21/fibdrv) 為例,追蹤 [fibdrv](https://github.com/sysprog21/fibdrv) 中,`fib_read` 這個函數。這邊假定模組已經先 `insmod` 好了:
```shell=
$ sudo insmod fibdrv.ko
```
> 因為在試驗的時候是使用 python 3 (BCC 手動編譯預設使用 python 2),所以終端機的指令或輸出部份能會與 python 2 不一致。
## 使用 attach*
### kprobe
舉例來說,想要紀錄每一次進入 `fib_read` 時的時間:
```python=
from bcc import BPF
prog = """
#include <uapi/linux/ptrace.h>
int probe_handler(struct pt_regs *ctx)
{
u64 ts = bpf_ktime_get_ns();
bpf_trace_printk("Enter fib_read at %llu\\n", ts);
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event="fib_read", fn_name="probe_handler")
b.trace_print()
```
使用 python 執行這個程式,那麼接下來只要有程式呼叫到 `fib_read` 時(比如說另外執行 `client` 程式的時候),就會出現以下輸出:
```shell=
$ sudo python3 fib_hello.py
b' client-5610 [001] .... 3345.315537: 0: Enter fib_read at 3345172238601'
b' client-5610 [001] .... 3345.315630: 0: Enter fib_read at 3345172333863'
b' client-5610 [001] .... 3345.315637: 0: Enter fib_read at 3345172341982'
b' client-5610 [001] .... 3345.315642: 0: Enter fib_read at 3345172346697'
b' client-5610 [001] .... 3345.315647: 0: Enter fib_read at 3345172351477'
b' client-5610 [001] .... 3345.315652: 0: Enter fib_read at 3345172356987'
b' client-5610 [001] .... 3345.315659: 0: Enter fib_read at 3345172363958'
b' client-5610 [001] .... 3345.315688: 0: Enter fib_read at 3345172392777'
b' client-5610 [001] .... 3345.315696: 0: Enter fib_read at 3345172400831'
b' client-5610 [001] .... 3345.315702: 0: Enter fib_read at 3345172406864'
[...]
```
> BCC 文件中並不推薦使用 `bpf_trace_printk` 作為輸出,而是應`BPF_PERF_OUTOUT` 以及 `perf_submit` 作為輸出。不過為了簡便,這邊先用 `bpf_trace_printk` 輸出。詳細可參考文件的 [Output](https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#output) 一節。
### kretprobe
接下來自然而然的想到能不能用 `kretprobe`,答案是可以。只要換成 `attach_kretprobe` 就可以了:
```python=
from bcc import BPF
prog = """
#include <uapi/linux/ptrace.h>
#define TASK_COMM_LEN 64
int probe_handler(struct pt_regs *ctx)
{
char comm[TASK_COMM_LEN];
u64 ts = bpf_ktime_get_ns();
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("in: %s %llu\\n", comm, ts);
return 0;
}
int ret_handler(struct pt_regs *ctx)
{
char comm[TASK_COMM_LEN];
u64 ts = bpf_ktime_get_ns();
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("out:%s %llu\\n", comm, ts);
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event="fib_read", fn_name="probe_handler")
b.attach_kretprobe(event="fib_read", fn_name="ret_handler")
b.trace_print()
```
用 python 執行之後再執行 `client`,就會發現:
```shll=
$ sudo python fibdrv-read-time.py
client-12158 [004] .... 7532.465565: 0: in: client 7532134752662
client-12158 [004] d... 7532.465576: 0: out:client 7532134765688
client-12158 [004] .... 7532.465637: 0: in: client 7532134826066
client-12158 [004] d... 7532.465639: 0: out:client 7532134828550
client-12158 [004] .... 7532.465643: 0: in: client 7532134832734
client-12158 [004] d... 7532.465644: 0: out:client 7532134833583
client-12158 [004] .... 7532.465647: 0: in: client 7532134837273
client-12158 [004] d... 7532.465648: 0: out:client 7532134838101
[...]
```
既然進入時間跟結束時間都可以紀錄,那麼就可以由兩者之間的差計算待在這個函數裡面的時間。仿照 bcc/tools 中的 [vfsreadlat](https://github.com/iovisor/bcc/blob/master/examples/tracing/vfsreadlat.c) 部份,寫一個檢測時間差的程式:
```python=
from bcc import BPF
prog = """
#include <uapi/linux/ptrace.h>
#define TASK_COMM_LEN 64
BPF_HASH(start, u64, u64);
int probe_handler(struct pt_regs *ctx)
{
u64 ts = bpf_ktime_get_ns();
u64 pid = bpf_get_current_pid_tgid();
start.update(&pid, &ts);
return 0;
}
int ret_handler(struct pt_regs *ctx)
{
u64 ts = bpf_ktime_get_ns();
u64 pid = bpf_get_current_pid_tgid();
u64 *tsp = (start.lookup(&pid));
if (tsp != 0) {
bpf_trace_printk("duration: %llu\\n", ts - *tsp);
start.delete(&pid);
}
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event="fib_read", fn_name="probe_handler")
b.attach_kretprobe(event="fib_read", fn_name="ret_handler")
b.trace_print()
```
並且執行:
```=
$ sudo python fibdrv-read-time.py
client-13603 [007] d... 10238.629451: 0: duration: 1855
client-13603 [007] d... 10238.629518: 0: duration: 450
client-13603 [007] d... 10238.629523: 0: duration: 227
client-13603 [007] d... 10238.629525: 0: duration: 215
client-13603 [007] d... 10238.629528: 0: duration: 195
client-13603 [007] d... 10238.629531: 0: duration: 196
client-13603 [007] d... 10238.629533: 0: duration: 198
client-13603 [007] d... 10238.629536: 0: duration: 200
client-13603 [007] d... 10238.629540: 0: duration: 287
client-13603 [007] d... 10238.629550: 0: duration: 586
client-13603 [007] d... 10238.629556: 0: duration: 297
client-13603 [007] d... 10238.629561: 0: duration: 340
client-13603 [007] d... 10238.629566: 0: duration: 338
client-13603 [007] d... 10238.629571: 0: duration: 359
[...]
```
更進一步,可以仿照 [disksnoop](https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md#lesson-6-disksnooppy)的例子,把這個函數吃的參數拉出來。並且搭配 `BPF.trace_fields()` ,只取出 `bpf_trace_printk()` 輸出的東西 (而不是像 `BPF.trace_print()` 那樣會把名稱, pid 等等資訊通通列出來):
```python=
from bcc import BPF
prog = """
#include <uapi/linux/ptrace.h>
#include <linux/fs.h>
BPF_HASH(start, u64, u64);
BPF_HASH(args, u64, loff_t);
int probe_handler(struct pt_regs *ctx,
struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
u64 ts = bpf_ktime_get_ns();
u64 pid = bpf_get_current_pid_tgid();
loff_t arg = *offset;
start.update(&pid, &ts);
args.update(&pid, &arg);
return 0;
}
int ret_handler(struct pt_regs *ctx)
{
u64 ts = bpf_ktime_get_ns();
u64 pid = bpf_get_current_pid_tgid();
u64 *tsp = start.lookup(&pid);
loff_t *offset = args.lookup(&pid);
if (tsp != 0 && offset != 0) {
bpf_trace_printk("%lld, %llu\\n", *offset, ts - *tsp);
start.delete(&pid);
args.delete(&pid);
}
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event="fib_read", fn_name="probe_handler")
b.attach_kretprobe(event="fib_read", fn_name="ret_handler")
while 1:
try:
res = b.trace_fields()
except ValueError:
continue
print(res[5].decode("UTF-8"))
```
就可以輸出類似 csv 格式,可供 gnuplot 繪圖的輸出:
```shell=
$ sudo python3 fibdrv-read-time.py
0, 3369
1, 622
2, 314
3, 308
4, 320
5, 320
6, 317
7, 315
8, 316
9, 326
10, 321
11, 332
12, 337
13, 340
14, 323
15, 325
[...]
```
> 正在嘗試在 fibdrv 的 [`client`](https://github.com/0xff07/fibdrv/blob/master/client.c) 加上一些 user space 的計時手段,並且搭配 `subprocess`,如:
>
> ```python=
> prog = ...
> b = BPF(text=prog)
> out = subprocess.check_output("./client")
>
> while 1:
> try:
> res = b.trace_fields()
> except ValueError:
> continue
> except KeyboardInterrupt:
> break
> print(res[5].decode('utf-8'))
>
> print(out.decode('utf-8'))
> ```
>
> 似乎就可以自動紀錄 ` clock_gettime` 得到的時間差跟 kprobe 得到的計時。
## 使用 kprobe__ 及 kretprobe__ 提示
另外一種方法是把 handler 前面加上 `kprobe__` 提示,也就是寫成 `kprobe__<fn_name>` 跟 `kretprobe__<fn_name>` 。比如說這個程式:
```python=
from bcc import BPF
prog = """
#include <uapi/linux/ptrace.h>
int probe_handler(struct pt_regs *ctx)
{
u64 ts = bpf_ktime_get_ns();
bpf_trace_printk("in: %llu\\n", ts);
return 0;
}
int ret_handler(struct pt_regs *ctx)
{
u64 ts = bpf_ktime_get_ns();
bpf_trace_printk("out:%llu\\n", ts);
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event="fib_read", fn_name="probe_handler")
b.attach_kretprobe(event="fib_read", fn_name="ret_handler")
b.trace_print()
```
也可以改成使用:
```python=
from bcc import BPF
prog = """
#include <uapi/linux/ptrace.h>
int kprobe__fib_read(struct pt_regs *ctx)
{
u64 ts = bpf_ktime_get_ns();
bpf_trace_printk("in: %llu\\n", ts);
return 0;
}
int kretprobe__fib_read(struct pt_regs *ctx)
{
u64 ts = bpf_ktime_get_ns();
bpf_trace_printk("out:%llu\\n", ts);
return 0;
}
"""
b = BPF(text=prog)
b.trace_print()
```