সরাসরি পর্বে যান
পর্ব ৩৮ · ৬৪-এর মধ্যে · মডিউল ৫

Linux-এ মেমরি — malloc, mmap, slab, huge page আর OOM killer

Memory in Real Linux — VMAs and smaps, malloc's brk and mmap Paths, the Buddy and Slab Allocators, Page Cache, Huge Pages, Overcommit and the OOM Killer

Taught in Bangla, this lesson examines memory in real Linux: VMAs, /proc maps and smaps, malloc's brk and mmap paths, VSZ versus RSS, arenas and why free() keeps RAM, zones and the buddy allocator, slab and SLUB, page cache and free versus available, swap, huge pages, THP and NUMA, overcommit, the OOM killer and cgroups, finding memory leaks, and Windows and macOS comparisons.

এই পর্বে যা শিখবেন

  • /proc/PID/maps আর smaps পড়ে একটি process-এর মেমরি বুঝতে, আর VSZ, RSS, PSS, USS হাতে হিসাব করতে
  • glibc malloc কখন brk আর কখন mmap ব্যবহার করে, arena কী, আর free() কেন সব সময় RAM ফেরত দেয় না — আসল প্রোগ্রাম চালিয়ে
  • Kernel-এর zone, buddy allocator, slab/SLUB, kmalloc আর vmalloc — আর /proc/buddyinfo থেকে fragmentation পড়তে
  • Page cache, dirty page, swap, swappiness, zswap/zram, huge page, THP আর NUMA-র বাস্তব ব্যবহার
  • Overcommit, OOM killer-এর বাছাই, oom_score_adj, cgroup v2-এর memory.max/memory.high, আর leak খোঁজার টুল — Windows আর macOS-এর সাথে তুলনাসহ

১ · একটি process মেমরিকে কীভাবে দেখে — VMA, maps আর smaps

পর্ব ৩০ থেকে ৩৭-এ আপনি তত্ত্ব দেখেছেন: address binding (পর্ব ৩০), buddy system (পর্ব ৩১), paging (পর্ব ৩২), TLB (পর্ব ৩৩), segmentation (পর্ব ৩৪), virtual memory, demand paging আর copy-on-write (পর্ব ৩৫), page replacement (পর্ব ৩৬), thrashing (পর্ব ৩৭)। এই পর্বে সেই টুকরোগুলো জোড়া লাগবে একটি আসল Linux যন্ত্রে — তত্ত্ব আবার শেখানো হবে না, আগের পর্বের নম্বর বলা থাকবে।

ঢাকার একটি বড় অফিস ভবনের কথা ভাবুন। ভাড়াটে কোম্পানি চুক্তিপত্রে লিখে নেয় “আমাদের পাঁচতলার পুরো ফ্লোর” — এটি তার প্রতিশ্রুত জায়গা। কিন্তু আসবাব বসেছে মাত্র কয়েকটি কামরায়; বাকি কামরা খালি, তালা দেওয়া। আবার নিচতলার ক্যান্টিন আর লিফট সব ভাড়াটে মিলে ব্যবহার করে। ম্যানেজার (kernel) সব খাতায় লিখে রাখেন। Process-এর মেমরিও ঠিক এভাবে হিসাব হয়: চুক্তির মাপ VSZ, আসবাব বসা কামরা RSS, আর ভাগের ক্যান্টিনের খরচ ভাগ করে ধরলে PSS.

VMA আর mm_struct — kernel-এর খাতা

Linux kernel প্রতিটি process-এর address space একটি mm_struct-এ রাখে (একই process-এর সব thread একটিই ভাগ করে)। এর ভেতরে থাকে page table-এর শিকড় (x86-এ CR3-এ যা বসে, পর্ব ০৯), heap আর stack-এর সীমা, আর VMA-র তালিকা. একটি VMA (struct vm_area_struct) হলো address space-এর একটানা একটি টুকরো, যার অনুমতি আর উৎস এক — যেমন “libc-এর কোড, পড়া আর চালানো যায়” বা “heap, পড়া আর লেখা যায়”.

struct vm_area_struct {
    unsigned long vm_start, vm_end;   /* [start, end) — page-এর সীমায় */
    struct mm_struct *vm_mm;          /* কোন address space-এর অংশ */
    pgprot_t vm_page_prot;            /* page-এর অনুমতি */
    unsigned long vm_flags;           /* VM_READ, VM_WRITE, VM_EXEC, VM_SHARED ... */
    struct file *vm_file;             /* ফাইল-map হলে ফাইল, anonymous হলে NULL */
    unsigned long vm_pgoff;           /* ফাইলের কোন page থেকে শুরু */
};

struct mm_struct {
    struct maple_tree mm_mt;          /* সব VMA (৬.১ থেকে maple tree; আগে rbtree + list) */
    pgd_t *pgd;                       /* সবার ওপরের page table */
    unsigned long start_brk, brk;     /* heap-এর শুরু আর বর্তমান শেষ */
    unsigned long start_stack;
    /* RSS-এর হিসাব: ফাইল, anonymous, swap আর shared-memory page-এর কাউন্টার */
};

একটি VMA থাকা মানে RAM পাওয়া নয়। VMA শুধু বলে “এই ঠিকানাগুলো বৈধ”. প্রথমবার ছুঁলে page fault হয়, kernel VMA দেখে বোঝে ছোঁয়াটি বৈধ, তারপর একটি frame জোগাড় করে page table-এ বসায় (পর্ব ৩৫)। VMA-র বাইরে ছুঁলে আসে SIGSEGV (পর্ব ১৩)।

/proc/PID/maps — VMA-র তালিকা পড়া

পর্ব ১১-এ দেখা /proc-এ প্রতিটি process-এর maps ফাইল তার VMA-গুলো ছাপে। একটি ছোট C প্রোগ্রামের সাধারণ চেহারা (উদাহরণ; ঠিকানা প্রতি রানে ASLR-এর কারণে বদলায়):

55d0c3a00000-55d0c3a01000 r--p 00000000 08:01 393228   /home/user/a.out
55d0c3a01000-55d0c3a02000 r-xp 00001000 08:01 393228   /home/user/a.out
55d0c3a03000-55d0c3a04000 rw-p 00002000 08:01 393228   /home/user/a.out
55d0c4e5f000-55d0c4e80000 rw-p 00000000 00:00 0        [heap]
7f3a1c000000-7f3a1c021000 rw-p 00000000 00:00 0
7f3a1c400000-7f3a1c428000 r--p 00000000 08:01 1835106  /usr/lib/x86_64-linux-gnu/libc.so.6
7f3a1c428000-7f3a1c5bd000 r-xp 00028000 08:01 1835106  /usr/lib/x86_64-linux-gnu/libc.so.6
7ffd5e8c0000-7ffd5e8e1000 rw-p 00000000 00:00 0        [stack]
7ffd5e9f8000-7ffd5e9fa000 r-xp 00000000 00:00 0        [vdso]

কলামগুলো: ঠিকানার পরিসর; অনুমতি (r/w/x, আর শেষে p = private copy-on-write বা s = shared); ফাইলের offset; device; inode; আর নাম। নামহীন লাইন (পঞ্চম লাইন) হলো anonymous memory — বড় malloc বা thread-এর stack সাধারণত এমন দেখায়। লক্ষ করুন [heap]-এর আকার 0x55d0c4e80000 − 0x55d0c4e5f000 = 0x21000 = ১৩২ KiB — glibc প্রথম malloc-এ সাধারণত heap এতটাই বাড়ায় (অংশ ২)। কোড অংশ r-xp, লেখা যায় না; ডেটা অংশ rw-p, চালানো যায় না — পর্ব ৪৮-এর W^X সুরক্ষার ভিত্তি এটিই।

smaps — প্রতিটি VMA-র RAM-এর হিসাব

/proc/PID/smaps একই VMA-গুলোর প্রত্যেকটির জন্য বিস্তারিত হিসাব দেয়। উপরের libc-এর প্রথম VMA-র একটি নমুনা (উদাহরণ):

7f3a1c400000-7f3a1c428000 r--p 00000000 08:01 1835106  /usr/lib/x86_64-linux-gnu/libc.so.6
Size:                160 kB
Rss:                 160 kB
Pss:                   5 kB
Shared_Clean:        160 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Anonymous:             0 kB
Swap:                  0 kB
  • Size — VMA-র মাপ (এখানে 160 kB). Rss — এর মধ্যে কতটা এখন RAM-এ।
  • Pss — প্রতিটি page-কে যতজন ভাগ করে, ততভাগে ভাগ করে যোগ। 160 kB-কে মোটামুটি ৩২টি process ভাগ করছে, তাই এই process-এর ভাগে ৫ kB.
  • Shared/Private × Clean/Dirty — page অন্য কেউ ব্যবহার করছে কি না, আর ডিস্কের কপি থেকে বদলেছে কি না। Clean page ফেলে দিয়ে পরে ফাইল থেকে আবার পড়া যায়; Private_Dirty anonymous page ফেলতে হলে swap-এ লিখতে হয়।
  • Swap — এই VMA-র কতটা এখন swap-এ। পুরো process-এর যোগফল একবারে চাইলে /proc/PID/smaps_rollup (Linux ৪.১৪ থেকে)।

VSZ, RSS, PSS আর USS — চারটি মাপ, চারটি প্রশ্ন

মাপকী গোনেকোথায় দেখবেনকোন প্রশ্নের উত্তর
VSZ (VIRT)সব VMA-র মোট আকারps-এর VSZ, top-এর VIRT, VmSizeকতটা ঠিকানা প্রতিশ্রুত — RAM-এর সাথে প্রায় সম্পর্কহীন
RSS (RES)RAM-এ থাকা সব page, ভাগেরগুলো পুরো ধরেps-এর RSS, top-এর RES, VmRSSএই process কতটা RAM ছুঁয়ে আছে — কিন্তু সবার RSS যোগ করলে ভাগের page বারবার গোনা হয়
PSSনিজস্ব page + ভাগের page ÷ ভাগীদারের সংখ্যাsmaps_rollup, smemমোট RAM-এর কতটা এই process-এর ঘাড়ে — সবার PSS যোগ ≈ ব্যবহৃত RAM
USSশুধু নিজস্ব (private) pagePrivate_Clean + Private_Dirtyprocess মারলে অন্তত কতটা RAM ফেরত আসবে
হিসাব — তিনটি process, একটি ভাগের library

A, B, C তিনটি process. প্রত্যেকের নিজস্ব anonymous মেমরি যথাক্রমে ১০, ২০ আর ৩০ MiB, সবটাই RAM-এ। তিনজনেই একটি ৬ MiB library ব্যবহার করে, যার পুরোটা RAM-এ। তাহলে A-এর RSS = ১০ + ৬ = ১৬ MiB, PSS = ১০ + ৬ ÷ ৩ = ১২ MiB, USS = ১০ MiB. তিনজনের RSS-এর যোগ ১৬ + ২৬ + ৩৬ = ৭৮ MiB, অথচ আসলে RAM লেগেছে ১০ + ২০ + ৩০ + ৬ = ৬৬ MiB — ঠিক তিনজনের PSS-এর যোগ (১২ + ২২ + ৩২ = ৬৬)। তাই “সব process-এর RSS যোগ করলে RAM-এর চেয়ে বেশি হচ্ছে কেন?” — উত্তর ভাগের page. Android-এর মেমরি হিসাবে PSS-ই প্রধান মাপ।

২ · malloc কোথা থেকে মেমরি আনে — brk না mmap

malloc একটি system call নয় — এটি C library-র (Linux-এ সাধারণত glibc) একটি ফাংশন, যা user space-এ নিজের হিসাব রাখে। প্রতিটি ১৬ বাইটের অনুরোধে kernel-এ যাওয়া খুব ধীর হতো (পর্ব ০৫-এ system call-এর দাম দেখেছেন), তাই malloc kernel-এর কাছ থেকে বড় টুকরো আনে আর নিজে কেটে কেটে দেয়। বড় টুকরো আনার দুটি পথ:

🧱 brk / sbrk — মূল heap বাড়ানো

brk heap-এর শেষ সীমা (program break) ওপরে সরায়। glibc-এর মূল arena ছোট অনুরোধ এখান থেকে মেটায়। সমস্যা: heap একটানা, তাই কমানো যায় শুধু মাথা থেকে — মাথায় একটি ব্লক ব্যবহৃত থাকলে নিচের সব ফাঁকা জায়গা আটকে থাকে।

maps-এ [heap]

🗺️ mmap — আলাদা anonymous এলাকা

বড় অনুরোধের জন্য glibc সরাসরি mmap(MAP_PRIVATE | MAP_ANONYMOUS) করে একটি নতুন VMA বানায়। free() করলে munmap — মেমরি সাথে সাথে OS-এ ফেরে। দাম: প্রতিবার system call, page fault, আর kernel-এর শূন্য করা page.

maps-এ নামহীন লাইন

সীমারেখা M_MMAP_THRESHOLD: 64-bit glibc-এ শুরুর মান 128 KiB; অনুরোধ (হিসাবের কয়েক বাইটসহ) এর সমান বা বড় হলে mmap. কিন্তু আধুনিক glibc-এ (বহু বছর ধরে) এই সীমা dynamic: mmap করা একটি ব্লক free হলে glibc ধরে নেয় এই আকারের ব্লক বারবার আসবে, তাই threshold বাড়িয়ে সেই ব্লকের আকারে নেয় (সর্বোচ্চ 64-bit-এ 32 MiB পর্যন্ত), আর heap ছাঁটার সীমা (M_TRIM_THRESHOLD) করে তার দ্বিগুণ। mallopt() বা MALLOC_MMAP_THRESHOLD_ environment variable দিয়ে মান স্থির করলে এই স্বয়ংক্রিয় বদল বন্ধ হয়। (সঠিক খুঁটিনাটি glibc সংস্করণভেদে বদলাতে পারে — man mallopt দেখুন।)

নিচের stepper একটি ছোট প্রোগ্রামের ছয়টি ধাপ দেখায়, glibc-এর ডিফল্ট মানে (heap-এর আকারগুলো glibc-এর হিসাবের নিয়মে, বাস্তবে অন্য বরাদ্দ থাকলে একটু বদলাবে):

glibc malloc-এর অবস্থা
কোড
পথ
[heap]-এর আকার
আলাদা anonymous mmap
mmap threshold
trim threshold
pointer কোথায়
  1. প্রথম ছোট malloc. ১০০ বাইট threshold-এর অনেক নিচে, তাই heap-এর পথ। কিন্তু heap এখনো খালি, তাই glibc brk ডাকে — শুধু ১১২ বাইট (হিসাবের ৮ বাইট যোগ করে ১৬-এর গুণিতকে) নয়, সাথে ১২৮ KiB-এর বাড়তি প্যাড (M_TOP_PAD), page-এ গোল করে মোট ১৩২ KiB. বাকিটা “top chunk” হয়ে পরের অনুরোধের জন্য অপেক্ষা করে।
  2. ২০০ KiB — বড় পথ। ২০০ KiB ≥ ১২৮ KiB, তাই glibc heap ছোঁয় না; mmap করে ২০০ KiB + হিসাবের কয়েক বাইট, page-এ গোল করে ২০৪ KiB (৫১টি page)-এর একটি নতুন VMA. এখনো কোনো page ছোঁয়া হয়নি, তাই VSZ বেড়েছে, RSS নয়।
  3. free(p1) — kernel কিছুই জানে না। ছোট ব্লকটি thread-এর নিজস্ব tcache তালিকায় যায় — পরের একই আকারের malloc lock ছাড়াই এটি পাবে। heap ১৩২ KiB-ই থাকে।
  4. free(p2) — munmap আর threshold-এর লাফ। mmap করা ব্লক সরাসরি munmap হয়: VSZ আর RSS দুটোই কমে। সাথে glibc শিখে নেয়: “২০৪ KiB-এর ব্লক আসে আর যায়” — mmap threshold হয় ২০৪ KiB, trim threshold তার দ্বিগুণ ৪০৮ KiB.
  5. আবার ২০০ KiB — এবার heap! একই অনুরোধ এখন নতুন threshold-এর কম, তাই heap-এর পথ। top chunk-এ মাত্র ~১৩২ KiB আছে, তাই brk heap-কে আরও প্রায় ২০০ KiB বাড়ায় — মোট প্রায় ৩৩২ KiB. বারবার mmap/munmap আর শূন্য-page-এর page fault-এর খরচ বাঁচানোই উদ্দেশ্য।
  6. free(p3) — RAM থেকে গেল। ব্লকটি top chunk-এর সাথে মিশে প্রায় ৩৩২ KiB ফাঁকা হলো, কিন্তু তা trim threshold ৪০৮ KiB-এর কম, তাই glibc heap ছাঁটে না। প্রোগ্রামের চোখে মেমরি “ফেরত দেওয়া”, OS-এর চোখে এখনো process-এর — অংশ ৩-এ এর পুরো গল্প।

এবার আসল glibc-কে পরীক্ষা: প্রোগ্রামটি বিভিন্ন আকারের malloc করে /proc/self/maps-এ খোঁজে ঠিকানাটি কোন VMA-তে — ঠিকানা নয়, শুধু শ্রেণি ছাপে, তাই আউটপুট নিশ্চিত।

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static const char *region(const void *p) {
    static char res[32];
    char line[512], path[256];
    unsigned long lo, hi, a = (unsigned long)p;
    FILE *f = fopen("/proc/self/maps", "r");
    strcpy(res, "not found");
    while (f && fgets(line, sizeof line, f)) {
        path[0] = '\0';
        if (sscanf(line, "%lx-%lx %*s %*s %*s %*s %255s", &lo, &hi, path) >= 2 && a >= lo && a < hi) {
            strcpy(res, strcmp(path, "[heap]") == 0 ? "[heap]  (brk)" : path[0] ? path : "anonymous mmap");
            break;
        }
    }
    if (f) fclose(f);
    return res;
}

int main(void) {
    size_t kib[] = { 1, 64, 127, 129, 1024 };
    for (int i = 0; i < 5; i++) {
        char *p = malloc(kib[i] * 1024);
        printf("malloc(%5zu KiB) -> %s\n", kib[i], region(p));
        free(p);
    }
    char *q = malloc(129 * 1024);               /* the same 129 KiB request again */
    printf("again (  129 KiB) -> %s\n", region(q));
    free(q);
    return 0;
}

আউটপুট:

malloc(    1 KiB) -> [heap]  (brk)
malloc(   64 KiB) -> [heap]  (brk)
malloc(  127 KiB) -> [heap]  (brk)
malloc(  129 KiB) -> anonymous mmap
malloc( 1024 KiB) -> anonymous mmap
again (  129 KiB) -> [heap]  (brk)

(১) ১২৭ KiB পর্যন্ত heap, ১২৯ KiB থেকে আলাদা mmap — 128 KiB-এর সীমা ঠিক এখানে। ১২৭ KiB-এর সাথে হিসাবের কয়েক বাইট যোগ করেও সীমার নিচে থাকে। (২) শেষ লাইনটিই আসল শিক্ষা: একই ১২৯ KiB অনুরোধ দ্বিতীয়বার heap-এ গেল। কারণ ১২৯ KiB-এর mmap ব্লক free হওয়ার সময় threshold বেড়ে প্রায় ১৩২ KiB হয়েছে, আর ১০২৪ KiB-এর ব্লক free হওয়ার সময় আরও বেড়ে প্রায় ১ MiB — stepper-এর ধাপ ৪-এর নিয়ম। (৩) প্রোগ্রামটি কোনো ঠিকানা ছাপেনি, শুধু শ্রেণি; তাই ASLR থাকলেও প্রতি রানে একই ফল। (৪) এটি glibc-এর আচরণ; musl (Alpine Linux), jemalloc বা tcmalloc-এ সীমা আর নিয়ম আলাদা — Alpine-ভিত্তিক container-এ একই প্রোগ্রাম অন্য ফল দিতে পারে।

৩ · VSZ বনাম RSS, arena, আর free() কেন RAM ফেরত দেয় না

প্রতিশ্রুতি বনাম আসল RAM — demand paging চোখের সামনে

malloc যখন mmap করে, kernel শুধু একটি VMA বানায় — কোনো frame দেয় না। প্রথম লেখায় প্রতিটি page-এ একটি page fault, আর kernel একটি শূন্য করা frame বসায় (পর্ব ৩৫-এর demand paging). নিচের প্রোগ্রাম 32 MiB নেয়, প্রথমে অর্ধেক, পরে পুরোটা ছোঁয়, তারপর free করে — প্রতিবার /proc/self/status থেকে VmSize (VSZ) আর VmRSS-এর বদল ছাপে (পরম মান container-ভেদে বদলায়, বদল নয়)।

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static long status_kb(const char *key) {
    char line[256]; long kb = -1;
    size_t n = strlen(key);
    FILE *f = fopen("/proc/self/status", "r");
    while (f && fgets(line, sizeof line, f))
        if (strncmp(line, key, n) == 0) { sscanf(line + n, "%ld", &kb); break; }
    if (f) fclose(f);
    return kb;
}
static long vsz0, rss0;
static void show(const char *step) {
    long vsz = status_kb("VmSize:"), rss = status_kb("VmRSS:");
    printf("%-22s VSZ %+4ld MiB   RSS %+4ld MiB\n", step,
           (vsz - vsz0 + 512) / 1024, (rss - rss0 + 512) / 1024);
}

int main(void) {
    const size_t SZ = 32u << 20;                 /* 32 MiB */
    vsz0 = status_kb("VmSize:"); rss0 = status_kb("VmRSS:");
    printf("start: RSS > 0 kB? %s\n", rss0 > 0 ? "yes" : "no");
    char *p = malloc(SZ);
    if (!p) return 1;
    show("after malloc(32 MiB)");
    memset(p, 1, SZ / 2);                        /* touch the first half */
    show("after touching 16 MiB");
    memset(p, 1, SZ);                            /* touch all of it */
    show("after touching 32 MiB");
    free(p);
    show("after free()");
    return 0;
}

আউটপুট:

start: RSS > 0 kB? yes
after malloc(32 MiB)   VSZ  +32 MiB   RSS   +0 MiB
after touching 16 MiB  VSZ  +32 MiB   RSS  +16 MiB
after touching 32 MiB  VSZ  +32 MiB   RSS  +32 MiB
after free()           VSZ   +0 MiB   RSS   +0 MiB

চারটি লাইনেই পুরো গল্প। malloc-এর পর VSZ +৩২, RSS +০ — ঠিকানা প্রতিশ্রুত, RAM নয়। অর্ধেক ছোঁয়ায় RSS +১৬ — যতটুকু ছোঁয়া, ততটুকুই RAM; ৪০৯৬টি page fault. পুরো ছোঁয়ায় +৩২. free()-এর পর দুটোই ০ — ৩২ MiB-এর ব্লক ১২৮ KiB-এর threshold-এর অনেক ওপরে, তাই ব্লকটি mmap-এ ছিল আর munmap-এ সাথে সাথে ফেরত গেছে। (যন্ত্রে THP “always” থাকলে বড় এলাকায় kernel 2 MiB-এর টুকরোয় page দিতে পারে, তখন RSS ২ MiB-এর ধাপে বাড়তে পারে — অংশ ৭।) এই কারণেই top-এ VIRT বিশাল দেখে ভয় পাওয়ার কিছু নেই; দেখার মাপ RES আর PSS.

Arena — বহু thread-এর জন্য বহু খাতা

একটিমাত্র heap আর একটি lock হলে ১৬টি thread একসাথে malloc করলে সবাই lock-এর জন্য লাইনে দাঁড়াত (পর্ব ২৪)। তাই glibc একাধিক arena রাখে: প্রধান arena brk-এর heap ব্যবহার করে, বাকিগুলো mmap দিয়ে আনা ৬৪ MiB-এর “heap” এলাকা (64-bit-এ)। নতুন thread একটি arena পায়; arena-র সংখ্যার সর্বোচ্চ সীমা ডিফল্টভাবে 64-bit-এ CPU-সংখ্যার ৮ গুণ। তার ওপরে glibc ২.২৬ (২০১৭) থেকে প্রতিটি thread-এর নিজস্ব tcache — ছোট ব্লকের lock-ছাড়া তালিকা।

Container-এ arena-র ফাঁদ: বহু thread-এর একটি সার্ভিস (Java-র native অংশ, বড় C++ সার্ভার) প্রতিটি arena-য় কিছু ফাঁকা মেমরি ধরে রাখে, আর VSZ শত শত MiB বা GiB দেখায়; RSS-ও ধীরে বাড়তে থাকে, leak না থাকলেও। প্রচলিত প্রতিকার MALLOC_ARENA_MAX=2 বা ৪ — কম অপচয়, একটু বেশি lock-এর কাড়াকাড়ি; আগে-পরে মেপে বসান।

free() কেন RAM ফেরত দেয় না — আর malloc_trim

heap একটানা, আর brk শুধু মাথা থেকে ছাঁটতে পারে। লাখো ছোট ব্লকের মাঝে শেষ ব্লকটি ব্যবহৃত থাকলে নিচের সব ফাঁকা জায়গা malloc-এর ভেতরে আটকে থাকে — প্রোগ্রামের চোখে “free”, OS-এর চোখে RSS. এটি user space-এর external fragmentation. glibc ২.৮ থেকে malloc_trim মাঝের পুরো ফাঁকা page-গুলোও madvise(MADV_DONTNEED) দিয়ে kernel-কে ফেরত দেয় — ঠিকানা থাকে, frame যায়।

#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static long rss_kb(void) {
    char line[256]; long kb = -1;
    FILE *f = fopen("/proc/self/status", "r");
    while (f && fgets(line, sizeof line, f))
        if (strncmp(line, "VmRSS:", 6) == 0) { sscanf(line + 6, "%ld", &kb); break; }
    if (f) fclose(f);
    return kb;
}

enum { N = 40000, SZ = 1000 };                  /* 40000 small blocks, about 40 MB */
static char *blk[N];

int main(void) {
    long base = rss_kb();
    for (int i = 0; i < N; i++) { blk[i] = malloc(SZ); if (!blk[i]) return 1; memset(blk[i], 7, SZ); }
    long full = rss_kb();
    for (int i = 0; i < N - 1; i++) free(blk[i]);   /* keep only the LAST block */
    long freed = rss_kb();
    struct mallinfo2 mi = mallinfo2();
    malloc_trim(0);
    long trimmed = rss_kb();
    printf("RSS growth after 40000 x malloc(1000): %4ld MiB\n", (full - base) / 1024);
    printf("after freeing all but the last block : %4ld MiB\n", (freed - base) / 1024);
    printf("  free bytes kept inside malloc      : %4zu MiB\n", mi.fordblks >> 20);
    printf("after malloc_trim(0)                 : %4ld MiB\n", (trimmed - base) / 1024);
    free(blk[N - 1]);
    return 0;
}

আউটপুট:

RSS growth after 40000 x malloc(1000):   39 MiB
after freeing all but the last block :   39 MiB
  free bytes kept inside malloc      :   38 MiB
after malloc_trim(0)                 :    0 MiB

৪০,০০০ × ১০০০ বাইট (প্রতিটির সাথে হিসাবের বাইট মিলিয়ে ১০০৮) ≈ ৪০.৩ MB ≈ ৩৯ MiB heap-এ। ৩৯,৯৯৯টি free করার পরেও RSS একই ৩৯ MiB, অথচ mallinfo2() বলছে malloc-এর ভেতরে ৩৮ MiB ফাঁকা — শেষ ব্লকটি heap-এর মাথায় বসে brk-কে নামতে দিচ্ছে না। malloc_trim(0)-এর পর বৃদ্ধি ০ MiB: glibc মাঝের ফাঁকা page-গুলো kernel-কে ফেরত দিয়েছে। সংখ্যা এক-আধ MiB এদিক-ওদিক হতে পারে। শিক্ষা: “free করেছি, তবু RSS কমছে না” মানেই leak নয়; আবার malloc_trim সব রোগের ওষুধও নয় — এটি নিজেই সময় নেয়, আর পরের malloc-এ আবার page fault হবে।

এই কারণে বড় সার্ভিসগুলো প্রায়ই অন্য allocator ব্যবহার করে: jemalloc (Jason Evans, প্রথমে FreeBSD-র জন্য; Redis Linux-এ ডিফল্টভাবে এটি নিয়ে build হয়), Google-এর tcmalloc, Microsoft-এর mimalloc (২০১৯)। এগুলো অব্যবহৃত page নিয়মিত madvise দিয়ে ফেরত দেয়; কোনোটি সব workload-এ সেরা নয়। নিজের একটি সরল allocator বানাবেন পর্ব ৫৯-এর ক্যাপস্টোনে; C-র দিক থেকে দেখতে C কোর্সের memory allocator পর্ব।

৪ · Kernel-এর দিক — zone আর buddy allocator

malloc যখন brk বা mmap চায়, আর পরে page fault-এ frame লাগে, তখন kernel-কে physical মেমরি থেকে page বের করতে হয়। এই কাজের নিচতলা দুটি ধারণায় দাঁড়িয়ে: zone আর buddy allocator.

Zone — ঠিকানার পরিসর অনুযায়ী ভাগ

সব physical মেমরি সব কাজে সমান নয়। কিছু পুরোনো ডিভাইস DMA (পর্ব ০৪) দিয়ে শুধু প্রথম 16 MiB ছুঁতে পারত, আর শুধু 32-bit ঠিকানা বোঝে এমন ডিভাইস শুধু প্রথম 4 GiB. তাই Linux প্রতিটি NUMA node-এর মেমরিকে zone-এ ভাগ করে। x86-64-এ সাধারণত:

ZonePhysical পরিসরকার জন্য
ZONE_DMAপ্রথম 16 MiB২৪-bit ঠিকানার পুরোনো ISA-ধাঁচের ডিভাইস
ZONE_DMA3216 MiB – 4 GiBশুধু ৩২-bit ঠিকানা বোঝে এমন ডিভাইস
ZONE_NORMAL4 GiB-এর ওপরেবাকি সব — বেশিরভাগ user page আর kernel ডেটা
ZONE_MOVABLE (ঐচ্ছিক)কনফিগারেশন অনুযায়ীশুধু সরানো যায় এমন page — memory hotplug আর huge page-এর সুবিধার জন্য

32-bit x86-এ আরও একটি ZONE_HIGHMEM ছিল, 64-bit-এ নেই। প্রতিটি zone-এর তিনটি জলসীমা (watermark: min, low, high) — ফাঁকা page low-এর নিচে নামলে kswapd জেগে পেছনে page ফেরত আনে, min-এর নিচে নামলে allocation করা thread নিজেই reclaim করে (direct reclaim — latency বাড়ায়)। কোন page ফেরত আসবে, তা ঠিক করে active/inactive তালিকা বা Linux ৬.১-এর MGLRU (পর্ব ৩৬)।

Buddy allocator — Linux-এর page allocator

পর্ব ৩১-এ buddy system-এর তত্ত্ব দেখেছেন। Linux-এ প্রতিটি zone-এর free_area[]-এ প্রতিটি order-এর জন্য একটি ফাঁকা-তালিকা: order n মানে ২nটি একটানা page. সাধারণ kernel-এ order ০ থেকে ১০ — 4 KiB থেকে 4 MiB. (Kernel-এর ধ্রুবকটি আগে ছিল MAX_ORDER = 11, অর্থাৎ “সর্বোচ্চ order-এর এক বেশি”; ৬.৪-এ অর্থ বদলে MAX_ORDER = 10 হয়, আর ৬.৮-এ নাম হয় MAX_PAGE_ORDER; ফল একই — ১১টি তালিকা।) একটি ব্লকের buddy খুঁজতে শুধু একটি XOR: page frame number (pfn) ⊕ ২order.

পুরান ঢাকার কাপড়ের পাইকারি দোকানে থানের কাপড় থাকে ৩২ গজ, ১৬ গজ, ৮ গজ… মাপে ভাঁজ করা। কেউ ৩ গজ চাইলে দোকানি ৪ গজের টুকরো দেন (১ গজ বাড়তি — internal fragmentation). ৪ গজের টুকরো না থাকলে ৮ গজের একটি অর্ধেক কেটে এক টুকরো দেন, অন্যটি ৪ গজের তাকে রাখেন। ফেরত আসা টুকরোর জোড়াও তাকে থাকলে দুটি আবার একসাথে ভাঁজ হয়ে বড় তাকে ওঠে।

নিচে একটি ছোট zone, ১৬টি page (pfn ০–১৫), order ০ থেকে ৪. A চায় ৩টি page, B চায় ১টি; তারপর দুজনেই ফেরত দেয়।

একটি zone-এর free_area (১৬টি page)
ঘটনা
order 4 (১৬ page)
order 3 (৮ page)
order 2 (৪ page)
order 1 (২ page)
order 0 (১ page)
buddyinfo-র ধাঁচে (order 0…4)
page-এর মানচিত্র
  1. শুরু — একটিই বড় ব্লক। ১৬টি page একসাথে order 4-এর তালিকায়: ০–১৫. buddyinfo-র সারিতে order 4-এর ঘরে ১, বাকি সব ০.
  2. A চায় ৩ page — গোল করে order 2. ৩-এর পরের ২-এর ঘাত ৪, তাই order 2. order 2 আর order 3-এর তালিকা খালি, order 4-এ একটি ব্লক আছে। সেটি ভাঙা হলো: ০–৭ আর ৮–১৫; ৮–১৫ গেল order 3-এর তালিকায়, ০–৭ হাতে রইল আরও ভাঙার জন্য।
  3. আবার ভাঙা, A পেল ০–৩. ০–৭ ভেঙে ০–৩ আর ৪–৭; ৪–৭ গেল order 2-এর তালিকায়, ০–৩ দেওয়া হলো A-কে। A চেয়েছিল ৩টি, পেল ৪টি — একটি page internal fragmentation-এ নষ্ট।
  4. B চায় ১ page. order 0 আর 1 খালি, order 2-এ ৪–৭ আছে। ৪–৭ ভেঙে ৪–৫ আর ৬–৭ (৬–৭ গেল order 1-এ), ৪–৫ ভেঙে ৪ আর ৫ (৫ গেল order 0-এ)। B পেল page ৪. buddyinfo এখন 1 1 0 1 0.
  5. A ফেরত দিল — জোড়া লাগল না। ০–৩ (order 2)-এর buddy: ০ ⊕ ৪ = ৪, অর্থাৎ ৪–৭. কিন্তু page ৪ B-এর হাতে, তাই ৪–৭ পুরো ফাঁকা নয়। ০–৩ একাই order 2-এর তালিকায়। এখন ১৬-এর ১৫টি page ফাঁকা, তবু সবচেয়ে বড় একটানা ব্লক ৮ page — order 4-এর (১৬ page) একটি অনুরোধ এলে ব্যর্থ বা অপেক্ষা। মাঝখানে একটি page আটকে থাকাই external fragmentation.
  6. B ফেরত দিল — চার ধাপে জোড়া। page ৪-এর buddy ৪ ⊕ ১ = ৫ ফাঁকা → ৪–৫ (order 1); তার buddy ৪ ⊕ ২ = ৬, অর্থাৎ ৬–৭ ফাঁকা → ৪–৭ (order 2); তার buddy ৪ ⊕ ৪ = ০, অর্থাৎ ০–৩ ফাঁকা → ০–৭ (order 3); তার buddy ০ ⊕ ৮ = ৮, অর্থাৎ ৮–১৫ ফাঁকা → ০–১৫ (order 4). আবার একটিই বড় ব্লক — buddy-র শক্তি এই দ্রুত জোড়ায়।

আসল যন্ত্রে এই তালিকাগুলোর গণনা /proc/buddyinfo-এ (অংশ ৬-এর bash ব্লকে চালানো হবে): প্রতিটি সারি একটি node আর zone, ১১টি সংখ্যা order ০ থেকে ১০-এর ফাঁকা ব্লক। ডানের সংখ্যা যত কম, fragmentation তত বেশি — huge page (order 9 = 2 MiB) তখন সহজে মেলে না, আর kernel compaction চালিয়ে সরানো যায় এমন page সরিয়ে ফাঁকা জায়গা একত্র করে। Fragmentation কমাতে Linux “movable” আর “unmovable” page আলাদা page-block-এ রাখারও চেষ্টা করে।

৫ · Slab আর SLUB — kernel-এর ছোট বস্তুর জন্য

Buddy-র সবচেয়ে ছোট একক 4 KiB. কিন্তু kernel-এর বেশিরভাগ বস্তু ছোট: একটি struct file কয়েকশো বাইট, একটি dentry ২০০-র কাছাকাছি, একটি task_struct কয়েক KiB. প্রতিটির জন্য পুরো page দিলে বিপুল অপচয়, আর প্রতিবার শুরু থেকে বস্তু সাজাতেও সময় যায়। সমাধান slab allocator: প্রতিটি বস্তু-ধরনের জন্য একটি cache; cache buddy থেকে কয়েকটি page (একটি “slab”) নেয়, সেটিকে সমান মাপের ঘরে কাটে, আর ফাঁকা ঘরের তালিকা রাখে।

Jeff Bonwick আর ১৯৯৪: Sun Microsystems-এর Jeff Bonwick ১৯৯৪ সালের USENIX সম্মেলনে SunOS ৫.৪-এর জন্য slab allocator-এর পত্র প্রকাশ করেন। মূল ধারণা: বস্তুর cache, আর ফেরত আসা বস্তুকে “সাজানো অবস্থায়” রেখে আবার দেওয়া। Linux ২.২-এর সময় থেকে এই ধারণা নেয়। পরে Christoph Lameter-এর SLUB (২.৬.২২, ২০০৭) কাঠামো সরল করে — জটিল সারির বদলে প্রতি-CPU-র একটি সক্রিয় slab আর ফাঁকা ঘরের তালিকা ঘরের ভেতরেই — আর পরে ডিফল্ট হয়। ছোট যন্ত্রের SLOB ৬.৪-এ আর পুরোনো SLAB ৬.৮-এ (২০২৪) সরানো হয়; এখন SLUB-ই একমাত্র।

সব slab cache-এর হিসাব /proc/slabinfo-এ, যা সাধারণত শুধু root পড়তে পারে; সাজানো চেহারায় দেখায় slabtop:

$ sudo slabtop -o -s c | head -9
 Active / Total Objects (% used)    : 2147310 / 2210468 (97.1%)
 Active / Total Slabs (% used)      : 61204 / 61204 (100.0%)
 Active / Total Caches (% used)     : 128 / 182 (70.3%)
 Active / Total Size (% used)       : 512340.18K / 528911.50K (96.9%)

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME
 399126 398870  99%    0.19K  19006       21     76024K dentry
 181440 180912  99%    0.57K   6480       28    103680K radix_tree_node
 118290 117801  99%    1.06K   3943       30    126176K ext4_inode_cache

এখানে dentry (পর্ব ৪২) আর inode cache-ই বড়; মেমরির চাপে kernel এগুলোর বড় অংশ ফেরত আনতে পারে। /proc/meminfo-র SReclaimable আর SUnreclaim এই দুই ভাগ দেখায়। SUnreclaim যদি দিনের পর দিন বাড়তেই থাকে, তা kernel বা driver-এর leak-এর লক্ষণ হতে পারে।

kmalloc বনাম vmalloc

বিষয়kmallocvmalloc
Physical-ভাবে একটানা?হ্যাঁনা — আলাদা আলাদা page, শুধু virtual-ভাবে একটানা
ভেতরে কীআকার-ভিত্তিক slab cache (kmalloc-64, kmalloc-128…), বড় হলে সরাসরি buddybuddy থেকে একটি একটি page, তারপর kernel-এর vmalloc এলাকায় নতুন page table entry
গতিদ্রুতধীর; TLB-তেও বেশি entry লাগে
কখনপ্রায় সব ছোট বস্তু, DMA bufferবড় buffer, যেখানে একটানা physical মেমরি পাওয়া কঠিন (module লোড, বড় টেবিল)

kvmalloc() আগে kmalloc চেষ্টা করে, না পেলে vmalloc. Windows kernel-এর সমতুল্য paged আর non-paged pool.

৬ · Page cache, “free” বনাম “available”, আর swap

Linux-এর একটি বিখ্যাত ভুল-বোঝাবুঝি: “আমার ১৬ GiB RAM-এর মাত্র ৫০০ MiB ফাঁকা — মেমরি শেষ!” প্রায় সব ক্ষেত্রে উত্তর: না। Kernel ফাঁকা RAM ফেলে রাখে না; ডিস্ক থেকে পড়া ফাইলের ডেটা page cache-এ রেখে দেয়, যাতে আবার পড়লে ডিস্কে যেতে না হয় (পর্ব ০৪-এর memory hierarchy মনে করুন)। দরকার হলে পরিষ্কার cache-page সাথে সাথে ফেলে দেওয়া যায়।

লাইব্রেরির পড়ার টেবিলে লাইব্রেরিয়ান সারাদিন যে বইগুলো বারবার চাওয়া হয়, সেগুলো তাক থেকে নামিয়ে টেবিলে রেখে দেন। টেবিল “ভরা” দেখায়, কিন্তু নতুন পাঠক এলে তিনি এক মুহূর্তে কয়েকটি বই তাকে তুলে জায়গা করে দেন। টেবিল ফাঁকা রাখলে কোনো লাভ ছিল না — বরং প্রতিবার তাকে হাঁটতে হতো। শুধু যে বইয়ে কেউ নোট লিখছেন (dirty page), সেটি তোলার আগে নোট খাতায় তুলতে হয়।

$ free -h
               total        used        free      shared  buff/cache   available
Mem:            15Gi       5.1Gi       512Mi       820Mi       9.8Gi        9.6Gi
Swap:          4.0Gi       128Mi       3.9Gi

free মাত্র ৫১২ MiB, কিন্তু available ৯.৬ GiB — নতুন কাজকে swap ছাড়াই মোটামুটি এতটা দেওয়া যাবে। মানটি আসে /proc/meminfo-র MemAvailable থেকে, যা Linux ৩.১৪ (২০১৪)-এ যোগ হয়: ফাঁকা RAM (জলসীমা বাদে) + ফেলে দেওয়া যায় এমন page cache-এর অংশ + ফেরতযোগ্য slab-এর অংশ — একটি অনুমান, নিখুঁত সংখ্যা নয়। মনিটরিংয়ে “MemFree কম” নয়, “MemAvailable কম আর কমছে”, swap-এর কার্যকলাপ আর PSI-এর মেমরি চাপ (পর্ব ৩৭) দেখুন।

Dirty page আর writeback

ফাইলে write() সাধারণত সরাসরি ডিস্কে যায় না — page cache-এর page বদলে dirty চিহ্ন পায়, আর পরে kernel-এর flusher thread দলবেঁধে লেখে (writeback). কতটা dirty জমতে দেওয়া হবে, তা vm.dirty_background_ratio আর vm.dirty_ratio ঠিক করে — দ্বিতীয়টি ছাড়ালে লিখতে থাকা process-কেই থামানো হয়। তাই USB drive-এ কপি “শেষ” দেখানোর পরেও নিরাপদে সরাতে sync/eject দরকার; ডেটাবেস নিশ্চয়তার জন্য fsync() ডাকে (পর্ব ৪৪)।

drop_caches — বুঝুন, কিন্তু অভ্যাস করবেন না

echo 3 > /proc/sys/vm/drop_caches (root) পরিষ্কার page cache আর ফেরতযোগ্য slab বস্তু (dentry, inode) ফেলে দেয়; ১ শুধু page cache, ২ শুধু slab. Dirty page ফেলে না, তাই আগে sync. এর সৎ ব্যবহার একটিই: benchmark-এ “ঠান্ডা cache” তৈরি। চলমান সার্ভারে নিয়মিত চালালে সব ফাইল আবার ডিস্ক থেকে পড়তে হয়, গতি কমে, আর আসল সমস্যা ঢাকা পড়ে।

Swap, swappiness, zswap আর zram

RAM-এর চাপে kernel দুই ধরনের page ফেরত আনতে পারে: ফাইল-page (পরিষ্কার হলে ফেলে দেওয়া, পরে আবার পড়া) আর anonymous page (swap-এ লেখা ছাড়া উপায় নেই)। কোনটির দিকে কতটা ঝুঁকবে, তা vm.swappiness দিয়ে বলা যায় — Linux ৫.৮ থেকে ০–২০০, বেশিরভাগ distro-তে ডিফল্ট সাধারণত ৬০. ০ মানে “swap একেবারে বন্ধ” নয়, শুধু anonymous page-কে অনেক কম পছন্দ; ১০০ মানে দুই ধরনকে সমান দাম; ১০০-র ওপরে swap-কে বেশি পছন্দ — দ্রুত zram-এর জন্য অর্থবহ। কোন মান ভালো, তা workload আর storage-এর গতির ওপর নির্ভর করে; অন্ধ নিয়ম নেই।

  • zswap (Linux ৩.১১ থেকে): swap-এ লেখার আগে page সংকুচিত করে RAM-এর একটি ছোট pool-এ রাখে; pool ভরলে তবেই আসল swap ডিভাইসে। ডিস্কে লেখা অনেক কমে।
  • zram: RAM-এর ভেতরেই একটি সংকুচিত block device, যাকে swap হিসেবে ব্যবহার করা হয় — ডিস্ক নেই। Android আর ChromeOS-এ বহুল ব্যবহৃত, আর Fedora ৩৩ থেকে ডিফল্ট swap হিসেবে zram ব্যবহার করে।

এবার এই অনলাইন রানারের যন্ত্রটি (একটি container) নিজেই দেখি — meminfo, buddyinfo, THP-র নীতি, overcommit, আর এই shell-এর OOM হিসাব। কোনো ফাইল না থাকলে script থামে না, শুধু জানায়:

echo "== /proc/meminfo (selected) =="
grep -E '^(MemTotal|MemFree|MemAvailable|Cached|Dirty|SwapTotal|SwapFree|HugePages_Total|HugePages_Free|Hugepagesize|AnonHugePages|CommitLimit|Committed_AS):' /proc/meminfo
echo "== /proc/buddyinfo (free blocks of order 0..10) =="
if [ -r /proc/buddyinfo ]; then cat /proc/buddyinfo; else echo "not readable here"; fi
echo "== THP =="
for f in enabled defrag; do
  p=/sys/kernel/mm/transparent_hugepage/$f
  if [ -r "$p" ]; then echo "$f: $(cat "$p")"; else echo "$f: not available"; fi
done
echo "== overcommit / swappiness =="
for f in overcommit_memory overcommit_ratio swappiness; do
  p=/proc/sys/vm/$f
  if [ -r "$p" ]; then echo "vm.$f = $(cat "$p")"; else echo "vm.$f: not readable"; fi
done
echo "== this shell =="
echo "oom_score=$(cat /proc/$$/oom_score) oom_score_adj=$(cat /proc/$$/oom_score_adj)"
for f in memory.max memory.high memory.current; do
  p=/sys/fs/cgroup/$f
  if [ -r "$p" ]; then echo "cgroup $f = $(cat "$p")"; else echo "cgroup $f: not readable"; fi
done

একটি আসল রানের আউটপুট:

== /proc/meminfo (selected) ==
MemTotal:        2008912 kB
MemFree:          161868 kB
MemAvailable:    1280060 kB
Cached:           835768 kB
SwapTotal:       6041596 kB
SwapFree:        5627256 kB
Dirty:              3232 kB
CommitLimit:     7046052 kB
Committed_AS:    1651516 kB
AnonHugePages:         0 kB
HugePages_Total:       0
HugePages_Free:        0
Hugepagesize:       2048 kB
== /proc/buddyinfo (free blocks of order 0..10) ==
Node 0, zone      DMA     90     64     43     31     24     13      3      3      2      1      0
Node 0, zone    DMA32   4665   2834   1016   1363    488    103     16      0      0      0      0
== THP ==
enabled: always [madvise] never
defrag: always defer defer+madvise [madvise] never
== overcommit / swappiness ==
vm.overcommit_memory = 0
vm.overcommit_ratio = 50
vm.swappiness = 60
== this shell ==
oom_score=666 oom_score_adj=0
cgroup memory.max = max
cgroup memory.high = max
cgroup memory.current = 1277952

free বনাম available: প্রায় ২ GiB-এর যন্ত্রে MemFree মাত্র ~১৫৮ MiB, কিন্তু MemAvailable ~১.২ GiB — কারণ ~৮১৬ MiB page cache (Cached). buddyinfo আর MemFree মেলে: এই যন্ত্রে RAM 4 GiB-এর কম, তাই Normal zone নেই, শুধু DMA আর DMA32. প্রতিটি ঘরে গণনা × ২order × ৪ KiB যোগ করলে DMA = ৩০৩৮ page = ১২১৫২ kB, DMA32 = ৩৭৪২৯ page = ১৪৯৭১৬ kB; মোট ১৬১৮৬৮ kB — ঠিক MemFree. আর DMA32-এ order 7 থেকে 10-এর ঘর শূন্য (সবচেয়ে বড় ব্লক order 6 = 256 KiB): ~১৪৬ MiB ফাঁকা থাকলেও একটি একটানা 2 MiB ব্লক (order 9) নেই — fragmentation চোখের সামনে; huge page চাইলে compaction লাগবে। (প্রতি রানে সংখ্যা বদলায়।)

THP-র নীতি [madvise] — বন্ধনীর ভেতরেরটিই চালু। Overcommit: vm.overcommit_memory = 0 (heuristic, অংশ ৮); CommitLimit = SwapTotal + MemTotal × ৫০% = ৬০৪১৫৯৬ + ১০০৪৪৫৬ = ৭০৪৬০৫২ kB — হুবহু মিলেছে, যদিও mode 0-তে সীমাটি শুধু দেখানো হয়, কড়াভাবে খাটানো হয় mode 2-তে। swappiness ৬০, ডিফল্ট। oom_score ৬৬৬ — অদ্ভুত সংখ্যাটির ব্যাখ্যা অংশ ৮-এর stepper-এ। আর এই container-এর cgroup-এ memory.max = max, অর্থাৎ আলাদা সীমা নেই।

৭ · Huge page, THP আর NUMA

কেন বড় page — TLB-র নাগাল

পর্ব ৩৩-এ দেখেছেন, প্রতিটি ঠিকানা অনুবাদের জন্য TLB দরকার; miss হলে চার স্তরের page table হাঁটতে হয়। TLB-র entry সীমিত, তাই প্রশ্ন: সব entry মিলিয়ে কতটা মেমরি ঢাকা যায় — একে বলে TLB reach.

হিসাব — TLB reach

ধরুন একটি CPU-র দ্বিতীয় স্তরের TLB-তে ১৫৩৬টি entry (আধুনিক x86 CPU-তে এমন মাপ দেখা যায়; মডেলভেদে আলাদা)। 4 KiB page-এ reach = ১৫৩৬ × ৪ KiB = ৬ MiB. একটি ডেটাবেস যদি ৬৪ GiB মেমরি এলোমেলোভাবে ছোঁয়, প্রায় প্রতিটি ছোঁয়ায় TLB miss. একই entry 2 MiB page-এ হলে reach = ১৫৩৬ × ২ MiB = ৩ GiB — ৫১২ গুণ। সাথে page table-ও ছোট হয়: 2 MiB page-এ শেষ স্তরটিই লাগে না। ৬৪ GiB ৪ KiB page-এ ঢাকতে ১৬৭,৭৭,২১৬টি PTE × ৮ বাইট = ১২৮ MiB page table; 2 MiB page-এ ৩২,৭৬৮টি entry, মাত্র ২৫৬ KiB.

দুই পথ: HugeTLBfs আর THP

📌 HugeTLBfs — আগে থেকে সংরক্ষিত

HugeTLBfs: admin আগে থেকে পুল বানান (vm.nr_hugepages, বা বুটে hugepages=); প্রোগ্রাম MAP_HUGETLB বা hugetlbfs ফাইল দিয়ে ব্যবহার করে। নিশ্চিত, swap হয় না, 1 GiB page-ও সম্ভব — কিন্তু পুলের মেমরি অন্য কাজে লাগে না। HugePages_Total উপরের রানে ০.

PostgreSQL huge_pages, Oracle, DPDK, VM

🪄 THP — kernel নিজে বসায়

Transparent Huge Pages (Andrea Arcangeli, Linux ২.৬.৩৮, ২০১১): anonymous মেমরির 2 MiB-সারিবদ্ধ এলাকায় page fault-এর সময় kernel একটি 2 MiB page দেওয়ার চেষ্টা করে; না পারলে ছোট page, আর পরে khugepaged পেছনে ৫১২টি ছোট page জোড়া লাগায়। AnonHugePages এর মাপ।

নীতি: always / madvise / never

THP-র দামও আছে। (১) Latency-র ঝাঁকুনি: 2 MiB ব্লক না থাকলে page fault-এর মধ্যেই compaction চলতে পারে (defrag নীতি তা ঠিক করে)। (২) মেমরি ফুলে ওঠা: প্রোগ্রাম 2 MiB এলাকার মাত্র কয়েক KiB ছুঁলেও পুরো 2 MiB RSS-এ আসতে পারে। (৩) fork-এর পর copy-on-write-এ বড় page কপি। তাই অনেক distro madvise নীতি নেয় — শুধু যে প্রোগ্রাম madvise(MADV_HUGEPAGE) চায়, সে পায়; কিছু distro always. ডেটাবেস নির্মাতাদের পরামর্শও এক নয় আর সময়ের সাথে বদলেছে: Redis শুরুতেই সতর্কবার্তা দেয় THP always থাকলে, Oracle আর অনেক ডেটাবেস ঐতিহ্যগতভাবে THP বন্ধ রেখে HugeTLBfs-এর পরামর্শ দিয়েছে, আবার কোনো কোনো নির্মাতা নতুন সংস্করণে THP চালু রাখতে বলছে। নিজের সংস্করণের ডকুমেন্টেশন দেখে, মেপে সিদ্ধান্ত নিন। Linux ৬.৮-এর “multi-size THP” (যেমন 64 KiB) ছবিটি আরও নমনীয় করছে।

NUMA — কাছের মেমরি, দূরের মেমরি

বড় সার্ভারে প্রতিটি CPU socket-এর নিজস্ব মেমরি থাকে (NUMA, পর্ব ২০); দূরের node-এর মেমরি ছুঁতে বেশি সময় লাগে। Linux-এর ডিফল্ট first-touch নীতি: একটি page যে CPU প্রথম ছোঁয়, তার node থেকে frame আসে। তাই একটি thread পুরো বড় array শূন্য করে দিলে সব মেমরি এক node-এ জমে, আর অন্য node-এর thread-গুলো দূরের মেমরিতে ধীরে চলে। সমাধান: প্রতিটি thread নিজের অংশ নিজে প্রথম ছোঁয়, অথবা numactl:

$ numactl --hardware | head -4
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 64215 MB
node 0 free: 1022 MB
$ numactl --cpunodebind=0 --membind=0 ./db-server   # এক node-এ বাঁধা
$ numactl --interleave=all ./big-cache             # সব node-এ পালা করে page
$ numastat -p db-server                            # কোন node-এ কত মেমরি

লক্ষ করুন, node 0-এ মাত্র ১ GB ফাঁকা — অন্য node-এ জায়গা থাকলেও একটি node-এ চাপ পড়লে reclaim শুরু হতে পারে। Linux পেছনে “automatic NUMA balancing” দিয়ে page আর task-কে কাছাকাছি আনার চেষ্টাও করে।

৮ · Overcommit, OOM killer আর cgroup — container কেন হঠাৎ মরে

Overcommit — ব্যাংকের মতো প্রতিশ্রুতি

একটি ব্যাংক সব গ্রাহকের জমা টাকা ভল্টে রাখে না; জানে, সবাই একদিনে টাকা তুলবে না। তাই জমার চেয়ে কম নগদ রেখে বাকিটা খাটায়। সাধারণ দিনে এটি দারুণ কৌশল। কিন্তু যেদিন সবাই একসাথে লাইনে দাঁড়ায়, ব্যাংককে কঠিন সিদ্ধান্ত নিতে হয়। Linux মেমরি নিয়ে ঠিক এমন ব্যাংক।

অংশ ৩-এ দেখেছেন, malloc শুধু ঠিকানা দেয়, RAM আসে ছোঁয়ার সময়। fork-এর পর copy-on-write (পর্ব ১০, ৩৫)-এ parent আর child-এর পুরো মেমরি “প্রতিশ্রুত”, অথচ বেশিরভাগ কখনো কপি হয় না। তাই Linux ডিফল্টভাবে overcommit করে — RAM আর swap মিলিয়ে যত আছে, তার চেয়ে বেশি প্রতিশ্রুতি (Committed_AS CommitLimit ছাড়াতে পারে)। vm.overcommit_memory-এর তিনটি মান:

  • ০ — heuristic (ডিফল্ট): স্পষ্টতই অসম্ভব অনুরোধ (যেমন একবারে মোট RAM + swap-এর চেয়ে বড়) প্রত্যাখ্যান, বাকি সব হ্যাঁ।
  • ১ — সবসময় হ্যাঁ: বড় sparse array-র কোড বা fork-ভিত্তিক snapshot নেওয়া Redis-এর মতো প্রোগ্রামের জন্য।
  • ২ — কড়া হিসাব: মোট প্রতিশ্রুতি CommitLimit = swap + RAM × overcommit_ratio% ছাড়ালে malloc/mmap ব্যর্থ (ENOMEM).

OOM killer — যখন প্রতিশ্রুতি রাখা যায় না

সবাই একসাথে মেমরি ছুঁলে, আর kswapd ও direct reclaim মিলেও কিছু ফেরত আনতে না পারলে, page fault মেটানোর মতো frame থাকে না। তখন kernel-এর OOM killer একটি process বেছে SIGKILL দেয়। বাছাইয়ের মূল ফাংশন oom_badness(), যার যুক্তি বর্তমান kernel-এ মোটামুটি এই (সংস্করণভেদে খুঁটিনাটি বদলেছে — যেমন পুরোনো kernel-এ root process-এর জন্য ছাড় ছিল, এখন নেই):

long oom_badness(struct task_struct *p, unsigned long totalpages)
{
    long points, adj = p->signal->oom_score_adj;

    if (adj == OOM_SCORE_ADJ_MIN || oom_unkillable_task(p))   /* -1000 */
        return LONG_MIN;                       /* কখনো বাছা হবে না */

    points = get_mm_rss(p->mm)                 /* RAM-এ থাকা page */
           + get_mm_counter(p->mm, MM_SWAPENTS) /* swap-এ থাকা page */
           + mm_pgtables_bytes(p->mm) / PAGE_SIZE;   /* page table-এর page */

    adj *= totalpages / 1000;                  /* adj-এর এক একক = মোট-এর ০.১% */
    return points + adj;                       /* সবচেয়ে বড় points-এর process মরবে */
}
/* totalpages = RAM + swap (cgroup-এর ভেতরে OOM হলে সেই cgroup-এর সীমা) */

oom_score_adj −১০০০ থেকে +১০০০; প্রতিটি একক মোট মেমরির ০.১%-এর সমান points যোগ বা বিয়োগ করে। −১০০০ মানে কখনো বাছা হবে না। এবার একটি numerical, ধাপে ধাপে: ৩ GiB RAM + ১ GiB swap = ৪ GiB = ১০,৪৮,৫৭৬টি 4 KiB page, তাই adj-এর এক একক = ১০,৪৮,৫৭৬ ÷ ১০০০ → পূর্ণসংখ্যায় ১০৪৮ page. (১ MiB = ২৫৬ page.)

চারটি process (একক: 4 KiB page)
ধাপ
postgres
browser-tab
sshd
java
  1. তথ্য। চারটি process: একটি ডেটাবেস (postgres), একটি ব্রাউজার ট্যাব যার adj +300 (কিছু ব্রাউজার পেছনের ট্যাবের adj বাড়িয়ে রাখে, যাতে আগে সেগুলোই মরে), SSH সার্ভার (systemd ইউনিটে adj −1000), আর একটি Java সার্ভিস যার admin adj −500 দিয়েছেন। RSS-এ সবচেয়ে বড় java.
  2. ভিত্তি points. MiB-কে ২৫৬ দিয়ে গুণ করে page: postgres ৩০৭২০০ + ২৫৬০০ + ১০২৪ = ৩৩৩৮২৪; browser-tab ২৩০৯১২; java ৪১১১৩৬ — এ পর্যন্ত java-ই এগিয়ে। sshd-এর হিসাবই হয় না।
  3. adj প্রয়োগ। এক একক = ১০৪৮ page. browser-tab: +৩০০ × ১০৪৮ = +৩১৪৪০০. java: −৫০০ × ১০৪৮ = −৫২৪০০০. postgres: ০. sshd-এর −১০০০ মানে LONG_MIN — তালিকা থেকেই বাদ।
  4. মোট points. postgres ৩৩৩৮২৪, browser-tab ২৩০৯১২ + ৩১৪৪০০ = ৫৪৫৩১২, java ৪১১১৩৬ − ৫২৪০০০ = −১১২৮৬৪ (ঋণাত্মকও হতে পারে)। সবচেয়ে বড় এখন browser-tab — RSS-এ যে সবচেয়ে ছোট, সে-ই প্রথম!
  5. বলি — SIGKILL. Kernel browser-tab-কে SIGKILL পাঠায় আর kernel log-এ লেখে। মৃত process-এর মেমরি দ্রুত ফেরত আনতে oom_reaper thread (৪.৬ থেকে) তার anonymous page আগেভাগে ছিঁড়ে নেয়। SIGKILL ধরা যায় না, তাই প্রোগ্রাম কিছু গোছাতে পারে না (পর্ব ১৩)।
  6. oom_score-এর রহস্য। /proc/PID/oom_score কাঁচা points নয়, ০–২০০০ মাপে রূপান্তর: (১০০০ + points × ১০০০ ÷ totalpages) × ২ ÷ ৩, পূর্ণসংখ্যায়। postgres: ৩৩৩৮২৪ × ১০০০ ÷ ১০৪৮৫৭৬ = ৩১৮ → (১৩১৮) × ২ ÷ ৩ = ৮৭৮; browser-tab ৫২০ → ১০১৩; java −১০৭ → ৫৯৫; sshd ০. আর ছোট একটি shell-এর points প্রায় ০, তাই (১০০০ + ০) × ২ ÷ ৩ = ৬৬৬ — অংশ ৬-এর রানে ঠিক এটাই দেখেছেন। (এই রূপান্তর সাম্প্রতিক kernel-এর; পুরোনো kernel-এ মাপ ছিল ০–১০০০.)

নিজের যন্ত্রে মান দেখা আর বদলানো (সাধারণ user নিজের process-এর adj বাড়াতে পারেন, কমাতে CAP_SYS_RESOURCE লাগে):

$ echo 500 > /proc/self/oom_score_adj        # এই shell আর তার child-রা আগে মরবে
$ choom -p 4100                               # util-linux: score আর adj দেখা
pid 4100's current OOM score: 878
pid 4100's current OOM score adjustment value: 0
$ systemctl show sshd -p OOMScoreAdjust       # systemd ইউনিটে স্থায়ীভাবে
OOMScoreAdjust=-1000
$ journalctl -k | grep -i "killed process"    # অতীতের OOM kill খোঁজা

cgroup v2 — দলের সীমা আর container-এর OOM

cgroup v2-এর memory controller একটি দলের মেমরি (anonymous, page cache, kernel-এর কিছু অংশ সহ) গোনে আর সীমা বসায় (container-এর পুরো গল্প পর্ব ৫১-এ):

  • memory.high — নরম সীমা। পেরোলে kernel দলটির page জোর করে reclaim করে আর দলকে ধীর করে, কিন্তু মারে না। ধীরে ধীরে চাপ দেওয়ার উপায়।
  • memory.max — কড়া সীমা। Reclaim করেও নিচে না নামলে দলের ভেতরেই OOM killer চলে, totalpages তখন দলের সীমা। memory.events-এর oom_kill গণনা বাড়ে। memory.oom.group = 1 দিলে পুরো দল একসাথে মরে (আধা-মরা সার্ভিসের চেয়ে ভালো)।
  • memory.low/memory.min উল্টো কাজ করে — দলের একটি অংশকে reclaim থেকে রক্ষা।

Docker-এ --memory=512m বা Kubernetes-এ limits.memory সরাসরি memory.max লেখে। সীমা পেরোলে container-এর প্রধান process SIGKILL পায়, আর exit status হয় ১২৮ + ৯ = ১৩৭ (exit code 137, পর্ব ১৩-এর signal-এর নিয়ম)। Kubernetes কারণ লেখে OOMKilled, Docker-এর docker inspect-এ "OOMKilled": true. তবে ১৩৭ মানেই OOM নয় — কেউ kill -9 দিলেও একই কোড, তাই কারণ মিলিয়ে নিন।

$ docker run --memory=256m --name hog myapp
$ echo $?
137
$ docker inspect -f '{{.State.OOMKilled}} {{.State.ExitCode}}' hog
true 137
$ cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.events
low 0
high 0
max 1843
oom 1
oom_kill 1

Kernel-এর OOM killer শেষ মুহূর্তের অস্ত্র — তার আগে যন্ত্র প্রায়ই thrashing-এ (পর্ব ৩৭) আটকে থাকে। তাই systemd-oomd PSI-এর মেমরি-চাপ দেখে আগেভাগে পুরো cgroup মারে, আর Android-এর lmkd পেছনের অ্যাপ বন্ধ করে।

৯ · Memory leak খোঁজা, আর Windows ও macOS কী করে

Memory leak মানে প্রোগ্রাম মেমরি নেয় কিন্তু আর কখনো ফেরত দেয় না, অথচ সেটি আর কাজেও লাগে না। ছোট টুলে leak প্রায় অদৃশ্য (process শেষে kernel সব ফেরত নেয়), কিন্তু সপ্তাহ ধরে চলা সার্ভারে RSS বাড়তে বাড়তে OOM kill. আবার RSS বাড়া মানেই leak নয় (অংশ ৩) — প্রমাণে টুল লাগে।

$ gcc -g -O0 leaky.c -o leaky
$ valgrind --leak-check=full ./leaky
==3127== HEAP SUMMARY:
==3127==     in use at exit: 4,000 bytes in 40 blocks
==3127== 4,000 bytes in 40 blocks are definitely lost in loss record 1 of 1
==3127==    at 0x48447A8: malloc (vg_replace_malloc.c:446)
==3127==    by 0x109176: read_record (leaky.c:12)
==3127==    by 0x1091B5: main (leaky.c:21)

$ gcc -g -fsanitize=address leaky.c -o leaky_asan && ./leaky_asan
==3140==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4000 byte(s) in 40 object(s) allocated from:
    #0 0x7f1c2b4b9a37 in malloc
    #1 0x55a1c0a001a9 in read_record leaky.c:12

$ heaptrack ./server            # চলার সময় প্রতিটি malloc-এর call stack লেখে
$ heaptrack_print heaptrack.server.12345.zst | head   # বা heaptrack_gui
টুলকীভাবে কাজ করেগতির দামকখন
Valgrind Memcheckপ্রোগ্রামকে ভার্চুয়াল CPU-তে চালিয়ে প্রতিটি মেমরি-ছোঁয়া পরীক্ষা; recompile লাগে নাঅনেক গুণ ধীর (প্রায়ই দশ গুণ বা বেশি)ছোট পরীক্ষা, যে binary আবার build করা যায় না
AddressSanitizer + LeakSanitizercompile-এর সময় পাহারার কোড আর shadow memory; সীমার বাইরে লেখা, use-after-free, শেষে leakমোটামুটি দুই গুণ, মেমরিও বেশিCI-তে test চালানো, fuzzing
heaptrackmalloc/free-এর প্রতিটি ঘটনা call stack-সহ লেখা; কে কতটা নিচ্ছে, সময়ের সাথেমাঝারি“মেমরি কোথায় যাচ্ছে” — leak আর বেশি-ব্যবহার দুটোই

চলমান সার্ভারে প্রথম ধাপ সহজ: কয়েক ঘণ্টা smaps_rollup-এর Pss_Anon লিখে রাখুন; সমান load-এ টানা বাড়লে তবেই টুল। GC-ভাষাতেও leak হয়, যদি কোথাও রেফারেন্স রয়ে যায়।

Windows আর macOS — একই সমস্যা, ভিন্ন নীতি

বিষয়LinuxWindowsmacOS
প্রতিশ্রুতির নীতিডিফল্টভাবে overcommit; ফুরোলে OOM killerCommit charge-এর কড়া হিসাব: মোট commit RAM + pagefile-এর commit limit ছাড়ালে allocation ব্যর্থ (pagefile বাড়ানো যায় বলে সীমাও বাড়তে পারে)overcommit-ধর্মী; চাপ বাড়লে compressor আর swap, iOS-এ jetsam অ্যাপ বন্ধ করে
“RAM-এ থাকা” মাপRSS, PSSWorking set (Task Manager-এ private working set)Real memory / footprint (Activity Monitor)
Swapswap partition/file, zswap, zrampagefile.sys (আর swapfile.sys)/private/var/vm/-এর swapfile, প্রয়োজনে বাড়ে
সংকোচনzswap/zram (ঐচ্ছিক)Windows 10 থেকে memory compression (Task Manager-এ “compressed”)Compressed memory, OS X 10.9 (২০১৩) থেকে ডিফল্ট
মেমরি শেষ হলেOOM killer একটি process মারেallocation ব্যর্থ, “low on memory” সতর্কবার্তা“memory pressure” বাড়ে, অ্যাপ বন্ধের অনুরোধ; iOS-এ jetsam

দর্শনের পার্থক্য: Windows প্রতিশ্রুতির সময়েই “না” বলে, Linux “হ্যাঁ” বলে পরে হিসাব মেলায় — বেশি দক্ষ, কিন্তু ব্যর্থতা আসে হঠাৎ। Windows-এর খুঁটিনাটি পর্ব ৫৬-এ।

আরও গভীরে: Linux ৫.১৬ থেকে kernel ক্রমে “page” থেকে folio-তে সরছে — একাধিক একটানা page-কে একটি এককে সামলানো, যাতে বড় ফাইল-cache আর multi-size THP-তে প্রতিটি 4 KiB আলাদা হিসাব করতে না হয় (Matthew Wilcox-এর কাজ)। VMA খোঁজা ৬.১-এ red-black tree থেকে maple tree-তে গেছে, আর page fault-এ পুরো mmap_lock না নিয়ে VMA-ভিত্তিক lock (per-VMA lock, ৬.৪-এর আশেপাশে) বহু-thread প্রোগ্রামের fault দ্রুত করেছে।

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • VSZ আর RSS-এর পার্থক্য? — VSZ সব VMA-র মোট ঠিকানা-আকার, RSS এই মুহূর্তে RAM-এ থাকা page; malloc-এর পর না ছোঁয়া মেমরি VSZ-এ আছে, RSS-এ নেই।
  • glibc malloc কখন mmap ব্যবহার করে? — অনুরোধ M_MMAP_THRESHOLD-এর (শুরুতে 128 KiB, dynamic) সমান বা বড় হলে; ছোটগুলো brk দিয়ে বাড়ানো heap থেকে।
  • free মেমরি কম, available বেশি — সমস্যা? — সাধারণত না; ফাঁকা RAM page cache-এ খাটছে, যা দরকারে ফেলে দেওয়া যায়; MemAvailable দেখুন।
  • OOM killer কাকে মারে? — RSS + swap + page table আর oom_score_adj মিলিয়ে সবচেয়ে বেশি points-এর process; adj −1000 হলে কখনো নয়।
  • Container exit code 137 কেন? — ১২৮ + ৯ (SIGKILL); সাধারণত cgroup-এর memory.max পেরিয়ে OOM kill.

১০ · AI-এর যুগে মেমরির প্রশ্ন — উত্তর যাচাই

“আমার Linux সার্ভারে RAM ভরে যাচ্ছে, কী করব?” — AI প্রায়ই প্রথমেই বলবে echo 3 > /proc/sys/vm/drop_caches cron-এ বসাতে, বা swapoff -a, বা vm.swappiness=0 “সবসময় ভালো”. অথচ অংশ ৬-এর মতো “free কম” হয়তো কোনো সমস্যাই নয়। AI-এর মেমরি-সংক্রান্ত সাধারণ ভুল:

AI-এর ভুলযা ঘটেকীভাবে ধরবেন
MemFree বা free কলাম দেখে “মেমরি শেষ” রায়অপ্রয়োজনীয় drop_caches, গতি কমেMemAvailable, swap-এর কার্যকলাপ (vmstat 1-এর si/so), PSI দেখুন
সব process-এর RSS যোগ করে মোট ব্যবহারভাগের page বারবার গোনা, ভুল সিদ্ধান্তPSS (smaps_rollup, smem) ব্যবহার করুন
“free() করলে RAM কমবেই” — না কমলে leak ঘোষণাভুল বাগ খোঁজাঅংশ ৩-এর মতো mallinfo2/malloc_trim দিয়ে যাচাই, তারপর heaptrack/ASan
OOM এড়াতে vm.overcommit_memory=2 বা সব জায়গায় adj −1000প্রোগ্রাম অকারণে ব্যর্থ, বা যন্ত্র পুরো আটকে যায়আগে কারণ খুঁজুন (journalctl -k, memory.events); শুধু জরুরি সার্ভিস রক্ষা করুন
পুরোনো বা ভুল OS/distro-র তথ্য — যেমন cgroup v1-এর memory.limit_in_bytes, পুরোনো free-এর “-/+ buffers/cache” লাইন, Windows-এর পরামর্শ Linux-এফাইল নেই, ভুল ব্যাখ্যাuname -r, stat -fc %T /sys/fs/cgroup (cgroup2fs = v2), man page মিলিয়ে নিন
THP বা swappiness-এর একটি “সর্বজনীন সেরা” মানএক workload-এ লাভ, অন্যটিতে ক্ষতিনিজের load-এ আগে-পরে latency আর RSS মাপুন

AI-কে মেমরি-সংক্রান্ত প্রশ্ন করার সময় পরিবেশ, প্রমাণ আর সীমা দিন:

আমার সার্ভার Debian 12, kernel 6.1, ৮ GiB RAM, ২ GiB swap, cgroup v2. একটি C++ সার্ভিস Docker container-এ চলে, --memory=2g. দুই-তিন দিন পর container exit code 137 দিয়ে বন্ধ হয়। ১. এটি সত্যিই OOM kill কি না, কোন কোন read-only কমান্ড দিয়ে নিশ্চিত হব (journalctl, memory.events, docker inspect) — ধাপে ধাপে বলো। ২. RSS বাড়ছে leak-এর কারণে, নাকি glibc-এর fragmentation বা arena-র কারণে — আলাদা করার উপায় বলো; smaps_rollup-এর কোন ক্ষেত্র দেখব। ৩. কোনো sysctl, drop_caches, swapoff বা sudo কমান্ড দিও না; শুধু পড়ার কমান্ড আর মাপার পরিকল্পনা। ৪. MALLOC_ARENA_MAX, jemalloc আর memory.high — প্রতিটির ঝুঁকি ও সুবিধা তুলনা করো, আর কোন তথ্য তুমি নিশ্চিত নও তা স্পষ্ট বলো। ৫. তোমার কোনো তথ্য যদি cgroup v1 বা অন্য distro-র হয়, আলাদা করে চিহ্নিত করো।

AI-এর দেওয়া মেমরি-পরামর্শ ব্যবহারের আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Linux ডিফল্টভাবে overcommit করে আর মেমরি ফুরোলে একটি নিরীহ process মেরে ফেলে; Windows প্রতিশ্রুতির সময়েই “না” বলে। কোন নীতি ভালো — আর OOM killer নিয়ে বিতর্ক এত পুরোনো কেন?

Overcommit-এর পক্ষে সবচেয়ে বড় যুক্তি Unix-এর fork(). ১০ GiB-এর একটি ডেটাবেস fork করে শুধু একটি ছোট প্রোগ্রাম exec করতে চায়; কড়া হিসাবে kernel-কে তখন আরও ১০ GiB প্রতিশ্রুতি “সংরক্ষণ” করতে হতো, যদিও copy-on-write-এর কারণে তার প্রায় কিছুই কখনো কপি হবে না। একই কথা বড় sparse array আর thread-এর stack-এর ক্ষেত্রে (ডিফল্ট ৮ MiB প্রতিশ্রুতি, ব্যবহার কয়েক KiB).

বিপক্ষের যুক্তিও শক্ত। Overcommit-এ ব্যর্থতা আসে ভুল জায়গায়, ভুল সময়ে: malloc সফল হয়, আর অনেক পরে একটি page ছোঁয়ার সময় — বা অন্য একটি process-এর page fault-এ — যন্ত্র আটকে যায়, আর OOM killer হয়তো এমন process মারে যার কোনো দোষ ছিল না। ২০০৪ সালে Linux kernel mailing list-এ Andries Brouwer একটি বিখ্যাত রূপক লিখেছিলেন: একটি বিমান সংস্থা খরচ বাঁচাতে প্লেনে কম জ্বালানি নেয়, আর জ্বালানি ফুরোলে কোন যাত্রীকে বাইরে ফেলবে তা বাছতে একটি নিয়ম বানায় — সেই নিয়ম যত চতুরই হোক, মূল নকশাই প্রশ্নবিদ্ধ। এই রূপক আজও আলোচনায় উদ্ধৃত হয়।

Windows-এর commit charge দেখায় অন্য পথটি কেমন: allocation-এর সময়ই হিসাব, তাই প্রোগ্রাম NULL পেয়ে সুন্দরভাবে ব্যর্থ হতে পারে; Windows-এ fork নেই, process তৈরি হয় CreateProcess দিয়ে, তাই overcommit-এর প্রধান কারণটিই নেই। দাম: বড় pagefile, আর অনেক প্রোগ্রাম অপ্রয়োজনে বেশি commit করে বলে সীমায় দ্রুত পৌঁছানো। বাস্তবে Linux-এর বড় সার্ভারগুলো মাঝামাঝি পথ নেয়: overcommit চালু, কিন্তু প্রতিটি সার্ভিস cgroup-এর memory.max-এ বাঁধা, জরুরি সার্ভিসের adj কম, আর PSI-ভিত্তিক user-space OOM daemon আগেভাগে নিয়ন্ত্রিতভাবে ব্যবস্থা নেয়।

মূল উপলব্ধি: overcommit একটি পরিসংখ্যানগত বাজি — গড়ে বিশাল লাভ, লেজে (tail) কঠিন ক্ষতি। ভালো নকশা বাজিটি বাতিল করে না, বরং ক্ষতিকে সীমার (cgroup) মধ্যে আটকায় আর কে আগে ক্ষতিগ্রস্ত হবে তা আগে থেকে ঠিক করে রাখে।

প্র ০২glibc-এর malloc “ভালো” হলেও Redis, Firefox বা বড় সার্ভিস প্রায়ই jemalloc, tcmalloc বা mimalloc ব্যবহার করে। একটি general-purpose allocator-এর মৌলিক টানাপোড়েনগুলো কী?

Allocator-কে একসাথে তিনটি পরস্পরবিরোধী লক্ষ্য মেলাতে হয়: গতি (প্রতিটি malloc কয়েক ন্যানোসেকেন্ডে), মেমরি-দক্ষতা (কম fragmentation, ফাঁকা মেমরি OS-কে ফেরত), আর বহু thread-এ মাপযোগ্যতা (lock-এর কাড়াকাড়ি কম)। glibc-এর malloc Doug Lea-র ১৯৮০-৯০-এর দশকের dlmalloc থেকে আসা ptmalloc-এর বংশধর; একটানা heap আর boundary tag-এর নকশা এক-thread যুগে চমৎকার ছিল। বহু thread-এর জন্য arena যোগ হলো, কিন্তু প্রতিটি arena নিজের ফাঁকা মেমরি ধরে রাখে, আর heap শুধু মাথা থেকে ছাঁটা যায় — অংশ ৩-এর প্রোগ্রামে যেমন দেখলেন, ৩৮ MiB ফাঁকা থাকলেও RSS কমেনি।

Jason Evans ২০০৫ সালের দিকে FreeBSD-র জন্য jemalloc লেখেন; পরে Firefox আর Facebook (Meta) এটি গ্রহণ করে উন্নত করে। এর মূল ধারণা আকার-শ্রেণি (size class) অনুযায়ী আলাদা “run”/“slab”, thread-এর cache, আর অব্যবহৃত page নিয়মিত madvise দিয়ে ফেরত (decay-ভিত্তিক) — ফলে দীর্ঘ সময় চলা সার্ভারে fragmentation কম। Google-এর tcmalloc প্রতিটি thread (নতুন সংস্করণে প্রতিটি CPU)-এর cache দিয়ে গতিতে জোর দেয়; Microsoft Research-এর mimalloc (২০১৯) ছোট “page”-ভিত্তিক free list sharding দিয়ে সরলতা আর গতি দুটোই দাবি করে। Redis Linux-এ ডিফল্টভাবে jemalloc নিয়ে build হয়, আর এর INFO memory-তে fragmentation ratio দেখায় — allocator-এর আচরণ যে সরাসরি পণ্যের মাপ, তার উদাহরণ।

কিন্তু “সেরা allocator” নেই। কিছু workload-এ jemalloc কম মেমরি নেয়, কিছুতে tcmalloc দ্রুত, আর glibc-ই সবচেয়ে পরীক্ষিত ও সব জায়গায় আছে। Allocator বদলানোর আগে নিজের load-এ RSS-এর সময়-রেখা, p99 latency আর CPU মাপা দরকার — benchmark-এর সাধারণ দাবির ওপর নয়। আর মনে রাখার বিষয়: allocator বদলালে leak সারে না, শুধু fragmentation-এর চেহারা বদলায়।

মূল উপলব্ধি: malloc একটি ছোট ফাংশন নয়, একটি নীতিগত সিদ্ধান্ত — গতি, মেমরি আর মাপযোগ্যতার মধ্যে আপস। দীর্ঘমেয়াদি সার্ভারে “ফাঁকা মেমরি কত দ্রুত OS-এ ফেরে” প্রশ্নটিই প্রায়ই নির্ধারক।

প্র ০৩Transparent Huge Pages ২০১১ সালে এসেছিল “বিনা খরচে গতি” হিসেবে, অথচ বহু বছর ধরে অনেক ডেটাবেসের ডকুমেন্টেশনে প্রথম পরামর্শই ছিল “THP বন্ধ করুন”। কেন — আর এখন ছবিটা কোথায়?

Andrea Arcangeli-র THP-র লক্ষ্য ছিল HugeTLBfs-এর ঝামেলা (আগে থেকে পুল সংরক্ষণ, প্রোগ্রাম বদলানো) ছাড়াই huge page-এর সুবিধা দেওয়া। অংশ ৭-এর হিসাবে দেখেছেন, TLB reach কয়েকশো গুণ বাড়তে পারে, আর virtual machine বা বড় heap-এর প্রোগ্রামে কয়েক শতাংশ থেকে দুই অঙ্কের উন্নতি দেখা গেছে। কিন্তু প্রথম দিকের always নীতির সাথে এসেছিল কয়েকটি বাস্তব সমস্যা।

প্রথমত, page fault-এর মধ্যে compaction: 2 MiB একটানা ব্লক না থাকলে kernel তখনই page সরিয়ে জায়গা বানাতে চেষ্টা করত, আর ডেটাবেসের একটি সাধারণ query হঠাৎ কয়েক মিলিসেকেন্ড বা আরও বেশি আটকে যেত — গড়ে ভালো, লেজে খারাপ। দ্বিতীয়ত, মেমরি ফুলে ওঠা: Redis-এর মতো in-memory store fork করে snapshot নেয়; parent প্রতিটি লেখায় copy-on-write-এ একটি 4 KiB নয়, পুরো 2 MiB কপি করাতে পারে, ফলে snapshot-এর সময় মেমরি অনেক বেড়ে যায়। তৃতীয়ত, khugepaged-এর পেছনের কাজ আর lock-এর প্রতিযোগিতা। তাই Redis শুরুতেই সতর্কবার্তা ছাপে, আর বিভিন্ন ডেটাবেস নির্মাতা দীর্ঘদিন THP বন্ধ রাখতে বলেছে।

Kernel-ও বদলেছে: defrag নীতিতে defer আর madvise এসেছে, যাতে fault-এর মধ্যে compaction না হয়; অনেক distro madvise-কে ডিফল্ট করেছে (এই পর্বের রানেও তাই দেখেছেন); ৬.৮-এ multi-size THP ছোট (যেমন 64 KiB) বড়-page দেয়, যা ফুলে ওঠা কমায়; আর folio-ভিত্তিক কাঠামো বড় page-কে kernel-এর সর্বত্র স্বাভাবিক করছে। কিছু নির্মাতা নতুন সংস্করণে THP-র পরামর্শ বদলেছেও। ফলে আজকের উত্তর “বন্ধ” বা “চালু” নয় — কোন নীতি, কোন kernel, কোন workload, আর মাপা ফল কী।

মূল উপলব্ধি: “স্বচ্ছ” অপ্টিমাইজেশন তখনই নিরাপদ, যখন তার খারাপ-ক্ষেত্রের দাম সীমিত। গড়ের লাভ আর লেজের ক্ষতি আলাদা করে মাপুন — আর পুরোনো ব্লগ-পরামর্শকে নিজের kernel সংস্করণের সাথে মিলিয়ে নিন।

প্র ০৪“Linux আমার RAM খেয়ে ফেলেছে!” — বহু বছর ধরে নতুন ব্যবহারকারীদের এই অভিযোগ কেন, আর free কমান্ডের পরিবর্তন ও MemAvailable এর সমাধানে কী ভূমিকা রেখেছে?

পুরোনো free কমান্ডের “free” কলাম ছিল আক্ষরিক অর্থে কেউ ব্যবহার করছে না এমন RAM. Linux page cache-এ যত পারে ফাইল রাখে, তাই কয়েক ঘণ্টা চালানো যন্ত্রে এই সংখ্যা খুব ছোট দেখাত, আর Windows-অভ্যস্ত ব্যবহারকারী ভাবতেন মেমরি শেষ। সমস্যা এত সাধারণ ছিল যে “linuxatemyram” নামে একটি ওয়েবসাইটই হয়ে গিয়েছিল, যার মূল কথা: “Don't panic! Your ram is fine!” — ফাঁকা RAM নষ্ট RAM; cache দরকারে সাথে সাথে ছেড়ে দেওয়া হয়।

কিন্তু আসল প্রশ্ন — “নতুন কাজের জন্য কতটা পাওয়া যাবে?” — তার সৎ উত্তর দেওয়া সহজ ছিল না। MemFree + Cached যোগ করা ভুল, কারণ cache-এর সব অংশ ফেলা যায় না (dirty page, tmpfs আর shared memory-ও Cached-এ গোনা হয়, mmap করে ব্যবহৃত ফাইল-page ফেললে আবার page fault), আর প্রতিটি zone-এর জলসীমা বাদ দিতে হয়। মনিটরিং টুলগুলো প্রত্যেকে নিজের সূত্র বানাত, ভিন্ন ভিন্ন ভুলসহ। ২০১৪ সালে Rik van Riel kernel-এ MemAvailable যোগ করেন (Linux ৩.১৪) — kernel নিজে, নিজের ভেতরের জ্ঞান দিয়ে অনুমান করে দেয়। প্রায় একই সময়ে procps-ng-এর free-এর নতুন সংস্করণ বিভ্রান্তিকর “-/+ buffers/cache” লাইন বাদ দিয়ে available কলাম যোগ করে।

শিক্ষাটি মনিটরিংয়ের জন্য গুরুত্বপূর্ণ। “MemFree ১০%-এর নিচে হলে alert” ধরনের নিয়ম মিথ্যা alarm-এ ভরে যায়, আর মানুষ drop_caches-এর মতো ক্ষতিকর অভ্যাসে যায়। ভালো সংকেত: MemAvailable-এর প্রবণতা, swap-in/swap-out-এর হার (শুধু swap-এর আকার নয়), major page fault, আর PSI-এর memory চাপ (পর্ব ৩৭), যা সরাসরি বলে কাজগুলো মেমরির অপেক্ষায় কতটা সময় হারাচ্ছে। অর্থাৎ “কতটা খালি” নয়, “অভাবের কারণে কতটা ভুগছে”।

মূল উপলব্ধি: একটি মাপ যা দেখায় আর যে প্রশ্নের উত্তর আপনি চান, তা এক না-ও হতে পারে। ভালো abstraction (MemAvailable) ভেতরের জটিলতা সেই জায়গায় রাখে, যেখানে জ্ঞান আছে — kernel-এ।

প্র ০৫Container-এর ভেতরে চলা Java বা অন্য runtime অনেক বছর ধরে নিয়মিত OOM kill হতো, যদিও heap-এর সীমা container-এর সীমার নিচে ছিল। কেন — আর cgroup-সচেতন runtime কী বদলেছে?

প্রথম সমস্যা ছিল দৃষ্টিসীমা। Container-এর ভেতরে /proc/meminfo আর sysconf() পুরো host-এর RAM দেখায় — এই পর্বের bash রানেও container-এর ভেতর থেকে MemTotal এসেছে যন্ত্রের হিসাবে, আর cgroup-এর সীমা আলাদা ফাইলে। পুরোনো JVM ডিফল্ট সর্বোচ্চ heap ঠিক করত “দেখা RAM-এর এক-চতুর্থাংশ” হিসেবে; ৬৪ GiB-এর host-এ ১ GiB সীমার container-এ সে ১৬ GiB heap-এর পরিকল্পনা করত, আর GC যথেষ্ট আগে চলত না। ফল: cgroup-এর memory.max পেরিয়ে SIGKILL, exit code ১৩৭, আর log-এ কোনো Java exception নেই — কারণ SIGKILL ধরা যায় না।

OpenJDK ১০ (২০১৮) container-সচেতনতা আনে, আর তা ৮u১৯১-এ ফিরিয়ে আনা হয়: JVM cgroup-এর সীমা পড়ে, আর -XX:MaxRAMPercentage দিয়ে heap সেই সীমার শতাংশ হিসেবে ঠিক হয়। পরে cgroup v2-এর সমর্থনও যোগ হয়। কিন্তু দ্বিতীয় সমস্যাটি থেকে গেল: heap-ই সব নয়. Thread-এর stack, JIT-এর code cache, metaspace, direct buffer, GC-র নিজস্ব কাঠামো, আর নিচের glibc malloc-এর arena — সব মিলিয়ে native মেমরি। ১ GiB container-এ ৯০০ MiB heap দিলে বাকি ১০০ MiB-এ এগুলো ধরে না। আবার অংশ ৩-এর arena-র কারণে বহু thread-এর JVM-এ native RSS ধীরে বাড়তে পারে, যার জন্য অনেক দল MALLOC_ARENA_MAX কমায় বা jemalloc ব্যবহার করে।

একই কাহিনি অন্য runtime-এও: Go ১.১৯ GOMEMLIMIT এনেছে, যাতে GC একটি নরম সীমার কাছে বেশি সক্রিয় হয়; Node.js-এ --max-old-space-size; Python-এ heap সীমা নেই, তাই cgroup-এর memory.high দিয়ে আগেভাগে চাপ দেওয়া একটি পথ। ভালো অনুশীলন: container-এর সীমা আর runtime-এর সীমার মধ্যে native মেমরির জন্য যথেষ্ট ফাঁক রাখা, memory.events আর PSS-এর সময়-রেখা মনিটর করা, আর load test-এ সত্যিকার শীর্ষ ব্যবহার মাপা।

মূল উপলব্ধি: isolation-এর নতুন স্তর (cgroup) এলে উপরের প্রতিটি স্তরকে — runtime, allocator, মনিটরিং — নতুন সীমাটি “দেখতে” শিখতে হয়। যে সীমা দেখা যায় না, তা আঘাত করে হঠাৎ আর নিঃশব্দে।

অনুশীলন

প্রথম দুটি কাগজ-কলমে হিসাব; তৃতীয়টির উত্তর আগে অনুমান করে তারপর মিলিয়ে দেখুন; চতুর্থটিতে কমান্ড লিখুন।

  1. RSS, PSS, USS (numerical): চারটি process P, Q, R, S একটি ৮ MiB library ভাগ করে, যার ৬ MiB RAM-এ আছে। P আর Q একই parent থেকে fork হওয়া; fork-এর পর তারা ১২ MiB anonymous মেমরি copy-on-write-এ ভাগ করছে (দুজনেই এখনো লেখেনি)। এ ছাড়া প্রত্যেকের নিজস্ব anonymous মেমরি RAM-এ: P ৪, Q ২, R ১০, S ৬ MiB. (ক) P-এর RSS, PSS, USS কত? (খ) চারজনের RSS-এর যোগ আর PSS-এর যোগ কত, আর আসলে কত RAM লেগেছে? (গ) Q যদি ভাগের ১২ MiB-এর অর্ধেকে লেখে, P-এর PSS কত হবে?

    (ক) P-এর RAM-এ থাকা page: নিজস্ব ৪ + COW-ভাগ ১২ + library ৬ = RSS ২২ MiB. PSS = ৪ + ১২ ÷ ২ + ৬ ÷ ৪ = ৪ + ৬ + ১.৫ = ১১.৫ MiB. USS = শুধু নিজস্ব = ৪ MiB (ভাগের COW page private mapping হলেও এই মুহূর্তে দুজনের, তাই USS-এ নয়)।

    (খ) RSS: P ২২, Q ২ + ১২ + ৬ = ২০, R ১০ + ৬ = ১৬, S ৬ + ৬ = ১২; যোগ ৭০ MiB. PSS: P ১১.৫, Q ২ + ৬ + ১.৫ = ৯.৫, R ১০ + ১.৫ = ১১.৫, S ৬ + ১.৫ = ৭.৫; যোগ ৪০ MiB. আসল RAM: ৪ + ২ + ১০ + ৬ + ১২ + ৬ = ৪০ MiB — PSS-এর যোগের সমান, RSS-এর যোগ ৩০ MiB বেশি গুনেছে।

    (গ) Q ৬ MiB-এ লিখলে সেই page-গুলোর কপি হয় — Q-এর নিজস্ব হয়, আর মূল ৬ MiB এখন শুধু P-এর। P-এর PSS = ৪ + (৬ নিজের একার) + ৬ ÷ ২ + ১.৫ = ১৪.৫ MiB; P-এর USS ৪ থেকে বেড়ে ১০ MiB, অথচ P নিজে কিছুই করেনি। মোট RAM এখন ৪৬ MiB.

  2. buddyinfo থেকে হিসাব আর buddy trace (numerical): একটি যন্ত্রে (4 KiB page) Node 0, zone Normal 120 40 10 6 2 1 0 0 0 0 0. (ক) এই zone-এ মোট কত ফাঁকা মেমরি? (খ) সবচেয়ে বড় একটানা ফাঁকা ব্লক কত বড়? একটি 2 MiB huge page কি সরাসরি পাওয়া যাবে? (গ) একটি order-3 অনুরোধ এলে buddy কী করবে, আর তারপর সারিটি কেমন হবে?

    (ক) page: ১২০×১ + ৪০×২ + ১০×৪ + ৬×৮ + ২×১৬ + ১×৩২ = ১২০ + ৮০ + ৪০ + ৪৮ + ৩২ + ৩২ = ৩৫২ page = ৩৫২ × ৪ KiB = ১৪০৮ KiB (১.৩৭৫ MiB).

    (খ) সবচেয়ে বড় ব্লক order 5 = ৩২ page = ১২৮ KiB. 2 MiB = order 9; মোট ফাঁকাই ২ MiB-এর কম, তাই এই zone থেকে কখনো নয় — compaction-ও এখানে সাহায্য করবে না, আগে reclaim লাগবে।

    (গ) order 3-এর তালিকায় ৬টি ব্লক আছে, তাই ভাঙার দরকার নেই — একটি সরাসরি দেওয়া হয়। নতুন সারি: 120 40 10 5 2 1 0 0 0 0 0. (order 3 খালি থাকলে order 4-এর একটি ব্লক ভেঙে একটি দেওয়া হতো আর অন্য অর্ধেক order 3-এ যেত।)

  3. আউটপুট অনুমান করুন: একটি 64-bit Linux যন্ত্রে glibc, THP madvise বা never. একটি প্রোগ্রাম শুরুতে char *a = malloc(100 * 1024); আর char *b = malloc(64u << 20); করে, তারপর b-এর প্রতি ৮১৯২ বাইটে একটি করে বাইট লেখে (for (i = 0; i < 64u<<20; i += 8192) b[i] = 1;). (ক) a আর b maps-এর কোন ধরনের VMA-তে পড়বে? (খ) লেখার আগে আর পরে VSZ ও RSS প্রায় কত বাড়বে? (গ) THP always হলে (খ)-এর উত্তর কীভাবে বদলাতে পারে?

    (ক) ১০০ KiB < ১২৮ KiB, তাই a [heap]-এ (brk). ৬৪ MiB অনেক বড়, তাই b একটি নামহীন anonymous mmap-এ।

    (খ) malloc-এর পর VSZ প্রায় +৬৪ MiB (আর heap-এর ছোট বৃদ্ধি), RSS প্রায় ০. প্রতি ৮১৯২ বাইটে একটি লেখা মানে প্রতি দুটি 4 KiB page-এর একটি ছোঁয়া: ৬৪ MiB ÷ ৮ KiB = ৮১৯২টি page, অর্থাৎ RSS প্রায় +৩২ MiB, ৮১৯২টি page fault. VSZ একই থাকে।

    (গ) always-এ kernel fault-এর সময় পুরো 2 MiB page দিতে পারে; তখন প্রতি 2 MiB-এ একটি ছোঁয়াই পুরো 2 MiB RSS-এ আনে — RSS প্রায় +৬৪ MiB পর্যন্ত যেতে পারে (huge page পাওয়া গেলে)। AnonHugePages দেখে মিলিয়ে নিন।

  4. কমান্ড লিখুন: (ক) এই মুহূর্তে যে process-এর oom_score সবচেয়ে বেশি, তার PID, নাম আর score ছাপানোর একটি shell কমান্ড লিখুন। (খ) একটি ব্যাকআপ script চালান এমনভাবে, যাতে মেমরি ফুরোলে OOM killer এটিকেই আগে বাছে — root ছাড়া। (গ) একটি Docker container ৫১২ MiB-এর কড়া সীমায় চালানোর কমান্ড, আর পরে সেটি OOM-এ মরেছিল কি না যাচাইয়ের কমান্ড লিখুন।

    (ক) একটি সমাধান (PID শেষ হয়ে যাওয়া process-এর ত্রুটি চেপে রেখে):

    for d in /proc/[0-9]*; do
      s=$(cat "$d/oom_score" 2>/dev/null) && echo "$s ${d#/proc/} $(cat "$d/comm" 2>/dev/null)"
    done | sort -rn | head -1

    (খ) সাধারণ user নিজের adj বাড়াতে পারেন, আর child তা উত্তরাধিকারে পায়: sh -c 'echo 1000 > /proc/self/oom_score_adj; exec ./backup.sh' — অথবা util-linux-এর choom -n 1000 -- ./backup.sh. (adj কমাতে CAP_SYS_RESOURCE লাগে।)

    (গ) docker run --memory=512m --memory-swap=512m --name job myimage (--memory-swap সমান দিলে swap-এর বাড়তি ছাড় নেই)। পরে: docker inspect -f '{{.State.OOMKilled}} {{.State.ExitCode}}' job — true 137 হলে OOM kill; সাথে host-এ journalctl -k | grep -i oom.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি প্রোগ্রাম malloc(1 MiB) করে প্রথমবার তাতে লেখে। সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📞 glibc দেখে অনুরোধ mmap threshold-এর বড়, তাই mmap system call
  2. 🗺️ kernel একটি নতুন anonymous VMA বানায় — এখনো কোনো frame নেই
  3. ✍️ প্রোগ্রাম প্রথম page-এ লেখে, MMU কোনো page table entry পায় না
  4. ⚠️ page fault: kernel VMA দেখে নিশ্চিত হয় ঠিকানাটি বৈধ
  5. 🧱 buddy allocator থেকে একটি frame, শূন্য করে page table-এ বসানো
  6. 🔁 লেখার instruction আবার চলে, এবার সফল — RSS এক page বাড়ে

64-bit Linux-এ glibc-এর ডিফল্ট সেটিংয়ে একটি প্রোগ্রাম শুরুতেই malloc(256 * 1024) করে। মেমরিটি সাধারণত কোথা থেকে আসে?

  • Process-এর stack থেকে
  • brk দিয়ে বাড়ানো [heap] থেকে, কারণ সব malloc heap-এ যায়
  • একটি আলাদা anonymous mmap এলাকা থেকে
  • kernel-এর slab cache থেকে

তিনটি process একটি ৯ MiB shared library ভাগ করে (পুরোটা RAM-এ), আর প্রত্যেকের নিজস্ব ৩ MiB মেমরি RAM-এ। একটি process-এর PSS কত?

  • ১২ MiB
  • ৬ MiB
  • ৩ MiB
  • ৯ MiB

একটি Linux সার্ভারে free -h দেখাচ্ছে free 300Mi, buff/cache 11Gi, available 10Gi; swap-এর কার্যকলাপ নেই। সবচেয়ে সঠিক মূল্যায়ন কোনটি?

  • মেমরি প্রায় শেষ, এখনই RAM বাড়াতে হবে
  • নিয়মিত drop_caches চালিয়ে free বাড়ানো উচিত
  • swap বন্ধ করে দেওয়া উচিত
  • সমস্যা নেই — RAM page cache-এ কাজে লাগছে, নতুন কাজকে প্রায় ১০ GiB দেওয়া যাবে

Kubernetes-এ একটি container বারবার exit code 137 দিয়ে বন্ধ হচ্ছে, কারণ হিসেবে OOMKilled. কী ঘটছে?

  • Container cgroup-এর মেমরি সীমা (memory.max) পেরিয়েছে, আর kernel process-কে SIGKILL (৯) দিয়েছে
  • প্রোগ্রাম segmentation fault (SIGSEGV)-এ মরেছে
  • CPU limit পেরিয়েছে বলে kernel থামিয়ে দিয়েছে
  • Kubernetes স্বাভাবিকভাবে SIGTERM পাঠিয়ে বন্ধ করেছে

Linux-এ একটি process-এর /proc/PID/oom_score_adj-এ −1000 লেখা হলে কী হয়?

  • process-টি আর নতুন মেমরি পায় না
  • OOM killer এই process-কে কখনো বলি হিসেবে বাছবে না
  • OOM-এর সময় এটিকেই সবার আগে মারা হবে
  • process-টির page কখনো swap হবে না

4 KiB page-এর একটি Linux যন্ত্রে /proc/buddyinfo-এর একটি সারির দশম সংখ্যাটি (order 9) 5. এর মানে কী?

  • ৫টি ফাঁকা ব্লক, প্রতিটি ৯টি page-এর
  • ৫টি ব্যবহৃত 2 MiB ব্লক
  • ৫টি ফাঁকা একটানা ব্লক, প্রতিটি 2 MiB — মোট ১০ MiB
  • order 9-এর মেমরির ৫% ফাঁকা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. একটি process-এর address space = VMA-র তালিকা (mm_struct, /proc/PID/maps). VSZ প্রতিশ্রুতি, RSS RAM-এ থাকা, PSS ভাগ করে গোনা (যোগ ≈ ব্যবহৃত RAM), USS শুধু নিজস্ব।
  2. glibc malloc: ছোট অনুরোধ brk-এর heap-এ, M_MMAP_THRESHOLD (শুরুতে 128 KiB, dynamic) বা বড়গুলো আলাদা mmap-এ; heap-এর ফাঁকা মেমরি free-এর পরেও প্রায়ই থেকে যায় — malloc_trim, arena-র সংখ্যা, অন্য allocator.
  3. Kernel: zone (DMA, DMA32, Normal) → buddy allocator (order 0–10, buddy = pfn ⊕ ২order, /proc/buddyinfo) → তার ওপরে SLUB (kmalloc); বড় অ-একটানা মেমরিতে vmalloc.
  4. “free” কম মানে বিপদ নয় — page cache; দেখুন MemAvailable, swap-এর কার্যকলাপ, PSI. drop_caches শুধু benchmark-এ; swappiness ডিফল্ট সাধারণত ৬০; zswap/zram সংকুচিত swap. Huge page TLB reach বাড়ায়, THP-র নীতি মেপে বাছুন।
  5. Linux overcommit করে; ফুরোলে OOM killer সবচেয়ে বেশি RSS + swap + page table + adj-এর process-কে SIGKILL দেয় (adj −1000 = কখনো নয়)। cgroup v2-এ memory.high নরম, memory.max কড়া — পেরোলে container-এর exit code ১৩৭.

পরের পর্বে: মেমরির মডিউল শেষ — এবার CPU আর মেমরির বাইরের জগৎ। কিবোর্ড, ডিস্ক আর নেটওয়ার্ক কার্ডের সাথে OS কীভাবে কথা বলে: polling, interrupt, DMA আর device driver — মডিউল ৬-এর শুরু।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান