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.
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.
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.
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].
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 |
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:
Al ejecutar DTrace, realice un seguimiento de arg0 del sondeo return en cuestión. arg0 contiene el desplazamiento de la instrucción de devolución de la función.
Una vez detenido DTrace, utilice mdb(1) para consultar la función. Si el desplazamiento del seguimiento contiene una llamada a otra función en lugar de una instrucción para la devolución de la función, entonces se ha efectuado una optimización de llamada final en esa llamada.
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.
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.
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.
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.
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.
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.
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.
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.