如果覺得文章對你有所啟發,可以考慮用 🌟 支持 Gthulhu 專案,短期目標是集齊 300 個 🌟 藉此被 CNCF Landscape 採納 [ref]。
在上一篇文章中,我們成功編寫了第一個 eBPF 程式,並學會了基本的程式結構和載入流程。今天我們將深入探討 eBPF Maps——這個連接內核空間和使用者空間的重要橋樑。
eBPF Maps 不僅僅是簡單的資料結構,它們是 eBPF 程式持久化狀態、程式間通訊、以及與使用者空間互動的核心機制。
eBPF Maps 解決了幾個核心問題:
eBPF Maps 可以按照不同維度分類:
筆者補充:詳細的 Map 種類請參考:https://docs.ebpf.io/linux/map-type/
mkdir -p ~/ebpf-maps-demo/{src/kernel,src/userspace,headers,bin}
cd ~/ebpf-maps-demo
# 建立 Makefile 和 Go module
cp ~/ebpf-hello-world/Makefile .
cp ~/ebpf-hello-world/go.mod .
Array Maps 是最簡單的 Map 型別,適合存儲固定大小的陣列資料。
// src/kernel/array_maps_demo.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
// 定義一個簡單的陣列 Map
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 256);  // 最大 256 個元素
    __type(key, __u32);        // 鍵類型:32位整數(索引)
    __type(value, __u64);      // 值類型:64位整數(計數)
} syscall_count_array SEC(".maps");
// 定義一個 Per-CPU 陣列 Map
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 10);
    __type(key, __u32);
    __type(value, __u64);
} percpu_stats SEC(".maps");
// 定義統計結構
struct syscall_stats {
    __u64 count;
    __u64 total_time;
    __u64 min_time;
    __u64 max_time;
};
// 複雜的陣列 Map
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 512);
    __type(key, __u32);
    __type(value, struct syscall_stats);
} detailed_stats SEC(".maps");
char LICENSE[] SEC("license") = "GPL";
SEC("tp/raw_syscalls/sys_enter")
int trace_syscall_enter(struct trace_event_raw_sys_enter *ctx)
{
    __u32 syscall_nr = ctx->id;
    __u64 *count;
    __u64 timestamp = bpf_ktime_get_ns();
    
    // 限制系統呼叫號範圍,避免越界
    if (syscall_nr >= 256)
        return 0;
    
    // 更新簡單計數陣列
    count = bpf_map_lookup_elem(&syscall_count_array, &syscall_nr);
    if (count) {
        __sync_fetch_and_add(count, 1);
    } else {
        __u64 initial = 1;
        bpf_map_update_elem(&syscall_count_array, &syscall_nr, &initial, BPF_ANY);
    }
    
    // 更新 Per-CPU 統計(假設只統計前10個系統呼叫)
    if (syscall_nr < 10) {
        __u64 *percpu_count = bpf_map_lookup_elem(&percpu_stats, &syscall_nr);
        if (percpu_count) {
            *percpu_count += 1;
        } else {
            __u64 initial = 1;
            bpf_map_update_elem(&percpu_stats, &syscall_nr, &initial, BPF_ANY);
        }
    }
    
    // 更新詳細統計(限制範圍)
    if (syscall_nr < 512) {
        struct syscall_stats *stats = bpf_map_lookup_elem(&detailed_stats, &syscall_nr);
        if (stats) {
            __sync_fetch_and_add(&stats->count, 1);
            if (stats->min_time == 0 || timestamp < stats->min_time) {
                stats->min_time = timestamp;
            }
            if (timestamp > stats->max_time) {
                stats->max_time = timestamp;
            }
        } else {
            struct syscall_stats new_stats = {
                .count = 1,
                .total_time = 0,
                .min_time = timestamp,
                .max_time = timestamp
            };
            bpf_map_update_elem(&detailed_stats, &syscall_nr, &new_stats, BPF_ANY);
        }
    }
    
    return 0;
}
Hash Maps 提供靈活的鍵值對存儲,適合動態資料。
// src/kernel/hash_maps_demo.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
// 定義進程統計結構
struct process_stats {
    __u64 file_opens;
    __u64 memory_allocs;
    __u64 network_ops;
    __u64 first_seen;
    __u64 last_seen;
    char comm[16];
};
// PID -> 進程統計的 Hash Map
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10000);
    __type(key, __u32);  // PID
    __type(value, struct process_stats);
} process_map SEC(".maps");
// 檔案名 Hash -> 存取次數
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1000);
    __type(key, __u64);  // 檔名雜湊
    __type(value, __u64); // 存取次數
} file_access_map SEC(".maps");
// Per-CPU Hash Map 示例
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_HASH);
    __uint(max_entries, 1000);
    __type(key, __u32);
    __type(value, __u64);
} percpu_process_stats SEC(".maps");
char LICENSE[] SEC("license") = "GPL";
// 簡單的字串雜湊函數
static __always_inline __u64 hash_string(const char *str, int len)
{
    __u64 hash = 5381;
    for (int i = 0; i < len && i < 16; i++) {
        if (str[i] == 0) break;
        hash = ((hash << 5) + hash) + str[i];
    }
    return hash;
}
SEC("tp/syscalls/sys_enter_openat")
int trace_file_opens(struct trace_event_raw_sys_enter *ctx)
{
    __u32 pid = bpf_get_current_pid_tgid() >> 32;
    __u64 now = bpf_ktime_get_ns();
    
    // 更新進程統計
    struct process_stats *stats = bpf_map_lookup_elem(&process_map, &pid);
    if (stats) {
        __sync_fetch_and_add(&stats->file_opens, 1);
        stats->last_seen = now;
    } else {
        struct process_stats new_stats = {
            .file_opens = 1,
            .memory_allocs = 0,
            .network_ops = 0,
            .first_seen = now,
            .last_seen = now
        };
        bpf_get_current_comm(&new_stats.comm, sizeof(new_stats.comm));
        bpf_map_update_elem(&process_map, &pid, &new_stats, BPF_ANY);
    }
    
    // 嘗試獲取檔案名並雜湊
    char filename[64];
    long ret = bpf_probe_read_user_str(filename, sizeof(filename), 
                                      (void *)ctx->args[1]);
    if (ret > 0) {
        __u64 file_hash = hash_string(filename, ret);
        __u64 *count = bpf_map_lookup_elem(&file_access_map, &file_hash);
        if (count) {
            __sync_fetch_and_add(count, 1);
        } else {
            __u64 initial = 1;
            bpf_map_update_elem(&file_access_map, &file_hash, &initial, BPF_ANY);
        }
    }
    
    // 更新 Per-CPU 統計
    __u64 *percpu_count = bpf_map_lookup_elem(&percpu_process_stats, &pid);
    if (percpu_count) {
        *percpu_count += 1;
    } else {
        __u64 initial = 1;
        bpf_map_update_elem(&percpu_process_stats, &pid, &initial, BPF_ANY);
    }
    
    return 0;
}
Ring Buffer 是現代 eBPF 推薦的事件通信機制。
// src/kernel/ringbuf_demo.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
// 定義事件結構
struct file_event {
    __u32 pid;
    __u32 tgid;
    __u32 uid;
    __u64 timestamp;
    __u32 filename_len;
    char comm[16];
    char filename[256];
};
// Ring Buffer Map
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024); // 256KB buffer
} events SEC(".maps");
char LICENSE[] SEC("license") = "GPL";
SEC("tp/syscalls/sys_enter_openat")
int trace_file_events(struct trace_event_raw_sys_enter *ctx)
{
    struct file_event *event;
    
    // 從 ring buffer 分配空間
    event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
    if (!event) {
        return 0; // 分配失敗,buffer 可能已滿
    }
    
    // 填充事件資料
    __u64 pid_tgid = bpf_get_current_pid_tgid();
    event->pid = pid_tgid >> 32;
    event->tgid = pid_tgid & 0xffffffff;
    event->uid = bpf_get_current_uid_gid() & 0xffffffff;
    event->timestamp = bpf_ktime_get_ns();
    
    // 獲取進程名稱
    bpf_get_current_comm(&event->comm, sizeof(event->comm));
    
    // 讀取檔案名
    long ret = bpf_probe_read_user_str(event->filename, sizeof(event->filename),
                                      (void *)ctx->args[1]);
    if (ret > 0) {
        event->filename_len = ret;
    } else {
        event->filename_len = 0;
        event->filename[0] = '\0';
    }
    
    // 提交事件到 ring buffer
    bpf_ringbuf_submit(event, 0);
    
    return 0;
}
現在編寫一個完整的 Go 程式來操作所有這些 Maps:
// src/userspace/maps_demo.go
package main
import (
	"bytes"
	"context"
	"encoding/binary"
	"fmt"
	"log"
	"os"
	"os/signal"
	"sort"
	"syscall"
	"time"
	"unsafe"
	bpf "github.com/aquasecurity/libbpfgo"
)
// Go 結構對應 C 結構
type ProcessStats struct {
	FileOpens    uint64
	MemoryAllocs uint64
	NetworkOps   uint64
	FirstSeen    uint64
	LastSeen     uint64
	Comm         [16]byte
}
type SyscallStats struct {
	Count     uint64
	TotalTime uint64
	MinTime   uint64
	MaxTime   uint64
}
type FileEvent struct {
	PID         uint32
	TGID        uint32
	UID         uint32
	Timestamp   uint64
	FilenameLen uint32
	Comm        [16]byte
	Filename    [256]byte
}
func main() {
	if os.Geteuid() != 0 {
		log.Fatal("需要 root 權限執行")
	}
	// 選擇要載入的程式
	var objFile string
	var progName string
	if len(os.Args) > 1 {
		switch os.Args[1] {
		case "array":
			objFile = "array_maps_demo.bpf.o"
			progName = "trace_syscall_enter"
		case "hash":
			objFile = "hash_maps_demo.bpf.o"
			progName = "trace_file_opens"
		case "ringbuf":
			objFile = "ringbuf_demo.bpf.o"
			progName = "trace_file_events"
		default:
			fmt.Println("使用方法: ./maps_demo [array|hash|ringbuf]")
			os.Exit(1)
		}
	} else {
		objFile = "hash_maps_demo.bpf.o"
		progName = "trace_file_opens"
	}
	fmt.Printf("載入 eBPF 程式: %s\n", objFile)
	// 載入 eBPF 程式
	bpfModule, err := bpf.NewModuleFromFile(objFile)
	if err != nil {
		log.Fatalf("載入 eBPF 模組失敗: %v", err)
	}
	defer bpfModule.Close()
	if err := bpfModule.BPFLoadObject(); err != nil {
		log.Fatalf("載入 eBPF 物件失敗: %v", err)
	}
	// 獲取並附加程式
	prog, err := bpfModule.GetProgram(progName)
	if err != nil {
		log.Fatalf("獲取程式失敗: %v", err)
	}
	var link *bpf.BPFLink
	if progName == "trace_syscall_enter" {
		link, err = prog.AttachTracepoint(&bpf.TracepointOpts{
			Group: "raw_syscalls",
			Name:  "sys_enter",
		})
	} else {
		link, err = prog.AttachTracepoint(&bpf.TracepointOpts{
			Group: "syscalls",
			Name:  "sys_enter_openat",
		})
	}
	if err != nil {
		log.Fatalf("附加程式失敗: %v", err)
	}
	defer link.Close()
	fmt.Printf("✓ eBPF 程式 %s 附加成功\n", progName)
	// 設定信號處理
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	// 根據程式類型執行不同的處理邏輯
	switch os.Args[1] {
	case "array":
		runArrayDemo(ctx, bpfModule, sigChan)
	case "hash":
		runHashDemo(ctx, bpfModule, sigChan)
	case "ringbuf":
		runRingBufDemo(ctx, bpfModule, sigChan)
	default:
		runHashDemo(ctx, bpfModule, sigChan)
	}
}
func runArrayDemo(ctx context.Context, module *bpf.Module, sigChan chan os.Signal) {
	syscallCountMap, err := module.GetMap("syscall_count_array")
	if err != nil {
		log.Fatalf("獲取 syscall_count_array 失敗: %v", err)
	}
	detailedStatsMap, err := module.GetMap("detailed_stats")
	if err != nil {
		log.Fatalf("獲取 detailed_stats 失敗: %v", err)
	}
	ticker := time.NewTicker(3 * time.Second)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case sig := <-sigChan:
			fmt.Printf("\n收到信號 %v,正在退出...\n", sig)
			cancel()
		case <-ticker.C:
			fmt.Println("\n=== Array Maps 統計 ===")
			
			// 顯示系統呼叫計數
			fmt.Println("系統呼叫計數 (前10個):")
			for i := uint32(0); i < 10; i++ {
				valueBytes, err := syscallCountMap.GetValue(unsafe.Pointer(&i))
				if err == nil && len(valueBytes) == 8 {
					count := binary.LittleEndian.Uint64(valueBytes)
					if count > 0 {
						fmt.Printf("  syscall_%d: %d 次\n", i, count)
					}
				}
			}
			// 顯示詳細統計
			fmt.Println("\n詳細統計 (有活動的系統呼叫):")
			for i := uint32(0); i < 20; i++ {
				valueBytes, err := detailedStatsMap.GetValue(unsafe.Pointer(&i))
				if err == nil && len(valueBytes) == int(unsafe.Sizeof(SyscallStats{})) {
					stats := (*SyscallStats)(unsafe.Pointer(&valueBytes[0]))
					if stats.Count > 0 {
						fmt.Printf("  syscall_%d: %d次, 最小時間: %d, 最大時間: %d\n",
							i, stats.Count, stats.MinTime, stats.MaxTime)
					}
				}
			}
		}
	}
}
func runHashDemo(ctx context.Context, module *bpf.Module, sigChan chan os.Signal) {
	processMap, err := module.GetMap("process_map")
	if err != nil {
		log.Fatalf("獲取 process_map 失敗: %v", err)
	}
	fileAccessMap, err := module.GetMap("file_access_map")
	if err != nil {
		log.Fatalf("獲取 file_access_map 失敗: %v", err)
	}
	ticker := time.NewTicker(3 * time.Second)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case sig := <-sigChan:
			fmt.Printf("\n收到信號 %v,正在退出...\n", sig)
			cancel()
		case <-ticker.C:
			fmt.Println("\n=== Hash Maps 統計 ===")
			
			// 顯示進程統計
			fmt.Println("進程檔案操作統計:")
			
			type ProcessInfo struct {
				PID   uint32
				Stats ProcessStats
			}
			
			var processes []ProcessInfo
			
			iterator := processMap.Iterator()
			for iterator.Next() {
				keyBytes := iterator.Key()
				valueBytes, _ := iterator.Value()
				
				if len(keyBytes) == 4 && len(valueBytes) == int(unsafe.Sizeof(ProcessStats{})) {
					pid := binary.LittleEndian.Uint32(keyBytes)
					stats := (*ProcessStats)(unsafe.Pointer(&valueBytes[0]))
					processes = append(processes, ProcessInfo{PID: pid, Stats: *stats})
				}
			}
			
			// 按檔案操作次數排序
			sort.Slice(processes, func(i, j int) bool {
				return processes[i].Stats.FileOpens > processes[j].Stats.FileOpens
			})
			
			// 顯示前10個進程
			count := 10
			if len(processes) < count {
				count = len(processes)
			}
			
			for i := 0; i < count; i++ {
				p := processes[i]
				comm := string(bytes.TrimRight(p.Stats.Comm[:], "\x00"))
				fmt.Printf("  PID %d (%s): %d 次檔案操作\n", 
					p.PID, comm, p.Stats.FileOpens)
			}
			
			// 顯示檔案存取統計
			fmt.Println("\n檔案存取統計 (雜湊):")
			fileIterator := fileAccessMap.Iterator()
			fileCount := 0
			for fileIterator.Next() && fileCount < 5 {
				keyBytes := fileIterator.Key()
				valueBytes, _ := fileIterator.Value()
				
				if len(keyBytes) == 8 && len(valueBytes) == 8 {
					hash := binary.LittleEndian.Uint64(keyBytes)
					count := binary.LittleEndian.Uint64(valueBytes)
					fmt.Printf("  Hash 0x%x: %d 次存取\n", hash, count)
					fileCount++
				}
			}
		}
	}
}
func runRingBufDemo(ctx context.Context, module *bpf.Module, sigChan chan os.Signal) {
	eventsMap, err := module.GetMap("events")
	if err != nil {
		log.Fatalf("獲取 events ringbuf 失敗: %v", err)
	}
	// 建立 ring buffer
	rb, err := module.InitRingBuf("events", make(chan []byte, 100))
	if err != nil {
		log.Fatalf("初始化 ring buffer 失敗: %v", err)
	}
	defer rb.Close()
	// 開始 polling
	rb.Poll(300) // 300ms timeout
	fmt.Println("開始監聽檔案事件 (Ring Buffer)...")
	fmt.Println("執行一些檔案操作來觸發事件...")
	eventCount := 0
	for {
		select {
		case <-ctx.Done():
			return
		case sig := <-sigChan:
			fmt.Printf("\n收到信號 %v,正在退出...\n", sig)
			cancel()
		case data := <-rb.EventsChannel:
			if len(data) == int(unsafe.Sizeof(FileEvent{})) {
				event := (*FileEvent)(unsafe.Pointer(&data[0]))
				
				comm := string(bytes.TrimRight(event.Comm[:], "\x00"))
				filename := string(bytes.TrimRight(event.Filename[:event.FilenameLen], "\x00"))
				
				eventCount++
				fmt.Printf("[%d] PID %d (%s) 開啟檔案: %s\n", 
					eventCount, event.PID, comm, filename)
			}
		}
	}
}
更新 Makefile 以支援多個程式:
# 更新 Makefile
BPF_PROGS := array_maps_demo.bpf.o hash_maps_demo.bpf.o ringbuf_demo.bpf.o
all: vmlinux.h $(BPF_PROGS) userspace
$(BPF_PROGS): %.bpf.o: $(KERNEL_SRC)/%.bpf.c | vmlinux.h
	@echo "編譯 eBPF 程式: $<"
	$(CLANG) $(BPF_CFLAGS) -I$(HEADERS) -c $< -o $@
	$(LLVM_STRIP) -g $@
userspace: bin/maps_demo
bin/maps_demo: $(USERSPACE_SRC)/maps_demo.go
	@echo "編譯使用者空間程式..."
	mkdir -p bin
	$(GO) build -o $@ $<
test-array: all
	sudo ./bin/maps_demo array
test-hash: all  
	sudo ./bin/maps_demo hash
test-ringbuf: all
	sudo ./bin/maps_demo ringbuf
編譯和測試:
# 編譯所有程式
make all
# 測試 Array Maps
make test-array
# 測試 Hash Maps  
make test-hash
# 測試 Ring Buffer
make test-ringbuf
不同 Map 類型的效能特性:
| Map 類型 | 查找複雜度 | 記憶體使用 | 併發性能 | 適用場景 | 
|---|---|---|---|---|
| Array | O(1) | 固定 | 優秀 | 固定索引、統計計數 | 
| Hash | O(1) 平均 | 動態 | 良好 | 動態鍵值、進程追蹤 | 
| LRU Hash | O(1) | 有界 | 良好 | 快取、最近使用 | 
| Ring Buffer | O(1) | 環形 | 優秀 | 事件流、日誌 | 
// Per-CPU Maps 避免鎖爭用
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_HASH);
    __uint(max_entries, 1000);
    __type(key, __u32);
    __type(value, __u64);
} percpu_map SEC(".maps");
// 在多核系統上,每個 CPU 有獨立的副本
SEC("tp/syscalls/sys_enter_write")
int count_writes(void *ctx)
{
    __u32 pid = bpf_get_current_pid_tgid() >> 32;
    __u64 *count = bpf_map_lookup_elem(&percpu_map, &pid);
    if (count) {
        *count += 1; // 無需原子操作,只在當前 CPU 上修改
    }
    return 0;
}
使用者空間需要聚合所有 CPU 的值:
// 讀取 Per-CPU Map 的值
func readPerCPUValue(m *bpf.BPFMap, key unsafe.Pointer) (uint64, error) {
    values, err := m.GetValuePerCPU(key)
    if err != nil {
        return 0, err
    }
    
    var total uint64
    for _, valueBytes := range values {
        if len(valueBytes) == 8 {
            val := binary.LittleEndian.Uint64(valueBytes)
            total += val
        }
    }
    return total, nil
}
// Map 大小設計原則
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10000);  // 根據實際需求設定
    __uint(map_flags, BPF_F_NO_PREALLOC); // 延遲分配記憶體
    __type(key, __u32);
    __type(value, struct large_value);
} optimized_map SEC(".maps");
// 使用 LRU Map 自動淘汰舊項目
struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __uint(max_entries, 1000);
    __type(key, __u32);
    __type(value, __u64);
} lru_map SEC(".maps");
// 使用原子操作
__sync_fetch_and_add(&stats->counter, 1);
// 或使用 CAS 操作
__u64 old_val, new_val;
do {
    old_val = stats->counter;
    new_val = old_val + 1;
} while (!__sync_bool_compare_and_swap(&stats->counter, old_val, new_val));
sudo bpftool map show
sudo bpftool map dump id <MAP_ID>
# 檢查 Map 統計
cat /proc/kallsyms | grep bpf_map
今天我們深入探討了 eBPF Maps 的各個方面:
Maps 是 eBPF 程式設計的核心,掌握了 Maps 的使用,就掌握了 eBPF 開發的精髓。在下一篇文章中,我們將探索 XDP 網路程式設計,學習如何在最底層處理網路封包。