Wyciek pamięci

ceer

Wyciek pamięci (ang. memory leak) - utrata kontaktu z pewnym obszarem zarezerwowanej pamięci.
Kod programu, który powoduje wycieki pamięci, jest kodem błędnym.

<font size="1">źródło: Wikipedia</span></dfn>
1 Opis zjawiska
     1.1 Śledzenie wycieków pamięci
          1.1.1 Narzędzia wspomagające śledzenie wycieków pamieci
          1.1.2 Użycie dyrektyw preprocesora

Opis zjawiska

Zjawisko wycieku pamięci szczególnie znane jest osobom programującym w językach nie posiadających odśmiecania/śmieciarza (ang. Garbage Collector), takich jak [[C|C/C++]]. Na pozór, nazwa tego zjawiska niejako odnosi się do wadliwego działania pamięci, w rzeczywistości jednak winę ponosi niewłaściwie napisany kod. Aby zobrazować na czym polega cały problem, warto przeanalizować poniższy przykład:

Przykład programu powodującego wyciek pamięci

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
  int **p;  /* Deklarujemy wskaźnik na wskaźnik na zmienną typu całkowitego do obszaru pamięci */
  p = (int**) malloc ( sizeof(int*) );  /* Rezerwujemy miejsce na 1 element typu wskaźnik na int pod tym wskaźnikiem */
  *p = (int*) malloc ( sizeof(int) );  /* Rezerwujemy miejsce na 1 element typu int pod wskaźnikiem na wskaźnik */
  free(p);  /* Zwalniamy obszar pamięci, wskazywany przez wskaźnik p */
  return 0;
}
Na początku została zaalokowana pamięć pod wskaźnik <var>p</var>, a następnie pod [[C/Wskaźniki|wskaźnik]] na wskaźnik <var>*p</var>. Po czym, za pomocą funkcji [[C/free]] został zwolniony obszar pamięci wskazywany przez wskaźnik.
Na pozór wszystko działa jak należy, a kompilacja nie wykazuje żadnych błędów. W rzeczywistości, każde wywołanie powyższego programu spowoduje wyciek 4 bajtów (bo tyle zazwyczaj wynosi rozmiar zmiennej typu [[C/int]]) pamięci. Nietrudno zauważyć dlaczego tak się dzieje. Wszystko spowodowane jest tym, że pamięć alokowana była dwa razy, a funkcja dealokująca pamięć [[C/free]] została wywołana tylko raz. 

Ponieważ język C pozbawiony jest tzw. "odśmiecacza", dlatego każdemu wywołaniu jednej z funkcji alokujących pamięć, takich jak: Malloc, Calloc lub Realloc, musi odpowiadać wywołanie funkcji Free.
Cały błąd w powyższym kodzie polega wiec na tym, że zwolniony został wskaźnik p, ale nie został zwolniony wskaźnik na ten wskaźnik. Problem jest o tyle skomplikowany, że po zwolnieniu wskaźnika p, nie mamy dostępu do wskaźnika *p, więc nie możemy zwolnić obszaru pamięci przezeń wskazywanego.

Poniższy kod został pozbawiony tego błędu:

Przykład programu potencjalnie pozbawionego wycieku pamięci

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
  int **p;  /* Deklarujemy wskaźnik na wskaźnik na zmienną typu całkowitego do obszaru pamięci */
  p = (int**) malloc ( sizeof(int*) );  /* Rezerwujemy miejsce na 1 element typu wskaźnik na int pod tym wskaźnikiem */
  *p = (int*) malloc ( sizeof(int) );  /* Rezerwujemy miejsce na 1 element typu int pod wskaźnikiem na wskaźnik */
  free(*p);  /* Zwalniamy obszar pamięci, wskazywany przez wskaźnik na wskaźnik p */
  free(p);  /* Zwalniamy obszar pamięci, wskazywany przez wskaźnik p */
  return 0;
}

Warto zauważyć jedną z cech dynamicznej alokacji pamięci:
Bardziej skomplikowane deklaracje, jak np. alokowanie pamięci pod wskaźnik na wskaźnik, albo pod wskaźnik na strukturę, w której znajdują się wskaźniki, zazwyczaj wiąże się ze zwalnianiem poszczególnych elementów w odwrotnej kolejności, niż zostały one zaalokowane.

Trzeba zwrócić uwagę na to, jakim problemem może być wyciek pamięci w programie, który dłuższy czas działa w systemie, np. serwerze, czy demonie. W takim wypadku program zajmuje coraz większą ilość pamięci, której nie może wykorzystać, ani tym bardziej zwolnić. Sam wyciek może nie tylko prowadzić do spadku wydajności, a także, w skrajnym przypadku zawieszenia się programu, ale nawet do zablokowania całego systemu.

Sam wyciek jest najczęściej wynikiem:

  • zapominalstwa
  • wielu ścieżek powrotu z funkcji
  • przypisania nowej wartości do wskaźnika przed wywołaniem Free
  • niezwolnienia elementów struktury po zwolnieniu struktury
  • nieświadomości, że funkcja wywoływana alokuje pamięć, którą funkcja wywołująca powinna zwolnić.

Śledzenie wycieków pamięci

Jak już zostało wspomniane, mimo, że wycieki pamięci są efektem bardzo niepożądanym, tak naprawdę czasami całkiem nieświadomie możemy doprowadzić do tego, że nasza aplikacja będzie traciła pamięć przez nieudolną alokację i dealokację. Jednym ze sposobów jest staranne prześledzenie zmiennych związanych z alokacją pamięci, linia po linii, od narodzin do śmierci, jednak nie jest to ani łatwy, ani przyjemny sposób. Nie mniej jednak śledzenie wycieków może być względnie proste. Mowa tu głównie o aplikacjach śledzących wycieki pamięci, albo wykorzystaniu dyrektyw [[C/preprocesor|preprocesora]].

Narzędzia wspomagające śledzenie wycieków pamieci

Użytkownicy linuksa, mogą skorzystać z konsolowj aplikacji ElectricFence albo valgrind: `valgrind -v ./nazwa_programu`

Użycie dyrektyw preprocesora

Dzięki użyciu dyrektyw preprocesora, możemy stworzyć własny prototyp każdej funkcji alokującej i dealokującej pamięć, np. poprzez użycie odpowiednich makrodefinicji:
#define MALLOC(SIZE) debug_malloc(_FILE_ , _LINE_, SIZE)
#define FREE(ptr) debug_free(_FILE_, _LINE_, ptr)

void* debug_malloc(const char* scr, int line, size_t size)
{
  void *p;
  p = malloc(size);
  printf("Alokacja pamieci w pliku %s, linia: %d\n", scr, line);
  return p;
}

void debug_free(const char* scr, int line, void*ptr)
{
  free(ptr);
  printf("Zwolnienie pamieci w pliku %s, linia: %d\n", scr, line);
  return;
}

int main()
{
  int *wsk;
  /* Alokacja pamięci, przy użyciu makra MALLOC */
  wsk = MALLOC(sizeof(int));
  *wsk = 13;
  /* Dealokacja pamięci przy użyciu makra FREE */
  FREE(wsk);
}
Powyższy przykład zakłada użycie makra MALLOC oraz FREE zamiast bezpośredniego wywołania [[C/malloc]] oraz [[C/free]]. Warto zauważyć, że przy wywołaniu nowego prototypu każdej z funkcji, jako argument preprocesor poda nazwę aktualnego pliku oraz linijkę, w które nastąpiła alokacja/dealokacja, dzięki czemu łatwiej śledzić, co dzieje się z alokowaną pamięcią. Oczywiście, jeżeli nasza aplikacja polega na wprowadzaniu i wypisywaniu danych w konsoli, rozwiązanie z <var>printf</var> może nie być za wygodne. Warto wówczas rozpatrzyć zapisywanie danych o wywołaniu alokacji/dealokacji bezpośrednio do pliku, zamiast na ekran.

Zobacz też:

1 komentarz

O, fajnie coś się zaczęło dziać w dziale C :)