Wie werden asynchrone Signalhandler unter Linux ausgeführt?

Lesezeit: 8 Minuten

Benutzeravatar von Daniel Trebbien
Daniel Trebien

Ich würde gerne genau wissen, wie die Ausführung von asynchronen Signalhandlern unter Linux funktioniert. Erstens ist mir unklar, wie die Thread führt den Signalhandler aus. Zweitens würde ich gerne die Schritte kennen, die 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 „Signale empfangen“ heißt es:

    Wenn ein Signal ankommt, wird der Prozess unterbrochen, die aktuellen Register werden gespeichert und der Signalhandler wird aufgerufen. Wenn der Signalhandler zurückkehrt, wird die unterbrochene Aktivität fortgesetzt.

  2. Die StackOverflow-Frage “Umgang mit asynchronen Signalen in Multithread-Programmen” lässt mich glauben, dass das Verhalten von Linux so ist wie SCO Unix:

    Wenn ein Signal an einen Prozess geliefert wird und es abgefangen wird, wird es von einem und nur einem der Threads verarbeitet, der eine der folgenden Bedingungen erfüllt:

    1. Ein Thread blockiert in a sigwait(2) Systemaufruf, dessen Argument tut beinhalten den Typ des abgefangenen Signals.

    2. Ein Thread, dessen Signalmaske nicht beinhalten den Typ des abgefangenen Signals.

    Weitere Überlegungen:

    • Ein Thread ist blockiert sigwait(2) wird gegenüber einem Thread bevorzugt, der den Signaltyp nicht blockiert.
    • Wenn mehr als ein Thread diese Anforderungen erfüllt (vielleicht rufen zwei Threads auf sigwait(2)), dann wird einer von ihnen ausgewählt. Diese Auswahl ist von Anwendungsprogrammen nicht vorhersagbar.
    • Wenn kein Thread geeignet ist, bleibt das Signal auf der Prozessebene “anstehend”, bis ein Thread geeignet wird.

    Ebenfalls, „The Linux Signals Handling Model“ von Moshe Bar erklärt “Asynchrone Signale werden an den ersten Thread geliefert, der das Signal nicht blockiert.”, was ich so interpretiere, dass das Signal an einen Thread mit seiner Sigmask geliefert wird nicht einschließlich des Signals.

Was ist richtig?

Zum zweiten, was passiert mit dem Stack und dem Registerinhalt für den ausgewählten Thread? Angenommen, der Thread-to-run-the-signal-handler T ist mitten in der Ausführung von a do_stuff() Funktion. Ist Faden TDer Stack von wird direkt verwendet, um den Signal-Handler auszuführen (d. h. die Adresse des Signal-Trampolins wird aufgeschoben Tgeht der Stack und der Kontrollfluss zum Signalhandler)? Wird alternativ ein separater Stack verwendet? Wie funktioniert es?

  • Dies kann einige Ihrer Fragen beantworten (definitiv nicht alle).

    – Chris Eberle

    4. August 2011 um 21:57 Uhr

Diese beiden Erklärungen sind wirklich nicht widersprüchlich, wenn Sie die Tatsache berücksichtigen, dass Linux-Hacker über den Unterschied zwischen einem Thread und einem Prozess verwirrt sind, hauptsächlich aufgrund des historischen Fehlers, so zu tun, als könnten Threads als Prozesse implementiert werden, die sich teilen Erinnerung. 🙂

Vor diesem Hintergrund ist Erklärung Nr. 2 viel detaillierter, vollständiger und korrekter.

Was den Stapel- und Registerinhalt betrifft, kann jeder Thread seinen eigenen alternativen Signalverarbeitungsstapel registrieren, und der Prozess kann auf Signalbasis auswählen, welche Signale auf alternativen Signalverarbeitungsstapeln geliefert werden. Der unterbrochene Kontext (Register, Signalmaske etc.) wird in a gespeichert ucontext_t Struktur auf dem (möglicherweise alternativen) Stack für den Thread, zusammen mit der Trampolin-Rücksendeadresse. Signal-Handler, die mit installiert werden SA_SIGINFO flag können dies prüfen ucontext_t Struktur, wenn sie möchten, aber das einzige, was sie damit machen können, ist, die gespeicherte Signalmaske zu untersuchen (und möglicherweise zu modifizieren). (Ich bin mir nicht sicher, ob das Ändern vom Standard genehmigt wird, aber es ist sehr nützlich, da es dem Signal-Handler ermöglicht, die Signalmaske des unterbrochenen Codes bei der Rückkehr atomar zu ersetzen, um beispielsweise das Signal blockiert zu lassen, damit es nicht noch einmal passieren kann .)

  • Warum halten Sie die Vereinheitlichung von „Prozessen“ und „Threads“ für einen Fehler?

    – Alex D

    29. Juni 2015 um 9:30 Uhr

  • @AlexD: Weil das zu Ergebnissen führt, die sich offensichtlich anders verhalten, als sie angegeben sind. Die Entscheidung, dies zu tun, war (teilweise) das Versagen, den Umfang zu verstehen, wie falsch das resultierende Verhalten sein würde, und (meistens) die Annahme, dass sich nichts/niemand darum kümmern würde, dass das Verhalten falsch ist.

    – R.. GitHub HÖR AUF, EIS ZU HELFEN

    29. Juni 2015 um 14:52 Uhr

  • Beziehen Sie sich mit “angegeben” auf POSIX-Spezifikationen oder auch auf andere Spezifikationen? Wie verhält sich das Verhalten von leichtgewichtigen Linux-Prozessen wie vom Userspace aus beobachtbar von diesen Spezifikationen abweichen?

    – Alex D

    29. Juni 2015 um 19:31 Uhr

  • Da die relevante Thread-API ist POSIX-Threads (“pthreads”), ja, die Spezifikation stammt von POSIX. Modernes Linux (seit 2.6.0) bietet echte Threads-Unterstützung auf Kernel-Ebene, also modulo einige Bugs, die in älteren Versionen häufiger auftraten und so ziemlich alle bis zum Ende der 2.6-Serie behoben wurden, gibt es nicht viel, was erkennbar falsch ist. Aber die Terminologie ist immer noch stark unpassend und inkonsistent. Prozesse werden vom Kernel als “Thread-Gruppen” und Threads als “Prozesse” bezeichnet. Außer an einigen Stellen, wo die Namen festgelegt wurden. 🙂 Also ziemlich verwirrend…

    – R.. GitHub HÖR AUF, EIS ZU HELFEN

    30. Juni 2015 um 2:31 Uhr

Benutzeravatar von George Koehler
Georg Köhler

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 Klon(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 Klon (2) sagt,

Signale können unter Verwendung von an eine Thread-Gruppe als Ganzes (dh eine TGID) gesendet werden töten (2)oder zu einem bestimmten Thread (dh TID) mit
tgkill(2).

Signaldispositionen und -aktionen sind prozessweit: Wenn ein unbehandeltes Signal an einen Thread geliefert wird, dann wird es alle Mitglieder der Thread-Gruppe beeinflussen (beenden, stoppen, fortfahren, darin ignoriert werden).

Jeder Thread hat seine eigene Signalmaske, wie von gesetzt sigprocmask(2), aber Signale können entweder anstehen: für den gesamten Prozess (dh zustellbar an jedes Mitglied der Thread-Gruppe), wenn sie mit kill(2) gesendet werden; oder für einen einzelnen Thread, wenn er mit tgkill(2) gesendet wird. Ein Anruf bei Anmeldung(2) gibt einen Signalsatz zurück, der die Vereinigung der für den gesamten Prozess anstehenden Signale und der für den aufrufenden Thread anstehenden Signale ist.

Wenn kill(2) verwendet wird, um ein Signal an eine Thread-Gruppe zu senden, und die Thread-Gruppe einen Handler für das Signal installiert hat, dann wird der Handler in genau einem willkürlich ausgewählten Mitglied der Thread-Gruppe aufgerufen, das die nicht blockiert hat Signal. Wenn mehrere Threads in einer Gruppe darauf warten, dasselbe Signal zu akzeptieren, verwenden Sie sigwaitinfo(2)wählt der Kernel willkürlich einen dieser Threads aus, um ein Signal zu empfangen, das mit kill(2) gesendet wird.

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,

Bei normaler Verwendung blockiert das aufrufende Programm die gesetzten Signale über einen vorherigen Aufruf von sigprocmask(2) (so dass die Standarddisposition für diese Signale nicht eintritt, wenn sie zwischen aufeinanderfolgenden Aufrufen von sigwaitinfo() oder sigtimedwait() anhängig werden) und richtet keine Handler für diese Signale ein. In einem Multithread-Programm sollte das Signal in allen Threads blockiert werden, um zu verhindern, dass das Signal gemäß seiner Standarddisposition in einem anderen Thread als demjenigen behandelt wird, der sigwaitinfo() oder sigtimedwait() aufruft.

Der Code zum Auswählen eines Threads für das Signal lebt darin 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 ausgeführt werden, wenn der Prozess verwendet wird Signalstack(2) um den Stack bereitzustellen, und Aktion(2) mit SA_ONSTACK, um den Handler festzulegen. Der Kernel schiebt einige Dinge auf den ausgewählten Stapel 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 Absenderadresse geht per Konvention auf den Stack oder in ein Register.

Der Kernel fügt in jeden Prozess ein Signaltrampolin ein. Dieses Trampolin ruft Rücksendung(2) 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 scheitern lassen oder einen kurzen Lese- oder Schreibvorgang zurückgeben.

1414400cookie-checkWie werden asynchrone Signalhandler unter Linux ausgeführt?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy