当前位置: 首页 > news >正文

bpf系统调用及示例

这次我们介绍 bpf 函数,它是 Linux 内核中 **Berkeley Packet Filter **(BPF) 子系统的用户态接口。


1. 函数介绍

bpf 是一个功能极其强大的 Linux 系统调用(内核版本 >= 3.18,但许多高级特性需要更新的内核),它提供了一种在内核空间安全、高效地运行用户定义程序的机制。

你可以把 BPF 想象成一个内核里的虚拟机

  • 你(用户态程序)可以编写一段用BPF 指令集编写的“小程序”(eBPF 程序)。
  • 你将这段程序加载到内核中。
  • 内核会验证这段程序的安全性(确保它不会导致死循环、不会访问非法内存等)。
  • 如果验证通过,内核会即时编译 (JIT) 这段程序为机器码,并将其附加到特定的内核钩子(hook points)上。
  • 当内核执行到这些钩子时(例如,收到网络包、进行系统调用、跟踪函数调用),就会执行你加载的 BPF 程序。
  • BPF 程序可以进行过滤修改收集信息(遥测)、路由等操作。

主要用途:

  • 网络编程: 高性能数据包过滤(tcpdump)、流量整形、负载均衡、XDP(eXpress Data Path)超高速网络处理。
  • 系统监控和追踪: 跟踪内核函数、用户态函数、系统调用,收集性能指标(如 perf)、调试信息。
  • 安全: 实施安全策略、沙箱、审计。
  • 性能分析: 无侵入式地分析应用程序和内核性能瓶颈。

2. 函数原型

#include <linux/bpf.h> // 必需,包含 BPF 相关常量和结构体long bpf(int cmd, union bpf_attr *attr, unsigned int size);

3. 功能

  • 统一接口: bpf 系统调用是操作 eBPF 子系统的统一入口点。几乎所有与 eBPF 相关的操作(创建、加载、附加、查询、删除等)都通过这个单一的系统调用来完成。
  • 多用途: 根据 cmd 参数的不同,bpf 可以执行完全不同的操作。

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 操作可能需要的参数。它的定义非常庞大,这里只列举几个关键成员以说明其结构:

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 映射。

// 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;
}

**代码解释 **(概念性):

1. 定义 sys_bpf 包装 syscall(__NR_bpf, ...),因为 glibc 可能没有直接包装 bpf
2. 声明 union bpf_attr attr 用于传递参数。
3. 清零 attr 联合体,这是一个好习惯,确保未使用的字段为 0。
4. 填充 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, ...): 设置映射的名称。
5. 调用 sys_bpf:
* cmd = BPF_MAP_CREATE: 指定创建映射操作。
* &attr: 指向填充好的参数联合体。
* sizeof(attr): 联合体的大小。
6. 检查返回值:
* 如果返回值 map_fd 是一个非负整数,表示成功,这个 map_fd 是新创建映射的文件描述符。
* 如果返回 -1,检查 errnoEPERM 表示权限不足,通常需要 root 权限。
7. 打印成功信息和返回的文件描述符。
8. 概念性操作: 注释掉了使用 BPF_MAP_UPDATE_ELEM 命令更新映射元素的代码。
9. 使用 close(map_fd) 关闭映射文件描述符,释放资源。

示例 2:使用 libbpf 创建和使用 BPF 映射 (推荐方式)

这个例子展示了使用 libbpf 库(现代推荐方式)来创建和操作 BPF 映射。

// 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 文档和示例。

**代码解释 **(概念性/伪代码):

  1. 包含 libbpf 库的头文件。
  2. 创建映射:
    • 调用 libbpf 提供的高级函数 bpf_map__new 来创建映射。
    • 这比直接使用 bpf 系统调用简单得多,库会处理联合体的填充和系统调用。
  3. 获取文件描述符:
    • 调用 bpf_map__fd 获取映射的文件描述符,用于后续操作。
  4. 操作映射:
    • 使用 libbpf 提供的辅助函数 bpf_map_update_elembpf_map_lookup_elem 来更新和查找映射中的元素。
    • 这些函数内部会调用 bpf 系统调用(如 BPF_MAP_UPDATE_ELEM)。
  5. 清理:
    • 调用 bpf_map__destroy 来销毁映射并释放所有相关资源(包括关闭文件描述符)。

重要提示与注意事项:

1. 内核版本: eBPF 是一个快速发展的领域,新特性和功能不断加入。确保你的 Linux 内核版本足够新以支持你需要的功能。
2. 权限: 使用 bpf 系统调用通常需要特殊权限,如 CAP_SYS_ADMINCAP_BPF(较新内核)。在生产环境中,应遵循最小权限原则。
3. libbpf 是推荐方式: 直接使用 bpf 系统调用非常复杂且容易出错。libbpf 库极大地简化了开发流程,提供了更好的可移植性和错误处理。
4. 程序加载: 加载 eBPF 程序(BPF_PROG_LOAD)比创建映射复杂得多,需要预先编译好的 BPF 字节码,并处理验证、日志等。
5. 安全性: eBPF 程序在加载到内核前会经过严格的验证器(verifier)检查,确保其安全性(无无限循环、无非法内存访问等)。这是 eBPF 能够安全运行在内核中的关键。
6. 性能: eBPF 程序在内核中运行,并且通常会被 JIT 编译成高效的机器码,性能非常高。
7. 调试: bpftoolbpf_trace_printk 是调试 eBPF 程序的常用工具。

总结:

bpf 系统调用是 Linux eBPF 子系统的核心接口,它提供了一种强大、安全且高效的方式让用户态程序在内核中执行自定义逻辑。虽然直接使用它非常底层和复杂,但通过 libbpf 等高级库,开发者可以更轻松地利用 eBPF 的强大功能来构建网络、安全、监控和性能分析等领域的前沿应用。理解其基本概念和工作原理对于现代 Linux 系统程序员来说至关重要。

http://www.lryc.cn/news/610552.html

相关文章:

  • K8S 性能瓶颈排查
  • CVE-2017-8291源码分析与漏洞复现(PIL远程命令执行漏洞)
  • 软件测试中,pytest 框架如何运行上传失败的测试用例?
  • docker国内镜像源列表
  • 软件测试中,pytest 如何运行多个文件或整个目录?
  • Python入门Day15:面向对象进阶(类变量,继承,封装,多态)
  • springboot + maven 使用资源占位符实现动态加载配置文件
  • Modstart 请求出现 Access to XMLHttpRequest at ‘xx‘
  • imx6ull-驱动开发篇9——设备树下的 LED 驱动实验
  • ubuntu的压缩工具zip的安装和使用
  • 【C++】类和对象1
  • 力扣106:从中序与后序遍历序列构造二叉树
  • 「PromptPilot 大模型智能提示词平台」—— PromptPilot × 豆包大模型 1.6:客户投诉邮件高效回复智能提示词解决方案
  • 工业级 CAN 与以太网桥梁:串口服务器CAN通讯转换器深度解析(上)
  • 【科研绘图系列】R语言绘制误差棒图
  • 姜 第四章 线性方程组
  • shmget等共享内存系统调用及示例
  • uniapp 类似popover气泡下拉框组件
  • Maven和Gradle在构建项目上的区别
  • uniapp Android App集成支付宝的扫码组件mPaaS
  • Linux驱动25 --- RkMedia音频API使用增加 USB 音视频设备
  • Linux驱动24 --- RkMedia 视频 API 使用
  • 技术文章推荐|解析 ESA 零售交易方案(技术分析+案例拆解)
  • 基于k8s环境下的pulsar常用命令(下)
  • JavaWeb02——基础标签及样式(黑马视频笔记)
  • 203.移除链表元素 707.设计链表 206.反转链表
  • 8.5 位|归并|递归
  • 腾讯云CodeBuddy AI IDE+CloudBase AI ToolKit打造理财小助手网页
  • C++ - 基于多设计模式下的同步异步日志系统(11w字)
  • 使用ProxySql实现MySQL的读写分离