BPF Maps 作為 Linux 核心中的高效能 key-value 儲存機制,廣泛應用於網路效能分析、安全監控等領域。理解其運作原理及不同型別對於核心程式開發至關重要。本文將詳細介紹 BPF Maps 的使用方法、操作方式,並深入探討各種不同型別的 Maps,包括 Hash Map、Array Map、Program Array Map 等,以及它們在實際應用中的價值。透過程式碼範例,示範如何更新、讀取和刪除 BPF Maps 元素,並說明錯誤處理技巧。此外,文章也涵蓋了 BPF Maps 的同步機制和優點,幫助讀者更全面地掌握 BPF Maps 的應用技巧。
BPF Maps 的使用和操作
BPF Maps 是 Linux 中的一種高效能的 key-value 儲存結構,廣泛用於各種應用場景。下面介紹如何使用和操作 BPF Maps。
定義 BPF Maps
首先,需要定義一個 BPF Map 結構體,指定其型別、鍵大小、值大小和最大條目數等屬性。例如:
struct bpf_map_def SEC("maps") my_map = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(int),
.value_size = sizeof(int),
.max_entries = 100,
.map_flags = BPF_F_NO_PREALLOC,
};
更新 BPF Maps 元素
更新 BPF Maps 元素可以使用 bpf_map_update_elem
函式,該函式的簽名在 kernel 和 user-space 中略有不同。在 kernel 中,該函式直接接受 map 指標、鍵指標、值指標和旗標作為引數;而在 user-space 中,則需要使用 file descriptor 來存取 map。
kernel 中更新元素
int key, value, result;
key = 1, value = 1234;
result = bpf_map_update_elem(&my_map, &key, &value, BPF_ANY);
user-space 中更新元素
int key, value, result;
key = 1, value = 1234;
result = bpf_map_update_elem(map_data[0].fd, &key, &value, BPF_ANY);
bpf_map_update_elem
函式傳回 0 表示更新成功,傳回負數表示更新失敗,並將錯誤碼儲存在 errno
中。
更新旗標
bpf_map_update_elem
函式的第四個引數是更新旗標,該旗標可以控制更新的行為。有三個可用的旗標:
BPF_ANY
(0):如果鍵存在則更新,如果鍵不存在則建立。BPF_NOEXIST
(1):只有當鍵不存在時才建立。BPF_EXIST
(2):只有當鍵存在時才更新。
錯誤處理
當 bpf_map_update_elem
函式傳回負數時,表示更新失敗,錯誤碼儲存在 errno
中。常見的錯誤碼包括:
EEXIST
:鍵已存在。ENOENT
:鍵不存在。
可以使用 strerror
函式將錯誤碼轉換為字串,以便輸出錯誤資訊。例如:
printf("Failed to update map with new value: %d (%s)\n", result, strerror(errno));
示例程式
以下是一個示例程式,展示如何使用 bpf_map_update_elem
函式更新 BPF Maps 元素:
int key, value, result;
key = 1, value = 1234;
result = bpf_map_update_elem(&my_map, &key, &value, BPF_ANY);
if (result == 0)
printf("Map updated with new element\n");
else
printf("Failed to update map with new value: %d (%s)\n", result, strerror(errno));
這個示例程式嘗試使用 BPF_ANY
旗標更新一個鍵為 1 的元素,如果鍵不存在則建立。如果更新成功,則輸出 “Map updated with new element”;否則,輸出錯誤資訊。
從 BPF 地圖中讀取元素
現在我們已經將新元素填充到地圖中,我們可以從程式碼的其他部分開始讀取它們。讀取 API 的語法會讓你感覺熟悉,因為它與 bpf_map_update_element
類別似。BPF 提供了兩個不同的 helper 函式來從地圖中讀取元素,兩者都叫做 bpf_map_lookup_elem
。就像更新 helper 函式一樣,它們的第一個引數不同;核心方法需要一個指向地圖的參照,而使用者空間 helper 函式需要地圖的檔案描述符識別符作為其第一個引數。兩種方法都傳回一個整數來表示操作是否失敗或成功,就像更新 helper 函式一樣。這些 helper 函式的第三個引數是一個指標,指向程式碼中將儲存從地圖中讀取的值的變數。
範例:從核心中讀取地圖元素
int key, value, result;
key = 1;
result = bpf_map_lookup_elem(&my_map, &key, &value);
if (result == 0)
printf("從地圖中讀取的值:'%d'\n", value);
else
printf("讀取地圖值失敗:%d (%s)\n", result, strerror(errno));
如果我們嘗試讀取的鍵不存在,bpf_map_lookup_elem
會傳回一個負數,並設定 errno
變數為「找不到」錯誤 (ENOENT
)。
範例:從使用者空間中讀取地圖元素
int key, value, result;
key = 1;
result = bpf_map_lookup_elem(map_data[0].fd, &key, &value);
if (result == 0)
printf("從地圖中讀取的值:'%d'\n", value);
else
printf("讀取地圖值失敗:%d (%s)\n", result, strerror(errno));
如你所見,我們已經用地圖的檔案描述符識別符取代了 bpf_map_lookup_elem
的第一個引數。helper 函式的行為與前面的範例相同。
從 BPF 地圖中刪除元素
我們可以在地圖上執行的第三個操作是刪除元素。與寫入和讀取元素一樣,BPF 提供了兩個不同的 helper 函式來刪除元素,兩者都叫做 bpf_map_delete_element
。就像前面的範例一樣,這些 helper 函式使用地圖的直接參照當你在核心中使用它們,並使用地圖的檔案描述符識別符當你在使用者空間中使用它們。
範例:從核心中刪除地圖元素
int key, result;
key = 1;
result = bpf_map_delete_element(&my_map, &key);
if (result == 0)
printf("元素已從地圖中刪除\n");
else
printf("刪除地圖元素失敗:%d (%s)\n", result, strerror(errno));
如果你嘗試刪除的元素不存在,核心會傳回一個負數,並設定 errno
變數為「找不到」錯誤 (ENOENT
)。
範例:從使用者空間中刪除地圖元素
int key, result;
key = 1;
result = bpf_map_delete_element(map_data[0].fd, &key);
if (result == 0)
printf("元素已從地圖中刪除\n");
else
printf("刪除地圖元素失敗:%d (%s)\n", result, strerror(errno));
如你所見,我們已經用地圖的檔案描述符識別符取代了 bpf_map_delete_element
的第一個引數。helper 函式的行為與內核的 helper 函式一致。
這結束了對 BPF 地圖的 create/read/update/delete (CRUD) 運作的介紹。核心暴露了一些額外的函式來幫助你完成其他常見的運作;我們將在接下來的兩個部分討論其中一些。
BPF Maps 的運作方式
BPF Maps 是一種在 Linux 核心中實作的資料結構,允許使用者空間程式和核心空間程式之間分享資料。BPF Maps 提供了一種高效且安全的方式來儲存和存取資料。
BPF Maps 的型別
BPF Maps 有多種型別,每種型別都有其特定的用途和特性。常見的 BPF Maps 型別包括:
- 陣列對映 (Array Map):是一種簡單的對映,使用陣列作為底層儲存結構。
- 雜湊對映 (Hash Map):是一種使用雜湊函式作為索引的對映,適合於快速查詢和插入資料。
- Cgroup 對映 (Cgroup Map):是一種用於控制群組 (Control Group) 的對映,允許使用者空間程式控制和監視核心空間程式的行為。
BPF Maps 的操作
BPF Maps 支援多種操作,包括:
- 查詢 (Lookup):根據鍵值查詢對應的資料。
- 插入 (Insert):將新資料插入到對映中。
- 更新 (Update):更新已經存在的資料。
- 刪除 (Delete):刪除指定的資料。
BPF Maps 的迭代
BPF Maps 支援迭代操作,允許使用者空間程式遍歷對映中的所有資料。迭代操作可以使用 bpf_map_get_next_key
函式實作。
BPF Maps 的同步
BPF Maps 支援同步操作,允許使用者空間程式鎖定對映中的資料,以防止其他程式同時存取和修改相同的資料。同步操作可以使用 bpf_spin_lock
和 bpf_spin_unlock
函式實作。
BPF Maps 的優點
BPF Maps 有多個優點,包括:
- 高效: BPF Maps 的操作非常高效,尤其是在查詢和插入資料時。
- 安全: BPF Maps 提供了一種安全的方式來儲存和存取資料,防止使用者空間程式直接存取核心空間的資料。
- 靈活: BPF Maps 支援多種型別和操作,允許使用者空間程式根據不同的需求選擇合適的對映型別和操作。
BPF Maps 的種類別
Linux 檔案將 Maps 定義為通用資料結構,可以儲存不同型別的資料。隨著時間的推移,核心開發人員增加了許多專門的資料結構,這些資料結構在特定使用案例中更高效。本文將探討每種 Map 型別及其使用方法。
散列表 Maps
散列表 Maps 是新增到 BPF 的第一種通用 Map。它們的型別定義為 BPF_MAP_TYPE_HASH
。其實作和使用方法與其他散列表類別似。你可以使用任意大小的鍵和值;核心會根據需要為你分配和釋放它們。當你在散列表 Map 上使用 bpf_map_update_elem
時,核心會原子地替換元素。
散列表 Maps 最佳化為查詢速度非常快,適合儲存經常讀取的結構化資料。讓我們看一個示例程式,它使用散列表 Maps 來跟蹤網路 IP 和其速率限制:
#define IPV4_FAMILY 1
struct ip_key {
union {
__u32 v4_addr;
__u8 v6_addr[16];
};
__u8 family;
};
struct bpf_map_def SEC("maps") counters = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(struct ip_key),
.value_size = sizeof(uint64_t),
.max_entries = 100,
.map_flags = BPF_F_NO_PREALLOC
};
在這個程式碼中,我們聲明瞭一個結構化的鍵,並將其用於儲存 IP 地址的資訊。定義了 Map 來跟蹤速率限制。你可以看到,我們在這個 Map 中使用 IP 地址作為鍵。值將是從特定 IP 地址接收網路包的次數。
讓我們編寫一個小程式碼片段來更新計數器:
uint64_t update_counter(uint32_t ipv4) {
uint64_t value;
struct ip_key key = {};
key.v4_addr = ipv4;
key.family = IPV4_FAMILY;
bpf_map_lookup_elem(counters, &key, &value);
(*value) += 1;
}
這個函式從網路包中提取 IP 地址,並使用複合鍵進行 Map 查詢。在這種情況下,我們假設計數器已經初始化為零值;否則,bpf_map_lookup_elem
將傳回負數。
陣列 Maps
陣列 Maps 是新增到內核的第二種 BPF Map 型別。它們的型別定義為 BPF_MAP_TYPE_ARRAY
。當你初始化陣列 Map 時,其所有元素都會預先分配在記憶體中並設定為其零值。由於這些 Map 由陣列支援,鍵是陣列中的索引,其大小必須正好是四個位元組。
使用陣列 Map 的缺點是 Map 中的元素不能被刪除,你也不能使陣列小於其當前大小。如果你嘗試在陣列 Map 上使用 map_delete_elem
,呼叫將失敗,並傳回錯誤程式碼 EINVAL
。
陣列 Maps 通常用於儲存可以改變值但通常具有固定行為的資訊。人們使用它們來儲存具有預定義分配規則的全域性變數。由於你不能刪除元素,你可以假設陣列中特定位置上的元素始終代表相同的元素。
還有一件事需要記住:map_update_elem
不是原子的,就像你在散列表 Map 中看到的一樣。如果有更新正在進行,同一個程式可以在同一時間從同一位置讀取不同的值。如果你在陣列 Map 中儲存計數器,你可以使用內核的內建函式 __sync_fetch_and_add
在 Map 值上執行原子操作。
程式陣列 Maps
程式陣列 Maps 是新增到內核的第一種專門 Map。它們的型別定義為 BPF_MAP_TYPE_PROG_ARRAY
。你可以使用這種 Map 型別來儲存對 BPF 程式的參照,使用其檔案描述符識別符號。在 bpf_tail_call
幫助下,這個 Map 允許你在程式之間跳轉,繞過單個 BPF 程式的最大指令限制並降低實作複雜性。
當你使用這種專門 Map 時,有幾件事需要考慮。第一個方面需要記住的是,鍵和值大小必須都是四個位元組。第二個方面需要記住的是,當你跳轉到新程式時,新程式將重用相同的記憶體堆積疊,因此你的程式不會消耗所有可用的記憶體。最後,如果你嘗試跳轉到 Map 中不存在的程式,尾部呼叫將失敗,當前程式將繼續執行。
讓我們深入瞭解一個詳細的示例,以便更好地理解如何使用這種 Map 型別:
struct bpf_map_def SEC("maps") programs = {
.type = BPF_MAP_TYPE_PROG_ARRAY,
.key_size = 4,
.value_size = 4,
.max_entries = 1024,
};
圖表翻譯:
graph LR A[程式陣列 Maps] -->|定義型別|> B[BPF_MAP_TYPE_PROG_ARRAY] B -->|鍵和值大小|> C[4 個位元組] C -->|最大條目數|> D[1024] D -->|使用 bpf_tail_call|> E[跳轉到新程式] E -->|重用記憶體堆積疊|> F[避免記憶體耗盡]
BPF Maps 介紹
BPF Maps 是 Linux 中的一種高效的資料儲存和查詢機制,廣泛用於觀測性工具、安全性和效能最佳化等領域。BPF Maps 提供了多種型別的儲存結構,包括陣列、雜湊表、程式陣列等,以滿足不同的應用需求。
程式陣列地圖(BPF_MAP_TYPE_PROG_ARRAY)
程式陣列地圖是一種特殊的 BPF Map,允許 BPF 程式之間進行跳轉和呼叫。這種地圖的 key 和 value 大小均為 4 個 byte。透過 bpf_prog_load
函式可以載入 BPF 程式,並將其 file descriptor 儲存在程式陣列地圖中。然後,透過 bpf_tail_call
函式可以實作 BPF 程式之間的跳轉。
struct bpf_insn prog[] = {
BPF_MOV64_IMM(BPF_REG_0, 0), // assign r0 = 0
BPF_EXIT_INSN(), // return r0
};
int key = 1;
prog_fd = bpf_prog_load(BPF_PROG_TYPE_KPROBE, prog, sizeof(prog), "GPL");
bpf_map_update_elem(&programs, &key, &prog_fd, BPF_ANY);
Perf Events 陣列地圖(BPF_MAP_TYPE_PERF_EVENT_ARRAY)
Perf Events 陣列地圖是一種用於儲存 Perf 事件資料的 BPF Map。這種地圖允許 BPF 程式將事件資料傳送到使用者空間進行進一步處理。透過 bpf_perf_event_output
函式可以將資料追加到地圖中。
struct data_t {
u32 pid;
char program_name[16];
};
struct bpf_map_def SEC("maps") events = {
.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
.key_size = sizeof(int),
.value_size = sizeof(u32),
.max_entries = 2,
};
SEC("kprobe/sys_exec")
int bpf_capture_exec(struct pt_regs *ctx) {
data_t data;
data.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&data.program_name, sizeof(data.program_name));
bpf_perf_event_output(ctx, &events, 0, &data, sizeof(data));
return 0;
}
Per-CPU 雜湊表(BPF_MAP_TYPE_PERCPU_HASH)
Per-CPU 雜湊表是一種特殊的 BPF Map,每個 CPU 都有自己的隔離版本。這種地圖適合用於高效能查詢和聚合操作。
Per-CPU 陣列地圖(BPF_MAP_TYPE_PERCPU_ARRAY)
Per-CPU 陣列地圖也是一種特殊的 BPF Map,每個 CPU 都有自己的隔離版本。這種地圖適合用於高效能查詢和聚合操作。
堆積疊追蹤地圖(BPF_MAP_TYPE_STACK_TRACE)
堆積疊追蹤地圖用於儲存堆積疊追蹤資料。透過 bpf_get_stackid
函式可以將堆積疊追蹤資料新增到地圖中。
Cgroup 陣列地圖(BPF_MAP_TYPE_CGROUP_ARRAY)
Cgroup 陣列地圖用於儲存 cgroup 參考。這種地圖適合用於控制流量、除錯和測試等場景。
flowchart TD A[開始] --> B[宣告程式陣列地圖] B --> C[載入 BPF 程式] C --> D[將程式 file descriptor 儲存在程式陣列地圖中] D --> E[實作 BPF 程式之間的跳轉]
圖表翻譯:
此圖表描述了使用程式陣列地圖實作 BPF 程式之間跳轉的過程。首先,宣告程式陣列地圖,然後載入 BPF 程式,並將其 file descriptor 儲存在程式陣列地圖中。最後,透過 bpf_tail_call
函式實作 BPF 程式之間的跳轉。
BPF Maps 型別與應用
BPF(Berkeley Packet Filter)Maps 是一種高效的資料儲存和查詢機制,廣泛應用於 Linux 核心和網路應用中。根據不同的需求和應用場景,BPF Maps 被分為多種型別,每種型別都有其特定的設計和優點。
1. LRU Hash 和 Per-CPU Hash Maps
LRU(Least Recently Used)Hash Maps 和 Per-CPU Hash Maps 是兩種特殊的 Hash 表,它們不僅提供了快速的資料查詢和儲存功能,還實作了 LRU 演算法,以便在 Map 內容超過最大限制時,自動移除最少使用的元素。這使得開發者可以在不擔心 Map 容量限制的情況下,插入新的元素。
2. LPM Trie Maps
LPM(Longest Prefix Match)Trie Maps 是根據 Trie 資料結構的 BPF Map,它們使用 LPM 演算法來查詢最長的字首匹配。這種 Map 特別適合於路由和網路應用,例如 IP 地址路由表的查詢和匹配。
3. Array of Maps 和 Hash of Maps
Array of Maps 和 Hash of Maps 允許開發者儲存其他 BPF Map 的參照。這種機制提供了一種靈活的方式來管理和更新複雜的資料結構,使得開發者可以輕鬆地實作資料的動態更新和替換。
4. Device Map Maps
Device Map Maps 是一種特殊的 BPF Map,用於儲存網路裝置的參照。這種 Map 在網路應用中非常有用,特別是當需要在核心層面操控網路流量時。
5. CPU Map Maps
CPU Map Maps 與 Device Map Maps 類別似,但它們儲存的是 CPU 的參照。這使得開發者可以將網路流量導向特定的 CPU,從而實作負載平衡和網路隔離。
6. Open Socket Maps
Open Socket Maps 是一種 BPF Map,用於儲存已經開啟的 socket 參照。這種 Map 主要用於將封包在 socket 之間轉發。
7. Socket Array 和 Hash Maps
Socket Array Maps 和 Socket Hash Maps 是兩種用於儲存 socket 參照的 BPF Map。它們的主要區別在於,一種使用陣列來儲存 socket,另一種使用 hash 表。這兩種 Map 都可以與 bpf_redirect_map
函式一起使用,以實作 socket 之間的封包轉發。
8. Cgroup Storage 和 Per-CPU Storage Maps
Cgroup Storage Maps 和 Per-CPU Storage Maps 是為了與 cgroup 子系統合作而設計的 BPF Map。它們允許開發者在 cgroup 附加的 BPF 程式中儲存和管理資料。這些 Map 的存取許可權受到限制,只有附加到相同 cgroup 的 BPF 程式才能存取。
深入剖析 BPF maps 的核心架構後,我們可以發現其作為高效能 key-value 儲存結構的優勢,不僅體現在多樣化的型別選擇上,更在於其與 Linux 核心深度整合所帶來的靈活性。從簡單的陣列和雜湊表到專門的程式陣列和 per-CPU 變種,BPF maps 提供了滿足各種應用場景的豐富工具。然而,不同 map 型別在效能和功能上存在差異,例如程式陣列 map 允許程式間跳轉,而陣列 map 的更新操作並非原子性,需要額外同步機制。權衡系統資源消耗與處理效率後,針對具體應用選擇合適的 map 型別至關重要。對於重視效能的核心繫統,per-CPU map 可有效減少鎖競爭;而對於需要程式間複雜互動的場景,程式陣列 map 則是不二之選。展望未來,BPF maps 的應用範圍將隨著 eBPF 技術的發展而持續擴大,預計將在網路安全、效能分析和系統追蹤等領域扮演更關鍵的角色。隨著更多功能的加入和效能的提升,BPF maps 將成為 Linux 系統中不可或缺的組成部分。玄貓認為,深入理解 BPF maps 的運作機制及不同型別的特性,才能更好地發揮其效能優勢,並在實務中構建更強大的應用程式。