Day 2:
- 原文:https://ithelp.ithome.com.tw/articles/10377305
- 發布日期:2025-09-12
如果覺得文章對你有所啟發,可以考慮用 🌟 支持 Gthulhu 專案,短期目標是集齊 300 個 🌟 藉此被 CNCF Landscape 採納 [ref]。
前言
歡迎來到「30 篇文帶你用 eBPF 與 Golang 打造 Linux Scheduler」系列的第一篇!在這個系列中,我們將從零開始深入探索 eBPF(extended Berkeley Packet Filter)技術,並最終實現一個完整的 Linux 排程器。
eBPF 被譽為「Linux 內核的 JavaScript」,它革命性地改變了我們與 Linux 內核互動的方式。但要真正掌握 eBPF,我們必須先了解它的來龍去脈。今天,讓我們一起回顧 eBPF 的誕生與演進史,理解這項技術的設計哲學和發展脈絡。
理論基礎
Classic BPF 的誕生(1992年)
故事要從 1992 年說起。當時,Steven McCanne 和 Van Jacobson 在加州大學柏克萊分校發表了一篇名為「The BSD Packet Filter: A New Architecture for User-level Packet Capture」的論文,提出了 Berkeley Packet Filter(BPF)的概念。
Classic BPF 的設計目標很簡單:在內核空間提供一個安全、高效的封包過濾機制。它的核心思想包括:
-
虛擬機器架構:BPF 定義了一個簡單的虛擬機器,包含:
- 32 位累加器(A)
- 32 位索引暫存器(X)
- 16 個 32 位記憶體位置
- 程式計數器
-
有限的指令集:只支援基本的算術、邏輯和跳躍指令
-
安全性保證:
- 程式必須終止(不允許無限迴圈)
- 只能讀取封包資料,不能修改
- 記憶體存取範圍受限
從 Classic BPF 到 eBPF 的轉變
儘管 Classic BPF 在封包過濾方面表現出色,但隨著系統需求的複雜化,它的局限性也逐漸顯現:
- 功能受限:只能用於封包過濾
- 程式大小限制:最多 4096 條指令
- 缺乏現代特性:沒有函數呼叫、迴圈控制等
2013 年,Alexei Starovoitov 開始著手改造 BPF。他的目標是建立一個更強大、更通用的內核程式設計平台。
PLUMgrid 的貢獻
PLUMgrid 是一家專注於網路虛擬化的公司,他們需要在內核中實現複雜的網路功能。傳統的方法需要修改內核原始碼或載入內核模組,這在雲端環境中是不現實的。
PLUMgrid 團隊意識到,如果能夠擴展 BPF 的能力,就能在不修改內核的情況下實現複雜的網路邏輯。於是他們開發了 iovisor.ko,這是 eBPF 的前身。
Internal BPF 的誕生
2014 年,Alexei Starovoitov 將 PLUMgrid 的工作整合到 Linux 內核中,創造了 Internal BPF(也稱為 eBPF)。這個新的 BPF 虛擬機器有以下特點:
-
64 位架構:
- 10 個 64 位暫存器(r0-r9)
- 512 字節的堆疊空間
- 更大的程式空間
-
豐富的指令集:
- 支援 64 位算術運算
- 原子操作
- 函數呼叫
-
多種程式類型:
- 網路程式(XDP、TC)
- 追蹤程式(kprobe、tracepoint)
- 排程器程式(sched_ext)
筆者補充:
起初 eBPF 的應用偏向可觀測性以及網路封包處理,在近幾年才發展成可用於排程器程式,甚至是 TCP 壅塞演算法的開發。
eBPF 的技術突破
1. Verifier 機制
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;
}
2. JIT 編譯
eBPF 程式在通過 Verifier 檢查後,會被 JIT(Just-In-Time)編譯器編譯成原生機器碼,實現接近原生程式碼的執行效能。
3. Maps 機制
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 Kernel 中的地位
核心子系統整合
eBPF 並不是一個獨立的功能,而是深度整合到 Linux 內核的各個子系統中:
-
網路子系統:
- XDP(eXpress Data Path)
- TC(Traffic Control)
- Socket filters
-
追蹤子系統:
- kprobes/kretprobes
- tracepoints
- perf events
-
安全子系統:
- LSM(Linux Security Modules)
- seccomp-bpf
-
排程子系統:
- sched_ext(Linux 6.12+)
設計哲學
eBPF 的設計遵循幾個重要原則:
- 安全第一:所有程式必須通過 Verifier 檢查
- 效能導向:JIT 編譯確保高效執行
- 通用性:一套機制支援多種應用場景
- 向後相容:與 Classic BPF 保持相容性
重要里程碑
讓我們梳理一下 eBPF 發展的重要時間點:
- 1992年:Classic BPF 論文發表
- 2013年:Alexei Starovoitov 開始 eBPF 開發
- 2014年:eBPF 合併到 Linux 3.15
- 2016年:XDP 框架引入(Linux 4.8)
- 2018年:BTF(BPF Type Format)引入
- 2019年:BPF_PROG_TYPE_TRACING 引入
- 2024年:sched_ext 合併到 Linux 6.12
實戰應用:觀察 eBPF 系統
雖然這篇文章主要是理論介紹,但讓我們透過一些簡單的命令來觀察現代 Linux 系統中的 eBPF:
檢查 eBPF 支援
# 檢查內核是否支援 eBPF
grep CONFIG_BPF /boot/config-$(uname -r)
# 檢查 BTF 支援
ls -la /sys/kernel/btf/vmlinux
# 檢查可用的 BPF 程式類型
cat /proc/kallsyms | grep bpf_prog_type
觀察已載入的 eBPF 程式
# 安裝 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 系統統計
# 檢視 eBPF 相關的內核統計
cat /proc/kallsyms | grep -E "bpf_|ebpf_" | wc -l
# 檢查 eBPF 程式的輸出
sudo cat /sys/kernel/debug/tracing/trace_pipe
深度分析:為什麼 eBPF 如此重要?
1. 安全性與隔離性
傳統的內核模組開發需要管理員權限,並且一個錯誤可能導致整個系統崩潰。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;
}
2. 效能優勢
eBPF 程式執行在內核空間,避免了使用者空間與內核空間的切換開銷:
- 零拷貝:直接在內核處理資料
- JIT 編譯:接近原生程式碼的效能
- 批次處理:可以批次處理多個事件
3. 可觀測性革命
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 機制等。這些知識將為我們後續的實作打下堅實的基礎。