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

Frame allocation, thrashing আর working set

Frame Allocation, Thrashing and the Working-Set Model — Equal and Proportional Allocation, PFF, PSI

Taught in Bangla, this lesson covers frame allocation limits, equal, proportional and priority allocation with calculations, global versus local replacement, thrashing where the disk is busy and the CPU idle, the locality model, Denning's working-set model, estimating working sets, page-fault frequency, and how Linux and Windows handle memory pressure, including PSI.

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

  • একটি প্রসেসকে সর্বনিম্ন কতটি frame দিতেই হবে, আর কেন সেটি hardware ঠিক করে
  • Equal, proportional আর priority allocation হাতে হিসাব করা, আর global বনাম local replacement-এর trade-off
  • Thrashing কেন হয়, CPU utilisation-এর বক্ররেখা কেন হঠাৎ ভেঙে পড়ে, আর দুষ্টচক্রটি কীভাবে ঘোরে
  • Denning-এর working-set model দিয়ে reference string থেকে WS(t, Δ) বের করা, D > m নিয়ম আর PFF নিয়ন্ত্রণ
  • Linux আর Windows বাস্তবে মেমরির চাপ কীভাবে সামলায় — kswapd, OOM killer, PSI, memory.high, working set manager

১ · প্রতিটি প্রসেস কয়টি frame পাবে — নিচের আর ওপরের সীমা

পর্ব ৩৫-এ দেখেছেন demand paging: প্রসেসের সব page শুরুতে RAM-এ আসে না; যে page দরকার, সেটি প্রথম ছোঁয়ার সময় page fault হয়, আর OS তখন ডিস্ক থেকে এনে একটি page frame-এ বসায়। পর্ব ৩৬-এ দেখেছেন frame খালি না থাকলে page replacement algorithm কাকে সরাবে — FIFO, Optimal, LRU, Clock. কিন্তু একটি প্রশ্ন এখনো খোলা: RAM-এ ধরুন মোট ৬২টি frame, আর চলছে দুটি বা দশটি প্রসেস। কে কয়টি frame পাবে? এই প্রশ্নের নাম frame allocation.

একটি সরকারি অফিসে ৬২টি ফাইল-তাক আছে, আর কয়েকজন কর্মকর্তা কাজ করছেন। প্রত্যেকের টেবিলের কাছে যত তাক, তত কম তাঁকে দূরের গুদামে (ডিস্কে) হেঁটে যেতে হয়। কাউকে একটি তাকও না দিলে তিনি একটি ফাইল খুলেই আবার গুদামে ছুটবেন। আবার একজনকেই সব তাক দিলে বাকিরা বসে থাকবেন। ভাগ করার নিয়মটিই frame allocation.

নিচের সীমা — instruction set ঠিক করে

একটি প্রসেসকে যত খুশি কম frame দেওয়া যায় না। কারণটি সূক্ষ্ম। Page fault হয় একটি instruction-এর মাঝখানে। OS page এনে দেওয়ার পর সেই instruction-টি আবার শুরু থেকে চালাতে হয়। এখন ধরুন একটি instruction তিনটি ভিন্ন page ছোঁয় — instruction নিজে এক page-এ, যে ঠিকানা থেকে পড়বে সেটি আরেক page-এ, যেখানে লিখবে সেটি তৃতীয় page-এ। প্রসেসের ভাগে মাত্র দুটি frame থাকলে কী হবে? তৃতীয় page আনতে প্রথমটি সরাতে হবে; instruction আবার শুরু হলে প্রথমটির জন্য আবার fault — চিরকাল ঘুরতেই থাকবে, একটি instruction-ও শেষ হবে না।

মূল ধারণা

সর্বনিম্ন frame-সংখ্যা = একটি instruction সবচেয়ে খারাপ ক্ষেত্রে একসাথে যতগুলো আলাদা page ছুঁতে পারে। এটি নির্ভর করে CPU-র instruction set architecture-এর ওপর, OS-এর ইচ্ছার ওপর নয়। সর্বোচ্চ সংখ্যাটি সহজ — যত physical frame আছে, তার বেশি কাউকে দেওয়া যায় না।

কয়েকটি উদাহরণ, সহজ থেকে কঠিন:

  • সরল load/store মেশিন (বেশিরভাগ RISC — ARM, RISC-V): প্রতিটি memory instruction একটিমাত্র memory-ঠিকানা ছোঁয়। Instruction-এর page + data-র page = ২টি। (Instruction নিজে দুই page-এর সীমানায় পড়লে আরও একটি লাগত — কিন্তু ক্লাসিক RISC-এ instruction-এর দৈর্ঘ্য নির্দিষ্ট (৪ byte) আর ঠিকানা সাজানো (aligned), তাই একটি instruction দুই page-এ ভাগ হয় না। ARM-এর Thumb-2 বা RISC-V-র compressed instruction-এর মতো মিশ্র দৈর্ঘ্যের রূপে অবশ্য এটি ঘটতে পারে।)
  • এক স্তরের indirect addressing: instruction-এর ভেতরের ঠিকানায় আসলে আরেকটি ঠিকানা লেখা, যেটি আসল ডেটার দিকে নির্দেশ করে। Instruction + ঠিকানার ঘর + ডেটা = ৩টি। বহু স্তরের indirection চললে তাত্ত্বিকভাবে পুরো virtual memory লাগতে পারে — তাই এমন মেশিন স্তরের একটি সীমা (একটি counter) বেঁধে দেয়।
  • IBM System/370-এর MVC (memory থেকে memory-তে ২৫৬ byte পর্যন্ত কপি): instruction নিজে ৬ byte, দুই page-এর সীমানায় পড়তে পারে; উৎস-ব্লকও দুই page-এ ছড়াতে পারে; গন্তব্য-ব্লকও। সবচেয়ে খারাপ ক্ষেত্রে ৬টি frame — পাঠ্যবইয়ের বিখ্যাত উদাহরণ।
  • x86: instruction-এর দৈর্ঘ্য পরিবর্তনশীল আর সাজানো থাকার বাধ্যবাধকতা নেই, আর movs-এর মতো string instruction উৎস আর গন্তব্য দুটোই memory-তে ছোঁয়; তাই x86-এর সর্বনিম্নও কয়েকটি page — নির্দিষ্ট সংখ্যা মডেলভেদে আলাদা হিসাব করা হয়।

বাস্তবের OS-এ এই নিচের সীমা নিয়ে আলাদা করে মাথা ঘামাতে হয় কমই, কারণ আজকের মেশিনে frame থাকে লক্ষ লক্ষ (8 GiB RAM ÷ 4 KiB = প্রায় ২০ লক্ষ frame). কিন্তু ধারণাটি পরীক্ষায় খুব জনপ্রিয়, আর এটি একটি গভীর সত্য শেখায়: OS-এর নীতি hardware-এর নকশা দিয়ে বাঁধা। একটি instruction মাঝপথে থেমে আবার শুরু হতে পারবে কি না — সেটিও hardware-এর দায়িত্ব।

২ · ভাগের নিয়ম — equal, proportional আর priority

নিচের আর ওপরের সীমার মাঝে OS কীভাবে ভাগ করবে? তিনটি ক্লাসিক নিয়ম, ধরুন m = মোট frame, n = প্রসেসের সংখ্যা।

⚖️ Equal allocation

সবাই সমান: প্রত্যেকে m/n টি (নিচের দিকে গোল করে)। ৬২টি frame, ২টি প্রসেস → ৩১টি করে। সহজ, কিন্তু ১০ page-এর ছোট প্রসেস ৩১টি নিয়ে ২১টি অপচয় করে, আর ১২৭ page-এর বড়টি টানাটানিতে থাকে।

ai = ⌊m / n⌋

📏 Proportional allocation

আকারের অনুপাতে: প্রসেস i-এর virtual memory-র আকার si page, সবার যোগফল S = Σ si. প্রত্যেকে পায় নিজের অংশ, নিচের দিকে গোল করে, কিন্তু সর্বনিম্নের কম নয়।

ai = ⌊(si / S) × m⌋

⭐ Priority allocation

অগ্রাধিকার অনুযায়ী: অনুপাত হিসাব করা হয় আকারের বদলে priority দিয়ে (বা দুটো মিলিয়ে)। আরেক রূপ: উচ্চ-priority প্রসেসের fault হলে নিচু-priority প্রসেসের frame থেকে একটি কেড়ে নেওয়া।

দামি কাজ আগে জায়গা পায়

পাঠ্যবইয়ের উদাহরণটি ধাপে ধাপে দেখুন — ৬২টি frame, দুটি প্রসেস: একটি ১০ page-এর ছাত্র-প্রসেস, আরেকটি ১২৭ page-এর ডেটাবেস। শেষ ধাপে একটি তৃতীয় প্রসেস আসে, আর দেখবেন সবার ভাগ কীভাবে ছোট হয়:

1 m = 62, s1 = 10, s2 = 1272 S = s1 + s2 = 1373 a1 = 10/137 x 62 = 4.53 -> 44 a2 = 127/137 x 62 = 57.47 -> 575 4 + 57 = 61, 1 frame free6 P3 (50 pages) arrives: S = 187
অবস্থা (ধাপের শেষে)
S (মোট page)
P1 (১০ page)
P2 (১২৭ page)
P3 (৫০ page)
খালি pool
  1. তথ্য। মোট frame m = ৬২. P1-এর আকার s1 = ১০ page, P2-এর s2 = ১২৭ page. সমান ভাগ করলে দুজনেই পেত ৩১টি।
  2. মোট আকার। S = ১০ + ১২৭ = ১৩৭ page. সবার মিলিত চাহিদা ১৩৭, কিন্তু frame আছে ৬২ — তাই প্রত্যেকে নিজের আকারের মোটামুটি ৬২/১৩৭ ≈ ৪৫% পাবে।
  3. P1-এর ভাগ। a1 = ১০/১৩৭ × ৬২ = ৪.৫৩ → নিচের দিকে গোল করে ৪টি। সমান ভাগের ৩১টির তুলনায় অনেক কম, কিন্তু ১০ page-এর প্রসেসের জন্য ৩১টি ছিল অপচয়।
  4. P2-এর ভাগ। a2 = ১২৭/১৩৭ × ৬২ = ৫৭.৪৭ → ৫৭টি। বড় প্রসেস বড় ভাগ পেল।
  5. যোগফল যাচাই। ৪ + ৫৭ = ৬১. নিচে গোল করার ফলে ১টি frame বেঁচে গেল — সেটি খালি frame-এর pool-এ থাকে, পরের page fault-এর জন্য তৈরি। যোগফল কখনো m-এর বেশি হতে পারে না; পরীক্ষার খাতায় এই যাচাইটি লিখুন।
  6. নতুন প্রসেস এল। P3 (৫০ page) ঢুকল: S = ১৮৭. এখন ১০/১৮৭ × ৬২ = ৩.৩২ → ৩, ১২৭/১৮৭ × ৬২ = ৪২.১১ → ৪২, ৫০/১৮৭ × ৬২ = ১৬.৫৮ → ১৬; মোট ৬১, খালি ১. Degree of multiprogramming বাড়লে প্রত্যেকের ভাগ কমে — এই সরল সত্যটিই পরে thrashing-এর বীজ।

নিচের calculator একই হিসাব করে, আর সর্বনিম্ন frame-এর নিয়মও মানে — ভাগ সর্বনিম্নের কম হলে বাড়িয়ে দেয়, তাতে মোট m ছাড়ালে সবচেয়ে বড় ভাগ থেকে কেটে নেয়:

// Frame allocation: equal (m / n) vs proportional a_i = floor(s_i / S * m).
// Every process gets at least minF frames; frames left over stay in the free pool.
function allocate(m, sizes, minF) {
  const n = sizes.length, S = sizes.reduce((x, y) => x + y, 0);
  const exact = sizes.map(s => s / S * m);
  const a = exact.map(e => Math.max(minF, Math.floor(e)));
  let used = a.reduce((x, y) => x + y, 0);
  while (used > m) {                                  // a minimum pushed us over m:
    const big = a.indexOf(Math.max(...a));            // take back from the largest
    a[big]--; used--;
  }
  console.log("m = " + m + ", S = " + S + ", minimum = " + minF + ", equal share = " + Math.floor(m / n));
  sizes.forEach((s, i) => console.log("  P" + (i + 1) + ": s = " + String(s).padStart(3) +
    "  s/S*m = " + exact[i].toFixed(2).padStart(5) + "  -> " + String(a[i]).padStart(2) + " frames"));
  console.log("  given " + used + ", free pool " + (m - used));
}
allocate(62, [10, 127], 2);       // the textbook example
allocate(40, [3, 60, 137], 2);    // a tiny process meets the minimum
allocate(62, [10, 127, 50], 2);   // a third process arrives: everyone shrinks

আউটপুট (প্রতিবার একই):

m = 62, S = 137, minimum = 2, equal share = 31
  P1: s =  10  s/S*m =  4.53  ->  4 frames
  P2: s = 127  s/S*m = 57.47  -> 57 frames
  given 61, free pool 1
m = 40, S = 200, minimum = 2, equal share = 13
  P1: s =   3  s/S*m =  0.60  ->  2 frames
  P2: s =  60  s/S*m = 12.00  -> 12 frames
  P3: s = 137  s/S*m = 27.40  -> 26 frames
  given 40, free pool 0
m = 62, S = 187, minimum = 2, equal share = 20
  P1: s =  10  s/S*m =  3.32  ->  3 frames
  P2: s = 127  s/S*m = 42.11  -> 42 frames
  P3: s =  50  s/S*m = 16.58  -> 16 frames
  given 61, free pool 1

প্রথম ব্লকটি পাঠ্যবইয়ের উত্তর: ৪ আর ৫৭, একটি frame খালি। দ্বিতীয় ব্লকে ৩ page-এর ছোট্ট P1-এর অনুপাত মাত্র ০.৬০ — শূন্য frame দিলে সে চলতেই পারবে না, তাই সর্বনিম্ন ২টি দেওয়া হলো। তাতে মোট হয় ২ + ১২ + ২৭ = ৪১, যা ৪০-এর বেশি; তাই সবচেয়ে বড় ভাগ P3-এর ২৭ থেকে একটি কেটে ২৬. তৃতীয় ব্লকটি stepper-এর শেষ ধাপ। খেয়াল করুন, “বেঁচে যাওয়া frame কাকে দেব” — এটি নীতির প্রশ্ন; এখানে আমরা সেগুলো খালি pool-এ রেখেছি, যা বাস্তব OS-ও করে।

একটি সীমাবদ্ধতা মনে রাখুন: equal আর proportional দুটোই প্রসেসের আকার দেখে, আচরণ দেখে না। ১২৭ page-এর ডেটাবেস হয়তো এই মুহূর্তে মাত্র ২০টি page নিয়ে কাজ করছে, আর ১০ page-এর প্রসেস একটি টাইট লুপে তার সব ১০টিই চাইছে। আকার নয়, এখন কতটা লাগছে — এই প্রশ্নের উত্তরই অংশ ৬-এর working set.

৩ · কার frame কেড়ে নেওয়া হবে — global না local replacement

Page fault হলো, কোনো frame খালি নেই। Replacement algorithm একটি victim বাছবে — কিন্তু কোন সেট থেকে? দুটি পথ:

দিকGlobal replacementLocal replacement
Victim কোথা থেকেসব প্রসেসের সব frame থেকে — অন্যের frame-ও নেওয়া যায়শুধু fault করা প্রসেসের নিজের frame থেকে
প্রসেসের frame-সংখ্যাচলার সময় বাড়ে-কমেস্থির থাকে (allocation নীতি না বদলালে)
Throughputসাধারণত বেশি — অলস প্রসেসের অব্যবহৃত frame ব্যস্ত প্রসেস পায়কম হতে পারে — একজনের অব্যবহৃত frame আরেকজন পায় না
গতির অনুমানযোগ্যতাকম — আপনার প্রোগ্রামের fault rate নির্ভর করে পাশের প্রোগ্রাম কী করছে তার ওপরবেশি — fault rate শুধু নিজের আচরণের ওপর নির্ভর করে
বিপদএকটি মেমরি-খেকো প্রসেস সবার frame কেড়ে সবাইকে ধীর করে দেয়ভাগ কম পড়লে প্রসেস নিজের ভেতরেই thrash করে, পাশে খালি frame থাকলেও

সাধারণ-উদ্দেশ্যের OS-এ global replacement বেশি প্রচলিত, কারণ মোট throughput বেশি। Linux-এর page reclaim মূলত global — cgroup-এর সীমা না থাকলে kernel প্রতিটি NUMA node-এ সব প্রসেসের page একই LRU তালিকাগুলোতে রাখে। তবে container-এর যুগে “একজন পাশের জনকে ধীর করে দিচ্ছে” (noisy neighbour) সমস্যাটি বড় হয়ে উঠেছে, তাই Linux cgroup দিয়ে প্রতিটি দলের জন্য আলাদা সীমা বসানো যায় — দলের ভেতরে reclaim হয় local-এর মতো (অংশ ৮)। Windows প্রতিটি প্রসেসের জন্য আলাদা working set রাখে আর নিজের working set-এর ভেতর থেকেই page সরায় — local-এর কাছাকাছি — কিন্তু মেমরি কম পড়লে সবার working set ছাঁটে, যা একটি global সিদ্ধান্ত। বাস্তব সিস্টেম তাই প্রায়ই দুটোর মিশ্রণ।

NUMA — কোন frame, সেটিও প্রশ্ন

বড় সার্ভারে সব RAM সমান দূরে নয়। NUMA মেশিনে প্রতিটি CPU-গুচ্ছের (node) নিজস্ব কাছের মেমরি থাকে; অন্য node-এর মেমরি পড়তে বেশি সময় লাগে। তাই প্রশ্ন শুধু “কয়টি frame” নয়, “কোন node-এর frame”. Linux-এর ডিফল্ট নীতি সাধারণত “first touch” — যে CPU প্রথম page-টি ছোঁয়, তার node থেকে frame দেওয়া হয়; আর scheduler প্রসেসকে সেই node-এর CPU-তেই রাখার চেষ্টা করে (affinity, পর্ব ২০)। numactl বা set_mempolicy() দিয়ে নীতি বদলানো যায়, আর Linux-এর automatic NUMA balancing প্রয়োজনে page-কে প্রসেসের কাছে সরিয়ে নিতে পারে। প্রতিটি node-এর জন্য আলাদা free list আর আলাদা reclaim thread থাকে — অর্থাৎ বড় মেশিনে allocation আসলে কয়েকটি ছোট মেশিনের allocation.

৪ · Thrashing — ব্যস্ত ডিস্ক, অলস CPU

একটি প্রসেস যে page-গুলো নিয়ে এই মুহূর্তে সক্রিয়ভাবে কাজ করছে, সেগুলোর জন্য যথেষ্ট frame না পেলে কী হয়? সে একটি page আনে, সেটি বসাতে আরেকটি সরায় — যেটি একটু পরেই আবার লাগবে। আবার fault, আবার সরানো। প্রসেসটি সময়ের বেশিরভাগ কাটায় page-এর জন্য অপেক্ষায়, কাজে নয়। এই অবস্থার নাম thrashing.

সংজ্ঞা

Thrashing: সিস্টেম (বা একটি প্রসেস) প্রকৃত কাজের চেয়ে বেশি সময় paging-এ — page আনা আর সরানোয় — ব্যয় করে। লক্ষণ: page-fault rate খুব বেশি, paging-ডিস্ক বা swap প্রায় ১০০% ব্যস্ত, CPU utilisation কম, আর প্রোগ্রামগুলো প্রায় এগোয় না।

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

CPU utilisation-এর বক্ররেখা

পর্ব ২-এ 1 − pn মডেলে দেখেছেন: প্রতিটি কাজ সময়ের p অংশ I/O-র অপেক্ষায় থাকলে, degree of multiprogramming n বাড়ালে CPU utilisation 1 − pn-এর মতো বাড়ে। সেখানে ধরে নেওয়া হয়েছিল মেমরিতে সবার জায়গা আছে। কিন্তু frame সীমিত হলে n বাড়ানো মানে প্রত্যেকের ভাগ কমানো (অংশ ২-এর শেষ ধাপ)। একটা সময় পর ভাগ এত ছোট হয় যে fault-এর বন্যা শুরু হয়, আর সব fault যায় একটিই paging-ডিস্কে। ডিস্ক একসাথে একটির বেশি fault সামলাতে পারে না — সে-ই তখন সীমা।

নিচের খেলনা-মডেলটি এই দুই শক্তিকে একসাথে বসায়। ১২০টি frame, প্রতিটি প্রসেস এখন ৩০টি page নিয়ে কাজ করে, প্রতি ১ ms CPU কাজের সাথে ১.৫ ms সাধারণ I/O-র অপেক্ষা, আর প্রতিটি page fault ডিস্কে ৫ ms খায়। ভাগ ৩০-এর কম হলে fault-এর হার দ্রুত বাড়ে:

// Toy thrashing model. m frames shared equally by n identical processes.
// Per 1 ms of CPU work a process also waits io ms for ordinary I/O, and
// takes f page faults, each costing T ms on ONE shared paging disk.
const m = 120, wss = 30, io = 1.5, T = 5;            // frames, pages, ms, ms
const faultsPerMs = a => a >= wss ? 0.002 : 0.002 + 2 * (1 - a / wss) ** 2;
for (let n = 1; n <= 10; n++) {
  const a = Math.floor(m / n), f = faultsPerMs(a);
  const p = (io + f * T) / (1 + io + f * T);            // fraction of time waiting
  const multi = 1 - p ** n;                             // 1 - p^n
  const disk = 1 / (f * T);                             // disk can serve 1 fault per T ms
  const u = Math.min(multi, disk);
  console.log("n=" + String(n).padStart(2) + " frames/proc=" + String(a).padStart(3) +
    " faults/ms=" + f.toFixed(3) + " CPU " + String(Math.round(u * 100)).padStart(3) + "% " +
    "#".repeat(Math.round(u * 40)) + (disk < multi ? "  <- paging disk is the limit" : ""));
}

আউটপুট (নির্ধারিত):

n= 1 frames/proc=120 faults/ms=0.002 CPU  40% ################
n= 2 frames/proc= 60 faults/ms=0.002 CPU  64% ##########################
n= 3 frames/proc= 40 faults/ms=0.002 CPU  78% ###############################
n= 4 frames/proc= 30 faults/ms=0.002 CPU  87% ###################################
n= 5 frames/proc= 24 faults/ms=0.082 CPU  88% ###################################
n= 6 frames/proc= 20 faults/ms=0.224 CPU  86% ##################################
n= 7 frames/proc= 17 faults/ms=0.378 CPU  53% #####################  <- paging disk is the limit
n= 8 frames/proc= 15 faults/ms=0.502 CPU  40% ################  <- paging disk is the limit
n= 9 frames/proc= 13 faults/ms=0.644 CPU  31% ############  <- paging disk is the limit
n=10 frames/proc= 12 faults/ms=0.722 CPU  28% ###########  <- paging disk is the limit

প্রতিটি # ২.৫% CPU. n = ১ থেকে ৪ পর্যন্ত সবার ৩০টি page RAM-এ আঁটে (৪ × ৩০ = ১২০), আর বক্ররেখা 1 − pn-এর মতোই ওঠে: ৪০% → ৮৭%. n = ৫-এ প্রত্যেকে পায় ২৪টি — fault একটু বাড়ে, কিন্তু পঞ্চম প্রসেসের বাড়তি কাজ তা ঢেকে দেয়, তাই ৮৮%. n = ৬-এ নেমে ৮৬%, আর n = ৭-এ হঠাৎ পতন — ৫৩%. কারণ লাইনের শেষে লেখা: ডিস্ক প্রতি ৫ ms-এ একটি fault সামলায়, আর প্রতি ms CPU কাজে ০.৩৭৮টি fault মানে ডিস্ক বড়জোর 1/(0.378 × 5) ≈ ৫৩% CPU-কে খাওয়াতে পারে। n = ১০-এ CPU মাত্র ২৮% — একটি প্রসেস একা যা পেত (৪০%), তার চেয়েও কম! সংখ্যাগুলো খেলনা-মডেলের; বাস্তবের বক্ররেখার আকার একই রকম হলেও নির্দিষ্ট সংখ্যা মেশিনভেদে আলাদা।

দুষ্টচক্র — OS নিজেই আগুনে ঘি ঢালে

১৯৬০-এর দশকের কিছু সিস্টেমে long-term scheduler-এর নিয়ম ছিল সরল: “CPU utilisation কম হলে আরও একটি কাজ মেমরিতে আনা হবে।” 1 − pn-এর জগতে এটি যুক্তিসংগত। কিন্তু thrashing-এ CPU কম ব্যস্ত কারণ প্রসেস বেশি। নিচের stepper-এ মডেলের সংখ্যাগুলো দিয়েই চক্রটি ঘুরিয়ে দেখুন:

  1. সুস্থ অবস্থাn = 4সবার page RAM-এ
  2. নতুন প্রসেসn = 6ভাগ ছোট হলো
  3. Fault-এর ঝড়n = 7ডিস্কে লাইন
  4. OS: “CPU খালি!”n = 8আরও প্রসেস আনে
  5. Thrashingn = 10প্রায় কাজ নেই
  6. সমাধানsuspendআবার n = 4
মডেলের অবস্থা (thrashing_curve.js থেকে)
প্রসেস n
প্রতিজনের frame
মোট চাহিদা D = n × 30
faults / ms CPU
CPU utilisation
  1. সুস্থ অবস্থা। ৪টি প্রসেস, প্রত্যেকে ৩০টি frame — ঠিক যতটা লাগে। মোট চাহিদা ১২০ = m. Fault প্রায় নেই, CPU ৮৭% ব্যস্ত।
  2. দুটি নতুন প্রসেস। n = ৬, প্রত্যেকের ভাগ নেমে ২০টি, যদিও দরকার ৩০. Global replacement-এ নতুনরা পুরোনোদের frame কেড়ে নেয়। Fault-এর হার ০.০০২ থেকে ০.২২৪ — ১০০ গুণের বেশি। তবু CPU ৮৬% — বিপদ এখনো চোখে পড়ে না।
  3. আরও একটি — fault-এর ঝড়। n = ৭, ভাগ ১৭টি। এখন প্রায় সব প্রসেস ready queue ছেড়ে paging-ডিস্কের লাইনে দাঁড়িয়ে। ডিস্ক সীমায় পৌঁছেছে, CPU নেমে ৫৩%.
  4. OS ভুল সংকেত পড়ে। পুরোনো নিয়ম বলে “CPU কম ব্যস্ত, তাই আরও কাজ আনা দরকার।” n = ৮, ভাগ ১৫টি, fault ০.৫০২, CPU ৪০%. প্রতিটি নতুন প্রসেস বাকিদের frame কেড়ে চক্রটিকে আরও জোরে ঘোরায়।
  5. পূর্ণ thrashing. n = ১০, ভাগ ১২টি — দরকারের অর্ধেকেরও কম। CPU ২৮%, ডিস্ক সারাক্ষণ ব্যস্ত, ব্যবহারকারী দেখেন মাউস নড়ে না। একা একটি প্রসেস চালালেও CPU এর চেয়ে বেশি কাজে লাগত (৪০%).
  6. চক্র ভাঙা। ওষুধ উল্টো দিকে: কয়েকটি প্রসেসকে পুরোপুরি swap out বা suspend করে n = ৪-এ ফেরা। তাদের frame বাকিরা পায়, CPU আবার ৮৭%. Suspend হওয়া প্রসেসগুলো পরে, চাপ কমলে ফেরে — এটি medium-term scheduler-এর কাজ।

Thrashing-এর কারণগুলো

  • অতিরিক্ত degree of multiprogramming — উপরের চক্র: সবার মোট সক্রিয় চাহিদা RAM-এর চেয়ে বেশি।
  • Global replacement-এর ছোঁয়াচে প্রভাব — একটি প্রসেস locality বদলালে (নতুন ধাপে ঢুকলে) অন্যদের frame কাড়ে, তারা fault করে আরও কাড়ে।
  • একটি প্রসেসের নিজের চাহিদা RAM-এর চেয়ে বড় — যেমন RAM-এর চেয়ে বড় একটি matrix সারি ধরে না পড়ে কলাম ধরে পড়া, বা RAM-এর চেয়ে বড় heap-এ garbage collector-এর পুরো heap ঘোরা। এখানে কোনো scheduling নীতিই বাঁচাতে পারে না।
  • ভুল নীতি — FIFO-র মতো যে replacement সক্রিয় page-কেও সরিয়ে দেয় (পর্ব ৩৬), বা ভাগের হিসাবে আচরণ না দেখে শুধু আকার দেখা।

৫ · Locality model — প্রোগ্রাম এক পাড়া থেকে আরেক পাড়ায় যায়

Thrashing ঠেকাতে হলে জানতে হবে একটি প্রসেসের আসলে কতটা লাগে। উত্তরের চাবি locality of reference: একটি সময়ে প্রোগ্রাম তার মেমরির ছোট একটি অংশই বারবার ছোঁয়। Locality model এর সাথে একটি গুরুত্বপূর্ণ কথা যোগ করে: locality সময়ের সাথে বদলায়।

একটি ফাংশন ডাকা হলে প্রোগ্রাম নতুন locality-তে ঢোকে: সেই ফাংশনের কোডের page, তার local ভ্যারিয়েবলের stack page, আর সে যে ডেটা নিয়ে কাজ করে তার page. ফাংশন শেষ হলে এই locality ছেড়ে পুরোনোটিতে ফেরে বা নতুন একটিতে যায়। দুটি locality-র কিছু page মিলতেও পারে। একটি payroll প্রোগ্রাম কল্পনা করুন — প্রথমে কর্মীদের ফাইল পড়ার ধাপ (এক locality), তারপর বেতন হিসাবের ধাপ (আরেক), শেষে রিপোর্ট ছাপার ধাপ (আরেক)।

বিশ্ববিদ্যালয়ের লাইব্রেরিতে একজন গবেষক। সপ্তাহখানেক তিনি পরিসংখ্যানের তাকের ৮–১০টি বই নিয়ে টেবিলে বসেন। তারপর লেখার ধাপে চলে যান অভিধান আর রচনার বইয়ের তাকে। প্রতিটি ধাপে তাঁর টেবিলে যতগুলো বই লাগে, ততটা জায়গা পেলেই তিনি কাজ করতে পারেন — পুরো লাইব্রেরি নয়। আর ধাপ বদলের সময় কিছুক্ষণ দুই তাকের বই-ই টেবিলে থাকে।

এখান থেকে thrashing-এর একটি নিখুঁত মানদণ্ড পাওয়া যায়: একটি প্রসেস তার বর্তমান locality-র সব page ধরে রাখার মতো frame পেলে সে locality বদলানোর সময় ছাড়া প্রায় fault করে না। তার চেয়ে কম পেলে সে thrash করবে। সমস্যা হলো, locality মাথায় রাখা সহজ, মাপা কঠিন — OS কীভাবে জানবে প্রসেস এখন কোন পাড়ায়?

৬ · Denning-এর working-set model

১৯৬০-এর দশকের শেষে IBM System/360 Model 67 আর MIT-এর Multics-এর মতো প্রথম দিকের virtual memory সিস্টেমগুলোতে দেখা গেল, বেশি কাজ ঢোকালে মেশিন হঠাৎ প্রায় থেমে যায়। MIT-এর তরুণ গবেষক Peter J. Denning ১৯৬৮ সালে দুটি বিখ্যাত লেখায় এর ব্যাখ্যা দেন — Communications of the ACM-এ “The Working Set Model for Program Behavior”, আর একই বছর AFIPS সম্মেলনে “Thrashing: Its Causes and Prevention”. তাঁর মূল চিন্তা: locality-কে সরাসরি মাপা না গেলেও তার একটি ভালো আনুমানিক রূপ মাপা যায় — সাম্প্রতিক অতীত।

সংজ্ঞা

প্রসেসের reference string (যে ক্রমে সে page ছোঁয়) r1, r2, … ধরুন। একটি স্থির সংখ্যা Δ — working-set window. সময় t-তে working set:

WS(t, Δ) = { rt−Δ+1, …, rt } — শেষ Δটি reference-এ যে আলাদা আলাদা page ছোঁয়া হয়েছে, তাদের সেট। এর আকার |WS(t, Δ)| হলো working-set size, সংক্ষেপে WSS.

Δ-এর ভূমিকা ক্যামেরার লেন্সের মতো। Δ খুব ছোট হলে working set পুরো locality ধরে না — দরকারি page বাদ পড়ে। Δ খুব বড় হলে কয়েকটি পুরোনো locality মিলে যায় — অকারণ বড় হিসাব। Δ = ∞ হলে প্রসেসের জীবনে ছোঁয়া সব page. (এই কোর্সের পর্ব ৯-এ working set শব্দটি দেখেছেন সাধারণ অর্থে, “সাম্প্রতিক ব্যবহৃত page”; Denning-এর সংজ্ঞা সেটিকেই Δ দিয়ে নিখুঁত করে।)

সমাধান করা উদাহরণ — Δ = ১০

একটি প্রসেসের ২৬টি reference: 2 6 1 5 7 7 7 7 5 1 6 2 3 4 1 2 3 4 4 4 3 4 3 4 4 4. শুরুতে প্রসেসটি page ১, ২, ৫, ৬, ৭ নিয়ে কাজ করে, শেষে শুধু ৩ আর ৪ নিয়ে। নিচে Δ = ১০-এর জানালাটি ডানে সরছে; ^ চিহ্নগুলো জানালার ভেতরের reference, t-সারির অঙ্কগুলো অবস্থানের শেষ অঙ্ক (১১ লেখা হয়েছে 1 হিসেবে):

জানালার হিসাব
সময় t
জানালা
WS(t, 10)
WSS
  1. t = ১০. জানালায় প্রথম ১০টি reference: ২ ৬ ১ ৫ ৭ ৭ ৭ ৭ ৫ ১. আলাদা page — {১, ২, ৫, ৬, ৭}, WSS = ৫. একই page বারবার (৭ চারবার) এলেও সেটে একবারই গোনা হয়।
  2. t = ১৪. জানালা r5…r14: ৭ ৭ ৭ ৭ ৫ ১ ৬ ২ ৩ ৪. পুরোনো locality-র ১, ২, ৫, ৬, ৭ এখনো জানালায়, আর নতুন ৩, ৪ ঢুকেছে: {১, …, ৭}, WSS = ৭ — সবচেয়ে বড়। Locality বদলের মুহূর্তে working set ফুলে ওঠে, কারণ দুই পাড়ার page-ই জানালায়।
  3. t = ১৮. জানালা r9…r18: ৫ ১ ৬ ২ ৩ ৪ ১ ২ ৩ ৪. ৭ শেষবার ছোঁয়া হয়েছিল r8-এ, এখন জানালার বাইরে — বাদ। {১, ২, ৩, ৪, ৫, ৬}, WSS = ৬.
  4. t = ২২. জানালা r13…r22: ৩ ৪ ১ ২ ৩ ৪ ৪ ৪ ৩ ৪. ৫ আর ৬-ও বেরিয়ে গেছে। {১, ২, ৩, ৪}, WSS = ৪.
  5. t = ২৬. জানালা r17…r26: ৩ ৪ ৪ ৪ ৩ ৪ ৩ ৪ ৪ ৪. প্রসেস পুরোপুরি নতুন locality-তে: {৩, ৪}, WSS = ২. এখন এই প্রসেসকে ২টির বেশি frame দেওয়া অপচয় — বাকি frame অন্য প্রসেস পেতে পারে। Working set এভাবে প্রসেসের আচরণ দেখে ভাগ ঠিক করে, আকার দেখে নয়।

নিচের calculator একই reference string-এ নির্দিষ্ট সময়গুলোর WS(t, ১০) ছাপে, তারপর তিনটি Δ-এর জন্য প্রতিটি সময়ের WSS, working-set নীতিতে মোট fault (যে reference-এর page শেষ Δটির মধ্যে ছোঁয়া হয়নি) আর গড় WSS:

// Working set WS(t, D) = distinct pages among the last D references r(t-D+1) .. r(t).
const refs = [2,6,1,5,7,7,7,7,5,1, 6,2,3,4,1,2,3,4,4,4, 3,4,3,4,4,4];
const ws = (t, D) => [...new Set(refs.slice(Math.max(0, t - D), t))].sort((a, b) => a - b);

console.log("t   r(t)  WS(t, 10)          WSS");
for (const t of [10, 14, 18, 22, 26])
  console.log(String(t).padEnd(4) + String(refs[t - 1]).padEnd(6) +
              ("{" + ws(t, 10).join(",") + "}").padEnd(19) + ws(t, 10).length);

for (const D of [3, 5, 10]) {
  let faults = 0, sum = 0;
  const line = refs.map((p, i) => {
    if (!ws(i, D).includes(p)) faults++;          // not referenced in the last D -> fault
    const w = ws(i + 1, D).length; sum += w; return w;
  });
  console.log("D=" + String(D).padEnd(3) + "WSS: " + line.join(" ") +
              "  faults=" + faults + " avg WSS=" + (sum / refs.length).toFixed(2));
}

আউটপুট:

t   r(t)  WS(t, 10)          WSS
10  1     {1,2,5,6,7}        5
14  4     {1,2,3,4,5,6,7}    7
18  4     {1,2,3,4,5,6}      6
22  4     {1,2,3,4}          4
26  4     {3,4}              2
D=3  WSS: 1 2 3 3 3 2 1 1 2 3 3 3 3 3 3 3 3 3 2 1 2 2 2 2 2 1  faults=16 avg WSS=2.27
D=5  WSS: 1 2 3 4 5 4 3 2 2 3 4 5 5 5 5 4 4 4 4 3 2 2 2 2 2 2  faults=10 avg WSS=3.23
D=10 WSS: 1 2 3 4 5 5 5 5 5 5 4 5 6 7 7 7 7 6 5 5 4 4 4 4 3 2  faults=8 avg WSS=4.62

প্রথম পাঁচ লাইন stepper-এর পাঁচটি ধাপ। পরের তিন লাইন Δ-এর trade-off স্পষ্ট দেখায়: Δ = ৩ থেকে ১০-এ গেলে fault ১৬ → ১০ → ৮, কিন্তু গড়ে frame লাগে ২.২৭ → ৩.২৩ → ৪.৬২টি। Δ = ১০-এর সারিতে t = ১৩ থেকে ১৭ পর্যন্ত WSS ৬–৭-এ ফুলে ওঠে — locality বদলের সেই “কুঁজ”। ৮টি fault-এর ৭টি হলো সাতটি আলাদা page-এর প্রথম ছোঁয়া (১, ২, ৩, ৪, ৫, ৬, ৭); বাকি একটি t = ১২-তে page ২ — তার আগের ছোঁয়া ছিল r1-এ, ঠিক ১১টি reference আগে, জানালার একটু বাইরে।

একটি ছোট প্রমাণ — Δ বাড়ালে fault কখনো বাড়ে না

পর্ব ৩৬-এ দেখেছেন FIFO-তে frame বাড়ালেও fault বাড়তে পারে (Belady's anomaly). Working-set নীতিতে এটি অসম্ভব। প্রমাণ:

  1. সংজ্ঞা অনুযায়ী WS(t, Δ) ⊆ WS(t, Δ + 1), কারণ শেষ Δটি reference শেষ Δ + 1টির অংশ। তাই একই t-তে WSS Δ-এর সাথে কখনো কমে না।
  2. Working-set নীতিতে reference rt fault করে ঠিক তখনই, যখন rt ∉ WS(t − 1, Δ) — অর্থাৎ ওই page-এর আগের ছোঁয়া Δটির বেশি reference আগে (বা কখনো নয়)।
  3. Δ বাড়ালে (১) অনুযায়ী WS(t − 1, Δ) শুধু বড় হয়, তাই কোনো hit কখনো fault-এ বদলায় না; কিছু fault hit-এ বদলাতে পারে। অতএব মোট fault Δ-এর একটি অ-বর্ধমান (non-increasing) ফাংশন। ∎

উপরের আউটপুটে ১৬ ≥ ১০ ≥ ৮ ঠিক এটাই দেখায়। দাম দিতে হয় মেমরিতে: গড় WSS Δ-এর সাথে বাড়ে।

মোট চাহিদা আর thrashing-এর নিয়ম

প্রতিটি প্রসেস i-এর working-set size WSSi জানা থাকলে সিস্টেমের মোট চাহিদা:

D = Σ WSSi  ·  D > m ⇒ thrashing আসছে

নিয়মটি সহজ। D ≤ m হলে প্রত্যেককে তার working set দেওয়া যায়, আর বাড়তি frame থাকলে নতুন প্রসেস আনা যায়। D > m হলে অন্তত একটি প্রসেস তার working set ধরে রাখতে পারবে না; সংজ্ঞা অনুযায়ী সে এমন page-এ fault করবে যা সে সাম্প্রতিক Δটির মধ্যে ছুঁয়েছে, অর্থাৎ বর্তমান locality-র page — এটাই thrashing-এর শুরু। তখন OS একটি প্রসেস suspend করে (সাধারণত সবচেয়ে কম priority-র, বা সবচেয়ে বড়টি, নীতিভেদে), তার frame বাকিদের দেয়, আর পরে D কমলে তাকে ফেরায়। অংশ ৪-এর stepper-এর ভাষায়: n = ৬-এ D = ১৮০ > ১২০, তাই নিয়মটি CPU পড়ে যাওয়ার আগেই সতর্ক করত। এভাবে working set একসাথে দুটি কাজ করে: thrashing ঠেকায়, আর নিরাপদ সীমার মধ্যে degree of multiprogramming যত বেশি সম্ভব রাখে।

৭ · Working set আন্দাজ করা, আর PFF

সংজ্ঞাটি সুন্দর, কিন্তু খরচ ভয়াবহ: প্রতিটি memory reference-এ জানালা সরানো আর সেট আপডেট করা মানে প্রতি কয়েক ন্যানোসেকেন্ডে হিসাব। কোনো hardware এটি করে না। তাই OS একটি আনুমানিক রূপ বানায় দুটি উপকরণ দিয়ে: পর্ব ৩৬-এর reference bit (page ছোঁয়া হলেই hardware যেটি ১ করে) আর একটি নিয়মিত timer interrupt.

পাঠ্যবইয়ের আনুমানিক কৌশল

ধরুন Δ = ১০,০০০ reference. Timer প্রতি ৫,০০০ reference-এ (সময় দিয়ে মাপা হয়, এখানে সহজ করে reference-এ বলা) একটি interrupt দেয়। প্রতিটি page-এর জন্য মেমরিতে রাখা হয় ২টি ইতিহাস-bit. প্রতিটি interrupt-এ OS:

  1. প্রতিটি page-এর ইতিহাস-bit-গুলো এক ঘর সরায় (পুরোনোটি হারিয়ে যায়), আর এখনকার reference bit-টি নতুন ঘরে কপি করে;
  2. সব reference bit শূন্য করে দেয়, যাতে পরের ৫,০০০ reference-এ কে ছোঁয়া হলো আবার দেখা যায়।

যেকোনো সময় একটি page-কে working set-এর অংশ ধরা হয়, যদি তার reference bit বা দুটি ইতিহাস-bit-এর অন্তত একটি ১ হয় — অর্থাৎ সেটি মোটামুটি শেষ ১০,০০০ থেকে ১৫,০০০ reference-এর মধ্যে ছোঁয়া হয়েছে।

অনিশ্চয়তা কোথায়: একটি bit শুধু বলে “এই ৫,০০০-এর খণ্ডে কোনো এক সময় ছোঁয়া হয়েছে” — কখন, বলে না। ধরুন একটি interrupt-এর ঠিক আগে একটি ইতিহাস-bit ১ দেখাচ্ছে, যা দুই খণ্ড আগের। Page-টি সেই খণ্ডের শুরুতে ছোঁয়া হয়ে থাকলে আসলে প্রায় ১৫,০০০ reference আগে — Δ-এর বাইরে, তবু working set-এ গোনা হলো। আবার খণ্ডের শেষে ছোঁয়া হলে ১০,০০০-এর ভেতরে। আনুমানিক working set তাই আসলটির চেয়ে কিছুটা বড় হতে পারে। প্রতিকার: বেশি bit আর ঘন interrupt — যেমন ১০টি bit, প্রতি ১,০০০ reference-এ interrupt — নির্ভুলতা বাড়ে, কিন্তু প্রতিটি interrupt-এ সব page ঘোরার খরচও বাড়ে। এটি পর্ব ৩৬-এর additional-reference-bits (aging) কৌশলেরই আত্মীয়।

Page-fault frequency (PFF) — ফলাফল দেখে নিয়ন্ত্রণ

Working set মাপার বদলে একটি সরাসরি পথ আছে: যা এড়াতে চাই, সেটিই মাপা। Thrashing মানে উঁচু fault rate. তাই page-fault frequency (PFF) কৌশলে OS প্রতিটি প্রসেসের fault rate মাপে আর দুটি সীমা বেঁধে দেয়:

⬆️ ওপরের সীমা ছাড়ালে

প্রসেসটি frame-এর অভাবে ভুগছে — তাকে আরও frame দিন। খালি frame না থাকলে কোনো একটি প্রসেস suspend করে তার frame দিন।

যেমন: ১,০০০ reference-এ ৮টির বেশি fault

⬇️ নিচের সীমার নিচে নামলে

প্রসেসটির হাতে দরকারের চেয়ে বেশি frame — কয়েকটি কেড়ে নিয়ে খালি pool-এ ফেরত দিন।

যেমন: ১,০০০ reference-এ ২টির কম fault

(সংখ্যাগুলো শুধু উদাহরণ; বাস্তব সীমা মেশিন আর ডিস্কের গতির ওপর নির্ভর করে।) PFF আসলে একটি feedback controller — ঘরের এসির thermostat-এর মতো: বেশি গরম হলে ঠান্ডা বাড়ানো, বেশি ঠান্ডা হলে কমানো। Working set-এর সাথে এর সম্পর্কও ঘনিষ্ঠ: locality বদলের সময় working set ফুলে ওঠে, তখন fault rate-ও লাফ দেয়; নতুন locality-র page সব RAM-এ এসে গেলে fault rate আবার নামে। অর্থাৎ fault rate-এর লাফগুলোই working set-এর পরিবর্তনের ছায়া।

৮ · বাস্তবের OS — Linux আর Windows কী করে

এখানে পাঠ্যবই আর বাস্তবের মধ্যে একটি বড় ফাঁক আছে, যা ইন্টারভিউতে প্রায়ই ধরা হয়: Linux কোনো স্পষ্ট working-set scheduler চালায় না। সে D গোনে না, D > m হলে নিজে থেকে প্রসেস suspend করে না। তার বদলে কয়েকটি স্তরের ব্যবস্থা কাজ করে (বিস্তারিত পর্ব ৩৮-এ):

  • Page reclaim আর kswapd: প্রতিটি NUMA node-এর জন্য একটি kernel thread, kswapd. খালি মেমরি একটি “low” watermark-এর নিচে নামলে সে জেগে ওঠে আর active/inactive LRU তালিকা থেকে কম ব্যবহৃত page মুক্ত করে — ডিস্কে লেখা আছে এমন file page সোজা ফেলে দেয়, পরিবর্তিত page আগে লিখে নেয়, আর anonymous page (heap, stack) swap-এ পাঠায়। চাপ আরও বাড়লে allocation-এর অনুরোধকারী প্রসেস নিজেই reclaim করতে বাধ্য হয় — direct reclaim, যা সেই প্রসেসকে ধীর করে।
  • OOM killer: reclaim করেও মেমরি না পেলে kernel একটি প্রসেস বেছে বন্ধ করে দেয় — মূলত যে সবচেয়ে বেশি মেমরি ধরে আছে, oom_score_adj দিয়ে সমন্বয় করা। Denning-এর “suspend” যেখানে প্রসেসকে পরে ফেরত আনে, OOM killer সেখানে স্থায়ীভাবে মেরে ফেলে — নিষ্ঠুর, কিন্তু সিস্টেম বাঁচে।
  • PSI (pressure stall information): Linux 4.20 (২০১৮) থেকে kernel মাপে, কাজগুলো মেমরির অপেক্ষায় কতটা সময় আটকে আছে। /proc/pressure/memory-তে দুটি লাইন: some — সময়ের কত শতাংশে অন্তত একটি কাজ মেমরির জন্য আটকে ছিল; full — কত শতাংশে সব সক্রিয় কাজ একসাথে আটকে ছিল। full-এর উঁচু মান thrashing-এর প্রায় সরাসরি মাপ। কিছু kernel-এ PSI বন্ধ থাকতে পারে (psi=1 boot option বা build config দিয়ে চালু হয়)।
  • cgroup v2-এর memory.high আর memory.max: একটি cgroup memory.high ছাড়ালে kernel সেই দলকে ধীর করে আর জোর দিয়ে তারই page reclaim করে — দলের ভেতরে local replacement-এর মতো। memory.max হলো কঠিন সীমা; সেটি ছাড়ালে দলের ভেতরেই OOM. প্রতিটি cgroup-এর নিজস্ব memory.pressure ফাইলও থাকে। systemd-oomd বা Android-এর lmkd-এর মতো user-space টুল PSI দেখে আগেভাগেই কম গুরুত্বপূর্ণ প্রসেস বন্ধ করে — এটিকে বলা যায় Denning-এর নিয়মের আধুনিক, user-space রূপ।

নিচের bash কোড এই কন্টেইনারের kernel থেকে আসল সংখ্যাগুলো পড়ে। /proc/vmstat-এর pgfault হলো বুটের পর মোট page fault, pgmajfault হলো তাদের মধ্যে major fault (ডিস্ক থেকে পড়তে হয়েছে), বাকিগুলো minor fault (page RAM-এই ছিল, শুধু page table-এ বসাতে হয়েছে):

echo "--- /proc/pressure/memory (PSI) ---"
cat /proc/pressure/memory 2>/dev/null || echo "(not available on this kernel)"
echo "--- /proc/vmstat: since boot, whole machine ---"
grep -E '^(pgfault|pgmajfault|pswpin|pswpout|pgscan_kswapd|oom_kill) ' /proc/vmstat
echo "--- /proc/meminfo ---"
grep -E '^(MemTotal|MemAvailable|SwapTotal|SwapFree):' /proc/meminfo
echo "--- this container's cgroup limits ---"
for f in memory.max memory.high memory.current; do
    if [ -r "/sys/fs/cgroup/$f" ]; then echo "$f = $(cat /sys/fs/cgroup/$f)"; fi
done
read -r -a st < /proc/$$/stat        # fields 10 and 12: minflt, majflt
echo "this shell: minor faults ${st[9]}, major faults ${st[11]}"

একটি আসল রানের আউটপুট (সংখ্যা প্রতিবার আলাদা হবে):

--- /proc/pressure/memory (PSI) ---
some avg10=0.00 avg60=0.00 avg300=0.00 total=14801991025
full avg10=0.00 avg60=0.00 avg300=0.00 total=8254130176
--- /proc/vmstat: since boot, whole machine ---
pswpin 105618903
pswpout 135327574
pgfault 96629579710
pgmajfault 110950864
pgscan_kswapd 1654945654
oom_kill 12
--- /proc/meminfo ---
MemTotal:        2008912 kB
MemAvailable:    1352316 kB
SwapTotal:       6041596 kB
SwapFree:        5626488 kB
--- this container's cgroup limits ---
memory.max = max
memory.high = max
memory.current = 937984
this shell: minor faults 284, major faults 0

এগুলো কন্টেইনারের নয়, পুরো host মেশিনের kernel-এর সংখ্যা (কন্টেইনার host-এর kernel ভাগ করে)। PSI-র avg10/avg60/avg300 হলো শেষ ১০, ৬০ আর ৩০০ সেকেন্ডে আটকে থাকা সময়ের শতাংশ — এই মুহূর্তে সব ০.০০, অর্থাৎ এখন কোনো চাপ নেই। কিন্তু total (মাইক্রোসেকেন্ডে, বুটের পর থেকে জমা) বলে অতীতের গল্প: some মোট প্রায় ৪.১ ঘণ্টা, full প্রায় ২.৩ ঘণ্টা — অর্থাৎ অতীতে সব মিলিয়ে দুই ঘণ্টার বেশি সময় এই মেশিনের সব সক্রিয় কাজ একসাথে মেমরির জন্য আটকে ছিল। /proc/vmstat-এর সংখ্যা page-এ গোনা: pswpout ১৩ কোটির বেশি page, 4 KiB করে প্রায় ৫১৬ GiB swap-এ লেখা হয়েছে, অথচ RAM মাত্র প্রায় ২ GB (MemTotal) — ছোট RAM-এর মেশিনে বারবার মেমরির টানাটানির চিহ্ন। oom_kill 12 মানে বুটের পর OOM killer ১২টি প্রসেস বন্ধ করেছে। মোট fault-এর মধ্যে major মাত্র প্রায় ০.১১% (pgmajfault ÷ pgfault) — বেশিরভাগ fault-ই minor, ডিস্ক ছাড়াই মেটে। কন্টেইনারের cgroup-এ memory.max আর memory.high দুটোই max, অর্থাৎ এই স্তরে কোনো সীমা বসানো নেই (ওপরের কোনো স্তরে থাকতে পারে), আর দলটি এখন মাত্র ৯১৬ KiB-এর মতো ব্যবহার করছে। শেষ লাইনে এই shell-এর নিজের হিসাব (/proc/$$/stat-এর ১০ আর ১২ নম্বর ঘর): ২৮৪টি minor fault, একটিও major নয় — প্রোগ্রাম শুরু হওয়ার সময় library আর heap-এর page প্রথমবার ছোঁয়ার খরচ, সবই ডিস্ক ছাড়া মিটেছে — library-র page আগে থেকেই page cache-এ ছিল, আর নতুন heap page শুধু শূন্য দিয়ে ভরা একটি frame.

নিজের Linux মেশিনে চাপ চলাকালীন দেখতে চাইলে vmstat 1 চালান: si আর so কলাম প্রতি সেকেন্ডে swap-in আর swap-out, wa কলাম CPU-র I/O-অপেক্ষার শতাংশ। Thrashing-এর ছবি হলো — si/so টানা উঁচু, wa উঁচু, us (user কাজ) নিচু। (এটি একটি উদাহরণ-ধারণা; কলামের বিস্তারিত পর্ব ৫৭-এ।)

Windows — working set manager

Windows-এর memory manager সরাসরি “working set” শব্দটিই ব্যবহার করে: প্রতিটি প্রসেসের working set মানে তার যে page-গুলো এখন RAM-এ আছে (Task Manager-এর “Memory” কলামের পেছনে মূলত এটিই)। প্রতিটি প্রসেসের একটি ন্যূনতম আর সর্বোচ্চ working set-সীমা থাকে, আর replacement প্রধানত প্রসেসের নিজের working set-এর ভেতরে হয়। Kernel-এর working set manager (balance set manager নামের system thread-এর অংশ) নিয়মিত, আর খালি মেমরি কমে গেলে আরও ঘন ঘন, প্রসেসগুলোর working set ছাঁটে — বহুদিন না ছোঁয়া page আগে সরে। ছাঁটা page সঙ্গে সঙ্গে হারায় না; standby তালিকায় থাকে, আবার লাগলে soft fault-এ ফিরে আসে। এটি Denning-এর ধারণার কাছাকাছি, যদিও Δ-জানালা গুনে নয়, বয়স আর চাপ দেখে। (macOS-ও নিজের পথে চলে — মেমরির চাপে প্রথমে page compress করে, আর iOS-এ চাপ বেশি হলে পেছনের app বন্ধ করে দেয়।)

আরও গভীরে: Linux-এর mm/workingset.c-এ Johannes Weiner-এর (২০১৪ সালের দিকে যুক্ত) “refault distance” কৌশল আছে, যা Denning-এর চিন্তাকে চুপিচুপি কাজে লাগায়। একটি file page সরানোর সময় kernel একটি ছোট “ছায়া-চিহ্ন” রাখে — তখন পর্যন্ত কতগুলো page সরানো হয়েছে তার হিসাব। Page-টি আবার fault করে ফিরলে (refault), এই দুই হিসাবের পার্থক্য বলে দেয় সেটি কত দূরত্বে ফিরেছে; দূরত্ব active তালিকার আকারের চেয়ে কম হলে page-টি আসলে working set-এর অংশ ছিল, তাই তাকে সরাসরি active তালিকায় তোলা হয়। এমন working-set page-এর refault-এর অপেক্ষা (direct reclaim-এর সময়ের সাথে) PSI-র memory stall-এ গোনা হয়। Linux 6.1-এ যুক্ত multi-gen LRU (MGLRU) page-এর বয়স আরও সূক্ষ্মভাবে ভাগ করে — working-set আন্দাজের আরেক আধুনিক রূপ।

বাস্তবের পরামর্শ — ওষুধ আসলে দুটিই

সব কৌশলের শেষে সত্যটি সহজ: সক্রিয় চাহিদা D যদি সত্যিই RAM-এর চেয়ে বেশি হয়, তাহলে কোনো replacement algorithm বা tuning চক্র থেকে বাঁচাতে পারে না। ওষুধ দুটি — বেশি RAM, নয়তো একসাথে কম প্রসেস (বা প্রসেসগুলোর মেমরি চাহিদা কমানো)। বাস্তবে এর অর্থ:

  • সার্ভারে swap-এর ব্যবহার আর PSI-র full মান নজরে রাখুন; টানা উঁচু হলে tuning নয়, ক্ষমতা পরিকল্পনা দরকার।
  • Container-এ memory.high দিয়ে নরম সীমা আর memory.max দিয়ে কঠিন সীমা বসান, যাতে একটি সার্ভিস বাকিদের thrash না করায়।
  • Worker-এর সংখ্যা (web server-এর process, database-এর connection) এমনভাবে বাঁধুন যাতে সবার working set মিলে RAM-এ আঁটে — “বেশি worker = বেশি গতি” সবসময় সত্য নয়, ঠিক অংশ ৪-এর বক্ররেখার মতো।
  • ডেস্কটপে মেশিন হঠাৎ ধীর আর ডিস্ক-বাতি জ্বলে থাকলে আগে দেখুন কোন প্রোগ্রাম মেমরি খাচ্ছে (Task Manager বা top); প্রায়ই একটি ব্রাউজারের শত ট্যাবই দোষী।

৯ · AI-এর যুগে — মেমরির চাপ নিয়ে AI-কে প্রশ্ন

“আমার সার্ভার ধীর, মেমরি ভরা” — এমন প্রশ্নে AI দ্রুত পরামর্শ দেয়, কিন্তু এই বিষয়ে কয়েকটি ভুল বারবার দেখা যায়:

AI-এর ভুলকেন ভুলকীভাবে যাচাই করবেন
“Linux working-set model দিয়ে thrashing ঠেকায়”Linux-এ আলাদা working-set scheduler নেই; reclaim, swap, OOM আর PSIKernel ডকুমেন্টেশনের PSI আর cgroup v2 অংশ পড়ুন
Proportional allocation-এ উপরের দিকে গোল করা, ফলে মোট m ছাড়িয়ে যাওয়াযোগফল m-এর বেশি হতে পারে নাভাগগুলো যোগ করে m-এর সাথে মেলান
WS(t, Δ)-এ জানালা এক ঘর ভুল জায়গায় শুরু করা (rt−Δ থেকে)জানালা rt−Δ+1 থেকে rt, ঠিক Δটি referenceজানালায় ঠিক Δটি reference আছে কি না গুনুন
“Swap বন্ধ করলেই thrashing শেষ”Anonymous page সরানোর পথ বন্ধ হয়, কিন্তু file page-এর thrashing চলে, আর OOM আগে আসেPSI আর pgmajfault দুই অবস্থায় মেপে দেখুন
echo 3 > /proc/sys/vm/drop_caches-কে ওষুধ বলাশুধু cache ফেলে দেয়; চাহিদা কমায় না, পরে আবার পড়তে হয়চালানোর পরে কয়েক মিনিট pgmajfault দেখুন
অন্য distro বা Windows-এর কমান্ড দেওয়া, বা সব কিছুতে sudoপড়ার কাজে root লাগে না; ভুল OS-এর কমান্ড কাজই করে নাman page দেখুন; আগে শুধু-পড়া কমান্ড চালান

সমস্যা বললে সাথে প্রমাণ দিন, আর নিরাপদ, যাচাইযোগ্য পদক্ষেপ চান:

আমার Ubuntu 24.04 সার্ভারে (RAM 8 GiB, swap 2 GiB) মাঝে মাঝে সব কিছু খুব ধীর হয়ে যায়। নিচে তখনকার আউটপুট দিলাম: [cat /proc/pressure/memory] [grep -E 'pgmajfault|pswpin|pswpout' /proc/vmstat — এক মিনিট ব্যবধানে দুবার] [ps -eo pid,comm,rss --sort=-rss | head] ১. এটি thrashing কি না, কোন সংখ্যা দেখে বলছ তা ব্যাখ্যা করো। ২. শুধু-পড়া (read-only) কমান্ড দিয়ে আর কী যাচাই করব, বলো; root লাগলে কেন লাগবে বলো। ৩. সমাধানের বিকল্পগুলো দাও (RAM বাড়ানো, worker কমানো, cgroup memory.high), প্রতিটির ঝুঁকি সহ। ৪. drop_caches বা swap বন্ধ করার পরামর্শ দিলে কেন, স্পষ্ট বলো। অনিশ্চিত হলে বলো।

AI-এর উত্তর মানার আগে — চেকলিস্ট

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

  • Thrashing কী, আর কেন হয়? — প্রসেস কাজের চেয়ে বেশি সময় paging-এ কাটায়; কারণ সক্রিয় page-এর মোট চাহিদা (Σ WSS) frame-সংখ্যার চেয়ে বেশি, সাধারণত অতিরিক্ত degree of multiprogramming থেকে।
  • ৬২টি frame, ১০ আর ১২৭ page-এর দুটি প্রসেস — proportional allocation? — ১০/১৩৭ × ৬২ ≈ ৪, ১২৭/১৩৭ × ৬২ ≈ ৫৭; একটি frame খালি থাকে।
  • Working set কী, আর thrashing ঠেকাতে কীভাবে ব্যবহার হয়? — শেষ Δটি reference-এ ছোঁয়া page-এর সেট; D = Σ WSSi > m হলে একটি প্রসেস suspend করা হয়।
  • Global আর local replacement-এর পার্থক্য? — Global-এ victim যেকোনো প্রসেসের, throughput বেশি কিন্তু গতি অনুমান-অযোগ্য; local-এ শুধু নিজের, গতি অনুমানযোগ্য কিন্তু frame অব্যবহৃত থাকতে পারে।
  • PFF কী? — প্রতিটি প্রসেসের page-fault rate-এর ওপর-নিচ দুটি সীমা; ওপরে গেলে frame দেওয়া, নিচে নামলে কেড়ে নেওয়া।

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

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

প্র ০১১৯৬০-এর দশকের শেষে virtual memory সিস্টেমগুলো হঠাৎ ভেঙে পড়ছিল কেন, আর Denning-এর working set কীভাবে ভাবনার ধরনটাই বদলে দিল?

Virtual memory-র প্রথম বড় বাস্তব রূপ ব্রিটেনের Manchester-এর Atlas কম্পিউটার (১৯৬২)। ধারণাটি জাদুর মতো লাগল — প্রোগ্রামারকে আর হাতে overlay সাজাতে হবে না, মেমরি যেন অসীম। এরপর IBM System/360 Model 67, MIT-এর Multics আর আরও কয়েকটি time-sharing সিস্টেম paging নিয়ে এল। কিন্তু সেগুলোকে বেশি ব্যবহারকারী আর বেশি কাজ দিয়ে চালাতে গিয়ে প্রকৌশলীরা একটি অদ্ভুত ঘটনা দেখলেন: একটি নির্দিষ্ট লোডের পর মেশিনের throughput ধীরে ধীরে নয়, হঠাৎ ভেঙে পড়ে। ডিস্ক সারাক্ষণ ঘরঘর করে, অথচ কাজ প্রায় এগোয় না। সে সময় অনেকে এটিকে খারাপ replacement algorithm-এর দোষ মনে করতেন, তাই নতুন নতুন algorithm নিয়ে পরীক্ষা চলছিল।

Peter Denning-এর অবদান ছিল প্রশ্নটিকে অন্যভাবে দেখা। ১৯৬৮-এর দুটি লেখায় তিনি দেখালেন, সমস্যাটি মূলত কোন page সরানো হবে তা নয়, বরং কতগুলো frame একটি প্রোগ্রাম পাচ্ছে। প্রতিটি প্রোগ্রামের একটি স্বাভাবিক “সক্রিয় সেট” আছে; সেটি মেমরিতে না থাকলে যেকোনো algorithm-ই হারবে। তিনি এই সেটকে মাপার যোগ্য সংজ্ঞা দিলেন — শেষ Δ সময়ে ছোঁয়া page-গুলো — আর দেখালেন thrashing হলো এক ধরনের ধারণক্ষমতার সমস্যা: সবার working set যোগ করে মেমরির চেয়ে বেশি হলে কাউকে বাইরে রাখতেই হবে।

এটি নীতির ভাষা বদলে দিল। আগে প্রশ্ন ছিল “কোন page সরাব?” (replacement). এখন যোগ হলো “কোন প্রোগ্রামকে মেমরিতে থাকতে দেব?” (load control). পরের দশকগুলোতে VMS-এর মতো সিস্টেম প্রতিটি প্রসেসের working-set সীমা রাখত, আর Windows NT সেই ধারা বহন করেছে — NT-র অনেক নকশাকার VMS থেকেই এসেছিলেন। Denning নিজে পরে locality নিয়ে বহু লেখায় দেখিয়েছেন যে এই নীতি শুধু মেমরিতে নয় — cache, web cache, এমনকি মানুষের মনোযোগের মডেলেও কাজে লাগে।

মূল উপলব্ধি: একটি সিস্টেম যখন হঠাৎ ভেঙে পড়ে, তখন প্রায়ই সমস্যা কৌশলে নয়, ধারণক্ষমতায়। সঠিক প্রশ্ন — “কে কত পাচ্ছে, আর মোট চাহিদা কত?” — কখনো কখনো একশোটি নতুন algorithm-এর চেয়ে বেশি কাজের।

প্র ০২পাঠ্যবইয়ে working-set model এত গুরুত্ব পায়, তবু Linux কেন কোনো স্পষ্ট working-set scheduler চালায় না?

প্রথম কারণ খরচ আর অনিশ্চয়তা। নিখুঁত working set মাপতে প্রতিটি reference দেখতে হয়, যা অসম্ভব; আনুমানিক রূপেও সব প্রসেসের সব page-এর reference bit নিয়মিত ঘুরে দেখতে হয়। কোটি কোটি page আর শত শত প্রসেসের মেশিনে এটি নিজেই ভারী কাজ। তার ওপর Δ কত হবে — সেটির কোনো সর্বজনীন উত্তর নেই; database, browser আর compiler-এর locality সম্পূর্ণ আলাদা।

দ্বিতীয় কারণ বাস্তবের মেমরি পাঠ্যবইয়ের মতো সমান নয়। Linux-এ RAM-এর বড় অংশ page cache — ফাইলের ডেটা, যা কোনো একটি প্রসেসের নয়, অনেকে ভাগ করে নেয়। Shared library, shared memory, copy-on-write — একই frame অনেক প্রসেসের। এখানে “প্রসেস i-এর WSS” সংজ্ঞায়িত করাই কঠিন। তাই Linux প্রসেস ধরে নয়, page ধরে চিন্তা করে: সব page-এর বয়স দেখে LRU-ধাঁচের তালিকায় রাখে, আর চাপ এলে সবচেয়ে ঠান্ডা page সরায় — একটি global, আচরণভিত্তিক নীতি।

তৃতীয় কারণ, suspend-এর বাস্তব অর্থ বদলে গেছে। ১৯৭০-এর batch সিস্টেমে একটি কাজকে কিছুক্ষণ থামিয়ে রাখলে কেউ টের পেত না। আজ একটি web server-কে ১০ সেকেন্ড থামানো মানে হাজার ব্যবহারকারীর timeout; একটি interactive অ্যাপ থামলে ব্যবহারকারী ভাবেন মেশিন হ্যাং। তাই আধুনিক পথ হলো আগেভাগে চাপ মাপা (PSI), সীমা বসানো (cgroup), আর শেষ পর্যন্ত কম গুরুত্বপূর্ণ কাজ বন্ধ করা (OOM killer, systemd-oomd, Android-এর lmkd). Android বা iOS-এ পেছনের app বন্ধ হয়ে যাওয়া আসলে এই নীতিরই চেহারা: পুনরায় খোলা সস্তা, তাই suspend-এর বদলে kill.

তবে Denning-এর চিন্তা হারায়নি, ভেতরে ঢুকে গেছে: refault distance দিয়ে কোন page working set-এর অংশ ছিল তা চেনা, PSI দিয়ে “সবাই আটকে আছে” মাপা — দুটোই working-set ধারণার উত্তরসূরি।

মূল উপলব্ধি: তত্ত্ব প্রায়ই সরাসরি কোড হয়ে টেকে না, কিন্তু তার মূল ধারণা নতুন রূপে বেঁচে থাকে। ইন্টারভিউতে “Linux working set ব্যবহার করে কি?” প্রশ্নের সবচেয়ে ভালো উত্তর “না, তবে …” দিয়ে শুরু হয়।

প্র ০৩Cloud-এ একই সার্ভারে বহু কোম্পানির container চলে। Global না local replacement — এখানে কোনটি ঠিক, আর cgroup কীভাবে মাঝামাঝি পথ দেয়?

একটি বড় সার্ভারে ধরুন তিনটি container: একটি ব্যাংকের লেনদেন-সার্ভিস, একটি ছবির প্রক্রিয়াকরণ কাজ, আর একটি লগ বিশ্লেষণ। শুদ্ধ global replacement-এ ছবির কাজটি হঠাৎ বিশাল ছবি নিয়ে মেমরি ভরিয়ে ফেললে kernel সবচেয়ে ঠান্ডা page সরাবে — যেগুলো হয়তো লেনদেন-সার্ভিসের। লেনদেন-সার্ভিসের নিজের কোনো দোষ নেই, তবু তার latency লাফিয়ে বাড়বে। Cloud-এ একে বলে “noisy neighbour” সমস্যা, আর এটি global replacement-এর অনুমান-অযোগ্যতার সরাসরি ফল। যে প্রতিষ্ঠান latency-র নিশ্চয়তা দিয়ে চুক্তি করেছে, তার জন্য এটি গ্রহণযোগ্য নয়।

শুদ্ধ local replacement — প্রত্যেককে স্থির সংখ্যক frame — অনুমানযোগ্য, কিন্তু অপচয়ী। রাতে লেনদেন কম, তখন তার ভাগের বড় অংশ অলস পড়ে থাকবে, আর পাশের লগ বিশ্লেষণ মেমরির অভাবে ধুঁকবে।

Linux cgroup v2 তাই স্তরে স্তরে নিয়ন্ত্রণ দেয়। memory.min আর memory.low দিয়ে একটি দলের জন্য সুরক্ষিত পরিমাণ রাখা যায় — চাপ এলে kernel অন্যদের থেকে আগে নেয়। memory.high একটি নরম ছাদ — ছাড়ালে সেই দলের ভেতরেই জোরালো reclaim আর ধীরগতি, অর্থাৎ local replacement-এর মতো আচরণ। memory.max কঠিন ছাদ — ছাড়ালে দলের ভেতরেই OOM, বাকিরা নিরাপদ। সীমার ভেতরে থাকলে অবশ্য মেমরি global-ভাবেই ভাগাভাগি হয়, তাই অলস মেমরি অপচয় হয় না। Kubernetes-এর মেমরি request আর limit শেষ পর্যন্ত এই cgroup সেটিংয়েই রূপ নেয়।

এটি ঠিক পাঠ্যবইয়ের proportional বা priority allocation নয়, কিন্তু চেতনা একই: কার কতটা নিশ্চিত পাওয়া উচিত, আর কে কতটা পর্যন্ত বাড়তে পারে — নীতিগতভাবে ঠিক করা।

মূল উপলব্ধি: Global বনাম local আসলে “সর্বোচ্চ মোট দক্ষতা” বনাম “প্রত্যেকের জন্য নিশ্চয়তা”-র লড়াই। আধুনিক সিস্টেম একটিকে বেছে নেয় না; সীমা আর সুরক্ষা দিয়ে দুটোর মাঝে নিজের জায়গা বেছে নেয়।

প্র ০৪“Thrashing-এর ওষুধ বেশি RAM” — কথাটি কতটা সত্য? কোন পরিস্থিতিতে RAM বাড়ালেও লাভ হয় না, আর কখন RAM না বাড়িয়েই সমাধান সম্ভব?

মূল সূত্রটি D > m. ওষুধ দুই দিক থেকে: m বাড়ানো (RAM) বা D কমানো। বেশিরভাগ ক্ষেত্রে RAM বাড়ালে সত্যিই কাজ হয় — বিশেষ করে যখন সমস্যা অনেকগুলো প্রসেসের যোগফল। কিন্তু কয়েকটি ক্ষেত্রে RAM সমাধান নয়।

প্রথম, একটি প্রোগ্রামের নিজের চাহিদা অসীমের দিকে। একটি memory leak থাকলে প্রোগ্রাম যত RAM পাবে, তত খাবে; RAM দ্বিগুণ করলে thrashing শুধু দেরিতে আসবে। এখানে ওষুধ বাগ ঠিক করা।

দ্বিতীয়, খারাপ locality. একটি বড় matrix যদি সারি ধরে মেমরিতে রাখা থাকে কিন্তু প্রোগ্রাম কলাম ধরে পড়ে, তাহলে প্রায় প্রতিটি reference নতুন page-এ পড়ে — working set বিশাল। লুপের ক্রম বদলালে (loop interchange) একই হিসাব অনেক কম page-এ হয়। পাঠ্যবইয়ের বিখ্যাত উদাহরণে ১২৮×১২৮ array-কে কলাম ধরে শূন্য করতে প্রায় ১৬,০০০ fault হতে পারে, সারি ধরে মাত্র ১২৮টি। এখানে কোড বদলানোই সস্তা ওষুধ।

তৃতীয়, garbage-collected ভাষা। Java বা Go-র heap-এর আকার যদি RAM-এর কাছাকাছি বা বেশি বসানো হয়, garbage collector পুরো heap ঘুরে দেখার সময় সব page ছোঁয় — working set হঠাৎ পুরো heap. Heap-এর সীমা ঠিক করা এখানে RAM কেনার চেয়ে ভালো।

চতুর্থ, অতিরিক্ত worker. একটি web server-এ ৫০টি worker প্রসেস, প্রত্যেকে ২০০ MB — মোট ১০ GB, অথচ RAM ৮ GB. Worker ৩০-এ নামালে throughput প্রায়ই বাড়ে, কারণ thrashing থামে — অংশ ৪-এর বক্ররেখার বাঁ দিকে ফেরা।

উল্টো দিকে, swap-কে দ্রুত SSD-তে রাখা বা Linux-এর zram (RAM-এর ভেতরে compressed swap, যা Android আর ChromeOS-এর মতো সিস্টেমে ব্যবহৃত হয়) fault-এর খরচ কমায়, তাই বক্ররেখার পতন নরম হয় — কিন্তু D > m-এর মূল সমস্যা মেটায় না।

মূল উপলব্ধি: আগে মাপুন D কেন বড় — অনেক প্রসেস, না একটি প্রোগ্রামের অভ্যাস। প্রথমটির ওষুধ RAM বা কম প্রসেস, দ্বিতীয়টির ওষুধ কোড।

প্র ০৫Working-set নীতি কি “সেরা” নীতি? LRU-র সাথে এর সম্পর্ক কী, আর Δ বাছাইয়ের তাত্ত্বিক trade-off কী?

দুটি নীতির তুলনাটি সূক্ষ্ম। LRU চলে স্থির সংখ্যক frame-এ (fixed-space): k টি frame থাকলে সবচেয়ে পুরোনো-ব্যবহৃত page সরায়। Working set চলে পরিবর্তনশীল জায়গায় (variable-space): Δটির মধ্যে না ছোঁয়া page সরিয়ে দেয়, frame-এর সংখ্যা যা-ই হোক। দুটোই একই তথ্য দেখে — একটি page শেষবার কবে ছোঁয়া হয়েছে। পার্থক্য হলো LRU জিজ্ঞেস করে “k জনের মধ্যে সবচেয়ে পুরোনো কে?”, working set জিজ্ঞেস করে “কে Δ-এর চেয়ে পুরোনো?”

দুটোরই একটি ভালো গুণ আছে: frame (LRU-তে k, working set-এ Δ) বাড়ালে fault কখনো বাড়ে না। LRU-র জন্য এটি stack property থেকে আসে (পর্ব ৩৬); working set-এর জন্য উপরের অংশ ৬-এর ছোট প্রমাণটি — WS(t, Δ) ⊆ WS(t, Δ + 1). তাই কোনোটিতেই Belady's anomaly নেই।

“সেরা” প্রশ্নে: স্থির frame-এ সেরা হলো Belady-র OPT (ভবিষ্যৎ জানা থাকলে)। পরিবর্তনশীল জায়গায় একই রকম একটি তাত্ত্বিক সেরা নীতির কথা গবেষণায় বলা হয় — ১৯৭০-এর দশকে Prieve আর Fabry-র VMIN নামে প্রস্তাবিত নীতি, যা ভবিষ্যতের দিকে তাকিয়ে একটি page রেখে দেয় কেবল যদি সেটি পরের Δটির মধ্যে আবার লাগবে। OPT-এর মতোই এটি বাস্তবে চালানো যায় না, কিন্তু তুলনার মানদণ্ড। Working set হলো তার “অতীত দেখে” রূপ — ঠিক যেমন LRU হলো OPT-এর অতীত-রূপ।

Δ বাছাইয়ের trade-off অংশ ৬-এর আউটপুটেই দেখেছেন: Δ = ৩, ৫, ১০-এ fault ১৬, ১০, ৮ আর গড় WSS ২.২৭, ৩.২৩, ৪.৬২. ছোট Δ মেমরি বাঁচায়, fault বাড়ায়; বড় Δ উল্টো। আর Δ বেশি বড় হলে পুরোনো locality-র মরা page অনেকক্ষণ জায়গা ধরে রাখে, যা মোট D বাড়িয়ে অন্য প্রসেসকে বাইরে রাখে — অর্থাৎ একজনের fault কমানো অন্যের ক্ষতি। তাই সঠিক Δ পুরো সিস্টেমের প্রশ্ন, একটি প্রসেসের নয়।

মূল উপলব্ধি: Working set আর LRU একই “সাম্প্রতিকতা”-র দুই প্রশ্ন — একটি জায়গা স্থির রেখে, অন্যটি সময় স্থির রেখে। দুটোরই আদর্শ রূপ ভবিষ্যৎ জানে, আর বাস্তব রূপ অতীত দিয়ে ভবিষ্যৎ আন্দাজ করে।

অনুশীলন

হাতে হিসাব, ট্রেস, কমান্ড আর ব্যাখ্যা — আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।

  1. হাতে হিসাব: মোট ১০০টি frame, তিনটি প্রসেস — P1 ৩০ page, P2 ৯০ page, P3 ৮০ page. (ক) Equal allocation-এ প্রত্যেকে কয়টি পাবে, কয়টি খালি থাকবে? (খ) Proportional allocation-এ? (গ) কোনটি ভালো, আর কেন দুটোরই একটি দুর্বলতা আছে?

    (ক) ⌊১০০/৩⌋ = ৩৩ করে, ৩ × ৩৩ = ৯৯, খালি ১টি।

    (খ) S = ৩০ + ৯০ + ৮০ = ২০০. a1 = ৩০/২০০ × ১০০ = ১৫, a2 = ৯০/২০০ × ১০০ = ৪৫, a3 = ৮০/২০০ × ১০০ = ৪০. যোগফল ঠিক ১০০, কিছু খালি নেই (এখানে ভাগফল পূর্ণসংখ্যা, তাই গোল করার দরকার পড়েনি)। Calculator-এ allocate(100, [30, 90, 80], 2) চালালে একই ফল আসে।

    (গ) Proportional ভালো, কারণ ৩০ page-এর P1 সমান ভাগে ৩৩টি পেয়ে ৩টি অপচয় করত আর ৯০ page-এর P2 মাত্র ৩৩টি পেত। কিন্তু দুটোই আকার দেখে — এই মুহূর্তে কার কতটা লাগছে (working set) দেখে না। P2 হয়তো এখন মাত্র ১০টি page নিয়ে কাজ করছে।

  2. ট্রেস করুন: Reference string 1 2 3 1 2 4 5 4 5 4 6 7 6 7 6, Δ = ৪. (ক) WS(৫, ৪), WS(১০, ৪) আর WS(১৫, ৪) বের করুন। (খ) Working-set নীতিতে মোট কয়টি fault? (গ) তিনটি প্রসেসের WSS এখন ৪০, ৩৫ আর ৩০, m = ১০০ — কী করা উচিত?

    (ক) t = ৫: জানালা r2…r5 = ২ ৩ ১ ২ → {১, ২, ৩}, WSS ৩. t = ১০: r7…r10 = ৫ ৪ ৫ ৪ → {৪, ৫}, WSS ২. t = ১৫: r12…r15 = ৭ ৬ ৭ ৬ → {৬, ৭}, WSS ২. প্রোগ্রামটি তিনটি locality পার হয়েছে।

    (খ) ৭টি — সাতটি আলাদা page-এর প্রথম ছোঁয়া। t = ৪-এ page ১-এর আগের ছোঁয়া t = ১-এ, মাত্র ৩টি আগে — জানালার ভেতরে, তাই hit; t = ৫-এ page ২-ও একইভাবে hit. বাকি সব পুনরাবৃত্তি ৪টির মধ্যেই। (Calculator-এর refs আর D বদলে মিলিয়ে নিন।)

    (গ) D = ৪০ + ৩৫ + ৩০ = ১০৫ > ১০০. Thrashing-এর ঝুঁকি — একটি প্রসেস suspend করতে হবে (যেমন সবচেয়ে কম priority-র)। ৩০-এরটি suspend করলে D = ৭৫, বাকি ২৫টি frame খালি থাকে; পরে চাপ কমলে সেটি ফেরে।

  3. কমান্ড লিখুন: একটি bash লুপ লিখুন, যা প্রতি সেকেন্ডে /proc/vmstat থেকে pgmajfault পড়ে আগের সেকেন্ডের সাথে পার্থক্য (প্রতি সেকেন্ডে major fault) ছাপে — ৫ বার। Root লাগবে কি?

    একটি সমাধান:

    prev=$(grep '^pgmajfault ' /proc/vmstat | cut -d' ' -f2)
    for i in 1 2 3 4 5; do
        sleep 1
        now=$(grep '^pgmajfault ' /proc/vmstat | cut -d' ' -f2)
        echo "major faults/s: $((now - prev))"
        prev=$now
    done

    Root লাগে না — /proc/vmstat সবাই পড়তে পারে। সংখ্যাটি পুরো মেশিনের, একটি প্রসেসের নয়। সুস্থ সার্ভারে এটি সাধারণত কাছাকাছি শূন্য থাকে; টানা শত বা হাজারের ঘরে থাকলে, সাথে PSI-র full উঁচু হলে, thrashing সন্দেহ করুন। vmstat 1-এর si/so কলামও কাছাকাছি তথ্য দেয়।

  4. ব্যাখ্যা করুন: Δ = ১০,০০০ reference, timer interrupt প্রতি ৫,০০০ reference-এ, প্রতিটি page-এর ২টি ইতিহাস-bit (প্রতি interrupt-এ সরানো হয়, তারপর reference bit শূন্য)। Interrupt হয়েছে ৫,০০০, ১০,০০০ আর ১৫,০০০-এ। Reference ১৭,০০০-এ একটি page X-এর অবস্থা: reference bit ০, নতুন ইতিহাস-bit (১৫,০০০-এ কপি) ০, পুরোনো ইতিহাস-bit (১০,০০০-এ কপি) ১. (ক) X-কে কি working set-এ ধরা হবে? (খ) X আসলে কত reference আগে ছোঁয়া হয়েছিল — কোন পরিসরে? (গ) সত্যিকারের WS(১৭,০০০, ১০,০০০)-এ X থাকবে কি না নিশ্চিত বলা যায় কি?

    (ক) হ্যাঁ — তিনটি bit-এর একটি ১, তাই নিয়ম অনুযায়ী working set-এ।

    (খ) ১০,০০০-এ কপি হওয়া bit মানে X ছোঁয়া হয়েছিল ৫,০০০ আর ১০,০০০-এর মাঝের খণ্ডে (তারপর আর নয়, কারণ নতুন দুটি bit ০)। ১৭,০০০ থেকে সেটি ৭,০০০ থেকে ১২,০০০ reference আগে।

    (গ) না। ৭,০০০ আগে হলে Δ = ১০,০০০-এর ভেতরে, আসলেই working set-এ; ১২,০০০ আগে হলে বাইরে। Bit শুধু খণ্ড বলে, খণ্ডের ভেতরের মুহূর্ত বলে না — এটাই আনুমানিক কৌশলের অনিশ্চয়তা। ১০টি bit আর প্রতি ১,০০০ reference-এ interrupt দিলে পরিসর ১,০০০-এ নামত, কিন্তু interrupt-এর খরচ পাঁচ গুণ হতো।

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

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

ক্রমপুরোনো ধাঁচের long-term scheduler-এ thrashing কীভাবে তৈরি হয় — ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. নতুন প্রসেস ঢোকায় প্রত্যেকের frame-এর ভাগ কমে যায়
  2. প্রসেসগুলো নিজেদের working set ধরে রাখতে না পেরে ঘন ঘন page fault করে
  3. প্রসেসগুলো paging-ডিস্কের লাইনে আটকে থাকে, ready queue খালি হয়ে যায়
  4. CPU utilisation নেমে যায়
  5. OS কম CPU ব্যবহার দেখে আরও প্রসেস মেমরিতে আনে

মোট ৬২টি frame, দুটি প্রসেস — একটি ১০ page-এর, আরেকটি ১২৭ page-এর। Proportional allocation-এ (নিচের দিকে গোল করে) তারা কয়টি করে frame পাবে?

  • ৩১ আর ৩১
  • ৪ আর ৫৭
  • ৫ আর ৫৮
  • ১০ আর ৫২

Reference string 2 6 1 5 7 7 7 7 5 1 6 2 3 4 1 2 3 4 4 4 3 4 3 4 4 4, Δ = ১০. শেষ reference-এর পরে (t = ২৬) working set কোনটি?

  • {১, ২, ৫, ৬, ৭}
  • {১, ২, ৩, ৪}
  • {৩, ৪}
  • {১, ২, ৩, ৪, ৫, ৬, ৭}

Thrashing চলাকালীন একটি সিস্টেমে সাধারণত কোন লক্ষণগুলো একসাথে দেখা যায়?

  • CPU ১০০% ব্যস্ত, ডিস্ক অলস
  • Page fault rate কম, CPU কম
  • খুব কম প্রসেস, সব কিছু দ্রুত
  • উঁচু page-fault rate, paging-ডিস্ক প্রায় পুরো ব্যস্ত, CPU utilisation কম

চারটি প্রসেসের working-set size ২৫, ৩০, ২০ আর ৩৫ page, মোট frame m = ১০০. Working-set নীতিতে OS কী করবে?

  • একটি প্রসেস suspend করবে, কারণ D = ১১০ > m
  • আরও একটি প্রসেস মেমরিতে আনবে
  • কিছুই করবে না, কারণ প্রত্যেকের WSS ১০০-এর কম
  • সবাইকে ২৫টি করে frame দেবে

Local replacement-এর সবচেয়ে বড় সুবিধা কোনটি?

  • মোট throughput সবসময় সবচেয়ে বেশি হয়
  • একটি প্রসেসের page-fault rate শুধু তার নিজের আচরণের ওপর নির্ভর করে, তাই গতি অনুমানযোগ্য
  • Thrashing কখনো হয় না
  • প্রত্যেক প্রসেস প্রয়োজনমতো frame বাড়িয়ে নিতে পারে

Linux সার্ভারে মেমরির চাপ মাপতে /proc/pressure/memory-এর full লাইনটি কী বোঝায়?

  • Swap কত শতাংশ ভরা
  • সময়ের যে অংশে অন্তত একটি কাজ মেমরির জন্য আটকে ছিল
  • সময়ের যে অংশে সব সক্রিয় কাজ একসাথে মেমরির জন্য আটকে ছিল
  • RAM কত শতাংশ ব্যবহৃত

শব্দকোষ

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

মনে রাখুন

  1. সীমা: সর্বনিম্ন frame = একটি instruction যত page ছুঁতে পারে (ISA ঠিক করে; IBM 370-এর MVC-তে ৬); সর্বোচ্চ = মোট frame.
  2. ভাগ: equal ⌊m/n⌋; proportional ⌊si/S × m⌋ — ৬২ frame, ১০ ও ১২৭ page → ৪ ও ৫৭, একটি খালি; priority অনুপাতে বা নিচু-priority থেকে কেড়ে। Global replacement-এ throughput বেশি, local-এ গতি অনুমানযোগ্য।
  3. Thrashing: paging-এ কাজের চেয়ে বেশি সময়; n বাড়ালে CPU utilisation প্রথমে বাড়ে, তারপর হঠাৎ ভাঙে; “CPU কম → আরও প্রসেস” নিয়ম চক্রটিকে আরও জোরে ঘোরায়।
  4. Working set: WS(t, Δ) = শেষ Δটি reference-এর আলাদা page; উদাহরণে t = ১০-এ {১, ২, ৫, ৬, ৭}, t = ২৬-এ {৩, ৪}. D = Σ WSSi > m হলে suspend. Δ বাড়ালে fault কখনো বাড়ে না। আনুমানিক রূপ: timer + reference bit; বিকল্প: PFF-এর দুই সীমা।
  5. বাস্তবে: Linux — kswapd/reclaim, swap, OOM killer, PSI (some/full), cgroup memory.high/memory.max; Windows — প্রতি-প্রসেস working set আর working set manager. ওষুধ: বেশি RAM বা কম প্রসেস।

পরের পর্বে: তত্ত্ব শেষ, এবার একটি আসল kernel. পর্ব ৩৮-এ Linux-এ মেমরি — malloc আর mmap ভেতরে কী করে, slab allocator, huge page, আর এই পর্বে নাম শোনা OOM killer কাকে, কীভাবে বেছে নেয়।

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

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