In diesem Kapitel wird der fbt-Provider (function boundary tracing) besprochen, der Prüfpunkte für den Eintritt in und die Rückkehr aus den meisten Funktionen im Solaris-Kernel zur Verfügung stellt. Die Funktion ist die Grundeinheit des Programmtexts. In einem gut gestalteten System führt jede Funktion eine diskrete und genau definierte Operation an einem angegebenen Objekt oder einer Serie gleicher Objekte durch. Aus diesem Grund stellt FBT selbst auf den kleinsten Solaris-Systemen rund 20.000 Prüfpunkte zur Verfügung.
Ähnlich wie andere DTrace-Provider bewirkt FBT keine Prüfaktivität, wenn er nicht ausdrücklich aktiviert wird. Bei Aktivierung löst FBT nur in den untersuchten Funktionen Prüfaktivität aus. Die FBT-Implementierung ist stark an der jeweiligen Befehlssatzarchitektur ausgerichtet. FBT wurde sowohl auf SPARC- als auch x86-Plattformen implementiert. Jeder Befehlssatz enthält einige wenige Funktionen, die keine anderen Funktionen aufrufen und vom Compiler maximal optimiert werden (so genannte Leaf-Funktionen) und nicht von FBT instrumentiert werden können. Für diese Funktionen bietet DTrace keine Prüfpunkte.
Eine effektive Nutzung von FBT-Prüfpunkten setzt die Kenntnis der Betriebssystemimplementierung voraus. Wir empfehlen daher, auf FBT nur zur Entwicklung von Kernelsoftware zurückzugreifen, oder wenn andere Prüfpunkte für den Zweck nicht ausreichen. Mithilfe anderer DTrace-Provider, einschließlich syscall, sched, proc und io, lassen sich die meisten Fragen in Bezug auf die Systemanalyse beantworten, ohne dass eine Kenntnis der Betriebssystemimplementierung erforderlich ist.
FBT stellt einen Prüfpunkt an der Grenze der meisten Funktionen im Kernel bereit. Die Grenze einer Funktion wird beim Eintritt in die Funktion und bei der Rückkehr von der Funktion übertreten. FBT bietet deshalb zwei Prüfpunkte für jede Funktion im Kernel: eine beim Funktionseintritt und eine bei Rückkehr von der Funktion. Diese Prüfpunkte heißen entry bzw. return. Als Teil des Prüfpunkts werden der Funktions- und der Modulname angegeben. Alle FBT-Prüfpunkte geben einen Funktions- und einen Modulnamen an.
Die Argumente für entry-Prüfpunkte stimmen mit den Argumenten für die entsprechende Funktion im Betriebssystemkernel überein. Auf die Argumente kann nach Art des Typs mit dem Vektor args[] zugegriffen werden. Auf diese Argumente kann in Form von int64_t über arg0 .. argn-Variablen zugegriffen werden.
Während eine Funktion nur einen einzigen Eintrittspunkt hat, kann sie an mehreren Punkten zum Aufrufer zurückkehren. In der Regel ist man entweder an dem Wert interessiert, den eine Funktion zurückgibt, oder an der Tatsache, dass sie überhaupt zurückkehrt, weniger jedoch, an dem spezifischen Rückkehrpfad. FBT ruft die verschiedenen Rückkehrpunkte einer Funktion deshalb in einen einzigen return-Prüfpunkt ab. Wenn Sie an dem genauen Rückkehrpfad interessiert sind, können Sie den args[0]-Wert des return-Prüfpunkts untersuchen, der den Versatz (Offset) in Byte der rückkehrenden Anweisung im Funktionstext wiedergibt.
Hat die Funktion einen Rückgabewert, ist dieser in args[1] gespeichert. Wenn eine Funktion keinen Rückgabewert besitzt, ist args[1] nicht definiert.
FBT ermöglicht Ihnen eine einfache Untersuchung der Kernelimplementierung. Das folgende Beispielskript zeichnet die erste ioctl(2) eines jeden xclock-Prozesses auf und folgt dann dem weiteren Codepfad durch den Kernel:
/*
* To make the output more readable, we want to indent every function entry
* (and unindent every function return). This is done by setting the
* "flowindent" option.
*/
#pragma D option flowindent
syscall::ioctl:entry
/execname == "xclock" && guard++ == 0/
{
self->traceme = 1;
printf("fd: %d", arg0);
}
fbt:::
/self->traceme/
{}
syscall::ioctl:return
/self->traceme/
{
self->traceme = 0;
exit(0);
}
Die Ausführung dieses Skripts erzeugt eine Ausgabe wie in folgendem Beispiel:
# dtrace -s ./xioctl.d dtrace: script './xioctl.d' matched 26254 probes CPU FUNCTION 0 => ioctl fd: 3 0 -> ioctl 0 -> getf 0 -> set_active_fd 0 <- set_active_fd 0 <- getf 0 -> fop_ioctl 0 -> sock_ioctl 0 -> strioctl 0 -> job_control_type 0 <- job_control_type 0 -> strcopyout 0 -> copyout 0 <- copyout 0 <- strcopyout 0 <- strioctl 0 <- sock_ioctl 0 <- fop_ioctl 0 -> releasef 0 -> clear_active_fd 0 <- clear_active_fd 0 -> cv_broadcast 0 <- cv_broadcast 0 <- releasef 0 <- ioctl 0 <= ioctl |
Die Ausgabe zeigt, dass ein xclock-Prozess ioctl() auf einem Dateibezeichner aufgerufen hat, der scheinbar einem Socket entspricht.
Darüber hinaus kann Ihnen FBT dabei helfen, das Verhalten von Kerneltreibern zu verstehen. Beispielsweise kann im Fall des Treibers ssd(7D) über zahlreiche Codepfade EIO zurückgegeben werden. Das folgende Beispiel zeigt, dass sich mit FBT problemlos der genaue Codepfad ermitteln lässt, der eine Fehlerbedingung verursacht hat:
fbt:ssd::return
/arg1 == EIO/
{
printf("%s+%x returned EIO.", probefunc, arg0);
}
Um weitere Informationen über eine Rückkehr von EIO zu erhalten, könnte man alle fbt-Prüfpunkte spekulativ verfolgen und anschließend, je nach Rückgabewert der spezifischen Funktion, commit() (oder discard()) anwenden. Ausführliche Informationen zur spekulativen Ablaufverfolgung finden Sie in Kapitel 13Spekulative Ablaufverfolgung.
Alternativ können Sie FBT einsetzen, um den innerhalb eines angegebenen Moduls aufgerufenen Funktionen auf den Grund zu gehen. Im nächsten Beispiel werden alle im UFS aufgerufenen Funktionen aufgelistet:
# dtrace -n fbt:ufs::entry'{@a[probefunc] = count()}'
dtrace: description 'fbt:ufs::entry' matched 353 probes
^C
ufs_ioctl 1
ufs_statvfs 1
ufs_readlink 1
ufs_trans_touch 1
wrip 1
ufs_dirlook 1
bmap_write 1
ufs_fsync 1
ufs_iget 1
ufs_trans_push_inode 1
ufs_putpages 1
ufs_putpage 1
ufs_syncip 1
ufs_write 1
ufs_trans_write_resv 1
ufs_log_amt 1
ufs_getpage_miss 1
ufs_trans_syncip 1
getinoquota 1
ufs_inode_cache_constructor 1
ufs_alloc_inode 1
ufs_iget_alloced 1
ufs_iget_internal 2
ufs_reset_vnode 2
ufs_notclean 2
ufs_iupdat 2
blkatoff 3
ufs_close 5
ufs_open 5
ufs_access 6
ufs_map 8
ufs_seek 11
ufs_addmap 15
rdip 15
ufs_read 15
ufs_rwunlock 16
ufs_rwlock 16
ufs_delmap 18
ufs_getattr 19
ufs_getpage_ra 24
bmap_read 25
findextent 25
ufs_lockfs_begin 27
ufs_lookup 46
ufs_iaccess 51
ufs_imark 92
ufs_lockfs_begin_getpage 102
bmap_has_holes 102
ufs_getpage 102
ufs_itimes_nolock 107
ufs_lockfs_end 125
dirmangled 498
dirbadname 498
|
Wenn Sie den Zweck oder die Argumente einer Kernelfunktion kennen, können Sie mithilfe von FBT nachvollziehen, wie oder weshalb diese Funktion aufgerufen wird. putnext(9F) nimmt beispielsweise als erstes Element einen Zeiger auf eine queue(9S)-Struktur an. Die Komponente q_qinfo der Struktur queue ist ein Zeiger auf eine qinit(9S)-Struktur. Die Komponente qi_minfo der Struktur qinitbesitzt einen Zeiger auf eine module_info(9S)-Struktur, die in ihrer Komponente mi_idname den Modulnamen enthält. Im nächsten Beispiel werden diese Informationen zusammengefügt. Dabei werden mit demFBT Prüfpunkt in putnextdie putnext(9F) -Aufrufe nach Modulnamen aufgezeichnet:
fbt::putnext:entry
{
@calls[stringof(args[0]->q_qinfo->qi_minfo->mi_idname)] = count();
}
Die Ausführung des obigen Skripts erzeugt eine Ausgabe wie in folgendem Beispiel:
# dtrace -s ./putnext.d ^C iprb 1 rpcmod 1 pfmod 1 timod 2 vpnmod 2 pts 40 conskbd 42 kb8042 42 tl 58 arp 108 tcp 126 ptm 249 ip 313 ptem 340 vuid2ps2 361 ttcompat 412 ldterm 413 udp 569 strwhead 624 mouse8042 726 |
Außerdem lässt sich mit FBT die in einer bestimmten Funktion verbrachte Zeit ermitteln. Aus dem nächsten Beispiel geht hervor, wie sich die Aufrufer der DDI-Verzögerungsroutinen drv_usecwait(9F) und delay(9F) ermitteln lassen.
fbt::delay:entry,
fbt::drv_usecwait:entry
{
self->in = timestamp
}
fbt::delay:return,
fbt::drv_usecwait:return
/self->in/
{
@snoozers[stack()] = quantize(timestamp - self->in);
self->in = 0;
}
Besonders interessant ist es, dieses Beispielskript beim Booten auszuführen. Kapitel 36Anonyme Ablaufverfolgung beschreibt das Vorgehen zum Ausführen einer anonymen Ablaufverfolgung während des Bootens eines Systems. Nach dem Neustart kann eine Ausgabe wie im folgenden Beispiel angezeigt werden:
# dtrace -ae
ata`ata_wait+0x34
ata`ata_id_common+0xf5
ata`ata_disk_id+0x20
ata`ata_drive_type+0x9a
ata`ata_init_drive+0xa2
ata`ata_attach+0x50
genunix`devi_attach+0x75
genunix`attach_node+0xb2
genunix`i_ndi_config_node+0x97
genunix`i_ddi_attachchild+0x4b
genunix`devi_attach_node+0x3d
genunix`devi_config_one+0x1d0
genunix`ndi_devi_config_one+0xb0
devfs`dv_find+0x125
devfs`devfs_lookup+0x40
genunix`fop_lookup+0x21
genunix`lookuppnvp+0x236
genunix`lookuppnat+0xe7
genunix`lookupnameat+0x87
genunix`cstatat_getvp+0x134
value ------------- Distribution ------------- count
2048 | 0
4096 |@@@@@@@@@@@@@@@@@@@@@ 4105
8192 |@@@@ 783
16384 |@@@@@@@@@@@@@@ 2793
32768 | 16
65536 | 0
kb8042`kb8042_wait_poweron+0x29
kb8042`kb8042_init+0x22
kb8042`kb8042_attach+0xd6
genunix`devi_attach+0x75
genunix`attach_node+0xb2
genunix`i_ndi_config_node+0x97
genunix`i_ddi_attachchild+0x4b
genunix`devi_attach_node+0x3d
genunix`devi_config_one+0x1d0
genunix`ndi_devi_config_one+0xb0
genunix`resolve_pathname+0xa5
genunix`ddi_pathname_to_dev_t+0x16
consconfig_dacf`consconfig_load_drivers+0x14
consconfig_dacf`dynamic_console_config+0x6c
consconfig`consconfig+0x8
unix`stubs_common_code+0x3b
value ------------- Distribution ------------- count
262144 | 0
524288 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 221
1048576 |@@@@ 29
2097152 | 0
usba`hubd_enable_all_port_power+0xed
usba`hubd_check_ports+0x8e
usba`usba_hubdi_attach+0x275
usba`usba_hubdi_bind_root_hub+0x168
uhci`uhci_attach+0x191
genunix`devi_attach+0x75
genunix`attach_node+0xb2
genunix`i_ndi_config_node+0x97
genunix`i_ddi_attachchild+0x4b
genunix`i_ddi_attach_node_hierarchy+0x49
genunix`attach_driver_nodes+0x49
genunix`ddi_hold_installed_driver+0xe3
genunix`attach_drivers+0x28
value ------------- Distribution ------------- count
33554432 | 0
67108864 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 3
134217728 | 0
|
Wenn eine Funktion mit dem Aufruf einer anderen Funktion endet, kann der Compiler eine Tail-Call-Optimierung durchführen, die darin besteht, dass die aufgerufene Funktion den Stack-Frame des Aufrufers wieder verwendet. Dieses Verfahren findet in der SPARC-Architektur sehr häufige Anwendung, wo der Compiler das Registerfenster des Aufrufers in der aufgerufenen Funktion wieder verwendet, um den Druck auf die Registerfenster zu minimieren.
Diese Optimierung bewirkt, dass der return-Prüfpunkt der aufrufenden Funktion vor dem entry-Prüfpunkt der aufgerufenen Funktion ausgelöst wird. Diese Reihenfolge kann durchaus Verwirrung schaffen. Wenn Sie beispielsweise alle aus einer bestimmten Funktion aufgerufenen Funktionen und alle von ihr aufgerufenen Funktionen aufzeichnen möchten, könnten Sie das folgende Skript verwenden:
fbt::foo:entry
{
self->traceme = 1;
}
fbt:::entry
/self->traceme/
{
printf("called %s", probefunc);
}
fbt::foo:return
/self->traceme/
{
self->traceme = 0;
}
Wenn jedoch foo() mit einem optimierten Endaufruf (Tail-Call) endet, wird die aus der Endposition rekursiv aufgerufene Funktion einschließlich aller Funktionen, die sie aufruft, nicht erfasst. Der Kernel kann nicht nach Bedarf dynamisch deoptimiert werden, und DTrace soll hier keine falschen Tatsachen über die Codestruktur vortäuschen. Deshalb sollte Ihnen bewusst sein, wann die Tail-Call-Optimierung möglicherweise angewendet wird.
Bei Quellcode in der Art des folgenden Beispiels ist die Verwendung der Tail-Call-Optimierung wahrscheinlich:
return (bar());
Auch in Quellcode wie diesem:
(void) bar(); return;
Umgekehrt kann der Aufruf von bar in Funktionsquellcode, der wie das folgende Beispiel endet, nicht() optimiert werden, da der Aufruf von bar() kein Endaufruf ist:
bar(); return (rval);
Um festzustellen, ob ein Aufruf einer Tail-Call-Optimierung unterzogen wurde, gehen Sie wie folgt vor:
Verfolgen Sie, während Sie DTrace ausführen, arg0 des betreffenden return-Prüfpunkts. arg0 enthält den Versatz der zurückkehrenden Anweisung in der Funktion.
mdb(1) Wenn der verfolgte Versatz einen Aufruf einer anderen Funktion anstatt einer von der Funktion zurückkehrenden Anweisung enthält, wurde der Aufruf Tail-Call-optimiert.
Aufgrund der Befehlssatzarchitektur ist die Tail-Call-Optimierung auf SPARC-Systemen wesentlich verbreiteter als auf x86-Systemen. In diesem Beispiel wird mdb eingesetzt, um die Tail-Call-Optimierung in der Kernelfunktion dup() zu entdecken:
# dtrace -q -n fbt::dup:return'{printf("%s+0x%x", probefunc, arg0);}'
|
Führen Sie, während dieser Befehl läuft, ein Programm aus, das ein dup(2) durchführt, zum Beispiel einen bash-Prozess. Der obige Befehl sollte eine Ausgabe wie in folgendem Beispiel liefern:
dup+0x10 ^C |
Untersuchen Sie die Funktion nun mit mdb:
# echo "dup::dis" | mdb -k dup: sra %o0, 0, %o0 dup+4: mov %o7, %g1 dup+8: clr %o2 dup+0xc: clr %o1 dup+0x10: call -0x1278 <fcntl> dup+0x14: mov %g1, %o7 |
Die Ausgabe zeigt, dass dup+0x10 ein Aufruf der Funktion fcntl() und keine ret-Anweisung ist. Der Aufruf von fcntl() ist also ein Beispiel für eine Tail-Call-Optimierung.
Vielleicht fallen Ihnen Funktionen auf, die zwar einzutreten, aber nie zurückzukehren scheinen oder umgekehrt. Bei diesen seltenen Funktionen handelt es sich um handcodierte Assemblerroutinen, die sich zur Mitte anderer handcodierter Assemblerfunktionen verzweigen. Sie sollten die Analyse nicht behindern: Die Funktion am Verzweigungsziel muss trotzdem zum Aufrufer der Funktion zurückkehren, von der die Verzweigung ausgeht. Das bedeutet, dass Sie bei Aktivierung aller FBT-Prüfpunkte den Eintritt in eine Funktion und die Rückkehr von einer anderen Funktion in derselben Stacktiefe sehen sollten.
Einige Funktionen können nicht mit FBT instrumentiert werden. Die genaue Natur nicht instrumentierbarer Funktionen ist kennzeichnend für die jeweilige Befehlssatzarchitektur.
Funktionen, die auf x86-Systemen keinen Stack-Frame erzeugen, können nicht mit FBT instrumentiert werden. Da der Registersatz für x86 außerordentlich klein ist, müssen die meisten Funktionen Daten auf den Stapel legen und deshalb einen Stack-Frame erzeugen. Einige x86-Funktionen erzeugen jedoch keinen Stack-Frame und können folglich nicht instrumentiert werden. Die tatsächlichen Zahlen variieren von System zu System. Es lässt sich aber feststellen, dass in der Regel weniger als fünf Prozent der Funktionen auf der x86-Plattform nicht instrumentierbar sind.
In Assemblersprache handcodierte Blattroutinen auf SPARC-Systemen können mit FBT nicht instrumentiert werden. Der Großteil des Kernels ist in C geschrieben, und alle in C geschriebenen Funktionen sind durch FBT instrumentierbar.
Die Funktionsweise von FBT beruht auf der dynamischen Änderung von Kerneltext. Da auch Kernel-Haltepunkte auf diese Weise funktionieren, verweigert FBT die Bereitstellung eines Prüfpunkts für eine Funktion, wenn vor dem Laden von DTrace an der Eintritts- oder Rückkehrposition ein Kernel-Haltepunkt gesetzt wurde - selbst dann, wenn der Kernel-Haltepunkt anschließend entfernt wird. Wird der Kernel-Haltepunkt nach dem Laden von DTrace gesetzt, beziehen sich sowohl der Haltepunkt als auch der DTrace-Prüfpunkt auf dieselbe Stelle im Text. In dieser Situation spricht der Haltepunkt zuerst an und der Prüfpunkt wird anschließend ausgelöst, wenn der Debugger den Kernel wieder aufnimmt. Es empfiehlt sich, Kernel-Haltepunkte nicht gleichzeitig mit DTrace zu benutzen. Greifen Sie bei Bedarf stattdessen auf die DTrace-Aktion breakpoint() zurück.
Der Solaris-Kernel kann Kernelmodule dynamisch laden und entladen. Wenn FBT geladen ist und ein Modul dynamisch geladen wird, liefert FBT automatisch neue Prüfpunkte für das neue Modul. Liegen für ein geladenes Modul nicht aktivierte FBT-Prüfpunkte vor, kann das Modul entladen (aus dem Speicher entfernt) werden; die zugehörigen Prüfpunkte werden beim Entladen des Moduls zerstört. Wenn für ein geladenes Modul aktivierte FBT-Prüfpunkte vorliegen, wird das Modul als belegt betrachtet und kann nicht aus dem Speicher entfernt werden.
Der Provider FBT beschreibt die verschiedenen Stabilitäten anhand des DTrace-Stabilitätsmechanismus gemäß der folgenden Tabelle. Weitere Informationen zum Stabilitätsmechanismus finden Sie in Kapitel 39Stabilität.
|
Element |
Namensstabilität |
Datenstabilität |
Abhängigkeitsklasse |
|---|---|---|---|
|
Provider |
Evolving |
Evolving |
ISA |
|
Modul |
Private |
Private |
Unknown |
|
Funktion |
Private |
Private |
Unknown |
|
Name |
Evolving |
Evolving |
ISA |
|
Argumente |
Private |
Private |
ISA |
Wenn FBT die Kernel-Implementierung offen legt, ist nichts daran stabil („Stable“). Die Stabilität von Modul- und Funktionsnamen sowie der Daten ist explizit „Private“. Die Datenstabilität für Provider und Name ist „Evolving“, alle anderen Datenstabilitäten sind „Private“. Sie sind Artefakte der aktuellen Implementierung. Die Abhängigkeitsklasse (dependency class) für FBT lautet ISA: FBT ist zwar auf allen aktuellen Befehlssatzarchitekturen verfügbar, es besteht aber keine Garantie, dass FBT auch in zukünftigen Befehlssatzarchitekturen zur Verfügung stehen wird.