ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/docs/pws2004/goofed.pod
Revision: 1.12
Committed: Sun Jun 27 20:13:36 2004 UTC (22 years, 3 months ago) by root
Branch: MAIN
CVS Tags: HEAD
Changes since 1.11: +1 -1 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 =head1 GOOFED - Generic Object Oriented Flexible Extensible Daemon
2    
3     =head2 Abstract
4    
5 root 1.2 Perl ist eine tolle Sprache. Leider hat sie auch viele
6 root 1.8 Abhängigkeiten. Besonders bei Embedding muss man im Allgemeinen eine
7 root 1.10 ganze Perl-Installation "mitschleppen". Bei einem bestimmten Projekt
8 root 1.8 musste ich ohne externe Dateien oder andere Abhängigkeiten auskommen. Um
9     trotzdem Perl einsetzen zu können, musste alles in das Binary
10     wandern. Die Probleme und Lösungen, die ich fand, sind im Folgenden
11 root 1.2 beschrieben.
12 root 1.1
13 root 1.10 =head2 Wozu, weshalb und warum?
14 root 1.2
15 root 1.8 Das Projekt, für das Goofed entstand, bestand darin, den Radius-Server
16     eines großen Internet-Providers in einigen Funktionen zu ersetzen. Da die
17 root 1.2 Anforderungen durch viele Jahre hinweg sehr speziell wurden (ein typisches
18 root 1.10 "I<Wir> setzen den Standard"-Phänomen) und die alte Codebasis neuen
19 root 1.2 Anforderungen nicht mehr gut gewachsen war, musste ein neuer Daemon her.
20    
21 root 1.10 =for latex
22     \begin{sloppypar}
23    
24 root 1.8 Recht früh kam die Idee, im Kern für die komplizierten Regeln eine
25     High-Level-Skriptsprache zu verwenden und nur für das eigentliche
26 root 1.2 Radius-Paket-Management C zu verwenden.
27    
28 root 1.10 =for latex
29     \end{sloppypar}
30    
31 root 1.8 Perl schien zuerst an vielen Gründen zu scheitern: kompliziertes
32     Build-System, viele Abhängigkeiten und Dateien, keine gute Kontrolle
33     über den Speicherbedarf (Leaks wären tödlich für einen Daemon, der
34     meherre Monate durchhalten muss) und Last-not-Least stand zu befürchten,
35     daß die Server-Abteilung die Lösung sofort ablehnen würde, sobald
36 root 1.10 bekannt würde, daß sie Perl benutzt (typisches "Real-World - macht
37     keinen Sinn"-Phänomen).
38 root 1.2
39 root 1.10 =head3 Eckpunkte der Implementierung
40 root 1.1
41 root 1.3 Trotz dieser Gegenanzeigen versprach Perl auch wichtige Vorteile, der
42     wichtigste war das Vorhandensein einer grossen Codebasis und viele
43 root 1.8 Module. Das und Perl selbst führen meist sehr schnell zu verwertbaren
44 root 1.3 Ergebnissen.
45    
46 root 1.10 Um die Anforderungen zu erfüllen, habe ich folgende Entscheidungen
47 root 1.8 gefällt:
48 root 1.3
49     =over 4
50    
51 root 1.10 =item Einzelnes Binary
52 root 1.3
53     Bis auf die Konfigurationsdateien sollte alles - Perl-Interpreter, Module,
54     Bibliotheksdateien - in einer einzigen Datei sein und keine externen
55 root 1.8 Dateien benötigen oder anlegen.
56 root 1.3
57 root 1.8 Dies schließt leider das (ansonsten hervorragende) PAR-Modul aus, da
58 root 1.3 dieses alle Bibliotheksdateien zwar in eine Datei packt, zur Laufzeit
59     diese aber lediglich entpackt und entsprechend viele Dateien schreibt.
60    
61 root 1.10 =item Festgelegte Perl-Version
62 root 1.3
63     Die Perl-Version sollte hartverdrahtet werden. Der Grund liegt in vielen,
64 root 1.8 kleinen Änderungen auch innerhalb stabiler Release-Zyklen, die zu
65 root 1.10 Build-Problemen oder Fehlern zur Laufzeit führen können. I<Never change a
66 root 1.3 running system> war hier ausschlaggebend.
67    
68 root 1.12 Ausserdem kann man, sobald man die Kontrolle über die Perl-Quellen
69 root 1.10 hat, bedenkenlos Anpassungen vornehmen (ich nenne das die "Lizenz zum
70     Rumsauen"-Regel).
71 root 1.3
72 root 1.8 Es sollte möglich sein, den Perl-Kern upzugraden, wobei aber klar sein
73     sollte, das dies Veränderungen am Quellcode nach sich zieht.
74 root 1.3
75 root 1.10 =item Festgelegte Module und festgelegte Versionen
76 root 1.3
77 root 1.8 Ähnliches gilt für Module: es sollte möglich sein, fast beliebige
78 root 1.3 Module einzubauen, aber auch diese sollten in einer festen Version kommen.
79    
80     Perl-Module auf CPAN haben - seien wir ehrlich - fast immer kleine und
81 root 1.8 große Bugs. Meistens kann man mit ihnen leben oder Workarounds finden
82     - häufig muss man dazu auf Interna zugreifen, die sich von Version zu
83     Version ändern.
84 root 1.3
85 root 1.10 =item "Normales" Build-System, kein Configure
86 root 1.3
87 root 1.8 Configure nichtinteraktiv auszuführen ist kompliziert, und sehr viel kann
88     schiefgehen. Ein einfaches Makefile fühlt sich wesentlich stabiler und
89 root 1.3 einfacher wartbar an.
90    
91     =back
92    
93 root 1.8 Da der Daemon sehr viele Anfragen erhält und in anderer Form
94     weiterleitet, wäre es schön, wenn man die Bearbeitung der Anfragen
95     verzahnen könnte. Dazu braucht man ein vernünftiges Event-System (der
96 root 1.3 Name sagts schon - das Event-Modul) und etwas, mit dem man effizient
97 root 1.8 und vor allem einfach Pseudoparallelität erreichen kann - Coroutinen,
98 root 1.3 implementiert mit dem Coro-Modul.
99    
100     Coro greift auf viele Perl-Interna zu und ist eigentlich recht
101     experimentell, ich hatte aber Erfahrungen mit langlaufenden Servern und
102 root 1.8 Coro und wußte daher, das es sehr stabil läuft, sobald es läuft.
103 root 1.3
104 root 1.8 Daß das Umfeld stabil ist, gab den Ausschlag.
105 root 1.3
106 root 1.10 =head2 Implementierung
107 root 1.3
108     Nachdem ein paar grundlegende Gedanken gedacht waren, konnte ich an die
109 root 1.8 Implementierung gehen. Flexibilität ist alles, wenn etwas schiefgeht.
110 root 1.1
111 root 1.10 =head3 Perl? .... Microperl!
112 root 1.3
113 root 1.10 Wenig bekannt ist, daß es auch eine Microperl-"Distribution" gibt,
114 root 1.3 die ein sehr kleines und portables Perl verspricht - ohne Module, ohne
115 root 1.8 Systemabhängigkeiten und ohne interaktives Configure.
116 root 1.3
117 root 1.10 Die "Distribution" ist sehr klein: sie besteht nur aus den drei Dateien
118 root 1.11 C<README.micro>, C<Makefile.micro> und C<uconfig.sh>. Daher ist sie bei
119     Perl gleich mit dabei.
120 root 1.3
121 root 1.8 Ich habe das Makefile und die nötigen Perl-Sourcen in ein
122     Unterverzeichnis kopiert und dann die Konfiguration, die standardmäßig
123     nur ISO-C89 + einige wenige Erweiterungen wie C<rename()> benötigt,
124 root 1.3 solange getuned, bis ich Zugriff auf die meisten POSIX-Funktionen hatte.
125    
126     Dazu habe ich die C<CFLAGS> von:
127    
128     -DPERL_CORE -DPERL_MICRO -DSTANDARD_C -DPERL_USE_SAFE_PUTENV
129    
130     auf
131    
132     -DPERL_CORE=1 -DPERL_MICRO=1
133    
134 root 1.8 geändert und die Konfiguration in C<uconfig.sh> getuned, compiliert und
135 root 1.3 solange iteriert, bis ich zum Link-Stadium kam.
136    
137     Danach hatte ich folgende Dateien in meinem C<uperl>-Verzeichnis:
138    
139 root 1.10 EXTERN.h globals.c opnames.h pp_hot.c sv.h
140     INTERN.h gv.c pad.c pp_pack.c taint.c
141     Makefile gv.h pad.h pp_proto.h thrdvar.h
142     XSUB.h handy.h patchlevel.h pp_sort.c thread.h
143     av.c hv.c perl.c pp_sys.c toke.c
144     av.h hv.h perl.h proto.h uconfig.sh
145     config_h.SH intrpvar.h perlapi.c reentr.c universal.c
146     configpm iperlsys.h perlapi.h reentr.h unixish.h
147     cop.h keywords.h perlio.c reentr.inc utf8.c
148     cv.h locale.c perlio.h regcomp.c utf8.h
149     deb.c mg.c perliol.h regcomp.h util.c
150     doio.c mg.h perlsdio.h regexec.c util.h
151     doop.c miniperlmain.c perlvars.h regexp.h warnings.h
152     dump.c nostdio.h perly.c regnodes.h xsutils.c
153     embed.h numeric.c perly.h run.c
154     embedvar.h op.c pp.c scope.c
155     ext.libs op.h pp.h scope.h
156     form.h opcode.h pp_ctl.c sv.c
157 root 1.3
158 root 1.8 Beim Bauen bekommt man die C<libuperl.a>, das Microperl-Äquivalent
159     zur C<libperl> und C<microperl>, das zum Bauen benötigt wird und auch
160 root 1.6 beim weiteren Build-Prozess hilfreich sein kann. C<microperl> wird z.B.
161     benutzt, um aus der C<uconfig.sh> das C<Config.pm>-Modul zu basteln.
162 root 1.3
163 root 1.6 Mit C<microperl>, C<libuperl.a>, C<Config.pm> und den Header-Dateien hat
164 root 1.8 man alles, was man zum Embedden benötigt.
165 root 1.3
166 root 1.8 Dieser Schritt war relativ leicht. Er wäre noch leichter gewesen, wenn
167 root 1.6 ich auf die meisten esoterischen Funktionen wie C<getpwnam>, C<mkdir>
168 root 1.8 oder Signal-Handling verzichtet hätte, da ich fast alles über die
169 root 1.6 C<POSIX>- und C<Event>-Module erreichen kann, und im Notfall auch auf C
170 root 1.8 zurückgreifen könnte. Im absoluten Notfall.
171 root 1.1
172 root 1.10 =head3 Bibliotheksdateien
173    
174     =for latex
175     \begin{sloppypar}
176 root 1.4
177 root 1.8 Im nächsten Schritt geht es um die Bilbiotheksdateien, genaugenommen das
178 root 1.6 I<lib>-Verzeichnis, in dem sich die C<.pm>-Dateien befinden. Theoretisch
179 root 1.8 könnte man Microperl ganz ohne dieses betreiben: das Ergebnis
180 root 1.10 wäre jedoch kaum noch "Perl" zu nennen, da auch Pragmas als Module
181 root 1.6 implementiert sind. Und wie man an anderen, einfacher embeddbaren Sprachen
182 root 1.10 sieht, ist Perl ohne Standardbibliothek eine nutzlosere embeddete
183 root 1.6 Sprache, als man denkt.
184    
185 root 1.10 =for latex
186     \end{sloppypar}
187    
188 root 1.8 Üblicherweise sind die C<.pm>-Dateien in den jeweiligen
189 root 1.6 Modul-Distributionen, aber bei Perl sind sie schon fertig abgepackt im
190     C<lib>-Verzeichnis.
191    
192 root 1.8 Das größte Problem war es, alle Dateien und Unterverzeichnisse in CVS
193 root 1.6 einzuchecken, nachdem ich sie aus der Perl-Distribution kopiert hatte
194     (C<cp -rp ~/../perl-5.8.x/lib lib-perl>).
195 root 1.4
196 root 1.8 Fragt sich noch, wie die Dateien in das Executable kommen. Dafür sorgt
197 root 1.7 folgende Regel im C<Makefile>:
198    
199 root 1.10 UPERL = uperl/microperl -Ilib-perl
200    
201     src/libfiles.c: s/gen_libfiles extensions uperl/microperl
202     $(UPERL) s/gen_libfiles >$@
203 root 1.7
204 root 1.10 =for latex
205     \begin{sloppypar}
206 root 1.7
207 root 1.8 Die sorgt dafür, daß das Perl-Skript C<s/gen_libfiles> aus allen Dateien
208     die Datei C<src/libfiles.c> generiert (C<s> heißen bei mir Verzeichnisse
209 root 1.7 mit Skripten, eine alte Amiga-Gewohnheit).
210    
211 root 1.10 =for latex
212     \end{sloppypar}
213    
214 root 1.7 Die erzeugte Datei sieht so aus:
215    
216 root 1.10 const char *embed_files[] = {
217     /* 0 */
218     "\025Attribute/Handlers.pm\000\000\030Apackage Attribute::Handlers;\01...
219     " "data eq 'ARRAY';\012\011\011\011\\$data = [ \\$data ] unless \\$wa...
220     ...
221     /* 1 */
222     "\007Carp.pm\000\000\016\017package Carp;\012\012our $VERSION = '1.01'...
223     ...
224     ...
225     ,
226     };
227 root 1.7
228 root 1.10 const int embed_file_num = 208;
229 root 1.7
230 root 1.8 Die Datei ist 1139622 Bytes groß und mußte hier leider zweidimensional
231     gekürzt werden. Wie man sofort sieht, folgt sie C99 und nicht C89, aber
232 root 1.7 das habe ich in Kauf genommen.
233    
234 root 1.8 Was man nicht sofort sieht: Alle Dateien werden in einem großen
235     String-Feld gespeichert, pro Datei ein String. Am Anfang steht die Länge
236     des Dateinamens, der Dateiname, danach die Länge der Datei und danach
237     der Inhalt. Dieses Format habe ich gewählt, damit man die Datei schnell
238     mittels binärer Suche finden kann und nicht einmal C<strlen> aufrufen
239     muß, bzw. auch binäre Dateien ablegen kann.
240 root 1.7
241     Die Funktion zum Suchen eines Eintrages sieht so aus (etwas umformatiert,
242 root 1.8 damit sie besser paßt):
243 root 1.7
244 root 1.10 SV *
245     libfile(char *path)
246     PREINIT:
247     const char *s = 0, *f;
248     int c, l = 0, m, r = embed_file_num;
249     CODE:
250     do
251     {
252     m = (l + r) >> 1;
253     f = embed_files[m];
254     c = strncmp (f + 1, path, *f);
255    
256     if (c > 0) r = m - 1;
257     else if (c < 0) l = m + 1;
258     else
259     {
260     s = f + 1 + f[0];
261     l = ((unsigned char)s[0] << 24) | ((unsigned char)s[1] << 16)
262     | ((unsigned char)s[2] << 8) | ((unsigned char)s[3] );
263     s += 4;
264     break;
265     }
266     }
267     while (l <= r);
268    
269     RETVAL = s ? newSVpvn (s, l) : &PL_sv_undef;
270     OUTPUT:
271     RETVAL
272 root 1.7
273 root 1.8 Sie implementiert eine ganz normale binäre Suche und liefert den Inhalt
274 root 1.10 der Datei als String, oder bei Nichtfinden C<undef>. Ursprünglich
275 root 1.7 habe ich eine lineare Suche benutzt (bei Rapid Prototyping bleibt
276     manchmal etwas auf der Strecke, wenn man interessantere Stadien der
277 root 1.8 Entwicklung erreichen will und eine binäre Suche auch nach Jahrzehnten
278 root 1.7 Programmiererfahrung nicht auf Anhieb hinkriegt) und erwartet, das es
279 root 1.8 keinen Unterschied macht. Die binäre Suche macht das Ergebnis dennoch
280 root 1.7 einige Prozent schneller beim Starten, also scheint Perl beim Parsen recht
281     flott zu sein.
282    
283 root 1.8 Das Erzeugen der C-Strings aus den Dateien hat sich übrigens als weit
284     komplizierter herausgestellt, als ich dachte. Zum einen muß man solche
285     nervigen Sachen wie die maximale Zeilenlänge (4096) beachten, zum anderen
286 root 1.7 ist das Quoting garnicht so einfach. Folgende Funktion quoted den String:
287    
288     # print a c-escaped string
289     sub str {
290     my $str = $_[0];
291     while (length $str) {
292     local $_ = substr $str, 0, 2000, ""; # 4096 is ISO-C max.
293     s/\\/\\\\/g;
294     s/([^\x20-\x7e])/sprintf "\\%03o", ord $1/ge;
295     s/"/\\"/g;
296     s/\\x0a/\\n/g;
297     print " \"$_\"\n";
298     }
299     print " ,\n";
300     }
301    
302 root 1.8 Das andere Problem war, daß ich die Unmengen an Dokumentation nicht
303     unbedingt im fertigen Programm brauche. Die Lösung ist C<Pod::Stripper>,
304 root 1.7 das sich der Benutzung allerdings widersetzt, da es unbedingt einen
305 root 1.8 FileHandle möchte und String-Streams in Microperl nicht zur Verfügung
306 root 1.7 stehen:
307    
308     open my $ifh, "<", "$base/$path$_"
309     or die "$base/$path$_: $!";
310    
311     pipe my ($r, $w);
312     if (fork == 0) {
313     close $r;
314     Pod::Stripper->new->parse_from_filehandle ($ifh, $w);
315     exit;
316     } else {
317     close $w;
318     }
319    
320     local $/;
321     $file{"$path$_"} = <$r>;
322    
323     Ein C<fork()> pro Datei und Parsen ist zwar nicht kostenlos, aber ich baue
324     ja nicht unter Cygwin. Auf meinem Rechner braucht es 3,5s, was leider
325     etwas lang ist, da ich schlecht eine Makefile-Regel bauen kann, die von
326 root 1.8 allen Dateien in C<lib-perl> abhängt und daher jedesmal das Skript
327 root 1.7 aufrufe, wenn ich linke.
328    
329 root 1.10 =head3 Module embedden
330 root 1.1
331 root 1.8 Für C<goofed> brauche ich im Moment folgende Module:
332 root 1.7
333     Compress-LZF Coro-Event Coro-State Crypt-Rijndael Crypt-Twofish
334     Cwd Data Digest Event Fcntl File Filter IO List MIME POSIX PerlIO
335     Pod-Stripper Socket Spread Sys Time attrs daemon re
336    
337     Header und Library sind zwar genug, um diese zu bauen, aber
338 root 1.8 Perl-Module wollen ein lauffähiges Perl und Zugriff auf Kleinkram wie
339     C<ExtUtils::MakeMaker>, damit man sie übersetzen kann.
340 root 1.7
341 root 1.10 =for latex
342     \begin{sloppypar}
343    
344 root 1.8 Zum Glück bietet C<ExtUtils::MakeMaker> extra Unterstützung für diesen
345     Fall. Das Extension-C<Makefile.PL> führe ich so aus:
346 root 1.7
347 root 1.10 =for latex
348     \end{sloppypar}
349    
350 root 1.7 $TOP/uperl/microperl -I$TOP/lib-perl Makefile.PL INSTALLDIRS=perl \
351     PERL_CORE=1 PERL_SRC=$TOP/uperl PERL_LIB=$TOP/lib-perl
352    
353     Anscheinend sind C<PERL_CORE=1> und C<SRC=> der Trick. Perl machts beim
354 root 1.8 Übersetzen genauso und es funktioniert, mehr Gedanken habe ich mir nicht
355 root 1.7 gemacht.
356    
357 root 1.8 Zum Übersetzen verwende ich folgenden C<make>-Aufruf:
358 root 1.7
359     make CC=$(CC) OPTIMIZE=$(OPTIMIZE) LINKTYPE=static CCCDLFLAGS= static
360    
361     Das und noch mehr ist in einem Skript namens C<s/make_ext> verewigt, das
362     nach dem Perl-Vorbild C<ext/util/make_ext> geschrieben wurde. Es macht
363 root 1.8 wenig mehr als die beiden Kommandos auszuführen: es baut automatisch
364 root 1.7 das C<Makefile>, falls es keines gibt und akzeptiert z.B. C<clean> als
365     Argument. Nichts, was man mit einem gescheiten C<Makefile> nicht sauberer
366 root 1.8 lösen könnte.
367 root 1.7
368 root 1.8 Dies baut für jede Extension, die XS benutzt, eine C<libextension.a>,
369 root 1.7 gegen die man linken muss.
370    
371 root 1.10 =head4 PM-Dateien von Erweiterungen
372 root 1.7
373     Erweiterungen besitzen fast immer auch eigene C<.pm>-Dateien, die in das
374 root 1.8 fertige Executable gehören. Mein (nicht ganz perfekter) Weg, dies zu
375 root 1.7 erreichen war, C<make install> in den fertig gebauten Erweiterungsmodulen
376     zu machen. Dies installiert die C<.pm>-Dateien (und eventuell andere
377     Bibliotheksdateien) im C<lib-perl>-Verzeichnis, wo sie beim Bauen
378 root 1.10 automatisch gefunden werden. Ich darf dann bloss nicht vergessen, sie ins
379 root 1.7 CVS einzuchecken.
380    
381 root 1.10 =head3 Linken und das Hauptprogramm
382 root 1.7
383     Das Linken geht relativ einfach:
384    
385     goofed: src/main.o src/xsinit.o src/libfiles.o extensions
386     $(CC) $(CFLAGS) -o $@ src/xsinit.o \
387     `find lib-perl/auto -name '*.a' -print` \
388     src/main.o src/libfiles.o uperl/libuperl.a $(LIBS)
389    
390     Es werden einfach alle statischen Libraries, C<libuperl.a> und die
391 root 1.8 übersetzten Dateien C<libfiles.c>, C<xsinit.c> und C<main.c> zusammengelinkt.
392 root 1.7
393 root 1.8 C<main.c> enthält das Hauptprogramm. Es macht im wesentlichen
394 root 1.7 das gleiche, das auch Perl macht (hier ohne error-checking, das
395 root 1.8 Dokument C<perlembed> enthält genauere Erläuterungen zum Embedding
396 root 1.7 allgemein). Darin sind zwei wichtige Aufrufe:
397    
398     perl_parse (my_perl, xs_init, EMBED_ARGC, EMBED_ARGV, (char **) NULL);
399     perl_run (my_perl);
400    
401     C<perl_parse> parsed die Kommandozeilenargumente, genau wie C<perl>, und
402     initialisiert die eingelinkten Erweiterungsmodule.
403    
404 root 1.8 Üblicherweise (d.h. heutzutage) werden Erweiterungen als Shared-Objects
405 root 1.7 gebaut und dynamisch (mit C<DynaLoader> oder C<XSLoader>) geladen. Der
406     C<DynaLoader> generiert aus dem Package-Namen einen Dateinamen und
407 root 1.8 versucht, diese Datei als Shared-Object zu öffnen. Danach sucht es darin
408 root 1.7 eine Funktion namens C<boot_>Name, z.B. C<boot_Data__Dumper>), registriert
409 root 1.8 sie bei Perl und führt sie aus.
410 root 1.7
411     Diese Funktion registriert alle XS-Funktionen als Perl-Funktionen und
412 root 1.8 führt den C<BOOT:>-Abschnitt aus.
413 root 1.7
414     Eine statisch eingelinkte Erweiterung kann nicht dynamisch nach einem
415 root 1.10 Namen durchsucht werden, daher muss man die Funktionen "per Hand"
416 root 1.7 bei Perl registrieren (das Aufrufen geschieht erst beim Laden des
417     Perl-Moduls). Dies erledigt die C<xs_init>-Funktion in der Datei
418     C<xsinit.c>.
419    
420     Diese Datei kann man sich mit Hilfe des C<ExtUtils::Embed>-Moduls
421 root 1.8 schreiben lassen (siehe C<perlembed>), ich hatte aber wenig Glück,
422 root 1.10 dies automatisch zu tun (manche Module waren doppelt, andere haben
423 root 1.7 gefehlt). Seither Pflege ich sie per Hand. Die Datei sieht in etwa so aus:
424    
425     #include "EXTERN.h"
426     #define PERL_IN_MINIPERLMAIN_C
427     #include "perl.h"
428    
429     EXTERN_C void boot_Compress__LZF (pTHX_ CV *cv);
430     ...
431     EXTERN_C void boot_Spread (pTHX_ CV *cv);
432     EXTERN_C void boot_daemon (pTHX_ CV *cv);
433    
434     EXTERN_C void
435     xs_init (pTHX)
436     {
437     char *file = __FILE__;
438    
439     newXS ("daemon::bootstrap", boot_daemon, file);
440     newXS ("Spread::bootstrap", boot_Spread, file);
441     ...
442     newXS ("Compress::LZF::bootstrap", boot_Compress__LZF, file);
443     }
444    
445     Wenn das Parsing vorbei ist, baue ich noch C<@ARGV> auf - mein Aufruf von
446     C<perl_parse> benutzt ja nicht die echten Kommandozeilenargumente - und
447 root 1.8 rufe C<perl_run> auf, das die Kontrolle endgültig an Perl abgibt.
448 root 1.7
449 root 1.10 =head3 Bootstrapping von Perl
450 root 1.7
451 root 1.8 Welches sind nun die Argumente für den Perl-Interpreter?
452 root 1.7
453 root 1.10 /* this array contains the initial startup code for the perl interpreter */
454     char *embed_argv[EMBED_ARGC] = {
455     "goofed",
456     "-e",
457     "\n"
458     " daemon->bootstrap (0);\n"
459     ... weiterer Perl-Code ...
460     };
461 root 1.7
462     Das erste Argument ist der Name des Programms. Danach kommt eine
463     C<-e>-Option mit dem Startprogramm. Es sieht so aus:
464    
465     daemon->bootstrap (0);
466     PerlIO::scalar->bootstrap (0);
467     @INC = sub {
468     open my $fh, '<', \(daemon::libfile ($_[1]))
469     or die "FATAL: loader can't in-memory stream\n";
470     $fh;
471     };
472     require daemon;
473     daemon->init;
474    
475     Die ersten beiden Zeilen initialisieren die beiden Module C<daemon> (ein
476 root 1.8 Modul, in das ich die meisten Spezial-C-Funktionen für C<goofed> gepackt
477 root 1.7 habe) und (ausgerechnet!) C<PerlIO::scalar>.
478    
479 root 1.8 Ersteres enthält die C<libfile>-Funktion, mit dem ich die Perl-Sourcen
480     bekomme und letzteres wird von Perl benötigt um Memory-Streams (C<< open
481 root 1.7 FH, "<", \$scalar >>) zu supporten. Die beiden C<bootstrap>-Funktionen
482 root 1.8 registrieren allerdings I<nur> die XS-Funktionen, die zugehörigen
483 root 1.7 Perl-Module kann man zu diesem Zeitpunkt nicht laden.
484    
485 root 1.8 Um das zu ermöglichen, überschreibe ich C<@INC> mit einer
486 root 1.7 Funktion. Versucht Perl nun eine Datei zu laden durchsucht es C<@INC>,
487 root 1.8 findet die Funktion und führt sie aus. Diese holt sich die entsprechende
488     Datei und gibt einen Filehandle zurück. Perl liest und parsed sie dann.
489 root 1.7
490     Danach kann man die Perl-Module ganz normal mit C<use> oder C<require>
491     laden, was auch prompt gemacht wird, indem das C<daemon>-Modul geladen und
492 root 1.8 dessen C<init>-Funktion ausgeführt wird.
493 root 1.7
494     An diesem Punkt hat man ein Perl, in das man relativ einfach CPAN-Module
495 root 1.8 einbinden kann und keinerlei Abhängigkeiten im Filesystem besitzt,
496 root 1.10 schnell bootstrapped, Support für binäre Objekte (Bilder etc.) hat und
497 root 1.8 die gesamte Power von Perl zur Verfügung stellt.
498 root 1.7
499 root 1.10 =head2 Benutzung
500 root 1.7
501     Nachdem ein solcher Aufwand getrieben wurde, wollte ich noch die Frage
502 root 1.8 klären, ob es sich gelohnt, Perl statt C zu benutzen.
503 root 1.7
504     Die Hauptaufgabe war es, einen Radius-Server zu bauen (wobei man mit
505 root 1.8 C<goofed> natürlich noch andere Aufgaben erfüllen kann, es dient also
506 root 1.7 eher als Anwendungsplattform :)
507    
508     Das Parsen von Radius-Paketen ist eine Byte-Pfriemelei, braucht aber sehr
509     viel Konfigurationsdaten. Das komplette Modul zum Parsen und Erzeugen von
510     Radius-Paketen ist (inklusive reichlich Dokumentation) 307 Zeilen lang.
511 root 1.8 Ähnlicher Code im I<Merit-Radiusd> besteht aus ca. 8000 Zeilen C. Ohne
512 root 1.7 Dokumentation.
513    
514     Allein hier hat sich Perl gelohnt. Richtig interessant wird es allerdings
515 root 1.10 bei den speziellen Anforderungen: eine Authentifizierung benötigt ca.
516 root 1.7 zehn LDAP-Anfragen pro Request. Im alten Radius-Daemon waren diese
517 root 1.8 blockierend und haben den Großteil der Realzeit verbraucht.
518 root 1.7
519     In C<goofed> sehen sie zuerst auch blockierend aus:
520    
521     my $ldap_user = $self->{ldap}->search ("uuRadiusUser=$user, uuRadiusRealm=$realm, $ldap_root")
522     or return $self->nak ("user unknown");
523     ... tue etwas
524     my $ldap_profile = $self->{ldap}->search ("uuRadiusProfile=$profile, $ldap_root");
525     ... tue noch mehr
526     $self->{ldap}->search ("uuRadiusNAS=$nas_ip, uuRadiusRealm=$realm, $ldap_root")
527     or return $self->nak ("roaming not allowed");
528     ... usw.
529    
530     Auch hier wird eine Anfrage gebastelt und auf das Ergebnis gewartet. Der
531     Unterschied ist, das die Anfrage asynchron verschickt wird und in der
532 root 1.8 gleichen Zeit andere Radius-Pakete entgegengenommen werden können und
533     Anfragen gestellt bzw. verarbeitet werden können.
534 root 1.7
535     Da das LDAP-Protokoll selbst asynchron ist, besteht bei einem entsprechend
536 root 1.8 intelligenten LDAP-Server (ich kenne keinen) sogar die Möglichkeit,
537     einfache Anfragen sofort zu beantworten während komplizierte im
538 root 1.7 Hintergrund erledigt werden.
539    
540     Das LDAP-Modul verwendet nicht das C<Net:LDAP>-Modul von CPAN, da ich
541     dieses nicht stabil mit OpenLDAP zusammenbringen konnte, bzw. es sich
542     extrem gegen Event-basierte Programmierung gewehrt hat. Es benutzt
543     stattdessen direkt die C<libldap> von C<OpenLDAP> (durch Xs-Funktionen in
544     C<daemon.xs> implementiert).
545    
546     Das erste, was es macht, ist, eine Verbindung zum LDAP-Server aufzumachen
547     und einen Event-Handler darauf zu binden:
548    
549     $self->{ldap} = new ldap::conn $self->{host}, $self->{port} || 389
550     or die "unable to connect to ldap server $self->{host}:$self->{port}: $!\n";
551     ...
552     $self->{w} = Event->io (
553     fd => $self->{ldap}->fd,
554     poll => 'r',
555     cb => [$self, "_recvresp"],
556     );
557    
558     Normalerweise ist es mein Stil, Callback-Funktionen als anonyme
559     Sub-Funktionen auszulegen. Nur leider gibt dies mit aktuellen
560 root 1.10 Coro-Versionen manchmal Memory-Leaks, die ich nichtmal mit C<valgrind> finde.
561 root 1.7
562     Die C<_recvresp>-Methode holt einen Ergebnis-Record vom Server ab, baut
563     bei Abbruch die Verbindung neu auf, und ruft den entsprechenden Callback
564 root 1.8 für die ursprüngliche Anfrage auf.
565 root 1.7
566     Anfragen sind etwas aufwendiger: Einerseits mag es der LDAP-Server nicht,
567     wenn zu viele Anfragen gleichzeitig ausstehen (warum, weiss ich nicht,
568     aber da wenige Anwendungen asynchron Anfragen stellen, ist dies vielleicht
569 root 1.8 nicht gut getestet): es treten Fehler auf und sie müssen wiederholt
570 root 1.7 werden; andererseits darf immer nur einer gleichzeitig eine Anfrage
571     stellen (die C-Library ist zwar I<reentrant>, nicht aber der FileHandle,
572     auf den sie schreibt).
573    
574 root 1.8 Sowohl für die globale Limitierung als auch für das gleichzeitige
575 root 1.7 schreiben verwende ich eine C<Coro::Semaphore>:
576    
577     # in sub new
578     my $self = bless {
579     host => $host,
580     lock => (new Coro::Semaphore),
581     limit => (new Coro::Semaphore 20),
582     %arg,
583     }, $class;
584    
585     Der Abfragecode macht dann:
586    
587     $self->{limit}->down; # limit # of simult. requests
588     my $sem = new Coro::Semaphore 0;
589     $self->{lock}->down; # ensure that there is only a single writer
590    
591 root 1.8 Uff, drei Semaphoren! Die erste sorgt dafür, das nur eine bestimmte
592     Anzahl von Abfragen gleichzeitig ausstehen können. Sie wird erst nach dem
593 root 1.7 Lesen des Ergebnisses wieder freigegeben.
594    
595     Die zweite Sempahore wird schon gelockt erzeugt und wird nur benutzt, um
596     auf das Ergebnis zu warten (mehr dazu gleich).
597    
598 root 1.8 Und die dritte Semaphore-Operation läßt immer nur einen gleichzeitig
599 root 1.7 Anfragen zum Server schicken.
600    
601     Dies geschieht danach: (Achtung: stark vereinfacht :)
602    
603     my $id = $self->{ldap}->search ($base);
604     # registriere Callback
605     $self->{id}{$id} = sub {
606     ... parse Ergebnis
607     $sem->up;
608     }
609    
610 root 1.8 Danach werden die Semaphoren wieder gelöst:
611 root 1.7
612     $self->{lock}->up;
613     $sem->down; # sleep till result is there
614     $self->{limit}->up;
615    
616 root 1.10 Dies geschieht in umgekehrter Reihenfolge (das ist notwendig,
617     um einen Deadlock zu verhindern) und definiert den Zeitablauf:
618 root 1.7
619     Zuerst wird die Schreibzugriff-Semaphore freigegeben, da die Anfrage mit
620     C<search> zum Server geschickt wurde und nun andere Anfragen schicken
621 root 1.8 dürfen.
622 root 1.7
623     Danach wird auf das Ergebnis gewartet: Die C<$sem>-Semaphore ist gelockt
624 root 1.8 und wird erst im Callback wieder gelöst (C<< $sem->up >>), wenn das
625 root 1.7 Ergebnis eingetrudelt ist.
626    
627     Zuletzt wird die Semaphore, die die maximale Anzahl ausstehender Anfragen
628 root 1.8 begrenzt, gelöst, da ja die Anfrage durchgeführt wurde und das Ergebnis
629 root 1.7 vorliegt.
630    
631     Das sind 177 Zeilen (die XS-Schnittstelle zur OpenLDAP-Library nicht
632 root 1.8 mitgezählt). Dadurch ist dir LDAP-Funktionalität abgekapselt und leicht
633 root 1.7 einzeln testbar.
634    
635 root 1.8 Der Vorteil ist, daß man den eigentlichen Authentifizerungscode sehr
636     natürlich schreiben kann, nämlich sequentiell ohne zwischendurch
637     irgendwelche Locks beachten zu müssen oder irgendwelche Zustände
638     zwischenzuspeichern, und trotzdem in den Genuss voller Parallelität von
639 root 1.7 LDAP-Server und Radiusd gelangt. In der Praxis ist deshalb der LDAP-Server
640     der Engpass bei der Authentifizierung.
641    
642 root 1.10 =head2 Hinweise und Verweise
643 root 1.7
644 root 1.8 Quellcode für C<goofed> ist leider nicht frei erhältlich. Ich hoffe
645     aber, das man ähnliches jetzt selbst nachbauen kann.
646 root 1.7
647     Handarbeit ist nicht immer das Beste. Man kann auch einfacher in den
648 root 1.8 Genuß von einzelnen Binaries kommen. Z.B. mit dem C<PAR>-Modul (von
649 root 1.7 CPAN), das aus allen Modulen eines Programms eine Art Archiv mit
650     Perl-Lader bauen kann. Es kommt dem obigen sehr nahe, da es ebenfalls ein
651     einzelnes Binary erzeugen kann. Allerdings entpackt es zur Laufzeit die
652     Dateien und kann auch keine Bibliotheken einlinken (Shared Objects bleiben
653     Shared Objects).
654    
655     Mit C<makeaperl> aus der Perl-Distribution kann man sich ein statisches
656     Perl-Binary mit allen Modulen bauen, die man braucht. Dies beinhaltet aber
657     nicht die Perl-Quellen dazu.
658 root 1.1
659