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.
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
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.
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.
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.
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 }
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.
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 }
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 long
s
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.
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!
Michael Schilliarbeitet 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. |