探索eBPF:Linux内核的黑科技(上)

简介: 探索eBPF:Linux内核的黑科技

Linux内核在2022年主要发布了5.16-5.19以及6.0和6.1这几个版本,每个版本都为eBPF引入了大量的新特性。本文将对这些新特性进行一点简要的介绍,更详细的资料请参考对应的链接信息。总体而言,eBPF在内核中依然是最活跃的模块之一,它的功能特性也还在高速发展中。某种意义上说,eBPF正朝着一个完备的内核态可编程接口快速进化。

eBPF 进阶: 内核新特性进展一览

  1. BPF kfuncs
  2. Bloom Filter Map:5.16
  3. Compile Once – Run Everywhere:Linux 5.17
  4. bpf_loop() 辅助函数:5.17
  5. BPF_LINK_TYPE_KPROBE_MULTI:5.18
  6. 动态指针和类型指针:5.19
  7. USDT:5.19
  8. bpf panic:6.1
  9. BPF 内存分配器、链表:6.1
  10. user ring buffer 6.1

精选文章推荐阅读:

一、eBPF概述

1.1eBPF是什么

eBPF 是一个基于寄存器的虚拟机,使用自定义的 64 位 RISC 指令集,能够在 Linux 内核内运行即时本地编译的 “BPF 程序”,并能访问内核功能和内存的一个子集。这是一个完整的虚拟机实现,不要与基于内核的虚拟机(KVM)相混淆,后者是一个模块,目的是使 Linux 能够作为其他虚拟机的管理程序。eBPF 也是主线内核的一部分,所以它不像其他框架那样需要任何第三方模块(LTTngSystemTap),而且几乎所有的 Linux 发行版都默认启用。熟悉 DTrace 的读者可能会发现 DTrace/BPFtrace 对比非常有用。

在内核内运行一个完整的虚拟机主要是考虑便利和安全。虽然 eBPF 程序所做的操作都可以通过正常的内核模块来处理,但直接的内核编程是一件非常危险的事情 - 这可能会导致系统锁定、内存损坏和进程崩溃,从而导致安全漏洞和其他意外的效果,特别是在生产设备上(eBPF 经常被用来检查生产中的系统),所以通过一个安全的虚拟机运行本地 JIT 编译的快速内核代码对于安全监控和沙盒、网络过滤、程序跟踪、性能分析和调试都是非常有价值的。部分简单的样例可以在这篇优秀的 eBPF 参考中找到。

基于设计,eBPF 虚拟机和其程序有意地设计为不是图灵完备的:即不允许有循环(正在进行的工作是支持有界循环【译者注:已经支持有界循环,#pragma unroll 指令】),所以每个 eBPF 程序都需要保证完成而不会被挂起、所有的内存访问都是有界和类型检查的(包括寄存器,一个 MOV 指令可以改变一个寄存器的类型)、不能包含空解引用、一个程序必须最多拥有 BPF_MAXINSNS 指令(默认 4096)、“主"函数需要一个参数(context)等等。当 eBPF 程序被加载到内核中,其指令被验证模块解析为有向环状图,上述的限制使得正确性可以得到简单而快速的验证。

主要区别如下:

  1. 允许使用C 语言编写代码片段,并通过LLVM编译成eBPF 字节码;
  2. cBPF 只实现了SOCKET_FILTER,而eBPF还有KPROBE 、PERF等。
  3. BPF使用socket 实现了用户态与内核交互,eBPF 则定义了一个专用于eBPF 的新的系统调用,用于装载BPF 代码段、创建和读取BPF map,更加通用。
  4. BPF map 机制,用于在内核中以key-value 的方式临时存储BPF 代码产生的数据。

对于eBPF可以简单的理解成kernel实现了一个虚拟机机制,将类C代码编译成字节码(后文有详细解释),挂在到内核的钩子上,当钩子被触发时,kernel在虚拟机的"沙盒"中运行字节码,这样既能方便的实现很多功能,也能通过沙箱保证内核的安全性。

-------------------------------感谢大家的支持------------------------------------

【文章福利】小编推荐自己的Linux内核技术交流群:【 865977150】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!

[内核资料领取,](https://docs.qq.com/doc/DTmFTc29xUGdNSnZ2)   [Linux内核源码学习地址。](https://ke.qq.com/course/4032547?flowToken=1044435)

1.2eBPF的演进

最初的[Berkeley Packet Filter (BPF) PDF]是为捕捉和过滤符合特定规则的网络包而设计的,过滤器为运行在基于寄存器的虚拟机上的程序。

在内核中运行用户指定的程序被证明是一种有用的设计,但最初BPF设计中的一些特性却并没有得到很好的支持。例如,虚拟机的指令集架构(ISA)相对落后,现在处理器已经使用64位的寄存器,并为多核系统引入了新的指令,如原子指令XADD。BPF提供的一小部分RISC指令已经无法在现有的处理器上使用。

因此Alexei Starovoitov在eBPF的设计中介绍了如何利用现代硬件,使eBPF虚拟机更接近当代处理器,eBPF指令更接近硬件的ISA,便于提升性能。其中最大的变动之一是使用了64位的寄存器,并将寄存器的数量从2提升到了10个。由于现代架构使用的寄存器远远大于10个,这样就可以像本机硬件一样将参数通过eBPF虚拟机寄存器传递给对应的函数。另外,新增的BPF_CALL指令使得调用内核函数更加便利。

将eBPF映射到本机指令有助于实时编译,提升性能。3.15内核中新增的eBPF补丁使得x86-64上运行的eBPF相比老的BPF(cBPF)在网络过滤上的性能提升了4倍,大部分情况下会保持1.5倍的性能提升。很多架构 (x86-64, SPARC, PowerPC, ARM, arm64, MIPS, and s390)已经支持即时(JIT)编译。

1.3ebpf环境搭建

编译运行源码samples/bpf中的代码

  1. 下载内核源码并解压
  2. /bin/sh: scripts/mod/modpost: No such file or directory 遇到这种错误,需要make scripts
  3. make M=samples/bpf 需要.config文件,需要保证这些项存在
  4. 遇到错误libcrypt1.so.1 not found,执行如下代码(https://www.mail-archive.com/debian-bugs-dist@lists.debian.org/msg1818037.html

$ cd /tmp
$ apt -y download libcrypt1
$ dpkg-deb -x libcrypt1_1%3a4.4.25-2_amd64.deb  .
$ cp -av lib/x86_64-linux-gnu/* /lib/x86_64-linux-gnu/
$ apt -y --fix-broken install

5.编译成功,可以执行samples/bpf中的可执行文件。

编译运行自己开发的代码

1. 下载linux source code,编译内核并升级

git clone https://github.com/torvalds/linux.git
cd linux/
git checkout -b v5.0 v5.0

配置文件

cp -a /boot/config-4.14.81.bm.15-amd64 ./.config

echo '
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_FTRACE_SYSCALLS=y
CONFIG_FUNCTION_TRACER=y
CONFIG_HAVE_DYNAMIC_FTRACE=y
CONFIG_DYNAMIC_FTRACE=y
CONFIG_HAVE_KPROBES=y
CONFIG_KPROBES=y
CONFIG_KPROBE_EVENTS=y
CONFIG_ARCH_SUPPORTS_UPROBES=y
CONFIG_UPROBES=y
CONFIG_UPROBE_EVENTS=y
CONFIG_DEBUG_FS=y
CONFIG_DEBUG_INFO_BTF=y
' >> ./.config

需要添加sid源安装dwarves

apt install dwarves
make oldconfig
apt install libssl-dev
make
make modules_install
make install
reboot

此时:

uname -a
Linux n231-238-061 5.0.0 #1 SMP Mon Dec 13 05:38:52 UTC 2021 x86_64 GNU/Linux

编译bpf helloworld

切换到https://github.com/bpftools/linux-observability-with-bpf的helloworld目录

sed -i 's;/kernel-src;/root/linux;' Makefile
make

有报错:

640.png

参考:http://www.helight.info/blog/2021/build-kernel-ebpf-sample/解决

cp /root/linux/include/uapi/linux/bpf.h /usr/include/linux/bpf.h

执行./monitor-exec,有报错

./monitor-exec: error while loading shared libraries: libbpf.so: cannot open shared object file: No such file or directory

解决方法

cd /root/linux/tools/lib/bpf/
make
make install

在 /etc/ld.so.conf 中添加 /usr/local/lib64这一行,运行 sudo ldconfig 重新生成动态库配置信息。

~/linux/tools/lib/bpf# ldconfig -v 2>/dev/null | grep libbpf
   libbpf.so.0 -> libbpf.so.0.5.0
   libbpf.so -> libbpf.so

最终执行情况:

640.png

可能需要安装apt-get install gcc-multilib g++-multilib

https://github.com/sirfz/tesserocr/issues/130

安装bpftrace

(1)debian 添加sid源 https://github.com/iovisor/bcc/blob/master/INSTALL.md#debian---source

deb http://deb.debian.org/debian sid main contrib non-free
deb-src http://deb.debian.org/debian sid main contrib non-free

(2)apt install bpftrace https://github.com/iovisor/bpftrace/blob/master/INSTALL.md

1.4使用eBPF可以做什么?

一个eBPF程序会附加到指定的内核代码路径中,当执行该代码路径时,会执行对应的eBPF程序。鉴于它的起源,eBPF特别适合编写网络程序,将该网络程序附加到网络socket,进行流量过滤,流量分类以及执行网络分类器的动作。eBPF程序甚至可以修改一个已建链的网络socket的配置。XDP工程会在网络栈的底层运行eBPF程序,高性能地进行处理接收到的报文。

从下图可以看到eBPF支持的功能:

640.png

BPF对网络的处理可以分为tc/BPF和XDP/BPF,它们的主要区别如下(参考该文档):

XDP的钩子要早于tc,因此性能更高:tc钩子使用sk_buff结构体作为参数,而XDP使用xdp_md结构体作为参数,sk_buff中的数据要远多于xdp_md,但也会对性能造成一定影响,且报文需要上送到tc钩子才会触发处理程序。由于XDP钩子位于网络栈之前,因此XDP使用的xdp_buff(即xdp_md)无法访问sk_buff元数据。

structxdp_buff{
/* Linux 5.8*/
void*data;
void*data_end;
void*data_meta;
void*data_hard_start;
structxdp_rxq_info*rxq;
structxdp_txq_info*txq;
u32 frame_sz
;/* frame size to deduce data_hard_end/reserved tailroom*/
};
structxdp_rxq_info{
structnet_device*dev;
u32 queue_index
;
u32 reg_state
;
structxdp_mem_info mem;
} ____cacheline_aligned;/* perf critical, avoid false-sharing */  
structxdp_txq_info{
structnet_device*dev;};

data指向page中的数据包的其实位置,data_end指向数据包的结尾。由于XDP允许headroom(见下文),data_hard_start指向page中headroom的起始位置,即,当对报文进行封装时,data会通过bpf_xdp_adjust_head()向data_hard_start移动。

相同的BPF辅助函数也可以用以解封装,此时data会远离data_hard_start。data_meta一开始指向与data相同的位置,但bpf_xdp_adjust_meta() 能够将其朝着 data_hard_start 移动,进而给用户元数据提供空间,这部分空间对内核网络栈是不可见的,但可以被tc BPF程序读取( tc 需要将它从 XDP 转移到 skb)。

反之,可以通过相同的BPF程序将data_meta远离data_hard_start来移除或减少用户元数据大小。data_meta 还可以单纯地用于在尾调用间传递状态,与tc BPF程序访问的skb->cb[]控制块类似。 对于struct xdp_buff中的报文指针,有如下关系 :data_hard_start <= data_meta <= data < data_end。rxq字段指向在ring启动期间填充的额外的与每个接受队列相关的元数据。BPF程序可以检索queue_index,以及网络设备上的其他数据(如ifindex等)。

tc能够更好地管理报文:tc的BPF输入上下文是一个sk_buff,不同于XDP使用的xdp_buff,二者各有利弊。当内核的网络栈在XDP层之后接收到一个报文时,会分配一个buffer,解析并保存报文的元数据,这些元数据即sk_buff。

该结构体会暴露给BPF的输入上下文,这样tc ingress层的tc BPF程序就能够使用网络栈从报文解析到的元数据。使用sk_buff,tc可以更直接地使用这些元数据,因此附加到tc BPF钩子的BPF程序可以读取或写入skb的mark,pkt_type, protocol, priority, queue_mapping, napi_id, cb[] array, hash, tc_classid 或 tc_index, vlan metadata等,而XDP能够传输用户的元数据以及其他信息。tc BPF使用的 struct __sk_buff定义在linux/bpf.h头文件中。xdp_buff 的弊端在于,其无法使用sk_buff中的数据,XDP只能使用原始的报文数据,并传输用户元数据。

XDP的能够更快地修改报文:sk_buff包含很多协议相关的信息(如GSO阶段的信息),因此其很难通过简单地修改报文数据达到切换协议的目的,原因是网络栈对报文的处理主要基于报文的元数据,而非每次访问数据包内容的开销。因此,BPF辅助函数需要正确处理内部sk_buff的转换。而xdp_buff 则不会有这种问题,因为XDP的处理时间早于内核分配sk_buff的时间,因此可以简单地实现对任何报文的修改(但管理起来要更加困难)。

tc/ebpf和xdp可以互补:如果用户需要修改报文,同时对数据进行比较复杂的管理,那么,可以通过运行两种类型的程序来弥补每种程序类型的局限性。XDP程序位于ingress,可以修改完整的报文,并将用户元数据从XDP BPF传递给tc BPF,然后tc可以使用XDP的元数据和sk_buff字段管理报文。

tc/eBPF可以作用于ingress和egress,但XDP只能作用于ingress:与XDP相比,tc BPF程序可以在ingress和egress的网络数据路径上触发,而XDP只能作用于ingress。

tc/BPF不需要改变硬件驱动,而XDP通常会使用native驱动模式来获得更高的性能。但tc BPF程序的处理仍作用于早期的内核网络数据路径上(GRO处理之后,协议处理和传统的iptables防火墙的处理之前,如iptables PREROUTING或nftables ingress钩子等)。而在egress上,tc BPF程序在将报文传递给驱动之前进行处理,即在传统的iptables防火墙(如iptables POSTROUTING)之后,但在内核的GSO引擎之前进行处理。一个特殊情况是,如果使用了offloaded的tc BPF程序(通常通过SmartNIC提供),此时Offloaded tc/eBPF接近于Offloaded  XDP的性能。

从下图可以看到TC和XDP的工作位置,可以看到XDP对报文的处理要先于TC:

640.png

内核执行的另一种过滤类型是限制进程可以使用的系统调用。通过seccomp BPF实现。

eBPF也可以用于通过将程序附加到tracepoints, kprobes,和perf events的方式定位内核问题,以及进行性能分析。因为eBPF可以访问内核数据结构,开发者可以在不编译内核的前提下编写并测试代码。对于工作繁忙的工程师,通过该方式可以方便地调试一个在线运行的系统。此外,还可以通过静态定义的追踪点调试用户空间的程序(即BCC调试用户程序,如Mysql)。

使用eBPF有两大优势:快速,安全。为了更好地使用eBPF,需要了解它是如何工作的。

1.5内核的eBPF校验器

在内核中运行用户空间的代码可能会存在安全和稳定性风险。因此,在加载eBPF程序前需要进行大量校验。首先通过对程序控制流的深度优先搜索保证eBPF能够正常结束,不会因为任何循环导致内核锁定。严禁使用无法到达的指令;任何包含无法到达的指令的程序都会导致加载失败。

第二个阶段涉及使用校验器模拟执行eBPF程序(每次执行一个指令)。在每次指令执行前后都需要校验虚拟机的状态,保证寄存器和栈的状态都是有效的。严禁越界(代码)跳跃,以及访问越界数据。

校验器不会检查程序的每条路径,它能够知道程序的当前状态是否是已经检查过的程序的子集。由于前面的所有路径都必须是有效的(否则程序会加载失败),当前的路径也必须是有效的,因此允许验证器“修剪”当前分支并跳过其模拟阶段。

校验器有一个"安全模式",禁止指针运算。当一个没有CAP_SYS_ADMIN特权的用户加载eBPF程序时会启用安全模式,确保不会将内核地址泄露给非特权用户,且不会将指针写入内存。如果没有启用安全模式,则仅允许在执行检查之后进行指针运算。例如,所有的指针访问时都会检查类型,对齐和边界冲突。

无法读取包含未初始化内容的寄存器,尝试读取这类寄存器中的内容将导致加载失败。R0-R5的寄存器内容在函数调用期间被标记未不可读状态,可以通过存储一个特殊值来测试任何对未初始化寄存器的读取行为;对于读取堆栈上的变量的行为也进行了类似的检查,确保没有指令会写入只读的帧指针寄存器。

最后,校验器会使用eBPF程序类型(见下)来限制可以从eBPF程序调用哪些内核函数,以及访问哪些数据结构。例如,一些程序类型可以直接访问网络报文。、

1.6pf()系统调用

使用bpf()系统调用和BPF_PROG_LOAD命令加载程序。该系统调用的原型为:

int bpf(int cmd, union bpf_attr *attr, unsigned int size);

bpf_attr允许数据在内核和用户空间传递,具体类型取决于cmd参数。

cmd可以是如下内容:

      BPF_MAP_CREATE
             Create a map and return a file descriptor that refers to the
             map.  The close-on-exec file descriptor flag (see fcntl(2)) is
             automatically enabled for the new file descriptor.

      BPF_MAP_LOOKUP_ELEM
             Look up an element by key in a specified map and return its
             value.

      BPF_MAP_UPDATE_ELEM
             Create or update an element (key/value pair) in a specified
             map.

      BPF_MAP_DELETE_ELEM
             Look up and delete an element by key in a specified map.

      BPF_MAP_GET_NEXT_KEY
             Look up an element by key in a specified map and return the
             key of the next element.

      BPF_PROG_LOAD
             Verify and load an eBPF program, returning a new file descrip‐
             tor associated with the program.  The close-on-exec file
             descriptor flag (see fcntl(2)) is automatically enabled for
             the new file descriptor.

size参数给出了bpf_attr联合体对象的字节长度。BPF_PROG_LOAD加载的命令可以用于创建和修改eBPF maps,maps是普通的key/value数据结构,用于在eBPF程序和内核空间或用户空间之间通信。其他命令允许将eBPF程序附加到一个控制组目录或socket文件描述符上,迭代所有的maps和程序,以及将eBPF对象固定到文件,这样在加载eBPF程序的进程结束后不会被销毁(后者由tc分类器/操作代码使用,因此可以将eBPF程序持久化,而不需要加载的进程保持活动状态)。完整的命令可以参考bpf()帮助文档

虽然可能存在很多不同的命令,但大体可以分为两类:与eBPF程序交互的命令,与eBPF maps交互的命令,或同时与程序和maps交互的命令(统称为对象)。

1.7eBPF程序类型

使用BPF_PROG_LOAD加载的程序类型确定了四件事:附加的程序的位置,验证器允许调用的内核辅助函数,是否可以直接访问网络数据报文,以及传递给程序的第一个参数对象的类型。实际上,程序类型本质上定义了一个API。创建新的程序类型甚至纯粹是为了区分不同的可调用函数列表(例如,BPF_PROG_TYPE_CGROUP_SKB 和BPF_PROG_TYPE_SOCKET_FILTER)。

当前内核支持的eBPF程序类型为:

  • BPF_PROG_TYPE_SOCKET_FILTER: a network packet filter
  • BPF_PROG_TYPE_KPROBE: determine whether a kprobe should fire or not
  • BPF_PROG_TYPE_SCHED_CLS: a network traffic-control classifier
  • BPF_PROG_TYPE_SCHED_ACT: a network traffic-control action
  • BPF_PROG_TYPE_TRACEPOINT: determine whether a tracepoint should fire or not
  • BPF_PROG_TYPE_XDP: a network packet filter run from the device-driver receive path
  • BPF_PROG_TYPE_PERF_EVENT: determine whether a perf event handler should fire or not
  • BPF_PROG_TYPE_CGROUP_SKB: a network packet filter for control groups
  • BPF_PROG_TYPE_CGROUP_SOCK: a network packet filter for control groups that is allowed to modify socket options
  • BPF_PROG_TYPE_LWT_*: a network packet filter for lightweight tunnels
  • BPF_PROG_TYPE_SOCK_OPS: a program for setting socket parameters
  • BPF_PROG_TYPE_SK_SKB: a network packet filter for forwarding packets between sockets
  • BPF_PROG_CGROUP_DEVICE: determine if a device operation should be permitted or not

随着新程序类型的增加,内核开发人员也会发现需要添加新的数据结构。

1.8eBPF数据结构

eBPF使用的主要的数据结构是eBPF map,这是一个通用的数据结构,用于在内核或内核和用户空间传递数据。其名称"map"也意味着数据的存储和检索需要用到key。

使用bpf()系统调用创建和管理map。当成功创建一个map后,会返回与该map关联的文件描述符。关闭相应的文件描述符的同时会销毁map。每个map定义了4个值:类型,元素最大数目,数值的字节大小,以及key的字节大小。eBPF提供了不同的map类型,不同类型的map提供了不同的特性。

  • BPF_MAP_TYPE_HASH: a hash table
  • BPF_MAP_TYPE_ARRAY: an array map, optimized for fast lookup speeds, often used for counters
  • BPF_MAP_TYPE_PROG_ARRAY: an array of file descriptors corresponding to eBPF programs; used to implement jump tables and sub-programs to handle specific packet protocols
  • BPF_MAP_TYPE_PERCPU_ARRAY: a per-CPU array, used to implement histograms of latency
  • BPF_MAP_TYPE_PERF_EVENT_ARRAY: stores pointers to struct perf_event, used to read and store perf event counters
  • BPF_MAP_TYPE_CGROUP_ARRAY: stores pointers to control groups
  • BPF_MAP_TYPE_PERCPU_HASH: a per-CPU hash table
  • BPF_MAP_TYPE_LRU_HASH: a hash table that only retains the most recently used items
  • BPF_MAP_TYPE_LRU_PERCPU_HASH: a per-CPU hash table that only retains the most recently used items
  • BPF_MAP_TYPE_LPM_TRIE: a longest-prefix match trie, good for matching IP addresses to a range
  • BPF_MAP_TYPE_STACK_TRACE: stores stack traces
  • BPF_MAP_TYPE_ARRAY_OF_MAPS: a map-in-map data structure
  • BPF_MAP_TYPE_HASH_OF_MAPS: a map-in-map data structure
  • BPF_MAP_TYPE_DEVICE_MAP: for storing and looking up network device references
  • BPF_MAP_TYPE_SOCKET_MAP: stores and looks up sockets and allows socket redirection with BPF helper functions

所有的map都可以通过eBPF或在用户空间的程序中使用 bpf_map_lookup_elem() 和bpf_map_update_elem()函数进行访问。某些map类型,如socket map,会使用其他执行特殊任务的eBPF辅助函数。eBPF的更多细节可以参见官方帮助文档

注: 在Linux4.4之前,bpf()要求调用者具有CAP_SYS_ADMIN capability权限,从Linux 4.4.开始,非特权用户可以使用BPF_PROG_TYPE_SOCKET_FILTER类型和相应的map创建受限的程序,然而这类程序无法将内核指针保存到map中,仅限于使用如下辅助函数: *  get_random *  get_smp_processor_id *  tail_call *  ktime_get_ns  可以通过sysctl禁用非特权访问: /proc/sys/kernel/unprivileged_bpf_disabled  eBPF对象(maps和程序)可以在不同的进程间共享。例如,在fork之后,子进程会继承引用eBPF对象的文件描述符。此外,引用eBPF对象的文件描述符可以通过UNIX域socket传输。引用eBPF对象的文件描述符可以通过dup(2)和类似的调用进行复制。当所有引用对象的文件描述符关闭后,才会释放eBPF对象。eBPF程序可以使用受限的C语言进行编写,并使用clang编译器编译为eBPF字节码。受限的C语言会禁用很多特性,如循环,全局变量,浮点数以及使用结构体作为函数参数。可以在内核源码的samples/bpf/*_kern.c 文件中查看例子。 内核中的just-in-time (JIT)可以将eBPF字节码转换为机器码,提升性能。在Linux 4.15之前,默认会禁用JIT,可以通过修改/proc/sys/net/core/bpf_jit_enable启用JIT。

  • 0 禁用JIT
  • 1 正常编译
  • 2 dehub模式。

从Linux 4.15开始,内核可能会配置CONFIG_BPF_JIT_ALWAYS_ON 选项,这种情况下,会启用JIT编译器,bpf_jit_enable 会被设置为1。

如下架构支持eBPF的JIT编译器:

  • * x86-64 (since Linux 3.18; cBPF since Linux 3.0);
  • * ARM32 (since Linux 3.18; cBPF since Linux 3.4);
  • * SPARC 32 (since Linux 3.18; cBPF since Linux 3.5);
  • * ARM-64 (since Linux 3.18);
  • * s390 (since Linux 4.1; cBPF since Linux 3.7);
  • * PowerPC 64 (since Linux 4.8; cBPF since Linux 3.1);
  • * SPARC 64 (since Linux 4.12);
  • * x86-32 (since Linux 4.18);
  • * MIPS 64 (since Linux 4.18; cBPF since Linux 3.16);
  • * riscv (since Linux 5.1)

1.9eBPF辅助函数

可以参考官方帮助文档查看libbpf库提供的辅助函数。

官方文档给出了现有的eBPF辅助函数。更多的实例可以参见内核源码的samples/bpf/和tools/testing/selftests/bpf/目录。

在官方帮助文档中有如下补充:

由于在编写帮助文档的同时,也同时在进行eBPF开发,因此新引入的eBPF程序或map类型可能没有及时添加到帮助文档中,可以在内核源码树中找到最准确的描述: include/uapi/linux/bpf.h:主要的BPF头文件。包含完整的辅助函数列表,以及对辅助函数使用的标记,结构体和常量的描述  net/core/filter.c:包含大部分与网络有关的辅助函数,以及使用的程序类型列表  kernel/trace/bpf_trace.c:包含大部分与程序跟踪有关的辅助函数  kernel/bpf/verifier.c:包含特定辅助函数使用的用于校验eBPF map有效性的函数  kernel/bpf/:该目录中的文件包含了其他辅助函数(如cgroups,sockmaps等)

如何编写eBPF程序

历史上,需要使用内核的bpf_asm汇编器将eBPF程序转换为BPF字节码。幸运的是,LLVM Clang编译器支持将C语言编写的eBPF后端编译为字节码。bpf()系统调用和BPF_PROG_LOAD命令可以直接加载包含这些字节码的对象文件。

可以使用C编写eBPF程序,并使用Clang的 -march=bpf参数进行编译。在内核的samples/bpf/ 目录下有很多eBPF程序的例子。大多数文件名中都有一个_kern.c后缀。Clang编译出的目标文件(eBPF字节码)需要由一个本机运行的程序进行加载(通常为使用_user.c开头的文件)。为了简化eBPF程序的编写,内核提供了libbpf库,可以使用辅助函数来加载,创建和管理eBPF对象。

例如,一个eBPF程序和使用libbpf的用户程序的大体流程为:

  1. 在用户程序中读取eBPF字节流,并将其传递给bpf_load_program()。
  2. 当在内核中运行eBPF程序时,将会调用bpf_map_lookup_elem()在一个map中查找元素,并保存一个新的值。
  3. 用户程序会调用 bpf_map_lookup_elem() 读取由eBPF程序保存的内核数据。

然而,大部分的实例代码都有一个主要的缺点:需要在内核源码树中编译自己的eBPF程序。幸运的是,BCC项目解决了这类问题。它包含了一个完整的工具链来编写并加载eBPF程序,而不需要链接到内核源码树。

二、eBPF框架

在开始说明之前先解释下eBPF上的名词,来帮忙更好的理解:

eBPF bytecode:将C语言写的钩子代码,通过clang编译成二进制字节码,通过程序加载到内核中,钩子触发后在kernel "虚拟机"中运行。

JIT: Just-in-time compilation,将字节码编译成本地机器码来提升运行速度,和Java中的概念类似。

Maps:钩子代码可以将一些统计类信息保存在键值对的map中,来与用户空间程序进行通信,传递数据。

关于eBPF机制详细的讲解网上有很多,这里就不展开了,这里先上一张图,这里包括了使用或者编写ebpf涉及到的所有东西,下面会对这个图进行详细的讲解。

640.png

foo_kern.c 钩子实现代码,主要负责:

  • 声明使用的Map节点
  • 声明钩子挂载点及处理函数

通过LLVM/clang编译成字节码

  • 编译命令:clang --target=bpf
  • android平台有集成eBPF的编译,后文会提到

foo_user.c 用户空间处理函数,主要负责:

  • 将foo_kern.c 编译成的字节码加载到kenel中
  • 读取Map中的信息并处理输出给用户

kernel当收到eBPF的加载请求时,会先对字节码进行验证,并通过JIT编译为机器码,当钩子事件来临后,调用钩子函数,kernel会对加载的字节码进行验证,来保证系统的安全性,主要验证规则如下:

  • a. 检查是否声明了GNU GPL,检查kernel的版本是否支持
  • b. 函数调用规则:

允许bpf函数之间的相互调用

只允许调用kernel允许的BPF helper函数,具体可以参考linux/bpf.h文件

上述以外的函数及动态链接都是不允许的。

  • c. 流程处理规则:

不允许使用loop循环以防止进入死循环卡死kernel

不允许有不可到达的分支代码

  • d. 堆栈大小被限制在MAX_BPF_STACK范围内。
  • e. 编译的字节码大小被限制在BPF_COMPLEXITY_LIMIT_INSNS范围内。

钩子挂载点,主要包括:

640.png

另外在kernel的源代码中samples/bpf目录下有大量的示例,感兴趣的可以阅读下。

相关文章
|
5天前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
26 6
|
4天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
19 9
|
2天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
18 6
|
3天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
18 5
|
1天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
14 4
|
4天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
5天前
|
存储 监控 安全
Linux内核调优的艺术:从基础到高级###
本文深入探讨了Linux操作系统的心脏——内核的调优方法。文章首先概述了Linux内核的基本结构与工作原理,随后详细阐述了内核调优的重要性及基本原则。通过具体的参数调整示例(如sysctl、/proc/sys目录中的设置),文章展示了如何根据实际应用场景优化系统性能,包括提升CPU利用率、内存管理效率以及I/O性能等关键方面。最后,介绍了一些高级工具和技术,如perf、eBPF和SystemTap,用于更深层次的性能分析和问题定位。本文旨在为系统管理员和高级用户提供实用的内核调优策略,以最大化Linux系统的效率和稳定性。 ###
|
4天前
|
Java Linux Android开发
深入探索Android系统架构:从Linux内核到应用层
本文将带领读者深入了解Android操作系统的复杂架构,从其基于Linux的内核到丰富多彩的应用层。我们将探讨Android的各个关键组件,包括硬件抽象层(HAL)、运行时环境、以及核心库等,揭示它们如何协同工作以支持广泛的设备和应用。通过本文,您将对Android系统的工作原理有一个全面的认识,理解其如何平衡开放性与安全性,以及如何在多样化的设备上提供一致的用户体验。
|
3天前
|
缓存 运维 网络协议
深入Linux内核架构:操作系统的核心奥秘
深入Linux内核架构:操作系统的核心奥秘
18 2
|
6天前
|
监控 网络协议 算法
Linux内核优化:提升系统性能与稳定性的策略####
本文深入探讨了Linux操作系统内核的优化策略,旨在通过一系列技术手段和最佳实践,显著提升系统的性能、响应速度及稳定性。文章首先概述了Linux内核的核心组件及其在系统中的作用,随后详细阐述了内存管理、进程调度、文件系统优化、网络栈调整及并发控制等关键领域的优化方法。通过实际案例分析,展示了这些优化措施如何有效减少延迟、提高吞吐量,并增强系统的整体健壮性。最终,文章强调了持续监控、定期更新及合理配置对于维持Linux系统长期高效运行的重要性。 ####