Grundlagen: Betriebssysteme und Systemsoftware - GBS Tutoring ...

Die Seite wird erstellt Jens Hiller
 
WEITER LESEN
Lehrstuhl für Connected Mobility
Fakultät für Informatik
Technische Universität München

                Grundlagen: Betriebssysteme und Systemsoftware
                                            IN0009, WiSe 2021/22

                                                Hausaufgabe 4
                    12.01.2022 13:00 Uhr über Artemis (https://artemis.ase.in.tum.de)

Hinweis:
Obgleich die Abgabe für Hausaufgabe 4 erst am 12.01.2022 um 13:00 Uhr fällig ist, werden wir ab dem
23.12.2021 keine Fragen dazu auf Zulip oder über sonstige Kontaktmöglichkeiten mehr beantworten!
Es wird am 23.11.2021 auch keine Programmierfragestunde geben.
Falls es zu Problemen mit Artemis oder der Rechnerhalle über Weihnachten kommt können wir nicht garantieren,
dass diese direkt behoben werden.
Auch die GBS-Übungsleitung möchte sich über Weihnachten frei nehmen.
Die Aufgaben Buddy-Algorithmus CSV und Clock-Algorithmus WEB sollen der Überprüfung dienen, ob sie
die beiden Algorithmen verstanden haben, bevor Sie diese implementieren. Die beiden Aufgaben geben keine
Punkte für den Notenbonus.

Aufgabe 1          Completely Fair Scheduling mit Red-Black-Tree (G)
Der Linux Kernel setzt als Schedulingverfahren Completely Fair Scheduling (CFS) ein. Dabei werden Prozesse
anhand ihrer schon zugeteilten Rechenzeit ausgewählt. Der Prozess mit der bisher kürzesten Rechenzeit wird
dabei als nächstes ausgewählt und vom Dispatcher an die CPU gebunden.
Scheduling sollte einen möglichst kleinen Overhead mit sich bringen, um kaum kostbare Rechenzeit zu
verschwenden. Die aufwendigste Operation bei CFS ist das Finden des Prozesses mit der kleinsten Rechenzeit.
Um den Aufwand zu reduzieren, werden die Prozesse in einer Datenstruktur gespeichert, welche die Prozesse
ordnet. Denkbar wäre eine sortierte Liste, aber dabei ist das Einfügen von neuen Elementen zu aufwendig
(O(n)). Eine andere Datenstruktur sind Bäume.
Sie haben in einer der vergangenen Hausaufgaben den unbalancierten Binärbaum kennengelernt. Durchschnitt-
lich werden bei diesem zum Suchen, Einfügen und Löschen O(log(n)) Operationen benötigt. Im Worst-Case
hingegen O(n). Dieser Fall tritt auf, wenn bereits vorsortierte Werte in den Baum eingefügt werden. In diesem
Fall entartet der Binärbaum zur verketteten Liste.
Um dies zu verhindern, werden selbstbalancierende Bäume eingesetzt, die ihre Struktur selbst anpassen, um
eine möglichst gleichbleibende hohe Performanz zu erzielen. Selbstbalancierende Bäume gehören zu den
wichtigsten Datenstrukturen in der Informatik. Es gibt viele verschiedene Arten. Dabei sind insbesondere AVL
und Red-Black-Trees (Rot-Schwarz-Bäume) zu nennen. CFS nutzt einen RB-Tree als Datenstruktur. Ein weiterer
populärer Einsatzort von RB-Trees ist die C++ Standardbibliothek, die damit Datenstrukturen wie std::set
umsetzt.
In den folgenden Teilaufgaben werden Sie einen RB-Tree selber anhand der vorgegebenen Funktionsprototypen
entwickeln und ihn für eine vereinfachte Version von CFS einsetzen.

    • Implementieren Sie die Funktion newNode. Diese erstellt dynamisch einen neuen Knoten vom Typ struct
      node (siehe unten) und initialisiert den Wert val mit dem übergebenen Wert. Alle Pointer sollen auf
      NULL gesetzt werden, und die Farbe des Knotens muss auf rot (vgl. Enumeration Color) gesetzt werden.
      Abschließend wird ein Pointer auf den neu erstellten Knoten zurückgegeben.
      struct node {
              int val ;
              bool color ;
              struct node * left ;
              struct node * right ;
              struct node * parent ;
      };

Noch Fragen? Dann schaut doch
mal im Zulip-Stream vorbei:
https://zulip.in.tum.de                All Rights Reserved, Do Not Distribute!                             1
Lehrstuhl für Connected Mobility
Fakultät für Informatik
Technische Universität München

                                A                       rotateLeft                        B

                                                       rotateRight
                     B                    3                                      1            A

         1                      2                                                         2           3

                                    Abbildung 1: Operationen rotateLeft und rotateRight

      Der Datentyp bool kann an dieser Stelle nur verwendet werden, wenn stdbool.h eingebunden wurde. Vor
      C99 gab es keinen bool Datentyp in C. Seit C99 gibt es den Typ _Bool. Um diesen angenehmer nutzen
      zu können, wird in stdbool.h folgende Definition durchgeführt:
      # define bool _Bool

      Des Weiteren sind true und false keine Keywords, sondern auch nur Definitionen:
      # define true 1
      # define false 0

      Daher können Sie zur Zuweisung der Farbe auch die vorgegebene Enumeration Color benutzen. Die
      Elemente einer Enumeration werden vom Compiler durchnummeriert und entsprechen damit in diesem
      Fall auch 0 und 1.
    • Zum Implementieren der Einfügefunktion benötigen Sie zwei Hilfsfunktionen:
      struct node * rotateLeft ( struct node * root , struct node * n ) ;

      struct node * rotateRight ( struct node * root , struct node * n ) ;

      Diese sollen die in Abbildung 1 dargestellte Operation durchführen. Der Parameter n ist der Knoten, über
      den die Drehung stattfindet. In Abbildung 1 entspricht n dem Knoten A im linken Baum und dem Knoten B
      im rechten Baum.
    • Implementieren Sie die Funktion insertRB. Gehen Sie dazu in zwei Schritten vor:
        1. Verwenden Sie die Einfügefunktion des einfachen Binärbaumes, um den neuen Knoten in den Baum
           einzufügen. Sie können Ihre eigene Funktion oder die Variante aus der Musterlösung verwenden.
           Beachten Sie hierbei, dass das Attribut parent richtig gesetzt werden muss.
        2. Durch das Einfügen kann es nötig werden, den Baum neu zu balancieren. Implementieren Sie dazu
           unter zur Hilfenahme der in der vorherigen Aufgabe implementierten Operationen die Neubalan-
           cierung. Es bietet sich an, diese in eine neue Funktion auszulagern. Die genaue Funktionsweise
           der Neubalancierung ist hinreichend in der Fachliteratur und diversen Webseiten beschrieben. Wir
           möchten Ihnen an dieser Stelle insbesondere folgende Webangebote empfehlen:
              – Implementierung RB-Tree im Linux Kernel: https://github.com/torvalds/linux/blob/master/
                lib/rbtree.c
              – https://www.cs.auckland.ac.nz/software/AlgAnim/red_black.html, hier insbesonders das Bei-
                spiel zur Einfügeoperation: https://www.cs.auckland.ac.nz/software/AlgAnim/red_black_op.
                html
             Sollten Sie während Ihrer Recherche auf verschiedene Varianten stoßen, können Sie frei wählen.
             Wichtig ist nur, dass diese einen gültigen RB-Tree (=Invarianten eines RB-Tree sind erfüllt) erzeugt
             (siehe Hinweis).
    • Die Operation zum Entfernen eines Knotens ist sehr lang und komplex, daher soll diese nicht näher
      betrachtet werden. Sollten Sie die Löschoperation für Ihre Implementierung der folgenden Aufgabe
      benötigen, können Sie unter Angabe der Quelle eine bestehende Implementierung verwenden.
    • Implementieren Sie die Funktion cfs. Diese erhält einen Pointer auf eine Liste von struct process und
      eine Zeitdauer als Übergabeparameter. Diese Funktion soll nun das CFS Scheduling simulieren:

Noch Fragen? Dann schaut doch
mal im Zulip-Stream vorbei:
https://zulip.in.tum.de                    All Rights Reserved, Do Not Distribute!                             2
Lehrstuhl für Connected Mobility
Fakultät für Informatik
Technische Universität München

         – Erweitern Sie die node Struktur um einen Zeiger data auf die process Struktur, die von diesem Knoten
           repräsentiert wird.
         – Fügen Sie die Prozesse in den von Ihnen implementierten RB-Tree ein. Das Attribut val jedes
           Knotens soll dabei auf den Wert der PID gesetzt werden (dies passiert im realen CFS nicht, aber ist
           hier sinnvoll, um einen zufälligen Zeitpunkt zu simulieren).
         – Implementieren Sie eine Schleife, die über die Anzahl der jeweiligen Zeitschritte iteriert. In jedem
           Zeitschritt soll folgendes passieren:
              * Wählen Sie den Knoten mit der kleinsten bisherigen Rechenzeit aus (Knoten mit kleinstem Wert
                in val).
              * Geben Sie die Rechenzeit (val) und PID in folgendem Format, gefolgt von einem Zeilenumbruch,
                aus:
                Time : ␣  ␣ PID : ␣ 

              * Erhöhen Sie den Wert der bisherigen Rechenzeit um 10, und sorgen Sie dafür, dass Ihr Baum
                valide bleibt (Knoten muss vielleicht an einer anderen Stelle eingefügt werden).
              * Um verschiedene Prioritäten zu simulieren, haben die Prozesse verschiedene "Decay"Faktoren.
                Die Idee ist, dass die bisherige Rechenzeit mancher Prozesse verringert wird, um den Prozess
                schneller wieder rechnen zu lassen. Nutzen Sie den Wert decay in struct process und subtra-
                hieren Sie diesen von der jeweiligen bisherigen Rechenzeit im zugehörigen Knoten. Achten Sie
                auch hier darauf, dass Ihr Baum valide bleibt.
         – Nach der Simulation des Schedulings geben Sie einen Pointer auf den RB-Baum zurück.

Hinweise und Bemerkungen:

    • Nutzen Sie für die Anordnung der Knoten dieselbe Invariante wie bei der Binary Tree Aufgabe (Der
      linke Kindknoten muss einen kleineren oder gleichen Wert, wohingegen der rechte Kindknoten nur einen
      größeren Wert als sein Elternknoten haben darf).
    • Wir haben Ihre Anmerkungen zur Binary Tree Aufgabe ernst genommen und das Testverfahren geändert.
      Anstatt Ihren Baum mit einer Referenzimplementierung zu vergleichen prüfen wir nun, ob es sich bei
      Ihrem Baum um einen validen RB-Tree handelt. Somit gibt es mehrere richtige Lösungen.
    • Wir hätten Ihnen gerne eine graphische Repräsentation Ihres Baums zur Verfügung gestellt, aber dies
      war mit Artemis leider nicht möglich (Ausgabelänge ist zu begrenzt).
    • Bitte implementieren Sie die geforderten Funktionalitäten an der dafür vorgesehenen Stelle. Nur so können
      die Tests unabhängig voneinander durchgeführt werden. Insbesondere werden jegliche Änderungen an
      der Datei main.c nicht vom Tester berücksichtigt.

Noch Fragen? Dann schaut doch
mal im Zulip-Stream vorbei:
https://zulip.in.tum.de                            All Rights Reserved, Do Not Distribute!                   3
Lehrstuhl für Connected Mobility
Fakultät für Informatik
Technische Universität München

Aufgabe 2            Dämonendomäne (G)
In dieser Aufgabe werden Sie mit Hilfe von systemd einen Daemon erstellen, der Texte in lower bzw. upper case
konvertieren kann. Zur Kommunikation sollen named pipes verwendet werden 1 .
Ein Beispiel:

     $ cd /tmp/
     $ ls -la
     drwxrwxrwt 12 root               root          4096 Dec 9 16:27 .
     drwxr-xr-x 22 root               root          4096 Sep 10 15:53 ..
     prw-r--r-- 1 root                root            0 Dec 9 16:27 echo_in
     prw-r--r-- 1 root                root            0 Dec 9 16:27 echoed
     $ echo "Hallo Welt" >>          echo_in
     $ cat echoed
     HALLO WELT
     $ echo -e "Abc Def Ghi          jkL 123 \t ^.°" >> echo_in
     $ cat echoed
     ABC DEF GHI JKL 123                 ^.°

Standardmäßig soll die Ausgabe wie dargestellt in upper case erfolgen. Durch die Verwendung von signals 2
soll dieses Verhalten gesteuert werden können:
Empfängt der Daemon-Prozess (im Folgenden ebenfalls echoed genannt) SIGUSR2, sollen künftige Eingaben
zu lower case konvertiert werden. Analog forciert der Empfang von SIGUSR1 die Konversion zu upper case. Es
sollen ausschließlich Zeichen verändert werden, die Buchstaben in ASCII darstellen:

     $ pkill -SIGUSR2 echoed
     $ echo "Hallo Welt" >> echo_in
     $ cat echoed
         hallo welt
     $ echo -e "Abc Def Ghi jkL 123 \t ^.°" >> echo_in
     $ cat echoed
         abc def ghi jkl 123      ^.°
     $ pkill -SIGUSR1 echoed
     $ echo "Hallo Welt" >> echo_in
     $ cat echoed
     HALLO WELT
     $ pkill -SIGUSR1 echoed
     $ echo "Hallo Welt" >> echo_in
     $ cat echoed
         HALLO WELT
     $ echo "Hallo Welt" >> echo_in
     $ cat echoed
     HALLO WELT

  1 siehe man 3 mkfifo (Linux)
  2 siehe z.B. kill -l oder man signal

Noch Fragen? Dann schaut doch
mal im Zulip-Stream vorbei:
https://zulip.in.tum.de                        All Rights Reserved, Do Not Distribute!                     4
Lehrstuhl für Connected Mobility
Fakultät für Informatik
Technische Universität München

Hintergrundwissen Daemons: Wie aus der Vorlesung bekannt, handelt es sich bei Daemons um (Hintergrund-)
Prozesse, die oft nur bei bestimmten Ereignissen (z.B. Empfang eines Netzwerkpakets) aktiv werden. Um dies
sichtbar zu machen, ist es Konvention, den Prozessnamen ein d nachzustellen (cupsd, netbiosd, xfce4-volumed,
. . . ). Um zum Daemon zu werden, muss ein Prozess forken, seine Standardfiledeskriptoren schließen und ggf.
erneut forken. Er wird somit nach Beendigung des Elternprozesses vom init Prozess adoptiert und ist nicht
mehr mit einem Terminal verbunden.
Dieser klassische Weg steht auch heute noch offen, für diese Aufgabe möchten wir aber eine komfortablere
Methode anwenden:
Seit einigen Jahren hat sich systemd bei den bekannten Linuxdistributionen als Standard zur Systeminitialisie-
rung etabliert (Default bei Fedora seit 2010, Arch und Debian seit 2012, RHEL seit 2014. . . ).
Zur Prozessverwaltung werden zahlreiche Funktionen wie z.B. das Monitoring von Dateien bereitgestellt, die
somit nicht mehr in jedem Daemon selbst implementiert werden müssen. Soll ein Daemon beispielsweise
erst gestartet werden, sobald eine bestimmte Datei existiert, kann dies in einer path unit konfiguriert werden.
Eigenschaften des gestarteten Dienstes werden in der zugehörigen service unit spezifiziert:

     $ cat /lib/systemd/system/example.path
         [Path]
         PathExists=/tmp/activation_file

          [Unit]
          Unit=example.service

     $ cat /lib/systemd/system/example.service
     [Unit]
     Description=A simple systemd service unit
     Documentation=man:example(7)

     [Service]
     ExecStart=/usr/bin/exampled
     Type=oneshot
     Restart=no

     [Install]
     WantedBy=multi-user.target

Das Type Attribut der [Service] Section ist hierbei besonders wichtig. Ist es, wie hier dargestellt, oneshot oder
simple, so muss sich der exampled Prozess nicht mehr selber um fork und das Schließen der Filedeskriptoren
kümmern. Die [Install] Section ermöglicht das automatische Starten des Dienstes, sobald das System
initialisiert ist.
Gehen Sie nun wie folgt vor:

    • Schreiben Sie ein C-Programm, das zunächst mittles mkfifo eine named pipe /tmp/echo_in und eine
      named pipe /tmp/echoed erzeugt. Fortan bleibe der Prozess in einer Dauerwarteschleife, ohne sich zu
      beenden. Sobald echo_in verändert wird (also von einem beliebigen Prozess neuer Inhalt hineingeschrie-
      ben wurde), soll das Programm wieder aktiv werden und den (neuen) Inhalt (in lower bzw. upper case
      konvertiert) in /tmp/echoed schreiben. Anschließend wird wieder auf neuen Input gewartet. Sie können
      davon ausgehen, dass die Signale SIGUSR1 und SIGUSR2, die bei Ankunft instantan bearbeitet werden, erst
      eine geraume Zeit nach der letzten Änderung von echo_in gesendet werden, um eine Änderung inmitten
      der Ausführung Ihrer Konvertierungsfunktion zu vermeiden.

Noch Fragen? Dann schaut doch
mal im Zulip-Stream vorbei:
https://zulip.in.tum.de                 All Rights Reserved, Do Not Distribute!                                5
Lehrstuhl für Connected Mobility
Fakultät für Informatik
Technische Universität München

    • Machen Sie sich mit der path unit von systemd vertraut. Wie können Sie damit Ihrem Daemon signalisie-
      ren, dass neuer Input vorhanden ist? Wie können Sie dabei die Warteschleife des Daemons unterbrechen?
      Erstellen Sie hierfür eine Einheit echoed_activate.path
      Dieser Mechanismus kann leider nicht im Testsystem verwendet werden. Hier bietet es sich an, dass der
      Warteschleife ein blockierendes read auf die Pipe zu Grunde liegt (wenn ein Prozess in /tmp/echo_in
      schreiben möchte blockiert dieser so lange, bis Ihr Daemon von der Pipe liest, da hierfür sonst vom
      Dateisystem ein Puffer bereitgestellt werden müsste).
    • Testen Sie Ihr Programm zunächst lokal und unabhängig von systemd. Wird wirklich nur der neue
      Input ausgegeben? Ist das casing korrekt? Funktioniert das Signalhandling auch, wenn z.B. mehrfach
      hintereinander das gleiche Signal ankommt? Ist sichergestellt, dass der Daemon nicht terminiert und
      immer wieder aus den Ruhephasen reaktiviert werden kann? Eine Sonderfallbehandlung, wenn die Pipes
      zu Beginn bereits existieren, ist nicht verlangt, Sie können davon ausgehen, dass sie noch nicht existieren.
    • Testen Sie nun das Zusammenspiel mit systemd. Mittels loginctl enable-linger $(whoami) können Sie
      ermöglichen, dass eine Instanz von systemd lediglich mit Ihren Benutzerrechten und nicht wie standard-
      mäßig als root läuft. Der Befehl systemctl --user start echoed_activate.path aktiviert nun die path unit, die in
      /home/$(whoami)/.config/systemd/user/ liegen muss.
      Hinweise zur Bewertung: Aus technischen Gründen können wir auf dem Testserver leider keine (echten)
      systemd-services nutzen, weshalb ausschließlich Ihr C-Programm bewertet wird. Zum lokalen Testen ist
      das Erstellen der systemd units allerdings äußerst wertvoll. Achten Sie darauf, dass das Programm nach
      Bearbeitung der Signale SIGUSR1 und SIGUSR2 nicht beendet wird.

Noch Fragen? Dann schaut doch
mal im Zulip-Stream vorbei:
https://zulip.in.tum.de                   All Rights Reserved, Do Not Distribute!                                   6
Lehrstuhl für Connected Mobility
Fakultät für Informatik
Technische Universität München

Aufgabe 3          Clock-Algorithmus WEB (E)
Lernziele:

    • Funktionsweise des Clock-Algorithmus.
    • In der kommenden Hausaufgabe wird es Ihre Aufgabe sein, das hier erworbene Wissen zu nutzen, um
      den Clock-Algorithmus selbst zu implementieren.

Aufgabenstellung:
Auf der Webseite https://paging.tum.sexy finden Sie ein von uns entwickeltes Tool zur Simulation des Clock-
Algorithmus. Wählen Sie dort unter ”Algorithmus auswählen” ”Clock” aus. Probieren Sie sich dort etwas herum
und lösen Sie einige Aufgaben. Über den Knopf ”Zufällig” können Sie sich beliebige konfigurationen erzeugen
lassen.
Beispielkonfiguration:
    • https://vmott42.in.tum.de/paging?iscsv=true&strategy=clock&frames=6&csv=2,2,4,5,3,3,6,5,4,3,
      4,6&rw=r,r,w,r,w,r,r,r,w,r,w,r

Noch Fragen? Dann schaut doch
mal im Zulip-Stream vorbei:
https://zulip.in.tum.de               All Rights Reserved, Do Not Distribute!                            7
Lehrstuhl für Connected Mobility
Fakultät für Informatik
Technische Universität München

Aufgabe 4          Buddy-Algorithmus CSV (E)
Lernziele:

    • Funktionsweise des Buddy-Algorithmus.
    • In der kommenden Hausaufgabe wird es Ihre Aufgabe sein, das hier erworbene Wissen zu nutzen, um
      den Buddy-Algorithmus selbst zu implementieren.

Aufgabenstellung:
Nehmen Sie an, es stehen für diese Aufgabe 1024 MiB Speicher zur Verfügung. Die Verwaltung dieses
Speichers soll mithilfe des Buddy-Algorithmus stattfinden. Es erfolgen die unten zu sehenden Allokationen und
Freigaben, welche von Ihnen Schritt für Schritt durchgeführt werden sollen.
Für die Dokumentation ihrer Ergebnisse sind CSV-Dateien vorgesehen. Dazu geben wir Ihnen für jeden
einzelnen Schritt eine eigene CSV-Datei vor. Die Dateien sind entsprechend der Nummer des jeweiligen
Schrittes benannt (für Schritt x: buddy_.csv). Dabei stellt die Datei buddy_0.csv die Belegung des initial
freien Speichers von 1024 MiB dar. Bitte geben Sie innerhalb der CSV-Dateien alle Speicherbereiche an, die
durch die Unterteilung des Speichers entstanden sind, unabhängig davon, ob sie belegt sind oder nicht. Halten
Sie sich bezüglich der Formatierung der CSV-Dateien an folgendes Beispiel:

                                              Start    Size     State
                                               0       256        b
                                              256      256        f
                                              512      512        f

Das Beispiel beschreibt einen Speicherbereich, der in drei Teile aufgespalten wurde. Der erst Bereich beginnt
bei Adresse 0, hat die Größe 256 MiB und ist belegt. Der zweite Bereich beginnt bei Adresse 256, ist 256 MiB
groß und frei. Der dritte Bereich beginnt bei Adresse 512, hat die Größe 512 MiB und ist ebenfalls frei.
Allokationen und Freigaben:

  1. Allokation: 100 MiB
  2. Allokation: 200 MiB
  3. Allokation: 256 MiB

  4. Freigabe: 200 MiB (aus 2.)
  5. Freigabe: 100 MiB (aus 1.)
  6. Freigabe: 256 MiB (aus 3.)

Noch Fragen? Dann schaut doch
mal im Zulip-Stream vorbei:
https://zulip.in.tum.de                All Rights Reserved, Do Not Distribute!                             8
Sie können auch lesen