DTrace es una herramienta extremedamente potente para comprender el comportamiento de los procesos de usuario. DTrace desempeña un papel excepcional a la hora de depurar o analizar problemas de rendimiento o simplemente para conocer el comportamiento de una aplicación compleja. Este capítulo se centra en las utilidades de DTrace relevantes para el seguimiento de la actividad de los procesos de usuario y proporciona ejemplos que ilustran su uso.
La interacción de DTrace con los procesos varía un poco con respecto a los depuradores más tradicionales o las herramientas de observabilidad. Muchas de estas herramientas parece que se ejecutan dentro del ámbito de los procesos, permitiendo que los usuarios establezcan que los punteros dejen de hacer referencia directa a las variables de programa. En lugar de parecer que se ejecutan dentro de un proceso o como una parte de él, los sondeos de DTrace se ejecutan en el núcleo de Solaris. Para acceder a los datos de proceso, un sondeo debe usar las subrutinas copyin() o copyinstr() para copiar los datos de proceso de usuario en el espacio de dirección del núcleo.
Por ejemplo, considere la siguiente llamada de sistema write(2):
ssize_t write(int fd, const void *buf, size_t nbytes);
El siguiente programa escrito en D ilustra un intento incorrecto de imprimir el contenido de una cadena pasada a la llamada de sistema write(2):
syscall::write:entry { printf("%s", stringof(arg1)); /* incorrect use of arg1 */ }
Si trata de ejecutar esta secuencia de comandos, DTrace generará mensajes de error similares al siguiente ejemplo:
dtrace: error on enabled probe ID 1 (ID 37: syscall::write:entry): \ invalid address (0x10038a000) in action #1 |
La variable arg1, que contiene el valor del parámetro buf, es una dirección que hace referencia a la memoria en el proceso que ejecuta la llamada de sistema. Para leer la cadena de esta dirección, use la subrutina copyinstr() y registre el resultado con la acción printf():
syscall::write:entry { printf("%s", copyinstr(arg1)); /* correct use of arg1 */
El resultado de esta secuencia de comandos muestra todas las cadenas que se están pasando a la llamada de sistema write(2). Ocasionalmente, no obstante, puede observarse una salida irregular, semejante a la del siguiente ejemplo:
0 37 write:entry mada��� |
La subrutina copyinstr() actúa en un argumento de entrada que es la dirección del usuario de una cadena ASCII con una terminación nula. Sin embargo, las memorias intermedias pasadas a la llamada de sistema write(2) pueden hacer referencia a datos binarios en lugar de a cadenas ASCII. Para imprimir sólo las cadenas que el llamador desee, use la subrutina copyin(), que considera un tamaño como su segundo argumento:
syscall::write:entry { printf("%s", stringof(copyin(arg1, arg2))); }
Observe que el operador stringof es necesario para que DTrace convierta correctamente en una cadena los datos de usuario recuperados usando copyin. () El uso de stringof no es necesario cuando se emplea copyinstr(), ya que esta función siempre devuelve el tipo string.
Las subrutinas copyin() y copyinstr() no se pueden leer desde direcciones de usuario que no se hayan tocado todavía, por lo que incluso una dirección válida puede provocar un error si la página que contiene esa dirección no ha experimentado errores al acceder. Considere el siguiente ejemplo:
# dtrace -n syscall::open:entry'{ trace(copyinstr(arg0)); }' dtrace: description 'syscall::open:entry' matched 1 probe CPU ID FUNCTION:NAME dtrace: error on enabled probe ID 2 (ID 50: syscall::open:entry): invalid address (0x9af1b) in action #1 at DIF offset 52 |
En la salida del ejemplo anterior, la aplicación funcionaba correctamente y la dirección de arg0 era válida, pero hacía referencia a una página a la que no había accedido todavía el proceso correspondiente. Para resolver este problema, espere a que el núcleo o la aplicación usen los datos antes de comenzar el seguimiento. Por ejemplo, puede esperar a que la llamada de sistema muestre su resultado para aplicar copyinstr(), tal y como se ilustra en el siguiente ejemplo:
# dtrace -n syscall::open:entry'{ self->file = arg0; }' \ -n syscall::open:return'{ trace(copyinstr(self->file)); self->file = 0; }' dtrace: description 'syscall::open:entry' matched 1 probe CPU ID FUNCTION:NAME 2 51 open:return /dev/null |
Si efectúa un seguimiento de todas las llamadas realizadas a la llamada de sistema write(2), provocará una cascada de resultados. Cada llamada realizada a write() provoca que el comando dtrace(1M) realice una llamada a write() cuando muestra la salida y así sucesivamente. El bucle de realimentación es un buen ejemplo de cómo el comando dtrace puede interferir con los datos deseados. Puede utilizar un predicado sencillo para impedir que se realice un seguimiento de estos datos no deseados:
syscall::write:entry /pid != $pid/ { printf("%s", stringof(copyin(arg1, arg2))); }
La variable de macro $pid se expande hasta el identificador del proceso que activó los sondeos. La variable pid contiene el identificador del proceso cuyo subproceso se estaba ejecutando en la CPU cuando se activó el sondeo. En consecuencia, el predicado /pid != $pid/ garantiza que la secuencia de comandos no realiza ningún seguimiento de eventos relacionados con la secuencia de comandos en sí.
El proveedor syscall le permite realizar un seguimiento de todas las entradas y las devoluciones de las llamadas de sistema. Las llamadas del sistema pueden ser un buen punto de partida para comprender el comportamiento de un proceso, especialmente si el proceso tarda mucho tiempo en las ejecuciones o queda bloqueado en el núcleo. Puede usar el comando prstat(1M) para ver dónde exactamente emplean el tiempo los procesos:
$ prstat -m -p 31337 PID USERNAME USR SYS TRP TFL DFL LCK SLP LAT VCX ICX SCL SIG PROCESS/NLWP 13499 user1 53 44 0.0 0.0 0.0 0.0 2.5 0.0 4K 24 9K 0 mystery/6 |
Este ejemplo muestra que el proceso está gastando una cantidad grande del tiempo del sistema. Una posible explicación para este comportamiento es que el proceso está realizando una cantidad importante de llamadas del sistema. Puede usar un programa sencillo escrito en D especificado en la línea de comandos para ver qué llamadas del sistema se están produciendo con más frecuencia:
# 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 |
Este informe muestra qué llamadas del sistema se producen con más frecuencia; en este caso se trata de la llamada de sistema write(2). Puede usar el proveedor syscall para examinar en profundidad el origen de todas las llamadas del sistema write():
# dtrace -n syscall::write:entry'/pid == 31337/{ @writes[arg2] = quantize(arg2); }' dtrace: description 'syscall::write:entry' matched 1 probe ^C value ------------- Distribution ------------- count 0 | 0 1 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 1037 2 |@ 3 4 | 0 8 | 0 16 | 0 32 |@ 3 64 | 0 128 | 0 256 | 0 512 | 0 1024 |@ 5 2048 | 0 |
La salida muestra que el proceso está ejecutando muchas llamadas del sistema write() con una cantidad de datos relativamente pequeña. Esta relación puede ser el origen de los problemas de rendimiento de este proceso concreto. En este ejemplo se muestra la metodología general para investigar el comportamiento de las llamadas del sistema.
El seguimiento de la pila del subproceso de un proceso en el momento en que se activa un sondeo concreto suele resultar útil para examinar un problema con mayor detalle. La acción ustack() realiza un seguimiento de la pila del subproceso del usuario. Si, por ejemplo, un proceso que abre varios archivos falla ocasionalmente en la llamada de sistema open(2), podrá usar la acción ustack() para descubrir la ruta de código que ejecuta la acción open() que ha fallado:
syscall::open:entry /pid == $1/ { self->path = copyinstr(arg0); } syscall::open:return /self->path != NULL && arg1 == -1/ { printf("open for '%s' failed", self->path); ustack(); }
Esta secuencia de comandos también ilustra el uso de la variable de macro $1 que toma el valor del primer operando especificado en la línea de comandos de dtrace(1M):
# dtrace -s ./badopen.d 31337 dtrace: script './badopen.d' matched 2 probes CPU ID FUNCTION:NAME 0 40 open:return open for '/usr/lib/foo' failed libc.so.1`__open+0x4 libc.so.1`open+0x6c 420b0 tcsh`dosource+0xe0 tcsh`execute+0x978 tcsh`execute+0xba0 tcsh`process+0x50c tcsh`main+0x1d54 tcsh`_start+0xdc |
La acción ustack() registra los valores del contador del programa (PC ) para la pila y dtrace(1M) resuelve estos valores PC en nombres de símbolos buscando en las tablas de símbolos del proceso. Si dtrace no puede resolver el valor PC en un símbolo, imprimirá el valor como un entero hexadecimal.
Si un proceso se cierra o se termina antes de que los datos de ustack() tengan el formato para la salida, es posible que dtrace no sea capaz de convertir los valores de PC del seguimiento de la pila en nombres de símbolos, por lo que estará obligado a mostrarlos como enteros hexadecimales. Para solucionar esta limitación, especifique un proceso de interés con la opción -c o -p para dtrace. Consulte el Capítulo 14Utilidad dtrace(1M) para obtener más información sobre estas y otras opciones. Si el ID del proceso o el comando no se conocen de antemano, se puede usar el siguiente programa escrito en D de ejemplo para solucionar esta limitación:
/* * This example uses the open(2) system call probe, but this technique * is applicable to any script using the ustack() action where the stack * being traced is in a process that may exit soon. */ syscall::open:entry { ustack(); stop_pids[pid] = 1; } syscall::rexit:entry /stop_pids[pid] != 0/ { printf("stopping pid %d", pid); stop(); stop_pids[pid] = 0; }
La secuencia de comandos anterior detiene un proceso justo antes de que se cierre si la acción ustack() se ha aplicado a un subproceso de dicho proceso. Esta técnica garantiza que el comando dtrace esté disponible para resolver los valores PC en nombres simbólicos. Tenga en cuenta que el valor de stop_pids[pid] se establece en 0 después de que se haya usado para borrar la variable dinámica. Recuerde que debe ejecutar de nuevo los procesos detenidos usando el comando prun(1); de lo contrario, el sistema acumulará demasiados procesos detenidos.
La matriz uregs[] le permite acceder a registros de usuario individuales. En las siguientes tablas se muestran índices de la matriz uregs[] correspondientes a cada una de las arquitecturas de sistema Solaris admitidas.
Tabla 33–1 Constantes uregs[] de SPARC
Constante |
Registrarme |
---|---|
R_G0..R_G7 |
%g0....%g7 registros globales |
R_O0..R_O7 |
%o0....%o7 registros externos |
R_L0..R_L7 |
%l0....%l7 registros locales |
R_I0..R_I7 |
%i0....%i7 en registros |
R_CCR |
%ccr registro de código de condición |
R_PC |
%pc contador del programa |
R_NPC |
%npc siguiente contador del programa |
R_Y |
%y registro de multiplicación/división |
R_ASI |
%asi registro del identificador del espacio de la dirección |
R_FPRS |
%fprs estado de los registros de punto flotante |
Tabla 33–2 Constantes uregs[] de x86
Constante |
Registrarme |
---|---|
R_CS |
%cs |
R_GS |
%gs |
R_ES |
%es |
R_DS |
%ds |
R_EDI |
%edi |
R_ESI |
%esi |
R_EBP |
%ebp |
R_EAX |
%eax |
R_ESP |
%esp |
R_EAX |
%eax |
R_EBX |
%ebx |
R_ECX |
%ecx |
R_EDX |
%edx |
R_TRAPNO |
%trapno |
R_ERR |
%err |
R_EIP |
%eip |
R_CS |
%cs |
R_ERR |
%err |
R_EFL |
%efl |
R_UESP |
%uesp |
R_SS |
%ss |
En las plataformas AMD64, la matriz uregs tiene el mismo contenido que en las plataformas x86, además de los elementos adicionales que figuran en la siguiente tabla:
Tabla 33–3 Constantes uregs[] de amd64
Constante |
Registrarme |
---|---|
R_RSP |
%rsp |
R_RFL |
%rfl |
R_RIP |
%rip |
R_RAX |
%rax |
R_RCX |
%rcx |
R_RDX |
%rdx |
R_RBX |
%rbx |
R_RBP |
%rbp |
R_RSI |
%rsi |
R_RDI |
%rdi |
R_R8 |
%r8 |
R_R9 |
%r9 |
R_R10 |
%r10 |
R_R11 |
%r11 |
R_R12 |
%r12 |
R_R13 |
%r13 |
R_R14 |
%r14 |
R_R15 |
%r15 |
Los alias que aparecen en la siguiente tabla se pueden usar en todas las plataformas:
Tabla 33–4 Constantes uregs[] comunes
Constante |
Registrarme |
---|---|
R_PC |
registro del contador del programa |
R_SP |
registro del puntero de la pila |
R_R0 |
primer código de devolución |
R_R1 |
segundo código de devolución |
El proveedor pid permite realizar un seguimiento de cualquier instrucción en un proceso. A diferencia de otros proveedores, los sondeos pid se crean a petición basándose en las descripciones de sondeos que se encuentran en los programas escritos en D. Como resultado, ningún sondeo pid aparece en la salida de dtrace-l hasta que los haya activado.
El modo más sencillo de funcionamiento del proveedor pid es actuar como espacio de usuario análogo al proveedor fbt. El siguiente programa de ejemplo realiza un seguimiento de todas las entradas de funciones y devuelve las que están compuestas de una única función. La variable de macro $1 (el primer operando de la línea de comandos) es el ID de proceso para el proceso que se debe seguir. La variable de macro $2 (el segundo operando de la línea de comandos) es el nombre de la función desde la que se deben seguir todas las llamadas a funciones.
pid$1::$2:entry { self->trace = 1; } pid$1::$2:return /self->trace/ { self->trace = 0; } pid$1:::entry, pid$1:::return /self->trace/ { }
Escriba la secuencia de comandos de ejemplo anterior y guárdela en un archivo que se llame userfunc.d y, a continuación, aplique chmod para que sea ejecutable. Esta secuencia de comandos genera un resultado similar al siguiente ejemplo:
# ./userfunc.d 15032 execute dtrace: script './userfunc.d' matched 11594 probes 0 -> execute 0 -> execute 0 -> Dfix 0 <- Dfix 0 -> s_strsave 0 -> malloc 0 <- malloc 0 <- s_strsave 0 -> set 0 -> malloc 0 <- malloc 0 <- set 0 -> set1 0 -> tglob 0 <- tglob 0 <- set1 0 -> setq 0 -> s_strcmp 0 <- s_strcmp ... |
El proveedor pid sólo se puede usar en procesos que ya se estén ejecutando. Puede usar la variable de macro $target (consulte el Capítulo 15Secuencias de comandos), y las opciones dtrace -c y -p para crear y obtener procesos de interés e instrumentarlos usando DTrace. Por ejemplo, la siguiente secuencia de comandos en D se puede usar para determinar la distribución de las llamadas a funciones que un proceso de sujeto concreto realiza a libc:
pid$target:libc.so::entry { @[probefunc] = count(); }
Para determinar la distribución de este tipo de llamadas realizadas mediante el comando date(1), guarde la secuencia de comandos en un archivo llamado libc.d y ejecute el siguiente comando:
# dtrace -s libc.d -c date dtrace: script 'libc.d' matched 2476 probes Fri Jul 30 14:08:54 PDT 2004 dtrace: pid 109196 has exited pthread_rwlock_unlock 1 _fflush_u 1 rwlock_lock 1 rw_write_held 1 strftime 1 _close 1 _read 1 __open 1 _open 1 strstr 1 load_zoneinfo 1 ... _ti_bind_guard 47 _ti_bind_clear 94 |
Puede usar el proveedor pid para realizar un seguimiento de instrucciones en cualquier función de usuario. A petición, el proveedor pid creará un sondeo para cada instrucción de una función. El nombre de cada sondeo es el desplazamiento de su instrucción correspondiente en la función expresada como un entero hexadecimal. Por ejemplo, para activar un sondeo asociado a la instrucción del desplazamiento 0x1c en la función foo del módulo bar.so en el proceso cuyo PID sea 123, deberá usar el siguiente comando:
# dtrace -n pid123:bar.so:foo:1c |
Para activar todos los sondeos de la función foo, incluido el sondeo para cada instrucción, puede usar el siguiente comando:
# dtrace -n pid123:bar.so:foo: |
Este comando muestra una técnica realmente eficaz para depurar y analizar aplicaciones de usuario. Los errores poco frecuentes pueden ser difíciles de depurar porque pueden ser complicados de reproducir. A menudo, se identifica un error después de que se produzca, por lo que es demasiado tarde para reconstruir la ruta del código. El siguiente ejemplo muestra cómo se combina el proveedor pid con seguimientos especulativos (consulte el Capítulo 13Seguimiento especulativo) para resolver este problema realizando el seguimiento de todas las instrucciones de una función.
pid$1::$2:entry { self->spec = speculation(); speculate(self->spec); printf("%x %x %x %x %x", arg0, arg1, arg2, arg3, arg4); } pid$1::$2: /self->spec/ { speculate(self->spec); } pid$1::$2:return /self->spec && arg1 == 0/ { discard(self->spec); self->spec = 0; } pid$1::$2:return /self->spec && arg1 != 0/ { commit(self->spec); self->spec = 0; }
La ejecución de errorpath.d da como resultado una salida similar a la siguiente:
# ./errorpath.d 100461 _chdir dtrace: script './errorpath.d' matched 19 probes CPU ID FUNCTION:NAME 0 25253 _chdir:entry 81e08 6d140 ffbfcb20 656c73 0 0 25253 _chdir:entry 0 25269 _chdir:0 0 25270 _chdir:4 0 25271 _chdir:8 0 25272 _chdir:c 0 25273 _chdir:10 0 25274 _chdir:14 0 25275 _chdir:18 0 25276 _chdir:1c 0 25277 _chdir:20 0 25278 _chdir:24 0 25279 _chdir:28 0 25280 _chdir:2c 0 25268 _chdir:return |