## Variable Monitor changelog ```log 11.9 多个变量监控支持 11.10 按照 pid 区分不同内核结构, 支持每个进程单独申请取消自己的监控. 11.13 用户接口 cancel_all_watch -> cancel_watch, 每个进程互不干扰. 11.28 完全重构,更新文档. 12.1 一个编译问题,添加说明. 12.5 编译问题补充 12.21 跟进更新,重新部分内容 12.27 测试工具更新 ``` ## 说明 监控 数值变量(给定 地址,长度), 达到设定条件打印 Task 信息(用户态堆栈/内核态堆栈/调用链信息). - 支持多进程/线程, 单个进程/线程 退出时,取消其注册的所有监控. - 同一个 Task(线程) 注册的所有监控中, 相同的定时间隔监控会分配到同一个定时器. - 一个定时器最多监控 32 个变量,全局最多 128 个定时器. - 以上数量限制定义在 `source/module/monitor_timer.h`. - `testcase/helloworld.c` 有测试到单进程 2049 个变量; - 用户态堆栈符号信息还原的准确度 取决于目标程序编译方式. - gcc 下 `-s` 会抹除符号信息,有可能导致无法将地址还原为正确符号信息. 文件结构 ```log ├── build // output ├── source // all source code │ ├── buffer // 模块与用户空间通信的缓冲区 │ ├── module // 模块代码 │ ├── uapi // 用户空间接口 │ ├── ucli // 用户空间命令行工具 │ └── ucli_py // 用户空间命令行 python (仅测试用,待完成) │ └── libunwind // python 解析堆栈信息移植库 ├── testcase // 测试用例 └── tools // 测试工具 ``` ## 使用 设定对变量监控有两种函数: 宏定义 或 定义 watch_arg 结构体 - 都需要添加 `source/uapi` 下的头文件 `#include "monitor_user.h"` 需要取消监控时调用 `cancel_watch();` variant_monitor 会取消该进程/线程 的所有监控. - 当进程/线程退出后,也会执行相同的操作,取消该进程/线程的 所有监控. - 因此调用 `cancel_watch();` 是个可选项,但依然建议调用以避免可能的内存泄漏. 对变量监控采取轮询方式, 变量超出阈值到抓取到 Task 信息,会有一定的时间间隔. 针对时效性, 模块提供了 2 种捕获模式 - `CAPTURE_IMMEDIATE`(1): 当检测到有变量超出阈值时,立刻抓取 Task 信息,不再等待同一个定时器的其他监控.若 Task 正在前台,则发送 ipi 中断在其上下文立刻获取 Task 信息. - 延迟低,约为 10us, 但只能抓取一个 struct_task 信息(线程 or 进程). - `CAPTURE_AGGREGATE`(0, 默认): 一次检测完定时器内全部变量,之后使用 workqueue 抓取 Task 信息. - 延迟高,约为 200us, 但可以抓取更多的 struct_task 信息. - 此值可以在 `/proc/variable_monitor/stack_capture_mode` 查看和修改. 获取 Task 信息是一项耗时操作,因此对 `CAPTURE_AGGREGATE`模式下 抓取范围 和 定时器重启间隔做了限制. - 定时器重启间隔默认 5s.此值可以在 `/proc/variable_monitor/dump_reset_sec` 查看和修改,修改后即刻生效. - `CAPTURE_AGGREGATE` 模式下默认抓取变量所在 **进程** (包括所有线程) 信息. 如果需要抓取系统全部堆栈信息可以修改 可以`echo 1 > /proc/variable_monitor/sample_all`. ### 挂载驱动 项目根目录 ```bash # 编译加载模块 make && insmod source/variable_monitor.ko # 卸载模块,清理编译文件 # rmmod source/variable_monitor.ko && make clean # 仅在 `kernel 5.17.15-1.el8.x86_64` 测试,其他内核版本未测试. ``` 编译问题: - 找不到 `libunwind.h` 头文件 ```shell # centos 系 确保启用了 epel # sudo dnf install -y epel-release # sudo dnf makecache sudo yum install -y libunwind-devel.x86_64 yum config-manager --set-enabled powertools sudo dnf makecache yum install -y libdwarf-devel.x86_64 # debian 系 sudo apt-get update && apt-get install -y elfutils sudo apt-get update && apt-get install -y libunwind8 ``` ### 宏定义 示例如 `testcase/helloworld.c`, 对常见数值类型宏定义 方便使用: - 其他类型见 `source/uapi/monitor_user_sw.h` ```c // 传入变量名 | 地址 | 阈值 START_WATCH_INT("temp", &temp, 150); START_WATCH_INT_LESS("temp", &temp, 150); ``` 使用宏定义,会将 此监控绑定到申请监控的线程上,当 线程退出时,会自动取消此线程的所有监控. - 注意不是进程. 默认情况下,使用宏定义 定时器的时间间隔为 10us; 此值可以在 `/proc/variable_monitor/def_interval_ns` 查看和修改. ### watch_arg 结构体 如果需要对定时间隔等有更多控制,请定义 watch_arg 结构体,start_watch 启动监控: - 对每个需要监控的变量 设置: 名称 && 地址 && 长度, 设置阈值, 比较方式, 定时器间隔(ns) 等. - 需要监控生命周期与进程相同传入的 task_id 要与进程的 tgid 相同. - `start_watch(watch_arg);` 启动监控 - 需要取消监控时调用 `cancel_watch();` ```c // start_watch 传入的是 watch_arg 结构体.各个字段意义如下 // - name 限制 `MAX_NAME_LEN`(15) 个有效字符 typedef struct { pid_t task_id; // current process id char name[MAX_NAME_LEN + 1]; // name (15+1) void *ptr; // virtual address int length_byte; // byte long long threshold; // threshold value unsigned char unsigned_flag; // unsigned flag (true: unsigned, false: signed) unsigned char greater_flag; // reverse flag (true: >, false: <) unsigned long time_ns; // timer interval (ns) } watch_arg; //一个初始化示例 watch_args = (watch_arg){ .task_id = getpid(), .ptr = &temp, .name = "temp", .length_byte = sizeof(int), .threshold = 150, .unsigned_flag = 0, .greater_flag = 1, .time_ns = 2000 + 5000 }; start_watch(watch_args); ``` ### 打印输出 定时器不断按照设定间隔轮询变量,当达到设定条件时,采集此时系统内符合要求的 Task 信息(用户态堆栈/内核态堆栈/调用链信息). - `dmesg` 可以查看到具体的超出设定条件的变量信息; - Task 信息被输出到缓存区,使用 ucli 工具查看. `dmesg` 打印示例如下 ```log [42865.640988] ------------------------------------- [42865.640992] -----------variable monitor---------- [42865.640993] 超出阈值:1701141698684973655 [42865.640994] : pid: 63936, name: temp0, ptr: 00000000bade6e61, threshold:110 [42865.648068] ------------------------------------- [42875.640703] ------------------------------------- [42875.640706] -----------variable monitor---------- [42875.640706] 超出阈值:1701141708684881779 [42875.640708] : pid: 63936, name: temp0, ptr: 00000000bade6e61, threshold:110 [42875.640710] : pid: 63936, name: temp1, ptr: 00000000ee645b96, threshold:111 [42875.640711] : pid: 63936, name: temp2, ptr: 00000000f62b7afe, threshold:112 [42875.640711] : pid: 63936, name: temp3, ptr: 00000000d100fa3c, threshold:113 [42875.640712] : pid: 63936, name: temp4, ptr: 000000006d31cae1, threshold:114 [42875.640712] : pid: 63936, name: temp5, ptr: 00000000723c7a2a, threshold:115 [42875.640713] : pid: 63936, name: temp6, ptr: 0000000026ef6e83, threshold:116 [42875.640714] : pid: 63936, name: temp7, ptr: 00000000fc1e5d5e, threshold:117 [42875.640714] : pid: 63936, name: temp8, ptr: 0000000069b2666e, threshold:118 [42875.640715] : pid: 63936, name: temp9, ptr: 000000000176263d, threshold:119 [42875.648023] ------------------------------------- ``` 默认情况下 `ucli` 编译后在 build 文件夹下 `ucli > output` - ucli 会将缓存区内容解析后输出到 `output` 文件中. - **此操作会清空缓存区** `ucli` 工具输出示例如下(详情见 output_example) - userstack 是 testcase 下的堆栈信息测试程序. ```log ##CGROUP:[/] 51666 [510] 采样命中[D] 进程信息: [/ / userstack], PID: 51666 / 51666 ##C++ pid 51666 用户态堆栈SP:7ffcd5822298, BP:2, IP:7f071c720838 #~ 0x7f071c720838 __GI___nanosleep ([symbol]) #~ 0x7f071c72076e __sleep ([symbol]) #~ 0x400a08 customFunction1 ([symbol]) #~ 0x400a64 customFunction3 ([symbol]) #~ 0x400a42 customFunction2 ([symbol]) #~ 0x400a21 customFunction1 ([symbol]) #~ 0x400a64 customFunction3 ([symbol]) #~ 0x400a42 customFunction2 ([symbol]) #~ 0x400a21 customFunction1 ([symbol]) #~ 0x400a64 customFunction3 ([symbol]) #~ 0x400a42 customFunction2 ([symbol]) #~ 0x400a21 customFunction1 ([symbol]) #~ 0x400a64 customFunction3 ([symbol]) #~ 0x400a42 customFunction2 ([symbol]) #~ 0x400a21 customFunction1 ([symbol]) #~ 0x400a64 customFunction3 ([symbol]) #~ 0x400a42 customFunction2 ([symbol]) #~ 0x400a21 customFunction1 ([symbol]) #~ 0x400a64 customFunction3 ([symbol]) #~ 0x400a42 customFunction2 ([symbol]) #~ 0x400a21 customFunction1 ([symbol]) #~ 0x400a64 customFunction3 ([symbol]) #~ 0x400a42 customFunction2 ([symbol]) #~ 0x400a21 customFunction1 ([symbol]) #~ 0x400a64 customFunction3 ([symbol]) #~ 0x400a42 customFunction2 ([symbol]) #~ 0x400a21 customFunction1 ([symbol]) #~ 0x400a64 customFunction3 ([symbol]) #~ 0x400a42 customFunction2 ([symbol]) #~ 0x400a21 customFunction1 ([symbol]) #~ 0x400a64 customFunction3 ([symbol]) #~ 0x400a42 customFunction2 ([symbol]) #~ 0x400a21 customFunction1 ([symbol]) #~ 0x400a75 main ([symbol]) #~ 0x7f071c661d85 __libc_start_main ([symbol]) #~ 0x40081e _start ([symbol]) 内核态堆栈: #@ 0xffffffff811730dd hrtimer_nanosleep ([kernel.kallsyms]) #@ 0xffffffff811733a6 __x64_sys_nanosleep ([kernel.kallsyms]) #@ 0xffffffff819fa117 do_syscall_64 ([kernel.kallsyms]) #@ 0xffffffff81c0007c entry_SYSCALL_64_after_hwframe ([kernel.kallsyms]) #@ 0xffffffff819fa117 do_syscall_64 ([kernel.kallsyms]) #@ 0xffffffff81c0007c entry_SYSCALL_64_after_hwframe ([kernel.kallsyms]) #@ 0xffffffff819fa117 do_syscall_64 ([kernel.kallsyms]) #@ 0xffffffff81c0007c entry_SYSCALL_64_after_hwframe ([kernel.kallsyms]) #* 0xffffffffffffff userstack (UNKNOWN) 进程链信息: #^ 0xffffffffffffff ./build/userstack (UNKNOWN) #^ 0xffffffffffffff /bin/bash --init-file /root/.vscode-server-insiders/cli/servers/Insiders-ca9da6c177fc4cf7429e1d0c1c52f710d6d953c6/server/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh (UNKNOWN) #^ 0xffffffffffffff /root/.vscode-server-insiders/cli/servers/Insiders-ca9da6c177fc4cf7429e1d0c1c52f710d6d953c6/server/node /root/.vscode-server-insiders/cli/servers/Insiders-ca9da6c177fc4cf7429e1d0c1c52f710d6d953c6/server/out/bootstrap-fork --type=ptyHost --logsPath /root/ (UNKNOWN) #^ 0xffffffffffffff /root/.vscode-server-insiders/cli/servers/Insiders-ca9da6c177fc4cf7429e1d0c1c52f710d6d953c6/server/node /root/.vscode-server-insiders/cli/servers/Insiders-ca9da6c177fc4cf7429e1d0c1c52f710d6d953c6/server/out/server-main.js --connection-token=remotessh --a (UNKNOWN) #^ 0xffffffffffffff sh /root/.vscode-server-insiders/cli/servers/Insiders-ca9da6c177fc4cf7429e1d0c1c52f710d6d953c6/server/bin/code-server-insiders --connection-token=remotessh --accept-server-license-terms --start-server --enable-remote-auto-shutdown --socket-path=/tmp/code (UNKNOWN) #^ 0xffffffffffffff /root/.vscode-server-insiders/code-insiders-ca9da6c177fc4cf7429e1d0c1c52f710d6d953c6 command-shell --cli-data-dir /root/.vscode-server-insiders/cli --on-port --require-token b5a047063eb7 (UNKNOWN) #^ 0xffffffffffffff /usr/lib/systemd/systemd --switched-root --system --deserialize 17 (UNKNOWN) ## ``` ## demo usercase 文件夹下 - `helloworld.c`: 测试大量变量监控 - `userstack.c`: 测试用户态堆栈输出 - `hptest.c`: 测试 hugePage 挂载 根目录 Dockerfile 文件对应 容器内用户堆栈输出测试. ```shell docker build -t userstack-image . docker run --privileged --device=/d/variable_monitor -v /:/host -it userstack-image ``` ## Test Tools 用户空间命令行工具 `ucli` 提供了查看给定 pid / tgid 的 Task 信息的功能. ```shell # pid 获取结果为线程对应 Task 信息 ./build/ucli --pid=2048 # tgid 获取结果为进程下所有线程对应 Task 信息 ./build/ucli --tgid=2048 ``` 测试工具使用独立缓存区,不会影响 variant_monitor 的输出. ## 源码说明 程序分为两部分: 字符设备 和 用户空间接口, 两者通过 ioctl 通信. 用户空间地址访问 - 用户程序传入的变量 虚拟地址, 使用 `get_user_pages_remote` 获取地址所在内存页, `kmap` 将其映射到内核. - 192.168.40.204 环境下,HugeTLB Pages 测试挂载正常. - 内存页地址 + 偏移量存入定时器对应的 `kernel_watch_arg` 中, hrTimer 轮询时访问 `kernel_watch_arg` 得到真实值. 定时器分组 - hrTimer 数据结构定义在全局数组 `kernel_wtimer_list`.分配定时器时,会检查遍历 `kernel_wtimer_list` 比较定时器间隔, - 属于 **同一个线程** 且 **相同定时间隔** 的 watch 分配到同一组,对应同一个 hrTimer. - 若一个定时器监控变量数量超过 `TIMER_MAX_WATCH_NUM` (32),则会创建一个新的 hrTimer. - hrTimer 的总数量(`kernel_wtimer_list` 数组长度)限制是 `MAX_TIMER_NUM`(128). 内存页 mount/unmount - `get_user_pages_remote`/ `kmap` 会增加对应的计数,需要对等的 `put_page`/`kunmap`. - 一个模块内全局链表 `watch_local_memory_list` 存储每一个成功挂载的变量对应的 page 和 kt,执行字符设备的 close 操作时,遍历并卸载. variable monitor 添加/删除 - kernel_watch_arg 数据结构中有 pid 的成员变量,但添加变量监控 按照传入的 pid 区分. - 删除时遍历全部监控变量,比较 pid. - 删除造成的缺位,将最后的变量移动到空位, sentinel--; hrTimer 同理. 堆栈输出条件: 条件参考自 [diagnose-tools::load.c](https://github.com/alibaba/diagnose-tools/blob/e285bc4626a7d207eabd4a69cb276e1a3b1b7c76/SOURCE/module/kernel/load.c#L209) - `TASK` 要满足 TASK_RUNNING 和 `__task_contributes_to_load` 和 `TASK_IDLE`(可能有阻塞进程). - `__task_contributes_to_load` 对应内核宏 `task_contributes_to_loa`. ```c // https://www.spinics.net/lists/kernel/msg3582022.html // remove from 5.8.rc3,but it still work // whether the task contributes to the load #define __task_contributes_to_load(task) \ ((READ_ONCE(task->__state) & TASK_UNINTERRUPTIBLE) != 0 && (task->flags & PF_FROZEN) == 0 && \ (READ_ONCE(task->__state) & TASK_NOLOAD) == 0) ```