Unterbrechung von Systemaufrufen, wenn ein Signal empfangen wird

29

Beim Lesen der Manpages auf den read()und write()-Anrufen scheint es, dass diese Anrufe durch Signale unterbrochen werden, unabhängig davon, ob sie blockiert werden müssen oder nicht.

Insbesondere annehmen

  • Ein Prozess erstellt einen Handler für ein Signal.
  • Ein Gerät wird geöffnet (z. B. ein Terminal), wenn es O_NONBLOCK nicht eingestellt ist (dh im Sperrmodus arbeitet).
  • Der Prozess read()ruft dann das System auf, um vom Gerät zu lesen, und führt als Ergebnis einen Kernel-Steuerpfad im Kernel-Space aus.
  • Während der Vorgang read()im Kernel-Space ausgeführt wird, wird das Signal, für das der Handler zuvor installiert wurde, an diesen Prozess übergeben und sein Signal-Handler aufgerufen.

Wenn man die Manpages und die entsprechenden Abschnitte in SUSv3 'System Interfaces Volume (XSH)' liest , findet man Folgendes :

ich. Wird a read()durch ein Signal unterbrochen, bevor es Daten liest (dh es musste blockieren, weil keine Daten verfügbar waren), gibt es -1 mit errnodem Wert [EINTR] zurück.

ii. Wird a read()durch ein Signal unterbrochen, nachdem es einige Daten erfolgreich gelesen hat (dh die Anforderung konnte sofort bearbeitet werden), wird die Anzahl der gelesenen Bytes zurückgegeben.

Frage A): Geht ich zu Recht davon aus, dass in beiden Fällen (Block / kein Block) die Übermittlung und Bearbeitung des Signals für den Empfänger nicht vollständig transparent ist read()?

Fall i. Dies erscheint verständlich, da durch das Blockieren read()der Prozess normalerweise in den TASK_INTERRUPTIBLEStatus versetzt wird, sodass der Kernel den Prozess in den Status versetzt, wenn ein Signal übermittelt wird TASK_RUNNING.

Wenn der read()jedoch nicht blockieren muss (Fall ii.) Und die Anforderung im Kernelraum verarbeitet, hätte ich gedacht, dass das Eintreffen eines Signals und seine Behandlung ähnlich wie das Eintreffen und die ordnungsgemäße Behandlung einer Hardware transparent wäre unterbrechen würde. Insbesondere möchte ich , dass bei der Lieferung des Signals angenommen haben, würde der Prozess vorübergehend in platziert werden Benutzermodus sein Signal - Handler auszuführen , von dem sie schließlich die unterbrochene zu beenden zurückkehren würde die Verarbeitung read()(im Kernel-Raum) , so dass das read()läuft seine Natürlich bis zum Ende, danach kehrt der Prozess zu dem Punkt unmittelbar nach dem Aufruf von read()(im Benutzerraum) zurück, wobei alle verfügbaren Bytes als Ergebnis gelesen werden.

Aber ii. scheint zu implizieren, dass das read()unterbrochen wird, da Daten sofort verfügbar sind, aber nur einige der Daten (statt aller) zurückgeben.

Dies bringt mich zu meiner zweiten (und letzten) Frage:

Frage B): Wenn meine Annahme unter A) richtig ist, warum wird das read()unterbrochen, obwohl es nicht gesperrt werden muss, weil Daten verfügbar sind, um die Anforderung sofort zu erfüllen? Mit anderen Worten, warum wird der read()Vorgang nach dem Ausführen des Signal-Handlers nicht fortgesetzt, was schließlich dazu führt, dass alle verfügbaren Daten (die schließlich verfügbar waren) zurückgegeben werden?

Darbehdar
quelle

Antworten:

29

Zusammenfassung: Sie haben Recht, dass der Empfang eines Signals weder für den Fall i (unterbrochen, ohne etwas gelesen zu haben) noch für den Fall ii (unterbrochen, nachdem ein Teil des Signals gelesen wurde) transparent ist. Andernfalls müsste ich grundlegende Änderungen an der Architektur des Betriebssystems und der Architektur der Anwendungen vornehmen.

Die OS-Implementierungsansicht

Überlegen Sie, was passiert, wenn ein Systemaufruf durch ein Signal unterbrochen wird. Der Signalhandler führt den Benutzermoduscode aus. Der Syscall-Handler ist jedoch Kernel-Code und vertraut keinem Benutzermodus-Code. Lassen Sie uns die Auswahlmöglichkeiten für den Syscall-Handler untersuchen:

  • Beenden Sie den Systemaufruf. Berichten Sie, wie viel mit dem Benutzercode getan wurde. Es liegt am Anwendungscode, den Systemaufruf bei Bedarf auf irgendeine Weise neu zu starten. So funktioniert Unix.
  • Speichern Sie den Status des Systemaufrufs, und ermöglichen Sie dem Benutzercode, den Anruf fortzusetzen. Dies ist aus mehreren Gründen problematisch:
    • Während der Benutzercode ausgeführt wird, kann der gespeicherte Status ungültig werden. Wenn Sie beispielsweise aus einer Datei lesen, wird die Datei möglicherweise abgeschnitten. Der Kernel-Code würde also eine Menge Logik benötigen, um diese Fälle zu behandeln.
    • Der gespeicherte Status kann keine Sperre beibehalten, da nicht garantiert werden kann, dass der Benutzercode den Systemaufruf jemals fortsetzt und die Sperre dann für immer aufrechterhalten wird.
    • Der Kernel muss neue Schnittstellen verfügbar machen, um laufende Systemaufrufe fortzusetzen oder abzubrechen, zusätzlich zur normalen Schnittstelle, um einen Systemaufruf zu starten. Dies ist eine Menge Komplikationen für einen seltenen Fall.
    • Der gespeicherte Zustand würde Ressourcen verbrauchen müssen (mindestens Speicher); Diese Ressourcen müssten vom Kernel zugewiesen und gehalten, aber auf die Zuteilung des Prozesses angerechnet werden. Dies ist nicht unüberwindlich, aber es ist eine Komplikation.
      • Beachten Sie, dass der Signalhandler möglicherweise Systemaufrufe durchführt, die selbst unterbrochen werden. Sie können also nicht einfach eine statische Ressourcenzuteilung haben, die alle möglichen Systemaufrufe abdeckt.
      • Und was ist, wenn die Ressourcen nicht zugewiesen werden können? Dann müsste der Syscall sowieso scheitern. Dies bedeutet, dass die Anwendung Code für diesen Fall benötigen würde, sodass dieser Entwurf den Anwendungscode nicht vereinfachen würde.
  • Bleiben Sie in Bearbeitung (aber ausgesetzt), erstellen Sie einen neuen Thread für den Signalhandler. Dies ist wiederum problematisch:
    • Frühe Unix-Implementierungen hatten einen einzelnen Thread pro Prozess.
    • Der Signalgeber würde das Risiko eingehen, die Schuhe des Systemanrufers zu überschreiten. Dies ist ohnehin ein Problem, aber im aktuellen Unix-Design ist es enthalten.
    • Für den neuen Thread müssten Ressourcen zugewiesen werden. siehe oben.

Der Hauptunterschied zu einem Interrupt besteht darin, dass der Interrupt-Code vertrauenswürdig und stark eingeschränkt ist. Es ist normalerweise nicht erlaubt, Ressourcen zuzuweisen, für immer zu laufen, Sperren zu übernehmen und sie nicht freizugeben oder andere unangenehme Dinge zu tun. Da der Interrupt-Handler vom OS-Implementierer selbst geschrieben wurde, weiß er, dass er nichts Schlechtes tun wird. Andererseits kann Anwendungscode alles tun.

Die Ansicht für den Anwendungsentwurf

Wenn eine Anwendung mitten in einem Systemaufruf unterbrochen wird, sollte der Systemaufruf bis zum Abschluss fortgesetzt werden? Nicht immer. Stellen Sie sich beispielsweise ein Programm wie eine Shell vor, die eine Zeile aus dem Terminal liest und die der Benutzer drückt Ctrl+C, um SIGINT auszulösen. Der Lesevorgang darf nicht vollständig sein, darum geht es beim Signal. Beachten Sie, dass dieses Beispiel zeigt, dass der readSyscall auch dann unterbrechbar sein muss, wenn noch kein Byte gelesen wurde.

Daher muss es für die Anwendung eine Möglichkeit geben, den Kernel anzuweisen, den Systemaufruf abzubrechen. Beim Unix-Design geschieht dies automatisch: Das Signal bewirkt, dass der Syscall zurückkehrt. Andere Designs würden eine Möglichkeit erfordern, mit der die Anwendung den Systemaufruf nach Belieben fortsetzen oder abbrechen kann.

Der readSystemaufruf ist so, wie er ist, weil es das Grundelement ist, das angesichts des allgemeinen Designs des Betriebssystems Sinn macht. Was es bedeutet, ist ungefähr "so viel wie möglich lesen, bis zu einer Grenze (der Puffergröße), aber aufhören, wenn etwas anderes passiert". Um einen vollständigen Puffer tatsächlich zu lesen, muss er readin einer Schleife ausgeführt werden, bis so viele Bytes wie möglich gelesen wurden. Dies ist eine übergeordnete Funktion fread(3). Anders read(2)als bei einem Systemaufruf freadhandelt es sich um eine Bibliotheksfunktion, die im Benutzerbereich darüber implementiert ist read. Es ist für eine Anwendung geeignet, die nach einer Datei sucht oder beim Versuch zu sterben. Dies ist weder für einen Befehlszeileninterpreter noch für ein Netzwerkprogramm geeignet, das die Verbindungen sauber drosseln muss, noch für ein Netzwerkprogramm, das über gleichzeitige Verbindungen verfügt und keine Threads verwendet.

Das Beispiel für das Einlesen einer Schleife finden Sie in der Linux-Systemprogrammierung von Robert Love:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror ("read");
    break;
  }
  len -= ret;
  buf += ret;
}

Es kümmert sich um case iund case iiund einige mehr.

Gilles 'SO - hör auf böse zu sein'
quelle
Vielen Dank an Gilles für eine sehr präzise und klare Antwort, die ähnliche Ansichten in einem Artikel über die UNIX-Designphilosophie bestätigt. Scheint mir sehr überzeugend zu sein, dass das Syscall-Unterbrechungsverhalten eher mit der UNIX-Designphilosophie als mit technischen Einschränkungen oder Hindernissen zu tun hat
darbehdar
@darbehdar Alles drei: Unix-Designphilosophie (hier hauptsächlich, dass Prozesse weniger vertrauenswürdig sind als der Kernel und beliebigen Code ausführen können, außerdem, dass Prozesse und Threads nicht implizit erstellt werden), technische Einschränkungen (bei der Ressourcenzuweisung) und Anwendungsdesign (dort) Es gibt Fälle, in denen das Signal den Systemanruf abbrechen muss.
Gilles 'SO- hör auf böse zu sein'
2

So beantworten Sie Frage A :

Ja, die Zustellung und Bearbeitung des Signals ist für den Empfänger nicht völlig transparent read().

Der read()Lauf auf halbem Weg belegt möglicherweise einige Ressourcen, während er durch das Signal unterbrochen wird. Und der Signal-Handler des Signals kann auch ein anderes read()(oder ein beliebiges anderes sicheres Async-Signal-System ) aufrufen . Das read()durch das Signal unterbrochene Signal muss daher zuerst gestoppt werden, um die verwendeten Ressourcen freizugeben. Andernfalls greift der read()vom Signalhandler auf die gleichen Ressourcen zu und verursacht wiedereintrittsbedingte Probleme.

Weil andere Systemaufrufe als read()vom Signalhandler aufgerufen werden könnten und sie möglicherweise auch den gleichen Satz von Ressourcen belegen wie dies der read()Fall ist. Um wiedereintretende Probleme zu vermeiden, ist es am einfachsten, die Unterbrechung read()jedes Mal zu stoppen, wenn ein Signal während des Laufs auftritt.

Justin
quelle