先看看之前的sockmap sockmap_ebpf sock_map2 ipvs-ebpf
EBPF:本質上它是一種內核代碼注入的技術
- 內核中實現了一個cBPF/eBPF虛擬機
- 用戶態可以用C來寫運行的代碼,再通過一個Clang&LLVM的編譯器將C代碼編譯成BPF目標碼
- 用戶態通過系統調用bpf()將BPF目標碼注入到內核當中
- 內核通過JIT(Just-In-Time)將BPF目編碼轉換成本地指令碼;如果當前架構不支持JIT轉換內核則會使用一個解析器(interpreter)來模擬運行,這種運行效率較低;
- 內核在packet filter和tracing等應用中提供了一系列的鈎子來運行BPF代碼。目前支持以下類型的BPF代碼
提供了一種在不修改內核代碼的情況下,可以靈活修改內核處理策略的方法
#include <uapi/linux/bpf.h> #include <uapi/linux/if_ether.h> #include <uapi/linux/if_packet.h> #include <uapi/linux/ip.h> #include <bpf/bpf_helpers.h> #include "bpf_legacy.h" struct { __uint(type, BPF_MAP_TYPE_ARRAY); __type(key, u32); __type(value, long); __uint(max_entries, 256); } my_map SEC(".maps"); SEC("socket1") int bpf_prog1(struct __sk_buff *skb) { int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol)); long *value; if (skb->pkt_type != PACKET_OUTGOING) return 0; value = bpf_map_lookup_elem(&my_map, &index); if (value) __sync_fetch_and_add(value, skb->len); return 0; } char _license[] SEC("license") = "GPL";
只有一個 my_map 數據結構和 bpf_prog1 函數;bpf_prog1 就是我們在內核執行的程序片段,它的入參是報文 skb。這個函數完成了以下功能:
- 統計各個協議報文的數據量
// SPDX-License-Identifier: GPL-2.0 #include <stdio.h> #include <assert.h> #include <linux/bpf.h> #include <bpf/bpf.h> #include <bpf/libbpf.h> #include "sock_example.h" #include <unistd.h> #include <arpa/inet.h> int main(int ac, char **argv) { struct bpf_object *obj; int map_fd, prog_fd; char filename[256]; int i, sock; FILE *f; snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]); /* 裝載文件 sockex1_kern.o */ if (bpf_prog_load(filename, BPF_PROG_TYPE_SOCKET_FILTER, &obj, &prog_fd)) return 1; map_fd = bpf_object__find_map_fd_by_name(obj, "my_map"); sock = open_raw_sock("lo"); /* 創建一個 socket, bind 到環回口設備 */ /* 設置 socket 的 SO_ATTACH_BPF 選項,傳入 prog_fd */ assert(setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)) == 0); f = popen("ping -4 -c5 localhost", "r"); (void) f; for (i = 0; i < 5; i++) { long long tcp_cnt, udp_cnt, icmp_cnt; int key; key = IPPROTO_TCP; assert(bpf_map_lookup_elem(map_fd, &key, &tcp_cnt) == 0); key = IPPROTO_UDP; assert(bpf_map_lookup_elem(map_fd, &key, &udp_cnt) == 0); key = IPPROTO_ICMP; assert(bpf_map_lookup_elem(map_fd, &key, &icmp_cnt) == 0); printf("TCP %lld UDP %lld ICMP %lld bytes\n", tcp_cnt, udp_cnt, icmp_cnt); sleep(1); } return 0; }
sock_user:代碼核心分析:
- bpf_prog_load的入參 sockex1_user.o 是如何轉換成虛擬機機器碼注入內核的?
- 內核代碼何時執行,執行的上下文是什么?
- 用戶空間和內核空間的程序是如何通過 map 進行通信?
bpf_prog_load是 libbpf苦衷提供的函數;最后會調用 sys_bpf(BPF_PROG_LOAD, &attr, sizeof(attr)); 將code 注入到內核!!、
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size){
...... case BPF_PROG_LOAD: err = bpf_prog_load(&attr); } static int bpf_prog_load(union bpf_attr *attr) { struct bpf_prog *prog; ...... /* 分配內核 bpf_prog 程序數據結構空間 */ prog = bpf_prog_alloc(bpf_prog_size(attr->insn_cnt), GFP_USER); ..... /* 將 bpf 虛擬機指令從用戶空間拷貝到內核空間 */ copy_from_user(prog->insns, u64_to_user_ptr(attr->insns), bpf_prog_insn_size(prog)); ..... /* 分配一個 fd 與 prog 關聯,最終這個 fd 將返回用戶空間 * /
/*此時 file->private_data = priv; 也就是 file->private_data = prog
表示 注入內核的BPF程序--字節碼 關聯到 fd的priva_data上
所以后續當內核執行hook的時候,根據hook的fd查找到private-data找到bpf代碼執行 */
err = bpf_prog_new_fd(prog); ..... return err; }
用戶空間通過系統調用陷入內核后,內核也會分配相應的數據結構 struct bpf_prog,並從用戶空間拷貝虛擬機指令。然后分配一個文件系統的 inode 節點,將它與 bpf_prog 關聯起來,最后將文件描述符返回給用戶空間。
eBPF 程序指令都是在內核的特定 Hook 點執行,不同類型的程序有不同的鈎子,有不同的上下文
將指令 load 到內核時,內核會創建 bpf_prog 存儲指令,但只是第一步,成功運行這些指令還需要完成以下兩個步驟:
- 將 bpf_prog 與內核中的特定 Hook 點關聯起來,也就是將程序掛到鈎子上。
- 在 Hook 點被訪問到時,取出 bpf_prog,執行這些指令。
比如:
SOCKET FILTER 類型 eBPF 程序通過 SO_ATTACH_BPF 選項完成設置
XDP 類型的 eBPF 程序,則通過 Netlink 的方式設置 Hook 點
每一個 load 到內核的 eBPF 程序都有一個 fd 會返回給用戶,它對應一個 bpf_prog。
XDP 程序設置 Hook 點的方式就是將這個 fd 與 一個網卡聯系起來,通過 Netlink 消息告訴內核。
int main(int argc, char **argv) { struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY}; struct bpf_prog_load_attr prog_load_attr = { .prog_type = BPF_PROG_TYPE_XDP, }; int prog_fd, map_fd, opt; struct bpf_object *obj; struct bpf_map *map; ---------------------- snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]); prog_load_attr.file = filename; if (bpf_prog_load_xattr(&prog_load_attr, &obj, &prog_fd)) return 1; map = bpf_map__next(NULL, obj); map_fd = bpf_map__fd(map); signal(SIGINT, int_exit); signal(SIGTERM, int_exit); if (bpf_set_link_xdp_fd(ifindex, prog_fd, xdp_flags) < 0) { } err = bpf_obj_get_info_by_fd(prog_fd, &info, &info_len); prog_id = info.id; poll_stats(map_fd, 2); return 0; }
其中 ifindex 為網卡的標識,而 prog_fd 為 load 的 eBPF 程序時返回的 fd
int bpf_set_link_xdp_fd(int ifindex, int fd, __u32 flags) { // code omitted ... nla->nla_type = NLA_F_NESTED | IFLA_XDP; // code omitted ... nla_xdp->nla_type = IFLA_XDP_FD; // code omitted ...
bpf_set_link_xdp_fd 打包 Netlink 消息,消息類型為 IFLA_XDP,子類型為 IFLA_XDP_FD, 表示要關聯 bpf_prog
內核收到該 Netlink 消息后, 根據消息類型,最終調用到 dev_change_xdp_fd
do_setlink { // code omitted ... if (tb[IFLA_XDP]) { // code omitted ... if (xdp[IFLA_XDP_FD]) { err = dev_change_xdp_fd(dev, extack, nla_get_s32(xdp[IFLA_XDP_FD]),
expected_fd, xdp_flags); } }
dev_change_xdp_fd 意為為 dev 關聯一個 XDP 程序的 fd。它使用網卡設備驅動程序的 do_bpf 方法,進行 XDP 程序的安裝
/** * dev_change_xdp_fd - set or clear a bpf program for a device rx path * @dev: device * @extack: netlink extended ack * @fd: new program fd or negative value to clear * @expected_fd: old program fd that userspace expects to replace or clear * @flags: xdp-related flags * * Set or clear a bpf program for a device */ int dev_change_xdp_fd(struct net_device *dev, struct netlink_ext_ack *extack, int fd, int expected_fd, u32 flags) { // return f.file->private_data; 同時檢測prog 是否為 TYPE_XDP prog = bpf_prog_get_type_dev(fd, BPF_PROG_TYPE_XDP, bpf_op == ops->ndo_bpf); err = dev_xdp_install(dev, bpf_op, extack, flags, prog); }
每個支持 XDP 的網卡都有自己的 ndo_bpf 實現,以 Intel i40e 為例,其實現為 i40e_xdp
static const struct net_device_ops i40e_netdev_ops = { // code omitted ... .ndo_bpf = i40e_xdp, } static int i40e_xdp(struct net_device *dev, struct netdev_bpf *xdp) { struct i40e_netdev_priv *np = netdev_priv(dev); struct i40e_vsi *vsi = np->vsi; switch (xdp->command) { case XDP_SETUP_PROG: return i40e_xdp_setup(vsi, xdp->prog); // add/remove an XDP program // code omitted ... } static int i40e_xdp_setup(struct i40e_vsi *vsi, struct bpf_prog *prog) { // code omitted ... old_prog = xchg(&vsi->xdp_prog, prog); // code omitted ... for (i = 0; i < vsi->num_queue_pairs; i++) WRITE_ONCE(vsi->rx_rings[i]->xdp_prog, vsi->xdp_prog); }
運行 Hook 點上設置的 eBPF 程序
i40e_clean_rx_irq | |- if (!skb) { xdp.data = page_address(rx_buffer->page) + rx_buffer->page_offset; xdp.data_hard_start = (void *)((u8 *)xdp.data - i40e_rx_offset(rx_ring)); xdp.data_end = (void *)((u8 *)xdp.data + size); skb = i40e_run_xdp(rx_ring, &xdp);i40e_xdp_setup }
static struct sk_buff *i40e_run_xdp(struct i40e_ring *rx_ring, struct xdp_buff *xdp) { int result = I40E_XDP_PASS; #ifdef HAVE_XDP_SUPPORT struct i40e_ring *xdp_ring; struct bpf_prog *xdp_prog; u32 act; int err; rcu_read_lock(); xdp_prog = READ_ONCE(rx_ring->xdp_prog); if (!xdp_prog) goto xdp_out; prefetchw(xdp->data_hard_start); /* xdp_frame write */ act = bpf_prog_run_xdp(xdp_prog, xdp); // 運行 eBPF 程序 switch (act) { case XDP_PASS: rx_ring->xdp_stats.xdp_pass++; break; case XDP_TX: xdp_ring = rx_ring->vsi->xdp_rings[rx_ring->queue_index]; result = i40e_xmit_xdp_ring(xdp, xdp_ring); rx_ring->xdp_stats.xdp_tx++; break; case XDP_REDIRECT: ----------------------------- case XDP_DROP: result = I40E_XDP_CONSUMED; rx_ring->xdp_stats.xdp_drop++; break; } xdp_out: rcu_read_unlock(); return (struct sk_buff *)ERR_PTR(-result); }
運行 eBPF 程序就是使用 BPF_PROG_RUN,對於 XDP 類型的程序來說,其參數除了指令(prog->insnsi)外,就是報文(struct xdp_buff* xdp )
#define BPF_PROG_RUN(filter, ctx) (*(filter)->bpf_func)(ctx, (filter)->insnsi) static u32 bpf_prog_run_xdp(const struct bpf_prog *prog, struct xdp_buff *xdp) { return BPF_PROG_RUN(prog, xdp); }
來自;https://switch-router.gitee.io/blog/bpf-3/