歡迎光臨
每天分享高質量文章

關於 BPF 和 eBPF 的筆記 | Linux 中國

在 BPF 出現之前,如果你想去做包過濾,你必須複製所有的包到使用者空間,然後才能去過濾它們
— Julia Evans


致謝
編譯自 | https://jvns.ca/blog/2017/06/28/notes-on-bpf—ebpf/ 
 作者 | Julia Evans
 譯者 | qhwdw ? ? ? ? ? 共計翻譯:106 篇 貢獻時間:193 天

今天,我喜歡的 meetup 網站上有一篇我超愛的文章!Suchakra Sharma[1]@tuxology[2] 在 twitter/github)的一篇非常棒的關於傳統 BPF 和在 Linux 中最新加入的 eBPF 的討論文章,正是它促使我想去寫一個 eBPF 的程式!

這篇文章就是 —— BSD 包過濾器:一個新的使用者級包捕獲架構[3]

我想在討論的基礎上去寫一些筆記,因為,我覺得它超級棒!

開始前,這裡有個 幻燈片[4] 和一個 pdf[5]。這個 pdf 非常好,結束的位置有一些連結,在 PDF 中你可以直接點選這個連結。

什麼是 BPF?

在 BPF 出現之前,如果你想去做包過濾,你必須複製所有的包到使用者空間,然後才能去過濾它們(使用 “tap”)。

這樣做存在兩個問題:

☉ 如果你在使用者空間中過濾,意味著你將複製所有的包到使用者空間,複製資料的代價是很昂貴的。
☉ 使用的過濾演演算法很低效。

問題 #1 的解決方法似乎很明顯,就是將過濾邏輯移到核心中。(雖然具體實現的細節並沒有明確,我們將在稍後討論)

但是,為什麼過濾演演算法會很低效?

如果你執行 tcpdump host foo,它實際上運行了一個相當複雜的查詢,用下圖的這個樹來描述它:

評估這個樹有點複雜。因此,可以用一種更簡單的方式來表示這個樹,像這樣:

然後,如果你設定 ether.type = IP 和  ip.src = foo,你必然明白匹配的包是 host foo,你也不用去檢查任何其它的東西了。因此,這個資料結構(它們稱為“控制流圖” ,或者 “CFG”)是表示你真實希望去執行匹配檢查的程式的最佳方法,而不是用前面的樹。

為什麼 BPF 要工作在核心中

這裡的關鍵點是,包僅僅是個位元組的陣列。BPF 程式是執行在這些位元組的陣列之上。它們不允許有迴圈(loop),但是,它們 可以  有聰明的辦法知道 IP 包頭(IPv6 和 IPv4 長度是不同的)以及基於它們的長度來找到 TCP 埠:

  1. x = ip_essay-header_length

  2. port = *(packet_start + x + port_offset)

(看起來不一樣,其實它們基本上都相同)。在這個論文/幻燈片上有一個非常詳細的虛擬機器的描述,因此,我不打算解釋它。

當你執行 tcpdump host foo 後,這時發生了什麼?就我的理解,應該是如下的過程。

☉ 轉換 host foo 為一個高效的 DAG 規則
☉ 轉換那個 DAG 規則為 BPF 虛擬機器的一個 BPF 程式(BPF 位元組碼)
☉ 傳送 BPF 位元組碼到 Linux 核心,由 Linux 核心驗證它
☉ 編譯這個 BPF 位元組碼程式為一個原生native程式碼。例如,這是個ARM 上的 JIT 程式碼[6] 以及 x86[7] 的機器碼
☉ 當包進入時,Linux 執行原生程式碼去決定是否過濾這個包。對於每個需要去處理的包,它通常僅需執行 100 – 200 個 CPU 指令就可以完成,這個速度是非常快的!

現狀:eBPF

畢竟 BPF 出現已經有很長的時間了!現在,我們可以擁有一個更加令人激動的東西,它就是 eBPF。我以前聽說過 eBPF,但是,我覺得像這樣把這些片斷拼在一起更好(我在 4 月份的 netdev 上我寫了這篇 XDP & eBPF 的文章[8]回覆)

關於 eBPF 的一些事實是:

◈ eBPF 程式有它們自己的位元組碼語言,並且從那個位元組碼語言編譯成核心原生程式碼,就像 BPF 程式一樣
◈ eBPF 執行在核心中
◈ eBPF 程式不能隨心所欲的訪問核心記憶體。而是透過核心提供的函式去取得一些受嚴格限制的所需要的內容的子集
◈ 它們  可以  與使用者空間的程式透過 BPF 對映進行通訊
◈ 這是 Linux 3.18 的 bpf 系統呼叫

kprobes 和 eBPF

你可以在 Linux 核心中挑選一個函式(任意函式),然後執行一個你寫的每次該函式被呼叫時都執行的程式。這樣看起來是不是很神奇。

例如:這裡有一個 名為 disksnoop 的 BPF 程式[9],它的功能是當你開始/完成寫入一個塊到磁碟時,觸發它執行跟蹤。下圖是它的程式碼片斷:

  1. BPF_HASH(start, struct request *);

  2. void trace_start(struct pt_regs *ctx, struct request *req) {

  3.    // stash start timestamp by request ptr

  4.    u64 ts = bpf_ktime_get_ns();

  5.    start.update(&req, &ts);

  6. }

  7. ...

  8. b.attach_kprobe(event="blk_start_request", fn_name="trace_start")

  9. b.attach_kprobe(event="blk_mq_start_request", fn_name="trace_start")

本質上它宣告一個 BPF 雜湊(它的作用是當請求開始/完成時,這個程式去觸發跟蹤),一個名為 trace_start 的函式將被編譯進 BPF 位元組碼,然後附加 trace_start 到核心函式 blk_start_request 上。

這裡使用的是 bcc 框架,它可以讓你寫 Python 式的程式去生成 BPF 程式碼。你可以在 https://github.com/iovisor/bcc 找到它(那裡有非常多的示例程式)。

uprobes 和 eBPF

因為我知道可以附加 eBPF 程式到核心函式上,但是,我不知道能否將 eBPF 程式附加到使用者空間函式上!那會有更多令人激動的事情。這是 在 Python 中使用一個 eBPF 程式去計數 malloc 呼叫的示例[11]

附加 eBPF 程式時應該考慮的事情

◈ 帶 XDP 的網絡卡(我之前寫過關於這方面的文章)
◈ tc egress/ingress (在網路棧上)
◈ kprobes(任意核心函式)
◈ uprobes(很明顯,任意使用者空間函式??像帶除錯符號的任意 C 程式)
◈ probes 是為 dtrace 構建的名為 “USDT probes” 的探針(像 這些 mysql 探針[12])。這是一個 使用 dtrace 探針的示例程式[13]
◈ JVM[14]
◈ 跟蹤點
◈ seccomp / landlock 安全相關的事情
◈ 等等

這個討論超級棒

在幻燈片裡有很多非常好的連結,並且在  iovisor 倉庫裡有個 LINKS.md[15]。雖然現在已經很晚了,但是我馬上要去寫我的第一個 eBPF 程式了!


via: https://jvns.ca/blog/2017/06/28/notes-on-bpf---ebpf/

作者:Julia Evans [17] 譯者:qhwdw 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

贊(0)

分享創造快樂