Solaris 动态跟踪指南

第 20 章 fbt 提供器

本章介绍函数边界跟踪 (Function Boundary Tracing, FBT) 提供器,该提供器提供了与进入和返回 Solaris 内核中的大多数函数相关的探测器。函数是程序文本的基本组成单元。在设计完善的系统中,每个函数会对指定的对象或一系列类似对象执行独立和定义完善的操作。所以,即使在最小型的 Solaris 系统中,FBT 也将提供 20,000 个探测器。

与其他 DTrace 提供器类似,在没有显式启用 FBT 提供器时,它不会产生探测效果。启用时,FBT 仅在被探测的函数中产生探测效果。虽然 FBT 实现高度特定于指令集体系结构,但在 SPARC 和 x86 平台上都已实现 FBT。对于每一个指令集,都有少量函数不调用其他函数,且由无法通过 FBT 检测的编译器(所谓的叶函数)高度优化。DTrace 中不存在这些函数的探测器。

要有效使用 FBT 探测器,需要对操作系统的实现有所了解。所以,建议您仅在开发内核软件或者其他提供器不满足要求时,才使用 FBT。其他 DTrace 提供器(包括 syscallschedprocio)都可用于回答大多数系统分析问题,不需要了解操作系统实现的知识。

探测器

在内核中大多数函数的边界,FBT 都提供了探测器。进入函数和从函数返回都会跨越函数边界。因此,FBT 为内核中的每个函数提供了两个探测器:一个用于进入函数时,一个用于从函数返回时。这两个探测器的名称分别为 entryreturn。函数名称和模块名称都指定为探测器的一部分。所有 FBT 探测器都指定了函数名称和模块名称。

探测器参数

entry 探测器

entry 探测器的参数与相应操作系统内核函数的参数相同。可以使用 args[] 数组以键入的方式访问这些参数。可以使用 arg0 ..argn 变量,以访问 int64_t 参数的方式访问这些参数。

return 探测器

虽然给定的函数仅有一个进入点,但它可能有许多不同的位置可返回到其调用方。通常,您可能只关心函数所返回的值或函数是否返回,而不关心所采用的具体返回路径。因此,FBT 将函数的多个返回位置收集到单个 return 探测器中。如果您对确切的返回路径感兴趣,可以检查 return 探测器的 args[0] 值,该值表示函数文本中返回指令的偏移量(以字节为单位)。

如果函数具有返回值,则该返回值存储在 args[1] 中。如果函数不具有返回值,则不会定义 args[1]

示例

可以使用 FBT 轻松地了解内核的实现。以下示例脚本记录来自任何 xclock 进程的第一个 ioctl(2),以及一直到内核的后续代码路径:

/*
 * To make the output more readable, we want to indent every function entry
 * (and unindent every function return).  This is done by setting the
 * "flowindent" option.
 */
#pragma D option flowindent

syscall::ioctl:entry
/execname == "xclock" && guard++ == 0/
{
	self->traceme = 1;
	printf("fd: %d", arg0);
}

fbt:::
/self->traceme/
{}

syscall::ioctl:return
/self->traceme/
{
	self->traceme = 0;
	exit(0);
}

运行此脚本将会生成与以下示例类似的输出:


# dtrace -s ./xioctl.d
dtrace: script './xioctl.d' matched 26254 probes
CPU FUNCTION                                 
  0  => ioctl                                 fd: 3
  0    -> ioctl                               
  0      -> getf                              
  0        -> set_active_fd                   
  0        <- set_active_fd                   
  0      <- getf                              
  0      -> fop_ioctl                         
  0        -> sock_ioctl                      
  0          -> strioctl                      
  0            -> job_control_type            
  0            <- job_control_type            
  0            -> strcopyout                  
  0              -> copyout                   
  0              <- copyout                   
  0            <- strcopyout                  
  0          <- strioctl                      
  0        <- sock_ioctl                      
  0      <- fop_ioctl                         
  0      -> releasef                          
  0        -> clear_active_fd                 
  0        <- clear_active_fd                 
  0        -> cv_broadcast                    
  0        <- cv_broadcast                    
  0      <- releasef                          
  0    <- ioctl                               
  0  <= ioctl

此输出说明,在看上去与套接字关联的文件说明符中,xclock 进程调用了 ioctl()

尝试了解内核驱动程序时,也可以使用 FBT。例如,ssd(7D) 驱动程序有很多代码路径,通过这些路径可以返回 EIO。使用 FBT 可以轻松地确定导致错误的准确代码路径,如下例所示:

fbt:ssd::return
/arg1 == EIO/
{
	printf("%s+%x returned EIO.", probefunc, arg0);
}

为了获取所返回的任何一个 EIO 的更多信息,您可能希望推理跟踪所有 fbt 探测器,然后根据特定函数的返回值执行 commit()(或 discard())函数。有关推理跟踪的详细信息,请参见第 13 章

或者,您可以使用 FBT 来了解指定模块中调用的函数。以下示例列出了 UFS 中调用的所有函数。


# dtrace -n fbt:ufs::entry'{@a[probefunc] = count()}'
dtrace: description 'fbt:ufs::entry' matched 353 probes
^C
  ufs_ioctl                                                         1
  ufs_statvfs                                                       1
  ufs_readlink                                                      1
  ufs_trans_touch                                                   1
  wrip                                                              1
  ufs_dirlook                                                       1
  bmap_write                                                        1
  ufs_fsync                                                         1
  ufs_iget                                                          1
  ufs_trans_push_inode                                              1
  ufs_putpages                                                      1
  ufs_putpage                                                       1
  ufs_syncip                                                        1
  ufs_write                                                         1
  ufs_trans_write_resv                                              1
  ufs_log_amt                                                       1
  ufs_getpage_miss                                                  1
  ufs_trans_syncip                                                  1
  getinoquota                                                       1
  ufs_inode_cache_constructor                                       1
  ufs_alloc_inode                                                   1
  ufs_iget_alloced                                                  1
  ufs_iget_internal                                                 2
  ufs_reset_vnode                                                   2
  ufs_notclean                                                      2
  ufs_iupdat                                                        2
  blkatoff                                                          3
  ufs_close                                                         5
  ufs_open                                                          5
  ufs_access                                                        6
  ufs_map                                                           8
  ufs_seek                                                         11
  ufs_addmap                                                       15
  rdip                                                             15
  ufs_read                                                         15
  ufs_rwunlock                                                     16
  ufs_rwlock                                                       16
  ufs_delmap                                                       18
  ufs_getattr                                                      19
  ufs_getpage_ra                                                   24
  bmap_read                                                        25
  findextent                                                       25
  ufs_lockfs_begin                                                 27
  ufs_lookup                                                       46
  ufs_iaccess                                                      51
  ufs_imark                                                        92
  ufs_lockfs_begin_getpage                                        102
  bmap_has_holes                                                  102
  ufs_getpage                                                     102
  ufs_itimes_nolock                                               107
  ufs_lockfs_end                                                  125
  dirmangled                                                      498
  dirbadname                                                      498

如果您知道内核函数的用途或参数,可以使用 FBT 来了解调用函数的方式或原因。例如,putnext(9F) 接受指向 queue(9S) 结构的指针作为其第一个成员。queue 结构的 q_qinfo 成员是指向 qinit(9S) 结构的指针。qinit 结构的 qi_minfo 成员具有一个指向 module_info(9S) 结构(其 mi_idname 成员中包含模块名称)的指针。以下示例在 putnext 中使用 FBT 探测器将这些信息收集到一起,以便根据模块名称跟踪 putnext(9F) 调用:

fbt::putnext:entry
{
	@calls[stringof(args[0]->q_qinfo->qi_minfo->mi_idname)] = count();
}

运行上面的脚本将会生成与以下示例类似的输出:


# dtrace -s ./putnext.d
^C

  iprb                                                              1
  rpcmod                                                            1
  pfmod                                                             1
  timod                                                             2
  vpnmod                                                            2
  pts                                                              40
  conskbd                                                          42
  kb8042                                                           42
  tl                                                               58
  arp                                                             108
  tcp                                                             126
  ptm                                                             249
  ip                                                              313
  ptem                                                            340
  vuid2ps2                                                        361
  ttcompat                                                        412
  ldterm                                                          413
  udp                                                             569
  strwhead                                                        624
  mouse8042                                                       726

也可以使用 FBT 确定特定函数花费的时间。以下示例说明了如何确定 DDI 延迟例程 drv_usecwait(9F)delay(9F) 的调用方。

fbt::delay:entry,
fbt::drv_usecwait:entry
{
	self->in = timestamp
}

fbt::delay:return,
fbt::drv_usecwait:return
/self->in/
{
	@snoozers[stack()] = quantize(timestamp - self->in);
	self->in = 0;
}

在引导期间运行此示例脚本特别有意义。第 36 章介绍了在系统引导期间执行匿名跟踪的过程。重新引导时,您可能会看到与以下示例类似的输出:


# dtrace -ae

              ata`ata_wait+0x34
              ata`ata_id_common+0xf5
              ata`ata_disk_id+0x20
              ata`ata_drive_type+0x9a
              ata`ata_init_drive+0xa2
              ata`ata_attach+0x50
              genunix`devi_attach+0x75
              genunix`attach_node+0xb2
              genunix`i_ndi_config_node+0x97
              genunix`i_ddi_attachchild+0x4b
              genunix`devi_attach_node+0x3d
              genunix`devi_config_one+0x1d0
              genunix`ndi_devi_config_one+0xb0
              devfs`dv_find+0x125
              devfs`devfs_lookup+0x40
              genunix`fop_lookup+0x21
              genunix`lookuppnvp+0x236
              genunix`lookuppnat+0xe7
              genunix`lookupnameat+0x87
              genunix`cstatat_getvp+0x134

           value  ------------- Distribution ------------- count    
            2048 |                                         0        
            4096 |@@@@@@@@@@@@@@@@@@@@@                    4105     
            8192 |@@@@                                     783      
           16384 |@@@@@@@@@@@@@@                           2793     
           32768 |                                         16       
           65536 |                                         0


              kb8042`kb8042_wait_poweron+0x29
              kb8042`kb8042_init+0x22
              kb8042`kb8042_attach+0xd6
              genunix`devi_attach+0x75
              genunix`attach_node+0xb2
              genunix`i_ndi_config_node+0x97
              genunix`i_ddi_attachchild+0x4b
              genunix`devi_attach_node+0x3d
              genunix`devi_config_one+0x1d0
              genunix`ndi_devi_config_one+0xb0
              genunix`resolve_pathname+0xa5
              genunix`ddi_pathname_to_dev_t+0x16
              consconfig_dacf`consconfig_load_drivers+0x14
              consconfig_dacf`dynamic_console_config+0x6c
              consconfig`consconfig+0x8
              unix`stubs_common_code+0x3b

           value  ------------- Distribution ------------- count    
          262144 |                                         0        
          524288 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@      221      
         1048576 |@@@@                                     29       
         2097152 |                                         0        


              usba`hubd_enable_all_port_power+0xed
              usba`hubd_check_ports+0x8e
              usba`usba_hubdi_attach+0x275
              usba`usba_hubdi_bind_root_hub+0x168
              uhci`uhci_attach+0x191
              genunix`devi_attach+0x75
              genunix`attach_node+0xb2
              genunix`i_ndi_config_node+0x97
              genunix`i_ddi_attachchild+0x4b
              genunix`i_ddi_attach_node_hierarchy+0x49
              genunix`attach_driver_nodes+0x49
              genunix`ddi_hold_installed_driver+0xe3
              genunix`attach_drivers+0x28

           value  ------------- Distribution ------------- count    
        33554432 |                                         0        
        67108864 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 3        
       134217728 |                                         0

尾部调用优化

当一个函数以调用另一个函数而结束时,编译器可能会进行尾部调用优化,优化后,被调用的函数将重用调用方的栈帧。此过程通常用于 SPARC 体系结构中,在此情况下,编译器在被调用的函数中重用调用方的注册窗口,以便使注册窗口的压力降到最低。

进行此优化会使调用函数的 return 探测器在被调用函数的 entry 探测器之前触发。这种触发顺序会导致比较严重的混乱情况。例如,如果要记录从某个特定函数调用的所有函数,以及此函数调用的所有函数,则可以使用以下脚本:

fbt::foo:entry
{
	self->traceme = 1;
}

fbt:::entry
/self->traceme/
{
	printf("called %s", probefunc);
}

fbt::foo:return
/self->traceme/
{
	self->traceme = 0;
}

但是,如果 foo() 以优化的尾部调用结束,那么,在尾部调用的函数以及它调用的任何函数都不会被捕获。不能即时动态取消内核优化,DTrace 不希望总是考虑如何虚构代码结构。所以,应清楚什么时候可以使用尾部调用优化。

可以在类似以下示例的源代码中使用尾部调用优化:

	return (bar());

或者在类似以下示例的源代码中使用尾部调用优化:

	(void) bar();
	return;

相反地,结尾方式与以下示例类似的函数源代码不能优化对 bar() 的调用,因为对 bar() 的调用不是尾部调用:

	bar();
	return (rval);

可以使用以下方法确定是否已对某个调用进行了尾部调用优化:

由于指令集体系结构,尾部调用优化在 SPARC 系统上要比在 x86 系统上更常见。以下示例使用 mdb 发现内核的 dup() 函数中的尾部调用优化:


# dtrace -q -n fbt::dup:return'{printf("%s+0x%x", probefunc, arg0);}'

运行此命令时,将运行执行 dup(2) 的程序(如 bash 进程)。上面的命令会提供与以下示例类似的输出:


dup+0x10
^C

现在使用 mdb 检查函数:


# echo "dup::dis" | mdb -k
dup:                            sra       %o0, 0, %o0
dup+4:                          mov       %o7, %g1
dup+8:                          clr       %o2
dup+0xc:                        clr       %o1
dup+0x10:                       call      -0x1278       <fcntl>
dup+0x14:                       mov       %g1, %o7

该输出说明,dup+0x10 是对 fcntl() 函数的调用而不是 ret 指令。所以,fcntl() 就是尾部调用优化的一个示例。

汇编函数

您可能会发现,有时似乎只进入某些函数但却不返回,或者出现相反的情况。这种函数很稀少,通常是手动编码的汇编例程,这些例程通常分支到其他手动编码的汇编函数中间。这些函数不应妨碍分析:分支到的目标函数仍然必须返回到其来源函数的调用方。即,如果启用所有 FBT 探测器,则应看到进入某个函数,同时从相同栈深度的另一个函数返回。

指令集限制

一些函数无法通过 FBT 进行检测。不可检测函数的准确特性特定于指令集体系结构。

x86 限制

在 x86 系统上不创建栈帧的函数无法通过 FBT 进行检测。因为 x86 的寄存器集非常小,大多数函数必须将数据放入栈中,从而创建栈帧。但是,一些 x86 函数不创建栈帧,因此无法对这些函数进行检测。x86 平台上无法进行检测的函数的实际数量不固定,但通常少于百分之五。

SPARC 限制

无法通过 FBT 对 SPARC 系统上以汇编语言进行手动编码的叶例程进行检测。大多数内核用 C 语言编写,所有用 C 语言编写的函数都可以通过 FBT 进行检测。

断点交互

FBT 通过动态修改内核文本进行工作。由于内核断点也通过修改内核文本进行工作,所以如果在装入 DTrace 之前将内核断点放在入口或返回位置,FBT 将拒绝提供用于函数的探测器,即使随后删除了内核断点也是如此。如果在装入 DTrace 之后放置内核断点,则内核断点和 DTrace 探测器将对应于文本中相同的位置。在此情况下,断点将首先触发,然后在调试器恢复内核时,探测器将触发。建议不要同时使用内核断点和 DTrace。如果必须使用断点,请改为使用 DTrace breakpoint() 操作。

模块装入

Solaris 内核可以动态装入和卸载内核模块。当装入 FBT 并动态装入模块时,FBT 将自动提供与新模块关联的新探测器。如果装入的模块未启用 FBT 探测器,则可以卸载模块;卸载模块时,相应的探测器将被破坏。如果装入的模块已启用 FBT 探测器,则该模块将被视为正忙,无法卸载该模块。

稳定性

FBT 提供器使用 DTrace 的稳定性机制描述其稳定性,如下表所示。有关稳定性机制的更多信息,请参见第 39 章

元素 

名称稳定性 

数据稳定性 

相关性类 

提供器 

发展中 

发展中 

ISA

模块 

专用 

专用 

未知 

功能 

专用 

专用 

未知 

名称 

发展中 

发展中 

ISA

参数 

专用 

专用 

ISA

FBT 显示内核实现时,所有关于实现的内容都不“稳定”-模块和函数名称以及数据稳定性都明确地显示为“专用”。提供器和名称的数据稳定性为“发展中”,但是所有其他数据稳定性都为“专用”:它们是当前实现的产物。FBT 的相关类为 ISA:虽然在当前的所有指令集体系结构中都可以使用 FBT,但不能保证在将来的任意指令集体系结构中都可以使用 FBT。