Handbuch zur dynamischen Ablaufverfolgung in Solaris

Kapitel 20 Der Provider fbt

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.

Prüfpunkte

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.

Prüfpunktargumente

entry-Prüfpunkte

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.

return-Prüfpunkte

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.

Beispiele

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

Tail-Call-Optimierung

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:

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.

Assemblerfunktionen

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.

Beschränkungen des Befehlssatzes

Einige Funktionen können nicht mit FBT instrumentiert werden. Die genaue Natur nicht instrumentierbarer Funktionen ist kennzeichnend für die jeweilige Befehlssatzarchitektur.

Beschränkungen bei x86-Systemen

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.

Beschränkungen bei SPARC-Systemen

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.

Interaktion mit Haltepunkten

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.

Laden von Modulen

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.

Stabilität

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.