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

Multilevel queue আর multilevel feedback queue

Multilevel Queue and Multilevel Feedback Queue (MLFQ) — Rules, Worked Examples, Priority Boost and Real Schedulers

Taught in Bangla, this lesson covers multilevel queue and multilevel feedback queue scheduling: fixed classes with fixed priority or time slicing between queues, MLFQ's five parameters, the textbook three-level example (q = 8, q = 16, FCFS) solved end to end with arrivals, Rules 1 to 5 including priority boost and gaming prevention, a simulator, why MLFQ approximates SJF without knowing bursts, tuning difficulty, and where Solaris, BSD, Windows and macOS use the idea.

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

  • Multilevel queue কীভাবে কাজ করে, আর queue-গুলোর মধ্যে fixed priority ও time slicing-এর পার্থক্য — পূর্ণ সংখ্যাসহ উদাহরণে
  • MLFQ-এর পাঁচটি প্যারামিটার, আর পাঠ্যবইয়ের ৩-স্তরের উদাহরণ (q = ৮, q = ১৬, FCFS) arrival-সহ শুরু থেকে শেষ পর্যন্ত হিসাব করা
  • Rule 1 থেকে Rule 5: কেন প্রতিটি নিয়ম দরকার, starvation আর “নিয়মের ফাঁক” কোথা থেকে আসে, আর simulator দিয়ে নিজের চোখে দেখা
  • MLFQ কেন ভবিষ্যৎ না জেনেও SJF/SRTF-এর কাছাকাছি যায়, আর এর tuning কেন কঠিন
  • Solaris, BSD, Windows আর macOS-এর scheduler-এ এই ধারণা কোথায় কীভাবে দেখা যায়

১ · এক লাইনের সমস্যা — সব কাজ একরকম নয়

পর্ব ১৭ আর ১৮-তে আপনি কয়েকটি scheduling নীতি দেখেছেন। প্রতিটির একটি করে দুর্বলতা আছে। FCFS-এ একটি লম্বা কাজ সামনে থাকলে পেছনের ছোট কাজগুলো আটকে থাকে। SJF আর SRTF গড় অপেক্ষা সবচেয়ে কম রাখে, কিন্তু তার জন্য আগে থেকেই জানতে হয় কোন কাজের CPU burst কত লম্বা — যা OS সাধারণত জানে না। Round Robin সবাইকে সমান ভাগ দেয় ঠিকই, কিন্তু টাইপ করা মানুষ আর ভিডিও রেন্ডার করা প্রোগ্রামকে একই চোখে দেখে।

আপনার ল্যাপটপে এই মুহূর্তে কী চলছে ভাবুন। একটি text editor, যে বেশিরভাগ সময় কিবোর্ডের অপেক্ষায় ঘুমিয়ে থাকে, আর একটি অক্ষর এলে কয়েক মাইক্রোসেকেন্ড কাজ করে আবার ঘুমায় — এটি I/O-bound. পটভূমিতে একটি বড় প্রজেক্ট compile হচ্ছে, যে সুযোগ পেলেই একটানা CPU খায় — এটি CPU-bound. Editor-এর কাছে দরকার কম response time; compile-এর কাছে দরকার মোট কাজ বা throughput. একটিমাত্র ready queue আর একটিমাত্র নিয়ম দিয়ে দুজনকে একসাথে খুশি রাখা কঠিন।

একটি ব্যাংকের শাখার কথা ভাবুন। সব গ্রাহক যদি এক লাইনে দাঁড়ান, তাহলে দশ সেকেন্ডের একটি চেক জমা দিতে আসা মানুষকেও আধা ঘণ্টার ঋণ-আবেদনের পেছনে দাঁড়াতে হয়। তাই ব্যাংক আলাদা কাউন্টার রাখে — নগদ জমা ও উত্তোলন, বিল পরিশোধ, ঋণ ও হিসাব খোলা। প্রতিটি কাউন্টারের নিজের নিয়ম, আর কর্মকর্তা কম থাকলে কোন কাউন্টার আগে চলবে তারও একটি নিয়ম। এটিই multilevel queue-এর চিন্তা। এখন ধরুন, টোকেন দেওয়ার সময় কেউ বুঝতে পারছেন না কার কাজ কত লম্বা। তখন সবাইকে প্রথমে দ্রুত-কাউন্টারে পাঠানো হলো; যাঁর কাজ পাঁচ মিনিটে শেষ হলো না, তাঁকে পরের কাউন্টারে পাঠানো হলো। এটিই multilevel feedback queue — কাজের আচরণ দেখে লাইন বদলানো।

মূল ধারণা

এই পর্বের দুই scheduler-এর মূল পার্থক্য একটি প্রশ্নে: প্রসেস কি এক queue থেকে আরেক queue-তে যেতে পারে? Multilevel queue-তে পারে না — শ্রেণি একবার ঠিক হলে স্থায়ী। Multilevel feedback queue-তে পারে — প্রসেস কতটা CPU খাচ্ছে, সেই “feedback” দেখে তার স্তর বদলানো হয়।

২ · Multilevel queue — স্থায়ী শ্রেণি, প্রতিটির নিজের নিয়ম

Multilevel queue scheduling-এ ready queue কয়েকটি আলাদা queue-তে ভাগ থাকে, আর প্রতিটি প্রসেস তার ধরন অনুযায়ী (মেমরির আকার, অগ্রাধিকার, প্রসেসের ধরন) একটি queue-তে স্থায়ীভাবে বসানো হয়। পাঠ্যবইয়ের একটি পরিচিত চিত্রে (Silberschatz-এর বইয়ের নতুন সংস্করণে) ওপর থেকে নিচে চারটি queue: real-time, system, interactive আর batch প্রসেস; পুরোনো সংস্করণে ছিল পাঁচটি — system, interactive, interactive editing, batch আর student প্রসেস।

Queue (ওপর থেকে নিচে)কারা থাকেসাধারণ algorithmকেন
SystemKernel-এর সহায়ক কাজ, daemonFCFS বা priorityসংখ্যায় কম, ছোট আর জরুরি
Interactive (foreground)Editor, shell, ব্রাউজারের জানালাRound Robin, ছোট quantumদ্রুত সাড়া চাই
Batch (background)রাতের হিসাব, ব্যাকআপ, বড় compileFCFSসাড়া নয়, কম overhead চাই

এরপর দুটি সিদ্ধান্ত লাগে। প্রথমত, প্রতিটি queue-এর ভেতরে কোন algorithm চলবে — ওপরের টেবিলে তা আছে। দ্বিতীয়ত, queue-গুলোর মধ্যে CPU কীভাবে ভাগ হবে। এর দুটি প্রচলিত উপায়:

Fixed priority (preemptive)

ওপরের queue-তে কেউ থাকলে নিচের queue চলবেই না। নিচের কেউ চলার সময় ওপরে কেউ এলে সঙ্গে সঙ্গে CPU কেড়ে নেওয়া হয় (preemption). সরল আর দ্রুত সাড়া দেয়, কিন্তু ওপরের queue সারাক্ষণ ব্যস্ত থাকলে নিচের কাজ starvation-এ পড়ে।

ঝুঁকি: batch কাজ অনির্দিষ্টকাল অপেক্ষা

Time slicing (যেমন ৮০/২০)

প্রতিটি queue CPU-সময়ের একটি নির্দিষ্ট ভাগ পায়। যেমন foreground ৮০%, background ২০%. দুই queue-ই ব্যস্ত থাকলে প্রতি ১০ ms-এ ৮ ms foreground, ২ ms background. Background ধীরে চলে, কিন্তু থেমে থাকে না।

দাম: foreground-এর সাড়া একটু কমে

Time slicing-এর ছোট হিসাব: ধরুন foreground queue সারাক্ষণ ব্যস্ত, আর background-এ একটি ৬ ms-এর কাজ t = ০-তে এল। প্রতিটি ১০ ms চক্রের শেষ ২ ms background পায়: ৮–১০, ১৮–২০, ২৮–৩০. তিন চক্রে ৬ ms পূর্ণ হয়, কাজটি শেষ হয় t = ৩০-এ। Fixed priority হলে foreground যতক্ষণ ব্যস্ত, এই কাজ একটুও এগোত না।

সমাধান করা উদাহরণ — fixed priority, তিনটি queue

প্রশ্ন: তিনটি queue — Q0 = system (FCFS), Q1 = interactive (RR, q = ২), Q2 = batch (FCFS). Queue-গুলোর মধ্যে fixed priority, preemptive. পাঁচটি প্রসেস (ms-এ):

প্রসেসQueueArrivalBurst
P1Q2 batch০৬
P2Q1 interactive১৪
P3Q1 interactive২৩
P4Q2 batch৩৩
P5Q0 system৫২

একটি নিয়ম ঠিক করে নিই, যা বেশিরভাগ বই মানে: নিচের queue-এর কেউ preempt হলে সে নিজের queue-এর সামনেই থাকে, আর RR-এর মাঝপথে থামলে বাকি quantum নিয়ে ফেরে। (প্রশ্নে অন্য নিয়ম দেওয়া থাকলে সেটিই মানবেন।) নিচে ধাপে ধাপে দেখুন।

1 0- 1  P1 (Q2 batch)2 1- 3  P2 (Q1), P1 preempted3 3- 5  P3 (Q1)4 5- 7  P5 (Q0 system)5 7-10  P2, P3 (Q1) done610-18  P1, P4 (Q2 FCFS)
অবস্থা (ধাপের শেষে)
t
Q0 system
Q1 interactive
Q2 batch
বাকি burst P1…P5
  1. ০–১: শুধু P1 আছে। P1 batch queue-এর, কিন্তু ওপরের দুই queue খালি, তাই সে চলে। ১ ms পরে তার বাকি ৫.
  2. t = ১: P2 এল — P1 preempted. P2 interactive (Q1), যা batch-এর ওপরে। তাই P1 থেমে Q2-এর সামনে অপেক্ষা করে। P2 এক quantum (২ ms) চলে, ১–৩; এর মধ্যে t = ২-এ P3 Q1-এ এসে পেছনে দাঁড়ায়। t = ৩-এ P2-এর বাকি ২, সে Q1-এর পেছনে যায়: [P3, P2].
  3. ৩–৫: P3-এর পালা। P3 দুই ms চলে, বাকি ১. এর মধ্যে t = ৩-এ P4 (batch) এসে Q2-তে P1-এর পেছনে দাঁড়ায়: [P1, P4]. t = ৫-এ P3-এর quantum শেষ, সে Q1-এর পেছনে: [P2, P3].
  4. t = ৫: P5 (system) এল — সবার ওপরে। Q0 এখন আর খালি নয়, তাই Q1-এর কেউ নয়, P5 চলে ৫–৭ আর শেষ হয়। তার অপেক্ষা ০ — system queue-এর এটাই সুবিধা।
  5. ৭–১০: Q1 খালি করা। P2 বাকি ২ চলে ৭–৯-এ শেষ, P3 বাকি ১ চলে ৯–১০-এ শেষ। পুরো সময়টা batch queue-এর দুজন চুপচাপ বসে ছিল।
  6. ১০–১৮: অবশেষে batch. FCFS ক্রমে P1 (বাকি ৫) চলে ১০–১৫, P4 চলে ১৫–১৮. ফল: TAT = P1 ১৫, P2 ৮, P3 ৮, P4 ১৫, P5 ২ — গড় ৪৮/৫ = ৯.৬ ms; WT = ৯, ৪, ৫, ১২, ০ — গড় ৩০/৫ = ৬ ms. খেয়াল করুন, সবচেয়ে বেশি অপেক্ষা batch-এর দুজনের।

নিচের simulator একই হিসাব করে। কোনো প্রসেসের cls বা arrive বদলে আবার চালিয়ে দেখুন — যেমন P5-কে batch-এ নিলে কী হয়।

// Multilevel queue: every process is fixed to one class (queue) for life.
// Queue 0 = system (FCFS), 1 = interactive (RR, q = 2), 2 = batch (FCFS).
// Between queues: fixed priority, preemptive (a lower queue runs only if all higher are empty).
const quantum = [Infinity, 2, Infinity];
const procs = [
  { name: "P1", cls: 2, arrive: 0, burst: 6 },
  { name: "P2", cls: 1, arrive: 1, burst: 4 },
  { name: "P3", cls: 1, arrive: 2, burst: 3 },
  { name: "P4", cls: 2, arrive: 3, burst: 3 },
  { name: "P5", cls: 0, arrive: 5, burst: 2 },
];
const Q = [[], [], []], ps = procs.map(p => ({ ...p, left: p.burst, used: 0 }));
let t = 0, gantt = [];
while (ps.some(p => p.left > 0)) {
  ps.forEach(p => { if (p.arrive === t) Q[p.cls].push(p); });
  const lv = Q.findIndex(q => q.length);
  const p = lv < 0 ? null : Q[lv][0];
  const name = p ? p.name : "idle", last = gantt[gantt.length - 1];
  if (last && last.name === name) last.end = t + 1; else gantt.push({ name, start: t, end: t + 1 });
  t++;
  if (!p) continue;
  p.left--; p.used++;
  if (p.left === 0) { p.fin = t; Q[lv].shift(); }
  else if (p.used === quantum[lv]) { p.used = 0; Q[lv].push(Q[lv].shift()); } // RR: to the tail
}
console.log(gantt.map(g => g.start + "-" + g.end + " " + g.name).join(" | "));
let tat = 0, wt = 0;
for (const p of [...ps].sort((a, b) => a.name.localeCompare(b.name))) {
  tat += p.fin - p.arrive; wt += p.fin - p.arrive - p.burst;
  console.log(p.name + " queue " + p.cls + ": finish " + p.fin + ", TAT " + (p.fin - p.arrive) + ", WT " + (p.fin - p.arrive - p.burst));
}
console.log("average TAT = " + tat / ps.length + ", average WT = " + wt / ps.length);

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

0-1 P1 | 1-3 P2 | 3-5 P3 | 5-7 P5 | 7-9 P2 | 9-10 P3 | 10-15 P1 | 15-18 P4
P1 queue 2: finish 15, TAT 15, WT 9
P2 queue 1: finish 9, TAT 8, WT 4
P3 queue 1: finish 10, TAT 8, WT 5
P4 queue 2: finish 18, TAT 15, WT 12
P5 queue 0: finish 7, TAT 2, WT 0
average TAT = 9.6, average WT = 6

প্রথম লাইনটি ঠিক উপরের stepper-এর Gantt chart: P1 শুরুতে ১ ms পেয়েই থেমে যায়, তারপর ১০ ms পর্যন্ত আর ফেরে না। প্রতিটি প্রসেসের WT = TAT − burst; যেমন P4: ১৮ − ৩ − ৩ = ১২. Batch-এর দুজনের WT (৯ আর ১২) বাকিদের চেয়ে অনেক বেশি — fixed priority-র দাম এটাই। Interactive queue-তে আরও প্রসেস আসতে থাকলে এই দুজন আরও পিছিয়ে যেত; ওপরের queue কখনো খালি না হলে তারা চিরকাল অপেক্ষা করত।

Multilevel queue-এর দুটি মৌলিক দুর্বলতা। এক, শ্রেণি ভুল হলে সংশোধনের উপায় নেই — একটি “interactive” প্রোগ্রাম যদি আসলে টানা হিসাব করে, সে সারাক্ষণ ওপরের queue দখল করে রাখবে। দুই, fixed priority-তে নিচের queue-এর starvation. এই দুই সমস্যার উত্তরেই এসেছে feedback — আচরণ দেখে শ্রেণি বদলানো।

৩ · Multilevel feedback queue — ইতিহাস আর পাঁচটি প্যারামিটার

MLFQ-এর ধারণাটি প্রথম বর্ণনা করা হয় ১৯৬২ সালে, MIT-র Fernando Corbató ও তাঁর সহকর্মীদের time-sharing সিস্টেম CTSS-এর প্রবন্ধে। CTSS-এ অনেক ব্যবহারকারী টার্মিনাল থেকে একই কম্পিউটার ভাগ করতেন, আর প্রোগ্রামকে মেমরি থেকে ডিস্কে আনা-নেওয়ার (swap) খরচ ছিল বিশাল। তাই ছোট, বারবার সাড়া চাওয়া কাজকে ওপরের স্তরে ছোট quantum, আর দীর্ঘ কাজকে নিচের স্তরে বড় quantum দেওয়া হতো — বড় কাজ কম ঘন ঘন swap হতো। পরে Multics-এও এই চিন্তা এগোয়। Time-sharing আর এই সিস্টেমগুলোর কাজের জন্য Corbató ১৯৯০ সালে Turing Award পান। (CTSS-এর scheduler-এর খুঁটিনাটি নিয়ে বিভিন্ন বইয়ের বর্ণনা একটু ভিন্ন; মূল ধারণাটি নিয়ে কোনো সন্দেহ নেই।)

MLFQ-তে প্রসেস স্তর বদলায়, তাই একে বর্ণনা করতে পাঠ্যবইয়ে সাধারণত পাঁচটি প্যারামিটার বলা হয়। পরীক্ষায় “MLFQ define করুন” প্রশ্নে এই তালিকাটিই উত্তরের কাঠামো:

প্যারামিটারপ্রশ্নটিসাধারণ উত্তর
১. Queue-এর সংখ্যাকয়টি স্তর?পাঠ্যবইয়ে ৩টি; Solaris-এর TS-এ ৬০টি priority
২. প্রতিটি queue-এর algorithmএকই স্তরে কে আগে?ওপরে RR, সবার নিচে FCFS বা বড় quantum-এর RR
৩. প্রতিটি স্তরের quantumএক পালায় কত সময়?ওপরে ছোট, নিচে ক্রমে বড় (যেমন ৮ → ১৬ → FCFS)
৪. Promotion আর demotion-এর নিয়মকখন নিচে নামবে, কখন ওপরে উঠবে?পুরো বরাদ্দ খেলে নামা; অপেক্ষা বা boost-এ ওঠা (aging)
৫. প্রবেশের queueনতুন প্রসেস কোথায় ঢোকে?সাধারণত সবার ওপরে

সংজ্ঞাটি তাই এক বাক্যে বলা যায়: MLFQ হলো কয়েকটি priority-স্তরের queue, যেখানে প্রসেসের সাম্প্রতিক CPU-ব্যবহার দেখে তার স্তর বদলানো হয়, আর সবসময় সবচেয়ে উঁচু অ-খালি স্তরের প্রসেস চলে। এই পাঁচটি প্যারামিটার বদলে প্রায় যেকোনো scheduler-এর মতো আচরণ পাওয়া যায় — একটিমাত্র স্তর আর বড় quantum দিলে সেটি FCFS, একটিমাত্র স্তর আর ছোট quantum দিলে Round Robin. এই নমনীয়তাই MLFQ-এর শক্তি, আবার tuning-এর মাথাব্যথাও (অংশ ৮)।

৪ · পাঠ্যবইয়ের ৩-স্তরের উদাহরণ — q = ৮, q = ১৬, FCFS

বিশ্ববিদ্যালয় আর চাকরির পরীক্ষায় সবচেয়ে বেশি আসা MLFQ-এর রূপটি এই:

Q0: RR, q = ৮ ms → Q1: RR, q = ১৬ ms → Q2: FCFS
নতুন প্রসেস Q0-তে ঢোকে · পুরো quantum খেলে এক স্তর নিচে · ওপরের queue-তে কেউ এলে নিচের জন preempted

অর্থাৎ যে কাজের burst ৮ ms বা কম, সে সবচেয়ে বেশি অগ্রাধিকার পায় আর দ্রুত শেষ হয়। ৮ থেকে ২৪ ms-এর কাজ Q1 থেকে বাকি সময় পায়। এর চেয়ে লম্বা কাজ Q2-তে নেমে যায়, আর ওপরের দুই queue খালি থাকলে তবেই চলে। এবার একটি পূর্ণ উদাহরণ।

প্রশ্ন: চারটি প্রসেস — P1 (arrival ০, burst ৪০), P2 (২, ৬), P3 (৪, ১০), P4 (৪৪, ৫). প্রতিটির completion, turnaround time (TAT) আর waiting time (WT) বের করুন। নিয়ম: preempt হওয়া প্রসেস নিজের স্তরেই থাকে আর সেই queue-এর সামনে থেকে আবার চলে।

1 0- 8  P1@Q0 (uses all 8)2 8-14  P2@Q0 (done in 6)314-22  P3@Q0 (uses all 8)422-38  P1@Q1 (uses all 16)538-40  P3@Q1 (done)640-44  P1@Q2 (FCFS)744-49  P4@Q0 preempts P1849-61  P1@Q2 (done)
অবস্থা (ধাপের শেষে)
t
P1-এর স্তর
Q0
Q1
Q2
বাকি P1/P2/P3/P4
  1. ০–৮: P1 Q0-তে পুরো quantum খায়। P1 একা এসেছে, তাই চলে। এর মধ্যে P2 (t = ২) আর P3 (t = ৪) Q0-তে এসে দাঁড়ায়, কিন্তু একই স্তরে RR — P1-কে থামানো হয় না। t = ৮-এ P1 পুরো ৮ ms খেয়েছে, তাই এক স্তর নিচে, Q1-এ। বাকি ৩২.
  2. ৮–১৪: P2 quantum শেষের আগেই শেষ। P2-এর burst ৬ < ৮, তাই সে Q0-তেই কাজ শেষ করে, t = ১৪. ছোট কাজের জন্য MLFQ ঠিক এটাই চায় — নামার আগেই বিদায়।
  3. ১৪–২২: P3 Q0-তে পুরো ৮ খায়। P3-এর burst ১০, তাই ৮ পরে বাকি ২, আর সে Q1-এ নামে — P1-এর পেছনে: Q1 = [P1, P3].
  4. ২২–৩৮: Q0 খালি, এবার Q1. Q1-এর সামনে P1, সে পুরো ১৬ ms চলে (এর মধ্যে Q0-তে কেউ আসেনি)। P1 পুরো quantum খেয়েছে, তাই আবার নিচে — Q2. বাকি ৩২ − ১৬ = ১৬.
  5. ৩৮–৪০: P3-এর বাকি ২ ms. P3 Q1-এ বাকি ২ চলে শেষ, t = ৪০. দেখুন, মাত্র ২ ms বাকি থাকা P3-কে P1-এর ১৬ ms-এর পেছনে বসে থাকতে হলো — MLFQ নিখুঁত SRTF নয়।
  6. ৪০–৪৪: P1 এখন Q2-তে, FCFS. ওপরের দুই queue খালি, তাই P1 চলে। Q2-তে quantum নেই — কেউ না এলে সে শেষ পর্যন্ত চলত।
  7. t = ৪৪: P4 এল — P1 preempted. নতুন প্রসেস Q0-তে ঢোকে, আর Q0 > Q2, তাই P1 থামে (বাকি ১২)। P4-এর burst ৫ < ৮, সে ৪৪–৪৯-এ শেষ। অপেক্ষা শূন্য।
  8. ৪৯–৬১: P1 শেষ। ফল — completion: P1 ৬১, P2 ১৪, P3 ৪০, P4 ৪৯. TAT: ৬১, ১২, ৩৬, ৫ → গড় ১১৪/৪ = ২৮.৫ ms. WT = TAT − burst: ২১, ৬, ২৬, ০ → গড় ৫৩/৪ = ১৩.২৫ ms.

একই প্রশ্ন simulator দিয়ে মিলিয়ে নিন। প্রসেসের তালিকা বদলে নিজের বইয়ের অনুশীলনীও যাচাই করতে পারবেন — কিন্তু আগে হাতে করুন, তারপর মেলান।

// Textbook MLFQ: Q0 = RR q 8, Q1 = RR q 16, Q2 = FCFS. Every new process enters Q0.
// Uses the full quantum -> moves one level down. A higher queue preempts a lower one;
// the preempted process stays at the head of its own queue.
const quantum = [8, 16, Infinity];
const procs = [
  { name: "P1", arrive: 0, burst: 40 },
  { name: "P2", arrive: 2, burst: 6 },
  { name: "P3", arrive: 4, burst: 10 },
  { name: "P4", arrive: 44, burst: 5 },
];
const Q = [[], [], []], ps = procs.map(p => ({ ...p, left: p.burst, used: 0 }));
let t = 0, gantt = [];
while (ps.some(p => p.left > 0)) {
  ps.forEach(p => { if (p.arrive === t) Q[0].push(p); });
  const lv = Q.findIndex(q => q.length);
  const p = lv < 0 ? null : Q[lv][0];
  const name = p ? p.name + "@Q" + lv : "idle", last = gantt[gantt.length - 1];
  if (last && last.name === name) last.end = t + 1; else gantt.push({ name, start: t, end: t + 1 });
  t++;
  if (!p) continue;
  p.left--; p.used++;
  if (p.left === 0) { p.fin = t; Q[lv].shift(); }
  else if (p.used === quantum[lv]) { p.used = 0; Q[lv].shift(); Q[lv + 1].push(p); } // demote
}
gantt.forEach(g => console.log(String(g.start).padStart(2) + "-" + String(g.end).padEnd(3) + g.name));
let tat = 0, wt = 0;
for (const p of ps) {
  tat += p.fin - p.arrive; wt += p.fin - p.arrive - p.burst;
  console.log(p.name + ": finish " + p.fin + ", TAT " + (p.fin - p.arrive) + ", WT " + (p.fin - p.arrive - p.burst));
}
console.log("average TAT = " + tat / ps.length + ", average WT = " + wt / ps.length);

আউটপুট:

 0-8  P1@Q0
 8-14 P2@Q0
14-22 P3@Q0
22-38 P1@Q1
38-40 P3@Q1
40-44 P1@Q2
44-49 P4@Q0
49-61 P1@Q2
P1: finish 61, TAT 61, WT 21
P2: finish 14, TAT 12, WT 6
P3: finish 40, TAT 36, WT 26
P4: finish 49, TAT 5, WT 0
average TAT = 28.5, average WT = 13.25

প্রতিটি লাইন একটি Gantt-টুকরো আর সেটি কোন স্তরে চলেছে: P1 তিনটি স্তরই ঘুরেছে (@Q0 → @Q1 → @Q2), আর ৪৪-এ P4-এর জন্য থেমে ৪৯-এ আবার Q2-তে ফিরেছে। P2 আর P4 কখনো Q0 ছাড়েনি। গড় TAT ২৮.৫ আর গড় WT ১৩.২৫ — stepper-এর হাতের হিসাবের সাথে হুবহু মেলে।

অন্য নীতির সাথে তুলনা

একই চারটি প্রসেস FCFS আর SRTF-এ (পর্ব ১৭) হাতে হিসাব করলে:

নীতিCompletion P1/P2/P3/P4গড় TATগড় WTburst জানা লাগে?
FCFS৪০ / ৪৬ / ৫৬ / ৬১৩৮.২৫২৩না
MLFQ (৮, ১৬, FCFS)৬১ / ১৪ / ৪০ / ৪৯২৮.৫১৩.২৫না
SRTF৬১ / ৮ / ১৮ / ৪৯২১.৫৬.২৫হ্যাঁ

SRTF সবচেয়ে ভালো, কারণ সে প্রতিটি burst আগে থেকে “জানে”। FCFS সবচেয়ে খারাপ, কারণ ৪০ ms-এর P1 সামনে থাকায় সবাই আটকে থাকে। MLFQ কিছুই না জেনে মাঝামাঝি — FCFS-এর চেয়ে গড় অপেক্ষা প্রায় অর্ধেক। ছোট P2 আর P4 MLFQ-তে প্রায় SRTF-এর মতোই দ্রুত সাড়া পেয়েছে (P2-এর প্রথম সাড়া t = ৮-এ, P4-এর সঙ্গে সঙ্গে)। এই “না জেনেও কাছাকাছি” কেন হয়, তা অংশ ৭-এ দেখব।

৫ · নিয়মগুলো এক এক করে — Rule 1 থেকে Rule 4

পাঠ্যবইয়ের উদাহরণে I/O ছিল না — সব প্রসেস টানা CPU চেয়েছে। বাস্তবে interactive প্রসেস একটু চলে, তারপর I/O-র জন্য ঘুমায়। এই অবস্থায় MLFQ কীভাবে আচরণ করবে, তা পরিষ্কার নিয়মে লিখেছেন Remzi ও Andrea Arpaci-Dusseau তাঁদের বিনামূল্যের বই Operating Systems: Three Easy Pieces (OSTEP)-এ। নিয়মগুলো ধাপে ধাপে গড়ে উঠেছে — প্রতিটি নতুন নিয়ম আগেরটির একটি সমস্যা সারায়।

নিয়মবক্তব্যকেন
Rule 1A-এর priority B-এর চেয়ে বেশি হলে A চলবে, B নয়স্তরের অর্থই অগ্রাধিকার
Rule 2A আর B-এর priority সমান হলে তারা সেই স্তরের quantum ধরে Round Robin-এ চলবেএকই স্তরে ন্যায্য ভাগ
Rule 3নতুন কাজ সবার ওপরের স্তরে ঢোকেপ্রথমে ধরে নেওয়া হয় কাজটি ছোট
Rule 4 (পুরোনো রূপ)৪ক: পুরো time slice খেলে এক স্তর নিচে; ৪খ: slice শেষের আগে CPU ছাড়লে একই স্তরে থাকেInteractive কাজ ওপরে থাকুক
Rule 4 (নতুন রূপ)একটি স্তরে মোট বরাদ্দ (allotment) খরচ হলেই নিচে নামে — মাঝে কতবার CPU ছেড়েছে তাতে কিছু যায় আসে নানিয়মের ফাঁক বন্ধ করা (নিচে দেখুন)
Rule 5প্রতি S সময় পরপর সব কাজকে সবার ওপরের স্তরে তোলাStarvation ঠেকানো, আচরণ-বদল ধরা (অংশ ৬)

Rule 3 আর পুরোনো Rule 4-এর জুটি দেখতে কেমন? ধরুন quantum Q0 = ৪, Q1 = ৮, Q2 = ১৬ ms. A একটি দীর্ঘ CPU-bound কাজ (৪০ ms), t = ০-তে আসে। B একটি interactive কাজ — t = ১২-তে আসে, প্রতিবার ১ ms চলে, তারপর ৩ ms I/O-তে ঘুমায়; মোট ৬ বার। নিচে দেখুন B কীভাবে ওপরে থেকে যায়।

1 0-12  A sinks: Q0 (4) -> Q1 (8) -> Q2212-13  B@Q0 runs 1, starts I/O313-16  A@Q2 runs while B waits for I/O416-17  B back: preempts A, runs 1517-46  same rhythm; B done 33, A done 46
অবস্থা (ধাপের শেষে)
t
A-এর স্তর
B-এর স্তর
B-এর ব্যবহার (পুরোনো Rule 4)
B-এর ব্যবহার (নতুন Rule 4)
  1. ০–১২: A নিচে নামে। A একা, Q0-তে পুরো ৪ ms খায় → Q1; সেখানে পুরো ৮ ms খায় → Q2. দীর্ঘ CPU-bound কাজের স্বাভাবিক পরিণতি: সবচেয়ে নিচের স্তর।
  2. ১২–১৩: B এল Q0-তে (Rule 3) আর সঙ্গে সঙ্গে চলল। Q0 > Q2, তাই B তখনই CPU পায় (Rule 1). ১ ms চলে সে I/O-তে যায় — quantum ৪-এর মাত্র ১ ব্যবহার করেছে। পুরোনো Rule ৪খ অনুযায়ী সে Q0-তেই থাকে, আর তার ব্যবহার-গণনা আবার ০ থেকে শুরু।
  3. ১৩–১৬: B ঘুমিয়ে, A কাজ করে। CPU খালি পড়ে থাকে না — নিচের স্তরের A এই ৩ ms পায়। Interactive আর CPU-bound কাজ মেশানোয় CPU-র পূর্ণ ব্যবহার হয়।
  4. t = ১৬: B ফিরল — A preempted. B-এর I/O শেষ, সে Q0-তে ফেরে, আর উঁচু স্তর বলে A-কে সঙ্গে সঙ্গে সরিয়ে দেয়। B-এর অপেক্ষা ০ ms — এটাই interactive কাজের জন্য ভালো response time.
  5. ১৭–৪৬: একই ছন্দ। B প্রতি ৪ ms-এ ১ ms চলে, t = ৩৩-এ ছয় নম্বর burst শেষে বিদায়; বাকি সময়টা A পায়, A শেষ t = ৪৬-এ। B একবারও অপেক্ষা করেনি। নতুন Rule 4-এ B-এর ব্যবহার জমতে থাকে — ১, ২, ৩, ৪ — তাই t = ২৪-এর burst-এর পর সে Q1-এ নামে; তবু A (Q2)-এর ওপরে থাকায় এই উদাহরণে ফল একই (B শেষ ৩৩, A শেষ ৪৬)।

নিয়মের ফাঁক — quantum শেষের ঠিক আগে CPU ছেড়ে দেওয়া

পুরোনো Rule ৪খ একটি ন্যায্যতার ফাঁক রেখে দেয়, যাকে OSTEP বলে “gaming the scheduler”. একটি প্রোগ্রাম যদি প্রতিবার quantum-এর ৯৯% ব্যবহার করে তারপর একটি তুচ্ছ I/O (যেমন একটি ছোট ফাইল-লেখা) করে, তাহলে নিয়ম অনুযায়ী সে কখনোই “পুরো slice” খায়নি — তাই চিরকাল সবার ওপরের স্তরে থাকে, আর বাকি সবার চেয়ে অনেক বেশি CPU পায়। এটি কোনো ভাঙন নয়, নিয়মটিরই দুর্বলতা; একটি প্রোগ্রাম ইচ্ছা করে না করেও (যেমন অনেক ছোট লেখা করে এমন সার্ভার) এই আচরণে পড়তে পারে। আর shared সার্ভারে এক ব্যবহারকারীর এমন প্রোগ্রাম অন্যদের ধীর করে দেয়। তাই scheduler-এর নকশাকারীরা প্রশ্নটিকে হিসাব রাখার (accounting) সমস্যা হিসেবে দেখেন: একটি স্তরে প্রসেস মোট কত CPU পেল, সেটিই গুনতে হবে।

নতুন Rule 4 ঠিক তাই করে — প্রতিটি স্তরে একটি time allotment থাকে, আর CPU ছেড়ে দিলেও ব্যবহৃত সময় মুছে যায় না। নিচের ছোট পরীক্ষায় A একটি সাধারণ CPU-bound কাজ, আর G প্রতিবার ৯ ms চলে ১ ms-এর I/O করে (quantum ১০)। দুই ধরনের হিসাবে ৫০০ ms-এ কে কত CPU পেল:

// Fairness check: job G gives up the CPU just before its slice ends (runs 9 ms, 1 ms I/O).
// Compare per-slice accounting (old rule) with allotment accounting (new Rule 4).
function share(keepUsage, horizon) {
  const quanta = [10, 20, 40], Q = [[], [], []];
  const js = [{ name: "A", run: 0 }, { name: "G", run: 9, io: 1 }]
    .map(j => ({ ...j, lvl: 0, used: 0, burst: 0, readyAt: 0, got: 0 }));
  for (let t = 0; t < horizon; t++) {
    js.forEach(j => { if (j.readyAt === t) Q[j.lvl].push(j); });
    const lv = Q.findIndex(q => q.length);
    if (lv < 0) continue;
    const j = Q[lv][0];
    j.got++; j.used++; j.burst++;
    const io = j.run && j.burst === j.run;
    if (io) { Q[lv].shift(); j.readyAt = t + 1 + j.io; j.burst = 0; if (!keepUsage) j.used = 0; }
    if (j.used >= quanta[lv]) {
      if (!io) Q[lv].shift();
      j.used = 0; j.lvl = Math.min(lv + 1, 2);
      if (!io) Q[j.lvl].push(j);
    }
  }
  return js.map(j => j.name + " got " + j.got + " ms (" + (100 * j.got / horizon).toFixed(0) + "%), now at Q" + j.lvl).join(", ");
}
console.log("per-slice accounting:  " + share(false, 500));
console.log("allotment accounting:  " + share(true, 500));

আউটপুট:

per-slice accounting:  A got 59 ms (12%), now at Q2, G got 441 ms (88%), now at Q0
allotment accounting:  A got 401 ms (80%), now at Q2, G got 99 ms (20%), now at Q2

Per-slice হিসাবে G প্রতিবার ৯ ms চলে slice শেষের আগে থামে, তাই তার গণনা প্রতিবার ০-তে ফেরে, আর সে চিরকাল Q0-তে — ৫০০ ms-এর ৪৪১ ms (৮৮%) তার। A নিচে বসে শুধু G-এর ১ ms I/O-র ফাঁকগুলো পায়: ৫৯ ms. Allotment হিসাবে G-এর ৯ + ১ ms জমে ১০ পূর্ণ হতেই সে নামে, শেষে দুজনই Q2-তে। সেখানে Rule 2 অনুযায়ী Round Robin, আর G নিজে থেকে বারবার CPU ছাড়ায় এবার উল্টো A বেশি পায় (৮০%). মূল কথা: কেউ আর আচরণের কৌশলে অন্যের ভাগ দখল করতে পারে না; কে কত পায় তা নির্ভর করে আসল CPU-চাহিদায়।

৬ · Rule 5 — priority boost, starvation-এর ওষুধ

Rule 1–4 মিলে আরেকটি সমস্যা থেকে যায়। ধরুন সিস্টেমে কয়েকটি interactive কাজ আছে, যারা পালা করে CPU চায় — একজন ঘুমালে আরেকজন জাগে। ওপরের স্তর কখনো খালি হয় না, তাই নিচের স্তরের দীর্ঘ কাজ একটুও CPU পায় না: starvation. আরেকটি সমস্যা হলো আচরণ-বদল: একটি প্রোগ্রাম শুরুতে টানা হিসাব করে নিচে নেমে গেল, পরে সে interactive হয়ে উঠল (যেমন একটি ছবি-সম্পাদক ফাইল খোলা শেষে ব্যবহারকারীর ক্লিকের অপেক্ষায়)। Rule 1–4-এ সে আর কখনো ওপরে উঠতে পারে না।

OSTEP-এর সমাধান সরল: Rule 5 — প্রতি S সময় পরপর সব কাজকে সবার ওপরের স্তরে তোলা। এতে দুটি সমস্যাই মেটে: নিচের কাজ অন্তত প্রতি S সময়ে একবার ওপরে উঠে CPU পায়, আর আচরণ বদলানো কাজ নতুন করে নিজের জায়গা খুঁজে নেয়। পাঠ্যবইয়ে একই চিন্তাকে বলা হয় aging — যে যত বেশি অপেক্ষা করেছে, তার অগ্রাধিকার তত বাড়ানো (পর্ব ১৮)।

নিচের simulator তিনটি স্তর (quantum ২, ৪, ৮ ms) নিয়ে চলে। A একটি দীর্ঘ CPU-bound কাজ (২০ ms), t = ০-তে আসে। B আর C দুটি interactive কাজ, t = ৬-এ আসে; প্রত্যেকে ১ ms চলে, ১ ms I/O করে, মোট ২০ ms CPU চায়। যেহেতু তারা পালা করে চলে, Q0 কখনো খালি হয় না। একবার boost ছাড়া, একবার প্রতি ১০ ms-এ boost দিয়ে চালানো হলো। আউটপুটে প্রতিটি লাইন একটি স্তর; অক্ষর মানে সেই ms-এ সেই স্তরে কে চলেছে, আর longest wait মানে প্রস্তুত থেকেও টানা কত ms CPU পায়নি।

// MLFQ simulator, 1 tick = 1 ms. quanta[i] = time slice (and allotment) of level i.
// keepUsage = false: usage resets when a job gives up the CPU (old per-slice rule).
function mlfq(jobs, quanta, S, keepUsage) {
  const Q = quanta.map(() => []), rows = quanta.map(() => "");
  const js = jobs.map(j => ({ ...j, left: j.cpu, lvl: 0, used: 0, burst: 0,
                              readyAt: j.arrive, fin: 0, gap: 0, maxGap: 0 }));
  for (let t = 0; js.some(j => !j.fin); t++) {
    if (S && t && t % S === 0) {                         // Rule 5: priority boost
      Q[0] = [].concat(...Q); for (let i = 1; i < Q.length; i++) Q[i] = [];
      js.forEach(j => { j.lvl = 0; j.used = 0; });
    }
    js.forEach(j => { if (!j.fin && j.readyAt === t) Q[j.lvl].push(j); });
    const lv = Q.findIndex(q => q.length), j = lv < 0 ? null : Q[lv][0];
    rows.forEach((r, i) => rows[i] += (i === lv ? j.name : "."));
    js.forEach(o => { if (o !== j && !o.fin && o.readyAt <= t) o.maxGap = Math.max(o.maxGap, ++o.gap); });
    if (!j) continue;
    j.gap = 0; j.left--; j.used++; j.burst++;
    if (!j.left) { j.fin = t + 1; Q[lv].shift(); continue; }
    const io = j.run && j.burst === j.run;               // job releases the CPU for I/O
    if (io) { Q[lv].shift(); j.readyAt = t + 1 + j.io; j.burst = 0; if (!keepUsage) j.used = 0; }
    if (j.used >= quanta[lv]) {                          // Rule 4: demote one level
      if (!io) Q[lv].shift();
      j.used = 0; j.lvl = Math.min(lv + 1, quanta.length - 1);
      if (!io) Q[j.lvl].push(j);
    }
  }
  for (let c = 0; c < rows[0].length; c += 30)
    rows.forEach((r, i) => console.log("Q" + i + " t=" + String(c).padStart(2) + " " + r.slice(c, c + 30)));
  js.forEach(j => console.log(j.name + ": finish " + j.fin + ", turnaround " + (j.fin - j.arrive) + ", longest wait " + j.maxGap));
}
const jobs = [{ name: "A", arrive: 0, cpu: 20 },                 // long, CPU-bound
              { name: "B", arrive: 6, cpu: 20, run: 1, io: 1 },  // interactive
              { name: "C", arrive: 6, cpu: 20, run: 1, io: 1 }]; // interactive
console.log("--- no boost");          mlfq(jobs, [2, 4, 8], 0, false);
console.log("--- boost every 10 ms"); mlfq(jobs, [2, 4, 8], 10, false);

আউটপুট:

--- no boost
Q0 t= 0 AA....BCBCBCBCBCBCBCBCBCBCBCBC
Q1 t= 0 ..AAAA........................
Q2 t= 0 ..............................
Q0 t=30 BCBCBCBCBCBCBCBC..............
Q1 t=30 ..............................
Q2 t=30 ................AAAAAAAAAAAAAA
A: finish 60, turnaround 60, longest wait 40
B: finish 45, turnaround 39, longest wait 0
C: finish 46, turnaround 40, longest wait 1
--- boost every 10 ms
Q0 t= 0 AA....BCBCAABCBCBCBCAABCBCBCBC
Q1 t= 0 ..AAAA........................
Q2 t= 0 ..............................
Q0 t=30 AABCBCBCBCAABCBCBCBCAABCBC....
Q1 t=30 ..........................AAAA
Q2 t=30 ..............................
A: finish 60, turnaround 60, longest wait 8
B: finish 55, turnaround 49, longest wait 2
C: finish 56, turnaround 50, longest wait 2

Boost ছাড়া: A প্রথম ৬ ms-এ Q0 → Q1 → Q2-তে নেমে যায়। তারপর t = ৬ থেকে ৪৬ পর্যন্ত Q0-তে শুধু BCBC… — A টানা ৪০ ms অপেক্ষা করে (longest wait 40), আর B, C শেষ হলে তবেই চলে। B আর C যদি কখনো শেষ না হতো (যেমন সারাদিন চালু থাকা দুটি সার্ভিস), A চিরকাল অপেক্ষা করত — এটাই starvation. প্রতি ১০ ms-এ boost: t = ১০, ২০, ৩০, ৪০, ৫০-এ A Q0-তে ওঠে, ২ ms চলে আবার নামে (AA দেখুন)। A-এর টানা অপেক্ষা ৪০ থেকে নেমে ৮ ms. দামটাও দেখুন: B আর C-এর সমাপ্তি ৪৫/৪৬ থেকে পিছিয়ে ৫৫/৫৬ — boost-এর সময় তাদের একটু অপেক্ষা করতে হয় (longest wait ২)। A-এর সমাপ্তি দুবারই ৬০, কারণ মোট কাজ একই আর CPU কখনো খালি থাকেনি; boost মোট গতি বাড়ায় না, অপেক্ষার সর্বোচ্চ সীমা বেঁধে দেয়।

1 0- 6  A alone: Q0 (2) -> Q1 (4) -> Q22 6-10  B, C alternate at Q0; A starves310-12  BOOST: all to Q0; A runs 2412-20  A back at Q1, waits 8520-52  boost at 20, 30, 40, 50652-60  B, C finish; A finishes
A-এর অবস্থা (ধাপের শেষে)
সময়
A-এর স্তর
A-এর কাজ হয়েছে
A-এর এই দফার অপেক্ষা
  1. ০–৬: A একা নিচে নামে। Q0-তে ২ ms (পুরো quantum) → Q1; সেখানে ৪ ms → Q2. ২০-এর মধ্যে ৬ ms কাজ শেষ।
  2. ৬–১০: B আর C এল, A অনাহারে। B চলে, I/O-তে যায়; তখন C চলে; তারপর B ফেরে… Q0 সারাক্ষণ ভরা। A প্রস্তুত, কিন্তু Q2-তে — ৪ ms ধরে অপেক্ষা চলছে, আর boost না থাকলে চলত t = ৪৬ পর্যন্ত।
  3. t = ১০: boost! S = ১০, তাই সব কাজ Q0-তে, সবার ব্যবহার-গণনা ০. সেই মুহূর্তে Q0-তে প্রস্তুত শুধু A (B এইমাত্র ফিরছে, তার পেছনে দাঁড়ায়)। A ১০–১২ চলে নিজের ২ ms quantum পূর্ণ করে, আর আবার Q1-এ নামে।
  4. ১২–২০: আবার অপেক্ষা, কিন্তু সীমিত। B আর C আবার পালা করে Q0 দখল করে। A Q1-এ ৮ ms অপেক্ষা করে — এটিই তার সবচেয়ে লম্বা টানা অপেক্ষা। কারণ পরের boost নিশ্চিত, t = ২০-এ।
  5. ২০–৫২: প্রতি ১০ ms-এ একই গল্প। t = ২০, ৩০, ৪০, ৫০-এ A ওপরে উঠে ২ ms করে পায়। ৫২-র মধ্যে A-এর ১৬ ms কাজ শেষ। ১০ ms-এ ২ ms, অর্থাৎ ২০% — বেশি নয়, কিন্তু নিশ্চিত।
  6. ৫২–৬০: সবার বিদায়। B শেষ ৫৫-এ, C ৫৬-এ; তারপর A Q1-এ বাকি ৪ ms চলে t = ৬০-এ শেষ। Boost ছাড়া A-এর টানা অপেক্ষা ছিল ৪০ ms, boost-সহ ৮ ms. S ছোট করলে A আরও ঘন ঘন পায়, কিন্তু interactive কাজের সাড়া তত ধীর হয় — S বাছাই একটি দর-কষাকষি।

প্রমাণের স্বাদ — boost কেন starvation অসম্ভব করে: ধরুন সিস্টেমে কখনো n টির বেশি কাজ একসাথে প্রস্তুত থাকে না, আর সবার ওপরের স্তরের quantum q0. প্রতিটি boost-এর পর সবাই Q0-তে, আর Q0-তে Round Robin. তাই boost-এর পর কোনো কাজকে Q0-তে প্রথম পালা পেতে সর্বোচ্চ বাকি (n − 1) জনের এক quantum করে, অর্থাৎ (n − 1)·q0 অপেক্ষা করতে হয়। S যদি (n − 1)·q0-এর চেয়ে বড় হয়, তাহলে প্রতিটি S-দৈর্ঘ্যের দফায় প্রতিটি প্রস্তুত কাজ অন্তত একবার CPU পায়। অর্থাৎ যেকোনো কাজের টানা অপেক্ষা মোটামুটি S + (n − 1)·q0-এর বেশি হতে পারে না — সীমাহীন অপেক্ষা (starvation) অসম্ভব। Rule 5 ছাড়া এমন কোনো সীমা নেই, কারণ ওপরের স্তর কখনো খালি না হওয়ার সম্ভাবনা থেকেই যায়।

৭ · না জেনেও SJF-এর কাছাকাছি — MLFQ কেন কাজ করে

পর্ব ১৭-এ দেখেছেন, সব কাজ একসাথে এলে SJF গড় waiting time-কে সর্বনিম্ন রাখে, আর preemptive রূপ SRTF-ও তার অনুরূপ। সমস্যা হলো burst জানা লাগে। পাঠ্যবইয়ের সমাধান ছিল আগের burst-গুলোর exponential average দিয়ে অনুমান। MLFQ আরেকটি চতুর পথ নেয়: অনুমান না করে, পর্যবেক্ষণ করে।

  • শুরুতে সবাইকে ছোট ধরা: নতুন কাজ ওপরে ঢোকে (Rule 3), তাই কাজটি সত্যিই ছোট হলে সে প্রায় SJF-এর মতোই দ্রুত শেষ হয় — উদাহরণের P2 আর P4 যেমন।
  • যত চলে, তত নামে: কেউ q0 খেয়ে ফেললে আমরা জানি তার burst অন্তত q0. q0 + q1 খেলে জানি অন্তত তত। অর্থাৎ স্তরটিই হলো “এ পর্যন্ত কত CPU পেয়েছে”-এর মোটা দাগের মাপ, আর ওপরের স্তর মানে কম পেয়েছে।
  • সবচেয়ে কম পাওয়াকে আগে চালানো: তাই MLFQ মোটামুটি এই নিয়মে চলে — “যে এ পর্যন্ত সবচেয়ে কম CPU পেয়েছে, সে আগে।” এই নীতির নাম least attained service (LAS) বা foreground-background scheduling; MLFQ এর ধাপে-ধাপে (quantum-এর মাপে) রূপ।

কখন এটি SJF-এর মতো ভালো হয়? একটি যুক্তি দেখুন। ধরুন সব কাজ t = ০-তে এল, I/O নেই। MLFQ-তে ওপরের স্তরের সবাই একবার করে q0 পর্যন্ত চলে। যে কাজের burst ≤ q0, সে প্রথম চক্রেই শেষ — কোনো দীর্ঘ কাজ q0-এর বেশি পাওয়ার আগেই। অর্থাৎ প্রতিটি ছোট কাজ প্রতিটি বড় কাজের আগে শেষ হয়, ঠিক SJF-এর মতো; পার্থক্য শুধু এটুকু যে বড় কাজগুলোও প্রথম চক্রে q0 করে পায়, যা ছোটদের অপেক্ষা সামান্য বাড়ায়। স্তর যত বেশি আর quantum-এর ধাপ যত সূক্ষ্ম, ক্রম তত SJF-এর কাছাকাছি। উল্টো দিকে, burst যদি কাছাকাছি হয় (যেমন সবার ১০ ms), MLFQ সবাইকে টুকরো টুকরো করে Round Robin-এর মতো আচরণ করে — তখন FCFS-ই বরং ভালো।

বাস্তবে এটি কেন ভালো কাজ করে, তার একটি পর্যবেক্ষণভিত্তিক কারণ আছে: অনেক সিস্টেমে কাজের দৈর্ঘ্যের বণ্টন “ভারী লেজওয়ালা” — বেশিরভাগ কাজ খুব ছোট, অল্প কয়েকটি বিশাল। এমন বণ্টনে “যে অনেকক্ষণ চলেছে, সে সম্ভবত আরও অনেকক্ষণ চলবে” — তাই যে কম পেয়েছে তাকে আগে চালানো প্রায়ই সঠিক বাজি। অবশ্য সব workload-এ এটি খাটে না।

আরও গভীরে: Queueing তত্ত্বে প্রমাণিত যে burst জানা থাকলে SRPT (shortest remaining processing time) গড় response time-কে সর্বনিম্ন রাখে, আর burst অজানা হলে কাজের দৈর্ঘ্যের বণ্টনের ওপর নির্ভর করে সর্বোত্তম নীতি হলো Gittins index নীতি — যার একটি বিশেষ রূপ হিসেবে LAS আসে, যখন বণ্টনের “hazard rate” কমতে থাকে। ২০০০-এর দশকে নেটওয়ার্ক রাউটার আর ওয়েব সার্ভারে LAS ও SRPT-ভিত্তিক scheduling নিয়ে বেশ কিছু গবেষণা হয়েছে, যেখানে ছোট অনুরোধগুলোর response অনেক কমতে দেখা গেছে। MLFQ তাই কেবল একটি “চতুর কৌশল” নয় — এটি একটি তাত্ত্বিকভাবে অর্থবহ নীতির সহজ, হার্ডওয়্যার-বান্ধব অনুকরণ।

৮ · Tuning-এর মাথাব্যথা আর বাস্তব OS

MLFQ-এর প্রশ্নগুলোর কোনো একক সঠিক উত্তর নেই: কয়টি স্তর? প্রতিটির quantum কত? Allotment কত? S কত ঘন ঘন? অনুশীলনীতে দেখবেন, S = ৫ করলে দীর্ঘ কাজ খুব ভালো পায়, কিন্তু interactive কাজ অনেক পিছিয়ে যায়; S = ১০০ হলে উল্টো। OSTEP জানায়, বিখ্যাত systems গবেষক John Ousterhout এমন মানগুলোকে বলতেন “voo-doo constants” — যেন ঠিক করতে কালো জাদু লাগে। OSTEP সেখান থেকে একটি পরামর্শও দেয়, যাকে বইটি “Ousterhout's Law” বলে: সম্ভব হলে এমন জাদুর সংখ্যা এড়িয়ে চলুন।

এবার দেখা যাক বাস্তব OS এই ধারণা কোথায় কীভাবে ব্যবহার করেছে। মনে রাখবেন, প্রতিটি সিস্টেমের খুঁটিনাটি সংস্করণভেদে বদলেছে; নিচে মূল চিন্তাটুকু দেওয়া হলো।

Solaris-এর time-sharing শ্রেণি (TS) একটি পরিষ্কার MLFQ: ৬০টি priority (০–৫৯, বড় সংখ্যা = বেশি অগ্রাধিকার), আর একটি dispatch table প্রতিটি priority-র জন্য বলে দেয় quantum কত, quantum ফুরালে নতুন priority কত (নিচে নামা), আর ঘুম থেকে ফিরলে কত (ওপরে ওঠা)। দীর্ঘ সময় CPU না পাওয়া thread-এর priority বাড়ানোর ঘরও আছে — starvation-এর বিরুদ্ধে। পাঠ্যবইয়ে ছাপা ডিফল্ট টেবিলের কয়েকটি সারি (সংস্করণভেদে মান আলাদা হতে পারে):

PriorityQuantum (ms)Quantum ফুরালেঘুম থেকে ফিরলে
০ (সবচেয়ে নিচে)২০০০৫০
১৫১৬০৫৫১
৩০৮০২০৫৩
৪৫৪০৩৫৫৬
৫৯ (সবচেয়ে ওপরে)২০৪৯৫৯

MLFQ-এর নিয়মগুলো হুবহু চেনা যায়: ওপরে ছোট quantum, নিচে বড়; quantum খেয়ে ফেললে priority কমে; I/O থেকে ফিরলে ৫০-এর ওপরে উঠে আসে। প্রশাসক dispadmin কমান্ড দিয়ে টেবিল দেখতে ও বদলাতে পারেন — voo-doo constants-এর একটি খোলা রূপ।

4.3BSD আর 4.4BSD-র scheduler-কেও বইয়ে MLFQ-এর রূপ বলা হয়, তবে স্তর বদলায় একটি সূত্রে: প্রসেসের সাম্প্রতিক CPU-ব্যবহারের একটি মান রাখা হয়, যা চলার সময় বাড়ে আর প্রতি সেকেন্ডে load অনুযায়ী ক্ষয় হয় (decay-usage); priority = ভিত্তি + ব্যবহারের ভাগ + nice-এর প্রভাব। বেশি খেলে priority কমে, অপেক্ষা করলে ব্যবহার ক্ষয় হয়ে priority ধীরে ফেরে — এক অর্থে নিরবচ্ছিন্ন boost. FreeBSD পরে ULE নামের নতুন scheduler আনে (প্রায় ২০০৩ সালে লেখা, ২০০৯-এর দিকে FreeBSD 7.1 থেকে ডিফল্ট), যা প্রতিটি thread কতক্ষণ ঘুমায় আর কতক্ষণ চলে তার অনুপাত থেকে একটি “interactivity score” হিসাব করে interactive thread-কে আলাদা অগ্রাধিকার দেয়।

Windows-এর scheduler ৩২টি priority স্তরের preemptive priority scheduler, প্রতিটি স্তরে Round Robin: ১৬–৩১ “real-time” স্তর, ১–১৫ “variable” বা dynamic স্তর, আর ০ একটি বিশেষ সিস্টেম thread-এর জন্য। প্রতিটি thread-এর একটি base priority থাকে (প্রসেসের priority class থেকে, যেমন Normal = ৮), আর variable স্তরে kernel সাময়িকভাবে তা বাড়ায় — dynamic priority boost:

  • I/O শেষ হলে boost: I/O সম্পন্ন করার সময় driver বলে দিতে পারে কতটা বাড়াতে হবে; বইয়ে উল্লেখিত উদাহরণ মানে ডিস্কের জন্য কম, কিবোর্ড-মাউসের জন্য বেশি, শব্দ-কার্ডের জন্য আরও বেশি।
  • Foreground-এর সুবিধা: সামনের জানালার প্রসেসের thread অপেক্ষা থেকে ফিরলে বাড়তি boost পায়, আর client সংস্করণে foreground প্রসেস লম্বা quantum পেতে পারে (“Programs” বনাম “Background services” সেটিং)।
  • অনাহারে boost: একটি সিস্টেম thread কয়েক সেকেন্ড ধরে প্রস্তুত অথচ CPU না পাওয়া thread খুঁজে সাময়িকভাবে উঁচু priority দেয় — Rule 5-এর আত্মীয়।

Boost প্রতি quantum-এ এক ধাপ করে কমে base priority-তে ফেরে, আর variable স্তরের সীমা (১৫) পেরোয় না; real-time স্তরে boost হয় না। খুঁটিনাটি Windows-এর সংস্করণভেদে বদলেছে (Microsoft-এর ডকুমেন্টেশন আর Windows Internals বই দেখুন)।

macOS আর iOS-এর kernel XNU-র scheduler এসেছে Mach থেকে: ১২৮টি priority স্তর, যা কয়েকটি band-এ ভাগ — সাধারণ ব্যবহারকারী-কাজ, উঁচু অগ্রাধিকারের সিস্টেম কাজ, kernel আর real-time. সাধারণ (timeshare) thread-এর priority তার সাম্প্রতিক CPU-ব্যবহারে ক্ষয় হয় আর বিশ্রামে ফেরে — decay-usage-এর মতো। Apple ২০১৪ সালের দিকে (OS X 10.10, iOS 8) QoS class আনে — user-interactive, user-initiated, utility, background — যাতে প্রোগ্রাম নিজেই জানায় কোন কাজ কতটা জরুরি; পরের XNU সংস্করণগুলোতে thread-দলভিত্তিক scheduling-ও যোগ হয়েছে। অর্থাৎ শ্রেণি অনেকটা multilevel queue-এর মতো আগে থেকে বলা, আর শ্রেণির ভেতরে আচরণ দেখে সমন্বয়।

Linux? Linux 2.6-এর প্রথম দিকের “O(1)” scheduler-এ ১৪০টি priority স্তর আর ঘুমের ইতিহাস থেকে interactivity-র অনুমান ছিল — MLFQ-এর ঘনিষ্ঠ আত্মীয়। কিন্তু সেই অনুমানের নিয়মগুলো জটিল আর কখনো কখনো ভুল হতো; ২০০৭ সালে Linux 2.6.23-এ CFS এসে স্তরের বদলে “কে কত কম CPU পেয়েছে” সরাসরি মাপা শুরু করে, আর Linux 6.6 (২০২৩) থেকে EEVDF. সেই গল্প পর্ব ২১-এ। Linux-এ আজও “শ্রেণি” আছে — real-time (SCHED_FIFO, SCHED_RR) শ্রেণি সবসময় সাধারণ শ্রেণির ওপরে — যা আসলে একটি multilevel queue. মজার ব্যাপার, ডিফল্ট সেটিংয়ে real-time শ্রেণি প্রতি সেকেন্ডে CPU-র সর্বোচ্চ প্রায় ৯৫% পায় (sched_rt_runtime_us সীমা), যাতে একটি ভুল real-time লুপ পুরো সিস্টেম জমিয়ে না দেয় — অর্থাৎ fixed priority-র সাথে এক টুকরো time slicing.

$ ps -eo pid,cls,rtprio,ni,comm | head -5
    PID CLS RTPRIO  NI COMMAND
      1  TS      -   0 systemd
      2  TS      -   0 kthreadd
     19  FF     99   - migration/0
    812  TS      -   0 sshd

এখানে TS মানে সাধারণ time-sharing শ্রেণি, FF মানে SCHED_FIFO real-time শ্রেণি, যা সবসময় TS-এর আগে চলে — একটি দুই-স্তরের multilevel queue চোখের সামনে। আপনার মেশিনে PID আর নাম আলাদা হবে।

৯ · AI-এর যুগে MLFQ

MLFQ-এর অঙ্ক বা ধারণা নিয়ে AI-কে জিজ্ঞেস করলে উত্তর সাধারণত আত্মবিশ্বাসী শোনায়, কিন্তু এই বিষয়ে কয়েকটি নির্দিষ্ট ভুল বারবার দেখা যায়:

AI-এর ভুলকেন ভুলকীভাবে যাচাই করবেন
Multilevel queue আর MLFQ-কে একই বলাপ্রথমটিতে প্রসেস স্তর বদলায় না, দ্বিতীয়টিতে বদলায়“প্রসেস কি queue বদলায়?” — এই একটি প্রশ্ন করুন
Gantt chart-এ ওপরের queue-এর arrival-এ preemption ভুলে যাওয়াক্লাসিক MLFQ-তে Q0-তে কেউ এলে Q1/Q2-এর জন থামেপ্রতিটি arrival-এর মুহূর্তে কে চলছিল, হাতে দেখুন
কাজ শেষ হলে বা I/O-তে গেলেও “demote” করাপুরোনো নিয়মে নামা শুধু পুরো quantum খেলেপ্রশ্নের নিয়মটি লিখে নিন, তারপর প্রতিটি ধাপ মেলান
“Linux MLFQ ব্যবহার করে”আধুনিক Linux-এ CFS/EEVDF; পুরোনো O(1) ছিল MLFQ-ঘনিষ্ঠKernel-এর সংস্করণ উল্লেখ করে জিজ্ঞেস করুন; kernel ডকুমেন্টেশন দেখুন
“দ্রুত করতে sudo chrt -f 99 বা nice -n -20 দিন”Real-time শ্রেণির একটি টানা লুপ পুরো core আটকে দিতে পারে, সিস্টেম সাড়া দেওয়া বন্ধ করতে পারেRoot দিয়ে priority বাড়ানোর আগে মাপুন; সাধারণ কাজে real-time শ্রেণি নয়
Windows-এর সংখ্যা (৩২ স্তর, boost-এর মান) বানিয়ে বলা বা পুরোনো সংস্করণের তথ্য দেওয়াবিবরণ সংস্করণভেদে বদলেছেMicrosoft-এর ডকুমেন্টেশন বা নির্ভরযোগ্য বইয়ের সাথে মিলান

MLFQ-এর অঙ্ক AI-কে দিলে নিয়মগুলো স্পষ্ট লিখে দিন, আর সমাধান যাচাইযোগ্য আকারে চান:

নিচের MLFQ সমস্যাটি সমাধান করো। নিয়ম: Q0 = RR q 8 ms, Q1 = RR q 16 ms, Q2 = FCFS. নতুন প্রসেস Q0-তে ঢোকে। পুরো quantum খেলে এক স্তর নিচে নামে। ওপরের queue-তে কেউ এলে নিচের চলমান প্রসেস preempted হয়, নিজের স্তরেই থাকে আর সেই queue-এর সামনে থেকে আবার চলে। কোনো boost নেই, context switch-এর খরচ ০. প্রসেস (arrival, burst): P1 (0, 40), P2 (2, 6), P3 (4, 10), P4 (44, 5). ১. প্রতিটি সময়-বিন্দুতে তিনটি queue-এর অবস্থা একটি টেবিলে দেখাও। ২. Gantt chart, প্রতিটি টুকরো কোন স্তরে চলেছে তা লিখে। ৩. প্রতিটি প্রসেসের completion, TAT, WT আর গড়। ৪. কোথাও নিয়ম অস্পষ্ট মনে হলে অনুমান না করে আলাদা করে বলো।

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

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

  • Multilevel queue আর MLFQ-এর পার্থক্য? — প্রথমটিতে প্রসেস স্থায়ী queue-তে থাকে; MLFQ-তে CPU-ব্যবহার দেখে প্রসেস queue বদলায়।
  • MLFQ define করতে কোন কোন প্যারামিটার লাগে? — queue-এর সংখ্যা, প্রতিটির algorithm, quantum, promotion/demotion-এর নিয়ম, আর নতুন প্রসেসের প্রবেশ-queue.
  • Q0 q = ৮, Q1 q = ১৬, Q2 FCFS; একা আসা ৩০ ms-এর প্রসেস কোন স্তরে কত সময় চলে? — Q0-তে ৮, Q1-এ ১৬, Q2-তে বাকি ৬.
  • MLFQ-তে starvation কেন হয়, আর সমাধান কী? — ওপরের স্তর সারাক্ষণ ব্যস্ত থাকলে নিচের কাজ CPU পায় না; সমাধান aging বা নির্দিষ্ট সময় পরপর priority boost.
  • Burst না জেনে MLFQ কীভাবে ছোট কাজকে আগে রাখে? — সবাইকে ওপরে ঢুকিয়ে, আর যারা বেশি CPU খায় তাদের নিচে নামিয়ে; স্তরই হয়ে ওঠে “এ পর্যন্ত কত চলেছে”-এর মাপ।

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

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

প্র ০১১৯৬২ সালের CTSS-এ কেন স্তরভিত্তিক scheduling দরকার হয়েছিল? সে যুগের হার্ডওয়্যারের সীমাবদ্ধতা এই নকশাকে কীভাবে গড়েছে?

১৯৫০-এর দশকের বড় কম্পিউটারগুলো চলত batch পদ্ধতিতে: প্রোগ্রামার পাঞ্চ কার্ডের গাদা জমা দিতেন, কয়েক ঘণ্টা বা পরের দিন ফল পেতেন। MIT-তে Fernando Corbató-র দল দেখালেন, একটি কম্পিউটারকে দ্রুত পালাবদলে অনেক ব্যবহারকারী একসাথে টার্মিনাল থেকে ব্যবহার করতে পারেন — প্রত্যেকে মনে করেন যন্ত্রটি শুধু তাঁর। এটিই time-sharing, আর CTSS ছিল এর প্রথম দিকের সফল নমুনাগুলোর একটি; প্রথম প্রদর্শন হয় ১৯৬১ সালে, প্রবন্ধ প্রকাশ হয় ১৯৬২ সালে।

কিন্তু সে যুগের মেমরি ছিল খুব ছোট আর দামি। অনেক ব্যবহারকারীর প্রোগ্রাম একসাথে মেমরিতে রাখা যেত না, তাই পালা এলে প্রোগ্রামকে ড্রাম বা ডিস্ক থেকে মেমরিতে আনতে হতো, পালা শেষে আবার ফেরত (swap). সব প্রোগ্রামকে একই ছোট quantum দিলে বড় প্রোগ্রামগুলো বারবার swap হতো, আর CPU-র বেশিরভাগ সময় যেত আনা-নেওয়ায়। আবার সবাইকে বড় quantum দিলে টার্মিনালে বসা মানুষ অনেকক্ষণ সাড়া পেতেন না।

স্তরভিত্তিক সমাধান দুটো চাহিদাকে আলাদা করে: যে কাজ ছোট আর বারবার সাড়া চায়, সে ওপরের স্তরে ছোট quantum পায় আর দ্রুত ফেরে; যে কাজ দীর্ঘ, সে নিচে নেমে কম ঘন ঘন কিন্তু বড় টুকরো পায় — ফলে swap-এর খরচ ভাগ হয় অনেক বেশি কাজের ওপর। বিভিন্ন বর্ণনা অনুযায়ী CTSS-এ নিচের স্তরে quantum ক্রমে দ্বিগুণ হতো, আর শুরুর স্তর প্রোগ্রামের আকারের সাথেও যুক্ত ছিল (বড় প্রোগ্রাম swap-এ বেশি খরচ করে)। পরে Multics-এ এই চিন্তা আরও এগোয়, আর Unix-এর প্রথম দিকের scheduler-ও “বেশি CPU খেলে priority কমবে” নীতি নেয়। ১৯৯০ সালে Corbató-র Turing Award-এর কারণ হিসেবে CTSS আর Multics-এর কাজই উল্লেখ করা হয়।

আজ মেমরি সস্তা, swap প্রায় হয়ই না, তবু MLFQ টিকে আছে — কারণ মূল সমস্যাটি ছিল আরও গভীর: কোন কাজ কতটা জরুরি, OS তা আগে থেকে জানে না। হার্ডওয়্যারের সীমা একটি সমাধান খুঁজতে বাধ্য করেছিল, আর সেই সমাধান একটি সাধারণ নীতি হয়ে উঠেছে।

মূল উপলব্ধি: একটি নকশার জন্মের কারণ (swap-এর খরচ) ফুরিয়ে গেলেও নকশাটি টিকে থাকে, যদি সে একটি গভীর সমস্যার — অজানা ভবিষ্যৎ নিয়ে সিদ্ধান্ত — উত্তর দেয়।

প্র ০২“Gaming the scheduler” কি নিরাপত্তার সমস্যা, নাকি শুধু ন্যায্যতার? একটি shared সার্ভারে OS কীভাবে নিশ্চিত করে যে কেউ নিজের ভাগের বেশি CPU নিতে পারবে না?

প্রথমে বোঝা দরকার, এখানে কোনো সুরক্ষা-ব্যবস্থা ভাঙা হয় না — প্রোগ্রামটি OS-এর নিয়ম মেনেই চলে, শুধু নিয়মের একটি দুর্বলতা থেকে বাড়তি সুবিধা পায়। তাই এটি মূলত সম্পদ-বণ্টনের ন্যায্যতা আর হিসাব রাখার সমস্যা। তবে একটি shared সার্ভার, বিশ্ববিদ্যালয়ের ল্যাব-সার্ভার বা ক্লাউডের একই মেশিনে চলা কয়েকজন গ্রাহকের ক্ষেত্রে এর ফল নিরাপত্তার কাছাকাছি: একজন ব্যবহারকারী অন্যদের কাজ ধীর করে দিতে পারেন, যাকে নিরাপত্তার ভাষায় সম্পদের ওপর আক্রমণ বা denial of service-এর একটি নরম রূপ বলা যায়।

OS-এর প্রতিরক্ষা কয়েক স্তরে। প্রথম স্তর scheduler-এর নিজের হিসাব: OSTEP-এর নতুন Rule 4-এর মতো, প্রতিটি thread মোট কত CPU পেয়েছে তা গোনা, CPU ছেড়ে দিলেও হিসাব মুছে না ফেলা। Linux-এর CFS আর EEVDF ঠিক এই চিন্তায় তৈরি — প্রতিটি task-এর প্রাপ্ত “virtual runtime” সরাসরি মাপা হয়, তাই আচরণের কৌশলে বাড়তি পাওয়ার সুযোগ কম। দ্বিতীয় স্তর দলভিত্তিক সীমা: একজন ব্যবহারকারী ১০০টি thread খুললে thread-ভিত্তিক ন্যায্যতায় তিনি ১০০ ভাগ পাবেন। তাই Linux-এ cgroup দিয়ে প্রতিটি ব্যবহারকারী, সার্ভিস বা container-কে একটি দল বানিয়ে CPU-র ভাগ (weight) বা ঊর্ধ্বসীমা (quota) দেওয়া যায় — systemd প্রতিটি লগইন-সেশন আর সার্ভিসকে এমন দলে রাখে। Windows-এ Job object আর CPU rate control একই কাজ করে।

তৃতীয় স্তর privilege: সাধারণ ব্যবহারকারী নিজের priority কমাতে পারেন, কিন্তু বাড়াতে বা real-time শ্রেণিতে যেতে বিশেষ অনুমতি লাগে — Linux-এ CAP_SYS_NICE বা RLIMIT_RTPRIO, Windows-এ বিশেষ privilege. এগুলো ঠিকভাবে সেট আছে কি না দেখা, আর ulimit -a বা systemd-এর সেটিং দিয়ে সীমা যাচাই করা একজন প্রশাসকের কাজ।

এখানে একটি নকশা-শিক্ষাও আছে: যে নিয়ম “আচরণের লক্ষণ” দেখে পুরস্কার দেয় (যেমন “তাড়াতাড়ি CPU ছাড়লে ওপরে থাকো”), সেই লক্ষণ নকল করা যায়। আর যে নিয়ম “আসল খরচ” গোনে, তাকে ফাঁকি দেওয়া কঠিন।

মূল উপলব্ধি: ন্যায্য scheduling মানে সঠিক হিসাব রাখা — thread-এর স্তরে, আর দলের (ব্যবহারকারী, সার্ভিস, container) স্তরেও; লক্ষণ নয়, প্রকৃত ব্যবহার মাপুন।

প্র ০৩Windows সামনের জানালার প্রোগ্রাম আর I/O থেকে ফেরা thread-কে বাড়তি priority দেয়। এটি ব্যবহারকারীর জন্য ভালো, কিন্তু সার্ভারের জন্য কেন সবসময় ভালো নয়?

Windows NT-র scheduler-এর মূল কাঠামো একটি priority scheduler, কিন্তু তার ১–১৫ স্তরে kernel thread-এর priority সাময়িকভাবে বাড়ায় — MLFQ-এর “I/O থেকে ফিরলে ওপরে ওঠা” নিয়মের একটি রূপ। যুক্তিটি সরল: যে thread কিবোর্ড, মাউস বা শব্দের অপেক্ষায় ছিল, সে জাগার পর দ্রুত চলুক, যাতে মানুষ দেরি টের না পান। শব্দের ক্ষেত্রে দেরি হলে আওয়াজ ভেঙে যায়, তাই সেখানে boost বেশি; ডিস্কের ক্ষেত্রে কম। Foreground প্রসেসের জন্য client সংস্করণে quantum লম্বা করার সুবিধাও আছে, যাতে আপনি যে প্রোগ্রামে কাজ করছেন সেটিই বেশি সময় পায়। Boost প্রতি quantum-এ এক ধাপ করে কমে base-এ ফেরে, তাই এটি সাময়িক।

ডেস্কটপে এটি চমৎকার কাজ করে, কারণ সেখানে “জরুরি” মানে মানুষের সামনে যা আছে। কিন্তু একটি সার্ভারে কোনো “সামনের জানালা” নেই; সব অনুরোধই সমান গুরুত্বপূর্ণ, আর লক্ষ্য throughput আর সমান latency. সেখানে foreground-এর বাড়তি সুবিধা অর্থহীন, বরং লম্বা আর সমান quantum বেশি উপযোগী — কম context switch, ভালো cache-ব্যবহার। এ কারণেই Windows Server সংস্করণগুলো ডিফল্টভাবে “background services”-এর পক্ষে সেট থাকে, আর client সংস্করণ “programs”-এর পক্ষে। একই kernel, ভিন্ন ডিফল্ট — voo-doo constants-এর আরেকটি উদাহরণ।

Boost-এর আরেকটি সূক্ষ্ম ঝুঁকি হলো অনুমানযোগ্যতা। যে কাজ প্রায়ই I/O করে, সে বেশি boost পায় — ফলে একই ধরনের দুটি সার্ভার-প্রোগ্রামের মধ্যে যেটি বেশি ছোট ছোট I/O করে সে বেশি CPU পেতে পারে, যা “gaming”-এর মতো অন্যায্যতা আনে। অন্যদিকে, অনাহারী thread-কে কয়েক সেকেন্ড পরপর boost দেওয়ার ব্যবস্থাটি priority inversion-এর মতো সমস্যা (পর্ব ২৯) সামলাতেও সাহায্য করে — নিচের thread একটি lock ধরে বসে থাকলে boost পেয়ে সে lock ছেড়ে দিতে পারে।

মূল উপলব্ধি: “কোন কাজ জরুরি” প্রশ্নের উত্তর ব্যবহারের ধরনের ওপর নির্ভর করে — ডেস্কটপে মানুষের চোখের সামনের কাজ, সার্ভারে সব অনুরোধের সমতা। তাই একই scheduler-এর ভিন্ন ডিফল্ট থাকা দুর্বলতা নয়, প্রয়োজন।

প্র ০৪Linux 2.6-এর O(1) scheduler-এর interactivity অনুমান ছেড়ে ২০০৭ সালে CFS আনা হলো কেন? MLFQ-ধাঁচের heuristic-এর সীমা কোথায়?

Ingo Molnár-এর O(1) scheduler (Linux 2.6-এর শুরু থেকে) নামের মতোই দ্রুত ছিল: ১৪০টি priority স্তর, প্রতিটির একটি queue, আর একটি bitmap থেকে সবচেয়ে উঁচু অ-খালি স্তর খুঁজে নেওয়া — যত task-ই থাকুক, সিদ্ধান্তের সময় স্থির। এর ভেতরে MLFQ-এর মতো একটি চিন্তা ছিল: প্রতিটি task কতক্ষণ ঘুমায় তার একটি গড় রাখা হতো, আর বেশি ঘুমানো task-কে “interactive” ধরে priority-তে বোনাস দেওয়া হতো (কয়েক ধাপ পর্যন্ত)। Quantum ফুরালে task “expired” তালিকায় যেত, আর interactive task-কে কখনো কখনো আবার “active” তালিকায় রাখা হতো।

সমস্যা দেখা দিল heuristic-এর প্রান্তে। কিছু প্রোগ্রাম ঘুমায় অনেক, কিন্তু interactive নয়; কিছু interactive প্রোগ্রাম (যেমন গেম বা ভিডিও প্লেয়ার) প্রায় ঘুমায় না, অথচ মসৃণ সাড়া চায়। অনুমানের নিয়মে অনেক বিশেষ শর্ত যোগ হতে থাকল, আর প্রতিটি সংশোধন কোনো না কোনো workload-এ নতুন সমস্যা আনত — ডেস্কটপে কখনো কখনো জমে যাওয়ার মতো অভিজ্ঞতা, বা নির্দিষ্ট আচরণের প্রোগ্রাম বেশি সুবিধা পাওয়া। এই অবস্থায় Con Kolivas নামের একজন ডেভেলপার “fair” scheduling-এর পরীক্ষামূলক scheduler (যেমন RSDL) দিয়ে দেখালেন, জটিল অনুমান ছাড়াই ভালো ডেস্কটপ সাড়া পাওয়া সম্ভব। সেই আলোচনার পর Molnár CFS (Completely Fair Scheduler) লেখেন, যা ২০০৭ সালে Linux 2.6.23-এ যুক্ত হয়।

CFS-এর মূল চিন্তা MLFQ-এর বিপরীত দিক থেকে আসে: স্তর বা অনুমান নয়, প্রতিটি task এ পর্যন্ত কত CPU পেয়েছে (nice অনুযায়ী ওজন দিয়ে) তা সরাসরি মাপা, আর যে সবচেয়ে কম পেয়েছে তাকে চালানো। খেয়াল করুন, এটি অংশ ৭-এর “least attained service”-এর একটি ন্যায্য রূপ — অর্থাৎ MLFQ যা মোটা দাগে অনুকরণ করত, CFS তা সূক্ষ্মভাবে মাপে। ঘুমিয়ে থাকা task জাগলে সে স্বাভাবিকভাবেই কম পেয়েছে বলে দ্রুত সুযোগ পায় — আলাদা “interactivity বোনাস” লাগে না। Linux 6.6 (২০২৩)-এ EEVDF এসে latency-র প্রশ্ন আরও পরিষ্কারভাবে সামলায় (পর্ব ২১)।

মূল উপলব্ধি: আচরণ দেখে অনুমান (heuristic) দ্রুত ফল দেয়, কিন্তু নিয়ম যত বাড়ে, প্রান্তিক ক্ষেত্রে আচরণ তত অনুমান-অযোগ্য হয়; সরাসরি মাপা একটি সরল নীতি প্রায়ই দীর্ঘমেয়াদে বেশি নির্ভরযোগ্য।

প্র ০৫আজকের মোবাইল OS আর ক্লাউডে multilevel queue-এর চিন্তা কোথায় দেখা যায়? “শ্রেণি আগে থেকে বলে দেওয়া” আর “আচরণ দেখে বোঝা” — কোনটি কখন ভালো?

Multilevel queue-কে প্রায়ই “পুরোনো আর অনমনীয়” বলা হয়, কিন্তু এর মূল চিন্তা — প্রোগ্রামের শ্রেণি আগে থেকে জানা থাকলে সেটিই ব্যবহার করা — আধুনিক সিস্টেমে ফিরে এসেছে। Android প্রতিটি অ্যাপকে তার অবস্থা অনুযায়ী দলে রাখে: যে অ্যাপ এখন পর্দায়, সে foreground দলে; পটভূমির অ্যাপ background দলে, যার CPU-ভাগ সীমিত। ভেতরে এটি Linux-এর cgroup আর priority দিয়ে বাস্তবায়িত। Apple-এর QoS class একই চিন্তা: প্রোগ্রামার নিজে বলে দেন কোন কাজ user-interactive (স্ক্রল, অ্যানিমেশন), কোনটি utility (ডাউনলোড), কোনটি background (ব্যাকআপ, indexing).

ক্লাউডে একই চিন্তা আরও বড় মাপে। Kubernetes-এ প্রতিটি pod-এর একটি QoS শ্রেণি থাকে (Guaranteed, Burstable, BestEffort), যা নির্ধারণ করে চাপের সময় কে আগে সম্পদ হারাবে। বড় কোম্পানির ক্লাস্টার-scheduler-গুলোতেও (যেমন Google-এর Borg সম্পর্কে প্রকাশিত প্রবন্ধে বর্ণিত) “production” আর “batch” কাজ আলাদা অগ্রাধিকারে চলে — ঠিক পাঠ্যবইয়ের foreground/background-এর মতো, শুধু একটি মেশিন নয়, হাজার মেশিনে।

তাহলে কখন কোনটি? শ্রেণি আগে থেকে বলা ভালো, যখন প্রোগ্রামার বা সিস্টেম সত্যিই জানে কাজটি কী, আর ভুল ঘোষণার শাস্তি আছে বা প্রণোদনা মিলে যায় — মোবাইলে পর্দার অ্যাপ নিশ্চিতভাবেই জরুরি। এতে অনুমানের ভুল থাকে না, আর আচরণ অনুমানযোগ্য। আচরণ দেখে বোঝা (MLFQ, decay-usage, CFS) ভালো, যখন কেউ কিছু বলে না, বা বলা তথ্য বিশ্বাসযোগ্য নয় — একটি সাধারণ Linux সার্ভারে হাজারো প্রোগ্রাম, যাদের কেউ নিজের শ্রেণি ঘোষণা করে না। বাস্তবের সেরা নকশাগুলো দুটো মেশায়: শ্রেণি দিয়ে মোটা ভাগ (multilevel queue), আর শ্রেণির ভেতরে আচরণ দেখে সূক্ষ্ম সমন্বয় (feedback). macOS-এর band আর QoS, Windows-এর priority class আর dynamic boost, Linux-এর real-time শ্রেণি আর CFS/EEVDF — সবই এই দুই স্তরের নকশা।

মূল উপলব্ধি: তথ্য থাকলে ব্যবহার করুন, না থাকলে পর্যবেক্ষণ করুন — আর দুটো মিলিয়ে ব্যবহার করাই আধুনিক scheduler-এর পথ।

অনুশীলন

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

  1. হাতে হিসাব: MLFQ — Q0 = RR q ৪ ms, Q1 = RR q ৮ ms, Q2 = FCFS. নতুন প্রসেস Q0-তে, পুরো quantum খেলে এক স্তর নিচে, ওপরের queue preempt করে। প্রসেস: P1 (arrival ০, burst ১৪), P2 (২, ৬), P3 (৭, ৩). Gantt chart আঁকুন, আর প্রতিটির completion, TAT, WT ও গড় বের করুন।

    Gantt: ০–৪ P1@Q0 (বাকি ১০, Q1-এ) │ ৪–৮ P2@Q0 (বাকি ২, Q1-এ) │ ৮–১১ P3@Q0 (৩ < ৪, শেষ) │ ১১–১৯ P1@Q1 (পুরো ৮, বাকি ২, Q2-তে) │ ১৯–২১ P2@Q1 (শেষ) │ ২১–২৩ P1@Q2 (শেষ)। খেয়াল করুন, P3 t = ৭-এ এলেও P2 তখন Q0-তেই চলছিল, তাই একই স্তরে preemption নেই — P3 অপেক্ষা করে t = ৮ পর্যন্ত।

    Completion: P1 ২৩, P2 ২১, P3 ১১. TAT: ২৩, ১৯, ৪ → গড় ৪৬/৩ ≈ ১৫.৩৩ ms. WT: ২৩ − ১৪ = ৯, ১৯ − ৬ = ১৩, ৪ − ৩ = ১ → গড় ২৩/৩ ≈ ৭.৬৭ ms. (Simulator-এ quantum = [4, 8, Infinity] আর এই তিনটি প্রসেস বসিয়ে মিলিয়ে নিতে পারেন।)

  2. আউটপুট অনুমান: অংশ ৬-এর simulator-এ boost-এর সময় ১০ থেকে কমিয়ে ৫ ms করলে (mlfq(jobs, [2, 4, 8], 5, false)) A-এর সমাপ্তি, A-এর longest wait আর B-এর সমাপ্তি কেমন বদলাবে বলে মনে হয়? আগে অনুমান লিখুন, তারপর চালান।

    আসল আউটপুটের শেষ তিন লাইন:

    A: finish 41, turnaround 41, longest wait 3
    B: finish 59, turnaround 53, longest wait 2
    C: finish 60, turnaround 54, longest wait 2

    প্রতি ৫ ms-এ A ওপরে উঠে Q0-তে B আর C-এর সাথে Round Robin-এ ভাগ পায়, তাই A অনেক আগে শেষ হয় (৬০ → ৪১), আর টানা অপেক্ষা মাত্র ৩ ms. কিন্তু দাম দেয় interactive কাজ: B-এর সমাপ্তি ৫৫ থেকে ৫৯-এ, boost ছাড়া ছিল ৪৫. অর্থাৎ S যত ছোট, দীর্ঘ কাজ তত ন্যায্য ভাগ পায়, আর interactive কাজের অগ্রাধিকার তত কমে — খুব ছোট S দিলে MLFQ প্রায় সাধারণ Round Robin হয়ে যায়। এটাই voo-doo constant-এর দর-কষাকষি।

  3. কোড বদলান: অংশ ৪-এর textbook simulator-এ Q2-কে FCFS-এর বদলে RR (q = ৩২) করতে চান। কোন লাইন বদলাবেন? এরপর কোনো প্রসেস Q2-তে পুরো ৩২ ms খেলে কোডের কোন লাইনে সমস্যা হবে, আর কীভাবে সারাবেন?

    প্রথম বদল: const quantum = [8, 16, 32];. কিন্তু demotion-এর লাইনে Q[lv + 1].push(p) আছে; সবচেয়ে নিচের স্তরে (lv = ২) Q[3] নেই, তাই প্রোগ্রাম TypeError দিয়ে থেমে যাবে। সমাধান: সবচেয়ে নিচের স্তরে নামার বদলে একই স্তরের পেছনে পাঠানো —

    else if (p.used === quantum[lv]) { p.used = 0; Q[lv].shift(); Q[Math.min(lv + 1, 2)].push(p); }

    এতে Q2-তে পুরো quantum খাওয়া প্রসেস Q2-এর পেছনে যায় — অর্থাৎ সবচেয়ে নিচের স্তরে Round Robin. এই পর্বের ডেটায় ফল বদলায় না, কারণ P1 Q2-তে মোট ১৬ ms চলে (৪ + ১২), কখনো ৩২ ছোঁয় না। অংশ ৬-এর simulator-এ একই কাজ Math.min(lv + 1, quanta.length - 1) দিয়ে করা হয়েছে।

  4. নিজের OS-এ দেখুন: Linux-এ ps -eo pid,cls,rtprio,ni,pri,comm চালিয়ে দেখুন কোন কোন প্রসেস TS আর কোনগুলো FF বা RR. Windows-এ Task Manager → Details-এ যেকোনো প্রসেসে ডান-ক্লিক করে “Set priority”-র তালিকাটি দেখুন (বদলাবেন না)। দুটি থেকে এই পর্বের কোন ধারণা চিনতে পারছেন?

    Linux-এ বেশিরভাগ প্রসেস TS (সাধারণ শ্রেণি, Linux-এ যার নাম SCHED_OTHER), আর কয়েকটি kernel thread (যেমন migration/0, watchdog) FF — real-time FIFO, rtprio ৯৯-এর মতো। এটি একটি multilevel queue: real-time শ্রেণির কেউ প্রস্তুত থাকলে TS-এর কেউ চলে না। TS-এর ভেতরে ni (nice) আর pri দেখায় সাধারণ শ্রেণির ভেতরের অগ্রাধিকার। Windows-এর তালিকায় Realtime, High, Above normal, Normal, Below normal, Low — এগুলো priority class, যা থেকে thread-এর base priority আসে (যেমন Normal = ৮, Realtime শ্রেণি ২৪-এর আশেপাশে)। এর ওপরে kernel-এর dynamic boost আলাদা করে কাজ করে। সতর্কতা: কোনো প্রোগ্রামকে Realtime দিলে একটি টানা-হিসাবের প্রোগ্রাম কিবোর্ড-মাউস পর্যন্ত জমিয়ে দিতে পারে — পরীক্ষার জন্যও এটি করবেন না।

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

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

ক্রমMLFQ-তে (Q0 q = ৮, Q1 q = ১৬, Q2 FCFS, priority boost চালু) একটি দীর্ঘ CPU-bound প্রসেসের ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. প্রসেসটি এসে সবার ওপরের queue Q0-তে ঢোকে
  2. Q0-তে পুরো ৮ ms খেয়ে Q1-এ নামে
  3. Q1-এ পুরো ১৬ ms খেয়ে Q2-তে নামে
  4. Q0 আর Q1 খালি হলে তবেই Q2-তে FCFS-এ চলে
  5. Priority boost-এর সময় আবার Q0-তে ওঠে

Multilevel queue আর multilevel feedback queue scheduling-এর মূল পার্থক্য কোনটি?

  • Multilevel queue-তে একটিমাত্র queue থাকে
  • MLFQ-তে প্রসেস আচরণ অনুযায়ী এক queue থেকে আরেক queue-তে যেতে পারে, multilevel queue-তে পারে না
  • শুধু MLFQ-তে প্রতিটি queue-এর আলাদা algorithm থাকে
  • MLFQ কখনো preemptive হয় না

একটি MLFQ-তে Q0 = RR q ৮ ms, Q1 = RR q ১৬ ms, Q2 = FCFS. ৩০ ms burst-এর একটি প্রসেস একা t = ০-তে এল, আর কেউ আসেনি। সে Q2-তে কত ms চলবে?

  • ০ ms
  • ২২ ms
  • ৬ ms
  • ১৪ ms

MLFQ-তে নির্দিষ্ট সময় পরপর সব কাজকে সবার ওপরের queue-তে তোলা (priority boost) মূলত কোন সমস্যার সমাধান করে?

  • নিচের স্তরের কাজের starvation, আর আচরণ বদলানো কাজকে আবার ওপরে ওঠার সুযোগ দেওয়া
  • Context switch-এর সংখ্যা কমানো
  • প্রতিটি প্রসেসের burst time আগে থেকে জানা
  • Interactive কাজের response time সবসময় কমানো

পুরোনো নিয়মে (“time slice শেষের আগে CPU ছাড়লে একই স্তরে থাকবে”) একটি প্রোগ্রাম প্রতিবার slice-এর ৯৯% চলে একটি ছোট I/O করে। এর ফল আর সমাধান কোনটি?

  • প্রোগ্রামটি দ্রুত সবচেয়ে নিচের স্তরে নেমে যায়; সমাধান লাগে না
  • অন্যরা বেশি CPU পায়; সমাধান quantum বড় করা
  • প্রোগ্রামটি kernel mode-এর সুরক্ষা ভেঙে ফেলে; সমাধান শুধু priority boost
  • প্রোগ্রামটি ওপরের স্তরে থেকে বেশি CPU পায়; সমাধান প্রতি স্তরে মোট ব্যবহৃত সময় (allotment) গোনা

একটি multilevel queue-তে foreground আর background queue-এর মধ্যে ৮০/২০ time slicing চলছে, আর দুই queue-তেই সবসময় কাজ আছে। ১০০ ms-এ background queue মোট কত ms CPU পাবে?

  • ০ ms
  • ২০ ms
  • ৫০ ms
  • ৮০ ms

Windows-এর ৩২-স্তরের priority scheduler-এ dynamic priority boost (যেমন I/O শেষ হলে বা foreground-এর জন্য) সাধারণত কোন thread-এর ক্ষেত্রে প্রযোজ্য?

  • শুধু real-time স্তরের (১৬–৩১) thread
  • শুধু priority ০-এর thread
  • Variable স্তরের (১–১৫) thread, আর boost ১৫-এর ওপরে যায় না
  • সব স্তরের thread সমানভাবে

শব্দকোষ

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

মনে রাখুন

  1. Multilevel queue: স্থায়ী শ্রেণির কয়েকটি queue, প্রতিটির নিজের algorithm; queue-গুলোর মধ্যে fixed priority (starvation-এর ঝুঁকি) বা time slicing (যেমন ৮০/২০)।
  2. MLFQ-এর পাঁচ প্যারামিটার: queue-সংখ্যা, প্রতিটির algorithm, quantum, promotion/demotion-এর নিয়ম, প্রবেশ-queue.
  3. Textbook উদাহরণ (৮, ১৬, FCFS): নতুন প্রসেস Q0-তে; পুরো quantum খেলে এক স্তর নিচে; ওপরের queue-তে কেউ এলে নিচের জন preempted. গড় TAT/WT সবসময় হাতে আর simulator-এ মিলিয়ে নিন।
  4. Rule 1–5: উঁচু স্তর আগে; সমান স্তরে RR; নতুন কাজ ওপরে; allotment পূর্ণ হলে নামা (gaming বন্ধ); প্রতি S সময়ে boost (starvation বন্ধ)।
  5. MLFQ burst না জেনে SJF/SRTF-এর কাছাকাছি যায়, কারণ স্তর মানে “এ পর্যন্ত কত চলেছে”; কিন্তু quantum, allotment, S-এর মতো voo-doo constants বাছাই কঠিন। Solaris, BSD, Windows, macOS নিজ নিজ রূপে এই ধারণা ব্যবহার করে।

পরের পর্বে: এতক্ষণ CPU ছিল একটি। এবার একাধিক core — কোন কাজ কোন core-এ যাবে, cache কেন affinity চায়, আর যেখানে deadline মিস করা চলে না, সেখানে Rate Monotonic আর EDF কীভাবে নিশ্চয়তা দেয়।

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

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