In diesem Blog-Post erläutert Chris Down, Kernel Engineer bei Meta, die Gefahren im Zusammenhang mit Linux-Signalen in Linux-Produktionsumgebungen und erklärt, warum Entwickler*innen Signale nach Möglichkeit vermeiden sollten.
Ein Signal ist ein Event, das Linux-Systeme als Antwort auf eine Bedingung generieren. Signale können vom Kernel an einen Prozess, von einem Prozess an einen anderen Prozess oder von einem Prozess an sich selbst gesendet werden. Wenn ein Prozess ein Signal empfängt, kann er eine Aktion ausführen.
Signale sind ein wesentlicher Bestandteil von Unix-Betriebsumgebungen. Sie werden schon seit einer halben Ewigkeit genutzt. Auf ihnen beruhen viele der Kernkomponenten eines Betriebssystems – Core-Dumping, Prozesslebenszyklus-Management usw. Im Allgemeinen haben sie sich in den etwa 50 Jahren, die wir sie jetzt schon nutzen, als nützlich erwiesen. Wenn jetzt also jemand sagt, dass ihre Verwendung für die Interprozesskommunikation (Interprocess Communication, kurz IPC) potenziell Gefahren bergen kann, hört sich das an, als würde man verzweifelt versuchen, das Rad neu zu erfinden. Dieser Artikel soll aber Fälle aufzeigen, bei denen Signale zu Produktionsproblemen geführt haben, und ein paar potenzielle Lösungen und Alternativen vorschlagen.
Signale scheinen aufgrund ihrer Standardisierung, ihrer breiten Verfügbarkeit und der Tatsache, dass sie keine zusätzlichen Abhängigkeiten außerhalb des Betriebssystems erfordern, attraktiv zu sein. Ihre sichere Verwendung kann sich aber als schwierig erweisen. Signale treffen unglaublich viele Annahmen, die unbedingt geprüft werden müssen, um ihre Anforderungen zu erfüllen. Wenn nicht, müssen sie sorgfältig konfiguriert werden. In der Realität ist das in vielen Anwendungen, selbst weithin bekannten, nicht der Fall. So können in Zukunft Vorfälle entstehen, die sich nur schwer debuggen lassen.
Sehen wir uns einen Vorfall an, der vor Kurzem in der Meta-Produktionsumgebung aufgetreten ist und die Gefahren von Signalen veranschaulicht. Zuerst gehen wir kurz auf die Geschichte einiger Signale und die Entwicklung bis heute ein. Dann stellen wir dem unsere aktuellen Anforderungen und Probleme in der Produktion gegenüber.
Fangen wir ganz am Anfang an. Das LogDevice-Team hat seine Codebase bereinigt, um nicht genutzten Code und Features zu entfernen. Eines der veralteten Features war ein Protokolltyp, in dem bestimmte vom Service ausgeführte Vorgänge dokumentiert wurden. Dieses Feature war am Ende redundant und wurde nicht genutzt. Also wurde es entfernt. Die Änderung ist hier auf GitHub zu sehen. So weit, so gut.
In der ersten Zeit nach der Änderung gab es keine Probleme. Der Produktionsbetrieb wurde normal fortgesetzt und hat Traffic wie immer verarbeitet. Nach ein paar Wochen wurde gemeldet, dass eine enorme Anzahl an Service-Nodes verloren ging. Das Problem hatte irgendetwas mit dem Rollout des neuen Release zu tun, aber niemand wusste genau, was eigentlich die Ursache war. Wie kam es zu diesen Schwierigkeiten?
Das betroffene Team grenzte das Problem auf die oben genannte Codeänderung ein, also das Verwerfen dieser Protokolle. Aber warum? Was ist denn nicht in Ordnung mit diesem Code? Wenn du es noch nicht weißt, solltest du dir die Diff ansehen und versuchen, es selber herauszufinden. Der Fehler ist nämlich nicht sofort ersichtlich und könnte jedem passieren.
logrotate ist mehr oder weniger das Standardtool für die Protokollrotation in Linux. Das Tool gibt es jetzt schon seit fast 30 Jahren und nutzt ein ganz einfaches Konzept: Verwaltung des Lebenszyklus von Protokollen durch Rotation und Leeren.
logrotate sendet selbst keine Signale. Deshalb werden diese auf der logrotate-Hauptseite oder seiner Dokumentation gar nicht oder kaum erwähnt. Für logrotate können aber Befehle festgelegt werden, die vor oder nach der Rotation ausgeführt werden. Hier siehst du ein ganz einfaches Beispiel für die logrotate-Standardkonfiguration in CentOS:
/var/log/cron /var/log/maillog /var/log/messages /var/log/secure /var/log/spooler { sharedscripts postrotate /bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true endscript }
Wir übersehen jetzt mal alle Mängel und gehen davon aus, das funktioniert wie gedacht. Gemäß dieser Konfiguration soll logrotate nach der Rotation einer der aufgelisteten Dateien SIGHUP
an die PID in /var/run/syslogd.pid
senden. Das sollte die PID der laufenden syslogd
-Instanz sein.
Das ist ja schön und gut mit einer stabilen öffentlichen API wie syslog. Wie ist das aber, wenn die Implementierung von SIGHUP
eine interne Komponente ist, die sich jederzeit ändern kann?
Eines der Probleme hier ist, dass die semantische Bedeutung von Signalen, außer bei Signalen, die nicht im Nutzungsbereich abgefangen werden können und daher nur eine Bedeutung haben, wie SIGKILL
und SIGSTOP
, von den App-Entwickler*innen und Nutzer*innen interpretiert und programmiert wird. In einigen Fällen macht das keinen Unterschied, wie bei SIGTERM
, das eigentlich immer als „so bald wie möglich ordnungsgemäß beenden“ verstanden wird. Im Fall von SIGHUP
ist die Bedeutung aber weitaus weniger eindeutig.
SIGHUP
wurde für serielle Verbindungen entwickelt und gab ursprünglich an, dass das andere Ende die Verbindung unterbrochen hat. Heutzutage wird SIGHUP
noch für sein modernes Äquivalent gesendet: wenn ein Pseudo- oder virtuelles Terminal geschlossen wird. (Mit Tools wie nohup
kann das Signal dabei unterdrückt werden).
In den Anfangszeiten von Unix mussten wiederholte Daemon-Ladevorgänge implementiert werden. Dabei wurden in der Regel zumindest eine Konfigurations-/Protokolldatei ohne Neustart neu geöffnet, und Signale schienen eine Möglichkeit, das ohne Abhängigkeiten zu erreichen. Es gab natürlich kein Signal dafür. Da diese Daemons aber kein Steuerterminal haben, sollten sie SIGHUP
aus keinem anderen Grund empfangen. Daher bot sich dieses Signal dafür an, es ohne offensichtliche Nebenwirkungen für diesen Zweck einzusetzen.
Dieser Plan hat aber einen kleinen Nachteil. Der Standardstatus für Signale ist nicht „ignoriert“, sondern signalspezifisch. Programme müssen SIGTERM
also beispielsweise nicht manuell konfigurieren, um ihre Anwendung zu beenden. Solange sie keinen anderen Signal-Handler festlegen, beendet der Kernel das Programm einfach, ohne dass Code im Nutzungsbereich erforderlich ist. Wie praktisch!
Nicht so praktisch ist aber, dass SIGHUP
als Standardverhalten ebenfalls das Programm sofort beendet. Das eignet sich hervorragend für den ursprünglichen Beendigungsfall, wenn diese Anwendungen wahrscheinlich nicht mehr benötigt werden, aber nicht so sehr für diese neue Bedeutung.
Das wäre natürlich kein Problem, wenn wir alle Stellen entfernen, die potenziell SIGHUP
an das Programm senden könnten. In großen, ausgereiften Codebases ist das aber schwierig. SIGHUP
ist kein eng kontrollierter IPC-Aufruf, nach dem wir ganz einfach mit „Grep“ in der Codebase suchen können. Signale können jederzeit von überall gesendet werden, mit nur wenigen Prüfungen ihrer Nutzung (außer ganz simplen Checks wie „bist du diese*r Nutzer*in oder hast du CAP_KILL
“). Fazit ist, dass wir nur schwer bestimmen können, von wo Signale gesendet werden könnten. Mit expliziterer IPC wissen wir aber, dass dieses Signal für uns nicht von Bedeutung ist und ignoriert werden sollte.
Jetzt könnt ihr euch wahrscheinlich schon vorstellen, was passiert ist. Ein LogDevice-Release wurde mit der oben erwähnten, verhängnisvollen Codeänderung eingeführt. Erst ging nichts schief, aber um Mitternacht am nächsten Tag fingen die Probleme an. Grund war der folgende Abschnitt in der logrotate-Konfiguration des Geräts, der einen jetzt nicht verarbeiteten (und daher als schwerwiegender Fehler geltenden) SIGHUP
an den logdevice-Daemon sendet:
/var/log/logdevice/audit.log { daily # [...] postrotate pkill -HUP logdeviced endscript }
Wenn man ein großes Feature entfernt, hat man sehr schnell einen kurzen Abschnitt einer logrotate-Konfiguration übersehen. Leider kann man auch kaum feststellen, ob jeder Bestandteil davon gleichzeitig entfernt wurde. Selbst bei Fällen, in denen die Validierung einfacher ist als hier, bleiben oft Rückstände nach der Codebereinigung übrig. Meistens hat das keine schwerwiegenden Folgen. Der Rest ist einfach unwirksamer Code oder no-op-Code.
Im Grunde sind der Vorfall selbst und seine Lösung einfach: Sende kein SIGHUP
, und verteile LogDevice-Aktionen zeitlich mehr (das heißt, führe das nicht um Punkt Mitternacht aus). Wir sollten uns aber nicht nur auf die Feinheiten dieses einen Vorfalls konzentrieren. Dieser Vorfall veranschaulicht in erster Linie, dass Signale in der Produktion ausschließlich für die grundlegendsten Fälle eingesetzt werden sollten.
Vorweg: Die Verwendung von Signalen als Mechanismus, um Änderungen im Prozessstatus des Betriebssystems zu bewirken, hat durchaus ihre Begründung. Dazu gehören Signale wie SIGKILL
, für die kein Signal-Handler installiert werden kann und die eine eindeutige Handlung ausführen, und das Kernel-Standardverhalten von SIGABRT
, SIGTERM
, SIGINT
, SIGSEGV
, SIGQUIT
und Ähnlichen, die im Allgemeinen für Nutzer*innen und Programmierer*innen leicht verständlich sind.
All diese Signale haben eins gemeinsam: Ihr Empfang führt zu einem beendenden Endstatus im Kernel selbst. Nach Erhalt von SIGKILL
oder SIGTERM
ohne Signal-Handler im Nutzungsbereich werden also keine Nutzungsbereichsanweisungen mehr ausgeführt.
Ein beendender Endstatus ist wichtig, weil du damit in der Regel die Komplexität des Stacks und den derzeit ausgeführten Code reduzierst. Andere erwünschte Statusangaben führen oft zu höherer Komplexität und komplexerer Logik, während Nebenläufigkeit und Codefluss unübersichtlicher werden.
Du hast vielleicht gemerkt, dass wir einige andere Signale, die Prozesse auch standardmäßig beenden, nicht erwähnt haben. Die folgenden Standardsignale beenden Prozesse standardmäßig (außer Code-Dump-Signale wie SIGABRT
oder SIGSEGV
, da diese sinnvoll sind):
Auf den ersten Blick scheinen diese Signale angemessen, aber es gibt einige Ausreißer:
Fast ein Drittel der beendenden Signale ist also im besten Fall fragwürdig und im schlimmsten Fall gefährlich, wenn Programme geändert werden müssen. Noch schlimmer: Selbst die angeblich „benutzerdefinierten“ Signale können zu einer Katastrophe führen, wenn jemand vergisst, sie explizit mit SIG_IGN
zu ignorieren. Selbst eine eigentlich harmlose Nutzung von SIGUSR1
oder SIGPOLL
kann Vorfälle verursachen.
Dabei geht es nicht nur um deine Kenntnisse. Ganz egal, wie gut du dich mit Signalen auskennst – es ist dennoch extrem schwierig, Code mit richtigen Signalen beim ersten Mal zu schreiben. Entgegen ihrer Erscheinung sind Signale nämlich viel komplexer als es scheint.
Programmierer*innen denken in der Regel nicht den ganzen Tag über die Funktionsweise von Signalen nach. Deshalb unterlaufen ihnen bei der tatsächlichen Implementierung von Signalverarbeitung oft geringfügige Fehler.
Damit meine ich noch nicht einmal die „trivialen“ Fälle, wie die Sicherheit in einer Signalverarbeitungsfunktion, die meist einfach durch Bumping von sig_atomic_t
oder mit atomic_signal_fence von C++ gelöst werden können. Nein. Diese Fälle sind in der Regel einfach auffindbar und als Hürden bekannt, wenn man sich erst einmal durch Signale gekämpft hat. Viel schwieriger ist die Logik des Codeflusses der nominalen Teile eines komplexen Programms bei Empfang eines Signals. Dazu muss man andauernd und explizit bei jedem Punkt des Anwendungslebenszyklus über Signale nachdenken. (Was ist mit EINTR
, ist SA_RESTART
hier ausreichend? Welcher Ablauf soll gestartet werden, wenn dieser Prozess vorzeitig beendet wird? Welche Auswirkungen hat mein nebenläufiges Programm?) Oder man muss sigprocmask
oder pthread_setmask
für einen Teil des Anwendungslebenszyklus einrichten und hoffen, dass der Codefluss sich nie ändert (was bei der schnelllebigen Entwicklung sehr unwahrscheinlich ist). Die Ausführung von signalfd
oder sigwaitinfo
in einem dedizierten Thread kann hier etwas Abhilfe schaffen, aber beide Signale sind aufgrund ihrer Ausnahmefälle und Nutzbarkeitsprobleme nicht unbedingt zu empfehlen.
Sicherlich wissen erfahrene Programmierer*innen mittlerweile, dass es sehr schwierig ist, Code mit sicheren Threads korrekt zu schreiben. Ihr dachtet vielleicht, Code mit sicheren Threads sei schwierig, aber Signale sind noch viel schwieriger. Signal-Handler dürfen ausschließlich auf Code ohne Sperren mit atomaren Datenstrukturen basieren, da sonst der Hauptausführungsfluss unterbrochen wird und wir nicht wissen, welche Sperren enthalten sind, und da der Hauptausführungsfluss sonst nicht-atomare Vorgänge ausführen könnte. Sie müssen außerdem in sich selbst verschachtelt werden können, da Signal-Handler sich überschneiden können, wenn ein Signal mehrmals gesendet wird (oder sogar mit einem Signal mit SA_NODEFER
). Das ist einer der Gründe, warum du keine Funktionen wie printf
oder malloc
in einem Signal-Handler verwenden kannst, da sie auf globalen Mutexen für die Synchronisierung basieren. Wenn du beim Signalempfang eine Sperre hältst und dann eine Funktion aufrufst, die diese Sperre erneut benötigt, führt das zu einem Anwendungs-Deadlock. Das kann man nur sehr schwer mit Logik umgehen. Deshalb schreiben viele Entwickler*innen einfach ähnlichen Code wie den Folgenden als Signalverarbeitung:
static volatile sig_atomic_t received_sighup; static void sighup(int sig __attribute__((unused))) { received_sighup = 1; } static int configure_signal_handlers(void) { return sigaction( SIGHUP, &(const struct sigaction){.sa_handler = sighup, .sa_flags = SA_RESTART}, NULL); } int main(int argc, char *argv[]) { if (configure_signal_handlers()) { /* failed to set handlers */ } /* usual program flow */ if (received_sighup) { /* reload */ received_sighup = 0; } /* usual program flow */ }
Das Problem ist, dass dieser Versuch, signalfd
oder andere Ansätze für die asynchrone Signalverarbeitung zwar recht einfach und robust erscheinen, aber ignorieren, dass der Unterbrechungspunkt genauso wichtig wie die Handlungen sind, die nach Signalempfang ausgeführt werden. Angenommen, der Nutzungsbereichscode führt I/O aus oder ändert die Metadaten von Objekten aus dem Kernel (wie inodes oder FDs). In diesem Fall befindest du dich zum Zeitpunkt der Unterbrechung wahrscheinlich in einem Kernel-Bereichs-Stack. So könnte beispielsweise ein Thread aussehen, wenn er versucht, einen Dateideskriptor zu schließen:
# cat /proc/2965230/stack [<0>] schedule+0x43/0xd0 [<0>] io_schedule+0x12/0x40 [<0>] wait_on_page_bit+0x139/0x230 [<0>] filemap_write_and_wait+0x5a/0x90 [<0>] filp_close+0x32/0x70 [<0>] __x64_sys_close+0x1e/0x50 [<0>] do_syscall_64+0x4e/0x140 [<0>] entry_SYSCALL_64_after_hwframe+0x44/0xa9
Hierbei ist __x64_sys_close
die x86_64-Variante des Systemaufrufs close
, der einen Dateideskriptor schließt. Zu diesem Punkt der Ausführung warten wir auf die Aktualisierung des sekundären Speichers (wait_on_page_bit
). Da I/O-Vorgänge in der Regel viel langsamer als andere Vorgänge sind, kannst du hier mit schedule
für den CPU-Scheduler des Kernels angeben, dass jetzt ein Vorgang mit hoher Latenz (wie Datenträger- oder Netzwerk-I/O) ausgeführt wird und fürs Erste ein anderer Prozess anstelle des aktuellen Prozesses geplant werden sollte. Das ist nützlich, da wir damit dem Kernel signalisieren können, dass er einen Prozess auswählen sollte, der die CPU auch tatsächlich nutzt, anstatt auf einen Prozess zu warten, der erst abgeschlossen werden kann, wenn eine Antwort von einem anderen, möglicherweise langsamen, Prozess eingegangen ist.
Angenommen, wir senden ein Signal an den Prozess, den wir ausgeführt haben. Das gesendete Signal hat einen Nutzungsbereichs-Handler im Empfänger-Thread. Also wird der Vorgang im Nutzungsbereich fortgesetzt. Dieses Rennen kann auf viele Arten enden. Der Kernel könnte beispielsweise versuchen, schedule
zu verlassen und den Stack weiter abzuarbeiten, und gibt schließlich die Fehlernummer ESYSRESTART
oder EINTR
an den Nutzungsbereich aus, um eine Unterbrechung anzugeben. Wie weit war der Vorgang zum Schließen aber fortgeschritten? Welchen Status hat der Dateideskriptor jetzt?
Nach der Rückkehr zum Nutzungsbereich führen wir den Signal-Handler aus. Nach Beendigung des Signal-Handlers propagieren wir den Fehler zum close
-Wrapper von libc im Nutzungsbereich und dann zur Anwendung, die theoretisch eine Handlung zur vorliegenden Situation ausführen kann. Ich sage hier „theoretisch“, weil man kaum bestimmen kann, wie mit vielen dieser Signalsituationen umgegangen werden sollte. Viele Services in der Produktion weisen hierbei Mängel auf. In Anwendungen, bei denen die Datenintegrität nicht so wichtig ist, mag das ja in Ordnung sein. Bei Produktionsanwendungen, bei denen die Datenkonsistenz und -integrität durchaus eine große Rolle spielen, stellt das ein erhebliches Problem dar: Der Kernel eröffnet keine granulare Möglichkeit, zu bestimmen, wie weit der Prozess fortgeschritten war, was erreicht wurde und was nicht und wie die Situation eigentlich verarbeitet werden sollte. Noch schlimmer: Wenn close
EINTR
zurückgibt, ist der Status des Dateideskriptors jetzt unbestimmt:
“If close() is interrupted by a signal [...] the state of [the file descriptor] is unspecified.”
Wie willst du da erreichen, dass die Anwendung sicher damit umgeht? Im Allgemeinen ist die Verarbeitung von EINTR
selbst bei einwandfreien Systemaufrufen kompliziert. Es gibt viele subtile Probleme, die allesamt dazu führen, dass SA_RESTART
nicht ausreicht. Nicht alle Systemaufrufe können neu gestartet werden. Wenn du außerdem erwartest, dass jede*r einzelne Entwickler*in einer Anwendung die vielseitigen Nuancen beim Abrufen eines Signals für jeden Systemaufruf an jedem Aufrufpunkt versteht und eindämmen kann, musst du eigentlich mit Ausfällen rechnen. Von man 7 signal
:
“The following interfaces are never restarted after being interrupted by a signal handler, regardless of the use of SA_RESTART; they always fail with the error EINTR [...]”
Gleichermaßen fordert man Fehler nahezu heraus, wenn man sigprocmask
verwendet und erwartet, dass der Codefluss statisch bleibt. Immerhin denken Entwickler*innen nicht andauernd an die Einschränkungen der Signalverarbeitung oder daran, wie sie Code mit korrekten Signalen erzeugen oder aufrecht erhalten können. Dasselbe gilt für die Verarbeitung von Signalen in einem dedizierten Thread mit sigwaitinfo
. Dabei kann es schnell passieren, dass GDB und ähnliche Tools den Prozess nicht debuggen können. Geringfügige Fehler in Codeflüssen oder beim Umgang mit Fehlern können zu Bugs, Abstürzen, schwer zu debuggenden Beschädigungen, Deadlocks und vielen weiteren Problemen führen, für die dein bevorzugtes Vorfallsverwaltungstool herangezogen werden muss.
Wenn du jetzt gedacht hast, Nebenläufigkeit, Neustartbarkeit und Atomarität seien schon kompliziert genug, wird das Ganze noch viel schlimmer, wenn Multithreading ins Spiel kommt. Das ist besonders wichtig, weil viele komplexe Anwendungen implizit separate Threads ausführen, beispielsweise im Rahmen von jemalloc, GLib o. Ä. Einige dieser Bibliotheken installieren sogar selbst Signal-Handler, um das Ganze noch komplizierter zu machen.
man 7 signal
sagt dazu Folgendes:
“A signal may be generated (and thus pending) for a process as a whole (e.g., when sent using kill(2)) or for a specific thread [...] If more than one of the threads has the signal unblocked, then the kernel chooses an arbitrary thread to which to deliver the signal.”
Kurz: „Für die meisten Signale sendet der Kernel das Signal an irgendeinen Thread, bei dem dieses Signal nicht mit sigprocmask
blockiert ist.“ SIGSEGV, SIGILL und ähnliche Signale ähneln Traps und richten Signale explizit auf den betreffenden Thread aus. Die meisten Signale können aber nicht explizit an einen einzelnen Thread in einer Thread-Gruppe gesendet werden, selbst mit tgkill
oder pthread_kill
.
Das bedeutet, dass man allgemeine Signalverarbeitungsmerkmale nicht einfach ändern kann, wenn eine Gruppe von Threads vorliegt. Wenn ein Service periodische Signalblockierung mit sigprocmask
im Haupt-Thread benötigt, musst du anderen Threads irgendwie extern mitteilen, wie sie damit umgehen sollen. Andernfalls kann das Signal von einem anderen Thread angenommen werden und verschwindet somit. Du kannst natürlich Signale in untergeordneten Threads blockieren, um das zu vermeiden. Wenn diese aber ihre eigene Signalverarbeitung erfordern, selbst für einfache Fälle wie waitpid
, wird das Ganze komplex.
All diese Probleme sind an sich nicht unüberwindbar. Man darf aber nicht ignorieren, dass die dafür erforderliche komplexe Synchronisierung aufwendig ist und zu Fehlern, Verwirrung oder Schlimmerem führen kann.
Signale werden asynchron im Kernel propagiert. Der Systemaufruf kill
wird zurückgegeben, sobald das ausstehende Signal für den Prozess oder die task_struct
des Threads aufgezeichnet wird. Daher ist die zeitnahe Zustellung nicht garantiert, selbst wenn das Signal nicht blockiert wird.
Selbst wenn das Signal zeitnah übermittelt wird, kann dem*der Signalaussteller*in nicht mitgeteilt werden, wie der Status der Anfrage lautet. Daher darf keine bedeutende Handlung von Signalen ausgeführt werden. Bei ihnen besteht nämlich keine Möglichkeit, den Erfolgs- oder Fehlerstatus von Zustellung und nachfolgenden Handlungen zu melden. Wie bereits erwähnt, können selbst scheinbar harmlose Signale gefährlich werden, wenn sie nicht im Nutzungsbereich konfiguriert sind.
Wir alle, die Linux schon lange verwenden, haben es schon einmal erlebt, dass ein Prozess abgebrochen werden sollte, dieser aber nicht reagiert.Nicht einmal auf Signale, die eigentlich immer zu einem Abbruch führen sollten, wie SIGKILL
. Das Problem ist, dass es nicht Zweck von kill(1) ist, Prozesse abzubrechen, sondern nur, eine Anfrage an den Kernel in die Warteschlange zu stellen (ohne Angabe zum Zeitpunkt der Verarbeitung).
Der Systemaufruf kill
dient dazu, das Signal in den Aufgabenmetadaten des Kernels als ausstehend zu markieren. Das wird auch erfolgreich ausgeführt, selbst wenn eine SIGKILL-Aufgabe nicht beendet wird. Im Falle von SIGKILL
garantiert der Kernel, dass keine weiteren Nutzungsmodusanweisungen ausgeführt werden. Wir müssen aber möglicherweise noch Anweisungen im Kernel-Modus ausführen, um Handlungen abzuschließen, die ansonsten zu Datenbeschädigung führen, oder um Ressourcen freizugeben. Aus diesem Grund sind wir auch dann erfolgreich, wenn der Status D lautet (ununterbrechbarer Schlaf). „Kill“ selbst schlägt nur fehl, wenn du ein ungültiges Signal angegeben hast, du nicht zum Senden dieses Signals berechtigt bist oder die angeforderte Ziel-PID für das Signal nicht vorhanden ist und nicht-beendende Statuswerte daher nicht zuverlässig an Anwendungen propagiert werden können.
Wenn du Probleme bei der Signalverarbeitung vermeiden möchtest, könntest du einfach gar keine Signale verarbeiten. Verwende aber bei Anwendungen mit Statusverarbeitung, die Fälle wie SIGTERM
verarbeiten müssen, am besten eine API auf hoher Ebene wie folly::AsyncSignalHandler
. Damit werden einige Problempunkte intuitiver.
sigprocmask
. So muss weniger Code regelmäßig auf korrekte Signale überprüft werden. Denke daran, dass die Maske bei Änderungen an Codeflüssen oder Threading-Strategien möglicherweise nicht wie beabsichtigt funktioniert.signal(SIGHUP, SIG_IGN); signal(SIGQUIT, SIG_IGN); signal(SIGUSR1, SIG_IGN); signal(SIGUSR2, SIG_IGN);
Logik für Signalverhalten ist selbst in gut geschriebenen Programmen extrem kompliziert. Du gehst mit der Verwendung von Signalen unnötige Risiken mit Anwendungen ein, für die auch Alternativen in Frage kommen. Verwende im Allgemeinen keine Signale für die Kommunikation mit dem Nutzungsbereichsteil deines Programms. Stattdessen sollte das Programm Events transparent selbst verarbeiten (zum Beispiel mit inotify) oder Nutzungsbereichs-Kommunikation verwendet werden, die Fehler an den*die Aussteller*in melden kann und zur Kompilierungszeit enumerierbar und demonstrierbar ist, wie Thrift, gRPC o. Ä.
Ich hoffe, dieser Artikel hat veranschaulicht, dass Signale alles andere als einfach sind, obwohl sie simpel erscheinen. Wenn man sie aufgrund ihrer augenscheinlichen Einfachheit als API für Nutzerbereichs-Software verwendet, muss man eine Reihe impliziter Designentscheidungen treffen, die mit den meisten modernen Produktions-Anwendungsfällen nicht vereinbar sind.
Versteht mich aber nicht falsch: Es gibt Anwendungsfälle, bei denen sich Signale anbieten. Signale eignen sich für die einfache Kommunikation mit dem Kernel zu einem gewünschten Prozessstatus, wenn keine Nutzungsbereichskomponente beteiligt ist, z. B. wenn ein Prozess beendet werden soll. Es ist aber schwer, direkt beim ersten Mal Code mit korrekten Signalen zu schreiben, wenn Signale erwartungsgemäß im Nutzungsbereich festgehalten werden sollten.
Signale erscheinen aufgrund ihrer Standardisierung, breiten Verfügbarkeit und ihrem Mangel an Abhängigkeiten attraktiv, bringen aber zahlreiche Gefahren mit sich, die bei wachsenden Projekten nur noch zunehmen. Hoffentlich hast du in diesem Artikel einige Lösungsstrategien und Alternativen gefunden, mit denen du deine Ziele auf sichere, weniger komplexe und intuitivere Weise erreichen kannst.
Um mehr über Meta Open Source zu erfahren, besuche unsere Open-Source-Website, abonniere unseren YouTube-Kanal oder folge uns auf Twitter, Facebook und LinkedIn.