Frage Warum werden malloc () und printf () als nicht-reentrant bezeichnet?


In UNIX-Systemen wissen wir malloc() ist eine nicht-reentrante Funktion (Systemaufruf). Warum das?

Ähnlich, printf() es wird auch gesagt, dass es nicht wiedereinwanderbar ist; Warum?

Ich kenne die Definition von Re-Entrance, aber ich wollte wissen, warum es für diese Funktionen gilt.   Was verhindert, dass sie Wiedereintritt garantiert werden?


37
2017-10-15 10:12


Ursprung


Antworten:


malloc und printf Normalerweise verwenden Sie globale Strukturen und verwenden die sperrbasierte Synchronisation intern. Deshalb sind sie nicht wiedereintretend.

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

  1. Malloc operiert auf einem globalen Heap, und es ist möglich, dass zwei verschiedene Aufrufe von malloc das zur gleichen Zeit passieren, den gleichen Speicherblock zurückgeben. (Der zweite Malloc-Aufruf sollte erfolgen, bevor eine Adresse des Chunks abgerufen wird, der Chunk jedoch nicht als nicht verfügbar markiert ist). Dies verletzt die Nachbedingung von mallocDaher wäre diese Implementierung nicht neu.

  2. Um diesen Effekt zu verhindern, wird eine thread-sichere Implementierung von malloc würde lock-basierte Synchronisation verwenden. Wenn jedoch malloc vom Signal-Handler aufgerufen wird, kann die folgende Situation auftreten:

    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 wird einfach aus verschiedenen Threads aufgerufen. In der Tat geht das Reentrancy-Konzept über die Thread-Sicherheit hinaus und erfordert auch, dass Funktionen ordnungsgemäß funktionieren selbst wenn einer seiner Aufrufe niemals beendet wird. Das ist im Grunde die Begründung, warum eine Funktion mit Sperren nicht wiedereinmalig wäre.

Das printf Funktion funktionierte auch auf globalen Daten. Jeder Ausgabestrom verwendet normalerweise einen globalen Puffer, der an die Ressourcendaten angehängt ist, an die gesendet wird (ein Puffer für das Terminal oder für eine Datei). Der Druckvorgang besteht in der Regel aus einer Folge von Datenkopien, um den Puffer anschließend zu puffern und zu leeren. Dieser Puffer sollte auf die gleiche Weise durch Sperren geschützt werden malloc tut. Deshalb, printf ist auch nicht-reentrant.


52
2017-10-15 10:56



Lass uns verstehen, was wir darunter verstehen Wiedereintritt. Eine Wiedereintrittsfunktion kann aufgerufen werden, bevor ein vorheriger Aufruf beendet wurde. Dies könnte passieren, wenn

  • Eine Funktion wird in einem Signal-Handler (oder allgemeiner als Unix-Interrupt-Handler) für ein Signal aufgerufen, das während der Ausführung der Funktion ausgelöst wurde
  • Eine Funktion wird rekursiv aufgerufen

malloc ist nicht reentrant, da es mehrere globale Datenstrukturen verwaltet, die freie Speicherblöcke verfolgen.

printf ist nicht einspringend, da es eine globale Variable modifiziert, d. h. den Inhalt von FILE * stout.


10
2017-10-15 10:46



Es gibt mindestens drei Konzepte, die alle in der Umgangssprache zusammengefasst sind, weshalb Sie vielleicht verwirrt waren.

  • fadensicher
  • Kritischer Abschnitt
  • Wiedereintritt

Um den einfachsten zuerst zu nehmen: Beide malloc und printf sind fadensicher. Sie sind garantiert seit 2011 in Standard C, seit 2001 in POSIX und seit langem in der Praxis garantiert. Dies bedeutet, dass das folgende Programm garantiert nicht abstürzt oder schlechtes Verhalten zeigt:

#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 nicht threadsicher ist strtok. Wenn du anrufst strtok aus zwei verschiedenen Threads gleichzeitig ist das Ergebnis undefiniertes Verhalten - weil strtok verwendet intern einen statischen Puffer, um den Status zu verfolgen. glibc fügt hinzu strtok_r um dieses Problem zu beheben, und C11 hinzugefügt das gleiche Ding (aber optional und unter einem anderen Namen, weil nicht erfunden hier) strtok_s.

Okay, aber nicht printf Verwenden Sie globale Ressourcen, um seine Ausgabe zu erstellen? In der Tat, was wäre es überhaupt bedeuten von zwei Threads auf stdout drucken gleichzeitig? Das bringt uns zum nächsten Thema. Offensichtlich printf wird ein Kritischer Abschnitt in jedem Programm, das es verwendet. Nur ein Thread der Ausführung darf sich gleichzeitig im kritischen Bereich befinden.

Zumindest in POSIX-konformen Systemen wird dies erreicht printf Beginnen Sie mit einem Anruf an flockfile(stdout) und mit einem Anruf enden funlockfile(stdout), das ist im Grunde wie ein globaler Mutex, der mit stdout assoziiert ist.

Allerdings unterscheiden sich alle FILE im Programm darf ein eigener Mutex sein. Dies bedeutet, dass ein Thread anrufen kann fprintf(f1,...) zur gleichen Zeit, dass ein zweiter Thread in der Mitte eines Anrufs ist fprintf(f2,...). Hier gibt es keine Wettlaufbedingungen. (Ob Ihre libc tatsächlich diese beiden Aufrufe parallel ausführt, ist a QoI Problem. Ich weiß nicht wirklich was Glibc macht.)

Ähnlich, malloc Es ist unwahrscheinlich, dass es ein kritischer Abschnitt in einem modernen System ist, weil moderne Systeme es sind intelligent genug, um für jeden Thread im System einen Speicherpool zu behalten, anstatt alle N Threads um einen einzelnen Pool kämpfen zu lassen. (Das sbrk Systemaufruf wird wohl noch ein kritischer Abschnitt sein, aber malloc verbringt sehr wenig Zeit in sbrk. Oder mmapoder was auch immer die coolen Kinder heutzutage benutzen.)

Okay, also was macht Wiedereintritt eigentlich gemein? Grundsätzlich bedeutet dies, dass die Funktion sicher rekursiv aufgerufen werden kann - der aktuelle Aufruf wird "gehalten", während ein zweiter Aufruf ausgeführt wird, und dann kann der erste Aufruf noch "dort weitermachen, wo er aufgehört hat". (Technisch dies Macht nicht aufgrund eines rekursiven Aufrufs: der erste Aufruf könnte in Thread A sein, der in der Mitte von Thread B unterbrochen wird, der den zweiten Aufruf ausführt. Aber dieses Szenario ist nur ein spezieller Fall von Fadensicherheit, damit wir es in diesem Absatz vergessen können.)

Weder printf Noch malloc kann möglicherweise Sein rekursiv von einem einzigen Thread aufgerufen, weil sie Blattfunktionen sind (sie rufen sich selbst nicht auf und rufen keinen benutzergesteuerten Code auf, der möglicherweise einen rekursiven Aufruf ausführen könnte). Und wie wir oben gesehen haben, waren sie seit 2001 Thread-sicher gegen * Multi-Thread-Reentry-Aufrufe (durch Sperren).

Also, wer auch immer dir das gesagt hat printf und malloc waren nicht wiedereintretend falsch; Was sie sagen wollten, war wahrscheinlich, dass beide das Potenzial haben, zu sein kritische Abschnitte in Ihrem Programm - Engpässe, bei denen nur ein Thread gleichzeitig durchkommen kann.


Pedantische Anmerkung: Glibc bietet eine Erweiterung, mit der printf kann dazu gebracht werden, einen beliebigen Benutzercode aufzurufen, einschließlich eines erneuten Anrufs selbst. Dies ist in allen seinen Permutationen vollkommen sicher - zumindest soweit es die Fadensicherheit betrifft. (Offensichtlich öffnet es die Tür zu absolut wahnsinnig format-string Sicherheitslücken.) Es gibt zwei Varianten: register_printf_function (das ist dokumentiert und vernünftig gesund, aber offiziell "veraltet") und register_printf_specifier (welches ist fast identisch bis auf einen zusätzlichen undokumentierten Parameter und a völliger Mangel an Dokumentation für den Benutzer). Ich würde keine von beiden 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);
}

3
2017-11-11 20:10



Höchstwahrscheinlich, weil Sie nicht mit dem Schreiben der Ausgabe beginnen können, während ein anderer Aufruf von printf noch selbst gedruckt wird. Dasselbe gilt für die Speicherzuweisung und -freigabe.


1
2017-10-15 10:16



Das liegt daran, dass beide mit globalen Ressourcen arbeiten: Heap-Speicherstrukturen und Konsole.

EDIT: der Haufen ist nichts anderes als eine Art verknüpfte Listenstruktur. Jeder malloc oder free ändert es, so dass mehrere Threads in der gleichen Zeit mit Schreibzugriff darauf seine Konsistenz beschädigen.

EDIT2: ein weiteres Detail: Sie könnten per Mutex standardmäßig in den Reentrant-Modus versetzt werden. Dieser Ansatz ist jedoch kostspielig, und es gibt keine Garantie, dass sie immer in der MT-Umgebung verwendet werden.

Also gibt es zwei Lösungen: um 2 Bibliotheksfunktionen zu machen, einen Reentrant und einen nicht oder den Mutex-Teil dem Benutzer zu überlassen. Sie haben die zweite gewählt.

Es kann auch daran liegen, dass die ursprünglichen Versionen dieser Funktionen nicht reentrant waren, weshalb sie aus Kompatibilitätsgründen deklariert wurden.


-2
2017-10-15 10:17



Wenn Sie versuchen, malloc aus zwei separaten Threads aufzurufen (es sei denn, Sie haben eine threadsichere Version, die nicht vom C-Standard garantiert wird), treten schlimme Dinge auf, weil es nur einen Heap für zwei Threads gibt. Gleiches gilt für printf- das Verhalten ist nicht definiert. Das macht sie in Wirklichkeit nicht wiedereinwanderbar.


-4
2017-10-15 10:20