=head1 GOOFED - Generic Object Oriented Flexible Extensible Daemon =head2 Abstract Perl ist eine tolle Sprache. Leider hat sie auch viele Abhängigkeiten. Besonders bei Embedding muss man im Allgemeinen eine ganze Perl-Installation "mitschleppen". Bei einem bestimmten Projekt musste ich ohne externe Dateien oder andere Abhängigkeiten auskommen. Um trotzdem Perl einsetzen zu können, musste alles in das Binary wandern. Die Probleme und Lösungen, die ich fand, sind im Folgenden beschrieben. =head2 Wozu, weshalb und warum? Das Projekt, für das Goofed entstand, bestand darin, den Radius-Server eines großen Internet-Providers in einigen Funktionen zu ersetzen. Da die Anforderungen durch viele Jahre hinweg sehr speziell wurden (ein typisches "I setzen den Standard"-Phänomen) und die alte Codebasis neuen Anforderungen nicht mehr gut gewachsen war, musste ein neuer Daemon her. =for latex \begin{sloppypar} Recht früh kam die Idee, im Kern für die komplizierten Regeln eine High-Level-Skriptsprache zu verwenden und nur für das eigentliche Radius-Paket-Management C zu verwenden. =for latex \end{sloppypar} Perl schien zuerst an vielen Gründen zu scheitern: kompliziertes Build-System, viele Abhängigkeiten und Dateien, keine gute Kontrolle über den Speicherbedarf (Leaks wären tödlich für einen Daemon, der meherre Monate durchhalten muss) und Last-not-Least stand zu befürchten, daß die Server-Abteilung die Lösung sofort ablehnen würde, sobald bekannt würde, daß sie Perl benutzt (typisches "Real-World - macht keinen Sinn"-Phänomen). =head3 Eckpunkte der Implementierung Trotz dieser Gegenanzeigen versprach Perl auch wichtige Vorteile, der wichtigste war das Vorhandensein einer grossen Codebasis und viele Module. Das und Perl selbst führen meist sehr schnell zu verwertbaren Ergebnissen. Um die Anforderungen zu erfüllen, habe ich folgende Entscheidungen gefällt: =over 4 =item Einzelnes Binary Bis auf die Konfigurationsdateien sollte alles - Perl-Interpreter, Module, Bibliotheksdateien - in einer einzigen Datei sein und keine externen Dateien benötigen oder anlegen. Dies schließt leider das (ansonsten hervorragende) PAR-Modul aus, da dieses alle Bibliotheksdateien zwar in eine Datei packt, zur Laufzeit diese aber lediglich entpackt und entsprechend viele Dateien schreibt. =item Festgelegte Perl-Version Die Perl-Version sollte hartverdrahtet werden. Der Grund liegt in vielen, kleinen Änderungen auch innerhalb stabiler Release-Zyklen, die zu Build-Problemen oder Fehlern zur Laufzeit führen können. I war hier ausschlaggebend. Ausserdem kann man, sobald man die Kontrolle über die Perl-Quellen hat, bedenkenlos Anpassungen vornehmen (ich nenne das die "Lizenz zum Rumsauen"-Regel). Es sollte möglich sein, den Perl-Kern upzugraden, wobei aber klar sein sollte, das dies Veränderungen am Quellcode nach sich zieht. =item Festgelegte Module und festgelegte Versionen Ähnliches gilt für Module: es sollte möglich sein, fast beliebige Module einzubauen, aber auch diese sollten in einer festen Version kommen. Perl-Module auf CPAN haben - seien wir ehrlich - fast immer kleine und große Bugs. Meistens kann man mit ihnen leben oder Workarounds finden - häufig muss man dazu auf Interna zugreifen, die sich von Version zu Version ändern. =item "Normales" Build-System, kein Configure Configure nichtinteraktiv auszuführen ist kompliziert, und sehr viel kann schiefgehen. Ein einfaches Makefile fühlt sich wesentlich stabiler und einfacher wartbar an. =back Da der Daemon sehr viele Anfragen erhält und in anderer Form weiterleitet, wäre es schön, wenn man die Bearbeitung der Anfragen verzahnen könnte. Dazu braucht man ein vernünftiges Event-System (der Name sagts schon - das Event-Modul) und etwas, mit dem man effizient und vor allem einfach Pseudoparallelität erreichen kann - Coroutinen, implementiert mit dem Coro-Modul. Coro greift auf viele Perl-Interna zu und ist eigentlich recht experimentell, ich hatte aber Erfahrungen mit langlaufenden Servern und Coro und wußte daher, das es sehr stabil läuft, sobald es läuft. Daß das Umfeld stabil ist, gab den Ausschlag. =head2 Implementierung Nachdem ein paar grundlegende Gedanken gedacht waren, konnte ich an die Implementierung gehen. Flexibilität ist alles, wenn etwas schiefgeht. =head3 Perl? .... Microperl! Wenig bekannt ist, daß es auch eine Microperl-"Distribution" gibt, die ein sehr kleines und portables Perl verspricht - ohne Module, ohne Systemabhängigkeiten und ohne interaktives Configure. Die "Distribution" ist sehr klein: sie besteht nur aus den drei Dateien C, C und C. Daher ist sie bei Perl gleich mit dabei. Ich habe das Makefile und die nötigen Perl-Sourcen in ein Unterverzeichnis kopiert und dann die Konfiguration, die standardmäßig nur ISO-C89 + einige wenige Erweiterungen wie C benötigt, solange getuned, bis ich Zugriff auf die meisten POSIX-Funktionen hatte. Dazu habe ich die C von: -DPERL_CORE -DPERL_MICRO -DSTANDARD_C -DPERL_USE_SAFE_PUTENV auf -DPERL_CORE=1 -DPERL_MICRO=1 geändert und die Konfiguration in C getuned, compiliert und solange iteriert, bis ich zum Link-Stadium kam. Danach hatte ich folgende Dateien in meinem C-Verzeichnis: EXTERN.h globals.c opnames.h pp_hot.c sv.h INTERN.h gv.c pad.c pp_pack.c taint.c Makefile gv.h pad.h pp_proto.h thrdvar.h XSUB.h handy.h patchlevel.h pp_sort.c thread.h av.c hv.c perl.c pp_sys.c toke.c av.h hv.h perl.h proto.h uconfig.sh config_h.SH intrpvar.h perlapi.c reentr.c universal.c configpm iperlsys.h perlapi.h reentr.h unixish.h cop.h keywords.h perlio.c reentr.inc utf8.c cv.h locale.c perlio.h regcomp.c utf8.h deb.c mg.c perliol.h regcomp.h util.c doio.c mg.h perlsdio.h regexec.c util.h doop.c miniperlmain.c perlvars.h regexp.h warnings.h dump.c nostdio.h perly.c regnodes.h xsutils.c embed.h numeric.c perly.h run.c embedvar.h op.c pp.c scope.c ext.libs op.h pp.h scope.h form.h opcode.h pp_ctl.c sv.c Beim Bauen bekommt man die C, das Microperl-Äquivalent zur C und C, das zum Bauen benötigt wird und auch beim weiteren Build-Prozess hilfreich sein kann. C wird z.B. benutzt, um aus der C das C-Modul zu basteln. Mit C, C, C und den Header-Dateien hat man alles, was man zum Embedden benötigt. Dieser Schritt war relativ leicht. Er wäre noch leichter gewesen, wenn ich auf die meisten esoterischen Funktionen wie C, C oder Signal-Handling verzichtet hätte, da ich fast alles über die C- und C-Module erreichen kann, und im Notfall auch auf C zurückgreifen könnte. Im absoluten Notfall. =head3 Bibliotheksdateien =for latex \begin{sloppypar} Im nächsten Schritt geht es um die Bilbiotheksdateien, genaugenommen das I-Verzeichnis, in dem sich die C<.pm>-Dateien befinden. Theoretisch könnte man Microperl ganz ohne dieses betreiben: das Ergebnis wäre jedoch kaum noch "Perl" zu nennen, da auch Pragmas als Module implementiert sind. Und wie man an anderen, einfacher embeddbaren Sprachen sieht, ist Perl ohne Standardbibliothek eine nutzlosere embeddete Sprache, als man denkt. =for latex \end{sloppypar} Üblicherweise sind die C<.pm>-Dateien in den jeweiligen Modul-Distributionen, aber bei Perl sind sie schon fertig abgepackt im C-Verzeichnis. Das größte Problem war es, alle Dateien und Unterverzeichnisse in CVS einzuchecken, nachdem ich sie aus der Perl-Distribution kopiert hatte (C). Fragt sich noch, wie die Dateien in das Executable kommen. Dafür sorgt folgende Regel im C: UPERL = uperl/microperl -Ilib-perl src/libfiles.c: s/gen_libfiles extensions uperl/microperl $(UPERL) s/gen_libfiles >$@ =for latex \begin{sloppypar} Die sorgt dafür, daß das Perl-Skript C aus allen Dateien die Datei C generiert (C heißen bei mir Verzeichnisse mit Skripten, eine alte Amiga-Gewohnheit). =for latex \end{sloppypar} Die erzeugte Datei sieht so aus: const char *embed_files[] = { /* 0 */ "\025Attribute/Handlers.pm\000\000\030Apackage Attribute::Handlers;\01... " "data eq 'ARRAY';\012\011\011\011\\$data = [ \\$data ] unless \\$wa... ... /* 1 */ "\007Carp.pm\000\000\016\017package Carp;\012\012our $VERSION = '1.01'... ... ... , }; const int embed_file_num = 208; Die Datei ist 1139622 Bytes groß und mußte hier leider zweidimensional gekürzt werden. Wie man sofort sieht, folgt sie C99 und nicht C89, aber das habe ich in Kauf genommen. Was man nicht sofort sieht: Alle Dateien werden in einem großen String-Feld gespeichert, pro Datei ein String. Am Anfang steht die Länge des Dateinamens, der Dateiname, danach die Länge der Datei und danach der Inhalt. Dieses Format habe ich gewählt, damit man die Datei schnell mittels binärer Suche finden kann und nicht einmal C aufrufen muß, bzw. auch binäre Dateien ablegen kann. Die Funktion zum Suchen eines Eintrages sieht so aus (etwas umformatiert, damit sie besser paßt): SV * libfile(char *path) PREINIT: const char *s = 0, *f; int c, l = 0, m, r = embed_file_num; CODE: do { m = (l + r) >> 1; f = embed_files[m]; c = strncmp (f + 1, path, *f); if (c > 0) r = m - 1; else if (c < 0) l = m + 1; else { s = f + 1 + f[0]; l = ((unsigned char)s[0] << 24) | ((unsigned char)s[1] << 16) | ((unsigned char)s[2] << 8) | ((unsigned char)s[3] ); s += 4; break; } } while (l <= r); RETVAL = s ? newSVpvn (s, l) : &PL_sv_undef; OUTPUT: RETVAL Sie implementiert eine ganz normale binäre Suche und liefert den Inhalt der Datei als String, oder bei Nichtfinden C. Ursprünglich habe ich eine lineare Suche benutzt (bei Rapid Prototyping bleibt manchmal etwas auf der Strecke, wenn man interessantere Stadien der Entwicklung erreichen will und eine binäre Suche auch nach Jahrzehnten Programmiererfahrung nicht auf Anhieb hinkriegt) und erwartet, das es keinen Unterschied macht. Die binäre Suche macht das Ergebnis dennoch einige Prozent schneller beim Starten, also scheint Perl beim Parsen recht flott zu sein. Das Erzeugen der C-Strings aus den Dateien hat sich übrigens als weit komplizierter herausgestellt, als ich dachte. Zum einen muß man solche nervigen Sachen wie die maximale Zeilenlänge (4096) beachten, zum anderen ist das Quoting garnicht so einfach. Folgende Funktion quoted den String: # print a c-escaped string sub str { my $str = $_[0]; while (length $str) { local $_ = substr $str, 0, 2000, ""; # 4096 is ISO-C max. s/\\/\\\\/g; s/([^\x20-\x7e])/sprintf "\\%03o", ord $1/ge; s/"/\\"/g; s/\\x0a/\\n/g; print " \"$_\"\n"; } print " ,\n"; } Das andere Problem war, daß ich die Unmengen an Dokumentation nicht unbedingt im fertigen Programm brauche. Die Lösung ist C, das sich der Benutzung allerdings widersetzt, da es unbedingt einen FileHandle möchte und String-Streams in Microperl nicht zur Verfügung stehen: open my $ifh, "<", "$base/$path$_" or die "$base/$path$_: $!"; pipe my ($r, $w); if (fork == 0) { close $r; Pod::Stripper->new->parse_from_filehandle ($ifh, $w); exit; } else { close $w; } local $/; $file{"$path$_"} = <$r>; Ein C pro Datei und Parsen ist zwar nicht kostenlos, aber ich baue ja nicht unter Cygwin. Auf meinem Rechner braucht es 3,5s, was leider etwas lang ist, da ich schlecht eine Makefile-Regel bauen kann, die von allen Dateien in C abhängt und daher jedesmal das Skript aufrufe, wenn ich linke. =head3 Module embedden Für C brauche ich im Moment folgende Module: Compress-LZF Coro-Event Coro-State Crypt-Rijndael Crypt-Twofish Cwd Data Digest Event Fcntl File Filter IO List MIME POSIX PerlIO Pod-Stripper Socket Spread Sys Time attrs daemon re Header und Library sind zwar genug, um diese zu bauen, aber Perl-Module wollen ein lauffähiges Perl und Zugriff auf Kleinkram wie C, damit man sie übersetzen kann. =for latex \begin{sloppypar} Zum Glück bietet C extra Unterstützung für diesen Fall. Das Extension-C führe ich so aus: =for latex \end{sloppypar} $TOP/uperl/microperl -I$TOP/lib-perl Makefile.PL INSTALLDIRS=perl \ PERL_CORE=1 PERL_SRC=$TOP/uperl PERL_LIB=$TOP/lib-perl Anscheinend sind C und C der Trick. Perl machts beim Übersetzen genauso und es funktioniert, mehr Gedanken habe ich mir nicht gemacht. Zum Übersetzen verwende ich folgenden C-Aufruf: make CC=$(CC) OPTIMIZE=$(OPTIMIZE) LINKTYPE=static CCCDLFLAGS= static Das und noch mehr ist in einem Skript namens C verewigt, das nach dem Perl-Vorbild C geschrieben wurde. Es macht wenig mehr als die beiden Kommandos auszuführen: es baut automatisch das C, falls es keines gibt und akzeptiert z.B. C als Argument. Nichts, was man mit einem gescheiten C nicht sauberer lösen könnte. Dies baut für jede Extension, die XS benutzt, eine C, gegen die man linken muss. =head4 PM-Dateien von Erweiterungen Erweiterungen besitzen fast immer auch eigene C<.pm>-Dateien, die in das fertige Executable gehören. Mein (nicht ganz perfekter) Weg, dies zu erreichen war, C in den fertig gebauten Erweiterungsmodulen zu machen. Dies installiert die C<.pm>-Dateien (und eventuell andere Bibliotheksdateien) im C-Verzeichnis, wo sie beim Bauen automatisch gefunden werden. Ich darf dann bloss nicht vergessen, sie ins CVS einzuchecken. =head3 Linken und das Hauptprogramm Das Linken geht relativ einfach: goofed: src/main.o src/xsinit.o src/libfiles.o extensions $(CC) $(CFLAGS) -o $@ src/xsinit.o \ `find lib-perl/auto -name '*.a' -print` \ src/main.o src/libfiles.o uperl/libuperl.a $(LIBS) Es werden einfach alle statischen Libraries, C und die übersetzten Dateien C, C und C zusammengelinkt. C enthält das Hauptprogramm. Es macht im wesentlichen das gleiche, das auch Perl macht (hier ohne error-checking, das Dokument C enthält genauere Erläuterungen zum Embedding allgemein). Darin sind zwei wichtige Aufrufe: perl_parse (my_perl, xs_init, EMBED_ARGC, EMBED_ARGV, (char **) NULL); perl_run (my_perl); C parsed die Kommandozeilenargumente, genau wie C, und initialisiert die eingelinkten Erweiterungsmodule. Üblicherweise (d.h. heutzutage) werden Erweiterungen als Shared-Objects gebaut und dynamisch (mit C oder C) geladen. Der C generiert aus dem Package-Namen einen Dateinamen und versucht, diese Datei als Shared-Object zu öffnen. Danach sucht es darin eine Funktion namens CName, z.B. C), registriert sie bei Perl und führt sie aus. Diese Funktion registriert alle XS-Funktionen als Perl-Funktionen und führt den C-Abschnitt aus. Eine statisch eingelinkte Erweiterung kann nicht dynamisch nach einem Namen durchsucht werden, daher muss man die Funktionen "per Hand" bei Perl registrieren (das Aufrufen geschieht erst beim Laden des Perl-Moduls). Dies erledigt die C-Funktion in der Datei C. Diese Datei kann man sich mit Hilfe des C-Moduls schreiben lassen (siehe C), ich hatte aber wenig Glück, dies automatisch zu tun (manche Module waren doppelt, andere haben gefehlt). Seither Pflege ich sie per Hand. Die Datei sieht in etwa so aus: #include "EXTERN.h" #define PERL_IN_MINIPERLMAIN_C #include "perl.h" EXTERN_C void boot_Compress__LZF (pTHX_ CV *cv); ... EXTERN_C void boot_Spread (pTHX_ CV *cv); EXTERN_C void boot_daemon (pTHX_ CV *cv); EXTERN_C void xs_init (pTHX) { char *file = __FILE__; newXS ("daemon::bootstrap", boot_daemon, file); newXS ("Spread::bootstrap", boot_Spread, file); ... newXS ("Compress::LZF::bootstrap", boot_Compress__LZF, file); } Wenn das Parsing vorbei ist, baue ich noch C<@ARGV> auf - mein Aufruf von C benutzt ja nicht die echten Kommandozeilenargumente - und rufe C auf, das die Kontrolle endgültig an Perl abgibt. =head3 Bootstrapping von Perl Welches sind nun die Argumente für den Perl-Interpreter? /* this array contains the initial startup code for the perl interpreter */ char *embed_argv[EMBED_ARGC] = { "goofed", "-e", "\n" " daemon->bootstrap (0);\n" ... weiterer Perl-Code ... }; Das erste Argument ist der Name des Programms. Danach kommt eine C<-e>-Option mit dem Startprogramm. Es sieht so aus: daemon->bootstrap (0); PerlIO::scalar->bootstrap (0); @INC = sub { open my $fh, '<', \(daemon::libfile ($_[1])) or die "FATAL: loader can't in-memory stream\n"; $fh; }; require daemon; daemon->init; Die ersten beiden Zeilen initialisieren die beiden Module C (ein Modul, in das ich die meisten Spezial-C-Funktionen für C gepackt habe) und (ausgerechnet!) C. Ersteres enthält die C-Funktion, mit dem ich die Perl-Sourcen bekomme und letzteres wird von Perl benötigt um Memory-Streams (C<< open FH, "<", \$scalar >>) zu supporten. Die beiden C-Funktionen registrieren allerdings I die XS-Funktionen, die zugehörigen Perl-Module kann man zu diesem Zeitpunkt nicht laden. Um das zu ermöglichen, überschreibe ich C<@INC> mit einer Funktion. Versucht Perl nun eine Datei zu laden durchsucht es C<@INC>, findet die Funktion und führt sie aus. Diese holt sich die entsprechende Datei und gibt einen Filehandle zurück. Perl liest und parsed sie dann. Danach kann man die Perl-Module ganz normal mit C oder C laden, was auch prompt gemacht wird, indem das C-Modul geladen und dessen C-Funktion ausgeführt wird. An diesem Punkt hat man ein Perl, in das man relativ einfach CPAN-Module einbinden kann und keinerlei Abhängigkeiten im Filesystem besitzt, schnell bootstrapped, Support für binäre Objekte (Bilder etc.) hat und die gesamte Power von Perl zur Verfügung stellt. =head2 Benutzung Nachdem ein solcher Aufwand getrieben wurde, wollte ich noch die Frage klären, ob es sich gelohnt, Perl statt C zu benutzen. Die Hauptaufgabe war es, einen Radius-Server zu bauen (wobei man mit C natürlich noch andere Aufgaben erfüllen kann, es dient also eher als Anwendungsplattform :) Das Parsen von Radius-Paketen ist eine Byte-Pfriemelei, braucht aber sehr viel Konfigurationsdaten. Das komplette Modul zum Parsen und Erzeugen von Radius-Paketen ist (inklusive reichlich Dokumentation) 307 Zeilen lang. Ähnlicher Code im I besteht aus ca. 8000 Zeilen C. Ohne Dokumentation. Allein hier hat sich Perl gelohnt. Richtig interessant wird es allerdings bei den speziellen Anforderungen: eine Authentifizierung benötigt ca. zehn LDAP-Anfragen pro Request. Im alten Radius-Daemon waren diese blockierend und haben den Großteil der Realzeit verbraucht. In C sehen sie zuerst auch blockierend aus: my $ldap_user = $self->{ldap}->search ("uuRadiusUser=$user, uuRadiusRealm=$realm, $ldap_root") or return $self->nak ("user unknown"); ... tue etwas my $ldap_profile = $self->{ldap}->search ("uuRadiusProfile=$profile, $ldap_root"); ... tue noch mehr $self->{ldap}->search ("uuRadiusNAS=$nas_ip, uuRadiusRealm=$realm, $ldap_root") or return $self->nak ("roaming not allowed"); ... usw. Auch hier wird eine Anfrage gebastelt und auf das Ergebnis gewartet. Der Unterschied ist, das die Anfrage asynchron verschickt wird und in der gleichen Zeit andere Radius-Pakete entgegengenommen werden können und Anfragen gestellt bzw. verarbeitet werden können. Da das LDAP-Protokoll selbst asynchron ist, besteht bei einem entsprechend intelligenten LDAP-Server (ich kenne keinen) sogar die Möglichkeit, einfache Anfragen sofort zu beantworten während komplizierte im Hintergrund erledigt werden. Das LDAP-Modul verwendet nicht das C-Modul von CPAN, da ich dieses nicht stabil mit OpenLDAP zusammenbringen konnte, bzw. es sich extrem gegen Event-basierte Programmierung gewehrt hat. Es benutzt stattdessen direkt die C von C (durch Xs-Funktionen in C implementiert). Das erste, was es macht, ist, eine Verbindung zum LDAP-Server aufzumachen und einen Event-Handler darauf zu binden: $self->{ldap} = new ldap::conn $self->{host}, $self->{port} || 389 or die "unable to connect to ldap server $self->{host}:$self->{port}: $!\n"; ... $self->{w} = Event->io ( fd => $self->{ldap}->fd, poll => 'r', cb => [$self, "_recvresp"], ); Normalerweise ist es mein Stil, Callback-Funktionen als anonyme Sub-Funktionen auszulegen. Nur leider gibt dies mit aktuellen Coro-Versionen manchmal Memory-Leaks, die ich nichtmal mit C finde. Die C<_recvresp>-Methode holt einen Ergebnis-Record vom Server ab, baut bei Abbruch die Verbindung neu auf, und ruft den entsprechenden Callback für die ursprüngliche Anfrage auf. Anfragen sind etwas aufwendiger: Einerseits mag es der LDAP-Server nicht, wenn zu viele Anfragen gleichzeitig ausstehen (warum, weiss ich nicht, aber da wenige Anwendungen asynchron Anfragen stellen, ist dies vielleicht nicht gut getestet): es treten Fehler auf und sie müssen wiederholt werden; andererseits darf immer nur einer gleichzeitig eine Anfrage stellen (die C-Library ist zwar I, nicht aber der FileHandle, auf den sie schreibt). Sowohl für die globale Limitierung als auch für das gleichzeitige schreiben verwende ich eine C: # in sub new my $self = bless { host => $host, lock => (new Coro::Semaphore), limit => (new Coro::Semaphore 20), %arg, }, $class; Der Abfragecode macht dann: $self->{limit}->down; # limit # of simult. requests my $sem = new Coro::Semaphore 0; $self->{lock}->down; # ensure that there is only a single writer Uff, drei Semaphoren! Die erste sorgt dafür, das nur eine bestimmte Anzahl von Abfragen gleichzeitig ausstehen können. Sie wird erst nach dem Lesen des Ergebnisses wieder freigegeben. Die zweite Sempahore wird schon gelockt erzeugt und wird nur benutzt, um auf das Ergebnis zu warten (mehr dazu gleich). Und die dritte Semaphore-Operation läßt immer nur einen gleichzeitig Anfragen zum Server schicken. Dies geschieht danach: (Achtung: stark vereinfacht :) my $id = $self->{ldap}->search ($base); # registriere Callback $self->{id}{$id} = sub { ... parse Ergebnis $sem->up; } Danach werden die Semaphoren wieder gelöst: $self->{lock}->up; $sem->down; # sleep till result is there $self->{limit}->up; Dies geschieht in umgekehrter Reihenfolge (das ist notwendig, um einen Deadlock zu verhindern) und definiert den Zeitablauf: Zuerst wird die Schreibzugriff-Semaphore freigegeben, da die Anfrage mit C zum Server geschickt wurde und nun andere Anfragen schicken dürfen. Danach wird auf das Ergebnis gewartet: Die C<$sem>-Semaphore ist gelockt und wird erst im Callback wieder gelöst (C<< $sem->up >>), wenn das Ergebnis eingetrudelt ist. Zuletzt wird die Semaphore, die die maximale Anzahl ausstehender Anfragen begrenzt, gelöst, da ja die Anfrage durchgeführt wurde und das Ergebnis vorliegt. Das sind 177 Zeilen (die XS-Schnittstelle zur OpenLDAP-Library nicht mitgezählt). Dadurch ist dir LDAP-Funktionalität abgekapselt und leicht einzeln testbar. Der Vorteil ist, daß man den eigentlichen Authentifizerungscode sehr natürlich schreiben kann, nämlich sequentiell ohne zwischendurch irgendwelche Locks beachten zu müssen oder irgendwelche Zustände zwischenzuspeichern, und trotzdem in den Genuss voller Parallelität von LDAP-Server und Radiusd gelangt. In der Praxis ist deshalb der LDAP-Server der Engpass bei der Authentifizierung. =head2 Hinweise und Verweise Quellcode für C ist leider nicht frei erhältlich. Ich hoffe aber, das man ähnliches jetzt selbst nachbauen kann. Handarbeit ist nicht immer das Beste. Man kann auch einfacher in den Genuß von einzelnen Binaries kommen. Z.B. mit dem C-Modul (von CPAN), das aus allen Modulen eines Programms eine Art Archiv mit Perl-Lader bauen kann. Es kommt dem obigen sehr nahe, da es ebenfalls ein einzelnes Binary erzeugen kann. Allerdings entpackt es zur Laufzeit die Dateien und kann auch keine Bibliotheken einlinken (Shared Objects bleiben Shared Objects). Mit C aus der Perl-Distribution kann man sich ein statisches Perl-Binary mit allen Modulen bauen, die man braucht. Dies beinhaltet aber nicht die Perl-Quellen dazu.