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