Bellek sızıntısı, yok olan bellek değildir. Hâlâ sizin olan, hâlâ ayrılmış olan ve geri verme yeteneğinizi kaybettiğiniz bellektir - çünkü ona giden son gösterici gitmiştir. Hiçbir şey çökmez. Program her seferinde biraz daha ağırlaşarak çalışmaya devam eder, ta ki alakasız bir yerde bir şey başarısız olana dek.
Bu sayfa sızıntıların nasıl ortaya çıktığını, çoğunu önleyen disiplini ve gerisini bulan iki aracı ele alıyor.
Bir Sızıntı Neye Benzer
Her yineleme block'un üzerine taze bir gösterici yazar. Önceki blok hâlâ tahsis edilmiştir; hiçbir değişken adresini tutmaz; asla serbest bırakılamaz. Üç yineleme on iki kilobayt kaybeder. Bunu istek başına bir kez yapan bir sunucu, istekler hangi hızda gelirse o hızda sonsuza dek kaybeder.
Çözüm tek bir satırdır - gövdenin sonunda free(block); - ama nereye ait olduğunu fark etmek asıl beceridir.
Sızıntılar Nasıl Oluşur
1. Kayıp gösterici
Canlı bir bloğa giden tek referansı hâlâ tutan bir göstericiye yapılan her atama onu sızdırır.
char *name = malloc(32);
name = malloc(64); /* ilk 32 bayt artık erişilemez */
Yukarıdaki döngü aynı hatanın döngü kılığındaki hâlidir. Bir yapı alanını yeniden atamak da öyledir ve calloc ve realloc sayfasındaki realloc kısayolu da öyle:
p = realloc(p, n); /* başarısızlıkta: p NULL olur, eski blok öksüz kalır */
2. Erken dönüş
Bir fonksiyondan çıkan her yolun, fonksiyonun çoktan aldığını serbest bırakması gerekir. Unutulan her zaman bir hata yoludur.
Mutlu yol doğrudur ve hata yolu sızdırır; bunun testten sağ çıkmasının sebebi budur: başarısız dal geliştirme sırasında neredeyse hiç çalışmaz. Çözüm, her yolun atladığı tek bir temizlik bölümüdür:
Deneyimli C programcılarının aktif olarak önerdiği tek goto kullanımı budur. İşe yarar, çünkü her gösterici NULL'dan başlar ve free(NULL) bir işlemsizliktir, dolayısıyla fonksiyon ne kadar ilerlemiş olursa olsun tek bir çıkış bloğu doğrudur.
3. Belirsiz sahiplik
En ince sızıntılar aslında kodlama hataları değildir - kimin işi olduğu konusunda anlaşamayan iki fonksiyondur.
char *build_message(void); /* bunu çağıran mı serbest bırakır? */
void store(char *text); /* store sahipliği alır mı? */
build_message tahsis edilmiş bellek döndürüyorsa ve store onu kopyalıyorsa, çağıran serbest bırakmalıdır. store göstericiyi saklıyorsa çağıran bırakmamalıdır. Kodda hangisi olduğunu söyleyen hiçbir şey yoktur, dolayısıyla iki varsayımdan biri iki kez yapılır - ve ya bir sızıntı ya da çifte serbest bırakma alırsınız.
Çare, tahsis yapan her fonksiyonun yanında belirtilen bir gelenektir:
/* Yeni tahsis edilmiş bir metin döndürür; çağıran onu serbest bırakmalıdır. */
char *build_message(void);
/* 'text'in sahipliğini alır; store_free() tarafından serbest bırakılacaktır. */
void store(char *text);
Kuralı bir tasarım belgesine değil fonksiyonun yanına yazın. C bellek yönetimindeki tek en değerli alışkanlıktır.
Sahiplik Disiplini
Dört kural neredeyse her şeyi kapsar:
- Her tahsisin tam olarak bir sahibi vardır - onu serbest bırakmaktan sorumlu bir kod parçası.
- Tahsis eden her fonksiyonu serbest bırakan biriyle eşleyin.
vec_init/vec_free,config_load/config_free. Simetri, eksik bir çağrıyı görünür kılar. - Fonksiyonun yorumu sahipliği açıkça devretmedikçe, tahsis eden katmanda serbest bırakın.
- Serbest bıraktıktan sonra bir göstericiyi
NULLyapın, böylece daha sonraki kazara bir kullanım, öbeği sessizce bozmak yerine hata noktasında çöker.
Sızıntıları Bulmak: valgrind
Linux'ta valgrind yeniden derleme gerektirmez, ama hata ayıklama sembolleri raporu okunaklı kılar:
gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program
Bu sayfanın tepesindeki sızdıran döngü için rapor şöyle bir şeyle biter:
==12345== HEAP SUMMARY:
==12345== in use at exit: 12,000 bytes in 3 blocks
==12345== total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345== by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 12,000 bytes in 3 blocks
Onu alttan okuyun. "definitely lost", çıkışta bloğa giden hiçbir gösterici olmadığı anlamına gelir - gerçek bir sızıntı. Yığın izi, onun kaybedildiği satırı değil onu oluşturan malloc'un satırını adlandırır ki bu genellikle eksik free'yi bulmaya yeter.
İki başka kategori daha belirir:
- indirectly lost - yalnızca kendisi kaybolmuş bir blok üzerinden erişilebilen bloklar, sızdırılmış bağlı bir listenin elemanları gibi. "Definitely lost" olanı düzeltin, bunlar yok olur.
- still reachable - çıkışta tahsisli ama canlı bir göstericisi olan, tipik olarak bir küresel önbellek. Tehlikeli anlamda bir sızıntı değildir, ama rapor boş kalsın diye serbest bırakmaya değer.
Valgrind ayrıca ilk değersiz bellek okumalarını ve bir bloğun sonunun ötesine yazmaları yakalar; sızıntının arkasındaki hatayı çoğu zaman böyle keşfedersiniz.
Sızıntıları Bulmak: AddressSanitizer
AddressSanitizer, GCC ve Clang'in içine yerleşiktir, valgrind'den çok daha hızlı çalışır ve valgrind'in çalışmadığı yerlerde çalışır (güncel macOS dahil):
gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program
Sızıntı raporu çıkışta otomatik olarak yazdırılır:
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 12000 byte(s) in 3 object(s) allocated from:
#0 0x7f... in malloc
#1 0x1086... in main program.c:6
SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).
ASan ayrıca serbest-bırakma-sonrası-kullanımları ve öbek tampon taşmalarını, daha sonra gizemli bozulmalar yerine anlık, açıkça etiketlenmiş durdurmalara dönüştürür. Test çalıştırmalarınızı onunla derleyin; yayın derlemelerini onsuz derleyin, çünkü bellek ve hıza mal olur.
Platformunuzda sızıntı saptama devreye girmiyorsa, çalıştırmadan önce ortamda ASAN_OPTIONS=detect_leaks=1 ayarlayın.
Sızdıran Bir Programı Adım Adım Düzeltmek
İşte üç ayrı sızıntısı olan küçük bir program:
Valgrind, üç farklı satır numarasıyla üç "definitely lost" kaydı bildirir. Teker teker düzeltilmiş hâli:
- düzeltme, sahiplik yorumunun hayata geçmiş hâlidir:
shouttahsis eder,mainserbest bırakır. 2. düzeltme, ilkini serbest bırakmak yerine çifte tahsisi tamamen kaldırır - daha basit kod aynı zamanda doğru koddur. 3. düzeltme, erken dönüşteki eksikfree'yi ekler; daha fazla tahsis söz konusu olduğunda, daha önce gösterilen tekcleanup:etiketi free'leri tekrarlamaktan daha iyi ölçeklenir.
Sızıntıları Önleyen Alışkanlıklar
free'yi,malloc'u yazdıktan hemen sonra yazın, sonra aradaki kodu doldurun.- Tahsis eden her fonksiyona eşleşen bir serbest bırakma fonksiyonu verin.
- Tahsis ettiği bir göstericiyi döndüren ya da alan her fonksiyonda sahipliği bir yorumda belirtin.
- Birkaç tahsis tutan fonksiyonlarda tek bir
cleanup:çıkış bloğu kullanın. - Testlerinizi yalnızca bir şey ters göründüğünde değil, kural olarak
-fsanitize=addressaltında çalıştırın. - "definitely lost: 0 bytes"ı geçen bir test çalıştırmasının parçası sayın.
Sıkça Sorulan Sorular
C'de bellek sızıntısı nedir?
malloc ile tahsis ettiğiniz ve artık serbest bırakamadığınız bellektir, çünkü programda hâlâ onu gösteren hiçbir şey yoktur. Blok, sürecin ömrü boyunca ayrılmış kalır. Bu bir çökme değildir - program çalışmaya devam eder, yalnızca her geçişte daha fazla bellek kullanır ve sonunda tükenir.
C'de bellek sızıntılarını nasıl bulurum?
Programı valgrind altında çalıştırın: valgrind --leak-check=full ./program. Çıkışta hâlâ tahsis edilmiş her bloğu, onu oluşturan malloc'un yığın iziyle birlikte bildirir. macOS'ta ya da valgrind'in olmadığı yerlerde -fsanitize=address ile derleyin; aynı rapor çıkışta gelir.
C'de bellek sızıntılarına ne yol açar?
Üç kalıp neredeyse hepsini kapsar: bir bloğa giden tek göstericinin üzerine yazmak (başarısızlıkta p = realloc(p, n) dahil), çoktan tahsis yapmış bir fonksiyondan erken dönmek ve belirsiz sahiplik - iki fonksiyonun her birinin, diğerinin serbest bırakacağını varsayması, dolayısıyla hiçbirinin bırakmaması.
Program nasılsa çıkacaksa bellek sızıntıları önemli mi?
Bir kez çalışıp çıkan bir program için işletim sistemi her şeyi geri alır, dolayısıyla pratik etkisi sıfırdır. Uzun süre çalışan herhangi bir şey için önemlidir - bir sunucu, bir oyun döngüsü, bir arka plan süreci - orada istek başına bir sızıntı sınırsız büyür. Yine de tutarlı biçimde serbest bırakın: bir sızıntı raporu, gerçekten önemli olanları gizleyen gürültüdür.