This repository has been archived on 2025-09-14. You can view files and clone it, but cannot push or open issues or pull requests.
Files
zhangyang-variable-monitor/README.md
2023-12-27 03:47:50 -05:00

14 KiB
Raw Permalink Blame History

Variable Monitor

changelog

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 会抹除符号信息,有可能导致无法将地址还原为正确符号信息.

文件结构

├── 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.

挂载驱动

项目根目录

# 编译加载模块
make && insmod source/variable_monitor.ko
# 卸载模块,清理编译文件
# rmmod source/variable_monitor.ko && make clean
# 仅在 `kernel 5.17.15-1.el8.x86_64` 测试,其他内核版本未测试.

编译问题:

  • 找不到 libunwind.h 头文件
# 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
// 传入变量名 | 地址 | 阈值
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();
// 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 打印示例如下

[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 下的堆栈信息测试程序.
##CGROUP:[/]  51666      [510]  采样命中[D]
    进程信息: [/ / userstack] PID 51666 / 51666
##C++ pid 51666
    用户态堆栈SP7ffcd5822298, 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 文件对应 容器内用户堆栈输出测试.

docker build -t userstack-image .
docker run --privileged --device=/d/variable_monitor -v /:/host -it userstack-image

Test Tools

用户空间命令行工具 ucli 提供了查看给定 pid / tgid 的 Task 信息的功能.

# 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

  • TASK 要满足 TASK_RUNNING 和 __task_contributes_to_loadTASK_IDLE(可能有阻塞进程).
  • __task_contributes_to_load 对应内核宏 task_contributes_to_loa.
// 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)