By samwan on
通过上一次的介绍,相信大家对DTRACE已经有了一个初步的认识。上一次结束时专门留了一个例子,可能大家第一次看有很多不明白的地方,没有关系,随着我们对DTRACE更多的介绍,很快就会”云开雾散“了。
D语言作为一种编程语言,自然就有其语法、关键字、数据结构、运算符、函数等,我将一一介绍。
D语言中标志符名称与C语言类似,由字母、数字和下划线组成,其中第一个字符必须是字母或者下划线。D语言预留了一些关键字供DTRACE本身使用,关键字不能用做D变量的名称。D关键字列表参阅《》,这里只列出一些常用的。
表1 - 常用DTRACE关键字
关键字 | 描述 |
inline | 编译期间将指定的D变量替换为预定义的值或者表达式,inline可以申明类型 |
sizeof | 计算对象的大小 |
self | 表示将D变量存放在线程(thread)的私有空间里 |
this | 表示D变量的有效范围在this所在的子句内 |
D语言中定义了整数类型和浮点类型,以及用于表示ASCII字符串的string类型。整数类型随机器字长的不同而不同。机器字长可以用命令isainfo -b来查看。
表2 - D整数类型
类型名称 | 32位机器字长 | 64位机器字长 |
char | 1个字节 | 1个字节 |
short | 2个字节 | 2个字节 |
int | 4个字节 | 4个字节 |
long | 4个字节 | 8个字节 |
longlong | 8个字节 | 8个字节 |
一点小知识,C语言中有ILP32和LP64两种数据模型,ILP32指的就是int/long/pointer(指针)是32位,LP64指的是long/pointer是64位。
对于整数类型,又分为带符号(signed)和无符号(unsigned)两种,因为是否带符号决定了对其最高位(most-significant)的解释。无符号整型通常是在相应的整型前面添加unsigned或者u限定符。
表3 - D整数类型别名
类型名称 | 说明 |
int8_t / uint8_t | 1字节带符号整数 / 1字节无符号整数 |
int16_t / uint16_t | 2字节带符号整数 / 2字节无符号整数 |
int32_t / uint32_t | 4字节带符号整数 / 4字节无符号整数 |
int64_t / uint64_t | 8字节带符号整数 / 8字节无符号整数 |
intptr_t / uintptr_t | 大小等于指针的带符号整数 / 大小等于指针的无符号整数 |
D语言中也定义了转义序列如'\\n'表示回车。
D语言中定义了算术运算符、关系运算符、逻辑运算符、按位运算符、赋值运算符、递增和递减运算符、条件表达式(即 ? : 运算符),由于这些运算符及其优先级与C语言基本相同,就不在这里占用篇幅了。D语言也支持”强制类型转换“,即把一种类型转换为另一种兼容类型,比如将指针转换为整数。
除了上面的数据类型,D语言还提供有数组(array)和关联数组(associative array),关联数组中有一种特殊类型叫做聚合(aggregation),在后面会看到。关联数组通过一个称为键(key)的名称来检索数据,用过Perl的朋友相信不会陌生。定义关联数组,只需作以下赋值操作即可:
name[key]=expression;
例如: people["sam.wan",30]=100
D语言中的变量是不需要预定义就可以直接使用的。但是在没有赋值之前,是不能出现在谓词中和赋值运算等号右侧。请看下面的3个例子:
例子1
# dtrace -n 'BEGIN{ a=1;exit(0);}END{printf("a=%d\\n",a);}'dtrace: description 'BEGIN' matched 2 probesCPU ID FUNCTION:NAME 0 1 :BEGIN 0 2 :END a=1例子2# dtrace -n 'BEGIN/a==0/{exit(0);}END{printf("a=%d\\n",a);}'dtrace: invalid probe specifier BEGIN/a==0/{exit(0);}END{printf("a=%d\\n",a);}: in predicate: failed to resolve a: Unknown variable name 例子3# dtrace -n 'BEGIN{ a=a+1;exit(0);}END{printf("a=%d\\n",a);}'dtrace: invalid probe specifier BEGIN{a=a+1;exit(0);}END{printf("a=%d\\n",a);}: in action list: a has not yet been declared or assigned缺省情况下,D语言中定义的变量是全局范围的。在多线程环境中,全局变量是不安全的,因为可能多个线程都会进行访问,因此,D语言引入了线程局部变量标志符self。通过在一个变量前面添加self->修饰,可以将该变量存放在线程自己的局部空间中,这样不会受到其它线程的影响,这种方法对于现在越来越多的并发操作环境十分有利。线程局部变量与全局变量在不同的名称空间(name space)中,因此即使名字相同也不会冲突,比如self->aaa和aaa是两个不同的变量。
特别需要提醒注意的是,在使用完一个变量之后,要将该标量赋值为'0',这样DTRACE就会回收释放其所占用的内存空间。对用一个好的程序员来说,释放空间和分配空间同样重要。
D语言中还有一种特殊的变量叫“子句局部变量(Clause Local)”,通过在变量名前添加this->修饰符完成。子句局部变量的作用域只在其定义的子句内有效。D语言中的变量缺省情况下会被赋值为0,但是子句局部变量除外。
除用户定义的变量外,D语言本身提供了一些非常有用的内置变量,所有这些内置变量都是全局变量。
表4 - DTrace内置变量
类型和名称 | 说明 |
int64_t arg0,...,arg9 | 探测器的前10个输入参数(64位整数)。如果当前探测器参数个数少于10,则未定义的参数值不确定 |
args[] | 与arg0...arg9不同,args[]是有类型的,其类型对应与当前探测器的参数类型。 |
uintptr_t caller | 进入当前探测器之前的当前线程的程序计数器(PC)位置 |
chipid_t chip | 当前物理芯片的CPU芯片标志符 |
processorid_t cpu | 当前CPU的编号 |
cpuinfo_t \*curcpu | 当前CPU的信息(具体结构后面会讲到) |
lwpsinfo_t \*curlwpsinfo | 与当前线程关联的轻量进程(LightWeight Process,LWP)的信息(具体结构见后) |
psinfo_t \*curpsinfo | 与当前线程关联的进程的信息 |
kthread_t \*curthread | 当前线程在内核中的数据结构(kthread_t)的地址,kthread_t的定义在<sys/thread.h>中。 |
string cwd | 当前进程的工作路径(Current Working Directory) |
uint_t epid | 当前探测器的已启用的探测器ID号。 |
int errno | 当前线程最后一次执行的系统调用的返回错误值 |
string execname | 当前进程的名称 |
gid_t gid | 组ID |
uint_t id | 当前探测器的唯一ID号,dtrace -l的第一列 |
uint_t ipl | 触发探测器时当前CPU的中断优先级(Interrupt Priority Level,IPL)。 |
lgrp_id_t lgrp | 当前CPU所属的延迟组(Latency Group)的ID |
pid_t pid | 当前进程号 |
pid_t ppid | 当前进程的父进程 |
string probefunc | 当前探测器的函数名 |
string probemod | 当前探测器的模块名 |
string probename | 当前探测器的名字 |
string probeprov | 当前探测器的提供器名 |
psetid_t pset | 当前CPU所属的处理器集(Processor Set)的ID |
string root | 当前进程的根目录名 |
uint_t stackdepth | 当前线程的栈帧(Stack Frame)的深度。即其调用的函数的层次数。 |
id_t tid | 当前线程的线程ID |
uint64_t timestamp | 以纳秒(ns)为单位的时间计数器。此计数器从过去的任意点递增,仅用于相对计算中。 |
uid_t uid | 当前进程的实际用户ID |
uint64_t uregs[] | 当前线程的用户寄存器值 |
uint64_t vtimestamp | 以纳秒(ns)为单位的时间计数器,实际是当前线程在CPU中已运行的时间减去DTrace谓词和操作所花费的时间。同timestamp一样,仅用于相对计算。 |
uint64_t walltimestamp | 自1970年1月1日00:00世界标准时间以来的纳秒数。 |
Dtrace还可以使用反引号(backquote `)访问操作系统内核中定义的变量,但不能进行修改。
内核中定义的变量可以通过查看内核的变量符号表(Name Symbol)来找到。
#echo "::nm"|mdb -k|more
比如我们想通过DTrace来查看每秒钟freemem的值。表示当前系统中可用的内存页数
#dtrace -qn 'tick-1sec{printf("%d pages of freemem\\n",`freemem)}'
由于Solaris可用动态加载模块,各个模块可能有相同的变量名,为了避免冲突,可用使用模块名来区分,比如访问a模块的x变量:a`x
DTrace还提供操作和子例程。
如果DTrace子句为空,则会采用缺省操作。缺省操作即显示已启用的探测器的标志符。
数据记录操作
数据记录操作总会往指定的缓冲区中放入数据。
void trace(expression) 将expression的结果放到指定的缓冲区(在后面会提到)。void tracemem(address,size_t nbytes),从address地址复制nbytes的内容到指定缓冲区。
void printf(string format,...) 格式化输出。具体格式可用参见printf(3C)手册页。
void printa(aggregation)/void printa(string format,aggregation) 显示及格式化聚合(在后面会提到)
void stack(void)/void stack(int nframes) 将指定长度的栈帧记录拷贝到指定的缓冲区。
void ustack(int nframes,int size)/void ustack(int nframes)/void ustack(void),同上,只是操作的是用户栈
破坏性操作
void stop(void) 停止触发当前探测器的进程
void raise(int signal) 将指定的信号signal发送至触发当前探测器的进程
void copyout(void \*buf,uintptr_t addr,size_t nbytes) 从buf地址拷贝nbytes字节到当前进程的addr地址处。
void copyoutstr(string str,uintptr_t addr,size_t maxlen) 将字符串string拷贝到当前进程的addr地址处
void system(string program,..) 执行程序
内核破坏性操作(下面的操作将会影响整个系统的运行)
void breakpoint(void) 发生一个内核断点
void panic(void) 触发panic()操作(这个相信大家都再熟悉不过了)
void chill(int nanoseconds) DTrace执行nanoseconds时间的spin操作(循环),如果nanoseconds> 500milliseconds,则会失败。
特殊操作
推测性操作(Speculative Actions),有speculate(),commit(),discard(),在后面会提到。
void exit(int status) 立即停止DTrace跟踪。
子例程
与操作不同,子例程只会影响DTrace的内部状态。
void \*alloca(size_t size) 分配size字节的临时空间,返回一个8字节对齐的指针。
string basename(char \*str) 从str中去除前缀和目录名
void bcopy(void \*src,void \*dest,sizt_t size) 从src拷贝size字节到dest。
string cleanpath(char \*str) 去除str中的/./和/../等
void \*copyin(uintptr_t addr,size_t size) 从用户地址空间addr处拷贝size字节到Dtrace临时缓冲区中,并返回缓冲区地址。
string \*copyinstr(uintptr_t addr) 从用户地址空间addr除拷贝已null结尾的ASCII字符串到Dtrace临时缓冲区,并返回缓冲区地址。
void copyinto(uintptr_t addr,size_t size,void \*dest) 从用户地址空间addr处拷贝size字节到Dtrace临时缓冲区的dest处。
string dirname(char \*str) 返回str的目录名
size_t msgdsize(mblk_t \*mp) 返回mp指向的数据消息的字节数
size_t msgsize(mblk_t \*mp) 返回mp消息字节数
int mutex_owned(kmutex_t \*mutex) 如果当前线程拥有互斥锁mutex,则返回非零;否则返回0
kthread_t \*mutex_owner(kmutex_t \*mutex) 返回mutex互斥锁的属主的线程数据结构kthread_t的指针。如果没有属主或者该互斥锁是自旋锁(Spin Mutex),则返回null。
int mutex_type_adaptive(kmutex_t \*mutex) 如果mutex是自适应互斥锁(MUTEX_ADAPTIVE类型),则返回非0,否则返回0。
int progenyof(pid_t pid) 如果触发当前探测器的进程是指定进程的子孙,则返回非0
int rand(void) 返回一个伪随机整数
int rw_iswriter(krwlock_t \*rwlock) 如果指定的读写锁rwlock被一个写入者占有或者要求获得,则返回非0,否则返回0
int rw_write_held(krwlock_t \*rwlock) 如果指定的读写锁当前被一个写入者占有,则返回非0,否则返回0
int speculation(void) 为speculate()操作预留一个推测性跟踪缓冲区,并返回这个缓冲区的标志符。
string strjoin(char \*str1,char \*str2) 串联str1和str2到临时空间,并返回其地址。
size_t strlen(string str) 返回指定字串的长度(不包括结尾的空字节null)
看了这么多操作和子例程,是不是有点受打击了?没有关系,慢慢来,先熟悉一下,在今后具体使用时,就会印象深刻。
关于前面的copyin/copyinstr/copyinto子例程再多作一点说明:
在Solaris(UNIX)系统中,用户程序是运行在用户地址空间里面,当用户程序执行系统调用比如open(2)时,才会进入到内核空间执行(我们通常称之为"陷入trap",为了访问用户地址空间的字符串,就必须将其拷贝到内核空间里面来,否则内核找不到相应的地址,就会报错。看下面的一个例子。
我们想知道是什么程序在调用open(2),以及打开什么文件。这里很自然我们会用到syscall提供器的open:entry探测器。此探测器的参数可用从open(2)的手册页查到(所有的syscall提供器提供的探测器都可用在相对应的系统调用手册中查到)
int open(const char \*path, int oflag, /\* mode_t mode \*/);
我们只关心第一个参数arg0,这是一个字符串指针(即将要打开的文件名)。对于字符串指针,可以使用"%s"进行格式化输出。
运行一下看看
# ./who_open_what.ddtrace: failed to compile script ./who_open_what.d: line 5: printf( ) argument #4 is incompatible with conversion #3 prototype: conversion: %s prototype: char [] or string (or use stringof) argument: int64_t
错误,为什么,因为传递给内核的是一个用户地址空间的指针,内核无法访问该地址,内核只看到一个指针,因此Dtrace认为格式化用的是"%s",但是传递的却是一个int64_t类型,不匹配。
正确的程序应该是:
再来看看
# ./who_open_what.dnfsmapid[272] opened /etc/default/nfsnfsmapid[272] opened /etc/resolv.confinit[1] opened /etc/inittabinit[1] opened /etc/svc/volatile/init-next.stateinit[1] opened /etc/svc/volatile/init-next.stateinit[1] opened /etc/inittab...