这次我们介绍 bpf 函数,它是 Linux 内核中 **Berkeley Packet Filter **(BPF) 子系统的用户态接口。
1. 函数介绍 bpf 是一个功能极其强大的 Linux 系统调用(内核版本 >= 3.18,但许多高级特性需要更新的内核),它提供了一种在内核空间安全、高效地运行用户定义程序的机制。
data-ad-format="fluid"
data-ad-layout-key="-7k+ex-4a-9w+4a">
你可以把 BPF 想象成一个内核里的虚拟机:
你(用户态程序)可以编写一段用BPF 指令集编写的“小程序”(eBPF 程序)。
你将这段程序加载到内核中。
内核会验证这段程序的安全性(确保它不会导致死循环、不会访问非法内存等)。
如果验证通过,内核会即时编译 (JIT) 这段程序为机器码,并将其附加到特定的内核钩子(hook points)上。
当内核执行到这些钩子时(例如,收到网络包、进行系统调用、跟踪函数调用),就会执行你加载的 BPF 程序。
BPF 程序可以进行过滤、修改、收集信息(遥测)、路由等操作。
主要用途:
网络编程: 高性能数据包过滤(tcpdump)、流量整形、负载均衡、XDP(eXpress Data Path)超高速网络处理。
系统监控和追踪: 跟踪内核函数、用户态函数、系统调用,收集性能指标(如 perf)、调试信息。
安全: 实施安全策略、沙箱、审计。
性能分析: 无侵入式地分析应用程序和内核性能瓶颈。
2. 函数原型 1 2 3 4 #include <linux/bpf.h> // 必需,包含 BPF 相关常量和结构体 long bpf(int cmd, union bpf_attr *attr, unsigned int size);
3. 功能
4. 参数 int cmd: 指定要执行的具体 BPF 操作。这是一个枚举值(定义在 <linux/bpf.h> 中)。常见的命令包括:
BPF_MAP_CREATE: 创建一个 BPF 映射(Map)。映射是 BPF 程序和用户态程序之间共享数据的高效机制。
BPF_PROG_LOAD: 将一个 BPF 程序加载到内核中。
BPF_OBJ_PIN / BPF_OBJ_GET: 将 BPF 对象(程序或映射)固定到文件系统路径或从路径获取。
BPF_PROG_ATTACH / BPF_PROG_DETACH: 将已加载的 BPF 程序附加到或从特定的挂钩点(如 cgroup、网络设备)分离。
BPF_PROG_RUN / BPF_PROG_TEST_RUN: (测试)运行 BPF 程序。
BPF_MAP_LOOKUP_ELEM / BPF_MAP_UPDATE_ELEM / BPF_MAP_DELETE_ELEM: 对 BPF 映射进行查找、更新、删除元素操作。
BPF_PROG_GET_NEXT_ID / BPF_PROG_GET_FD_BY_ID: 枚举和通过 ID 获取 BPF 程序。
BPF_MAP_GET_NEXT_ID / BPF_MAP_GET_FD_BY_ID: 枚举和通过 ID 获取 BPF 映射。
… 还有很多其他命令 …
union bpf_attr *attr: 这是一个指向 union bpf_attr 结构体的指针。这个联合体包含了执行 cmd 指定操作所需的所有可能参数。根据 cmd 的不同,bpf 系统调用会从这个联合体中读取或写入特定的成员。
例如,对于 BPF_MAP_CREATE,它会读取 attr->map_type, attr->key_size, attr->value_size, attr->max_entries 等成员。
对于 BPF_PROG_LOAD,它会读取 attr->prog_type, attr->insn_cnt, attr->insns, attr->license 等成员。
unsigned int size: 指定 attr 指向的 union bpf_attr 结构体的大小(以字节为单位)。内核使用这个大小来进行兼容性检查和内存访问边界控制。
5. union bpf_attr 结构体 union bpf_attr 是一个巨大的联合体,包含了所有 BPF 操作可能需要的参数。它的定义非常庞大,这里只列举几个关键成员以说明其结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 union bpf_attr { struct { /* anonymous struct for BPF_MAP_CREATE */ __u32 map_type; // 映射类型 (BPF_MAP_TYPE_*) __u32 key_size; // 键大小 __u32 value_size; // 值大小 __u32 max_entries; // 最大元素个数 __u32 map_flags; // 标志位 __u32 inner_map_fd; // 用于 array/hash of maps __u32 numa_node; // NUMA 节点 char map_name[BPF_OBJ_NAME_LEN]; // 映射名称 __u32 map_ifindex; // 网络接口索引 // ... 更多字段 ... }; // BPF_MAP_CREATE 使用这些字段 struct { /* anonymous struct for BPF_PROG_LOAD */ __u32 prog_type; // 程序类型 (BPF_PROG_TYPE_*) __u32 insn_cnt; // 指令数量 __aligned_u64 insns; // 指向指令数组的用户态指针 __aligned_u64 license; // 指向许可证字符串的用户态指针 ("GPL") __u32 log_level; // 日志级别 __u32 log_size; // 日志缓冲区大小 __aligned_u64 log_buf; // 指向日志缓冲区的用户态指针 __u32 kern_version; // 内核版本 (用于追踪程序) __u32 prog_flags; // 程序标志 char prog_name[BPF_OBJ_NAME_LEN]; // 程序名称 __u32 prog_ifindex; // 网络接口索引 // ... 更多字段 ... }; // BPF_PROG_LOAD 使用这些字段 // ... 还有很多其他匿名结构体,对应不同的 cmd ... };
6. 返回值 成功时: 返回值取决于具体的 cmd。
对于 BPF_MAP_CREATE, BPF_PROG_LOAD 等创建操作:通常返回一个新的文件描述符(fd),用于引用新创建的 BPF 映射或程序。
对于 BPF_MAP_LOOKUP_ELEM 等查询操作:可能返回 0 表示成功。
对于 BPF_PROG_ATTACH 等操作:可能返回 0 表示成功。
失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EINVAL 参数无效,EACCES 权限不足,ENOMEM 内存不足,E2BIG 程序太大或映射太大,EPERM 操作不被允许等)。
7. 相似函数,或关联函数
libbpf: 一个 C 库,提供了对 bpf 系统调用的高级封装,简化了 eBPF 程序的加载、映射操作和附加过程。这是编写 eBPF 应用程序的推荐方式。
bpftool: 一个命令行工具,用于检查、调试和操作 eBPF 程序和映射。它本身就是 bpf 系统调用的使用者。
LLVM/Clang: 用于将 C 语言编写的 eBPF 程序编译成 BPF 字节码。
perf: 可以与 eBPF 结合使用进行性能分析。
bcc / bpftrace: 更高级别的工具和库,进一步简化了 eBPF 的使用,允许用 Python 或特定领域语言编写脚本。
8. 示例代码 重要提示: 直接使用 bpf 系统调用编写 eBPF 程序非常复杂,涉及大量的底层细节、内存管理和联合体操作。下面的示例将展示一个极其简化的、概念性的 C 代码,旨在说明 bpf 系统调用的调用方式和参数结构。实际的 eBPF 开发通常使用 libbpf 库。
示例 1:概念性地使用 bpf 系统调用 这个例子展示了如何直接调用 bpf 系统调用(通过 syscall)来创建一个简单的 BPF 映射。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 // bpf_conceptual.c // 注意:这是一个非常简化的概念性示例,不包含实际的 eBPF 程序加载。 // 实际使用需要 libbpf 或 LLVM/Clang 工具链。 #define _GNU_SOURCE #include <linux/bpf.h> // 包含 BPF 相关定义 #include <sys/syscall.h> // syscall #include <unistd.h> // close #include <stdio.h> // perror, printf #include <stdlib.h> // exit #include <string.h> // memset #include <errno.h> // errno // 简化包装 syscall static inline long sys_bpf(int cmd, union bpf_attr *attr, unsigned int size) { return syscall(__NR_bpf, cmd, attr, size); } int main() { union bpf_attr attr; int map_fd; printf("Using bpf syscall directly to create a map...\n"); // 1. 清零 attr 联合体 memset(&attr, 0, sizeof(attr)); // 2. 填充 BPF_MAP_CREATE 所需的参数 attr.map_type = BPF_MAP_TYPE_ARRAY; // 创建一个数组类型的映射 attr.key_size = sizeof(int); // 键是 int 类型 (4 bytes) attr.value_size = sizeof(long long); // 值是 long long 类型 (8 bytes) attr.max_entries = 10; // 数组大小为 10 // attr.map_flags = 0; // 可以设置标志,这里用默认值 snprintf(attr.map_name, sizeof(attr.map_name), "my_array_map"); // 设置映射名称 printf("Creating BPF_MAP_TYPE_ARRAY with:\n"); printf(" map_type: %u (BPF_MAP_TYPE_ARRAY)\n", attr.map_type); printf(" key_size: %u bytes\n", attr.key_size); printf(" value_size: %u bytes\n", attr.value_size); printf(" max_entries: %u\n", attr.max_entries); printf(" map_name: %s\n", attr.map_name); // 3. 调用 bpf 系统调用 (BPF_MAP_CREATE) printf("Calling bpf(BPF_MAP_CREATE, ...)\n"); map_fd = sys_bpf(BPF_MAP_CREATE, &attr, sizeof(attr)); if (map_fd < 0) { perror("bpf BPF_MAP_CREATE failed"); if (errno == EPERM) { printf("Permission denied. You might need to run this as root or adjust capabilities.\n"); printf("Try: sudo ./bpf_conceptual\n"); } exit(EXIT_FAILURE); } printf("BPF map created successfully. File descriptor: %d\n", map_fd); // 4. (概念性) 使用 map_fd 进行后续操作 // 例如,使用 BPF_MAP_UPDATE_ELEM 更新元素 // union bpf_attr update_attr; // memset(&update_attr, 0, sizeof(update_attr)); // update_attr.map_fd = map_fd; // int key = 5; // long long value = 1234567890LL; // update_attr.key = (unsigned long)&key; // update_attr.value = (unsigned long)&value; // update_attr.flags = BPF_ANY; // 如果存在则更新,否则创建 // if (sys_bpf(BPF_MAP_UPDATE_ELEM, &update_attr, sizeof(update_attr)) == -1) { // perror("bpf BPF_MAP_UPDATE_ELEM failed"); // } else { // printf("Successfully updated element at key %d to value %lld\n", key, value); // } // 5. 关闭映射文件描述符 printf("Closing BPF map file descriptor...\n"); if (close(map_fd) == -1) { perror("close BPF map fd failed"); } else { printf("BPF map file descriptor closed.\n"); } printf("Conceptual bpf syscall example completed.\n"); return 0; }
**代码解释 **(概念性):
定义 sys_bpf 包装 syscall(__NR_bpf, …),因为 glibc 可能没有直接包装 bpf。
声明 union bpf_attr attr 用于传递参数。
清零 attr 联合体,这是一个好习惯,确保未使用的字段为 0。
填充 attr:
map_type = BPF_MAP_TYPE_ARRAY: 指定创建数组映射。
key_size = sizeof(int): 键是 4 字节整数。
value_size = sizeof(long long): 值是 8 字节长整数。
max_entries = 10: 数组包含 10 个元素。
snprintf(attr.map_name, …): 设置映射的名称。
调用 sys_bpf:
检查返回值:
打印成功信息和返回的文件描述符。
概念性操作: 注释掉了使用 BPF_MAP_UPDATE_ELEM 命令更新映射元素的代码。
使用 close(map_fd) 关闭映射文件描述符,释放资源。
示例 2:使用 libbpf 创建和使用 BPF 映射 (推荐方式) 这个例子展示了使用 libbpf 库(现代推荐方式)来创建和操作 BPF 映射。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 // bpf_libbpf_example.c // 编译: gcc -o bpf_libbpf_example bpf_libbpf_example.c -lbpf // 注意:需要安装 libbpf-dev 包 /* #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include <bpf/libbpf.h> // libbpf 库 #include <bpf/bpf.h> // bpf_map_update_elem, bpf_map_lookup_elem 等辅助函数 #include <stdio.h> // printf, perror #include <stdlib.h> // exit #include <unistd.h> // close (如果需要) int main() { int map_fd = -1; int err; int key = 5; long long value = 9876543210LL; long long lookup_value; printf("Using libbpf to create and manipulate a BPF map...\n"); // 1. 使用 libbpf 创建 BPF 映射 struct bpf_map *map = bpf_map__new(BPF_MAP_TYPE_ARRAY, sizeof(int), sizeof(long long), 10, 0, "my_libbpf_array_map"); if (!map) { fprintf(stderr, "Failed to create BPF map using libbpf.\n"); exit(EXIT_FAILURE); } // 2. 获取映射的文件描述符 map_fd = bpf_map__fd(map); if (map_fd < 0) { fprintf(stderr, "Failed to get map file descriptor.\n"); bpf_map__destroy(map); // 清理 exit(EXIT_FAILURE); } printf("BPF map created using libbpf. File descriptor: %d\n", map_fd); // 3. 使用 libbpf 辅助函数更新映射元素 printf("Updating element at key %d with value %lld...\n", key, value); err = bpf_map_update_elem(map_fd, &key, &value, BPF_ANY); if (err) { perror("bpf_map_update_elem failed"); bpf_map__destroy(map); exit(EXIT_FAILURE); } printf("Element updated successfully.\n"); // 4. 使用 libbpf 辅助函数查找映射元素 printf("Looking up element at key %d...\n", key); err = bpf_map_lookup_elem(map_fd, &key, &lookup_value); if (err) { perror("bpf_map_lookup_elem failed"); bpf_map__destroy(map); exit(EXIT_FAILURE); } printf("Found element at key %d with value %lld.\n", key, lookup_value); // 5. 清理资源 printf("Destroying BPF map...\n"); bpf_map__destroy(map); // 这会关闭 fd 并释放资源 printf("BPF map destroyed.\n"); printf("libbpf example completed.\n"); return 0; } */ // 由于 libbpf 依赖和编译可能较为复杂,此处提供伪代码框架。 // 实际使用请参考 libbpf 文档和示例。
**代码解释 **(概念性/伪代码):
包含 libbpf 库的头文件。
创建映射:
获取文件描述符:
调用 bpf_map__fd 获取映射的文件描述符,用于后续操作。
操作映射:
清理:
调用 bpf_map__destroy 来销毁映射并释放所有相关资源(包括关闭文件描述符)。
重要提示与注意事项: 内核版本: eBPF 是一个快速发展的领域,新特性和功能不断加入。确保你的 Linux 内核版本足够新以支持你需要的功能。
权限: 使用 bpf 系统调用通常需要特殊权限,如 CAP_SYS_ADMIN 或 CAP_BPF(较新内核)。在生产环境中,应遵循最小权限原则。
libbpf 是推荐方式: 直接使用 bpf 系统调用非常复杂且容易出错。libbpf 库极大地简化了开发流程,提供了更好的可移植性和错误处理。
程序加载: 加载 eBPF 程序(BPF_PROG_LOAD)比创建映射复杂得多,需要预先编译好的 BPF 字节码,并处理验证、日志等。
安全性: eBPF 程序在加载到内核前会经过严格的验证器(verifier)检查,确保其安全性(无无限循环、无非法内存访问等)。这是 eBPF 能够安全运行在内核中的关键。
性能: eBPF 程序在内核中运行,并且通常会被 JIT 编译成高效的机器码,性能非常高。
调试: bpftool 和 bpf_trace_printk 是调试 eBPF 程序的常用工具。
总结:
bpf 系统调用是 Linux eBPF 子系统的核心接口,它提供了一种强大、安全且高效的方式让用户态程序在内核中执行自定义逻辑。虽然直接使用它非常底层和复杂,但通过 libbpf 等高级库,开发者可以更轻松地利用 eBPF 的强大功能来构建网络、安全、监控和性能分析等领域的前沿应用。理解其基本概念和工作原理对于现代 Linux 系统程序员来说至关重要。