linux内核源码包
1992年,Lawrence Berkeley实验室的Steven McCanne和Van Jacobson提出了一种BSD Unix系统的解决方案,该解决方案通过实现称为Berkeley Packet Filter(BPF)的内核内数据包过滤器,将对用户空间的有害网络数据包副本最小化。 1997年,它在Linux内核版本2.1.75中引入。
BPF的目的是尽早过滤所有不需要的数据包,因此过滤机制必须从用户空间实用程序(如tcpdump)转移到内核内虚拟机。 它发送一组类似于汇编的指令,以通过系统调用bpf()过滤从用户空间到内核的必要数据包。 内核在加载程序之前会对其进行静态分析,并确保它们不会挂起或损害正在运行的系统。
BPF机
由一个累加器,一个索引寄存器(x),一个暂存存储器和一个隐式程序计数器组成。 它具有少量的算术,逻辑和跳转指令。 累加器用于算术运算,而索引寄存器提供到数据包或暂存器区域的偏移量。 这是一个用BPF字节码编写的小型BPF程序的示例:
ldh
[
12
]
jeq
#ETHERTYPE_IP, l1, l2
l1: ret
#TRUE
l2: ret
#0
ldh指令从以太网数据包中的偏移量12开始向累加器加载一个半字(16位)值,该值是以太网类型的字段。 如果不是IP数据包,则将返回0 ,并且该数据包将被拒绝。
BPF即时编译器
实时(JIT)编译器于2011年引入内核 ,以加快BPF字节码的执行速度。 该编译器将BPF字节码转换为主机系统的汇编代码。 这样的编译器存在的x86-64,SPARC,PowerPC上,ARM,ARM64,MIPS和系统390,并且可以通过CONFIG_BPF_JIT启用。
eBPF机
扩展BPF(eBPF)是对BPF(现在称为cBPF,代表传统BPF)的增强,具有更多资源 ,例如10个寄存器和1-8字节加载/存储指令。 BPF有向前跳转,而eBPF有向后跳转和向前跳转,因此可以有一个循环,当然,内核可以确保正确终止。 它还包括一个称为maps的全局数据存储,并且此maps状态在事件之间保持不变。 因此,eBPF也可以用于汇总事件的统计信息。 此外,eBPF程序可以用类似于C的函数编写,可以使用GNU编译器集合(GCC)/ LLVM编译器进行编译。 eBPF被设计为具有一对一映射的JIT,因此它可以生成非常优化的代码,其执行速度与本地编译的代码一样快。
eBPF和跟踪审查
上游内核开发
Linux中传统的内置跟踪器以后期处理方式使用,它们会转储固定事件详细信息,然后,诸如perf或trace-cmd之类的用户空间工具可以发布进程以获取所需的信息(例如perf stat ); 但是,eBPF可以在内核上下文中准备用户信息,并且仅将所需的信息传输到用户空间。 到目前为止,在上游内核中已经实现了对使用eBPF的kprobes , tracepoints和perf_events过滤的支持。 Arch x86-64 , AArch64 , S390x , PowerPC 64和SPARC64已支持它们。
有关更多信息,请查看以下Linux内核文件:
用户空间开发
已为内核内树和内核外树开发了用户空间工具。 查看这些文件和目录,以了解更多有关eBPF使用的上游内核:
BCC是另一个内核外树工具,具有针对特定用途的高效内核跟踪程序(例如funccount ,该程序对与模式匹配的函数进行计数)。
Perf还具有一个BPF接口,可用于将eBPF对象加载到内核中。
eBPF跟踪:用户空间到内核空间的流动
BPF系统调用和BPF映射是两个可以与eBPF内核进行交互的有用实体。
BPF系统调用
用户可以使用bpf()系统调用与eBPF内核进行交互,其原型为:
int bpf ( int cmd, union bpf_attr * attr, unsigned int size ) ;
以下是这些论点的摘要; 有关更多详细信息,请参见手册页BPF 。
- cmd可以是任何已定义的枚举bpf_cmd ,它告诉内核有关地图区域的管理(例如,创建,更新,删除,查找元素,附加或分离程序等)。
- attr可以是用户定义的结构,可由其各自的命令使用。
- size是attr的大小。
BPF地图
eBPF跟踪计算内核域本身中的统计信息。 我们将需要内核中某种类型的内存/数据结构来创建此类统计信息。 映射是一种通用的数据结构,以键值对的形式存储不同类型的数据。 它们允许在eBPF内核程序之间以及内核和用户空间应用程序之间共享数据。
地图的重要属性包括:
- 类型(map_type)
- 最大元素数(max_entries)
- 密钥大小,以字节为单位(key_size)
- 值大小(以字节为单位)(value_size)
根据使用或需要选择不同类型的映射(例如,哈希,数组,程序数组等)。 例如,如果键是字符串(或者不是整数序列),则可以使用哈希映射来加快查找速度; 但是,如果键像索引一样,则数组映射将提供最快的查找方法。
键不能大于key_size ,不能存储大于value_size的值,并且max_entries是可在映射中存储的键-值对的最大数量。
这是要注意的两个重要命令:
- BPF_PROG_LOAD:以下是该程序的重要属性:
prog_type :对跟踪有用的程序类型:
BPF_PROG_TYPE_KPROBE
BPF_PROG_TYPE_TRACEPOINT
BPF_PROG_TYPE_PERF_EVENT
insns :指向结构bpf_insn的指针,该结构具有由内核BPF虚拟机执行的BPF指令
insn_cnt : insns上存在的指令总数
license:string ,必须与GPL兼容才能调用标记为gpl_only的帮助函数
kern_version :内核树的版本
- BPF_MAP_CREATE:接受BPF映射部分中讨论的属性,创建一个新的映射,然后返回引用该映射的新文件描述符。 返回的map_fd可用于查找或通过诸如BPF_MAP_LOOKUP_ELEM , BPF_MAP_UPDATE_ELEM , BPF_MAP_DELETE_ELEM 或BPF_MAP_GET_NEXT_KEY之类的命令更新地图元素。 这些map-manipulation命令接受带有map_fd ,key和value的属性。
要了解其工作原理,请在GitHub上查看此独立的eBPF演示 ; 它不需要任何其他eBPF库代码。 它有一个小的库,用于加载BPF内核代码的不同部分( bpf_load.c ),然后在bpf()系统调用( bpf.c )之上包装函数,以操作映射并加载内核BPF代码。 编译此代码时,我们得到两个可执行文件: memcpy_kprobe和memcpy_stat 。
- memcpy_kprobe:对于每个应用程序,我们都有一个_kern文件和另一个_user文件。 _kern文件具有int bpf_prog1(struct pt_regs * ctx)函数 。 此函数在内核中执行,因此可以访问内核变量和函数。 Memcpy_kprobe_kern.c分别具有针对程序,许可证和版本的三个部分映射。 这些部分中的数据是系统调用bpf(BPF_PROG_LOAD,...)属性的一部分,然后内核根据prog_type属性执行已加载的BPF指令。 所以,在memcpy_kprobe_kern.c BPF代码将当kprobe仪器在内核的memcpy的条目()执行的是命中。 执行此BPF代码时,它将读取memcpy()的第三个参数,例如副本的大小,然后在跟踪缓冲区中打印一个有关memcpy大小的语句。 Memcpy_kprobe_user.c加载内核程序并继续读取跟踪缓冲区,以显示正在将什么内核eBPF程序写入其中。
- memcpy_stat:这将在内核本身中准备memcpy()复制大小的统计信息。 Memcpy_stat_kern.c还有另外一部分,如地图。 bpf_prog1()读取memcpy()的大小并更新映射表。 相应的用户空间程序memcpy_stat_user.c每两秒钟读取一次映射表,并在控制台上打印统计信息。
这些简单的示例说明了如何编写内核eBPF代码以进行内核跟踪和统计信息准备。
如果您使用过eBPF并希望分享建议,请在评论中留下注释。
linux内核源码包