এই পর্বে যা শিখবেন
- Static আর dynamic priority, preemptive আর non-preemptive priority — arrival time-সহ পুরো Gantt chart আর CT, TAT, WT, RT-এর হিসাব
- Starvation কেন হয়, aging কীভাবে তা ঠেকায় (প্রমাণসহ), আর priority inversion-এর প্রাথমিক ধারণা
- Round Robin-এর ready queue ধাপে ধাপে — একই মুহূর্তে আগমন আর preemption হলে সারির ক্রম কী
- Quantum-এর আকার কীভাবে response, overhead আর turnaround বদলায় — একটি simulator দিয়ে q = 1, 2, 4, 8 যাচাই
- Priority-র সাথে RR মেশানো, lottery আর stride scheduling, আর Linux-এ
niceনিজের চোখে দেখা
১ · অগ্রাধিকার কেন — হাসপাতালের জরুরি বিভাগ
পর্ব ১৭-তে দেখেছেন FCFS, SJF, SRTF আর HRRN — সেখানে বাছাইয়ের ভিত্তি ছিল আসার সময় বা burst-এর দৈর্ঘ্য। কিন্তু বাস্তবে সব কাজ সমান জরুরি নয়। একটি সরকারি হাসপাতালের জরুরি বিভাগের কথা ভাবুন। সারিতে আগে এসেছেন জ্বরের এক রোগী, পরে এলেন সড়ক দুর্ঘটনায় আহত একজন। ডাক্তার আগে কাকে দেখবেন? আহতকে — কারণ তাঁর অগ্রাধিকার বেশি। এই বাছাইয়ের নাম triage. CPU-র ক্ষেত্রেও একই চিন্তা: মাউসের নড়াচড়ায় সাড়া দেওয়া, অডিও বাজানো বা নেটওয়ার্ক প্যাকেট সামলানো এমন কাজ, যা দেরি হলে ব্যবহারকারী সঙ্গে সঙ্গে টের পান; অথচ রাতের ব্যাকআপ কয়েক সেকেন্ড পিছিয়ে গেলে কারও ক্ষতি নেই।
Priority scheduling-এ প্রতিটি প্রসেসের সাথে একটি অগ্রাধিকার-সংখ্যা (priority) যুক্ত থাকে। Scheduler ready queue-এর প্রসেসদের মধ্যে যার অগ্রাধিকার সবচেয়ে বেশি, তাকে CPU দেয়। দুজনের অগ্রাধিকার সমান হলে এই পর্বে নিয়ম: যে আগে এসেছে, সে আগে (অর্থাৎ সমান স্তরে FCFS).
ছোট সংখ্যা = বেশি অগ্রাধিকার। Priority 1-এর প্রসেস priority 3-এর আগে চলবে। Silberschatz-সহ অনেক পাঠ্যবই এভাবেই লেখে, আর Linux-এর nice value-ও একই দিকে চলে (nice −20 সবচেয়ে বেশি CPU পায়)। কিন্তু সব সিস্টেম এক নয় — নিচের টেবিল দেখুন। পরীক্ষার প্রশ্নে কোন দিক ধরা হয়েছে লেখা না থাকলে উত্তরের শুরুতেই নিজের ধরে নেওয়া নিয়মটি লিখে দিন।
| ব্যবস্থা | সংখ্যার পরিসর | কোনটি বেশি অগ্রাধিকার |
|---|---|---|
| এই পর্ব ও অনেক পাঠ্যবই | 0, 1, 2 … | ছোট সংখ্যা |
| Linux / Unix-এর nice | −20 … 19 | ছোট সংখ্যা (তবে এটি কঠোর অগ্রাধিকার নয়, CPU-র ভাগের অনুপাত বদলায় — পর্ব ২১) |
| Linux real-time নীতি (SCHED_FIFO, SCHED_RR) | 1 … 99 | বড় সংখ্যা |
Linux kernel-এর ভেতরের prio | 0 … 139 | ছোট সংখ্যা (0–99 real-time, 100–139 সাধারণ প্রসেস) |
| Windows | 0 … 31 | বড় সংখ্যা |
| macOS/iOS-এর XNU kernel | 0 … 127 | বড় সংখ্যা |
খেয়াল করুন, একই Linux-এর ভেতরেই দুই দিক আছে: user-কে দেখানো real-time priority-তে ৯৯ সবচেয়ে জরুরি, আর kernel-এর ভেতরের হিসাবে ০ সবচেয়ে জরুরি। তাই AI বা কোনো ব্লগ “priority বেশি” বললে আগে জিজ্ঞেস করুন — কোন সংখ্যার কথা হচ্ছে?
Static আর dynamic priority
Static priority
প্রসেস তৈরির সময় অগ্রাধিকার ঠিক হয়, পরে নিজে থেকে বদলায় না। সরল আর অনুমানযোগ্য — real-time সিস্টেমে (যেমন গাড়ির ব্রেক-নিয়ন্ত্রক) ঠিক এটাই দরকার, যেখানে নকশার সময়েই জানা থাকে কোন কাজ বেশি জরুরি।
Linux SCHED_FIFO, RTOS-এর task priorityDynamic priority
চলার সময় OS অগ্রাধিকার বদলায় — বেশি CPU খেলে নামায়, বেশি অপেক্ষা করলে বা I/O শেষ হলে তোলে। উদ্দেশ্য interactive প্রসেসকে দ্রুত সাড়া দেওয়া আর কাউকে চিরকাল বঞ্চিত না রাখা।
পুরোনো BSD Unix, Windows-এর priority boostঅগ্রাধিকার আসে কোথা থেকে? ভেতরের (internal) মাপ থেকে — মেমরির চাহিদা, খোলা ফাইলের সংখ্যা, গড় CPU burst আর I/O burst-এর অনুপাত; অথবা বাইরের (external) সিদ্ধান্ত থেকে — কাজটি কতটা গুরুত্বপূর্ণ, কে টাকা দিচ্ছে, কোন বিভাগের কাজ। একটি সুন্দর সংযোগও আছে: SJF আসলে priority scheduling-এরই একটি বিশেষ রূপ, যেখানে priority = পরের CPU burst-এর অনুমিত দৈর্ঘ্য (ছোট burst = বেশি অগ্রাধিকার)। তাই SJF-এর starvation-সমস্যাও priority-র starvation-সমস্যার মতোই।
২ · Non-preemptive আর preemptive priority — পূর্ণ হিসাব
Priority scheduling দুইভাবে চলতে পারে। Non-preemptive হলে একবার CPU পাওয়া প্রসেস তার পুরো CPU burst শেষ (বা block) না হওয়া পর্যন্ত চলে; নতুন কেউ এলে সে সারিতে অপেক্ষা করে। Preemptive হলে নতুন আসা প্রসেসের অগ্রাধিকার চলতি প্রসেসের চেয়ে বেশি হওয়া মাত্র preemption হয় — চলতিটিকে থামিয়ে নতুনটিকে CPU দেওয়া হয়। মাপকাঠিগুলো পর্ব ১৬-এর: TAT = CT − AT, WT = TAT − BT, আর RT (response time) = প্রথমবার CPU পাওয়ার সময় − AT.
উদাহরণ ১: চারটি প্রসেস। ছোট সংখ্যা = বেশি অগ্রাধিকার।
| প্রসেস | Arrival (AT) | Burst (BT) | Priority |
|---|---|---|---|
| P1 | 0 | 4 | 3 |
| P2 | 1 | 3 | 1 |
| P3 | 2 | 5 | 4 |
| P4 | 3 | 2 | 2 |
Non-preemptive: t = 0-তে শুধু P1 আছে, সে চলে ০–৪, মাঝে P2, P3, P4 এলেও থামে না। t = ৪-এ ready: P2 (১), P3 (৪), P4 (২) → P2 চলে ৪–৭; তারপর P4 ৭–৯; শেষে P3 ৯–১৪.
│ P1 0-4 │ P2 4-7 │ P4 7-9 │ P3 9-14 │
Preemptive: এখানে প্রতিটি আগমন একটি সিদ্ধান্তের মুহূর্ত। নিচের stepper-এ ধাপে ধাপে দেখুন — বিশেষ করে t = ১-এ কী ঘটে।
1t=0 P1 arrives (pr 3), runs2t=1 P2 arrives (pr 1) -> preempts P13t=2 P3 arrives (pr 4), waits4t=3 P4 arrives (pr 2), waits5t=4 P2 done -> best is P4 (pr 2)6t=6 P4 done -> P1 (pr 3)7t=9 P1 done -> P3 (pr 4)8t=14 P3 done
| t | |
| CPU-তে | |
| ready (pr) | |
| বাকি P1/P2/P3/P4 |
- t = ০: শুধু P1 এসেছে, তাই অগ্রাধিকার ৩ হলেও সে-ই চলে। তার RT = ০.
- t = ১ — preemption: P2 এল অগ্রাধিকার ১ নিয়ে, যা P1-এর ৩-এর চেয়ে বেশি। Scheduler সঙ্গে সঙ্গে P1-কে থামায়; P1-এর বাকি ৪ − ১ = ৩. P2 চলা শুরু করে, তার RT = ১ − ১ = ০.
- t = ২: P3 এল অগ্রাধিকার ৪ নিয়ে — চলতি P2-এর (১) চেয়ে কম, তাই কিছুই বদলায় না। P3 সারিতে অপেক্ষা করে।
- t = ৩: P4 এল অগ্রাধিকার ২ নিয়ে। ২ কি ১-এর চেয়ে বেশি অগ্রাধিকার? না। তাই P2 চলতেই থাকে। Preemption শুধু তখনই, যখন নতুনটি কঠোরভাবে বেশি অগ্রাধিকারের।
- t = ৪: P2 শেষ (CT = ৪)। Ready-তে P1 (৩), P3 (৪), P4 (২) — সবচেয়ে ছোট সংখ্যা P4-এর, সে চলে ৪–৬.
- t = ৬: P4 শেষ (CT = ৬)। এবার P1 (৩) আর P3 (৪) — P1 তার বাকি ৩ একক চালায়, ৬–৯.
- t = ৯: P1 শেষ (CT = ৯)। একা P3 বাকি, চলে ৯–১৪. P3 প্রথম CPU পেল t = ৯-এ, তাই তার RT = ৯ − ২ = ৭.
- ফল: CT = ৯, ৪, ১৪, ৬; TAT = ৯, ৩, ১২, ৩ → গড় ২৭/৪ = ৬.৭৫; WT = ৫, ০, ৭, ১ → গড় ৩.২৫; গড় RT = (০ + ০ + ৭ + ১)/৪ = ২. Non-preemptive-এ একই ডেটায় গড় TAT ৭, গড় WT ৩.৫, গড় RT ৩.৫ — preemption বেশি জরুরি কাজকে দ্রুত সাড়া দিয়েছে।
| প্রসেস | Non-pre: CT / TAT / WT / RT | Preemptive: CT / TAT / WT / RT |
|---|---|---|
| P1 | 4 / 4 / 0 / 0 | 9 / 9 / 5 / 0 |
| P2 | 7 / 6 / 3 / 3 | 4 / 3 / 0 / 0 |
| P3 | 14 / 12 / 7 / 7 | 14 / 12 / 7 / 7 |
| P4 | 9 / 6 / 4 / 4 | 6 / 3 / 1 / 1 |
| গড় | TAT 7, WT 3.5, RT 3.5 | TAT 6.75, WT 3.25, RT 2 |
Preemptive priority-তে একটি সূক্ষ্ম ফাঁদ: কম অগ্রাধিকারের P1 এখানে বারবার পিছিয়েছে — preemptive-এ তার WT ০ থেকে বেড়ে ৫. একই ধারা চলতে থাকলে, অর্থাৎ বেশি অগ্রাধিকারের নতুন কাজ আসতেই থাকলে, P1 আর কখনো শেষই হতো না। পরের অংশের বিষয় ঠিক এটাই।
৩ · Starvation আর aging
ব্যাংকের একটি কাউন্টারে নিয়ম: প্রবীণ নাগরিক আর প্রতিবন্ধী গ্রাহকেরা আগে সেবা পাবেন। ভালো নিয়ম। কিন্তু ধরুন সেদিন সকাল থেকে বিকেল পর্যন্ত একের পর এক প্রবীণ গ্রাহক আসতেই থাকলেন। সাধারণ টোকেন হাতে বসে থাকা ছাত্রটি সারা দিনেও ডাক পেলেন না। এটাই starvation (অনির্দিষ্ট অপেক্ষা) — প্রসেস প্রস্তুত, কিন্তু তার চেয়ে বেশি অগ্রাধিকারের কেউ না কেউ সবসময় থাকায় সে কখনো CPU পায় না। একটি বহুল-প্রচলিত গল্প আছে: ১৯৭৩ সালে MIT-র IBM 7094 কম্পিউটার বন্ধ করার সময় নাকি ১৯৬৭ সালে জমা দেওয়া একটি কম-অগ্রাধিকারের job পাওয়া গিয়েছিল, যা তখনো চলেনি। গল্পটির বিস্তারিত যাচাই করা কঠিন, কিন্তু শিক্ষাটি পরিষ্কার।
সমাধান aging: যে প্রসেস যত বেশি সময় ready queue-তে অপেক্ষা করে, তার অগ্রাধিকার ধীরে ধীরে তত বাড়ানো হয়। ব্যাংকের উদাহরণে — “যিনি এক ঘণ্টা বসে আছেন, তিনি প্রবীণদের সমান গণ্য হবেন।”
উদাহরণ ২ (aging): L প্রসেস আসে t = ০-তে, BT = ২, priority ৪. সাথে উঁচু অগ্রাধিকারের (priority ১) পাঁচটি H প্রসেস, প্রতিটির BT = ৩, আসে t = ০, ২, ৫, ৮, ১১-তে। Scheduling non-preemptive. Aging-এর নিয়ম: ready queue-তে প্রতি ৩ একক অপেক্ষার পর priority-সংখ্যা ১ কমে (সর্বনিম্ন ০)। সিদ্ধান্তের মুহূর্তে aging আগে হিসাব হয়, তারপর বাছাই; সমান হলে আগে আসা আগে।
Aging ছাড়া উত্তর সহজ: H1 ০–৩, H2 ৩–৬, H3 ৬–৯, H4 ৯–১২, H5 ১২–১৫, তারপর L ১৫–১৭. L-এর WT = ১৫. আর H আসা যদি থামতই না, L কখনো চলত না। এবার aging-সহ:
1t=0 L(4) vs H1(1) -> H12t=3 L waited 3 -> pr 3; -> H23t=6 L waited 6 -> pr 2; -> H34t=9 L waited 9 -> pr 1 = H4(1); L came first -> L5t=11 L done; H4 waited 3 -> pr 0; -> H46t=14 H5 waited 3 -> pr 0; -> H57t=17 all done
| t | |
| ready (pr) | |
| L-এর অপেক্ষা | |
| CPU পেল |
- t = ০: L (৪) আর H1 (১) — H1 জেতে, চলে ০–৩. L অপেক্ষা শুরু করে।
- t = ৩: L তিন একক অপেক্ষা করেছে, তাই তার priority ৪ → ৩. কিন্তু H2 (১, এসেছে t = ২-এ) এখনো বেশি অগ্রাধিকারের; H2 চলে ৩–৬.
- t = ৬: L-এর অপেক্ষা ৬ → priority ২. H3 (১) তবু এগিয়ে; চলে ৬–৯.
- t = ৯: L-এর অপেক্ষা ৯ → priority ১, এখন H4-এর সমান। সমান হলে আগে আসা আগে — L এসেছে t = ০-তে, H4 t = ৮-এ। তাই L চলে ৯–১১. Aging না থাকলে এখানে আবার H4 চলত।
- t = ১১: L শেষ, CT = ১১, WT = ১১ − ০ − ২ = ৯. এদিকে H4 নিজেও ৩ একক অপেক্ষা করেছে (৮ থেকে ১১), তাই তার priority ১ → ০. H4 চলে ১১–১৪.
- t = ১৪: H5 (এসেছে ১১-তে) ৩ একক অপেক্ষার পর priority ০; একাই আছে, চলে ১৪–১৭.
- ফল: L-এর WT ১৫ থেকে নেমে ৯; H4 আর H5-এর WT ১ থেকে বেড়ে ৩ — aging-এর দাম। গড় WT ৩.১৭ থেকে কমে ২.৮৩. সবচেয়ে বড় কথা, H যত দিনই আসুক, L-এর অপেক্ষার একটি ঊর্ধ্বসীমা তৈরি হলো।
প্রমাণ — aging কেন starvation দূর করে
দাবি: প্রতি k একক অপেক্ষায় priority-সংখ্যা ১ কমলে, সর্বনিম্ন সংখ্যা ০ হলে, আর সমান priority-তে আগে আসা প্রসেস আগে চললে, কোনো প্রসেস অনির্দিষ্টকাল অপেক্ষা করে না।
- প্রসেস X t₀ সময়ে আসে, priority p₀. অপেক্ষায় থাকলে বড়জোর k·p₀ একক পরে তার priority ০, যা সর্বোচ্চ অগ্রাধিকার।
- এরপর X-এর আগে চলতে পারে শুধু সেই প্রসেস, যার priority-ও ০ এবং যে X-এর আগে এসেছে। X-এর পরে আসা কেউ সমান priority-তে X-কে টপকাতে পারে না।
- X-এর আগে আসা প্রসেস সসীম-সংখ্যক, আর প্রত্যেকের বাকি burst সসীম। (Non-preemptive হলে তখন চলতি প্রসেসের বাকি burst-ও যোগ হবে।)
- তাই X-এর মোট অপেক্ষা ≤ k·p₀ + (আগে আসা সবার বাকি burst-এর যোগফল) — একটি সসীম সংখ্যা। অর্থাৎ starvation নেই। ∎
খেয়াল করুন, প্রমাণে tie-নিয়মটি জরুরি। সমান priority-তে যদি এলোমেলোভাবে বাছা হতো, তাহলে priority ০-তে নতুন আসা প্রসেসেরা X-কে অনির্দিষ্টকাল টপকে যেতে পারত।
নিচের simulator উদাহরণ ১ আর ২ দুটোই চালায়। preemptive আর AGE বদলে দেখুন — যেমন উদাহরণ ১-এ preemptive-এর সাথে aging দিলে কী হয়।
// Priority scheduling. SMALLER number = HIGHER priority. Tie: earlier arrival.
// Aging: after every AGE time units spent waiting, priority number drops by 1 (min 0).
function run(title, procs, preemptive, AGE = Infinity) {
const ps = procs.map(([id, at, bt, pr]) => ({ id, at, bt, pr, left: bt, w: 0, start: -1, ct: 0 }));
let t = 0, cur = null; const seg = [];
while (ps.some(p => p.left > 0)) {
const ready = ps.filter(p => p.at <= t && p.left > 0);
if (!ready.length) { t++; continue; } // CPU idle
if (!cur || preemptive) { // decision point
const best = [...ready].sort((a, b) => a.pr - b.pr || a.at - b.at)[0];
if (best !== cur) seg.push({ id: best.id, from: t, pr: best.pr });
cur = best;
if (cur.start < 0) cur.start = t;
}
cur.left--; // run 1 time unit
for (const p of ready) if (p !== cur && ++p.w % AGE === 0 && p.pr > 0) p.pr--;
t++;
if (cur.left === 0) { cur.ct = t; cur = null; }
}
seg.forEach((s, k) => (s.to = k + 1 < seg.length ? seg[k + 1].from : t));
console.log(title + "\n " + seg.map(s => s.id + " " + s.from + "-" + s.to + " (pr " + s.pr + ")").join(" │ "));
let tat = 0, wt = 0;
for (const p of ps) {
tat += p.ct - p.at; wt += p.ct - p.at - p.bt;
console.log(" " + p.id + ": CT=" + p.ct + " TAT=" + (p.ct - p.at) + " WT=" + (p.ct - p.at - p.bt) + " RT=" + (p.start - p.at));
}
console.log(" avg TAT=" + (tat / ps.length).toFixed(2) + " avg WT=" + (wt / ps.length).toFixed(2));
}
const A = [["P1", 0, 4, 3], ["P2", 1, 3, 1], ["P3", 2, 5, 4], ["P4", 3, 2, 2]];
run("Non-preemptive", A, false);
run("Preemptive", A, true);
const B = [["L", 0, 2, 4], ["H1", 0, 3, 1], ["H2", 2, 3, 1], ["H3", 5, 3, 1], ["H4", 8, 3, 1], ["H5", 11, 3, 1]];
run("No aging", B, false);
run("Aging every 3 units", B, false, 3);আউটপুট (simulator নির্ধারিত, প্রতিবার একই):
Non-preemptive
P1 0-4 (pr 3) │ P2 4-7 (pr 1) │ P4 7-9 (pr 2) │ P3 9-14 (pr 4)
P1: CT=4 TAT=4 WT=0 RT=0
P2: CT=7 TAT=6 WT=3 RT=3
P3: CT=14 TAT=12 WT=7 RT=7
P4: CT=9 TAT=6 WT=4 RT=4
avg TAT=7.00 avg WT=3.50
Preemptive
P1 0-1 (pr 3) │ P2 1-4 (pr 1) │ P4 4-6 (pr 2) │ P1 6-9 (pr 3) │ P3 9-14 (pr 4)
P1: CT=9 TAT=9 WT=5 RT=0
P2: CT=4 TAT=3 WT=0 RT=0
P3: CT=14 TAT=12 WT=7 RT=7
P4: CT=6 TAT=3 WT=1 RT=1
avg TAT=6.75 avg WT=3.25
No aging
H1 0-3 (pr 1) │ H2 3-6 (pr 1) │ H3 6-9 (pr 1) │ H4 9-12 (pr 1) │ H5 12-15 (pr 1) │ L 15-17 (pr 4)
L: CT=17 TAT=17 WT=15 RT=15
...
avg TAT=6.00 avg WT=3.17
Aging every 3 units
H1 0-3 (pr 1) │ H2 3-6 (pr 1) │ H3 6-9 (pr 1) │ L 9-11 (pr 1) │ H4 11-14 (pr 0) │ H5 14-17 (pr 0)
L: CT=11 TAT=11 WT=9 RT=9
...
H4: CT=14 TAT=6 WT=3 RT=3
H5: CT=17 TAT=6 WT=3 RT=3
avg TAT=5.67 avg WT=2.83
(জায়গা বাঁচাতে H1–H3-এর একই রকম লাইন ... দিয়ে বাদ দেওয়া।) প্রথম দুটি অংশ উপরের টেবিলের সাথে হুবহু মেলে। শেষ অংশের Gantt-এ দেখুন, L চলার সময় তার priority লেখা pr 1 — অর্থাৎ শুরুর ৪ থেকে aging তাকে তিন ধাপ তুলেছে। H4 আর H5-এর pr 0-ও aging-এর ফল: তারাও ৩ একক অপেক্ষা করেছিল। কোডের মূল লাইনটি একটিই — ++p.w % AGE === 0 হলে p.pr--. বাস্তব OS-এ priority সাধারণত চলার পর আবার মূল মানে ফিরিয়ে আনা হয়; এই ছোট simulator-এ প্রতিটি প্রসেস একবারই চলে বলে সেটি দরকার হয়নি।
৪ · Priority inversion — যখন কম জরুরি কাজ বেশি জরুরিকে আটকে দেয়
Priority-র আরেকটি বিপদ আসে যখন প্রসেসেরা কোনো সম্পদ ভাগ করে — যেমন একটি mutex দিয়ে সুরক্ষিত ডেটা (মডিউল ৪-এ বিস্তারিত)। তিনটি task ধরুন: H (উচ্চ), M (মাঝারি), L (নিম্ন)। L একটি lock R নিয়ে কাজ করছে। H-এর ওই R দরকার। তাহলে H-কে অপেক্ষা করতে হবে L-এর জন্য — এটুকু স্বাভাবিক, কারণ L শিগগিরই R ছেড়ে দেবে। সমস্যা শুরু হয় যখন M এসে পড়ে।
1L runs, lock(R) succeeds2H wakes, preempts L; lock(R) -> BLOCKED3M wakes, preempts L (M > L)4M runs on and on ... H still waiting5FIX: L inherits H's priority, unlocks, H runs
| CPU-তে | |
| R-এর মালিক | |
| H | |
| L-এর কার্যকর priority |
- L lock নেয়। নিম্ন-অগ্রাধিকারের L চলছে, আর critical section-এ ঢোকার জন্য lock R নিয়েছে।
- H জাগে, কিন্তু আটকে যায়। H বেশি অগ্রাধিকারের, তাই L-কে preempt করে। কিন্তু H-এরও R দরকার — R L-এর হাতে, তাই H block হয়। L আবার চলে। এই অপেক্ষাটুকু অনিবার্য, আর এটি সীমিত — L-এর critical section যতক্ষণ।
- M জাগে। M-এর অগ্রাধিকার L-এর চেয়ে বেশি, তাই M L-কে preempt করে। M-এর R লাগে না, সে নিজের কাজ চালায়। L CPU পাচ্ছে না, তাই R-ও ছাড়তে পারছে না।
- Inversion: ফল দাঁড়াল — সবচেয়ে জরুরি H অপেক্ষা করছে মাঝারি M-এর জন্য, যার সাথে H-এর কোনো সম্পর্কই নেই। M-এর মতো কাজ আরও এলে H-এর অপেক্ষার কোনো ঊর্ধ্বসীমা নেই (unbounded priority inversion).
- প্রতিকার — priority inheritance: H যখন R-এর জন্য block হয়, R-এর মালিক L সাময়িকভাবে H-এর অগ্রাধিকার “উত্তরাধিকার” পায়। ফলে M আর L-কে সরাতে পারে না; L দ্রুত critical section শেষ করে R ছাড়ে, নিজের নিম্ন অগ্রাধিকারে ফেরে, আর H সঙ্গে সঙ্গে R পেয়ে চলে। H-এর অপেক্ষা এখন L-এর critical section-এর দৈর্ঘ্যে সীমিত।
সবচেয়ে বিখ্যাত উদাহরণ মঙ্গল গ্রহ থেকে। ১৯৯৭ সালের জুলাইয়ে NASA-র Mars Pathfinder মঙ্গলে নামার কিছুদিন পর এর কম্পিউটার বারবার নিজে থেকে রিসেট হতে থাকে। প্রকল্পের প্রকৌশলীদের পরে প্রকাশিত বহুল-উদ্ধৃত বিবরণ অনুযায়ী, কারণ ছিল ঠিক এই ছবিটি: VxWorks real-time OS-এ একটি উচ্চ-অগ্রাধিকারের task একটি ভাগের lock-এর জন্য নিম্ন-অগ্রাধিকারের task-এর অপেক্ষায় আটকে ছিল, আর মাঝারি অগ্রাধিকারের task-গুলো নিম্নটিকে সরিয়ে রাখছিল; watchdog দেখল উচ্চ task সময়মতো শেষ হচ্ছে না, আর পুরো সিস্টেম রিসেট করল। সমাধান হিসেবে সেই mutex-এর priority inheritance বিকল্প চালু করার নির্দেশ পৃথিবী থেকে পাঠানো হয়েছিল বলে জানা যায়। Priority inversion-এর পূর্ণ আলোচনা — priority ceiling, lock-free বিকল্প — থাকবে পর্ব ২৯-এ।
৫ · Round Robin — বৃত্তাকার সারি আর tie-নিয়ম
পাড়ার চায়ের দোকানে একজন দোকানি, আর পাঁচজন অতিথি। কেউ চাইছেন শুধু এক কাপ চা, কেউ চা-বিস্কুট-কলা আর পান। দোকানি যদি একজনের সব কাজ শেষ করে তবে পরের জনের কাছে যান (FCFS), তাহলে শুধু চা চাওয়া মানুষটি অনেকক্ষণ দাঁড়িয়ে থাকেন। বুদ্ধিমান দোকানি তাই পালা করে প্রত্যেকের এক-একটি জিনিস দেন — এক চক্রে সবাই কিছু না কিছু পান, আর কারও কাজ শেষ না হলে তিনি আবার সারির পেছনে যান। এটাই Round Robin (RR), time-sharing সিস্টেমের মূল নীতি।
- Ready queue একটি বৃত্তাকার FIFO সারি; নতুন প্রসেস যোগ হয় সারির পেছনে।
- Scheduler সামনের প্রসেসটি নেয় আর একটি timer বসায় q সময়ের জন্য।
- প্রসেসের বাকি burst q-এর চেয়ে কম হলে সে নিজেই শেষ হয় (বা block হয়), CPU ছেড়ে দেয়। না হলে q শেষে timer interrupt আসে, প্রসেসটি সারির পেছনে যায় — RR তাই একটি preemptive নীতি।
- Tie-নিয়ম: একই মুহূর্তে একটি নতুন প্রসেস আসে আর চলতি প্রসেসের quantum ফুরায়, তাহলে নতুন প্রসেস আগে সারিতে ঢোকে, তারপর সময়-ফুরানো প্রসেসটি। অনেক পাঠ্যবই ও সমাধান-বই এই প্রথা মানে, তবে সবাই নয় — উল্টো প্রথাও প্রচলিত। পরীক্ষায় উত্তরের শুরুতে কোন নিয়ম ধরেছেন লিখে দিন।
উদাহরণ ৩: পাঁচটি প্রসেস, q = ২. এই ডেটা ইচ্ছে করে এমনভাবে বাছা, যাতে tie-নিয়ম দুবার কাজে লাগে (t = ২ আর t = ৪)।
| প্রসেস | P1 | P2 | P3 | P4 | P5 |
|---|---|---|---|---|---|
| Arrival (AT) | 0 | 1 | 2 | 3 | 4 |
| Burst (BT) | 5 | 3 | 1 | 2 | 3 |
1t=0 queue [P1] run P1 0-22t=2 P3 new, then P1 run P2 2-43t=4 P5 new, then P2 run P3 4-5 (done)4t=5 run P1 5-75t=7 P1 to tail run P4 7-9 (done)6t=9 run P5 9-117t=11 P5 to tail run P2 11-12 (done)8t=12 run P1 12-13 (done)9t=13 run P5 13-14 (done)10t=14 all done
| t | |
| ready queue | |
| CPU পেল | |
| বাকি P1..P5 |
- t = ০: সারিতে শুধু P1. সে চলে ০–২; মাঝে t = ১-এ P2 এসে সারিতে বসে। P1-এর বাকি ৫ − ২ = ৩.
- t = ২ — প্রথম tie: এই মুহূর্তে P3 আসে, আবার P1-এর quantum-ও ফুরায়। নিয়ম অনুযায়ী আগে নতুন P3, তারপর P1. সারি: [P2 P3 P1]. সামনের P2 চলে ২–৪, তার বাকি ১.
- t = ৪ — দ্বিতীয় tie: t = ৩-এ P4 এসে বসেছিল। এখন t = ৪-এ P5 আসে আর P2-এর quantum ফুরায় — আগে P5, তারপর P2. সারি: [P3 P1 P4 P5 P2]. P3-এর burst মাত্র ১, তাই সে ৪–৫ চলেই শেষ (CT = ৫) — পুরো quantum লাগেনি, CPU আগেই ছেড়ে দিল।
- t = ৫: সারি [P1 P4 P5 P2]. P1 চলে ৫–৭, বাকি ১.
- t = ৭: P1 পেছনে: [P4 P5 P2 P1]. P4-এর burst ২ = q, চলে ৭–৯ আর শেষ (CT = ৯)।
- t = ৯: সারি [P5 P2 P1]. P5 চলে ৯–১১, বাকি ১.
- t = ১১: P5 পেছনে: [P2 P1 P5]. P2-এর বাকি ১, চলে ১১–১২, শেষ (CT = ১২)।
- t = ১২: সারি [P1 P5]. P1-এর শেষ ১ একক, ১২–১৩ (CT = ১৩)।
- t = ১৩: শুধু P5, ১৩–১৪ (CT = ১৪)।
- ফল: CT = ১৩, ১২, ৫, ৯, ১৪. TAT = CT − AT = ১৩, ১১, ৩, ৬, ১০ → গড় ৮.৬. WT = TAT − BT = ৮, ৮, ২, ৪, ৭ → গড় ৫.৮. প্রথম CPU পাওয়া t = ০, ২, ৪, ৭, ৯ → RT = ০, ১, ২, ৪, ৫ → গড় ২.৪.
একই ডেটা, q = ৪
P1 চলে ০–৪ (বাকি ১)। এর মধ্যে P2, P3, P4 এসে গেছে; t = ৪-এ P5 আসে আর P1-এর quantum ফুরায় — আগে P5, পরে P1: সারি [P2 P3 P4 P5 P1]. এরপর P2 ৪–৭, P3 ৭–৮, P4 ৮–১০, P5 ১০–১৩, P1 ১৩–১৪ — কেউই আর quantum ফুরিয়ে ফেরে না, শুধু P1 শেষে।
│ P1 0-4 │ P2 4-7 │ P3 7-8 │ P4 8-10 │ P5 10-13 │ P1 13-14 │
| প্রসেস | q = 2: CT / TAT / WT / RT | q = 4: CT / TAT / WT / RT |
|---|---|---|
| P1 | 13 / 13 / 8 / 0 | 14 / 14 / 9 / 0 |
| P2 | 12 / 11 / 8 / 1 | 7 / 6 / 3 / 3 |
| P3 | 5 / 3 / 2 / 2 | 8 / 6 / 5 / 5 |
| P4 | 9 / 6 / 4 / 4 | 10 / 7 / 5 / 5 |
| P5 | 14 / 10 / 7 / 5 | 13 / 9 / 6 / 6 |
| গড় | TAT 8.6, WT 5.8, RT 2.4 | TAT 8.4, WT 5.6, RT 3.8 |
উল্টো প্রথা ধরলে কী বদলায়?
কিছু বই ও শিক্ষক উল্টো নিয়ম মানেন: সময়-ফুরানো প্রসেস আগে সারিতে ফেরে, একই মুহূর্তে আসা নতুন প্রসেস তার পরে। তাহলে q = ২-এ t = ২-এর সারি হয় [P2 P1 P3], আর পুরো চার্ট বদলে যায়:
│ P1 0-2 │ P2 2-4 │ P1 4-6 │ P3 6-7 │ P4 7-9 │ P2 9-10 │ P5 10-12 │ P1 12-13 │ P5 13-14 │
এখানে CT = ১৩, ১০, ৭, ৯, ১৪; গড় TAT আর গড় WT কাকতালীয়ভাবে একই (৮.৬ আর ৫.৮) থাকলেও প্রতিটি প্রসেসের আলাদা উত্তর বদলেছে, আর গড় RT ২.৪ থেকে বেড়ে ৩.০. q = ৪-এ গড় TAT ৮.৪ থেকে কমে ৮.০ হয়। তাই “আমার উত্তর বইয়ের সাথে মিলছে না” — এমন হলে প্রথম সন্দেহ এই tie-নিয়ম। দুটোই যুক্তিসঙ্গত; দরকার শুধু স্পষ্টভাবে লিখে দেওয়া।
৬ · RR simulator, response time আর একটি গ্যারান্টি
হাতে Gantt chart আঁকা শিখেছেন; এবার একই কাজ কোডে। নিচের simulator উপরের নিয়ম হুবহু মানে — admit() আগে, তারপর preempted প্রসেস সারিতে। প্রতিটি quantum-সীমায় সারির অবস্থা ছাপে, আর q = ১, ২, ৪, ৮-এর জন্য প্রতিটি প্রসেসের CT, TAT, WT, RT দেখায়। শেষ লাইনটি পরের অংশের জন্য।
// Round Robin. Tie rule: at the same instant a NEW arrival is enqueued
// BEFORE the process whose quantum just expired. Input sorted by arrival.
const jobs = [["P1", 0, 5], ["P2", 1, 3], ["P3", 2, 1], ["P4", 3, 2], ["P5", 4, 3]];
function rr(list, q, show) {
const ps = list.map(([id, at, bt]) => ({ id, at, bt, left: bt, start: -1, ct: 0 }));
let t = 0, next = 0; const queue = [], gantt = [], qlog = [];
const admit = () => { while (next < ps.length && ps[next].at <= t) queue.push(ps[next++]); };
admit();
while (queue.length || next < ps.length) {
if (!queue.length) { t = ps[next].at; admit(); } // CPU idle until next arrival
const p = queue.shift();
if (p.start < 0) p.start = t;
const run = Math.min(q, p.left);
gantt.push(p.id + " " + t + "-" + (t + run));
t += run; p.left -= run;
admit(); // 1) new arrivals first
if (p.left > 0) queue.push(p); else p.ct = t; // 2) then the preempted one
qlog.push(t + ":[" + queue.map(x => x.id).join(" ") + "]");
}
let tat = 0, wt = 0, rt = 0;
if (show) console.log("q = " + q + "\n Gantt: " + gantt.join(" │ ") + "\n queue: " + qlog.join(" "));
for (const p of ps) {
tat += p.ct - p.at; wt += p.ct - p.at - p.bt; rt += p.start - p.at;
if (show) console.log(" " + p.id + " CT=" + p.ct + " TAT=" + (p.ct - p.at) +
" WT=" + (p.ct - p.at - p.bt) + " RT=" + (p.start - p.at));
}
const n = ps.length;
if (show) console.log(" avg TAT=" + tat / n + " WT=" + wt / n + " RT=" + rt / n);
return tat / n;
}
for (const q of [1, 2, 4, 8]) rr(jobs, q, true);
const four = [["P1", 0, 6], ["P2", 0, 3], ["P3", 0, 1], ["P4", 0, 7]];
console.log("avg TAT for bursts 6,3,1,7: " +
[1, 2, 3, 4, 5, 6, 7].map(q => "q" + q + "=" + rr(four, q, false)).join(" "));আউটপুট (q = ১-এর লম্বা সারি-লাইনটি এখানে ছোট করা; পুরোটা চালালেই দেখবেন):
q = 1
Gantt: P1 0-1 │ P2 1-2 │ P1 2-3 │ P3 3-4 │ P2 4-5 │ P4 5-6 │ P1 6-7 │ P5 7-8 │ P2 8-9 │ P4 9-10 │ P1 10-11 │ P5 11-12 │ P1 12-13 │ P5 13-14
queue: 1:[P2 P1] 2:[P1 P3 P2] 3:[P3 P2 P4 P1] 4:[P2 P4 P1 P5] ... 13:[P5] 14:[]
...
avg TAT=8 WT=5.2 RT=1.2
q = 2
Gantt: P1 0-2 │ P2 2-4 │ P3 4-5 │ P1 5-7 │ P4 7-9 │ P5 9-11 │ P2 11-12 │ P1 12-13 │ P5 13-14
queue: 2:[P2 P3 P1] 4:[P3 P1 P4 P5 P2] 5:[P1 P4 P5 P2] 7:[P4 P5 P2 P1] 9:[P5 P2 P1] 11:[P2 P1 P5] 12:[P1 P5] 13:[P5] 14:[]
P1 CT=13 TAT=13 WT=8 RT=0
P2 CT=12 TAT=11 WT=8 RT=1
P3 CT=5 TAT=3 WT=2 RT=2
P4 CT=9 TAT=6 WT=4 RT=4
P5 CT=14 TAT=10 WT=7 RT=5
avg TAT=8.6 WT=5.8 RT=2.4
q = 4
Gantt: P1 0-4 │ P2 4-7 │ P3 7-8 │ P4 8-10 │ P5 10-13 │ P1 13-14
queue: 4:[P2 P3 P4 P5 P1] 7:[P3 P4 P5 P1] 8:[P4 P5 P1] 10:[P5 P1] 13:[P1] 14:[]
...
avg TAT=8.4 WT=5.6 RT=3.8
q = 8
Gantt: P1 0-5 │ P2 5-8 │ P3 8-9 │ P4 9-11 │ P5 11-14
queue: 5:[P2 P3 P4 P5] 8:[P3 P4 P5] 9:[P4 P5] 11:[P5] 14:[]
...
avg TAT=7.4 WT=4.6 RT=4.6
avg TAT for bursts 6,3,1,7: q1=11 q2=11.5 q3=10.75 q4=11.5 q5=12.25 q6=10.5 q7=10.5
q = ২-এর সারি-লাইন stepper-এর প্রতিটি ধাপের সাথে মেলে: 2:[P2 P3 P1] — t = ২-এ নতুন P3 সময়-ফুরানো P1-এর আগে। q = ৮-এ সবচেয়ে বড় burst (৫)-ও এক quantum-এ শেষ হয়, তাই চার্টটি হুবহু FCFS-এর। এবার RT-এর সারি দেখুন: q = ১, ২, ৪, ৮-এ গড় RT ১.২ → ২.৪ → ৩.৮ → ৪.৬ — quantum যত ছোট, প্রত্যেকে তত তাড়াতাড়ি প্রথম সাড়া পায়। কিন্তু গড় TAT (৮ → ৮.৬ → ৮.৪ → ৭.৪) কোনো সরল ধারা মানে না — শেষ লাইনটি অংশ ৭-এ ব্যাখ্যা করা হবে।
RR-এর গ্যারান্টি — প্রমাণসহ
RR-এর আসল শক্তি গড় নয়, নিশ্চয়তা। দাবি: ready queue-তে মোট n টি প্রসেস থাকলে (context switch-এর খরচ বাদে) কোনো প্রসেস পরপর দুই পালার মাঝে (n − 1)·q-এর বেশি অপেক্ষা করে না।
যুক্তি: প্রসেস X যখন সারির পেছনে ঢোকে, তার সামনে আছে বড়জোর n − 1 টি প্রসেস। সারি FIFO, তাই পরে যারা আসবে তারা X-এর পেছনে বসবে — X-এর অবস্থান কেবল সামনে এগোয়। সামনের প্রত্যেকে একবারে সর্বোচ্চ q চলে (তার আগে শেষ বা block হলে আরও কম)। তাই X-এর অপেক্ষা ≤ (n − 1)·q. ∎ উদাহরণ: ৫টি প্রসেস, q = ২০ ms হলে কেউ ৮০ ms-এর বেশি সাড়াহীন থাকে না। Context switch-এর খরচ s যোগ করলে হিসাব ও CPU efficiency q/(q + s) পর্ব ৯-এর Context switch আর dispatcher-এ পূর্ণভাবে করা আছে — এখানে পুনরাবৃত্তি করছি না।
এজন্যই interactive সিস্টেমে RR জনপ্রিয়: টাইপ করা অক্ষরটি পর্দায় আসতে কতক্ষণ লাগবে, তার একটি ঊর্ধ্বসীমা জানা। Priority বা SJF-এ এমন সীমা নেই — বড় কাজ বা কম অগ্রাধিকারের কাজ যেকোনো দৈর্ঘ্যে পিছিয়ে যেতে পারে।
৭ · Quantum কত বড় হবে?
পুরো RR-এর আচরণ একটি সংখ্যায় বাঁধা — q. দুই প্রান্ত থেকে ভাবুন।
q → ∞ : RR = FCFS
q যদি সবচেয়ে বড় burst-এর সমান বা বড় হয়, কোনো প্রসেসের quantum কখনো ফুরায় না; প্রত্যেকে প্রথম পালাতেই শেষ। কেউ সারিতে ফেরে না, তাই ক্রম শুধু আসার ক্রম — অর্থাৎ হুবহু FCFS (উপরের q = ৮). ছোট কাজগুলো আবার বড়দের পেছনে আটকে যায় (convoy effect, পর্ব ১৭)।
q → 0 : processor sharing
তাত্ত্বিকভাবে n টি প্রসেস যেন প্রত্যেকে নিজস্ব ১/n গতির CPU পেয়েছে — একে বলে processor sharing. কিন্তু বাস্তবে প্রতিটি পালাবদলে context switch-এর খরচ আছে; q সেই খরচের কাছাকাছি নামলে CPU-র বড় অংশ যায় পালাবদলে (পর্ব ৯-এর q/(q + s) সূত্র)।
তাহলে মাঝামাঝি কোথায়? পাঠ্যবইয়ের (যেমন Silberschatz) একটি মোটা দাগের নিয়ম: quantum এমন হোক যাতে মোটামুটি ৮০% CPU burst q-এর চেয়ে ছোট হয়। তাহলে বেশিরভাগ প্রসেস এক quantum-এর মধ্যেই কাজ শেষ করে নিজে থেকে block হয় — preemption আর বাড়তি switch কমে, অথচ লম্বা CPU-bound কাজও কাউকে দীর্ঘক্ষণ আটকে রাখতে পারে না। এটি একটি নির্দেশিকা, প্রমাণিত সূত্র নয়; burst-এর বণ্টন কাজের ধরন অনুযায়ী বদলায়। পাঠ্যবইয়ে আধুনিক সিস্টেমের quantum সাধারণত ১০–১০০ ms ধরা হয়, আর context switch-এর সময় তার তুলনায় অনেক ছোট (মাইক্রোসেকেন্ডের ঘরে)।
Turnaround q-এর সাথে সরলভাবে কমে না
অনেকে ভাবেন quantum বাড়ালে গড় turnaround নিশ্চয়ই কমবে, কারণ switch কমে। কিন্তু উপরের simulator-এর শেষ লাইনটি দেখুন — চারটি প্রসেস, সবাই t = ০-তে, burst ৬, ৩, ১, ৭ (switch-এর খরচ শূন্য ধরে):
| q | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| গড় TAT | 11 | 11.5 | 10.75 | 11.5 | 12.25 | 10.5 | 10.5 |
সংখ্যাগুলো ওঠানামা করছে! কারণটি বুঝতে q = ৩ আর q = ৫ পাশাপাশি রাখুন। q = ৩-এ P2 (৩) প্রথম চক্রেই ঠিক শেষ হয়, P3 (১)-ও; ফলে দুটি ছোট কাজ দ্রুত বিদায় নেয় (CT = ৬ আর ৭)। q = ৫-এ P1 প্রথমেই ৫ একক নিয়ে নেয়, P2 আর P3 সেই সময় অপেক্ষা করে (CT = ৮ আর ৯) — একটি বড় quantum ছোট কাজদের পিছিয়ে দিল, অথচ P1 নিজেও শেষ হলো না। শিক্ষা: গড় turnaround কমে তখন, যখন বেশিরভাগ প্রসেস একটি quantum-এর মধ্যেই শেষ হয়; q-এর সাথে এর সম্পর্ক monotonic নয়। পরীক্ষায় “q বাড়ালে TAT কমে” — এমন সাধারণ দাবি তাই ভুল।
৮ · দুটো মেলানো, আর অনুপাতে ভাগ — lottery থেকে CFS-এর পথে
Priority + Round Robin
বাস্তবের অনেক scheduler দুটো নীতি একসাথে ব্যবহার করে: সবচেয়ে উঁচু অগ্রাধিকার-স্তরে যারা আছে শুধু তারাই চলে, আর সেই স্তরের ভেতরে Round Robin. ট্রাফিক মোড়ে অ্যাম্বুলেন্স সবার আগে যায়; কিন্তু দুটি অ্যাম্বুলেন্স একসাথে এলে পুলিশ পালা করে ছাড়ে।
উদাহরণ ৪: সবাই t = ০-তে, q = ২. P1 (BT ৪, pr ১), P2 (BT ৩, pr ১), P3 (BT ৫, pr ২), P4 (BT ২, pr ২)। প্রথমে স্তর ১-এ P1 আর P2 RR-এ চলে; তারা শেষ হলে স্তর ২-এ P3 আর P4.
│ P1 0-2 │ P2 2-4 │ P1 4-6 │ P2 6-7 │ P3 7-9 │ P4 9-11 │ P3 11-13 │ P3 13-14 │
CT = ৬, ৭, ১৪, ১১. শেষে P3 একা থাকায় পরপর দুটি quantum পায় (১১–১৩, ১৩–১৪) — সারিতে আর কেউ নেই, তাই কোনো context switch-ও হয় না। Linux-এর SCHED_RR নীতি ঠিক এই নকশা: real-time priority ১–৯৯-এর প্রতিটি স্তরে আলাদা সারি, সবচেয়ে উঁচু অ-খালি স্তর চলে, আর একই স্তরের task-গুলো time slice ভাগ করে। (একই স্তরে SCHED_FIFO হলে task নিজে ছাড়া পর্যন্ত চলে।) স্তরগুলোর মধ্যে প্রসেস ওঠানামা করতে পারলে সেটি হয় multilevel feedback queue — পরের পর্বের বিষয়।
অনুপাতে ভাগ — proportional share
Priority বলে “কে আগে”; কিন্তু অনেক সময় প্রশ্নটা হলো “কে কত ভাগ”। একটি সার্ভারে দুটি বিভাগের কাজ চলছে, চুক্তি: হিসাব বিভাগ পাবে CPU-র ৬০%, মানবসম্পদ ৪০%. কঠোর priority দিয়ে এটা হয় না — উঁচু যে, সে সব নিয়ে নেয়। দরকার proportional-share scheduling.
- Lottery scheduling (Carl Waldspurger ও William Weihl, ১৯৯৪): প্রতিটি প্রসেস কিছু টিকিট পায়। প্রতিটি time slice-এর শুরুতে একটি এলোমেলো টিকিট টানা হয়; যার টিকিট, সে চলে। t টিকিটের মালিক মোট T টিকিটের মধ্যে প্রতিবার জেতার সম্ভাবনা t/T, তাই দীর্ঘমেয়াদে তার গড় ভাগ t/T. Starvation নেই — যার একটি টিকিটও আছে, তার জেতার সম্ভাবনা শূন্য নয়। দুর্বলতা: অল্প সময়ের হিসাবে ভাগ এলোমেলো।
- Stride scheduling (একই গবেষকদের, ১৯৯৫): এলোমেলোতার বদলে নির্ধারিত হিসাব। প্রত্যেকের stride = একটি বড় সংখ্যা ÷ টিকিট, আর একটি “pass” গণক। প্রতিবার যার pass সবচেয়ে ছোট সে চলে, তারপর তার pass-এ stride যোগ হয়। কম stride (বেশি টিকিট) মানে pass ধীরে বাড়ে, তাই বেশি পালা।
- Weighted fair queueing (Demers, Keshav ও Shenker, ১৯৮৯) মূলত নেটওয়ার্ক রাউটারে প্যাকেট পাঠানোর জন্য তৈরি — প্রতিটি প্রবাহ তার weight অনুপাতে লিংকের ভাগ পায়। Stride-এর pass আর WFQ-এর “virtual finish time” একই ধারণার দুই রূপ।
// Proportional share: lottery (random) vs stride (deterministic).
const tickets = { A: 50, B: 30, C: 20 }; // A should get 50% of the CPU
let seed = 12345; // fixed seed -> same output every run
const rand = () => (seed = (seed * 48271) % 2147483647) / 2147483647; // exact MINSTD generator
function lottery(slices) {
const total = Object.values(tickets).reduce((a, b) => a + b, 0), won = { A: 0, B: 0, C: 0 };
for (let i = 0; i < slices; i++) {
let r = Math.floor(rand() * total); // winning ticket number 0..total-1
for (const p in tickets) { if (r < tickets[p]) { won[p]++; break; } r -= tickets[p]; }
}
return won;
}
for (const n of [10, 100, 10000]) {
const w = lottery(n);
console.log("lottery " + String(n).padStart(5) + " slices: A=" + w.A + " B=" + w.B + " C=" + w.C +
" (A share " + (100 * w.A / n).toFixed(1) + "%)");
}
// Stride: stride = BIG / tickets; always run the smallest "pass", then pass += stride.
const BIG = 600, pass = {}, stride = {}, won = { A: 0, B: 0, C: 0 }, order = [];
for (const p in tickets) { stride[p] = BIG / tickets[p]; pass[p] = 0; }
for (let i = 0; i < 20; i++) {
const p = Object.keys(pass).sort((x, y) => pass[x] - pass[y] || (x < y ? -1 : 1))[0];
order.push(p); won[p]++; pass[p] += stride[p];
}
console.log("stride strides: A=" + stride.A + " B=" + stride.B + " C=" + stride.C);
console.log("stride order: " + order.join(""));
console.log("stride 20 slices: A=" + won.A + " B=" + won.B + " C=" + won.C);আউটপুট (নির্দিষ্ট seed, তাই প্রতিবার একই):
lottery 10 slices: A=4 B=5 C=1 (A share 40.0%)
lottery 100 slices: A=50 B=27 C=23 (A share 50.0%)
lottery 10000 slices: A=4954 B=3011 C=2035 (A share 49.5%)
stride strides: A=12 B=20 C=30
stride order: ABCABACABAABCABACABA
stride 20 slices: A=10 B=6 C=4
Lottery-তে ১০টি slice-এ A পেল মাত্র ৪০%, অথচ তার টিকিট ৫০% — এবং B পেল A-র চেয়েও বেশি! ১০,০০০ slice-এ ভাগ ৪৯.৫%, প্রায় নিখুঁত। কারণ জেতার সংখ্যা একটি binomial চলক: n slice-এ গড় n·p, আর বিচ্যুতি প্রায় √(n·p·(1−p)); আপেক্ষিক ভুল তাই 1/√n হারে কমে — দীর্ঘমেয়াদে ঠিক, স্বল্পমেয়াদে এলোমেলো। Stride-এ ২০টি slice-এ ভাগ হুবহু ১০ : ৬ : ৪ = ৫০% : ৩০% : ২০%, আর ক্রমটিও সমানভাবে ছড়ানো। Stride-এ যেকোনো দুজনের ভাগের ভুল কখনো এক slice-এর বেশি হয় না, n যত বড়ই হোক — ভুল সময়ের সাথে জমে না। একটি সতর্কতা: এখানে tie হলে নামের ক্রমে (A আগে) বাছা হয়েছে; অন্য tie-নিয়মে ক্রম সামান্য বদলাতে পারে, ভাগ নয়। (৪৮২৭১ গুণ করে ২৩১ − ১ দিয়ে ভাগশেষ নেওয়া একটি পুরোনো, সরল random-সংখ্যা জেনারেটর — শিক্ষার জন্য যথেষ্ট, নিরাপত্তার কাজে নয়।)
CFS-এর সেতু: Linux-এর Completely Fair Scheduler (CFS, ২০০৭ সালে Linux 2.6.23) stride-এর মতোই চিন্তা করে: প্রতিটি task-এর একটি “virtual runtime” — তার আসল চলার সময়কে weight দিয়ে ভাগ করে বাড়ে — আর সবসময় সবচেয়ে কম virtual runtime-এর task চলে। weight আসে nice থেকে: nice 0-এ 1024, আর nice-এর প্রতি এক ধাপে weight প্রায় ১.২৫ গুণ বদলায় (মোটামুটি ১০% CPU-র পার্থক্য)। Linux 6.6 (২০২৩) থেকে মূল নীতি EEVDF. এগুলোর পুরো ভেতরের কথা পর্ব ২১-এ।
নিজের চোখে দেখুন — Linux-এ একটি প্রসেসের nice আর kernel-এর চোখে তার priority. /proc/<pid>/stat-এর ১৮ আর ১৯ নম্বর ঘর যথাক্রমে priority আর nice; সাধারণ (non-real-time) প্রসেসে priority = nice + 20. awk এখানে /proc/self/stat পড়ছে, অর্থাৎ নিজের তথ্য।
# Fields 18 (priority) and 19 (nice) of /proc/self/stat, as seen by the kernel
awk '{print "default : priority=" $18 " nice=" $19}' /proc/self/stat
nice -n 7 awk '{print "nice -n 7 : priority=" $18 " nice=" $19}' /proc/self/stat
nice -n 7 nice -n 5 awk '{print "7 then +5 : priority=" $18 " nice=" $19}' /proc/self/stat
nice -n 50 awk '{print "nice -n 50 : priority=" $18 " nice=" $19}' /proc/self/stat
{ nice -n -5 awk '{print "nice -n -5 : priority=" $18 " nice=" $19}' /proc/self/stat; } 2>&1
ps -o pid,ni,cls,comm -p $$
echo "SCHED_RR timeslice (ms): $(cat /proc/sys/kernel/sched_rr_timeslice_ms 2>/dev/null || echo n/a)"আউটপুট (একটি আসল রান):
default : priority=30 nice=10
nice -n 7 : priority=37 nice=17
7 then +5 : priority=39 nice=19
nice -n 50 : priority=39 nice=19
nice: cannot set niceness: Permission denied
nice -n -5 : priority=30 nice=10
PID NI CLS COMMAND
2 10 TS bash
SCHED_RR timeslice (ms): 100
মজার ব্যাপার, এই runner-এর container-টি নিজেই nice 10-এ চলে (আপনার নিজের Linux-এ সাধারণত 0 দেখবেন) — তাই প্রথম লাইনে priority = 10 + 20 = 30. nice -n 7 যোগ করে 17. পরের লাইনে 10 + 7 + 5 = 22 হওয়ার কথা, কিন্তু সীমা 19, তাই kernel 19-এ আটকে দেয়; -n 50-ও একই কারণে 19. nice-এর সর্বোচ্চ 19, তাই priority-র সর্বোচ্চ 19 + 20 = 39. nice -n -5 মানে nice কমানো, অর্থাৎ অগ্রাধিকার বাড়ানো — সাধারণ ব্যবহারকারী তা পারেন না, তাই সতর্কবার্তা আসে, আর কমান্ডটি আগের nice 10-এই চলে। ps-এর CLS কলামে TS মানে সাধারণ time-sharing নীতি, অর্থাৎ SCHED_OTHER. আর শেষ লাইন দেখাচ্ছে, এই kernel-এ SCHED_RR-এর time slice 100 ms — এই পর্বের Round Robin-এর আসল quantum. Windows-এ এর কাছাকাছি কাজ Task Manager-এর “Set priority” বা PowerShell-এ (Get-Process name).PriorityClass দিয়ে, তবে সেখানে সংখ্যার দিক উল্টো।
Windows-এ ৩২টি স্তর (০–৩১, বড় সংখ্যা বেশি জরুরি): ১৬–৩১ real-time স্তর, ১–১৫ সাধারণ (variable) স্তর, আর ০ সংরক্ষিত একটি বিশেষ system thread-এর জন্য। সাধারণ স্তরে Windows dynamic priority ব্যবহার করে — I/O শেষ হলে বা জানালা সামনে এলে thread সাময়িক “boost” পায়। Windows Internals বইয়ের বর্ণনা অনুযায়ী একটি kernel thread (balance set manager) নিয়মিত খোঁজে কোন thread প্রায় ৪ সেকেন্ড ধরে ready থেকেও চলতে পারেনি, আর তাকে সাময়িকভাবে priority ১৫-তে তুলে দেয় — একেবারে বাস্তব aging, priority inversion কমানোর একটি মোটা দাগের উপায়ও।
আরও গভীরে: Linux-এ SCHED_RR-এর ডিফল্ট time slice সাধারণত ১০০ ms, যা /proc/sys/kernel/sched_rr_timeslice_ms-এ দেখা ও (root হলে) বদলানো যায়। Real-time task সাধারণ task-কে চিরকাল বঞ্চিত করতে পারে — তাই Linux ডিফল্টভাবে “RT throttling” রাখে: sched_rt_runtime_us = 950000 আর sched_rt_period_us = 1000000, অর্থাৎ প্রতি সেকেন্ডে real-time task-গুলো সর্বোচ্চ ০.৯৫ সেকেন্ড পায়, বাকি ৫% সাধারণ task-এর জন্য। এটি OS-স্তরের একটি starvation-রক্ষা। পুরোনো BSD Unix-এর scheduler প্রতি সেকেন্ডে প্রতিটি প্রসেসের সাম্প্রতিক CPU ব্যবহার ক্ষয়-সূত্রে (decay) হিসাব করে priority নতুন করে ঠিক করত — বেশি CPU খেলে নামত, অপেক্ষায় ধীরে ধীরে উঠত — যা aging আর dynamic priority-র ক্লাসিক বাস্তব রূপ।
৯ · AI-এর যুগে priority আর Round Robin
Scheduling-এর numerical-এ AI খুব আত্মবিশ্বাসী উত্তর দেয় — আর প্রায়ই নিঃশব্দে একটি নিয়ম ধরে নেয়, যা আপনার প্রশ্নের নিয়মের সাথে মেলে না। বাস্তব সিস্টেমে আরও বিপজ্জনক পরামর্শও আসে। নিচের টেবিলে সাধারণ ভুলগুলো:
| AI-এর ভুল | কেন ভুল | কীভাবে যাচাই করবেন |
|---|---|---|
| “বড় সংখ্যা = বেশি priority” ধরে নিয়ে হিসাব | প্রশ্নের নিয়ম উল্টো হলে পুরো Gantt chart ভুল | প্রম্পটে নিয়ম লিখে দিন; উত্তরের প্রথম সিদ্ধান্তটি হাতে মিলিয়ে নিন |
| RR-এ একই মুহূর্তে আগমন আর preemption হলে নিয়ম না বলে যেকোনো একটি ব্যবহার | প্রতিটি প্রসেসের CT, RT বদলে যায় (উপরে দেখেছেন) | প্রতিটি quantum-সীমায় সারির তালিকা চেয়ে নিন, simulator দিয়ে মেলান |
| Non-preemptive প্রশ্নে preemptive হিসাব (বা উল্টো) | প্রশ্নে “preemptive” শব্দ না থাকলে অনেক বইয়ে non-preemptive ধরা হয় — কিন্তু নিশ্চিত হতে হয় | নিজে নিয়ম ঠিক করে দিন; arrival-এর মুহূর্তে কে চলছে দেখুন |
| WT = CT − BT লেখা, AT বাদ দেওয়া | WT = TAT − BT = CT − AT − BT | অন্তত একটি প্রসেসের সংখ্যা হাতে কষুন |
| “q বাড়ালে turnaround সবসময় কমে” | সম্পর্ক monotonic নয় (৬, ৩, ১, ৭-এর টেবিল) | দুটি q-এর জন্য simulator চালিয়ে দেখুন |
অ্যাপ দ্রুত করতে sudo chrt -f 99 বা sudo renice -20 | Real-time ৯৯-এর একটি লুপে আটকে যাওয়া প্রোগ্রাম সেই core-এর প্রায় সব সময় দখল করতে পারে — অন্য প্রোগ্রাম, এমনকি terminal-ও সাড়া দেয় না | আগে মাপুন সমস্যাটি সত্যিই CPU-র কি না; real-time শুধু প্রমাণিত প্রয়োজনে, সীমা (RT throttling, cgroup) রেখে |
Linux-এর nice-এর সংখ্যা Windows-এর priority-র সাথে গুলিয়ে ফেলা | দিক আর পরিসর দুটোই আলাদা | প্রম্পটে OS আর সংস্করণ লিখুন; man page (man 1 nice, man 7 sched) দেখুন |
Numerical সমাধানে AI-কে নিয়ম, ফরম্যাট আর যাচাইয়ের শর্ত — তিনটিই দিন:
AI-এর উত্তর মানার আগে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Starvation কী, আর aging কীভাবে প্রতিকার করে? — উঁচু অগ্রাধিকারের কাজ আসতে থাকায় নিচেরটি কখনো CPU না পাওয়া; অপেক্ষার সাথে অগ্রাধিকার বাড়িয়ে অপেক্ষার একটি ঊর্ধ্বসীমা তৈরি করা।
- RR-এ q খুব বড় বা খুব ছোট হলে কী হয়? — বড় হলে FCFS-এর মতো আচরণ; ছোট হলে context switch-এর খরচে CPU efficiency কমে।
- n টি প্রসেস, quantum q — একটি প্রসেস সর্বোচ্চ কত অপেক্ষা করে পরের পালার জন্য? — (n − 1)·q (switch-এর খরচ বাদে)।
- Priority inversion কী, একটি সমাধান বলুন। — নিম্ন task-এর হাতে থাকা lock-এর জন্য উচ্চ task আটকে, আর মাঝারি task নিম্নটিকে সরিয়ে রাখে; সমাধান priority inheritance (বা priority ceiling).
- SJF কি priority scheduling? — হ্যাঁ, যেখানে priority = পরের CPU burst-এর অনুমিত দৈর্ঘ্য।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Mars Pathfinder-এর ঘটনা থেকে কী শেখার আছে — শুধু priority দিয়ে কি “জরুরি কাজ আগে” নিশ্চিত করা যায়?
Priority scheduling-এর প্রতিশ্রুতি সরল: যার অগ্রাধিকার বেশি, সে আগে চলবে। কিন্তু এই প্রতিশ্রুতি তখনই খাটে, যখন task-গুলো একে অপরের ওপর নির্ভর করে না। যে মুহূর্তে একটি উচ্চ task নিম্ন task-এর হাতে থাকা কোনো সম্পদের (lock, buffer, device) অপেক্ষায় থাকে, তখন উচ্চ task-এর অগ্রগতি নির্ভর করে নিম্ন task-এর অগ্রগতির ওপর — আর নিম্ন task-কে scheduler নিজের নিয়মেই পিছিয়ে রাখে। ফলে নিয়ম মেনেই নিয়মের উদ্দেশ্য ভেঙে যায়।
১৯৯৭ সালের Mars Pathfinder-এর ঘটনাটি এর পাঠ্যবই-উদাহরণ হয়ে গেছে। প্রকল্পের প্রকৌশলীদের পরে দেওয়া বিবরণ অনুযায়ী, যানটির VxWorks-চালিত কম্পিউটারে একটি উচ্চ-অগ্রাধিকারের task একটি ভাগের তথ্য-ব্যবস্থার lock চাইত, যা কখনো কখনো একটি নিম্ন-অগ্রাধিকারের আবহাওয়া-তথ্যের task-এর হাতে থাকত; মাঝারি অগ্রাধিকারের, দীর্ঘক্ষণ চলা যোগাযোগের task সেই নিম্ন task-কে সরিয়ে রাখত। উচ্চ task সময়মতো শেষ না হওয়ায় watchdog timer পুরো সিস্টেম রিসেট করত। পৃথিবীতে একই রকম একটি যন্ত্রে সমস্যাটি পুনরায় ঘটিয়ে দেখার পর, mutex-এর priority inheritance বিকল্প চালু করার একটি ছোট পরিবর্তন পাঠানো হয়েছিল বলে জানা যায়। শিক্ষণীয় দিক হলো: পরীক্ষার সময় এই সমস্যা কম দেখা গিয়েছিল, কারণ এটি ঘটে শুধু নির্দিষ্ট সময়-বিন্যাসে — concurrency-বাগের চিরচেনা চরিত্র।
এখান থেকে তিনটি নকশা-শিক্ষা আসে। প্রথমত, lock-এর সাথে priority-র যোগাযোগ রাখতে হয় — priority inheritance (মালিক সাময়িকভাবে অপেক্ষমাণদের সর্বোচ্চ priority পায়) বা priority ceiling (lock নেওয়ার সময়ই মালিককে একটি নির্ধারিত উচ্চ স্তরে তোলা)। Linux-এর kernel-এ rt_mutex আর POSIX-এর PTHREAD_PRIO_INHERIT বিকল্প এ জন্যই। দ্বিতীয়ত, critical section যত ছোট, inversion-এর সম্ভাব্য সময় তত কম। তৃতীয়ত, watchdog-এর মতো “নিরাপত্তা-জাল” দরকার, কারণ সব বাগ আগে ধরা যায় না।
মূল উপলব্ধি: Priority শুধু scheduler-এর একটি সংখ্যা নয়; সম্পদ ভাগাভাগির সাথে মিশলে এটি একটি পুরো-সিস্টেমের বৈশিষ্ট্য। বিস্তারিত সমাধান আর তাদের দাম পর্ব ২৯-এ দেখবেন।
প্র ০২Round Robin কি সত্যিই “ন্যায্য”? CPU-bound আর I/O-bound প্রসেস একসাথে থাকলে কে বেশি পায়?
RR-এর ন্যায্যতা একটি নির্দিষ্ট অর্থে: প্রতি পালায় প্রত্যেকে সর্বোচ্চ একই q পায়। কিন্তু “সর্বোচ্চ” শব্দটিই ফাঁক। একটি I/O-bound প্রসেস (যেমন একটি editor বা ডেটাবেস-ক্লায়েন্ট) সাধারণত q-এর অনেক আগেই I/O-র জন্য block হয় — ধরুন ২ ms চলে, তারপর ডিস্ক বা নেটওয়ার্কের অপেক্ষা। I/O শেষ হলে সে আবার সারির পেছনে দাঁড়ায়। এদিকে CPU-bound প্রসেসগুলো (যেমন ভিডিও এনকোডিং) প্রতিবার পুরো q, ধরুন ২০ ms, ব্যবহার করে। তিনটি CPU-bound আর একটি I/O-bound থাকলে এক চক্রে CPU-bound তিনটি পায় ৬০ ms, I/O-bound পায় মাত্র ২ ms — অথচ তার I/O শেষ হয়ে বসে থাকে প্রায় ৬০ ms. ফলে I/O ডিভাইসও অলস থাকে, আর interactive প্রোগ্রাম ধীর মনে হয়।
এই অন্যায্যতা নিয়ে পাঠ্যবইয়ে কয়েকটি প্রতিকার আছে। William Stallings-এর বইয়ে বর্ণিত “virtual round robin”-এ I/O শেষ হওয়া প্রসেসগুলো একটি সহায়ক সারিতে যায়, যা মূল সারির আগে সেবা পায়, আর আগের পালার না-ব্যবহৃত অংশটুকুই সে পায়। আরও সাধারণ সমাধান multilevel feedback queue (পর্ব ১৯): যে প্রসেস quantum ফুরানোর আগেই CPU ছেড়ে দেয়, সে উঁচু স্তরে থাকে; যে পুরো quantum খায়, সে নিচে নামে। ফলে scheduler আচরণ দেখে নিজেই শিখে নেয় কে interactive. Windows-এর I/O-শেষে priority boost আর পুরোনো BSD-র CPU-ব্যবহার-ভিত্তিক priority একই উদ্দেশ্যে।
CFS-এর মতো proportional-share scheduler আবার ন্যায্যতার সংজ্ঞাই বদলায়: সমান “quantum” নয়, সমান (বা weight-অনুপাতে) মোট CPU সময়। যে প্রসেস বেশি ঘুমিয়েছে, তার virtual runtime কম, তাই জেগে উঠলে সে দ্রুত CPU পায় — I/O-bound প্রসেস এতে স্বাভাবিকভাবেই সুবিধা পায়, তবে একটি সীমার মধ্যে, যাতে দীর্ঘ ঘুমের পর কেউ সবাইকে আটকে না রাখে।
মূল উপলব্ধি: “ন্যায্য” শব্দের একাধিক অর্থ — প্রতি পালার সমান সুযোগ, সমান মোট সময়, নাকি সমান সাড়া। কোন সংজ্ঞা বেছে নিচ্ছেন, সেটিই scheduler-এর নকশা ঠিক করে।
প্র ০৩Real-time সিস্টেম কেন এখনো কঠোর static priority পছন্দ করে, যখন ডেস্কটপ আর সার্ভার dynamic বা proportional-share-এর দিকে গেছে?
দুই জগতের লক্ষ্য আলাদা। ডেস্কটপ বা সার্ভারে লক্ষ্য গড়ে ভালো সাড়া, বেশি throughput আর কাউকে বঞ্চিত না করা — কখন কোন প্রোগ্রাম চলবে আগে থেকে জানা নেই, তাই OS-কে আচরণ দেখে মানিয়ে নিতে হয়। Real-time সিস্টেমে (গাড়ির এয়ারব্যাগ, পেসমেকার, কারখানার রোবট-বাহু, বিমানের নিয়ন্ত্রণ) লক্ষ্য deadline কখনো না ফসকানো। সেখানে গড় নয়, সবচেয়ে খারাপ অবস্থাই গুরুত্বপূর্ণ, আর সেই সবচেয়ে খারাপ অবস্থা আগে থেকে প্রমাণ করা চাই।
Static priority এই প্রমাণকে সম্ভব করে। প্রতিটি task-এর সর্বোচ্চ চলার সময় আর কত ঘন ঘন সে আসে জানা থাকলে, নির্দিষ্ট priority-ক্রমে প্রতিটি task-এর সবচেয়ে খারাপ response time হিসাব করা যায় (response-time analysis). ১৯৭৩ সালে Liu আর Layland দেখান, পর্যায়ক্রমিক task-এ ছোট period-কে বেশি priority দিলে (rate-monotonic) একটি সরল CPU-ব্যবহারের সীমার নিচে সব deadline রক্ষা নিশ্চিত — পর্ব ২০-এ এর পুরো প্রমাণ দেখবেন। Dynamic priority বা lottery-র এলোমেলোতায় এমন নিশ্চয়তা দেওয়া কঠিন, কারণ priority সময়ের সাথে কোন পথে বদলাবে, তা আগে থেকে বাঁধা নয়।
কিন্তু static priority-র দামও আছে, আর তা এই পর্বের বিষয়গুলোই — starvation আর priority inversion. Real-time জগতে starvation অনেক সময় ইচ্ছাকৃত: কম জরুরি কাজ পরে হবে, এটাই নকশা। তবে সাধারণ-উদ্দেশ্যের OS-এ real-time task চালালে সমস্যা দেখা দেয়; তাই Linux ডিফল্টভাবে RT throttling রাখে (প্রতি সেকেন্ডে ৯৫%-এর বেশি নয়), যাতে একটি ভুল real-time প্রোগ্রাম পুরো মেশিন অচল না করে। ২০২৪ সালে PREEMPT_RT অংশ মূল Linux-এ (6.12) যুক্ত হওয়ায় সাধারণ Linux-ও এখন কঠোর latency-র কাজে আরও উপযোগী। অনেক ক্ষুদ্র যন্ত্রে আবার FreeRTOS বা Zephyr-এর মতো RTOS-এ সমান priority-র task-দের মধ্যে time slicing (অর্থাৎ priority + RR) একটি কনফিগারযোগ্য বিকল্প।
মূল উপলব্ধি: নীতি বাছাই নির্ভর করে আপনি কী নিশ্চিত করতে চান তার ওপর। গড় ভালো চাইলে মানিয়ে-নেওয়া নীতি; প্রমাণযোগ্য সবচেয়ে খারাপ অবস্থা চাইলে সরল, কঠোর, অনুমানযোগ্য নীতি।
প্র ০৪Lottery আর stride scheduling গবেষণাপত্র থেকে বাস্তবে কীভাবে পৌঁছাল? Proportional share আজ কোথায় কোথায় দেখা যায়?
১৯৯৪ সালে MIT-র Carl Waldspurger আর William Weihl lottery scheduling প্রকাশ করেন। মূল ধারণাটি ছিল একটি সহজ বিমূর্ততা — “টিকিট” — যা দিয়ে সম্পদের ভাগ প্রকাশ করা যায়। টিকিটের সুবিধা শুধু ভাগ নির্ধারণ নয়: টিকিট হস্তান্তর করা যায় (একটি client সার্ভারের উত্তরের অপেক্ষায় থাকলে নিজের টিকিট সার্ভারকে ধার দিতে পারে — priority inversion-এর এক ধরনের সমাধান), আর একটি দলের ভেতরে আলাদা “মুদ্রা” তৈরি করে দলের ভেতরের ভাগ দলের বাইরের ভাগ থেকে আলাদা রাখা যায়। এলোমেলোতার স্বল্পমেয়াদি অসমতা দূর করতে পরের বছর তাঁরা stride scheduling প্রস্তাব করেন, যা একই অনুপাত নির্ধারিতভাবে দেয়।
এর পূর্বসূরি নেটওয়ার্ক জগতে: ১৯৮৯ সালের weighted fair queueing রাউটারে প্রতিটি প্রবাহকে তার weight অনুযায়ী লিংকের ভাগ দিত, প্রতিটি প্যাকেটের একটি “virtual finish time” হিসাব করে। CPU-তে stride-এর pass, WFQ-এর virtual time, আর Linux CFS-এর virtual runtime — তিনটিই একই মূল চিন্তা: প্রত্যেকের জন্য একটি ঘড়ি রাখা, যা তার weight অনুযায়ী ধীরে বা দ্রুত চলে, আর সবচেয়ে পিছিয়ে থাকা ঘড়ির মালিককে সেবা দেওয়া।
বাস্তবে proportional share আজ সর্বত্র। Linux-এর CFS আর তার উত্তরসূরি EEVDF nice থেকে weight নেয়; cgroup-এর cpu.weight (আগের cpu.shares) দিয়ে container-দের মধ্যে CPU ভাগ হয় — Kubernetes-এ একটি pod-এর CPU request শেষ পর্যন্ত এমন weight-এ রূপ নেয়। Hypervisor-এও একই ধারণা: VMware-এর মতো প্ল্যাটফর্মে virtual machine-দের CPU-র “shares” দেওয়া যায়, আর Xen-এর credit scheduler-ও weight-ভিত্তিক। লক্ষণীয়, Waldspurger নিজে পরে VMware-এ কাজ করেছেন। নেটওয়ার্কে Linux-এর traffic control-এর বিভিন্ন queueing discipline fair queueing-এর বংশধর।
মূল উপলব্ধি: একটি ভালো বিমূর্ততা (টিকিট, weight, virtual time) এক ক্ষেত্র থেকে আরেক ক্ষেত্রে চলে যায় — রাউটার থেকে CPU, CPU থেকে container আর virtual machine. পর্ব ২১-এ CFS পড়ার সময় এই সূত্রটি মাথায় রাখুন।
প্র ০৫বাস্তব OS-এ quantum কে ঠিক করে, আর একই মেশিনে কেন সব প্রসেসের quantum এক নয়?
পাঠ্যবইয়ের RR-এ q একটি স্থির সংখ্যা, সবার জন্য এক। বাস্তবে প্রায় কোনো বড় OS এমন করে না, কারণ একই মেশিনে একসাথে চলে নানা স্বভাবের প্রোগ্রাম — browser, compiler, গান, ব্যাকআপ। প্রত্যেকের জন্য আদর্শ quantum আলাদা।
Windows: Windows Internals-এর বর্ণনা অনুযায়ী quantum মাপা হয় clock tick-এর এককে; client সংস্করণে (Windows 10/11) ডিফল্ট quantum ছোট (প্রায় ২ tick), আর server সংস্করণে লম্বা (প্রায় ১২ tick) — কারণ সার্ভারে অনুরোধ-সামলানো thread-কে একবারে বেশি সময় দিলে switch কমে, throughput বাড়ে। ডেস্কটপে সামনের (foreground) জানালার প্রসেসের thread-কে লম্বা quantum দেওয়ার বিকল্পও আছে, যাতে আপনি যে অ্যাপ ব্যবহার করছেন সেটি বেশি সাড়া দেয়।
Linux: সাধারণ task-এর জন্য CFS-এ কোনো স্থির quantum ছিল না। একটি “target latency” — যত সময়ের মধ্যে সব runnable task অন্তত একবার চলবে — task-সংখ্যা আর weight দিয়ে ভাগ করে প্রতিটির slice হিসাব হতো, একটি ন্যূনতম সীমার নিচে নয়, যাতে অনেক task থাকলেও switch-এর খরচ সীমা ছাড়ায় না। EEVDF-এ task নিজের পছন্দের slice জানাতে পারে। শুধু SCHED_RR real-time task-এ পাঠ্যবইয়ের মতো স্থির slice (সাধারণত ১০০ ms).
এই পর্বের ৮০% নিয়মটি এখানে কাজে আসে, তবে ভিন্ন রূপে: আধুনিক scheduler প্রসেসের সাম্প্রতিক burst দেখে অনুমান করে, আর slice এমনভাবে ঠিক করে যাতে interactive প্রসেস একবারেই কাজ শেষ করে ঘুমাতে পারে। অডিও বা গেমের মতো কম-latency কাজ আবার ছোট slice আর ঘন ঘন সুযোগ চায়; ব্যাচ কাজ লম্বা slice-এ cache উষ্ণ রেখে বেশি কাজ করে। তাছাড়া turnaround যে q-এর সাথে monotonic নয়, তা থেকেও বোঝা যায় — একটি “সেরা” q খুঁজে লাভ নেই; কাজের মিশ্রণ বদলালেই সেরা q বদলায়।
মূল উপলব্ধি: Quantum একটি নীতি-সিদ্ধান্ত, যা OS কাজের ধরন, সংস্করণ (client বনাম server) আর লক্ষ্য (সাড়া বনাম throughput) অনুযায়ী বদলায়। পাঠ্যবইয়ের স্থির q হলো ধারণা বোঝার মডেল, বাস্তবের হুবহু ছবি নয়।
অনুশীলন
হাতে হিসাব, সারির ট্রেস, আউটপুট অনুমান আর কোড — আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।
-
হাতে হিসাব (priority): P1 (AT ০, BT ৬, pr ৩), P2 (AT ২, BT ২, pr ১), P3 (AT ৩, BT ৩, pr ২), P4 (AT ৫, BT ১, pr ১)। ছোট সংখ্যা = বেশি অগ্রাধিকার, সমান হলে আগে আসা আগে। Non-preemptive আর preemptive — দুটোর Gantt chart, প্রতিটি প্রসেসের WT আর গড় WT ও গড় TAT বের করুন।
Non-preemptive: │ P1 0-6 │ P2 6-8 │ P4 8-9 │ P3 9-12 │ Preemptive: │ P1 0-2 │ P2 2-4 │ P3 4-5 │ P4 5-6 │ P3 6-8 │ P1 8-12 │Non-preemptive: t = ৬-এ P2 (১, এসেছে ২-এ) আর P4 (১, এসেছে ৫-এ) সমান — আগে আসা P2. CT = ৬, ৮, ১২, ৯; TAT = ৬, ৬, ৯, ৪ → গড় ৬.২৫; WT = ০, ৪, ৬, ৩ → গড় ৩.২৫.
Preemptive: t = ২-এ P2 এসে P1-কে সরায়; t = ৪-এ P3 (২) P1 (৩)-এর আগে; t = ৫-এ P4 (১) এসে P3-কে সরায়। CT = ১২, ৪, ৮, ৬; TAT = ১২, ২, ৫, ১ → গড় ৫; WT = ৬, ০, ২, ০ → গড় ২. (উপরের
priority_sim.js-এ ডেটা বসালে হুবহু এই সংখ্যা আসে।) -
সারির ট্রেস (RR): q = ৩. P1 (AT ০, BT ৭), P2 (AT ১, BT ৪), P3 (AT ৩, BT ২), P4 (AT ৬, BT ৩)। নিয়ম: একই মুহূর্তে নতুন আগে, সময়-ফুরানো পরে। প্রতিটি quantum-সীমায় ready queue, Gantt chart, আর গড় TAT, WT, RT বের করুন। তারপর বলুন উল্টো নিয়মে গড়গুলো কী হয়।
queue: 3:[P2 P3 P1] 6:[P3 P1 P4 P2] 8:[P1 P4 P2] 11:[P4 P2 P1] 14:[P2 P1] 15:[P1] 16:[] │ P1 0-3 │ P2 3-6 │ P3 6-8 │ P1 8-11 │ P4 11-14 │ P2 14-15 │ P1 15-16 │t = ৩ আর t = ৬ — দুবারই tie (P3 আর P4 ঠিক quantum-সীমায় আসে)। CT = ১৬, ১৫, ৮, ১৪; TAT = ১৬, ১৪, ৫, ৮ → গড় ১০.৭৫; WT = ৯, ১০, ৩, ৫ → গড় ৬.৭৫; RT = ০, ২, ৩, ৫ → গড় ২.৫.
উল্টো নিয়মে (পুরোনোটি আগে): │P1 0-3│P2 3-6│P1 6-9│P3 9-11│P2 11-12│P4 12-15│P1 15-16│ → CT = ১৬, ১২, ১১, ১৫; গড় TAT ১১, গড় WT ৭, গড় RT ৩.৫. এই ডেটায় নিয়ম বদলালে গড়ও বদলায়।
-
আউটপুট অনুমান: একটি সাধারণ (root নয়) Linux ব্যবহারকারী, যার shell-এর nice ০, এই তিনটি কমান্ড চালালেন। প্রতিটি কী ছাপবে? (ক)
nice -n 3 nice -n 4 nice(খ)nice -n 25 nice(গ)nice -n -1 niceশুধু
nice(কোনো কমান্ড ছাড়া) বর্তমান nice ছাপে। (ক) 7 — nice যোগ হয়: ০ + ৩ + ৪. (খ) 19 — সীমা ১৯-এর ওপরে যায় না, kernel চুপচাপ ১৯-এ আটকে দেয়। (গ) প্রথমে stderr-এ একটি সতর্কবার্তা (nice: cannot set niceness: Permission denied), তারপর 0 — সাধারণ ব্যবহারকারী সাধারণত nice কমিয়ে (অর্থাৎ অগ্রাধিকার বাড়িয়ে) দিতে পারেন না (RLIMIT_NICE সীমা অনুমতি না দিলে); GNUniceসতর্ক করে, কিন্তু কমান্ডটি আগের nice-এই চালায়। অংশ ৮-এর bash উদাহরণে একই আচরণ দেখেছেন। -
কোড লিখুন: অংশ ৬-এর
rr_sim.jsবদলে উল্টো tie-নিয়ম (সময়-ফুরানো প্রসেস আগে, একই মুহূর্তে আসা নতুনটি পরে) চালু করুন। q = ২-এ নতুন Gantt chart আর গড় RT কত আসা উচিত?tবাড়ানোর পরের দুটি লাইন বদলে তিনটি লাইন — আগে যারা t-এর আগে এসেছে, তারপর preempted প্রসেস, তারপর ঠিক t-তে আসা:t += run; p.left -= run; while (next < ps.length && ps[next].at < t) queue.push(ps[next++]); // arrived strictly before t if (p.left > 0) queue.push(p); else p.ct = t; // preempted first admit(); // arrivals exactly at t go behindq = ২-এ ফল: │P1 0-2│P2 2-4│P1 4-6│P3 6-7│P4 7-9│P2 9-10│P5 10-12│P1 12-13│P5 13-14│; সারি t = ২-এ [P2 P1 P3]. গড় TAT ৮.৬ আর গড় WT ৫.৮ (আগের মতোই), কিন্তু গড় RT ৩.০ (আগে ২.৪) — অংশ ৫-এর টেবিলের সাথে মেলে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমRound Robin-এ একটি প্রসেসের একটি পালা — quantum ফুরানো পর্যন্ত — সঠিক ক্রমে সাজান (নিয়ম: একই মুহূর্তে নতুন আগমন আগে সারিতে)। ▲▼ চেপে সরান।
- Scheduler ready queue-র সামনের প্রসেসটি বের করে
- Dispatcher তাকে CPU দেয়, timer q সময়ের জন্য বসে
- প্রসেসটি q সময় চলে, কিন্তু তার burst শেষ হয় না
- Timer interrupt আসে, kernel প্রসেসটিকে থামায়
- ঠিক এই মুহূর্তে আসা নতুন প্রসেস সারির পেছনে যোগ হয়
- থামানো প্রসেসটি তার পরে, সারির একদম পেছনে যোগ হয়
Round Robin-এ time quantum q যদি সব প্রসেসের সবচেয়ে বড় CPU burst-এর চেয়েও বড় হয়, তাহলে scheduling কার মতো আচরণ করে?
- SJF
- FCFS
- SRTF
- Priority scheduling
Preemptive priority scheduling (ছোট সংখ্যা = বেশি অগ্রাধিকার, সমান হলে আগে আসা আগে)। P1 (AT 0, BT 4, pr 3), P2 (AT 1, BT 3, pr 1), P3 (AT 2, BT 5, pr 4), P4 (AT 3, BT 2, pr 2). P1-এর completion time কত?
- 4
- 6
- 9
- 14
Priority scheduling-এ aging কোন সমস্যার প্রতিকার?
- নিম্ন-অগ্রাধিকারের প্রসেসের starvation
- Context switch-এর খরচ
- FCFS-এর convoy effect
- Deadlock
Round Robin, q = 2. P1 (AT 0, BT 5) আর P2 (AT 2, BT 2). নিয়ম: একই মুহূর্তে নতুন প্রসেস আসা আর quantum ফুরানো ঘটলে নতুনটি আগে সারিতে ঢোকে। P2-এর completion time কত?
- 4
- 6
- 2
- 7
উচ্চ-অগ্রাধিকারের task H একটি lock-এর জন্য অপেক্ষা করছে, যা নিম্ন-অগ্রাধিকারের L-এর হাতে; এদিকে মাঝারি M চলছে আর L-কে CPU পেতে দিচ্ছে না। এই অবস্থার নাম আর একটি প্রচলিত সমাধান কোনটি?
- Deadlock; সমাধান সব lock একই ক্রমে নেওয়া
- Starvation; সমাধান aging
- Convoy effect; সমাধান SJF
- Priority inversion; সমাধান priority inheritance
Lottery scheduling-এ A-র 50, B-র 30 আর C-র 20টি টিকিট। দীর্ঘ সময় ধরে চললে B মোটামুটি CPU-র কত ভাগ পাবে, আর মাত্র 10টি time slice-এর ক্ষেত্রে কী বলা যায়?
- ঠিক 30%, প্রতি 10টি slice-এ ঠিক 3টি
- দীর্ঘমেয়াদে প্রায় 30%, কিন্তু 10টি slice-এ অনেক কম-বেশি হতে পারে
- প্রায় 0%, কারণ A সবসময় জেতে
- প্রায় 33%, কারণ তিনটি প্রসেস
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Priority scheduling: সবচেয়ে বেশি অগ্রাধিকারের ready প্রসেস চলে; কোন দিকের সংখ্যা বেশি জরুরি (এই পর্বে ছোট) আর tie-নিয়ম সবসময় লিখে দিন। SJF এর একটি বিশেষ রূপ।
- Preemptive-এ বেশি অগ্রাধিকারের আগমন সঙ্গে সঙ্গে CPU নেয়; non-preemptive-এ burst শেষ হওয়া পর্যন্ত অপেক্ষা।
- Starvation ঠেকায় aging (অপেক্ষার সাথে অগ্রাধিকার বাড়ে, অপেক্ষার ঊর্ধ্বসীমা তৈরি হয়); lock-এর সাথে priority মিশলে priority inversion, প্রতিকার priority inheritance.
- RR: বৃত্তাকার FIFO, প্রত্যেকে সর্বোচ্চ q; একই মুহূর্তে নতুন আগমন আগে, preempted পরে (প্রথা ভিন্ন হতে পারে)। অপেক্ষা ≤ (n − 1)·q; q → ∞ হলে FCFS, খুব ছোট q-তে overhead; প্রায় ৮০% burst q-এর চেয়ে ছোট — মোটা দাগের নিয়ম; turnaround q-এর সাথে monotonic নয়।
- Priority + RR (Linux SCHED_RR), আর অনুপাতে ভাগ — lottery (এলোমেলো, দীর্ঘমেয়াদে ঠিক), stride (নির্ধারিত), WFQ — যা থেকে CFS-এর virtual runtime.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৯ · Multilevel queue আর multilevel feedback queueপরবর্তী পর্বএকাধিক সারি, স্তরের মধ্যে ওঠানামা আর আচরণ দেখে শেখা scheduler.
- অপারেটিং সিস্টেম · কোর্স হোম৬৪টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ০৯ · Context switch আর dispatcherপেছনে ফিরেSwitch-এর খরচ আর CPU efficiency q/(q + s)-এর পূর্ণ হিসাব।
- পর্ব ১৭ · FCFS, SJF আর SRTFপেছনে ফিরেGantt chart-এর ভিত্তি, convoy effect আর SJF-এর optimality.
- DSA · Queue আর dequeডেটা স্ট্রাকচারবৃত্তাকার সারি — RR-এর ready queue-র পেছনের ডেটা স্ট্রাকচার।
- DSA · Heap আর priority queueডেটা স্ট্রাকচারসবচেয়ে বেশি অগ্রাধিকারের প্রসেস দ্রুত খুঁজে বের করার কাঠামো।