İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat