差生文具多之(一)eBPF

2023-11-06 04:30
文章标签 ebpf 差生 文具

本文主要是介绍差生文具多之(一)eBPF,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

在问题排查过程中, 通常包含: 整体观测, 数据采集, 数据分析这几个阶段. 对于简单问题的排查, 可以跳过前两个步骤, 无需额外收集数据, 直接通过分析日志中的关键信息就可以定位根因; 而对于复杂问题的排查, 为了对应用的行为有更完整的了解, 可以通过以下形式收集更多的行为数据帮助分析:

  1. 调高日志级别生成数据, 例如将内核的 /proc/sys/kernel/sched_schedstats 配置为 1, 例如将应用的日志级别从 INFO 调整为 DEBUG;

  2. 利用静态埋点生成数据, 例如内核的 tracepoint, 例如应用的 USDT;

  3. 利用动态埋点生成数据, 例如内核的 kprobe, 例如应用的 uprobe.

本篇主角 eBPF 提供 "动态将代码挂载在执行点(用户/内核/动态/静态)上进行数据采集, 通过内置数据结构(数组/哈希/函数映射)保存数据及和用户态程序通信" 的能力. 翻译成人话是:

  1. eBPF 通过验证器保证了挂载程序的安全性, 避免恶意程序的挂载;

  2. eBPF 虚拟机执行挂载程序, 进行数据采集, 数据过滤;

  3. eBPF 仅在用户空间请求时才将内核中的数据拷贝到用户空间, 保证消耗最小化.

eBPF 是如此强力(且流行)的机制, 我们怎么利用它对系统进行观测呢? eBPF 支持多种用户空间语言, 让我们先从简单的脚本语言 bpftrace 开始.

bpftrace

bpftrace 是基于 eBPF 的, 用于进行系统追踪的脚本语言, 语法和 awk 基本一致, 如下:

BEGIN
{// BEGIN 语句块, 一开始会被执行一次
} kprobe:foo // 指定挂载点
/condition/ // 仅当匹配该条件, 以下语句块才会被执行
{// 该脚本运行过程中, 每次匹配条件都会执行的语句块
}END
{// END 语句块, 结束时会被执行一次
} 

bpftrace 指定 -e 可以指定需要解析的语句块, 例如以下的语句可以打印一句 "hello world":

$ bpftrace -e 'BEGIN{printf("hello world.\n")}'

bpftrace 指定 -l 可以列出可用的挂载点, 可用挂载点的数量一定程度上反映了 eBPF 的能力边界, 例如在 CentOS Linux release 8.4.2105 上存在 8w 多个挂载点可供挂载函数. (如果降低内核编译优化等级, 可以导出更多可供挂载的符号)

$ bpftrace -l
...
kprobe:do_sys_open
...
$ bpftrace -l | wc -l
82589

例如我们想查看系统中 open 系统调用正在打开哪些文件, 只需要在挂载点的位置填上 kprobe:do_sys_open 即可:

$ bpftrace -e 'kprobe:do_sys_open {printf("%-7d %-10s %s\n", pid, comm, str(uptr(arg1)))}'
9016    awk        /proc/self/maps
9016    awk        /dev/null
1095    ksmtuned   /sys/kernel/mm/ksm/run
9017    ksmtuned   /dev/null

其中:

  1. kprobe:do_sys_open: 指明挂载点, 即每次执行 do_sys_open 函数体执行前先执行一次语句块 {printf("%-7d %-10s %s\n", pid, comm, str(uptr(arg1)))};

  2. pid: bpftrace 的内置变量, 保存了 curr 进程的进程号;

  3. comm: bpftrace 的内置变量, 保存了 curr 进程的进程名;

  4. str: bpftrace 的内置函数, 用于标志字符串;

  5. uptr: bpftrace 的内置函数, 用于标识指针来自用户空间, 当打印 char __user * 类型变量时需要用到;

  6. arg1: bpftrace 的内置变量, 下标从 0 开始, 此处指代内核函数 do_sys_open(int dfd, const char __user *filename, ...) 第二个参数. bpftrace 提供了很多实用的内置变量, 具体参考 reference_guide.

当 eBPF 程序写得越来越长, 单独写成 bpftrace 脚本是更合适的. 以下脚本实现了每秒输出当前系统每个 CPU 运行队列中等待运行进程的个数:

$ wget -qO - https://raw.githubusercontent.com/lilstaz/perf-tool-examples/main/bpftrace/runqlen.bt
...
profile:hz:99 // 1
{$cfs_rq = curtask->se.cfs_rq; // 2@tmp[cpu] = $cfs_rq->nr_running; // 3
}interval:s:1 // 4
{printf("@[%s]: %s\n", "CPU", "RQ_LEN");print(@tmp);
}
...
$ wget -qO - https://raw.githubusercontent.com/lilstaz/perf-tool-examples/main/bpftrace/runqlen.bt | bpftrace - # 5
@[CPU]: RQ_LEN
@[0]: 0
@[3]: 0
@[1]: 0
@[2]: 1

其中:

  1. 在 profile 模式中, 可以指定采样的频率, 当配置为 99HZ, 每个 CPU 每秒会产生大约 99 次时钟中断, 通过时钟中断注册的回调函数对所需的信息进行采集;

  2. bpftrace 脚本中变量前 $ 标志该变量是局部变量, @ 标志该变量是全局变量, 这里通过 bpftrace 内置变量 curtask 拿到了 curr 进程, 数据类型为 task_struct*, 通过 curtask->se.cfs_rq 获取到当前进程调度实体被挂载哪个运行队列;

  3. 将当前运行的 CPU 作为哈希 key, 将队列上调度实体的个数作为哈希 value, 保存到全局变量 @tmp 中;

  4. 每隔一秒钟输出一次列名及整个哈希表的内容;

  5. 运行效果, 当前虚拟机有 4 个核心, 其中 CPU 2 的运行队列中存在一个进程.

以上两个案例展示了: eBPF 程序可以挂载在内核函数上, 例如挂在 do_sys_open 函数上, 此时可以通过解析函数的输入获取所需的信息; 也可以注册为 perf_event 的回调函数, 利用 perf 提供的采样机制, 在中断上下文提取所需信息.

bcc

bcc 是一个开源的 Linux 动态跟踪工具. 无第三方模块依赖, 该工具继承 BPF 这个强大的内核中虚拟机的功能, 可对程序进行高效而且安全的跟踪. 在安装了 bcc 之后我们可以在目录 /usr/share/bcc 中找到它, 其 tools 子目录包含了大量实用的观测工具, 大部分由 python 代码写成:

$ ll /usr/share/bcc/tools/
....
-rwxr-xr-x. 1 root root  9528 Jan 24  2023 runqlat
-rwxr-xr-x. 1 root root  7919 Jan 24  2023 runqlen
-rwxr-xr-x. 1 root root  8929 Jan 24  2023 runqslower
...

以我们熟悉的 runqlen 作为例子, 该脚本几乎提供了上节例 2 一模一样的功能:

bpf_text = """
...
struct cfs_rq_partial { // 1struct load_weight load;RUNNABLE_WEIGHT_FIELDunsigned int nr_running, h_nr_running;
};
BPF_HISTOGRAM(dist, cpu_key_t, MAX_CPUS);int do_perf_event()
{unsigned int len = 0;pid_t pid = 0;struct task_struct *task = NULL;struct cfs_rq_partial *my_q = NULL;task = (struct task_struct *)bpf_get_current_task(); // 2my_q = (struct cfs_rq_partial *)task->se.cfs_rq;len = my_q->nr_running; // 3if (len > 0) // 4len--;STOREreturn 0;
}bpf_text.replace('STORE', ...) // 5
b = BPF(text=bpf_text, ...) // 6
b.attach_perf_event(ev_type=PerfType.SOFTWARE, // 7ev_config=PerfSWConfig.CPU_CLOCK, fn_name="do_perf_event",sample_period=0, sample_freq=99)

该 python 脚本主要包含三个部分: 挂载程序(bpf_text); 对挂载程序的修正(bpf_text.replace 对 bpf_text 做字符串替换); 使用 BPF 加载 eBPF 程序文本. 其中:

  1. 内核没有导出 cfs_rq 结构体, 需要我们自己定义一个数据结构, 目的是以正确的偏移获取 cfs_rq 的成员 nr_running;

  2. eBPF 程序通过 bpf_get_current_task 获取 curr 进程. 此处 task 等同于 bpftrace 脚本中的 curtask;

  3. 因为标号 1 处结构体的定义, 此处可以以正确的偏移量拿到 nr_running 数值;

  4. nr_running 计数包含当前运行的进程, 减去 1 之后才是等待队列的长度. 这里需要对 len == 0 的场景做特殊处理. 因为我们通过时钟中断注册的回调函数对所需的信息进行采集, 被中断打断的进程可能是 idle 进程, idle 进程运行是不计入 nr_running 的. 这里做了场景特判;

  5. 字符串处理, 此处替换 STORE 字符串. 脚本中有些字符串替换是为了兼容系统版本, 例如高版本 cfs_rq 才引入 runnable_weight 成员, 所以在高版本内核运行该脚本时, 将标号 1 数据结构中的 RUNNABLE_WEIGHT_FIELD 替换为 runnable_weight 成员, 而在低版本将 RUNNABLE_WEIGHT_FIELD 替换为空; 有些字符串替换是为了根据 bcc 脚本的输入做 bpf 代码的调整, 例如脚本输出的单位默认为纳秒, 可以指定参数 -m 将输出的单位替换为微秒;

  6. 验证并加载 BTF 元数据到内核, 创建相关的数据结构例如 dist;

  7. 这里指定使用 perf 基于时钟驱动的软件事件, 即利用 perf 的采样模式, frequency 指定为 99 HZ, 入口函数指定为 eBPF 程序中的 do_perf_event. 因此该语句主要涉及以下工作:
    1. 验证及加载 eBPF 程序到内核

    2. 为系统中每一个 CPU 注册 perf 软件事件

    3. 将每个 CPU 注册的软件事件的回调函数替换为 do_perf_event 函数

至此, 我们已经见识过用户态程序使用 bpftrace 及使用 python 写成的 runqlen, 用户态程序除了用于打印 eBPF 程序中收集得到的值, 还可以用于进行用户态内核态通信, 或者用于火焰图生成等数据后处理; 两份 runqlen 代码中 eBPF 程序的内容也基本一致. 下面让我们忽略这些微的差异, 从系统调用的角度看看 runqlen 和内核进行了哪些交互.

syscall

这一节采用 strace 工具对 bpftrace 写成的 runqlen 进行观测, 相关系统调用梳理如下:

bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_PERF_EVENT, insn_cnt=33, insns=0x5579141cf330, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(0, 0, 0), prog_flags=0, prog_name="99",..) = 15 // 1
/* 以下两句, 重复 CPU 个数次 */
perf_event_open({type=PERF_TYPE_SOFTWARE, size=0 /* PERF_ATTR_SIZE_??? */, config=PERF_COUNT_SW_CPU_CLOCK, sample_freq=99, sample_type=0, read_format=0, freq=1, precise_ip=0 /* arbitrary skid */, ...}, -1, 0, -1, PERF_FLAG_FD_CLOEXEC) = 16 // 2
ioctl(16, PERF_EVENT_IOC_SET_BPF, 15) // 3bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_PERF_EVENT, insn_cnt=37, insns=0x5579141e1580, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(0, 0, 0), prog_flags=0, prog_name="1", ...) // 4
perf_event_open({type=PERF_TYPE_SOFTWARE, size=0 /* PERF_ATTR_SIZE_??? */, config=PERF_COUNT_SW_CPU_CLOCK, sample_period=1000000000, sample_type=0, read_format=0, precise_ip=0 /* arbitrary skid */, ...}, -1, 0, -1, PERF_FLAG_FD_CLOEXEC) = 23 // 5
ioctl(23, PERF_EVENT_IOC_SET_BPF, 24)   = 0 // 6

因为脚本中有两段 eBPF 程序, 所以可以看到系统调用分为两个部分(line 1-3, line 4-6), 其中:

  1. 加载采样频率为 99HZ 的 eBPF 程序;

  2. 为 0 号 CPU (CPU ID 由倒数第三个参数指定) 注册一个以时钟驱动的软件事件; 使用 perf_event 的 sampling 模式, 当频率被设置为 99HZ, 每秒采样的次数由 sample_freq 参数指定为 99;

  3. 将步骤 2 软件事件的回调函数修改为步骤 1 加载的 eBPF 程序, 假设系统中有 4 个核, 步骤 2 和 3 会重复 4 次.

  4. 加载 1 秒打印 1 次输出的 eBPF 函数;

  5. 为 0 号 CPU 注册一个 1 秒触发一次的软件事件;

  6. 将步骤 5 软件事件的回调函数修改为步骤 4 加载的 eBPF 程序. 因为每秒只需要输出一次, 此处让 0 号 CPU 负责周期性执行该段 eBPF 程序.

后记

本篇主要从一个小例子 runqlen 入手, 串联介绍了 bpftrace bcc 工具集和相关的系统调用. 因为 eBPF 挂载点渗透到内核每个子系统, 并且对 eBPF 程序提供了安全性验证, 极大增强了内核的可观测性. 如果将内核状态机看作时序维度上的一叠黑胶片, eBPF 就是你手里那杯显影液.

ref

  1. bcc python developer tutorial

  2. linux observability with bpf

  3. https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md

这篇关于差生文具多之(一)eBPF的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/354464

相关文章

Android中使用eBPF跟踪 FD打开与关闭

我们知道在Android系统中 fd 泄露,可以通过 google 开发的 fdtrack来进行排查,但是有些情况下我们想在外发release版本去监控fd泄露情况,fdtrack就不能很好的满足需求了。可以用eBPF去监控fd泄露。 Android中使用eBPF跟踪 FD打开与关闭

观测云核心技术解密:eBPF Tracing 实现原理

前言 eBPF 是一种强大的内核技术,允许在内核中安全地执行自定义代码。通过 eBPF,开发者可以在不修改内核源码的情况下,对内核功能进行扩展和监控。eBPF Tracing 利用这一技术,对系统调用、内核函数等进行跟踪,从而实现对应用行为的深入洞察。 与传统的监控方式相比,eBPF Tracing 具有以下优势: 无侵入性:无需修改应用代码即可进行监控。高性能:在内核层面执行,减少了对应用

Android 中ebpf 的集成和调试

1. BPF 简介 BPF,是Berkeley Packet Filter的简称,最初构想提出于 1992 年。是一种网络分流器和数据包过滤器,允许早操作系统级别捕获和过滤计算机网络数据包。它为数据链路层提供了一个原始接口,允许发送和接收原始链路层数据包,并允许用户空间进程提供一个过滤程序来制定它想要接收哪些数据包。BPF 仅返回通过进程提供的过滤器的数据包。这样避免了将不需要的数据包从操作系统

网络编程之XDP技术的基础eBPF

一、XDP和TC的技术支撑 在前面分析了XDP和TC技术,从它们的细节里可以看出,它们都在调用eBPF的钩子函数。那么eBPF是什么呢?在2021年曾经写过一篇《eBPF介绍》的初级文章,对eBPF做了一个入门级的普及。但是未曾在技术层面上进行展开。这次借着XDP分析的机会,将其一并进行一下分析。先看一下XDP、TC及eBPF它们的关系图: 从图上可以看出,XDP和TC通过Maps调用内核中

openEuler 2203 中利用ebpf实现文件变更监控

在利用ebpf监控文件变更操作中,记录一些内核参数获取的过程。 1.环境信息   openeuler 2203 2.常用的修改文件的方式或者命令 1).直接使用文件系统的操作接口,比如write等; 2.)系统命令,比如vim/vi,sed,echo,cp,mv 涉及到的系统调用主要有: sys_enter_writesys_enter_renameat2sys_enter_rename

小玉买文具(洛谷-P1421)

题目描述 班主任给小玉一个任务,到文具店里买尽量多的签字笔。已知一只签字笔的价格是1元9角,而班主任给小玉的钱是a元b角,小玉想知道,她最多能买多少只签字笔呢。 输入输出格式 输入格式: 输入的数据,在一行内,包括两个整数,依次表示a和b,a<=10000,b<=9。 输出格式: 输出一个整数,表示小玉最多能买多少只签字笔。 输入输出样例 输入样例#1:  10 3 输出样例#1:  5

btrace:binder_transaction+eBPF+Golang实现通用的Android APP动态行为追踪工具

一、简介:     在进行Android恶意APP检测时,需要进行自动化的行为分析,一般至少包括行为采集和行为分析两个模块。其中,行为分析有基于规则、基于机器学习、基于深度学习甚至基于大模型的方案,各有各的优缺点,不是本文关注的重点,本文主要关注Android APP的动态行为采集。在做Android APP逆向分析时经常需要通过hook系统调用观察APP的行为,也需要一个动态行为追踪工具。

均匀分布差生正态分布

文章目录 中心极限定理 中心极限定理 中心极限定理是说,n只要越来越大,这n个数的样本均值会趋近于正态分布,并且这个正态分布以u为均值,sigma^2/n为方差。 换句话说,假设我们与样本 x 1 , x 2.... x n x1, x2....x_n x1,x2....xn​, 并且已经知道 E ( x ) = u , D ( x ) = σ 2 E(x) = u, D(x)

linux可观测性ebpf(一) ----------- 环境搭建

参考书籍 开发环境 Ubuntu 18.04.6 LTS (GNU/Linux 5.4.0-150-generic x86_64) 1.1 下载内核源码 cd /usr/src/sudo git clone -b v5.4 https://github.com/torvalds/linux.git 1.2 下载书中代码 git clone https://github.com/

深入理解Linux调试工具eBPF和strace、内存泄漏处理、Kubernetes容器调试以及C++协程的崩溃信息收集

在软件开发领域,无论是初级开发者还是资深工程师,都需要面对复杂的调试工作。本文将介绍几个重要的调试工具和技术,并提供实际调试方法的指导,包括Linux环境下的eBPF和strace,内存泄漏问题的处理,Kubernetes容器环境下的调试技巧,以及C++20协程中的崩溃信息收集策略 一、Linux环境下的eBPF和strace eBPF调试方法 eBPF简介:作为一项强大的虚拟机技