23.01.2014 Aufrufe

Betriebssysteme - Prozesse

Betriebssysteme - Prozesse

Betriebssysteme - Prozesse

MEHR ANZEIGEN
WENIGER ANZEIGEN

Erfolgreiche ePaper selbst erstellen

Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.

<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

<strong>Betriebssysteme</strong> - <strong>Prozesse</strong><br />

→ alois.schuette@h-da.de<br />

Alois Schütte<br />

20. November 2013<br />

1 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Inhaltsverzeichnis<br />

Ein Prozess kann als die Abstraktion eines Programms, das von einem<br />

Prozessor ausgeführt wird, angesehen werden.<br />

Hier wird behandelt, was <strong>Prozesse</strong> sind und wie sie in <strong>Betriebssysteme</strong>n<br />

implementiert werden, inklusive Prozesskommunikation, -synchronisation<br />

und Scheduling.<br />

1 Prozessmodell<br />

2 Interprozesskommunikation<br />

3 IPC Probleme<br />

4 Scheduling<br />

2 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Prozessmodell<br />

1 Prozessmodell<br />

Prozesszustände<br />

Implementierung von <strong>Prozesse</strong>n<br />

Abstraktion des Prozessmodells in Java<br />

2 Interprozesskommunikation<br />

3 IPC Probleme<br />

4 Scheduling<br />

3 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Prozessmodell<br />

Um zu verstehen, wie unterschiedliche Aktivitäten scheinbar gleichzeitig<br />

ablaufen, braucht man ein Modell eines sich in Ausführung befindenden<br />

Programms.<br />

Ein (sequentieller) Prozess ist ein sich in Ausführung befindendes<br />

(sequentielles) Programm zusammen mit:<br />

• dem aktuellen Wert des Programmzähler,<br />

• den Registerinhalten,<br />

• den Werten der Variablen und<br />

• dem Zustand der geöffneten Dateien und Netzverbindungen.<br />

Konzeptionell besitzt jeder Prozess seinen eigenen Prozessor - in der<br />

Praxis wird aber immer der reale Prozessor zwischen den <strong>Prozesse</strong>n hinund<br />

hergeschaltet.<br />

4 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Sichtweisen<br />

Beim Mehrprogrammbetrieb mit 4 Programmen (A-D) ergeben sich<br />

damit folgende Sichtweisen:<br />

5 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Prozess-Hierarchie<br />

Ein Betriebssystem mit einen solchen Prozessmodell muss in der Lage<br />

sein, dass von einem (Initial-) Programm andere <strong>Prozesse</strong> erzeugt werden<br />

können.<br />

• In Unix dient dazu der fork-Systemaufruf.<br />

• Beim ”<br />

Hochfahren“ des Betriebssystems<br />

werden dann alle erforderlichen <strong>Prozesse</strong><br />

erzeugt für Systemdienste, wie<br />

• Scheduler,<br />

• Dateidienst,<br />

• Terminaldienst,<br />

• ...<br />

6 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Prozesskategorien und Ausführungsmodi<br />

Damit das Betriebssystem allein die Ressourcen verwalten kann, werden<br />

die <strong>Prozesse</strong> in Kategorien unterteilt:<br />

• Kernel-<strong>Prozesse</strong> laufen von anderen <strong>Prozesse</strong>n abgeschottet; sie<br />

sind privilegiert, direkt auf Ressourcen zuzugreifen.<br />

• User <strong>Prozesse</strong> verwenden stets Kernel-Funktionalität, um indirekt<br />

auf Ressourcen zuzugreifen.<br />

Heutige Prozessoren unterscheiden prinzipiell zwei Ausführungsmodi für<br />

Instruktionen:<br />

• privilegierten Modus (Kernel-<strong>Prozesse</strong>):<br />

z.B. E/A-Befehle, Registerzugriff und Interruptmaskierung<br />

• Normalmodus (User-<strong>Prozesse</strong>)<br />

Ein Userprozess ruft über Systemfunktionen (system calls) einen<br />

Dienst im eigenen Adressraum auf.<br />

Kein Prozess hat dabei direkten Zugriff auf den Speicher des Kerns.<br />

7 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Trap-Mechanismus<br />

Eine Kernel-Funktion wird stets über den<br />

Trap-Mechanismus (Kernel-Aufruf) aufgerufen:<br />

1 Speichern des Funktionskodes und der<br />

Parameter in ?speziellen Registern<br />

2 Auslösen eines Software Interrupts, d.h<br />

• Inkrementieren des Programmzählers<br />

• Umschalten in privilegierten Modus User<br />

Modus<br />

3 Ausführen der Kernelfunktion<br />

4 Speichern des Ergebnisses in Registern<br />

5 Umschalten in Normalmodus<br />

6 Zurückladen der Ergebnisse aus dem Register<br />

7 Ausführen der nächsten Instruktion der<br />

Anwendung<br />

1<br />

2<br />

3<br />

4<br />

5<br />

6<br />

7<br />

User-<br />

Modus<br />

Kernel-<br />

Modus<br />

User-<br />

Modus<br />

8 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Prozesszustände<br />

Prozesszustände<br />

Eine Aufgabe des Betriebssystems ist das Multiplexen des physischen<br />

Prozessors.<br />

Diese Aufgabe übernimmt der Scheduler zusammen mit dem<br />

Dispatcher.<br />

<strong>Prozesse</strong> können sich in verschiedenen Zustände befinden.<br />

1 rechnend (running)<br />

der Prozessor ist dem Prozess zugeteilt<br />

2 bereit (ready)<br />

der Prozess ist ausführbar, aber ein anderer Prozess ist gerade<br />

rechnend<br />

3 blockiert (waiting)<br />

der Prozess kann nicht ausgeführt werden, da er auf ein externes<br />

Ereignis wartet (z.B. Benutzer hat Taste auf Tastatur gedrückt)<br />

Diese Zuständen bilden eine Entscheidungsgrundlage für die Auswahl<br />

eines geeigneten Kandidaten bei einem Prozesswechsel.<br />

9 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Prozesszustände<br />

Zustandsübergänge<br />

1 Prozess wartet auf externes<br />

Ereignis<br />

2 Scheduler wählt anderen<br />

Prozess aus, da die Zeitscheibe<br />

des <strong>Prozesse</strong>s abgelaufen ist<br />

3 Scheduler wählt diesen Prozess<br />

aus<br />

4 externes Ereignis ist eingetreten<br />

5 ein neuer Prozess wird erzeugt<br />

6 der Prozess terminiert<br />

6<br />

rechnend<br />

1 2<br />

3<br />

blockiert<br />

bereit<br />

4<br />

5<br />

Die Zustandsübergänge werden vom Dispatcher durchgeführt, die<br />

Auswahl eines rechenbereiten <strong>Prozesse</strong>s übernimmt der Scheduler.<br />

10 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Prozesszustände<br />

Modell für Prozesssystem<br />

Damit ergibt sich ein Modell für ein Prozesssystem, bei dem<br />

• die unterste Schicht die Unterbrechungen behandelt und das<br />

Scheduling der <strong>Prozesse</strong> inklusive Erzeugung und Abbrechen von<br />

<strong>Prozesse</strong>n erledigt;<br />

• alle anderen <strong>Prozesse</strong> befinden sich auf gleicher Ebene darüber und<br />

haben einen sequentiellen Kontrollfluss.<br />

• Somit gibt es stets einen Wechsel von Aktivitäten eines<br />

(User-)<strong>Prozesse</strong>s und des Kerns.<br />

<strong>Prozesse</strong><br />

1 2 3 ... n<br />

User-Modus<br />

Kernel-Modus<br />

Dispatcher / Scheduler<br />

11 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Implementierung von <strong>Prozesse</strong>n<br />

Implementierung von <strong>Prozesse</strong>n<br />

Das o.g. Prozessmodell kann in einem Betriebssystem durch eine<br />

Prozesstabelle, die für jeden Prozess einen Eintrag enthält, realisiert<br />

werden.<br />

Ein Eintrag muss alle Informationen enthalten, um einen Prozess wieder<br />

anlaufen zu lassen, wenn er suspendiert wurde:<br />

• Zustand,<br />

• Programmzähler,<br />

• Stack-Zeiger,<br />

• belegten Speicher,<br />

• Zustand der geöffneten Dateien und<br />

• Verwaltungsinformationen (bisher belegte CPU Zeit,<br />

Schedulinginformationen, etc.)<br />

Welche Information muss auf Ebene des HAL bei Wechsel des<br />

HAL-Programms/<strong>Prozesse</strong>s gespeichert werden? Also was muss<br />

passieren, wenn ein HAL-Prozessor einen Prozesswechsel durchführt?<br />

12 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Implementierung von <strong>Prozesse</strong>n<br />

Prozesstabelle<br />

Je nach Betriebssystem variieren diese Felder der Prozesstabelle.<br />

$ ps -o "%p %r %c %a %x %t"<br />

PID PGID COMMAND COMMAND TIME<br />

3427 3427 bash -bash 00:00:00<br />

3496 3496 ps ps -o %p %r %c % 00:00:00<br />

$<br />

Ein Prozesswechsel kann durch ein externes Ereignis ausgelöst werden.<br />

Dazu wird jeder Klasse von E/A-Geräten (Festplatten, Terminals, etc.)<br />

ein Unterbrechungsvektor zugeordnet, der die Adresse einer<br />

Unterbrechungsbehandlungsprozedur (Interrupt Routine) enthält.<br />

Wenn z.B. ein Terminal eine Unterbrechung auslöst, dann wird<br />

hardwareseitig (Microcode) folgendes getan:<br />

1 Programmzähler des aktuell laufender Prozess und einiger Register<br />

auf Stack legen<br />

2 Aufruf der entsprechenden Unterbrechungsbehandlungsprozedur<br />

Alle anderen Aktivitäten (Auswahl des nächsten <strong>Prozesse</strong>s, der nach der<br />

Unterbrechungsroutine laufen soll) muss durch die Betriebssystemsoftware<br />

erfolgen.<br />

13 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Implementierung von <strong>Prozesse</strong>n<br />

Dämonen<br />

In Unix werden Verwaltungsaufgaben von speziellen Programmen, die als<br />

Hintergrundprozesse ablaufen, übernommen.<br />

Beispiele sind:<br />

cron Systemcronometer<br />

at Ausführen eines Kommandos zu einer vorgegebenen Zeit<br />

inetd Internet Service Dämon<br />

sshd Secure Shell Dämon<br />

Solche <strong>Prozesse</strong> werden oft beim Hochfahren des Betriebssystems<br />

gestartet und erst beim Shutdown des Systems beendet.<br />

Auf Shellebene kann man sich Dämonen ansehen durch das<br />

ps-Kommando. Sie sind daran zu erkennen, dass die Spalte ”<br />

TTY“ ein<br />

Fragezeichen ”<br />

?“ enthält.<br />

$ ps -el | grep \?<br />

1277 ? 00:02:22 crond<br />

1291 ? 00:00:00 atd<br />

1578 ? 00:00:03 sshd<br />

30045 ? 00:03:41 sendmail<br />

14 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Implementierung von <strong>Prozesse</strong>n<br />

Diese <strong>Prozesse</strong> sind als Dämonen realisiert:<br />

• Jeder Prozess (außer init) in Unix hat einen Vater und gehört zu<br />

einer Prozessgruppe.<br />

• Jede Prozessgruppe hat einen Prozess als Prozessgruppenleiter<br />

und besitzt maximal ein Kontrollterminal. Das Kontrollterminal<br />

liefert für alle Mitglieder der Prozessgruppe die Standardeinstellungen<br />

von stdin, stdout und stderr. Das Kontrollterminal<br />

lässt sich immer unter dem Namen ”<br />

/dev/tty“ ansprechen.<br />

• Ein Signal des Kontrollterminals wird an alle Mitglieder der<br />

Prozessgruppe gesendet (die Shell schützt sich und alle von ihr<br />

initiierten <strong>Prozesse</strong> allerdings davor).<br />

• <strong>Prozesse</strong>, die zwar Mitglieder einer Prozessgruppe sind, aber kein<br />

Kontrollterminal haben, nennt man Dämonen.<br />

• Da sie kein Kontrollterminal besitzen, sind sie nicht über<br />

Signale abzubrechen.<br />

• Diese Dämonen treiben ihr Unwesen ”<br />

im Bauch der Maschine“.<br />

15 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Implementierung von <strong>Prozesse</strong>n<br />

Zur Implementierung von Dämonen sollen folgende Regeln eingehalten<br />

werden:<br />

• Zunächst sollte ein fork ausgeführt werden, der Elternprozess sollte<br />

danach ein exit ausführen. Dadurch wird folgendes erreicht:<br />

1 Wird der Prozess von der Shell gestartet, so denkt die Shell, der<br />

Prozess wäre terminiert, da der Vater beendet ist.<br />

2 Das Kind erbt vom Vater die Prozessgruppe, bekommt aber (durch<br />

fork) eine neue PID. Dadurch ist ausgeschlossen, dass der Prozess<br />

ein Prozessgruppenführer ist.<br />

• Der nächste Schritt ist, den Systemaufruf ”<br />

setsid()“ auszuführen.<br />

Dadurch wird eine neue Session erzeugt und der Aufrufer wird<br />

Prozessgruppenführer.<br />

• Nun wird in das aktuelle Verzeichnis gesetzt. Es sollte das<br />

Root-Verzeichnis sein, damit nicht ein eventuelle gemountetes<br />

Dateisystem (wenn das das Home-Verzeichnis des Aufrufers war)<br />

dazu führt, dass das Betriebssystem nicht heruntergefahren werden<br />

kann.<br />

• Die Maske zum Dateierzeugen (umask) wird auf 0 gesetzt, damit<br />

nicht geerbte Maske vom Aufrufer dazu führt, dass der Dämon<br />

Dateien nicht anlegen kann.<br />

• Letztlich sollten alle nicht benötigten Filedeskriptoren geschlossen 16 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Implementierung von <strong>Prozesse</strong>n<br />

Beispiel Dämon<br />

daemon.c<br />

1 # include <br />

2 # include <br />

3 # include <br />

4 int daemon_init ( void ) {<br />

5 pid_t pid ;<br />

6 if (( pid = fork ()) < 0)<br />

7 return ( -1);<br />

8 else if ( pid != 0)<br />

9 exit (0); /* parent goes bye - bye */<br />

10 /* child */<br />

11 setsid (); /* become session leader */<br />

12 chdir ("/"); /* change working directory */<br />

13 umask (0); /* clear our file mode creatio mask */<br />

14 return (0);<br />

15 }<br />

16 int main () { /* ... * some initial tasks */<br />

17 daemon_init (); /* make a daemon out of the program */<br />

18 sleep (100); /* daemon code , we see the<br />

19 daemon with ps -x */<br />

20 exit (0);<br />

21 }<br />

17 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Implementierung von <strong>Prozesse</strong>n<br />

Dämonen<br />

$ ./ daemon<br />

$ ps -xl<br />

F UID PID PPID PRI WCHAN STAT TTY TIME COMMAND<br />

0 500 22873 22871 20 wait Ss pts /0 0:00 -bash<br />

1 500 23030 1 20 hrtime Ss ? 0:00 ./ daemon ←<br />

0 500 23031 22873 20 R+ pts /0 0:00 ps -xl<br />

$<br />

Nach de Start des Dämon kann man mit ”<br />

ps –xl“ sehen, dass der Dämon<br />

aktiv ist und als Vater und den Prozess 1 besitzt.<br />

Ein Problem bei Dämonen ist, dass es nicht möglich ist, stderr und<br />

stdout zu verwenden. Also muss man eine Methode entwickeln, wie ein<br />

Dämon die Aktivitäten protokollieren kann. In Unix existiert das syslog<br />

Werkzeug, um Nachrichten geordnet zu verarbeiten.<br />

18 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Implementierung von <strong>Prozesse</strong>n<br />

Zombies<br />

Ein Prozess terminiert und hört auf zu existieren, wenn zwei Ereignisse<br />

eingetreten sind:<br />

1 Der Prozess hat sich selbst beendet oder wurde durch ein Signal<br />

beendet und<br />

2 der Vaterprozess hat ”<br />

wait()“ aufgerufen.<br />

Ein Prozess, der beendet wird, bevor der Vater einen wait-Aufruf für ihn<br />

ausgeführt hat, erhält einen besonderen Zustand, er wird ein Zombie:<br />

• Der Prozess wird vom Scheduler nicht mehr berücksichtigt, er wird<br />

aber nicht aus der Prozesstabelle entfernt.<br />

• Wenn der Vater dann einen wait-Aufruf veranlasst, wird der Prozess<br />

aus der Prozesstabelle entfernt und der belegte (Haupt-) Speicher<br />

wird frei gegeben.<br />

ps -l:<br />

0 laufend<br />

S schlafend (sleeping)<br />

R auf den Prozessor wartend (runable) ...<br />

Z Zombie<br />

19 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Implementierung von <strong>Prozesse</strong>n<br />

Beispiel Zombie<br />

zombie.c<br />

1 /* Create zombie to illustrate "ps -el */<br />

2 # include <br />

3 int main () {<br />

4 int pid ;<br />

5 if (( pid = fork ()) == -1)<br />

6 fprintf ( stderr ," fork failed !\n");<br />

7 if ( pid == 0) { /* Child */<br />

8 printf (" child : %d\n", getpid ());<br />

9 exit (1); /* because father will not wait , a zombie is born */<br />

10 } else { /* fahter */<br />

11 printf (" father : %d\n", getpid ());<br />

12 sleep (30);<br />

13 /* now you can use ps -el to see during 30 secs that<br />

14 child is a zombie */<br />

15 exit (0);<br />

16 }<br />

17 }<br />

20 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Implementierung von <strong>Prozesse</strong>n<br />

Beispiel Zombie<br />

$ ./ zombie &<br />

[1] 23036<br />

$ father : 23036<br />

child : 23037<br />

ps -l<br />

F S PID PPID WCHAN TTY TIME CMD<br />

0 S 22873 22871 wait pts /0 00:00:00 bash<br />

0 S 23036 22873 hrtime pts /0 00:00:00 zombie ←<br />

1 Z 23037 23036 exit pts /0 00:00:00 zombie ←<br />

0 R 23038 22873 - pts /0 00:00:00 ps<br />

$<br />

21 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Implementierung von <strong>Prozesse</strong>n<br />

Zombies entfernen per SIGCHLD<br />

Wenn ein Vater nicht auf die Beendigung seiner Kinder warten kann (z.B.<br />

bei der Shell mit Hintergrundprozessen), kann man die Zombies<br />

eliminieren, indem<br />

• der Vater auf das Signal SIGCHLD reagiert<br />

• die Signalbehandlungsroutine dann mittels waitpid den Zobmiestatus<br />

des Kindes aufhebt.<br />

zombie cleanup.c<br />

/* Simplest dead child cleanup in a SIGCHLD handler .<br />

* Prevent zombie processes but don 't actually do anything<br />

* with the information that a child died .<br />

*/<br />

# include <br />

...<br />

/* SIGCHLD handler . */<br />

static void sigchld_hdl ( int sig ) {<br />

/* Wait for all dead processes .<br />

* We use a non - blocking call to be sure this signal handler<br />

* will not block if a child was cleaned up in another<br />

* part of the program .<br />

*/<br />

while ( waitpid (-1, NULL , WNOHANG ) > 0) ; ←<br />

}<br />

22 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Implementierung von <strong>Prozesse</strong>n<br />

Zombies entfernen per SIGCHLD<br />

int main ( int argc , char * argv []) {<br />

int i;<br />

if ( signal ( SIGCHLD , sigchld_hdl )) { ←<br />

perror (" signal ");<br />

return 1;<br />

}<br />

}<br />

/* Make some children . */<br />

for (i = 0; i < 5; i ++) {<br />

switch ( fork ()) {<br />

case -1:<br />

perror (" fork ");<br />

return 1;<br />

case 0: // do something<br />

return 0;<br />

}<br />

}<br />

while (1) {<br />

// father performs something<br />

}<br />

return 0;<br />

23 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

Abstraktion des Prozessmodells in Java<br />

Um Eigenschaften und Probleme mit <strong>Prozesse</strong>n zeigen zu können, werden<br />

Algorithmen in der Vorlesung in Java (oder C) beschrieben.<br />

Betrachtet werden Java-Threads als Abstraktion von <strong>Prozesse</strong>n eines<br />

Rechners:<br />

Java Prozess<br />

Threads<br />

Rechner<br />

<strong>Prozesse</strong><br />

gemeinsamer<br />

Speicher<br />

Speicher<br />

Deshalb wird hier zunächst gezeigt, wie in Java Threads erzeugt werden<br />

können.<br />

24 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

Erzeugen und Starten vonJava-Threads<br />

Beim Start eines Java Programms wird ein Prozess erzeugt, der einen<br />

Thread enthält, der die Methode main der angegebenen Klasse ausführt.<br />

Der Code weiterer Threads muss in einer Methode mit Namen run<br />

realisiert werden.<br />

public void run () {<br />

// Code wird in eigenem Thread ausgeführt<br />

}<br />

Ein Programm, das Threads erzeugt, erbt von der Klasse Thread und<br />

überschreibt die Methode run() (auf Interfaces wird später eingegangen):<br />

25 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

Hello World<br />

MyThread.java<br />

1 public class MyThread extends Thread { ←<br />

2 public void run () {<br />

3 System . out . println (" Hello World ");<br />

4 }<br />

6 public static void main ( String [] args ) {<br />

7 MyThread t = new MyThread (); ←<br />

8 t. start (); ←<br />

9 }<br />

10 }<br />

Die Methode start() ist in der Klasse thread definiert und startet den<br />

Thread, genauer die Methode run(). Somit wird zunächst ein Thread<br />

erzeugt (new ...), dann durch start die Methode run aufgerufen und<br />

somit ”<br />

Hello World“ ausgegeben.<br />

26 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

mehrere Threads<br />

Werden mehrere Thread erzeugt, so ist die Ausführungsreihenfolge nicht<br />

vorhersehbar!<br />

Loop1.java<br />

1 public class Loop1 extends Thread {<br />

2 private String myName ;<br />

3 public Loop1 ( String name ) {<br />

4 myName = name ;<br />

5 }<br />

7 public void run () {<br />

8 for ( int i = 1; i


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

Threads in komplexen Klassenhierarchien<br />

Wenn sich die Methode run() in einer Klasse befinden soll, die selbst<br />

bereits aus einer anderen Klasse abgeleitet ist, so kann diese Klasse nicht<br />

zusätzlich von Thread abgeleitet werden (Java unterstützt keine<br />

Mehrfachvererbung).<br />

In diesem Fall kann das Interface Runnable des Package java.lang<br />

verwendet werden.<br />

MyRunnableThread.java<br />

1 public class MyRunnableThread implements Runnable { ←<br />

2 public void run () {<br />

3 System . out . println (" Hello World ");<br />

4 }<br />

5 public static void main ( String [] args ) {<br />

6 MyRunnableThread runner = new MyRunnableThread (); ←<br />

7 Thread t = new Thread ( runner ); ←<br />

8 t. start ();<br />

9 }<br />

10 }<br />

28 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

Threadtermination<br />

Ein Thread terminiert, wenn seine run()-Methode (bzw. die Methode<br />

main() im Fall des Ursprungs-Thread) beendet ist.<br />

Sind alle von einem Prozess initiierten Threads beendet, so terminiert der<br />

Prozess (falls es kein Dämon ist).<br />

Die Klasse Thread stellt eine Methode isAlive bereit, mit der abfragbar<br />

ist, ob ein Thread noch lebt (schon gestartet und noch nicht terminiert<br />

ist).<br />

Damit könnte aktives Warten etwa wie folgt programmiert werden:<br />

1 // MyThread sei aus Thread abgeleitet<br />

2 MyThread t = new myThread ();<br />

3 t. start ();<br />

4 while (t. isAlive ())<br />

5 ;<br />

6 // hier ist jetzt : t. isAlive == false , der Thread t ist terminiert<br />

Man sollte es so aber nie tun, da aktives Warten sehr rechenintensiv ist.<br />

wieso?<br />

29 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

Warten bis Thread beendet ist<br />

Wenn in einer Anwendung auf das Ende eines Thread gewartet werden<br />

muss, etwa um die Rechenergebnisse des Thread weiterzuverarbeiten,<br />

kann die Methode join der Klasse Thread benutzt werden.<br />

Der Thread wird blockiert, bis der Thread, auf den man wartet, beendet<br />

ist.<br />

1 // MyThread sei aus Thread abgeleitet<br />

2 MyThread t = new myThread ();<br />

3 t. start ();<br />

4 t. join (); // blockiert , bis t beendet ist .<br />

5 // auch hier jetzt : t. isAlive == false , der Thread t ist terminiert<br />

wieso ist das besser?<br />

30 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

Verwendung von join zum Abfragen von Ergebnissen<br />

Das folgende Beispiel verwendet Threads, um ein grosses Feld von<br />

Boolean zu analysieren, indem mehrere Threads parallel arbeiten, wobei<br />

je Thread ein Bereich des Feldes bearbeitet wird.<br />

1 0 0 0 1 0 1 1 0 0 0 0 1 1 1 1<br />

T1:1 T2:3 T3:0 T4:4<br />

main: 8<br />

31 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

1 class Service implements Runnable { ←<br />

2 private boolean [] array ;<br />

3 private int start ;<br />

4 private int end ;<br />

5 private int result ;<br />

7 public Service ( boolean [] array , int start , int end ) {<br />

8 this . array = array ;<br />

9 this . start = start ;<br />

10 this . end = end ;<br />

11 }<br />

13 public int getResult () {<br />

14 return result ;<br />

15 }<br />

17 public void run () { ←<br />

18 for ( int i = start ; i


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

1 public class AsynchRequest {<br />

2 private static final int ARRAY_SIZE = 100000; ←<br />

3 private static final int NUMBER_OF_SERVERS = 100; ←<br />

5 public static void main ( String [] args ) {<br />

6 // start time<br />

7 long startTime = System . currentTimeMillis ();<br />

9 // array creation , init with random boolean values ←<br />

10 boolean [] array = new boolean [ ARRAY_SIZE ];<br />

11 for ( int i = 0; i < ARRAY_SIZE ; i ++) {<br />

12 if( Math . random () < 0.1)<br />

13 array [i] = true ;<br />

14 else<br />

15 array [i] = false ;<br />

16 }<br />

18 // creation of array for service objects and threads ←<br />

19 Service [] service = new Service [ NUMBER_OF_SERVERS ]; ←<br />

20 Thread [] serverThread = new Thread [ NUMBER_OF_SERVERS ]; ←<br />

22 int start = 0;<br />

23 int end ;<br />

24 int howMany = ARRAY_SIZE / NUMBER_OF_SERVERS ;<br />

33 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

1 // creation of services and threads ←<br />

2 for ( int i = 0; i < NUMBER_OF_SERVERS ; i ++) {<br />

3 end = start + howMany - 1;<br />

4 service [i] = new Service ( array , start , end ); ←<br />

5 serverThread [i] = new Thread ( service [i ]); ←<br />

6 serverThread [i]. start (); // start thread i ←<br />

7 start = end + 1;<br />

8 }<br />

10 // wait for termination of each service ( thread ) ←<br />

11 try {<br />

12 for ( int i = 0; i < NUMBER_OF_SERVERS ; i ++)<br />

13 serverThread [i]. join (); ←<br />

14 } catch ( InterruptedException e) {}<br />

16 // accumulate service results ←<br />

17 int result = 0;<br />

18 for ( int i = 0; i < NUMBER_OF_SERVERS ; i ++) {<br />

19 result += service [i]. getResult (); ←<br />

20 }<br />

34 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

1 // end time<br />

2 long endTime = System . currentTimeMillis ();<br />

3 float time = ( endTime - startTime ) / 1000.0 f;<br />

4 System . out . println (" computation time : " + time );<br />

6 // print result<br />

7 System . out . println (" result : " + result );<br />

8 } // main<br />

9 }<br />

$ java AsynchRequest<br />

Rechenzeit : 0.11<br />

Ergebnis : 9953<br />

$<br />

35 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

zum Üben für zu Hause<br />

1 Testen des Verhaltens, wenn NUMBER OF SERVERS mit 1, 10,<br />

100, 1.000, 10.000 und 100.00 gesetzt wird und Erklärung des<br />

Ergebnisses.<br />

2 Ändern der Metode run() und Test und Erklärung der Auswirkung<br />

gegenüber dem Verhalten von 1.<br />

public void run () {<br />

for ( int i = start ; i


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Prozessmodell<br />

Abstraktion des Prozessmodells in Java<br />

Beenden von Threads per Programm<br />

Die Klasse Thread besitzt die Methode stopp(), zum Abbruch von<br />

Threads.<br />

Die Verwendung ist aber problematisch, da ein inkonsistenter Zustand<br />

erreicht werden kann.<br />

• Dies ist der Fall, wenn der Thread gerade eine<br />

synchronized()-Methode (später) ausführt und den Zustand eines<br />

Objektes verändert und dies aber nicht bis zum Schluss durchführen<br />

kann (wegen Stopp()).<br />

• Durch Stopp werden alle Sperren aufgehoben, der Thread konnte<br />

aber nicht fertig werden.<br />

Eine Abhandlung zum Stoppen von Threads kann nachgelesen werden in<br />

threadPrimitiveDeprecation oder HowToStopThreads.<br />

Wir werden später nochmals darauf zurückkommen.<br />

37 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Interprozesskommunikation<br />

1 Prozessmodell<br />

2 Interprozesskommunikation<br />

Probleme des gemeinsamen Zugriffs<br />

Kritischer Bereich<br />

Lösungsansätze in Java<br />

Semaphore<br />

Monitore und andere Primitive<br />

Nachrichtenaustausch<br />

3 IPC Probleme<br />

4 Scheduling<br />

38 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Interprozesskommunikation<br />

<strong>Betriebssysteme</strong> stellen unterschiedliche Mechanismen zur Verfügung, mit<br />

denen zwei <strong>Prozesse</strong> kommunizieren können:<br />

• beide <strong>Prozesse</strong> können sich den Arbeitsspeicher teilen, oder<br />

• gemeinsamer Speicher ist nicht verfügbar, es werden externe Medien,<br />

wie Dateien verwendet, auf die gemeinsam zugegriffen werden kann.<br />

Die Probleme sind aber unabhängig davon, welcher der beiden<br />

Mechanismen verwendet wird. Im folgenden werden die Probleme und<br />

Lösungen für den Zugriff auf gemeinsam verwendete Ressourcen gezeigt.<br />

39 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Probleme des gemeinsamen Zugriffs<br />

Probleme des gemeinsamen Zugriffs<br />

Wenn mehrere Threads in Java gemeinsam auf Daten zugreifen, müssen<br />

sich die einzelnen Threads ”<br />

verständigen“, wer wann was machen darf.<br />

Dazu werden die Möglichkeiten, die Java bietet, am Beispiel von<br />

Buchungen eines Bankkontos gezeigt.<br />

Eine Bank wird modelliert durch 4 Klassen:<br />

Die Klasse Konto repräsentiert ein Konto mit dem Attribut kontostand<br />

und den Methoden zum setzen und abfragen des aktuellen Kontostandes.<br />

BankOperation.java<br />

1 class Konto {<br />

2 private float kontostand ;<br />

3 public void setzen ( float betrag ) {<br />

4 kontostand = betrag ;<br />

5 }<br />

6 public float abfragen () {<br />

7 return kontostand ;<br />

8 }<br />

9 }<br />

40 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Probleme des gemeinsamen Zugriffs<br />

Bank<br />

Die Klasse Bank hat als Attribut ein Feld von Referenzen auf<br />

Konto-Objekte (Konten). Die Methode buchen implementiert einen<br />

Buchungsvorgang auf ein Konto.<br />

1 class Bank {<br />

2 private Konto [] konten ;<br />

4 public Bank () {<br />

5 konten = new Konto [100];<br />

6 for ( int i = 0; i < konten . length ; i ++) {<br />

7 konten [i] = new Konto ();<br />

8 }<br />

10 public void buchen ( int kontonr , float betrag ) {<br />

11 float alterStand = konten [ kontonr ]. abfragen ();<br />

12 float neuerStand = alterStand + betrag ;<br />

13 konten [ kontonr ]. setzen ( neuerStand );<br />

14 }<br />

15 }<br />

41 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Probleme des gemeinsamen Zugriffs<br />

Bankangestellte<br />

Die Klasse Bankangestellte ist aus der Klasse Thread abgeleitet. Der<br />

Name der Angestellten wird der Name des Thread, da mehrere<br />

Bankangestellte (Threads) für die Bank arbeiten können sollen.<br />

Buchungen werden in der run-Methode durch Zufallszahlen erzeugt.<br />

1 class BankAngestellte extends Thread {<br />

2 private Bank bank ;<br />

3 public BankAngestellte ( String name , Bank bank ) {<br />

4 super ( name ); this . bank = bank ;<br />

5 start ();<br />

6 }<br />

8 public void run () {<br />

9 for ( int i = 0; i < 10000; i ++) {<br />

10 /* Kontonummer einlesen ; simuliert durch Wahl einer<br />

11 Zufallszahl zwischen 0 und 99 */<br />

12 int kontonr = ( int )( Math . random ()*100);<br />

13 /* Überweisungsbetrag einlesen ; simuliert durch Wahl einer<br />

14 Zufallszahl zwischen -500 und +499 */<br />

15 float betrag = ( int )( Math . random ()*1000) - 500;<br />

16 bank . buchen ( kontonr , betrag );<br />

17 }<br />

18 }<br />

19 }<br />

42 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Probleme des gemeinsamen Zugriffs<br />

Bankbetrieb<br />

In der Klasse Bankbetrieb wird eine Bank mit 2 Bankangestellten<br />

simuliert.<br />

Die Bankangestellten fangen an zu Arbeiten, wenn die Objekte erzeugt<br />

werden (durch new und dann die Methode start im Konstruktor von<br />

Bankangestellte).<br />

1 public class Bankbetrieb {<br />

2 public static void main ( String [] args ) {<br />

3 Bank sparkasse = new Bank ();<br />

4 BankAngestellte müller =<br />

5 new BankAngestellte (" Andrea Müller ", sparkasse ); ←<br />

6 BankAngestellte schmitt =<br />

7 new BankAngestellte (" Petra Schmitt ", sparkasse ); ←<br />

8 }<br />

9 }<br />

sparkasse ist das gemeinsam verwendete Objekt !<br />

43 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Probleme des gemeinsamen Zugriffs<br />

verlorene Buchungen<br />

Bei dieser Bank können aber Buchungen verloren gehen!<br />

→ am Rechner<br />

Jede der 2 Bankangestellten bebucht das<br />

• Konto 1<br />

• jeweils 1000 mal mit<br />

• jeweils 1 EUR<br />

Also muss das Konto 1 am Ende des Tages einen Kontostand von 2000<br />

EUR aufweisen !<br />

Dieses Problem tritt immer auf, wenn mehrere Threads auf ein<br />

gemeinsam nutzbares Objekt (hier das Feld Konto der Klasse Bank)<br />

zugreifen und es keine Schutzmechanismen gibt.<br />

44 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Probleme des gemeinsamen Zugriffs<br />

Verdeutlichung<br />

Gehen wir von folgender Situation aus:<br />

• Andrea Müller bucht -100 EUR auf Konto 47, es sollen also 100<br />

EUR abgebucht werden. Der Kontostand sein 0.<br />

• Petra Schmitt führt auch Buchungen für Konto 47 aus, es sollen<br />

1000 EUR gutgeschrieben werden.<br />

Thread Andrea Müller:<br />

1 public void buchen ( int kontonr =47 , float betrag = -100) { Konten[47]=0<br />

2 float alterStand = konten [ kontonr ]. abfragen (); alterStand=0<br />

3 -> Umschalten auf Thread Petra Schmitt<br />

4 float neuerStand = alterStand + betrag ;<br />

5 konten [ kontonr ]. setzen ( neuerStand );<br />

6 }<br />

Thread Petra Schmitt:<br />

1 public void buchen ( int kontonr =47 , float betrag =1000) { Konten[47]=0<br />

2 float alterStand = konten [ kontonr ]. abfragen (); alterStand=0<br />

3 float neuerStand = alterStand + betrag ; neuerStand=1000<br />

4 konten [ kontonr ]. setzen ( neuerStand ); Konten[47]=1000<br />

5 }<br />

6 -> Umschalten auf Thread Andrea Müller 45 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Probleme des gemeinsamen Zugriffs<br />

Verdeutlichung<br />

Thread Andrea Müller:<br />

1 public void buchen ( int kontonr =47 , float betrag = -100) { Konten[47]=0<br />

2 float alterStand = konten [ kontonr ]. abfragen (); alterStand=0<br />

3 -> Weiter nach Unterbrechung<br />

4 float neuerStand = alterStand + betrag ; neuerStand=-100<br />

5 konten [ kontonr ]. setzen ( neuerStand ); Konten[47]=-100<br />

6 }<br />

Nun ist die Gutschrift, die Petra Schmitt gebucht hat verloren! Der<br />

aktuelle Kontostand ist -100 anstatt +900.<br />

Die Ursache des Problems ist, dass eine Buchung aus mehreren<br />

Java Anweisungen besteht und zwischen diesen Anweisungen<br />

zwischen Threads umgeschaltet werden kann.<br />

46 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Kritischer Bereich<br />

Kritischer Bereich<br />

Das gezeigte Problem der verlorenen Buchungen kann auch abstrakt<br />

formuliert werden.<br />

Der Teil eines Programms, in dem auf gemeinsame Ressourcen<br />

zugegriffen wird, wird kritischer Bereich genannt.<br />

Zeitkritische Abläufe (wie das Buchen des selben Kontos)<br />

können vermieden werden, wenn sich zu keinem Zeitpunkt zwei<br />

<strong>Prozesse</strong> in ihrem kritischen Bereich befinden.<br />

Im folgenden werden Lösungsversuche (nicht korrekte) diskutiert.<br />

47 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Kritischer Bereich<br />

Lösungsversuch 1 (nur eine Anweisung)<br />

In der Klasse Bank wird das Buchen durch eine Java Anweisung realisiert:<br />

1 class Konto {<br />

2 private float kontostand ;<br />

3 public void buchen ( float betrag ) {<br />

4 kontostand += betrag ; ← nur eine Anweisung<br />

5 } also kein Umschalten möglich<br />

6 }<br />

8 class Bank {<br />

9 private Konto [] konten ;<br />

11 public Bank () {<br />

12 konten = new Konto [100];<br />

13 for ( int i = 0; i < konten . length ; i ++) {<br />

14 konten [i] = new Konto ();<br />

15 }<br />

17 public void buchen ( int kontonr , float betrag ) {<br />

18 konten [ kontonr ]. buchen ( betrag ); ←<br />

19 }<br />

20 }<br />

Wie beurteilen Sie den Ansatz ?<br />

48 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Kritischer Bereich<br />

Lösungsversuch 1<br />

Dies ist keine Lösung, denn Java-Programme werden in Bytekode<br />

übersetzt.<br />

Dabei wird aus der einer Java-Anweisung<br />

1 kontostand += betrag ;<br />

schematisch etwa folgender Code:<br />

1 LOAD ( kontostand );<br />

2 ADD ( betrag );<br />

3 STORE ( kontostand );<br />

Die JVM führt also 3 Anweisungen aus, wobei dann auch wieder<br />

dazwischen umgeschaltet werden kann.<br />

49 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Kritischer Bereich<br />

Lösungsversuch 2 (Sperrvariable)<br />

Die Idee ist die Verwendung von Sperrvariablen:<br />

• Zu einem Zeitpunkt darf nur eine Angestellte eine Buchung<br />

ausführen.<br />

• Erst wenn die Buchung abgeschlossen ist, darf eine andere<br />

Angestellte buchen.<br />

Dies ist offensichtlich eine korrekte Lösung: der kritische Bereich wird zu<br />

einem Zeitpunkt nur einmal betreten.<br />

Ein Implementierungsversuch ist: Alle Klassen entsprechen den des<br />

Ausgangsbeispiels; nur die Klasse Bank wird wie folgt geändert.<br />

50 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Kritischer Bereich<br />

1 class Bank {<br />

2 private Konto [] konten ;<br />

3 private boolean gesperrt ; ←<br />

5 public Bank () {<br />

6 konten = new Konto [100];<br />

7 for ( int i = 0; i < konten . length ; i ++) {<br />

8 konten [i] = new Konto ();<br />

9 }<br />

10 gesperrt = false ; ←<br />

11 }<br />

13 public void buchen ( int kontonr , float betrag ) {<br />

14 while ( gesperrt ); ←<br />

15 gesperrt = true ; ←<br />

16 float alterStand = konten [ kontonr ]. abfragen ();<br />

17 float neuerStand = alterStand + betrag ;<br />

18 konten [ kontonr ]. setzen ( neuerStand );<br />

19 gesperrt = false ; ←<br />

20 }<br />

21 }<br />

Die 3 Anweisungen zum Buchen können nur ausgeführt werden, falls die<br />

Bank momentan nicht gesperrt ist. Ist die Bank gesperrt, wartet der<br />

Thread, bis sie durch den sperrenden Thread wieder entsperrt wird.<br />

51 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Kritischer Bereich<br />

Diese Realisierung ist auf den ersten Blick korrekt; es gibt aber folgende<br />

Probleme:<br />

1 Paralleles Arbeiten wird unmöglich: parallel könnten<br />

unterschiedlichen Konten bebucht werden – dies ist hier<br />

ausgeschlossen.<br />

2 Durch das aktive Warten (while (gesperrt) ;) wird kostbare<br />

Rechenzeit aufgewendet, um nichts zu tun.<br />

3 Die Realisierung ist nicht korrekt. Das aktive Warten ist keine<br />

unteilbare Aktion, denn der Bytekode sieht etwa wie folgt aus:<br />

while ( gesperrt ); 1: LOAD ( gesperrt );<br />

2: JUMPTRUE 1;<br />

gesperrt = true ; 3: LOADNUM ( TRUE );<br />

4: STORE ( geperrt )<br />

Wenn hierbei zwischen Befehl 1 und 2 umgeschaltet wird und die<br />

Sperre frei ist (geperrt=false) so kann ein wartender Thread buchen.<br />

Um eine korrekte Lösung in Java zu programmieren, sind in Java<br />

Sprachelemente zur Synchronisation verfügbar.<br />

52 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Lösungsansätze in Java<br />

Die erste korrekte (aber ineffiziente) Lösung für die Probleme 2 (aktives<br />

Warten) und 3 (verlorene Buchungen) nutzt die Möglichkeit von Java,<br />

Methoden als synchronized zu kennzeichnen.<br />

Dazu wird einfach die Klasse buchen mit dem Attribut synchronized<br />

versehen – ansonsten ändert sich nichts.<br />

1 class Bank {<br />

2 private Konto [] konten ;<br />

4 public Bank () {<br />

5 konten = new Konto [100];<br />

6 for ( int i = 0; i < konten . length ; i ++) {<br />

7 konten [i] = new Konto ();<br />

8 }<br />

10 public synchronized void buchen ( int kontonr , float betrag ) {<br />

11 float alterStand = konten [ kontonr ]. abfragen ();<br />

12 float neuerStand = alterStand + betrag ;<br />

13 konten [ kontonr ]. setzen ( neuerStand );<br />

14 }<br />

15 }<br />

53 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

synchronized<br />

Die Java-Superklasse Object beinhaltet als Eigenschaft eine Sperre. Da<br />

jede Klasse von Object abgeleitet ist, besitzen alle Klassen diese<br />

Eigenschaft.<br />

Das Sperren gehorcht dem ”<br />

acquire-release Protokoll“:<br />

Die Sperre wird gesetzt (acquire), beim Betreten einer<br />

synchronized-Methode (oder synchronized Blocks) und entfernt<br />

(release) bei Verlassen des Blocks (auch bei Verlassen durch<br />

einee Exception).<br />

54 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

acquire release Protokoll<br />

• Wird eine Methode einer Klasse mit<br />

synchronized gekennzeichnet, so muss diese<br />

Sperre zuerst gesetzt werden, bevor die<br />

Methode ausgeführt wird, hier versucht von<br />

Thread B.<br />

• Hat ein anderer Thread A die Sperre bereits<br />

gesetzt (seine Methode ist in Ausführung), so<br />

wird der aufrufende Thread B blockiert.<br />

• Das Blockieren ist aber nicht durch aktives<br />

Warten realisiert, sondern der Thread B wird<br />

beim Thread-Umschalten nicht mehr<br />

berücksichtigt.<br />

• Wenn die Methode des Thread A beendet ist,<br />

wird die Sperre entfernt und der Thread B<br />

wird wieder beim Scheduling wieder<br />

berücksichtigt.<br />

Tread A<br />

Sperre<br />

Object<br />

Thread B<br />

55 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Somit ist das Problem 2 und 3 gelöst. Diese Lösung ist aber ineffizient<br />

(Problem 1), da die ganze Bank gesperrt wird, obwohl doch nur<br />

Probleme auftauchen, wenn auf das selbe Konto gebucht wird.<br />

Dies wird im Beispielprogramm umgesetzt,indem die Java-Eigenschaft,<br />

Blöcke als synchronized zu kennzeichnen, verwendet wird.<br />

1 class Bank {<br />

2 ...<br />

4 public void buchen ( int kontonr , float betrag ) {<br />

5 synchronized(konten[kontonr]) {<br />

6 float alterStand = konten [ kontonr ]. abfragen ();<br />

7 float neuerStand = alterStand + betrag ;<br />

8 konten [ kontonr ]. setzen ( neuerStand );<br />

9 }<br />

10 }<br />

11 }<br />

Ein Block (...) wird dabei mit dem Schlüsselwort synchronized versehen<br />

und das Objekt, auf das sich die Sperre bezieht wird danach angegeben.<br />

56 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Verwendung von synchronized<br />

Wenn alle Threads nur lesend auf ein Objekt zugreifen, ist keine<br />

Synchronisation erforderlich – hier würde durch synchronized ein<br />

unnötiger Rechenaufwand erzeugt werden (Setzen und Lösen der Sperre).<br />

Generell gilt folgende Regel:<br />

Wenn von mehreren Threads auf ein Objekt zugegriffen<br />

wird, wobei mindestens ein Thread den Zustand<br />

(repräsentiert durch die Werte der Attribute) des Objekts<br />

ändert, dann müssen alle Methoden, die auf den Zustand<br />

lesend oder schreibend zugreifen, mit synchronized<br />

gekennzeichnet werden.<br />

57 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Petrinetz für synchronized<br />

Betrachten wir folgendes Beispiel, ein Programm, bei dem alle Threads<br />

auf demselben Objekt arbeiten:<br />

1 class K {<br />

2 public synchronized void m1 () { action1 (); }<br />

3 public synchronized void m2 () { action2 (); }<br />

4 private void action1 () { ... }<br />

5 private void action2 () { ... }<br />

6 }<br />

8 class T extends Thread {<br />

9 private K einK ;<br />

10 public T(K k) {<br />

11 einK = k;<br />

12 }<br />

13 public void run () {<br />

14 while (...) {<br />

15 switch (...) {<br />

16 case ...: einK .m1 (); break ;<br />

17 case ...: einK .m2 (); break ;<br />

18 }<br />

19 }<br />

20 }<br />

21 }<br />

→ petrinet<br />

58 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Petrinetz für synchronized<br />

• Die Stellen des Petri-Netzes entsprechen den<br />

Stellen zwischen den Anweisungen im<br />

Programmtext.<br />

• Die Transitionen sind die Anweisungen und die<br />

Marken sind die Threads bzw. die Sperre des<br />

gemeinsam benutzten K-Objektes, die durch<br />

synchronized belegt wird.<br />

• Zum Start eines Thread muss sowohl eine<br />

Marke auf ”<br />

start“ als auch auf ”<br />

lock“ liegen.<br />

• Schaltet eine Transition, so wird die Marke<br />

von lock“ entfernt und erst wieder auf sie<br />

”<br />

gelegt, wenn die synchronized Methode<br />

syncEnd“ erreicht hat.<br />

”<br />

• Damit kann zu einem Zeitpunkt höchstens ein<br />

Thread eine der synchronized Methoden auf<br />

ein Objekt anwenden.<br />

59 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Test, ob eine Sperre gesetzt ist<br />

Der Test, ob eine Sperre gesetzt ist, ist durch holdsLock möglich:<br />

Main.java<br />

1 public class Main {<br />

2 public static void main ( String [] argv ) throws Exception {<br />

3 Object o = new Object ();<br />

5 System . out . println ( Thread . holdsLock (o )); ←<br />

6 synchronized (o) {<br />

7 System . out . println ( Thread . holdsLock (o )); ←<br />

8 }<br />

9 }<br />

10 }<br />

60 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Hörsaalübung<br />

Welche Ausgabe erzeugt das Programm? Test.java<br />

1 class Even {<br />

2 private int n = 0;<br />

3 public int next () { // POST : next is always even ←<br />

4 ++n;<br />

5 try { Thread . sleep (( long )( Math . random ()*10));<br />

6 } catch ( InterruptedException e) { }<br />

7 ++n;<br />

8 return n;<br />

9 }<br />

10 }<br />

11 public class Test extends Thread {<br />

12 private Even e;<br />

13 public Test ( Even e) { this .e = e;}<br />

14 public void run () {<br />

15 for ( int i = 1 ; i


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

→ Hörsaalübung<br />

Welche Ausgabe erzeugt das Programm? Test1.java<br />

1 class Even {<br />

2 ...<br />

3 }<br />

4 public class Test1 extends Thread {<br />

5 private Even e;<br />

6 public Test1 ( Even e) {<br />

7 this .e = e;<br />

8 }<br />

9 public void run () {<br />

10 for ( int i = 1 ; i


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

wait und notify<br />

Werden Objekte von mehreren Threads benutzt und dabei deren<br />

Zustände verändert, dann gewährleisten synchronized-Methoden, dass ein<br />

Thread immer einen konsistenten Zustand eines Objektes vorfindet.<br />

In vielen Anwendungssituationen ist es erforderlich, dass eine Methode<br />

nur dann ausgeführt wird,<br />

• wenn zusätzlich zum konsistenten Zustand<br />

• weitere anwendungsspezifische Bedingungen erfüllt sind.<br />

63 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Beispiel Parkhaus<br />

Ein Parkhaus wird modelliert, indem der Zustand durch die aktuell noch<br />

freie Parkplätze beschrieben wird und Autos durch Threads mit<br />

Methoden Einfahren (enter()) und Ausfahren (leave()) realisiert sind.<br />

• Beim Einfahren vermindert sich die Anzahl der freien Plätze um 1;<br />

beim Ausfahren eines Autos wird sie um 1 erhöht.<br />

• Die freien Plätze können nicht erhöht werden, wenn das Parkhaus<br />

voll ist (freie Plätze == 0).<br />

• Da ein Parkhaus von mehreren Autos (Threads) benutzt wird und<br />

der Zustand (=freie Plätze) durch ein Auto verändert wird, müssen<br />

die Methoden enter() und leave() synchronized sein.<br />

Zunächst werden zwei vergebliche Versuche, das Problem ”<br />

freie Plätze“<br />

zu lösen gezeigt, dann eine korrekte Implementierung.<br />

64 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

unkorrekter Versuch<br />

ParkingGarageOperation.java<br />

1 class ParkingGarage {<br />

2 private int places ;<br />

3 public ParkingGarage ( int places ) {<br />

4 if ( places < 0) places = 0; this . places = places ;<br />

5 }<br />

6 public synchronized void enter () { // enter parking garage<br />

7 while ( places == 0) ; ← actives Warten<br />

8 places - -;<br />

9 }<br />

10 public synchronized void leave () { // leave parking garage<br />

11 places ++;<br />

12 }<br />

13 }<br />

Diese Lösung hat zwei Probleme:<br />

1 Aktives Warten führt zu schlechter Performance.<br />

65 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

unkorrekter Versuch<br />

ParkingGarageOperation.java<br />

1 class ParkingGarage {<br />

2 private int places ;<br />

3 public ParkingGarage ( int places ) {<br />

4 if ( places < 0) places = 0; this . places = places ;<br />

5 }<br />

6 public synchronized void enter () { // enter parking garage<br />

7 while ( places == 0) ; ← actives Warten<br />

8 places - -;<br />

9 }<br />

10 public synchronized void leave () { // leave parking garage<br />

11 places ++;<br />

12 }<br />

13 }<br />

Diese Lösung hat zwei Probleme:<br />

1 Aktives Warten führt zu schlechter Performance.<br />

2 Die Lösung arbeitet nicht korrekt, wenn ein Parkhaus einmal voll<br />

geworden ist. Wenn ein Auto in ein volles Parkhaus einfahren will (Aufruf<br />

Methode enter), dann kann das Parkhaus nicht mehr benutzt werden, weil<br />

der Thread wegen synchronized und der while-Schleife die Sperre nie mehr<br />

freigibt, da kein anderes Auto ausfahren kann (wegen der Sperre).<br />

65 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

unkorrekter Versuch<br />

ParkingGarageOperation.java<br />

1 class ParkingGarage {<br />

2 private int places ;<br />

3 public ParkingGarage ( int places ) {<br />

4 if ( places < 0) places = 0; this . places = places ;<br />

5 }<br />

6 public synchronized void enter () { // enter parking garage<br />

7 while ( places == 0) ; ← actives Warten<br />

8 places - -;<br />

9 }<br />

10 public synchronized void leave () { // leave parking garage<br />

11 places ++;<br />

12 }<br />

13 }<br />

Diese Lösung hat zwei Probleme:<br />

1 Aktives Warten führt zu schlechter Performance.<br />

2 Die Lösung arbeitet nicht korrekt, wenn ein Parkhaus einmal voll<br />

geworden ist. Wenn ein Auto in ein volles Parkhaus einfahren will (Aufruf<br />

Methode enter), dann kann das Parkhaus nicht mehr benutzt werden, weil<br />

der Thread wegen synchronized und der while-Schleife die Sperre nie mehr<br />

freigibt, da kein anderes Auto ausfahren kann (wegen der Sperre).<br />

65 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Korrekte Lösung ohne aktives Warten<br />

Das aktive Warten auf einen freien Platz ist weder im gesperrten Zustand<br />

des Parkhaus-Objektes noch im nicht gesperrten Zustand korrekt.<br />

Durch die Methoden wait() und notify() der Klasse Objekt kann das<br />

Problem gelöst werden.<br />

public class Object {<br />

...<br />

public final void wait () throws InterruptedException {...}<br />

public final void notify () { ...}<br />

}<br />

Diese beiden Methoden müssen auf ein Objekt angewendet werden, das<br />

durch synchronized gesperrt ist, ansonsten tritt die Ausnahme<br />

” IllegalMonitorStateException“ auf. 66 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

wait, notify, notifyAll<br />

Ein wait bewirkt die folgenden Aktionen:<br />

1 Der laufende Thread blockiert.<br />

2 Wenn der laufende Thread unterbrochen wurde, wird die Ausnahme<br />

InterruptedException erzeugt.<br />

3 Die JVM fügt den laufenden Thread in eine Menge (wait set) ein,<br />

die mit dem Objekt assoziiert ist.<br />

4 Der synchronization Lock für das Objekt wird freigegeben (released),<br />

alle anderen Locks bleiben erhalten.<br />

67 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

wait, notify, notifyAll<br />

Ein notify bewirkt die folgenden Aktionen:<br />

1 Ein zufälliger Thread t wird aus dem Waitingset des Objektes<br />

ausgewählt.<br />

2 t muss den Lock des Objektes wieder erhalten, d.h. Er blockiert<br />

solange, bis der Thread der notify aufgerufen hat, den Lock besitzt<br />

oder bis ein anderer Thread, der den Lock hält, ihn freigegeben hat.<br />

3 t wird nach erhalten des Lock nach seinem wait weitermachen.<br />

Ein notifyAll arbeitet genauso, nur dass alle Threads im Waitingset<br />

ausgewählt werden (Achtung: nur einer kann aber weitermachen, da die<br />

anderen ja auf den Erhalt des Lock warten. sind.<br />

68 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

wait, notify, notifyAll<br />

69 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

wait, notify, notifyAll<br />

70 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

wait, notify, notifyAll<br />

71 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

wait, notify, notifyAll<br />

72 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

wait, notify, notifyAll<br />

73 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Parkhaus mit korrekter Lösung<br />

ParkingGarageOperation.java<br />

1 class ParkingGarage {<br />

2 private int places ;<br />

4 public ParkingGarage ( int places ) {<br />

5 if ( places < 0)<br />

6 places = 0;<br />

7 this . places = places ;<br />

8 }<br />

10 public synchronized void enter () { // enter parking garage<br />

11 while ( places == 0) {<br />

12 try {<br />

13 wait (); ←<br />

14 } catch ( InterruptedException e) {}<br />

15 }<br />

16 places - -;<br />

17 }<br />

19 public synchronized void leave () { // leave parking garage<br />

20 places ++;<br />

21 notify (); ←<br />

22 }<br />

23 }<br />

74 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Parkhaus mit korrekter Lösung<br />

1 class Car extends Thread {<br />

2 private ParkingGarage parkingGarage ;<br />

4 public Car ( String name , ParkingGarage p) {<br />

5 super ( name );<br />

6 this . parkingGarage = p;<br />

7 start ();<br />

8 }<br />

10 public void run () {<br />

11 while ( true ) {<br />

12 try {<br />

13 sleep (( int )( Math . random () * 10000)); // drive before parking<br />

14 } catch ( InterruptedException e) {}<br />

15 parkingGarage . enter ();<br />

16 System . out . println ( getName ()+ ": entered ");<br />

17 try {<br />

18 sleep (( int )( Math . random () * 20000)); // stay into the garage<br />

19 } catch ( InterruptedException e) {}<br />

20 parkingGarage . leave ();<br />

21 System . out . println ( getName ()+ ": left ");<br />

22 }<br />

23 }<br />

24 }<br />

75 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Parkhaus mit korrekter Lösung<br />

1 public class ParkingGarageOperation {<br />

2 public static void main ( String [] args ){<br />

3 ParkingGarage parkingGarage = new ParkingGarage (10);<br />

4 for ( int i =1; i


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Frage<br />

1 class ParkingGarage {<br />

2 ...<br />

4 public synchronized void enter () { // enter parking garage<br />

5 while ( places == 0) { ← if anstelle von while<br />

6 try {<br />

7 wait ();<br />

8 } catch ( InterruptedException e) {}<br />

9 }<br />

10 places - -;<br />

11 }<br />

13 public synchronized void leave () { // leave parking garage<br />

14 places ++;<br />

15 notify (); }<br />

16 }<br />

Was passiert, wenn man in der Methode enter(), die while-Schleife durch<br />

ein if ersetzt?<br />

Die Lösung müsste immer noch korrekt sein, da der Thread doch erst<br />

geweckt wird, wenn ein Platz frei wurde und somit kann er in das<br />

Parkhaus einfahren. Ist die Argumentation korrekt?<br />

77 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Antwort<br />

Nein, denn obwohl er geweckt wird, wenn ein Platz frei geworden ist,<br />

heißt das ja nicht, dass er auch vom Scheduler ausgewählt wird. Es<br />

könnte ein anderer Thread, der gerade einfahren will ausgewählt werden,<br />

der dann den freien Platz belegt.<br />

Dies entspricht in der Realität dem Tatbestand, dass ein Auto vor dem<br />

Einlass steht und von einem anderen dahinter stehenden Wartenden<br />

überholt wird, der dann in die letzte Parklücke einfährt.<br />

78 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Modellierung von wait- und notify durch Petri-Netze<br />

class K {<br />

public synchronized void m1 () {<br />

while (...)<br />

wait (); ←<br />

action1 (); }<br />

public synchronized void m2 () {<br />

action2 ();<br />

notify (); ←<br />

}<br />

private void action1 () { ... }<br />

private void action2 () { ... }<br />

}<br />

class T extends Thread {<br />

private K einK ;<br />

public T(K k) {<br />

einK = k;<br />

}<br />

public void run () {<br />

while (...)<br />

switch (...) {<br />

case ...: einK .m1 (); break ;<br />

case ...: einK .m2 (); break ;<br />

}<br />

}<br />

}<br />

79 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Die Stelle checkCond1“ entspricht der Überprüfung der Wartebedingung. Der<br />

”<br />

Aufruf von wait() wird durch die Transition wait1“ modelliert. Die Anzahl der<br />

”<br />

Marken in der Stelle waiting1“ entspricht der Anzahl der wartenden Threads.<br />

”<br />

Das Schalten der Transition wait1“ bewirkt, dass eine Marke auf die Stelle<br />

”<br />

lock“ gelegt wird, dies entspricht der Freigabe der Sperre beim Aufruf von<br />

”<br />

wait() – dadurch können weitere Threads die Methode m1“ aufrufen.<br />

”<br />

Irgendwann wird ein Thread die Methode m2“ aufrufen. Dann wird nach<br />

”<br />

Schalten der Transition action2“ eine Marke auf der Stelle afterAction2“<br />

” ”<br />

liegen. Nun folgt notify(): die Transition notify2“ kann auf jeden Fall schalten.<br />

”<br />

Falls sich mindestens eine Marke in der Stelle waiting1“ befindet, kann<br />

”<br />

zusätzlich die Transition wakeup1“ schalten. Dadurch entsteht zwischen<br />

”<br />

” wakeup1“ und notify2“ ein Konflikt. Durch Zuweisen einer höheren Priorität<br />

”<br />

an wakeup1“ erreicht man, dass nur die Transition wackup1“ schaltet. Die<br />

” ”<br />

Transition notify2“ soll nämlich nur Schal- ten, wenn sich keine Marke auf<br />

”<br />

waiting1“ befindet (dies ist die Semantik non notify(): notify() weckt genau<br />

”<br />

einen Thread, wenn es einen solchen gibt; ansonsten ist notify() wirkungslos).<br />

80 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

wait() und notifyAll()<br />

Probleme mit wait() und notify() entstehen, wenn mehrere Threads in<br />

der Warteschlange stehen und der falsche Thread geweckt wird.<br />

Dies wird am Erzeuger-Verbraucher Problem demonstriert.<br />

• Ein Erzeuger (producer) will Werte an<br />

einen Verbraucher (consumer) senden.<br />

• Erzeuger und Verbraucher sind Threads.<br />

• Die Werte, die ausgetauscht werden, sind<br />

in einem Puffer (implementiert durch<br />

Integervariable) abgelegt, auf die beide<br />

Threads Zugriff haben.<br />

• Der Erzeuger verwendet die Methode<br />

put(), um einen Wert in den Puffer zu<br />

schreiben;<br />

• der Verbraucher kann einen Wert aus dem<br />

Puffer durch die Methode get() lesen.<br />

81 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Erzeuger-Verbraucher Problem<br />

• Bei der Realisierung darf es nicht<br />

vorkommen, dass ein Wert verloren geht,<br />

etwa weil der Erzeuger einen neuen Wert<br />

in den Puffer schreibt, bevor der<br />

Verbraucher den alten Wert gelesen hat.<br />

• Weiterhin darf ein Wert nicht mehrmals<br />

gelesen werden, etwa wenn der<br />

Verbraucher erneut liest, bevor der<br />

Erzeuger einen neuen Wert geschrieben<br />

hat.<br />

• Diese Problematik soll durch Warten<br />

realisiert werden:<br />

• der Erzeuger wartet mit dem Schreiben,<br />

bis der Verbraucher den Wert gelesen<br />

hat;<br />

• der Verbraucher wartet, bis ein neuer<br />

Wert in den Puffer geschrieben ist.<br />

82 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Unkorrekte Lösung<br />

Der Puffer wird durch die Klasse Buffer mit<br />

• den Attributen<br />

• data (Werte) und<br />

• available (Flag zeigt an, ob Daten bereit stehen) und den<br />

• Methoden<br />

• put() und<br />

• get()<br />

realisiert.<br />

83 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

1 class Buffer {<br />

2 private boolean available = false ;<br />

3 private int data ;<br />

5 public synchronized void put ( int x) {<br />

6 while ( available ) {<br />

7 try {<br />

8 wait (); ←<br />

9 } catch ( InterruptedException e) {}<br />

10 }<br />

11 data = x;<br />

12 available = true ;<br />

13 notify (); ←<br />

14 }<br />

16 public synchronized int get () {<br />

17 while (! available ) {<br />

18 try {<br />

19 wait (); ←<br />

20 } catch ( InterruptedException e) {}<br />

21 }<br />

22 available = false ;<br />

23 notify (); ←<br />

24 return data ;<br />

25 }<br />

26 }<br />

84 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Erzeuger und Verbraucher werden als Threads modelliert, wobei im<br />

Konstruktor eine Referenz auf das gemeinsam benutzte Objekt (buffer)<br />

übergeben wird.<br />

Der Erzeuger produziert 100 Werte, der Verbraucher konsumiert 100<br />

Werte.<br />

1 class Producer extends Thread {<br />

2 private Buffer buffer ;<br />

3 private int start ;<br />

5 public Producer ( Buffer b, int s) {<br />

6 buffer = b;<br />

7 start = s;<br />

8 }<br />

10 public void run () {<br />

11 for ( int i = start ; i < start + 100; i ++) {<br />

12 buffer . put (i);<br />

13 }<br />

14 }<br />

15 }<br />

85 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

1 class Consumer extends Thread {<br />

2 private Buffer buffer ;<br />

4 public Consumer ( Buffer b) {<br />

5 buffer = b;<br />

6 }<br />

8 public void run () {<br />

9 for ( int i = 0; i < 100; i ++) {<br />

10 int x = buffer . get ();<br />

11 System . out . println (" got " + x);<br />

12 }<br />

13 }<br />

14 }<br />

86 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

In der Klasse ProduceConsume werden ein Puffer, ein Erzeuger und ein<br />

Verbraucher erzeugt und die beiden Threads gestartet.<br />

1 public class ProduceConsume {<br />

2 public static void main ( String [] args ) {<br />

3 Buffer b = new Buffer ();<br />

4 Consumer c = new Consumer (b);<br />

5 Producer p = new Producer (b, 1);<br />

6 c. start ();<br />

7 p. start ();<br />

8 }<br />

9 }<br />

Ein Aufruf des Programms bewirkt also:<br />

$ java ProduceConsume<br />

got 1<br />

got 2<br />

got 3<br />

got 4<br />

got 5<br />

...<br />

got 100<br />

$<br />

87 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Normalerweise hat man die Situation, dass mehrere Erzeuger und<br />

mehrere Verbraucher parallel arbeiten:<br />

ProduceConsume2.java<br />

1 public class ProduceConsume2 {<br />

2 public static void main ( String [] args ) {<br />

3 Buffer b = new Buffer ();<br />

4 Consumer c1 = new Consumer (b);<br />

5 Consumer c2 = new Consumer (b);<br />

6 Consumer c3 = new Consumer (b);<br />

7 Producer p1 = new Producer (b, 1);<br />

8 Producer p2 = new Producer (b, 101);<br />

9 Producer p3 = new Producer (b, 201);<br />

10 c1. start ();<br />

11 c2. start ();<br />

12 c3. start ();<br />

13 p1. start ();<br />

14 p2. start ();<br />

15 p3. start ();<br />

16 }<br />

17 }<br />

88 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Ein Lauf des Programms kann folgende Ausgabe bewirken (das hängt<br />

von Betriebssystem und Java-Version ab):<br />

$ java ProduceConsume2<br />

got 1<br />

got 101<br />

got 2<br />

got 102<br />

got 103<br />

got 201<br />

got 3<br />

got 104<br />

got 202<br />

...<br />

got 230<br />

got 231<br />

got 33<br />

got 8<br />

got 232<br />

D.h. das Programm bleibt stehen, es passiert nichts mehr; es wird keine<br />

neue Ausgabe mehr erzeugt, das Programm ist aber noch nicht beendet.<br />

Dieses Verhalten wurde verursacht, da durch notify() der ”<br />

falsche“<br />

Thread geweckt wurde.<br />

89 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Erklärung<br />

1 Alle Verbraucher c1, c2 und c3 können laufen. Da der Puffer<br />

anfänglich leer ist, werden alle 3 Threads durch wait() innerhalb<br />

get() blockiert und in die Warteschlange des Objektes b<br />

aufgenommen.<br />

2 Nun startet der Thread p1, der eine Wert in den Puffer b ablegt und<br />

einen Verbraucher weckt, nehmen wir an c1. Nun hat die<br />

Warteschlange und der Puffer folgendes Aussehen:<br />

90 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Erklärung<br />

3 p1 läuft weiter und will einen Wert in den Puffer schreiben. Da der<br />

Puffer voll ist, blockiert er und wird in die Warteschlange eingereiht:<br />

4 Nehmen wir an, es wird auf den Erzeuger p2 umgeschaltet. Da der<br />

Puffer immer noch voll ist, wird auch p2 blockiert. Dies wiederholt<br />

sich für p3: Warteschlange für Objekt b<br />

91 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Erklärung<br />

5 Der einzige Thread, auf den nun noch umgeschaltet werden kann, ist<br />

der vorher geweckte Thread c1. Der nimmt nun einen Wert aus dem<br />

Puffer. Dann wird einer der Threads in der Warteschlange<br />

geweckt, gehen wir von c2 aus.<br />

6 c1 arbeitet weiter und will einen Wert aus dem Puffer nehmen; der<br />

Puffer ist leer, also wird c1 blockiert und es steht noch immer kein<br />

Wert im Puffer:<br />

92 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Erklärung<br />

7 Der Thread, auf den als einziges umgeschaltet werden kann, ist der<br />

Verbraucher c2. Er ver- sucht einen Wert zu lesen, der Puffer ist<br />

leer, also blockiert er:<br />

Da nun alle Thread in der Warteschlange sind, kann keiner weiter<br />

arbeiten, das Programm steht.<br />

Schritt 5 hat außerdem gezeigt : der Verbraucher c1 hat einen<br />

anderen Verbraucher (c2) geweckt. Dies war der ”<br />

falsche“ Thread.<br />

Die Lösung des Problems kann nun darin bestehen, dass nicht ein<br />

Thread, sondern alle Thread geweckt werden. Da nur einer ausgewählt<br />

wird und jeder in einer while-Schleife prüft, ob er arbeiten kann, wird das<br />

funktionieren.<br />

93 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Korrekte Lösung durch wait-notifyAll<br />

Die Klasse Objekt besitzt neben dem einfachen notify(), eine weitere<br />

Methode zum Aufwecken von Threads: notifyAll() weckt alle in der<br />

Warteschlange eines Objekts befindlichen Threads.<br />

Die korrekte Lösung besteht also darin, notify() durch notifyAll() zu<br />

ersetzen: ProduceConsume3.java<br />

1 class Buffer { ...<br />

2 public synchronized void put ( int x) {<br />

3 while ( available ) {<br />

4 try { wait ();} catch ( InterruptedException e) {}<br />

5 }<br />

6 data = x;<br />

7 available = true ;<br />

8 notifyAll (); ←<br />

9 }<br />

10 public synchronized int get () {<br />

11 while (! available ) {<br />

12 try { wait ();} catch ( InterruptedException e) {}<br />

13 }<br />

14 available = false ;<br />

15 notifyAll (); ←<br />

16 return data ;<br />

17 }<br />

18 }<br />

94 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Regel für notifyAll<br />

Prinzipiell kann man immer notify() durch notifyAll() ersetzen, die<br />

Umkehrung allerdings wäre falsch (wie im letzten Beispiel gesehen).<br />

Die Methode notifyAll() ist zu verwenden, wenn mindestens eine der<br />

beiden folgenden Situationen zutrifft:<br />

1 In der Warteschlange befinden sich Threads, mit unterschiedlichen<br />

Wartebedingungen (z.B. Puffer leer, Puffer voll). Dann kann bei<br />

Verwendung von notify() der ”<br />

falsche“ Thread geweckt werden.<br />

2 Durch die Veränderung des Zustands eines Objekts können mehrere<br />

Threads weiterlaufen (Wert im Puffer - alle wartenden Verbraucher<br />

können arbeiten).<br />

95 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Lösungsansätze in Java<br />

Modellierung von wait- und notifyAll durch Petri-Netze<br />

class K {<br />

public synchronized void m1 () {<br />

while (...)<br />

wait ();<br />

←<br />

action1 (); }<br />

public synchronized void m2 () {<br />

action2 ();<br />

notifyAll (); ←<br />

}<br />

private void action1 () { ... }<br />

private void action2 () { ... }<br />

}<br />

class T extends Thread {<br />

private K einK ;<br />

public T(K k) {<br />

einK = k;<br />

}<br />

public void run () {<br />

while (...)<br />

switch (...) {<br />

case ...: einK .m1 (); break ;<br />

case ...: einK .m2 (); break ;<br />

}<br />

}<br />

}<br />

96 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Semaphore<br />

1965 wurde von Dijkstra das Konzept der Semaphore zur Synchronisation<br />

vorgeschlagen.<br />

• Eine Semaphore ist eine Integervariable, deren<br />

• Wert Null anzeigt, dass kein Prozess (Thread) mehr zu wecken ist,<br />

• ein Wert grösser Null bedeutet, dass Wecksignale zu<br />

berücksichtigen sind.<br />

• Als Operationen wurde definiert ”<br />

DOWN“ (=p) und ”<br />

UP“ (=v).<br />

• ”<br />

Down“ prüft, ob der Semaphore größer als Null ist, dann wird der<br />

Wert vermindert; ist der Wert Null, wird der Thread schlafen gelegt<br />

und die DOWN-Operation ist beendet.<br />

• ”<br />

UP“ inkrementiert den Semaphore.<br />

Sowohl ”<br />

UP“ als auch ”<br />

DOWN“ sind dabei atomare Operationen,<br />

d.h. es ist sichergestellt, dass während der Ausführung der<br />

Operration kein anderer Thread auf den Semaphore zugreifen kann.<br />

97 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Semaphore Implementierung in Java<br />

Semaphore.java<br />

public class Semaphore {<br />

private int value ;<br />

public Semaphore ( int init ) { ←<br />

if( init < 0)<br />

init = 0;<br />

value = init ;<br />

}<br />

public synchronized void p() { ← Dijkstra’s operation p=down<br />

while ( value == 0) {<br />

try { wait (); } ←<br />

catch ( InterruptedException e) {}<br />

}<br />

value - -;<br />

}<br />

}<br />

public synchronized void v() { ← Dijkstra’s operation v=up<br />

value ++;<br />

notify ();<br />

←<br />

}<br />

98 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Gegenseitiger Ausschluss mit Semaphoren<br />

Gegenseitiger Ausschluss bezeichnet eine Situation, in der ein bestimmtes<br />

Programmstück zu einer Zeit von höchstens einem Thread<br />

ausgeführt werden darf.<br />

• In Java kann dazu eine synchronized Methode verwendet werden:<br />

auf ein Objekt kann dadurch zu einem Zeitpunkt nur von einem<br />

Thread zugegriffen werden.<br />

• Hier soll nun nicht mit synchronized gearbeitet werden, sondern es<br />

wird eine Klasse mit einer Semaphore verwendet.<br />

Als kritischen Abschnitt bezeichnet man das Programmstück, das von<br />

höchstens einem Thread betreten werden darf.<br />

99 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Gegenseitiger Ausschluss mit Semaphoren<br />

Im folgenden Beispiel werden die Aktionen, die ein Thread im kritischen<br />

Abschnitt ausführt simuliert durch eine Sleep-Operation. In einer realen<br />

Anwendung würde hier das Codefragment stehen, das auf die<br />

gemeinsamen Objekte zugreift.<br />

Der kritische Abschnitt wird durch Semaphore geschützt, indem<br />

1 vor Betreten die Semaphor-Methode down(),<br />

2 danach die Methode up()<br />

aufgerufen wird.<br />

100 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

MutualExclusion.java<br />

1 class MutexThread extends Thread {<br />

2 private Semaphore mutex ;<br />

4 public MutexThread ( Semaphore mutex , String name ) {<br />

5 super ( name );<br />

6 this . mutex = mutex ;<br />

7 start ();<br />

8 }<br />

10 public void run () {<br />

11 while ( true ) {<br />

12 mutex .p (); ←<br />

13 System . out . println (" kritischen Abschnitt betreten : "<br />

14 + getName ());<br />

15 try { sleep (( int )( Math . random () * 100));}<br />

16 catch ( InterruptedException e) {}<br />

17 mutex .v (); ←<br />

18 System . out . println (" kritischen Abschnitt verlassen : "<br />

19 + getName ());<br />

20 }<br />

21 }<br />

22 }<br />

101 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

MutualExclusion.java<br />

1 public class MutualExclusion {<br />

2 public static void main ( String [] args ) {<br />

3 int noThreadsInCriticalSection =1;<br />

4 if ( args . length != 1) {<br />

5 System . err . println (<br />

6 " usage : java MutualExclusion < NoThreadsInCriticalSection >");<br />

7 System . exit (1);<br />

8 } else<br />

9 noThreadsInCriticalSection = Integer . parseInt ( args [0]);<br />

10 Semaphore mutex = new Semaphore ( noThreadsInCriticalSection );<br />

11 for ( int i = 1; i


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Der Aufruf zeigt, dass stets nur so viele Threads im kritischen Abschnitt<br />

sind, wie man beim Aufruf angegeben hat:<br />

1 $ java MutualExclusion 1<br />

2 kritischen Abschnitt betreten : Thread 1 ← 1<br />

3 kritischen Abschnitt verlassen : Thread 1 ← 0<br />

4 kritischen Abschnitt betreten : Thread 1 ← 1<br />

5 kritischen Abschnitt verlassen : Thread 1 ← 0<br />

6 kritischen Abschnitt betreten : Thread 3 ← 1<br />

7 kritischen Abschnitt verlassen : Thread 3 ← 0<br />

8 kritischen Abschnitt betreten : Thread 1 ← 1<br />

9 kritischen Abschnitt verlassen : Thread 1 ← 0<br />

10 ...<br />

11 $ java MutualExclusion 3<br />

12 kritischen Abschnitt betreten : Thread 1 ← 1<br />

13 kritischen Abschnitt betreten : Thread 2 ← 2<br />

14 kritischen Abschnitt betreten : Thread 3 ← 3<br />

15 kritischen Abschnitt verlassen : Thread 1 ← 2<br />

16 kritischen Abschnitt betreten : Thread 1 ← 3<br />

17 kritischen Abschnitt verlassen : Thread 1 ← 2<br />

18 kritischen Abschnitt betreten : Thread 4 ← 3<br />

19 kritischen Abschnitt verlassen : Thread 4 ← 2<br />

20 kritischen Abschnitt betreten : Thread 1 ← 3<br />

21 ...<br />

103 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Frage<br />

Was passiert durch Aufruf von<br />

$ java MutualExclusion 0<br />

104 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Hörsaalübung<br />

Ändern Sie das letzte Programm (EvenThread) so ab, dass die Klasse<br />

Even ohne synchronized- Methoden auskommt.<br />

Verwenden Sie dazu Semaphore. Test.java<br />

1 class Even {<br />

2 private int n = 0;<br />

3 public int next () { // POST : next is always even ←<br />

4 ++n;<br />

5 try { Thread . sleep (( long )( Math . random ()*10));<br />

6 } catch ( InterruptedException e) { }<br />

7 ++n;<br />

8 return n;<br />

9 }<br />

10 }<br />

105 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Bearbeitungsreihenfolge von Threads festlegen<br />

In vielen Anwendungen ist es erforderlich, dass Threads parallel arbeiten<br />

und dennoch eine Abarbeitungsreihenfolge einzuhalten ist. Dabei starten<br />

alle Threads gleichzeitig, führen gewisse Aktionen aus und müssen dann<br />

warten, bis andere Threads Aktivitäten abgeschlossen haben.<br />

Insgesamt lassen sich solche Abhängigkeiten durch einen Graphen<br />

darstellen:<br />

1 Knoten sind Threads,<br />

2 eine Kante existiert von einem Thread T1 zu T2, falls T1 seine<br />

Aktivitäten beendet haben muss, bevor T2 starten kann.<br />

Das folgende Beispiel demonstriert eine solche Situation.<br />

106 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Die Umsetzung in Java erfolgt durch Semaphore:<br />

• Für jede Abhängigkeit (Kante im Graph) wird<br />

eine Semaphore verwendet. Die Semaphoren<br />

sind in einem Array sems zusammengefasst, so<br />

dass die nebenstehend gezeigten<br />

Abhängigkeiten definiert sind.<br />

• Bevor eine Aktion ausgeführt wird, wird die<br />

p()-Methode für alle Semaphoren, die den<br />

eingehenden Kanten entsprechen, ausgeführt;<br />

• nach der Aktion die v()-Methode auf allen<br />

Semaphoren der ausgehenden Kanten.<br />

• Der Einfachheit halber bekommen alle<br />

Threads eine Referenz auf das<br />

Semaphoren-Array, auch wenn nicht alle<br />

Threads jede Semaphore benutzen.<br />

107 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Aus Sicht des Threads i werden folgende Aktionen ausgeführt:<br />

1 i führt p()-Operation aus: Warten auf v-Operation von i-1<br />

2 i-1 hat v()-Operation ausgeführt: i kann Aktion ausführen<br />

3 i führt v-Operation aus: i+1 kann aktiv werden<br />

108 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

TimingRelation.java<br />

1 class T1 extends Thread {<br />

2 private Semaphore [] sems ;<br />

4 public T1( Semaphore [] sems ) {<br />

5 this . sems = sems ;<br />

6 start ();<br />

7 }<br />

9 private void a1 () {<br />

10 System . out . println ("a1");<br />

11 try {<br />

12 sleep (( int )( Math . random () * 10));<br />

13 } catch ( InterruptedException e) {}<br />

14 }<br />

16 public void run () {<br />

17 a1 ();<br />

18 sems [0]. v ();<br />

19 sems [1]. v ();<br />

20 sems [2]. v ();<br />

21 }<br />

22 }<br />

109 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

TimingRelation.java<br />

1 class T2 extends Thread {<br />

2 private Semaphore [] sems ;<br />

4 public T2( Semaphore [] sems ) {<br />

5 this . sems = sems ;<br />

6 start ();<br />

7 }<br />

9 private void a2 () {<br />

10 System . out . println ("a2");<br />

11 try {<br />

12 sleep (( int )( Math . random () * 10));<br />

13 } catch ( InterruptedException e) {}<br />

14 }<br />

16 public void run () {<br />

17 sems [0]. p (); ←<br />

18 a2 ();<br />

19 sems [3]. v (); ←<br />

20 }<br />

21 }<br />

110 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

TimingRelation.java<br />

1 class T3 extends Thread {<br />

2 private Semaphore [] sems ;<br />

4 public T3( Semaphore [] sems ) {<br />

5 this . sems = sems ;<br />

6 start ();<br />

7 }<br />

9 private void a3 () {<br />

10 System . out . println ("a3");<br />

11 try {<br />

12 sleep (( int )( Math . random () * 10));<br />

13 } catch ( InterruptedException e) {}<br />

14 }<br />

16 public void run () {<br />

17 sems [1]. p ();<br />

18 a3 ();<br />

19 sems [4]. v ();<br />

20 }<br />

21 }<br />

111 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

TimingRelation.java<br />

1 class T4 extends Thread {<br />

2 private Semaphore [] sems ;<br />

4 public T4( Semaphore [] sems ) {<br />

5 this . sems = sems ;<br />

6 start ();<br />

7 }<br />

9 private void a4 () {<br />

10 System . out . println ("a4");<br />

11 try {<br />

12 sleep (( int )( Math . random () * 10));<br />

13 } catch ( InterruptedException e) {}<br />

14 }<br />

16 public void run () {<br />

17 sems [2]. p ();<br />

18 a4 ();<br />

19 sems [5]. v ();<br />

20 }<br />

21 }<br />

112 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

TimingRelation.java<br />

1 class T5 extends Thread {<br />

2 private Semaphore [] sems ;<br />

4 public T5( Semaphore [] sems ) {<br />

5 this . sems = sems ;<br />

6 start ();<br />

7 }<br />

9 private void a5 () {<br />

10 System . out . println ("a5");<br />

11 try {<br />

12 sleep (( int )( Math . random () * 10));<br />

13 } catch ( InterruptedException e) {}<br />

14 }<br />

16 public void run () {<br />

17 sems [3]. p ();<br />

18 sems [4]. p ();<br />

19 sems [5]. p ();<br />

20 a5 ();<br />

21 }<br />

22 }<br />

113 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

TimingRelation.java<br />

1 public class TimingRelation {<br />

2 public static void main ( String [] args ) {<br />

3 Semaphore [] sems = new Semaphore [6];<br />

4 for ( int i = 0; i < 6; i ++) {<br />

5 sems [i] = new Semaphore (0);<br />

6 }<br />

7 new T4( sems );<br />

8 new T5( sems );<br />

9 new T1( sems );<br />

10 new T2( sems );<br />

11 new T3( sems );<br />

12 }<br />

13 }<br />

$ java TimingRelation<br />

a1<br />

a3<br />

a2<br />

a4<br />

a5<br />

$ java TimingRelation<br />

a1<br />

a2<br />

a3<br />

a4<br />

a5<br />

$<br />

114 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Additive Semaphoren<br />

Semaphoren sind Integerwerte, wobei die p()- und v()-Operationen den<br />

Integerwert jeweils um eins inkrementieren bzw. dekrementieren.<br />

Eine Verallgemeinerung davon stellen additive Semaphore dar:<br />

der Integer kann um beliebige Werte inkrementiert bzw.<br />

dekrementiert werden.<br />

115 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Die Methoden p() und v() haben ein Integerargument, das angibt, um<br />

welchen Wert erniedrigt bzw. erhöht werden soll.<br />

Dieses Argument muss positiv sein, ansonsten könnte z.B. eine p-<br />

Operation die Semaphore erhöhen.<br />

AdditiveSemaphore.java<br />

1 public class AdditiveSemaphore {<br />

2 private int value ;<br />

4 public synchronized void p( int x) {<br />

5 if(x


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

AdditiveSemaphore.java<br />

1 public synchronized void v( int x) {<br />

2 if(x 0)<br />

10 v(x);<br />

11 else if(x < 0)<br />

12 p(-x);<br />

13 }<br />

Die Methode v(int) muss notifyAll() verwenden. Würde notify()<br />

verwendet werden, könnten u.U. mehrere Threads weiterlaufen.<br />

117 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Der Programmcode zeigt, dass eine additive Semaphore ”<br />

auf einen<br />

Schlag“ erhöht oder erniedrigt wird.<br />

Dies bedeutet, dass die Verwendung von einer p-Operation p(n) nicht<br />

identisch ist mit n p-Operationen p(1):<br />

• Nehmen wir an, zwei Threads verwenden eine additive Semaphore<br />

zur Synchronisation; der aktuelle Wert sei 4. Beide wollen den Wert<br />

um 3 erniedrigen.<br />

• richtige Vorgehensweise:<br />

• T1 und T2 führen p(3) aus.<br />

• T1 wird ausgewählt und p(3) ist abgeschlossen. T2 blockiert beim<br />

Aufruf P(3).<br />

• falsche Vorgehensweise:<br />

• T1 und T2 führen jeweils p(1); p(1); p(1); aus.<br />

• T1 wird ausgewählt und (p(1); p(1);) ist abgeschlossen (Semaphor<br />

== 2), dann wird auf T2 umgeschaltet.<br />

• T2 führt (p(1); p(1);) nun blockiert beim Aufruf p(1);<br />

• T1 blockiert beim Aufruf p(1) → Verklemmung<br />

118 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Semaphorgruppen<br />

Eine Semaphorgruppe ist eine Verallgemeinerung einer additiven<br />

Semaphore:<br />

• Ein Aufruf der Methode change() erhöht oder erniedrigt eine<br />

Menge von Semaphoren, die zur selben Gruppe gehören.<br />

• Die Änderungen werden nur durchgeführt, wenn alle Semaphoren<br />

der Gruppe nicht durch die Änderung negativ werden; in diesem<br />

Fall wird gewartet ohne dass eine Änderung vollzogen wird.<br />

119 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

Die Realisierung der Klasse SemaphoreGroup verwendet ein Integerarray<br />

(values) zur Darstellung der Semaphorwerte. Die Umsetzung mit<br />

mehreren Objekten vom Typ AdditiveSemaphore würde zu<br />

Verklemmungssituationen führen.<br />

SemaphoreGroup.java<br />

1 public class SemaphoreGroup {<br />

2 private int [] values ;<br />

4 public SemaphoreGroup ( int numberOfMembers ) {<br />

5 if( numberOfMembers


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

SemaphoreGroup.java<br />

1 public synchronized void changeValues ( int [] deltas ) {<br />

2 if( deltas . length != values . length )<br />

3 return ;<br />

4 while (! canChange ( deltas )) {<br />

5 try { wait ();<br />

6 } catch ( InterruptedException e) {}<br />

7 }<br />

8 doChange ( deltas );<br />

9 notifyAll ();<br />

10 }<br />

Der Parameter der Methode changeValues gibt an, um welchen Wert die<br />

Semaphore jeweils verändert werden soll: deltas[i] gibt an, wie values[i]<br />

geändert werden soll; delta[i] kann positiv oder negativ sein.<br />

121 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Semaphore<br />

SemaphoreGroup.java<br />

1 private boolean canChange ( int [] deltas ) {<br />

2 for ( int i = 0; i < values . length ; i ++)<br />

3 if( values [i] + deltas [i] < 0)<br />

4 return false ;<br />

5 return true ;<br />

6 }<br />

8 private void doChange ( int [] deltas ) {<br />

9 for ( int i = 0; i < values . length ; i ++)<br />

10 values [i] = values [i] + deltas [i];<br />

11 }<br />

13 public int getNumberOfMembers () {<br />

14 return values . length ;<br />

15 }<br />

Die private Methode canChange() gibt an, ob alle Änderungen durchführbar<br />

sind oder nicht.<br />

Die private Methode doChange führt die Änderungen durch. Danach werden in<br />

changeValues() alle wartenden Thread informiert (notifyAll()).<br />

Die öffentliche Methode getNumberOfMembers() dient zum Abfragen der<br />

Größe der Semaphorgruppe.<br />

Das Applet semgrp.html verdeutlicht den Gebrauch von Semaphorgruppen.<br />

122 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Monitore und andere Primitive<br />

Monitore und andere Primitive<br />

Semaphore in ihren unterschiedlichen Ausprägungen sind Primitive der<br />

Interprozesskommunikation. Andere Primitive wurden entwickelt, sie sind<br />

aber alle jeweils durch Semaphore abbildbar, z.B.:<br />

• Hoare und Brich Hansen haben 1975 Monitore vorgeschlagen:<br />

ein Monitor ist eine gekapselte Datenstruktur, in der eine<br />

Bindungsvariable durch die Operationen WAIT und<br />

SIGNAL verwaltet wird.<br />

(vgl. Java Beispiele mit wait und notify)<br />

• Campell und Habermann führten 1974 Pfadausdrucke ein.<br />

• Atkinson und Hewitt diskutierten 1979 Serializer.<br />

123 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Nachrichtenaustausch<br />

Die bisher betrachteten Primitive haben vorausgesetzt, dass es einen<br />

gemeinsam nutzbaren Speicher gibt.<br />

Die Idee des Nachrichtenaustausch ist es,<br />

• ein Kommunikationsmedium (etwa ein Netz, einen Bus) zu<br />

verwenden, so dass<br />

• ein Prozess (Sender) einem anderen Prozess (Empfänger) eine<br />

Nachricht sendet.<br />

• Beide <strong>Prozesse</strong> synchronisieren sich automatisch, indem der<br />

Empfänger wartet, bis er eine Nachricht erhalten hat.<br />

Als Kommunikationsprimitive sind erforderlich:<br />

• send(destination, &message) und<br />

• receive(source, &message).<br />

Diese Thematik wird u.a. in der Vorlesung ”<br />

Verteilte Systeme“<br />

behandelt. Hier wird wieder mittels Java auf<br />

• Message Queues und<br />

• Pipes<br />

eingegangen.<br />

124 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Nachrichtenaustausch<br />

Zunächst wird ein Puffer mit n Elemente definiert, der es erlaubt, Daten<br />

fester Grösse zwischen Sender und Empfänger auszutauschen.<br />

Dann wird gezeigt, wie Daten beliebiger Länger transferiert werden<br />

können.<br />

125 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Puffer mit n Elementen<br />

• Der Erzeuger fügt Daten am<br />

Pufferende (Position tail ) ein.<br />

• Der Verbraucher entnimmt den Wert<br />

am Pufferanfang (an Position 0<br />

head )) und<br />

• reorganisiert den Puffer, d.h. alle<br />

Elemente werden nach oben<br />

verschoben.<br />

Was halten Sie von dieser Lösung?<br />

126 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Puffer mit n Elementen<br />

Das Reorganisieren kann vermieden<br />

werden, wenn der Puffer zyklisch<br />

organisiert wird:<br />

127 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Puffer in Java<br />

BufferN.java<br />

1 public class BufferN {<br />

2 private int head ;<br />

3 private int tail ;<br />

4 private int numberOfElements ;<br />

5 private int [] data ;<br />

7 public BufferN ( int n) {<br />

8 data = new int [n];<br />

9 head = 0;<br />

10 tail = 0;<br />

11 numberOfElements = 0;<br />

12 }<br />

13 ...<br />

14 }<br />

128 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Puffer in Java<br />

BufferN.java<br />

1 public synchronized void put ( int x) {<br />

2 while ( numberOfElements == data . length ) {<br />

3 try {<br />

4 wait ();<br />

5 } catch ( InterruptedException e) {}<br />

6 }<br />

7 data [ tail ++] = x;<br />

8 if( tail == data . length )<br />

9 tail = 0;<br />

10 numberOfElements ++;<br />

11 notifyAll ();<br />

12 }<br />

Wenn der Puffer voll geworden ist, muss die Methode put() warten. Die<br />

Schleife ist erforder- lich, da wir notifyAll() verwenden müssen. Wenn eine<br />

freie Position vorhanden ist, wird der Wert gespeichert und die Variable<br />

” tail“ zyklisch inkrementiert, danach wird ein wartender Thread geweckt. 129 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Puffer in Java<br />

BufferN.java<br />

1 public synchronized int get () {<br />

2 while ( numberOfElements == 0) {<br />

3 try {<br />

4 wait ();<br />

5 } catch ( InterruptedException e) {}<br />

6 }<br />

7 int result = data [ head ++];<br />

8 if( head == data . length )<br />

9 head = 0;<br />

10 numberOfElements - -;<br />

11 notifyAll ();<br />

12 return result ;<br />

13 }<br />

Die Methode get() funktioniert analog.<br />

130 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Message Queues<br />

Nun wird gezeigt, wie Daten beliebiger Länge ausgetauscht werden<br />

können.<br />

Dabei muss die Nachrichtengrenze bewahrt bleiben, d.h. wird z.B.<br />

Byte-Array gesendet so wird exakt diese Menge an Daten empfangen:<br />

131 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Message Queues<br />

MessagaQueue.java<br />

1 public class MessageQueue {<br />

2 private byte [][] msgQueue = null ;<br />

3 private int qsize = 0; // size of message queue as<br />

4 // number of entries ( not number of bytes )<br />

5 private int head = 0;<br />

6 private int tail = 0;<br />

8 public MessageQueue ( int capacity ) {<br />

9 if( capacity


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Message Queues<br />

MessagaQueue.java<br />

1 public synchronized void send ( byte [] msg ) {<br />

2 while ( qsize == msgQueue . length ) {<br />

3 try {<br />

4 wait ();<br />

5 } catch ( InterruptedException e) {}<br />

6 }<br />

8 msgQueue [ tail ] = new byte [ msg . length ]; // copy message<br />

9 // and store the copy<br />

10 for ( int i = 0; i < msg . length ; i ++)<br />

11 msgQueue [ tail ][i] = msg [i];<br />

12 qsize ++;<br />

13 tail ++;<br />

14 if( tail == msgQueue . length )<br />

15 tail = 0;<br />

16 notifyAll ();<br />

17 }<br />

133 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Message Queues<br />

MessagaQueue.java<br />

1 public synchronized byte [] receive () {<br />

2 while ( qsize == 0) {<br />

3 try {<br />

4 wait ();<br />

5 } catch ( InterruptedException e) {}<br />

6 }<br />

7 byte [] result = msgQueue [ head ];<br />

8 msgQueue [ head ] = null ;<br />

9 qsize - -;<br />

10 head ++;<br />

11 if( head == msgQueue . length )<br />

12 head = 0;<br />

13 notifyAll ();<br />

14 return result ;<br />

15 }<br />

Durch ein Applet kann das Verhalten von MessagaQueue demonstriert<br />

werden.<br />

134 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Pipes<br />

Message Queues bewahren die Nachrichtengrenzen; deshalb wird diese<br />

Art der Kommunikation auch ”<br />

nachrichtenorientiert“ genannt.<br />

Demgegenüber ist es für den Empfänger einer Nachricht bei<br />

streamorientierten“ Kommunkation nicht möglich, die einzelnen<br />

”<br />

Nachrichten zu unterscheiden, die an der Kommunikation beteiligt<br />

sind.<br />

Diese Verhalten verdeutlicht die Kommunikation über Pipes (mit einem<br />

Byte-Array fester Grösse)<br />

→ pipe<br />

135 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Pipe in Java<br />

Pipe.java<br />

1 public class Pipe {<br />

2 private byte [] buffer = null ;<br />

3 private int bsize = 0;<br />

4 private int head = 0;<br />

5 private int tail = 0;<br />

7 public Pipe ( int capacity ) {<br />

8 if( capacity


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Die send()-Operation, die Daten in eine Pipe schreibt, muss als atomare<br />

Operation realisiert werden: wenn die Nachricht grösser ist, als der noch<br />

verfügbar freie Platz in der Pipe, muss der Sender blockieren, bis ein<br />

Empfänger durch receive() Platz in der Pipe gemacht hat.<br />

Pipe.java<br />

1 public synchronized void send ( byte [] msg ) {<br />

2 if(msg . length buffer . length - bsize ) {<br />

5 try { wait ();<br />

6 } catch ( InterruptedException e) {}<br />

7 }<br />

9 // copy message into buffer<br />

10 for ( int i = 0; i < msg . length ; i ++) {<br />

11 buffer [ tail ] = msg [i];<br />

12 tail ++;<br />

13 if( tail == buffer . length )<br />

14 tail = 0;<br />

15 }<br />

16 bsize += msg . length ;<br />

17 notifyAll ();<br />

18 } else {<br />

19 ...<br />

137 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Wenn die gesamte Länge der Nachricht grösser ist, als die Kapazität der<br />

Pipe, so wird die Nachricht in kleine Stücke geteilt und jeweils nur ein<br />

Stück gesendet.<br />

1 public synchronized void send ( byte [] msg ) {<br />

2 ...<br />

3 else { // send in portions<br />

4 int offset = 0;<br />

5 int stillToSend = msg . length ;<br />

6 while ( stillToSend > 0) {<br />

7 while ( bsize == buffer . length ) {<br />

8 try { wait ();<br />

9 } catch ( InterruptedException e) {}<br />

10 }<br />

11 int sendNow = buffer . length - bsize ;<br />

12 if( stillToSend < sendNow ) sendNow = stillToSend ;<br />

13 for ( int i = 0; i < sendNow ; i ++) {<br />

14 buffer [ tail ] = msg [ offset ];<br />

15 tail ++;<br />

16 if( tail == buffer . length ) tail = 0;<br />

17 offset ++;<br />

18 }<br />

19 bsize += sendNow ; stillToSend -= sendNow ;<br />

20 notifyAll ();<br />

21 }<br />

22 }<br />

23 }<br />

138 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Interprozesskommunikation<br />

Nachrichtenaustausch<br />

Beim Empfang einer Nachricht, muss blockiert werden, bis Daten in der<br />

Pipe verfügbar sind. Ein Parameter beim receive() gibt die erwartete<br />

Grösse der Nachricht an; wenn weniger Daten in der Pipe sind, wird nicht<br />

blockiert, sondern nur der verfügbare Teil gelesen.<br />

Pipe.java<br />

1 public synchronized byte [] receive ( int noBytes ) {<br />

2 while ( bsize == 0) {<br />

3 try {<br />

4 wait ();<br />

5 } catch ( InterruptedException e) {}<br />

6 }<br />

7 if( noBytes > bsize )<br />

8 noBytes = bsize ;<br />

9 byte [] result = new byte [ noBytes ];<br />

10 for ( int i = 0; i < noBytes ; i ++) {<br />

11 result [i] = buffer [ head ];<br />

12 head ++;<br />

13 if( head == buffer . length )<br />

14 head = 0;<br />

15 }<br />

16 bsize -= noBytes ;<br />

17 notifyAll ();<br />

18 return result ;<br />

19 }<br />

20 }<br />

139 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

IPC Problem<br />

1 Prozessmodell<br />

2 Interprozesskommunikation<br />

3 IPC Probleme<br />

Problem speisender Philosophen<br />

Lese-Schreiber Problem<br />

Das Problem des schlafenden Friseurs<br />

4 Scheduling<br />

140 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

Problem speisender Philosophen<br />

Problem speisender Philosophen (Dijkstra, 1965)<br />

• Fünf Philosophen sitzen um einen runden Tisch.<br />

• Jeder hat einen Teller mit Spagetti vor sich auf<br />

dem Teller.<br />

• Zwischen jedem Teller liegt eine Gabel. Um Essen<br />

zu können, braucht man immer zwei Gabeln.<br />

• Das Leben eines Philosophen besteht aus Essen<br />

und Denken.<br />

• Wenn ein Philosoph hungrig wird, versucht er die<br />

linke und rechte Gabel zu nehmen und zu Essen.<br />

Das Problem:<br />

Es ist ein Programm für einen Philosophen zu finden, das, auf<br />

alle Philosophen angewendet, nie dazu führt, dass einer der<br />

Philosophen verhungert.<br />

141 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

Problem speisender Philosophen<br />

Dieses Problem ist typisch für <strong>Betriebssysteme</strong>: es verdeutlicht den<br />

Wettbewerb um eine begrenzte Anzahl von exklusiv nutzbaren<br />

Betriebsmitteln, wie CPU oder E/A Geräte.<br />

Eine offensichtliche (aber nicht korrekte) Lösung in C<br />

ist:<br />

1 const int N =5;<br />

2 philosophers ( int i) {<br />

3 while ( true ) {<br />

4 think ();<br />

5 takeFork (i); // take left fork<br />

6 take_fork ((i +1)% N); // take right fork<br />

7 eat ();<br />

8 putFork (i); // put left fork back<br />

9 putFork ((i +1)% N); // put right fork back<br />

10 }<br />

11 }<br />

Die Funktion takeFork() wartet, bis die entsprechende<br />

Gabel frei ist und nimmt dann die Gabel. Ist die Gabel<br />

nicht frei, wird eine Zeit lang gewartet und erneut<br />

versucht, die Gabel zu nehmen.<br />

Was halten Sie<br />

von dieser<br />

Lösung?<br />

142 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

Problem speisender Philosophen<br />

1 const int N =5;<br />

2 philosophers ( int i) {<br />

3 while ( true ) {<br />

4 think ();<br />

5 takeFork (i); // take left fork<br />

6 take_fork ((i +1)% N); // take right fork<br />

7 eat ();<br />

8 putFork (i); // put left fork back<br />

9 putFork ((i +1)% N); // put right fork back<br />

10 }<br />

11 }<br />

Die Lösung funktioniert nicht, wenn z.B. alle Philosophen gleichzeitig die<br />

linke Gabel nehmen, da niemand die rechte Gabel nehmen kann und so<br />

alle verhungern (Deadlock).<br />

143 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

Problem speisender Philosophen<br />

Mit dem synchronized-Konzept von Java ist eine Lösung realisierbar,<br />

denn das Nehmen einer Gabel wird atomar: Philosophers.java<br />

1 class Table {<br />

2 private boolean [] usedFork ;<br />

4 public Table ( int numberForks ) {<br />

5 usedFork = new boolean [ numberForks ];<br />

6 for ( int i = 0; i < usedFork . length ; i ++)<br />

7 usedFork [i] = false ;<br />

8 }<br />

10 private int left ( int i) {<br />

11 return i;<br />

12 }<br />

14 private int right ( int i) {<br />

15 if(i+1 < usedFork . length )<br />

16 return i +1;<br />

17 else<br />

18 return 0;<br />

19 }<br />

20<br />

144 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

Problem speisender Philosophen<br />

21 public synchronized void takeForks ( int position ) {<br />

22 while ( usedFork [ left ( position )]<br />

23 || usedFork [ right ( position )]) {<br />

24 try { wait ();<br />

25 } catch ( InterruptedException e) {}<br />

26 }<br />

27 usedFork [ left ( position )] = true ;<br />

28 usedFork [ right ( position )] = true ;<br />

29 }<br />

31 public synchronized void positionBackForks ( int position ) {<br />

32 usedFork [ left ( position )] = false ;<br />

33 usedFork [ right ( position )] = false ;<br />

34 notifyAll ();<br />

35 }<br />

36 } // Table<br />

37<br />

145 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

Problem speisender Philosophen<br />

38 class Philosoph extends Thread {<br />

39 private Table Table ;<br />

40 private int position ;<br />

42 public Philosoph ( Table Table , int position ) {<br />

43 this . Table = Table ;<br />

44 this . position = position ;<br />

45 start ();<br />

46 }<br />

48 public void run () { life of a philosoph<br />

49 while ( true ) {<br />

50 thinking ( position );<br />

51 Table . takeForks ( position );<br />

52 eating ( position );<br />

53 Table . positionBackForks ( position );<br />

54 }<br />

55 }<br />

57 private void thinking ( int position ) {<br />

58 System . out . println (" Philosoph " + position<br />

59 + " is thinking .");<br />

60 try {<br />

61 sleep (( int )( Math . random () * 20000));<br />

62 } catch ( InterruptedException e) { }<br />

63 }<br />

146 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

Problem speisender Philosophen<br />

65 private void eating ( int position ) {<br />

66 System . out . println (" Philosoph " + position<br />

67 + " starts eating .");<br />

68 try {<br />

69 sleep (( int )( Math . random () * 20000));<br />

70 } catch ( InterruptedException e) {}<br />

71 System . out . println (" Philosoph " + position<br />

72 + " finished eating .");<br />

73 }<br />

74 }<br />

76 public class Philosophers {<br />

77 private static final int numberForks = 5;<br />

79 public static void main ( String [] args ) {<br />

80 Table Table = new Table ( numberForks );<br />

81 for ( int i = 0; i < numberForks ; i ++)<br />

82 new Philosoph ( Table , i);<br />

83 }<br />

84 }<br />

Hier eine Lösung, die Semaphorgruppen verwendet, um die Operation<br />

atomar zu machen.<br />

Ein Applet verdeutlicht dies.<br />

147 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

Lese-Schreiber Problem<br />

Lese-Schreiber Problem<br />

Das Lese-Schreiber Problem modelliert den Zugriff auf eine Datenbank:<br />

Auf einen Datenbestand wollen mehrere <strong>Prozesse</strong> gleichzeitig<br />

zugreifen.<br />

• Es ist erlaubt, dass mehrere <strong>Prozesse</strong> gleichzeitig lesen,<br />

• aber nur einer darf zu einem Zeitpunkt schreiben.<br />

• Wenn geschrieben wird, darf kein Prozess (auch kein<br />

lesender) zugreifen.<br />

148 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

Lese-Schreiber Problem<br />

1 semaphore mutex = 1; // controls access to rc<br />

2 semaphore db = 1; // controls access to DB<br />

3 int rc = 0; // # processes reading or wanting to<br />

5 reader () {<br />

6 while ( true ) { // repeat forever<br />

7 down ( mutex );<br />

8 rc ++;<br />

9 if (rc == 1) down (db ); // for first reader<br />

10 up( mutex );<br />

11 read_data_base ();<br />

12 down ( mutex );<br />

13 rc - -;<br />

14 if (rc == 0) up(db ); // release if last reader<br />

15 up( mutex );<br />

16 use_data_read ();<br />

17 }<br />

18 }<br />

19 writer () {<br />

20 while ( true ) {<br />

21 think_up_data ();<br />

22 down (& db );<br />

23 write_data_base ()(); // update the data<br />

24 up(db ); /<br />

25 }<br />

26 }<br />

Lösung in Java? → Übung<br />

Die Lösung dazu verwendet eine<br />

Semaphore db, um den Zugriff auf die<br />

Datenbank zu synchronisieren.<br />

• Der erste Leser (reader) ruft<br />

für den Zugriff auf den<br />

Datenbestand die Methode<br />

down() mit der Semaphore<br />

db“ auf,<br />

”<br />

• nachfolgende Leser erhöhen<br />

lediglich den Zähler ”<br />

rc“.<br />

• Wenn Leser den kritischen<br />

Bereich verlassen, erniedrigen<br />

sie den Zähler und<br />

• der letzte Leser ruft up() für<br />

die Semaphore auf und weckt<br />

einen potentiellen Schreiber.<br />

149 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

Das Problem des schlafenden Friseurs<br />

Das Problem des schlafenden Friseurs<br />

In einem Friseursalon mit einem Friseurstuhl und n Stühlen für wartende<br />

Kunden arbeitet ein Friseur.<br />

• Falls kein Kunde die Haare geschnitten haben möchte (also alle<br />

Stühle leer sind), setzt sich der Friseur auf den Friseurstuhl und<br />

schläft.<br />

• Ein eintreffender Kunde muss den Friseur wecken, um die Haare<br />

geschnitten zu bekommen.<br />

• Während der Friseur Haare schneidet, müssen weitere Kunden<br />

entweder Platz nehmen und warten, bis der Friseur frei ist oder<br />

später nochmal kommen (falls alle Stühle besetzt sind).<br />

Lösung mit Semaphoren oder Java Synchronisation als Übung.<br />

150 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

Das Problem des schlafenden Friseurs<br />

Lösungsidee<br />

Mittels Semaphore kann man das Problem lösen:<br />

customers Anzahl der Wartenden Kunden<br />

barbers Anzahl schlafender Frisöre<br />

mutex für den Schutz der Zählvariablen für wartende Kunden<br />

1 # define chairs 5 // # chairs for waiting customers<br />

2 typedef int semaphore ; // use your imagination<br />

3 semaphore customers = 0; // # customers waiting for service<br />

4 semaphore barbers = 0; // # barbers waiting for customers<br />

5 int waiting = 0; // customers are waiting , not being cut<br />

151 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

IPC Probleme<br />

Das Problem des schlafenden Friseurs<br />

Frisör, Kunde<br />

1 void barber ( void ) {<br />

2 while ( true ) {<br />

3 down (& customers ); // go to sleep if # customers =0<br />

4 down (& mutex ); // acquire accesss to waiting<br />

5 waiting - -; // dec . cout of waiting customers<br />

6 up (& barbers ); // one barber is now ready to cut hairs<br />

7 up (& mutex ); // release waiting<br />

8 cut_hair ();<br />

9 }<br />

10 }<br />

11 void customer ( void ) {<br />

12 down (& mutex ); // enter critic region<br />

13 if ( waiting < chairs ) { // if there are no free chairs , leave<br />

14 waiting ++;<br />

15 up (& customers ); // wake up barber if necessary<br />

16 up (& mutex ); // release access to waiting<br />

17 down (& barbers ); // go to sleep if # of free barbers is 0<br />

18 get_haircut ();<br />

19 } else<br />

20 up (& mutex ); // shop is full ; do not wait<br />

21 }<br />

152 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Scheduling<br />

1 Prozessmodell<br />

2 Interprozesskommunikation<br />

3 IPC Probleme<br />

4 Scheduling<br />

Round-Robin Scheduling<br />

Scheduling mit Prioritäten<br />

Shortest-Job-First<br />

Garantiertes Scheduling<br />

Zweistufiges Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

153 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Scheduling<br />

Der Teil des Betriebssystems, der entscheidet,<br />

• welcher der <strong>Prozesse</strong>,<br />

• die im Zustand bereit“ sind,<br />

”<br />

• ausgewählt wird und dann<br />

• den Prozessor zugeteilt bekommt,<br />

heißt Scheduler.<br />

Das Verfahren, wie ausgewählt wird, nennt man Scheduling-Algorithmus.<br />

154 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Kriterien für Scheduling-Algorithmus<br />

Fairness Jeder Prozess wird gleichermaßen berücksichtigt; es gibt<br />

keine privilegierten <strong>Prozesse</strong>.<br />

Effizienz Der Prozessor wird stets vollständig ausgelastet.<br />

Antwortzeit Die Antwortzeit für die interaktiv arbeitenden Benutzer<br />

wird minimiert.<br />

Verweilzeit Die Zeit, die auf Ausgabe von Stapelaufträge gewartet<br />

werden muss, ist minimal.<br />

Durchsatz Die Anzahl der Jobs, die in einem gegebenen Zeitintervall<br />

ausgeführt werden, wird maximiert.<br />

Ressourcenbedarf Der für die Implementierung benötigte<br />

Ressourcenbedarf ist minimal.<br />

Nicht alle Kriterien sind gleichzeitig zu erfüllen, da sie teilweise<br />

widersprüchlich sind (z.B. Antwortzeit – Verweilzeit).<br />

155 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Kategorisierung<br />

Ein Betriebssystem nutzt den Unterbrechungsmechanismus moderner<br />

CPU’s, um sicher zu stellen, dass kein Prozess zu lange ausgeführt wird:<br />

Wenn die Hardware einen Interrupt (etwa 100 mal pro Sekunde)<br />

ausgelöst hat, erhält das Betriebssystem die Kontrolle und der<br />

Scheduler wählt einen neuen Prozess aus.<br />

Je nachdem, wie der Scheduler die Auswahl der <strong>Prozesse</strong> vornimmt,<br />

werden Scheduling-Algorithmen unterschieden in:<br />

• preemptive Scheduling:<br />

rechnende <strong>Prozesse</strong> können unterbrochen werden.<br />

• run to completion (non-preemptive) Scheduling:<br />

der rechnende Prozess wird nicht unterbrochen.<br />

156 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Round-Robin Scheduling<br />

Round-Robin Scheduling<br />

Ein einfaches und häufig angewendetes Verfahren ist das Round-Robin<br />

Verfahren:<br />

• Jeder Prozess erhält eine Zeitscheibe (Quantum), die er maximal<br />

für die Ausführung zugeteilt bekommt.<br />

• Ist diese Zeit abgelaufen, wird ihm der Prozessor entzogen und<br />

• ein anderer Prozess wird ausgewählt.<br />

• Bei Blockierung wg. E/A wird dem Prozess der Prozessor entzogen,<br />

auch wenn sein Quantum noch nicht abgelaufen ist.<br />

Zur Implementierung verwaltet der Scheduler eine Queue mit<br />

rechenbereiten <strong>Prozesse</strong>n, wobei ein aktiver Prozess nach Ablauf des<br />

Quantums wieder hinten in die Liste eingereiht wird:<br />

157 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Round-Robin Scheduling<br />

Dauer für das Quantum<br />

Eine Implementierung dieses Verfahrens muss eine “richtige” Wahl für die<br />

Dauer des Quantums finden:<br />

Angenommen, ein Kontextwechsel dauert 5 Millisekunden.<br />

• Quantum=20 Millisekunden<br />

→ 5/20 ∗ 100 = 25% Verwaltungsaufwand sind zu viel.<br />

• Quantum=500 Millisekunden<br />

→ 5/500 ∗ 100 = 1% Verwaltungsaufwand bedeutet, dass u.U. ein<br />

Benutzer zu lange warten (5 Sekunde, wenn 10 Benutzer gleichzeitig<br />

arbeiten) muss, bevor seine Anfrage (Tastendruck während<br />

Editorsitzung) bearbeitet wird.<br />

158 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Round-Robin Scheduling<br />

Dauer für das Quantum<br />

Folgerung:<br />

• Quantum zu klein → wegen häufiger Kontextwechsel sinkt<br />

Prozessorausnutzung<br />

• Quantum zu gross → schlechte Antwortzeiten bei vielen kurzen<br />

Anfragen<br />

• Erfahrungswert: Quantum=50 Millisekunden ist guter Kompromiss<br />

E/A intensive <strong>Prozesse</strong>, die häufig vor Ablauf des Quantums eine<br />

blockierende Systemfunktion aufrufen und damit den Prozessor entzogen<br />

bekommen, werden durch Round-Robin benachteiligt.<br />

159 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Round-Robin Scheduling<br />

Dauer für das Quantum<br />

In Unix/Linux kann das Quantum des aktuellen <strong>Prozesse</strong>s wie folgt<br />

ermittelt werden (in C):<br />

getQuantum.c<br />

1 # include <br />

2 # include < unistd .h><br />

3 # include ←<br />

4 /*<br />

5 struct timespec {<br />

6 time_t tv_sec ;<br />

7 long tv_nsec ; // Nanosekunden =10** -9 Sekunden<br />

8 } ;<br />

9 */<br />

10 int main () {<br />

11 struct timespec t;<br />

12 if ( sched_rr_get_interval ( getpid () ,&t )==0) ←<br />

13 printf (" sec : %d millisec : %ld\n", t. tv_sec , t. tv_nsec /1000000);<br />

14 }<br />

$ getQuantum<br />

sec : 0 millisec : 24<br />

$<br />

160 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Scheduling mit Prioritäten<br />

Scheduling mit Prioritäten<br />

Die Grundidee des Prioritäts-Scheduling ist:<br />

• Jeder Prozess bekommt eine Priorität zugewiesen.<br />

• Es wird immer der rechenbereite Prozess mit der höchsten Priorität<br />

ausgeführt.<br />

Problem:<br />

Wird kein weiterer Mechanismus realisiert, so kommt es vor,<br />

dass <strong>Prozesse</strong> mit hoher Priorität verhindern, dass ein Prozess<br />

mit niedriger Priorität je den Prozessor bekommen<br />

(Verhungern).<br />

161 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Scheduling mit Prioritäten<br />

Scheduling mit Prioritäten<br />

Deshalb wird folgender Mechanismus als Lösung implementiert:<br />

• Der Scheduler verringert die Priorität des rechnenden <strong>Prozesse</strong>s bei<br />

jedem Interrupt, der durch die interne Prozessoruhr ausgelöst wird.<br />

• Damit fällt die Priorität des laufenden <strong>Prozesse</strong>s irgendwann unter<br />

die Priorität eines anderen rechenbereiten <strong>Prozesse</strong>s;<br />

• in diesem Fall wird ein solcher rechenbereiter Prozess mit höherer<br />

Priorität ausgewählt.<br />

162 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Scheduling mit Prioritäten<br />

Prioritäten können statisch oder dynamisch vergeben werden.<br />

statisch Die Priorität wird vor dem Prozessstart festgelegt. Z.B.<br />

kann man folgende Regel anwenden:<br />

Batchprozesse < Systemprozesse < Interaktive <strong>Prozesse</strong> <<br />

...<br />

dynamisch Dynamische Vergabe von Prioritäten wird häufig realisiert,<br />

um Systemziele zu erreichen.<br />

Z.B. Optimierung des I/O-Verhaltens: dann muss ein<br />

Prozess, der bei E/A Ereignis rechenbereit wird, hohe<br />

Priorität bekommen, damit das E/A Ereignis behandelt<br />

werden kann.<br />

163 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Scheduling mit Prioritäten<br />

In modernen <strong>Betriebssysteme</strong>n werden <strong>Prozesse</strong> in Klassen eingeteilt,<br />

wobei<br />

• zwischen den Klassen Prioritäts-Scheduling und<br />

• innerhalb der Klasse Round-Robin Scheduling<br />

verwendet wird.<br />

164 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Scheduling mit Prioritäten<br />

Thread Prioritäten in Java<br />

Die JVM soll plattformunabhängig implementierbar sein. Daher gibt<br />

es in Java keine Vorgaben bzgl. des Scheduling.<br />

Methoden, mit denen die Priorität eines Thread beeinflusst werden kann:<br />

• Jeder Thread hat eine Priorität zwischen MIN PRIORITY und<br />

MAX PRIORITY ([1,10]).<br />

• Wird ein neuer Thread erzeugt, erbt er die Priorität vom Vater.<br />

• Der Defaultwert, wenn ein Thread aus main() erzeugt wurde, ist<br />

Thread.NORM PRIORITY (5).<br />

• Die Methode getPriority() liefert die aktuelle Priorität eines Threads.<br />

• Mit setPriority()kann die Priorität verändert werden.<br />

165 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Scheduling mit Prioritäten<br />

Thread Prioritäten in Java<br />

Achtung:<br />

• Prioritäten beeinflussen weder Semantik noch Korrektheit eines<br />

Java Programms.<br />

• Prioritäten dürfen beim Programmieren nicht als Ersatz für Locking<br />

verwendet werden!<br />

• Prioritäten dürfen nur verwendet werden, um die relative<br />

Wichtigkeit unterschiedlicher Threads untereinander zu definieren.<br />

166 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Scheduling mit Prioritäten<br />

3 Threads mit unterschiedlicher Priorität in Java I<br />

ThreadPriority.java<br />

1 import java . util .*;<br />

2 import java .io .*;<br />

4 class ThreadPriority {<br />

5 public static void main ( String [] args ) {<br />

6 ThreadPriority t = new ThreadPriority ();<br />

7 t. doIt ();<br />

8 }<br />

10 public void doIt () {<br />

11 MyThread t1 = new MyThread (" Thread One : ");<br />

12 t1. setPriority (t1. getPriority () -4); // Default is 5<br />

13 MyThread t2 = new MyThread (" Thread Two : ");<br />

14 MyThread t3 = new MyThread (" Thread Three : ");<br />

15 t3. setPriority (10);<br />

16 t1. start ();<br />

17 t3. start ();<br />

18 t2. start ();<br />

19 }<br />

20 }<br />

21<br />

167 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Scheduling mit Prioritäten<br />

3 Threads mit unterschiedlicher Priorität in Java II<br />

ThreadPriority.java<br />

22 class MyThread extends Thread {<br />

23 static String spacerString ="";<br />

24 public String filler ;<br />

26 public MyThread ( String ThreadNameIn ) {<br />

27 filler = spacerString ;<br />

28 spacerString = spacerString + " ";<br />

29 setName ( ThreadNameIn );<br />

30 }<br />

32 public void run () {<br />

33 for ( int k =0; k < 5; k ++) {<br />

34 System . out . println ( filler +<br />

35 Thread . currentThread (). getName () +<br />

36 " Iteration = " + Integer . toString (k) ) ;<br />

37 }<br />

38 }<br />

39 }<br />

168 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Scheduling mit Prioritäten<br />

3 Threads mit unterschiedlicher Priorität in Java<br />

ThreadPriority.java<br />

$ java ThreadPriority<br />

Thread Three : Iteration = 0<br />

Thread Three : Iteration = 1<br />

Thread Three : Iteration = 2<br />

Thread Three : Iteration = 3<br />

Thread Three : Iteration = 4<br />

Thread One : Iteration = 0<br />

Thread One : Iteration = 1<br />

Thread One : Iteration = 2<br />

Thread One : Iteration = 3<br />

Thread One : Iteration = 4<br />

Thread Two : Iteration = 0<br />

Thread Two : Iteration = 1<br />

Thread Two : Iteration = 2<br />

Thread Two : Iteration = 3<br />

Thread Two : Iteration = 4<br />

Versuche Sie das Programm auf Ihrem Rechner.<br />

Ist die Ausgabe genau so ?<br />

169 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Scheduling mit Prioritäten<br />

Prozessprioritäten in Unix/Linux<br />

In Linux ist die Priorität eines <strong>Prozesse</strong>s ein Integer im Bereich -20 bis 20<br />

(-20 ist höchste Priorität, Default ist 0).<br />

Das folgende Programm zeigt, wie die Priorität eines <strong>Prozesse</strong>s gesetzt<br />

und abgefragt werden kann: getPriority.c<br />

1 # include <br />

2 # include <br />

3 # include // wg. PRIO_USER<br />

4 # include <br />

6 extern int errno ;<br />

7 int main () {<br />

8 int prio = 0;<br />

9 if ( setpriority ( PRIO_USER , 0, 10) != 0) { ←<br />

10 fprintf ( stderr , " Error setpriority : %s\n", strerror ( errno ));<br />

11 exit (1);<br />

12 }<br />

13 printf ("%d\n", getpriority ( PRIO_USER , 0)); ←<br />

14 exit (0);<br />

15 }<br />

170 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Shortest-Job-First<br />

Shortest-Job-First<br />

Die beiden letzten Verfahren sind für interaktive Systeme entworfen<br />

worden.<br />

Ein Verfahren für Systeme, bei denen die Ausführungszeiten der<br />

einzelnen <strong>Prozesse</strong> im voraus bekannt sind, benutzt eine Warteschlange<br />

für die gleichgewichtigen <strong>Prozesse</strong>.<br />

Der Scheduler wählt den Prozess mit der kürzesten<br />

Ausführungszeit zuerst.<br />

Gemäss dieser Methode wird die durchschnittliche Verweilzeit (Zeit,<br />

die der Prozess im System verweilt ) von <strong>Prozesse</strong>n minimiert:<br />

171 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Shortest-Job-First<br />

Gegeben seien 4 <strong>Prozesse</strong> mit folgenden Ausführungszeiten:<br />

A 8 Sekunden<br />

B 6 Sekunden<br />

C 4 Sekunden<br />

D 6 Sekunden<br />

Ausführungsreihenfolge A, B, C, D:<br />

Verweilzeit<br />

A 8<br />

B 8 + 6 = 14<br />

C 8 + 6 + 4 = 18<br />

D 8 + 6 + 4 + 6 = 24<br />

→ durchschnittliche Verweilzeit = (8 + 14 + 18 + 24)/4 = 16<br />

172 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Shortest-Job-First<br />

Gegeben seien 4 <strong>Prozesse</strong> mit folgenden Ausführungszeiten:<br />

A 8 Sekunden<br />

B 6 Sekunden<br />

C 4 Sekunden<br />

D 6 Sekunden<br />

Ausführungsreihenfolge C, B, C, A:<br />

Verweilzeit<br />

C 4<br />

B 4 + 6 = 10<br />

D 4 + 6 + 6 = 16<br />

A 4 + 6 + 6 + 8 = 24<br />

→ durchschnittliche Verweilzeit = (4 + 10 + 16 + 24)/4 = 13, 5<br />

173 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Shortest-Job-First<br />

Behauptung:<br />

Den Prozess mit der kürzesten Ausführungszeit zu wählen ist<br />

optimal bzgl. der mittleren Verweilzeit.<br />

Beweis:<br />

Gegeben seien 4 <strong>Prozesse</strong> A-D mit den jeweiligen Verweilzeiten<br />

a, b, c, d, die in der Reihenfolge A,B,C,D ausgeführt werden.<br />

Verweilzeit<br />

A a<br />

B a + b<br />

C a + b + c<br />

D a + b + c + d<br />

→ durchschnittliche Verweilzeit = (4a + 3b + 2c + d)/4<br />

d.h. a beeinflusst die durchschnittliche Verweilzeit am meisten,<br />

gefolgt von b und c; daraus folgt:<br />

die durchschnittliche Verweilzeit ist minimal, wenn zuerst A der<br />

Prozess mit der kleinsten Ausführungszeit ist, B der Prozess mit<br />

der zweit kleinsten Ausführungszeit, etc<br />

174 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Shortest-Job-First<br />

Ausführungszeit bestimmen<br />

Das Problem bei interaktiven <strong>Betriebssysteme</strong>n ist es, die<br />

Ausführungszeit der <strong>Prozesse</strong> zu bestimmen.<br />

Idee:<br />

Schätze die Ausführungszeit auf Basis der gemessenen Zeit<br />

der Vergangenheit M.<br />

• Der Scheduler wählt dann den Prozess mit der kürzesten<br />

geschätzten Zeit.<br />

• Der neue geschätzte Wert S zum Zeitpunkt n+1 wird aus<br />

dem gewichteten Durchschnitt des aktuellen und des<br />

vorherigen Wertes gebildet.<br />

S n+1 = α ∗ M n + (1 − α) ∗ S n<br />

α(0 ≤ α ≤ 1) ist dabei der Faktor, der den Einfluss der<br />

zurückliegenden Periode auf die Schätzung angibt; Werte<br />

nahe 1 ordnen der geschätzten Vergangenheit wenig<br />

Stellenwert zu.<br />

175 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Shortest-Job-First<br />

Hörsaalübung<br />

Gegeben seien 5 <strong>Prozesse</strong> mit folgenden Ausführungszeiten:<br />

A 6 Sekunden<br />

B 6 Sekunden<br />

C 8 Sekunden<br />

D 2 Sekunden<br />

E 4 Sekunden<br />

Welche Ausführungsreihenfolge führt zur optimalen durchschnittlichen<br />

Verweilzeit?<br />

Prozess<br />

A<br />

B<br />

C<br />

D<br />

E<br />

Verweilzeit<br />

→ durchschnittliche Verweilzeit =<br />

176 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Garantiertes Scheduling<br />

Garantiertes Scheduling<br />

Beim garantierten Scheduling wird einem Prozess eine gewisse<br />

Performance versprochen und die Einhaltung garantiert.<br />

Mögliche Versprechen:<br />

• Bei interaktiven Systemen mit n Benutzern, erhält jeder 1/n der<br />

verfügbaren Prozessorzeit<br />

• Bei Echtzeitsystemen wird vom System garantiert, dass ein<br />

Prozess innerhalb einer Zeitschranke abgeschlossen ist.<br />

177 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Garantiertes Scheduling<br />

interaktive Systeme<br />

Zu jeder Benutzersitzung sei die insgesamt erhaltene Prozessorzeit seit<br />

Sitzungsbeginn e.<br />

Beispiel<br />

Der Scheduler berechnet die dem Benutzer zustehende Zeit z<br />

und ermittelt das Verhältnis von erhaltener Zeit e und<br />

zustehender Zeit z.<br />

Das Verhältnis 0,5 bedeutet, dass der Prozess halb so viel Zeit<br />

verbraucht hat, wie ihm garantiert wurde.<br />

Der Scheduler wählt immer den Prozess, mit dem<br />

berechneten kleinsten Verhältnis e/z.<br />

Prozess erhaltene versprochene e/z ausgewählter<br />

Zeit e Zeit z Prozess<br />

A 14 (14+11+20)/3=15 14/15=0,93<br />

B 11 (14+11+20)/3=15 11/15=0,73 X<br />

C 20 (14+11+20)/3=15 20/15=1,33<br />

178 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Garantiertes Scheduling<br />

Echtzeit-Systeme<br />

Bei Echtzeitsystemen wird vom System garantiert, dass ein Prozess<br />

innerhalb einer Zeitschranke abgeschlossen ist.<br />

Beispiel<br />

Der Scheduler wählt den Prozess, bei dem die Gefahr am<br />

grössten ist, dass die Zeitschranke nicht eingehalten werden<br />

kann.<br />

Prozess zugesicherte bisher er- Dauer bis Ablauf ausgewählter<br />

Zeitschranke haltene Zeit der Zeitschranke Prozess<br />

A 14 10 14-10=4<br />

B 11 9 11-9=2 X<br />

C 20 15 20-15=5<br />

179 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Garantiertes Scheduling<br />

Hörsaalübung<br />

1. Ein interaktives System sei geben durch:<br />

Prozess erhaltene versprochene e/z ausgewählter<br />

Zeit e Zeit z Prozess<br />

A 8<br />

B 2<br />

C 2<br />

D 2<br />

2. Ein Echtzeitsystem sei gegeben durch:<br />

Prozess zugesicherte bisher er- Dauer bis Ablauf ausgewählter<br />

Zeitschranke haltend Zeit der Zeitschranke Prozess<br />

A 4 1<br />

B 2 1<br />

C 2 1<br />

D 6 2<br />

In welcher Reihenfolge werden die <strong>Prozesse</strong> in beiden Systemen<br />

ausgewählt (jeweils 5 Kontextwechsel, Quantum 1 Sekunde)?<br />

180 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Zweistufiges Scheduling<br />

Zweistufiges Scheduling<br />

Die bisherigen Scheduling-Verfahren haben gemeinsam, dass alle<br />

rechenbereiten <strong>Prozesse</strong> im Hauptspeicher ablaufen.<br />

• Wenn nicht genug Hauptspeicher verfügbar ist, müssen einige dieser<br />

<strong>Prozesse</strong> auf der Festplatte abgelegt werden.<br />

• Der Aufwand für einen Prozesswechsel ist aber höher, wenn ein<br />

Prozess zunächst in den Hauptspeicher zurückgeladen werden muss.<br />

181 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Zweistufiges Scheduling<br />

Ein zweistufiges Verfahren teilt die<br />

rechenbereiten <strong>Prozesse</strong> in 2 disjunkte<br />

Teilmengen:<br />

• <strong>Prozesse</strong> im Hauptspeicher H<br />

• <strong>Prozesse</strong> auf Platte (Sekundärspeicher<br />

S)<br />

Es existieren zwei Scheduler:<br />

• lokaler Scheduler wählt stets einen<br />

Prozess aus H<br />

• periodisch lagert ein globaler Scheduler<br />

<strong>Prozesse</strong> von H und S aus und ersetzt<br />

sie.<br />

182 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Zweistufiges Scheduling<br />

Als Verfahren für den lokalen Scheduler<br />

kommt einer der vorherigen Algorithmen in<br />

Frage.<br />

Der globale Scheduler kann folgende<br />

Kriterien zur Auswahl des Plattenspeicher<br />

<strong>Prozesse</strong>s, der ausgetauscht werden soll:<br />

• Wie lange ist der Prozess schon in S?<br />

• Wie groß ist der Prozess in S (es<br />

passen mehrere kleine in den<br />

Hauptspeicher)?<br />

• Priorität des <strong>Prozesse</strong>s in S?<br />

183 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Fallbeispiel: Linux Scheduler<br />

Die Beschreibung basiert auf dem Beitrag ”<br />

Der O(1)-Scheduler im Kernel<br />

2.6“ von Timo Hönig im Linux-Magazin.<br />

184 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Prozessverwaltung<br />

• Linux verwaltet alle Prozessinformationen<br />

mit Hilfe einer doppelt verketteten Liste -<br />

der Taskliste.<br />

• Die Listenelemente sind die<br />

Prozessdeskriptoren (≫task struct≪) der<br />

<strong>Prozesse</strong>.<br />

• Der Deskriptor hält alle Informationen<br />

seines <strong>Prozesse</strong>s fest (im Wesentlichen,<br />

das was man mit ps sieht).<br />

185 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Den Zustand eines <strong>Prozesse</strong>s speichert die Variable ≫state≪ des<br />

Prozessdeskriptors.<br />

Der Scheduler kennt insgesamt fünf Zustände:<br />

1 TASK RUNNING kennzeichnet den Prozess als lauffähig.<br />

Er muss auf kein Ereignis warten und kann daher vom Scheduler der<br />

CPU zugeordnet werden. Alle <strong>Prozesse</strong> im Zustand<br />

≫TASK RUNNING≪ zieht der Scheduler für die Ausführung in<br />

Betracht.<br />

2 TASK INTERRUPTIBLE kennzeichnet blockierte <strong>Prozesse</strong>.<br />

Der Prozess wartet auf ein Ereignis. Ein Prozess im Zustand<br />

TASK INTERRUPTIBLE wird über zwei unterschiedliche Wege in<br />

den Zustand TASK RUNNING versetzt:<br />

1 Entweder tritt das Ereignis ein, auf das er gewartet hat,<br />

2 oder der Prozess wird durch ein Signal aufgeweckt.<br />

186 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

3 TASK UNINTERRUPTIBLE gleicht dem Zustand<br />

TASK INTERRUPTIBLE, mit dem Unterschied, dass ein Signal den<br />

Prozess nicht aufwecken kann.<br />

Der Zustand TASK UNINTERRUPTIBLE wird nur verwendet, wenn<br />

zu erwarten ist, dass das Ereignis, auf das der Prozess wartet, zügig<br />

eintritt, oder wenn der Prozess ohne Unterbrechung warten soll.<br />

4 Wurde ein Prozess beendet, dessen Elternprozess noch nicht den<br />

Systemaufruf ≫wait4()≪ ausgeführt hat, verbleibt er im Zustand<br />

TASK ZOMBIE. So kann auch nach dem Beenden eines<br />

Kindprozesses der Elternprozess noch auf seine Daten zugreifen.<br />

Nachdem der Elternprozess ≫wait4()≪ aufgerufen hat, wird der<br />

Kindprozess endgültig beendet, seine Datenstrukturen werden<br />

gelöscht. Endet ein Elternprozess vor seinen Kindprozessen,<br />

bekommt jedes Kind einen neuen Elternprozess zugeordnet. Dieser<br />

ist nunmehr dafür verant- wortlich, ≫wait4()≪ aufzurufen, sobald<br />

der Kindprozess beendet wird. Ansonsten könnten die Kindprozesse<br />

den Zustand TASK ZOMBIE nicht verlassen und würden als Leichen<br />

im Hauptspeicher zurückbleiben.<br />

187 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

5 Den Zustand TASK STOPPED<br />

erreicht ein Prozess, wenn er beendet<br />

wurde und nicht weiter ausführbar ist.<br />

In diesen Zustand tritt der Prozess ein,<br />

sobald er eines der Signale<br />

≫SIGSTOP≪, ≫SIGTST≪,<br />

≫SIGTTIN≪ oder ≫SIGTTOU≪ erhält.<br />

188 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Entwicklungsziele für den Scheduler<br />

Neben den allgemeinen Zielen (Auslastung der CPU, ...) waren hier<br />

zusätzlich folgende Punkte maßgebend:<br />

• gute SMP-Skalierung<br />

• geringe Latenz auch bei hoher Systemlast<br />

• faire Prioritätenverteilung<br />

• Komplexität der Ordnung O(1)<br />

Alle Linux-Scheduler bisher besaßen eine Komplexität der Ordnung O(n): Die<br />

Kosten für das Scheduling wuchsen linear mit der Anzahl n der lauffähigen<br />

<strong>Prozesse</strong>. Bei einem Kontextwechsel wird in einer verketteten Liste nach einem<br />

Prozess gesucht, dessen Priorität am niedrigsten ist.<br />

189 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Prozessprioritäten<br />

Die Prozessprioritäten entscheiden, welchen lauffähigen Prozess die CPU<br />

beim nächsten Kontextwechsel zugeteilt bekommt - den mit der zum<br />

Zeitpunkt des Kontextwechsels höchsten Priorität. Die Priorität eines<br />

<strong>Prozesse</strong>s ändert sich dabei dynamisch während seiner Laufzeit.<br />

Es gibt zwei unterschiedliche Prioritäten:<br />

• die statische Prozesspriorität, also die vom ≫nice≪-Wert bestimmte<br />

≫static prio≪<br />

Der Wertebereich des ≫nice≪-Values reicht von -20 (dem höchsten)<br />

bis 19 (dem niedrigsten).<br />

• die dynamische (effektive) Prozesspriorität (≫prio≪), die der<br />

Scheduler aufgrund der Interaktivität eines <strong>Prozesse</strong>s berechnet.<br />

190 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Linux 2.6 kennt standardmäßig 140<br />

Prioritätslevels.<br />

• Hierbei entspricht null der höchsten<br />

und 139 der niedrigsten Priorität.<br />

• Die Levels von eins bis 99 sind für<br />

Tasks mit Echtzeitpriorität reserviert.<br />

• Alle anderen <strong>Prozesse</strong> erhalten<br />

zunächst gemäß ihres ≫nice≪-Werts<br />

eine Priorität: Der ≫nice≪-Wert (-20<br />

bis 19) wird einfach in den Bereich ab<br />

101 gemappt.<br />

• Während des Ablaufs eines <strong>Prozesse</strong>s<br />

verändert sich durch seinen<br />

Interaktivitätsgrad aber seine Priorität.<br />

191 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Alle lauffähigen <strong>Prozesse</strong> verwaltet der Scheduler in einer Run-Queue (pro<br />

CPU). Sie ist die zentrale Datenstruktur, auf der der Scheduler arbeitet.<br />

struct runqueue {<br />

/* Spinlock um Run - Queue zu schützen */<br />

spinlock_t lock ;<br />

/* Zahl der lauffähigen <strong>Prozesse</strong> */<br />

unsigned long nr_running ;<br />

/* Zahl der bisherigen Kontextwechsel */<br />

unsigned long nr_switches ;<br />

/* Zeitstempel des letzten Tauschs von active - und expired - Array */<br />

unsigned long expired_timestamp ;<br />

/* Zahl der Prozess im Zustand TASK_UNINTERRUPTIBLE */<br />

unsigned long nr_uninterruptible ;<br />

/* Verweis auf Prozessdeskriptor des momentan ablaufenden <strong>Prozesse</strong>s */<br />

task_t * curr ;<br />

/* Verweis auf Prozessdeskriptor der Idle - Task */<br />

task_t * idle ;<br />

/* Verweis auf Memory Map des zuletzt ablaufenden <strong>Prozesse</strong>s */<br />

struct mm_struct * prev_mm ;<br />

/* Verweise auf active - und expired - Array */<br />

prio_array_t * active , * expired ;<br />

/* Priority - Arrays */<br />

prio_array_t arrays [2];<br />

... }<br />

192 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Neben Verweisen auf die gerade laufende Task, enthält die Run-Queue<br />

Verweise zu den zwei Priority-Arrays ≫active≪ und ≫expired≪.<br />

struct prio_array {<br />

int nr_active ; /* Zahl der <strong>Prozesse</strong> */<br />

unsigned long bitmap [ BITMAP_SIZE ]; /* Priorität - Bitmap */<br />

/* Für jede Priorität eine Liste der <strong>Prozesse</strong> */<br />

struct list_head queue [ MAX_PRIO ];<br />

};<br />

• Das ≫active≪-Array listet alle lauffähigen <strong>Prozesse</strong>, deren<br />

Zeitscheibe noch nicht abgelaufen ist.<br />

• Wenn die Zeitscheibe eines <strong>Prozesse</strong> abläuft, verschiebt der<br />

Scheduler den Eintrag vom ≫active≪- in das zweite Array<br />

≫expired≪.<br />

• Beide, das ≫active≪- und das ≫expired≪-Array, führen für jede<br />

Priorität eine verkettete Liste der <strong>Prozesse</strong> mit entsprechender<br />

Priorität.<br />

• Eine Bitmap hält fest, für welche Priorität mindestens eine Task<br />

existiert. Alle Bits werden bei der Initialisierung auf null gesetzt.<br />

Beim Eintragen eines <strong>Prozesse</strong>s in eines der beiden Priority-Arrays,<br />

wechselt entsprechend der Priorität des <strong>Prozesse</strong>s das<br />

korrespondierende Bit im Priorität-Bitmap auf eins.<br />

193 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

• Startet ein Prozess mit dem Nice null, setzt<br />

der Scheduler das 120. Bit des Priorität-Bitmaps<br />

im ≫active≪-Array und reiht ihn in die<br />

Prozessliste mit Priorität 120 ein.<br />

• Analog dazu löscht sich das entsprechende Bit<br />

im Priorität-Bitmap, sobald der Scheduler den<br />

letzten Prozess einer gegebenen Priorität aus<br />

einem der beiden Priority-Arrays austrägt.<br />

• Der Scheduler muss nur das erste gesetzte Bit<br />

des Priorität-Bitmaps finden (da Bitmap<br />

konstante Größe hat folgt O(1)). Anschließend<br />

führt der Scheduler den ersten Prozess aus der<br />

verketteten Liste dieser Priorität aus. <strong>Prozesse</strong><br />

gleicher Priorität bekommen die CPU<br />

nacheinander in einem Ringverfahren (Round<br />

Robin) zugeteilt.<br />

194 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Prozess-Zeitscheiben und Neuberechnung der Prioritäten<br />

Die Größe der Zeitscheibe eines <strong>Prozesse</strong>s ist von seiner Priorität<br />

abhängig:<br />

• <strong>Prozesse</strong> mit hoher Priorität erhalten mehr CPU-Zeit als solche<br />

mit niedriger.<br />

• Die kleinste Zeitscheibe beträgt 10, die längste 200 Millisekunden.<br />

Ein Prozess mit dem ≫nice≪-Wert null erhält die Standard-<br />

Zeitscheibe von 100 Millisekunden.<br />

Ist die Zeitscheibe eines <strong>Prozesse</strong>s aufgebraucht, muss der Scheduler<br />

sie neu berechnen und den Prozess aus dem ≫active≪- in das<br />

≫expired≪-Array verschieben. Sobald ≫active≪ leer ist - alle<br />

lauffähigen <strong>Prozesse</strong> haben ihre Zeitscheibe aufgebraucht -, tauscht der<br />

Scheduler einfach das ≫active≪- gegen das ≫expired≪-Array aus.<br />

Effektiv wechseln nur die zwei Pointer der Run-Queue die Plätze.<br />

In Linux 2.4 werden die Zeitscheiben aller <strong>Prozesse</strong> auf einmal neu berechnet - immer dann, wenn alle <strong>Prozesse</strong> ihre Zeitscheiben<br />

aufgebraucht haben. Mit steigender Prozesszahl dauert die Berechnung immer länger.<br />

195 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Die dynamische Priorität errechnet der Scheduler aus der statischen<br />

und der Prozessinter- aktivität. Gemäß seiner Interaktivität erhält ein<br />

Prozess vom Scheduler entweder einen Bonus oder ein Penalty (Malus).<br />

Interaktive <strong>Prozesse</strong> gewinnen über einen Bonus maximal fünf<br />

Prioritätslevels hinzu, während jene <strong>Prozesse</strong>, die eine geringe<br />

Interaktivität aufweisen, maximal fünf Prioritätslevels durch ein Penalty<br />

verlieren. Die dynamische Priorität eines <strong>Prozesse</strong>s mit einem<br />

≫nice≪-Wert fünf beträgt demnach im besten Fall null und im<br />

schlechtesten zehn.<br />

196 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Um den Grad der Interaktivität eines <strong>Prozesse</strong>s zu bestimmen, muss<br />

bekannt sein, ob der Prozess eher I/O-lastig oder eher CPU-intensiv ist.<br />

Um <strong>Prozesse</strong> einer der beiden Kategorien zuordnen zu können,<br />

protokolliert der Kernel für jeden Prozess, wie viel Zeit er mit<br />

Schlafen verbringt, und wie lange er die CPU in Anspruch nimmt. Die<br />

Variable ≫sleep avg≪ (Sleep Average) im Prozessdeskriptor speichert<br />

dafür eine Entsprechung in dem Wertebereich von null und zehn<br />

(≫MAX SLEEP AVG≪).<br />

Läuft ein Prozess, verringert seine ≫sleep avg≪ mit jedem<br />

Timer-Tick ihren Wert. Sobald ein schlafender Prozess aufgeweckt wird<br />

und in den Zustand ≫TASK RUNNING≪ wechselt, wird<br />

≫sleep avg≪ entsprechend seiner Schlafzeit erhöht - maximal bis zu<br />

≫MAX SLEEP AVG≪. Der Wert von ≫sleep avg≪ ist somit maßgebend,<br />

ob ein Prozess I/O- oder Processor-Bound ist. Interaktive <strong>Prozesse</strong> haben<br />

eine hohe ≫sleep avg≪, minder interaktive eine niedrige.<br />

197 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Mit der Funktion ≫effective prio()≪ berechnet der Scheduler die<br />

dynamische Priorität ≫prio≪ basierend auf der statischen<br />

≫static prio≪ und der Interaktivität ≫sleep avg≪ des <strong>Prozesse</strong>s. Zum<br />

Berechnen der neuen Zeitscheibe greift der Scheduler auf die dynamische<br />

Prozesspriorität zurück. Dazu mappt er den Wert in den<br />

Zeitscheibenbereich ≫MIN TIMESLICE≪ (Default: 10 Millisekunden) bis<br />

≫MAX TIMESLICE≪ (200 Millisekunden).<br />

Interaktive <strong>Prozesse</strong> mit hohem Bonus und großer Zeitscheibe können<br />

ihre Priorität jedoch nicht missbrauchen, um die CPU zu blockieren: Da<br />

die Zeit, die der Prozess beim Ausführen im Zustand<br />

≫TASK RUNNING≪ verbringt, in die Berechnung der<br />

≫sleep avg≪-Variablen eingeht, verliert solch ein Prozess schnell seinen<br />

Bonus und mit ihm seine hohe Priorität und seine große Zeitscheibe.<br />

Ein Prozess mit sehr hoher Interaktivität erhält nicht nur eine hohe<br />

dynamische Priorität und somit eine große Zeitscheibe: Der Scheduler<br />

trägt den Prozess nach Ablauf seiner Zeitscheibe auch wieder sofort in<br />

das ≫active≪-Array ein, statt wie gewöhnlich ins ≫expired≪-Array. Der<br />

Prozess wird dadurch seiner Priorität gemäß wieder zugeordnet und muss<br />

nicht auf das Austauschen der Priority-Arrays warten.<br />

198 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Kontextwechsel<br />

Alle blockierten <strong>Prozesse</strong> werden in den so genannten Wait-Queues<br />

verwaltet. <strong>Prozesse</strong>, die von ≫TASK RUNNING≪ in den Zustand<br />

≫TASK INTERRUPTIBLE≪ oder ≫TASK<br />

UNINTERRUPTIBLE≪ wechseln, gelangen in diese Warteschlange.<br />

Anschließend ruft der Kernel ≫schedule()≪ auf, damit ein anderer<br />

Prozess die CPU erhält.<br />

Sobald das Ereignis eintritt, auf das der Prozess in einer Wait-Queue<br />

wartet, wird er aufgeweckt, wechselt seinen Zustand in<br />

≫TASK RUNNING≪ zurück, verlässt die Wait-Queue und betritt die<br />

Run-Queue. Falls der aufgewachte Prozess eine höhere Priorität besitzt<br />

als der gerade ablaufende, unterbricht der Scheduler den aktuell<br />

laufenden Prozess zugunsten des eben aufgewachten.<br />

199 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Kernel-Preemption<br />

Anders als Kernel 2.4 ist der neue Linux-Kernel preemptiv: Kernelcode,<br />

der gerade ausgeführt wird, kann unterbrochen werden. Vor dem<br />

Unterbrechen muss gewährleistet sein, dass sich der Kernel in einem<br />

Zustand befindet, der eine Neuzuordnung der <strong>Prozesse</strong> zulässt.<br />

Die Struktur ≫thread info≪ jedes <strong>Prozesse</strong>s enthält zu diesem Zweck den<br />

Zähler ≫preempt count≪. Ist er null, ist der Kernel in einem sicheren<br />

Zustand und darf unterbrochen werden. Die Funktion<br />

≫preempt disable()≪ erhöht den Zähler ≫preempt count≪ beim Setzten<br />

eines so genannten Locks um eins; die Funktion<br />

≫preempt enable()≪ erniedrigt ihn um eins, sobald ein Lock aufgelöst<br />

wird. Das Setzten des Locks (und damit das Verbot der<br />

Kernel-Preemption) wird immer dann notwendig, wenn beispielsweise eine<br />

von zwei <strong>Prozesse</strong>n genutzte Variable vor konkurrierenden Zugriffen zu<br />

sichern ist.<br />

200 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Realtimefähigkeit<br />

Für <strong>Prozesse</strong> mit so Echtzeitpriorität (Priorität 1 bis 99) gibt es zwei<br />

Strategien:<br />

• ≫SCHED FIFO≪<br />

ist ein einfacher First-in/First-out-Algorithmus, der ohne<br />

Zeitscheiben arbeitet. Wird ein Echtzeitprozess mit<br />

≫SCHED FIFO≪ gestartet, läuft er so lange, bis er blockiert oder<br />

freiwillig über die Funktion ≫sched yield()≪ den Prozessor abgibt.<br />

Alle anderen <strong>Prozesse</strong> mit einer niedrigeren Priorität sind solange<br />

blockiert und werden nicht ausgeführt.<br />

• ≫SCHED RR≪<br />

verfolgt die gleiche Strategie wie ≫SCHED FIFO≪, aber zusätzlich<br />

mit vorgegebenen Zeitscheiben.<br />

Die CPU-Bedürfnisse der Echtzeitprozesse gleicher Priorität befriedigt der<br />

Scheduler per Round-Robin. <strong>Prozesse</strong> mit einer niedrigeren Priorität<br />

kommen überhaupt nicht zum Zuge. Der Scheduler vergibt für<br />

Echtzeitprozesse keine dynamischen Prioritäten. <strong>Prozesse</strong> ohne<br />

Echtzeitpriorität führt er mit der Strategie ≫SCHED OTHER≪ aus.<br />

201 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Realtimefähigkeit<br />

Die Echtzeit-Strategien von Linux garantieren jedoch keine<br />

Antwortzeiten, was die Voraussetzung für ein hartes<br />

Echtzeit-Betriebssystem wäre.<br />

Der Kernel stellt jedoch sicher, dass ein lauffähiger Echtzeit-Prozess<br />

immer die CPU bekommt, wenn er auf kein Ereignis warten muss, er<br />

freiwillig die CPU abgibt und wenn kein lauffähiger Echtzeitprozess<br />

höherer Priorität existiert.<br />

202 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Leistungsvergleich O(n) vs O(1) Scheduler<br />

Benötigte Zeit zur Interprozess-Kommunikation in Abhängigkeit von der<br />

Anzahl beteiligter <strong>Prozesse</strong> auf einem Singleprozessor-System mit Kernel<br />

2.4 (rot) und 2.6 (grün):<br />

203 / 204


<strong>Betriebssysteme</strong> - <strong>Prozesse</strong> → alois.schuette@h-da.de<br />

Scheduling<br />

Fallbeispiel: Linux Scheduler<br />

Benötigte Zeit zur Interprozess-Kommunikation in Abhängigkeit von der<br />

Anzahl beteiligter <strong>Prozesse</strong> auf Systemen mit ein, zwei, vier und acht<br />

CPUs.<br />

Es ist deutlich zu sehen, dass Linux 2.6 bei steigender Prozesszahl auf<br />

allen vier Systemen ungleich besser skaliert als der alte Kernel.<br />

204 / 204

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!