NEBEN - LÄUFIGE PROG RAM -

Größe: px
Ab Seite anzeigen:

Download "NEBEN - LÄUFIGE PROG RAM -"

Transkript

1 Free ebooks ==> carsten VOGT NEBEN - LÄUFIGE PROG RAM - M IERUNG EIN ARBEITSBUCH MIT UNIX/LINUX UND JAVA EXTRA: Mit kostenlosem E-Book Im Internet: Programmbeispiele, Lösungen zu den Aufgaben und Zusatzmaterialien

2 Free ebooks ==> Vogt Nebenläufige Programmierung vbleiben Sie einfach auf dem Laufenden: Sofort anmelden und Monat für Monat die neuesten Infos und Updates erhalten.

3

4 Carsten Vogt Nebenläufige Programmierung Ein Arbeitsbuch mit UNIX / Linux und Java mit 93 Bildern und 39 Programmen

5 Free ebooks ==> Dr. Carsten Vogt ist Professor am Institut für Nachrichtentechnik der Fachhochschule Köln. Er lehrt dort Programmierung in Java und C, Betriebssysteme / Verteilte Systeme sowie Mobilgeräteprogrammierung. Alle in diesem Buch enthaltenen Informationen, Verfahren und Darstellungen wurden nach bestem Wissen zusammengestellt und mit Sorgfalt getestet. Dennoch sind Fehler nicht ganz auszuschließen. Aus diesem Grund sind die im vorliegenden Buch enthaltenen Informationen mit keiner Verpflichtung oder Garantie irgendeiner Art verbunden. Autoren und Verlag übernehmen infolgedessen keine juristische Verantwortung und werden keine daraus folgende oder sonstige Haftung übernehmen, die auf irgendeine Art aus der Benutzung dieser Informationen oder Teilen davon entsteht. Ebenso übernehmen Autoren und Verlag keine Gewähr dafür, dass beschriebene Verfahren usw. frei von Schutzrechten Dritter sind. Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Buch berechtigt deshalb auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften. Bibliografische Information Der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über abrufbar. Dieses Werk ist urheberrechtlich geschützt. Alle Rechte, auch die der Übersetzung, des Nachdruckes und der Vervielfältigung des Buches, oder Teilen daraus, vorbehalten. Kein Teil des Werkes darf ohne schriftliche Genehmigung des Verlages in irgendeiner Form (Fotokopie, Mikrofilm oder ein anderes Verfahren), auch nicht für Zwecke der Unterrichtsgestaltung mit Ausnahme der in den 53, 54 URG genannten Sonderfälle, reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, vervielfältigt oder verbreitet werden Carl Hanser Verlag München Lektorat: Dr. Martin Feuchte Herstellung: Dipl.-Ing. Franziska Kaufmann Coverconcept: Marc Müller-Bremer, München Coverrealisierung: Stephan Rönigk Datenbelichtung, Druck und Bindung: Kösel, Krugzell Ausstattung patentrechtlich geschützt. Kösel FD 351, Patent-Nr Printed in Germany ISBN: E-Book-ISBN:

6 Vorwort Für Computernutzer ist es heute selbstverständlich, dass sie mit ihren Geräten verschiedene Dinge gleichzeitig tun können einen Text schreiben, Musik hören, einen Film ablaufen lassen, Nachrichten empfangen und so weiter. Ebenso selbstverständlich ist es für sie, sich über das Internet mit anderen Computern zu verbinden und mit diesen zu kommunizieren und zu kooperieren. Heutige Computer arbeiten also nebenläufig, das heißt, sie können mehrere Aktionen zur selben Zeit ausführen. Nebenläufigkeit ist daher ein zentrales Themengebiet der Informatik: In der Technischen Informatik wird nebenläufig arbeitende Computerhardware entwickelt, die Praktische Informatik bietet Betriebssysteme und Programmiersprachen zur Erstellung und Steuerung nebenläufiger Programme an, und die Theoretische Informatik befasst sich mit Modellen, mit denen man nebenläufige Vorgänge darstellen und analysieren kann. In diesem weit gefächerten Feld konzentriert sich dieses Buch auf nebenläufige Software, gibt also eine grundständige Einführung in Begriffe und Techniken zur Realisierung nebenläufiger Programme. Das Buch wendet sich an Studierende und andere Interessierte, die bereits Grundkenntnisse in C und/oder Java sowie in Betriebssystemen wie UNIX oder Linux haben und die nun die Möglichkeiten kennenlernen wollen, die diese Sprachen und Systeme zur nebenläufigen Programmierung bieten. Leserinnen und Leser werden erfahren, wie man mit Prozessen und Threads nebenläufige Aktivitäten programmiert, wie man solche Aktivitäten synchronisiert (also zeitlich untereinander abstimmt) und wie man sie, lokal und im Internet, miteinander kommunizieren und kooperieren lässt. Sie werden jeweils zunächst eine allgemeine Einführung in die entsprechenden Grundbegriffe und Techniken erhalten und dann sehen, wie man diese an der C-Programmierschnittstelle von UNIX/Linux und in der Programmiersprache Java praktisch einsetzt. Anhand von zahlreichen Aufgaben können sie ihr neu erworbenes Wissen anwenden und vertiefen. Der Autor führt seit vielen Jahren Lehrveranstaltungen zur nebenläufigen Programmierung, zu Betriebssystemen und zu Verteilten Systemen im dritten und vierten Fachsemester von Informatikstudiengängen durch. Er kennt also die typischen Schwierigkeiten und Probleme, die Neulinge in diesem Gebiet haben. In seinen Vorlesungen, Übungen und Programmierpraktika bemüht er sich daher, seine Zuhörerschaft dort abzuholen, wo sie sich üblicherweise anfangs und während des zweiten Studienjahrs befindet und er möchte dies auch mit diesem Buch zu tun: Zahlreiche Beispiele, Grafiken, Codestücke sowie Übungsaufgaben sollen das Verständnis erleichtern und zum Einstieg in die eigenständige Arbeit anregen. Das Buch steht nicht allein: Es wird ergänzt durch die Webseite auf der man die Programmbeispiele, die Lösungen der Aufgaben und zusätzliche Materialien findet. Über Kommentare und (hoffentlich wenige) Fehlermeldungen freut sich der Autor unter carsten.vogt@fh-koeln.de. Köln / Bergisch Gladbach, im Januar 2012 Carsten Vogt

7

8 Inhalt 1 Einführung Basistechniken Formen der Nebenläufigkeit Hard- und Software eine Kurzeinführung Computer-Hardware Computer-Software Nebenläufigkeit in Hardware Nebenläufigkeit in Software Die Rolle des Betriebssystems Systemarchitekturen Aufgaben und Schnittstellen Virtualisierung Netzdienste und verteilte Systeme Betriebsarten Prozesse und Threads Prozesse Threads Der Lebenszyklus Implementierungsaspekte Buchführung Dispatching Scheduling Prozesse und Threads in UNIX/Linux Kommandos der Benutzerschnittstelle Grundlegende API-Funktionen für Prozesse Die Funktion fork() Weitere Funktionen Programmbeispiele Grundlegende API-Funktionen für Threads Pthreads: pthread_create(), pthread_exit() Pthreads: pthread_join(), pthread_cancel() vfork() und clone()...59

9 8 Inhalt 2.4 Threads in Java Die Klasse Thread run() und start() join() Weitere Methoden Grundlegende Programmiertechniken Zugriff auf gemeinsame Variablen Beenden von Threads Zusammenfassung und Ausblick A Basistechniken: Aufgaben A.1 Wissens- und Verständnisfragen A.2 Sprachunabhängige Anwendungsaufgaben A.3 Programmierung unter UNIX/Linux A.4 Programmierung in Java Synchronisation Synchronisationsbedingungen Elementare Bedingungen Wechselseitiger Ausschluss Reihenfolgebedingung Komplexere Probleme Erzeuger-Verbraucher-Problem Leser-Schreiber-Problem Philosophenproblem Einfache Synchronisationsmechanismen Grundlegende Eigenschaften Interruptsperren Spinlocks Signale und Events Synchronisation durch Semaphore Arbeitsprinzip von Semaphoren Datenstrukturen und Operationen Semaphoroperationen in Bild und Notation Varianten und Erweiterungen Einsatz bei Standardproblemen Wechselseitiger Ausschluss...96

10 Free ebooks ==> Inhalt Reihenfolgebedingung Erzeuger-Verbraucher-Problem Leser-Schreiber-Problem Philosophenproblem Systematische Lösung von Problemen Fehlerquellen Deadlocks: Problematik Deadlocks: Lösungen Missachtung der Atomarität Einsatz von sleep() Mangelnde Fairness Synchronisation durch Monitore Grundprinzip von Monitoren Definition des Monitorbegriffs Beispiel: Einfacher Ringpuffer mit Überschreiben Bedingungsvariablen Zweck und Einsatz Beispiel: Ringpuffer für Erzeuger/Verbraucher Lösung weiterer Standardprobleme Reihenfolgebedingung Leser-Schreiber-Problem Philosophenproblem Mechanismen in UNIX/Linux Signale Lock-Dateien Semaphore Erzeugen von Semaphorgruppen Initialisieren und Löschen P- und V-Operationen Programmstrukturen und -beispiele Mutexe mit Bedingungsvariablen Mutexe Bedingungsvariablen Beispiel: Erzeuger-Verbraucher mit Ringpuffer Mechanismen in Java Atomare Operationen Basistypen Collections...136

11 10 Inhalt Semaphore Die Klasse Semaphore Beispiel: Reihenfolgebeziehung Monitore synchronized wait() und notify() Die Interfaces Lock und Condition Weitere Mechanismen Zusammenfassung und Ausblick A Synchronisation: Aufgaben A.1 Wissens- und Verständnisfragen A.2 Sprachunabhängige Anwendungsaufgaben A.3 Programmierung unter UNIX/Linux A.4 Programmierung in Java Kommunikation Grundlegende Begriffe Arten der Kommunikation Sender-Empfänger-Beziehungen Ein oder mehrere Sender und Empfänger Direkte vs. indirekte Kommunikation Enge vs. lose zeitliche Kopplung Kommunikation in Rechnernetzen Schnittstellen: Sockets Protokolle und Protokollstacks Der Protokollstack des Internets Techniken in UNIX/Linux Shared Memory API-Funktionen Programmbeispiel: Erzeuger-Verbraucher-System Pipes Benannte Pipes Unbenannte Pipes Message Queues API-Funktionen: Erzeugen und Löschen API-Funktionen: Senden und Empfangen Programmbeispiel: Erzeuger-Verbraucher-System...181

12 Inhalt Sockets Domains und Typen API-Funktionen: Übersicht API-Funktionen: Erzeugen und Schließen API-Funktionen: Verbinden und Kommunizieren Programmbeispiel: Stream-Sockets Programmbeispiel: Datagram-Sockets Techniken in Java Übersicht Piped Streams Sockets Stream-Sockets Datagram-Sockets Zusammenfassung und Ausblick A Kommunikation: Aufgaben A.1 Wissens- und Verständnisfragen A.2 Sprachunabhängige Anwendungsaufgaben A.3 Programmierung unter UNIX/Linux A.4 Programmierung in Java Kooperation Modelle und Techniken Das Client-Server-Modell Grundlegende Struktur Zeitliche Abläufe Implementierungsaspekte Das Peer-to-Peer-Modell Programmiertechniken Prozedurorientierte Kooperation Objektorientierte Kooperation Webbasierte Kooperation Techniken in UNIX/Linux Kooperation über Sockets Remote Procedure Call (RPC) Komponenten und ihr Zusammenspiel Schritte der Programmierung...230

13 12 Inhalt 5.3 Techniken in Java Remote Method Invocation (RMI) Komponenten und ihr Zusammenspiel Schritte der Programmierung Dynamische Webseiten Applets Servlets und Java Server Pages Web Services Zusammenfassung A Kooperation: Aufgaben A.1 Wissens- und Verständnisfragen A.2 Sprachunabhängige Anwendungsaufgaben A.3 Programmierung unter UNIX/Linux A.4 Programmierung in Java Literatur und Internet Index

14 1 Einführung Im Vorwort wurde bereits knapp umrissen, worum es in diesem Buch geht nämlich um eine praktische Einführung in die Programmierung nebenläufiger Software. Die Fragen, die dort nur kurz angesprochen wurden, sollen in dieser Einführung systematisiert und detaillierter beantwortet werden. Was ist Nebenläufigkeit und warum ist sie wichtig? Nebenläufigkeit bedeutet, ganz grob gesprochen, dass mehrere Aktionen gleichzeitig stattfinden. Bezogen auf Computer heißt das, dass mehrere Befehle zum selben Zeitpunkt ausgeführt werden oder dass mehrere Befehlsfolgen im selben Zeitintervall ablaufen. Nebenläufigkeit ist damit ein grundlegender und zentraler Aspekt der Informatik, denn heutige Computersysteme arbeiten auf mehreren Ebenen nebenläufig sowohl in ihrer Hardware als auch in ihrer Software. Manches davon wird dem Benutzer bewusst, manches nur dem Programmierer, und manches bleibt beiden verborgen: Ein Benutzer kann auf einem Computer mehrere Anwendungsprogramme gleichzeitig ausführen beispielsweise Musik hören, ein Spiel spielen und in sozialen Netzen kommunizieren. Das Betriebssystem schaltet den Prozessor (oder die Prozessoren) des Computers zwischen den Anwendungen hin und her und treibt diese damit gleichmäßig voran. Auf dieselbe Weise können mehrere Benutzer gleichzeitig am selben Computer arbeiten. Auch hier schaltet das Betriebssystem den Prozessor zwischen den Anwendungen der einzelnen Benutzer in rascher Folge um. Computer, die durch ein Rechnernetz gekoppelt sind, können über dieses Netz Daten austauschen und somit nebenläufig eine gemeinsame Aufgabe lösen. Multiprozessorsysteme umfassen mehrere Prozessorchips, die nebeneinander selbstständig jeweils ein Programm ausführen können. Auf diese Prozessoren kann die Arbeitslast des Computers verteilt werden. Ein moderner Prozessorchip besteht aus mehreren Komponenten, die gleichzeitig Operationen ausführen. Beispielsweise können die momentan aktiven Programme auf mehrere Kerne des Prozessors verteilt werden. Da also Nebenläufigkeit eine zentrale Rolle spielt, befasst sich eine ganze Reihe von Fächern eines Informatikstudiums mit ihr. Dazu gehören beispielsweise Betriebssysteme, wo es um die Steuerung und Koordination nebenläufiger Programmausführungen geht, Rechnerarchitektur, wo der Aufbau und die Funktionsweise nebenläufiger Hardware behandelt werden,

15 14 1 Einführung BILD 1.1 Aufbau des Buchs Programmiersprachen, wo Sprachkonstrukte zur Programmierung nebenläufiger Anwendungen eine Rolle spielen, Datennetze und Verteilte Systeme, wo Anwendungen betrachtet werden, bei denen nebenläufige Aktivitäten verschiedener Rechnerknoten kooperieren, und Datenbanken, wo bei nebenläufigen Operationen die Datenkonsistenz sicherzustellen ist. Was ist die Zielgruppe dieses Buchs und welches Vorwissen wird verlangt? Im Kern der Zielgruppe des Buchs stehen Bachelor-Studierende der Informatik oder benachbarter Studiengänge im Verlauf ihres zweiten Studienjahrs. Sie müssen in diesem Zeitraum Kenntnisse über Techniken der nebenläufigen Programmierung erwerben, und sie müssen lernen, diese Kenntnisse auf Anwendungsprobleme anzuwenden. Das Grundwissen, das sie dafür benötigen, stammt aus ihrem ersten Studienjahr: Sie haben praktische Programmiererfahrungen in Java und idealerweise auch in C erworben (wobei hier reinen Java-Programmierern die C-Lehrbücher [Daus11] und [Vogt07] empfohlen werden). Zudem besitzen sie zumindest übersichtsartige Kenntnisse über den Aufbau und die Funktionsweise eines Computers und seiner Komponenten. Das Buch richtet sich natürlich auch an alle anderen Interessierten, die entsprechende Vorkenntnisse haben. Wie ist das Buch aufgebaut und was behandelt es im Einzelnen? Das Buch behandelt in seinen vier Hauptkapiteln Begriffe, Techniken, Probleme und Lösungen bei der Erstellung nebenläufiger Programme. Die Themen werden zunächst unabhängig von einer bestimmten Programmiersprache diskutiert und dann am Beispiel der C-Schnittstelle von UNIX/Linux sowie von Java in die Programmierpraxis umgesetzt. Jedem Hauptkapitel ist eine umfangreiche Sammlung von Aufgaben zugeordnet (von denen einige nicht hier abgedruckt, sondern im Web zu finden sind). Im Einzelnen werden aufeinander aufbauend die folgenden Aspekte angesprochen: Kap. 5: Kooperation Kap. 4: Kommunikation Kap. 5A: Aufgaben Kap. 4A: Aufgaben Kap. 3: Synchronisation Kap. 2: Basistechniken der Nebenläufigkeit Kap. 3A: Aufgaben Kap. 2A: Aufgaben Kapitel 2 behandelt Basisbegriffe und -techniken der Nebenläufigkeit. Zunächst wird gezeigt, in welchen Formen Nebenläufigkeit in der Hard- und der Software eines Computers auftritt. Anschließend wird die besondere Rolle des Betriebssystems erörtert. Im Zentrum stehen hier Prozesse und Threads, mit denen nebenläufige Vorgänge in einem Computer programmiert und gesteuert werden. Kapitel 3 befasst sich mit der Synchronisation von Prozessen und Threads, also mit ihrer zeitlichen Abstimmung untereinander. Es wird zunächst eine Reihe typischer Synchronisationsbedingungen besprochen, also Anforderungen an den zeitlichen Ablauf der Vorgänge. Danach werden verschiedene Mechanismen eingeführt, mit

16 1 Einführung 15 denen diese Forderungen durchgesetzt werden können. Ein besonderes Augenmerk gilt dabei zwei Standard-Synchronisationsmechanismen, den so genannten Semaphoren und Monitoren. In Kapitel 4 geht es um Kommunikation, also um den Datenaustausch zwischen Prozessen. Die verschiedenen Ansätze, die behandelt werden, ermöglichen eine lokale Kommunikation, also einen Datenaustausch auf einem Computer, und/oder eine Kommunikation in einem Rechnernetz, also über Computergrenzen hinweg. Kapitel 5 gibt schließlich eine Einführung in Techniken zur Kooperation von Prozessen, also für ihre Zusammenarbeit bei der Lösung von Problemen. Im Fokus steht dabei die Zusammenarbeit zwischen Prozessen, die auf verschiedenen Computern eines Rechnernetzes laufen. Warum nennt sich das Buch Arbeitsbuch? Das Buch ist nicht nur für die bloße Lektüre gedacht, denn man wird Problematik und Techniken der Nebenläufigkeit erst richtig verstehen, wenn man die Dinge praktisch ausprobiert. Leserinnen und Leser sollten also selbst aktiv werden: Erstens sollten sie die Beispielprogramme auf einem Computer ausführen, sie dabei auch nach eigener Phantasie verändern und die Effekte beobachten. Die Quellcodes kann man von herunterladen. Als Plattform benötigt man ein UNIX- oder Linux-System (das auch, wie beispielsweise Knoppix [Knoppix], auf einem Windows-Computer gestartet werden kann) und eine Installation des Java SDK [JavaSDK]. Zweitens sollten sie die Aufgaben gründlich durcharbeiten. Man sollte die hier geforderten C- und Java-Programme nicht bloß auf dem Papier hinschreiben, sondern sie auch praktisch ausführen. Die Lösungen findet man unter ebenso weitere Aufgaben. Im Buch stehen am Textrand Hinweise auf jeweils entsprechende Aufgaben, so dass man das Gelesene unmittelbar anwenden kann. Dabei wird zwischen zwei Arten von Hinweisen unterschieden: ( Aufgabe xx): Ist der Aufgabenhinweis geklammert, dann bezieht sich die genannte Aufgabe zwar auf den Lehrtext an dieser Stelle, man wird sie aber erst mit Informationen, die später folgen, vollständig lösen können. Aufgabe xx: Ist der Aufgabenhinweis nicht geklammert, so kann man die Aufgabe mit dem, was man bis zu diesem Abschnitt (einschließlich) gelesen hat, lösen. Was bietet das Buch also und was bietet es nicht? Die Absicht und der Rahmen dieses Buchs sind damit wie folgt: Das Buch will eine möglichst leicht verständliche, praktische Einführung in die Programmierung nebenläufiger Anwendungen geben. Es möchte Leserinnen und Leser in die Begriffswelt und Techniken der Nebenläufigkeit einführen, und es

17 16 1 Einführung möchte sie in die Lage versetzen, entsprechende Probleme praktisch zu lösen sowohl allgemein als auch mit UNIX/Linux-C und mit Java. Das Buch beschränkt sich bewusst auf die Programmierung nebenläufiger Software mit den Mitteln, die weit verbreitete Sprachen wie C unter UNIX/Linux und Java bereitstellen. Hardware-Nebenläufigkeit wird daher nur sehr knapp behandelt. Weiterführende, spezielle Ansätze, wie beispielsweise die Parallelisierung von Programmen zur optimalen Ausnutzung nebenläufiger Hardware, werden gar nicht betrachtet. Ebenfalls werden theoretische Aspekte, wie zum Beispiel Techniken zur abstrakten Modellierung nebenläufiger Vorgänge oder Verfahren zum Beweis der Korrektheit nebenläufiger Programme, nicht angesprochen. Welchen Hintergrund haben Buch und Autor? Hervorgegangen ist das Buch aus der Lehrveranstaltung Betriebssysteme und Verteilte Systeme, die der Autor seit längerem an der Fachhochschule Köln in Studiengängen der Informatik und der Elektrotechnik hält. Ein Großteil der Erläuterungen, Beispielprogramme und Grafiken hat sich in den Vorlesungen bewährt, und viele der Aufgaben wurden in den Übungen und Praktika des Fachs erprobt oder als Klausuraufgaben verwendet. Der Autor weiß aus seiner langjährigen Lehrpraxis, welche Dinge Neulingen nicht unmittelbar eingängig sind und somit einen erhöhten Erklärungsaufwand erfordern. Dies hat er im Buch (hoffentlich zur Zufriedenheit des Lesers) besonders berücksichtigt. Die Praxis hat zudem gezeigt, auf welche Details man bei der Programmierung in Java und C besonders achten muss und wo man gar Gefahr läuft, ins Schleudern zu geraten. Hierauf wird durch zwei Symbole am Rand des Texts explizit hingewiesen. Wie lässt sich das Buch für eine Lehrveranstaltung einsetzen? Umfang und Inhalt des Buchs entsprechen einer Veranstaltung über ein Semester, die aus einer zweistündigen Vorlesung sowie ergänzenden Übungen und Laborpraktika besteht. Der Stoff kann jedoch auch in zweisemestrige Veranstaltungen eingebettet werden beispielsweise in eine klassische Betriebssystemvorlesung, die in einem Schwerpunkt die nebenläufige Programmierung mit Funktionen der Systemschnittstelle lehrt und im zweiten Schwerpunkt Implementierungstechniken für Betriebssysteme behandelt. Wo findet man weitere Informationen? Die Webseite zum Buch, enthält relevante Links, die Lösungen der Übungsaufgaben, weitere Aufgaben, zusätzliche Grafiken und eine Liste von Fehlern, die nach der Drucklegung entdeckt wurden. Eine weitere Vertiefung bietet sowohl die gedruckte Literatur als auch das Internet: Fundamentale Aspekte der Software-Nebenläufigkeit werden üblicherweise in Lehrbüchern zum Thema Betriebssysteme behandelt, wie beispielsweise [Silb10], [Stev09], [Tane08] oder [Vogt01]. Sie werden ergänzt durch Bücher zu Verteilten Systemen, wie beispielsweise [Beng08], [Tane07] oder [Coul12].

18 1 Einführung 17 Nebenläufigkeit in Java wird sehr ausführlich in [Oech11] behandelt; auch [Abts10] ist zu empfehlen. Alle Details findet man in der Java-Spezifikation von Oracle [JavaSpec], die durch die Tutorien in [JavaTutConc] und [JavaTutNet] ergänzt wird. Gute Einführungen in die C-Schnittstelle von UNIX/Linux bieten [Ehse12], [Hero04], [Stev05] sowie auch [Robb03]. Die vollständige Spezifikation der Schnittstelle findet man beispielsweise unter [Linux]. Spezielle Aspekte werden zum Beispiel in [Come01] und [Poll09] besprochen.

19 2 Basistechniken Kapitel 2 beschäftigt sich mit grundlegenden Techniken der Nebenläufigkeit. Im Mittelpunkt stehen dabei das Betriebssystem, insbesondere UNIX/Linux, sowie die Sprache Java. Kooperation Kommunikation Synchronisation Basistechniken 2.1 Formen der Nebenläufigkeit...20 Abschnitt 2.1 wiederholt zunächst die wichtigsten Grundbegriffe bezüglich Hard- und Software und stellt dann dar, auf welche Weisen Nebenläufigkeit in Hard- und Software realisiert werden kann Hard- und Software eine Kurzeinführung Nebenläufigkeit in Hardware Nebenläufigkeit in Software Die Rolle des Betriebssystems...25 Abschnitt 2.2 diskutiert die zentrale Rolle des Betriebssystems, das durch seine Architektur und seine Betriebsart(en) Art und Grad der Nebenläufigkeit bestimmt. Das Betriebssystem unterstützt Prozess- und Thread-Konzepte, mit denen nebenläufige Aktivitäten programmiert und verwaltet werden Systemarchitekturen Betriebsarten Prozesse und Threads Implementierungsaspekte Prozesse und Threads in UNIX/Linux...42 Abschnitt 2.3 führt die wichtigsten UNIX/Linux-Benutzerkommandos und Funktionen seiner Programmierschnittstelle ein, mit denen nebenläufige Aktivitäten gesteuert werden können Kommandos der Benutzerschnittstelle Grundlegende API-Funktionen für Prozesse Grundlegende API-Funktionen für Threads Threads in Java...60 Abschnitt 2.4 stellt die Java-Klasse Thread mit ihren grundlegenden Methoden zur Programmierung von Nebenläufigkeit vor Die Klasse Thread Grundlegende Programmiertechniken Zusammenfassung und Ausblick...67

20 Free ebooks ==> 2 Basistechniken Der Begriff Multitasking also das Erledigen mehrerer Dinge gleichzeitig ist in der Alltagssprache angekommen. So wird diskutiert, ob Frauen dieses Multitasking besser beherrschen als Männer. Ältere wundern sich über das Multitasking Jüngerer, die gleichzeitig essen, trinken, telefonieren, SMS schreiben und über die Straße gehen. Und man beklagt das Multitasking im Arbeitsleben, in dem viele Dinge möglichst zur selben Zeit erledigt werden sollen. Eine tiefere Wurzel hat das Multitasking jedoch in der Informatik. Das ist angemessen, denn Computer sind im gleichzeitigen (also nebenläufigen) Erledigen von Aufgaben wesentlich besser als wir Menschen. So ist es für heutige Personal Computer selbstverständlich, dass man mit ihnen zur selben Zeit Musik hören, Spiele spielen und s empfangen kann; Web Server können eine Vielzahl von Surfern gleichzeitig bedienen, und auf Großrechnern können alle Mitarbeiter einer Firma nebenläufig arbeiten: hören spielen CPU Speicher Mail empfangen Netz BILD 2.1 Nebenläufigkeit aus Benutzersicht Netz CPU Speicher Aufträge CPU Speicher Dieses Kapitel zeigt, welche Formen der Nebenläufigkeit durch die Hard- und Software eines Computers realisiert werden, welche Rolle das Betriebssystem dabei spielt und welche grundlegenden Techniken UNIX/Linux und Java bieten, um nebenläufige Anwendungen zu programmieren.

21 20 2 Basistechniken 2.1 Formen der Nebenläufigkeit Nebenläufigkeit wird in Computern erstens durch Gruppen von Hardwarekomponenten realisiert, die echt gleichzeitig Aktionen ausführen können. Zweitens wird Nebenläufigkeit in Software implementiert: Mehrere Programmstücke können gleichzeitig ausgeführt werden, sofern es logisch nicht zwingend erforderlich ist, dass sie hintereinander ablaufen. Software-Nebenläufigkeit kann durch Hardware-Nebenläufigkeit unterstützt werden, muss es aber nicht: Eine nicht nebenläufige Hardware kann im raschen Wechsel zwischen nebenläufigen Softwarekomponenten hin- und hergeschaltet werden, so dass ein Beobachter den Eindruck bekommt, dass mehrere Dinge zur selben Zeit geschehen Hard- und Software eine Kurzeinführung Als Einstieg in diesen Abschnitt soll kurz das nötigste Basiswissen zur Rolle der Hardund Software in Computern präsentiert werden. Wer sich hier fit fühlt, kann die folgenden zweieinhalb Seiten überspringen und bei Bedarf später hierher zurückkehren. BILD 2.2 Eine einfache Hardware-Architektur Computer-Hardware Die Hardware eines Computers gliedert sich in Zentraleinheit und Peripherie: Zentraleinheit CPU Hauptprozessor (CPU) zur Programmausführung RAM Hauptspeicher (RAM) zur kurzfristigen Datenspeicherung Peripherie Geräte zur Ein-/Ausgabe Geräte zur langfristigen Datenspeicherung Zur Zentraleinheit gehören der Hauptprozessor und der Hauptspeicher. Der Hauptprozessor (engl.: central processing unit, CPU) führt Programme aus, die als Folgen von Maschinenbefehlen vorliegen. Maschinenbefehle sind, im Gegensatz zu Befehlen höherer Sprachen, so einfach, dass sie von der Hardware unmittelbar ausgeführt werden können. Der Prozessor stützt sich dabei auf Register kleine Speicherstellen, die die Ein- und Ausgabedaten des aktuell ausgeführten Befehls aufnehmen und zudem Informationen über die Programmausführung speichern. Zu den Prozessorregistern gehören insbesondere der Befehlszähler (engl.: program counter, instruction pointer), der angibt, wo im Maschinenprogramm sich die Ausführung aktuell befindet, das Programmstatuswort, das im Wesentlichen Informationen über das Ergebnis des zuletzt ausgeführten Befehls enthält (zum Beispiel, ob es gleich null oder negativ war), sowie Bus

22 2.1 Formen der Nebenläufigkeit 21 der Stack Pointer, der die aktuelle Spitze des Aufrufstacks angibt. Der Aufrufstack ist ein Speicherbereich, der die Parameter und lokalen Variablen geschachtelter Funktionsaufrufe enthält. Der Hauptspeicher (engl.: main memory, auch random access memory, RAM) speichert die Maschinenbefehle der aktuell laufenden Programme und ihre Daten. Maschinenbefehle können ihre Operanden durch Hauptspeicheradressen identifizieren, also durch Nummern von Hauptspeicherzellen, auf die der Prozessor dann direkt zugreift. Häufig benötigte Daten und Befehle können in einen Cache aufgenommen werden einen kleinen, aber schnellen Speicher, der näher am Prozessor liegt. Bei Platzmangel können Hauptspeicherbereiche auf Hintergrundspeicher ausgelagert werden. Zur Peripherie gehören Hintergrundspeicher, wie insbesondere der Plattenspeicher, sowie Ein-/Ausgabegeräte. Auch Anschlüsse für Datennetze und für Wechseldatenträger (insbesondere USB-Sticks und CDs/DVDs) sind Teil der Peripherie. Die Komponenten von Zentraleinheit und Peripherie sind durch ein oder mehrere Busse, also lineare Leitungsbündel zur Datenübertragung, oder komplexer aufgebaute Leitungsnetzwerke miteinander verbunden Computer-Software Die Hardware eines Computers wird durch Software, also durch Programme gesteuert. Üblicherweise werden Programme in einer höheren Programmiersprache wie C oder Java geschrieben. Die Befehle einer solchen höheren Sprache sind allerdings zu komplex, um direkt durch die Prozessor-Hardware ausgeführt zu werden. Sie müssen daher in Maschinenprogramme (also Folgen von Maschinenbefehlen) umgesetzt werden. Hierzu gibt es zwei Ansätze: Bei der Übersetzung (Compilierung) wird ein höhersprachiges Programm in ein äquivalentes Maschinenprogramm umgeformt, das anschließend durch den Prozessor ausgeführt werden kann. Ein Beispiel für eine höhere Sprache, deren Programme in Maschinensprache übersetzt werden, ist C. Programm in höherer Sprache int ggt(int x, int y) { while (x!=y) { if (x<y) y=y-x; else x=x-y; return x; Compiler übersetzt Programm in Maschinensprache XX1000 LDA XX2000 XX1001 CMP XX2001 XX1002 JZE XX1011 XX1003 JNG XX1007 XX1004 SUB XX Prozessor führt aus CPU BILD 2.3 Übersetzung von Programmen Bei der Interpretation ( Bild nächste Seite) wird ein höhersprachiges Programm Befehl für Befehl durchlaufen. Dabei wird für jeden Befehl unmittelbar ein entsprechendes kleines Maschinenprogramm ausgeführt. Der Durchlauf durch den Programmtext, seine Analyse und die Aufrufe der Maschinenprogramme werden durch einen Interpreter, ein Dienstprogramm, gesteuert. Ein Beispiel für eine höhere Sprache, deren Programme interpretiert werden, ist PHP.

23 22 2 Basistechniken BILD 2.4 Interpretation von Programmen Programm in höherer Sprache C = A + B F = D E I = G + H Interpreter Befehlsanalyse CPU Maschinenprogramm für Addition Maschinenprogramm für Subtraktion Bei der Ausführung von Java-Programmen kommt eine Mischform aus Übersetzung und Interpretation zum Einsatz: Ein Java-Programm wird zunächst durch den Java- Compiler in maschinennahen Bytecode übersetzt. Die Java Virtual Machine (JVM) interpretiert dann diesen Bytecode, lässt ihn also auf der realen Hardware ausführen. BILD 2.5 Übersetzung und Interpretation eines Java-Programms Java-Programm Datei prog.java Bytecode-Programm Datei prog.class 1. Java-Compiler 2. Java Virtual Machine übersetzt interpretiert Java Virtual Machine (JVM) CPU Befehlsanalyse Befehlsausführung Software, also insbesondere Maschinenprogramme, wird in Dateien auf Hintergrundspeichern oder Wechseldatenträgern gespeichert. Sie muss in den Hauptspeicher geladen werden und wird von dort (zusammen mit den zugehörigen Daten) Befehl für Befehl durch den Prozessor gelesen und ausgeführt. BILD 2.6 Laden und Ausführen von Software Programmdatei 1. Programm in den Hauptspeicher laden Programm Magnetplatte Programmdatei Daten RAM Memory Stick Programmdatei 2. Programmbefehle mit Datenwerten einzeln in den Prozessor holen und dort ausführen CD-ROM, DVD Programmbefehl Datenwerte CPU Nebenläufigkeit in Hardware Hardware-Nebenläufigkeit wird in heutigen Computern auf verschiedenen Ebenen realisiert. Eine grundlegende Rolle spielen dabei die Prozessoren (CPUs), die für die Ausführung von Programmen zuständig sind:

24 2.1 Formen der Nebenläufigkeit 23 Multiprozessor: Computer CPU Cluster/Multicomputer oder verteiltes System: Computer BILD 2.7 Nebenläufigkeit in Hardware Mehrkernprozessor: CPU Kern CPU Hauptspeicher Computer... Kern Kern Kern CPU Computer Heutige Prozessor-Chips enthalten oft mehrere Prozessorkerne (kurz Kerne genannt). Ein Kern umfasst zumindest ein Rechenwerk (ALU = Arithmetic-Logic Unit) zur Ausführung arithmetischer und logischer Operationen sowie Register zur kurzfristigen Speicherung von Daten und Speicheradressen. Dazu können noch weitere Komponenten wie beispielsweise Caches (kleine, schnelle Datenspeicher) kommen. Ein Kern ist damit in der Lage, Maschinenbefehle auszuführen. Sind also mehrere Kerne vorhanden, so können mehrere Programme oder Programmteile echt gleichzeitig ausgeführt werden. Mehrere Prozessor-Chips können zu einem Multiprozessorsystem zusammengefasst werden, das sich in einem einzelnen Computer befindet. Die Prozessoren sind über einen Bus (ein Leitungsbündel) oder ein komplexer aufgebautes Verbindungsnetz mit einem gemeinsamen Hauptspeicher verbunden und können so eng zusammenarbeiten. Da jeweils nur ein Prozessor gleichzeitig auf den Hauptspeicher zugreifen kann, kann dieser zum Engpass ( Bottleneck ) werden. Als Gegenmaßnahme ordnet man den einzelnen Prozessoren private Caches zu, die Kopien der am häufigsten benutzten Hauptspeicherdaten enthalten und somit den Hauptteil der Speicherzugriffe abfangen. Schließlich können mehrere Computer ( Rechnerknoten ) zu einem Gesamtsystem kombiniert werden. Die beteiligten Knoten haben jeweils ihre eigenen Prozessoren und Hauptspeicher und sind über ein Kommunikationsnetz miteinander verbunden. Über das Netz werden Daten und Aufträge ausgetauscht, so dass die Computer bei der Lösung komplexer Aufgaben kooperieren und die Arbeitslast untereinander verteilen können. Man kann hier zwei Architekturen unterscheiden: Bei einem Cluster (auch Multicomputer genannt) sind die Computer eng miteinander verbunden: Sie stehen örtlich nah beieinander, werden möglicherweise durch ein gemeinsames Betriebssystem gesteuert und teilen sich eventuell Peripheriegeräte. Das Kommunikationsnetz ist schnell. Bei einem verteilten System (zur Begriffsdefinition siehe auch ) ist der Verbund dagegen lockerer: Die Computer können räumlich weit verteilt sein und haben jeweils ihre eigenen Betriebssysteme und Peripheriegeräte. Das Kommunikationsnetz (meist das Internet) ist relativ langsam.

25 24 2 Basistechniken Offensichtlich realisieren alle Ansätze eine echte Nebenläufigkeit: Da mehrere Funktionseinheiten vorhanden sind, können zum selben Zeitpunkt mehrere Operationen ausgeführt werden, also echt gleichzeitig. Jedoch sind die Funktionseinheiten unterschiedlich stark gekoppelt: Die Kopplung bei Multiprozessorsystemen ist eng, da die Prozessoren über ein schnelles Kommunikationsmedium und einen gemeinsamen Hauptspeicher verbunden sind. Die Kopplung bei Clustern und mehr noch bei verteilten Systemen ist demgegenüber lose: Hier hat jeder Computer seinen eigenen Hauptspeicher und die Kommunikation läuft (unter anderem aufgrund der Entfernungen) langsamer als in einem Multiprozessor ab Nebenläufigkeit in Software Auch die Nebenläufigkeit von Software kann auf verschiedenen Ebenen realisiert werden: BILD 2.8 Nebenläufigkeit in Software höhersprachliches sequentielles Programm parallelisierender Compiler höhersprachliches Programm nebenläufige Teilstücke nebenläufige eigenständige Programme Maschinenprogramm nebenläufige Teilstücke Steuerung der nebenläufigen Ausführung durch das Betriebssystem Ein Compiler kann ein höhersprachliches sequentielles Programm parallelisieren und somit beschleunigen: Der Programmierer hat das Programm als Folge von Schritten geschrieben, von denen er denkt, dass sie hintereinander (also sequentiell ) ablaufen sollen. Der Compiler ermittelt jedoch (anhand von Datenabhängigkeiten) voneinander unabhängige Programmstücke, die problemlos auch nebenläufig ausgeführt werden können, und übersetzt sie in entsprechende Teilfolgen von Maschinenbefehlen. Diese können dann auf die verschiedenen Prozessoren oder Prozessorkerne der Hardware ( 2.1.2) gebracht werden. Der Programmierer kann sein Programm selbst in nebenläufige Teile untergliedern. Er benutzt dazu höhersprachliche Befehle, die so genannte Prozesse oder Threads erzeugen und steuern. Das Betriebssystem bringt dann diese Prozesse und Threads auf der Hardware nebenläufig zur Ausführung. Die Programmierung mit solchen Prozessen und Threads wird ein zentrales Thema dieses Buchs sein. Das Betriebssystem kann mehrere Programme, die nicht unbedingt etwas miteinander zu tun haben, gleichzeitig auf dem Computer ausführen lassen. Es teilt dabei die Programme den nebenläufigen Hardwarekomponenten zu und/oder schaltet die Hardware zwischen den Programmen um.

26 2.2 Die Rolle des Betriebssystems 25 Die Nebenläufigkeit von Software ist oft nur eine Pseudonebenläufigkeit: Sind mehr nebenläufige Software-Komponenten (z.b. Programme) als nebenläufige Hardware-Einheiten (z.b. Prozessoren) vorhanden, so können die Software-Komponenten nicht alle echt gleichzeitig ausgeführt werden. In diesem Fall wird die Hardware in derart hoher Frequenz zwischen den Programmen, Prozessen oder Threads hin- und hergeschaltet, dass ein Beobachter den Eindruck der gleichzeitigen Ausführung hat. Fasst man die Betrachtungen dieses Abschnitts 2.1 zusammen, so kommt man zu der folgenden Definition der Nebenläufigkeit in Computern: Nebenläufigkeit (engl.: concurrency) ist die gleichzeitige Ausführung von Aktivitäten. Der Begriff Aktivität bezeichnet hier eine Operationsfolge, die auf der Computer-Hardware in Bearbeitung ist. Bei echter Nebenläufigkeit können zum selben Zeitpunkt t Operationen mehrerer Aktivitäten auf der Hardware ausgeführt werden. Bei der Pseudonebenläufigkeit ist zu jedem Zeitpunkt t höchstens eine Operation auf der Hardware in Ausführung; die Hardware wird jedoch derart rasch zwischen den Aktivitäten umgeschaltet, dass ein menschlicher Betrachter den Eindruck einer echt gleichzeitigen Ausführung hat. DEFINITION Nebenläufigkeit 2.2 Die Rolle des Betriebssystems Betriebssysteme sind Verwalter von Computern. Sie spielen damit bei der Realisierung von Nebenläufigkeit eine zentrale Rolle. Durch ihre Betriebsart bestimmen sie, welcher Grad von Nebenläufigkeit überhaupt möglich ist hier reicht die Bandbreite von strenger Sequentialität (also Hintereinanderausführung einzelner Aufträge) bis zu voller Nebenläufigkeit. Zudem unterstützen sie das Konzept der Prozesse und Threads. Sie stellen so Dienste bereit, über die Benutzer und Anwendungsprogramme nebenläufige Aktivitäten starten und steuern können, und sie bringen diese Aktivitäten auf der realen Hardware zur Ausführung Systemarchitekturen Zum Einstieg in diesen Abschnitt soll etwas Basiswissen über Betriebssysteme vermittelt werden, das im Zusammenhang mit Nebenläufigkeit relevant ist. Eine vertiefende Darstellung findet man in der einschlägigen Betriebssysteme-Literatur (z.b. [Silb10], [Stal12], [Tane08] oder [Vogt01]) Aufgaben und Schnittstellen In seiner klassischen Form setzt ein Betriebssystem auf die reale Hardware auf, nutzt also deren Dienste und steuert sie ( Bild nächste Seite). Nach oben bietet das Betriebssystem Dienste und Funktionen an, über die Benutzer und Anwendungsprogramme den Computer nutzen können. Ein Betriebssystem hat also zwei Aufgaben:

27 26 2 Basistechniken BILD 2.9 Position und Aufgaben des Betriebssystems Anwendungssoftware Benutzer Betriebssystem verwaltet den Computer bietet Schnittstellen Benutzerschnittstelle (User Interface UI) Programmierschnittstelle (Application Programming Interface API) Hardware Hardware-Schnittstelle Das Betriebssystem verwaltet und steuert die Komponenten des Computers. Dabei nutzt es die Funktionen der Hardware, die diese an ihrer Hardware-Schnittstelle bereitstellt. Zu den Kernaufgaben gehören hier die Verwaltung von Prozessorzeit, also die Scheduling -Entscheidung ( ), wann welche Aktivität ausgeführt wird, die Zuteilung von Hauptspeicherplatz und sonstigen Ressourcen an die einzelnen Aktivitäten und die Steuerung des Zugriffs auf Peripheriegeräte und Kommunikationsnetze. Besonders anspruchsvoll sind diese Aufgaben bei einer Hardware mit mehreren Prozessoren und/oder Prozessorkernen ( 2.1.2), da hier die Aktivitäten auf die Prozessoren/Prozessorkerne verteilt, deren Speicherzugriffe aufeinander abgestimmt ( synchronisiert ) und die Datenübertragung zwischen den einzelnen Hard- und Software-Einheiten organisiert werden müssen. Das Betriebssystem stellt Benutzern und Anwendungsprogrammen Schnittstellen zur Verfügung, über die diese den Computer nutzen können. Zu den Schnittstellen gehören die Benutzer- und die Programmierschnittstelle ( Bild nächste Seite): Über die Benutzerschnittstelle (engl.: user interface, UI) kann der Benutzer mit dem Computer kommunizieren. Man unterscheidet textorientierte und grafische Schnittstellen, über die Kommandos in einer textuellen Kommandosprache bzw. über eine Maus oder einen berührungsempfindlichen Bildschirm eingegeben werden. Die Programmierschnittstelle (engl.: application programming interface, API) ist eine Sammlung von Funktionen, die aus einem höhersprachigen Programm heraus aufgerufen werden können. Über die Programmierschnittstelle kann also ein Anwendungsprogrammierer auf die Dienste des Betriebssystems zugreifen. UNIX/Linux realisiert eine Programmierschnittstelle in der Programmiersprache C. Sie wird angeboten durch den UNIX- bzw. Linux-Kern (also den innersten Teil des Betriebssystems, der unmittelbar auf die Hardware aufsetzt) zusammen mit der C-Bibliothek. Das folgende Programm zeigt als Beispiel

28 2.2 Die Rolle des Betriebssystems 27 BILD 2.10 Textorientierte und grafische Benutzerschnittstelle Anzeige der aktiven nebenläufigen Prozesse : an der textorientierten Linux-Schnittstelle (oben) an der grafischen Windows-Schnittstelle (links) Aufrufe der UNIX/Linux-Schnittstellenfunktionen open(), write() und close(), durch die eine Datei neu erzeugt und mit einem Text gefüllt wird: #include <stdio.h> /* Standard-Ein-/Ausgabe */ #include <string.h> /* Verarbeitung von Zeichenketten */ #include <stdlib.h> /* Funktion exit() u.a. */ #include <errno.h> /* Fehlerbehandlung */ main() { int fd; /* Deskriptor für die geöffnete Datei */ int err; /* Zwischenspeicher für Fehlermeldungen */ /* Eine Datei namens "myfile" neu erzeugen (O_CREAT) und zum Schreiben (O_WRONLY) öffnen. PROG 2.1 Aufruf von Funktionen der UNIX/Linux- Programmierschnittstelle (API)

29 28 2 Basistechniken Die Rückgabe ist ein int-wert, der die Datei eindeutig identifiziert (der so genannte Deskriptor der Datei). */ fd = open("myfile",o_creat O_WRONLY,0600); /* Fehlerprüfung */ if (fd==-1) { printf("fehler Nr. %d\n",errno); /* Fehlernummer ausgeben */ perror(""); /* Fehlermeldung ausgeben */ exit(-1); /* Programm beenden */ /* Den Text "HALLO" in die Datei schreiben */ err = write(fd,"hallo",strlen("hallo")); if (err==-1) {... wie oben... /* Die Datei wieder schließen */ err = close(fd); if (err==-1) {... wie oben... Wie man sieht, werden Funktionen der Programmierschnittstelle auf dieselbe Weise aufgerufen wie selbstgeschriebene C-Funktionen. Der Programmierer muss also keine neuen Techniken lernen, sondern kann wie gewohnt vorgehen. Entsprechende UNIX/Linux-Funktionen zur Steuerung nebenläufiger Aktivitäten werden ein zentrales Thema dieses Kapitels und der folgenden sein. Abschließend noch eine Bemerkung zum Programm: Der Programmtext enthält an allen Stellen, an denen Fehler auftreten können, Fehlerabfragen. Im Fehlerfall wird jeweils der Fehlercode numerisch (über die globale Variable errno) und textuell (über die zugehörige Funktion perror()) ausgegeben und das Programm beendet. Solche Fehlerbehandlungen sind zwar wichtig, blähen den Programmtext aber recht stark auf. Um die Programmbeispiele in diesem Buch übersichtlich zu halten, werden daher die Fehlerabfragen oft weggelassen; sie sollten aber in realen Programmen ausprogrammiert werden Virtualisierung BILD 2.9 zeigte die traditionelle Architektur eines Betriebssystems: Das Betriebssystem setzt auf die reale, also technisch-materiell vorhandene Hardware auf und bietet nach oben Dienste für Anwendungsprogramme und Benutzer. Charakteristisch für diese Dienste ist, dass sie von der Hardware abstrahieren, also deutlich bequemer zu nutzen sind als die Funktionen der realen Hardware. So kann beispielsweise ein Benutzer mit einem relativ komfortablen Datei-Manager arbeiten und bleibt von den Details der elektronischen Plattensteuerung verschont. Seit einiger Zeit haben zusätzliche Konzepte an Bedeutung gewonnen, die diesen Sprung von der primitiven Hardware zur komfortablen Dienstplattform nicht machen. Es handelt sich dabei um Software zur Virtualisierung der Hardware. Wie ein klassisches Betriebssystem setzt diese Software auf die reale Hardware auf und steuert sie. Nach oben werden aber Schnittstellen angeboten, die keine Veredelung der Hardware bieten, sondern auf demselben Abstraktionsniveau angesiedelt sind wie die reale Hardwareschnittstelle. Die Virtualisierungssoftware emuliert also eine Hardware.

30 2.2 Die Rolle des Betriebssystems 29 Die Virtualisierung, also die Emulation von Hardware, tritt in der Praxis in den folgenden beiden Formen auf ( BILD 2.11 bzw. BILD 2.12): In der ersten Erscheinungsform emuliert die Virtualisierungs-Software (das Host- Betriebssystem) die realen Hardware-Schnittstellen mehrerer Computer. Auf jeder dieser Schnittstellen wird ein eigenes Betriebssystem (ein Gast-Betriebssystem) mit Anwendungsprogrammen installiert. Diese Gastsysteme sind isoliert voneinander, laufen also jeweils auf einem eigenen (virtuellen) Computer. Real ist nur eine Computer-Hardware vorhanden, die durch das Host-Betriebssystem gesteuert wird und somit die Gastsysteme pseudonebenläufig ausführt. Vorteilhaft ist hier das Einsparen von Ressourcen und die Beschränkung von Systemfehlern auf jeweils eine emulierte Hardware. Xen und VMware arbeiten nach diesem Prinzip. Software-System 1 Anwendungen Gast-Betriebssystem virtuelle Hardware Nr. 1 virtuell Software-System 2 Anwendungen Gast-Betriebssystem virtuelle Hardware Nr. 2 virtuell (H O S T -) B E T R I E B S S Y S T E M reale Hardware... Software-System n virtuelle Hardware Nr. n real Anwendungen Gast-Betriebssystem virtuell VIRT REAL BILD 2.11 Hardware-Virtualisierung mit Vervielfältigung von Schnittstellen REAL = reale Hardwareschnittstelle VIRT = virtuelle Hardwareschnittstellen In der zweiten Erscheinungsform emuliert die Virtualisierungs-Software nur eine Hardware-Schnittstelle, und zwar die Schnittstelle einer virtuellen (also real so nicht vorhandenen) Hardware-Architektur. Die untere Schnittstelle dieser Software ist der realen Plattform angepasst, auf der sie läuft. Die Virtualisierungs-Software ist damit real ausführbar, verbirgt aber die Eigenschaften der realen Hardware, indem sie nach oben eine systemunabhängig-einheitliche virtuelle Hardware-Schnittstelle bietet. Software, die auf diese virtuelle Schnittstelle aufsetzt, kann also auf unterschiedlicher realer Hardware ausgeführt werden. Die Programmiersprache Java arbeitet nach diesem Prinzip ( Bild nächste Seite oben): Der Bytecode, der bei der Übersetzung eines Java-Programms entsteht, besteht aus Befehlen der virtuellen Hardware-Schnittstelle der Java Virtual Machine (JVM). Bei der Ausführung interpretiert die JVM-Software diese Befehle sie führt sie also aus, indem sie Dienste des darunterliegenden Betriebssystems und damit auch der realen Hardware nutzt. Mit Hilfe verschiedener JVMs, die der jeweiligen Plattform angepasst sind, kann derselbe Bytecode auf unterschiedlichen Plattformen laufen. Wie später gezeigt wird, erlaubt es Java mit der JVM, nebenläufige Programme zu schreiben und auszuführen. In dieser Hinsicht realisiert die JVM also Möglichkeiten eines Betriebssystems zur nebenläufigen Programmierung.

31 30 2 Basistechniken BILD 2.12 Hardware-Virtualisierung durch die Java Virtual Machine Java-Bytecode: prog.class Java Virtual Machine (JVM) Betriebssystem Hardware virtuelle Hardwareschnittstelle Programmierschnittstelle (API) des Betriebssystems reale Hardwareschnittstelle Netzdienste und verteilte Systeme Computer, die über ein Kommunikationsnetz miteinander verbunden sind, sind lose gekoppelt ( 2.1.2): Sie sind eigenständig arbeitende Rechnerknoten, die über das relativ langsame Netz Daten austauschen. Mit Hilfe des Betriebssystems und ergänzender Dienstprogramme kann jedoch auf der Basis dieser lose gekoppelten Hardware ein eng(er) gekoppeltes Softwaresystem realisiert werden. Dafür sind zwei Ansätze möglich: Der erste Ansatz ergänzt die Betriebssysteme der einzelnen Knoten durch zusätzliche Netzdienste. Durch sie kann ein Benutzer über das Netz auf die Ressourcen eines anderen Knotens zugreifen. Ein Beispiel für einen Netzdienst ist FTP (File Transfer Protocol), mit dem man im Internet Dateien überträgt. BILD 2.13 Lokale Dienste vs. Netzdienste keine Ortstransparenz lokaler Dienst: Windows Explorer Netzdienst: WS FTP Charakteristisch ist hier, dass der Benutzer bemerkt, ob er lokal (also auf dem eigenen Knoten) oder entfernt ( remote, also auf einem anderen Knoten) arbeitet. Eine Ortstransparenz (siehe nächster Absatz) liegt also nicht vor. Der zweite Ansatz realisiert ein verteiltes System mit Ortstransparenz, in dem Benutzern und Programmierern nicht bewusst wird, wo die von ihnen genutzten Dienste ablaufen. Eine Ortstransparenz findet man beispielsweise bei einem Internet-Browser, dessen graphische Oberfläche dem Benutzer gegenüber verbirgt, dass die aufgerufenen Webseiten von verschiedenen, räumlich weit verteilten Web Servern stammen.

32 2.2 Die Rolle des Betriebssystems 31 BILD 2.14 Ortstransparenz an einer Browser-Oberfläche Eine Ortstransparenz dem Benutzer gegenüber wird durch entsprechend gestaltete Benutzeroberflächen realisiert. Eine Ortstransparenz dem Programmierer gegenüber stützt sich oft auf eine Middleware. Knoten A Knoten B Knoten C lokales Betriebssystem Verteiltes Anwendungsprogramm Middleware = Verteilungsplattform lokales Betriebssystem lokales Betriebssystem einheitliche Schnittstelle auf allen Knoten lokale Schnittstellen BILD 2.15 Middleware Eine Middleware ist eine (System-)Software, die auf die Programmierschnittstellen der Betriebssysteme der einzelnen Rechnerknoten aufsetzt und nach oben, also den Anwendungsprogrammen gegenüber, eine einheitliche Schnittstelle bietet. Charakteristisch für diese Schnittstelle ist die Ortstransparenz: Bei der Nutzung ihrer Dienste muss (zumindest im hier angenommenen Idealfall) nicht angegeben werden, welcher Knoten diesen Dienst erbringt oder erbringen soll. Zugriffe auf lokal und entfernt angebotene Dienste erfolgen also auf eine einheitliche, ortsunabhängige Weise. Eine Middleware ist damit eine Verteilungsplattform, die eine Infrastruktur zur Programmierung und Nutzung verteilter Dienste und Anwendungen bietet. Zu ihren Kernaufgaben gehört die Führung von Verzeichnissen, die angeben, welche Knoten die angebotenen Dienste implementieren und wie sie aufzurufen sind, sowie die Übertragung von Dienstaufrufen und -antworten zwischen den Knoten.

33 32 2 Basistechniken In diesem Zusammenhang ist noch anzumerken, dass der Begriff des verteilten Systems nicht einheitlich verwendet wird: Oft wird bereits ein (Hardware-)System aus mehreren vollständigen Computern, die räumlich verteilt und über ein Netz miteinander verbunden sind, so bezeichnet (siehe hierzu auch 2.1.2). Ein strengerer Sprachgebrauch fordert zusätzlich eine Ortstransparenz, also das Verbergen der Verteiltheit gegenüber Benutzern und Programmierern. Sie wird, wie oben gezeigt, durch Software erreicht Betriebsarten Wie stark die Software-Nebenläufigkeit ausgeprägt ist, hängt wesentlich von der Betriebsart des Betriebssystems ab. Im Folgenden wird angenommen, dass ein Computer eine Menge von Aufträgen zu bearbeiten hat. Ferner wird angenommen, dass der Computer nur einen Hauptprozessor mit einem Kern besitzt; die Betrachtungen lassen sich aber leicht auf Systeme mit mehreren Prozessoren und/oder Kernen erweitern. ( Aufgabe 2A.2.1.) Die einfachste und älteste Betriebsart ist der Einprogrammbetrieb (engl.: single tasking mode), bei dem ein Programm die volle Kontrolle über den Computer hat. Ein zweites Programm kann erst dann gestartet werden, wenn das erste Programm seine Arbeit beendet hat. Ein typischer Ablauf sieht also so aus wie in der folgenden Abbildung dargestellt: Ein einzelner Benutzer belegt den gesamten Computer und lässt seine Aufträge streng sequentiell ausführen. Jeder Auftrag wird zunächst eingegeben, wird dann durch den Prozessor ausgeführt und gibt schließlich sein Ergebnis aus. Anschließend kommt der nächste Auftrag an die Reihe. Ist der Benutzer mit seiner Arbeit fertig, so übergibt er das Gerät an den nächsten Benutzer, der ebenso arbeitet. BILD 2.16 Einprogrammbetrieb (Single Tasking Mode) Benutzer A Benutzerwechsel Benutzer B Auftrag 1 Auftrag 2 Auftrag 3 Eingabe Ausgabe Bearbeitung Eingabe Ausgabe Bearbeitung Eingabe Ausgabe Bearbeitung Prozessor ist unbeschäftigt ( idle ) Prozessor bearbeitet einen Auftrag Offensichtlich ist beim Einprogrammbetrieb der Prozessor nur sehr schwach ausgelastet, da er während der Ein- und Ausgabe von Daten unbeschäftigt ( idle ) ist. Um die Ein-/Ausgabezeiten zu verkürzen und damit die Auslastung des Prozessors zu verbessern, wurde die Stapelverarbeitung (engl.: batch mode) eingeführt. Bei dieser Betriebsart ist die Ein-/Ausgabe automatisiert: In der Frühzeit der Informatik verwendete man für die Eingabe Lochkarten, bei denen Daten und auch Programmcode durch Lochmuster in rechteckigen Karten aus Karton codiert wurden ( Bild nächste Seite oben). Die Karten eines oder auch mehrerer Aufträge wurden zu einem Stapel (einem Batch ) zusammengefasst, der dann über einen Lochkartenleser schnell in den Computer eingelesen werden konnte. Die anschließende Ausgabe erfolgte entsprechend über einen Drucker.

34 2.2 Die Rolle des Betriebssystems 33 Auftrag 1 PROGRAMM Lochkarte BILD 2.17 Lochkartenstapel Auftrag 2 Auftrag 3 Lochkartenstapel ( Batch ) Benutzer A Benutzer B Auftrag 1 Auftrag 2 Auftrag 3 Eing. Ausg. Eing. Ausg. Eing. Ausg. Bearb. Bearb. Bearb. BILD 2.18 Stapelverarbeitung (Batch Mode) Einprogrammbetrieb und Stapelverarbeitung arbeiten streng sequentiell: Erst wenn ein Auftrag fertig bearbeitet ist, beginnt die Arbeit am nächsten. Eine streng sequentielle Auftragsbearbeitung wirkt auf uns heutige Computerbenutzer archaisch, da wir (siehe Einführung) eine starke Nebenläufigkeit gewohnt sind. Jedoch werden auch heute Aufträge, die lange Ausführungszeiten haben und keine Interaktion mit dem Benutzer erfordern, im Batch-Betrieb bearbeitet. Die Eingabe erfolgt dabei aber nicht mehr über Lochkarten, sondern aus Dateien, z.b. von Festplatten oder Magnetbändern. Der Batch-Betrieb läuft oft im Hintergrund ab, also nebenläufig zum Dialogbetrieb, bei dem die Benutzer über ihre Terminals mit dem Computer interagieren. Der erste Schritt zur Nebenläufigkeit war die Einführung des Spooling-Prinzips. Hier wurde der Hauptprozessor ergänzt durch Ein- und Ausgabeprozessoren. Während sich der Hauptprozessor nach wie vor um die eigentliche Auftragsbearbeitung kümmerte, waren die Ein- und Ausgabeprozessoren für das Einlesen der Auftragsdaten bzw. für die Ausgabe der Ergebnisse zuständig. Die Prozessoren arbeiteten echt nebenläufig, so dass während der Bearbeitung eines Auftrags schon sein Nachfolger eingelesen werden konnte, während gleichzeitig die Ergebnisse seines Vorgängers ausgegeben wurden. Die Prozessoren übertrugen Eingaben und Resultate über den Hauptspeicher und nutzten dabei die DMA-Technik (DMA = Direct Memory Access), mit der sie im raschen zeitlichen Wechsel auf den Speicher zugriffen. Offensichtlich erhöht das Spooling-Prinzip die Auslastung des Hauptprozessors und verkürzt die Wartezeiten der Aufträge. Eingabeprozessor Hauptprozessor Ausgabeprozessor Der Durchbruch zur Software-Nebenläufigkeit kam schließlich mit Einführung des Mehrprogrammbetriebs (engl.: multiprogramming und multitasking), der heute zum alltäglichen Standard geworden ist. Hier wird der Prozessor nicht nur dann zu ei- nebenläufig BILD 2.19 Stapelverarbeitung mit Spooling Aufgabe 2A.2.1.

35 Free ebooks ==> Basistechniken nem anderen Auftrag umgeschaltet, wenn der vorherige Auftrag seine Arbeit beendet hat, sondern auch zwischendurch. Dies kann beispielsweise geschehen, wenn ein Auftrag auf Benutzereingaben oder auf Daten von der Festplatte wartet und daher zur Zeit nicht weiter ausgeführt werden kann, um einen neu eingetroffenen dringenderen Auftrag bevorzugt zu bearbeiten oder um Aufträge abwechselnd zu bedienen und somit gleichmäßig voranzutreiben. BILD 2.20 Mehrprogrammbetrieb Bearbeitungsphasen eines Auftrags Frühe Systeme konnten nur dann zu einem anderen Auftrag umschalten, wenn der bisher ausgeführte Auftrag den Prozessor freiwillig aufgab beispielsweise, weil er auf eine Eingabe wartete. In der Literatur wird der Begriff Multiprogramming im Sinne dieser Einschränkung verwendet. Als Multitasking wird dann die Möglichkeit bezeichnet, einen Prozess auch von außen zu unterbrechen, ihn also gewissermaßen mit Zwang vom Prozessor zu verdrängen. Multitasking ist die Grundlage des Timesharing-Prinzips. Hier arbeiten mehrere Benutzer (jeder mit seinem eigenen Terminal, BILD 2.1 unten) gleichzeitig an einem Computer. Das Betriebssystem schaltet den Prozessor derart rasch zwischen den Aufträgen der Benutzer hin und her, dass sie gleichmäßig vorangetrieben werden und jeder Benutzer die Illusion hat, an einer eigenen Maschine zu arbeiten Prozesse und Threads Ein Betriebssystem muss die nebenläufigen Aktivitäten eines Computers verwalten. Es muss dazu Informationen über diese Aktivitäten führen, es muss entscheiden, wann welche Aktivität auf der realen Hardware ausgeführt wird, und es muss die Aktivitäten untereinander abstimmen. Zudem muss es an seinen Schnittstellen Dienste bereitstellen, über die Benutzer und Anwendungsprogramme nebenläufige Aktivitäten nutzen können. Die Verwaltung nebenläufiger Aktivitäten und die Arbeit mit ihnen beruht auf zwei grundlegenden Begriffen dem Prozess und dem Thread. Ein Prozess ist eine eigenständig ablaufende Aktivität, die gegenüber anderen Aktivitäten abgeschottet ist, also insbesondere ihren privaten Speicher besitzt. Threads können dagegen enger miteinander gekoppelt sein, indem sie über einen gemeinsamen Speicher kooperieren Prozesse Menschliche Aktivitäten (zumindest solche, die schematisch ablaufen) erfordern zweierlei: Erstens muss der Mensch wissen, was er tun soll. Ein Anfängerstudent folgt im Labor gern einer vorgegebenen Arbeitsanweisung ( BILD 2.21), und ein Wartungstechniker hat eine bestimmte Folge von Schritten abzuarbeiten. Zweitens benö-

36 2.2 Die Rolle des Betriebssystems 35 tigt der Mensch Arbeitsmittel, die er bei seiner Aktivität benutzt. So verwendet der Student die Einrichtungen seines Laborarbeitsplatzes und der Techniker seine Werkzeuge und Verbrauchsmaterialien. Ähnlich sieht es bei Aktivitäten aus, die auf einem Computer ausgeführt werden. Eine solche aktuell laufende Aktivität ist durch zwei Dinge definiert: Ihr Programm: Das Programm legt die Schritte fest, die die Aktivität ausführen soll. Ihren Kontext: Die Ausführung des Programms stützt sich auf eine Reihe von Ressourcen, nämlich die Registerinhalte des Prozessors, auf dem das Programm ausgeführt wird oder werden soll ( 2.1), die Bereiche des direkt zugreifbaren Speichers, auf die das laufende Programm zugreifen kann, insbesondere Hauptspeicherbereiche, geöffnete Dateien, auf die das Programm zugreifen kann, Peripheriegeräte, die dem Programm zugeordnet sind, sowie Verwaltungsinformationen für das Betriebssystem ( ). Beides zusammen das Programm und der zugehörige Kontext wird als Prozess bezeichnet. Prozess im Alltag: Student im Labor Programm : Kontext : Geräte Notizen Prozess im Computer: Programm: mov ax, [7890] mov [7981], ax mov ax, 0 xchg cx, ax... Speicherbereiche Kontext: Registerinhalte Schritt 1: Computer anschalten Schritt 2: Editor aufrufen Schritt 3: Programm schreiben Arbeitsanweisungen 5: Programm ausführen Schritt 4: Programm übersetzen Schritt Schritt 6: Fehler beseitigen Schritt 7: Programmtext drucken Peripheriegeräte Info Dateien BILD 2.21 Bestandteile eines Prozesses: Programm und Kontext führt aus benutzt dabei führt aus benutzt dabei Prozessor CPU Prozessor Dass ein Prozess existiert, bedeutet keineswegs, dass sich der Prozessor jederzeit mit seiner Ausführung beschäftigt. Wie bei der Einführung des Multiprogramm-Prinzips gesehen ( 2.2.2), sind meist mehr Prozesse als Prozessoren vorhanden. Im Extremfall gibt es nur einen Prozessor, der zwischen den Prozessen hin- und hergeschaltet wird. Es gibt damit zu jedem Zeitpunkt nur einen Prozess, der tatsächlich auf dem Prozessor läuft. Die anderen Prozesse warten darauf, dass ihnen das Betriebssystem den Prozessor (wieder) zuteilt.

37 36 2 Basistechniken Zusammenfassend ergibt sich die folgende Definition des Prozessbegriffs: DEFINITION Prozess Ein Prozess (engl.: task) ist ein Programm in Ausführung. Zu einem Prozess gehören der Programmcode, der die Folge der auszuführenden Befehle definiert, und ein Kontext mit den Ressourcen, die bei der Ausführung verwendet werden. Ein Prozess kann durch das Betriebssystem auf den Prozessor gebracht werden, der dann die Befehle ausführt. Prozesse haben zwei grundlegende Eigenschaften: Prozesse sind voneinander isoliert: Ein Prozess kann nicht auf den Kontext eines anderen Prozesses zugreifen (zumindest nicht ohne explizites Dazutun des anderen Prozesses und/oder des Betriebssystems), kommt also insbesondere nicht an die Daten des anderen Prozesses heran. Der Vorteil dabei ist, dass ein Prozess vor unberechtigten Zugriffen von außen geschützt ist. Nachteilig ist, dass eine unmittelbare Kooperation von Prozessen, bei der sie einen Datenbestand gemeinsam nutzen, nicht so ohne weiteres möglich ist. Prozesse sind schwergewichtig: Jeder Prozess besitzt seinen eigenen vollständigen Kontext. Vorteilhaft ist hier, dass ein Prozess unabhängig von anderen Aktivitäten ablaufen, also eigenständig ausgeführt werden kann. Nachteilig ist, dass beim Umschalten des Prozessors der volle Kontext gewechselt werden muss ( ), was zeitaufwendig sein kann Threads Bei Anwendungen mit starker Nebenläufigkeit ist es wichtig, rasch zwischen den einzelnen Aktivitäten hin- und herschalten zu können. Zudem müssen Aktivitäten oft eng kooperieren und dabei viele Daten austauschen. In beiden Fällen realisiert man die Aktivitäten besser nicht durch verschiedene schwergewichtige Prozesse, sondern durch mehrere leichtgewichtige Threads innerhalb eines Prozesses. Hier besitzt nicht jede Aktivität ihren eigenen Kontext, sondern mehrere Aktivitäten sind im selben Kontext aktiv und teilen sich damit einen Ressourcen-Bestand. Threads mit gemeinsamem Kontext lassen sich durch eine Gruppe Studierender veranschaulichen, die zusammen eine Laboraufgabe lösen. Sie sind dabei am selben Arbeitsplatz tätig und arbeiten eine gemeinsame Arbeitsanweisung ab. Möglicherweise erledigt dabei aber jeder einen anderen Teil der Aufgabe, so dass die Studierenden an unterschiedlichen Stellen der Anweisung tätig sind. Auf Threads in Computern übertragen bedeutet der letzte Satz, dass Threads durch einen gemeinsamen Programmtext gesteuert werden, aber jeweils unterschiedliche Teile des Programms ausführen. Das ist möglich, da jeder Thread seinen eigenen Befehlszähler (Instruction Pointer) besitzt und somit seinen eigenen Weg ( Kontrollfluss ) durch die Folge der Programmbefehle realisiert. Visualisieren lässt sich ein solcher Kontrollfluss durch einen Faden, der sich durch die Folge der ausgeführten Programmbefehle zieht wodurch sich dann auch der Name Thread erklärt.

38 2.2 Die Rolle des Betriebssystems 37 Zusammenfassend erhält man für den Thread-Begriff die folgende Definition: Ein Thread ist eine nebenläufig auszuführende Aktivität innerhalb eines Prozesses. Der Prozess kann dabei einen oder mehrere Threads enthalten. Jeder Thread besitzt nur einen kleinen eigenen Datenbestand (insbesondere die Registerinhalte mit Instruction und Stack Pointer sowie den Aufrufstack); der größte Teil des Kontexts (insbesondere der Adressraum der Speichervariablen) ist dem Prozess zugeordnet und wird von allen seinen Threads gemeinsam benutzt. DEFINITION Thread Prozess (Task): Programm: Kontext: für alle Threads gemeinsam: BILD 2.22 Prozess mit mehreren Threads Adressraum / Variablen... Dateien Geräte für jeden Thread gesondert: Thread 1: Thread 2: Registerinhalte: Befehlszähler Stack Pointer... Registerinhalte: Befehlszähler Stack Pointer... Thread 1 Thread 2 Aufrufstack... Aufrufstack... Threads haben zwei grundlegende Eigenschaften: Threads desselben Prozesses sind eng miteinander gekoppelt: Sie arbeiten auf einem gemeinsamen Variablenbestand im direkt zugreifbaren Speicher und können so rasch Daten untereinander austauschen. Der Speicherbereich eines Threads ist gegen Zugriffe eines anderen Threads nicht geschützt. Threads sind leichtgewichtig: Zwischen zwei Threads desselben Prozesses kann der Prozessor rasch umgeschaltet werden, da der Kontext nicht gewechselt werden muss Der Lebenszyklus Das Leben eines Prozesses oder Threads hat den folgenden Ablauf (ab hier wird der Einfachheit halber nur von Prozessen auf Einprozessorsystemen die Rede sein; das Gesagte gilt aber ebenso für Threads und für Mehrkern- und Multiprozessorsysteme): Ein Prozess wird dynamisch erzeugt also bei Bedarf zu einem beliebigen Zeitpunkt. Im Prinzip können beliebig viele Prozesse neu entstehen, in der Praxis gibt es aber Obergrenzen für die Gesamtzahl der Prozesse in einem System und die Zahl der Prozesse eines einzelnen Benutzers.

39 38 2 Basistechniken Aufgabe 2A.3.1. BILD 2.23 Lebenszyklus eines Prozesses Der Prozess durchläuft anschließend eine oder mehrere Phasen der folgenden Art: Der Prozess ist rechnend (engl.: running), wird also auf dem Prozessor ausgeführt. Der Prozess ist bereit (engl.: ready), könnte also auf dem Prozessor ausgeführt werden, wird es aber nicht, da zur Zeit ein anderer Prozess ausgeführt wird. Der Prozess ist blockiert (engl.: blocked), kann also nicht weiter ausgeführt werden, da er zur Zeit auf ein bestimmtes Ereignis wartet zum Beispiel auf eine Eingabe des Benutzers, die Rückmeldung eines Ein-/Ausgabegeräts oder der Festplatte, ein Zeitsignal oder die Beendigung der Aktion eines anderen Prozesses. Schließlich terminiert der Prozess, beendet sich also selbst oder wird durch ein Signal von außen beendet. (Dazu eine Randbemerkung: Das Verb terminieren im Sprachgebrauch der Informatik stammt vom englischen Wort to terminate und bedeutet somit beenden, also nicht einen Termin festlegen.) Graphisch lässt sich der Lebenszyklus eines Prozesses durch das folgende Zustandsübergangsdiagramm beschreiben: Prozess startet Prozess erhält die CPU Prozess terminiert bereit rechnend Prozess verliert die CPU Ereignis ist eingetreten Prozess terminiert Prozess wartet auf ein Ereignis Prozess terminiert blockiert Ein Prozess befindet sich also jeweils in einem bestimmten Zustand, der angibt, ob er gerade ausgeführt wird und ob er überhaupt ausgeführt werden kann. Er kann durch eigene Aktionen, Aktionen des Betriebssystems oder bestimmte Ereignisse in einen anderen Zustand übergehen. Dabei gilt insbesondere Folgendes: Ein neu gestarteter oder entblockierter Prozess geht stets in den bereit-zustand über. Er erhält also nicht sofort den Prozessor, sondern muss warten, bis ihm das Betriebssystem ihn zuteilt. Ein Prozess kann aus jedem Zustand heraus beendet werden. Dies kann durch eine Aktion des Betriebssystems, eines anderen Prozesses oder eines Benutzers geschehen. Ein Prozess im Zustand rechnend kann sich zudem selbst beenden, indem er einen entsprechenden Terminierungsbefehl ausführt oder das Ende seines Programmtexts erreicht.

40 Free ebooks ==> Die Rolle des Betriebssystems 39 Das Diagramm in BILD 2.23 stellt eine Minimalform dar; in realen Betriebssystemen gibt es oft weitere Zustände. So kennt UNIX/Linux beispielsweise den Zombie-Zustand, in den Prozesse nach ihrer Terminierung übergehen und in dem man noch Informationen über die Prozesse abfragen kann Implementierungsaspekte Betriebssysteme implementieren Software-Nebenläufigkeit ausschließlich durch Prozesse und Threads: Jede nebenläufige Aktivität im System wird durch einen Prozess oder einen Thread dargestellt; das Betriebssystem bringt Prozesse und Threads auf der Hardware zur Ausführung und steuert ihr Zusammenspiel. Dieser Abschnitt spricht die grundlegendsten Aspekte der Implementierung von Prozessen und Threads kurz an; eine ausführliche Darstellung findet man in der einschlägigen Betriebssysteme-Literatur. Im Folgenden wird zur Vereinfachung meist nur von Prozessen die Rede sein; die Aussagen gelten aber jeweils auch für Threads Buchführung Ein Prozess wird durch eine Prozessnummer (engl.: process identifier, PID), meist eine nichtnegative ganze Zahl, eindeutig identifiziert. Die PID wird dem Prozess bei seiner Erzeugung fest zugeordnet und bleibt während seiner gesamten Lebensdauer unverändert. Ein Prozess gehört einem Benutzer oder auch dem Betriebssystem, erledigt also eine Aufgabe in dessen Auftrag. Jeder Prozess wird durch einen eigenen Prozesskontrollblock (engl. process control block, PCB) beschrieben, der sämtliche relevanten Informationen über den Prozess enthält. Dazu gehören typischerweise seine PID, sein Besitzer (also der Benutzer, dem er zugeordnet ist, oder das Betriebssystem sowie eventuell eine Benutzergruppe), sein aktueller Zustand, das von ihm ausgeführte Programm, Angaben über sämtliche Bestandteile seines Kontexts ( ), sein Home Directory (also das Verzeichnis, in dem die Dateien und Unterverzeichnisse des Prozesses stehen) und sein aktuelles Arbeitsverzeichnis im Dateisystem, Signale (also Meldungen von außen, auf die er reagieren muss), Verwaltungsinformationen (zum Beispiel seine Priorität gegenüber anderen Prozessen, Eltern-Kind- und Gruppenbeziehungen zu anderen Prozessen sowie Quoten, also Obergrenzen von Ressourcen, die dem Prozess zugeteilt werden können) sowie Abrechnungsdaten (zum Beispiel sein Startzeitpunkt und die bisher verbrauchte Prozessorzeit).

41 40 2 Basistechniken Das Betriebssystem führt eine Prozesstabelle (engl.: process table), in der die Prozesskontrollblöcke aller existierenden Prozesse zusammengefasst sind. Mit dieser Tabelle hat also das Betriebssystem eine vollständige Übersicht über die aktuell laufenden Aktivitäten im System. BILD 2.24 Prozesstabelle und Prozesskontrollblöcke Prozesstabelle (global für alle Prozesse)... Prozesskontrollblöcke (PCBs) (pro Prozess ein eigener) Info über Prozess 0 Info über Prozess Info 1 über Prozess 2 BILD 2.25 Dispatching Dispatching Prozessoren werden zwischen Prozessen umgeschaltet, um jedem von ihnen Ausführungszeit zu geben. Der technische Vorgang des Umschaltens wird als Dispatching bezeichnet. Der Dispatch-Vorgang ist interrupt-gesteuert, wird also durch ein Hardware-Signal initiiert, das beispielsweise von der Systemuhr kommt. Das Dispatching muss sicherstellen, dass ein Prozess, der den Prozessor nach einer Unterbrechung erneut zugeteilt bekommt, an der Stelle fortsetzen kann, an der er früher unterbrochen wurde. Hierzu werden beim Dispatching die Registerinhalte gerettet, also im PCB des Prozesses, der vom Prozessor verdrängt wird, gespeichert. Anschließend werden aus dem PCB des Prozesses, der den Prozessor nun erhalten soll, die entsprechenden Werte in die Prozessorregister übertragen. Prozessor Register 1. Retten 2. Neu setzen PCB des verdrängten Prozesses... Registerinhalte... PCB des auszuführenden Prozesses... Registerinhalte Scheduling Dem Dispatching übergeordnet ist das Scheduling. Das Scheduling trifft strategische Entscheidungen, wann Umschaltvorgänge stattfinden und zu welchem Prozess dann umgeschaltet wird. Es entscheidet also, wann und für wie lange die einzelnen Prozesse auf dem Prozessor ausgeführt werden. Man unterscheidet zwei Arten des Schedulings ( Bild nächste Seite): Beim nichtunterbrechenden (engl.: non-preemptive) Scheduling kann das Betriebssystem den aktuell ausgeführten Prozess nicht vom Prozessor verdrängen, sondern es muss warten, bis er ihn freiwillig freigibt. Im Extremfall wird ein dringender Prozess verzögert, bis der laufende, möglicherweise weniger dringende Prozess seine Ausführung beendet hat.

42 2.2 Die Rolle des Betriebssystems 41 nicht-unterbrechendes Scheduling: P low läuft bis zum Ende P high kommt P high wartet unterbrechendes Scheduling: P high dringend, P low weniger dringend P high läuft P low fertig BILD 2.26 Nichtunterbrechendes vs. unterbrechendes Scheduling P low läuft P high läuft P low läuft weiter P high kommt P low wartet P high fertig Beim unterbrechenden (engl.: preemptive) Scheduling kann das Betriebssystem den aktuell ausgeführten Prozess von außen unterbrechen, um beispielsweise einen dringenderen Prozess auf den Prozessor zu bringen. Unterbrechendes Prozessor-Scheduling ist in heutigen Betriebssystemen Stand der Technik. In Allzweck-Betriebssystemen werden insbesondere die beiden folgenden Scheduling-Strategien eingesetzt: Beim prioritätengesteuerten Scheduling erhält jeder Prozess einen Prioritätswert, der seine Dringlichkeit ausdrückt. Es wird jeweils der Prozess mit der höchsten Priorität ausgeführt; Prozesse mit niedrigerer Priorität werden dazu, falls nötig, vom Prozessor verdrängt. Prioritäten können statisch oder dynamisch sein, also während der gesamten Lebenszeit des Prozesses gleich bleiben bzw. sich ändern. Hohe statische Prioritäten werden benutzt, um fristgebundene Realzeitprozesse zeitgerecht auszuführen. Dynamische Prioritäten können zum Beispiel mit dem Alter des Prozesses steigen, um ein Verhungern von Prozessen, also ein endloses Warten, zu verhindern. Dynamische Prioritäten können auch das bisherige Verhalten der Prozesse berücksichtigen also beispielsweise bei Prozessen, die in jüngster Zeit viel Prozessorzeit bekommen haben, gesenkt werden, um auch anderen Prozessen ein Chance zu geben. Beim zeitscheibenbasierten Scheduling (engl.: round robin scheduling) sind die Prozesse gedanklich in einem Kreis angeordnet. Jeder Prozess erhält den Prozessor maximal für eine bestimmte Zeitdauer (die Zeitscheibe, engl.: time slice) und gibt ihn dann an den nächsten Prozess im Kreis weiter. Je länger die Gesamtausführungsdauer eines Prozesses ist, um so mehr Ausführungszyklen benötigt er. Betriebssysteme wie UNIX und Linux benutzen eine Kombination dieser beiden Ansätze. So werden hohe statische Prioritäten an Realzeitprozesse vergeben und niedrigere dynamische Prioritäten an nicht fristengebundene Prozesse. Die dynamischen Prioritäten werden so berechnet, dass sich die Prozessorzeit möglichst gleichmäßig auf die Prozesse verteilt beispielsweise erhält ein Prozess, der lange gewartet hat, eine höhere Priorität. Prozesse mit derselben Priorität werden nach dem Round-Robin-Schema behandelt. Aufgabe 2A.2.2.

43 42 2 Basistechniken 2.3 Prozesse und Threads in UNIX/Linux Die Software-Nebenläufigkeit in UNIX und Linux beruht ausschließlich auf Prozessen und Threads. Bei Eingabe der meisten Benutzerkommandos wird ein neuer Prozess erzeugt, der die Ausführung des Kommandos übernimmt und anschließend terminiert. Zudem gibt es an der C-Programmierschnittstelle Funktionen, mit denen Prozesse und Threads dynamisch ins Leben gerufen werden können. Weitere Kommandos und Funktionen dienen zur Abfrage von Informationen über Prozesse/ Threads und ihre Steuerung. Prozesse und Threads werden (mit Ausnahme des allerersten Prozesses, siehe unten) stets durch andere Prozesse bzw. Threads erzeugt. Hierdurch entstehen Vater-Sohn- Beziehungen (manchmal auch geschlechtsneutral: Eltern-Kind, engl.: parent-child genannt). Ein neuer Prozess ist ein Sohn des Prozesses, der die Funktion zu seiner Erzeugung aufgerufen hat; dieser wird damit zu seinem Vaterprozess. Da Sohnprozesse ihrerseits Prozesse erzeugen können, entsteht eine baumstrukturierte Hierarchie von Prozessen (siehe auch BILD 2.27). Die Wurzel dieser Hierarchie ist der Prozess, der beim Start des Systems als erster entsteht und von dem alle anderen Prozesse direkt oder indirekt abstammen Kommandos der Benutzerschnittstelle An der Benutzerschnittstelle erzeugt man Prozesse implizit, indem man (wie oben geschildert) Kommandos eingibt. Hierzu gehört auch der Aufruf eines Shellscripts (also einer Datei, die eine Folge von Benutzerkommandos enthält) oder einer Maschinenprogrammdatei (also einer ausführbaren Datei mit Maschinenbefehlen, die durch Übersetzung eines höhersprachigen Programms entstanden ist, ). Das grundlegende UNIX/Linux-Kommando zur Kontrolle von Prozessen ist ps, mit dem man sich den aktuellen Prozess-Status anzeigen lassen kann. Das Kommando lässt sich mit zahlreichen verschiedenen Optionen nutzen, so zum Beispiel den folgenden ( Bild nächste Seite): ( Aufgabe 2A.3.2.) ps -f und ps -l geben Prozessinformationen in Voll- bzw. Langform aus. Angezeigt werden für jeden Prozess: F: Flags des Prozesses, u.a. Angabe, ob der Prozess Super User -Rechte, also Administrator-Privilegien hat (nur ps -l) S: aktueller Zustand des Prozesses (nur ps -l) UID: User Identifier des Besitzers des Prozesses (root steht hier für den Super User, also den Administrator und das Betriebssystem) PID: Process Identifier, also die eindeutige Nummer des Prozesses PPID: Parent Process Identifier, also die Nummer des Vaterprozesses C: bisher benötigte CPU-Zeit, gewichtet (CPU-Zeit, deren Benutzung weit zurückliegt, wird schwächer gewichtet als unmittelbar zuvor gebrauchte Zeit)

44 2.3 Prozesse und Threads in UNIX/Linux 43 Ausgabe von ps -f: UID PID PPID C STIME TTY TIME CMD gonzo :13 pts/0 00:00:00 -bash gonzo :47 pts/0 00:00:00./prog1 gonzo :47 pts/0 00:00:00./prog2 gonzo :47 pts/0 00:00:00./prog2 gonzo :47 pts/0 00:00:00 ps -f BILD 2.27 Ausgaben des ps- Kommandos Ausgabe von ps -l: F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 0 S wait pts/0 00:00:00 bash 0 S hrtime pts/0 00:00:00 prog1 0 S hrtime pts/0 00:00:00 prog2 1 S hrtime pts/0 00:00:00 prog2 0 R pts/0 00:00:00 ps Ausgabe von ps -ef: UID PID PPID C STIME TTY TIME CMD root Mar14? 00:00:01 /sbin/init root Mar14? 00:00:00 [kthreadd] root Mar14? 00:00:00 [migration/0]... gonzo :13? 00:00:00 sshd: gonzo@pts/0 gonzo :13 pts/0 00:00:00 -bash gonzo :47 pts/0 00:00:00./prog1 gonzo :47 pts/0 00:00:00./prog2 gonzo :47 pts/0 00:00:00./prog2 gonzo :48 pts/0 00:00:00 ps -ef 6936: sshd Vater-Sohn-Beziehungen der Prozesse 6937: bash 6962: prog1 6963: prog2 6967: ps -ef 6964: prog2 PRI: Priorität des Prozesses (nur ps -l) NI: Nice -Wert, mit dem die Priorität additiv verändert, insbesondere freiwillig verschlechtert werden kann (nur ps -l) STIME: Startzeit des Prozesses (nur ps -f) ADDR: Speicheradresse des Prozesses (nur ps -l)

45 Free ebooks ==> Basistechniken SZ: Speichergröße des Prozesses (nur ps -l) WCHAN: Ereignis, auf das der Prozess wartet ( Wait Channel, nur ps -l) TTY: Terminal (also Ein-/Ausgabe-Gerät), dem der Prozess zugeordnet ist TIME: bisher benötigte CPU-Zeit, absolut CMD: Kommando, das der Prozess ausführt (auch Name einer Maschinenprogramm-Datei oder eines Shellscripts) Wählt man zusätzlich die Option e, so wird Information über sämtliche Prozesse im System angezeigt. Ansonsten bekommt man nur die Prozesse zu sehen, die vom aktuellen Terminal aus gestartet wurden. BILD 2.28 Ausgaben des ps- Kommandos (Fortsetzung) Ausgabe von ps -fu gonzo: UID PID PPID C STIME TTY TIME CMD gonzo :47? 00:00:00 sshd: gonzo@pts/0 gonzo :47 pts/0 00:00:00 -bash gonzo :50? 00:00:00 sshd: gonzo@pts/1 gonzo :50 pts/1 00:00:00 -bash gonzo :50 pts/0 00:00:00 ps -fu gonzo Mit der Option u lassen sich alle Prozesse anzeigen, die einem bestimmten Benutzer zugeordnet sind. Die Ausgabe in BILD 2.28 zeigt die Prozesse des Benutzers gonzo, der zweimal eingeloggt ist. Er besitzt damit zwei Secure Shell Demon - Prozesse und zwei bash-prozesse. Ein Shell-Demon-Prozess steuert die Verbindung zwischen dem PC, von dem aus der Benutzer sich über das Netz in den Linux-Computer eingeloggt hat, und eben diesem Linux-Computer. Die bash ist eine Shell, also ein Programm, das UNIX/Linux-Benutzerkommandos entgegennimmt und ihre Ausführung steuert. Über die Angaben in den Spalten PID und PPID kann man die Vater-Sohn-Beziehungen zwischen den Prozessen ermitteln. BILD 2.27 zeigt unten einen entsprechenden Baum, der sich aus der ps-ausgabe direkt darüber ergibt: Wurzel des (Teil-)Baums ist ein Secure Shell Demon -Prozess (siehe oben). Der Shell-Demon-Prozess ist Vater eines Prozesses, der die Shell (in diesem Fall die bash ) ausführt. Der hier gezeigte Shell-Prozess wurde gestartet, als sich der Benutzer gonzo einloggte, und kümmert sich seitdem um die Ausführung seiner Kommandos. Der Shell-Prozess startet für (fast) jedes Kommando, das der Benutzer eingibt, einen eigenen Prozess und wird damit dessen Vater. Momentan sind drei solche Prozesse aktiv. Sie führen die Maschinenprogrammdateien prog1 und prog2 sowie das ps-kommando aus. Man beachte bei den Ausgaben in BILD 2.27 und BILD 2.28, dass die ps- Prozesse unterschiedliche Prozessnummern haben. Für die drei eingegebenen ps- Kommandos wird also jeweils ein eigener neuer Prozess erzeugt, der nur diese Eingabe bearbeitet und anschließend sofort terminiert.

46 2.3 Prozesse und Threads in UNIX/Linux 45 Aus dem Maschinenprogramm prog2 heraus wurde ein weiterer Prozess gestartet, der ebenfalls prog2 ausführt (siehe hierzu die Funktion fork() in ). Weitere nützliche Befehle zur Steuerung von Prozessen sind die folgenden: Mit kill kann man ein Signal an einen Prozess schicken. UNIX und Linux definieren verschiedene Arten von Signalen insbesondere das Signal SIGKILL, mit dem man einen Prozess abbrechen, also von außen terminieren kann. Beispielsweise stoppt der Aufruf kill -SIGKILL 1234 den Prozess mit der PID In der Praxis schreibt man oft kill -9..., da SIG- KILL eine symbolische Konstante üblicherweise mit dem Wert 9 ist. sleep erzeugt einen Prozess, der sich für eine bestimmte Zeitdauer blockiert und dann terminiert. Man kann diesen Befehl einem zweiten Befehl vorschalten und somit dessen Ausführung verzögern. Beispielsweise lässt der Aufruf (sleep 5400; echo Vorlesung zu Ende ) & nach einer Verzögerung von 90 Minuten (= 5400 Sekunden) den Text Vorlesung zu Ende auf dem Bildschirm erscheinen. Das & am Ende der Eingabe führt dazu, dass die Befehle im Hintergrund ausgeführt werden. Die Tastatur wird also während der Ausführung nicht blockiert, sondern bleibt frei zur Eingabe weiterer Kommandos. wait erzeugt einen Prozess, der auf einen anderen Prozess wartet sich also blockiert, bis der andere Prozess terminiert. Auch diesen Befehl kann man einem zweiten Befehl vorschalten. Beispielsweise führt der Aufruf (wait 1234; exit) & ein Logout aus, sobald der Prozess mit der PID 1234 beendet ist Grundlegende API-Funktionen für Prozesse Das Thema des Buchs ist die Programmierung nebenläufiger Anwendungen. Man kann hierzu die Funktionen benutzen, die das Betriebssystem an seiner Programmierschnittstelle (an seiner API, ) bereitstellt. Da die UNIX/Linux-API Funktionsköpfe (Funktionsschnittstellen, Prototypen ) in der höheren Programmiersprache C definiert, lassen sich die Dienste von UNIX/Linux aus C-Programmen heraus unmittelbar nutzen ( PROG 2.1 in ). Dieses Buch beschreibt nur die wichtigsten Aspekte der UNIX/Linux-Programmierschnittstelle. Insbesondere nennt es nur die am häufigsten auftretenden Parameterund Rückgabewerte der Funktionen. Eine vollständige Dokumentation findet man in den Man(ual) Pages von UNIX/Linux, die man online unter [Linux] oder (wenn man auf einem UNIX/Linux-System arbeitet) durch Eingabe des man-kommandos einsehen kann. Sehr ausführliche Darstellungen mit zahlreichen Beispielen bieten auch die Standardwerke [Stev05], [Robb03] und [Hero04].

47 46 2 Basistechniken Zu Beginn noch zwei praktische Anmerkungen: Die include-dateien, die im Folgenden jeweils genannt werden, definieren Konstanten, Typen und Funktionsschnittstellen, die im Zusammenhang mit den beschriebenen Funktionen stehen. Die Funktionen selbst sind jedoch meist auch ohne die entsprechende #include-anweisung bekannt, d.h. Quellcodeprogramme lassen sich ohne ein explizites #include übersetzen. In den Beispielprogrammen werden Fehlerabfragen zur besseren Lesbarkeit meist weggelassen. Für die Praxis gilt aber das, was in gesagt wurde. ( Aufgaben 2A ) Die Funktion fork() Die fundamentale UNIX/Linux-Funktion zur Arbeit mit Prozessen ist fork(). Sie ist die einzige Funktion, mit der Prozesse dynamisch erzeugt und gestartet werden können. fork() erhält keinen Parameter und liefert einen Ganzzahlwert zurück: pid_t fork(void) #include <unistd.h> Rückgabe an den erzeugenden Vaterprozess: Die PID des neu erzeugten Sohns (wenn erfolgreich) 1 (im Fehlerfall) Rückgabe an den erzeugten Sohnprozess: 0 pid_t ist ein systemabhängiger Ganzzahltyp, z.b. int oder long fork() erzeugt eine identische Kopie des Prozesses, der die Funktion aufgerufen hat aus dem aufrufenden Vaterprozess entsteht ein gleich aussehender Sohnprozess. Dieser Vorgang mag auf den ersten Blick etwas verwirrend wirken, und daher werden ihn eine Reihe von Beispielen ( BILD 2.29, ) erhellen. Zunächst aber sollen die grundlegenden Auswirkungen von fork() und die Eigenschaften des Vaterund des Sohnprozesses genannt werden: Vater und Sohn sind eigenständige Prozesse; jeder besitzt also seine eigene PID. ( Aufgabe 2A.3.6.) Der Speicher des Sohns entsteht als Kopie des Speichers des Vaters. Insbesondere werden alle Variablen mit ihren Namen und aktuellen Werten vom Vater zum Sohn kopiert. Anschließend haben Vater und Sohn ihre eigenen Speicherbereiche und arbeiten jeweils auf ihren eigenen Variablen. Die Bereiche sind voneinander isoliert: Der Vater kann nicht ohne weiteres auf den Speicher des Sohns zugreifen, der Sohn kommt nicht an den Speicher des Vaters heran ( PROG 2.3 in ). Vater und Sohn führen dasselbe Programm aus und dies zunächst sogar an derselben Stelle, nämlich unmittelbar hinter dem Aufruf von fork() (denn weil der Vater fork() aufgerufen hat und der Sohn eine Kopie des Vaters ist, kehren beide und zwar jeder für sich aus dem fork()-aufruf zurück). Damit Vater und Sohn dennoch anschließend unterschiedliche Dinge tun können, liefert ihnen fork() unterschiedliche Rückgabewerte: Der Vater erhält die PID seines neuen Sohns zurück, die stets ungleich 0 ist; der Sohn erhält eine 0. Man kann den Rückgabewert in einer if-anweisung auf Gleichheit mit 0 prüfen und damit Vater und Sohn in verschiedene Teile des Programms verzweigen lassen:

48 2.3 Prozesse und Threads in UNIX/Linux 47 if (fork()==0) {... Code des Sohns Code des Vaters... Beispiele für diese Programmiertechnik und eine vertiefende Diskussion folgen in BILD 2.29 in zeigt eine entsprechende ps-ausgabe für ein Programm prog2, in dem ein Vaterprozess durch fork() einen Sohnprozess erzeugt hat. BILD 2.29 zeigt ein Analogbeispiel für diese Vorgehensweise und Eigenschaften von fork(). 1. Ein Mann betritt die Klinik, um sich klonen zu lassen. Klon-Klinik Dr. Fork Eingang Ausgang Mann = Prozess Klinik betreten = fork() aufrufen BILD 2.29 Funktionsweise von fork() ein Analogbeispiel Peter Schmitz 2. Zwei identische Männer verlassen die Klinik: Sie tun zunächst dasselbe am selben Ort, sie haben zunächst dieselbe Laune, sie sind aber zwei eigenständige Individuen mit eigenen Namen. Klon-Klinik Dr. Fork Eingang Ausgang Peter Schmitz Paul Schmitz 3. Dann können sie ihrer eigenen Wege gehen und ihre Laune individuell ändern. Klinik verlassen = aus fork() zurückkehren etwas am selben Ort tun = ein Programm an derselben Stelle ausführen Laune = Speicherinhalt Name = PID Klon-Klinik Dr. Fork Eingang Ausgang Peter Schmitz Paul Schmitz eigene Wege gehen = ein Programm an verschiedenen Stellen ausführen Laune individuell ändern = eigenen Speicherinhalt ändern (unabhängig vom anderen)

49 48 2 Basistechniken Weitere Funktionen fork() lässt sich nur im Verbund mit anderen Funktionen sinnvoll einsetzen. Daher sollen zunächst einige dieser Funktionen allgemein eingeführt werden, bevor im nächsten Abschnitt ( ) entsprechende Beispielprogramme folgen. In den anschließenden Kapiteln geht es dann um spezielle Funktionen zur Synchronisation von Prozessen und zur Datenübertragung zwischen ihnen. exit() lässt den aufrufenden Prozess terminieren; der Prozess beendet sich mit dem Aufruf also selbst. Er terminiert ebenfalls, wenn er das Ende seines Programmtexts erreicht; hier muss exit() nicht explizit aufgerufen werden. void exit(int status) #include <stlib.h> Rückkehrstatus zur Übergabe an den Vater ( Aufgaben 2A.3.2./7.) exit() erhält als Parameter den Rückkehrstatus eine ganze Zahl, über die der terminierende Prozess seinem Vater melden kann, ob seine Ausführung erfolgreich abgeschlossen wurde. 0 steht dabei üblicherweise für Alles in Ordnung, eine Zahl ungleich 0 codiert einen Fehler oder eine spezifische Meldung. Statt numerischer Werte kann man auch die symbolischen Konstanten EXIT_SUCCESS und EXIT_FAILURE aus stdlib.h verwenden. Der Vater verschafft sich den Rückkehrstatus durch Aufruf von wait() oder waitpid(). wait() und waitpid() lassen den aufrufenden Prozess auf das Ende eines seiner Sohnprozesse warten. Bei wait() verbleibt der Prozess so lange im Zustand blockiert, bis ein beliebiger Sohnprozess terminiert ist; bei waitpid() blockiert er, bis ein bestimmter Prozess beendet ist. pid_t wait(int *status) #include <sys/types.h> #include <sys/wait.h> Rückkehrstatus des Sohns (Rückgabeparameter!) PID des terminierten Sohns ( 1 im Fehlerfall) pid_t waitpid(pid_t pid, int *status, int options) im Normalfall 0 Rückkehrstatus des Sohns PID des Sohns, auf den gewartet werden soll PID des terminierten Sohns ( 1 im Fehlerfall) Man beachte, dass der status-parameter ein Rückgabeparameter ist: Hierüber erhält der Vaterprozess den exit()-rückgabecode des Sohns und kann ihn anschließend auswerten ( PROG 2.5). Ein terminierter Sohnprozess verbleibt im Zombie-Zustand ( ), bis ihn sein Vater durch wait() oder waitpid() von dort abholt. Erst dann wird er endgültig gelöscht. Führt also der Vater wait()/waitpid() aus, wenn sich ein

50 Free ebooks ==> Prozesse und Threads in UNIX/Linux 49 bzw. der Sohn bereits im Zombiezustand befindet, so wird er nicht blockiert, sondern der Abholvorgang findet unmittelbar statt. Terminiert der Vater vor seinem Sohn, ohne ein wait() oder waitpid() auszuführen, so wird der init-prozess Nr. 1 zum Vater des Waisen und führt den entsprechenden wait()-aufruf aus. Die Funktion sleep() blockiert den aufrufenden Prozess für eine bestimmte Anzahl von Sekunden. ( Aufgabe 2A.3.5.) unsigned int sleep(unsigned int seconds) #include <unistd.h> Schlafzeit in Sekunden 0, wenn Schlafzeit verstrichen Anzahl verbleibende Schlafsekunden, wenn Schlaf durch Signal unterbrochen wurde Mit sleep() kann das Zeitverhalten eines Prozesses nur relativ grob gesteuert werden. Das liegt nicht nur daran, dass der Parameter lediglich eine Auflösung im Sekundenbereich hat, sondern auch daran, dass ein Prozess nach seinem Erwachen im Allgemeinen nicht sofort weiterläuft. Er geht vielmehr zunächst in den Zustand bereit über ( ) und muss dort auf die Zuteilung des Prozessors warten. Der Programmierer hat keinen Einfluss auf die Länge dieser Wartezeit. getpid() (get process ID) und getppid() (get parent process ID) liefern dem aufrufenden Prozess seine eigene Prozessnummer bzw. die seines Vaters. ( Aufgabe 2A.3.3.) pid_t getpid(void) #include <sys/types.h> #include <unistd.h> PID des aufrufenden Prozesses pid_t getppid(void) PID des Vaters des aufrufenden Prozesses Es gibt keine Funktion, mit der ein Prozess die IDs seiner Söhne abfragen könnte. Er muss also unmittelbar beim fork()-aufruf den Rückgabewert speichern, um später die ID des neu entstandenen Sohnprozesses zur Verfügung zu haben (siehe hierzu PROG 2.4). Mit execv() führt der aufrufende Prozess ein Maschinenprogramm aus, das in einer anderen Datei steht ( PROG 2.6). Im fehlerfreien Fall terminiert der Prozess am Ende der Ausführung dieses Programms; er kehrt also nur dann aus dem execv()-aufruf zurück, wenn ein Fehler aufgetreten ist. int execv(const char *path, char *const argv[]) Parameter für das gerufene Programm Pfad und Name der auszuführenden Datei 1 im Fehlerfall #include <unistd.h>

51 50 2 Basistechniken ( Aufgabe 2A.3.8.) Schließlich gibt es die Funktion kill(), mit der man ein Signal an einen anderen Prozess schicken kann. Im folgenden Abschnitt wird die Funktion zum Terminieren eines anderen Prozesses verwendet ( PROG 2.7: kill(pid,sigkill)); in ihrer allgemeinen Form wird die Funktion in eingeführt Programmbeispiele Das erste Programm ist ein Minimalbeispiel für die Anwendung von fork(): PROG 2.2 Erzeugung eines Sohnprozesses durch fork() #include <stdio.h> #include <stdlib.h> main() { if (fork()==0) { /* Code des Sohns (im if-block) */ printf("hier ist der Sohn\n"); exit(0); /* Terminierung des Sohns! */ /* Code des Vaters (im Programmteil nach if) */ printf("hier ist der Vater\n"); Wie BILD 2.30 zeigt, dupliziert fork() den aufrufenden Prozess. Es existieren also anschließend zwei Prozesse, von denen jeder für sich dieses Programm ausführt der Vater- und der neu entstandene Sohnprozess. Da der Sohn den fork()-rückgabewert 0 erhält, verzweigt er in den if-block. Der Vater erhält dagegen die PID des Sohns zurück, die ungleich 0 ist. Er setzt daher seine Ausführung hinter dem if-block fort. BILD 2.30 Ein Prozess ( ) ruft fork() auf. Wirkungsweise von fork() UNIX/Linux kopiert den Prozess innerhalb des Aufrufs. if (fork()==0) {... if (fork()==0) {... Zwei identische Prozesse kehren aus fork() zurück: Der bereits vorhandene Vater ( ) und der neu erzeugte Sohn ( ). if (fork()==0) { printf("hier ist der Sohn\n"); exit(0); printf("hier ist der Vater\n"); Der fork()-rückgabewert an den Vater ist die PID des Sohns 0. Der Vater springt hinter den if-block. if (fork()==0) { printf("hier ist der Sohn\n"); exit(0); printf("hier ist der Vater\n"); Der fork()-rückgabewert an den Sohn ist 0. Der Sohn springt in den if-block.

52 2.3 Prozesse und Threads in UNIX/Linux 51 Das exit() am Ende des Codes des Sohns ist wichtig! Würde es fehlen, so würde der Sohn in den Code des Vaters hineinlaufen und auch dessen Anweisungen ausführen, was meist nicht im Sinn des Programmierers ist. Beim Vater darf das exit() dagegen fehlen, denn er beendet sich automatisch, wenn das Ende des Programmtexts erreicht ist. Das zweite Programm demonstriert, dass Vater und Sohn auf jeweils eigenen Variablen arbeiten selbst dann, wenn diese denselben Namen tragen: #include <stdio.h> #include <stdlib.h> main() { int i = 1; if (fork()==0) { printf("i im Sohn: %d\n",i); /* Ausgabe: 1 */ i = 2; printf("i im Sohn: %d\n",i); /* Ausgabe: 2 */ exit(0); sleep(2); /* Vater blockiert für 2 Sekunden */ printf("i im Vater: %d\n",i); /* Ausgabe: 1 */ BILD 2.31 zeigt, wie die Ausgabewerte, die in den Programmkommentaren angegeben sind, zustande kommen. int i = 1; if (fork()==0) { printf("i im Sohn: %d\n",i); i = 2; printf("i im Sohn: %d\n",i); exit(0); sleep(2); printf("i im Vater: %d\n",i); Der Vater deklariert und initialisiert eine Variable i. Vater: i 1 Aufgabe 2A.3.6. PROG 2.3 Getrennte Speicherbereiche bei Vater und Sohn BILD 2.31 Speicherorganisation bei fork() int i = 1; if (fork()==0) { printf("i im Sohn: %d\n",i); i = 2; printf("i im Sohn: %d\n",i); exit(0); sleep(2); printf("i im Vater: %d\n",i); int i = 1; if (fork()==0) { printf("i im Sohn: %d\n",i); i = 2; printf("i im Sohn: %d\n",i); exit(0); sleep(2); printf("i im Vater: %d\n",i); Der Vater erzeugt einen Sohn. Der Sohn erhält einen eigenen Speicher als Kopie des Speichers des Vaters. Vater: i 1 Sohn: i Der Sohn gibt seinen i-wert aus. Der Vater schläft zwei Sekunden. Vater: i 1 i 1 Sohn: 1 Ausgabe: 1 Fortsetzung nächste Seite

53 52 2 Basistechniken int i = 1; if (fork()==0) { printf("i im Sohn: %d\n",i); i = 2; printf("i im Sohn: %d\n",i); exit(0); sleep(2); printf("i im Vater: %d\n",i); int i = 1; if (fork()==0) { printf("i im Sohn: %d\n",i); i = 2; printf("i im Sohn: %d\n",i); exit(0); sleep(2); printf("i im Vater: %d\n",i); Der Sohn ändert seinen i-wert und gibt i erneut aus. Vater: Sohn: i 1 i 2 Ausgabe: 2 Der Vater gibt nach zwei Sekunden seinen (unveränderten) i-wert aus. Vater: i 1 Ausgabe: 1 Aufgabe 2A.3.3. PROG 2.4 Zugriff auf die PIDs des Vaters und des Sohns Im dritten Programm ist zu sehen, wie der Sohnprozess mit getpid() und getppid() an seine eigene PID und die seines Vaters gelangt und wie der Vaterprozess an seine eigene PID und die seines Sohns kommt: #include <stdio.h> #include <stdlib.h> main() { int sohn_pid; if ((sohn_pid=fork())==0) { printf("sohn: Eigene PID ist %d\n",getpid()); printf("sohn: Vater-PID ist %d\n",getppid()); exit(0); sleep(1); /* Vaterausgabe soll nach der Sohnausgabe kommen */ printf("vater: Eigene PID ist %d\n",getpid()); printf("vater: Sohn-PID ist %d\n",sohn_pid); Interessant ist hier insbesondere, dass der Vater unmittelbar beim Aufruf von fork() dessen Rückgabewert, also die PID seines neuen Sohns, in einer Variablen sohn_pid speichert. Er hat nämlich keine Möglichkeit, diese PID noch nachträglich zu ermitteln. Übrigens besitzt hier auch der Sohn eine sohn_pid-variable. Sie ist ihm allerdings zu nichts nütze, denn sie enthält seinen fork()-rückgabewert, also immer eine 0. sohn_pid müsste eigentlich mit dem Typ pid_t deklariert werden. In der Praxis ist man aber oft etwas lässig und arbeitet mit int, was eigentlich nicht ganz korrekt ist. Man beachte die Klammerung bei der Speicherung des fork()-rückgabewerts: (sohn_pid=fork())==0. Sie ist wichtig, da in C der Vergleichsoperator == stärker bindet als der Zuweisungsoperator =. Ließe man die Klammern weg, so würde der Variablen sohn_pid nicht die PID des neuen Sohnprozesses zugewiesen, sondern das Ergebnis der Vergleichsoperation!

54 2.3 Prozesse und Threads in UNIX/Linux 53 Das vierte Programm zeigt, wie ein Vaterprozess mit wait() auf die Terminierung seines Sohns wartet und dabei dessen exit()-rückkehrstatus übernimmt: #include <stdio.h> #include <stdlib.h> main() { int status; if (fork()==0) { printf("sohn: Ich schlafe jetzt\n"); sleep(2); printf("sohn: Ich bin jetzt fertig\n"); exit(0); printf("vater: Ich warte auf den Sohn\n"); wait(&status); printf("vater: Sohn ist jetzt fertig\n"); printf("vater: status = %d\n",status); Die Variable status wird per Referenzaufruf an wait() übergeben und ist damit ein Rückgabeparameter. Über sie erhält der Vater den Rückkehrstatus des Sohns, den dieser an exit() übergeben hat, und weitere Informationen. Mit vordefinierten Makros können diese Informationen extrahiert werden: WIFEXITED(status) liefert true, wenn sich der Sohn durch Aufruf von exit() selbst terminiert oder das Ende seines Hauptprogrammtexts erreicht hat. WIFSIGNALED(status) liefert true, wenn der Sohn durch ein Signal terminiert wurde ( PROG 2.7). WEXITSTATUS(status) liefert den Rückkehrstatus, also den exit()-parameterwert (falls der Sohn exit() aufgerufen hat). WTERMSIG(status) liefert die Nummer des Signals, durch das der Sohn terminiert wurde (falls ihn ein Signal beendet hat). BILD 2.32 illustriert das Zusammenspiel von wait() und exit(). Es zeigt insbesondere die Rolle des UNIX/Linux-Kerns, der den Sohn erzeugt, den Vater blockiert, den Vater dann wieder entblockiert und ihm die Rückkehrinformationen übergibt. Vaterprozess: fork()... wait(&status) Auswertung von status UNIX/Linux-Kern: erzeugt Sohnprozess blockiert Vater registriert Adresse seiner Variablen status Sohnprozess: Codeausführung mit exit(rückkehrstatus) Aufgaben 2A.3.2./4./7. PROG 2.5 Warten auf das Ende des Sohns BILD 2.32 Zusammenspiel von wait() und exit() Variable status rückkehrstatus und Zusatzinfo schreibt rückkehrstatus und Zusatzinfo nach status entblockiert Vater

55 54 2 Basistechniken PROG 2.6 Ausführung eines Programms in einer anderen Datei Aufgabe 2A.3.8. Im fünften Programm sieht man, wie ein Sohnprozess per execv() ein Programm in einer anderen Datei aufruft: /* Programm des Vaters in einer Datei */ #include <stdio.h> #include <string.h> #include <stdlib.h> main() { int err, status; char *parameter[3]; parameter[0] = malloc(10); parameter[1] = malloc(10); parameter[2] = malloc(10); strcpy(parameter[0],"programm"); /* Zuweisung der Parameter */ strcpy(parameter[1],"1. Parameter"); strcpy(parameter[2],"2. Parameter"); if (fork()==0) { /* Aufruf einer Maschinenprogramm-Datei (siehe unten) */ err = execv("sohn",parameter); printf("sohn: Rueckkehr aus execv, Rueckgabe = %d\n",err); exit(err); /* Rückkehr nur im Fehlerfall! */ wait(&status); printf("vater: Sohn beendet, status = %d\n",status); /* Hauptprogramm des Sohns in einer zweiten Datei (z.b. Datei sohn.c, deren entsprechendes Maschinenprogramm in der Datei sohn steht) */ #include <stdio.h> #include <stdlib.h> main(int argc, char *argv[]) { int i; printf("sohnprogramm:\n"); for (i=0;i<argc;i++) /* Ausgabe der erhaltenen Parameter */ printf(" Parameter %d: %s\n",i,argv[i]); exit(0); Die Datei programm, deren Name execv() als Parameter übergeben wird, muss ein ausführbares Maschinenprogramm enthalten in diesem Fall sollte das übersetzte zweite main()-programm sein. Der Sohn erreicht das exit() im if-fork()-block nur, wenn beim execv()-aufruf ein Fehler aufgetreten ist. Ansonsten terminiert er bereits am Ende des unteren main()-programms und gibt dort den Rückkehrstatus 0 zurück, den der Vater dann in seinem wait() entgegennimmt. Das sechste Programm demonstriert schließlich, wie ein Vaterprozess durch kill(...,sigkill) seinen Sohn terminiert:

56 2.3 Prozesse und Threads in UNIX/Linux 55 #include <signal.h> #include <stdio.h> main() { int sohn_pid, i=0; if ((sohn_pid=fork())==0) { /* Endlosschleife des Sohns */ while (1) printf("sohn: %d",i++); sleep(2); /* Der Vater beendet den Sohn */ kill(sohn_pid,sigkill); SIGKILL ist ein Interruptsignal, das den Empfängerprozess auf jeden Fall beendet (sofern der Absender die Berechtigung dazu hat). Daneben gibt es weitere Signale, auf die der Empfänger durch Ausführung eines Signal Handler reagieren kann, also nicht unbedingt terminiert wird. Details dazu werden in besprochen. PROG 2.7 Terminieren eines anderen Prozesses Grundlegende API-Funktionen für Threads Zur Arbeit mit Threads gibt es in UNIX/Linux das Konzept der Pthreads (= POSIX Threads). Zusätzlich stellt Linux die Funktionen clone() und vfork() bereit, mit denen Prozesse erzeugt werden können, die sich Ressourcen mit ihren Vätern teilen. Die vollständige Dokumentation hierzu findet man wieder unter [Linux]. Aufgabe 2A Pthreads: pthread_create(), pthread_exit() Die grundlegende Pthreads-Funktion ist pthread_create(). Durch sie wird ein neuer Pthread erzeugt: int pthread_create( pthread_t *th, pthread_attr_t *attr, void* (*start_routine)(void*), void *arg) 0 (bei fehlerfreier Ausführung) oder Fehlercode #include <pthread.h> ID des neu erzeugten Threads (Rückgabe) Attribute für den neu erzeugten Thread (NULL = Standard-Eigenschaften) Referenz auf die Funktion, die der neue Thread ausführen soll Parameter für diese Funktion pthread_t ist wie pid_t ein systemabhängiger Ganzzahltyp. Die ID eines Threads ist eine eindeutige Nummer; sie ist zu unterscheiden von der PID des Prozesses, dem der Thread zugeordnet ist ( PROG 2.8). Ein Pthread beendet sich selbst durch Aufruf von pthread_exit(): void pthread_exit(void *value_ptr) #include <pthread.h> Wert zur Rückgabe an den Thread, der in pthread_join() ( ) auf das Ende dieses Threads wartet

57 56 2 Basistechniken PROG 2.8 Pthreads mit Zugriff auf einen gemeinsamen Speicher Das folgende Programmbeispiel illustriert die Anwendung der beiden Funktionen: #include <pthread.h> #include <stdio.h> /* globale Variable, die für alle Threads zugreifbar ist */ int wertspeicher = 0; /* Funktion, die von einem Thread ausgeführt werden soll */ void *ausgabe(void *p) { printf("hier ist ein Thread\n"); printf("mein Funktionsparameter: %ld\n", *(long *)p); printf("meine PID: %d\n",getpid()); wertspeicher++; /* erhöht die gemeinsame globale Variable */ printf("wertspeicher: %d\n\n",wertspeicher); pthread_exit(null); /* beendet den Thread */ /* Hauptprogramm */ int main(int argc, char *argv[]) { pthread_t th1, th2; /* IDs der neu erzeugten Threads */ long param1, param2; /* Parameter zur Übergabe an die Threads */ int err; /* Fehlercode von pthread_create() */ printf("pid des Hauptprogramms: %d\n",getpid()); printf("wertspeicher: %d\n\n",wertspeicher); /* Erzeugung eines Threads */ printf("erzeuge den ersten Thread\n"); param1 = 1111; err = pthread_create(&th1, NULL, ausgabe, &param1); /* 1. Parameter: Rückgabewert = ID des neuen Threads 2. Parameter: Attribute (hier: Standardattribute) 3. Parameter: Funktion, die der Thread ausführen soll 4. Parameter: Parameter für die Funktion */ if (err!=0)... Fehlerbehandlung... printf("thread-id %lu\n\n",th1); /* Erzeugung eines zweiten Threads */ printf("erzeuge den zweiten Thread\n"); param2 = 2222; err = pthread_create(&th2, NULL, ausgabe, &param2); if (err!=0)... Fehlerbehandlung... printf("thread-id %lu\n\n",th2);... /* Thread des Hauptprogramms läuft weiter */ Die Ausgabe dieses Programms könnte wie folgt aussehen: PID des Hauptprogramms: Wertspeicher: 0 Erzeuge den ersten Thread Thread-Nr Erzeuge den zweiten Thread Thread-Nr

58 2.3 Prozesse und Threads in UNIX/Linux 57 Hier ist ein Thread Mein Funktionsparameter: 2222 Meine PID: Wert des Wertspeichers: 1 Hier ist ein Thread Mein Funktionsparameter: 1111 Meine PID: Wert des Wertspeichers: 2 An dieser Ausgabe lassen sich drei wichtige Beobachtungen machen: Die Thread-IDs, die das Hauptprogramm nach der Erzeugung der Threads ausgibt (also die Werte der Variablen th1 und th2), sind verschieden. getpid() liefert jedoch in Hauptprogramm und Thread-Funktion jeweils denselben Rückgabewert. Es werden hier also tatsächlich unterschiedliche Threads ausgeführt, die demselben Prozess angehören. Die Threads greifen auf dieselbe Variable wertspeicher zu, wie es ja von Threads desselben Prozesses auch erwartet wird. Sie haben also tatsächlich keine eigenen Speicherbereiche im Gegensatz zu Prozessen, die durch fork() erzeugt wurden (vergleiche PROG 2.3 mit BILD 2.31). Die Threads werden nicht unbedingt in der Reihenfolge ausgeführt, in der sie erzeugt wurden. Wie man an den ausgegebenen Parameterwerten erkennt, läuft der Thread, der als zweiter erzeugt wurde, als erster ab. Um eine bestimmte Reihenfolge zu erzwingen, muss man zusätzliche Synchronisationsmechanismen einsetzen ( 3.5.4). Bezüglich des Programms selbst sind die folgenden Dinge zu beachten: Für jeden Thread sollte eine eigene aktuelle Parametervariable benutzt werden (wie hier mit param1 und param2 geschehen). Da nämlich ein Thread per Referenz auf diese Variable des Aufrufers zugreift, muss sichergestellt werden, dass der Aufrufer ihren Wert während der Bearbeitung des Aufrufs nicht ändert. Beide Threads greifen auf die globale Variable wertspeicher zu. Um gegenseitige Störungen auszuschließen, müssen im gezeigten Programm Synchronisationsmechanismen ( 3.5.4) hinzugefügt werden. Es ist hier nicht ausgeschlossen, dass der Thread, der das Hauptprogramm ausführt, das Ende von main() erreicht, bevor die beiden Threads, die von ihm erzeugt wurden, ihre Arbeit getan haben. Damit würde der gesamte Prozess terminieren also vorzeitig auch die beiden Threads. Man kann dieses Problem dadurch lösen, dass man pthread_exit() als letzte Anweisung von main() ausführt. Hierdurch würde nur der Hauptprogramm- Thread, nicht aber die anderen Threads beendet. Man müsste dann aber die beiden Parameter param1 und param2 global deklarieren, damit sie weiterhin zur Verfügung stehen. Die sauberste Lösung aber ist, das Ende der beiden Threads durch pthread_join() abzuwarten ( ).

59 58 2 Basistechniken Programme, die Pthreads-Funktionen verwenden, müssen unter Linux mit der cc- Option -pthread (oder auch -lpthread) übersetzt werden, also cc -pthread programmname.c Pthreads: pthread_join(), pthread_cancel() Das Hauptprogramm in PROG 2.8 terminiert, ohne auf das Ende der dort erzeugten Threads zu warten. Um ihre Beendigung abzuwarten, könnte man die Funktion pthread_join() benutzen: int pthread_join(pthread_t thread, void **value_ptr) 0 im fehlerfreien Fall Fehlercode sonst #include <pthread.h> Rückkehrstatus des Threads (Rückgabeparameter) ID des Threads, auf den gewartet werden soll Das folgende Programmbeispiel zeigt, dass pthread_join() auf dieselbe Weise wie waitpid() ( ) arbeitet: PROG 2.9 Warten auf das Ende eines Pthreads #include <pthread.h> #include <stdio.h> /* Funktion, die der Thread ausführen soll */ void *schlafe(void *schlafzeit) { int sz = *(int*)schlafzeit; static int exitcode = 0; printf("thread schlaeft %d Sekunden\n",sz); sleep(sz); /* Thread blockiert sich eine Zeit lang */ printf("thread ist fertig\n"); pthread_exit(&exitcode); /* Thread beendet sich */ /* Hauptprogramm */ int main(int argc, char *argv[]) { int schlafzeit = 2; /* Schlafzeit des Threads */ pthread_t thread_id; /* ID des Threads */ void *status; /* Rückkehrstatus des Threads */ printf("main erzeugt Thread\n"); pthread_create(&thread_id, NULL, schlafe, &schlafzeit); printf("main wartet auf Thread in pthread_join()\n"); pthread_join(thread_id, &status); printf("main: Thread beendet mit Status %d\n",*(int*)status); Ein Pthread kann einen anderen durch pthread_cancel() terminieren: int pthread_cancel(pthread_t thread) 0 im fehlerfreien Fall Fehlercode sonst #include <pthread.h> ID des Threads, der terminiert werden soll

60 Free ebooks ==> Prozesse und Threads in UNIX/Linux 59 Das folgende Programmbeispiel zeigt die Anwendung dieser Funktion: #include <pthread.h> #include <stdio.h> /* Funktion, die der Thread ausführen soll */ void *endlosschleife() { while (1) printf("hier ist ein Thread\n"); /* Hauptprogramm */ int main(int argc, char *argv[]) { pthread_t th; /* Erzeugung eines Threads */ pthread_create(&th, NULL, endlosschleife, NULL); /* Beenden des Threads nach einer Sekunde */ sleep(1); pthread_cancel(th); printf("thread ist beendet\n"); Es ist entscheidend, dass pthread_cancel() benutzt wird, um den Thread zu beenden. Durch kill(..,sigkill), das in PROG 2.7 benutzt wurde, würde nämlich der gesamte Prozess beendet (einschließlich des Threads, der das Hauptprogramm ausführt) und somit die abschließende Ausgabe nicht mehr erscheinen. PROG 2.10 Terminieren eines anderen Pthreads vfork() und clone() Unter Linux können auch die Funktionen vfork() und clone() verwendet werden. Sie erzeugen allerdings keine echten Threads, die ja Bestandteile eines gemeinsamen, übergeordneten Prozesses sind. Vielmehr entstehen Linux-Prozesse mit jeweils eigener PID, die den gesamten Kontext oder Teile davon mit ihren Vätern gemeinsam haben. Die Funktion vfork() hat im Wesentlichen denselben Effekt wie fork(); allerdings haben Vater und Sohn denselben Adressraum, greifen also auf denselben Variablenbestand zu: pid_t vfork(void) #include <unistd.h> Rückgabe an den erzeugenden Vaterprozess: Die PID des neu erzeugten Sohns (wenn erfolgreich) 1 (im Fehlerfall) Rückgabe an den erzeugten Sohnprozess: 0 Mit der Funktion clone() kann man stärker als mit vfork() differenzieren. Über ihre Parameter lässt sich steuern, welche Kontextteile Vater und Sohn gemeinsam besitzen und welche voneinander getrennt geführt werden sollen:

61 60 2 Basistechniken int clone( int (*fn)(void *), void *child_stack, int flags, void *arg) PID des Sohnprozesses oder 1 im Fehlerfall #include <sched.h> Referenz auf die Funktion, die der neue Prozess ausführen soll Aufrufstack, den der neue Prozess benutzen soll Angaben, welche Teile des Kontexts Vater und Sohn gemeinsam benutzen sollen Parameter für die auszuführende Funktion Ein möglicher flags-parameterwert ist CLONE_VM. Hierdurch wird erreicht, dass Vater und Sohn auf einem gemeinsamen Variablenbestand arbeiten. Weitere solche Flag- Konstanten lassen sich durch bitweises Oder (also mit dem -Operator) hinzufügen. 2.4 Threads in Java Die Programmierung nebenläufiger Java-Anwendungen basiert auf Threads. Dieser Abschnitt gibt einen knappen Überblick über grundlegende Methoden und Anwendungsmöglichkeiten der Thread-Klasse. Spätere Kapitel behandeln die Synchronisation von Java-Threads sowie ihre Kommunikation und Kooperation ( 3.6, 4.3, 5.3). Eine vertiefende Darstellung findet man in der Java-Online-Spezifikation [JavaSpec], in Java-Tutorials [JavaTutConc] und in der Spezialliteratur [Oech11] Die Klasse Thread In Java kann man eine nebenläufige Aktivität durch ein Objekt der Klasse java.lang.thread repräsentieren und über seine Methoden steuern: class Thread { void run() // void start() // void join() // void interrupt() // , static void sleep(long millis) // static void yield() // static Thread currentthread() // Thread.State getstate() // boolean isalive() // boolean isinterrupted() // void setpriority(int newpriority) // void setname(string name) // String getname() // long getid() //

62 Free ebooks ==> Threads in Java 61 Es werden hier nur die wichtigsten Komponenten der Klassendefinition gezeigt und besprochen; eine vollständige Darstellung bietet die Java-Online-Spezifikation [Java- Spec]. Weitere Unterstützung bei der Programmierung von Nebenläufigkeit findet man im Paket java.util.concurrent ( 3.6) run() und start() Die beiden zentralen Methoden der Klasse Thread sind run() und start(): run() enthält die Anweisungen, die der Thread ausführen soll. Durch Aufruf von start() werden diese Anweisungen nebenläufig zu anderen Threads ausgeführt. Einen Java-Programmteil, der nebenläufig ausgeführt werden soll, programmiert man also im einfachsten Fall wie folgt: Man definiert eine Klasse, die von java.lang.thread abgeleitet ist. Man schreibt in ihre run()-methode die Anweisungen, die nebenläufig auszuführen sind. Man erzeugt ein Objekt der Klasse, indem man einen ihrer Konstruktoren aufruft. Damit entsteht ein neuer Thread in Wartestellung. Man ruft auf dem neuen Objekt die start()-methode auf. Die Java Virtual Machine führt das Thread-Programmstück damit nebenläufig aus. Es ist wichtig, dass hier start() und nicht run() aufgerufen wird! Durch Aufruf von run() würde das Thread-Programm zwar auch ausgeführt, aber nicht nebenläufig: Die Aktionen würden im aufrufenden Thread sequentiell zu seinen anderen Aktionen ablaufen (siehe auch das Beispiel unten). Sollen mehrere Threads ausgeführt werden, so erzeugt und startet man entsprechend viele Objekte; sollen die Threads unterschiedliche Dinge tun, so definiert man mehrere Thread-Klassen mit verschiedenen run()-methoden. Das folgende Beispielprogramm erzeugt zwei Threads, die nebenläufig Texte auf den Bildschirm ausgeben: // Klasse für die Threads dieses Beispiels: Ein Thread // dieser Klasse gibt dreimal hintereinander einen Text aus, // wobei er zwischen zwei Ausgaben jeweils eine Sekunde pausiert. class BeispielThread extends Thread { private String ausgabetext; // auszugebender Text // Konstruktor, der den Ausgabetext setzt BeispielThread(String ausgabetext) { this.ausgabetext = ausgabetext; // run() definiert den Code, den der Thread ausführen soll public void run() { for (int i=0; i<3; i++) { Aufgabe 2A.4.1. PROG 2.11 Nebenläufige Ausführung von Java- Threads

63 62 2 Basistechniken // Blockieren für eine Sekunde try { sleep(1000); catch (InterruptedException e) { // Ausgabe des Texts zusammen mit der ID des Threads System.out.println( "Thread "+Thread.currentThread().getId()+": "+ausgabetext); // Klasse für das Hauptprogramm public class GrundlagenVonThreads { public static void main(string[] args) { // Ausgabe der Thread-ID des Hauptprogramms System.out.println( "Hauptprogramm: Thread "+Thread.currentThread().getId()); // Erzeugung zweier Threads mit unterschiedlichen Ausgabetexten BeispielThread t1 = new BeispielThread("AAA"); BeispielThread t2 = new BeispielThread(" BBB"); // Start der beiden Threads, die dann nebenläufig // jeweils ihre run()-methode ausführen t1.start(); t2.start(); Neben den Methoden run() und start() benutzt dieses Programm die Methoden sleep(), mit der der aktuell laufende Thread für eine bestimmte Anzahl von Millisekunden blockiert wird, currentthread(), die eine Referenz auf den aktuell ausgeführten Thread liefert, und getid(), die die ID eines Threads zurückgibt ( ). Die Ausgabe dieses Programms könnte wie folgt aussehen: Hauptprogramm: Thread 1 Thread 8: AAA Thread 9: BBB Thread 8: AAA Thread 9: BBB Thread 9: BBB Thread 8: AAA Hieran lassen sich drei Dinge erkennen: Die Aktionen werden tatsächlich von drei verschiedenen Threads ausgeführt, nämlich dem Thread des Hauptprogramms, der beim Start des Programms entsteht, und den beiden innerhalb des Programms erzeugten Threads. Die run()-methoden der beiden Threads werden nebenläufig abgearbeitet, wobei der Prozessor zwischen ihnen hin- und hergeschaltet wird. Es ist nicht garantiert, dass der Thread, der zuerst gestartet wurde, auch als erster seine jeweiligen Aktionen ausführt. Abhängig von der jeweiligen Scheduling-Ent-

64 2.4 Threads in Java 63 scheidung ( ) kann es sein, dass ein Thread den anderen überholt. Das ist hier bei der letzten Ausgabe von BBB der Fall. Würde das Hauptprogramm die Thread-Methode run() aufrufen (also z.b. t1.run() statt t1.start()), so würden die Threads nicht nebenläufig ablaufen, sondern ihre Aktionen würden sequentiell vom Hauptprogramm-Thread ausgeführt. Die Ausgabe sähe dann so aus: Hauptprogramm: Thread 1 Thread 1: AAA Thread 1: AAA Thread 1: AAA Thread 1: BBB Thread 1: BBB Thread 1: BBB join() Auf die Terminierung eines Threads kann man mit der Methode join() warten. Sie blockiert den aufrufenden Thread so lange, bis der Thread, auf dem sie aufgerufen wurde, beendet wurde: // Klasse für den Thread, auf dessen Ende gewartet werden soll: // Der Thread blockiert sich für zwei Sekunden und terminiert dann. class BeispielThread extends Thread { public void run() { System.out.println("Thread: Ich laufe an"); try { sleep(2000); catch (InterruptedException e) { System.out.println("Thread: Ich terminiere"); // Hauptprogramm, dessen Thread auf den Beispiel-Thread wartet public class WarteAufThread { public static void main(string[] args) { // Erzeugung und Start des Threads BeispielThread t = new BeispielThread(); t.start(); // Warten auf das Ende des Threads try { t.join(); catch (InterruptedException e) { System.out.println("main(): Thread ist beendet"); Offensichtlich arbeitet join() auf dieselbe Weise wie die UNIX/Linux-Funktion waitpid() ( ). Aufgabe 2A.4.3. PROG 2.12 Warten auf die Beendigung eines Threads

65 64 2 Basistechniken Weitere Methoden Neben run(), start() und join() definiert die Klasse Thread eine Reihe weiterer Methoden. Dazu gehören die folgenden: getid() liefert die ID des Threads, auf dem die Methode aufgerufen wurde ( PROG 2.11); getname() liefert seinen Namen. Die ID ist eine eindeutige Nummer, die dem Thread durch die Java Virtual Machine (JVM) bei seiner Erzeugung zugeteilt wird. Der Name des Threads ist eine frei wählbare Zeichenkette, die der Thread im Konstruktor oder durch Aufruf von setname() erhält. Die statische Methode currentthread() liefert eine Referenz auf den Thread, der diesen Aufruf ausführt ( PROG 2.11). Mit der statischen Methode sleep() kann sich der aktuell laufende, also der aufrufende Thread für eine bestimmte Zeitdauer selbst blockieren ( PROG 2.11). Mit der statischen Methode yield() bietet er der JVM an, den Prozessor abzugeben, so dass andere Threads ausgeführt werden können. ( Aufgabe 2A.4.4.) Mit interrupt() wird das Interrupt-Flag eines Threads gesetzt. Der Thread kann dieses Flag in seinem Programmcode durch isinterrupted() abfragen und sich dann programmgesteuert beenden ( ). getstate() liefert den Zustand eines Threads (NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED). Mit isalive() kann geprüft werden, ob ein Thread lebt, also gestartet wurde und noch nicht terminiert ist. Aufgabe 2A.4.6. Mit setpriority() kann die Priorität eines Threads gesetzt und mit getpriority() abgefragt werden ( ). Thread-Methoden, die die Synchronisation (also die zeitliche Abstimmung) von Threads ermöglichen, werden in 3.6 besprochen Grundlegende Programmiertechniken Aufgaben 2A.4.2./5. PROG 2.13 Gemeinsame Variablen für Java- Threads Zugriff auf gemeinsame Variablen Da Threads Bestandteile eines laufenden Programms sind, können sie auf die Variablen dieses Programms zugreifen und somit einen gemeinsamen Datenbestand nutzen. Dabei gelten natürlich die üblichen Sichtbarkeitsregeln von Java. Am einfachsten macht man eine Variable für alle Threads sichtbar, indem man sie als statische Variable in einer der beteiligten Klassen deklariert: // Klasse für das Hauptprogramm und gemeinsam benutzbare Variablen public class ThreadDatenDemo { // Variable, die von mehreren Threads benutzt werden soll static int i = 1; // Hauptprogramm public static void main(string[] args) { // Erzeugung und Start zweier Threads Thread1 t1 = new Thread1();

66 2.4 Threads in Java 65 Thread2 t2 = new Thread2(); t1.start(); t2.start(); // Thread 1: weist nach einer Sekunde der Variablen den Wert 2 zu class Thread1 extends Thread { public void run() { try { sleep(1000); catch (InterruptedException e) { ThreadDatenDemo.i = 2; // Thread 2: gibt sofort und nach zwei Sekunden den Wert von i aus class Thread2 extends Thread { public void run() { System.out.println(ThreadDatenDemo.i); // Ausgabe: 1 try { sleep(2000); catch (InterruptedException e) { System.out.println(ThreadDatenDemo.i); // Ausgabe: 2 Thread2 gibt als i-wert zunächst eine 1 und nach zwei Sekunden eine 2 aus, da Thread1 in der Zwischenzeit i einen neuen Wert zugewiesen hat. Das Beispielprogramm demonstriert den gemeinsamen Zugriff anhand einer einfachen int-variablen. Auf dieselbe Weise lassen sich natürlich auch beliebig komplexe Objekte gemeinsam nutzen, indem man Referenzen auf sie in statischen Variablen speichert. Alternativ könnte man die Objektreferenzen den Thread-Konstruktoren als Parameter übergeben und somit in den Threads lokal speichern ( ). Generell ist darauf zu achten, dass sich Threads bei Zugriffen auf gemeinsame Variablen und Objekte nicht stören. Mit solchen Synchronisationsproblemen und ihrer Lösung befasst sich Kapitel Beenden von Threads Threads beenden sich selbst, indem sie aus ihrer run()-methode (bzw. beim Hauptprogramm-Thread aus main()) zurückkehren. Beendet sich der Thread, der sie erzeugt hat, so beeinflusst sie das nicht. So laufen beispielsweise die Threads in PROG 2.13 weiter, obwohl der Thread des Hauptprogramms sofort nach ihrer Erzeugung terminiert. Zudem kann ein Thread von einem anderen Thread terminiert werden. Hierzu war ursprünglich die Methode stop() vorgesehen, die ähnlich wie die UNIX/Linux-Funktionen kill() ( ) und pthread_cancel() ( ) arbeitet. Der Einsatz dieser Methode wird jedoch seit längerem nicht mehr empfohlen sie ist, im Jargon Aufgabe 2A.4.4.

67 66 2 Basistechniken PROG 2.14 Unterbrechung eines Java-Threads der Java-Dokumentation, deprecated. stop() beendet einen Thread nämlich sofort, also auch dann, wenn er gerade eine kritische Operation ausführt, die besser zu Ende laufen sollte, um ein betroffendes Objekt nicht in einem inkonsistenten Zustand zurückzulassen. Als Ersatz für stop() dient ein Ansatz, der die Kooperation des zu beendenden Threads erfordert. Sie basiert auf dem Interrupt-Flag, das jeder Java-Thread besitzt. Dieses Flag kann von einem anderen Thread durch Aufruf der Methode interrupt() auf true gesetzt werden. Der zu unterbrechende Thread sollte das Flag durch die Methode isinterrupted() regelmäßig abfragen und sich, falls diese true liefert, selbst terminieren: // Thread: Er gibt in einer Schleife einen Text wiederholt aus. // Sobald von außen die Methode interrupt() aufgerufen wird, // verlässt er die Schleife. class BeispielThread extends Thread { public void run() { while (!isinterrupted()) System.out.println("Hier ist der Thread"); // Hauptprogramm: Erzeugt einen BeispielThread, blockiert sich für // zwei Sekunden und terminiert den Thread dann wieder. public class ThreadInterrupt { public static void main(string[] args) { BeispielThread t = new BeispielThread(); t.start(); try { Thread.currentThread().sleep(2000); catch (InterruptedException e) { t.interrupt(); System.out.println("Thread ist beendet"); interrupt() wirkt auch bei Threads, die zur Zeit blockiert sind beispielsweise in einem sleep(). Sie werden dann sofort wieder aktiv, wobei eine InterruptedException ausgelöst wird. Dies erklärt insbesondere, warum sleep()-aufrufe durch einen entsprechenden try-catch-block geklammert sein müssen (siehe die Beispielprogramme). Eine InterruptedException setzt übrigens das Interrupt-Flag wieder zurück, so dass nachfolgende isinterrupted()-abfragen den Rückgabewert false liefern. Möchte man den Flag-Wert als true erhalten, so muss man ihn in der catch-klausel der Exception wieder explizit setzen. Nur so wird im folgenden Programmstück die Schleife tatsächlich verlassen:

68 2.5 Zusammenfassung und Ausblick 67 while (!isinterrupted()) {... try { sleep(...); catch (InterruptedException e) { this.interrupt(); 2.5 Zusammenfassung und Ausblick Kapitel 2 gab eine Einführung in grundlegende Techniken zur Programmierung nebenläufiger Anwendungen: Nebenläufigkeit wird in Hardware und Software realisiert. In Software handelt es sich oft um eine Pseudonebenläufigkeit, die auf dem raschen Umschalten der Hardware zwischen den einzelnen Aktivitäten beruht. Das Betriebssystem spielt bei der Realisierung von Nebenläufigkeit eine zentrale Rolle: Es bestimmt durch seine Betriebsart, bis zu welchem Grad Nebenläufigkeit möglich ist. Es verwaltet die Hardwarekomponenten und teilt sie den nebenläufigen Aktivitäten zu. Die Aktivitäten werden dabei durch Prozesse und Threads repräsentiert. Es bietet an seiner Programmierschnittstelle (API) Funktionen, mit denen der Programmierer Prozesse und Threads erzeugen und steuern kann. Das Betriebssystem UNIX/Linux ermöglicht durch seine Programmierschnittstelle die Arbeit mit Prozessen. Daneben gibt es das Pthreads-Konzept zur Realisierung von Threads. Die Programmiersprache Java unterstützt durch ihre Standardpakete und -klassen die nebenläufige Programmierung mit Threads. Kapitel 2 konzentrierte sich darauf, nebenläufige Aktivitäten zur Ausführung zu bringen. An verschiedenen Stellen wurde aber bereits deutlich, dass diese Aktivitäten nicht einfach nebeneinander herlaufen, sondern dass sie sich aufeinander abstimmen müssen. Entsprechende einfache Funktionen wurden bereits eingeführt nämlich wait() und join(), mit denen ein UNIX-Prozess bzw. ein Java-Thread auf die Terminierung eines anderen wartet. Dieser Aspekt der gegenseitigen zeitlichen Abstimmung, der so genannten Synchronisation, ist Thema des nächsten Kapitels. Die darauf folgenden Kapitel befassen sich dann mit der Datenübertragung zwischen Aktivitäten und der Zusammenarbeit zwischen ihnen.

69

70 Free ebooks ==> 2A Basistechniken: Aufgaben Die Lösungen und weitere Aufgaben findet man auf der Webseite zum Buch. 2A.1 Wissens- und Verständnisfragen 1. Kreuzen Sie die richtige(n) Aussage(n) an: a.) API ist die Abkürzung für O Automated Process Interrupt. O Advanced Programming Interface. O Application Programming Interface. b.) Wenn ein Prozessor zwischen Prozessen hin- und hergeschaltet wird, so ist das O Microprogramming. O Multiprogramming. O Verteilte Programmierung. c.) Ein Multiprozessorsystem ist ein System mit O einem Prozessor, der besonders schnell zwischen den Prozessen umgeschaltet wird. O mehreren Prozessoren, die über einen Bus miteinander verbunden sind. O mehreren Computern, die z.b. über das Internet miteinander verbunden sind. d.) Nebenläufigkeit gibt es in O jedem Stapelverarbeitungssystem. O Stapelverarbeitungssystemen mit Spooling. O Clustern. e.) Ein Prozess O ist ein Programm, das gerade ausgeführt wird. O hat in UNIX/Linux eine eindeutige Nummer. O ist dasselbe wie ein Thread. f.) Die UNIX/Linux-Funktion fork() O liefert an den Sohnprozess die PID des Vaters zurück und an den Vaterprozess eine 0. O liefert an den Vaterprozess die PID des Sohns zurück und an den Sohnprozess eine 0. O liefert an den Sohnprozess die PID des Vaters zurück und an den Vaterprozess die PID des Sohns.

71 70 2A Basistechniken: Aufgaben g.) Für das Verhältnis zwischen Prozessen und Threads gilt: O Ein Thread kann mehrere Prozesse enthalten. O Ein Prozess kann mehrere Threads enthalten. O Keins von beiden. h.) Ein Java-Thread wird ausgeführt nach Aufruf der Methode O start(). O init(). O Thread(). 2. Füllen Sie in den folgenden Aussagen die Lücken: a.) Ein Betriebssystem verwaltet den Computer, und es stellt bereit, über die Programmierer und Benutzer Dienste aufrufen können. b.) Bei der Betriebsart bearbeitet die CPU Aufträge streng hintereinander, Ein- und Ausgaben können aber nebenläufig dazu stattfinden. c.) Informationen über einen Prozess stehen in dessen (abgekürzt: ). d.) Speicherbereiche, Registerinhalte usw. gehören zum eines Prozesses. e.) Ein UNIX/Linux-Prozess wartet auf die Beendigung eines Sohns mit der Funktion. f.) Um einen UNIX/Linux-Prozess für eine bestimmte Anzahl von Sekunden warten zu lassen, benutzt man die C-Funktion. 3. Sind die folgenden Aussagen wahr oder falsch? Begründung! a.) Nebenläufigkeit setzt immer voraus, dass mehrere Prozessoren vorhanden sind. b.) Ein UNIX/Linux-Prozess kann mehrere Söhne haben. c.) Ein UNIX/Linux-Prozess kann mehrere Väter haben. d.) Wenn unter UNIX/Linux ein Vaterprozess eine Variable deklariert hat, so kann sein Sohnprozess ohne weiteres auf diese Variable seines Vaters zugreifen. e.) Die UNIX/Linux-Funktion waitpid() und die Java-Methode join() haben im Wesentlichen dieselbe Aufgabe und Wirkung. 4. Welcher Begriff passt jeweils nicht in die Reihe? Begründung! a.) Hardware-Schnittstelle, API, UI, CPU b.) Multiprozessorsystem, Multiprogrammingsystem, Mehrkernprozessor, Cluster c.) Einprogrammbetrieb, Mehrprogrammbetrieb, Scheduling, Spooling d.) synchronisiert, blockiert, rechnend, bereit e.) fork, exit, ps, getpid f.) kill, fork, getpid, start

72 2A.2 Sprachunabhängige Anwendungsaufgaben Auf der linken Seite sind Begriffe angegeben, rechts stehen Charakteristika. Welcher Begriff gehört zu welcher Charakteristik? Ziehen Sie genau fünf Pfeile von links nach rechts! Mehrkernprozessor Pseudo-Nebenläufigkeit Spooling Festlegung der Ausführungsreihenfolge Multiprogramming Kooperation mehrerer Computer Verteiltes System echte Nebenläufigkeit Scheduling Ein-/Ausgabe gleichzeitig mit Ausführung 6. Beantworten Sie die folgenden Fragen: a.) Welche Schnittstellen bietet ein Betriebssystem? Worin unterscheiden sie sich? b.) Was ist der Unterschied zwischen Pseudonebenläufigkeit und echter Nebenläufigkeit? Wo findet man diese beiden Formen der Nebenläufigkeit in einem Computer? c.) Aus welchen zwei (Haupt-)Komponenten besteht ein Prozess? d.) Was ist der Unterschied zwischen Prozessen und Threads? e.) Was gehört zum Kontext eines Prozesses? (mindestens drei Nennungen) f.) Wie sieht das Zustandsübergangsdiagramm eines Prozesses aus? Zeichnen Sie es aus dem Gedächtnis! g.) Welcher Zusammenhang besteht zwischen den Parametern der UNIX/Linux-Funktionen exit() und wait()? h.) Was ist der Unterschied zwischen den Java-Methoden run() und start()? 2A.2 Sprachunabhängige Anwendungsaufgaben 1. Gegeben ist ein System mit einem Prozessor, das drei Aufträge zu bearbeiten hat. Jeder der drei Aufträge besteht aus vier aufeinanderfolgenden Phasen: Zuerst werden Daten eingegeben, dann wird der erste Teil des Auftrags auf dem Prozessor bearbeitet, dann folgt eine zweite Eingabe und schließlich die Bearbeitung des zweiten Teils. Die Zeitdauern für die jeweiligen Eingaben und Bearbeitungen sind wie folgt festgelegt: Eingabe 1 Bearb. 1 Eingabe 2 Bearb. 2 Auftrag A Auftrag B Auftrag C Das Systemverhalten unter streng sequentiellem Einprogrammbetrieb soll mit dem Verhalten unter (pseudo-)nebenläufigem Mehrprogrammbetrieb verglichen werden. Dabei gelten die folgenden Annahmen: Die Eingaben für verschiedene Aufträge können gleichzeitig stattfinden (was allerdings nur für den Mehrprogrammbetrieb interessant ist). Es kann jedoch nur ein Auftrag gleichzeitig bearbeitet werden; andere Aufträge müssen gegebenenfalls warten, bis der Prozessor wieder frei ist.

73 Free ebooks ==> A Basistechniken: Aufgaben Die Aufträge sind in der Reihenfolge A > B > C priorisiert. Im Einprogrammbetrieb wird also zunächst A vollständig zu Ende bearbeitet, dann B und dann C (wobei die Eingabe des nächsten Auftrags erst beginnt, wenn der vorherige Auftrag ganz fertig ist). Im Mehrprogrammbetrieb kann ein Auftrag höherer Priorität die Ausführung eines Auftrags niedrigerer Priorität unterbrechen; der niederpriore Auftrag wird dann später fortgesetzt (wobei seine bisherige Bearbeitungszeit angerechnet wird). Das Umschalten des Prozessors zwischen Aufträgen verursacht keinen zusätzlichen Zeitaufwand. Zeichnen Sie, für Ein- und Mehrprogrammbetrieb gesondert, je eine Zeitlinie, die zeigt, wann der Prozessor welchen Auftrag bearbeitet und wann er unbeschäftigt ( idle ) ist. Ermitteln Sie für Ein- und Mehrprogrammbetrieb die folgenden Kennzahlen: Die Aufenthaltsdauern der einzelnen Aufträge also die Länge der Zeit zwischen dem Eintreffen des Auftrags im System (hier immer 0) und seiner Fertigstellung. Die mittlere ( durchschnittliche ) Aufenthaltsdauer der drei Aufträge. Die Auslastung des Prozessors also die Zeit, in der der Prozessor Aufträge bearbeitet hat, im prozentualen Verhältnis zur Gesamtzeit (Summe aller Bearbeitungs- und Idle-Zeiten). 2. Gegeben ist ein System mit einem Prozessor, das vier Aufträge zu bearbeiten hat. Die Aufträge treffen zu unterschiedlichen Zeitpunkten ein und sind dann sofort zur Bearbeitung bereit, müssen also keine weiteren Eingaben abwarten. Jeder Auftrag benötigt den Prozessor für eine bestimmte Gesamtzeitdauer: Auftrag Ankunftszeitpunkt Prozessorzeit A B C D Für dieses System sollen die folgenden Schedulingstrategien betrachtet werden: Prioritätengesteuert/nichtunterbrechend: Jeweils der ausführungsbereite Auftrag mit der höchsten Priorität erhält den Prozessor (Zur Beachtung: Aufträge, die noch nicht eingetroffen sind, sind auch nicht ausführungsbereit!). Ein Auftrag gibt den Prozessor erst wieder frei, wenn seine Bearbeitung beendet ist. Er wird also in einem Rutsch ausgeführt und kann nicht durch einen später eintreffenden höherprioren Auftrag unterbrochen werden. Auftrag C hat die höchste Priorität, dann folgt Auftrag B und schließlich die Aufträge A und D, die dieselbe Priorität haben. Bei gleicher Priorität wird der Auftrag mit der kürzeren (Rest-)bearbeitungszeit bevorzugt. Prioritätengesteuert/unterbrechend: Wie oben. Allerdings übernimmt ein neu eintreffender Auftrag sofort den Prozessor vom laufenden Auftrag, sofern dessen Priorität niedriger ist. Der unterbrochene Auftrag wird später fortgesetzt, wobei seine bisherige Prozessorzeit angerechnet wird (er muss also nicht von vorn beginnen). Zeitscheibenbasiert (Round Robin) mit Zeitscheibenlänge 100: Die Reihenfolge der Aufträge im Zyklus ist A, B, C, D. Wird ein Auftrag schon vor Ende der Zeitscheibe fertig, so wird der Prozessor sofort an den nächsten Auftrag weitergegeben.

74 2A.3 Programmierung unter UNIX/Linux 73 Zeitscheibenbasiert (Round Robin) mit Zeitscheibenlänge 50: Wie oben, nur mit kürzerer Zeitscheibe. Das Umschalten des Prozessors zu einem anderen Auftrag dauert eine Zeiteinheit, ebenso das Starten eines Auftrags auf dem bisher freien Prozessor. Zeichnen Sie, für jede der vier Strategien gesondert, eine Zeitlinie, die zeigt, wann der Prozessor welchen Auftrag bearbeitet, wann er unbeschäftigt ( idle ) ist und wann Umschaltungen stattfinden. Vergleichen Sie dann die Strategien anhand der Reihenfolge, in der die Aufträge beendet werden, und der Anzahl der Umschaltungen, die auftreten. 2A.3 Programmierung unter UNIX/Linux 1. Gegeben ist das folgende Programm für die UNIX/Linux-C-Schnittstelle: #include <stdio.h> main() { printf("prozess ist gestartet\n"); sleep(2); printf("prozess terminiert jetzt\n"); Zeigen Sie durch das Zustandsübergangsdiagramm, welche Zustände der ausführende Prozess durchläuft. 2. Das C-Programm #include <stdlib.h> main() { int status; if (fork()==0) { sleep(5); exit(0); wait(&status); wird in eine Maschinenprogrammdatei prog übersetzt, die dann durch./prog & ausgeführt wird. Während dieser Ausführung erzeugt der Benutzer eine Bildschirmausgabe der Form UID PID PPID... CMD joe bash joe... prog joe... prog Welches UNIX/Linux-Benutzerkommando hat er dazu eingegeben? Welche Aufgabe hat hier der Prozess 2700? Füllen Sie die Leerstellen mit geeigneten Werten. 3. Gegeben ist das folgende Programmstück: int p, status; if ((p=fork())==0) { printf("%d %d ",getpid(),getppid()); exit(0); wait(&status); printf("%d %d\n",getpid(),p);

75 74 2A Basistechniken: Aufgaben Was erscheint auf dem Bildschirm, wenn der Vaterprozess die Nummer 100 und der Sohnprozess die Nummer 101 hat? Welchen Wert hat die Variable p im Sohnprozess? 4. Betrachten Sie den folgenden Ausschnitt eines Unix/Linux-C-Programms: int s; if (fork()!=0) { printf("hier ist der Sohn. Ich schlafe 2 Sekunden.\n"); wait(2); printf("ich, der Sohn, bin fertig.\n"); printf("hier ist der Vater. Ich warte auf den Sohn\n"); sleep(&s); Der Programmausschnitt enthält vier Fehler. Korrigieren Sie sie im Programmtext. 5. Betrachten Sie das folgende Programmstück: int i=10; if (fork()==0) while(1) { sleep(1); i=i+10; sleep(3); printf("i = %d",i); Ist die Ausgabe des Programms eindeutig? Wenn ja: Wie lautet sie? Wenn nein: Warum nicht? 6. Gegeben ist das folgende Programmstück: int x, y; x = 5; y = 7; if (fork()==0) { x = 3; /* S1 */ exit(0); if (fork()==0) { y = 1; /* S2 */ exit(0); x = 2; /* V */ wait(...); wait(...); Der Vaterprozess soll sich mit seiner Ausführung an der Stelle V befinden, die beiden Söhne an den Stellen S1 bzw. S2 (also jeweils nach den Zuweisungen). Skizzieren Sie in der folgenden Zeichnung den aktuellen Speicherzustand der Prozesse: Zeichnen Sie für jede existierende Variable ein kleines Rechteck, das Sie mit dem Namen der Variablen versehen und in das Sie den aktuellen Wert der Variablen hineinschreiben: Ist eindeutig bestimmt, welcher der drei Prozesse als letzter fertig wird? Wenn ja, welcher? Begründung! Wie könnte der erste Sohnprozess die PID seines Vaters feststellen? Wie könnte der Vaterprozess die PID seines ersten Sohns feststellen?

76 2A.4 Programmierung in Java 75 Variablen des Vaters: Variablen des 1. Sohns: Variablen des 2. Sohns: Von allen Prozessen gemeinsam benutzbare Variablen: Wie viele Prozesskontrollblöcke (PCBs) existieren bei der Ausführung des Programms? 7. Schreiben Sie ein Programm, in dem Folgendes geschieht: Ein Vaterprozess erzeugt einen Sohnprozess, der zunächst zwei Sekunden schläft, dann eine Bildschirmausgabe macht und sich anschließend beendet. Der Vater wartet, bis dieser Sohn fertig ist, und erzeugt dann einen zweiten Sohn. Der zweite Sohn gibt die Zahlen von 1 bis 20 aus und beendet sich dann. Der Vater wartet nicht ab, bis der zweite Sohn fertig ist, sondern beendet sich sofort. 8. Schreiben Sie ein Programm, in dem Folgendes geschieht: Der Vaterprozess erzeugt einen Sohnprozess. Er wartet dann, bis dieser Sohnprozess terminiert (also sich beendet hat) und terminiert dann selbst. Der Sohnprozess erzeugt seinerseits einen Sohnprozess (also gewissermaßen einen Enkel des Vaters). Er wartet dann 15 Sekunden, beendet anschließend den Enkel und terminiert schließlich selbst. Der Enkel gibt in einer Endlosschleife die natürlichen Zahlen, beginnend mit 0 aus. Nach jeder Ausgabe einer Zahl wartet er eine Sekunde. 9. Lösen Sie die Aufgaben 7. und 8. sowie auch 2A.4.5. (siehe unten) mit Pthreads. In der Lösung zu 7. soll der Vater allerdings auch auf das Ende des zweiten Threads warten. Allgemeiner Hinweis: Bevor Sie sich ausloggen, sollten Sie mit ps prüfen, ob noch Prozesse für Sie aktiv sind und diese (natürlich außer der Shell) mit kill löschen. 2A.4 Programmierung in Java 1. Gegeben ist das folgende Programmstück: class MyThread extends Thread { public void run() { for (int i=0; i<3; i++) {... eine Sekunde warten... System.out.print("A"); public class MyProgram { public static void main(string[] args) { MyThread t = new MyThread(); t.run(); for (int i=0; i<3; i++) {... eine Sekunde warten... System.out.print("B");

77 76 2A Basistechniken: Aufgaben Führt man das Programm aus, so erscheint auf dem Bildschirm die Ausgabe AAABBB. Thread t läuft also nicht nebenläufig zum Thread des Hauptprogramms ab, obwohl der Programmierer das eigentlich beabsichtigt hatte. Wo liegt der Fehler? 2. Im folgenden Programmstück soll ein Thread auf eine Variable zugreifen, die in der Hauptprogramm-Klasse deklariert ist. Der Compiler meldet aber einen Fehler. Warum? class MyThread extends Thread { public void run() { MyProgram.var=123; public class MyProgram { int var; public static void main(string[] args) { (new MyThread()).run(); Schreiben Sie ein Programm, in dem unmittelbar hintereinander zwei nebenläufige Threads gestartet werden. Der erste Thread soll dreimal hintereinander (mit je einer halben Sekunde Abstand) den Text Thread 1 ausgeben und sich dann beenden. Der zweite Thread soll das Ende des ersten Threads abwarten und dann auf dieselbe Weise dreimal Thread 2 ausgeben. Der Thread, der das Hauptprogramm ausführt, soll das Ende des zweiten Threads abwarten und schließlich eine Abschlussmeldung ausgeben. Tipp: Übergeben Sie an den Konstruktur des zweiten Threads als Parameter eine Referenz auf den ersten Thread, damit der zweite Thread weiß, auf wen er warten muss. 4. Lösen Sie Aufgabe 2A.3.8. mit Java-Threads. 5. Schreiben Sie ein Programm, in dem unmittelbar hintereinander zehn Threads t0,..., t9 erzeugt und gestartet werden. Nur t0 führt seine Aktion sofort aus; jeder weitere Thread ti wartet auf das Ende seines Vorgängers ti-1. Das Hauptprogramm stellt eine gemeinsam benutzbare String-Variable bereit, in der die Threads einen Satz nach dem Stille-Post-Prinzip zusammenstellen sollen (t0 schreibt das erste Wort hinein, t1 hängt das zweite Wort an usw.). Der Thread, der das Hauptprogramm ausführt, wartet das Ende von t9 ab und gibt dann den entstandenen Satz aus. Tipps: Erzeugen Sie die Threads in einer Schleife. Übergeben Sie dem Konstruktor jeweils das Wort, das der Thread anfügen soll, sowie eine Referenz auf den Thread, auf den er warten soll. 6. Schreiben Sie ein Programm, das wie folgt vorgeht: In der Klasse des Hauptprogramms wird ein Objekt a der Klasse AtomicInteger erzeugt und mit 0 initialisiert (import java.util.concurrent.atomic.atomicinteger erforderlich). Objekte dieser Klasse speichern jeweils einen Ganzzahlwert und sind thread-safe, so dass sich nebenläufige Zugriffe mehrerer Threads nicht gegenseitig stören ( , [JavaSpec]). Im Hauptprogramm werden zwei Threads erzeugt und gestartet. Vor dem Start werden sie auf die kleinstmögliche Priorität gesetzt (Thread.MIN_PRORITY). Die Threads laufen jeweils in einer Schleife, bis sie unterbrochen werden. Der erste Thread erhöht in jedem Schleifendurchlauf den Wert von a um

78 2A.4 Programmierung in Java 77 1 (a.incrementandget(1)), der zweite Thread senkt ihn entsprechend um 1 (a.decrementand- Get(1)). Während die Threads laufen, gibt das Hauptprogramm fünfzigmal, jeweils mit 200 ms Pause dazwischen, den Wert von a aus (geliefert durch a.get()). Das Hauptprogramm beendet die Threads und setzt a auf 0 zurück (a.set(0)). Anschließend geht es nochmals wie oben vor (zwei Threads erzeugen usw.), gibt aber nun einem der beiden Threads die Priorität Thread.MIN_PRORITY+4. Nach weiteren 50 Ausgaben beendet das Hauptprogramm die Threads. Was beobachten Sie, wenn Sie das Programm ausführen?

79 3 Synchronisation Kooperation Kapitel 3 beschäftigt sich mit der gegenseitigen Abstimmung von Prozessen und Threads: Kommunikation Synchronisation Durch Synchronisation setzen sie Anforderungen an ihre zeitlichen Abläufe durch. Basistechniken 3.1 Synchronisationsbedingungen...79 Abschnitt 3.1 formuliert elementare und komplexere Forderungen an den zeitlichen Ablauf nebenläufiger Ausführungen Elementare Bedingungen (79), Komplexere Probleme (82) 3.2 Einfache Synchronisationsmechanismen...84 Abschnitt 3.2 stellt einfache Mechanismen dar, die jeweils eine elementare Synchronisationsbedingung durchsetzen Grundlegende Eigenschaften (85), Interruptsperren (85), Spinlocks (86), Signale und Events (90) 3.3 Synchronisation durch Semaphore...91 Abschnitt 3.3 befasst sich mit Semaphoren, einem für viele Bedingungen flexibel anwendbaren Synchronisationsmechanismus Arbeitsprinzip von Semaphoren (91), Einsatz bei Standardproblemen (96), Systematische Lösung von Problemen (102), Fehlerquellen (105) 3.4 Synchronisation durch Monitore Abschnitt 3.4 behandelt Monitore Datenspeicher, auf die nur jeweils ein Prozess oder Thread gleichzeitig zugreifen kann Grundprinzip von Monitoren (110), Bedingungsvariablen (112), Lösung weiterer Standardprobleme (115) 3.5 Mechanismen in UNIX/Linux Abschnitt 3.5 diskutiert Synchronisationsmechanismen in UNIX Signale (118), Lock-Dateien (120), Semaphore (121), Mutexe mit Bedingungsvariablen (132) 3.6 Mechanismen in Java Abschnitt 3.5 stellt Synchronisationsmechanismen von Java dar Atomare Operationen (135), Semaphore (136), Monitore (138), Weitere Mechanismen (143)

80 Free ebooks ==> Synchronisation Der moderne Mensch erwartet, dass alles sicher, effizient und bequem abläuft: Anschlusszüge sollen aufeinander warten, Autos sollen auf Kreuzungen nicht zusammenstoßen, Waren sollen erst nach ihrer Bezahlung ausgehändigt werden und einen Mittagsschlaf soll niemand unterbrechen. Da sich diese Erwartungen meist nicht von allein erfüllen, setzt er Mittel und Mechanismen ein, um sie durchzusetzen wie Eisenbahnsignale, Verkehrsampeln, Quittungen oder Türschlösser. Auch in Computern müssen sich Aktivitäten, also Prozesse und Threads, untereinander abstimmen. So darf in bestimmten Bereichen jeweils nur ein Prozess gleichzeitig arbeiten, um gegenseitige Störungen zu vermeiden. Prozesse, die Daten verarbeiten sollen, müssen warten, bis andere Prozesse diese Daten geliefert haben. Einige wenige Mittel, solche Anforderungen durchzusetzen, wurden bereits in Kapitel 2 besprochen nämlich die Funktionen wait(), pthread_join() und join(), mit denen ein Prozess bzw. Thread auf das Ende eines anderen warten kann. Probleme, die die zeitliche Abstimmung nebenläufiger Aktivitäten betreffen, werden in der Informatik unter dem Stichwort Synchronisation behandelt: Synchronisation ist die zeitliche Abstimmung nebenläufiger Aktivitäten. Durch Synchronisation sollen Synchronisationsbedingungen erfüllt werden Anforderungen an das zeitliche Verhalten der Aktivitäten. Zur Durchsetzung der Bedingungen werden Synchronisationsmechanismen eingesetzt, die den zeitlichen Ablauf der Aktivitäten beeinflussen. DEFINITION Synchronisation, Synchronisationsbedingungen und -mechanismen Dieses Kapitel diskutiert zuerst typische Synchronisationsbedingungen, die in Computern durchzusetzen sind, und geht dann auf entsprechende Synchronisationsmechanismen ein. Das Augenmerk richtet sich dabei besonders auf die flexibel einsetzbaren Semaphore und Monitore. Die Konzepte werden wieder allgemein eingeführt und dann am Beispiel von UNIX/Linux und Java in die Programmierpraxis umgesetzt. 3.1 Synchronisationsbedingungen Elementare Bedingungen Bei der Synchronisation nebenläufiger Aktivitäten werden Synchronisationsbedingungen durchgesetzt Forderungen, die das zeitliche Verhalten der Aktivitäten betreffen. Im Alltagsleben und in Computern gibt es, wie schon in der Einführung gesagt, vielfältige Beispiele für solche Bedingungen. Sie basieren oft auf zwei elementaren Synchronisationsbedingungen, nämlich dem wechselseitigen Ausschluss und der Reihenfolgebedingung.

81 80 3 Synchronisation Wechselseitiger Ausschluss Beim wechselseitigen Ausschluss wird gefordert, dass bestimmte Aktionen nur von höchstens einer Aktivitität zur selben Zeit ausgeführt werden. Dabei liegt eine Konkurrenzsituation um eine bestimmte Ressource vor, die nie von zwei Aktivitäten gleichzeitig benutzt werden darf: BILD 3.1 Wechselseitiger Ausschluss zwischen zwei Zügen Höchstens ein Zug gleichzeitig darf in der Engstelle sein In einem eingleisigen Streckenabschnitt darf sich immer nur ein Zug befinden, damit es nicht zu Zusammenstößen kommt. Aus demselben Grund dürfen Straßenkreuzungen jeweils nur in Längs- oder in Querrichtung befahren werden. Auf vielen Peripheriegeräten eines Computers, wie z.b. Druckern, darf nur ein Prozess gleichzeitig arbeiten, damit sich Ausgaben nicht überlagern. Auf einem Datenbankeintrag darf jeweils nur ein Prozess Schreiboperationen ausführen, damit es nicht zu Inkonsistenzen kommt. Alle diese Beispiele werden von der folgenden Definition abgedeckt: DEFINITION Wechselseitiger Ausschluss und kritischer Abschnitt Der wechselseitige Ausschluss (w.a., engl.: mutual exclusion) ist eine Synchronisationsbedingung. Sie fordert, dass auf einer Ressource (oder einer Gruppe von Ressourcen) höchstens eine Aktivität gleichzeitig arbeitet. Diese Arbeit auf der Ressource kann aus einer Folge von Einzelschritten bestehen, während deren Ausführung dann keine andere Aktivität auf die Ressource zugreifen darf. Eine solche Folge zusammengehöriger Einzelschritte wird als kritischer Abschnitt (engl.: critical section oder critical region) bezüglich der Ressource bezeichnet. Eine Folge zusammengehöriger Einzelschritte in diesem Sinne ist beispielsweise die Änderung mehrerer Einzelwerte eines Datenbankeintrags (Ort, Postleitzahl, Straße,...) oder die Ausgabe mehrerer einzelner Zeilen auf einen Drucker. Für pseudonebenläufige Systeme ( 2.1.3), in denen Prozessoren zwischen Aktivitäten hin- und hergeschaltet werden, bedeutet die Definition Folgendes: Befindet sich eine Aktivität in einem kritischen Abschnitt, so darf der Prozessor nicht zu einer anderen Aktivität umgeschaltet werden, die einen kritischen Abschnitt bezüglich derselben Ressource ausführen möchte. Das folgende Beispiel zeigt, warum diese Forderung entscheidend für ein korrektes Programmergebnis ist: Zwei Prozesse möchten von einem gemeinsamen Konto Geld abheben Prozess A 100 Euro, Prozess B 50 Euro. Eine Abhebung soll nur dann ausgeführt werden, wenn der Kontostand dabei nicht ins Minus gerät:

82 Free ebooks ==> Synchronisationsbedingungen 81 Prozess A: if (kontostand>=100) kontostand -= 100; Prozess B: if (kontostand>=50) kontostand -= 50; In jedem der beiden Prozesse sind der Test in der if-klammer und die nachfolgende Subtraktion ein kritischer Abschnitt bezüglich der Ressource kontostand. Sie müssen also ohne Unterbrechung, das heißt ohne Umschaltung zum anderen Prozess ausgeführt werden. Ansonsten wäre nämlich der folgende Ablauf möglich: Der Kontostand beträgt zu Beginn 100 Euro. Prozess A führt seinen if-test aus und verzweigt, da dieser ein true liefert, in die darauffolgende Zeile. Bevor er diese ausführen kann, wird der Prozessor zu Prozess B umgeschaltet. Da der Kontostand immer noch 100 Euro beträgt, führt Prozess B seine Abhebung aus. Anschließend wird zu Prozess A zurückgeschaltet, der unmittelbar vor seiner Subtraktionsoperation gewartet hat und diese nun ausführt. Insgesamt resultiert also ein negativer Kontostand von 50 Euro, was ja vermieden werden sollte. Man beachte, dass dieser Fehler nicht immer auftritt, sondern nur dann, wenn das Betriebssystem den Prozessor so unglücklich wie beschrieben umschaltet. Da aber die Umschaltung bei mehrfacher Ausführung des Programms nicht immer zur selben Zeit stattfindet, ist der Fehler also nur manchmal zu beobachten. Solche so genannten Race Conditions, bei denen das Endergebnis von der zeitlichen Abfolge der Prozessausführungen abhängt, sind daher nur schwer zu finden und zu beheben. Beim wechselseitigen Ausschluss ist zu betonen, dass er keine bestimmte Reihenfolge festlegt, in der die Aktionen der nebenläufigen Aktivitäten ausgeführt werden müssen. Damit sind die folgenden Abläufe beide zulässig: zulässiger Ablauf beim wechselseitigen Ausschluss: Aktivität 1: kritischer Abschnitt Aktivität 2: Warten kritischer Abschnitt BILD 3.2 Zulässige Abläufe beim wechselseitigen Ausschluss ebenfalls zulässig: Aktivität 1: Aktivität 2: Warten kritischer Abschnitt kritischer Abschnitt Reihenfolgebedingung Eine Reihenfolgebedingung fordert, dass Aktionen von Aktivitäten in einer bestimmten zeitlichen Abfolge ausgeführt werden. Dabei liegt oft eine Kooperationssituation vor, bei der eine Aktivität erst dann weiterlaufen kann, wenn eine andere Aktivität die Voraussetzungen dafür geschaffen hat: Im Bahnverkehr muss ein Zug auf einen anderen warten, damit Fahrgäste umsteigen können.

83 82 3 Synchronisation BILD 3.3 Reihenfolgebeziehung zwischen zwei Zügen Erst muss dieser Zug in Köln ankommen,... Frankfurt-Köln..., dann darf dieser Zug aus Köln abfahren Köln-Hamburg DEFINITION Reihenfolgebedingung Beim Staffellauf muss ein Sportler warten, bis sein Vorläufer angekommen ist. Ein Prozess, der mit bestimmten Daten arbeiten soll, muss auf einen anderen Prozess, der ihm diese Daten liefert, warten. Ein Computer darf erst heruntergefahren werden, wenn alle laufenden Prozesse sauber beendet sind. Hier trifft also die folgende allgemeine Definition zu: Die Reihenfolgebedingung ist eine Synchronisationsbedingung. Sie weist Aktivitäten die Rollen von Vorgängern und Nachfolgern zu und fordert, dass ein Nachfolger eine bestimmte Aktion erst dann ausführt, wenn ein Vorgänger eine bestimmte andere Aktion beendet hat. Ein einfaches Programmbeispiel zeigt zwei Prozesse, von denen der eine Geld auf ein (vorher leeres) Konto einzahlt und der andere dieses Geld wieder abhebt. Hier muss der Einzahler vor dem Abheber arbeiten: Prozess A: kontostand += 100; Prozess B: kontostand -= 100; Eine Reihenfolgebedingung legt also im Gegensatz zum wechselseitigen Ausschluss fest, welche Aktion als erste und welche als zweite ausgeführt werden soll: BILD 3.4 Zulässiger Ablauf bei der Reihenfolgebedingung Vorgänger: Nachfolger: Warten Komplexere Probleme Aufgabe 3A In der Praxis treten wechselseitiger Ausschluss und Reihenfolgebedingung nicht nur in ihrer reinen Form auf, sondern man findet sie auch als Teil komplexerer Synchronisationsprobleme. Einige dieser Probleme, die bei der praktischen Programmierung immer wieder zu lösen sind und/oder mit denen man die Ausdrucksfähigkeit und Nutzbarkeit von Synchronisationsmechanismen prüft, haben es zu eigenen Namen gebracht. Dazu gehören das Erzeuger-Verbraucher-Problem, das Leser-Schreiber-Problem und das Philosophenproblem, die im Folgenden besprochen werden. Zwei weitere Standardprobleme, nämlich das Sleeping Barber Problem und das Cigarette Smokers Problem, findet man im Aufgabenteil ( 3A bzw. 3A.3.11.).

84 3.1 Synchronisationsbedingungen Erzeuger-Verbraucher-Problem Beim Erzeuger-Verbraucher-Problem liefert ein Erzeugerprozess Produkte an einen Verbraucherprozess. Erzeuger und Verbraucher laufen jeweils in einer Schleife: Der Erzeuger erzeugt in jedem Schleifendurchlauf ein Produkt und liefert es dann aus; der Verbraucher holt in jedem Durchlauf ein Produkt ab und verbraucht es dann. Erzeuger und Verbraucher arbeiten nicht unbedingt gleich schnell, und ihre Geschwindigkeiten können schwanken. Um sie daher zeitlich voneinander zu entkoppeln, liegt zwischen ihnen ein Pufferspeicher, der eine bestimmte Anzahl von Produkten aufnehmen kann. In ihn stellt der Erzeuger seine Produkte ein, und aus ihm holt der Verbraucher die Produkte ab: Erzeuger Produkte Puffer Produkte Verbraucher Im Erzeuger-Verbraucher-Problem sind die folgenden Bedingungen zu erfüllen: Der Zugriff auf den Puffer muss wechselseitig ausgeschlossen erfolgen. Der Erzeuger muss mit dem Einfügen warten, solange der Puffer voll ist. Der Verbraucher muss mit dem Entnehmen warten, solange der Puffer leer ist. Wie man dieses Problem mit Synchronisationsmechanismen (so genannten Semaphoren und Monitoren ) lösen kann, wird in , und gezeigt. Implementationsbeispiele folgen in (UNIX: Semaphore), (UNIX: Pthreads mit Bedingungsvariablen), (Java: Monitore), (UNIX: Shared Memory), (UNIX: Message Queues) und (Java: Piped Streams). BILD 3.5 Erzeuger und Verbraucher mit Pufferspeicher Leser-Schreiber-Problem Beim Leser-Schreiber-Problem greifen mehrere Prozesse auf einen gemeinsamen Speicher zu. Manche von ihnen sind Leser, die die Speicherdaten nur lesen; die anderen sind Schreiber, die Daten in den Speicher schreiben, also seinen Inhalt ändern: Schreiber Schreiber Speicher Leser Leser Leser BILD 3.6 Leser und Schreiber mit gemeinsamem Speicher Als Synchronisationsbedingung wird gefordert, dass eine Schreiboperation zu allen anderen Operationen wechselseitig ausgeschlossen ist. Führt also ein Schreiber eine Operation auf dem Speicher aus, müssen alle anderen Schreiber und auch alle Leser mit ihren Speicherzugriffen warten. Leseoperationen sollen jedoch uneingeschränkt nebenläufig zueinander ausgeführt werden können. Lösungen dieses Problems mit Synchronisationsmechanismen werden in und diskutiert.

85 84 3 Synchronisation Philosophenproblem Am Philosophenproblem sind fünf Prozesse, die so genannten Philosophen, beteiligt. Jeder Philosoph führt einen endlosen Lebenszyklus aus, der nur aus Denken und Essen besteht: Er denkt zunächst eine Weile lang, geht dann zum gemeinsamen Esstisch, um dort eine Mahlzeit einzunehmen, denkt dann wieder und so weiter. Jedem Philosophen ist am Tisch ein fester Platz zugeordnet, an den er sich setzen muss. Zum Essen benötigt er die beiden Gabeln, die rechts und links von seinem Teller liegen und die er sich mit dem jeweiligen Nachbarn teilt. Er muss sich diese Gabeln vor dem Essen beschaffen und gibt sie erst nach dem Essen wieder frei: BILD 3.7 Tischgestaltung beim Philosophenproblem Ph1 G1 Ph0 G0 Ph4 Ph0 = Sitzplatz des Philosophen 0 usw. G0 = Gabel 0 usw. G2 G4 Ph2 G3 Ph3 Als grundlegende Synchronisationsbedingung wird hier der wechselseitige Ausschluss bei der Benutzung der Gabeln gefordert. Es können also nie zwei benachbarte Philosophen gleichzeitig essen, und ein Philosoph muss gegebenenfalls darauf warten, dass seine beiden Nachbarn ihre Gabeln wieder freigeben. In und werden Lösungen dieses Problems mit Synchronisationsmechanismen präsentiert. 3.2 Einfache Synchronisationsmechanismen Synchronisationsbedingungen erfüllen sich nicht von allein, sondern sie müssen explizit durchgesetzt werden, damit die Software stets sicher und korrekt arbeitet. Man kann sich also bei der Programmierung nicht darauf verlassen, dass die Dinge schon gutgehen werden, sondern man muss nebenläufige Aktivitäten so steuern, dass sie nachweislich in jedem Fall ein richtiges Ergebnis liefern und keinen unsicheren Zustand erreichen. Bei der praktischen Programmierung kann man auf verschiedene Mechanismen zur Synchronisation zurückgreifen. In diesem Abschnitt werden zunächst ihre grundlegenden Eigenschaften aufgezeigt. Anschließend werden einige einfache Mechanismen eingeführt, die jeweils auf einen bestimmten Typ von Synchronisationsproblemen ausgerichtet sind: Interrupt-Sperren und Spinlocks für wechselseitige Ausschlüsse, Signale und Events für Reihenfolgebedingungen. Die nachfolgenden Abschnitte befassen sich mit höheren Mechanismen, die flexibler einsetzbar sind, nämlich Semaphoren und Monitoren. Gesprochen wird dabei verkürzend jeweils nur von Prozesssynchronisation; Threads sind aber immer mit eingeschlossen.

86 Free ebooks ==> Einfache Synchronisationsmechanismen Grundlegende Eigenschaften Im Prinzip könnte das Betriebssystem Synchronisationsbedingungen durchsetzen, indem es durch sein Scheduling ( ) die Prozesse entsprechend steuert. Solch ein zentralisierter Ansatz stößt jedoch schnell an seine Grenzen, wenn viele Prozesse zu verwalten sind, die zudem dynamisch entstehen und wieder verschwinden. Hier ist es praktikabler, den Prozessen ihre Koordinierung selbst zu überlassen und ihnen dazu Synchronisationsmechanismen zur Verfügung zu stellen. Aus dem Alltag kennt man viele solcher dezentral arbeitenden Mechanismen, wie zum Beispiel die folgenden: Eine Fußgängerampel wird von einem Fußgänger auf Rot gestellt und sichert ihn so gegen den Autoverkehr ab. Ein Staffelstab wird von einem Läufer an seinen Nachfolger übergeben und lässt diesen damit loslaufen. Eine Bezahlquittung wandert von der Kasse zur Warenausgabe und gibt so die gekaufte Ware frei. Charakteristisch für Synchronisationsmechanismen ist, dass sie Prozesse an bestimmten Stellen ihrer Ausführung nur dann weiterlaufen lassen, wenn bestimmte Bedingungen erfüllt sind. Die Prozesse können dabei aktiv oder passiv warten: Beim aktiven Warten prüft ein Prozess selbst, ob ein bestimmtes Ereignis eingetreten ist und er somit weiterlaufen kann. Man nennt diesen Vorgang auch Polling. Beispielsweise wartet ein Schüler aktiv, wenn er ständig auf die Uhr schaut, ob die Unterrichtsstunde endlich vorbei ist. Beim passiven Warten wird ein Prozess von außen benachrichtigt, wenn ein bestimmtes Ereignis eingetreten ist und er weiterlaufen kann. Beispielsweise wartet ein Schüler passiv, wenn er sich am Ende der Stunde durch die Schulglocke wecken lässt. Wartet ein Prozess aktiv, so verbraucht er Prozessorzeit, da er in einer Warteschleife Prüfoperationen ausführt. Ein passiv wartender Prozess befindet sich dagegen im Blockiert-Zustand ( ), belastet den Prozessor also nicht. Mechanismen mit passivem Warten überführen somit Prozesse vom Zustand rechnend in den Zustand blockiert und später von dort in den Zustand bereit ( BILD 2.23). Beispielsweise blockiert die UNIX-Funktion waitpid() den aufrufenden Prozess, wenn der betreffende Sohnprozess noch nicht terminiert ist; das Ende des Sohns entblockiert ihn dann wieder ( ) Interruptsperren Der einfachste Mechanismus, den wechselseitigen Ausschluss durchzusetzen, ist die Interruptsperre. Sie verhindert, dass ein Prozess während eines kritischen Abschnitts unterbrochen wird. Eine Interruptsperre wirkt wie eine geschlossene Tür, die durch ein Schild oder ein Schloss gesichert ist:

87 86 3 Synchronisation BILD 3.8 Interruptsperre Prof. H. Tibatong BITTE NICHT STÖREN! Die Hardware eines Computers implementiert eine Interruptsperre, wie es der Name schon sagt: Durch Sperrung des Interrupteingangs des Prozessors wird verhindert, dass der Prozess, der gerade ausgeführt wird, zugunsten eines anderen Prozesses vom Prozessor verdrängt wird ( ). Der laufende Prozess kann also die Operationen seines kritischen Abschnitts in einem Zug ausführen, ohne dass eine Aktion eines anderen Prozesses dazwischenkommt. Kurz formuliert gilt also: DEFINITION Interruptsperre Die Interruptsperre ist ein Synchronisationsmechanismus, der den wechselseitigen Ausschluss durchsetzt. Sie blockiert den Interrupteingang des Prozessors und verhindert so, dass der laufende Prozess von außen unterbrochen wird. Interruptsperren sind zwar effektiv, haben aber erhebliche Nachteile: Die Dauer einer Interruptsperre muss kurz sein, da auf dringende Unterbrechungsereignisse, die ja auch während der Sperre auftreten können, rasch reagiert werden muss. Der Prozess, der die Interruptsperre gesetzt hat, darf im kritischen Abschnitt auf keinen Fall in eine Endlosschleife geraten, da außer ihm niemand die Sperre wieder löschen kann. Interruptsperren setzen den wechselseitigen Ausschluss nur in Einprozessor-Computern durch. Bei Mehrkern- oder Multiprozessorsystemen ( 2.1.2) können Prozesse, die auf anderen Kernen bzw. Prozessoren laufen, den Prozess in seinem kritischen Abschnitt stören. Interruptsperren können nur den wechselseitigen Ausschluss durchsetzen, für Reihenfolgebedingungen oder komplexere Synchronisationsprobleme sind sie ungeeignet. Interruptsperren sollten also nur im geprüften Code des Betriebssystems eingesetzt werden, um dort kurze kritische Abschnitte zu schützen. Der normale Anwendungsprogrammierer sollte sie dagegen nicht benutzen dürfen Spinlocks Ein zweiter Mechanismus für den wechselseitigen Ausschluss ist der Spinlock. Ein Spinlock wirkt wie eine Verkehrsampel oder ein Eisenbahnsignal, das den Zugang zu einem Streckenabschnitt kontrolliert, in dem sich nur ein Fahrzeug gleichzeitig befinden darf:

88 3.2 Einfache Synchronisationsmechanismen 87 Halt, solange Signal rot gemeinsames Signal: Halt, solange Signal rot Signal auf Rot stellen grün rot Signal auf Rot stellen kritischer Abschnitt Signal auf Grün stellen Signal auf Grün stellen BILD 3.9 Sicherung einer Engstelle durch ein Eisenbahnsignal Züge, die von links herangefahren kommen, müssen warten, solange das gemeinsame Signal auf Rot steht. Das Warten ist dabei aktiv ( 3.2.1); die Lokführer müssen also ständig selbst nachschauen, ob das Signal auf Grün wechselt. Ein grünes Signal darf passiert werden und wird dabei sofort auf Rot gestellt, wenn der Zug in den Streckenabschnitt einfährt. Beim Verlassen des Abschnitts wird das Signal wieder auf Grün gestellt, so dass ein möglicherweise wartender Zug nachfolgen kann. In einem Computer wird ein Spinlock mit Hilfe einer booleschen Lockvariablen (engl. lock = Schloss) realisiert. Jeder zu schützenden Ressource ist eine eigene solche Variable zugeordnet, die anzeigt, ob die Ressource frei oder belegt ist. Der Wert false bedeutet dabei, dass ein kritischer Abschnitt bezüglich der Ressource betreten werden darf; der Wert true besagt, dass sich zur Zeit ein anderer Prozess in einem kritischen Abschnitt bezüglich der Ressource befindet. Die folgenden Programmablaufpläne zeigen, wie sich zwei nebenläufige Prozesse über eine Lockvariable synchronisieren: boolean lock Prozess 1: gemeinsame Variable Prozess 2: BILD 3.10 Spinlock lock == true? ja lock == true? ja nein nein lock true kritischer Abschnitt lock true kritischer Abschnitt Das Symbol bezeichnet eine Wertzuweisung lock false lock false Aus den Ablaufplänen erklärt sich insbesondere der Name Spinlock (engl. to spin = kreisen): Ein Prozess kreist aktiv in einer Warteschleife, bevor er in den kritischen Abschnitt eintreten darf.

89 88 3 Synchronisation Ein Spinlock scheint so, wie er hier dargestellt wurde, den wechselseitigen Ausschluss effektiv durchzusetzen. Das ist aber nicht so! Es kann nämlich durchaus vorkommen, dass zwei Prozesse gleichzeitig in ihre kritischen Abschnitte eintreten: BILD 3.11 Versagen des Spinlocks 1. Zwei Züge kommen gleichzeitig an, Signal steht auf Grün: Halt, solange Signal rot Signal auf Rot stellen grün rot kritischer Abschnitt Halt, solange Signal rot Signal auf Rot stellen 2. Beide Züge fahren weiter, da beide Lokführer ein grünes Signal sehen: Halt, solange Signal rot Signal auf Rot stellen grün rot kritischer Abschnitt Halt, solange Signal rot Signal auf Rot stellen 3. Beide Züge stellen das Signal auf Rot und prüfen es nicht noch einmal: Halt, solange Signal rot Signal auf Rot stellen grün rot kritischer Abschnitt Halt, solange Signal rot Signal auf Rot stellen 4. Die Züge kollidieren im kritischen Abschnitt: Halt, solange Signal rot Signal auf Rot stellen grün rot kritischer Abschnitt Halt, solange Signal rot Signal grünauf rot Rot stellen Das Problem besteht offensichlich darin, dass zwischen der Prüfung des Signals und der Umstellung auf Rot durch den ersten Prozess eine Aktion des zweiten Prozesses

90 Free ebooks ==> Einfache Synchronisationsmechanismen 89 stattfindet: Dieser prüft dann seinerseits das Signal, das zu diesem Zeitpunkt noch grün ist, und schließt daraus, dass er nicht warten muss. Beide Prozesse schließen also ihre Prüfung erfolgreich ab, laufen weiter und kollidieren dann. Man löst dieses Problem dadurch, dass man die Schritte Signal prüfen und Signal auf Rot stellen ununterbrechbar ausführt also so, dass zwischen diesen beiden Schritten keine Aktion eines anderen Prozesses stattfinden kann. Man sagt, dass die beiden Schritte atomar (= unteilbar) ausgeführt werden: lock == true? nein lock true ja atomar = unteilbar Aufgabe 3A.2.1. BILD 3.12 Korrekter Spinlock mit atomarer Operation... Zusammenfassend gilt die folgende Definition: Ein Spinlock ist ein Synchronisationsmechanismus, der den wechselseitigen Ausschluss durchsetzt. Er basiert auf einer booleschen Lockvariablen, die anzeigt, ob ein kritischer Abschnitt bezüglich einer bestimmten Ressource betreten werden darf. Ein Prozess prüft die Lockvariable vor dem kritischen Abschnitt aktiv in einer Warteschleife. Er setzt sie unmittelbar vor Eintritt in den kritischen Abschnitt und setzt sie nach dessen Ende wieder zurück. Die Schritte des Prüfens und Setzens müssen dabei atomar, also ohne Unterbrechung, durchgeführt werden. DEFINITION Spinlock Ein Spinlock kann auf verschiedenen Ebenen implementiert werden: Maschinenprogramme können spezielle atomare Maschinenbefehle nutzen. Beispielsweise setzt der Befehl test_and_set(z) den Inhalt einer Speicherzelle z auf 1 (entsprechend true) und liefert den Wert zurück, den sie zuvor (also vor dem Eins-Setzen) hatte. Damit ist die folgende Programmstruktur zum Schutz eines kritischen Abschnitts möglich (vergleiche BILD 3.10 und BILD 3.12): do { belegt = test_and_set(lock); while (belegt==1);... kritischer Abschnitt... lock = 0; Diese Technik funktioniert, im Gegensatz zur Interruptsperre, auch in Multiprozessorsystemen, da die Atomarität des Maschinenbefehls durch eine Bussperre durchgesetzt wird. Somit kann während der Befehlsausführung keiner der Prozessoren über den Bus auf den gemeinsamen Speicher zugreifen ( BILD 2.7) und kommt daher nicht an die Lockvariable heran. Ein Fehler wie in BILD 3.11 ist damit ausgeschlossen.

91 90 3 Synchronisation Java definiert in seinem Paket java.util.concurrent.atomic Klassen mit atomaren Methoden, die analog zu test_and_set() arbeiten ( 3.6.1). Die Programmierschnittstellen von Betriebssystemen bieten Funktionen an, die atomar prüfen, ob eine Datei existiert, und sie, falls das nicht der Fall ist, erzeugen. Damit kann man eine Datei mit einem festgelegten Namen benutzen um anzuzeigen, ob eine Ressource belegt (= Datei existiert) oder frei (= Datei existiert nicht) ist. Die Datei, die man Lock-Datei (engl.: lock file) nennt, simuliert also eine boolesche Variable und wird so wie die Lockvariable in BILD 3.10 eingesetzt. In wird der Einsatz von Lock-Dateien in UNIX/Linux gezeigt. Spinlocks haben gegenüber der Interruptsperrung den Vorteil, dass sie in Mehrprozessorsystemen eingesetzt werden können und dass sie im Prinzip auch längere kritische Abschnitte schützen können. Aber auch Spinlocks haben erhebliche Nachteile: Das aktive Warten belastet den Prozessor stark. Spinlocks sind unfair: Wird die geschützte Ressource wieder freigegeben, so wird sie nicht unbedingt dem Prozess zugeteilt, der am längsten wartet, sondern demjenigen, der dann (durch die Scheduling-Entscheidung des Betriebssystems, ) zufällig den Prozessor enthält und damit seine Warteschleife verlassen kann. Es kommt also gewissermaßen zu einem Wettrennen (engl.: race) zwischen den wartenden Prozessen, dessen Ausgang nicht vorhersehbar ist Signale und Events Zur Durchsetzung von Reihenfolgebedingungen zwischen zwei Prozessen können Signale eingesetzt werden: DEFINITION Signal Ein Signal ist eine Meldung, die ein Prozess an einen bestimmten anderen Prozess schickt. Ein Prozess kann auf ein bestimmtes Signal warten, d.h. sich blockieren, bis das Signal eintrifft. BILD 3.13 Reihenfolge durch Signal Vorgänger: Nachfolger: Warten auf Signal Signal DEFINITION Event Der Begriff Signal hat hier also eine andere Bedeutung als bei der Eisenbahn, deren Signale Verkehrsampeln gleichzusetzen sind ( BILD 3.9). Eine spezielle Art von Signalen wurde bereits in PROG 2.5 gezeigt, wo ein Vaterprozess auf das Ende- Signal seines Sohns wartet. Der Signalbegriff kann zum Begriff des Events verallgemeinert werden: Ein Event ist ein Ereignis, auf dessen Eintreten mehrere Prozesse warten können. Tritt das Ereignis ein, so werden alle Prozesse (oder wahlweise nur einer von ihnen) entblockiert.

92 Free ebooks ==> Synchronisation durch Semaphore 91 Im Unterschied zu Signalen richtet sich ein Event an keinen bestimmten Prozess; bei seiner Auslösung muss also kein Empfänger benannt werden. Signale und Events setzen (im Gegensatz zu Spinlocks sowie Semaphoren und Monitoren, die im Folgenden präsentiert werden) nicht voraus, dass die beteiligten Prozesse über einen gemeinsamen Speicher verfügen. Man kann sie damit nicht nur in lokalen, sondern auch in verteilten Systemen ( ) einsetzen. 3.3 Synchronisation durch Semaphore Die bisher besprochenen Synchronisationsmechanismen können jeweils nur einen bestimmten Typ von Bedingungen durchsetzen, und sie haben zum Teil erhebliche Nachteile. Es soll nun ein Mechanismus vorgestellt werden, der diese Nachteile nicht aufweist und der bei den meisten praktischen Synchronisationsproblemen eingesetzt werden kann der Semaphor. Semaphore wurden vom Niederländer E. W. Dijkstra im Jahr 1965 eingeführt und seither in verschiedenen Varianten definiert und implementiert. Im Folgenden wird gezeigt, wie Semaphore arbeiten, wie man mit ihnen Synchronisationsprobleme löst und welche Fehler man dabei vermeiden muss Arbeitsprinzip von Semaphoren Semaphore arbeiten nach einem Prinzip, das zum Beispiel früher in der DDR genutzt wurde, um den Zugang zu Geschäften zu regeln: Vor jedem Geschäft stand ein Stapel von Plastikkörben. Jeder Kunde, der das Geschäft betreten wollte, musste einen Korb nehmen, ihn mit sich führen und nach Verlassen des Geschäfts wieder zurückgeben. War der Stapel leer, so mussten Kunden darauf warten, dass ein Korb zurückkehrte Datenstrukturen und Operationen Aus dem Beispiel ergibt sich die folgende allgemeine Definition und das zugehörige Strukturbild für Semaphore: Ein Semaphor (engl.: semaphore) ist ein Objekt, das einen Zähler und einen Warteraum umfasst. Der Zähler wird mit einer nichtnegativen ganzen Zahl initialisiert (also bei der Erzeugung des Semaphors vorbesetzt) und nimmt anschließend stets nichtnegative ganzzahlige Werte an. Der Warteraum ist zu Beginn leer und dient dann zur Aufnahme passiv wartender Prozesse. Auf einem Semaphor können zwei Arten von Operationen ausgeführt werden: Eine P-Operation dekrementiert den Zähler, senkt ihn also um 1. Hat der Zähler allerdings bereits den Wert 0, so wird der ausführende Prozess im Warteraum blockiert und die Dekrementierung zunächst nicht durchgeführt. Eine V-Operation inkrementiert den Zähler, erhöht ihn also um 1. Warten Prozesse im Warteraum, so wird einer davon so entblockiert, dass er seine P-Operation beendet und dabei den Zähler wieder dekrementiert. DEFINITION Semaphor

93 92 3 Synchronisation BILD 3.14 Strukturbild eines Semaphors P-Operation: Zähler>0? ja Zähler-- Zähler immer 0 nein Warteraum V-Operation: Zähler++ ja warten Prozesse? nein entblockiere einen Prozess Der Zähler eines Semaphors gibt also an, wie viele Prozesse eine P-Operation auf dem Semaphor ausführen können, ohne blockiert zu werden. Die Ausführung einer V-Operation auf dem Semaphor erhöht diesen Wert. Der Zusammenhang zum Kunden-Körbe-Beispiel ist offensichtlich: Der Zähler nennt die Anzahl der Körbe, die sich zur Zeit auf dem Stapel befinden; sie kann nie negativ werden. Ein Kunde, der das Geschäft betreten will, führt eine P-Operation aus, senkt also die Anzahl der Körbe, sofern noch ein Korb vorhanden ist, oder wartet sonst in der Schlange. Ein Kunde, der das Geschäft verlässt, führt eine V-Operation durch, erhöht also die Anzahl der Körbe und aktiviert damit gegebenenfalls einen anderen Kunden, der dann die Anzahl der Körbe wieder senkt und damit seine P-Operation beendet. Ein kleiner Unterschied zwischen Definition und Beispiel liegt darin, dass die Definition nicht festlegt, welchen der wartenden Prozesse die V-Operation entblockiert. Dies kann der Prozess sein, der am längsten wartet, muss es aber nicht. Ein Warteschlange von Kunden arbeitet dagegen stets nach dem First-Come-First-Served-Prinzip (oder sollte es zumindest), ist also immer fair. In wird diese Fairnessbetrachtung noch etwas weiter vertieft werden. Entscheidend ist, dass bestimmte Abläufe auf Semaphoren atomar stattfinden, also ohne dass ein anderer Prozess dazwischenfunkt : In einer P-Operation müssen der (erfolgreiche) Test des Zählers und seine anschließende Dekrementierung atomar durchgeführt werden. Nur so ist garantiert, dass der Zählerwert immer noch größer 0 ist, wenn er gesenkt wird (vergleiche das Kontobeispiel in und auch die Spinlock-Problematik in 3.2.3). Diese Atomarität kann durch Interruptsperren ( 3.2.2), Bussperren oder Spinlocks ( 3.2.3) erreicht werden. Aus demselben Grund darf der gesamte folgende Ablauf nicht unterbrochen werden: Ein Prozess inkrementiert in einer V-Operation den Zähler und entblockiert einen anderen Prozess, der den Zähler dann wieder dekrementiert. Dies soll verhindern, dass ein dritter Prozess zwischenzeitlich eine P-Operation ausführt, was schließlich zu einem negativen Zählerwert führen würde.

94 3.3 Synchronisation durch Semaphore 93 Die Atomarität dieser Schrittfolge, an der zwei Prozesse beteiligt sind, ist in der Praxis nur schwer durchzusetzen. Es gibt daher Varianten der P-Operation, die nach der Entblockade den Zählertest erneut durchführen ( BILD 3.18) und damit nur eine Atomarität wie im ersten Punkt verlangen. Übrigens gehen die Bezeichnungen P-Operation und V-Operation auf zwei niederländische Verben zurück. Welche das sind, variiert je nach Literaturstelle beispielsweise liest man passeren und vrijgeven. Eingängiger sind englischsprachige Bezeichnungen, die ebenfalls benutzt werden, wie zum Beispiel down und up Semaphoroperationen in Bild und Notation Bei der Lösung von Synchronisationsproblemen ist es manchmal nützlich, sich einen Semaphor als eine Schale vorzustellen, die eine Anzahl von Marken (engl.: token) enthält. Diese Anzahl entspricht dem aktuellen Wert des Semaphorzählers. P-Operationen nehmen Marken aus der Schale heraus (und blockieren zunächst, wenn die Schale leer ist), V-Operationen legen Marken in die Schale hinein: Semaphor mit Zählerwert 2: P-Operation: V-Operation: BILD 3.15 Bildliche Vorstellung eines Semaphors Entsprechend illustrieren die folgenden beiden Abbildungen mögliche Abläufe bei der Benutzung eines Semaphors: 1.a) P-Operation, wenn Zähler>0: ja Zähler>0? Zähler-- BILD 3.16 Mögliche Abläufe der P-Operation Zähler nein Warteraum 3 2 Der Zähler ist größer als 0. Der Prozess, der die P-Operation ausführt, kann ihn daher dekrementieren und so seine Operation beenden. 1.b) P-Operation, wenn Zähler=0: Zähler>0? ja Zähler-- Zähler nein Warteraum 0 X leer leer Der Zähler ist gleich 0. Der Prozess, der die P-Operation ausführt, wird daher im Warteraum blockiert. Dort wartet er passiv, bis er durch die V-Operation eines anderen Prozesses entblockiert wird.

95 Free ebooks ==> Synchronisation BILD 3.17 Mögliche Abläufe der V-Operation 2.a) V-Operation, wenn Warteraum leer: Zähler 1 2 Warteraum Zähler++ warten Prozesse? ja entblockiere einen Prozess nein Der Prozess, der die V-Operation ausführt, inkrementiert den Zähler und beendet damit die Operation. 2.b) V-Operation, wenn Warteraum nicht leer: P-Operation: Zähler>0? ja Zähler-- Zähler 0 10 nein Warteraum X V-Operation: Zähler++ ja warten Prozesse? nein entblockiere Prozess beginnt V-Operation beendet V-Operation leer beendet P-Operation wartet in P-Operation wurde geweckt Der Prozess, der die V-Operation ausführt, inkrementiert den Zähler. Da ein anderer Prozess wartet, wird dieser entblockiert. Er dekrementiert seinerseits den Zähler wieder und beendet damit seine P-Operation. Die Notation (also die Schreibweise) der Semaphoroperationen hängt von der verwendeten Programmiersprache ab. In den Abschnitten und werden die Operationen in C unter UNIX/Linux bzw. in Java besprochen. Zuvor wird eine Pseudo-Programmiersprache benutzt, in der S.INIT(n) für die Erzeugung eines Semaphors S, der mit dem Anfangswert n initialisiert wird,

96 3.3 Synchronisation durch Semaphore 95 S.P() für eine P-Operation auf dem Semaphor S und S.V() für eine V-Operation auf dem Semaphor S stehen Varianten und Erweiterungen Gegenüber der Definition in sind die folgenden Varianten von Semaphoren und ihren Operationen denkbar: Bei einem binären Semaphor kann der Zähler nur die Werte 0 und 1 annehmen. Zusätzlich kann verlangt werden, dass nur der Prozess eine V-Operation auf dem Semaphor ausführen darf, der auf ihm zuvor eine P-Operation durchgeführt hat. Da nach diesem Schema der wechselseitige Ausschluss durchgesetzt wird ( mutual exclusion, ), nennt man den Semaphor dann Mutex. P- und V-Operation entsprechen hier dem Schließen und Öffnen eines Schlosses und werden daher auch mit lock() und unlock() bezeichnet. (Manche Publikationen unterscheiden nicht zwischen Mutex und binärem Semaphor, sondern verwenden beide Begriffe synonym.) Die P-Operation kann so vorgehen, dass als Erstes der Zähler dekrementiert wird und sich der Prozess dann blockiert, wenn der Zählerwert negativ ist. Der Zähler kann hier also durchaus Werte annehmen, die kleiner als 0 sind; sein Absolutwert gibt dann die Anzahl der Prozesse an, die sich im Warteraum befinden. Übrigens ist dies die ursprüngliche Dijkstra-Definition von Semaphoren. Die P-Operation kann als abweisende Operation gestaltet werden, die den ausführenden Prozess nicht blockiert, sondern mit einem entsprechenden Rückgabewert zurückkehrt. Hier entfällt also der Warteraum; dafür muss ein Prozess gegebenenfalls in einer Schleife aktiv warten ( 3.2.1). Der Warteraum eines Semaphors kann nach dem First-Come-First-Served-Prinzip (FCFS) organisiert sein, so dass stets der Prozess entblockiert wird, der am längsten wartet. Dies sorgt für eine faire Behandlung der blockierten Prozesse. Die V-Operation kann alle wartenden Prozesse entblockieren, die sich dann ein Wettrennen liefern. Einer der Prozesse senkt den Zähler; die anderen müssen sich erneut blockieren. Im Gegensatz zur FCFS-Variante ist dieser Ansatz nicht fair. Zudem setzt er voraus, dass die entblockierten Prozesse den Zähler erneut prüfen (siehe hierzu auch BILD 3.18). Darüber hinaus sind zwei Verallgemeinerungen möglich, die insbesondere in UNIX/ Linux so implementiert sind ( 3.5.3): P- und V-Operationen können so generalisiert werden, dass sie den Zähler auch um höhere Werte als 1 senken bzw. anheben dürfen. Bildlich gesprochen, entnimmt eine P-Operation also eine bestimmte Anzahl von Marken aus der Schale. Dies geschieht nicht Stück für Stück, sondern atomar nach dem Alles-odernichts-Prinzip. Hier werden alle geforderten Marken auf einen Schlag entnommen oder, wenn nicht genügend Marken vorhanden sind, überhaupt keine, und der Prozess blockiert. Diese Vorgehensweise bietet sich an, wenn ein Prozess meh-

97 96 3 Synchronisation rere Exemplare einer Ressource benötigt und es nichts nutzt (oder sogar nicht korrekt ist), ihm nur einen Teil davon zu geben (siehe hierzu auch eine der Lösungen des Leser-Schreiber-Problems in ). Eine V-Operation fügt eine bestimmte Anzahl von Marken in die Schale ein. Sie entblockiert sinnvollerweise alle wartenden Prozesse, die sich dann gewissermaßen um die Marken streiten. Wie BILD 3.18 zeigt, prüfen sie dazu jeweils, ob der Zählerstand hoch genug für ihre jeweilige Forderung ist und senken ihn entsprechend (beides zusammen als atomare Operation) oder blockieren sich wieder. BILD 3.18 Semaphor mit verallgemeinerten P- und V-Operationen P-Operation: P(m) Zähler m? Zähler ja nein Warteraum Zähler -= m immer 0 V-Operation: V(n) Zähler += n warten Prozesse? nein ja entblockiere alle Prozesse Es können Gruppen von Semaphoren gebildet werden, bei denen atomar P-Operationen auf mehreren Semaphoren einer Gruppe ausgeführt werden. Auch eine solche Mehrfach-P-Operation arbeitet nach dem Alles-oder-nichts-Prinzip: Der ausführende Prozess blockiert nur dann nicht, wenn alle beteiligten einzelnen P- Operationen nicht blockieren wenn also, bildlich gesprochen, in allen betroffenen Schalen jeweils ausreichend viele Marken vorhanden sind. Ansonsten bleiben zunächst sämtliche Semaphorzähler unverändert, und der Prozess blockiert. Diese Vorgehensweise ist nützlich, wenn man mehrere Ressourcen gleichzeitig beschaffen muss wie beim Philosophenproblem, bei dem ein Philosoph zwei Gabeln zum Essen benötigt ( , ) Einsatz bei Standardproblemen Mit Semaphoren lassen sich die meisten Synchronisationsprobleme lösen. Dieser Abschnitt zeigt einige Standardbeispiele dafür. ( Aufgaben 3A.2.5./6.), Aufgabe 3A Wechselseitiger Ausschluss Den wechselseitigen Ausschluss beim Zugriff auf eine Ressource ( ) setzt man mit einem Semaphor durch, der den Anfangswert 1 hat. In jedem Prozess, der einen entsprechenden kritischen Abschnitt bezüglich der Ressource ausführen möchte, klammert man diesen Abschnitt mit einer P- und einer V-Operation des Semaphors. In Pseudo-Programmcode sieht die Lösung also wie folgt aus:

98 3.3 Synchronisation durch Semaphore 97 Semaphor: S_WA.INIT(1) Prozess 1: unkritische Operationen S_WA.P() kritischer Abschnitt S_WA.V() unkritische Operationen Prozess 2: unkritische Operationen S_WA.P() kritischer Abschnitt S_WA.V() unkritische Operationen... (Kopfzeile: Semaphor, der von allen Prozessen benutzt wird, mit Anfangswert; Spalten: nebenläufige Prozesse mit ihren Ausführungsschritten) Beim wechselseitigen Ausschluss muss also ein Prozess, der einen kritischen Abschnitt betreten möchte, gewissermaßen vorher eine Marke aus einer zugehörigen Schale entnehmen und nach dem kritischen Abschnitt wieder zurückgeben. Da es nur eine Marke gibt, kann sich nur ein Prozess gleichzeitig im kritischen Abschnitt befinden. Bei einer abgeschwächten Form des wechselseitigen Ausschlusses fordert man, dass nie mehr als n Prozesse gleichzeitig in einem kritischen Abschnitt aktiv sind. In diesem Fall wird der Semaphor mit dem Wert n initialisiert; die Struktur des Programms ist dieselbe wie oben. Beziehen sich die kritischen Abschnitte der einzelnen Prozesse auf unterschiedliche Ressourcen, so werden diese natürlich durch verschiedene Semaphore geschützt. Gibt es beispielsweise zwei Drucker, so wird jedem von ihnen ein eigener Semaphor zugeordnet, so dass auf ihnen gleichzeitig gearbeitet werden kann: Semaphore: S_WA_A.INIT(1); S_WA_B.INIT(1) Prozesse 1, 3, 5,...: S_WA_A.P() Zugriff auf Drucker A S_WA_A.V() Prozesse 2, 4, 6,...: S_WA_B.P() Zugriff auf Drucker B S_WA_B.V() Reihenfolgebedingung Eine Reihenfolgebedingung ( ) setzt man mit einem Semaphor durch, der mit 0 initialisiert wird. Der Vorgänger meldet durch eine V-Operation auf diesem Semaphor, dass er seine Aktionen beendet hat. Der Nachfolger wartet in einer P-Operation auf diese Meldung: Aufgaben 3A.2.5./6./10./11. Semaphor: S_REIHE.INIT(0) Vorgänger: Vorgänger-Operationen S_REIHE.V() Nachfolger: S_REIHE.P() Nachfolger-Operationen

99 98 3 Synchronisation Der Nachfolger muss also, bildlich gesprochen, auf eine Marke warten, die der Vorgänger mitbringt (vergleiche BILD 3.16 unten). Dieses Schema lässt sich auch einsetzen, um eine verallgemeinerte Reihenfolgebedingung durchzusetzen, bei der Vorgänger und Nachfolger in Schleifen laufen: Semaphor: S_REIHE.INIT(0) Vorgänger: while (true) { Vorgänger-Operationen S_REIHE.V() Nachfolger: while (true) { S_REIHE.P() Nachfolger-Operationen Hier wird sichergestellt, dass der Vorgänger stets mindestens so viele Durchläufe absolviert hat wie der Nachfolger. Das trifft zum Beispiel auf das Erzeuger-Verbraucher- System zu, bei dem der Erzeuger dem Verbraucher eine Folge von Produkten liefert. Aufgabe 3A Erzeuger-Verbraucher-Problem Im Zentrum des Erzeuger-Verbraucher-Problems ( ) steht ein Puffer, der maximal n Produkte speichern kann. Bei vollem Puffer muss der Erzeuger blockiert werden, bei leerem Puffer der Verbraucher, und der Pufferzugriff ist wechselseitig auszuschließen. Mit Semaphoren können diese Synchronisationsbedingungen wie folgt durchgesetzt werden: Semaphore: S_BELEGT.INIT(0); S_FREI.INIT(n); S_WA.INIT(1) Erzeuger: while (true) { erzeuge Produkt S_FREI.P() S_WA.P() speichere Produkt im Puffer S_WA.V() S_BELEGT.V() Verbraucher: while (true) { S_BELEGT.P() S_WA.P() entnimm Produkt aus Puffer S_WA.V() S_FREI.V() verbrauche Produkt Der Semaphor S_BELEGT gibt an, wie viele Pufferplätze zur Zeit belegt sind, also wie viele Produkte im Puffer stehen. Der Erzeuger erhöht den Zähler dieses Semaphors, wenn er ein Produkt in den Puffer geschrieben hat; der Verbraucher senkt ihn, wenn er ein Produkt entnimmt. Ist der Puffer leer (wie es insbesondere beim Systemstart der Fall ist; der Anfangswert ist also 0), blockiert der Verbraucher und wird erst durch die nächste Einfügeoperation wieder entblockiert. S_FREI gibt an, wie viele Pufferplätze zur Zeit frei sind. Der Semaphor sagt also, wie viele Produkte noch eingefügt werden können, bis der Puffer voll ist und der Erzeuger blockiert werden muss. Die Operationen auf S_FREI sind damit exakt gegenläufig zu denen auf S_BELEGT. S_WA schließlich setzt den wechselseitigen Ausschluss beim Zugriff auf den Puffer durch, wie in beschrieben.

100 Free ebooks ==> Synchronisation durch Semaphore 99 Damit sollte klar geworden sein, wie die Lösung des Erzeuger-Verbraucher-Problems arbeitet und warum sie korrekt ist. Wie man selbst auf eine solche Lösung kommt, wird in gezeigt werden Leser-Schreiber-Problem Beim Leser-Schreiber-Problem ( ) ist sicherzustellen, dass während des Speicherzugriffs eines Schreibers keine anderen Prozesse auf den Speicher zugreifen, dass aber Leser uneingeschränkt nebenläufig zueinander auf dem Speicher arbeiten können. Die im Folgenden präsentierten Lösungen gehen davon aus, dass es eine beliebige Anzahl von Schreibern und n Leser gibt. Die erste Lösung definiert für jeden Leser einen eigenen Semaphor, der den wechselseitigen Ausschluss zwischen diesem Leser und allen Schreibern durchsetzt: Semaphore: Sem_1.INIT(1);...; Sem_n.INIT(1) Schreiber: while (true) { unkritische Operationen Sem_1.P()... Sem_n.P() schreibe Sem_1.V()... Sem_n.V() Leser i (i=1..n): while (true) { unkritische Operationen Sem_i.P() lies Sem_i.V() Die linke Spalte gibt dabei die Schritte an, die jeder der Schreiber auszuführen hat. Ein Schreiber muss sich also, bildlich gesprochen, sämtliche Marken aus allen Schalen beschaffen, bevor er schreiben darf. Da somit keine Marken übrig bleiben, werden alle Leser und auch alle anderen Schreiber blockiert. Die Leser stören sich dagegen untereinander nicht, da jeder von ihnen auf seinem eigenen Semaphor arbeitet. Leser konkurrieren also nicht um Marken. Die zweite Lösung kommt mit deutlich weniger Semaphoren aus: Semaphore: S_WA_LES_SCHR.INIT(n); S_WA_SCHR.INIT(1) Schreiber: while (true) { unkritische Operationen S_WA_SCHR.P() for (i=0;i<n;i++) S_WA_LES_SCHR.P() schreibe for (i=0;i<n;i++) S_WA_LES_SCHR.V() S_WA_SCHR.V() Leser i (i=1..n): while (true) { unkritische Operationen S_WA_LES_SCHR.P() lies S_WA_LES_SCHR.V()

101 100 3 Synchronisation Die bildliche Vorstellung ist hier, dass es eine Schale mit n Marken gibt nämlich den Semaphor S_WA_LES_SCHR. Ein Leser muss sich vor dem Lesen eine dieser Marken beschaffen, ein Schreiber vor dem Schreiben alle n Marken. Der Effekt ist also derselbe wie bei der ersten Lösung: Wenn ein Schreiber schreibt, besitzt er alle Marken, und alle anderen Prozesse sind blockiert; ein Leser benötigt nur eine Marke, so dass genug für die anderen n 1 Leser übrig bleiben. Der Semaphor S_WA_LES_SCHR reicht aus, um die eigentliche Synchronisationsbedingung durchzusetzen; mit ihm allein ist die Lösung aber noch nicht korrekt. Es könnte nämlich sein, dass zwei Schreiber gleichzeitig ihre Schreiboperationen ausführen möchten und daher zur selben Zeit beginnen, sich Marken zu beschaffen. Da diese Beschaffung Stück für Stück geschieht, werden die Schreiber schließlich jeweils nur einen Teil der Marken (also weniger als die benötigten n) besitzen und beide nicht weiterkommen. Eine solche Situation, in der sich Prozesse gegenseitig blockieren, nennt man Deadlock (siehe hierzu 3.3.4). Damit der beschriebene Deadlock nicht auftreten kann, muss ein zweiter Semaphor, nämlich S_WA_SCHR, sicherstellen, dass nur ein Schreiber gleichzeitig in den kritischen Abschnitt der Markenbeschaffung eintreten kann. Die dritte Lösung setzt voraus, dass P- und V-Operationen den Zähler atomar auch um mehr als 1 dekrementieren bzw. inkrementieren können ( ): Semaphore: S_WA_LES_SCHR.INIT(n) Schreiber: while (true) { unkritische Operationen S_WA_LES_SCHR.P(n) schreibe S_WA_LES_SCHR.V(n) Leser i (i=1..n): while (true) { unkritische Operationen S_WA_LES_SCHR.P(1) lies S_WA_LES_SCHR.V(1) Aufgabe 3A.2.4. P(n) steht hier für eine P-Operation, die versucht, den Zähler atomar um n zu senken; entsprechend erhöht V(n) den Zähler um n. Ein Schreiber versucht also, aus der Schale n Marken auf einen Schlag zu entnehmen; sind weniger als n Marken vorhanden, blockiert er. Ein Deadlock wie in der vorherigen Lösung kann hier offensichtlich nicht auftreten, so dass der zweite Semaphor wegfallen kann. Generell lässt sich feststellen, dass bei allen drei Lösungen die Schreiber gegenüber den Lesern benachteiligt sind. Schreiber müssen sich (bildlich gesprochen) sämtliche Marken beschaffen, bevor sie auf den Speicher zugreifen können, Lesern genügt eine Marke. Dies wirkt sich bei der dritten Lösung besonders negativ aus: Damit ein Schreiber zum Zuge kommt, müssen alle Marken gleichzeitig da sein, was im Fall von vielen aktiven Lesern recht unwahrscheinlich ist. Im Extremfall arbeitet immer mindestens ein Leser auf dem Speicher, was zum Verhungern (engl.: starvation) der Schreiber führt.

102 3.3 Synchronisation durch Semaphore 101 Es gibt semaphorbasierte Lösungen des Leser-Schreiber-Problems, die die Schreiber besser behandeln also beispielsweise alle noch nicht zugreifenden Leser warten lassen, sobald ein Schreiber seinen Zugriffswunsch angemeldet hat. Diese Lösungen sind aber leider nicht so elegant wie die oben gezeigten, denn es müssen explizite Zähler geführt werden, die die Anzahl der zugreifenden Leser und Schreiber angeben und deren Benutzung durch weitere Semaphore geschützt werden muss. Ein entsprechender Ansatz wird im Zusammenhang mit Monitoren, einem weiteren Synchronisationsmechanismus, gezeigt ( ) Philosophenproblem Beim Philosophenproblem ( ) muss jeder Philosoph, bevor er essen kann, die beiden Gabeln belegen, die links und rechts von seinem Platz liegen und die er sich mit seinem linken bzw. rechten Nachbarn teilt ( BILD 3.7). Da also jede Gabel von zwei Philosophen wechselseitig ausgeschlossen benutzt werden muss, erscheint auf den ersten Blick die folgende Lösung angemessen: Semaphore: S_GABEL_0.INIT(1);...; S_GABEL_4.INIT(1) Philosoph i (i=0..3): while (true) { denke S_GABEL_i.P() S_GABEL_i+1.P() iss S_GABEL_i.V() S_GABEL_i+1.V() Philosoph 4: while (true) { denke S_GABEL_4.P() S_GABEL_0.P() iss S_GABEL_4.V() S_GABEL_0.V() Dieses System ist aber deadlockgefährdet, denn es kann vorkommen, dass alle Philosophen gleichzeitig essen wollen. Jeder Philosoph i belegt dann durch S_GA- BEL_i.P() seine jeweils linke Gabel, womit alle Gabeln vergeben sind. Anschließend blockiert bei jedem Philosophen die zweite P-Operation, und es ist ein Deadlock entstanden, da keiner die Gabel, die er bereits besitzt, wieder aufgibt. Es gibt mehrere Möglichkeiten, das System deadlockfrei zu gestalten. Beispielsweise kann man einen Philosophen umdrehen, ihn also erst nach rechts und dann nach links greifen lassen. Auch kann man einen Wächter einführen, der höchstens vier Philosophen gleichzeitig an den Tisch lässt realisiert durch einen zusätzlichen Semaphor, der mit 4 initialisiert wird. Auf ihm führt jeder Philosoph eine P-Operation aus, bevor er die P-Operationen auf den Gabeln versucht, sowie eine V-Operation, nachdem er die Gabeln wieder freigegeben hat. Die korrekte Lösung, die im Folgenden präsentiert wird, setzt voraus, dass ein Prozess P-Operationen auf mehreren Semaphoren gleichzeitig, also atomar ausführen kann ( ). Sie sieht fast genau so aus wie die Lösung oben:

103 102 3 Synchronisation Semaphore: S_GABEL_0.INIT(1);...; S_GABEL_4.INIT(1) Philosoph i (i=0..3): while (true) { denke S_GABEL_i.P() & S_GABEL_i+1.P() iss S_GABEL_i.V() S_GABEL_i+1.V() Philosoph 4: while (true) { denke S_GABEL_4.P() & S_GABEL_0.P() iss S_GABEL_4.V() S_GABEL_0.V() Das & zwischen den beiden P-Operationen steht für das Alles-oder-nichts-Prinzip ( ): Der Prozess führt die P-Operationen mit ihren Dekrementierungen nur dann aus, wenn beide nicht blockieren. Ansonsten dekrementiert er keinen der Zähler, lässt also auch die Gabel liegen, die frei ist, und wartet darauf, dass er beide Gabeln gleichzeitig greifen kann Systematische Lösung von Problemen Aufgaben 3A Der korrekte Umgang mit Synchronisationsmechanismen ist manchmal nicht einfach. Das gilt insbesondere für Semaphore, die deutlich flexibler einsetzbar sind als die primitiven Mechanismen, die aber eine besondere Sorgfalt bei der Programmierung verlangen: Man muss hier genau überlegen, welche Semaphore man braucht, wie man sie initialisiert und wo im Programm man ihre P- und V-Operationen platziert. Hierbei hilft eine systematische Vorgehensweise. Wie man ein Synchronisationsproblem mit Semaphoren systematisch löst, soll an folgendem Szenario dargestellt werden: Eine Kneipe wird von der benachbarten Brauerei mit Bierfässern beliefert. Jedes Mal, wenn der Brauer ein Fass abgefüllt hat, bringt er es in den Keller der Kneipe. Jedes Mal, wenn der Kneipenwirt im Gastraum ein neues Bierfass benötigt, steigt er in seinen Keller und holt eines. Falls der Keller leer ist, wartet der Wirt, bis der Brauer ein neues Fass dorthin geliefert hat. Der Keller kann höchstens zehn Bierfässer aufnehmen ist er voll, so wartet der Brauer bei einer Lieferung, bis der Wirt ein Fass aus dem Keller geholt hat. Der Keller ist so eng, dass sich nur eine Person gleichzeitig darin aufhalten kann; man kann jedoch von oben feststellen, ob sich gerade jemand dort befindet und wie viele Fässer momentan vorhanden sind. Zu Beginn ist der Keller leer. Um solch ein Problem systematisch zu lösen, sollte man folgende Schritte ausführen: Schritt 1: Die Prozesse des Systems identifizieren Ein Prozess ist eine Aktivität, die eine Aktionsfolge ausführt und sich dabei mit anderen Aktivitäten, die gleichzeitig ablaufen, synchronisieren muss. Welche Prozesse vorhanden sind, ergibt sich meist unmittelbar aus der Problembeschreibung. Im Beispiel gibt es zwei Prozesse, den Brauer und den Wirt. Der Keller und die Bierfässer sind dagegen keine Prozesse, da sie ja selbst keine Aktionen ausführen.

104 3.3 Synchronisation durch Semaphore 103 Schritt 2: Die Aktionen der einzelnen Prozesse skizzieren Hierzu notiert man in Stichworten die Aktionen der einzelnen Prozesse. Dabei muss deutlich werden, welcher Prozess was tut, in welcher zeitlichen Reihenfolge seine Aktionen ablaufen und wo es Schleifen und Verzweigungen gibt. Die Aktionen und ihre Abfolge ergeben sich wie die Prozesse meist unmittelbar aus der Problemstellung. Zur übersichtlichen Darstellung stellt man (wie schon in den vorangehenden Abschnitten geschehen) am besten ein mehrspaltiges Schema auf. Seine Spalten sind den einzelnen Prozessen zugeordnet und beschreiben deren Aktionen. Synchronisationsoperationen, also die P- und V-Operationen auf Semaphoren, fügt man hier allerdings noch nicht ein; sie werden erst später ergänzt. Das Schema für das Beispielproblem sieht wie folgt aus: Brauer: while (true) { fülle ein Fass gehe zur Kneipe warte, bis im Keller Platz für ein Fass und dort keine andere Person ist bringe das Fass in den Keller gehe zur Brauerei Wirt: while (true) { gehe zur Kellertreppe warte, bis im Keller ein Fass und dort keine andere Person ist hole ein Fass aus dem Keller gehe in den Gastraum leere das Fass Brauer und Wirt führen also Endlosschleifen aus, in denen sie immer wieder dieselben Aktionsfolgen ausführen. Schritt 3: Die Synchronisationsbedingungen identifizieren Eine Synchronisationsbedingung ist eine zeitliche Beziehung, die zwischen zwei oder mehr Prozessen durchzusetzen ist. Die Synchronisationsbedingung selbst ist also kein Prozess, sie ist vielmehr eine Abhängigkeit zwischen Prozessen. Man identifiziert die Bedingungen am besten dadurch, dass man die Problembeschreibung genau durchliest und jede zeitliche Bedingung, die dort beschrieben wird, in einer Liste notiert oder mit einer eigenen Farbe markiert. Im Beispielproblem gibt es drei Synchronisationsbedingungen: Im Keller darf sich nur eine Person gleichzeitig aufhalten. Der Wirt muss warten, bis mindestens ein Fass im Keller ist, das heißt, bis der Brauer (bei leerem Keller) mindestens ein Fass geliefert hat. Der Brauer muss warten, bis mindestens ein freier Platz für ein Fass im Keller ist, das heißt, bis der Wirt (bei vollem Keller) mindestens ein Fass entfernt hat. Es müssen hier also ein wechselseitiger Ausschluss und zwei Reihenfolgebedingungen durchgesetzt werden.

105 104 3 Synchronisation Schritt 4: Die Semaphore definieren Jede der Synchronisationsbedingungen muss mit einem eigenen Semaphor durchgesetzt werden. Dazu muss man nun angeben, wie die Semaphore heißen sollen, welchen Bedingungen sie jeweils zugeordnet sind und mit welchen Anfangswerten sie initialisiert werden. Dabei sollte man schon eine grobe Idee davon haben, wie man die Semaphore einsetzen, also wie man ihre P- und V-Operationen in den Prozessen platzieren wird. Die Semaphore des Beispielproblems und ihre Initialisierungen sind wie folgt: S_WA_KELLER mit Anfangswert 1: zum wechselseitigen Ausschluss des Zugriffs auf den Keller. Hier kann man unmittelbar die Standardlösung wechselseitiger Ausschluss mit Semaphoren ( ) übernehmen. S_FÄSSER_DA mit Anfangswert 0: zum Blockieren des Wirts bei leerem Keller. Hier kann man unmittelbar die Standardlösung Reihenfolge mit Semaphoren übernehmen, wobei man ausnutzt, dass ein zyklisch laufender Vorgänger den Semaphor auf Vorrat mehrmals hintereinander hochzählen darf ( ): Der Wirt als Nachfolger muss auf Lieferungen des Brauers, also seines Vorgängers, warten, wobei der Brauer durchaus mehrere Fässer auf Vorrat liefern kann. Die Zählvariable des Semaphors gibt offensichtlich an, wie viele Fässer sich gerade im Keller befinden daher der Name des Semaphors. Ihr Anfangswert ist 0 wie in der Standardlösung. S_PLÄTZE_FREI mit Anfangswert 10: zum Blockieren des Brauers bei vollem Keller. Zur Durchsetzung der dritten Bedingung könnte man auf die Idee kommen, den Brauer zu blockieren, wenn S_FÄSSER_DA den Wert 10 erreicht hat. Das geht jedoch nicht: Die einzige blockierende Operation auf einem Semaphor ist die P-Operation, und sie blockiert nur dann, wenn der Semaphor den Wert 0 hat. Man setzt die dritte Bedingung daher mit einem eigenen Semaphor durch, der ebenfalls eine Reihenfolgebeziehung realisiert. Hier ist der Brauer der Nachfolger, der wartet, bis sein Vorgänger, der Wirt, einen freien Platz im Keller geschaffen hat. Dabei kommt es nicht auf die Anzahl der Fässer, sondern auf die Anzahl der freien Plätze im Keller an. Der neue Semaphor, S_PLÄTZE_FREI, gibt diese Anzahl an und wird, da der Keller zu Beginn leer ist, mit 10 initialisiert. Er wird dann wie bei der Standardlösung Reihenfolge mit Semaphoren eingesetzt ( ), wobei allerdings der Nachfolger einen möglichen Vorlauf von 10 Runden hat. Wie man am Beispiel sieht, lassen sich der Anfangswert und der spätere Einsatz eines Semaphors oft direkt an Lösungen der Standardprobleme ablesen. Manchmal muss man allerdings selbst ein wenig überlegen, wie man den Semaphor einsetzen wird, und aus diesen Überlegungen dann den Anfangswert ableiten.

106 3.3 Synchronisation durch Semaphore 105 Schritt 5: Die Aktionsfolgen aus Schritt 2 durch Semaphoroperationen ergänzen Zum Abschluss müssen die P- und V-Operationen der Semaphore in die Programme aus Schritt 2 eingefügt werden. P-Operationen müssen dabei an die Stellen gesetzt werden, an denen ein Prozess möglicherweise warten muss an denen er also eventuell blockiert wird, bis ein bestimmtes Ereignis eintritt. V-Operationen kommen an die Stellen, an denen ein Prozess einem anderen mitteilt, dass er weiterlaufen kann an denen er also möglicherweise einen wartenden Prozess entblockiert. Für das Beispielproblem sehen die vollständigen Programme so aus: Semaphore: S_WA_KELLER.INIT(1); S_PLÄTZE_FREI.INIT(10); S_FÄSSER_DA.INIT(0) Brauer: while (true) { fülle ein Fass gehe zur Kneipe S_PLÄTZE_FREI.P() S_WA_KELLER.P() bringe das Fass in den Keller S_WA_KELLER.V() S_FÄSSER_DA.V() gehe zur Brauerei Wirt: while (true) { gehe zur Kellertreppe S_FÄSSER_DA.P() S_WA_KELLER.P() hole ein Fass aus dem Keller S_WA_KELLER.V() S_PLÄTZE_FREI.V() gehe in den Gastraum leere das Fass Der Brauer bzw. der Wirt führt also eine V-Operation durch, wenn er ein Fass geliefert bzw. einen freien Platz geschaffen hat. Diese V-Operation entblockiert seinen Partner, falls dieser wartet. Beim Einsetzen der P- und V-Operationen muss man natürlich darauf achten, dass man sich keine Deadlocks einhandelt siehe hierzu und die Beispiele in Wie man sicher bemerkt hat, ist das hier gelöste Problem das klassische Erzeuger-Verbraucher-Problem ( ), und die Lösung, die sich durch systematische Überlegungen ergeben hat, ist die, die bereits in präsentiert wurde Fehlerquellen Ein Programmierer hat, wie schon gesagt, große Freiheiten bei der Definition und Initialisierung von Semaphoren und bei der Nutzung ihrer P- und V-Operationen. Diese Freiheiten bergen die Gefahr, bei der Programmierung Fehler zu machen. Durch eine systematische Vorgehensweise, wie in empfohlen, und den Rückgriff auf Standardlösungen kann man viele dieser Fehler vermeiden. Dennoch treten manche Arten von Fehlern immer wieder auf, so dass man sie kennen sollte. Beliebt sind insbesondere Deadlocks und die Missachtung der Atomarität der Semaphoroperationen.

107 106 3 Synchronisation Deadlocks: Problematik Ein Deadlock blockiert mehrere Prozesse so, dass keiner von ihnen weiterlaufen kann. Ein solches Problem kann nicht nur bei der Programmierung, sondern auch im Alltagsleben auftreten: Zwei Kinder im Sandkasten, von denen eines den Eimer und das andere die Schaufel hat, befinden sich in einem Deadlock, denn jedes möchte das Spielzeug des anderen haben, aber das eigene nicht herausrücken. Keines von beiden kommt also zum Spielen. Im Straßenverkehr sind solche Deadlocks ebenfalls möglich beispielsweise, indem sich vier Verkehrsströme in einem Kreisel derart verklemmen, dass kein Auto mehr weiterkommt: BILD 3.19 Deadlocks im Alltag Im Sandkasten: Ich will die Schaufel! Im Kreisverkehr:... und ich den Eimer! Die Deadlock-Gefahr im Leser-Schreiber-Problem ( ) und im Philosophenproblem ( ) wurde bereits diskutiert. Auch beim Erzeuger-Verbraucher-Problem ( ) kann bei ungeschickter Platzierung der P-Operationen ein Deadlock auftreten: Belegt der Verbraucher zuerst den Puffer für sich und stellt danach fest, dass dieser leer ist, so kommt die Programmausführung zum Erliegen. Der Verbraucher kommt nämlich nicht weiter, da er nichts aus dem Puffer lesen kann, und der Erzeuger kommt nicht weiter, da ihm der Verbraucher den Zugang zum Puffer versperrt. Auf alle diese Beispiele trifft die folgende allgemeine Definition zu: DEFINITION Deadlock Ein Deadlock (deutsch auch: Verklemmung) ist eine Situation bei der nebenläufigen Ausführung mehrerer Prozesse, in der jeder dieser Prozesse nicht weiterlaufen kann, weil er auf eine Aktion eines anderen dieser Prozesse wartet. Ein Deadlock liegt insbesondere dann vor, wenn jeder der Prozesse auf die Freigabe einer Ressource wartet, die ein anderer der Prozesse belegt. Alle Prozesse, die an einem Deadlock beteiligt sind, befinden sich also im Zustand blockiert ( BILD 2.23) und könnten aus diesem Zustand nur durch einen anderen Prozess befreit werden, der aber ebenfalls blockiert ist. Die Prozesse haben sich damit so ineinander verklemmt, dass sie von allein, also ohne Zutun von außen, nicht mehr weiterkommen. Wichtig für die Analyse von Deadlock-Problemen ist der so genannte Belegungs-Anforderungs-Graph. Dieser Graph enthält für jeden Prozess einen Knoten, und es führt eine Kante von Prozess A zu Prozess B, wenn A eine Ressource anfordert, die

108 3.3 Synchronisation durch Semaphore 107 zur Zeit von B belegt ist. Im Fall eines Deadlocks enthält dieser Graph stets einen Zyklus. Die Zyklen bei den beiden Alltagsbeispielen sind bereits in BILD 3.19 sichtbar. Für das Philosophenproblem sieht der Graph bei einem Deadlock wie folgt aus: fordert Gabel 0 besitzt Gabel 4 Ph4 fordert Gabel 4 besitzt Gabel 0 Ph0 fordert Gabel 1 Ph1 besitzt Gabel 1 fordert Gabel 2 BILD 3.20 Deadlock beim Philosophenproblem: Zyklus im Belegungs- Anforderungs- Graphen besitzt Gabel 3 Ph3 Ph2 besitzt Gabel 2 fordert Gabel Deadlocks: Lösungen Zur Lösung des Deadlock-Problems gibt es verschiedene Ansätze: Die Deadlock-Verhinderung basiert auf allgemeinen Regeln für die Programmierung. Werden diese Regeln beim Schreiben von Prozessen eingehalten, ist es strukturell unmöglich, dass diese in einen Deadlock geraten (siehe unten). Bei der Deadlock-Vermeidung prüft das Betriebssystem jeden Schritt eines Prozesses darauf, ob er zu einem Deadlock führen könnte, und verhindert oder verzögert ihn gegebenenfalls. Die Deadlock-Aufhebung behebt einen Deadlock, wenn er eingetreten ist, dadurch, dass Betriebssystem und/oder Administrator den Prozessen Ressourcen entziehen, Prozesse abbrechen oder (im Extremfall) das System neu starten. Für den Programmierer nebenläufiger Prozesse ist die Deadlock-Verhinderung interessant, da er mit ihr selbst seine Programme deadlockfrei gestalten kann. Eine oft erfolgreiche Strategie ist zu verhindern, dass es im Belegungs-Anforderungs-Graphen zu Zyklen kommt, denn ohne Zyklen kann kein Deadlock vorliegen ( ). Hierfür hat der Programmierer die folgenden beiden Möglichkeiten: Er kann seine Prozesse (oder zumindest bestimmte Phasen davon) so programmieren, dass sie ihre Ressourcen nach dem Alles-oder-nichts-Prinzip belegen also warten, bis alle benötigten Betriebsmittel frei sind, und sie dann auf einen Schlag belegen (vergleiche ). Da damit ein Prozess entweder Ressourcen belegt oder Ressourcen fordert, aber nie beides, ist ein Zyklus nicht möglich. Er kann seine Prozesse (oder zumindest bestimmte Phasen davon) so programmieren, dass alle Prozesse die Ressourcen in einer vorgegebenen Reihenfolge anfordern. Genauer gesprochen, gibt man den Ressourcen Ordnungsnummern und fordert, dass ein Prozess, der Ressourcen der Ordnung i besitzt, nur Ressourcen einer Ordnung j mit j>i anfordern darf. Diese geordnete Ressourcenanforderung, bei der Ressourcen nur in echt aufsteigender Rangfolge belegt werden dürfen, verhindert ebenfalls Zyklen im Belegungs-Anforderungs-Graphen. Aufgaben 3A.2.15./17.

109 108 3 Synchronisation Aufgabe 3A Zur Verdeutlichung sollen kurz einige Beispiele betrachtet werden. Die deadlockgefährdete Vorgehensweise bei den Kindern im Sandkasten sieht so aus: Semaphore: S_EIMER.INIT(1); S_SCHAUFEL.INIT(1) Kind A: S_EIMER.P() S_SCHAUFEL.P()... Kind B: S_SCHAUFEL.P() S_EIMER.P()... Hier kann der in BILD 3.19 gezeigte Zyklus entstehen. Ebenso deadlockgefährdet ist die erste Lösung des Philosophenproblems in Die korrekte Lösung des Sandkasten-Problems nach dem Alles-oder-nichts-Prinzip lautet: Semaphore: S_EIMER.INIT(1); S_SCHAUFEL.INIT(1) Kind A: S_EIMER.P() & S_SCHAUFEL.P()... Kind B: S_EIMER.P() & S_SCHAUFEL.P()... Der &-Ausdruck bedeutet hier, dass der ausführende Prozess blockiert, bis beide Semaphorzähler den Wert 1 haben, und sie erst dann also nur beide zusammen! dekrementiert. Eine entsprechende Lösung des Philosophenproblems wurde am Ende von gezeigt. Die Lösung nach dem Prinzip der geordneten Ressourcenanforderung sieht so aus: Semaphore: S_EIMER.INIT(1); S_SCHAUFEL.INIT(1) Kind A: S_EIMER.P() S_SCHAUFEL.P()... Kind B: S_EIMER.P() S_SCHAUFEL.P()... Beim Philosophenproblem ergibt sich eine solche Lösung, wenn man das erste Schema in so ändert, dass Philosoph 4 erst zur Gabel 0 und dann zur Gabel 4 greift Missachtung der Atomarität Anfänger versuchen bisweilen, so hat es zumindest der Autor beobachtet, die Atomarität der P-Operation zu umgehen. Für eine P-Operation wird bekanntlich gefordert, dass der (erfolgreiche) Test, ob der Semaphorzähler größer als 0 ist, und seine anschließende Dekrementierung ohne Unterbrechung durchgeführt werden ( ).

110 3.3 Synchronisation durch Semaphore 109 Die Umgehung besteht nun darin, dass man eine if-anweisung der folgenden Form programmiert: if (Sem.Zähler>0) Sem.P() Sem.Zähler ist dabei der aktuelle Zählerwert des Semaphors. Die P-Operation soll also nur dann ausgeführt werden, wenn der Zähler größer als 0 ist; eine Blockierung des Prozesses soll also in jedem Fall vermieden werden. Da aber hier Test und Dekrementierung nicht atomar ablaufen, ist die Vorgehensweise nicht korrekt. Es könnte nämlich ein anderer Prozess mit seiner P-Operation dazwischenkommen und damit den Zähler auf 0 senken, so dass der ursprüngliche Prozess dann in seiner P-Operation doch blockiert. Aufgaben 3A.2.1./9., 3A Einsatz von sleep() Ein weiterer beliebter Fehler ist, bei Reihenfolgebedingungen auf Semaphore (und andere Synchronisationsmechanismen) ganz zu verzichten und stattdessen den Nachfolger um eine feste Zeitdauer zu verzögern beispielsweise durch die UNIX/Linux- Funktion sleep() ( ): Vorgänger: Aktionen des Vorgängers Nachfolger: sleep(2 sec) Aktionen des Nachfolgers Dass hier der Vorgänger möglicherweise schneller als in zwei Sekunden fertig ist und der Nachfolger dann länger warten muss als nötig, ließe sich verschmerzen. Schlimmer ist, dass nicht garantiert werden kann, dass der Vorgänger nie mehr als zwei Sekunden (oder was immer als Schlafzeit festgelegt wird) benötigt. In ungünstigen Fällen zum Beispiel wenn viele Prozesse auszuführen sind oder ein dringender hochpriorer Prozess ansteht können die Scheduling-Entscheidungen des Betriebssystems ( ) dazu führen, dass der Vorgänger sehr lange verzögert wird und dann der Nachfolger, der nunmehr wieder wach ist, vor ihm den Prozessor erhält Mangelnde Fairness Lösungen, die die Synchronisationsbedingungen nicht immer durchsetzen oder die die Gefahr eines Deadlocks bergen, sind falsch und dürfen daher nicht eingesetzt werden. Aber auch eine korrekte Lösung sollte nicht benutzt werden, wenn sie Prozesse nicht fair behandelt. Ein Beispiel für eine unfaire Vorgehensweise ist die dritte Lösung des Leser-Schreiber-Problems, bei dem Schreiber verhungern können ( ). Leider sind die Übergänge zwischen fairen und unfairen Lösungen fließend. Beispielsweise behandeln die erste und die zweite Lösung des Leser-Schreiber-Problems die Schreiber besser als die dritte, aber (im Vergleich zu den Lesern) immer noch nicht gut. Für eine bessere Fairness sind dort, wie bei anderen Problemen auch, kompliziertere Lösungen nötig ( ). Noch komplexer wird es, wenn Semaphore keine First-Come-First-Served-(FCFS-)Reihenfolge garantieren, sondern es jeweils zu Aufgaben 3A.2.2., 3A.3.1. Aufgabe 3A.2.4.

111 110 3 Synchronisation Wettrennen kommt. Hier müssten gegebenenfalls FCFS-Warteschlangen explizit ausprogrammiert werden. Es ist daher schwer, allgemeine Regeln zur Programmierung fairer Nebenläufigkeit anzugeben. Als Rat an den Programmierer bleibt nur, die möglichen Abläufe in Gedanken durchzuspielen, dabei auf eine potentiell unfaire Behandlung bestimmter Prozesse zu achten und die Lösung entsprechend zu überarbeiten. 3.4 Synchronisation durch Monitore Bei der Synchronisation mit Semaphoren muss der Programmierer die Semaphore erzeugen, er muss sie geeignet initialisieren, und er muss ihre P- und V-Operationen in das Programm einsetzen. Die Programmierung mit Semaphoren lässt somit viele Freiheiten, sie ist dafür aber recht aufwendig und birgt, wie schon gesagt, Fehlergefahren. Für die Lösung von Standardproblemen der Synchronisation, wie zum Beispiel der wechselseitige Ausschluss, sind daher einfachere, narrensichere Synchronisationsmechanismen wünschenswert. Ein leicht zu benutzendes Konzept zur Durchsetzung des wechselseitigen Ausschlusses sind Monitore. Sie wurden von C.A.R. Hoare und Per Brinch Hansen 1974/75 vorgeschlagen Grundprinzip von Monitoren BILD 3.21 Bankautomat als Monitor Definition des Monitorbegriffs Ein Monitor lässt sich mit einem Gerät vergleichen, das nur von jeweils einer Person gleichzeitig genutzt werden kann beispielsweise einem Bankautomaten: BANK AUTOMAT Bitte einzeln herantreten! Angebot: Abheben Stand prüfen Überweisen... Ein solches Gerät bietet den Aktivitäten, die an ihn herantreten, mehrere Zugriffsmöglichkeiten, ist aber selbst passiv. Von den Zugriffsmöglichkeiten kann nur eine zur selben Zeit genutzt werden und dies auch nur von einer Aktivität. Diese Beobachtungen führen zu der folgenden Definition: DEFINITION Monitor Ein Monitor ist ein Objekt mit Attributen (Wertspeichern) und Methoden (Zugriffsoperationen). Die Methoden auf einem Monitor werden stets wechselseitig ausgeschlossen ausgeführt. Monitore realisieren also das Prinzip der Datenkapselung, das von der objektorientierten Programmierung her bekannt ist: Sie speichern Daten in ihren Attributen und definieren Methoden, über die (und nur die!) auf die Attribute zugegriffen werden kann.

112 3.4 Synchronisation durch Monitore 111 Zusätzlich zu den Eigenschaften normaler Objekte ist sichergestellt, dass auf einem Monitor nur ein Prozess gleichzeitig arbeiten kann. Ein Prozess, der eine Methode eines Monitors aufruft, belegt ihn damit und gibt ihn erst mit Ende der Methodenausführung wieder frei. Andere Prozesse, die ebenfalls eine der Methoden dieses Monitors ausführen möchten, müssen bis zu dieser Freigabe warten. Dabei ist, im Gegensatz zum Bankautomaten-Beispiel, nicht sichergestellt, dass jeweils der am längsten wartende Prozess Zugriff auf den Monitor bekommt; vielmehr kann der Scheduler ( ) irgendeinen der wartenden Prozesse auswählen. Gegenüber Synchronisationsmechanismen wie Spinlocks oder Semaphore haben Monitore insbesondere die folgenden beiden Vorteile: Um einen wechselseitigen Ausschluss zu realisieren, muss ein Programmierer lediglich ein Objekt als Monitor kennzeichnen. Der Compiler kümmert sich dann automatisch um seine Durchsetzung, zum Beispiel mit einem Mutex ( ). Die Synchronisationsoperationen können im engen Zusammenhang mit den zu schützenden Daten und deren Zugriffsoperationen programmiert werden. Damit wird der Programmcode deutlich übersichtlicher, als wenn sich die Operationen auf die Programmstücke der zugreifenden Prozesse verteilen wie es bei Semaphoren mit ihren P- und V-Operationen der Fall ist. Monitore eignen sich besonders gut zur Realisierung von Datenstrukturen, bei denen sichergestellt werden muss, dass ein schreibender Zugriff (der möglicherweise aus mehreren aufeinanderfolgenden Teiloperationen besteht) ohne gleichzeitige andere Zugriffe abläuft. Ein einfaches Beispiel ist eine Telefonliste, bei der während der Änderung eines Eintrags (Name, Adresse,...) andere Zugriffe warten müssen Beispiel: Einfacher Ringpuffer mit Überschreiben Ein Beispiel, das im Detail betrachtet werden soll, ist ein einfacher Ringpuffer, in den Daten geschrieben werden können. Der Ringpuffer hat eine begrenzte Kapazität; ist er voll, so überschreibt eine Schreiboperation den jeweils ältesten Dateneintrag. Zudem soll es eine Ausgabeoperation geben, die den gesamten Inhalt des Puffers auf den Bildschirm ausgibt. Ein solcher Ringpuffer arbeitet also nach demselben Prinzip wie ein Flugdatenschreiber (eine Black Box ), der jeweils die Daten der letzten 30 Flugminuten enthält und bei Bedarf ausgelesen werden kann. Ein einfacher Ringpuffer kann durch einen Array mit einem Schreibindex implementiert werden. Der Schreibindex gibt die Arrayposition an, an der die nächste Schreiboperation stattfindet. Er wird bei jeder Operation um 1 erhöht und auf 0 zurückgesetzt, wenn er die obere Arraygrenze überschreitet. Der Index durchläuft den Array also zyklisch, wodurch sich der Name Ringpuffer erklärt: Aufgabe 3A Array zur Speicherung der Daten: 1. Anfangssituation: 2. Nach dreimal Schreiben: S Schreibindex S 3. Nach nochmals dreimal Schreiben: S Ausgabe nach Schritt 3: BILD 3.22 Implementierung eines einfachen Ringpuffers durch einen Array mit Schreibindex

113 112 3 Synchronisation Die folgende Abbildung zeigt die Klassendefinition für solche einfachen Ringpuffer: BILD 3.23 Attribute und Methoden eines einfachen Ringpuffers Monitor EinfacherRingpuffer - kapazität : int =... - inhalt : int[kapazität] - schreibindex : int = 0 + schreiben(neu : int) + ausgeben() (Notation an UML angelehnt, = gibt den Initialwert an) schreiben(neu : int): inhalt[schreibindex] = neu schreibindex++ if (schreibindex > kapazität-1) schreibindex = 0 ausgeben(): für i von schreibindex-1 hinunter bis 0: gib inhalt[i] aus für i von kapazität-1 hinunter bis schreibindex: gib inhalt[i] aus Eine entsprechende Java-Implementation findet man in PROG 3.6. Aus den Definitionen der Methoden wird unmittelbar klar, warum auf einem Ringpuffer nur eine Schreiboperation zur selben Zeit stattfinden darf und warum währenddessen auch Ausgabeoperationen ausgeschlossen werden müssen: Die Zuweisung an den Array sowie die Erhöhung und gegebenenfalls Rücksetzung des Index müssen atomar (also in einem Rutsch ) ablaufen, da es sonst zu Inkonsistenzen kommen kann. Im Vergleich zu den Semaphorbeispielen in 3.3 fällt auf, dass explizite Synchronisationsoperationen fehlen. Sie sind auch nicht nötig: Der Ringpuffer ist als Monitor gekennzeichnet, und so wird der wechselseitige Ausschluss automatisch durchgesetzt Bedingungsvariablen DEFINITION Bedingungsvariable eines Monitors Zweck und Einsatz Nur in einfachen Anwendungsfällen reicht es aus, lediglich den wechselseitigen Ausschluss sicherzustellen. In vielen Szenarien müssen weitere Synchronisationsbedingungen durchgesetzt werden nämlich dann, wenn ein Prozess eine Zugriffsoperation erst dann beginnen oder fortführen kann, wenn eine bestimmte Bedingung erfüllt ist. Ein Beispiel ist das Erzeuger-Verbraucher-Problem ( ), bei dem man in einen Puffer nur dann etwas einfügen oder aus ihm etwas entnehmen kann, wenn er nicht voll bzw. nicht leer ist. Bedingungen, die über den wechselseitigen Ausschluss hinausgehen, werden mit Hilfe von Bedingungsvariablen durchgesetzt: Eine Bedingungsvariable (engl.: condition variable) eines Monitors repräsentiert eine Synchronisationsbedingung bezüglich dieses Monitors. Auf eine Bedingungsvariable cond kann nur innerhalb einer Zugriffsoperation ihres Monitors zugegriffen werden. Dabei sind zwei Operationen möglich: Die Operation wait(cond) blockiert den aufrufenden Prozess auf der Variablen cond und gibt den Monitor für Zugriffe anderer Prozesse frei. Die Operation signal(cond) entblockiert einen Prozess, der sich auf der Variablen cond blockiert hat (sofern vorhanden).

114 3.4 Synchronisation durch Monitore 113 Bedingungsvariablen werden wie folgt verwendet (Beispiele siehe und 3.4.3): Blockierung eines Prozesses: Ein Prozess führt eine Methode, also eine Zugriffsoperation auf einem Monitor aus und kann darin nur unter einer bestimmten Bedingung weiterlaufen. Er prüft die Bedingung, indem er einen booleschen Ausdruck auswertet, der vom Programmierer seines Programmcodes ausformuliert wurde. Stellt sich dabei heraus, dass er nicht weiterlaufen kann, ruft er die Funktion wait(cond) auf. cond ist der Name der Bedingungsvariablen, die der Programmierer der Bedingung zugeordnet hat. Der Prozess blockiert sich damit und gibt den Monitor frei, so dass ein anderer Prozess auf den Monitor zugreifen kann. Entblockierung eines Prozesses: Ein Prozess führt eine Zugriffsoperation auf einem Monitor aus und erfüllt darin eine Bedingung, unter der ein wartender Prozess weiterlaufen kann. Er ruft daher die Funktion signal(cond) auf. cond ist dabei wiederum der Name der Bedingungsvariablen, die der Bedingung zugeordnet ist. Damit wird einer der Prozesse, die auf cond warten, entblockiert. Obwohl es auf den ersten Blick so scheinen mag, ist damit noch nicht alles geklärt. Die Definition und ihre bisherige Erläuterung lassen nämlich offen, wie der Monitor an den entblockierten Prozess übergeben wird. Nach Definition eines Monitors ist es nämlich ausgeschlossen, dass entblockierender und entblockierter Prozess gleichzeitig im Monitor weiterlaufen. Zudem ist noch unklar, ob der Monitor direkt an den entblockierten Prozess übergeht oder ob es möglich ist, dass sich ein dritter Prozess dazwischendrängt. Hierzu gibt es in der Praxis verschiedene Ansätze (wobei im Folgenden P 1 den entblockierenden und P 2 den entblockierten Prozess bezeichnet): Ansatz 1: P 2 übernimmt den Monitor sofort von P 1 und läuft in seiner Zugriffsoperation weiter. Für P 1 gibt es hier zwei Möglichkeiten: Möglichkeit 1a: P 1 wird nach seinem signal()-aufruf blockiert und läuft erst später weiter, wenn der Monitor wieder frei ist. Möglichkeit 1b: P 1 führt signal() als letzte Aktion seiner Zugriffsoperation aus, gibt den Monitor also unmittelbar frei, und P 2 übernimmt ihn. Ansatz 2: P 2 wird zwar entblockiert, muss aber warten, bis der Monitor wieder frei wird. Es gibt hier zwei Möglichkeiten dafür, wann P 2 den Monitor erhält: Möglichkeit 2a: P 2 übernimmt den Monitor stets unmittelbar von P 1, sobald dieser ihn freigegeben hat. Möglichkeit 2b: P 2 übernimmt den Monitor irgendwann nach dessen Freigabe, also möglicherweise erst, wenn weitere Prozesse auf ihn zugegriffen haben. Zudem gibt es Implementationen (zum Beispiel in UNIX/Linux, ), in denen möglicherweise mehrere wartende Prozesse entblockiert werden und sich dann ein Wettrennen um den Monitor liefern. Bei der Programmierung muss man berücksichtigen, welchen dieser Ansätze die Plattform, auf der man arbeitet, realisiert. Nur wenn der entblockierte Prozess weiß, dass er den Monitor direkt vom entblockierenden Prozess übernommen hat, kann er unmit-

115 114 3 Synchronisation telbar weiterlaufen. Ansonsten muss er die Bedingung erneut prüfen (da möglicherweise ein dritter Prozess sie zwischenzeitlich geändert hat) und sich gegebenenfalls wieder blockieren. Programmiertechnisch gesehen, muss die Bedingung hier also in einem while und nicht in einem if geprüft werden ( , 3.4.3, ). Im Zweifelsfall sollte man immer ein while verwenden. Man muss sich bewusst sein, dass Bedingungsvariablen keine booleschen Variablen und auch keine Zähler (wie bei Semaphoren) sind! Ein Signal an eine Bedingungsvariable, auf der kein Prozess wartet, wird nicht für ein nachfolgendes wait() gespeichert, sondern es geht verloren. Ein Prozess darf also nicht einfach wait() aufrufen, sondern er muss zuvor (wie oben geschildert) explizit prüfen, ob die Wartebedingung erfüllt ist. Die Gefahr, dass sich zwischen diesem Test und dem wait()-aufruf der Wahrheitswert der Bedingung ändert (vergleiche ), besteht hier nicht, da sich der prüfende Prozess ja in einer Monitoroperation befindet und somit nicht von anderen Prozessen gestört werden kann. Aufgabe 3A Beispiel: Ringpuffer für Erzeuger/Verbraucher Das Beispiel, das näher diskutiert werden soll, ist ein Ringpuffer, wie er beim Erzeuger-Verbraucher-Problem ( ) benötigt wird. Er hat, wie der einfache Puffer in , eine begrenzte Kapazität, definiert seine Schreib- und Leseoperationen aber wie folgt: Die Schreiboperation blockiert bei vollem Puffer, überschreibt also keinen vorhandenen Eintrag. Die Leseoperation liefert den Eintrag, der am längsten im Puffer steht, und entfernt ihn dabei; ist der Puffer leer, so blockiert sie. Der Puffer ist also nach dem FIFO-Prinzip (First In First Out) organisiert. Realisiert werden kann dieser Puffer wiederum durch einen Array, dem hier aber zwei Indizes zugeordnet sind ein Schreibindex und ein Leseindex. Der Schreibindex gibt wieder die Arrayposition an, an der die nächste Schreiboperation stattfindet, der Leseindex entsprechend die Position der nächsten Leseoperation. Ein Index wird bei der entsprechenden Operation um 1 erhöht und bei Erreichen der oberen Arraygrenze auf 0 zurückgesetzt: BILD 3.24 Implementierung eines FIFO-Ringpuffers durch einen Array mit Lese- und Schreibindex Array zur Speicherung der Daten: L Leseindex S Schreibindex Kapazität des Puffers = Arraylänge 1 (siehe Erklärung im Text) Indizes wandern von vorn nach hinten; sie werden auf den Anfang zurückgesetzt, wenn sie das Ende des Arrays erreichen. 1.) Anfangssituation: 2.) Nach dreimaligem Schreiben: L S Puffer leer L S 3.) Nach zweimaligem Lesen: 4.) Nach weiterem dreimaligen Schreiben: L S S L Puffer voll

116 3.4 Synchronisation durch Monitore 115 Wie die Abbildung zeigt, läuft der Leseindex dem Schreibindex zyklisch hinterher. Dabei entspricht die Situation, in der Lese- und Schreibindex gleich sind, einem leeren Puffer und die Situation, in der der Schreibindex eine Position hinter dem Leseindex steht, einem vollen Puffer. Durch diesen Programmiertrick kann man die beiden Fälle Puffer leer und Puffer voll leicht voneinander unterscheiden; die effektive Kapazität des Puffers ist dafür aber um eins kleiner als die Arraylänge. Für den Puffer sind, neben dem wechselseitigen Ausschluss, die beiden weiteren Synchronisationsbedingungen des Erzeuger-Verbraucher-Problems durchzusetzen: Ein Prozess, der in einer Schreiboperation den Puffer voll vorfindet, muss blockiert werden; ein Prozess, der in einer Leseoperation den Puffer leer vorfindet, muss ebenfalls warten. Um den Puffer als Monitor zu programmieren, benötigt man also zwei Bedingungsvariablen voll und leer. Die folgende Abbildung zeigt die entsprechende Klassendefinition für Ringpuffer mit Lese- und Schreiboperationen sowie die Vorgehensweise der Methoden: Monitor Ringpuffer - voll : cond - leer : cond - kapazität : int - inhalt : int[kapazität+1] - schreibindex : int = 0 - leseindex : int = 0 + schreiben(neu : int) + lesen() : int (cond = Bedingungsvariable) schreiben(neu : int): while (schreibindex eine Position hinter leseindex) wait(voll) inhalt[schreibindex] = neu schreibindex++ if (schreibindex > maximaler Arrayindex) schreibindex = 0 signal(leer) lesen(): while (leseindex == schreibindex) wait(leer) rueckgabewert = inhalt[leseindex] leseindex++ if (leseindex > maximaler Arrayindex) leseindex = 0 signal(voll) return rueckgabewert BILD 3.25 Attribute und Methoden eines Ringpuffers mit Leseund Schreiboperationen Man erkennt hier deutlich einerseits die booleschen Ausdrücke, mit denen die Bedingungen explizit geprüft werden, und andererseits die zugeordneten Bedingungsvariablen, auf denen die ausführenden Prozesse blockiert werden. Zudem sieht man, dass die jeweilige Bedingung nach einer Entblockierung durch while neu geprüft wird; unter Umständen würde auch ein if genügen (vergleiche die Diskussion in ). In wird mit PROG 3.4 ein Programm gezeigt, das einen solchen Ringpuffer mit Hilfe von UNIX/Linux-Pthreads und ihren Bedingungsvariablen realisiert Lösung weiterer Standardprobleme Reihenfolgebedingung Eine Reihenfolgebedingung ( ) fordert, dass die Ausführung einer bestimmten Aktion warten muss, bis eine andere Aktion beendet wurde. Eine Klasse für Monitore, mit denen man solche Bedingungen durchsetzen kann, sieht wie folgt aus:

117 116 3 Synchronisation BILD 3.26 Monitorklasse zur Durchsetzung einer Reihenfolgebedingung Monitor Reihenfolge - musswartencond : cond - musswartenbool : boolean = true + signalisierenachfolger() + warteaufvorgaenger() signalisierenachfolger(): musswartenbool = false signal(musswartencond) warteaufvorgaenger(): while (musswartenbool) wait(musswartencond) Zwei Prozesse, die die Vorgänger- bzw. Nachfolgeraktionen ausführen, synchronisieren sich dann mit Hilfe eines gemeinsamen Objekts rf dieser Monitorklasse: Vorgänger: Aktionen des Vorgängers rf.signalisierenachfolger() Nachfolger: rf.warteaufvorgaenger() Aktionen des Nachfolgers Natürlich muss für jede Reihenfolgebedingung ein eigenes solches Objekt erzeugt werden. Soll der Monitor zudem mehrfach benutzt werden und nach jeder Vorgängeraktivität nur jeweils eine Nachfolgeraktivität stattfinden, so muss am Ende von warteaufvorgaenger() die Variable musswartenbool wieder auf true gesetzt werden. BILD 3.27 Monitorklasse zur Kontrolle der Leseund Schreiboperationen beim Leser- Schreiber-Problem Leser-Schreiber-Problem Beim Leser-Schreiber-Problem ( ) sind die Zugriffe von Lesern und Schreibern so zu koordinieren, dass ein Schreibzugriff zu allen anderen Zugriffen wechselseitig ausgeschlossen ist. Dies kann man mit einem Monitor erreichen, der Operationen bereitstellt, die von einem Leser bzw. Schreiber vor und nach seinem Zugriff aufgerufen werden. Dieser Monitor ist also gewissermaßen ein erweiterter Semaphor: Monitor LeserSchreiberSteuerung - warteauflesezugriff : cond - warteaufschreibzugriff : cond - anzleserbeimlesen : int = 0 - anzschreiberda : int = 0 - schreiberschreibt : boolean = false + vordemlesen() + nachdemlesen() + vordemschreiben() + nachdemschreiben() vordemlesen(): while (anzschreiberda>0) wait(warteauflesezugriff) anzleserbeimlesen++ signal(warteauflesezugriff) nachdemlesen(): anzleserbeimlesen-- if (anzleserbeimlesen == 0) signal(warteaufschreibzugriff) vordemschreiben(): anzschreiberda++ while (anzleserbeimlesen>0 or schreiberschreibt) wait(warteaufschreibzugriff) schreiberschreibt = true nachdemschreiben(): anzschreiberda-- schreiberschreibt = false if (anzschreiberda>0) signal(warteaufschreibzugriff) else signal(warteauflesezugriff)

118 3.4 Synchronisation durch Monitore 117 Ein Monitor der hier gezeigten Klasse besitzt fünf Attribute: Auf den Bedingungsvariablen warteauflesezugriff und warteaufschreibzugriff können sich Leser bzw. Schreiber blockieren, um auf ihren Speicherzugriff zu warten. Die Zählvariable anzleserbeimlesen gibt an, wie viele Leser zur Zeit auf dem Speicher aktiv sind. Dabei werden die wartenden Leser nicht mitgezählt. Die Zählvariable anzschreiberda nennt die Zahl der momentan anwesenden Schreiber also die Anzahl der wartenden Schreiber plus gegebenenfalls der eine Schreiber, der zur Zeit seine Schreiboperation ausführt. Die boolesche Variable schreiberschreibt besagt, ob zur Zeit ein Schreiber seine Schreiboperation ausführt. Auf diesen Attributen arbeiten vier Methoden: vordemlesen() wird von einem Leser vor seinem eigentlichen Lesezugriff aufgerufen. Er prüft dort, ob Schreiber anwesend sind, und blockiert sich, wenn das der Fall ist. Da dabei auch Schreiber berücksichtigt werden, die auf ihre Schreiboperation warten, kommt ein Leser nur dann weiter, wenn kein Schreiber auf den Speicher zugreifen will. Die hier präsentierte Lösung bevorzugt also Schreiber vor Lesern im Gegensatz zu den semaphorbasierten Lösungen in Die abschließende Anweisung signal(warteauflesezugriff) stellt sicher, dass ein entblockierter Leser einen weiteren Leser entblockiert, dieser wiederum einen weiteren Leser und so weiter, so dass dann alle bisher wartenden Leser gleichzeitig an den Speicher gelangen. nachdemlesen() wird von einem Leser nach seinem Lesezugriff aufgerufen. Ist kein weiterer Leser mehr in einer Leseoperation aktiv, wird ein wartender Schreiber entblockiert. vordemschreiben() wird von einem Schreiber vor seinem eigentlichen Schreibzugriff aufgerufen. Er prüft dort, ob Leser oder ein anderer Schreiber gerade auf dem Speicher arbeiten, und blockiert sich, wenn das der Fall ist. Ansonsten belegt er den Speicher mit Hilfe der booleschen Variablen schreiberschreibt für sich. nachdemschreiben() wird von einem Schreiber nach seinem Schreibzugriff aufgerufen. Er gibt damit den Speicher wieder frei und entblockiert einen wartenden Schreiber, falls vorhanden, sonst einen Leser Philosophenproblem Zur Lösung des Philosophenproblems ( ) kann man den gemeinsamen Tisch als Monitor implementieren, der die folgenden Attribute und Zugriffsmethoden definiert: Ein Array mit fünf Bedingungsvariablen. Sie bieten jedem der Philosophen die individuelle Möglichkeit sich zu blockieren, wenn nicht beide benötigte Gabeln frei sind.

119 118 3 Synchronisation Ein Array mit fünf Einträgen, der die Zustände der einzelnen Philosophen und damit die Verfügbarkeit der Gabeln angibt. Eine Operation pickup(). Ein Philosoph ruft sie vor dem Essen auf, prüft darin die Verfügbarkeit der Gabeln, blockiert sich gegebenenfalls und nimmt schließlich die Gabeln an sich. Eine Operation putdown(). Ein Philosoph ruft sie nach dem Essen auf, gibt darin die Gabeln zurück und signalisiert gegebenenfalls die Bedingungsvariablen wartender anderer Philosophen. Den detaillierten Programmcode der Lösung kann man beispielsweise in [Silb10] nachlesen. 3.5 Mechanismen in UNIX/Linux Ein spezieller Synchronisationsmechanismus von UNIX/Linux wurde bereits in den Abschnitten und besprochen nämlich die Funktionen wait() und pthread_join(), mit denen man auf die Terminierung eines Prozesses bzw. eines Threads warten kann. Allgemeiner nutzbare Mechanismen in UNIX/Linux sind Signale, Spinlocks und Semaphore. Im Folgenden werden die wichtigsten Schnittstellenfunktionen zur Arbeit mit diesen Synchronisationsmechanismen besprochen. Wie im vorherigen Kapitel werden nur die Parameter- und Rückgabewerte genannt, die am häufigsten auftreten. Eine vollständige Dokumentation findet man online in den Manual Pages von UNIX/Linux [Linux]. Sehr ausführliche Erläuterungen mit Beispielen bieten auch verschiedene Lehrbücher, insbesondere [Stev05], [Robb03] und [Hero04] Signale Aufgaben 3A Signale werden in UNIX/Linux mit der Funktion kill() verschickt: Ein Signal richtet sich an einen oder mehrere Prozesse. Ein einzelner Prozess wird durch seine PID (also seine Prozessnummer, ) angesprochen, die als positive ganze Zahl übergeben wird. Daneben gibt es verschiedene Möglichkeiten, Signale an Gruppen von Prozessen zu schicken (siehe hierzu [Linux]). Die Nummer des Signals wird meist durch eine symbolische Konstante angegeben. Beispielsweise gibt es Konstanten für Signale, die das System zur Anzeige von Fehlern verschickt (wie SIGSEV = Segmentation Violation = fehlerhafter Speicherzuint kill(pid_t pid, int sig) #include <sys/types.h> #include <signal.h> Nummer des Signals Prozess(e), an den/die das Signal geschickt wird 0 bei fehlerfreier Ausführung 1 bei Fehler

120 3.5 Mechanismen in UNIX/Linux 119 griff), und für Signale, die die Ausführung eines anderen Prozesses beenden (SIG- KILL, SIGSTOP; siehe hierzu auch 2.3.2). Anwendungsprogrammierer können die Konstanten SIGUSR1 und SIGUSR2 benutzen, um Signale zwischen Prozessen zu versenden. Das Gegenstück zu kill() ist die Funktion pause(). Mit ihr kann sich ein Prozess blockieren, bis ein Signal eintrifft: int pause(void) #include <unistd.h> 1, falls der Prozess aus dem Aufruf zurückkehrt (also nicht durch das Signal terminiert wurde) Alternativ kann die Funktion sigsuspend() verwendet werden (siehe [Linux]). Die meisten Signale terminieren den empfangenden Prozess im Normalfall sie schießen ihn ab, wie man salopp sagt. Dies kann der Prozess allerdings dadurch verhindern, dass er Signale abfängt. Er muss dazu zuvor einen Signal Handler an ein bestimmtes Signal binden, also eine Funktion benennen, die ausgeführt werden soll, wenn das Signal eintrifft. Dies ist bei allen Signalen möglich außer bei SIGKILL und SIGSTOP, so dass diese beiden Signale die Prozessausführung auf jeden Fall beenden. Signal Handler bindet man mit der Funktion signal() (deren Benutzung allerdings nicht mehr empfohlen wird) und ihrer Verallgemeinerung sigaction(). Die Schnittstelle von sigaction() ist recht komplex und wird daher hier nicht im Einzelnen dokumentiert (siehe bei Bedarf [Linux]). Wie man die Funktion benutzt, wird jedoch aus dem folgenden Programmbeispiel klar, in dem eine Reihenfolgebedingung zwischen Vater- und Sohnprozess durchgesetzt wird: #include <signal.h> #include <stdio.h> #include <stdlib.h> /* Signal Handler */ void sighand() { printf("signal ist eingetroffen\n"); /* Hauptprogramm */ main() { int sohn_pid; /* Bindung des Signal Handlers sighand() an das Signal SIGUSR1 (Details zu sigaction() und struct sigaction siehe [Linux]) */ struct sigaction sigact; sigact.sa_handler = sighand; sigemptyset(&sigact.sa_mask); sigact.sa_flags = 0; sigaction(sigusr1,&sigact,0); if ((sohn_pid=fork())==0) { printf("sohn wartet auf Signal\n"); pause(); PROG 3.1 Reihenfolgebeziehung mit kill() und pause()

121 120 3 Synchronisation printf("sohn terminiert\n"); exit(0); printf("vater tut zunächst etwas anderes\n"); sleep(2); printf("vater schickt Signal an Sohn\n\n"); kill(sohn_pid,sigusr1); Es ist zu beachten, dass Signale nicht gespeichert werden: Wird kill() vor pause() ausgeführt, blockiert der Empfänger möglicherweise endlos. Um auch diesen Fall abzudecken, muss man ähnlich wie in BILD 3.26 vorgehen Lock-Dateien BILD 3.28 Lock-Datei in UNIX/Linux Wie in geschildert, kann man Spinlocks mit Hilfe von Lock-Dateien realisieren. Mit einer Lock-Datei eines bestimmten Namens wird angezeigt, ob ein Prozess in einen kritischen Abschnitt eintreten darf oder warten muss. Dabei interessiert nur, ob die Datei existiert (= kritischer Abschnitt ist gesperrt) oder nicht (= kritischer Abschnitt ist frei); ihr Inhalt ist irrelevant. In UNIX/Linux kann man zur Arbeit mit Lock-Dateien die Funktion open() mit ihren Optionen O_CREAT und O_EXCL nutzen. Sie erzeugt eine Datei mit einem bestimmten Namen, sofern diese noch nicht existiert, und liefert in ihrem Rückgabewert die Information darüber, ob die Datei bereits vorhanden war. Die Funktion geht also wie der test_and_set-befehl ( 3.2.3) vor und ist wie dieser atomar. Die folgende Abbildung zeigt in Anlehnung an BILD 3.10 und BILD 3.12 die konkrete Vorgehensweise: Datei Lock1 erzeugen Gab es Lock1 schon vorher? nein kritischer Abschnitt ja UNIX: open("lock1",o_creat O_EXCL,...) atomare Operation! Datei Lock1 löschen UNIX: unlink("lock1") Da eine Lock-Datei im gemeinsamen UNIX/Linux-Dateisystem angelegt wird, können alle Prozesse darauf zugreifen.

122 3.5 Mechanismen in UNIX/Linux Semaphore UNIX/Linux-Semaphore realisieren die beiden Verallgemeinerungen aus : P- und V-Operationen können Semaphorzähler um beliebige Werte senken bzw. anheben. Dabei gilt bei P-Operationen das Alles-oder-nichts-Prinzip: Ist der Zählerwert nicht hoch genug, um die Anforderung zu erfüllen, bleibt er unverändert, und der Prozess blockiert sich (vergleiche BILD 3.18). Man kann Gruppen von Semaphoren bilden, in denen jeweils P-Operationen auf mehreren Semaphoren gleichzeitig ausgeführt werden können. Auch hier gilt das Alles-oder-nichts-Prinzip: Der ausführende Prozess blockiert nur dann nicht, wenn alle beteiligten einzelnen P-Operationen nicht blockieren; ansonsten bleiben zunächst sämtliche Semaphorzähler unverändert Erzeugen von Semaphorgruppen Semaphore können in UNIX/Linux dynamisch (also durch Operationen von Prozessen) erzeugt und gelöscht werden. Das Betriebssystem stützt sich dabei auf eine Semaphortabelle, die Informationen über alle aktuell vorhandenen Semaphore enthält: globale Semaphortabelle Semaphorgruppe Einzelsemaphore mit ihren Zählern Semaphornummern innerhalb der Gruppe Tabellenindizes = Nummern von Semaphorgruppen Aufgaben 3A BILD 3.29 Semaphortabelle mit Semaphorgruppen Wie die Abbildung zeigt, werden in UNIX/Linux Semaphorgruppen gebildet, die jeweils einen oder mehrere Semaphore enthalten und durch einen Index der Semaphortabelle identifiziert werden. Ein einzelner Semaphor wird über die Indexnummer seiner Gruppe und seine Positionsnummer in der Gruppe angesprochen. Bei den Semaphoren in einer Gruppe handelt es sich um vollwertige Semaphore, die unabhängig von Semaphoren ihrer Gruppe und anderer Gruppen benutzt werden können. Mit der Funktion semget() kann ein Prozess eine neue Semaphorgruppe erzeugen oder sich Zugriff auf eine bereits bestehende Gruppe verschaffen: int semget(key_t key, int nsems, int semflg) Nummer der Semaphorgruppe (= Index in der Sem.tabelle) 1 bei Fehler #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> Flags zur Steuerung der Ausführung (insbes. IPC_CREAT für Erzeugung) und Zugriffsrechte auf die Gruppe Anzahl von Semaphoren in der Gruppe Schlüssel der Semaphorgruppe oder IPC_PRIVATE (key_t ist ein systemabhängiger Ganzzahltyp)

123 122 3 Synchronisation semget() liefert den Index in der Semaphortabelle ( BILD 3.29) zurück, mit dem nachfolgende Operationen auf die Gruppe zugreifen können. Dieser Index wird nur innerhalb von Programmen benutzt. Zusätzlich kann jede Semaphorgruppe einen ganzzahligen Schlüssel besitzen, der auch für Benutzer sichtbar ist (siehe das Beispiel zum ipcs-kommando am Ende dieses Abschnitts). Dieser Schlüssel wird in zwei Fällen als key-parameter an semget() übergeben erstens, wenn eine neu erzeugte Semaphorgruppe einen solchen Schlüssel erhalten soll, und zweitens, wenn ein Programm auf eine Semaphorgruppe zugreifen will, die zuvor durch ein anderes Programm unter Angabe dieses Schlüssels erzeugt wurde. Soll kein Schlüssel benannt werden, übergibt man IPC_PRIVATE. Die beiden folgenden Abbildungen illustrieren diese Alternative. Die erste Abbildung zu semget() zeigt die Erzeugung einer Semaphorgruppe mit mehreren Semaphoren, ohne dass ein Schlüssel vereinbart wird: BILD 3.30 Erzeugung einer neuen Semaphorgruppe semid = semget(ipc_private,3,ipc_creat 0777); prozesslokale Variable int semid 100 globale Semaphortabelle neue Semaphorgruppe Die Semaphorgruppe auf der rechten Seite wurde neu erzeugt (bewirkt durch das Flag IPC_CREAT) und unter dem Index 100 (der bislang nicht belegt war und vom Betriebssystem ausgewählt wurde) in die Semaphortabelle eingetragen. Der Index wurde von semget() zurückgeliefert und in der prozesslokalen Variablen semid abgelegt. Der Prozess, der semget() aufgerufen hatte, und seine Söhne können dann später über semid auf die Semaphore zugreifen insbesondere, um P- und V-Operationen auszuführen ( ). Die zweite Abbildung illustriert, wie ein Prozess auf eine Semaphorgruppe zugreift und dabei einen Schlüssel verwendet, der von einem anderen Prozess an diese Gruppe vergeben wurde: BILD 3.31 Erzeugung einer Semaphorgruppe mit einem Schlüssel und Zugriff darauf Prozess 1: Neuerzeugung einer Gruppe mit dem Schlüssel 4711 semid_1 = semget(4711,3,ipc_creat 0777); prozesslokale Variable int semid_1 100 globale Semaphortabelle 99 Schlüssel der neu erzeugten Gruppe

124 3.5 Mechanismen in UNIX/Linux 123 Prozess 1: prozesslokale Variable int semid_1 100 Prozess 2: Zugriff auf die Gruppe mit dem Schlüssel 4711 semid_2 = semget(4711,3,0777); prozesslokale Variable int semid_2 100 BILD 3.31 (Forts.) Erzeugung einer Semaphorgruppe mit einem Schlüssel und Zugriff darauf globale Semaphortabelle Prozess 1 erzeugt hier wie oben eine Semaphorgruppe und ordnet ihr zusätzlich einen Schlüssel zu. Prozess 2 ruft mit diesem Schlüssel semget() auf, verwendet dabei nicht das Flag IPC_CREAT und erhält somit den Tabellenindex der bestehenden Semaphorgruppe denselben wie zuvor auch Prozess 1. Ab dann können also beide Prozesse auf die Semaphorgruppe zugreifen und sich über sie synchronisieren Initialisieren und Löschen Die Funktion semctl() ermöglicht verschiedene Steuerungsoperationen auf einer Semaphorgruppe oder auf einzelnen Semaphoren innerhalb einer Gruppe: #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid, int semnum, int cmd,...) Rückgabewert abhängig von der ausgeführten Operation 1 bei Fehler Nummer des Semaphors innerhalb seiner Gruppe Nummer der Semaphorgruppe (= Index in der Semaphortabelle) Parameter für die Operation auszuführende Operation (siehe unten) Die Operation, die semctl() ausführen soll, wird durch ihren cmd-parameter bestimmt. Hier kann man symbolische Konstanten übergeben, zu denen insbesondere die folgenden gehören: SETALL zur Initialisierung der Zähler aller Semaphoren in der Gruppe. Als vierter Parameter wird hier ein Array des Typs unsigned short übergeben, der die Anfangswerte der Zähler angibt. Der semnum-parameter wird ignoriert, da hier kein einzelner Semaphor innerhalb der Gruppe angesprochen wird. Geht man beispielsweise vom Szenario in BILD 3.29 aus und führt die Befehle unsigned short init_array[3]; init_array[0] = 4; init_array[1] = 0; init_array[2] = 3; semctl(semid,0,setall,init_array);

125 124 3 Synchronisation BILD 3.32 Initialisierung von Semaphorzählern aus, so ergibt sich das folgende Bild: int semid unsigned short init_array[3] Semaphortabelle Zuweisung Zählerwerte der Semaphore Zur Übergabe der Anfangswerte muss unbedingt ein unsigned-short-array verwendet werden! int-arrays oder Arrays anderer Typen führen zu Fehlern. SETVAL zur Initialisierung des Zählers eines einzelnen Semaphors in der Gruppe. Der vierte Parameter ist hier eine ganze Zahl mit dem gewünschten Anfangswert; der semnum-parameter gibt die Nummer des Semaphors innerhalb der Gruppe an. GETVAL und GETALL zur Abfrage des Zählerwerts eines Semaphors bzw. der Zählerwerte aller Semaphore in der Gruppe. Bei GETVAL wird der betreffende Semaphor durch den semnum-parameter benannt, und die Funktion liefert seinen Zählerstand als Rückgabewert. Bei GETALL wird als vierter Parameter ein unsigned-short-array übergeben, in den die Funktion dann die aktuellen Zählerwerte einträgt. Man sollte sich hier nicht dazu verführen lassen, den Wert eines Semaphors abzufragen und anhand des Resultat zu entscheiden, ob anschließend eine P-Operation ausgeführt wird. Dies würde die Atomarität der P-Operation verletzen, wie in geschildert. IPC_RMID zur Löschung einer Semaphorgruppe. Durch semctl(semid,0,ipc_rmid,0) wird die Gruppe mit der Nummer semid aus dem System entfernt und ihre Position in der Semaphortabelle freigegeben. Werden Semaphorgruppen nicht explizit gelöscht, so bleiben sie über das Ende des erzeugenden Prozesses hinaus bestehen. Vergisst man also die semctl()-löschoperation am Ende eines Programms oder stürzt das Programm vor ihrer Ausführung öfter ab, so sammeln sich immer mehr Semaphore im System an. Ist schließlich eine Obergrenze erreicht, können keine Semaphore mehr erzeugt werden. Spätestens dann muss man die Semaphore durch Benutzerkommandos löschen: Mit ipcs -s stellt man die Nummern der existierenden Semaphorgruppen fest und entfernt sie anschließend mit ipcrm -s nummer. Beispielsweise könnte ipcs -s die folgende Ausgabe liefern: Semaphorenfelder Schlüssel SemID Besitzer Rechte nsems 0x goofy x goofy x goofy 777 1

126 3.5 Mechanismen in UNIX/Linux 125 Hier müsste Benutzer Goofy also drei Semaphorgruppen löschen, was mit ipcrm -s 0 -s s geschieht. Generell sollte man sich angewöhnen, nach jedem Programmabsturz (und auch sonst ab und zu) ipcs auszuführen, um das System sauber zu halten! Übrigens zeigt die ipcs-ausgabe ganz rechts die Anzahl der Semaphore in der Gruppe und neben der Nummer der Gruppe ganz links auch ihren Schlüssel. Die ersten beiden Semaphorgruppen der Beispielausgabe sind offensichtlich per semget(ipc_private,...) erzeugt worden, haben also keinen Schlüssel, die dritte Gruppe mit semget(0x ,...) P- und V-Operationen Zur Ausführung von P- und V-Operationen dient die Funktion semop(): #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid, struct sembuf *sops, unsigned nsops) 0 bei Erfolg 1 bei Fehler Nummer der Semaphorgruppe (= Index in der Sem.tabelle) Anzahl der Operationen auszuführende P- und/oder V-Operationen semop() ist eine recht mächtige Funktion: Mit ihr können P- und V-Operationen auf einem oder mehreren Semaphoren einer Gruppe durchgeführt werden, wobei die Semaphorzähler um beliebige Werte gesenkt oder erhöht werden können. Dabei wird das Alles-oder-nichts-Prinzip angewandt, wie zu Beginn dieses Abschnitts geschildert. Die Operationen werden durch einen Array mit dem Komponententyp struct sembuf (siehe unten) definiert, der über den Parameter sops übergeben wird. Jede Komponente des Arrays beschreibt eine P- oder V-Operation auf einem einzelnen Semaphor der Gruppe. Der Parameter nsops gibt die Länge des Arrays an. Ist nsops größer als 1, so handelt es sich also um mehrere Operationen, die ( alles-oder-nichts ) auf mehreren Semaphoren der Gruppe ausgeführt werden sollen. Ist nsops gleich 1, so ist nur eine Operation auszuführen. Dann kann man statt eines Arrays auch einen Zeiger auf eine einzelne Struktur des angegebenen Typs übergeben. Eine Operation wird, wie gesagt, durch eine Datenstruktur des vorgegebenen Typs struct sembuf beschrieben. Dieser Typ ist wie folgt definiert: struct sembuf { short sem_num; short sem_op; short sem_flg; Die einzelnen Komponenten dieses Strukturtyps haben die folgenden Bedeutungen: sem_num gibt den Semaphor in der Gruppe an, auf dem die P- oder V-Operation durchgeführt werden soll.

127 126 3 Synchronisation BILD 3.33 P-Operation auf einem einzelnen Semaphor sem_op definiert die auszuführende Operation: Ist sem_op>0, so handelt es sich um eine V-Operation, die den Semaphorzähler um den Betrag von sem_op erhöht. Alle Prozesse, die auf dem Semaphor blockiert sind, werden entblockiert und versuchen in einem Wettrennen, den Zähler zu senken und weiterzulaufen ( , BILD 3.18). Prozesse, denen das nicht gelingt, werden wieder blockiert. Ist sem_op<0, so handelt es sich um eine P-Operation, die versuchen soll, den Semaphorzähler um den Betrag von sem_op zu senken. Der ausführende Prozess blockiert, wenn dies nicht möglich ist. Ist sem_op=0, so blockiert der Prozess, falls der Semaphorzähler ungleich 0 ist. Er wird entblockiert, wenn der Zähler zu 0 wird. sem_flg ermöglicht die Übergabe von Flags: Mit IPC_NOWAIT blockiert ein Prozess in einer P-Operation nicht, wenn der Semaphorzähler auf 0 steht, sondern der semop()-aufruf kehrt mit dem Rückgabewert 1 zurück. Man kann somit eine P- Operation auf Probe ausführen. SEM_UNDO bewirkt, dass die ausgeführte Operation rückgängig gemacht wird, wenn der Prozess terminiert. Die erste Abbildung zu semop() illustriert eine einfache P-Operation auf einem einzelnen Semaphor: struct sembuf sem_p; sem_p.sem_num = 0; sem_p.sem_op = -1; sem_p.sem_flg = 0; semop(semid,&sem_p,1); Semaphoroperation vorbereiten durch Wertzuweisungen an eine Struktur des Typs struct sembuf Semaphoroperation durchführen durch Aufruf von semop() (Gruppennummer und Struktur als Parameter) int semid 100 Gruppennummer struct sembuf sem_p 0 sem_num 1 sem_op 0 sem_flg Semaphornummer innerhalb der Gruppe globale Semaphortabelle auszuführende Operation Gruppe mit einem Semaphor (vorheriger Wert: 4) Soll anstelle der P-Operation eine einfache V-Operation ausgeführt werden, muss der sem_op-komponente der Wert +1 zugewiesen werden. Außerdem sollte man die Struktur zur besseren Lesbarkeit in sem_v umbenennen. Soll der Semaphorwert nicht um 1, sondern um n gesenkt bzw. erhöht werden, so weist man der sem_op-komponente der Wert n bzw. +n zu.

128 3.5 Mechanismen in UNIX/Linux 127 Die zweite Abbildung zu semop() illustriert P-Operationen auf mehreren Semaphoren einer Gruppe, die nach dem Alles-oder-nichts-Prinzip ausgeführt werden: struct sembuf sem_p[2]; sem_p[0].sem_num = 1; sem_p[1].sem_num = 2; sem_p[0].sem_op = -1; sem_p[1].sem_op = -1; sem_p[0].sem_flg = 0; sem_p[1].sem_flg = 0; semop(semid,sem_p,2); Anzahl der auszuführenden Operationen BILD 3.34 P-Operationen auf mehreren Semaphoren einer Gruppe int semid 100 struct sembuf sem_p[2] 1 2 sem_num 1 1 sem_op 0 0 sem_flg globale Semaphortabelle Versuch, die Zähler der Semaphore 1 und 2 jeweils um 1 zu senken Keiner der beiden Werte wird geändert, da Semaphor 1 blockiert Man beachte die Unterschiede zwischen den beiden Beispielen: Im ersten Beispiel ist nur eine Operation auszuführen, was durch die 1 im dritten semop()-parameter angezeigt wird. Die Operation wird durch eine Struktur des Typ struct sembuf definiert, die per Referenz (also mit dem Adressoperator &) übergeben wird. Im zweiten Beispiel sollen zwei Operationen ausgeführt werden, so dass der dritte semop()-parameter hier 2 ist. Die Operationen werden nun durch einen Array mit dem Komponententyp struct sembuf festgelegt. Auf welchen Semaphor sich eine Einzeloperation innerhalb des Arrays bezieht, besagt die jeweilige Strukturkomponente sem_num (also nicht der Arrayindex!). Da Arrays in C stets per Referenz übergeben werden (der Arrayname steht in C für einen Zeiger auf den Speicherbereich des Arrays), muss hier der Adressoperator weggelassen werden Programmstrukturen und -beispiele Ein Programm mit mehreren Prozessen, die sich über Semaphore synchronisieren, hat typischerweise den folgenden Aufbau: Der Vaterprozess erzeugt eine oder mehrere Semaphorgruppen durch int semid_x = semget(ipc_private,...,ipc_creat 0700), initialisiert die Semaphore durch semctl(semid_x,0,setall,array_x), startet zwei oder mehr Söhne durch fork(),

129 128 3 Synchronisation PROG 3.2 Reihenfolgebeziehung mit Semaphoroperationen wartet auf deren Terminierung durch wait() oder beendet sie nach einer bestimmten Zeit durch kill() und löscht schließlich die Semaphore durch semctl(semid_x,0,ipc_rmid,0). Die Sohnprozesse erben die Variablen semid_x vom Vater und synchronisieren sich untereinander durch semop(semid_x,...). Auf dieselbe Weise kann sich auch der Vater mit seinen Söhnen, Enkeln usw. synchronisieren, ebenso alle Prozesse, die einen gemeinsamen Vorfahren haben. Das folgende Beispiel zeigt zwei Sohnprozesse, für die eine Reihenfolgebedingung durchgesetzt wird: #include <sys/sem.h> #include <stdlib.h> #include <stdio.h> main() { int semid; /* Nummer der Semaphorgruppe */ unsigned short init_array[1]; /* Anfangswert des Semaphors */ struct sembuf sem_p, sem_v; /* P- und V-Operationen */ int status; /* Rückgabevariable für wait() */ /* Vater: Erzeugung und Initialisierung eines Semaphors */ semid = semget(ipc_private,1,ipc_creat 0777); init_array[0] = 0; semctl(semid,0,setall,init_array); /* Vater: Vorbereitung der P- und V-Operationen */ sem_p.sem_num = 0; sem_v.sem_num = 0; sem_p.sem_op = -1; sem_v.sem_op = 1; sem_p.sem_flg = 0; sem_v.sem_flg = 0; /* Sohn 1: Vorgänger */ if (fork()==0) { Aktionen des Vorgängers semop(semid,&sem_v,1); /* Entblockierung des Nachfolgers */ exit(0); /* Sohn 2: Nachfolger */ if (fork()==0) { semop(semid,&sem_p,1); /* Warten auf den Vorgänger */ Aktionen des Nachfolgers exit(0); /* Vater: Warten auf die Terminierung der Söhne */ wait(&status); wait(&status);

130 3.5 Mechanismen in UNIX/Linux 129 /* Vater: Löschen der Semaphorgruppe */ semctl(semid,0,ipc_rmid,0); Der wechselseitige Ausschluss wird mit denselben Techniken durchgesetzt; die Programmstruktur ergibt sich aus Zwei oder mehr Prozesse, die jeweils ein eigenes Hauptprogramm ausführen, stehen in keiner Verwandtschaftsbeziehung zueinander. Sie können sich also nur über Semaphorgruppen synchronisieren, die einen Schlüssel besitzen. Dazu gehen sie so vor: Der Prozess, der als erster gestartet wird, erzeugt eine oder mehrere Semaphorgruppen mit int semid_x = semget(schlüsselnrx,...,ipc_creat 0777), initialisiert die Semaphore mit semctl(semid_x,0,setall,array_x), synchronisiert sich mit den anderen Prozessen mit semop(semid_x,...) und löscht die Semaphore mit semctl(semid_x,0,ipc_rmid,0), wenn sie nicht mehr benötigt werden. Die anderen Prozesse verschaffen sich Zugriff auf die Semaphorgruppe(n) mit int semid_x = semget(schlüsselnrx,...,0777) und synchronisieren sich untereinander und mit dem erzeugenden Prozess durch semop(semid_x,...). Das folgende Beispiel zeigt eine entsprechende Implementation der Synchronisationsvorgänge in einem Erzeuger-Verbraucher-System, bei der Erzeuger und Verbraucher durch zwei Hauptprogramme in getrennten Dateien realisiert werden (vergleiche auch das Beispiel in ): Datei erzverb.h: Header-Datei für grundlegende Konstanten #include <sys/sem.h> #include <stdio.h> #include <stdlib.h> #define PUFFERKAP 3 /* Kapazität des Puffers */ #define ANZAHL_RUNDEN 10 /* Anzahl der Erzeugungs- bzw. Verbrauchsvorgänge */ #define S_BELEGT 0 /* Symbolische Namen für die Sem.nummern */ #define S_FREI 1 /* innerhalb der Gruppe (vergleiche */ #define S_WA 2 /* Beispiel in und siehe unten) */ Datei verbraucher.c: Programm des Verbrauchers #include "erzverb.h" main() { int i; /* Schleifenzähler */ int semid; /* Nummer der Semaphorgruppe */ PROG 3.3 Erzeuger-Verbraucher-System mit Semaphoren

131 130 3 Synchronisation unsigned short init_array[3]; /* Anfangswerte der Semaphore */ struct sembuf sem_p[2], sem_v[2]; /* P- und V-Operationen */ /* Erzeugung von drei Semaphoren mit dem Schlüssel 4711: Semaphor 0 (= S_BELEGT) zählt die Anzahl belegter Plätze, soll bei leerem Puffer blockieren Semaphor 1 (= S_FREI) zählt die Anzahl freier Plätze, soll bei vollem Puffer blockieren Semaphor 2 (= S_WA) soll den Pufferzugriff wechselseitig ausschließen */ semid = semget(4711,3,ipc_creat 0777); /* Initialisierung der Semaphore */ init_array[s_belegt] = 0; init_array[s_frei] = PUFFERKAP; init_array[s_wa] = 1; semctl(semid,0,setall,init_array); /* Vorbereitung zweier P-Operationen auf den Semaphoren S_BELEGT und S_WA */ sem_p[0].sem_num = S_BELEGT; sem_p[1].sem_num = S_WA; sem_p[0].sem_op = sem_p[1].sem_op = -1; sem_p[0].sem_flg = sem_p[1].sem_flg = 0; /* Vorbereitung zweier V-Operationen auf den Semaphoren S_FREI und S_WA */ sem_v[0].sem_num = S_FREI; sem_v[1].sem_num = S_WA; sem_v[0].sem_op = sem_v[1].sem_op = 1; sem_v[0].sem_flg = sem_v[1].sem_flg = 0; /* Verbrauchsvorgänge in der gewünschten Anzahl */ for (i=0;i<anzahl_runden;i++) { /* Blockierung, wenn der Puffer leer oder durch den anderen Prozess gesperrt ist */ semop(semid,sem_p,2);... Daten aus Puffer lesen... /* Inkrementierung der Anzahl der freien Plätze und Freigabe des Puffers */ semop(semid,sem_v,2);... Daten verbrauchen... /* Löschung der Semaphore */ semctl(semid,0,ipc_rmid,0);

132 3.5 Mechanismen in UNIX/Linux 131 Datei erzeuger.c: Programm des Erzeugers #include "erzverb.h" main() { int i; /* Schleifenzähler */ int semid; /* Nummer der Semaphorgruppe */ struct sembuf sem_p[2], sem_v[2]; /* P- und V-Operationen */ /* Zugriff auf die drei Semaphore mit dem Schlüssel 4711, die vom Verbraucher generiert wurden (siehe Kommentar dort) */ semid = semget(4711,3,0777); if (semid==-1) { printf("fehler: Semaphorgruppe existiert nicht!"); exit(-1); /* Vorbereitung zweier P-Operationen auf den Semaphoren S_FREI und S_WA */ sem_p[0].sem_num = S_FREI; sem_p[1].sem_num = S_WA; sem_p[0].sem_op = sem_p[1].sem_op = -1; sem_p[0].sem_flg = sem_p[1].sem_flg = 0; /* Vorbereitung zweier V-Operationen auf den Semaphoren S_BELEGT und S_WA */ sem_v[0].sem_num = S_BELEGT; sem_v[1].sem_num = S_WA; sem_v[0].sem_op = sem_v[1].sem_op = 1; sem_v[0].sem_flg = sem_v[1].sem_flg = 0; /* Erzeugungsvorgänge in der gewünschten Anzahl */ for (i=0;i<anzahl_runden;i++) {... Daten erzeugen... /* Blockierung, wenn der Puffer voll oder durch den anderen Prozess gesperrt ist */ semop(semid,sem_p,2);... Daten schreiben... /* Inkrementierung der Anzahl der belegten Plätze und Freigabe des Puffers */ semop(semid,sem_v,2); Man beachte, dass zuerst der Verbraucher gestartet werden muss, da er die Semaphorgruppe erzeugt. Startet man den Erzeuger zuerst, so liefert sein semget()-aufruf den Wert 1 zurück, und er beendet sich mit einer Fehlermeldung. Statt einer Semaphorgruppe mit drei Semaphoren könnten auch drei Gruppen mit je einem Semaphor benutzt werden. Es müssten dann aber jeweils zwei einzelne P- bzw. V-Operationen hintereinander ausgeführt werden (siehe ).

133 132 3 Synchronisation Mutexe mit Bedingungsvariablen Aufgabe 3A Das Pthreads-Package von UNIX/Linux ( ) stellt Typen und Funktionen zur Verfügung, mit denen sich Threads durch Mutexe ( ) und Bedingungsvariablen ( 3.4.2) synchronisieren können Mutexe Ein Mutex für Pthreads wird durch eine Variable des Typs pthread_mutex_t repräsentiert. Mit der Funktion pthread_mutex_lock() belegt man einen Mutex, mit pthread_mutex_unlock() gibt man ihn wieder frei. Die Programmstruktur sieht hier wie folgt aus (mit vorherigem #include <pthread.h>): Zu Beginn des Programms wird die benötigte Mutex-Variable deklariert und initialisiert: pthread_mutex_t mymutex; pthread_mutex_init(&mymutex,null); In den Threads werden die jeweiligen kritischen Abschnitte mit Operationen auf diesem Mutex geklammert: pthread_mutex_lock(&mymutex); kritischer Abschnitt pthread_mutex_unlock(&mymutex); Wird die Mutex-Variable nicht mehr benötigt, kann sie wieder gelöscht werden: pthread_mutex_destroy(&mymutex); Beziehen sich die kritischen Abschnitte auf verschiedene Synchronisationsbedingungen, müssen natürlich mehrere Mutex-Variable deklariert werden (siehe letztes Beispiel in ) Bedingungsvariablen Im Zusammenhang mit Mutexen können Bedingungsvariablen eingesetzt werden, um komplexere Bedingungen als den wechselseitigen Ausschluss durchzusetzen. Sie werden mit dem Typ pthread_cond_t deklariert; die zugehörigen Operationen sind pthread_cond_wait() und pthread_cond_signal() (entsprechend den Operationen wait und signal in 3.4.2): #include <pthread.h> int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 0 bei Erfolg Fehlernummer bei Fehler Mutex, dem die Bedingungsvariable zugeordnet wird Bedingungsvariable, auf der gewartet werden soll int pthread_cond_signal(pthread_cond_t *cond) 0 bei Erfolg Fehlernummer bei Fehler Bedingungsvariable, die signalisiert werden soll

134 3.5 Mechanismen in UNIX/Linux 133 Die Funktion pthread_cond_wait() blockiert den aufrufenden Thread auf der Bedingungsvariablen, die im ersten Parameter angegeben ist. Zusätzlich wird der Funktion eine Referenz auf einen Mutex übergeben, dem die Bedingungsvariable damit zugeordnet wird (was für die nachfolgende Entblockierung eine Rolle spielt). Dabei wird verlangt, dass der aufrufende Thread den Mutex zuvor per pthread_mutex_lock() für sich belegt hat. pthread_cond_wait() gibt diesen Mutex wieder frei. Die Funktion pthread_cond_signal() entblockiert mindestens einen der Threads, die sich auf der im Parameter genannten Bedingungsvariablen blockiert haben (sofern ein solcher Thread vorhanden ist). Der oder die entblockierte(n) Thread(s) versuchen dann, den zugehörigen Mutex (siehe pthread_cond_wait()) wieder für sich zu belegen. Der Thread, dem dies gelingt, läuft weiter; die anderen blockieren sich wieder auf dem Mutex. Zudem gibt es die Funktion pthread_cond_broadcast(). Sie entblockiert alle Threads, die sich auf der im Parameter genannten Bedingungsvariablen blockiert haben. Auch hier liefern sich diese Threads ein Wettrennen um die Wiederbelegung des Mutex. Bedingungsvariablen müssen, wie Mutexe, initialisiert werden und können nach Gebrauch wieder gelöscht werden. Dazu dienen die Funktionen pthread_cond_init() bzw. pthread_cond_destroy() (siehe PROG 3.4 in ) Beispiel: Erzeuger-Verbraucher mit Ringpuffer Das folgende Programm zeigt, wie das Erzeuger-Verbraucher-Problem mit Hilfe von Pthreads-Bedingungsvariablen gelöst werden kann. Es implementiert einen Ringpuffer wie in besprochen: #include <pthread.h> #define PUFFERKAP... /* Kapazität des Ringpuffers */ #define ERZZEIT... /* Dauer des Erzeugens (Sek.) */ #define VBRZEIT... /* Dauer des Verbrauchens (Sek.) */ #define GESAMTLAUFZEIT... /* Gesamtlaufzeit des Programms (Sek.) */ pthread_mutex_t mutex; /* Mutex für den wechselseitigen Ausschluss der Pufferzugriffe */ pthread_cond_t cond_voll, cond_leer; /* Bedingungsvariablen zur Blockade bei vollem bzw. leerem Puffer */ int puffer[pufferkap+1], lese_index=0, schreib_index=0; /* Puffer mit Lese- und Schreibindex */ int hilf; /* Hilfsvariable zur Speicherung des gelesenen Werts */ /* Funktion zur zyklischen Erhöhung eines Indexwerts */ void incr_index(int *index) { *index = (*index+1)%(pufferkap+1); /* Die Modulo-Operation % setzt den Index auf 0 zurück, wenn er das Pufferende überschritten hat. */ PROG 3.4 Erzeuger- Verbraucher-System mit Pthreads- Bedingungsvariablen

135 134 3 Synchronisation /* Funktionen zur Prüfung der Wartebedingungen */ int puffer_voll() { return ((schreib_index+1)%(pufferkap+1))==lese_index; int puffer_leer() { return schreib_index==lese_index; /* Funktion, die der Erzeuger-Thread ausführt: Der Erzeuger schreibt die Folge 10, 20,... in den Puffer. */ void *erzeuger_loop(void *p) { int wert = 10; while (1) { wert += 10; /* Erzeugung des nächsten zu schreibenden Werts */ sleep(erzzeit); /* Verzögerung um die gewünschte Zeit */ pthread_mutex_lock(&mutex); /* Belegung des Puffers */ while (puffer_voll()) /* Prüfung Wartebedingung und... */ pthread_cond_wait(&cond_voll,&mutex); /*... ggf. Blockade */ puffer[schreib_index] = wert; /* Schreiben des Werts */ incr_index(&schreib_index); /* Erhöhung Schreibindex */ pthread_cond_signal(&cond_leer); /* Signalis. Verbraucher */ pthread_mutex_unlock(&mutex); /* Freigabe des Puffers */ /* Funktion, die der Verbraucher-Thread ausführt: Der Verbraucher liest Werte aus dem Puffer und gibt sie aus. */ void *verbraucher_loop(void *p) { while (1) { pthread_mutex_lock(&mutex); /* Belegung des Puffers */ while (puffer_leer()) /* Prüfung Wartebedingung und... */ pthread_cond_wait(&cond_leer,&mutex); /*... ggf. Blockade */ hilf = puffer[lese_index]; /* Lesen des Werts */ incr_index(&lese_index); /* Erhöhung des Leseindex */ pthread_cond_signal(&cond_voll); /* Signalisierung Erzeuger */ pthread_mutex_unlock(&mutex); /* Freigabe des Puffers */ printf(" gelesen: %d\n",hilf); /* Ausgabe des Werts */ sleep(vbrzeit); /* Verzögerung um die gewünschte Zeit */ /* Hauptprogramm zum Starten und Beenden des Ablaufs */ int main(int argc, char *argv[]) { pthread_t erzeuger, verbraucher; /* Erz.-/Verbr.-Threads */ pthread_mutex_t mutex; /* Mutex für w.a. */ /* Initialisierung des Mutex und der Bedingungsvariablen */ pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond_voll, NULL); pthread_cond_init(&cond_leer, NULL); /* Start der Threads */ pthread_create(&erzeuger, NULL, erzeuger_loop, NULL); pthread_create(&verbraucher, NULL, verbraucher_loop, NULL);

136 3.6 Mechanismen in Java 135 /* Warten für die gewünschte Laufzeit */ sleep(gesamtlaufzeit); /* Stoppen der Threads, Löschen von Mutex und Bedingungsvariablen */ pthread_cancel(erzeuger); pthread_cancel(verbraucher); pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond_voll); pthread_cond_destroy(&cond_leer); 3.6 Mechanismen in Java In wurde bereits ein einfacher Java-Synchronisationsmechanismus diskutiert nämlich die Methode join(), mit der ein Thread auf die Terminierung eines anderen Threads wartet. Zudem lassen sich Java-Threads wie folgt synchronisieren: Erstens kann man die Methoden einer Klasse als synchronized kennzeichnen und damit ihre Ausführung, wie bei einem Monitor, wechselseitig ausschließen. Mit speziellen Methoden werden hier zusätzlich Reihenfolgebedingungen durchgesetzt. Zweitens stellt das Paket java.util.concurrent eine Reihe von Klassen zur Verfügung, durch die atomare Operationen, Semaphore und Monitore mit Bedingungsvariablen realisiert werden. Die beiden Ansätze werden im Folgenden nicht einer nach dem anderen beschrieben, sondern der Text geht wieder von den einfachen hin zu den mächtigeren Mechanismen vor, woraus sich eine gewisse Durchmischung ergibt. Eine vertiefte Darstellung findet man in der Java-Online-Spezifikation [JavaSpec] und den zugehörigen Tutorials [JavaTutConc] sowie in der gedruckten Spezialliteratur sehr ausführlich und eingängig in [Oech11] Atomare Operationen Basistypen Das Paket java.util.concurrent.atomic definiert Wrapper-Klassen für einige grundlegende Datentypen. Zu diesen Typen gehören boolean, int, long und Referenzen auf Objekte beliebiger Klassen sowie Arrays mit Einträgen dieser Typen (außer boolean). Die Wrapper-Klassen heißen entsprechend AtomicBoolean, Atomic- Integer und so weiter. Objekte der Wrapper-Klassen kapseln Werte der zugrundeliegenden Typen. Auf diese Werte kann also nicht unmittelbar zugegriffen werden, sondern nur über Methoden der Objekte. Die Methoden sind thread-safe implementiert also so, dass mehrere Threads, die ein Objekt gemeinsam benutzen, sich dabei nicht gegenseitig stören. Das wird dadurch erreicht, dass die Java Virtual Machine eine Methode atomar (also ohne Unterbrechung) ausführt entweder durch Benutzung entsprechender Maschinenbefehle ( 3.2.3) oder durch Einsatz von Mutexen ( ). Aufgabe 3A.4.7.

137 136 3 Synchronisation Jede Wrapper-Klasse definiert eine get()- und eine set()-methode, mit denen man gespeicherte Werte lesen bzw. ändern kann. Dazu kommen typspezifische Methoden, wie beispielsweise die Methode compareandset() in der Klasse AtomicBoolean, die ähnlich wie der in beschriebene Maschinenbefehl test_and_set() eingesetzt werden kann. Die numerischen Wrapper-Klassen bieten Methoden, mit denen sich Zahlenwerte atomar erhöhen oder senken lassen beispielsweise increment- AndGet() und decrementandget() Collections Dynamische Datenstrukturen (also Datensammlungen, die wachsen und schrumpfen können) werden in Java durch Unterklassen des Interface Collection realisiert. Nicht alle dieser Unterklassen sind thread-safe es ist also nicht sichergestellt, dass die Operationen mehrerer Threads auf einer Collection wechselseitig ausgeschlossen ablaufen. Was geschieht, wenn mehrere Threads gleichzeitig auf eine Collection zugreifen, ist damit nicht definiert. Jedoch bietet die Klasse java.util.collections Methoden, die zu einem Collection-Objekt ein entsprechendes zweites Objekt mit der Thread-Safe-Eigenschaft liefern. Die Methoden heißen synchronizedcollection(), synchronizedlist(), synchronizedmap() und so weiter Semaphore Aufgabe 3A Die Klasse Semaphore Über die Klasse java.util.concurrent.semaphore lassen sich einzelne Semaphore erzeugen (jedoch keine Semaphorgruppen wie in UNIX/Linux). In den P- und V- Operationen können die Zähler um beliebige Werte gesenkt bzw. erhöht werden: class Semaphore { Semaphore(int permits) Semaphore(int permits, boolean fair) int availablepermits(int permits) void acquire() void acquire(int permits) boolean tryacquire() boolean tryacquire(int permits) boolean tryacquire(int permits, long timeout,...) void release() void release(int permits) int drainpermits() void reducepermits(int reduction) boolean hasqueuedthreads() int getqueuelength() Collection<Thread> getqueuedthreads()...

138 3.6 Mechanismen in Java 137 Der aktuelle Zählerstand eines Java-Semaphor-Objekts wird als die Anzahl der Permits bezeichnet, die der Semaphor zur Zeit bereithält entsprechend dem Bild einer Schale mit Marken in Der anfängliche Zählerstand wird dem Konstruktor als Parameter übergeben, wenn das Semaphorobjekt erzeugt wird. Zusätzlich kann man beim Konstruktoraufruf festlegen, ob der Semaphor fair sein soll, also seine Thread-Warteschlange nach dem First-In-First-Out-Prinzip verwaltet. Der P-Operation entspricht die Methode acquire(), die mit einem int-parameter (= Wert, um den der Zähler gesenkt werden soll) oder auch ohne Parameter (= Senken des Zählers um 1) aufgerufen werden kann. Der V-Operation entspricht die Methode release(), die man analog zu acquire() mit oder ohne Parameter nutzen kann. Mit tryacquire() kann ein Thread eine P-Operation auf Probe ausführen ist der Semaphorzähler gleich 0, so blockiert der Thread nicht, sondern kehrt aus der Methode mit dem Rückgabewert false zurück. Der Thread kann hier zunächst eine bestimmte Zeit darauf warten, von einem anderen Thread entblockiert zu werden. Zusätzlich gibt es eine Version der P-Operation, in der ein wartender Thread nicht per interrupt() unterbrochen werden kann (oben nicht dargestellt). Neben diesen grundlegenden Semaphoroperationen stellt die Klasse eine Reihe weiterer Methoden bereit. Mit ihnen kann die Anzahl der verfügbaren Permits um einen bestimmten Wert gesenkt (reducepermits()) oder auf 0 gebracht (drainpermits()) werden. Zudem kann geprüft werden, ob Threads warten (hasqueued- Threads()), und ihre Anzahl (getqueuelength()) sowie Referenzen auf sie (get- QueuedThreads()) abgefragt werden. Die drei letztgenannten Methoden sind jedoch nicht thread-safe ihr Ergebnis ist möglicherweise nicht korrekt, wenn sich der Systemzustand während ihrer Ausführung ändert Beispiel: Reihenfolgebeziehung Das folgende Beispiel zeigt ein Java-Programm, in dem eine Reihenfolgebedingung für zwei Threads mit Hilfe eines Semaphors durchgesetzt wird. Der Semaphor wird im Hauptprogramm erzeugt und den neuen Threads bei ihren Konstruktoraufrufen als Parameter übergeben: import java.util.concurrent.*; class Vorgaenger extends Thread { private Semaphore sem; // Semaphor, über den sich der Thread // mit dem Nachfolger synchronisiert. Vorgaenger(Semaphore sem) { this.sem = sem; public void run() { Aktionen des Vorgängers sem.release(); // Entblockierung des Nachfolgers class Nachfolger extends Thread { private Semaphore sem; // Semaphor, über den sich der Thread // mit dem Vorgänger synchronisiert. PROG 3.5 Reihenfolgebeziehung mit Java- Semaphor

139 138 3 Synchronisation Nachfolger(Semaphore sem) { this.sem = sem; public void run() { try { sem.acquire(); // Warten auf den Vorgänger catch (InterruptedException e) { Aktionen des Nachfolgers // Hauptprogramm class ThreadReihenfolgeMitSemaphor { public static void main(string[] args) { // Erzeugung des Semaphor-Objekts, // mit dem die Reihenfolgebedingung durchgesetzt wird Semaphore sem = new Semaphore(0); // Erzeugung und Start zweier Threads, // die sich über den Semaphor synchronisieren Vorgaenger t1 = new Vorgaenger(sem); Nachfolger t2 = new Nachfolger(sem); t1.start(); t2.start(); Monitore In Java kann man jede selbstgeschriebene Klasse zu einer Monitorklasse machen, indem man ihre Methoden als synchronized kennzeichnet. Zusätzlich gibt es hier die Methoden wait() und notify()/notifyall((), mit denen Reihenfolgebedingungen durchgesetzt werden können, sowie die Interfaces Lock und Condition zur Realisierung von Bedingungsvariablen. Aufgaben 3A synchronized Schreibt man eine Java-Klasse, so kann man ihren Methodendefinitionen das Schlüsselwort synchronized voranstellen. Im einfachsten Fall kennzeichnet man sämtliche nichtstatischen (also objektbezogenen) Methoden auf diese Weise und macht damit Objekte dieser Klasse zu Monitoren, auf denen jeweils höchstens ein Thread gleichzeitig arbeiten kann (vergleiche die Monitordefinition in ). Es ist jedoch auch möglich, nur einige Methoden als synchronized zu markieren, und man kann dies auch mit statischen (also klassenbezogenen) Methoden tun. Dies bewirkt Folgendes: Auf einem Objekt kann höchstens ein Thread gleichzeitig in einer der nichtstatischen Methoden aktiv sein, die mit synchronized gekennzeichnet sind. Die übri-

140 3.6 Mechanismen in Java 139 gen Methoden sind jedoch unbeschränkt nebenläufig dazu und zueinander ausführbar. In einer Klasse kann höchstens ein Thread gleichzeitig in einer der statischen Methoden aktiv sein, die mit synchronized gekennzeichnet sind. Die übrigen Methoden sind jedoch unbeschränkt nebenläufig dazu und zueinander ausführbar. Insbesondere können eine statische und ein nichtstatische synchronized-methode gleichzeitig ablaufen. Zur Durchsetzung dieser wechselseitigen Ausschlüsse besitzt jedes Objekt und jede Klasse einen Mutex ( , auch Lock genannt). Bei Aufruf einer nichtstatischen synchronized-methode wird das Objekt mit Hilfe des Mutex belegt (sofern es noch frei ist, sonst blockiert der Thread) und bei Rückkehr aus der Methode wieder freigegeben. Dies geschieht jedoch nicht, wenn der ausführende Thread das Objekt bereits für sich belegt hat. Er kann also aus einer synchronized-methode eine weitere solche Methode aufrufen, ohne dadurch blockiert zu werden. Statische synchronized-methoden benutzen einen gesonderten Mutex, der der Klasse zugeordnet ist. Alle nicht gekennzeichneten Methoden führen keine Mutex-Operationen aus. Das folgende Beispiel zeigt eine Implementation des einfachen Ringpuffers aus : class EinfacherRingpuffer { private int inhalt[]; // Werte, die im Puffer gespeichert sind private int schreibindex; // Index für die nächste Schreiboperation // Konstruktor EinfacherRingpuffer(int kap) { inhalt = new int[kap]; schreibindex = 0; // Schreiben eines neuen Werts in den Puffer synchronized void schreiben(int wert) { inhalt[schreibindex] = wert; schreibindex = (schreibindex+1)%inhalt.length; // Ausgabe des aktuellen Pufferinhalts auf den Bildschirm, // beginnend mit dem zuletzt geschriebenen Wert synchronized void ausgeben() { for (int i=schreibindex-1;i>=0;i--) System.out.print(" "+inhalt[i]); for (int i=inhalt.length-1;i>=schreibindex;i--) System.out.print(" "+inhalt[i]); PROG 3.6 Einfacher Ringpuffer als Java-Monitor Neben kompletten Methoden lassen sich auch Teile von Methoden als synchronized kennzeichnen. Ein solcher synchronized-block sieht so aus: synchronized(object) { code

141 140 3 Synchronisation Ein synchronized-block bezieht sich also stets auf ein Objekt object. Vor Beginn der code-ausführung wird das Objekt belegt und nach der Ausführung wieder freigegeben. Ist es bereits belegt, so wartet der Thread auf seine Freigabe. Verschiedene synchronized-blöcke, die in ihrem Kopf dasselbe Objekt benennen, werden also wechselseitig ausgeschlossen ausgeführt. Befinden sich die Blöcke alle in Methoden desselben Objekts, so kann man nach dem folgenden Schema vorgehen: class XYZ { methode1() { methode2() { synchronized(this) { synchronized(this) { kritischer Abschnitt kritischer Abschnitt this bezieht sich dabei auf das Objekt, zu dem die Methoden gehören. Aufgaben 3A.4.5./ wait() und notify() Reihenfolgebedingungen lassen sich mit den Methoden wait() und notify() oder notifyall() durchsetzen. Sie dürfen nur innerhalb einer synchronized-methode eines Monitors Obj (oder eines synchronized-blocks, der sich auf ein Objekt Obj bezieht) aufgerufen werden also nur durch den Thread, der Obj zur Zeit belegt hat: wait() blockiert den aufrufenden Thread auf Obj. Der Thread gibt Obj damit frei. Ein Thread, der in einem wait()-aufruf blockiert ist, kann von außen durch interrupt() unterbrochen werden, wobei eine InterruptedException ausgelöst wird ( ). Der wait()-aufruf muss daher in einen entsprechenden trycatch-block eingebettet werden. Bezüglich der Behandlung des Interrupt-Flags (und der Schwierigkeiten, die dabei auftreten können) gilt dasselbe, was am Ende von bezüglich sleep() gesagt wurde. notify() entblockiert einen Thread, der sich auf Obj blockiert hat (sofern vorhanden). Wenn es mehrere solche Threads gibt, ist nicht vorherbestimmt, welcher von ihnen geweckt wird. Der entblockierte Thread übernimmt den Monitor nicht sofort von seinem Befreier, sondern er muss möglicherweise mit anderen Threads darum konkurrieren. Es kann also durchaus vorkommen, dass ein anderer Thread, der an der Reihenfolgebeziehung unbeteiligt ist, dem entblockierten Thread den Monitor vor der Nase wegschnappt. Der entblockierte Thread muss also erneut prüfen, ob er tatsächlich weiterlaufen kann, und sich gegebenenfalls wieder blockieren (siehe die while- Schleifen im Beispiel unten und auch im Beispiel in ). notifyall() entblockiert alle Threads, die sich auf Obj blockiert haben. Man benutzt notifyall() anstelle von notify(), wenn mehrere Threads weiterlaufen können, sobald eine bestimmte Bedingung eingetreten ist. Mit dieser Me-

142 3.6 Mechanismen in Java 141 thode lassen sich also Events ( 3.2.4) realisieren. Man benutzt sie auch, wenn zwar nur ein Thread auf das Eintreten dieser speziellen Bedingung wartet, sich aber mehrere Threads auf Obj blockiert haben. Da man den betreffenden Thread nicht gezielt wecken kann, weckt man alle und überlässt es ihnen dann selbst festzustellen, ob sie tatsächlich weiterlaufen können (siehe while-schleife in PROG 3.7 unten). wait() und notify()/notifyall() lassen sich offensichtlich flexibler einsetzen als join() ( ): Mit join() kann nur auf die Terminierung eines Threads gewartet werden, nicht jedoch darauf, dass eine bestimmte (aber beliebige) Stelle im Programm erreicht wird. Das folgende Programmbeispiel zeigt die Realisierung eines Erzeuger-Verbraucher-Systems mit unbegrenztem Zwischenpuffer. Der Speicher, in den beliebig viele Werte geschrieben werden können, wird durch ein Objekt der Klasse LinkedList realisiert. Da diese Klasse nicht thread-safe ist und hier zudem die Synchronisationsbedingung Verbraucher blockiert bei leerem Puffer durchgesetzt werden muss, wird der Speicher in einen Monitor eingebettet: import java.util.*; class Puffer { private LinkedList<Integer> speicher = new LinkedList<Integer>(); // Schreiben eines neuen Werts in den Puffer synchronized void schreiben(int wert) { speicher.add(wert); // Benachrichtigung eines wartenden Threads (siehe lesen()) notify(); // Lesen eines Werts aus dem Puffer synchronized int lesen() { // Blockierung, solange der Puffer leer ist while (speicher.size()==0) try { wait(); catch (InterruptedException e) {... return speicher.removefirst(); PROG 3.7 Unbegrenzter Puffer als Java-Monitor mit wait()/notify() Statt dieser selbstgeschriebenen Klasse kann man auch Klasse LinkedBlocking- Queue benutzen, die im Paket java.util.concurrent definiert ist. Für ihre Objekte können jeweils Kapazitätsgrenzen festgelegt werden, so dass man mit ihnen Erzeuger-Verbraucher-Systeme im Sinne von implementieren kann. Lösungen für das Leser-Schreiber-Problem und das Philosophenproblem, die auf Monitoren mit wait() und notify() beruhen, findet man zum Beispiel in [Oech11].

143 142 3 Synchronisation Aufgabe 3A Die Interfaces Lock und Condition Eine Alternative zur Synchronisation mit synchronized, wait() und notify() bietet das Paket java.util.concurrent.locks. Dieses Paket enthält Schnittstellen und Klassen zur Realisierung von Monitoren mit Bedingungsvariablen insbesondere die Interfaces Lock und Condition. Das Interface java.util.concurrent.locks.lock ist wie folgt definiert: interface Lock { void lock() void lockinterruptibly() boolean trylock() boolean trylock(long time, TimeUnit unit) void unlock() Condition newcondition() Locks setzen einen wechselseitigen Ausschluss wie Mutexe ( ) durch: Höchstens ein Thread gleichzeitig darf ein Lock-Objekt belegen; er belegt es, indem er dessen Methode lock() aufruft. Ist das Objekt bei einem lock()-aufruf bereits belegt, so wird der Thread blockiert. Verschiedene Varianten der Methode erlauben, einen Thread während des Wartens zu unterbrechen (lockinterruptibly()), eine Belegung zu versuchen, ohne dabei zu blockieren (trylock()), oder nur eine begrenzte Zeit auf die Belegungsmöglichkeit zu warten (trylock(time,...)). unlock() hebt eine Belegung wieder auf und gibt damit den Zugang für andere Threads frei. unlock()-aufrufe sollten stets in eine finally-klausel eingebettet werden um sicherzustellen, dass eine Belegung immer (also auch, wenn ein Ausnahmeereignis eintritt) aufgehoben wird: Lock mylock =...; mylock.lock(); try { kritischer Abschnitt finally { mylock.unlock(); Offensichtlich kann man mit Lock denselben Effekt wie mit synchronized erzielen, indem man nämlich Methodenkörper von Monitorobjekten durch lock() und unlock() klammert. Man kann die Operationen aber auch anders platzieren (beispielsweise in zwei verschiedene Objektmethoden), so dass die Programmierung hier flexibler, aber auch fehleranfälliger ist. Da Lock lediglich eine Schnittstelle ist, legt der Java-Standard [JavaSpec] das Verhalten der Methoden nicht bis ins Letzte fest, sondern gibt an einigen Stellen nur Empfehlungen für die Implementierung. Die Klasse ReentrantLock implementiert die

144 3.6 Mechanismen in Java 143 Schnittstelle. Auf ihrer Grundlage kann man dann Lock-Objekte mit einem definierten Verhalten (siehe [JavaSpec]) erzeugen und nutzen. Neben Lock gibt es die Schnittstelle ReadWriteLock und die zugehörige Klasse ReentrantReadWriteLock. Sie bieten Paare von Locks eins für Lese- und eins für Schreibzugriffe und lösen damit das Leser-Schreiber-Problem ( ). Eng mit Lock verbunden ist das Interface java.util.concurrent.locks.condition, mit dem sich Bedingungsvariablen ( 3.4.2) realisieren lassen: interface Condition { void await() void await(long time, TimeUnit unit)... void signal() void signalall() Ein Condition-Objekt wird erzeugt, indem man auf einem Lock-Objekt die Methode newcondition() aufruft. Das Condition-Objekt ist damit dem Lock-Objekt zugeordnet und realisiert für dieses eine Bedingungsvariable. Ein Thread darf die Condition-Methoden await(), signal() und signalall() nur dann ausführen, wenn er das zugehörige Lock-Objekt für sich belegt hat. Die Methoden arbeiten analog zu wait(), notify() bzw. notifyall(), blockieren also Threads und geben sie wieder frei. Der Unterschied besteht darin, dass sich hier ein Thread auf einem bestimmten Condition-Objekt blockiert. Eine Freigabe betrifft also nicht sämtliche blockierten Threads, sondern nur die, die auf dem Condition- Objekt warten. signal() weckt also Threads gezielter als notify(). Ein Programmbeispiel, nämlich die Implementation eines beschränkten Puffers für das Erzeuger-Verbraucher-Problem, findet man in der Java-Online-Spezifikation [JavaSpec] des Interface Condition. Dort kann man auch zahlreiche Details nachlesen, die hier aus Platzgründen fehlen Weitere Mechanismen Das Paket java.util.concurrent bietet zahlreiche weitere Klassen und Schnittstellen zum synchronisierten Zugriff auf Daten. Dazu gehören die folgenden: Mit dem Interface BlockingQueue und zugehörigen Klassen kann man Thread-sichere, FIFO(First In First Out)-organisierte Datenstrukturen erzeugen. Eine dieser Klassen ist LinkedBlockingQueue, wie in angesprochen. Mit den Klassen CyclicBarrier und CountDownLatch lassen sich Barrieren implementieren besser gesagt Treffpunkte, an denen jeweils eine vorgegebene Anzahl von Threads aufeinander warten muss. Ihre await()-methode blockiert

145 144 3 Synchronisation aufrufende Threads, bis die gewünschte Anzahl erreicht ist. Dann dürfen alle blockierten Threads weiterlaufen. Der Unterschied zwischen den beiden Klassen liegt im Verhalten nach dem Entblockieren: Bei einer CyclicBarrier beginnt das Wartespiel von neuem; nachfolgende await()-aufrufe blockieren also zunächst wieder. Ein CountDown- Latch bleibt nach der Entblockierung offen. Die Klasse Exchanger ermöglicht zwei Threads, sich nach Art eines Rendezvous zu treffen und dabei zwei Werte untereinander auszutauschen. 3.7 Zusammenfassung und Ausblick Kapitel 3 befasste sich mit der Aufgabe, nebenläufige Aktivitäten zeitlich aufeinander abzustimmen: Die zeitliche Abstimmung nebenläufiger Aktivitäten wird als Synchronisation bezeichnet. Synchronisationsbedingungen sind dabei Anforderungen, die an die zeitliche Abfolge der Ausführungen gestellt werden. Synchronisationsmechanismen setzen diese Bedingungen durch. Elementare Synchronisationsbedingungen sind der wechselseitige Ausschluss (w.a.) zum Schutz kritischer Abschnitte vor gegenseitigen Störungen und die Reihenfolgebedingung als Grundlage der Zusammenarbeit. Man findet sie in reiner oder auch abgewandelter Form in vielen Synchronisationsproblemen der Praxis so im Erzeuger-Verbraucher-Problem und im Leser-Schreiber-Problem. Nebenläufige Prozesse und/oder Threads synchronisieren sich untereinander, indem sie Dienste von Synchronisationsmechanismen nutzen. Die Mechanismen lassen sich danach klassifizieren, ob sie Prozesse/Threads aktiv oder passiv warten lassen. Oft ist passives Warten vorzuziehen, da aktives Warten bei längeren Wartezeiten den Prozessor stark belastet. Einfache Synchronisationsmechanismen, mit denen jeweils eine der beiden grundlegenden Bedingungen durchgesetzt wird, sind Interruptsperren und Spinlocks (wechselseitiger Ausschluss) sowie Signale und Events (Reihenfolge). Semaphore sind ein sehr flexibler Synchronisationsmechanismus, mit dem sich die meisten Synchronisationsprobleme lösen lassen. Ein Semaphor basiert auf einem Zähler, der nicht negativ werden kann und durch P- und V-Operationen herunterbzw. heraufgezählt wird. Würde der Zähler durch eine P-Operation negativ, so wird er zunächst nicht verändert und der aufrufende Prozess/Thread blockiert. V- Operationen entblockieren solche Prozesse/Threads dann wieder. Monitore sind Objekte mit Zugriffsoperationen. Der Zugriff auf einen Monitor ist wechselseitig ausgeschlossen, da sich höchstens ein Prozess/Thread gleichzeitig in einer Zugriffsoperation des Monitors befinden kann. Durch Einsatz von Bedingungsvariablen lassen sich weitere Bedingungen auf Monitoren durchsetzen.

146 3.7 Zusammenfassung und Ausblick 145 UNIX/Linux unterstützt Signale, Lock-Dateien zur Realisierung von Spinlocks, Semaphore und (für Threads) Mutexe mit Bedingungsvariablen. In Java gibt es Klassen mit atomaren Operationen, die somit wechselseitig ausgeschlossen ausgeführt werden, Semaphore, Monitore mit Bedingungsvariablen sowie eine Sammlung von Schnittstellen und Klassen für Spezialanwendungen. In Kapitel 3 stand die zeitliche Abstimmung von Aktivitäten im Vordergrund. An einigen Stellen wurden aber bereits die Aspekte der Datenübertragung ( Kommunikation ) und der Zusammenarbeit ( Kooperation ) von Prozessen und Threads sichtbar. So kann man einem Empfänger einfache Informationen zukommen lassen, indem man Signale unterschiedlicher Typen verwendet (siehe die Signalnummern in UNIX/ Linux, 3.5.1). Besonders eng miteinander verwoben sind Synchronisation und Datenübertragung bei Monitoren: Hier sind die Synchronisationsmechanismen in Objektmethoden eingebettet, und diese Objektmethoden greifen im Allgemeinen auf Speicher zu, über die Daten zwischen Prozessen/Threads ausgetauscht werden (siehe die Ringpuffer-Beispiele, , ). Die beiden folgenden Kapitel betrachten die Aspekte der Kommunikation und Kooperation systematisch. Ein entscheidender Faktor dabei ist (wie sich zeigen wird), ob die betroffenen Prozesse/Threads auf demselben Computer oder auf verschiedenen Computern in einem Rechnernetz laufen also ob sie auf einen gemeinsamen Speicher zugreifen können oder nicht. Dies hat auch Einfluss auf die Wahl der Synchronisationsmechanismen: Signale und Events setzen keinen gemeinsamen Speicher voraus, Semaphore und Monitore benötigen ihn.

147

148 3A Synchronisation: Aufgaben Die Lösungen und weitere Aufgaben findet man auf der Webseite zum Buch. 3A.1 Wissens- und Verständnisfragen 1. Kreuzen Sie die richtige(n) Aussage(n) an: a.) Bei einem Spinlock O wird aktiv gewartet. O wird passiv gewartet. O wird für eine Zeitdauer gewartet, die der Systemverwalter festlegt. b.) Lock-Dateien dienen O zur Realisierung von Java-Threads. O zur Durchsetzung des wechselseitigen Ausschlusses. O zur Zwischenspeicherung von Ausgabedaten. c.) Ein Semaphor umfasst O einen Verweis auf das Gerät, für das er den wechselseitigen Ausschluss realisiert. O eine Zählvariable. O eine Warteschlange für blockierte Prozesse. d.) Die UNIX/Linux-C-Funktion kill() kann man verwenden, um O einen anderen Prozess zu beenden. O einem anderen Prozess ein Signal zu schicken. O einen anderen Prozess einen Signal Handler ausführen zu lassen. e.) Die Zählvariable eines UNIX/Linux-Semaphors kann O auch um andere Werte als 1 erhöht und gesenkt werden. O negative Werte annehmen. O außer mit 0 oder 1 auch mit anderen nichtnegativen Werten initialisiert werden. f.) Die UNIX/Linux-Systemfunktion semget() kann benutzt werden O zur Erzeugung einer neuen Semaphorgruppe. O zur Löschung einer Semaphorgruppe. O zum Zugriff auf eine existierende Semaphorgruppe.

149 148 3A Synchronisation: Aufgaben g.) Die UNIX/Linux-Systemfunktion semctl() kann benutzt werden O zur Zuweisung von Anfangswerten an die Semaphorzähler. O zur Löschung von Semaphoren. O zur Durchführung von P- und V-Operationen. h.) In Java kann eine Reihenfolgebedingung durchgesetzt werden durch die Methoden O join(). O wait()/notify(). O semp()/semv(). 2. Füllen Sie in den folgenden Aussagen die Lücken: a.) Synchronisationsbedingungen setzt man durch mit (allgemeiner Begriff). b.) An ein Signal bindet man einen Signal Handler mit der Methode. c.) Ein Objekt, auf das wechselseitig ausgeschlossen zugriffen wird, heißt. d.) Ein verallgemeinertes Signal, auf das mehrere Prozesse warten können, heißt. e.) In Java kann man einen wechselseitigen Ausschluss dadurch realisieren, dass man Methoden mit dem Schlüsselwort kennzeichnet. f.) Eine Situation, in der sich mehrere Prozesse gegenseitig blockieren, heißt. 3. Sind die folgenden Aussagen wahr oder falsch? Begründung! a.) Wechselseitiger Ausschluss und Reihenfolgebedingung sind Synchronisationsmechanismen. b.) An einem Deadlock sind immer mindestens zwei Prozesse beteiligt. c.) In einer Lock-Datei müssen Daten gespeichert sein. d.) Statt einer Lock-Datei kann man auch einen Semaphor benutzen. e.) Mit Semaphoren kann man nur Prozesse synchronisieren, aber keine Threads. f.) Eine Bedingungsvariable ist stets einem Monitor zugeordnet. g.) Wenn mehrere Prozesse mehrere V-Operationen in falscher Reihenfolge ausführen, so kann es zu einem Deadlock kommen. h.) Die Java-Methode wait() bewirkt im Wesentlichen dasselbe wie die UNIX/Linux-C-Funktion wait(). 4. Welcher Begriff passt jeweils nicht in die Reihe? Begründung! a.) Thread, Interruptsperrung, Lock-Datei, Semaphor b.) Signal, Semaphor, wechselseitiger Ausschluss, Mutex c.) Monitor, Lock-Datei, Signal, Mutex d.) kill(), pause(), sigaction(), fork() e.) IPC_RMID, IPC_PRIVATE, GETALL, SETALL f.) acquire(), notify(), release(), drainpermits()

150 Free ebooks ==> 3A.1 Wissens- und Verständnisfragen Auf der linken Seite sind Begriffe angegeben, rechts stehen Eigenschaften. Welcher Begriff gehört zu welcher Eigenschaft? Ziehen Sie genau fünf Pfeile von links nach rechts! Monitor kann sich an mehrere Prozesse richten Bedingungsvariable ist ein Spezialfall eines Semaphors Spinlock ist ein Objekt mit w.a.-zugriff Mutex befindet sich in einem Monitor Event arbeitet mit aktivem Warten Interruptsperre wird in Hardware realisiert 6. Beantworten Sie die folgenden Fragen: a.) Was ist Synchronisation? b.) Welche zwei grundlegenden Arten von Synchronisationsbedingungen unterscheidet man? c.) Was sind Synchronisationsmechanismen? d.) Was ist ein Deadlock? e.) Welchen Nachteil hat ein Spinlock gegenüber einem Semaphor? f.) Welche zwei Semaphoroperationen (außer der Initialisierung) gibt es und wie arbeiten sie? g.) Was ist ein Monitor? h.) Ist ein Monitor thread-safe? i.) Welcher Zusammenhang besteht zwischen Monitoren und Bedingungsvariablen? j.) Wozu dienen in UNIX/Linux die Funktionen kill() und pause()? k.) Wie hängen die UNIX/Linux-Funktionen kill() und sigaction() zusammen? l.) Welchen Vorteil hat man davon, dass man in UNIX/Linux Semaphorgruppen bilden kann? m.)wie können UNIX/Linux-Semaphorgruppen gelöscht werden? Nennen Sie erstens einen Befehl der UNIX/Linux-Benutzerschnittstelle und zweitens eine Funktion der UNIX/Linux-C-Schnittstelle. n.) Mit welcher Java-Klasse lassen sich Bedingungsvariablen realisieren? 7. Tragen Sie in die linke Spalte der folgenden Tabelle die Namen von fünf Synchronisationsmechanismen ein. Kreuzen Sie jeweils an, für welche Synchronisationsbedingung(en) man den jeweiligen Mechanismus benutzen kann: w.a. Reihenfolge

151 150 3A Synchronisation: Aufgaben 8. Die beiden Standardoperationen auf einem Semaphor lassen sich in Pseudo-C-Code (etwas vereinfacht) wie folgt formulieren: SEM.COUNT++; if (SEM.COUNT>0) if (es gibt einen Prozess, SEM.COUNT--; der auf SEM blockiert ist) else { blockiere; entblockiere den Prozess; SEM.COUNT--; Schreiben Sie die passenden Operationsnamen über die Programmstücke. Markieren Sie, welcher Teil der Operationen mindestens atomar (d.h. ohne dass ein anderer Prozess dazwischenkommt) ausgeführt werden muss, damit der Semaphor ordnungsgemäß funktioniert. Was könnte passieren, wenn dieser Teil nicht atomar wäre? 9. Skizzieren Sie aus dem Gedächtnis, wie mit Hilfe von Semaphoren der wechselseitige Ausschluss zwischen zwei Prozessen A und B eine Reihenfolge zwischen zwei Prozessen A und B durchgesetzt werden kann. Dabei muss deutlich werden, welche Semaphore benutzt werden, was ihre Anfangswerte sind und an welchen Stellen der Programme von A und B die P- und V-Operationen stehen. 3A.2 Sprachunabhängige Anwendungsaufgaben 1. Weil Sie den neuesten Blockbuster sehen möchten, rufen Sie beim Kino an und fragen, ob es noch Karten gibt. Man bejaht Ihnen diese Frage, Sie bedanken sich und legen auf. Als Sie aber später an der Kinokasse eintreffen, ist alles ausverkauft. Welchen Fehler haben Sie hinsichtlich der Atomarität Ihrer Vorgehensweise gemacht? 2. Die Bahnstrecke zwischen Köln-Dellbrück und Bergisch Gladbach ist nur eingleisig ausgebaut. Laut Fahrplan trifft die S-Bahn aus Köln um 14:47 Uhr in Bergisch Gladbach ein. Kann man also am Morgen dem Fahrdienstleiter die Anweisung geben: Lass den Güterzug von Bergisch Gladbach nach Köln um 14:49 Uhr losfahren! (alternativ: 14:55 Uhr oder 15:00 Uhr)? Begründung! Was bedeutet das für Prozess-Systeme, in denen man versucht, Reihenfolgebedingungen durch sleep()-operationen (vergleiche UNIX/Linux oder Java) durchzusetzen? 3. Sie wollen Ihre superlange Stretch-Limousine auf einem bewachten Parkplatz abstellen und benötigen dafür vier zusammenhängende Stellplätze. Der Parkplatz ist momentan so voll, dass zwar einzelne Stellplätze frei sind, jedoch keine vier zusammenhängenden. Die Fluktuation ist aber hoch; es fahren also ständig (normale) Autos aus und ein. Der Parkwächter will Sie erst einlassen, wenn irgendwo zufällig vier zusammenhängende Plätze frei geworden sind. Halten Sie diesen Vorschlag für fair? Wenn nicht: Haben Sie einen Gegenvorschlag? Übertragen Sie das Beispiel und seine Lösung auf ein System, in dem Prozesse Speicherplatz reservieren wollen. 4. In einem System wird der wechselseitige Ausschluss bezüglich einer Ressource wie folgt durchgesetzt: Prozesse, die in einen kritischen Abschnitt eintreten wollen, prüfen nicht, ob die Resource frei ist, sondern blockieren sich sofort. Das Betriebssystem stellt von Zeit zu Zeit fest, ob die Ressource noch belegt ist, und weckt gegebenenfalls einen der wartenden Prozesse. Worin liegt der grundlegende Unterschied zwischen dem Synchronisationsprinzip in diesem System und dem Prinzip bei Spinlocks oder Semaphoren?

152 3A.2 Sprachunabhängige Anwendungsaufgaben Gegeben ist ein Prozess-System, das sich über Semaphore synchronisiert: Semaphore: S1.INIT(0); S2.INIT(1) Prozess 1: warte eine Sekunde Ausgabe "P1 " S1.V() S1.V() Prozess 2: S1.P() S2.P() Ausgabe "P2 " warte eine Sekunde Ausgabe "P2 " S2.V() Prozess 3: S1.P() S2.P() Ausgabe "P3 " warte eine Sekunde Ausgabe "P3 " S2.V() Kreuzen Sie die Ausgaben an, die bei der Ausführung dieses Systems auftreten könnten (dabei bedeutet beispielsweise P1 P2 P3, dass zuerst P1 auf dem Bildschirm erscheint, dann P2, dann P3): O P1 P2 P2 P3 P3 O P1 P2 P3 P2 P3 O P2 P2 P1 P3 P3 O P1 P3 P3 P2 P2 Begründen Sie für jede nicht angekreuzte Ausgabe kurz, warum sie nicht möglich ist. 6. Gegeben ist das folgende Prozess-System: Semaphore: S1.INIT(0); S2.INIT(1) Prozess A: Aktionen S1.V() S1.V() Prozess B: S1.P() S2.P() Aktionen S2.V() Prozess C: S1.P() S2.P() Aktionen S2.V() Wie nennt man die Synchronisationsbedingungen, die durch S1 bzw. durch S2 durchgesetzt werden? Beschreiben Sie kurz das zeitliche Verhalten des Gesamtsystems, das sich ergibt. 7. Gegeben sind zwei Prozesse P1 und P2. P2 soll warten, bis P1 fertig ist, und dann weiterarbeiten. P1 gibt zum Ende seiner Arbeit eine Meldung auf den Drucker aus; P2 macht eine Ausgabe, bevor er mit seiner Arbeit beginnt. Der Zugriff auf den Drucker soll wechselseitig ausgeschlossen sein. Vorgeschlagen wird die folgende Lösung mit Semaphoren: Semaphore: S_REIHE.INIT(0); S_WA.INIT(1) P1: arbeite S_WA.P() Ausgabe S_WA.V() S_REIHE.V() P2: S_WA.P() S_REIHE.P() Ausgabe S_WA.V() arbeite Welches Problem gibt es bei dieser Lösung? Was könnte man gegen dieses Problem tun?

153 152 3A Synchronisation: Aufgaben 8. Gegeben ist ein System von insgesamt 10 Prozessen. Wie sieht die Synchronisationsbedingung aus, die hier durchgesetzt wird? Beschreiben Sie sie umgangssprachlich möglichst kurz, aber exakt! Semaphore: S.INIT(3) Prozess 1: while (true) { arbeite außerhalb des kritischen Abschnitts for (i=0;i<=2;i++) S.P() arbeite im kritischen Abschnitt for (i=0;i<=2;i++) S.V() Prozesse 2-10: while (true) { arbeite außerhalb des kritischen Abschnitts S.P() arbeite im kritischen Abschnitt S.V() 9. Sie wollen ein Prozess-System mit einem kritischen Abschnitt so programmieren, dass es sich wie folgt verhält: Befindet sich ein Prozess in seinem kritischen Abschnitt, so soll der andere Prozess nicht versuchen, seinerseits in seinen kritischen Abschnitt zu kommen, sondern etwas anderes tun. Auf keinen Fall soll also ein Prozess blockieren. Dazu gehen Sie wie folgt vor: Semaphore: S_WA.INIT(0) Prozess A: if (S_WA.Zähler>0) { S_WA.P() krit. Abschnitt S_WA.V() else etwas anderes Prozess B: if (S_WA.Zähler>0) { S_WA.P() krit. Abschnitt S_WA.V() else etwas anderes Erreichen Sie mit dieser Lösung Ihr Ziel? Ist sie sogar unsicher, d.h. besteht die Gefahr, dass beide Prozesse gleichzeitig in ihren kritschen Abschnitten aktiv sind? 10. Gegeben ist ein Ablaufplan für die Ausführung von vier Prozessen: Prozess 1 Prozess 2 Prozess 3 Prozess 4 Ein Pfeil von einem Prozess A zu einem Prozess B bedeutet dabei, dass A vollständig zu Ende ausgeführt sein muss, bevor B mit seiner Ausführung beginnen kann. Skizzieren Sie, wie diese Reihenfolgebeziehungen mit Hilfe von Semaphoren durchgesetzt werden können (im Stil wie in den vorangehenden Aufgaben). 11. Ein System von drei Prozessen soll folgendermaßen ausgeführt werden: Zuerst soll Prozess A eine Ausgabe machen, danach Prozess B, dann wieder Prozess A und zum Abschluss Prozess C. Zeigen Sie, im Stil wie

154 3A.2 Sprachunabhängige Anwendungsaufgaben 153 die Systembeschreibungen in den vorangehenden Aufgaben, wie diese Synchronisationsbedingungen mit Hilfe von Semaphoren durchgesetzt werden können. 12. Lösen Sie mit Hilfe von Semaphoren die folgende Synchronisationsaufgabe: Ein Professor P erstellt ein Aufgabenblatt und gibt dann jeweils eine Kopie davon an die Studenten S1, S2 und S3 weiter. Die Studenten warten, bis sie jeweils ihr Exemplar erhalten haben, und bearbeiten dann die Aufgaben jeder für sich. Zur Arbeit benötigt ein Student einen Computer, an den aber nur zwei Bildschirme angeschlossen sind. Er muss also nach Erhalt des Blatts eventuell warten, bis ein Bildschirm für ihn frei wird. 13. Auf einer Burg leben ein alter Ritter, sein Diener und sein Hund. Jeden Mittag begibt sich der Ritter zum Esstisch, auf dem bereits ein Teller mit drei Koteletts steht. Er wartet, bis der Diener den Wein gebracht hat, und beginnt dann zu essen, wobei er fünf Koteletts (eins nach dem anderen) verspeist. Der Diener liefert dazu nach einer gewissen Zeit ein Kotelett und etwas später noch ein weiteres Kotelett nach; der Ritter muss möglicherweise jeweils darauf warten. Nach dem Essen wirft der Ritter die Knochen zur Seite und geht zu seinem Ledersessel, um dort einen Mittagsschlaf zu halten. Der Hund, der auf die Knochen gelauert hat, schnappt sie und rennt dann ebenfalls zum Sessel. Derjenige, der den Sessel als erster erreicht, belegt ihn für eine Stunde; der andere muss warten, bis der Sessel wieder freigegeben wird. Stellen Sie ein Prozess-System auf, in dem dieses Problem mit Semaphoren gelöst wird. Orientieren Sie sich dabei an der Vorgehensweise in Geben Sie zudem bei den einzelnen Synchronisationsbedingungen an, ob sie eine Reihenfolgebedingung oder ein wechselseitiger Ausschluss ist. 14. Sleeping Barber Problem mit unbegrenztem Warteraum: Ein Friseur betreibt allein ein kleines Friseurgeschäft. Bei seiner Arbeit schaut er jeweils zunächst nach, ob ein Kunde bedient werden möchte. Ist das nicht der Fall, so schläft er, bis er von einem neuen Kunden geweckt wird; ansonsten nimmt er einen wartenden Kunden an die Reihe. Er bedient diesen Kunden, entlässt ihn dann und beginnt das Spiel von vorn. Ein Kunde, der den Laden betritt, muss warten, bis er an die Reihe kommt. Sollte er allerdings bemerken, dass der Friseur gerade schläft, so weckt er ihn auf. Der Kunde nimmt, wenn er an die Reihe kommt, im Friseurstuhl Platz und bleibt dort so lange, bis der Barbier ihn entlässt. Er verlässt dann den Laden. Stellen Sie ein Prozess-System auf, in dem dieses Problem mit Semaphoren gelöst wird. Orientieren Sie sich auch hier an der Vorgehensweise in In einem Kindergarten liegen auf einem Tisch ein roter, ein grüner und ein blauer Buntstift. Am Tisch sitzen Anna, Bolle und Christina, die jeweils ein Bild malen wollen. Anna will erst einen Apfelbaum malen (wozu sie sowohl den roten als auch den grünen Stift benötigt) und anschließend den Himmel (dazu braucht sie nur den blauen Stift). Bolle möchte zunächst ein brennendes Haus malen (mit allen drei Stiften) und danach ein Feuerwehrauto (nur roter Stift). Christina malt eine abstrakte Grafik in drei Schritten erst nur die grünen Teile, dann nur die blauen und dann nur die roten. Stellen Sie ein entsprechendes Prozess-System auf, das sich mit Semaphoren synchronisiert. Von welcher Art sind alle Synchronisationsbedingungen, die hier durchzusetzen sind? Wir nehmen nun an, dass (für andere Bilder) Anna den roten und den grünen, Bolle den roten und den blauen und Christina den grünen und den blauen Stift benötigen. Wie könnte hier ein Deadlock auftreten? Auf welche zwei Arten könnte man einen solchen Deadlock verhindern?

155 154 3A Synchronisation: Aufgaben 16. Sie haben in das Erzeuger-Verbraucher-Problem mit seinen drei Synchronisationsbedingungen kennengelernt. Vorgeschlagen wird die folgende Lösung (für einen Puffer der Kapazität 5): Semaphore: S_WA.INIT(1); S_LEER.INIT(0); S_VOLL.INIT(5) Erzeuger: while (true) { Produkt erzeugen S_WA.P() S_VOLL.P() Produkt einfügen S_LEER.V() S_WA.V() Verbraucher: while (true) { S_WA.P() S_LEER.P() Produkt entnehmen S_VOLL.V() S_WA.V() Produkt verbrauchen Welches Problem tritt bei dieser Lösung auf? Wie könnte es behoben werden? 17. Betrachten Sie das folgende Szenario: Mehrere Studenten sollen jeweils eine Seminararbeit schreiben. Bevor ein Student mit dem Schreiben beginnen kann, muss er sich von Buch A und Buch B jeweils ein Exemplar aus der Bibliothek ausleihen. Nach dem Schreiben gibt er die Bücher wieder zurück. Die Bibliothek hat nur eine begrenzte Anzahl von Büchern vorrätig. Wie kann es hier zu einem Deadlock kommen? Wie können solche Deadlocks verhindert werden? Wie können solche Deadlocks vermieden werden? Wie können solche Deadlocks aufgehoben werden? (Zu den Begriffen Deadlock-Verhinderung/-Vermeidung/-Aufhebung siehe ) 18. Skizzieren Sie (im Stil wie BILD 3.25) einen Monitor, der einen Stack mit begrenzter Kapazität realisiert. Der Stack arbeitet nach dem Last-In-First-Out-Prinzip (LIFO): Die Schreiboperation fügt einen neuen Eintrag an der Stackspitze hinzu; ist der Stack voll, so wird sie bis zur nächsten Leseoperation blockiert. Die Leseoperation entfernt den Eintrag von der Stackspitze; ist der Stack leer, so wird sie bis zur nächsten Schreiboperation blockiert. 19. Skizzieren Sie einen Monitor, der Werte gesichert speichert. Seine Schreiboperation kann Werte unmittelbar (d.h. ohne besondere Sicherheitstests) im Monitor ablegen. Seine Leseoperation liefert gespeicherte Werte zurück aber erst, wenn zuvor zwei Entsperrfunktionen ausgeführt wurden. Die Entsperrfunktionen schließen den Monitor also gewissermaßen zum Lesen auf. Wie sie arbeiten, ist hier unerheblich; es kommt nur auf die zeitliche Reihenfolge erst entsperren, dann lesen an. Die Lesesperre soll zu Beginn gesetzt sein und durch jeden Lesevorgang neu gesetzt werden. Um die Lösung einfach zu halten, soll der Monitor nur einen int-wert speichern können. Tipp: Verwenden Sie nur eine Bedingungsvariable, aber zwei boolesche Variablen, die anzeigen, ob die einzelnen Entsperrfunktionen bereits ausgeführt wurden. Wie sieht die Lösung aus, wenn vor dem Lesen nur eine der beiden Entsperrfunktionen ausgeführt sein muss (welche der beiden es ist, ist unerheblich)?

156 3A.3 Programmierung unter UNIX/Linux 155 3A.3 Programmierung unter UNIX/Linux 1. Im folgenden Programmstück soll die Reihenfolge erst Sohn 2, dann Sohn 1 durchgesetzt werden: if (fork()==0) { sleep(1); Aktionen von Sohn 1 exit(0); if (fork()==0) { Aktionen von Sohn 2 exit(0); Ist diese Vorgehensweise korrekt? Begründung! 2. Betrachten Sie das folgende Programmstück: Ein Vaterprozess erzeugt zwei Sohnprozesse, die jeweils in einer Endlosschleife laufen und dabei wiederholt einen kritischen Bereich betreten. Dabei soll der wechselseitige Ausschluss zwischen den beiden Söhnen sichergestellt werden. Der Vater löscht die Söhne nach einer bestimmten Wartezeit. int sohn1, sohn2; shared int lock = 0; // Erklärung siehe unten if ((sohn1=fork())==0) while (1) {... // mache irgendetwas while (lock==1); // warte auf Freigabe des krit. Bereichs lock=1; // belege kritischen Bereich... // arbeite im kritischen Bereich lock=0; // gib kritischen Bereich frei if ((sohn2=fork())==0)... wie Sohn 1... sleep(...); kill(sohn1,sigkill); kill(sohn2,sigkill); shared int lock ist hierbei eine Kurzschreibweise für eine Variable, die für alle Prozesse zugreifbar ist. In einem realen Programm müsste man natürlich die entsprechenden UNIX/Linux-Mechanismen verwenden ( 4.2.1). Ist das Programm so korrekt? Begründen Sie Ihre Antwort und nennen Sie gegebenenfalls eine Korrekturmöglichkeit. 3. Betrachten Sie das folgende Programmstück:... Anbindung des Signal Handlers an SIGUSR1... int p1, p2; if ((p1=fork())==0) { pause(); printf("aaa\n"); exit(0); if ((p2=fork())==0) { pause(); printf("bbb\n"); exit(0); sleep(10);

157 156 3A Synchronisation: Aufgaben printf("ccc\n"); kill(p2,sigusr1); wait(&status); kill(p1,sigusr1); wait(&status); printf("ddd\n"); Welche Ausgabe erscheint auf dem Bildschirm? Begründung! Können Sie exakt angeben, wie viele Sekunden nach Programmstart die letzte Ausgabe erscheint? Warum bzw. warum nicht? 4. Schreiben Sie ein Programm, in dem drei Sohnprozesse gestartet werden, die sich über Signale synchronisieren. Zunächst soll einer der Prozesse dreimal das Wort AAA ausgeben und dabei zwischen den einzelnen Ausgaben jeweils eine Sekunde pausieren. Die beiden anderen Prozesse sollen warten, bis er fertig ist, und dann in Endlosschleifen die Wörter BBB bzw. CCC ausgeben (jeweils mit einer Sekunde Pause dazwischen, eine Durchmischung der Ausgaben ist erwünscht). Der Vaterprozess soll die beiden Prozesse nach 10 Sekunden löschen. Tipp zur Programmierung: Starten Sie die BBB- und CCC-Prozesse zuerst und speichern Sie deren PIDs in Variablen. Der anschließend gestartete AAA-Prozess kann auf diese Variablen zugreifen und weiß somit, wohin er seine Signale schicken muss. 5. Betrachten Sie das folgende einfache System: Semaphore: SEM.INIT(0) Prozess 1: SEM.P() Aktionen Prozess 2: Aktionen SEM.V() Gehen Sie davon aus, dass die P-Operation immer vor der V-Operation ausgeführt wird. Können Sie die Semaphoroperationen durch kill() und pause() ersetzen? Wenn nein, warum nicht? Wenn ja, so streichen Sie SEM.P() und SEM.V() und schreiben Sie stattdessen jeweils an der richtigen Stelle kill() bzw. pause() hin. 6. Betrachten Sie das folgende Teilstück eines C-Programms auf einem UNIX/Linux-System: int s; unsigned short anf[1]; struct sembuf op1, op2; op1.sem_num = 0; op2.sem_num = 0; op1.sem_op = 1; op2.sem_op = -1; op1.sem_flg = 0; op2.sem_flg = 0; s = semget(ipc_private,1,ipc_creat 0777); anf[0] = 0; semctl(s,0,setall,anf); if (fork()==0) {... Aktionen 1... semop(s,&op1,1); exit(0);

158 3A.3 Programmierung unter UNIX/Linux 157 if (fork()==0) { semop(s,&op2,1);... Aktionen 2... exit(0); Wie viele Semaphore werden hier erzeugt? Kurze Begründung! Wie nennt man die Synchronisationsbedingung, die hier durchgesetzt wird? Welchen Wert hat/haben der/die Semaphor(e), wenn beide Sohnprozesse vollständig abgelaufen sind? Begründung! Schreiben Sie eine C-Anweisung, mit der die Semaphorgruppe gelöscht wird. 7. Betrachten Sie das folgende Teilstück eines UNIX/Linux-C-Programms: int semid; unsigned short initarray[5] = {1,0,1,1,1; struct sembuf sem_p[3]; semid = semget(ipc_private,5,ipc_creat 0777); semctl(semid,5,setall,initarray); sem_p[0].sem_num = 1; sem_p[1].sem_num = 3; sem_p[2].sem_num = 4; sem_p[0].sem_op = sem_p[1].sem_op = sem_p[2].sem_op = -1; sem_p[0].sem_flg = sem_p[1].sem_flg = sem_p[2].sem_flg = 0; semop(semid,sem_p,3); Was bewirken die Aufrufe von semget() und semctl() in diesem Programm? Was ist der Effekt des semop()-aufrufs erstens für den ausführenden Prozess und zweitens für die Werte der Zählvariablen der Semaphore? (Gehen Sie davon aus, dass andere Prozesse zwischenzeitlich keine semop()-aufrufe gemacht haben.) Was wäre der Effekt, wenn der Aufruf folgendermaßen aussähe: semop(semid,&sem_p[1],2)? 8. Gegeben ist das folgende Teilstück eines UNIX/Linux-C-Programms: int semid; struct sembuf sem_p; semid = semget(ipc_private,4,ipc_creat 0777);... sem_p.sem_num = 1; sem_p.sem_op = -1; sem_p.sem_flg = 0; semop(semid,&sem_p,1); sem_p.sem_num = 3; semop(semid,&sem_p,1); In diesem Programmstück werden offensichtlich zwei P-Operationen hintereinander ausgeführt. Ändern Sie es so, dass die beiden P-Operationen gleichzeitig (also atomar nach dem Alles-oder-nichts-Prinzip ) ablaufen. 9. Wählen Sie aus den Aufgaben 3A nach Belieben einige aus. Implementieren Sie die Aufgabenbeschreibungen bzw. -lösungen durch UNIX/Linux-C-Programme mit Semaphoren. 10. Implementieren Sie Ihre Lösung zu Aufgabe 3A (Kinder mit Buntstiften) durch ein UNIX/Linux-Prozess-System. Wenn ein Kind mehrere Buntstifte braucht, so soll das Programm die entsprechenden P-Operationen auf mehreren Semaphoren atomar ausführen.

159 158 3A Synchronisation: Aufgaben 11. Cigarette Smokers Problem: Drei Männer sitzen an einem Tisch und möchten gern rauchen. Dazu brauchen sie Tabak, Papier und Streichhölzer. Der erste besitzt zwar unbegrenzt viel Tabak, der zweite Papier und der dritte Streichhölzer; da aber alle drei unkooperativ sind, gibt keiner den anderen etwas ab. Ab und zu kommt aber ein vierter Mann (der Lieferant ) vorbei und wirft nach dem Zufallsprinzip zwei der drei benötigten Materialien auf den Tisch. Einer der Männer kann jetzt also rauchen und tut es auch, nachdem er die Materialien vom Tisch genommen hat. Kommt der Lieferant, während ein Mann noch raucht, so wartet er, bis dieser fertig ist. Lösen Sie das Problem durch eine UNIX/Linux-C-Programm mit vier Semaphoren: Einer blockiert den Lieferanten, solange ein Raucher raucht. Die anderen drei sind den Materialien Tabak, Papier und Streichholz zugeordnet. Nutzen Sie hier die Möglichkeit, dass in UNIX/Linux zwei P-Operationen auf zwei Semaphoren atomar ausgeführt werden können. Sonst bestünde die Gefahr, dass sich zwei Raucher gegenseitig blockieren beispielsweise dadurch, dass einer den Tabak und der andere das Papier vom Tisch nimmt. 12. Implementieren Sie Ihre Lösung zu Aufgabe 3A (Stack mit begrenzter Kapazität) mit Pthreads, einem Mutex und Bedingungsvariablen. Praktischer Hinweis: Bevor Sie sich aus Ihrem Computer ausloggen, müssen Sie mit ipcs prüfen, ob noch Semaphorgruppen, die Sie erzeugt haben, existieren, und diese mit ipcrm löschen. Zudem sollten Sie mit ps prüfen, ob (außer der Shell) noch weitere Prozesse für Sie aktiv sind und diese mit kill löschen. 3A.4 Programmierung in Java 1. Wählen Sie aus den Aufgaben 3A und 3A nach Belieben einige aus. Implementieren Sie die Aufgabenbeschreibungen bzw. -lösungen durch Java-Programme mit Threads und Semaphoren. 2. Schreiben Sie eine Klasse für Objekte, die eine Methode zur Bildschirmausgabe bereitstellen. Die Methode soll einen String als Parameter übergeben bekommen und ihn dreimal ausgeben. Zwischen zwei Ausgaben soll der ausführende Thread jeweils eine Pause vom 200 ms machen. Für ein Objekt dieser Klasse soll sichergestellt werden, dass seine Methode immer nur von einem Thread gleichzeitig ausgeführt werden kann, dass sich also Ausgaben, die über dieses Objekt laufen, nicht zeitlich durchmischen. 3. Ändern Sie die Lösung der vorherigen Aufgabe so, dass die Ausgabevorgänge nicht mehr wechselseitig ausgeschlossen sind, sondern sich zeitlich durchmischen können. Zusätzlich soll nun jedes Objekt der Klasse eine Zählvariable besitzen, die angibt, wie viele Threads seine Ausgabemethode zur Zeit ausführen. Die Variable wird zu Beginn einer Methodenausführung inkrementiert und am Ende dekrementiert. Diese Zähleroperationen (und nur diese) müssen wechselseitig ausgeschlossen ausgeführt werden. Tipp: Nutzen Sie die Möglichkeit, Blöcke in Methoden als synchronized zu kennzeichnen. 4. Lösen Sie Aufgabe 3A (Kinder mit Buntstiften) durch ein Java-Programm. Programmieren Sie dazu für jedes Kind einen eigenen Thread, in dessen run()-methoden synchronized-blöcke stehen. Durch diese Blöcke (die möglicherweise geschachtelt sind) soll der wechselseitige Ausschluss bei der Benutzung der Buntstifte durchgesetzt werden. Dazu werden drei Objekte einer Klasse Buntstift benötigt, auf die alle drei Threads zugreifen können. Die Klasse Buntstift muss keine Methoden und keine Attribute definieren, sondern dient lediglich zur Erzeugung der Objekte, über die sich die Threads synchronisieren. Ändern Sie anschließend Ihr Programm so, dass ein Deadlock provoziert wird. Dazu soll Anna zuerst nach dem roten Stift greifen und nach einer Sekunde Pause nach dem grünen. Bolle soll umgekehrt vorgehen.

160 Free ebooks ==> 3A.4 Programmierung in Java Schreiben Sie eine Klasse, die eine Barriere realisiert. An einer solchen Barriere werden Threads blockiert, bis eine bestimmte Anzahl wartender Threads erreicht ist. Dann werden alle wartenden Threads entblockiert. Die Klasse muss dazu eine Zählvariable bereitstellen, die die Anzahl der wartenden Threads angibt, und eine Methode, die von Threads aufgerufen wird, die die Barriere passieren wollen. In der Methode blockiert sich der aufrufende Thread per wait(), oder er entblockiert alle wartenden Threads per notifyall(). Überlegen Sie sich eine Lösung, in der nicht notifyall(), sondern notify() benutzt wird. 6. Ändern Sie Ihre Lösung zur vorherigen Aufgabe so, dass jeweils nur der Thread weiterläuft (und damit den Monitor verlässt), der am längsten wartet. Gehen Sie dazu wie folgt vor: Referenzen auf die wartenden Threads werden in einer LinkedList gespeichert, in der jeweils die Referenz auf den am längsten wartenden Threads vorn steht. Wenn (bei Ankunft eines weiteren Threads an der Barriere) die geforderte Anzahl wartender Threads erreicht ist, werden alle wartenden Threads per notifyall() entblockiert. Jeder dieser Threads prüft, ob er in der LinkedList ganz vorn steht. Ist das der Fall ist, so läuft der Thread weiter; ansonsten blockiert er sich wieder. Der neu angekommene Thread blockiert sich in jedem Fall. 7. Schreiben Sie ein Java-Programm, in dem eine int- und eine AtomicInteger-Variable mit 0 initialisiert werden. Es werden dann zwei nebenläufige Threads gestartet, die jeweils in einer Schleife laufen und in jedem Schleifendurchgang die Werte der beiden Variablen um 1 erhöhen. Der Zugriff auf die int-variable soll nicht besonders synchronisiert werden. Der Hauptprogramm-Thread stoppt nach vier Sekunden die beiden Threads und gibt dann die beiden Variablenwerte aus. Was beobachten Sie, und wie erklären Sie sich das? (Alle Details zur Klasse AtomicInteger finden Sie in der Java-Online-Dokumentation [JavaSpec].) 8. Implementieren Sie Ihre Lösung zu Aufgabe 3A (Stack begrenzter Kapazität) mit den Java-Interfaces Lock und Condition.

161 4 Kommunikation Kooperation Kapitel 4 befasst sich mit der Übertragung von Kommunikation Daten zwischen Prozessen und Threads. Die Synchronisation Prozesse/Threads können dabei auf demselben Computer laufen oder auch auf verschiedenen Computern eines Rechnernetzes, Basistechniken insbesondere dem Internet. 4.1 Grundlegende Begriffe Abschnitt 4.1 führt eine Reihe von Grundbegriffen der Datenkommunikation ein. Es geht dabei um verschiedene Mechanismen zur Datenübertragung, nämlich gemeinsam nutzbare Speicherbereiche, Nachrichten und Datenströme. Darüber hinaus werden Möglichkeiten besprochen, die übertragenen Daten zwischenzuspeichern und die Datenübertragung zeitlich zu organisieren. Ein weiteres Thema ist die Kommunikation in Rechnernetzen. Hierfür sind Kommunikationsprotokolle zur Steuerung der Abläufe sowie Schnittstellen zum Zugriff auf die Protokolle wichtig Arten der Kommunikation Sender-Empfänger-Beziehungen Kommunikation in Rechnernetzen Techniken in UNIX/Linux Abschnitt 4.2 präsentiert die Kommunikationsmechanismen, die die Programmierschnittstelle von UNIX/Linux anbietet. Sie unterstützt sämtliche Arten der Datenübertragung, die in 4.1 eingeführt werden sowohl lokal als auch über das Internet Shared Memory Pipes Message Queues Sockets Techniken in Java Abschnitt 4.3 stellt (teilweise unter Verweis auf Nachbarkapitel) die Java-Konzepte zur Datenübertragung dar. Auch Java realisiert sämtliche Arten der Datenübertragung aus Übersicht Piped Streams Sockets Zusammenfassung und Ausblick...202

162 4 Kommunikation Wie das vorige Kapitel gezeigt hat, werden bei der Synchronisation Daten zwischen Prozessen oder auch Threads übertragen. Diese Daten dienen primär dazu, die Prozesse zeitlich aufeinander abzustimmen. Bei einem wechselseitigen Ausschluss ist dies ihr einziger Zweck, denn hier soll nur sichergestellt werden, dass sich die Prozesse nicht gegenseitig stören. Eine Reihenfolgebedingung hingegen ist meist dadurch begründet, dass ein Nachfolger mit dem weiterarbeiten möchte, was ihm ein Vorgänger liefert. Die Synchronisationsdaten können also zusätzlich genutzt werden, um Informationen vom Vorgänger an den Nachfolger zu übertragen. Das ist beispielsweise bei den unterschiedlichen Signalnummern von UNIX/Linux der Fall ( 3.5.1). In diesem Kapitel soll der Aspekt der Datenübertragung zwischen Prozessen (und auch Threads) vertieft und verallgemeinert werden. Es geht also um die Kommunikation zwischen Prozessen/Threads: Kommunikation (engl.: inter-process communication, IPC) bedeutet hier die Übertragung von Daten zwischen Prozessen und Threads. Die Prozesse/Threads können dabei auf demselben Computer oder auch auf unterschiedlichen Rechnerknoten eines Netzes ablaufen. DEFINITION Kommunikation Kommunikation ist Grundlage von Kooperation, also der Zusammenarbeit von Prozessen zur Lösung von Problemen. Dieser Gesichtspunkt wird im nachfolgenden Kapitel betrachtet. 4.1 Grundlegende Begriffe Arten der Kommunikation Man kann drei Ansätze zur Übertragung von Daten unterscheiden nämlich die speicher-, die nachrichten- und die strombasierte Kommunikation: Speicherbasierte Kommunikation basiert auf einem gemeinsam zugreifbaren Speicherbereich (engl.: shared memory), in den ein Sender Daten schreibt und aus dem ein Empfänger Daten liest. Nachrichten- und strombasierte Kommunikation stützen sich auf ein Kommunikationssystem, das Dienste zum Senden und Empfangen von Daten anbietet. Bei nachrichtenbasierter Kommunikation werden die Daten als Einheiten endlicher Länge (Nachrichten, engl.: messages) verschickt, bei strombasierter Kommunikation als fortlaufender Datenstrom. DEFINITION Speicherbasierte, nachrichtenbasierte und strombasierte Kommunikation

163 162 4 Kommunikation Die folgenden Alltagsbeispiele illustrieren die Eigenschaften dieser Ansätze: BILD 4.1 Arten der Kommunikation Alltagsbeispiele Schwarzes Brett: speicherbasierte Kommunikation Aushang Postwesen: nachrichtenbasierte Kommunikation Rundfunk: strombasierte Kommunikation Die Beispiele zeigen, dass zwischen speicherbasierter Kommunikation einerseits und nachrichten- sowie strombasierter Kommunikation andererseits ein grundlegender Unterschied besteht: Bei der speicherbasierten Kommunikation stellt das zugrundeliegende System lediglich einen gemeinsamen Speicher bereit; eine darüber hinausgehende Unterstützung bietet es nicht. Sender und Empfänger müssen also selbst dafür sorgen, dass die Daten am gewünschten Ziel ankommen. Zudem müssen sie sich selbst synchronisieren, also den wechselseitigen Ausschluss beim Speicherzugriff und ihre Reihenfolgebeziehung selbst durchsetzen. Wie das geht, wurde am Beispiel des Erzeuger-Verbraucher-Problems in Kapitel 3 gezeigt. Bei der nachrichten- und strombasierten Kommunikation stellt das System Dienste zum Senden und Empfangen von Daten zur Verfügung. Die Prozesse müssen lediglich diese Dienste geeignet aufrufen; um die Zustellung der Nachrichten und die dabei nötige Synchronisation kümmert sich das System. In Computern ist es die Systemsoftware (und dabei insbesondere das Betriebssystem), die die verschiedenen Arten der Kommunikation realisiert: Für die speicherbasierte Kommunikation stellt die Programmierschnittstelle des Betriebssystems ( ) Funktionen bereit, mit denen man Speicherbereiche für den Zugriff mehrerer Prozesse einrichten kann. Diese Speicherbereiche liegen dann, so sagt man, in den Adressräumen mehrerer Prozesse. Die explizite Freigabe von Speicherbereichen ist notwendig, da die Speicher der Prozesse normalerweise voneinander isoliert sind ( ). Prozesse können dann über Speicheradressen auf die einzelnen Zellen eines solchen Shared Memory lesend und schreibend zugreifen. Für die Threads eines Prozesses ist speicherbasierte Kommunikation unmittelbar gegeben, da sie sich per definitionem ( ) einen gemeinsamen direkt zugreifbaren Speicher teilen.

164 4.1 Grundlegende Begriffe 163 Zur nachrichten- und strombasierten Kommunikation stellt die Systemsoftware Sende- und Empfangsdienste bereit. An der Betriebssystemschnittstelle sind das typischerweise send()- und receive()-funktionen. In Rechnernetzen und verteilten Systemen kommen Protokolldienste des Kommunikationssystems hinzu ( 4.1.3), die die Datenübertragung zwischen den Knoten im Netz ( 2.1.2) steuern. Sende- und Empfangsdienste sorgen implizit auch für die erforderliche Synchronisation der beteiligten Prozesse. Speicherbasierte Kommunikation beschränkt sich also im Prinzip auf einen einzelnen Computer; nachrichten- und strombasierte Kommunikation ist auch über Rechnergrenzen hinweg möglich. Der Vollständigkeit halber ist anzumerken, dass Prozesse mit Hilfe von Distributed Shared Memory (DSM) auch Speicherzellen adressieren können, die in den Speichern anderer Rechnerknoten liegen. Auf dieser Basis kann dann ein rechnerübergreifender gemeinsamer virtueller Speicher realisiert werden. Letztendlich kommt aber auch hier eine nachrichtenbasierte Kommunikation zum Einsatz, die Speicherinhalte zwischen den Rechnerknoten überträgt Sender-Empfänger-Beziehungen Bei der Übertragung von Nachrichten können ein oder mehrere Sender und Empfänger auftreten, sie können direkt oder indirekt miteinander kommunizieren, und sie können zeitlich eng oder lose miteinander gekoppelt sein Ein oder mehrere Sender und Empfänger Eine Nachricht kann an einen oder auch an mehrere Empfänger geschickt werden. Man spricht dann von Unicast bzw. Multicast. Richtet sich eine Nachricht an alle, also beispielsweise an sämtliche Prozesse in einem System, so handelt es sich um einen Broadcast: a.) 1:1 ( Unicast ): S E 1 b.) 1:m ( Multicast ): S E 1 c.) 1:all ( Broadcast ): S E 1 BILD 4.2 Sender-Empfänger- Beziehungen E 2 E 2 E 2 E 3 E 3 E 3 E 4 E 4 E Umgekehrt können mehrere Sender ihre Nachrichten an denselben Empfänger richten. Diese Art der Kommunikation tritt insbesondere in Client-Server-Systemen auf, bei denen mehrere Nutzer (die Clients ) die Dienste eines Anbieters (des Servers ) aufrufen ( 5.1.1).

165 164 4 Kommunikation Aufgaben 4A.2.1./2. DEFINITION Mailbox, Port Direkte vs. indirekte Kommunikation Ein Sender kann Nachrichten direkt oder indirekt an einen Empfänger schicken. Bei direkter Kommunikation werden die Nachrichten unmittelbar vom Sender an den Empfänger übertragen; bei indirekter Kommunikation legt sie der Sender in einem Zwischenspeicher, einer Mailbox oder einem Port, ab, wo sie der Empfänger abholt: Mailboxen und Ports sind Zwischenspeicher für die nachrichtenbasierte Kommunikation. Auf eine Mailbox können im Prinzip mehrere Empfänger zugreifen; auf einem Port besitzt nur ein Empfänger Empfangsrechte. BILD 4.3 Direkte vs. indirekte Übertragung a.) direkte Übertragung: S send(e,message) zu sendende Nachricht Empfänger E receive(&message) receive(s,&message) empfangene Nachricht (Rückgabeparameter) gewünschter Sender b.) indirekte Übertragung über Mailbox (m:n-kommunikation): S 1. Mailbox.. S m Zwischenspeicher für mehrere Empfänger send(mailbox,message) E 1... E n receive(mailbox,&message) c.) indirekte Übertragung über Port (m:1-kommunikation): S 1... S m send(port,message) Port Zwischenspeicher für genau einen Empfänger E receive(port,&message) Wie die Abbildung zeigt, hat die Art der Kommunikation einen Einfluss auf die Parameter der Sende- und Empfangsfunktionen: Bei direkter Kommunikation nennt die Sendefunktion den oder die Empfänger der Nachricht, und die Empfangsfunktion nennt den Sender, von dem die Nachricht kommen soll (sofern eine solche Einschränkung gewünscht wird). Bei indirekter Kommunikation geben Sende- und Empfangsfunktionen die entsprechende Mailbox oder den Port an. Mit Mailboxen und Ports kann man Sender und Empfänger in zweierlei Hinsicht voneinander entkoppeln: Erstens muss der jeweilige Partner nicht explizit genannt werden

166 4.1 Grundlegende Begriffe 165 eine Nachricht richtet sich lediglich an einen Zwischenspeicher, wo sie dann von einem Empfänger abgeholt wird, der dem Sender möglicherweise unbekannt bleibt. Zweitens müssen Sende- und Empfangsvorgang nicht synchron ablaufen, die Partner also nicht aufeinander warten (siehe hierzu auch ). Es bleibt anzumerken, dass der Port-Begriff in der Informatik etwas unterschiedlich benutzt wird. Der hier eingeführte strenge Begriff entspricht den Ports in den Betriebssystemkernen von Mach und Windows, für die jeweils nur ein Prozess das Empfangsrecht besitzen kann. In solchen Kernen werden Ports benutzt, um Client-Server-Beziehungen ( 5.1.1) zu implementieren: Clients schreiben ihre Aufträge in die Ports von Servern, Server schicken ihre Antworten in den Port des jeweils auftraggebenden Clients Enge vs. lose zeitliche Kopplung Sende- und Empfangsfunktionen können blockierend oder nicht blockierend vorgehen: Beim blockierenden Senden und Empfangen (blocking send bzw. blocking receive) warten Sender und Empfänger, bis der Partner empfangs- bzw. sendebereit ist. Beim nichtblockierenden Senden (nonblocking send) legt der Sender seine Nachricht in einem Zwischenspeicher ab und läuft weiter; der Empfänger liest die Nachricht dann später aus dem Speicher. Beim nichtblockierenden Empfangen (nonblocking receive) übernimmt der Empfänger die Nachricht nur, wenn sie bereits vorliegt; ansonsten läuft er weiter, und die Nachricht, die später eintrifft, geht verloren. Aufgabe 4A.2.3. Blocking Send: Sender send wartet Empfänger Nachricht Blocking Receive: Sender Empfänger send Nachricht BILD 4.4 Synchrone vs. asynchrone Kommunikation receive receive wartet Non-Blocking Send: Non-Blocking Receive: Sender Sender send send Empfänger Nachricht Zwischenspeicher Empfänger Nachricht Verlust receive receive Blockierendes Senden und Empfangen führen also zu einer zeitlichen Kopplung und damit zu einer Synchronisation von Sender und Empfänger. Blockieren beide Operationen, so müssen beide Partner aufeinander warten die Kommunikation ist damit

167 166 4 Kommunikation synchron, und es kommt zu einem Rendezvous der Prozesse. Blockieren die Operationen nicht, so nennt man die Kommunikation asynchron. Der Begriff des blockierenden Sendens und Empfangens impliziert ein passives Warten ( 3.2.1), bei dem der Prozess blockiert wird, bis der Empfänger bzw. der Sender eintrifft. Alternativ ist ein Polling ( 3.2.1) möglich, bei dem der wartende Prozess wiederholt aktiv nachfragt, ob sein Partner nunmehr bereit ist Kommunikation in Rechnernetzen Prozesse, die miteinander kommunizieren, können auf demselben Computer oder auf verschiedenen Computern eines Rechnernetzes laufen. Zur lokalen Kommunikation auf einem Computer werden Dienste seines Betriebssystems genutzt; bei der Kommunikation zwischen Rechnerknoten kommen zusätzlich die Dienste eines Kommunikationssystems zum Einsatz, das die Datenübertragung über das Netz steuert. Im Folgenden sollen nur diejenigen Aspekte von Kommunikationssystemen kurz angesprochen werden, die für die Programmierung nebenläufiger Anwendungen interessant sind. Eine umfassende, tiefergehende Darstellung findet man in der Literatur zu Rechnernetzen (z.b. [Come09], [Tane11]) Schnittstellen: Sockets Um ein Kommunikationssystem benutzen zu können, muss man seine Schnittstellen kennen, also die Dienste, die es seinen Nutzern anbietet. Wie das Netz intern arbeitet, muss dagegen nicht bekannt sein. Dieses Prinzip ist aus dem Alltagsleben bekannt: Möchte jemand einen Brief verschicken oder ein Telefonat führen, so muss er wissen, wie man mit einem Briefkasten bzw. mit einem Telefon umgeht. Die technische Funktionsweise der Systeme, die jeweils dahinterstehen (also das Postwesen oder das Telekommunikationsnetz), muss er nicht kennen. BILD 4.5 Kommunikationssysteme, ihre Schnittstellen und Nutzer Telekommunikationsnetz Postwesen Programm/Prozess Socket Rechnernetz/ Internet Für Programmierer sind die so genannten Sockets die Standardschnittstelle zu Rechnernetzen und insbesondere zum Internet ( ). Einen Socket (wörtliche Übersetzung: Steckdose ) kann man sich bildlich vorstellen wie eine Telefon- oder Ethernetsteckdose: Sie stellt einen Anschlusspunkt dar, über den ein Programm auf ein Rechnernetz zugreifen kann, ohne seine genaue Arbeitsweise kennen zu müssen. DEFINITION Socket Sockets sind Schnittstellen, über die Anwendungsprogramme auf Rechnernetze zugreifen können. Sie bieten Dienste zum Senden und Empfangen von Daten sowie zur Steuerung der Datenübertragungen.

168 4.1 Grundlegende Begriffe 167 Datenübertragungen zwischen zwei Prozessen finden jeweils über ein Paar von Sockets statt: Die Daten, die der eine Prozess in seinen Socket schreibt, kann der andere Prozess aus seinem Socket lesen und umgekehrt. Somit lassen sich über ein Socketpaar Daten bidirektional, das heißt in beiden Richtungen übertragen. Derselbe Socket kann also gleichzeitig für ein- und ausgehende Daten genutzt werden. Rechnerknoten Prozess Socket Rechnernetz/Internet Daten Rechnerknoten Prozess Socket BILD 4.6 Datenübertragung über ein Socketpaar Socket-Implementationen gibt es unter anderen in UNIX/Linux ( 4.2.4, dort werden dann auch weitere Details zu Sockets besprochen) und in Java ( 4.3.3) Protokolle und Protokollstacks Kommunikation muss geordnet ablaufen, also bestimmten Regeln gehorchen. Bei einem Telefongespräch ist es beispielsweise üblich, dass man sich zu Beginn mit Namen begrüßt, dann Informationen austauscht, indem man ordentlich abwechselnd spricht, und sich schließlich mit bestimmten Floskeln voneinander verabschiedet. Ein System solcher Regeln wird als Kommunikationsprotokoll bezeichnet: Ein Kommunikationsprotokoll (engl.: communication protocol, kurz: Protokoll) in einem Rechnernetz ist ein System von Regeln, die den Ablauf der Datenübertragung bestimmen. Sie legen insbesondere fest, wie die übertragenen Daten formatiert sind, welche zeitlichen Abfolgen beim Datenaustausch zulässig sind und ob und wie auf Fehler reagiert wird. Eine Protokollinstanz (engl.: protocol instance) ist die Implementation eines Protokolls in Soft- oder Hardware. Aufgaben 4A.2.4./5. DEFINITION Kommunikationsprotokoll Protokolle lassen sich unter anderem nach folgenden Gesichtspunkten klassifizieren: Ein Protokoll kann verbindungsorientiert oder verbindungslos arbeiten. Bei einem verbindungsorientierten Protokoll wird zunächst eine Verbindung zwischen den Kommunikationspartnern aufgebaut, über die anschließend die Daten übertragen werden wie bei einem Telefongespräch. Bei einem verbindungslosen Protokoll werden die Daten ohne vorherigen Verbindungsaufbau losgeschickt wie beim Versenden eines Briefs. Ein Protokoll kann zuverlässig oder nicht zuverlässig arbeiten. Ein zuverlässiges Protokoll garantiert, dass die Daten beim Empfänger ankommen beispielsweise durch automatische Wiederholung der Sendung bei Übertragungsfehlern. Bei einem nicht zuverlässigen Protokoll ist das nicht der Fall. Ein Protokoll kann eine sequentielle Datenübertragung garantieren oder auch nicht. Bei der sequentiellen Übertragung ist sichergestellt, dass die Daten stets

169 168 4 Kommunikation BILD 4.7 Protokollstack im Mobiltelefon (sehr grob vereinfachter Ausschnitt) in der Reihenfolge beim Empfänger eintreffen, in der sie der Sender abgeschickt hat. Ist dies nicht garantiert, so kann es vorkommen, dass sich Daten überholen. In Rechnernetzen müssen Protokolle sehr viele Dinge regeln. Dies beginnt bei der Codierung von Bitdaten durch elektrische oder optische Signale, setzt sich mit der Regelung des Zugriffs mehrerer Rechnerknoten auf ein gemeinsames Übertragungsmedium fort, geht bei der Routenplanung für die gesendeten Nachrichten weiter und endet bei der Steuerung der Ende-zu-Ende-Verbindungen zwischen den Kommunikationspartnern sowie der Bereitstellung spezifischer Dienste wie Mail, Zugriff auf Webseiten und so weiter. Versuchte man, all dieses durch ein einziges Protokoll zu regeln, so erhielte man sehr unübersichtliche Protokollspezifikationen und -implementationen. Um Protokollsysteme übersichtlich zu halten, verteilt man die Aufgaben daher auf mehrere Protokolle, die aufeinander aufbauen. Ein Protokoll bietet dabei Dienste an, die das nächsthöhere Protokoll nutzen kann. Das folgende Bild, das die Situation bei einem Mobiltelefon (als grob vereinfachten Ausschnitt) darstellt, illustriert diese Idee: Tastatur/ Display Protokollinstanz zum Verbindungsaufbau Tastendrücke Telefon A Telefon B Tastatur/ Display Protokollinstanz zum Verbindungsaufbau eintreffender Anruf Wunsch zum Verbindungsaufbau (bitcodiert) Schnittstelle zum Funkmodul Protokollinstanz zur Umformung Bits Funksignale und zur Signalübertragung Funkwellen Schnittstelle zum Funkmodul Wunsch zum Verbindungsaufbau (bitcodiert) Protokollinstanz zur Umformung Bits Funksignale und zur Signalübertragung Funkwellen Funknetz Das Basisprotokoll in diesem Bild regelt die Codierung und Übertragung von Bits, also von Nullen und Einsen, durch Funksignale. Eine Instanz des Protokolls auf einem Mobilgerät kommuniziert dabei mit ihrer Partnerinstanz (ihrem so genannten Peer) auf einem anderen Gerät. An Schnittstellen bieten die Protokollinstanzen nach oben entsprechende Dienste an, mit denen Bits gesendet und empfangen werden können. Das darüberliegende Protokoll befasst sich beispielsweise mit dem Aufbau von Telefonverbindungen. Es nutzt dabei die Dienste des darunterliegenden Proto-

170 4.1 Grundlegende Begriffe 169 kolls, muss sich also um den Zugriff auf das reale Funknetz nicht selbst kümmern. Die entsprechenden Protokollinstanzen auf den einzelnen Geräten haben somit den Eindruck, unmittelbar miteinander zu kommunizieren (gestrichelte Linie in der Abbildung). Sie bieten ihrerseits obere Schnittstellen, über die Benutzer Gesprächspartner anwählen können. Die beiden Protokolle sind damit, bildlich gesprochen, nach Art eines Stapels übereinander angeordnet: Ein Protokollstack (deutsch auch: Protokollstapel) ist ein hierarchisches System von Kommunikationsprotokollen, die jeweils bestimmte Teilaspekte der Datenübertragung regeln. Jedes Protokoll bietet an einer oberen Schnittstelle Dienste an, die vom nächsthöheren Protokoll genutzt werden. Dabei muss dem höheren Protokoll nur diese Schnittstelle bekannt sein; weiter unten liegende Schnittstellen und die Vorgehensweise aller niedrigeren Protokolle bleiben ihm verborgen. DEFINITION Protokollstack Die einzelnen Protokolle eines Protokollstacks werden auch als Schichten (engl.: layers) des Stacks bezeichnet Der Protokollstack des Internets Der Protokollstack des Internets umfasst in seinem Kern drei Schichten; hinzu kommen Basisprotokolle: HTTP, FTP, SMTP,..., Anwendungsprogramme Sockets Sockets TCP UDP spezifische Anwendungen Ende-zu-Ende-Kommunikation BILD 4.8 Protokollstack im Internet IP Wegewahl ( Routing ) Basisprotokolle Netzzugang des Computers reales Netz kursiv = nicht durch Internetstandards spezifiziert Die Basisprotokolle regeln den Zugang zum Übertragungsmedium (beispielsweise zum Telefonnetz, zum Ethernet oder zum WLAN) sowie die Codierung und Übertragung von Bits auf dem Kommunikationskanal. Die Internetstandards setzen voraus, dass solche Protokolle vorhanden sind, definieren diese Protokolle jedoch selbst nicht. Das Internet Protocol (IP) setzt auf die Basisprotokolle auf. Es regelt die Übertragung von Datenpaketen vom Ausgangsknoten über Zwischenknoten (Router) zum Zielknoten und stützt sich dabei auf Internetadressen (IP-Adressen), wie zum Beispiel (Benutzer können statt dieser numerischen Form auch eine

171 170 4 Kommunikation symbolische Schreibweise verwenden, wie beispielsweise IP ist insbesondere für die Wegewahl (das Routing) zuständig, leitet also die Daten über die Router zielgerichtet weiter. IP arbeitet verbindungslos und ist nicht zuverlässig. IP ist die Basis für das Transmission Control Protocol (TCP) und das User Datagram Protocol (UDP), die sich um die Ende-zu-Ende-Kommunikation zwischen Anwendungen auf Rechnerknoten kümmern. Zur Adressierung benutzen sie (in Ergänzung zu IP-Adressen, die Rechnerknoten als Ganzes angeben) Portnummern, die verschiedene Ports (vergleiche ) des Knotens und damit die jeweilige Anwendung identifizieren. Beispielsweise ist die Portnummer 80 für die HTTP-basierte Kommunikation mit Web Servern reserviert. Der grundlegende Unterschied zwischen TCP und UDP ist, dass TCP verbindungsorientiert und zuverlässig arbeitet, UDP dagegen verbindungslos und nicht zuverlässig. Zudem garantiert TCP eine sequentielle Datenübertragung, bei UDP können sich dagegen Pakete überholen. Auf die Dienste beider Protokolle kann man über Sockets ( ) zugreifen, wobei man Stream-Sockets für die verbindungsorientierte Kommunikation über TCP und Datagram-Sockets für die verbindungslose Kommunikation über UDP unterscheidet. Mit Sockets lassen sich also Internetverbindungen auf- und abbauen sowie Daten über das Internet senden und empfangen. Sockets können von den Protokollen der darüberliegenden Schicht, aber auch von Anwendungsprogrammen ( 4.2.4, 4.3.3) genutzt werden. TCP und auch UDP sind schließlich Dienstanbieter für die Protokolle der obersten Schicht, die bestimmte Anwendungen unterstützen wie zum Beispiel das Hypertext Transfer Protocol (HTTP) zum Zugriff auf Webseiten, das File Transfer Protocol (FTP) und das Simple Mail Transfer Protocol (SMTP). 4.2 Techniken in UNIX/Linux Aufgaben 4A.3.1./1./11. UNIX/Linux bietet mehrere Mechanismen an, die die verschiedenen Arten der Kommunikation aus realisieren: Shared Memory für die speicherbasierte Kommunikation, Pipes für die lokale strombasierte Kommunikation, Message Queues für die lokale nachrichtenbasierte Kommunikation sowie Sockets zur Datenübertragung über Kommunikationsnetze. Zudem können Prozesse über Dateien im gemeinsamen Dateisystem kommunizieren. Im Folgenden werden die wichtigsten Schnittstellenfunktionen zur Arbeit mit diesen Mechanismen besprochen. Wie zuvor werden nur die am häufigsten auftretenden Parameter- und Rückgabewerte der Funktionen genannt. Eine vollständige Dokumentation findet man wieder in den Manual Pages von UNIX/Linux [Linux]; sehr ausführliche Erläuterungen bieten auch hier die Lehrbücher [Stev05], [Robb03] und [Hero04].

172 4.2 Techniken in UNIX/Linux Shared Memory Speicherbereiche, auf die mehrere UNIX/Linux-Prozesse zugreifen können, werden Shared-Memory-Segmente genannt. Diese Segmente werden wie Semaphore ( ) dynamisch erzeugt und gelöscht und mit Hilfe einer Tabelle verwaltet. Prozesse können aus C-Programmen heraus über Pointer auf die Segmente zugreifen: Aufgaben 4A.3.4./ globale Shared-Memory-Tabelle Shared-Memory-Segment Speicherzellen mit Inhalten Pointer Tabellenindizes = Nummern von Shared-Memory-Segmenten Prozess A Prozess B BILD 4.9 Shared-Memory- Tabelle mit Shared- Memory-Segment API-Funktionen Grundlegend für Shared-Memory-Segmente ist die Funktion shmget(), die analog zu semget() ( ) vorgeht. Mit ihr kann ein Prozess ein neues Shared-Memory- Segment erzeugen oder sich den Tabellenindex eines bereits bestehenden Segments verschaffen: int shmget(key_t key, size_t size, int shmflg) #include <sys/ipc.h> #include <sys/shm.h> Nummer des Segments (= Index in der Shm.tabelle) 1 bei Fehler Flags zur Steuerung der Ausführung (insbes. IPC_CREAT für Erzeugung) sowie Zugriffsrechte auf das Segment Größe des Segments (in Bytes) Schlüssel des Segments oder IPC_PRIVATE (key_t und size_t sind systemhängige Ganzzahltypen) Beispielsweise erzeugt der Aufruf int shmid = shmget(ipc_private,5*sizeof(float),ipc_creat 0777); ein Shared-Memory-Segment, das fünf float-werte aufnehmen kann (aber auch Werte anderer Typen siehe unten). shmget() liefert den Index der Shared-Memory-Tabelle zurück, über den nachfolgende Operationen auf das Segment zugreifen können. Dieser Index wird nur innerhalb von Programmen benutzt. Zusätzlich kann jedes Segment (wie eine Semaphorgruppe, ) einen ganzzahligen Schlüssel besitzen. Dieser Schlüssel kann an den key-parameter von shmget() übergeben werden, wenn ein Programm auf ein Segment zugreifen will, das zuvor durch ein anderes Programm unter Angabe dieses Schlüssels erzeugt wurde. Soll kein Schlüssel benannt werden, übergibt man IPC_PRIVATE. Damit ergeben sich dieselben Möglichkeiten wie bei Semaphorgruppen ( ).

173 172 4 Kommunikation Mit der Funktion shmat() (at = attach ) kann ein Prozess ein Shared-Memory-Segment in seinen Adressraum einbinden. Er erhält einen Pointer (nämlich die Adresse der ersten Speicherzelle des Segments) zurück, über den er anschließend auf das Segment zugreifen kann: #include <sys/types.h> #include <sys/shm.h> Die Operation, die shmctl() ausführen soll, wird durch ihren cmd-parameter bestimmt, eine symbolische Konstante. Wesentlich ist hier das Kommando IPC_RMID, mit dem man ein Segment löscht: shmctl(shmid,ipc_rmid,0). Werden Shared-Memory-Segmente nicht explizit gelöscht, so bleiben sie über das Ende der Prozessausführung hinaus bestehen. Es ergeben sich damit dieselben Problevoid *shmat(int shmid, const void *shmaddr, int shmflg) Adresse der ersten Speicherzelle des Segments 1 bei Fehler üblicherweise 0 Flags zur Steuerung der Ausführung (meist 0) Nummer des Segments (= Index in der Shared-Memory-Tabelle) Beispielsweise beschafft sich ein Prozess durch das Codefragment float *f_pointer = (float *) shmat(shmid,0,0); *f_pointer = 0.5; *(f_pointer+1) = 1.0; einen Pointer auf den Anfang des Segments, das oben erzeugt wurde, und schreibt dann an dessen erste Position den Wert 0.5 und an die zweite Position den Wert 1.0. Das Segment kann somit wie ein Array benutzt werden; es wäre also auch die Index- Schreibweise f_pointer[0], f_pointer[1],... möglich. Der shmat()-rückgabetyp void * macht deutlich, dass die Daten in einem Shared- Memory-Segment nicht getypt sind. Man kann also den Bitmustern in einem Segment nicht ansehen, welche Art von Daten sie codieren, und auch die Shared-Memory-Tabelle legt in dieser Hinsicht nichts fest. Shared-Memory-Segmente können damit flexibel zur Übertragung von Daten unterschiedlicher Typen benutzt werden. Dabei besteht aber die Gefahr, dass der Leseprozess die Daten anders interpretiert als der Schreibprozess (zum Beispiel: Schreiber schreibt einen double-wert, Leser liest acht char-werte). Der oder die Programmierer müssen selbst darauf achten, dass dies nicht geschieht; das Betriebssystem bietet hier keine Unterstützung. Die Funktion shmctl() ermöglicht verschiedene Steuerungsoperationen auf Shared- Memory-Segmenten: #include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf) Rückgabewert abhängig von der ausgeführten Operation 1 bei Fehler Parameter für die Operation auszuführende Operation Nummer des Segments (= Index in der Shared-Memory-Tabelle)

174 4.2 Techniken in UNIX/Linux 173 me wie bei nicht gelöschten Semaphoren ( ). Man muss also auch hier gegebenenfalls Segmente durch Benutzerkommandos entfernen: Mit dem Befehl ipcs -m stellt man die Nummern der existierenden Shared-Memory-Segmente fest und löscht sie anschließend mit ipcrm -m nummer Programmbeispiel: Erzeuger-Verbraucher-System Das folgende Programmbeispiel zeigt ein Erzeuger-Verbraucher-System, in dem die Daten über ein Shared-Memory-Segment übertragen werden. Die nötigen Synchronisationsoperationen sind lediglich durch Kommentare angedeutet; sie können, wie in gezeigt, durch Semaphorperationen implementiert werden. #include <sys/shm.h> #include <stdio.h> #include <stdlib.h> #define PUFFERKAP 3 /* Kapazität des Puffers */ #define ANZAHL_RUNDEN 10 /* Anzahl der Erzeugungs- bzw. Verbrauchsvorgänge */ main() { int shmid; /* Nummer des Shared-Memory-Segments */ float *shmptr; /* Pointer auf das Segment */ int ix; /* Lese- bzw. Schreibindex im Segment */ float zwisch; /* Zwischenspeicher */ int i; /* Schleifenzähler */ int status; /* Rückgabeparameter für wait() */ /* Erzeugung des Shared-Memory-Segments */ shmid = shmget(ipc_private,pufferkap*sizeof(float),ipc_creat 0777); /* Verbraucher-Prozess */ if (fork()==0) { ix = 0; /* Leseindex auf Segmentanfang setzen */ shmptr = (float *) shmat(shmid,0,0); /* Pointer auf Segment */ for (i=0;i<anzahl_runden;i++) { /*... hier Blockierung, solange der Puffer leer ist oder durch den anderen Prozess benutzt wird... */ zwisch = *(shmptr+ix); /* Lesen aus der ix-ten Segmentpos. */ ix = (ix+1)%pufferkap; /* Weiterschalten des Index */ printf("gelesen: %f\n",zwisch); exit(0); /* Erzeuger-Prozess */ if (fork()==0) { ix = 0; /* Schreibindex auf Segmentanfang setzen */ PROG 4.1 Erzeuger-Verbraucher-System mit Shared Memory

175 174 4 Kommunikation shmptr = (float *) shmat(shmid,0,0); /* Pointer auf Segment */ for (i=0;i<anzahl_runden;i++) { zwisch = 0.5*i; /* zu schreibender Wert (im Prinzip beliebig) */ /*... hier Blockierung, solange der Puffer voll ist oder durch den anderen Prozess benutzt wird... */ *(shmptr+ix) = zwisch; /* Schreiben an die ix-te Segmentpos. */ ix = (ix+1)%pufferkap; /* Zyklisches Weiterschalten des Index */ exit(0); /* Warten auf das Ende von Erzeuger und Verbraucher */ wait(&status); wait(&status); /* Löschung des Shared-Memory-Segments */ shmctl(shmid,ipc_rmid,0); Bei der Programmierung ist unbedingt sicherzustellen, dass nicht über das Segmentende hinaus zugegriffen wird! Wie bei Arrays in C findet auch bei Shared-Memory- Segmenten keine automatische Längenprüfung statt, so dass Speicherverletzungen nicht ausgeschlossen sind Pipes Aufgaben 4A.3.3./6. BILD 4.10 Pipe Zur lokalen strombasierten Kommunikation werden Pipes benutzt. Pipes sind von der Benutzerschnittstelle eines Betriebssystems her bekannt: Man kann hier zwei Kommandos durch den Pipe-Operator miteinander verknüpfen (zum Beispiel ls -l more), wodurch die Daten der Standardausgabe des ersten Kommandos in die Standardeingabe des zweiten fließen. Bildlich kann man sich eine Pipe wie eine Rohrleitung vorstellen (der englische Name besagt es), durch die ein Bitstrom von einem Schreibende zu einem Leseende fließt: Pipe Bitstrom Schreibprozess Leseprozess Schreibende Leseende Wie das Bild zeigt, verläuft die Datenübertragung unidirektional, also nur in einer Richtung. Der Leseprozess darf also nicht dieselbe Pipe benutzen, um Daten an den Schreibprozess zurückschicken hierzu müsste eine zweite Pipe eingerichtet werden ( PROG 4.3)! UNIX/Linux unterscheidet zwischen benannten und unbenannten Pipes.

176 4.2 Techniken in UNIX/Linux Benannte Pipes Benannte Pipes haben Namen, die im Dateisystem verzeichnet sind. Über diese Namen können prinzipiell beliebige Prozesse auf sie zugreifen. In der Ausgabe des Kommandos ls -l sind Pipes durch den Kennbuchstaben p gekennzeichnet: prw-rw-rw- 1 gonzo muppets :03 PIPE_1 prw-rw-rw- 1 gonzo muppets :05 PIPE_2 -rwxr-xr-x 1 gonzo muppets :22 normale_datei Eine benannte Pipe wird mit der Funktion mkfifo() erzeugt: #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> main() { char buffer[6]; /* Speicher für die empfangenen Daten */ int fd; /* Deskriptor für die Pipe */ fd=open("pipe_1",o_rdonly); /* Öffnen der Pipe zum Lesen */ read(fd,buffer,6); /* Lesen von 6 Zeichen aus der Pipe */ printf("gelesen: %s\n",buffer); /* Ausgabe hier: HALLO */ unlink("pipe_1"); /* Löschen der Pipe */ Das Programmbeispiel hat die Besonderheit, dass der Sender seinen Text in zwei Teilen abschickt, der Empfänger ihn aber in einem Stück liest. Damit illustriert es die Eiint mkfifo(const char *pathname, mode_t mode) mode_t ist ein systemabhängiger Ganzzahltyp 0 bei Erfolg 1 bei Fehler Name der Pipe Zugriffsrechte auf die Pipe Auf einer benannten Pipe arbeitet man mit Standard-Dateioperationen: open() öffnet die Pipe für Zugriffe, write() schreibt Daten in sie, read() liest Daten aus ihr und unlink() löscht sie. Das folgende Programmbeispiel zeigt zwei C-Programme, zwischen denen eine Zeichenfolge über eine Pipe namens PIPE_1 übertragen wird: Datei sender.c: Programm des Sendeprozesses (zuerst zu starten) #include <fcntl.h> main() { int fd; /* Deskriptor für die Pipe */ mkfifo("pipe_1",0666); /* Erzeugung der benannten Pipe */ fd=open("pipe_1",o_wronly); /* Öffnen der Pipe zum Schreiben */ write(fd,"hal",3); /* Schreiben zweier Zeichenfolgen */ write(fd,"lo",3); /* in die Pipe (inkl. Stringendezeichen \0) */ Datei empfaenger.c: Programm des Empfängerprozesses (anschließend zu starten) PROG 4.2 Datenkommunikation über eine benannte Pipe

177 176 4 Kommunikation genschaft von Pipes, Datenströme zu übertragen also keine Nachrichten, die durch Grenzen voneinander getrennt sind. Die abschließende Operation unlink() löscht die Pipe aus dem Dateisystem. Ließe man sie weg, so bliebe die Pipe auch nach dem Ende des Programms bestehen. Übrigens wird ein Prozess, der eine Pipe zum Schreiben öffnet, blockiert, bis die Pipe auch zum Lesen geöffnet wird. Daten können also erst abgeschickt werden, wenn der Empfänger aktiv ist ein längeres Zwischenspeichern wird damit vermieden. Die Datenübertragung selbst erfolgt allerdings asynchron über einen Zwischenpuffer die write()-operation blockiert also nicht bis zum Aufruf von read() Unbenannte Pipes Unbenannte Pipes haben, im Gegensatz zu benannten, keine Namen im Dateisystem. Sie werden über zwei Deskriptoren identifiziert, die dem erzeugenden Prozess übergeben werden, und sind damit nur für diesen Prozess und seine Nachkommen (also seine Söhne, deren Söhne usw.) zugreifbar. Zur Erzeugung unbenannter Pipes dient die Funktion pipe(): int pipe(int pipefd[2]) #include <unistd.h> 0 bei Erfolg 1 bei Fehler Pipe-Deskriptoren (Rückgabeparameter): filedes[0]: Leseende der Pipe filedes[1]: Schreibende der Pipe Das folgende Programmbeispiel zeigt, wie Vater- und Sohnprozess in zwei Richtungen über zwei unbenannte Pipes miteinander kommunizieren: PROG 4.3 Verschicken von Daten und einer Rückmeldung über zwei unbenannte Pipes #include <stdio.h> #include <stdlib.h> main() { char buffer[20]; /* Puffer zum Datenempfang */ int fda[2], fdb[2]; /* Deskriptoren für Leseenden (fdx[0]) */ /* und Schreibenden (fdx[1]) der Pipes */ pipe(fda); /* Erzeugung zweier unbenannter Pipes; Speichern der */ pipe(fdb); /* Deskriptoren für Lese-/Schreibenden in fda und fdb */ if (fork()==0) { /* Sohn: sendet Stringnachricht und empfängt Rückantwort */ close(fda[0]); /* nicht benötigte Lese- und */ close(fdb[1]); /* Schreibdeskriptoren schließen */ /* 6 Bytes in Pipe A schreiben (5 Zeichen + Ende-Zeichen \0) */ write(fda[1],"hallo",6); /* Rückantwort aus Pipe B lesen und ausgeben */ read(fdb[0],buffer,20); printf("\nsohn liest %s aus der Pipe B\n\n",buffer); exit(0);

178 4.2 Techniken in UNIX/Linux 177 /* Vater: empfängt Stringnachricht und sendet Rückantwort */ close(fda[1]); /* nicht benötigte Lese- und */ close(fdb[0]); /* Schreibdeskriptoren schließen */ /* String aus Pipe A lesen und ausgeben */ read(fda[0],buffer,20); printf("\nvater liest %s aus der Pipe A\n\n",buffer); /* Rückantwort in Pipe B schreiben */ write(fdb[1],"hallo ZURUECK",14); Message Queues Message Queues dienen unter UNIX/Linux zur lokalen nachrichtenbasierten Kommunikation. Im Unterschied zu Pipes, die Datenströme transportieren, sind die übertragenen Daten also in einzelne Nachrichten untergliedert und werden Nachricht für Nachricht gesendet und empfangen. Eine Message Queue realisiert damit in der Terminologie von eine Mailbox oder einen Port. Message Queues werden analog zu Semaphoren ( 3.5.3) und Shared Memory ( 4.2.1) geführt; sie werden also mit Hilfe einer Tabelle verwaltet und können dynamisch erzeugt und gelöscht werden: globale Message-Queue-Tabelle Message Queue mit einer Nachricht Tabellenindizes = Nummern von Message Queues Message Queue mit drei Nachrichten BILD 4.11 Message-Queue- Tabelle mit Nachrichten in Queues Wie das Bild zeigt, ist eine Message Queue entweder leer oder sie enthält eine Liste von Nachrichten. Die Listen werden im Prinzip nach dem First-In-First-Out-Prinzip verwaltet; Nachrichten werden also vom Listenanfang gelesen (und dabei aus der Liste entfernt), und neue Nachrichten werden an das Listenende angefügt. Daneben ist es aber möglich, bestimmte Nachrichten bevorzugt zu lesen, also auch von anderen Positionen als dem Listenanfang zu holen ( ). Die Struktur der Nachrichten ist nicht vorgegeben, sondern kann vom Programmierer frei festgelegt werden API-Funktionen: Erzeugen und Löschen Wie für Semaphore und Shared-Memory-Segmente gibt es auch für Message Queues eine grundlegende Get-Funktion: msgget() erzeugt eine Queue neu oder greift auf eine bereits bestehende Queue zu. Zurückgeliefert wird jeweils der Index in der Message-Queue-Tabelle: Aufgabe 4A.3.3.

179 178 4 Kommunikation #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget(key_t key, int msgflg) Nummer der Queue (= Index in der Tabelle) 1 bei Fehler Flags zur Steuerung der Ausführung (insbesondere IPC_CREAT für Erzeugung) sowie Zugriffsrechte auf die Queue Schlüssel der Queue oder IPC_PRIVATE (key_t ist ein systemabhängiger Ganzzahltyp) Analog zu semget() ( ) und shmget() ( ) kann über den ersten msgget()-parameter entweder die Konstante IPC_PRIVATE oder ein Queue-Schlüssel übergeben werden. Damit bieten sich auch hier die Möglichkeiten, die bei der Einführung von semget() diskutiert wurden. msgctl() ist eine Steuerungsfunktion für Message Queues, die man analog zu semctl() ( ) und shmctl() ( ) einsetzt: #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgctl(int msqid, int cmd, struct msqid_ds *buf) Rückgabewert abhängig von der ausgeführten Operation -1 bei Fehler Nummer der Queue (= Index in der Queue-Tabelle) Parameter für die Operation auszuführende Operation Der wichtigste cmd-parameter ist wiederum IPC_RMID, mit dem eine Message Queue gelöscht wird: msgctl(msgid,ipc_rmid,0). Für Message Queues besteht dieselbe Gefahr wie für Semaphore und Shared-Memory-Segmente: Werden sie nicht explizit gelöscht, so bleiben sie über das Ende der Prozessausführung hinaus bestehen. Man muss also gegebenenfalls auch Queues durch Benutzerkommandos entfernen: Mit dem Befehl ipcs -q stellt man die Nummern der existierenden Queues fest und löscht sie anschließend mit ipcrm -q nummer. Aufgabe 4A API-Funktionen: Senden und Empfangen Nachrichten, die über Message Queues übertragen werden, sind aus Sicht des Betriebssystems (im Prinzip) beliebige Bitmuster. Aus Sicht des Anwendungsprogrammierers sind Nachrichten C-struct-Variablen, die einen (fast) beliebigen Aufbau haben können. Eine Nachricht könnte also beispielsweise wie folgt aussehen: struct bestellung { long mtype; char warenname[20]; int kennziffer; float preis; meinebestellung; Als einzige Einschränkung gilt, dass die erste Komponente einer Nachricht vom Typ long sein muss (wie im Beispiel zu sehen). Diese Komponente ist der Typ der

180 Free ebooks ==> Techniken in UNIX/Linux 179 Nachricht. Er wird beim Lesen von Nachrichten ausgewertet und erlaubt dabei dem Anwendungsprogrammierer, gezielt Nachrichten eines bestimmten Typs zu empfangen. Hierdurch kann man beispielsweise wichtige Nachrichten bevorzugt aus der Queue lesen ( ). Die Bedeutung die einzelnen Typnummern legt allein der Programmierer fest; UNIX/Linux gibt in dieser Hinsicht nichts vor. Der Typwert in einer Nachricht muss stets echt größer als 0 sein auch dann, wenn das Programm die Typen ansonsten nicht weiter berücksichtigt. Nachrichten mit Typeintrag 0 werden nicht korrekt übertragen. Zum Senden und Empfangen von Nachrichten dient das Funktionenpaar msgsnd() und msgrcv(): #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgsnd( int msqid, const void *msgp, size_t msgsz, int msgflg) 0 bei Erfolg 1 bei Fehler Nummer der Queue (= Index der Queue-Tabelle) zu versendende Nachricht Größe der Nachricht (in Bytes, ohne die erste long-komponente) Flags zur Steuerung der Ausführung (insbes. IPC_NOWAIT für nichtblockierendes Senden) ssize_t msgrcv( int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg) Nummer der Queue (= Index der Queue-Tabelle) empfangene Nachricht (Rückgabeparameter) maximal zulässige Größe der empfangenen Nachricht (in Bytes, ohne die erste long-komponente) gewünschter Typ der empfangenen Nachricht (0, wenn Nachrichten aller Typen akzeptiert werden) Flags zur Steuerung der Ausführung (insbes. IPC_NOWAIT für nichtblockierendes Empfangen) Größe der empfangenen Nachricht (in Bytes, ohne die erste long-komponente) oder 1 bei Fehler (ssize_t ist ein systemabhängiger Ganzzahltyp) msgsnd() verschickt also eine Nachricht, die zuvor in einer struct-variablen zusammengestellt wurde. Für die oben deklarierte Variable meinebestellung könnte das wie folgt aussehen: meinebestellung.mtype = 1; strcpy(meinebestellung.warenname,"usb-stick"); meinebestellung.kennziffer = 1234; meinebestellung.preis = 9.95; msgsnd(msqid,&meinebestellung, sizeof(meinebestellung)-sizeof(long),0); Bei der Ausführung von msgsnd() wird die Nachricht in einen Speicherbereich des Betriebssystemkerns kopiert. Die übergebene Variable wird also wieder frei zur Zusammenstellung der nächsten Nachricht; insbesondere kann eine einmal abgeschickte Nachricht über sie nicht nachträglich verändert werden. Das Betriebssystem fügt die Nachricht an das Ende der Queue an und entblockiert alle Prozesse, die auf eine Nach-

181 180 4 Kommunikation richt dieses Typs warten (siehe unten). Es kommt dann zu einem Wettrennen: Ein Prozess gewinnt das Rennen und erhält die Nachricht, die anderen blockieren sich wieder. Die Länge einer Nachricht sollte stets mit dem Ausdruck sizeof(variablenname)- sizeof(long) berechnet werden. Addiert man nämlich die Längen ihrer einzelnen Komponenten auf, so ist der resultierende Wert möglicherweise zu klein, da die Struktur aufgrund von Füllbytes größer als die Summe der Einzelgrößen sein kann. Eine Message Queue kann nur eine bestimmte Maximalzahl von Bytes aufnehmen (wobei man diese Grenze durch msgctl() ändern kann). Würde mit der neuen Nachricht dieser Wert überschritten und ist das Flag IPC_NOWAIT nicht gesetzt, so blockiert der sendende Prozess, bis wieder genug Platz frei ist. Bei gesetztem IPC_NOWAIT-Flag (also msgsnd(...,ipc_nowait) endet der Aufruf sofort mit Rückgabe von 1. msgrcv() empfängt (und entfernt) die erste Nachricht aus der Queue, deren Typeintrag mit dem gewünschten zu empfangenden Typ übereinstimmt (siehe unten). Beim Aufruf übergibt man einen Zeiger auf eine struct-variable, in die das Betriebssystem dann den Inhalt der Nachricht kopiert beispielsweise so: struct bestellung deinebestellung; msgrcv(msqid,&deinebestellung, sizeof(deinebestellung)-sizeof(long),0,0); Natürlich ist streng darauf zu achten, dass die Variablen, die bei msgsnd() und msgrcv() übergeben werden, in ihrem Aufbau übereinstimmen. UNIX/Linux überträgt nur den reinen Byte-Inhalt einer Nachricht und kann daher nicht prüfen, ob der Empfänger die Bytes korrekt interpretiert. Wichtig zur Steuerung der Übertragung sind die drei letzten Parameter von msgrcv(): Der Parameter msgsz verhindert einen Speicherüberlauf, da das Betriebssytem nie mehr Bytes in die struct-variable übertragen wird als hier angegeben. Ist das MSG_NOERROR-Flag im letzten Parameter gesetzt, so wird der Inhalt der Nachricht entsprechend abgeschnitten; ansonsten verbleibt die Nachricht in der Queue, und msgrcv() kehrt mit dem Rückgabewert 1 zurück. Der Parameter msgtyp gibt den Typ an, den die empfangene Nachricht haben soll. Ist sein Wert gleich 0, so wird jede Nachricht akzeptiert. Ist er größer als 0, so wird die erste Nachricht in der Queue empfangen, die diesen Typeintrag hat. Diese Nachricht muss nicht unbedingt an der Queue-Spitze stehen, sondern kann auch von weiter hinter geholt werden. Hierdurch kann man beispielsweise Nachrichten mit Prioritäten versehen ( ). Der Parameter msgflg legt unter anderem fest, was geschehen soll, wenn keine passende Nachricht vorliegt. Ist hier das IPC_NOWAIT-Flag gesetzt (d.h. wird die Konstante IPC_NOWAIT als Parameter übergeben), so kehrt der Aufruf sofort mit dem Rückgabewert 1 zurück. Der Prozess kann dann später nochmals versuchen, eine Nachricht zu empfangen, und somit aktiv warten ( 3.2.1). Ist IPC_NOWAIT nicht gesetzt, so wird der Prozess blockiert und wartet damit passiv. Wie man IPC_NOWAIT zum prioritätengesteuerten Empfangen von Nachrichten einsetzt, wird gleich in gezeigt.

182 4.2 Techniken in UNIX/Linux Programmbeispiel: Erzeuger-Verbraucher-System Als Programmbeispiel dient wieder das Erzeuger-Verbraucher-System. Ein Sendeprozess sendet eine Reihe von Bestellungen ( ) in eine Message Queue, und ein Empfangsprozess liest sie von dort aus: #include <sys/ipc.h> #include <sys/msg.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> main() { int msqid, verbr_id, i, status; struct bestellung { /* Typ für Bestellungen */ long mtype; char warenname[20]; int kennziffer; float preis; ; /* Vater: Erzeugt die Message Queue */ msqid = msgget(ipc_private,ipc_creat 0777); /* Erzeuger: Schickt in Sekundenabständen fünf Bestellungen ab */ if (fork()==0) { struct bestellung meinebestellung; /* zu sendende Bestellung */ for (i=0;i<5;i++) { meinebestellung.mtype = 1; strcpy(meinebestellung.warenname,"..."); meinebestellung.kennziffer =...; meinebestellung.preis =...; msgsnd(msqid,&meinebestellung, sizeof(meinebestellung)-sizeof(long),0); sleep(1); /* Verzögerung bis zum nächsten Senden */ exit(0); /* Verbraucher: Nimmt beliebig viele Bestellungen entgegen */ if ((verbr_id=fork())==0) { struct bestellung deinebestellung; /* zum Empfang e. Bestellung */ while(1) { msgrcv(msqid,&deinebestellung, sizeof(deinebestellung)-sizeof(long),0,0); printf("gelesen: %s %d %f\n", deinebestellung.warenname, deinebestellung.kennziffer, deinebestellung.preis); PROG 4.4 Erzeuger-Verbraucher-System mit Message Queue

183 182 4 Kommunikation /* Vater: Räumt auf */ wait(&status); /* Warten auf Ende des Erzeugers */ kill(verbr_id,sigkill); /* Terminieren des Verbrauchers */ msgctl(msqid,ipc_rmid,0); /* Löschen der Message Queue */ Das System kann so erweitert werden, dass Nachrichten prioritätengesteuert gesendet und empfangen werden. Dazu versieht der Sender dringende Nachrichten zum Beispiel mit dem Typwert 2 und normale Nachrichten mit dem Typwert 1. Beim Empfänger sieht dann der Empfangsvorgang wie folgt aus: if (msgrcv(msqid,&deinebestellung,...,2,ipc_nowait)==-1) msgrcv(msqid,&deinebestellung,...,0,0); Der Empfänger versucht also zunächst, eine Nachricht des Typs 2 zu bekommen, also eine dringende Nachricht. Liegt eine solche Nachricht tatsächlich vor, so liest er sie aus der Queue und arbeitet mit ihr weiter. Wartet keine Typ-2-Nachricht, so kehrt der erste msgrcv()-aufruf mit dem Rückgabewert 1 zurück. Der Empfänger versucht dann, eine beliebige Nachricht zu lesen. Gelingt auch dies nicht, so wird er blockiert, bis eine Nachricht eintrifft. Man beachte, dass die Vorgehensweise if (msgrcv(msqid,&deinebestellung,...,2,ipc_nowait)==-1) msgrcv(msqid,&deinebestellung,...,1,0); nicht zum Ziel führt. Erstens kann zwischen den beiden msgrcv()-aufrufen eine Typ- 2-Nachricht eintreffen, die dann nicht empfangen würde. Zweitens würde eine Typ-2- Nachricht, die später eintrifft, den blockierten Prozess nicht wieder wecken Sockets Zum Zugriff auf Kommunikationsnetze und ihre Protokollstacks bietet UNIX/Linux einen Socket-Mechanismus ( , als vertiefende Literatur siehe zum Beispiel [Come01] und [Poll09]). Mit ihm lassen sich Daten zwischen verschiedenen Rechnerknoten, die durchaus heterogen (also unterschiedlich) sein können, übertragen. Auch Prozesse, die auf demselben Computer laufen, können über Sockets kommunizieren. Es kann zwischen einer nachrichten- und einer strombasierten Datenübertragung ( 4.1.1) gewählt werden Domains und Typen Jeder UNIX/Linux-Socket gehört einer Domain an und hat einen Typ. Die Socket-Domain gibt an, ob die kommunizierenden Prozesse auf demselben oder auf verschiedenen Rechnerknoten laufen: Die Internet-Domain umfasst die Sockets, die zur Kommunikation über das Internet benutzt werden, also von Prozessen, die auf verschiedenen Rechnerknoten laufen (vergleiche BILD 4.6). Internet-Domain-Sockets werden durch eine Kombination aus IP-Adresse und Portnummer ( ) identifiziert.

184 4.2 Techniken in UNIX/Linux 183 Die UNIX-Domain umfasst die Sockets, über die UNIX/Linux-Prozesse auf demselben Rechnerknoten kommunizieren. UNIX-Domain-Sockets werden mit Namen im Dateisystem geführt und in der Ausgabe von ls -l durch den Buchstaben s gekennzeichnet (vergleiche die Ausgabe zu benannten Pipes, ). Der Socket-Typ legt die Art der Datenübertragung fest. Unter anderem gibt es Stream- und Datagram-Sockets: Sockets des Stream-Typs ( Stream-Sockets ) dienen zur strombasierten Datenkommunikation. Sie übertragen Daten üblicherweise über TCP ( ), also verbindungsorientiert, zuverlässig und sequentiell. Sockets des Datagram-Typs ( Datagram-Sockets ) dienen zur nachrichtenbasierten Datenkommunikation. Sie übertragen Daten üblicherweise über UDP ( ), also verbindungslos und nicht notwendigerweise zuverlässig oder sequentiell. Man kann bei der Definition eines Sockets diese beiden Typen und die Domains beliebig miteinander kombinieren (siehe Beispiel in und ). Selbstverständlich müssen aber die Sockets eines Socketpaars, über das Daten übertragen werden ( BILD 4.6), in ihren Typen und Domains übereinstimmen: Rechnerknoten Socket (UNIX/ Datagram) Socket (UNIX/ Stream) Prozess 1 Socket (Internet/ Datagram) Socket (Internet/ Stream) Rechnerknoten Socket (Internet/ Stream) Prozess 3 Socket (Internet/ Datagram) BILD 4.12 Sockets unterschiedlicher Typen und Domains Socket (UNIX/ Datagram) Prozess 2 Socket (UNIX/ Stream) Rechnernetz/Internet API-Funktionen: Übersicht UNIX/Linux stellt ein Bündel von Funktionen bereit, mit denen Sockets erzeugt, benutzt und wieder geschlossen werden können. Im Folgenden soll zunächst ein kurzer Überblick über die Anwendung dieser Funktionen und die entsprechenden zeitlichen Abläufe gegeben werden. Die nachfolgenden Abschnitte erläutern dann die einzelnen Funktionen detaillierter und geben vollständige Programmbeispiele. Grundlegend für die Kommunikation über ein Socketpaar ist das Client-Server-Modell, das in allgemein besprochen wird: Ein Server (Dienstanbieter) bietet ei- Aufgaben 4A.3.9./10.

185 184 4 Kommunikation BILD 4.13 Zeitlicher Ablauf bei Stream-Sockets nen Server-Socket unter einer bestimmten Adresse an; ein Client (Dienstnutzer) baut von seine, Client-Socket eine Verbindung zu einem Server-Socket auf oder schickt unmittelbar Daten an ihn ab. Die folgende Abbildung illustriert den zeitlichen Ablauf bei der Datenübertragung über ein Paar von Stream-Sockets: 1. Erzeugung des Server-Sockets: socket(), bind(), listen() 1. Server-Socket zur Kontaktaufnahme Server- Prozess 4. Server-Socket zur Kommun. mit dem Client 2. Erzeugung des 3.) Wunsch zum Client-Sockets: Verbindungsaufbau: socket() connect() 4. Verbindung: accept() 2. Client-Socket 5. Datenübertragung: send()/recv(), write()/read() 6. Schließen des Sockets: close() Client- Prozess Server-Prozess (als Empfänger) Client-Prozess (als Sender) socket() bind() listen() accept() read() close() socket() connect() write() close() connect() muss nach listen(), kann aber vor accept() aufgerufen werden. Wird accept() vor connect() aufgerufen, so blockiert der accept()-aufruf bis zum connect()-aufruf. Wird read() vor write() aufgerufen, so blockiert der read()-aufruf bis zum write()-aufruf. Wird write() vor read() aufgerufen, so werden die Daten in einen Zwischenspeicher geschrieben, und write() blockiert nicht, es sei denn, der Speicher ist voll. Der Server (als primärer Empfänger von Daten) erzeugt durch Aufruf von socket() einen Stream-Socket, bindet mit bind() einen Namen oder eine Adresse an ihn und initialisiert mit listen() eine Warteschlange für eintreffende Wünsche zum Verbindungsaufbau. Der Client (als primärer Sender von Daten) erzeugt seinerseits einen Stream-Socket, indem er ebenfalls socket() aufruft. Mit connect() richtet er dann einen Verbindungsaufbauwunsch an den Server, den dieser mit accept() entgegennimmt bildlich gesprochen ruft also der Client den Server an, und der Server nimmt diesen Anruf entgegen. Dabei erzeugt der Server einen weiteren Socket und verbindet ihn mit der Client-Socket. Über diese Verbindung läuft anschließend der Datenverkehr mit dem Client ab; der ursprüngliche Server-Socket bleibt frei für den Aufbau weiterer Verbindungen. Zur Datenübertragung (auch bidirektional, also in

186 4.2 Techniken in UNIX/Linux 185 beide Richtungen) dienen dann write()/read()- oder send()/recv()-aufrufe. Schließlich werden die Sockets durch close() geschlossen. Server-Prozess (als Empfänger) socket() bind() recvfrom() close() Client-Prozess (als Sender) socket() sendto() close() sendto() muss nach bind(), kann aber vor recvfrom() aufgerufen werden. Wird recvfrom() vor sendto() aufgerufen, so blockiert der recvfrom()-aufruf bis zum sendto()-aufruf. Wird sendto() vor recvfrom() aufgerufen, so werden die Daten in einen Zwischenspeicher geschrieben, und sendto() blockiert nicht (ausreichend freier Speicher vorausgesetzt). BILD 4.14 Zeitlicher Ablauf bei Datagram-Sockets Bei Datagram-Sockets ist der Ablauf deutlich einfacher: Auch hier erzeugen Server und Client mit socket() je einen Socket, wobei der Server seinen Socket durch bind() mit einer Adresse oder einem Namen versieht. Danach können jedoch sofort mit sendto()/recvfrom() Daten übertragen werden. Hier werden also Nachrichten (so genannte Datagramme) verschickt, ohne dafür zuvor eine Verbindung aufzubauen API-Funktionen: Erzeugen und Schließen Zur Erzeugung von Sockets der verschiedenen Typen und Domains dient die Funktion socket(): #include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol) Socket-Deskriptor (ganzzahliger Identifikator) 1 bei Fehler zu verwendendes Protokoll (0, wenn nicht festgelegt) Socket-Typ (u.a. SOCK_STREAM, SOCK_DGRAM) Socket-Domain (u.a. AF_INET, AF_UNIX) Der zurückgelieferte Socket-Deskriptor wird in nachfolgenden Funktionsaufrufen verwendet, um den Socket zu identifizieren. Mit bind() kann einem Socket eine Adresse gegeben werden. Dies ist insbesondere bei Server-Sockets erforderlich, da sie von Clients adressiert werden müssen: #include <sys/types.h> #include <sys/socket.h> int bind( int sockfd, const struct sockaddr *addr, socklen_t addrlen) 0 bei erfolgreicher Ausführung 1 bei Fehler Socket-Deskriptor (wie von socket() geliefert) Adresse, die an den Socket gebunden wird (Domain-abhängig, siehe Beispiele unten) Länge von *addr (in Bytes) socklen_t ist ein systemabhängiger Ganzzahltyp

187 186 4 Kommunikation Die Form der Adresse hängt von der Socket-Domain ab: In der UNIX-Domain werden Sockets durch Namen, die wie Dateinamen aufgebaut sind, identifiziert. Hier wird bind() beispielsweise wie folgt aufgerufen: struct sockaddr address; address.sa_family = AF_UNIX; strcpy(address.sa_data,"serversocket"); bind(socketdeskriptor,&address,sizeof(struct sockaddr)); Der Typ struct sockaddr ist in der Header-Datei sys/socket.h definiert. In der Internet-Domain besteht eine Socket-Adresse aus einer IP-Adresse, die einen Rechnerknoten identifiziert, sowie einer Portnummer. struct sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = inet_addr(" "); address.sin_port = htons(55555); /* htons(): Umwandlung in ein netzweit lesbares Format. */ bind(socketdeskriptor,(struct sockaddr *) &address, sizeof(struct sockaddr)); Die Portnummer sollte stets aus dem Bereich der frei verfügbaren Dynamic Ports zwischen und gewählt werden. Der Typ struct sockaddr_in ist in der Header-Datei netinet/in.h definiert. Alternativ kann die Knotenadresse symbolisch angegeben werden, muss dann aber explizit in die numerische Form umgewandelt werden, wozu die Funktion gethostbyname() benutzt wird: struct sockaddr_in address; address.sin_family = AF_INET; struct hostent *host; host = gethostbyname(" bcopy(host->h_addr,&server_addr.sin_addr,host->h_length); address.sin_port = htons(55555); bind(socketdeskriptor,(struct sockaddr *) &address, sizeof(struct sockaddr)); Der Typ struct hostent ist in der Header-Datei netdb.h definiert. listen() kennzeichnet einen Socket als Server-Socket, also als einen Socket, an den Wünsche zum Verbindungsaufbau gerichtet werden können. Dabei wird eine Warteschlange aufgebaut, in der Clients, die connect() aufgerufen haben, auf die accept()-aufrufe des Servers warten: int listen(int sockfd, int backlog) #include <sys/types.h> #include <sys/socket.h> maximale Länge der Warteschlange Socket-Deskriptor 0 bei erfolgreicher Ausführung; 1 bei Fehler

188 4.2 Techniken in UNIX/Linux 187 Sockets werden mit close() oder shutdown() geschlossen; als Parameter erhalten diese Funktionen jeweils den Socket-Deskriptor. Zudem muss ein Server am Ende seines Programm seine lokalen Sockets (also UNIX-Domain-Sockets) mit unlink("socketname") aus dem Dateisystem entfernen. Insbesondere nach Programmabstürzen kann es sein, dass Sockets der UNIX-Domain nicht korrekt gelöscht wurden, sondern noch im Dateisystem verzeichnet sind. Sie müssen vor einer erneuten Ausführung unbedingt per rm-befehl entfernt werden, da sonst das Programm nicht korrekt abläuft API-Funktionen: Verbinden und Kommunizieren Zwischen Stream-Sockets muss zunächst eine Verbindung aufgebaut werden, bevor Daten zwischen ihnen übertragen werden können. Hierzu dient das Funktionenpaar connect() und accept(). Der Client ruft connect() auf, um seinen Socket mit einem Server-Socket zu verbinden; der Server nimmt diesen Wunsch in einem accept()-aufruf entgegen ( BILD 4.13): #include <sys/types.h> #include <sys/socket.h> int connect( int sockfd, const struct sockaddr *addr, socklen_t addrlen) Deskriptor des Client-Sockets Adresse des Server-Sockets (Domain-abhängig, siehe Beispiele) Länge von *addr (in Bytes) 0 (bei fehlerfreier Ausführung) 1 (bei Fehler) int accept( int sockfd, struct sockaddr *addr, socklen_t *addrlen) Deskriptor des Server-Sockets Rückgabe: Adresse des Client-Sockets, deren connect() entgegengenommen wurde Rückgabe: Länge von *addr (in Bytes) Deskriptor des neuen Sockets, der mit dem Client-Socket verbunden wurde ( Bild 4.13) 1 (bei Fehler) Man beachte den Unterschied zwischen listen() und accept(): Mit listen() wird lediglich eine Warteschlange für eingehende Verbindungsaufbauwünsche initialisiert. Erst mit accept() werden diese Wünsche entgegengenommen und somit Verbindungen aufgebaut. Die Datenübertragung erfolgt dann mit write()und read(): #include <unistd.h> ssize_t write(int sockfd, const void *buf, size_t count) Anzahl der geschriebenen Bytes 1 bei Fehler Anzahl zu schreibender Daten (in Bytes) zu schreibende Daten Deskriptor des Sockets, in den geschrieben werden soll

189 188 4 Kommunikation ssize_t read(int sockfd, void *buf, size_t count) Anzahl der gelesenen Bytes 0 bei Ende des Stroms 1 bei Fehler max. Anzahl zu lesender Daten (in Bytes) Puffer, in den die Daten eingelesen werden Deskriptor des Sockets, aus dem gelesen werden soll ssize_t und size_t sind systemabhängige Ganzzahltypen Alternativ können die Funktionen send() und recv() benutzt werden. Sie unterscheiden sich von write() bzw. read() lediglich durch einen zusätzlichen flags- Parameter, durch den der Sende- und der Empfangsvorgang bei Bedarf detaillierter gesteuert werden können. Bei Datagram-Sockets wird (ohne vorherigen Verbindungsaufbau) das Funktionenpaar sendto()/recvfrom() eingesetzt. Diese Funktionen senden bzw. empfangen Datagramme, also Nachrichten: #include <sys/types.h> #include <sys/socket.h> ssize_t sendto( int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) Anzahl gesendeter Bytes 1 (bei Fehler) Deskriptor des Sender-Sockets zu sendende Daten Anzahl zu sendender Daten (in Bytes) Flags zur Steuerung des Sendevorgangs Adresse des Empfänger-Sockets (Domain-abhängig, siehe Beispiele) Länge von *dest_addr (in Bytes) ssize_t recvfrom( int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) Anzahl empfangener Bytes 1 (bei Fehler) Deskriptor des Empfänger-Sockets Puffer zur Aufnahme der empfangenen Daten max. Anzahl zu empfangender Daten (in Bytes) Flags zur Steuerung des Empfangsvorgangs Rückgabe: Adresse des Sender-Sockets Rückgabe: Länge von *src_addr (in Bytes) ssize_t, size_t und socklen_t sind systemabhängige Ganzzahltypen Programmbeispiel: Stream-Sockets Das erste vollständige Programmbeispiel zu Sockets demonstriert die strombasierte Kommunikation in der Internet-Domain, also im Netz über Rechnergrenzen hinweg. Ein Server-Programm bietet einen Server-Socket an, mit der ein Client-Programm seinen clientseitige Socket verbindet und dann eine Nachricht an den Server schickt. Der Server antwortet darauf über dasselbe Socketpaar mit einer Rückmeldung. Die Vorgehensweise ist also wie in BILD 4.13 beschrieben:

190 4.2 Techniken in UNIX/Linux 189 Datei server.c: Programm des Servers (zuerst zu starten) #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> main() { int server_acc_sock; /* Desk. des Sockets für Verbindungsaufbau */ int server_comm_sock; /* Desk. des Sockets für die Kommunikation mit dem Client */ struct sockaddr_in server_addr; /* Adresse des Server-Sockets */ struct sockaddr client_addr; /* Adresse des Client-Sockets */ char buffer[256]; /* zur Aufnahme der empfangenen Nachricht */ int addr_len, err; /* Hilfsvariable */ struct { int l_onoff; int l_linger; linger; /* siehe Kommentar unten */ /* Erzeugen eines eigenen Sockets, über den Verbindungsaufbauwünsche von Clients entgegengenommen werden */ server_acc_sock = socket(af_inet,sock_stream,0); /* Sicherstellen, dass der Socket am Ende des Serverprogramms sofort geschlossen wird */ linger.l_onoff = 1; linger.l_linger = 0; setsockopt(server_acc_sock,sol_socket,so_linger, &linger,sizeof(linger)); /* Binden einer IP-Adresse und Portnummer an den eigenen Socket */ server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("x1.x2.x3.x4"); /* Hier IP-Adresse in numerischer Form einsetzen */ /* Adressen in symbolischer Form: siehe */ server_addr.sin_port = htons(55555); bind(server_acc_sock,(struct sockaddr *) &server_addr, sizeof(struct sockaddr)); /* Einrichten der Warteschlange für Verbindungsaufbauwünsche */ listen(server_acc_sock,1); /* Warten, bis ein Verbindungsaufbauwunsch eintrifft, und Akzeptieren dieses Wunschs. Ab dann kann die Verbindung zur Datenübertragung in beide Richtungen genutzt werden. */ addr_len = sizeof(client_addr); server_comm_sock = accept(server_acc_sock,&client_addr,&addr_len); /* Entgegennehmen einer Nachricht des Clients */ read(server_comm_sock,buffer,sizeof(buffer)); printf("\nserver hat gelesen: %s\n\n",buffer); PROG 4.5 Kommunikation über Stream-Sockets in der Internet-Domain

191 190 4 Kommunikation /* Senden einer Rückmeldung */ write(server_comm_sock,"nachricht ist angekommen",25); /* Schließen des Sockets */ close(server_acc_sock); close(server_comm_sock); Datei client.c: Programm des Clients (anschließend zu starten) #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> main() { int client_sock; /* Deskriptor des eigenen Sockets */ struct sockaddr_in server_addr; /* Adresse des Server-Sockets */ char buffer[256]; /* zur Aufnahme der empfangenen Rückantwort */ int error; /* Hilfsvariable */ /* Erzeugung eines eigenen Sockets */ client_sock = socket(af_inet,sock_stream,0); /* Zusammenstellen der Adresse des Server-Sockets */ server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("x1.x2.x3.x4"); /* Hier IP-Adresse in numerischer Form einsetzen */ /* Adressen in symbolischer Form: siehe */ server_addr.sin_port = htons(55555); /* Verbinden des eigenen Sockets mit dem Server-Socket */ error = connect(client_sock, (struct sockaddr *) &server_addr,sizeof(struct sockaddr)); if (error == -1) exit(-1); /* Übertragen einer Nachricht an den Server */ write(client_sock,"hallo",6); /* Lesen der Rückantwort des Servers */ read(client_sock,buffer,sizeof(buffer)); printf("\nclient hat gelesen: %s\n\n",buffer); Das Programm demonstriert auch, dass die Kommunikation über Sockets bidirektional verläuft: Die Nachricht vom Client an den Server und die Rückantwort werden über dasselbe Socketpaar übertragen. Kommunizieren Client und Server in der UNIX-Domain, also auf demselben Computer, so wird als Socket-Adresse ein Dateiname verwendet ( ). Zudem wird der Server-Socket am Ende des Ablaufs mit unlink() (also wie eine Datei) entfernt. Der Rest der Programme sieht wie im vorigen Beispiel aus:

192 4.2 Techniken in UNIX/Linux 191 Datei server.c: Programm des Servers... struct sockaddr server_addr, client_addr; server_acc_sock = socket(af_unix,sock_stream,0); server_addr.sa_family = AF_UNIX; strcpy(server_addr.sa_data,"serversocket"); bind(server_acc_sock,&server_addr,sizeof(struct sockaddr));... listen(), accept() und Datenübertragung wie oben... Datei client.c: Programm des Clients... struct sockaddr server_addr; client_sock = socket(af_unix,sock_stream,0); server_addr.sa_family = AF_UNIX; strcpy(server_addr.sa_data,"serversocket"); error = connect(client_sock,&server_addr,sizeof(struct sockaddr));... Datenübertragung wie oben... unlink("serversocket"); Dass die Datenübertragung bei Stream-Sockets tatsächlich strombasiert ist, sieht man, wenn man den Client die write()-funktion zweimal hintereinander aufrufen lässt: write(client_sock,"hallo ",6); write(client_sock,"hallo",6); Der anschließende (einzelne) read()-aufruf des Servers read(client_sock,buffer,sizeof(buffer)); liefert in buffer den Text "Hallo Hallo", also keine zwei getrennten Nachrichten. Ist nicht von vornherein bekannt, wie viele Bytes eintreffen, bettet man die Leseoperation am besten in eine Schleife ein. Der Datenstrom wird damit blockweise eingelesen und verarbeitet: char buffer[1024]; int gelesene_bytes;... do { gelesene_bytes = read(client_sock,buffer,1024);... Weiterverarbeitung der Bytes in buffer... while (gelesene_bytes>0); PROG 4.6 Kommunikation über Stream-Sockets in der UNIX-Domain Programmbeispiel: Datagram-Sockets Das zweite Programmbeispiel illustriert die nachrichtenbasierte Kommunikation in der UNIX-Domain, also auf demselben Computer. Ein Server-Programm bietet einen Socket an, an den ein Client-Programm zwei getrennte Nachrichten ( Datagramme ) unterschiedlichen Typs schickt. Der Server liest diese beiden Nachrichten einzeln aus;

193 192 4 Kommunikation PROG 4.7 Kommunikation über Datagram-Sockets in der UNIX-Domain Grenzen zwischen Nachrichten bleiben also bei der Übertragung erhalten. Die Vorgehensweise ist wie in (BILD 4.14) beschrieben: Datei server.c: Programm des Servers (zuerst zu starten) #include <stdio.h> #include <string.h> #include <sys/socket.h> main() { char buffer[256]; /* zur Aufnahme der ersten Client-Nachricht */ struct bestellung { /* zur Aufnahme */ char warenname[20]; /* der zweiten Client-Nachricht */ int kennziffer; float preis; clientbestellung; int sock; /* Deskriptor des Server-Sockets */ struct sockaddr server_addr; /* Adresse des Server-Sockets */ /* Erzeugen eines Sockets, über die Nachrichten von Clients entgegengenommen werden */ sock = socket(af_unix,sock_dgram,0); /* Anbinden eines Namens (= einer lokalen Adresse) an den Socket */ server_addr.sa_family = AF_UNIX; strcpy(server_addr.sa_data,"serversocket"); bind(sock,&server_addr,sizeof(struct sockaddr)); /* Lesen der ersten Nachricht des Clients */ recvfrom(sock,buffer,sizeof(buffer),0,0,0); printf("\ngelesen: %s\n\n",buffer); /* Lesen der zweiten Nachricht des Clients */ recvfrom(sock,&clientbestellung,sizeof(clientbestellung),0,0,0); printf("gelesen:\n"); printf(" Warenname: %s\n",clientbestellung.warenname); printf(" Kennziffer: %d\n",clientbestellung.kennziffer); printf(" Preis: %.2f\n",clientBestellung.preis); /* Schließen und Löschen des Sockets */ close(sock); unlink("serversocket"); Datei client.c: Programm des Clients (anschließend zu starten) #include <stdio.h> #include <string.h> #include <sys/socket.h> main() { struct bestellung { /* für die zweite Client-Nachricht */ char warenname[20]; int kennziffer;

194 4.3 Techniken in Java 193 float preis; meinebestellung; int sock; /* Deskriptor des Client-Sockets */ struct sockaddr server_addr; /* Adresse des Server-Sockets */ /* Erzeugen der Client-Socket */ sock = socket(af_unix,sock_dgram,0); /* Senden der ersten Nachricht */ server_addr.sa_family = AF_UNIX; strcpy(server_addr.sa_data,"serversocket"); sendto(sock,"hallo",6,0,&server_addr,sizeof(struct sockaddr)); /* Senden der zweiten Nachricht */ strcpy(meinebestellung.warenname,"usb-stick"); meinebestellung.kennziffer = 1234; meinebestellung.preis = 9.95; sendto(sock,&meinebestellung,sizeof(meinebestellung), 0,&server_addr,sizeof(struct sockaddr)); 4.3 Techniken in Java Java stellt, ebenso wie UNIX/Linux, eine Palette von Mechanismen zur Verfügung, die die verschiedenen Arten der Kommunikation aus realisieren. Im Unterschied zum prozeduralen C, das UNIX/Linux zugrundeliegt, ist Java aber eine objektorientierte Sprache. Objekte bieten durch ihre Methoden Dienste an, die von anderen Objekten genutzt werden können. In Java überwiegt damit der Aspekt der Kooperation (also der gegenseitigen Nutzung von Diensten) gegenüber dem Aspekt der Kommunikation (also der bloßen Übertragung von Daten). Kapitel 5 wird hierauf zurückkommen Übersicht Die Sammlung der Java-Mechanismen zur Kommunikation und Kooperation ist leider nicht sehr klar strukturiert, denn sie verteilt sich über eine ganze Reihe von Paketen. Es ist daher nicht immer ganz einfach, sich hier zurechtzufinden, den passenden Mechanismus auszuwählen und dabei nichts zu übersehen: Java-Threads (Paket java.lang) bringen Shared Memory unmittelbar mit sich: Wie in 2.4 (insbesondere ) besprochen, können die Threads, die während der Ausführung eines Programms gestartet wurden, auf gemeinsamen skalaren Variablen und strukturierten Objekten arbeiten. In 3.6 wurden verschiedene Techniken gezeigt, mit denen sich Threads dabei synchronisieren können (u.a. Paket java.util.concurrent).

195 194 4 Kommunikation Threads verschiedener Programme, die auf demselben Computer ausgeführt werden, können Daten über das Dateisystem austauschen. Hierzu stehen die Pakete java.io und java.nio zur Verfügung [JavaSpec]. Piped Streams (Paket java.io) dienen zur strombasierten lokalen Kommunikation ( 4.3.2). Sockets (Paket java.net) sind flexibel zur strom- und nachrichtenbasierten Kommunikation einsetzbar lokal und in Computernetzen ( 4.3.3). Über sie kann man auch mit Anwendungen anderer Plattformen kommunizieren, beispielsweise mit UNIX/Linux-Prozessen, die auf C-Programmen basieren ( 4.2.4). Zur nachrichtenbasierten Kommunikation (lokal und im Netz) kann man auch JMS, den Java Message Service, verwenden, der Bestandteil der Java Enterprise Edition (Java EE) ist (Paket javax.jms). Details hierzu findet man unter [JMS] und bei [Abts10]. Außerdem gibt es Java-Implementationen von MPI, dem Message Passing Interface (siehe zum Beispiel [MPIJava]). Zur objektbasierten Kooperation (lokal und im Netz) können Techniken wie RMI (Remote Method Invocation, Paket java.rmi, 5.3.1) oder JAX-WS (Java API for XML Web Services, Paket javax.jws, 5.3.3) eingesetzt werden. Zudem bieten in Java EE die Enterprise JavaBeans (EJB, [Back11], [Eber11]) eine Infrastruktur zur Programmierung verteilter Anwendungen. Die Programmierung dynamischer Webseiten ist mit Applets (Paket java.applet, ) und Servlets (Java-EE-Paket javax.servlet, ) sowie mit JSP (Java Server Pages, ) möglich Piped Streams Aufgabe 4A.4.1. Wie UNIX/Linux ( 4.2.2) realisiert auch Java ein Pipe-Konzept zur Übertragung ungetypter Byteströme. Lese- und Schreibende einer Pipe ( BILD 4.10) werden dabei durch einen Eingabe- und einen Ausgabedatenstrom repräsentiert. Die entsprechenden Klassen java.io.pipedinputstream und java.io.pipedoutputstream sind wie folgt definiert: class PipedInputStream { PipedInputStream() PipedInputStream(PipedOutputStream src) void connect(pipedoutputstream src) void close()... int read() int read(byte[] b, int off, int len)...

196 4.3 Techniken in Java 195 class PipedOutputStream { PipedOutputStream() PipedOutputStream(PipedInputStream snk) void connect(pipedinputstream snk) void close() void write(int b) void write(byte[] b, int off, int len)... Man erkennt, dass eine Pipe durch die Verbindung eines PipedInputStreams mit einem PipedOutputStream entsteht. Das kann durch Übergabe des einen Stroms an den Konstruktor des anderen geschehen oder auch später durch Aufruf einer der beiden connect()-methoden. Mit close() kann ein Strom wieder geschlossen werden. Die Datenübertragung erfolgt über Aufrufe von read() und write(): An write() wird ein einzelnes Byte (als int-wert) oder ein Byte-Array übergeben; read() liefert entsprechend ein einzelnes Byte oder eine Folge von Bytes (wobei mit den Parametern off und len Ausschnitte des Quell- bzw. Zielarrays festgelegt werden können). Daten werden also ungetypt übertragen; für die korrekte Interpretation muss das Programm selbst sorgen. Die übertragenen Daten werden in einem Ringpuffer beschränkter Größe zwischengespeichert, so dass read() und write() asynchron arbeiten. Grenzen zwischen einzelnen Nachrichten gibt es nicht, so dass mit einem read()-aufruf die Daten mehrerer write()-aufrufe gelesen werden können. Das folgende Programmbeispiel zeigt ein Erzeuger-Verbraucher-System mit Piped Streams: import java.util.concurrent.*; import java.io.*; // Verbraucher-Thread: liest beliebig viele Werte aus einer Pipe class Verbraucher extends Thread { private PipedInputStream pin; // Input-Stream, in dem die Daten aus der Pipe eintreffen Verbraucher(PipedInputStream pin) { this.pin = pin; public void run() { while (true) { byte gelesen = -1; // zur Aufnahme des gelesenen Werts try { gelesen = (byte) pin.read(); // liest byte-wert aus der Pipe catch (IOException exc) { // Eine Exception wird insbes. ausgelöst, wenn der Erzeuger PROG 4.8 Kommunikation über Piped Streams in Java

197 196 4 Kommunikation // terminiert und damit seinen PipedOutputStream gelöscht // hat. Damit terminiert auch der Verbraucher, indem er hier // per return aus seiner run()-methode zurückkehrt. return; System.out.println("Gelesen: "+gelesen); // Erzeuger-Thread: schreibt die Werte 10,..., 50 in eine Pipe class Erzeuger extends Thread { private PipedOutputStream pout; // Output-Stream, über den die Daten in die Pipe gelangen Erzeuger(PipedOutputStream pout) { this.pout = pout; public void run() { for (byte wert=10;wert<=50;wert+=10) { try { pout.write(wert); // schreibt byte-wert in die Pipe catch (IOException exc) {... try { Thread.sleep(2000); catch (Exception e) { // Hauptprogramm public class ErzeugerVerbraucherMitPipes { public static void main(string[] args) throws java.io.ioexception, InterruptedException { // Zwei zusammengehörige Piped Streams erzeugen PipedInputStream pin = new PipedInputStream(); PipedOutputStream pout = new PipedOutputStream(pin); // Kommunizierende Threads erzeugen und starten Erzeuger erzeuger = new Erzeuger(pout); Verbraucher verbraucher = new Verbraucher(pin); erzeuger.start(); verbraucher.start(); Sockets Java unterstützt wie UNIX/Linux ( 4.2.4) Sockets, über die lokal und im Internet kommuniziert werden kann. Auch hier wird zwischen Stream-Sockets zur strom-

198 4.3 Techniken in Java 197 basierten, verbindungsorientierten Kommunikation und Datagram-Sockets zur nachrichtenbasierten, verbindungslosen Kommunikation unterschieden ( ). Server bieten Sockets mit bestimmten Adressen an, mit denen Clients ihre Sockets verbinden oder an die sie Nachrichten schicken können. Im Folgenden wird nur eine knappe Übersicht über Sockets in Java gegeben; Details findet man in [JavaSpec], [JavaTutNet] und [Oech11]. Ein wichtiger Aspekt soll vorweg betont werden: Da Sockets eine Schnittstelle zum Internet und seinen Protokollen sind ( ) und das Internet heterogene Computer miteinander verbindet, können Daten zwischen beliebigen Anwendungsprogrammen ausgetauscht werden. So können insbesondere (wie im nächsten Abschnitt gezeigt wird) Java-Programme mit C-Programmen unter UNIX/Linux kommunizieren Stream-Sockets Zur Realisierung von Stream-Sockets dienen die Klassen java.net.socket (für Client-Sockets) und java.net.serversocket (für Server-Sockets): Aufgaben 4A.4.4./5. class Socket { Socket() Socket(InetAddress address, int port) Socket(InetAddress address, int port, InetAdress localaddr, int localport) Socket(String host, int port) Socket(String host, int port, InetAdress localaddr, int localport)... void connect(socketaddress endpoint) void close()... InputStream getinputstream() OutputStream getoutputstream()... class ServerSocket { ServerSocket() ServerSocket(int port) ServerSocket(int port, int backlog) ServerSocket(int port, int backlog, InetAddress bindaddr) void bind(socketaddress endpoint) Socket accept() void close()...

199 198 4 Kommunikation PROG 4.9 Kommunikation über Stream-Sockets in Java Der zeitliche Ablauf ist im Prinzip derselbe wie in UNIX/Linux ( BILD 4.13): Ein Server-Thread erzeugt einen Server-Socket (im einfachsten Fall durch Aufruf des ServerSocket-Standardkonstruktors), bindet mit bind() eine Adresse daran (meist nur eine Portnummer, ; als IP-Adresse wird dann die Adresse des Computers übernommen, auf dem der Server läuft) und wartet in accept() auf Verbindungsaufbauwünsche, die von Clients eintreffen. Ein Client-Thread erzeugt seinerseits einen Socket (durch Aufruf des Socket-Standardkonstruktors) und verbindet ihn per connect() mit dem Server-Socket. Der Server kehrt daraufhin aus seinem accept()- Aufruf zurück und erhält als Rückgabewert eine Referenz auf einen neuen Socket, über den dann der Datenaustausch mit dem Client verläuft. getinputstream() und getoutputstream() liefern Referenzen auf die Ein- und Ausgabeströme, die mit den Sockets verbunden sind. Auf ihnen können dann Client und Server read()- und write()-aufrufe tätigen. In der Programmierpraxis lässt sich dieser Weg verkürzen, indem man einige Vorbereitungen bereits in den Konstruktoraufrufen erledigt: Die Klasse ServerSocket bietet Konstruktoren, die die Serveradresse an den neu erzeugten Server-Socket binden (also ein explizites bind() überflüssig machen) und zudem die Länge der Warteschlange für eingehende Verbindungsaufbauwünsche festlegen (Parameter backlog, vergleiche die listen()-funktion in UNIX/ Linux, ). Die Klasse Socket bietet Konstruktoren, denen eine Serveradresse übergeben wird und die dann den neu erzeugten Socket mit dem entsprechenden Server-Sokket verbinden. Ein explizites connect() ist also nicht nötig. Beides zusammen führt zu etwas kompakteren Programmen als in UNIX/Linux, wie das folgende Beispiel zeigt: Datei Server.java: Programm des Servers (zuerst zu starten) import java.io.*; import java.net.*; public class StreamSocketServer { public static void main(string args[]) throws java.io.ioexception { ServerSocket sockacc = new ServerSocket(55555); // Server-Socket für den Verbindungsaufbau // mit beliebiger Portnummer zwischen und Socket sockcomm = null; // Socket für die Kommunikation mit dem Client // Verbindungsaufbauwunsch des Clients engegennehmen try { sockcomm = sockacc.accept(); catch (IOException e) { System.out.println("Accept fehlgeschlagen"); System.exit(-1);

200 Free ebooks ==> Techniken in Java 199 // Nachricht des Clients lesen DataInputStream instream = new DataInputStream(sockComm.getInputStream()); byte[] buf = new byte[256]; instream.read(buf); System.out.println("Server hat gelesen: "+(new String(buf))); // Antwort zurückschicken PrintStream outstream = new PrintStream(sockComm.getOutputStream()); outstream.print("nachricht ist angekommen\0"); // (\0 = String-Ende-Zeichen; nötig bei Kommunikation // mit einem C-basierten Partner - siehe unten) Datei Client.java: Programm des Clients (anschließend zu starten) import java.io.*; import java.net.*; public class StreamSocketClient { public static void main(string args[]) throws java.io.ioexception { Socket sock = new Socket("x1.x2.x3.x4",55555); // erster Parameter: IP-Adresse des Servers // (in numerischer oder symbolischer Form) // zweiter Parameter: Portnummer des Servers // Nachricht an den Server schicken PrintStream outstream = new PrintStream(sock.getOutputStream()); outstream.print("hallo\0"); // (\0 = String-Ende-Zeichen; nötig bei Kommunikation // mit einem C-basierten Partner - siehe unten) // Antwort des Servers lesen DataInputStream instream = new DataInputStream(sock.getInputStream()); byte[] buf = new byte[256]; instream.read(buf); System.out.println("Client hat gelesen: "+(new String(buf))); Der Server und der Client dieses Java-Programms können jeweils auch mit dem UNIX/Linux-basierten Client bzw. Server aus PROG 4.5 Daten austauschen. Socket- Kommunikation ist also (wie schon eingangs gesagt) nicht nur über Computergrenzen hinweg, sondern auch zwischen verschiedenen Programmiersprachen und Betriebssystemen möglich.

201 200 4 Kommunikation Aufgaben 4A.4.3./6./ Datagram-Sockets Datagram-Sockets werden mit der Klasse java.net.datagramsocket programmiert: class DatagramSocket { DatagramSocket() DatagramSocket(int port) DatagramSocket(int port, InetAddress laddr)... void bind(socketaddress addr) void close()... void send(datagrampacket p) void receive(datagrampacket p)... Aus der Klassendefinition ist ersichtlich, dass an einen Datagram-Socket eine Portnummer und zusätzlich eine IP-Adresse gebunden werden kann. Dies ist auf Seiten eines Servers, also eines Empfängers von Nachrichten, notwendig, da dieser auf einem Port ( ) auf das Eintreffen von Nachrichten lauschen soll. Nachrichten werden per send() verschickt und per receive() empfangen. Sie sind Objekte der Klasse java.net.datagrampacket: class DatagramPacket { DatagramPacket(byte[] buf, int length) DatagramPacket(byte[] buf, int length InetAddress address, int port)... void setsocketaddress(socketaddress address) void setdata(byte[] buf) byte[] getdata()... Wie man sieht, enthält eine Nachricht Nutzdaten, die in einem byte-array enthalten sind. Der Array wird über einen Konstruktorparameter oder durch setdata() in die Nachricht eingesetzt und durch getdata() abgerufen. Die Adresse des Sockets, an den eine Nachricht geschickt werden soll ( ), kann über Konstruktorparameter oder durch Methoden wie setsocketaddress() festgelegt werden. Das folgende Beispiel zeigt die bidirektionale Übertragung von Strings über Datagram-Sockets:

202 4.3 Techniken in Java 201 Datei Server.java: Programm des Servers (zuerst zu starten) import java.io.*; import java.net.*; public class DatagramSocketServer { public static void main(string args[]) throws java.io.ioexception { DatagramSocket sock = new DatagramSocket(55555); // Server-Socket mit beliebiger Portnummer // zwischen und // Nachricht des Clients empfangen byte[] buf = new byte[256]; DatagramPacket packin = new DatagramPacket(buf,buf.length); sock.receive(packin); System.out.println("Server hat empfangen: "+ new String(packIn.getData(),0,packIn.getLength())); // Antwort an Client zurückschicken InetAddress ipaddr = packin.getaddress(); int port = packin.getport(); buf = "Nachricht ist angekommen".getbytes(); DatagramPacket packout = new DatagramPacket(buf,buf.length,ipaddr,port); sock.send(packout); Datei Client.java: Programm des Clients (anschließend zu starten) import java.io.*; import java.net.*; public class DatagramSocketClient { public static void main(string args[]) throws java.io.ioexception { DatagramSocket sock = new DatagramSocket(); // Client-Socket // DatagrammPacket zusammenstellen byte[] buf = new byte[256]; buf = "Hallo".getBytes(); InetAddress ipaddr = InetAddress.getLocalHost(); // wenn der Server auf demselben Knoten wie der Client läuft. // alternativ: // InetAddress ipaddr = InetAddress.getByName("xyz"); // xyz = numerische oder symbolische IP-Adresse des Servers int port = 55555; DatagramPacket packout = new DatagramPacket(buf,buf.length,ipaddr,port); // DatagramPacket abschicken sock.send(packout); PROG 4.10 Kommunikation über Datagram-Sockets in Java

203 202 4 Kommunikation // Antwort des Servers empfangen buf = new byte[256]; DatagramPacket packin = new DatagramPacket(buf,buf.length); sock.receive(packin); System.out.println("Client hat empfangen: "+ new String(packIn.getData(),0,packIn.getLength())); Das Programm des Servers zeigt insbesondere, dass eine Nachricht eine Absenderangabe (IP-Adresse und Portnummer) enthält, die beim Versenden automatisch eingetragen wird. Der Empfänger kann sie abfragen und somit eine Antwort zurückschicken. 4.4 Zusammenfassung und Ausblick In Kapitel 4 ging es um die Übertragung von Daten zwischen nebenläufigen Aktivitäten: Die Übertragung von Daten wird als Kommunikation bezeichnet. Man unterscheidet zwischen speicherbasierten, nachrichtenbasierten und strombasierten Kommunikationsmechanismen. Daten können direkt (und damit synchron) zwischen Sender(n) und Empfänger(n) übertragen werden oder indirekt (und damit asynchron) über Mailboxen und Ports. Die Datenkommunikation in Rechnernetzen wird durch Kommunikationsprotokolle gesteuert. Man unterscheidet unter anderem verbindungsorientierte und verbindungslose Protokolle. Protokolle sind oft in einem Stapel (Stack) angeordnet, in dem höhere Protokolle die Dienste niederer Protokolle nutzen. Das zentrale Protokoll des Internet-Stacks ist IP, auf das TCP (verbindungsorientiert) und UDP (verbindungslos) aufsetzen. Über die Socket-Schnittstelle können Anwendungsprogramme auf Protokolldienste zugreifen. Die C-Schnittstelle von UNIX/Linux bietet zur lokalen Kommunikation Shared Memory (speicherbasiert), Pipes (strombasiert) und Message Queues (nachrichtenbasiert). Zur Kommunikation in Rechnernetzen (und auch zur lokalen Kommunikation) dienen Sockets, die strombasiert und verbindungsorientiert (Stream- Sockets) oder nachrichtenbasiert und verbindungslos (Datagram-Sockets) arbeiten können. Java unterstützt durch sein Thread-Konzept unmittelbar eine speicherbasierte Kommunikation. Darüber hinaus gibt es Piped Streams für die lokale strombasierte Kommunikation und Sockets zur Kommunikation in Rechnernetzen. Weitere Java-Mechanismen werden im folgenden Kapitel besprochen. Kapitel 4 befasste sich mit der reinen Datenübertragung. Wozu die übertragenen Daten dienen, interessierte hier noch nicht. Das folgende, letzte Kapitel dieses Buchs beschäftigt sich nun mit der Kooperation zwischen nebenläufigen Aktivitäten. Es geht

204 4.4 Zusammenfassung und Ausblick 203 also darum, Daten mit einem bestimmten Zweck zu übertragen, nämlich zur Zusammenarbeit von Prozessen bei der Lösung von Problemen. Im Mittelpunkt wird dabei das Client-Server-Modell stehen, bei dem Server Dienste anbieten und Clients diese Dienste nutzen.

205

206 4A Kommunikation: Aufgaben Die Lösungen findet man auf der Webseite zum Buch. 4A.1 Wissens- und Verständnisfragen 1. Kreuzen Sie die richtige(n) Aussage(n) an: a.) Ein Kommunikationsprotokoll legt im Allgemeinen fest: O die zur Datenübertragung verwendete Hardware. O die möglichen zeitlichen Abläufe von Kommunikationsvorgängen. O die Struktur der übertragenen Daten. b.) Eine Pipe dient zur O längerfristigen Datenspeicherung. O Prozesskommunikation. O Fehlertoleranz. c.) Sockets können eingesetzt werden O zur lokalen Kommunikation auf einem Computer. O zur Kommunikation über das Internet. O zum direkten Abspeichern von Daten in Dateien. d.) Folgende Techniken können zum Austausch beliebiger Daten zwischen UNIX/Linux-Prozessen benutzt werden: O Shared Memory. O Semaphore. O Message Queues. e.) Unterschiede zwischen Shared Memory und Message Queues in UNIX/Linux sind, O dass man über Shared Memory Daten beliebigen Typs übergeben kann und bei Message Queues nicht. O dass Shared-Memory-Bereiche von mehr als zwei Prozessen zugreifbar sind, während Message Queues nur jeweils zwei Prozessen zur Verfügung stehen. O dass sich der Programmierer bei Shared Memory selbst um die Synchronisation kümmern muss und bei Message Queues nicht.

207 206 4A Kommunikation: Aufgaben f.) Eine benannte Pipe wird erzeugt mit der UNIX/Linux-C-Funktion O mkfifo(). O pipe(). O open(). g.) Der Begriff UNIX-Domain steht im Zusammenhang mit O Sockets. O der Benutzerschnittstelle. O Semaphoren. h.) Eine Verbindung zwischen den Sockets zweier UNIX/Linux-Prozesse ist hergestellt, O sobald beide die Funktion socket() ausgeführt haben. O sobald der Empfänger die Funktion listen() ausgeführt hat. O sobald ein Prozess die Funktion connect() und der andere die Funktion accept() ausgeführt hat. i.) Zu den Socket Domains gehören O die Internet Domain. O die UNIX Domain. O die Datagram Domain. 2. Füllen Sie in den folgenden Aussagen die Lücken: a.) Eine Mailbox, auf die nur ein Prozess lesend zugreifen darf, heißt auch. b.) Wird dieselbe Nachricht an mehrere Empfänger geschickt, so ist das ein. c.) Eine Menge von Regeln zum Datenaustausch über ein Netz heißt. d.) ist das verbindungsorientierte Internet-Protokoll, das auf IP aufsetzt. Das entsprechende verbindungslose Protokoll heißt. e.) Eine Message Queue erzeugt man unter UNIX/Linux mit der Funktion. f.) Aus C-Programmen heraus kann man Daten lokal und ins Internet verschicken, indem man benutzt. 3. Sind die folgenden Aussagen wahr oder falsch? Begründung! a.) Beim Polling wird passiv gewartet. b.) TCP bietet, im Vergleich zum darunterliegenden IP, mehr zusätzliche Funktionalität an als UDP. c.) Ein Shared-Memory-Segment in UNIX/Linux wird automatisch gelöscht, wenn der Prozess, der es erzeugt hat, terminiert. d.) Stream Sockets implementieren eine UDP-basierte Kommunikation. e.) Das Internet-Protokoll HTTP nutzt direkt die Dienste von IP. f.) In Java gibt es für die Sockets, die Server nach außen anbieten, und die Sockets, über die Clients mit Servern kommunizieren, zwei verschiedene Klassen.

208 4A.1 Wissens- und Verständnisfragen Welcher Begriff passt jeweils nicht in die Reihe? Begründung! a.) speicherbasiert, nachrichtenbasiert, schnittstellenbasiert, strombasiert b.) Message Queue, Shared Memory, Socket, Rendezvous c.) Shared Memory, Semaphor, Message Queue, Pipe d.) speicherbasiert, verbindungslos, zuverlässig, sequentiell übertragend e.) TCP, IP, API, HTTP f.) connect(), wait(), accept(), listen() 5. Auf der linken Seite sind Begriffe angegeben, rechts stehen Eigenschaften. Welcher Begriff gehört zu welcher Eigenschaft? Ziehen Sie genau fünf Pfeile von links nach rechts! Shared Memory blockierend Rendezvous speicherbasiert Protokoll (un)benannt Message Queue nachrichtenbasiert Pipe verbindungsorientiert/-los 6. Beantworten Sie die folgenden Fragen: a.) Gibt es bei einem Telefongespräch, das unmittelbar zwischen zwei menschlichen Partnern ablaufen soll, ein blocking send oder ein non-blocking send? Wie sieht es aus, wenn ein Anrufbeantworter benutzt wird? Begründung! b.) Was ist ein Port (im Vergleich zu einer Mailbox)? Nennen Sie zwei Mechanismen der UNIX/Linux-C- Schnittstelle, mit denen man Ports und Mailboxen realisieren kann. c.) Was ist ein Kommunikationsprotokoll? Welche Rolle spielt in diesem Zusammenhang der Begriff Peer? d.) Wozu dienen Portnummern im Internet? e.) Was bedeutet, allgemein gesprochen, der Begriff Client-Server-System? f.) Kann man in einem C-Programm Semaphore verwenden, um beliebige Daten auszutauschen? Begründung! g.) Welche Funktion sollte am Ende eines UNIX/Linux-C-Programms aufgerufen werden, wenn das Programm ein Shared-Memory-Segment erzeugt hat? h.) Welche zwei Techniken können Sie verwenden, wenn Sie in einem C-Programm unter UNIX/Linux eine lokale nachrichtenbasierte Kommunikation realisieren wollen? i.) Wozu dienen in UNIX/Linux die Message-Typen bei Message Queues? j.) Was verwenden Sie, um aus einem UNIX/Linux-C-Programm heraus über das Internet zu kommunizieren? k.) Über welchen Mechanismus können UNIX/Linux-C-Programme und Java-Programme miteinander kommunizieren?

209 208 4A Kommunikation: Aufgaben 4A.2 Sprachunabhängige Anwendungsaufgaben 1. Eine große Suchmaschine besteht aus mehreren Servern. Jeder der Server kann Benutzeranfragen bearbeiten; die Arbeitslast wird also auf die einzelnen Server verteilt. Schreibt man in diesem System die hereinkommenden Benutzeranfragen besser in einen Port oder besser in eine Mailbox? Begründung! 2. Betrachten Sie ein System mit drei Servern und vielen Clients. Die Server bieten (wie in der vorangehenden Aufgabe) jeweils denselben Dienst an, so dass ein Client-Auftrag von jedem dieser Sever bearbeitet werden kann. Nach der Bearbeitung des Auftrags schickt der Server dem Client, der den Auftrag erteilt hat, eine Antwort zurück. Skizzieren Sie (mit den Bausteinen aus BILD 4.3, Mitte und unten) die Struktur des Systems aus Client- und Server-Prozessen, Mailboxen und Ports. Wie geht man am besten vor, wenn man nur Ports, aber keine Mailboxen verwenden darf? (Tipp: Sie dürfen einen weiteren Prozess einführen.) 3. Betrachten Sie das folgende Zeitdiagramm, das das Verhalten von zwei kommunizierenden Prozessen P1 und P2 zeigt: P1 P2 erarbeite arbeite einen Wert W irgendetwas (2 ms) (7 ms) Übergabe W arbeite erarbeite mit W irgendetwas eine Antwort A (4 ms) (4 ms) arbeite mit A weiter (3 ms) Übergabe A arbeite irgendetwas (3 ms) Ende Ende t P1 schickt also zum Zeitpunkt 2 den Wert W an P2 ab; P2 empfängt ihn zum Zeitpunkt 4. P2 schickt zum Zeitpunkt 8 den Wert A an P1; P1 empfängt ihn zum Zeitpunkt 9. Ist solch ein Zeitverhalten bei blocking send oder bei nonblocking send zu beobachten? Zeichnen Sie das Diagramm, wie es sich für die andere Art von Senden ergibt. Die Länge der einzelnen Phasen, in denen mit W und/oder A gearbeitet wird (oben angegeben durch die Zeitwerte in Klammern), soll dabei beibehalten werden. 4. Zur Steuerung der Kommunikation kann man entweder einen Protokollstack mit mehreren Protokollen verwenden oder ein einziges großes Protokoll, das alles regelt. Nennen Sie einen Vorteil des Protokollstacks gegenüber dem großen Protokoll. Gibt es auch Nachteile? 5. In Hintertupfingen im Bayerischen Wald lebt der Wastl, der einen Brief an König Ludwig II. in München schreiben will. Da er aber nicht genau weiß, wie man das macht, geht er zum Bürgermeister. Der schreibt den Brief für ihn und bringt ihn zum örtlichen Postvorsteher. Dieser schickt den Brief nach München, wo ihn ein Briefträger zustellt. Der Privatsekretär des Königs nimmt den Brief entgegen und liest ihn seiner Majestät vor. Aus wie vielen Schichten besteht der Protokollstack? Welche Funktionseinheiten sind diesen Schichten jeweils zugeordnet und was sind die jeweiligen Peers? Welche Dienste werden an den jeweiligen Schnittstellen nach oben angeboten?

210 Free ebooks ==> 4A.3 Programmierung unter UNIX/Linux 209 4A.3 Programmierung unter UNIX/Linux 1. Setzen Sie in der folgenden Tabelle oben die Namen von vier Unix/Linux-Kommunikationsmechanismen ein. Kreuzen Sie dann an, welche Aussagen für die einzelnen Mechanismen zutreffen:. Erfordert eine explizit ausprogrammierte Reihenfolge-Synchronisation (z.b. durch Semaphore). Ist ein nachrichtenbasierter Mechanismus. Ist auch zur Kommunikation zwischen zwei Computern nutzbar. Basiert oft auf TCP/IP. Kann als Basis eines lokalen Client-Server-Systems dienen. 2. Betrachten Sie ein UNIX/Linux-C-Programm, in dem zwei Prozesse über eine Message Queue kommunizieren. Neben dem normalen Datenverkehr sollen über dieselbe Queue auch dringende Nachrichten übertragen werden, die der Empfänger bevorzugt lesen soll. Wie lässt sich das programmtechnisch realisieren? 3. Ihr Kollege und Sie arbeiten am selben Computer unter UNIX und möchten jeder ein C-Programm schreiben. Die beiden C-Programme stehen in getrennten Dateien und werden von Ihrem Kollegen und Ihnen jeweils einzeln gestartet. Die Programme sollen untereinander Daten austauschen und dabei Pipes benutzen. Welche Art von Pipes müssen Sie hier benutzen? Begründung! Können die Programme über eine Message Queue miteinander kommunizieren? Wenn nein: Warum nicht? Wenn ja: Wie einigen sich die Programme oder deren Programmierer, welche Queue dazu benutzt wird. Wie stellen Sie bei Message Queues sicher, dass der Empfänger mit einem blocking receive arbeitet? Wie erreichen Sie ein nonblocking receive? 4. Betrachten Sie den folgenden Ausschnitt aus einem Programm, in dem zwei Prozesse kommunizieren: Ein Prozess übergibt einen Wert (in diesem Fall die Konstante 10) an einen zweiten Prozess, der ihn dann ausgibt. int id, *p; float *f;... id = shmget(ipc_private,sizeof(int),ipc_creat 0777); if (fork()==0) { p = (int *) shmat(id,0,0); *p = 10;

211 210 4A Kommunikation: Aufgaben exit(0); f = (float *) shmat(id,0,0); printf("%f\n",*f); Welcher Prozess führt den printf()-aufruf aus, der Vater- oder der Sohnprozess? Wie nennt man die Technik zur Datenübergabe, die hier benutzt wird? Welchen Fehler hat der Programmierer hinsichtlich der Datenübertragung gemacht? Ist die Synchronisation von Vater- und Sohnprozess korrekt? Wenn ja: Warum? Wenn nein: Warum nicht, und wie könnte das Programm korrigiert werden? 5. Gegeben ist ein Programmstück, bei dem zwei Prozesse über ein Shared-Memory-Segment kommunizieren und sich dabei über einen Semaphor synchronisieren: int semid, shmid, i, status; char *cp; unsigned short init_array[1]; struct sembuf sem_p, sem_v; semid = semget(ipc_private,1,ipc_creat 0777); init_array[0] = 0; semctl(semid,0,setall,init_array); sem_p.sem_num = sem_v.sem_num = 0; sem_p.sem_op = -1; sem_v.sem_op = 1; sem_p.sem_flg = sem_v.sem_flg = 0; shmid = shmget(ipc_private,5,ipc_creat 0777); if (fork()==0) { semop(semid,&sem_v,1); cp = (char *) shmat(shmid,0,0); printf("%c%c%c%c%c",*(cp+1),*(cp+3),*(cp+4),*cp,*(cp+1)); exit(0); cp = (char *) shmat(shmid,0,0); *cp= h ; *(cp+1)= a ; *(cp+2)= l ; *(cp+3)= l ; *(cp+4)= o ; semop(semid,&sem_p,1); wait(&status); Welche Rolle hat der Vaterprozess in diesem Programm? Sender oder Empfänger d.h. schickt er Daten an seinen Sohn oder empfängt er Daten vom Sohn? Der Semaphor soll durchsetzen, dass der Empfänger wartet, bis der Sender seine Daten in das Shared- Memory-Segment geschrieben hat. Hier enthält das Programm aber einen Fehler. Wie muss es korrigiert werden? Setzen Sie nun voraus, dass die Prozesse in der richtigen Reihenfolge ablaufen. Welche Ausgabe erscheint dann auf dem Bildschirm? Zum sauberen Abschluss des Programms fehlen noch zwei Operationen. Schreiben Sie sie vollständig hin. Wieso ist es wichtig, dass vor diesen Operationen wait() aufgerufen wird?

212 4A.3 Programmierung unter UNIX/Linux Gegeben ist das folgende Programm: main() {... int fd[2]; pipe(fd); if (fork()==0) { close(fd[0]);... close(fd[1]);... Wozu dienen die beiden close()-aufrufe? Wird hier eine benannte oder eine unbenannte Pipe erzeugt? Was ist bei benannten Pipes möglich, was man bei unbenannten Pipes nicht kann? Ergänzen Sie das Programmstück so, dass der Sender-Prozess dem Empfänger den Text Message schickt und der Empfänger den empfangenen Text auf den Bildschirm ausgibt. 7. Gegeben ist das folgende Programmstück: struct message { float a; char b; mymessage; int msqid; mymessage.a = 1.23; mymessage.b = x ; msqid = msgget(ipc_private,ipc_creat 0777); msgsnd(msqid,&mymessage,sizeof(mymessage)-sizeof(long),0); Welchen Fehler hat der Programmierer hier gemacht? 8. Gegeben ist das folgende Programmstück: if (msgrcv(msqid,&message,...,2,ipc_nowait)==-1) msgrcv(msqid,&message,...,1,0); Offensichtlich soll das Programm Nachrichten des Typs 2 bevorzugt empfangen und Nachrichten des Typs 1 nur dann, wenn keine Typ-2-Nachrichten vorliegen. Ist es so korrekt? Begründung! 9. Gegeben sind Ausschnitte aus zwei C-Programmen, die über Sockets miteinander kommunizieren. Tragen Sie in die Lücken die Namen der aufgerufenen Funktionen ein. Programm 1: int sd1; struct sockaddr addr2; sd1 = (AF_UNIX,SOCK_STREAM,0); addr2.sa_family = AF_UNIX; strcpy(addr2.sa_data,"unseresocket"); (sd1,&addr2,sizeof(struct sockaddr)); (sd1,"dies ist ein Test",18);

213 212 4A Kommunikation: Aufgaben Programm 2: int sd2, sd3, addr_len; char buf[256]; struct sockaddr addr1, addr2; sd2 = (AF_UNIX,SOCK_STREAM,0); addr2.sa_family = AF_UNIX; strcpy(addr2.sa_data,"unseresocket"); (sd2,&addr2,sizeof(struct sockaddr)); (sd2,1); addr_len = sizeof(addr1); sd3 = (sd2,&addr1,&addr_len); (sd3,buf,sizeof(buf)); Beantworten Sie nun die folgenden Fragen: Verläuft die Kommunikation hier über das Internet oder lokal auf einem Computer? In welcher Variablen stehen nach der Ausführung die übertragenen Daten? An der eigentlichen Datenübertragung sind nur zwei Sockets beteiligt. Warum gibt es trotzdem drei Socketdeskriptoren (sd1-sd3)? 10. Der folgende (noch lückenhafte) Programmtext realisiert einen einfachen HTTP-Client: #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> main() { int sock, error, numbytes, fd; char buffer[1024]; struct sockaddr_in server_addr; struct hostent *host; /* Stream-Socket in der Internet-Domain erstellen */ sock = server_addr.sin_family = AF_INET; host = gethostbyname(" bcopy(host->h_addr,&server_addr.sin_addr,host->h_length); server_addr.sin_port = htons(80); /* Socket mit dem Socket des Servers verbinden */ error = if (error == -1) { printf("connect fehlgeschlagen\n"); exit(-1); /* "GET /vogt/buecher/nebenlaeufigkeit/ HTTP/1.1\n" in den Socket schreiben */

214 4A.4 Programmierung in Java 213 /* "Host: in den Socket schreiben */ write(sock,"connection: Close\n\n",19); fd = open("x.html",o_creat O_WRONLY,0777); do { /* Maximal 1024 Bytes aus dem Socket nach buffer lesen */ numbytes = write(fd,buffer,numbytes); while (numbytes>0); close(fd); Vervollständigen Sie den Programmtext, übersetzen Sie ihn und führen Sie ihn aus. Was steht anschließend in der neu entstandenen Datei x.html? Schicken Sie statt des GET-Kommandos ein HEAD-Kommando an den Server und betrachten Sie auch hier das Ergebnis. 11. Betrachten Sie das folgende Programm: main() { int a, b; if (fork()==0) { sleep(2); printf("mein Bruder hat zugewiesen: a=%d, b=%d\n",a,b); exit(0); if (fork()==0) { /* Der "Bruder" */ a=1; b=2; exit(0); Warum funktioniert dieses Programm nicht so, wie der Programmierer sich das gedacht hat? Ergänzen Sie das Programm durch einen UNIX/Linux-Kommunikationsmechanismus Ihrer Wahl, so dass es funktioniert. 4A.4 Programmierung in Java 1. Übertragen Sie das UNIX/Linux-Pipe-Beispiel PROG 4.2 in ein Java-Programm, das Piped Streams verwendet. 2. Gegeben ist der folgende lückenhafte Programmtext. Vervollständigen Sie ihn! (Der Server soll auf dem Knoten laufen und den Port benutzen.) public class Server { public static void main(string args[]) throws java.io.ioexception { sockacc = new (54321); sockcomm = null;

215 214 4A Kommunikation: Aufgaben try { sockcomm =. (); catch (IOException e) {... DataInputStream instream = new DataInputStream(.get ()); byte[] buf = new byte[256]; instream. (buf); System.out.println("Server hat gelesen: "+(new String(buf))); public class Client { public static void main(string args[]) throws java.io.ioexception { sock = new (, ); PrintStream outstream = new PrintStream(sock.get ()); outstream.print("hallo"); 3. Gegeben sind die folgenden Programmausschnitte: Server: ServerSocket sockacc = new ServerSocket(55555); Socket sockcomm = null; try { sockcomm = sockacc.accept(); catch (IOException e) {... Client: DatagramSocket sock = new DatagramSocket(); byte[] buf = new byte[256]; buf = "Hallo Server!".getBytes(); DatagramPacket packout = new DatagramPacket( buf,buf.length,inetaddress.getlocalhost(),55555); sock.send(packout); Warum funktioniert hier die Kommunikation zwischen Server und Client nicht? 4. Gegeben sind die folgenden Programmausschnitte: Server: ServerSocket sockacc = new ServerSocket(80); Socket sockcomm = null; try { sockcomm = sockacc.accept(); catch (IOException e) {...

216 4A.4 Programmierung in Java 215 Client: Socket sock = new Socket("...Server-Adresse...",80); PrintStream outstream = new PrintStream(sock.getOutputStream()); outstream.print("hallo Server!"); Auch dieser Code enthält einen entscheidenden Fehler. Welchen? (Tipp: Lesen Sie sich Abschnitt nochmals gründlich durch.) 5. Schreiben Sie einen Client in Java, der dasselbe leistet wie das Programm in Aufgabe 4A Schreiben Sie einen Server in Java, der einen Text aus Groß- und Kleinbuchstaben entgegennimmt und einen entsprechenden Text, der nur aus Großbuchstaben besteht, zurückliefert. Der Server soll einen Stream- Socket und einen Datagram-Socket anbieten, über die man wahlweise diesen Dienst von außen nutzen kann. Schreiben Sie zudem einen Client mit einem Stream-Socket und einen Client mit einem Datagram-Socket, die je zwei Aufträge an den Server schicken und von ihm über dieselben Sockets die Antworten zurückerhalten. (Hinweis zur Programmierung: Starten Sie innerhalb des Servers zwei Threads, von denen der eine den Stream-Socket und der andere den Datagram-Socket bedient.) 7. Ändern Sie den UNIX/Linux-basierten Server in PROG 4.7 so, dass er in der Internet Domain arbeitet und zudem nur eine String-Nachricht (also nur das erste Datagramm) empfängt. Schreiben Sie dann ein Client- Programm in Java, das an den Server ein passendes Datagramm schickt.

217 5 Kooperation Kooperation Kapitel 5 befasst sich mit der Kooperation von Kommunikation Prozessen, das heißt mit ihrer Zusammenarbeit bei der Lösung von Problemen. Im Vor- Synchronisation dergrund steht dabei die Kooperation in Rechnernetzen, also über Computergrenzen hinweg. Basistechniken 5.1 Modelle und Techniken Abschnitt 5.1 führt in grundlegende Modelle und Techniken der Kooperation ein. Zwei wichtige Modelle sind das Client-Server-Modell, in dem Server Dienste anbieten und Clients diese Dienste nutzen, sowie das Peer-to-Peer-Modell, bei dem sich Ressourcen auf mehrere gleichberechtigte Prozesse verteilen. Als Programmiertechniken kann man den Prozedurfernaufruf (Remote Procedure Call), Methodenaufrufe entfernt liegender Objekte (Remote Method Invocation, Remote Objects) sowie WWW-basierte Techniken (unter anderem Web Services) einsetzen Das Client-Server-Modell Das Peer-to-Peer-Modell Programmiertechniken Techniken in UNIX/Linux Abschnitt 5.2 bespricht zwei Techniken, die an der C-Programmierschnittstelle von UNIX/Linux zur netzweiten Kooperation eingesetzt werden können nämlich Sockets, auf deren Basis die Kooperationsschritte explizit ausprogrammiert werden, und Remote Procedure Calls Kooperation über Sockets Remote Procedure Call (RPC) Techniken in Java Abschnitt 5.3 behandelt eine Reihe von Java-Techniken zur Kooperation nämlich Remote Method Invocation, dynamische Webseiten mit Applets, Servlets und Java Server Pages sowie Web Services mit JAX-WS Remote Method Invocation (RMI) Dynamische Webseiten Web Services Zusammenfassung...243

218 5 Kooperation Bei der Kommunikation werden, wie im vorigen Kapitel besprochen, Daten zwischen Prozessen oder auch Threads übertragen. Kommunikation in Computern ist (anders als bei manchen Menschen) kein Selbstzweck, denn die kommunizierenden Prozesse nutzen die Daten, um zu kooperieren, das heißt um gemeinsam Probleme zu lösen. Kommunikation ist also die Grundlage von Kooperation: Kooperation ist die Zusammenarbeit von Prozessen/Threads, um gemeinsam eine Aufgabe zu bearbeiten. Kooperation stützt sich auf Kommunikation, also auf die Übertragung von Daten. Kooperation ist innerhalb eines Computers oder zwischen mehreren Computern in einem Rechnernetz möglich. DEFINITION Kooperation Um eine Kooperation nebenläufiger Aktivitäten zu implementieren, benötigt man also ein Kommunikationssystem oder einen gemeinsam nutzbaren Speicher ( 4.1.1), über das bzw. den die benötigten Daten übertragen werden: Aktivität A Daten Kooperation zur Lösung einer Aufgabe Aktivität B Daten BILD 5.1 Kommunikation als Grundlage von Kooperation Kommunikationssystem oder Shared Memory Bei der Implementierung kann ein Programmierer unmittelbar auf das Kommunikationssystem (beispielsweise eine Socket-Schnittstelle, 4.2.4) aufsetzen und die Kooperationsvorgänge selbst programmieren. Er kann sich aber auch an vorgegebenen Modellen orientieren und entsprechende Mechanismen und Schnittstellen nutzen, die vom System bereitgestellt werden. Dieses Kapitel bespricht solche Modelle und Techniken der Kooperation und zeigt ihre Umsetzung in UNIX/Linux und in Java. Kooperationsmodelle und -techniken sind eine wichtige Grundlage zur Realisierung verteilter Systeme ( ). Weitere Details findet man daher in der Literatur zu diesem Thema, beispielsweise in [Tane07]. 5.1 Modelle und Techniken Das Client-Server-Modell Das klassische Kooperationsmodell ist das Client-Server-Modell, das bei der Einführung von Sockets ( 4.2.4) bereits angesprochen wurde. Aufgaben 5A.2.1./4.

219 218 5 Kooperation DEFINITION Client-Server- Modell Grundlegende Struktur Das Client-Server-Modell ist ein Kooperationsmodell, in dem Server jeweils einen oder mehrere Dienste anbieten und Clients diese Dienste nutzen. Dazu schicken Clients Aufträge (engl.: requests) an die Server und können später Antworten (engl.: replies) von ihnen zurückerhalten. Client und Server können auf demselben Computer oder auf verschiedenen Computern eines Rechnernetzes laufen. Das folgende Bild illustriert die Komponenten und Abläufe in einem Client-Server- System: BILD 5.2 Kooperation zwischen Client und Server Clients 1. Auftrag ( Request ) 3. Antwort ( Reply ) Server A 2. Auftragsbearbeitung Knoten A Netz Knoten B Server B Ein bekanntes Beispiel für ein Client-Server-System ist das World Wide Web (WWW). Hier bieten Web Server Dokumente an, die Web Clients (konkret: Browser) herunterladen können. Weitere Beispiele sind File Server, die Dateien bereitstellen, Print Server, die Druckaufträge bearbeiten, und Mail Server, die sich um ein- und ausgehende Mails kümmern. Man beachte, dass sich die Einstufung eines Prozesses/Threads als Client oder Server jeweils nur auf einen bestimmten Dienst bezieht. Es ist durchaus möglich, dass ein Prozess einen Dienst anbietet und einen anderen Dienst nutzt also Server bezüglich des einen Diensts und Client bezüglich des anderen Diensts ist. Charakteristisch für die meisten Client-Server-Systeme ist, dass nur wenige Server, aber viele Clients vorhanden sind. Die Dienste sind also relativ stark zentralisiert. Das erleichtert ihre Verwaltung und auch ihren Schutz, macht aber das Gesamtsystem anfällig gegen Überlastung und Ausfall zentraler (Server-)Komponenten Zeitliche Abläufe Bei der Client-Server-Kooperation sind unterschiedliche zeitliche Abläufe denkbar ( Bild nächste Seite): Im einfachsten Fall erwartet der Client keine Antwort des Servers. Nachdem der Auftrag abgeschickt ist, arbeiten also Client und Server uneingeschränkt nebenläufig zueinander weiter.

220 Free ebooks ==> Modelle und Techniken 219 Auftrag ohne Antwort Client Request Server Warten auf Auftrag Weiterarbeit Auftrags- Warten bearbeitung auf nächsten Auftrag BILD 5.3 Mögliche zeitliche Abläufe bei der Client-Server- Kooperation Auftrag mit Antwort: Synchrone Kooperation Client Request Warten auf Antwort Reply Server Auftragsbearbeitung Auftrag mit Antwort: Asynchrone Kooperation mit Nachfrage durch Polling Client Server Request Reply da? nein? nein? Reply Auftragsbearbeitung Reply Auftrag mit Antwort: Asynchrone Kooperation mit Benachrichtigung durch Interrupt Client Server Request irgendeine andere Arbeit Auftragsbearbeitung Unterbrechung Reply Weiterarbeit Meist benötigt der Client jedoch eine Antwort des Servers. Hier sind die folgenden Abläufe möglich: Bei der synchronen Kooperation wartet der Client passiv auf die Antwort. Er kann also während der Wartezeit keine anderen Tätigkeiten ausführen und liegt somit brach. Bei der asynchronen Kooperation führt der Client während der Arbeit des Servers andere Tätigkeiten aus. Er kann hier auf zwei unterschiedliche Arten an die Antwort des Servers gelangen: Beim Polling fragt er selbst wiederholt nach, ob die Antwort vorliegt. Bei einem Interrupt unterbricht der Server die Tätigkeit des Clients und stellt ihm dabei die Antwort zu. Beim Polling wartet der Client also aktiv, bei Interrupts passiv.

221 220 5 Kooperation BILD 5.4 Implementierung von Client und Server durch Prozesse und Threads Implementierungsaspekte Client und Server werden durch Prozesse und/oder Threads implementiert: Lokales System: Client und Server auf demselben Computer Knoten Knoten Prozess Prozess 1 Prozess 2 Thread 1 Thread 2 oder Client Server Client Server Verteiltes System: Client und Server auf verschiedenen Computern eines Rechnernetzes Knoten A Prozess 1 Client Knoten B Prozess 2 Server Server Thread für Auftrag I Thread für Auftrag II... Aufgabe 5A.2.2. In einem lokalen System laufen Client und Server auf demselben Computer. Sie können als eigenständige Prozesse oder als Threads desselben Prozesses realisiert werden und sämtliche Kommunikationstechniken nutzen, die in Kapitel 4 besprochen wurden. Das Client-Server-Modell dient hier vornehmlich zur übersichtlichen Strukturierung der Software. In einem Rechnernetz laufen Client und Server oft auf unterschiedlichen Computern. Sie müssen dann als eigenständige Prozesse implementiert werden und können nur netzfähige Kommunikationstechniken (insbesondere Sockets, 4.2.4) nutzen. Das Client-Server-Modell dient hier auch zur Verteilung von Diensten im Rechnernetz und ermöglicht ihre Nutzung über Rechnergrenzen hinweg. Es ist damit die Basis zur Realisierung eines verteilten Systems ( ). Ist der Server ein eigenständiger Prozess, so ist es sinnvoll, jeden Auftrag durch einen eigenen Thread innerhalb dieses Prozesses bearbeiten zu lassen: Trifft ein Auftrag ein, so wird für ihn ein Thread neu erzeugt oder einem Pool bereitstehender Threads entnommen. Der Thread kümmert sich dann nur um diesen Auftrag und wird am Ende der Bearbeitung gelöscht oder in den Pool zurückgegeben. Die Bearbeitungen mehrerer Aufträge werden also sauber getrennt als nebenläufige Aktivitäten programmiert und können den Datenbestand des Server-Prozesses unmittelbar gemeinsam nutzen. Bei der Implementierung ist weiterhin zu klären, wie die Funktionalität auf Client und Server verteilt werden soll. Als Grundlage dient hier oft das Three Tier Model, das die Software in drei Schichten strukturiert: Die oben liegende Benutzerschnittstelle (zur Ein- und Ausgabe von Daten) und die unten liegende Datenbasis (zur Speicherung von Daten) werden durch die Verarbeitungslogik verbunden, in der die eigentliche Datenverarbeitung stattfindet. Wie die folgende Abbildung zeigt, können die drei

222 5.1 Modelle und Techniken 221 Schichten unterschiedlich verteilt werden, woraus Fat Clients oder Thin Clients resultieren. Zudem können die unteren beiden Schichten durch zwei verschiedene Server realisiert werden, so dass der Verarbeitungslogik-Server als Client gegenüber dem Datenbasis-Server auftritt: Benutzerschnittstelle (User-Interface Level) Verarbeitungslogik (Processing Level) Client Client Client Client Server 1 BILD 5.5 Three Tier Model zur Verteilung der Funktionalität auf Client und Server Datenbasis (Data Level) Server Server Server Server 2 Three Tier Model Fat Client Thin Client Das Peer-to-Peer-Modell Eine Alternative zum Client-Server-Modell ist das Peer-to-Peer-Modell: Das Peer-to-Peer-Modell (kurz: P2P-Modell) ist ein Kooperationsmodell, in dem die Ressourcen der Dienste auf die beteiligten Prozesse verteilt sind. Die Prozesse nutzen ihre Ressourcen wechselseitig und sind damit Peers (deutsch: Gleichrangige). Die Peers und damit die Ressourcen befinden sich üblicherweise auf mehreren Computern eines Rechnernetzes. Aufgabe 5A.2.4. DEFINITION Peer-to-Peer- Modell Ein bekanntes Beispiel sind File-Sharing-Systeme, bei denen eine Sammlung von Dateien auf die Festplatten der beteiligten Peers verteilt und dort für jeden der Peers zugreifbar ist: Peer 1 Datei Datei Request Peer 2 Datei Request Datei Datei Request Datei Peer 3 BILD 5.6 Peer-to-Peer- Kooperation in einem File-Sharing- System Datei Datei Request Datei Datei Peer 4

223 222 5 Kooperation Im Gegensatz zu Client-Server-Systemen arbeiten Peer-to-Peer-Systeme dezentral. Vor- und Nachteile sind damit umgekehrt zu denen im Client-Server-Modell: Ressourcen und Arbeitslast werden über das ganze Netz verteilt, so dass Überlastungen seltener auftreten und Ausfälle weniger kritisch sind. Jedoch erschwert die dezentrale Organisation die Suche nach bestimmten Ressourcen sowie den Schutz des Systems Programmiertechniken Kooperation wird, wie eingangs gesagt, auf der Basis eines Kommunikationssystems realisiert. Im einfachsten Fall arbeitet der Programmierer direkt auf der Schnittstelle des Kommunikationssystems (beispielsweise mit Sockets, 4.2.4) und programmiert alle Schritte der Kooperation selbst aus. Das Abstraktionsniveau ist hier allerdings niedrig, da das Programm viele Details explizit regeln muss. Es empfiehlt sich daher, Kooperationstechniken mit einem höheren Abstraktionsgrad zu benutzen: Bei prozeduralen Techniken (Remote Procedure Call, RPC) spezifiziert man Dienste durch Funktionsschnittstellen, die wie Funktionsköpfe in einer prozeduralen Sprache (zum Beispiel C) aussehen. Ruft ein Client eine solche Funktion auf, so wird der Aufruf automatisch über das Netz an den Server übertragen. Objektorientierte Techniken (zum Beispiel Remote Method Invocation, RMI) gehen ähnlich vor, spezifizieren die Dienste jedoch nicht durch Funktionen, sondern durch Objekte mit Methoden. Webbasierte Techniken nutzen die allgemein verfügbaren Mechanismen des World Wide Web, um Aufträge und Antworten über das Internet zu übertragen Prozedurorientierte Kooperation Aus BILD 5.3 wird deutlich, dass die synchrone Client-Server-Kooperation wie ein Funktionsaufruf abläuft: Der Client übergibt einen Auftrag mit Parametern an den Server, wartet bis zum Ende seiner Bearbeitung, erhält dann ein Resultat zurück und arbeitet weiter. Damit bietet es sich an, die Schnittstelle, die der Server bereitstellt und der Client benutzt, syntaktisch so zu formulieren wie bei einer Funktion in einer prozeduralen Sprache also in der Form rückgabetyp funktionsname(parameter). Da der Server meist auf einem anderen Knoten läuft als der Client, werden Aufruf und Rückgabe über das Netz übertragen. Man spricht daher von einem Remote Procedure Call (RPC, manchmal auch deutsch: Prozedurfernaufruf). Das Bild auf der nächsten Seite zeigt den Ablauf eines RPC und die beteiligten Komponenten: Der Server definiert seinen Dienst durch eine Funktion, also mit Kopf und Körper. Der Körper berechnet, wie üblich, den Funktionswert und gibt ihn durch eine return-anweisung zurück. Da diese Funktion nicht nur lokal auf dem Server, sondern auch von anderen Rechnerknoten aus aufgerufen werden kann, wird sie als Remote Procedure bezeichnet. Auf Seiten des Clients liegt der so genannte Client-Stub, eine Funktion mit (im Wesentlichen) derselben Schnittstelle wie die Funktion des Servers. Der Körper dieses Stubs berechnet jedoch das Resultat nicht selbst, sondern leitet Aufrufe über

224 5.1 Modelle und Techniken 223 Client Anwendungsprogramm dieselbe Schnittstelle wie die Server-Funktion BILD 5.7 Remote Procedure Call (RPC) rückgabetyp funktionsname(parameter) Client-Stub Aufruf Wertrückgabe Server Server-Stub rückgabetyp funktionsname(parameter)... return wert Server-Funktion ( Remote Procedure ) das Netz an den Server weiter, kümmert sich also nur um die Kommunikation mit dem Server. Sein Gegenstück auf der Server-Seite ist dabei der Server-Stub. Das Anwendungsprogramm des Clients ruft seinen Stub als lokale Funktion auf und übergibt dabei aktuelle Parameter. Der Client-Stub erzeugt daraus eine Nachricht mit der Auftragsbeschreibung (insbesondere mit Funktionsnamen und Parametern) und schickt sie über das Kommunikationssystem an den Server-Stub. Der Server-Stub ruft die Server-Funktion mit den erhaltenen Parametern lokal auf, erhält das Resultat zurück und schickt dieses dann über das Kommunikationssystem zurück an den Client-Stub. Dieser kehrt mit Ergebnisrückgabe wie aus einem lokalen Funktionsaufruf zurück. Das Einpacken von Parametern in eine Nachricht (mit Umwandlung in eine externe, also allgemein verständliche Datendarstellung) wird auch als Marshalling bezeichnet (to marshal sth. = etw. ordnen). Der umgekehrte Vorgang heißt Unmarshalling. Damit sieht ein RPC für den Anwendungsprogrammierer des Clients im Idealfall wie der Aufruf einer lokalen Funktion aus. Die Kommunikation wird durch die dahinterliegenden Stubs erledigt. Sie bleibt für den Programmierer unsichtbar, da die Stubs automatisch aus der Schnittstellenspezifikation der Server-Funktion generiert werden (siehe dazu die Vorgehensweise in UNIX/Linux, 5.2.2). Für diese Spezifikation wird oft eine Interface Definition Language (IDL) benutzt, die sowohl auf Serverals auch auf Client-Seite verstanden wird ( 5.2.2). Leider unterscheiden sich in der Praxis entfernte Aufrufe von lokalen Aufrufen stärker, als es zunächst den Anschein haben mag: Ein Aufruf über das Netz dauert deutlich länger als ein lokaler Aufruf, so dass ein passives Warten des Clients (vergleiche BILD 5.3) von Nachteil ist. Zudem können bei der Datenübertragung Fehler auftreten, oder der Server kann ganz ausfallen. Weiterhin haben Server und Client getrennte Adressräume, so dass die Übergabe von Zeigern (Pointern) nicht unmittelbar möglich ist. Schließlich sind Client und Server möglicherweise heterogen, codieren also dieselben Werte durch unterschiedliche Bitmuster. All dies muss bei der Realisierung und Nutzung eines RPC-Systems berücksichtigt werden.

225 224 5 Kooperation Aufgabe 5A.2.3. BILD 5.8 Remote Method Invocation (RMI) Objektorientierte Kooperation Neben prozedurale Programmiersprachen wie C sind im Laufe der Zeit objektorientierte Sprachen wie Java getreten. Es lag also nahe, die Idee des RPC auf den Aufruf von Objektmethoden zu übertragen also auf die Benutzung von Remote Objects, die auf einem anderen Computer liegen als der Aufrufer. Man spricht hier, insbesondere in Java, von Remote Method Invocation (RMI). Client Anwendungsprogramm Schnittstelle einer Objektmethode: rückgabetyp methodenname(parameter) Proxy-Objekt Aufruf Wie die Abbildung zeigt, ist die grundlegende Struktur dieselbe wie beim RPC. Die Aufgabe des Client-Stubs übernimmt hier ein Proxy-Objekt (kurz: Proxy, deutsch: Stellvertreter; manchmal auch hier Client-Stub genannt), das dieselbe Schnittstelle wie das Server-Objekt hat und sich um die Kommunikation mit dem Server kümmert. Sein Gegenstück auf Seiten des Servers ist ein Skeleton (manchmal auch Server-Stub genannt). Die Ausführung der eigentlichen Methode findet, analog zum RPC, auf Seiten des Servers im Remote Object statt. Proxies und Skeletons werden, wie Stubs beim RPC, automatisch aus der Schnittstellenbeschreibung des Remote Objects generiert. Objektorientierte Kooperation kann auf zwei Arten realisiert werden: Ein sprachspezifischer Ansatz ergänzt eine bestimmte objektorientierte Sprache um Konzepte, mit denen Remote Objects definiert und genutzt werden können. Ein Beispiel hierfür ist Java RMI ( 5.3.1). Aufgrund der Homogenität (alle Teile des verteilten Anwendungsprogramms sind in derselben Sprache geschrieben) ist dieses Ansatz relativ einfach zu realisieren. Nachteilig ist jedoch die mangelnde Offenheit: Software, die in einer anderen Sprache geschrieben wurde, kann nicht unmittelbar eingebunden werden. Ein sprachunabhängiger Ansatz basiert auf einer Middleware ( ), die Objektaufrufe in einem verteilten System vermittelt. Diese Middleware unterstützt Heterogenität; die beteiligten Objekte können also in unterschiedlichen Sprachen programmiert sein. Beispiele sind CORBA (Common Object Request Broker Architecture) und.net; auch Web Services ( ) lassen sich hier einordnen. Dass sich die Objekte untereinander verstehen, wird erreicht durch eine übergreifende Interface Definition Language (IDL, ), die die Objektschnitt- Wertrückgabe Server Skeleton Remote Object des Servers

226 5.1 Modelle und Techniken 225 stellen spezifiziert, sowie durch die Übersetzung von Parameter- und Rückgabewerten in eine Form, die der jeweilige Empfänger versteht. Dies macht natürlich den sprachunabhängigen Ansatz komplexer als den sprachabhängigen Webbasierte Kooperation Die meisten heutigen Computer haben Zugang zum World Wide Web (WWW), implementieren also HTTP auf der Grundlage von TCP/IP ( ) sowie Browser, die mit Web Servern kommunizieren und deren Dokumente ( Webseiten ) weiterverarbeiten und anzeigen können. Es bietet sich also an, diese Mechanismen zur Kooperation im Internet einzusetzen. Zwei wichtige Ansätze hierfür sind dynamische Webseiten und Web Services: Webseiten werden zu dynamischen Seiten, indem man Programmcode in sie einbettet. Je nachdem, wo dieser Code abläuft, unterscheidet man zwischen serverseitiger und clientseitiger Ausführung: Bei der serverseitigen Ausführung läuft das Programm auf dem Server ab. Dies geschieht, um zur Anfrage eines Clients ein passendes aktuelles Dokument zu generieren wie es beispielsweise bei einer Suchmaschine oder Fahrplanauskunft geschieht. Techniken und Programmiersprachen, die hier verwendet werden, sind ursprünglich das Common Gateway Interface (CGI) und heute auch PHP, Java Servlets und Java Server Pages (JSP) ( 5.3.2). Bei der clientseitigen Ausführung läuft das Programm auf dem Client ab. Dies geschieht, um dem Benutzer einen dynamischen Umgang mit der angezeigten Seite zu ermöglichen beispielsweise, indem die Anzeige je nach Position des Mauszeigers geändert wird oder indem die Seite auf Benutzereingaben reagiert. Entsprechende Techniken und Sprachen sind Java Applets und JavaScript. Eine aktuelle Weiterentwicklung ist Ajax (Asynchronous JavaScript and XML), das Teile der gezeigten Seite dynamisch vom Server nachlädt und so die Anzeige verändert. Dynamische Webseiten richten sich unmittelbar an menschliche Benutzer. Web Services implementieren dagegen Dienste, die aus Anwendungsprogrammen heraus (also gewissenmaßen eine Ebene tiefer) genutzt werden können. Client als Nutzer eines Web Service Server als Anbieter eines Web Service BILD 5.9 Web Services und SOAP SOAP- Nachricht HTTP Internet mit Kommunikationsprotokollen und WWW-Infrastruktur HTTP SOAP- Nachricht Die Übertragung von Aufträgen und Antworten für Web Services basiert auf SOAP. Diese Abkürzung stand früher für Simple Object Access Protocol. Da aber SOAP in der Praxis nicht übermäßig einfach ist und außerdem nicht nur für Objektzugriffe verwendet werden kann, sieht man SOAP heute als Namen ohne dahinterliegende Bedeutung an.

227 226 5 Kooperation SOAP konzentriert sich im Wesentlichen darauf, das Format der übertragenen Nachrichten zu spezifizieren, wobei es sich auf die Extensible Markup Language (XML) stützt. Die Nachrichten können Parameter und Rückgabewerte für Fernaufrufe codieren (in diesem Fall spricht man von einem XML-RPC); sie können aber auch beliebig strukturierte Dokumente enthalten. Die Übertragungsvorgänge selbst regelt SOAP nicht; hierfür ist ein standardisiertes Internet-Protokoll (meist HTTP) zuständig. SOAP wird ergänzt durch WSDL (Web Services Description Language). WSDL ist eine XML-basierte Sprache, in der die Schnittstellen der angebotenen Dienste beschrieben werden. Aus einer WSDL-Beschreibung kann ein Client automatisch und dynamisch (also während der Laufzeit) den Code generieren, den er zum Aufruf des Server-Diensts benötigt. Zu SOAP und XML, die im Verbund recht komplex werden können, gibt es einfachere Alternativen. So beschreibt REST (REpresentational State Transfer), wie Dienste allein mit Hilfe der Standard-HTTP-Kommandos aufgerufen werden können. JSON (JavaScript Object Notation) ermöglicht eine im Vergleich zu XML zwar eingeschränkte, aber dafür auch weniger komplexe Codierung von Nachrichten. 5.2 Techniken in UNIX/Linux Die UNIX/Linux-Programmschnittstelle beruht auf der prozeduralen Programmiersprache C und unterstützt daher Prozedurfernaufrufe (RPC) unmittelbar. Daneben kann man auf der Basis von Sockets Kooperationsvorgänge explizit ausprogrammieren. Ist auf einem UNIX/Linux-System zusätzlich Java implementiert, stehen natürlich auch dessen objektorientierte Techniken zur Verfügung ( 5.3) Kooperation über Sockets PROG 5.1 Laden einer WWW- Seite durch ein Benutzerkommando Kooperationsvorgänge zwischen Client und Server werden durch Protokolle ( ) gesteuert. Kennt man also die Spezifikation des Protokolls, das ein Server versteht, so kann man einen Client programmieren, der über einen Socket ( 4.2.4) eine Verbindung zum Server aufbaut und dann mit ihm per read() und write() Protokollnachrichten austauscht. Recht einfach ist dies bei Protokollen, die nur einige wenige anwendungsnahe, textuelle Befehle austauschen und Details der Kommunikationssteuerung niedrigeren Protokollschichten überlassen. Das ist beispielsweise beim WWW-Protokoll HTTP der Fall, das auf TCP und IP aufsetzt ( ). Das folgende Beispiel zeigt, dass man schon über die UNIX/Linux-Benutzerschnittstelle HTTP-Befehle übertragen kann. Mit seinen fünf Befehlszeilen, die man über die Tastatur eingibt, lädt man eine Seite von einem Web Server: telnet 80 >webpage.html<cr> GET /vogt/index.html HTTP/1.1<CR> Host: Connection: Close<CR> <CR>

228 5.2 Techniken in UNIX/Linux 227 <CR> steht für Carriage Return, also das Drücken der Enter-Taste. Der telnet-befehl in der ersten Zeile baut eine Verbindung zu Port 80 des Rechnerknotens auf also des hier anzusprechenden Web Servers. Port 80 ist standardmäßig für die HTTP-basierte Kommunikation zuständig, also für die Übertragung von Webseiten (vergleiche ). Mit der Ausgabeumlenkung > wird die eintreffende Seite in der Datei webpage.html abgelegt. Die nächsten Zeilen formulieren einen HTTP-GET-Befehl bezüglich der Seite /vogt/index.html und schicken ihn über die Verbindung an den Server ab. Der Server reagiert darauf, indem er die Seite liefert. Aus einem C-Programm heraus kann man analog vorgehen. Befehle und Daten werden hier, wie oben skizziert, über einen Socket mit read()- und write()-aufrufen übertragen. Das folgende Programmbeispiel arbeitet dazu wie PROG 4.5: #include <netdb.h> #include <fcntl.h> #include <stdlib.h> #include <string.h> #define BUFSIZE 8192 main() { int sock; /* ID des eigenen Sockets */ int error, numbytes, fd; /* Hilfsvariablen */ char buffer[bufsize]; /* Zwischenspeicher für eintreffende Daten */ struct sockaddr_in server_addr; /* Server-Adresse */ struct hostent *host; /* zur Verarbeitung einer symbol. Adresse */ /* Erzeugung eines Stream-Sockets in der Internet-Domain */ sock = socket(af_inet,sock_stream,0); /* Zusammenstellen der Server-Adresse */ server_addr.sin_family = AF_INET; host = gethostbyname(" bcopy(host->h_addr,&server_addr.sin_addr,host->h_length); server_addr.sin_port = htons(80); /* Verbindung des Sockets mit dem Socket des Servers */ error = connect(sock,(struct sockaddr *) &server_addr, sizeof(struct sockaddr)); if (error == -1) exit(-1); /* Übertragung des HTTP-GET-Befehls */ write(sock,"get /vogt/index.html HTTP/1.1\n",30); write(sock,"host: write(sock,"connection: Close\n\n",19); /* Erzeugung einer lokalen Datei zur Aufnahme der Web-Seite */ fd = open("webpage.html",o_creat O_WRONLY,0777); /* Blockweises Einlesen und Speichern der Seite (vergleiche ) */ PROG 5.2 Laden einer WWW-Seite über einen Socket

229 228 5 Kooperation do { numbytes = read(sock,buffer,bufsize); write(fd,buffer,numbytes); while (numbytes>0); close(fd); Remote Procedure Call (RPC) Aufgaben 5A Linux und andere UNIX-Versionen unterstützen ONC RPC (Open Network Computing, früher: Sun RPC) als Mechanismus für Prozedurfernaufrufe. ONC RPC setzt üblicherweise auf die Protokollkombinationen UDP/IP oder TCP/IP auf, die die Datenübertragung über das Internet regeln. Im Folgenden wird nur ein kurzer Abriss des Umgangs mit ONC RPC gegeben; eine sehr detaillierte Beschreibung findet man beispielsweise in [Come01] Komponenten und ihr Zusammenspiel Die ONC-RPC-Programmierung stützt sich im Wesentlichen auf drei Dinge, nämlich die Interface Definition Language XDR (External Data Representation), den Stub-Generator rpcgen und die RPC-Aufruf-Funktionen callrpc() sowie clnt_create(). BILD 5.10 Kooperation durch ONC RPC Client-Knoten Schnittstellen-Spezifikation in XDR rpcgen Server-Knoten Header-Datei Header-Datei Client-Stub Marshalling Server-Stub Marshalling C-Quellcode Client-Hauptprogramm Server-Funktion Client Übersetzung Aufruf mit callrpc() oder auf Basis von clnt_create() Server Übersetzung Registrierung Betriebssystem ausführbarer Code Diese Komponenten setzt man wie folgt ein (vergleiche jeweils , ein konkretes Beispiel wird in besprochen): Man spezifiziert in XDR die Schnittstellen der Server-Funktionen, zugehörige Datentypen und Konstanten sowie eindeutige Nummern für Server-Programme, ihre Versionen und die einzelnen Funktionen eines Servers. XDR-Beschreibungen werden in Dateien mit der Namensendung.x abgelegt; sie enthalten sämtliche Informationen, die man für die weitere RPC-Programmierung benötigt.

230 Free ebooks ==> Techniken in UNIX/Linux 229 Aus der XDR-Beschreibung erzeugt man mit dem RPC-Generator rpcgen Quellcode-Dateien, die zur Kommunikation und Kooperation zwischen Client und Server dienen. Im Einzelnen sind dies eine Header-Datei mit den Funktionsprototypen sowie zugehörigen Datentypen und Konstanten, zwei Dateien mit dem Client- und dem Server-Stub sowie eine Datei mit Funktionen zum Marshalling der Daten. Die Server-Stub-Datei enthält nicht nur Programmcode für die eigentliche Kommunikation mit dem Client-Stub, sondern auch für die Registrierung (also öffentliche Bekanntmachung ) des Servers beim Betriebssystem sowie für die Aktivierung des Servers, damit er spätere Aufrufe entgegennehmen kann. Die Server-Funktion programmiert man mit normalen C-Sprachkonstrukten, benötigt hierfür also nichts RPC-Spezifisches. Bei der Übersetzung bindet man sie mit den Dateien zusammen, die rpcgen erzeugt hat, und erhält dadurch ein netzfähiges Server-Programm. Im Client-Hauptprogramm ruft man die Server-Funktion auf. Dazu kann man die UNIX/Linux-Schnittstellenfunktion callrpc() benutzen: int callrpc( char *host, unsigned long prognum, unsigned long versnum, unsigned long procnum, xdrproc_t inproc, char *in, xdrproc_t outproc, char *out) 0 bei fehlerfreier Ausführung Fehlercode sonst #include <rpc/rpc.h> IP-Adresse des Computers, auf dem der Server läuft (numerisch oder symbolisch) Nummer des Server-Programms (gemäß XDR-Datei) Version des Server-Programms (gemäß XDR-Datei) Nummer der aufzurufenden Server-Funktion (gemäß XDR-Datei) Funktion zum Marshalling der Parameter Referenz auf die Parameter der Server-Funktion Funktion zum Unmarshalling der Rückgabe Speicheradresse, an der der Rückgabewert der Server-Funktion abgelegt werden soll Die Namen der Funktionen zum Marshalling bzw. Unmarschalling lauten einheitlich xdr_typname, wobei typname der Name des Parameter- bzw. Rückgabetyps ist. Die Funktionen sind entweder vordefiniert (nämlich für die skalaren Standardtypen) oder werden durch rpcgen erzeugt. Eine Alternative zu callrpc() ist die Funktion clnt_create(): CLIENT *clnt_create( char *host, unsigned long prog, unsigned long vers, char *proto) Handle für Serverzugriff oder NULL im Fehlerfall #include <rpc/rpc.h> IP-Adresse des Computers, auf dem der Server läuft (numerisch oder symbolisch) Nummer des Server-Programms (gemäß XDR-Datei) Version des Server-Programms (gemäß XDR-Datei) zu benutzendes Transportprotokoll ( TCP / UDP )

231 230 5 Kooperation clnt_create() liefert einen Handle für ein Server-Programm, der anschließend mehrfach für die Kooperation mit dem Server verwendet werden kann beispielsweise für mehrere Funktionsaufrufe. Bei der Übersetzung wird das Client-Programm mit den oben erzeugten Client- Stub- und Marshalling-Funktionen zusammengebunden. Die Aufrufe werden dann mit Hilfe des Betriebssystems des Server-Knotens an den Server-Prozess geleitet. PROG 5.3 Kooperation durch ONC RPC Schritte der Programmierung Wie man bei der Programmierung im Einzelnen vorgeht, wird am Beispiel eines Servers gezeigt, der eine Funktion zur Multiplikation zweier Gleitkommazahlen anbietet: Schritt 1: Die Server-Schnittstelle durch eine XDR-Datei spezifizieren Der Programmierer erstellt die Datei arith.x mit dem folgenden XDR-Code: struct floatpair { float a; float b; ; program ARITH_PROG { version ARITH_VERS { float MULT(floatpair) = 1; = 1; = 0x26E1A3F2; Der erste Teil dieser Datei definiert einen Typ für Paare von Gleitkommazahlen, die der Funktion als Parameter übergeben werden sollen. Im zweiten Teil folgt die Definition der Server-Schnittstelle: Die Zeile im Innersten der Klammerschachtelung besagt, dass die Funktion MULT heißt, dass sie ein Paar von Gleitkommazahlen als Parameter erwartet und eine Gleitkommazahl zurückliefert. Die 1 am Ende der Zeile ist die Ordnungsnummer der Funktion beim Server. Man könnte darunter also die Schnittstellen weiterer Funktionen anfügen und mit 2, 3,... durchnummerieren. Der Klammerblock ARITH_VERS mit der zugehörigen 1 benennt die Version des Servers. Es können also mehrere Versionen eines Servers nebeneinander existieren, die man anhand dieser Versionsnummer unterscheidet. Der äußere Klammerblock ARITH_PROG versieht das Server-Programm mit einer eindeutigen Identifikationsnummer. Sie muss eine achtstellige Hexadezimalzahl zwischen 0x und x3fffffff sein. Schritt 2: Mit dem RPC-Generator den Hilfscode für Fernaufrufe erzeugen Der Programmierer ruft über die Benutzerschnittstelle den RPC-Generator auf: rpcgen arith.x Dieser Aufruf erzeugt vier C-Quellcode-Dateien die Header-Datei arith.h, Client- und Server-Stubs arith_clnt.c bzw. arith_svc.c und Marshalling-Funktionen arith_xdr.c. Die Header-Datei enthält C-Konstanten, C-Typen und C- Funktionsprototypen, die den XDR-Spezifikationen aus arith.x entsprechen.

232 5.2 Techniken in UNIX/Linux 231 Schritt 3: Die neu erzeugten Dateien auf die Ziel-Computer bringen Der Programmierer bringt die Dateien, die von rpcgen erzeugt wurden, auf die Rechnerknoten, auf denen der Server bzw. der Client ausgeführt werden sollen: Auf den Server-Knoten kommen die Dateien arith.h, arith_svc.c und arith_xdr.c. Auf den Client-Knoten kommen die Dateien arith.h, arith_clnt.c und arith_xdr.c. Das muss natürlich nur dann geschehen, wenn rpcgen nicht auf dem Client- bzw. Server-Knoten aufgerufen wurde. Schritt 4: Den Server programmieren Der Programmierer erstellt für den Server die Datei arithserver.c mit dem folgenden C-Quellcode: #include <rpc/rpc.h> #include "arith.h" float* mult_1_svc(floatpair *param, struct svc_req *r) { static float result; result = param->a * param->b; return (&result); Offensichtlich muss man also nur die Server-Funktion selbst programmieren. Alles andere erzeugt rpcgen automatisch insbesondere auch das Hauptprogramm des Servers, in dem auf Aufträge gewartet und deren Ausführung gesteuert wird. Die Schnittstelle der Funktion ergibt sich aus den Festlegungen der XDR-Datei, die durch das #include der C-Header-Datei eingebunden werden. Der Funktionsname wird aus dem in der XDR-Datei angegebenen Namen (hier in Kleinbuchstaben!) und der dort genannten Versionsnummer des Servers (also nicht der Funktionsnummer!) gebildet; zudem wird die Zeichenfolge _svc angehängt. Der Rückgabewert wird in einer statischen Variablen abgelegt, die das Ende der Funktionsausführung überdauert, und als Funktionswert wird eine Referenz auf diese Variable zurückgegeben. Auch die Parameter werden per Referenz übergeben. Der erste Parameter umfasst in einer Struktur alle Werte, die an die Server- Funktion übergeben werden sollen; der zweite Parameter enthält Informationen über den Auftrag und muss im Server nicht unbedingt ausgewertet werden. Schritt 5: Den Server übersetzen und starten Der Programmierer übersetzt den Server: cc -o arithserver arithserver.c arith_svc.c arith_xdr.c Hier wird der selbst programmierte Server-Code mit den Programmteilen, die von rpcgen erstellt wurden, zusammengebunden. Es resultiert ein ausführbares Server-Programm, das nun gestartet wird (vorzugsweise als Hintergrundprozess, damit weitere Tastatureingaben möglich sind):./arithserver &

233 232 5 Kooperation Der Code, der aus arith_svc.c hervorgegangen ist, registriert den Server beim Betriebssystem, teilt ihm also mit, welchen Dienst er anbietet. Zudem wird der Server in einen Wartezustand versetzt, in dem er auf eintreffende Aufrufe wartet. Unter Linux ist der Portmapper-Prozess portmap für die Registrierung von Diensten zuständig. Ihm ist standardmäßig Port 111 ( ) zugeordnet. Clients (siehe nächster Schritt) wenden sich zunächst an diesen fest vorgegebenen Port, um dann mit Unterstützung des Portmappers den eigentlichen Server kontaktieren zu können. Schritt 6: Den Client programmieren Der Programmierer erstellt für den Client die Datei arithclient.c mit dem folgenden C-Quellcode: #include "arith.h" #include <rpc/rpc.h> #include <stdio.h> main(int argc, char *argv[]) { char *server; floatpair parameter; float *ergebnis; int error; server = argv[1]; parameter.a =...; parameter.b =...; ergebnis = malloc(sizeof(float)); error = callrpc(argv[1],arith_prog,arith_vers,mult, (xdrproc_t)xdr_floatpair,(char *)&parameter, (xdrproc_t)xdr_float,(char *)ergebnis); if (error!=0) { /* Bildschirmausgabe einer spezifischen Fehlermeldung */ clnt_perrno(error); exit(-1); printf("ergebnis: %f\n",*ergebnis); Die callrpc()-funktion ist offensichtlich recht komplex. Es müssen zahlreiche RPC-spezifische Informationen explizit übergeben werden, und es müssen Typen explizit angepasst werden. Damit sieht ein entfernter Funktionsaufruf längst nicht so einfach aus wie ein lokaler Aufruf. Deutlich übersichtlicher wird es, wenn man die vorbereitende Funktion clnt_create() benutzt. Das Client-Programm hat dann die folgende Form: #include "arith.h" #include <rpc/rpc.h> #include <stdio.h> main(int argc, char *argv[]) { CLIENT *cl; char *server;

234 5.2 Techniken in UNIX/Linux 233 floatpair parameter; float *ergebnis; int error; server = argv[1]; cl = clnt_create(server,arith_prog,arith_vers,"udp"); parameter.a =...; parameter.b =...; ergebnis = mult_1(&parameter,cl); if (ergebnis==null) { clnt_perrno(error); exit(-1); printf("ergebnis: %f\n",*ergebnis); Der Name der Server-Funktion, der in der zweiten Client-Version verwendet wird, ergibt sich aus den Angaben in der XDR-Datei: Es ist der dort genannte Funktionsname (hier in Kleinbuchstaben!) mit der Versionsnummer des Servers (also nicht der Funktionsnummer!). Ruft man einen dieser beiden Clients auf, so muss man die Server-Adresse als Parameter übergeben; der Client übernimmt sie dann über argv[1]. Alternativ könnte man die Adresse in callrpc() bzw. clnt_create() als Konstante übergeben, wodurch man aber an Flexibilität einbüßen würde. Die Server-Funktion liefert eine Referenz auf den Funktionswert. Dies ist zu beachten, wenn man mit ihm weiterarbeitet (hier: Dereferenzierung *ergebnis in der letzten Programmzeile). Wenn der Aufruf fehlschlug (beispielsweise weil der Server nicht aktiv war), erhält man in der zweiten Client-Version die NULL-Referenz zurück. Schritt 7: Den Client übersetzen und starten Der Programmierer übersetzt den Client: cc -o arithclient arithclient.c arith_clnt.c arith_xdr.c Auch wird hier also der selbst programmierte Client-Code mit den Programmteilen, die von rpcgen erstellt wurden, zusammengebunden. Es resultiert ein ausführbares Client-Programm, das nun gestartet wird:./arithclient xxx.fh-koeln.de oder./arithclient yyy Der Client-Stub in arith_clnt.c schickt mit Hilfe des Portmappers den Aufruf an den Server und erhält das Ergebnis von ihm zurück. Wie in Schritt 6 besprochen, muss bei diesem Client-Beispielprogramm die Internet-Adresse des Servers, in symbolischer oder numerischer Form, als Parameter übergeben werden. Sollen Server und Client getrennt voneinander programmiert werden, so geht man wie folgt vor: Zunächst schreibt der Server-Programmierer die XDR-Datei, erledigt die Programmierschritte, die den Server betreffen (einschließlich rpcgen-aufruf), startet

235 234 5 Kooperation das resultierende Server-Programm und legt die XDR-Datei an einer allgemein zugänglichen Stelle ab. Später lädt der Client-Programmierer die XDR-Datei zu sich herunter, führt die Schritte zur Programmierung des Clients aus (einschließlich rpcgen-aufruf), startet sein Programm und kontaktiert damit den wartenden Server. Offensichtlich enthält die XDR-Datei alle Informationen, die der Client-Programmierer benötigt; er muss also den Quellcode des Servers nicht einsehen. Natürlich können auch mehrere Clients dieselbe Server-Funktion aufrufen. 5.3 Techniken in Java Java als objektorientierte Sprache unterstützt Remote Method Invocation (RMI) zum Zugriff auf Remote Objects. Darüber hinaus lassen sich dynamische Webseiten durch Applets, Servlets und Java Server Pages (JSP) sowie Web Services durch JAX-WS programmieren. Schließlich kann man auch hier (analog zu 5.2.1) mit Sockets arbeiten. In Java EE bieten Enterprise JavaBeans eine breite Infrastruktur zur Programmierung verteilter Anwendungen (im Folgenden nicht behandelt, siehe hierzu [Back11] oder [Eber11]) Remote Method Invocation (RMI) Die folgende Beschreibung von RMI beschränkt sich auf die wichtigsten Grundlagen; viele weitere Einzelheiten findet man beispielsweise bei [Abts10] oder [Oech11].) Komponenten und ihr Zusammenspiel Die Java-RMI-Programmierung stützt sich im Wesentlichen auf drei Dinge, nämlich Remote Interfaces, die die Schnittstellen von Remote Objects spezifizieren, Remote Objects, die über das Internet zugreifbar sind, sowie eine Registry, die Informationen über die verfügbaren Remote Objects speichert. BILD 5.11 Kooperation durch Java RMI Client-Knoten Client-Programm Anfrage nach einem Objekt mit einem bestimmten Namen Server-Knoten RMI Registry dynam. Erzeugung Objekt-Referenz Registrierung unter einem Namen Remote Interface Proxy Methodenaufruf Remote Object Auf dieser Grundlage ergeben sich die folgenden Komponenten eines Java-RMI-Programms (vergleiche auch ; ein konkretes Beispiel folgt in ):

236 5.3 Techniken in Java 235 Die Schnittstellen der Operationen des Servers werden durch Java-Interfaces spezifiziert. Sie erben vom vorgegebenen Interface Remote und werden so zu Remote Interfaces. Die Operationen des Servers werden in Java-Klassen ausprogrammiert, die die Remote Interfaces implementieren. Diese Klassen erben von der vorgegebenen Klasse UnicastRemoteObject, die Dienste zum Export von Objekten bietet, also zu ihrer Bereitstellung nach außen. Objekte dieser Klassen sind damit Remote Objects, also über das Netz zugreifbar. Remote Objects werden vom Server erzeugt und bei der RMI Registry registriert, wobei jedes Objekt einen frei wählbaren Namen erhält. Die Registry ist also ein zentrales Verzeichnis der zugreifbaren Remote Objects. Der Client kontaktiert die Registry des Server-Knotens und übergibt dabei eine URI, die einen dort registrierten Objektnamen enthält. Die Registry gibt eine entsprechende Referenz zurück, über die der Client (mit Hilfe eines dynamisch erzeugten Proxy) Methoden des Remote Objects aufrufen kann. Methodendefinitionen und -aufrufe bei Remote Objects unterscheiden sich syntaktisch nicht von denen lokaler Objekte. Im Unterschied zum ONC RPC werden hier also keine besonderen Funktionen oder Sprachkonstrukte verwendet Schritte der Programmierung Wie beim ONC RPC ( ) soll auch hier die detaillierte Vorgehensweise am Beispiel eines Arithmetik-Servers gezeigt werden, der eine Methode zur Multiplikation zweier Gleitkommazahlen anbietet: Schritt 1: Die Server-Schnittstelle durch ein Remote Interface spezifizieren Der Programmierer erstellt die Datei ArithInterface.java, die die Schnittstelle des Servers spezifiziert: import java.rmi.*; public interface ArithInterface extends Remote { public double mult(double x, double y) throws RemoteException; Die Definition nimmt Bezug auf das Interface Remote. Dieses Interface definiert weder Methoden noch Attribute, sondern zeigt lediglich an, dass Objekte implementierender Klassen von außerhalb zugreifbar (also Remote Objects) sein sollen. RemoteExceptions sind Ausnahmeereignisse, die bei der Kommunikation über das Netz auftreten können. Schritt 2: Das Remote Interface durch eine Klasse implementieren Der Programmierer erstellt die Datei ArithImpl.java mit dem Körper der Server-Funktion: import java.net.*; import java.rmi.*; import java.rmi.server.unicastremoteobject; Aufgaben 5A PROG 5.4 Kooperation durch Java RMI

237 236 5 Kooperation public class ArithImpl extends UnicastRemoteObject implements ArithInterface { public ArithImpl() throws RemoteException { public double mult(double x, double y) throws RemoteException { return x*y; Die Oberklasse UnicastRemoteObject macht Objekte dieser Klasse exportfähig, stellt also Methoden bereit, mit denen man sie nach außen anbieten und von außen zugreifen kann. Die Objekte werden damit zu Remote Objects. Schritt 3: Den Server programmieren Der Programmierer erstellt die Datei ArithServer.java: import java.rmi.*; import java.rmi.server.*; public class ArithServer { public static void main(string args[]) { try { ArithImpl serverobjekt = new ArithImpl(); Naming.rebind("Arith",serverobjekt); catch (Exception e) {... Das Server-Programm erzeugt das Remote Object und registriert es unter dem Namen Arith in der RMI Registry. Die Kommunikation mit der Registry geschieht über die Klasse Naming hier mit der statischen Registrierungsfunktion rebind(). Alternativ kann man die Methode bind() benutzen, die allerdings ein Ausnahmeereignis auslöst, wenn der angegebene Name bereits registriert ist. Wie man sieht, enthält der Server keinen expliziten Code, mit dem er auf Client- Aufrufe wartet und/oder Aufrufe entgegennimmt. Implizit sorgt das RMI-System dafür, dass Aufrufe an den registrierten Server weitergeleitet und von ihm bedient werden. Schritt 4: Den Server übersetzen und starten Der Programmierer übersetzt das Server-Programm: javac ArithServer.java ArithInterface.java ArithImpl.java Anschließend startet er die RMI Registry: rmiregistry & (unter UNIX/Linux) bzw. start rmiregistry (unter Windows) (Alternativ kann die Registry im Server-Programm durch java.rmi.registry.locateregistry.createregistry(1099) gestartet werden die Bedeutung des Werts 1099 wird weiter unten erklärt.) Den Start der Registry darf man nicht vergessen, da sonst das Client-Server-System nicht ausgeführt werden kann. Die Fehlermeldung, die das Java-Laufzeit-

238 5.3 Techniken in Java 237 system des Clients in diesem Fall gibt ( Connection refused... ), ist dabei nicht sehr hilfreich. Schließlich startet er das Server-Programm: java ArithServer & Ist (unter UNIX/Linux) die IP-Adresse des Servers in der Datei /etc/hosts nicht verzeichnet, so muss der Aufruf wie folgt lauten: java -Djava.rmi.server.hostname=x1.x2.x3.x4, wobei x1.x2.x3.x4 die IP-Adresse des Servers ist. Die Adresse kann auch in symbolischer Form angegeben werden. Schritt 5: Die Spezifikation des Remote Interface zum Client-Knoten bringen Der Programmierer überträgt die Datei ArithInterface.java zum Rechnerknoten des Clients. Das muss natürlich nur dann geschehen, wenn (wie es allerdings meist der Fall ist) der Client auf einem anderen Rechnerknoten läuft als der Server. Schritt 6: Den Client programmieren Der Programmierer erstellt die Datei ArithClient.java: import java.rmi.*; public class ArithClient { public static void main(string args[]) { try { ArithInterface arith; String path; path = new String("rmi://" + args[0] + "/Arith"); arith = (ArithInterface) Naming.lookup(path); double a =...; double b =...; double ergebnis = arith.mult(a,b); System.out.println("Ergebnis: "+ergebnis); catch (Exception e) { System.out.println("Error: " + e.getmessage()); Der URI in der Variablen path gibt den Server sowie einen Objektnamen an, der in der Registry des Servers verzeichnet ist ( Schritt 3). path wird an die statische Methode Naming.lookup() übergeben. Diese Methode setzt sich mit der Registry des Servers in Verbindung (standardmäßig über Port 1099, ) und liefert eine Zugriffsmöglichkeit auf das Remote Object. Konkret handelt es sich dabei um eine Referenz auf ein Client-lokales Proxy-Objekt, das zur Laufzeit erzeugt wird und mit dem Remote Object des Servers kommuniziert ( BILD 5.8). Beim Start dieses Clients muss die Server-Adresse als Parameter übergeben werden; sie wird über argv[0] übernommen. Alternativ könnte man path mit einer entsprechenden Konstanten initialisieren, was aber deutlich weniger flexibel wäre.

239 238 5 Kooperation Schritt 7: Den Client übersetzen und starten Der Programmierer übersetzt das Client-Programm: javac ArithClient.java ArithInterface.java Anschließend startet er das Programm: java ArithClient x1.x2.x3.x4, wobei x1.x2.x3.x4 die numerische IP-Adresse des Servers ist. Alternativ kann auch hier die Adresse in symbolischer Form angegeben werden Dynamische Webseiten Aufgabe 5A Applets Applets sind Java-Programmstücke, die in Webseiten eingebettet sind und im Browser des Clients ausgeführt werden. BILD 5.12 Clientseitige Ausführung von Java Applets Applet in Java Java-Compiler Applet-Bytecode HTML-Datei mit <APPLET> Web Server Internet-Download Applet-Bytecode HTML-Datei Web Client mit Browser Browser mit JVM Anzeige/Interaktion Ein Applet wird auf Seiten des Servers programmiert. Dazu schreibt und übersetzt man eine Klasse, die eine Unterklasse von java.applet.japplet ist und eine Methode namens start() implementiert. Die start()-methode definiert den Anfangs- Code, der auf Seiten des Clients ausgeführt werden soll. Zudem wird eine HTML-Datei erstellt, die ein <APPLET>-Tag enthält. Das CODE-Attribut dieses Tags nennt die Bytecode-Datei mit dem übersetzten Applet. Ruft ein Client über seinen Browser die HTML-Seite auf, so wird zusammen mit ihr der Applet-Bytecode heruntergeladen. Die Java Virtual Machine (JVM) des Browsers führt ihn aus, beginnend mit der start()-methode des Applets. Ein Applet kann beispielsweise dazu dienen, im Browser eine Animation ablaufen zu lassen. Man kann dafür einen Thread programmieren, der periodisch die paint()- Methode des Applets aufruft. Die paint()-methode zeichnet dann jeweils die Bildschirmanzeige neu:

240 5.3 Techniken in Java 239 Datei webseite_mit_animation.html: Webseite <HTML> <HEADER> <TITLE>Ein Applet mit animierter Grafik</TITLE> </HEADER> <BODY> <H2>Hier ist die Animation:</H2> <APPLET CODE="AnimationsApplet.class" WIDTH=300 HEIGHT=250> </APPLET> </BODY> </HTML> Datei AnimationsApplet.java: Applet import java.awt.*; import javax.swing.*; // Der SteuerThread läuft in einer Endlosschleife // und ruft periodisch die paint()-methode des Applets auf. class SteuerThread extends Thread { private JApplet applet; // Applet, dessen paint()-methode aufgerufen werden soll // Konstruktor SteuerThread(JApplet applet) { this.applet = applet; // Aktionen des Threads public void run() { try { for (;;) { sleep(1000); // Periode: 1000 Millisekunden applet.repaint(); catch (InterruptedException e) {... // Applet mit Animation public class AnimationsApplet extends JApplet { // start() wird von Browser bei Anzeige der Webseite aufgerufen. // Die Methode erzeugt und startet einen SteuerThread. public void start() { SteuerThread st = new SteuerThread(this); st.start(); // paint() führt Zeichenoperationen auf der Bildschirmausgabe aus public void paint(graphics g) { g.setcolor(color.red); g.fillrect(50,50,100,200);... PROG 5.5 Webseite mit Animation durch ein Applet

241 240 5 Kooperation Servlets und Java Server Pages Mit Servlets und Java Server Pages (JSP) kann man Java-Code programmieren, der auf Seiten eines Web Servers ausgeführt wird. Dieser Code generiert, auf Anfrage eines Web Clients, ein neues HTML-Dokument und kann dabei Parameter, die mit der Client-Anfrage eintrafen, berücksichtigen. Dieser Abschnitt gibt nur einen sehr knappen Überblick über die beiden Techniken; eine ausführliche Darstellung findet man in [Oech11]. PROG 5.6 Servlet-Fragment PROG 5.7 JSP-Fragment Ein Servlet wird durch eine Klasse programmiert, die eine Unterklasse von javax.servlet.http.httpservlet in Java EE ist. HttpServlet spezifiziert unter anderem die Methode doget(httpservletrequest req, HttpServletResponse resp). Der Web Server ruft sie auf, wenn ein HTTP-GET-Request (vergleiche 5.2.1) eintrifft. Dabei werden mit dem Parameter req Informationen über den Request übergeben. Sie können bei der doget()-ausführung abgerufen werden, indem man HTTP- ServletRequest-Methoden wie getrequesturl() oder getquerystring() benutzt. Der Parameter resp ist ein Rückgabeparameter, in dem das Dokument, das der Client erhalten soll, zusammengestellt wird. Dies geschieht mit println()-aufrufen, die den HTML-Code des Dokuments aufbauen beispielsweise so: PrintWriter writ = resp.getwriter(); writ.println("<html>");... writ.println("hier ist eine <I>Zufallszahl</I>:"); writ.println(math.random());... writ.println("</html>"); Für die weiteren HTTP-Befehle (POST, PUT, DELETE,...) stellt HttpServlet entsprechende Methoden bereit. Mit Java Server Pages (JSP) programmiert man in gewissem Sinne umgekehrt zu Servlets: Während bei Servlets HTML-Code in ein Java-Programm eingebettet wird (nämlich in die println()-anweisungen), wird bei JSP Java-Code in HTML-Dokumente integriert. Dies geschieht durch Scriptlets Java-Anweisungen, die durch <%%> geklammert sind: <HTML>... Hier ist eine <I>Zufallszahl</I>: <% out.println(math.random()) %>... </HTML> Dies ist gegenüber Servlets dann vorteilhaft, wenn die dynamischen Dokumente aus viel HTML-Code und nur wenigen Java-Anweisungen bestehen.

242 5.3 Techniken in Java Web Services Web Services können in Java mit Hilfe von JAX-WS (Java API for XML Web Services) bereitgestellt und aufgerufen werden. Die grundsätzliche Arbeitsweise ist sehr ähnlich zu der von RMI ( 5.3.1, BILD 5.8): Server definieren ihre Dienste durch Klassen mit Methoden, erzeugen Objekte dieser Klassen und exportieren sie. Clients verschaffen sich Referenzen auf die Server-Objekte und rufen über Proxies deren Methoden auf. Im Unterschied zu RMI verwendet JAX-WS jedoch keine Java-spezifische Registry, sondern benutzt (wie in beschrieben) die Infrastruktur des World Wide Web mit HTTP, SOAP und WSDL. Die Vorgehensweise mit JAX-WS wird wieder anhand eines Diensts beschrieben, der zwei Gleitkommazahlen miteinander multipliziert (das Beispiel orientiert sich an [JAXWSBsp]). Auch hier werden nur die wichtigsten Grundlagen besprochen; Details findet man beispielsweise bei [Abts10] oder auch [Heus10]. Schritt 1: Den Dienst des Servers durch eine Klasse programmieren Datei serverdir/service/arith.java auf dem Server-Knoten: package service; import javax.jws.webservice; import javax.jws.soap.soapbinding; public class Arith { public double mult(double x, double y) { return x*y; Die zeigen an, dass hier ein Web Service definiert wird, der durch entfernte Methodenaufrufe (RPC) genutzt werden kann. Schritt 2: Das Hauptprogramm des Servers schreiben Datei serverdir/server/arithserver.java auf dem Server-Knoten: package server; import javax.xml.ws.endpoint; import service.arith; public class ArithServer { public static void main (String args[]) { Arith arithserver = new Arith(); Endpoint endpoint = Endpoint.publish( " arithserver); Aufgabe 5A.4.5. PROG 5.8 Kooperation durch JAX-WS

243 242 5 Kooperation Der Server erzeugt ein Objekt, das seinen Service realisiert, und registriert es unter dem Namen arith für den Port beim Web Server des Server-Knotens. Für serveradresse ist dabei die Internet-Adresse des Server-Knotens einzusetzen. Schritt 3: Den Server übersetzen und starten Der Server wird übersetzt, indem man im Verzeichnis serverdir/server des Server-Knotens das folgende Kommando eingibt: javac -sourcepath.. *.java Zu seinem Start gibt man im Verzeichnis serverdir des Server-Knotens ein: java server/arithserver Die WSDL-Beschreibung des Services ist nun im Internet verfügbar (allerdings nur, solange der Server läuft). Zu Testzwecken kann man sie mit einem Browser unter betrachten. Schritt 4: Den Dienst des Servers beim Client importieren Bei laufendem Server importiert man den Dienst beim Client, indem man im Verzeichnis clientdir des Client-Knotens das folgende Kommando eingibt: wsimport -keep Hierdurch entstehen im Verzeichnis clientdir/service die Dateien Arith.java, Arith.class, ArithService.java und ArithService.class. Der Client hat sich damit über das WWW alles beschafft, was er zum Aufruf des Service benötigt (siehe nächster Schritt). Schritt 5: Den Client programmieren Datei clientdir/client/arithclient.java auf dem Client-Knoten: package client; import service.arith; import service.arithservice; public class ArithClient { public static void main(string args[]) { ArithService service = new ArithService(); Arith arith = service.getarithport(); double a =...; double b =...; double ergebnis = arith.mult(a,b); System.out.println("Ergebnis: "+ergebnis); Die Klasse ArithService, die in Schritt 4 aus der WSDL-Beschreibung des Service erzeugt wurde, ist eine Unterklasse von javax.xml.ws.service. Damit stellt sie unter anderem eine Methode mit dem Namen getarithport() zur Verfügung, mit der Proxies für Zugriffe auf den Server generiert werden können. Dies geschieht dann auch, und eine Referenz auf den neu erzeugten Proxy wird in der

244 5.4 Zusammenfassung 243 Variablen arith gespeichert. Über arith wird dann (syntaktisch wie ein lokaler Methodenaufruf) der Server-Dienst aufgerufen. Schritt 6: Den Client übersetzen und ausführen Der Client wird übersetzt, indem man im Verzeichnis clientdir/client des Client-Knotens das folgende Kommando eingibt: javac -sourcepath.. *.java Zur Ausführung des Clients gibt man im Verzeichnis clientdir des Client-Knotens ein: java client/arithclient Web Services mit SOAP können alternativ auch mit Apache Axis (Apache extensible Interaction System) programmiert werden [Axis]. Zur Programmierung mit REST ( ) gibt es JAX-RS (Java API for RESTful Web Services) [JAXRS]. 5.4 Zusammenfassung Kapitel 5 beschäftigte sich mit der Kooperation zwischen Prozessen, also mit ihrer Zusammenarbeit zur Bearbeitung von Aufgaben. Praktisch besonders relevant ist die Kooperation im Internet über Rechnergrenzen hinweg. Programmiert man ein System kooperierender Prozesse, so orientiert man sich meist an einem Modell so am Client-Server-Modell oder am Peer-to-Peer-Modell. Beim Client-Server-Modell bieten Server Dienste an und Clients nutzen sie; beim Peer-to-Peer-Modell verteilen sich Ressourcen eines Diensts auf mehrere gleichberechtigte Prozesse und Rechnerknoten. Die Programmiertechniken, die man nutzen kann, hängen von der zugrundeliegenden Plattform ab, also von Programmiersprache, Betriebssystem und Netzanbindung. Eine prozedurale Programmiersprache wie C an der UNIX/Linux-API unterstützt den Remote Procedure Call, also den Aufruf von Funktionen, die auf einem anderen Computer als dem des aufrufenden Prozesses implementiert sind. Eine objektorientierte Sprache wie Java ermöglicht Methodenaufrufe auf Remote Objects, die ebenfalls auf anderen Computern liegen. Eine dritte Klasse von Techniken, zu denen dynamische Web-Seiten sowie Web Services gehören, stützen sich auf die Infrastruktur des World Wide Web. An der C-Programmierschnittstelle von UNIX/Linux kann man mit ONC RPC Remote Procedure Calls programmieren. Ist das Kommunikationsprotokoll, das der Partnerprozess versteht, im Detail bekannt, können die Kooperationsschritte explizit auf der Basis von Sockets ausprogrammiert werden. In Java können Objekte, die auf unterschiedlichen Computern liegen, durch Remote Method Invocation (RMI) oder durch Web Services mit JAX-WS kooperieren. Ebenso unterstützt Java dynamische Webseiten durch Applets, Servlets und Java Server Pages (JSP).

245

246 5A Kooperation: Aufgaben Die Lösungen findet man auf der Webseite zum Buch. 5A.1 Wissens- und Verständnisfragen 1. Kreuzen Sie die richtige(n) Aussage(n) an: a.) Vorteile beim Client-Server-Modell in einem verteilten System sind, O dass nicht auf jedem Computer alle Programme und Daten vorgehalten werden müssen. O dass damit auch Geräte auf anderen Rechnerknoten angesprochen werden können. O dass damit auf das Multiprogramming verzichtet werden kann. b.) Lokale Client-Server-Systeme (also Systeme auf einem UNIX/Linux-Computer) können realisiert werden mit O Sockets. O Message Queues. O Shared Memory. c.) Ein Client kann vom Ende der Auftragsbearbeitung erfahren durch O Polling. O einen Interrupt. O Marshalling. d.) Wenn Client und Server auf verschiedenen Computern laufen, können sie kooperieren durch O API. O RMI. O RPC. e.) Middleware ist O ein Programm, das teils in Hardware und teils in Software implementiert ist. O eine Sammlung von Diensten in verteilten Systemen, die von darüberliegenden Anwendungen genutzt werden können. O eine andere Bezeichnung für die Peripherie eines Rechensystems. f.) RPC ist die Abkürzung für O Rapid Process Communication.

247 246 5A Kooperation: Aufgaben O Remote Programm Cooperation. O Remote Procedure Call. g.) Schnittstellen können beschrieben werden mit O einer IDL. O XDR. O WSDL. h.) Serverseitig ausgeführt werden O Remote Interfaces. O Servlets. O Applets. 2. Füllen Sie in den folgenden Aussagen die Lücken: a.) Ein bietet Dienste an, ein nutzt sie. b.) Fragt ein Client immer wieder beim Server nach, ob sein Auftrag fertig bearbeitet wurde, so nennt man das. c.) Das Einpacken von Parametern, die an einen RPC-Server geschickt werden, heißt. d.) RMI ist die Abkürzung für. e.) Das UNIX-Kommando erzeugt aus einer Datei file.x die zugehörige Datei file.h. f.) In Java RMI speichert die Informationen über die verfügbaren Dienste. g.) Mit JAX-WS kann man programmieren. 3. Sind die folgenden Aussagen wahr oder falsch? Begründung! a.) Polling entspricht aktivem Warten. b.) Bei einem entfernten Prozeduraufruf (RPC) kann es zu Problemen kommen, die bei einem lokalen Prozedur- oder Funktionsaufruf nicht auftreten. c.) Beim ONC RPC benutzt man Proxy-Objekte. d.) Man kann ein Applet nur dann ausführen, wenn es in der Registry verzeichnet ist. e.) Java Servlets stehen in engem Zusammenhang mit dem World Wide Web. 4. Welcher Begriff passt jeweils nicht in die Reihe? Begründung! a.) Client-Server, Peer-to-Peer, Registry, Three Tier b.) XDR, TCP, WSDL, IDL c.) Remote Object, Proxy, RMI, RPC d.) Remote Object, RMI Registry, Remote Interface, Java Server Pages e.) Applet, Servlet, Scriptlet, Web Service f.) RPC, RMI, Web Service, IP

248 5A.2 Sprachunabhängige Anwendungsaufgaben Auf der linken Seite sind Abkürzungen angegeben, rechts stehen Eigenschaften. Welche Eigenschaft gehört zu welcher Abkürzung? Ziehen Sie genau fünf Pfeile von links nach rechts! P2P prozedural RMI zur Schnittstellenspezifikation XDR webbasiert RPC Kooperation zwischen Gleichberechtigten JAX-WS objektorientiert 6. Beantworten Sie die folgenden Fragen: a.) Was ist der Unterschied zwischen synchroner und asynchroner Client-Server-Kooperation? Welche zwei Arten der asynchronen Kooperation gibt es? b.) Was ist das Three Tier Model? c.) Was ist ein Stub? d.) Welche grundlegende Gemeinsamkeit besteht zwischen einem RPC-Stub und einem RMI-Proxy, welcher grundlegende Unterschied? e.) Was lässt sich über die Ortstransparenz ( ) bei der Programmierung mit den Funktionen clnt_create() (UNIX/Linux) und Naming.lookup() (Java) sagen? f.) Wozu dient ein Remote Interface? g.) Was ist der grundlegende Unterschied zwischen Applets und Servlets? 5A.2 Sprachunabhängige Anwendungsaufgaben 1. Ein Auskunftssystem kann als Chat organisiert werden, bei dem sich die Teilnehmer gegenseitig ihre Fragen beantworten, oder mit einer zentralen Stelle, an die Benutzer ihre Anfragen richten. Welche Kooperationsmodelle werden durch die beiden Ansätze realisiert? Wann ist beim zentralen Auskunftssystem eine synchrone Kooperation sinnvoll, wann nicht? Sollte das zentrale Auskunftssystem unter asynchroner Kooperation besser mit Polling oder besser mit Antworten per Interrupt arbeiten? 2. Ein Patentanwalt prüft für Firmen, ob deren geplante Produkte bestehende Rechte verletzen. Er hat dazu ein Archiv mit einem erfahrenen Mitarbeiter, der ihm zu einer Patentnummer oder zu einem Stichwort die passenden Patentschriften liefert. In einer großen Firma, die mit dem Anwalt arbeitet, gibt es einen bestimmten Mitarbeiter, der für den Kontakt mit dem Anwaltsbüro zuständig ist und an den sich die Ingenieure der Firma bei Bedarf wenden. Wie nennt man den Typ von Client-Server-System, der hier realisiert wird? Welcher Typ von Client liegt dabei vor? Skizzieren Sie einen typischen Arbeitsablauf in diesem System. Nennen Sie dabei (in der Terminologie des zugrundeliegenden Modells) die Rollen, die die einzelnen Personen spielen.

249 248 5A Kooperation: Aufgaben 3. Automaten in Bahnhöfen können Fahrkarten verkaufen und Auskünfte geben. Sie sind dazu mit zentralen Datenbanken (insbesondere für Fahrplan- und Bankdaten) verbunden. Kann man sie als Proxies in einem verteilten System bezeichnen? 4. Die folgende Skizze stellt ein System aus zwei Clients und zwei Servern dar: Client 1 Server A Client 2 Server B Der Arbeitsablauf in diesem System soll wie folgt sein: Ein Client schickt einen Auftrag an Server A. Server A prüft, ob er den Auftrag unmittelbar selbst bearbeiten kann. Wenn ja, schickt er eine Antwort an den betreffenden Client zurück. Wenn nein, so gibt er den Auftrag an Server B weiter. Server B bearbeitet den Auftrag und gibt die Antwort direkt an den betreffenden Client zurück. Ergänzen Sie oben die Skizze so um Ports ( ), dass die genannten Schritte ablaufen können. Zeigen Sie dabei durch Pfeile die Wege der Datenübertragungen an. Wie sieht das Bild aus, wenn die Antworten an die beiden Clients über eine Mailbox zurückgegeben werden? Welche Information muss bei der Mailbox-Lösung der Antwort mitgegeben werden? 5. Die folgende Skizze zeigt einen Client und drei Server, wobei jeder dieser Prozesse durch ein Oval symbolisiert wird: Server 0 Client Server 1 Server 2 Der Arbeitsablauf in diesem System ist wie folgt: Der Client schickt einen Auftrag an Server 0. Server 0 prüft, ob der Auftrag überhaupt bearbeitet werden kann. Wenn nein, schickt er eine Antwort an den Client zurück. Wenn ja, so gibt er den Auftrag an einen der beiden anderen Server weiter. Server 1 oder Server 2 bearbeitet den Auftrag und gibt eine Antwort an Server 0 zurück. Server 0 prüft diese Antwort und schickt sie dann an den Client weiter. Beantworten Sie nun die folgenden Fragen bzw. lösen Sie die folgenden Aufgaben: Nach welchem Kriterium könnte Server 0 entscheiden, ob er einen Auftrag an Server 1 oder an Server 2 weitergibt? Es wird vorausgesetzt, dass beide Server dasselbe leisten.

250 Free ebooks ==> 5A.3 Programmierung unter UNIX/Linux 249 Ergänzen Sie nun die Skizze so um Ports, dass die genannten Schritte ablaufen können. Geben Sie zudem durch Pfeile an, von wo nach wo Datenübertragungen stattfinden können. An welcher Stelle könnten Ports durch eine Mailbox ersetzt werden? Wir nehmen nun an, dass zwei Clients vorhanden sind. Welche Information muss nun einem Auftrag zusätzlich mitgegeben werden, damit der Arbeitsablauf immer noch ordnungsgemäß funktioniert? 5A.3 Programmierung unter UNIX/Linux 1. Gegeben ist der folgende Code: struct drei_floats { float f1; float f2; float f3; ; program MAXIMALWERT_PROG { version MAXIMALWERT_VERS { float MAXIMALWERT(drei_floats) = 1; = 1; = 0x ; a.) Beantworten Sie die folgenden Fragen: Sie haben mehrere Techniken kennengelernt, mit denen man Client-Server-Systeme programmieren kann. Bei welcher Art der Programmierung wird dieser Code eingesetzt? Wozu dient der Code, allgemein gesprochen? Was wäre ein geeigneter Name für eine Datei, die den Code enthält? Mit welchem UNIX/Linux-Benutzerkommando wird eine solche Datei weiterverarbeitet? b.) Schreiben Sie einen Server, der zu diesem Code passt. c.) Wie heißt die Funktion, mit der der Client den Kontakt zum Server aufbaut, und wie heißt die Funktion, mit der er dann den Dienst aufruft? Muss der Client bei seiner Ausführung wissen, auf welchem Computer der Server ausgeführt wird? 2. Gegeben ist eine XDR-Datei mittel.x mit folgendem Inhalt: struct drei_ints { int a1; int a2; int a3; ; program MITTEL_PROG { version MITTEL_VERS { float MITTEL(drei_ints) = 1; = 1; = 0x ; Sie beschreibt offensichtlich die Schnittstelle einer RPC-Funktion, die das arithmetische Mittel dreier int- Werte berechnet. Schreiben Sie ein vollständiges Client-Programm, das diesen Dienst mit den int-werten 2, 3 und 4 aufruft.

251 250 5A Kooperation: Aufgaben Wie heißen die Dateien, die der Aufruf rpcgen mittel.x erzeugt? 3. Schreiben Sie eine XDR-Datei für einen Server, der die folgenden arithmetischen Funtionen anbietet: Berechnung des größten gemeinsamen Teilers (ggt) zweier ganzer Zahlen. Berechnung des kleinsten gemeinsamen Vielfachen (kgv) zweier ganzer Zahlen. Berechnung des Minimums von vier Zahlen mit Nachkommastellen. Der Server soll in zwei verschiedenen Versionen angeboten werden. Geben Sie die Funktionsnamen an, die ein Client für Aufrufe benutzen kann. 5A.4 Programmierung in Java 1. Gegeben ist der folgende Code, der die Schnittstellendefinition eines RMI-Service sein soll: public class ServiceSchnittstelle extends Remote { public double method1(); public int method2(float a, char b); Welche Fehler erkennen Sie? 2. Gegeben ist die folgende Schnittstellendefinition: public interface MinMaxInterface extends Remote { public int minimum(int x1, int x2, int x3) throws RemoteException; public int maximum(int x1, int x2, int x3) throws RemoteException; Schreiben Sie einen dazu passenden Server. 3. Schreiben Sie einen Server, der auf Java RMI basiert und das arithmetische Mittel dreier Zahlen mit Nachkommastellen berechnet. Schreiben Sie einen dazu passenden Client. Welcher Teil des Server-Codes muss also auch dem Client bekannt sein? 4. Schreiben Sie ein Applet, das in Abständen von (ungefähr) einer Sekunde die aktuelle Uhrzeit in einer zugehörigen Webseite anzeigt. 5. Gegeben ist der folgende Code, der zwei Dienste eines Servers definiert: package service; import javax.jws.webservice; import javax.jws.soap.soapbinding; public class MinMax { public int minimum(int x1, int x2, int x3) {...

252 5A.4 Programmierung in Java 251 public int maximum(int x1, int x2, int x3) {... Bei welcher Java-Technik setzt man Code dieser Form ein? Ergänzen Sie die Körper der Methoden, schreiben Sie ein zugehöriges Server-Hauptprogramm und einen dazu passenden Client. Welchen Befehl müssen Sie an der Benutzerschnittstelle des Client-Computers eingeben, damit der Client auf den Server-Dienst zugreifen kann?

253

254 Literatur und Internet Bücher [Abts10] Abts, D.: Masterkurs Client-Server-Programmierung mit Java. Vieweg+Teubner [Back11] Backschat, M.; Rücker, B.: Enterprise JavaBeans und JPA. Spektrum, 3. Aufl [Beng08] Bengel, G. et al.: Masterkurs Parallele und Verteilte Systeme. Vieweg+Teubner [Come01] Comer, D.; Stevens, D.: Internetworking with TCP/IP Vol. III Client-Server Programming and Applications. Prentice Hall [Come09] Comer, D.: Computer Networks and Internets. 5th Ed. Pearson / Prentice Hall [Coul12] Coulouris, G. et al.: Distributed Systems Concepts and Design. 5th Ed. Addison-Wesley [Daus11] Dausmann, M.; Bröckl, U.; Goll, J.: C als erste Programmiersprache. 7. Aufl. Hanser [Eber11] Eberling, W.; Leßner, J.: Enterprise JavaBeans 3.1. Hanser, 2. Aufl [Ehse12] Ehses, E. et al.: Systemprogrammierung in UNIX/Linux. Vieweg+Teubner [Hero04] Herold, H.: Linux/Unix Systemprogrammierung. 3. Aufl. Addison-Wesley [Heus10] Heuser, O.; Holubek, A.: Java-Web-Services in der Praxis. dpunkt [Oech11] Oechsle, R.: Parallele und verteilte Anwendungen in Java. 3. Aufl. Hanser [Poll09] Pollakowski, M.: Grundkurs Socketprogrammierung mit C unter Linux. 2. Aufl. Vieweg+Teubner [Robb03] Robbins, K.; Robbins, S.: UNIX Systems Programming Communication, Concurrency, and Threads. Prentice Hall [Silb10] Silberschatz, A. et al.: Operating System Concepts. 8th Ed. Wiley [Stev05] Stevens, W.; Rago, S.: Advanced Programming in the UNIX Environment. 2nd Ed. Addison-Wesley [Stal12] Stallings, W.: Operating Systems Internals and Design Principles. 7th Ed. Prentice Hall [Tane07] Tanenbaum, A.; van Steen, M.: Distributed Systems Principles and Paradigms. 2nd Ed. Pearson / Prentice Hall [Tane08] Tanenbaum, A.: Modern Operating Systems. 3rd Ed. Pearson / Prentice Hall [Tane11] Tanenbaum, A.; Wetherall, D.: Computer Networks. 5th Ed. Pearson / Prentice Hall [Vogt01] Vogt, C.: Betriebssysteme. Spektrum Akademischer Verlag [Vogt07] Vogt, C.: C für Java-Programmierer. Hanser (In gedruckter Form vergriffen, aber elektronisch weiterhin erhältlich, z.b. unter Kurzfassung auf der Webseite:

255 254 Literatur und Internet Internet-Quellen [Axis] [JavaSDK] The Apache Software Foundation: Apache Axis2 / Java. Oracle: Java SE Downloads. [JavaSpec] Oracle: Java Platform, Standard Edition 7 API Specification. [JavaTutConc] Oracle: The Java Tutorials Concurrency. [JavaTutNet] Oracle: The Java Tutorials Custom Networking. [JAXRS] Java Community Process: JSR 311: JAX-RS The Java API for RESTful Web Services. [JAXWSBsp] theserverside.de: WebService in Java. [JMS] [Knoppix] [Linux] [MPIJava] Oracle: Java Message Service (JMS). Knoppix. The Linux Kernel Archives. Pervasive Technology Labs: mpijava Home Page. Die Webseite zum Buch:

256 Index A aktives Warten 85, 219 Aktivität 25 Alles-oder-nichts-Prinzip 95, 96, 102, 107 in UNIX/Linux 121, 127 Apache Axis 243 Applet 238 Application Programming Interface (API) 26 Atomarität 89 Fehler bei Missachtung 108 B Barriere in Java 143 Batch Mode 32 Bedingungsvariable 112 in UNIX/Linux 132 Belegungs-Anforderungs-Graph 106 Benutzerschnittstelle 26 Betriebsart 32 Betriebssystem 25 Aufgaben 25 Kern 26 Netzdienste 30 Schnittstellen 26 bidirektionale Kommunikation 167 Blocking Send/Receive 165 Broadcast 163 Bussperre 89 C Cigarette Smokers Problem 158 client-/serverseitige Ausführung 225 Client-Server-Modell 218 Client-Socket in Java 197 in UNIX/Linux 184 Cluster 23 CORBA 224 D Datagramm 185 Datagram-Socket 170 in Java 200 in UNIX/Linux 183, 191 Datenstrom 161 Deadlock 100, 106 Aufhebung 107 beim Philosophenproblem 101 Verhinderung 107 Vermeidung 107 Dispatching 40 Distributed Shared Memory (DSM) 163 Dynamic Port 186 dynamische Webseite 225, 238 E Einprogrammbetrieb 32 Eltern-Kind-Beziehung 42, 44, 46 Enterprise JavaBeans (EJB) 194 Erzeuger-Verbraucher-Problem 83 mit allgemeinen Semaphoren 98, 102 mit Java-Condition 143 mit Java-Piped-Streams 195 mit Java-wait()/notify() 141 mit Monitor 114 mit Pthreads-Bedingungsvariablen 133 mit UNIX/Linux-Pthreads 133 mit UNIX-Message-Queue 181 mit UNIX-Semaphoren 129, 173 mit UNIX-Shared-Memory 173 Event 90 in Java 141 F Fairness 109 Fat Client 221

257 256 Index G Gast-Betriebssystem 29 geordnete Ressourcenanforderung 107 H Hardware eines Computers 20 Hardware-Nebenläufigkeit 22 Host-Betriebssystem 29 Hypertext Transfer Protocol (HTTP) 170 in UNIX/Linux 226 I Interface Definition Language (IDL) 223 in UNIX/Linux 228, 230 Internet Protocol (IP) 169 Internet-Domain 182 Inter-Process Communication (IPC) 161 Interrupt 219 Interruptsperre 85 J Java Applet 238 Interrupt-Flag 66 Pipe 194 Remote Method Invocation (RMI) 234 Servlet 240 Shared Memory 64 Socket 196 Thread 60 Web Service 241 Java Message Service (JMS) 194 Java Server Pages (JSP) 240 Java Virtual Machine (JVM) 22, 29, 238 Java-Klassen/Interfaces Atomic 135 BlockingQueue 143 Collections 136 Condition 143 CountDownLatch 143 CyclicBarrier 143 DatagramPacket 200 DatagramSocket 200 Exchanger 144 InterruptedException 66, 140 LinkedBlockingQueue 141 Lock 142 PipedInputStream 194 PipedOutputStream 195 ReadWriteLock 143 ReentrantLock 142 Semaphore 136 ServerSocket 197 Socket 197 Thread 60 Java-Methoden currentthread() 64 getid()/getname()/setname() 64 getstate() 64 interrupt()/isinterrupted() 64, 66 isalive() 64 join() 63 notify() 140 notifyall() 140 run() 61 setpriority()/getpriority() 64 sleep() 64 start() 61 synchronized 138, 139 synchronizedxxx() 136 wait() 140 yield() 64 JAX-RS 243 JAX-WS 241 JSON 226 K Kern eines Betriebssystems 26 eines Prozessors 23 Kommunikation 161 direkte vs. indirekte 164 synchrone vs. asynchrone 166 Kommunikationsprotokoll 167 Kommunikationssystem 161, 166 Konkurrenz 80 Kontext 35 Kooperation 81, 217 objektorientierte 224

258 Index 257 prozedurorientierte 222 synchrone vs. asynchrone 219 Kopplung (enge vs. lose) 24 kritischer Abschnitt 80, 96 L Lebenszyklus 37 Leser-Schreiber-Problem 83 in Java 143 mit allgemeinen Semaphoren 99 mit Monitor 116 Lock-Datei 90 in UNIX/Linux 120 Lock-Variable 87 M Mailbox 164 in UNIX/Linux 177 Marshalling 223 Mehrprogrammbetrieb 33 Message Passing Interface (MPI) 194 Message Queue 177 Middleware 31 Monitor 110 Bedingungsvariable 112 in Java 138 Multicast 163 Multicomputer 23 Multiprogramming 33 Multiprozessorsystem 23 Multitasking 19, 33 Mutex 95 bei Java-Monitoren 139 in UNIX/Linux 132 N.NET 224 Nachricht 161, 185 nachrichtenbasierte Kommunikation 161 Nebenläufigkeit 13, 19, 25 durch Compiler 24 echte 24, 25 in Hardware 22 in Software 24 Pseudo- 25 Netzdienste 30 Nonblocking Send/Receive 165 O ONC RPC 228 Ortstransparenz 30 P Parallelisierung 24 passives Warten 85, 219 Peer-to-Peer-Modell (P2P) 221 Philosophenproblem 84 mit allgemeinen Semaphoren 101 mit Monitor 117 Pipe 174, 194 benannte 175 unbenannte 176 Polling 85, 166, 219 P-Operation 91 Port 164 in UNIX/Linux 177 Nummer 170, 186 Portmapper 232 Process Identifier (PID) 39 Programmierschnittstelle (API) 26 Protokoll 167 Protokollinstanz 167 Protokollstack 169 Proxy 224 Prozedurfernaufruf 222 Prozess 34, 36 in UNIX/Linux 42, 46 Kontext 35 Lebenszyklus 37, 38 Priorität 41 Terminierung 38 Prozesskontrollblock 39 Prozessorkern 23 Prozesstabelle 40 Pseudonebenläufigkeit 25 Pthread 55 Synchronisation 132 Pufferspeicher (siehe auch Ringpuffer) 83

259 258 Index R Race Condition 81 Rechnerknoten 23 Registry 235, 236 Reihenfolgebedingung 82 mit allgemeinen Semaphoren 97 mit Java-Semaphoren 137 mit Monitor 115 mit UNIX-Semaphoren 128 Remote Interface 235 Remote Method Invocation (RMI) 224 in Java 234 Remote Object 224, 235, 236 Remote Procedure Call (RPC) 222 in UNIX/Linux 228 Rendezvous 166 REST 226, 243 Ringpuffer als Monitor 111, 114, 139 in Java 139 in UNIX/Linux 133 mit Lese-/Schreiboperationen 114, 133 nur mit Schreiboperation 111, 139 RMI Registry 235, 236 Round Robin 41 RPC-Generator rpcgen 229, 230 S Scheduling 40 Scriptlet 240 Semaphor 91 Alles-oder-nichts-Prinzip 95, 96, 102, 107, 121 binärer 95 in Java 136 in UNIX/Linux 121 sequentielle Bearbeitung 32, 33 Server-Socket in Java 197 in UNIX/Linux 184 Servlet 240 Shared Memory 161 in Java 64 in UNIX/Linux 171 Signal 90 in UNIX/Linux 118 Signal Handler 119 Single Tasking Mode 32 Skeleton 224 Sleeping Barber Problem 153 SOAP 225 Socket 166, 170 Domains 182 in Java 196 in UNIX/Linux 182 Typen 183 Software eines Computers 21 Software-Nebenläufigkeit 24 speicherbasierte Kommunikation 161 Spinlock 86, 89 mit Lock-Dateien in UNIX/Linux 120 Spooling 33 Stapelverarbeitung 32 Stream-Socket 170 in Java 197 in UNIX/Linux 183, 188 strombasierte Kommunikation 161 Stub 222 Synchronisation 79 Synchronisationsbedingung 79 Synchronisationsmechanismus 79, 85 T Task 36 Terminierung 38 Thin Client 221 Thread 36, 37 in Java 60 in UNIX/Linux 55 Lebenszyklus 37, 38 thread-safe 135 Three Tier Model 220 Timesharing 34 Transmission Control Protocol (TCP) 170 U Unicast 163 unidirektionale Kommunikation 174 UNIX/Linux Bedingungsvariable 132 Benutzerkommandos 42

260 Free ebooks ==> Index 259 Lock-Datei 120 Message Queue 177 Mutex 132 Pipe 174 Programmierschnittstelle (API) 26, 45 Prozess 42, 46 Semaphor 121 Shared Memory 171 Signal 118 Socket 182 Thread 55 UNIX/Linux-API 26, 45 accept() 184, 187 bind() 184, 185 callrpc() 229 clnt_create() 229 clone() 59 close() 185, 187 connect() 184, 187 errno 28 execv() 49, 54 exit() 48, 53 Fehlerabfrage 28 fork() 46, 50 getpid() 49, 52 getppid() 49, 52 kill() 50, 54, 118 listen() 184, 186 mkfifo() 175 msgctl() 178 msgget() 177 msgrcv() 179, 180 msgsnd() 179 pause() 119 perror() 28 pipe() 176 pthread_cancel() 58 pthread_cond_xxx() 132 pthread_create() 55 pthread_exit() 55 pthread_join() 58 pthread_mutex_xxx() 132 read() 185, 187 recv() 185 recvfrom() 185, 188 semctl() 123 semget() 121 semop() 125 send() 185 sendto() 185, 188 shmat() 172 shmctl() 172 shmget() 171 shutdown() 187 sigaction() 119 signal() 119 sigsuspend() 119 sleep() 49 socket() 184, 185 vfork() 59 wait() 48, 53 waitpid() 48 write() 185, 187 UNIX/Linux-Benutzerkommandos 42 ipcrm 124, 173, 178 ipcs 124, 173, 178 kill 45 ps 42 rpcgen 229, 230 sleep 45 wait 45 UNIX-Domain 183 User Datagram Protocol (UDP) 170 V Vater-Sohn-Beziehung 42, 44, 46 Verhungern 100 Verklemmung 106 Verteiltes System 23, 30, 32, 217 Virtualisierung 28 VMware 29 V-Operation 91 W Warten, aktives vs. passives 85 Web Service 225 in Java 241 Webseite 225, 238 wechselseitiger Ausschluss 80 in Java 138

261 260 Index mit allgemeinen Semaphoren 96 mit Monitor 110 mit UNIX-Semaphoren 129 WSDL 226 X XDR 228, 230 Xen 29 XML-RPC 226 Z Zombie-Zustand 39, 48 Zustandsübergangsdiagramm 38

262 Informatikwissen kompakt. Schneider Taschenbuch der Informatik 7., neu bearbeitete Auflage 736 Seiten. 271 Abb. 99 Tab. ISBN Eine umfassende Gesamtübersicht zu den wichtigsten Teilgebieten der Informatik in einem Taschenbuch das macht die Einzigartigkeit dieses Werkes aus. Es spannt den Bogen von den theoretischen und technischen Grundlagen der Informatik über die verschiedenen Teilgebiete der praktischen Informatik bis hin zu vielen aktuellen Anwendungen in technischen und (betriebs-)wirtschaftlichen Bereichen. Die kompakte und übersichtliche Darstellung ermöglicht eine konzentrierte Wissensvermittlung. Für die 7. Auflage wurde das Werk komplett überarbeitet und aktualisiert.»nicht nur Aspiranten auf einen Abschluss in der Informatik, auch ehemalige Studenten werden das Taschenbuch als schnell greifbare Referenz zu schätzen wissen.«c't Mehr Informationen unter

263 Parallel ist schneller! Oechsle Parallele und verteilte Anwendungen in Java 3., erweiterte Auflage 416 Seiten. 79 Abbildungen. ISBN Das Buch behandelt zwei eng miteinander verknüpfte Themen, die Programmierung paralleler (nebenläufiger) und verteilter Anwendungen. Als Programmiersprache wird Java verwendet. Es werden zunächst anhand zahlreicher Beispiele grundlegende Synchronisationskonzepte für die Programmierung paralleler Abläufe präsentiert. Neben den»klassischen«synchroni - sationsmechanismen von Java werden auch die Konzepte aus der Java-Concurrency- Klassenbibliothek vorgestellt. Die Autoren wenden sich dann der Entwicklung verteilter Client-Server-Anwendungen zu. Das letzte Kapitel beschreibt die Programmierung webbasierter Anwendungen mit Hilfe von Servlets und Java Server Pages (JSP). Dabei werden auch AJAX und GWT (Google Web Toolkit) eingesetzt. Das Buch wendet sich an Leser mit Grundkenntnissen in Java und Objektorientierung. Mehr Informationen unter

264 Genau zum richtigen Zeitpunkt! Kienzle/Friedrich Programmierung von Echtzeitsystemen 320 Seiten. 190 Abb. 15 Tab. ISBN Das Buch vermittelt Fachwissen zu Funktion und Programmierung von Echtzeitsystemen. Sein Schwerpunkt liegt auf der Programmierung. Die wesentlichen Echtzeit-Programmier - konzepte für Einprozessor-Rechner werden allgemein erläutert. Stichworte sind Software - entwicklung, Echtzeitverhalten, nebenläufige Prozesse, Multitasking und seine Programmie - rung, Speicherverwaltung, Unterbrechungstechnik und Ein-/Ausgabesystem. Eine Reihe von Programmbeispielen sind auf der Basis eines industriellen Echtzeit- Betriebssystems (RMOS3, Siemens AG) erstellt. Mit einer kostenlosen Demo-Version dieses Betriebssystems können die Beispiele ausgeführt und der Lernerfolg kontrolliert werden. Zahlreiche Übungsaufgaben unterstützen zusätzlich das Selbststudium. Mehr Informationen unter

265 ebooks zum sofortigen Download find ou t! en tfe rn t. u r ei n en K lic k K no w H ow is t n d beziehen. fort per Downloa so ie S en nn kö hä ltl ic h. - eb oo ks ka p it el w ei se er er d o z an g nd su ch e. - eb oo ks si rt ab le Vo ll te xt fo m ko ne ei - eb oo ks bi et en

Inhaltsverzeichnis. Carsten Vogt. Nebenläufige Programmierung. Ein Arbeitsbuch mit UNIX/Linux und Java ISBN:

Inhaltsverzeichnis. Carsten Vogt. Nebenläufige Programmierung. Ein Arbeitsbuch mit UNIX/Linux und Java ISBN: Inhaltsverzeichnis Carsten Vogt Nebenläufige Programmierung Ein Arbeitsbuch mit UNIX/Linux und Java ISBN: 978-3-446-42755-6 Weitere Informationen oder Bestellungen unter http://www.hanser.de/978-3-446-42755-6

Mehr

Leseprobe. Carsten Vogt. Nebenläufige Programmierung. Ein Arbeitsbuch mit UNIX/Linux und Java ISBN:

Leseprobe. Carsten Vogt. Nebenläufige Programmierung. Ein Arbeitsbuch mit UNIX/Linux und Java ISBN: Leseprobe Carsten Vogt Nebenläufige Programmierung Ein Arbeitsbuch mit UNIX/Linux und Java ISBN: 978-3-446-42755-6 Weitere Informationen oder Bestellungen unter http://www.hanser.de/978-3-446-42755-6 sowie

Mehr

NEBEN - LÄUFIGE PROG RAM -

NEBEN - LÄUFIGE PROG RAM - carsten VOGT NEBEN - LÄUFIGE PROG RAM - M IERUNG EIN ARBEITSBUCH MIT UNIX/LINUX UND JAVA EXTRA: Mit kostenlosem E-Book Im Internet: Programmbeispiele, Lösungen zu den Aufgaben und Zusatzmaterialien Vogt

Mehr

WIRTSCHAFTS- INFORMATIK

WIRTSCHAFTS- INFORMATIK franz LEHNER stephan WILDNER michael SCHOLZ WIRTSCHAFTS- INFORMATIK EINE EINFÜHRUNG Für Bachelors geeignet 2. Auflage Lehner/Wildner/Scholz Wirtschaftsinformatik vbleiben Sie einfach auf dem Laufenden:

Mehr

WIRTSCHAFTS- INFORMATIK

WIRTSCHAFTS- INFORMATIK franz LEHNER stephan WILDNER michael SCHOLZ WIRTSCHAFTS- INFORMATIK EINE EINFÜHRUNG Für Bachelors geeignet 2. Auflage Lehner/Wildner/Scholz Wirtschaftsinformatik vbleiben Sie einfach auf dem Laufenden:

Mehr

Parallele und verteilte Anwendungen in Java

Parallele und verteilte Anwendungen in Java Rainer Oechsle Parallele und verteilte Anwendungen in Java ISBN-10: 3-446-40714-6 ISBN-13: 978-3-446-40714-5 Leseprobe Weitere Informationen oder Bestellungen unter http://www.hanser.de/978-3-446-40714-5

Mehr

Fit für die Prüfung Elektrotechnik Effektives Lernen mit Beispielen und ausführlichen Lösungen

Fit für die Prüfung Elektrotechnik Effektives Lernen mit Beispielen und ausführlichen Lösungen Jan Luiken ter Haseborg Christian Schuster Manfred Kasper Fit für die Prüfung Elektrotechnik Effektives Lernen mit Beispielen und ausführlichen Lösungen ter Haseborg, Schuster, Kasper Fit für die Prüfung

Mehr

DIN EN ISO 9001 : 2015 UMSETZEN. Pocket Power ANNI KOUBEK. Herausgegeben von Gerd F. Kamiske

DIN EN ISO 9001 : 2015 UMSETZEN. Pocket Power ANNI KOUBEK. Herausgegeben von Gerd F. Kamiske ANNI KOUBEK Herausgegeben von Gerd F. Kamiske DIN EN ISO 9001 : 2015 UMSETZEN Pocket Power Pocket Power Anni Koubek DIN EN ISO 9001:2015 umsetzen QM-System aufbauen und weiterentwickeln HANSER Die Wiedergabe

Mehr

Objektorientiertes Programmieren für Ingenieure

Objektorientiertes Programmieren für Ingenieure Uwe Probst Objektorientiertes Programmieren für Ingenieure Anwendungen und Beispiele in C++ Probst Objektorientiertes Programmieren für Ingenieure Bleiben Sie auf dem Laufenden! Hanser Newsletter informieren

Mehr

Erstellung eines Prototyps zum sicheren und gesteuerten Zugriff auf Dateien und Dokumente auf Basis von Lotus Domino und Notes

Erstellung eines Prototyps zum sicheren und gesteuerten Zugriff auf Dateien und Dokumente auf Basis von Lotus Domino und Notes Technik Jan Kröger Erstellung eines Prototyps zum sicheren und gesteuerten Zugriff auf Dateien und Dokumente auf Basis von Lotus Domino und Notes Diplomarbeit Bibliografische Information der Deutschen

Mehr

Vorwort. Carsten Vogt. Nebenläufige Programmierung. Ein Arbeitsbuch mit UNIX/Linux und Java ISBN: 978-3-446-42755-6

Vorwort. Carsten Vogt. Nebenläufige Programmierung. Ein Arbeitsbuch mit UNIX/Linux und Java ISBN: 978-3-446-42755-6 Vorwort Carsten Vogt Nebenläufige Programmierung Ein Arbeitsbuch mit UNIX/Linux und Java ISBN: 978-3-446-42755-6 Weitere Informationen oder Bestellungen unter http://www.hanser.de/978-3-446-42755-6 sowie

Mehr

Systeme I: Betriebssysteme Kapitel 4 Prozesse. Wolfram Burgard

Systeme I: Betriebssysteme Kapitel 4 Prozesse. Wolfram Burgard Systeme I: Betriebssysteme Kapitel 4 Prozesse Wolfram Burgard Version 18.11.2015 1 Inhalt Vorlesung Aufbau einfacher Rechner Überblick: Aufgabe, Historische Entwicklung, unterschiedliche Arten von Betriebssystemen

Mehr

Einführung in die Programmierung, mathematische Anwendungen und Visualisierungen

Einführung in die Programmierung, mathematische Anwendungen und Visualisierungen hans-bernhard WOYAND PYTHON FÜR INGENIEURE UND NATURWISSENSCHAFTLER Einführung in die Programmierung, mathematische Anwendungen und Visualisierungen Im Internet: Beispiele und Lösungen zu den Aufgaben

Mehr

Programmierung von verteilten Systemen und Webanwendungen mit Java EE

Programmierung von verteilten Systemen und Webanwendungen mit Java EE Programmierung von verteilten Systemen und Webanwendungen mit Java EE Frank Müller-Hofmann Martin Hiller Gerhard Wanner Programmierung von verteilten Systemen und Webanwendungen mit Java EE Erste Schritte

Mehr

Systeme I: Betriebssysteme Kapitel 4 Prozesse. Maren Bennewitz

Systeme I: Betriebssysteme Kapitel 4 Prozesse. Maren Bennewitz Systeme I: Betriebssysteme Kapitel 4 Prozesse Maren Bennewitz Version 13.11.2013 1 Inhalt Vorlesung Aufbau einfacher Rechner Überblick: Aufgabe, Historische Entwicklung, unterschiedliche Arten von Betriebssystemen

Mehr

Computeranwendung in der Chemie Informatik für Chemiker(innen) 3. Software

Computeranwendung in der Chemie Informatik für Chemiker(innen) 3. Software Computeranwendung in der Chemie Informatik für Chemiker(innen) 3. Software Jens Döbler 2003 "Computer in der Chemie", WS 2003-04, Humboldt-Universität VL3 Folie 1 Grundlagen Software steuert Computersysteme

Mehr

Medientechnik. Basiswissen Nachrichtentechnik, Begriffe, Funktionen, Anwendungen. Ulrich Freyer

Medientechnik. Basiswissen Nachrichtentechnik, Begriffe, Funktionen, Anwendungen. Ulrich Freyer Ulrich Freyer Medientechnik Basiswissen Nachrichtentechnik, Begriffe, Funktionen, Anwendungen Freyer Medientechnik Bleiben Sie auf dem Laufenden! Hanser Newsletter informieren Sie regel mäßig über neue

Mehr

Rechnergrundlagen. Vom Rechenwerk zum Universalrechner

Rechnergrundlagen. Vom Rechenwerk zum Universalrechner Rechnergrundlagen. Vom Rechenwerk zum Universalrechner von Rainer Kelch 1. Auflage Hanser München 2003 Verlag C.H. Beck im Internet: www.beck.de ISBN 978 3 446 22113 0 Zu Leseprobe schnell und portofrei

Mehr

Grundlagen der Wahrscheinlichkeitsrechnung und Statistik

Grundlagen der Wahrscheinlichkeitsrechnung und Statistik Erhard Cramer Udo Kamps Grundlagen der Wahrscheinlichkeitsrechnung und Statistik Eine Einführung für Studierende der Informatik, der Ingenieur- und Wirtschaftswissenschaften 4. Auflage Springer-Lehrbuch

Mehr

Echtzeit-Multitasking

Echtzeit-Multitasking Technische Informatik Klaus-Dieter Thies Echtzeit-Multitasking Memory Management und System Design im Protected Mode der x86/pentium-architektur. Shaker Verlag Aachen 2002 Die Deutsche Bibliothek - CIP-Einheitsaufnahme

Mehr

Informatik im Fokus. Herausgeber: Prof. Dr. O. Günther Prof. Dr. W. Karl Prof. Dr. R. Lienhart Prof. Dr. K. Zeppenfeld

Informatik im Fokus. Herausgeber: Prof. Dr. O. Günther Prof. Dr. W. Karl Prof. Dr. R. Lienhart Prof. Dr. K. Zeppenfeld Informatik im Fokus Herausgeber: Prof. Dr. O. Günther Prof. Dr. W. Karl Prof. Dr. R. Lienhart Prof. Dr. K. Zeppenfeld Informatik im Fokus Rauber, T.; Rünger, G. Multicore: Parallele Programmierung. 2008

Mehr

Echtzeit-Multitasking

Echtzeit-Multitasking Technische Informatik Klaus-Dieter Thies Echtzeit-Multitasking Memory Management und System Design im Protected Mode der x86/pentium-architektur. Shaker Verlag Aachen 2002 Die Deutsche Bibliothek - CIP-Einheitsaufnahme

Mehr

Betriebssysteme. Grundlagen, Konzepte, Systemprogrammierung. von Eduard Glatz. 1. Auflage

Betriebssysteme. Grundlagen, Konzepte, Systemprogrammierung. von Eduard Glatz. 1. Auflage Betriebssysteme Grundlagen, Konzepte, Systemprogrammierung von Eduard Glatz 1. Auflage Betriebssysteme Glatz schnell und portofrei erhältlich bei beck-shop.de DIE FACHBUCHHANDLUNG dpunkt.verlag 2005 Verlag

Mehr

Netzwerkprogrammierung unter Linux und UNIX

Netzwerkprogrammierung unter Linux und UNIX Netzwerkprogrammierung unter Linux und UNIX Bearbeitet von Stefan Fischer, Walter Müller 2. Auflage 1999. Buch. XII, 228 S. Hardcover ISBN 978 3 446 21093 6 Format (B x L): 14 x 20,9 cm Gewicht: 329 g

Mehr

Betriebssysteme Vorstellung

Betriebssysteme Vorstellung Am Anfang war die Betriebssysteme Vorstellung CPU Ringvorlesung SE/W WS 08/09 1 2 Monitor CPU Komponenten eines einfachen PCs Bus Holt Instruktion aus Speicher und führt ihn aus Befehlssatz Einfache Operationen

Mehr

Systeme I: Betriebssysteme Kapitel 4 Prozesse. Maren Bennewitz

Systeme I: Betriebssysteme Kapitel 4 Prozesse. Maren Bennewitz Systeme I: Betriebssysteme Kapitel 4 Prozesse Maren Bennewitz Version 21.11.2012 1 Begrüßung Heute ist Tag der offenen Tür Willkommen allen Schülerinnen und Schülern! 2 Testat nach Weihnachten Mittwoch

Mehr

Elektrotechnik 3. Grundgebiete der. Aufgaben. Arnold Führer Klaus Heidemann Wolfgang Nerreter. 3., neu bearbeitete Auflage

Elektrotechnik 3. Grundgebiete der. Aufgaben. Arnold Führer Klaus Heidemann Wolfgang Nerreter. 3., neu bearbeitete Auflage Arnold Führer Klaus Heidemann Wolfgang Nerreter Grundgebiete der Elektrotechnik 3 Aufgaben 3., neu bearbeitete Auflage Führer/Heidemann/Nerreter Grundgebiete der Elektrotechnik Band 3: Aufgaben Bleiben

Mehr

2. Computer (Hardware) K. Bothe, Institut für Informatik, HU Berlin, GdP, WS 2015/16

2. Computer (Hardware) K. Bothe, Institut für Informatik, HU Berlin, GdP, WS 2015/16 2. Computer (Hardware) K. Bothe, Institut für Informatik, HU Berlin, GdP, WS 2015/16 Version: 14. Okt. 2015 Computeraufbau: nur ein Überblick Genauer: Modul Digitale Systeme (2. Semester) Jetzt: Grundverständnis

Mehr

STRATEGISCHES PROZESS - MANAGEMENT

STRATEGISCHES PROZESS - MANAGEMENT inge HANSCHKE rainer LORENZ STRATEGISCHES PROZESS - MANAGEMENT EINFACH UND EFFEKTIV EIN PRAKTISCHER LEITFADEN EXTRA: Mit kostenlosem E-Book Hanschke/Lorenz Strategisches Prozessmanagement einfach und effektiv

Mehr

Matthias Moßburger. Analysis in Dimension 1

Matthias Moßburger. Analysis in Dimension 1 Matthias Moßburger Analysis in Dimension 1 Matthias Moßburger Analysis in Dimension1 Eine ausführliche Erklärung grundlegender Zusammenhänge STUDIUM Bibliografische Information der Deutschen Nationalbibliothek

Mehr

Betriebssysteme. Vorlesung im Herbstsemester 2010 Universität Mannheim. Kapitel 6: Speicherbasierte Prozessinteraktion

Betriebssysteme. Vorlesung im Herbstsemester 2010 Universität Mannheim. Kapitel 6: Speicherbasierte Prozessinteraktion Betriebssysteme Vorlesung im Herbstsemester 2010 Universität Mannheim Kapitel 6: Speicherbasierte Prozessinteraktion Felix C. Freiling Lehrstuhl für Praktische Informatik 1 Universität Mannheim Vorlesung

Mehr

Übersicht. Nebenläufige Programmierung. Praxis und Semantik. Einleitung. Sequentielle und nebenläufige Programmierung. Warum ist. interessant?

Übersicht. Nebenläufige Programmierung. Praxis und Semantik. Einleitung. Sequentielle und nebenläufige Programmierung. Warum ist. interessant? Übersicht Aktuelle Themen zu Informatik der Systeme: Nebenläufige Programmierung: Praxis und Semantik Einleitung 1 2 der nebenläufigen Programmierung WS 2011/12 Stand der Folien: 18. Oktober 2011 1 TIDS

Mehr

Bleiben Sie auf dem Laufenden!

Bleiben Sie auf dem Laufenden! Badach/Hoffmann Technik der IP-Netze Bleiben Sie auf dem Laufenden! Unser Computerbuch-Newsletter informiert Sie monatlich über neue Bücher und Termine. Profitieren Sie auch von Gewinnspielen und exklusiven

Mehr

Hans-Georg Kemper Henning Baars. Business Intelligence Arbeits- und Übungsbuch

Hans-Georg Kemper Henning Baars. Business Intelligence Arbeits- und Übungsbuch Hans-Georg Kemper Henning Baars Business Intelligence Arbeits- und Übungsbuch Aus dem Bereich IT erfolgreich lernen Unternehmensweites Datenmanagement von Rolf Dippold, Andreas Meier, Walter Schnider und

Mehr

Günter Schmidt. Prozessmanagement. Modelle und Methoden. 3. überarbeitete Auflage

Günter Schmidt. Prozessmanagement. Modelle und Methoden. 3. überarbeitete Auflage Prozessmanagement Günter Schmidt Prozessmanagement Modelle und Methoden 3. überarbeitete Auflage Günter Schmidt Universität des Saarlandes Operations Research and Business Informatics Saarbrücken, Deutschland

Mehr

Vorlesung Rechnerarchitektur. Einführung

Vorlesung Rechnerarchitektur. Einführung Vorlesung Rechnerarchitektur Einführung Themen der Vorlesung Die Vorlesung entwickelt an Hand von zwei Beispielen wichtige Prinzipien der Prozessorarchitektur und der Speicherarchitektur: MU0 Arm Speicher

Mehr

Matthias Bartelmann Björn Feuerbacher Timm Krüger Dieter Lüst Anton Rebhan Andreas Wipf. Theoretische Physik

Matthias Bartelmann Björn Feuerbacher Timm Krüger Dieter Lüst Anton Rebhan Andreas Wipf. Theoretische Physik Matthias Bartelmann Björn Feuerbacher Timm Krüger Dieter Lüst Anton Rebhan Theoretische Physik Theoretische Physik Matthias Bartelmann Björn Feuerbacher Timm Krüger Dieter Lüst Anton Rebhan Theoretische

Mehr

Vermögenseinlagen stiller Gesellschafter, Genußrechtskapital und nachrangige Verbindlichkeiten als haftendes Eigenkapital von Kreditinstituten

Vermögenseinlagen stiller Gesellschafter, Genußrechtskapital und nachrangige Verbindlichkeiten als haftendes Eigenkapital von Kreditinstituten Wirtschaft Markus Stang Vermögenseinlagen stiller Gesellschafter, Genußrechtskapital und nachrangige Verbindlichkeiten als haftendes Eigenkapital von Kreditinstituten Vergleichende Darstellung sowie kritische

Mehr

CATIA V5 downloaded from by on January 20, For personal use only.

CATIA V5 downloaded from  by on January 20, For personal use only. Patrick Kornprobst CATIA V5 Volumenmodellierung Konstruktionsmethodik zur Modellierung von Volumenkörpern vbleiben Sie einfach auf dem Laufenden: www.hanser.de/newsletter Sofort anmelden und Monat für

Mehr

Kundenzufriedenheit im Mittelstand

Kundenzufriedenheit im Mittelstand Wirtschaft Daniel Schallmo Kundenzufriedenheit im Mittelstand Grundlagen, methodisches Vorgehen bei der Messung und Lösungsvorschläge, dargestellt am Beispiel der Kienzer GmbH Diplomarbeit Bibliografische

Mehr

Usability Analyse des Internetauftritts der Firma MAFI Transport-Systeme GmbH

Usability Analyse des Internetauftritts der Firma MAFI Transport-Systeme GmbH Wirtschaft Markus Hartmann Usability Analyse des Internetauftritts der Firma MAFI Transport-Systeme GmbH Diplomarbeit Bibliografische Information der Deutschen Nationalbibliothek: Bibliografische Information

Mehr

Mathematik für Physiker Band 3

Mathematik für Physiker Band 3 Helmut Fischer Helmut Kaul Mathematik für Physiker Band 3 Variationsrechnung Differentialgeometrie Mathemati sche Grundlagen der Allgemeinen Relativitätstheorie 4. Auflage Mathematik für Physiker Band

Mehr

Bibliografische Information der Deutschen Nationalbibliothek:

Bibliografische Information der Deutschen Nationalbibliothek: Bibliografische Information der Deutschen Nationalbibliothek: Bibliografische Information der Deutschen Nationalbibliothek: Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie;

Mehr

NEBEN - LÄUFIGE PROG RAM -

NEBEN - LÄUFIGE PROG RAM - carsten VOGT NEBEN - LÄUFIGE PROG RAM - M IERUNG EIN ARBEITSBUCH MIT UNIX/LINUX UND JAVA EXTRA: Mit kostenlosem E-Book Im Internet: Programmbeispiele, Lösungen zu den Aufgaben und Zusatzmaterialien Inhalt

Mehr

Algorithmen versus Programmiersprachen

Algorithmen versus Programmiersprachen Coma I Einleitung Computer und Algorithmen Programmiersprachen Algorithmen versus Programmiersprachen Literaturhinweise Computer und Algorithmen Programmiersprachen Algorithmen versus Programmiersprachen

Mehr

Betriebssysteme. Tutorium 2. Philipp Kirchhofer

Betriebssysteme. Tutorium 2. Philipp Kirchhofer Betriebssysteme Tutorium 2 Philipp Kirchhofer philipp.kirchhofer@student.kit.edu http://www.stud.uni-karlsruhe.de/~uxbtt/ Lehrstuhl Systemarchitektur Universität Karlsruhe (TH) 4. November 2009 Philipp

Mehr

TECHNISCHE HOCHSCHULE NÜRNBERG GEORG SIMON OHM Die Mikroprogrammebene eines Rechners Das Abarbeiten eines Arbeitszyklus eines einzelnen Befehls besteht selbst wieder aus verschiedenen Schritten, z.b. Befehl

Mehr

Peter Kröner. Einstieg in CSS3. Standards und Struktur. 1. Auflage. Open Source Press

Peter Kröner. Einstieg in CSS3. Standards und Struktur. 1. Auflage. Open Source Press Peter Kröner Einstieg in CSS3 Standards und Struktur 1. Auflage Open Source Press Alle in diesem Buch enthaltenen Programme, Darstellungen und Informationen wurden nach bestem Wissen erstellt. Dennoch

Mehr

Video-Marketing mit YouTube

Video-Marketing mit YouTube Video-Marketing mit YouTube Christoph Seehaus Video-Marketing mit YouTube Video-Kampagnen strategisch planen und erfolgreich managen Christoph Seehaus Hamburg Deutschland ISBN 978-3-658-10256-2 DOI 10.1007/978-3-658-10257-9

Mehr

Prüfprozesseignung nach VDA 5 und ISO

Prüfprozesseignung nach VDA 5 und ISO Edgar Dietrich Michael Radeck Prüfprozesseignung nach VDA 5 und ISO 22514-7 Pocket Power Edgar Dietrich Michael Radeck Prüfprozesseignung nach VDA 5 und ISO 22514-7 1. Auflage Die Wiedergabe von Gebrauchsnamen,

Mehr

Übung 1 - Betriebssysteme I

Übung 1 - Betriebssysteme I Prof. Dr. Th. Letschert FB MNI 13. März 2002 Aufgabe 0: Basiswissen Rechnerarchitektur: Übung 1 - Betriebssysteme I Aus welchen Komponenten besteht ein Rechner mit Von-Neumann Architektur? Was sind Bits

Mehr

Diplomarbeit BESTSELLER. Eva-Maria Matzker. Einrichtungen des Gesundheitswesens strategisch steuern. Anwendung der Balanced Scorecard

Diplomarbeit BESTSELLER. Eva-Maria Matzker. Einrichtungen des Gesundheitswesens strategisch steuern. Anwendung der Balanced Scorecard Diplomarbeit BESTSELLER Eva-Maria Matzker Einrichtungen des Gesundheitswesens strategisch steuern Anwendung der Balanced Scorecard Matzker, Eva-Maria: Einrichtungen des Gesundheitswesens strategisch steuern

Mehr

Betriebssysteme. FU Berlin WS 2004/05 Klaus-Peter Löhr. bs-1.1 1

Betriebssysteme. FU Berlin WS 2004/05 Klaus-Peter Löhr. bs-1.1 1 Betriebssysteme FU Berlin WS 2004/05 Klaus-Peter Löhr bs-1.1 1 1 Einführung Zur Erinnerung: Informatische Fachbegriffe in Deutsch und Englisch findet man unter http://www.babylonia.org.uk bs-1.1 2 Software

Mehr

Die gesetzliche Unfallversicherung - von der Behörde zum modernen Dienstleistungsunternehmen

Die gesetzliche Unfallversicherung - von der Behörde zum modernen Dienstleistungsunternehmen Wirtschaft Michael Zechmeister Die gesetzliche Unfallversicherung - von der Behörde zum modernen Dienstleistungsunternehmen Dargestellt am Beispiel der Württembergischen Bau-Berufsgenossenschaft Diplomarbeit

Mehr

Waveletanalyse von EEG-Zeitreihen

Waveletanalyse von EEG-Zeitreihen Naturwissenschaft Heiko Hansen Waveletanalyse von EEG-Zeitreihen Diplomarbeit Bibliografische Information der Deutschen Nationalbibliothek: Bibliografische Information der Deutschen Nationalbibliothek:

Mehr

A Kompilieren des Kernels... 247. B Lineare Listen in Linux... 251. C Glossar... 257. Interessante WWW-Adressen... 277. Literaturverzeichnis...

A Kompilieren des Kernels... 247. B Lineare Listen in Linux... 251. C Glossar... 257. Interessante WWW-Adressen... 277. Literaturverzeichnis... 1 Einführung................................................ 1 1.1 Was ist ein Betriebssystem?............................... 1 1.1.1 Betriebssystemkern................................ 2 1.1.2 Systemmodule....................................

Mehr

Grundlagen Rechnerarchitektur und Betriebssysteme

Grundlagen Rechnerarchitektur und Betriebssysteme Grundlagen Rechnerarchitektur und Betriebssysteme Johannes Formann Definition Computer: Eine Funktionseinheit zur Verarbeitung von Daten, wobei als Verarbeitung die Durchführung mathematischer, umformender,

Mehr

SPD als lernende Organisation

SPD als lernende Organisation Wirtschaft Thomas Schalski-Seehann SPD als lernende Organisation Eine kritische Analyse der Personal- und Organisationsentwicklung in Parteien Masterarbeit Bibliografische Information der Deutschen Nationalbibliothek:

Mehr

Lernen zu lernen. Werner Metzig Martin Schuster. Lernstrategien wirkungsvoll einsetzen

Lernen zu lernen. Werner Metzig Martin Schuster. Lernstrategien wirkungsvoll einsetzen Lernen zu lernen Werner Metzig Martin Schuster Lernstrategien wirkungsvoll einsetzen Lernen zu lernen Werner Metzig Martin Schuster Lernen zu lernen Lernstrategien wirkungsvoll einsetzen 9. Auflage Werner

Mehr

Spezielle und allgemeine Relativitätstheorie Grundlagen, Anwendungen in Astrophysik und Kosmologie sowie relativistische Visualisierung

Spezielle und allgemeine Relativitätstheorie Grundlagen, Anwendungen in Astrophysik und Kosmologie sowie relativistische Visualisierung Sebastian Boblest Thomas Müller Spezielle und allgemeine Relativitätstheorie Grundlagen, Anwendungen in Astrophysik und Kosmologie sowie relativistische Visualisierung Spezielle und allgemeine Relativitätstheorie

Mehr

Gradle. Ein kompakter Einstieg in modernes Build-Management. Joachim Baumann. Joachim Baumann, Gradle, dpunkt.verlag, ISBN

Gradle. Ein kompakter Einstieg in modernes Build-Management. Joachim Baumann. Joachim Baumann, Gradle, dpunkt.verlag, ISBN D3kjd3Di38lk323nnm Joachim Baumann Gradle Ein kompakter Einstieg in modernes Build-Management Joachim Baumann joachim.baumann@codecentric.de Lektorat: René Schönfeldt Copy Editing: Sandra Gottmann, Münster-Nienberge

Mehr

Übersicht. Virtuelle Maschinen Erlaubnisse (Permission, Rechte) Ringe. AVS SS Teil 12/Protection

Übersicht. Virtuelle Maschinen Erlaubnisse (Permission, Rechte) Ringe. AVS SS Teil 12/Protection Übersicht Virtuelle Maschinen Erlaubnisse (Permission, Rechte) Ringe 2 Behandelter Bereich: Virtualisierung Syscall-Schnittstelle Ports Server Apps Server Apps Betriebssystem Protokolle Betriebssystem

Mehr

Ist Europa ein optimaler Währungsraum?

Ist Europa ein optimaler Währungsraum? Wirtschaft Alexander Charles Ist Europa ein optimaler Währungsraum? Eine Analyse unter Berücksichtigung der EU-Osterweiterung Bachelorarbeit Bibliografische Information der Deutschen Nationalbibliothek:

Mehr

Betriebssysteme. FU Berlin SS 2003 Klaus-Peter Löhr

Betriebssysteme. FU Berlin SS 2003 Klaus-Peter Löhr Betriebssysteme FU Berlin SS 2003 Klaus-Peter Löhr 1 Einführung Zur Erinnerung: Informatische Fachbegriffe in Deutsch und Englisch findet man unter http://www.babylonia.org.uk Software zwischen Hardware

Mehr

Kommunikation im Krankenhaus

Kommunikation im Krankenhaus Gaby Baller Bernhard Schaller Kommunikation im Krankenhaus Erfolgreich kommunizieren mit Patienten, Arztkollegen und Klinikpersonal Kommunikation im Krankenhaus Gaby Baller Bernhard Schaller Kommunikation

Mehr

Anja Schüler. Arbeit für alle?! Berufliche Teilhabe von Menschen mit geistiger Behinderung in Deutschland und den USA.

Anja Schüler. Arbeit für alle?! Berufliche Teilhabe von Menschen mit geistiger Behinderung in Deutschland und den USA. Anja Schüler Arbeit für alle?! Berufliche Teilhabe von Menschen mit geistiger Behinderung in Deutschland und den USA Bachelorarbeit BACHELOR + MASTER Publishing Schüler, Anja: Arbeit für alle?! Berufliche

Mehr

Springer Spektrum, Springer Vieweg und Springer Psychologie.

Springer Spektrum, Springer Vieweg und Springer Psychologie. essentials Essentials liefern aktuelles Wissen in konzentrierter Form. Die Essenz dessen, worauf es als State-of-the-Art in der gegenwärtigen Fachdiskussion oder in der Praxis ankommt. Essentials informieren

Mehr

X.systems.press ist eine praxisorientierte Reihe zur Entwicklung und Administration von Betriebssystemen, Netzwerken und Datenbanken.

X.systems.press ist eine praxisorientierte Reihe zur Entwicklung und Administration von Betriebssystemen, Netzwerken und Datenbanken. X. systems.press X.systems.press ist eine praxisorientierte Reihe zur Entwicklung und Administration von Betriebssystemen, Netzwerken und Datenbanken. Martin Grotegut Windows Vista Service Pack 1 123 Martin

Mehr

Systeme I: Betriebssysteme Kapitel 2 Überblick Betriebssysteme. Maren Bennewitz

Systeme I: Betriebssysteme Kapitel 2 Überblick Betriebssysteme. Maren Bennewitz Systeme I: Betriebssysteme Kapitel 2 Überblick Betriebssysteme Maren Bennewitz 1 Überblick Betriebssysteme Aufgabe von Betriebssystemen Historische Entwicklung von Betriebssystemen Unterschiedliche Arten

Mehr

Grundsoftware üblicher Computersysteme

Grundsoftware üblicher Computersysteme Informatik 1 für Nebenfachstudierende Grundmodul Grundsoftware üblicher Computersysteme Kai-Steffen Hielscher Folienversion: 22. November 2017 Informatik 7 Rechnernetze und Kommunikationssysteme Inhaltsübersicht

Mehr

/.../ ein absolut lohnenswertes Buch. Prof. Dr. Stephan Kleuker, FH Nordakademie

/.../ ein absolut lohnenswertes Buch. Prof. Dr. Stephan Kleuker, FH Nordakademie Leserstimmen zur 2. Auflage: /.../ sorgfältig ausgewählt und zusammengestellt. Eine leicht verständliche und gut strukturierte Abhandlung des Themas. Empfehlenswert. Prof. Dr. Bernhard Bürg, FH Karlsruhe

Mehr

Bibliografische Information der Deutschen Nationalbibliothek:

Bibliografische Information der Deutschen Nationalbibliothek: Bibliografische Information der Deutschen Nationalbibliothek: Bibliografische Information der Deutschen Nationalbibliothek: Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie;

Mehr

Informatik 12 Kapitel 2 - Kommunikation und Synchronisation von Prozessen

Informatik 12 Kapitel 2 - Kommunikation und Synchronisation von Prozessen Fachschaft Informatik Informatik 12 Kapitel 2 - Kommunikation und Synchronisation von Prozessen Michael Steinhuber König-Karlmann-Gymnasium Altötting 9. Februar 2017 Folie 1/40 Inhaltsverzeichnis I 1 Kommunikation

Mehr

Dr. Monika Meiler. Inhalt

Dr. Monika Meiler. Inhalt Inhalt 15 Parallele Programmierung... 15-2 15.1 Die Klasse java.lang.thread... 15-2 15.2 Beispiel 0-1-Printer als Thread... 15-3 15.3 Das Interface java.lang.runnable... 15-4 15.4 Beispiel 0-1-Printer

Mehr

Gerd Czycholl. Theoretische Festkörperphysik Band 1. Grundlagen: Phononen und Elektronen in Kristallen 4. Auflage

Gerd Czycholl. Theoretische Festkörperphysik Band 1. Grundlagen: Phononen und Elektronen in Kristallen 4. Auflage Theoretische Festkörperphysik Band 1 Grundlagen: Phononen und Elektronen in Kristallen 4. Auflage Theoretische Festkörperphysik Band 1 Theoretische Festkörperphysik Band 1 Grundlagen: Phononen und Elektronen

Mehr

Arbeitsbuch Mathematik

Arbeitsbuch Mathematik Arbeitsbuch Mathematik Tilo Arens Frank Hettlich Christian Karpfinger Ulrich Kockelkorn Klaus Lichtenegger Hellmuth Stachel Arbeitsbuch Mathematik Aufgaben, Hinweise, Lösungen und Lösungswege 3. Auflage

Mehr

Management globaler Produktionsnetzwerke

Management globaler Produktionsnetzwerke Thomas Friedli Stefan Thomas Andreas Mundt Management globaler Produktionsnetzwerke Strategie Konfiguration Koordination EXTRA Mit kostenlosem E-Book Friedli/Thomas/Mundt Management globaler Produktionsnetzwerke

Mehr

Kennzahlenbasiertes Prozeßcontrolling für den Produktionsbereich in einem Unternehmen der Investitionsgüterindustrie

Kennzahlenbasiertes Prozeßcontrolling für den Produktionsbereich in einem Unternehmen der Investitionsgüterindustrie Wirtschaft Marc Joos Kennzahlenbasiertes Prozeßcontrolling für den Produktionsbereich in einem Unternehmen der Investitionsgüterindustrie Diplomarbeit Bibliografische Information der Deutschen Nationalbibliothek:

Mehr

Sport. Silke Hubrig. Afrikanischer Tanz. Zu den Möglichkeiten und Grenzen in der deutschen Tanzpädagogik. Examensarbeit

Sport. Silke Hubrig. Afrikanischer Tanz. Zu den Möglichkeiten und Grenzen in der deutschen Tanzpädagogik. Examensarbeit Sport Silke Hubrig Afrikanischer Tanz Zu den Möglichkeiten und Grenzen in der deutschen Tanzpädagogik Examensarbeit Bibliografische Information der Deutschen Nationalbibliothek: Bibliografische Information

Mehr

Nebenläufige Programmierung in Java: Threads

Nebenläufige Programmierung in Java: Threads Nebenläufige Programmierung in Java: Threads Wahlpflicht: Fortgeschrittene Programmierung in Java Jan Henke HAW Hamburg 10. Juni 2011 J. Henke (HAW) Threads 10. Juni 2011 1 / 18 Gliederung 1 Grundlagen

Mehr

Betriebssysteme Betriebssysteme und. Netzwerke. Netzwerke Theorie und Praxis

Betriebssysteme Betriebssysteme und. Netzwerke. Netzwerke Theorie und Praxis Einführung Einführung in in Betriebssysteme Betriebssysteme und und Theorie und Praxis Theorie und Praxis Oktober 2006 Oktober 2006 Prof. Dr. G. Hellberg Prof. Dr. G. Hellberg Email: hellberg@drhellberg.de

Mehr

Praktische Informatik 1

Praktische Informatik 1 Praktische Informatik 1 Imperative Programmierung und Objektorientierung Karsten Hölscher und Jan Peleska Wintersemester 2011/2012 Session 2 Programmierung Begriffe C/C++ Compiler: übersetzt Quellcode

Mehr

Kundenorientierung von Dienstleistungsunternehmen als kritischer Erfolgsfaktor

Kundenorientierung von Dienstleistungsunternehmen als kritischer Erfolgsfaktor Wirtschaft Madlen Martin Kundenorientierung von Dienstleistungsunternehmen als kritischer Erfolgsfaktor Kundenorientierung im Mittelpunkt des Wettbewerbes Diplomarbeit Bibliografische Information der Deutschen

Mehr

Word Basiswissen

Word Basiswissen Word 2013 Basiswissen Verlag: BILDNER Verlag GmbH Bahnhofstraße 8 94032 Passau http://www.bildner-verlag.de info@bildner-verlag.de Tel.: +49 851-6700 Fax: +49 851-6624 ISBN: 978-3-8328-0057-4 Autorin:

Mehr

OpenCL. Programmiersprachen im Multicore-Zeitalter. Tim Wiersdörfer

OpenCL. Programmiersprachen im Multicore-Zeitalter. Tim Wiersdörfer OpenCL Programmiersprachen im Multicore-Zeitalter Tim Wiersdörfer Inhaltsverzeichnis 1. Was ist OpenCL 2. Entwicklung von OpenCL 3. OpenCL Modelle 1. Plattform-Modell 2. Ausführungs-Modell 3. Speicher-Modell

Mehr

Betriebssysteme Kap A: Grundlagen

Betriebssysteme Kap A: Grundlagen Betriebssysteme Kap A: Grundlagen 1 Betriebssystem Definition DIN 44300 Die Programme eines digitalen Rechensystems, die zusammen mit den Eigenschaften dieser Rechenanlage die Basis der möglichen Betriebsarten

Mehr

Für Oma Christa und Opa Karl. Ihr seid die Besten - Danke.

Für Oma Christa und Opa Karl. Ihr seid die Besten - Danke. Weber, Stefanie: Kreative Wege zum literarischen Text im Spanischunterricht: Enrique Paez: Abdel. Beispiele zum Themenschwerpunkt movimientos migratorios, Hamburg, Bachelor + Master Publishing 2016 Originaltitel

Mehr

Günther Bourier. Statistik-Übungen

Günther Bourier. Statistik-Übungen Günther Bourier Statistik-Übungen Günther Bourier Statistik-Übungen Beschreibende Statistik Wahrscheinlichkeitsrechnung Schließende Statistik 4., aktualisierte Auflage Bibliografische Information der Deutschen

Mehr

Mädchen spielen mit Puppen Jungen auch?

Mädchen spielen mit Puppen Jungen auch? Staatsexamensarbeit Anika Wawzyniak Mädchen spielen mit Puppen Jungen auch? Vorstellung eines konkreten Unterrichtskonzepts zur möglichen Thematisierung von Geschlechterstereotypen im Deutschunterricht

Mehr

4D Server v12 64-bit Version BETA VERSION

4D Server v12 64-bit Version BETA VERSION 4D Server v12 64-bit Version BETA VERSION 4D Server v12 unterstützt jetzt das Windows 64-bit Betriebssystem. Hauptvorteil der 64-bit Technologie ist die rundum verbesserte Performance der Anwendungen und

Mehr

Supply Chain Management: Einführung im Rahmen einer ganzheitlichen ERP-Implementierung

Supply Chain Management: Einführung im Rahmen einer ganzheitlichen ERP-Implementierung Wirtschaft Sascha Pattberg Supply Chain Management: Einführung im Rahmen einer ganzheitlichen ERP-Implementierung Dargestellt am Beispiel eines kleinen, mittleren Unternehmens Diplomarbeit Bibliografische

Mehr

Zielvereinbarung - Erfolgsfaktoren bei der Umsetzung

Zielvereinbarung - Erfolgsfaktoren bei der Umsetzung Wirtschaft Dörte Lukas, geb. Cermak Zielvereinbarung - Erfolgsfaktoren bei der Umsetzung Diplomarbeit Bibliografische Information der Deutschen Nationalbibliothek: Bibliografische Information der Deutschen

Mehr

Spätes Bietverhalten bei ebay-auktionen

Spätes Bietverhalten bei ebay-auktionen Wirtschaft Christina Simon Spätes Bietverhalten bei ebay-auktionen Diplomarbeit Bibliografische Information der Deutschen Nationalbibliothek: Bibliografische Information der Deutschen Nationalbibliothek:

Mehr

Alfred Böge I Walter Schlemmer. Lösungen zuraufgabensammlung Technische Mechanik

Alfred Böge I Walter Schlemmer. Lösungen zuraufgabensammlung Technische Mechanik Alfred Böge I Walter Schlemmer Lösungen zuraufgabensammlung Technische Mechanik Lehr- und Lernsystem Technische Mechanik Technische Mechanik (Lehrbuch) von A. Böge Aufgabensammlung Technische Mechanik

Mehr

Informatik. Christian Kuhn. Web 2.0. Auswirkungen auf internetbasierte Geschäftsmodelle. Diplomarbeit

Informatik. Christian Kuhn. Web 2.0. Auswirkungen auf internetbasierte Geschäftsmodelle. Diplomarbeit Informatik Christian Kuhn Web 2.0 Auswirkungen auf internetbasierte Geschäftsmodelle Diplomarbeit Bibliografische Information der Deutschen Nationalbibliothek: Bibliografische Information der Deutschen

Mehr

Erfolgreiches Produktmanagement

Erfolgreiches Produktmanagement Erfolgreiches Produktmanagement Klaus Aumayr Erfolgreiches Produktmanagement Tool-Box für das professionelle Produktmanagement und Produktmarketing 4., aktualisierte und erweiterte Auflage Klaus Aumayr

Mehr

Systeme I: Betriebssysteme Kapitel 8 Speicherverwaltung

Systeme I: Betriebssysteme Kapitel 8 Speicherverwaltung Systeme I: Betriebssysteme Kapitel 8 Speicherverwaltung Version 21.12.2016 1 Inhalt Vorlesung Aufbau einfacher Rechner Überblick: Aufgabe, Historische Entwicklung, unterschiedliche Arten von Betriebssystemen

Mehr

Das Konzept der organisationalen Identität

Das Konzept der organisationalen Identität Wirtschaft Ute Staub Das Konzept der organisationalen Identität Eine kritische Analyse Diplomarbeit Bibliografische Information der Deutschen Nationalbibliothek: Bibliografische Information der Deutschen

Mehr