Handbuch zur dynamischen Ablaufverfolgung in Solaris

Kapitel 7 Strukturen und Unionen

Ansammlungen zusammenhängender Variablen können in Datenobjekten gruppiert werden, die als Strukturen (structs) und Unionen (unions) bezeichnet werden. In D lassen sich diese Objekte definieren, indem Sie neue Typen für sie vereinbaren. Sie können Ihre neuen Typen für beliebige D-Variablen, einschließlich der Werte assoziativer Vektoren, verwenden. In diesem Kapitel betrachten wir die Syntax und Semantik für die Erzeugung und Manipulation dieser zusammengesetzten Typen sowie die D-Operatoren, die mit ihnen interagieren. Die Syntax für Strukturen und Unionen wird anhand verschiedener Beispielprogramme dargestellt, die den Nutzen der DTrace-Provider fbt und pid demonstrieren.

Strukturen

Das D-Schlüsselwort struct, Abkürzung für structure (Struktur), dient zur Einführung eines neuen, aus einer Gruppe anderer Typen zusammengesetzten Typs. Der neue Struct-Typ kann als Typ für D-Variablen und Vektoren verwendet werden, sodass Sie Gruppen zusammenhängender Variablen unter einem einzigen Namen gruppieren können. D-Strukturen sind mit den entsprechenden Konstrukten in C und C++ identisch. An Java gewöhnte Programmierer sollten sich eine D-Struktur wie eine Klasse (class) mit Datenmitgliedern, aber ohne Methoden vorstellen.

Nehmen wir an, es soll ein anspruchsvolleres D-Programm für die Ablaufverfolgung von Systemaufrufen geschrieben werden, das verschiedene Informationen über jeden von der Shell ausgeführten read(2) und write(2) Systemaufruf aufzeichnet. Bei diesen Informationen kann es sich um die verstrichene Zeit, die Anzahl der Aufrufe oder die größte als Argument übergebene Byteanzahl handeln. Sie könnten, wie in folgendem Beispiel, eine D-Klausel zum Aufzeichnen dieser Eigenschaften in drei separaten assoziativen Vektoren schreiben:

syscall::read:entry, syscall::write:entry
/pid == 12345/
{
	ts[probefunc] = timestamp;
	calls[probefunc]++;
	maxbytes[probefunc] = arg2 > maxbytes[probefunc] ?
	    arg2 : maxbytes[probefunc];
}

Diese Klausel ist jedoch nicht effizient, da DTrace drei separate assoziative Vektoren erzeugen und für jeden eine separate Kopie der identischen Tupelwerte von probefunc speichern muss. Wenn Sie stattdessen eine Struktur verwenden, können Sie nicht nur Platz sparen, sondern erhalten auch ein Programm, das sich leichter lesen und pflegen lässt. Deklarieren Sie zu Beginn der Programmquelldatei zuerst einen neuen Struct-Typ:

struct callinfo {
	uint64_t ts;      /* timestamp of last syscall entry */
	uint64_t elapsed; /* total elapsed time in nanoseconds */
	uint64_t calls;   /* number of calls made */
	size_t maxbytes;  /* maximum byte count argument */
};

Auf das Schlüsselwort struct folgt ein optionaler Bezeichner zum Verweis auf unseren neuen Typ, der nun als struct callinfo bekannt ist. Anschließend sind die Komponenten der Struktur in geschweiften Klammern { } aufgeführt und die gesamte Deklaration endet mit einem Strichpunkt (; ). Die Definition der einzelnen Strukturkomponenten (members) erfolgt in der Syntax für D-Variablendeklarationen. Dabei steht zuerst der Typ der Komponente, gefolgt von einem Bezeichner, der die Komponente benennt, und einem weiteren Strichpunkt (;).

Die Strukturdeklaration selbst definiert einfach den neuen Typ; sie erzeugt weder eine Variable noch reserviert sie Speicherplatz in DTrace. Nach einmaliger Deklaration können Sie struct callinfo im gesamten Rest des D-Programms als Typ verwenden, und jede Variable des Typs struct callinfo speichert eine Kopie der vier mit unserer Strukturvorlage beschriebenen Variablen. Die Komponenten werden im Speicher gemäß der Komponentenliste und mit dem ggf. für die Datenobjektausrichtung benötigten Zwischenraum zwischen den einzelnen Komponenten angeordnet.

Für den Zugriff auf die einzelnen Komponentenwerte mit dem Operator „.” können Sie die Komponentennamen verwenden. Schreiben Sie hierzu einen Ausdruck in der Form:

Variablenname.Komponentenname

Das folgende Beispiel zeigt ein verbessertes Programm mit dem neuen Strukturtyp. Geben Sie dieses D-Programm in einen Texteditor ein und speichern Sie es unter dem Namen rwinfo.d:


Beispiel 7–1 rwinfo.d: : Erfassen statistischer Daten über read(2) und write(2)

struct callinfo {
	uint64_t ts;      /* timestamp of last syscall entry */
	uint64_t elapsed; /* total elapsed time in nanoseconds */
	uint64_t calls;   /* number of calls made */
	size_t maxbytes;  /* maximum byte count argument */
};

struct callinfo i[string];	/* declare i as an associative array */

syscall::read:entry, syscall::write:entry
/pid == $1/
{
	i[probefunc].ts = timestamp;
	i[probefunc].calls++;
	i[probefunc].maxbytes = arg2 > i[probefunc].maxbytes ?
		arg2 : i[probefunc].maxbytes;
}

syscall::read:return, syscall::write:return
/i[probefunc].ts != 0 && pid == $1/
{
	i[probefunc].elapsed += timestamp - i[probefunc].ts;
}

END
{
	printf("        calls  max bytes  elapsed nsecs\n");
	printf("------  -----  ---------  -------------\n");
	printf("  read  %5d  %9d  %d\n",
	    i["read"].calls, i["read"].maxbytes, i["read"].elapsed);
	printf(" write  %5d  %9d  %d\n",
	    i["write"].calls, i["write"].maxbytes, i["write"].elapsed);
}

Wenn Sie das Programm eingegeben haben, führen Sie dtrace -q -s rwinfo.d aus und geben dabei einen Ihrer Shell-Prozesse an. Geben Sie dann den ein oder anderen Befehl in die Shell ein und drücken Sie abschließend im dtrace-Terminal die Tastenkombination Strg-C, um den Prüfpunkt END auszulösen und die Ergebnisse anzeigen zu lassen:


# dtrace -q -s rwinfo.d `pgrep -n ksh`
^C
        calls  max bytes  elapsed nsecs
------  -----  ---------  -------------
  read     36       1024  3588283144
 write     35         59  14945541
#

Zeiger auf Strukturen

Verweise auf Strukturen über Zeiger sind in C und D sehr üblich. Für den Zugriff auf Strukturkomponenten über einen Zeiger steht der Operator -> zur Verfügung. Besitzt struct s eine Komponente m und es liegt ein Zeiger namens sp auf diese Struktur vor (d. h. sp ist eine Variable des Typs struct s *), können Sie entweder den *-Operator einsetzen, um den Zeiger sp für den Zugriff auf die Komponente zunächst zu dereferenzieren:

struct s *sp;

(*sp).m

oder Sie können diese Schreibweise anhand des Operators -> kürzer fassen. Die beiden folgenden D-Fragmente sind gleichbedeutend, sofern sp ein Zeiger auf eine Struktur ist:

(*sp).m				sp->m

DTrace umfasst mehrere integrierte Variablen, die Zeiger auf Strukturen darstellen. Beispiele sind curpsinfo und curlwpsinfo. Diese Zeiger verweisen auf die Strukturen psinfo bzw. lwpsinfo und ihr Inhalt bietet eine Momentaufnahme der Informationen über den Zustand des aktuellen Prozesses und leichtgewichtigen Prozesses (LWP) des Threads, der den aktuellen Prüfpunkt ausgelöst hat. Ein Solaris-LWP ist die Kerneldarstellung eines Benutzer-Threads, auf dem die Solaris- und POSIX-Thread-Schnittstellen aufbauen. Der Einfachheit halber exportiert DTrace diese Informationen in derselben Form wie die /proc-Dateisystemdateien /proc/PID/psinfo und /proc/PID/lwps/LWPID/lwpsinfo. Die·/proc -Strukturen werden von Überwachungs- und Debugging-Tools wie ps(1), pgrep(1) und truss(1) genutzt, sind in der Systemdefinitionsdatei <sys/procfs.h> definiert und in der Manpage proc(4) beschrieben. Beispiele für Ausdrücke mit curpsinfo, ihre Typen und Bedeutung sind:

curpsinfo->pr_pid

pid_t

aktuelle Prozess-ID 

curpsinfo->pr_fname

char []

Name der ausführbaren Datei 

curpsinfo->pr_psargs

char []

anfängliche Befehlszeilenargumente 

Sie sollten sich die vollständige Strukturdefinition später noch einmal genauer ansehen. Untersuchen Sie dazu die Definitionsdatei <sys/procfs.h> und die entsprechenden Beschreibungen in proc(4). Im nächsten Beispiel wird mit der Komponente pr_psargs durch einen Vergleich von Befehlszeilenargumenten ein bestimmter Prozess angegeben.

Strukturen werden häufig zum Erzeugen komplexer Datenstrukturen in C-Programmen eingesetzt. Folglich stellt die Fähigkeit, Strukturen aus D zu beschreiben und zu referenzieren, eine leistungsfähige Einrichtung zur Beobachtung der inneren Abläufe des Solaris-Betriebssystemkernels und seiner Systemschnittstellen dar. Der nächste Beispielcode enthält nicht nur die bereits erwähnte Struktur curpsinfo, sondern untersucht auch einige Kernelstrukturen, indem er die Beziehung zwischen dem Treiber ksyms(7D) und den read(2)-Anforderungen beobachtet. Zum Reagieren auf die Anforderungen von Lesezugriffen auf die zeichenorientierte Gerätedatei /dev/ksyms stützt sich der Treiber auf die häufig verwendeten Strukturen uio(9S) und iovec(9S).

Die uio-Struktur, auf die über den Namen struct uio oder den Typ-Aliasnamen uio_t zugegriffen wird, ist in der Manpage uio(9S) beschrieben. Sie dient zum Beschreiben von E/A-Anforderungen, für die Daten zwischen dem Kernel und einem Benutzerprozess kopiert werden müssen. uio enthält selbst einen Vektor einer oder mehrerer iovec(9S)-Strukturen, die jeweils einen Teil der E/A-Anforderung beschreiben, falls unter Verwendung der Systemaufrufe readv(2) oder writev(2) mehrere Chunks erforderlich sein sollten. Eine der Kernel-DDI-Routinen (DDI = Gerätetreiberschnittstelle), die auf struct uio einwirken, ist die Funktion uiomove(9F). Sie gehört zu einer Familie von Funktionen, derer sich Kerneltreiber bedienen, um auf read(2)-Anforderungen für Benutzerprozesse zu antworten und Daten in Benutzerprozesse zurückzukopieren.

Der Treiber ksyms verwaltet eine zeichenorientierte Gerätedatei namens /dev/ksyms. Dabei handelt es sich scheinbar um eine ELF-Datei mit Informationen über die Symboltabelle des Kernels, die aber tatsächlich nur eine vom Treiber anhand der derzeit im Kernel geladenen Module erzeugte „Illusion“ ist. uiomove(9F)-Anforderungen reagiert der Treiber mit der read(2)-Routine. Das nächste Beispiel veranschaulicht, wie die Argumente und Aufrufe von read(2) durch /dev/ksyms die Treiberaufrufe von uiomove(9F) abgleichen, um das Ergebnis wieder in den Benutzeradressraum an der für read(2) angegebenen Position zu kopieren.

Mit dem Dienstprogramm strings(1); und der Option -a können zahlreiche Lesezugriffe durch /dev/ksyms erzwungen werden . Führen Sie strings -a /dev/ksyms in Ihrer Shell aus und betrachten Sie die Ausgabe. Geben Sie in einen Texteditor die erste Klausel des Beispielskripts ein und speichern Sie es unter dem Namen ksyms.d:

syscall::read:entry
/curpsinfo->pr_psargs == "strings -a /dev/ksyms"/
{
	printf("read %u bytes to user address %x\n", arg2, arg1);
}

In dieser ersten Klausel kommt der Ausdruck curpsinfo->pr_psargs zum Zugriff auf und den Vergleich der Befehlszeilenargumente unseres Befehls strings(1) vor. So wird gewährleistet, dass das Skript vor der Verfolgung der Argumente die richtigen read(2)-Anforderungen auswählt. Beachten Sie: Durch Verwendung des Operators == mit einem Argument auf der linken Seite, das ein Vektor des Typs char ist, und einem Argument auf der rechten Seite, bei dem es sich um eine Zeichenkette handelt, schließt der D-Compiler, dass das linke Argument auf den String-Typ erweitert und ein Zeichenkettenvergleich durchgeführt werden soll. Geben Sie den Befehl dtrace -q -s ksyms.d ein und führen Sie ihn in einer Shell aus. Geben Sie dann in eine andere Shell den Befehl strings -a /dev/ksyms ein. Während der Ausführung von strings(1) generiert DTrace eine Ausgabe wie diese:


# dtrace -q -s ksyms.d
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
...
^C
#

Dieses Beispiel lässt sich mithilfe einer gebräuchlichen D-Programmiertechnik so ausweiten, dass ein Thread von dieser anfänglichen read(2)-Anforderung bis tief in den Kernel verfolgt werden kann. Bei Eintritt des Kernels in syscall::read:entry setzt das nächste Skript eine thread-lokale Flag-Variable, die angibt, dass dieser Thread von Interesse ist und bei syscall::read:return wieder gelöscht wird. Das einmal gesetzte Flag kann als Prädikat für andere Prüfpunkte zur Instrumentierung von Kernelfunktionen wie zum Beispiel uiomove(9F) verwendet werden. Der DTrace-Provider fbt (function boundary tracing) veröffentlicht Eintritt- und Rückkehr-Prüfpunkte für Funktionen, die im Kernel definiert sind, einschließlich jener in der Gerätetreiberschnittstelle. Geben Sie folgenden Quellcode ein, in dem der Provider fbtzur Instrumentierung von uiomove(9F) eingesetzt wird, und speichern Sie ihn unter dem Namen ksyms.d:


Beispiel 7–2 ksyms.d: Beziehung zwischen Ablaufverfolgung von read(2) und uiomove(9F)

/*
 * When our strings(1) invocation starts a read(2), set a watched flag on
 * the current thread.  When the read(2) finishes, clear the watched flag.
 */
syscall::read:entry
/curpsinfo->pr_psargs == "strings -a /dev/ksyms"/
{
	printf("read %u bytes to user address %x\n", arg2, arg1);
	self->watched = 1;
}

syscall::read:return
/self->watched/
{
	self->watched = 0;
}

/*
 * Instrument uiomove(9F).  The prototype for this function is as follows:
 * int uiomove(caddr_t addr, size_t nbytes, enum uio_rw rwflag, uio_t *uio);
 */
fbt::uiomove:entry
/self->watched/
{
	this->iov = args[3]->uio_iov;

	printf("uiomove %u bytes to %p in pid %d\n",
	    this->iov->iov_len, this->iov->iov_base, pid);
}

Die abschließende Klausel dieses Beispiels verwendet die thread-lokale Variable self->watched, um festzustellen, wann ein bestimmter Kernel-Thread die DDI-Routine uiomove(9F) betritt. An diesem Punkt angelangt, greift das Skript mithilfe des integrierten args-Vektors auf das vierte Argument (args[3]) von uiomove() zu, das ein Zeiger auf struct uio ist, die wiederum die Anforderung darstellt. Der D-Compiler weist jedem Element des args-Vektors automatisch den dem C-Funktionsprototypen für die instrumentierte Kernel-Routine entsprechenden Typ zu. Die uio_iov-Komponente enthält einen Zeiger auf die Struktur struct iovec für die Anforderung. Zur Verwendung in unserer Klausel wird eine Kopie dieses Zeigers in der klausel-lokalen Variable this->iov gespeichert. In der letzten Anweisung dereferenziert das Skript this->iov, um auf die iovec-Komponenten iov_len und iov_base zuzugreifen, die die Bytelänge bzw. die Ziel-Basisadresse von uiomove(9F) darstellen. Diese Werte sollten mit den Eingabeparametern des auf dem Treiber ausgeführten Systemaufrufs read(2) übereinstimmen. Wechseln Sie zu Ihrer Shell und führen Sie dtrace -q -s ksyms.d aus. Geben Sie anschließend wieder den Befehl strings -a /dev/ksyms in eine andere Shell ein. Es müsste eine Ausgabe wie im folgenden Beispiel produziert werden:


# dtrace -q -s ksyms.d
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
...
^C
#

Die Adressen und Prozess-IDs in Ihrer Ausgabe weichen zwar hiervon ab, doch sollten Sie beobachten, dass die für read(2) eingegebenen Argumente mit den vom Treiber ksyms an uiomove(9F) übergebenen Parametern übereinstimmen.

Unionen

Unionen sind eine weitere Art der von ANSI-C und D unterstützten zusammengesetzten Typen. Sie sind eng mit Strukturen verwandt. Eine Union ist ein zusammengesetzter Typ, in dem Alternativen (Komponenten) unterschiedlicher Typen definiert sind und alle Alternativenobjekte denselben Speicherbereich belegen. Sie ist folglich ein Objekt eines veränderlichen Typs, in dem, abhängig von der Zuweisung der Union, zu einem gegebenen Zeitpunkt nur je eine Alternative gültig ist. In der Regel wird die aktuelle gültige Unionsalternative durch eine andere Variable oder ein Statuselement angegeben. Eine Union hat die Größe ihrer größten Alternative, und die für sie verwendete Speicherausrichtung entspricht der maximalen für die Unionsalternativen benötigten Ausrichtung.

Das Solaris-Framework kstat definiert eine Struktur, die die in folgendem Beispiel zur Veranschaulichung von C- und D-Unionen verwendete Union enthält. Das kstat-Framework dient zum Exportieren eines Satzes benannter Zähler, die Kernel-Statistiken wie die Speichernutzung und den E/A-Datendurchsatz darstellen. Mit dem Framework werden Dienstprogramme wie mpstat(1M) und iostat(1M) implementiert. In diesem Framework stellt struct kstat_named einen benannten Zähler und dessen Werte dar. Es ist wie folgt definiert:

struct kstat_named {
	char name[KSTAT_STRLEN]; /* name of counter */
	uchar_t data_type;	/* data type */
	union {
		char c[16];
		int32_t i32;
		uint32_t ui32;
		long l;
		ulong_t ul;
		...
	} value;	/* value of counter */
};	

Die untersuchte Deklaration ist aus Gründen der Einfachheit abgekürzt. Die vollständige Strukturdefinition befindet sich in der Definitionsdatei <sys/kstat.h> und ist in kstat_named(9S) beschrieben. Die obige Deklaration ist sowohl in ANSI-C als auch in D gültig. Sie definiert eine Struktur, zu deren Komponenten ein Unionswert mit Alternativen unterschiedlicher Typen zählt, die vom Typ des Zählers abhängen. Beachten Sie, dass auf einen formalen Namen des Unionstyps verzichtet wurde, da die Union selbst innerhalb eines anderen Typs, struct kstat_named, deklariert ist. Dieser Deklarationsstil wird als anonyme Union bezeichnet. Die Komponente namens value nimmt den in der vorangehenden Deklaration beschriebenen Union-Typ an, aber dieser Union-Typ selbst hat keinen Namen, da er an keiner anderen Stelle benutzt werden muss. Der Strukturkomponente data_type wird ein Wert zugewiesen, der angibt, welche Alternative der Union für die einzelnen Objekte des Typs struct kstat_named gültig ist. Als Werte für data_type sind verschiedene C-Preprozessor-Symbole definiert. So ist beispielsweise das Symbol KSTAT_DATA_CHAR gleich Null und gibt an, dass sich die Alternative value.c an der derzeitigen Speicherposition des Werts befindet.

Beispiel 7–3 zeigt den Zugriff auf die Union kstat_named.value durch die Ablaufverfolgung eines Benutzerprozesses. Die kstat-Zähler können von Benutzerprozessen mithilfe der Funktion kstat_data_lookup(3KSTAT) geprüft werden, die einen Zeiger auf struct kstat_named zurückgibt. Bei seiner Ausführung ruft das Dienstprogramm mpstat(1M) diese Funktion wiederholt auf, um die neuesten Zählerwerte zu prüfen. Führen Sie mpstat 1 in Ihrer Shell aus und beobachten Sie die Ausgabe. Brechen Sie mpstat nach einigen Sekunden durch Drücken von Strg-C in der Shell ab. Zum Beobachten der Zählerprüfung möchten wir einen Prüfpunkt aktivieren, der mit jedem Aufruf der Funktion kstat_data_lookup(3KSTAT) in libkstat durch den Befehl mpstat ausgelöst wird. Hierfür werden wir einen neuen DTrace-Provider einsetzen: pid. Der Provider pid ermöglicht die dynamische Erzeugung von Prüfpunkten in Benutzerprozessen an C-Symbolpositionen wie beispielsweise Eintrittspunkten. Mit Prüfpunktbeschreibungen in der folgenden Form können Sie den Provider pid dazu auffordern, zum Eintritt in eine und Rückkehr aus einer Benutzerfunktion einen Prüfpunkt zu erzeugen:

pidProzess-ID:Objektname:Funktionsname:entry

 pidProzess-ID: Objektname:Funktionsname :return

Wenn Sie beispielsweise einen Prüfpunkt in dem Prozess mit der ID erzeugen möchten, der beim Eintritt in kstat_data_lookup(3KSTAT) ausgelöst wird, formulieren Sie die Prüfpunktbeschreibung wie folgt:

pid12345:libkstat:kstat_data_lookup:entry

Der Provider pid fügt an der der Prüfpunktbeschreibung entsprechenden Programmposition eine dynamische Instrumentation in den angegebenen Benutzerprozess ein. Die Prüfpunktimplementierung „lockt“ jeden Benutzer-Thread, der die instrumentierte Programmposition erreicht, in den Betriebssystemkernel und erzwingt dessen Eintritt in DTrace, wodurch der entsprechende Prüfpunkt ausgelöst wird. Obwohl also die Instrumentationsposition mit einem Benutzerprozess verbunden ist, werden die von Ihnen angegebenen DTrace-Prädikate und -Aktionen weiterhin im Kontext des Betriebssystemkernels ausgeführt. Der Provider pid wird in Kapitel 30Der Provider pid ausführlicher behandelt.

Damit Sie einen D-Programmquellcode nicht jedes Mal bearbeiten müssen, wenn Sie das Programm auf einen anderen Prozess anwenden möchten, können Sie so genannte Makrovariablen in Ihr Programm einsetzen, die zur Kompilierungszeit des Programms ausgewertet und durch die zusätzlichen dtrace-Befehlszeilenargumente ersetzt werden. Makrovariablen werden mit dem Dollarzeichen $, gefolgt von einem Bezeichner oder einer Ziffer angegeben. Wenn Sie den Befehl dtrace -s Skript foo bar baz ausführen, definiert der D-Compiler die Makrovariablen $1, $2 und $3 automatisch als die Symbole foo, bar bzw. baz. Makrovariablen können in D-Programmausdrücken oder in Prüfpunktbeschreibungen benutzt werden. Die folgenden Prüfpunktbeschreibungen instrumentieren beispielsweise jede Prozess-ID, die als zusätzliches Argument für dtrace angegeben wird:

pid$1:libkstat:kstat_data_lookup:entry
{
	self->ksname = arg1;
}

pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 != NULL/
{
	this->ksp = (kstat_named_t *)copyin(arg1, sizeof (kstat_named_t));
	printf("%s has ui64 value %u\n", copyinstr(self->ksname),
	    this->ksp->value.ui64);
}

pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 == NULL/
{
	self->ksname = NULL;
}

Makrovariablen und wieder verwendbare Skripten werden in Kapitel 15Scripting ausführlicher besprochen. Nachdem wir jetzt wissen, wie Benutzerprozesse anhand ihrer Prozess-ID instrumentiert werden, kehren wir zum Prüfen von Unionen zurück. Geben Sie den Quellcode für unser vollständiges Beispiel in einen Texteditor ein und speichern Sie ihn unter dem Namen kstat.d:


Beispiel 7–3 kstat.d: Ablaufverfolgungsaufrufe von kstat_data_lookup(3KSTAT )

pid$1:libkstat:kstat_data_lookup:entry
{
	self->ksname = arg1;
}

pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 != NULL/
{
	this->ksp = (kstat_named_t *) copyin(arg1, sizeof (kstat_named_t));
	printf("%s has ui64 value %u\n",
	    copyinstr(self->ksname), this->ksp->value.ui64);
}

pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 == NULL/
{
	self->ksname = NULL;
}

Führen Sie jetzt in einer Shell den Befehl mpstat 1 aus, um mpstat(1M) in einem Modus zu starten, in dem statistische Daten gesammelt und einmal pro Sekunde gemeldet werden. Sobald mpstat läuft, führen Sie in der anderen Shell den Befehl dtrace -q -s kstat.d `pgrep mpstat` aus. Sie sehen eine den Statistiken, auf die zugegriffen wird, entsprechende Ausgabe. Drücken Sie Strg-C, um dtrace abzubrechen und zur Shell-Eingabeaufforderung zurückzukehren.


# dtrace -q -s kstat.d `pgrep mpstat`
cpu_ticks_idle has ui64 value 41154176
cpu_ticks_user has ui64 value 1137
cpu_ticks_kernel has ui64 value 12310
cpu_ticks_wait has ui64 value 903
hat_fault has ui64 value 0
as_fault has ui64 value 48053
maj_fault has ui64 value 1144
xcalls has ui64 value 123832170
intr has ui64 value 165264090
intrthread has ui64 value 124094974
pswitch has ui64 value 840625
inv_swtch has ui64 value 1484
cpumigrate has ui64 value 36284
mutex_adenters has ui64 value 35574
rw_rdfails has ui64 value 2
rw_wrfails has ui64 value 2
...
^C
#

Wenn Sie die Ausgabe in jedem Terminalfenster erfassen und jeden Wert von dem durch die vorige Iteration gemeldeten Wert in der Statistik subtrahieren, sollte es möglich sein, eine Beziehung zwischen der dtrace- und der mpstat-Ausgabe zu erkennen. Das Beispielprogramm zeichnet den Zählernamenzeiger beim Eintritt in die Lookup-Funktion auf und führt einen Großteil der Ablaufverfolgung nach der Rückkehr aus kstat_data_lookup(3KSTAT) durch. Wenn arg1() (der Rückgabewert) nicht NULL() ist, kopieren die in D integrierten Funktionen copyinstr und copyin die Funktionsresultate aus dem Benutzerprozess zurück nach DTrace. Nach dem Kopieren der kstat-Daten meldet das Beispielprogramm den ui64-Zählerwert aus der Union. In diesem vereinfachten Beispiel wird davon ausgegangen, dass mpstat Zähler prüft, die die Alternative value.ui64 ansprechen. Versuchen Sie nun übungshalber, kstat.d mehrere Prädikate ansprechen und die der Komponente data_type entsprechende Unionsalternative ausgeben zu lassen. Sie können auch eine Version von kstat.d schreiben, die die Differenz zwischen aufeinander folgenden Datenwerten berechnet und eine ähnliche Ausgabe wie mpstat erzeugt.

Komponentengrößen und Versatz

Mit dem Operator sizeof lässt sich die Größe in Byte aller beliebigen D-Typen oder -Ausdrücke sowie von Strukturen oder Unionen ermitteln. Der Operator sizeof kann entweder auf einen Ausdruck oder auf den Namen eines in Klammern stehenden Typs angewendet werden:

sizeof Ausdruck				sizeof (Typname)

So würde beispielsweise der Ausdruck sizeof (uint64_t) den Wert 8 zurückgeben, und der Ausdruck sizeof (callinfo.ts) würde im Quellcode des obigen Programmbeispiels ebenfalls 8 zurückgeben. Der formale Rückgabetyp des Operators sizeof ist der Typ-Aliasname size_t, der laut Definition eine vorzeichenlose Ganzzahl derselben Größe eines Zeigers im aktuellen Datenmodell ist und zur Darstellung der Byte-Anzahl verwendet wird. Bei Anwendung des Operators sizeof auf einen Ausdruck wird der Ausdruck vom D-Compiler überprüft, doch die resultierende Objektgröße wird zur Kompilierungszeit berechnet und es erfolgt keine Generierung von Code für den Ausdruck. Sie können sizeof überall dort einsetzen, wo eine ganzzahlige Konstante erforderlich ist.

Mit dem Operator offsetof lässt sich der Versatz (in Byte) einer Struktur- oder Unionskomponente zum Anfang des einem beliebigen Objekt des Typs struct oder union zugewiesenen Speicherbereichs ermitteln. Der Operator offsetof wird in Ausdrücken der folgenden Form verwendet:

offsetof (Typname, Komponentenname)

Dabei ist Typname der Name eines Struktur- oder Union-Typs oder ein Typ-Aliasname, und Komponentenname ist der Bezeichner einer Komponente dieser Struktur bzw. einer Alternative dieser Union. Genau wie sizeof gibt auch offsetof den Typ size_t zurück und kann überall dort in einem D-Programm eingesetzt werden, wo eine ganzzahlige Konstante zulässig ist.

Bit-Felder

D erlaubt auch die Definition von ganzzahligen Strukturkomponenten und Unionsalternativen mit beliebiger Anzahl Bits. Diese werden als Bit-Felder bezeichnet. Zur Deklaration eines Bit-Felds werden ein vorzeichenbehafteter oder vorzeichenloser Integer-Basistyp, ein Komponentenname und die dem Feld zuweisende Anzahl Bits angegeben:

struct s {
	int a : 1;
	int b : 3;
	int c : 12;
};

Die Breite des Bit-Felds ist eine ganzzahlige Konstante, die vom Komponentennamen durch einen angehängten Strichpunkt getrennt ist. Die Bit-Feldbreite muss positiv sein und ihre Bitanzahl darf die Breite des entsprechenden Integer-Basistyps nicht überschreiten. Bit-Felder mit einer Größe von über 64 Bit dürfen in D nicht deklariert werden. Bit-Felder in D bieten Kompatibilität mit der entsprechenden ANSI-C-Fähigkeit und Zugriff auf diese. In der Regel werden Bit-Felder zum Sparen von Speicherplatz eingesetzt oder dann, wenn das Strukturlayout mit dem Layout eines Hardwareregisters übereinstimmen muss.

Ein Bit-Feld ist ein Compiler-Konstrukt, mit dem sich die Anordnung einer Ganzzahl und einer Gruppe von Masken zum Extrahieren der Komponentenwerte automatisieren lässt. Dasselbe Ergebnis erhalten Sie, indem Sie die Masken einfach selbst definieren und den Operator & verwenden. Der C- und der D-Compiler versuchen stets, Bits so effizient wie möglich zusammenzupacken. Sie haben hierfür aber keine Vorgabe hinsichtlich der Reihenfolge oder Art und Weise. Deshalb ergeben identische Bit-Felder auf unterschiedlichen Compilern oder Architekturen nicht unbedingt dieselbe Bit-Anordnung. Wenn Sie ein stabiles Bit-Layout benötigen, sollten Sie die Bit-Masken selbst konstruieren und die Werte mit dem Operator & extrahieren.

Der Zugriff auf eine Komponente eines Bit-Felds erfolgt einfach durch die Angabe seines Namens in Kombination mit einem der Operatoren „.” oder ->, wie bei jeder anderen Strukturkomponente oder Unionsalternative. Das Bit-Feld wird automatisch auf den nächstgrößeren Integer-Typ zur Verwendung in einem beliebigen Ausdruck erweitert. Da der Speicherbereich für ein Bit-Feld nicht an einer Byte-Grenze ausgerichtet sein oder eine gerade Bytezahl als Größe aufweisen darf, können die Operatoren sizeof und offsetof nicht auf Komponenten eines Bit-Felds angewendet werden. Der D-Compiler verbietet außerdem das Abrufen der Adresse einer Bit-Feldkomponente mit dem Operator &.