এই পর্বে যা শিখবেন
- Fixed partition (IBM OS/360-এর MFT) আর variable partition (MVT) — কোনটিতে কোন ধরনের fragmentation হয়, আর কেন
- First, next, best আর worst fit-এর সঠিক নিয়ম, আর পরীক্ষার ক্লাসিক অঙ্কের পূর্ণ সমাধান — কোন অনুরোধ কোথায় বসে, কোনটি অপেক্ষা করে
- Internal আর external fragmentation-এর সঠিক সংজ্ঞা, ৫০-শতাংশ নিয়মের বক্তব্য আর ছোট প্রমাণ, আর compaction-এর শর্ত ও দাম
- Buddy system-এ ব্লক ভাগ (split) আর জোড়া (coalesce), XOR দিয়ে buddy-র ঠিকানা, আর এর internal fragmentation
- Program-এর ভেতরের malloc কীভাবে কাজ করে — free list, boundary tag, size class, আর glibc, jemalloc, tcmalloc, mimalloc-এর মূল ভাবনা
১ · এক টুকরো টানা জায়গা — contiguous allocation কী
পর্ব ৩০-এ দেখেছেন, প্রতিটি প্রসেস logical address দিয়ে কাজ করে, আর MMU সেটিকে physical ঠিকানায় অনুবাদ করে। সবচেয়ে সরল অনুবাদ-ব্যবস্থায় MMU-তে থাকে মাত্র দুটি register: একটি base (বা relocation register), যা যোগ হয়, আর একটি limit, যার বাইরে গেলে trap. এই ব্যবস্থায় একটি শর্ত আপনা থেকেই চলে আসে — প্রসেসের পুরো মেমরি physical RAM-এ এক টুকরো, টানা হতে হবে, কারণ base + offset একটি সরল রেখায় চলে।
এভাবে মেমরি ভাগ করার নামই contiguous allocation. সাধারণত মেমরির এক প্রান্তে (অনেক সিস্টেমে নিচের দিকে, যেখানে interrupt vector থাকে) OS নিজে বসে, বাকিটা user প্রসেসদের। নতুন প্রসেস এলে OS খোঁজে: কোথায় এর পুরো আকারের একটি খালি টানা জায়গা আছে? প্রসেস শেষ হলে তার জায়গা খালি হয়, আর পাশের খালি জায়গার সাথে মিশে বড় খালি জায়গা হতে পারে।
বিয়েবাড়ির কমিউনিটি সেন্টারের কথা ভাবুন। লম্বা সারিতে চেয়ার, অতিথিরা আসছেন-যাচ্ছেন। একটি পরিবার এসেছে আটজনের — তাঁরা পাশাপাশি এক সারিতে বসতে চান। হলে হয়তো মোট বারোটি চেয়ার খালি, কিন্তু ছড়িয়ে ছিটিয়ে: এখানে তিনটি, ওখানে দুটি, কোণে চারটি। আটটি পাশাপাশি খালি চেয়ার কোথাও নেই — পরিবারটিকে দাঁড়িয়ে থাকতে হয়। এটাই external fragmentation. আবার কেউ যদি নিয়ম করেন “প্রতিটি পরিবার একটি গোটা দশ-চেয়ারের টেবিল পাবে”, তাহলে ছয়জনের পরিবারের টেবিলে চারটি চেয়ার খালি পড়ে থাকে, অথচ অন্য কেউ সেখানে বসতে পারেন না — এটা internal fragmentation. এই পর্বের সব কৌশল আসলে এই দুই অপচয়ের মধ্যে দর-কষাকষি।
আজকের Linux, Windows বা macOS সাধারণ প্রসেসকে এভাবে মেমরি দেয় না — তারা paging ব্যবহার করে (পর্ব ৩২), যেখানে প্রসেসের মেমরি RAM-এর যেকোনো জায়গায় ছড়ানো ছোট ছোট frame-এ থাকতে পারে। তাহলে এই পর্ব কেন? চারটি কারণে:
- Paging কেন এল, তা বোঝার জন্য। External fragmentation-এর যন্ত্রণাই paging-এর মূল প্রেরণা।
- একই সমস্যা সবখানে। একটি প্রোগ্রামের ভেতরে malloc তার heap-এ ঠিক এই কাজ করে; kernel physical page-গুলো ভাগ করে buddy system দিয়ে; file system ডিস্কে টানা block খোঁজে (পর্ব ৪৩)। প্রতিটিতে fit কৌশল আর fragmentation একই রূপে ফিরে আসে।
- MMU-হীন সিস্টেমে এখনো এটিই বাস্তব। অনেক ছোট microcontroller-এ MMU নেই; সেখানে RTOS-এর heap সরাসরি contiguous. যেমন FreeRTOS-এর heap_4 নামের allocator first fit ব্যবহার করে আর পাশাপাশি খালি ব্লক জোড়া লাগায়।
- পরীক্ষার প্রিয় অঙ্ক। First/best/worst fit-এর placement বের করা বিশ্ববিদ্যালয় আর চাকরির লিখিত পরীক্ষায় খুব পরিচিত numerical.
২ · Fixed partition আর internal fragmentation
সবচেয়ে পুরোনো আর সরল উপায়: সিস্টেম চালুর সময় মেমরিকে কয়েকটি নির্দিষ্ট আকারের partition-এ ভাগ করে রাখা। প্রতিটি partition-এ একবারে ঠিক একটি প্রসেস। একটি partition খালি হলে অপেক্ষার সারি থেকে একটি প্রসেস সেখানে বসে। এর নাম fixed partitioning.
IBM-এর OS/360 (১৯৬০-এর দশকের মাঝামাঝি থেকে) কয়েকটি রূপে আসত। তার একটি ছিল MFT — “Multiprogramming with a Fixed number of Tasks”: মেমরি কয়েকটি নির্দিষ্ট partition-এ ভাগ, যাদের আকার মূলত অপারেটর বা সিস্টেম-কনফিগারেশন ঠিক করত। আরেকটি ছিল MVT — “Multiprogramming with a Variable number of Tasks”, যেখানে প্রতিটি কাজ ঠিক যতটা চায় ততটার একটি region পেত (পরের অংশ)। দুটির খুঁটিনাটি বিভিন্ন সংস্করণে বদলেছে; এখানে শুধু মূল ভাবনাটি নেওয়া হলো। পাঠ্যবইয়ে এই দুই নাম fixed আর variable partition-এর ঐতিহাসিক উদাহরণ হিসেবেই আসে।
একটি উদাহরণ দেখুন। ব্যবহারকারীদের মেমরি ১০০০K, চারটি partition — ১০০K, ২০০K, ৩০০K আর ৪০০K. চারটি কাজ এসেছে, প্রতিটি নিজের মাপের সবচেয়ে কাছের partition-এ বসেছে:
| Partition | কাজের আকার | ভেতরে অব্যবহৃত (internal fragmentation) |
|---|---|---|
| ১০০K | ৯০K | ১০K |
| ২০০K | ১৫০K | ৫০K |
| ৩০০K | ২৬০K | ৪০K |
| ৪০০K | ৩৫০K | ৫০K |
| মোট: ১০০০K-এর মধ্যে কাজে লাগছে ৮৫০K, partition-এর ভেতরে নষ্ট ১৫০K | ||
এই ১৫০K খালি, কিন্তু কেউ ব্যবহার করতে পারবে না — কারণ প্রতিটি টুকরো এমন একটি partition-এর ভেতরে, যা ইতিমধ্যে একজনকে দেওয়া। এটাই internal fragmentation: দেওয়া জায়গা চাওয়ার চেয়ে বড়, আর বাড়তি অংশটুকু ব্লকের ভেতরে আটকে থাকে। আরও দুটি সীমাবদ্ধতা চোখে পড়ে:
- সবচেয়ে বড় partition-এর চেয়ে বড় কাজ কখনো চলতে পারে না। ৪৫০K-এর একটি কাজ এখানে চিরকাল অপেক্ষা করবে, যদিও পুরো মেমরি ১০০০K.
- Degree of multiprogramming আগেই বাঁধা। চারটি partition মানে একসাথে সর্বোচ্চ চারটি কাজ, তারা যত ছোটই হোক।
অপেক্ষার সারি হতে পারে partition-প্রতি আলাদা, অথবা একটি — partition খালি হলে সেখান থেকে মানানসই কাজ বেছে নেওয়া হয়। বিনিময়ে লাভ সরলতা: partition-এর সীমানা বদলায় না, তাই external fragmentation প্রায় নেই।
৩ · Variable partition, hole আর dynamic storage-allocation problem
Internal fragmentation কমানোর স্বাভাবিক চিন্তা: প্রতিটি প্রসেসকে ঠিক যতটা চায় ততটাই দেওয়া। এটাই variable partitioning (MVT-এর ভাবনা)। শুরুতে পুরো user মেমরি একটিমাত্র বড় খালি অংশ। প্রসেস আসে, প্রত্যেকে নিজের মাপের টুকরো কেটে নেয়; প্রসেস শেষ হয়, তার টুকরো খালি হয়। কিছুক্ষণ পর মেমরি দেখতে হয় এরকম: ব্যবহৃত ব্লক আর খালি অংশ পালা করে। প্রতিটি খালি টানা অংশকে বলে hole.
0K 1950K
│ hole 100 │ X │ hole 500 │ X │ hole 200 │ X │ hole 300 │ X │ hole 600 │ X │
X = 50K used by some other process (holes cannot merge across it)
উপরের ছবিটিই এই পর্বের ক্লাসিক অঙ্কের শুরুর অবস্থা: পাঁচটি hole, ঠিকানার ক্রমে ১০০K, ৫০০K, ২০০K, ৩০০K আর ৬০০K, মাঝে মাঝে অন্য প্রসেসের ৫০K করে ব্লক (তাই hole গুলো জোড়া লাগতে পারে না)। OS এই hole-গুলোর একটি তালিকা রাখে — free list. এখন প্রশ্ন:
Dynamic storage-allocation problem: hole-এর একটি তালিকা দেওয়া আছে। n আকারের একটি অনুরোধ এলে কোন hole থেকে জায়গা দেবেন? নির্বাচিত hole-এর আকার n-এর বেশি হলে সেটি দুই ভাগ হয় (splitting) — n আকারের অংশ প্রসেস পায়, বাকিটা ছোট একটি hole হয়ে তালিকায় থাকে। আর একটি ব্লক মুক্ত হলে সেটি পাশের hole-এর সাথে জোড়া লাগে (coalescing).
মুক্ত হলে জোড়া লাগানো — চারটি ঘটনা
একটি ব্লক মুক্ত হলে তার বাঁ আর ডান প্রতিবেশী দেখতে হয়। চারটি সম্ভাবনা, আর প্রতিটিতে hole-এর সংখ্যা কীভাবে বদলায় — অংশ ৫-এর প্রমাণে এটিই লাগবে:
| বাঁয়ে | ডানে | কী হয় | hole-সংখ্যা |
|---|---|---|---|
| ব্যবহৃত | ব্যবহৃত | নতুন একটি hole তৈরি হয় | +১ |
| hole | ব্যবহৃত | বাঁয়ের hole বড় হয় | ০ |
| ব্যবহৃত | hole | ডানের hole বড় হয় (শুরু সরে আসে) | ০ |
| hole | hole | তিনটি মিলে একটি hole | −১ |
এই জোড়া লাগানো দ্রুত করতে free list সাধারণত ঠিকানার ক্রমে সাজানো রাখা হয় (প্রতিবেশী পাশাপাশি থাকে), অথবা প্রতিটি ব্লকের দুই মাথায় তার আকার আর “ব্যবহৃত কি না” লেখা থাকে — boundary tag (অংশ ৮)। একটি সতর্কতা: পাশাপাশি দুটি hole কখনো আলাদা থাকা উচিত নয়; থাকলে বুঝবেন coalescing ঠিকমতো হয়নি — বড় অনুরোধ অকারণে ব্যর্থ হবে।
৪ · চার fit কৌশল — নিয়ম আর ক্লাসিক অঙ্ক
| কৌশল | নিয়ম (n আকারের অনুরোধে) | কত hole দেখতে হয় |
|---|---|---|
| First fit | তালিকার শুরু থেকে খুঁজে প্রথম যে hole-এর আকার ≥ n, সেটি | প্রথম মানানসই পেলেই থামে |
| Next fit | First fit-এর মতোই, কিন্তু খোঁজা শুরু হয় আগের বরাদ্দ যেখানে শেষ হয়েছিল সেখান থেকে; শেষে পৌঁছালে শুরুতে ফিরে আসে (roving pointer) | প্রথম মানানসই পেলেই থামে |
| Best fit | যত hole-এর আকার ≥ n, তাদের মধ্যে সবচেয়ে ছোটটি (সমান হলে নিচের ঠিকানারটি) | সব (তালিকা আকার অনুযায়ী সাজানো না থাকলে) |
| Worst fit | সবচেয়ে বড় hole, যাতে বাকি অংশও বড় থাকে আর পরে কাজে লাগে | সব (সাজানো না থাকলে) |
চারটিতেই কোনো hole যথেষ্ট বড় না হলে অনুরোধটি অপেক্ষা করে (বা ব্যর্থ হয়) — যদিও মোট খালি জায়গা হয়তো অনেক। এবার ক্লাসিক প্রশ্ন:
প্রশ্ন: ঠিকানার ক্রমে পাঁচটি hole — ১০০K, ৫০০K, ২০০K, ৩০০K আর ৬০০K. প্রসেসগুলো এই ক্রমে আসে: P1 = ২১২K, P2 = ৪১৭K, P3 = ১১২K, P4 = ৪২৬K. প্রতিটি কৌশলে কে কোথায় বসবে? কোন কৌশল মেমরি সবচেয়ে ভালো ব্যবহার করে?
একটি সূক্ষ্ম বিষয় আগে মেনে নিন (অনেক বইয়ে স্পষ্ট লেখা থাকে না): এগুলো variable partition — একটি hole থেকে কেটে নেওয়ার পর বাকি অংশ নতুন ছোট hole হয়ে তালিকায় থাকে, আর পরের অনুরোধ সেখানেও বসতে পারে। প্রশ্নে যদি fixed partition বলা থাকে (একটি partition-এ একটিই প্রসেস), উত্তর আলাদা হতে পারে। প্রথমে first fit আর best fit পাশাপাশি দেখুন:
1 holes: 100 500 200 300 6002 P1 asks 212K3 P2 asks 417K4 P3 asks 112K5 P4 asks 426K
| First fit বেছে নিল | |
| Best fit বেছে নিল | |
| মোট খালি (দুটিতেই) |
- শুরু। দুই কৌশলের সামনে একই পাঁচটি hole, মোট ১৭০০K খালি। First fit বাঁ থেকে প্রথম মানানসইটি নেবে; best fit সব দেখে সবচেয়ে আঁটসাঁটটি নেবে।
- P1 = ২১২K. First fit: ১০০K ছোট, ৫০০K যথেষ্ট — সেখানে বসে, বাকি ২৮৮K. Best fit: মানানসই ৫০০, ৩০০ আর ৬০০-এর মধ্যে সবচেয়ে ছোট ৩০০K — বাকি ৮৮K.
- P2 = ৪১৭K. First fit: ১০০, ২৮৮, ২০০, ৩০০ সবই ছোট; ৬০০K-এ বসে, বাকি ১৮৩K. Best fit: মানানসই শুধু ৫০০ আর ৬০০ — ছোটটি ৫০০K, বাকি ৮৩K. লক্ষ করুন, best fit বড় ৬০০K-টি অক্ষত রেখেছে।
- P3 = ১১২K. First fit: প্রথম মানানসই ২৮৮K (P1-এর পরের অবশিষ্ট), বাকি ১৭৬K. Best fit: মানানসই ২০০ আর ৬০০ — ছোটটি ২০০K, বাকি ৮৮K. মোট খালি দুটিতেই ৯৫৯K.
- P4 = ৪২৬K — পার্থক্য এখানে। First fit-এ সবচেয়ে বড় hole ৩০০K, তাই P4 অপেক্ষা করে — যদিও মোট ৯৫৯K খালি। এটাই external fragmentation-এর হাতে-কলমে রূপ। Best fit-এ ৬০০K এখনো আছে: P4 বসে, বাকি ১৭৪K, মোট খালি ৫৩৩K.
চারটি কৌশলই একসাথে চালিয়ে দেখুন। Simulator ঠিকানার ক্রমে hole আর অন্যদের ৫০K ব্লক সাজায়, প্রতিটি অনুরোধে নিয়ম মেনে hole বেছে নেয়, তারপর একটি “free P2” ঘটনা ঘটায় — যাতে দেখা যায় মুক্ত হওয়া জায়গা জোড়া লাগার পর অপেক্ষায় থাকা P4 জায়গা পায় কি না।
// Variable partitions: first, next, best and worst fit on the same request/free sequence.
const HOLES = [100, 500, 200, 300, 600]; // free holes in address order, in K
const SEQ = [["P1", 212], ["P2", 417], ["P3", 112], ["P4", 426], ["free", "P2"]];
function run(rule) {
const mem = []; let at = 0, rover = 0, waiting = [];
for (const h of HOLES) { // a hole, then a 50K block someone else uses
mem.push({ at, size: h, who: null }, { at: at + h, size: 50, who: "X" }); at += h + 50;
}
const place = (p, k) => {
let c = mem.filter(b => !b.who && b.size >= k); // holes big enough, address order
if (rule === "next") c = [...c.filter(b => b.at >= rover), ...c.filter(b => b.at < rover)];
if (rule === "best") c.sort((a, b) => a.size - b.size); // stable sort: ties -> lower address
if (rule === "worst") c.sort((a, b) => b.size - a.size);
if (!c.length) { console.log(` ${p} ${k}K: no hole >= ${k}K -> WAIT`); return false; }
const h = c[0], old = h.size, rest = old - k;
mem.splice(mem.indexOf(h), 1, { at: h.at, size: k, who: p },
...(rest ? [{ at: h.at + k, size: rest, who: null }] : []));
rover = h.at + k; // next fit resumes here
console.log(` ${p} ${k}K -> hole ${old}K at ${h.at}K, ${rest}K left`); return true;
};
console.log(rule.toUpperCase() + " FIT");
for (const [p, k] of SEQ) {
if (p !== "free") { if (!place(p, k)) waiting.push([p, k]); }
else {
mem.find(b => b.who === k).who = null; console.log(` free ${k}`);
for (let i = mem.length - 1; i > 0; i--) // merge neighbouring holes
if (!mem[i].who && !mem[i - 1].who) { mem[i - 1].size += mem[i].size; mem.splice(i, 1); }
waiting = waiting.filter(([q, s]) => !place(q, s)); // retry whoever is waiting
}
console.log(" holes: " + mem.filter(b => !b.who).map(b => b.size).join(" "));
}
const free = mem.filter(b => !b.who).map(b => b.size), total = free.reduce((a, b) => a + b);
console.log(` free ${total}K in ${free.length} holes, largest ${Math.max(...free)}K\n`);
}
["first", "next", "best", "worst"].forEach(run);আউটপুট (নির্ধারিত, প্রতিবার একই):
FIRST FIT
P1 212K -> hole 500K at 150K, 288K left
holes: 100 288 200 300 600
P2 417K -> hole 600K at 1300K, 183K left
holes: 100 288 200 300 183
P3 112K -> hole 288K at 362K, 176K left
holes: 100 176 200 300 183
P4 426K: no hole >= 426K -> WAIT
holes: 100 176 200 300 183
free P2
P4 426K -> hole 600K at 1300K, 174K left
holes: 100 176 200 300 174
free 950K in 5 holes, largest 300K
NEXT FIT
P1 212K -> hole 500K at 150K, 288K left
holes: 100 288 200 300 600
P2 417K -> hole 600K at 1300K, 183K left
holes: 100 288 200 300 183
P3 112K -> hole 183K at 1717K, 71K left
holes: 100 288 200 300 71
P4 426K: no hole >= 426K -> WAIT
holes: 100 288 200 300 71
free P2
P4 426K: no hole >= 426K -> WAIT
holes: 100 288 200 300 417 71
free 1376K in 6 holes, largest 417K
BEST FIT
P1 212K -> hole 300K at 950K, 88K left
holes: 100 500 200 88 600
P2 417K -> hole 500K at 150K, 83K left
holes: 100 83 200 88 600
P3 112K -> hole 200K at 700K, 88K left
holes: 100 83 88 88 600
P4 426K -> hole 600K at 1300K, 174K left
holes: 100 83 88 88 174
free P2
holes: 100 500 88 88 174
free 950K in 5 holes, largest 500K
WORST FIT
P1 212K -> hole 600K at 1300K, 388K left
holes: 100 500 200 300 388
P2 417K -> hole 500K at 150K, 83K left
holes: 100 83 200 300 388
P3 112K -> hole 388K at 1512K, 276K left
holes: 100 83 200 300 276
P4 426K: no hole >= 426K -> WAIT
holes: 100 83 200 300 276
free P2
P4 426K -> hole 500K at 150K, 74K left
holes: 100 74 200 300 276
free 950K in 5 holes, largest 300K
প্রথম চার অনুরোধের ফল পাঠ্যবইয়ের পরিচিত উত্তরের সাথে মেলে: first fit ২১২→৫০০, ৪১৭→৬০০, ১১২→২৮৮, ৪২৬ অপেক্ষা; best fit ২১২→৩০০, ৪১৭→৫০০, ১১২→২০০, ৪২৬→৬০০; worst fit ২১২→৬০০, ৪১৭→৫০০, ১১২→৩৮৮, ৪২৬ অপেক্ষা; আর next fit ২১২→৫০০, ৪১৭→৬০০, তারপর খোঁজা শুরু হয় ১৭১৭K থেকে, তাই ১১২ বসে ১৮৩K-এর অবশিষ্টে (বাকি ৭১K), আর ৪২৬ অপেক্ষা করে। এই অঙ্কে best fit-ই একমাত্র কৌশল, যা চারটিকে একসাথে জায়গা দেয়।
“free P2”-এর পরের লাইনগুলো আরও শিক্ষণীয়। First fit-এ P2-এর ৪১৭K ডানের ১৮৩K-এর সাথে জোড়া লেগে ৬০০K হয়, P4 সেখানে বসে। Worst fit-এ P2 ছিল ১৫০K ঠিকানায়, তার ৪১৭K ডানের ৮৩K-এর সাথে মিলে ৫০০K — P4 বসে। কিন্তু next fit-এ P2-এর ডানে P3 বসে ছিল, তাই ৪১৭K একা থাকে — ৪২৬-এর চেয়ে ৯K ছোট, P4 এখনো অপেক্ষায়, যদিও মোট খালি ১৩৭৬K. sort stable বলে best fit-এ সমান আকারের hole থাকলে নিচের ঠিকানারটি আগে আসে।
তুলনা — গতি আর fragmentation
| কৌশল | গতি | কী ধরনের hole রেখে যায় | সাধারণ মূল্যায়ন |
|---|---|---|---|
| First fit | দ্রুত; প্রথম মানানসই পেলেই থামে, পুরো তালিকা দেখতে হয় না | তালিকার শুরুর দিকে ছোট ছোট টুকরো জমে | simulation আর বাস্তব trace-এ ভালো; সরল ও জনপ্রিয় |
| Next fit | first fit-এর মতো, শুরুর ছোট টুকরোগুলো বারবার দেখতে হয় না | বরাদ্দ পুরো মেমরিতে ছড়িয়ে পড়ে, বড় hole টেকে না | অনেক গবেষণায় first fit-এর চেয়ে বেশি fragmentation |
| Best fit | সব দেখতে হয় — আকার-ভিত্তিক tree বা সাজানো তালিকা থাকলে দ্রুত | খুব ছোট টুকরো (এখানে ৮৩K, ৮৮K) — প্রায়ই কারও কাজে আসে না | মেমরি ব্যবহারে সাধারণত ভালো, first fit-এর কাছাকাছি |
| Worst fit | সব দেখতে হয় (বা max-heap) | বড় hole তাড়াতাড়ি ভেঙে যায় | simulation-এ সাধারণত সবচেয়ে খারাপ |
পাঠ্যবইয়ের simulation-ভিত্তিক সিদ্ধান্ত: first fit আর best fit সময় আর মেমরি দুই দিকেই worst fit-এর চেয়ে ভালো; মেমরি ব্যবহারে first আর best প্রায় সমান, কিন্তু first fit সাধারণত দ্রুত। একটি অঙ্কের ফল দিয়ে এই সিদ্ধান্ত টানবেন না — উপরের অঙ্কে best fit জিতেছে, কিন্তু অনুশীলনের অঙ্কে দেখবেন অন্য চিত্র। বাস্তব workload-এ কোনটি ভালো, তা নির্ভর করে অনুরোধের আকার আর আয়ুর ধরনের ওপর।
৫ · Internal বনাম external fragmentation, আর ৫০-শতাংশ নিয়ম
Internal fragmentation: একটি প্রসেসকে দেওয়া ব্লকের ভেতরের যে অংশ সে ব্যবহার করে না — কারণ বরাদ্দের একক (partition, block, page) চাওয়ার চেয়ে বড়। জায়গাটি “দেওয়া হয়ে গেছে”, তাই অন্য কেউ পায় না। External fragmentation: বরাদ্দের বাইরে মোট খালি জায়গা একটি অনুরোধ মেটানোর মতো যথেষ্ট, কিন্তু সেটি এমন অনেক টুকরোয় ছড়ানো যে কোনো একটি টুকরো যথেষ্ট বড় নয়।
Internal fragmentation কোথায়
Fixed partition (অংশ ২-এর ১৫০K); buddy system (১০০K চাইলে ১২৮K); paging-এ প্রতিটি প্রসেসের শেষ page-এর অব্যবহৃত অংশ (পর্ব ৩২); malloc-এ alignment আর header (অংশ ৮)।
কারণ: নির্দিষ্ট মাপের এককExternal fragmentation কোথায়
Variable partition (first fit-এ ৯৫৯K খালি, তবু ৪২৬K জায়গা নেই); segmentation (পর্ব ৩৪); ডিস্কে contiguous file allocation (পর্ব ৪৩); দীর্ঘদিন চলা প্রোগ্রামের heap.
কারণ: যেকোনো মাপের টুকরো কাটাExternal fragmentation কতটা খারাপ, তা মাপার একটি প্রচলিত উপায়: 1 − (সবচেয়ে বড় hole ÷ মোট খালি). শূন্য মানে সব খালি একটি টুকরোয়; ১-এর কাছে মানে খালি জায়গা গুঁড়ো হয়ে গেছে। Simulator-এর শেষ অবস্থায় first fit-এ ১ − ৩০০/৯৫০ ≈ ০.৬৮, আর best fit-এ ১ − ৫০০/৯৫০ ≈ ০.৪৭।
৫০-শতাংশ নিয়ম (fifty-percent rule)
Donald Knuth তাঁর The Art of Computer Programming-এর প্রথম খণ্ডে (dynamic storage allocation অংশে) একটি চমৎকার ফল দেখান। পাঠ্যবই এটিকে এভাবে বলে: first fit-এ, কিছু উন্নতির পরেও, N টি বরাদ্দ ব্লক থাকলে আরও প্রায় ০.৫N টি ব্লক fragmentation-এ হারিয়ে যায় — অর্থাৎ মেমরির প্রায় এক-তৃতীয়াংশ অব্যবহারযোগ্য হতে পারে। কথাটি সাবধানে পড়ুন, কারণ এতে দুটি আলাদা দাবি আছে:
- Knuth-এর আসল বক্তব্য hole-এর সংখ্যা নিয়ে: সিস্টেম যদি একটি স্থিত অবস্থায় (equilibrium) পৌঁছায়, যেখানে গড়ে N টি ব্লক বরাদ্দ থাকে, প্রতিটি ব্লকের পরের মুক্ত হওয়ার সম্ভাবনা সমান, আর একটি বরাদ্দ ঠিক মাপের hole পায় না (hole ভাগ হয়) সম্ভাবনা p নিয়ে — তাহলে hole-এর গড় সংখ্যা ≈ pN/2. Exact fit বিরল হলে p ≈ ১, তাই hole ≈ N/2.
- “এক-তৃতীয়াংশ” একটি বাড়তি অনুমান থেকে আসে: যদি hole-এর গড় আকার ব্লকের গড় আকারের সমান ধরা হয়, তাহলে মোট জায়গার N + N/2 ভাগের N/2 ভাগ hole — অর্থাৎ ১/৩. বাস্তবে hole-গুলো সাধারণত ব্লকের চেয়ে ছোট বা বড় হতে পারে, তাই “এক-তৃতীয়াংশ” একটি মোটা দাগের অনুমান, নিয়ম নয়।
প্রমাণের ছক: পাশাপাশি দুটি hole থাকে না (coalescing), তাই প্রতিটি hole-এর দুই পাশে বরাদ্দ ব্লক (দুই প্রান্তের প্রভাব উপেক্ষা করছি)। বরাদ্দ ব্লকগুলোকে তিন দলে ভাগ করুন: A দলের দুই পাশেই hole, B দলের এক পাশে, C দলের কোনো পাশে নয়। তাহলে N = A + B + C, আর প্রতিটি hole দুটি “ব্লক-পাশ” ছোঁয় বলে 2M = 2A + B (M = hole-সংখ্যা)। এখন অংশ ৩-এর টেবিল: একটি A-ব্লক মুক্ত হলে M এক কমে, B-তে বদলায় না, C-তে এক বাড়ে; সব ব্লক সমান সম্ভাবনায় মুক্ত হলে প্রতি মুক্তিতে M-এর প্রত্যাশিত বদল (C − A)/N. প্রতিটি বরাদ্দে M বদলায় না সম্ভাবনা p নিয়ে (hole ছোট হয়), আর এক কমে সম্ভাবনা ১ − p নিয়ে (exact fit-এ hole মুছে যায়)। স্থিত অবস্থায় মুক্তি আর বরাদ্দের হার সমান, M-এর গড় বদল শূন্য: (C − A)/N = ১ − p, অর্থাৎ C − A = N(১ − p). তাহলে 2M = 2A + (N − A − C) = N − (C − A) = N − N(১ − p) = pN, অর্থাৎ M = pN/2. ∎ লক্ষ করুন, যুক্তিটি কোনো নির্দিষ্ট fit নিয়মের ওপর সরাসরি নির্ভর করে না — সমান সম্ভাবনায় মুক্ত হওয়ার অনুমানটিই আসল ভার বহন করে।
শিক্ষাটা গুরুত্বপূর্ণ: variable partition-এ fragmentation কোনো দুর্ঘটনা নয়, স্থিত অবস্থার স্বাভাবিক ফল। ভালো fit নিয়ম hole-এর আকার ভালো রাখতে পারে, কিন্তু hole থাকবেই। দুটি পথ খোলা থাকে — জায়গা সরিয়ে hole এক করা (compaction), অথবা “এক টুকরো টানা” শর্তটিই তুলে দেওয়া (paging, পর্ব ৩২)।
৬ · Compaction — জায়গা সরিয়ে hole এক করা
Compaction মানে ব্যবহৃত ব্লকগুলোকে সরিয়ে এক পাশে জড়ো করা, যাতে সব hole মিলে একটি বড় hole হয়। কমিউনিটি সেন্টারের উদাহরণে: বসে থাকা অতিথিদের অনুরোধ করে এক দিকে সরিয়ে দেওয়া, যাতে আটজনের পরিবার একসাথে বসতে পারে। কিন্তু তার আগে দুটি প্রশ্ন।
- সরানো কি আদৌ সম্ভব? একটি প্রসেসকে নতুন ঠিকানায় সরালে তার ভেতরের সব ঠিকানাও বদলাতে হয়। ঠিকানা যদি compile বা load-এর সময় বেঁধে ফেলা হয় (পর্ব ৩০), তাহলে প্রোগ্রামের ভেতরে physical ঠিকানা লেখা আছে — সরালেই সব ভাঙবে। Compaction তখনই সম্ভব, যখন execution-time binding: প্রোগ্রাম শুধু logical ঠিকানা জানে, আর MMU-র relocation register যোগ হয় প্রতিটি মেমরি-ব্যবহারে। তাহলে ব্লক কপি করে শুধু register-এর মান বদলালেই চলে।
- কোনো I/O চলছে কি? কোনো device যদি DMA দিয়ে প্রসেসের একটি buffer-এ physical ঠিকানায় লিখছে, তখন সেই ব্লক সরানো যাবে না — হয় I/O শেষ হওয়া পর্যন্ত ব্লকটি আটকে রাখা (pin), নয়তো I/O শুধু OS-এর নিজের buffer-এ করা।
নিচের stepper-এ first fit-এর শেষ অবস্থা থেকে (P4 = ৪২৬K অপেক্ষায়, মোট খালি ৯৫৯K) compaction দেখুন। ঠিকানা K-তে, X1…X5 অন্য প্রসেসের ৫০K করে ব্লক।
1 state after first fit, P4 426K waits2 move X1 100->0, P1 150->503 move P3 362->262, X2 650->3744 move X3 ->424, X4 ->474, P2 ->5245 one hole of 959K: place P4 at 9416 cheaper: from step 1, move only X2
| মোট কপি করা | |
| Hole (আকার@ঠিকানা) | |
| P4 (৪২৬K) |
- শুরু। পাঁচটি hole — ১০০, ১৭৬, ২০০, ৩০০ আর ১৮৩K, মোট ৯৫৯K. সবচেয়ে বড়টি ৩০০K, তাই ৪২৬K-এর P4 অপেক্ষায়।
- নিচ থেকে সরানো শুরু। X1 (৫০K) ঠিকানা ১০০ থেকে ০-তে, P1 (২১২K) ১৫০ থেকে ৫০-এ। মোট কপি ২৬২K. ১০০K-এর hole এখন ২৬২K ঠিকানায়, কিন্তু hole-এর সংখ্যা এখনো পাঁচ।
- P3 আর X2. P3 (১১২K) ২৬২-তে, X2 (৫০K) ৩৭৪-এ। মোট কপি ৪২৪K. এবার ১০০ + ১৭৬ + ২০০ মিলে ৪২৪K ঠিকানায় একটি ৪৭৬K hole — P4 এখনই বসতে পারত। বুদ্ধিমান compaction এখানেই থামতে পারে।
- পুরো compaction. X3 ৪২৪-এ, X4 ৪৭৪-এ, P2 (৪১৭K) ৫২৪-এ। মোট কপি ৯৪১K, আর ৯৪১K থেকে ১৯০০K পর্যন্ত একটিমাত্র ৯৫৯K hole. X5 আগে থেকেই একদম শেষে, তাকে সরানোর দরকার নেই।
- P4 বসে। ৯৪১K ঠিকানায় ৪২৬K, বাকি ৫৩৩K hole. খরচ: প্রায় এক মেগাবাইট কপি, আর প্রতিটি সরানো প্রসেসের relocation register বদলানো — সরানোর সময় তারা থেমে থাকে।
- সস্তা বিকল্প। ধাপ ১-এ ফিরে যান। শুধু X2 (৫০K)-কে ৬৫০ থেকে ৯৫০K-এ (৩০০K hole-এর শুরুতে) সরালে ১৭৬ + ৫০ + ২০০ = ৪২৬K-এর একটি hole হয় ঠিক ৪৭৪K-এ — P4 পুরোপুরি এঁটে যায়। কপি মাত্র ৫০K. কোন ব্লক সরালে সবচেয়ে কম কপিতে কাজ হয়, তা খোঁজা সাধারণভাবে কঠিন একটি অপ্টিমাইজেশন সমস্যা।
দাম কত? Compaction মূলত মেমরি কপি। ধরুন একটি মেশিন সেকেন্ডে প্রায় ১০ GB কপি করতে পারে (একটি কল্পিত সংখ্যা, মেশিনভেদে আলাদা); তাহলে ৮ GB সরাতে প্রায় ০.৮ সেকেন্ড, আর সেই সময় সরানো প্রসেসগুলো থেমে থাকে। তাই compaction সাধারণত মাঝে মাঝে, দরকার হলে করা হয় — প্রতিটি free-তে নয়।
আজ compaction কোথায়? সাধারণ প্রসেসের জন্য নয় — paging থাকায় দরকার পড়ে না। কিন্তু Linux kernel physical মেমরিতে compaction করে, যাতে বড় টানা জায়গা (যেমন huge page, পর্ব ৩৮) পাওয়া যায়; এটি সম্ভব কারণ page সরিয়ে শুধু page table বদলালেই প্রসেস টের পায় না। Java বা .NET-এর মতো runtime-এর garbage collector-ও heap compact করে, কারণ তারা প্রতিটি reference খুঁজে বদলাতে পারে। C-এর malloc পারে না — প্রোগ্রামের হাতে কাঁচা pointer, কোনটি কোথায় লেখা আছে allocator জানে না।
৭ · Buddy system — ২-এর ঘাতে ভাগ আর জোড়া
Variable partition-এ coalescing-এর জন্য প্রতিবেশী খুঁজতে হয়, আর hole যেকোনো মাপের হতে পারে। Buddy system দুটিকেই সরল করে: সব ব্লকের আকার ২-এর ঘাত (power of two), আর প্রতিটি ব্লকের জোড়া লাগার সঙ্গী — তার buddy — ঠিকানা থেকেই হিসাব করে বের করা যায়।
Binary buddy system-এর প্রথম বর্ণনা সাধারণত Bell Labs-এর Kenneth Knowlton-কে দেওয়া হয় (১৯৬৫ সালের “A Fast Storage Allocator” প্রবন্ধ); কিছু সূত্র Harry Markowitz-এর আগের কাজের কথাও বলে। Knuth তাঁর বইয়ে পদ্ধতিটির বিশ্লেষণ করেন। আজ Linux kernel-এর physical page allocator এই ধারণার ওপরই দাঁড়িয়ে (পর্ব ৩৮)।
নিয়ম
- বরাদ্দ: n চাইলে আকার বাড়িয়ে নিকটতম ২-এর ঘাত s ≥ n ধরুন (১০০K → ১২৮K). s মাপের free list-এ ব্লক থাকলে সেটি দিন। না থাকলে তার চেয়ে বড় সবচেয়ে ছোট খালি ব্লক নিন, আর বারবার অর্ধেক করুন — প্রতিবার এক অর্ধেক রাখুন, অন্য অর্ধেক (সেটিই buddy) উপযুক্ত free list-এ দিন — যতক্ষণ না আকার s হয়।
- মুক্তি: ব্লকের buddy খালি আর একই মাপের হলে দুটি জোড়া লাগিয়ে দ্বিগুণ মাপের ব্লক করুন; নতুন ব্লকের buddy-ও খালি হলে আবার জোড়া — যতক্ষণ সম্ভব। এরপর ফলের ব্লকটি free list-এ দিন।
- প্রতিটি মাপের আলাদা free list: ৬৪K, ১২৮K, ২৫৬K… প্রতিটির নিজস্ব তালিকা — যেটি একধরনের segregated free list.
buddy(a, s) = a XOR s — যেখানে a হলো pool-এর শুরু থেকে ব্লকের ঠিকানা (offset), আর s ব্লকের আকার। কেন কাজ করে: s মাপের প্রতিটি ব্লক s-এর গুণিতক ঠিকানায় শুরু হয় (ভাগ থেকেই এটি নিশ্চিত হয়)। একটি 2s ব্লক ভাগ করলে দুই অর্ধেকের ঠিকানা শুধু s-এর bit-টিতে আলাদা — একটিতে ০, অন্যটিতে ১. সেই একটি bit উল্টালেই সঙ্গীকে পাওয়া যায়, আর XOR ঠিক তা-ই করে। যেমন ১৯২K-এ ৬৪K ব্লক: ১৯২ XOR ৬৪ = ১২৮; ২৫৬K-এ ২৫৬K ব্লক: ২৫৬ XOR ২৫৬ = ০. জোড়া লাগানো ব্লকের ঠিকানা হয় দুটির ছোটটি, অর্থাৎ a AND NOT s.
এবার পাঠ্যবইয়ের পরিচিত উদাহরণ: ১ MB (১০২৪K) pool; অনুরোধ A = ১০০K, B = ২৪০K, C = ৬৪K, D = ২৫৬K; তারপর একে একে free B, free A, free C, free D.
1 pool: 1024K free at 02 A = 100K -> 128K3 B = 240K -> 256K4 C = 64K -> 64K5 D = 256K -> 256K6 free B7 free A8 free C9 free D
| ব্লক-মানচিত্র | |
| Free list (আকার:[ঠিকানা]) | |
| Internal waste |
- শুরু। পুরো ১০২৪K একটি খালি ব্লক, ঠিকানা ০. কোনো অপচয় নেই।
- A = ১০০K → ১২৮K. ১২৮K-এর ব্লক নেই, তাই ১০২৪ ভেঙে ৫১২ + ৫১২, নিচের ৫১২ ভেঙে ২৫৬ + ২৫৬, নিচের ২৫৬ ভেঙে ১২৮ + ১২৮. A পায় ০-তে ১২৮K; ১২৮, ২৫৬ আর ৫১২ ঠিকানার ওপরের অর্ধেকগুলো free list-এ। A-র ভেতরে নষ্ট ২৮K.
- B = ২৪০K → ২৫৬K. ২৫৬K-এর তালিকায় ২৫৬ ঠিকানার ব্লকটি তৈরি আছে — কোনো ভাগ লাগে না। নষ্ট আরও ১৬K, মোট ৪৪K.
- C = ৬৪K → ৬৪K. ৬৪K নেই; সবচেয়ে ছোট বড় ব্লক ১২৮@১২৮, সেটি ভেঙে ৬৪@১২৮ (C) আর ৬৪@১৯২ (খালি)। ৬৪ ঠিক ২-এর ঘাত, তাই কোনো নতুন অপচয় নেই।
- D = ২৫৬K. ২৫৬K-এর তালিকা খালি, তাই ৫১২@৫১২ ভেঙে ২৫৬@৫১২ (D) আর ২৫৬@৭৬৮ (খালি)। এখন ৭০৪K দেওয়া, চাওয়া ছিল ৬৬০K — নষ্ট ৪৪K.
- free B. B-র buddy = ২৫৬ XOR ২৫৬ = ০. কিন্তু ০ ঠিকানায় এখন ২৫৬K-এর কোনো খালি ব্লক নেই — সেটি ভেঙে A আর C বসে আছে। জোড়া হয় না; ২৫৬@২৫৬ শুধু free list-এ যায়।
- free A. A-র buddy = ০ XOR ১২৮ = ১২৮. সেখানে ১২৮K-এর খালি ব্লক নেই — সেটি C আর ৬৪@১৯২-তে ভাঙা। এবারও জোড়া নেই। খেয়াল করুন: মোট খালি ৬৪ + ১২৮ + ২৫৬ + ২৫৬ = ৭০৪K, অথচ সবচেয়ে বড় খালি ব্লক ২৫৬K — buddy system-এও external fragmentation আছে।
- free C — ধারাবাহিক জোড়া। C-র buddy ১৯২ খালি → ১২৮@১২৮; তার buddy ০ খালি (A মুক্ত) → ২৫৬@০; তার buddy ২৫৬ খালি (B মুক্ত) → ৫১২@০; তার buddy ৫১২ — সেখানে D বসে আছে, থামুন। একটি মুক্তিতে তিনবার জোড়া।
- free D. D-র buddy ৭৬৮ খালি → ৫১২@৫১২; তার buddy ০ খালি → ১০২৪@০. Pool আবার একটি অখণ্ড ব্লক — শুরুর অবস্থা।
// Binary buddy system on 1 MB (1024K). Sizes in K; addresses are offsets from the pool start.
const TOTAL = 1024, free = { [TOTAL]: [0] }, used = {}; // free[size] = list of addresses
const take = (s, a) => free[s].splice(free[s].indexOf(a), 1);
function alloc(name, k) {
let need = 1; while (need < k) need *= 2; // round up to a power of two
let s = need; while (s <= TOTAL && !(free[s] || []).length) s *= 2;
if (s > TOTAL) return console.log(`${name} ${k}K: no block -> WAIT`);
const a = Math.min(...free[s]); take(s, a);
while (s > need) { s /= 2; (free[s] = free[s] || []).push(a + s); } // split, keep low half
used[a] = { name, size: need, k };
show(`${name} ${k}K -> ${need}K block at ${a}K (waste ${need - k}K)`);
}
function release(name) {
let a = +Object.keys(used).find(x => used[x].name === name), s = used[a].size;
delete used[a];
let log = `free ${name}: ${s}K at ${a}K`;
while (s < TOTAL && (free[s] || []).includes(a ^ s)) { // buddy = address XOR size
take(s, a ^ s); log += `, merge with buddy ${a ^ s}K`;
a = Math.min(a, a ^ s); s *= 2;
}
(free[s] = free[s] || []).push(a);
show(log + ` -> ${s}K free at ${a}K`);
}
function show(msg) {
const blocks = [];
for (const s in free) for (const a of free[s]) blocks.push([a, `${s}`]);
for (const a in used) blocks.push([+a, `${used[a].name}${used[a].size}`]);
const waste = Object.values(used).reduce((t, u) => t + u.size - u.k, 0);
console.log(msg + "\n map: " + blocks.sort((x, y) => x[0] - y[0]).map(b => b[1]).join(" | ")
+ ` (internal waste ${waste}K)\n free lists: ` + Object.keys(free).map(Number)
.sort((x, y) => x - y).filter(s => free[s].length).map(s => `${s}K:[${free[s]}]`).join(" "));
}
alloc("A", 100); alloc("B", 240); alloc("C", 64); alloc("D", 256);
release("B"); release("A"); release("C"); release("D");আউটপুট (নির্ধারিত):
A 100K -> 128K block at 0K (waste 28K)
map: A128 | 128 | 256 | 512 (internal waste 28K)
free lists: 128K:[128] 256K:[256] 512K:[512]
B 240K -> 256K block at 256K (waste 16K)
map: A128 | 128 | B256 | 512 (internal waste 44K)
free lists: 128K:[128] 512K:[512]
C 64K -> 64K block at 128K (waste 0K)
map: A128 | C64 | 64 | B256 | 512 (internal waste 44K)
free lists: 64K:[192] 512K:[512]
D 256K -> 256K block at 512K (waste 0K)
map: A128 | C64 | 64 | B256 | D256 | 256 (internal waste 44K)
free lists: 64K:[192] 256K:[768]
free B: 256K at 256K -> 256K free at 256K
map: A128 | C64 | 64 | 256 | D256 | 256 (internal waste 28K)
free lists: 64K:[192] 256K:[768,256]
free A: 128K at 0K -> 128K free at 0K
map: 128 | C64 | 64 | 256 | D256 | 256 (internal waste 0K)
free lists: 64K:[192] 128K:[0] 256K:[768,256]
free C: 64K at 128K, merge with buddy 192K, merge with buddy 0K, merge with buddy 256K -> 512K free at 0K
map: 512 | D256 | 256 (internal waste 0K)
free lists: 256K:[768] 512K:[0]
free D: 256K at 512K, merge with buddy 768K, merge with buddy 0K -> 1024K free at 0K
map: 1024 (internal waste 0K)
free lists: 1024K:[0]
প্রতিটি ঘটনার পর তিনটি জিনিস ছাপা হয়: কী ঘটল, ব্লক-মানচিত্র (অক্ষরসহ ব্লক বরাদ্দ, শুধু সংখ্যা মানে খালি), আর প্রতিটি মাপের free list. সংখ্যাগুলো হুবহু stepper-এর। a ^ s লাইনটিই buddy-র XOR সূত্র, আর Math.min(a, a ^ s) জোড়া লাগানো ব্লকের ঠিকানা। একই অনুরোধে internal waste ৪৪K, অর্থাৎ দেওয়া ৭০৪K-এর ৬.২৫%. কিন্তু এটি ভাগ্যের ব্যাপার: ১২৯K চাইলে ২৫৬K দিতে হবে — প্রায় অর্ধেক নষ্ট। ২-এর ঘাতের ঠিক ওপরের অনুরোধে buddy system-এর internal fragmentation সবচেয়ে খারাপ, প্রায় ৫০%.
লাভ-ক্ষতির হিসাব
- দ্রুত coalescing: প্রতিবেশী খুঁজতে কোনো তালিকা ঘাঁটতে হয় না — একটি XOR আর “ওই ঠিকানার ব্লক কি খালি?” প্রশ্ন। মোট ২k মাপের pool-এ বরাদ্দ বা মুক্তি সর্বোচ্চ k ধাপের ভাগ বা জোড়া।
- Internal fragmentation: আকার ২-এর ঘাতে গোল করতে হয়, তাই অপচয় ০ থেকে প্রায় ৫০% পর্যন্ত; গড় কত, তা অনুরোধের আকারের বণ্টনের ওপর নির্ভর করে।
- External fragmentation পুরো যায় না: শুধু buddy-র সাথেই জোড়া হয়। পাশাপাশি দুটি খালি ব্লক buddy না হলে (যেমন ৬৪@১৯২ আর ২৫৬@২৫৬) কখনো মিশবে না।
Linux kernel physical page-গুলো এভাবেই রাখে: “order” k-এর তালিকায় ২k টি পাশাপাশি page-এর ব্লক। /proc/buddyinfo ফাইলে প্রতিটি order-এ এখন কতগুলো খালি ব্লক আছে, দেখা যায় — নিচের কমান্ড চালালে আপনার সামনের সিস্টেমের (এখানে একটি container-এর ভেতর থেকে দেখা host kernel-এর) সংখ্যা আসবে।
# Linux keeps one free list per "order": order k holds blocks of 2^k pages.
echo "page size: $(getconf PAGESIZE) bytes"
echo "columns after the zone name = free blocks of order 0 1 2 ... 10"
cat /proc/buddyinfo 2>/dev/null || echo "/proc/buddyinfo is not readable here"একটি চালানোর আউটপুট (সংখ্যাগুলো প্রতিবার আর প্রতিটি মেশিনে আলাদা হবে):
page size: 4096 bytes
columns after the zone name = free blocks of order 0 1 2 ... 10
Node 0, zone DMA 56 54 39 33 27 9 1 3 1 0 1
Node 0, zone DMA32 1335 3968 2656 1852 799 161 29 0 0 0 0
প্রতিটি সারি একটি memory zone, আর নামের পরের ১১টি সংখ্যা order ০ থেকে ১০-এর free list-এ কতগুলো ব্লক আছে: order k মানে ২k টি পাশাপাশি ৪ KiB page, অর্থাৎ order ০ = ৪ KiB, order ১০ = ৪ MiB. এই চালানোয় DMA32 zone-এ মোট ৫৪৫০৩টি page (প্রায় ২১৩ MiB) খালি, কিন্তু order ৭ থেকে ১০-এর ঘর শূন্য — সবচেয়ে বড় টানা খালি ব্লক order ৬, মাত্র ২৫৬ KiB. এটাই physical মেমরির external fragmentation, ঠিক এই পর্বের ভাষায়। এই মেশিনে কোনো “Normal” zone-এর সারি দেখা যায়নি; বেশি RAM-এর সাধারণ মেশিনে সাধারণত Normal সারিও থাকে। Zone আর এই সংখ্যার ব্যবহার পর্ব ৩৮-এ।
Slab আর segregated free list — ছোট object-এর জন্য
Kernel-কে প্রতি সেকেন্ডে হাজার হাজার ছোট object লাগে — একটি process descriptor, একটি inode, একটি network buffer-এর header — প্রত্যেকটি কয়েকশো বাইট। এগুলোর জন্য buddy থেকে গোটা একটি page (৪ KiB) নিলে বিশাল internal fragmentation. সমাধান হলো slab allocator-এর ভাবনা (Jeff Bonwick, Sun-এর Solaris-এর জন্য, ১৯৯৪ সালের প্রবন্ধ): buddy থেকে কয়েকটি page নিয়ে সেগুলোকে একই মাপের অনেক ঘরে ভাগ করা, আর প্রতিটি object-ধরনের জন্য আলাদা cache রাখা। একই মাপের ঘর বলে ভেতরে fragmentation প্রায় নেই, আর মুক্ত ঘর আবার একই ধরনের object-এর জন্য তৈরি থাকে। মূল নীতি — আকার অনুযায়ী আলাদা তালিকা (segregated free list) — user-space malloc-এরও ভিত্তি। Linux-এ slab-এর বর্তমান রূপ (SLUB), /proc/slabinfo আর বাস্তব খুঁটিনাটি পর্ব ৩৮-এ।
৮ · প্রোগ্রামের ভেতরে — malloc-ও একই সমস্যা সমাধান করে
OS প্রসেসকে মেমরি দেয় বড় টুকরোয় — brk দিয়ে heap বাড়িয়ে, বা mmap দিয়ে। কিন্তু প্রোগ্রাম চায় ২৪ বাইট, ১০০ বাইট, ৪ KB — লাখ লাখ বার। সেই বড় টুকরোর ভেতরে ছোট ছোট অংশ কেটে দেওয়া আর ফেরত নেওয়ার কাজ করে C library-র memory allocator — malloc আর free. এটি ঠিক এই পর্বের dynamic storage-allocation problem, শুধু একটি প্রসেসের virtual মেমরির ভেতরে। মূল কৌশলগুলো:
- Free list: খালি অংশগুলোর linked list — প্রতিটি খালি অংশের ভেতরেই পরের খালি অংশের pointer রাখা হয়, তাই আলাদা মেমরি লাগে না (DSA-র linked list পর্ব)।
- Boundary tag: প্রতিটি অংশের শুরুতে (header) আর দরকারে শেষে (footer) তার আকার আর “ব্যবহৃত কি না” লেখা — Knuth-এর বর্ণিত কৌশল। মুক্ত করার সময় header দেখে ডানের প্রতিবেশী, footer দেখে বাঁয়ের প্রতিবেশী O(1)-এ পাওয়া যায়, coalescing-এর জন্য তালিকা ঘাঁটতে হয় না।
- Size class আর bin: অনুরোধের আকার কয়েকটি নির্দিষ্ট মাপে গোল করা (যেমন ১৬, ৩২, ৪৮… বাইট), আর প্রতিটি মাপের আলাদা তালিকা — slab-এর মতোই segregated free list. ছোট অনুরোধ প্রায় O(1), বিনিময়ে কিছু internal fragmentation.
- বড় অনুরোধ সরাসরি OS থেকে: খুব বড় অংশ (glibc-তে ডিফল্ট সীমা প্রায় ১২৮ KiB, যা চলার সময় বদলাতে পারে) আলাদা
mmapদিয়ে আনা হয়, free করলে সরাসরি OS-কে ফেরত — heap-এর fragmentation থেকে আলাদা থাকে। - থ্রেডপ্রতি বা CPU-প্রতি cache আর arena: অনেক থ্রেড একই free list-এ lock নিয়ে লড়লে ধীর হয়, তাই আধুনিক allocator থ্রেডপ্রতি ছোট cache আর একাধিক আলাদা heap (arena) রাখে।
| Allocator | কোথায় দেখবেন | মূল ভাবনা (সংক্ষেপে) |
|---|---|---|
| ptmalloc (glibc malloc) | বেশিরভাগ Linux distribution-এর ডিফল্ট | Doug Lea-র dlmalloc থেকে উদ্ভূত; boundary tag-সহ chunk, আকার অনুযায়ী bin, থ্রেডের জন্য একাধিক arena, আর নতুন সংস্করণে থ্রেডপ্রতি ছোট cache (tcache) |
| jemalloc | FreeBSD-এর libc, অনেক বড় সার্ভার-সফটওয়্যার (যেমন Redis Linux-এ ডিফল্টভাবে) | Jason Evans-এর নকশা; size class, একাধিক arena, fragmentation কম রাখা আর বিস্তারিত পরিসংখ্যানের ওপর জোর |
| tcmalloc | Google-এর সার্ভার; আলাদা লাইব্রেরি হিসেবে লিংক করা যায় | “thread-caching malloc” — থ্রেড বা CPU-প্রতি cache থেকে ছোট অনুরোধ lock ছাড়া মেটানো, পেছনে size class-ভিত্তিক কেন্দ্রীয় তালিকা |
| mimalloc | Microsoft Research-এর (২০১৯-এর দিকে প্রকাশিত) open-source allocator | প্রতিটি ছোট page-এর নিজস্ব free list (“free list sharding”), সরল আর দ্রুত পথ |
Windows-এর নিজস্ব heap (HeapAlloc, Low-Fragmentation Heap, নতুন সংস্করণে segment heap) আর macOS-এর libmalloc আলাদা নকশা, কিন্তু একই হাতিয়ার — size class, cache আর coalescing — নানা মিশ্রণে ব্যবহার করে। নিজে একটি ছোট malloc বানাতে চাইলে C কোর্সের “নিজের malloc বানানো” পর্ব দেখুন।
নিচের C প্রোগ্রাম glibc-র malloc থেকে বিভিন্ন মাপের অংশ চেয়ে দেখে: আসলে কত বাইট ব্যবহার করা যায় (malloc_usable_size, একটি glibc extension — standard C নয়), ঠিকানা ১৬-এর গুণিতক কি না, আর পাশাপাশি দুটি malloc(100)-এর মাঝে কত বাইটের দূরত্ব।
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <malloc.h> /* malloc_usable_size: a glibc extension, not standard C */
int main(void) {
size_t sizes[] = {1, 24, 25, 40, 100, 1000, 4000, 200000};
int n = sizeof sizes / sizeof sizes[0];
void *p[8];
printf("%8s %8s %6s %8s\n", "asked", "usable", "extra", "addr%16");
for (int i = 0; i < n; i++) {
p[i] = malloc(sizes[i]);
if (!p[i]) return 1;
size_t u = malloc_usable_size(p[i]);
printf("%8zu %8zu %6zu %8u\n", sizes[i], u, u - sizes[i],
(unsigned)((uintptr_t)p[i] % 16));
}
char *a = malloc(100), *b = malloc(100); /* two neighbours on the heap */
printf("gap between two malloc(100): %ld bytes\n",
(long)((uintptr_t)b - (uintptr_t)a));
for (int i = 0; i < n; i++) free(p[i]);
free(a); free(b);
return 0;
}আউটপুট (Wandbox, Linux x86-64, glibc):
asked usable extra addr%16
1 24 23 0
24 24 0 0
25 40 15 0
40 40 0 0
100 104 4 0
1000 1000 0 0
4000 4008 8 0
200000 200688 688 0
gap between two malloc(100): 112 bytes
extra কলামটিই একটি বাস্তব allocator-এর internal fragmentation. glibc-র x86-64 সংস্করণে প্রতিটি অংশের (chunk) সামনে ৮ বাইটের একটি header থাকে, chunk-এর আকার ১৬-এর গুণিতকে গোল হয়, আর সবচেয়ে ছোট chunk ৩২ বাইট। তাই ১ বাইট চাইলেও ২৪ বাইট ব্যবহারযোগ্য পান (মোট খরচ ৩২ বাইট), আর ২৫ চাইলে পরের ধাপ ৪০। ১০০ চাইলে chunk হয় ১১২ বাইট — ঠিক সেটিই দুটি malloc(100)-এর ঠিকানার দূরত্ব। ২০০০০০ বাইট mmap-এর সীমার (ডিফল্ট প্রায় ১২৮ KiB) ওপরে, তাই আলাদা mmap দিয়ে আনা হয়েছে আর page-এ গোল হয়েছে — ৬৮৮ বাইট বাড়তি। সব ঠিকানা ১৬-এর গুণিতক (addr%16 = 0), কারণ malloc যেকোনো ধরনের ডেটার জন্য উপযুক্ত alignment দিতে বাধ্য। এই সংখ্যাগুলো glibc আর x86-64-এর; অন্য allocator বা প্ল্যাটফর্মে আলাদা হবে, আর malloc_usable_size-এর ওপর প্রোগ্রামের যুক্তি দাঁড় করাবেন না — চাওয়া মাপের বাইরে লেখা এখনও ভুল।
দীর্ঘদিন চলা সার্ভারে fragmentation
একটি ওয়েব সার্ভার বা cache সপ্তাহের পর সপ্তাহ চলে। নানা মাপের অনুরোধ আসে-যায়; কিছু অংশ অনেকদিন বেঁচে থাকে, মাঝের অংশগুলো মুক্ত হয়। ধীরে ধীরে heap-এ hole জমে — ৫০-শতাংশ নিয়মের বাস্তব রূপ। লক্ষণ: প্রোগ্রামের আসল ব্যবহৃত ডেটা স্থির, কিন্তু OS-এর চোখে প্রসেসের RSS বাড়তেই থাকে — কারণ allocator এমন page ফেরত দিতে পারে না, যেখানে একটিও জীবিত অংশ আছে। এটি memory leak নয়, কিন্তু বাইরে থেকে দেখতে একই রকম।
- মাপা: allocator-এর নিজের হিসাব (জীবিত বাইট) আর OS-এর RSS তুলনা করুন। যেমন Redis-এর
INFO memory-তেmem_fragmentation_ratioঠিক এই অনুপাত দেখায়। - উপায়: allocator বদলানো বা tune করা, একই আয়ুর object একসাথে রাখা, কিছু সিস্টেমে চলন্ত defragmentation — বিস্তারিত ভাবনার প্রশ্ন ৫-এ। যেটিই করুন, নিজের workload-এ মেপে দেখুন।
আরও গভীরে: Linux kernel-এর buddy allocator-এ buddy-র হিসাব ঠিক এই পর্বের সূত্র — page frame number-এর ওপর pfn ^ (1 << order), তবে প্রতিটি memory zone আর NUMA node-এর আলাদা free area রাখা হয়, আর page-গুলোকে “movable/unmovable” ধরনে ভাগ করে রাখা হয় যাতে compaction সহজ হয়। Wilson, Johnstone, Neely ও Boles-এর ১৯৯৫ সালের বিখ্যাত সমীক্ষা (“Dynamic Storage Allocation: A Survey and Critical Review”) দেখিয়েছিল যে কৃত্রিম random workload দিয়ে fit নিয়ম মূল্যায়ন করা বিভ্রান্তিকর হতে পারে — বাস্তব প্রোগ্রামের trace-এ ঠিকানার ক্রমে সাজানো first fit আর best fit দুটিরই fragmentation বেশ কম পাওয়া গিয়েছিল।
৯ · AI-এর যুগে মেমরি allocation
Fit কৌশলের অঙ্ক আর allocator নিয়ে AI-কে প্রশ্ন করলে উত্তর দ্রুত আসে, কিন্তু কয়েকটি ভুল বারবার দেখা যায়:
| AI-এর ভুল | কেন ভুল | কীভাবে যাচাই করবেন |
|---|---|---|
| Hole ভাগ হওয়ার পর অবশিষ্ট অংশ ভুলে যাওয়া (যেমন first fit-এ ১১২K-কে ২৮৮K-এর বদলে ২০০K-এ বসানো) | Variable partition-এ অবশিষ্টও একটি hole | প্রতিটি ধাপের পর hole তালিকা লিখে নিন; মোট খালি = আগের খালি − অনুরোধ |
| Next fit-কে first fit-এর মতোই সমাধান করা | খোঁজা শুরু হয় আগের বরাদ্দের পর থেকে | Simulator-এ next চালিয়ে মিলিয়ে নিন |
| “Buddy system-এ external fragmentation নেই” | Buddy না হলে খালি ব্লক জোড়া লাগে না | উপরের ধাপ ৭: ৭০৪K খালি, বড়টি ২৫৬K |
| “৫০-শতাংশ নিয়ম মানে অর্ধেক মেমরি নষ্ট” | নিয়মটি hole-এর সংখ্যা ≈ N/2; এক-তৃতীয়াংশ একটি অনুমানের ফল | Knuth বা পাঠ্যবইয়ের মূল বাক্য পড়ুন |
malloc(n) ঠিক n বাইট দেয়, বা সব allocator একই রকম | alignment, header, size class; glibc-র নিয়ম অন্য allocator-এ খাটে না | নিজের মেশিনে malloc_usable_size চালিয়ে দেখুন |
| মেমরি বাড়লেই “leak” বলে রায়, বা বিনা মাপে allocator বদলানোর পরামর্শ | fragmentation-ও RSS বাড়ায়; উপকার workload-নির্ভর | জীবিত বাইট বনাম RSS মাপুন, লোড দিয়ে পরীক্ষা করুন |
অঙ্ক দেওয়ার সময় নিয়মগুলো স্পষ্ট লিখে দিন, আর প্রতিটি ধাপ যাচাইযোগ্য আকারে চান:
AI-এর উত্তর মানার আগে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Internal আর external fragmentation-এর পার্থক্য? — Internal: দেওয়া ব্লকের ভেতরের অব্যবহৃত অংশ; external: মোট খালি যথেষ্ট, কিন্তু টানা কোনো hole যথেষ্ট বড় নয়।
- Hole 100, 500, 200, 300, 600K-এ 212, 417, 112, 426K — কোন কৌশল সবাইকে জায়গা দেয়? — শুধু best fit (৩০০, ৫০০, ২০০, ৬০০); first আর worst fit-এ ৪২৬K অপেক্ষা করে।
- ৫০-শতাংশ নিয়ম কী বলে? — First fit-এ স্থিত অবস্থায় N টি ব্লকের সাথে প্রায় N/2 টি hole; hole-এর গড় আকার ব্লকের সমান ধরলে মেমরির প্রায় এক-তৃতীয়াংশ অব্যবহারযোগ্য।
- Compaction-এর শর্ত কী? — Execution-time (dynamic) relocation; আর চলমান DMA I/O-র buffer সরানো যাবে না।
- Buddy system-এ ঠিকানা a, আকার s হলে buddy কোথায়? — a XOR s; ২-এর ঘাতে গোল করায় internal fragmentation সর্বোচ্চ প্রায় ৫০%.
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১IBM OS/360-এর MFT থেকে MVT, তারপর virtual memory — contiguous allocation-এর অভিজ্ঞতা কীভাবে ইতিহাসকে paging-এর দিকে ঠেলে দিল?
১৯৬০-এর দশকে মেমরি ছিল অত্যন্ত দামি — বড় মেইনফ্রেমেও কয়েকশো কিলোবাইট থেকে কয়েক মেগাবাইট। Multiprogramming-এর পুরো যুক্তি ছিল এই দামি মেমরি আর CPU-কে ব্যস্ত রাখা: একটি কাজ I/O-র জন্য অপেক্ষা করলে আরেকটি চলবে। MFT-তে partition আগে থেকে ঠিক করা, তাই সিস্টেম সরল, কিন্তু অপারেটরকে আগেই ভাবতে হতো কোন মাপের কাজ বেশি আসবে। ভুল অনুমান মানে হয় বড় partition-এ ছোট কাজ বসে internal fragmentation, নয়তো বড় কাজের জন্য কোনো partition যথেষ্ট নয়।
MVT সেই সমস্যা সমাধান করল — প্রতিটি কাজ ঠিক যতটা চায় ততটা — কিন্তু নতুন সমস্যা আনল: external fragmentation. কিছুক্ষণ চলার পর মেমরিতে ছোট ছোট hole জমে, আর একটি বড় কাজ মোট খালি জায়গা থাকা সত্ত্বেও অপেক্ষা করে। এই পর্বের ক্লাসিক অঙ্কে first fit-এর ৯৫৯K খালি থাকতেও ৪২৬K-এর জায়গা না পাওয়া ঠিক সেই অভিজ্ঞতার ছোট রূপ। Compaction তাত্ত্বিক সমাধান, কিন্তু সে যুগের ধীর মেমরিতে বড় ব্লক কপি করা মানে মূল্যবান CPU সময় নষ্ট, আর load-time binding-এ লেখা প্রোগ্রাম সরানোই যেত না।
একই সময়ে অন্যত্র ভিন্ন পথ তৈরি হচ্ছিল। ম্যানচেস্টারের Atlas কম্পিউটার (১৯৬০-এর দশকের শুরু) paging আর “one-level store” দেখিয়েছিল; পরে MIT-র Multics segmentation আর paging মিলিয়েছিল। Paging-এর মূল কথা হলো “এক টুকরো টানা” শর্তটিই বাদ দেওয়া — প্রসেসের মেমরি সমান মাপের page-এ ভাগ, আর যেকোনো খালি frame-এ যেকোনো page বসতে পারে। তাতে external fragmentation কার্যত শেষ, internal fragmentation শুধু প্রতিটি প্রসেসের শেষ page-এ। IBM-ও ১৯৭০-এর দশকের শুরুতে System/370-এ virtual memory আর OS/VS সংস্করণগুলো আনে (OS/VS2-কে MVT-এর উত্তরসূরি হিসেবে দেখা হয়)।
মূল উপলব্ধি: External fragmentation কোনো খারাপ fit নিয়মের দোষ নয়, “টানা জায়গা চাই” শর্তের অনিবার্য ফল — তাই শেষ সমাধান এসেছিল শর্তটি তুলে দিয়ে, নিয়ম বদলে নয়।
প্র ০২নামেই “best” — তবু best fit কি সত্যিই সবচেয়ে ভালো? Simulation আর বাস্তব trace কী বলে, আর worst fit-এর চিন্তা কোথায় ভুল?
Best fit-এর যুক্তি সহজ: সবচেয়ে আঁটসাঁট hole নিলে বড় hole অক্ষত থাকে, পরে বড় অনুরোধ এলে কাজে লাগে। এই পর্বের ক্লাসিক অঙ্কে ঠিক তাই ঘটেছে — best fit ৬০০K অক্ষত রেখে ৪২৬K-কে জায়গা দিয়েছে। কিন্তু উল্টো দিকও আছে: আঁটসাঁট hole থেকে কাটলে যা পড়ে থাকে, তা খুব ছোট — এখানে ৮৩K, ৮৮K, ৮৮K. এমন টুকরো প্রায়ই আর কারও কাজে আসে না; সময়ের সাথে মেমরি অসংখ্য অব্যবহারযোগ্য গুঁড়োয় ভরে যেতে পারে।
Worst fit-এর যুক্তি ঠিক উল্টো: সবচেয়ে বড় hole থেকে কাটলে অবশিষ্টও বড়, তাই কাজে লাগবে। শুনতে যুক্তিসঙ্গত, কিন্তু পাঠ্যবইয়ের simulation আর বেশিরভাগ গবেষণায় worst fit সবচেয়ে খারাপ ফল দেয়। কারণ এটি বড় hole-গুলোকেই প্রথমে ভাঙে — যে জিনিসটি সবচেয়ে দুর্লভ আর মূল্যবান, সেটিই সবার আগে খরচ হয়। এই পর্বের অঙ্কে worst fit ৬০০K ভেঙে ২১২K দিয়েছে, আর শেষে ৪২৬K-এর জায়গা থাকেনি।
১৯৯৫ সালে Paul Wilson ও সহকর্মীরা একটি প্রভাবশালী সমীক্ষায় দেখান, আগের অনেক গবেষণা কৃত্রিম random অনুরোধ দিয়ে fit নিয়ম মূল্যায়ন করেছিল, যা বাস্তব প্রোগ্রামের আচরণ থেকে অনেক আলাদা। বাস্তব প্রোগ্রামে অনুরোধের আকার অল্প কয়েকটি মাপে জমাট (একই struct বারবার), আর একসাথে তৈরি object প্রায়ই একসাথে মরে। বাস্তব trace-এ ঠিকানার ক্রমে সাজানো first fit আর best fit দুটিরই fragmentation বেশ কম পাওয়া গিয়েছিল, আর next fit প্রায়ই খারাপ ছিল — কারণ এটি একসাথে তৈরি object-গুলোকে মেমরির নানা জায়গায় ছড়িয়ে দেয়, ফলে তারা একসাথে মরলেও বড় hole তৈরি হয় না।
তাই প্রশ্নটির সৎ উত্তর হলো “workload-এর ওপর নির্ভর করে”, আর মূল্যায়ন করতে হয় বাস্তব trace দিয়ে। আধুনিক allocator এই কারণে কোনো একটি “fit”-এর ওপর ভরসা না করে size class, আলাদা আলাদা তালিকা আর একই আয়ুর object একসাথে রাখার মতো কৌশল মেশায়।
মূল উপলব্ধি: একটি নিয়মের নাম বা একটি অঙ্কের ফল দিয়ে তার মান বিচার করা যায় না; fragmentation নির্ভর করে অনুরোধের আকার আর আয়ুর ধরনের ওপর — আর সেটি মাপতে হয় বাস্তব ডেটায়।
প্র ০৩Linux kernel physical মেমরি compact করে, Java-র garbage collector heap compact করে — তাহলে C-এর malloc কেন পারে না? “Compaction সম্ভব কি না” আসলে কীসের ওপর নির্ভর করে?
Compaction-এর মূল শর্ত একটি: কোনো কিছু সরালে, তাকে যারা খুঁজে পায়, তাদের সবাইকে নতুন ঠিকানা জানাতে পারতে হবে। পুরোনো variable partition-এ এই কাজটি করত relocation register — প্রসেসের ভেতরের সব ঠিকানা logical, তাই ব্লক কপি করে শুধু register বদলালেই চলত। অর্থাৎ পরোক্ষ ঠিকানা (একটি indirection-এর স্তর) থাকলেই সরানো সম্ভব।
Linux kernel-এর physical memory compaction একই নীতিতে চলে, শুধু indirection-এর স্তর এখানে page table. একটি user প্রসেসের page যে physical frame-এই থাকুক, প্রসেস দেখে শুধু virtual ঠিকানা। Kernel page-টি কপি করে নতুন frame-এ বসায়, page table-এ ঠিকানা বদলায়, TLB পরিষ্কার করে — প্রসেস টেরও পায় না। কিন্তু সব page সরানো যায় না: kernel-এর নিজের অনেক ডেটা সরাসরি physical ঠিকানায় ব্যবহৃত হয়, আর DMA চলছে এমন buffer সরানো বিপজ্জনক। তাই kernel page-গুলোকে movable আর unmovable দলে আলাদা রাখার চেষ্টা করে, যাতে movable অঞ্চলে compaction কার্যকর হয়।
Java বা .NET-এর runtime-এ indirection নেই, কিন্তু আছে সম্পূর্ণ জ্ঞান: garbage collector জানে heap-এর প্রতিটি object-এর কোন field-এ reference আছে, আর stack-এর কোন জায়গায় pointer আছে। তাই সে object সরিয়ে প্রতিটি reference খুঁজে আপডেট করতে পারে।
C-তে দুটির কোনোটিই নেই। malloc একটি কাঁচা ঠিকানা দেয়; প্রোগ্রাম সেটি যেকোনো জায়গায় রাখতে পারে — আরেকটি struct-এ, একটি integer-এ cast করে, এমনকি ফাইলে লিখে। Allocator জানে না কোথায় কোথায় কপি আছে, তাই কিছু সরালে সেই কপিগুলো ঝুলন্ত pointer হয়ে যাবে। তাই C allocator-এর হাতিয়ার শুধু প্রতিরোধ — ভালো fit, size class, একই আয়ুর object একসাথে রাখা — আর OS-এর সাহায্যে খালি page ফেরত দেওয়া। কিছু গবেষণা (যেমন Mesh) virtual memory দিয়ে একাধিক অর্ধ-খালি page-কে একই physical frame-এ মিলিয়ে এই সীমা আংশিক পেরোনোর চেষ্টা করেছে, কারণ তাতে virtual ঠিকানা বদলাতে হয় না।
মূল উপলব্ধি: সরানো যায় কেবল তখনই, যখন ঠিকানা হয় পরোক্ষ (relocation register, page table) নয়তো সব reference জানা (GC) — কাঁচা pointer-এর জগতে fragmentation প্রতিরোধ করতে হয়, সারানো যায় না।
প্র ০৪Buddy system internal fragmentation-এ প্রায় অর্ধেক পর্যন্ত নষ্ট করতে পারে, তবু Linux kernel-এর page allocator কেন এটিই বেছে নিল? আর ছোট object-এর জন্য আলাদা slab কেন?
প্রশ্নটির উত্তর লুকিয়ে আছে “কী বরাদ্দ হচ্ছে” তার মধ্যে। Linux-এর buddy allocator byte বা kilobyte দেয় না, দেয় page — ২order টি পাশাপাশি page. বেশিরভাগ অনুরোধ order ০, অর্থাৎ একটিমাত্র ৪ KiB page, যা নিজেই ২-এর ঘাত, তাই গোল করার অপচয় নেই। বড় টানা অনুরোধ (যেমন DMA buffer, huge page) সাধারণত নিজেই ২-এর ঘাতের মাপে চাওয়া হয়। ফলে buddy-র সবচেয়ে বড় দুর্বলতা — ২-এর ঘাতে গোল করা — এখানে বড় আঘাত করে না।
অন্যদিকে buddy-র শক্তিগুলো kernel-এর কাছে খুব মূল্যবান। প্রথমত গতি: বরাদ্দ আর মুক্তি দুটিই সর্বোচ্চ কয়েক ধাপ (order-এর সংখ্যা, সাধারণত ১১টি স্তর), আর buddy খুঁজতে শুধু একটি XOR. দ্বিতীয়ত coalescing-এর নিশ্চয়তা: page মুক্ত হলেই সঙ্গী খালি থাকলে জোড়া লাগে, তাই বড় টানা ব্লক তৈরির সুযোগ থাকে — যা DMA আর huge page-এর জন্য দরকারি, কারণ সেখানে physically টানা মেমরি চাই, paging সেখানে সাহায্য করে না।
কিন্তু kernel-এর ভেতরে অসংখ্য ছোট object লাগে — কয়েক ডজন থেকে কয়েকশো বাইট। প্রতিটির জন্য একটি পুরো page নিলে internal fragmentation ভয়াবহ। এখানেই ১৯৯৪ সালে Jeff Bonwick-এর Solaris slab allocator-এর ভাবনা: buddy থেকে কয়েকটি page নিয়ে একই মাপের ঘরে ভাগ করা, প্রতিটি object-ধরনের জন্য আলাদা cache. একই মাপের ঘর বলে ভেতরে অপচয় প্রায় শূন্য, বরাদ্দ একটি তালিকা থেকে একটি ঘর তুলে নেওয়া, আর object-এর শুরুর অবস্থা (constructor) আগে থেকে তৈরি রাখার সুযোগও থাকে। Linux পরে SLAB, SLOB আর SLUB — তিন বাস্তবায়ন রেখেছিল; সাম্প্রতিক kernel-গুলোতে SLUB-ই প্রধান (পর্ব ৩৮)।
অর্থাৎ kernel দুই স্তরের নকশা নিয়েছে: নিচে buddy (বড়, ২-এর ঘাতের page ব্লক, দ্রুত জোড়া), ওপরে slab (ছোট, নির্দিষ্ট মাপের object, প্রায় শূন্য অপচয়)। প্রতিটি স্তর অন্যটির দুর্বলতা ঢাকে।
মূল উপলব্ধি: কোনো allocator সব মাপে ভালো নয়; বাস্তব সিস্টেম স্তর সাজায় — বড় ব্লকের জন্য buddy, ছোট object-এর জন্য segregated slab — যাতে প্রতিটি স্তরের অনুরোধ তার শক্তির জায়গায় পড়ে।
প্র ০৫একটি সার্ভিসের মেমরি সপ্তাহে সপ্তাহে বাড়ছে, অথচ কোড-রিভিউতে কোনো leak পাওয়া যায়নি। Fragmentation কি দায়ী? কীভাবে প্রমাণ করবেন, আর কী করবেন?
প্রথমে দুটি সংখ্যা আলাদা করতে হবে: প্রোগ্রাম আসলে কত বাইট জীবিত ডেটা ধরে আছে, আর OS-এর চোখে প্রসেস কত physical মেমরি (RSS) নিচ্ছে। Leak হলে দুটিই বাড়ে — জীবিত বাইটও বাড়তে থাকে, কারণ মুক্ত হওয়ার কথা এমন অংশ মুক্ত হয় না। Fragmentation হলে জীবিত বাইট মোটামুটি স্থির, কিন্তু RSS বাড়ে বা উঁচুতে আটকে থাকে। জীবিত বাইট জানতে allocator-এর নিজের পরিসংখ্যান লাগে: glibc-তে malloc_stats বা mallinfo2, jemalloc-এ তার stats ইন্টারফেস, Redis-এর মতো সফটওয়্যারে নিজস্ব রিপোর্ট (used_memory বনাম used_memory_rss, আর তাদের অনুপাত mem_fragmentation_ratio).
কেন fragmentation RSS ধরে রাখে? Allocator OS-কে মেমরি ফেরত দিতে পারে কেবল পুরো page বা বড় টানা অংশ হিসেবে। একটি ৪ KiB page-এ যদি একটিমাত্র ছোট জীবিত object থাকে, বাকিটা খালি হলেও page-টি ফেরত দেওয়া যায় না। দীর্ঘদিন চলা সার্ভারে নানা আয়ুর object মিশে গেলে অসংখ্য page এভাবে “আংশিক জীবিত” থাকে। glibc-তে আরেকটি পরিচিত কারণ হলো অনেক থ্রেডের জন্য অনেক arena — প্রতিটির নিজস্ব খালি জায়গা আলাদা পড়ে থাকে; এ কারণেই অনেক দল MALLOC_ARENA_MAX কমিয়ে উপকার পাওয়ার কথা জানায়।
প্রমাণের জন্য পরীক্ষা সাজান: একই লোড দিয়ে কয়েক ঘণ্টা চালিয়ে জীবিত বাইট আর RSS-এর সময়-রেখা আঁকুন; তারপর একই পরীক্ষা অন্য allocator (jemalloc বা tcmalloc, সাধারণত LD_PRELOAD দিয়ে) বা কম arena দিয়ে চালান। RSS-এর রেখা নিচে নামলে fragmentation-ই প্রধান কারণ। সমাধানের পথ: allocator বদলানো বা tune করা; একই আয়ুর object একসাথে বরাদ্দ করা (request-প্রতি arena, memory pool); বড় buffer পুনর্ব্যবহার; কিছু সিস্টেমে চলন্ত defragmentation (Redis-এর activedefrag); আর glibc-তে malloc_trim দিয়ে খালি অংশ OS-কে ফেরত দেওয়ার চেষ্টা। কোনটি কাজ করবে, তা আগে থেকে বলা যায় না — মেপে দেখতে হয়।
মূল উপলব্ধি: “মেমরি বাড়ছে” মানেই leak নয়; জীবিত বাইট আর RSS আলাদা করে মাপলেই leak আর fragmentation আলাদা হয় — আর fragmentation-এর ওষুধ কোডের বাগ সারানো নয়, বরাদ্দের ধরন বদলানো।
অনুশীলন
হাতে হিসাব, আউটপুট অনুমান, কোড আর ব্যাখ্যা — আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।
-
হাতে হিসাব: ঠিকানার ক্রমে ছয়টি hole — ৩০০K, ৬০০K, ৩৫০K, ২০০K, ৭৫০K আর ১২৫K (মাঝে অন্য প্রসেস, তাই জোড়া লাগে না)। প্রসেস এই ক্রমে আসে: ১১৫K, ৫০০K, ৩৫৮K, ২০০K, ৩৭৫K. First fit, best fit আর worst fit-এ প্রতিটি কোথায় বসবে? কোনটি অপেক্ষা করবে?
First fit: ১১৫ → ৩০০ (বাকি ১৮৫); ৫০০ → ৬০০ (বাকি ১০০); ৩৫৮ → ৭৫০ (বাকি ৩৯২); ২০০ → ৩৫০ (বাকি ১৫০); ৩৭৫ → ৩৯২ (বাকি ১৭)। সবাই জায়গা পায়; শেষে hole ১৮৫, ১০০, ১৫০, ২০০, ১৭, ১২৫ — মোট ৭৭৭K.
Best fit: ১১৫ → ১২৫ (বাকি ১০); ৫০০ → ৬০০ (বাকি ১০০); ৩৫৮ → ৭৫০ (বাকি ৩৯২); ২০০ → ২০০ (exact fit, hole মুছে যায়); ৩৭৫ → ৩৯২ (বাকি ১৭)। সবাই জায়গা পায়; শেষে hole ৩০০, ১০০, ৩৫০, ১৭, ১০ — মোট ৭৭৭K, সবচেয়ে বড় ৩৫০K.
Worst fit: ১১৫ → ৭৫০ (বাকি ৬৩৫); ৫০০ → ৬৩৫ (বাকি ১৩৫); ৩৫৮ → ৬০০ (বাকি ২৪২); ২০০ → ৩৫০ (বাকি ১৫০); ৩৭৫ — সবচেয়ে বড় hole ৩০০K, তাই অপেক্ষা, যদিও মোট খালি ১১৫২K. (Next fit-এও ৩৭৫ অপেক্ষা করে।) এখানে first fit আর best fit দুটিই সফল — অংশ ৪-এর ক্লাসিক অঙ্কে যেখানে শুধু best fit পেরেছিল। Simulator-এ
HOLESআরSEQবদলে এই ফলগুলো মিলিয়ে নেওয়া যায়। -
আউটপুট অনুমান: Buddy simulator-এর শেষ লাইন বদলে লেখা হলো
release("B"); alloc("E", 300); alloc("F", 129);(অর্থাৎ A, B, C, D-এর পর শুধু B মুক্ত, তারপর দুটি নতুন অনুরোধ)। E আর F-এর লাইনে কী ছাপা হবে?B মুক্ত হওয়ার পর free list:
64K:[192] 256K:[768,256]— মোট খালি ৫৭৬K. E = ৩০০K-কে গোল করলে ৫১২K; ৫১২ বা ১০২৪-এর কোনো খালি ব্লক নেই, তাই ছাপা হবেE 300K: no block -> WAIT. মোট ৫৭৬K খালি থাকতেও — buddy system-এর external fragmentation. F = ১২৯K-কে গোল করলে ২৫৬K; ২৫৬K-এর তালিকায় দুটি ব্লক, simulator ছোট ঠিকানাটি নেয়:F 129K -> 256K block at 256K (waste 127K). একটিমাত্র অনুরোধে ১২৭K নষ্ট — ২-এর ঘাতের ঠিক ওপরের অনুরোধের সবচেয়ে খারাপ রূপ। (Simulator-এ চালিয়ে আমরা ঠিক এই দুই লাইন পেয়েছি।) -
Buddy-র ঠিকানা আর ৫০-শতাংশ নিয়ম: (ক) একটি buddy pool-এ ৯৬K ঠিকানায় ৩২K ব্লক, আর ২০৮K ঠিকানায় ১৬K ব্লক — দুটির buddy কোথায়? (খ) কেউ বলল “৯৬K ঠিকানায় একটি ৬৪K ব্লক আছে” — এটি কেন অসম্ভব? (গ) First fit-এ স্থিত অবস্থায় গড়ে ১২০০টি ব্লক বরাদ্দ, আর প্রায় কখনো exact fit হয় না। আনুমানিক কতগুলো hole থাকবে?
(ক) ৯৬ = ৬৪ + ৩২, তাই ৯৬ XOR ৩২ = ৬৪ — buddy ৬৪K ঠিকানায় (দুটি মিলে ৬৪K ঠিকানায় ৬৪K ব্লক)। ২০৮ = ১২৮ + ৬৪ + ১৬, তাই ২০৮ XOR ১৬ = ১৯২ — buddy ১৯২K-এ (মিলে ১৯২K-এ ৩২K).
(খ) Buddy system-এ s মাপের ব্লক সবসময় s-এর গুণিতক ঠিকানায় শুরু হয়। ৯৬ ৬৪-এর গুণিতক নয় (৯৬ ÷ ৬৪ = ১.৫), তাই ৯৬K-এ ৬৪K ব্লক তৈরিই হতে পারে না — এই alignment-ই XOR সূত্রকে সঠিক রাখে।
(গ) ৫০-শতাংশ নিয়মে hole ≈ pN/2; p ≈ ১, N = ১২০০, তাই প্রায় ৬০০টি hole. hole-এর গড় আকার ব্লকের সমান ধরলে মেমরির প্রায় ৬০০ ÷ ১৮০০ = এক-তৃতীয়াংশ hole-এ — কিন্তু মনে রাখুন, এই শেষ অংশটি একটি বাড়তি অনুমান।
-
কোড বদলান: Fit simulator-এর
SEQ-এর শেষে আরেকটি ঘটনা যোগ করুন —["free", "P1"]. চালানোর আগে বলুন, first fit-এর শেষ hole তালিকা আর শেষ লাইন কী হবে, আর P1-এর জায়গা কেন পাশের hole-এর সাথে জোড়া লাগবে না।First fit-এ P1 বসে ছিল ১৫০K ঠিকানায় (২১২K). তার বাঁয়ে ১০০–১৫০K-এ X (অন্য প্রসেসের ৫০K), ডানে ৩৬২K-এ P3 — দুই পাশেই ব্যবহৃত, তাই অংশ ৩-এর টেবিলের প্রথম ঘটনা: নতুন একটি hole, hole-সংখ্যা এক বাড়ে। শেষ অংশ হবে:
free P1 holes: 100 212 176 200 300 174 free 1162K in 6 holes, largest 300Kমোট খালি ৯৫০ + ২১২ = ১১৬২K, কিন্তু সবচেয়ে বড় hole তবু ৩০০K. বাঁয়ের ১০০K hole আর নতুন ২১২K-এর মাঝে X বসে থাকায় তারা মিশতে পারে না; আর নতুন ২১২K আর ১৭৬K-এর মাঝে P3. ঠিক এই কারণেই external fragmentation-এ মোট খালি বাড়লেও বড় অনুরোধের সুবিধা হয় না। (Simulator-এ চালিয়ে আমরা ঠিক এই লাইনগুলো পেয়েছি।)
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমBuddy system-এ একটি অনুরোধ মেটানো থেকে ব্লক ফেরত পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- অনুরোধের আকার বাড়িয়ে নিকটতম ২-এর ঘাত s ধরা
- s বা তার চেয়ে বড় মাপের সবচেয়ে ছোট খালি ব্লক খোঁজা
- ব্লকটি বারবার অর্ধেক করা, প্রতিবার এক অর্ধেক free list-এ রাখা, যতক্ষণ না আকার s
- s মাপের ব্লকটি প্রসেসকে দেওয়া
- মুক্তির সময় ঠিকানা XOR আকার দিয়ে buddy বের করে, খালি থাকলে জোড়া লাগানো — বারবার
মোট খালি জায়গা একটি অনুরোধের চেয়ে বেশি, কিন্তু কোনো একটি টানা hole অনুরোধের মাপের নয়, তাই অনুরোধটি অপেক্ষা করছে। এটি কী?
- Internal fragmentation
- External fragmentation
- Thrashing
- Compaction
ঠিকানার ক্রমে hole: ১০০K, ৫০০K, ২০০K, ৩০০K, ৬০০K. প্রথম অনুরোধ ২১২K. Best fit কোন hole বেছে নেবে?
- ১০০K
- ৫০০K
- ৩০০K
- ৬০০K
একটি binary buddy pool-এ (ঠিকানা pool-এর শুরু থেকে) ১৯২K ঠিকানায় একটি ৬৪K ব্লক মুক্ত হলো। তার buddy কোন ঠিকানায়?
- ১২৮K
- ২৫৬K
- ০K
- ১৬০K
Knuth-এর ৫০-শতাংশ নিয়ম (first fit, স্থিত অবস্থা, exact fit বিরল) অনুযায়ী গড়ে N টি ব্লক বরাদ্দ থাকলে কোনটি সঠিক?
- মেমরির অর্ধেক সবসময় নষ্ট হয়
- প্রায় N টি hole থাকে
- প্রতিটি hole ব্লকের অর্ধেক মাপের হয়
- প্রায় N/2 টি hole থাকে; hole আর ব্লকের গড় আকার সমান ধরলে প্রায় এক-তৃতীয়াংশ মেমরি hole-এ
Variable partition-এ compaction করতে হলে কোন শর্ত অবশ্যই লাগবে?
- Compile-time address binding
- Execution-time (dynamic) relocation — যেমন base বা relocation register
- Best fit বরাদ্দ ব্যবহার করা
- মেমরি buddy system-এ ভাগ থাকা
একটি binary buddy allocator-এ ১২৯K চাওয়া হলো (ব্লকের মাপ ২-এর ঘাতে, K-এককে)। কত বড় ব্লক দেওয়া হবে, আর internal fragmentation কত?
- ১২৯K, অপচয় ০
- ১২৮K, অপচয় ১K
- ২৫৬K, অপচয় ১২৭K
- ৫১২K, অপচয় ৩৮৩K
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Contiguous allocation: প্রতিটি প্রসেস এক টুকরো টানা physical মেমরি পায় (base + limit). Fixed partition-এ internal fragmentation, variable partition-এ external fragmentation.
- চার fit: first (প্রথম মানানসই), next (আগের জায়গা থেকে), best (সবচেয়ে ছোট মানানসই), worst (সবচেয়ে বড়)। ক্লাসিক অঙ্কে (100, 500, 200, 300, 600K; 212, 417, 112, 426K) শুধু best fit চারটিকে বসায়; first আর worst fit-এ ৪২৬K অপেক্ষা করে।
- ৫০-শতাংশ নিয়ম: first fit-এ স্থিত অবস্থায় N টি ব্লকে প্রায় pN/2 ≈ N/2 টি hole; গড় আকার সমান ধরলে মেমরির প্রায় এক-তৃতীয়াংশ। Fragmentation স্থিত অবস্থার স্বাভাবিক ফল।
- Compaction: hole এক করে, কিন্তু execution-time binding লাগে, আর কপির খরচ বড়; কম ব্লক সরিয়েও প্রায়ই কাজ হয় (এখানে ৯৪১K-এর বদলে ৫০K).
- Buddy আর malloc: ২-এর ঘাতে split আর coalesce, buddy = a XOR s; internal fragmentation সর্বোচ্চ প্রায় ৫০%, external পুরো যায় না। Slab আর malloc-এর size class একই segregated-list ভাবনা; দীর্ঘদিন চলা সার্ভারে fragmentation RSS বাড়ায় — leak থেকে আলাদা করে মাপতে হয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩২ · Paging — page, frame আর page tableপরবর্তী পর্বটানা জায়গার শর্ত তুলে দিয়ে external fragmentation থেকে মুক্তি।
- অপারেটিং সিস্টেম · কোর্স হোম৬৪টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ৩০ · মেমরির ভিত্তি — address binding, logical আর physical addressপেছনে ফিরেBase আর limit register, MMU আর binding-এর তিন সময়।
- পর্ব ৩৮ · Linux-এ মেমরি — malloc, mmap, slab, huge page আর OOM killerবাস্তব kernelLinux-এর buddy allocator, SLUB আর memory compaction.
- C · নিজের malloc বানানো — memory allocator-এর ভেতরেহাতে-কলমেFree list আর coalescing দিয়ে নিজের allocator লেখা।
- DSA · Linked listডেটা স্ট্রাকচারFree list-এর ভিত্তি — singly আর doubly linked list.