ProveVue: Praktische Einführung II

Im zweiten Teil der praktischen Einführung von ProbeVue soll die Verwendung des Intervall Proben Managers und der speziellen BEGIN Klausel (clause) vorgestellt werden. Zur Illustrierung soll als durchgängiges Beispiel ein kleines Monitoring Tool entwickelt werden, der Prozeß Aktivitäts Reporter (Vue Skript par.e). Dieser soll für einen ausgewählten Prozeß in 1 Sekunden Intervallen die Anzahl der read und write System-Calls und der dabei übertragenen Daten in KB/s ausgeben.

Der Intervall Proben Manager

Mit Hilfe einer Probe des Proben Managers interval, kann in regelmäßigen Abständen ein beliebiger Aktionsblock ausgeführt werden. Ein Proben Punkt des Intervall Managers hat dabei die folgende Form:

interval:*:clock:<number of milli seconds>

Wir wählen ein 1000 ms (1 Sekunde) Intervall und lassen zum Start erst einmal jede Sekunde eine Meldung ausgeben:

#  cat par.e
#! /usr/bin/probevue

/* Description: A process activity reporter */

@@interval:*:clock:1000
{
        printf("clock tick\n");
}

#  chmod +x par.e
#

Wir starten das Vue-Skript par.e und überprüfen die Funktion:

# ./par.e
clock tick
clock tick
clock tick
clock tick
^C
#

Das sieht schon mal ganz gut aus, aber ist der Abstand zwischen den Meldungen auch wirklich jeweils 1 Sekunde? ProbeVue bietet über die Funktion timestamp() die Möglichkeit an, auf die aktuelle Zeit zuzugreifen und über die Funktion printf() mit dem Konversions Bezeichner „%A“ auszugeben:

# cat par.e
#! /usr/bin/probevue

/* Description: A process activity reporter */

@@interval:*:clock:1000
{
        printf("%A clock tick\n",timestamp());
}

#

Ein kurzer Test des Skripts zeigt, dass das Intervall wirklich 1 Sekunde beträgt:

# ./par.e
Mar/20/21 08:35:16 clock tick
Mar/20/21 08:35:17 clock tick
Mar/20/21 08:35:18 clock tick
Mar/20/21 08:35:19 clock tick
Mar/20/21 08:35:20 clock tick
^C
#

Globale Variablen

Als nächstes wollen wir die Anzahl der read System-Calls eines Prozesses mitzählen. Hierzu verwenden wir, wie im ersten Teil, wieder den Proben Manager syscall. Die PID des zu überwachenden Prozesses soll über ein Argument beim Aufruf des Skriptes übergeben werden. Der zu verwendende Proben Punkt wäre dann:

@@syscall:$1:read:entry

Wobei „$1“ wieder für das erste übergebene Argument des Skripts (PID) steht, und im dritten Feld der read System-Call ausgewählt wurde.

Die Anzahl der read System-Calls speichern wir in einer globalen Variablen, die wir nreads nennen. Der Typ der Variablen ist int, das erlaubt das Speichern von ganzzahligen Werten. Die Variable wird automatisch mit 0 initialisiert. Als weitere Anweisung im Aktionsblock erhöhen wir die Variable nreads um 1 (++nreads). Die printf() Anweisung im Aktionsblock der Intervall-Probe erweitern wir um die Ausgabe der Integer-Variablen nreads. Insgesamt ergibt sich dann das folgende Skript:

# cat par.e
#! /usr/bin/probevue

/* Description: A process activity reporter */

@@interval:*:clock:1000
{
        printf("%A %d\n",timestamp(),nreads);
}

@@syscall:$1:read:entry
{
        int nreads;
        ++nreads;
}

#

Jedes Mal, wenn der angegebene Prozeß einen read System-Call startet, wird der Wert der globalen Variablen nreads um 1 erhöht. Nach Ablauf einer Sekunde wird der aktuelle Zeitstempel zusammen mit der Anzahl der ausgeführten read System-Calls seit Starten des Vue-Skripts ausgegeben:

# ./par.e 10682852
Mar/20/21 08:53:30 0
Mar/20/21 08:53:31 779
Mar/20/21 08:53:32 779
Mar/20/21 08:53:33 779
Mar/20/21 08:53:34 779
Mar/20/21 08:53:35 779
Mar/20/21 08:53:36 779
Mar/20/21 08:53:37 779
Mar/20/21 08:53:38 884
Mar/20/21 08:53:39 1662
Mar/20/21 08:53:40 1662
Mar/20/21 08:53:41 1662
Mar/20/21 08:53:42 1662
Mar/20/21 08:53:43 1662
Mar/20/21 08:53:44 1767
Mar/20/21 08:53:45 1767
^C
#

Die Deklaration der globalen Variablen nreads (Zeile: „int nreads;„) kann auch außerhalb des Aktionsblocks, vor allen Klauseln, erfolgen. Es ist dann eventuell besser zu erkennen, das diese Variable global ist, und an jeder beliebigen Stelle benutzt werden kann.

Anstelle von absoluten Werten möchten wir gerne die Anzahl der Aufrufe im letzten Zeitintervall ausgeben. Dazu benötigt man eine zweite globale Variable (wir verwenden oreads), in welcher der Stand am Anfang des Intervalls festgehalten wird. Durch Differenz-Bildung (nreads – oreads) bekommt man dann die Anzahl der Aufrufe während des letzten Zeitintervalls:

# cat par.e
#! /usr/bin/probevue

/* Description: A process activity reporter  */

int nreads;
int oreads;

@@interval:*:clock:1000
{
        printf("%A %d\n",timestamp(),nreads-oreads);
        oreads = nreads;
}

@@syscall:$1:read:entry
{
        ++nreads;
}

#

Ein Probelauf des Vue-Skriptes zeigt, das es die gewünschte Ausgabe liefert:

# ./par.e 10682852
Mar/20/21 09:47:29 0ll
Mar/20/21 09:47:30 0
Mar/20/21 09:47:31 0
Mar/20/21 09:47:32 779
Mar/20/21 09:47:33 105
Mar/20/21 09:47:34 778
Mar/20/21 09:47:35 0
Mar/20/21 09:47:36 0
Mar/20/21 09:47:37 105
Mar/20/21 09:47:38 778
Mar/20/21 09:47:39 0
Mar/20/21 09:47:40 105
^C
#

Ermitteln des Lese-Durchsatzes

Bei jedem read System-Call muß die Anzahl der zu lesenden Daten angegeben werden. Der Aufruf von read sieht so aus („man 2 read„):

ssize_t  read  (FileDescriptor,  Buffer,  NBytes)
       int FileDescriptor;
       void  * Buffer;
       size_t NBytes;

Das dritte Argument ist die Anzahl der zu lesenden Bytes. Vom ersten Teil des Artikels wissen wir schon, das man den Aufruf eines System-Calls mit Argumenten in Form eines Funktions-Prototyp deklarieren muss, wenn man auf die Argumente im Vue-Skript zugreifen möchte. Wir übernehmen dementsprechend die folgende Deklaration für den read System-Call in unser Skript:

ssize_t read( int FileDescriptor , void* Buffer , size_t NBytes );

Auf das dritte Argument kann man dann im Skript über die spezielle Builtin-Variable __arg3 zugreifen. Für die gelesenen Bytes führen wir die beiden globalen Variablen nreadBytes und oreadBytes ein, analog zu den beiden bisherigen Variablen. Im Aktionsblock der syscall Probe verwenden wir __arg3 um die Anzahl zu lesenden Bytes hochzuzählen. Und im Aktionblock für die Intervall-Probe geben wir die gelesenen Bytes im Intervall aus:

# cat par.e
#! /usr/bin/probevue

/* Description: A process activity reporter  */

ssize_t read( int FileDescriptor , void* Buffer , size_t NBytes );

int nreads;
int oreads;
int nreadBytes;
int oreadBytes;

@@interval:*:clock:1000
{
        printf("%A %d %d\n",timestamp(),nreads-oreads,nreadBytes-oreadBytes);
        oreads = nreads;
        oreadBytes = nreadBytes;
}

@@syscall:$1:read:entry
{
        ++nreads;
        nreadBytes += __arg3;
}

#

Leider lässt sich das Skript jetzt nicht mehr starten, es kommt sofort eine Fehlermeldung:

# ./par.e 10682852
./par.e: 8: unsupported operator in typecast: ,
#

Die Zeile 8 enthält die Deklaration des read System-Calls. Das Problem ist, das ProbeVue die benutzerdefinierten Datentypen ssize_t und size_t nicht kennt. Der Typ ssize_t ist letztlich der Datentyp signed long, und der Typ size_t der Datentyp unsigned long. Letztere Datentypen sind ProbeVue bekannt. Wir ersetzten daher in der Deklaration von read die beiden benutzerdefinierten Datentypen ssize_t und size_t durch die Datentypen signed long bzw. unsigned long.

# cat par.e
#! /usr/bin/probevue

/* Description: A process activity reporter  */

signed long read( int FileDescriptor , void* Buffer , unsigned long NBytes );

int nreads;
int oreads;
int nreadBytes;
int oreadBytes;

@@interval:*:clock:1000
{
        printf("%A %d %d\n",timestamp(),nreads-oreads,nreadBytes-oreadBytes);
        oreads = nreads;
        oreadBytes = nreadBytes;
}

@@syscall:$1:read:entry
{
        ++nreads;
        nreadBytes += __arg3;
}

#

Jetzt lässt sich das Vue-Skript auch wieder starten und produziert die gewünschte Ausgabe:

# ./par.e 10682852
Mar/20/21 10:26:58 0 0
Mar/20/21 10:26:59 0 0
Mar/20/21 10:27:00 0 0
Mar/20/21 10:27:01 0 0
Mar/20/21 10:27:02 779 2500602
Mar/20/21 10:27:03 0 0
Mar/20/21 10:27:04 105 85632
Mar/20/21 10:27:05 778 2496506
Mar/20/21 10:27:06 105 85632
Mar/20/21 10:27:07 778 2496506
Mar/20/21 10:27:08 105 85632
Mar/20/21 10:27:09 0 0
Mar/20/21 10:27:10 0 0
Mar/20/21 10:27:11 0 0
^C
#

Da wir auch an den write System-Calls und dem zugehörigen Durchsatz interessiert sind, erweitern wir unser Skript und überwachen auch die write System-Calls. Der write System-Call hat genauso wie der read System-Call 3 Argumente, wobei das dritte Argument die Anzahl der zu schreibenden Bytes ist. Wir können also im wesentlichen den Code von read übernehmen und einfach read durch write ersetzen:

# cat par.e
#! /usr/bin/probevue

/* Description: A process activity reporter */

signed long read( int FileDescriptor , void* Buffer , unsigned long NBytes );
signed long write( int FileDescriptor , const void* Buffer , unsigned long NBytes );

int nreads;
int oreads;
int nreadBytes;
int oreadBytes;
int nwrites;
int owrites;
int nwriteBytes;
int owriteBytes;

@@interval:*:clock:1000
{
        printf("%A %d %d %d %d\n",timestamp(),nreads-oreads,nreadBytes-oreadBytes,nwrites-owrites,nwriteBytes-owriteBytes);
        oreads = nreads;
        oreadBytes = nreadBytes;
        owrites = nwrites;
        owriteBytes = nwriteBytes;
}

@@syscall:$1:read:entry
{
        ++nreads;
        nreadBytes += __arg3;
}

@@syscall:$1:write:entry
{
        ++nwrites;
        nwriteBytes += __arg3;
}

#

Das Skript funktioniert auf Anhieb und liefert jetzt sowohl die Anzahl der Lese- und Schreib-Operationen eines Prozesses, als auch den entsprechenden Durchsatz:

# ./par.e 10682852
Mar/20/21 10:33:59 0 0 0 0
Mar/20/21 10:34:00 0 0 0 0
Mar/20/21 10:34:01 0 0 0 0
Mar/20/21 10:34:02 779 2500602 94 3530
Mar/20/21 10:34:03 0 0 0 0
Mar/20/21 10:34:04 0 0 0 0
Mar/20/21 10:34:05 105 85632 6 1966
Mar/20/21 10:34:06 778 2496506 94 3530
Mar/20/21 10:34:07 105 85632 6 1966
Mar/20/21 10:34:08 778 2496506 94 3530
Mar/20/21 10:34:09 105 85632 6 1966
Mar/20/21 10:34:10 0 0 0 0
Mar/20/21 10:34:11 0 0 0 0
^C
#

Formatierte Ausgabe

Die Ausgabe der einzelnen Spalten verschiebt sich von Zeile zu Zeile, was die Lesbarkeit der Ausgabe verschlechtert. Die Funktion printf() bietet die Möglichkeit die Breite einer Ausgabe vorzugeben, dazu wird im Format-String jeweils hinter dem Zeichen „%“ die gewünschte Anzahl von Zeichen angegeben. Ist eine Ausgabe kürzer, wird einfach mit Leerzeichen aufgefüllt. Für die Anzahl der read bzw. write System-Calls sehen wir erst einmal maximal 6 stellige Ausgaben vor, bei den Werten zum Durchsatz möchten wir eine Ausgabe in KB/s haben, wobei wir hier bis zu 7 Stellen vorsehen. Um auf KB zu kommen, müssen wir durch 1024 dividieren. Da bei der Integer-Division nicht gerundet wird, runden wir selber, indem wir im Zähler 512 addieren:

# cat par.e
#! /usr/bin/probevue

/* Description: A process activity reporter  */

signed long read( int FileDescriptor , void* Buffer , unsigned long NBytes );
signed long write( int FileDescriptor , const void* Buffer , unsigned long NBytes );

int nreads;
int oreads;
int nreadBytes;
int oreadBytes;
int nwrites;
int owrites;
int nwriteBytes;
int owriteBytes;

@@interval:*:clock:1000
{
        printf("%A %6d %7d KB/s %6d %7d KB/s\n",timestamp(),nreads-oreads,(nreadBytes-oreadBytes+512)/1024,nwrites-owrites,(nwriteBytes-owriteBytes+512)/1024);
        oreads = nreads;
        oreadBytes = nreadBytes;
        owrites = nwrites;
        owriteBytes = nwriteBytes;
}

@@syscall:$1:read:entry
{
        ++nreads;
        nreadBytes += __arg3;
}

@@syscall:$1:write:entry
{
        ++nwrites;
        nwriteBytes += __arg3;
}

#

Ein Test der neuen Skript Version zeigt, dass die Ausgabe der Spalten jetzt korrekt untereinander erfolgt, was deutlich besser lesbar ist:

# ./par.e 10682852
Mar/20/21 11:47:19      0       0 KB/s      0       0 KB/s
Mar/20/21 11:47:20      0       0 KB/s      0       0 KB/s
Mar/20/21 11:47:21    778    2438 KB/s     94       3 KB/s
Mar/20/21 11:47:22    105      84 KB/s      6       2 KB/s
Mar/20/21 11:47:23    779    2442 KB/s     94       3 KB/s
Mar/20/21 11:47:24    119      97 KB/s      4       2 KB/s
Mar/20/21 11:47:25    778    2438 KB/s     94       3 KB/s
Mar/20/21 11:47:26     73      31 KB/s     18       6 KB/s
^C
#

BEGIN Block

Schön wäre jetzt noch eine Überschrift über den einzelnen Spalten, damit auch klar ist, welche Werte in welcher Spalte ausgegeben werden. Die Überschrift müsste einmalig, zu Beginn des Skripts ausgegeben werden, bevor einer der Aktionsblöcke ausgeführt wird. ProbeVue bietet die Möglichkeit einen sogenannten BEGIN-Block zu definieren. Dazu wird als Pseudo Proben-Typ „@@BEGIN“ verwendet. Der folgende Aktionsblock wird dann einmalig zu Beginn des Skript-Laufs ausgeführt. Wir benutzen einen solchen BEGIN-Block, um mittels printf() eine Überschrift für die Spalten auszugeben:

# cat par.e
#! /usr/bin/probevue

/* Description: A process activity reporter  */

signed long read( int FileDescriptor , void* Buffer , unsigned long NBytes );
signed long write( int FileDescriptor , const void* Buffer , unsigned long NBytes );

int nreads;
int oreads;
int nreadBytes;
int oreadBytes;
int nwrites;
int owrites;
int nwriteBytes;
int owriteBytes;

@@BEGIN
{
        printf("%18s %6s %7s      %6s %7s\n","TIMESTAMP","#READ","READ/s","#WRITE","WRITE/s");
}

@@interval:*:clock:1000
{
        printf("%A %6d %7d KB/s %6d %7d KB/s\n",timestamp(),nreads-oreads,(nreadBytes-oreadBytes+512)/1024,nwrites-owrites,(nwriteBytes-owriteBytes+512)/1024);
        oreads = nreads;
        oreadBytes = nreadBytes;
        owrites = nwrites;
        owriteBytes = nwriteBytes;
}

@@syscall:$1:read:entry
{
        ++nreads;
        nreadBytes += __arg3;
}

@@syscall:$1:write:entry
{
        ++nwrites;
        nwriteBytes += __arg3;
}

#

Damit haben jetzt alle Spalten eine Überschrift:

# ./par.e 2568903
         TIMESTAMP  #READ  READ/s      #WRITE WRITE/s
Mar/20/21 13:02:46      1      16 KB/s   5914  378432 KB/s
Mar/20/21 13:02:47     13      17 KB/s   5702  364160 KB/s
Mar/20/21 13:02:48     17     256 KB/s   3048  190977 KB/s
Mar/20/21 13:02:49      2      16 KB/s      1       0 KB/s
Mar/20/21 13:02:50     12      39 KB/s   4863  304977 KB/s
Mar/20/21 13:02:51      4      16 KB/s  10671  682816 KB/s
Mar/20/21 13:02:52      1      16 KB/s  11470 -3460287 KB/s
Mar/20/21 13:02:53     22     241 KB/s   5931  375361 KB/s
^C
#

Einige Verbesserungen

Schaut man sich die letzte Ausgabe genauer an, speziell die Zeile von 13:02:52, dann sieht man das dort ein negativer Wert auftaucht. Das ist sehr unschön, und der Wert ist auch mit Sicherheit nicht korrekt! Die Ursache liegt darin begründet das wir den Datentyp int (32-bit) für die Anzahl gelesener bzw. geschriebener Bytes genommen haben. Offensichtlich ist es hier zu einem Überlauf gekommen! Der Datentyp für die Anzahl Bytes im read und write System-Call war unsigned long (64-bit). Wir ändern daher den Datentyp von int auf unsigned long ab. Bei der Ausgabe mit printf() müssen wir aber dann darauf achten den Format-Bezeichner von „%7d“ (32-bit Integer) auf „%7lld“ (64-bit Integer) abzuändern:

#  cat par.e
#! /usr/bin/probevue

/* Description: A process activity reporter */

signed long read( int FileDescriptor , void* Buffer , unsigned long NBytes );
signed long write( int FileDescriptor , const void* Buffer , unsigned long NBytes );

int nreads;
int oreads;
unsigned long nreadBytes;
unsigned long oreadBytes;
int nwrites;
int owrites;
unsigned long nwriteBytes;
unsigned long owriteBytes;

@@BEGIN
{
        printf("%18s %6s %7s      %6s %7s\n","TIMESTAMP","#READ","READ/s","#WRITE","WRITE/s");
}

@@interval:*:clock:1000
{
        printf("%A %6d %7lld KB/s %6d %7lld KB/s\n",timestamp(),nreads-oreads,(nreadBytes-oreadBytes+512)/1024,nwrites-owrites,(nwriteBytes-owriteBytes+512)/1024);
        oreads = nreads;
        oreadBytes = nreadBytes;
        owrites = nwrites;
        owriteBytes = nwriteBytes;
}

@@syscall:$1:read:entry
{
        ++nreads;
        nreadBytes += __arg3;
}

@@syscall:$1:write:entry
{
        ++nwrites;
        nwriteBytes += __arg3;
}

#

Nach dieser Korrektur tritt der Überlauf nicht mehr (bzw. erst nach ca. 16 Millionen TB) auf.

Mit dem vorgestellten Vue-Skript lässt sich das I/O-Verhalten von einzelnen Prozessen ganz gut beobachten. Natürlich sind noch beliebige weitere Verbesserungen möglich. Dies sei aber dem Leser überlassen.

Weitere Artikel zum Thema ProbeVue

ProbeVue: Praktische Einführung

ProbeVue in Action: Identifizieren eines Abstürzenden Prozesses

ProbeVue in Action: Überwachen der „Queue Depth“ von Platten