lecture-notes/inputs/processes-and-threads/processes.tex
2022-02-15 20:57:33 +01:00

239 lines
9.7 KiB
TeX

%! 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}