Guia de rastreamento dinâmico Solaris

Capítulo 1 Introdução

Bem-vindo ao rastreio dinâmico do Sistema operacional Solaris! Se você quer conhecer o comportamento do seu sistema, o DTrace é a ferramenta ideal. O DTrace é um recurso de rastreio dinâmico abrangente, incluído no Solaris, que pode ser usado por administradores e desenvolvedores em sistemas de produção dinâmicos para examinar o comportamento dos programas do usuário e do próprio sistema operacional. O DTrace permite que você explore o sistema para compreender como ele funciona, rastreie problemas de desempenho em muitas camadas do software ou localize a causa de um comportamento anormal. Como você irá perceber, o DTrace permite que você crie seus próprios programas personalizados para instrumentar dinamicamente o sistema e fornecer respostas imediatas e concisas para questões arbitrárias que você pode formular usando a linguagem de programação D do DTrace. A primeira seção deste capítulo fornece uma rápida introdução ao DTrace e mostra como você pode escrever seu primeiro programa em linguagem D. O resto do capítulo introduz o conjunto completo de regras para programação em D assim como as dicas e as técnicas para realizar uma análise detalhada do sistema. Você pode compartilhar suas experiências e scripts do DTrace com o restante da comunidade DTrace na Web em http://www.sun.com/bigadmin/content/dtrace/ . Todos os scripts de exemplo apresentados neste guia podem ser encontrados no sistema Solaris no diretório /usr/demo/dtrace.

Guia de introdução

O DTrace ajuda-o a compreender um sistema de software permitindo que modifique dinamicamente o kernel do sistema operacional e processos do usuário para registrar dados adicionais que você especificar em locais de interesse, chamados de testes. Um teste é um local ou atividade ao qual o DTrace pode vincular uma solicitação para realizar um conjunto de ações, como gravar um rastreamento de pilha, um carimbo de data/hora ou o argumento de uma função. Os testes são como sensores programáveis distribuídos em locais interessantes do sistema Solaris. Se você quiser saber o que está acontecendo, use o DTrace para programar os sensores apropriados para registrar as informações do seu interesse. Depois, quando cada teste for disparado, o DTrace obterá os dados resultantes e os informará para você. Se você não especificar as ações de um teste, o DTrace apenas anotará quando o teste for disparado.

Cada teste no DTrace possui dois nomes: um ID de inteiro exclusivo e um nome legível. Você irá começar a aprender o DTrace criando algumas solicitações muito simples usando o teste chamado BEGIN, que é acionado sempre que você inicia uma nova solicitação de rastreio. Você pode usar a opção -n do utilitário dtrace(1M) para habilitar um teste usando seu nome de seqüência de texto. Digite o seguinte comando:


# dtrace -n BEGIN

Depois de uma breve pausa, o DTrace irá informar que um teste foi ativado e você verá uma linha de resultado indicando que o teste BEGIN foi acionado. Quando esse resultado é exibido, o dtrace permanece em pausa aguardando que os outros testes sejam acionados. Como você não ativou outros testes e BEGIN só é acionado uma vez, pressione Control-C no shell para sair de dtrace e retornar ao prompt do shell:


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

O resultado informa que o teste chamado BEGIN foi disparado uma vez e seu nome e ID de inteiro, 1, são impressos. Observe que, por padrão, o nome de inteiro da CPU na qual esse teste foi disparado é exibido. Neste exemplo, a coluna CPU indica que o comando dtrace estava sendo executado na CPU 0 quando o teste foi acionado.

Você pode construir solicitações DTrace usando números arbitrários de testes e ações. Vamos criar uma solicitação simples usando dois testes adicionando o teste END ao comando do exemplo anterior. O teste END é acionado uma vez quando o rastreio é concluído. Digite o seguinte comando e depois pressione novamente Control-C no shell depois que a linha do resultado do teste BEGIN for exibida:


# 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 você pode ver, ao pressionar Control-C para sair de dtrace, você aciona o teste END. dtrace reporta o acionamento desse teste antes de sair.

Agora que você já sabe um pouco sobre nomeação e ativação de testes, está pronto para escrever a versão do DTrace do primeiro programa de todas as pessoas, "Olá, mundo". Além de construir experimentos do DTrace na linha de comando, você também pode escrevê-los em arquivos de texto usando a linguagem de programação D. Em um editor de texto, crie um novo arquivo chamado hello.d e digite seu primeiro programa em D:


Exemplo 1–1 hello.d: Ola, mundo da linguagem de programação D

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

Depois de salvar o programa, você poderá executá-lo usando a opção -s do dtrace. Digite o seguinte comando:


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

Como você pode ver, o dtrace imprimiu o mesmo resultado de antes seguido pelo texto “ola, mundo”. Ao contrário do exemplo anterior, você não precisou esperar e pressionar Control-C. Essas alterações foram o resultado das ações especificadas para o teste BEGIN em hello.d. Vamos explorar a estrutura do programa em D em mais detalhes para entender o que aconteceu.

Cada programa em D consiste em uma série de cláusulas, cada uma descrevendo um ou mais testes a serem ativados e um conjunto opcional de ações a serem realizadas quando o teste for acionado. As ações são listadas como uma série de instruções entre chaves { } após o nome do teste. Cada instrução é encerrada com ponto-e-vírgula (;). A primeira instrução usa a função trace() para indicar que o DTrace deve registrar o argumento especificado, a seqüência “ola, mundo ”, quando o teste BEGIN for acionado e depois imprimi-lo. A segunda instrução usa a função exit() para indicar que o DTrace deve encerrar o rastreio e sair do comando dtrace . O DTrace fornece um conjunto de funções úteis como trace() e exit() para que você chame seus programas em D. Para chamar uma função, especifique seu nome seguido por uma lista de argumentos entre parênteses. O conjunto completo das funções D é descrito no Capítulo 10Ações e sub-rotinas.

Se você já estiver familiarizado com a linguagem de programação C, provavelmente já percebeu pelo nome e pelos exemplos que a linguagem de programação D do DTrace é muito similar à C. Na verdade, a D é derivada de um grande subconjunto de C combinado com um conjunto especial de funções e variáveis para ajudar a facilitar o rastreamento. Você aprenderá mais sobre esses recursos nos próximos capítulos. Se você já tiver escrito um programa em C antes, poderá transferir imediatamente grande parte do seu conhecimento para construir programas de rastreio em D. Se você nunca tiver escrito um programa em C, aprender a linguagem D, mesmo assim, é muito fácil. Você aprenderá toda a sintaxe até o final do capítulo. Mas primeiro, vamos deixar as regras de linguagem um pouco de lado e descobrir mais sobre como o DTrace funciona e depois vamos voltar para aprender como construir programas em D mais interessantes.

Provedores e testes

Nos exemplos anteriores, você aprendeu a usar dois testes simples chamados BEGIN e END. Mas de onde vêm esses testes? Os testes do DTrace vêm de um conjunto de módulos do kernel chamados provedores. Cada um deles realiza um tipo particular de instrumentação para criar testes. Quando você usa o DTrace, cada provedor recebe uma oportunidade de publicar os testes que ele pode fornecer na estrutura do DTrace. Você depois pode ativar e vincular suas ações de rastreio a um dos testes que foram publicados. Para listar todos os testes disponíveis no seu sistema, digite o 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 ...

#

Pode demorar algum tempo para que todo o resultado seja exibido. Para contar todos os testes, você pode digitar o comando:


# dtrace -l | wc -l
        30122

Você pode observar um total diferente na sua máquina, pois o número de testes varia dependendo da sua plataforma operacional e do software que você instalou. Como você pode ver, há muitos testes disponíveis , assim, você pode vasculhar todos os cantos escuros do sistema. Na verdade, até mesmo esse resultado não é a lista completa porque, como você verá posteriormente, alguns provedores oferecem a capacidade de criar novos testes dinâmicos com base nas suas solicitações de rastreio, tornado o número real de testes do DTrace virtualmente ilimitados.

Agora, observe o resultado de dtrace -l na janela do seu terminal. Observe que cada teste possui os dois nomes mencionados anteriormente, um ID de inteiro e um nome legível. O nome legível é composto de quatro partes, mostradas como colunas separadas no resultado de dtrace . As quatro partes do nome de um teste são:

Provedor 

O nome do provedor do DTrace que está publicando este teste. O nome do provedor geralmente corresponde ao nome do módulo do kernel do DTrace que realiza a instrumentação para ativar o teste. 

Módulo 

Se este teste corresponder a um local de programa específico, o nome do módulo no qual o teste está localizado. Este nome é o nome de um módulo do kernel ou o nome de uma biblioteca do usuário. 

Função 

Se este teste corresponder a um local de programa específico, o nome da função do programa na qual o teste está localizado. 

Nome 

O componente final do nome do teste é um nome que dá alguma idéia do significado semântico do teste, como BEGIN ou END.

Ao escrever o nome legível completo de um teste, escreva as quatro partes do nome separadas por dois-pontos, como por exemplo:

provedor:módulo: função:nome

Observe que alguns dos testes na lista não possuem um módulo e uma função, como os testes BEGIN e END usados anteriormente. Esses dois campos permanecem em branco em alguns testes porque esses testes não correspondem a um local ou a uma função de programa instrumentada específica. Em vez disso, esses testes fazem referência a um conceito mais abstrato como a idéia do final da solicitação de rastreio. Um teste que possui um módulo e uma função como parte de seu nome é conhecido como teste ancorado e um que não possui é conhecido como não ancorado.

Por convenção, se você não especificar todos os campos do nome de um teste, o DTrace corresponderá sua solicitação com todos os testes que possuam valores correspondentes nas partes do nome que você especificar. Em outras palavras, quando você usou o nome do teste BEGIN anteriormente, na verdade estava dizendo ao DTrace para corresponder qualquer teste cujo campo nome fosse BEGIN, independentemente do valor dos campos provedor, módulo e função. Quando isso acontece, há apenas um teste que corresponde a essa descrição, sendo assim, o resultado é o mesmo. Mas agora você sabe que o nome verdadeiro do teste BEGIN é dtrace:::BEGIN, o que indica que esse teste é fornecido pela própria estrutura do DTrace e não está ancorado a qualquer função. Assim, o programa >hello.d poderia ter sido escrito da maneira a seguir e produziria o mesmo resultado:

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

Agora que você entende a origem dos testes e como eles são nomeados, irá aprender um pouco mais sobre o que acontece quando você ativa testes e solicita que o DTrace faça alguma coisa, e depois retornaremos ao rápido tour da linguagem D.

Compilação e instrumentação

Quando você escreve programas tradicionais no Solaris, usa um compilador para converter o seu programa do código-fonte em código de objeto que possa ser executado. Quando você usa o comando dtrace, está chamando o compilador da linguagem D usada anteriormente para escrever o programa hello.d. Depois que o programa é compilado, ele é enviado para o kernel do sistema operacional para execução pelo DTrace. Lá, os testes que forem nomeados no seu programa são ativados e o provedor correspondente realiza toda a instrumentação necessária para ativá-los.

Toda a instrumentação no DTrace é completamente dinâmica: os testes são ativados separadamente somente quando estiverem sendo utilizados. Testes inativos não possuem código instrumentado, sendo assim, o seu sistema não sofre qualquer tipo de redução de desempenho quando você não está usando o DTrace. Quando o seu experimento estiver completo e o comando dtrace for encerrado, todos os testes que você usou serão automaticamente desativados e a instrumentação será removida, retornando o sistema ao seu estado original exato. Não há uma diferença efetiva entre um sistema no qual o DTrace não está ativo e um no qual o software DTrace não está instalado.

A instrumentação de cada teste é realizada dinamicamente no sistema operacional em execução dinâmico ou nos processos do usuário selecionados. O sistema não é desativado ou pausado, e o código de instrumentação é adicionado somente para os testes ativados. Como resultado, o efeito do teste do uso de DTrace é limitado a exatamente o que você pede que o DTrace faça: nenhum dado desnecessário é rastreado, nenhuma grande “opção de rastreio” é ativada no sistema e toda a instrumentação de DTrace é projetada para ser o mais eficiente possível. Esses recursos permitem que você use o DTrace em produção para solucionar problemas reais em tempo real.

A estrutura do DTrace também fornece suporte a um número arbitrário de clientes virtuais. Você pode executar simultaneamente quantos experimentos e comandos do DTrace desejar, limitado somente pela capacidade de memória do seu sistema. E todos os comandos operam independentemente usando a mesma instrumentação base. Essa mesma capacidade também permite que qualquer número de usuários distintos no sistema aproveitem o DTrace simultaneamente: desenvolvedores, administradores e pessoal de serviço podem trabalhar todos juntos ou em problemas distintos no mesmo sistema usando o DTrace sem que um interfira no trabalho do outro.

Ao contrário de programas escritos em C e C++ e similar aos programas escritos na linguagem de programação JavaTM, os programas em D do DTrace são compilados em uma forma intermediária segura que é usada para execução quando os testes são acionados. Essa forma intermediária é validada por segurança quando o seu programa é examinado pela primeira vez pelo software do kernel do DTrace. O ambiente de execução do DTrace também lida com os erros em tempo de execução que podem ocorrer durante a execução do programa em D, incluindo dividir por zero, apontar para memória inválida e assim por diante, e os informa para você. Como resultado, você não consegue construir um programa inseguro que possa fazer com que o DTrace danifique inadvertidamente o kernel do Solaris ou um dos processos em execução no sistema. Esses recursos de segurança permitem que você use o DTrace em um ambiente de produção sem se preocupar com a queda do sistema ou que ele seja corrompido. Se você cometer um erro de programação, o DTrace informará o seu erro, desabilitará a instrumentação e você poderá corrigir o erro e tentar novamente. Os recursos de relatório e depuração de erros do DTrace são descritos posteriormente neste livro.

O diagrama a seguir mostra os diferentes componentes da arquitetura do DTrace, incluindo provedores, testes, o software do kernel do DTrace e o comando dtrace.

Figura 1–1 Visão geral da arquitetura e dos componentes do DTrace

Arquitetura do DTrace: o recurso do kernel e os provedores, uma interface de driver do kernel para uma biblioteca e a biblioteca que fornece suporte a um conjunto de comandos.

Agora que você já sabe como o DTrace funciona, vamos retornar ao tour da linguagem de programação D e começar a escrever alguns programas mais interessantes.

Variáveis e expressões aritméticas

Nosso próximo programa de exemplo utiliza o provedor profile do DTrace para implementar um contador simples baseado em hora. O provedor de perfil pode criar novos testes com base nas descrições encontradas no programa em D. Se você criar um teste chamado profile:::tick-nsec para algum inteiro n, o provedor de perfil criará um teste que dispare a cada n segundos. Digite o seguinte código-fonte e salve-o em um arquivo chamado 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);
}

Quando executado, o programa conta o número de segundos decorridos até que você pressione Control-C e depois imprime o total no 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
#

As primeiras três linhas do programa são um comentário para explicar o que o programa faz. Similar às linguagens de programação C, C++ e Java, o compilador de D ignora os caracteres entre os símbolos /* e */. Os comentários podem ser usados em qualquer lugar de um programa em D, incluindo dentro e fora das cláusulas do teste.

A cláusula do teste BEGIN define uma nova variável denominada i e atribui a ela o valor inteiro zero usando a instrução:

i = 0;

Ao contrário das linguagens de programação C, C++ e Java, as variáveis de D podem ser criadas simplesmente usando-as em uma instrução do programa; declarações explícitas de variável não são necessárias. Quando uma variável é usada pela primeira vez em um programa, o tipo da variável é definido com base no tipo da sua primeira atribuição. Cada variável possui somente um tipo durante a duração do programa, sendo assim, as referências subseqüentes devem estar de acordo com o mesmo tipo da atribuição inicial. Em counter.d, a primeira atribuição da variável i é uma constante de inteiro zero, sendo assim, seu tipo é definido como int. D fornece os mesmos tipos básicos de dados de inteiro que C, incluindo:

char

Caractere ou inteiro de byte único 

int

Inteiro padrão 

short

Inteiro curto 

long

Inteiro longo 

long long

Inteiro longo estendido 

Os tamanhos destes tipos dependem do modelo de dados do kernel do sistema operacional, descrito no Capítulo 2Tipos, operadores e expressões. D também fornece nomes amigáveis internos para tipos de inteiros atribuídos e não atribuídos de vários tamanhos fixos, assim como milhares de outros tipos que são definidos pelo sistema operacional.

A parte central de counter.d é a cláusula do teste que incrementa o contador i:

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

Esta cláusula nomeia o teste profile:::tick-1sec, que informa ao provedor profile para criar um novo teste que é disparado uma vez por segundo em um processador disponível. A cláusula contém duas instruções, a primeira atribuindo i como o valor anterior mais um e a segunda rastreando o novo valor de i. Todos os operadores aritméticos usuais de C estão disponíveis em D. A lista completa pode ser encontrada no Capítulo 2Tipos, operadores e expressões. Além disso, como em C, o operador ++ pode ser usado como abreviação para incrementar a variável correspondente em um. A função trace() toma qualquer expressão de D como argumento, assim, você poderia escrever counter.d de forma mais concisa, como a seguir:

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

Se você desejar controlar explicitamente o tipo da variável i, poderá colocar o tipo desejado entre parênteses quando atribuí-lo para converter o inteiro zero em um tipo específico. Por exemplo, caso desejasse determinar o tamanho máximo de um char em D, você poderia alterar a cláusula BEGIN da seguinte maneira:

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

Após executar counter.d por um tempo, você verá o valor tracejado aumentar e depois voltar para zero. Se você não tiver paciência para esperar o valor voltar para zero, tente alterar o nome do teste profile para profile:::tick-100msec para fazer um contador que aumente a cada 100 milissegundos ou 10 vezes por segundo.

Predicados

Uma diferença importante entre D e as outras linguagens de programação como C, C++ e Java é a ausência de construções de fluxo de controle, como instruções if e loops. As cláusulas do programa em D são escritas como listas únicas de instrução em linha reta que rastreiam uma quantidade opcional, fixa de dados. D fornece a capacidade de rastrear dados condicionalmente e modificar o fluxo de controle usando expressões lógicas chamadas predicados que podem ser usadas para prefixar cláusulas do programa. Uma expressão de predicado é avaliada quando o teste é disparado, antes da execução de quaisquer instruções associadas à cláusula correspondente. Se o predicado for avaliado como verdadeiro, representado por um valor diferente de zero, a lista de instruções será executada. Se o predicado for falso, representado por um valor zero, nenhuma das instruções será executada e o teste não será acionado.

Digite o seguinte código-fonte para o próximo exemplo e salve-o em um arquivo chamado countdown.d:

dtrace:::BEGIN
{
	i = 10;
}

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

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

Este programa em D implementa um temporizador de contagem regressiva de 10 segundos usando predicados. Quando executado, countdown.d começa uma contagem regressiva a partir de 10 e depois imprime uma mensagem e encerra:

# 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!
# 

Este exemplo usa o teste BEGIN para inicializar um inteiro i como 10 para iniciar a contagem regressiva. Depois, como no exemplo anterior, o programa usa o teste tick-1sec para implementar um temporizador que é acionado uma vez por segundo. Observe que em countdown.d, a descrição do teste tick-1sec é usada em duas cláusulas diferentes, cada uma com um predicado e uma lista de ações diferentes. O predicado é uma expressão lógica entre barras / / que aparece após o nome do teste e antes das chaves { } que delimitam a lista de instruções da cláusula.

O primeiro predicado testa se i é maior que zero, indicando que o temporizador ainda está sendo executado:

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

O operador relacional > significa maior que e retorna o valor de inteiro zero para falso e um para verdadeiro. Todos os operadores relacionais de C são suportados em D. A lista completa pode ser encontrada no Capítulo 2Tipos, operadores e expressões. Se i ainda não for zero, o script rastreia i e depois o diminui em um usando o operador --.

O segundo predicado usa o operador == para retornar verdadeiro quando i for exatamente igual a zero, indicando que a contagem regressiva está concluída:

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

Similar ao primeiro exemplo, hello.d, countdown.d usa uma seqüência de caracteres entre aspas duplas, chamada de constante de seqüências, para imprimir uma mensagem final quando a contagem regressiva estiver concluída. A função exit() é então usada para encerrar dtrace e retornar ao prompt do shell.

Se você analisar a estrutura de countdown.d, verá que ao criar duas cláusulas com a mesma descrição de teste mas predicados e ações diferentes, você criou o fluxo lógico eficientemente:

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

Quando você desejar escrever programas complexos usando predicados, tente primeiro visualizar seu algoritmo desta maneira, e depois transforme cada caminho de suas construções condicionais em uma cláusula e um predicado separados.

Agora, vamos combinar predicados com um novo provedor, o syscall , e criar nosso primeiro programa real de rastreio em D. O provedor syscall permite que você ative testes na entrada ou retorno de qualquer chamada do sistema Solaris. O próximo exemplo usa o DTrace para observar cada vez que o shell realiza uma chamada do sistema de read(2) ou write(2). Primeiro, abra duas janelas no terminal, uma para o DTrace e a outra contendo o processo do shell que você vai observar. Na segunda janela, digite o seguinte comando para obter o ID do processo deste shell:


# echo $$
12345

Agora, volte para a primeira janela do terminal e digite o seguinte programa em D e salve-o em um arquivo chamado rw.d. Quando você digitar o programa, substitua 12345 pelo ID do processo do shell impresso em resposta ao seu comando echo.

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

}

Observe que o corpo da cláusula do teste de rw.d é deixado em branco porque o programa destina-se somente a rastrear a notificação de disparos de teste e não a rastrear dados adicionais. Quando terminar de digitar no rw.d, use o dtrace para iniciar o seu experimento e depois vá para a segunda janela do shell e digite alguns comandos, pressionando a tecla de retorno após cada comando. Enquanto você digita, verá dtrace reportar testes acionados na primeira janela, similar ao seguinte exemplo:


# 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 
...

Você agora está observando o shell realizar chamadas do sistema de read(2) e write(2) para ler um caractere da janela do terminal e retornar o resultado! Este exemplo inclui muitos dos conceitos descritos até agora e também alguns novos. Primeiro, para instrumentar read(2) e write(2) da mesma maneira, o script usa uma única cláusula de teste com várias descrições de teste separando as descrições com vírgulas, da seguinte maneira:

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

Por questões de legibilidade, a descrição de cada teste aparece em sua própria linha. Esta organização não é obrigatória, mas facilita a leitura do script. Em seguida, o script define um predicado que corresponde somente às chamadas do sistema que são executadas pelo processo do shell:

/pid == 12345/

O predicado usa a variável predefinida do DTrace pid, que sempre tem o valor do ID do processo associado ao segmento que acionou o teste correspondente. O DTrace oferece muitas definições de variáveis internas para coisas úteis como o ID do processo. Veja a seguir uma lista de algumas variáveis do DTrace que você pode usar para escrever seus primeiros programas em D:

Nome da variável 

Tipo de dados 

Significado 

errno

int

Valor do errno atual para chamadas do sistema

execname

string

Nome do arquivo executável do processo atual 

pid

pid_t

ID do processo atual 

tid

id_t

ID do segmento atual 

probeprov

string

Campo do provedor da descrição do teste atual 

probemod

string

Campo do módulo da descrição do teste atual 

probefunc

string

Campo da função da descrição do teste atual 

probename

string

Campo do nome da descrição do teste atual 

Agora que você escreveu um programa de instrumentação real, tente experimentá-lo nos diferentes processos em execução no seu sistema, alterando o ID do processo e os testes de chamada do sistema que são instrumentados. Depois, você pode fazer mais uma simples alteração e transformar o rw.d em uma versão muito simples de uma ferramenta de rastreio de chamada do sistema como truss(1). Um campo de descrição de teste vazio atua como um curinga, correspondendo a qualquer teste, sendo assim, altere o programa para o novo código-fonte a seguir para rastrear qualquer chamada do sistema executada pelo shell:

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

}

Tente digitar alguns comandos no shell como cd, ls e date e veja o que o programa DTrace reporta.

Formatação de saída

O rastreio de chamada do sistema é uma maneira eficiente de observar o comportamento da maioria dos processos do usuário. Se você já tiver usado o recurso truss(1) do Solaris antes como administrador ou desenvolvedor, provavelmente já aprendeu que ele é uma ferramenta útil de se ter por perto sempre que houver um problema. Se você nunca tiver usado truss antes, faça uma experiência agora digitando este comando em um dos shells:


$ truss date

Você verá um rastreio formatado de todas as chamadas do sistema executadas por date(1) seguido pelo resultado de uma linha no final. O exemplo a seguir apresenta uma melhoria em relação ao programa rw.d anterior formatando seu resultado para ficar mais parecido com truss(1) para que você possa compreender o resultado com mais facilidade. Digite o seguinte programa e salve-o em um arquivo chamado trussrw.d :


Exemplo 1–2 trussrw.d: rastrear chamadas do sistema com formato de saída 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);
}

Neste exemplo, a constante 12345 é substituída pelo rótulo $1 em cada predicado. Esse rótulo permite que você especifique o processo de seu interesse como um argumento para o script: $1 é substituído pelo valor do primeiro argumento quando o script é compilado. Para executar trussrw.d, use as opções -q e -s do dtrace, seguidas pelo ID do processo do shell como o argumento final. A opção - q indica que dtrace deve ser silencioso e suprimir a linha do cabeçalho e as colunas CPU e ID mostradas nos exemplos anteriores. Como resultado, você verá somente a saída dos dados que rastreou explicitamente. Digite o seguinte comando (substituindo 12345 pelo ID de um processo do shell) e depois pressione a tecla de retorno algumas vezes no shell especificado:


# 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
#

Agora, vamos examinar o programa em D e a sua saída mais detalhadamente. Primeiro, uma cláusula similar ao programa anterior instrumenta cada uma das chamadas do shell para read(2) e write(2). Mas, para este exemplo, uma nova função, printf(), é usada para rastrear dados e imprimi-los em um formato específico:

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

A função printf() combina a capacidade de rastrear dados, como se fosse pela função trace() usada anteriormente, com a capacidade de retornar os dados e outro texto em um formato específico descrito por você. A função printf() informa ao DTrace para rastrear os dados associados a cada argumento após o primeiro argumento e depois para formatar os resultados usando as regras descritas pelo primeiro argumento de printf (), conhecido como seqüência de formato.

A seqüência de formato é uma seqüência regular que contém inúmeras conversões de formato, cada uma começando com o caractere %, que descreve como formatar o argumento correspondente. A primeira conversão na seqüência de formato corresponde ao segundo argumento printf(), a segunda conversão ao terceiro argumento, e assim por diante. Todo o texto entre conversões é impresso textualmente. O caractere que segue o caractere de conversão % descreve o formato a ser usado para o argumento correspondente. Veja a seguir os significados das três conversões de formato usadas em trussrw.d:

%d

Imprimir o valor correspondente como inteiro decimal 

%s

Imprimir o valor correspondente como uma seqüência 

%x

Imprimir o valor correspondente como um inteiro hexadecimal 

A função printf() do DTrace funciona como a rotina de biblioteca de C printf(3C) ou o recurso printf(1) do shell. Se você nunca tiver visto a função printf() antes, os formatos e as opções são explicados em detalhes no Capítulo 12Formatação de saída. Você deve ler esse capítulo atentamente mesmo que já esteja familiarizado com a printf() de outra linguagem. Em D, printf() é interna e algumas conversões de formato novas específicas para o DTrace estão disponíveis.

Para ajudá-lo a escrever programas corretos, o compilador de D valida cada seqüência de formato de printf() em comparação com a lista de argumentos. Tente alterar probefunc na cláusula acima para o inteiro 123. Se você executar o programa modificado, verá uma mensagem de erro informando que a conversão de formato de seqüência %s não é apropriada para uso com um argumento de inteiro:


# 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
#

Para imprimir o nome da chamada do sistema read ou write e seus argumentos, use a instrução printf():

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

para rastrear o nome da função atual do teste e os primeiros três argumentos de inteiro para a chamada do sistema, disponíveis nas variáveis arg0, arg1 e arg2 do DTrace. Para obter mais informações sobre os argumentos do teste, consulte o Capítulo 3Variáveis. O primeiro argumento para read(2) e write(2) é um descritor de arquivo, impresso em decimal. O segundo argumento é um endereço de buffer, formatado como um valor hexadecimal. O argumento final é o tamanho do buffer, formatado como um valor decimal. O especificador de formato %4d é usado para o terceiro argumento a fim de indicar que o valor deve ser impresso usando-se a conversão de formato %d com uma largura de campo mínima de 4 caracteres. Se o inteiro tiver menos que 4 caracteres, printf() irá inserir espaços em branco extras para alinhar o resultado.

Para imprimir o resultado da chamada do sistema e completar cada linha do resultado, use a seguinte cláusula:

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

Observe que o provedor syscall também publica um teste chamado return para cada chamada do sistema além de entry. A variável arg1 do DTrace para os testes return de syscall tem o valor de return da chamada do sistema. O valor de return é formatado como um inteiro decimal. As seqüências de caractere que começam com barras invertidas na seqüência de formato se expandem para tabulação (\t) e nova linha (\n) respectivamente. Essas seqüências de escape facilitam a impressão ou o registro de caracteres difíceis de digitar. D oferece suporte ao mesmo conjunto de seqüências de escape que as linguagens de programação C, C++ e Java. A lista completa de seqüências de escape pode ser encontrada no Capítulo 2Tipos, operadores e expressões.

Matrizes

D permite que você defina variáveis que sejam inteiros, assim como outros tipos para representar seqüências e tipos compostos chamados structs e uniões. Se você estiver familiarizado com a programação C, ficará feliz em saber que pode usar em D todos os tipos que pode em C. Se você não for especialista em C, não se preocupe: os tipos diferentes de dados são todos descritos no Capítulo 2Tipos, operadores e expressões. D também oferece suporte a um tipo especial de variável chamada de matriz de associação. Uma matriz de associação é similar a uma matriz normal, pois ela associa um conjunto de chaves a um conjunto de valores, mas em uma matriz de associação, as chaves não são limitadas a inteiros de um intervalo fixo.

As matrizes de associação em D podem ser indexadas por uma lista de um ou mais valores de qualquer tipo. Juntos, os valores das chaves individuais formam uma tupla que é usada para indexar na matriz e acessar ou modificar o valor correspondente a essa chave. Cada tupla usada com uma determinada matriz de associação deve estar de acordo com a assinatura de mesmo tipo; ou seja, cada chave da tupla deve ter o mesmo tamanho e ter os mesmos tipos de chave na mesma ordem. O valor associado a cada elemento de uma determinada matriz de associação é também de um tipo fixo único para toda a matriz. Por exemplo, a seguinte instrução de D define uma nova matriz de associação a do tipo de valor int com a assinatura de tupla [ string, int ] e armazena o valor de inteiro 456 na matriz:

a["hello", 123] = 456;

Quando uma matriz é definida, seus elementos podem ser acessados como qualquer outra variável de D. Por exemplo, a seguinte instrução de D modifica o elemento da matriz previamente armazenado em a aumentando o valor de 456 para 457:

a["hello", 123]++;

Os valores dos elementos de uma matriz que você ainda não tenha atribuído são definidos como zero. Vamos usar uma matriz de associação em um programa em D. Digite o seguinte programa e salve-o em um arquivo chamado rwtime.d:


Exemplo 1–3 rwtime.d: chamadas read(2) e write(2) de tempo

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]);
}

Assim como em trussrw.d, especifique o ID do processo do shell quando executar rwtime.d. Se você digitar alguns comandos do shell, verá o tempo decorrido durante cada chamada do sistema. Digite o seguinte comando e pressione a tecla de retorno algumas vezes no seu outro 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 rastrear o tempo decorrido para cada chamada do sistema, você deve instrumentar a entrada e o retorno de read(2) e write(2) e fazer amostragem do tempo em cada ponto. Em seguida, no retorno de uma determinada chamada do sistema, você deve calcular a diferença entre o primeiro e o segundo carimbo de data/hora. Você poderia usar variáveis separadas para cada chamada do sistema, mas assim seria complicado estender o programa a chamadas adicionais do sistema. Em vez disso, é mais fácil usar uma matriz de associação indexada pelo nome da função do teste. Esta é a primeira cláusula do teste:

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

Esta cláusula define uma matriz denominada ts e atribui ao membro apropriado o valor da variável timestamp do DTrace. Essa variável retorna o valor de um contador de nanossegundos progressivo, similar à rotina de biblioteca do Solaris gethrtime(3C). Quando o carimbo de data/hora da entrada é salvo, o teste de retorno correspondente realiza a amostragem de timestamp novamente e reporta a diferença entre a hora atual e o valor salvo:

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

O predicado no teste de retorno requer que o DTrace esteja rastreando o processo apropriado e que o teste entry correspondente já tenha sido acionado e atribuído um valor diferente de zero a ts[probefunc]. Esse truque elimina resultados inválidos quando o DTrace é iniciado pela primeira vez. Se o shell já estiver aguardando em uma chamada do sistema read(2) por entrada quando você executar dtrace, o teste read:return será acionado sem um read:entry precedente para essa primeira read(2) e ts[probefunc] terá valor zero porque ainda não foi atribuído.

Tipos e símbolos externos

A instrumentação do DTrace é executada dentro do kernel do sistema operacional Solaris, assim, além de acessar argumentos de teste e variáveis especiais do DTrace, você também pode acessar estruturas de dados do kernel, símbolos e tipos. Esses recursos permitem que usuários avançados do DTrace, administradores, pessoal de serviço e desenvolvedores de drivers examinem o comportamento de baixo nível do kernel do sistema operacional e drivers de dispositivo. A lista de leitura no início deste livro inclui livros que podem ajudá-lo a aprender mais sobre as partes internas do sistema operacional Solaris.

D usa aspa invertida (`) como um operador de escopo especial para acessar símbolos definidos no sistema operacional e não no programa em D. Por exemplo, o kernel do Solaris contém uma declaração C de um sistema ajustável chamado kmem_flags para ativar os recursos de depuração do alocador de memória. Consulte o Solaris Tunable Parameters Reference Manual para obter informações sobre kmem_flags. Esse ajuste é declarado em C no código-fonte do kernel da seguinte maneira:

int kmem_flags;

Para rastrear o valor desta variável em um programa em D, você pode escrever a instrução de D:

trace(`kmem_flags);

DTrace associa cada símbolo do kernel ao tipo usado para ele no código correspondente de C no sistema operacional, fornecendo fácil acesso baseado em fonte às estruturas de dados do sistema operacional nativo. Os nomes de símbolos do kernel são mantidos em um espaço de nome separado de identificadores de função e variáveis de D, assim, você não precisa se preocupar com o conflito entre esses nomes e o das variáveis de D.

Você agora completou um rápido tour do DTrace e aprendeu muitos dos blocos de construção básicos do DTrace necessários para a construção de programas em D maiores e mais complexos. Os capítulos a seguir descrevem o conjunto completo de regras de D e demonstram como o DTrace pode estabelecer medidas complexas de desempenho e análise funcional do sistema facilmente. Posteriormente, você verá como usar o DTrace para conectar o comportamento do aplicativo do usuário ao comportamento do sistema, dando a você a capacidade de analisar toda a pilha do software.

Você está apenas começando!