Guía de seguimiento dinámico de Solaris

Capítulo 1 Introducción

Bienvenido al seguimiento dinámico en el sistema operativo Solaris. Si desea conocer cómo funciona su sistema, DTrace es la herramienta que necesita. DTrace es una utilidad de seguimiento dinámico incorporada en Solaris que pueden utilizar los administradores y programadores en los sistemas de producción en vivo para examinar el comportamiento de los programas de usuario y del propio sistema operativo. DTrace permite examinar el sistema para conocer cómo funciona, realizar un seguimiento de los problemas en los diferentes niveles del software o descubrir la causa de un comportamiento anómalo. Como podrá comprobar, DTrace le permite crear sus propios programas personalizados para administrar dinámicamente el sistema y proporcionar respuestas concisas a las preguntas más diversas que le puedan surgir al usar el lenguaje de programación en D de DTrace. La primera sección de este capítulo proporciona una rápida introducción a DTrace y muestra cómo escribir su propio programa D. El resto del capítulo presenta el conjunto completo de reglas para la programación en D, así como sugerencias y técnicas para realizar un análisis exhaustivo del sistema. Puede compartir sus experiencias y secuencias de comandos de DTrace con el resto de la comunidad de DTrace en el sitio Web http://www.sun.com/bigadmin/content/dtrace/. Todas las secuencias de comandos presentadas en esta guía están disponibles en el directorio /usr/demo/dtrace del sistema Solaris.

Procedimientos iniciales

DTrace le ayuda a conocer un sistema de software, ya que le permite instrumentar dinámicamente los procesos del núcleo y de los usuarios del sistema operativo para registrar los datos adicionales que especifique en las ubicaciones que le interesen (denominados sondeos). Un sondeo es una ubicación o actividad a la que DTrace puede enlazar una solicitud para realizar un conjunto de acciones como, por ejemplo, registrar un seguimiento de la pila, una marca de hora o el argumento de una función. Los sondeos son como sensores programables distribuidos en ubicaciones relevantes de todo el sistema Solaris. Si desea conocer lo que está pasando en el sistema, utilice DTrace para programar los sensores adecuados y registrar la información que le interese. A continuación, a medida que cada sondeo se activa, DTrace recopila los datos de los sondeos y le proporciona la información. Si no especifica ninguna acción para un sondeo, DTrace sólo recopilará información cada vez que éste se active.

Cada sondeo de DTrace tiene dos nombres: un Id. exclusivo compuesto por un entero y un nombre formado por una cadena que puede leer el usuario. En primer lugar, vamos a aprender el funcionamiento de DTrace mediante la creación de varias solicitudes muy sencillas con el sondeo denominado BEGIN, que se activa cada vez que inicia una nueva solicitud de seguimiento. Puede utilizar la opción -n de la utilidad dtrace(1M) para habilitar un sondeo por su nombre de cadena. Escriba el siguiente comando:


# dtrace -n BEGIN

Después de una breve pausa, comprobará cómo DTrace le indica que se ha habilitado un sondeo y verá una línea de salida que le indica que se ha activado el sondeo BEGIN. Una vez que vea esta salida, dtrace permanece en estado de pausa, en espera de que se activen otros sondeos. Como no se ha habilitado ninguno de los otros sondeos y BEGIN sólo se activa una vez, pulse Control-C en la shell para salir de dtrace y volver al indicador de la shell:


# dtrace -n BEGIN
dtrace: description 'BEGIN' matched 1 probe
CPU     ID		      FUNCTION:NAME
  0      1                  :BEGIN
^C
#

La salida le indica que se ha activado el sondeo con el nombre BEGIN y tanto su nombre como el Id., 1, se han imprimido. Tenga en cuenta que se muestra de forma predetermina el nombre en forma de entero de la CPU en la que se activa este sondeo. En este ejemplo, la columna de la CPU indica que el comando dtrace se estaba ejecutando en la CPU 0 cuando se activó el sondeo.

Puede crear solicitudes de DTrace mediante números arbitrarios de sondeos y acciones. Vamos a crear una solicitud sencilla con dos sondeos agregando el sondeo END al comando del ejemplo anterior. El sondeo END se activa una vez al completar el seguimiento. Escriba el siguiente comando y, a continuación, pulse Control-C en la shell cuando vea la línea de salida del sondeo BEGIN:


# dtrace -n BEGIN -n END
dtrace: description 'BEGIN' matched 1 probe
dtrace: description 'END' matched 1 probe
CPU     ID		      FUNCTION:NAME
  0      1                  :BEGIN
^C
  0      2                    :END
#

Como puede observar, al pulsar Control-C para salir de dtrace, se activa el sondeo END. dtrace informa de la activación de este sondeo antes de salir.

Ahora que ya tiene conocimientos básicos sobre cómo asignar un nombre a un sondeo y cómo activarlo, puede escribir la versión de DTrace del primer programa general, "Hola Mundo". Además de la creación de experimentos de DTrace en la línea de comandos, también puede escribirlos en archivos de texto utilizando el lenguaje de programación D. En el editor de texto, cree un nuevo archivo denominado hello.d y escriba su primer programa D:


Ejemplo 1–1 hello.d: "Hola, mundo" con el lenguaje de programación D

BEGIN
{
	trace("hello, world");
	exit(0);
}

Una vez guardado el programa, puede ejecutarlo con la opción -s de dtrace. Escriba el siguiente comando:


# dtrace -s hello.d
dtrace: script 'hello.d' matched 1 probe
CPU     ID		      FUNCTION:NAME
  0	    1                  :BEGIN   hello, world
#

Como puede ver, dtrace imprimió el mismo resultado que antes seguido del texto "Hola, mundo". A diferencia del ejemplo anterior, no ha tenido que esperar ni pulsar Control-C. Estos cambios son el resultado de las acciones especificadas en el sondeo BEGIN de hello.d. Examinemos la estructura del programa D con más detalle para conocer lo que ha ocurrido.

Cada programa D está formado por una serie de cláusulas; cada una de ellas describe uno o varios de los sondeos que se van a habilitar, y un conjunto de acciones opcionales que se van a realizar una vez activado el sondeo. Estas acciones aparecen enumeradas como una serie de instrucciones entre llaves { } a continuación del nombre del sondeo. Cada instrucción finaliza con un punto y coma (;). La primera instrucción utiliza la función trace() para indicar que DTrace debe registrar el argumento especificado, la cadena "Hola mundo", al archivarse el sondeo BEGIN y, a continuación, imprimirse. La segunda instrucción utiliza la función exit() para indicar que DTrace debería detener el seguimiento y salir del comando dtrace. DTrace proporciona un conjunto de funciones útiles como, por ejemplo, trace() y exit() a las que puede llamar en sus programas D. Para llamar a una función, debe especificar su nombre seguido de una lista de los argumentos entre paréntesis. El conjunto completo de funciones D se describe en el Capítulo 10Acciones y subrutinas.

Si está familiarizado con el lenguaje de programación C, es probable que ya se haya dado cuenta de que el nombre y los ejemplos del lenguaje de programación D son muy parecidos a los del lenguaje C. De hecho, el lenguaje D se deriva de un amplio subconjunto de C combinado con un conjunto especial de funciones y variables que le facilitan la realización del seguimiento. Obtendrá más información sobre estas funciones en los siguientes capítulos. Si ya ha escrito anteriormente un programa C, podrá utilizar inmediatamente la mayor parte de sus conocimientos en la creación de programas de seguimiento en D. Si, por el contrario, no ha escrito nunca ningún programa C, el aprendizaje del lenguaje D le resultará de todas formas muy sencillo. Conocerá toda la sintaxis al finalizar este capítulo. En primer lugar, dejemos a un lado por un momento las reglas del lenguaje y examinemos cómo funciona DTrace. Regresaremos más adelante al aprendizaje de cómo crear programas D más interesantes.

Proveedores y sondeos

En los ejemplos anteriores, hemos aprendido a utilizar dos sencillos sondeos denominados BEGIN y END. Pero, ¿de dónde proceden estos sondeos? Los sondeos de DTrace provienen de un conjunto de módulos del núcleo denominados proveedores; cada uno de ellos realiza un tipo determinado de instrumentación para crear sondeos. Al utilizar DTrace, a cada proveedor se le concede la oportunidad de publicar los sondeos que puede proporcionar a la estructura de DTrace. A continuación, puede habilitar y enlazar las acciones de seguimiento a cualquiera de los sondeos publicados. Para mostrar todos los sondeos disponibles en el sistema, escriba el comando:


# dtrace -l
  ID   PROVIDER            MODULE          FUNCTION NAME
   1     dtrace                                     BEGIN
   2     dtrace                                     END
   3     dtrace                                     ERROR
   4   lockstat           genunix       mutex_enter adaptive-acquire
   5   lockstat           genunix       mutex_enter adaptive-block
   6   lockstat           genunix       mutex_enter adaptive-spin
   7   lockstat           genunix       mutex_exit  adaptive-release

   ... many lines of output omitted ...

#

Es posible que el conjunto de resultados tarde en aparecer. Para realizar un recuento de todos los sondeos, puede escribir el comando:


# dtrace -l | wc -l
        30122

Es posible que observe una cantidad total diferente en el equipo, ya que el número de sondeos varía en función de la plataforma operativa y el software que tenga instalados. Como puede observar, hay un gran número de sondeos disponibles, por lo que podrá examinar cada rincón anteriormente inexplorado del sistema. De hecho, esta lista no incluye todos los resultados, ya que, como observaremos más adelante, algunos proveedores permiten crear nuevos sondeos sobre la marcha en función de las solicitudes de seguimiento, por lo que el número real de sondeos de DTrace es casi ilimitado.

Ahora volvamos a la salida de dtrace -l en la ventana del terminal. Tenga en cuenta que cada sondeo tiene dos nombres, como hemos mencionado anteriormente, un Id. compuesto por un entero y un nombre que pueden leer los usuarios. Este nombre está compuesto por cuatro partes, que se muestran en diferentes columnas en la salida de dtrace. Las cuatro partes del nombre del sondeo son:

Proveedor 

El nombre del proveedor de DTrace que publica este sondeo. El nombre de proveedor se corresponde normalmente con el nombre del módulo del núcleo de DTrace que realiza la instrumentación para habilitar el sondeo. 

Módulo 

Si este sondeo se corresponde con una ubicación de programa específica, se trata del nombre del módulo en el que se encuentra el sondeo. Este nombre puede ser el nombre de un módulo del núcleo o el nombre de una biblioteca de usuario. 

Función 

Si este sondeo se corresponde con una ubicación de programa específica, se trata del nombre de la función del programa en la que se encuentra el sondeo. 

Nombre 

El componente final del nombre del sondeo es el nombre que le indica de forma aproximada el significado semántico del sondeo como, por ejemplo, BEGIN o END.

Al escribir el nombre de un sondeo que pueden leer los usuarios, escriba sus cuatro partes separadas por dos puntos como se muestra a continuación:

proveedor:módulo: función:nombre

Tenga en cuenta que algunos de los sondeos de la lista no tienen ningún módulo ni función como, por ejemplo, los sondeos BEGIN y END utilizados anteriormente. Algunos sondeos dejan estos dos campos en blanco porque no se corresponden con ninguna ubicación o función de programa instrumentada específica. En su lugar, estos sondeos hacen referencia a un concepto más abstracto, como la idea del fin de la solicitud de seguimiento. A un sondeo con un módulo y una función como parte de su nombre se le conoce como sondeo fijado, y a uno sin nombre ni función se le conoce como no fijado.

Por convención, si no se especifican todos los campos de un nombre de sondeo, DTrace hace coincidir la solicitud con todos los sondeos que tengan valores coincidentes en las partes del nombre especificadas. En otros términos, al utilizar anteriormente el nombre de sondeo BEGIN, le estaba indicando realmente a DTrace que devolviese cualquier sondeo cuyo campo de nombre sea BEGIN, independientemente del valor de los campos de proveedor, módulo y función. En este caso, sólo hay un sondeo que coincida con la descripción, por lo que el resultado es el mismo. Sin embargo, ahora sabe que el verdadero nombre del sondeo BEGIN es dtrace:::BEGIN, que indica que la estructura de DTrace proporciona este sondeo y que no se ha fijado a una función. Por lo tanto, el programa hello.d podría haberse escrito de la siguiente forma y produciría el mismo resultado:

dtrace:::BEGIN
{
	trace("hello, world");
	exit(0);
}

Ahora que ya sabe dónde se originan los sondeos y cómo asignarles un nombre, vamos a aprender un poco más sobre qué ocurre al habilitar los sondeos y solicitar a DTrace que realice alguna acción. A continuación, regresaremos a nuestro recorrido general por el lenguaje D.

Compilación e instrumentación

Al escribir programas tradicionales en Solaris, debe utilizar un compilador para convertir el programa del código fuente a código de objeto que pueda utilizar. Al utilizar el comando dtrace, está llamando al compilador del lenguaje D utilizado anteriormente para escribir el programa hello.d. Una vez compilado el programa, se envía al núcleo del sistema operativo para que DTrace lo ejecute. A continuación, los sondeos a los que se les ha asignado un nombre en el programa se habilitan, y el proveedor correspondiente realiza la instrumentación necesaria para activarlos.

Toda la instrumentación que se realiza en DTrace es completamente dinámica: los sondeos se habilitan de forma discreta sólo cuando se utilizan. No hay ningún código instrumentado presente para los sondeos inactivos, por lo que el sistema no experimenta ningún tipo de degradación del rendimiento al no utilizar DTrace. Después de completar el experimento y salir de dtrace, todos los sondeos utilizados se deshabilitan automáticamente y se elimina su instrumentación, devolviendo el sistema a su estado original exacto. Aparentemente, no existe ninguna diferencia entre un sistema en el que DTrace no esté activo y otro en el que no se haya instalado el software de DTrace.

La instrumentación de cada sondeo se realiza dinámicamente en el sistema operativo que se esté ejecutando en vivo o en los procesos de usuario que seleccione. El sistema no se detiene temporalmente ni entra en modo inactivo de ninguna forma; además, el código de instrumentación sólo se agrega para los sondeos que se hayan habilitado. Como resultado, el efecto de los sondeos en el uso de DTrace se limita exactamente a lo que ha solicitado que haga esta aplicación: no se realiza ningún seguimiento de datos ajenos ni se activa ningún gran "conmutador de seguimiento"; toda la instrumentación de DTrace está diseñada para ser lo más eficaz posible. Estas funciones le permiten utilizar DTrace en producción para resolver problemas auténticos en tiempo real.

La estructura de DTrace también admite un número arbitrario de clientes virtuales. Puede ejecutar tantos experimentos y comandos simultáneos de DTrace como desee, con la única limitación de la capacidad de memoria del sistema; los comandos funcionan todos de forma independiente con la misma instrumentación subyacente. Esta misma función también permite que múltiples usuarios distintos del sistema saquen provecho simultáneamente de DTrace: los programadores, los administradores y el personal de mantenimiento pueden trabajar conjuntamente o en distintos problemas del mismo sistema mediante DTrace, sin interferir unos con otros.

A diferencia de los programas escritos en C y C++ y similar a los programas escritos con el lenguaje de programación JavaTM, los programas D de DTrace se compilan en un formato intermedio seguro que se utiliza para la ejecución cuando se activan los sondeos. Este formato intermedio se valida por motivos de seguridad cuando el software del núcleo de DTrace examina por primera vez el programa. El entorno de ejecución de DTrace también administra cualquier error de tiempo de ejecución que pueda producirse durante la ejecución del programa D, incluido la división por cero o la anulación de las referencias de memoria no válida y otros errores, y le informa de ellos. Como resultado, no podrá nunca crear un programa peligroso que pueda provocar que DTrace dañe de forma accidental el núcleo de Solaris o uno de los procesos que se ejecutan en el sistema. Estas funciones de seguridad le permiten utilizar DTrace en un entorno de producción sin necesidad de preocuparse de que se produzcan bloqueos o daños en el sistema. Si comete un error de programación, DTrace le informará del error y deshabilitará la instrumentación. A continuación, podrá corregir el error e intentarlo de nuevo. Las funciones de depuración y elaboración de informes de errores de DTrace se describen más adelante en este manual.

El siguiente diagrama muestra los diferentes componentes de la arquitectura de DTrace, incluidos los proveedores, los sondeos, el software del núcleo de DTrace y el comando dtrace.

Figura 1–1 Visión general de la arquitectura y los componentes de DTrace

Arquitectura de DTrace: la utilidad del núcleo y los proveedores, una interfaz de controlador del núcleo a una biblioteca y la biblioteca que admite un conjunto de comandos.

Ahora que conoce cómo funciona DTrace, volvamos al recorrido por el lenguaje de programación D y comencemos a escribir algunos programas más interesantes.

Variables y expresiones aritméticas

En el próximo ejemplo, el programa utiliza el proveedor profile de DTrace para implementar un contador simple basado en tiempo. El proveedor de perfiles (profile) puede crear nuevos sondeos basados en las descripciones que se encuentran en el programa D. Si crea un sondeo con el nombre profile:::tick-n sec para algún entero n, el proveedor de perfiles (profile) creará un sondeo que se activa cada n segundos. Escriba el siguiente código fuente y guárdelo en un archivo con el nombre counter.d:

/*
 * Count off and report the number of seconds elapsed
 */
dtrace:::BEGIN
{
	i = 0;
}

profile:::tick-1sec
{
	i = i + 1;
	trace(i);
}

dtrace:::END
{
	trace(i);
}

Al ejecutarlo, el programa cuenta el número de segundos transcurridos hasta que pulse Control-C y, después, se imprime el total al final:


# dtrace -s counter.d
dtrace: script 'counter.d' matched 3 probes
CPU     ID                    FUNCTION:NAME
  0  25499                       :tick-1sec         1
  0  25499                       :tick-1sec         2
  0  25499                       :tick-1sec         3
  0  25499                       :tick-1sec         4
  0  25499                       :tick-1sec         5
  0  25499                       :tick-1sec         6
^C
  0      2                             :END         6
#

Las tres primeras líneas del programa son comentarios para explicar las acciones que lleva a cabo el programa. Al igual que en C, C++ y el lenguaje de programación Java, el compilador D omite los caracteres entre los símbolos /* y */. Los comentarios pueden utilizarse en cualquier ubicación de un programa D, incluido dentro y fuera de las cláusulas del sondeo.

La cláusula del sondeo BEGIN define una variable con el nombre i y le asigna el valor de entero cero mediante la instrucción:

i = 0;

A diferencia de C, C++ y el lenguaje de programación de Java, las variables del lenguaje D pueden crearse simplemente utilizándolas en una instrucción del programa; no es necesario utilizar declaraciones de variables explícitas. Cuando se utiliza por primera vez una variable en un programa, el tipo de la variable se define en función del tipo de su primera asignación. Cada variable sólo tiene un tipo durante la duración del programa, por lo que las referencias posteriores deben ajustarse al mismo tipo que la asignación inicial. En counter.d, a la variable i se le asigna primero la constante de entero cero, por lo que su tipo se define como int. D proporciona los mismos tipos de datos de enteros básicos que el lenguaje C, incluidos:

char

Un carácter o un entero de un sólo byte 

int

Entero predeterminado 

short

Entero corto 

long

Entero largo 

long long

Entero largo ampliado 

El tamaño de estos tipos depende del modelo de datos del núcleo del sistema operativo, como se describen en el Capítulo 2Tipos, operadores y expresiones. D proporciona también nombres sencillos integrados para tipos de enteros con signo y sin signo de diversos tamaños fijos, así como miles de otros tipos definidos por el sistema operativo.

La parte central de counter.d es la cláusula del sondeo que incrementa el contador i:

profile:::tick-1sec
{
	i = i + 1;
	trace(i);
}

Esta cláusula asigna el nombre profile:::tick-1sec al sondeo, que indica que el proveedor profile va a crear un nuevo sondeo que se activa una vez por segundo en un procesador disponible. La cláusula contiene dos instrucciones, la primera asigna i al valor anterior más uno y la segunda realiza un seguimiento del nuevo valor de i. Todos los operadores aritméticos de C habituales están disponibles en D; puede encontrarse la lista completa en el Capítulo 2Tipos, operadores y expresiones. Al igual que en C, el operador ++ puede utilizarse como abreviatura para incrementar la variable correspondiente en uno. La función trace() utiliza cualquier expresión D como su argumento, por lo que puede escribir counter.d de forma más concisa de la siguiente forma:

profile:::tick-1sec
{
	trace(++i);
}

Si desea controlar de forma explícita el tipo de la variable i, puede poner entre paréntesis el tipo que desee al asignarlo para convertir el valor de entero cero en un tipo específico. Por ejemplo, si desea determinar el tamaño máximo de un char en D, puede cambiar la cláusula BEGIN de la siguiente forma:

dtrace:::BEGIN
{
	i = (char)0;
}

Después de ejecutar counter.d durante un periodo de tiempo, debe consultar el crecimiento del valor del que se ha realizado un seguimiento y restablecerlo a cero. Si, al restablecer el valor, la espera le resulta demasiado larga, intente cambiar el nombre del sondeo profile por profile:::tick-100msec para hacer que el contador aumente una vez cada 100 milisegundos o 10 veces por segundo.

Predicados

Una gran diferencia entre D y otros lenguajes de programación como, por ejemplo, C, C++ y el lenguaje de programación de Java es la ausencia de construcciones de flujo de control como, por ejemplo, las instrucciones "if" y los bucles. Las cláusulas de un programa D se escriben en forma de listas de instrucciones de una sola línea que realizan un seguimiento de una cantidad fija y opcional de datos. D permite realizar un seguimiento condicional de los datos y modificar el flujo de control mediante expresiones lógicas denominadas predicados, que pueden utilizarse para agregar prefijos a las cláusulas del programa. Una expresión de predicado se evalúa al activarse un sondeo, antes de ejecutar cualquier instrucción asociada a la cláusula correspondiente. Si el predicado se evalúa como "true", se representa con un valor diferente a cero y se ejecuta la lista de instrucciones. Si, por el contrario, el predicado es "false", se representa con el valor cero, no se ejecuta ninguna instrucción y se omite la activación del sondeo.

Escriba el siguiente código fuente para el próximo ejemplo y guárdelo en un archivo con el nombre countdown.d:

dtrace:::BEGIN
{
	i = 10;
}

profile:::tick-1sec
/i > 0/
{
	trace(i--);
}

profile:::tick-1sec
/i == 0/
{
	trace("blastoff!");
	exit(0);
}

Este programa D implementa un temporizador de cuenta atrás de 10 segundos utilizando predicados. Al ejecutarlo, countdown.d realiza la cuenta atrás a partir de 10 y, a continuación, imprime un mensaje y se cierra:

# dtrace -s countdown.d
dtrace: script 'countdown.d' matched 3 probes
CPU     ID                    FUNCTION:NAME
	0  25499                       :tick-1sec        10
	0  25499                       :tick-1sec         9
	0  25499                       :tick-1sec         8
	0  25499                       :tick-1sec         7
	0  25499                       :tick-1sec         6
	0  25499                       :tick-1sec         5
	0  25499                       :tick-1sec         4
	0  25499                       :tick-1sec         3
	0  25499                       :tick-1sec         2
	0  25499                       :tick-1sec         1
	0  25499                       :tick-1sec   blastoff!
# 

En este ejemplo, se utiliza el sondeo BEGIN para inicializar un entero i con el valor 10 y comenzar la cuenta atrás. A continuación, al igual que en el ejemplo anterior, el programa utiliza el sondeo tick-1sec para implementar un temporizador que se active una vez por segundo. Tenga en cuenta que, en countdown.d, la descripción del sondeo tick-1sec se utiliza en dos cláusulas diferentes, cada una con una lista de acciones y un predicado distintos. El predicado es una expresión lógica que debe incluirse entre barras diagonales / /, y que aparece detrás del nombre del sondeo y delante de los corchetes { } que engloban la lista de instrucciones de la cláusula.

El primer predicado comprueba si i es superior a cero, lo que indica que el temporizador aún se está ejecutando:

profile:::tick-1sec
/i > 0/
{
	trace(i--);
}

El operador relacional > significa superior a y devuelve el valor de entero cero para "false" y el valor uno para "true". Todos los operadores aritméticos de C habituales están disponibles en D; encontrará la lista completa en el Capítulo 2Tipos, operadores y expresiones. Si i no es aún cero, la secuencia de comandos realiza un seguimiento de i y, a continuación, se reduce en uno mediante el operador --.

El segundo predicado utiliza el operador == para devolver el valor "true" cuando i es exactamente igual a cero, lo que indica que se ha completado la cuenta atrás.

profile:::tick-1sec
/i == 0/
{
	trace("blastoff!");
	exit(0);
}

Del mismo modo que en el primer ejemplo, hello.d, countdown.d utiliza una secuencia de caracteres entre comillas dobles denominada constante de cadena para imprimir un mensaje final una vez completada la cuenta atrás. La función exit() se utiliza a continuación para salir de dtrace y volver al indicador de la shell.

Si observa la estructura de countdown.d, comprobará que, al crear dos cláusulas con la misma descripción de sondeo, pero diferentes acciones y predicados, hemos creado, de hecho, el flujo lógico:

i = 10
once per second,
	if i is greater than zero
		trace(i--);
	otherwise if i is equal to zero
		trace("blastoff!");
		exit(0);

Si desea escribir programas complejos utilizando predicados, intente visualizar primero el algoritmo de esta forma y, a continuación, transforme cada ruta de la construcción condicional en una cláusula y predicado independientes.

Ahora combinemos los predicados con un nuevo proveedor, syscall, y creemos nuestro primer programa de seguimiento D real. El proveedor syscall le permite habilitar sondeos en la entrada o en la devolución de cualquier llamada del sistema Solaris. En el ejemplo siguiente se utiliza DTrace para efectuar la supervisión cada vez que la shell realice una llamada del sistema read(2) o write(2). En primer lugar, abra dos ventanas de terminal, una para DTrace y otra que contenga el proceso de la shell que va a supervisar. En la segunda ventana, escriba el siguiente comando para obtener el Id. de proceso de esta shell:


# echo $$
12345

Ahora regrese a la primera ventana de terminal y escriba el siguiente programa D, y guárdelo en un archivo con el nombre rw.d. A medida que escribe el programa, sustituya la constante 12345 por el ID de proceso de la shell que se ha imprimido en respuesta al comando echo.

syscall::read:entry,
syscall::write:entry
/pid == 12345/
{

}

Tenga en cuenta que el cuerpo de la cláusula del sondeo rw.d se ha dejado vacío, ya que el programa sólo pretende realizar un seguimiento de las notificaciones de activación del sondeo, pero no de ningún dato adicional. Una vez que haya terminado de escribir en rw.d, utilice dtrace para iniciar el experimento. A continuación, acceda a la segunda ventana de la shell y escriba varios comandos, pulsando Intro después de introducir cada comando. Mientras escribe, debería ver la información sobre las activaciones del sondeo proporcionada por dtrace en la primera ventana, como se muestra en el siguiente ejemplo:


# dtrace -s rw.d
dtrace: script 'rw.d' matched 2 probes
CPU     ID                    FUNCTION:NAME
	0     34                      write:entry
	0     32                       read:entry
	0     34                      write:entry
	0     32                       read:entry
	0     34                      write:entry
	0     32                       read:entry
	0     34                      write:entry
	0     32                       read:entry
...

Ahora puede ver cómo la shell realiza llamadas del sistema read(2) y write(2) para leer un carácter de la ventana del terminal y devolver el resultado. En este ejemplo, se incluyen muchos de los conceptos descritos hasta ahora, así como algunos nuevos. En primer lugar, para instrumentar read(2) y write(2) del mismo modo, la secuencia de comandos utiliza una única cláusula del sondeo con múltiples descripciones de sondeos separadas por comas, como se muestra a continuación:

syscall::read:entry,
syscall::write:entry

Para una mejor lectura, cada descripción de sondeo aparece en su propia línea. Esta disposición no es estrictamente necesaria, pero permite leer con mayor facilidad la secuencia de comandos. A continuación, la secuencia de comandos define un predicado que coincida sólo con las llamadas del sistema que ha ejecutado el proceso de la shell:

/pid == 12345/

El predicado utiliza la variable pid predefinida de DTrace, que se evalúa siempre con el Id. de proceso asociado al subproceso que ha activado el sondeo correspondiente. DTrace proporciona un gran número de definiciones de variables integradas para cosas útiles como, por ejemplo, el Id. de proceso. A continuación, se muestra una lista de algunas de las variables de DTrace que puede utilizar para escribir sus primeros programas D:

Nombre de la variable 

Tipo de datos 

Significado 

errno

int

Valor actual de errno para las llamadas del sistema

execname

string

Nombre del archivo ejecutable del proceso actual 

pid

pid_t

Id. de proceso del proceso actual 

tid

id_t

Id. de subproceso del subproceso actual 

probeprov

string

Campo de proveedor de la descripción de sondeo actual 

probemod

string

Campo de módulo de la descripción de sondeo actual 

probefunc

string

Campo de función de la descripción de sondeo actual 

probename

string

Campo de nombre de la descripción de sondeo actual 

Ahora que ya ha escrito un programa de instrumentación real, intente probarlo en diferentes procesos que se ejecuten en el sistema, cambiando el Id. de proceso y los sondeos de llamadas del sistema instrumentados. A continuación, puede realizar un sencillo cambio más y convertir rw.d en una versión muy sencilla de una herramienta de seguimiento de llamadas del sistema como, por ejemplo, truss(1). El campo de descripción de sondeo vacío actúa como comodín, devolviendo cada sondeo coincidente, por lo que puede cambiar el programa al siguiente nuevo código fuente para realizar un seguimiento de cualquier llamada del sistema ejecutada por la shell:

syscall:::entry
/pid == 12345/
{

}

Intente escribir varios comandos en la shell como, por ejemplo, cd, ls y date para comprobar la información que devuelve el programa de DTrace.

Formato de salida

El seguimiento de las llamadas del sistema supone un método eficaz para observar el comportamiento de la mayoría de los procesos de usuario. Si ha utilizado anteriormente la utilidad truss(1) de Solaris como administrador o programador, es probable que ya sepa que se trata de una herramienta muy útil para tenerla a mano en caso de producirse un problema. Si, por el contrario, nunca ha utilizado truss anteriormente, escriba este comando en uno de las shell para probarlo ahora mismo:


$ truss date

Verá un seguimiento con formato de todas las llamadas del sistema ejecutadas por date(1), seguido de una única línea de salida al final. En el siguiente ejemplo, se ha mejorado el programa rw.d mediante la aplicación de formato a la salida para que tenga un aspecto más parecido a truss(1), lo que le permitirá comprender de forma más fácil la salida. Escriba el siguiente programa y guárdelo en un archivo con el nombre trussrw.d :


Ejemplo 1–2 trussrw.d: seguimiento de las llamadas del sistema con el formato de salida truss(1)

syscall::read:entry,
syscall::write:entry
/pid == $1/
{
	printf("%s(%d, 0x%x, %4d)", probefunc, arg0, arg1, arg2);
}

syscall::read:return,
syscall::write:return
/pid == $1/
{
	printf("\t\t = %d\n", arg1);
}

En este ejemplo, la constante 12345 se sustituye por la etiqueta $1 en cada predicado. Esta etiqueta permite especificar el proceso pertinente como argumento en la secuencia de comandos: $1 se sustituye por el valor del primer argumento al compilar la secuencia de comandos. Para ejecutar trussrw.d, utilice las opciones de dtrace, -q y -s, seguidas del Id. de proceso de la shell como argumento final. La opción - q indica que dtrace debería estar en modo silencioso, y suprimir la línea de encabezado y las columnas de CPU e Id. mostradas en los ejemplos anteriores. Por lo tanto, sólo verá como salida los datos de los que realice un seguimiento de forma explícita. Escriba el siguiente comando (sustituyendo 12345 por el Id. del proceso de la shell) y, a continuación, pulse Intro varias veces en la shell especificada:


# dtrace -q -s trussrw.d 12345
	                 = 1
write(2, 0x8089e48,    1)                = 1
read(63, 0x8090a38, 1024)                = 0
read(63, 0x8090a38, 1024)                = 0
write(2, 0x8089e48,   52)                = 52
read(0, 0x8089878,    1)                 = 1
write(2, 0x8089e48,    1)                = 1
read(63, 0x8090a38, 1024)                = 0
read(63, 0x8090a38, 1024)                = 0
write(2, 0x8089e48,   52)                = 52
read(0, 0x8089878,    1)                 = 1
write(2, 0x8089e48,    1)                = 1
read(63, 0x8090a38, 1024)                = 0
read(63, 0x8090a38, 1024)                = 0
write(2, 0x8089e48,   52)                = 52
read(0, 0x8089878,    1)^C
#

Ahora examinemos más detalladamente el programa D y su salida: En primer lugar, una cláusula similar al programa anterior instrumenta cada llamada de la shell en read(2) y write(2). Sin embargo, en este ejemplo, se utiliza la nueva función printf() para realizar un seguimiento de los datos e imprimirlos en un formato específico:

syscall::read:entry,
syscall::write:entry
/pid == $1/
{
	printf("%s(%d, 0x%x, %4d)", probefunc, arg0, arg1, arg2);
}

La función printf() combina la capacidad de realizar un seguimiento de los datos, al igual que con la función trace() utilizada anteriormente, con la capacidad para proporcionar los datos y otro texto adicional en el formato específico que describa. La función printf() indica a DTrace que debe realizar un seguimiento de los datos asociados a cada argumento ubicado detrás del primer argumento y, a continuación, debe aplicar un formato a los resultados mediante las reglas descritas en el primer argumento printf(), conocido como cadena de formato.

La cadena de formato es una cadena normal que contiene una serie de conversiones de formato; cada una de ellas comienza con el carácter % y describe el formato que debe aplicarse al argumento correspondiente. La primera conversión de la cadena de formato se corresponde con el segundo argumento printf(), la segunda conversión con el tercer argumento, y así sucesivamente. Todo el texto que se encuentra entre una conversión y otra se imprime de forma literal. El carácter que se incluye después del carácter de conversión % describe el formato que se utilizará para el argumento correspondiente. A continuación, se muestran los significados de las tres conversiones de formato utilizadas en trussrw.d:

%d

Imprimir el valor correspondiente como entero decimal. 

%s

Imprimir el valor correspondiente como cadena. 

%x

Imprimir el valor correspondiente como entero hexadecimal. 

La función printf() de DTrace funciona del mismo modo que la rutina de biblioteca printf(3C) de C o la utilidad de la shell printf(1). Si no ha utilizado anteriormente la función printf(), los formatos y las opciones se explican detalladamente en el Capítulo 12Formato de salida. Debe leer detenidamente este capítulo, incluso aunque esté familiarizado con la función printf() de otro lenguaje. En D, printf() se proporciona como una función integrada y hay disponibles nuevas conversiones de formato diseñadas específicamente para DTrace.

Para ayudarle a escribir programas correctos, el compilador del lenguaje D valida cada cadena de formato printf() en su lista de argumentos. Intente cambiar probefunc en la cláusula anterior por el entero 123. Si ejecuta el programa modificado, recibirá un mensaje de error en el que se le indica que la conversión de formato de cadena %s no se puede utilizar con un argumento de entero:


# dtrace -q -s trussrw.d
dtrace: failed to compile script trussrw.d: line 4: printf( )
	   argument #2 is incompatible with conversion #1 prototype:
	        conversion: %s
	         prototype: char [] or string (or use stringof)
	          argument: int
#

Si desea imprimir el nombre de la llamada del sistema de lectura o escritura y sus argumentos, utilice la instrucción printf():

printf("%s(%d, 0x%x, %4d)", probefunc, arg0, arg1, arg2);

para realizar un seguimiento del nombre de la función de sondeo actual y los tres primeros argumentos de enteros en la llamada del sistema, disponibles en las variables de DTrace arg0, arg1 y arg2. Para obtener más información sobre los argumentos de sondeos, consulte el Capítulo 3Variables. El primer argumento para read(2) y write(2) es un descriptor de archivo, que se imprime en valores decimales. El segundo argumento es una dirección de memoria intermedia con un formato de valor hexadecimal. El argumento final es el tamaño de memoria intermedia con un formato de valor decimal. El especificador de formato %4d se utiliza para el tercer argumento con el fin de indicar que el valor debe imprimirse mediante la conversión de formato %d con un ancho de campo mínimo de 4 caracteres. Si el entero presenta un ancho inferior a 4 caracteres, printf() insertará espacios en blanco para alinear la salida.

Para imprimir el resultado de la llamada del sistema y completar cada línea de salida, utilice la siguiente cláusula:

syscall::read:return,
syscall::write:return
/pid == $1/
{
	printf("\t\t = %d\n", arg1);
}

Tenga en cuenta que el proveedor syscall publica también un sondeo con el nombre return para cada llamada del sistema, además de entry. La variable arg1 de DTrace para los sondeos return de syscall evalúa en el valor de retorno de la llamada del sistema. Este valor de retorno presenta un formato de número entero decimal. Las secuencias de caracteres que comienzan por barras diagonales inversas en la cadena de formato se expanden para insertar un tabulador (\t) y una nueva línea (\n) respectivamente. Estas secuencias de escape le ayudan a imprimir o registrar caracteres difíciles de escribir. D admite el mismo conjunto de secuencias de escape que C, C++ y el lenguaje de programación de Java. Encontrará la lista completa de secuencias de escape en el Capítulo 2Tipos, operadores y expresiones.

Matrices

D permite definir variables en forma de enteros, así como otros tipos para representar cadenas y tipos compuestos denominados estructuras y uniones. Si está familiarizado con el lenguaje de programación C, le agradará saber que puede utilizar también los tipos de C en el lenguaje D. Si, por el contrario, no es un experto en C, no se preocupe: las diferentes clases de tipos de datos se describen en el Capítulo 2Tipos, operadores y expresiones. D también admite un tipo especial de variable denominada matriz asociativa. Una matriz asociativa es parecida a una matriz normal, en el sentido en que se asocia un conjunto de claves a un conjunto de valores, pero, en la matriz asociativa, las claves no se limitan a los enteros de un intervalo fijo.

Las matrices asociativas del lenguaje D pueden indexarse mediante una lista de uno o varios valores de cualquier tipo. Los valores de claves individuales conforman una tupla, que se utiliza para realizar la indexación en la matriz, y acceder o modificar el valor correspondiente a esa clave. Cada tupla utilizada en una determinada matriz asociativa debe ajustarse a la misma firma de tipo; es decir, cada clave de la tupla debe tener la misma longitud y los mismos tipos de claves en el mismo orden. El valor asociado a cada elemento de una determinada matriz asociativa debe ser también de un único tipo fijo para toda la matriz. Por ejemplo, la siguiente instrucción del lenguaje D define una matriz asociativa a con el tipo de valor int y la firma de tupla [ string, int ], y almacena el valor de entero 456 en la matriz:

a["hello", 123] = 456;

Una vez definida una matriz, se puede acceder a sus elementos del mismo modo que en cualquier otra variable del lenguaje D. Por ejemplo, la siguiente instrucción del lenguaje D modifica el elemento de la matriz almacenada anteriormente en a, aumentando el valor de 456 a 457:

a["hello", 123]++;

Los valores de los elementos de la matriz que aún no se hayan asignado se establecerán en cero. Ahora utilicemos una matriz asociativa en un programa D. Escriba el siguiente programa y guárdelo en un archivo con el nombre rwtime.d:


Ejemplo 1–3 rwtime.d: llamadas de read(2) y write(2) de tiempo

syscall::read:entry,
syscall::write:entry
/pid == $1/
{
	ts[probefunc] = timestamp;
}

syscall::read:return,
syscall::write:return
/pid == $1 && ts[probefunc] != 0/
{
	printf("%d nsecs", timestamp - ts[probefunc]);
}

Al igual que con trussrw.d, especifique el Id. del proceso de la shell al ejecutar rwtime.d. Si escribe varios comandos de la shell, verá el tiempo transcurrido durante cada llamada del sistema. Escriba el siguiente comando y, a continuación, pulse Intro varias veces en la otra shell:


# dtrace -s rwtime.d `pgrep -n ksh`
dtrace: script 'rwtime.d' matched 4 probes
CPU     ID                    FUNCTION:NAME
  0     33                      read:return 22644 nsecs
  0     33                      read:return 3382 nsecs
  0     35                     write:return 25952 nsecs
  0     33                      read:return 916875239 nsecs
  0     35                     write:return 27320 nsecs
  0     33                      read:return 9022 nsecs
  0     33                      read:return 3776 nsecs
  0     35                     write:return 17164 nsecs
...
^C
#

Para realizar un seguimiento del tiempo transcurrido para cada llamada del sistema, debe instrumentar la entrada a read(2) y write(2) y la devolución, y obtener muestras del tiempo en cada punto. A continuación, en la devolución de una determinada llamada del sistema, debe calcular la diferencia entre la primera y la segunda marca de tiempo. Puede utilizar diferentes variables para cada llamada del sistema, pero esto dificultaría la ampliación a llamadas del sistema adicionales en el programa. En su lugar, es más sencillo usar una matriz asociativa indexada por el nombre de función del sondeo. A continuación se muestra la primera cláusula del sondeo:

syscall::read:entry,
syscall::write:entry
/pid == $1/
{
	ts[probefunc] = timestamp;
}

Esta cláusula define una matriz con el nombre ts y asigna al miembro adecuado el valor de la variable timestamp de DTrace. Esta variable devuelve el valor de un contador de nanosegundos siempre incremental, parecido a la rutina de biblioteca gethrtime(3C) de Solaris. Una vez guardada la marca del tiempo de la entrada, el sondeo de devolución correspondiente obtiene de nuevo muestras de timestamp e informa de la diferencia entre el tiempo actual y el valor guardado:

syscall::read:return,
syscall::write:return
/pid == $1 && ts[probefunc] != 0/
{
	printf("%d nsecs", timestamp - ts[probefunc]);
}

Es necesario que DTrace realice un seguimiento del proceso adecuado y que el sondeo entry correspondiente se haya activado y haya asignado un valor diferente a cero a ts[probefunc]. Este truco permite eliminar la salida no válida cuando se inicia por primera vez DTrace. Si la shell ya está esperando la entrada en una llamada del sistema read(2) al ejecutar dtrace, el sondeo read:return se activará sin necesidad de una entrada read:entry precedente, ya que esta primera acción read(2) y ts[probefunc] se evaluará en cero debido a que aún no se ha asignado.

Tipos y símbolos externos

La instrumentación de DTrace se ejecuta en el núcleo del sistema operativo Solaris, por lo que, además de acceder a los argumentos del sondeo y las variables especiales de DTrace, puede acceder también a los símbolos, los tipos y las estructuras de datos del núcleo. Estas funciones permiten a los usuarios avanzados, los administradores, el personal de mantenimiento y los programadores de controladores de DTrace examinar el comportamiento a bajo nivel del núcleo del sistema operativo y los controladores de dispositivos. La lista de lecturas incluida al principio de este manual incluye libros que pueden ayudarle a obtener más información sobre los componentes internos del sistema operativo Solaris.

D utiliza un carácter de comilla inversa (`) como operador de ámbito para acceder a los símbolos definidos en el sistema operativo, pero no en el programa D. Por ejemplo, el núcleo de Solaris contiene una declaración escrita en C de un valor ajustable del sistema llamado kmem_flags para activar las funciones de depuración del asignador de memoria. Consulte Solaris Tunable Parameters Reference Manual para obtener más información sobre kmem_flags. Este ajuste se declara en C en el código fuente del núcleo de la siguiente forma:

int kmem_flags;

Para realizar un seguimiento del valor de esta variable en un programa D, puede escribir la siguiente instrucción del lenguaje D:

trace(`kmem_flags);

DTrace asocia cada símbolo del núcleo al tipo utilizado para éste en el código C correspondiente del sistema operativo, proporcionando un acceso fácil basado en la fuente a las estructuras de datos nativas del sistema operativo. Los nombres de los símbolos del núcleo se incluyen en un espacio de nombre distinto al de los identificadores de funciones y variables del lenguaje D, por lo que no tiene que preocuparse de que estos nombres entren en conflicto con las variables de D.

Ya ha completado el recorrido general por DTrace y conoce gran parte de los bloques de creación básicos de Trace necesarios para crear programas D de mayor tamaño y complejidad. En los siguientes capítulos se describe el conjunto completo de reglas de D y se demuestra cómo DTrace puede facilitar las mediciones de rendimiento y los análisis funcionales del sistema complejos. A continuación, aprenderá a utilizar DTrace para asociar el comportamiento de la aplicación de usuario al comportamiento del sistema, lo que le permitirá analizar toda la pila de software.

Esto es sólo el principio.