【Linux】BPF學習筆記 - 技術背景[2]

本學習筆記來自於閱讀 Brendan Gregg的《BPF Performance Tools》

一、CLASSICAL BPF (BPF)

用戶使用針對BPF虛擬機的指令集(也稱爲BPF字節碼)定義過濾器表達式, 然後傳遞給內核以供解釋器執行. 這使得過濾可以在內核級別進行,而無需將每個數據包複製到用戶級別的進程中, 提升了tcpdump使用的數據包篩選的性能.

它還提供了安全性,因爲可以在執行之前驗證用戶空間中不受信任的篩選器的安全性.

# 打印出用於過濾器表達式的BPF指令
tcpdump -d host 127.0.0.1 and port 80
# output
(000) ldh      [12]
(001) jeq      #0x800           jt 2    jf 18
(002) ld       [26]
(003) jeq      #0x7f000001      jt 6    jf 4
(004) ld       [30]
[...] 

最初的BPF (classical BPF) 是一臺有限的虛擬機, 它有兩個寄存器, 一個由十六個存儲插槽組成的暫存存儲器 以及 一個程序計數器. 這些都以 32-bit 寄存器大小運行. 自從將BPF添加到Linux內核以來,已有一些重要的改進.

  • Linux 3.0中添加了BPF即時(JIT)編譯器, 從而提高了解釋器的性能
  • 2012年爲 seccomp (安全計算) 系統調用策略添加了BPF過濾器, 這是BPF在 networking外部的首次使用,並顯示了將BPF用作通用執行引擎的潛力

二、EXTENDED BPF (EBPF)

EBPF將使BPF擴展成爲通用虛擬機, 它添加了更多的寄存器,從 32-bit 改爲 64-bit. 另外, 創建了靈活的BPF “map” 存儲,並允許調用某些受限制的內核功能.

它還設計成通過一對一映射到 native 指令和寄存器進行JIT,從而允許將先前的 native指令優化技術重新用於BPF。 BPF驗證程序也進行了更新以處理這些擴展,並拒絕任何不安全的代碼

1. 運行時體系機構

Linux BPF運行時的體系結構如下圖所示,顯示了BPF指令如何通過BPF驗證程序以由BPF虛擬機執行。

BPF虛擬機實現同時具有解釋器和JIT編譯器:JIT編譯器生成用於直接執行的本機指令。 驗證者拒絕不安全的操作,包括無界的循環: BPF程序必須在有界的時間內完成。

BPF可以利用幫助程序來獲取內核狀態,並使用BPF映射來進行存儲。 BPF程序在事件中執行,這些event包括kprobes,uprobes和tracepoint

2. 爲什麼性能工具要使用BPF

性能工具使用BPF來實現其可編程性, BPF程序可以執行自定義等待時間計算和統計摘要。另外它還高效且生產安全,並且內置於Linux內核中。 使用BPF,可以在生產環境中運行這些工具,而無需添加任何新的內核組件。

舉例說明: bitehist以直方圖的形式顯示磁盤I/O

bitehist
Tracing block device I/O... Interval 5 secs. Ctrl-C to end.

     kbytes          : count     distribution
       0 -> 1        : 3        |                                      |
       2 -> 3        : 0        |                                      |
       4 -> 7        : 3395     |************************************* |
[...] 

Before BPF: 步驟2-4對於高I/O系統具有高性能開銷。例如, 每秒將一萬個磁盤I/O跟蹤記錄傳輸到用戶空間程序以進行分析和彙總

內核中

  1. 啓用對磁盤I/O事件的插樁
  2. 對於每個事件, 將一條記錄寫入 perf buffer 如果使用跟蹤點(首選),則記錄包含有關磁盤I/O的元數據的幾個字段

用戶空間中

  1. 定期將所有事件的緩衝區複製到用戶空間

  2. 遍歷每個事件,爲字節字段解析事件元數據,其他字段將被忽略

  3. 生成字節字段的直方圖(Histogram)摘要

Using BPF: 關鍵的變化是直方圖可以在內核中生成,避免了將事件複製到用戶空間並對其進行重新處理的成本, 還避免了複製未使用的元數據字段。從而大大減少了複製到用戶空間的數據量。

內核中

  1. 啓用對磁盤I/O事件的插樁,並附加一個由bitesize定義的BPF程序
  2. 對於每個事件, 運行BPF程序. 它僅獲取字節字段,並將其保存到自定義BPF映射直方圖中

用戶空間

  1. 一次讀取BPF映射直方圖並打印出來

3. 對比內核模塊

使用BPF而不是內核模塊進行跟蹤的好處是

  • BPF程序通過驗證程序進行檢查; 內核模塊可能會引入錯誤或安全漏洞
  • BPF通過 map提供了豐富的數據結構
  • BPF程序可以編譯一次,然後在任何地方運行,因爲BPF指令集,map,幫助程序和基礎結構是穩定的ABI
  • BPF程序不需要編譯內核構建工件
  • BPF編程比開發內核模塊所需的內核工程更容易學習,使更多的人可以使用它

使用內核模塊好處是

  • 可以使用其他內核功能和設施,而不僅限於BPF helper 調用

三、bpftool

在Linux 4.15中添加了bpftool來查看和操作BPF對象,包括程序和map

1. bpftool

默認輸出顯示bpftool操作的對象類型

bpftool
# output
Usage: bpftool [OPTIONS] OBJECT { COMMAND | help }
       bpftool batch file FILE
       bpftool version

       OBJECT := { prog | map | cgroup | perf | net | feature | btf }
       OPTIONS := { {-j|--json} [{-p|--pretty}] | {-f|--bpffs} |
                   {-m|--mapcompat} | {-n|--nomount} }

每個OBJECT都有一個單獨的幫助頁面, 例如: bpftool prog help

2.bpftool perf

該指令顯示了通過perf_event_open()附加的BPF程序

bpftool perf
# output: shows three different PIDs with various BPF programs.
pid 1765  fd 6: prog_id 26  kprobe  func blk_account_io_start  offset 0
pid 1765  fd 8: prog_id 27  kprobe  func blk_account_io_done  offset 0
pid 1765  fd 11: prog_id 28  kprobe  func sched_fork  offset 0
[...] 

prog_id是BPF的 program ID,可以使用以下命令來打印

3. bpftool prog show

列出了bpftrace的program ID (232), BCC的program ID (263) 以及其他BPF programs.

請注意,BCC kprobe程序具有BPF類型格式(BTF)信息,此輸出中存在btf_id來顯示該信息

bpftool prog show
# output
[...]
232: kprobe  name END  tag b7cc714c79700b37  gpl
       loaded_at 2019-06-18T21:29:26+0000  uid 0
       xlated 168B  jited 138B  memlock 4096B  map_ids 130
[...]
258: cgroup_skb  tag 7be49e3934a125ba  gpl
       loaded_at 2019-06-18T21:31:27+0000  uid 0
       xlated 296B  jited 229B  memlock 4096B  map_ids 153,154
[...]
263: kprobe  name trace_req_done  tag d9bc05b87ea5498c  gpl
       loaded_at 2019-06-18T21:37:51+0000  uid 0
       xlated 912B  jited 567B  memlock 4096B  map_ids 158,157
       btf_id 5
[...]

4. bpftool prog dump xlated

每個BPF程序都可以通過其ID打印dump, xlated模式將打印轉換爲彙編的BPF指令.

輸出顯示了BPF可以使用的受限內核調用之一: bpf_probe_read()

bpftool prog dump xlated id 234
# output
   0: (bf) r6 = r1
   1: (07) r6 += 112
   2: (bf) r1 = r10
   3: (07) r1 += -8
   4: (b7) r2 = 8
   5: (bf) r3 = r6
   6: (85) call bpf_probe_read#-51584”

將其與BCC 塊 program ID 263對比: 其包括來自BTF的源信息, 例如 ; struct request *req = ctx->di;

bpftool prog dump xlated id 263
# output
int trace_req_done(struct pt_regs * ctx):
; struct request *req = ctx->di;
   0: (79) r1 = *(u64 *)(r1 +112)
; struct request *req = ctx->di;
   1: (7b) *(u64 *)(r10 -8) = r1
; tsp = bpf_map_lookup_elem((void *)bpf_pseudo_fd(1, -1), &req);
   2: (18) r1 = map[id:158]
   4: (bf) r2 = r10
;
   5: (07) r2 += -8”

linum修飾符將包括源文件和行號信息,也來自BTF(如果有的話).其中行號信息是指BCC在運行程序時創建的虛擬文件

如下所示, 多了[file:/virtual/main.c line_num:42 line_col:29]

int trace_req_done(struct pt_regs * ctx):
; struct request *req = ctx->di; [file:/virtual/main.c line_num:42 line_col:29]
   0: (79) r1 = *(u64 *)(r1 +112)

opcodes修飾符將包含BPF指令操作碼

; struct request *req = ctx->di;
   0: (79) r1 = *(u64 *)(r1 +112)
       79 11 70 00 00 00 00 00”

visual 修飾符,它以DOT格式發出控制流程圖信息,以供外部軟件可視化

bpftool prog dump xlated id 263 visual > biolatency_done.dot
# 使用GraphViz及其dot有向圖工具
dot -Tpng -Elen=2.5 biolatency_done.dot -o biolatency_done.png

四、BPF API

1. BPF Helper Functions

詳細信息可查看bpf.h文件

bpf_probe_read(): BPF中的內存訪問僅限於BPF寄存器和堆棧(以及通過 helper 的BPF map)。 要讀取任意內存,必須通過該方法進行讀取,內存會執行安全檢查。 它還可以在讀取此任意內存時,禁用頁面錯誤,以確保讀取不會引起探針上下文的錯誤。除了讀取內核內存外,該幫助程序還用於將用戶空間內存讀入內核空間。

2. BPF Program Types

部分 BPF map types

bpf_map_type 描述
BPF_MAP_TYPE_HASH 哈希表: 鍵/值對
BPF_MAP_TYPE_PERCPU_HASH 基於每個CPU維護的更快的哈希表
BPF_MAP_TYPE_ARRAY 元素數組

五、BPF併發控制

通過跟蹤,並行線程可以並行查找和更新BPF map 字段,存在一個線程覆蓋另一個線程的更新的問題,這也稱爲丟失更新問題,其中併發讀寫導致更新丟失。

BCC和bpftrace儘可能使用 per-CPU 哈希和數組 map 類型,以避免這種破壞。爲每個CPU創建使用實例, 從而防止並行線程更新共享位置。 例如,可以將計數事件的映射更新爲每個CPU的映射,然後可以在需要總計數時組合每個CPU的值。

示例: 以下bpftrace使用 per-CPU 哈希值進行計數:

strace -febpf bpftrace -e 'k:vfs_read { @ = count(); }'
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_PERCPU_HASH, key_size=8, value_size=8, max_entries=128, map_flags=0, inner_map_fd=0}, 72) = 3
[...]

以下bpftrace使用普通哈希進行計數:

strace -febpf bpftrace -e 'k:vfs_read { @++; }'
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=8, value_size=8, max_entries=128, map_flags=0, inner_map_fd=0}, 72) = 3
[...]

在8-CPU系統上同時使用它們,並跟蹤一個頻繁且可能並行運行的功能:

bpftrace -e 'k:vfs_read { @cpuhash = count(); @hash++; }'
Attaching 1 probe...
^C
# 結果表明, 普通哈希將事件計數降低了0.01%
@cpuhash: 1061370
@hash: 1061269

還存在其他方式進行併發控制, 詳見原書P93

六、BPF 的限制

  • BPF程序不能調用任意內核功能, 僅限於API中列出的BPF helper 程序功能
  • BPF程序也不能執行循環,因爲它們必須在一定時間內完成
  • BPF堆棧大小限制爲MAX_BPF_STACK,設置爲512. 在開發工具時有時會遇到此限制,尤其是在堆棧上存儲多個字符串緩衝區時: char[256]
    • 解決方案是改爲使用BPF映射存儲, 而不是堆棧存儲
  • 指令數限制爲4096,長BPF程序有時會遇到此限制

參考資料

Git: bpf.h

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