【Learning eBPF-2】eBPF 的“Hello world”

_hong發表於2024-04-01

前一章講了 eBPF 為什麼這麼吊,不理解沒關係,現在開始,我們透過一個 “Hello world” 例子,來真正入門一下。

BCC Python 框架是上手 eBPF 的最友好方式。來看。

2.1 BCC 的 Hello World

下面的程式是一段 BCC 框架的 Hello World 程式。

#!/usr/bin/python3
from bcc import BPF

program = r"""
int hello(void *ctx) {
	bpf_trace_printk("Hello World!\n");
	return 0;
}
"""

b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")

b.trace_print()

這段程式包含了兩部分:

  • 執行在核心態的 eBPF 程式本身(hello());
  • 執行在使用者態的,用於載入 eBPF 程式到核心空間並讀取它生成的 trace 控制程式(hello.py)。

下圖顯示了這段程式碼執行時的狀態。

【Learning eBPF-2】eBPF 的“Hello world”

下面來逐行解釋這段程式碼。

第一行告訴你,這是一個 Python 程式。實際上 #!/usr/bin/python3 是指定預設的 Python 直譯器。

eBPF 程式本身是 C 語言編寫的。這部分程式碼為:

int hello(void *ctx) {
	bpf_trace_printk("Hello World!");
	return 0;
}

其中,bpf_trace_printk() 是 eBPF 輔助函式,用於列印一條訊息。有關輔助函式的更多討論,見第 5 章。

這段 eBPF 程式是以靜態字串 program 的形式被定義在 Python 指令碼中,並作為引數,傳遞給 BPF 物件:

b = BPF(text=program)

當然,C 程式最終會由 BCC 框架負責編譯執行。

eBPF 程式需要繫結到一個事件上。在這個例子中,我們選擇的事件為 execve 系統呼叫。當有任何應用程式執行時,都會呼叫 execve(),從而觸發我們繫結的 eBPF。然而,execve 系統呼叫在不同架構的 Linux 上可能會有不同的實現方式。但是,eBPF 提供了一種非常方便的方式(透過名稱)來尋找當前支援的系統呼叫,就像這樣:

syscall = b.get_syscall_fnname("execve")

現在,變數 syscall 指代了系統呼叫。接下來,使用一個探針 kprobe(詳見第 1 章)來將 hello() 函式繫結到 execve 事件上。

b.attach_kprobe(event=syscall, fn_name="hello")

此時,eBPF 程式已經被成功載入到核心,並完成了繫結。那麼,當有一個程序被執行時,將觸發這段 hello() 程式,完成一條訊息的列印。剩下的工作,就是去讀取 trace 的輸出,並列印到標準輸出中。

b.trace_print()

trace_print()函式將進入無限迴圈,直到你鍵入Ctrl+C終止這段 eBPF 程式。

下面這張圖顯示了這段 eBPF 程式的執行原理:

【Learning eBPF-2】eBPF 的“Hello world”

根據這張圖回顧一下整個流程。

1)這段 Python 程式編譯了 C 程式碼,載入核心,並與 execve() 完成繫結。

2)當有其他程序執行時,執行 execve() 系統呼叫,觸發 eBPF 中的 hello() 程式段,列印一行輸出(在 pipe 中,後文會再次提到)。

3)使用者態的程式讀取這些輸出,並列印到螢幕上。

2.2 執行 Hello World

執行這段程式,其結果取決於你當前的執行環境正在或即將執行的程序。

如果這段程式碼啥也沒輸出,請再起一個終端,手動執行一個程式。eBPF 將列印一行行的 Hello world 訊息。

書裡沒有提到,但是很重要,執行 BCC 框架的 eBPF 程式,需要先安裝 bcc-python 庫。譯者使用 REHL8-x86 作業系統,因此透過 yum 包管理器來安裝: yum install -y python3-bcc.x86_64

【Learning eBPF-2】eBPF 的“Hello world”

這裡書中再次強調,eBPF 程式是立即生效的。首先是不需要重啟,其次是對應用程式無侵入(已經重複很多遍了)。這是因為,eBPF 所繫結的是 execve() 系統呼叫,因此和應用程式沒關係。即使你寫了一個指令碼,手動呼叫這個系統呼叫,那麼,這個 eBPF 也會觸發。

列印輸出除了 “Hello World” 字串以外,還有其他資訊。例如,執行 execve 的程序 ID 為 5412,並使用了 bash 命令等等。Python 程式從哪裡讀取這個輸出資訊的呢?實際上,bpf_trace_printk() 輔助函式會把列印寫入 /sys/kernel/debug/tracing/trace_pipe 檔案中。你可以透過 cat 指令來檢視(需要 root 許可權)。

eBPF 程式使用這種方式列印資訊,雖然簡單,但卻有下面兩點侷限性:

  • 僅支援字串型別的輸出。你想傳結構體型別?沒門。
  • trace_pipe 檔案只有這一個。也就是說,所有正在執行的 eBPF 都會把輸出寫入到這裡。難受吧!

那麼,有沒有一種更好的方式傳遞資料呢??答案就是:eBPF 對映(maps)。

2.3 eBPF 對映:maps

對映 maps 是 eBPF 的擴充套件功能,它是一類可以讓 eBPF 程式和使用者態程式訪問的資料結構

maps 支援核心態 eBPF 之間的通訊,也支援 eBPF 到使用者態程式之間的通訊。主要的作用包括以下幾種:

  • 使用者空間寫入需要由 eBPF 程式檢索的配置資訊。
  • 一個 eBPF 儲存狀態,以供另一個 eBPF 程式(或者同一 eBPF 的後續指令)使用。
  • eBPF 程式將資料寫入 maps ,以供使用者空間應用程式讀取,從而列印結果。

eBPF maps 有很多種型別,在 uapi/linux/bpf.h 檔案中可以檢視,核心文件中也有相關的介紹。

通常,eBPF maps 都是鍵值對型別結構,但具體 keyvalue 的指代和形式又有所區別。本章,將主要介紹 hashperfring buffer 以及 eBPF 程式陣列

誠然,eBPF maps 不止這些。

有些 map,形似陣列,但其 key 小得僅有 4 位元組;【array】

有些 map,如雜湊表,key的種類能夠包羅永珍;【hash】

有些 map,便利操作,或 FIFO 列隊而伺,或 FILO 作棧而生;或 LRU 行冷熱資料分離,或 LPM 做最長字首匹配;【queue、stack、lru、lpm、Bloom filter】

有些 map,特殊物件專用,拓寬網路和尾呼叫的技術;【sockmaps、devmaps、program array、map-of-map】

有些 map,對應CPU核心,尋求併發操作的可能性。【cpu-*】

接下來的例子,我們來看一下使用雜湊表型別的 map 基本用法。

2.3.1 雜湊表 map

在上一個給出的例子中,我們的 eBPF 程式繫結了 execve() 系統呼叫。接下來,要用雜湊表 HASH 做一下改編,key用來儲存使用者 ID,value 用來記錄某個使用者下的程序執行呼叫 execve() 的次數。這個程式統計了不同的使用者分別執行了多少個程式。

來看這個 eBPF 程式的 C 程式碼。

BPF_HASH(counter_table); 				// A

int hello(void *ctx) {
    u64 uid;
    u64 counter = 0;
    u64 *p;
    
    uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;	// B
    p = counter_table.lookup(&uid);			// C
    if (p != 0) {					// D
        counter = *p;
    }
    counter++;						// E
    counter_table.update(&uid, &counter);		// F
    return 0;
}

程式碼解釋:

【A】BPF_HASH() 是一個 BCC 宏宣告的雜湊表。

【B】bpf_get_current_uid_gid() 是一個輔助函式,用來獲取當前程序的使用者 ID。這個輔助函式返回值是一個 64 位的值,其中,使用者 ID 儲存在低 32 位(高 32 位為使用者組 ID)。

【C】透過 key 查詢雜湊表中的 value。這裡是透過 uid 查詢 p。返回一個指標。

【D】如果指定的 uid ,在雜湊表中存在一個 p,將雜湊表中的 p 值設定給 counter;若雜湊表中不存在對應 uidpcounter 的值將為預設值 0

【E】無論 counter 值為多少,在這裡都對其進行自增操作。

【F】使用新的 counter 值,更新對應 uid 的雜湊表。

我們仔細看一下這兩行程式碼。首先是查詢雜湊表 value

p = counter_table.lookup(&uid);

然後是更新雜湊表:

counter_table.update(&uid, &counter);

你可能會有點疑問了:C 語言能這麼寫?結構體可以直接呼叫成員函式?不對吧?實際上,你是對的,C 語言確實不支援在結構體中定義這樣的函式。但是,BCC 框架中的 C,實際上是一種不嚴格的 C。BCC 在真正執行 C 程式碼的編譯前,會重寫這些不嚴格的語法(實際上是透過若干個 BCC 宏來實現的)。

接下來,和前面的例子一樣,將這段 C 程式宣告為一個 program 字串,然後透過 BCC 將其編譯載入核心,並繫結在 execve() 系統呼叫上。

b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")

但這次,還需要一些額外的工作,在使用者態中讀取雜湊表的內容。

while true:						# A
    sleep(2)
    s = ""
    for k, v in b["counter_table"].items():		# B
        s += f"ID {k.value}: {v.value}\t"
    print(s)

程式碼解釋:

【A】無限迴圈。每隔 2s 列印輸出。

【B】BCC 框架會自動建立一個 Python 物件來指代雜湊表。這個迴圈將會遍歷 eBPF 定義的 counter_table 雜湊表中的所有鍵值對,然後完成列印。

執行這段程式,你需要兩個終端。終端 1 執行 eBPF 程式,終端 2 執行指令。

【Learning eBPF-2】eBPF 的“Hello world”

可以看到,每 2s 輸出一行。我們關注最後一行的兩個鍵值對:

  • key = 501, value = 5
  • key = 0, value = 2

在第二個終端裡,作者的使用者 ID 為 501。當執行 ls 命令時,值為 501 的 uid 計數器自增 1。而當執行 sudo ls 時,發生了兩次 execve()。第一次是在 501 使用者下的 sudo 命令,第二次是在 root 使用者下的 ls 命令。

這個例子給出了使用雜湊表 map 從核心態向使用者態傳遞資料的方式。當然,你也可以使用陣列型別的 map 來實現這個功能(因為 key 為整數)。

Linux 核心中存在一個 名為perf 的子系統,也可以傳遞核心態資料到使用者空間,eBPF 剛好也支援這種方式。我們來看一下。

2.3.2 Perf 和 Ring buffer map

在這一小節中,我們再來看一種更復雜的 “Hello World” BCC 程式,它使用了 Perf 環形緩衝區,用來向使用者態傳遞自定義的資料結構。

環形緩衝區:核心 5.8 版本才引入的結構,在這之前為普通的基於共享記憶體的緩衝區。實際上 perf 環形緩衝區更有優勢,具體可以參考 Andrii Nakryiko 的這篇部落格: https://nakryiko.com/posts/bpf-ringbuf/

那麼,問題來了,什麼是環形緩衝區?

環形緩衝區 是一種資料結構,它不是 eBPF 獨有的。環形緩衝區實際上是一段記憶體空間,其空間中的地址在邏輯上首尾相連成環。環形緩衝區包括兩個工作指標,一個負責讀,一個負責寫,二者同向移動。寫指標指向的位置就是下個資料被寫入的位置(資料可以任意長度,其長度資訊包含在資料頭中),同理,讀指標指向的位置就是下一個需要讀取的資料開頭(根據資料頭中的長度,控制讀指標移動距離)。

下圖直觀的展示了環形緩衝區的樣貌。

【Learning eBPF-2】eBPF 的“Hello world”

讀指標和寫指標始終朝著一個方向運動。若在某一時刻,讀指標追上了寫指標,則說明緩衝區沒資料可讀了。相反,若寫指標追上了讀指標,則說明緩衝區沒空間可寫了,那麼此時,需要寫入的資料就會被丟棄(丟棄計數器會增加)。如果你控制的好,讀寫指標以相同的速率運動,始終不會相遇,那麼恭喜你,你便擁有了一個無限大的迴圈緩衝區可以使用。

瞭解了環形緩衝區的概念後,我們再來改進一下之前繫結到 execve() 的 eBPF 程式,來實時列印執行程序的簡單資訊。

BPF_PERF_OUTPUT(output);							// A

struct data_t {									// B
    int pid;
    int uid;
    char command[16];
    char message[12];
};

int hello(void *ctx) {
    struct data_t data = {};							// C
    char message[12] = "Hello World";

    dara.pid = bpf_get_current_pid_tgid() >> 32;				// D
    data.uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;				// E

    bpf_get_current_comm(&data.command, sizeof(data.command));			// F
    bpf_probe_read_kernel(&data.message, sizeof(data.message), message);	// G

    output.perf_submit(ctx, &data, sizeof(data));				// H
    return 0;
}

程式碼解釋:

【A】BCC 框架宣告瞭一個宏定義 BPF_PERF_OUTPUT ,用來建立一個 perf 對映區域,以便核心態可以向使用者態傳遞訊息。這裡定義為 output

【B】每次 hello() 執行之時,都會填充一個結構體來儲存關鍵欄位。這是結構體定義,包括程序 ID、使用者 ID、當前執行指令名稱以及 message 資訊。

【C】data 被定義為本地變數,message 被賦值為 "Hello world" 字串。

【D】bpf_get_current_pid_tgid() ,輔助函式,用於獲取觸發當前 eBPF 程式的程序 ID。該函式返回一個 64 位的值,高 32 位是程序 ID(低 32 位為執行緒組 ID,對於單執行緒的程序,同為程序 ID)。

【E】bpf_get_current_uid_gid(),輔助函式,前文介紹過,用於獲取使用者 ID。

【F】bpf_get_current_comm(),輔助函式,用於獲取當前執行的指令名稱。

在 C 語言中,你不可以直接使用 "=" 賦值字串,你需要傳入一個待寫入字串的地址。

【G】這個例子中,message = "Hello World"bpf_probe_read_kernel() 輔助函式會將它複製到 data 結構體的對應位置。

【H】此時,data 結構體中已經填充了 piduidcommand[] 以及 message[]。這裡呼叫 output.perf_submit()data 結構體提交到 map 中。

接下來,與第一個 “Hello World” 程式類似,這一段 C 程式將被定義為一段字串 program,下面是 Python 程式碼。

b = BPF(text=program)							# A
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")

def print_event(cpu, data, size):					# B
    data = b["output"].event(data)
    print(f"{data.pid} {data.uid} {data.command.decode()} {data.message.decode()}")

b["output"].open_perf_buffer(print_event)				# C
while True:								![image](uploading...)# D
    b.perf_buffer_poll()

程式碼解釋:

【A】編譯、載入、繫結 eBPF C程式。不再贅述。

【B】print_event()是一個回撥函式,用於將 data 的內容列印到螢幕上。BCC 已經做了很多復繁重的工作,因此你只需要簡單的 b["output"].event() 來從核心態 map 中獲取資料。

【C】b["output"].open_perf_buffer() 用於開啟 perf ring buffer。該函式接收 print_event 引數,是將其宣告為一個回撥。即,當 perf ring buffer 中有資料時,觸發回撥,列印這個資料。

【D】無限迴圈,呼叫 perf_buffer_poll() 拉取 perf ring buffer 內容。

執行這段程式,你能得到以下輸出:

【Learning eBPF-2】eBPF 的“Hello world”

和以前一樣,你可能需要另起一個終端,執行命令,來驗證你的程式。

這個例子和最初的 “Hello World” 程式最大的不同就是,我們不再使用有限的 trace pipe 傳遞資料,而使用了 perf ring buffer。執行原理通先前也有了些許區別,如下圖所示。

【Learning eBPF-2】eBPF 的“Hello world”

透過環形緩衝區傳遞資料會不會仍然使用了 trace pipe 呢?你可以執行一下命令檢驗一下:

cat /sys/kernel/debug/tracing/trace_pipe

這個例子還給出了一些輔助函式的使用示例,第 7 章我們會更加詳細討論。

這些輔助函式主要輔助於檢索事件觸發時的上下文資訊,輔助函式的合理使用,能夠極大提高效能。因為這些上下文資訊產生於核心、收集於核心、最後仍然應用於核心。這減少了很多不必要的核心態和使用者態的切換。

2.3.3 函式呼叫

能否在 eBPF 程式的 C 程式碼中將重複程式碼塊抽象成函式,並執行函式呼叫?這個看似簡單的動作,在早先的 eBPF版本中並不支援(僅支援呼叫輔助函式)。如果你非要呼叫自定義函式,有沒有方法呢?當然有,你可以將其宣告為行內函數。就像下面這樣。

static __always_inline void my_function(void *ctx, int val)

__always_inline 修飾符會在編譯期間,對當前函式進行最佳化。

那麼,普通函式和行內函數有什麼區別呢?我們可以用一張圖來加以說明:

【Learning eBPF-2】eBPF 的“Hello world”

對於普通函式(上圖左側),當函式 F 被呼叫時,順序執行的指令會跳轉到函式 F 的起始地址(函式呼叫實際上就是地址切換),執行 F 的指令序列。當函式 F 執行完畢,return 語句會再次跳轉回函式 F 呼叫前的位置,接續進行。

對於行內函數(上圖右側),並沒有地址跳轉,因為編譯時這個函式會完全編譯到順序執行的指令序列中。

但是,行內函數是有侷限性的。如果你在多個位置呼叫了同一個行內函數,那麼在最終的可執行檔案中,必然會產生該函式的多個指令副本。(這也是為啥透過 kprobe 探針無法繫結到核心行內函數的原因,我們第 7 章再來看這個問題)

直到 4.16 版本的核心以及 6.0 版本的 LLVM,eBPF 中行內函數的限制才被取消。因此,在這之後,你可以放心地定義函式呼叫(但必須是 static 的)。

2.3.4 尾呼叫

尾呼叫是什麼?引用 ebpf.io 網站的一句介紹:“尾呼叫允許 eBPF 呼叫和執行另一個 eBPF 並替換執行上下文,類似於一個程序執行 execve() 系統呼叫的方式。”

換句話說,尾呼叫之後,函式不會再返回給呼叫者了。

Tail calls can call and execute another eBPF program and replace the execution context, similar to how the execve() system call operates for regular processes.

尾呼叫也不是 eBPF 獨有的思想。eBPF 為什麼要使用尾呼叫呢?這是因為,eBPF 的執行棧太有限了(僅有 512 位元組),在遞迴呼叫函式時(實際上是向執行棧中一節一節地新增棧幀),很容易導致棧溢位。而尾呼叫恰恰允許在不增加堆疊的情況下,呼叫一系列函式。這是非常有效且實用的。

你可以使用下面的輔助函式來增加一個尾呼叫:

long bpf_tail_call(void *ctx, struct bpf_map *prog_array_map, u32 index)

其三個引數的含義分別是:

  • ctx 向被呼叫者傳遞當前 eBPF 程式的上下文資訊。
  • prog_array_map 是一個程式陣列(BPF_MAP_TYPE_PROG_ARRAY)型別的 eBPF map,用於記錄一組 eBPF 程式的檔案描述符。
  • index 為程式陣列中需要呼叫的 eBPF 程式索引。

這個輔助函式一旦執行成功,就不會返回了。因為呼叫者的執行棧已經被下一個 eBPF 程式的執行棧替換了。當然,如果指定 index 的 eBPF 程式不存在,該輔助函式也會執行失敗,此時呼叫者繼續執行。無事發生。

需要注意的是,若使用尾呼叫,所有需要執行的 eBPF 程式需要同時載入到核心中。而且還需要設定好程式陣列 map

使用 BCC 框架如何進行尾呼叫呢?可以使用下面簡單的方式:

prog_array_map.call(ctx, index)

在編譯它之前,BCC 框架會自動將其轉換為標準的尾呼叫輔助函式:

bpf_tail_call(ctx, prog_array_map, index)

下面來看一個使用尾呼叫的 BCC 框架的具體例子。

BPF_PROG_ARRAY(syscall, 300);						// A

int hello(struct bpf_raw_tracepoint_args *ctx) {			// B
    int opcode = ctx->args[1];						// C
    syscall.call(ctx, opcode);						// D
    bpf_trace_printk("Another syscall: %d", opcode);			// E
    return 0;
}

int hello_execve(void *ctx) {						// F
    bpf_trace_printk("Executing a program");
    return 0;
}

int hello_timer(struct bpf_raw_tracepoint_args *ctx) {			// G
    if (ctx->args[1] == 222) {
        bpf_trace_printk("Creating a timer");
    } else if (ctx->args[1] == 226) {
        bpf_trace_printk("Deleting a timer");
    } else {
        bpf_trace_printk("Some other timer operation");
    }
    return 0;
}

int ignore_opcode(void *ctx) { 						// H
    return 0;
}

程式碼解釋:

【A】BPF_PROG_ARRAY 宏定義,對應對映型別 BPF_MAP_TYPE_PROG_ARRAY。在這裡,命名為 syscall,容量為 300。

【B】即將被使用者態程式碼繫結在 sys_enter 類別的 Tracepoint 上,即當有任何系統呼叫被執行時,都會觸發這個函式。bpf_raw_tracepoint_args 型別的結構體 ctx 存放上下文資訊。

譯者注:sys_enterraw_syscalls 型別的 Tracepoint;同族還有 sys_exit

詳細資訊可檢視檔案:/sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/format

【C】對於 sys_enter 型別的追蹤點,其引數第 2 項為操作碼,即指代即將執行的系統呼叫號。這裡賦值給變數 opcode

【D】這一步,我們把 opcode 作為索引,進行尾呼叫,執行下一個 eBPF 程式。

再次提醒,這裡的寫法是 BCC 最佳化,在真正編譯前,BCC 最終會將其重寫為 bpf_tail_call 輔助函式。

【E】如果尾呼叫成功,這一行將永遠不會被執行。新增這一行的原因是保底輸出,防止程式陣列 map 沒有命中。

【F】hello_execve(),程式陣列的一項,對應 execve()系統呼叫。經由尾呼叫觸發。

【G】hello_timer(),程式陣列的一項,對應計時器相關的系統呼叫。經由尾呼叫觸發。

【H】ignore_opcode(),程式陣列的一項,用於忽略我們不關心的系統呼叫。經由尾呼叫觸發。

現在,我們來看一下使用者態的程式(重點,如何載入和設定尾呼叫)。

b = BPF(text=program)
b.attach_raw_tracepoint(tp="sys_enter", fn_name="hello")		# A

ignore_fn = b.load_func("ignore_opcode", BPF.RAW_TRACEPOINT)		# B
exec_fn = b.load_func("hello_exec", BPF.RAW_TRACEPOINT)
timer_fn = b.load_func("hello_timer", BPF.RAW_TRACEPOINT)

prog_array = b.get_table("syscall")					# C
prog_array[ct.c_int(59)] = ct.c_int(exec_fn.fd)
prog_array[ct.c_int(222)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(223)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(224)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(225)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(226)] = ct.c_int(timer_fn.fd)

# Ignore same syscalls that come up a lot				# D
prog_array[ct.c_int(21)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(22)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(25)] = ct.c_int(ignore_fn.fd)
...

b.trace_print()								# E

程式碼解釋:

【A】與前文繫結到 kprobe 不同,這次使用者態將 hello() 主 eBPF 程式繫結到 sys_enter 追蹤點(Tracepoint)上.

【B】這些 load_func() 方法用來將每個尾呼叫函式載入核心,並返回尾呼叫函式的檔案描述符。尾呼叫需要和父呼叫保持相同的程式型別(這裡是 BPF.RAW_TRACEPOINT)。

一定不要混淆,每個尾呼叫程式本身就是一個 eBPF 程式。

【C】接下來,向我們建立好的 syscall 程式陣列中添充條目。大可不必全部填滿,如果執行時遇到空的,那也沒啥影響。同樣的,將多個 index 指向同一個尾呼叫也是可以的(事實上這段程式就是這樣做的,將計時器相關的系統呼叫指向同一個 eBPF 尾呼叫)。

譯者注:這裡的 ct.c_int() 來自 Python 的 ctypes 庫,用於 Python 到 C 的型別轉換。

【D】由於一些系統呼叫會頻繁地被執行,所以使用 ignore_opcode() 尾呼叫將他們忽略掉。

【E】不斷列印輸出,直到使用者終止程式。

執行這段程式,獲得下面的輸出:

【Learning eBPF-2】eBPF 的“Hello world”

當遇到尾呼叫沒匹配上的系統呼叫時,會輸出 “Another syscall”。

核心 4.2 版本才開始支援尾呼叫,然而在很長的一段時間內,尾呼叫和 BPF 的編譯過程不太相容(尾呼叫需要 JIT 編譯器的支援)。直到 5.10 版本才解決了這個問題。

你可以最多連結 33 個尾呼叫(而每個 eBPF 程式的指令複雜度最大支援 100w)。這樣一來,eBPF 才能真正發揮出巨大潛力來了。

2.4 小結

本章給出了 eBPF BCC 框架實現的 “Hello World” 程式,以及它的一些變體。同時,也介紹了 eBPF maps 在核心和使用者態互動之間的應用。

BCC 框架為我們提供了很好的封裝,我們不需要了解程式具體要如何編譯、如何載入核心以及如何繫結事件,即可成功執行我們的自定義邏輯。

但作為學習者,僅瞭解這些是不夠的。eBPF 程式到底怎麼執行?看來要深入地剖析了。且聽下回分解。