当前位置: 首页 > 工具软件 > DTrace > 使用案例 >

dtrace linux_将DTrace探针添加到您的应用程序

鲁建茗
2023-12-01

Solaris(包括OpenSolaris),FreeBSD和Mac OS X内置的动态跟踪(DTrace)功能为动态跟踪应用程序提供了简单的环境。 与调试不同,DTrace可以随意打开或关闭,并且您无需提供特殊的应用程序构建即可利用跟踪功能。

以上所有平台均支持使用标准DTrace探针。 涵盖了操作系统在代码内不同功能边界上公开的内容。 这些探针(称为功能边界跟踪(FBT))使您可以识别何时开始或停止执行给定功能。

此功能的局限性在于它只能用于探测应用程序的功能,而不能用于探测功能片段。 如果要检查构成单个操作的多个功能的执行情况,或者要查看单个功能的一部分,FBT则无济于事。

对于您自己的应用程序,您可以使用用户域静态定义的跟踪(USDT)解决此问题。 USDT使您作为开发人员能够在您认为重要的代码点上为应用程序添加特定的探针。 USDT系统还使您能够从正在运行的应用程序中公开数据,在跟踪应用程序时可以将其作为探针的参数进行访问。

在开始将USDT探针添加到系统之前,首先需要考虑要报告的探针,探针可能提供的信息以及潜在的性能问题。

探头设计

一旦确定标准FBT探针不适合您的需求,就需要开始考虑将静态探针添加到您的应用程序中。

使用DTrace向应用程序添加探针时,首先要考虑的是首先确定实际使用探针的目的。 探针可以帮助您识别各种问题和信息,但是您应该针对探针。 您应该挑选出特定的区域,例如功能,性能或其他可测量的信息,这些信息超出了仅使用所有功能障碍中提供的标准进入/退出探测器所能找到的范围。

因此,从简单的角度来看,您应该考虑有两种主要的探针类型:

  1. 信息探针 :这些信息探针会暴露或总结一条信息,否则这些信息在程序执行过程中将很难确定。 这里的好例子包括内部结构的大小或内容,或者不是由函数直接处理的事件的操作或触发。 现有的操作系​​统探针中有许多示例。 例如,您可以获得有关磁盘I / O统计信息或虚拟内存系统内故障的信息。
  2. 操作探针 :这些探针将特定事件或语句序列括起来,以便您可以使用它们在序列的开头和结尾获取有关内部结构的特定信息,或监视一组特定语句的执行时间。 因为您可以将这些探针放置在任何地方以指示操作的开始和结束,所以它们可以跨越多个功能,或者仅覆盖给定功能的一小部分。 这些功能比使用可能提供过多或不足范围的功能边界更为实用。 按照惯例,这些探针通常以start和done作为后缀。

确定探针的类型后,下一步要考虑的是是否要在探针中公开任何其他信息,如果是,则要提供什么信息以及采用哪种格式。 在DTrace中,探针可以通过参数公开信息,这些参数可在编写合适的DTrace脚本或单行代码时进行处理。 例如,如果要检测文件I / O功能,则可以将要写入文件的名称添加到该功能的探针中。

在定义探测器时, probe write__file__start(int id, char *filename);这些参数指定名称和类型: 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示例,您可能具有一个探针结构,该探针结构在不同级别上提供了不同的信息,并且这些探针名义上是嵌套的,以便您可以确定所需的信息:

  • 写文件开始(id)
  • 写文件数据(文件名,缓冲区)
  • 写文件完成(id)

监视应用程序时,如果只想监视写文件操作的速度,则可以使用提供引用ID的write-file-start和write-file-done探针。 如果需要文件名数据,则可以选择监视“写文件数据”探针以在处理期间输出该信息。

最后,对于所有探针,都应记住,有权监视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);

};

一旦在应用程序中安装了探针,提供程序就是提供程序的名称。 DTrace中的探针名称由提供者,模块,功能和探针名称标识: provider:module:function:name 。 对于USDT探针,您只能指定此规范的提供者和名称部分。

探针的名称是从probe关键字之后的字符串中获取的。 您可以使用双下划线在探针名称中分隔单词。 要在跟踪过程中使用探针时,它将转换为单个连字符。 例如,可以使用primes::primecalc-start名称(结合提供程序和探针名称)来标识此文件中的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;
    }
}

清单4显示了修改后的代码,其中添加了DTrace探针。

清单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探针既提供数字,也提供数字是否被确定为素数。 实际上,即使在if语句之前不使用确定的值,即使在for循环结束之后,您实际上也知道该数字是否为质数。 另外,请注意,通过提供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. 最终的prime可执行文件将被探测,启用并准备使用。

在FreeBSD / Mac OS X上,您不必生成单独的探针对象文件来进行链接。 这使编译过程更加简单:

  1. 将每个源文件编译为一个目标文件,例如: $ gcc -c primes.c
  2. 链接您的应用程序,包括所有目标文件: $ gcc -o primes primes.o
  3. 带有DTrace探针的应用程序已准备就绪,可以使用。

现在,让我们尝试使用探针。

编写脚本以使用您的探针

您仅对基本程序添加了一些非常基本的探针,但是仍然可以从执行中获得一些有用的信息。 例如,您可以使用“开始”和“完成”探针来查找查找所有素数和所有非素数所需的时间。 由于素数在非素数上的分布要小得多,因此您应该看到这两个元素的时序之间存在显着差异。 清单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);
}

该脚本使用谓词来标识计算最终是针对质数还是非质数的,从开始探针到完成探针的时序数据汇总。 信息被放入一个关联数组,使用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源代码中使用从该文件生成的宏的情况。 尽管根据您的平台需要完成一些较小的步骤,但是编译相对简单。 只要为您牢记有关将探针放置在何处以及如何选择要使用的探针的建议,通常所带来的好处将超过将探针实际包含在应用程序中的少量开销。


翻译自: https://www.ibm.com/developerworks/aix/library/au-dtraceprobes.html

 类似资料: