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

# Content
1 =head1 GOOFED - Generic Object Oriented Flexible Extensible Daemon
2
3 =head2 Abstract
4
5 Perl ist eine tolle Sprache. Leider hat sie auch viele
6 Abhängigkeiten. Besonders bei Embedding muss man im Allgemeinen eine
7 ganze Perl-Installation "mitschleppen". Bei einem bestimmten Projekt
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 beschrieben.
12
13 =head2 Wozu, weshalb und warum?
14
15 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 Anforderungen durch viele Jahre hinweg sehr speziell wurden (ein typisches
18 "I<Wir> setzen den Standard"-Phänomen) und die alte Codebasis neuen
19 Anforderungen nicht mehr gut gewachsen war, musste ein neuer Daemon her.
20
21 =for latex
22 \begin{sloppypar}
23
24 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 Radius-Paket-Management C zu verwenden.
27
28 =for latex
29 \end{sloppypar}
30
31 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 bekannt würde, daß sie Perl benutzt (typisches "Real-World - macht
37 keinen Sinn"-Phänomen).
38
39 =head3 Eckpunkte der Implementierung
40
41 Trotz dieser Gegenanzeigen versprach Perl auch wichtige Vorteile, der
42 wichtigste war das Vorhandensein einer grossen Codebasis und viele
43 Module. Das und Perl selbst führen meist sehr schnell zu verwertbaren
44 Ergebnissen.
45
46 Um die Anforderungen zu erfüllen, habe ich folgende Entscheidungen
47 gefällt:
48
49 =over 4
50
51 =item Einzelnes Binary
52
53 Bis auf die Konfigurationsdateien sollte alles - Perl-Interpreter, Module,
54 Bibliotheksdateien - in einer einzigen Datei sein und keine externen
55 Dateien benötigen oder anlegen.
56
57 Dies schließt leider das (ansonsten hervorragende) PAR-Modul aus, da
58 dieses alle Bibliotheksdateien zwar in eine Datei packt, zur Laufzeit
59 diese aber lediglich entpackt und entsprechend viele Dateien schreibt.
60
61 =item Festgelegte Perl-Version
62
63 Die Perl-Version sollte hartverdrahtet werden. Der Grund liegt in vielen,
64 kleinen Änderungen auch innerhalb stabiler Release-Zyklen, die zu
65 Build-Problemen oder Fehlern zur Laufzeit führen können. I<Never change a
66 running system> war hier ausschlaggebend.
67
68 Ausserdem kann man, sobald man die Kontrolle über die Perl-Quellen
69 hat, bedenkenlos Anpassungen vornehmen (ich nenne das die "Lizenz zum
70 Rumsauen"-Regel).
71
72 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
75 =item Festgelegte Module und festgelegte Versionen
76
77 Ähnliches gilt für Module: es sollte möglich sein, fast beliebige
78 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 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
85 =item "Normales" Build-System, kein Configure
86
87 Configure nichtinteraktiv auszuführen ist kompliziert, und sehr viel kann
88 schiefgehen. Ein einfaches Makefile fühlt sich wesentlich stabiler und
89 einfacher wartbar an.
90
91 =back
92
93 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 Name sagts schon - das Event-Modul) und etwas, mit dem man effizient
97 und vor allem einfach Pseudoparallelität erreichen kann - Coroutinen,
98 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 Coro und wußte daher, das es sehr stabil läuft, sobald es läuft.
103
104 Daß das Umfeld stabil ist, gab den Ausschlag.
105
106 =head2 Implementierung
107
108 Nachdem ein paar grundlegende Gedanken gedacht waren, konnte ich an die
109 Implementierung gehen. Flexibilität ist alles, wenn etwas schiefgeht.
110
111 =head3 Perl? .... Microperl!
112
113 Wenig bekannt ist, daß es auch eine Microperl-"Distribution" gibt,
114 die ein sehr kleines und portables Perl verspricht - ohne Module, ohne
115 Systemabhängigkeiten und ohne interaktives Configure.
116
117 Die "Distribution" ist sehr klein: sie besteht nur aus den drei Dateien
118 C<README.micro>, C<Makefile.micro> und C<uconfig.sh>. Daher ist sie bei
119 Perl gleich mit dabei.
120
121 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 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 geändert und die Konfiguration in C<uconfig.sh> getuned, compiliert und
135 solange iteriert, bis ich zum Link-Stadium kam.
136
137 Danach hatte ich folgende Dateien in meinem C<uperl>-Verzeichnis:
138
139 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
158 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 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
163 Mit C<microperl>, C<libuperl.a>, C<Config.pm> und den Header-Dateien hat
164 man alles, was man zum Embedden benötigt.
165
166 Dieser Schritt war relativ leicht. Er wäre noch leichter gewesen, wenn
167 ich auf die meisten esoterischen Funktionen wie C<getpwnam>, C<mkdir>
168 oder Signal-Handling verzichtet hätte, da ich fast alles über die
169 C<POSIX>- und C<Event>-Module erreichen kann, und im Notfall auch auf C
170 zurückgreifen könnte. Im absoluten Notfall.
171
172 =head3 Bibliotheksdateien
173
174 =for latex
175 \begin{sloppypar}
176
177 Im nächsten Schritt geht es um die Bilbiotheksdateien, genaugenommen das
178 I<lib>-Verzeichnis, in dem sich die C<.pm>-Dateien befinden. Theoretisch
179 könnte man Microperl ganz ohne dieses betreiben: das Ergebnis
180 wäre jedoch kaum noch "Perl" zu nennen, da auch Pragmas als Module
181 implementiert sind. Und wie man an anderen, einfacher embeddbaren Sprachen
182 sieht, ist Perl ohne Standardbibliothek eine nutzlosere embeddete
183 Sprache, als man denkt.
184
185 =for latex
186 \end{sloppypar}
187
188 Üblicherweise sind die C<.pm>-Dateien in den jeweiligen
189 Modul-Distributionen, aber bei Perl sind sie schon fertig abgepackt im
190 C<lib>-Verzeichnis.
191
192 Das größte Problem war es, alle Dateien und Unterverzeichnisse in CVS
193 einzuchecken, nachdem ich sie aus der Perl-Distribution kopiert hatte
194 (C<cp -rp ~/../perl-5.8.x/lib lib-perl>).
195
196 Fragt sich noch, wie die Dateien in das Executable kommen. Dafür sorgt
197 folgende Regel im C<Makefile>:
198
199 UPERL = uperl/microperl -Ilib-perl
200
201 src/libfiles.c: s/gen_libfiles extensions uperl/microperl
202 $(UPERL) s/gen_libfiles >$@
203
204 =for latex
205 \begin{sloppypar}
206
207 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 mit Skripten, eine alte Amiga-Gewohnheit).
210
211 =for latex
212 \end{sloppypar}
213
214 Die erzeugte Datei sieht so aus:
215
216 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
228 const int embed_file_num = 208;
229
230 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 das habe ich in Kauf genommen.
233
234 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
241 Die Funktion zum Suchen eines Eintrages sieht so aus (etwas umformatiert,
242 damit sie besser paßt):
243
244 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
273 Sie implementiert eine ganz normale binäre Suche und liefert den Inhalt
274 der Datei als String, oder bei Nichtfinden C<undef>. Ursprünglich
275 habe ich eine lineare Suche benutzt (bei Rapid Prototyping bleibt
276 manchmal etwas auf der Strecke, wenn man interessantere Stadien der
277 Entwicklung erreichen will und eine binäre Suche auch nach Jahrzehnten
278 Programmiererfahrung nicht auf Anhieb hinkriegt) und erwartet, das es
279 keinen Unterschied macht. Die binäre Suche macht das Ergebnis dennoch
280 einige Prozent schneller beim Starten, also scheint Perl beim Parsen recht
281 flott zu sein.
282
283 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 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 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 das sich der Benutzung allerdings widersetzt, da es unbedingt einen
305 FileHandle möchte und String-Streams in Microperl nicht zur Verfügung
306 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 allen Dateien in C<lib-perl> abhängt und daher jedesmal das Skript
327 aufrufe, wenn ich linke.
328
329 =head3 Module embedden
330
331 Für C<goofed> brauche ich im Moment folgende Module:
332
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 Perl-Module wollen ein lauffähiges Perl und Zugriff auf Kleinkram wie
339 C<ExtUtils::MakeMaker>, damit man sie übersetzen kann.
340
341 =for latex
342 \begin{sloppypar}
343
344 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
347 =for latex
348 \end{sloppypar}
349
350 $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 Übersetzen genauso und es funktioniert, mehr Gedanken habe ich mir nicht
355 gemacht.
356
357 Zum Übersetzen verwende ich folgenden C<make>-Aufruf:
358
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 wenig mehr als die beiden Kommandos auszuführen: es baut automatisch
364 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 lösen könnte.
367
368 Dies baut für jede Extension, die XS benutzt, eine C<libextension.a>,
369 gegen die man linken muss.
370
371 =head4 PM-Dateien von Erweiterungen
372
373 Erweiterungen besitzen fast immer auch eigene C<.pm>-Dateien, die in das
374 fertige Executable gehören. Mein (nicht ganz perfekter) Weg, dies zu
375 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 automatisch gefunden werden. Ich darf dann bloss nicht vergessen, sie ins
379 CVS einzuchecken.
380
381 =head3 Linken und das Hauptprogramm
382
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 übersetzten Dateien C<libfiles.c>, C<xsinit.c> und C<main.c> zusammengelinkt.
392
393 C<main.c> enthält das Hauptprogramm. Es macht im wesentlichen
394 das gleiche, das auch Perl macht (hier ohne error-checking, das
395 Dokument C<perlembed> enthält genauere Erläuterungen zum Embedding
396 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 Üblicherweise (d.h. heutzutage) werden Erweiterungen als Shared-Objects
405 gebaut und dynamisch (mit C<DynaLoader> oder C<XSLoader>) geladen. Der
406 C<DynaLoader> generiert aus dem Package-Namen einen Dateinamen und
407 versucht, diese Datei als Shared-Object zu öffnen. Danach sucht es darin
408 eine Funktion namens C<boot_>Name, z.B. C<boot_Data__Dumper>), registriert
409 sie bei Perl und führt sie aus.
410
411 Diese Funktion registriert alle XS-Funktionen als Perl-Funktionen und
412 führt den C<BOOT:>-Abschnitt aus.
413
414 Eine statisch eingelinkte Erweiterung kann nicht dynamisch nach einem
415 Namen durchsucht werden, daher muss man die Funktionen "per Hand"
416 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 schreiben lassen (siehe C<perlembed>), ich hatte aber wenig Glück,
422 dies automatisch zu tun (manche Module waren doppelt, andere haben
423 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 rufe C<perl_run> auf, das die Kontrolle endgültig an Perl abgibt.
448
449 =head3 Bootstrapping von Perl
450
451 Welches sind nun die Argumente für den Perl-Interpreter?
452
453 /* 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
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 Modul, in das ich die meisten Spezial-C-Funktionen für C<goofed> gepackt
477 habe) und (ausgerechnet!) C<PerlIO::scalar>.
478
479 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 FH, "<", \$scalar >>) zu supporten. Die beiden C<bootstrap>-Funktionen
482 registrieren allerdings I<nur> die XS-Funktionen, die zugehörigen
483 Perl-Module kann man zu diesem Zeitpunkt nicht laden.
484
485 Um das zu ermöglichen, überschreibe ich C<@INC> mit einer
486 Funktion. Versucht Perl nun eine Datei zu laden durchsucht es C<@INC>,
487 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
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 dessen C<init>-Funktion ausgeführt wird.
493
494 An diesem Punkt hat man ein Perl, in das man relativ einfach CPAN-Module
495 einbinden kann und keinerlei Abhängigkeiten im Filesystem besitzt,
496 schnell bootstrapped, Support für binäre Objekte (Bilder etc.) hat und
497 die gesamte Power von Perl zur Verfügung stellt.
498
499 =head2 Benutzung
500
501 Nachdem ein solcher Aufwand getrieben wurde, wollte ich noch die Frage
502 klären, ob es sich gelohnt, Perl statt C zu benutzen.
503
504 Die Hauptaufgabe war es, einen Radius-Server zu bauen (wobei man mit
505 C<goofed> natürlich noch andere Aufgaben erfüllen kann, es dient also
506 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 Ähnlicher Code im I<Merit-Radiusd> besteht aus ca. 8000 Zeilen C. Ohne
512 Dokumentation.
513
514 Allein hier hat sich Perl gelohnt. Richtig interessant wird es allerdings
515 bei den speziellen Anforderungen: eine Authentifizierung benötigt ca.
516 zehn LDAP-Anfragen pro Request. Im alten Radius-Daemon waren diese
517 blockierend und haben den Großteil der Realzeit verbraucht.
518
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 gleichen Zeit andere Radius-Pakete entgegengenommen werden können und
533 Anfragen gestellt bzw. verarbeitet werden können.
534
535 Da das LDAP-Protokoll selbst asynchron ist, besteht bei einem entsprechend
536 intelligenten LDAP-Server (ich kenne keinen) sogar die Möglichkeit,
537 einfache Anfragen sofort zu beantworten während komplizierte im
538 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 Coro-Versionen manchmal Memory-Leaks, die ich nichtmal mit C<valgrind> finde.
561
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 für die ursprüngliche Anfrage auf.
565
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 nicht gut getestet): es treten Fehler auf und sie müssen wiederholt
570 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 Sowohl für die globale Limitierung als auch für das gleichzeitige
575 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 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 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 Und die dritte Semaphore-Operation läßt immer nur einen gleichzeitig
599 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 Danach werden die Semaphoren wieder gelöst:
611
612 $self->{lock}->up;
613 $sem->down; # sleep till result is there
614 $self->{limit}->up;
615
616 Dies geschieht in umgekehrter Reihenfolge (das ist notwendig,
617 um einen Deadlock zu verhindern) und definiert den Zeitablauf:
618
619 Zuerst wird die Schreibzugriff-Semaphore freigegeben, da die Anfrage mit
620 C<search> zum Server geschickt wurde und nun andere Anfragen schicken
621 dürfen.
622
623 Danach wird auf das Ergebnis gewartet: Die C<$sem>-Semaphore ist gelockt
624 und wird erst im Callback wieder gelöst (C<< $sem->up >>), wenn das
625 Ergebnis eingetrudelt ist.
626
627 Zuletzt wird die Semaphore, die die maximale Anzahl ausstehender Anfragen
628 begrenzt, gelöst, da ja die Anfrage durchgeführt wurde und das Ergebnis
629 vorliegt.
630
631 Das sind 177 Zeilen (die XS-Schnittstelle zur OpenLDAP-Library nicht
632 mitgezählt). Dadurch ist dir LDAP-Funktionalität abgekapselt und leicht
633 einzeln testbar.
634
635 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 LDAP-Server und Radiusd gelangt. In der Praxis ist deshalb der LDAP-Server
640 der Engpass bei der Authentifizierung.
641
642 =head2 Hinweise und Verweise
643
644 Quellcode für C<goofed> ist leider nicht frei erhältlich. Ich hoffe
645 aber, das man ähnliches jetzt selbst nachbauen kann.
646
647 Handarbeit ist nicht immer das Beste. Man kann auch einfacher in den
648 Genuß von einzelnen Binaries kommen. Z.B. mit dem C<PAR>-Modul (von
649 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
659