2 快速入门
在本章中,我将使用简单的"Hello World"示例来让你更好地了解它。
在阅读本书的过程中,你会了解到有多种不同的库和框架可用于编写eBPF应用程序。作为热身,我将向你展示一种从编程角度来看可能是最易学的方法:BCC Python 框架。这为编写基本的eBPF程序提供了一种非常简单的方法。现在我不推荐你使用这种方法来编写你打算发布给其他用户的应用程序,但它非常适合你迈出第一步。
2.1 BCC Hello World”
BCC的安装参见:https://github.com/iovisor/bcc/blob/master/INSTALL.md
下面是 hello.py 的完整源代码,它是使用 BCC 的 Python 库编写的 eBPF "Hello World "应用程序1:- #!/usr/bin/python3
- from bcc import BPF
- program = r"""
- int hello(void *ctx) {
- bpf_trace_printk("Hello World!");
- return 0;
- }
- """
- b = BPF(text=program)
- syscall = b.get_syscall_fnname("execve")
- b.attach_kprobe(event=syscall, fn_name="hello")
- b.trace_print()
复制代码 这段代码由两部分组成:将在内核中运行的 eBPF 程序本身,以及将 eBPF 程序加载到内核中并读出其产生的跟踪的一些用户空间代码。hello.py 是该应用程序的用户空间部分,而 hello() 则是在内核中运行的 eBPF 程序。
eBPF 程序所做的就是使用一个辅助函数 bpf_trace_printk() 来编写一条信息。辅助函数是 "扩展 "BPF 区别于其 "经典 "前身的另一个特征。它们是一组函数,eBPF 程序可以调用这些函数与系统交互;我将在第 5 章进一步讨论它们。现在,你可以把它想象成打印一行文本。
整个 eBPF 程序在 Python 代码中被定义为一个名为 program 的字符串。这个 C 语言程序在执行之前需要编译,但 BCC 可以帮你完成。eBPF 程序需要附加到一个事件中,在本例中,我选择附加到系统调用 execve 中,它是用来执行程序的系统调用。每当有任何事物或任何人在这台机器上启动一个新程序时,都会调用 execve(),从而触发 eBPF 程序。虽然 "execve() "名称是 Linux 的标准接口,但内核中实现它的函数名称取决于芯片架构,不过 BCC 提供了一种方便的方法,让我们可以查找所运行机器的函数名称:syscall = b.get_syscall_fnname("execve") 。
现在,syscall 表示我要使用 kprobe 附加的内核函数名称(第 1 章中介绍了 kprobe 的概念):b.attach_kprobe(event=syscall, fn_name="hello") 。
至此,eBPF 程序已加载到内核中,并附加到一个事件上,因此每当有新的可执行文件在机器上启动时,程序就会被触发。在 Python 代码中要做的就是读取内核输出的跟踪信息并将其写入屏幕:
b.trace_print() 。这个 trace_print() 函数将无限循环(直到您停止程序,也许用 Ctrl+C 停止),显示任何跟踪。
下图展示了这段代码。Python 程序编译 C 代码,将其加载到内核,并将其连接到 execve_syscall_kprobe。只要该(虚拟)机器上的任何应用程序调用 execve(),就会触发 eBPF hello() 程序,该程序会将一行跟踪记录写入特定的伪文件。(Python 程序会从伪文件中读取跟踪信息并显示给用户。
2.2 运行“Hello World”
运行此程序,根据您所使用的(虚拟)机器上的情况,您可能会立即看到跟踪信息生成,因为其他进程可能正在使用execve系统调用执行程序。如果您没有看到任何内容,请打开第二个终端并执行任何您喜欢的命令,您将看到“Hello World”生成的相应跟踪信息:- # python hello.py
- b' agent-3189934 [075] d... 5010918.056552: bpf_trace_printk: Hello World!'
- b' bash-3189935 [076] d... 5010918.057489: bpf_trace_printk: Hello World!'
- b' bash-3189937 [077] d... 5010918.058402: bpf_trace_printk: Hello World!'
- b' bash-3189938 [078] d... 5010918.058468: bpf_trace_printk: Hello World!'
- b' <...>-3189939 [079] d... 5010918.058530: bpf_trace_printk: Hello World!'
- b' bash-3189940 [080] d... 5010918.058646: bpf_trace_printk: Hello World!'
- b' bash-3189943 [080] d... 5010918.061652: bpf_trace_printk: Hello World!'
- b' bash-3189945 [079] d... 5010918.062962: bpf_trace_printk: Hello World!'
复制代码 由于eBPF非常强大,使用它需要特殊权限。权限会自动分配给root用户,因此运行eBPF程序最简单的方法是以root身份运行,例如使用sudo。
CAP_BPF 在内核版本 5.8 中引入,它提供了足够的权限来执行某些 eBPF 操作,如创建特定类型的映射。然而,您可能还需要额外权限:
- CAP_PERFMON 和 CAP_BPF 均需启用才能加载跟踪程序。
- CAP_NET_ADMIN 和 CAP_BPF 均需启用才能加载网络程序。
关于此内容的更多细节,请参阅 Milan Landaverde 的博客文章《CAP_BPF 入门》(https://mdaverde.com/posts/cap-bpf/)。
一旦 hello eBPF 程序加载并绑定到事件,它将被现有进程生成的事件触发。这应强化你在第 1 章中学习的几个要点:
- eBPF 程序可用于动态更改系统行为。无需重启机器或重新启动现有进程。eBPF 代码在绑定到事件后立即生效。
- 无需对其他应用程序进行任何修改即可使其对eBPF可见。只要您在该机器上拥有终端访问权限,无论在终端中运行何种可执行文件,该文件都会调用execve()系统调用。若您已将hello程序附加到该系统调用,它将被触发以生成跟踪输出。同样,若您运行一个调用可执行文件的脚本,该脚本也会触发hello eBPF程序。您无需对终端的 shell、脚本或正在运行的可执行文件进行任何更改。
跟踪输出不仅显示“Hello World”字符串,还包含触发 hello eBPF 程序运行的事件的一些额外上下文信息。在本节开头显示的示例输出中,执行 execve 系统调用的进程的进程 ID 为 3189935,且正在运行 bash 命令。对于跟踪消息,此上下文信息作为内核跟踪基础设施的一部分添加(该基础设施并非特定于 eBPF),但如本章后文所述,您也可以在 eBPF 程序内部检索此类上下文信息。
你可能会好奇 Python 代码如何知道从哪里读取跟踪输出。答案并不复杂——内核中的 bpf_trace_printk() 辅助函数始终将输出发送到同一个预定义的伪文件位置:/sys/kernel/debug/tracing/trace_pipe。你可以使用 cat 命令查看其内容;访问该文件需要 root 权限。
对于简单的“Hello World”示例或基本调试目的,单一跟踪管道位置已足够,但其功能非常有限。输出格式几乎没有灵活性,且仅支持字符串输出,因此不适合传递结构化信息。更重要的是,整个(虚拟)机器上仅存在一个此类位置。若有多个 eBPF 程序同时运行,它们将把跟踪输出写入同一跟踪管道,这会让人工操作者感到非常困惑。
从 eBPF 程序中获取信息的更好方式是使用 eBPF 映射。
2.3 BPF映射
映射是一种数据结构,可从 eBPF 程序和用户空间访问。映射是扩展 BPF 与经典 BPF 相比的真正显著特征之一。(你可能会认为这意味着它们通常被称为“eBPF 映射”,但你经常会看到“BPF 映射”。与通常情况一样,这两个术语可以互换使用。)
映射可用于在多个 eBPF 程序之间共享数据,或在用户空间应用程序与内核中运行的 eBPF 代码之间进行通信。典型用途包括:
- 用户空间将配置信息写入映射,供 eBPF 程序读取
- eBPF 程序存储状态信息,供另一个 eBPF 程序(或同一程序的未来运行实例)后续读取
- eBPF 程序将结果或指标写入映射,供用户空间应用程序检索并展示结果
在 Linux 的uapi/linux/bpf.h文件中定义了多种类型的 BPF 映射,内核文档中也对它们有相关说明。总体而言,它们都是键值存储结构,本章将展示用于哈希表、性能监控(perf)和环形缓冲区(ring buffers)的映射示例,以及 eBPF 程序数组的映射示例。
某些映射类型被定义为数组,其键类型始终为 4 字节索引;其他映射则是哈希表,可使用任意数据类型作为键。
还有一些映射类型针对特定类型的操作进行了优化,例如先进先出队列、先进后出栈、最近最少使用数据存储、最长前缀匹配以及布隆过滤器(一种概率数据结构,设计用于快速判断元素是否存在)。
某些 eBPF 映射类型存储特定类型对象的信息。例如,sockmaps 和 devmaps 存储关于套接字和网络设备的信息,并被网络相关的 eBPF 程序用于重定向流量。程序数组映射存储一组索引的 eBPF 程序,(如本章后文所述)这用于实现尾调用,即一个程序可以调用另一个程序。甚至还有一种映射的映射类型,用于存储关于映射的信息。
某些映射类型具有按 CPU 核心划分的变体,也就是说,内核为每个 CPU 核心的映射版本使用不同的内存块。这可能会让你对非按 CPU 核心划分的映射的并发问题产生疑问,因为多个 CPU 核心可能同时访问同一映射。在内核版本 5.1 中为(某些)映射添加了自旋锁支持,我们将在第 5 章中再次讨论这一主题。
下一个示例(hello-map.py)展示了使用哈希表映射的一些基本操作。它还演示了 BCC 的一些便捷抽象,使使用映射变得非常简单。
2.3.1 哈希表映射
与本章之前的示例类似,此 eBPF 程序将附加到 execve 系统调用入口处的 kprobe。它将用键值对填充哈希表,其中键是用户 ID,值是该用户 ID 下运行的进程调用 execve 的次数计数器。实际上,这个示例将显示每个不同用户运行程序的次数。
首先,让我们看看 eBPF 程序本身的 C 代码:
hello-map.py:- #!/usr/bin/python3
- from bcc import BPF
- from time import sleep
- program = r"""
- BPF_HASH(counter_table);
- int hello(void *ctx) {
- u64 uid;
- u64 counter = 0;
- u64 *p;
- uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
- p = counter_table.lookup(&uid);
- if (p != 0) {
- counter = *p;
- }
- counter++;
- counter_table.update(&uid, &counter);
- return 0;
- }
- """
- b = BPF(text=program)
- syscall = b.get_syscall_fnname("execve")
- b.attach_kprobe(event=syscall, fn_name="hello")
- # Attach to a tracepoint that gets hit for all syscalls
- # b.attach_raw_tracepoint(tp="sys_enter", fn_name="hello")
- while True:
- sleep(2)
- s = ""
- for k,v in b["counter_table"].items():
- s += f"ID {k.value}: {v.value}\t"
- print(s)
复制代码 BPF_HASH() 是 BCC 宏,用于定义哈希表映射。bpf_get_current_uid_gid() 是一个辅助函数,用于获取触发此 kprobe 事件的进程的用户 ID。用户 ID 存储在返回的 64 位值的最低 32 位中。(最高 32 位存储组 ID,但该部分被屏蔽。)在哈希表中查找与用户 ID 匹配的键的条目。它返回哈希表中对应值的指针。如果哈希表中存在该用户 ID 的条目,将计数器变量设置为哈希表中当前值(由 p 指针指向)。如果哈希表中不存在该用户 ID 的条目,指针将为 0,计数器值保持为 0。无论当前计数器值为何,均将其加1。
C 语言不支持在结构体上定义方法,说明 BCC 的 C 语言版本是一种非常松散的 C 样式语言,BCC 会在将代码发送给编译器之前对其进行重写。BCC 提供了一些方便的快捷方式和宏,它会将这些转换为“标准”的 C 代码。
执行结果:- # python hello-map.py
- ID 0: 1 ID 1084: 1
- ID 0: 1 ID 1084: 16
- ID 0: 2 ID 1084: 17
- ID 0: 2 ID 1084: 32
- # python hello-map.py
- ID 1084: 1 ID 0: 1
- ID 1084: 16 ID 0: 1
- ID 1084: 16 ID 0: 1
- ID 1084: 17 ID 0: 2
- ID 1084: 32 ID 0: 2
复制代码 $ ./hello-map.py
此示例每两秒生成一行输出,无论是否发生任何变化。在输出结束时,哈希表中包含两个条目:
在第二个终端中,我的用户 ID 为 1084。使用此用户 ID 运行 ls 命令会增加 execve 计数器。当我运行 sudo ls 时,这会导致两次 execve 调用:一次是 sudo 的执行,用户 ID 为 501;另一次是 ls 的执行,用户 ID 为 root 的 0。
在此示例中,我使用哈希表将数据从 eBPF 程序传递到用户空间。(我也可以在此处使用数组类型的映射,因为键是一个整数;哈希表允许使用任意类型作为键。)当数据自然以键值对形式存在时,哈希表非常方便,但用户空间代码必须定期轮询该表。Linux 内核已支持 perf 子系统,用于从内核向用户空间发送数据,而 eBPF 包含对 perf 缓冲区及其继任者 BPF 环形缓冲区的支持。让我们来看看。
2.3.2 Perf 和环形缓冲区映射(Ring Buffer Map)
在本节中,我将描述一个略微复杂的“Hello World”示例,该示例使用 BCC 的 BPF_PERF_OUTPUT 功能,允许你将数据写入自定义结构的 perf 环形缓冲区映射中。
有一种较新的构造称为“BPF 环形缓冲区”,现在通常优于 BPF perf 缓冲区,如果你使用的是 5.8 版本或更高的内核。Andrii Nakryiko 在他的 BPF 环形缓冲区博客文章中讨论了它们的区别。你将在第 4 章中看到 BCC 的 BPF_RINGBUF_OUTPUT 的示例。
环形缓冲区绝非 eBPF 独有,但我还是解释一下,以防您之前未接触过。您可以将环形缓冲区视为逻辑上以环形组织的一块内存,拥有独立的“写入”和“读取”指针。任意长度的数据会被写入写入指针所在的位置,数据的长度信息包含在该数据的头部中。写入指针会移动到该数据末尾之后,准备进行下一次写入操作。
同样,对于读取操作,数据会从读取指针所在的位置读取,并通过头部信息确定读取多少数据。读取指针与写入指针沿相同方向移动,以便指向下一个可用的数据块。如图所示,这是一个包含三个不同长度数据项的环形缓冲区,这些数据项可供读取。
如果读指针追上写指针,这仅仅意味着没有数据可读。如果写操作会导致写指针超过读指针,数据将不会被写入,且丢失计数器会被递增。读操作会包含丢失计数器,以指示自上次成功读取以来是否丢失了数据。
如果读写操作以完全相同的速率且无波动发生,且每次操作包含相同数量的数据,那么理论上可以使用一个刚好能容纳该数据大小的环形缓冲区。但在大多数应用中,读写操作之间的时间间隔会存在波动,因此缓冲区大小需要进行调整以适应这种情况。
代码:- #!/usr/bin/python3
- from bcc import BPF
- program = r"""
- BPF_PERF_OUTPUT(output);
- struct data_t {
- int pid;
- int uid;
- char command[16];
- char message[12];
- };
- int hello(void *ctx) {
- struct data_t data = {};
- char message[12] = "Hello World";
- data.pid = bpf_get_current_pid_tgid() >> 32;
- data.uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
- bpf_get_current_comm(&data.command, sizeof(data.command));
- bpf_probe_read_kernel(&data.message, sizeof(data.message), message);
- output.perf_submit(ctx, &data, sizeof(data));
- return 0;
- }
- """
- b = BPF(text=program)
- syscall = b.get_syscall_fnname("execve")
- b.attach_kprobe(event=syscall, fn_name="hello")
- def print_event(cpu, data, size):
- data = b["output"].event(data)
- print(f"{data.pid} {data.uid} {data.command.decode()} {data.message.decode()}")
- b["output"].open_perf_buffer(print_event)
- while True:
- b.perf_buffer_poll()
复制代码 BCC 定义了宏 BPF_PERF_OUTPUT,用于创建一个用于从内核向用户空间传递消息的映射。我将此映射命名为 output。每次运行 hello() 时,代码将写入一个结构体的数据。这是该结构体的定义,其中包含进程 ID、当前运行命令的名称以及一条文本消息的字段。data 是用于存储待提交数据结构的局部变量,而 message 则存储“Hello World”字符串。bpf_get_current_pid_tgid() 是一个辅助函数,用于获取触发此 eBPF 程序运行的进程 ID。它返回一个 64 位值,其中前 32 位为进程 ID。bpf_get_current_uid_gid() 是你在前一个示例中看到的用于获取用户 ID 的辅助函数。同样,bpf_get_current_comm() 是一个辅助函数,用于获取在执行 execve 系统调用的进程中运行的可执行文件(或“命令”)的名称。这是一个字符串,而非像进程 ID 和用户 ID 那样的数值。在 C 语言中,你不能直接使用 = 赋值字符串。你需要将字符串应写入的字段地址 &data.command 作为参数传递给辅助函数。对于这个示例,消息始终是“Hello World”。bpf_probe_read_kernel() 将其复制到数据结构中的正确位置。
执行:- # python hello-buffer.py
- 3707042 1084 agent Hello World
- 3707043 1084 bash Hello World
- 3707045 1084 bash Hello World
- 3707046 1084 bash Hello World
- 3707047 1084 bash Hello World
- 3707048 1084 bash Hello World
- 3707051 1084 bash Hello World
- 3707053 1084 bash Hello World
- 3707056 1084 bash Hello World
- 3707058 1084 bash Hello World
- 3707061 1084 bash Hello World
- 3707064 1084 bash Hello World
- 3707067 1084 bash Hello World
- 3707069 1084 bash Hello World
- 3707072 1084 bash Hello World
- 3707081 1084 agent Hello World
- 3707082 0 agent Hello World
- 3707085 1084 agent Hello World
- 3707086 1084 bash Hello World
- 3707088 1084 bash Hello World
- 3707089 1084 bash Hello World
- 3707090 1084 bash Hello World
- 3707091 1084 bash Hello World
- 3707094 1084 bash Hello World
复制代码 $ sudo ./hello-buffer.py
11654 node Hello World
11655 sh Hello World
...
与之前一样,你可能需要在同一台(虚拟)机器上打开第二个终端并执行一些命令以触发输出。
与原始“Hello World”示例的最大区别在于,不再使用单一的中央跟踪管道,而是通过名为 output 的环形缓冲区映射传递数据,该映射由本程序为自身使用而创建,如图所示。
你可以通过执行 cat /sys/kernel/debug/tracing/trace_pipe 来验证信息是否未发送到跟踪管道。
除了演示环形缓冲区映射的使用外,此示例还展示了用于获取触发 eBPF 程序运行的事件上下文信息的 eBPF 辅助函数。在此您已看到辅助函数用于获取用户 ID、进程 ID 以及当前命令名称。如第 7 章所示,可用的上下文信息集以及可用于检索这些信息的有效辅助函数集,取决于程序类型及触发该程序的事件类型。
此类上下文信息可供eBPF代码访问,正是其在可观察性方面如此有价值的原因。每次发生事件时,eBPF程序不仅可以报告事件发生的事实,还可以报告触发事件的相关信息。它还具有很高的性能,因为所有这些信息都可以在内核中收集,无需进行任何同步上下文切换到用户空间。
本书后续内容将展示更多示例,其中 eBPF 辅助函数用于收集其他上下文数据,以及 eBPF 程序修改上下文数据甚至完全阻止事件发生的示例。
参考资料
- 软件测试精品书籍文档下载持续更新 https://github.com/china-testing/python-testing-examples 请点赞,谢谢!
- 本文涉及的python测试开发库 谢谢点赞! https://github.com/china-testing/python_cn_resouce
- python精品书籍下载 https://github.com/china-testing/python_cn_resouce/blob/main/python_good_books.md
- Linux精品书籍下载 https://www.cnblogs.com/testing-/p/17438558.html
- python八字排盘 https://github.com/china-testing/bazi
- 联系方式:钉ding或V信: pythontesting
2.3.3 函数调用
您已看到 eBPF 程序可调用内核提供的辅助函数,但若想将所写代码拆分为函数呢?在软件开发中,通常认为将常见代码提取到一个函数中,以便从多个位置调用,而不是重复编写相同的代码行,是一种良好的实践。但在早期,eBPF 程序不允许调用除辅助函数以外的其他函数。为了绕过这一限制,程序员会指示编译器“始终内联”他们的函数,如下所示:- static __always_inline void my_function(void *ctx, int val)
复制代码 通常,源代码中的函数会导致编译器生成跳转指令,使执行流程跳转到被调用函数的指令集(并在该函数完成后跳回)。如图2-5左侧所示。右侧展示了函数被内联时的行为:没有跳转指令;取而代之的是,函数的指令副本直接在调用函数内部生成。
如果函数从多个位置被调用,这将导致编译后的可执行文件中包含该函数指令的多份副本。(有时编译器可能会出于优化目的选择内联函数,这也是为什么你可能无法将 kprobe 附加到某些内核函数的原因。我将在第 7 章中再次讨论这一点。)
从 Linux 内核 4.16 和 LLVM 6.0 开始,取消了要求函数必须内联的限制,以便 eBPF 程序员能够更自然地编写函数调用。然而,这一功能(称为“BPF 到 BPF 函数调用”或“BPF 子程序”)目前尚未被 BCC 框架支持,因此我们将在下一章中再讨论它。(当然,如果你使用的是内联函数,仍然可以继续使用 BCC。)
在 eBPF 中,还有另一种将复杂功能分解为更小部分的机制:尾调用。
2.3.4 Tail调用
如 ebpf.io 所述,“尾调用可以调用并执行另一个 eBPF 程序,并替换执行上下文,类似于 execve() 系统调用在常规进程中的工作方式。” 换句话说,尾调用完成后,执行不会返回给调用者。
尾调用绝非 eBPF 编程的专属特性。尾调用的核心动机是避免在函数递归调用时反复向栈中添加帧,这最终可能导致栈溢出错误。如果你能将代码安排为在最后一步调用递归函数,则与调用函数关联的栈帧实际上并未执行任何有用操作。尾调用允许在不增长栈的情况下调用一系列函数。这在eBPF中尤为有用,因为其栈大小仅限于512字节。
尾调用通过 bpf_tail_call() 辅助函数实现,其签名如下:- long bpf_tail_call(void *ctx, struct bpf_map *prog_array_map, u32 index)
复制代码 该函数的三个参数含义如下:
- ctx 用于将调用 eBPF 程序的上下文传递给被调用函数。
- prog_array_map 是类型为 BPF_MAP_TYPE_PROG_ARRAY 的 eBPF 映射,其中存储了一组用于标识 eBPF 程序的文件描述符。
- index 指示应调用该映射中的哪个 eBPF 程序。
该辅助函数较为特殊,因为如果调用成功,它不会返回。当前运行的 eBPF 程序将被调用的程序替换到栈中。该辅助函数可能失败,例如,如果指定的程序不存在于映射中,此时调用程序将继续执行。
用户空间代码需要将所有 eBPF 程序加载到内核中(如往常一样),并设置程序数组映射。
让我们看一个用 Python 和 BCC 编写的简单示例:主 eBPF 程序附加在所有系统调用的通用入口点的跟踪点上。该程序使用尾调用跟踪特定系统调用操作码的特定消息。如果某个操作码没有尾调用,程序将跟踪一个通用消息。- prog_array_map.call(ctx, index)
复制代码 如果你使用 BCC 框架,要实现尾调用可以使用稍显简化的形式:- bpf_tail_call(ctx, prog_array_map, index)
复制代码 完整代码- #!/usr/bin/python3
- from bcc import BPF
- import ctypes as ct
- program = r"""
- BPF_PROG_ARRAY(syscall, 500);
- int hello(struct bpf_raw_tracepoint_args *ctx) {
- int opcode = ctx->args[1];
- syscall.call(ctx, opcode);
- bpf_trace_printk("Another syscall: %d", opcode);
- return 0;
- }
- int hello_exec(void *ctx) {
- bpf_trace_printk("Executing a program");
- return 0;
- }
- int hello_timer(struct bpf_raw_tracepoint_args *ctx) {
- int opcode = ctx->args[1];
- switch (opcode) {
- case 222:
- bpf_trace_printk("Creating a timer");
- break;
- case 226:
- bpf_trace_printk("Deleting a timer");
- break;
- default:
- bpf_trace_printk("Some other timer operation");
- break;
- }
- return 0;
- }
- int ignore_opcode(void *ctx) {
- return 0;
- }
- """
- b = BPF(text=program)
- b.attach_raw_tracepoint(tp="sys_enter", fn_name="hello")
- ignore_fn = b.load_func("ignore_opcode", BPF.RAW_TRACEPOINT)
- exec_fn = b.load_func("hello_exec", BPF.RAW_TRACEPOINT)
- timer_fn = b.load_func("hello_timer", BPF.RAW_TRACEPOINT)
- prog_array = b.get_table("syscall")
- # Ignore all syscalls initially
- for i in range(len(prog_array)):
- prog_array[ct.c_int(i)] = ct.c_int(ignore_fn.fd)
- # Only enable few syscalls which are of the interest
- prog_array[ct.c_int(59)] = ct.c_int(exec_fn.fd)
- prog_array[ct.c_int(222)] = ct.c_int(timer_fn.fd)
- prog_array[ct.c_int(223)] = ct.c_int(timer_fn.fd)
- prog_array[ct.c_int(224)] = ct.c_int(timer_fn.fd)
- prog_array[ct.c_int(225)] = ct.c_int(timer_fn.fd)
- prog_array[ct.c_int(226)] = ct.c_int(timer_fn.fd)
- b.trace_print()
复制代码 BCC 提供了一个 BPF_PROG_ARRAY 宏,用于轻松定义类型为 BPF_MAP_TYPE_PROG_ARRAY 的映射。我将该映射命名为 syscall,并允许包含 300 个条目,这对于本示例而言已足够。 在稍后看到的用户空间代码中,我将把这个 eBPF 程序附加到 sys_enter 原生跟踪点,该跟踪点在每次系统调用时触发。附加到原生跟踪点的 eBPF 程序接收的上下文采用 bpf_raw_tracepoint_args 结构的形式。在 sys_enter 的情况下,原始跟踪点参数包括标识正在调用哪个系统调用的操作码。这里我们对程序数组中与操作码匹配的条目进行尾调用。这行代码将在 BCC 将源代码传递给编译器之前,被重写为对 bpf_tail_call() 辅助函数的调用。如果尾调用成功,这行输出操作码值的代码将永远不会被执行。我使用此方法为映射中没有程序条目的操作码提供默认跟踪行。hello_exec() 是一个将被加载到系统调用程序数组映射中的程序,当操作码指示为 execve() 系统调用时,将作为尾调用执行。它仅会生成一条跟踪行,告知用户正在执行新程序。hello_timer() 是另一个将被加载到系统调用程序数组中的程序。在此情况下,它将被程序数组中的多个条目引用。ignore_opcode() 是一个尾调用程序,不执行任何操作。我将使用它来处理那些不希望生成任何跟踪信息的系统调用。
现在让我们看看用户空间代码如何加载和管理这组 eBPF 程序:与之前看到的不同,这次用户空间代码将主 eBPF 程序附加到 sys_enter 跟踪点,而不是附加到 kprobe。对 b.load_func() 的调用会为每个尾调用程序返回一个文件描述符。请注意,尾调用程序必须与父程序具有相同的程序类型——在本例中为 BPF.RAW_TRACEPOINT。此外,值得指出的是,每个尾调用程序本身就是一个独立的 eBPF 程序。
用户空间代码会在系统调用映射中创建条目。该映射无需为每个可能的操作码都填充完整;如果某个操作码没有对应条目,则表示不会执行尾调用。此外,多个条目指向同一个eBPF程序也是完全可以的。在此情况下,我希望hello_timer()尾调用在任何与定时器相关的系统调用时被执行。某些系统调用被系统频繁调用,导致每条调用都会在跟踪输出中占用一行,使输出变得难以阅读。我已为多个系统调用使用了ignore_opcode()尾调用。
执行:- # python hello-tail.py
- b' python-3839308 [112] d... 5072367.259263: bpf_trace_printk: Another syscall: 280'
- b' auditd-3295 [092] d... 5072367.259381: bpf_trace_printk: Another syscall: 207'
- b' auditd-3295 [092] d... 5072367.259391: bpf_trace_printk: Another syscall: 64'
- b' python-3839308 [112] d... 5072367.259399: bpf_trace_printk: Another syscall: 280'
- b' auditd-3295 [092] d... 5072367.259400: bpf_trace_printk: Another syscall: 98'
- b' auditd-3295 [092] d... 5072367.259404: bpf_trace_printk: Another syscall: 72'
- b' auditd-3298 [084] d... 5072367.259408: bpf_trace_printk: Another syscall: 98'
- b' auditd-3298 [084] d... 5072367.259412: bpf_trace_printk: Another syscall: 64'
- b' auditd-3298 [084] d... 5072367.259417: bpf_trace_printk: Another syscall: 98'
- b' sedispatch-3297 [085] d... 5072367.259420: bpf_trace_printk: Another syscall: 63'
- b' sedispatch-3297 [085] d... 5072367.259427: bpf_trace_printk: Another syscall: 72'
- b' auditd-3295 [092] d... 5072367.259625: bpf_trace_printk: Another syscall: 207'
- b' auditd-3295 [092] d... 5072367.259633: bpf_trace_printk: Another syscall: 64'
- b' auditd-3295 [092] d... 5072367.259641: bpf_trace_printk: Another syscall: 44'
- b' auditd-3295 [092] d... 5072367.259653: bpf_trace_printk: Another syscall: 98'
- b' python-3839308 [112] d... 5072367.259655: bpf_trace_printk: Another syscall: 280'
- b' auditd-3295 [092] d... 5072367.259659: bpf_trace_printk: Another syscall: 72'
- b' auditd-3298 [084] d... 5072367.259660: bpf_trace_printk: Another syscall: 98'
- b' auditd-3298 [084] d... 5072367.259662: bpf_trace_printk: Another syscall: 64'
- b' auditd-3298 [084] d... 5072367.259666: bpf_trace_printk: Another syscall: 98'
- b' sedispatch-3297 [085] d... 5072367.259668: bpf_trace_printk: Another syscall: 63'
- b' sedispatch-3297 [085] d... 5072367.259673: bpf_trace_printk: Another syscall: 72'
- b' auditd-3295 [092] d... 5072367.259882: bpf_trace_printk: Another syscall: 207'
- b' auditd-3295 [092] d... 5072367.259890: bpf_trace_printk: Another syscall: 64'
- b' auditd-3295 [092] d... 5072367.259897: bpf_trace_printk: Another syscall: 44'
- b' auditd-3295 [092] d... 5072367.259903: bpf_trace_printk: Another syscall: 98'
- b' auditd-3295 [092] d... 5072367.259908: bpf_trace_printk: Another syscall: 72'
- b' auditd-3298 [084] d... 5072367.259908: bpf_trace_printk: Another syscall: 98'
- b' auditd-3298 [084] d... 5072367.259910: bpf_trace_printk: Another syscall: 64'
- b' auditd-3298 [084] d... 5072367.259914: bpf_trace_printk: Another syscall: 98'
- b' sedispatch-3297 [085] d... 5072367.259915: bpf_trace_printk: Another syscall: 63'
- b' sedispatch-3297 [085] d... 5072367.259923: bpf_trace_printk: Another syscall: 72'
- b'CPU:50 [LOST 113 EVENTS]'
- b' pstree-3839310 [050] d... 5072367.259962: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.259973: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.259981: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.259984: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.259989: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.259993: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.259997: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.260002: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.260006: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.260016: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.260023: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.260029: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.260034: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.260037: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.260041: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.260045: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.260049: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.260055: bpf_trace_printk: Another syscall: 61'
- b' pstree-3839310 [050] d... 5072367.260058: bpf_trace_printk: Another syscall: 57'
- b' pstree-3839310 [050] d... 5072367.260063: bpf_trace_printk: Another syscall: 57'
- b' pstree-3839310 [050] d... 5072367.260066: bpf_trace_printk: Another syscall: 56'
- b' pstree-3839310 [050] d... 5072367.260074: bpf_trace_printk: Another syscall: 79'
- b' pstree-3839310 [050] d... 5072367.260076: bpf_trace_printk: Another syscall: 80'
复制代码 具体执行的系统调用并不重要,但你可以看到不同的尾调用被调用并生成跟踪消息。你还可以看到默认消息“另一个系统调用”,用于那些在尾调用程序映射中没有条目的操作码。参考: Paul Chaignon 关于不同内核版本中 BPF 尾调用成本的博客文章。
尾调用自内核版本 4.2 起在 eBPF 中得到支持,但长期以来与 BPF 到 BPF 函数调用不兼容。这一限制在内核 5.10.10 中被取消。由于可以将多达 33 个尾调用串联起来,再加上每个 eBPF 程序的指令复杂度限制为 100 万条指令,这意味着当今的 eBPF 程序员在内核中编写非常复杂的代码时有很大的灵活性。
2.4 小结
我希望通过展示一些 eBPF 程序的具体示例,本章能帮助你巩固对在内核中由事件触发的 eBPF 代码的理解。你还看到了使用 BPF 映射将数据从内核传递到用户空间的示例。
使用BCC框架隐藏了程序构建、加载到内核以及与事件关联的许多细节。在下一章中,我将向您展示一种不同的“Hello World”编写方法,并深入探讨这些隐藏的细节。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |