Performanz Lastausgleich und Betriebssysteme

This commit is contained in:
WieErWill 2022-03-22 09:54:06 +01:00
parent 403ba9f9ec
commit ab67e49300
2 changed files with 437 additions and 8 deletions

Binary file not shown.

View File

@ -795,10 +795,10 @@
\item Open-Source-Mikrokernel-basiertes Betriebssystem für IoT
\item Architektur: Mikrokernel
\begin{itemize*}
\item minimale Algorithmenkomplexität
\item vereinfachtes Threadkonzept $\rightarrow$ keine Kontextsicherung erforderlich
\item keine dynamische Speicherallokation
\item energiesparende Hardwarezustände vom Scheduler ausgelöst (inaktive CPU)
\item minimale Algorithmenkomplexität
\item vereinfachtes Threadkonzept $\rightarrow$ keine Kontextsicherung erforderlich
\item keine dynamische Speicherallokation
\item energiesparende Hardwarezustände vom Scheduler ausgelöst (inaktive CPU)
\item Mikrokerneldesign unterstützt komplementäre NFE: Adaptivität, Erweiterbarkeit
\item Kosten: IPC (hier gering)
\end{itemize*}
@ -928,7 +928,7 @@
\item[$\Rightarrow$] Schichtendifferenzierung ( layered operating system )
\item Modularisierung
\end{itemize*}
Modularer Makrokernel
\begin{itemize*}
\item Kernelfunktionen in Module unterteilt $\rightarrow$ Erweiter-/Portierbarkeit
@ -991,7 +991,7 @@
\subsubsection{Modularer Makrokernel vs. Mikrokernel}
\begin{itemize*}
%\item \includegraphics[width=\linewidth]{Assets/AdvancedOperatingSystems-modularer-makrokernel.png}
\item minimale Kernelfunktionalität:
\item minimale Kernelfunktionalität
\item keine Dienste, nur allgemeine Schnittstellenfür diese
\item keine Strategien, nur grundlegende Mechanismen zur Ressourcenverwaltung
\item neues Problem: minimales Mikrokerneldesign
@ -3434,6 +3434,435 @@
\item definierte Länge des kritischen Abschnitts $\rightarrow$ unnötiges Sperren sehr preiswert
\end{itemize*}
Spinlock
\begin{itemize*}
\item Locking für längere kritische Abschnitte
\begin{itemize*}
\item wechselseitiger Ausschluss durch aktives Warten
\item Granularität: einfach-exklusive Ausführung von Code, der ...
\item ... nicht blockiert
\item ... (vorhersehbar) kurze Zeit den kritischen Abschnitt nutzt
\item Performanz: keine Scheduler-Aktivierung, keine sperrbedingten Kontextwechsel (alle wartenden Threads bleiben ablaufbereit!)
\end{itemize*}
\item Benutzung:
\begin{itemize*}
\item explizite Lock-Datenstruktur $\rightarrow$ Deadlocks durch Mehrfachsperrung oder multiple Locks möglich
\item variable Länge des kritischen Abschnitts $\rightarrow$ unnötiges Sperren sehr teuer! (aktives Warten verschwendet Prozessorzeit)
\end{itemize*}
\end{itemize*}
Semaphor
\begin{itemize*}
\item Locking für komplexere kritische Abschnitte:
\begin{itemize*}
\item wechselseitiger Ausschluss durch suspendieren von Threads
\item Granularität: n - exklusive Ausführung von (nahezu) beliebigem Code, über längere oder unbekannt lange (aber endliche) kritische Abschnitte
\item Performanz: nutzt Scheduler(Suspendierung und Reaktivierung wartender Threads), verursacht potenziell Kontextwechsel, implementiert Warteschlange
\end{itemize*}
\item Benutzung
\begin{itemize*}
\item mehrfach nutzbare krit. Abschnitte möglich ($\rightarrow$ begrenzt teilbare Ressourcen)
\item explizite Reaktion auf Signale während des Wartens möglich
\item Deadlocks möglich, unnötiges Sperren teuer
\end{itemize*}
\end{itemize*}
R/W-Lock
\begin{itemize*}
\item Reader/Writer Locks: Spezialformen von Spinlocksund binären Semaphoren ( n = 1)
\begin{itemize*}
\item Steigerung der Locking-Granularität $\rightarrow$ Optimierung des Parallelitätsgrades
\item Idee: sperrende Threads nach Zugriffssemantik auf kritischen Abschnitt in zwei Gruppen unterteilt:
\item Reader: nur lesende Zugriffe auf Variablen
\item Writer: mindestens ein schreibender Zugriff auf Variablen
\item R/W-Spinlock/Semaphor muss explizit als Reader oder Writer gesperrt werden
\item Performanz: analog Spinlock/Semaphor
\end{itemize*}
\item Benutzung analog Spinlock/Semaphor
\end{itemize*}
Fazit: Locks im Linux-Kernel
\begin{itemize*}
\item Locking-Overhead: Sperrkosten bei korrekter Benutzung
\item Lock-Granularität (ebenfalls bei korrekter Benutzung):
\begin{itemize*}
\item mögliche Länge des kritischen Abschnitts
\item Granularität der selektiven Blockierung (z.B. nur Leser , nur Schreiber )
\item ![](Assets/AdvancedOperatingSystems-locks-in-linux.png)
\end{itemize*}
\item Locking-Sicherheit: Risiken und Nebenwirkungen hinsichtlich ...
\begin{itemize*}
\item fehlerhaften Sperrens (Auftreten von Synchronisationsfehlern)
\item Deadlocks
\item unnötigen Sperrens (Parallelität ad absurdum ...)
\end{itemize*}
\end{itemize*}
\subsection{Lastangleichung und Lastausgleich}
Probleme bei ungleicher Auslastung von Multicore-Prozessoren
\begin{enumerate*}
\item Performanzeinbußen
\begin{itemize*}
\item stark belastete Prozessorkerne müssen Multiplexing zwischen mehr Threadsausführen als schwach belastete
\item dadurch: Verlängerungder Gesamtbearbeitungsdauer
\end{itemize*}
\item Akkumulation der Wärmeabgabe
\begin{itemize*}
\item bedingt durch bauliche Gegebenheiten (Miniaturisierung, CMP)
\item Kontrolle erforderlich zur Vermeidung thermischer Probleme (bis zu Zerstörung der Hardware)
\end{itemize*}
\end{enumerate*}
\begin{itemize*}
\item Lösung: Angleichung der Last auf einzelnen Prozessorkernen
\end{itemize*}
Lastangleichung
\begin{itemize*}
\item Verfahren zur Lastangleichung
\begin{itemize*}
\item statische Verfahren: einmalige Zuordnung von Threads
\begin{itemize*}
\item Korrekturen möglich nur durch geeignete Verteilung neuer Threads
\item Wirksamkeit der Korrekturen: relativ gering
\end{itemize*}
\item dynamische Verfahren: Optimierung der Zielfunktion zur Laufzeit
\begin{itemize*}
\item Zuordnungsentscheidungen dynamisch anpassbar
\item Korrekturpotentialzur Laufzeitdurch Zuweisung eines anderen Prozessorkerns an einen Thread ( Migration )
\item aber: algorithmisch komplexer, höhere Laufzeitkosten
\end{itemize*}
\end{itemize*}
\item verbreitete Praxis in Universalbetriebssystemen: dynamische
\end{itemize*}
Dynamische Lastangleichung
\begin{itemize*}
\item Anpassungsgründe zur Laufzeit
\begin{itemize*}
\item Veränderte Lastsituation: Beenden von Threads
\item Verändertes Verhaltens einzelner Threads: z.B. Änderung des Parallelitätsgrads, vormals E/A-Thread wird prozessorintensiv, etc.
\item Zuordnungsentscheidungen
\begin{itemize*}
\item auf Grundlage von a-priori - Informationen über Prozessverhalten
\item durch Messungen tatsächlichen Verhaltens $\rightarrow$ Übergang von Steuerung zu Regelung
\end{itemize*}
\item Kosten von Thread-Migration
\begin{itemize*}
\item zusätzliche Thread-bzw. Kontextwechsel
\item kalte Caches
\item $\rightarrow$ Migrationskosten gegen Nutzen aufwiegen
\end{itemize*}
\end{itemize*}
\end{itemize*}
grundlegende Strategien:
\begin{enumerate*}
\item Lastausgleich (loadbalancing oder loadleveling)
\begin{itemize*}
\item Ziel: gleichmäßige Verteilung der Last auf alle Prozessorkerne
\end{itemize*}
\item Lastverteilung (loadsharing)
\begin{itemize*}
\item verwendet schwächere Kriterien
\item nur extreme Lastunterschiede zwischen einzelnen Prozessorkernen ausgeglichen(typisch: Ausgleich zwischen leerlaufenden bzw. überlasteten Kernen)
\end{itemize*}
\end{enumerate*}
Anmerkungen
\begin{enumerate*}
\item Lastausgleich: verwendet strengere Kriterien, die auch bei nicht leerlaufenden und überlasteten Kernen greifen
\item 100\%tiger Lastausgleich nicht möglich (2 Threads auf 3 CPUs etc.)
\end{enumerate*}
Lastverteilung beim Linux-Scheduler: globale Entscheidungen
\begin{enumerate*}
\item anfängliche Lastverteilung auf Prozessorkerne, grob bzgl. Ausgewogenheit
\item dynamische Lastverteilung auf Prozessorkerne durch
\begin{enumerate*}
\item loadbalancing-Strategie
\item idlebalancing-Strategie
\end{enumerate*}
\end{enumerate*}
Verteilungsstrategien
\begin{itemize*}
\item ,,load balancing'' - Strategie
\begin{itemize*}
\item für jeden Kern nach Zeitscheibenprinzip (z.B. 200 ms) aktiviert
\item ,,Thread-Stehlen'' vom am meisten ausgelasteten Kern
\item aber: bestimmte Threads werden (vorerst) nicht migriert
\begin{enumerate*}
\item solche, die nicht auf allen Kernen lauffähig
\item die mit noch ,,heißem Cache''
\end{enumerate*}
\item erzwungene Thread-Migration (unabhängig von heißen Caches), wenn ,,loadbalancing'' mehrmals fehlgeschlagen
\end{itemize*}
\item ,,idle balancing'' - Strategie
\begin{itemize*}
\item von jedem Kern aktiviert, bevor er in Leerlauf geht:
\item Versuch einer ,,Arbeitsbeschaffung''
\item mögliche Realisierung: gleichfalls als ,,Thread-Stehlen'' vom am meisten ausgelasteten Kern
\end{itemize*}
\end{itemize*}
Heuristische Lastverteilung
\begin{itemize*}
\item in der Praxis: Heuristiken werden genutzt ...
\begin{itemize*}
\item ... für alle Entscheidungen bzgl.
\begin{itemize*}
\item loadbalancing (,,Migrieren oder nicht?'')
\item threadselection (,,Welchen Threads von anderem Prozessor migrieren?'')
\item time-slicing (,,Zeitscheibenlänge für loadbalancing?'')
\end{itemize*}
\item ... unter Berücksichtigung von
\begin{itemize*}
\item Systemlast
\item erwarteter Systemperformanz
\item bisherigem Thread-Verhalten
\end{itemize*}
\end{itemize*}
\item Bewertung
\begin{itemize*}
\item Heuristiken verwenden Minimum an Informationen zur Realisierung schneller Entscheidungen $\rightarrow$ Performanz der Lastverteilung
\item Tradeoff: höhere Performanz für aktuelle Last durch teurere Heuristiken (Berücksichtigung von mehr Informationen)?
\end{itemize*}
\end{itemize*}
\subsection{Grenzen der Parallelisierbarkeit}
(oder: warum nicht jedes Performanzproblemdurch zusätzliche Hardware gelöst wird)
\begin{itemize*}
\item Parallelarbeit aus Anwendungssicht: Welchen Performanzgewinn bringt die Parallelisierung?
\item naiv:
\begin{itemize*}
\item 1 Prozessor $\rightarrow$ Bearbeitungszeit t
\item n Prozessoren $\rightarrow$ Bearbeitungszeit t/n
\end{itemize*}
\item Leistungsmaß für den Performanzgewinn: Speedup $S(n)=\frac{T(1)}{T(n)}$, $T(n)$ bearbeitungszeit bei n Prozessoren
\item Amdahls Gesetz: ,,Auch hochgradig parallele Programme weisen gewisse Teile strenger Datenabhängigkeit auf - die nur sequenziell ausgeführt werden können -und daher den erzielbaren Speed-up grundsätzlich limitieren.''
\item Jedes Programm besteht aus einem nur sequenziell ausführbaren und einem parallelisierbaren Anteil: $T(1)=T_s + T_p$
\item mit als sequenziellem Anteil (z.B. $\rightarrow$ 10% sequenzieller Anteil): Effizienz $f=\frac{T_s}{T_s+T_p}$ wobei $0\leq f \leq 1$
\item Damit ergibt sich im günstigsten Fall für Bearbeitungszeit bei Prozessoren: $T(n)=f*T(1)+ (1-f)\frac{T(1)}{n}$
\item Speedup nach Amdahl: $S(n)=\frac{T(1)}{T(n)}=\frac{1}{f+\frac{1-f}{n}}$
\end{itemize*}
Praktische Konsequenz aus Amdahls Gesetz:
\begin{itemize*}
\item mögliche Beschleunigung bei Parallelisierung einer Anwendung i.A. durch die Eigenschaften der Anwendungselbst begrenzt (sequenzieller Anteil)
\item Erhöhung der Prozessorenanzahlüber bestimmtes Maß hinaus bringt nur noch minimalen Performanzgewinn
\end{itemize*}
Annahme bisher
\begin{itemize*}
\item parallelisierbarer Anteil auf beliebige Anzahl Prozessoren parallelisierbar
\item genauere Problembeschreibung möglich durch Parallelitätsprofil: maximaler Parallelitätsgrad einer Anwendung in Abhängigkeit von der Zeit
\item Für ein bestimmtes Problem bringt ab einer Maximalzahl ($p_{max}$) jede weitere Erhöhung der Prozessoren-Anzahl keinen weiteren Gewinn, da die Parallelisierbarkeitdes Problems begrenzt ist.
\end{itemize*}
Einfluss von Kommunikation und Synchronisation
\begin{itemize*}
\item Threads eines Prozesses müssen kommunizieren (Daten austauschen), spätestens nach ihrer Beendigung
\item hierzu i.A. auch Synchronisation (Koordination) erforderlich
\item Resultat: Prozessoren nicht gesamte Zeit für Berechnungen nutzbar
\end{itemize*}
\subsubsection{Overhead durch Kommunikation und Synchronisation}
aus Erfahrungen und theoretischen Überlegungen: Zeitaufwand für Kommunikation und Synchronisation zwischen Prozessen (bzw. Threads) einer Anwendung auf verschiedenen Prozessoren:
\begin{itemize*}
\item streng monoton wachsende Funktion der Prozessoren-Anzahl n (d.h. Funktion nicht nach oben beschränkt)
\item praktische Konsequenz von Kommunikation und Synchronisation:
\begin{itemize*}
\item zu viele Prozessoren für eine Anwendung bedeutet
\item keine Geschwindigkeitssteigerung
\item sogar Erhöhung der Gesamtbearbeitungszeit $T_{\sum}$
\item $\rightarrow$ fallender Speedup
\end{itemize*}
\item Für minimale Bearbeitungsdauer gibt es eine optimale Anzahl Prozessoren
\end{itemize*}
Leistungsmaß Effizienz: Effizienz des Speedups $E(n)=\frac{S(n)}{n}$
\begin{itemize*}
\item Normierung des Speedupauf CPU-Anzahl entspricht Wirkungsgrad der eingesetzten Prozessoren
\item Idealfall: $S(n)=n\Rightarrow E(n)=\frac{S(n)}{n}=1$
\end{itemize*}
\paragraph{Optimale Prozessorenanzahl}
$T(n)$ Gesamtbearbeitungszeit
\begin{itemize*}
\item Im Bereich des Minimums: sehr flacher Verlauf
\item (geringe) Änderung der Prozessoren-Anzahl in diesem Bereich: kaum Auswirkungen auf Rechenzeit (im steilen, fallenden Bereich hat Prozessorenanzahl jedoch gewaltige Auswirkungen auf Rechenzeit)
\end{itemize*}
$E(n)$ Effizienz (Auslastung der Prozessoren)
\begin{itemize*}
\item verringert sich stetig mit zunehmender Prozessoren-Anzahl
\item Konflikt zwischen Kostenminimierung (= Minimierung der Ausführungszeit bzw. Maximierung des Speed-up) und Nutzenmaximierung (= Maximierung der Effizienz)
\item $\rightarrow$ Kompromissbereich
\end{itemize*}
Nutzen($E(n)$)-Kosten($T(n)$)-Quotienten maximieren: $\mu(n)=\frac{E(n)}{T(n)} T(1)=S(n)*E(n)=\frac{S(n)^2}{n}$
\begin{enumerate*}
\item Effizienz maximieren: $n_{opt}=n^*_E = 1$ unsinnig
\item Speedup maximieren = Ausführungszeit minimieren: $n_{opt}=n^*_S$ Individuell für jedes Programm
\item Nutzen-Kosten-Funktion maximieren: $n_{opt}=n^*_{\mu}$ individuell für jedes Programm
\end{enumerate*}
\subsection{Beispiel-Betriebssysteme}
\begin{itemize*}
\item Auswahl: Betriebssysteme, die mit Fokus auf Hochparallelität entworfen wurden
\item Konsequenzen für BS-Architekturen:
\begin{itemize*}
\item Parallelität durch Abstraktion von Multicore-Architekturen: Multikernel
\item Parallelität durch netzwerkbasierteMulticore-Architekturen: Verteilte Betriebssysteme
\end{itemize*}
\end{itemize*}
\subsubsection{Multikernel: Barrelfish}
In a nutshell:
\begin{itemize*}
\item seit ca. 2009 entwickeltes, experimentelles Forschungs-Betriebssystem (open source)
\item Untersuchungen zur Struktur künftiger heterogener Multicore-Systeme
\item gegenwärtig in Entwicklung an ETH Zürich in Zusammenarbeit mit Microsoft
\item Forschungsfragen
\begin{enumerate*}
\item Betriebssystemarchitektur
\begin{itemize*}
\item Trend stetig wachsender Anzahl Prozessorkerne $\rightarrow$ Skalierbarkeit des Betriebssystems zur Maximierung von
Parallelität?
\item zunehmende Menge an Varianten der Hardware-Architektur, betreffend z.B.
\begin{itemize*}
\item Speicherhierarchien
\item Verbindungsstrukturen
\item Befehlssatz
\item E/A-Konfiguration
\item $\rightarrow$ Unterstützung zunehmend heterogener Hardware-Vielfalt (insbesondere noch zu erwartender Multicore-Prozessoren)?
\end{itemize*}
\end{itemize*}
\item Wissensdarstellung
\begin{itemize*}
\item Betriebssystem und Anwendungen Informationen über aktuelle Architektur zur Laufzeit liefern
\item $\rightarrow$ Informationen zur Adaptivitätdes BS an Last und Hardware zur Maximierung von Parallelität?
\end{itemize*}
\end{enumerate*}
\item Betriebssystem-Architektur für heterogene Multicore-Rechner
\begin{itemize*}
\item Idee: Betriebssystem wird strukturiert ...
\begin{itemize*}
\item als verteiltes System von Kernen,
\item die über Botschaften kommunizieren (Inter-Core-Kommunikation) und
\item keinen gemeinsamen Speicher besitzen.
\end{itemize*}
\item Entwurfsprinzipien:
\begin{enumerate*}
\item alle Inter-Core-Kommunikation explizit realisiert d.h. keine implizite Kommunikation z.B. über verteilte Speichermodelle wie DSM (Distributed SharedMemory), Botschaften zum Cache-Abgleich etc.
\item Betriebssystem-Struktur ist Hardware-neutral
\item Zustandsinformationen als repliziert (nicht als verteilt)betrachtet $\rightarrow$ schwächere Synchronisationsanforderungen!
\end{enumerate*}
\item Ziele dieser Entwurfsprinzipien
\begin{enumerate*}
\item Verbesserte Performanz auf Grundlage des Entwurfs als verteiltes System
\item Natürliche Unterstützung für Hardware-Inhomogenitäten
\item Größere Modularität
\item Wiederverwendbarkeit von Algorithmen, die für verteilte Systeme entwickelt wurden
\end{enumerate*}
\item Implementierung: auf jedem Prozessorkern Betriebssystem-Instanz aus CPU-''Treiber'' und Monitor-Prozess
\end{itemize*}
\end{itemize*}
CPU Driver
\begin{itemize*}
\item Aufgabe: Umsetzung von Strategien zur Ressourcenverwaltung, z.B.
\begin{itemize*}
\item Durchsetzung von Zugriffssteuerungsentscheidungen
\item Zeitscheiben-Multiplexing von Threads
\item Vermittlung von Hardware-Zugriffendes Kernels (MMU, Clockusw.)
\item Ereignisbehandlung
\end{itemize*}
\item Implementierung:
\begin{itemize*}
\item hardwarespezifisch
\item vollständig ereignisgesteuert, single-threaded, nicht-präemptiv
\item läuft im privilegierten Modus (Kernel-Modus)
\item ist absolut lokal auf jedem Prozessor-Kern (gesamte Inter-Core-Kommunikation durch die Monitore realisiert)
\item ist vergleichsweise klein, so dass er im Lokalspeicher des Prozessorkerns untergebracht werden kann $\rightarrow$ Wartbarkeit, Robustheit, Korrektheit
\end{itemize*}
\end{itemize*}
Monitor
\begin{itemize*}
\item Wichtige Angaben
\begin{itemize*}
\item läuft im Nutzer-Modus
\item Quellcode fast vollständig prozessorunabhängig
\end{itemize*}
\item Monitore auf allen Prozessorkernen: koordinieren gemeinsam systemweite Zustandsinformationen
\begin{itemize*}
\item auf allen Prozessor-Kernen replizierte Datenstrukturen: mittels Abstimmungsprotokollkonsistent gehalten (z.B. Speicherzuweisungstabellen u. Adressraum-Abbildungen)
\item $\rightarrow$ implementiert Konsensalgorithmenfür verteilte Systeme
\end{itemize*}
\item Monitore: enthalten die meisten wesentlichen Mechanismen und Strategien, die in monolithischem Betriebssystem-Kernel (bzw. $\mu$Kernel-Serverprozessen) zu finden sind
\end{itemize*}
\subsubsection{Verteilte Betriebssysteme}
\begin{itemize*}
\item hier nur Einblick in ein breites Themenfeld
\item Grundidee: Ortstransparenz
\begin{itemize*}
\item FE des Betriebssystems:
\begin{itemize*}
\item abstrahieren ...
\item multiplexen ...
\item schützen ...
\end{itemize*}
\item hierzu: lokaler BS-Kernel kommuniziert mit über Netzwerk verbundenen, physisch verteilten anderen Kernels (desselben BS)
\item Anwendungssicht auf (nun verteilte) Hardware-Ressourcen identisch zu Sicht auf lokale Hardware $\rightarrow$ Ortstransparenz
\end{itemize*}
\item Zwei Beispiele:
\begin{itemize*}
\item Amoeba: Forschungsbetriebssystem, Python-Urplattform
\item Plan 9 fom Bell Labs: everything is a file
\end{itemize*}
\end{itemize*}
\paragraph{Amoeba}
Architektur
\begin{itemize*}
\item verteiltes System aus drei (nicht zwingend disjunkten)Arten von Knoten:
\begin{itemize*}
\item Workstation (GUI/Terminalprozess, Benutzerinteraktion)
\item Pool Processor (nicht-interaktive Berechnungen)
\item Servers(File ~, Directory ~, Networking ~, ...)
\end{itemize*}
\item Betriebssystem: $\mu$Kernel (identisch auf allen Knoten) + Serverprozesse (dedizierte Knoten, s.o.: Servers )
\item $\rightarrow$ Vertreter einer verteilten $\mu$Kernel-Implementierung
\item Kommunikation:
\begin{itemize*}
\item LAN (Ethernet)
\item RPCs (Remote ProcedureCalls)
\item $\rightarrow$ realisieren ortstransparenten Zugriff auf sämtliche BS-Abstraktionen (Amoeba: objects )
\end{itemize*}
\end{itemize*}
\paragraph{Plan 9}
\begin{itemize*}
\item verteiltes BS der Bell Labs (heute: Open Source Projekt)
\item Besonderheit: Semantik der Zugriffe auf verteilte BS-Abstraktionen
\begin{itemize*}
\item ortstransparent (s. Amoeba)
\item mit ressourcenunabhängig identischen Operationen - deren spezifische Funktionalität wird für die Anwendung transparent implementiert $\rightarrow$ Konzept ,,everything is a file''... bis heute in unixoiden BS und Linux!
\end{itemize*}
\item Beispiele:
\begin{itemize*}
\item Tastatureingabenwerden aus (Pseudo-) ,,Datei'' gelesen
\item Textausgabenwerden in ,,Datei'' geschrieben -aus dieser wiederum lesen Terminalanwendungen
\item all dies wird logisch organisiert durch private Namensräume
\item $\rightarrow$ Konzept hierarchischer Dateisysteme
\end{itemize*}
\end{itemize*}
\end{multicols}
\end{document}