差生文具多之(一)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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/131511.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python---字符串中的查找方法--index()--括号里是要获取的字符串

index()方法其功能与find()方法完全一致,唯一的区别在于当要查找的子串没有出现在字符串中时,find()方法返回-1,而index()方法则直接 报错。 find()方法相关链接:Python---字符串中的查找方法--find()--括…

postMessage

A:端口3000 import React, { useEffect } from react;function App() {useEffect(() > {const childWindow document.getElementById(child).contentWindow;const sendMessageToChild () > {childWindow.postMessage("主页面消息", "http://localhost:…

【电路笔记】-谐波

谐波 文章目录 谐波1、概述2、频谱分析3、已知信号4、未知信号5、总结 周期性信号并不总是完美的正弦模式,例如我们之前有关 正弦波的文章之一中介绍的那样。 有时,信号确实可以是简单正弦波的叠加,它们被称为复杂波形。 在本文中&#xff0…

【C语法学习】17 - fwrite()函数

文章目录 1 函数原型2 参数3 返回值4 示例 1 函数原型 fwrite():将ptr指向的内存空间中储存的数据块写入与指定流stream相关联的二进制文件中,函数原型如下: size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream)2 参…

基于stm32F4的智能宠物喂食器的设计:LVGL界面、定时喂食喂水通风

宠物喂食器 一、功能设计二、元器件选型三、UI设计四、原理图设计五、源代码设计六、成品展示 实物链接:https://m.tb.cn/h.5iCUX6H?tkPL65WXCEipQ CZ3457 一、功能设计 1、设计一个触摸屏作为人机交互 2、通过触摸屏设置时间定时喂食喂水通风 3、获取当前水槽的…

没想到这么齐全!这份 Python 实战干货yyds

今天我分享一些Python学习神器资料,有需要的小伙文末自行免费领取。 1.200Python练手案例: 2.Python全套视频教程等: 3.浙大Python学习套装: * 4.Python实战案例: 5.Pandas学习大礼包 6.学习手册大礼包 Python知识…

2023年【熔化焊接与热切割】免费试题及熔化焊接与热切割考试总结

题库来源:安全生产模拟考试一点通公众号小程序 熔化焊接与热切割免费试题参考答案及熔化焊接与热切割考试试题解析是安全生产模拟考试一点通题库老师及熔化焊接与热切割操作证已考过的学员汇总,相对有效帮助熔化焊接与热切割考试总结学员顺利通过考试。…

单链表的应用(2)

环形链表的约瑟夫问题 编号为 1 到 n 的 n 个人围成一圈。从编号为 1 的人开始报数,报到 m 的人离开。 下一个人继续从 1 开始报数。 n-1 轮结束以后,只剩下一个人,问最后留下的这个人编号是多少? 利用链表实现 思路&#xff1…

Firefox修改缓存目录的方法

打开Firefox,在地址栏输入“about:config” 查找是否有 browser.cache.disk.parent_directory,如果没有就新建一个同名的字符串,然后修改值为你要存放Firefox浏览器缓存的目录地址(E:\FirefoxCacheFiles) 然后重新…

Linux第一个小程序进度条

缓冲区 ​ 在写进度条程序之前我们需要介绍一下缓冲区,缓冲区有两种,输入和输出缓冲区,这里主要介绍输出缓冲区。在我们用C语言写代码时,输出一些信息,实际上是先输出到输出缓冲区里,然后才输出到我们的显…

数据包端到端的流程

流程 A给F发送一个数据包的流程: 首先 A(192.168.0.1)通过子网掩码(255.255.255.0)计算出自己与 F(192.168.2.2)并不在同一个子网内,于是决定发送给默认网关(192.168.0.…

Redis之哨兵模式

文章目录 前言一、主从复制1.概述2.作用3.模拟实践搭建场景模拟实践 二、哨兵模式1.概述2.配置使用3.优缺点4.sentinel.conf完整配置 总结 前言 从主从复制到哨兵模式。 一、主从复制 1.概述 主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Red…

简单工厂模式、工厂方法模式、抽象工厂模式

简介 将实例化代码提取出来,放到一个类中统一管理和维护,达到和主项目依赖关系的解耦,从而提高项目的扩展性和维护性。 工厂模式将复杂的对象创建工作隐藏起来,而仅仅暴露出一个接口供客户使用,具体的创建工作由工厂管…

再见了,提示~ 谷歌发布自适应提示方法,从此告别提示工程!

夕小瑶科技说 原创 作者 | 谢年年、ZenMoore 大模型虽好,但却存在着一个恼人的问题:大模型回答得好不好,取决于我们问题问得怎么样。一个好的、详细的问题往往可以产生惊人的效果... 所以... ChatGPT 问世之后,最火的书可能不是…

系列十二、过滤器 vs 拦截器

一、过滤器 vs 拦截器 1.1、区别 (1)触发时机不一样,过滤器是在请求进入容器后Servlet之前进行预处理的,请求结束返回也是,是在Servlet处理完后,返回给前端之前; (2)过滤…

ke9案例三:页面提交文件,我服务器端接收

案例三:页面提交文件,我服务器端接收 ProcessFile.java 1value "/process-file" 2获取邮件消息的所有部分part--Collection<Part> partsrequest.getParts(); 3遍历每一个part 4之后可以打印头文件等String headerpart.getHeader("content-disposition&q…

map set

目录 一、关联式容器 二、键值对 三、树形结构的关联式容器 3.1 set 3.1.1 set的介绍 3.1.2 set的使用 3.2 multiset 3.2.1 multiset的介绍 3.2.2 multiset的使用 3.3 map 3.3.1 map的介绍 3.3.2 map的使用 …

【ICN综述】信息中心网络隐私安全

ICN基本原理&#xff1a; 信息中心网络也是需要实现在不可信环境下可靠的信息交换和身份认证 信息中心网络采用以数据内容为中心的传输方式代替现有IP 网络中以主机为中心的通信方式&#xff0c;淡化信息数据物理或逻辑位置的重要性&#xff0c;以内容标识为代表实现数据的查找…

竞赛 深度学习猫狗分类 - python opencv cnn

文章目录 0 前言1 课题背景2 使用CNN进行猫狗分类3 数据集处理4 神经网络的编写5 Tensorflow计算图的构建6 模型的训练和测试7 预测效果8 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习猫狗分类 ** 该项目较为新颖&a…

leetcode经典面试150题---5.多数元素

目录 题目描述 前置知识 代码 方法一 排序法 思路 实现 复杂度 方法二 哈希表 思路 实现 题目描述 给定一个大小为 n 的数组 nums &#xff0c;返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的&#xff0c;并且给…