Guía de seguimiento dinámico de Solaris

Capítulo 20 Proveedor fbt

En este capítulo se describe el proveedor Seguimiento de límite de función (FBT, Function Boundary Tracing), que proporciona sondeos asociados a la entrada y devolución desde la mayoría de las funciones del núcleo de Solaris. La función es la unidad fundamental del texto del programa. En un sistema diseñado correctamente, cada función realiza una operación claramente definida y discreta en un objeto especificado o en una serie de objetos similares. Por lo tanto, incluso en los sistemas Solaris de menor tamaño, FBT proporcionará aproximadamente 20.000 sondeos.

De forma similar a los demás proveedores de DTrace, FBT no presenta ningún efecto de sondeo a menos que se habilite explícitamente. Cuando está habilitado, FBT sólo provoca efectos de sondeo en funciones sondeadas. Aunque la implementación FBT es específica de la arquitectura del conjunto de instrucciones, FBT se ha implementado tanto en las plataformas SPARC como en las plataformas x86. En cada conjunto de instrucciones, hay un número reducido de funciones que no llaman a otras funciones y que están sumamente optimizadas por el compilador (las denominadas funciones hoja) que FBT no puede instrumentar. Los sondeos de estas funciones no están presentes en DTrace.

Para utilizar de forma eficaz los sondeos FBT , es necesario conocer la implementación del sistema operativo. Por lo tanto, es recomendable que sólo utilice FBT al desarrollar software del núcleo o cuando el uso de los otros proveedores no ofrezca suficientes resultados. Los demás proveedores de DTrace, incluidos syscall, sched, proc e io, pueden utilizarse para responder a la mayoría de las preguntas de análisis del sistema sin necesidad de contar con conocimientos sobre la implementación del sistema operativo.

Sondeos

FBT proporciona un sondeo en el límite de la mayoría de funciones del núcleo. El límite de una función se supera durante la entrada a la función y la devolución de la función. Por lo tanto, FBT proporciona dos funciones para cada función del núcleo: una durante la entrada a la función y otra durante la devolución de la función. Estos sondeos reciben el nombre de entry y return respectivamente. El nombre de la función y el nombre del módulo se especifican como parte del sondeo. Todos los sondeos de FBT especifican un nombre de función y un nombre de módulo.

Argumentos de los sondeos

Sondeos entry

Los argumentos de los sondeos entry son iguales a los argumentos de la función correspondiente del núcleo del sistema operativo. Puede accederse a estos argumentos mediante escritura utilizando la matriz args[]. También se puede acceder a ellos como int64_tutilizando arg0 .. Variables argn.

Sondeos return

Aunque una función específica sólo tiene un punto de entrada, puede contar con muchos puntos distintos cuando se devuelve al emisor de la llamada. Normalmente, el interés del usuario se centra en el valor que ha devuelto la función o en la propia devolución de la función en lugar de en la ruta de devolución específica utilizada. Por lo tanto, FBT recopila los diversos sitios de devolución de una función en un único sondeo return. Si le interesa conocer la ruta de devolución exacta, puede examinar el valor args[0] del sondeo return, que indica el desplazamiento (en bytes) de la instrucción de devolución en el texto de la función.

Si la función tiene un valor de devolución, éste se almacena en args[1]. Si, por el contrario, no tiene ningún valor de devolución, no se define args[1].

Ejemplos

Puede utilizar FBT para examinar fácilmente la implementación del núcleo. La siguiente secuencia de comandos de ejemplo registra el primer elemento ioctl(2) de cualquier proceso xclock y, a continuación, sigue la ruta de código siguiente a través del núcleo:

/*
 * 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);
}

La ejecución de esta secuencia de comandos devuelve una salida similar a la que se muestra en el siguiente ejemplo:


# 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

La salida muestra que un proceso xclock ha llamado a ioctl() en un descriptor de archivo que, aparentemente, está asociado a un socket.

Puede utilizar FBT para tratar de comprender los controladores del núcleo. Por ejemplo, el controlador ssd(7D) tiene varias rutas de código mediante las que se puede devolver EIO. FBT puede utilizarse fácilmente para determinar la ruta de código precisa que ha provocado una condición de error, como se muestra en el siguiente ejemplo:

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

Para obtener más información sobre cualquier devolución de EIO, es posible que desee realizar un seguimiento especulativo de todos los sondeos de fbt y, a continuación, efectuar la acción commit()(o discard()) en función del valor de devolución de la función específica. Consulte el Capítulo 13Seguimiento especulativo para obtener más información sobre el seguimiento especulativo.

También puede utilizar FBT para conocer las funciones a las que se ha llamado en el módulo especificado. En el siguiente ejemplo, se muestran las funciones a las que se ha llamado en 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

Si conoce la finalidad o los argumentos de una función del núcleo, puede utilizar FBT para saber cómo o por qué se ha llamado a la función. Por ejemplo, putnext(9F) utiliza un puntero a una estructura queue(9S) como primer miembro. El miembro q_qinfo de la estructura queue es un puntero a una estructura qinit(9S). El miembro qi_minfo de la estructura qinit presenta un puntero a una estructura module_info(9S), que contiene el nombre del módulo en el miembro mi_idname. En el ejemplo siguiente, se agrupa esta información mediante el sondeo de FBT en putnext para realizar un seguimiento de las llamadas de putnext(9F) por nombre de módulo:

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

La ejecución de la secuencia de comandos anterior devuelve una salida similar al siguiente ejemplo:


# 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

Puede utilizar también FBT para determinar el tiempo transcurrido en una función determinada. En el ejemplo siguiente se muestra cómo determinar cuáles son los emisores de las llamadas de las rutinas de retraso DDI drv_usecwait(9F) y 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;
}

Esta secuencia de comandos de ejemplo es particularmente interesante ejecutarla durante el arranque. El Capítulo 36Seguimiento anónimo describe el procedimiento para realizar un seguimiento anónimo durante el arranque del sistema. Al reiniciar, es posible que vea una salida similar a la siguiente:


# 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

Optimización de llamada final

Cuando una función finaliza llamando a otra función, el compilador puede utilizar la optimización de llamada final, en la que la función a la que se está llamando reutiliza el marco de pila del emisor de la llamada. Este procedimiento se suele usar con mayor frecuencia en la arquitectura SPARC cuando el compilador reutiliza la ventana de registro del emisor de la llamada en la función a la que se está llamando para reducir al mínimo la presión de la ventana de registro.

La presencia de esta optimización provoca que el sondeoreturn de la función que realiza la llamada se active antes que el sondeo entry de la función a la que se ha llamado. Este orden puede provocar cierta confusión. Por ejemplo, si desea registrar todas las funciones a las que se ha llamado desde una determinada función y todas las funciones a las que llama esta función, debe utilizar la siguiente secuencia de comandos:

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

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

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

Sin embargo, foo() termina con una llamada final optimizada, la última función a la que se ha llamado y, por lo tanto, todas las funciones a las que ésta llama no se capturarán. No se puede anular dinámicamente la optimización del núcleo sobre la marcha; además, DTrace no desea participar en una falsedad acerca de cómo está estructurado el código. Por lo tanto, debería ser consciente de cuándo se puede utilizar la optimización de llamada final.

Es probable que la optimización de llamada final se utilice en un código fuente similar al del siguiente ejemplo:

	return (bar());

O en un código fuente similar al del ejemplo siguiente:

	(void) bar();
	return;

Por el contrario, no se pueden optimizar las llamadas a bar() del código fuente de la función que finaliza como el siguiente ejemplo, debido a que la llamada a bar() no es una llamada final:

	bar();
	return (rval);

Puede determinar si se ha efectuado una optimización de llamada final en una llamada mediante la siguiente técnica:

Debido a la arquitectura del conjunto de instrucciones, la optimización de llamada final es mucho más frecuente en los sistemas SPARC que en los sistemas x86. En el siguiente ejemplo, se utiliza mdb para detectar la optimización de llamada final en la función dup() del núcleo:


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

Mientras se ejecuta este comando, ejecute un programa que realice una operación dup(2), por ejemplo, un proceso bash. El comando anterior debería proporcionar una salida similar a la siguiente:


dup+0x10
^C

Ahora, examine la función con 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

La salida muestra que dup+0x10 es una llamada a la función fcntl() y no una instrucción ret. Por lo tanto, la llamada a fcntl() es un ejemplo de optimización de llamada final.

Funciones de ensamblaje

Es posible que haya observado funciones que aparentemente entran, pero que no se devuelven o viceversa. Estas funciones poco frecuentes son normalmente rutinas de ensamblaje codificadas que se ramifican en medio de otras funciones de ensamblaje codificadas. Estas funciones no deberían impedir el análisis: la función de destino de la bifurcación debería devolverse de todos modos al emisor de la llamada de la función de origen de la bifurcación. Es decir, si se habilitan todos los sondeos de FBT, debería ver la entrada a una función y la devolución de otra función en el mismo nivel de profundidad de la pila.

Limitaciones de los conjuntos de instrucciones

FBT no puede instrumentar algunas funciones. La naturaleza exacta de las funciones que no se pueden instrumentar es específica de la arquitectura del conjunto de instrucciones.

Limitaciones de x86

FBT no puede instrumentar las funciones que no crean ningún marco de pila en los sistemas x86. Como el conjunto de registros para x86 es extraordinariamente pequeño, la mayoría de las funciones deben incluir datos en la pila y, por lo tanto, crear un marco de pila. Sin embargo, algunas funciones de x86 no crean ningún marco de pila, por lo que no pueden instrumentarse. Las cifras reales pueden variar, pero normalmente no se pueden instrumentar menos de un cinco por ciento de las funciones en la plataforma x86.

Limitaciones de SPARC

FBT no puede instrumentar las rutinas de hoja codificadas con un lenguaje de ensamblaje en los sistemas SPARC. La mayor parte del núcleo se escribe con el lenguaje C y FBT puede instrumentar todas las funciones escritas en C.

Interacción de los puntos de interrupción

FBT funciona mediante la modificación dinámica del texto del núcleo. Como los puntos de interrupción también funcionan mediante la modificación del texto del núcleo, si se coloca un punto de interrupción en un sitio de entrada o devolución antes de cargar DTrace, FBT no proporcionará ningún sondeo para la función, aunque el punto de interrupción del núcleo se elimine posteriormente. Si se coloca el punto de interrupción después de cargar DTrace, tanto el punto de interrupción del núcleo como el sondeo de DTrace se corresponderán con el mismo punto en el texto. En este caso, el punto de interrupción se activará primero y, a continuación, se activará el sondeo cuando el depurador reanude la actividad del núcleo. Es recomendable que los puntos de interrupción del núcleo no se utilicen de forma simultánea con DTrace. Si son necesarios los puntos de interrupción, utilice en su lugar la acción breakpoint() de DTrace.

Carga del módulo

El núcleo de Solaris puede cargar y descargar dinámicamente los módulos del núcleo. Al cargar FBT y cargar dinámicamente un módulo, FBT proporciona automáticamente nuevos sondeos asociados al nuevo módulo. Si un módulo cargado ha deshabilitado los sondeos FBT, es posible que se descargue el módulo; los sondeos correspondientes se destruirán a medida que se descarga el módulo. Si, por el contrario, un módulo cargado ha habilitado los sondeos de FBT, el módulo se considera ocupado y no puede descargarse.

Estabilidad

El proveedor FBT utiliza el mecanismo de estabilidad de DTrace para describir sus características de estabilidad, como se muestra en la siguiente tabla: Para obtener más información sobre el mecanismo de estabilidad, consulte el Capítulo 39Estabilidad.

Elemento 

Estabilidad del nombre 

Estabilidad de los datos 

Clase de dependencia 

Proveedor 

Evolutivo 

Evolutivo 

ISA

Módulo 

Privado 

Privado 

Desconocido 

Función 

Privado 

Privado 

Desconocido 

Nombre 

Evolutivo 

Evolutivo 

ISA

Argumentos 

Privado 

Privado 

ISA

Mientras FBT muestra la implementación del núcleo, ningún elemento relacionado con éste es Stable, y la estabilidad de datos y nombres del módulo y la función se establece explícitamente como Private. La estabilidad de datos del proveedor y el nombre se encuentran Evolving (en evolución), pero todos los demás niveles de estabilidad son Private (privados): son artefactos de la implementación actual. La clase de dependencia de FBT es ISA: mientras que FBT está disponible en todas las arquitecturas de conjuntos de instrucciones actuales, no hay ninguna garantía de que FBT vaya a estar disponible en futuras arquitecturas de conjuntos de instrucciones.