Handbuch zur dynamischen Ablaufverfolgung in Solaris

Kapitel 1 Einführung

Willkommen bei der dynamischen Ablaufverfolgung im Betriebssystem Solaris! Wenn Sie schon immer einmal das Verhalten Ihres Systems verstehen wollten, ist DTrace das richtige Tool für Sie. DTrace ist ein in Solaris integriertes umfassendes Framework für die dynamische Ablaufverfolgung (auch „Tracing“ genannt), das sowohl von Administratoren als auch Entwicklern zur Untersuchung des Verhaltens von Benutzerprogrammen und des Betriebssystems auf laufenden Produktionssystemen eingesetzt werden kann. DTrace bietet Ihnen die Möglichkeit, Ihr System genau zu durchleuchten, um seine Funktionsweise zu verstehen, Leistungsprobleme auf den verschiedensten Softwareebenen aufzuspüren oder die Ursachen von Fehlverhalten zu ermitteln. Wie Sie sehen werden, können Sie mit DTrace Ihre eigenen Programme für die dynamische Instrumentation des Systems und zur Ausgabe sofortiger, prägnanter Antworten auf beliebige Fragen schreiben, die Sie in der DTrace-Programmiersprache D formulieren. Im ersten Teil dieses Kapitels erhalten Sie eine schnelle Einführung in DTrace und erfahren, wie Sie Ihr erstes D-Programm schreiben können. Im verbleibenden Teil des Kapitels werden der vollständige Regelsatz für die Programmierung in D vorgestellt und Tipps und Techniken für tief greifende Analysen des Systems erläutert. Unter http://www.sun.com/bigadmin/content/dtrace/ können Sie Ihre Erfahrungen und DTrace-Skripten mit dem Rest der DTrace-Gemeinde teilen. Alle in diesem Handbuch aufgeführten Beispielskripten finden Sie auf Ihrem Solaris-System im Verzeichnis /usr/demo/dtrace.

Erste Schritte

DTrace hilft Ihnen, Softwaresysteme genau zu verstehen, indem Sie den Betriebssystemkernel und Benutzerprozesse dynamisch so verändern, dass an Stellen besonderen Interesses, den so genannten Prüfpunkten, zusätzliche, von Ihnen angegebene Informationen aufgezeichnet werden. Ein Prüfpunkt ist eine Stelle oder eine Aktivität, an die DTrace eine Anforderung zur Durchführung einer Gruppe von Aktionen binden kann. Dabei kann es sich beispielsweise um die Aufzeichnung eines Stackprotokolls, einer Zeitmarke oder des Arguments einer Funktion handeln. Prüfpunkte sind mit programmierbaren Sensoren oder Messfühlern vergleichbar, die an interessanten Stellen im gesamten Solaris-System zu finden sind. Wenn Sie einer Sache auf den Grund gehen möchten, programmieren Sie mit DTrace die entsprechenden Sensoren so, dass die für Sie interessanten Informationen aufgezeichnet werden. Wenn anschließend die Prüfpunkte ausgelöst werden, werden die Daten der Prüfpunkte von DTrace abgerufen und an Sie ausgegeben. Wenn Sie für einen Prüfpunkt keine Aktion festlegen, verzeichnet DTrace lediglich, wie oft der Prüfpunkt ausgelöst wird.

Jeder Prüfpunkt in DTrace besitzt zwei Namen: eine eindeutige, ganzzahlige ID und einen vom Menschen lesbaren textuellen Namen. Zum Erlernen von DTrace sollen zunächst einige sehr einfache Anforderungen mit dem Prüfpunkt namens BEGIN erzeugt werden, der zu Beginn jeder neuen Tracing-Anforderung einmal ausgelöst wird. Mit der Option des Dienstprogrammsdtrace(1M) lässt -n sich ein Prüfpunkt über seinen Zeichenfolgennamen aktivieren. Geben Sie den folgenden Befehl ein:


# dtrace -n BEGIN

Nach einer kurzen Pause wird Ihnen von DTrace mitgeteilt, dass ein Prüfpunkt aktiviert wurde, und Sie sehen eine Ausgabezeile, die auf die Auslösung des Prüfpunkts BEGIN hinweist. Wenn diese Ausgabe erscheint, wartet dtrace darauf, dass weitere Prüfpunkte ausgelöst werden. Da Sie keine weiteren Prüfpunkte aktiviert haben und BEGIN nur einmal ausgelöst wird, drücken Sie in Ihrer Befehls-Shell die Tastenkombination Strg-C, um dtrace zu beenden und zur Shell-Eingabeaufforderung zurückzukehren:


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

An der Ausgabe erkennen Sie, dass der Prüfpunkt namens BEGIN einmal ausgelöst wurde. Sie enthält sowohl die ganzzahlige ID als auch den Namen des Prüfpunkts. Standardmäßig wird die numerische Bezeichnung der CPU angezeigt, auf der der Prüfpunkt ausgelöst wird. In diesem Beispiel geht aus der Spalte „CPU“ hervor, dass der Befehl dtrace bei Auslösung des Prüfpunkts auf CPU 0 ausgeführt wurde.

Sie können DTrace-Anforderungen mit beliebig vielen Prüfpunkten und Aktionen erstellen. Formulieren Sie nun eine einfache Anforderung mit zwei Prüfpunkten, indem Sie dem vorigen Beispielbefehl den Prüfpunkt END hinzufügen. Der Prüfpunkt END wird einmal zum Abschluss der Ablaufverfolgung ausgelöst. Geben Sie den folgenden Befehl ein und drücken Sie nach der Ausgabezeile für den Prüfpunkt BEGIN erneut Strg-C in Ihrer Befehls-Shell:


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

Sie sehen, dass die Betätigung von Strg-C zum Beenden von dtrace den Prüfpunkt END auslöst. dtrace meldet die Auslösung dieses Prüfpunkts und wird beendet.

Nachdem Sie sich jetzt ein wenig mit der Benennung und Aktivierung von Prüfpunkten angefreundet haben, sind Sie bereit, die DTrace-Version des klassischen Einsteigerprogramms „Hello, World“ zu schreiben. DTrace-Versuche lassen sich nicht nur in der Befehlszeile erstellen, sondern auch in der Programmiersprache D in Textdateien schreiben. Erstellen Sie in einem Texteditor eine neue Datei mit dem Namen hello.d und geben Sie Ihr erstes D-Programm ein:


Beispiel 1–1 hello.d: „Hello, World“ in der Programmiersprache D

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

Nachdem Sie das Programm gespeichert haben, können Sie es mit der dtrace-Option -s ausführen. Geben Sie den folgenden Befehl ein:


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

Wie Sie sehen, generiert dtrace dieselbe Ausgabe wie zuvor, gefolgt von dem Text „hello, world”. Im Gegensatz zum vorherigen Beispiel mussten Sie diesmal weder warten noch Strg-C drücken. Diese Abweichungen sind das Ergebnis der Aktionen, die Sie in hello.d für den Prüfpunkt BEGIN angegeben haben. Um verstehen zu können, was hier vor sich gegangen ist, müssen wir uns die Struktur des D-Programms genauer anschauen.

Jedes D-Programm besteht aus einer Folge von Klauseln, die je einen oder mehrere zu aktivierende Prüfpunkte beschreiben, und einem optionalen Satz Aktionen, die bei Auslösung des Prüfpunkts durchgeführt werden sollen. Die Aktionen werden nach dem Namen des Prüfpunkts als eine Reihe von Anweisungen in geschweiften Klammern { } aufgelistet. Jede Anweisung endet mit einem Strichpunkt (;). In Ihrer ersten Anweisung kommt die Funktion trace() zum Einsatz. Mit ihr weisen Sie DTrace an, das angegebene Argument, die Zeichenkette „hello, world”, bei Auslösung des Prüfpunkts BEGIN aufzuzeichnen und anschließend auszugeben. In der zweiten Anweisung verwenden Sie die Funktion exit(), um die Ablaufverfolgung mit DTrace abzubrechen und den Befehl dtrace zu beenden. DTrace bietet einen Satz hilfreicher Funktionen wie trace() und exit(), die Sie in Ihren D-Programmen aufrufen können. Sie rufen eine Funktion auf, indem Sie ihren Namen, gefolgt von einer in Klammern stehenden Liste von Argumenten angeben. Alle D-Funktionen werden in Kapitel 10Aktionen und Subroutinen beschrieben.

Wenn Sie mit der Programmiersprache C vertraut sind, haben Sie an dem Namen und unseren Beispielen mittlerweile sicherlich festgestellt, dass sie der DTrace-Programmiersprache D sehr ähnlich ist. Tatsächlich ist D eine Ableitung eines sehr weiten Teilsatzes von C, kombiniert mit einem speziellen, die Ablaufverfolgung erleichternden Funktions- und Variablensatz. In den nachfolgenden Kapiteln werden Sie mehr über diese Leistungsmerkmale erfahren. Wenn Sie schon einmal ein C-Programm geschrieben haben, können Sie Ihr Wissen großteilig unmittelbar auf die Erstellung von Ablaufverfolgungsprogrammen in D übertragen. Aber selbst wenn Sie noch nie ein C-Programm geschrieben haben, lässt sich D sehr leicht erlernen. In diesem Kapitel wird Ihnen die gesamte Syntax verständlich gemacht. Lassen Sie uns aber zunächst noch einmal von den Sprachregeln zur Funktionsweise von DTrace und anschließend zum Erstellen etwas interessanterer D-Programme zurückkehren.

Provider und Prüfpunkte

In den vorangehenden Beispielen haben Sie die Verwendung zwei einfacher Prüfpunkte namens BEGIN und END erlernt. Woher kamen diese Prüfpunkte eigentlich? Prüfpunkte in DTrace stammen aus einer Gruppe von Kernelmodulen, den Providern. Jeder Provider nimmt eine bestimmte Art der Instrumentation zur Erstellung von Prüfpunkten vor. Bei der Verwendung von DTrace erhält jeder Provider die Gelegenheit, die Prüfpunkte zu veröffentlichen, die er dem DTrace-Framework zur Verfügung stellt. Sie können dann Ablaufverfolgungsaktionen aktivieren und einem beliebigen der veröffentlichten Prüfpunkte zuordnen. Zum Auflisten aller auf dem System verfügbaren Prüfpunkte geben Sie folgenden Befehl ein:


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

#

Die Anzeige der gesamten Ausgabe kann eine Weile dauern. Für die Anzeige der Gesamtanzahl aller Prüfpunkte geben Sie folgenden Befehl ein:


# dtrace -l | wc -l
        30122

Die Anzahl auf Ihrem System kann hiervon abweichen, da sie von der jeweiligen Betriebsplattform und der installierten Software abhängt. Die enorme Menge an verfügbaren Prüfpunkten gewährt Ihnen einen Einblick in jeden bislang dunklen Winkel des Systems. Selbst diese Ausgabe ist noch keine vollständige Liste aller Prüfpunkte, da einige Provider, wie Sie später erfahren werden, die Möglichkeit bieten, auf Grundlage Ihrer Tracing-Anforderungen neue Prüfpunkte on-the-fly zu erstellen und die tatsächliche Anzahl der DTrace-Prüfpunkte somit schier ins Unendliche treiben.

Schauen Sie sich noch einmal die Ausgabe von dtrace -l im Terminalfenster an. Beachten Sie, dass jeder Prüfpunkt mit den zwei zuvor genannten Namen, der ganzzahligen ID und dem textuellen Namen, aufgeführt ist. Der textuelle Name besteht aus vier Teilen, die in der dtrace-Ausgabe als vier separate Spalten angezeigt werden. Es werden die folgenden Bestandteile eines Prüfpunktnamens unterschieden:

Provider 

Der Name des DTrace-Providers, der diesen Prüfpunkt veröffentlicht. Der Name des Providers stimmt bezeichnenderweise mit dem Namen des DTrace-Kernelmoduls überein, das die Instrumentation zur Aktivierung des Prüfpunkts durchführt. 

Modul 

Bei Prüfpunkten für eine bestimmte Programmposition der Name des Moduls, in dem sich der Prüfpunkt befindet. Dabei handelt es sich entweder um den Namen eines Kernelmoduls oder einer Benutzerbibliothek. 

Funktion 

Bei Prüfpunkten für eine bestimmte Programmposition der Name der Programmfunktion, in der sich der Prüfpunkt befindet. 

Name 

Der letzte Bestandteil des Prüfpunktnamens gibt, wie beispielsweise BEGIN oder END, in gewissem Maße Aufschluss über die semantische Bedeutung des Prüfpunkts.

Geben Sie beim Ausschreiben des vollständigen textuellen Namens eines Prüfpunkts alle vier, durch Doppelpunkt getrennte Bestandteile an:

Provider:Modul: Funktion:Name

Für einige Prüfpunkte, wie beispielsweise BEGIN und END, sind in der Liste weder Modul noch Funktion aufgeführt. Bei einigen Prüfpunkten bleiben diese Felder leer, da sie keiner bestimmten instrumentierten Programmfunktion oder -position entsprechen. Sie beziehen sich vielmehr auf ein abstraktes Konzept wie zum Beispiel das Ende einer Tracing-Anforderung. Prüfpunkte, deren Namen Modul- und Funktionsbestandteile enthalten, werden als verankerte Prüfpunkte bezeichnet, solche ohne diese Bestandteile als nicht verankerte Prüfpunkte.

Wenn Sie nicht alle Felder eines Prüfpunktnamens angeben, wird Ihre Anforderung von DTrace konventionsgemäß auf alle Prüfpunkte mit übereinstimmenden Werten in den von Ihnen angegebenen Namensbestandteilen angewendet. Das heißt also, dass Sie DTrace zuvor mit der Angabe des Prüfpunktnamens BEGIN angewiesen haben, unabhängig von den Werten im Provider-, Modul- und Funktionsfeld alle Prüfpunkte mit dem Namensfeld BEGIN zu suchen. Zufällig gibt es nur einen Prüfpunkt mit dieser Beschreibung, und deshalb fällt das Ergebnis gleich aus. Doch Sie wissen nun, dass dtrace:::BEGIN der richtige Name des Prüfpunkts BEGIN ist und darauf hinweist, dass dieser Prüfpunkt vom DTrace-Framework selbst bereitgestellt wird (DTrace ist sein Provider) und an keiner Funktion verankert ist. Wir könnten das Programm hello.d also auch wie folgt schreiben und dasselbe Ergebnis erhalten:

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

Nachdem Sie jetzt wissen, woher Prüfpunkte kommen und wie sie benannt werden, betrachten wir ein wenig genauer, was geschieht, wenn Sie Prüfpunkte aktivieren und DTrace zu einem Vorgang anweisen. Anschließend kehren wir zu unserem Blitzkurs in D zurück.

Kompilierung und Instrumentation

Beim Schreiben herkömmlicher Programme für Solaris konvertieren Sie den Quellcode des Programms mit einem Compiler in den ausführbaren Objektcode. Indem Sie den Befehl dtrace absetzen, rufen Sie den Compiler für die Programmiersprache D auf, in der Sie zuvor das Programm hello.d geschrieben haben. Das kompilierte Programm wird an den Betriebssystemkernel gesendet, wo es durch DTrace ausgeführt wird. Dort werden die im Programm genannten Prüfpunkte aktiviert und der entsprechende Provider nimmt die erforderliche Instrumentation vor, um sie in Funktion zu setzen.

Die gesamte Instrumentation in DTrace erfolgt vollständig dynamisch: Prüfpunkte werden einzeln und nur, wenn Sie sie verwenden, aktiviert. Für inaktive Prüfpunkte liegt kein instrumentierter Code vor, sodass das System keinerlei Leistungseinbußen erfährt, wenn DTrace nicht verwendet wird. Wenn das Experiment abgeschlossen und der Befehl dtrace beendet wird, werden alle von Ihnen verwendeten Prüfpunkte automatisch deaktiviert und ihre Instrumentation gelöscht. Dadurch kehrt das System zu exakt dem vorherigen Zustand zurück. Zwischen einem System, auf dem DTrace nicht aktiv ist, und einem System, auf dem die DTrace-Software nicht installiert ist, besteht effektiv kein Unterschied.

Die Instrumentation für jeden Prüfpunkt erfolgt dynamisch auf dem laufenden Betriebssystem oder den von Ihnen ausgewählten Benutzerprozessen. Das System wird weder stillgelegt (quiesce) noch auf eine andere Weise unterbrochen. Nur für die von Ihnen aktivierten Prüfpunkte wird Instrumentationscode hinzugefügt. Das heißt, dass die Prüftätigkeit durch die Verwendung von DTrace genau darauf beschränkt ist, was Sie von DTrace verlangen: Keine anderen Daten werden nachverfolgt, kein einzelner, großer „Tracing-Schalter“ wird im System eingeschaltet. Die gesamte Instrumentation in DTrace ist auf maximale Effizienz ausgelegt. Dank dieser Leistungsmerkmale lässt sich DTrace in der Produktionsumgebung zum Lösen echter Probleme in Echtzeit einsetzen.

Das DTrace-Framework bietet außerdem Unterstützung für eine beliebige Anzahl virtueller Clients. Sie können so viele DTrace-Experimente gleichzeitig ausführen, wie Sie für nötig halten. Dabei stellt die Speicherkapazität der Systeme die einzige Einschränkung dar. Alle Befehle arbeiten unabhängig voneinander unter Verwendung derselben zugrunde liegenden Instrumentation. Derselben Fähigkeit ist es auch zu verdanken, dass beliebig viele unterschiedliche Systembenutzer DTrace gleichzeitig einsetzen können: Entwickler, Administratoren und Wartungsfachleute können mit DTrace gemeinsam oder an getrennten Problemen auf demselben System arbeiten, ohne sich gegenseitig zu behindern.

Im Gegensatz zu Programmen in C und C++ und ähnlich wie in der Programmiersprache JavaTM geschriebene Programme werden die D-Programme in DTrace in eine sichere Zwischenform kompiliert, die bei der Auslösung der Prüfpunkte ausgeführt wird. Die Sicherheitsüberprüfung dieser Zwischenform erfolgt, wenn das Programm erstmals von der DTrace-Kernelsoftware untersucht wird. Die DTrace-Ausführungsumgebung behandelt außerdem etwaige während der Ausführung des D-Programms auftretende Laufzeitfehler, einschließlich Divisionen durch Null, Dereferenzierungen ungültiger Speicherbereiche usw., und gibt Meldungen darüber aus. Sie können also gar kein unsicheres Programm erzeugen, das DTrace dazu veranlassen würde, den Solaris-Kernel oder einen der auf dem System ausgeführten Prozesse zu beschädigen. Aufgrund dieser Sicherheitsmerkmale können Sie DTrace in Produktionsumgebungen einsetzen, ohne sich über Abstürze oder Beschädigungen des Systems Gedanken machen zu müssen. Im Fall eines Programmierfehlers meldet DTrace den Fehler und deaktiviert die Instrumentation, sodass Sie den Fehler korrigieren und einen neuen Versuch starten können. Die DTrace-Leistungsmerkmale zum Melden von Fehlern und Debuggen werden später in diesem Buch beschrieben.

Das folgende Schaubild zeigt die einzelnen Komponenten der DTrace-Architektur, einschließlich der Provider, Prüfpunkte, DTrace-Kernelsoftware und des Befehls dtrace.

Abbildung 1–1 DTrace-Architektur und -Komponenten im Überblick

DTrace-Architektur: Kernel-Dienstprogramm und Provider, eine Treiberschnittstelle zwischen Kernel und einer Bibliothek sowie die den Befehlssatz unterstützende Bibliothek.

Nun, nachdem die Funktionsweise von DTrace geklärt ist, kehren wir zur Einführung in die Programmiersprache D zurück und beginnen, einige interessantere Programme zu schreiben.

Variablen und arithmetische Ausdrücke

In unserem nächsten Beispielprogramm wird mit dem DTrace-Provider profile ein einfacher, auf der verstreichenden Zeit basierender Zähler implementiert. Dieser Provider hat die Fähigkeit, neue Prüfpunkte auf der Grundlage der im D-Programm enthaltenen Beschreibungen zu erstellen. Wenn Sie einen Prüfpunkt namens profile:::tick-nsec für die Ganzzahl n erstellen, generiert der Provider einen Prüfpunkt, der alle n Sekunden ausgelöst wird. Schreiben Sie den folgenden Quellcode und speichern Sie ihn in einer Datei namens 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);
}

Wenn das Programm ausgeführt wird, werden die ablaufenden Sekunden gezählt, bis Sie Strg-C drücken, und anschließend wird die Gesamtzahl angezeigt:


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

Die ersten drei Programmzeilen sind ein Kommentar, aus dem die Funktion des Programms hervorgeht. Ähnlich wie in C, C++ und der Programmiersprache Java ignoriert der D-Compiler alle Zeichen zwischen den Symbolen /* und */. Kommentare sind an jeder Stelle in einem D-Programm zulässig, sowohl innerhalb als auch außerhalb der Prüfpunkt-Klausel.

Die Klausel für den Prüfpunkt BEGIN definiert eine neue Variable namens i und weist ihr mit folgender Anweisung den ganzzahligen Wert Null zu:

i = 0;

Im Gegensatz zu C, C++ und Java lassen sich D-Variablen einfach durch ihre Verwendung in einer Programmanweisung erzeugen. Es sind keine expliziten Variablendeklarationen erforderlich. Bei der ersten Verwendung einer Variablen in einem Programm wird der Variablentyp auf Grundlage der Art ihrer ersten Zuweisung festgelegt. Eine Variable kann im Verlauf der Programmlebensdauer nur einen Typ aufweisen. Nachfolgende Referenzen müssen also dem Typ der ersten Zuweisung entsprechen. In counter.d wird der Variable i zuerst die ganzzahlige Konstante Null zugewiesen. Sie wird deshalb auf den Datentyp int festgelegt. D bietet dieselben einfachen Integer-Datentypen wie C:

char

Zeichen oder Ganzzahl von 1 Byte 

int

Standard-Integer 

short

Kurze Ganzzahl 

long

Lange Ganzzahl 

long long

Sehr lange Ganzzahl 

Die Größen dieser Typen hängen von dem in Kapitel 2Typen, Operatoren und Ausdrücke beschriebenen Datenmodell des Betriebssystemkerns ab. Darüber hinaus bietet D integrierte einfache Namen für Integer-Typen mit und ohne Vorzeichen von unterschiedlicher, festgelegter Größe sowie Tausende anderer Typen, die vom Betriebssystem definiert werden.

Der zentrale Teil von counter.d ist die Prüfpunktklausel für die Erhöhung des Zählers i:

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

Mit dieser Klausel wird der Prüfpunkt profile:::tick-1sec benannt. Dies weist den Provider profile an, einen neuen Prüfpunkt zu erzeugen, der einmal pro Sekunde auf einem verfügbaren Prozessor ausgelöst wird. Die Klausel enthält zwei Anweisungen. Die erste ordnet i den vorigen Wert plus 1 zu und die zweite überwacht den neuen Wert von i. In D sind alle aus C bekannten arithmetischen Operatoren verfügbar. Eine vollständige Liste finden Sie in Kapitel 2Typen, Operatoren und Ausdrücke. Ebenso wie in C kann der Operator ++ als Kurzform für die Erhöhung der entsprechenden Variable um 1 verwendet werden. Die Funktion trace() übernimmt jeden D-Ausdruck als Argument. counter.d ließe sich also auch knapper fassen:

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

Wenn Sie den Typ der Variable i ausdrücklich festlegen möchten, können Sie den gewünschten Typ bei der Zuweisung einklammern, um die Ganzzahl Null ausdrücklich in einen bestimmten Typ umzuwandeln (Casting). Wenn Sie in D beispielsweise die maximale Größe einer char-Variable ermitteln möchten, könnten Sie die BEGIN-Klausel wie folgt ändern:

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

Wenn Sie counter.d eine Weile laufen lassen, müssten Sie sehen, wie der überwachte Wert anwächst und schließlich wieder bei Null beginnt. Sollte Ihnen das zu lange dauern, ändern Sie den Namen des Prüfpunkts profile in profile:::tick-100msec ab. Dadurch erhalten Sie einen Zähler, der alle 100 Millisekunden (10-mal pro Sekunde) erhöht wird.

Prädikate

Einer der Hauptunterschiede zwischen D und anderen Programmiersprachen wie C, C++ und Java besteht im Verzicht auf Kontrollstrukturen wie beispielsweise if-Anweisungen und Schleifen. Klauseln von D-Programmen werden als einfache, geradlinige Anweisungslisten geschrieben, die eine optionale, festgelegte Menge an Daten verfolgen. D bietet die Möglichkeit der bedingten Datenverfolgung und der Einflussnahme auf den Kontrollfluss anhand von logischen Ausdrücken namens Prädikaten, die Programmklauseln vorangesetzt werden können. Ein Prädikatausdruck wird bei der Prüfpunktauslösung ausgewertet, bevor die Anweisungen in der entsprechenden Klausel ausgeführt werden. Wenn die Prädikatauswertung „wahr“ ergibt, was durch einen beliebigen Wert ungleich Null dargestellt wird, erfolgt die Ausführung der Anweisungsliste. Ergibt die Prädikatauswertung bei einem Wert von Null „falsch“, wird keine der Anweisungen ausgeführt und die Auslösung des Prüfpunkts ignoriert.

Schreiben Sie für das nächste Beispiel den folgenden Quellcode und speichern Sie ihn in einer Datei namens countdown.d:

dtrace:::BEGIN
{
	i = 10;
}

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

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

Dieses D-Programm implementiert unter Verwendung von Prädikaten einen Timer für einen 10-sekündigen Countdown. Wenn countdown.d ausgeführt wird, zählt das Programm von 10 abwärts, gibt anschließend eine Meldung aus und wird beendet:

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

In diesem Beispiel wird mit dem Prüfpunkt BEGIN zum Starten des Countdowns ein Integer i auf 10 initialisiert. Wie im vorigen Beispiel wird anschließend mit dem Prüfpunkt tick-1sec ein einmal pro Sekunde ausgelöster Timer implementiert. Beachten Sie, dass die Beschreibung des Prüfpunkts tick-1sec in countdown.d in zwei verschiedenen Klauseln und mit zwei verschiedenen Prädikaten und Aktionslisten verwendet wird. Das Prädikat ist ein zwischen Schrägstrichen / / stehender logischer Ausdruck, der dem Prüfpunktnamen folgt und der geschweiften Klammer { } um die Klausel-Anweisungsliste vorangeht.

Das erste Prädikat testet, ob i größer als Null ist, was darauf hinweist, dass der Timer noch läuft:

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

Der relationale Operator > bedeutet größer als und gibt als „falsch“ den ganzzahligen Wert Null und als „wahr“ den Wert 1 zurück. In D werden alle aus C bekannten relationalen Operatoren unterstützt. Eine vollständige Liste finden Sie in Kapitel 2Typen, Operatoren und Ausdrücke. Wenn i noch nicht Null ist, wird i durch das Skript überwacht und dann mithilfe des Operators -- verringert.

Das zweite Prädikat verwendet den Operator ==, um „wahr“ zurückzugeben, wenn i genau gleich Null ist. Dies weist darauf hin, dass der Countdown abgeschlossen ist:

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

Ähnlich wie in unserem ersten Beispiel hello.d kommt in countdown.d eine Folge von Zeichen in Anführungsstrichen zum Einsatz. Diese Stringkonstante stellt die zum Abschluss des Countdowns auszugebende Meldung dar. Anschließend wird die Funktion exit() verwendet, um dtrace zu beenden und zur Shell-Eingabeaufforderung zurückzukehren.

Wenn Sie nun auf die Struktur von countdown.d zurückblicken, erkennen Sie, wie durch die Erstellung von zwei Klauseln mit derselben Prüfpunktbeschreibung, aber unterschiedlichen Prädikaten und Aktionen effektiv ein logischer Fluss erzeugt wurde:

i = 10
einmal pro Sekunde,
wenn i größer als Null,
dann trace(i--);
anderenfalls, wenn i gleich Null,
dann trace("blastoff!");
; exit(0);

Wenn Sie komplexe Programme mit Prädikaten schreiben möchten, bietet es sich an, den Algorithmus zunächst auf diese Weise darzustellen. Setzen Sie dann jeden Bestandteil des bedingten Konstrukts in eine separate Klausel mit Prädikat um.

Wir werden jetzt Prädikate mit einem neuen Provider, syscall, kombinieren und ein erstes wirkliches Ablaufverfolgungsprogramm in D schreiben. Mit dem Provider syscall lassen sich Prüfpunkte zu Beginn oder nach Abschluss eines jeden Solaris-Systemaufrufs aktivieren. Im nächsten Beispiel wird mit DTrace jeder read(2) bzw.·write(2)-Systemaufruf Ihrer Shell beobachtet. Öffnen Sie zunächst zwei Terminalfenster - eines für DTrace und das andere für den Shell-Prozess, den Sie überwachen möchten. Geben Sie in das zweite Fenster den folgenden Befehl zur Ermittlung der Prozess-ID dieser Shell ein:


# echo $$
12345

Schreiben Sie nun das nachfolgende D-Programm in das erste Terminalfenster und speichern Sie es unter dem Namen rw.d. Ersetzen Sie bei der Eingabe des Programms 12345 durch die Prozess-ID der Shell, die Sie als Antwort auf den Befehl echo erhalten haben.

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

}

Beachten Sie, dass der Rumpf der Prüfpunktklausel in rw.d leer bleibt, da das Programm nur zur Verfolgung von Benachrichtigungen über Prüfpunktauslösungen und nicht zum Aufzeichnen zusätzlicher Informationen vorgesehen ist. Wenn Sie rw.d fertig eingegeben haben, starten Sie das Experiment mit dtrace und wechseln zum zweiten Shell-Fenster. Geben Sie dort einige Befehle ein und drücken Sie nach jedem Befehl die Eingabetaste. Während Sie die Befehle eingeben, meldet dtrace im ersten Fenster die Prüfpunktauslösungen, etwa wie in diesem Beispiel:


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

Sie beobachten gerade, wie die Shell mit read(2) - und write(2) -Systemaufrufen Zeichen aus dem Terminalfenster liest und das Ergebnis zurückgibt! In diesem Beispiel sind viele der bisher betrachteten Konzepte enthalten - und auch einige neue. Zunächst wird in dem Skript eine einzige Prüfpunktklausel mit mehreren Prüfpunktbeschreibungen verwendet, um read(2) und write(2) auf gleiche Weise zu instrumentieren. Dabei werden die Beschreibungen wie folgt durch Kommas getrennt:

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

Zur besseren Lesbarkeit erhält jede Prüfpunktbeschreibung eine eigene Zeile. Diese Anordnung ist nicht obligatorisch, sorgt aber für übersichtlichere Skripten. Als Nächstes definiert das Skript ein Prädikat, das nur die von Ihrem Shell-Prozess ausgeführten Systemaufrufe herausfiltert:

/pid == 12345/

In diesem Prädikat kommt die vordefinierte DTrace-Variable pid zum Einsatz, deren Auswertung stets die Prozess-ID des Threads ergibt, der den entsprechenden Prüfpunkt ausgelöst hat. DTrace stellt zahlreiche integrierte Variablendefinitionen für nützliche Informationen wie die Prozess-ID bereit. Sehen Sie hier eine Liste einiger DTrace-Variablen, die Sie in Ihren ersten D-Programmen verwenden können:

Variablenname 

Datentyp 

Bedeutung 

errno

int

Aktueller errno-Wert für Systemaufrufe

execname

string

Name der ausführbaren Datei des aktuellen Prozesses 

pid

pid_t

Prozess-ID des aktuellen Prozesses 

tid

id_t

Thread-ID des aktuellen Threads 

probeprov

string

Providerfeld der aktuellen Prüfpunktbeschreibung 

probemod

string

Modulfeld der aktuellen Prüfpunktbeschreibung 

probefunc

string

Funktionsfeld der aktuellen Prüfpunktbeschreibung 

probename

string

Namensfeld der aktuellen Prüfpunktbeschreibung 

Ändern Sie nun in Ihrem Instrumentationsprogramm versuchsweise die Prozess-ID und die instrumentierten Systemaufruf-Prüfpunkte ab, um das Programm auf andere auf dem System laufende Prozesse anzuwenden. Anschließend können Sie rw.d durch eine weitere kleine Änderung in eine sehr einfache Version eines Tracing-Tools für Systemaufrufe wietruss(1). umwandeln. Ein leeres Feld in einer Prüfpunktbeschreibung fungiert als Platzhalter, mit dem alle Prüfpunkte als Übereinstimmung interpretiert werden. Ändern Sie das Programm also auf den folgenden neuen Quellcode ab, sodass sämtliche von der Shell ausgeführten Systemaufrufe verfolgt werden:

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

}

Geben Sie einige Befehle wie beispielsweise cd, ls oder date in die Shell ein und beobachten Sie die Ausgabe des DTrace-Programms.

Formatierung der Ausgabe

Die Ablaufverfolgung von Systemaufrufen ist eine sehr wirkungsvolle Methode zur Beobachtung des Verhaltens der meisten Benutzerprozesse. Wenn Sie das Solaris-Dienstprogramm truss(1) als Administrator oder Entwickler bereits verwendet haben, wissen Sie wahrscheinlich, dass es im Problemfall immer einmal nützlich sein kann. Wenn Sie truss noch nie eingesetzt haben, sollten Sie es jetzt versuchen, indem Sie diesen Befehl in eine Ihrer Shells eingeben:


$ truss date

Das Ergebnis ist ein formatiertes Ablaufprotokoll aller von date(1) ausgeführten Systemaufrufe, gefolgt von einer einzeiligen Befehlsausgabe. Das folgende Beispiel ist eine im Hinblick auf die Formatierung der Ausgabe verbesserte Version des vorherigen Programms rw.d. Die an truss(1) angelehnte Ausgabe ist nun leichter verständlich. Geben Sie das folgende Programm ein und speichern Sie es unter dem Namen trussrw.d :


Beispiel 1–2 trussrw.d: Ablaufverfolgung von Systemaufrufen mit dem Ausgabeformat von 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);
}

Hier wurde die Konstante 12345 in jedem Prädikat durch den Bezeichner $1 ersetzt. Über diesen Bezeichner kann der gewünschte Prozess dem Skript als Argument übergeben werden: $1 wird bei der Kompilierung des Skripts durch den Wert des ersten Arguments ersetzt. Zum Ausführen von trussrw.d verwenden Sie die dtrace-Optionen -q und -s, gefolgt von der Prozess-ID der Shell als abschließendes Argument. Die Option -q gibt an, dass dtrace mit minimaler Ausgabe ausgeführt werden und sowohl die Kopfzeile als auch die CPU- und ID-Spalten aus den vorangehenden Beispielen unterdrücken soll. Sie sehen also nur die Ausgabe für die Daten, die ausdrücklich verfolgt werden sollten. Geben Sie den folgenden Befehl ein (wobei Sie 12345 durch die Prozess-ID eines Shell-Prozesses ersetzen) und drücken Sie in der angegebenen Shell wiederholt die Eingabetaste:


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

Nun werden wir uns das D-Programm und seine Ausgabe genauer betrachten. Zuerst wird mit einer Klausel wie im vorigen Programm jeder Aufruf von read(2) und write(2) der Shell instrumentiert. In diesem Beispiel verwenden wir jedoch die neue Funktion printf(), um Daten zu verfolgen und diese in einem bestimmten Format auszugeben:

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

Die Funktion printf() bietet zwei Fähigkeiten in Einem: Ähnlich wie die bereits verwendete Funktion trace() verfolgt sie Daten und zusätzlich kann sie die Daten und anderen Text in einem spezifischen, von Ihnen beschriebenen Format ausgeben. Die Funktion printf() weist DTrace an, die jedem Argument nach dem ersten Argument zugehörigen Daten zu verfolgen und die Ergebnisse gemäß den mit dem ersten printf()-Argument, der Formatzeichenkette, beschriebenen Regeln auszugeben.

Die Formatzeichenkette ist eine normale Zeichenkette (string). Sie kann beliebig viele mit dem Zeichen % angeführte Formatumwandlungen enthalten, die beschreiben, wie das entsprechende Argument formatiert werden soll. Die erste Konvertierung in der Formatzeichenkette bezieht sich auf das zweite Argument von printf(), die zweite Konvertierung auf das dritte Argument und so weiter. Text zwischen den Umwandlungen wird wörtlich wiedergegeben. Das auf das %-Umwandlungszeichen folgende Zeichen beschreibt das für das entsprechende Argument zu verwendende Format. Die drei Formatumwandlungen in trussrw.d haben folgende Bedeutung:

%d

Der entsprechende Wert wird als Dezimalzahl ausgegeben. 

%s

Der entsprechende Wert wird als Zeichenkette ausgegeben. 

%x

Der entsprechende Wert wird als Hexadezimalzahl ausgegeben. 

DTrace printf() wirkt wie die C-Bibliotheksroutine printf(3C) bzw. das Shell-Dienstprogramm printf(1). Falls Ihnen·printf() neu ist, klärt Sie Kapitel 12Formatierung der Ausgabe im Detail über die Formate und Optionen auf. Lesen Sie dieses Kapitel aber auch dann aufmerksam durch, wenn Sie printf() bereits aus einer anderen Sprache kennen. printf() ist in D integriert und bietet Ihnen einige neue, speziell für DTrace entwickelte Formatumwandlungen.

Zur Unterstützung beim Schreiben fehlerfreier Programme prüft der D-Compiler jede printf()-Formatzeichenkette auf ihre Argumentliste. Ändern Sie probefunc in der obigen Klausel in die Ganzzahl 123 ab. Wenn Sie das abgeänderte Programm ausführen, erhalten Sie eine Fehlermeldung, die besagt, dass die String-Formatumwandlung %s nicht für ein Integer-Argument geeignet ist:


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

Für die Ausgabe der read- oder write-Systemaufrufe und ihrer Argumente verwenden Sie die Anweisung printf():

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

Damit werden der Name der aktuellen Prüfpunktfunktion und die ersten drei Integer-Argumente des Systemaufrufs, die in den DTrace-Variablen arg0, arg1 und arg2 zur Verfügung stehen, verfolgt. Weitere Informationen zu Prüfpunktargumenten finden Sie in Kapitel 3Variablen. Das erste Argument von read(2) und write(2) ist ein als Dezimalzahl dargestellter Dateideskriptor. Das zweite Argument ist eine als Hexadezimalwert formatierte Pufferadresse. Bei dem letzen Argument handelt es sich schließlich um die in Form eines Dezimalwerts wiedergegebene Puffergröße. Die Formatangabe %4d für das dritte Argument bedeutet, dass der Wert mit der Formatumwandlung %d und einer minimalen Feldbreite von 4 Zeichen dargestellt werden soll. Ist die Ganzzahl weniger als 4 Zeichen lang, fügt printf() zur Ausrichtung der Ausgabe zusätzliche Leerzeichen ein.

Um das Ergebnis des Systemaufrufs wiederzugeben und jede Ausgabezeile abzuschließen, verwenden Sie die folgende Klausel:

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

Beachten Sie, dass der Provider syscall außer entry für jeden Systemaufruf auch einen Prüfpunkt namens return veröffentlicht. Durch die DTrace-Variable arg1 für die syscall-Prüfpunkte return wird der Rückgabewert des Systemaufrufs eingesetzt. Der Rückgabewert wird als Dezimalzahl formatiert. Die in der Formatzeichenkette mit umgekehrten Schrägstrichen beginnenden Zeichenfolgen erstrecken sich bis zum Tabulator (\t) bzw. zur neuen Zeile (\n). Diese „Escape-Folgen“ erleichtern die Wiedergabe oder Aufzeichnung schwer darzustellender Zeichen. D unterstützt denselben Ersatzdarstellungssatz wie C, C++ und Java. Eine vollständige Liste der Escape-Sequenzen finden Sie in Kapitel 2Typen, Operatoren und Ausdrücke.

Vektoren

D bietet Ihnen die Möglichkeit, Variablen des Typs Integer und anderer Typen zur Darstellung von Zeichenketten sowie gemischte Typen zu definieren, die als structs (Strukturen) und unions (Unionen) bezeichnet werden. Wenn Sie sich mit der C-Programmierung auskennen, wird es Sie freuen, dass Sie in D dieselben Typen wie in C verwenden können. Wenn Sie kein C-Experte sind, gibt es trotzdem keinen Grund zur Besorgnis: Sämtliche verschiedenen Datentypen werden in Kapitel 2Typen, Operatoren und Ausdrücke erklärt. Außerdem unterstützt D eine besondere Variablenart, den so genannten assoziativen Vektor. Ein assoziativer Vektor gleicht einem einfachen Vektor (Array) insofern, als er Schlüsseln Werte zuordnet. In einem assoziativen Vektor sind die Schlüssel jedoch nicht auf Ganzzahlen eines festgelegten Wertebereichs beschränkt.

Assoziative Vektoren in D lassen sich durch eine Liste von einem oder mehreren Werten beliebigen Typs indizieren. Die Schlüsselwerte bilden zusammen ein Tupel, das zur Indizierung im Vektor und zum Zugriff auf den oder Ändern des Werts des entsprechenden Schlüssels eingesetzt wird. Jedes für einen bestimmten assoziativen Vektor verwendetes Tupel muss derselben Typensignatur entsprechen. Das heißt, dass jedes Tupel dieselbe Länge und dieselben Schlüsseltypen in derselben Reihenfolge aufweisen muss. Außerdem sind die den Elementen eines gegebenen assoziativen Vektors zugeordneten Werte für den gesamten Vektor auf einen einzigen Typ festgelegt. Die folgende D-Anweisung definiert beispielsweise einen neuen assoziativen Vektor a des Wertetyps int mit der Tupelsignatur [ string, int ] und speichert den ganzzahligen Wert 456 im Vektor:

a["hello", 123] = 456;

Auf die Elemente eines definierten Vektors kann wie auf jede andere D-Variable zugegriffen werden. So ändern wir beispielsweise mit der folgenden D-Anweisung das zuvor in a gespeicherte Vektorelement, indem wir den Wert von 456 auf 457 erhöhen:

a["hello", 123]++;

Die Werte aller noch nicht zugewiesenen Vektorelemente werden auf Null gesetzt. Setzen wir nun einen assoziativen Vektor in ein D-Programm ein. Geben Sie das folgende Programm ein und speichern Sie es unter dem Namen rwtime.d:


Beispiel 1–3 rwtime.d: Zeit für·read(2)- und write(2)-Aufrufe

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

Geben Sie bei der Ausführung von rwtime.d wie schon bei trussrw.d die ID des Shell-Prozesses an. Wenn Sie einige Shell-Befehle eingeben, sehen Sie die Dauer jedes Systemaufrufs. Geben Sie folgenden Befehl ein und drücken Sie anschließend in der anderen Shell mehrmals die Eingabetaste:


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

Um ein Protokoll der für jeden Systemaufruf abgelaufenen Zeit zu erhalten, müssen Sie sowohl den Eintritt in als auch die Rückkehr aus read(2) und write(2) instrumentieren und die Zeit an jedem Punkt prüfen. Anschließend ist bei der Rückkehr aus einem gegebenen Systemaufruf die Differenz zwischen der ersten und der zweiten Zeitmarke zu berechnen. Hierbei könnten Sie für jeden Systemaufruf eine eigene Variable verwenden, doch das würde die Erweiterung des Programms um zusätzliche Systemaufrufe sehr beschwerlich machen. Einfacher ist es, stattdessen auf einen durch den Namen der Prüfpunktfunktion indizierten assoziativen Vektor zurückzugreifen. Sehen Sie hier die erste Prüfpunktklausel:

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

Diese Klausel definiert einen Vektor namens ts und weist dem entsprechenden Element den Wert der DTrace-Variable timestampzu. Diese Variable gibt den Wert eines stets anwachsenden Nanosekundenzählers zurück, ähnlich der Solaris-Bibliotheksroutinegethrtime(3C). Wenn die Zeitmarke des Eintritts gespeichert ist, wird timestamp erneut durch den Prüfpunkt für die Rückkehr geprüft, der dann die Differenz zwischen der aktuellen Uhrzeit und dem gespeicherten Wert meldet:

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

Das Prädikat des return-Prüfpunkts setzt voraus, dass DTrace den betreffenden Prozess verfolgt und der entsprechende entry-Prüfpunkt bereits ausgelöst wurde und ts[probefunc] einen Wert ungleich Null zugewiesen hat. Durch diesen Trick lässt sich eine ungültige Ausgabe beim Start von DTrace vermeiden. Wartet die Shell, wenn Sie dtrace ausführen, bereits in einem read(2)-Systemaufruf auf eine Eingabe, wird der Prüfpunkt read:return ohne einen vorangehenden read:entry für diesen ersten read(2)-Aufruf ausgelöst, und die Prüfung von ts[probefunc] ergibt Null, da noch keine Zuweisung stattgefunden hat.

Externe Symbole und Typen

Die DTrace-Instrumentation wird innerhalb des Solaris-Betriebssystemkerns ausgeführt. Das heißt, dass neben den speziellen DTrace-Variablen und Prüfpunktargumenten auch auf Kerneldatenstrukturen, Symbole und Typen zugegriffen werden kann. Dank dieser Fähigkeiten können fortgeschrittene DTrace-Benutzer, Administratoren, Wartungstechniker und Treiberentwickler das Verhalten des Betriebssystemkernels und der Gerätetreiber auf den unteren Ebenen beobachten. In den Literaturangaben zu Beginn dieses Dokuments finden Sie Titel zu den Interna des Betriebssystems Solaris.

In D kommt das Backquote-Zeichen (oder Accent grave) (`) als spezieller Bereichsoperator zum Ansprechen von Symbolen zum Einsatz, die zwar im Betriebssystem, nicht aber im D-Programm definiert sind. So enthält der Solaris-Kernel beispielsweise eine C-Deklaration eines über das System abstimmbaren Parameters (Tunables) namens kmem_flags zum Aktivieren von Debugging-Leistungsmerkmalen für die Speicherzuweisung. Im Solaris Tunable Parameters Reference Manual finden Sie weitere Informationen zu kmem_flags. Dieses Tunable ist im Kernel-Quellcode wie folgt in C deklariert:

int kmem_flags;

Mit folgender D-Anweisung lässt sich der Wert dieser Variable in einem D-Programm verfolgen:

trace(`kmem_flags);

DTrace weist jedem Kernelsymbol den entsprechenden Typ aus dem C-Betriebssystemcode zu und bietet dadurch einen einfachen quellcodebasierten Zugriff auf die nativen Datenstrukturen des Betriebssystems. Die Namen von Kernelsymbolen werden in einem von den D-Variablen- und Funktionsbezeichnern getrennten Namensraum gehalten, sodass keine Gefahr eines Konflikts zwischen diesen Namen und Ihren D-Variablen besteht.

Sie haben nun Ihre Blitztour durch DTrace beendet und viele der grundlegenden DTrace-Bausteine kennen gelernt, die zum Schreiben größerer, komplexerer D-Programme benötigt werden. In den nachfolgenden Kapiteln wird der vollständige Regelsatz für D beschrieben und erläutert, wie DTrace komplexe Performancemessungen und Funktionsanalysen des Systems zu einer leichten Angelegenheit macht. Später erfahren Sie, wie Sie mit DTrace das Verhalten von Benutzeranwendungen mit dem Systemverhalten verbinden können und sich so der gesamte Software-Stack analysieren lässt.

Sie haben gerade erst begonnen!