【Linux】BPF學習筆記 - bpftrace開發[7]

bpftrace是基於BPF和BCC構建的開源跟蹤程序。 與BCC一樣,bpftrace附帶了許多性能工具和支持文檔。 但是,它也提供了高級編程語言,允許創建功能強大的單行代碼和簡短的工具。 bpftrace是使用自定義單行代碼和簡短腳本的臨時工具的理想選擇,而BCC是複雜工具和守護程序的理想選擇

BPFTRACE 組件

bpftrace包含有關工具,手冊頁和示例文件的文檔,以及bpftrace編程教程(單行教程)和編程語言參考指南。 bpftrace工具的擴展名爲.bt. 前端使用lex和yacc解析bpftrace編程語言,並使用Clang解析結構。 後端將bpftrace程序編譯爲LLVM中間表示,然後由LLVM庫編譯爲BPF

一、運行

將執行程序,檢測其定義的所有事件。 該程序將一直運行到Ctrl-C或顯式調用exit()爲止

1. one-liner

-e參數運行的bpftrace程序稱爲one-liner

$ bpftrace -e program

2. 文件

可以將程序保存到文件並使用以下命令執行。.bt擴展名不是必需的,但有助於以後識別

$ bpftrace file.bt

也將文件設置爲可執行文件(chmod a + x file.d)並像運行其他程序一樣運行. 注意 bpftrace必須由root用戶執行

$ sudo ./file.bt

二、程序結構

在文件頂部放置解釋行

#!/usr/local/bin/bpftrace

bpftrace程序是一系列具有相關操作的探針(probes),當probes將執行相關的操作, 之前可以包含一個可選的過濾器表達式. 只有在過濾器表達式爲true時,纔會觸發該操作

probes { actions }
// 可選的過濾器表達式
probes /filter/ { actions }
...

1. Probe

探針以探針類型名稱開頭,然後以冒號分隔的層次結構

type:identifier1[:identifier2[...]]

層次結構由探針類型定義,例如:

  • kprobe探針類型檢測內核函數調用,只需要一個標識符:內核函數名
  • uprobe探針類型用於檢測用戶級別的函數調用,並且需要binary路徑和函數名稱
kprobe:vfs_read
uprobe:/bin/bash:readline

可以使用逗號分隔符指定多個探針以執行相同的操作. 有兩種不需要額外標識符的特殊探針類型:bpftrace程序的開頭和結尾均使用BEGIN和END觸發

probe1,probe2,... { actions }

a. Probe 通配符

**示例:**將檢測所有以vfs_開頭的kprobes(內核函數)

kprobe:vfs_*

檢測過多的探針可能會導致不必要的性能開銷。 爲了避免意外發生,bpftrace具有將啓用的可調的最大探針數,可以通過BPFTRACE_MAX_PROBES環境變量(當前默認爲5125)進行設置

您可以在使用bpftrace -l之前測試通配符

$ bpftrace -l 'kprobe:vfs_*'
kprobe:vfs_fallocate
kprobe:vfs_truncate
[...]
$ bpftrace -l 'kprobe:vfs_*' | wc -l
56

2. Filters

過濾器是布爾表達式,用於決定是否執行某項操作,例如:

# 僅當pid等於123時,才執行操作
/pid == 123/
# 等同於 pid != 0
/pid/
# 可以與布爾運算符(例如 &&)結合使用
/pid > 100 && pid < 1000/

3. Action

Action可以是單個語句,也可以是多個由分號分隔的語句

{ action one; action two; action three }

語句使用類似於C語言,並且可以操縱變量並執行bpftrace函數調用. 將變量$x設置爲42,然後使用printf()打印該變量

{ $x = 42; printf("$x is %d", $x); }

到這裏, 我們可以理解以下代碼

// 單行, 打印 Hello World!
$ bpftrace -e 'BEGIN { printf("Hello World!\n") }'
// 寫作文件形式
#!/usr/local/bin/bpftrace
BEGIN {
    	printf("Hello World!\n");
}

三、變量

共有三種變量類型:built-ins, scratch, 和 maps

1. Built-in

內置變量是bpftrace預先定義和提供的,通常是隻讀信息源。 其中包括進程號的pid,進程名的comm,時間戳 (以納秒爲單位) 和curtask (當前線程的 task_struct 地址)

變量 含義 變量 含義
pid 進程ID retval 返回值
tid 線程ID func trace函數名
uid 用戶ID probe probe的全名
nsecs 納秒級時間戳 kstack 多行字符串的形式返回內核級堆棧跟蹤
cpu 處理器ID ustack 多行字符串的形式返回用戶級堆棧跟蹤
comm process name args 參數

a. 位置參數

通過命令行傳遞給程序的,並且基於Shell腳本中使用的位置參數, $1表示第一個參數,$2表示第二個參數,以此類推

格式

bpftrace ./watchconn.bt 181
bpftrace -e 'program' 181

**示例1: **監視在命令行上傳遞的PID ./watchconn.bt 181

BEGIN
{
       printf("Watching connect() calls by PID %d\n", $1);
}

tracepoint:syscalls:sys_enter_connect
/pid == $1/
{
       printf("PID %d called connect()\n", $1);
}

示例2: 默認情況下,它們是整數。 如果將字符串用作參數,則必須通過str()調用對其進行訪問

$ bpftrace -e 'BEGIN { printf("Hello, %s!\n", str($1)); }' Reader

2. Scratch

臨時變量可用於臨時計算,並具有$前綴。 他們的名稱和類型是在他們第一次分配時設定的. 這些只能在分配了它們的操作塊中使用。 如果在沒有賦值的情況下引用變量,則bpftrace將出錯

# 將$x聲明爲整數
$x = 1;
# 將$y聲明爲字符串
$y = "hello";
# 將$z聲明爲指向結構task_struct的指針
$z = (struct task_struct *)curtask;

3. Map

映射變量使用BPF map 存儲對象,並具有@前綴。 它們可用於全局存儲,在操作之間傳遞數據。

格式:

@name
@name[key]
@name[key1, key2[, ...]]

**示例1:**當probe1觸發時,將1分配給@a,然後當probe2觸發時,將@a分配給$x。 如果先觸發probe1,然後觸發probe2,則$x將設置爲1,否則將設置爲0(未初始化)

probe1 { @a = 1; }
probe2 { $x = @a; }

示例2: 將nsecs內置變量,分配給名爲@start的映射,並且將tid(當前線程ID)設爲鍵值, 時間戳爲對應的值。 這樣一來,線程就可以存儲不會被其他線程覆蓋的自定義時間戳

@start[tid] = nsecs;

示例3: 其中同時使用pid內置變量和$fd變量作爲鍵

@path[pid, $fd] = str(arg0);

四、函數

1.內置函數

除了用於打印格式化輸出的printf(),其他內置函數包括

printf(): 輸出格式與C語言類似, %d

printf("%16s %-6d\n", comm, pid)

join(char *arr[]): 將帶有空格字符的字符串數組連接起來並打印出來

$ bpftrace -e 'tracepoint:syscalls:sys_enter_execve { join(args->argv); }'
Attaching 1 probe...
ls -l
df -h
date
ls -l bashreadline.bt biolatency.bt biosnoop.bt bitesize.bt

str(char *): 從指針返回字符串

$ bpftrace -e 'ur:/bin/bash:readline { printf("%s\n", str(retval)); }'

kstack(mode [, limit]) / ustack: 返回內核級/用戶級堆棧

// 通過跟蹤block:block_rq_insert跟蹤點,顯示導致創建塊I/O的前三個內核框架
$ bpftrace -e 't:block:block_rq_insert { @[kstack(3), comm] = count(); }'
// mode參數允許堆棧輸出採用不同的格式。 當前僅支持兩種模式:bpftrace (默認模式) 和 perf,其生成的堆棧格式類似於Linux perf
$ bpftrace -e 'k:do_nanosleep { printf("%s", ustack(perf)); }'

ksym(), usym() : 將地址解析爲其符號名稱

$ bpftrace -e 'tracepoint:timer:hrtimer_start { @[args->function] = count(); }'
[...]
@[-1169114960]: 2517
@[-1169048384]: 8237// 這些是原始地址, 使用ksym()將它們轉換爲內核函數名稱
$ bpftrace -e 'tracepoint:timer:hrtimer_start { @[ksym(args->function)] = count(); }'
[...]
@[it_real_fn]: 2269
@[hrtimer_wakeup]: 7714
@[tick_sched_timer]: 27092

system(format[, arguments ...]): 在shell上運行命令

$ bpftrace --unsafe -e 't:syscalls:sys_enter_nanosleep { system("ps -p %d\n", pid); }'
Attaching 1 probe...
  PID TTY          TIME CMD
 1148 ?        00:02:43 google_osconfig
  PID TTY          TIME CMD
 1148 ?        00:02:43 google_osconfig
  PID TTY          TIME CMD
 1148 ?        00:02:43 google_osconfig

2. Map 函數

map 也可以分配給特殊功能,這些以自定義方式存儲和打印數據。

count(): 計數事件,在打印時將打印計數。 這使用了per-CPU map@x成爲類型計數的特殊對象

$ bpftrace -e 'tracepoint:block:* { @[probe] = count(); }'
Attaching 18 probes...
^C

@[tracepoint:block:block_rq_issue]: 1
@[tracepoint:block:block_rq_insert]: 1
@[tracepoint:block:block_dirty_buffer]: 24
[...]

// 使用interval 探針, 可以打印每個間隔的速率
$ bpftrace -e 'tracepoint:block:block_rq_i* { @[probe] = count(); }
    interval:s:1 { print(@); clear(@); }'
Attaching 3 probes...
@[tracepoint:block:block_rq_issue]: 1
@[tracepoint:block:block_rq_insert]: 1

@[tracepoint:block:block_rq_insert]: 6
@[tracepoint:block:block_rq_issue]: 8
[...]

sum(), avg(), min(), max(): 存儲基本統計信息

@y = sum($x);

hist(int n): 將$x存儲在2n2^n的直方圖中,並且在打印時將打印存儲區計數和ASCII直方圖

$ bpftrace -e 'tracepoint:syscalls:sys_exit_read { @ret = hist(args->ret); }'
Attaching 1 probe...
^C

@ret:
(..., 0)             237 |@@@@@@@@@@@@@@                                      |
[0]                   13 |                                                    |
[1]                  859 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[2, 4)                57 |@@@                                                 |
[4, 8)                 5 |                                                    |
[...]

lhist(int n, int min, int max, int step): 這會將值存儲爲線性直方圖

$ bpftrace -e 'tracepoint:syscalls:sys_exit_read { @ret = lhist(args->ret, 0, 1000, 100); }'
Attaching 1 probe...
^C

@ret:
(..., 0)             101 |@@@                                                 |
[0, 100)            1569 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[100, 200)             5 |                                                    |
[200, 300)             0 |                                                    |
[...]

delete(): 從@start map中刪除鍵是 tid 的鍵/值對

delete(@start[tid]);

clear(), zero(): 清空和置0

clear(@map)
zero(@map)

3. 示例

Timing vfs_read(): 該程序vfsread.bt,測量vfs_read()內核函數中的時間,並將時間打印爲直方圖(以微秒爲單位)

程序一直運行到輸入Ctrl-C ,然後打印此輸出並終止。 該直方圖被命名爲"us",因爲它可以打印輸出名稱,因此可以在輸出中包含單位,給map賦予有意義的名稱

#!/usr/local/bin/bpftrace

// this program times vfs_read()

// 通過使用kprobe檢測其開始並將時間戳記存儲在以線程ID爲鍵的@start map中
kprobe:vfs_read
{
       @start[tid] = nsecs;
}
// 使用kretprobe檢測其結束並計算增量爲vfs_read()內核函數的持續時間. 使用過濾器來確保記錄了開始時間
kretprobe:vfs_read
/@start[tid]/
{
       $duration_us = (nsecs - @start[tid]) / 1000;
       @us = hist($duration_us);
       delete(@start[tid]);
}

此腳本可以根據需要進行自定義. 這說明了bpftrace最有用的功能之一。 使用傳統的系統工具(如iostat和vmstat),輸出是固定的,無法輕鬆自定義。 但是用bpftrace,您可以將看到的指標進一步細分爲多個部分,並通過其他探針的指標進行增強,直到獲得所需的答案爲止

@us[pid, comm] = hist($duration_us);
// output
@us[1847, gdbus]:
[1]                    2 |@@@@@@@@@@                                          |
[2, 4)                10 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[4, 8)                10 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|

@us[1630, ibus-daemon]:
[2, 4)                 9 |@@@@@@@@@@@@@@@@@@@@@@@@@@@                         |
[4, 8)                17 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[...]

五、Probe類型

1. tracepoint - 內核靜態插樁

a. 格式

tracepoint_name是跟蹤點的全名,包括冒號,它將冒號分隔成自己的類和事件名稱的層次結構,例如net:netif_rx

tracepoint:tracepoint_name

b. 參數

跟蹤點提供可通過內置args在bpftrace中訪問的信息字段。

例如,net:netif_rx有一個稱爲len的字段,表示可以使用args -> len訪問的數據包長度

如果您不熟悉bpftrace和跟蹤,則系統調用跟蹤點是很好的檢測目標。 它們提供了廣泛的內核資源使用範圍,並具有文檔齊全的API。例如: 檢測read系統調用的開始和結束

syscalls:sys_enter_read
syscalls:sys_exit_read

對於sys_enter_read跟蹤點, 可以使用 -l and -v 查看詳情. 如下所示, 其參數應爲args-> fd

$ bpftrace -lv tracepoint:syscalls:sys_enter_read
tracepoint:syscalls:sys_enter_read
    int __syscall_nr;
    unsigned int fd;
    char * buf;
    size_t count;

使用示例:

$ bpftrace -e 'tracepoint:syscalls:sys_enter_read {
    printf("-> count() by %s PID %d\n", comm, pid); }
  tracepoint:syscalls:sys_exit_read {
    printf("<- count() return %d, %s PID %d\n", args->ret, comm, pid); }'

2. usdt - 用戶級靜態插樁

a. 格式

usdt可以通過提供完整路徑來檢測可執行binaries或shared庫。 probe_name是binary中的USDT探針名稱

usdt:binary_path:probe_name
usdt:library_path:probe_name
usdt:binary_path:probe_namespace:probe_name
usdt:library_path:probe_namespace:probe_name

在不指定探針namespace的情況下,它的默認名稱與binary或庫的名稱相同。 如果它有許多不同的探針,則必須有namespace。 一個示例是libjvm(JVM庫)中的hotspot namespace 探針。 例如(完整庫路徑被截斷)

usdt:/.../libjvm.so:hotspot:method__entry

b. 參數

USDT探針的任何參數都可以作爲內置args的成員使用,可以使用-l列出binary中的可用探針

$ bpftrace -l 'usdt:/usr/local/cpython/python'
usdt:/usr/local/cpython/python:line
usdt:/usr/local/cpython/python:function__entry
[...]

3. kprobe, kretprobe - 內核動態插樁

a. 格式

kprobe表示函數的開始(入口),而kretprobe表示函數的結束(返回). function_name是內核函數名稱。 例如,可以使用kprobe:vfs_readkretprobe:vfs_read來檢測vfs_read()內核函數。

kprobe:function_name
kretprobe:function_name

b. 參數

kprobe的參數: arg0, arg1, …, argN是函數的輸入參數,爲uint64。 如果它們是C結構的指針,則可以將它們強制轉換爲該struct.

kretprobe的參數:retval內置函數具有該函數的返回值。 retval始終是uint64; 如果這與該函數的返回類型不匹配,則需要將其強制轉換爲該類型。

c. 示例

例如,使用kretprobe將vfs_read()返回值(字節或錯誤值)彙總爲直方圖

$ bpftrace -e 'kretprobe:vfs_read { @bytes = hist(retval); }'

4. uprobe, uretprobe - 用戶級動態插樁

a. 格式

uprobe表示函數的開始(入口),而uretprobe表示函數的結束(返回)

uprobe:binary_path:function_name
uprobe:library_path:function_name
uretprobe:binary_path:function_name
uretprobe:library_path:function_name

b. 參數

uprobe的參數: arg0, arg1, …, argN是函數的輸入參數,爲uint64。 如果它們是C結構的指針,則可以將它們強制轉換爲該struct.

uretprobe的參數: retval內置函數具有該函數的返回值。 retval始終是uint64; 如果這與該函數的返回類型不匹配,則需要將其強制轉換爲該類型

5. software, hardware

這些是預定義的軟件和硬件事件

a. 格式

software:event_name:count
software:event_name:
hardware:event_name:count
hardware:event_name:

軟件事件類似於跟蹤點,但適用於基於計數的指標和基於樣本的檢測。 硬件事件是用於處理器級分析的PMC的選擇。兩種事件類型都可能發生得如此頻繁,以至於對每個事件進行檢測都會產生大量開銷,從而降低系統性能。 通過使用採樣和計數字段可以避免這種情況,該字段會觸發探針在每個[count]個事件中觸發一次。 如果未提供計數,則使用默認值

b. 可用的事件

軟件

硬件

6. profile, interval

這些是基於計時器的事件

a. 格式

profile 文件類型會在所有CPU上觸發,並且可用於採樣CPU使用率。 interval 類型僅在一個CPU上觸發,可用於打印基於間隔的輸出

profile:hz:rate
profile:s:rate
profile:ms:rate
profile:us:rate
interval:s:rate
interval:ms:rate

六、Operators

1. 運算符

=, +, -, *, /, ++, --, &, |, ^, !
// 左移,右移
<<, >>
// 比較
+=, -=, *=, /=, %=, &=, ^=, <<=, >>=

2. 三元運算符

test ? true_statement : false_statement

示例: 計算絕對值

$abs = $x >= 0 ? $x : - $x;

3. IF

當前不支持else if語句。

if (test) { true_statements }
if (test) { true_statements } else { false_statements }

示例: 在IPv4和IPv6上執行不同動作的程序

if ($inet_family == $AF_INET) {
    // IPv4
   ...
} else {
    // IPv6
    ...
}

4. Unrolled 循環

BPF在受限的環境中運行,在該環境中必須能夠驗證程序是否已結束,並且不會陷入無限循環中。 對於需要某些循環功能的程序,bpftrace通過unroll()支持展開循環.

unroll (count) { statements }

count是一個整數常量,最大爲20。不支持將計數作爲變量提供,因爲必須在BPF編譯階段知道循環迭代的次數

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章