Maps: Bộ Nhớ và Cầu Nối Với Userspace

K
Kai··4 min read

Chương trình eBPF ở các bài trước chạy một lần cho mỗi sự kiện rồi kết thúc — verifier còn bắt buộc nó phải kết thúc (Bài 2). Vậy nó nhớ gì giữa hai lần chạy? Làm sao userspace lấy được kết quả nó tính? Câu trả lời cho cả hai là maps.

Maps là gì

Maps là cấu trúc dữ liệu key-value sống trong nhân, tách rời khỏi vòng đời một lần chạy của chương trình. Chúng phục vụ hai việc:

  • Giữ trạng thái giữa các lần chương trình chạy (mỗi lần exec một event, chương trình ghi vào map; map còn đó ở lần sau).
  • Cầu nối với userspace: tiến trình userspace đọc/ghi cùng map đó qua bpf() syscall — đây là cách một công cụ eBPF báo cáo kết quả ra ngoài, và cách userspace cấu hình chương trình.
   sự kiện ──► [chương trình eBPF] ──ghi──► ┌─────────┐ ◄──đọc/ghi── [userspace]
   (mỗi lần                                  │   MAP   │              (bpftool,
    chạy rồi tắt)                            └─────────┘               app của bạn)
                                          (sống độc lập, qua bpf() syscall)

Có nhiều loại map cho nhu cầu khác nhau: array (chỉ số), hash (key tùy ý), percpu_* (mỗi CPU một bản, tránh tranh chấp), lru_hash (tự đào thải), ringbuf (đẩy sự kiện sang userspace)... Phần lớn datapath Cilium giữ trạng thái trong maps.

Soi một map production

bpftool map dump đọc thẳng nội dung map đang chạy. Lấy cilium_metrics (id 171, kiểu percpu_hash) mà Bài 0 đã thấy:

sudo bpftool map dump id 171
key:
03 01 63 04 01 00 00 00
value (CPU 00): cb 33 03 00 00 00 00 00  ...
value (CPU 01): 96 43 04 00 00 00 00 00  ...

Đây là metric thật Cilium đang gom — và chú ý mỗi CPU một value riêng (percpu). Đó là một thủ thuật quan trọng: nếu nhiều CPU cùng tăng một bộ đếm chung thì phải khóa/atomic (chậm); với per-CPU map, mỗi CPU ghi bản riêng không tranh chấp, userspace cộng lại khi đọc. Cilium dùng kiểu này cho đường nóng xử lý gói.

Tự viết: đếm exec vào một map

Để thấy trọn vòng "chương trình ghi — userspace đọc", viết một chương trình đếm mỗi lần hệ thống exec một tiến trình. Map khai bằng cú pháp BTF ngay trong C:

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, __u64);
} exec_count SEC(".maps");

SEC("tracepoint/sched/sched_process_exec")
int count_exec(void *ctx)
{
    __u32 key = 0;
    __u64 *val = bpf_map_lookup_elem(&exec_count, &key);   // helper truy cập map
    if (val)
        __sync_fetch_and_add(val, 1);                  // tăng atomic
    return 0;
}

Hai điểm cơ chế: chương trình không đụng map trực tiếp mà gọi helper bpf_map_lookup_elem (Bài 0 — eBPF chỉ gọi helper, không hàm nhân tùy ý); và vì đây là array thường (một value chung cho mọi CPU), phải __sync_fetch_and_add (atomic) để hai CPU không ghi đè nhau — chính là cái per-CPU map tránh được.

Biên dịch, nạp và tự gắn vào tracepoint:

clang -O2 -g -target bpf -I/usr/include/x86_64-linux-gnu -c count_exec.bpf.c -o count_exec.bpf.o
sudo bpftool prog loadall count_exec.bpf.o /sys/fs/bpf/cexec autoattach

Đọc counter, chạy vài lệnh (mỗi lệnh là một exec), rồi đọc lại:

sudo bpftool map dump name exec_count          # value: 206
for i in 1 2 3 4 5; do /bin/true; /bin/date >/dev/null; done
sudo bpftool map dump name exec_count
[{
        "key": 0,
        "value": 233
    }
]

206233. Bộ đếm tăng vì chương trình eBPF trong nhân tăng nó mỗi lần có exec (cả lệnh ta chạy lẫn exec nền của hệ thống), còn bpftool ở userspace đọc cùng map đó qua bpf() syscall. Không có map, chương trình chạy xong là quên; có map, nó tích lũy và nói được kết quả ra ngoài. (Con số khởi điểm là 206 chứ không phải 0 vì từ lúc gắn tới lúc đọc đầu, hệ thống đã exec 206 lần — nó đếm sự kiện thật.)

🧹 Dọn dẹp

sudo rm -rf /sys/fs/bpf/cexec          # gỡ pin -> giải phóng chương trình + map
rm -f /tmp/count_exec.bpf.*

Gỡ pin là chương trình lẫn map nó tạo đều được giải phóng; node về 140 chương trình. Mã nguồn ở github.com/nghiadaulau/ebpf-from-scratch, thư mục 03-maps.

Tổng kết

Chương trình eBPF chạy theo sự kiện rồi tắt, không giữ biến giữa các lần — maps là bộ nhớ của nó và là cầu nối với userspace: chương trình ghi qua helper (bpf_map_lookup_elem/update), userspace đọc/ghi cùng map qua bpf() syscall. Ta soi cilium_metrics thật (kiểu percpu_hash, mỗi CPU một value để tránh tranh chấp trên đường nóng), rồi tự viết một chương trình tracepoint đếm exec vào một array map (dùng __sync_fetch_and_add atomic vì array chia chung): nạp + autoattach, và bpftool map dump cho thấy counter tăng 206→233 khi hệ thống exec — chương trình nhân ghi, userspace đọc, đúng vòng. Chọn loại map (array/hash/percpu/lru/ringbuf) là chọn đánh đổi giữa cấu trúc key, tranh chấp CPU, và cách đẩy dữ liệu ra.

Bài 4 trả lời câu "chương trình gắn vào đâu": các program typehook — vì sao một chương trình XDP thấy được gói tin còn một tracepoint thấy được tham số syscall, và mỗi loại được phép làm gì.

Related Posts