Initial commit

This commit is contained in:
Josia Pietsch 2022-02-15 20:57:33 +01:00
commit e880928cd0
15 changed files with 4250 additions and 0 deletions

51
.gitignore vendored Normal file
View File

@ -0,0 +1,51 @@
## Core latex/pdflatex auxiliary files:
*.aux
*.lof
*.log
*.lot
*.fls
*.out
*.toc
*.fmt
*.fot
*.cb
*.cb2
*.loe
## Intermediate documents:
*.dvi
*-converted-to.*
## Bibliography auxiliary files (bibtex/biblatex/biber):
*.bbl
*.bcf
*.blg
*-blx.aux
*-blx.bib
*.brf
*.run.xml
*.latexmain
## Build tool auxiliary files:
*.fdb_latexmk
*.synctex
*.synctex.gz
*.synctex.gz(busy)
*.pdfsync
*.tdo
##swap files
*.swp
*.synctex(busy)
##other help files
*.idx
*.ilg
*.ind
*.lec
*.cnt
sysprog.pdf
.init-submodule-cert
build/

3
.latexmkrc Normal file
View File

@ -0,0 +1,3 @@
ensure_path('TEXINPUTS', './LatexPackagesBuild//'); # set texinputs to find custom packages
$pdf_mode = 1; # generate a pdf file by default
$out_dir = 'build';

15
Makefile Normal file
View File

@ -0,0 +1,15 @@
pdf: .init-submodule-cert
latexmk
clean:
latexmk -c
clean-all:
latexmk -C
.init-submodule-cert:
@echo "[Make] Initialising git submodule for packages"
git submodule update --init --rebase
@touch .init-submodule-cert
.PHONY: pdf, clean, clean-all

View File

@ -0,0 +1,892 @@
\section{Motivation}
\begin{definition}[Übersetzer]
Ein \vocab{Übersetzer} erzeugt zu einem Programm in einer Quellsprache ein äquivalentes Programm in einer Zielsprache.
Ist die Quellsprache mächtiger als die Zielsprache, so wird der Übersetzer auch als \vocab{Compiler} bezeichnet.
\begin{figure}[H]
\centering
\begin{tikzcd}
\text{Konkretes Programm} & \text{Wirkung der Programmausführung}\\
\text{Quellsprache} \arrow{r}{\text{Sprachdefinition}} \arrow{d}{\text{Übersetzung}}& \text{Abstrakte Maschine} \arrow{d}{\text{gleiche Ergebnisse}} \\
\text{Zielsprache} \arrow{r}{\text{Maschinenbeschreibung}} & \text{Reale Maschine}
\end{tikzcd}
\end{figure}
\end{definition}
Selbst in Assembler zu programmieren, wird eher selten benötigt, ggf. für Embedded Devices und hoch optimierten Code. Allerdings hilft ein Verständnis von Assembler zum besseren Verständnis von Hochsprachen. Ferner ermöglicht es, Malware zu analysieren, Reverse Engineering, Patchen und Cracken.
\subsection{Programmierung in Maschinensprache}
Rechner verwenden intern sehr elementare Befehle:
\begin{itemize}
\item Zugriff auf Speicherzellen
\item Zugriff auf (wenige) Register
\item Arithmetische und logische Operationen
\item Sprünge zu anderen Befehlen
\end{itemize}
Jede CPU besitzt einen spezifische Menge an Befehlen. Intern werden diese binär codiert. Da dies, wie auch hexadezimale Codierung, für Menschen nicht zumutbar ist, existieren Assemblersprachen, die die Darstellung durch \vocab{mnemonische Symbole} erleichtern.
Mit Assemblersprache kann angenehmer als mit Maschinencode gearbeitet werden.
Allerdings sind auch Assemblersprachen stark von einer bestimmten Maschine abhängig.
Wird ein Assemblerprogramm für eine andere Maschine benötigt, so muss es vollständig neugeschrieben werden.
\subsection{Maschinenunabhängige Programmierung}
Um Unabhängigkeit von der konkreten Maschine zu erlangen, werden in höheren Programmiersprachen mächtigere Befehle definiert, die automatisch in Assembler-Sprachen übersetzt werden können und nicht vom spezifischen Befehlssatz einer CPU abhängen.
Damit sind diese Programme (theoretisch) auf allen Rechnern einsetzbar.
Praktisch existieren häufig Spracherweiterungen, die von maschinenspezifischen Charakteristika Gebrauch machen.
\section{Bezug zur Systemnahen Informatik}
\subsection{Der Von-Neumann-Rechner}
\begin{definition}[Von-Neumann-Rechner]
Der \vocab{Von-Neumann-Rechner} besteht aus fünf Funktionseinheiten:
\begin{itemize}
\item \vocab{Steuerwerk} (control unit): Lädt und decodiert Programmbefehle, Koordiniert die Befehlsausführung
\item \vocab{Arithmetisch-logische Einheit} (ALU): Führt arithmetische und logische Operationen unter Kontrolle des Steuerwerks aus. Das Steuerwerk stellt hierfür die Operanden bereit.
\item \vocab{Speicher}: Der Speicher ist in nummerierte, gleich große Zellen geteilt. Über die jeweilige Adresse kann auf Zellinhalte lesend oder schreibend zugegriffen werden.
\item \vocab{Eingabewerk}
\item \vocab{Ausgabewerk}
\end{itemize}
\todoimg{1 S.12}
\end{definition}
\subsection{BORIS}
\todoimg{1 S.13}
Ein einfaches Programm für BORIS wäre
\begin{lstlisting}
LOAD A,[10] // Lade den Inhalt von Speicherzelle 10 in Akkumulator
ADD A,[11] // Addiere den Inhalt von Speicherzelle 11 zum Inhalt des Akkumulator
STORE A,[12] // Lege Inhalt des Akkumulators in Speicherzelle 12 ab
\end{lstlisting}
\subsection{Gestalt von Maschinenbefehlen}
Maschinenbefehle werden in Worten oder Halbworten gespeichert. Ein Befehl hat prinzipiell die folgende Gestalt:
\begin{table}[htpb]
\centering
\label{tab:maschinenbefehl}
\begin{tabular}{|c|c|c|c|c|c|c|}
\hline
Format & Op-Code & Daten $1$ & \dots & Daten $k$ & Ziel & Folge \\
\hline
\end{tabular}
\end{table}
\begin{itemize}
\item Format: Angabe der Länge und Position der einzelnen Felder. Kann entfallen, falls eindeutig durch den Op-Code bestimmt.
\item Op-Code: Angabe der Operation
\item Daten: Wo sind die Operanden?
\begin{itemize}
\item unmittelbar (immediate): Daten als Konstanten im Maschinencode
\item implizit: Akkumulator oder Stack
\item direkt: Angabe der Adressdarstellung im Befehl
\item indirekt: Angabe der Adresse der Speicherzelle, welche die Adresse des Operanden enthält
\end{itemize}
\item Ziel: Wo soll das Ergebnis gespeichert werden?
\begin{itemize}
\item ggf. Überdeckung (Quelle ist auch Ziel)
\item ggf. Implizierung (Akkumulator oder Stack als Ziel)
\end{itemize}
\item Folge: Adresse des nächsten Befehls. Entfällt bei sequentieller Abarbeitung.
\end{itemize}
Da lange Befehle nicht effizient bearbeitet werden können, werden in der Regel nur wenige Adressen verwendet.
Die häufigsten Formen sind
\begin{itemize}
\item 0-Adress-Format: Op-Code
\item 1-Adress-Format: Op-Code, Quelle = Ziel
\item 2-Adress-Format: Op-Code, Quelle1, Quelle2 = Ziel
\item 3-Adress-Format: Op-Code, Quelle1, Quelle2, Ziel
(selten, eine Variante von \code{imul})
\end{itemize}
\subsection{Assembler-Sprachen}
Eine Assembler-Sprache ist eine maschinenorientierte Programmiersprache. Statt der binären Darstellung von Maschinensprachen werden mnemonische Symbole eingesetzt. Ein Befehl einer Assembler-Sprache umfasst:
\begin{itemize}
\item Bezeichnung der durchzuführenden Operation
\item ggf. Angaben zu den Operanden
\item ggf. Marken (d.h. symbolische Adressen) zur Kennzeichnung von der Programmzeile
\end{itemize}
Darüber hinaus stellen Assembler-Sprachen oft auch \vocab{Makros} zur Verfügung. Ein Makro fasst mehrere Befehle oder Deklarationen zu einer Einheit zusammen. Dem Makro wird ein eindeutiger Bezeichner zugeordnet. Wenn der Bezeichner im Programmtext auftaucht, wird er vom Assembler durch den zugehörigen Text ersetzt, bevor die Umwandlung in Maschinencode vorgenommen wird. Ferner sind parametrisierte Makrobefehle möglich.
Ein Assembler-Programm muss vor der Ausführung in ein entsprechendes Maschinenprogramm übersetzt werden. Der hierzu erforderliche \vocab{Assembler} muss:
\begin{itemize}
\item Befehle und Operanden in Binärcode umsetzen
\item Marken in Adressen umrechnen
\item ``Pseudobefehle'' bearbeiten (z.B. Reservierung von Speicherplatz)
\end{itemize}
\subsection{Pseudoassembler ($\alpha$-Notation)}
Da Assembler stark maschinenabhängig ist, wurde im Rahmen der Systemnahen Informatik die sogenannte $\alpha$-Notation verwendet.
\begin{table}[htpb]
\centering
\label{tab:alphanotation}
\begin{tabular}{ll}
$\alpha $ & Akkumulator\\
$\rho(i)$ & Inhalt der Speicherzelle mit Adresse $i$\\
$+,-,\cdot, /$ & Operationen
\end{tabular}
\end{table}
In der $\alpha$-Notation können Marken zum Kennzeichnen von Zellen des Datenspeichers sowie des Programmspeichers verwendet werden.
\subsection{Vom Assemblerprogramm zum Maschinencode}
Zum Übersetzen von Assembler-Code in ein Objektprogramm wird ein sogenannter \vocab{Assembler} benötigt. Dieser setzt die mnemonischen Symbole in Binärcode um, löst symbolische Adressen auf und wandelt Literale in Binärdarstellung um.
Ein Assembler benötigt i.d.R.~mindestens zwei Durchgänge:
\begin{enumerate}[1.]
\item Durchgang:
\begin{itemize}
\item Bestimmung der Länge von Maschineninstruktionen
\item Verwaltung eines Adresszählers
\item Zuordnung symbolischer Adressen
\item Zuordnung von Literalen
\item Verarbeitung einiger Assembler-Instruktionen
\end{itemize}
\item Durchgang:
\begin{itemize}
\item Heranziehung der Symbolwerte
\item Erzeugung von Maschineninstruktionen
\item Erzeugung von Daten (Konstanten)
\item Verarbeitung der restlichen Assembler-Instruktionen
\end{itemize}
\end{enumerate}
Hierfür werden folgende Tabellen benötigt:
\begin{itemize}
\item Maschineninstruktionen (statisch)
\item Assemblerinstruktionen (statisch)
\item Symboltabelle
\item ggf. weitere
\end{itemize}
\section{Allgemeines zur 80x86 Assembler-Programmierung}
Online-Literaturhinweise:
\begin{itemize}
\item Assembler unter Linux: \url{https://asm.sourceforge.net/}
\item PC-Assembler Tutorial: \url{https://pacman128.github.io/pcasm/}
\item Linux Assembly HOWTO \url{https://www.tldp.org/HOWTO/Assembly-HOWTO/}
\item NASM Documentation: \url{https://nasm.us/doc/nasmdoc0.html}
\item Intel CodeTable: \url{https://www.jegerlehner.ch/intel/index.html}
\item System Call Table: \url{https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/}
\end{itemize}
\subsection{x86-64 Architektur}
Im Folgenden wird mit dem 64-Bit x64-Befehlssatz im Protected Mode gearbeitet. x86-Prozessoren adressieren den Speicher byteweise. Instruktionen können auf einem oder mehreren Bytes im Arbeitsspeicher arbeiten. Je nach Anzahl der Bytes werden die folgenden Begriffe verwendet:
\begin{table}[htpb]
\centering
\begin{tabular}{ccc}
\textbf{Name} & \textbf{Wert} & \textbf{Intel Syntax}\\
byte & 1 Byte & \code{db}, \code{.byte}\\
\vocab{word} & 2 Byte & \code{dw}, \code{.word}\\
\vocab{double word} (\vocab{dword}) & 4 Byte & \code{dd}, \code{.double}\\
\vocab{quad word} (\vocab{qword}) & 8 Byte & \code{dq}, \code{.quad} \\
\vocab{paragraph} & 16 Byte &
\end{tabular}
\end{table}
\begin{itemize}
\item word $\coloneqq 2 $ Byte
\item double word (dword) $\coloneqq 4$ Byte
\item quad word $\coloneqq 8$ Byte
\item paragraph $\coloneqq 16$ Byte
\end{itemize}
Die \vocab{Bytereihenfolge} ist \vocab{Little Endian}, d.h. das niedrigstwertige Byte wird an erster Stelle gespeichert.
\begin{warning}
Das ist nicht die Network Byte Order!
\end{warning}
\subsubsection{Register}
Neben einem Register sind in x86-64 i.d.R. auch die untere Hälfte, das untere Viertel und die letzten beiden Byte einzeln adressierbar:
\begin{table}[htpb]
\centering
\caption{Register}
\label{tab:register}
\begin{tabular}{cccccc}
\textbf{Verwendung} & 64bit & 32bit & 16bit& oberes Byte & unteres Byte\\
\hline
Accumulator & rax & eax & ax & ah & al\\
Allgemein & rbx & ebx & bx &bh & bl\\
Counter für Schleifen & rcx & ecx & cx & ch & cl\\
Datenregister (mul, div, IO) & rdx & edx & dx & dh & dl\\
String-Op Source & rsi & esi & si & \\
String-Op Dest & rdi & edi & di &\\
Basepointer & rbp & ebp & bp \\
Stackpointer & rsp & esp & sp \\
Allg. (X $\in \{8,\ldots,15\}$ ) & rX & rXd & rXw & & rXb\\
Flags & rflags & efalgs & flags \\
Segmentregister & \multicolumn{5}{c}{im Protected Mode nicht verwendbar} \\
\end{tabular}
\end{table}
Im \vocab{Statusregister} werden diverse Flags gespeichert. In der CodeTable lässt sich nachlesen, welcher Befehl welche Flags liest oder beeinflusst.
\begin{table}[htpb]
\centering
\caption{Control Flags (Einfluss auf Ausführung von Instruktionen)}
\label{tab:controlflags}
\begin{tabular}{cll}
& \textbf{Name} & \textbf{Bedeutung des Wert 1} \\
D & Direction (String-Op) & von hohen nach niedrigen Adressen\\
I & Interrupt & Interrupt enabled\\
T & Trap/ Trace & Debugging Single Step/Trace Modus\\
\end{tabular}
\end{table}
\begin{table}[htpb]
\centering
\caption{Status Flags}
\label{tab:statusflags}
\begin{tabular}{cll}
& \textbf{Name} & \textbf{Bedeutung des Wert 1} \\
C & Carry & Unsigned Op. zu groß oder $<0$\\
O & Overflow & Signed Op. zu groß oder zu klein\\
S & Sign & negativ\\
Z & Zero & Ergebnis ist $0$\\
A & Aux. Carry für BCD-Operationen \\
P & Parity & Gerade Anzahl an Einsen\\
\end{tabular}
\end{table}
\subsubsection{Befehle}
Operationen lassen sich in verschiedene Mengen einteilen:
\begin{itemize}
\item Data movement instructions -Verschieben von Daten in und von Registern:
\code{mov, lea, les, push, pop, pushf, popf}
\item Conversions - Konvertierungen Byte, Word, Double, Extended Double:
\code{cbw, cwd, cwde}
\item Arithmetik
\code{add, inc, sub, dec, cmp, neg, mul, imul, div, idiv}, \sout{\code{lea}}
\item Logical, shift, rotate and bit instructions
\code{and, or, xor, not, shl, shr, rcl, rcr}
\item I/O instructions
\code{in, out}
\item String instructions
\code{movs, stos, lods}
\item Program flow control instructions
\code{jmp, call, ret, (conditional jumps)}
\item Miscellaneous instructions
\code{clc, stc, cmc}
\end{itemize}
\todo{move to appendix?}
\begin{longtable}{ccc}
\caption{Wichtige x86 Befehle}\\
\textbf{Name} & \textbf{Kommentar} & \textbf{Syntax}\\
\hline
\endhead
\code{MOV} & move (copy) & \code{MOV Dest, Source}\\
\code{XCHG} & exchange & \code{XCHG Op1, Op2}\\
\hline
\code{STC} & set carry & \code{STC}\\
\code{CLC} & clear carry & \code{CLC} \\
\code{CMC} & complement carry & \code{CMC} \\
\code{STD} & set direction & \code{STD} \\
\code{CLD} & clear direction & \code{CLD} \\
\code{STI} & set interrupt & \code{STI} \\
\code{CLI} & clear interrupt & \code{CLI} \\
\code{PUSH} & push onto stack & \code{PUSH Source} \\
\code{PUSHF} & push flags & \code{PUSHF} \\
\code{PUSHA} & push all gerneral registers & \code{PUSHA} \\
\code{POP} & pop from stack & \code{POP Dest} \\
\code{POPF} & pop flags & \code{POPF} \\
\code{POPA} & pop all general registers & \code{POPA} \\
\hline
\code{CBW} & convert byte to word & \code{CBW} \\
\code{CWD} & convert word to double & \code{CWD} \\
\code{CWDE} & convert word to extended double & \code{CWDE} \\
\hline
\code{IN} & input & \code{IN Dest, Port} \\
\code{OUT} & output & \code{OUT Port, Source}\\
\hline
\code{ADD} & add & \code{ADD Dest, Source} \\
\code{ADC} & add with carry & \code{ADC Dest, Source} \\
\code{SUB} & subtract & \code{SUB Dest, Source} \\
\code{SBB} & subtract with borrow & \code{SBB Dest, Source} \\
\code{DIV} & divide (unsigned) & \code{DIV Op}\\
\code{IDIV} & signed integer divide & \code{IDIV Op} \\
\code{MUL} & multiply (unsigned) & \code{MUL Op} \\
\code{IMUL} & signed integer multiply & \code{IMUL Op} \\
\code{INC} & increment & \code{INC Op} \\
\code{DEC} & decrement & \code{DEC Op} \\
\code{CMP} & compare & \code{CMP Op1, Op2} \\
\hline
\code{SAL} & shift arithmetic left & \code{SAL Op, Quantity} \\
\code{SAR} & shift arithmetic right & \code{SAR Op, Quantity} \\
\code{RCL} & rotate left through carry & \code{RCL Op, Quantity} \\
\code{RCR} & rotate right through carry & \code{RCR Op, Quantity} \\
\code{ROL} & rotate left & \code{ROL Op, Quantity} \\
\code{ROR} & rotate right & \code{ROR Op, Quantity}\\
\hline
\code{NEG} & negate (two-complement) & \code{NEG Op} \\
\code{NOT} & invert each bit & \code{NOT Op} \\
\code{AND} & logical and & \code{AND Dest, Source} \\
\code{OR} & logical or & \code{OR Dest, Source} \\
\code{XOR} & logical xor & \code{XOR Dest, Source} \\
\code{SHL} & shift logical left & \code{SHL Op, Quantity} \\
\code{SHR} & shift logical right & \code{SHR Op, Quantity} \\
\hline
\code{NOP} & no operation & \code{NOP} \\
\code{LEA} & load effective address & \code{LEA Dest, Source} \\
\code{INT} & interrupt & \code{INT Nr}\\
\hline
\code{CALL} & call subroutine & \code{CALL Proc}\\
\code{RET} & return from subroutine & \code{RET} \\
\code{JMP} & jump & \code{JMP Dest}\\
\code{JE} & jump if equal & \code{JE Dest}\\
\code{JZ} & jump if zero & \code{JZ Dest}\\
\code{JCXZ} & jump if CX zero & \code{JCXZ Dest}\\
\code{JP} & jump if parity (even) & \code{JP Dest}\\
\code{JPE} & jump if parity even & \code{JPE Dest}\\
\code{JNE} & jump if not equal & \code{JNE Dest}\\
\code{JNZ} & jump if not zero & \code{JNZ Dest}\\
\code{JECXZ} & jump if ECX zero & \code{JECXZ Dest}\\
\code{JNP} & jump if not parity & \code{JNP Dest}\\
\code{JPO} & jump if parity odd & \code{JPO Dest}\\
\code{JA} & jump if above (unsigned) & \code{JA Dest}\\
\code{JAE} & jump if above or equal (unsigned) & \code{JAE Dest}\\
\code{JB} & jump if below (unsigned) & \code{JB Dest}\\
\code{JBE} & jump if below or equal (unsigned) & \code{JBE Dest}\\
\code{JNA} & jump if not above (unsigned) & \code{JNA Dest}\\
\code{JNAE} & jump if not [above or equal] (unsigned) & \code{JNAE Dest}\\
\code{JNB} & jump if not below (unsigned) & \code{JNB Dest}\\
\code{JNBE} & jump if not [below or equal] (unsigned) & \code{JNBE Dest}\\
\code{JC} & jump if carry & \code{JC Dest}\\
\code{JNC} & jump if no carry & \code{JNC Dest}\\
\code{JG} & jump if greater (signed) & \code{JG Dest}\\
\code{JGE} & jump if greater or equal (signed) & \code{JGE Dest}\\
\code{JL} & jump if less (signed) & \code{JL Dest}\\
\code{JLE} & jump if less or equal (signed) & \code{JLE Dest}\\
\code{JNG} & jump if not greater (signed) & \code{JNG Dest}\\
\code{JNGE} & jump if not [greater or equal] (signed) & \code{JNGE Dest}\\
\code{JNL} & jump if not less (signed) & \code{JNL Dest}\\
\code{JNLE} & jump if not [less or equal] (signed) & \code{JNLE Dest}\\
\code{JO} & jump if overflow & \code{JO Dest}\\
\code{JNO} & jump if not overflow & \code{JNO Dest}\\
\code{JS} & jump if sign (negative) & \code{JS Dest}\\
\code{JNS} & jump if no sign (positive) & \code{JMS Dest}\\
\end{longtable}
\subsection{Intel Syntax} % TODO rename or move
In der Vorlesung wird die \vocab{Intel Syntax} verwendet.
\begin{lstlisting}
// Reihenfolge der Operationen
cmd dest, src
// Beispiel:
mov rax, rcx // rax := rcx
// Indizierung
[base+index*scale+disp]
// Beispiel:
mov rax, [rbx] // rax := *rbx
mov rax, [rbx + 3] // rax := *(rbx + 3)
mov rax, [rbx+rcx*4h-20h] // rax := *(rbx + 0x4 * rcx - 0x20)
\end{lstlisting}
\subsubsection{Adressierungsarten}
Die meisten Befehle können ihre Operanden aus Registern, aus dem Speicher oder unmittelbar (immediate) einer Konstante entnehmen.
\begin{itemize}
\item Registeradressierung: \code{mov rbx, rdi}
\item Unmittelbare Adressierung: \code{mov rbx, 1000}
\item Direkte Adressierung: \code{mov rbx, [1000]}
\item Register-indirekte Adressierung: \code{mov rbx, [rax]}
\item Basis-Register Adressierung: \code{mov rax, [rsi+10]}
\item Allgemein: \code{mov register, segreg\footnote{Segmentregister sind nur im Real Mode relevant.}:[base + index*scale+disp]}
(\code{base, index} General-Purpose Register, \code{scale} $\in \{1,2,4,8\}$, \code{disp} beliebige Konstente)
\end{itemize}
Als General Purpose Register für Basis und Index bei indirekter Adressierung können \code{rax, rbx, rcx, rdx, rsi, rdi, r8, ..., r15} verwendet werden.
\begin{warning}
Die Adressierungsarten sind nicht beliebig miteinander kombinierbar. Z.B.~können nicht zwei Operanden mit Register- bzw. indirekter Adressierung verwendet werden.
\end{warning}
\subsubsection{Beispielprogramme}
\begin{lstlisting}[language={[x86masm]Assembler}]
; syscall "write"
%macro write 3
mov rax, 1
mov rdi, %1
mov rsi, %2
mov rdx, %3
syscall
%endmacro
SECTION .data
hello: db "Hello World!", 10
helloLen: equ $ - hello
global _start ; Fuer den Linker sichtbares Label.
SECTION .text ; Textteil des Assembler-Programms, das eigentliche Programm selbst.
_start: ; _start ist Label fuer Start des Hauptprogramms.
write 1, hello, helloLen
mov rax, 60
mov rdi, 0
syscall
\end{lstlisting}
Das Programm kann wie folgt kompiliert und ausgeführt werden:
\begin{lstlisting}[language=bash]
# Programmtext in helloworld.S speichern
# Aufruf des Assemblers
nasm -f elf64 -o helloworld.o helloworld.S
# Aufruf des Linkers
ld -o helloworld helloworld.o
# Aufruf des Linkers (clang)
ld.lld -o helloworld helloworld.o
# Ausfuehren
./helloworld
strace ./helloworld > /dev/null
gdb ./helloworld
\end{lstlisting}
\begin{example}[Summe]
~
\begin{minipage}[t]{0.33\textwidth}
\textbf{C}
\begin{lstlisting}
summe = a + b + c + d;
\end{lstlisting}
\end{minipage}
\begin{minipage}[t]{0.33\textwidth}
\textbf{Assembler (konzeptuell)}
\begin{lstlisting}
summe = a;
summe += b;
summe += c;
summe += d;
\end{lstlisting}
\end{minipage}
\begin{minipage}[t]{0.33\textwidth}
\textbf{Assembler (Intel Syntax)}
\begin{lstlisting}[language={[x86masm]Assembler}]
; a,b,c,d sind
; Speicherstellen
mov rax, [a]
add rax, [b]
add rax, [c]
add rax, [d]
\end{lstlisting}
\end{minipage}
\end{example}
~
\begin{example}[If-Then-Else]
~
\begin{minipage}[t]{0.33\textwidth}
\textbf{C}
\begin{lstlisting}
if (a == 4711) {
// ...
} else {
// ...
}
\end{lstlisting}
\end{minipage}
\begin{minipage}[t]{0.33\textwidth}
\textbf{Assembler (konzeptuell)}
\begin{lstlisting}
if(a != 4711){
goto ungleich;
}
gleich:
// ...
goto weiter;
ungleich:
// ...
weiter:
// ...
\end{lstlisting}
\end{minipage}
\begin{minipage}[t]{0.33\textwidth}
\textbf{Assembler (Intel Syntax)}
\begin{lstlisting}[language={[x86masm]Assembler}]
cmp rax, 4711
jne ungleich
gleich:
; ...
jmp weiter
ungleich:
; ...
weiter:
; ...
\end{lstlisting}
\end{minipage}
\end{example}
\begin{example}[Schleife]
~
\begin{minipage}[t]{0.49\textwidth}
\textbf{C}
\begin{lstlisting}
for (int i = 100; i > 0; --i) {
summe = summe + a;
}
\end{lstlisting}
\end{minipage}
\begin{minipage}[t]{0.49\textwidth}
\textbf{Assembler (Intel Syntax)}
\begin{lstlisting}[language={[x86masm]Assembler}]
mov rcx, 100
schleife:
add rax, [a]
; decrement rcx and jump if rcx != 0
loop schleife
\end{lstlisting}
\end{minipage}
\end{example}
\begin{example}[Schleife (ohne \code{loop}) ]
~
\begin{minipage}[t]{0.49\textwidth}
\textbf{C}
\begin{lstlisting}
for (int i = 0; i < 100; ++i) {
summe = summe + a;
}
\end{lstlisting}
\end{minipage}
\begin{minipage}[t]{0.49\textwidth}
\textbf{Assembler (Intel Syntax)}
\begin{lstlisting}[language={[x86masm]Assembler}]
mov rcx, 0
schleife:
add rax, [a]
inc rcx
cmp 100, rcx
jne schleife
\end{lstlisting}
\end{minipage}
\end{example}
\begin{example}[Schleife (indirekte Adressierung)]
~
\begin{minipage}[t]{0.49\textwidth}
\textbf{C}
\begin{lstlisting}
int a = 0;
for(int i = 100; i > 0; --i){
a += b[2 * i - 2];
}
\end{lstlisting}
\end{minipage}
\begin{minipage}[t]{0.49\textwidth}
\textbf{Assembler (Intel Syntax)}
\begin{lstlisting}[language={[x86masm]Assembler}]
mov rcx, 100
mov rax, 0
mov rbx, array
schleife:
add rax, [rbx+rcx*2-2]
loop schleife
\end{lstlisting}
\end{minipage}
\end{example}
\section{Calling Conventions}
\begin{definition}[Calling Conventions]
Wenn Assembler-Routinen aus einer Hochsprache aufgerufen werden sollen, müssen bestimmte \vocab{Calling-Conventions} eingehalten werden. Diese legen fest, wie Parameter und Rückgabewerte übergeben werden, und welche Register nach dem Aufruf noch zur Verfügung stehen.
Auch innerhalb eines Assembler-Programms macht es Sinn, sich an (eigene) Calling-Conventions zu halten.
\end{definition}
\begin{example}
~
\begin{lstlisting}[language={[x86masm]Assembler}]
SECTION .data
input1: db 0
input2: dd 0
SECTION .text
get_int:
call read_int ;jump and push address to stack
mov [rbx], rax
ret ;pop return address from stack and jump
; ...
mov rbx, input1
call get_int
mov rbx, input2
call get_int
; ...
\end{lstlisting}
\end{example}
\subsection{cdecl Calling Convention (32 bit)}
\begin{itemize}
\item Parameterübergabe über den Stack: Der Aufrufer legt Argumente auf den Stack
Die Parameter werden in umgekehrter Reihenfolge auf den Stack gelegt.
Nach dem Aufruf muss der Aufrufer die Argumente wieder vom Stack entfernen.
\item Die Unterroutine sichert den Basepointer (\code{ebp}) auf den Stack und schreibt den Stackpointer (\code{esp}) in den Basepointer
\item \vocab{Lokale Variablen} werden auf dem Stack abgelegt
\item Der Rückgabewert wird in \code{eax} übergeben (falls möglich, sonst in Spezialregister)
\item Retten von Registern: Die Register \code{ebx, esi, edi, ebp, cs, ds, ss, es}\footnote{\code{cs, ds, ss, es} sind Segmentregister} werden nicht in der Routine verändert (ggf. mit \code{push} und \code{pop} sichern).
\code{eax, ecx, edx} können bedenkenlos verwendet werden.
\end{itemize}
Der Stack wächst von oben nach unten, d.h. in Richtung kleiner werdender Adressen.
Auf den $i$-ten Parameter kann mit \code{ebp + 4 * i} zugegriffen werden, auf die $i$-te lokale Variable mit \code{ebp - 4 * i}
\begin{example}[cdecl]
~
\begin{minipage}[t]{0.49\textwidth}
\textbf{Aufrufer}
\begin{lstlisting}[language={[x86masm]Assembler}]
; call starts(3,12)
push 12
push 3
call stars
; adjust stack ("delete" arguments)
add esp, 8
\end{lstlisting}
\end{minipage}
\begin{minipage}[t]{0.49\textwidth}
\textbf{Funktion}
\begin{lstlisting}[language={[x86masm]Assembler}]
; void stars(int low, int high)
stars:
; set up stack frame
push ebp
mov ebp, esp
; save ebx (if required)
push ebx
; do something ...
; restore ebx
pop ebx
; reconstruct stack-
; and base-pointer
leave
;return
ret
\end{lstlisting}
\end{minipage}
\end{example}
\begin{table}[htpb]
\centering
\caption{Stack}
\label{tab:stack}
\begin{tabular}{cc|c}
& & \dots\\
& \code{ebp + 12} & Parameter high\\
& \code{ebp + 8} & Parameter low\\
\code{esp + 8} & \code{ebp + 4} & Rücksprungadresse\\
\code{esp + 4} & \code{ebp} & gerettetes \code{ebp} \\
\code{esp} & \code{ebp - 4} & lokale Variable \\
\end{tabular}
\end{table}
\subsection{System V AMD64 ABI Calling Convention (64 bit)}
\begin{itemize}
\item Parameterübergabe über Register \code{rdi, rsi, rdx, r8, r9}. Weitere Parameter in umgekehrter Reihenfolge auf dem Stack
\item \code{rbx, rsp, rbp, r12 - r15} werden nicht von der Funktion verändert (ggf. mit \code{push} und \code{pop} sichern).
\code{rax,rcx,rdx,rsi,rdi,r8-r11} können bedenkenlos verwendet werden.
\end{itemize}
\begin{table}[htpb]
\centering
\caption{Calling Conventions}
\label{tab:registercallingconventions}
\begin{tabular}{cccccc}
\textbf{Verwendung} & 64bit & 32bit & cdecl & System V AMD64 ABI \\
\hline
Accumulator & rax & eax & return & return \\
Allgemein & rbx & ebx & call-preserved & call-preserved \\
Counter für Schleifen & rcx & ecx & call-clobbered & Argument 4, call-clobbered\\
Datenregister (mul, div, IO) & rdx & edx & call-clobbered & Argument 3, call-clobbered\\
String-Op Source & rsi & esi & call-preserved & Argument 2, call-clobbered\\
String-Op Dest & rdi & edi & call-preserved & Argument 1, call-clobbered\\
Basepointer & rbp & ebp & call-preserved & call-preserved\\
Stackpointer & rsp & esp & call-preserved & call-preserved\\
Allg. & r8 & r8d & & Argument 5, call-clobbered \\
Allg. & r9 & r9d & & Argument 6, call-clobbered \\
Allg. (X $\in \{10,11\}$ ) & rX & rXd & & call-clobbered \\
Allg. (X $\in \{12,\ldots,15\}$ ) & rX & rXd & & call-preserved \\
\end{tabular}
\end{table}
\begin{table}[htpb]
\centering
\caption{Calling Conventions (syscall)}
\label{tab:registercallingconventionssyscall}
\begin{tabular}{cccccc}
\textbf{Verwendung} & 64bit & 32bit & \cc{int 0x80} (32 bit) & \cc{syscall} (64 bit) \\
\hline
Accumulator & rax & eax & \multicolumn{2}{c}{\color{red}syscall number / return} \\
Allgemein & rbx & ebx & Argument 1 & call-preserved \\
Counter für Schleifen & rcx & ecx & Argument 2 & {\color{red} call-clobbered}\\
Datenregister (mul, div, IO) & rdx & edx & Argument 3 & Argument 3\\
String-Op Source & rsi & esi & Argument 4 & Argument 2\\
String-Op Dest & rdi & edi & Argument 5 & Argument 1\\
Basepointer & rbp & ebp & Argument 6 & \\
Stackpointer & rsp & esp & call-preserved &\\
Allg. & r8 & r8d & & Argument 5\\
Allg. & r9 & r9d & & Argument 6\\
Allg. & r10 & r10d & & Argument 4\\
Allg. & r11 & r11d & &{\color{red}call-clobbered} \\
Allg. (X $\in \{12,\ldots,15\}$ ) & rX & rXd & & call-preserved \\
\end{tabular}
\end{table}
\begin{example}[Funktionsaufruf (64 bit)]
~
\begin{lstlisting}[language={[x86masm]Assembler}]
; void starts(int low, int high)
; call stars(3,12)
mov rdi, 3
mov rsi, 12
call stars
\end{lstlisting}
\end{example}
\section{Beispiele}
\todo{Beispiele}
\section{RISC-Architekturen}
\subsection{ARM-Assembly}
Die meisten Smartphones nutzen ARM.
Unterschiede zu x86:
\begin{itemize}
\item \vocab{RISC} statt \code{CISC}:
ARM ist ein Reduced-Instruction-Set-Computing-Prozessor. Es stehen deutlich weniger Befehle zur Verfügung, dafür aber mehr allgemein verfügbare Register als beim Complex Instruction Set Computing.
Einzelne Befehle können schneller ausgeführt werden.
\item ARM-Befehle arbeiten nur auf Registern und nutzen Load/Store-Befehle für den Speicherzugriff
\item Der ARM-Befehlssatz nutzt 31 allgemein verfügbare 64-bit-Register \code{X0} - \code{X30}
\end{itemize}
\subsection{MIPS-Assembly}
MIPS ist ebenfalls eine RISC-Architektur. Es gibt 3 Befehlsformate:
\begin{itemize}
\item R-Befehle: \code{instruction rd, rs, rt}
\code{rs} und \code{rt} sind Quell-Register, \code{rd} ist das Zielregister
\item I-Befehle: \code{instruction rt, rs, imm}
\code{rs} ist das Quellregister, \code{rt} ist das Zielregister, \code{imm} ist ein 16-bit immediate-Wert
\item J-Befehle: \code{instruction addr}
Springt an die angegebene (absolute) Adresse
\end{itemize}
\section{Speicherverwaltung}
In C muss Speicher manuell mit \code{malloc} und \code{free} verwaltet werden:
\begin{lstlisting}
int *p;
p = malloc(1024 * sizeof(int));
if(p == NULL) {/* error * /}
free(p);
\end{lstlisting}
\code{malloc} und \code{free} verwalten freien Speicher in einer zyklisch verketteten Liste.
Jedem freien Speicherblock geht ein Header voraus. Der Header enthält die Größe des freien Speicherblocks (ohne Header) und einen Zeiger auf den nächsten freien Speicherblock.
Ein Aufruf von \code{malloc} durchläuft die Liste freier Blöcke und sucht einen passenden. Eine mögliche Strategie ist first fit.
Wenn das genau passt, wird der Block aus der free-Liste entfernt. Anderenfalls wird die benötigte Menge Speicher hinten abgeschnitten und die Größe im Header entsprechend angepasst.
Wenn kein passender Block gefunden wird, wird vom Betriebssystem mehr Speicher angefordert (\code{mmap}, \code{brk}). Hieraus kann ein neuer Block erstellt werden. Der Rückgabewert ist ein Zeiger auf das erste Byte hinter dem Header.
\code{free} erhält (hoffentlich) einen Zeiger, den ein früherer Aufruf von \code{malloc} zurückgegeben hat.
Vor der Adresse liegt also ein \code{malloc}-Header mit der Größenangabe. \code{free} verwendet diese Information um einen freien Block zu generieren, freie Blöcke unmittelbar davor oder dahinter werden ggf. mit diesem Block zusammengefasst.
\todo{Beispiel aus Aufgabe einfügen}
\subsection{Typische Fehler}
\begin{itemize}
\item Es wird nicht überprüft, ob die Rückgabe \code{NULL} ist.
\item Speicher wird nicht wieder freigegeben $\leadsto$ \vocab{memory leak}
\item Ein Zeiger, der in bereits freigegebenen Speicherbereich zeigt, wird verwendet: Undefiniertes Verhalten. Ggf. ein \vocab{Segmentation Fault}, falls die Adressen dem Programm mittlerweile nicht mehr gehören.
\item Aufruf von \code{free} auf einem Zeiger, der nicht Ergebnis eines \code{malloc}-Aufrufs war: Undefiniertes Verhalten.
\end{itemize}
\begin{remark}
Das Programm \code{valgrind} oder die Option \code{-fsanitize=address} beim Kompilieren helfen beim Debuggen solcher Fehler.
\end{remark}

View File

@ -0,0 +1,248 @@
\subsection{Motivation}
Ein Programm soll häufig auf Eingaben von verschiedenen Quellen reagieren können. Beispielsweise ein TCP-Socket, \cc{STDIN} oder sonstige IPC.
Eingabefunktionen von Sockets (\cc{recvfrom()}, \cc{accept()}, \cc{read()}) blockieren typischerweise, ebenso blockiert \cc{fgets()} zum Lesen von der Tastatur.
Beispiele sind:
\begin{itemize}
\item Generischer TCP-Client, z.B. Telnet
\item Anfragen über TCP und UDP sollen entgegengenommen werden können
\item Anfragen von mehreren Sockets sollen gleichzeitig bedient werden.
\item Ein TCP-Server soll gleichzeitig seinen Listening Socket und seine Connected Sockets bedienen.
\end{itemize}
Es existieren verschiedene Ansätze:
\begin{itemize}
\item Blockierender I/O (unsere Situation)
\item Nicht-blockierender I/O
\item Signalgesteuerter Ablauf
\item I/O Multiplexing mit speziellen Hilfsfunktionen (\cc{select()})
\end{itemize}
\subsection{Blockierender I/O}
Eine Eingabeoperation besteht aus 2 Phasen:
\begin{enumerate}[1.]
\item Warten auf Eintreffen von Daten
\item Kopieren der Daten vom Kernel zum Prozess
\end{enumerate}
Während gewartet wird können offenbar keine anderen Quellen bearbeitet werden.
Dem Betriebssystem ist allerdings bekannt, dass der Prozess wartet, und es kann diesen in den Sleep-Modus versetzen. Der Prozess verbraucht dann keine CPU-Zeit, bis Eingabedaten anliegen.
\subsection{Nicht-blockierender I/O}
Zur Vorbereitung muss zunächst ein Socket (oder allgemein ein file descriptor) in den \vocab[File descriptor!non-blocking Mode]{non-blocking Mode} versetzt werden.
Ein Aufruf einer Funktion, der blockieren würde, liefert dann den Error \code{EWOULDBLOCK}.
Dies ist mit der Systemfunktion \ccintro{fcntl()} (File Control) möglich.
\begin{lstlisting}[gobble=2]
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
int fcntl(int fd, int cmd, /* arg (type depends on cmd) */ ... );
\end{lstlisting}
Als \code{cmd} stehen hier zur flag manipulation die Konstanten
\ccintro{F_GETFL} und \ccintro{F_SETFL} zur Verfügung, die die Flags
abrufen bzw.~setzen.
Üblicherweise liest man dann die flags aus
und kann das flag \ccintro{O_NONBLOCK} hinzufügen,
um einen socket in den non-blocking mode zu versetzen:
\begin{lstlisting}[gobble=2]
// set sockfd to non-blocking mode
int flags, err;
flags = fcntl(sockfd, F_GETFL, 0);
err = fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
// now, sockfd is in non-blocking mode
\end{lstlisting}
Ist der socket im non-blocking modus, so kehrt jeder Aufruf
einer Funktion, die zum blocken eines threads führen würde,
sofort mit der Fehlernummer \ccintro{EWOULDBLOCK} zurück
( \cc{errno} wird entsprechend gesetzt).
\begin{example}[Non-blocking I/O]
\leavevmode
\begin{lstlisting}[gobble=4]
while(!done) {
// ...
if ( ( n = read(STDIN_FILENO, ...) < 0) {
if(errno != EWOULDBLOCK) {
/* error */
} else {
write(sockfd, ...);
}
}
if ( (n = read(sockfd, ...) < 0) {
if(errno != EWOULDBLOCK) {
/* error */
} else {
write(STDOUT_FILENO, ...);
}
}
// ...
}
\end{lstlisting}
\end{example}
Das Problem dieses Ansatzes ist, das ein Busy Waiting realisiert wird. Die Schleife fragt endlos alle Eingabequellen ab, bis Daten anliegen. Dies verbraucht unnötig viel CPU-Zeit.
\subsection{Signal-gesteuerter I/O}
\AP Das Signal \ccintro{SIGIO} kann verwendet werden, um zu erkennen, dass Daten anliegen.
Der Prozess setzt mit \cc{sigaction()} einen Signal Handler. Dieser reagiert auf das Signal \cc{SIGIO} und liest dann entsprechend die eingehenden Daten.
Der Signal Handler muss diese Daten irgendwie an den Hauptprozess weitergeben. Dies ist umständlich.
Ein Vorteil ist, dass der Prozess in diesem Modell weiterarbeiten kann. Für UDP ist Signal-gesteuerter I/O relativ einfach umsetzbar; für jedes ankommende Datagramm wird ein \cc{SIGIO} ausgelöst. Bei TCP ist dies wesentlich komplizierter, ein \cc{SIGIO} kann hier vieles bedeuten.
\begin{remark}
NTP-Server können Signal-gesteuerten I/O verwenden. Der Prozess selbst hat viel zu tun, Datagramme benötigen bei Ankunft aber einen exakten Zeitstempel.
\end{remark}
\subsection{Die Hilfsfunktion \cc{select()}}
Die Hilfsfunktion \ccintro{select()} stellt universelle Funktionalität für I/O-Multiplexing mehrerer Eingabequellen zur Verfügung.
\cc{select()} kann sowohl blockierend als auch nicht-blockierend verwendet werden.
\begin{lstlisting}[gobble=2]
#include <sys/select.h>
#include <sys/time.h>
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
int select(int maxfdp1, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
// maxfdp1 - Nummer des hoechsten File Descriptor plus 1
// readfds - File Descriptoren, die zum Lesen ueberprueft werden
// writefds - File Descriptoren, die zum Schreiben ueberprueft werden
// execptfds - File Descriptoren, die auf Exceptions ueberprueft werden
// timeout - Maximale Wartedauer; NULL sorgt fuer blockierenden Aufruf
// Rueckgabe:
// > 0 - Anzahl der Descriptoren im Zustand ready for I/O
// = 0 - Timeout
// = -1 - Fehler
\end{lstlisting}
\code{readfds}, \code{writefds} und \code{exceptfds} werden manipuliert.
Lese- bzw. schreibbereite fd-Bits bleiben gesetzt, alle anderen werden auf \code{0} gesetzt.
Mengen von File-Descriptoren (\ccintro{fd_set}) werden als Bit-Flag implementiert.
Es existieren hierzu die Hilfsfunktionen \ccintro{FD_ZERO}, \ccintro{FD_SET},
\ccintro{FD_CLR} sowie \ccintro{FD_ISSET}:
\begin{lstlisting}[gobble =2]
/*!\cc{FD_ZERO}!*/(fd_set *set); // clear all bits
/*!\cc{FD_SET}!*/(int fd, fd_set *set); // turn on bit
/*!\cc{FD_CLR}!*/(int fd, fd_set *set); // clear bit
/*!\cc{FD_ISSET}!*/(int fd, fd_set *set); // check if fd is set
\end{lstlisting}
\begin{warning}
Ein \cc{fd_set} muss unbedingt mit \cc{FD_ZERO} initialisiert werden!
\end{warning}
\cc{select()} wird wie folgt verwendet:\klausurrelevant
\begin{lstlisting}[gobble=2]
int myfd1 = 5, myfd2 = 7;
char buf[1024];
/*!\cc{fd_set}!*/ myreadfds;
while(1) {
// 1. Loeschen
/*!\cc{FD_ZERO}!*/(myreadfds);
// 2. File-Descriptoren hinzufuegen
/*!\cc{FD_SET}!*/(myfd1, myreadfds);
/*!\cc{FD_SET}!*/(myfd2, myreadfds);
// ...
// 3. Aufruf von select
int res = /*!\cc{select}!*/(8, &myreadfds, NULL, NULL, NULL);
// 4. Rueckgabewert ueberpruefen und bereite File-Descriptoren bearbeiten
if(res == 0){
// timeout
} else if(res < 0) {
// error
} else {
if(/*!\cc{FD_ISSET}!*/ (myfd1, myreadfds)){
// read from myfd1
bzero(buf, sizeof(buf));
int rres = read(myfd1, buf, 1024);
if(rres < 0) { /* error */ }
else if(rres == 0) { /* EOF */ }
else { /* data */ }
// ...
}
if(/*!\cc{FD_ISSET}!*/(myfd2, myreadfds)){
// read from myfd2 ...
}
// ...
}
}
\end{lstlisting}
\subsubsection{Wann wird ein Socket File Descriptor bereit?}
\begin{enumerate}[1.]
\item
Ein Socket wird bereits zum Lesen in den Folgenden Fällen:
\begin{itemize}
\item
Es liegen Daten zum Lesen an
\item
Der Leseteil eines TCP-Sockets wurde geschlossen.
$\leadsto$\cc{read()} wird \code{0} zurückliefern).
\item
Ein TCP listening Socket hat einen komplettierten Verbindungsaufbauwunsch.
$\leadsto$ \cc{accept()} wird erfolgreich sein.
\item
Ein Socket-Error liegt vor.%
\footnote{Im Falle eines Socket-Errors wird readable und writeable markiert.}
$\leadsto$ \cc{read()} wird \code{-1} zurückliefern,
\cc{errno} gibt weiteren Aufschluss.
\end{itemize}
\item Ein Socket wird bereit zum Schreiben:
\begin{itemize}
\item
Der Socket Sendepuffer hat genügend Platz zum Schreiben
\item
Der Schreibteil eines Sockets ist geschlossen.
\item
Nach einem nicht-blockierenden Aufruf von \cc{connect()}
wurde der Verbindungsaufbau abgeschlossen (erfolgreich oder fehlerhaft).
\item
Ein Socket-Error liegt vor.
$\leadsto$ \code{write()} wird \code{-1} zurückliefern,
\code{errno} gibt weiteren Aufschluss.
\end{itemize}
\item Beim Socket liegt eine Ausnahmebedingung vor.
\end{enumerate}
Im wesentlichen relevant ist die Benutzung von \cc{select()} für eingehende Daten.
Ein selten genutzter Fall sind sogenannte Out-of-band Daten am TCP-Socket.
Dies sind wichtige Daten, die normale Daten überholen sollen.

View File

@ -0,0 +1,208 @@
\subsection{Wie funktioniert das Internet?}
Wir stellen uns das Internet als abstrakte Wolke vor, bei dem
jeder Nutzer eine Adresse hat, und über diese mit anderen kommunizieren kann.
Also Analogie dient der Briefversand, der grundsätzlich ähnlich funktioniert.
Ein Teilnehmer am Internet zeichnet sich aus durch
\begin{itemize}
\item Seine konkrete \vocab{IP-Adresse}.
\item Den \vocab{Port}, den er verwendet.
\item Das Protokoll, über das er kommuniziert (üblicherweise TCP oder UDP)
\end{itemize}
\todoimg{3-1 11}
\subsection{Das \acf{ip}}
Zur Zeit wird \vocab{IPv4}, d.h.~die vierte Version des \acl{ip}, verwendet.
\Ac{ip} ist ein \vocab{Netzwerkprotokoll}, d.h.~es dient zur Kommunikation von
\begin{itemize}
\item Rechner-Router
\item Router-Router
\end{itemize}
im Netzwerk.
Das \acf{ip} sendet hierzu immer sogenannte \vocab{IP-Datagramme} auf einmal, deren Länge durch $64 \unit{KByte}$ begrenzt ist. In der Praxis kann diese jedoch kleiner sein, und ist durch die Netzwerke beschränkt. Das Ziel des Datagrammes wird durch die IP-Adresse angegeben, die $32$ Bit lang ist.
\begin{warning}
Datagramme können \alert{verloren} gehen sowie \alert{verdoppelt}, \alert{verfälscht} oder in \alert{falscher Reihenfolge} ausgeliefert werden. IP hat hierzu keinerlei Garantien.
\end{warning}
Das neuere Protokoll \vocab{IPv6} funktioniert ähnlich wie IPv4. Es vergrößert den Adressraum auf 128 Bit und ermöglicht es so, jedem Gerät im Internet eine eigene Adresse zuzuweisen.
\subsection{Das \acf{udp}}
Das \ac{udp} ist ein minimales \vocab{Transportprotokoll}. Es arbeitet nur in Endgeräten und nutzt den Dienst von IP um mit anderen Zielrechnern zu kommunizieren.
\ac{udp} nutzt Ports, um mehrerer Prozesse gleichzeitig zu unterstützen.
\begin{warning}
Da \ac{udp} auf \ac{ip} aufsetzt und keine zusätzlichen Garantien bietet, ist \ac{udp} ebenfalls unzuverlässig.
\end{warning}
Ein \ac{udp} Datagramm besteht aus
\begin{itemize}
\item Source Port
\item Länge der übermittelten Daten
\item Destination Port
\item Checksum
\item übertragene Daten
\end{itemize}
Die Checksumme ist hierbei tatsächlich der einzige (optionale!) Schutz vor Datenkorruption.
Im Gegensatz zum \ac{tcp} ist \ac{udp} verbindungslos.
\subsection{Das \acf{tcp}}
Das \ac{tcp} ist wesentlich aufwendiger als \ac{udp},
löst aber die Unzuverlässigkeiten des IP.
Auch hier werden source und destination port angegeben, jedoch auch weitaus mehr Metainformationen,
die Folgendes sicherstellen:
\begin{itemize}
\item Verlorene Datagramme werden wiederholt.
\item Verdoppelte Datagramme werden verworfen.
\item Datagramme werden potenziell umsortiert, um die Reihenfolge sicherzustellen.
\item Verfälschte Datagramme werden erkannt (Checksumme) und ggf.~wiederholt.
\end{itemize}
Der größte Unterschied hierbei ist jedoch vor allem, dass \alert{eine feste Verbindung}
zwischen Sender und Empfänger aufgebaut werden muss, um \ac{tcp} zu nutzen.
Hierzu \vocab[Verbindungsaufbau!initiieren]{initiiert} ein Anwenderprogramm einen Verbindungsaufbau. Akzeptiert die gegenüberliegende Seite diesen Verbindungsaufbau, können Daten in beide Richtungen versendet werden.
\begin{table}[htpb]
\centering
\begin{minipage}{0.45\textwidth}
\begin{tabular}{l | l}
Decimal & Description \\
\hline
$0$ & Reserved \\
$1$ & TCP Multiplexer \\
$5$ & Remote Job Entry \\
$7$ & Echo \\
$9$ & Discard \\
$11$ & Active Users \\
$13$ & Daytime \\
$15$ & Network status program \\
$17$ & Quote of the Day \\
$19$ & Character Generator \\
$20$ & File Transfer Protocol (data) \\
$21$ & File Transfer Protocol \\
$22$ & ssh (secure shell) \\
$23$ & Telnet \\
$25$ & SMTP (Mail Transfer) \\
$37$ & Time \\
$42$ & Host Name Server \\
$43$ & Who is \\
\end{tabular}
\end{minipage}
\begin{minipage}{0.45\textwidth}
\begin{tabular}{l | l}
Decimal & Description \\
\hline
$53$ & Domain Name Server \\
$77$ & Any private RJE service \\
$79$ & Finger \\
$80$ & World Wide Web HTTP \\
$93$ & Device Control Protocol \\
$95$ & SUPDUP Protocol \\
$101$ & NIC Host Name Server \\
$102$ & ISO-TSAP \\
$103$ & X.400 Mail Service \\
$104$ & X.400 Mail Sending \\
$111$ & Sun Microsystems RPC \\
$113$ & Authentication Service \\
$117$ & UUCP Path Service \\
$119$ & USENET News Transfer Protocol \\
$129$ & Password Generator Protocol \\
$139$ & NETBIOS Session Service \\
$160-223$ & Reserved \\
\end{tabular}
\end{minipage}
\caption{Well-Known Ports bei der Verwendung von \ac{tcp}}
\label{tab:well-known-tcp-ports}
\end{table}
\begin{table}[htpb]
\centering
\begin{minipage}{0.45\textwidth}
\begin{tabular}{l | l}
Decimal & Description \\
\hline
$0$ & Reserved \\
$7$ & Echo \\
$9$ & Discard \\
$11$ & Active Users \\
$13$ & Daytime \\
$15$ & Who is up or NETSTAT \\
$17$ & Quote of the Day \\
$19$ & Character Generator \\
$22$ & ssh (secure shell) \\
$37$ & Time \\
$42$ & Host Name Server \\
$43$ & Who is \\
$53$ & Domain Name Server \\
\end{tabular}
\end{minipage}
\begin{minipage}{0.45\textwidth}
\begin{tabular}{l | l}
Decimal & Description \\
\hline
$67$ & Bootstrap Protocol Server \\
$68$ & Bootstrap Protocol Client \\
$69$ & Trivial File Transfer \\
$80$ & World Wide Web HTTP \\
$111$ & Sun Microsystems RPC \\
$123$ & Network Time Protocol \\
$161$ & SNMP net monitor \\
$162$ & SNMP traps \\
$512$ & UNIX comsat \\
$513$ & UNIX rwho daemon \\
$514$ & system log \\
$525$ & Time daemon \\
\end{tabular}
\end{minipage}
\caption{Well-Known Ports bei der Verwendung von \ac{udp}}
\label{tab:well-known-udp-ports}
\end{table}
\autoref{tab:well-known-tcp-ports} und \autoref{tab:well-known-udp-ports} zeigen Ports von \ac{tcp} und \ac{udp}, die standardmäßig für die Server-Prozesse der
beschriebenen Dienste vorhergesehen sind.
Prinzipiell spricht nichts dagegen, über einen dieser Ports andere Verbindungen zu realisieren,
allerdings erfordert das Verwenden eines Ports $< 1024$ in den meisten UNIX-Umgebungen Administrator-Rechte.
Client-Anwendungen verwenden üblicherweise einen beliebigen Port.
Betriebssysteme vergeben hierfür \vocab{Dynamic Ports} (zwischen \code{49152 = 0xC000} und \code{65535 = 0xFFFF}).\footnote{Unter Linux teilweise auch $32768$ bis $61000$. Ursprünglich waren alle Ports $\ge 1024$ für Client-Anwendungen vorgesehen.}
\todoimg{3-1 21}
\subsection{\ac{tcp} und \ac{udp} über \ac{ip}}
Wir fassen also zusammen, dass \ac{tcp} und \ac{udp} über \ac{ip} kommunizieren.
Die (potenziell vielen) Router, die zwischen den beiden Endgeräten sitzen, müssen jedoch
nichts von \ac{udp} oder \ac{tcp} wissen, sie kommunizieren nur über \ac{ip}.
\subsection{Das Format eines \ac{ip}-Datagrammes}
Ein \ac{ip}-Datagramm umfasst insbesondere
\begin{itemize}
\item Quelle und Ziel als IP-Adressen: 32 Bit (IPv4) oder 128 Bit (IPv6)
\item Das Protokoll der höheren Schicht (nur als Spezifikation, z.B.~\ac{tcp} oder \ac{udp}).
\item Einige Kontrollfunktionen.
\end{itemize}
\subsection{Vergleich von \ac{tcp} und \ac{udp} über \ac{ip}}
Bei der Abwägung, welches der beiden Protokolle man verwenden sollte,
sollte Folgendes beachtet werden:
\begin{itemize}
\item \ac{tcp} ist zuverlässig, verbindungsorientiert und eignet sich für Byte-Streams. Es entsteht allerdings ein erheblicher Overhead.
\item \ac{udp} ist ein minimaler Dienst, der möglichst wenig Overhead erzeugt.
\end{itemize}
\subsection{IPC}
Die Netzwerkkommunikation ist insbesondere ein Fall von IPC, die jedoch auch Kommunikation zwischen Prozessen auf verschieden Rechnern ermöglicht.
Diese Prozesse können u.U.~auf verschiedenen Betriebssystemen laufen.
Netzwerkkommunikation ermöglicht auch IPC auf dem gleichen System, hierzu wird die Loopback-Adresse \code{127.0.0.1} bzw. \code{::1} verwendet.
Außerdem stehen \vocab{Unix Domain Sockets} zur Verfügung, die das selbe API verwenden, aber nur für die Kommunikation innerhalb eines Systems verwendet werden.

View File

@ -0,0 +1,203 @@
\subsection{Mögliche Eigenschaften von Servern}
Es soll ein Kommunikationsserver entworfen und programmiert werden, der in der Lage ist, viele Anfragen entgegenzunehmen.
Hierzu sind verschiedene Eigenschaften auszuwählen und zu realisieren:\footnote{Die Kombination aus concurrent und UDP wurde in der Vorlesung nicht behandelt.}
\begin{itemize}
\item iterativ (nacheinander) vs. concurrent (gleichzeitig, nebenläufig)
\item verbindungsorientiert (TCP) vs. verbindungslos (UDP)
\item stateless vs. stateful
\end{itemize}
Welche Konzepte sich eignen, hängt von verschiedenen Faktoren der jeweiligen Anwendung ab:
\begin{itemize}
\item erwartete Anzahl gleichzeitiger Anfragen
\item Größe der Anfrage
\item Schwankungen der Anfrage-Größe
\item verfügbare System-Ressourcen
\end{itemize}
\subsubsection{Iterativ vs. nebenläufig}
\begin{definition}[Iterativer Server]
Ein \vocab[Server!iterativ]{iterativer Server} bearbeitet zu einem Zeitpunkt genau eine Client-Anfrage, d.h.~alle Anfragen werden hintereinander bearbeitet.
\end{definition}
\begin{definition}[Nebenläufiger Server]
Ein \vocab[Server!nebenläufig]{nebenläufiger} (\vocab[Server!concurrent]{concurrent}) Server kann mehrere Client-Anfragen gleichzeitig bearbeiten.\footnote{Es ist auch möglich, dies auf mehrere Rechner zu verteilen, das wurde in der Vorlesung allerdings nicht behandelt.}
\end{definition}
Nebenläufige Server werden häufig eingesetzt, wenn Anfragen lang sind, oder variable Länge haben. Ein nebenläufiger Server besitzt ferner eine höhere Komplexität und benötigt i.d.R.~mehr Systemressourcen.
Iterative Server werden eingesetzt, wenn Anfragen kurz sind bzw. feste Länge besitzen.
\subsubsection{Verbindungslos vs. verbindungsorientiert}
\begin{minipage}[t]{0.49\textwidth}
\textbf{Verbindungslos}
\begin{itemize}
\item Weniger Overhead
\item Keine Begrenzung der Anzahl von Clients
\end{itemize}
\end{minipage}
\begin{minipage}[t]{0.49\textwidth}
\textbf{Verbindungsorientiert}
\begin{itemize}
\item Einfacher zu programmieren, TCP sorgt für zuverlässige Übertragung
\item Benötigt getrennte Sockets für jede aktive Verbindung. Hierdurch ist die Anzahl an Clients begrenzt.
\end{itemize}
\end{minipage}
\subsubsection{Stateless vs. stateful}
\begin{definition}[State]
Die Information, die ein Server über den Status einer aktuellen Client-Interaktion aufrecht erhält, wird als \vocab[Server!State]{State} bezeichnet.
Ein Server, welcher State besitzt, wird als \vocab[Server!stateful]{stateful} bezeichnet (einfacher mit verbindungsorientierten Servern realisierbar), ein Server ohne State heißt \vocab[Server!stateless]{stateless} (typisch für iterativ und verbindungslos).
\end{definition}
\begin{warning}
Verbindungslose Server mit State müssen besonders vorsichtig und sorgfältig konzipiert werden:
\begin{itemize}
\item Ein Client kann jederzeit ausfallen
\item Ein Client kann jederzeit neu starten (d.h. aus der Sicht des Servers mehrmals)
\item Nachrichten können verloren gehen
\item Nachrichten können dupliziert werden
\end{itemize}
\end{warning}
\subsection{Design-Ansätze für Server}
% TODO Slide 122
\subsubsection{UDP Server, iterativ}
Der Server blockiert, bis eine Anfrage empfangen wird und verarbeitet diese dann. Anschließend wird eine Antwort gesendet und auf die nächste Anfrage gewartet.
\todoimg{3 S.123}
Typischerweise wird keine Zustandsinformation gespeichert. Der Server ist somit in dieser Hinsicht unabhängig von der Anzahl der Clients.
Allerdings müssen Clients warten, während andere Anfragen bearbeitet werden. Ankommende Anfragen werden im \vocab{Socket Receive Buffer} zwischengespeichert. Dies ist problematisch, wenn Anfragen sehr lange dauern, da der Socket Receive Buffer nur eine begrenzte Zahl von Anfragen puffern kann und ggf. Anfragen verloren gehen können.
Anfragen, die nicht in ein UDP/IP Datagramm passen, können von diesem Server-Typ nicht unterstützt werden.
\begin{warning}
Sehr große UDP Datagramme sind u.U.~problematisch, da die Wahrscheinlichkeit einer korrekten Übertragung mit der große das Datagrammes abnimmt. Geht ein Teil eines Datagrammes verloren, so wird das gesamte Datagramm verworfen.
\end{warning}
\subsubsection{TCP Server, iterativ}
\todoimg{S. 128}
\begin{lstlisting}
int sock_listen_fd, newfd;
/* initialize server */
while (1) {
newfd = accept(sock_listen_fd, ...);
//handle request
close(newfd);
}
\end{lstlisting}
Der iterative TCP-Server funktioniert analog zum iterativen UDP-Server. Eine Client-Verbindung wird komplett bearbeitet, bevor die nächste Verbindung angenommen wird. Dadurch kann nur eine Verbindung gleichzeitig aktiv sein.
Die Anzahl an wartenden Verbindungen ist begrenzt, daher müssen bei langer Bearbeitungszeit einer Anfrage ggf. Verbindungsaufbauwünsche abgewiesen werden.
Möglicherweise wird der Server nicht gut ausgelastet, wenn Anfragen lange auf andere Ressource (z.B. Festplatte) warten.
Daher ist ein nebenläufiger Server hier i.d.R.~zu bevorzugen.
\subsubsection{TCP Server, nebenläufig}
Für jede Verbindung zu einer Client-Anfrage wird mit \code{fork()}\footnote{Siehe \ref{processeslinux}} in neuer Prozess generiert, der sich um die Bearbeitung der Anfrage kümmert.
Die Verbindung läuft nach der Rückkehr von \code{accept()} auf einem neuen Socket. Der Parent-Prozess schließt die neue Verbindung und wartet sofort wieder auf weitere Anfragen.
\todoimg{S. 131, 132}
\begin{lstlisting}
int main() {
int sock_listen_fd, newfd, child_pid;
/* initialize server... */
while (1) {
newfd = accept(sock_listen_fd, ...);
if (newfd < 0) { /* error */ }
child_pid = fork();
if(child_pid < 0) {
/* error */
} else if (child_pid == 0) { // child
close(sock_listen_fd);
// handle request ...
close(newfd);
exit(0);
} else { // parent
close(newfd);
}
}
\end{lstlisting}
\begin{observe}
Der Aufruf von \code{close()} dekrementiert die Anzahl aktiver Descriptoren und schließt noch nicht direkt den Socket! % Siehe \ref{closefile}.
\end{observe}
\begin{warning}
Nach dem Bearbeiten der Anfrage werden die Child-Prozesse zu Zombies. Der Parent muss sich darum kümmern, diese aufzuräumen.
\end{warning}
\begin{warning}
Wenn bei der Bearbeitung der Anfrage auf shared state zugegriffen werden soll, muss entsprechende IPC verwendet werden.
\end{warning}
Der Systemaufruf \code{fork()} ist relativ aufwendig. Ein möglicher anderer Ansatz wäre es, Threads einzusetzen. Ferner existieren Preforked Server.
\begin{definition}[Preforked Server]
Ein \vocab{Preforked Server} erstellt bereits bevor Anfragen eingehen eine Menge von Child-Prozessen. Jeder Child-Prozess ruft \code{accept()} auf, um auf Verbindungen zu warten. Das Betriebssystem entscheidet bei einer eingehenden Anfrage, welcher der Clients die Verbindung entgegennimmt.
\end{definition}
\begin{example}[Preforked Server]
~
\begin{lstlisting}
#define NB_PROC 10 // number of preforked children
// iterative server
void recv_requests(int fd) {
int f;
while(1) {
f=accept(fd, ...);
// handle request
close(f);
}
}
int main () {
int fd;
// initialize server ...
for (int i = 0; i <NB_PROC; ++i) {
if(fork() == 0){
recv_requests(fd);
}
}
while (1) {
pause();
}
}
\end{lstlisting}
\end{example}
\begin{warning}
Unter manchen Betriebssystemen funktioniert der gleichzeitige Zugriff auf \code{accept} nicht. Dann müsste dies mit einem Mutex geschützt werden.
\end{warning}
Alternativ kann der Parent-Prozess dynamisch die Anzahl von Preforked Children steuern. Statt Prozessen können natürlich auch hier Threads eingesetzt werden (Prethreading).
\begin{remark}
Der Apache Web Server verwendet ab Version 2.0 Preforking.
\end{remark}
\subsubsection{Select Loop}
Ein einziger Prozess kann auch alle Sockets mit Hilfe von \code{select()} bedienen. Dies wird als \vocab{Select Loop} bezeichnet.
Es ist extrem schwierig, dies korrekt zu implementieren, da Client-Anfragen, die ggf. mehrere \code{read() write()}-Zyklen benötigen, korrekt verarbeitet werden müssen.
Hierzu werden komplexe Datenstrukturen benötigt.
\begin{remark}
NGINX, Node.js und Squid WWW Cache verwenden Select Loops.
\end{remark}

View File

@ -0,0 +1,724 @@
\subsection{Einführung zu Sockets}
Das \vocab[Generisches API]{Generische API}
\begin{itemize}
\item unterstützt \vocab[Nachrichten-orientiert]{Nachrichten-orientierte} und \vocab[Verbindungs-orientiert]{Verbindungs-orientierte} Kommunikation.
\item ist betriebssystemunabhängig
\item nutzt I/O Dienste des Betriebssystems
\item unterstützt verschiedene Protokollfamilien
\item ist unabhängig von der konkreten Adress-Repräsentation der kommunizierenden Endpunkte
\item unterstützt ggf.~manche Clients und Server auf spezielle Weise
\end{itemize}
Es gibt verschiedene APIs für den Zugriff auf TCP / IP:
\begin{itemize}
\item (Berkeley-) Sockets, diese werden wir uns genauer ansehen
\item TLI (System V Transport Layer Interface)
\item XTI (X/Open Transport Interface)
\item Winsock
\item MacTCP
\item Asio C++ Library (plattformunabhängig)
\end{itemize}
\subsubsection{Benötigte Funktionen des Network API}
Ein Network API stellt grundsätzlich Folgendes zur Verfügung:
Vorbereitung:
\begin{itemize}
\item Identifikation der beiden Endpunkte der Kommunikation
\item Verbindungsaufbau (ggfs.~kein Verbindungsaufbau, d.h.~verbindungslos)
\item Warten auf eingehende Verbindungswünsche (typisch für Server)
\end{itemize}
Datentransferphase:
\begin{itemize}
\item Senden und Empfangen von Daten
\item Fehlerbehandlung
\end{itemize}
Nachbereitung:
\begin{itemize}
\item Ordentlicher Abbau einer Verbindung
\item ggf.~Fehlerbehandlung
\end{itemize}
\subsubsection{Berkeley-Sockets}
\vocab{Berkeley-Sockets} stammen ursprünglich aus Berkeley-Unix, mittlerweile werden sie einfach Sockets genannt.
Diese werden wir uns genauer ansehen.
Ein Socket ist eine Datenstruktur innerhalb eines Programms und stellt eine \emph{abstrakte Repräsentation} eines kommunizierenden Endpunktes
dar.
Sockets sind durch Systemfunktionen implementiert und arbeiten ähnlich wie andere Unix I/O-Dienste.
Sockets unterstützen verschiedene Protokollfamilien und sind insbesondere unabhängig von der konkreten Adressdarstellung.
\subsubsection{Socket framework}
Mit einem Socket können wir vom User Space aus auf Funktionalität des Kernel Space, insbesondere
TCP oder UDP, und damit indirekt auf IP und Netzwerktreiber zugreifen. Der direkte Zugriff auf \ac{ip} benötigt in der Regel
privilegierte Rechte.
\subsubsection{Unix Descriptor Table}
Unix führt eine \vocab{Descriptor Table}, die jeweilige Datenstrukturen enthält (siehe auch \autoref{descriptortable}).
Dieser kann sowohl Dateideskriptoren (wie bereits kennengelernt), als auch Socket
Descriptoren enthalten.
Ein \vocab{Socket Descriptor} ist ein normaler File Descriptor, dessen Eintrag in der Descriptor Table jedoch eine Address-Family, einen Service, Local IP, Remote IP, Local Port und Remote Port enthält.
\subsubsection{Socket Domain Families}
Die \vocab{Protokollfamilie} eines Sockets muss bei Initialisierung festgelegt werden.
Es gibt die folgenden Schlüssel:
\begin{itemize}
\item \vocab{Internet Domain Sockets}, verfügbar mittels \ccintro{AF_INET}
\item \vocab{Unix Domain Sockets}, mittels \ccintro{AF_UNIX} oder \ccintro{AF_LOCAL}
\item \vocab{Novell IPX}, mittels \ccintro{AF_IPX}
\item \vocab{AppleTalk} (buuuuh!), mittels \ccintro{AF_APPLETALK}
\end{itemize}
\begin{remark}
Beachte, dass alle diese macros mit \code{AF_} beginnen, dies steht für \enquote{address family}.
Mittlerweile wird allerdings nicht mehr zwischen Protokollfamilien und Adressfamilien unterschieden.
\end{remark}
\subsubsubsection{Service Typ eines Sockets}
Ein Socket kann einen von drei Typen haben
\begin{description}
\item[Stream]
Schlüssel \ccintro{SOCK_STREAM}, Verbindungs- und Byte-Stream orientiert.
Unter \code{AF_INET} wird \ac{tcp} eingesetzt.
\item[Datagram]
Schlüssel \ccintro{SOCK_DGRAM}, verbindungslos und unzuverlässig.
Unter \code{AF_INET} wird \ac{udp} eingesetzt.
\item[Direktzugriff]
Schlüssel \ccintro{SOCK_RAW}.
Dieser umgeht das Transportprotokoll und greift direkt auf das Kommunikationsprotokoll (im Fall von \code{AF_INET} \ac{ip}) zu.
In der Regel werden spezielle Rechte benötigt.
\end{description}
Die letzte Möglichkeit besteht zwar, erfordert allerdings viel aufwand,
weil IP selbst nicht ports spezifiziert (das machen \ac{udp} / \ac{tcp} ),
und dies somit manuell geschehen muss.
Anwendung findet dies in speziellen Routern oder eigenen Protokollen.
\autoref{tab:protokollfamilien} zeigt die Protokollfamilien und den Service-Typ eines sockets,
und die entsprechenden Protokolle, die verwendet werden.
\begin{table}[htpb]
\centering
\begin{tabular}{c | c | c | c | c | c}
& \cc{AF_INET} & \cc{AF_INET6} & \cc{AF_LOCAL}, \cc{AF_UNIX} & \cc{AF_ROUTE} & \cc{AF_KEY} \\
\hline
\cc{SOCK_STREAM} & \ac{tcp} & \ac{tcp} & Ja & & \\
\cc{SOCK_DGRAM} & \ac{udp} & \ac{udp} & Ja & & \\
\cc{SOCK_RAW} & IPv4 & IPv6 & & Ja & Ja
\end{tabular}
\caption{Kombinationsmöglichkeiten von Protokollfamilie und Socket Type für Internetprotokolle}
\label{tab:protokollfamilien}
\end{table}
\subsubsubsection{Erzeugung eines Sockets}
Vergleiche für die beiden Parameter auch \autoref{tab:protokollfamilien}
Das Erzeugen eines sockets geschieht mit \ccintro{socket()}.
\begin{lstlisting}[gobble=2]
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// domain gibt die Protokollfamilie an: AF_INET, AF_INET6, AF_LOCAL, ...
// type gibt SOCK_STREAM, SOCK_DGRAM oder SOCK_RAW an
// protocol setzt spezielle Werte im Protokoll-feld
// wird aber bei TCP und UDP automatisch behandelt, und deswegen auf 0 gesetzt.
// Rueckgabewerte: ein socketdeskriptor, -1 im Fehlerfall
\end{lstlisting}
Solch ein Socket stellt nun abstrakte Ressourcen bereit, um zu kommunizieren,
kümmert sich jedoch noch nicht um Adressierung etc.
Um einen Socket zu benutzen, um empfangsbereit zu sein, verwendet man:
\begin{lstlisting}[gobble=2]
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
// sockfd: der behandlete socket file deskriptor
// myaddr: ein Zeiger auf eine (generische) Adresse
// addrlen: Laengenangabe des Adress-structs unter @myaddr
// gibt -1 im Fehlerfall zurueck, 0 sonst
\end{lstlisting}
\subsubsubsection{Socket-Adressen}
Ein \ccintro{struct sockaddr} sieht wie folgt aus:
\begin{lstlisting}[gobble=2]
typedef uint16_t sa_family_t;
struct sockaddr {
sa_family_t sa_family; // address family
char sa_data[14]; // up to 14 bytes of direct address
}
\end{lstlisting}
% \todoquestion{sehe ich das richtig, dass sowohl der socket als auch der addr die adressfamilie bekommen müssen?} Ja. Siehe https://stackoverflow.com/questions/5263262/why-does-struct-sockaddr-contain-a-address-family-field
\begin{remark}
Auf manchen Betriebssystemen gibt es noch ein \ccintro[sin_len]{uint8_t sin_len}
im \cc{struct sockaddr}, das interessiert uns jedoch ersteinmal nicht.
\end{remark}
\begin{table}[htpb]
\centering
\caption{Typen}
\label{tab:types}
\begin{tabular}{lll}
\textbf{Datentyp} & \textbf{Beschreibung} & \textbf{Header} \\
\hline
\ccintro{int8_t}, \ccintro{int16_t} & signed integer ($8 / 16 / 32 / 64$ bit) & \code{sys/types.h}\\
\ccintro{int32_t},\ccintro{int64_t} \\
\ccintro{uint8_t}, \ccintro{uint16_t} & unsigned integer ($8 / 16 / 32 / 64$ bit) & \code{sys/types.h}\\
\ccintro{uint32_t}, \ccintro{uint64_t}\\
\hline
\multirow{2}*{\ccintro{sa_family_t}} & address family of socket address structure,& \multirow{2}*{\code{sys/socket.h}} \\
& normally \cc{uint16_t}& \\
\multirow{2}*{\ccintro{socklen_t}} & length of socket address structure,&\multirow{2}*{\code{sys/socket.h}} \\
& normally \cc{uint32_t} &\\
\hline
\ccintro{in_addr_t} & IPv4 address, normally \cc{uint32_t} & \code{netinet/in.h} \\
\ccintro{in_port_t} & TCP or UDP port, normally \cc{uint16_t} & \code{netinet/in.h}\\
\ccintro{struct in6_addr} & IPv6 address & \code{netinet/in.h}
\end{tabular}
\end{table}
Die Adressen der Internet-Protokollfamilie sehen wie folgt aus:
\begin{lstlisting}
struct in_addr {
in_addr_t s_addr; // 32 bit IPv4 address
}
struct sockaddr_in {
sa_family_t sin_family; // here: AF_INET
in_port_t sin_port; // 16-bit TCP / UDP port number. network byte order!
struct in_addr sin_addr; // 32 bit IPv4 address. network byte order!
char sin_zer[8]; // unused, total size 16 bytes
};
\end{lstlisting}
Das \cc{struct sockaddr_in} wird somit auf ein \cc{struct sockaddr} gecastet,
um an \cc{socket()} übergeben zu werden.
Unser socket enthält somit bereits IP-Adresse, Protokoll-Type und Portnummer,
um einen verbindungsaufbau zu ermöglichen.
\subsubsubsection{Network byte order}
Die Werte \cc{sin_port} sowie \cc{sin_addr} müssen in der sogenannten
\vocab{network byte order} abgelegt sein.
Das liegt daran, dass verschieden Systeme \vocab{Little Endian} oder \vocab{Big Endian}
benutzen können, und wir für die Netzwerkübertragung eine einheitliche Darstellung benötigen.
Die \vocab{Host Byte Order} ist die byte order des hosts.
\begin{definition}
Die \vocab{Network Byte Order} ist als Big Endian definiert.
\end{definition}
\begin{warning}
Beim spezifizieren von Netzwerk-Parametern müssen wir also auf die Endianness achten!
\end{warning}
Hierzu stehen folgende Hilfsfunktionen zur Verfügung:
\begin{lstlisting}
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);
\end{lstlisting}
Hierzu werden die Abkürzungen \emph host, \emph network, \emph short und \emph long benutzt.
\subsubsubsection{Komplettbeispiel: Öffnen eines sockets mit \ac{udp}}
\begin{lstlisting}[gobble=2]
int fd, err;
struct sockaddr_in addr;
fd = socket(AF_INET,SOCK_DGRAM,0);
if (fd<0) { ... } // error handling
addr.sin_family = AF_INET;
addr.sin_port = htons(4711);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
err = bind(fd, (struct sockaddr *) &addr, sizeof(struct sockaddr_in));
if (err<0) { ... } // error handling
\end{lstlisting}
\begin{remark}
Das macro \ccintro{INADDR_ANY} gibt an, dass das Betriebssystem die IP-Adresse
automatisch bestimmt, und zwar die des eigenen Rechners.
Analog kann auch die Portnummer als 0 spezifiziert werden,
dann kümmert sich das Betriebssystem automatisch darum,
einen geeigneten Port zuzuweisen. Diese ist $> 1023$.
\end{remark}
\subsubsubsection{Weitere Socket-Adress-Strukturen}
Die structs for die IPv6-Familie oder für Unix-Domain Sockets können auch
deutlich länger als 16 Byte sein. Deswegen ist die übergabe der Länge
des struct essentiell.
\subsubsubsection{Kommunikation über Sockets mit \ac{udp}}
Wir kennen bereits \code{socket()} und \code{bind()}.
Um nun zwischen Client und Server zu kommunizieren,
führt üblicherweise nur der Server ein \code{bindto()} aus,
und wartet mit \code{recvfrom()} auf eingehende Datagramme.
Mit \code{sendto()} können nun sowohl client als auch Server
Datagramme verschicken.
Auch der Client kann nun mit \code{recvfrom()} Daten empfangen.
Typischerweise befinden sich nun Client und Server in einem
Zyklus aus \vocab{Empfangen}, \vocab{Verarbeiten}, \vocab{Beantworten}.
Mit \code{close()} kann der Socket am Ende geschlossen werden.
\begin{warning}
Es ist sehr wichtig, den Socket am Ende wieder zu schließen,
sonst bleibt der Port auch über das Programmende eine
gewisse Zeit im kernelspace, weil Sockets (grundsätzlich)
kernel-persistence besitzen.
Der Kernel schließt den socket allerdings nach einer
gewissen Zeit ($\approx$ 1min.), das kann jedoch bereits
nervig sein.
\end{warning}
\todoquestion{kann ich einen bestehenden socket übernehmen?}
\subsubsubsection{\code{sendto()}}
Der Aufruf von \code{sendto()} hat die Signatur:
\begin{lstlisting}[gobble=2]
#include <sys/types.h>
#include <sys/socket.h>
int sendto(int sockfd, const void *buff, size_t nbytes,
int flags, const struct sockaddr *to, socklen_t tolen);
// sockfd: file descriptor des sockets
// buff: zu sendender Buffer
// nbytes: Anzahl zu sendender Bytes
// flags: Optionen, spaeter behandlet
// to: Zeiger auf Zieladress-Struktur
// tolen: Laenge der Ziel-Adressstruktur
// return: -1 bei Fehler, sonst Anzahl gesendeter Bytes
\end{lstlisting}
\begin{example}[Senden von Daten]
\label{ex:senden-von-daten-udp}
\begin{lstlisting}[gobble=2]
// nimm an, dass wir den socket sockfd bereits erstellt haben
// (nicht zwingend gebinded)
char msg[64];
int err;
struckt sockaddr_in dest;
strcpy(msg, "hello, world!");
dest.sin_family = AF_INET;
dest.sin_port = htons(4711);
dest.sin_addr.s_addr = inet_addr("130.37.193.13");
err = sendto(
sockfd, msg, strlen(msg) + 1, 0,
(struct sockaddr*) &dest, sizeof(struct sockaddr_in)
);
if (err < 0) { ... } // Fehlerbehandlung
...
\end{lstlisting}
\end{example}
\begin{remark}
Man sollte auf jeden Fall den Fehlerstatus überprüfen.
Die Anzahl der gesendeten bytes sollte vor allem dann
überprüft werden, wenn man potenziell zu viele bytes
gesendet hat.
\end{remark}
\subsubsubsection{Hilfsfunktionen für \ac{ip}-Adressen}
\ac{ip}-Adressen werden zur Verbesserung der Lesbarkeit üblicherweise
als \vocab{Dotted-Decimal-Schreibweise} durch 4 Dezimalzahlen dargestellt,
die jeweist 8 bit, also Zahlen von $0$ bis $255$.
Es stehen zwei Hilfsfunktionen zur Verfügung:
\begin{lstlisting}
#include <arpa/inet.h>
in_addr_t inet_addr(const char *dotted); // Conversion of dotted address to in_addr_t
char *inet_ntoa(struct in_addr network); // Conversion of in_addr_t to dotted
\end{lstlisting}
\begin{warning}
Der Aufruf von \code{inet_ntoa} returned die Adresse in einem
statisch allokierten Buffer, der bei weiteren Aufrufen überschrieben wird.
Damit ist der Aufruf insbesondere \alert{nicht} thread-safe.
\end{warning}
\begin{remark}[Der Vollständigkeit halber]
Es gibt auch \code{inet_aton(const char *cp, struct in_addr *inp)},
die ebenfalls von der dotted schreibweise konvertiert,
und das Ergebnis im entsprechenden struct ablegt.
Man hätte also in \autoref{ex:senden-von-daten-udp} auch folgendes schreiben
können:
\begin{lstlisting}
// dest.sin_addr.s_addr = inet_addr("130.37.193.13");
char dest_addr[15] = "130.37.193.13";
inet_aton( &dest_addr, &dest.sin_addr );
\end{lstlisting}
\end{remark}
\subsubsubsection{\code{recvfrom()} - Empfangen von Daten}
\begin{lstlisting}
#include <sys/types.h>
#include <sys/socket.h>
int recvfrom(int sockfd, void *buff, size_t nbytes,
int flags, struct sockaddr *from, socklen_t *fromlen);
// sockfd: File Deskriptor des sockets
// buff: buffer fuer zu empfangende daten
// nbytes: laenge des buffers
// flags: optionen, standard 0 (weiter spaeter)
// from: zeiger auf socket-adress-strucktur mit quelladresse (port + ip-adresse)
// fromlen: Zeiger auf lange der adress-struktur.
//return: -1 bei fehler, sonst laenge der empfangenen daten
// das argument fromlen ist ein value-return argument, d.h.
// es wird bei erfolgreichem funktionsaufruf geupdatet,
// und zwar auf die tatsaechliche laenge der
// struct sockaddr *from struktur
// war diese laenger als die urspruengliche eingabe von fromlen
// wird die from - addresse abgeschnitten,
// und fromlen auf den entsprechend groesseren wert
// gesetzt
\end{lstlisting}
\begin{remark}
Mit Quelle ist hierbei nicht gemeint, dass man \emph{nur} Datagramme
von dieser Quelle akzeptiert.
Diese ist unspezifiziert, die Quelladresse wird jedoch
im \code{struct sockaddr *from} abgelegt.
Der Aufruf von \code{recvfrom()} blockiert (im Normalfall),
bis Daten empfangen wurden.
Man kann \code{from} und \code{fromlen} auch auf \code{NULL} setzen,
dann wird die Sender-Adressstruktur nicht gespeichert.
Stattdessen kann man auch einfach \code{recv()} aufrufen,
das die beiden argumente nicht erwartet.
\end{remark}
\begin{example}[Beispiel von \code{recvfrom()}]
\begin{lstlisting}[gobble=4]
char msg[64];
int len, flen;
struck sockaddr_in from;
flen = sizeof(struct sockaddr_in);
len = recvfrom(
sockfd, msg, sizeof(msg), 0,
(struckt sockaddr*) &from, &flen);
if (len < 0) { ... } // fehlerbehandlung
printf("Received %d bytes from host %s with port %d: %s",
len, inot_ntoa(from.sin_addr), ntohs(from.sin_port), msg);
\end{lstlisting}
\end{example}
\begin{warning}
Das obige Beispiel ist natürlich Sicherheitstechnisch völlig grauenhaft.
Wenn wir nicht gerade einen string bekommen haben, könnte das
printen mittels '\%s' der erhaltenen nachricht unseren gesamten speicher
leaken / segfaulten / \ldots
\end{warning}
\begin{warning}
Die manpage behauptet, dass überschüssige Daten eines Datagrammes,
das zu lange für den angegebenen Buffer ist, je nach sockettyp
verworfen werden können.
In der Vorlesung wurden 2 Euro verwettet, weil fälschlicherweise
geclaime wurde,
dass dies bei UDP nicht der Fall ist,
mit einem weiteren \code{recvfrom()} kann dann der Rest der Nachricht
abgerufen werden.
UDP verliert überschüssige Daten.
\end{warning}
\subsubsubsection{Schließen eines sockets}
Funktioniert gleich wie bei files:
\begin{lstlisting}
#include <unistd.h>
int close(int sockfd);
\end{lstlisting}
\subsection{Kommunikation über Sockets mit \ac{tcp}}
Da \ac{tcp} ein Verbindungsorientiertes Protokoll ist,
sieht die Kommunikation etwas anderst aus.
Für einen Server stehen \code{listen()} und \code{accept()}
zur Verfügung, um auf eingehende Verbindungswünsche zu hören,
und sie zu akzeptieren.
Der Client verwendet \code{connect()}, um eine Verbindung aufzubauen.
Besteht die Verbindung, so stehen \code{read()} und \code{write()}
auf beiden Seiten zur Verfügung.
Mit \code{close()} schließt der Client die Verbindung,
dies kann beim server beim nächsten \code{read()} erkannt werden.
\todoimg{3.2 - 70}
\subsubsubsection{Servervorbereitung bei \ac{tcp}}
Nachdem ein Socket mit \code{socket()} erstellt und mit \code{bind()}
gebinded wurde, versetzt man ihn mit \code{listen()} in einen aktiven
Zustand, d.h.~dieser wartet auf Verbindungsaufbauten
\begin{lstlisting}
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// sockfd: socket file deskriptor
// backlog: maximale anzahl gleicher verbindungsaufbauten. typisch: 5
//return: -1 bei fehler, 0 bei erfolg
\end{lstlisting}
\begin{remark}
Der Parameter \code{backlog()} beschränkt nicht die Anzahl der möglichen
Verbindungen, nur die Anzahl derjenigen, die
eine Verbindung aufbauen, aber noch nicht akzeptiert wurden.
\end{remark}
\subsubsubsection{Clientvorbereitung bei \ac{tcp}}
Nachdem der client mit \code{socket()} einen socket erstellt hat,
kann er mit \code{connect()} einen Verbindungsaufbau beginnen.
\begin{lstlisting}
#include <sys/types.h>
#incldue <sys/socket.h>
int connect(int sockfd. const struct sockaddr *serv_addr, socklen_t addrlen);
// sockfd: sockt file deskriptor
// serv_addr: zeiger auf adresse des servers (die enthaelt ip + port)
// addrlen: leange der adress struktur
// return: -1 bei fehler, 1 bei erfolg
\end{lstlisting}
\subsubsubsection{Server: Akzeptieren von Verbindungen bei \ac{tcp}}
Hierzu steht \code{accept()} zur Verfügung.
\begin{lstlisting}
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// sockfd: socket file deskriptor
// addr: zeiger auf adress-struktur
// addrlen: laenge der adress-struktur
// return: *neuer* socket-deskriptor, der die verbindung darstellt,
// -1 bei Fehler
\end{lstlisting}
Auch hier wird \code{addrlen} als value-return dynamisch angepasst.
\begin{example}
\begin{lstlisting}[gobble=4]
int sockfd, newsock, res;
sockaddr_in client_addr;
socklen_t addrlen;
res = listen(sockfd,5);
if (res<0) { ... } // fehlerbehandlung
addrlen = sizeof(struct sockaddr_in);
newsock = accept(sockfd,(struct sockaddr *) &client_addr, &addrlen);
if (newsock < 0) { ... }
else {
printf("Received connection from %s!\n", inet_ntoa(client_addr.sin_addr);
}
\end{lstlisting}
\end{example}
\subsubsubsection{Schwebezustand im Verbindungsaufbau}
\todoimg{3.2 - s.76}
Führt ein Client \code{connect()} aus, so findet ein sog. 3-Wege-Handshake
statt.
Nach dem 2. Handshake kehrt \code{connect()} zurück.
Nach dem 3. Handshake kehrt ein evtl. vorhandener \code{accept()}
Aufruf unmittelbar zurück.
\begin{remark}
Wir werden später betrachten, wie ein Server mit mehreren
Verbindungen umgehen kann.
\end{remark}
\subsubsubsection{Senden von Daten bei \ac{tcp}}
Der Aufruf ist ähnlich wie der von \code{sendto()} im Falle von \ac{udp}.
\begin{lstlisting}
#include <unistd.h>
ssize_t write(int sockfd, const void *buff, size_t count);
// sockfd: File deskriptor des sockets
// buff: zu sendender buffer
// count: laenge der zu sendenden daten
// return: -1 bei fehler, sonst anzahl gesendeter bytes
\end{lstlisting}
\begin{remark}
Die Anzahl der gesendeten bytes kann durchaus weniger als \code{count} sein.
Mas sollte deswegen immer den Rückgabewert prüfen und ggfs.~restliche Daten
erneut senden.
\end{remark}
\subsubsubsection{Empfangen von Daten bei \ac{tcp}}
Hierzu gibt es \code{read()}
\begin{lstlisting}[gobble=2]
#include <unistd.h>
ssize_t read(int sockfd, void *buff, size_t count);
// sockfd: file deskriptor des sockets
// buff: buffer, in den gelesen wird
// count: groesse des buffers
// return: -1 bei felher
// 0: sonderfall: kennzeichnung von end of file
// >0 anzahl der empfangenen bytes
\end{lstlisting}
\begin{remark}
Der Rückgabewert von \code{read()} kann natürlich kleiner als \code{count()} sein.
Der return value $0$ für EOF signalisiert den Wunsch auf Verbindungsabbau,
d.h.~der Kommunikationspartner hat \code{close()} aufgerufen.
\end{remark}
\subsubsubsection{Aufruf von \code{close()}}
\code{close()} \vocab{markiert} einen Socket zum schließen.
Erst wenn alle existierenden file-deskriptoren des sockets
geschlossen wurden, schließt das Betriebssystem diesen tatsächlich.
Bereits an den socket übergebene, aber von \ac{tcp} noch nicht
versendete Daten gehen beim schließen \emph{nicht} verloren.
\begin{remark}
Es ist möglich, dass ein Prozess mit bestehender \ac{tcp}-Verbindung
\code{fork()} aufruft, und der parent die verbindung schließt.
Das child kann weiterhin die Verbindung nutzen.
\end{remark}
\subsubsubsection{Alternative zu \code{close()}: \code{shutdown()}}
Mit \code{shutdown()} kann man einseitig eine Verbindung schließen.
\begin{lstlisting}
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
\end{lstlisting}
\code{howto} ist hierbei eines von \code{SHUT_RD}, \code{SHUT_WR} oder \code{SHUT_RDWR},
die kanonisch nur read, nur write, oder beide Teile der Verbindung schließen können.
\subsubsection{Sockets in Rust}
Wir geben einen groben Überblick über sockets in Rust,
um die Analogien auch über Programmiersprachen hinweg zu geben.
\subsubsection{Übersicht der Adressen bei \ac{udp}}
\todoimg{3.2 - 93, 94}
\subsubsection{Übersicht der Adressen bei \ac{tcp}}
\todoimg{3.2 - 95, 96}
\subsubsection{Hilfsfunktionen bei \ac{tcp} und \ac{udp}}
\ac{udp} und \ac{tcp} bestimmen die Adressen in der Regel automatisch,
dies umfasst
\begin{itemize}
\item Portnummer beim Client
\item IP-Adresse beim Client
\item IP-Adresse beim Server
\item ggfs.~Portnummer beim SEvrer
\end{itemize}
Die konkreten Adressen kann man mit \code{getsockname()} und \code{recvmsg()}
selbst auslesen.
Dies sollen hier jedoch nicht näher betrachtet werden.

View File

View File

@ -0,0 +1,776 @@
\subsection{Grundlagen}
\subsubsection{Varianten von IPC}
\begin{itemize}
\item System V IPC
\item POSIX IPC (wird in dieser Vorlesung behandelt)
\item D-Bus
\end{itemize}
\subsubsection{Teilen von Informationen}
\todoimg{2-3-13}
\begin{definition}[Persistence]\klausurrelevant
``We can define the \vocab{persistence} of any type of IPC as how long an object of that type remains in existence'' (Stevens 1999).
\begin{itemize}
\item \vocab[Persistence!process]{Process persistence}
IPC Objekte existieren bis alle beteiligten Prozesse die Verbindung beenden. Beispiel: Netzwerksockets
\item \vocab[Persistence!kernel]{Kernel persistence}
IPC Objekte existieren bis diese gelöscht werden oder der Kernel neu gestartet wird. Beispiel: Message queue
\item \vocab[Persistence!filesystem]{Filesystem persistence}
IPC Objekte existieren bis sie gelöscht werden. Beispiel: Dateien, Datenbanken. Dies wird i.d.R.~aber nicht als IPC verwendet, es ist unüblich, dass IPC einen Neustart überlebt.
\end{itemize}
\end{definition}
\begin{remark}
Persistenz ist zu unterscheiden von der Art und Weise wie Information gespeichert wird.
Insbesondere folgt daraus, dass z.B.~Pipes vom Kernel verwaltet werden, nicht, dass diese kernel persistence hätten.
Ferner haben FIFOs Namen im Dateisystem, die Information geht jedoch verloren, sobald der letzte Prozess die FIFO schließt.
\end{remark}
\begin{table}[H]
\centering
\caption{Persistenz verschiedener Arten von IPC Objekten}
\begin{tabular}{ll}
\textbf{Art von IPC} & \textbf{Persistenz} \\
Pipe & process\\
FIFO & process\\
\hline
Posix mutex & process\\
Posix condition variable & process\\
Posix read-write lock & process\\
fcntl record locking & process \\ % TODO: was ist das
\hline
Posix message queue & kernel \\
Posix named semaphore & kernel\\
Posix memory-based semaphore & process\\
Posix shared memory & kernel\\
\hline
SysV message queue & kernel\\
SysV semaphore & kernel\\
SysV shared memory & kernel\\
\hline
TCP / UDP socket & process\\
Unix domain socket & process
\end{tabular}
\end{table}
\subsubsection{Mögliche Effekte von \code{fork, exec, \_exit}}
\begin{itemize}
\item \code{fork}
\begin{itemize}
\item Kinder erhalten Kopien von Handles. Das Kind kann so mit dem selben Objekt interagieren wie sein Parent. Dies wird oft zur Interaktion von Parent und Child verwendet.
\item Shared memory wird auch mit dem Child geteilt.
\item Objekte, die im shared memory liegen und das process-shared attribute gesetzt haben, werden mit dem Kind geteilt, z.B. Mutex und Semaphore.% TODO
\end{itemize}
\item \code{exec}
\begin{itemize}
\item Handles bleiben geöffnet (Beispiel: Dateien). Dies ermöglicht anonyme IPC zwischen verschiedenen executables, kann aber zum Sicherheitsproblem werden.
\item Handles werden automatisch geschlossen (Beispiel: POSIX IPC)
\item IPC Objekte verschwinden, sofern sie sich nicht in shared memory befinden und process-shared sind.
% TODO: 2-3-20
\end{itemize}
\item \code{_exit} (Wird von \code{exit()} zum Aufräumen aufgerufen).
\begin{itemize}
\item Alle handles werden geschlossen.
Je nach Persistenz wird dabei das ggf. IPC Objekt zerstört.
\item Thread synchronization ist zu diesem Zeitpunkt egal (der gesamte Prozess wird beendet).
\item Locks werden freigegeben, Posix semaphores jedoch {\color{red} nicht} erhöht.
\end{itemize}
\end{itemize}
\subsection{Dateien}
\begin{definition}[Pfad]
Ein \vocab{path name} enthält maximal \ccintro{PATH_MAX} Byte (inkl. Nullbyte).
Abgesehen von \code{\textbackslash 0} sind alle Zeichen erlaubt, es kann jedoch dateisystemspezifische Einschränkungen geben.
\end{definition}
\begin{definition}[File Descriptor]
Funktionen arbeiten mit \vocab[File Descriptor]{File Descriptoren} (\code{int}).
Standardmäßig definiert sind \ccintro{STDIN_FILENO}, \ccintro{STDOUT_FILENO} und \ccintro{STDERR_FILENO}.
\end{definition}
\subsubsection{Linux Files API}
\subsubsection{Dateien öffnen und erstellen}
\begin{lstlisting}[language=C]
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int oflag, ... /* mode_t mode */);
int creat(const char *path, mode_t mode);
// creat(p,m) = open(p, O_WRONLY|O_CREAT|O_TRUNC,m)
\end{lstlisting}
Es wird ein File Descriptor oder \code{-1} zurückgegeben. Mögliche oflags sind
\begin{itemize}
\item \code{O_RDOLNY, O_WRONLY, O_RDWR}: Lesen und/oder schreiben. Genau eines dieser Flags wird benötigt
\item \code{O_APPEND}: an Datei anhängen
\item \code{O_TRUNC}: Datei zuerst auf Länge $0$ kürzen
\item \code{O_CREAT}: Datei ggf. erstellen. Es müssen dann Zugriffsberechtigungen angegeben werden.
\item \code{O_EXCL}: Wirft einen Fehler falls \code{O_CREAT} gesetzt wurde und die Datei existiert.
\end{itemize}
Berechtigungen:
\begin{itemize}
\item \code{S_IRUSR, S_IWUSR, S_IXUSR}
\item \code{S_IRGRP, S_IWGRP, S_IXGRP}
\item \code{S_IROTH, S_IWOTH, S_IXOTH}
\end{itemize}
\subsubsection{Dateien lesen}
\begin{lstlisting}[language=C]
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
\end{lstlisting}
Liest bis zu \code{nbytes} viele Bytes ein. Gibt die Anzahl an gelesenen Bytes zurück, $0$ falls \code{EOF} und $-1$ bei Fehlern. Wird von einem Terminal gelesen, so wird in der Regel pro Aufruf nur eine Zeile eingelesen. Außerdem kann das Lesen durch ein signal unterbrochen werden.
\begin{lstlisting}[language=C]
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
\end{lstlisting}
Bewegt den file pointer für die nächste Lese/Schreiboperation.
Whence: Setze den Pointer relativ zu
\begin{itemize}
\item \code{SEEK_SET}: Beginn der Datei
\item \code{SEEK_CUR}: Aktuelle Position
\item \code{SEEK_END}: Ende der Datei
\end{itemize}
Gibt die absolute Position nach dem Suchen aus. Insbesondere kann mit \code{lseek(filedes,0,SEEK_CUR)} die aktuelle Position herausgefunden werden.
Bei einem Fehler wird \code{-1} zurückgegeben.
\subsubsection{In Dateien schreiben}
\begin{lstlisting}[language=C]
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
\end{lstlisting}
Gibt die Anzahl an geschriebenen Bytes zurück, bei Fehler $-1$.
\subsubsection{Dateien schließen}
\label{closefile}
\begin{lstlisting}[language=C]
#include <unistd.h>
int close(int fd);
int unlink(const char *path); // Entfernt Namen aus Dateisystem
\end{lstlisting}
\subsubsection{Portable (Buffered) File API / Stream API}
Definiert in \code{stdio.h}, \code{fopen, fread, fwrite, fseek} entsprechen \code{open, read, write, lseek}.
Dieses API ist plattformunabhängig, gepuffert und einfacher zu nutzen.
\noindent
\begin{minipage}{0.5\textwidth}
\begin{lstlisting}[language=C]
FILE fp = fopen(filename, "w+");
size_t len = strlen(data);
fwrite(data, sizeof(char), len, fp);
fclose(fp);
\end{lstlisting}
\end{minipage}
\begin{minipage}{0.49\textwidth}
\begin{lstlisting}[language=C]
int fd = open(filename,
O_WRONLY | O_CREAT | O_APPEND, 0666);
size_t len = strlen(data);
write(fd, data, len);
close(fd);
\end{lstlisting}
\end{minipage}
\subsubsection{File Management Structures}
\begin{definition}[v-node table]
Die \vocab{v-node table} enthält Informationen über Dateien auf der Festplatte (Berechtigungen, Größe, ...). Diese wird von allen Prozessen geteilt.
\end{definition}
\begin{definition}[File Table]
Die \vocab{File Table} enthält für jede geöffnete Datei einen Eintrag mit Pointer auf die v-node table, offset, status flags, flags von \code{open} etc. Die File Table wird von allen Prozessen geteilt.
\end{definition}
\begin{definition}[Descriptor Table]
\label{descriptortable}
Jeder Prozess besitzt eine \vocab{Descriptor Table}. Diese speichert für jeden File Descriptor einen Pointer auf den entsprechenden Eintrag in der File Table.
\end{definition}
Wenn zwei Prozesse die selbe Datei öffnen werden verschiedene Einträge in der File Table erzeugt, die auf den selben Eintrag in der v-node table zeigen.
Bei einem Fork wird die Descriptor Table kopiert, die Einträge zeigen jedoch auf die selben Einträge in der File Table. Insbesondere wird der Offset geteilt.
Alle Operationen sind atomar. Zwischen einzelnen Aufrufen gibt es allerdings keine Garantien, insbesondere die Kombination \code{lseek} und \code{write} ist u.U.~problematisch. Hier muss ggf. \code{O_APPEND} verwendet werden.
\subsection{Message Passing}
\subsubsection{Pipes}
\begin{definition}
Eine \vocab{Pipe} besteht aus zwei verbundenen File Descriptoren. In einen kann geschrieben, aus dem anderen gelesen werden.
\end{definition}
Pipes ermöglichen nur die Kommunikation zwischen Prozessen mit gemeinsamem Ancestor (i.d.R. zwischen Parent und Child). \code{read} blockiert, wenn keine Daten verfügbar sind. \code{seek} ist nicht möglich. Es ist möglich, mit mehreren Prozessen lesend und schreibend zuzugreifen, dies ist jedoch unüblich. \code{read} entfernt die Daten aus dem Puffer, somit kann nur ein Prozess die Information empfangen.
\begin{lstlisting}
#include <unistd.h>
int pipe(int fd[2]); // create pipe
// fd[0] for reading, fd[1] for writing
// return -1 if on error, 0 otherwise
\end{lstlisting}
Üblicherweise wird zunächst \code{pipe} aufgerufen und anschließend \code{fork}. Danach schließen Parent und Child je einen der File Descriptoren.
Es ist wichtig, dass auch lesende Prozesse den write fd schließen, da sonst das Ende der Pipe nie erreicht wird.
Wenn alle lesenden Prozesse die Pipe geschlossen haben, wird beim Schreiben ein \code{SIGPIPE}-Signal generiert.
Die Konstante \code{PIPE_BUF} spezifiziert die Größe des Puffers. Lese- und Schreiboperationen sind nur dann atomar, wenn \code{nbytes <= PIPE_BUF}.
\begin{lstlisting}
pid_t pid;
int fds[2];
if(pipe(fds) < 0){ /* error */ }
if((pid = fork()) < 0){
/* error */
} else if (pid > 0) { //parent
close(fds[0]);
write(fds[1], "Test\n", 6);
close(fds[1]);
} else { //child
close(fds[1]);
n = read(fd[0], line, MAXLINE);
//...
}
exit(0);
\end{lstlisting}
\subsubsection{Redirection}
\vocab{Redirection} wird verwendet um z.B. \code{ls > file.txt} zu implementieren.
\begin{lstlisting}
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
\end{lstlisting}
Beide Funktionen duplizieren einen File Descriptor. \ccintro{dup} gibt den nächsten freien File Descriptor zurück und sorgt dafür, dass dieser auf den selben Eintrag in der File table zeigt.
\ccintro{dup2} tut im Wesentlichen das Selbe, schließt aber zunächst \code{fd2}. Dies geschieht atomar, ist also nicht äquivalent zu \code{close(fd2);fd2= dup(fd);}.
Falls \code{fd2} ungültig ist, wird ein Fehler geworfen. Falls \code{fd == fd2} wird \code{fd2} zurückgegeben und nicht geschlossen.
\begin{lstlisting}
int fd = open("test.txt", O_WRONLY | O_APPEND);
dup2(fd, 1);
printf("test\n"); // writes to test.txt
\end{lstlisting}
\subsubsection{ \code{popen} und \code{pclose} }
\code{popen} kombiniert pipe, fork und system.
\begin{lstlisting}
#include <stdio.h>
FILE *popen(const char *cmd, const char *type);
\end{lstlisting}
Führt \code{cmd} als neuen Prozess aus und verbindet diesen mit einer pipe.
Falls der Typ \code{"r"} ist, wird \code{STDOUT} verbunden und der Parent kann lesen.
Bei \code{"w"} kann der Parent schreiben.
\begin{lstlisting}
int pclose(FILE *fp);
\end{lstlisting}
Schließt die Pipe und gibt den termination status des Child zurück.
\subsubsection{FIFOs (Named Pipes)}
\begin{definition}[FIFO]
\vocab[FIFO]{FIFOs} sind Pipes, denen ein Name im Dateisystem zugewiesen wird. Somit wird die Notwendigkeit eines gemeinsamen Ancestor umgangen. FIFOs werden vom Kernel verwaltet und funktionieren daher nicht über Netzwerkdateisysteme. Der Inhalt wird verworfen sobald der letzte Prozess die FIFO schließt.
\end{definition}
\begin{lstlisting}
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
// returns 0 on success, -1 on error
// mode and pathname as for open()
// implies O_CREAT | O_EXCL
\end{lstlisting}
FIFOs können ganz normal mit \code{open} verwendet werden, entweder mit \code{O_RDONLY} oder mit \code{O_WRONLY}. \code{open} blockiert dabei, bis die FIFO lesend und schreibend geöffnet wurde. Die Reihenfolge des Öffnens ist daher relevant, falls mehrere FIFOs verwendet werden. Hier können Deadlocks auftreten!
Operationen sind nur dann atomar, wenn \code{nbytes <= PIPE_BUF}. Wenn alle Schreiber die Pipe geschlossen haben, wird \code{0} für EOF zurückgegeben. Wenn alle Leser die Pipe geschlossen haben, wird \code{SIGPIPE} generiert.
FIFOs können wie normale Dateien geschlossen (\code{close}) und gelöscht (\code{unlink}) werden.
Um zu verhindern, dass Lese- und Schreiboperationen blockieren, kann das Flag \code{O_NONBLOCK} verwendet werden.
\begin{lstlisting}
int flags;
if((flags = fcntl(fd, F_GETFL, 0)) < 0) { /* error */ }
flags |= O_NONBLOCK;
// flags &= ~O_NONBLOCK;
if(fcntl(fd, F_SETFL, flags) < 0) { /* error */ }
\end{lstlisting}
\begin{table}[htpb]
\centering
\caption{Operationen auf FIFOs}
\label{tab:fifoops}
\begin{tabular}{l|l|l|l}
Operation & bereits geöffnet? & blockierend & nicht blockierend \\
\hline
Öffnen zum Lesen & zum Schreiben geöffnet& ok & ok \\
& nicht zum Schreiben geöffnet& blockiert & ok \\
\hline
Öffnen zum Schreiben & zum Lesen geöffnet & ok & ok \\
& nicht zum Lesen geöffnet & blockiert & \code{ENXIO} \\
\hline
Leere Pipe lesen & zum Schreiben geöffnet & blockiert & \code{EAGAIN}\\
Leere Pipe lesen & nicht zum Schreiben geöffnet & \code{0 (EOF)} & \code{0 (EOF)}\\
\hline
Schreiben & zum Lesen geöffnet & ok & ok\\
& voll & blockiert & \code{EAGAIN}\\
& nicht zum Lesen geöffnet & \code{SIGPIPE} & \code{EPIPE}
\end{tabular}
\end{table}
\subsubsection{Pipes vs. Channels}
Pipes speichern nur einen Puffer und sind daher nur atomar falls \code{nbytes <= PIPE_BUF}. Es ist möglich, dass nur Teile gelesen werden.
Channel hingegen speichern records (typensicher). Sie können wahlweise mit oder ohne Puffer verwendet werden, sind synchronisiert und garantieren, dass records vollständig übertragen werden.
% 2.3.3.2
\subsubsection{Posix Message Queues (MQ)}
\begin{definition}[MQ]\label{posixmq}
\vocab[Message Queue]{Posix Message Queues} implementieren IPC mit einer verketteten Liste von Nachrichten. Diese können in FIFO-Ordnung hinzugefügt und entfernt werden. Zusätzlich werden Prioritäten unterstützt. Die Übertragung ist \textbf{record oriented}\footnote{Pipes sind im Gegensatz dazu \textbf{stream oriented}}, d.h.~jeder Record hat eine definierte Größe, Daten und Priorität.
\end{definition}
Indirekte Kommunikation ist möglich, Prozesse kommunizieren nur mit einer Mailbox, es ist daher nicht notwendig, dass Lesen und Schreiben gleichzeitig erfolgt. MQs sind Kernel-persistent.
MQs verwenden nicht das File API. Statt File Descriptoren kommen \vocab[Message Queue!descriptor]{message queue descriptors} (\code{int}) zum Einsatz.
MQs haben Namen (\code{/somename}). Diese können optional in das Dateisystem gemountet werden.
Zur Verwendung muss gegen eine real-time library gelinkt werden ( \code{-lrt}).
\begin{lstlisting}
#include <mqueue.h>
mqd_t mq_open(const char *name, int oflag);
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);
// oflag and mode as in open for filesystem
// attr can be NULL or pointer to mq attributes
\end{lstlisting}
\vocab[Message Queue!Attributes]{Message Queue Attributes}
\begin{lstlisting}
#include <mqueue.h>
struct mq_attr {
long mq_flags; // 0 or O_NONBLOCK
long mq_maxmsg; // Max. # of messages in queue
long mq_msgsite; // Max. message size (bytes)
long mq_curmsgs; // # of messages currently in queue
};
mqd_t mq_getattr(mqd_t mqdes, struct mq_attr *attr);
mqd_t mq_setattr(mqd_t mqdes, struct mq_attr *newattr, struct mq_attr *oldattr);
// changes only mq_flags
\end{lstlisting}
\subsubsection{Sending and receiving messages}
\begin{lstlisting}
#include <mqueue.h>
int mq_send(mqd_t mqd, const char *msg_ptr, size_t msg_len, unsigned int msg_prio);
ssize_t mq_receive(mqd_t mqd, char *msg_ptr, size_t msg_len, unsigned int *msg_prio);
\end{lstlisting}
\code{mq_send} funktioniert ähnlich wie \code{write}. Rückgabewert \code{0} für OK, \code{-1} bei Fehler. Die Nachricht wird entweder ganz oder gar nicht geschrieben. Sie wird nach den Nachrichten mit selber Priorität und vor denen mit niedrigerer Priorität eingefügt. Die Größe darf \code{mq_msgsize} nicht überschreiten.
\code{mq_read} funktioniert ähnlich wie \code{read}. Zurückgegeben wird die Größe der Nachricht oder \code{-1}. Gelesen wird die älteste der Nachrichten mit maximaler Priorität. Falls \code{msg_len < mq_msgsize} schlägt der Aufruf fehl, egal wie groß die Nachricht tatsächlich ist.
Mit \code{mq_close} und \code{mq_unlink} können MQs wie Dateien geschlossen und gelöscht werden.
\begin{minipage}{0.49\textwidth}
\begin{lstlisting}
mqd_t mqd;
void *ptr;
size_t len;
uint_t prio;
// ...
mqd = mq_open("/mymq", O_WRONLY);
mq_send(mqd, ptr, len, prio);
mq_close(mqd);
\end{lstlisting}
\end{minipage}
\begin{minipage}{0.49\textwidth}
\begin{lstlisting}
struct mq_attr attr;
mqd_t mqd = mq_open("/mymq", O_RDONLY);
mq_getattr(mqd, &attr);
buff = malloc(attr.mq_msgsize);
n = mq_receive(mqd, buff, attr.mq_msgsize, &prio);
// ...
mq_close(mqd);
\end{lstlisting}
\end{minipage}
Soll das Lesen weder blockieren, noch durch regelmäßiges Lesen die CPU unnötig belastet werden, so können \vocab[Message Queue!Notification]{Notifications} verwendet werden.
\begin{lstlisting}
union sigval {
int sival_int;
void *sival_ptr;
};
struct sigevent {
int sigev_notify; // Notification method
// SIGEV_NONE -> no notification
// SIGEV_SIGNAL -> sigev_signo is sent
// SIGEV_THREAD -> thread with notification and sigev_thread_function is created
int sigev_signo; // Notification signal
union sigval sigev_value; // Data passed with notification
void (*sigev_notify_function) (union sigval);
void *sigev_notify_attributes; // Attributes for thread function
};
int mq_notify(mqd_t mqd, const struct sigevent *notification);
// return 0 for ok, -1 on error
// if notification == NULL, the registration is removed
\end{lstlisting}
\begin{lstlisting}
static void tfunc(union sigval sv){
struct mq_attr attr;
ssize_t nr;
void *buf;
mqd_t mqd = *((mqd_t *) sv.sival_ptr);
if(mq_getattr(mqd, &attr) == -1){ /* error */ }
buf = malloc(attr.mq_msgsize);
nr = mq_receive(mqd, buf, attr.mq_msgsize, NULL);
if(nr == -1){ /* error */ }
//...
free(buf);
exit(EXIT_SUCCESS);
}
// ...
mqd_t mqd;
struct sigevent not;
if((mqd = mq_open("/mymq", O_RDONLY)) == -1 ){ /* error */ }
not.sigev_notify = SIGEV_THREAD;
not.sigev_notify_function = tfunc;
not.sigev_notify_attributes = NULL;
not.sigev_value.sival_ptr = &mqd;
if(mq_notify(mqd, &not) == -1){ /* error */ }
pause();
\end{lstlisting}
\subsubsection{Sockets}
Siehe \ref{networksockets} für mehr Details.
Sockets können auch für die Kommunikation innerhalb eines einzelnen Systems eingesetzt werden. Hierfür existieren \vocab{Unix Domain Sockets}, welche im Gegensatz zu Internetsockets keinen Protocol Overhead besitzen und daher effizienter sind. Sockets sind Prozess-persistent, ermöglichen Full-duplex Kommunikation und sind Record- oder Stream-orientiert.
Unix Domain Sockets verwenden das selbe API wie Internet Domain Sockets.
\begin{lstlisting}
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* pathname */
}
\end{lstlisting}
Namen werden ins Dateisystem eingehängt, können aber nicht geöffnet werden.
\begin{lstlisting}
int fd, size;
struct sockaddr_un un;
un.sun_family = AF_UNIX;
strcpy(un.sun_path, "foo.socket");
if((fd = socket(AF_UNIX; SOCK_STREAM, 0)) < 0){ /* error */ }
size = offsetof(struct sukcaddr_un, sun_path) + strlen(un.sun_path);
if(bind(fd, (struct sockaddr *) &un, size) < 0) { /* error*/ }
//...
\end{lstlisting}
\subsection{Posix Shared Memory}
Genau wie Threads können sich auch Prozesse Arbeitsspeicher teilen. Das ist effizienter als Message Passing. Allerdings muss man sich selbst um die Synchronization kümmern.
Geteilter Speicher ist Kernel-persistent.
Möglichkeiten dafür sind
\begin{itemize}
\item Posix shared memory: Mappe den geteilten Speicher in den normalen Adressraum
\item Memory-mapped files
\item Anonymes Memory-Mapping (\code{/dev/zero} oder \code{MAP_ANONYMOUS})
\end{itemize}
\subsection{IPC Synchronization}
\subsubsection{Semaphoren}
Es gibt zwei Arten von Posix \vocab[Semaphore]{Semaphoren}:
\begin{itemize}
\item \vocab[Semaphore!Shared-memory based]{Shared-memory based} wie für Threads, siehe \ref{semaphore}
\item \vocab[Semaphore!Named]{Named Semaphores} werden im Folgenden behandelt.
\end{itemize}
Beide können für IPC verwendet werden. Shared-memory semaphores benötigen dabei natürlich shared memory.
\begin{definition}[Named semaphore]
Named Semaphores ermöglichen es, dass Prozesse, die sich gegenseitig nicht kennen, kommunizieren können. Für Namen gelten dabei die selben Regeln wir für Posix Message Queues (\ref{posixmq}).
\end{definition}
\begin{table}[H]
\centering
\caption{Semaphoren}
\label{tab:semaphores}
\begin{tabular}{c c}
\textbf{named} & \textbf{shared memory based} \\
\hline
\code{sem_open()} & \code{sem_init()}\\
\hline
\multicolumn{2}{c}{\code{sem_wait()}}\\
\multicolumn{2}{c}{\code{sem_trywait()}}\\
\multicolumn{2}{c}{\code{sem_post()}}\\
\multicolumn{2}{c}{\code{sem_getvalue()}}\\
\hline
\code{sem_close()} & \code{sem_destroy()}\\
\code{sem_unlink()}
\end{tabular}
\end{table}
\begin{lstlisting}
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
\end{lstlisting}
\code{sem_open} funktioniert im Wesentlichen wie \code{mq_open}.
Gibt Pointer auf Semaphore oder \code{SEM_FAILED} zurück.
\code{sem_close} ändert den Wert der Semaphore nicht. Falls die Semaphore geschlossen wird, ohne zuvor aufzuräumen, kann dies zu Deadlocks führen!
\subsection{Signals}
\label{signals}
Siehe auch \url{https://man7.org/linux/man-pages/man7/signal.7.html}.
\begin{definition}[Signal]
\vocab[Signal]{Signale} sind asynchrone Benachrichtigungen.
Wenn ein Prozess ein Signal empfängt wird zunächst die Ausführung gestoppt, dann das Signal verarbeitet und anschließen die Ausführung fortgesetzt.
Signale können blockierende system calls unterbrechen.
\vocab[Signal!Type]{Signal types} werden durch eine signal number unterschieden. Für die meisten signal types kann das Programm
\begin{itemize}
\item mit einem Signal Handler reagieren
\item das Signal ignorieren
\end{itemize}
\end{definition}
\begin{table}[htpb]
\centering
\caption{signals}
\label{tab:signals}
\begin{tabular}{l|l|l|l}
\textbf{Signal} & \textbf{Wert} & \textbf{Aktion} & \\
\code{SIGHUP} & 1 & Term & Death of controlling process\\
\code{SIGINT} & 2 & Term & Keyboard interrupt\\
\code{SIGQUIT} & 3 & Core & Keyboard quit\\
\code{SIGILL} & 4 & Core & Illegal Instruction\\
\code{SIGABRT} & 6 & Core & Abort signal\\
\code{SIGFPE} & 8 & Core & Floating point exception\\
{\color{red}\code{SIGKILL}} & 9 & Term & Kill signal\\
\code{SIGSEGV} & 11 & Core & Segmentation Fault\\
\code{SIGPIPE} & 13 & Term & Broken pipe\\
\code{SIGALRM} & 14 & Term & Timer signal\\
\code{SIGTERM} & 15 & Term & Termination signal\\
\code{SIGUSR1} & 30,10,16 & Term & user defined 1\\
\code{SIGUSR2} & 31,12,17 & Term & user defined 2\\
\code{SIGCHLD} & 20,17,18 & Ign & Child stopped or terminated \\
\code{SIGCONT} & 18,19,25 & Cont & Continue if stopped\\
{\color{red}\code{SIGSTOP}} & 17,19,23 & Stop & Stop process\\
\code{SIGTSTP} & 18,20,24 & Stop & Stop typed at tty\\
\code{SIGTTIN} & 21,21,26 & Stop & tty input for background process\\
\code{SIGTTOU} & 22,22,27 & Stop & tty output for background process
\end{tabular}
\end{table}
Signale werden oft zu folgenden Zwecken verwendet:
\begin{itemize}
\item Explizite Benachrichtigungen von anderen Prozessen:
\begin{itemize}
\item Prozess beenden / pausieren / fortsetzen
\item Konfiguration neu einlesen
\end{itemize}
\item Benachrichtigungen vom System
\begin{itemize}
\item Fehler: \code{SIGSEGV, SIGPIPE, ...}
\item Process Events
\begin{itemize}
\item Death of controlling process: \code{SIGHUP}
\item Death of child: \code{SIGCHILD}
\end{itemize}
\item Timer
\end{itemize}
\end{itemize}
Signale können in alten Versionen von UNIX \textbf{unzuverlässig} sein, d.h.~ggf.~verloren gehen.
Signal handler wurden nach einem signal call \textbf{automatisch deregistriert}. In einem kurzen Zeitraum zwischen Deregistrierung und Reregistrierung wurde die Default-Aktion ausgeführt.
Daher sollte das alte \code{signal()} API nicht verwendet werden.
POSIX spezifiziert \textbf{reliable signals}, welche im Folgenden behandelt werden.
\subsubsection{Terminologie}
Signale werden \vocab[Signal!generiert]{generiert / gesendet}, wenn ein Event auftritt.
Das Signal is anschließend \vocab[Signal!pending]{pending} und wird schließlich \vocab[Signal!delivered]{delivered}, d.h.~die zugehörige Aktion wird ausgeführt.
Mögliche Signal Actions sind:
\begin{itemize}
\item \vocab[Signal!default action]{Default action}
\item \vocab[Signal!catch]{catch}, d.h.~ein custom Handler
\item \vocab[Signal!ignore]{ignore}
\end{itemize}
Die Zustellung von Signalen kann blockiert werden. Ein Signal wird aber in jedem Fall generiert.
Die Menge der blockierten Signale wird als \vocab[Signal!Signal mask]{signal mask} bezeichnet.
Insbesondere kann der generierende Prozess nicht feststellen, ob das Signal verarbeitet wird.
Die meisten Implementierungen stellen ein mehrfach generiertes Signal nur einmal zu. Die Reihenfolge der Zustellung verschiedener Signale verschiedenen Typs ist nicht spezifiziert. Signal handler können von weiteren Signalen unterbrochen werden. Dies kann mit \code{SA_NODEFER} verhindert werden.
System calls können den Fehler \code{EINTR} zurückgeben, wenn während der Ausführung ein Signal generiert wird. Manche Implementierung starten system calls automatisch neu. XSI extension-konforme Implementierungen tun das nicht, es kann jedoch das Flag \code{SA_RESTART} gesetzt werden.\footnote{Das sollte man nur machen, wenn man weiß was man tut.}
Mit \code{SA_RESETHAND} kann der Signal Handler nach einem empfangenen Signal automatisch deregistriert werden.\footnote{Der genaue Nutzen ist unklar.}
Im Signal Handler verwendete Funktionen müssen \textbf{eintrittsinvariant} (\textbf{reentrant}) sein. Es gelten ähnliche Anforderungen wie für Thread-Sicherheit. Insbesondere dürfen \code{malloc} und \code{free} nicht verwendet werden.
\subsubsection{Generieren von Signalen}~
\begin{lstlisting}
#include <signal.h>
int kill(pid_t pid, int signo);
// return 0 for OK, -1 on error
// pid > 0 -> send to process with pid
// pid == 0 -> send to process group of current process
// pid < 0 -> send to process group of |pid|
// pid == 1 -> send to all processes
int raise(int signo); // kill(getpid(), signo)
\end{lstlisting}
Ein Signal wird nur gesendet, wenn die Berechtigungen dies erlauben.
\subsubsection{Abfangen von Signalen}
~~
\begin{lstlisting}
struct siginfo {
int si_signo; // signal number
int si_errno; // errno from errno.h
int si_code; // additional info
pid_t si_pid; // pid of sender
uid_t si_uid; // real user ID of sender
void *si_addr; // address that cuased the fault
int si_status; // exit value or signal number
long si_band; // band number for SIGPOLL
/* possibly more */
};
// signal handler
void handler(int signo){ ... }
// alternative handler with flag SA_SIGINFO
void handler(int signo, siginfo_t *info, void *context){ ... }
\end{lstlisting}
\code{info} enthält dabei zusätzliche Informationen über das Signal, ist aber nur teilweise standardisiert.
\code{context} enthält Informationen über den Prozesskontext als das Signal generiert wurde. Das wird hier nicht weiter behandelt.
\begin{table}[htpb]
\centering
\caption{Signale - \code{si_code} }
\label{tab:signalcodes}
\begin{tabular}{lll}
\textbf{Signal} & \textbf{Code} & \textbf{Reason} \\
\hline
\code{SIGILL} & \code{ILL_ILLOPC} & illegal opcode\\
Illegal Instruction & \code{ILL_ILLOPN} & illegal operand\\
& \code{ILL_ILLADR} & illegal addessing mode\\
& \code{ILL_ILLTRP} & illegal trap\\
& \code{ILL_PRVOPC} & privileged opcode\\
& \code{ILL_PRVREG} & privileged register\\
& \code{ILL_COPROC} & coprocessor error\\
& \code{ILL_BADSTK} & internal stack error\\
\hline
\code{SIGFPE} & \code{FPE_INTDIV} & integer divide by zero\\
Floating Point Exception & \code{FPE_INTOVF} & integer overflow\\
& \code{FPE_FLTDIV} & float devide by zero\\
& \code{FPE_FLTOVF} & float overflow\\
& \code{FPE_FLTUND} & float underflow\\
& \code{FPE_FLTRES} & float inexact result\\
& \code{FPE_FLTINV} & invalid float operation\\
& \code{FPE_FLTSUB} & subscript out of range\\
\hline
\code{SIGPOLL} & \code{POLL_IN} & data can be read\\
Async IO & \code{POLL_OUT} & data can be written\\
& \code{POLL_MSG} & input message available\\
& \code{POLL_ERR} & I/O error\\
& \code{POLL_PRI} & high priority message available\\
& \code{POLL_HUP} & device disconnected\\
\end{tabular}
\end{table}
\subsubsection{Signal Handler setzen}
~
\begin{lstlisting}
#include <signal.h>
struct sigaction {
void (*sa_handler) (int);
sigset_t sa_mask; // additional signals to block during handler
int sa_flags; // signal options
void (*sa_sigaction)(int, siginfo_t *, void *);
};
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
// act != NULL -> change action
// oact != NULL -> copy old Actions
\end{lstlisting}
\code{SIG_IGN}: Ignoriere Signal, \code{SIG_DFL}: default action wiederherstellen.
\begin{lstlisting}
struct sigaction act;
memset(&act, '\0', sizeof(act));
act.sa_sigaction = &hdl;
act.sa_flags = SA_SIGINFO; // use sa_sigaction instead of sa_handler
if(sigaction(SIGINT, &act, NULL) < 0){ /* error */ }
//...
\end{lstlisting}
\subsubsection{Signale blockieren}
~
\begin{lstlisting}
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
int pthread_sigmask(...);
\end{lstlisting}
Das Verhalten von \code{sigprocmask} ist nur für single-threaded Prozesse wohldefiniert.
Mögliche Werte für \code{how}:
\begin{itemize}
\item \code{SIG_BLOCK}
\item \code{SIG_SETMASK}
\item \code{SIG_UNBLOCK}
\end{itemize}
\code{SIGKILL} und \code{SIGSTOP} können nicht blockiert oder abgefangen werden.
\code{SIGFPE}, \code{SIGILL}, \code{SIGSEGV} und \code{SIGBUS} können i.d.R.~nicht sinnvoll behandelt werden. Wird die Ausführung weitergeführt, so ist dies undefiniertes Verhalten (außer das Signal wurde von \code{kill} generiert).
\subsubsection{Signal Sets}
~
\begin{lstlisting}
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
\end{lstlisting}
\subsubsection{Auf Signale warten}
~
\begin{lstlisting}
int pause(void); // unistd.h
int sigsuspend(const sigset_t *sigmask); // replaces current signal mask with sigmask and restores it after return
int sigwait(const sigset_t *set, int *sig); // selects pending signal from set and returns. Signal action is not taken. Blocks if no signal is pending
\end{lstlisting}
\code{sigsuspend} ist atomar.
\todo{SIGNALS AND THREADS, AB S. 108}
In shell scripts können Signale mit \code{trap} abgefangen werden.

View File

@ -0,0 +1,238 @@
%! TeX root = ../../sysprog.tex
\subsection{Überblick}
Eine CPU führt ein Programm sequentiell aus; eine Anweisung wird gelesen, ausgeführt und dann der \vocab{program counter} (PC) erhöht.
Um mit Hardware zu interagieren stehen außerdem Interrupts zur Verfügung, welche den PC auf einen spezifizierten Wert ändern.
Die CPU kennt also nur ein Programm.\footnote{Tatsächlich ist das in modernerer Hardware komplizierter, es existieren Pipelines, Hyperthreading, DMA etc. Ferner stellt die CPU verschiedene execution modes zur Verfügung, nämlich kernel mode und user mode.}
\begin{definition}[Process]
Ein \vocab{Prozess} ist eine vom Betriebssystem bereitgestellte Abstraktion um eine Instanz eines Programmes auszuführen und ermöglicht so die ``gleichzeitige'' Ausführung mehrerer Instanzen.
Konzeptuell wird jeder Prozess sequentiell ausgeführt, besitzt einen eigenen virtuellen Prozessor und ist unabhängig von anderen Prozessen.
\end{definition}
Typischerweise hat ein Computer weniger Prozessor als Prozesse ausgeführt werden sollen, das OS kümmert sich daher darum, zwischen Prozessen zu wechseln.
Ein Prozess besitzt eine beschränkte Menge von Speicher, welcher in folgende Sektionen aufgeteilt ist:
\begin{itemize}
\item Text (Code)
\item Data (Globale Variablen)
\item Stack (Frames für nicht abgeschlossene Funktionsaufrufe)
\begin{itemize}
\item Parameter
\item Lokale Variablen
\item Rücksprungadressen
\end{itemize}
\item Heap (dynamisch alloziierte Daten)
\end{itemize}
\todoimg{2-1 p10}
Es wird zwischen \vocab{kernel-mode} und \vocab{user-mode} unterschieden.
Prozesse können nur den jeweils zugewiesenen Speicher nutzen. Der Prozessor läuft im user mode und sorgt dabei für \vocab{Memory segregation}.
Um mit dem Betriebssystem interagieren zu können existieren \vocab{System calls}. Diese ermöglichen das Lesen und Schreiben von Dateien und Sockets, IPC usw.
\todo{Liste von Syscalls}
\todoimg{2-1 12}
\begin{definition}[Process Control Block]
Im \vocab{Process Control Block} (PCB) speichert das Betriebssystem weitere Informationen über den Prozess:
\begin{itemize}
\item Process state
\item Program counter
\item CPU Register
\item CPU scheduling information
\item Memory-management information
\item Accounting information
\item I/O status
\end{itemize}
\end{definition}
\begin{definition}[Context Switch]
Das Wechseln des auf einem Prozessor ausgeführten Prozesses wird als \vocab{context switch} bezeichnet. Das OS speichert den Zustand des alten Prozesse und lädt den Zustand des neuen Prozesses (PCB). Für einen context switch muss das OS die Kontrolle besitzen, d.h.~das ist während eines system call oder mit Hilfe eines interrupt handler möglich.
Context switches sind Overhead; das System verrichtet in dieser Zeit keine sinnvolle Arbeit.
\end{definition}
\todoimg{2-1 15}
\begin{definition}[Process State]
Während ein Prozess ausgeführt wird durchläuft er mehrere \vocab[Process State]{Status}.\footnote{Lang lebe die u-Deklination!}
\begin{itemize}
\item new: Der Prozess wird erstellt
\item running: Befehle werden ausgeführt
\item waiting: Der Prozess wartet
\item ready: Der Prozess wartet, einem Prozessor zugewiesen zu werden
\item terminated: Der Prozess wurde beendet
\end{itemize}
\end{definition}
\todoimg{2-1 16}
\begin{definition}[Scheduling]
Das OS muss entscheiden, wann welcher Prozess ausgeführt wird, dieses Problem wird als \vocab{Scheduling}
Es sind verschiedene Ziele möglich, z.B.~hoher Durchsatz, geringe Latenz oder Real-Time Anforderungen.
Verschiedene Modelle existieren:
\begin{itemize}
\item cooperative: Die Prozesse geben durch einen system call die Kontrolle ab
\item preemptive: Prozesse werden vom OS gestoppt (z.B. mittels timer interrupt)
\end{itemize}
\end{definition}
\subsubsection{Prozesse unter UNIX}
Prozesse werden dynamisch erstellt. Ein Prozess kann über einen \vocab{process identifier}[process identifier] (pid) angesprochen werden.
Alle Prozesse werden von anderen Prozessen erstellt. Hieraus ergibt sich eine Arboreszenz von Eltern-Kind-Beziehungen. Unix speichert diese und kennt somit das Konzept von \vocab{process groups}. Kinder können auf die pid des Parent (ppid) zugreifen.
Es sind verschiedene Optionen denkbar in Bezug auf
\begin{itemize}
\item Resource sharing
\begin{itemize}
\item Parent und Children teilen sich alle Ressourcen
\item manche Ressourcen
\item keine Ressourcen
\end{itemize}
\item Ausführung
\begin{itemize}
\item Parent und Child werden gleichzeitig ausgeführt
\item Parent wartet bis Children ausgeführt wurden
\end{itemize}
\item Adressraum
\begin{itemize}
\item Das Kind ist eine Kopie des Parent (Linux)
\item In das Kind wird ein Programm geladen
\end{itemize}
\end{itemize}
\subsubsection{Process Termination}
Prozesse können aus verschiedenen Gründen beendet werden:
\begin{itemize}
\item Alle Befehle wurden ausgeführt und der Prozess beendet sich selbst (\vocab[Process!exit]{\code{exit}})
\begin{itemize}
\item Über einen Rückgabewert wird Erfolg angezeigt
\item Mittels \code{wait} können Daten von einem Child zum Parent übertragen werden
\item Die Ressourcen werden automatisch vom OS dealloziiert.
\end{itemize}
\item Prozess wird vom OS beendet (\vocab[Process!fatal exit]{\code{fatal exit}})
\begin{itemize}
\item z.B.~bei segmentation fault oder illegal instruction
\end{itemize}
\item Der Parent-Prozess beendet die Ausführung (\vocab[Process!abort]{\code{abort}})
\begin{itemize}
\item z.B.~wenn das Child zu viele Ressourcen verwendet oder nicht mehr gebraucht wird
\item unter manchen Systemen automatisch wenn der Parent-Prozess endet
\end{itemize}
\item Der Prozess wird auf Anfrage eines anderen Prozesses beendet
\begin{itemize}
\item Es wird eine entsprechende Berechtigung benötigt
\end{itemize}
\end{itemize}
\subsection{Prozesse unter Linux}
\label{processeslinux}
Prozesse bilden unter Linux eine Arboreszenz, die Wurzel ist der \code{init}-Prozess.
\begin{lstlisting}
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
\end{lstlisting}
Um einen Prozess zu erzeugen gibt es zwei Möglichkeiten:
\begin{itemize}
\item \code{system()} startet eine Shell, welche einen Befehl ausführt
\item \code{fork()} und \code{exec()}
\end{itemize}
\begin{lstlisting}
int system(const char *cmd);
// forks and executes execl("/bin/sh", "sh", "-c", cmd, (char *) NULL);
// returns when child terminates
pid_t fork(); // returns child pid or 0
// replace program with exec
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ....);
int execle(const char *path, const char *args, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
...
// p -> search in PATH
// e -> specify environment
// l -> argument list
// v -> argument vector
noreturn void exit(int status);
noreturn void abort(void);
pid_t wait(int *stat_loc); // wait for child to terminate
\end{lstlisting}
\begin{example}
~
\begin{lstlisting}
int main()
{
pid_t pid = fork();
if (pid < 0) { /* error */
exit(-1);
} else if (pid == 0) { /* child process */
execlp("/bin/ls", "ls", NULL);
} else { /* parent process */
/* parent will wait for the child to complete */
wait(NULL);
exit(0);
}
\end{lstlisting}
\end{example}
\subsubsection{Signals}
Siehe \ref{signals}
\subsubsection{Zombies}
\begin{definition}[Zombie]
Ein \vocab{Zombie}-Prozess ist ein Child-Prozess, der bereits terminiert ist, aber dessen Parent nicht \code{wait()} aufgerufen hat.
\end{definition}
Wenn ein Child terminiert tritt einer der folgenden Fälle ein:
\begin{itemize}
\item Parent hat \code{wait()} aufgerufen: Child terminiert mit exit status code.
\item Parent hat \code{wait()} nicht aufgerugen: Child wird zum Zombie. Wenn der Parent \code{wait()} nicht aufruft, so wird der Zombie beim Terminieren der Parent durch \code{init} aufgeräumt.
\end{itemize}
Ggf. ist nicht erwünscht, dass der Parent durch \code{wait()} blockiert wird. Alternativ kann regelmäßig \code{wait3()} oder \code{wait4()} aufgerufen werden, oder ein signal handler für das signal \code{SIGCHLD} eingesetzt werden.
\begin{lstlisting}
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
sig_atomic_t child_exist_status;
void clean_up_child_process(int signum) {
int status;
wait(&status);
child_exist_status = status;
}
int main() {
struct sigaction sigchld_action;
bzero(&sigchld_action, sizeof(sigchld_action));
sigchld_action.sa_handler = &clean_up_child_process;
sigaction(SIGCHLD, &sigchld_action, NULL);
// do stuff and fork...
return 0;
}
\end{lstlisting}
\subsubsection{Nachteile von Prozessen}
Das Erstellen von Prozessen ist verhältnismäßig aufwendig. Ferner ist memory isolation nicht in jedem Fall wünschenswert, da dann auf kompliziertere IPC zurückgegriffen werden muss. Eine leichtgewichtigere Möglichkeit um Parallelisierung umzusetzen bieten Threads.
\subsubsection{Tools}
\begin{tabular}{ll}
\code{ps}, \code{pstree}, \code{top} & Laufenden Processes anzeigen\\
\hline
\code{kill} & Signals senden\\
\hline
\code{nice}, \code{renice} & Ausführung ändern\\
\code{\&}, \code{<Ctrl+Z>}, \code{fg}, \code{bg}
\end{tabular}

View File

@ -0,0 +1,693 @@
\subsection{Grundlagen}
\begin{definition}[Thread]
\vocab[Thread]{Threads} erlauben mehrere execution flows innerhalb eines Prozesses.
Alle Threads werden unabhängig voneinander parallel ausgeführt.
Threads teilen sich einen gemeinsamen Adressraum und können somit auf die selben globalen Variablen zugreifen.
Threads teilen sich:
\begin{itemize}
\item Adressraum
\item Geöffnete Dateien
\item Child-Prozesse
\item Alarme und Signale
\end{itemize}
Jeder Thread besitzt:
\begin{itemize}
\item Stack
\item Program counter
\item Register
\item State (executing, blocked, \dots)
\end{itemize}
\end{definition}
\begin{warning}
Das Betriebssystem schützt Threads des selben Prozesses nicht voreinander. Threads können beispielsweise gegenseitig auf den jeweils anderen Stack zugreifen und diesen manipulieren.
\end{warning}
Threads ermöglichen die parallele Ausführung mehrerer Aufgabe, so kann z.B.~ein Thread blockiert sein, während ein anderer weiterarbeitet. Auf Mehrprozessorsystemen können Threads außerdem verschiedene Kerne nutzen.
\subsubsection{Beispielszenario}
Threads werden häufig eingesetzt, wenn ein Prozess \vocab{responsive} bleiben soll, während eine längere Zeit in Anspruch nehmende Aufgabe ausgeführt wird.
Denkbar ist z.B.~folgende Aufteilung
\begin{itemize}
\item \textbf{User interface thread} kümmert sich um das UI
\item \textbf{Processing thread} erledigt aufwändige Berechnungen (viel CPU-Zeit)
\item \textbf{Communication thread} sendet, empfängt und verarbeitet Nachrichten. Blockiert, während auf externe Ressourcen zugegriffen wird.
\end{itemize}
\subsubsection{Threads vs. Processes}
\begin{minipage}[t]{0.49\textwidth}
\textbf{Threads}
\todoimg{2-2-10}
\begin{itemize}
\item Gemeinsamer Adressraum
\item Schnelle context switches
\item Effiziente Kommunikation und Kooperation
\item Schwieriger zu synchronisieren
\item Ein Fehler in einem Thread betrifft auch alle anderen Threads
\end{itemize}
\end{minipage}
\begin{minipage}[t]{0.49\textwidth}
\textbf{Processes}
\todoimg{2-2-10}
\begin{itemize}
\item Voneinander isoliert
\item Kommunikation und Kooperation sind komplex.
\end{itemize}
\end{minipage}
\subsubsection{OS Support für Threads}
Threading kann entweder auf User level oder auf Kernel level angeboten werden.
\vocab[User Thread]{User Threads} werden von einer Library auf User Level angeboten. Der Kernel weiß nicht, dass es sich um Threads handelt. User Threads können daher schnell erstellt und verwaltet werden, jedoch nur einen Prozessorkern nutzen und ein blockierender system call kann ggf. alle Threads blockieren.
\vocab[Kernel Thread]{Kernel Threads} sind als Teil des OS implementiert. Der Kernel kümmert sich um die Verwaltung und das Scheduling.
Threads auf User level können auf verschiedene Art und Weise auf Kernel Threads abgebildet werden:
\begin{itemize}
\item One-to-One
\begin{itemize}
\item Alle Vor- und Nachteile von Kernel Threads
\item Linux, Solaris, Windows
\end{itemize}
\item Many-to-One
\begin{itemize}
\item Alle Vor- und Nachteile von User Threads
\item Beispiel: GNU Portable Threads
\end{itemize}
\item Many-to-Many
\begin{itemize}
\item Etwas flexibler
\item Ältere Versionen von Solaris, Golang
\end{itemize}
\end{itemize}
\subsection{Threads unter Linux}
Linux unterscheide im Kernel nicht wirklich zwischen Prozessen und Threads. Sie unterscheiden sich nur darin, dass Threads den Adressraum teilen.
Der Begriff \vocab{Task} wird verwendet, um Prozesse oder Threads zu beschreiben.
Neue Threads werden mit \code{clone()} erstellt. Das funktioniert analog zu \code{fork()}. Es kann konfiguriert werden, was genau geteilt wird, z.B. Adressraum, file descriptor table, signal handler, \dots
In älteren Kernelversionen hatte jeder Thread eine eigene pid, dies wurde allerdings mittlerweile geändert. Threads haben eine bzgl. des Prozesses eindeutige Thread ID.
In der Regel wird nicht direkt mit \code{clone()} gearbeitet, sondern mit Bibliotheken, welche intern \code{clone()} einsetzen.
In \code{C11} wurde \code{<threads.h>} eingeführt. Glibc unterstützt ab 2.3 die ``Native POSIX Thread Library''.
POSIX Threads (auch \vocab{PThreads}) sind ein standardisiertes Interface für Threads und für verschiedene Betriebssysteme implementiert. Linux unterstützte ursprünglich LinuxThreads\footnote{deprecated}, seit 2.6 auch \vocab{NPTL} (Native POSIX Threads Library.
Funktionen und Datentypen sind in \code{<pthread.h>} implementiert. Beim Linken muss \textbf{libpthread} mit \code{-lpthread} eingebunden werden.
\subsubsection{PThreads}
\subsubsubsection{Creating PThreads}
\begin{lstlisting}
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void*), void *arg);
// thread - Data structure representing the thread
// attr - Attributes. NULL to use defaults
// start_routine - Pointer to thread function
// arg - Arguments passed to start routine
// returns 0 if ok, error codes otherwise
\end{lstlisting}
\begin{example}
~
\begin{lstlisting}
#include <pthread.h>
#include <stdio.h>
pthread_t workThreadID;
void *workThreadFunction(void *arg) {
// ...
return ((void *) 0);
}
int main() {
pthread_create(&workThreadID, NULL, workThreadFunction, NULL);
sleep(2);
return 0;
}
\end{lstlisting}
\end{example}
\subsubsubsection{PThread Thread Attributes}
\begin{lstlisting}
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
// getters and setters are of the form:
int pthead_attr_setAttrName(pthread_attr_t *attr, AttrType t);
int pthead_attr_getAttrName(pthread_attr_t *attr, AttrType *t);
// detach state (PTHREAD_CREATE_DETACHED or PTHREAD_CREATE_JOINABLE)
int pthread_attr_setdetachstate(pthread_attr_t *attr, int d);
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *d);
// scheduling
// schedpolicy, scope, inheritsched
// stack size and address
// stacksize, stackaddr
\end{lstlisting}
\begin{example}
~
\begin{lstlisting}
int main() {
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&threadID, &attr, threadFunction, NULL);
// attr is no longer relevant after thread was created
pthread_attr_destroy(&attr);
// ...
}
\end{lstlisting}
\end{example}
\subsubsubsection{Parameter Passing}
Argumente werden als \code{void*} übergeben.
\begin{warning}
Die übergebenen Argumente sollten auf dem Heap leben und müssen insbesondere noch existieren, wenn der Thread sie verwendet.
\end{warning}
\subsubsubsection{Threads beenden}
Threads können auf verschiedene Arten beendet werden:
\begin{itemize}
\item Implizit: Wenn die Thread Funktion ausgeführt wurde, ist der Rückgabewert der Funktion Rückgabewert des Threads.
\item Explizit mit \code{pthread_exit}
\begin{lstlisting}
void pthread_exit(void *return_val);
\end{lstlisting}
\item Durch einen anderen Thread des selben Prozesses:
\begin{lstlisting}
int pthread_cancel(pthread_t thread);
\end{lstlisting}
Das ist i.d.R. aber keine gute Lösung.
Ob ein Thread auf diese Weise beendet werden kann wird durch die \vocab[Thread!cancelability]{cancelability} festgelegt:
\begin{lstlisting}
int pthread_setcancelstate(int state, int *oldstate);
// PTHREAD_CANCEL_ENABLE or PTHREAD_CANCEL_DISABLE
\end{lstlisting}
Außerdem ist \vocab[Thread!deferred cancelable]{deferred cancelable} (synchron, \code{PTHREAD_CANCLE_DEFERRED}, default) und \vocab[Thread!asynchronous cancelable]{asynchronous cancelable} ( \code{PTHREAD_CANCEL_ASYNCHRONOUS}) möglich:
\begin{lstlisting}
int pthread_setcanceltype(int type, int *oldtype);
\end{lstlisting}
Ein Thread, welcher synchron cancelable ist, kann nur zu bestimmten cancellation points beendet werden. Diese sind explizite Aufrufe von \code{pthread_testcancel()} oder implizit, z.B. \code{pthread_join}, \code{pthread_cond_wait}, \code{sem_wait}
\end{itemize}
Asynchronous canceling ist in der Regel eine schlecht Idee. Synchronous cancelling ist schwierig, da man auf implizite cancellation points achten muss (insbesondere auch in Subroutinen). Empfehlenswert ist es daher, canceling zu vermeiden und einem Thread explizit, z.B. über eine geteilte Variable, mitzuteilen, dass sich dieser beenden soll.
\subsubsubsection{Joining a Thread}
Der Hauptthread sollte warten bis alle Threads beendet wurden.\footnote{ \code{sleep} ist keine gute Lösung}
Um auf einen Thread zu warten und an dessen return value zu gelangen existiert
\begin{lstlisting}
int pthread_joint(pthread_t thread, void **return_val);
\end{lstlisting}
\begin{example}
\begin{lstlisting}
void *workThreadFunction(void *arg) {
int answer = 42;
return ((void*) answer);
}
int main() {
int result;
pthread_create(&workThreadID, NULL, workThreadFunction, NULL);
pthread_join(workThreadID, (void*) &threadResult);
//...
}
\end{lstlisting}
\end{example}
\begin{warning}
Rückgabewerte von Threads sollte auf dem Heap liegen.
\end{warning}
\subsubsubsection{Detach State}
Ein Thread ist entweder \vocab[Thread!joinable]{joinable} (default) oder \vocab[Thread!detached]{detached}. Dies kann beim Starten spezifiziert werden. Ferner können mit \code{int pthread_detach(pthread_t thread);} Threds detached werden.
Ein joinable Thread existiert, bis explizit \code{pthread_join} aufgerufen wird. So kann ein Ergebnis aus dem Thread gelesen werden.
Ein detached Thread wird automatisch aufgeräumt wenn er terminiert. Es ist nicht möglich, den Rückgabewert auszulesen. Detached Threads sind also nur sinnvoll, wenn sie entweder völlig unabhängig sind, oder ihr Ergebnis anderweitig kommunizieren (z.B. über shared state).
Ein Thread, der weder detached ist noch gejoint wurde, wird nach dem Terminieren zum \vocab{Zombie}.
\subsection{Thread Synchronization}
Es ist nicht garantiert, in welcher Reihenfolge Threads ausgeführt werden. Allerdings sind Threads nicht unabhängig voneinander, Teilen sich die selben Daten, warten auf Ergebnisse anderer Threads, benötigen die selben Ressourcen, \dots
\begin{definition}[Race Condition]
Eine \vocab{Race Condition} liegt vor, wenn das Ergebnis einer Berechnung von der Ausführungsreihenfolge abhängt, z.B. weil auf die selbe Ressource zugegriffen wird, oder eine bestimmte Reihenfolge von Operationen erwartet, aber nicht erzwungen wird.
\end{definition}
\begin{example}
In eine doppelt verkettete Liste fügen mehrere Threads Elemente ein. Dabei kann viel schief gehen.
\end{example}
\begin{definition}[Critical Section / Region]
A \vocab{critical section} is a part of the code that accesses shared resources that must not be concurrently accessed by more than one thread of execution.
\end{definition}
Um gleichzeitigen Zugriff auf critical sections zu verhindert werden synchronization primitives benötigt.
\subsubsection{Mutex variables}
\begin{definition}[Mutex]
Ein \vocab{Mutex} (\textbf{Mut}ual \textbf{Ex}clusion) sorgt dafür, dass nur ein Thread zeitgleich in einen kritischen Bereich für eine bestimmte Ressource eintreten kann.
\textbf{Invariante}: Zu jedem Zeitpunkt besitzt nur ein Thread einen \vocab{Lock} für eine Mutex Variable.
Ein Thread, der ein Lock anfragt, muss ggf. warten, wenn ein anderer Thread dieses bereits hat.
Es gibt keine FIFO Garantie für die Reihenfolge, in der das Lock angefragt wird.
\end{definition}
\subsubsubsection{PThread Mutexes}
\begin{lstlisting}
pthread_mutex_t // mutex variable
pthread_mutexattr_t // mutex attributes
// initialization
// dynamically
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);
// statically
pthread_mutex_t mutexVar = PTHREAD_MUTEX_INITIALIZER;
// destruction
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// initialization of arguments
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
// destruction
int pthread_mutexattr_destroy(pthread-mutexattr_t *attr);
// setting attributes
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
// lock
int pthread_mutex_lock(pthread_mutex_t *mutex); // blocks
int pthread_mutex_trylock(pthread_mutex_t *mutex); // 0 if locked, EBUSY otherwise
// unlock
int pthread_mutex_unlock(pthread_mutex_t *mutex);
\end{lstlisting}
\paragraph{Mutex Types}
\begin{itemize}
\item \code{PTHREAD_MUTEX_NORMAL}
\begin{itemize}
\item schnell, keine Überprüfungen
\item Deadlock wenn der selbe Thread zweimal ein Lock anfordert
\item Undefined behavior wenn ein Mutex unlocked wird, der nicht gelocked war
\end{itemize}
\item \code{PTHREAD_MUTEX_ERRORCHECK} : Fehler bei relock oder illegalem unlock
\item \code{PTHREAD_MUTEX_RECURSIVE}: Mehrfache Locks des selben Thread sind möglich, es muss entsprechend oft wieder unlocked werden.
\item \code{PTHREAD_MUTEX_DEFAULT}: Hängt von der Implementierung ab
\end{itemize}
\subsubsection{Condition variables}
\begin{definition}[Condition Variable]
\vocab[Condition Variable]{Condition Variables} lösen das Problem, das ein Thread auf Eintreten eines bestimmten Zustands warten möchte.
Condition Variables werden immer zusammen mit einem Mutex benutzt. Um auf die Condition zu warten, wird ein Lock auf den Mutex benötigt. Sobald gewartet wird, wird dieses Lock implizit aufgegeben. Wird die Bedingung erfüllt, so erhält der Thread das Lock zurück und wird fortgesetzt.
Es gibt dabei keinerlei FIFO-Garantien.
\end{definition}
\begin{example}
Ein Consumer Thread wartet auf Daten eines Producer Thread.
\end{example}
Eine schlechte Lösung für diese Problem ist \vocab{Busy Waiting} / \vocab{Spinning}:
\begin{lstlisting}
int localFlag;
while(!localFlag){
pthread_mutex_lock(&flagMutex);
localFlag = flag;
pthread_mutex_unlock(&flagMutex);
}
// Condition fulfilled!
\end{lstlisting}
Hierdurch wird unnötig Rechenleistung verschwendet.
\todoimg{2-2-65}
\subsubsubsection{PThread Condition Variables}
\begin{definition}
Eine \vocab{condition variable} ist immer mit einem mutex assoziiert.
Ein Thread, der das mutex hält, kann auf die condition warten, und gibt
dadurch das mutex auf.
Durch ein Signal auf diese condition variable wird der thread wieder
aufgeweckt, sobald das mutex frei ist, und erhält das mutex wieder.
\end{definition}
\begin{lstlisting}
pthread_cond_t // condition variable data type
pthread-condattr_t // condition variable attributes
//initialization
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
pthread_cond_t condVar = PTHREAD_COND_INITIALIZER;
//destructor
int pthread_cond_destroy(pthread_cond_t *cond);
//initialization and destruction of attributes
int pthread_condattr_init(pthread_cond_attr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthead_mutex_t *mutex,
const struc timespec *abstime);
// waits only until abstime, returns ETIMEDOUT if abstime passed
int pthread_cond_signal(pthread_cond_t *cond);
// unblocks at least one thread (if there are threads waiting)
int pthread_cond_broadcast(pthread_cond_t *cond);
// unblocks all threads
\end{lstlisting}
In der Regel werden keine besonderen Attribute benötigt.
Die Nutzung sieht dann so aus:
\begin{lstlisting}[gobble=2]
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
// Signal has been received
// Do work
pthread_mutex_unlock(&mutex);
\end{lstlisting}
oder
\begin{lstlisting}[gobble=2]
struct timespec tspec;
struct timeval tval;
pthread_mutex_lock(&mutex);
result = gettimeofday(&tval, NULL);
tspec.tv_sec = tval.tv_sec;
tspec.tv_nsec = tval.tv_usec * 1000;
tspec.tv_sec += 5;
if(pthread_cond_timedwait(&cond, &mutex, &tsepc) == ETIMEDOUT) {
/* timeout */
} else { /* signal received */ }
pthread_mutex_unlock(&mutex);
\end{lstlisting}
\begin{warning}
Der Thread kann länger blockiert bleiben als die maximale Wartezeit, da das Lock auf den Mutex benötigt wird.
\end{warning}
% Kehren wir nicht bei |abstime| zurück und erhalten gerade \emph{nicht} das mutex lock? ~ Max
% nein. Wenn der Thread unblocked wird erhalten wir immer das Lock zurück (siehe 2-2 S.71) ~ Josia
\begin{warning}
Spurious wakeups sind möglich, d.h. dass die Condition bereits von einem anderen Thread behandelt wurde und möglicherweise nicht mehr besteht. Daher sollte Folgendes verwendet werden:
\begin{lstlisting}[gobble=4]
pthread_mutex_lock(&threadFlagMutex);
while(!threadFlag) { //also covers the case that condition already exists
pthread_cond_wait(&thredFlagCondition, &threadFlagMutex);
}
pthread_mutex_unlock(&threadFlagMutex);
\end{lstlisting}
\end{warning}
% Wir sagen, dass ein signal 'unblocks at least one thread'.
% Sagen wir, thread B wartet auf eine condition,
% A holt sich jetzt das mutex, gibt ein signal
% Dann kann B noch nicht aufwachen, weil A das mutex hält.
% Bedeutet ein 'signal' dann einfach nur dass
% zum nächsten zeitpunkt, an dem das mutex frei ist,
% einer der wartenden threads aufgeweckt wird und das mutex erhält?
%
% Falls ja, wie verhält sich das dann mit einem \code{pthread_cond_broadcast}?
% Wir können nicht alle threads gleichzeitig aufwecken,
% wegen den mutexes, die sie erhalten müssen.
%
% Bedeutet das umgekehrt nur, dass jeder dieser threads,
% solange das mutex hinreichend oft frei ist,
% irgendwann aufgeweckt wird?
% ~ Max
% ja, exakt ~ Josia
\subsubsection{Semaphores}
Manchmal ist es notwendig, die Anzahl an geleichzeitig auf eine Ressource zugreifenden Threads zu begrenzen.
\begin{definition}[Semaphore]\label{semaphore}
Eine \vocab{Semaphore} ist ein geteilter Counter, der zählt, wie viele Threads auf eine Ressource zugreifen dürfen.
Ein Thread, der zugreifen möchte, wartet bis der Wert $>0$ ist und dekrementiert diesen dann. Nach dem Zugriff wird der Wert wieder inkrementiert.
\end{definition}
\begin{remark}\label{semaphoremutex}
Ein Mutex ist der Spezialfall einer mit $1$ initialisierten Semaphore.
\end{remark}
\begin{warning}
Bei der Verwendung von Semaphoren kann man leicht Fehler machen. Häufig benötigt man Semaphoren nicht. Insbesondere ist \autoref{semaphoremutex} nicht als Anleitung zu verstehen.
\end{warning}
\begin{remark}
Es kann durchaus sinnvoll sein, eine Semaphore mit $0$ zu initialisieren. Damit kann beispielsweise eine Anzahl von zu konsumierenden Objekten verwaltet werden. Ein Produzent inkrementiert dann die Semaphore ohne sie zuvor dekrementiert zu haben.
\end{remark}
\todoimg{2-2 S.78}
\subsubsubsection{PThread Semaphores}
\begin{lstlisting}
#include <semaphore.h>
sem_t // semahore data type
//initialization
int sem_init(sem_t *sem, int pshared, unsigned int value);
// sem - the semaphore
// pshared - indicates whether shared among processes
// value - initial value
// destructor
int sem_destroy(sem_t *sem);
//waiting
int sem_wait(sem_t *sem); // blocks
int sem_trywait(sem_t *sem); // 0 if locked, otherwise EAGAIN
//releasing
int sem_post(sem_t *sem);
\end{lstlisting}
\begin{remark}
\code{sem_wait(sem_t *sem)} dekrementiert den wert der Semaphore,
sobald der aufruf returned.
\code{sem_post(sem_t *sem)} inkrementiert den Wert.
Der Aufruf ist auch legal, wenn man davor nicht \code{sem_wait} aufgerufen hat,
der Wert wird wie üblich inkrementiert.
Dadurch kann man z.B.~verwalten, wie viele Ressourcen man bearbeiten muss.
Ein producer thread kann dann die semaphore inkrementieren.
\end{remark}
\subsubsection{Barriers}
Threads müssen manchmal aufeinander warten. Erst wenn alle Threads einen bestimmten Punkt erreicht haben, soll die Ausführung fortgesetzt werden. Hierfür eignen sich Barriers:
\begin{definition}[Barrier]
Eine \vocab{Barrier} zählt die Anzahl an Threads, die an einer bestimmten Stelle warten. Sie wird mit der Zahl der beteiligten Threads initialisiert.
Jeder Thread, der die \vocab[Barrier!wait operation]{wait operation} ausführt, inkrementiert einen Zähler. Falls \code{counter < nBarr} blockiert der Thread. Falls \code{counter == nBarr} werden alle Threads unblocked und der Counter zurück auf $0$ gesetzt.
\end{definition}
\todoimg{2-2 S. 85}
\subsubsubsection{PThread Barriers}
\begin{lstlisting}
pthread_barrier_t // barrier data type
pthread_barrierattr_t
// initialization
int pthread_barrier_init(pthread_barrier_t *restrict barrier, pthread_barrierattr_t *restrict attr, unsigned count);
int pthread_barrierattr_init(pthread_barrierattr_t *attr);
// destructor
int pthread_barrier_destroy(pthread_barrier_t *barrier);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);
// getter and setter
int pthread_barrierattr_getpshared(const pthread_barrierattr_t *restrict attr, int *restrict shared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr, int shared);
// waiting
int pthread_parrier_wait(pthread_barrier_t *);
\end{lstlisting}
\begin{example}
~
\begin{lstlisting}
void* workThreadFunction(void* args) {
// do work
pthread_barrier_wait(&threadBarrier);
// do more work
}
void init() {
pthread_barrier_init(&threadBarrier, NULL, 5);
for(int i = 0; i < 5; ++i){
pthread-create(&threadID[i], NULL, workThreadFunction, NULL);
}
// ...
}
\end{lstlisting}
\end{example}
\subsubsection{Read-write locks}
\begin{idea}
Wenn nur gelesen und nichts geändert wird, kann das problemlos gleichzeitig passieren. Daher macht es Sinn, lesenden und schreibenden Zugriff zu unterscheiden.
\end{idea}
\begin{definition}[Read-Write Lock]
Ein \vocab{Read-Write Lock} stellt sicher, dass zu jedem Zeitpunkt eine der folgenden Bedingungen gilt:
\begin{itemize}
\item Ein Thread hat Schreibrechte, kein anderer Thread hat Leserechte
\item Kein Thread hat Schreibrechte, beliebig viele Threads haben Leserechte
\end{itemize}
Wird ein Lesezugriff angefragt, so wird blockiert, falls aktuell ein Schreibzugriff vergeben wurde.
Wird ein Schreibzugriff angefragt, so wird blockiert bis alle anderen Locks aufgegeben wurden.
\end{definition}
\todoimg{2-2 S.90}
\subsubsubsection{PThread Read-Write Lock}
\begin{lstlisting}
pthread_rwlock_t // rw lock data type
pthread_rwlockattr_t // rw lock attributes data type
//initialization
int pthread_rwlock-init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// destructor
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//acquiring read lock
int pthread_rwlock_rdlock(pthread_rwlock_t * rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t * rwlock);
//acquiring write lock
int pthread_rwlock_wrlock(pthread_rwlock_t * rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t * rwlock);
//releasing a lock (read or write)
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
\end{lstlisting}
\subsubsection{Channels}
\begin{definition}[Channel]
Ein \vocab{Channel} ermöglicht die Kommunikation zwischen Threads ohne dass sich der Programmierer selbst um die Synchronization kümmern muss. In einen Channel wird von einer Seite geschrieben und von der anderen Seite gelesen werden. Bei einem vollen Puffer blockiert der Channel.
\end{definition}
\subsection{Deadlocks}
\begin{definition}
Ein \vocab{Deadlock} ist eine Situation, in der mindestens zwei Threads (oder Prozesse) aufeinander warten, aber dabei den jeweils anderen Thread blockieren.
\end{definition}
\todoimg{2-2 S.98}
Damit ein Deadlock entstehen kann müssen alle der folgenden \vocab{Deadlock Conditions} erfüllt sein:
\begin{itemize}
\item \textbf{Mutual exclusion}: Ressourcen können nur von einem Thread zur Zeit verwendet werden.
\item \textbf{Non preemption}: Ressourcen werden nur freiwillig aufgegeben und können nicht gewaltsam entzogen werden.
\item \textbf{Hold \& wait}: Threads können Ressourcen besitzen, während sie auf andere Threads warten.
\item \textbf{Circular wait}: Es existiert ein gerichteter Kreis von Threads, die auf eine Ressource ihres jeweiligen Nachbarn warten.
\end{itemize}
Die ersten drei Bedingungen sind durch Synchronization gegeben.
\begin{definition}[Ressource-Allocation Graph]
Ein \vocab{Ressource-Allocation Graph} ist ein bipartiter Digraph, dessen Knoten Prozesse (Kreise) und Ressourcen (Rechtecke) sind.
Der Wait-For Graph enthält die Kanten $(p,r)$, für die der Prozess $p$ auf die Ressource $r$ wartet, und die Kanten $(r,p)$, für die die Ressource $r$ dem Prozess $p$ gehört.
Alle Ressourcen haben folglich Augangsgrad $\le 1$.
\end{definition}
\begin{definition}[Process Wait-For Graph]
Ein \vocab{Process Wait-For Graph} geht aus einem Ressource-Allocation Graph hervor, indem die alle Kanten $(r,p)$ von Ressourcen zu Prozessen kontrahiert und alle Ressourcen mit Ausgangsgrad $0$ entfernt werden.
\end{definition}
\begin{observe}
Ein gerichteter Kreis im Ressource-Allocation Graph oder Process Wait-For Graph enspricht der Deadlock Condition des Circular Wait.
\end{observe}
Es gibt verschiedene Möglichkeiten, mit Deadlocks umzugehen:
\begin{itemize}
\item \textbf{Deadlock prevention}: Stelle sicher, dass eine der Deadlock Conditions nie eintritt.
Es existieren mehrere Ansätze:
\begin{itemize}
\item Jeder Thread darf nur eine Ressource zur Zeit benutzen (kein Hold \& wait)
\item Wenn ein Thread wartet werden alle Ressourcen entzogen (keine Preemption condition)
\item Totalordnung auf den Ressourcen angeben, Ressourcen dürfen nur in dieser Reihenfolge angefragt werden (kein Circular wait)
\end{itemize}
All dass kann sehr teuer sein!
\item \textbf{Deadlock avoidance}: Ein Supervisor teilt die Ressourcen zu und besitzt dafür Informatione darüber, welcher Thread welche Ressourcen anfragen wird.
\item \textbf{Deadlock detection}: Deadlocks können auftreten. Es wird regelmäßig überprüft, ob ein Deadlock besteht und ggf. ein Prozess beendet.
\item \textbf{Suspecting deadlocks}: Prozesse werden beendet, wenn der Verdacht besteht, dass diese ein Deadlock verursachen.
\end{itemize}
\subsection{Wichtige Threading Mechanismen}
\subsubsection{Worker Threads}
Programme müssen häufig lange laufende Aufgaben erledigen. Das Programm soll dabei weiterhin auf Eingaben oder andere Ereignisse reagieren können.
\begin{definition}[Worker Threads]
Ein \vocab{Dispatcher}-Thread empfängt alle zu erledigenden Aufgaben. Der Dispatcher erstellt für jede Aufgabe einen \vocab{Worker Thread}. Wenn ein Worker Thread fertig ist, meldet dieser das Ergebnis an den Dispatcher.
\end{definition}
Diese Modell wird häufig für Webserver verwendet, in diesem Fall melden die Worker Threads sich nicht beim Dispatcher zurück, sondern senden die Ergebnisse direkt an den Client.
Dieser Mechanismus ist konzeptuell sehr einfach. Wenn sehr viele Tasks verwendet werden, kann Synchronization, z.B. für den Zugriff auf Daten oder das Zurückmelden von Ergebnissen, jedoch aufwändig werden.
\todoimg{2-2}
\subsubsection{Thread Pools}
\begin{definition}[Thread Pool]
\vocab[Thread Pool]{Thread Pools} werden eingesetzt, wenn viele unabhängige, häufig kurzlebige Aufgaben erledigt werden müssen. Um den Overhead durch Threaderstellung zu minimieren, wird eine feste Anzahl an Threads (der Thread Pool) zu Beginn gestartet. Diese arbeiten Aufgaben aus einer \vocab{Task Queue} ab. Existieren dort keine Aufgaben, so blockieren Threads, die nichts zu tun haben. Sobald weitere Aufgaben anfallen werden diese Threads benachrichtigt.
\end{definition}
\todoimg{2-2 111}
Der Zugriff auf die request queue muss synchronisiert werden.
Resultate können ggf. in eine weitere Queue geschreiben werden.
Als Variante ist es möglich, die Zahl der Threads im Pool dynamisch anzupassen. Außerdem kann statt einer queue eine priority queue für die anfallenden Aufgaben eingesetzt werden.
\subsubsection{Thread-Safe Libraries}
\begin{definition}[Thread safe]
Code oder Bibliotheken heißen \vocab{thread safe}, wenn diese parallel von mehreren Threads verwendet werden können.
\end{definition}
Es ist wichtig, ein der Bibliothek nachzulesen, ob ein Aufruf tatsächlich thread safe ist. In manchen Fällen sind spezielle wiedereintretbare Versionen von Funktionen verfügbar.
Für die Threadsicherheit muss Folgendes sicher gestellt werden:
\begin{itemize}
\item \vocab{Wiedereintrittsfähigkeit} (Reentrancy) von Funktionen:
Eine Funktion kann unterbrochen und während der Unterbrechung neu aufgerufen werden. Insbesondere darf dafürnur auf \textbf{purely local state} zugegriffen werden.
\item \vocab{Mutual exclusion}: Zugriff auf critical sections muss geschützt werden.
\end{itemize}
Folgendes ist dafür hilfreich:
\begin{itemize}
\item \textbf{Thread-local data}: Daten, die nicht mit anderen Threads geteilt werden sollen, werden für jeden Thread kopiert.
\item \textbf{Atomare Operationen für den Zugriff auf geteilte Daten}
\item \textbf{Speicher mittels Kommunikation teilen}
\end{itemize}
\begin{warning}
Thread-safety sorgt in der Regel für Overhead und wird daher nicht überall unterstützt.
\end{warning}

View File

@ -0,0 +1,4 @@
% TODO: Beispiele
% - IPC
% - Prozesse und Threads erstellen
% - SIGNAL HANDLER

121
sysprog.sty Normal file
View File

@ -0,0 +1,121 @@
\ProvidesPackage{sysprog}[2022/02/10 - Style file for the Systemnahe Programmierung course in WS 21/22 at Uni Bonn]
\usepackage[ngerman]{babel}
\usepackage[cache]{fancythm}
\usepackage{mkessler-mathfont}
\usepackage{mkessler-math}
\usepackage{enumerate}
\usepackage{mkessler-todo}
\usepackage[index]{mkessler-vocab}
\usepackage{mkessler-code}
\usepackage[a4paper, margin=2.5cm]{geometry}
%\usepackage{mkessler-math}
\usepackage[normalem]{ulem}
\usepackage{pdflscape}
\usepackage{longtable}
\usepackage{xcolor}
\usepackage{dsfont}
\usepackage{csquotes}
\usepackage{tikz}
\usepackage{tikz-cd}
%\usepackage{wrapfig}
\usepackage{listings}
\usepackage{multirow}
\definecolor{KeywordColor}{cmyk}{0.64,0,0.95,0.40} %TODO
\definecolor{mGray}{rgb}{0.5,0.5,0.5}
\definecolor{mOrange}{rgb}{0.9,0.53,0.35}
\lstset{
language=C, % Code langugage
basicstyle=\ttfamily, % Code font, Examples: \footnotesize, \ttfamily
keywordstyle=\color{KeywordColor}, % Keywords font ('*' = uppercase)
commentstyle=\color{mGray}, % Comments font
numberstyle=\tiny\color{mGray},
stepnumber=1, % Step between two line-numbers
numbersep=5pt, % How far are line-numbers from code
frame=none, % A frame around the code
tabsize=2, % Default tab size
captionpos=b, % Caption-position = bottom
breaklines=true, % Automatic line breaking?
breakatwhitespace=false, % Automatic breaks only at whitespace?
showspaces=false, % Dont make spaces visible
showtabs=false, % Dont make tabls visible
columns=flexible, % Column format
stringstyle=\color{mOrange},
escapeinside={/*!}{!*/},
}
\usepackage{float}
%\usepackage{algorithmicx}
\newcounter{subsubsubsection}[subsubsection]
\renewcommand\thesubsubsubsection{\thesubsubsection.\arabic{subsubsubsection}}
\newcommand\subsubsubsection[1]
{
\stepcounter{subsubsubsection}
\medskip
\textbf{\thesubsubsubsection~#1}
\medskip
}
\newcommand\todoimg[1]
{
\todo{FEHLENDES BILD: #1}
}
\usepackage{siunitx}
% Wenn auf die Klausurrelevanz EXPLIZIT hingewiesen wurde
\newcommand\klausurrelevant{
\footnote{\color{red}klausurrelevant!}
}
\usepackage{acro}
\DeclareAcronym{ip}{short = IP, long = Internet Protocol}
\DeclareAcronym{Ip}{short = IP, long = Internet Protocol}
\DeclareAcronym{udp}{short = UDP, long = User Datagram Protocol}
\DeclareAcronym{tcp}{short = TCP, long = Transmission Control Protocol}
\def\alert#1{{\color{red} #1}}
\usepackage{imakeidx}
\makeindex[name = ccode, title = \texttt{C} functions and macros]
\usepackage{hyperref}
\usepackage[quotation]{knowledge}[22/02/12]
\newcommand\main[1]{\underline{#1}}
\newcommand\usage[1]{\textit{#1}}
\knowledgestyle{ccode}{color=purple!30!black, index style = usage, wrap = \code}
\knowledgestyle{ccode unknown}{ wrap = \code, color = brown}
\knowledgestyle{ccode unknown cont}{ wrap = \code}
\knowledgestyle{ccode intro}{color=blue, boldface, index style = main, wrap = \code}
\knowledgestyle{autoref link}{autoref link}
\knowledgestyle{autoref target}{autoref target}
\knowledgenewvariant\cc{
default style = {autoref link, ccode},
unknown style = {ccode unknown},
unknown style cont = {ccode unknown cont},
% unknown warning = false,
% unknown diagnose = false,
}
\knowledgenewvariant\ccintro {
auto knowledge = {autoref, scope=document, also now, index, index name = ccode, wrap = \code},
default style = {autoref target, ccode intro},
unknown style = ccode unknown,
unknown style cont = ccode unknown
}
\knowledgevariantmodifier{\intro*\cc}\ccintro
\knowledgevariantmodifier{\cintro*\cc}\ccintro
\hypersetup{colorlinks, citecolor=violet, urlcolor=blue!80!black, linkcolor=red!50!black, pdfauthor=\@author, pdftitle=\ifdef{\@course}{\@course}{\@title}}
\NewFancyTheorem[thmtools = { style = thmredmargin} , group = { big } ]{warning}

74
sysprog.tex Normal file
View File

@ -0,0 +1,74 @@
\documentclass[10pt,ngerman,a4paper]{mkessler-script}
\course{Systemnahe Programmierung}
\lecturer{Dr. Matthias Frank, Dr. Matthias Wübbeling}
\author{Maximilian Keßler, Josia Pietsch}
\usepackage{sysprog}
\begin{document}
\maketitle
\cleardoublepage
\tableofcontents
\cleardoublepage
\part{Maschinenprogrammierung in Assembler}
\input{inputs/assembler/assembler.tex}
% \subsection{Speicherverwaltung}
\part{Prozesse und Threads}
\section{Prozesse}
\input{inputs/processes-and-threads/processes.tex}
\section{Threads}
\input{inputs/processes-and-threads/threads.tex}
\section{Inter-Process Communication}
\input{inputs/processes-and-threads/ipc.tex}
\part{Netzwerkprogrammierung}
\section{Motivation}
\input{inputs/network_programming/motivation.tex}
\section{Sockets \& Co}
\label{networksockets}
\input{inputs/network_programming/sockets.tex}
\section{I/O Multiplexing}
\input{inputs/network_programming/io_multiplexing.tex}
\section{Server-Strukturen}
\input{inputs/network_programming/server_structure.tex}
\section{Zusammenfassung}
\input{inputs/network_programming/summary.tex}
\part{Übersicht}
\input{inputs/summary/summary.tex}
\cleardoublepage
\printvocabindex
\cleardoublepage
{
\catcode`_=\active
\def_{\_}
\printindex[ccode]
}
\end{document}