BPF的可移植性和CO-RE (Compile Once – Run Everywhere) (4)

BCC会将task->pid重写为对bpf_probe_read()的调用,非常方便(虽然有时候不会成功,具体取决于使用的表达式的复杂度)。当使用libbpf时,由于它没有BCC的代码重写功能,因此需要使用其他方式来得到相同的结果。

如果添加了BTF_PROG_TYPE_TRACING 程序,那么就可以轻松掌握BPF验证程序,允许理解和跟踪BTF类型的本质,并允许使用指针直接读取内核内存,避免使用bpf_probe_read()调用。

Libbpf + BPF_PROG_TYPE_TRACING 方式:

pid_t pid = task->pid;

将该功能与BPF CO-RE配合使用,可以支持可移植(即可重定位)的字段读取,此时需要将此代码封装到编译器内置的__builtin_preserve_access_index中

BPF_PROG_TYPE_TRACING + BPF CO-RE 方式:

pid_t pid = __builtin_preserve_access_index(({ task->pid; }));

这种方式能够正常工作,同时也支持不同内核版本间的可移植性。但鉴于BPF_PROG_TYPE_TRACING的前沿性,因此必须显式地使用bpf_probe_read()。

非CO-RE libbpf方式:

pid_t pid; bpf_probe_read(&pid, sizeof(pid), &task->pid);

现在,使用CO-RE+libbpf,我们有两种方式来实现访问pid字段的值。一种是直接使用bpf_core_read()替换bpf_probe_read():

pid_t pid; bpf_core_read(&pid, sizeof(pid), &task->pid);

bpf_core_read()是一个简单的宏,它会将所有的参数直接传递给bpf_probe_read(),但也会使Clang通过__builtin_preserve_access_index()记录第三个参数(&task->pid)的字段的偏移量。

bpf_probe_read(&pid, **sizeof**(pid), __builtin_preserve_access_index(&task->pid));

但像bpf_probe_read()/bpf_core_read()这样的调用方式很快就会变得难以维护,特别是获取通过指针连在一起的结构体时。例如,获取当前进程的可执行文件的inode号时,可以使用BCC获取:

u64 inode = task->mm->exe_file->f_inode->i_ino;

当使用 bpf_probe_read()/bpf_core_read()时,将会变为4个调用,并使用一个临时变量来保存这些中间指针,才能最终获得i_ino字段。当使用BPF CO-RE时,我们可以使用一个辅助宏来使用类似BCC的方式获得该字段的值:

BPF CO-RE方式

u64 inode = BPF_CORE_READ(task, mm, exe_file, f_inode, i_ino);

此外,如果想要使用一个变量保存内容,则可以使用如下方式,避免使用额外的中间变量:

u64 inode; BPF_CORE_READ_INTO(&inode, task, mm, exe_file, f_inode, i_ino);

还有一个对应的 bpf_core_read_str(),可以直接替换bpf_probe_read_str();还有一个BPF_CORE_READ_STR_INTO()宏,其工作方式与BPF_CORE_READ_INTO()类似,但会在最后一个字段执行bpf_probe_read_str()调用。

可以通过bpf_core_field_exists()宏校验目标内核是否存在某个字段,并以此作相应的处理。

pid_t pid = bpf_core_field_exists(task->pid) ? BPF_CORE_READ(task, pid) : -1;

此外,可以通过bpf_core_field_size()宏捕获任意字段的大小,以此来保证不同内核版本间的字段大小没有发生变化。

u32 comm_sz = bpf_core_field_size(task->comm); /* will set comm_sz to 16 */

除此之外,在某些情况下,当读取一个内核结构体的比特位字段时,可以使用特殊的BPF_CORE_READ_BITFIELD() (使用直接内存读取) 和BPF_CORE_READ_BITFIELD_PROBED() (依赖bpf_probe_read() 调用)宏。它们抽象了提取比特位字段繁琐而痛苦的细节,同时保留了跨内核版本的可移植性:

struct tcp_sock *s = ...; /* with direct reads */ bool is_cwnd_limited = BPF_CORE_READ_BITFIELD(s, is_cwnd_limited); /* with bpf_probe_read()-based reads */ u64 is_cwnd_limited; BPF_CORE_READ_BITFIELD_PROBED(s, is_cwnd_limited, &is_cwnd_limited);

字段重定位和相关的宏是BFP CO-RE提供的主要能力。它涵盖了很多实际的使用案例。

处理内核版本和配置差异

在一些场景下,BPF程序不得不处理内核间的差异。如某些字段名称的变更导致其变为了一个完全不同的字段(但具有相同的意义)。反之亦然,当字段不变,但其含义发生了变化。如在内核4.6之后,task_struct结构体的utime和stime字段从以秒为单位换为以纳秒为单位,这种情况下,不得不进行一些转换工作。有时,需要提取的数据存在于某些内核配置中,但已在其他内核配置中进行了编译。还有在很多其他场景下,不可能有一个适合所有内核的通用类型。

为了处理上述问题,BPF CO-RE提出了两种补充方案:libbpf提供了extern Kconfig variablesstruct flavors.

Libbpf提供的外部变量很简单。BPF程序可以使用一个知名名称(如LINUX_KERNEL_VERSION,用于获取允许的内核的版本)定义一个外部变量,或使用Kconfig的键(如CONFIG_HZ,用于获取内核的HZ值),libbpf会使BPF程序可以将这类外部变量用作任何其他全局变量。这些变量具有正确的值,与执行BPF程序的活动内核相匹配。此外,BPF校验器会跟踪这些变量,并能够使用它们进行高级控制流分析和消除无效代码。查看如下例子,了解如何使用BPF CO-RE抽取线程的CPU用户时间:

extern u32 LINUX_KERNEL_VERSION __kconfig; extern u32 CONFIG_HZ __kconfig; u64 utime_ns; if (LINUX_KERNEL_VERSION >= KERNEL_VERSION(4, 11, 0)) utime_ns = BPF_CORE_READ(task, utime); else /* convert jiffies to nanoseconds */ utime_ns = BPF_CORE_READ(task, utime) * (1000000000UL / CONFIG_HZ);

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpfpjy.html