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

来源:https://www.cnblogs.com/lianyihong/p/18107681
-Advertisement-
Play Games

摘要: 本系列為《Learning eBPF》一書的翻譯系列。 (內容並非機翻,部分夾帶私貨)筆者學習自用,歡迎大家討論學習。 ...


前一章講了 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)。

下圖顯示了這段代碼運行時的狀態。

下麵來逐行解釋這段代碼。

第一行告訴你,這是一個 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 程式的運行原理:

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

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

這裡書中再次強調,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 運行指令。

可以看到,每 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 獨有的。環形緩衝區實際上是一段記憶體空間,其空間中的地址在邏輯上首尾相連成環。環形緩衝區包括兩個工作指針,一個負責讀,一個負責寫,二者同向移動。寫指針指向的位置就是下個數據被寫入的位置(數據可以任意長度,其長度信息包含在數據頭中),同理,讀指針指向的位置就是下一個需要讀取的數據開頭(根據數據頭中的長度,控制讀指針移動距離)。

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

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

瞭解了環形緩衝區的概念後,我們再來改進一下之前綁定到 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 內容。

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

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

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

通過環形緩衝區傳遞數據會不會仍然使用了 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 修飾符會在編譯期間,對當前函數進行優化。

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

對於普通函數(上圖左側),當函數 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】不斷列印輸出,直到用戶終止程式。

運行這段程式,獲得下麵的輸出:

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

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

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

2.4 小結

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

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

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 實驗準備: 一臺dns1域控制器,一臺虛擬機登錄域用戶驗證設置 一:“財務處”用戶自動映射z盤 打開dns1,在c盤下新建share目錄,添加test文本文件 修改share目錄的共用為所有人 完成可以看到share目錄的網路路徑 在共用裡面複製網路路徑 打開組策略管理器 右鍵財務部GPO,點擊編輯 ...
  • 哈嘍大家好,我是鹹魚。 我們知道 CentOS 7 之後,Systemd 代替了原來的 SystemV 來管理服務,相比 SystemV ,Systemd 能夠很好地解決各個服務間的依賴關係,還能讓所有的服務同時啟動,而不是串列啟動。 通常情況下,yum 安裝的軟體會由系統的包管理器(如 RPM)安 ...
一周排行
    -Advertisement-
    Play Games
  • 隨著Aspire發佈preview5的發佈,Microsoft.Extensions.ServiceDiscovery隨之更新, 服務註冊發現這個屬於老掉牙的話題解決什麼問題就不贅述了,這裡主要講講Microsoft.Extensions.ServiceDiscovery(preview5)以及如何 ...
  • 概述:通過使用`SemaphoreSlim`,可以簡單而有效地限制非同步HTTP請求的併發量,確保在任何給定時間內不超過20個網頁同時下載。`ParallelOptions`不適用於非同步操作,但可考慮使用`Parallel.ForEach`,儘管在非同步場景中謹慎使用。 對於併發非同步 I/O 操作的數量 ...
  • 1.Linux上安裝Docken 伺服器系統版本以及內核版本:cat /etc/redhat-release 查看伺服器內核版本:uname -r 安裝依賴包:yum install -y yum-utils device-mapper-persistent-data lvm2 設置阿裡雲鏡像源:y ...
  • 概述:WPF界面綁定和渲染大量數據可能導致性能問題。通過啟用UI虛擬化、非同步載入和數據分頁,可以有效提高界面響應性能。以下是簡單示例演示這些優化方法。 在WPF中,當你嘗試綁定和渲染大量的數據項時,性能問題可能出現。以下是一些可能導致性能慢的原因以及優化方法: UI 虛擬化: WPF提供了虛擬化技術 ...
  • 引言 上一章節介紹了 TDD 的三大法則,今天我們講一下在單元測試中模擬對象的使用。 Fake Fake - Fake 是一個通用術語,可用於描述 stub或 mock 對象。 它是 stub 還是 mock 取決於使用它的上下文。 也就是說,Fake 可以是 stub 或 mock Mock - ...
  • 為.net6在CentOS7上面做準備,先在vmware虛擬機安裝CentOS 7.9 新建CentOS764位的系統 因為CentOS8不更新了,所以安裝7;簡單就一筆帶過了 選擇下載好的操作系統的iso文件,下載地址https://mirrors.aliyun.com/centos/7.9.20 ...
  • 經過前面幾篇的學習,我們瞭解到指令的大概分類,如:參數載入指令,該載入指令以 Ld 開頭,將參數載入到棧中,以便於後續執行操作命令。參數存儲指令,其指令以 St 開頭,將棧中的數據,存儲到指定的變數中,以方便後續使用。創建實例指令,其指令以 New 開頭,用於在運行時動態生成並初始化對象。方法調用指... ...
  • LiteDB 是一個輕量級的嵌入式 NoSQL 資料庫,其設計理念與 MongoDB 類似,但它是完全使用 C# 開發的,因此與 C# 應用程式的集成非常順暢。與 SQLite 相比,LiteDB 提供了 NoSQL(即鍵值對)的數據存儲方式,並且是一個開源且免費的項目。它適用於桌面、移動以及 We ...
  • 1 開源解析和拆分文檔 第三方的工具去對文件解析拆分,去將我們的文件內容給提取出來,並將我們的文檔內容去拆分成一個小的chunk。常見的PDF word mark down, JSON、HTML。都可以有很好的一些模塊去把這些文件去進行一個東西去提取。 優勢 支持豐富的文檔類型 每種文檔多樣化選擇 ...
  • OOM是什麼?英文全稱為 OutOfMemoryError(記憶體溢出錯誤)。當程式發生OOM時,如何去定位導致異常的代碼還是挺麻煩的。 要檢查OOM發生的原因,首先需要瞭解各種OOM情況下會報的異常信息。這樣能縮小排查範圍,再結合異常堆棧、heapDump文件、JVM分析工具和業務代碼來判斷具體是哪 ...