Perché malloc () e printf () sono definiti come non rientranti?

Nei sistemi UNIX sappiamo che malloc() è una funzione non rientranti (chiamata di sistema). Perché?

Analogamente, anche printf() è non-rientranti; perché?

Conosco la definizione di re-entrancy, ma volevo sapere perché si applica a queste funzioni. Cosa impedisce loro di essere rientranti garantiti?

malloc e printf solito utilizzano strutture globali e utilizzano internamente la sincronizzazione basata su lock. Ecco perché non sono rientranti.

La funzione malloc potrebbe essere thread-safe o thread-safe. Entrambi non sono rientranti:

  1. Malloc opera su un heap globale ed è ansible che due diverse invocazioni di malloc che si verificano contemporaneamente restituiscano lo stesso blocco di memoria. (La seconda chiamata malloc deve essere eseguita prima che venga recuperato un indirizzo del blocco, ma il blocco non è contrassegnato come non disponibile). Ciò viola la post-condizione di malloc , quindi questa implementazione non sarebbe rientrante.

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

     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 si verifica quando malloc viene semplicemente chiamato da diversi thread. In effetti, il concetto di rientranza va oltre la sicurezza del thread e richiede anche che le funzioni funzionino correttamente anche se una delle sue invocazioni non termina mai . Questo è fondamentalmente il ragionamento per cui qualsiasi funzione con serrature non sarebbe rientrante.

La funzione printf funziona anche su dati globali. Qualsiasi stream di output utilizza in genere un buffer globale collegato ai dati della risorsa inviati (un buffer per terminale o per un file). Il processo di stampa di solito è una sequenza di copia dei dati da bufferizzare e svuotare il buffer in seguito. Questo buffer dovrebbe essere protetto da blocchi nello stesso modo di malloc . Pertanto, printf è anche non rientranti.

Capiamo cosa intendiamo per ri-entrante . Una funzione di rientro può essere invocata prima che sia terminata un’invocazione precedente. Questo potrebbe accadere se

  • una funzione viene chiamata in un gestore di segnale (o più in generale di Unix qualche gestore di interrupt) per un segnale che è stato generato durante l’esecuzione della funzione
  • una funzione è chiamata ricorsivamente

malloc non è rientrante perché sta gestendo diverse strutture di dati globali che tracciano blocchi di memoria liberi.

printf non è rientranti perché modifica una variabile globale, ovvero il contenuto del file FILE * stout.

Ci sono almeno tre concetti qui, che sono tutti fusi in un linguaggio colloquiale, che potrebbe essere il motivo per cui sei stato confuso.

  • thread-safe
  • sezione critica
  • rientrante

Per prima cosa prendi la più semplice: sia malloc che printf sono thread-safe . Sono stati garantiti per essere thread-safe in Standard C dal 2011, in POSIX dal 2001 e in pratica da molto tempo prima. Ciò significa che è garantito che il seguente programma non si arresti in modo anomalo o non mostri un comportamento errato:

 #include  #include  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 thread diversi contemporaneamente, il risultato è un comportamento indefinito – poiché 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 facoltativamente e con un nome diverso, perché Not Invented Here) come strtok_s .

Ok, ma printf non usa le risorse globali per build anche il suo output? In effetti, cosa significherebbe anche stampare su stdout da due thread contemporaneamente? Questo ci porta al prossimo argomento. Ovviamente printf una sezione critica in qualsiasi programma che la usa. È consentito solo un thread di esecuzione all’interno della sezione critica in una sola volta.

Almeno nei sistemi conformi a POSIX, questo si ottiene facendo in modo che printf cominci con una chiamata a flockfile(stdout) e termini con una chiamata a funlockfile(stdout) , che è fondamentalmente come prendere un mutex globale associato a stdout.

Tuttavia, ogni FILE distinto nel programma può avere il proprio mutex. Ciò significa che un thread può chiamare fprintf(f1,...) nello stesso momento in cui un secondo thread si trova nel mezzo di una chiamata a fprintf(f2,...) . Non ci sono condizioni di gara qui. (Se la tua libc esegue effettivamente queste due chiamate in parallelo è un problema di QoI . In realtà non so cosa faccia glibc.)

Allo stesso modo, malloc è improbabile che sia una sezione critica in qualsiasi sistema moderno, perché i sistemi moderni sono abbastanza intelligenti da mantenere un pool di memoria per ogni thread nel sistema , piuttosto che avere tutti i thread N in un singolo pool. (La chiamata di sistema sbrk sarà probabilmente ancora una sezione critica, ma malloc trascorre molto poco del suo tempo in sbrk . O mmap , o qualsiasi altra cosa che i cool kids usano in questi giorni.)

Okay, quindi cosa significa in realtà re-entrancy ? Fondamentalmente, significa che la funzione può essere tranquillamente chiamata in modo ricorsivo: l’invocazione corrente viene “messa in attesa” mentre viene eseguita una seconda chiamata, e quindi la prima chiamata è ancora in grado di “riprendere da dove era stata interrotta”. (Tecnicamente questo potrebbe non essere dovuto a una chiamata ricorsiva: la prima invocazione potrebbe essere nel thread A, che viene interrotto nel mezzo dal thread B, che effettua la seconda chiamata, ma quello scenario è solo un caso speciale di sicurezza del thread , quindi possiamo dimenticarcene in questo paragrafo).

printfmalloc possono essere chiamati ricorsivamente da un singolo thread, perché sono funzioni foglia (non chiamano se stessi né richiamano alcun codice controllato dall’utente che potrebbe eventualmente effettuare una chiamata ricorsiva). E, come abbiamo visto sopra, sono stati protetti da thread contro le chiamate multi-* re-filed * dal 2001 (usando i lock).

Quindi, chi ti ha detto che printf e malloc non erano rientranti era sbagliato; quello che intendevano dire era probabilmente che entrambi hanno il potenziale per essere sezioni critiche nel tuo programma – colli di bottiglia in cui può passare solo un thread alla volta.


Nota pedante: glibc fornisce un’estensione grazie alla quale printf può essere fatto per chiamare codice utente arbitrario, inclusa la ri-chiamata stessa. Questo è perfettamente sicuro in tutte le sue permutazioni, almeno per quanto riguarda la sicurezza del filo. (Ovviamente apre le porte a folle vulnerabilità formato-stringa.) Ci sono due varianti: register_printf_function (che è documentata e ragionevolmente sana, ma ufficialmente “deprecata”) e register_printf_specifier (che è quasi identica eccetto per un parametro extra non documentato e un totale mancanza di documentazione rivolta all’utente ). Non consiglierei nessuno di loro, e menzionarli qui solo come una parte interessante.

 #include  #include  // 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 è ansible iniziare a scrivere output mentre un’altra chiamata a printf sta ancora stampando da sola. Lo stesso vale per l’allocazione della memoria e la deallocazione.

È perché entrambi funzionano con risorse globali: strutture di memoria heap e console.

EDIT: l’heap non è altro che una sorta di struttura di liste collegate. Ogni malloc o free modifica, quindi avere più thread nello stesso tempo con accesso in scrittura danneggerà la sua consistenza.

EDIT2: un altro dettaglio: potrebbero essere resi rientranti di default usando mutex. Ma questo approccio è costoso e non è garantito che vengano sempre utilizzati nell’ambiente MT.

Quindi ci sono due soluzioni: creare 2 funzioni di libreria, una rientranza e una non o lasciare all’utente la parte mutex. Hanno scelto il secondo.

Inoltre, può essere dovuto al fatto che le versioni originali di queste funzioni non erano rientranti, quindi sono state dichiarate per compatibilità.

Se provi a chiamare malloc da due thread separati (a meno che tu non abbia una versione thread-safe, non garantita dallo standard C), accadono cose brutte, perché c’è solo un heap per due thread. Lo stesso per printf- il comportamento non è definito. Questo è ciò che li rende in realtà non rientranti.