在应用程序中添加 DTrace 探测

DTrace 提供丰富的用于监视系统各方面(从内核直到应用程序)运行情况的探测。我们可以在不修改应用程序的情况下执行很多检查,但是要想获得详细的统计数据,就需要在应用程序中添加探测。在本文中,我们要讨论如何设计探测、从哪里将它们添加到应用程序中、放置探测的最佳位置以及如何有效地构建和使用已经添加的探测。

Martin Brown, 自由作家, Freelance Developer

Martin Brown 作为一名专业作家已有八年多。他撰写了不少书和文章,内容涉及很多主题。他的特长涉猎很多开发语言和平台,包括 Perl、Python、Java、JavaScript、Basic、Pascal、Modula-2、C、C++、Rebol、Gawk、Shellscript、Windows、Solaris、Linux、BeOS、Mac OS/X、Web 编程以及系统管理和集成等。Martin 是 ServerWatch.com、LinuxToday.com 和 IBM developerWorks 专栏作家,定期在 Computerworld、The Apple Blog 和其他站点上发表博客文章,同时也是微软的 Subject Matter Expert(SME)。可以通过他的网站(http://www.mcslp.com)与 Martin Brown 联系。



2010 年 6 月 21 日

简介

Solaris(包括 OpenSolaris)、FreeBSD 和 Mac OS X 中内置的 Dynamic Tracing (DTrace) 功能提供一个用于动态地跟踪应用程序的简单环境。与调试不同,DTrace 可以根据需要打开或关闭,而且使用跟踪功能不需要以特殊方式构建应用程序。

上面的所有平台都支持使用标准的 DTrace 探测。这包括在代码中不同函数边界由操作系统实现的那些探测。这些探测称为 Function Boundary Tracing (FBT),可以通过它们探测特定函数的启动或停止。

这个功能的局限是,只能使用它探测函数,而不能探测应用程序的功能性片段。如果想探测组成同一操作的多个函数,或者想检查某一函数的片段,FBT 就无能为力了。

对于自己的应用程序,可以使用 User-land Statically Defined Tracing (USDT) 解决这个问题。USDT 让开发人员可以在代码中重要的位置添加特定的探测。还可以使用 USDT 从正在运行的应用程序获取数据,这些数据可作为跟踪应用程序的探测的参数而被访问。

开始在系统中添加 USDT 探测之前,首先需要考虑想让探测报告什么、它们可能提供什么信息以及潜在的性能问题。


探测设计

如果发现标准的 FBT 探测不适合您的需求,就需要考虑在应用程序中添加静态的探测。

当使用 DTrace 在应用程序中添加探测时,要考虑的第一个问题是,您实际上想用探测实现什么目的。探测有助于查明许多问题和信息,但是您应该明确探测的目标。应该选择特定的领域,比如功能性、性能和其他可测量的信息,这些信息应该是只使用函数边界上的标准进入/退出探测无法获得的。

因此,在简单的层面上,应该考虑两种主要的探测类型:

  1. 信息探测:这些探测提供或汇总在执行程序期间难以以其他方式获得的信息。例如,内部结构的大小或内容,或者不由函数直接处理的事件的操作或触发。在现有的操作系统探测中有许多这类探测。例如,可以获取磁盘 I/O 统计数据或虚拟内存系统中的错误数量。
  2. 操作探测:这些探测在特定事件或语句序列前后触发,可以在语句序列的开头和末尾使用它们获取内部结构的特定信息,或者监视一组语句的执行时间。因为可以把这些探测放在任何地方以表示操作的开始和结束,所以它们可以跨多个函数,也可以只覆盖函数中的一小部分。这比使用函数边界实用得多,函数边界提供的范围可能太大或太小。根据惯例,这些探测通常包含后缀 start 和 done。

决定了探测的类型之后,要考虑的下一个问题是,是否希望在探测中暴露更多信息,如果是这样,是什么信息以及应该采用什么格式提供它们。在 DTrace 中,探测可以通过参数暴露信息,可以通过编写适当的 DTrace 脚本或单行代码访问这些参数。例如,如果要检查一个文件 I/O 函数,可以把正在写的文件的名称添加到函数的探测中。

在定义探测时,指定这些参数的名称和类型:probe write__file__start(int id, char *filename);

在监视期间,DTrace 脚本可以通过变量 arg0arg1 等访问每个参数。因此,可以使用 printf("%s\n", copyinstr(arg1)); 输出文件名(第二个参数)。

要想选择正确的要暴露的数据,必须了解您希望在监视应用程序时从探测获得什么。例如,对于上面的 I/O 函数示例,如果这个函数用于写多个文件,那么文件名可能很重要。但是,如果此函数只写同一个文件,那么完全不需要在探测中暴露文件名。

因此,必须考虑以什么方式提供信息。希望能够按操作类型、文件或网络端口汇总数据吗?希望知道数据大小,还是想知道正在写的实际数据?这些问题都与程序和环境的具体情况相关。

还应该考虑以这种方式提供信息的开销,应该通过共享信息尽可能降低开销,尤其是对于大型结构。应该限制提供的信息量,如果可能的话,应该进行信息汇总(但是注意,对于 DTrace 探测来说,字符串的复制、缩减或重新格式化对性能影响更大)。

以这种方式引入探测有两种方法:多个探测以及使用特殊的 ‘是否启用探测’ 检查。当编写探测并使用 dtrace 命令生成头文件时,以一种非常简单的风格支持后一个解决方案。可以把代码块放在判断此探测是否已经启用(例如是否正在监视它)的检查代码内,还可以在其中执行更多操作,这对于汇总或整理数据很有用(见 清单 1)。

清单 1. 检查探测是否已经启用的代码
if (WRITE_FILE_START_ENABLED())
{
    ...
}

前一种方法使用单独的探测,这让用户可以使用所需的探测获取所需的信息。例如,对于 I/O 示例,可以通过概念上嵌套的探测结构提供不同层次的信息,这样就可以决定需要的信息:

  • write-file-start (id)
  • write-file-data(filename, buffer)
  • write-file-done (id)

在监视应用程序时,如果只想监视写文件操作的速度,可以使用 write-file-start 和 write-file-done 探测,它们提供供引用的 ID。如果需要文件名数据,可以监视 write-file-data 探测以输出这一信息。

最后,对于所有探测,必须牢记一点:有权监视 DTrace 探测的任何人都能够访问它们暴露的信息。例如,如果在探测中暴露电子邮件地址或邮件内容,那么具有 DTrace 权限的任何用户都能够读取这些信息。以这种方式暴露敏感信息一定要小心。如果可能的话,应该只提供统计数据;如果必须暴露可能敏感的真实信息,可以考虑对数据进行模糊处理,让别人判断不出真正的内容。


定义探测

在 Solaris/OpenSolaris 上,可以使用 /usr/include/sys/sdt.h 中的宏来定义探测。根据希望包含的参数数量,通过调用适当的宏,在代码中插入探测。例如,可以使用 DTRACE_PROBE("prime","calc-start") 插入一个无参数的探测。

如果希望共享参数,根据在触发探测时共享的参数数量,使用不同的宏(从 1 编号到 5)。例如,使用 DTRACE_PROBE("prime","calc-start",prime) 共享一个参数。

只在 Solaris/OpenSolaris 上支持这种方法。在 Solaris/OpenSolaris、FreeBSD 和 Mac OS X 上都支持的另一种可移植性更好的方法(这也提供更简便的在代码中插入探测的方法)是创建探测定义文件,其中包含希望插入代码中的每个探测,包括希望通过每个探测共享的参数的定义。

这个文件的格式与 C 语言相似。必须指定一个或多个提供者,在每个提供者中指定希望在代码中支持的探测。探测定义的示例见 清单 2

清单 2. 示例探测定义
provider primes {

/* Start of the prime calculation */

   probe primecalc__start(long prime);

/* End of the prime calculation */

   probe primecalc__done(long prime, int isprime);

/* Exposes the size of the table of existing primes */

   probe primecalc__tablesize(long tablesize);

};

provider 是在应用程序中安装探测之后提供者的名称。DTrace 中的探测由提供者、模块、函数和探测名标识:provider:module:function:name。对于 USDT 探测,可以只指定其中的 provider 和 name 部分。

探测的名称取自 probe 关键字后面的字符串。可以用双下划线分隔探测名中的单词。在跟踪期间希望使用探测时,把双下划线改为单一连字符。例如,这个文件中的探测名 primecalc__start() 可以表示为 primes::primecalc-start(提供者和探测名的组合)。

定义中每个探测的参数用来标识 C 代码中参数的数据类型。在跟踪期间用 arg0arg1argN 等引用参数。因此,对于 primecalc__done 探测,素数是 arg0,不确定是否是素数则用 arg1 表示。

创建了探测定义文件之后,使用 dtrace 命令把探测定义转换为头文件:$ dtrace -o probes.h -h -s probes.d

上面的命令指定输出文件名 (-o)、希望生成头文件 (-h) 以及源探测定义文件名 (-s)。

产生的头文件包含宏,可以通过在代码中放置这些宏插入探测。可以在代码中希望触发探测的任何地方使用它们,次数不限。

探测宏的名称取决于您定义的探测名。例如,primecalc__done 的宏是 PRIMES_PRIMECALC_DONE。既然有了探测定义文件(在构建应用程序时还要使用它)和头文件,现在就该在 C 源代码中插入探测了。


标识探测的位置

为了说明把探测放在什么地方,我们来看一个用于判断素数的简单程序。这里的代码不是最高效的,DTrace 探测可以帮助我们发现问题。

最初的源代码见 清单 3

清单 3. 用于判断素数的程序的源代码
#include <stdio.h>

long primes[1000000] = { 3 };
long primecount = 1;

int main(int argc, char **argv)
{
  long divisor = 0;
  long currentprime = 5;
  long isprime = 1;

  while (currentprime < 1000000)
    {
      isprime = 1;
       for(divisor=0;divisor<primecount;divisor++)
        {
          if (currentprime % primes[divisor] == 0)
            {
              isprime = 0;
            }
        }
      if (isprime)
        {
          primes[primecount++] = currentprime;
          printf("%d is a prime\n",currentprime);
        }
      currentprime = currentprime + 2;
    }
}

添加 DTrace 探测之后的代码见 清单 4

清单 4. 添加 DTrace 探测之后的代码
#include <stdio.h>
#include "probes.h"

long primes[1000000] = { 3 };
long primecount = 1;

int main(int argc, char **argv)
{
  long divisor = 0;
  long currentprime = 5;
  long isprime = 1;

  while (currentprime < 1000000)
    {
      isprime = 1;
      PRIMES_PRIMECALC_START(currentprime);
      for(divisor=0;divisor<primecount;divisor++)
        {
          if (currentprime % primes[divisor] == 0)
            {
              isprime = 0;
            }
        }
      PRIMES_PRIMECALC_DONE(currentprime,isprime);
      if (isprime)
        {
          primes[primecount++] = currentprime;
          PRIMES_PRIMECALC_TABLESIZE(primecount);
          printf("%d is a prime\n",currentprime);
        }
      currentprime = currentprime + 2;
    }

}

决定探测的位置要考虑许多因素:

  • 由 dtrace 生成的头文件已经包含在源代码中。
  • primecalc-startprimecalc-done 探测的位置紧挨着执行计算的主循环外边。您可能想把探测放在外层 while 循环的开头和末尾,因为这似乎是插入探测的合理位置。但是,正如前面提到的,对于这些用来监视特定功能领域的探测,应该尽可能接近要监视的实际操作。如果把探测放在 while 循环的开头和末尾,就会包含许多与素数的实际计算无关的操作。这些额外步骤与您真正想监视的操作花费的时间会加在一起,尽管在这个应用程序中不会有显著的差异,但是在其他应用程序中可能差异很大。
  • primecalc-tablesize 探测的设计目的不是监视执行时间,而是监视表的大小。显然,放置这个探测的位置应该尽可能接近修改值的地方。这一点很重要,因为从跟踪的角度来说,即使不打算监视值随时间的变化,也希望知道修改值的准确位置。
  • 注意,done 探测提供数字和这个数字是否判断为素数。在 for 循环结束之后,就已经知道数字是否是素数,尽管在 if 语句之前并不使用判断结果。另外,通过提供 isprime 变量的值,可以在代码中放置 done 探测,用这个值作为参数,以此替代其他探测。在脚本中,可以使用这个值通过谓词分别统计素数和非素数花费的时间。
  • 对于其他操作,应用相同的规则。应该确保提供统计数据(不一定是计时数据)的任何探测尽可能接近修改此信息的地方。可以在代码中多次放置同一个探测。在这里,可以在主代码块的头中变量初始化代码后面放上 PRIMES_PRIMECALC_TABLESIZE 宏,从而提供最初的值。

编译应用程序

可以使用 C 编译器像编译其他应用程序一样编译 DTrace 应用程序。在 Mac OS X/FreeBSD 上,可以完全像平常一样编译应用程序。但是在 Solaris/OpenSolaris 上,必须修改对象文件并生成一个包含 DTrace 探测的新的对象文件。

在 Solaris/OpenSolaris 上,编译过程会在最终链接之前修改对象文件,还必须链接一个单独生成的对象文件,其中包含希望在应用程序中启用的探测。修改对象文件的过程是自动的 — 也就是说,您指定对象文件,过程会修改文件并把修改保存回源代码文件。在这个过程中生成对象文件。一般的操作次序是:

  1. 把每个源代码文件编译为对象文件,例如:$ gcc -c primes.c
  2. 编译所有源代码文件之后,创建一个 DTrace 探测对象文件,其中包含要链接进主程序的探测。例如,对于单一对象文件,可以使用以下命令:$ dtrace -G -s probes.d -o probes.o primes.o
  3. 上面的命令读取对象文件 primes.o 和探测定义 probes.d,然后生成探测对象文件 probes.o。如果有多个包含 DTrace 探测的对象文件,可以在命令行上指定更多对象文件。例如:$ dtrace -G -s probes.d -o probes.o file1.o file2.o file3.o
  4. 链接应用程序,包括所有对象文件和生成的探测对象文件:$ gcc -o primes primes.o probes.o
  5. 生成最终的 primes 可执行程序,可以运行并探测它了。

在 FreeBSD/Mac OS X 上不需要生成单独的探测对象文件。这使编译过程大大简化了:

  1. 把每个源代码文件编译为对象文件,例如:$ gcc -c primes.c
  2. 链接应用程序,包括所有对象文件:$ gcc -o primes primes.o
  3. 包含 DTrace 探测的应用程序已经准备好了。

现在来尝试使用探测。


通过编写脚本使用探测

我们只在一个非常基本的程序中添加了一些非常基本的探测,但是仍然可以获得一些有用的执行信息。例如,可以使用 start 和 done 探测查明找到所有素数和所有非素数花费多长时间。因为素数比非素数少得多,应该可以看到这两类元素的计时数据有很大差异。示例脚本见 清单 5

清单 5. 显示寻找素数和非素数的时间差异的脚本
#!/usr/sbin/dtrace -s

#pragma D option quiet

primes*:::primecalc-start
{
  self->start = timestamp;
}

primes*:::primecalc-done
/arg1 == 1/
{
        @times["prime"] = sum(timestamp - self->start);
}

primes*:::primecalc-done
/arg1 == 0/
{
        @times["nonprime"] = sum(timestamp - self->start);
}

END
{
        normalize(@times,1000000);
        printa(@times);
}

这个脚本使用谓词区分最终结果为素数和非素数的计算,把从 start 探测到 done 探测的时间数据累加起来。时间数据放在一个关联数组中,使用聚合函数 sum() 执行聚合。

END 块中的 normalize() 函数把结果除以一百万,得到以毫秒为单位的时间数据。如果在运行 primes 程序的同时运行这个脚本,会得到与 清单 6 相似的输出。

清单 6. 输出
$ dtrace -s timing.d            
^C

  prime                                             10784
  nonprime                                       16340221

结果表明,寻找非素数花费的时间远远高于寻找素数的时间。这是因为非素数比素数多得多。


技巧和注意事项

在使用 DTrace 探测时要考虑许多问题,下面是您应该了解的一些问题:

  • 不要把探测放在函数进入点和退出点。因为已经可以使用 FBT 探测自动地访问这些位置,所以这么做的惟一好处只是提供与函数名不同的探测名。如果要添加 USDT 探测,应该在希望探测的操作点上创建探测,不应该在函数边界上。
  • 不要让 DTrace 探测作为函数中的最后一个语句。否则,对于某些平台和编译器组合,代码优化过程可能会把探测优化掉(实际上完全删除探测),或者把探测与发出调用的函数联系在一起(这可能会消除参数数据,或者导致怪异的计时问题)。
  • 如果要编译供链接的库,希望能够使用探测,就需要在创建库之前对对象文件运行 DTrace 进程。另外,在 Solaris/OpenSolaris 上,需要在库中包含生成的对象文件和普通的对象文件。在使用复杂的构建过程时必须格外小心,比如 automake 应用的构建过程会把对象文件放在一个临时目录中,在生成库时显式地使用它。必须确保对添加到库中的对象文件运行 dtrace 命令。
  • 生成的探测对象文件与它基于的对象文件必须匹配。如果对象文件改变了,必须重新生成探测对象文件,否则链接会失败。
  • 如果使用 autoconf、cmake 或相似的程序,希望保持跨平台兼容性,对于 dtrace 要注意的惟一一点是使用 -G 生成探测对象文件。很容易在配置期间对此进行测试。

结束语

使用基于函数的跟踪和 DTrace 可以提供很多信息,但是添加自己的静态探测能够提供最大的灵活性。可以选择静态探测的位置、名称和它暴露的信息,这样就可以根据自己的需要调整暴露的信息。

添加静态探测需要创建适当的定义文件,然后用这个文件生成宏,在 C 源代码中使用这些宏。尽管根据平台不同需要完成一些额外步骤,但是编译相当简单。只要记住关于放置探测的位置和选择所用探测的建议,在应用程序中包含探测的好处常常会超过其开销。

参考资料

学习

  • 阅读 http://www.ibm.com/developerworks/cn/aix/library/au-satstandardsh.html,学习如何跨多台机器使用相同的命令。
  • System Administration Toolkit:查阅本系列中的其他文章。
  • 让 UNIX 和 Linux 一起工作讲解如何让传统的 UNIX® 发行版和 Linux 一起工作。
  • 不同的系统使用不同的工具,IBM Redbook Solaris to Linux Migration: A Guide for System Administrators 可以帮助您了解一些关键工具。
  • AIX and UNIX 专区:developerWorks 的“AIX and UNIX 专区”提供了大量与 AIX 系统管理的所有方面相关的信息,您可以利用它们来扩展自己的 UNIX 技能。
  • AIX and UNIX 新手入门:访问“AIX and UNIX 新手入门”页面可了解更多关于 AIX 和 UNIX 的内容。
  • AIX and UNIX 专题汇总:AIX and UNIX 专区已经为您推出了很多的技术专题,为您总结了很多热门的知识点。我们在后面还会继续推出很多相关的热门专题给您,为了方便您的访问,我们在这里为您把本专区的所有专题进行汇总,让您更方便的找到您需要的内容。
  • AIX and UNIX 下载中心:在这里你可以下载到可以运行在 AIX 或者是 UNIX 系统上的 IBM 服务器软件以及工具,让您可以提前免费试用他们的强大功能。
  • IBM Systems Magazine for AIX 中文版:本杂志的内容更加关注于趋势和企业级架构应用方面的内容,同时对于新兴的技术、产品、应用方式等也有很深入的探讨。IBM Systems Magazine 的内容都是由十分资深的业内人士撰写的,包括 IBM 的合作伙伴、IBM 的主机工程师以及高级管理人员。所以,从这些内容中,您可以了解到更高层次的应用理念,让您在选择和应用 IBM 系统时有一个更好的认识。
  • 要想听听为软件开发人员准备的有意思的访谈和讨论,请查阅 developerWorks podcasts
  • developerWorks 技术活动网络广播:随时关注 developerWorks 技术活动和网络广播。

获得产品和技术

  • 使用 IBM 试用软件 改进您的下一个开放源码开发项目,这些软件可以下载或者通过 DVD 获得。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX
ArticleID=497162
ArticleTitle=在应用程序中添加 DTrace 探测
publish-date=06212010