Domanda

In sistemi UNIX sappiamo malloc() è una funzione non rientrante (chiamata di sistema). Perché?

Analogamente, anche printf() si dice essere non rientrante; perché?

So che la definizione di re-entrancy, ma volevo sapere perché si applica a queste funzioni.   Che cosa impedisce loro di essere garantiti da rientro?

È stato utile?

Soluzione

malloc e printf solito usano strutture globali e sincronizzazione basati serratura impiego internamente. È per questo che non siamo rientrante.

La funzione malloc potrebbe o essere thread-safe o filo pericoloso. Entrambi non sono da rientro:

  1. Malloc opera su un cumulo globale, ed è possibile che due differenti invocazioni di malloc che avvengono contemporaneamente, restituiscono lo stesso blocco di memoria. (La chiamata 2 ° malloc dovrebbe avvenire prima di un indirizzo del pezzo è inverosimile, ma il pezzo non è contrassegnato come non disponibile). Questo viola la postcondizione di malloc, in modo da questa implementazione non sarebbe rientrante.

  2. Per evitare questo effetto, un'implementazione thread-safe di malloc userebbe sincronizzazione lock-based. Tuttavia, se malloc viene chiamato dal gestore di segnale, la seguente situazione può accadere:

    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
    

    Questa situazione non accadrà quando malloc viene semplicemente chiamato da diversi thread. Infatti, il concetto reentrancy va oltre thread-sicurezza e richiede anche funzioni per funzionare correttamente anche se uno dei suoi invocazione non termina . Questo è fondamentalmente il ragionamento per cui qualsiasi funzione con le serrature sarebbe non rientrante.

La funzione printf anche operato dati globali. Qualsiasi flusso di uscita di solito impiega un buffer globale attaccato ai dati di risorse vengono inviati a (un buffer per terminale, o per un file). Il processo di stampa è di solito una sequenza di copiare i dati per tamponare e vampate di calore il buffer dopo. Questo buffer deve essere protetto da blocchi nello stesso modo malloc fa. Pertanto, printf è anche non-rientrante.

Altri suggerimenti

Cerchiamo di capire cosa intendiamo per rientrante . Una funzione rientranza può essere richiamato prima una chiamata precedente è terminata. Questo potrebbe accadere se

  • una funzione viene richiamata in un gestore di segnale (o più in generale di Unix alcuni gestore di interrupt) per un segnale che è stato sollevato durante l'esecuzione della funzione
  • una funzione viene chiamata in modo ricorsivo

malloc non è rientrante perché gestisce diverse strutture dati globali che tengono traccia dei blocchi di memoria liberi.

printf non è rientrante perché modifica una variabile globale cioè il contenuto del file * robusto.

Ci sono almeno tre concetti qui, che sono tutti conflated nel linguaggio colloquiale, che potrebbe essere il motivo per cui sono stati confusi.

  • thread-safe
  • sezione critica
  • rientrante

Per fare il più facile primo: Sia malloc e printf sono thread-safe . Sono stati garantiti come thread-safe in standard C dal 2011, in POSIX dal 2001, e in pratica da molto tempo prima che. Ciò significa che il seguente programma è garantito a non cadere o esporre il cattivo comportamento:

#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);
}

Un esempio di una funzione che è non thread-safe è strtok. Se si chiama strtok da due fili diversi simultaneamente, il risultato è un comportamento indefinito - perché strtok utilizza internamente un buffer statico per tenere traccia del suo stato. glibc aggiunge strtok_r per risolvere questo problema, e C11 ha aggiunto la stessa cosa (ma opzionalmente e con un nome diverso, perché non inventato qui) come strtok_s.

D'accordo, ma lo fa utilizzare le risorse non printf globali per costruire la sua uscita, troppo? In realtà, ciò che sarebbe anche media per stampare su stdout da due thread contemporaneamente? che ci porta al prossimo argomento. Ovviamente printf sta per essere un sezione critica in qualsiasi programma che l'utilizza. solo thread di esecuzione è permesso di essere all'interno della sezione critica contemporaneamente.

Almeno in sistemi POSIX-compliant, ciò si ottiene facendo printf inizia con una chiamata a flockfile(stdout) e alla fine con una chiamata a funlockfile(stdout), che è fondamentalmente come prendere un mutex globale associato stdout.

Tuttavia, ogni FILE distinta nel programma è permesso di avere una propria mutex. Ciò significa che un thread può chiamare fprintf(f1,...) allo stesso tempo che un secondo filo è nel mezzo di una chiamata a fprintf(f2,...). Non c'è nessuna condizione gara qui. (Se il tuo libc esegue effettivamente quei due chiamate in parallelo è un QoI problema. io in realtà non so cosa glibc fa.)

Allo stesso modo, malloc è improbabile che sia una sezione critica in qualsiasi sistema moderno, perché i sistemi moderni sono abbastanza intelligente per mantenere un pool di memoria per ogni filo nel sistema , piuttosto che avere tutte le discussioni N combattono un singolo pool. (La chiamata di sistema sbrk sarà ancora probabilmente una sezione critica, ma malloc spende molto poco del suo tempo in sbrk. Oppure mmap, o qualunque sia il cool kids sta utilizzando in questi giorni.)

Ok, re-entrancy in realtà media in pratica, ciò significa che la funzione può tranquillamente essere chiamato ricorsivamente -? l'invocazione corrente viene "messa in attesa", mentre un secondo run invocazione, e poi la prima invocazione è ancora in grado di "raccogliere dove si era interrotto." (Tecnicamente questo potrebbe non essere a causa di una chiamata ricorsiva: la prima invocazione potrebbe essere nella Discussione A, che viene interrotto al centro da filo B, il che rende la seconda invocazione Ma questo scenario è solo una. caso speciale di thread-sicurezza , in modo che possiamo dimenticare in questo paragrafo.)

printfmalloc possono eventualmente essere chiamato ricorsivamente da un unico filo, perché sono funzioni foglia (che non si definiscono né chiamano a qualsiasi utente-controllCodice ndr che potrebbe fare una chiamata ricorsiva). E, come abbiamo visto sopra, sono stati contro * * multi-filettati rientranti chiamate thread-safe dal 2001 (utilizzando serrature).

Quindi, chi ti ha detto che printf e malloc erano non rientrante era sbagliato; che cosa hanno significato per dire che era probabilmente entrambi hanno il potenziale per essere sezioni critiche nel programma -. colli di bottiglia dove solo un thread può ottenere attraverso alla volta


Pedantic nota: glibc non fornisce un interno che printf può essere fatto per chiamare codice utente arbitrario, compresa una nuova chiamata stessa. Questo è perfettamente sicuro in tutte le sue permutazioni - almeno per quanto filo di sicurezza è interessato. (Ovviamente si apre la porta assolutamente a insano vulnerabilità format-string.) Ci sono due varianti: register_printf_function (che è documentata e ragionevolmente sano di mente, ma ufficialmente "deprecato") e register_printf_specifier (che è quasi identiche tranne che per un parametro non documentato in più e un totale mancanza di documentazione per l'utente rivolto ). Io non consiglierei uno di loro, e li menzionare qui solo come un interessante a parte.

#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);
}

Molto probabilmente perché non è possibile iniziare a scrivere di uscita, mentre un altro chiamata a printf è ancora la stampa è di per sé. Lo stesso vale per l'allocazione della memoria e la deallocazione.

E 'perché entrambe le opere con le risorse globali: strutture di memoria heap e console.

EDIT: il cumulo non è altro che una struttura di tipo lista collegata. Ogni malloc o free modifica, in modo da avere diversi thread nello stesso tempo con la scrittura accesso ad esso può danneggiare la sua consistenza.

EDIT2: un altro particolare: essi potrebbero essere fatte da rientro di default tramite mutex. Ma questo approccio è costoso, e non c'è garanty che saranno sempre utilizzati in ambiente MT.

Quindi, ci sono due soluzioni: per fare 2 funzioni di libreria, uno rientrante e non si o lasciare la parte mutex per l'utente. Hanno stati scelti la seconda.

Inoltre, può essere perché le versioni originali di queste funzioni erano non rientrante, quindi the've stati dichiarati in modo per la compatibilità.

Se si tenta di chiamare malloc da due thread separati (a meno che non si dispone di una versione thread-safe, non garantita da standard C), succedono cose brutte, perché c'è solo un heap per due thread. Lo stesso vale per printf- il comportamento è indefinito. Questo è ciò che li rende in realtà non rientrante.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top