目录
SEC("tp/sched/sched_process_exec")
("tp/sched/sched_process_exec")
trace_event_raw_sched_process_exec
fname_off = ctx->__data_loc_filename & 0xFFFF
bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off)
SEC("tp/sched/sched_process_exit")
duration_ns = bpf_ktime_get_ns() - *start_ts
e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL
ring_buffer__poll(rb, 100 /* timeout, ms */)
下面介绍的,是libbpf-bootstrap库中example下的bootstrap代码:
bootstrap
介绍
是一个基于ebpf的Linux 内核跟踪程序
- 它跟踪了每一个进程的启动时间,运行时间等执行事件,并将相关信息记录到环形缓冲区中,以便后续的处理和分析
运行情况
会监视:
进程的启动时间,记录启动/退出事件,进程的pid,进程ppid,进程文件名,退出码,持续运行时间
选项
- -v 显示详细信息
- -d + 设置最小运行时间(如果设置了最短持续时间,将不会触发exec事件)
(但我感觉有点不合理,所以我修改了一下 -- 设置了最短持续时间后,会过滤掉持续时间未达到该阈值的进程):
代码解释
.bpf.c
源码(handle_exec)
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause /* Copyright (c) 2020 Facebook */ #include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> #include <bpf/bpf_core_read.h> #include "bootstrap.h" char LICENSE[] SEC("license") = "Dual BSD/GPL"; struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 8192); __type(key, pid_t); __type(value, u64); } exec_start SEC(".maps"); //存储进程启动时的时间戳 struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); } rb SEC(".maps"); //环形缓冲区 const volatile unsigned long long min_duration_ns = 0; //最小持续时间 SEC("tp/sched/sched_process_exec") int handle_exec(struct trace_event_raw_sched_process_exec *ctx) //一个新的进程被执行时,就会触发此事件。该函数会从事件上下文中获取相关信息,并将其放入环形缓冲区以供后续处理 { struct task_struct *task; unsigned fname_off; //存储文件名的偏移量 struct event *e; pid_t pid; u64 ts; //时间戳 /* remember time exec() was executed for this PID */ pid = bpf_get_current_pid_tgid() >> 32; ts = bpf_ktime_get_ns(); bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY); /* don't emit exec events when minimum duration is specified */ if (min_duration_ns) return 0; /* reserve sample from BPF ringbuf */ e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); if (!e) return 0; /* fill out the sample with data */ task = (struct task_struct *)bpf_get_current_task(); e->exit_event = false; e->pid = pid; e->ppid = BPF_CORE_READ(task, real_parent, tgid); bpf_get_current_comm(&e->comm, sizeof(e->comm)); fname_off = ctx->__data_loc_filename & 0xFFFF; bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off); /* successfully submit it to user-space for post-processing */ bpf_ringbuf_submit(e, 0); return 0; }
总之,就是在进程启动时,记录当前有关信息,并打包成结构体event的形式,交给环形缓冲区里
语法
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, pid_t);
__type(value, u64);
} exec_start SEC(".maps");定义了一个bpf哈希映射
__uint
- 是一个编译器宏,用于指定 BPF Map 的类型以及其他属性
- type -- 指定了哈希映射的类型
- BPF_MAP_TYPE_HASH -- 表示是哈希表类型的bpf映射
- BPF_MAP_TYPE_RINGBUF -- 表示环形缓冲区
- max_entries -- 定义了哈希映射的最大条目数
环形缓冲区
- 它是一种特殊的数据结构,用于在内核中跟踪事件并将其传递给用户空间的程序进行处理
- 具有固定大小的缓冲区,新的数据可以不断地写入缓冲区的尾部,而读取操作则可以从缓冲区的头部进行
- 当缓冲区满时,最旧的数据将会被覆盖,从而实现了循环利用
__type
- 是一个编译器宏,用于指定 BPF Map 中key和value的类型
SEC("tp/sched/sched_process_exec")
指示编译器将该函数映射到特定的 BPF 事件点上
tp(tracepoint)
- 是内核中预定义的一些关键点(内核开发人员事先定义并注册的事件点或跟踪点)
- 它们允许用户在这些点上执行BPF程序,从而监视、记录或操作系统的各种活动和事件
sched
- 代表调度(scheduler)子系统,负责管理和调度系统中的进程和线程
- 调度子系统中包括了一些跟踪点,用于跟踪进程的调度活动,比如进程的创建、执行、休眠、唤醒和退出等
sched_process_exec
- 表示在调度子系统中的进程执行事件
("tp/sched/sched_process_exec")
- 当一个进程开始执行时,内核会触发这个tracepoint,从而允许BPF程序捕获并处理相关事件
trace_event_raw_sched_process_exec
用于表示跟踪事件的上下文,结构体包含了与进程执行相关的各种信息
- 例如进程的 PID、父进程的 PID、进程的命令名称等
- 通过这个参数,可以获取事件中的各种字段,从而进行进一步的分析、记录或处理
- 可以把它当做pt_regs结构体指针一样的作用(之前在kprobe中使用的,用于提供寄存器的值)
event
用于描述特定事件的数据结构
- 包含事件相关的各种信息
- 例如进程/线程标识符,退出码,运行时间,进程名,文件名,事件类型(启动/退出)
bpf_ktime_get_ns()
在内核中获取当前时间的纳秒级别时间戳
bpf_map_update_elem()
用于在内核层代码中更新或插入一个键值对到一个特定的 eBPF map 中
int bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)
bpf_ringbuf_reserve()
从一个环形缓冲区中预留空间以存储事件数据
int bpf_ringbuf_reserve(struct ringbuf *ringbuf, u64 size, u64 flags)
- flags的值:
bpf_get_current_task()
获取当前正在执行的任务的tast_struct指针
BPF_CORE_READ()
表示正在进行核心级别的读取操作
bpf_get_current_comm()
是 eBPF 的一个 helper 函数,用于获取当前任务(进程)的命令名称
char *bpf_get_current_comm(void *buf, int size_of_buf);
fname_off = ctx->__data_loc_filename & 0xFFFF
ctx指向的结构体里存放了文件名的偏移量,表示文件名在 eBPF 上下文结构体中的位置
- 这里只保留低 16 位
bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off)
用于从内核空间复制一个以 null 结尾的字符串到用户空间
int bpf_probe_read_str(void *dst, u32 size, const void *unsafe_ptr);
- 如果字符串的长度超过了size,只会复制size-1个字符
bpf_ringbuf_submit(e, 0)
用于将先前预留的事件数据提交到环形缓冲区中(也就是提交到用户空间),以便进行后续处理
int bpf_ringbuf_submit(void *data,u64 flags);
- flags的值:
源码(handle_exit)
SEC("tp/sched/sched_process_exit") int handle_exit(struct trace_event_raw_sched_process_template *ctx) //当一个进程退出时,内核会触发此事件。该函数会计算进程的生命周期持续时间,并将相关信息放入环形缓冲区中 { struct task_struct *task; struct event *e; pid_t pid, tid; u64 id, ts, *start_ts, duration_ns = 0; /* get PID and TID of exiting thread/process */ id = bpf_get_current_pid_tgid(); pid = id >> 32; tid = (u32)id; /* ignore thread exits */ if (pid != tid) return 0; /* if we recorded start of the process, calculate lifetime duration */ start_ts = bpf_map_lookup_elem(&exec_start, &pid); if (start_ts) duration_ns = bpf_ktime_get_ns() - *start_ts; else if (min_duration_ns) return 0; bpf_map_delete_elem(&exec_start, &pid); /* if process didn't live long enough, return early */ if (min_duration_ns && duration_ns < min_duration_ns) return 0; /* reserve sample from BPF ringbuf */ e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); if (!e) return 0; /* fill out the sample with data */ task = (struct task_struct *)bpf_get_current_task(); e->exit_event = true; e->duration_ns = duration_ns; e->pid = pid; e->ppid = BPF_CORE_READ(task, real_parent, tgid); e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff; bpf_get_current_comm(&e->comm, sizeof(e->comm)); /* send data to user-space for post-processing */ bpf_ringbuf_submit(e, 0); return 0; }
和上面那个类似,只不过是在进程退出时才会触发该事件
- 退出前会将运行时间+其他相关信息打包成event事件,发送到环形缓冲区
语法
SEC("tp/sched/sched_process_exit")
声明要捕获 Linux 内核中进程退出时的相关信息
- 当一个进程退出时,内核会触发这个tracepoint,从而允许BPF程序捕获并处理相关事件
bpf_map_lookup_elem()
在内核层代码中,从一个 eBPF map 中查找与给定键关联的值
void *bpf_map_lookup_elem(void *map, const void *key);
- 通过提供的map指针和key值指针,可以拿到对应的value值
duration_ns = bpf_ktime_get_ns() - *start_ts
- 计算从进程开始执行到当前时间点的持续时间
- bpf_ktime_get_ns获取当前时间的纳秒级别的时间戳,用该值-开始时间,即为持续时间
bpf_map_delete_elem()
从一个 eBPF map 中删除指定的键值对
int bpf_map_delete_elem(void *map, const void *key);
- 因为进程已经退出了,之前在map中记录的进程启动时间就没有用了,所以要删掉
e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
提取进程的退出码
- --父子进程之间的等待(wait和waitpid的介绍+原理),status的介绍+恢复退出码(位运算+宏),非阻塞等待(宏),signal查看_父进程wait和waitpid-CSDN博客
- 因为退出码实际上是在低16位中的高8位,所以需要右移8位,来提取退出码
.c
源码
/ SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) /* Copyright (c) 2020 Facebook */ #include <argp.h> //解析命令行参数 #include <signal.h> #include <stdio.h> #include <time.h> #include <sys/resource.h> #include <bpf/libbpf.h> #include "bootstrap.h" #include "bootstrap.skel.h" static struct env { //环境变量 bool verbose; //是否启用详细输出 long min_duration_ms; //最小持续时间 } env; const char *argp_program_version = "bootstrap 0.0"; //版本号 const char *argp_program_bug_address = "<bpf@vger.kernel.org>"; //存储程序的Bug报告地址 const char argp_program_doc[] = "BPF bootstrap demo application.\n" //存储程序的文档字符串,包括程序的简要说明和用法 "\n" "It traces process start and exits and shows associated \n" "information (filename, process duration, PID and PPID, etc).\n" "\n" "USAGE: ./bootstrap [-d <min-duration-ms>] [-v]\n"; static const struct argp_option opts[] = { //用于存储命令行参数的选项信息 { "verbose", 'v', NULL, 0, "Verbose debug output" }, { "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" }, {}, }; static error_t parse_arg(int key, char *arg, struct argp_state *state) //解析命令行参数 { switch (key) { case 'v': env.verbose = true; break; case 'd': errno = 0; env.min_duration_ms = strtol(arg, NULL, 10); if (errno || env.min_duration_ms <= 0) { fprintf(stderr, "Invalid duration: %s\n", arg); argp_usage(state); } break; case ARGP_KEY_ARG: argp_usage(state); break; default: return ARGP_ERR_UNKNOWN; } return 0; } static const struct argp argp = { //包含了命令行参数的选项信息、解析函数和程序文档 .options = opts, .parser = parse_arg, .doc = argp_program_doc, }; static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) { if (level == LIBBPF_DEBUG && !env.verbose) return 0; return vfprintf(stderr, format, args); } static volatile bool exiting = false; static void sig_handler(int sig) { exiting = true; } static int handle_event(void *ctx, void *data, size_t data_sz) //处理跟踪到的进程事件,并打印相关信息 { const struct event *e = data; struct tm *tm; char ts[32]; time_t t; time(&t); tm = localtime(&t); strftime(ts, sizeof(ts), "%H:%M:%S", tm); if (e->exit_event) { printf("%-8s %-5s %-16s %-7d %-7d [%u]", ts, "EXIT", e->comm, e->pid, e->ppid, e->exit_code); if (e->duration_ns) printf(" (%llums)", e->duration_ns / 1000000); printf("\n"); } else { printf("%-8s %-5s %-16s %-7d %-7d %s\n", ts, "EXEC", e->comm, e->pid, e->ppid, e->filename); } return 0; } int main(int argc, char **argv) { struct ring_buffer *rb = NULL; struct bootstrap_bpf *skel; int err; /* Parse command line arguments */ err = argp_parse(&argp, argc, argv, 0, NULL, NULL); if (err) return err; /* Set up libbpf errors and debug info callback */ libbpf_set_print(libbpf_print_fn); /* Cleaner handling of Ctrl-C */ signal(SIGINT, sig_handler); signal(SIGTERM, sig_handler); /* Load and verify BPF application */ skel = bootstrap_bpf__open(); if (!skel) { fprintf(stderr, "Failed to open and load BPF skeleton\n"); return 1; } /* Parameterize BPF code with minimum duration parameter */ skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL; //设置了 BPF 程序的参数,即最小持续时间,单位为纳秒 /* Load & verify BPF programs */ err = bootstrap_bpf__load(skel); if (err) { fprintf(stderr, "Failed to load and verify BPF skeleton\n"); goto cleanup; } /* Attach tracepoints */ err = bootstrap_bpf__attach(skel); if (err) { fprintf(stderr, "Failed to attach BPF skeleton\n"); goto cleanup; } /* Set up ring buffer polling */ rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL); //创建一个环形缓冲区,并设置好缓冲区的回调函数 if (!rb) { err = -1; fprintf(stderr, "Failed to create ring buffer\n"); goto cleanup; } /* Process events */ printf("%-8s %-5s %-16s %-7s %-7s %s\n", "TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE"); while (!exiting) { //不断地轮询环形缓冲区以获取事件 err = ring_buffer__poll(rb, 100 /* timeout, ms */); /* Ctrl-C will cause -EINTR */ if (err == -EINTR) { err = 0; break; } if (err < 0) { printf("Error polling perf buffer: %d\n", err); break; } } cleanup: /* Clean up */ ring_buffer__free(rb); bootstrap_bpf__destroy(skel); return err < 0 ? -err : 0; }
语法
strtol()
字符串转换为长整型数值
long strtol(const char *nptr, char **endptr, int base);
argp_usage()
输出标准的用法消息
* Possibly output the standard usage message for ARGP to stderr and exit. */ extern void argp_usage (const struct argp_state *__state); extern void __argp_usage (const struct argp_state *__state);
struct argp
是 GNU Argp 参数解析库中的一个结构体,用于定义参数解析的规则和选项
LIBBPF_DEBUG
是 libbpf 库中定义的一个枚举常量,用于表示调试级别的日志输出
struct tm
用于表示日期和时间的各个组成部分
struct tm { int tm_sec; /* Seconds. [0-60] (1 leap second) */ int tm_min; /* Minutes. [0-59] */ int tm_hour; /* Hours. [0-23] */ int tm_mday; /* Day. [1-31] */ int tm_mon; /* Month. [0-11] */ int tm_year; /* Year - 1900. */ int tm_wday; /* Day of week. [0-6] */ int tm_yday; /* Days in year.[0-365] */ int tm_isdst; /* DST. [-1/0/1]*/ # ifdef __USE_MISC long int tm_gmtoff; /* Seconds east of UTC. */ const char *tm_zone; /* Timezone abbreviation. */ # else long int __tm_gmtoff; /* Seconds east of UTC. */ const char *__tm_zone; /* Timezone abbreviation. */ # endif };
localtime()
将一个时间戳转换为本地时间,并将结果存储在tm结构体中
struct tm *localtime(const time_t *timep);
strftime()
将tm结构体中的时间信息格式化为字符串,并将结果存储在指定缓冲区
size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr);
ring_buffer系列函数
- 其中,__new用于创建并初始化一个新的环形缓冲区
- 后面两个参数可以设置为null
skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL
将环形缓冲区的只读数据(rodata)的最小持续时间赋值
- 是环境变量中的最小持续时间(也就是-d选项设置的数值)由毫秒转换为纳秒
ring_buffer__poll(rb, 100 /* timeout, ms */)
在环形缓冲区上进行轮询,等待有新的数据可用