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

777 lines
33 KiB
TeX

\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.