《Linux指令:SystemTap内核跟踪和探测工具》

之前的文章曾利用systemtap的"watchpoint"功能来监测变量数值的变化,但systemtap在内核debug中的应用远不止此,本文将借由对SLUB的调试,展现systemtap的其他一些功能。

相比起近两年才出现的bpftrace,自2005年就作为RHEL 4.2中technology preview推出的systemtap算是历史悠久。伴随这么多年的发展,systemtap积累了丰富且成熟的功能,但同时它也是一个相对「重型」的工具。说它重型,是因为它要求安装的软件包,比crash工具还要多:

# rpm -qa | egrep -e kernel-'(devel|debug)' -e systemtap
kernel-debuginfo-common-x86_64-3.10.0-1062.el7.x86_64
kernel-debuginfo-3.10.0-1062.el7.x86_64
kernel-devel-3.10.0-1062.el7.x86_64
systemtap-client-4.0-11.el7.x86_64
systemtap-devel-4.0-11.el7.x86_64
systemtap-runtime-4.0-11.el7.x86_64

除了crash工具依赖的debuginfo,systemtap还需要额外的一些组件,而它的使用之所以需要这些包,其实可以从它的workflow看出来:

把这堆东西安装完后,可通过以下命令验证功能是否真正可用:

stap -v -e 'probe vfs.read { printf("read performed\n"); exit() }'

所安装的"kernel-devel"不仅需与当前内核版本保持一致,其编译时间也需匹配(对,就是这么矫情),否则会出现错误 "ERROR: module version mismatch",可对照"/proc/version"修改"/usr/src/kernels/$(uname -r)/include/generated/compile.h"文件来解决。

systemtap有着一套自己的语法体系,不过与C语言的风格差别不大,配合其众多的examples(安装后位于"/usr/share/systemtap/"目录),入门并不困难。事实上,systemtap脚本(以下称"stap")在结合debuginfo的信息解析了symbol后,也是转化为C语言,进而编译成一个kernel module来运行的。

又是解析,又是编译,整个过程在第一次的时候耗时还是比较长的,所以笔者一般都会加上代表verbose的"-v"参数来观察这中间的过程。好在它会将编译的结果以cache的形式保存起来,下次再执行同样的stap脚本,就比较迅速了。

其实它能做的事,你自己写一个kernel module,理论上也能做,但写脚本,还是方便多了。如果用传统的printk打印的话,可能还需要重新编译内核,就更麻烦了。

不仅是syscall(在stap中关键字为"syscall",对比strace),也不仅是函数的入口和出口(在stap中关键字为"function",对比ftrace),这家伙能追踪的点,基本是任一address(在stap中关键字为"statement"),甚至以源代码行数的形式喂给它都认得(毕竟有debuginfo不是),比如:

probe kernel.statement("tcp_sendmsg@net/ipv4/tcp.c:1320")

有时我们想追一个驱动模块的启动阶段(模块的probe格式为"module(xxx)"),那么在这个模块加载前,就通过systemtap埋伏好点位,也是完全可以达到目的的(现在有没有觉得前面安装那么多东西还是值得的)。

这种强悍的能力,源于它基于的底层机制,即kprobe(不过stap脚本使用的关键字是"probe",反倒是后面文章要介绍的bpftrace用的关键字"kprobe",不要搞混了)。

kprobe的实现机理在不同的arch上有一定的差异,就x86而言,它采用的是将被追踪指令先保存起来,然后将其第一个字节替换成breakpoint指令(比如"int3")。当CPU执行到这条指令,触发trap,相关寄存器被保存,控制权被kprobe劫持,然后,就可以为所欲为了。

干完自己想干的事,kprobe会单步执行之前保存的原始指令,接下来它可以直接归还控制权,或者再执行个"post-handler"(在原始指令执行前的动作则对应为"pre-handler")。

用户态进程可以通过gdb被真正地暂停,使用RTOS或者bare-metal的嵌入式系统也可以通过硬件断点或者软件断点的方式停下来,但Linux内核不行,因此,借助systemtap来在线获取变量信息、打印backtrace、修改返回值等,就成为了尤为重要的调试手段。

说起「修改返回值」,systemtap还有一个妙用。在coredump领域几乎是事实标准的crash工具,也可以用于在线分析,包括动态地修改指定内存的值,但是在crash工具中直接使用"wr"命令往往是不行的,因为"/dev/mem"的使用被限制了。而我们的systemtap,则可以通过修改"devmem_is_allowed"函数的返回值,来帮助crash工具绕过这一限制。

stap -g -e 'probe kernel.function("devmem_is_allowed").return { $return = 1 }'

说了那么多,还是回到用systemtap收集SLUB的信息上来(要不然本文真的有跑题的嫌疑……)。

要分析SLUB的内存分配,最好是track调用链比较内层的"slab_alloc_node"函数,以获得更全面的覆盖。可惜这个函数被inline了,无法识别入参,因此本文退而求其次,改为追踪"kmem_cache_alloc"函数。脚本大致如下:

#!/usr/bin/stapglobal slab_alloc_stackprobe kernel.function("kmem_cache_alloc"){
slab_alloc_stack[backtrace()]++
}probe timer.s(5){
foreach (bt in slab_alloc_stack) {
println("")
print_syms(bt)
printf("alloc %d times\n", slab_alloc_stack[bt])}}

"slab_alloc_stack"是一个associative array(关联数组,即不仅可以通过整数来索引,还可以使用字符串或其他类型的值来索引的数组)。在这个示例中, "backtrace()"就是索引(key),某一种backtrace出现的次数就是"value"。

考虑到「关联数组」通常体积较大,不适合动态分配,且往往被多个函数共享,因此其在systemtap中只能被定义为「全局变量」的形式(关键字为"global")。

由于stap脚本通常用来追踪一个函数,因此说到变量,需要注意区分是stap脚本自己定义的变量,还是所追踪的函数的变量(target varible)。不知道你注意到没有,stap脚本自己用的变量并没有申明类型,这是因为其类型是在赋值时被自动判定的。

"kernel.function"前面已经介绍过了,它属于同步触发的probe点。而"timer"属于异步触发的probe点,表示每隔多长时间执行一次(这里是5秒),"foreach"用于对关联数组的遍历。

使用"stap -v slub-top.stp"执行该脚本:

打印的stack里面有的地方只能显示address,不能显示symbol,同时在stap脚本的执行过程中,会出现很多这样的打印:

WARNING: Missing unwind data for a module, rerun with 'stap -d xxx'

缺失的这些symbol属于内核模块,默认没有被加载,如果确切地知道是缺少哪个内核模块的debug信息,可以通过"-d"指定,如果缺失的比较多(比如本文这种情况),那就直接用"--all-modules"吧。

现在symbol的问题算是解决了,但输出的内容里,既没有申请分配SLUB的进程名称,也没有SLUB对象的类型和大小,并没有太多的分析价值。而要想获得这些额外的信息,可以从被probe的函数的参数入手。

# stap -L 'kernel.function("kmem_cache_alloc")' kernel.function("kmem_cache_alloc@mm/slub.c:2719") $s:struct kmem_cache* $gfpflags:gfp_t

dereference入参指针"s",便能拿到对应SLUB的名称和大小。"s"就是前面提到的target varible,使用时需加上"$"符号。在stap的语法里,不管是结构体还是结构体指针,统一使用"->"来获取成员变量("."在stap里是用来连接字符串的)。至于进程名么,可直接用stap提供的"execname()"内置函数。

probe kernel.function("kmem_cache_alloc")
{
printf("alloc "%s" with size "%d", for process %s\n", $s->name, $s->size, execname())}

执行一下,出现了错误:

semantic error: type mismatch: expected string but found long: $s->name

因为"s->name"是"const char *"类型,会被识别为指针,所以这里应该用"kernel_string($s->name)"转换一下。

现在,该有的信息倒是都齐备了,但是未经统计和归纳,难以直观地判断在一段时间内,到底哪些进程才是消耗SLUB的狂魔,又到底哪些SLUB在被这些狂魔贪婪地占有。下面,将上述的2个示例组合一下,来一点stap的进阶用法:

probe kernel.function("kmem_cache_alloc")
{
slab_alloc_stack[execname(),kernel_string($s->name),backtrace()] <<< $s->size
}probe timer.s($1){
foreach ([proc,slub,bt] in slab_alloc_stack- limit $2) {
printf("Proc:%s Slub:%s Count:%d Sum:%d\n", proc, slub,
@count(slab_alloc_stack[proc,slub,bt], @sum(slab_alloc_stack[proc,slub,bt]))
print_syms(bt)
}exit()}

这个组合后的脚本的主要目的,是根据关联数组的value来进行排序。在foreach循环里,数组名后面跟的"-"代表递减排序,"limit"表示取最为靠前的几个。

同时,为了实现更灵活地控制(以及展现stap的更多用法),笔者分别使用了"$1"和"$2"来设定时长和limit数目,而它们对应的都是脚本执行时传入的整型类参数(如果是字符串类参数则应该用"@1", "@2"):。

stap -v --all-modules slub-top-sort.stp 10 5

这里,作为关联数组的"slab_alloc_stack"有进程名、SLUB名称和backtrace三个key,其效果类似于一个三维数组。系统中有多个进程,每个进程在进入内核态后可能申请的SLUB也有多种,即便是同一个进程申请的同一种SLUB,其调用栈的路径也可能不同。

对于这样一个key的组合,假设对应SLUB的大小为256,且在10秒的执行时间内被命中了4次,使用"<<<"符号,可以将这4次的数据都保存下来,便于之后的统计处理。"@sum"用于聚合数据的累加值(即1024),一个可能有点容易混淆的概念是"@count",它汇总的是次数之和(即4)。

在脚本中进一步结合"if"条件过滤,可以单独看一个进程申请最多的前几种SLUB,如果依然以SLUB的大小作为统计数值,那么每次probe命中时的数值大小都可能不同,这时计算最小值、平均值和最大值的"@min", "@avg"和"@max"就派上了用场。

原作者:兰新宇

Linux中使用Systemtap调试SLUB相关推荐

  1. Linux中使用GDB调试NS2

    在使用ns2过程中,避免不了要修改或者添加一些模块,对C++代码进行改动.编写好自己的功能模块以后,进行编译执行.但通常不会这么容易通过,经常会出现编译可以通过,但执行是会出错.比较常见的是" ...

  2. linux中的 slab/slob/slub

    很久很久以bai前:一个叫做Mark Hemment的哥儿们du写了Slab.在接下来的一些年里zhi,其他人对Slab进行了完善dao.一年半以前,SLOB问世了.SLOB的目标是针对嵌入式系统的, ...

  3. Linux中的gdb调试方法总结

    转载于:https://www.cnblogs.com/lanjianhappy/p/6763937.html

  4. Linux下性能测量和调试诊断工具Systemtap

    一.简介 SystemTap是一个诊断Linux系统性能或功能问题的开源软件.它使得对运行时的Linux系统进行诊断调式变得更容易.更简单.有了它,开发者或调试人员不再需要重编译.安装新内核.重启动等 ...

  5. Linux课程---11、Linux中软件安装和调试

    Linux课程---11.Linux中软件安装和调试 一.总结 一句话总结: 启动过程:1.安装软件,2.修改配置文件,3.启动服务 查看过程:4.查看进程,5.查看端口 关闭过程:6.关闭软件,7. ...

  6. Linux 中启用 Shell 脚本的调试模式

    shell 脚本调试系列 Linux 中启用 Shell 脚本的调试模式 在 Shell 脚本中执行语法检查调试模式 在 Shell 脚本中跟踪调试命令的执行 概述 脚本是存储在一个文件的一系列命令. ...

  7. linux中使用gbd进行单布调试

    在linux 中使用gdb命令行进行单步调试,将整个过程介绍如下: 1.在当前路径下新建文件夹main, 并进入文件夹,新建文件main.cpp mkdir main cd main touch ma ...

  8. 《Linux C编程从入门到精通》》一2.4 Linux中的调试环境gdb

    本节书摘来自异步社区<<Linux C编程从入门到精通>>一书中的第2章,第2.4节,作者 宋磊 , 程钢,更多章节内容可以访问云栖社区"异步社区"公众号查 ...

  9. 下面linux程序中哪一个是调试器,【编程】noi2009笔试复习题(1)

    noi2009笔试复习题 第一部分:单选题 1. NOI 机试使用的操作系统是: A. Windows B. Linux C. MacOS D. Vxworks 答案:B 2. Linux 中为文件改 ...

最新文章

  1. CUDA error: an illegal memory access was encountered
  2. linux netlink 编程示例(一)内核端
  3. BJUI修改详情页的标题内容
  4. EF支持复杂类型的实现
  5. vue-cli脚手架中webpack配置基础文件详解
  6. Android录制音频的三种方式
  7. VS2017控制台打印问题
  8. 【网络流24题】餐巾计划问题(费用流)
  9. R中读取文件,找不到路径问题 No such file or directory
  10. jquery遍历的radio的取值问题
  11. linux简介及安装使用
  12. python调用cmd执行命令_python怎么运行cmd命令
  13. XP高仿win7宽栏风格主题
  14. DIY_红外计数模块
  15. 力扣刷题 DAY_81 贪心
  16. NodeJS解析前端请求图片链接,将服务器目录下的图片返回给前端用于页面展示
  17. 全网搜php,PHP实现中文全文搜索的原理介绍
  18. 智能家居项目进度控制表
  19. crypto-js前端解密工具
  20. element-ui 阻止冒泡事件

热门文章

  1. 程序员学会精刷LeetCode后,会变得有多强...
  2. Java优雅的记录日志:log4j实战篇
  3. Java多线程学习二十八:原子类和 volatile 有什么异同?
  4. cdr非法软件 您的产品已被禁用怎么回事_多层刺网非法捕鱼 顺庆男子被取保候审...
  5. 算法总结系列之八:复读机的故事 - 散列表.NET应用的研究(下集)
  6. [JAVA基础]你知道Java的四种引用类型吗
  7. html template--(来自网易)
  8. vscode终端无法识别node
  9. 实现第一个Servlet程序
  10. mysql什么是覆盖索引,MySQL中的覆盖索引