BS Übung 10 (Stefan Bosse) [03.02.2025]
Gruppe und Namen der Mitglieder
Punkte:Total/21./22./23./2

Übung 10: Synchronisation und Nachrichtenaustausch

In dieser Übung sollen:

  1. Kommunikation zwischen Prozessen über Pipes implementiert und evaluiert werden,
  2. Client-Server Kommunikation über Pipes implementiert werden.

Was benötigt wird:

  1. Eine Linux/Unix Entwicklungsumgebung
  2. Der gcc Compiler (und binutils)

Vertiefende Informationen können hier gefunden werden:

  1. Zu der Programmierung von pipes: https://cs4118.github.io/www/2023-1/lect/05-ipc.html
  2. Für die Nutzung der select Funktion: https://www.tutorialspoint.com/unix_system_calls/_newselect.htm

Pipes

Prozesse können über Pipes kommunizieren. Dabei werden wesentliche Aspekte adressiert:

  1. Synchronisation, also Warten auf ein Ereignis mit Prozessblockierung (hier das Lesen aus der Pipe);
  2. Datenübermittlung, also Nachrichtenaustausch;
  3. First-in-First-Out Reihenfolge.

Eine Pipe ist also eine Warteschlange (Queue) mit einer FIFO Reihenfolge. Eine Pipe verwendet zwei Dateideskriptoren mit dem ersten Deskriptor für das Lesen aus und den zweiten Deskriptor für das Schreiben in die Pipe.

Definition 1. Pipes unter Linux/Unix mit zwei Dateideskriptoren erzeugen.
#include <unistd.h>
int fd[2];
int pipe(int fd[2]);
// Returns: 0 if OK, –1 on error

#pipe1

Das Lesen und Schreiben kann mit den read und write Operationen durchgeführt werden. Beide Operationen können den Prozess blockieren. Bei der read Operation ist das der Fall wenn keine Daten in der Pipe (Queue) sind, und bei der write Operation wenn die Queue der Pipe voll ist (Größe des Puffers ist aber unbekannt).

Definition 2. Lesen aus und Schreiben in Pipes.
#include <unistd.h>
int fd[2];
#define READ  0
#define WRITE 1
void foo() {
  char recvbuf[10],sendbuf[10];
  int n=pipe(fd);
  if (n!=0) error();
  ...
  n=write(fd[WRITE],sendbuf,numbytes);
  // Returns number of written bytes or error
  if (n<=0) error();
  ...
  n=read(fd[READ],recvbuf,bufsize);
  // Returns number of read bytes or error
  if (n<=0) error();
}

Dateideskriptoren überwachen

Read und Write Operationen können den Prozess blockieren. Will man aus mehreren Kanälen "parallel" lesen wäre das mit den blockierenden Operationen nicht möglich.

Zum Beispiel soll ein Konsumten aus zwei Pipes Daten lesen. Im Programm wird zuerst die Read Operation auf der ersten Pipe durchgeführt und dann auf der zweiten Pipe. Jetzt hat ein anderer Produzentenprozess Daten in die Pipe 2 geschrieben, jedoch nicht in Pipe 1. Die Daten würden bis dahin nicht aus der Pipe 2 gelesen werden da der Konsument auf Pipe 1 wartet und blockiert.

Wir brauchen eine Möglichkeit mehrere Dateideskriptoren zu überwachen bevor das Programm aus einem Dateideskriptor liest. Das geschieht unter Unix mit der select Operation.

Die Benutzung der select Funktion erfordert etwas Aufwand wie nachfolgend skizziert ist. Die Select Operation blockiert den Prozess solange bis einer der zu überwachenden Dateideskriptoren "bereit" ist, d.h. z.B. Daten zum Lesen hat (wie die Pipe) oder ein Schreiben in einen Puffer wieder möglich (da nicht mehr voll). Weiterhin kann ein Timeout definiert werden nachdem die Select Operation garantiert zurück kehrt (d.h. die Prozessblockierung aufgehoben wird).

Definition 3. Verwendung der select Operation. Das erste Argument gibt den höchsten Dateideskriptor + 1 an (und nicht die Anzahl!). Das zweite Argument ist ein Vektor von Dateideskriptoren die auf zu lesende Daten überwacht werden, das dritte Argument wäre ein Vektor von Dateideskriptoren die auf zu schreibende Daten überwacht werden. Soll es keinen Timeout geben dann ist als fünftes Argument NULL zu übergeben (und die Zeitstruktur timeval wird nicht benötigt).
#define MAX(a,b) (a>b?a:b)
int fdA,fdB,...;
fd_set rfds;
struct timeval tv;
int nfds,retval;
...
FD_ZERO(&rfds);
FD_SET(fdA, &rfds);
FD_SET(fdB, &rfds);
nfds=MAX(fdA,fdB)+1;
/* Wait up to five seconds. */
tv.tv_sec = 5;
tv.tv_usec = 0;
// https://www.tutorialspoint.com/unix_system_calls/_newselect.htm
retval = select(nfds, &rfds, NULL, NULL, &tv);
if (retval<0) wegotatimeout();
if (FD_ISSET(fdA,&rfds)) {
  int n=read(fdA,buf,bufsite);
  ...
}
if (FD_ISSET(fdB,&rfds)) {
  int n=read(fdA,buf,bufsite);
  ...
}

Produzenten-Konsumenten Systeme

Nun wollen wir mehrere Prozesse miteinander kommunizieren lassen. Dazu verwenden wir eine Master-Slave oder auch Produzenten-Konsumenten Architektur:

  1. Der Produzent (i.a. ein Kindprozess) schreibt Daten in eine Pipe;
  2. Ein Kosument (i.a. der Elternprozess) liest die Daten aus einer Pipe um diese zu verarbeiten.

Das wäre unidirektionale Kommunikation. Bidirektionale Kommunikation bedeutet aber die Rücksendung einer Antwort an den anfragenden Prozess. D.h. Produzentenrolle und Komsumentenrolle kehren sich um.

Wir können ein Multiprozesssystem einfach mittels Forking aufbauen, d.h. ein Elternprozess erzeugt Kindprozesse. Die bidrektionale Kommunikation (Frage-Antwort) bezeichnet man auch als Remote Procedure Call (RPC) Kommunikation. Ein Prozess (Klient) fragt etwas bei einem Server an (der Call) und bekommt vom Server eine Antwort.

Wir können Pipes benutzen um RPC zu implementieren:

#pipe2

Aber das Diagramm ist etwas irreführend: Pipes sind nur Halbduplex (Einwegkommunikation), und obiges Beispiel würde im Vollduplex dazu führen dass ein schreibender Prozess wieder seine eigenen Daten lesen würde. Man kann nur einen der folgenden Schritte ausführen:

  1. Elternteil schreibt an fd [1], Kind liest von fd [0]
  2. Kind schreibt in fd [1], Elternteil liest aus fd [0]

Wir brauchen für RPC immer (mindestens) zwei Pipes. Eine für die Anfrage, und eine für die Antwort. Daher kann (und sollte) jeder Prozess die Seite einer Pipe schließen (mit close) die er nicht benötigt.

Algorithmus 1. Template für RPC zwischen zwei Prozessen. Annahme: Die Prozesse seien aus einer Fork Operation entstanden.
#define READ  0
#define WRITE 1
int chreq[2],chrep[2];
pipe(chreq);
pipe(chrep);

// Prozess 1: Klient
close(chreq[READ]);
close(chrep[WRITE]);
write(chreq[WRITE],sendbuf,msglen);
read(chrep[READ],recvbuf,nbuflen);
// Prozess 2: Server
close(chreq[WRITE]);
close(chrep[READ]);
read(chreq[READ],recvbuf,buflen);
write(chrep[WRITE],sendbuf,msglen);

Ist die Nachrichtenversendung über Pipes atomar? Was wäre wenn es mehrere Prozesse gäbe die in eine Pipe eine Nachricht schreiben? Können sich die Daten vermischen? Wir machen ein Experiment.

Aufgabe 1. Implementiere ein Produzenten-Konsumenten System: Ein einfacher Echo Server. Es gibt einen Elternprozess der als Server arbeitet, und zwei Kindprozesse die als Klienten arbeiten (durch Forking erzeugt). Es soll jeweils eine Pipe für die Anfrage (req) und die Antwort (rep) geben. Alle Klienten nutzen die gleichen Pipes. Ein Nachricht besteht nur aus einem Zeichen, der ID ('1' oder '2') des Klientenprozesses. Der Server sendet die Anfrage einfach zurück (also das ID Zeichen '1' oder '2'). Gebe die Anfragen und Antworten aus. Führe mehrfach Tests durch. Es wird die sleep Funktion verwendet um sicher zu stellen dass die Klienten nicht vor dem Server laufen. Was ist zu beobachten, was geht schief?

Das Programm kann mit dem nachfolgenden Programmkode einfach erstellt werden:

gcc -o pipes0 pipes0.c
Ein einfaches drei-Prozess System mit Pipes.

 ▸ 
 ✗ 
 ≡ 


Aufgabe 2. Jetzt soll das obige Programm derart modifiziert werden dass jeder Klient seinen eigenen (privaten) Antwortkanal bekommt. Was ist jetzt zu beobachten, bekommt jeder Klient seine Antwort? Zur Vereinfachung wird jetzt die Klientennummer als Integer Zahl verarbeitet (hier 0/1).

Ein einfaches drei-Prozess System mit Pipes. Vervollständige und teste den Kode.

 ▸ 
 ✗ 
 ≡ 

Ly8gaHR0cHM6Ly9jczQxMTguZ2l0aHViLmlvL3d3dy8yMDIzLTEvbGVjdC8wNS1pcGMuaHRtbAojaW5jbHVkZSA8c3RkaW8uaD4KI2luY2x1ZGUgPHN0ZGxpYi5oPgojaW5jbHVkZSA8c3RyaW5nLmg+CiNpbmNsdWRlIDx1bmlzdGQuaD4KI2luY2x1ZGUgPHN5cy93YWl0Lmg+CiNpbmNsdWRlIDxzeXMvdHlwZXMuaD4KCgojZGVmaW5lIFJFQUQgIDAKI2RlZmluZSBXUklURSAxCiNkZWZpbmUgTlVNQ0xJRU5UUyAyCgppbnQgY2hyZXFbMl0sY2hhY2tbMipOVU1DTElFTlRTXTsKCi8vIElEIGlzIG5vdyAwLzEKdm9pZCBjbGllbnQoaW50IG15aWQpICB7CiAgY2hhciByZWN2YnVmWzEwXSxzZW5kYnVmWzEwXTsKICBjbG9zZShjaHJlcVtteWlkKjIrUkVBRF0pOyAgIC8vIENsb3NlIHJlYWQgZW5kIG9mIHJlcSBwaXBlCiAgY2xvc2UoY2hhY2tbbXlpZCoyK1dSSVRFXSk7CiAgZm9yKGludCBpPTA7aTwxMDtpKyspIHsKICAgIHNsZWVwKDEpOwogICAgcHJpbnRmKCJDaGlsZCAlYyA9PlxuIixteWlkKzEpOwogICAgc2VuZGJ1ZlswXT0nMScrbXlpZDsKICAgIHdyaXRlKGNocmVxW1dSSVRFXSxzZW5kYnVmLDEpOwogICAgaW50IG49cmVhZChjaGFja1tteWlkKjIrUkVBRF0scmVjdmJ1ZiwxMCk7CiAgICBwcmludGYoIkNoaWxkICVkICglZCkgJWMgPT4gUkVQPSVjXG4iLG15aWQrMSxuLCcxJytteWlkLHJlY3ZidWZbMF0pOyAgICAKICB9CiAgc2xlZXAoMTApOwp9CgppbnQgbWFpbihpbnQgYXJnYywgY2hhciAqKmFyZ3YpCnsKICBmZF9zZXQgcmZkczsKICBzdHJ1Y3QgdGltZXZhbCB0djsKICBpbnQgcmV0dmFsOwogIGludCBuZmRzOwogIHBpZF90IHBpZDE9MCwgcGlkMj0wOwogIGNoYXIgcmVjdmJ1ZlsxMDBdLHNlbmRidWZbMTAwXTsKCiAgLy8gZmRbMF06IHJlYWQsIGZkWzFdOndyaXRlCiAgcGlwZShjaHJlcSk7CiAgZm9yKGludCBpPTA7aTxOVU1DTElFTlRTO2krKykgewogICAgcGlwZSgmY2hhY2tbaSoyXSk7CiAgfQoKICBpZiAoKHBpZDEgPSBmb3JrKCkpID09IDApIHsKICAgIGNsaWVudCgwKTsKICB9CgogIGlmIChwaWQxICE9IDAgJiYgKHBpZDIgPSBmb3JrKCkpID09IDApIHsKICAgIGNsaWVudCgxKTsKICB9CgogIC8vIFBhcmVudCBkb2VzIG5vdCBuZWVkIGVpdGhlciBlbmQgb2YgdGhlIHBpcGUKICBpZiAocGlkMSE9MCAmJiBwaWQyIT0wKSB7CiAgICBwcmludGYoIlBhcmVudFslZCAlZF1cbiIscGlkMSxwaWQyKTsKICAgIGNsb3NlKGNocmVxW1dSSVRFXSk7CiAgICBmb3IoaW50IGk9MDtpPE5VTUNMSUVOVFM7aSsrKSB7CiAgICAgIGNsb3NlKGNoYWNrW2kqMitSRUFEXSk7IAogICAgfQogICAgcHJpbnRmKCJXYWl0aW5nIGZvciByZXF1ZXN0cyBmcm9tIFslZCAlZF1cbiIscGlkMSxwaWQyKTsKCiAgICBkbyB7CiAgICAgIEZEX1pFUk8oJnJmZHMpOwogICAgICBGRF9TRVQoY2hyZXFbUkVBRF0sICZyZmRzKTsKICAgICAgbmZkcz1jaHJlcVtSRUFEXSsxOwogICAgICAvKiBXYWl0IHVwIHRvIGZpdmUgc2Vjb25kcy4gKi8KICAgICAgdHYudHZfc2VjID0gNTsKICAgICAgdHYudHZfdXNlYyA9IDA7CiAgICAgIC8vIGh0dHBzOi8vd3d3LnR1dG9yaWFsc3BvaW50LmNvbS91bml4X3N5c3RlbV9jYWxscy9fbmV3c2VsZWN0Lmh0bQogICAgICByZXR2YWwgPSBzZWxlY3QobmZkcywgJnJmZHMsIE5VTEwsIE5VTEwsICZ0dik7CgogICAgICBpZiAoRkRfSVNTRVQoY2hyZXFbUkVBRF0sJnJmZHMpKSB7CiAgICAgICAgaW50IGksbj1yZWFkKGNocmVxW1JFQURdLHJlY3ZidWYsMTAwKTsKICAgICAgICByZWN2YnVmW25dPTA7CiAgICAgICAgcHJpbnRmKCJSRVFbJWRdXG4iLG4pOwogICAgICAgIGlmIChuPT0wKSBleGl0KC0xKTsKICAgICAgICBjaGFyICpwPXJlY3ZidWY7CiAgICAgICAgd2hpbGUgKCpwKSB7CiAgICAgICAgICBpbnQgY2xpZD0qcC0nMSc7CiAgICAgICAgICBzZW5kYnVmWzBdPSpwOwogICAgICAgICAgd3JpdGUoY2hhY2tbY2xpZCoyK1dSSVRFXSxzZW5kYnVmLDEpOwogICAgICAgICAgcCsrOwogICAgICAgIH0KICAgICAgfQogICAgfSB3aGlsZSAocmV0dmFsPjApOwogICAgd2FpdHBpZChwaWQxLCBOVUxMLCAwKTsKICAgIHdhaXRwaWQocGlkMiwgTlVMTCwgMCk7CiAgfQogIHJldHVybiAwOwp9

Ein einfaches Kommunikationsprotokoll

Bisher hatten wir nur ein einfaches Echo implementiert. Im nächsten Schritt sollen Funktionen im Server implementiert werden die Daten von Klienten verarbeiten und zurück senden. Wir nehmen an dass es nur einstellige binäre (Boolesche) Zahlen 0-1 als Eingabe gibt. Der Server soll die grundlegenden Booleschen Operationen (Und, Oder, Exklusiv-Oder) verarbeiten und das Ergebnis zurücksenden (also 0/1). Es muss immer die ID des Prozesses als ASCII Zeichen 0 oder 1 vorangestellt werden (die numerische ID ist dann wieder id=c-'0', das ASCII Zeichen c=id+'0').

Die Nachrichten von mehreren Klienten können aufeinmal vom Server gelesen werden. Hoffentlich sind die Nachrichten aber atomar, da sie aber sehr kurz sind ist davon auszugehen! Daher muss es einen Token Parser geben. Das erste Zeichen ist immer die Klienten ID, das zweite die Operation, und das dritte und vierte Zeichen die Operanden (die auch wieder in numerische Werte umgewandelt werden müssen, und bei der Antwort umgekehrt in ein Textzeichen).

<id>&ab  => (a and b) 
<id>|ab  => (a or b)
<id>*ab  => (a exor b)

Beispiele:  
0&10 => 0
1|01 => 1

Aufgabe 3. Implementiere das einfache RPC Protokoll und teste es mit den zwei Klientenprozessen wie oben. Prüfe auf Richtigkeit und Konsistenz. Kommentare können unten eingetragen werden. Trage den Programmkode im nachfolgenden Editor ein. Dort ist ein Grundgerüst zu sehen.

Ein drei-Prozess System mit Pipes mit einem einfachen RPC Protokoll. Vervollständige und teste den Kode.

 ▸ 
 ✗ 
 ≡ 

I2luY2x1ZGUgPHN0ZGlvLmg+CiNpbmNsdWRlIDxzdGRsaWIuaD4KI2luY2x1ZGUgPHN0cmluZy5oPgojaW5jbHVkZSA8dW5pc3RkLmg+CiNpbmNsdWRlIDxzeXMvd2FpdC5oPgojaW5jbHVkZSA8c3lzL3R5cGVzLmg+CgojZGVmaW5lIFJFQUQgIDAKI2RlZmluZSBXUklURSAxCiNkZWZpbmUgTlVNQ0xJRU5UUyAyCgovLyBKZXR6dCBmw7xyIGplZGVuIEtsaWVudGVuIGVpbmUgY2hhY2sgUGlwZQppbnQgY2hyZXFbMl0sY2hhY2tbTlVNQ0xJRU5UUyoyXTsKCi8vIElEIGlzIG5vdyAwLzEKdm9pZCBjbGllbnQoaW50IG15aWQpICB7CiAgaW50IG47CiAgY2hhciByZWN2YnVmWzEwXSxzZW5kYnVmWzEwXTsKICBjbG9zZShjaHJlcVtteWlkKjIrUkVBRF0pOyAgIC8vIENsb3NlIHJlYWQgZW5kIG9mIHJlcSBwaXBlCiAgY2xvc2UoY2hhY2tbbXlpZCoyK1dSSVRFXSk7CiAgZm9yKGludCBpPTA7aTwxMDtpKyspIHsKICAgIHNsZWVwKDEpOwogICAgc2VuZGJ1ZlswXT0nMScrbXlpZDsKICAgIHNlbmRidWZbMV09JyYnOwogICAgc2VuZGJ1ZlsyXT0nMCcrKGklMik7CiAgICBzZW5kYnVmWzNdPScwJysoKDIqaS8zKSUyKTsgICAgCiAgICBwcmludGYoIkNoaWxkICVkICVjJWMlYyVjID0+XG4iLG15aWQrMSxzZW5kYnVmWzBdLHNlbmRidWZbMV0sc2VuZGJ1ZlsyXSxzZW5kYnVmWzNdKTsKICAgIG49d3JpdGUoY2hyZXFbV1JJVEVdLHNlbmRidWYsNCk7CiAgICBuPXJlYWQoY2hhY2tbMipteWlkK1JFQURdLHJlY3ZidWYsMTApOyAKICAgIHByaW50ZigiQ2hpbGQgJWQgWyVkXSA9PiBSRVA9JWNcbiIsbXlpZCsxLG4scmVjdmJ1ZlswXSk7ICAgIAogIH0KICBzbGVlcCgxMCk7Cn0KCgppbnQgbWFpbihpbnQgYXJnYywgY2hhciAqKmFyZ3YpIHsKICBmZF9zZXQgcmZkczsKICBzdHJ1Y3QgdGltZXZhbCB0djsKICBpbnQgcmV0dmFsOwogIGludCBuZmRzOwogIHBpZF90IHBpZDE9MCwgcGlkMj0wOwogIGNoYXIgcmVjdmJ1ZlsxMF0sc2VuZGJ1ZlsxMF07CiAgcGlwZShjaHJlcSk7CiAgZm9yKGludCBpPTA7aTxOVU1DTElFTlRTO2krKykgewogICAgcGlwZSgmY2hhY2tbaSoyXSk7CiAgfQoKICBpZiAoKHBpZDEgPSBmb3JrKCkpID09IDApIHsKICAgIGNsaWVudCgwKTsKICB9CgogIGlmIChwaWQxICE9IDAgJiYgKHBpZDIgPSBmb3JrKCkpID09IDApIHsKICAgIGNsaWVudCgxKTsKICB9CgogIC8vIFBhcmVudCBkb2VzIG5vdCBuZWVkIGVpdGhlciBlbmQgb2YgdGhlIHBpcGUKICBpZiAocGlkMSE9MCAmJiBwaWQyIT0wKSB7CiAgICBwcmludGYoIlBhcmVudFslZCAlZF1cbiIscGlkMSxwaWQyKTsKICAgIGNsb3NlKGNocmVxW1dSSVRFXSk7CiAgICBmb3IoaW50IGk9MDtpPE5VTUNMSUVOVFM7aSsrKSAgY2xvc2UoY2hhY2tbUkVBRCtpKjJdKTsgCiAgICBkbyB7CiAgICAgIEZEX1pFUk8oJnJmZHMpOwogICAgICBGRF9TRVQoY2hyZXFbUkVBRF0sICZyZmRzKTsKICAgICAgbmZkcz1jaHJlcVtSRUFEXSsxOwogICAgICAvKiBXYWl0IHVwIHRvIGZpdmUgc2Vjb25kcy4gKi8KICAgICAgdHYudHZfc2VjID0gNTsKICAgICAgdHYudHZfdXNlYyA9IDA7CiAgICAgIC8vIGh0dHBzOi8vd3d3LnR1dG9yaWFsc3BvaW50LmNvbS91bml4X3N5c3RlbV9jYWxscy9fbmV3c2VsZWN0Lmh0bQogICAgICByZXR2YWwgPSBzZWxlY3QobmZkcywgJnJmZHMsIE5VTEwsIE5VTEwsICZ0dik7CiAgICAgIHByaW50ZigiR290IGl0IC8lZC8uXG4iLHJldHZhbCk7CiAgICAgIGlmIChGRF9JU1NFVChjaHJlcVtSRUFEXSwmcmZkcykpIHsKICAgICAgICBpbnQgaSxuPXJlYWQoY2hyZXFbUkVBRF0scmVjdmJ1ZiwxMDApOwogICAgICAgIHJlY3ZidWZbbl09MDsKICAgICAgICBwcmludGYoIlJFUVslZF1cbiIsbik7CiAgICAgICAgY2hhciAqcD1yZWN2YnVmOwogICAgICAgIC8vIFRva2VuIFBhcnNlcjoKICAgICAgICB3aGlsZSAoKnApIHsKICAgICAgICAgIGludCBjbGlkPSgqcCsrKS0nMScsCiAgICAgICAgICAgICAgb3A9KnArKywKICAgICAgICAgICAgICBhID0oKnArKyktJzAnLAogICAgICAgICAgICAgIGIgPSgqcCsrKS0nMCcsCiAgICAgICAgICAgICAgeSA9IDA7CiAgICAgICAgICBwcmludGYoImNsaWVudCAlZCAlZCAlYyAlZFxuIixjbGlkLGEsb3AsYik7CiAgICAgICAgICBzd2l0Y2ggKG9wKSB7CiAgICAgICAgICAgIGNhc2UgJyYnOiB5PWEmYjsgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgJ3wnOiB5PWF8YjsgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgJyonOiB5PWFeYjsgYnJlYWs7CiAgICAgICAgICB9CiAgICAgICAgICBzZW5kYnVmWzBdPScwJyt5OwogICAgICAgICAgd3JpdGUoY2hhY2tbY2xpZCoyK1dSSVRFXSxzZW5kYnVmLDEpOyAKICAgICAgICB9CiAgICAgIH0KICAgIH0gd2hpbGUgKHJldHZhbD4wKTsKICAgIHdhaXRwaWQocGlkMSwgTlVMTCwgMCk7CiAgICB3YWl0cGlkKHBpZDIsIE5VTEwsIDApOwogIH0gCn0=


Created by the NoteBook Compiler Ver. 1.36.2 (c) Dr. Stefan Bosse (Tue Feb 11 2025 14:35:56 GMT+0100 (Central European Standard Time))