实现一个中断驱动的采样分析器

Implementing an Interrupt driven Sampling Profiler

本文关键字:中断驱动 采样 分析器 一个 实现      更新时间:2023-10-16

我正在尝试创建一个在linux上工作的采样分析器,我不确定如何发送中断或如何获得程序计数器(pc),因此我可以找出程序在中断时的位置。

我已经尝试使用信号(SIGUSR1, Foo*)和调用backtrace,但是当我raise(SIGUSR1)而不是程序正在运行的线程时,我得到了线程的堆栈。我真的不确定这是否是正确的方法……

任何建议吗?

如果您必须编写一个分析器,让我建议您使用一个好的(Zoom)作为您的模型,而不是一个坏的(gprof)。这些是它的特点。

有两个阶段。首先是数据收集阶段:

  • 当它获取一个样本时,它读取整个调用堆栈,而不仅仅是程序计数器。

  • 即使进程由于I/O,睡眠或其他任何原因而阻塞,也可以进行采样。

  • 您可以打开/关闭采样,以便仅在您关心的时间进行采样。例如,在等待用户输入某些内容时,采样是没有意义的。

第二个是数据表示阶段。你所拥有的是一个堆栈样本的集合,其中堆栈样本是一个内存地址的向量,这些地址几乎都是返回地址。每个返回地址表示函数中的一行代码,除非它是在某些没有符号信息的系统例程中。

关键的有用信息是驻留分数(通常表示为百分比)。如果总共有m堆栈样本,并且代码行L出现在样本的n上的任何位置,则其驻留分数为n/m。这是成立的即使L在一个样本中出现了不止一次,那仍然只是它出现的一个样本。驻留分数的重要性在于它直接表明了语句L占用了多少时间。如果你取了m=1000个样本,L出现在n=300个样本上,那么L的居住分数是300/1000或30%。这意味着如果L可以被移除,总时间将减少30%。它通常被称为包括的百分比。

您不仅可以为代码行确定驻留分数,还可以为您可以描述的任何其他内容确定驻留分数。例如,代码行L位于某个函数F内。因此您可以确定函数的驻留分数,而不是代码行数。这样就得到了包含函数的百分比。你可以看看函数对,比如在样本的多少百分比上你看到函数F调用函数G。这将为您提供构成呼叫图的信息。

你可以从堆栈样本中得到各种各样的信息。一种常见的是"蝴蝶视图",在这种视图中,您将"焦点"放在一行L或函数F上,并在一边显示堆栈样本中紧邻它的所有行或函数,而在另一边显示紧邻它的所有函数行。在每一个上面,你都可以显示居住分数。您可以在这里单击,以尝试找到具有高驻留分数的代码行,您可以找到消除或减少这些代码行的方法。这就是提高代码速度的方法。

无论您为输出做什么,我认为允许用户实际检查其中的一小部分是非常重要的,这些是随机选择的。它们传达的信息比任何浓缩信息的方法都要深刻得多。

知道分析器应该做什么很重要,知道不应该做什么也很重要,即使很多其他分析器都在做:

  • 自我时间。一个无用的号码。看看一些合理大小的程序,你就知道为什么了。

  • 调用计数。对于寻找具有高驻留分数的代码没有任何帮助,而且无论如何,您无法仅通过示例获得它。

  • 高频采样。令人惊讶的是,很多人,当然是分析器构建者,认为获得大量的样本是很重要的。假设直线L是1000个样本的30%那么它真正的包容性百分比是30±1.4%。另一方面,如果它在10样品的30%上,则其包含百分比为30 +/- 14%。它仍然相当大——大到足以修复。在大多数分析器中发生的事情是人们认为他们需要"数值精度",所以他们采取了大量的样本并积累了他们所谓的"统计",然后扔掉了样本。这就像挖出钻石,称重,然后扔掉。真正的价值在于样本本身,因为它们能告诉你问题出在哪里。

可以使用目标线程的pthread_kill和tid (gettid())向特定线程发送信号。

创建简单分析器的正确方法是使用setitimer,它可以发送周期性信号(SIGALRMSIGPROF),例如,每10毫秒;或者posix计时器(timer_create, timer_settime,或timerfd),不需要单独的线程来发送分析信号。检查google-perftools (gperftools)的源代码,它们使用setitimer或posix定时器,并通过回溯收集配置文件。

gprof也使用setitimer来实现cpu时间分析。安排内核定期向进程发送信号(通常通过setitimer())")。

示例:gperftools源代码中setitimer的代码研究结果:https://code.google.com/p/gperftools/codesearch#search/&q=setitimer&sq=package:gperftools&type=cs

void ProfileHandler::StartTimer() {
  if (!allowed_) {
    return;
  }
  struct itimerval timer;
  timer.it_interval.tv_sec = 0;
  timer.it_interval.tv_usec = 1000000 / frequency_;
  timer.it_value = timer.it_interval;
  setitimer(timer_type_, &timer, 0);
}

你应该知道settimer有forkclone的问题;它不适用于多线程应用程序。有尝试创建helper包装器:http://sam.zoy.org/writings/programming/gprof.html(错误的一个),但我不记得,它是否正常工作(setitimer通常发送进程范围的信号,而不是线程范围的)。UPD:似乎自linux内核2.6.12以来,setitimer的信号被定向到整个进程(任何线程都可能得到它)。

要将信号从timer_create引导到特定的线程,需要gettid() (#include <sys/syscall.h>, syscall(__NR_gettid))和SIGEV_THREAD_ID flag。不要检查如何使用thread_create创建周期性posix计时器(可能使用timer_settime和非零it_interval)。

PS:维基教科书中有一些概要分析:http://en.wikibooks.org/wiki/Introduction_to_Software_Engineering/Tools/Profiling