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

নিজের malloc বানানো — memory allocator-এর ভেতরে

Build Your Own malloc — Inside a Memory Allocator

Taught in Bangla, this lesson builds a malloc from scratch: getting memory with brk, sbrk and mmap, block headers and alignment, a first-fit allocator with splitting, free with coalescing, fragmentation, how real allocators use size classes and thread caches, arena and pool allocators, and heap corruption bugs.

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

  • Heap-এর মেমরি অপারেটিং সিস্টেম থেকে কীভাবে আসে — brk/sbrk আর mmap
  • Block header, alignment আর flag-কে আকারের নিচের বিটে লুকিয়ে রাখার কৌশল
  • নিজ হাতে একটি first-fit allocator — খোঁজা, ভাগ করা, ফেরত নেওয়া, জোড়া দেওয়া
  • Internal ও external fragmentation, আর first/next/best fit-এর trade-off
  • Arena ও pool allocator, বাস্তব allocator-এর কৌশল, আর AI-এর লেখা allocator যাচাইয়ের চেকলিস্ট

১ · malloc জাদু নয় — একটি সাধারণ C কোড

পর্ব ২০-এ আপনি malloc আর free ব্যবহার করতে শিখেছেন। চাইলেন, জায়গা পেলেন; কাজ শেষে ফেরত দিলেন। কিন্তু একটি প্রশ্ন তখন খোলা রেখে এসেছিলাম — malloc-এর ভেতরে আসলে কী ঘটে?

malloc কোনো বিশেষ হার্ডওয়্যার বা অপারেটিং সিস্টেমের গোপন কৌশল নয়। এটি কারও লেখা সাধারণ C কোড। Linux-এ এটি থাকে glibc-এর ভেতরে। কোডটি অপারেটিং সিস্টেমের কাছ থেকে একবারে একটি বড় মেমরির এলাকা নেয়, তারপর সেটিকে টুকরো টুকরো করে আপনার প্রোগ্রামকে দেয়। এই কোডের নাম allocator.

ধরুন একটি নতুন মার্কেটের লম্বা একটি বারান্দা। মার্কেট কমিটি সেখানে দোকানের জায়গা ভাড়া দেয় — কেউ চায় ৪ ফুট, কেউ ১০ ফুট। কমিটির ম্যানেজার প্রতিটি দোকানের শুরুতে একটি ছোট সাইনবোর্ড পুঁতে দেন: “এখান থেকে ৮ ফুট, ভাড়া দেওয়া”। কেউ দোকান ছেড়ে দিলে সাইনবোর্ডে লেখেন “খালি”; পাশাপাশি দুটি খালি জায়গা থাকলে দুটি মিলিয়ে একটি বড় জায়গা বানান। এই ম্যানেজারই allocator, বারান্দাটি heap, আর সাইনবোর্ডটি হলো block header.

এই ছোট গল্পেই allocator-এর চারটি মূল কাজ লুকিয়ে আছে:

১. দ্রুত খুঁজে দেওয়া

একটি বড় প্রোগ্রাম সেকেন্ডে লাখো বার malloc ডাকতে পারে। প্রতিবার পুরো heap ঘুরে দেখলে প্রোগ্রাম ধীর হয়ে যাবে।

গতি

২. আকার মনে রাখা

free(p)-এ আপনি শুধু ঠিকানা দেন, আকার নয়। তাই allocator-কে নিজেই মনে রাখতে হয় কোন block কত বড়।

হিসাব

৩. Aligned ঠিকানা

malloc জানে না আপনি সেখানে char রাখবেন না double. তাই সবচেয়ে কড়া alignment মেনে ঠিকানা দিতে হয়।

নিয়ম

৪. টুকরো হওয়া ঠেকানো

বারবার নেওয়া-ছাড়ার পরে মেমরি ছোট ছোট ফাঁকা টুকরোয় ভেঙে যায়। মোট জায়গা থাকলেও বড় অনুরোধ মেটানো যায় না।

fragmentation

এই পর্বে আমরা নিজেই একটি ছোট allocator বানাব। উদ্দেশ্য glibc-এর সাথে পাল্লা দেওয়া নয়। উদ্দেশ্য তিনটি জিনিস হাড়ে হাড়ে বোঝা: pointer আসলে byte-এর হিসাব ছাড়া কিছু নয়; alignment কেন বাধ্যতামূলক; আর use-after-free বা double free কেন এত ভয়ংকর — কারণ এগুলো allocator-এর নিজের খাতাই নষ্ট করে দেয়।

নামের একটি গোলমাল আগেই মিটিয়ে নিই। পর্ব ৩৬-এ আপনি heap নামের একটি ডেটা স্ট্রাকচার দেখেছেন (প্রায়োরিটি কিউ)। মেমরির heap তার সাথে সম্পর্কহীন — এখানে “heap” মানে শুধু “চাইলে পাওয়া যায় এমন মেমরির স্তূপ”। দুটি জিনিসের একই নাম হয়ে গেছে ইতিহাসের দুর্ঘটনায়।

২ · মেমরি আসে কোথা থেকে — brk, sbrk আর mmap

Allocator নিজে মেমরি তৈরি করতে পারে না; তাকেও কারও কাছে চাইতে হয়। সেই “কেউ” হলো অপারেটিং সিস্টেমের kernel, আর চাওয়ার উপায় system call. পর্ব ২০-এর ভাবনার প্রশ্নে সংক্ষেপে দেখেছিলেন — allocator পাইকারি দরে বড় টুকরো কেনে, খুচরা বেচে। এবার পাইকারি বাজারটি একটু কাছ থেকে দেখি।

উপায়কীভাবে কাজ করেকখন ব্যবহার হয়
brk / sbrkHeap-এর শেষ সীমা (“program break”) উপরের দিকে ঠেলে দেয়। নতুন জায়গাটি আগের heap-এর ঠিক পরেই, টানা।পুরোনো Unix-এর পদ্ধতি; glibc আজও ছোট অনুরোধের মূল heap বাড়াতে ব্যবহার করে
mmapঠিকানা-জগতের যেকোনো ফাঁকা জায়গায় নতুন কয়েকটি page (সাধারণত ৪ KB করে) জুড়ে দেয়। আলাদা করে ফেরতও দেওয়া যায় (munmap).খুব বড় অনুরোধ (glibc-এ ডিফল্টে প্রায় ১২৮ KB-র বেশি), আর প্রতিটি thread-এর আলাদা heap
static unsigned char heap[N]Compile করার সময়েই নির্দিষ্ট মাপের একটি array. বাড়ে না, কমে না।এমবেডেড বোর্ড যেখানে OS নেই, আর এই পর্বের ডেমো

একটি গুরুত্বপূর্ণ সূক্ষ্মতা আছে। mmap বা brk সফল হলেই আসল RAM পাওয়া যায় না। Kernel শুধু virtual memory-র ঠিকানা বরাদ্দ করে। আপনি প্রথমবার কোনো page ছুঁলে একটি page fault হয়, তখন kernel আসল RAM-এর একটি ফ্রেম জুড়ে দেয়। System call আর page fault — দুটিরই খরচ আছে, তাই সব allocator OS-এর কাছে কম যেতে আর হাতে থাকা মেমরি বারবার ব্যবহার করতে চায়।

mmap-এর আরও অনেক রূপ (ফাইলকে মেমরির মতো পড়া, প্রসেসের মধ্যে মেমরি ভাগ করা) আসবে পর্ব ৫১-এ। এই পর্বে আমাদের ডেমো allocator কোনো system call ডাকবে না। সে কাজ করবে একটি static array-র ওপর:

static _Alignas(max_align_t) size_t heap[1024 / sizeof(size_t)];   /* আমাদের ১ KB-র “OS” */

কেন? এটি যেকোনো OS-এ একইভাবে চলে, আর ফলাফল প্রতিবার হুবহু এক — আমরা কাঁচা ঠিকানা না ছাপিয়ে array-র শুরু থেকে কত byte দূরে (offset), সেটি ছাপাব। তা ছাড়া allocator-এর আসল বুদ্ধি মেমরি আনায় নয়, ভাগ করায়।

“program break” আর brk/sbrk এসেছে Unix-এর একেবারে প্রথম দিক থেকে। Dennis Ritchie ও Brian Kernighan-এর বিখ্যাত বই The C Programming Language-এর (১৯৭৮) অষ্টম অধ্যায়ে একটি ছোট storage allocator পুরোটা লিখে দেখানো আছে — ফাঁকা block-এর একটি গোল তালিকা, প্রতিটি block-এর আগে আকার লেখা header, আর OS-এর কাছ থেকে sbrk দিয়ে বড় টুকরো আনা। প্রায় পঞ্চাশ বছর পরেও এই পর্বের মূল ধারণা সেই একই।

৩ · দুটি নিয়ম — alignment আর block header

নিয়ম ১: সবচেয়ে কড়া alignment

পর্ব ২২-এ দেখেছেন, double সাধারণত ৮-এর গুণিতক ঠিকানায় বসতে চায়, আর struct-এর ভেতরে এজন্য padding বসে। malloc-এর সমস্যা আরও বড় — সে জানেই না আপনি block-এ কী রাখবেন। তাই C স্ট্যান্ডার্ড বলে, malloc-এর দেওয়া ঠিকানা এমন হতে হবে যাতে যেকোনো সাধারণ টাইপ সেখানে নিরাপদে বসতে পারে।

“সবচেয়ে কড়া alignment কত?” — এর উত্তর দিতে C11 একটি বিশেষ টাইপ এনেছে: max_align_t (<stddef.h>-এ)। _Alignof(max_align_t) বলে দেয় এই প্ল্যাটফর্মে সবচেয়ে বড় মৌলিক alignment কত। ৬৪-বিট Linux-এ (x86-64, GCC) এটি ১৬ — এই কোর্সের compiler-এ চালিয়ে আমরা নিশ্চিত হয়েছি। কারণ long double-এর মতো টাইপ ১৬ byte alignment চায়।

তাহলে কেউ ২৪ byte চাইলে আমরা আকারটিকে ১৬-এর পরের গুণিতকে গোল করব — ৩২. সূত্রটি ছোট, কিন্তু প্রায় সব allocator-এ পাবেন:

size_t rounded = (n + AL - 1) / AL * AL;   /* AL = 16: 24 → 32, 32 → 32, 1 → 16 */
/* AL দুইয়ের ঘাত হলে একই কাজ বিটে: (n + AL - 1) & ~(AL - 1) */

নিয়ম ২: প্রতিটি block-এর আগে header

এবার আকার মনে রাখার সমস্যা। সমাধানটি সুন্দর: allocator আপনাকে যে ঠিকানা দেয়, তার ঠিক আগে কয়েকটি byte নিজের জন্য রেখে দেয়। সেখানে লেখে block-এর আকার। আপনি free(p) ডাকলে সে p থেকে কয়েক byte পিছিয়ে header পড়ে নেয়। ব্যবহারকারী যে অংশটি পান, তার নাম payload.

একটি block-এর গঠন

Header first, then the payload the user sees

  1. Headersize | used১৬ byte · allocator-এর খাতা
  2. Payloadp = header + 16এই ঠিকানাই malloc ফেরত দেয়
  3. পরের blockheader + sizeআকার জানলেই পরেরটির ঠিকানা
ছবি ৪৭.১ — header-এ আকার থাকায় পুরো heap একটি “অদৃশ্য তালিকা” হয়ে যায়: শুরু থেকে আকার যোগ করে করে প্রতিটি block-এ পৌঁছানো যায়।

আমাদের header-এর আকার রাখব ১৬ byte, যদিও আকার লিখতে লাগে মাত্র ৮ byte (একটি size_t). কেন? Header ১৬ byte হলে আর block-এর শুরু ১৬-এর গুণিতকে হলে payload-ও ১৬-এর গুণিতকে পড়বে — alignment-এর নিয়ম আপনাআপনি মানা হলো। বাস্তবের glibc আরও হিসাবি; সে বাড়তি জায়গা নষ্ট না করতে আরও চতুর বিন্যাস ব্যবহার করে।

Flag লুকানো — আকারের নিচের বিটে

Header-এ আরও একটি তথ্য লাগে: block-টি এখন ব্যবহার হচ্ছে, না খালি? এর জন্য আলাদা ঘর না বানিয়ে একটি পুরোনো কৌশল কাজে লাগানো হয়। সব block-এর আকার ১৬-এর গুণিতক — মানে বাইনারিতে আকারের নিচের চারটি বিট সবসময় 0. সেই অব্যবহৃত বিটের একটিতে “used” flag রেখে দিলেই হলো। পর্ব ৯-এর bit mask এখানে সরাসরি কাজে লাগে:

size_t h = 48 | 1;            /* আকার 48, used = 1  → h = 49 */
size_t size = h & ~(size_t)1; /* নিচের বিট মুছে আকার: 48 */
int used = h & 1;             /* শুধু নিচের বিট: 1 */

glibc-ও ঠিক এই ধারণা ব্যবহার করে — তার chunk-এর আকারের নিচের কয়েকটি বিটে কয়েকটি flag থাকে। আর বাস্তবের malloc আপনার চাওয়ার চেয়ে কিছুটা বেশি দেয়, সেটি চোখেও দেখা যায়। glibc-এর একটি নিজস্ব ফাংশন আছে, malloc_usable_size(p) — block-এ আসলে কত byte ব্যবহারযোগ্য, তা বলে। এই কোর্সের Linux compiler-এ চালালে পাওয়া যায়:

malloc(1):  usable=24
malloc(14): usable=24
malloc(27): usable=40
malloc(40): usable=40

১ byte চাইলেও ২৪ byte! কারণ glibc ৬৪-বিটে প্রতিটি chunk ন্যূনতম ৩২ byte রাখে, তার মধ্যে ৮ byte header. খেয়াল রাখবেন, malloc_usable_size শুধু glibc-এর (<malloc.h>), স্ট্যান্ডার্ড C-এর নয় — Windows বা Mac-এ এর নাম ও আচরণ আলাদা। আর বাড়তি byte-এ লেখা “বৈধ” মনে করে কোড লিখবেন না; চাওয়া মাপের বাইরে লেখা সবসময় ভুল।

৪ · First-fit allocator — খোঁজা আর ভাগ করা

সবচেয়ে সহজ কৌশল দিয়ে শুরু করি: first fit. Heap-এর শুরু থেকে একটি একটি block দেখো; প্রথম যে খালি block যথেষ্ট বড়, সেটিই দাও। যদি সেটি দরকারের চেয়ে অনেক বড় হয়, তাহলে ভেঙে দুই ভাগ করো — সামনের অংশ দাও, বাকিটা খালি রেখে দাও। এই ভাঙার নাম splitting.

নিচের heap-এ চারটি block আছে। সংখ্যাগুলো payload-এর আকার (byte); প্রতিটির আগে ১৬ byte header আছে, আর @-এর পরের সংখ্যা হলো header-এর offset. প্রথমে malloc(24), তারপর malloc(100) — “পরে” চেপে দেখুন allocator কী ভাবে:

used 32@0
free 64@48
used 32@48 · নতুন
free 16@96 · ভাগের বাকি
used 16@128
free 128@160
used 128@160 · পুরোটা
allocator-এর হিসাবheader = 16 byte, AL = 16
request: malloc(24)
  1. শুরুর heap. চারটি block: দুটি ব্যবহার হচ্ছে (হলুদ), দুটি খালি (সবুজ)। অনুরোধ এলো malloc(24). Allocator-এর কাছে আর কোনো তালিকা নেই — শুধু header-গুলো।
  2. আকার গোল করা। ২৪ ১৬-এর গুণিতক নয়, তাই ৩২. সাথে header-এর ১৬ byte — মোট ৪৮ byte-এর একটি block লাগবে। এই গোল করার কারণেই payload সবসময় aligned থাকবে।
  3. প্রথম block: used. Allocator offset ০-এর header পড়ল — flag বিট 1, অর্থাৎ ব্যবহার হচ্ছে। এড়িয়ে যাও। পরের header কোথায়? এই block-এর মোট আকার (১৬ + ৩২ = ৪৮) যোগ করলেই — offset ৪৮.
  4. দ্বিতীয় block: মাপে হয়! খালি, আর ৬৪ ≥ ৩২. First fit এখানেই থামে — পেছনে ১২৮-এর বড় block-টির দিকে তাকায়ও না। এটাই first fit-এর গতি, আবার এটাই তার দুর্বলতা (অংশ ৬-এ দেখবেন)।
  5. Split — ভেঙে দুই ভাগ। ৬৪-এর মধ্যে ৩২ দিলে ৩২ বাকি থাকে। সেখানে নতুন একটি header (১৬) বসিয়ে বাকি ১৬ byte-এর একটি খালি block বানানো হলো, offset ৯৬-এ। Heap-এ এখন একটি block বেশি।
  6. Payload-এর ঠিকানা ফেরত। Header offset ৪৮-এ, তাই ব্যবহারকারী পান ৪৮ + ১৬ = offset ৬৪. তিনি কখনো header দেখেন না — কিন্তু তাঁর ঠিকানার ঠিক আগেই allocator-এর খাতা। এই খাতায় ভুল করে লিখলে কী হয়, তা অংশ ৯-এ।
  7. দ্বিতীয় অনুরোধ: malloc(100). গোল করে ১১২. আবার শুরু থেকে খোঁজা: used, used, ১৬ (খুব ছোট), used — শেষে offset ১৬০-এর ১২৮ মাপে হলো। খেয়াল করুন, প্রতিটি অনুরোধে শুরু থেকে হাঁটতে হচ্ছে; block হাজারটা হলে এটি ধীর।
  8. এবার ভাঙা হলো না। বাকি থাকে ১৬ byte — নতুন header-এর জায়গাই হয় না, payload তো দূরের কথা। তাই পুরো ১২৮-ই দেওয়া হলো; ১৬ byte block-এর ভেতরে নষ্ট হলো। এর নাম internal fragmentation — অংশ ৬-এর বিষয়।

এবার এই ধারণাগুলো আসল কোডে। নিচের প্রোগ্রামে পুরো একটি allocator আছে — my_malloc, my_free, আর heap-এর অবস্থা ছাপার জন্য dump. কোডটি অন্য পর্বের চেয়ে লম্বা; ধীরে পড়ুন, তারপর চালান।

#include <stdio.h>
#include <stddef.h>
#define HEAP 1024
#define AL _Alignof(max_align_t)                 /* 16 on x86-64 Linux */
static _Alignas(max_align_t) size_t heap[HEAP / sizeof(size_t)];
#define H(off) heap[(off) / sizeof(size_t)]       /* header word at byte offset */
#define SIZE(off) (H(off) & ~(size_t)1)           /* low bit = used flag */
static void *my_malloc(size_t n) {
    if (n > HEAP) return NULL;
    size_t need = AL + (n + AL - 1) / AL * AL;      /* header + rounded payload */
    for (size_t off = 0; off < HEAP; off += SIZE(off)) {
        size_t sz = H(off);
        if ((sz & 1) || sz < need) continue;        /* used or too small: skip */
        if (sz - need >= 2 * AL) { H(off + need) = sz - need; sz = need; } /* split */
        H(off) = sz | 1;                            /* mark used */
        return (char *)heap + off + AL;             /* payload after header */
    }
    return NULL;
}
static void my_free(void *p) {
    size_t off = (size_t)((char *)p - (char *)heap) - AL;
    if (!(H(off) & 1)) { printf("double free at offset %zu!\n", off); return; }
    H(off) &= ~(size_t)1;                           /* clear used flag */
    for (off = 0; off < HEAP; off += SIZE(off))     /* coalesce free neighbours */
        while (!(H(off) & 1) && off + H(off) < HEAP && !(H(off + H(off)) & 1))
            H(off) += H(off + H(off));
}
static void dump(const char *tag) {
    printf("%-7s", tag);
    for (size_t off = 0; off < HEAP; off += SIZE(off))
        printf(" [%zu %s %zu]", off, (H(off) & 1) ? "used" : "free", SIZE(off));
    printf("\n");
}
int main(void) {
    H(0) = HEAP;                                    /* one big free block */
    char *a = my_malloc(20), *b = my_malloc(100), *c = my_malloc(10);
    dump("start");
    my_free(b); dump("free b");
    my_free(b);                                     /* double free on purpose */
    my_free(a); dump("free a");
    char *d = my_malloc(40); dump("d=40");
    my_free(c); my_free(d); dump("end");
}

Heap আর header: heap হলো ১০২৪ byte-এর একটি size_t array, ১৬-তে aligned. H(off) ম্যাক্রো byte-offset থেকে সেই জায়গার header-কে একটি size_t হিসেবে পড়ে বা লেখে। শুরুতে H(0) = HEAP — পুরো heap একটি বড় খালি block.

প্রথম লাইন start: a-র ২০ গোল হয়ে ৩২, header নিয়ে ৪৮; b-র ১০০ → ১১২ + ১৬ = ১২৮; c-র ১০ → ১৬ + ১৬ = ৩২. প্রতিবার বড় খালি block ভেঙে সামনের অংশ দেওয়া হয়েছে, বাকি ৮১৬ শেষে খালি পড়ে আছে।

free b আর ইচ্ছাকৃত double free: b-র flag মুছে গেল, কিন্তু দুই পাশের প্রতিবেশী used, তাই জোড়া লাগল না। তারপর একই b আবার free — header-এ flag আগেই 0, তাই allocator চিৎকার করল double free at offset 48! এবং কিছু বদলাল না।

free a — coalescing: a খালি হতেই ঠিক পাশের b-ও খালি, তাই দুটি মিলে ৪৮ + ১২৮ = ১৭৬-এর একটি block. d=40: ৪০ → ৪৮ + ১৬ = ৬৪, first fit সেই ১৭৬ থেকেই কেটে নিল (offset ০), বাকি ১১২ খালি। শেষে সব free করলে আবার একটিমাত্র [0 free 1024] — কোনো byte হারায়নি। এই শেষ লাইনটিই প্রমাণ যে split আর coalesce-এর হিসাব মেলে।

সৎ সীমাবদ্ধতা: আমাদের my_free জোড়া দিতে প্রতিবার পুরো heap ঘোরে — সহজ, কিন্তু ধীর। আর n > HEAP পরীক্ষাটি বাদ দিলে বিশাল n-এ n + AL - 1 ঘুরে ছোট সংখ্যা হয়ে যেত (পর্ব ৮-এর wraparound) — অনুশীলন ৩-এ সেটি হিসাব করবেন।

৫ · free আর coalescing — পাশের ফাঁকা জায়গা জোড়া দেওয়া

free-এর সবচেয়ে সহজ রূপ হলো শুধু flag মুছে দেওয়া। কিন্তু তাতে একটি নীরব সমস্যা তৈরি হয়। পাশাপাশি তিনটি ছোট খালি block থাকলে allocator সেগুলোকে তিনটি আলাদা ছোট জায়গা হিসেবেই দেখে — একটি বড় অনুরোধ এলে বলবে “জায়গা নেই”, অথচ জায়গা আছে! সমাধান: free করার সময় প্রতিবেশী খালি কি না দেখে জোড়া দেওয়া, যার নাম coalescing.

পরের প্রতিবেশী খুঁজে পাওয়া সহজ — নিজের header-এ আকার আছে, যোগ করলেই পরের header. কিন্তু আগের প্রতিবেশী? header শুধু সামনের দিকে পথ দেখায়। এর সমাধান দিয়েছিলেন Donald Knuth — boundary tag: প্রতিটি block-এর শেষেও আকারের একটি কপি রাখো (footer). তাহলে যেকোনো block-এর header-এর ঠিক আগের কয়েকটি byte পড়লেই আগের block-এর আকার ও অবস্থা জানা যায়। দুই দিকেই জোড়া দেওয়া যায় এক ধাপে, পুরো heap না ঘুরেই।

A used 32@0
B used 64@48
B free 64@48
C used 16@128
C free 16@128
D free 128@160
C+D free 160@128
B+C+D free 240@48
used 208@48
free 16@272
allocator-এর হিসাবপ্রতিটি block-এ header + footer
largest free block: 128
  1. শুরু। A, B, C ব্যবহার হচ্ছে; D খালি, payload ১২৮. এই মুহূর্তে কেউ malloc(200) চাইলে পাবে না — সবচেয়ে বড় খালি block ১২৮.
  2. free(B). B-এর flag মুছে গেল। দুই পাশে A আর C — দুজনেই ব্যবহার হচ্ছে, তাই জোড়ার কিছু নেই। B এখন used block-এর মাঝখানে একটি “গর্ত”।
  3. free(C) — কিন্তু জোড়া না দিলে? এখন B, C, D পাশাপাশি তিনটিই খালি, মোট ৬৪ + ১৬ + ১২৮ = ২০৮ byte payload, আর মাঝের দুটি header মিলিয়ে আরও ৩২. তবু allocator-এর চোখে এরা তিনটি আলাদা টুকরো — malloc(200) এখনো ব্যর্থ। এটাই coalescing না করার শাস্তি।
  4. সামনের দিকে জোড়া। C-এর header-এ আকার ১৬, তাই পরের header offset ১৬০-এ — D, খালি। দুটি মিলে এক block: C-এর ১৬ + D-এর header-এর ১৬ + D-এর ১২৮ = ১৬০. D-এর header আর দরকার নেই, সেটি এখন payload-এর অংশ।
  5. পেছনের দিকে জোড়া — boundary tag. C-এর header-এর ঠিক আগের ৮ byte হলো B-এর footer: “আকার ৬৪, খালি”। তাই B-ও যুক্ত হলো: ৬৪ + ১৬ + ১৬০ = ২৪০. Footer না থাকলে B-কে খুঁজতে heap-এর শুরু থেকে হাঁটতে হতো — আমাদের ছোট কোড ঠিক সেটাই করে।
  6. এবার বড় অনুরোধ সফল। malloc(200) গোল হয়ে ২০৮, ২৪০-এর ভেতরে আঁটে; split করে বাকি ১৬ byte-এর ছোট একটি খালি block থাকল। মোট মেমরি এক byte-ও বাড়েনি — শুধু হিসাব গুছিয়ে রাখায় অসম্ভব কাজটি সম্ভব হলো।

Double free ধরা — flag আর magic number

উপরের কোডে my_free আগে flag দেখে নেয় — flag আগে থেকেই 0 হলে বুঝে যায় এটি double free. আরও কড়া allocator header-এ একটি magic number রাখে — ধরুন used block-এ একটি নির্দিষ্ট বিশেষ সংখ্যা, free block-এ আরেকটি। free-এর সময় সংখ্যাটি না মিললে বোঝা যায়, হয় ঠিকানাটি কখনো malloc দেয়নি, নয়তো কেউ header-এর ওপর লিখে ফেলেছে।

এই পরীক্ষা নিখুঁত নয়। Coalescing-এর পরে পুরোনো header-টি অন্য block-এর payload-এর ভেতরে চলে যায়, যেখানে নতুন মালিকের ডেটাও বসতে পারে। সেই ডেটা ঘটনাক্রমে “used”-এর মতো দেখালে double free ধরাই পড়বে না, বরং হিসাব নষ্ট হবে। তাই header-এর পরীক্ষা ধোঁয়া-সংকেত মাত্র, গ্যারান্টি নয়; আসল সমাধান এমন বাগ হতে না দেওয়া আর sanitizer চালানো।

৬ · Fragmentation — জায়গা আছে, তবু পাওয়া যায় না

Fragmentation দুই রকম, আর দুটির কারণ ও প্রতিকার আলাদা:

🧱 Internal fragmentation

Block-এর ভেতরে নষ্ট জায়গা। ১৭ byte চাইলেন, alignment-এর জন্য পেলেন ৩২ — ১৫ byte কারও কাজে আসে না। Header-এর জায়গাও এক রকম খরচ।

কারণ: গোল করা, ন্যূনতম মাপ

🕳️ External fragmentation

Block-গুলোর মাঝে ছড়ানো ছোট ছোট ফাঁকা জায়গা। মোট ফাঁকা অনেক, কিন্তু কোনো একটি টানা টুকরো যথেষ্ট বড় নয়।

কারণ: বিভিন্ন মাপ ও আয়ুর block মিশে থাকা

নিচের ডায়াগ্রামে external fragmentation-কে চোখের সামনে ঘটতে দেখুন। সরলতার জন্য এখানে header বাদ দিয়ে শুধু payload দেখানো হয়েছে:

used 321
used 322
free 322
used 323
used 324
free 324
used 325
used 326
free 326
used 327
used 328
free 328
হিসাবheap = 256 byte
used 256, free 0
  1. ভরা heap. একটি প্রোগ্রাম ৩২ byte করে ৮টি জিনিস নিল — ধরুন একটি চ্যাট অ্যাপের ৮টি মেসেজ। ২৫৬ byte-এর heap পুরো ভরা।
  2. এক ঘর পরপর free. জোড় নম্বরের মেসেজগুলো মুছে ফেলা হলো। এখন মোট ১২৮ byte খালি — heap-এর ঠিক অর্ধেক।
  3. malloc(100) ব্যর্থ! Allocator চারটি গর্তই দেখল — প্রতিটি মাত্র ৩২. মোট ফাঁকা ১০০-র চেয়ে বেশি, কিন্তু টানা ১০০ byte কোথাও নেই। এটাই external fragmentation.
  4. কেন সারানো যায় না? গর্তগুলোর মাঝে ৩, ৫, ৭ নম্বর block ব্যবহার হচ্ছে, তাই coalescing কাজে আসে না। আর C-তে used block সরানো যায় না — তাদের ঠিকানা কোনো না কোনো pointer-এ রাখা আছে।
  5. ভেতরের অপচয়ও আছে। ১ নম্বর ঘরে আসলে ১৭ byte চাওয়া হয়েছিল, alignment-এর জন্য পেল ৩২ — ১৫ byte internal fragmentation. ছোট ছোট অনুরোধে এই অনুপাত ভয়ানক হতে পারে।
  6. প্রতিকার কোথায়? একই মাপের জিনিস এক জায়গায় রাখা (size class), একই সময়ে মরবে এমন জিনিস এক arena-তে রাখা, আর অকারণে ছোট টুকরো না ভাঙা। এগুলো অংশ ৭ ও ৮-এর বিষয়।
মূল ধারণা

Java, C# বা Go-র মতো ভাষার garbage collector দরকার হলে বেঁচে থাকা জিনিসগুলো সরিয়ে এক পাশে জড়ো করতে পারে (compaction), কারণ সে নিজেই সব reference বদলে দিতে পারে। C-তে pointer হলো কাঁচা ঠিকানা — allocator জানে না কোথায় কোথায় সেগুলো রাখা আছে। তাই C-এর allocator কখনো block সরাতে পারে না। External fragmentation C-তে তাই প্রতিরোধের বিষয়, প্রতিকারের নয়।

কোন খালি block বেছে নেবেন — placement policy

খালি block বেছে নেওয়ার নিয়মকে বলে placement policy. তিনটি ক্লাসিক নিয়ম:

নিয়মকীভাবে বাছেসুবিধাঅসুবিধা
First fitশুরু থেকে প্রথম মাপমতো blockসহজ, সাধারণত দ্রুত থামেশুরুর দিকে ছোট ছোট টুকরো জমে, প্রতিবার সেগুলো পেরোতে হয়
Next fitআগের বার যেখানে থেমেছিল, সেখান থেকে খোঁজা শুরুশুরুর ভিড় এড়ায়গবেষণায় প্রায়ই fragmentation বেশি দেখা গেছে
Best fitসব খালি block দেখে সবচেয়ে ছোট মাপমতোটিবড় block অক্ষত থাকে, অপচয় কমসব দেখতে হয় (সাজানো গঠন না থাকলে ধীর); খুব ছোট অকেজো টুকরো তৈরি করতে পারে

কোনটি “সেরা”, তার সর্বজনীন উত্তর নেই — নির্ভর করে প্রোগ্রাম কোন মাপের জিনিস কোন ক্রমে নেয় ও ছাড়ে, তার ওপর।

৭ · বাস্তব allocator কীভাবে দ্রুত হয় — size class, thread cache, realloc, calloc

মাপ অনুযায়ী আলাদা তালিকা

একটি লম্বা তালিকা ঘুরে মাপমতো block খোঁজা ধীর। বাস্তবের প্রায় সব বড় allocator তাই segregated free list ব্যবহার করে: মাপ অনুযায়ী আলাদা আলাদা free list. ১৬ byte-এর খালি block এক তালিকায়, ৩২-এর আরেকটিতে, ৪৮-এর আরেকটিতে… কেউ ৪০ চাইলে সরাসরি ৪৮-এর তালিকার মাথা থেকে একটি তুলে দাও — খোঁজাই লাগে না, কাজটি প্রায় O(1). এই নির্দিষ্ট মাপগুলোর নাম size class.

বাসস্ট্যান্ডের পাশের জুতার দোকানের কথা ভাবুন। দোকানি সব জুতা একটি স্তূপে রাখলে প্রতিটি ক্রেতার জন্য পুরো স্তূপ ঘাঁটতে হতো। তাই তিনি তাক বানিয়েছেন — ৬ নম্বর এক তাকে, ৭ নম্বর আরেক তাকে। ক্রেতা “৭” বললেই হাত বাড়িয়ে দেন। সাড়ে ছয় নম্বর পায়ের মানুষও ৭-ই নেন — একটু ঢিলা (internal fragmentation), কিন্তু কেনাবেচা দ্রুত।

Size class আরেকটি উপকার করে: একই মাপের জিনিস পাশাপাশি থাকে, তাই external fragmentation কমে। খুব বড় অনুরোধ (glibc-এ ডিফল্টে প্রায় ১২৮ KB-র বেশি) সাধারণত আলাদা পথে, সরাসরি mmap দিয়ে সামলানো হয়।

একাধিক thread — খাতা একটি, হাত অনেক

পর্ব ৫২-এ আপনি thread নিয়ে কাজ করবেন। তখন একটি নতুন সমস্যা দাঁড়ায়: আমাদের allocator-এর heap আর header-গুলো সব thread-এর মধ্যে ভাগ করা। দুটি thread একসাথে একই খালি block-কে “আমার” বানাতে গেলে data race — দুজনেই একই জায়গা পাবে, হিসাব ভেঙে যাবে। সহজতম সমাধান একটি mutex: যে malloc ডাকছে, সে তালা দিয়ে ঢুকবে। কিন্তু ৩২টি core-এর সার্ভারে সবাই একটি তালার সামনে লাইন দিলে (lock contention) গতি ভেঙে পড়ে।

বাস্তবের allocator-গুলো তাই কাজ ভাগ করে দেয়। glibc-এর ptmalloc একাধিক arena রাখে — আলাদা আলাদা heap, যার প্রতিটির নিজের তালা; বিভিন্ন thread বিভিন্ন arena ব্যবহার করে। ২০১৭ সালের glibc ২.২৬ থেকে প্রতিটি thread-এর একটি ছোট নিজস্ব ভান্ডারও আছে, tcache: সম্প্রতি ছাড়া কয়েকটি ছোট block সেখানে জমা থাকে, আর পরের malloc তালা ছাড়াই সেখান থেকে পায়। পর্ব ২০-এ যে মেসেজটি দেখেছিলেন — free(): double free detected in tcache 2 — সেটি এই tcache-এরই পাহারা।

অন্য বিখ্যাত allocator-গুলোও একই মূল ধারণায় দাঁড়িয়ে — thread-প্রতি বা core-প্রতি cache, তার পেছনে ভাগ করা কেন্দ্রীয় ভান্ডার, আর size class: jemalloc (FreeBSD থেকে আসা, Facebook-এ বহুল ব্যবহৃত), Google-এর tcmalloc (“thread-caching malloc”), আর Microsoft Research-এর mimalloc.

realloc-এর আসল চেহারা

realloc দেখতে একটি ফাংশন, কিন্তু ভেতরে তিনটি আলাদা পথ। নিজের allocator-এ লিখলে মোটামুটি এমন হবে:

void *my_realloc(void *p, size_t n) {
    if (p == NULL) return my_malloc(n);        /* NULL হলে সাধারণ malloc */
    size_t old = payload_size(p);              /* header থেকে পুরোনো আকার */
    if (n <= old) return p;                    /* ১. জায়গা আছে: একই ঠিকানা (চাইলে split) */
    if (grow_into_next_free_block(p, n))       /* ২. পাশের block খালি ও যথেষ্ট বড়: */
        return p;                              /*    জোড়া দিয়ে একই ঠিকানায় বাড়ানো */
    void *q = my_malloc(n);                    /* ৩. নতুন জায়গা, কপি, পুরোনো free */
    if (q == NULL) return NULL;                /*    ব্যর্থ হলে পুরোনো block অক্ষত! */
    memcpy(q, p, old);
    my_free(p);
    return q;
}

তিন নম্বর পথটিই পর্ব ২০-এর সেই সতর্কতার কারণ — ঠিকানা বদলে যেতে পারে, আর ব্যর্থ হলে পুরোনো block থেকে যায়। তাই p = realloc(p, n) লিখলে ব্যর্থতার সময় একমাত্র ঠিকানাটি হারিয়ে leak হয়; সবসময় একটি অস্থায়ী ভ্যারিয়েবলে নিন। আরেকটি সূক্ষ্মতা: realloc(p, 0)-এর আচরণ নিয়ে বিভিন্ন library বরাবর একমত ছিল না, আর C23 এটিকে সরাসরি undefined behavior বলে দিয়েছে — এমন ডাক এড়িয়ে চলুন।

calloc আর গুণের overflow

calloc-এর মূল্য শুধু শূন্য দিয়ে ভরায় নয়। count * size গুণ করতে গেলে size_t-এর সীমা পেরিয়ে ছোট একটি সংখ্যা হয়ে যেতে পারে — তখন ছোট block দেওয়া হবে, আর ব্যবহারকারী বড় ভেবে লিখতে থাকবেন। ভালো calloc গুণ করার আগেই ভাগ করে যাচাই করে:

void *safe_calloc(size_t count, size_t size) {
    if (size != 0 && count > SIZE_MAX / size) return NULL;  /* গুণ করলে overflow হতো */
    void *p = malloc(count * size);
    if (p != NULL) memset(p, 0, count * size);
    return p;
}

এই কোর্সের Linux compiler-এ glibc-এর আসল calloc(SIZE_MAX / 2, 4) চালিয়ে দেখেছি — নিরাপদে NULL দেয়। আধুনিক বিকল্পও আছে: GCC/Clang-এর __builtin_mul_overflow, আর C23-এর <stdckdint.h>-এ ckd_mul — দুটিই checked arithmetic. OpenBSD এই একই চিন্তা থেকে reallocarray এনেছিল, যা পরে glibc-তেও এসেছে।

৮ · বিশেষ কাজের allocator — arena আর pool

সাধারণ malloc সবার জন্য বানানো। কিন্তু মেমরি ব্যবহারের ধরন জানা থাকলে অনেক সহজ, অনেক দ্রুত allocator বানানো যায়।

Arena (bump) allocator — সব একসাথে ছেড়ে দেওয়া

একটি দোকানের দিনের বিক্রির CSV ফাইল পড়ার কথা ভাবুন। প্রতিটি লাইনে দোকানের নাম আর টাকার অঙ্ক। প্রোগ্রামটি শত শত ছোট string বানাবে, হিসাব করবে, রিপোর্ট দেবে — তারপর সবকিছু একসাথে ফেলে দেবে। প্রতিটি string-এর জন্য আলাদা malloc আর আলাদা free অপ্রয়োজনীয় খরচ, আর একটি free ভুলে গেলেই leak.

Arena allocator এই ধরনের জন্য তৈরি। একটি বড় buffer আর একটি সংখ্যা — used, অর্থাৎ কতদূর ভরা হয়েছে। নতুন অনুরোধ এলে used-কে aligned জায়গায় গোল করো, সেখান থেকে দাও, used সামনে ঠেলে দাও (“bump”). কোনো header নেই, কোনো free list নেই, খোঁজাও নেই। আর ছেড়ে দেওয়ার সময় একটি লাইন: used = 0.

1void *arena_alloc(Arena *a, size_t n) {2    size_t start = round_up(a->used, 16);3    if (start > a->cap || n > a->cap - start) return NULL;4    a->used = start + n;5    return a->buf + start;6}7/* no arena_free(name) exists */8ar.used = 0;        /* reset */
Arena-র অবস্থা
n
start
used
  1. খালি arena. ২৫৬ byte-এর একটি buffer, used = 0. পুরো allocator-এর “খাতা” এই একটিমাত্র সংখ্যা।
  2. প্রথম নাম: "Rahim Store". ১১টি অক্ষর আর শেষের '\0' — ১২ byte. used ০, গোল করেও ০; তাই offset ০ থেকে দেওয়া হলো, used হলো ১২. পুরো কাজটি কয়েকটি যোগ-বিয়োগ।
  3. দ্বিতীয় নাম: "Karim Tea Stall". ১৬ byte লাগবে। used ১২-কে ১৬-এর গুণিতকে গোল করলে ১৬ — মাঝের ৪ byte alignment-এর padding. Offset ১৬ থেকে দেওয়া হলো, used এখন ৩২.
  4. তৃতীয় নাম: "Nadia Fashion". ১৪ byte, offset ৩২ থেকে, used ৪৬. খেয়াল করুন লাইন ৩: n > a->cap - start লেখা হয়েছে, start + n > cap নয় — যোগফল overflow হয়ে ঘুরে যাওয়ার ভয় নেই।
  5. দাম: একটিকে আলাদা ছাড়া যায় না। মাঝের "Karim Tea Stall" আর লাগবে না? দুঃখিত — arena-তে কোনো header নেই, তাই কোনটি কত বড়, কারও মনে নেই। একক free-এর সুবিধা ছেড়ে দিয়েই arena এত দ্রুত।
  6. Reset — সব একসাথে মুক্ত। used = 0, ব্যস — শত শত string এক নির্দেশে ছাড়া হলো। কিন্তু সাবধান: আগের তিনটি pointer এখনো পুরোনো ঠিকানা ধরে আছে; reset-এর পরে সেগুলো ব্যবহার করা এক ধরনের use-after-free.

এবার পুরো প্রোগ্রামটি — একটি ছোট CSV থেকে দোকানের নামগুলো arena-তে কপি করে মোট বিক্রি হিসাব করা হচ্ছে:

#include <stdio.h>
#include <string.h>
#include <stddef.h>
typedef struct { unsigned char *buf; size_t cap, used; } Arena;
static void *arena_alloc(Arena *a, size_t n) {
    size_t al = _Alignof(max_align_t);
    size_t start = (a->used + al - 1) / al * al;            /* round up */
    if (start > a->cap || n > a->cap - start) return NULL;  /* no overflow */
    a->used = start + n;                                    /* just bump */
    return a->buf + start;
}
int main(void) {
    static _Alignas(max_align_t) unsigned char mem[256];
    Arena ar = { mem, sizeof mem, 0 };
    const char *p = "Rahim Store,1200\nKarim Tea Stall,350\nNadia Fashion,4100\n";
    long total = 0;
    while (*p) {
        size_t len = strcspn(p, ",");
        char *name = arena_alloc(&ar, len + 1);   /* many small strings */
        if (name == NULL) { printf("arena full\n"); break; }
        memcpy(name, p, len);
        name[len] = '\0';
        long amount = 0;
        sscanf(p + len + 1, "%ld", &amount);
        total += amount;
        printf("%-16s %5ld  offset %td\n", name, amount, (unsigned char *)name - mem);
        p += strcspn(p, "\n") + 1;
    }
    printf("total %ld, arena used %zu of %zu bytes\n", total, ar.used, ar.cap);
    ar.used = 0;                                  /* free everything at once */
    printf("after reset: used %zu\n", ar.used);
    return 0;
}

প্রতিটি লাইনের কমা পর্যন্ত অংশের দৈর্ঘ্য strcspn(p, ",") দিয়ে মাপা হলো, তারপর arena_alloc থেকে ঠিক ততটা (আর '\0'-এর ১ byte) নিয়ে নামটি কপি করা হলো। Offset ০, ১৬, ৩২ — প্রতিটি নাম ১৬-এর গুণিতকে শুরু, ঠিক stepper-এর মতো। মোট বিক্রি ১২০০ + ৩৫০ + ৪১০০ = ৫৬৫০ টাকা, আর ২৫৬ byte-এর মধ্যে ব্যবহার হয়েছে মাত্র ৪৬. পুরো প্রোগ্রামে একটিও malloc বা free নেই, তাই leak হওয়ার সুযোগও নেই।

Pool allocator — একই মাপের অনেক জিনিস

পর্ব ৩৩-এর linked list-এর কথা মনে করুন। প্রতিটি node একই মাপের, আর node বারবার যোগ ও মোছা হয়। প্রতিটির জন্য সাধারণ malloc ডাকলে প্রতিবার header-এর খরচ, খোঁজার খরচ, আর node-গুলো heap-এর এদিক-ওদিক ছড়িয়ে পড়ে (cache miss বাড়ে)।

Pool allocator শুরুতেই একই মাপের অনেকগুলো ঘর (slot) বানিয়ে রাখে। খালি ঘরগুলো একটি free list-এ গাঁথা — আর সেই তালিকার next pointer থাকে খালি ঘরের নিজের ভেতরেই, কারণ ঘরটি তো এখন খালি! পর্ব ৩৩-এ “malloc-এর ভেতরের free list” প্রসঙ্গে ঠিক এই কৌশলের কথা বলেছিলাম:

struct Node { int value; struct Node *next; };
typedef union Slot { union Slot *next_free; struct Node node; } Slot;
static Slot slots[1000];
static Slot *free_head;

void pool_init(void) {
    for (int i = 0; i < 999; i++) slots[i].next_free = &slots[i + 1];
    slots[999].next_free = NULL;
    free_head = &slots[0];
}
struct Node *pool_alloc(void) {
    if (free_head == NULL) return NULL;      /* pool খালি */
    Slot *s = free_head;
    free_head = s->next_free;                /* তালিকার মাথা সরাও: O(1) */
    return &s->node;
}
void pool_free(struct Node *n) {
    Slot *s = (Slot *)n;                     /* union: একই ঠিকানা */
    s->next_free = free_head;                /* মাথায় ঢোকাও: O(1) */
    free_head = s;
}

union (পর্ব ২২) এখানে দারুণ কাজ করছে: একটি ঘর হয় খালি — তখন তার ভেতরে next_free; নয়তো ব্যবহৃত — তখন ভেতরে একটি Node. একসাথে দুটো কখনো নয়, তাই একই জায়গা ভাগ করা নিরাপদ। নেওয়া আর ছাড়া দুটোই দুই-তিনটি নির্দেশ, কোনো header নেই, internal fragmentation নেই, আর সব node এক টানা array-তে — cache-বান্ধব। Linux kernel-এর slab allocator একই চিন্তার বড় রূপ: প্রতিটি ঘন ঘন ব্যবহৃত kernel struct-এর জন্য আলাদা cache.

Allocatorমাপছাড়ার নিয়মকোথায় মানায়
সাধারণ mallocযেকোনোযেকোনো ক্রমে, একটি একটিযখন ধরন জানা নেই — বেশিরভাগ কোড
Arenaযেকোনোশুধু সব একসাথেparser, compiler, ওয়েব request, game-এর প্রতিটি frame
Poolএকটিই নির্দিষ্ট মাপযেকোনো ক্রমে, একটি একটিlist/tree-র node, network packet, game-এর গুলি বা কণা

৯ · যখন allocator-এর খাতা নষ্ট হয় — heap-এর বাগ ভেতর থেকে

এখন আপনি জানেন প্রতিটি payload-এর ঠিক আগে ও পরে allocator-এর নিজের তথ্য — heap metadata — থাকে। পর্ব ২০-এর পাঁচটি বিপজ্জনক ভুল এবার ভেতর থেকে বোঝা যায়। এখানে উদ্দেশ্য প্রতিরক্ষা: বাগটি কীভাবে খাতা নষ্ট করে, আর কীভাবে ধরবেন।

বাগAllocator-এর খাতায় যা ঘটেলক্ষণ
Heap buffer overflow৩২ byte-এর payload-এ ৪০ byte লিখলে শেষের ৮ byte পড়ে পরের block-এর header-এ। আকার বা flag বদলে যায়।বাগ এক জায়গায়, crash অনেক পরে অন্য কোনো malloc/free-এর ভেতরে
Double freeএকই block দুবার খালি-তালিকায় ঢোকে। পরে দুটি আলাদা malloc একই জায়গা পায়।দুটি ভিন্ন জিনিস একে অপরের ডেটা নষ্ট করে
Use-after-freeখালি block-এর প্রথম byte-গুলোতে allocator free list-এর next pointer রাখে; পুরোনো pointer দিয়ে লিখলে সেই তালিকা নষ্ট হয়, অথবা নতুন মালিকের ডেটা বদলে যায়।এলোমেলো crash, ভুল ডেটা, নিরাপত্তা-ত্রুটি
ভুল ঠিকানা freeAllocator ঠিকানার আগের byte-কে header ভেবে পড়ে — সেখানে আসলে অন্য কিছু।free(): invalid pointer দিয়ে abort, বা নীরব নষ্ট

সবচেয়ে কষ্টের বিষয় হলো কারণ আর লক্ষণের মধ্যে দূরত্ব। আমাদের ছোট allocator-এ a-তে ৩২-এর জায়গায় ৪০ byte লিখলে b-এর header নষ্ট হবে, কোনো error ছাড়াই। Crash দেখা যাবে পরে my_free-এর ভেতরে — অথচ দোষ a-তে লেখা কোডের।

পাহারাদার — sanitizer, Valgrind আর hardened allocator

পর্ব ৩১-এ যে sanitizer আর Valgrind দেখেছেন, সেগুলো ঠিক এই সমস্যার জন্যই। AddressSanitizer প্রতিটি block-এর দুই পাশে নিষিদ্ধ এলাকা (redzone) রাখে আর free হওয়া block কিছুক্ষণ আটকে রাখে (quarantine) — তাই overflow বা use-after-free যে লাইনে ঘটে, সেখানেই ধরা পড়ে, পরের কোনো দুর্ঘটনায় নয়। Valgrind কোড না বদলেই প্রতিটি মেমরি-ব্যবহার পরীক্ষা করে।

gcc -g -fsanitize=address,undefined my_alloc_test.c -o t && ./t
valgrind --leak-check=full ./t

একটি সূক্ষ্ম কথা: ASan চেনে শুধু আসল malloc-এর block — আপনার static array-র ভেতরের ভুল সে সাধারণত দেখতে পায় না। তাই নিজের allocator-কে আলাদা করে test করুন: হাজার বার এলোমেলো malloc/free চালিয়ে প্রতিবার হিসাব মেলান (সব block-এর আকারের যোগফল = পুরো heap? পাশাপাশি দুটি খালি block নেই তো?)।

Production-এর জন্যও প্রতিরক্ষা আছে। Hardened allocator নিরাপত্তাকে গতির ওপরে রাখে: metadata-কে payload থেকে দূরে আলাদা জায়গায় রাখা, free list-এর pointer গোপন চাবি দিয়ে এনকোড করা, block-এর ঠিকানা এলোমেলো করা, double free কড়াভাবে ধরা। Android-এর Scudo, GrapheneOS-এর hardened_malloc, আর Chrome-এর PartitionAlloc এই ধারার উদাহরণ; glibc-ও তার free list-এ “safe-linking” নামে pointer এনকোড করার কৌশল যোগ করেছে। এগুলো বাগ দূর করে না — শুধু বাগকে হামলায় পরিণত করা কঠিন করে।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই বিষয়ে যেসব প্রশ্ন আসতে পারে:

  • free(p) কীভাবে জানে কত byte ছাড়তে হবে? — malloc payload-এর ঠিক আগে একটি header-এ block-এর আকার লিখে রাখে; free সেখান থেকে পড়ে।
  • Internal আর external fragmentation-এর পার্থক্য কী? — প্রথমটি block-এর ভেতরে অপচয় (গোল করা); দ্বিতীয়টি block-গুলোর মাঝে ছড়ানো ফাঁকা জায়গা, যা মিলিয়ে বড় অনুরোধ মেটানো যায় না।
  • First fit আর best fit-এর মধ্যে কোনটি ভালো? — সর্বজনীন উত্তর নেই; first fit সহজ ও দ্রুত থামে, best fit বড় block বাঁচায় কিন্তু সব খুঁজতে হয়।
  • malloc আর calloc-এর পার্থক্য? — calloc শূন্য দিয়ে ভরে, আর count × size-এর overflow যাচাই করে NULL দেয়।
  • একটি fixed-size object pool ডিজাইন করুন। — একই মাপের slot-এর array, খালি slot-গুলোর ভেতরেই next রেখে free list; নেওয়া ও ছাড়া দুটোই O(1).

অন্য ভাষায় একই ধারণা। C++-এ new/delete-এর পেছনে সাধারণত এই malloc-ই থাকে, আর C++17-এর std::pmr::monotonic_buffer_resource একটি তৈরি arena. Java-তে নতুন object নেওয়া আসলে thread-প্রতি buffer (TLAB) থেকে একটি bump allocation, আর ছাড়ার কাজটি করে garbage collection. CPython-এর pymalloc ছোট object-এর জন্য size class ব্যবহার করে।

১০ · AI-এর যুগে — AI-এর লেখা allocator যাচাই

“একটা memory allocator লিখে দাও” — AI কয়েক সেকেন্ডে পরিষ্কার দেখতে কোড দেবে। কিন্তু allocator-এর ভুল ধরা পড়ে লাখ বার malloc/free-এর পরে, অন্য thread-এ, অন্য মাপে। নিচের ভুলগুলো AI-এর allocator-এ বারবার দেখা যায়:

AI-এর ভুলকেন ক্ষতিকরকীভাবে ধরবেন
Alignment ভুল: ৮-এ গোল করা, বা ১২ byte-এর header৬৪-বিটে long double বা SIMD টাইপ ভুল ঠিকানায়; কিছু CPU-তে crashপ্রতিটি ফেরত ঠিকানা % _Alignof(max_align_t) == 0 কি না assert করুন
Coalescing নেই, বা শুধু এক দিকেদীর্ঘ সময় চললে heap টুকরো টুকরো, বড় অনুরোধ ব্যর্থসব free করার পরে heap আবার একটিমাত্র খালি block কি না দেখুন
আকারের হিসাবে integer overflow (n + header, count * size)বিশাল অনুরোধে ছোট block — নিশ্চিত buffer overflowSIZE_MAX-এর কাছাকাছি মাপ দিয়ে test করুন
Thread safety নেই — global free list, কোনো তালা নেইদুই thread একই block পায়; ভুল হয় কদাচিৎ, তাই ধরা কঠিনThreadSanitizer (-fsanitize=thread) দিয়ে বহু-thread test
Header-এর ঠিকানা ফেরত দেওয়া, বা free-তে ভুল দিকে পিছানোব্যবহারকারী প্রথম লেখাতেই খাতা মুছে ফেলেনmalloc-এর পরেই পুরো payload ভরে ফেলে হিসাব যাচাই
ব্যর্থ realloc-এ পুরোনো block free করে ফেলাNULL ফেরতের সাথে ডেটাও হারায়ব্যর্থতার পথ আলাদা করে test (ছোট heap দিয়ে)

AI-কে allocator লিখতে বলার চেয়ে বেশি কাজের হলো তাকে পর্যালোচক বানানো — আর শেষ রায়ের ভার দেওয়া টুলের হাতে। এমনভাবে প্রশ্ন করুন:

নিচে আমার লেখা একটি ছোট C memory allocator (first fit, split, coalescing). কোড আবার লিখে দিও না। শুধু পর্যালোচনা করো: [এখানে কোড পেস্ট করুন] ১. প্রতিটি ফেরত দেওয়া ঠিকানা কি _Alignof(max_align_t)-এর গুণিতক? কোন হিসাবে ভুল হতে পারে? ২. আকারের কোনো যোগ বা গুণে size_t overflow হতে পারে কি? কোন ইনপুটে? ৩. free-এর পরে পাশাপাশি দুটি খালি block থেকে যেতে পারে এমন কোনো পথ আছে? ৪. দুটি thread একসাথে ডাকলে কোন লাইনে data race হবে? ৫. এমন একটি test লিখে দাও যা হাজার বার এলোমেলো malloc/free করে প্রতিবার heap-এর হিসাব যাচাই করে, আর বলো কোন sanitizer flag দিয়ে চালাব।

AI-এর allocator হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Knuth-এর boundary tag আর buddy allocation — ১৯৬০-এর দশকের এই দুই ধারণা আজও কেন টিকে আছে?

১৯৬৮ সালে Donald Knuth তাঁর The Art of Computer Programming-এর প্রথম খণ্ডে “dynamic storage allocation” নিয়ে একটি পুরো অংশ লেখেন। সেখানে দুটি কৌশল বিস্তারিত বিশ্লেষণ করা হয়, যা আজও allocator-এর পাঠ্যসূচির ভিত্তি।

  • Boundary tag: প্রতিটি block-এর দুই প্রান্তে আকার ও অবস্থা লেখা। ফলে free করার সময় আগের আর পরের দুই প্রতিবেশীকেই এক ধাপে (O(1)) দেখা যায়, তালিকা ঘুরতে হয় না। খরচ হলো প্রতিটি block-এ বাড়তি footer-এর জায়গা। পরে allocator-গুলো একটি চতুর সাশ্রয় বের করে: footer শুধু খালি block-এ রাখা হয় (তখন payload-এর জায়গা তো খালিই), আর পরের block-এর header-এর একটি বিট বলে দেয় “আগেরটি খালি কি না”। glibc আজও এই ধারণা ব্যবহার করে।
  • Buddy system: Kenneth Knowlton ১৯৬৫ সালে এটি বর্ণনা করেন। সব block-এর মাপ দুইয়ের ঘাত (৩২, ৬৪, ১২৮…)। ১২৮-এর block ভাঙলে দুটি ৬৪-এর “জমজ ভাই” (buddy) হয়। জাদুটি হলো, একটি block-এর buddy-র offset বের করতে লাগে মাত্র একটি XOR: buddy = offset ^ size. ফেরত দেওয়ার সময় buddy-ও খালি হলে দুটি মিলে আবার বড় block — দ্রুত coalescing. দাম হলো internal fragmentation: ৬৫ byte চাইলে ১২৮ দিতে হয়, প্রায় অর্ধেক নষ্ট।

Knuth আরও একটি মজার পর্যবেক্ষণ বিশ্লেষণ করেন, যা “fifty percent rule” নামে পরিচিত: নির্দিষ্ট কিছু শর্তে, স্থিতিশীল অবস্থায় খালি block-এর সংখ্যা ব্যবহৃত block-এর প্রায় অর্ধেক থাকে। অর্থাৎ fragmentation কোনো দুর্ঘটনা নয় — এটি নেওয়া-ছাড়ার স্বাভাবিক ফল।

আজ কোথায় টিকে আছে? Linux kernel physical memory-র page বরাদ্দ করে buddy system দিয়েই, কারণ সেখানে মাপগুলো স্বাভাবিকভাবেই page-এর গুণিতক আর গতি সবচেয়ে জরুরি। ছোট kernel object-এর জন্য তার ওপরে বসে slab allocator — অর্থাৎ বাস্তবে কয়েকটি কৌশল স্তরে স্তরে সাজানো।

মূল উপলব্ধি: ভালো ধারণা পুরোনো হয় না — বদলায় শুধু প্রয়োগের জায়গা। আর প্রতিটি কৌশল একটি দাম দিয়ে একটি সুবিধা কেনে: boundary tag জায়গা দিয়ে গতি কেনে, buddy অপচয় দিয়ে সরলতা কেনে।

প্র ০২একজন অধ্যাপকের ১৯৮৭ সালের লেখা allocator কীভাবে কোটি কোটি Linux প্রোগ্রামের malloc হয়ে উঠল?

১৯৮৭ সালে মার্কিন অধ্যাপক Doug Lea নিজের C++ প্রোগ্রামের জন্য একটি allocator লিখতে শুরু করেন, যা পরে dlmalloc নামে পরিচিত হয়। তিনি কোডটি পাবলিক ডোমেইনে উন্মুক্ত রাখেন এবং বছরের পর বছর উন্নত করেন। এর মূল নকশাটি এই পর্বের প্রায় সব ধারণা একসাথে জুড়েছে:

  • প্রতিটি chunk-এ boundary tag, আকারের নিচের বিটে flag.
  • খালি chunk-গুলো মাপ অনুযায়ী অনেকগুলো “bin”-এ ভাগ — ছোট মাপের জন্য নির্দিষ্ট মাপের bin, বড় মাপের জন্য পরিসরভিত্তিক bin (segregated free list).
  • free-এর সময় সাথে সাথে coalescing, আর খুব বড় অনুরোধ সরাসরি mmap দিয়ে।

সমস্যা দেখা দিল thread-এর যুগে। dlmalloc-এর একটিই heap, একটিই তালা — বহু-thread প্রোগ্রামে সবাই লাইনে দাঁড়ায়। Wolfram Gloger dlmalloc-এর ওপর ভিত্তি করে ptmalloc (“pthreads malloc”) বানান, যেখানে একাধিক arena থাকে। glibc-এর malloc এসেছে এই ptmalloc (ptmalloc2) থেকে। পরে glibc নিজেও অনেক বদলেছে — যেমন ২০১৭ সালে (glibc ২.২৬) thread-প্রতি tcache যোগ হয়, যা ছোট অনুরোধকে তালা ছাড়াই মেটায়। dlmalloc নিজেও হারিয়ে যায়নি — Android-এর libc বহু বছর এটি ব্যবহার করেছে, আর WebAssembly-র জন্য Emscripten toolchain আজও ডিফল্টে এটিই দেয়।

এখানে একটি নিরাপত্তার শিক্ষাও আছে। tcache গতি এনেছিল, কিন্তু শুরুর দিকে তার পরীক্ষা ছিল খুব কম — double free সহজে ধরা পড়ত না। পরের সংস্করণগুলোতে পাহারা যোগ হয়েছে; পর্ব ২০-এর double free detected in tcache 2 বার্তাটি সেই পাহারারই ফল। গতি আর নিরাপত্তার এই টানাপোড়েন allocator-এর ইতিহাস জুড়ে।

মূল উপলব্ধি: একটি ভালোভাবে লেখা, উন্মুক্ত, দীর্ঘদিন যত্ন নেওয়া কোড পুরো শিল্পের ভিত্তি হয়ে উঠতে পারে। আপনি প্রতিদিন যে malloc ডাকেন, তার শেকড় একজন মানুষের নিজের প্রয়োজনে লেখা কোডে।

প্র ০৩jemalloc-এর মতো allocator কেন তৈরি হলো, আর বড় কোম্পানিগুলো কেন কোড না বদলে শুধু allocator বদলায়?

২০০৫ সালে Jason Evans FreeBSD অপারেটিং সিস্টেমের জন্য একটি নতুন allocator লেখেন — jemalloc. লক্ষ্য ছিল বহু-core কম্পিউটারে ভালোভাবে চলা আর দীর্ঘ সময় চললেও fragmentation কম রাখা। এটি পরে FreeBSD-এর ডিফল্ট allocator হয়। পরে Evans Facebook-এ যোগ দেন, আর সেখানকার বিশাল সার্ভার-বহরে jemalloc বহুল ব্যবহৃত হয়ে ওঠে; Firefox-ও দীর্ঘদিন jemalloc-ভিত্তিক allocator ব্যবহার করেছে।

কেন বড় সার্ভিস allocator বদলায়? কারণ তাদের সমস্যাগুলো ছোট প্রোগ্রামে দেখাই যায় না:

  • দীর্ঘ আয়ুর fragmentation: একটি সার্ভার সপ্তাহের পর সপ্তাহ চলে। ধীরে ধীরে মেমরি টুকরো হয়ে প্রসেসের RAM ব্যবহার বাড়তেই থাকে, যদিও কোনো leak নেই। হাজার হাজার মেশিনে কয়েক শতাংশ RAM বাঁচানো মানে বিপুল অর্থ সাশ্রয়।
  • বহু-core-এ গতি: ৬৪টি thread একসাথে malloc করলে তালার লাইন বা core-গুলোর মধ্যে cache line টানাটানি (false sharing) বড় খরচ।

আর বদলানো সহজ, কারণ সবাই একই চুক্তি মানে — malloc, free, realloc-এর একই নাম ও আচরণ। Linux-এ প্রোগ্রাম আবার compile না করেও LD_PRELOAD দিয়ে অন্য allocator বসিয়ে চালানো যায়।

সতর্কতা: কোনো allocator সব কাজে সেরা নয়। একটির গতি বেশি তো আরেকটি RAM কম খায়। তাই পেশাদার নিয়ম — নিজের আসল কাজের চাপে মেপে দেখুন (পর্ব ৫৫-এর মূল কথা), তারপর সিদ্ধান্ত নিন।

মূল উপলব্ধি: একটি পরিষ্কার interface (malloc/free) থাকলে তার পেছনের বাস্তবায়ন বদলে পুরো সিস্টেমের আচরণ বদলানো যায় — একটি লাইনও না ছুঁয়ে। ভালো interface-এর শক্তি এটাই।

প্র ০৪ব্রাউজারের নিরাপত্তা রিপোর্টে use-after-free এত বেশি কেন? আর এজন্যই কি memory-safe ভাষার দিকে ঝোঁক বাড়ছে?

Google-এর Chromium দল আর Microsoft — দুজনেই আলাদাভাবে জানিয়েছে, তাদের গুরুতর নিরাপত্তা-ত্রুটির প্রায় ৭০% মেমরি-নিরাপত্তা সংক্রান্ত। Chromium-এর হিসাবে এর মধ্যে বড় একটি অংশ use-after-free. কেন ব্রাউজারে এটি এত বেশি?

  • জটিল মালিকানা: একটি ওয়েবপেজে লাখো object — DOM node, style, event listener, ছবি — যারা একে অপরকে নির্দেশ করে। কে কখন free করবে, তার হিসাব রাখা অত্যন্ত কঠিন।
  • বাইরের নিয়ন্ত্রণ: যেকোনো ওয়েবসাইটের JavaScript ঠিক করতে পারে কোন object কখন তৈরি ও মুছে যাবে, কোন মাপের মেমরি কখন নেওয়া হবে। অর্থাৎ allocator-এর আচরণে বাইরের কারও প্রভাব আছে।
  • এই পর্বের শিক্ষা: free হওয়া block allocator প্রায়ই একই মাপের পরের অনুরোধকে দেয়। ফলে পুরোনো pointer এখন অন্য কোনো object-কে দেখায় — ভুলটি ডেটা নষ্ট থেকে নিয়ন্ত্রণ হারানো পর্যন্ত গড়াতে পারে।

প্রতিরক্ষা এসেছে কয়েক স্তরে। Chrome নিজস্ব allocator PartitionAlloc ব্যবহার করে, যা বিভিন্ন ধরনের object আলাদা এলাকায় রাখে; আর MiraclePtr নামের কৌশল free হওয়া মেমরিতে এখনো pointer থাকলে সেই মেমরি তখনই পুনর্ব্যবহার হতে দেয় না।

দীর্ঘমেয়াদি উত্তর হলো ভাষা বদলানো। C++-এ RAII আর smart pointer মালিকানাকে টাইপের অংশ বানায় — কে free করবে, তা কোডেই লেখা থাকে। Rust আরও এগিয়ে, compile-এর সময়েই use-after-free আটকায়। Firefox-এর কিছু অংশ, Android-এর নতুন কোড, এমনকি Linux kernel-এর কিছু driver এখন Rust-এ লেখা হচ্ছে, আর বিভিন্ন দেশের সাইবার-নিরাপত্তা সংস্থাও memory-safe ভাষা-র দিকে যাওয়ার পরামর্শ দিচ্ছে। তবু কোটি কোটি লাইনের পুরোনো C/C++ কোড রাতারাতি বদলানো যাবে না — তাই এই পর্বের জ্ঞান আরও বহু বছর দরকার থাকবে।

মূল উপলব্ধি: মানুষের সতর্কতা দিয়ে লাখো object-এর মালিকানা নিখুঁতভাবে সামলানো যায় না। নিরাপদ সিস্টেম তাই নিয়মকে মানুষের মাথা থেকে সরিয়ে টাইপ-সিস্টেম, টুল আর allocator-এর নকশায় বসায়।

প্র ০৫Garbage collection থাকলে তো এত ঝামেলাই নেই — তাহলে C-এর মতো হাতে মেমরি সামলানো কেন আজও টিকে আছে?

Garbage collection (GC) আর হাতে সামলানো (manual) মেমরি — দুটিই আসলে দাম আর সুবিধার আলাদা হিসাব।

  • GC-এর সুবিধা: use-after-free, double free আর বেশিরভাগ leak প্রায় অসম্ভব। নতুন object নেওয়াও অত্যন্ত দ্রুত — Java-র মতো ভাষায় এটি আসলে এই পর্বের bump allocation, thread-প্রতি আলাদা buffer থেকে। আর moving collector জীবিত object সরিয়ে জড়ো করতে পারে, তাই external fragmentation দূর করা যায় — যা C কখনো পারে না। বিস্তারিত দেখুন Java কোর্সের JVM মেমরি ও GC পর্বে.
  • GC-এর দাম: ভালো গতিতে চলতে GC সাধারণত জীবিত ডেটার চেয়ে বেশ খানিকটা বাড়তি মেমরি চায়। Collector চলার সময় কাজ থামতে পারে (pause) — আধুনিক collector-এ সেটি অনেক ছোট, তবু একেবারে শূন্য নয়, আর কখন হবে তা পুরোপুরি আপনার হাতে নয়। ২০২০ সালে Discord জানিয়েছিল, তাদের একটি Go সার্ভিসে GC-এর কারণে নিয়মিত বিরতিতে latency বেড়ে যেত; সার্ভিসটি Rust-এ নতুন করে লেখার পর সেই সমস্যা চলে যায়।
  • Manual-এর সুবিধা: মেমরি কখন ছাড়বে তা পুরোপুরি নির্ধারিত, বাড়তি মেমরি কম, আর arena বা pool-এর মতো কাজের ধরন-ভিত্তিক allocator বানানো যায়। Operating system, game engine, এমবেডেড firmware, ডেটাবেস — যেখানে প্রতিটি মিলিসেকেন্ড বা প্রতিটি KB গোনা হয়, সেখানে এর কদর।

মাঝামাঝি পথও আছে। Python আর Swift প্রধানত reference counting ব্যবহার করে — শেষ reference চলে গেলেই সাথে সাথে free, তবে চক্রাকার reference সামলাতে বাড়তি ব্যবস্থা লাগে। C++-এর RAII আর Rust-এর ownership আরেকটি পথ: GC ছাড়াই compiler-কে দিয়ে সঠিক জায়গায় free বসানো।

মূল উপলব্ধি: প্রশ্নটি “কোনটি ভালো” নয়, “কোন কাজে কোন দাম দিতে রাজি”। আর যে পদ্ধতিই বেছে নিন, নিচে কোথাও একটি allocator ঠিক এই পর্বের কাজগুলোই করছে — সেটি বুঝলে যেকোনো ভাষার মেমরি-আচরণ বুঝতে পারবেন।

অনুশীলন

প্রথম তিনটি খাতা-কলমের কাজ; শেষটিতে ছোট একটি কোড লিখবেন।

  1. First fit বনাম best fit: একটি heap-এ block-গুলো এই ক্রমে আছে (payload, header ১৬ byte): [used 32] [free 128] [used 16] [free 48]. অনুরোধ এলো malloc(40). First fit আর best fit — কে কোন block দেবে, আর প্রতিটির পরে heap দেখতে কেমন হবে?

    ৪০ গোল হয়ে ৪৮, block লাগবে ১৬ + ৪৮ = ৬৪.

    First fit প্রথম খালি block ১২৮-এই থামে। ১২৮ − ৪৮ = ৮০ বাকি, যা header (১৬) + ৬৪ payload-এর জন্য যথেষ্ট — তাই ভাঙবে: [used 32] [used 48] [free 64] [used 16] [free 48].

    Best fit সব খালি block দেখে সবচেয়ে ছোট মাপমতো ৪৮ বাছবে — পুরোটাই ঠিক মাপে, ভাঙতে হবে না: [used 32] [free 128] [used 16] [used 48]. লক্ষ করুন, best fit বড় ১২৮-এর block অক্ষত রাখল — পরে কেউ ১০০ চাইলে পাবে। First fit-এর পরে সবচেয়ে বড় খালি block মাত্র ৬৪.

  2. Coalescing তুলে দিলে: উপরের tiny_alloc.c-তে my_free-এর শেষ তিন লাইন (coalescing-এর for লুপ) মুছে দিলে free a আর end লাইন দুটি কেমন ছাপা হবে? আগে খাতায় অনুমান করুন, তারপর চালিয়ে মিলিয়ে নিন।

    free a: [0 free 48] [48 free 128] [176 used 32] [208 free 816] — দুটি খালি block পাশাপাশি, কিন্তু আলাদা।

    d=40-এর ৬৪-এর block তখন প্রথম খালি ৪৮-এ আঁটে না, তাই ১২৮ থেকে কাটা হবে (offset ৪৮)। শেষে সব free করার পরেও heap থাকবে কয়েকটি আলাদা খালি টুকরোয় — [0 free 48] [48 free 64] [112 free 64] [176 free 32] [208 free 816]. একটিমাত্র [0 free 1024] আর ফেরত আসবে না; কেউ ১০০০ byte চাইলে ব্যর্থ হবে, যদিও পুরো heap খালি।

  3. Overflow হিসাব: my_malloc-এর প্রথম লাইন if (n > HEAP) return NULL; মুছে দিলাম। ৬৪-বিট মেশিনে কেউ my_malloc(SIZE_MAX - 5) ডাকল। need-এর মান কত হবে, আর কী বিপদ ঘটবে?

    n + AL - 1 = SIZE_MAX - 5 + 15 = SIZE_MAX + 10, যা ঘুরে হয় ৯ (কারণ SIZE_MAX + 1 মানে ০)। ৯ / ১৬ * ১৬ = ০, তাই need = ১৬ + ০ = ১৬ — শুধু একটি header-এর মাপ!

    Allocator খুশি মনে ১৬ byte-এর একটি block দেবে, যার payload শূন্য byte. কলকারী ভাববেন প্রায় পুরো ঠিকানা-জগৎ পেয়েছেন, আর লিখতে শুরু করলেই পরের header আর বাকি heap মুছে যাবে — নিশ্চিত heap overflow. এজন্যই আকারের প্রতিটি যোগ-গুণের আগে সীমা যাচাই করতে হয়।

  4. নিজের pool, নিজের হিসাব: অংশ ৮-এর pool allocator ব্যবহার করে একটি ছোট প্রোগ্রাম লিখুন: ৫টি node নিন, ২ ও ৪ নম্বর ছেড়ে দিন, তারপর আরও ২টি নিন। প্রতিবার node-এর slot নম্বর ((Slot *)n - slots) ছাপান। নতুন দুটি কোন slot পাবে? আর এই x86-64 Linux-এ ১০,০০০ node-এর জন্য pool আর glibc malloc-এ মোটামুটি কত মেমরি লাগবে?

    প্রথম পাঁচটি পাবে slot ০, ১, ২, ৩, ৪. তারপর ২ নম্বর node (slot ১) আর ৪ নম্বর node (slot ৩) ছাড়লে free list-এর মাথায় থাকবে শেষে ছাড়া slot ৩, তার পরে slot ১. তাই নতুন দুটি পাবে slot ৩, তারপর slot ১ — stack-এর মতো শেষে-ছাড়া-আগে-পাওয়া (LIFO). এটি cache-এর জন্যও ভালো: সদ্য ছাড়া ঘরটি সম্ভবত এখনো cache-এ আছে।

    এখানে struct Node ১৬ byte (int ৪ + padding ৪ + pointer ৮), sizeof(Slot)-ও ১৬. Pool: ১০,০০০ × ১৬ = ১,৬০,০০০ byte. glibc-এ প্রতিটি malloc(16) একটি ৩২ byte-এর chunk নেয় (৮ byte header, ১৬-এর গুণিতকে গোল করা, ন্যূনতম ৩২) — মোট প্রায় ৩,২০,০০০ byte, অর্থাৎ দ্বিগুণ। সাথে প্রতিবার malloc/free-এর খরচ আর ছড়িয়ে থাকা node-এর cache miss.

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

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

ক্রমFirst-fit malloc(n)-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📏 n-কে alignment-এর গুণিতকে গোল করে header-এর মাপ যোগ করা
  2. 🔍 শুরু থেকে প্রথম খালি ও যথেষ্ট বড় block খোঁজা
  3. ✂️ Block অনেক বড় হলে ভেঙে বাকি অংশ খালি রাখা
  4. 🏷️ Header-এ আকার লিখে used flag বসানো
  5. 📦 Header-এর ঠিক পরের ঠিকানা (payload) ফেরত দেওয়া

free(p)-এ শুধু ঠিকানা দেওয়া হয়, আকার নয়। তবু allocator কীভাবে জানে কত byte ফেরত নিতে হবে?

  • Operating system প্রতিটি block-এর আকার মনে রাখে
  • ঠিকানার ঠিক আগে রাখা header থেকে আকার পড়ে
  • Compiler প্রতিটি free-এর পাশে আকার বসিয়ে দেয়
  • Block-এর শেষে একটি '\0' খুঁজে আকার বের করে

৬৪-বিট x86-64 Linux-এ GCC-তে _Alignof(max_align_t)-এর মান সাধারণত কত, আর malloc-এর কাছে এর অর্থ কী?

  • ১ — malloc যেকোনো ঠিকানা দিতে পারে
  • ৮ — শুধু double-এর মাপ
  • ১৬ — malloc-এর প্রতিটি ঠিকানা ১৬-এর গুণিতক হতে হয়
  • ৬৪ — একটি cache line

একটি heap-এ মোট ১২৮ byte খালি, কিন্তু চারটি আলাদা ৩২ byte-এর টুকরোয় — মাঝে মাঝে ব্যবহৃত block. malloc(100) ব্যর্থ হলো। এটি কী?

  • Memory leak
  • Internal fragmentation
  • Double free
  • External fragmentation

Boundary tag (প্রতিটি block-এর শেষে আকারের একটি কপি, footer) রাখার মূল উদ্দেশ্য কী?

  • free-এর সময় আগের block খুঁজে এক ধাপে জোড়া দেওয়া
  • Internal fragmentation কমানো
  • একাধিক thread-কে নিরাপদ রাখা
  • Payload-কে aligned রাখা

একটি CSV parser শত শত ছোট string বানায়, আর ফাইল শেষে সবগুলো একসাথে ফেলে দেয়। কোন allocator সবচেয়ে মানানসই?

  • Fixed-size pool allocator
  • Buddy allocator
  • Arena (bump) allocator
  • প্রতিটি string-এর জন্য আলাদা malloc আর free

নিরাপদ calloc(count, size) লিখতে গুণ করার আগে কোন পরীক্ষাটি জরুরি?

  • count * size > SIZE_MAX কি না
  • size != 0 && count > SIZE_MAX / size কি না
  • count ঋণাত্মক কি না
  • count * size জোড় সংখ্যা কি না

শব্দকোষ

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

মনে রাখুন

  1. malloc সাধারণ C কোড: OS থেকে বড় টুকরো আনে (brk/mmap), তারপর ভাগ করে বিলি করে।
  2. প্রতিটি block-এর আগে header — আকার আর used flag (আকারের নিচের বিটে); ঠিকানা সবসময় max_align_t-তে aligned.
  3. malloc খোঁজে ও split করে; free flag মোছে ও পাশের খালি block-এর সাথে coalesce করে — boundary tag দুই দিকেই পথ দেখায়।
  4. Internal fragmentation ভেতরের অপচয়, external fragmentation মাঝের ছড়ানো ফাঁক; C-তে block সরানো যায় না, তাই প্রতিরোধই উপায়।
  5. কাজের ধরন জানলে arena (সব একসাথে ছাড়া) বা pool (একই মাপ); আর heap-এর বাগ মানে allocator-এর খাতা নষ্ট — sanitizer দিয়ে ধরুন।

পরের পর্বে: আজ দেখলেন একটি ভুল লেখা কীভাবে allocator-এর খাতা নষ্ট করে। এবার আরও বড় ছবি — undefined behavior কী, compiler কেন ধরে নেয় আপনি কখনো ভুল করবেন না, আর নিরাপদ C লেখার পেশাদার নিয়মগুলো।

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

  • পর্ব ৪৮ · Undefined Behavior ও নিরাপদ কোডিংপরবর্তী পর্বযে ভুল compiler-কেও বিভ্রান্ত করে, আর তা থেকে বাঁচার নিয়ম।
  • C প্রোগ্রামিং · কোর্স হোম৬০টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
  • Operating Systemsগভীরে যানVirtual memory, page আর kernel-এর মেমরি ব্যবস্থাপনা — allocator-এর নিচের স্তর।
  • C++ · RAII ও smart pointerপাশের ট্র্যাকমালিকানাকে টাইপের অংশ বানিয়ে free ভুলে যাওয়া বন্ধ করা।
  • Cybersecurityপাশের ট্র্যাকমেমরি-বাগ কেন নিরাপত্তার সবচেয়ে বড় দুর্বলতাগুলোর একটি।
এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান