Skip to content

超越 eBPF 的极限:在内核模块中定义自定义 kfunc

你是否曾经觉得 eBPF 的能力有限?也许你遇到了现有 eBPF 功能无法实现目标的情况。或许你需要与内核进行更深层次的交互,或者标准 eBPF 运行时无法解决的性能问题。如果你曾经希望在 eBPF 程序中拥有更多的灵活性和强大功能,那么本教程正适合你。

引言:添加 strstr kfunc 以突破 eBPF 运行时的限制

eBPF(扩展伯克利包过滤器) 通过允许开发者在内核中运行受沙箱限制的程序,彻底改变了 Linux 系统编程。它在网络、安全和可观测性方面具有革命性的作用,能够实现强大的功能,而无需修改内核源代码或加载传统的内核模块。

但尽管 eBPF 非常强大,它也并非没有局限性:

  • 功能差距: 有时,eBPF 运行时的现有功能无法提供你所需的特定能力。
  • 复杂需求: 某些任务需要更复杂的内核交互,而 eBPF 无法开箱即用地处理这些需求。
  • 性能问题: 在某些情况下,eBPF 运行时的开销会引入延迟,或者在高性能需求下效率不够。

这些挑战源于整个 eBPF 运行时的限制,而不仅仅是其辅助函数。那么,如何在不修改内核本身的情况下克服这些障碍呢?

引入kfunc(BPF 内核函数)。通过在内核模块中定义你自己的 kfunc,可以将 eBPF 的能力扩展到默认限制之外。这种方法让你能够:

  • 增强功能: 引入标准 eBPF 运行时中不可用的新操作。
  • 定制行为: 根据你的特定需求定制内核交互。
  • 提升性能: 通过在内核上下文中直接执行自定义代码,优化关键路径。

在本教程中,我们将特别添加一个 strstr kfunc。 由于 eBPF 的验证器限制,直接在 eBPF 中实现字符串搜索是具有挑战性的,而将其定义为 kfunc 则允许我们安全高效地绕过这些限制,执行更复杂的操作。

最棒的是,你可以在不修改核心内核的情况下实现这一目标,保持系统的稳定性和代码的安全性。

在本教程中,我们将展示如何定义自定义 kfunc 以填补 eBPF 功能的任何空白。我们将逐步讲解如何创建一个引入新 kfunc 的内核模块,并演示如何在 eBPF 程序中使用它们。无论你是希望克服性能瓶颈,还是需要 eBPF 运行时未提供的功能,自定义 kfunc 都能为你的项目解锁新的可能性。

理解 kfunc:扩展 eBPF 超越辅助函数

什么是 kfunc?

BPF 内核函数(kfuncs) 是 Linux 内核中的专用函数,供 eBPF 程序使用。与标准的 eBPF 辅助函数不同,kfuncs 没有稳定的接口,并且在不同的内核版本之间可能有所变化。这种可变性意味着使用 kfuncs 的 BPF 程序需要与内核更新同步更新,以保持兼容性和稳定性。

为什么使用 kfuncs?

  1. 扩展功能: kfuncs 允许执行标准 eBPF 辅助函数无法完成的操作。
  2. 定制化: 定义针对特定用例量身定制的逻辑,增强 eBPF 程序的灵活性。
  3. 安全与稳定: 通过将 kfuncs 封装在内核模块中,避免直接修改核心内核,保持系统完整性。

kfuncs 在 eBPF 中的角色

kfuncs 作为 eBPF 程序与更深层次内核功能之间的桥梁。它们允许 eBPF 程序执行更复杂的操作,通过暴露现有内核函数或引入专为 eBPF 交互设计的新包装函数。这种集成在确保 eBPF 程序保持安全和可维护的同时,促进了更深入的内核交互。

需要注意的是,Linux 内核已经包含了大量的 kfuncs。这些内置的 kfuncs 覆盖了广泛的功能,大多数开发者无需定义新的 kfuncs 就能完成任务。然而,在现有 kfuncs 无法满足特定需求的情况下,定义自定义 kfuncs 就变得必要。本教程将演示如何定义新的 kfuncs 以填补任何空白,确保你的 eBPF 程序能够利用你所需的确切功能。eBPF 也可以扩展到用户空间。在用户空间 eBPF 运行时 bpftime 中,我们也在实现 ufuncs,它们类似于 kfuncs,但扩展了用户空间应用程序。

kfuncs 及其演变概述

要理解 kfuncs 的重要性,必须了解它们与 eBPF 辅助函数的演变关系。

累计辅助函数和 kfunc 时间线

关键要点:

  • 辅助函数的稳定性: eBPF 辅助函数保持了高度的稳定性,新增内容较少。
  • kfuncs 的快速增长: kfuncs 的采用和创建显著增加,表明社区有兴趣通过 kfuncs 扩展内核交互。
  • 向更深层次内核集成的转变: 自 2023 年以来,新的用例主要利用 kfuncs 影响内核行为,显示出通过 kfuncs 实现更深层次内核集成的趋势。

这一趋势凸显了社区通过 kfuncs 更深入地与内核集成,推动 eBPF 能力边界的决心。

定义你自己的 kfunc:分步指南

为了利用 kfuncs 的强大功能,你需要在内核模块中定义它们。这个过程确保你的自定义函数能够安全地暴露给 eBPF 程序,而无需修改核心内核。

编写内核模块

让我们从创建一个简单的内核模块开始,该模块定义一个 strstr kfunc。这个 kfunc 将执行子字符串搜索操作,作为理解机制的基础。

文件:hello.c

#include <linux/init.h>       // 模块初始化宏
#include <linux/module.h>     // 加载模块的核心头文件
#include <linux/kernel.h>     // 内核日志宏
#include <linux/bpf.h>
#include <linux/btf.h>
#include <linux/btf_ids.h>

/* 声明 kfunc 原型 */
__bpf_kfunc int bpf_strstr(const char *str, u32 str__sz, const char *substr, u32 substr__sz);

/* 开始 kfunc 定义 */
__bpf_kfunc_start_defs();

/* 定义 bpf_strstr kfunc */
__bpf_kfunc int bpf_strstr(const char *str, u32 str__sz, const char *substr, u32 substr__sz)
{
    // 边界情况:如果 substr 为空,返回 0(假设空字符串在开始处找到)
    if (substr__sz == 0)
    {
        return 0;
    }
    // 边界情况:如果子字符串比主字符串长,则无法找到
    if (substr__sz > str__sz)
    {
        return -1; // 返回 -1 表示未找到
    }
    // 遍历主字符串,考虑大小限制
    for (size_t i = 0; i <= str__sz - substr__sz; i++)
    {
        size_t j = 0;
        // 将子字符串与当前主字符串位置进行比较
        while (j < substr__sz && str[i + j] == substr[j])
        {
            j++;
        }
        // 如果整个子字符串都匹配
        if (j == substr__sz)
        {
            return i; // 返回第一次匹配的索引
        }
    }
    // 如果未找到子字符串,返回 -1
    return -1;
}

/* 结束 kfunc 定义 */
__bpf_kfunc_end_defs();

/* 定义 BTF kfuncs ID 集 */
BTF_KFUNCS_START(bpf_kfunc_example_ids_set)
BTF_ID_FLAGS(func, bpf_strstr)
BTF_KFUNCS_END(bpf_kfunc_example_ids_set)

/* 注册 kfunc ID 集 */
static const struct btf_kfunc_id_set bpf_kfunc_example_set = {
    .owner = THIS_MODULE,
    .set = &bpf_kfunc_example_ids_set,
};

/* 模块加载时执行的函数 */
static int __init hello_init(void)
{
    int ret;

    printk(KERN_INFO "Hello, world!\n");
    /* 注册 BPF_PROG_TYPE_KPROBE 的 BTF kfunc ID 集 */
    ret = register_btf_kfunc_id_set(BPF_PROG_TYPE_KPROBE, &bpf_kfunc_example_set);
    if (ret)
    {
        pr_err("bpf_kfunc_example: 注册 BTF kfunc ID 集失败\n");
        return ret;
    }
    printk(KERN_INFO "bpf_kfunc_example: 模块加载成功\n");
    return 0; // 成功返回 0
}

/* 模块卸载时执行的函数 */
static void __exit hello_exit(void)
{
    /* 取消注册 BTF kfunc ID 集 */
    unregister_btf_kfunc_id_set(BPF_PROG_TYPE_KPROBE, &bpf_kfunc_example_set);
    printk(KERN_INFO "再见,世界!\n");
}

/* 定义模块的初始化和退出点的宏 */
module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");                 // 许可证类型(GPL)
MODULE_AUTHOR("Your Name");            // 模块作者
MODULE_DESCRIPTION("一个简单的模块"); // 模块描述
MODULE_VERSION("1.0");                 // 模块版本

代码解释:

  • 声明 kfunc: __bpf_kfunc 宏声明一个 eBPF 程序可以调用的函数。在这里,bpf_strstr 执行给定字符串中的子字符串搜索。

  • BTF 定义: __bpf_kfunc_start_defs__bpf_kfunc_end_defs 宏标示 kfunc 定义的开始和结束。BTF_KFUNCS_START 及相关宏帮助将 kfuncs 注册到 BPF 类型格式(BTF)。

  • 模块初始化: hello_init 函数注册 kfunc ID 集,使 bpf_strstr 可用于 BPF_PROG_TYPE_KPROBE 类型的 eBPF 程序。

  • 模块清理: hello_exit 函数确保在模块移除时取消注册 kfunc ID 集,保持系统整洁。

文件:Makefile

obj-m += hello.o  # hello.o 是目标

# 启用 BTF 生成
KBUILD_CFLAGS += -g -O2

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Makefile 解释:

  • 目标定义: obj-m += hello.o 指定 hello.o 是要构建的模块。

  • BTF 生成标志: KBUILD_CFLAGS += -g -O2 启用调试信息和优化,便于 BTF 生成。

  • 构建命令:

  • all: 通过调用内核构建系统编译内核模块。
  • clean: 清理构建产物。

注意: 提供的代码在 Linux 内核版本 6.11 上进行了测试。如果你使用的是较早的版本,可能需要实现一些变通方法,例如引用 compact.h

编译内核模块

在内核模块源代码和 Makefile 就位后,按照以下步骤编译模块:

  1. 导航到模块目录:
cd /path/to/bpf-developer-tutorial/src/43-kfuncs/module/
  1. 编译模块:
make

该命令将生成一个名为 hello.ko 的文件,即编译后的内核模块。

加载内核模块

要将编译好的模块插入内核,使用 insmod 命令:

sudo insmod hello.ko

验证模块加载

加载模块后,通过检查内核日志验证其是否成功插入:

dmesg | tail

预期输出:

[ 1234.5678] Hello, world!
[ 1234.5679] bpf_kfunc_example: 模块加载成功

移除内核模块

当不再需要该模块时,使用 rmmod 命令卸载它:

sudo rmmod hello

验证移除:

dmesg | tail

预期输出:

[ 1234.9876] 再见,世界!

处理编译错误

在编译过程中,可能会遇到以下错误:

Skipping BTF generation for /root/bpf-developer-tutorial/src/43-kfuncs/module/hello.ko due to unavailability of vmlinux

解决方案:

  1. 安装 dwarves 包:

dwarves 包提供了生成 BTF 所需的工具。

sudo apt install dwarves
  1. 复制 vmlinux 文件:

确保包含 BTF 信息的 vmlinux 文件在构建目录中可用。

sudo cp /sys/kernel/btf/vmlinux /usr/lib/modules/$(uname -r)/build/

该命令将 vmlinux 文件复制到适当的构建目录,确保成功生成 BTF。

本教程的完整代码可在 bpf-developer-tutorial 仓库 的 GitHub 上找到。此代码在 Linux 内核版本 6.11 上进行了测试,对于较低版本,可能需要参考 compact.h 进行一些修改。

在 eBPF 程序中使用自定义 kfunc

有了定义自定义 strstr kfunc 的内核模块后,下一步是创建一个利用此函数的 eBPF 程序。此交互展示了 kfuncs 引入的增强功能。

编写 eBPF 程序

创建一个附加到 do_unlinkat 内核函数并使用自定义 bpf_strstr kfunc 的 eBPF 程序。

文件:kfunc.c

/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#define BPF_NO_GLOBAL_DATA
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

typedef unsigned int u32;
typedef long long s64;

/* 声明外部 kfunc */
extern int bpf_strstr(const char *str, u32 str__sz, const char *substr, u32 substr__sz) __ksym;

char LICENSE[] SEC("license") = "Dual BSD/GPL";

SEC("kprobe/do_unlinkat")
int handle_kprobe(struct pt_regs *ctx)
{
    pid_t pid = bpf_get_current_pid_tgid() >> 32;
    char str[] = "Hello, world!";
    char substr[] = "wor";
    int result = bpf_strstr(str, sizeof(str) - 1, substr, sizeof(substr) - 1);
    if (result != -1)
    {
        bpf_printk("'%s' found in '%s' at index %d\n", substr, str, result);
    }
    bpf_printk("Hello, world! (pid: %d) bpf_strstr %d\n", pid, result);
    return 0;
}

eBPF 代码解释:

  • 外部 kfunc 声明: extern 关键字声明 bpf_strstr 函数,使其在 eBPF 程序中可用。

  • Kprobe 附加: SEC("kprobe/do_unlinkat") 宏将 eBPF 程序附加到 do_unlinkat 内核函数。每次调用 do_unlinkat 时,handle_kprobe 函数都会执行。

  • 使用 kfunc:handle_kprobe 中,eBPF 程序调用 bpf_strstr,传入四个参数:

  • str: 要搜索的主字符串。
  • str__sz: 主字符串的大小。
  • substr: 要搜索的子字符串。
  • substr__sz: 子字符串的大小。

结果(子字符串在主字符串中的首次出现索引,或 -1 表示未找到)然后通过 bpf_printk 打印,显示 PID 和结果。

重要提示: 由于验证器限制,直接在 eBPF 中实现类似 strstr 的函数具有挑战性,因为这限制了循环和复杂的内存访问。通过将 strstr 实现为 kfunc,我们绕过了这些限制,使得在 eBPF 程序中执行更复杂和高效的字符串操作成为可能。

编译 eBPF 程序

要编译 eBPF 程序,确保你已安装必要的工具,如 clangllvm。以下是编译程序的步骤:

  1. 导航到 eBPF 程序目录:
cd /path/to/bpf-developer-tutorial/src/43-kfuncs/
  1. 为 eBPF 程序创建一个 Makefile
# 文件:Makefile

CLANG ?= clang
LLVM_STRIP ?= llvm-strip
BPF_TARGET := bpf

CFLAGS := -O2 -g -target $(BPF_TARGET) -Wall -Werror -I/usr/include

all: kfunc.o

kfunc.o: kfunc.c
    $(CLANG) $(CFLAGS) -c $< -o $@

clean:
    rm -f kfunc.o
  1. 编译 eBPF 程序:
make

该命令将生成一个名为 kfunc.o 的文件,即编译后的 eBPF 对象文件。

运行 eBPF 程序

假设你有一个用户空间应用程序或工具来加载和附加 eBPF 程序,你可以执行它以观察 eBPF 程序与自定义 kfunc 之间的交互。

示例输出:

# sudo ./load_kfunc
BPF 程序已加载并成功附加。按 Ctrl-C 退出。

然后,当调用 do_unlinkat 函数时(例如,当文件被取消链接时),你可以检查内核日志:

dmesg | tail

预期输出:

[ 1234.5678] 'wor' found in 'Hello, world!' at index 7
[ 1234.5679] Hello, world! (pid: 2075) bpf_strstr 7

输出解释:

每次内核调用 do_unlinkat 函数时,eBPF 程序都会打印一条消息,指示进程的 PID 以及 kfunc 调用的结果。在此示例中,子字符串 "wor" 在字符串 "Hello, world!" 的索引 7 处被找到。

总结与结论

在本教程中,我们深入探讨了通过定义和使用自定义内核函数(kfuncs)来扩展 eBPF 的能力。以下是我们涵盖的内容回顾:

  • 理解 kfuncs: 理解了 kfuncs 的概念及其在标准辅助函数之外增强 eBPF 的角色。
  • 定义 kfuncs: 创建了一个内核模块,定义了自定义的 strstr kfunc,确保其能够安全地暴露给 eBPF 程序,而无需修改核心内核。
  • 编写包含 kfuncs 的 eBPF 程序: 开发了一个利用自定义 kfunc 的 eBPF 程序,展示了增强的功能。
  • 编译与执行: 提供了逐步指南,编译、加载并运行内核模块和 eBPF 程序,确保你可以在自己的系统上复制设置。
  • 错误处理: 解决了潜在的编译问题,并提供了解决方案,确保顺利的开发体验。

关键要点:

  • 克服辅助函数的限制: kfuncs 弥合了标准 eBPF 辅助函数留下的空白,提供了针对特定需求的扩展功能。
  • 维护系统稳定性: 通过将 kfuncs 封装在内核模块中,确保系统稳定性,而无需对内核进行侵入性更改。
  • 社区驱动的演变: kfuncs 的快速增长和采用凸显了 eBPF 社区致力于通过 kfuncs 推动内核级编程可能性的决心。
  • 利用现有 kfuncs: 在定义新的 kfuncs 之前,探索内核提供的现有 kfuncs。它们涵盖了广泛的功能,减少了除非绝对必要,否则无需创建自定义函数的需求。

准备好进一步提升你的 eBPF 技能了吗? 访问我们的教程仓库探索我们网站上的更多教程。深入丰富的示例,深化你的理解,并为 eBPF 的动态世界做出贡献!

祝你在 eBPF 的旅程中愉快!

参考资料

附加资源

如果你想了解更多关于 eBPF 的知识和实践,可以访问我们的开源教程代码仓库 bpf-developer-tutorial 或访问我们的网站 eunomia.dev/tutorials 以获取更多示例和完整代码。