
| Aggregat Eine Einheit, die durch Zusammensetzung einzelner, relativ selbstständiger Teile zustande kommt. Hier: Statistische Größen, die sich durch einzelne Messwerte ergeben. |
| Assoziatives Array Sonderform eines Arrays. Statt eines numerischen Indexes wird ein Schlüssel zur Indizierung und damit zur Adressierung eines Elementes verwendet. |
| AWK Programmiersprache zur Bearbeitung und Auswertung von Textdateien. Sie ist benannt nach den Initialen ihrer Programmierer: Alfred V. Aho, Peter J. Weinberger und Brian W. Kernighan. |
| dexplorer Skript aus der DtraceToolkits Skriptsammlung. |
| Externe Variablen Variable, die extern, also außerhalb des fraglichen Programms definiert wurde. |
| Header-Datei C-Quellcode, der typischerweise Definitionen von Variablen und Strukturen enthält. |
| Histogramm Graphische Darstellung der Häufigkeitsverteilung von Messwerten. |
| Thread Elementaraufgabe. Die Befehle eines Threads sind in sich so abgeschlossen, dass sie auf einer CPU zusammenhängend ausgeführt werden können. Um Programme mehrprozessorfähig zu gestalten, müssen die Abläufe in Threads untergliedert sein. |
| Threadlocal zu einem Thread gehörend |
| Speicher deallokieren Softwareprogramme können allokierten Speicher wieder deallokieren, d. h. wieder freigeben. |
| Aggregatsfunktion Eine mathematische Funktion, die aus einzelnen Werten eine Aggregatsgröße (z. B. Mittelwert, Summe, Anzahl) ermittelt. |
| Speicher allokieren Softwareprogramme können Hauptspeicher oder Ressourcen allokieren, d. h. zur eigenen Verwendung ansprechen oder reservieren. |
| Array Mit Hilfe eines Arrays können Daten eines einheitlichen Datentyps geordnet so im Speicher eines Computers abgelegt werden, dass ein Zugriff auf die Daten über einen ganzzahligen Index möglich wird. |
| Dispatcher Im Rahmen der Prozessverwaltung eines Betriebssystems dient der Dispatcher dazu, bei einem Prozesswechsel dem derzeit aktiven Prozess die CPU zu entziehen und anschließend dem nächsten Prozess die CPU zuzuteilen. Demgegenüber wird die Entscheidung, welcher Prozess der nächste ist, vom Scheduler im Rahmen der Warteschlangenorganisation getroffen. |
| Instanz Meist ist ein Treiber für eine Vielzahl gleichartiger Geräte zuständig. Jedes Gerät des gleichen Treibers bildet eine eigene Instanz. |
| Interrupt (lat.: Unterbrechung) Die kurzfristige Unterbrechung eines Programms durch eine von der CPU abzuarbeitende Befehlssequenz. Interrupts werden durch Hardware ausgelöst und vom BIOS, einem speziellen Controller oder dem entsprechenden Hardwaretreiber abgearbeitet. |
| Interruptbehandlung Die Befehlsfolge, die beim Eintreten eines Interrupts abgearbeitet wird. |
| Kernel Der Kernel ist der zentrale Bestandteil des Betriebssystems. In ihm ist die Prozess- und Datenorganisation festgelegt, auf der alle weiteren Softwarebestandteile des Betriebssystems aufbauen. Im Kernel ist die Kommunikation mit der Hardware, die Prozessorverwaltung, die Speicherverwaltung, die Prozessverwaltung, die Geräteverwaltung und das Dateisystem integriert. |
| Majornumber stellt die Beziehung zwischen der im Filesystem angelegten Gerätedatei auf der einen Seite und dem Treiber auf der anderen Seite her. |
| Minornumber stellt die Beziehung zwischen der im Filesystem angelegten Gerätedatei einerseits und dem tatsächlichen Gerät andererseits her. Die Minornumber dient dem Gerätetreiber zur weiteren Differenzierung, z. B. zur Bestimmung des genauen Gerätes. |
| Scheduler trifft die Entscheidung, welcher Prozess der nächste ist; siehe Dispatcher. |
| Struktur Datentyp, in dem Daten unterschiedlicher Datentypen zusammengefasst werden. |
Weiterführende Links
Aus dem ersten Teil der Reihe in Ausgabe 4/2005 kennen wir die prinzipielle Funktionsweise von DTrace und den rudimentären Aufbau eines D-Skriptes. So wurde unter anderem truss nachgebildet.
In Teil II (Ausgabe 1/2006) haben wir die vorhandenen Messpunkte kategorisiert und somit einen Überblick über mögliche Messungen erlangt. Im Folgenden wollen wir uns der Sprache D genauer widmen und dabei das DtraceToolkit kennenlernen.
Dtrace bietet unzählige Analysemöglichkeiten, sofern entsprechende Skripte vorhanden sind. Der Aufwand, ein eigenes Analyseskript zu schreiben, lässt sich vermeiden, indem vorgefertigte Skripte genutzt werden.
Als wichtige Skriptsammlung ist das DtraceToolkit von Brendan Gregg zu nennen [1]. Es bietet etwa 70 verschiedene Skripte, womit Fragestellungen zu den unterschiedlichen Themen (Netzwerk, Prozesse, CPU, IO, ...) analysiert werden können.
Das Skript dexplorer sammelt die unterschiedlichen Messdaten. Jede Fragestellung wird in einer separaten Datei beantwortet und alle zusammen werden in ein Tar-Archiv gepackt. Tatsächlich ist dexplorer ein Shellskript, das nacheinander unterschiedliche D-Skripte zur Ausführung bringt. Diese einzelnen Skripte bieten sich an, um daran Dtrace-spezifische Einzelheiten aus dexplorer kennenzulernen. Das gesamte Skript und alle weiteren DtraceToolkit-Skripte finden sich unter [1] .
Um die Ausgaben der unterschiedlichen D-Skripte innerhalb von dexplorer mit einer einheitlichen Überschrift zu versehen, wird in der Shell-Variable header der Beginn der dtrace-Skripte inklusive Ausgabe der Überschrift definiert.
Abbildung 1 zeigt den Programmcode, während Abbildung 2 die dadurch entstehende Überschrift darstellt. Es wird deutlich, dass neben dem Datum (walltimestamp), der Betriebssystemname, der Rechnername, die Kernelversion und die Prozessorarchitektur dargestellt werden. Das Skript greift dazu auf Externe Variablen zu.
170 header='dtrace:::BEGIN {
171 printf("%Y, ", walltimestamp);
172 printf("%s %s %s %s %s, ", `utsname.sysname, `utsname.nodename,
173 `utsname.release, `utsname.version, `utsname.machine);
174 printf("%d secs\n",'$interval');
175 }
176 profile:::tick-'$interval'sec { exit(0); }
177 '
|
| Abb. 1: Der Programmcode für die Überschrift in dexplorer nutzt externe Variablen. |
2006 May 18 23:48:56, SunOS jupiter 5.10 Generic_118844-26 i86pc, 5 secs |
| Abb. 2: Die Ausgabe der Überschrift in dexplorer. |
Externe Variablen sind Variablen des zu analysierenden Programms (z. B. des Kernels). Sie erhalten im Skript den Accent grave (`) als Präfix. Häufig kann in Header-Dateien die Definition der Variablen ermittelt werden. Der Aufbau der Struktur utsname ist z. B. in der Datei /usr/include//sys/utsname.h definiert.
Durch die Zeile 176 wird nach einem vordefinierten Messzeitraum das Programm beendet. Ist die Shellvariable $interval z. B mit dem Wert 5 belegt, so wird der Messpunkt profile:::tick-5sec nach 5 Sekunden exit 0 ausführen. Dies führt zur Beendigung der Messung, wobei ein möglicher dtrace:::END Messpunkt noch ausgeführt wird.
Dtrace kennt assoziative Arrays, worin, ähnlich wie bei AWK, Werte gespeichert werden können. Weitaus häufiger als Arrays werden so genannte Aggregate verwendet. Im Unterschied zu den Arrays, wird in einem Aggregat nicht ein einmalig aufgetretener Wert zu einem speziellen Index dargestellt, sondern meist eine Zusammenfassung häufig aufgetretener Werte zu einem bestimmten Index (z. B. der Mittelwert oder die Anzahl).
Gleich das erste Dtrace-Skript "Interrupts by CPU..." innerhalb dexplorer greift zu Aggregaten und ermittelt die Anzahl aufgetretener Interrupts pro CPU (Aggregat: Anzahl pro CPU).
In Abbildung 3 Zeile 244 wird das Aggregat @num[cpu] mit Hilfe der Aggregatsfunktion count() gefüllt. Das Aggregat wird durch das Präfix @ als solches kenntlich. Rechts vom Zuweisungszeichen = darf nur eine Aggregatsfunktion stehen. Einfache Aggregatsfunktionen sind in Abbildung 4 dargestellt.
242 dstatus "Interrupts by CPU..."
243 $dtrace -qn "$header"'
244 sdt:::interrupt-start { @num[cpu] =
245 dtrace:::END
246 {
247 printf("%-16s %16s\n", "CPU", "INTERRUPTS");
248 printa("%-16d %@16d\n", @num);
249 }
250 ' | $clean > Cpu/interrupt_by_cpu
|
| Abb. 3: Um die Interrupts pro CPU zu ermitteln, werden Aggregate genutzt. |
| Funktionsname | Argument | Ergebnis |
| Count | keines | Anzahl Aufrufe |
| Sum | Zahl | Gesamtsumme der angegebenen Zahlen. |
| Avg | Zahl | Arithmetisches Mittel der angegebenen Zahlen. |
| min | Zahl | Kleinster Wert unter den angegebenen Zahlen. |
| max | Zahl | Größter Wert unter den angegebenen Zahlen. |
| Abb. 4: Einfache Aggregatsfunktionen. | ||
Die Ausgabe eines Aggregats erfolgt implizit am Ende des Skriptes, ohne dafür eine Zeile Programmcode investieren zu müssen. Will der Skriptentwickler die Ausgabe bzw. das Ausgabeformat beeinflussen, so kann er dies mit der Funktion printa tun.
CPU INTERRUPTS 0 516 |
| Abb. 5: Die Ausgabe des Aggregats gibt an, wie viele Interrupts pro CPU eintraten. |
In Abbildung 3 wird in Zeile 248 das gesamte Aggregat mit allen gesammelten Werten ausgegeben. Die dadurch entstehende Ausgabe ist auf dem hier dargestellten Einprozessorsystem nicht sonderlich spektakulär. Sie ist in Abbildung 5 dargestellt und zeigt, wie erwähnt, die Anzahl der aufgetretenen Interrupts pro CPU.
Schon das nächste Skript "Interrupt times ..." nutzt alle bisher kennengelernten Techniken und noch weitere. Da Interrupts durch Geräte verursacht werden, soll pro Gerät die Dauer der Interrupt-Behandlung ermittelt werden.
In Zeile 254 wird die Zeit zu Beginn der Interrupt-Bearbeitung mittels vtimestamp ermittelt (Abbildung 6). In Zeile 262 wird die verstrichene Zeit der Interrupt-Bearbeitung durch Subtraktion ermittelt und in einem Aggregat festgehalten.
252 dstatus "Interrupt times..."
253 $dtrace -qn "$header"'
254 sdt:::interrupt-start { self->ts =
255 sdt:::interrupt-complete
256 /self->ts && arg0 != 0/
257 {
258 this->devi = (struct dev_info *)
259 self->name = this->devi != 0 ?
260 stringof('devnamesp[this->devi->.dn_name) : "?";
261 this->inst = this->devi != 0 ?->devi_instance : 0;
262 @num[self->name, this->inst] = (vtimestamp - self->ts);
263 self->name = 0;
264 }
265 sdt:::interrupt-complete { self->
266 dtrace:::END
267 {
268 printf("%11s %16s\n", "DEVICE", "
269 printa("%10s%-3d %@16d\n", @num);
270 }
271 ' | $clean > Cpu/interrupt_time
|
| Abb. 6: Zur Ermittlung der Interrupt-Dauer werden Thread-lokale Variablen verwendet. |
Damit die Interrupt-Bearbeitung unterschiedlicher Threads unabhängig voneinander analysiert werden kann, wird die Startzeit durch Angabe des Präfixes self-> als Threadlokaler Wert, und damit unabhängig von allen anderen Threads, gespeichert. Die Messwerte sind somit eindeutig getrennt.
Über das Argument arg0 in Zeile 258, welches durch den Messpunkt sdt:::interrupt-complete bereitgestellt wird, wird auf eine externe Struktur zugegriffen und in der Variablen this->-devi hinterlegt. In Zeile 260 wird die Majornumber des Interruptauslösenden Gerätes genutzt, um letztlich den Treibernamen zu ermitteln. Dieser wird in der Variablen self->name hinterlegt.
Für den genauen Aufbau der hier verwendeten Strukturen müssen wiederum die entsprechenden Header-Dateien ermittelt und analysiert werden.
Bitte beachten Sie, dass die Zuweisungen in Zeile 259, 260 und in Zeile 261 bedingte Zuweisungen sind. Zeile 261 meint: Wenn die Variable this->devi nicht den Wert 0 hat, dann nutze den Wert this->devi->devi_instance, sonst nutze den Wert 0. Auf diese Weise werden Treibername und Instanzname als 0 angenommen, wenn die entsprechenden Parameter nicht vorhanden sind.
DEVICE TIME (ns) ata1 1342875 ata0 9300243 |
| Abb. 7: Die Interrupt-Dauer wird aufgelistet nach Geräten ausgegeben. |
Nun sind alle Parameter bekannt, um sich der Zeile 262 zu widmen. Die verstrichene Zeit wird berechnet und in einem Aggregat festgehalten, welches die Gesamtzeit der Interrupt-Bearbeitung in Abhängigkeit vom Treibernamen (self->name) und vom Instanznamen (this->inst) ermittelt.
Durch die Zuordnung des Wertes 0 in Zeile 263 wird in der Sprache D der Speicher für die Variable self->name deallockiert. Die Ausgabe wird durch die Zeilen 268 und 269 erzeugt und ist in Abbildung 7 dargestellt.
273 dstatus "Dispatcher queue length by CPU..."
274 $dtrace -qn "$header"'
275 profile:::profile-1000
276 {
277 this->num = curthread->t_cpu->cpu_disp->disp_nrunnable;
278 @length[cpu] = lquantize(this->num, 0, 100, 1);
279 }
280 dtrace:::END { printa(" CPU %d%@d\n", @length);}
281 ' | $clean > Cpu/dispqlen_by_cpu |
| Abb. 8: Die Aggregatsfunktion lquantize ermöglicht die Ausgabe von Histogrammen. |
Das nächste Skript "Dispatcher queue length by CPU ..." ist wieder etwas kürzer, bietet jedoch ein Highlight. In Zeile 277 (Abbildung 8) wird über die Dtrace-Built-In Struktur cur-thread die Anzahl wartender Threads für eine CPU ermittelt. Das Aggregat @length[cpu] wird in Zeile 278 mit diesem Wert durch die Aggregatsfunktion lquantize gefüllt. Dadurch wird bei der Ausgabe pro CPU ein Histogramm erstellt.
CPU 0 value ------- Distribution ------- count < 0 | 0 0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 4829 1 | 54 2 | 8 3 | 1 4 | 1 5 | 1 6 | 0 |
| Abb. 9: Das Histogramm gibt Aufschluss über die Verteilung der Messwerte. |
In Abbildung 9 sehen wir, dass während des Messzeitraums 4829 mal 0 Threads auf CPU-Zeit gewartet haben. 54 mal war genau ein Thread in der Warteschlange des Prozessors 0. 8 mal warteten 2 Threads. Einmal warteten 3, einmal 4 und einmal 5 Threads. 6 oder mehr Threads und weniger als 0 Threads warteten nie auf die CPU 0. Da es sich bei dem fraglichen System um eine Einprozessormaschine handelt, werden keine weiteren Histogramme angezeigt.
Beim Aufruf der Funktion lquantize() in Zeile 278 wurde neben dem zu messenden Wert noch der Bereich (0 bis 100) und die Stufenweite (1) angegeben. Die Funktion quantize liefert demgegenüber ein logarithmisches Histogramm. Die Angabe der Schrittweite beziehungsweise der Bereichsgrenzen ist dabei nicht notwendig (siehe Abbildung 10).
| Funktionsname | Argument | Ergebnis |
| Quantize | Zahl | Liefert ein logarithmisches Verteilungsdiagramm der angegebenen Ausdrücke. Alle Werte, die kleiner oder gleich dem in der Zeile angegebenen, aber größer als der eine Zeile darüber angegebene Wert sind, wurden bei der Anzahl beachtet. |
| Quantize | Zahl, untere Grenze, obere Grenze, Schrittweite | Liefert ein lineares Verteilungsdiagramm der angegebenen Ausdrücke. Die Staffelung erfolgt entsprechend der Schrittweite von der Ober- bis zur Untergrenze. |
| Abb. 10: Aggregatsfunktionen zur Erzeugung von Histogrammen. | ||
Die weiteren D-Skripte innerhalb von dexplorer sind ähnlich aufgebaut und bieten uns keine grundlegend neuen Erkenntnisse. Für die detaillierte Analyse der verwendeten Kernelstrukturen müssen häufig der "Solaris Dynamic Tracing Guide" und die Header-Dateien im Verzeichnis /usr/include als Informationsquelle herangezogen werden.
Es wird deutlich, wie mächtig Dtrace ist und welche Techniken zur Analyse angewendet werden können. Ein Blick auf das DtraceToolkit lohnt sich, denn auch die weiteren Skripte sind lesenswert und ermöglichen Erkenntnisse über das System, die sich ohne Dtrace wahrscheinlich nur vermuten ließen.
Markus Schreier (info@ordix.de).