Eingang zur Unterwelt (Linux-Magazin, Februar 2003)

Systemprogrammierung unter Unix in Perl ist ja bekanntlich leicht zu bewerkstelligen, solange man im POSIX-Bereich bleibt. socket(), shmget(), ioctl() und Konsorten sind alles Funktionen, die der Perl-Kern dem Programmierer von Haus aus zur Verfügung stellt. Und das Perl lange schon beiliegende POSIX-Modul bietet auch Exoten wie uname() oder sigprocmask() an.

Für manche Erweiterungen unter Linux gibt's allerdings nur eine C-Schnittstelle -- aber die lässt sich superleicht aus Perl heraus ansprechen. Wie kann man zum Beispiel feststellen, wieviel RAM auf dem gerade genutzten Rechner zur Verfügung steht?

Unter Linux gibt's dafür sysinfo(), eine C-Funktion, die, wie man mit man sysinfo zutage fördert, einen Pointer auf eine sysinfo-Struktur entgegennimmt und diese dann mit allerlei Nützlichem füllt. Abbildung 1 zeigt die Manualseite, die die Struktur sysinfo in ihre Teilfelder aufschlüsselt und die Aufruf- und Rückgabeparameter der Funktion sysinfo() anzeigt:

    #include <sys/sysinfo.h>
    sysinfo(struct sysinfo *info);

Abbildung 1: Die sysinfo-Manualseite mit sysinfo-Struktur

Gibt sysinfo() den Wert 0 zurück, was in C Fehlerfreiheit signalisiert, enthält die Struktur hinter dem Pointer info Informationen darüber, wie lange der Ofen schon läuft, drei Werte für die gegenwärtige Last (als Ganzzahlen von 0 bis 65535, gemittelt über 1, 5, 15 Minuten), wieviel RAM installiert und verfügbar ist, die Anzahl der gegenwärtig laufenden Prozesse und ähnliches mehr.

Um von Perl auf Funktionen von C-Bibliotheken zuzugreifen, nutzt man traditionell die sogenannte XS-Schnittstelle, ein berühmt-berüchtigtes Sprachengemisch, das dazu dient, Perl und C zusammenzuleimen. Ein neues Perl-Modul Sysinfo::RAM mit XS-Anbindung ist schnell mittels

    h2xs -An Sysinfo::RAM

erzeugt. Füllt man das so erzeugte Template RAM.xs mit dem Inhalt nach Listing RAM.xs und tippt die übliche Installationsfolge

    perl Makefile.PL
    make
    make install

dann kann man in einem Perl-Programm ganz lässig

    use Sysinfo::RAM;
    print Sysinfo::RAM::free_ram(), " Bytes freies RAM\n";

schreiben und es funktioniert tatsächlich!

Die Zeilen 1 bis 5 in RAM.xs ziehen Header-Dateien für Perls C-Schnittstelle herein. Zeile 6 holt sys/sysinfo.h, das das später aufgerufene sysinfo() braucht. Zeile 8 legt den Namen des später verwendeten Perl-Moduls und des darin definierten package fest. Die beiden Zuweisungen müssen durch ein TAB getrennt stehen.

Die Zeilen 11 und 12 geben Rückgabewert und Namen der definierten Funktion an. Es ist wichtig, auf die Zeilentrennung zu achten, da Perls XS-Parser kein richtiges C, sondern nur einfache Texterkennung beherrscht.

Nach dem Schlüsselwort CODE: in Zeile 14 folgt der C-Code der später im Perl-Raum ansprechbaren Funktion free_ram(), die lediglich eine sysinfo-Struktur anlegt, einen Pointer darauf an die System-Funktion sysinfo() weitergibt und überprüft, ob 0 oder ein Fehlerwert zurückkommt. Geht der Aufruf gut, gibt die Funktion das Feld freeram der sysinfo-Struktur (ein unsigned long-Wert, wie die sysinfo-Manualseite verrät) zurück. Im Fehlerfall liefert free_ram() 0 zurück. Die Rückgabe erfolgt nicht etwa mittels return() wie allgemein in C üblich, sondern über eine Zuweisung an die XS-Pseudo-Variable RETVAL, die später, nach dem OUTPUT:-Schlüsselwort in Zeile 23, XS den Rückgabewert signalisiert.

Listing 1: RAM.xs

    01 #include "EXTERN.h"
    02 #include "perl.h"
    03 #include "XSUB.h"
    04 
    05 #include "ppport.h"
    06 #include <sys/sysinfo.h>
    07 
    08 MODULE=Sysinfo::RAM	PACKAGE=Sysinfo::RAM		
    09 
    10 ###########################################
    11 unsigned long
    12 free_ram()
    13 ###########################################
    14     CODE:
    15     {   struct sysinfo si;
    16 
    17         if(sysinfo(&si) == 0) {
    18             RETVAL = si.freeram;
    19         } else {
    20             RETVAL = (unsigned long) 0;
    21         }
    22     }
    23     OUTPUT:
    24     RETVAL

Einfacher mit Inline

Dieses XS-Sprachengemisch ist nun nicht jedermanns Sache, und deswegen hat der etwas exzentrische Brian Ingerson (auf jeder Perl-Konferenz leicht an der Anzahl seiner Tatoos zu erkennen) schon vor einiger Zeit Inline.pm auf den Weg gebracht -- richtig, ein Perl-Modul, mit dem man einfach C-Programme in Perl-Skripts einbinden kann. Ruck-zuck installiert es sich mit der CPAN-Shell installiert.

Listing uptime zeigt, wie ein scheinbar hinter dem __END__ des Perl-Programms abgekoppeltes C-Codestück die sysinfo()-Funktion nutzt, um das uptime-Feld der sysinfo-Struktur zu extrahieren und einen long-Wert, die Anzahl der seit dem System-Boot verstrichenen Sekunden, zurückliefert. Im Fehlerfall kommt der long-Wert -1L zurück.

Listing 2: uptime

    01 #!/usr/bin/perl
    02 #####################################################
    03 # uptime - determine Linux' uptime
    04 # Mike Schilli, 2002 (m@perlmeister.com)
    05 #####################################################
    06 use warnings;
    07 use strict;
    08 use Date::Calc qw(Normalize_DHMS);
    09 use Inline "C";
    10 
    11 my $secs = uptime();
    12 
    13 my ($d,$h,$m,$s) = Normalize_DHMS(0, 0, 0, $secs);
    14 
    15 printf "Uptime: $d days $h hours $m mins $s secs\n";
    16 
    17 __END__
    18 __C__
    19 
    20 #include <sys/sysinfo.h>
    21 
    22 long uptime() {
    23    struct sysinfo si;
    24 
    25    if(sysinfo(&si) == 0) {
    26        return si.uptime;
    27    } else {
    28        return -1L;
    29    }
    30 }

Zeile 9 in Listing uptime zieht das Inline-Modul mit dem Parameter "C" herein, um ihm zu signalisieren, dass hinter dem __END__ des Programms noch eine __C__-Sektion steht, die ein C-Programm enthält, dessen kompilierte Version vom Skript genutzt wird. Das Skript selbst muss dann nur in Zeile 11 die Funktion uptime() aufrufen, schon kommt eine Sekundenzahl zurück, die es darauf per Normalize_DHMS() aus dem altbekannten Date::Calc-Modul von Steffen Beyer in Tage, Stunden, Minuten und Sekunden umwandelt. Hexerei? Keineswegs. Das Inline-Module fuhrwerkt allerdings hinter den Kulissen mächtig herum, um beim ersten Aufruf des Skripts nicht nur das C-Programm zu kompilieren, daraus eine Shared-Library zu erstellen und diese ans laufende Programm zu binden. Es legt ein Unterverzeichnis _Inline an, in dem es die ermittelten Ergebnisse speichert. Bei folgenden Aufrufen des Skripts ist keinerlei Neukompiliation erforderlich. Ändert man hingegen den C-Code des Skripts, merkt Inline das anhand einer MD5-Prüfsumme und sorgt sorgt automatisch dafür, dass die shared-lib wieder auf dem neuesten Stand ist, bevor das Programm schließlich ausgeführt wird.

Es kommt eine Liste

Einfache Datentypen als Rückgabewerte wie int, long, unsigned short und so weiter beherrscht Inline.pm im Schlaf. Aber Perl kann bekanntlich, wovon andere Programmiersprachen nur träumen, wie zum Beispiel Wertelisten von variabler Länge zurückgeben.

Perls Funktionen kommunizieren üblicherweise über einen eigenen Perl-Stack miteinander: Parameter werden über ihn hereingereicht und auch wieder zurückgegeben. Mit ein paar Makros aus Inline.pm zeigt Listing sysinfo, wie man sämtliche Felder der Struktur einfach als Liste ans Perlprogramm zurückgibt. Da die aus Inline in den Perl-Raum hineingereichte Funktion nicht sysinfo heißen darf, weil deren Signatur bereits für die gleichnamige Systemfunktion vergeben ist, definiert das C-Snippet in Listing sysinfo die Funktion sysinfo_as_list.

In ihr muss das Makro Inline_Stack_Vars in der Variablendeklarationssektion der C-Funktion stehen, damit es unter der Hand weitere interne Variablen deklarieren kann.

Inline_Stack_Reset initialisiert den Rückgabestack, auf den Inline_Stack_Push() jeweils einen Perl-Skalar hinaufschiebt. Wie entsteht aus einer C-Variablen ein Perl-Skalar? Sofern es sich um einen numerischen Wert handelt (int, long, short, etc.), schafft das newSViv(), das in C einen Pointer auf eine SV-Struktur zurückgibt, Perls interne Repräsentation eines Skalars. Tut man aber nichts weiter, entstehen so Variablen, die der Garbage-Collector nie mehr einsammelt, weswegen ätzende Memory-Leaks entstehen, die ein Programm über Zeit mehr und mehr aufblasen. Deswegen ist es wichtig, ein sv_2mortal() darumzuwickeln, das den entstehenden Skalar 'sterblich' macht -- irgendwann während das C-Interface verlassen und die Kontrolle ans Perlprogramm zurückgegeben wird. Zeile 26 definiert deswegen mit

    #define MO_IV(x) sv_2mortal(newSViv(x))

ein C-Makro als Schreibhilfe, das aus einer numerischen C-Variablen (Platzhalter x) mittels MO_IV(variable) einen sterblichen Skalar macht.

Sind alle Inline_Stack_Push()-Vorgänge abgeschlossen, signalisiert Inline_Stack_Done, dass der Stack genug gewachsen und bereit zur Rückgabe in die Perl-Welt ist. Das Perl-Skript gibt demensprechend etwas wie

    Uptime: 14955 secs
    Load: 11424, 3808, 672
    71 processes running

aus.

Im Fehlerfall bricht Zeile 30 die C-Funktion ab, und gibt einen leeren Stack zurück, worauf im Perl-Programm alle Listenwerte auf der linken Seite undefiniert sind. Zeile 15 fängt diesen Fall ab.

Wichtig ist es, den Rückgabetyp der im C-Code definierten Funktion in Zeile 28 mit void anzugeben, damit Inline weiß, dass der C-Code die Stackgestaltung selbst übernimmt. Tut man das nicht, sondern definiert sie beispielsweise mit int, wirbelt Inline leider ohne Warnung den Stack durcheinander.

Listing 3: sysinfo.pl

    01 #!/usr/bin/perl
    02 ###########################################
    03 # sysinfo - Show Linux System Statistics
    04 # Mike Schilli, 2002 (m@perlmeister.com)
    05 ###########################################
    06 use warnings;
    07 use strict;
    08 use Inline "C";
    09 
    10 my ($uptime, $load1, $load5, $load15, 
    11     $totalram, $freeram, $sharedram, 
    12     $bufferram, $totalswap, $freeswap, 
    13     $procs) = sysinfo_as_list();
    14 
    15 die "sysinfo failed" unless 
    16                      defined $uptime;
    17 
    18 print "Uptime: $uptime secs\n";
    19 print "Load: $load1, $load5, $load15\n";
    20 print "Ram: $totalram, $freeram, $sharedram\n";
    21 print "$procs processes running\n";
    22 
    23 __END__
    24 __C__
    25 
    26 #include <sys/sysinfo.h>
    27 #define MO_IV(x) sv_2mortal(newSViv(x))
    28 
    29 void sysinfo_as_list() {
    30    Inline_Stack_Vars;
    31    struct sysinfo si;
    32 
    33    if(sysinfo (&si)) {
    34        return;
    35    }
    36 
    37    Inline_Stack_Reset;
    38    Inline_Stack_Push(MO_IV(si.uptime));
    39    Inline_Stack_Push(MO_IV(si.loads[0]));
    40    Inline_Stack_Push(MO_IV(si.loads[1]));
    41    Inline_Stack_Push(MO_IV(si.loads[2]));
    42    Inline_Stack_Push(MO_IV(si.totalram));
    43    Inline_Stack_Push(MO_IV(si.freeram));
    44    Inline_Stack_Push(MO_IV(si.sharedram));
    45    Inline_Stack_Push(MO_IV(si.bufferram));
    46    Inline_Stack_Push(MO_IV(si.totalswap));
    47    Inline_Stack_Push(MO_IV(si.freeswap));
    48    Inline_Stack_Push(MO_IV(si.procs));
    49    Inline_Stack_Done;
    50 }

Es kommt eine Hashreferenz

Eine weitere Möglichkeit, mit einer C-Funktion zu kommunizieren, die eine Struktur zurückgibt, ist eine Schnittstelle, die eine Referenz auf einen Hash zurückgibt, dessen Schlüssel die Feldnamen der Struktur angeben und dessen Werte den Feldwerten entsprechen.

Listing sysinfo_hash zeigt die Implementierung. Zeile 27 erzeugt mit newHV() einen neuen leeren Hash und macht ihn mit sv_2mortal() sterblich. Falls der sysinfo-Aufruf in Zeile 29 einen Fehler zurückliefert, gibt Zeile 30 über einen Pointer auf die vordefinierte Variable PL_sv_undef den Wert undef in die Perl-Welt zurück.

Falls alles gut geht, pumpen die hv_store()-Aufrufe ab Zeile 33 neue Schlüssel-Wertepaare in den Hash. Auf jeden Schlüsselstring folgt dessen Länge und ein neuer Skalar und der Wert 0 , damit Perl den internen Hash-Wert des Eintrags selbst ausrechnet. Die Werte des Hashs dürfen nicht mit sv_2mortal() behandelt werden, da sie auch in der Perl-Welt noch weiterexistieren müssen. Der Garbage-Collector wird sie aber automatisch wegputzen sobald der Hash stirbt und nicht noch andere Referenzen auf die Einträge zeigen.

Listing 4: sysinfo_hash

    01 #!/usr/bin/perl
    02 ###########################################
    03 # sysinfo - Show Linux System Statistics
    04 # Mike Schilli, 2002 (m@perlmeister.com)
    05 ###########################################
    06 use warnings;
    07 use strict;
    08 use Inline "C";
    09 
    10 my $h = sysinfo_as_hashref();
    11 
    12 print "Uptime: $h->{uptime} secs\n";
    13 print "Load: $h->{load1}, $h->{load5}, " .
    14       "$h->{load15}\n";
    15 print "$h->{procs} processes running\n";
    16 
    17 __END__
    18 __C__
    19 
    20 #include <sys/sysinfo.h>
    21 
    22 HV *sysinfo_as_hashref() {
    23     Inline_Stack_Vars;
    24     struct sysinfo si;
    25     HV *hash;
    26 
    27     hash = (HV*) sv_2mortal((SV*)newHV());
    28 
    29     if(sysinfo (&si)) {
    30        return &PL_sv_undef;
    31     }
    32 
    33     hv_store(hash, "uptime", 6, 
    34              newSViv(si.uptime), 0);
    35     hv_store(hash, "load1",  5, 
    36              newSViv(si.loads[0]), 0);
    37     hv_store(hash, "load5",  5, 
    38              newSViv(si.loads[1]), 0);
    39     hv_store(hash, "load15", 6, 
    40              newSViv(si.loads[2]), 0);
    41     hv_store(hash, "procs",  5, 
    42              newSViv(si.procs), 0);
    43 
    44     return hash;    
    45 }

Eingabeparameter

Auch ans C-Interface übergebene Parameter erfasst Inline automatisch, sofern es sich um C-Standardtypen handelt, deren Zuordnungen zu entsprechenden Perl-Typen in der Perl beiliegenden sogenannten typemap-Datei aufgeführt sind.

In Listing timer zapfen wir die Linux-Systemfunktion setitimer an, die dafür sorgt, dass der laufende Prozess nach einem bestimmten Zeitraum und dann in ebenfalls einstellbaren Abständen das Signal SIGVTALRM erhält. Dies fängt das Skript dann in einem Perl-Signalhandler ab und ermöglicht es somit, den Prozess selbst dann noch zu kontrollieren, falls er sich in einer Endlosschleife festgefressen hat.

Wie die Manualseite verrät, hört setitimer auf die Signatur

    int setitimer( 
        int which, 
        const struct itimerval *value,
        struct itimerval *ovalue);

Für which wählen wir den in sys/time.h festgelegten Wert ITIMER_VIRTUAL, um nur die vom Prozess selbst verbratene Zeit zu zählen. Der zweite Parameter value ist eine Struktur, die zwei Einträge enthält: it_interval und it_value, beide sind Strukturen vom Typ timeval, die jeweils Sekunden- und Mikrosekundenwerte als longs enthalten. it_interval gibt an, nach welcher Zeit der Timer das erste Mal das Signal schickt und it_value, in welchen Zeitabständen das regelmäßig nach dem ersten Mal erfolgen soll.

Der dritte Parameter von setitimer(), ovalue, dient zum Auslesen des bisher festgesetzten Werts und soll hier nicht interessieren, er wird im Listing einfach auf NULL gesetzt.

Die über Inline von C nach Perl importierte Funktion interval_set() erwartet der Einfachheit halber keinen Strukturenwirrwarr, sondern schlichtweg vier long-Werte: Ein Sekunden-Mikrosekunden-Paar für das erste Intervall und ein weiteres Paar für die folgenden Intervalle. Der Aufruf

    interval_set(10, 0, 0, 250000);

lässt den Prozess also zunächst 10 Sekunden und 0 Mikrosekunden orgeln, bevor das erste Signal kommt, und sendet ab dann jede Viertelsekunde ein neues.

Listing 5: timer

    01 #!/usr/bin/perl
    02 ###########################################
    03 # Interrupt a busy process in intervals
    04 # Mike Schilli, 2002 (m@perlmeister.com)
    05 ###########################################
    06 use warnings;
    07 use strict;
    08 
    09 use Inline "C";
    10 
    11 $SIG{VTALRM} = sub {
    12     print "Just checking!\n";
    13 };
    14 
    15 interval_set(10, 0, 0, 250000);
    16 
    17 while(1) {
    18     # Crunch, crunch!
    19 }
    20 
    21 __END__
    22 __C__
    23 
    24 #include <sys/time.h> 
    25 
    26 /* ##################################### */
    27 void interval_set( 
    28     long secs, long usecs, 
    29     long isecs, long iusecs
    30 ){
    31 /* ##################################### */
    32     struct itimerval timer; 
    33 
    34     timer.it_value.tv_sec     = secs;
    35     timer.it_value.tv_usec    = usecs;
    36     timer.it_interval.tv_sec  = isecs;
    37     timer.it_interval.tv_usec = iusecs;
    38 
    39     setitimer (ITIMER_VIRTUAL, 
    40                &timer, NULL); 
    41 }

Der ab Zeile 11 definierte Signalhandler fängt die Signale ab (und verhindert damit den sofortigen Programmabbruch) und gibt nur eine kurze Meldung aus.

Die ab Zeile 17 startende Endlosschleife lässt die CPU heulend Kreise drehen, verhindert aber nicht, dass regelmäßig der Signalhändler angesprungen wird.

setitimer und andere exotische System-Calls sind übrigens schön in [5] beschrieben.

Mit XS oder Inline und auch mit dem hier nicht besprochenen Werkzeug SWIG lassen sich selbst die kompliziertesten Perl-C-Anbindungen bewerkstelligen. Strukturen, Arrays, Objekte, sogar C++-Bibliotheken -- alles lässt sich von Perl aus ansprechen, alles kein Problem. Das nicht ganz einfach zu lesende Standardwerk [2] zeigt auch Lösungen zu den verzwicktesten Problemen und [3] bietet eine knappe aber nützliche Einführung in die Thematik. Die Manualseiten [4] gibt Einblick in die C-Datenstrukturen, die man kennen muss, wenn man zwischen den Welten wandert. Nur keine Angst!

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2001/09/Perl

[2]
``Extending and Embedding Perl'', Tim Jenness, Simon Cozens, Manning, 2002.

[3]
``Writing Perl Modules for CPAN'', Sam Tregar, Apress, 2002.

[4]
Perl-Manualseiten: Inline::C-Cookbook, perlapi, perlguts, perlintern, perlxs, perlxstut, Dokumentation der Perl-C-Schnittstelle

[5]
``Advanced Linux Programming'', Mark Mitchell, Jeffrey Oldham, Alex Samuel, New Rider, 2002, http://perlmeister.com/cgi/amz/0735710430

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.