iT邦幫忙

2025 iThome 鐵人賽

DAY 2
1

如果覺得文章對你有所啟發,可以考慮用 🌟 支持 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 的設計目標很簡單:在內核空間提供一個安全、高效的封包過濾機制。它的核心思想包括:

  1. 虛擬機器架構:BPF 定義了一個簡單的虛擬機器,包含:

    • 32 位累加器(A)
    • 32 位索引暫存器(X)
    • 16 個 32 位記憶體位置
    • 程式計數器
  2. 有限的指令集:只支援基本的算術、邏輯和跳躍指令

  3. 安全性保證

    • 程式必須終止(不允許無限迴圈)
    • 只能讀取封包資料,不能修改
    • 記憶體存取範圍受限

從 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 虛擬機器有以下特點:

  1. 64 位架構

    • 10 個 64 位暫存器(r0-r9)
    • 512 字節的堆疊空間
    • 更大的程式空間
  2. 豐富的指令集

    • 支援 64 位算術運算
    • 原子操作
    • 函數呼叫
  3. 多種程式類型

    • 網路程式(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 內核的各個子系統中:

  1. 網路子系統

    • XDP(eXpress Data Path)
    • TC(Traffic Control)
    • Socket filters
  2. 追蹤子系統

    • kprobes/kretprobes
    • tracepoints
    • perf events
  3. 安全子系統

    • LSM(Linux Security Modules)
    • seccomp-bpf
  4. 排程子系統

    • sched_ext(Linux 6.12+)

設計哲學

eBPF 的設計遵循幾個重要原則:

  1. 安全第一:所有程式必須通過 Verifier 檢查
  2. 效能導向:JIT 編譯確保高效執行
  3. 通用性:一套機制支援多種應用場景
  4. 向後相容:與 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),請參考:

  1. https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_TRACING/
  2. https://docs.ebpf.io/linux/concepts/trampolines/

總結

eBPF 的設計使它被多家科技公司採納,也因此得以在 kernel 社群中快速成長。我們可以預期 eBPF 在未來將被應用在 kernel 中的各個領域,以更安全、高效的方式提供給開發者一個全新的選擇。
在下一篇文章中,我們將深入探討 eBPF 的核心架構,包括虛擬機器、指令集、Verifier 機制等。這些知識將為我們後續的實作打下堅實的基礎。


上一篇
30 篇文帶你用 eBPF 與 Golang 打造 Linux Scheduler
下一篇
eBPF 架構深度解析
系列文
30 篇文帶你用 eBPF 與 Golang 打造 Linux Scheduler3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言