2010-04-01 13 views
17

ist es eine Möglichkeit, setjmp und longjmp Funktionen zu implementieren Multitasking mitMultitasking mit setjmp, longjmp

+1

[Tony Finchs Picoro (kleine Co-Routinen)] (http://dotat.at/cgi/git?p=picoro.git;a=blob;f=picoro.c;hb=HEAD). Co-Routinen sind in Knuths Computerkunst und kooperatives Multitasking. Simon Tatham hat auch eine [Co-Routinen-Webseite] (http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html) mit netten Erklärungen. –

+0

Auch sollte darauf geachtet werden; Die 'setjmp()' und 'longjmp()' werden meistens/immer in Assembler implementiert und ähneln OS-Kontext-Umschaltcode. Sie können jedoch einige Zustände wie * Gleitkomma *, * SIMD-Zustand * usw. nicht speichern. Ob dies ein Implementierungsfehler oder ein Standardproblem ist, weiß ich nicht. Dieses Problem wird jedoch häufig in der Praxis bestehen. Zu wissen, welcher Status gespeichert werden soll, kann die Kontextwechselgeschwindigkeit erheblich verbessern. –

+0

Siehe: ['setjmp()' und fpmode] (http://www-personal.umich.edu/~williams/archive/computation/setjmp-fpmode.html) für mehr über andere CPU-Zustände. –

Antwort

12

Sie können in der Tat. Es gibt mehrere Möglichkeiten, dies zu erreichen. Der schwierige Teil besteht darin, die jmpbufs zu erhalten, die auf andere Stapel zeigen. Longjmp ist nur für jmpfuf-Argumente definiert, die von setjmp erstellt wurden. Daher gibt es keine Möglichkeit, dies zu tun, ohne entweder die Assembly zu verwenden oder nicht definiertes Verhalten zu verwenden. Threads auf Benutzerebene sind von Natur aus nicht übertragbar, daher ist Portabilität kein starkes Argument dafür, es nicht wirklich zu tun.

Schritt 1 Sie benötigen einen Ort, um die Zusammenhänge von verschiedenen Threads zu speichern, so dass eine Warteschlange von jmpbuf stuctures machen jedoch viele Threads Sie wollen.

Schritt 2 Sie müssen einen Stapel für jedes dieser Threads malloc.

Schritt 3 Sie müssen einige jmpbuf-Kontexte abrufen, die Stapelzeiger in den Speicherpositionen enthalten, die Sie gerade zugewiesen haben. Sie können die Struktur von jmpbuf auf Ihrem Computer überprüfen und herausfinden, wo der Stapelzeiger gespeichert ist. Rufen Sie setjmp auf und ändern Sie den Inhalt so, dass sich der Stapelzeiger in einem Ihrer zugewiesenen Stapel befindet. Die Stacks werden normalerweise kleiner, sodass Sie Ihren Stack-Zeiger wahrscheinlich in der Nähe des höchsten Speicherplatzes haben. Wenn Sie ein grundlegendes C-Programm schreiben und einen Debugger verwenden, um es zu zerlegen, und dann Anweisungen finden, die es ausführt, wenn Sie von einer Funktion zurückkehren, können Sie herausfinden, wie der Offset sein sollte.Wenn zum Beispiel System V auf x86 Conventions aufruft, sehen Sie, dass es% ebp (den Frame-Pointer) aufruft und dann ret aufruft, was die Rücksprungadresse vom Stack löscht. Beim Eintritt in eine Funktion drückt es also die Rückkehradresse und den Rahmenzeiger. Bei jedem Drücken wird der Stapelzeiger um 4 Byte nach unten verschoben, sodass der Stapelzeiger an der hohen Adresse der zugewiesenen Region mit -8 Byte beginnen soll (als ob Sie gerade eine Funktion aufgerufen hätten, um dorthin zu gelangen). Wir werden die nächsten 8 Bytes füllen.

Die andere Sache, die Sie tun können, ist schreiben Sie eine sehr kleine (eine Zeile) Inline-Assembly, um den Stack-Zeiger zu manipulieren, und rufen Sie dann Setjmp auf. Dies ist tatsächlich portabler, weil in vielen Systemen die Zeiger in einem jmpbuf aus Sicherheitsgründen manipuliert werden, so dass sie nicht einfach geändert werden können.

Ich habe es nicht versucht, aber Sie könnten in der Lage sein, die ASM zu vermeiden, indem Sie den Stack absichtlich überfluten, indem Sie ein sehr großes Array deklarieren und so den Stack-Zeiger verschieben.

Schritt 4 Sie benötigen ausgehende Threads, um das System in einen sicheren Zustand zu versetzen. Wenn Sie dies nicht tun und einer der Threads zurückkehrt, nimmt er die Adresse direkt über Ihrem zugewiesenen Stack als Rückgabeadresse und springt zu einer Speicherstelle für Müll und wahrscheinlich zu einem Segmentfehler. Also brauchen Sie zuerst einen sicheren Ort, zu dem Sie zurückkehren können. Rufen Sie dies ab, indem Sie setjmp im Hauptthread aufrufen und jmpfuf an einem global zugänglichen Ort speichern. Definieren Sie eine Funktion, die keine Argumente annimmt und longjmp nur mit dem gespeicherten globalen jmpfuf aufruft. Rufen Sie die Adresse dieser Funktion ab und kopieren Sie sie in die zugewiesenen Stapel, in denen Sie Platz für die Absenderadresse gelassen haben. Sie können den Rahmenzeiger leer lassen. Wenn nun ein Thread zurückkehrt, wird er zu der Funktion weitergeleitet, die longjmp aufruft, und springt jedes Mal zurück in den Hauptthread, in dem Sie setjmp aufgerufen haben.

Schritt 5 direkt nach dem setjmp des Haupt-Thread, wollen Sie einen Code haben, welcher Thread zu springen zum nächsten bestimmt, die entsprechenden jmpbuf aus der Warteschlange ziehen und longjmp dorthin zu gehen, rufen. Wenn in dieser Warteschlange keine Threads mehr vorhanden sind, ist das Programm beendet.

Schritt 6 einen Kontext Umschaltfunktion schreiben, die setjmp und speichert zurück in der Warteschlange den aktuellen Status aufruft, und dann auf einem anderen jmpbuf aus der Warteschlange longjmp.

Fazit Das sind die Grundlagen. Solange die Threads den Kontextwechsel aufrufen, wird die Warteschlange immer wieder gefüllt und es werden unterschiedliche Threads ausgeführt. Wenn ein Thread zurückkehrt, falls noch irgendwelche übrig sind, wird einer vom Haupt-Thread ausgewählt, und wenn keiner übrig ist, wird der Prozess beendet. Mit relativ wenig Code können Sie ein ziemlich einfaches kooperatives Multitasking-Setup haben. Es gibt mehr Dinge, die Sie wahrscheinlich tun möchten, wie zum Beispiel eine Aufräumfunktion, um den Stapel eines toten Threads freizugeben, etc. Sie können auch die Vorbelegung mit Signalen implementieren, aber das ist viel schwieriger, da setjmp das Fließkommaregister nicht speichert Zustand oder die Merkerregister, die notwendig sind, wenn das Programm asynchron unterbrochen wird.

+0

Einige spezifische Implementierungen von setjmp/longjmp funktionieren möglicherweise so, dass man sie wie gewünscht verhalten kann, und es ist möglich, dass manche Compiler * sogar spezifizieren *, dass ihre Implementierungen auf eine bestimmte Art und Weise funktionieren, die dies erlauben würde Sie müssen sich auf undokumentiertes/undefiniertes Verhalten verlassen, wenn Sie solche Compiler anvisieren, aber ich würde stattdessen vorschlagen, ein paar Zeilen Assemblercode zu verwenden, um die Stack/Register-Switches auszuführen. Die Verwendung von setjmp/longjmp ist nicht portabler als Assembler-Code, könnte jedoch die Illusion von Portabilität vermitteln. – supercat

+0

Das ist gesagt worden, ich denke, es gibt viel zu sagen für kooperatives Multitasking. Viele Compiler dokumentieren ausdrücklich, welche Register (falls vorhanden) durch externe Assembler-Sprachmodule erhalten werden müssen. Ein Präemptiv-Multitasker müsste alle Register beibehalten, die ein Compiler verwenden könnte, was ein Problem sein könnte, wenn z. Ein Compiler nutzt eine Hardware-Multiplikationsbeschleunigungs-Beschleunigungseinheit, von der der Multitasker nichts weiß, aber kooperative Multitasking vermeiden solche Probleme. Das wurde gesagt ... – supercat

+1

... Dinge wie C++ - Ausnahmen, je nachdem, wie sie implementiert sind, können vielleicht sogar mit kooperativem Multitasking nett spielen. Man müsste untersuchen, wie Ausnahmen implementiert werden, um zu wissen, was von den von den laufenden Threads verwalteten Stacks benötigt wird. – supercat

8

Es kann die Regeln ein wenig sein Biegen, aber GNU pth tut dies. Es ist möglich, aber Sie sollten es wahrscheinlich nicht selbst ausprobieren, außer als eine akademische Proof-of-Concept-Übung, verwenden Sie die pth-Implementierung, wenn Sie es ernst und in einer leicht tragbaren Weise tun wollen - Sie werden verstehen, warum, wenn Sie lesen der pth-Thread-Erstellungscode.

(im Wesentlichen ein Signal-Handler verwendet das Betriebssystem in die Schaffung eines neuen Stapel zu betrügen, dann longjmp ist es aus und hält den Stapel herum. Es funktioniert, offensichtlich, aber es ist skizzenhaft wie die Hölle.)

In Produktion Wenn Ihr Betriebssystem maccontext/swapcontext unterstützt, verwenden Sie stattdessen diese Codes. Wenn CreateFiber/SwitchToFiber unterstützt wird, verwenden Sie diese stattdessen. Und seien Sie sich der enttäuschenden Wahrheit bewusst, dass eine der zwingendsten Anwendungen von Coroutinen - das heißt die Kontrolle durch das Ausgeben von Ereignishandlern, die mit fremdem Code aufgerufen werden - unsicher ist, weil das aufrufende Modul wiedereintrittsfähig sein muss, und das können Sie im Allgemeinen beweise das nicht. Aus diesem Grund werden Fasern in .NET immer noch nicht unterstützt.

+2

Die Netscape Portable Runtime (NSPR) scheint auch Makros zu definieren, um dies mit einer einfacheren, aber haarigeren Methode zu tun: Sie rufen nur setjmp auf und ändern dann den Maschinenstapelzeiger und den Befehlszeiger im Puffer. Google "_MD_INIT_CONTEXT" für eine unterhaltsame Lektüre. – user414736

3

Dies ist eine Form des sogenannten Userspace-Kontextwechsels.

Es ist möglich, aber fehleranfällig, besonders wenn Sie die Standardimplementierung von setjmp und longjmp verwenden. Ein Problem mit diesen Funktionen besteht darin, dass sie in vielen Betriebssystemen nur eine Teilmenge von 64-Bit-Registern und nicht den gesamten Kontext speichern. Dies ist oft nicht genug, z.B. im Umgang mit System-Bibliotheken (meine Erfahrung hier ist mit einer benutzerdefinierten Implementierung für AMD64/Windows, die alle Dinge ziemlich stabil gearbeitet).

Das heißt, wenn Sie nicht versuchen, mit komplexen externen Codebasen oder Event-Handler zu arbeiten, und Sie wissen, was Sie tun, und (besonders) wenn Sie Ihre eigene Version in Assembler schreiben, die mehr von der aktuellen speichert Kontext (wenn Sie 32-Bit-Windows oder Linux verwenden, ist dies möglicherweise nicht notwendig, wenn Sie einige Versionen von BSD verwenden, stelle ich mir vor, es ist fast definitiv), und Sie debuggen, achten Sie sorgfältig auf die Disassembly-Ausgabe, dann können Sie sein in der Lage zu erreichen, was Sie wollen.

1

Wie bereits von Sean Ogden erwähnt wurde, ist longjmp() für Multitasking nicht gut, da es nur nach oben den Stapel bewegen und Sprung zwischen verschiedenen Stapeln nicht möglich. Nein, geh damit.

Wie user414736 erwähnt, können Sie getContext/makecontext/swapcontext Funktionen, aber das Problem ist mit denen, dass sie nicht vollständig in User-Space sind. Sie tatsächlich Aufruf der sigprocmask() syscall, weil sie die Signalmaske als Teil der Kontextwechsel wechseln. Dies macht swapcontext() viel langsamer als longjmp(), und Sie wahrscheinlich nicht die langsamen Co-Routinen.

Nach meinem Wissen gibt es keine POSIX-Standardlösung zu dieses Problem, so kompilierte ich meine eigenen aus verschiedenen verfügbaren Quellen. Sie können die kontext Manipulation Funktionen aus libtask hier extrahiert finden:
https://github.com/stsp/dosemu2/tree/devel/src/arch/linux/mcontext
Die Funktionen sind: getmcontext(), setmcontext(), makemcontext() und swapmcontext(). Sie haben die ähnliche Semantik zu den Standardfunktionen mit ähnlichen Namen, , aber sie imitieren auch die setjmp() - Semantik in getmcontext() gibt 1 (statt 0) zurück, wenn von setmcontext() gesprungen wird.

Hinzu kommt, dass Sie einen Port von libpcl verwenden können, die Koroutine Bibliothek:
https://github.com/stsp/dosemu2/tree/devel/src/base/misc/libpcl
Damit ist es möglich, die schnelle kooperative User-Space Einfädeln zu implementieren. Es funktioniert auf Linux, auf i386 und x86_64 Bögen.

Verwandte Themen