Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort
Geleitwort des Fachgutachters
1 Einführung
2 Mathematische und technische Grundlagen
3 Hardware
4 Netzwerkgrundlagen
5 Betriebssystemgrundlagen
6 Windows
7 Linux und UNIX
8 Grundlagen der Programmierung
9 Konzepte der Programmierung
10 Software-Engineering
11 Datenbanken
12 Server für Webanwendungen
13 Weitere Internet-Serverdienste
14 XML
15 Weitere Datei- und Datenformate
16 Webseitenerstellung mit (X)HTML und CSS
17 Webserveranwendungen
18 JavaScript und Ajax
19 Computer- und Netzwerksicherheit
A Glossar
B Zweisprachige Wortliste
C Kommentiertes Literatur- und Linkverzeichnis
Stichwort

Download:
- ZIP, ca. 6,6 MB
Buch bestellen
Ihre Meinung?

Spacer
 <<   zurück
IT-Handbuch für Fachinformatiker von Sascha Kersken
Der Ausbildungsbegleiter
Buch: IT-Handbuch für Fachinformatiker

IT-Handbuch für Fachinformatiker
3., aktualisierte und erweiterte Auflage
1014 S., 34,90 Euro
Galileo Computing
ISBN 978-3-8362-1015-7
gp 9 Konzepte der Programmierung
  gp 9.1 Algorithmen und Datenstrukturen
    gp 9.1.1 Ein einfaches Praxisbeispiel
    gp 9.1.2 Sortier-Algorithmen
    gp 9.1.3 Such-Algorithmen
    gp 9.1.4 Ausgewählte Datenstrukturen
  gp 9.2 Reguläre Ausdrücke
    gp 9.2.1 Muster für reguläre Ausdrücke
    gp 9.2.2 Programmierung mit regulären Ausdrücken
  gp 9.3 Systemnahe Programmierung
    gp 9.3.1 Prozesse und Pipes
    gp 9.3.2 Threads
  gp 9.4 Einführung in die Netzwerkprogrammierung
    gp 9.4.1 Die Berkeley Socket API
    gp 9.4.2 Ein praktisches Beispiel
    gp 9.4.3 Ein Ruby-Webserver
  gp 9.5 Verteilte Anwendungen mit J2EE
    gp 9.5.1 Enterprise Java Beans (EJB)
    gp 9.5.2 Java Servlets
    gp 9.5.3 Web Services
  gp 9.6 GUI- und Grafikprogrammierung
    gp 9.6.1 Zeichnungen und Grafiken erstellen
    gp 9.6.2 Animation
    gp 9.6.3 Programmierung fensterbasierter Anwendungen
    gp 9.6.4 Java-Applets
  gp 9.7 Die Entwicklungsumgebung Eclipse
    gp 9.7.1 Überblick
    gp 9.7.2 Java-Entwicklung mit Eclipse
  gp 9.8 Zusammenfassung


Galileo Computing

9.3 Systemnahe Programmierung  downtop

Einige Programmieraufgaben erfordern Zugriffe auf Funktionen, die das Betriebssystem bereitstellt. In UNIX-Systemen werden solche Anweisungen als Systemaufrufe (system calls) bezeichnet. In diesem Abschnitt werden einige der wichtigsten Aspekte der systemnahen Programmierung behandelt: die gleichzeitige Ausführung mehrerer Aufgaben, die sogenannte Nebenläufigkeit, durch Prozesse und Threads sowie die Kommunikation zwischen ihnen.


Galileo Computing

9.3.1 Prozesse und Pipes  downtop

Das Prinzip des Prozesses wurde bereits in Kapitel 5, Betriebssystemgrundlagen, angesprochen: Jedes unter einem modernen Betriebssystem laufende Programm wird als separater Prozess oder Task ausgeführt, der seinen eigenen Speicherbereich und seine eigenen Ein- und Ausgabeschnittstellen besitzt. Threads dagegen stellen eine einfache Möglichkeit zur Verfügung, innerhalb desselben Prozesses mehrere Aufgaben parallel zu erledigen. In diesem Unterabschnitt werden beide Verfahren kurz praktisch vorgestellt.

Das Erzeugen neuer Prozesse ist das herkömmliche Verfahren, um in UNIX-Programmen mehrere parallele Verarbeitungseinheiten zu realisieren: Zwei verschiedene Aufgaben müssen nicht aufeinander warten, um ausgeführt zu werden. Dies ermöglicht beispielsweise die effektive Nutzung von Wartezeiten, die durch die verhältnismäßig langsamen I/O-Operationen entstehen können.

fork( )

Das UNIX-Prozessmodell erzeugt einen neuen Prozess durch den Systemaufruf fork(), der eine absolut identische Kopie des ursprünglichen Prozesses erzeugt. Jede Codezeile, die hinter einem Aufruf von fork() steht, wird in nicht vorhersagbarer Reihenfolge doppelt ausgeführt. Da der ursprüngliche Prozess (Parent-Prozess) in der Regel etwas anderes tun soll als der neu erzeugte (Child-Prozess), müssen diese beiden irgendwie voneinander unterschieden werden. In diesem Zusammenhang ist es nützlich, dass fork() im Parent-Prozess die Prozess-ID (PID) des Child-Prozesses zurückgibt und im Child-Prozess 0. Folglich sehen praktisch alle fork()-Aufrufe in C-Programmen unter UNIX schematisch so aus:

int f; 
... 
if (f = fork()) { 
   ... 
   /* Parent-Prozess: 
      Hier werden die Parent-Aufgaben erledigt; 
      f enthält PID des Childs.               */ 
} else { 
   ... 
   /* Child-Prozess: 
      Hier werden die Child-Aufgaben erledigt; 
      getppid() liefert PID des Parents.      */ 
}

Um die Forking-Funktionalität in C-Programmen verwenden zu können, müssen Sie die Header-Datei sys/types.h einbinden:

#include <sys/types.h>

Unter Windows wird dieses Verfahren von Hause aus nicht unterstützt; die Win32-API definiert stattdessen eine Funktion namens CreateProcess(), die einen neuen, leeren Prozess erzeugt. In neuen Windows-Versionen von Perl wird trotzdem eine UNIX-kompatible fork()-Funktion angeboten. Aufruf und Funktionalität von fork() sind in Perl übrigens mit C identisch.

Das folgende Beispiel zeigt ein kleines Perl-Programm, das im Parent- und im Child-Prozess jeweils eine Schleife von 1 bis 10.000 durchzählt und anzeigt. Im Parent-Prozess werden die Werte ein wenig eingerückt:

#!/usr/bin/perl 
if (fork()) { 
   # Parent-Prozess 
   for ($i = 1; $i <= 10000; $i++) { 
      print "     $i\n"; 
   } 
} else { 
   # Child-Prozess 
   for ($i = 1; $i <= 10000; $i++) { 
      print "$i\n"; 
   } 
}

Wenn Sie das Programm ausführen, werden Sie den Wechsel zwischen ein- und ausgerückten Zahlen bemerken. Sie können die Ausgabe auch genauer untersuchen, indem Sie sie mittels >Dateiname in eine Datei umleiten.

Natürlich ist dieses Beispiel nicht besonders sinnvoll. Zu den bedeutendsten Aufgaben von fork() gehört die Implementierung von Netzwerkservern, die mit mehreren Clients zur gleichen Zeit kommunizieren. Beispiele finden Sie im nächsten Abschnitt.

Neben fork() gibt es noch einige weitere Anweisungen, die im Zusammenhang mit Prozessen nützlich sind:

  • exec(), verfügbar in C und Perl, führt das Programm aus, dessen Pfad als Argument angegeben wurde, und kehrt nicht zurück – nachdem das Programm beendet ist, wird auch der Prozess beendet. Dies ist nützlich, um einen mittels fork() erzeugten Prozess mit der Ausführung eines externen Programms zu beauftragen.
  • system(), ebenfalls in C und Perl einsetzbar, führt das als Argument angegebene Programm aus und kehrt anschließend zurück. Dies wird vorzugsweise für den Aufruf von Systembefehlen genutzt. Beispielsweise können Sie sich in einem UNIX-Programm folgendermaßen den Inhalt des aktuellen Verzeichnisses anzeigen lassen:
system ("ls");
    • Unter Windows muss die Anweisung natürlich modifiziert werden, weil der Befehl für die Verzeichnisanzeige hier anders lautet:
system ("dir");
  • Der Backtick-Operator (der Pfad eines Programms in ``) ist nur in Perl (und diversen UNIX-Shells) verfügbar und liefert die Ausgabe des aufgerufenen externen Programms als Wert zurück. Es ist ein wenig umständlich, das Zeichen ` zu erzeugen: Da es sich um ein Akzentzeichen handelt, müssen Sie zunächst Keyboard Shift + Keyboard ' drücken und anschließend die Keyboard Leertaste . [Wenn Sie üblicherweise keine Akzentzeichen, aber häufig Sonderzeichen wie ^ oder den Backtick benötigen, können Sie unter Linux ein Tastaturlayout mit der Variante nodeadkeys einstellen, die das Akzentverhalten solcher Tasten abschaltet. ] Backticks sind extrem nützlich, um die Ausgabe eines Befehls genau zu analysieren – besonders in Zusammenarbeit mit Perls RegExp-Fähigkeiten.
    • Der folgende Code gibt auf einem UNIX-System beispielsweise sämtliche Dateien des ausführlichen Inhaltsverzeichnisses zurück, in denen der Dateiname (letzter Posten in der Zeile) den Buchstaben a enthält:
$dir = `ls -l`; 
@dirs = split (/\n/, $dir); 
foreach $d(@dirs) { 
   print "$d\n" if $d =~ /a[^\s]+$/; 
}

Kommunikation zwischen Prozessen durch Pipes

Mithilfe von fork() können Sie zwar beliebig viele Prozesse erzeugen, aber diese Prozesse laufen völlig beziehungslos nebeneinander. In der bisher vorgestellten Form sind sie also nicht dafür geeignet, mehrgliedrige Aufgaben kooperativ zu erledigen. Da Programme und Tasks in der Praxis jedoch auf vielfältige Weise miteinander interagieren müssen, sind Mittel zur Kommunikation zwischen den verschiedenen Prozessen erforderlich. In Kapitel 5, Betriebssystemgrundlagen, wurde bereits angedeutet, dass es vielerlei Möglichkeiten dafür gibt, unter anderem Shared Memory, Signale, Semaphoren oder Pipes. Als praktisches Beispiel wird hier die Verwendung von Pipes angesprochen.

Pipes in Perl-Programmen

Die Verwendung von Pipes zur Kommunikation wird hier am Beispiel von Perl demonstriert, da sie in diesem Fall besonders leicht und effizient vonstatten geht. Im Grunde gibt es keinen Unterschied zwischen der Anwendung einer Pipe auf der Kommandozeile und aus einem Perl-Programm heraus. In einem Perl-Programm erzeugen Sie eine Pipe zu einem anderen Programm, indem Sie mittels open ein Dateihandle öffnen und statt des üblichen Dateinamens ein lauffähiges Programm angeben. Ein Pipe-Symbol vor dem Programmnamen ("|Programm") bedeutet dabei, dass Sie die Ausgabe Ihres Programms an dieses Programm weitergeben möchten, während ein dahinter stehendes Pipe-Symbol es Ihnen ermöglicht, zeilenweise aus der Ausgabe des externen Programms zu lesen.

Beispielsweise können Sie die Ausgabe Ihres Programms folgendermaßen an den Linux-Pager less weiterleiten (unter Windows könnten Sie stattdessen "|more" schreiben), um Daten bildschirmweise auszugeben:

open (AUSG, "|less"); 
# Ausgabebefehl: 
print AUSG "Text...";

Auch die Eingabe aus einer Pipe ist unproblematisch. Sie können zum Beispiel folgendermaßen zeilenweise aus der UNIX-Prozesstabelle lesen:

open (EING, "ps aux|"); 
while ($prozess = <EING>) { 
   # ... Zeile verarbeiten 
}

Der Systemaufruf pipe( )

Eine bequemere Möglichkeit, eine Pipe für die Kommunikation zwischen zwei Prozessen innerhalb Ihres Programms einzusetzen, bietet der Systemaufruf pipe(). Die gleichnamige Perl-Anweisung erzeugt zwei durch eine Pipe verbundene Dateihandles, deren Namen Sie durch Übergabe entsprechender Argumente festlegen können. Das erste Argument gibt das lesende, das zweite das schreibende Dateihandle an:

pipe (READER, WRITER);

In dem nachfolgenden kleinen Programm erzeugt der Parent-Prozess in einer for-Schleife eine Reihe von Zahlen, die in das Schreibhandle geschrieben werden. Der Child-Prozess liest sie aus dem zugehörigen Lesehandle und berechnet jeweils ihr Quadrat. Besonders sinnvoll ist diese Anwendung nicht, demonstriert aber das Prinzip:

#/usr/bin/perl -w 
use strict; 
pipe (READER, WRITER); 
my $child = fork(); 
if ($child) { 
   # Parent-Prozess liest nur; WRITER schließen 
   close WRITER; 
   while (my $line = <READER>) { 
      chomp $line; 
      print ("Das Quadrat von $line ist ". 
           $line * $line. "\n"); 
   } 
} else { 
   # Child-Prozess schreibt nur; READER schließen 
   close READER; 
   for (my $i = 0; $i < 100; $i++) { 
      print "Bin bei $i\n"; 
      print WRITER "$i\n"; 
   } 
}

Wie Sie bemerken, schließt jeder der beiden Prozesse zu Beginn eines der beiden Pipe-Handles. Es würde auch nichts nützen, das nicht zu tun, da das Pipe-Handle nur in eine Richtung funktioniert, für die Sie sich von vornherein entscheiden müssen. Im vorliegenden Fall behält der Child-Prozess das schreibende Dateihandle offen, der Parent-Prozess das lesende. Wenn die Kommunikation in zwei Richtungen stattfinden soll, müssen Sie über einen zweiten pipe()-Aufruf ein weiteres Paar von Dateihandles erzeugen.


Galileo Computing

9.3.2 Threads  toptop

Eine ähnliche Technik wie das Erzeugen mehrerer Prozesse ist das Erzeugen mehrerer Threads innerhalb eines Prozesses. Genau wie das Betriebssystem dafür sorgt, dass die verschiedenen Prozesse abwechselnd abgearbeitet werden, findet auch die Verarbeitung der Threads im Wechsel statt; in den meisten Fällen kann wie bei Prozessen eine Priorität gewählt werden. Der Hauptunterschied zwischen Prozessen und Threads besteht darin, dass Threads im selben Speicherraum laufen und gemeinsamen Zugriff auf Ressourcen besitzen.

Inzwischen unterstützen alle modernen Betriebssysteme Threads, unter Umständen wird diese Unterstützung auch von der Bibliothek der verwendeten Programmiersprache statt vom System selbst zur Verfügung gestellt. Allerdings ist das Programmieren von Multithreading-Anwendungen nicht in allen Sprachen einfach, weil das Verfahren noch nicht in allen Sprachen standardisiert ist.

Threads in Java

Am praktischsten lässt sich die Programmierung von Threads am Beispiel von Java erläutern, weil die Thread-Funktionalität in dieser Sprache von Anfang an verankert wurde. Sie wird im Wesentlichen durch die Klasse java.lang.Thread bereitgestellt, die einen einzelnen lauffähigen Thread repräsentiert.

Wenn eine Java-Instanz als Thread ausgeführt werden soll, muss die zugrunde liegende Klasse eine der beiden folgenden Bedingungen erfüllen:

  • Sie muss von java.lang.Thread abgeleitet sein. Innerhalb dieser Klasse können Sie die Methode run() überschreiben, die die eigentlichen Anweisungen enthält, die als Thread ausgeführt werden sollen.
  • Alternativ kann sie das Interface Runnable implementieren. Zu diesem Zweck muss sie ebenfalls eine Methode namens run() bereitstellen.

Wenn Sie eine Klasse von Thread ableiten, können Sie eine Instanz davon erzeugen; ein Aufruf ihrer Methode start() beginnt mit der Ausführung der Anweisungen in run(). Eine Instanz einer Klasse, die Runnable implementiert, wird dagegen als Argument an den Konstruktor der Klasse Thread übergeben. Auch dieser neue Thread wird mittels start() gestartet. Letzteres ist besonders nützlich, falls Sie innerhalb des aktuellen Programms einen zweiten Thread starten möchten, ohne eine externe Instanz zu verwenden: Sie können Runnable einfach durch Ihr aktuelles Programm implementieren.

In beiden Fällen läuft das aktuelle Hauptprogramm ebenfalls als Thread weiter. Dass jedes Java-Programm automatisch ein Thread ist, können Sie besonders gut an Fehlermeldungen bemerken. Diese lauten häufig derart, dass ein Fehler im Thread main aufgetreten sei.

Das folgende Beispiel zeigt eine von Thread abgeleitete Klasse namens BGSearcher, die im Hintergrund ein als Argument übergebenes Array von int-Werten linear nach einem ebenfalls übergebenen Wert durchsucht. Die Klasse BGSearchTest ist ein Testprogramm für BGSearcher, das ein Array mit Zufallswerten füllt und an den Hintergrundsucher übergibt. Schließlich benötigen Sie noch das Interface SearchInfo, das von einer Klasse, die den BGSearcher verwenden möchte, implementiert werden muss. Tippen Sie alle drei Klassen ab und speichern Sie sie in entsprechenden .java-Dateien im gleichen Verzeichnis. Es genügt, anschließend die Datei BGSearchTest.java zu kompilieren – die beiden anderen werden automatisch mitkompiliert.

public class BGSearcher extends Thread { 
   private int[] liste; 
   private int wert; 
   private SearchInfo info; 
 
   public BGSearcher (int[] l, int w, SearchInfo n) { 
      this.liste = l; 
      this.wert = w; 
      this.info = n; 
   } 
 
   public void run () { 
      for (int i = 0; i < liste.length; i++) { 
         if (liste[i] == wert) 
            info.searchinfo (i); 
      } 
   } 
} 
 
public class BGSearchTest implements SearchInfo { 
   public static void main (String args[]) { 
      int werte[] = new int[10000]; 
      System.out.println ("Erzeuge Zufallswerte!"); 
      for (int i = 0; i < 10000; i++) { 
         werte[i] = (int)(Math.random() * 10) + 1; 
      } 
      // Programm-Instanz zur Übergabe an BGSearcher: 
      BGSearchTest test = new BGSearchTest(); 
      // Sucher erzeugen und aufrufen 
      BGSearcher searcher = 
           new BGSearcher (werte, 4, test); 
      searcher.start(); 
      // Eigenen Aufgaben nachgehen 
      for (int i = 0; i < 1000; i++) { 
         System.out.println ("Bin jetzt bei " + i); 
      } 
   } 
 
   public void searchinfo (int pos) { 
      System.err.println ("Gefunden bei " + pos); 
   } 
} 
 
public interface SearchInfo { 
   public void searchinfo (int pos); 
}

Auch für dieses Beispiel sind wieder einige Erklärungen erforderlich:

  • Die Klasse BGSearcher ist von Thread abgeleitet, damit die Methode run() in einem Thread laufen kann. Die Methode start(), die ein Programm aufrufen muss, damit eine Instanz von BGSearcher mit der Arbeit beginnt, muss hier nicht explizit definiert werden; sie wird von Thread geerbt.
  • Der Konstruktor von BGSearcher erwartet drei Argumente: das zu durchsuchende int-Array, den gesuchten Wert und eine Instanz von SearchInfo. Letzteres kann eine Instanz einer beliebigen Klasse sein, die das Interface SearchInfo implementiert – in der Regel, wie im vorliegenden Fall, das Programm, das die Hintergrundsuche einsetzt.

Callback-Methoden

    • Die Übergabe der SearchInfo-Instanz ist notwendig, damit BGSearcher jedes Mal eine Methode des implementierenden Programms aufrufen kann, wenn der gesuchte Wert im Array gefunden wird. Eine solche Methode, die Sie in einem eigenen Programm bereitstellen, damit sie bei Bedarf von außen aufgerufen wird, heißt Callback-Methode. Der Einsatz von Callbacks ist immer dann sinnvoll, wenn Sie ein externes, asynchron auftretendes Ereignis (hier zum Beispiel einen Sucherfolg) in Ihrem eigenen Code verarbeiten möchten.
  • Die Klassendefinition des Testprogramms BGSearchTest enthält die Klausel implements SearchInfo. Mit dieser Klausel garantiert die Klasse, dass sie Implementierungen der Methoden des genannten Interfaces (in diesem Fall nur die eine Methode searchinfo()) bereitstellt. Dies gibt anderen Klassen die Sicherheit, ein Objekt der Klasse, die das Interface implementiert, als Instanz dieses Interfaces betrachten zu können. Gerade die hier gezeigte Verwendung einer Callback-Methode ist ein hervorragendes Beispiel für die Nützlichkeit eines Interfaces: Ein Programm kann BGSearcher einsetzen, wenn es SearchInfo implementiert, und darf ansonsten von einer beliebigen Klassenhierarchie abstammen.
  • In der Methode main() erzeugt BGSearchTest ein 10.000 Elemente großes Array, das mit zufälligen int-Werten gefüllt wird. Für die Erzeugung von Zufallszahlen zwischen 1 und 10 wird die Zufallsgenerator-Methode Math.random() verwendet, die pseudozufällige Fließkommawerte zwischen 0 und 1 zurückgibt. Mithilfe von Multiplikation, Addition und Typecasting wird der erhaltene Wert in den richtigen Bereich umgerechnet.
  • Die Anweisung in BGSearchTest, die vielleicht am merkwürdigsten erscheint, ist die Erzeugung einer Instanz der Klasse selbst:
BGSearchTest test = new BGSearchTest();
    • Die erzeugte Instanz test ist erforderlich, um sie als Referenz auf das aktuelle Programm an das als Nächstes erzeugte BGSearcher-Objekt zu übergeben, damit es wiederum die Callback-Methode searchinfo() aufrufen kann.
  • Nachdem das BGSearcher-Objekt erzeugt ist, muss übrigens seine Methode start() aufgerufen werden, damit es mit der eigentlichen Suche beginnt. Anschließend kann sich das Programm um seine eigenen Aufgaben kümmern, weil der Wechsel zwischen den beiden Threads automatisch vonstatten geht.
  • Die Callback-Methode searchinfo() wird automatisch jedes Mal aufgerufen, wenn der gesuchte Wert im Array gefunden wird. Sie erhält als Übergabewert die Suchposition. Im vorliegenden Beispielprogramm tut die Methode nichts besonders Interessantes mit dem Wert, sie gibt ihn nur aus.
    • Bemerkenswert ist lediglich, dass die Ausgabe auf den Ausgabestrom System.err erfolgt, die Standardfehlerausgabe (in UNIX und C stderr genannt). Dieser alternative Konsolen-Ausgabekanal dient speziell zum Anzeigen von Warnungen und Fehlermeldungen. Er ist besonders nützlich, wenn Sie die Standardausgabe mittels >Dateiname in eine Datei umgeleitet haben: Die Ausgabe auf stderr garantiert, dass wichtige Meldungen nicht mit der normalen Ausgabe eines Programms in der Umleitung landen, sondern auf dem Bildschirm angezeigt werden.
  • Das Interface SearchInfo deklariert, wie bei Interfaces üblich, lediglich den leeren Header der Methode searchinfo(). Diese Methode muss von einer Klasse bereitgestellt werden, die das Interface implementieren möchte.

In den nächsten beiden Abschnitten finden Sie weitere Beispiele für die Anwendung von Threads: einen Ruby-Netzwerkserver sowie eine flimmerfreie Animation in Java.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.






 <<   zurück
  
  Zum Katalog
Zum Katalog: IT-Handbuch für Fachinformatiker






IT-Handbuch für Fachinformatiker
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Linux






 Linux


Zum Katalog: Konzepte und Lösungen für Microsoft-Netzwerke






 Konzepte und
 Lösungen für
 Microsoft-Netzwerke


Zum Katalog: Webseiten programmieren und gestalten






 Webseiten
 programmieren
 und gestalten


Zum Katalog: C/C++






 C/C++


Zum Katalog: Java ist auch eine Insel






 Java ist auch
 eine Insel


Zum Katalog: Einstieg in SQL






 Einstieg in SQL


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Galileo Press 2008
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de