DTrace的原理
本系列文章详细地介绍了一个 Linux 下的全新的调式、诊断和性能测量工具 Systemtap 和它所依赖的基础 kprobe 以及促使开发该工具的先驱 DTrace 并给出实际使用例子使读者更进一步了解和认识这些工具。 本文是该系列文章之二,它详细地讲解了 DTrace 的原理。本系列文章之一讲解了 kprobe 的原理、编程接口、局限性和使用注意事项并给出实际使用示例帮助读者理解和认识 kprobe。本系列文章之三讲解了 Systemtap 的原理,以及 Systemtap 与 DTrace 比较,并通过一个例子向读者展示 Systemtap 的工作机理。
一、DTrace 简介
DTrace是一个强大的动态跟踪框架,它允许管理员、开发者和服务团队精确地回答关于操作系统和用户程序的任何问题。用户可以使用它管理成千上万个探测点,为每一个探测点指定执行条件(Predicates)和执行的动作(Actions),动态管理跟踪缓存和探测点开销。用户通过它能够对正在运行的系统跟踪来查看问题,也可以根据系统崩溃时产生的dump数据来查看问题。开发人员则可以实现新的提供者(Provider)和消费者(Consumer)以及配置管理探测点的工具。
DTrace对探测点上下文没有任何限制,也就说可以对任何函数进行探测,这是因为DTrace框架是完全非阻塞的,它基本上没有显式或隐式地调用内核其他功能。
Dtrace主要包含以下组件:
消费者(Consumer)
消费者是一个用户态应用,如dtrace,它能够通过Dtrace框架提供的接口库来访问DTrace内核组件
提供者(Provider)
提供者实现作内核模块,每一个模块建立某一种类的探测点,消费者能够使能提供者实现的任何探测点,并且可以把任何动作(action)绑定到这些探测点。提供者也能根据用户的跟踪请求创建新的探测点。
DTrace已经实现了16个提供者(当然用户可以实现自己的提供者),包括dtrace、lockstat、profile、fbt(Function Boundary Tracing)、syscall、sdt(Statcially Defined Tracing)、sysinfo、vminfo、proc、sched、io、mib、fpuinfo、pid、plockstat和fasttrap。
探测点(Probes)
探测点是由提供者创建的用于标识要探测的模块和函数。每一个探测点有一个名字。每一个探测点可以用四元组provider:module:function:name来标识,它也有独一无二的整数标识符。
预测(Predicates)
预测是一个用/括起来的表达式,类似于条件,它在探测点命中时计算以决定关联的动作是否被执行。预测是主要的条件结构,在D脚本中的复杂的控制流都是用它来实现的。对于任何探测点,该部分可以被完全省略,这时,关联到探测点的动作(action)将在任何一次命中后都被执行。预测必须是一个可计算的表达式。在D语言中,0值解释为假,1解释为真。
动作(Actions)
动作是用D语言编写的一些语句,它被Dtrace虚拟机在内核中执行。关联到探测点的动作会在该探测点命中时执行。一般地,动作用来记录指定的系统状态信息,但是它也能改变系统状态,这种动作称为破坏性动作(destructive action),缺省情况下不允许这种动作。
D脚本语言
D语言类似于awk脚本,用户使用它来编写动作和预测。
DTrace可以被用于性能监视、用户进程跟踪、匿名跟踪和推测跟踪(匿名跟踪没有消费者,这只能被超级用户执行,而且至多只能有一个匿名跟踪;推测跟踪则是指当探测点被命中后,动作是根据条件来执行的,predicates被用于实现推测跟踪),并且在没有使能跟踪的情况下没有任何开销,对系统性能没有任何影响。
二、DTrace原理
DTrace的核心组件全部在内核中实现,包括探测点处理、缓存以及instrumentation,用户态进程作为DTrace消费者(consumer)通过利用DTrace库能够和内核中的DTrace组件进行通信,DTrace提供的应用工具(dtrace就是一个典型的消费者,只是它使得用户可以非常方便地使用DTrace提供的任何功能)。DTrace框架不执行任何instrumentation,它只是负责指派instrumentation给对应的提供者(provider)。当DTrace核心组件指令提供者执行instrumentation时,提供者自己确定要instrument的点,然后回调DTrace框架提供的接口来创建探测点。为了创建一个探测点,提供者需要指定模块名、函数名以及探测点名。
每一个探测点用一个独一无二的四元组表示(简介部分已经讲到),四元组包括提供者、模块、函数名和探测点名,创建的探测点并不会被立即执行,它还需要消费者使能(探测点也可以被消费者失效),只有使能的探测点才会被执行。DTrace框架提供的探测点创建接口函数在执行成功时会给提供者返回一个探测点标识符。这些创建的探测点会通知给消费者,消费者能使能它感兴趣的探测点,当使能探测点时,DTrace将创建一个使能控制块(ECB -- Enabling Control Block)并绑定到该探测点,如果该探测点原来是失效的(即没有其它ECB绑定到该探测点),DTrace还将调用该探测点的提供者来真正使能该探测点(当然,如果该探测点已经是使能的,即它的ECB链不为空,这个操作是不必要的)。当CPU执行到一个使能的探测点(即运行到提供该探测点的提供者)时,对应的提供者会把控制传递到DTrace框架的入口并传递探测点的标识符作为第一个参数,DTrace框架得到控制后会失效当前CPU上的中断,然后执行该探测点ECB链上的每一个ECB指定的活动(一个探测点可以绑定多个动作),然后使能中断并把控制返回到该提供者。提供者不需要考虑如何多路复用处理一个探测点的多个消费者,ECB能很好地处理它。
每一个ECB可以有预测或条件(predicates),如果ECB有预测或条件,那么动作只有在该条件满足时才被执行。每一个ECB可以有多个动作,它们都会在条件满足时执行。如果动作要求保存一些数据,数据将被保存到对应于相应的消费者的缓存。动作不可以保存数据到内核内存,不可以修改寄存器,也不可以对系统状态做任何变化(破坏性动作除外,那要求用户必须是特权用户)。
DTrace为每一个DTrace消费者在每一个CPU上分配一些内核缓存,消费者状态指向该缓存,消费者的所有ECB都有一个指向消费者状态的指针。当一个ECB的动作要求保存数据时,数据就被保存到这些缓存中,一个ECB保存的数据量是一个常数,不同的ECB该常数可以不同。在处理ECB之前,会核对那些缓存看是否有足够的空间保存数据,如果没有足够的空间,对应于该缓存的一个丢失计数器会加1,该ECB上的所有动作将被跳过。消费者需要定期地读取缓存中的数据,否则会导致数据丢失。缓存实现完全是免锁的,即消费者读的同时,ECB的动作仍能在需要时立刻保存数据,不必锁等待。它是这样来实现的:每一个CPU上有两个缓存,一个是活动的(active),一个是非活动的(inactive),当消费者想读取指定CPU的缓存时,两个缓存被交换,即活动的变成非活动的,非活动的变成活动的,这个交换操作可以在极其短的时间内完成,并使用中断失效来保护(ECB的动作保存数据或其它消费者的读取操作需要访问这些缓存,因而必须被同步)。在交换之后,消费者从不活动的缓存读取数据,而ECB能够向活动的缓存写数据。在每一个CPU上的缓存中,每一次的数据保存都会把探测点标识符(EPID -- Enabled Probe Identifier,EPID与ECB一一对应,因此可用于查询对应的ECB已经保存的数据的长度)放在首部。
动作(actions)和预测/条件(predicates)用D语言编写,它们会转换成DIF(D Intermediate Forat,DTrace实现的一种虚拟机指令),DIF简化了仿真和代码生成。
它DIF指令是解释执行的,安全检查可以在解释时进行,因此安全性可以得到保证。当DIF指令装入内核时,操作码,保留位,寄存器,字符串引用,变量引用以及一些基本的安全检查都要被严格检查。一些运行时错误(如除0)并不能从静态分析中发现,因此它们被DIF虚拟机处理。当遇到这样的指令,DIF虚拟机将不执行它们,并导致当前ECB的处理终止,相应的消费者得到一个运行时错误。从设备的I/O内存区装入数据也是不允许的,DIF可以根据要装入的数据地址很容易地做判断。对于无效的数据装入地址,DIF虚拟机判断它是否有效很复杂,因此借用硬件已有的错误处理机制来实现,DTrace修改页失效处理函数来检查这种情况并做特殊处理,当遇到这样的情况,将导致页失效,页失效处理函数会检查导致页失效的指令是否来自DIF虚拟机,如果是,页失效处理函数将设置一个标志位来表示发生了页失效,然后跳到导致页失效指令的下一条指令执行,也即让DIF虚拟机继续执行其余的DIF虚拟机指令,因此DIF虚拟机能够在执行下一条虚拟机指令前能够有机会检查那个标志位。每次DIF虚拟机在执行一条虚拟机指令前都会检查那个标志位,如果被设置了,当前的ECB处理将被终止,相应的消费者将得到一个运行时错误。这种实现增加了页失效处理函数的开销,但是相对于整个页失效处理函数的开销,它是非常小的,因此对系统性能的影响可以忽略不记。
D语言类似于awk脚本语言,它支持所有ANSI C支持的操作符,变量类型,也支持typedef并能够定义struct、union和enum。也允许访问内核定义的数据类型和全局变量。D语言支持C语法的变量声明,变量可以声明,也可以不声明,如果不声明,它的类型在赋值时确定,实际类型为所赋值的类型。
D语言也可以调用DTrace提供的函数和变量。D语言支持全局变量、语句内变量、线程本地变量和关联数组。为了区分语句内变量和线程本地变量,在引用变量时需要有前缀,this->表示语句内变量,self->表示线程本地变量。语句内变量类似于C语言的静态局部变量,线程本地变量就是每一个访问该变量的线程都有自己的变量存储,不同的线程互不影响,C语言没有类似的变量与之对应。D语言也支持一种特殊的数据类型,聚合(Aggregations)。为了实现聚合,需要实现相应的聚合函数。用一个实例可以很好地说明何为聚合:
syscall::write:entry
{
@counts[execname] = count();
}
其中@表示该变量是聚合,counts就是聚合变量的名称,用户可以随意取名,execname是一个DTrace变量,表示当前进程名,用户可以根据需要指定不同的值,它是这个聚合的索引,类似于数组的索引,只是它可以为整数,也可以是字符串。count()是聚合函数,它返回@counts[execname] + 1,也就说语句@counts[execname] = count()实现了@counts[execname] 加1。
聚合不同于关联数组,赋值操作必须通过聚合函数来做,索引也不同,关联数组的索引是逗号分割的一个或多个表达式列表。 用户态工具dtrace能把D语言转换成DIF指令。一个D程序的结构如下:
probe-descriptions
/predicate/
{
action-statements
}
probe-descriptions用于指定探测点,它是一个四元组,与前面讲到的探测点四元组的组成一样,包括提供者、模块、函数名、探测点名,只是这四个元素的每一个都可以不提供(那表示匹配任何值),predicate就是预测或条件,它可以没有,action-statements就是动作,它可以是一条或多条D语句。
DTrace能够跟踪用户态应用(Systemtap目前不能),这是由pid提供者实现的。
DTrace还实现了一种特殊的跟踪推测跟踪(Speculative Tracing)。有时消费者在执行探测点时并不知道是否该探测是它所需要的,只有在探测执行后一段时间才能知道,因此预测没法覆盖这种情况。推测跟踪的原理是先执行探测并暂时保存数据在一个临时缓存,如果它发现那些数据是感兴趣的,就提交到真正的缓存,否则就丢弃那些数据。
三、实例解析
下面是一个实际的使用示例:
# dtrace -n syscall:::entry'/pid == 31337/{ @syscalls[probefunc] = count(); }'
dtrace: description 'syscall:::entry' matched 215 probes
^C open 1
lwp_park 2
times 4
fcntl 5
close 6
sigaction 6
read 10
ioctl 14
sigprocmask 106
write 1092
选项-n用于使能指定的探测点,syscall:::entry就是前面提到的四元组,它用于描述哪些探测点将被使能,如果某个元素为空,表示将匹配该元素对应的任何组件。该例子中的四元组指定的探测点为系统调用入口。/括起来的部分就是预测(Predicate),它表示只有对当前进程PID为31337的进程的系统调用执行后面指定的动作,大括号包含的部分为真正的动作,@表示它为Dtrace会聚,会聚是一个变量类型,它被用于采集到的数据的综合处理,一般在性能监视中使用最为普遍,syscalls为会聚的变量名,probefunc为会聚的键,它是Dtrace内嵌变量,表示被探测的函数的名称,你可以把syscalls理解为关联数组,当然可以随意取名,count为Dtrace为会聚实现的内部函数,它返回它被调用的次数,注意,不同的探测点count互不相干。
小结
本文讲解了DTrace以及它的实现原理,并通过一个实际的例子给读者一个直观的认识,如果读者对它有兴趣可以阅读参考资料中的DTrace指南来了解更多详细的信息。本文是系列文章“Linux下的一个全新的性能测量和调式诊断工具 -- Systemtap”之二,有兴趣的读者可以阅读该系列文章之一和三。