当前位置: 首页 > news >正文

淄博网站设计策划方案维护wordpress评论翻页

淄博网站设计策划方案维护,wordpress评论翻页,网页版qq注册,网上找工程项目怎么找Linux 存在众多 tracing tools#xff0c;比如 ftrace、perf#xff0c;他们可用于内核的调试、提高内核的可观测性。众多的工具也意味着繁杂的概念#xff0c;诸如 tracepoint、trace events、kprobe、eBPF 等#xff0c;甚至让人搞不清楚他们到底是干什么的。本文尝试理清…Linux 存在众多 tracing tools比如 ftrace、perf他们可用于内核的调试、提高内核的可观测性。众多的工具也意味着繁杂的概念诸如 tracepoint、trace events、kprobe、eBPF 等甚至让人搞不清楚他们到底是干什么的。本文尝试理清这些概念。 注入 Probe 的机制 Probe Handler 如果我们想要追踪内核的一个函数或者某一行代码查看执行的上下文和执行情况通用的做法是在代码或函数的执行前后 printk 打印日志然后通过日志来查看追踪信息。但是这种方式需要重新编译内核并重启非常麻烦。如果是在生产环境排查问题这种方式也是无法接受的。 一种比较合理的方式是在内核正常运行时自定义一个函数注入到我们想要追踪的内核函数执行前后当内核函数执行时触发我们定义的函数我们在函数中实现获取我们想要的上下文信息并保存下来。同时因为增加了内核函数的执行流程我们定义的函数最好是需要的时候开启不需要的时候关闭避免对内核函数造成影响。 这个自定义的函数就是 probe handler注入 probe handler 的地方被称为探测点或者 Hook 点在探测点前执行的 probe handler 叫 pre handler, 执行后的叫 post handler注入 probe handler 的方式被称为“插桩”内核提供了多种 probe handler 注入机制。接下来我们聊一聊他们是如何实现在内核运行时注入 probe handler。 Kprobes 机制 Kprobes 是一个动态 tracing 机制能够动态的注入到内核的任意函数中的任意地方采集调试信息和性能信息并且不影响内核的运行。Kprobes 有两种类型kprobes、kretprobes。kprobes 用于在内核函数的任意位置注入 probe handlerkretprobes 用于在函数返回位置注入 probe handler。出于安全性考虑在内核代码中并非所有的函数都能“插桩”kprobe 维护了一个黑名单记录了不允许插桩的的函数比如 kprobe 自身防止递归调用。 kprobes 机制如何实现注入 probe handler 内核提供了一个 krpobe 注册接口当我们调用接口注册一个 kprobe 在指定探测点注入 probe handler 时内核会把探测点对应的指令复制一份记录下来并且把探测点的指令的首字节替换为「断点」指令在 x86 平台上也就是 int3 指令。 cpu 执行断点指令时会触发内核的断点处理函数「do_int3」它判断是否为 kprobe 引起的断点如果是 kprobe 机制触发的断点会保存这个程序的状态比如寄存器、堆栈等信息并通过 Linux 的「notifier_call_chain」机制将 cpu 的使用权交给之前 kprobe 的 probe handler同时会把内核所保存的寄存器、堆栈信息传递给 probe handler。 前面已经提到了probe handler 分两种类型一种是 pre handler、一种是 post handler。pre handler 将首先被调用如果有的话pre handler 执行完成后内核会将 cpu 的 flag 寄存器的值设置为 1开始单步执行原指令单步执行是 cpu 的一个 debug 特性当 cpu 执行完一个指令后便会产生一个 int1 异常触发中断处理函数「do_debug」执行do_debug 函数会检查本次中断是否为 kprobe 引起如果是的话执行 post handler执行完毕后关闭单步恢复原始执行流。 kretprobe 探针很有意思Kprobe 会在函数的入口处注册一个 kprobe当函数执行时这个 krpobe 会把函数的返回地址暂存下来并把它替换为 trampoline 地址。 Kprobe 也会在 trampoline 注册一个 kprobe函数执行返回时cpu 控制权转移到 trampoline此时又会触发 trampoline 上的 kprobe 探针继续陷入中断并执行 probe handler。 为什么有了 kprobe 还需要 kretprobe Kprobe 在可以函数的任意位置插入 probe理论上他也能实现 kretprobe 的功能但是实际上会面临几个挑战。 比如当我们在函数的最后一行代码上注入探针试图使用 kprobe 实现 kretprobe 的效果但是实际上这种方式并不好函数可能会存在多个返回情况比如不满足 if 条件发生异常等情况此时代码完全有可能不会执行最后一行代码而是在某个地方就返回了也就意味着不会触发探针执行。 kretprobe 的优势就在于它可以稳定的在函数返回时触发 probe handler 执行无论函数是基于什么情况下返回。 另外一方面 kprobe 虽然可以在函数的任意位置插入探针但是实际情况下都是在函数入口处插入探针因为函数入口是有一条标准的指令序列 prologue 可以进行断点替换而函数内部的其他位置可能会存在跳转指令、循环指令等情况指令序列不太规则不方便做断点替换。 Uprobes Uprobes 也分为 uprobes 和 uretprobes和 Kprobes 从原理上来说基本上是类似的通过断点指令替换原指令实现注入 probe handler 的能力并且他没有 Kprobes 的黑名单限制。Uprobes 需要我们提供「探测点的偏移量」探测点的偏移量是指从程序的起始虚拟内存地址到探测点指令的偏移量。我们可以通过一个简单的例子来理解 rootzfane-maxpower:~/traceing# cat hello.c #include stdio.h void test(){ printf(hello world); } int main() {test(); return 0; } rootzfane-maxpower:~/traceing# gcc hello.c -o hello 通过 readelf 读取程序的 ELF 信息拿到程序的符号表、节表。符号表包含程序中所有的符号例如全局变量、局部变量、函数、动态链接库符号以及符号对应的虚拟内存地址。 汇编语言是按照节来编写程序的例如.text 节、.data 节。每个节都包含程序中的特定数据或代码节表就是程序中各个节的信息表。 通过符号表可以拿到 hello 函数的虚拟内存地址通过节表拿到.text 节的虚拟内存地址以及.text 节相较于 ELF 起始地址的偏移量。 rootzfane-maxpower:~/traceing# readelf -s hello|grep test 36: 0000000000001149 31 FUNC GLOBAL DEFAULT 16 test rootzfane-maxpower:~/traceing# readelf -S hello|grep .text[16] .text PROGBITS 0000000000001060 00001060 那么 test 函数的指令在 hello 二进制文件的偏移量就可以计算出来了。 offsettest 函数的虚拟地址 - .text 段的虚拟地址 .text 端偏移量 offset 0000000000001149 - 0000000000001060 00001060 offset 0000000000001149 现在我们可以通过编写内核模块向二进制程序注入 probe handler 获取数据了。 #include linux/kernel.h #include linux/init.h #include linux/module.h #include linux/fs.h #include linux/uprobes.h #include linux/namei.h #include linux/string.h #include linux/uaccess.h#define DEBUGGEE_FILE /home/zfane/hello/hello #define DEBUGGEE_FILE_OFFSET (0x1149) static struct inode *debuggee_inode;static int uprobe_sample_handler(struct uprobe_consumer *con,struct pt_regs *regs) {printk(handler is executed, arg0: %s\\n,regs-di);return 0; }static int uprobe_sample_ret_handler(struct uprobe_consumer *con, unsigned long func,struct pt_regs *regs) {printk(ret_handler is executed\\n); return 0; }static struct uprobe_consumer uc {.handler uprobe_sample_handler,.ret_handler uprobe_sample_ret_handler };static int __init init_uprobe_sample(void) { int ret; struct path path;ret kern_path(DEBUGGEE_FILE, LOOKUP_FOLLOW, path); if (ret) { return -1;}debuggee_inode igrab(path.dentry-d_inode);path_put(path);ret uprobe_register(debuggee_inode,DEBUGGEE_FILE_OFFSET, uc); if (ret 0) { return -1;}printk(KERN_INFO insmod uprobe_sample\\n); return 0; }static void __exit exit_uprobe_sample(void) {uprobe_unregister(debuggee_inode,DEBUGGEE_FILE_OFFSET, uc);printk(KERN_INFO rmmod uprobe_sample\\n); }module_init(init_uprobe_sample); module_exit(exit_uprobe_sample);MODULE_LICENSE(GPL); Tracepoint Tracepoint 是一个静态的 tracing 机制开发者在内核的代码里的固定位置声明了一些 Hook 点通过这些 hook 点实现相应的追踪代码插入一个 Hook 点被称为一个 tracepoint。 tracepoint 有开启和关闭两种状态默认处于关闭状态对内核产生的影响非常小只是增加了极少的时间开销一个分支条件判断极小的空间开销一条函数调用语句和几个数据结构。 在 x86 环境下内核代码编译后关闭状态的 tracepoint 代码对应的 cpu 指令是nop 指令 启用 tracepoint 时通过 Linux 内核提供的 static jump patch 静态跳转补丁机制nop 指令会被替换为 jmp 指令jmp 指令将 cpu 的使用权转移给 static_call 静态跳转函数这个函数会遍历 tracepoint probe handler 数组获取当前 tracepoint 注册的 probe handler并进一步跳转到 probe handler 执行probe handler 执行完成后再通过 jmp 指令跳转回原函数继续执行。 #include linux/module.h #include linux/ftrace.h #include linux/tracepoint.h #include linux/proc_fs.h #include linux/seq_file.h #include linux/hashtable.h #include linux/slab.h #include linux/time.h #include linux/percpu.h #include trace/events/sched.hstatic void probe_sched_switch(void *ignore, bool preempt,struct task_struct *prev, struct task_struct *next, unsigned int prev_state) {pr_info(probe_sched_switch: pid [%d] - [%d] \\n,prev-tgid, next-tgid); }struct tracepoints_table { const char *name; void *fct; struct tracepoint *value; char init; };struct tracepoints_table interests[] {{.name sched_switch, .fct probe_sched_switch}};#define FOR_EACH_INTEREST(i) \\for (i 0; i sizeof(interests) / sizeof(struct tracepoints_table); i)static void lookup_tracepoints(struct tracepoint *tp, void *ignore) { int i;FOR_EACH_INTEREST(i) { if (strcmp(interests[i].name, tp-name) 0) interests[i].value tp;} }static void cleanup(void) { int i;// Cleanup the tracepointsFOR_EACH_INTEREST(i) { if (interests[i].init) {tracepoint_probe_unregister(interests[i].value, interests[i].fct,NULL);}} }static void __exit tracepoint_exit(void) { cleanup(); }static int __init tracepoint_init(void) { int i; // Install the tracepointsfor_each_kernel_tracepoint(lookup_tracepoints, NULL);FOR_EACH_INTEREST(i) { if (interests[i].value NULL) {printk(Error, %s not found\\n, interests[i].name);cleanup(); return 1;}tracepoint_probe_register(interests[i].value, interests[i].fct, NULL);interests[i].init 1;}return 0; }module_init(tracepoint_init) module_exit(tracepoint_exit) MODULE_LICENSE(GPL); 资料直通车Linux内核源码技术学习路线视频教程内核源码 学习直通车Linux内核源码内存调优文件系统进程管理设备驱动/网络协议栈 通过追踪工具来注入 Probe Event Tracing 在前面的代码示例中我们需要通过编写 kernel module 的方式注册 probe handler看上去非常简单但在实际开发的过程当中编写内核模块是一个很大的挑战如果内核模块的代码写的有问题会直接导致内核 crash在生产环境上使用内核模块需要谨慎考虑。 Linux 内核为此提供了一个不需要编写内核模块就能使用 tracepoint 的机制event tracing。他抽象出了如下概念 TraceEvent事件是在程序执行过程中发生的特定事情例如函数调用、系统调用或硬件中断。事件被描述为一个有限的结构包含有关事件的元数据和数据。每个事件都有一个唯一的标识符和名称。Event Provider事件提供程序是一个模块或应用程序用于在事件跟踪系统中注册和定义事件。事件提供程序负责确定事件的格式和语义并将事件发送到跟踪缓冲区。Event Consumer事件消费者是从事件跟踪缓冲区中读取事件的进程或应用程序。事件消费者可以将事件输出到文件、控制台或通过网络发送到远程主机。Event Tracing Session事件跟踪会话是一个包含多个事件提供程序和事件消费者的 ETI 实例。在一个事件跟踪会话中可以收集多个事件源的事件数据并将其聚合到单个跟踪缓冲区中。Trace Buffer跟踪缓冲区是一个在内核中分配的内存区域用于存储事件数据。事件提供程序将事件写入跟踪缓冲区事件消费者从跟踪缓冲区读取事件数据。Trace Event Format (TEF)跟踪事件格式是一个描述事件数据布局和语义的模板。它指定事件的名称、参数和字段以及每个字段的大小和类型。在 ETI 中跟踪事件格式可以由事件提供程序静态定义或动态生成。Trace Event Id (TEID)跟踪事件 ID 是唯一标识一个跟踪事件的整数值。每个事件提供程序都有自己的 TEID 命名空间它们使用不同的整数值来标识它们的事件。在内核代码中包含 tracepoint 代码的函数就可以理解为是一个 event providerevent provider 通过在 tracepoint 上注册一个 probe handler。当这个函数执行到 tracepoint 时触发 probe handler 执行它会构建一个 TraceEvent。内核代码中已经有了专门用于构建 trace event 的 probe handler无需我们自己注入了。 TraceEvent 会包含当前函数的上下文和参数probe handler 会将 event 保存至在 Trace Buffer 中接下来对于事件的分析、处理操作可以放在用户态执行通过系统调用从 Trace Buffer 中读取 event或者直接通过 mmap 直接将 Trace Buffer 映射到用户态的内存空间读取 event。 我们现在可以这样使用 tracepoint 查看当前内核支持的 event。 cat /sys/kernel/debug/tracing/available_events 启用 syscalls:sys_enter_connect 这个事件。 echo 1 /sys/kernel/debug/tracing/events/syscalls/sys_enter_connect/enable 查看事件数据。 rootzfane-powerpc:~# cat /sys/kernel/debug/tracing/trace # tracer: nop # # entries-in-buffer/entries-written: 195/195 #P:16 # # _----- irqs-off/BH-disabled # / _---- need-resched # | / _--- hardirq/softirq # || / _-- preempt-depth # ||| / _- migrate-disable # |||| / delay # TASK-PID CPU# ||||| TIMESTAMP FUNCTION # | | | ||||| | |sd-resolve-809 [001] ..... 1401.623886: sys_connect(fd: c, uservaddr: 7f618b836d8c, addrlen: 10)sd-resolve-809 [001] ..... 1411.634396: sys_connect(fd: c, uservaddr: 7f618b836d8c, addrlen: 10)systemd-resolve-793 [001] ..... 1411.634827: sys_connect(fd: 14, uservaddr: 7ffe2e97d050, addrlen: 10)systemd-resolve-793 [001] ..... 1411.634967: sys_connect(fd: 13, uservaddr: 7ffe2e97d000, addrlen: 10)sd-resolve-809 [001] ..... 1421.645348: sys_connect(fd: c, uservaddr: 7f618b836d8c, addrlen: 10)rsyslogd-848 [002] ..... 1426.678287: sys_connect(fd: 6, uservaddr: 7f3be1fb3bc0, addrlen: 6e)sd-resolve-809 [001] ..... 1431.655820: sys_connect(fd: c, uservaddr: 7f618b836d8c, addrlen: 10)systemd-resolve-793 [001] ..... 1436.661514: sys_connect(fd: 13, uservaddr: 7ffe2e97d050, addrlen: 10)systemd-resolve-793 [001] ..... 1436.661679: sys_connect(fd: 14, uservaddr: 7ffe2e97d000, addrlen: 10)rsyslogd-848 [009] ..... 1436.677930: sys_connect(fd: 6, uservaddr: 7f3be1fb3bc0, addrlen: 6e)rsyslogd-848 [009] ..... 1436.686721: sys_connect(fd: 6, uservaddr: 7f3be1fb3bc0, addrlen: 6e)sd-resolve-809 [001] ..... 1441.666368: sys_connect(fd: c, uservaddr: 7f618b836d8c, addrlen: 10)systemd-resolve-793 [001] ..... 1451.675741: sys_connect(fd: 13, uservaddr: 7ffe2e97d050, addrlen: 10)sd-resolve-809 [000] ..... 1451.675874: sys_connect(fd: c, uservaddr: 7f618b836d8c, addrlen: 10) 在这个示例中我们只是查看了 sys_enter_connect 这个 trace event没有做进一步的分析和处理操作在后面我们可以借助一些工具消费 trace event。 基于 tracepoint 的 Trace Event 虽然解决了 tracepoint 的 probe handler 注册需要编写内核模块才能使用的问题但任然有 2 个问题没有解决 并非所有的内核函数都有 Tracepoint即使有某个内核函数有 Tracepoint如果内核开发者没有为这个 Tracepoint 实现构建 Event 和保存 Event 到 Trace Buffer 的逻辑同样也没有办法获取 Trace 信息。内核开发者需要编写代码将 trace 信息保存到 Trace Buffer作为内核的用户我们只能看到内核开发者想让我们看到的数据根据前面提到的 trace event 的实现原理event 就是 probe handler 构建的那么如果我们在 kprobe 的 probe handler 中实现构建一个 event 并保存的逻辑不就能实现一个基于 kprobe 的 Trace Event 吗Event Trace 已经支持了这样的骚操作下面是 Linux 内核给出的示例添加基于 kprobe、kretprobe 的 event。 echo p:myprobe do_sys_open dfd%ax filename%dx flags%cx mode4($stack) /sys/kernel/tracing/kprobe_events 他的语法格式按照如下约定 p[:[GRP/]EVENT] [MOD:]SYM[offs]|MEMADDR [FETCHARGS] : Set a probe r[MAXACTIVE][:[GRP/]EVENT] [MOD:]SYM[0] [FETCHARGS] : Set a return probe p:[GRP/]EVENT] [MOD:]SYM[0]%return [FETCHARGS] : Set a return probe -:[GRP/]EVENT : Clear a probe [GRP/][EVENT] 定义一个 event[MOD:]SYM[offs]|MEMADDR, 定义一个 kprobe。[FETCHARGS] 是设置参数的类型。在上面的示例中为什么往这个文件里写入一些文本就可以实现 kprobe 的 probe handler 的能力这主要依赖于 TraceFS 文件系统。 Tracefs 是什么 TraceFS 是 Linux 内核提供的一个虚拟文件系统他提供了一组文件和目录用户可以通过读写这些文件和目录来与内核中的跟踪工具交互。 以 kprobe_event 为例krpobe_event 在 tracefs 文件系统中注册了一个回调函数 init_kprobe_trace在挂载 tracefs 文件系统时执行他会创建 kprobe_events 文件并注册对这个文件的读写操作监听。 static const struct file_operations kprobe_events_ops {.owner THIS_MODULE,.open probes_open,.read seq_read,.llseek seq_lseek,.release seq_release,.write probes_write, };/* Make a tracefs interface for controlling probe points */ static __init int init_kprobe_trace(void) { struct dentry *d_tracer; struct dentry *entry;if (register_module_notifier(trace_kprobe_module_nb)) return -EINVAL;d_tracer tracing_init_dentry(); if (IS_ERR(d_tracer)) return 0;entry tracefs_create_file(kprobe_events, 0644, d_tracer, NULL, kprobe_events_ops);/* Event list interface */ if (!entry)pr_warning(Could not create tracefs kprobe_events entry\\n);/* Profile interface */entry tracefs_create_file(kprobe_profile, 0444, d_tracer, NULL, kprobe_profile_ops);if (!entry)pr_warning(Could not create tracefs kprobe_profile entry\\n); return 0; } fs_initcall(init_kprobe_trace); 当 kprobe_event 文件有写操作时便会触发create_trace_kprobe函数执行按照特定的语法解析 kprobe_event 文件内容创建一个 kprobe。 static ssize_t probes_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos) { return traceprobe_probes_write(file, buffer, count, ppos,create_trace_kprobe); } 在内核追踪技术的发展初期追踪相关的文件都放在 debugfs 虚拟文件系统中debugfs 主要设计目的是为了提供一个通用的内核调试接口内核的任意子系统都有可能使用 debugfs 做调试所以很多人出于安全考虑 debugfs 是不启用的这就导致无法使用内核的追踪能力tracefs 随之诞生了他会创建一个/sys/kernel/tracing目录但为了保证兼容性tracefs 仍然挂载在/sys/kernel/debug/tracing 下。如果没有启用 debugfstracefs 可以挂载在/sys/kernel/tracing。 随着 Linux 追踪技术的发展TraceFS 文件系统也成为了追踪系统的基础设施很多跟踪工具都使用 TraceFS 作为管理接口比如 Perf、LTTng 等。 Function Trace 前面提到的 event trace 机制与基于 tracefs 文件系统管理 event 的机制最初就是 Ftrace 的一部分能力现在已经成为 Linux 内核追踪系统的通用模块很多追踪工具也都依赖它。那么 Ftrace 是什么呢 Ftrace 有两层含义 为函数注入 probe handler 的函数跟踪的机制基于 trace fs 和 event trace 机制的 trace 框架。我们前面已经了解了 kprobes、tracepoint 两种注入 probe handler 的机制而 Ftrace 又带了一种新的实现方式编译时注入。 gcc 有一个编译选项-pg当使用这个编译选项编译代码时他会在每一个函数的入口添加对 mcount 函数的调用mcount 函数由 libc 提供它的实现会根据具体的机器架构生成相应的代码。一般情况下 mcount 函数会记录当前函数的地址、耗时等信息在程序执行结束后生成一个.out 文件用于给 gprof 来做性能分析的。我们可以编译一个 hello.c 文件查看汇编代码中包含了 mcount 调用。 rootzfane-maxpower:~/traceing# cat hello.c #include stdio.h void test(){ printf(hello world); } int main() {test(); return 0; } rootzfane-maxpower:~/traceing# gcc -pg -S hello.c rootzfane-maxpower:~/traceing# cat hello.s.file hello.c.text.section .rodata .LC0:.string hello world.text.globl test.type test, function test: .LFB0:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6 1: call *mcountGOTPCREL(%rip) // 在这个地方添加了 mcount 调用leaq .LC0(%rip), %raxmovq %rax, %rdimovl $0, %eaxcall printfPLTnoppopq %rbp.cfi_def_cfa 7, 8ret.cfi_endproc .LFE0:.size test, .-test.globl main.type main, function main: .LFB1:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6 1: call *mcountGOTPCREL(%rip) // 在这个地方添加了 mcount 调用movl $0, %eaxcall testmovl $0, %eaxpopq %rbp.cfi_def_cfa 7, 8ret.cfi_endproc .LFE1:.size main, .-main.ident GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0.section .note.GNU-stack,,progbits.section .note.gnu.property,a.align 8.long 1f - 0f.long 4f - 1f.long 5 0:.string GNU 1:.align 8.long 0xc0000002.long 3f - 2f 2:.long 0x3 3:.align 8 4: 内核代码的编译是不依赖 libc 库而 ftrace 提供了一个 mcount 函数在这个函数中实现 probe handler 的能力如果所有的内核函数都在函数入口添加 mcount 调用运行时会对性能造成极大的影响我们之前介绍的 kprobes、tracepoint 都具备动态开启和关闭的能力尽可能的减少对内核的影响Ftrace 也不例外他具备动态开启某个函数的 probe handler 的能力其实现思路有一点特别。 内核编译时设置 -pg 的编译选项在汇编阶段生成.o 的目标文件再调用 ftrace 在内核代码包中放置的一个 Perl 脚本 Recordmcount.pl他会扫描每一个目标文件查找 mcount 函数调用的地址并记录到一个临时的.s 文件中一个目标文件对应一个.s 文件查找完成后将临时的.s 文件编译成.o 目标文件和原来的.o 文件链接到一起。 在编译过程的链接阶段vmlinux.lds.h 把所有的 mcount_loc 端的内容放在 vmlinux 的.init.data 端并声明了两个全局符号start_mcount_loc 和 __stop_mcount_loc 来开启和关闭 mcount 函数调用。 在内核启动阶段会调用 ftrace_init 函数在这个函数中根据记录的 mcount 函数偏移地址把所有的 mcount 函数调用对应的指令修改为 NOP 指令。ftrace_init 函数在 start_kernel 中调用比 kerne__init 还要先执行此时不会有任何内核代码执行修改指令不会有任何影响。 在对某个函数启用 ftrace probe handler会将 NOP 指令修改为对 ftrace probe handler 的调用即可和 kprobe trap 一样的原理找到需要被 trace 的函数函数的 mcount 调用是 NOP 指令把 NOP 指令的第一个字节改为 int 3也就是断点指令再把 NOP 指令调整为 probe handler 的地址。 在内核 4.19 版本提升了最低版本的 gcc 限制最低可允许 gcc 4.6 版本编译gcc 4.6 版本支持 -mfentry 编译参数使用 fentry 的特殊函数调用作为所有函数的第一条指令他可以替代 mcount 函数调用并且性能更好。 Ftrace 这种通过编译参数注入的 probe handler 非常好用编译完成后相当于各个内核函数都声明了 tracepoint在内核运行时可以动态打开和关闭。那我们能否可以只使用 Ftrace 的 probe handler 注入能力呢也是可以的他有一个新的名字叫 fprobe在 2022 年合入内核代码他是 ftrace 的包装器可以仅使用 ftrace 的函数追踪的功能。 #define pr_fmt(fmt) %s: fmt, __func__#include linux/kernel.h #include linux/module.h #include linux/fprobe.h #include linux/sched/debug.h #include linux/slab.h#define BACKTRACE_DEPTH 16 #define MAX_SYMBOL_LEN 4096 static struct fprobe sample_probe; static unsigned long nhit;static char symbol[MAX_SYMBOL_LEN] kernel_clone; module_param_string(symbol, symbol, sizeof(symbol), 0644); MODULE_PARM_DESC(symbol, Probed symbol(s), given by comma separated symbols or a wildcard pattern.);static char nosymbol[MAX_SYMBOL_LEN] ; module_param_string(nosymbol, nosymbol, sizeof(nosymbol), 0644); MODULE_PARM_DESC(nosymbol, Not-probed symbols, given by a wildcard pattern.);static bool stackdump true; module_param(stackdump, bool, 0644); MODULE_PARM_DESC(stackdump, Enable stackdump.);static bool use_trace false; module_param(use_trace, bool, 0644); MODULE_PARM_DESC(use_trace, Use trace_printk instead of printk. This is only for debugging.);static void show_backtrace(void) { unsigned long stacks[BACKTRACE_DEPTH]; unsigned int len;len stack_trace_save(stacks, BACKTRACE_DEPTH, 2);stack_trace_print(stacks, len, 24); }static void sample_entry_handler(struct fprobe *fp, unsigned long ip, struct pt_regs *regs) { if (use_trace) /** This is just an example, no kernel code should call* trace_printk() except when actively debugging.*/trace_printk(Enter %pS ip 0x%p\\n, (void *)ip, (void *)ip); elsepr_info(Enter %pS ip 0x%p\\n, (void *)ip, (void *)ip);nhit; if (stackdump)show_backtrace(); }static void sample_exit_handler(struct fprobe *fp, unsigned long ip, struct pt_regs *regs) { unsigned long rip instruction_pointer(regs);if (use_trace) /** This is just an example, no kernel code should call* trace_printk() except when actively debugging.*/trace_printk(Return from %pS ip 0x%p to rip 0x%p (%pS)\\n,(void *)ip, (void *)ip, (void *)rip, (void *)rip); elsepr_info(Return from %pS ip 0x%p to rip 0x%p (%pS)\\n,(void *)ip, (void *)ip, (void *)rip, (void *)rip);nhit; if (stackdump)show_backtrace(); }static int __init fprobe_init(void) { char *p, *symbuf NULL; const char **syms; int ret, count, i;sample_probe.entry_handler sample_entry_handler;sample_probe.exit_handler sample_exit_handler;if (strchr(symbol, *)) { /* filter based fprobe */ret register_fprobe(sample_probe, symbol,nosymbol[0] \\0 ? NULL : nosymbol); goto out;} else if (!strchr(symbol, ,)) {symbuf symbol;ret register_fprobe_syms(sample_probe, (const char **)symbuf, 1); goto out;}/* Comma separated symbols */symbuf kstrdup(symbol, GFP_KERNEL); if (!symbuf) return -ENOMEM;p symbuf;count 1; while ((p strchr(p, ,)) ! NULL)count;pr_info(%d symbols found\\n, count);syms kcalloc(count, sizeof(char *), GFP_KERNEL); if (!syms) {kfree(symbuf); return -ENOMEM;}p symbuf; for (i 0; i count; i)syms[i] strsep(p, ,);ret register_fprobe_syms(sample_probe, syms, count);kfree(syms);kfree(symbuf); out: if (ret 0)pr_err(register_fprobe failed, returned %d\\n, ret); elsepr_info(Planted fprobe at %s\\n, symbol);return ret; }static void __exit fprobe_exit(void) {unregister_fprobe(sample_probe);pr_info(fprobe at %s unregistered. %ld times hit, %ld times missed\\n,symbol, nhit, sample_probe.nmissed); }module_init(fprobe_init) module_exit(fprobe_exit) MODULE_LICENSE(GPL); 除了编写内核模块的方式能否通过 event trace 机制来使用呢答案是可以的需要使用最新版的内核才行fprobe 支持 event trace 是在 23 年 4 月份刚合并到内核里。 Perf Perf 是一个 Linux 下的性能分析工具的集合最初由英特尔公司的 Andi Kleen 开发于 2008 年首次发布。Perf 设计之初是为了解决英特尔处理器性能分析工具集Intel Performance Tuning Utilities在 Linux 上的移植问题而开发的它可以利用英特尔的硬件性能监视器Hardware Performance Monitoring来对 CPU 性能进行采样和分析。随着时间的推移Perf 逐渐成为了一个通用的性能分析工具也支持内核追踪。 有了前面提到的 Ftrace为什么 Perf 也要支持内核跟踪机制呢主要原因在于 perf 有着特殊的分析方式采样分析。采样的对象是 event以基于时间的采样方式为例他的大致流程是这样的每隔一段时间就在所有 CPU 上产生一个中断查看当前是哪个 pid哪个函数在执行并将 pid/func 构建成一个 event 做统计在采样结束后我们就能知道 CPU 大部分时间耗在哪个 pid/func 上。 除了上面提到的基于时间的采样perf 还支持如下采样方式 计数. 统计某个事件的发生次数。基于事件的采样. 每当发生的事件数达到特定的阈值时就会记录一个样本。基于指令的采样. 处理器跟踪按给定时间间隔出现的指令并对这些指令生成的事件采样。这样便可以跟踪各个指令并查看哪些是对性能至关重要的指令。最开始 perf 是仅支持由硬件产生的 Hardware event这种方式可以推广到各种事件比如 trace event 事件当这个事件发生的时候上来冒个头看看击中了谁然后算出分布我们就知道谁会引发特别多的那个事件了。 接下来我们看一下 perf 是如何使用 trace event。 我们可以通过 perf 命令设置一个 probe。 $ sudo perf probe -x /usr/lib/debug/boot/vmlinux-$(uname -r) -k do_sys_open 接下来通过 record 子命令 启用 Trace Event并将 trace 信息保存到 perf.data。 $ sudo perf record -e probe:do_sys_open -aR sleep 1 现在我们可以通过 report 子命令分析 trace 信息。 $ sudo perf report -i perf.data perf 采样拿到的 event 最终会被放到一个叫做 perf event 的数据结构里面因为 event 都是在内核态产生的采样时需要一个数据结构存储采集到的 event并在采样结束后将采集到的 event 从内核态发送到用户态来使用perf event 就是用来做这个事情的我们通常说的 perf 是指用户态的工具perf event 是内核态的数据结构。perf 工具通过系统调用 perf_event_open 来创建 perf event。 在内核中perf_event 结构体存储该事件的配置和运行状态。创建 perf event 时还会创建 perf event 对应的 ring buffer 用来存储 trac event 数据。perf 工具通过 perf_event_open 系统调用拿到 perf event 的 fd 后就可以通过 mmap 内存映射机制 将内核态的 ringbuffer 映射到用户态来访问最终 perf 将数据写到 perf.data 中以供后续分析。 Perf 使用 Trace Event Perf 工具是基于 Perf Event 这个数据结构来实现分析能力的当使用 Perf 添加 Trace Event 时内核会将追踪数据写到 perf event 对应的 ringbuffer。 还是以上面的 perf 使用案例为例。我们通过 perf probe 子命令添加一个 uprobe event在 TraceFS 中也可以看到 uprobe_event 的定义但处于禁用状态。 rootzfane-maxpower:~/traceing# perf probe -x /root/traceing/hello show_testtest Added new event:probe_hello:show_test (on test in /root/traceing/hello)You can now use it in all perf tools, such as:perf record -e probe_hello:show_test -aR sleep 1rootzfane-maxpower:~/traceing# cat /sys/kernel/tracing/uprobe_events p:probe_hello/show_test /root/traceing/hello:0x0000000000001169 rootzfane-maxpower:~/traceing# cat /sys/kernel/tracing/events/probe_hello/enable 0 rootzfane-maxpower:~/traceing# 同样是往 uprobe_events 文件中写 trace event definition为什么手动写就是往 Trace Buffer 里发送数据用 perf 写就是往 perf event ring buffer 发送数据呢 在使用 perf record 子命令采集数据时会通过 perf_event_open 创建 perf eventperf event 在初始化阶段扫描所有的 trace event, 检查是否存在与 perf event 关联的 uprobe_event找到对应的 uprobe event 事件后就可以启用 urpobe event 了。 uprobe event 启用时才会触发 uprobe 注册操作但是 perf event 不是通过 TraceFS 的 enable 文件来注册 uprobe event 的而是直接调用 uprobe event 注册接口uprobe event 注册接口有两种注册类型TRACE_REG_PERF_REGISTER、TRACE_REG_REGISTER。TRACE_REG_PERF_REGISTER 表示由 perf event 注册uprobe event 有一个 flag 属性 用于存储注册类型TRACE_REG_PERF_REGISTER 对应的 flag 值为 TP_FLAG_PROFILE其他的则是 TP_FLAG_TRACE。 uprobe event 的 probe handler 固定是 uprobe_dispatcher 函数uprobe_dispatcher 函数会根据 uprobe event 的 flag 属性来判断往哪个 ring buffer 里写追踪数据kprobe 也是同理。tracepoint 和它俩不一样用于声明 tracepoint 的 TRACE_EVENT 宏定义中包含了专门给 perf event 使用的 probe handler他会直接往 perf event 的 ringbuffer 中写数据。 为什么要有两套 Ring buffer Event Tracing 框架下内核中的追踪数据往 Ring Buffer 中写入我们可以通过 Tracefs 文件系统来访问 Ring Buffer为什么 perf 工具不直接使用这个 Ring Buffer 来获取追踪信息而是在内核中让 Trace Event 的追踪数据直接写入到 Perf Event 的 ring buffer 中。 其实主要原因就是 Ftrace 实现的 Ring Buffer 无法满足 Perf 的需要Perf 需要在 NMI 场景下也能往 Ring Buffer 中写入数据。 Non-Maskable Interrupt (NMI) 是一种中断信号它可以打破处理器的正常执行流程而且无法被忽略或屏蔽。一般来说NMI 通常用于紧急情况下的故障处理或者硬件监控等场景。NMI 信号通常是由硬件触发的例如内存错误、总线错误、电源故障等这些故障可能会导致系统崩溃或者停机。为了避免在故障发生后丢失重要的性能事件数据Perf 需要将这些数据尽可能快地写入 ring buffer 中以确保数据不会丢失这就要求 Ring Buffer 的实现上不可以有写竞争或可能导致死锁的情况。 很不凑巧的是Ftrace 的 Ring Buffer 在设计上使用了自旋锁来防止并发访问自旋锁会一直占用 CPU 资源直到锁可用在 NMI 的场景下如果 Ftrace 正在持有自旋锁NMI 中断处理程序就无法获取自旋锁可能会导致系统死锁或者卡死。 另外一点就是 NMI 场景下 RingBuffer 的访问一定要快处理器必须尽可能快地响应 NMI 中断信号任何慢速的操作都可能会导致系统的稳定性和性能受到影响。Ftrace Ring Buffer 也没有足够的快最终 Perf 的开发人员自行实现了一套新的 无锁 Ring Buffer。 通过编写 eBPF 代码来注入 probe 如何使用 eBPF 追踪内核 由于内核态和用户态的内存空间是隔离的他们的虚拟内存实现原理不同想要从内核态向用户态传递数据需要经过地址转换和数据拷贝比较耗时。而在分析网络数据包时如果所有的网络数据包都从内核态发到用户态带来的成本也更大很多时候我们都是只需一部分数据包就可以了所以最理想的方式是内核态有一个 Packet Filter 机制能够过滤我们不需要的数据包这样就大大减少了内核需要拷贝的数据。 早期 unix 系统也提供了 packet filter 机制提供了一个基于内存栈的虚拟机来对内核态的数据包做过滤计算比如 CMU/Stanford Packet FilterCSPF、NIT(Network Interface Tap) 等它们的性能不够好。tcpdump 的作者 Steve McCanne 和 Van Jacobson 在 BSD 操作系统上实现了一个全新架构的 Packet Filter 机制Berkeley Packet Filter (BPF)抛弃了之前基于内存栈虚拟机的设计改为基于寄存器的虚拟机号称性能比之前的 packet filter 机制快很多。同时可以在内核态接到 device interface 传过来的包时就进行 filter不需要的包直接丢弃不会多出任何无效 copy。凭借优秀的架构设计和性能表现BPF 被移植到了很多操作系统。 BPF 的作者发表了一篇论文 The BSD Packet Filter: A New Architecture for User-level Packet Capture 来详细描述了 BPF 的设计理念与实现思路感兴趣的可以看一下。 BSD 系统的 BPF 在被移植到 Linux 上后被称为 Linux Socket FilterLSF但是大家依然称呼它为 BPFBPF 在 Linux 内核最初也是提供 Packet filter 的能力用户态使用 BPF 字节码来定义过滤表达式然后传递给内核由内核虚拟机解释执行。 随着时间的推移Linux 内核开发者为 BPF 添加了更多的能力比如 Linux 3.0 版本增加 BPF JIT 编译器在 2014 年 Alexei Starovoitov 为 BPF 带来了一次革命性的更新将 BPF 扩展为一个通用的虚拟机也就是 eBPF。eBPF 不仅扩展了寄存器的数量引入了全新的 BPF 映射存储还在 4.x 内核中将原本单一的数据包过滤事件逐步扩展到了内核态函数、用户态函数、跟踪点、性能事件perf_events以及安全控制等。 话说回 Linux 追踪技术。eBPF 的影响也来到了内核追踪领域2015 年 eBPF 支持 kprobe、2016 年开始支持 tracepoint、perf event现在我们可以通过在 eBPF 虚拟机运行自定义的 probe handler 获取跟踪数据并通过 eBPF Map 共享到用户态来对跟踪数据做分析。相比于编写内核代码或是 ftrace、perf 灵活性大大增强。 eBPF 的本质是一个在内核态的虚拟机可以在虚拟机中执行简单代码一个完整的 eBPF 程序通常包含用户态和内核态两部分用户态程序通过 BPF 系统调用完成 eBPF 程序的加载、事件挂载以及映射创建和更新而内核态中的 eBPF 程序可以理解为我们的 probe handler用来获取追踪数据。 eBPF 程序根据其用途划分为多种类型在追踪方面有如下类型 BPF_PROG_TYPE_KPROBEBPF_PROG_TYPE_TRACEPOINTBPF_PROG_TYPE_PERF_EVENTBPF_PROG_TYPE_RAW_TRACEPOINTBPF_PROG_TYPE_RAW_TRACEPOINT_WRITEABLEBPF_PROG_TYPE_TRACING 从类型名称也能看出来对应类型的 eBPF 程序是如何实现追踪能力的比如 kprobes 类型的 eBPF 程序就是通过 kprobes 机制注入 probe handlerprobe handler 就是我们在内核态虚拟机中运行的 eBPF 代码。同时 eBPF 程序类型里面没有 UPROBE主要原因是因为 uprobes 和 kprobes 原理相同KPROBE 类型的 eBPF 程序也可以使用 uprobes。 那么 eBPF 是如何使用 kprobe、tracepoint 等机制将自己作为 probe handler 注入到内核函数中的 在前面的介绍里我们如果使用 kprobe 机制探测内核函数可以使用 register_kprobe 函数、event trace、perf event 方式来注册 probe handler。**eBPF 采用 perf event 将内核态程序做为 probe handler** 在 eBPF 用户态程序中可以通过 attach_kprobe 函数将内核态 eBPF 程序通过 kprobes 机制附加到某个内核函数中。attach_kprobe 函数会创建一个 perf event再将 eBPF 内核态程序附加到 perf event。每个 perf event 的 kprobe probe handler 都是 kprobe_dispatch 函数他会去 perf event 中获取注册在当前 perf event 的回调函数列表并依次执行同时将指向 perf ringbuffer 的指针的传递给 eBPF 程序eBPF 程序可以通过 libbpf 封装好的 PT_REGS_PARAMx 宏定义来获取缓冲区中的数据。 static int kprobe_dispatcher(struct kprobe *kp, struct pt_regs *regs) { struct trace_kprobe *tk container_of(kp, struct trace_kprobe, rp.kp); int ret 0;raw_cpu_inc(*tk-nhit);if (trace_probe_test_flag(tk-tp, TP_FLAG_TRACE))kprobe_trace_func(tk, regs); #ifdef CONFIG_PERF_EVENTS if (trace_probe_test_flag(tk-tp, TP_FLAG_PROFILE))ret kprobe_perf_func(tk, regs); // 调用 perf event 的 probe handler #endif return ret; }/* Kprobe profile handler */ static int kprobe_perf_func(struct trace_kprobe *tk, struct pt_regs *regs) { struct trace_event_call *call trace_probe_event_call(tk-tp); struct kprobe_trace_entry_head *entry; struct hlist_head *head; int size, __size, dsize; int rctx;if (bpf_prog_array_valid(call)) { unsigned long orig_ip instruction_pointer(regs); int ret;ret trace_call_bpf(call, regs); // 在这里调用 bpf 程序/** We need to check and see if we modified the pc of the* pt_regs, and if so return 1 so that we dont do the* single stepping.*/ if (orig_ip ! instruction_pointer(regs)) return 1; if (!ret) return 0;}head this_cpu_ptr(call-perf_events); if (hlist_empty(head)) return 0;dsize __get_data_size(tk-tp, regs);__size sizeof(*entry) tk-tp.size dsize;size ALIGN(__size sizeof(u32), sizeof(u64));size - sizeof(u32);entry perf_trace_buf_alloc(size, NULL, rctx); if (!entry) return 0;entry-ip (unsigned long)tk-rp.kp.addr; memset(entry[1], 0, dsize);store_trace_args(entry[1], tk-tp, regs, sizeof(*entry), dsize);perf_trace_buf_submit(entry, size, rctx, call-event.type, 1, regs,head, NULL); return 0; } 不论是 kprobes、tracepoint 类型的 eBPF 程序都是复用 perf event 来实现 probe handler 注入在某个内核版本eBPF 的负责人 Alex 提出了一个新的方式 Raw Tracepoint不需要依赖 perf eventeBPF 程序直接作为 probe handler 注册到 tracepoint 上。 从使用上来说tracepoint 类型的 eBPF 程序需要定义好 tracepoint 关联的函数的参数的数据结构这个可以在 TraceFS 中查看比如 sched_process_exec 这个 tracepoint。 rootzfane-maxpower:~# cat /sys/kernel/tracing/events/sched/sched_process_exec/format name: sched_process_exec ID: 311 format:field:unsigned short common_type; offset:0; size:2; signed:0;field:unsigned char common_flags; offset:2; size:1; signed:0;field:unsigned char common_preempt_count; offset:3; size:1; signed:0;field:int common_pid; offset:4; size:4; signed:1;field:__data_loc char[] filename; offset:8; size:4; signed:1;field:pid_t pid; offset:12; size:4; signed:1;field:pid_t old_pid; offset:16; size:4; signed:1;print fmt: filename%s pid%d old_pid%d, __get_str(filename), REC-pid, REC-old_pid tracepoint 定义好数据结构配合 bpf 辅助函数提取 tracepoint 传递过来的数据。 struct sched_process_exec_args{ // 声明数据结构 unsigned short common_type; unsigned char common_flags; unsigned char common_preempt_count; int common_pid; int __data_loc; pid_t pid; pid_t old_pid; };SEC(tracepoint/sched/sched_process_exec) int tracepoint_demo(struct sched_process_exec_args *ctx) { struct event *e;e bpf_ringbuf_reserve(events, sizeof(*e), 0); if (!e) { return 0;} unsigned short filename_offsetctx-__data_loc 0xFFFF; char *filename(char *)ctx filename_offset;bpf_core_read(e-filename,sizeof(e-filename),filename); // 通过辅助函数读取值e-pidbpf_get_current_pid_tgid() 32;bpf_get_current_comm(e-command,sizeof(e-command));bpf_ringbuf_submit(e, 0);return 0; }char _license[] SEC(license) GPL; eBPF 程序接受到的数据是由 perf probe 传递过来的。tracepoint 关联的函数的参数会写到 perf ringbuffer 缓冲区perf probe 会将指向缓冲区的指针传递给 eBPF 程序。tracepoint 关联的函数参数在缓冲区的布局如下 --------- | 8 bytes | hidden struct pt_regs * (inaccessible to bpf program) --------- | N bytes | static tracepoint fields defined in tracepoint/format (bpf readonly) --------- | dynamic | __dynamic_array bytes of tracepoint (inaccessible to bpf yet) --------- perf probe 传递了指向缓冲区的指针eBPF 也无法直接使用指针访问内存上的数据各个内核函数的参数不一样在不知道数据的类型、长度无法保证安全访问所以需要借助 bpf 辅助函数读取数据。 再说回 raw tracepoint 类型的 eBPF 程序从使用上来说它的函数参数结构体变成了 struct bpf_raw_tracepoint_args不在需要我们定义 tracepoint 关联的结构体了。SEC 声明也改成 raw_traceoint其他的在使用上和 tracepoint 类型的 eBPF 程序保持一致。 // include/trace/events/sched.h SEC(raw_tracepoint/sched_process_exec) int raw_tracepoint_demo(struct bpf_raw_tracepoint_args *ctx) {struct event *e;ebpf_ringbuf_reserve(events,sizeof(*e),0); if (!e) { return 0;}bpf_core_read(e-filename,sizeof(e-filename),ctx-args[0]);e-pidbpf_get_current_pid_tgid() 32;bpf_get_current_comm(e-command,sizeof(e-command));bpf_ringbuf_submit(e, 0); return 0; } raw_tracepoint 类型的 eBPF 程序相比于普通的 tracepoint 类型的 eBPF 程序核心的改变是直接附加在 tracepoint 上可以提供参数的“原始访问“。直接附加在 tracepoint 的意思是tracepoint 对应的函数执行时内核将直接调用 bpf 程序执行为此内核提供了 tracepoint 注册 bpf 程序的注册接口 bpf_raw_tracepoint_open。而参数的原始访问不好描述但可以对比 raw_tracepoint 和 tracepoint 参数传递方式来理解。 对于 tracepoint 类型 eBPF 程序是 perf event 在 ringbuffer 中分配一块内存空间然后内核会将函数的参数写到这个内存空间中perf probe 再把这个内存空间的地址传递给 eBPF 程序而原始访问则是直接把函数参数全部转换为 u64 类型得到一个数组并把数组传递给 eBPF 程序。更短的调用链和跳过参数处理相比于 tracepoint raw tracepoint 有更好的性能。 samples/bpf/test_overhead performance on 1 cpu:tracepoint base kprobebpf tracepointbpf raw_tracepointbpf task_rename 1.1M 769K 947K 1.0M urandom_read 789K 697K 750K 755K BTF-enabled raw_tracepoint 在内核 4.18 版本引入了 BTF BPF Type Format它用来描述 BPF prog 和 map 相关调试信息的 元数据格式后面 BTF 又进一步拓展成可描述 function info 和 line info。BTF 为 Struct 和 Union 类型提供了对应成员的 offset 信息并结合 Clang 的扩展主要是[__builtin_preserve_access_index(https://clang.llvm.org/docs/LanguageExtensions.html)和 BPF 加载器BPF Prog 就可以准确访问某个 Struct 或者 Union 类型的成员而不用担心重定位问题。 在内核 5.5 版本专门定义了一个 BPF_PROG_TYPE_TRACING 类型支持访问 BTF 信息率先支持的就是 raw_tracepoint不再需要辅助函数访问内存。 SEC(tp_btf/sched_process_exec) int BPF_PROG(sched_process_exec,struct task_struct *p, pid_t old_pid, struct linux_binprm *bprm) { struct event *e;e bpf_ringbuf_reserve(events,sizeof (*e),0); if (!e){ return 0;}bpf_printk(filename : %s,bprm-filename); // 直接访问bpf_core_read(e-filename,sizeof(e-filename),bprm-filename);e-pidbpf_get_current_pid_tgid() 32;bpf_get_current_comm(e-command,sizeof(e-command));bpf_ringbuf_submit(e, 0); return 0; } 内核函数与 BPF 程序的桥梁BPF Trampoline BPF_PROG_TYPE_TRACING 类型的 eBPF 程序通过不同的 Attach 类型可以实现不同的能力除了支持 raw_tracepoint attach 类型外还支持 FENTRY/FEXIT。FENTRY、FEXIT 已经是老朋友了在前面介绍 Ftrace 时就有提到过这俩是用于函数追踪的FENTRY 类似于 kprobe、FEXIT 类似于 kretprobe除了函数返回值FEXIT 还可以获取到函数的参数。 它们依赖 gcc 的 -pg -mentry 编译参数在每个函数入口添加 fentry 调用在不开启 fentry 时fentry 调用指令会被替换为 NOP 指令避免影响性能开启时 fentry 指令会被替换为 BPF Trampoline 函数调用指令在 BPF Trampoline 函数中会调用 eBPF 程序执行。 BPF Trampoline 是一个内核函数和 bpf 程序之间的一个桥梁它允许内核函数调用 BPF 程序当我们通过 Fentry 机制 attach 到某个内核函数时内核会为这个 eBPF 程序生成一个 BPF Trampoline 函数被追踪的内核函数的参数会被转换成 u64 数组存储到 Trampoline 函数栈中指向这个栈的指针又存储到 eBPF 程序可以访问的 R1 寄存器中再根据 BTF 信息BPF 程序可以直接访问内存了同样也不需要辅助函数来读取数据。 Fentry、FEXIT 这种基于 Trampoline 方式的 probe handler 注入方式没有额外的 kprobe、perf event 数据结构引入其开销成本非常小如果内核支持 FENTRY 机制函数追踪场景使用 FENTRY 代替 kprobes 有更好的性能。 eBPF 如何从内核态向用户态传递数据 BPF Map 是 eBPF 在用户态和内核态共享数据的方式在上面的示例中我特意使用了 BPF ringbuffer Map 从内核态向用户态传递数据它需要内核 5.8 及其以上的版本才可以使用。在此之前perf event Map 是事实上的标准通过 perf ring buffer 可以高效的在内核态与用户态之间传递数据。 但在实践中发现perf ring buffer 存在两个缺点内存浪费和数据乱序。 perf ring buffer 需要在每一个 cpu 上创建每一个 cpu 都有可能执行 BPF 代码产生的数据会存储到当前 CPU 的 perf ring buffer 上如果某个时刻执行的 BPF 程序可能会产生大量的数据perf ring buffer 空间满了的情况下就覆盖掉老数据造成一部分数据丢失但是大部分情况下不会产生很多的数据针对这种情况要么容忍数据丢失要么就每个 cpu 创建大容量的 perf ringbuffer防止突发的数据暴增但大部分时间空着。 同时每个 cpu 具有独立的 perf ring buffer可能会导致连续的追踪数据分布在不同的 perf ringbuffer 上比如追踪进程的生命周期 fork、exec、exiteBPF 程序在 3 个不同的 cpu 上执行用户态是通过轮询 cpu 上 perf ringbuffer 来接收数据的可能就会出现 exit 事件比 exec 事件先接收。 perf ringbuffer 这两个问题并非无解比如可以在构建一个跨 cpu 的全局计数器每一次往 perf ringbuffer 写入数据时带上序列号。在用户态聚合所有的 perf ringbuffer 上的数据时创建一个队列并根据序列号按序入队这样就可以保证事件的顺序这种方案总归是增加了用户态程序的复杂度和带来额外的成本。 为此社区内提出了一个新的 ring buffer 设计BPF ringbuffer它是一个跨 CPU 共享、MPSC 模型的 ringbuffer可以直接通过 mmap 机制映射到用户态访问 ringbuffer。对于低效率内存使用的问题由于是跨 cpu 共享的 ring buffer, 所以这个问题就不存在了对于数据乱序的问题每个事件被写入 bpf ringbuffer 时都会被分配一个唯一的 sequence number并且 sequence number 会递增。这样在读取 buffer 数据时可以根据 sequence number 来判断哪些事件先发生哪些事件后发生从而保证读取的数据是有序的。 应该选择哪个内核追踪技术 Brendan Gregg 博客中有一片文章讨论了选择哪个 trace 追踪工具发布于 2015 年我认为直到现在依然有帮助Choosing a Linux Tracer (2015)于我个人而言排查问题和检测性能时我会优先考虑 perf 系列的工具它可以帮助我获取追踪数据并快速的得到一个分析结果。如果构建一个常驻的内核追踪程序eBPF 是我的好帮手它具备可编程性可以让我在多个节点上按照期望的方式拿到追踪数据并汇总计算。 总 结 kprobes、uprobes、tracepoint、fprobe(fentry/fexit) 是注入 probe handler 调用的机制。kprobes、uprobes 通过动态指令替换实现在指令执行时调用 probe handler。 tracepoint 是代码里静态声明了 probe handler 的调用提供 probe handler 的注册接口内核开发者定义发给 probe handler 的追踪数据执行 tracepoint 时将追踪数据传递给 probe handler可以动态开启和关闭tracepoint 由内核开发者维护稳定性很好。 fprobe(fentry/fexit) 是通过在内核编译期间对函数添加第三方调用可以动态开启和关闭达到了类似于 tracepoint 的效果除了 frpobe eBPF 同样也可以实现 fentry/fexit 的机制他们都是通过 Trampoline 来跳转到 probe handler 执行。 probe handler 在内核态执行抓取到的追踪数据往往需要传递到用户态做分析使用perf_event、trace_event_ring_buffer、eBPF Map 是从内核态向用户态传递数据的方式。 perf_event 存储的追踪数据可以通过 MMAP 映射到用户态来访问。trace_event_ring_buffer 是通过虚拟文件系统 TraceFS 的方式暴露追踪数据。eBPF Map 有多种实现方式有基于 perf event 的、有基于系统调用的有基于 BPF ringbuffer 的。 原文作者极客重生
http://wiki.neutronadmin.com/news/287801/

相关文章:

  • 网站开发一般用哪些语言建设工程公司名字大全三个字
  • 莲花网站企业营销策划
  • dede网站迁移步骤做教程网站资源放哪里
  • 漳州建设局网站龙华营销型网站
  • 做数据图表网站centos 6.8 wordpress
  • 武进建设局网站首页网络营销主要干什么
  • 泉州企业免费建站网站设计公司排行榜
  • 网站建设维护升级模板网站和定制网站的区别是什么
  • 食品品牌网站策划九江市区
  • 网站的设计思路范文免费装修设计图app
  • 宜昌网站设计制作公司唐山做网站那家好
  • 2017年网站建设招标书响应式网站做mip
  • 唐山彩钢中企动力提供网站建设旅游网站网页设计方案
  • 做渠道的网站有哪些有网站开发经验怎么写简历
  • 直播视频下载济南优化哪家好
  • 西安市城乡建设档案馆网站3d打印网站开发
  • 利于seo的建站系统有哪些济南装饰行业网站建设
  • 接做网站单子昆明百度推广优化排名
  • 商城网站设计网站建设怎样设置动态背景
  • 网站开发 兼职项目儿童玩具网站建设实训报告
  • 九江网站建设多少钱百度 网站速度诊断
  • 西安建网站哪家好wordpress是php文件路径
  • 加盟网站建设服务网站建设关键的问题是
  • 河南企业网站排名优化企业建网站的目的
  • 城乡建设吧部网站将page转换为wordpress
  • 中山精品网站建设价位徐州做网站的哪个好
  • 培训网站系统建设个人做流量大的网站
  • 成都 网站原创做英文行程的网站
  • 宁波英文网站建设联系深圳网站制作公司
  • 龙岩网站建设要多久建筑建设行业网站