Frage

In UNIX-Systeme wissen wir malloc() ist ein nicht-Reentry-Funktion (Systemaufruf). Warum das?

In ähnlicher Weise printf() auch gesagt wird, nichtablaufinvarianten sein; warum?

Ich kenne die Definition des Neueintritt, aber ich wollte wissen, warum es auf diese Funktionen gilt.   Was hindert sie einspringenden garantiert werden?

War es hilfreich?

Lösung

malloc und printf verwenden in der Regel globale Strukturen und beschäftigen Lock-basierte Synchronisation intern. Deshalb sind sie nicht einspringenden sind.

Die malloc Funktion könnte entweder Thread-sicher oder fade unsicher sein. Beide sind nicht einspringenden:

  1. Malloc arbeitet mit einem globalen Heap, und es ist möglich, dass zwei verschiedene Anrufungen von malloc, die zur gleichen Zeit geschehen, den gleichen Speicherblock zurück. (Der zweite malloc Aufruf sollte geschehen, bevor eine Adresse des Blocks geholt wird, aber der Brocken nicht als nicht verfügbar markiert). Dies verstößt gegen die Nachbedingung von malloc, so dass diese Umsetzung nicht einspringenden wäre.

  2. Um diesen Effekt zu vermeiden, wird ein Thread-sichere Implementierung von malloc würde Lock-basierte Synchronisation verwenden. Wenn jedoch malloc von Signal-Handler aufgerufen wird, kann die folgende Situation eintreten:

    malloc();            //initial call
      lock(memory_lock); //acquire lock inside malloc implementation
    signal_handler();    //interrupt and process signal
    malloc();            //call malloc() inside signal handler
      lock(memory_lock); //try to acquire lock in malloc implementation
      // DEADLOCK!  We wait for release of memory_lock, but 
      // it won't be released because the original malloc call is interrupted
    

    Diese Situation wird nicht passieren, wenn malloc einfach aus verschiedenen Threads aufgerufen wird. Tatsächlich erfordert das reentrancy Konzept geht über die Thread-Sicherheit und auch Funktionen zur Arbeit richtig auch wenn einer seiner Aufruf endet nie . Das ist im Grunde die Begründung, warum jede Funktion mit Schlössern wäre nicht einspringenden.

Die printf Funktion auch auf globale Daten betrieben. Jeder Ausgabestrom verwendet in der Regel einen globalen Puffer auf die Ressourcendaten gebunden sind, gesendet (einen Puffer für Terminal oder eine Datei). Der Druckvorgang ist normalerweise eine Sequenzdaten des Kopierens der Puffer danach zu puffern und Spülen. Dieser Puffer sollte durch Sperren in der gleichen Art und Weise geschützt werden malloc tut. Daher printf ist auch nichtablaufinvarianten.

Andere Tipps

verstehen Lassen Sie uns, was wir von bedeuten einspringenden . Eine einspringende Funktion kann vor hat sich zu einem früheren Aufruf beendet aufgerufen werden. Dies könnte passieren, wenn

  • eine Funktion ist, in einem Signal-Handler (oder allgemeiner als Unix einig Interrupt-Handler) für ein Signal bezeichnet, die während der Ausführung der Funktion
  • angehoben
  • eine Funktion aufgerufen wird rekursiv

malloc ist nicht einspringenden, weil es mehrere globale Datenstrukturen verwalten, die freien Speicherblöcke zu verfolgen.

printf ist nicht einspringenden, weil es eine globale Variable heißt den Inhalt der Datei * Stout modifiziert.

Es gibt mindestens drei Begriffe hier, von denen alle in der Umgangssprache verschmelzt werden, weshalb Sie waren verwirrt werden könnten.

  • thread-safe
  • kritische Abschnitt
  • einspringenden

So nehmen Sie die einfachste zuerst: Sowohl malloc und printf sind thread-safe . Sie sind seit 2011, seit 2001 in POSIX seinen Thread-sicher in Standard C garantiert, und in der Praxis seit langem davor. Das bedeutet, dass das folgende Programm garantiert nicht zum Absturz bringen oder zeigen schlechtes Verhalten:

#include <pthread.h>
#include <stdio.h>

void *printme(void *msg) {
  while (1)
    printf("%s\r", (char*)msg);
}

int main() {
  pthread_t thr;
  pthread_create(&thr, NULL, printme, "hello");        
  pthread_create(&thr, NULL, printme, "goodbye");        
  pthread_join(thr, NULL);
}

Ein Beispiel für eine Funktion, die ist nicht Thread-sicher ist strtok. Wenn Sie strtok von zwei verschiedenen Threads gleichzeitig aufrufen, ist das Ergebnis nicht definiertes Verhalten - weil strtok intern einen statischen Puffer verwendet Spur von seinem Zustand zu halten. glibc fügt strtok_r dieses Problem zu beheben, und C11 hinzugefügt, um die gleiche Sache (aber optional und unter einem anderen Namen, weil nicht hier erfunden) als strtok_s.

Okay, aber nicht printf Verwendung globale Ressourcen, um ihre Ausgabe zu bauen, auch? In der Tat, was würde es auch mittlere auf der Standardausgabe von zwei Threads drucken gleichzeitig? Das bringt uns zum nächsten Thema. Offensichtlich printf wird ein kritischer Abschnitt sein in jedem Programm, das verwendet es. ist nur ein Thread der Ausführung innerhalb des kritischen Abschnitts sein darf auf einmal.

Mindestens in POSIX-kompatibelen Systemen, dies, indem printf mit einem Aufruf an flockfile(stdout) und Ende mit einem Aufruf an funlockfile(stdout) beginnen erreicht wird, das ist im Grunde wie ein globales Mutex mit stdout assoziiert nehmen.

Doch jedes einzelne FILE im Programm darf eigene Mutex haben. Dies bedeutet, dass ein Thread fprintf(f1,...) zur gleichen Zeit, dass ein zweiter Thread in der Mitte eines Anrufs zu fprintf(f2,...) aufrufen können. Es gibt kein Rennen hier Zustand. (Ob Ihr libc läuft tatsächlich diese zwei Anrufe parallel ist ein QoI Problem. ich weiß nicht wirklich, was glibc der Fall ist.)

In ähnlicher Weise ist malloc unwahrscheinlich, dass ein kritischer Abschnitt in jedem modernen System sein, denn moderne Systeme sind intelligent genug, um für jeden Thread in dem System einen Pool von Speichern zu halten, anstatt alle N Threads über einen einzigen Pool kämpfen zu müssen. (Der sbrk Systemaufruf wird wahrscheinlich noch ein kritischer Abschnitt sein, aber malloc verbringt sehr wenig von seiner Zeit in sbrk. Oder mmap, oder was auch immer die coolen Kids in diese Tage verwenden.)

Okay, so Was bedeutet Neueintritt eigentlich Mittelwert Im Grunde bedeutet es, dass die Funktion sicher rekursiv aufgerufen werden kann - der aktuelle Aufruf während eines zweiten Aufrufs runs „auf Eis gelegt“ wird, und dann bis dem erste Aufruf ist noch in der Lage zu „holen wo sie aufgehört haben.“ (Technisch dieses Macht nicht aufgrund eines rekursiven Aufruf: der erste Aufruf könnte in Thread A sein, das von Thread B in der Mitte unterbrochen wird, die den zweiten Aufruf macht Aber das Szenario ist nur ein. Sonderfall von Thread-Sicherheit , so dass wir über sie in diesem Absatz vergessen.)

Weder printf noch malloc möglicherweise kann wird rekursiv von einem einzigen Thread genannt, weil sie Blattfunktionen sind (sie sich nicht nennen, noch rufen an jeden Benutzer-controlled-Code, der möglicherweise einen rekursiven Aufruf machen könnte). Und, wie wir oben gesehen haben, sie haben gewesen Thread-sicher gegen * multi- * Gewinde einspringenden Anrufe seit 2001 (durch Sperren verwendet wird).

Also, wer Ihnen gesagt, dass printf und malloc waren nichtablaufinvarianten war falsch; was sie gemeint war wohl zu sagen, dass sie beide das Potenzial haben, sein kritische Abschnitte in Ihrem Programm -. Engpaß bei denen nur ein Thread zu einem Zeitpunkt durchkommen kann


Pedantic Anmerkung: glibc eine Erweiterung bereitstellt, durch die printf gemacht werden können beliebige Benutzer-Code aufzurufen, einschließlich sich selbst wieder aufrufen. Dies ist vollkommen sicher in allen seinen Permutationen - zumindest so weit wie Thread-Sicherheit angeht. (Offensichtlich öffnet es die Tür zu absolut verrückt Format-String-Schwachstellen.) Es gibt zwei Varianten: register_printf_function (das dokumentiert ist und einigermaßen gesund, aber offiziell „veralteten“) und register_printf_specifier (die fast identisch mit Ausnahme eines zusätzlichen undokumentierte Parameter und einem völligen Mangel an Benutzer gerichtete Dokumentation ). Ich würde auch nicht von ihnen empfehlen, und erwähne sie hier nur als eine interessante Seite.

#include <stdio.h>
#include <printf.h>  // glibc extension

int widget(FILE *fp, const struct printf_info *info, const void *const *args) {
  static int count = 5;
  int w = *((const int *) args[0]);
  printf("boo!");  // direct recursive call
  return fprintf(fp, --count ? "<%W>" : "<%d>", w);  // indirect recursive call
}
int widget_arginfo(const struct printf_info *info, size_t n, int *argtypes) {
  argtypes[0] = PA_INT;
  return 1;
}
int main() {
  register_printf_function('W', widget, widget_arginfo);
  printf("|%W|\n", 42);
}

Wahrscheinlich, weil Sie nicht ausgegeben Schreiben beginnen kann, während ein anderer Anruf zu printf noch ist das Selbstdruck. Das gleiche gilt für Speicherzuweisung und Aufhebung der Zuordnung.

Es ist, weil beiden Werke mit globalen Ressourcen: Heap-Speicherstrukturen und Konsole.

EDIT: der Heap ist nichts anderes als eine Art verknüpft Listenstruktur. Jeder malloc oder free ändert es, so dass mehrere Threads in der gleichen Zeit, das mit dem Schreiben Zugriff darauf wird seine Konsistenz beschädigt werden.

EDIT2: ein weiteres Detail: sie machen einspringenden standardmäßig werden könnten, indem mutexes verwenden. Aber dieser Ansatz ist teuer, und es gibt keine Garantie, dass sie immer in MT-Umgebung verwendet werden.

So gibt es zwei Lösungen: 2-Library-Funktionen zu machen, eine einspringende und eine nicht oder den Mutex Teil den Benutzer überlassen. Sie haben die zweite choosed.

Auch kann es sein, weil die ursprünglichen Versionen dieser Funktionen waren nichtablaufinvarianten, the've so so für Kompatibilität erklärt.

Wenn Sie versuchen, malloc aus zwei separaten Threads aufrufen (es sei denn Sie eine Thread-sichere Version haben, nicht durch C-Standard garantiert), schlechte Dinge passieren, weil es für zwei Threads nur ein Haufen ist. Das Gleiche gilt für printf- das Verhalten ist nicht definiert. Das ist, was sie in Wirklichkeit nichtablaufinvarianten macht.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top