| 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 |
|