bcc Python開發者教程(譯:bcc Python Developer Tutorial)


翻譯自:https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md

bcc Python Developer Tutorial


 這個教程主要目的是展示如何使用python來進行bcc工具開發和編程。教程主要分為兩個部分:可觀察性和網絡。

文中的代碼片段均都來自於bcc:代碼片段的licenses見bcc中具體文件。

也可參考bcc開發者手冊reference_guide.md以及end-users工具教程: tutorial.md。此外bcc還開放有lua接口。

Observability


"可觀察性"教程包含17個課程和46個要學習列舉的事項。

Lesson 1. Hello World

我們通過運行examples/hello_world.py這個例子來開啟我們的學習之旅。在一個終端運行這個腳本,同時在另外一個終端運行一些命令(例如"ls")。正常的預期是在新任務運行時打印"Hello, World!",如果結果不符合預期,說明還有一些東西沒有准備好:參考 INSTALL.md

# ./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  https://github.com/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. 示例輸出如下 (命令運行在另外一個終端):

# ./examples/tracing/hello_fields.py
TIME(s)            COMM             PID    MESSAGE
24585001.174885999 sshd             1432   Hello, World!
24585001.195710000 sshd             15780  Hello, World!
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(event=b.get_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(event=b.get_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 sync's... Ctrl-C to end
At time 0.00 s: multiple syncs detected, last 95 ms ago
At time 0.10 s: multiple syncs detected, last 96 ms ago

這個程序在 examples/tracing/sync_timing.py :

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

    // 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:
    (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

看看 examples/tracing/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  4096       42.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!
[...]

代碼取自 examples/tracing/hello_perf_output.py :

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(&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
    print("%-18.9f %-16s %-6d %s" % (time_s, event.comm, event.pid,
        "Hello, perf_output!"))

# loop with callback to print_event
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
Tracing... 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      |**************************************|

下面在來自 examples/tracing/bitehist.py

from __future__ import print_function
from bcc import BPF
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("Tracing... 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: 對參數成員進行引用。

需要學習的新東西:

  1. BPF_HISTOGRAM(dist): 定義一個histogram類型BPF map 對象,其名字為"dist"。
  2. dist.increment(): 增加dist標記索引,第一個參數表示增加的步長,如果沒有指定參數,默認步長為1。或者也可以自己將第二個參數作為步長。
  3. bpf_log2l(): 對參數進行log-2計算,其結果作為直方圖索引,這樣我們可以構建一個2階直方圖。
  4. b["dist"].print_log2_hist("kbytes"): 打印"dist"的2階直方圖, 以kbytes作為輸出列的頭信息。由於 bucket計數是內核到用戶態傳輸的唯一數據,因此這種方式效率很高。

Lesson 10. disklatency.py

編寫一個程序記錄disk I/O的時間,並打印出這些延遲時間的直方圖。Disk I/O 的監測和計時可參考上一節的disksnoop.py程序,直方圖代碼可以參考上一節的bitehist.py程序。

Lesson 11. vfsreadlat.py

這個例子程序拆分為單獨的Python 和 C 文件。輸出如下:

# ./vfsreadlat.py 1
Tracing... Hit Ctrl-C to end.
     usecs               : count     distribution
         0 -> 1          : 0        |                                        |
         2 -> 3          : 2        |***********                             |
         4 -> 7          : 7        |****************************************|
         8 -> 15         : 4        |**********************                  |

     usecs               : count     distribution
         0 -> 1          : 29       |****************************************|
         2 -> 3          : 28       |**************************************  |
         4 -> 7          : 4        |*****                                   |
         8 -> 15         : 8        |***********                             |
        16 -> 31         : 0        |                                        |
        32 -> 63         : 0        |                                        |
        64 -> 127        : 0        |                                        |
       128 -> 255        : 0        |                                        |
       256 -> 511        : 2        |**                                      |
       512 -> 1023       : 0        |                                        |
      1024 -> 2047       : 0        |                                        |
      2048 -> 4095       : 0        |                                        |
      4096 -> 8191       : 4        |*****                                   |
      8192 -> 16383      : 6        |********                                |
     16384 -> 32767      : 9        |************                            |
     32768 -> 65535      : 6        |********                                |
     65536 -> 131071     : 2        |**                                      |

     usecs               : count     distribution
         0 -> 1          : 11       |****************************************|
         2 -> 3          : 2        |*******                                 |
         4 -> 7          : 10       |************************************    |
         8 -> 15         : 8        |*****************************           |
        16 -> 31         : 1        |***                                     |
        32 -> 63         : 2        |*******                                 |
[...]

代碼可參考examples/tracing/vfsreadlat.pyexamples/tracing/vfsreadlat.c

需要學習的東西:

  1. b = BPF(src_file = "vfsreadlat.c"): 從一個單獨的C語言文件讀取程序。
  2. b.attach_kretprobe(event="vfs_read", fn_name="do_return"): 將BPF C函數do_return() 添加到內核函數 vfs_read()的返回點kprobe鈎子中,也就是kretprobe:跟蹤的是一個內核函數的返回點,而非進入點。
  3. b["dist"].clear(): 清除histogram.

Lesson 12. urandomread.py

跟蹤dd if=/dev/urandom of=/dev/null bs=8k count=5命令:

# ./urandomread.py
TIME(s)            COMM             PID    GOTBITS
24652832.956994001 smtp             24690  384
24652837.726500999 dd               24692  65536
24652837.727111001 dd               24692  65536
24652837.727703001 dd               24692  65536
24652837.728294998 dd               24692  65536
24652837.728888001 dd               24692  65536

哈!我偶然抓到了smtp。這個例子的代碼在 examples/tracing/urandomread.py

from __future__ import print_function
from bcc import BPF

# load BPF program
b = BPF(text="""
TRACEPOINT_PROBE(random, urandom_read) {
    // args is from /sys/kernel/debug/tracing/events/random/urandom_read/format
    bpf_trace_printk("%d\\n", args->got_bits);
    return 0;
}
""")

# header
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "GOTBITS"))

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

本節需要學習:

  1. TRACEPOINT_PROBE(random, urandom_read): 跟蹤內核tracepoint函數random:urandom_read。由於tracepoint的API比較穩定,一般不會輕易修改;因而相較於kprobe,只要能夠使用tracepoint就盡量選擇tracepoint跟蹤。可以通過perf list命令來列出有哪些可用的tracepoints。在Linux >= 4.7 的版本中要求將BPF 程序 attach 到tracepoints。
  2. args->got_bits: args 是自動生成的,其類型為tracepoint參數類型數據結構。上面代碼中的注釋提示可以在哪里查找到這個結構體,Eg:
# cat /sys/kernel/debug/tracing/events/random/urandom_read/format
name: urandom_read
ID: 972
format:
	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
	field:int common_pid;	offset:4;	size:4;	signed:1;

	field:int got_bits;	offset:8;	size:4;	signed:1;
	field:int pool_left;	offset:12;	size:4;	signed:1;
	field:int input_left;	offset:16;	size:4;	signed:1;

print fmt: "got_bits %d nonblocking_pool_entropy_left %d input_entropy_left %d", REC->got_bits, REC->pool_left, REC->input_left

在這個案例中我們打印的是got_bits成員。

Lesson 13. disksnoop.py fixed版

對前一節的disksnoop.py程序進行修改,這次跟蹤block:block_rq_issue 和 block:block_rq_complete 兩個tracepoints點。

Lesson 14. strlen_count.py

這一課中我們的程序要跟蹤的是strlen()這個用戶態庫函數,並統計這個函數中不同參數出現的頻次。用例輸出如下:

# ./strlen_count.py
Tracing strlen()... Hit Ctrl-C to end.
^C     COUNT STRING
         1 " "
         1 "/bin/ls"
         1 "."
         1 "cpudist.py.1"
         1 ".bashrc"
         1 "ls --color=auto"
         1 "key_t"
[...]
        10 "a7:~# "
        10 "/root"
        12 "LC_ALL"
        12 "en_US.UTF-8"
        13 "en_US.UTF-8"
        20 "~"
        70 "#%^,~:-=?+/}"
       340 "\x01\x1b]0;root@bgregg-test: ~\x07\x02root@bgregg-test:~# "

這些是這次跟蹤到的該庫函數處理的各種字符串參數,同時還打印了他們出現頻次情況。例如strlen()使用"LC_ALL" 字符串參數調用了12次。

代碼在 examples/tracing/strlen_count.py

from __future__ import print_function
from bcc import BPF
from time import sleep

# load BPF program
b = BPF(text="""
#include <uapi/linux/ptrace.h>

struct key_t {
    char c[80];
};
BPF_HASH(counts, struct key_t);

int count(struct pt_regs *ctx) {
    if (!PT_REGS_PARM1(ctx))
        return 0;

    struct key_t key = {};
    u64 zero = 0, *val;

    bpf_probe_read_user(&key.c, sizeof(key.c), (void *)PT_REGS_PARM1(ctx));
    // could also use `counts.increment(key)`
    val = counts.lookup_or_try_init(&key, &zero);
    if (val) {
      (*val)++;
    }
    return 0;
};
""")
b.attach_uprobe(name="c", sym="strlen", fn_name="count")

# header
print("Tracing strlen()... Hit Ctrl-C to end.")

# sleep until Ctrl-C
try:
    sleep(99999999)
except KeyboardInterrupt:
    pass

# print output
print("%10s %s" % ("COUNT", "STRING"))
counts = b.get_table("counts")
for k, v in sorted(counts.items(), key=lambda counts: counts[1].value):
    print("%10d \"%s\"" % (v.value, k.c.encode('string-escape')))

本節需要學習:

  1. PT_REGS_PARM1(ctx): 這個宏用於獲取被跟蹤函數strlen()的第一個參數,也就是要處理的字符串。
  2. b.attach_uprobe(name="c", sym="strlen", fn_name="count"):Attach "c"庫(if this is the main program, use its pathname),跟蹤其用戶態函數strlen(),並在strlen()函數執行時調用我們的掛接的uprobe函數count()。

Lesson 15. nodejs_http_server.py

這一次我們將跟蹤用戶態靜態定義tracing (USDT) 探針,這就是用戶態版本的tracepoint。示例輸出如下:

# ./nodejs_http_server.py 24728
TIME(s)            COMM             PID    ARGS
24653324.561322998 node             24728  path:/index.html
24653335.343401998 node             24728  path:/images/welcome.png
24653340.510164998 node             24728  path:/images/favicon.png

相關代碼來自於 examples/tracing/nodejs_http_server.py

from __future__ import print_function
from bcc import BPF, USDT
import sys

if len(sys.argv) < 2:
    print("USAGE: nodejs_http_server PID")
    exit()
pid = sys.argv[1]
debug = 0

# load BPF program
bpf_text = """
#include <uapi/linux/ptrace.h>
int do_trace(struct pt_regs *ctx) {
    uint64_t addr;
    char path[128]={0};
    bpf_usdt_readarg(6, ctx, &addr);
    bpf_probe_read_user(&path, sizeof(path), (void *)addr);
    bpf_trace_printk("path:%s\\n", path);
    return 0;
};
"""

# enable USDT probe from given PID
u = USDT(pid=int(pid))
u.enable_probe(probe="http__server__request", fn_name="do_trace")
if debug:
    print(u.get_text())
    print(bpf_text)

# initialize BPF
b = BPF(text=bpf_text, usdt_contexts=[u])

本節需要學習:

  1. bpf_usdt_readarg(6, ctx, &addr): 從USDT probe中讀取第6個參數地址到addr中。
  2. bpf_probe_read_user(&path, sizeof(path), (void *)addr):現在將addr 指向path。
  3. u = USDT(pid=int(pid)): 使用指定的PID初始化USDT tracing。
  4. u.enable_probe(probe="http__server__request", fn_name="do_trace"): 將我們的BPF C函數do_trace() 添加到USDT探測點Node.js的http__server__request 處。
  5. b = BPF(text=bpf_text, usdt_contexts=[u]): 需要將我們的USDT對象"u"傳入到BPF對象的創建函數中。

Lesson 16. task_switch.c

這是前期課程中已經包含的一部分內容, 這里主要是回顧和鞏固一下我們前面已經學過的內容。

這是一個比Hello World稍復雜的跟蹤實例。這個程序會在每次任務切換時被調用,它會把新/老任務的pids記錄到BPF map中。

下面這段C程序引入了一個新概念: 參數 prev。這個參數由BCC前端進行特殊處理,因此對該變量的訪問是從kprobe基礎結構傳遞的已保存上下文中獲取。從位置1開始的參數的原型應該與正在被探測的內核函數的原型匹配。這樣一來程序就對函數參數的訪問權無縫對接。

#include <uapi/linux/ptrace.h>
#include <linux/sched.h>

struct key_t {
    u32 prev_pid;
    u32 curr_pid;
};

BPF_HASH(stats, struct key_t, u64, 1024);
int count_sched(struct pt_regs *ctx, struct task_struct *prev) {
    struct key_t key = {};
    u64 zero = 0, *val;

    key.curr_pid = bpf_get_current_pid_tgid();
    key.prev_pid = prev->pid;

    // could also use `stats.increment(key);`
    val = stats.lookup_or_try_init(&key, &zero);
    if (val) {
      (*val)++;
    }
    return 0;
}

用戶態組件加載上面文件的內容並attache到finish_task_switch()內核函數的kprobe探測鈎子中。 通過[]這個操作將可以訪問到程序中BPF對象中BPF_HASH元素,這樣就可以直接訪問到內核中的變量。使用這個對象就像python中的其他對象一樣:read, update, and deletes 等等內置的函數都是標准配置。

from bcc import BPF
from time import sleep

b = BPF(src_file="task_switch.c")
b.attach_kprobe(event="finish_task_switch", fn_name="count_sched")

# generate many schedule events
for i in range(0, 100): sleep(0.01)

for k, v in b["stats"].items():
    print("task_switch[%5d->%5d]=%u" % (k.prev_pid, k.curr_pid, v.value))

這些程序可以在這些文件中找到 examples/tracing/task_switch.c 以及 examples/tracing/task_switch.py 。

Lesson 17. Further Study

要想進一步學習可參考Sasha Goldshtein的linux-tracing-workshop , 這里還提供了額外的實驗;此外bcc項目的 /tools牧中也有許多工具值得學習。

如果您想為bcc貢獻工具,請閱讀CONTRIBUTING-SCRIPTS.md 文檔。 在README.md 文檔的尾部你還可以找到我們的聯系方式。Good luck, and happy tracing!

Networking


 To do.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM