Nach Hause telefonieren

Intelligentes Leben außerhalb der Erde halten nicht nur Steven Spielberg und Erich von Däniken, sondern auch seriöse Wissenschaftler für möglich. Die durchaus irdischen Mittel von Perl können bei der Suche helfen.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 10 Min.
Von
  • Steffen Merunka
Inhaltsverzeichnis

Ähnlich den weltweiten Versuchen, RSA-Schlüssel zu knacken, funktioniert das SETI@Home-Projekt (Search for Extraterrestrial Intelligence) der Universität von Berkeley. Es ist ein wissenschaftliches Experiment, das die Rechenleistung hunderttausender Computer weltweit zur Suche nach Signalen extraterrestrischer Intelligenzen verwendet. Jeder Internetbenutzer kann teilnehmen, indem er ein freies Programm installiert, das kleine Datenpakete des Arecibo Radioteleskops vom SETI@Home-Server herunterlädt und offline analysiert. So kann ein Laie das leise elektronische Flüstern einer außerirdischen Intelligenz entdecken - wenn auch die Wahrscheinlichkeit dafür gering ist. Mehr als eine Million Nutzer sind bereits registriert. Details zum Projekt finden sich unter http://www.setihome.com.

Der SETI-Client ist für eine Vielzahl von Betriebssystem- und Hardwareplattformen vorhanden [1]. Ausschließlich für Windows und Mac gibt es eine grafische Variante, die zum Beispiel als Bildschirmschoner laufen kann. Auf allen anderen Plattformen arbeitet eine Textversion, die jedoch den Statistiken des Projekts [2] zufolge nur 30 Prozent zum Ergebnis beiträgt.

SETI@Home-Daten benötigen viel Rechenzeit (100%ige CPU-Auslastung), schließlich findet fortgeschrittene digitale Signalverarbeitung statt [3]. Die durchschnittliche Rechenzeit liegt derzeit bei über 27 Stunden für ein einzelnes Datenpaket.

Dabei reicht die Bandbreite von über 35 Stunden für die Kombination Pentium/Windows bis 80 Minuten einer auf Alpha EV6. Für die meisten Nutzer stellt sich wegen dieses Zeitaufwands nach der ersten Euphorie wohl schnell Ernüchterung ein.

Danach beginnt man vielleicht nach Wegen zu suchen, die Rechenleistung zu steigern. Dabei bringen Mehrprozessorsysteme leider wenig, denn nur ein SETI-Client kann pro Rechner aktiv sein. Bei einer Maschine mit zwei CPUs wird lediglich die Grafikarbeit auf den zweiten Prozessor verlagert, bei vier Prozessoren sinkt die Gesamtauslastung der CPUs auf unter 40 Prozent.

Eine Alternative bietet der Text-Client, von dem Instanzen gleichzeitig auf einem Rechner laufen können. Hierzu muss lediglich jeder in seinem eigenen Verzeichnis gestartet werden. Je nach Plattform kann man dann entweder die CPU-Affinität fix setzen (vorteilhaft, wenn nebenbei noch etwas ‘Richtiges’ zu rechnen ist) oder die Lastverteilung dem Betriebssystem überlassen. Die Beschreibung einer mittelgroßen Beispielkonfiguration enthält der Kasten auf Seite 173.

Mehr Infos

Beispielinstallation

SETI-Clients waren auf einigen Mehrprozessorrechnern zu installieren (jeweils ein Client pro CPU) und zentral auszuwerten. Dazu wurde auf einem Fileserver ein SETI-Fileshare angelegt. Die Installationsskripte erzeugen bei Ausführung auf dem entsprechenden Rechner ein Verzeichnis ‘Servername’ auf dem Fileserver. Dort bekam jede CPU im Unterverzeichnis seti ein Unterverzeichnis cpu<X>. Dann wurden die SETI-Startskripte für den betreffenden Rechner erzeugt, wobei die Werte für Proxy-Einstellungen und den SETI-Benutzer-Account aus globalen Vorlagen stammten. So entstand eine zentrale Verzeichnishierarchie für alle SETI-Prozesse auf dem Fileserver, die ein zentrales Monitoring vereinfacht.

Leider bietet der Text-Client nur eine eingeschränkte Prozessüberwachung: Sie gibt Rechenfortschritt und FFT-Daten aus, andere Parameter, wie gefundene Signalstärke und vor allem CPU-Zeit, fehlen. Insbesondere, wenn mehrere Mehrprozessorrechner als Clients arbeiten - unter Nutzung eines gemeinsamen SETI-Account - geht schnell der Überblick verloren, und die Motivation sinkt. Ein SETI-Monitor für eine Mehrprozessumgebung war also gefordert.

Alle SETI-Clients schreiben Statusinformationen in diverse Textdateien. Für eine zentrale Überwachung liest der Monitor die Parameter aus diesen Dateien. Daraus kann er weitere Werte berechnen, zum Beispiel die verbleibende Rechenzeit pro Datenpaket. Schließlich stellt er diese Daten in tabellarischer Form dar, zusätzlich ist eine Sortierung nach verschiedenen Parametern möglich. Perl-Skripte erledigen diese Aufgaben mit wenig Aufwand. Zur Darstellung dient HTML mit CGI, was Plattformunabhängigkeit garantiert und Einsatz auf beliebigen Rechnern ohne Zusatzsoftware ermöglicht.

Grundsätzlich besteht die Überwachung aus zwei Phasen:

  • Zuerst sammelt ein Parser die in den Statusdateien verteilt vorliegenden Daten ein und konsolidiert sie in einer einzelnen Datei. Ein Scheduler startet ihn regelmäßig und hält so die Prozessdaten aktuell.
  • Aufbereitung und Darstellung der Informationen erledigt das HTML/CGI-Frontend. Es erzeugt einen HTML-Header, der für eine Aktualisierung in kurzen Abständen sorgt. Ein Echtzeitbetrieb, das heißt direktes Analysieren der Statusdateien innerhalb des CGI-Skriptes, wurde aus Performancegründen verworfen. Außerdem würden die Daten im Mehrbenutzerbetrieb häufiger analysiert als nötig, da der Monitor sie nicht zwischenspeichert.

Das Skript setiparsel.pl (Listing 1) durchsucht die gesamte SETI-Hierarchie auf dem Dateiserver und sammelt die so erhaltenen Prozessinformationen in nodesdata.txt. Dazu durchsucht das Programm den übergegebenen Verzeichnisbaum nach Vorkommnissen der Statusdateien user_info.txt und state.txt. Beide sind genau einmal für jeden aktiven SETI-Prozess vorhanden. Das Skript erwartet sie in einer Verzeichnishierarchie, die der Kasten ‘Beispielinstallation’ beschreibt.

Mehr Infos

Listing 1: setiparse.pl

setiparse.pl liest die Statusdateien aller laufenden SETI-Prozesse und erzeugt daraus eine aktuelle Datei nodesdata.txt.

  1 #!/usr/bin/perl
2 use strict;
3 use diagnostics;
4 use CGI qw(:standard);
5 use Time::localtime;
6 use File::Find;
...
49 #browse all files in directory tree and build a list of
50 #all relevant files
51 find (\&wanted, "$base_dir");
52
53 sub wanted {
54 my $name = $File::Find::name;
55 # machine cpu file
56 if ($name =~ m|/seti/(.+)/seti/(.+)/(user_info.txt)|) {
57 #found user info, update global values (totals)
58 $total_work_processes++;
59
60 open (TMPFILE, $3) || die "can't open $3: $!";
...
64 join("", <TMPFILE>) =~
65 /name=(.+)\s
66 url.*nwus=(\d+)\s
67 nresults=(\d+)\s
68 total_cpu=(\d+)/xs;
69 close (TMPFILE);
70 #set total values to bigger value
71 $total_work_units = $2
72 if ($2 > $total_work_units);
73 $total_results=$3
74 if ($3 > $total_results);
75 $total_cpu_time=$4
76 if ($4 > $total_cpu_time);
...
79 } elsif ($name =~ m|/seti/(.+)/seti/(.+)/(state.txt)|) {
80 $tmp_node_name = "$1-$2";
...
88 $tmpstr =~ /fl=(\d+).*
89 cpu=(\d+\.\d+).*
90 prog=(\d+\.\d+).*
91 bs_power=(\d+\.\d+).*
92 bs_score=(\d+\.\d+)/sx;
...
96 push (@nodes_data, {
97 "node" => $tmp_node_name,
98 "fl" => $1,
99 "cpu" => $2,
100 "prog" => $3,
101 "peak" => $4
102 }
103 );
104 } #if
105 } #sub wanted
...
111 if (open(SETI_DATA_FILE, "<$nodes_file_name")) {
112 #read line by line, and extract process info values
113 while (<SETI_DATA_FILE>) {
114 #keep only node entries, throw away old entries!!
115 if ( /^NODE:/) {
116 s/NODE/OLD/;
117 $tmpstr .= $_;
118 }
119 }
...
132 print SETI_DATA_FILE $tmpstr;
133
134 #print the new data
135 foreach $rtmp_node (@nodes_data) {
136 # node
137 # progress
138 # cputime
139 # flops
140 # peak
141 printf SETI_DATA_FILE ("NODE: %s %s %s %s %s\n",
142 $$rtmp_node{"node"},
143 $$rtmp_node{"prog"},
144 $$rtmp_node{"cpu"},
145 $$rtmp_node{"fl"},
146 $$rtmp_node{"peak"}
147 );
148 } #foreach $entry
...

find() (Zeile 51) aus dem Perl-Modul File::find erledigt die Dateisuche. Neben dem Basisverzeichnis bekommt es als Parameter eine Referenz auf wanted() übergeben. Diese Funktion führt find() für jede gefundene Datei aus.

In Zeile 53 beginnt wanted(). Bei seinem Aufruf enthält $File::Find::name den kompletten Dateinamen der aktuell von find() ermittelten Datei. Zuerst prüft die Routine, ob es sich um user_info.txt handelt, öffnet dann die Datei, liest sie komplett und extrahiert die relevanten Parameter mit einem regulären Ausdruck (Zeile 64). Ein Beispiel für user_info.txt zeigt Listing 2. Bei diesen Werten handelt es sich jeweils um die Summe für alle Prozesse des Benutzers aus der Datenbank des SETI@Home-Servers, die er bei jedem Verbindungsaufbau aktualisiert. Die Maxima über alle SETI-Prozesse stellen also die aktuellen Werte dar. setiparse.pl setzt ab Zeile 66 die Globalwerte entsprechend auf das jeweilige Maximum.

Mehr Infos

Listing 2: Auszüge

Für jeden Prozess speichert SETI Benutzer- und Zustandsdaten in zwei Dateien.

  ### Auszug aus user_info.txt 
country=Germany
postal_code=
show_name=yes
show_email=yes
venue=2
register_time= 2451386.11473 (Mon Jul 26 14:45:12 1999)
last_wu_time= 2451400.63628 (Tue Aug 10 03:16:14 1999)
last_result_time= 2451400.63728 (Tue Aug 10
03:17:40 1999)
nwus=914
nresults=787
total_cpu=35925595.519580
params_index=0

### Auszug aus state.txt
ncfft=568
cr=7.708326e-001
fl=131072
cpu=4014.875000
prog=0.063572
bs_power=156.350021
bs_score=0.592038
bs_bin=65478

Handelt es sich bei der zu überprüfenden Datei um state.txt, kann man die interessierenden SETI-Prozessdaten auslesen. Der reguläre Ausdruck in Zeile 80 prüft auf state.txt und konstruiert als Erstes aus dem Pfadnamen einen Bezeichner für den SETI-Prozess. Dafür benutzt er den Rechnernamen und den CPU-Bezeichner: Aus seti/grimsel/seti/cpu0/state.txt entsteht grimsel-cpu0 als Prozessname. Dieser wird später in der Tabelle erscheinen. Als Begrenzer dient im regulären Ausdruck für den Pfadnamen der besseren Übersichtlichkeit wegen ‘|’ statt des sonst üblichen ‘/’. Als nächstes liest wanted() die Datei in einem Rutsch und extrahiert die Parameter fl, cpu, prog, bs_power und bs_score. Aus ihnen und dem Prozessnamen erzeugt das Skript einen anonymen Hash (ab Zeile 96) und speichert eine Referenz auf ihn im Array @nodes_data. Wenn File::find die komplette Verzeichnisstruktur durchsucht hat, referenziert jeder Eintrag in diesem Array die Daten eines einzelnen Prozesses.

Ab Zeile 141 schreibt das Skript diese Daten zeilenweise in nodesdata.txt. Jede Zeile bekommt den Präfix ‘NODE’. Am Ende stehen die globalen Daten und die aktuelle Zeit mit den Präfixen ‘TOTALS’ beziehungsweise ‘DATAupdate’. Da die bisher extrahierten Informationen lediglich eine Momentaufnahme darstellen, ist eine Methode zum Halten von Prozessinformationen über mehrere setiparse-Durchgänge nötig: Existiert nodesdata.txt, liest das Skript nur Zeilen mit dem Präfix ‘NODE’. Dieses ersetzt es durch ‘OLD’ und schreibt die Zeilen an den Anfang einer neuen nodesdata.txt. Der Monitor setimon.pl kann diese Werte dazu benutzen, Veränderungen gegenüber dem letzten Durchgang zu ermitteln und so zum Beispiel gestoppte Prozesse erkennen.

Setimon.pl (Listing 3) gibt die in nodesdata.txt gesammelten Informationen als HTML-Seite aus. Es bietet das Sortieren zum Beispiel nach Prozessname oder Restlaufzeit an. Die Spaltenüberschriften der Tabelle sind selbstreferenzierende URLs mit Angabe des entsprechenden Sortierparameters.

Mehr Infos

Listing 3: setimon.pl

Das CGI-Script setimon.pl stellt die von setiparse.pl erzeugten Daten als Tabelle dar.

  1 #!/usr/bin/perl
2 use strict;
3 use diagnostics;
4 use CGI qw(:standard);
5 use Time::localtime;
6 use File::Find;
...
64 #create CGI object
65 $cgi_dataset = CGI->new();
...
72 while (<SETI_DATA_FILE>) {
73 my $ignore;
74
75 # node progress cputime flops peak
76 if (/^OLD/) {
...
80 $node=(split())[1];
81 $old_node_data{$node} = $_;
82 } elsif (/^NODE/) {
...
85 ($tmp_node, $tmp_prog,
86 $tmp_cpu, $tmp_fl, $tmp_peak) = (split())[1,2,3,4,5];
...
98 $tmp_old_prog = 0;
99 $tmp_old_cpu = 0;
100 if ( $tmpstr = $old_node_data{$tmp_node} ) {
101 ($tmp_old_prog,$tmp_old_cpu) = (split(/\s/,$tmpstr))[2,3];
102 }
...
106 push (@nodes_data, {
107 "node" => $tmp_node,
108 "prog" => $tmp_prog,
...
116 }
117 );
118 } elsif (/^TOTALS:/) {
...
125 }
126 }
...
133 $sort_type = $cgi_dataset->param("sort") || "node" ;
...
139 my %sort_fct = ( 'node' =>
140 sub { $$a{"node"} cmp $$b{"node"} },
141 'prog_asc' =>
142 sub { $$a{"prog"} <=> $$b{"prog"} },
143 'prog_desc' =>
144 sub { $$b{"prog"} <=> $$a{"prog"} },
145 'cpu_asc' =>
146 sub { $$a{"cpu"} <=> $$b{"cpu"} },
147 'cpu_desc' =>
148 sub { $$b{"cpu"} <=> $$a{"cpu"} },
...
159 );
160
161 my $fct = exists $sort_fct{$sort_type} ?
162 $sort_fct{$sort_type} : $sort_fct{"node"};
163
164 @nodes_data = sort { &$fct } @nodes_data;
...
180 my $bgcolor='"#006666"'; # background color
181 my $ftcolor='"#FFFFFF"'; # font color white
182
...
210 #create href links for table headers (with sort param set and color)
211 my %task_table_headers;
212 my $smallfont = sub {
213 return "<FONT COLOR=$ftcolor SIZE=\"-2\">$_[0]</FONT>";
214 };
215
216 my $url = $cgi_dataset->url();
217 $url .="?sort=";
218 %task_table_headers = (
219 'node',
220 a( {-href=>$url . "node"},
221 "<FONT COLOR=$ftcolor>NODE</FONT>"
222 ),
223 'prog',
224 "% Done<br>".
225 a( {-href=> $url ."prog_asc"},
226 &$smallfont("0..100")
227 ).
228 &$smallfont("&nbsp;&nbsp;").
229 a( {-href=> $url . "prog_desc"},
230 &$smallfont("100..0")
231 ),
...
266 my $head;
267
268 foreach ('node','prog','per_hour','cpu_time','time_left','peak') {
269 $head .= "<th valign=\"top\" bgcolor=$bgcolor\n" .
270 "<FONT COLOR=$ftcolor> $task_table_headers{$_}</FONT></th>\n";
271 } # foreach
...
275 print "<table border=1>\n<tr>$head</tr>\n";
...
283 foreach $rtmp_node (@nodes_data) {
284 print "<tr>";
...
302 printf ("<td>$tmp_delta_icon &nbsp; %s</td>\n ".
303 "<td align=right>%.3f <SUB>(+%.3f)</SUB> &nbsp;</td>\n".
304 "<td align=right>%.2f &nbsp;</td>\n ".
305 "<td align=right>%s &nbsp;</td>\n ".
306 "<td align=right>%s &nbsp;</td>\n ".
307 "<td align=right>%.2f</td>\n",
308 $$rtmp_node{"node"},
309 $$rtmp_node{"prog"}*100,
...
315 );
316 print "</tr>\n\n";
317 } #foreach $entry
...

Zuerst initialisiert das Skript in Zeile 65 ein CGI-Objekt $cgi_dataset. Sieben Zeilen später liest es die Prozessdaten von nodesdata.txt und speichert alle mit ‘OLD’ beginnenden Zeilen im Hash %old_node_data (Zeile 76 bis 81). Dazu benutzt es split und indiziert direkt in die von dieser Funktion gelieferte Liste - das spart kompliziertere reguläre Ausdrücke. Beginnt der Eintrag in nodesdata.txt mit ‘NODE’, extrahiert der Code ab Zeile 85 die dazugehörigen Daten. Vor deren Speichern in @nodes_data ist der zugehörige Satz von Altdaten zu besorgen. Er steht im Hash %old_node_data mit den Prozessdaten als Key und kann daraus einfach abgefragt werden. Zeile 101 fischt die Parameter wieder mit split und Indizierung heraus. Schließlich erzeugt das Skript einen Eintrag für den gerade bearbeiteten SETI-Prozess, indem es in @nodes_data einen anonymen Hash speichert.

Der letzte Schritt besteht im Sortieren dieses Arrays für die Ausgabe als (HTML)-Tabelle. Das Sortierverhalten legt der CGI-Parameter sort fest, der in der URL übergeben und in Zeile 133 ausgelesen wird. Fehlt er, sortiert das Skript nach Prozessnamen. Das Sortieren erledigt das Script mit einer anonymen Subroutine, die der Schlüssel sort aus dem Hash %sort_fctextrahiert. Diese Methode ist eleganter und flexibler als ein längliches if...elsif-Konstrukt. Die jeweilige Subroutine dereferenziert die Zeiger aus @nodes_data mittels $$a{$key}. Numerische Werte wie die CPU-Zeit vergleicht der ‘Spaceship’-Operator <=> (Zeile 144), alphanumerische Werte der cmp-Operator (Zeile 140). Weitere Hinweise zum Thema Sortieren sind in [4] zu finden.

Zur Ausgabe der jetzt aufbereiteten Prozessdaten dient das Perl-Modul CGI. Es übernimmt den größten Teil der HTML-Generierung, beispielsweise des HTTP-Headers. Danach beginnt der Aufbau der SETI-Prozesstabelle mit den URLs für die Spaltenüberschriften: Sie sind mit den Sortierparametern besetzt, so dass zum Beispiel ein Klick auf die Spaltenüberschrift ‘Nodes’ das Skript erneut mit dem Parameter ‘?sort=node’ aufruft. Um den Code übersichtlicher zu gestalten, stehen diese URLs im Hash %task_table_headers mit den Spaltennamen als Key und dem kompletten HTML-Code für die Spaltenüberschrift als Wert (ab Zeile 218). Die Methode url() liefert die URL des aktuellen Aufrufes. An diese wird dann der Sortierparameter angehängt und daraus die URL erstellt.

Ab Zeile 302 schreibt printf die sortierten Daten aller SETI-Prozesse Zeile für Zeile heraus. Für jeden Prozess errechnet das Skript die Veränderung in Prozent gegenüber dem letzten Durchlauf. Solche, die keinen Fortschritt zeigen, erhalten ein rotes Icon, alle anderen ein grünes.

Die Installation des kompletten SETI-Monitors gestaltet sich relativ einfach:

  • Parser setiparse.pl mittels Scheduler (cron oder ähnliches) konfigurieren, sodass es zum Beispiel alle 3 Minuten startet und nodesdata.txt aktuell hält.
  • Webserver für das CGI-Skript setimon.pl konfigurieren.

Es empfiehlt sich, Parser und Monitor in getrennten Verzeichnissen zu halten und die Zugriffsrechte auf nodesdata.txt so zu setzen, dass der entsprechende Anwender (zum Beispiel wwwuser oder IUSR_WWW) Leserechte hat.

Steffen Merunka
arbeitet als Technical Consultant bei Compaq EMEA, München, im SAP International Competence Center.

[1] SETI-Clients

[2] SETI-Statistiken

[3] The SETI@Home Sky Survey. Technical Paper.

[4] http://w3.stonehenge.com/merlyn/UnixReview/col06.html (ck)