task_iter 是一个使用 BPF 迭代器(BPF Iterators)的示例程序。该示例会遍历主机上的所有任务(task),并获取这些任务的进程标识符(pid)、进程名称、内核栈(kernel stack)以及任务状态(state)。

用户可将某个进程标识符作为可执行文件的第一个参数传入,此举会过滤掉所有不属于该指定进程的任务。注:你可以使用 BlazeSym 工具对内核栈追踪信息(kernel stacktraces)进行符号化解析(类似 profile 示例中的做法),但为简化代码,相关实现代码已被省略。

前置知识

NOTE:

《Learning eBPF》 中并没有关于 BPF 迭代器的内容。

运行

1
2
3
4
5
$ ./task_iter
Task Info. Pid: 290. Process Name: systemd. Kernel Stack Len: 5. State: INTERRUPTIBLE
Task Info. Pid: 291. Process Name: init-systemd(Ub. Kernel Stack Len: 5. State: INTERRUPTIBLE
Task Info. Pid: 361. Process Name: init. Kernel Stack Len: 8. State: <unknown>
Task Info. Pid: 361. Process Name: init. Kernel Stack Len: 6. State: INTERRUPTIBLE

struct task_info

1
2
3
4
5
6
7
8
9
10
11
12
13
#define TASK_COMM_LEN 16
#define MAX_STACK_LEN 127

struct task_info {
pid_t pid;
pid_t tid;
__u32 state;
char comm[TASK_COMM_LEN];

int kstack_len;

__u64 kstack[MAX_STACK_LEN];
};

The BPF side

BPF_MAP_TYPE_PERCPU_ARRAY 📌

这是 BPF_MAP_TYPE_ARRAY 映射类型的 per-CPU variant。

这种 per-CPU 版本的 map 会为为每个逻辑 CPU 分配独立的数组。当通过大部分辅助函数(helper function)访问该映射时,会隐式访问当前 eBPF 程序所在 CPU 对应的那个数组。

由于程序执行期间会禁用抢占机制,因此不会有其他程序能并发访问同一块内存。这一特性确保了永远不会出现任何竞态条件,同时也因无需阻塞和同步逻辑而提升了性能,但其代价是会产生较大的内存占用。

1
2
3
4
5
6
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, struct task_info);
} task_info_buf SEC(".maps");

value 的类型基本不做限制,但 key 必须为 32 位无符号整数。

task_info_buf 仅 1 个元素:用于临时存储单条任务信息(因为 iter 程序遍历每个 task 时,每次只处理一条)。

get_task_state

跨 Linux 内核版本(5.14 前后)安全获取 task_struct 的进程状态字段(5.14 内核将 state 重命名为 __state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct task_struct___post514 {
unsigned int __state;
} __attribute__((preserve_access_index));

struct task_struct___pre514 {
long state;
} __attribute__((preserve_access_index));

static __u32 get_task_state(void *arg)
{
if (bpf_core_field_exists(struct task_struct___pre514, state)) {
struct task_struct___pre514 *task = arg;

return task->state;
} else {
struct task_struct___post514 *task = arg;

return task->__state;
}
}

preserve_access_index

__attribute__((preserve_access_index)) 是 BPF CO-RE(Compile Once – Run Everywhere)的必需属性:

  • 告诉 Clang 编译器为结构体字段生成「访问索引」(access index),BPF 加载器会利用这个索引结合内核 BTF 信息,自动适配不同内核版本的内存布局;

  • 若缺少该属性,自定义结构体无法和内核真实 task_struct 做字段映射,会导致 BPF_VERIFIER 校验失败。

相关阅读:

bpf_core_field_exists 📌

用于查询待加载程序的目标内核中是否存在某个结构体字段。

SEC(“iter/task”)

遍历内核中的任务。

1
2
SEC("iter/task")
int get_tasks(struct bpf_iter__task *ctx)

可以在 vmlinux.h 中找到 bpf_iter__task 结构体的定义:

1
2
3
4
5
6
7
8
struct bpf_iter__task {
union {
struct bpf_iter_meta *meta;
};
union {
struct task_struct *task;
};
};

seq_file

  • 为上层开发者提供的类似【迭代器】的简易文件读取接口
1
2
3
4
5
6
7
struct seq_file *seq = ctx->meta->seq;
struct task_struct *task = ctx->task;
struct task_info *t;
long res;

if (!task)
return 0;

bpf_map_lookup_elem

1
2
3
4
5
6
7
t = bpf_map_lookup_elem(&task_info_buf, &zero);
if (!t)
return 0;

t->pid = task->tgid;
t->tid = task->pid;
t->state = get_task_state(task);

bpf_probe_read_kernel_str 📌

1
bpf_probe_read_kernel_str(t->comm, TASK_COMM_LEN, task->comm);

将以空字符(NUL)结尾的字符串从内核不安全地址 unsafe_ptr复制到目标缓冲区 dst

bpf_get_task_stack 📌

1
2
res = bpf_get_task_stack(task, t->kstack, sizeof(__u64) * MAX_STACK_LEN, 0);
t->kstack_len = res <= 0 ? res : res / sizeof(t->kstack[0]);

在 BPF 程序提供的缓冲区中返回用户栈或内核栈。注:仅当目标任务为当前任务时,才会填充用户栈;其他所有任务都会返回 -EOPNOTSUPP 错误码。

要实现此功能,该辅助函数需要传入 task 参数 —— 这是一个指向 struct task_struct 结构体的有效指针。为存储栈追踪信息,BPF 程序需提供一个大小非负的缓冲区 buf。最后一个参数 flags 用于存放需要跳过的栈帧数量(取值范围为 0 至 255)。

bpf_seq_write 📌

1
bpf_seq_write(seq, t, sizeof(struct task_info));

t 写入 seq 中。

The user-space side

get_task_state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static const char *get_task_state(__u32 state)
{
/* Taken from:
* https://elixir.bootlin.com/linux/latest/source/include/linux/sched.h#L85
* There are a lot more states not covered here but these are common ones.
*/
switch (state) {
case 0x0000: return "RUNNING";
case 0x0001: return "INTERRUPTIBLE";
case 0x0002: return "UNINTERRUPTIBLE";
case 0x0200: return "WAKING";
case 0x0400: return "NOLOAD";
case 0x0402: return "IDLE";
case 0x0800: return "NEW";
default: return "<unknown>";
}
}

open_and_load

1
2
3
4
5
6
/* Open, load, and verify BPF application */
skel = task_iter_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
goto cleanup;
}

bpf_iter_attach_opts 📌

1
2
3
4
5
6
7
8
9
LIBBPF_OPTS(bpf_iter_attach_opts, opts);
union bpf_iter_link_info linfo;
pid_t pid_filter = 0;

/* Attach BPF iterator program */
memset(&linfo, 0, sizeof(linfo));
linfo.task.pid = pid_filter; /* If the pid is set to zero, no filtering logic is applied */
opts.link_info = &linfo;
opts.link_info_len = sizeof(linfo);

bpf_program__attach_iter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
skel->links.get_tasks = bpf_program__attach_iter(skel->progs.get_tasks, &opts);
if (!skel->links.get_tasks) {
err = -errno;
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
/* Alternatively, if the user doesn't want to provide any option, the following simplified
* version can be used:
* err = task_iter_bpf__attach(skel);
* if (err) {
* fprintf(stderr, "Failed to attach BPF skeleton\n");
* goto cleanup;
* }
*/

bpf_iter_create

1
2
3
4
5
6
iter_fd = bpf_iter_create(bpf_link__fd(skel->links.get_tasks));
if (iter_fd < 0) {
err = -1;
fprintf(stderr, "Failed to create iter\n");
goto cleanup;
}

while

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while (true) {
ret = read(iter_fd, &buf, sizeof(struct task_info));
if (ret < 0) {
if (errno == EAGAIN)
continue;
err = -errno;
break;
}
if (ret == 0)
break;
if (buf.kstack_len <= 0) {
printf("Error getting kernel stack for task. Task Info. Pid: %d. Process Name: %s. Kernel Stack Error: %d. State: %s\n",
buf.pid, buf.comm, buf.kstack_len, get_task_state(buf.state));
} else {
printf("Task Info. Pid: %d. Process Name: %s. Kernel Stack Len: %d. State: %s\n",
buf.pid, buf.comm, buf.kstack_len, get_task_state(buf.state));
}
}

cleanup

1
2
3
4
cleanup:
/* Clean up */
close(iter_fd);
task_iter_bpf__destroy(skel);