1.BPF起源
BPF源頭起源于一篇1992年的論文,這篇論文主要提出一種新的網(wǎng)絡(luò)數(shù)據(jù)包的過濾的框架,如下圖所示。
提出bpf的原因其實也很簡單,早期我們從網(wǎng)卡中接收到很多的數(shù)據(jù)包,我們要想從中過濾出我們想要的數(shù)據(jù)包,我們需要將網(wǎng)卡接收的數(shù)據(jù)包都要從內(nèi)核空間拷貝一份到用戶空間。然后,用戶程序在對這些進(jìn)行過濾。那么,我們可以從中就能夠發(fā)現(xiàn)一個問題。數(shù)據(jù)包必須全部拷貝。然后再過濾出所需的數(shù)據(jù)包,那么對于那些不需要的數(shù)據(jù)包,我們拷貝的操作是無效的、浪費的。并且對于內(nèi)存數(shù)據(jù)的拷貝是很費cpu系統(tǒng)的資源的。所以,這篇論文,就提出了一種新的框架,在內(nèi)核中直接過濾,這也可以避免一些無用的、浪費的拷貝。
其背后的思想其實就是:與其把數(shù)據(jù)包復(fù)制到用戶空間執(zhí)行用戶態(tài)程序過濾,不如把過濾程序灌進(jìn)內(nèi)核去。
這種新的框架,其實還是很容易理解的。大概的理解就是,當(dāng)我們從網(wǎng)卡接收到一個數(shù)據(jù)包的時候,我們數(shù)據(jù)鏈路層,將數(shù)據(jù)包額外的拷貝一份。然后這個新的數(shù)據(jù)包就交給BPF程序進(jìn)行處理,這個BPF根據(jù)用戶編寫的過濾規(guī)則對這個新的數(shù)據(jù)包進(jìn)行匹配。如果符合此規(guī)則就將數(shù)據(jù)包放到接收隊列中,那么用戶事后就可以從接收隊列中將這個數(shù)據(jù)包從內(nèi)核空間拷貝到用戶空間,這樣就減少了無用的數(shù)據(jù)包的拷貝。
像tcpdump/wireshark等用戶工具就是基于BPF框架實現(xiàn)的。其大概實現(xiàn)的過程就是,編寫B(tài)PF指令集的過濾規(guī)則,然后創(chuàng)建raw/packet類型的套接字socket,將網(wǎng)卡設(shè)置為混雜模式。在通過setsockopt函數(shù)將BPF代碼拷貝到內(nèi)核,并attach到相關(guān)聯(lián)的socket套接字上。當(dāng)網(wǎng)卡接收到數(shù)據(jù)包的時候,因為設(shè)置的混雜模式,那么就會額外的拷貝一份新的數(shù)據(jù)包,然后在根據(jù)BPF的代碼進(jìn)行過濾,將符合規(guī)則的數(shù)據(jù)包接收到socket套接字的接收隊列里面。最后用戶程序就可以從這個接收隊列獲取到過濾后的數(shù)據(jù)包了。這類工具的實現(xiàn)流程就是大概這個樣子。
2.偽機器碼、BPF指令集、JIT
使用過tcpdump工具的應(yīng)該都見過在tcpdump命令后面會加一些表達(dá)式,用來表示過濾規(guī)則。
如:sudo tcpdump -d -i lo tcp and dst port 7070
注意不要以為這個表達(dá)式就是BPF程序了,其實這不是的。這個表達(dá)式是要經(jīng)過編譯過后才會變成BPF程序的。在我們早期是生產(chǎn)這類編譯器,那么是如何將這個表達(dá)式編譯出BPF指令集的呢?
tcpdump的實現(xiàn)是基于libcap庫的,tcpdump使用的過濾表達(dá)式是使用libcap庫進(jìn)行解析的,生成我們BPF指令集。那為什么沒有單獨做成一個這類的編譯器?究其原因就是但是的BPF框架使用的功能較少,只用在了網(wǎng)絡(luò)的數(shù)據(jù)包過濾方面。除此之外,當(dāng)時的BPF指令集個數(shù)很少,所以沒有必要花費大量的資源單獨做一個編譯器。但是隨著BPF的發(fā)展,指令集的復(fù)雜、支持的BPF程序類型越來越多,就急需要一個編譯器了。那這個就是我們后面將要提到的eBPF和clang/llvm編譯器了。
偽機器碼:假的機器碼,機器碼都是能夠在物理機上直接執(zhí)行的,偽機器碼不能夠直接執(zhí)行,需要在虛擬機上執(zhí)行。
BPF指令集:BPF指令集就是一個偽機器碼,是不能夠在物理機上直接執(zhí)行的,需要一個虛擬機才能夠執(zhí)行。我們都知道不同的處理器體系結(jié)構(gòu)有自己的不同指令集,這邊的BPF指令集可以理解為在BPF虛擬機上執(zhí)行的指令集。
JIT:just in time 的縮寫,我們將編譯好的BPF指令集需要在虛擬機上執(zhí)行,虛擬機需要一條一條的解析為本機機器碼才能夠執(zhí)行,所以這個執(zhí)行效率會很低,但是如果我們的處理器有了JIT就能夠?qū)⑽覀傿PF直接直接編譯為能夠在機器直接執(zhí)行的機器碼,這樣大大提高了執(zhí)行的速度。
3.eBPF介紹
eBPF是extend BPF的簡稱,擴展的BPF。我們剛了解BPF了,都知道BPF的功能比較單一只能夠作用于網(wǎng)路的數(shù)據(jù)包的過濾上,但是擴展后的BPF的功能得到了很大的豐富,可以這樣說基本上可以使用在Linux各個子系統(tǒng)中。除了功能上的擴展,BPF程序的指令集也變得相當(dāng)復(fù)雜了,所以就出現(xiàn)了專門用于編譯BPF程序的clang/llvm編譯。在框架上BPF的框架也發(fā)生了變化,所以擴展后的BPF不再是早期的BPF的可以比擬的。因而,早期的BPF被稱為cBPF,擴展后的BPF被稱為eBPF。
現(xiàn)在看下擴展后的BPF的框架,如下圖所示:
注意:我們后面說的BPF指的是cBPF和eBPF的統(tǒng)稱,除非特別說明。
雖然,框架發(fā)生變化,但是其基本的思想還沒有發(fā)生變化的。都是將BPF程序進(jìn)行編譯后生成字節(jié)碼,然后將BPF字節(jié)碼注入到內(nèi)核中,當(dāng)發(fā)生事件觸發(fā)的時候,我們就會執(zhí)行相應(yīng)的BPF程序。
現(xiàn)在,我們對cBPF和eBPF進(jìn)行對比:
一、cBPF支持的功能比較單一,只能夠作用于網(wǎng)絡(luò)的數(shù)據(jù)包的過濾上。而eBPF除了能夠支持網(wǎng)絡(luò)的數(shù)據(jù)包的過濾上,也支持其他的事件類型,如上圖中的XDP、Perf Event、kprobe、tracepoint等等。cBPF的功能在eBPF其實對應(yīng)的就是Socket的部分。
二、引入Map機制。在cBPF我們通過接收隊列將過濾后數(shù)據(jù)獲取出來,但是在eBPF我們可以將數(shù)據(jù)放到Map空間中。Map空間是用戶空間和內(nèi)核空間共享的,所以一般是在內(nèi)核中將數(shù)據(jù)存入到Map空間中,然后在用戶空間取出數(shù)據(jù)。
三、指令集變得更復(fù)雜了,與此同時,有了專門的用于編譯BPF字節(jié)碼的編譯器clang/llvm。
四、還有在安全機制方面等等一些改變。
4.eBPF類型和Map機制
首先,看下eBPF支持的類型,其中BPF_PROG_TYPE_SOCKET_FILTER對應(yīng)的就是早期cBPF的功能。只不過在eBPF中使用的框架不再是以前的cBPF的框架了,但是其實現(xiàn)的功能是一樣的。
bpf_prog_type | BPF prog 入口參數(shù)(R1) | 程序類型 |
---|---|---|
BPF_PROG_TYPE_SOCKET_FILTER | struct __sk_buff | 用于過濾進(jìn)出口網(wǎng)絡(luò)報文,功能上和 cBPF 類似。 |
BPF_PROG_TYPE_KPROBE | struct pt_regs | 用于 kprobe 功能的 BPF 代碼。 |
BPF_PROG_TYPE_TRACEPOINT | 這類 BPF 的參數(shù)比較特殊,根據(jù) tracepoint 位置的不同而不同。 | 用于在各個 tracepoint 節(jié)點運行。 |
BPF_PROG_TYPE_XDP | struct xdp_md | 用于控制 XDP(eXtreme Data Path)的 BPF 代碼。 |
BPF_PROG_TYPE_PERF_EVENT | struct bpf_perf_event_data | 用于定義 perf event 發(fā)生時回調(diào)的 BPF 代碼。 |
BPF_PROG_TYPE_CGROUP_SKB | struct __sk_buff | 用于在 network cgroup 中運行的 BPF 代碼。功能上和 Socket_Filter 近似。具體用法可以參考范例 test_cgrp2_attach。 |
BPF_PROG_TYPE_CGROUP_SOCK | struct bpf_sock | 另一個用于在 network cgroup 中運行的 BPF 代碼,范例 test_cgrp2_sock2 中就展示了一個利用 BPF 來控制 host 和 netns 間通信的例子。 |
Map機制的優(yōu)勢:
Map機制引入的原因,其中一個最大的原因就是通信。對于Map空間是用戶和內(nèi)核共享的,我們可以在內(nèi)核中將處理后的數(shù)據(jù)直接存入Map空間。然后,可以從用戶空間中進(jìn)行獲取。這樣就是大大方便了通信。除此之外,我們在內(nèi)核中進(jìn)行數(shù)據(jù)處理后,相應(yīng)的數(shù)據(jù)的占用的空間就會變小的很多,然后,在將數(shù)據(jù)存入到Map空間中。想比較于cBPF需要將數(shù)據(jù)獲取到后,在進(jìn)行處理,這樣可以大大節(jié)省存儲空間。
Map機制下的常見的數(shù)據(jù)類型:
Category | Source | Bpf_map_type | 用途 |
---|---|---|---|
Array | Arraymap.c | BPF_MAP_TYPE_ARRAY, BPF_MAP_TYPE_CGROUP_ARRAY, BPF_MAP_TYPE_PERF_EVENT_ARRAY BPF_MAP_TYPE_PERCPU_ARRAY BPF_MAP_TYPE_ARRAY_OF_MAPS | 實際就是數(shù)組,所以所有的 key 必須是整數(shù)。 |
BPF_MAP_TYPE_PROG_ARRAY | 該類型是一個特例,主要用于自定義函數(shù),利用 JUMP_TAIL_CALL令跳轉(zhuǎn) | ||
Hash | Hashmap.c | BPF_MAP_TYPE_HASH,BPF_MAP_TYPE_PERCPU_HASH BPF_MAP_TYPE_LRU_PERCPU_HASH BPF_MAP_TYPE_HASH_OF_MAPS | 真正意義上的 map 數(shù)據(jù)類型,如果 key 值為整數(shù)以外的類型必須使用 |
Stack Trace | Stackmap.c | BPF_MAP_TYPE_STACK_TRACE | 真正意義上的 map 數(shù)據(jù)類型,如果 key 值為整數(shù)以外的類型必須使用存儲特定應(yīng)用在某一特定時間點的棧狀態(tài)(包括內(nèi)核態(tài)和用戶態(tài)),key 只有兩個:分別為內(nèi)核棧 id 和用戶棧 id,利用 bpf_get_stackid()獲取; |
Longest Prefix Match Trie | Lpm_trie.c | BPF_MAP_TYPE_LPM_TRIE | 基于 Longest Prefix Match 前綴樹實現(xiàn),適宜處理以 CIBR 為鍵值時的情況 |
5.BPF程序編寫使用的語言
對于早期的cBPF程序的編寫,一般都是直接使用BPF指令集來編寫程序。像tcpdump這類工具,提供的使用方法可以類似于高級語言的人性化的表達(dá)式的使用,但是其實還是一樣的,只不過是讓libcap進(jìn)行解析了。BPF程序的編寫難度是極高的。
后來,由于BPF的擴展,急需要一種高級語言來編寫B(tài)PF程序,就出現(xiàn)了c語言的編程。通過c語言進(jìn)行編寫,然后,通過clang/llvm將c語言編譯為BPF字節(jié)碼,然后在注入到內(nèi)核中。但是對于注入的方式,還是需要通過自己手動的方式才能夠注入。
后來,就出現(xiàn)了BPF Compiler Collection(BCC),BCC 是一個 python 庫,但是其中有很大一部分的實現(xiàn)是基于 C 和 C++的,python 只不過實現(xiàn)了對 BCC 應(yīng)用層接口的封裝而已。使用bcc的最大好處是,用戶只需要關(guān)注BPF程序的設(shè)計,對于剩余的工作都不用管,包括編譯、解析 ELF、加載 BPF 代碼塊以及創(chuàng)建 map 等等基本可以由 BCC 一力承擔(dān),無需多勞開發(fā)者費心。
6.BPF工作原理總結(jié)
首先,看下BPF框架圖,如下圖所示:
一般我們都是通過c語言編寫B(tài)PF程序,然后通過clang/llvm編譯器,將BPF程序編譯為BPF字節(jié)碼。然后通過bpf系統(tǒng)調(diào)用,將BPF字節(jié)碼注入到內(nèi)核中,在注入的時候,我們必須要經(jīng)過BPF程序的驗證,來保證我們寫的BPF程序沒有問題,以防干掉我們的系統(tǒng)。然后,在判斷是否開啟了JIT,然后開啟了,還需要將BPF字節(jié)碼編譯為本機機器碼,以加快運行速度。
當(dāng)我們BPF程序attach的事件觸發(fā)了,就會執(zhí)行我們的BPF程序,然如是經(jīng)過JIT編譯過后的就能夠直接執(zhí)行,然后沒有開啟JIT就需要通過虛擬機進(jìn)行解析在執(zhí)行。在執(zhí)行BPF程序的過程中,會將需要保存的數(shù)據(jù)存儲到map空間中,用戶時候可以從map空間讀取出數(shù)據(jù)。BPF程序的大致流程就是這個樣子。
注意:BPF是基于事件觸發(fā)的。這是什么意思呢?
就是如果我們將BPF程序attach到某個事件上,當(dāng)這個事件觸發(fā)的時候,就會執(zhí)行我們這個BPF程序。其實這就是BPF的工作原理。
如,我們將BPF程序attach到kprobe類型的事件上,這個kprobe事件是個函數(shù),當(dāng)cpu執(zhí)行到這個函數(shù)的時候,就會觸發(fā)。然后就會執(zhí)行我們的BPF程序。
7.eBPF的作用
eBPF能夠用于內(nèi)核追蹤、應(yīng)用性能調(diào)優(yōu)/監(jiān)控、流量控制等方面,是非常有用的。
針對用于監(jiān)控、跟蹤使用的eBPF程序來說,主要是通過在內(nèi)核運行的過程中,來獲取內(nèi)核運行時的一些參數(shù)和統(tǒng)計信息。例如:系統(tǒng)調(diào)用的參數(shù)值、返回值,然后通過Map空間,將得到的信息傳遞給用戶態(tài)的程序,進(jìn)而可以在用戶程序中在進(jìn)行邏輯處理。
eBPF除了能夠獲取內(nèi)核運行的狀態(tài)信息,也能夠改變內(nèi)核的處理流程,可以在內(nèi)核某些路徑上加入直接的處理邏輯,來改變內(nèi)核的運行的流程。例如:XDP,就是在網(wǎng)卡驅(qū)動中,在進(jìn)入內(nèi)核協(xié)議棧之前插入eBPF的擴展的網(wǎng)絡(luò)包的過濾和轉(zhuǎn)發(fā)功能。
開發(fā)
直接 eBPF 編碼比較困難,人們通過框架來使用 eBPF。對于 tracing 來說,主要的框架就是 bcc 和 bpftrace,這兩個框架并不在內(nèi)核代碼中,他們在名為 iovisor 的 Linux Foundation project 中維護(hù)。
2. eBPF tracing 示例
tcplife
是一個基于 eBPF 的工具,可以顯示完整的 TCP session, 以及對應(yīng)的進(jìn)程號(PID) ,命令(COMM),收發(fā)字節(jié)數(shù)(TX_KB, RX_KB),以及時長(MS):
# tcplife
PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS
22597 recordProg 127.0.0.1 46644 127.0.0.1 28527 0 0 0.23
3277 redis-serv 127.0.0.1 28527 127.0.0.1 46644 0 0 0.28
22598 curl 100.66.3.172 61620 52.205.89.26 80 0 1 91.79
22604 curl 100.66.3.172 44400 52.204.43.121 80 0 1 121.38
22624 recordProg 127.0.0.1 46648 127.0.0.1 28527 0 0 0.22
3277 redis-serv 127.0.0.1 28527 127.0.0.1 46648 0 0 0.27
22647 recordProg 127.0.0.1 46650 127.0.0.1 28527 0 0 0.21
3277 redis-serv 127.0.0.1 28527 127.0.0.1 46650 0 0 0.26
并不是 eBPF 才使得這樣的工具成為可能,完全可以利用其他內(nèi)核技術(shù)特性重寫tcplife
。但是如果這么做了,我們將因為性能開銷、安全等因素而無法在生產(chǎn)環(huán)境中運行這個工具。eBPF 做的恰恰是讓這個工具變得實用,tcplife
是高效并且安全的。舉例來說,tcplife
并不會像其他內(nèi)核技術(shù)特性一樣去追蹤每個網(wǎng)絡(luò)包(packet),那樣會帶來太多的性能開銷。相反地,tcplife
只追蹤 TCP session 事件,這類事件相對來說頻率較低。這使得tcplife
的負(fù)載極低,以至于我們可以在生產(chǎn)環(huán)境中24小時持續(xù)運行這個工具。
3. 如何使用 eBPF
對于初學(xué)者,嘗試一下 bcc tools。bcc 的安裝很簡單,并有詳細(xì)的說明文檔,例如在 Ubuntu 上,效果如下
# sudo apt-get update
# sudo apt-get install bpfcc-tools
# sudo /usr/share/bcc/tools/opensnoop
PID COMM FD ERR PATH
25548 gnome-shell 33 0 /proc/self/stat
10190 opensnoop -1 2 /usr/lib/python2.7/encodings/ascii.x86_64-linux-gnu.so
10190 opensnoop -1 2 /usr/lib/python2.7/encodings/ascii.so
10190 opensnoop -1 2 /usr/lib/python2.7/encodings/asciimodule.so
10190 opensnoop 18 0 /usr/lib/python2.7/encodings/ascii.py
10190 opensnoop 19 0 /usr/lib/python2.7/encodings/ascii.pyc
25548 gnome-shell 33 0 /proc/self/stat
這里我通過運行 opensnoop
來驗證 bcc tools 是否工作,如果你順利到這一步,說明你已經(jīng)在使用 eBPF 了!
Netflix 和 Facebook 等公司已經(jīng)在服務(wù)器上默認(rèn)安裝 bcc ,或許你也想這么做。
4. 初學(xué)者入門教程
Brendan Gregg 提供了一個 bcc 入門教程,方便初學(xué)者很好地開始 eBPF tracing.
作為初學(xué)者,你不必開發(fā) eBPF 代碼。bcc 自帶超過 70 多個工具可以直接使用。bcc 入門教程里你將接觸 其中 11 個工具:execsnoop, opensnoop, ext4slower (or btrfs, xfs, zfs), biolatency, biosnoop, cachestat, tcpconnect, tcpaccept, tcpretrans, runqlat, and profile.
一旦你開始入門,你需要清楚 bcc tracing 工具還有很多:
這些工具都有很詳細(xì)的文檔,包括使用手冊和示例。示例文件(xxx_example.txt)展示了屏幕截圖和對應(yīng)的解釋:比如 biolatency_example.txt. Brendan Gregg 撰寫了許多類似的示例文檔、使用手冊、工具,都在 bcc repo 中。
生產(chǎn)環(huán)境中的 bcc tracing 示例沒有提供,Brendan Gregg 在撰寫這篇文檔時,eBPF 剛發(fā)展起來并只在測試機器上可用,因此大多數(shù)使用示例都是構(gòu)造的測試用例。之后這個教程里會提供真實世界的用例,這是初學(xué)者可以貢獻(xiàn)的方向:如果你通過 bcc 工具解決了一個實際問題,考慮發(fā)布一個博客文章來共享屏幕截圖,或者添加到 bcc repo 的 examples 文件中。
Intermediate
到這里,你應(yīng)該已經(jīng)可以運行 bcc 并嘗試了上述工具,然后你肯定會想定制開發(fā)自己的 bcc 工具。最佳實踐是切換到 bpftrace,bpftrace 提供高級語言可以使得入門開發(fā)更簡單。壞處就是 bpftrace 不如 bcc 那么拓展友好。因此,你最終還是會遇到瓶頸,繼而切換回 bcc。
參考 bpftrace 安裝說明,bpftrace 是不同于 bcc 的另一個項目。此時 bpftrace 還沒有在各個平臺打包發(fā)布。在不久的將來,可以很方便地通過類似 apt-get install bpftrace
的方式來安裝。
1. bpftrace 教程
Brendan Gregg 開發(fā)一個了 bpftrace 教程,通過一系列命令來學(xué)習(xí) bpftrace,一共有 12 個示例循序漸進(jìn)。
其中一個示例的截圖如下,這里使用 open
syscall tracepoint 來跟蹤 PID 和對應(yīng)的打開文件路徑。
# bpftrace -e 'tracepoint:syscalls:sys_enter_open { printf("%d %s\n", pid, str(args->filename)); }'
Attaching 1 probe...
181 /proc/cpuinfo
181 /proc/stat
1461 /proc/net/dev
1461 /proc/net/if_inet6
2. bpftrace 參考指南
關(guān)于 bpftrace 的更多參考信息,Brendan Gregg 整理了一個 參考指南,提供了關(guān)于 bpftrace 的語法、探針、內(nèi)建等的示例。
這個參考指南的目的很簡單:Brendan Gregg 盡可能地把主題、摘要、截圖都放在一個屏幕上。如果你查找一些東西,需要滾動瀏覽多頁,那就太長了。
3. bpftrace 示例
bpftrace repo 中有 20 多個工具,可以通過這些工具來學(xué)習(xí)開發(fā)。例如:
# cat tools/biolatency.bt
[...]
BEGIN
{
printf("Tracing block device I/O... Hit Ctrl-C to end.\n");
}
kprobe:blk_account_io_start
{
@start[arg0] = nsecs;
}
kprobe:blk_account_io_completion
/@start[arg0]/
{
@usecs = hist((nsecs - @start[arg0]) / 1000);
delete(@start[arg0]);
}
和 bcc 工具一樣,bpftrace 工具也有完整的使用手冊和示例文檔,例如 biolatency_example.txt。
Advanced
https://github.com/eunomia-bpf/bpf-developer-tutorial
Brendan Gregg 提供了兩個幫助文檔:
在 bcc/tools/*.py
中有很多示例。bcc tools 分為兩大部分:1)面向內(nèi)核的 BPF 代碼,用 C 語言開發(fā);2)用戶態(tài)工具,用 Python (lua, C++) 等語言開發(fā)。開發(fā) bcc tools 一定程度來說是高階的,可能會深入復(fù)雜的內(nèi)核或應(yīng)用程序的內(nèi)部。