Vielen Dank für Ihre Aufmerksamkeit auf unseren zuvor übersetzten Beitrag zu REST . Heute schlagen wir vor, das Thema Systemdesign aus einem etwas anderen Blickwinkel zu betrachten und eine Übersetzung eines Artikels von Stephen Brennan, einem Linux-Star, zu veröffentlichen, der über seine eigene Implementierung von Multitasking im Userspace spricht und wie es nützlich sein kann.
Multitasking ist, wie viele andere Funktionen des Betriebssystems, nicht nur eine Selbstverständlichkeit, sondern wird auch als etwas Gewöhnliches wahrgenommen. Bei unseren leistungsstarken Computern und Smartphones scheint die Vorstellung, dass ein Computer nicht in der Lage ist, Hunderte von Prozessen zu jonglieren, seltsam. Ich denke, dies ist eine der Möglichkeiten, die Computer so nützlich gemacht haben, aber aus diesem Grund sind sie so komplex, dass es manchmal wie Magie erscheint.
Es ist schwierig, sich mit Code zu beschäftigen, der Multitasking implementiert, und es ist nicht immer klar, in welchen Fällen es besser ist, ihn selbst zu implementieren, damit Sie nicht ein ganzes Betriebssystem schreiben müssen. Ich bin mir ziemlich sicher, dass es unmöglich ist, das Phänomen vollständig zu verstehen, bis Sie es selbst erkennen. Aus diesem Grund habe ich beschlossen, einen Artikel zu schreiben, in dem ich Ihnen erläutere, wie Sie mit einer einfachen Threading-Implementierung spielen können. In diesem Artikel implementieren wir einfache Streams in einem regulären
C-Programm (kein Betriebssystem).
Lyrischer Exkurs über setjmp und longjmp
Der Scheduler ist stark abhängig von den Funktionen
setjmp()
und longjmp()
. Sie wirken ein wenig magisch, also werde ich zuerst beschreiben, was sie tun, und dann werde ich mir ein wenig Zeit nehmen und Ihnen genau sagen, wie.
Mit dieser Funktion
setjmp()
können Sie Informationen darüber aufzeichnen, in welcher Ausführungsphase sich das Programm befand, damit Sie später wieder zu diesem Punkt springen können. Es wird eine Typvariable übergeben jmp_buf
, in der wir diese Informationen speichern. Bei der ersten Rückkehr gibt die Funktion setjmp()
0 zurück.
Später können Sie die Funktion verwenden
longjmp(jmp_buf, value)
, um die Ausführung sofort an dem Punkt fortzusetzen, an dem sie aufgerufen wurde setjmp()
. In Ihrem Programm sieht diese Situation so aus, als wäre sie setjmp()
ein zweites Mal aufgetreten. Das Argument wird diesmal zurückgegebenvalue
dass Sie bestanden haben longjmp()
- es ist bequemer, die zweite Rückkehr von der ersten zu unterscheiden. Hier ist ein Beispiel, um diesen Punkt zu veranschaulichen:
#include <stdio.h>
#include <setjmp.h>
jmp_buf saved_location;
int main(int argc, char **argv)
{
if (setjmp(saved_location) == 0) {
printf("We have successfully set up our jump buffer!\n");
} else {
printf("We jumped!\n");
return 0;
}
printf("Getting ready to jump!\n");
longjmp(saved_location, 1);
printf("This will never execute...\n");
return 0;
}
Wenn wir dieses Programm kompilieren und ausführen, erhalten wir die folgende Ausgabe:
We have successfully set up our jump buffer!
Getting ready to jump!
We jumped!
Wow! Es ist wie eine goto-Anweisung, aber in diesem Fall kann sie sogar verwendet werden, um außerhalb einer Funktion zu springen. Es ist auch schwieriger zu lesen als
goto
es ist, weil es wie ein normaler Funktionsaufruf aussieht. Wenn Ihr Code Anwendungen setjmp()
und in Hülle und Fülle longjmp()
, dann wird es sehr schwierig sein , sie für jedermann zu lesen (einschließlich Ihnen).
Wie es der Fall ist
goto
, wird generell empfohlen, setjmp()
und zu vermeiden longjmp()
. Aber wiegoto
können die oben genannten Funktionen nützlich sein, wenn sie sparsam und konsequent verwendet werden. Der Scheduler muss in der Lage sein, den Kontext zu wechseln, daher werden wir diese Funktionen verantwortungsbewusst verwenden. Am wichtigsten ist, dass wir diese Funktionen aus unserer API verwenden, damit sich unsere Planerbenutzer nicht mit dieser Komplexität auseinandersetzen müssen.
Setjmp und longjmp speichern Ihren Stack nicht
True, Funktionen
setjmp()
undlongjmp()
sind nicht dazu gedacht, irgendeine Art von Hüpfen zu unterstützen. Sie wurden für einen ganz bestimmten praktischen Fall entwickelt. Stellen Sie sich vor, Sie führen eine komplexe Operation aus, z. B. eine HTTP-Anforderung. In diesem Fall handelt es sich um einen komplexen Satz von Funktionsaufrufen. Wenn einer von ihnen fehlschlägt, müssen Sie von jedem einen speziellen Fehlercode zurückgeben. Ein solcher Code sieht in der folgenden Auflistung so aus, wo immer Sie die Funktion aufrufen (möglicherweise dutzende Male):
int rv = do_function_call();
if (rv != SUCCESS) {
return rv;
}
Die Bedeutung
setjmp()
und longjmp()
das, was setjmp()
hilft, einen Platz abzustecken, bevor man sich der Aufgabe wirklich schwer macht. Dann können Sie Ihre gesamte Fehlerbehandlung an einem Ort zentralisieren:
int rv;
jmp_buf buf;
if ((rv = setjmp(buf)) != 0) {
/* */
return;
}
do_complicated_task(buf, args...);
Wenn eine der beteiligten Funktionen ausfällt
do_complicated_task()
, geschieht dies einfach longjmp(buf, error_code)
. Dies bedeutet, dass jede Funktion in der Komposition do_complicated_task()
davon ausgehen kann, dass ein Funktionsaufruf erfolgreich ist. Dies bedeutet, dass Sie diesen Code nicht zur Behandlung von Fehlern in jedem Funktionsaufruf verwenden können (in der Praxis wird dies fast nie durchgeführt, dies ist jedoch ein Thema für einen separaten Artikel). ...
Die Grundidee hier ist, dass
longjmp()
Sie nur aus tief verschachtelten Funktionen herausspringen können. Sie können nicht in diese tief verschachtelte Funktion springen, aus der Sie zuvor herausgesprungen sind. So sieht der Stapel aus, wenn Sie aus der Funktion springen. Sternchen (*) bezeichnet den Stapelzeiger, an dem es gespeichert ist setjmp()
.
| Stack before longjmp | Stack after longjmp
+-------------------------+----------------------------
stack | main() (*) | main()
grows | do_http_request() |
down | send_a_header() |
| | write_bytes() |
v | write() - fails! |
Wie Sie sehen, können Sie sich auf dem Stapel nur rückwärts bewegen, sodass keine Gefahr einer Datenbeschädigung besteht. Stellen Sie sich andererseits vor, wie es wäre, wenn Sie zwischen Aufgaben springen möchten. Wenn Sie anrufen
setjmp()
und dann zurückkehren, einige andere Dinge tun und versuchen, die bereits zuvor geleistete Arbeit fortzusetzen, tritt ein Problem auf:
| Stack at setjmp() | Stack later | Stack after longjmp()
+-------------------+------------------+----------------------
stack | main() | main() | main()
grows | do_task_one() | do_task_two() | do_stack_two()
down | subtask() | subtask() | subtask()
| | foo() | | ???
v | bar() (*) | (*) | ??? (*)
Der gespeicherte Stapelzeiger
setjmp()
zeigt auf einen Stapelrahmen, der nicht mehr vorhanden ist und möglicherweise zu einem bestimmten Zeitpunkt von anderen Daten überschrieben wurde. Wenn wir versuchen, longjmp()
zu der Funktion zurückzukehren, von der wir mit Hilfe zurückgekehrt sind , beginnen sehr seltsame Dinge, die zum Zusammenbruch des gesamten Programms führen können.
Moral: Wenn Sie komplexe Aufgaben wie diese verwenden
setjmp()
und longjmp()
zwischen ihnen wechseln möchten, müssen Sie sicherstellen, dass jede Aufgabe einen eigenen Stapel hat. In diesem Fall ist das Problem vollständig beseitigt, da longjmp()
das Programm selbst beim Zurücksetzen des Stapelzeigers den Stapel durch den gewünschten ersetzt und keine Stapellöschung auftritt.
Schreiben wir eine Scheduler-API
Der Exkurs ist etwas langwierig, aber mit dem, was wir gelernt haben, können wir jetzt User-Space-Flows implementieren. Zunächst stelle ich fest, dass es sehr nützlich ist, die API zum Initialisieren, Erstellen und Starten von Threads selbst zu entwerfen. Wenn wir dies im Voraus tun, werden wir viel besser verstehen, was genau wir bauen wollen!
void scheduler_init(void);
void scheduler_create_task(void (*func)(void*), void *arg);
void scheduler_run(void);
Diese Funktionen werden verwendet, um den Scheduler zu initialisieren, Aufgaben hinzuzufügen und schließlich die Aufgaben im Scheduler zu starten. Nach dem Start wird es
scheduler_run()
ausgeführt, bis alle Aufgaben abgeschlossen sind. Derzeit ausgeführte Aufgaben verfügen über die folgenden APIs:
void scheduler_exit_current_task(void);
void scheduler_relinquish(void);
Die erste Funktion ist für das Beenden der Aufgabe verantwortlich. Das Verlassen der Aufgabe ist auch möglich, wenn Sie von ihrer Funktion zurückkehren, sodass diese Konstruktion nur der Einfachheit halber existiert. Die zweite Funktion beschreibt, wie unsere Threads dem Scheduler mitteilen, dass eine andere Aufgabe für eine Weile ausgeführt werden muss. Wenn eine Aufgabe aufgerufen wird
scheduler_relinquish()
, kann sie vorübergehend angehalten werden, während andere Aufgaben ausgeführt werden. aber irgendwann wird die Funktion zurückkehren und die erste Aufgabe kann fortgesetzt werden.
Betrachten wir als konkretes Beispiel einen hypothetischen Anwendungsfall für unsere API, mit dem wir den Scheduler testen werden:
#include <stdlib.h>
#include <stdio.h>
#include "scheduler.h"
struct tester_args {
char *name;
int iters;
};
void tester(void *arg)
{
int i;
struct tester_args *ta = (struct tester_args *)arg;
for (i = 0; i < ta->iters; i++) {
printf("task %s: %d\n", ta->name, i);
scheduler_relinquish();
}
free(ta);
}
void create_test_task(char *name, int iters)
{
struct tester_args *ta = malloc(sizeof(*ta));
ta->name = name;
ta->iters = iters;
scheduler_create_task(tester, ta);
}
int main(int argc, char **argv)
{
scheduler_init();
create_test_task("first", 5);
create_test_task("second", 2);
scheduler_run();
printf("Finished running all tasks!\n");
return EXIT_SUCCESS;
}
In diesem Beispiel erstellen wir zwei Aufgaben, die dieselbe Funktion ausführen, jedoch unterschiedliche Argumente verwenden. Somit kann ihre Implementierung separat verfolgt werden. Jede Aufgabe führt eine festgelegte Anzahl von Iterationen durch. Bei jeder Iteration wird eine Nachricht gedruckt und eine andere Aufgabe ausgeführt. Wir erwarten so etwas wie die Ausgabe des Programms:
task first: 0
task second: 0
task first: 1
task second: 1
task first: 2
task first: 3
task first: 4
Finished running all tasks!
Lassen Sie uns die Scheduler-API implementieren
Um die API zu implementieren, benötigen wir eine Art interne Darstellung der Aufgabe. Kommen wir also zur Sache. Sammeln wir die Felder, die wir brauchen:
struct task {
enum {
ST_CREATED,
ST_RUNNING,
ST_WAITING,
} status;
int id;
jmp_buf buf;
void (*func)(void*);
void *arg;
struct sc_list_head task_list;
void *stack_bottom;
void *stack_top;
int stack_size;
};
Lassen Sie uns jedes dieser Felder separat diskutieren. Alle erstellten Aufgaben müssen vor der Ausführung im Status "erstellt" sein. Wenn eine Aufgabe ausgeführt wird, wird sie in den Status "Ausführen" versetzt. Wenn die Aufgabe jemals auf eine asynchrone Operation warten muss, kann sie in den Status "Warten" versetzt werden. Ein Feld
id
ist einfach eine eindeutige Kennung für eine Aufgabe. Diese buf
enthält Informationen darüber, wann die longjmp()
Aufgabe ausgeführt wird, um fortzufahren. Die Felder func
und arg
werden an übergeben scheduler_create_task()
und sind erforderlich, um die Aufgabe zu starten. Das Feld ist task_list
erforderlich, um eine doppelt verknüpfte Liste aller Aufgaben zu implementieren. Die Felder stack_bottom
, stack_top
und stack_size
alle gehören in einem separaten Stapel gewidmet speziell auf diese Aufgabe. Unten ist die von zurückgegebene Adressemalloc()
"top" ist jedoch ein Zeiger auf die Adresse direkt über dem angegebenen Bereich im Speicher. Da der x86-Stapel nach unten wächst, müssen Sie den Stapelzeiger auf einen Wert setzen stack_top
, nicht stack_bottom
.
Unter solchen Bedingungen können Sie die Funktion implementieren
scheduler_create_task()
:
void scheduler_create_task(void (*func)(void *), void *arg)
{
static int id = 1;
struct task *task = malloc(sizeof(*task));
task->status = ST_CREATED;
task->func = func;
task->arg = arg;
task->id = id++;
task->stack_size = 16 * 1024;
task->stack_bottom = malloc(task->stack_size);
task->stack_top = task->stack_bottom + task->stack_size;
sc_list_insert_end(&priv.task_list, &task->task_list);
}
Durch die Verwendung
static int
garantieren wir, dass bei jedem Aufruf der Funktion das ID-Feld inkrementiert wird und eine neue Nummer vorhanden ist. Alles andere sollte ohne Erklärung klar sein, mit Ausnahme der Funktion sc_list_insert_end()
, die einfach struct task
zur globalen Liste hinzugefügt wird. Die globale Liste wird in einer zweiten Struktur gespeichert, die alle privaten Daten des Schedulers enthält. Nachfolgend finden Sie die Struktur selbst sowie ihre Initialisierungsfunktion:
struct scheduler_private {
jmp_buf buf;
struct task *current;
struct sc_list_head task_list;
} priv;
void scheduler_init(void)
{
priv.current = NULL;
sc_list_init(&priv.task_list);
}
Das Feld wird
task_list
verwendet, um auf eine Aufgabenliste zu verweisen (nicht überraschend). Das Feld current
speichert die Aufgabe, die gerade ausgeführt wird (oder null
, falls derzeit keine solchen Aufgaben vorhanden sind). Am wichtigsten ist, dass das Feld verwendet buf
wird, um in den Code zu springen scheduler_run()
:
enum {
INIT=0,
SCHEDULE,
EXIT_TASK,
};
void scheduler_run(void)
{
/* ! */
switch (setjmp(priv.buf)) {
case EXIT_TASK:
scheduler_free_current_task();
case INIT:
case SCHEDULE:
schedule();
/* , */
return;
default:
fprintf(stderr, "Uh oh, scheduler error\n");
return;
}
}
Sobald die Funktion aufgerufen wird
scheduler_run()
, setzen wir den Puffer setjmp()
so, dass wir immer zu dieser Funktion zurückkehren können. Beim ersten Mal wird 0 (INIT) zurückgegeben, und wir rufen sofort an schedule()
. Anschließend können wir die Konstanten SCHEDULE oder EXIT_TASK übergeben longjmp()
, was zu unterschiedlichen Verhaltensweisen führt. Lassen Sie uns zunächst den Fall EXIT_TASK ignorieren und direkt zur Implementierung springen schedule()
:
static void schedule(void)
{
struct task *next = scheduler_choose_task();
if (!next) {
return;
}
priv.current = next;
if (next->status == ST_CREATED) {
/*
* .
* , .
*/
register void *top = next->stack_top;
asm volatile(
"mov %[rs], %%rsp \n"
: [ rs ] "+r" (top) ::
);
/*
*
*/
next->status = ST_RUNNING;
next->func(next->arg);
/*
* , . – ,
*
*/
scheduler_exit_current_task();
} else {
longjmp(next->buf, 1);
}
/* */
}
Zuerst rufen wir die innere Funktion auf, um die nächste auszuführende Aufgabe auszuwählen. Dieser Planer funktioniert wie ein normales Karussell und wählt einfach eine neue Aufgabe aus der Liste aus. Wenn diese Funktion NULL zurückgibt, müssen keine Aufgaben mehr ausgeführt werden, und wir kehren zurück. Andernfalls müssen wir entweder die Ausführung der Task starten (wenn sie sich im Status ST_CREATED befindet) oder ihre Ausführung fortsetzen.
Um die erstellte Aufgabe auszuführen, verwenden wir die Assembly-Anweisung für x86_64, um das Feld einem
stack_top
Register rsp
(Stapelzeiger) zuzuweisen . Dann ändern wir den Status der Aufgabe, führen die Funktion aus und beenden sie. Hinweis: Stapelzeiger werden setjmp()
sowohl longjmp()
gespeichert als auch neu angeordnet. Daher müssen wir hier nur Assembler verwenden, um den Stapelzeiger zu ändern.
Wenn die Aufgabe bereits gestartet wurde,
buf
muss das Feld den Kontext enthalten, den wir longjmp()
zum Fortsetzen der Aufgabe benötigen. Das tun wir also.
Schauen wir uns als nächstes eine Hilfsfunktion an, die die nächste auszuführende Aufgabe auswählt. Dies ist das Herzstück des Schedulers, und wie ich bereits sagte, funktioniert dieser Scheduler wie ein Karussell:
static struct task *scheduler_choose_task(void)
{
struct task *task;
sc_list_for_each_entry(task, &priv.task_list, task_list, struct task)
{
if (task->status == ST_RUNNING || task->status == ST_CREATED) {
sc_list_remove(&task->task_list);
sc_list_insert_end(&priv.task_list, &task->task_list);
return task;
}
}
return NULL;
}
Wenn Sie mit meiner Implementierung einer verknüpften Liste (aus dem Linux-Kernel) nicht vertraut sind, ist das keine große Sache. Eine Funktion
sc_list_for_each_entry()
ist ein Makro, mit dem Sie alle Aufgaben in der Aufgabenliste durchlaufen können. Die erste auswählbare Aufgabe (nicht in einem ausstehenden Zustand), die wir finden, wird von ihrer aktuellen Position entfernt und an das Ende der Aufgabenliste verschoben. Dies stellt sicher, dass wir beim nächsten Ausführen des Schedulers eine weitere Aufgabe erhalten (falls vorhanden). Wir geben die erste zur Auswahl verfügbare Aufgabe zurück oder NULL, wenn überhaupt keine Aufgaben vorhanden sind.
Fahren wir abschließend mit der Implementierung fort, um
scheduler_relinquish()
zu sehen, wie sich die Aufgabe selbst beseitigen lässt:
void scheduler_relinquish(void)
{
if (setjmp(priv.current->buf)) {
return;
} else {
longjmp(priv.buf, SCHEDULE);
}
}
Dies ist ein weiterer Anwendungsfall für die Funktion
setjmp()
in unserem Scheduler. Grundsätzlich mag diese Option etwas verwirrend erscheinen. Wenn die Task diese Funktion aufruft setjmp()
, speichern wir damit den aktuellen Kontext (einschließlich des tatsächlichen Stapelzeigers). Dann verwenden wir longjmp()
es, um den Scheduler (wieder in scheduler_run()
) einzugeben und die SCHEDULE-Funktion zu übergeben; Daher bitten wir Sie, eine neue Aufgabe zuzuweisen.
Wenn die Aufgabe fortgesetzt wird, gibt die Funktion
setjmp()
mit einem Wert ungleich Null zurück und wir beenden jede Aufgabe, die wir möglicherweise zuvor ausgeführt haben!
Schließlich geschieht Folgendes, wenn eine Aufgabe beendet wird (dies erfolgt entweder explizit durch Aufrufen der Exit-Funktion oder durch Rückkehr von der entsprechenden Task-Funktion):
void scheduler_exit_current_task(void)
{
struct task *task = priv.current;
sc_list_remove(&task->task_list);
longjmp(priv.buf, EXIT_TASK);
/* */
}
static void scheduler_free_current_task(void)
{
struct task *task = priv.current;
priv.current = NULL;
free(task->stack_bottom);
free(task);
}
Dies ist ein zweiteiliger Prozess. Die erste Funktion wird direkt von der Aufgabe selbst zurückgegeben. Wir entfernen den entsprechenden Eintrag aus der Aufgabenliste, da er nicht mehr vergeben wird. Dann
longjmp()
kehren wir mit zur Funktion zurück scheduler_run()
. Diesmal verwenden wir EXIT_TASK. Auf diese Weise teilen wir dem Scheduler mit, was er aufrufen soll, bevor er eine neue Aufgabe zuweist scheduler_free_current_task()
. Wenn Sie zur Beschreibung zurückkehren scheduler_run()
, werden Sie feststellen, dass dies genau das ist, was es tut scheduler_run()
.
Wir haben dies in zwei Schritten getan, seit wann
scheduler_exit_current_task()
verwendet aktiv den in der Aufgabenstruktur enthaltenen Stapel. Wenn Sie den Stapel freigeben und weiter verwenden, besteht die Möglichkeit, dass die Funktion weiterhin auf denselben Stapelspeicher zugreifen kann, den wir gerade freigegeben haben! Um sicherzustellen, dass dies nicht geschieht, müssen wir longjmp()
mithilfe eines separaten Stapels mithilfe des separaten Stapels zum Scheduler zurückkehren. Dann können wir die Daten, die sich auf die Aufgabe beziehen, sicher freigeben.
Daher haben wir die gesamte Implementierung des Schedulers vollständig analysiert. Wenn wir versuchen würden, es zu kompilieren, indem wir meine Implementierung der verknüpften Liste und das obige Hauptprogramm daran anhängen, würden wir einen voll funktionsfähigen Scheduler erhalten! Um Sie nicht durch Kopieren und Einfügen zu stören, leite ich Sie zum Repository auf github , das den gesamten Code für diesen Artikel enthält.
Was nützt der beschriebene Ansatz?
Wenn Sie bisher gelesen haben, müssen Sie meines Erachtens nicht davon überzeugt werden, dass das Beispiel interessant ist. Aber auf den ersten Blick mag es nicht sehr nützlich erscheinen. Schließlich können Sie in C "echte" Threads verwenden, die parallel ausgeführt werden können und nicht aufeinander warten müssen, bis einer von ihnen aufruft
scheduler_relinquish()
.
Ich sehe dies jedoch als Ausgangspunkt für eine ganze Reihe spannender Implementierungen nützlicher Funktionen. Wir können über schwere E / A-Aufgaben sprechen, über die Implementierung einer asynchronen Single-Thread-Anwendung, genauso wie neue asynchrone Dienstprogramme in Python funktionieren. Mit einem solchen System können auch Generatoren und Coroutinen implementiert werden. Schließlich kann dieses System mit harter Arbeit auch mit "echten" Betriebssystem-Threads befreundet werden, um bei Bedarf zusätzliche Parallelität bereitzustellen. Hinter jeder dieser Ideen verbirgt sich ein interessantes Projekt, und ich empfehle Ihnen, eine davon selbst fertigzustellen, lieber Leser.
Es ist sicher?
Ich denke es ist eher nein als ja! Inline-Assemblycode, der sich auf den Stapelzeiger auswirkt, kann nicht als sicher angesehen werden. Riskieren Sie nicht, diese Dinge in der Produktion zu verwenden, aber basteln Sie daran und recherchieren Sie!
Eine sicherere Implementierung eines solchen Systems kann mithilfe einer "nicht kontextbezogenen" API (siehe man getcontext) erstellt werden, mit der Sie zwischen diesen Arten von "Streams" für den Benutzerbereich wechseln können, ohne Assemblycode einzubetten. Leider wird eine solche API nicht von den Standards abgedeckt (sie wurde aus der POSIX-Spezifikation entfernt). Aber es kann immer noch als Teil von glibc verwendet werden.
Wie kann ein solcher Mechanismus verdrängt werden?
Dieser Scheduler funktioniert, wie hier dargestellt, nur, wenn die Threads die Kontrolle explizit zurück an den Scheduler übertragen. Dies ist nicht gut für ein allgemeines Programm, zum Beispiel für ein Betriebssystem, da ein schlecht erstellter Thread die Ausführung aller anderen blockieren kann (obwohl dies die Verwendung von kooperativem Multitasking unter MS-DOS nicht verhinderte!). Ich denke nicht, dass dies kooperatives Multitasking eindeutig schlecht macht. es hängt alles von der Anwendung ab.
Bei Verwendung einer nicht standardmäßigen API außerhalb des Kontexts behalten POSIX-Signale den Kontext des zuvor ausgeführten Codes bei. Durch Einstellen des Timers auf einen Piepton kann der User-Space-Scheduler tatsächlich eine funktionierende Version des präemptiven Multitasking bereitstellen! Dies ist ein weiteres cooles Projekt, das einen separaten Artikel verdient.