Manuel de suivi dynamique Solaris

Chapitre 1 Introduction

Bienvenue dans le Guide de suivi dynamique du système d'exploitation Solaris ! Si vous avez toujours souhaité comprendre le comportement de votre système, DTrace est l'outil qu'il vous faut. DTrace est un utilitaire de suivi dynamique complet. Intégré à Solaris, il peut être utilisé par des administrateurs et des développeurs travaillant sur des systèmes de production en direct, à des fins d'analyse du comportement des programmes utilisateur et du système d'exploitation lui-même. DTrace vous permet d'explorer votre système, d'en comprendre le fonctionnement, de suivre les problèmes de performance dans les nombreuses couches du logiciel ou de localiser la cause d'un comportement anormal. Vous constaterez que DTrace vous permet de créer vos propres programmes personnalisés pour instrumenter le système de manière dynamique et fournir des réponses concises et immédiates aux questions arbitraires que vous pouvez formuler à l'aide du langage de programmation D de DTrace. La première section de ce chapitre fournit une brève introduction à DTrace et vous explique comment écrire votre tout premier programme D. Le reste du chapitre présente l'ensemble des règles de programmation en D ainsi que des conseils et techniques pour procéder à une analyse approfondie de votre système. Vous pouvez partager votre expérience et vos scripts DTrace avec la communauté DTrace sur Internet à l'adresse http://www.sun.com/bigadmin/content/dtrace/. Tous les scripts donnés dans ce guide à titre d'exemple figurent dans le répertoire /usr/demo/dtrace de votre système Solaris.

Démarrage

DTrace vous aide à comprendre un système logiciel en vous permettant de modifier de manière dynamique le noyau du système d'exploitation et les processus utilisateur afin d'enregistrer les données que vous indiquez à des emplacements choisis, appelés sondes. Une sonde est un emplacement ou une activité à laquelle DTrace peut lier une requête pour effectuer une série d'actions, comme l'enregistrement d'un suivi de pile, d'un horodatage ou d'un argument à une fonction. Les sondes s'apparentent à des capteurs programmables répartis sur l'ensemble de votre système Solaris, à des emplacements stratégiques. Si vous souhaitez en comprendre le fonctionnement, utilisez DTrace pour programmer les capteurs appropriés afin d'enregistrer les informations que vous estimez utiles. Ensuite, dès qu'une sonde se déclenche, DTrace rassemble les données obtenues et les consigne dans un rapport. Si vous ne spécifiez pas d'action à effectuer pour une sonde, DTrace se contentera de prendre note de chaque déclenchement de sonde.

Dans DTrace, chaque sonde porte deux noms : un ID unique sous forme de nombre entier et une chaîne interprétable par l'utilisateur. Notre apprentissage de DTrace va commencer par la création de requêtes très simples avec la sonde appelée BEGIN, qui se déclenche à chaque nouvelle requête de suivi. Vous pouvez utiliser l'option -n de l'utilitaire dtrace(1M) pour activer une sonde utilisant son nom de chaîne. Tapez la commande suivante :


# dtrace -n BEGIN

Après un court instant, DTrace vous informe de l'activation de la sonde et une ligne de sortie s'affiche, indiquant que la sonde BEGIN s'est déclenchée. Dès l'affichage de cette sortie, dtrace reste à l'état de pause dans l'attente du déclenchement d'autres sondes. Puisque vous n'avez pas activé d'autres sondes et que BEGIN ne se déclenche qu'une seule fois, appuyez sur Control-C dans votre shell pour quitter dtrace et revenir à votre invite de shell :


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

La sortie indique que la sonde appelée BEGIN s'est déclenchée une fois et son nom ainsi que son ID de nombre entier, 1, sont affichés. Notez que, par défaut, le nom sous forme de nombre entier de la CPU sur laquelle la sonde s'est déclenchée s'affiche. Dans cet exemple, la colonne CPU indique que la commande dtrace était en cours d'exécution sur la CPU 0 lors du déclenchement de la sonde.

Vous pouvez créer des requêtes DTrace avec n'importe quel nombre de sondes et d'actions. Procédons à la création d'une requête simple avec deux sondes en ajoutant la sonde END à la commande de l'exemple précédent. La sonde END se déclenche lors de l'achèvement de l'opération de suivi. Saisissez la commande suivante, puis appuyez à nouveau sur Control-C dans votre shell après l'affichage de la ligne de sortie pour la sonde 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
#

Vous constaterez qu'en appuyant sur Control-C pour quitter dtrace la sonde END se déclenche. dtrace signale le déclenchement de cette sonde avant la fermeture du programme.

Maintenant que vous en savez un peu plus sur le nommage et l'activation de sondes, vous êtes prêt à écrire la version DTrace du programme classique “Hello, World.” Vous pouvez non seulement saisir les tentatives DTrace sur la ligne de commande, mais également les écrire dans des fichiers texte avec le langage de programmation D. Dans un éditeur de texte, créez un nouveau fichier appelé hello.d et saisissez votre premier programme D :


Exemple 1–1 hello.d : Hello, World en langage de programmation D

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

Après avoir enregistré votre programme, vous pouvez l'exécuter avec l'option -s de dtrace. Tapez la commande suivante :


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

Comme vous pouvez le constater, dtrace affiche la même sortie que précédemment, suivie du texte “hello, world”. Contrairement à l'exemple précédent, vous n'avez pas eu besoin d'attendre ou d'appuyer sur Control-C. Ces modifications ont été le résultat des actions spécifiées pour la sonde BEGIN dans hello.d. Procédons à l'exploration de la structure de votre programme D de façon plus détaillée afin de comprendre ce qui s'est produit.

Chaque programme D se compose d'une série de clauses, chacune d'entre elles décrivant une ou plusieurs sondes à activer et un ensemble d'actions facultatives à exécuter lors du déclenchement de la sonde. Ces actions sont répertoriées sous la forme d'une série d'instructions entre accolades { } précédées du nom de la sonde. Chaque instruction se termine par un point-virgule (;). Votre première instruction utilise la fonction trace() pour indiquer que DTrace doit enregistrer l'argument spécifié, la chaîne “hello, world”, lors du déclenchement de la sonde BEGIN puis afficher le résultat. La seconde instruction utilise la fonction exit() pour indiquer que DTrace doit interrompre le suivi et quitter la commande dtrace. DTrace fournit un ensemble de fonctions utiles telles que trace() et exit() pour appeler vos programmes D. Pour appeler une fonction, spécifiez son nom suivi d'une liste d'arguments entre parenthèses. Toutes les fonctions D sont décrites au·Chapitre10Actions et sous-routines.

À présent, si vous connaissez le langage de programmation C, vous avez dû vous rendre compte, à la lumière des exemples et des noms utilisés, que le langage de programmation D présentait de fortes similarités avec C. En effet, D est dérivé d'un grand sous-ensemble de C, combiné à un ensemble spécial de fonctions et de variables permettant de faciliter le suivi. Ces fonctionnalités vous seront présentées plus en détails dans les chapitres suivants. Si vous avez déjà écrit un programme C auparavant, vous pourrez transférer immédiatement la plupart de vos connaissances pour créer des programmes de suivi en D. Dans le cas contraire, l'apprentissage de D reste très accessible. À la fin de ce chapitre, vous maîtriserez l'ensemble de la syntaxe. Mais pour commencer, oublions un peu les règles du langage pour nous intéresser au fonctionnement de DTrace, avant de revenir à l'apprentissage de la création de programmes D plus intéressants.

Fournisseurs et sondes

Dans les exemples précédents, vous avez appris à utiliser deux sondes simples appelées BEGIN et END. Mais d'où ces sondes proviennent-elles ? Les sondes DTrace proviennent d'un ensemble de modules de noyau appelés fournisseurs, dont chacun exécute une instrumentation particulière pour créer des sondes. Lorsque vous utilisez DTrace, chaque fournisseur a la possibilité de publier les sondes qu'il peut fournir dans la structure DTrace. Vous pouvez ensuite activer vos actions de suivi et les lier à n'importe quelle sonde publiée. Pour répertorier toutes les sondes disponibles sur votre système, tapez la commande suivante :


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

#

Vous risquez de devoir patienter quelques instants avant que la sortie ne s'affiche. Pour connaître le nombre total de sondes, vous pouvez taper la commande suivante :


# dtrace -l | wc -l
        30122

Il est possible que le total affiché par votre machine ne soit pas le même car le nombre de sondes dépend de votre plate-forme d'exploitation ainsi que des logiciels installés. Comme vous pouvez le constater, le nombre de sondes disponibles est très élevé, permettant ainsi d'observer les moindres recoins, autrefois obscurs, de votre système. En fait, même cette sortie n'est pas la liste complète car, comme vous le verrez ultérieurement, certains fournisseurs offrent la possibilité de créer de nouvelles sondes à la volée, en fonction de vos requêtes de suivi, rendant ainsi le nombre réel de sondes DTrace presque illimité.

Revenez maintenant à la sortie de dtrace -l dans la fenêtre de votre terminal. Vous pouvez remarquer que chaque sonde comporte les deux noms mentionnés précédemment, à savoir un ID sous forme de nombre entier et un nom interprétable par l'utilisateur. Le nom interprétable par l'utilisateur se compose de quatre parties, présentées sous la forme de colonnes séparées dans la sortie dtrace. Un nom de sonde se compose des quatre parties suivantes :

Fournisseur 

Nom du fournisseur DTrace publiant cette sonde. Il correspond généralement au nom du module de noyau DTrace qui exécute l'instrumentation permettant d'activer la sonde. 

Module 

Si cette sonde correspond à l'emplacement d'un programme spécifique, nom du module dans lequel se situe la sonde. Il s'agit soit du nom d'un module de noyau, soit du nom d'une bibliothèque utilisateur. 

Fonction 

Si cette sonde correspond à l'emplacement d'un programme spécifique, nom de la fonction du programme dans laquelle se situe la sonde. 

Nom 

Le composant final du nom de la sonde est un nom vous donnant une idée approximative de la signification sémantique de la sonde, comme BEGIN ou END.

Lors de l'écriture du nom complet d'une sonde, interprétable par l'utilisateur, écrivez les quatre parties du nom séparées par deux points, comme suit :

nom:fonction:module:fournisseur

Vous remarquerez que certaines sondes de la liste n'ont ni module ni fonction, comme les sondes BEGIN et END utilisées précédemment. Dans le cas de certaines sondes, ces champs restent vides car elles ne correspondent ni à une fonction ni à un emplacement de programme instrumenté. À la place, ces sondes font référence à un concept plus abstrait comme l'idée de fin de votre requête de suivi. Les sondes dont le nom est composé d'un module ou d'une fonction sont appelées sondes ancrées, tandis que les autres sont qualifiées de non ancrées.

Par convention, si vous ne spécifiez pas tous les champs d'un nom de sonde, DTrace effectue la recherche sur l'ensemble des sondes dont les parties du nom spécifiées correspondent aux critères. En d'autres termes, lorsque vous avez utilisé précédemment le nom de sonde BEGIN, cela revenait en fait à demander à DTrace de rechercher n'importe quelle sonde dont le champ de nom correspondait à BEGIN, quelle qu'ait été la valeur des champs fournisseur, module et fonction. Dans le cas présent, une seule sonde correspond à ces critères ; le résultat reste donc identique. Mais vous savez désormais que le vrai nom de la sonde BEGIN est dtrace:::BEGIN, ce qui indique que cette sonde est fournie par la structure DTrace elle-même et qu'elle n'est ancrée à aucune fonction. Par conséquent, le programme hello.d aurait pu être écrit comme suit et donner un résultat identique :

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

Maintenant que vous avez compris d'où viennent les sondes et comment elles sont nommées, nous allons en apprendre davantage sur ce qui se produit lorsque vous activez des sondes et que vous demandez à DTrace d'exécuter des actions. Nous reprendrons ensuite notre présentation de D.

Compilation et instrumentation

Lorsque vous écrivez des programmes classiques dans Solaris, vous utilisez un compilateur pour convertir le code source de votre programme en code objet exécutable. Lorsque vous utilisez la commande dtrace, vous demandez au compilateur du langage D utilisé précédemment d'écrire le programme hello.d. Une fois compilé, votre programme est envoyé au noyau du système d'exploitation en vue d'être exécuté par DTrace. Les sondes nommées dans votre programme sont activées et le fournisseur correspondant exécute l'instrumentation nécessaire pour les activer.

L'ensemble de l'instrumentation dans DTrace est complètement dynamique : les sondes ne sont activées que lorsque vous les utilisez. Aucun code instrumenté n'est présent pour les sondes inactives ; votre système ne rencontre donc aucune dégradation des performances lorsque vous n'utilisez pas DTrace. Lorsque votre tentative est terminée et que la commande dtrace se ferme, toutes les sondes utilisées sont automatiquement désactivées et leur instrumentation supprimée, restaurant votre système à son état exact d'origine. Il n'existe aucune réelle différence entre un système sur lequel DTrace est inactif et un système sur lequel le logiciel DTrace n'est pas installé.

L'instrumentation de chaque sonde est réalisée de façon dynamique sur le système d'exploitation en cours d'exécution en direct ou sur les processus utilisateur sélectionnés. À aucun moment le système ne passe à l'état de veille ou de pause et le code d'instrumentation n'est ajouté que pour les sondes que vous activez. Ainsi, l'impact de l'utilisation de DTrace sur la sonde se limite uniquement aux actions que vous demandez à DTrace d'exécuter : aucune donnée étrangère ne fait l'objet d'un suivi, aucun “commutateur de suivi” d'envergure n'est activé sur le système et l'instrumentation DTrace dans son ensemble est conçue pour être aussi efficace que possible. Ces fonctions vous permettent d'utiliser DTrace en production pour résoudre des problèmes effectifs en temps réel.

La structure DTrace prend également en charge un nombre arbitraire de clients virtuels. Vous pouvez exécutez autant d'expériences et de commandes DTrace que vous le souhaitez, à condition de ne pas dépasser la limite de capacité de mémoire de votre système. Les commandes fonctionnent toutes de manière indépendante en utilisant la même instrumentation sous-jacente. Cette même fonction permet également à un nombre quelconque d'utilisateurs différents présents sur le même système d'utiliser DTrace simultanément : les développeurs, les administrateurs et les techniciens peuvent tous travailler ensemble ou sur des problèmes différents sur le même système en utilisant DTrace, sans qu'il n'y ait d'interférence entre eux.

Contrairement aux programmes écrits en C et C++ et à l'instar des programmes écrits dans le langage de programmation JavaTM les programmes D de DTrace sont compilés dans un formulaire intermédiaire sécurisé exécuté lors du déclenchement des sondes. Ce formulaire intermédiaire est validé à des fins de sécurité lorsque votre programme est examiné pour la première fois par le logiciel de noyau DTrace. L'environnement d'exécution DTrace gère également toutes les erreurs d'exécution susceptibles de se produire pendant l'exécution de votre programme D, comme la division par zéro, le déréférencement de mémoire non valide, etc. et les consigne dans un rapport. Il est donc impossible de créer un programme DTrace non sûr, qui risquerait d'endommager par inadvertance le noyau Solaris ou l'un des processus en cours d'exécution sur votre système. Ces fonctions de sécurité vous permettent d'utiliser DTrace dans un environnement de production, sans risque d'arrêt brutal ou d'endommagement de votre système. Si vous faites une erreur de programmation, DTrace la consigne dans un rapport et désactive votre instrumentation. Vous pouvez alors corriger l'erreur et réessayer. Les fonctions DTrace de création de rapports d'erreurs et de débogage sont décrites ultérieurement dans ce manuel.

Le diagramme suivant présente les différents composants de l'architecture DTrace, notamment les fournisseurs, les sondes, le logiciel de noyau DTrace et la commande dtrace.

Figure 1–1 Aperçu de l'architecture DTrace et de ses composants

Architecture DTrace : l'utilitaire de noyau et les fournisseurs, une interface pilote entre le noyau et la bibliothèque et la bibliothèque prenant en charge un ensemble de commandes.

Maintenant que vous avez assimilé le fonctionnement de DTrace, revenons à la présentation du langage de programmation D pour commencer à écrire des programmes plus intéressants.

Variables et expressions arithmétiques

Notre prochain programme donné à titre d'exemple utilise le fournisseur profile DTrace pour implémenter un compteur temporel simple. Le fournisseur de profil permet de créer de nouvelles sondes en fonction des descriptions trouvées dans votre programme D. Si vous créez une sonde appelée profile:::tick-nsec pour certains nombres entiers n, le fournisseur de profil va créer une sonde qui se déclenche toutes les n secondes. Entrez le code source suivant et enregistrez-le dans le fichier 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);
}

Lorsqu'il est exécuté, le programme compte le nombre de secondes écoulées avant que vous n'appuyiez sur Control-C puis affiche le total à la fin :


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

Les trois premières lignes du programme sont des lignes de commentaire expliquant la finalité du programme. À l'instar des langages de programmation C, C++ et Java, le compilateur D ignore tous les caractères compris entre les symboles /* et */. Les commentaires peuvent être utilisés n'importe où dans un programme D, à l'intérieur comme à l'extérieur de vos clauses de sonde.

La clause de la sonde BEGIN définit une nouvelle variable appelée i et lui assigne la valeur de nombre entier égal à zéro avec l'instruction :

i = 0;

Contrairement aux langages de programmation C, C++ et Java, il suffit d'utiliser les variables D dans une instruction de programme pour les créer ; aucune déclaration de variable explicite n'est requise. Lorsqu'une variable est utilisée pour la première fois dans un programme, son type est défini en fonction du type de sa première assignation. Chaque variable se voit assigner un seul type tout au long de la vie du programme ; les références ultérieures doivent donc correspondre au type de l'assignation d'origine. Dans counter.d, la variable i se voit d'abord assigner la valeur constante de nombre entier égal à zéro. Son type est donc défini sur int. D fournit les mêmes types de données de nombres entiers de base que C, notamment :

char

Caractère ou nombre entier à octet unique 

int

Nombre entier par défaut 

short

Nombre entier court 

long

Nombre entier long 

long long

Nombre entier long étendu 

Les formats de ces types dépendent du modèle de données du noyau du système d'exploitation, décrit dans le Chapitre2Types, opérateurs et expressions D fournit également des noms conviviaux intégrés pour les types de nombres entiers signés et non signés de taille fixe différente, ainsi que des milliers d'autres types définis par le système d'exploitation.

La partie centrale de counter.d est la clause de sonde qui incrémente le compteur i :

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

Cette clause nomme la sonde profile:::tick-1sec, indiquant au fournisseur profile qu'il doit créer une nouvelle sonde qui se déclenche une fois par seconde sur un processeur disponible. La clause comporte deux instructions : la première assigne à i la valeur précédente plus un, la seconde procède au suivi de la nouvelle valeur de i. Tous les opérateurs arithmétiques habituels de C sont disponibles en D ; la liste complète figure dans le Chapitre2Types, opérateurs et expressions Ainsi, comme dans C, l'opérateur ++ peut être utilisé comme abrégé pour incrémenter de un la variable correspondante. La fonction trace() prend comme argument n'importe quelle expression D, de façon à écrire counter.d de façon plus concise, comme suit :

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

Si vous souhaitez contrôler de façon explicite le type de variable i, vous pouvez mettre entre parenthèses le type souhaité lors de son assignation, afin de forcer le type du nombre entier égal à zéro vers un type spécifique. Par exemple, si vous souhaitez déterminer le nombre maximum de char en D, vous pouvez modifier la clause BEGIN comme suit :

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

Après l'exécution de counter.d, vous verrez la valeur faisant l'objet d'un suivi augmenter puis revenir à zéro au bout de quelque temps, une fois la recherche circulaire effectuée. Si l'attente vous semble trop longue, essayez de modifier le nom de la sonde profile en profile:::tick-100msec pour que l'incrémentation se fasse toutes les 100 millisecondes, soit 10 fois par seconde.

Prédicats

L'une des grandes différences entre D et d'autres langages de programmation comme C, C++ et Java réside dans l'absence de constructions de flux de contrôle comme les instructions Si et les boucles. Les clauses du programme D sont écrites en tant que listes d'instructions simples sur une seule ligne qui procèdent au suivi d'une quantité de données fixe, facultative. D fournit la possibilité de suivre et modifier le flux de contrôle sous certaines conditions à l'aide d'expressions logiques appelées prédicats pouvant être utilisées pour placer en préfixe des clauses du programme. Une expression de prédicat est évaluée lors du déclenchement de la sonde avant l'exécution de l'une des instructions associées à la clause correspondante. Si le prédicat renvoie True, représenté par une valeur différente de zéro, la liste d'instructions est exécutée. Si le prédicat renvoie False, représenté par une valeur égale à zéro, aucune des instructions n'est exécutée et le déclenchement de la sonde est ignoré.

Tapez le code source suivant pour le prochain exemple et enregistrez-le dans un fichier appelé countdown.d :

dtrace:::BEGIN
{
	i = 10;
}

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

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

Ce programme D implémente un compte à rebours de 10 secondes avec des prédicats. Lorsqu'il est exécuté, le fichier countdown.d effectue un compte à rebours à partir de 10, affiche un message et se ferme :

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

Cet exemple utilise la sondeBEGIN pour initialiser un nombre entier i sur 10 pour commencer le compte à rebours. Ensuite, comme dans l'exemple précédent, le programme utilise la sonde tick-1sec pour implémenter une horloge qui se déclenche une fois par seconde. Vous remarquerez que dans countdown.d, la description de la sonde tick-1sec est utilisée dans deux clauses différentes, chacune avec un prédicat et une liste d'actions différents. Le prédicat est une expression logique entre barres obliques / / qui suit le nom de la sonde et précède les accolades { } entourant la liste d'instructions.

Le premier prédicat teste si i est supérieur à zéro, indiquant que l'horloge est toujours en cours d'exécution :

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

L'opérateur relationnel > signifie supérieur à et renvoie un nombre entier d'une valeur égale à zéro pour False et égale à un pour True. Tous les opérateurs arithmétiques relationnels de C sont pris en charge en D ; la liste complète figure dans le Chapitre2Types, opérateurs et expressions Si i n'est pas encore égal à zéro, le script effectue le suivi de i, puis le décrémente en utilisant l'opérateur --.

Le second prédicat utilise l'opérateur == pour renvoyer True lorsque i est égal à zéro, indiquant que le compte à rebours est terminé :

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

Comme dans le premier exemple, hello.d countdown.d utilise une séquence de caractères entre guillemets appelée constante de chaîne, pour afficher un message final lorsque le compte à rebours est terminé. La fonction exit() est utilisée pour quitter dtrace et revenir à l'invite de shell.

Si vous regardez la structure de countdown.d, vous constaterez qu'en créant deux clauses avec la même description de sonde mais avec différents prédicats et actions, nous avons effectivement créé le flux logique :

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

Lorsque vous souhaitez écrire des programmes complexes avec des prédicats, essayez d'abord de visualiser votre algorithme de cette façon, puis transformez chaque chemin de vos constructions conditionnelles en une clause et un prédicat séparés.

Vous pouvez maintenant combiner des prédicats avec un nouveau fournisseur, le fournisseur syscall et créer votre premier vrai programme de suivi D. Le fournisseur syscall vous permet d'activer des sondes lors de l'entrée ou du renvoi de n'importe quel appel système Solaris. Le prochain exemple utilise DTrace pour observer à chaque fois que votre shell effectue un appel système read(2) ou write(2). Ouvrez d'abord deux fenêtres de terminal, l'une pour utiliser DTrace et l'autre contenant le processus de shell que vous allez observer. Dans la seconde fenêtre, tapez la commande suivante pour obtenir l'ID de processus de ce shell :


# echo $$
12345

Revenez maintenant à votre première fenêtre de terminal, tapez le programme D suivant et enregistrez-le dans un fichier appelé rw.d. En tapant le programme, remplacez 12345 par l'ID de processus du shell affiché en réponse à votre commande echo.

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

}

Vous remarquerez que la clause de sonde rw.d reste vide car le programme est uniquement destiné à suivre les notifications de déclenchement de sondes et non à suivre des données supplémentaires. Lorsque vous avez terminé la saisie dans rw.d, utilisez dtrace pour commencer votre expérience puis accédez à la seconde fenêtre de shell et tapez quelques commandes, en appuyant sur Entrée après chacune. Lors de la saisie, vous devez voir les déclenchements de sonde dtrace consignés dans un rapport dans votre première fenêtre, comme dans l'exemple qui suit :


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

Vous observez à présent votre shell exécuter des appels système read(2) et write(2) pour lire un caractère à partir de votre fenêtre de terminal et renvoyer le résultat. Cet exemple comprend un grand nombre des concepts décrits jusqu'à présent et quelques nouveaux concepts également. Tout d'abord, pour instrumenter read(2) et write(2) de la même façon, le script utilise une seule clause de sonde avec plusieurs descriptions de sonde en les séparant par des virgules, comme suit :

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

Pour une meilleure lisibilité, chaque description de sonde apparaît sur sa propre ligne. Cette disposition n'est pas strictement requise mais elle contribue à une meilleure lisibilité du script. Le script définit ensuite un prédicat correspondant uniquement aux appels système exécutés par votre processus de shell :

/pid == 12345/

Le prédicat utilise la variable DTrace prédéfinie pid, qui évalue toujours l'ID de processus associé au thread ayant déclenché la sonde correspondante. DTrace fournit de nombreuses définitions de variables intégrées pour obtenir des informations utiles comme l'ID de processus. Voici une liste de quelques variables DTrace pouvant être utilisées pour écrire vos premiers programmes D :

Nom de variable 

Type de données 

Signification 

errno

int

Valeur errno actuelle pour les appels système

execname

string

Nom du fichier exécutable du processus actuel 

pid

pid_t

ID de processus du processus actuel 

tid

id_t

ID de thread du thread actuel 

probeprov

string

Champ du fournisseur de la description de sonde actuelle 

probemod

string

Champ du module de la description de sonde actuelle 

probefunc

string

Champ de la fonction de la description de sonde actuelle 

probename

string

Champ du nom de la description de sonde actuelle 

Maintenant que vous avez écrit un vrai programme d'instrumentation, essayez de l'expérimenter sur différents processus s'exécutant sur votre système en modifiant l'ID de processus et les sondes d'appels système instrumentées. Vous pouvez ensuite effectuer une modification supplémentaire et changer rw.d en une version très simple d'un outil de suivi des appels système comme truss(1). Utiliser un champ de description de sonde vide revient à utiliser un caractère générique, correspondant à n'importe quelle sonde. Vous devez donc modifier votre programme selon le nouveau code source suivant pour suivre n'importe quel appel système exécuté par votre shell :

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

}

Essayez de taper quelques commandes dans le shell comme cd, ls et date et examinez le rapport créé par votre programme DTrace.

Format de sortie

Le suivi d'appels système est une méthode efficace pour observer le fonctionnement de la plupart des processus utilisateur. Si vous avez utilisé l'utilitaire Solaris truss(1) auparavant en tant qu'administrateur ou développeur, vous avez sans doute pu constater qu'il s'agit d'un outil utile qu'il convient de garder à portée de main en cas de problème. Si vous n'avez jamais utilisé truss auparavant, essayez-le dès à présent en tapant cette commande dans l'un de vos shells :


$ truss date

Un suivi formaté de tous les appels système exécutés par date(1) s'affiche, suivi de sa propre ligne de sortie à la fin. L'exemple suivant améliore le programme précédent rw.d en formatant sa sortie sur truss(1) de façon à mieux la comprendre. Tapez le programme suivant et enregistrez-le dans le fichier trussrw.d :


Exemple 1–2 trussrw.d : suivi des appels système avec le format de sortie 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);
}

Dans cet exemple, la constante 12345 est remplacée par l'étiquette $1 dans chaque prédicat. Cette étiquette vous permet de spécifier le processus d'intérêt en tant qu'argument au script : $1 est remplacé par la valeur du premier argument lorsque le script est compilé. Pour exécuter trussrw.d, utilisez les options dtrace, -q et -s, suivies par l'ID de processus de votre shell en tant qu'argument final. L'option - q indique que dtrace doit être silencieux et doit supprimer la ligne d'en-tête et les colonnes de CPU et d'ID présentées dans les exemples précédents. Ainsi, seul le résultat des données suivies explicitement s'affichera. Tapez la commande suivante (en remplaçant 12345 par l'ID d'un processus de shell) puis appuyez sur Entrée plusieurs fois dans le shell spécifié :


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

Examinons plus en détail votre programme D et sa sortie. D'abord, une clause similaire au programme précédent instrumente chacun des appels du shell sur read(2) et write(2). Mais pour cet exemple, une nouvelle fonction, printf(), est utilisée pour effectuer le suivi des données et les afficher dans un format spécifique.

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

La fonction printf() combine la capacité de suivi des données comme dans la fonction trace() utilisée précédemment, avec la capacité de sortie des données et d'autres textes dans un format spécifique décrit. La fonction printf() indique à DTrace de suivre les données associées à chaque argument faisant suite au premier argument, puis de formater les résultats en utilisant les règles décrites par le premier argument printf() connu sous le nom de chaîne de format.

La chaîne de format est une chaîne normale contenant n'importe quel nombre de conversions de format, chacune commençant par le caractère % qui décrit comment formater l'argument correspondant. La première conversion dans la chaîne de format correspond au second argument printf(), la seconde conversion au troisième argument, etc. L'ensemble du texte entre les conversions est affiché textuellement. Le caractère suivant le caractère de conversion % décrit le format à utiliser pour l'argument correspondant. Voici la signification des conversions de format utilisées dans trussrw.d :

%d

Imprime la valeur correspondante en tant que nombre entier décimal 

%s

Imprime la valeur correspondante en tant que chaîne 

%x

Imprime la valeur correspondante en tant que nombre entier hexadécimal 

La fonction printf() de DTrace fonctionne de la même façon que la routine de bibliothèque printf(3C) C ou l'utilitaire printf(1) du shell. Si vous n'avez jamais rencontré la fonction printf() auparavant, les formats et options sont expliqués en détails dans le Chapitre12Format de sortie. Vous devez lire ce chapitre attentivement, même si vous connaissez déjà la fonction printf() dans un autre langage. En D, printf() est fourni de façon intégrée et certaines nouvelles conversions de format sont disponibles, conçues spécifiquement pour DTrace.

Pour vous aider à écrire des messages corrects, le compilateur D valide chaque chaîne de format printf() par rapport à sa liste d'arguments. Essayez de modifier probefunc dans la clause précédente en nombre entier 123. Si vous exécutez le programme modifié, un message d'erreur s'affiche, vous informant que la conversion du format de la chaîne %s ne convient pas pour une utilisation avec un argument de nombre entier.


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

Pour afficher le nom de l'appel système de lecture et d'écriture et ses arguments, utilisez l'instruction printf() :

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

pour suivre le nom de la fonction de sonde actuelle et les trois premiers arguments de nombre entier dans l'appel système, disponible dans les variables DTrace arg0, arg1 et arg2. Pour plus d'informations sur les arguments de sonde, reportez-vous au Chapitre3Variables. Le premier argument pour read(2) et write(2) est un descripteur de fichier, imprimé en décimales. Le second argument est une adresse tampon, formatée en tant que valeur hexadécimale. L'argument final est le format tampon, formaté en tant que valeur décimale. Le spécificateur de format %4d est utilisé pour le troisième argument pour indiquer que la valeur doit être imprimée en utilisant la conversion de format %d avec un champ d'une largeur maximale de 4 caractères. Si le nombre entier fait moins de 4 caractères de large, printf() insère des espaces supplémentaires pour aligner le résultat.

Pour afficher le résultat de l'appel système et compléter chaque ligne de sortie, utilisez la clause suivante :

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

Vous remarquerez que le fournisseur syscall publie également une sonde appelée return pour chaque appel système, en plus de entry. La variable DTrace arg1 pour les sondes syscall return évalue la valeur de retour de l'appel système. La valeur de retour est formatée en tant que nombre entier décimal. Les séquences de caractères commençant par des barres obliques inversées dans la chaîne de format s'étendent à la tabulation (\t) et à la nouvelle ligne (\n) respectivement. Ces séquences d'échappement vous aident à afficher ou à enregistrer des caractères difficiles à saisir. D prend en charge le même ensemble de séquences d'échappement que les langages de programmation C, C++ et Java. Vous trouverez la liste complète des séquences d'échappement au Chapitre2Types, opérateurs et expressions.

Tableaux

D vous permet de définir des variables qui sont des nombres entiers ainsi que d'autres types pour représenter des chaînes et des types composites appelés structs et unions. Si vous connaissez la programmation en C, vous serez ravi d'apprendre que tout ce que vous pouvez saisir en C peut l'être en D. Si vous n'êtes pas un expert en C, ne vous en faites pas : les différents types de données sont tous décrits dans le Chapitre2Types, opérateurs et expressions D prend également en charge un type de variable appelé tableau associatif. Un tableau associatif ressemble à un tableau normal, dans la mesure où il associe un jeu de clés à un jeu de valeurs, mais dans un tableau associatif, les clés ne sont pas limitées aux nombres entiers d'une plage fixe.

Les tableaux associatifs D peuvent être indexés par une liste comportant au moins une valeur, de n'importe quel type. Ensemble, les valeurs des clés individuelles forment un tuple utilisé pour l'indexage dans le tableau et l'accès ou la modification de la valeur correspondant à cette clé. Chaque tuple utilisé avec un tableau associatif donné doit correspondre à la signature du même type, c'est-à-dire que toutes les clés de tuple doivent avoir la même longueur et leurs types doivent être identiques et dans le même ordre. La valeur associée à chaque élément d'un tableau associatif donné est également un type fixe unique pour l'ensemble du tableau. Par exemple, l'instruction D suivante définit un nouveau tableau associatif a, dont la valeur est de type int avec la signature de tuple [ string, int ] et stocke la valeur de nombre entier 456 dans le tableau.

a["hello", 123] = 456;

Dès qu'un tableau est défini, il est possible d'accéder à ces éléments comme n'importe quelle autre variable D. Par exemple, l'instruction D suivante modifie l'élément de tableau précédemment stocké dans a en incrémentant la valeur de 456 à 457 :

a["hello", 123]++;

Les valeurs de tous les éléments de tableau qui n'ont pas encore été attribués sont définies sur zéro. Utilisons maintenant un tableau associatif dans un programme D. Tapez le programme suivant et enregistrez-le dans le fichier rwtime.d :


Exemple 1–3 rwtime.d : heure read(2) et write(2) appels

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

Comme dans le cas de trussrw.d, spécifiez l'ID de processus shell lorsque vous exécutez rwtime.d. Si vous tapez quelques commandes shell, le temps écoulé pendant chaque appel système s'affiche. Tapez la commande suivante puis appuyez sur Entrée plusieurs fois dans votre autre 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
#

Pour suivre le temps écoulé pour chaque appel système, vous devez instrumenter à la fois l'entrée et le retour de read(2) et write(2) et tester le temps écoulé à chaque point. Puis, lors du retour d'un appel système donné, vous devez calculer la différence entre notre premier et notre second horodatage. Il est possible d'utiliser des variables séparées pour chaque appel système, mais il peut s'avérer gênant pour le programme de s'étendre vers des appels système supplémentaires. À la place, il est plus facile d'utiliser un tableau associatif indexé par le nom de la fonction de sonde. Voici la première clause de sonde :

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

Cette clause définit un tableau appelé ts et assigne au membre approprié la valeur de la variable DTrace timestamp. Cette variable renvoie la valeur d'un compteur à incrémentation constante en nanosecondes, comme la routine de bibliothèque Solaris gethrtime(3C). Dès que l'horodatage d'entrée est enregistré, les sondes de retour correspondantes timestamp sont à nouveau testées et les différences entre l'heure actuelle et la valeur enregistrée sont consignées dans un rapport.

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

Le prédicat de la sonde de retour nécessite que DTrace effectue le suivi du processus approprié et que la sonde entry correspondante se soit déjà déclenchée et ait déjà assigné une valeur ts[probefunc] différente de zéro. Cette opération permet d'éliminer les résultats non valides lors du démarrage de DTrace. Si votre shell est déjà en attente d'un appel système read(2) pour la saisie lors de l'exécution de dtrace, la sonde read:return se déclenchera même si elle n'est pas précédée de read:entry, car ce premier read(2) et ts[probefunc] renverront une valeur égale à zéro car elle n'a pas encore été assignée.

Types et symboles externes

L'instrumentation DTrace s'exécute à l'intérieur du noyau du système d'exploitation Solaris. En plus d'accéder à des variables DTrace spéciales et à des arguments de sonde, vous pouvez donc également accéder à des structures de données du noyau, des symboles et des types. Ces capacités permettent à des utilisateurs, administrateurs, techniciens et développeurs pilotes DTrace d'étudier le fonctionnement à faible niveau du noyau du système d'exploitation et des pilotes de périphériques. La liste de lecture figurant au début de ce manuel comporte des livres pouvant vous aider à en savoir plus sur le fonctionnement interne du système d'exploitation.

D utilise le caractère d'apostrophe inversée (`) en tant qu'opérateur d'étendue spéciale pour accéder à des symboles définis dans le système d'exploitation et non dans votre programme D. Par exemple, le noyau de Solaris contient une déclaration en C du réglage système kmem_flags pour activer les fonctions de débogage du programme d'allocation de mémoire. Reportez-vous au manuel Solaris Tunable Parameters Reference Manual pour plus d'informations sur kmem_flags. Ce réglage est déclaré en C dans le code source du noyau, comme suit :

int kmem_flags;

Pour effectuer le suivi de la valeur de cette variable dans un programme D, vous pouvez écrire l'instruction D :

trace(`kmem_flags);

DTrace associe chaque symbole de noyau au type utilisé dans le code C du système d'exploitation correspondant, fournissant un accès source facile aux structures de données de système d'exploitation. Les noms des symboles de noyau sont conservés dans un espace de nom séparé de la variable D et des identificateurs de fonction. Vous n'avez donc jamais besoin de vous soucier des noms en conflit avec vos variables D.

Vous avez maintenant terminé la présentation de DTrace et vous connaissez à présent un grand nombre des blocs de construction DTrace nécessaires à la création de programmes D de plus grande envergure et d'une plus grande complexité. Les chapitres suivants décrivent l'ensemble des règles à connaître en D et montrent comment DTrace peut effectuer des mesures de performance complexes et faciliter l'analyse fonctionnelle du système. Plus tard, vous apprendrez à utiliser DTrace pour établir le lien entre le fonctionnement de l'application utilisateur et celui du système, vous donnant ainsi la possibilité d'analyser votre pile logicielle dans son intégralité.

Ce n'est que le début !