zoukankan      html  css  js  c++  java
  • 解析Systemtap

    SystemTap 的架构

    让我们深入探索 SystemTap 的某些细节,理解它如何在运行的内核中提供动态探针。您还将看到 SystemTap 是如何工作的,从构建进程脚本到在运行的内核中激活脚本。

    动态地检查内核

    SystemTap 用于检查运行的内核的两种方法是 Kprobes 返回探针。但是理解任何内核的最关键要素是内核的映射,它提供符号信息(比如函数、变量以及它们的地址)。有了内核映射之后,就可以解决任何符号的地址,以及更改探针的行为。

    Kprobes 2.6.9 版本开始就添加到主流的 Linux 内核中,并且为探测内核提供一般性服务。它提供一些不同的服务,但最重要的两种服务是 Kprobe KretprobeKprobe 特定于架构,它在需要检查的指令的第一个字节中插入一个断点指令。当调用该指令时,将执行针对探针的特定处理函数。执行完成之后,接着执行原始的指令(从断点开始)。

    Kretprobes 有所不同,它操作调用函数的返回结果。注意,因为一个函数可能有多个返回点,所以听起来事情有些复杂。不过,它实际使用一种称为 trampoline 的简单技术。您将向函数条目添加一小段代码,而不是检查函数中的每个返回点。这段代码使用 trampoline 地址替换堆栈上的返回地址 —— Kretprobe 地址。当该函数存在时,它没有返回到调用方,而是调用 Kretprobe(执行它的功能),然后从 Kretprobe 返回到实际的调用方。

    SystemTap 的流程

    1 展示了 SystemTap 的基本流程,涉及到 3 个交互实用程序和 5 个阶段。该流程首先从 SystemTap 脚本开始。您使用stap实用程序将 stap 脚本转换成提供探针行为的内核模块。stap 流程从将脚本转换成解析树开始 (pass 1)。然后使用细化(elaboration)步骤 (pass 2) 中关于当前运行的内核的符号信息解析符号。接下来,转换流程将解析树转换成C源代码 (pass 3) 并使用解析后的信息和 tapset 脚本SystemTap 定义的库,包含有用的功能)。stap 的最后步骤是构造使用本地内核模块构建进程的内核模块 (pass 4)

    图 1. SystemTap 流程

     

    有了可用的内核模块之后,stap完成了自己的任务,并将控制权交给其他两个实用程序 SystemTapstaprunstapio。这两个实用程序协调工作,负责将模块安装到内核中并将输出发送到 stdout (pass 5)。如果在 shell 中按组合键 Ctrl-C 或脚本退出,将执行清除进程,这将导致卸载模块并退出所有相关的实用程序。

    SystemTap 的一个有趣特性是缓存脚本转换的能力。如果安装后的脚本没有更改,您可以使用现有的模块,而不是重新构建模块。图 2 显示了 user-space kernel-space 元素以及基于 stap 的转换流程。

    图 2. 从 kernel/user-space 角度了解 SystemTap 流程

     


    回页首

    SystemTap 脚本编写

    SystemTap 中编写脚本非常简单,但也很灵活,有许多您需要使用的选项。参考资料 提供一个详述语言和可行性的手册的链接,但这个小节仅讨论一些例子,让您初步了解 SystemTap 脚本。

    探针

    SystemTap 脚本由探针和在触发探针时需要执行的代码块组成。探针有许多预定义模式,表 1 列出了其中的一部分。这个表列举了几种探针类型,包括调用内核函数和从内核函数返回。

    表 1. 探针模式例子

    探针类型

    说明

    begin

    在脚本开始时触发

    end

    在脚本结束时触发

    kernel.function("sys_sync")

    调用sys_sync时触发

    kernel.function("sys_sync").call

    同上

    kernel.function("sys_sync").return

    返回sys_sync时触发

    kernel.syscall.*

    进行任何系统调用时触发

    kernel.function("*@kernel/fork.c:934")

    到达 fork.c 的第 934 行时触发

    module("ext3").function("ext3_file_write")

    调用 ext3 write函数时触发

    timer.jiffies(1000)

    每隔 1000 个内核 jiffy 触发一次

    timer.ms(200).randomize(50)

    每隔 200 毫秒触发一次,带有线性分布的随机附加时间(-50 +50

    我们通过一个简单的例子来理解如何构造探针,并将代码与该探针相关联。清单 3 显示了一个样例探针,它在调用内核系统调用sys_sync时触发。当该探针触发时,您希望计算调用的次数,并发送这个计数以及表示调用进程 IDPID)的信息。首先,声明一个任何探针都可以使用的全局值(全局名称空间对所有探针都是通用的),然后将它初始化为 0。其次,定义您的探针,它是一个探测内核函数sys_sync的条目。与探针相关联的脚本将递增count变量,然后发出一条消息,该消息定义调用的次数和当前调用的 PID。注意,这个例子与C语言中的探针非常相似(探针定义语法除外),如果具有C语言背景将非常有帮助。

    清单 3. 一个简单的探针和脚本

    global count=0
     
    probe kernel.function("sys_sync") {
      count++
      printf( "sys_sync called %d times, currently by pid %d
    ", count, pid );
    }

    您还可以声明探针可以调用的函数,尤其是希望供多个探针调用的通用函数。这个工具还支持递归到给定深度。

    变量和类型

    SystemTap 允许定义多种类型的变量,但类型是从上下文推断得出的,因此不需要使用类型声明。在 SystemTap 中,您可以找到数字(64 位签名的整数)、整数(64 位)、字符串和字面量(字符串或整数)。您还可以使用关联数组和统计数据(我们稍后讨论)。

    表达式

    SystemTap 提供C语言中常用的所有必要操作符,并且用法也是一样的。您还可以找到算术操作符、二进制操作符、赋值操作符和指针废弃。您还看到从C语言带来的简化,其中包括字符串连接、关联数组元素和合并操作符。

    语言元素

    在探针内部,SystemTap 提供一组类似于C一样易于使用的语句。注意,尽管该语言允许您开发复杂的脚本,但每个探针只能执行 1000 条语句(这个数量是可配置的)。表 2 列出了一小部分语句作为例子。注意,在这里的许多元素和C中的一样,尽管有一些附加的东西是特定于 SystemTap 的。

    表 2. SystemTap 的语言元素

    语句

    说明

    if (exp) {} else {}

    标准的if-then-else语句

    for (exp1 ; exp2 ; exp3 ) {}

    一个for循环

    while (exp) {}

    标准的while循环

    do {} while (exp)

    一个do-while循环

    break

    退出迭代

    continue

    继续迭代

    next

    从探针返回

    return

    从函数返回一个表达式

    foreach (VAR in ARRAY) {}

    迭代一个数组,将当前的键分配给VAR

    本文在样例脚本中探索了统计数据和聚合功能,因为这是C语言中不存在的。

    最后,SystemTap 提供许多内部函数,这些函数提供关于当前上下文的额外信息。例如,您可以使用caller()识别当前的调用函数,使用cpu()识别当前的处理器号码,以及使用pid()返回 PIDSystemTap 还提供许多其他函数,提供对调用堆栈和当前注册表的访问。


    回页首

    SystemTap 例子

    在简单介绍了 SystemTap 的要点之后,我们接下来通过一些简单的例子来了解 SystemTap 的工作原理。本文还展示了该脚本语言的一些有趣方面,比如聚合。

    系统调用监控

    前一个小节探索了一个监控sync系统调用的简单脚本。现在,我们查看一个更加具有代表性的脚本,它可以监控所有系统调用并收集与它们相关的额外信息。

    清单 4 显示的简单脚本包含一个全局变量定义和 3 个独立的探针。在首次加载脚本时调用第一个探针(begin探针)。在这个探针中,您可以发出一条表示脚本在内核中运行的文本消息。接下来是一个syscall探针。注意这里使用的通配符 (*),它告诉 SystemTap 监控所有匹配的系统调用。当该探针触发时,将为特定的 PID 和进程名增加一个关联数组元素。最后一个探针是 timer 探针。这个探针在 10,000 毫秒(10 秒)之后触发。与这个探针相关联的脚本将发送收集到的数据(遍历每个关联数组成员)。当遍历了所有成员之后,将调用exit调用,这导致卸载模块和退出所有相关的 SystemTap 进程。

    清单 4. 监控所有系统调用 (profile.stp)

    global syscalllist
     
    probe begin {
      printf("System Call Monitoring Started (10 seconds)...
    ")
    }
     
    probe syscall.*
    {
      syscalllist[pid(), execname()]++
    }
     
    probe timer.ms(10000) {
      foreach ( [pid, procname] in syscalllist ) {
        printf("%s[%d] = %d
    ", procname, pid, syscalllist[pid, procname] )
      }
      exit()
    }

    清单 4 中的脚本的输出如清单 5 所示。从这个脚本中您可以看到运行在用户空间中的每个进程,以及在 10 秒钟内发出的系统调用的数量。

    清单 5. profile.stp 脚本的输出

    $ sudo stap profile.stp
    System Call Monitoring Started (10 seconds)...
    stapio[16208] = 104
    gnome-terminal[6416] = 196
    Xorg[5525] = 90
    vmware-guestd[5307] = 764
    hald-addon-stor[4969] = 30
    hald-addon-stor[4988] = 15
    update-notifier[6204] = 10
    munin-node[5925] = 5
    gnome-panel[6190] = 33
    ntpd[5830] = 20
    pulseaudio[6152] = 25
    miniserv.pl[5859] = 10
    syslogd[4513] = 5
    gnome-power-man[6215] = 4
    gconfd-2[6157] = 5
    hald[4877] = 3
    $

    特定的进程的系统调用监控

    在这个例子中,您稍微修改了上一个脚本,让它收集一个进程的系统调用数据。此外,除了仅捕捉计数之外,还捕捉针对目标进程的特定系统调用。清单 6 显示了该脚本。

    这个例子根据特定的进程进行了测试(在本例中为syslog守护进程),然后更改关联数组以将系统调用名映射到计数数据。

    清单 6. 新系统调用监控脚本 (syslog_profile.stp)

    global syscalllist
     
    probe begin {
      printf("Syslog Monitoring Started (10 seconds)...
    ")
    }
     
    probe syscall.*
    {
      if (execname() == "syslogd") {
        syscalllist[name]++
      }
    }
     
    probe timer.ms(10000) {
      foreach ( name in syscalllist ) {
        printf("%s = %d
    ", name, syscalllist[name] )
      }
      exit()
    }

    清单 7 提供了该脚本的输出。

    清单 7. 新脚本的 SystemTap 输出 (syslog_profile.stp)

    $ sudo stap syslog_profile.stp
    Syslog Monitoring Started (10 seconds)...
    writev = 3
    rt_sigprocmask = 1
    select = 1
    $

    使用聚合步骤数字数据

    聚合实例时捕捉数字值的统计数据的出色方法。当您捕捉大量数据时,这个方法非常高效有用。在这个例子中,您收集关于网络包接收和发送的数据。清单 8 定义两个新的探针来捕捉网络 I/O。每个探针捕捉特定网络设备名、PID 和进程名的包长度。在用户按 Ctrl-C 调用的 end 探针提供发送捕获的数据的方式。在本例中,您将遍历recv聚合的内容、为每个元组(设备名、PID 和进程名)相加包的长度,然后发出该数据。注意,这里使用提取器来相加元组:@count提取器获取捕获到的长度(包计数)。您还可以使用@sum提取器来执行相加操作,分别使用@min@max来收集最短或最长的程度,以及使用@avg来计算平均值。

    清单 8. 收集网络包长度数据 (net.stp)

    global recv, xmit
     
    probe begin {
      printf("Starting network capture (Ctl-C to end)
    ")
    }
     
    probe netdev.receive {
      recv[dev_name, pid(), execname()] <<< length
    }
     
    probe netdev.transmit {
      xmit[dev_name, pid(), execname()] <<< length
    }
     
    probe end {
      printf("
    End Capture
    
    ")
     
      printf("Iface Process........ PID.. RcvPktCnt XmtPktCnt
    ")
     
      foreach ([dev, pid, name] in recv) {
        recvcount = @count(recv[dev, pid, name])
        xmitcount = @count(xmit[dev, pid, name])
        printf( "%5s %-15s %-5d %9d %9d
    ", dev, name, pid, recvcount, xmitcount )
      }
     
      delete recv
      delete xmit
    }

    清单 9 提供了清单 8 中的脚本的输出。注意,当用户按 Ctrl-C 时退出脚本,然后发送捕获的数据。

    清单 9. net.stp 的输出

    $ sudo stap net.stp
    Starting network capture (Ctl-C to end)
    ^C
    End Capture
     
    Iface Process........ PID.. RcvPktCnt XmtPktCnt
     eth0 swapper         0           122        85
     eth0 metacity        6171          4         2
     eth0 gconfd-2        6157          5         1
     eth0 firefox         21424        48        98
     eth0 Xorg            5525         36        21
     eth0 bash            22860         1         0
     eth0 vmware-guestd   5307          1         1
     eth0 gnome-screensav 6244          6         3
    Pass 5: run completed in 0usr/50sys/37694real ms.
    $

    捕获柱状图数据

    最后一个例子展示 SystemTap 用其他形式呈现数据有多么简单 —— 在本例中以柱状图的形式显示数据。返回到是一个例子中,将数据捕获到一个名为 histogram 的聚合中(见清单 10)。然后,使用netdev接收和发送探针以捕捉包长度数据。当探针结束时,您将使用@hist_log提取器以柱状图的形式呈现数据。

    清单 10. 步骤和呈现柱状图数据 (nethist.stp)

    global histogram
     
    probe begin {
      printf("Capturing...
    ")
    }
     
    probe netdev.receive {
      histogram <<< length
    }
     
    probe netdev.transmit {
      histogram <<< length
    }
     
    probe end {
      printf( "
    " )
      print( @hist_log(histogram) )
    }

    清单 11 显示了清单 10 的脚本的输出。在这个例子中,使用了一个浏览器会话、一个 FTP 会话和ping来生成网络流量。@hist_log提取器是一个以 2 为底数的对数柱状图(如下所示)。还可以步骤其他柱状图,从而使您能够定义 bucket 的大小。

    清单 11. nethist.stp 的柱状图输出

    $ sudo stap nethist.stp 
    Capturing...
    ^C
    value |-------------------------------------------------- count
        8 |                                                      0
       16 |                                                      0
       32 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@            1601
       64 |@                                                    52
      128 |@                                                    46
      256 |@@@@                                                164
      512 |@@@                                                 140
     1024 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  2033
     2048 |                                                      0
     4096 |                                                      0
     
    $

  • 相关阅读:
    数据库三大范式
    sql 外键 on update cascade 和 on delete cascade 作用区别?
    Mybatis入门简版(二)
    Mybatis入门简版(一)
    Mybatis入门简版(补充)
    SQL中ON和WHERE的区别
    MySQL基础(五)常见运算符
    MySQL基础(四)常用函数
    MySQL基础(三)多表查询(各种join连接详解)
    MySQL基础(二)
  • 原文地址:https://www.cnblogs.com/fuhaots2009/p/3435829.html
Copyright © 2011-2022 走看看