Wiedereintritt (Computer) - Reentrancy (computing)

In der Datenverarbeitung wird ein Computerprogramm oder eine Subroutine als reentrant bezeichnet, wenn mehrere Aufrufe gleichzeitig auf mehreren Prozessoren oder auf einem Einzelprozessorsystem sicher ausgeführt werden können, wobei eine reentrante Prozedur mitten in ihrer Ausführung unterbrochen und dann sicher wieder aufgerufen werden kann (" re-entered"), bevor die vorherigen Aufrufe die Ausführung abschließen. Die Unterbrechung könnte durch eine interne Aktion wie einen Sprung oder Aufruf oder durch eine externe Aktion wie eine Unterbrechung oder ein Signal verursacht werden , im Gegensatz zu einer Rekursion , bei der neue Aufrufe nur durch einen internen Aufruf verursacht werden können.

Diese Definition stammt aus Multiprogramming-Umgebungen, in denen der Steuerungsfluss durch einen Interrupt unterbrochen und an eine Interrupt-Service-Routine (ISR) oder eine "Handler"-Subroutine übertragen werden könnte. Jedes vom Handler verwendete Unterprogramm, das möglicherweise ausgeführt wurde, als der Interrupt ausgelöst wurde, sollte reentrant sein. Unterroutinen, auf die über den Betriebssystemkernel zugegriffen werden kann, sind häufig nicht reentrant. Daher sind Interrupt-Service-Routinen in den Aktionen beschränkt, die sie ausführen können; zum Beispiel sind sie normalerweise daran gehindert, auf das Dateisystem zuzugreifen und manchmal sogar Speicher zuzuweisen.

Diese Definition von Wiedereintritt unterscheidet sich von der der Thread-Sicherheit in Multithread-Umgebungen. Eine reentrante Subroutine kann Thread-Sicherheit erreichen, aber reentrant allein reicht möglicherweise nicht aus, um in allen Situationen Thread-sicher zu sein. Umgekehrt muss threadsicherer Code nicht unbedingt reentrant sein (Beispiele siehe unten).

Andere Begriffe, die für wiedereintretende Programme verwendet werden, umfassen "gemeinsamer Code". Wiedereintretende Unterprogramme werden manchmal im Referenzmaterial als "signalsicher" gekennzeichnet. Reentry-Programme sind oft "reine Prozeduren".

Hintergrund

Wiedereintritt ist nicht dasselbe wie Idempotenz , bei der die Funktion mehr als einmal aufgerufen werden kann, aber genau die gleiche Ausgabe erzeugt, als ob sie nur einmal aufgerufen worden wäre. Im Allgemeinen erzeugt eine Funktion Ausgabedaten basierend auf einigen Eingabedaten (obwohl beide im Allgemeinen optional sind). Auf freigegebene Daten kann jederzeit von jeder Funktion zugegriffen werden. Wenn Daten durch eine beliebige Funktion geändert werden können (und keine diese Änderungen nachverfolgt), gibt es keine Garantie für diejenigen, die ein Datum teilen, dass dieses Datum dasselbe ist wie zu irgendeinem Zeitpunkt zuvor.

Daten haben ein Merkmal namens scope , das beschreibt, wo in einem Programm die Daten verwendet werden dürfen. Der Datenbereich ist entweder global (außerhalb des Gültigkeitsbereichs einer Funktion und mit unbestimmter Ausdehnung) oder lokal (bei jedem Aufruf einer Funktion erstellt und beim Beenden zerstört).

Lokale Daten werden nicht von Routinen geteilt, ob sie erneut eintreten oder nicht; Daher hat es keinen Einfluss auf den Wiedereintritt. Globale Daten werden außerhalb von Funktionen definiert und können von mehr als einer Funktion aufgerufen werden, entweder in Form von globalen Variablen (von allen Funktionen gemeinsam genutzte Daten) oder als statische Variablen (von allen Aufrufen derselben Funktion gemeinsam genutzte Daten). In der objektorientierten Programmierung werden globale Daten im Geltungsbereich einer Klasse definiert und können privat sein, sodass sie nur für Funktionen dieser Klasse zugänglich sind. Es gibt auch das Konzept der Instanzvariablen , bei dem eine Klassenvariable an eine Klasseninstanz gebunden ist. Aus diesen Gründen ist diese Unterscheidung in der objektorientierten Programmierung meist den außerhalb der Klasse zugänglichen Daten (public) und den von Klasseninstanzen unabhängigen Daten (statisch) vorbehalten.

Wiedereintritt unterscheidet sich von Thread-Sicherheit , ist aber eng damit verbunden . Eine Funktion kann threadsicher und dennoch nicht reentrant sein. Zum Beispiel könnte eine Funktion rundum mit einem Mutex umschlossen werden (was Probleme in Multithreading-Umgebungen vermeidet), aber wenn diese Funktion in einer Interrupt-Service-Routine verwendet würde, könnte es verhungern, auf die erste Ausführung zu warten, um den Mutex freizugeben. Der Schlüssel zur Vermeidung von Verwirrung besteht darin, dass sich Reentrant nur auf einen ausgeführten Thread bezieht . Es ist ein Konzept aus der Zeit, als es noch keine Multitasking-Betriebssysteme gab.

Regeln für die Wiedereinreise

Wiedereintretender Code darf ohne Serialisierung keine statischen oder globalen nicht konstanten Daten enthalten .
Wiedereintretende Funktionen können mit globalen Daten arbeiten. Zum Beispiel könnte eine wiedereintretende Interrupt-Service-Routine einen Hardware-Status erfassen, um damit zu arbeiten (z. B. den Lesepuffer des seriellen Ports), der nicht nur global, sondern auch flüchtig ist. Die typische Verwendung statischer Variablen und globaler Daten wird jedoch nicht empfohlen, da außer in nicht serialisierten Codeabschnitten in diesen Variablen nur atomare Read-Modify-Write- Anweisungen verwendet werden sollten (dies sollte nicht möglich sein für ein Interrupt oder ein Signal, das während der Ausführung eines solchen Befehls kommt). Beachten Sie, dass in C selbst ein Lesen oder Schreiben nicht garantiert atomar ist; es kann in mehrere Lese- oder Schreibvorgänge aufgeteilt werden. Der C-Standard und SUSv3 sehen sig_atomic_thierfür, allerdings mit Garantien, nur einfaches Lesen und Schreiben vor, nicht aber Inkrementieren oder Dekrementieren. Komplexere atomare Operationen sind in C11 verfügbar , das stdatomic.h.
Wiedereintretender Code darf sich ohne Serialisierung nicht selbst ändern .
Das Betriebssystem kann einem Prozess erlauben, seinen Code zu ändern. Dafür gibt es verschiedene Gründe ( zB schnelles Blitten von Grafiken), aber dies erfordert im Allgemeinen eine Serialisierung, um Probleme mit dem Wiedereintritt zu vermeiden.

Es kann sich jedoch selbst modifizieren, wenn es sich in seinem eigenen einzigartigen Speicher befindet. Das heißt, wenn jeder neue Aufruf eine andere physische Maschinencodestelle verwendet, an der eine Kopie des ursprünglichen Codes erstellt wird, beeinflusst er andere Aufrufe nicht, selbst wenn er sich während der Ausführung dieses bestimmten Aufrufs (Threads) ändert.

Reentry - Code kann nichtablaufinvarianten nicht nennen Computerprogramme oder Routinen .
Mehrere Ebenen der Benutzer, ein Objekt oder Prozess Priorität oder Multiprozessing erschweren in der Regel die Kontrolle der Reentry - Code. Es ist wichtig, alle Zugriffe oder Nebenwirkungen im Auge zu behalten, die innerhalb einer Routine durchgeführt werden, die auf Wiedereintritt ausgelegt ist.

Der Wiedereintritt einer Subroutine, die mit Betriebssystemressourcen oder nicht-lokalen Daten arbeitet, hängt von der Atomarität der jeweiligen Operationen ab. Wenn das Unterprogramm beispielsweise eine globale 64-Bit-Variable auf einem 32-Bit-Rechner modifiziert, kann die Operation in zwei 32-Bit-Operationen aufgeteilt werden, und somit, wenn das Unterprogramm während der Ausführung unterbrochen und vom Interrupt-Handler erneut aufgerufen wird , kann sich die globale Variable in einem Zustand befinden, in dem nur 32 Bits aktualisiert wurden. Die Programmiersprache kann Atomizitätsgarantien für Unterbrechungen bereitstellen, die durch eine interne Aktion wie einen Sprung oder einen Aufruf verursacht werden. Dann würde die Funktion fin einem Ausdruck wie (global:=1) + (f()), bei dem die Auswertungsreihenfolge der Unterausdrücke in einer Programmiersprache beliebig sein könnte, die globale Variable entweder auf 1 oder auf ihren vorherigen Wert setzen, aber nicht in einem Zwischenzustand, in dem nur ein Teil wurde Aktualisiert. (Letzteres kann in C passieren , da der Ausdruck keinen Sequenzpunkt hat .) Das Betriebssystem kann Atomizitätsgarantien für Signale bereitstellen , wie z. B. ein Systemaufruf, der durch ein Signal unterbrochen wird, das keine partielle Wirkung hat. Die Prozessorhardware kann Atomizitätsgarantien für Interrupts bereitstellen , wie beispielsweise unterbrochene Prozessorbefehle, die keine partiellen Auswirkungen haben.

Beispiele

Um den Wiedereintritt zu veranschaulichen, verwendet dieser Artikel als Beispiel eine C- Dienstprogrammfunktion swap(), die zwei Zeiger verwendet und ihre Werte transponiert, und eine Interrupt-Behandlungsroutine, die auch die Swap-Funktion aufruft.

Weder reentrant noch threadsicher

Dies ist eine beispielhafte Auslagerungsfunktion, die nicht reentrant oder threadsicher ist. Da die tmpVariable ohne Synchronisation global von allen gleichzeitigen Instanzen der Funktion geteilt wird, kann eine Instanz die Daten stören, auf die sich eine andere verlässt. Als solches sollte es nicht in der Interrupt-Service-Routine verwendet werden isr():

int tmp;

void swap(int* x, int* y)
{
    tmp = *x;
    *x = *y;
    /* Hardware interrupt might invoke isr() here. */
    *y = tmp;    
}

void isr()
{
    int x = 1, y = 2;
    swap(&x, &y);
}

Thread-sicher, aber nicht reentrant

Die Funktion swap()im vorherigen Beispiel kann thread-sicher gemacht werden, indem tmp thread-local gemacht wird . Es ist immer noch nicht reentrant, und dies führt weiterhin zu Problemen, wenn isr()es im selben Kontext wie ein bereits ausgeführter Thread aufgerufen wird swap():

_Thread_local int tmp;

void swap(int* x, int* y)
{
    tmp = *x;
    *x = *y;
    /* Hardware interrupt might invoke isr() here. */
    *y = tmp;    
}

void isr()
{
    int x = 1, y = 2;
    swap(&x, &y);
}

Wiedereintretend, aber nicht threadsicher

Die folgende (etwas erfundene) Modifikation der Swap-Funktion, die darauf achtet, die globalen Daten beim Verlassen in einem konsistenten Zustand zu belassen, ist reentrant; es ist jedoch nicht threadsicher, da keine Sperren verwendet werden, es kann jederzeit unterbrochen werden:

int tmp;

void swap(int* x, int* y)
{
    /* Save global variable. */
    int s;
    s = tmp;

    tmp = *x;
    *x = *y;      /*If hardware interrupt occurs here then it will fail to keep the value of tmp. So this is also not a reentrant example*/
    *y = tmp;     /* Hardware interrupt might invoke isr() here. */

    /* Restore global variable. */
    tmp = s;
}

void isr()
{
    int x = 1, y = 2;
    swap(&x, &y);
}

Reentrant und Thread-sicher

Eine Implementierung swap(), die tmpauf dem Stack statt global allokiert und die nur mit nicht geteilten Variablen als Parameter aufgerufen wird, ist sowohl threadsicher als auch reentrant. Thread-sicher, da der Stack lokal für einen Thread ist und eine Funktion, die nur auf lokalen Daten reagiert, immer das erwartete Ergebnis liefert. Es gibt keinen Zugriff auf geteilte Daten, also kein Datenrennen.

void swap(int* x, int* y)
{
    int tmp;
    tmp = *x;
    *x = *y;
    *y = tmp;    /* Hardware interrupt might invoke isr() here. */
}

void isr()
{
    int x = 1, y = 2;
    swap(&x, &y);
}

Wiedereintretender Interrupt-Handler

Ein wiedereintretender Interrupt-Handler ist ein Interrupt-Handler , der Interrupts früh im Interrupt-Handler wieder aktiviert. Dies kann die Interrupt-Latenz verringern . Generell wird beim Programmieren von Interrupt-Service-Routinen empfohlen, Interrupts so schnell wie möglich im Interrupt-Handler wieder zu aktivieren. Diese Vorgehensweise hilft zu vermeiden, dass Interrupts verloren gehen.

Weitere Beispiele

Im folgenden Code sind weder Funktionen fnoch gFunktionen reentrant.

int v = 1;

int f()
{
    v += 2;
    return v;
}

int g()
{
    return f() + 2;
}

Oben f()hängt von einer nicht konstanten globalen Variablen ab v; Wenn f()also während der Ausführung von einem ISR unterbrochen wird, der ändert v, dann f()wird der erneute Eintritt in den falschen Wert von zurückgegeben v. Der Wert von vund daher der Rückgabewert von fkann nicht mit Sicherheit vorhergesagt werden: Sie variieren je nachdem, ob ein Interrupt vwährend fder Ausführung von geändert wurde . Daher fist kein Wiedereintritt. Das ist auch nicht der Fall g, weil es ruft f, was nicht reentrant ist.

Diese leicht veränderten Versionen sind reentrant:

int f(int i)
{
    return i + 2;
}

int g(int i)
{
    return f(i) + 2;
}

Im Folgenden ist die Funktion threadsicher, aber nicht (notwendig) reentrant:

int function()
{
    mutex_lock();

    // ...
    // function body
    // ...

    mutex_unlock();
}

Im obigen function()kann problemlos von verschiedenen Threads aufgerufen werden. Wenn die Funktion jedoch in einem reentranten Interrupt-Handler verwendet wird und ein zweiter Interrupt innerhalb der Funktion auftritt, bleibt die zweite Routine für immer hängen. Da die Interrupt-Bearbeitung andere Interrupts deaktivieren kann, könnte das gesamte System darunter leiden.

Anmerkungen

Siehe auch

Verweise

zitierte Werke

Weiterlesen