Aylar sonra çöken cihazın profili

Saha arızalarının en can sıkıcı sınıfı şudur: cihaz kurulur, haftalarca kusursuz çalışır, sonra bir sabah malloc NULL döner ya da OOM killer süreci vurur. Kod değişmemiştir, yük artmamıştır, sızıntı avcısı temiz rapor vermiştir. Toplam boş bellek hâlâ boldur — ama bitişik boş bellek kalmamıştır.

Bu, bellek parçalanmasıdır ve gömülü sistemlerde masaüstünden farklı davranır, çünkü sizin cihazınız yeniden başlatılmaz. Bir masaüstü uygulaması gün içinde kapanıp heap'ini sıfırlar; bir hat kenarı denetleyicisi aynı süreci yıllarca ayakta tutar. Aynı ayırma deseni, aynı ayırıcı, çok daha uzun bir maruziyet. Aşağıda konu bir bakım sorunu değil mimari karar olarak ele alınıyor: hangi ayırıcı, hangi disiplin, hangi teşhis.

Sızıntı ile parçalanmayı ayırmak

İkisi aynı belirtiyi verir — RSS büyür — ama tedavileri zıttır.

Gözlem Sızıntı Parçalanma
Kullanımdaki bayt (in-use) Sürekli artar Uzun vadede düz
Süreç RSS'i Artar Artar
Boş blok sayısı Değişmez Artar, bloklar küçülür
En büyük bitişik boş blok Normal Düşer
ASan / Valgrind raporu Yakalar Yakalamaz
Çözüm Hatalı kodu düzelt Ayırma desenini veya ayırıcıyı değiştir

Kritik ayrım son satırda: sızıntı bir hatadır, parçalanma bir sonuçtur. Parçalanmada kaybolan bellek yoktur; her blok defterde durur, sadece hiçbiri istediğiniz boyutta değildir. Bu yüzden sızıntı araçları sessiz kalır ve ekip haftalarca yanlış yerde arar.

Parçalanma neden zamanla birikir

İç parçalanma, ayırıcının 40 baytlık isteği 48 baytlık bir boyut sınıfına yuvarlamasıdır: kaybı sabittir, hesaplanabilir, genellikle kabul edilebilir. Dış parçalanma ise boş alanın kullanılamaz parçalara bölünmesidir; birikimlidir ve asıl tehlike budur.

Teorik sınır iç karartıcıdır. Robson'ın 1974 tarihli JACM makalesi, en kötü durumda bir ayırıcının — aynı anda kullanımdaki azami bellek M, en büyük/en küçük blok oranı n iken — kabaca M · log₂(n) kadar alana ihtiyaç duyabileceğini gösterir. Pratikte bu sınıra ulaşılmaz; Wilson, Johnstone, Neely ve Boles'un 1995 tarihli derlemesi, gerçek programların rastgele sentetik izlerden çok daha iyi davrandığını, çünkü gerçek ömür desenlerinin kümelendiğini gösterir. Ama "çok daha iyi", "sıfır" değildir.

Aylar geçmesinin sebebi olasılıktır. Parçalanmayı üreten şey kötü bir ayırma sırasının art arda gelmesidir: uzun ömürlü küçük bir nesne, kısa ömürlü büyük bir bloğun tam ortasına düşer ve o bölgenin geri verilmesini kalıcı olarak engeller. Tek tek bu olay nadirdir; saniyede binlerce ayırma yapan bir süreçte ise nadirlik sadece bir zaman ölçeğidir.

glibc'de bunu hızlandıran somut bir mekanizma var. Ana arena brk ile büyür ve yalnızca tepeden küçültülebilir; mallopt(3)'e göre M_TRIM_THRESHOLD varsayılanı 128 KB'dir. Yüksek adreste hayatta kalan tek bir küçük nesne, altındaki megabaytların iade edilmesini engeller. Aynı belge dinamik mmap eşiğini de tanımlar: M_MMAP_THRESHOLD başlangıçta 128 KB'dir, fakat eşikten büyük bir blok serbest bırakıldığında eşik o bloğun boyuna yükseltilir — 64 bit sistemlerde DEFAULT_MMAP_THRESHOLD_MAX olan 32 MB'a kadar. Sonuç: başlangıçta mmap ile karşılanan büyük tamponlar saatler sonra heap'ten karşılanmaya başlar ve arena kalıcı olarak şişer. Bu bir hata değil, belgelenmiş bir davranıştır; sadece uzun ömürlü süreçler için yanlış varsayılandır.

Linux tarafında tek değil, dört ayrı parçalanma

Ekipler "bellek parçalanması" derken genellikle sadece birincisini kastediyor. Dördü ayrı arızalanır ve ayrı teşhis edilir.

Katman Nerede görünür Tipik arıza
Kullanıcı alanı heap malloc_info(3), malloc_stats(3) malloc NULL döner, RSS şişer
Çekirdek sayfa ayırıcısı (buddy) /proc/buddyinfo, /proc/pagetypeinfo dmesg'de page allocation failure: order:N
CMA / bitişik fiziksel tampon /proc/meminfo içindeki CmaTotal, CmaFree Kamera veya kodek tamponu ayrılamaz
Slab (SLUB) /proc/slabinfo dentry/inode önbelleği büyür, geri kazanım yavaşlar

İkinci satır özellikle sinsi: yüksek dereceli (order ≥ 1) bir istek, sistemde bolca boş bellek varken de başarısız olabilir; çünkü boş sayfalar tek tek dağılmıştır. Çekirdeğin kcompactd ile yaptığı sıkıştırmanın sonucu /proc/vmstat içinde compact_stall, compact_fail ve pgmigrate_fail sayaçlarında görünür; vm.min_free_kbytes ve vm.extfrag_threshold bu davranışı ayarlar. CONFIG_DEBUG_FS açıksa /sys/kernel/debug/extfrag/unusable_index, her derece için boş belleğin ne kadarının o dereceye elverişsiz olduğunu doğrudan verir.

Üçüncü satır, video işleyen kartların klasik geç arızasıdır: CMA bölgesi taşınabilir sayfalarla dolduğunda dma_alloc_coherent başarısız olur ve sürücü, uygulama hiç dokunmadan tamponunu kaybeder. Sıfır kopya boru hattı tasarlıyorsanız bu, GStreamer zero-copy pipeline kararlarının doğrudan parçasıdır.

RTOS tarafında: tek heap, tek şans

Linux'ta sanal adres alanı parçalanmanın bir kısmını gizler; MMU'suz veya düz haritalı bir RTOS'ta bu tampon yoktur. Bir Cortex-M üzerinde parçalanma doğrudan NULL demektir.

FreeRTOS'un beş heap uygulaması aslında beş ayrı politika kararıdır: heap_1 serbest bırakmayı desteklemez; heap_2 serbest bırakır ama komşu blokları birleştirmez (yeni tasarımlar için önerilmez); heap_3 standart malloc/free'yi zamanlayıcıyı askıya alarak sarar; heap_4 bitişik blokları birleştiren first-fit uygular; heap_5 aynı algoritmayı birbirine bitişik olmayan birden fazla bölgeye yayar. Uzun ömürlü bir cihazda heap_2 ile heap_4 arasındaki fark, sahada aylara mal olur.

Asıl mesele izlenen metriktir. xPortGetFreeHeapSize() toplam boşu verir ve yanlış metriktir. heap_4/heap_5 ile gelen vPortGetHeapStats(), HeapStats_t içinde xSizeOfLargestFreeBlockInBytes ve xNumberOfFreeBlocks alanlarını da döndürür; parçalanma tam olarak bu ikisinin birlikte hareketidir.

static void bellek_saglik_bildir(void)
{
    HeapStats_t s;
    vPortGetHeapStats(&s);

    telemetri_yaz("heap_bos",         s.xAvailableHeapSpaceInBytes);
    telemetri_yaz("heap_en_buyuk",    s.xSizeOfLargestFreeBlockInBytes);
    telemetri_yaz("heap_blok_sayisi", s.xNumberOfFreeBlocks);
    telemetri_yaz("heap_dip",         s.xMinimumEverFreeBytesRemaining);
}

void vApplicationMallocFailedHook(void)
{
    bellek_saglik_bildir();
    guvenli_duruma_gec();
    NVIC_SystemReset();
}

configUSE_MALLOC_FAILED_HOOK açık değilse başarısız bir ayırma sessizce NULL döner ve arıza, kök nedenden çok sonra başka bir yerde patlar. Zephyr tarafında k_mem_slab sabit boyutlu blok havuzu sunar — tanımı gereği dış parçalanma üretmez — ve sys_heap için CONFIG_SYS_HEAP_RUNTIME_STATS ile çalışma zamanı istatistikleri açılabilir.

Ayırıcı seçimi

Ayırıcı Strateji En kötü durum süresi Nereye uygun
dlmalloc / glibc ptmalloc2 Bin'li best-fit, sınır etiketleri Sınırsız Genel amaçlı Linux; uzun ömür için ayar ister
musl mallocng (1.2.1 ve sonrası) Boyut sınıfları, sağlamlaştırılmış meta veri Sınırsız Küçük ayak izli Linux imajları
jemalloc Boyut sınıfları, arena'lar, decay ile iade Sınırsız Çok iş parçacıklı, yüksek ayırma hızı
TLSF İki seviyeli ayrık uyum, good-fit Sınırlı, sabit Dinamik ayırmanın zorunlu olduğu gerçek zamanlı yol
FreeRTOS heap_4 Birleştirmeli first-fit Blok sayısıyla artar MCU, ılımlı dinamiklik
Sabit boyutlu havuz Serbest liste Sabit Kritik yol; tercih edilen çözüm

TLSF'i ayıran şey hızı değil sınırlılığıdır: Masmano, Ripoll, Crespo ve Real'in 2004 ECRTS makalesi, ayırma ve serbest bırakma işlemlerinin blok sayısından bağımsız sabit zamanlı bir sınıra oturtulduğunu tarif eder. Gerçek zamanlı bir yolda önemli olan ortalama süre değil en kötü durumdur — bu ayrımı AM67A'da Linux + Cortex-R5F bölüşümü yazısında da aynı gerekçeyle kuruyoruz.

jemalloc'ta ayarlanacak şey MALLOC_CONF üzerinden dirty_decay_ms ve muzzy_decay_ms; izlenecek şey stats.allocated, stats.resident ve stats.retained üçlüsüdür. resident / allocated oranı, parçalanmanın tek satırlık göstergesidir.

Spikedge görüşü: Ayırıcı değiştirmek çoğu zaman bir çözüm değil, bir ertelemedir. Ayırma deseni bozuksa yeni ayırıcı sadece çöküş tarihini öteler. Önce deseni düzeltin, sonra ayırıcıyı seçin.

Statik ayırma disiplini

Gömülü dünyanın en eski cevabı hâlâ en güçlüsü: çalışma zamanında ayırma yapmayın. MISRA C:2012 Kural 21.3, <stdlib.h> içindeki malloc, calloc, realloc ve free kullanımını yasaklar. Holzmann'ın 2006'da IEEE Computer'da yayımladığı "Power of Ten" kurallarından üçüncüsü aynı şeyi söyler: başlatmadan sonra dinamik bellek ayırma yok.

Uygulanabilir orta yol, ayırmayı bir faza hapsetmektir: her şey açılışta ayrılır, sistem hazır olduğunda heap donar. Disiplinli hâli nesne başına havuzdur — her tip için sabit boyutlu, sabit sayıda blok. Yan faydası kapasitenin derleme zamanında görünür olmasıdır: "bu cihaz aynı anda en fazla 32 iz sürebilir" cümlesi bir tahmin değil, bir bildirim olur.

Linux tarafında gerçek zamanlı bir süreç için karşılığı, heap'i açılışta sabitlemektir:

void bellegi_dondur(size_t on_isinma_bayt)
{
    mallopt(M_TRIM_THRESHOLD, -1);
    mallopt(M_MMAP_MAX, 0);
    mallopt(M_ARENA_MAX, 1);

    if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0)
        hata("mlockall basarisiz");

    void *p = malloc(on_isinma_bayt);
    if (p != NULL) {
        memset(p, 0, on_isinma_bayt);
        free(p);
    }
}

Üç mallopt çağrısı sırasıyla şunu yapar: heap'i işletim sistemine geri verme, büyük blokları mmap ile karşılama, arena sayısını bire sabitle. mallopt(3)'e göre bu parametrelerden herhangi biri elle ayarlandığında dinamik mmap eşiği devre dışı kalır — yani yukarıdaki şişme mekanizması kapanır. M_ARENA_MAX özellikle önemlidir: aynı belgeye göre parametre ayarlanmazsa 64 bit sistemlerde azami arena sayısı çekirdek sayısının sekiz katı olarak hesaplanır ve her arena kendi parçalanmasını taşır. mlockall sayfaların takaslanmasını engeller; bu deseni PREEMPT_RT çekirdek yapılandırması rehberindeki diğer gerçek zamanlı ayarlarla birlikte düşünün.

C++ tarafında gizli ayırmalar ayrı bir avdır: std::string kısa metin optimizasyonunun dışına çıktığında, std::function yakaladığı durum inline tampona sığmadığında, std::vector her genişlemede ve std::shared_ptr kontrol bloğu için ayırır. Kritik yolda reserve() ve C++17 ile gelen std::pmr::monotonic_buffer_resource bu ayırmaları kaldırır.

Teşhis: sızıntı mı, parçalanma mı?

Anlık görüntü hiçbir şey söylemez; bu arıza sınıfının kanıtı eğimdir. Aynı sayıları günler boyunca sabit aralıkla toplayın, sonra iki eğriyi üst üste koyun: kullanımdaki bayt ve süreç RSS'i. Birlikte artıyorlarsa sızıntı, in-use düzken RSS artıyorsa parçalanma.

grep -E 'VmRSS|VmHWM|VmData' /proc/$PID/status
awk '/^Private_Dirty/ {s+=$2} END {print "private_dirty_kb", s}' /proc/$PID/smaps

cat /proc/buddyinfo
grep -E 'compact_stall|compact_fail|pgmigrate_fail' /proc/vmstat
cat /sys/kernel/debug/extfrag/unusable_index

cat /sys/fs/cgroup/system.slice/uygulama.service/memory.current
cat /sys/fs/cgroup/system.slice/uygulama.service/memory.events
cat /proc/pressure/memory

Kullanıcı alanı heap'inin içini görmek için malloc_info(0, stderr) çağrısını bir sinyal işleyicisine bağlayın; çıktıdaki XML, her arena için kullanımdaki toplam ile <system type="current"> değerini ayrı ayrı verir. İkisinin oranı, glibc altında parçalanmanın doğrudan ölçüsüdür.

Bir de hızlandırma gerekir: haftalarca beklemeden aynı deseni yoğunlaştırılmış hâlde koşturun. Üretimdeki ayırma çağrılarını LD_PRELOAD ile bir sarmalayıcıdan geçirip boyut ve ömür histogramını kaydedin, sonra bu izi tekrar oynatan sentetik bir yük yazın. Ölçümün kendisi kadar önemli olan tekrarlanabilirliği bu verir; aynı disiplinin başka bir alandaki uygulamasını EtherCAT determinizm ölçüm metodolojisi yazısında anlatıyoruz.

OOM killer gerçekte ne yapar

Parçalanma tartışmasının sonu genellikle OOM killer'a çıkar, ama ikisi aynı olay değildir. Çekirdeğin OOM killer'ı fiziksel bellek tükendiğinde devreye girer; yüksek dereceli bir ayırmanın parçalanma yüzünden başarısız olması ayrı bir arızadır ve hiçbir süreci öldürmeden bir sürücüyü çalışamaz hâle getirebilir. dmesg içinde order: kelimesini gördüğünüzde ikinci durumdasınız.

proc(5)'e göre her sürecin /proc/<pid>/oom_score_adj değeri -1000 ile 1000 arasındadır ve -1000 süreci seçilemez kılar. Buradaki tuzak şu: seçim büyük ölçüde bellek kullanımına dayandığı için kurban genellikle asıl uygulamanızdır, suçlu değil. vm.overcommit_memory 0 ise çekirdek sezgisel davranır, 1 ise her isteği kabul eder, 2 ise CommitLimit üzerinden sert sınır uygular — takas alanı olmayan bir gömülü kartta 2, arızayı OOM killer'dan önce malloc NULL'una çevirir ki bu genellikle daha yönetilebilir bir arızadır.

Asıl kaldıraç cgroup v2'dir. Süreci kendi sınırına hapsedin ve sınırı bir alarm hattı olarak kullanın:

[Service]
ExecStart=/usr/bin/uygulama
MemoryHigh=192M
MemoryMax=256M
OOMPolicy=stop
Restart=always

MemoryHigh aşıldığında süreç kısılır ve memory.events içindeki high sayacı artar; MemoryMax aşıldığında oom_kill sayacı artar. Bu iki sayaç, çöküşten önce alarm veren erken uyarı hattıdır; telemetride bulunması gereken şey budur. /proc/pressure/memory (PSI) aynı bilgiyi zaman payı olarak verir ve trend izlemeye daha uygundur. Bir uyarı: Restart=always parçalanmayı gizler. Yeniden başlatma sayacını telemetriye koymayan bir sistemde bu arıza yıllarca fark edilmez.

Nerede yanlış gider — tuzaklar

1. Toplam boş belleğe bakmak. Doğru metrikler en büyük bitişik blok ve boş blok sayısıdır; toplam boş bellek parçalanmış bir heap'te bile iyi görünür.

2. Sekiz saatlik testi kanıt saymak. Bu arıza sınıfının zaman sabiti gün veya haftadır. Kısa testin tek meşru kullanımı, hızlandırılmış ve tekrar oynatılan bir ayırma izidir.

3. malloc_trim()'i çare sanmak. Ana arena yalnızca tepeden kırpılır; ortadaki delikler yerinde kalır. Kırpma bir raporlama aracıdır, bir tedavi değil.

4. Değişken boyutlu ayırmayı sıcak yola koymak. Kare başına, mesaj başına, döngü başına malloc — parçalanma üretecinin tanımıdır. Sıcak yol havuzdan veya halka tampondan beslenmelidir.

5. Çekirdek tarafına hiç bakmamak. Kullanıcı alanı heap'i tertemizken CMA veya buddy ayırıcısı tükenmiş olabilir; dört katmanın dördü de izlenmelidir.

6. RTOS tarafında başarısız ayırmayı sessiz bırakmak. Kanca kapalıysa arıza, kök nedenden uzakta ve anlamsız bir yerde görünür.

7. Desenin ürün ömrü boyunca sabit kalacağını varsaymak. Yeni bir özellik, yeni bir ayırma ömrü demektir. Parçalanma bütçesi de tıpkı gecikme bütçesi gibi sürüm sürüm doğrulanır.

Karar kriteri

Bu kod yolu kaçırılan bir teslim tarihinde fiziksel sonuç üretiyor mu? Evetse dinamik ayırma yoktur: her şey başlatmada ayrılır, havuzlar sabit boyutludur, kapasite derleme zamanında bellidir. MISRA C:2012 Kural 21.3 ile "Power of Ten" üçüncü kuralının buluştuğu nokta burasıdır.

Dinamik ayırma gerçekten zorunlu ve yol gerçek zamanlı mı? O zaman sınırlı en kötü durum sunan bir ayırıcı seçin; TLSF bu iş için tasarlanmıştır. Ortalama süreyi değil, sınırı savunun.

Uzun ömürlü bir Linux servisi mi? Ayırıcıyı ayarlayın ve sınırlayın: M_ARENA_MAX bire sabit, dinamik mmap eşiği kapalı, süreç cgroup limiti içinde, memory.events ve PSI telemetride. Sonra günler boyunca in-use ile RSS eğrilerini yan yana izleyin; ayrışıyorlarsa cevabınız var.

Üçünün ortak paydası şudur: parçalanma bir sürpriz değil, ölçülmemiş bir bütçedir. Bütçeyi ilk günden koyan ekipler bu arızayı hiç görmez; koymayanlar onu garanti süresinin bittiği ay öğrenir.

Kendi cihazınızda bu bütçeyi kurmak veya sahadaki bir geç çöküşün kök nedenini ayrıştırmak istiyorsanız, ayırma deseni haritası ve izleme planı birlikte çıkarılır: hard real-time ve RTOS yetkinliğimiz bu iki çıktıyla başlar, kapsamı belirlemek için gömülü sistem mimari denetimi planlayabilirsiniz.

Kaynaklar

  1. Linux man-pages — mallopt(3)https://man7.org/linux/man-pages/man3/mallopt.3.html
  2. Linux man-pages — proc(5)https://man7.org/linux/man-pages/man5/proc.5.html
  3. Linux man-pages — malloc_info(3)https://man7.org/linux/man-pages/man3/malloc_info.3.html
  4. Linux çekirdek dokümantasyonu — Control Group v2https://docs.kernel.org/admin-guide/cgroup-v2.html
  5. Linux çekirdek dokümantasyonu — Documentation for /proc/sys/vmhttps://docs.kernel.org/admin-guide/sysctl/vm.html
  6. J. M. Robson — Bounds for Some Functions Concerning Dynamic Storage Allocation, Journal of the ACM 21(3), 1974 — https://dl.acm.org/doi/10.1145/321832.321846
  7. P. R. Wilson, M. S. Johnstone, M. Neely, D. Boles — Dynamic Storage Allocation: A Survey and Critical Review, IWMM 1995 — https://link.springer.com/chapter/10.1007/3-540-60368-9_19
  8. M. Masmano, I. Ripoll, A. Crespo, J. Real — TLSF: A New Dynamic Memory Allocator for Real-Time Systems, ECRTS 2004 — http://www.gii.upv.es/tlsf/
  9. FreeRTOS — Memory Managementhttps://www.freertos.org/a00111.html
  10. Zephyr Project — Memory Heaps and Memory Slabshttps://docs.zephyrproject.org/latest/kernel/memory_management/heap.html
  11. G. J. Holzmann — The Power of 10: Rules for Developing Safety-Critical Code, IEEE Computer, 2006 — https://spinroot.com/gerard/pdf/P10.pdf
  12. MISRA — MISRA C:2012, Kural 21.3 — https://misra.org.uk/
  13. jemalloc — jemalloc(3)https://jemalloc.net/jemalloc.3.html

Kaynaklara erişim tarihi: 5 Eylül 2026. glibc, çekirdek ve RTOS belgeleri sürüm alır; varsayılan değerler ve dosya yolları kullandığınız sürümün belgesinden doğrulanmalıdır.