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.