PDA

View Full Version : [C] free()


Oceans11
23-01-2014, 11:48
In un recente thread ho scritto:
Sono memory leak innocui per programmi tipo questo che non è un demone ne un server. Ricordo che all'uscita del programma si occupa il s.o. di liberare la memoria allocata.

Innanzitutto ho scoperto che non tutti i S/O si preoccupano di liberare la memoria allocata dai programmi (ma quali sono lo ignoro)
Poi, siccome l'argomento mi sembra interessante e io sono tutt'altro che bravo nello spiegarmi nonchè autorevole, volevo linkare questi due articoli che trattano l'argomento.

articolo #1 (http://c-faq.com/malloc/freeb4exit2.html)
articolo #2 (http://c-faq.com/malloc/free2os.es.html)

Senza far partire flame, mi piacerebbe conoscere l'opinione dei più "navigati" e non, l'importante sono le motivazioni della scelta.

Ciao

Daniels118
23-01-2014, 12:36
Io la vedo in questo modo: bisogna sempre essere precisi.
Che il sistema operativo in uso liberi la memoria o meno, bisogna sempre liberare ciò che si è allocato. E' una pratica che aiuta ad assumere quest'atteggiamento sempre, così si evita di "dimenticarsi" quando ce n'è veramente bisogno.

Poi, che il SO debba liberare la memoria è un dovere: il SO è uno, i programmi sono tanti, ne nasce uno nuovo ogni giorno, è matematico che i programmi abbiano qualche bug. I sistemi operativi hanno una vita molto lunga e tanto tempo per migliorarsi: se il SO può risolvere un problema comune deve farlo. Se non lo fa e devo implementare tutto io, tanto vale fare a meno del sistema operativo (ovviamente questa è un'esagerazione, ma è giusto per rendere l'idea).

Oceans11
23-01-2014, 12:41
Io la vedo in questo modo: bisogna sempre essere precisi.
Che il sistema operativo in uso liberi la memoria o meno, bisogna sempre liberare ciò che si è allocato. E' una pratica che aiuta ad assumere quest'atteggiamento sempre, così si evita di "dimenticarsi" quando ce n'è veramente bisogno.
porsi il problema basterebbe come atteggiamento, no?
così si evitano chiamate a funzioni inutili (quando a breve il programma termina) e il rischio di chiamare la free() su, ad esempio, un puntatore a memoria già liberata, causando gravi problemi.


Poi, che il SO debba liberare la memoria è un dovere: il SO è uno, i programmi sono tanti, ne nasce uno nuovo ogni giorno, è matematico che i programmi abbiano qualche bug. I sistemi operativi hanno una vita molto lunga e tanto tempo per migliorarsi: se il SO può risolvere un problema comune deve farlo. Se non lo fa e devo implementare tutto io, tanto vale fare a meno del sistema operativo (ovviamente questa è un'esagerazione, ma è giusto per rendere l'idea).
tanto vale fare a meno di "quel" sistema operativo, non è affatto un'esagerazione. Un bug del sistema operativo è molto più grave di un bug di un programma.

ps: grazie per aver detto la tua.

sottovento
23-01-2014, 13:07
Innanzitutto ho scoperto che non tutti i S/O si preoccupano di liberare la memoria allocata dai programmi (ma quali sono lo ignoro)

Fra quelli che conosco, VxWorks, pSOS+, VMEExec. Tutti a seconda della configurazione e del tipo di allocazione che vai a fare.
Nei sistemi embedded non e' poi cosi' strano, e questo non significa che il sistema operativo non sia utile, anzi.


Ciao
Ciao

Oceans11
23-01-2014, 13:35
Fra quelli che conosco, VxWorks, pSOS+, VMEExec. Tutti a seconda della configurazione e del tipo di allocazione che vai a fare.
Nei sistemi embedded non e' poi cosi' strano, e questo non significa che il sistema operativo non sia utile, anzi.

ecco i sistemi embedded non li stavo proprio a considerare.
Ma a pensarci su ora, bisognerebbe vedere se si hanno a disposizione tali funzioni di allocazione dinamica, no? (non so nulla a riguardo)
Non ho capito "il tipo di allocazione che vai a fare", (sto supponendo che l'heap sia comunque disponibile)

sottovento
23-01-2014, 13:51
ecco i sistemi embedded non li stavo proprio a considerare.
Ma a pensarci su ora, bisognerebbe vedere se si hanno a disposizione tali funzioni di allocazione dinamica, no? (non so nulla a riguardo)
Non ho capito "il tipo di allocazione che vai a fare", (sto supponendo che l'heap sia comunque disponibile)

VxWorks puo' girare su CPU senza MMU, ed i "processi" condividono le aree globali. Addirittura le variabili globali! Pertanto si puo' dichiarare una variabile in un "processo" ed utilizzarla in un altro.
pSOS+, VMEExec possono allocare delle regioni di memoria, con un'API del tipo allocate_region(nome, quantita',....).
Essendo sistemi operativi HARD real-time, le regioni di memoria hanno dimensioni fissate al momento del build del kernel e vengono allocate in first fit. Ne risulta che anche l'allocazione ha tempi deterministici. In altri casi, anche durante l'allocazione della memoria c'e' un time out, allo scadere del quale l'allocazione fallisce.

(Questo e' un ulteriore motivo per arrabbiarsi tanto per il "blind faith" (http://en.wikipedia.org/wiki/Blind_faith_%28computer_programming%29). Il 90% degli esempi fatti nelle scuole di tutti i livelli danno sempre per scontato che l'allocazione vada bene, provocando la mia ira :D )

Una volta create, le regioni sono visibili a tutti e le si possono identificare tramite il nome assegnato.

Un altro "trucco" che che viene (veniva?) spesso usato con questi sistemi, specialmente in ambito VME era quello di inserire una scheda di memoria sul bus (VME appunto) e di "smapparla" dal sistema operativo. In questo modo l'area di memoria in questione risulta non gestito da nessun sistema operativo/CPU che si affaccia sul bus.
Essendo l'indirizzo noto, l'area di memoria viene spesso usata come "database di processo"

Oceans11
23-01-2014, 14:58
VxWorks puo' girare su CPU senza MMU, ed i "processi" condividono le aree globali. Addirittura le variabili globali! Pertanto si puo' dichiarare una variabile in un "processo" ed utilizzarla in un altro.

sarà meglio che ci lavori su una singola persona allora :D
potente, ma immagino i bordelli


(Questo e' un ulteriore motivo per arrabbiarsi tanto per il "blind faith" (http://en.wikipedia.org/wiki/Blind_faith_%28computer_programming%29). Il 90% degli esempi fatti nelle scuole di tutti i livelli danno sempre per scontato che l'allocazione vada bene, provocando la mia ira :D )
dalla malloc(3) man page:

NOTES
By default, Linux follows an optimistic memory allocation strategy.
This means that when malloc() returns non-NULL there is no guarantee
that the memory really is available.

lo so bene :rolleyes: e poi "optimistic" dice tutto

tutto il resto è molto interessante, ma non riesco ad apprezzare a pieno per colpa di mancanza di conoscenze.

sottovento
23-01-2014, 15:33
Il tuo post mi ha fatto sollevare un sopracciglio. Mi riferivo semplicemente al controllo del ritorno a NULL nel caso della malloc() o alla gestione dell'eccezione std::bad_alloc per la new.

Sono andato a controllare, ed in effetti hai ragione. Pero' in una versione delle man pages presenti in rete c'era scritto:

By default, Linux follows an optimistic memory allocation strategy. This means that when malloc() returns non-NULL there is no guarantee that the memory really is available. This is a really bad bug. In case it turns out that the system is out of memory, one or more processes will be killed by the infamous OOM killer. In case Linux is employed under circumstances where it would be less desirable to suddenly lose some randomly picked processes, and moreover the kernel version is sufficiently recent, one can switch off this overcommitting behavior using a command like:
...


sembrerebbe un bug, dunque. E la cosa spaventa parecchio.
Non mi riferivo certamente a questo, ma al piu' classico dei blind faith: per esempio, se apri uno qualsiasi dei thread di questa pagina che parla di liste linkate, vedrai malloc() dappertutto senza che ci sia mai un controllo che la funzione abbia avuto successo.

Quello che hai trovato h una portata ben diversa e non ne so assolutamente nulla!
Ne sai qualcosa di piu? Qualcun altro ne sa di piu'?
Come faccio ad esser veramente sicuro che l'allocazione sia andata a buon fine?

Daniels118
23-01-2014, 15:43
Diciamo che i programmi che vedi in giro sul forum servono più che altro per dimostrare il funzionamento delle strutture dati astratte, che la gestione delle eccezioni. Comunque questo bug è disarmante, non ci sono parole...

sottovento
23-01-2014, 16:13
Diciamo che i programmi che vedi in giro sul forum servono più che altro per dimostrare il funzionamento delle strutture dati astratte, che la gestione delle eccezioni. Comunque questo bug è disarmante, non ci sono parole...

Hai ragione. E' cosi' disarmante che non mi sembra vero.
Anzi, cosi' disarmante che non posso credere che sia caratteristico di Linux e nessuno abbia mai detto o fatto nulla!
Immagino che, nonostante quanto le man dicano, se un simile bug esiste non puo' che essere presente solo in alcune distribuzioni, o versioni delle stesse. Altrimenti non avrebbe senso usarlo, no?
Penso che sia necessario approfondire il problema...

Per quanto riguarda i programmi sul forum: hai ragione un'altra volta.
Tuttavia, penso che sia necessario che i docenti non si limitino a spiegare la parte algoritmica, quella "importante", ma che si debba chiarire che un'applicazione e' considerata non funzionante anche se mancano questi dettagli, che di teorico non hanno nulla, ma che sono vitali per il corretto e robusto funzionamento.

Oceans11
23-01-2014, 17:52
Il tuo post mi ha fatto sollevare un sopracciglio. Mi riferivo semplicemente al controllo del ritorno a NULL nel caso della malloc() o alla gestione dell'eccezione std::bad_alloc per la new.

Sono andato a controllare, ed in effetti hai ragione. Pero' in una versione delle man pages presenti in rete c'era scritto:


sembrerebbe un bug, dunque. E la cosa spaventa parecchio.
Non mi riferivo certamente a questo, ma al piu' classico dei blind faith: per esempio, se apri uno qualsiasi dei thread di questa pagina che parla di liste linkate, vedrai malloc() dappertutto senza che ci sia mai un controllo che la funzione abbia avuto successo.

Quello che hai trovato h una portata ben diversa e non ne so assolutamente nulla!
Ne sai qualcosa di piu? Qualcun altro ne sa di piu'?
Come faccio ad esser veramente sicuro che l'allocazione sia andata a buon fine?

Non volevo far agitare il tuo sopracciglio.
Non so perchè ho pensato subito alla allocazione fallita piuttosto che al fallimento della chiamata a funzione.

Ad ogni modo, leggo che optimistic sta a causa della strategia di overcommit dei processi ossia i processi chiedono al s/o più memoria di quanta gliene potrebbe effettivamente servire.

Dalla documentazione del kernel Documentation/vm/overcommit-accounting:
obvious overcommits of address space are refused

Ma se serve più memoria ecco che viene lanciato l' OOM killer (Out-Of-Memory).
Capisco che crollino le certezze di un utente che rischia di vedere il suo processo ammazzato di punto in bianco secondo chissà quali strane euristiche (da qualche parte ho letto infatti che non viene necessariamente killato un processo pieno zeppo di memory leak) ma stiamo parlando di un qualcosa che non deve accadere tutti i giorni.

Voglio far notare che la swap è considerata (in quest'ambito) memoria.
Quindi ce ne passa (almeno in ambito desktop) prima di occupare tutta la ram e tutta la swap per finire OOM.

Nell'89, quando il mio nuovissimo pc aveva 1MB di ram, forse il problema era maggiormente pressante per i programmatori :D

Oceans11
23-01-2014, 17:58
Nel frattempo ho trovato questo articolo (http://lwn.net/Articles/317814/). Quoto la parte relativa ai sistemi embedded.

Low Memory in Embedded Systems
The Android developers required a greater degree of control over the low memory situation because the OOM killer does not kick in till late in the low memory situation, i.e. till all the cache is emptied. Android wanted a solution which would start early while the free memory is being depleted. So they introduced the "lowmemory" driver, which has multiple thresholds of low memory. In a low-memory situation, when the first thresholds are met, background processes are notified of the problem. They do not exit, but, instead, save their state. This affects the latency when switching applications, because the application has to reload on activation. On further pressure, the lowmemory killer kills the non-critical background processes whose state had been saved in the previous threshold and, finally, the foreground applications.

Keeping multiple low memory triggers gives the processes enough time to free memory from their caches because in an OOM situation, user-space processes may not be able to run at all. All it takes is a single allocation from the kernel's internal structures, or a page fault to make the system run out of memory. An earlier notification of a low-memory situation could avoid the OOM situation with a little help from the user space applications which respond to low memory notifications.

Killing processes based on kernel heuristics is not an optimal solution, and these new initiatives of offering better control to the user in selecting the process to be the sacrificial lamb are steps to a robust design to give more control to the user. However, it may take some time to come to a consensus on a final control solution.

sottovento
24-01-2014, 09:45
Nel frattempo ho trovato questo articolo (http://lwn.net/Articles/317814/). Quoto la parte relativa ai sistemi embedded.

Wow! E' davvero interessante. Grazie per le info, cerchero' di approfondire il problema

Oceans11
24-01-2014, 13:09
Stavo facendo qualche prova per vedere il comportamento e le soglie sul mio sistema:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define SIZE (10*1024*1024)

int main() {
int i = 0;
printf("page size=%d\n", getpagesize());
while(1) {
char *mem = malloc(SIZE);
printf("\rAllocation #%d", ++i);
if(mem == NULL) {
printf("Out of memory.\n");
fflush(stdout);
exit(1);
}
}

return 0;
}


kernel: 3.12.8 SMP PREEMPT x86_64
swap: disabled
Memory: 7872048K/8082980K available (5116K kernel code, 807K rwdata, 1628K rodata, 1144K init, 1288K bss, 210932K reserved)
/proc/sys/vm/overcommit_ratio = 50
/proc/sys/vm/overcommit_memory = 0

Mi aspettavo di vedere crashare il sistema (overcommit_memory a 0)
mentre invece il programma termina correttamente (intendo dire che mem == NULL)
allora aggiungo il contatore per vedere quanta memoria alloca prima di terminare: si ferma a 65515! sempre a 65515.
Infine aggiungo la chiamata a getpagesize(), non sicuro di quanto siano grandi le pagine per un 64bit: 4k è il risultato.

Assolutamente strano!
Facendo un paio di conti a braccio, ho circa 7.5GB di memoria disponibile in userspace, allocando 10 MB alla volta mi aspettavo di terminare la memoria in 750 chiamate a malloc, senza considerare l'overcommit.
Con quest'ultimo, essendo la ratio a 50, dovrebbe terminare a total_memory+total_memory*0.5+swap_space = 7.5+3.7+0 = 11GB circa, quindi circa 1100 chiamate.
Stranamente, cambiando SIZE il risultato è sempre 65515.
E tanto per togliermi i dubbi sulla page size, 65515 * 4k fa tipo 256MB, non diversi GB.

Qualcuno mi dice dove sbaglio a fare i conti?
O del perchè il programma non sembra mostare il comportamento dichiarato nelle pagine di manuale di malloc?

bancodeipugni
24-01-2014, 14:24
in effetti malloc() come open() e fopen() andrebbe sempre controllata a scanso di cantonate oltre che castata (castata, non castrata:D )

Oceans11
24-01-2014, 14:54
in effetti malloc() come open() e fopen() andrebbe sempre controllata a scanso di cantonate oltre che castata (castata, non castrata:D )

No, castare mai!
link (http://c-faq.com/malloc/mallocnocast.html)

bancodeipugni
24-01-2014, 21:44
castare malloc intendo non open

Oceans11
24-01-2014, 23:36
castare malloc intendo non open

anch'io intendo malloc. Hai visto il link che ho postato?

Oceans11
25-01-2014, 08:36
la spiegazione data in quel link è praticamente senza senso..

per amore della chiarezza: link (http://stackoverflow.com/a/605858)

Oceans11
25-01-2014, 12:26
non ricordarsi di importare stdlib.h non mi sembra meno grave del credere che si stia programmando in C quando la realtà è diversa.
_Personalmente_ non vedo perchè aggiungere codice inutile (il cast) e che oltretutto rende lo stesso meno leggibile.
Poi questi sono gusti.

Sulla free e sul fallimento della malloc che mi dite?

Tommo
25-01-2014, 19:20
in effetti malloc() come open() e fopen() andrebbe sempre controllata a scanso di cantonate oltre che castata (castata, non castrata:D )

Onestamente controllare malloc() e' al limite della paranoia...

-sia iOS che Android hanno una gestione estremamente aggressiva della memoria e il malloc che fa traboccare il vaso non ritorna e causa la terminazione del programma.
Il programma puo' anche essere terminato a necessita' dall'OS molto prima che succeda, e in realta' quello che succede e' quasi sempre questo.

-su windows da XP in poi non e' possibile ottenere null a meno di non superare la dimensione massima dell'allocazione, tutto il resto va in paging... ok, a un certo punto crasha, ma parliamo di cosi' tanta memoria allocata e usata tutta assieme che non sara' certo un if a salvare il programma (e il sistema operativo)

-su Mac idem

-su Linux idem e l'if e' anche inutile di fronte a quel bug

-su console la dimensione e' costante e se finisci la memoria hai fatto tu male i conti e non dovrebbe mai accadere, wrappare malloc in una macro che asserta il puntatore e' piu' che sufficiente, quando proprio non c'e' un malloc custom

In sostanza ha senso controllare in alcuni ambiti estremamente ristretti tipo embedded, ma nel caso generale non ha veramente senso sporcare il codice tentando di salvare una situazione che in ogni caso e' compromessa e puo' solo essere il risultato di un bug parecchio brutto.

WarDuck
25-01-2014, 19:51
Stavo facendo qualche prova per vedere il comportamento e le soglie sul mio sistema:

Assolutamente strano!
Facendo un paio di conti a braccio, ho circa 7.5GB di memoria disponibile in userspace, allocando 10 MB alla volta mi aspettavo di terminare la memoria in 750 chiamate a malloc, senza considerare l'overcommit.
Con quest'ultimo, essendo la ratio a 50, dovrebbe terminare a total_memory+total_memory*0.5+swap_space = 7.5+3.7+0 = 11GB circa, quindi circa 1100 chiamate.
Stranamente, cambiando SIZE il risultato è sempre 65515.
E tanto per togliermi i dubbi sulla page size, 65515 * 4k fa tipo 256MB, non diversi GB.

Qualcuno mi dice dove sbaglio a fare i conti?
O del perchè il programma non sembra mostare il comportamento dichiarato nelle pagine di manuale di malloc?

Penso sia dovuto ad una strategia opportunistica nella scrittura delle pagine di memoria (una sorta di copy-on-write).

Cioè una pagina viene effettivamente consumata se tu ci scrivi dentro qualcosa, e nel tuo codice questo non avviene mai.

Probabilmente quando tu fai una malloc il sistema è abbastanza intelligente da non consumare memoria fin tanto che tu non ci scrivi dentro.

Fai una prova a scriverci qualcosa dentro (anche un solo carattere), vedi se cambia qualcosa.

Oceans11
25-01-2014, 23:21
Penso sia dovuto ad una strategia opportunistica nella scrittura delle pagine di memoria (una sorta di copy-on-write).

Cioè una pagina viene effettivamente consumata se tu ci scrivi dentro qualcosa, e nel tuo codice questo non avviene mai.

Probabilmente quando tu fai una malloc il sistema è abbastanza intelligente da non consumare memoria fin tanto che tu non ci scrivi dentro.

Fai una prova a scriverci qualcosa dentro (anche un solo carattere), vedi se cambia qualcosa.

Il cow me l'ero proprio dimenticato, ho fatto il test che ti dà perfettamente ragione: Allocation [1] 899 killed ./test

su tutte le codebase in ambito industriale in cui ho lavorato ho sempre visto il void* castato esplicitamente al tipo di destinazione, per la suddetta ragione. Se non casti, il codice non passa la code review, quindi non sono semplicemente gusti.

Programmo in C per diletto, quindi non so nulla di code review e di ambito industriale.
Chiarisco che ho detto che il cast "aggiunge codice" (e da qui a mio parere la diminuzione di leggibilità) "inutile" visto che è prevista dal linguaggio la promozione del tipo void* in qualsiasi altro tipo (il cast è implicito).
Quindi chiedo, come mai in questi ambiti ci sono tali richieste?

Oceans11
26-01-2014, 12:08
perché non sei il solo a lavorare, a debuggare e mantenere parti di codice, e tutto quello che può aumentarne la leggibilità a la manutenibilità è fondamentale. in un altro post qui ho letto una cosa del tipo "l'importante è che funzioni"... quando hai a che fare con progetti veri il fatto che qualcosa funzioni è dato per scontato, l'importante è avere codice che sia facilmente manutenibile indipendente da chi ci sta lavorando sopra...

E su questo siamo d'accordo, il problema qua sembra essere la definizione di "leggibile" :)
Fammi capire, ci deve essere qualcuno che stabilisca alcuni standard tra tutti i programmatori, no?

mone.java
26-01-2014, 12:35
E su questo siamo d'accordo, il problema qua sembra essere la definizione di "leggibile" :)
Fammi capire, ci deve essere qualcuno che stabilisca alcuni standard tra tutti i programmatori, no?

penso che tra almeno quelli di un azienda si. Se io fossi un team leader imporrei un certo stile di programmazione per tutti (ovviamente discusso con il team stesso).. Come la nomenclatura delle variabli/funzioni/classi (nel caso il linguaggio abbia le classi)...
Purtroppo non sono ancora un team leader :fagiano:

mone.java
26-01-2014, 12:37
ho letto una cosa del tipo "l'importante è che funzioni"...

I soggetti di questo tipo li chiuderei in galera e getterei la chiave onde evitare che inquinino il mondo con il codice.