Wie werden asynchrone Signalhandler unter Linux ausgeführt?

Wie werden asynchrone Signalhandler unter Linux ausgeführt?


Ich würde gerne genau wissen, wie die Ausführung von asynchronen Signalhandlern unter Linux funktioniert. Erstens ist mir unklar, welche Thread führt den Signalhandler aus. Zweitens würde ich gerne wissen, welche Schritte befolgt werden, damit der Thread den Signalhandler ausführt.


Zur ersten Frage habe ich zwei verschiedene, scheinbar widersprüchliche Erklärungen gelesen:



  1. Der Linux-Kernel, von Andries Brouwer, §5.2 „Empfang von Signalen“ besagt:



  2. Die StackOverflow-Frage „Umgang mit asynchronen Signalen in Multi-Thread-Programmen“ lässt mich glauben, dass das Verhalten von Linux dem von SCO Unix entspricht:



    Außerdem heißt es in "The Linux Signals Handling Model" von Moshe Bar "Asynchrone Signale werden an den ersten gefundenen Thread geliefert, der das Signal nicht blockiert.", was ich so interpretiere, dass das Signal an einen Thread geliefert wird, dessen Sigmask nicht ist einschließlich des Signals.



Welche ist richtig?


Zum zweiten, was passiert mit dem Stack und dem Registerinhalt für den ausgewählten Thread? Nehmen wir den Thread-zum-Ausführen-des-Signal-Handler T an ist mitten in der Ausführung von do_stuff() Funktion. Ist Thread T Der Stack von wird direkt verwendet, um den Signalhandler auszuführen (d. h. die Adresse des Signaltrampolins wird auf T geschoben geht der Stack und der Kontrollfluss zum Signalhandler)? Wird alternativ ein separater Stack verwendet? Wie funktioniert es?


Antworten:


Quelle Nr. 1 (Andries Brouwer) ist für einen Single-Threaded-Prozess korrekt. Quelle Nr. 2 (SCO Unix) ist für Linux falsch, weil Linux keine Threads in sigwait(2) bevorzugt. Moshe Bar hat Recht mit dem ersten verfügbaren Thread.


Welcher Thread bekommt das Signal? Die Handbuchseiten von Linux sind eine gute Referenz. Ein Prozess verwendet clone(2) mit CLONE_THREAD, um mehrere Threads zu erstellen. Diese Threads gehören zu einer "Thread-Gruppe" und teilen sich eine einzige Prozess-ID. Das Handbuch für clone(2) sagt,



Linux ist kein SCO-Unix, da Linux das Signal an jeden Thread weitergeben kann, selbst wenn einige Threads auf ein Signal warten (mit sigwaitinfo, sigtimedwait oder sigwait) und andere Threads nicht. Das Handbuch für sigwaitinfo(2) warnt,



Der Code zum Auswählen eines Threads für das Signal befindet sich in linux/kernel/signal.c (der Link verweist auf den Spiegel von GitHub). Siehe die Funktionen Wants_signal() und Completes_signal(). Der Code wählt den ersten verfügbaren Thread für das Signal aus. Ein verfügbarer Thread ist einer, der das Signal nicht blockiert und keine anderen Signale in seiner Warteschlange hat. Der Code überprüft zuerst den Hauptthread und dann die anderen Threads in einer mir unbekannten Reihenfolge. Wenn kein Thread verfügbar ist, bleibt das Signal hängen, bis ein Thread das Signal entsperrt oder seine Warteschlange leert.


Was passiert, wenn ein Thread das Signal erhält? Wenn es einen Signal-Handler gibt, veranlasst der Kernel den Thread, den Handler aufzurufen. Die meisten Handler werden auf dem Stack des Threads ausgeführt. Ein Handler kann auf einem alternativen Stack laufen, wenn der Prozess sigaltstack(2) verwendet, um den Stack bereitzustellen, und sigaction(2) mit SA_ONSTACK, um den Handler zu setzen. Der Kernel schiebt einige Dinge auf den ausgewählten Stack und setzt einige der Register des Threads.


Um den Handler auszuführen, muss der Thread im Userspace ausgeführt werden. Wenn der Thread im Kernel läuft (vielleicht für einen Systemaufruf oder einen Seitenfehler), dann führt er den Handler nicht aus, bis er in den Userspace geht. Der Kernel kann einige Systemaufrufe unterbrechen, sodass der Thread den Handler jetzt ausführt, ohne auf die Beendigung des Systemaufrufs zu warten.


Der Signal-Handler ist eine C-Funktion, daher befolgt der Kernel die Konvention der Architektur zum Aufrufen von C-Funktionen. Jede Architektur, wie arm, i386, powerpc oder sparc, hat ihre eigene Konvention. Um für PowerPC handler(signum) aufzurufen, setzt der Kernel das Register r3 auf signum. Der Kernel setzt auch die Rücksprungadresse des Handlers auf das Signaltrampolin. Die Rücksendeadresse geht per Konvention auf den Stack oder in ein Register.


Der Kernel fügt in jeden Prozess ein Signaltrampolin ein. Dieses Trampolin ruft sigreturn(2) auf, um den Thread wiederherzustellen. Im Kernel liest sigreturn(2) einige Informationen (wie gespeicherte Register) aus dem Stack. Der Kernel hatte diese Informationen auf den Stack geschoben, bevor er den Handler aufrief. Wenn es einen unterbrochenen Systemaufruf gab, könnte der Kernel den Aufruf neu starten (nur wenn der Handler SA_RESTART verwendet hat) oder den Aufruf mit EINTR fehlschlagen oder einen kurzen Lese- oder Schreibvorgang zurückgeben.