Dive into eBPF (3): 虚拟机程序执行的时机
在 Dive into eBPF(2)中,我们通过例子了解了 eBPF 程序是如何被 load 到内核的,而本文将回答上一篇文章中的第 2 个问题,即:
Q2: 内核指令何时执行,执行的上下文是什么?
先给出答案:eBPF 程序指令都是在内核的特定 Hook 点执行,不同类型的程序有不同的钩子,有不同的上下文
将指令 load 到内核时,内核会创建 bpf_prog 存储指令,但只是第一步,成功运行这些指令还需要完成以下两个步骤:
- 将 bpf_prog 与内核中的特定 Hook 点关联起来,也就是将程序挂到钩子上。
- 在 Hook 点被访问到时,取出 bpf_prog,执行这些指令。
将 bpf_prog 设置到 Hook 点
不同类型的 eBPF 程序在内核中有不同的 Hook 点,它们的设置方式也有些区别。SOCKET FILTER 类型 eBPF 程序通过 SO_ATTACH_BPF 选项完成设置(见Dive into eBPF 1) ,而另一种 XDP 类型的 eBPF 程序,则通过 Netlink 的方式设置 Hook 点。
XDP (eXpress Data Path) 是一个快速的报文处理路径(DataPath), 它的 Hook 点在网卡的驱动程序中(协议栈下面).如下图所示, XDP 可以用来做 DDoS 攻击解决方案, 由于处理时机提前了,它比内核自己的 Netfilter 更加高效.
在 Dive into eBPF 2 中我们知道,每一个 load 到内核的 eBPF 程序都有一个 fd 会返回给用户,它对应一个 bpf_prog。 XDP 程序设置 Hook 点的方式就是将这个 fd 与 一个网卡联系起来,通过 Netlink 消息告诉内核。我们可以通过 sample 目录下的 xdp1_user.c 看到这个过程
int main(int argc, char **argv)
{
// code omitted ...
bpf_set_link_xdp_fd(ifindex, prog_fd, xdp_flags)
// code omitted ...
}
其中 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]),
xdp_flags);
}
}
dev_change_xdp_fd 意为为 dev 关联一个 XDP 程序的 fd。它使用网卡设备驱动程序的 do_bpf 方法,进行 XDP 程序的安装
int dev_change_xdp_fd(struct net_device *dev, struct netlink_Ext_Ack *extack, int fd, u32 flags)
{
const struct net_device_ops *ops = dev->netdev_ops;
bpf_op = bpf_chk = ops->ndo_bpf;
......
prog = bpf_prog_get_type_dev(fd, BPF_PROG_TYPE_XDP, bpf_op == ops->ndo_bpf);
......
dev_xdp_install(dev, bpf_op, extack, flags, prog); // 调用设备驱动的 ndo_bpf 方法,命令为 XDP_SETUP_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 ...
对 i40e 网卡来说,安装 eBPF 程序即是将 bpf_prog 记录到 vsi 和 vsi->rx_rings 上。
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 程序
设备驱动程序在得到报文时,就会看是否安装过 eBPF 程序,如果有,则运行它,返回运行的结果 (如 PASS 还是 DROP)
i40e_clean_rx_irq
|
|- i40e_run_xdp
static struct sk_buff *i40e_run_xdp(struct i40e_ring *rx_ring, struct xdp_buff *xdp)
{
xdp_prog = READ_ONCE(rx_ring->xdp_prog);
act = bpf_prog_run_xdp(xdp_prog, xdp); // 运行 eBPF 程序
switch (act) {
case XDP_PASS:
break;
// code omitted ...
case XDP_DROP:
result = I40E_XDP_CONSUMED;
break;
}
// code omitted ...
}
运行 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);
}