Ders 09 / 15
Bellek Sızıntıları
Bir bellek sızıntısı ne bir hata mesajı verir ne programı çökertir — yalnız ayrılan ile bırakılan arasındaki farkı büyütür. Ders iki yoldan sızıntı üretiyor, ikisini de sayaç farkıyla yakalıyor ve toplamın neden tam 2 çıktığını gösteriyor.
Yayın Güncelleme
Bu derste
- Bir işaretçinin üzerine yazılmasının önceki bloğu nasıl ulaşılamaz bıraktığını sayaç farkıyla gösterir
- Dönen bir değerin yok sayılmasının aynı sonucu nasıl ürettiğini ölçer
- Bellek sızıntısının bir hata mesajı değil, bir sayaç farkı olduğunu açıklar
İçindekiler
Önceki ders bir sözleşmenin nasıl kırılabileceğini yalnız tarif etti — çift bırakma da, bırakılmayan bir kopya da orada koşturulmadı. Bu ders, sözleşmenin kırılmasının bir türünü gerçekten koşturuyor: kimsenin bırakmakla yükümlü olmadığı bir blok. Bu, önceki üç sınıftan (dil, çağıran, çağrılan) hiçbirine girmez — kursun dördüncü ve son sınıfı sahipsizdır. Sahipsiz sınıf bir tasarım değil, bir kusurdur; ve göründüğü tek yer sayaç farkıdır.
İki Yoldan Aynı Sonuç
Bir blok iki farklı biçimde sahipsiz kalabilir: ona ulaşan tek işaretçinin üzerine yazılmasıyla, ya da onu döndüren bir çağrının dönüş değerinin hiç tutulmamasıyla. İkisi de aynı şeyi üretir — artık hiçbir değişkenden ulaşılamayan bir blok — ama biri bir atama hatası, öteki bir gözden kaçırmadır.
// sayac.h — her ayirmayi ve her birakmayi sayan sarmalayici #ifndef SAYAC_H #define SAYAC_H #include <stddef.h> void *say_ayir(size_t boyut); void *say_buyut(void *eski, size_t boyut); void say_birak(void *blok); int ayrilan_sayisi(void); int birakilan_sayisi(void); #endif
// sayac.c — sayaclar yalniz bu birimde tutulur
#include <stdlib.h>
#include "sayac.h"
static int ayrilan = 0;
static int birakilan = 0;
void *say_ayir(size_t boyut) {
void *blok = malloc(boyut);
if (blok != NULL) {
ayrilan++;
}
return blok;
}
void *say_buyut(void *eski, size_t boyut) {
int yeni_blok = (eski == NULL);
void *yeni = realloc(eski, boyut);
if (yeni != NULL && yeni_blok) {
ayrilan++;
}
return yeni;
}
void say_birak(void *blok) {
if (blok != NULL) {
birakilan++;
}
free(blok);
}
int ayrilan_sayisi(void) {
return ayrilan;
}
int birakilan_sayisi(void) {
return birakilan;
}
// sahip.h — kaydin ici gizli, sahiplik sozlesmesi yorumlarda yazili #ifndef SAHIP_H #define SAHIP_H #include <stddef.h> typedef struct Kayit Kayit; /* Cagiran sahiplenir ve kayit_yok_et ile birakir. Ad sinira sigmazsa NULL doner; o ana kadar ayrilan bellegi islev kendisi birakir. */ Kayit *kayit_olustur(const char *ad, size_t sinir); /* Kaydi ve kaydin icindeki ad tamponunu birlikte birakir. */ void kayit_yok_et(Kayit *k); /* Kayit sahiplenir: donen isaretci kayit yok edilene dek gecerlidir, cagiran onu birakmaz. */ const char *kayit_ad(const Kayit *k); /* Cagiran sahiplenir: donen kopya say_birak ile birakilir. */ char *kayit_ad_kopyasi(const Kayit *k); #endif
// sahip.c — sozlesmenin uygulandigi birim
#include <string.h>
#include "sahip.h"
#include "sayac.h"
struct Kayit {
char *ad;
size_t uzunluk;
};
Kayit *kayit_olustur(const char *ad, size_t sinir) {
Kayit *k = say_ayir(sizeof *k);
if (k == NULL) {
return NULL;
}
k->uzunluk = strlen(ad);
if (k->uzunluk >= sinir) {
say_birak(k);
return NULL;
}
k->ad = say_ayir(k->uzunluk + 1);
if (k->ad == NULL) {
say_birak(k);
return NULL;
}
memcpy(k->ad, ad, k->uzunluk + 1);
return k;
}
void kayit_yok_et(Kayit *k) {
if (k == NULL) {
return;
}
say_birak(k->ad);
say_birak(k);
}
const char *kayit_ad(const Kayit *k) {
return k->ad;
}
char *kayit_ad_kopyasi(const Kayit *k) {
char *kopya = say_ayir(k->uzunluk + 1);
if (kopya != NULL) {
memcpy(kopya, k->ad, k->uzunluk + 1);
}
return kopya;
}
// sizinti.c — uzerine yazma ile yok sayilan donusun karsilastirilmasi
#include <stdio.h>
#include "sahip.h"
#include "sayac.h"
static int fark(void) {
return ayrilan_sayisi() - birakilan_sayisi();
}
static void gozle(const char *ad, const char *sinif, const char *kanit) {
printf("%-30s%-10s%s\n", ad, sinif, kanit);
}
int main(void) {
char tampon[64];
int once;
int birinci_fark, ikinci_fark;
printf("%-30s%-10s%s\n", "gozlem", "sahip", "kanit");
/* --- uzerine yazilan isaretci: ilk bloga ulasilamaz --- */
once = fark();
int *kayip = say_ayir(sizeof *kayip);
kayip = say_ayir(sizeof *kayip);
say_birak(kayip);
birinci_fark = fark() - once;
snprintf(tampon, sizeof tampon, "sayac farki %d", birinci_fark);
gozle("uzerine yazilan isaretci", "sahipsiz", tampon);
/* --- yok sayilan donus: kopya hic tutulmadan kaybolur --- */
Kayit *k = kayit_olustur("taslak", 16);
once = fark();
if (k != NULL) {
kayit_ad_kopyasi(k);
}
ikinci_fark = fark() - once;
snprintf(tampon, sizeof tampon, "sayac farki %d", ikinci_fark);
kayit_yok_et(k);
gozle("yok sayilan donus", "sahipsiz", tampon);
printf("toplam sahipsiz sayac farki %d\n", birinci_fark + ikinci_fark);
return 0;
}
gozlem sahip kanit uzerine yazilan isaretci sahipsiz sayac farki 1 yok sayilan donus sahipsiz sayac farki 1 toplam sahipsiz sayac farki 2
BE8. uzerine yazilan isaretci gözleminde kayip iki kez ayrılıyor ama yalnız bir kez
bırakılıyor: ikinci say_ayir çağrısı kayip’in üzerine yazınca, ilk çağrının döndürdüğü
adresi tutan hiçbir değişken kalmıyor. say_birak(kayip) yalnız ikinci bloğu
bırakabiliyor — birinciye artık hiçbir yoldan ulaşılamıyor. Sayaç farkı 1: bir blok
ayrıldı, karşılığı hiç bırakılmadı.
BE9. yok sayilan donus gözlemi farklı bir yoldan aynı sonuca varıyor. kayit_ad_kopyasi
her çağrıldığında yeni bir blok ayırıyor ve onu döndürüyor — işlevin kendisi bloğu
bırakmıyor, çünkü sözleşmesi bunu çağırana bırakıyor (Serbest Bırakma Disiplini dersi).
Burada dönüş değeri hiçbir değişkene atanmadı; işlev başarıyla çalıştı, blok ayrıldı, ama
ona ulaşan hiçbir işaretçi hiç var olmadı. Sayaç farkı yine 1. İki gözlem birlikte
toplam sahipsiz sayac farki 2 veriyor — bu sayı, ölçüm çekirdeğinin tam koşumundaki
son satırla aynı fark 2dir; burada aynı sonucu yalnız iki sahipsiz gözlemden topluyor.
Sızıntı Tekrarlanınca
Önceki bölümdeki iki gözlem sızıntıyı tek seferlik ölçtü: her biri sayaç farkını bir artırdı. Ama “uzun süre çalışan bir süreçte sayaç sürekli büyür” iddiası henüz yalnız sözle söylendi, ölçülmedi. Aynı sızdıran düzeneği bir döngü içinde birkaç kez çalıştırmak, bu iddiayı doğrudan bir sayıya çevirir.
// tekrar.c — ayni sizintinin dongude tekrarlanmasi
#include <stdio.h>
#include "sahip.h"
#include "sayac.h"
static int fark(void) {
return ayrilan_sayisi() - birakilan_sayisi();
}
static void sizdiran_cagri(const Kayit *k) {
/* donus deger kasten tutulmuyor: her cagri bir blok sizdirir */
kayit_ad_kopyasi(k);
}
int main(void) {
char tampon[64];
Kayit *k = kayit_olustur("dongu", 16);
int once = fark();
for (int i = 1; i <= 3; i++) {
sizdiran_cagri(k);
int su_an = fark() - once;
snprintf(tampon, sizeof tampon, "cagri %d sonrasi sayac farki %d", i, su_an);
printf("%-30s%s\n", "sizinti tekrari", tampon);
}
kayit_yok_et(k);
return 0;
}
sizinti tekrari cagri 1 sonrasi sayac farki 1 sizinti tekrari cagri 2 sonrasi sayac farki 2 sizinti tekrari cagri 3 sonrasi sayac farki 3
BE10. Her sizdiran_cagri çağrısı kayit_ad_kopyasi üzerinden bir blok ayırıyor ve
dönüşü hiç tutmuyor — tıpkı yok sayilan donus gözlemindeki gibi, ama burada tek bir çağrı
yerine üç kez üst üste. Sayaç farkı çağrı başına tam 1 artıyor: 1, 2, 3. Bu üç sayı,
“kısa süren bir programda bu fark önemsiz kalabilir; uzun süre çalışan bir süreçte sayaç
sürekli büyür” cümlesinin artık bir gözlem, iddia olmadığını gösteriyor. Döngü üç turda
durduğu için fark üçte duruyor; gerçek bir süreçte böyle bir çağrı saniyede binlerce kez
çalışabilir ve aynı doğrusal artış hiç durmadan sürer — sayaç bu kursta elle tutulduğu için
görünür, gerçek bir programda görünmez ama olmaz değildir.
Kısmi Sızıntı: Yarım Bırakılan Dizi
Önceki iki gözlem sızıntıyı ya tam yaptı ya da hiç yapmadı — bir çağrının dönüşü ya tutuldu ya hiç tutulmadı. Ama gerçek kodda daha sinsi bir örüntü de olur: bir dizi bloğun çoğu doğru bırakılır, biri atlanır. Toplam sayaç farkı bunu da aynı duyarlılıkla yakalıyor — kaçının doğru yapıldığından bağımsız, yalnız kaçının eksik kaldığını sayıyor.
// kismi.c — dort kopyadan ucunun birakilip birinin atlanmasi
#include <stdio.h>
#include "sahip.h"
#include "sayac.h"
static int fark(void) {
return ayrilan_sayisi() - birakilan_sayisi();
}
int main(void) {
char tampon[80];
enum { ADET = 4 };
char *kopyalar[ADET];
Kayit *k = kayit_olustur("kismi", 16);
int once = fark();
for (int i = 0; i < ADET; i++) {
kopyalar[i] = kayit_ad_kopyasi(k);
}
for (int i = 0; i < ADET; i++) {
if (i == 2) {
continue; /* ucuncu kopya kasten birakilmiyor */
}
say_birak(kopyalar[i]);
}
int kalan_fark = fark() - once;
kayit_yok_et(k);
snprintf(tampon, sizeof tampon,
"ayrilan %d, birakilan %d, kalan sayac farki %d", ADET, ADET - 1, kalan_fark);
printf("%-30s%s\n", "kismi birakma", tampon);
return 0;
}
kismi birakma ayrilan 4, birakilan 3, kalan sayac farki 1
BE11. Döngü dört kopya ayırdı, ikinci döngü bunlardan üçünü bıraktı ve yalnız i == 2
olan kopyayı atladı. kalan_fark 1 çıkıyor — tam olarak atlanan kopya sayısı kadar,
ne fazla ne eksik. Bu gözlem, sızıntının “ya hep ya hiç” olmadığını gösteriyor: bir döngü
içindeki dört bırakma çağrısından üçünün doğru çalışması, dördüncüsünün eksikliğini
gizlemiyor, sayaç farkı yine de tam eksik kalan kadar büyüyor. Gerçek bir programda bu
örüntü, bir döngünün erken continue ya da break ile atladığı bir dalda ortaya çıkar —
kodun geri kalanı tamamen doğru bırakma disiplini uygularken, tek bir kontrol akışı dalı
bu disiplinin dışında kalır.
Sınırlayıcı Ölçüm
Bu iki gözlemin ikisinde de program çökmedi, hiçbir uyarı basmadı, hiçbir işlev NULL
döndürmedi. Ayırma işlevleri sözleşmelerini tam olarak yerine getirdi: istenen boyutta bir
blok verdiler. Sorun onların tarafında değil, çağıranın tarafında — bir atama unutuldu ya
da bir dönüş değeri yok sayıldı. Bu yüzden bir bellek sızıntısı, çalışma zamanında hiçbir
gözlemlenebilir belirti vermez; yalnız ayrılan ile bırakılan arasındaki fark büyür. Kısa
süren bir programda bu fark önemsiz kalabilir; uzun süre çalışan bir süreçte aynı düzenek
tekrar tekrar işletilirse, sayaç sürekli büyür ve sonunda bellek tükenir.
Bu, M10/K02’de ölçülen ömür sorusuyla karışmaz. Orada bir değerin ömrünün nerede bittiği soruluyordu ve o yeri dil belirliyordu — kapsam dışına çıkan bir yerel değişken otomatik olarak geri alınırdı. Burada bloğun ömrünü bitirecek hiçbir dil kuralı yok; blok, dil tarafından değil, çağıranın kendi bırakma çağrısıyla geri alınır. O çağrı hiç yapılmazsa, dil hiçbir şeyi kendiliğinden bitirmez — bu yüzden sahipsiz sınıf diğer üçünden farklı olarak bir eksikliğin adıdır, bir kuralın değil.
Diğer üç sınıfın her biri, sahibin kim olduğuna bir cevap verir: dil sınıfında sahip
kapsamın kendisi, çağıran sınıfında sahip çağıran tarafın kodu, çağrılan sınıfında sahip
modülün içindeki sözleşmedir. Sahipsiz sınıfın verdiği cevap yoktur — çünkü soru hiç
sorulmamıştır. uzerine yazilan isaretci gözleminde blok, ayrıldığı anda çağıran
sınıfına aitti; say_ayir’in döndürdüğü işaretçi kimin sorumluluğunda olduğu konusunda
belirsiz değildi. Belirsizlik sonradan, o işaretçi ikinci bir atamayla ezilince doğdu. Aynı
şey yok sayilan donus için de geçerli: kayit_ad_kopyasi’nin sözleşmesi net (çağıran
sahiplenir), ama sahiplenecek çağıran tarafında hiçbir değişken o rolü üstlenmedi.
Sahipsizlik bu yüzden dördüncü, bağımsız bir sınıf değil; önceden var olan bir
sahiplik ilişkisinin koddaki izinin silinmesidir. Bir bloğun sahibi hiçbir zaman
“sahipsiz” olarak tasarlanmaz — yalnız öyle bırakılır.
Özet
- Bir bellek sızıntısı iki yoldan doğar: bir işaretçinin üzerine yazılması ya da dönen bir değerin yok sayılması; bu dersin ölçtüğü bellek sınıfı sahipsizdır.
- Sızıntı hiçbir hata mesajı ya da çökme üretmez; tek görünür kanıtı sayaç farkıdır.
- İki gözlemin her biri sayaç farkını 1 artırdı, toplamı 2 verdi — ölçüm çekirdeğinin bağlayıcı son satırındaki fark ile aynı sayı.
- Sahipsiz sınıf bir dil kuralı değil, bir eksikliktir: dil hiçbir bırakma çağrısını kendiliğinden yapmaz.
Sonraki Adım
Bu ders sızıntıyı sayaç farkıyla ölçtü, çünkü çekirdeğin kendisi sayaçlı bir
sarmalayıcıydı. Gerçek bir programda böyle bir sayaç elle tutulmaz — bunun yerine bu işi
yapan bir araç çalıştırılır. Sıradaki ders, bu dersin sayaçlı sarmalayıcısının aslında
böyle bir aracın yaptığını nasıl modellediğini gösteriyor: yakalandi ya da yakalanmadi,
başka hiçbir isim geçmeden.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.