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
存儲在的直方圖中,並且在打印時將打印存儲區計數和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_read
和kretprobe: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編譯階段知道循環迭代的次數