如果覺得文章對你有所啟發,可以考慮用 🌟 支持 Gthulhu 專案,短期目標是集齊 300 個 🌟 藉此被 CNCF Landscape 採納 [ref]。
歡迎來到「30 篇文帶你用 eBPF 與 Golang 打造 Linux Scheduler」系列的第一篇!在這個系列中,我們將從零開始深入探索 eBPF(extended Berkeley Packet Filter)技術,並最終實現一個完整的 Linux 排程器。
eBPF 被譽為「Linux 內核的 JavaScript」,它革命性地改變了我們與 Linux 內核互動的方式。但要真正掌握 eBPF,我們必須先了解它的來龍去脈。今天,讓我們一起回顧 eBPF 的誕生與演進史,理解這項技術的設計哲學和發展脈絡。
故事要從 1992 年說起。當時,Steven McCanne 和 Van Jacobson 在加州大學柏克萊分校發表了一篇名為「The BSD Packet Filter: A New Architecture for User-level Packet Capture」的論文,提出了 Berkeley Packet Filter(BPF)的概念。
Classic BPF 的設計目標很簡單:在內核空間提供一個安全、高效的封包過濾機制。它的核心思想包括:
虛擬機器架構:BPF 定義了一個簡單的虛擬機器,包含:
有限的指令集:只支援基本的算術、邏輯和跳躍指令
安全性保證:
儘管 Classic BPF 在封包過濾方面表現出色,但隨著系統需求的複雜化,它的局限性也逐漸顯現:
2013 年,Alexei Starovoitov 開始著手改造 BPF。他的目標是建立一個更強大、更通用的內核程式設計平台。
PLUMgrid 是一家專注於網路虛擬化的公司,他們需要在內核中實現複雜的網路功能。傳統的方法需要修改內核原始碼或載入內核模組,這在雲端環境中是不現實的。
PLUMgrid 團隊意識到,如果能夠擴展 BPF 的能力,就能在不修改內核的情況下實現複雜的網路邏輯。於是他們開發了 iovisor.ko,這是 eBPF 的前身。
2014 年,Alexei Starovoitov 將 PLUMgrid 的工作整合到 Linux 內核中,創造了 Internal BPF(也稱為 eBPF)。這個新的 BPF 虛擬機器有以下特點:
64 位架構:
豐富的指令集:
多種程式類型:
筆者補充:
起初 eBPF 的應用偏向可觀測性以及網路封包處理,在近幾年才發展成可用於排程器程式,甚至是 TCP 壅塞演算法的開發。
eBPF 最重要的創新是引入了 Verifier(驗證器)。Verifier 在程式載入時進行靜態分析,確保程式的安全性:
// Verifier 檢查的項目包括:
// 1. 程式必須終止
// 2. 記憶體存取安全
// 3. 函數呼叫合法性
// 4. 暫存器狀態追蹤
static int check_func_call(struct bpf_verifier_env *env,
struct bpf_insn *insn, int *insn_idx)
{
int subprog = find_subprog(env, *insn_idx + insn->imm + 1);
// 檢查函數是否存在
if (subprog < 0) {
verbose(env, "function not found\n");
return -EINVAL;
}
// 檢查呼叫堆疊深度
if (env->subprog_cnt >= BPF_MAX_SUBPROGS) {
verbose(env, "too many subprograms\n");
return -E2BIG;
}
return 0;
}
eBPF 程式在通過 Verifier 檢查後,會被 JIT(Just-In-Time)編譯器編譯成原生機器碼,實現接近原生程式碼的執行效能。
eBPF Maps 提供了程式與使用者空間、程式與程式之間的資料共享機制:
// 定義一個 Hash Map
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10000);
__type(key, __u32);
__type(value, __u64);
} packet_count SEC(".maps");
eBPF 並不是一個獨立的功能,而是深度整合到 Linux 內核的各個子系統中:
網路子系統:
追蹤子系統:
安全子系統:
排程子系統:
eBPF 的設計遵循幾個重要原則:
讓我們梳理一下 eBPF 發展的重要時間點:
雖然這篇文章主要是理論介紹,但讓我們透過一些簡單的命令來觀察現代 Linux 系統中的 eBPF:
# 檢查內核是否支援 eBPF
grep CONFIG_BPF /boot/config-$(uname -r)
# 檢查 BTF 支援
ls -la /sys/kernel/btf/vmlinux
# 檢查可用的 BPF 程式類型
cat /proc/kallsyms | grep bpf_prog_type
# 安裝 bpftool(如果尚未安裝)
sudo apt update && sudo apt install linux-tools-common linux-tools-generic
# 列出所有已載入的 eBPF 程式
sudo bpftool prog list
# 列出所有 eBPF Maps
sudo bpftool map list
# 檢視特定程式的詳細資訊
sudo bpftool prog show id <PROG_ID> --pretty
# 檢視 eBPF 相關的內核統計
cat /proc/kallsyms | grep -E "bpf_|ebpf_" | wc -l
# 檢查 eBPF 程式的輸出
sudo cat /sys/kernel/debug/tracing/trace_pipe
傳統的內核模組開發需要管理員權限,並且一個錯誤可能導致整個系統崩潰。eBPF 透過 Verifier 提供了安全的內核程式設計環境:
// Verifier 確保這個程式是安全的
SEC("xdp")
int xdp_drop_packets(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
// Verifier 檢查邊界
if (eth + 1 > data_end)
return XDP_PASS;
// 只能返回預定義的值
if (eth->h_proto == bpf_htons(ETH_P_IP))
return XDP_DROP;
return XDP_PASS;
}
eBPF 程式執行在內核空間,避免了使用者空間與內核空間的切換開銷:
eBPF 讓系統可觀測性達到了前所未有的程度:
// 追蹤系統呼叫,且開銷非常低
SEC("tracepoint/syscalls/sys_enter_openat")
int trace_openat(struct trace_event_raw_sys_enter *ctx)
{
char filename[256];
bpf_probe_read_user_str(filename, sizeof(filename),
(void *)ctx->args[1]);
bpf_printk("Process %d opening file: %s\n",
bpf_get_current_pid_tgid() >> 32, filename);
return 0;
}
筆者補充:
這邊 Copilot 給的範例比較古老,自 Linux v5.5 開始支援了更接近 zero overhead 的方式(利用 eBPF Trampolines),請參考:
eBPF 的設計使它被多家科技公司採納,也因此得以在 kernel 社群中快速成長。我們可以預期 eBPF 在未來將被應用在 kernel 中的各個領域,以更安全、高效的方式提供給開發者一個全新的選擇。
在下一篇文章中,我們將深入探討 eBPF 的核心架構,包括虛擬機器、指令集、Verifier 機制等。這些知識將為我們後續的實作打下堅實的基礎。