Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 475 – Wiederholung von Systemaufrufen, die mit EINTR fehlschlagen

Autor:
Charles-François Natali <cf.natali at gmail.com>, Victor Stinner <vstinner at python.org>
BDFL-Delegate:
Antoine Pitrou <solipsis at pitrou.net>
Status:
Final
Typ:
Standards Track
Erstellt:
29-Jul-2014
Python-Version:
3.5
Resolution:
Python-Dev Nachricht

Inhaltsverzeichnis

Zusammenfassung

Systemaufruf-Wrapper, die in der Standardbibliothek bereitgestellt werden, sollten automatisch wiederholt werden, wenn sie mit EINTR fehlschlagen, um den Anwendungscode von dieser Last zu befreien.

Unter Systemaufrufen verstehen wir die Funktionen der Standard-C-Bibliothek, die sich auf E/A oder die Verwaltung anderer Systemressourcen beziehen.

Begründung

Unterbrochene Systemaufrufe

Auf POSIX-Systemen sind Signale verbreitet. Code, der Systemaufrufe tätigt, muss darauf vorbereitet sein, diese zu behandeln. Beispiele für Signale

  • Das häufigste Signal ist SIGINT, das Signal, das gesendet wird, wenn STRG+C gedrückt wird. Standardmäßig löst Python eine Ausnahme KeyboardInterrupt aus, wenn dieses Signal empfangen wird.
  • Beim Ausführen von Unterprozessen wird das Signal SIGCHLD gesendet, wenn ein Kindprozess beendet wird.
  • Die Größenänderung des Terminals sendet das Signal SIGWINCH an die im Terminal laufenden Anwendungen.
  • Das Verschieben der Anwendung in den Hintergrund (z. B. durch Drücken von STRG+Z und anschließendes Eingeben des Befehls bg) sendet das Signal SIGCONT.

Das Schreiben eines C-Signalhandlers ist schwierig: Nur „async-signal-safe“ Funktionen dürfen aufgerufen werden (z. B. sind printf() und malloc() nicht async-signal-safe), und es gibt Probleme mit der Wiederaufrufbarkeit. Wenn also ein Signal während der Ausführung eines Systemaufrufs von einem Prozess empfangen wird, kann der Systemaufruf mit dem Fehler EINTR fehlschlagen, um dem Programm die Möglichkeit zu geben, das Signal ohne die Einschränkung auf Signal-sichere Funktionen zu behandeln.

Dieses Verhalten ist systemabhängig: Auf bestimmten Systemen werden mit dem Flag SA_RESTART einige Systemaufrufe automatisch wiederholt, anstatt mit EINTR zu fehlschlagen. Unabhängig davon löscht die Funktion signal.signal() von Python beim Setzen des Signalhandlers das Flag SA_RESTART: Alle Systemaufrufe werden in Python wahrscheinlich mit EINTR fehlschlagen.

Da der Empfang eines Signals kein außergewöhnliches Ereignis ist, muss robuster POSIX-Code darauf vorbereitet sein, EINTR zu behandeln (was in den meisten Fällen bedeutet, in einer Schleife zu wiederholen, in der Hoffnung, dass der Aufruf schließlich erfolgreich ist). Ohne spezielle Unterstützung von Python kann dies den Anwendungscode wesentlich umständlicher machen als nötig.

Status in Python 3.4

In Python 3.4 wird die Behandlung der Ausnahme InterruptedError (die dedizierte Ausnahme-Klasse für EINTR) an jeder Aufrufstelle einzeln behandelt. Nur wenige Python-Module behandeln diese Ausnahme tatsächlich, und Korrekturen dauerten normalerweise mehrere Jahre, um ein ganzes Modul abzudecken. Beispiel für Code, der file.read() bei InterruptedError wiederholt

while True:
    try:
        data = file.read(size)
        break
    except InterruptedError:
        continue

Liste der Python-Module in der Standardbibliothek, die InterruptedError behandeln

  • asyncio
  • asyncore
  • io, _pyio
  • multiprocessing
  • selectors
  • socket
  • socketserver
  • subprocess

Andere Programmiersprachen wie Perl, Java und Go wiederholen Systemaufrufe, die mit EINTR fehlschlagen, auf einer niedrigeren Ebene, sodass sich Bibliotheken und Anwendungen nicht darum kümmern müssen.

Anwendungsfall 1: Signale nicht beachten

In den meisten Fällen möchten Sie nicht durch Signale unterbrochen werden und erwarten keine InterruptedError-Ausnahmen. Möchten Sie beispielsweise wirklich solch komplexen Code für ein „Hallo Welt“-Beispiel schreiben?

while True:
    try:
        print("Hello World")
        break
    except InterruptedError:
        continue

InterruptedError kann an unerwarteten Stellen auftreten. Zum Beispiel können os.close() und FileIO.close() eine InterruptedError auslösen: siehe den Artikel close() und EINTR.

Der Abschnitt Python-Probleme im Zusammenhang mit EINTR unten gibt Beispiele für Fehler, die durch EINTR verursacht werden.

Die Erwartung in diesem Anwendungsfall ist, dass Python die InterruptedError ausblendet und Systemaufrufe automatisch wiederholt.

Anwendungsfall 2: Schnellstmöglich über Signale informiert werden

Manchmal erwartet man jedoch einige Signale und möchte sie so schnell wie möglich behandeln. Sie möchten zum Beispiel ein Programm sofort beenden, indem Sie die Tastenkombination STRG+C verwenden.

Außerdem sind einige Signale nicht von Interesse und sollten die Anwendung nicht stören. Es gibt zwei Optionen, eine Anwendung nur bei *einigen* Signalen zu unterbrechen

  • Richten Sie einen benutzerdefinierten Signalhandler ein, der eine Ausnahme auslöst, wie z. B. KeyboardInterrupt für SIGINT.
  • Verwenden Sie eine E/A-Multiplexing-Funktion wie select() zusammen mit dem Signal-Wakeup-Dateideskriptor von Python: siehe die Funktion signal.set_wakeup_fd().

Die Erwartung in diesem Anwendungsfall ist, dass der Python-Signalhandler rechtzeitig ausgeführt wird und der Systemaufruf fehlschlägt, wenn der Handler eine Ausnahme auslöst – andernfalls neu startet.

Vorschlag

Dieses PEP schlägt vor, EINTR und Wiederholungen auf der niedrigsten Ebene zu behandeln, d. h. in den von der Standardbibliothek bereitgestellten Wrappern (im Gegensatz zu höherrangigen Bibliotheken und Anwendungen).

Insbesondere muss der Python-Wrapper, wenn ein Systemaufruf mit EINTR fehlschlägt, den angegebenen Signalhandler aufrufen (unter Verwendung von PyErr_CheckSignals()). Wenn der Signalhandler eine Ausnahme auslöst, bricht der Python-Wrapper ab und schlägt mit der Ausnahme fehl.

Wenn der Signalhandler erfolgreich zurückkehrt, wiederholt der Python-Wrapper den Systemaufruf automatisch. Wenn der Systemaufruf einen Timeout-Parameter beinhaltet, wird der Timeout neu berechnet.

Geänderte Funktionen

Beispiele für Standardbibliotheksfunktionen, die geändert werden müssen, um diesem PEP zu entsprechen

  • open(), os.open(), io.open()
  • Funktionen des Moduls faulthandler
  • Funktionen von os
    • os.fchdir()
    • os.fchmod()
    • os.fchown()
    • os.fdatasync()
    • os.fstat()
    • os.fstatvfs()
    • os.fsync()
    • os.ftruncate()
    • os.mkfifo()
    • os.mknod()
    • os.posix_fadvise()
    • os.posix_fallocate()
    • os.pread()
    • os.pwrite()
    • os.read()
    • os.readv()
    • os.sendfile()
    • os.wait3()
    • os.wait4()
    • os.wait()
    • os.waitid()
    • os.waitpid()
    • os.write()
    • os.writev()
    • Sonderfälle: os.close() und os.dup2() ignorieren jetzt den Fehler EINTR, der Systemaufruf wird nicht wiederholt
  • select.select(), select.poll.poll(), select.epoll.poll(), select.kqueue.control(), select.devpoll.poll()
  • Methoden von socket.socket()
    • accept()
    • connect() (außer für nicht-blockierende Sockets)
    • recv()
    • recvfrom()
    • recvmsg()
    • send()
    • sendall()
    • sendmsg()
    • sendto()
  • signal.sigtimedwait(), signal.sigwaitinfo()
  • time.sleep()

(Hinweis: Das Modul selector wiederholt bereits bei InterruptedError, berechnet den Timeout jedoch noch nicht neu)

os.close, close()-Methoden und os.dup2() sind ein Sonderfall: Sie ignorieren EINTR anstatt eine Wiederholung durchzuführen. Der Grund dafür ist komplex und beinhaltet das Verhalten unter Linux und die Tatsache, dass der Dateideskriptor tatsächlich geschlossen sein kann, auch wenn EINTR zurückgegeben wird. Siehe Artikel

Die Methode socket.socket.connect() wiederholt connect() für nicht-blockierende Sockets nicht, wenn sie durch ein Signal unterbrochen wird (Schlägt mit EINTR fehl). Die Verbindung läuft asynchron im Hintergrund. Der Aufrufer ist dafür verantwortlich, zu warten, bis der Socket schreibbar wird (z. B. mit select.select()) und dann socket.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) aufzurufen, um zu prüfen, ob die Verbindung erfolgreich war (getsockopt() gibt 0 zurück) oder fehlgeschlagen ist.

Behandlung von InterruptedError

Da unterbrochene Systemaufrufe automatisch wiederholt werden, sollte die Ausnahme InterruptedError beim Aufruf dieser Systemaufrufe nicht mehr auftreten. Daher kann die manuelle Behandlung von InterruptedError, wie in Status in Python 3.4 beschrieben, entfernt werden, was den Code der Standardbibliothek vereinfacht.

Abwärtskompatibilität

Anwendungen, die sich darauf verlassen, dass Systemaufrufe mit InterruptedError unterbrochen werden, werden hängen bleiben. Die Autoren dieses PEP glauben nicht, dass es solche Anwendungen gibt, da sie anderen Problemen wie Race Conditions ausgesetzt wären (es besteht die Möglichkeit auf Deadlock, wenn das Signal vor dem Systemaufruf eintrifft). Außerdem wäre ein solcher Code nicht portierbar.

Auf jeden Fall müssen solche Anwendungen korrigiert werden, um Signale anders zu behandeln und ein zuverlässiges Verhalten auf allen Plattformen und allen Python-Versionen zu gewährleisten. Eine mögliche Strategie ist die Einrichtung eines Signalhandlers, der eine klar definierte Ausnahme auslöst, oder die Verwendung eines Wakeup-Dateideskriptors.

Für Anwendungen, die ereignisgesteuerte Schleifen verwenden, ist signal.set_wakeup_fd() die empfohlene Option zur Behandlung von Signalen. Der Low-Level-Signalhandler von Python schreibt Signalnummern in den Dateideskriptor und die ereignisgesteuerte Schleife wird geweckt, um sie zu lesen. Die ereignisgesteuerte Schleife kann diese Signale ohne die Einschränkung von Signalhandlern behandeln (z. B. kann die Schleife in jedem Thread geweckt werden, nicht nur im Hauptthread).

Anhang

Wakeup-Dateideskriptor

Seit Python 3.3 schreibt signal.set_wakeup_fd() die Signalnummer in den Dateideskriptor, während es vorher nur ein Nullbyte schrieb. Es wird möglich, Signale über den Wakeup-Dateideskriptor zu unterscheiden.

Linux verfügt über den Systemaufruf signalfd(), der mehr Informationen zu jedem Signal liefert. So ist es beispielsweise möglich, die PID und UID zu kennen, die das Signal gesendet haben. Diese Funktion ist in Python noch nicht verfügbar (siehe issue 12304).

Unter Unix verwendet das Modul asyncio den Wakeup-Dateideskriptor, um seine ereignisgesteuerte Schleife zu wecken.

Multithreading

Ein C-Signalhandler kann von jedem Thread aus aufgerufen werden, aber Python-Signalhandler werden immer im Haupt-Python-Thread aufgerufen.

Die C-API von Python stellt die Funktion PyErr_SetInterrupt() zur Verfügung, die den Signalhandler für SIGINT aufruft, um den Haupt-Python-Thread zu unterbrechen.

Signale unter Windows

Steuerungsereignisse

Windows verwendet „Steuerungsereignisse“

  • CTRL_BREAK_EVENT: Abbruch (SIGBREAK)
  • CTRL_CLOSE_EVENT: Schließungsereignis
  • CTRL_C_EVENT: STRG+C (SIGINT)
  • CTRL_LOGOFF_EVENT: Abmeldung
  • CTRL_SHUTDOWN_EVENT: Herunterfahren

Die Funktion SetConsoleCtrlHandler() kann verwendet werden, um einen Steuerungs-Handler zu installieren.

Die Ereignisse CTRL_C_EVENT und CTRL_BREAK_EVENT können mit der Funktion GenerateConsoleCtrlEvent() an einen Prozess gesendet werden. Diese Funktion wird in Python als os.kill() exponiert.

Signale

Die folgenden Signale werden unter Windows unterstützt

  • SIGABRT
  • SIGBREAK (CTRL_BREAK_EVENT): nur unter Windows verfügbares Signal
  • SIGFPE
  • SIGILL
  • SIGINT (CTRL_C_EVENT)
  • SIGSEGV
  • SIGTERM

SIGINT

Der Standard-Python-Signalhandler für SIGINT setzt ein Windows-Ereignisobjekt: sigint_event.

time.sleep() wird mit WaitForSingleObjectEx() implementiert, es wartet auf das Objekt sigint_event mit dem Parameter von time.sleep() als Timeout. Daher kann der Schlaf durch SIGINT unterbrochen werden.

_winapi.WaitForMultipleObjects() fügt sigint_event automatisch zur Liste der überwachten Handles hinzu, sodass es ebenfalls unterbrochen werden kann.

PyOS_StdioReadline() verwendete ebenfalls sigint_event, wenn fgets() fehlschlug, um zu prüfen, ob STRG+C oder STRG+Z gedrückt wurde.

Implementierung

Die Implementierung wird in issue 23285 verfolgt. Sie wurde am 07. Februar 2015 committet.


Quelle: https://github.com/python/peps/blob/main/peps/pep-0475.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT