我们先来看一个内核的变更,内核 commit 2f064a59a11f 对 `struct task_struct` 中的 `state` 字段做了修改:
sched: Change task_struct::state
Change the type and name of task_struct::state. Drop the volatile and
shrink it to an 'unsigned int'. Rename it in order to find all uses
such that we can use READ_ONCE/WRITE_ONCE as appropriate.
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Reviewed-by: Daniel Bristot de Oliveira <bristot@redhat.com>
Acked-by: Will Deacon <will@kernel.org>
Acked-by: Daniel Thompson <daniel.thompson@linaro.org>
Link: https://lore.kernel.org/r/20210611082838.550736351@infradead.org
这个变更其实很简单,就是对一个结构体字段重命名并修改了类型,将 `volatile long state;` 修改成了 `unsigned int __state;`。这导致了几个 BCC 工具在内核版本 5.14 以上的内核不可用(参见 1)。
如下这段代码,在内核 5.14 的版本以前是可以正确运行的:
SEC("kprobe/wake_up_new_task")
int BPF_KPROBE(sched_switch, struct task_struct *tsk)
{
__s64 state = BPF_CORE_READ(tsk, state);
bpf_printk("task state: %d\n", state);
return 0;
}
但是在 5.14 及以后的版本中,由于字段名称的修改,我们也要做相应的修改:
SEC("kprobe/wake_up_new_task")
int BPF_KPROBE(sched_switch, struct task_struct *tsk)
{
__s64 state = BPF_CORE_READ(tsk, __state);
bpf_printk("task state: %d\n", state);
return 0;
}
如果是这样的话,同一个 BPF 程序就需要为不同内核版本做相应的修改并编译。要解决这个问题,就要靠 libbpf 的 CO-RE 解决方案:struct flavors (参见 2)
代码如下:
struct task_struct___x {
unsigned int __state;
} __attribute__((preserve_access_index));
static __s64 get_task_state(void *task)
{
struct task_struct___x *t = task;
if (bpf_core_field_exists(t->__state))
return BPF_CORE_READ(t, __state);
return BPF_CORE_READ((struct task_struct *)task, state);
}
然后在 BPF 程序中使用 `__s64 state = get_task_state(tsk);` 获取 state/__state 字段。
简单地说,就是定义一个新的结构体 `struct task_struct___x`,这个结构体的名称在 libbpf 看来和 `struct task_struct` 是一样的,即忽略 `___` 及其后缀,借助 `bpf_core_field_exists` 宏及编译器的魔法,来解决内核版本之间的差异导致的兼容性问题。
关键细节:
BPF_CORE_READ
__attribute__((preserve_access_index))
bpf_core_field_exists
鉴于本文只作一个简单的科普,我们不会深入以上细节,待我们把 libbpf CO-RE 的另外几个方面讲完,我们再回过头来深入细节及源码。
参考链接
https://github.com/iovisor/bcc/pull/3659
https://nakryiko.com/posts/bpf-portability-and-co-re/