এই পর্বে যা শিখবেন
- Linux scheduler কীভাবে O(n) থেকে O(1), তারপর CFS আর EEVDF-এ এল — প্রতিটি পরিবর্তন কোন সমস্যার উত্তর
- vruntime, weight আর nice-এর হিসাব হাতে করা — CPU-র ভাগ, slice, আর “এক ধাপে ১০%” নিয়মের প্রমাণ
- Red-black tree-তে বাঁয়ের node বাছাই, targeted latency, ঘুম থেকে ফেরা আর নতুন task-কে কোথায় বসানো হয়
- EEVDF-এর lag, eligibility আর virtual deadline — ছোট উদাহরণে CFS-এর সাথে তুলনা
- Scheduling class, policy,
ps/top-এর PRI আর NI, autogroup, cgroup v2, Kubernetes-এর CPU limit আর sched_ext — বাস্তবে যাচাই করে
১ · পাঠ্যবইয়ের নিয়ম থেকে আসল kernel — সমস্যাটা কোথায়
পর্ব ১৬ থেকে ২০ পর্যন্ত আপনি পাঠ্যবইয়ের scheduler-গুলো দেখেছেন: FCFS, SJF, SRTF (পর্ব ১৭), priority আর Round Robin (পর্ব ১৮), multilevel feedback queue (পর্ব ১৯), আর বহু-CPU ও real-time scheduling (পর্ব ২০)। প্রতিটির একটি পরিষ্কার নিয়ম আছে, আর Gantt chart এঁকে হিসাব মেলানো যায়। কিন্তু আপনার ফোন, ল্যাপটপ বা সার্ভারের Linux kernel-কে প্রতিদিন যা সামলাতে হয়, তা কোনো একটি পাঠ্যবইয়ের নিয়মে ধরে না।
একটি সাধারণ Linux ল্যাপটপে একই সময়ে কয়েকশো process আর হাজারখানেক thread থাকে: ব্রাউজার, যেখানে মাউস নাড়ালে সাথে সাথে সাড়া চাই; ভিডিও কল, যার প্রতিটি ফ্রেমের সময়সীমা আছে; পেছনে কোড compile — যে যত CPU পায় তত খুশি। সার্ভারে একই যন্ত্রে বিশটি container, প্রত্যেকে টাকা দিয়ে নির্দিষ্ট পরিমাণ CPU কিনেছেন। আর Android ফোনেও একই kernel, যেখানে ব্যাটারি প্রধান।
Scheduler-এর কাছে তাই চাওয়া অনেক, আর কিছু চাওয়া পরস্পরবিরোধী:
- ন্যায্যতা: একই গুরুত্বের কাজ সমান CPU পাবে; কেউ starvation-এ পড়বে না।
- সাড়া (latency): কিবোর্ড, অডিওর মতো ছোট কিন্তু জরুরি কাজ দ্রুত CPU পাবে।
- Throughput: বড় কাজ বারবার থামানো যাবে না — প্রতিটি context switch-এর দাম আছে (পর্ব ০৯)।
- নিয়ন্ত্রণ আর মাপযোগ্যতা: nice, real-time policy, cgroup দিয়ে ভাগ বদলানো যাবে; আর ২ থেকে কয়েকশো CPU-তে একই kernel চলবে।
পুরান ঢাকার একটি রেস্তোরাঁর রান্নাঘরে একটিমাত্র বড় চুলা, আর সামনে অনেক অর্ডার। কোনোটি এক কাপ চা (দুই মিনিটের কাজ, কিন্তু খদ্দের দাঁড়িয়ে আছেন), কোনোটি বিয়েবাড়ির দশ ডেগ বিরিয়ানি (ঘণ্টার পর ঘণ্টা)। বাবুর্চি যদি বিরিয়ানি শেষ না করে চা না বানান, চায়ের খদ্দের চলে যাবেন। আবার প্রতি মিনিটে ডেগ নামিয়ে চা বসালে বিরিয়ানি কখনো শেষ হবে না। আর যিনি বেশি টাকা দিয়ে “জরুরি” অর্ডার দিয়েছেন, তাঁকে বেশি সময় দিতেই হবে — কিন্তু বাকিদের একেবারে ভুলে গেলে চলবে না। Linux-এর scheduler-এর গল্পটা আসলে এই বাবুর্চির খাতার গল্প: কে কতক্ষণ চুলা পেল, তার হিসাব কীভাবে রাখা হয়।
এই পর্বে সেই খাতাটি খুলব: ইতিহাস, CFS-এর হিসাব, nice থেকে weight, ২০২৩ সালের EEVDF, তারপর scheduling class, cgroup আর container — প্রতিটি দাবি আসল Linux-এ চালিয়ে।
আদর্শ ন্যায্য CPU কল্পনা করুন: n-টি task প্রত্যেকে একই সাথে CPU-র ১/n গতিতে চলছে। বাস্তবে CPU একবারে একজনকেই চালায়। আধুনিক Linux scheduler-এর কাজ এই আদর্শের যত কাছে সম্ভব থাকা — কে আদর্শের তুলনায় কতটা পিছিয়ে বা এগিয়ে, তার হিসাব রেখে। CFS এই হিসাবের নাম দিয়েছে vruntime, EEVDF দিয়েছে lag.
২ · তিন যুগের ইতিহাস — O(n), O(1), আর ন্যায্যতার বিতর্ক
প্রথম যুগ: O(n) scheduler (১৯৯১–২০০৩ মোটামুটি)
Linux ২.৪ পর্যন্ত সব runnable task ছিল একটি বৈশ্বিক তালিকায়; প্রতিবার schedule() পুরো তালিকা ঘুরে প্রত্যেকের একটি “goodness” নম্বর হিসাব করত। প্রতিটি task-এর counter-এ থাকত কত tick বাকি; সবার counter শূন্য হলে নতুন “epoch”, আর সবার — ঘুমন্তদেরও — counter হতো প্রায় counter/2 + priority. ঘুমন্তদের বাকি counter-ই ছিল ইন্টারঅ্যাকটিভ কাজের বাড়তি সুবিধা। সমস্যা: প্রতিটি সিদ্ধান্ত O(n), আর একটি lock-এর জন্য সব CPU-র কাড়াকাড়ি — বড় সার্ভারে গুরুতর বাধা।
দ্বিতীয় যুগ: Ingo Molnár-এর O(1) scheduler (২.৬.০, ২০০৩)
২০০২ সালের শুরুতে Ingo Molnár ২.৫ উন্নয়ন-শাখায় একটি নতুন scheduler আনেন, যা ডিসেম্বর ২০০৩-এ প্রকাশিত Linux ২.৬.০-এর অংশ হয়। নাম O(1) scheduler, কারণ task যতই থাকুক, পরের task বাছতে সময় স্থির। কৌশলটি সুন্দর:
- প্রতিটি CPU-র নিজের run queue — বৈশ্বিক lock-এর কাড়াকাড়ি শেষ।
- ১৪০টি priority স্তর — ০–৯৯ real-time, ১০০–১৩৯ সাধারণ task (nice −২০ থেকে ১৯). প্রতিটি স্তরের জন্য একটি আলাদা তালিকা, আর ১৪০-bit-এর একটি bitmap, যার bit বলে কোন স্তরে কেউ অপেক্ষায় আছে।
- দুটি array — active আর expired. কেউ নিজের timeslice শেষ করলে expired-এ যায়। Active খালি হলে দুটি pointer অদলবদল — পুরো epoch-এর পুনর্হিসাব এক নিমেষে।
- বাছাই: bitmap-এ প্রথম set bit খোঁজা (CPU-র bit-খোঁজার instruction দিয়ে কয়েক ধাপেই হয়), সেই স্তরের তালিকার প্রথম task — O(1).
Timeslice দেওয়া হতো static priority অনুযায়ী — nice 0-তে প্রায় ১০০ ms. আসল জটিলতা ছিল ইন্টারঅ্যাকটিভিটির অনুমান: প্রতিটি task-এর sleep_avg (কতটা ঘুমায়) দেখে বেশি ঘুমানো task-এর dynamic priority ৫ ধাপ পর্যন্ত বাড়ত, কম ঘুমানোর কমত; “ইন্টারঅ্যাকটিভ” task slice শেষেও আবার active-এ ঢুকতে পারত। এই heuristic গড়ে ভালো, কিন্তু ভিডিও প্লেয়ার বা গেমের মতো কাজে — যা অনেক CPU খায় আবার নিয়মিত ঘুমায় — অনুমান কখনো মিলত, কখনো না; কিছু প্রোগ্রাম কৌশলে বাড়তি priority আদায়ও করত। ভুল সারাতে heuristic-এর ওপর heuristic জমতে থাকে।
Con Kolivas আর ন্যায্যতার বিতর্ক (২০০৪–২০০৭): অস্ট্রেলিয়ার একজন anaesthetist (চিকিৎসক) অবসরে desktop-এর জন্য নিজের -ck patchset বানাতেন। তাঁর যুক্তি: অনুমান বাদ দিয়ে কঠোর ন্যায্য, পূর্বানুমেয় নিয়ম বানালেই desktop মসৃণ হবে। প্রথমে Staircase scheduler, তারপর মার্চ ২০০৭-এ RSDL (Rotating Staircase Deadline), পরে নাম বদলে SD. RSDL-এ কোনো sleep-অনুমান ছিল না; task priority-র “সিঁড়ি” বেয়ে নামত, প্রতি স্তরে নির্দিষ্ট কোটা নিয়ে। Linus Torvalds সহ অনেকে আগ্রহ দেখান। এপ্রিল ২০০৭-এ Ingo Molnár কয়েক দিনের মধ্যে লেখা একটি সম্পূর্ণ নতুন scheduler পোস্ট করেন — CFS — যা Kolivas-এর ন্যায্যতার ধারণাকে স্বীকৃতি দিয়েছিল, কিন্তু ভিন্ন কৌশলে। শেষে CFS-ই গৃহীত হয়, আর Kolivas সে বছরই মূলধারার kernel কাজ থেকে সরে দাঁড়ান। পরে তিনি BFS (২০০৯) আর MuQSS বানান। ঘটনাটি নিয়ে আলোচনা আজও হয় (ভাবনার প্রশ্ন ১)।
তৃতীয় যুগ: CFS (২.৬.২৩, অক্টোবর ২০০৭) — আর তার উত্তরসূরি EEVDF (৬.৬, ২০২৩)
CFS অক্টোবর ২০০৭-এ Linux ২.৬.২৩-এ আসে আর প্রায় ষোল বছর টিকে থাকে। ঘোষণা ছিল বৈপ্লবিক: timeslice নেই, sleep-অনুমান নেই, priority array নেই — শুধু একটি সংখ্যা, আদর্শ ন্যায্য CPU-র তুলনায় কে কতটা চলেছে। ২০২৩ সালের শেষে Linux ৬.৬-এ Peter Zijlstra CFS-এর বাছাইয়ের নিয়মটি EEVDF দিয়ে বদলে দেন; vruntime-এর হিসাব, weight, tree — বেশিরভাগ কাঠামো রয়ে গেছে (অংশ ৬)।
| যুগ | কাঠামো | বাছাইয়ের খরচ | ইন্টারঅ্যাকটিভিটি কীভাবে | মূল দুর্বলতা |
|---|---|---|---|---|
| O(n) (২.৪ পর্যন্ত) | একটি বৈশ্বিক তালিকা, epoch | O(n), একটি lock | ঘুমন্তদের বাকি counter-এর বাড়তি | বহু task ও বহু CPU-তে ধীর |
| O(1) (২.৬.০–২.৬.২২) | প্রতি-CPU queue, ১৪০ স্তর, active/expired | O(1) | sleep_avg heuristic, ±৫ bonus | অনুমান ভুল হলে অনিশ্চিত আচরণ |
| CFS (২.৬.২৩–৬.৫) | প্রতি-CPU red-black tree, vruntime | বাছাই O(1), insert O(log n) | কম চলা task-এর vruntime কম, তাই সে আগে | latency নিয়ন্ত্রণে নানা জোড়াতালি |
| EEVDF (৬.৬ থেকে) | একই tree, lag আর virtual deadline | O(log n) | ছোট slice = আগের deadline | নতুন; কিছু workload-এ আচরণ বদলেছে |
৩ · CFS-এর হৃদয় — virtual runtime
CFS প্রতিটি task-এর (kernel-এর ভাষায় scheduling entity) জন্য একটি সংখ্যা রাখে: vruntime, ন্যানোসেকেন্ডে। Task যখন CPU-তে চলে, তার vruntime বাড়ে:
এখানে 1024 হলো nice 0-এর weight (kernel-এ NICE_0_LOAD). তাই nice 0-এর task ১ ms চললে vruntime-ও ১ ms বাড়ে; বেশি weight-এর task-এর ধীরে, কম weight-এর দ্রুত। বাছাইয়ের নিয়ম: যে runnable task-এর vruntime সবচেয়ে কম, সে-ই পরের পালা পাবে।
যে task কম CPU পেয়েছে, তার vruntime পিছিয়ে, তাই সে আগে বাছাই হয়ে ধরে ফেলে। দীর্ঘ সময়ে সবার vruntime প্রায় সমান — মানে প্রত্যেকে weight-এর অনুপাতে আসল সময় পেয়েছে (অংশ ৪-এ প্রমাণ)। “ইন্টারঅ্যাকটিভ” অনুমানও লাগে না: টেক্সট এডিটর বেশিরভাগ সময় ঘুমায়, তাই জেগে উঠলেই সে সারির সামনে।
ক্রিকেটের নেট প্র্যাকটিসে একজন কোচ একটি খাতায় লেখেন, কে কত মিনিট ব্যাট করল। নিয়ম সহজ: খাতায় যার মিনিট সবচেয়ে কম, পরের পালা তার। জাতীয় দলের ক্যাম্পে থাকা খেলোয়াড়ের জন্য কোচ নিয়ম করলেন — তাঁর আসল ৩ মিনিট খাতায় লেখা হবে ১ মিনিট; তাই তিনি অন্যদের প্রায় তিন গুণ সময় পান। চা খেয়ে ফেরা খেলোয়াড়ের (ঘুমন্ত task) মিনিট কম, তাই তিনি সাথে সাথে পালা পান। খাতাটিই vruntime, আর “আসল ৩ = খাতায় ১” নিয়মটিই weight.
সবচেয়ে কম vruntime দ্রুত খুঁজতে CFS প্রতিটি CPU-র runnable task-গুলো একটি red-black tree-তে রাখে, vruntime-কে key করে। এটি স্ব-ভারসাম্য binary search tree (DSA-র red-black tree পর্ব): insert, delete O(log n), সবচেয়ে ছোট key বাঁয়ের node-এ, আর kernel সেই node-এর pointer আলাদা রাখে (rb_root_cached) — বাছাই O(1).
নিচের ডায়াগ্রামে একটি CPU-তে তিনটি সবসময়-ব্যস্ত task: A আর B nice 0 (weight 1024), C nice 5 (weight 335). সহজ করার জন্য scheduler প্রতি ১ ms tick-এ সিদ্ধান্ত নেয়, আর vruntime সমান হলে যে আগে সারিতে ঢুকেছে সে আগে। C ১ ms চললে তার vruntime বাড়ে 1024 ÷ 335 ≈ ৩.০৫৭ ms.
| সময় | |
| A (w 1024) | |
| B (w 1024) | |
| C (w 335) | |
| এইমাত্র যে চলল | |
| পরের পালা (বাঁয়ের node) | |
| মোট CPU (ms) A : B : C |
- শুরু — সবাই শূন্যে। তিনটি task-এর vruntime 0. গাছের in-order ক্রম A, B, C (সমান key-তে আগে আসা বাঁয়ে), তাই বাঁয়ের node A. (সহজ করার জন্য চলমান task-কেও গাছে দেখানো হলো; আসল kernel চলার সময় task-টিকে গাছ থেকে বের করে রাখে, পালা শেষে আবার ঢোকায়।)
- A ১ ms চলল। A-এর weight 1024, তাই vruntime বাড়ল ১ × 1024 ÷ 1024 = ১.০০০. A আবার গাছে ঢুকল, এবার ডান দিকে। ক্রম: B 0, C 0, A 1 — বাঁয়ের node B.
- B ১ ms চলল। B-ও ১.০০০-এ। ক্রম: C 0, A 1, B 1 — এবার C-এর পালা, যদিও তার weight কম। কম weight মানে কম ভাগ, “কখনো না” নয়।
- C ১ ms চলল — vruntime লাফ দিল ৩.০৫৭-এ। 1024 ÷ 335 ≈ ৩.০৫৭, তাই C-এর খাতায় আসল ১ ms লেখা হলো প্রায় ৩ ms. ক্রম: A 1, B 1, C 3.057 — বাঁয়ে আবার A.
- A আর B একবার করে। দুজনেই ২.০০০-এ, তবু C-এর ৩.০৫৭-এর চেয়ে কম। তাই C অপেক্ষা করছে, আর A আবার বাঁয়ে।
- A, B, A, B — তারপর C. চার tick পর A আর B ৪.০০০-এ, C তখনো ৩.০৫৭-এ। এখন C সবচেয়ে বাঁয়ে। C-এর একবারের “ঋণ” শোধ হতে A আর B-কে তিনবার করে চলতে হলো।
- দশ tick-এ ফল ৪ : ৪ : ২. C আবার ১ ms চলল, vruntime ৬.১১৩. দশ ms-এ ভাগ ৪০% : ৪০% : ২০% — তত্ত্বের 42.97% : 42.97% : 14.06% থেকে দূরে, কারণ এত অল্প সময়ে ১ ms-এর দানাদারিই বড়; দীর্ঘ সময়ে ভাগ তত্ত্বে পৌঁছায় — নিচের simulator দেখাবে।
এবার একই নিয়ম অনেকক্ষণ চালিয়ে দেখি। নিচের JavaScript simulator প্রতি tick-এ সবচেয়ে কম vruntime-এর task বেছে নেয়, kernel-এর মতো পূর্ণসংখ্যায় (ন্যানোসেকেন্ডে) vruntime বাড়ায়, আর বিভিন্ন সময়ে কে কত শতাংশ CPU পেল ছাপায়:
// CFS in miniature: 1 CPU, 3 always-runnable tasks, the scheduler decides every 1 ms tick
const NICE_0_LOAD = 1024, TICK = 1000000; // tick in nanoseconds
const tasks = [
{ name: "A", nice: 0, w: 1024 },
{ name: "B", nice: 0, w: 1024 },
{ name: "C", nice: 5, w: 335 }, // sched_prio_to_weight[nice + 20]
].map(t => ({ ...t, vruntime: 0, ran: 0 }));
let picks = "", done = 0;
function tick() {
let cur = tasks[0]; // "leftmost": smallest vruntime wins
for (const t of tasks) if (t.vruntime < cur.vruntime) cur = t;
cur.ran++;
cur.vruntime += Math.floor(TICK * NICE_0_LOAD / cur.w); // calc_delta_fair()
if (picks.length < 21) picks += cur.name;
done++;
}
const W = tasks.reduce((s, t) => s + t.w, 0);
const pct = x => (100 * x).toFixed(2).padStart(6) + "%";
for (const upto of [21, 100, 1000, 10000]) {
while (done < upto) tick();
console.log(`after ${String(done).padStart(5)} ticks: ` +
tasks.map(t => `${t.name} ${pct(t.ran / done)}`).join(" "));
}
console.log("theory w / total : " + tasks.map(t => `${t.name} ${pct(t.w / W)}`).join(" "));
console.log("first 21 picks : " + picks);
console.log("vruntime now (ms) : " +
tasks.map(t => `${t.name} ${(t.vruntime / 1e6).toFixed(3)}`).join(" "));আউটপুট:
after 21 ticks: A 42.86% B 42.86% C 14.29%
after 100 ticks: A 43.00% B 43.00% C 14.00%
after 1000 ticks: A 43.00% B 42.90% C 14.10%
after 10000 ticks: A 42.97% B 42.97% C 14.06%
theory w / total : A 42.97% B 42.97% C 14.06%
first 21 picks : ABCABABABCABABABCABAB
vruntime now (ms) : A 4297.000 B 4297.000 C 4297.743
(১) দশ হাজার tick-এ ভাগ হুবহু তত্ত্বের সাথে মিলে গেছে — 1024 ÷ 2383 = 42.97% আর 335 ÷ 2383 = 14.06%. (২) পালার ধরন ABC ABABAB C ABABAB C… — C প্রতি সাত পালায় একবার, কারণ তার এক পালা খাতায় প্রায় তিন ms; একুশ পালায় তিনবার = ১৪.২৯%. (৩) শেষে তিনজনের vruntime প্রায় সমান (≈ ৪২৯৭ ms), যদিও C আসলে চলেছে মাত্র ১৪০৬ ms — vruntime সমান রাখা আর weight-অনুপাতে ভাগ করা একই কথা।
৪ · nice থেকে weight — “এক ধাপে ১০%” নিয়ম
পর্ব ১১-এ nice আর renice চালিয়েছেন: −২০ থেকে ১৯, ডিফল্ট ০, বেশি মানে “ভদ্র”। কিন্তু nice 5 মানে ঠিক কতটা কম CPU? উত্তর kernel/sched/core.c-এর ৪০ ঘরের সারণি sched_prio_to_weight[]:
const int sched_prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};নিয়মটি হলো: nice 0 = 1024, আর প্রতি ধাপে weight প্রায় ১.২৫ গুণ বদলায় (1024 ÷ 820 ≈ ১.২৪৯, 820 ÷ 655 ≈ ১.২৫২…)। কেন ১.২৫? Kernel-এর মন্তব্যেই উদ্দেশ্য লেখা: একটি CPU-নির্ভর task nice 0 থেকে nice 1-এ গেলে, nice 0-তে থাকা আরেকটি CPU-নির্ভর task-এর তুলনায় সে মোটামুটি ১০% কম CPU পাবে। নিচের stepper-এ হিসাবটি দেখবেন। প্রভাবটি আপেক্ষিক ও গুণোত্তর: যেকোনো nice থেকে এক ধাপ সরলে একই প্রভাব, কারণ অনুপাত সব জায়গায় ১.২৫.
ধরুন একটি CPU-তে n-টি task সবসময় runnable, weight w1 … wn, মোট W. একটি লম্বা সময় T-তে task i আসল সময় পেল ti, তাই তার vruntime বাড়ল ti × 1024 ÷ wi. CFS সবসময় সবচেয়ে ছোট vruntime-কে চালায়, তাই যেকোনো দুই task-এর vruntime-এর পার্থক্য একটি স্থির সীমার (মোটামুটি একটি slice-এর) বেশি হতে পারে না — যে পিছিয়ে, সে না ধরা পর্যন্ত বাছাই হতে থাকে। তাহলে T বড় হলে সব task-এর vruntime-বৃদ্ধি প্রায় সমান: ti ÷ wi ≈ tj ÷ wj = একটি ধ্রুবক k. যোগ করলে Σti = T = k × W, তাই k = T ÷ W, আর ti ÷ T ≈ wi ÷ W. ভুল একটি slice-এর ক্রমের স্থির সংখ্যা, তাই T বাড়লে আপেক্ষিক ভুল শূন্যের দিকে যায় — simulator-এ যেমন দেখলেন।
এবার পুরো হিসাব ধাপে ধাপে — পরীক্ষার ধাঁচের numerical: একটি CPU-তে A, B (nice 0) আর C (nice 5); শেষ দুই ধাপে দুটি বিখ্যাত তুলনা।
| প্রশ্ন | |
| A | |
| B | |
| C | |
| ফল |
- সারণি থেকে weight. nice মানকে index বানাতে ২০ যোগ: nice 0 → ঘর ২০ → 1024; nice 5 → ঘর ২৫ → 335. সম্পর্কটি রৈখিক নয়, গুণোত্তর।
- মোট weight. W = 1024 + 1024 + 335 = 2383. গোনা হয় শুধু এই CPU-র runnable task — কেউ ঘুমালে W কমে, বাকিদের ভাগ বাড়ে।
- CPU-র ভাগ = নিজের weight ÷ মোট. A ও B প্রত্যেকে 1024 ÷ 2383 ≈ 42.97%, C 335 ÷ 2383 ≈ 14.06%. simulator ঠিক এই সংখ্যাই দেখিয়েছিল।
- Slice — একটি period-এর মধ্যে কতক্ষণ। ক্লাসিক CFS-এর “targeted latency” period-এ (ভিত্তি ৬ ms; অংশ ৫) প্রত্যেকের slice = period × ভাগ: A ও B ২.৫৭৮ ms, C ০.৮৪৩ ms — যোগফল ৬ ms.
- vruntime-এর গতি. আসল ১ ms চললে A ও B-এর খাতায় ১ ms, C-এর খাতায় 1024 ÷ 335 ≈ ৩.০৫৭ ms. তাই একই slice-এর হিসাবে দেখলে: A-এর ২.৫৭৮ ms মানে vruntime ২.৫৭৮; C-এর ০.৮৪৩ ms মানে vruntime ০.৮৪৩ × ৩.০৫৭ ≈ ২.৫৭৮ — period শেষে সবার vruntime সমান বাড়ে। এটিই ন্যায্যতার সংজ্ঞা।
- দশ শতাংশের নিয়ম. দুটি task, একটি nice 0, অন্যটি nice 1: W = 1844, ভাগ ৫৫.৫৩% বনাম ৪৪.৪৭%. সমান ভাগ (৫০ : ৫০) থেকে একজন প্রায় ৫ শতাংশ-বিন্দু বেশি, অন্যজন প্রায় ৫ কম — পার্থক্য প্রায় ১১, কাছাকাছি “১০%”. আর weight-এর অনুপাত 1.25.
- দশ ধাপের পার্থক্য. nice 0 বনাম nice 10: 1024 ÷ 110 ≈ ৯.৩১, অর্থাৎ ১.২৫১০ ≈ ৯.৩১. ভাগ ৯০.৩০% বনাম ৯.৭০%. চরম প্রান্তে nice −২০ বনাম ১৯: 88761 ÷ 15 ≈ ৫৯১৭ গুণ — nice 19-এর task পায় মাত্র ০.০১৭%. পরের C প্রোগ্রামে এই অনুপাত আসল kernel-এ মাপব।
এবার আসল kernel-কে পরীক্ষা। প্রোগ্রামটি নিজেকে একটি CPU-তে বাঁধে (CPU affinity, পর্ব ২০), তারপর দুটি child বানায় — affinity উত্তরাধিকারে যায়, তাই দুজনে একই CPU-র জন্য লড়বে। Child ২ nice 10 ধাপ বাড়ায়। দুজনেই একই মুহূর্ত পর্যন্ত (প্রায় ১ সেকেন্ড) গণনা চালিয়ে pipe দিয়ে জানায় কত কাজ করল।
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <sys/wait.h>
static const int weight[40] = { /* sched_prio_to_weight[], nice -20 .. 19 */
88761, 71755, 56483, 46273, 36291, 29154, 23254, 18705, 14949, 11916,
9548, 7620, 6100, 4904, 3906, 3121, 2501, 1991, 1586, 1277,
1024, 820, 655, 526, 423, 335, 272, 215, 172, 137,
110, 87, 70, 56, 45, 36, 29, 23, 18, 15 };
static double now(void) {
struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t);
return t.tv_sec + t.tv_nsec / 1e9;
}
int main(void) {
cpu_set_t one; CPU_ZERO(&one); CPU_SET(sched_getcpu(), &one);
if (sched_setaffinity(0, sizeof one, &one) != 0) return 1; /* children inherit */
int fd[2]; long r[2][3], t[3]; if (pipe(fd) != 0) return 1;
double end = now() + 1.0; /* both children stop at the same moment */
for (long k = 0; k < 2; k++)
if (fork() == 0) {
long ni = nice(k == 0 ? 0 : 10); /* child 1 keeps its nice, child 2: +10 */
volatile unsigned long n = 0;
while (now() < end) for (int i = 0; i < 100000; i++) n++;
long rec[3] = { k, ni, (long)(n / 100000) };
_exit(write(fd[1], rec, sizeof rec) == sizeof rec ? 0 : 1);
}
for (int k = 0; k < 2; k++) { wait(NULL); if (read(fd[0], t, sizeof t) == sizeof t) for (int j = 0; j < 3; j++) r[t[0]][j] = t[j]; }
for (int k = 0; k < 2; k++)
printf("child %d: nice %3ld weight %5d work %ld units\n", k + 1, r[k][1], weight[r[k][1] + 20], r[k][2]);
printf("measured work ratio : %.2f\n", (double)r[0][2] / r[1][2]);
printf("weight ratio (theory): %.2f\n", (double)weight[r[0][1] + 20] / weight[r[1][1] + 20]);
return 0;
}একটি আসল রানের আউটপুট:
child 1: nice 10 weight 110 work 8665 units
child 2: nice 19 weight 15 work 1238 units
measured work ratio : 7.00
weight ratio (theory): 7.33
প্রথমেই অবাক হওয়ার কথা: nice 0 আর 10 নয়, 10 আর 19! এই অনলাইন সার্ভার সব process আগে থেকেই nice 10-এ চালায় (পর্ব ১১), তাই child ২-এর 10 + 10 = 20 আটকে গেল 19-এ — nice() নতুন মান ফেরত দেয়, প্রোগ্রাম সেটিই ছেপেছে; তত্ত্বের অনুপাত 110 ÷ 15 ≈ ৭.৩৩. মাপা অনুপাত ৭.০০ — প্রায় ৫ শতাংশের মধ্যে মিল (আরেকটি রানে পেয়েছিলাম ৭.২৯)। নিজের Linux যন্ত্রে (শুরু nice 0) পাবেন nice 0 আর 10, অনুপাত ৯-এর আশেপাশে (তত্ত্বে ≈ ৯.৩১)।
প্রতি রানে সংখ্যা একটু বদলাবে। তিনটি নকশা লক্ষ করুন: (১) একই CPU-তে বাঁধা — না বাঁধলে দুই CPU-র যন্ত্রে দুজনেই পুরো একটি CPU পেত, অনুপাত হতো ১; nice কাজ করে শুধু প্রতিযোগিতায়। (২) একই শেষ-মুহূর্ত end, fork-এর আগে হিসাব করা। (৩) ফল pipe দিয়ে, ছাপানো এক জায়গা থেকে — কোন child আগে শেষ হয় অনিশ্চিত, তাই child-এর নম্বর রেখে parent সাজিয়ে ছেপেছে।
nice-এর প্রভাব পরম নয়, আপেক্ষিক — আর কাদের মধ্যে আপেক্ষিক, তা অংশ ৮-এ autogroup আর cgroup বদলে দেবে। আরেকটি সাধারণ ভুল: “nice 19 মানে কখনো চলবে না”। না — কেউ না থাকলে nice 19-এর task-ও পুরো CPU পায়। পুরোপুরি অলস সময়েই চলা কাজের জন্য আছে SCHED_IDLE policy (অংশ ৭)। আর nice ডিস্কের অগ্রাধিকার বদলায় না — তার জন্য ionice.
৫ · CFS-এর ভেতরের কলকবজা — slice, min_vruntime, ঘুম আর জন্ম
“সবচেয়ে কম vruntime চলবে” নিয়মটি একা চললে দুটি সমান task প্রতি মুহূর্তে অদলবদল করত, আর সব সময় যেত context switch-এ। আবার নতুন বা অনেকক্ষণ ঘুমিয়ে থাকা task-এর vruntime কত ধরা হবে? এই প্রশ্নগুলোর উত্তরই CFS-এর বাস্তব নকশা।
Targeted latency আর minimum granularity
ক্লাসিক CFS একটি targeted latency (sched_latency_ns) রাখত: এই সময়ের মধ্যে প্রতিটি runnable task অন্তত একবার চলবে। ভিত্তি-মান ৬ ms, আর CPU-র সংখ্যা অনুযায়ী বাড়ানো হতো (গুণক ১ + log2(CPU-সংখ্যা), সর্বোচ্চ ৮ CPU পর্যন্ত গোনা — ৮ বা বেশি CPU-তে ২৪ ms). প্রতিটি task-এর slice = period × নিজের weight ÷ মোট weight — ঠিক আগের stepper-এর ধাপ ৪। কিন্তু task অনেক হলে slice অসম্ভব ছোট হয়ে যেত, তাই ছিল একটি তলা: minimum granularity (ভিত্তি ০.৭৫ ms). Runnable task ৮-এর বেশি হলে (৬ ÷ ০.৭৫ = ৮) period-ই বাড়ানো হতো: period = task-সংখ্যা × ০.৭৫ ms. — অনেক task-এ latency ছেড়ে throughput বাঁচানো। আর wakeup granularity (ভিত্তি ১ ms): জেগে ওঠা task চলমান task-কে সরাবে কেবল vruntime-এর পার্থক্য এর বেশি হলে।
মানগুলো পুরোনো kernel-এ /proc/sys/kernel/sched_latency_ns-এর মতো ফাইলে ছিল; Linux ৫.১৩ থেকে debugfs-এ (/sys/kernel/debug/sched/) সরানো হয়, কারণ অনেকে ইন্টারনেটের পরামর্শে অন্ধভাবে বদলাতেন। EEVDF-এ latency-র মানটিই নেই, আছে base_slice_ns. (সংখ্যাগুলো সংস্করণভেদে বদলেছে।)
min_vruntime — খাতার “আজকের তারিখ”
প্রতিটি CPU-র run queue (cfs_rq) একটি min_vruntime রাখে — মোটামুটি সবচেয়ে ছোট vruntime-এর মান, যা শুধু সামনে এগোয়, কখনো পেছায় না। নতুন বা জেগে ওঠা task-কে বসানোর সময় এটিই মাপকাঠি।
ঘুম থেকে ফেরা — sleeper fairness
এক ঘণ্টা ঘুমানো task পুরোনো vruntime নিয়ে জাগলে সবার চেয়ে এক ঘণ্টা “পিছিয়ে” — অনেকক্ষণ সে একাই CPU দখল করত! তাই CFS জেগে ওঠা task-এর vruntime বসাত প্রায় max(নিজের vruntime, min_vruntime − sched_latency/2)-এ: অর্ধেক latency period-এর বাড়তি সুবিধা, যাতে জেগেই দ্রুত চলতে পারে — কিন্তু অসীম ঋণ নয়। এই নীতির নাম sleeper fairness, আর “অর্ধেক” করার পতাকাটির নাম ছিল GENTLE_FAIR_SLEEPERS.
নতুন task — fork করে কৌশল ঠেকানো
নতুন task-এর vruntime 0 দিলে একটি প্রোগ্রাম বারবার fork করে CPU দখল করতে পারত। তাই CFS নতুন task-কে বসাত min_vruntime-এর মোটামুটি একটি slice পরে (START_DEBIT পতাকা)। আর child আর parent-এর কে আগে চলবে? CFS-যুগে sched_child_runs_first-এর ডিফল্ট ছিল 0, অর্থাৎ সাধারণত parent চলতে থাকে — পর্ব ১০-এর fork-এর আউটপুটের ক্রম কেন অনিশ্চিত, তার একটি কারণ এখানে।
নিচের কোড দুটি মূল ফাংশনের সরলীকৃত রূপ — নাম, ফিল্ড আর যুক্তি আসল kernel-এর, শুধু lock, পরিসংখ্যান, group scheduling আর বিশেষ ক্ষেত্র বাদ দেওয়া। প্রতিটি timer tick-এ, task ঘুমাতে গেলে বা জেগে উঠলে update_curr() ডাকা হয়।
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec = now - curr->exec_start; /* শেষ হিসাবের পর কত ns চলল */
curr->exec_start = now;
curr->sum_exec_runtime += delta_exec; /* আসল সময়ের মোট */
curr->vruntime += calc_delta_fair(delta_exec, curr);
/* = delta_exec * NICE_0_LOAD / curr->load.weight */
update_min_vruntime(cfs_rq); /* শুধু সামনে এগোয় */
}
static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
{
/* rb_root_cached বাঁয়ের node মনে রাখে, তাই খোঁজা O(1) */
struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);
return rb_entry(left, struct sched_entity, run_node);
/* আসল কোডে next/last/skip "buddy" নামের কিছু ব্যতিক্রম ছিল:
যেমন সদ্য জাগানো task-কে cache গরম থাকতে আগে চালানো */
}calc_delta_fair আসলে ভাগ করে না — আগে থেকে হিসাব করা উল্টো মান (sched_prio_to_wmult[]) দিয়ে গুণ আর bit-shift করে, কারণ ভাগ ধীর। আর tick-এ চলমান task slice পেরোলে তার ওপর need_resched পতাকা বসে, kernel থেকে ফেরার পথে schedule() নতুন বাছাই করে (পর্ব ০৯)।
৬ · EEVDF (Linux ৬.৬, ২০২৩) — lag, eligibility আর virtual deadline
CFS-এর কাছে একটি প্রশ্নের পরিষ্কার উত্তর ছিল না: দুটি task-এর ভাগ সমান, কিন্তু একজনের দরকার ঘন ঘন ছোট পালা (অডিও), অন্যজনের লম্বা পালা (compile) — কাকে কখন? nice শুধু ভাগ বদলায়, latency নয়; এর জন্য জমেছিল জোড়াতালি — wakeup granularity, sleeper-এর বাড়তি, buddy-নিয়ম। ২০১৯ থেকে প্রস্তাবিত “latency-nice” মানের অর্থও CFS-এর কাঠামোয় পরিষ্কার করা যাচ্ছিল না।
Peter Zijlstra ২০২৩ সালে একটি পুরোনো গবেষণাপত্রে ফিরে যান: Ion Stoica আর Hussein Abdel-Wahab-এর ১৯৯৫ সালের Earliest Eligible Virtual Deadline First. বাছাইয়ের অংশটি তিনি এর ওপর নতুন করে লেখেন, যা Linux ৬.৬-এ (অক্টোবর ২০২৩) আসে। Weight, vruntime, tree, SCHED_OTHER — সব একই; বদলেছে “কাকে বাছব” আর “কতক্ষণ চালাব”।
তিনটি ধারণা
- V — গড় vruntime: runnable task-দের vruntime-এর weight-ভারিত গড়, V = Σ wi vi ÷ Σ wi. আদর্শ ন্যায্য CPU-তে এই মুহূর্তে সবার vruntime যা হওয়ার কথা, V তা-ই (kernel-এ
avg_vruntime())। - Lag: lagi = wi × (V − vi) — আদর্শে task-টির যতটা পাওয়ার কথা, তার চেয়ে কত কম পেয়েছে। ধনাত্মক মানে পাওনা আছে, ঋণাত্মক মানে বেশি নিয়ে ফেলেছে। Eligible শুধু তারা, যাদের lag ≥ ০, অর্থাৎ vi ≤ V.
- Virtual deadline: di = vi + slicei × 1024 ÷ wi — task-টি নিজের চাওয়া slice পুরোটা পেলে খাতায় কোথায় পৌঁছাবে। ছোট slice চাওয়া task-এর deadline কাছে।
Eligibility ন্যায্যতা পাহারা দেয় — ভাগের বেশি নেওয়া task-কে অপেক্ষা করতে হয়, deadline যত কাছেই হোক; deadline latency ঠিক করে — সীমার মধ্যে যে ছোট পালা চায়, সে আগে পায়। CFS-এ দুটি একটি সংখ্যায় মেশানো ছিল।
V-এর সংজ্ঞা থেকে Σ wi(V − vi) = V·Σwi − Σwivi = ০, অর্থাৎ সব lag-এর যোগফল শূন্য। শূন্য যোগফলের সব সংখ্যা ঋণাত্মক হতে পারে না, তাই অন্তত একটি lag ≥ ০ — eligible তালিকা কখনো খালি নয়। বিশেষত সবচেয়ে কম vruntime-এর task (CFS যাকে বাছত) সবসময় eligible, কারণ সবচেয়ে ছোট মান গড়ের চেয়ে বড় হতে পারে না। Stoica ও Abdel-Wahab-এর পত্রে আরও শক্ত ফল আছে: আদর্শ ভাগ থেকে প্রতিটি task-এর বিচ্যুতি (lag) একটি slice-এর ক্রমের সীমায় বাঁধা থাকে।
একটি উদাহরণে তিন নিয়ম পাশাপাশি: CFS (সবচেয়ে কম vruntime), শুধু-deadline (eligibility ছাড়া) আর EEVDF. চারটি task, সবার weight 1024 (তাই virtual slice = আসল slice), একক ms; D একটি অডিও thread, slice ০.৫ ms.
| A (slice 6) | |
| B (slice 3) | |
| C (slice 3) | |
| D (slice 0.5) | |
| V (গড়) | |
| CFS বাছত | |
| শুধু deadline বাছত | |
| EEVDF বাছল |
- শুরুর অবস্থা। চারটি task, weight সমান। vruntime: A ৮, B ১০, C ১৩, D ১১. চাওয়া slice: A ৬ (বড় batch কাজ), B ও C ৩, D ০.৫ (অডিও)। কে চলবে?
- V হিসাব। weight সমান, তাই সাধারণ গড়: (৮ + ১০ + ১৩ + ১১) ÷ ৪ = ৪২ ÷ ৪ = ১০.৫ — আদর্শ CPU-তে সবার vruntime এখন এটাই হতো।
- Lag আর eligibility. lag = V − v (weight 1024-কে এক একক ধরে): A +২.৫, B +০.৫, C −২.৫, D −০.৫. যোগফল শূন্য। Eligible শুধু A আর B; C আর D ভাগের চেয়ে এগিয়ে।
- Virtual deadline. d = v + slice: A ৮ + ৬ = ১৪, B ১০ + ৩ = ১৩, C ১৩ + ৩ = ১৬, D ১১ + ০.৫ = ১১.৫. সবচেয়ে কাছের deadline D-এর।
- তিন নিয়ম, তিন উত্তর। CFS নেয় সবচেয়ে কম vruntime — A. শুধু-deadline নেয় D (১১.৫) — কিন্তু D ইতিমধ্যে ভাগের বেশি পেয়েছে; এভাবে ছোট slice চেয়ে যে কেউ CPU দখল করত। EEVDF eligible {A, B}-এর মধ্যে আগের deadline নেয় — B (১৩): B-এর slice ছোট, দ্রুত শেষ করে ছেড়ে দেবে।
- B পুরো slice (৩ ms) চলল। B-এর v = ১৩, deadline পৌঁছে গেছে, তাই নতুন deadline ১৩ + ৩ = ১৬. নতুন V = (৮ + ১৩ + ১৩ + ১১) ÷ ৪ = ১১.২৫. এখন D-এর lag +০.২৫ — সে eligible হয়ে গেল! Eligible {A, D}-এর মধ্যে D-এর deadline ১১.৫ আগে, তাই D চলবে। CFS এখনো A-ই বাছত।
- D ০.৫ ms চলল। D-এর v = ১১.৫, নতুন deadline ১২. V = (৮ + ১৩ + ১৩ + ১১.৫) ÷ ৪ = ১১.৩৭৫. D-এর lag −০.১২৫, তাই অযোগ্য, যদিও deadline (১২) সবচেয়ে কাছে। একমাত্র eligible A — A চলবে। ছোট slice D-কে দ্রুত পালা দিয়েছে, ভাগের বেশি নয়।
Kernel-এ EEVDF কীভাবে লেখা
Tree এখনো red-black tree, তবে augmented: প্রতিটি node তার subtree-র একটি সারাংশ রাখে, তাই “eligible-দের মধ্যে আগের deadline” O(log n)-এ মেলে (প্রথম সংস্করণে tree vruntime-ক্রমে, পরে deadline-ক্রমে — বিস্তারিত সংস্করণভেদে বদলেছে)। V-এর জন্য kernel Σ wi(vi − min_vruntime) আর Σ wi জমা রাখে।
/* eligible? v <= V, ভাগ না করে: (v - min_v) * Σw <= Σ w*(v_i - min_v) */
static int entity_eligible(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
s64 avg = cfs_rq->avg_vruntime; /* Σ w_i * (v_i - min_vruntime) */
long load = cfs_rq->avg_load; /* Σ w_i */
return avg >= (s64)(se->vruntime - cfs_rq->min_vruntime) * load;
}
/* update_curr()-এর শেষে ডাকা হয়: slice শেষ হলে নতুন deadline */
static void update_deadline(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
if ((s64)(se->vruntime - se->deadline) < 0)
return; /* এখনো slice বাকি */
se->deadline = se->vruntime + calc_delta_fair(se->slice, se);
resched_curr(rq_of(cfs_rq)); /* আবার বাছাই */
}
/* pick_eevdf(): eligible entity-দের মধ্যে সবচেয়ে আগের deadline —
augmented rbtree-তে O(log n) খোঁজা; কেউ না মিললে বাঁয়ের node */EEVDF-এ “কতক্ষণ চালাব”-ও বদলেছে: period ভাগ করে slice নয়, প্রতিটি task-এর নিজের slice (ডিফল্ট base_slice_ns; অংশ ৭-এর রানে se.slice 1400000, অর্থাৎ ১.৪ ms), deadline পেরোলেই আবার বাছাই। Task-প্রতি আলাদা slice চাওয়া এসেছে পরে — যতদূর জানা যায়, প্রায় ৬.১২ থেকে sched_setattr()-এর sched_runtime দিয়ে, একটি সীমার মধ্যে। “latency-nice” মানটি এই লেখা পর্যন্ত মূলধারায় আসেনি — নিজের kernel-এর Documentation/scheduler/sched-eevdf.rst দেখে নিশ্চিত হোন।
সব task-এর slice সমান হলে EEVDF-এর বাছাই CFS-এর খুব কাছাকাছি, তাই বেশিরভাগ ব্যবহারকারী পরিবর্তন টেরই পাননি। ৬.৬-এর পর কিছু workload-এ গতির পরিবর্তনের খবর এসেছিল, আর পরের সংস্করণে নিয়ম ঘষামাজা হয়েছে — যেমন ঘুমন্ত task-এর lag কিছুক্ষণ ধরে রাখা (“delayed dequeue”).
৭ · Scheduling class আর policy — ps আর top-এর সংখ্যা পড়া
এ পর্যন্ত যা দেখলেন — CFS বা EEVDF — তা Linux-এর একমাত্র scheduler নয়, শুধু সাধারণ task-এর। Kernel-এ কয়েকটি scheduling class আছে — প্রত্যেকে struct sched_class-এ ফাংশন pointer-এর একটি টেবিল — কঠোর ক্রমে সাজানো। schedule() ওপরের class থেকে খোঁজে: এই class-এ কেউ runnable আছে কি না; প্রথম যেখানে পায়, সেই task চলে।
| ক্রম | Class | Policy (user যা দেখেন) | কার জন্য |
|---|---|---|---|
| ১ (সবার ওপরে) | stop | — (user-এর জন্য নয়) | প্রতি CPU-তে kernel-এর একটি বিশেষ thread; task অন্য CPU-তে সরানো বা CPU বন্ধের মতো কাজে সবাইকে থামিয়ে দেয় |
| ২ | deadline | SCHED_DEADLINE | runtime/deadline/period দিয়ে বর্ণিত real-time কাজ, EDF আর CBS দিয়ে (পর্ব ২০) |
| ৩ | rt | SCHED_FIFO, SCHED_RR | স্থির priority ১–৯৯-এর real-time task (পর্ব ২০) |
| ৪ | fair | SCHED_OTHER (= SCHED_NORMAL), SCHED_BATCH, SCHED_IDLE | প্রায় সব সাধারণ প্রোগ্রাম — CFS/EEVDF |
| (৪ আর ৫-এর মাঝে) | ext | SCHED_EXT | BPF প্রোগ্রামে লেখা scheduler, Linux ৬.১২ থেকে (অংশ ৯) |
| ৫ (সবার নিচে) | idle | — | প্রতি CPU-র idle task — কেউ না থাকলে CPU-কে ঘুমের অবস্থায় পাঠায় |
ক্রমটির মানে কঠোর: একটি runnable SCHED_FIFO task থাকলে fair class-এর কেউ সেই CPU পায় না — nice −২০-ও না। (তবে ডিফল্টভাবে real-time task-রা প্রতি ১ সেকেন্ডে সর্বোচ্চ ০.৯৫ সেকেন্ড পায়, sched_rt_runtime_us; বাকি ৫% সাধারণ task-এর জন্য — ভুল real-time কোড পুরো যন্ত্র আটকে ফেলবে না।) Fair class-এর ভেতরের তিনটি policy:
- SCHED_OTHER — ডিফল্ট; nice অনুযায়ী weight.
- SCHED_BATCH — একই weight, কিন্তু scheduler ধরে নেয় task-টি CPU-নির্ভর আর latency-তে উদাসীন: জেগে উঠে সে চলমান task-কে সরায় না, তাই context switch কম, cache ভালো থাকে। রাতের ব্যাকআপ, ডেটা প্রক্রিয়াকরণের জন্য।
- SCHED_IDLE — weight মাত্র ৩, nice 19-এর (15) চেয়েও অনেক কম। প্রায় শুধু তখনই চলে, যখন আর কেউ নেই।
PR, NI, PRI — তিনটি সংখ্যা, তিন রকম নিয়ম
পর্ব ১১-এ top-এর PR = ২০ + NI দেখেছেন। পুরো ছবিটা এই: kernel-এর ভেতরে প্রতিটি task-এর একটি prio, ০–১৩৯, যেখানে ছোট সংখ্যা = বেশি গুরুত্ব। ০–৯৯ real-time (prio = ৯৯ − rt_priority), ১০০–১৩৯ সাধারণ (prio = ১২০ + nice). বিভিন্ন টুল এটি বিভিন্নভাবে দেখায়:
| যেখানে দেখবেন | nice 0-এর task | nice 10 | nice −5 | SCHED_FIFO priority 50 |
|---|---|---|---|---|
kernel-এর prio (/proc/PID/sched) | 120 | 130 | 115 | 49 |
top-এর PR (= prio − ১০০) | 20 | 30 | 15 | −51 (৯৯ হলে rt) |
top/ps-এর NI | 0 | 10 | −5 | সাধারণত 0 বা - (প্রভাব নেই) |
ps -o pri (procps-এর নিয়মে ১৯ − nice; বড় = বেশি গুরুত্ব) | 19 | 9 | 24 | 90 |
ps-এর pri উল্টো দিকে চলে, আর ps -l-এর PRI আরেক নিয়মে — তাই আগে জানুন কোন টুলের কোন কলাম। আসল যন্ত্রে মিলিয়ে দেখি:
ps -o pid,ni,pri,cls,comm # NI = nice, PRI = ps-style priority, CLS = policy
nice -n 10 sh -c 'ps -o ni= -p $$' # ask for +10 more
chrt -p $$ # policy of this bash
for n in 0 4 9; do # nice -> weight, read from the kernel itself
nice -n $n sh -c 'printf "NI %s -> se.load.weight %s\n" "$(ps -o ni= -p $$)" \
"$(awk "/se.load.weight/{print \$3}" /proc/$$/sched)"'
done
echo "autogroup: $(cat /proc/self/autogroup)"
echo "cgroup cpu.max: $(cat /sys/fs/cgroup/cpu.max), cpu.weight: $(cat /sys/fs/cgroup/cpu.weight)"
if [ -r /proc/$$/sched ]; then
grep -E '^(se\.vruntime|se\.sum_exec_runtime|nr_switches|policy|prio|se\.slice) ' /proc/$$/sched
else
echo "/proc/PID/sched is not available (kernel built without SCHED_DEBUG)"
fiএকটি আসল রানের আউটপুট:
PID NI PRI CLS COMMAND
1 10 9 TS podman-init
2 10 9 TS bash
3 10 9 TS ps
19
pid 2's current scheduling policy: SCHED_OTHER
pid 2's current scheduling priority: 0
NI 10 -> se.load.weight 112640
NI 14 -> se.load.weight 46080
NI 19 -> se.load.weight 15360
autogroup: /autogroup-8473039 nice 0
cgroup cpu.max: max 100000, cpu.weight: 100
se.vruntime : 933.944650
se.sum_exec_runtime : 10.423888
nr_switches : 21
policy : 0
prio : 130
se.slice : 1400000
NI 10, PRI 9, CLS TS — এই container-এ সবাই nice 10-এ (পর্ব ১১), ps-এর pri = ১৯ − ১০ = ৯, TS = SCHED_OTHER (অন্যগুলো: FF, RR, B, IDL, DLN). nice -n 10-এর ফল 19 — ২০ হতো, কিন্তু ১৯-এ আটকে গেছে, ত্রুটি ছাড়াই। chrt নিশ্চিত করল SCHED_OTHER.
সবচেয়ে মজার তিনটি weight: 112640, 46080, 15360. সারণিতে nice 10, 14, 19-এর weight 110, 45, 15 — আর 110 × 1024 = 112640, 45 × 1024 = 46080, 15 × 1024 = 15360. অর্থাৎ 64-bit kernel weight ভেতরে ১০২৪ গুণ বড় করে রাখে (group scheduling-এর হিসাবে নির্ভুলতার জন্য); অনুপাত একই। prio 130 = ১২০ + 10; se.slice 1400000 ns = ১.৪ ms — EEVDF-যুগের kernel (৬.৮)। Autogroup আর cgroup-এর লাইন পরের অংশে। (/proc/PID/sched থাকে শুধু CONFIG_SCHED_DEBUG-ওয়ালা kernel-এ, তাই script আগে পরীক্ষা করেছে।)
নিজের যন্ত্রে policy বদলানোর কয়েকটি কমান্ড (উদাহরণ; real-time policy-তে সাধারণত root বা CAP_SYS_NICE লাগে, তাই অনলাইন ব্লকে চালানো হয়নি):
$ chrt -b 0 ./nightly-report.sh # SCHED_BATCH-এ চালানো
$ chrt -i 0 ./index-photos # SCHED_IDLE: শুধু অলস সময়ে
$ sudo chrt -f 50 ./audio-engine # SCHED_FIFO, priority 50 — সাবধানে!
$ chrt -p 4242
pid 4242's current scheduling policy: SCHED_BATCH
pid 4242's current scheduling priority: 0
$ ps -eo pid,cls,rtprio,ni,pri,comm | head -4
PID CLS RTPRIO NI PRI COMMAND
1 TS - 0 19 systemd
17 FF 99 - 139 migration/0
23 TS - 0 19 kthreadd৮ · Task-এর ওপরে দল — autogroup, cgroup v2 আর container-এর CPU limit
এক terminal-এ make -j16 (১৬টি compiler) আর অন্য জানালায় এক-thread-এর ব্রাউজার: task-ভিত্তিক ন্যায্যতায় ব্রাউজার পায় ১/১৭ ভাগ, অথচ ব্যবহারকারীর চোখে দুটি “কাজ”। সমাধান group scheduling (২.৬.২৪ থেকে): একটি দল fair class-এর কাছে একটি entity — নিজের weight, নিজের vruntime — আর দলের ভেতরে আরেকটি cfs_rq. অর্থাৎ tree-র ভেতরে tree.
Autogroup — terminal ধরে স্বয়ংক্রিয় দল
Linux ২.৬.৩৮-এ (২০১১) Mike Galbraith-এর autogroup আসে: প্রতিটি session (পর্ব ১১; সাধারণত একটি terminal বা SSH সংযোগ) আপনা-আপনি একটি দল — ১৬টি compiler এক দল, ব্রাউজার আরেক, প্রত্যেকে প্রায় অর্ধেক। উপরের রানের autogroup: /autogroup-8473039 nice 0 এই script-এর session-এর দল।
Autogroup-এর একটি ফাঁদ: চালু থাকলে (/proc/sys/kernel/sched_autogroup_enabled = 1) একটি process-এর nice শুধু তার নিজের দলের ভেতরে প্রভাব ফেলে। আলাদা terminal-এর ব্যাকআপকে renice 19 দিলেও তার দল সমান ভাগ পায় — nice যেন “কাজ করছে না”! দলের ভাগ বদলাতে লিখতে হয় /proc/PID/autogroup-এ (যেমন echo 10 > /proc/4242/autogroup). আর root cgroup-এর বাইরের task-এ (systemd-চালিত অনেক যন্ত্রে) autogroup প্রযোজ্যই নয় — সেখানে দলের কাজ করে cgroup.
cgroup v2-এর cpu controller
cgroup process-দের একটি গাছে দল বাঁধে আর প্রতিটি দলের CPU, মেমরি, I/O নিয়ন্ত্রণ করে (পুরো গল্প পর্ব ৫১-এ)। CPU-র দুটি ফাইল, cgroup v2-এ:
cpu.weight(১–১০০০০, ডিফল্ট ১০০) — দলের আপেক্ষিক ভাগ, task-এর weight-এর মতো; শুধু প্রতিযোগিতায় কার্যকর, অলস যন্ত্রে weight ১-এর দলও পুরো CPU পায়। (cgroup v1-এ এর নাম ছিলcpu.shares, ডিফল্ট ১০২৪।)cpu.max— কড়া সীমা: “quota period” মাইক্রোসেকেন্ডে।50000 100000= প্রতি ১০০ ms-এ সব CPU মিলিয়ে সর্বোচ্চ ৫০ ms, অর্থাৎ অর্ধেক CPU — যন্ত্র অলস থাকলেও। উপরের রানেরmax 100000মানে কোনো সীমা নেই।
cpu.max-এর পেছনের যন্ত্রটির নাম CFS bandwidth control, Linux ৩.২-এ (২০১২) যুক্ত — মূলত Google আর IBM-এর প্রকৌশলীদের কাজ। প্রতি period-এ দল একটি কোটা পায় (প্রতিটি CPU ছোট টুকরো ধার নেয়); কোটা শেষ হলে দলের সব task throttle হয় — period-এর বাকি সময় চলতে পারে না, CPU অলস থাকলেও।
Kubernetes-এর CPU request আর limit — আর throttling-এর ফাঁদ
Container-এর টুলগুলো সরাসরি এই ফাইল লেখে: Docker-এ --cpus=1.5 মানে cpu.max = 150000 100000; Kubernetes-এ requests.cpu হয় weight (ভিড়ে আনুপাতিক ভাগ), আর limits.cpu হয় cpu.max (কড়া সীমা)। সমস্যা আসে বহু-thread প্রোগ্রামে:
একটি pod-এর limit ২ CPU → cpu.max = 200000 100000: প্রতি ১০০ ms-এ ২০০ ms CPU-সময়। Node-এ ৮টি core, আর অ্যাপের ৮টি thread একসাথে একটি অনুরোধ প্রক্রিয়া করছে। ৮টি thread প্রতি ১ ms আসল সময়ে খরচ করে ৮ ms কোটা, তাই ২০০ ms কোটা শেষ হয় মাত্র ২৫ ms-এ। তারপর বাকি ৭৫ ms সব thread throttled — যন্ত্রের CPU অলস পড়ে থাকলেও। গড় দেখাবে “২ CPU, limit-এর মধ্যেই”, অথচ যে অনুরোধ ৮টি thread-এ টানা ৩০ ms চলার কথা (মোট ২৪০ ms CPU-সময়), সেটি ২৫ ms চলে, ৭৫ ms থামে, পরের period-এ বাকি ৫ ms চলে — শেষ হয় প্রায় ১০৫ ms-এ। এক মিনিটে এটি প্রতিটি period-এ ঘটলে cpu.stat-এ দেখা যাবে nr_periods 600 আর nr_throttled 600. throttled_usec সাধারণত প্রতিটি CPU-র throttled সময় যোগ করে গোনা হয়, তাই ৮টি CPU-তে এটি ≈ ৬০০ × ৮ × ৭৫০০০ µs = ৩৬০ সেকেন্ড — এক মিনিটের চেয়েও বেশি; এটিকে wall-clock সময় ভাববেন না (হিসাবের খুঁটিনাটি kernel-সংস্করণভেদে বদলেছে)।
$ cat /sys/fs/cgroup/kubepods.slice/.../cpu.max
200000 100000
$ cat /sys/fs/cgroup/kubepods.slice/.../cpu.stat
usage_usec 118204331
user_usec 101330120
system_usec 16874211
nr_periods 600
nr_throttled 600
throttled_usec 360102874তাই দেরি-সংবেদনশীল সার্ভিসে CPU limit নিয়ে বিতর্ক আছে — অনেকে শুধু request রাখেন, অন্যরা বলেন limit ছাড়া একটি ভুল pod পুরো node অনিশ্চিত করে (ভাবনার প্রশ্ন ৪)। সাহায্য এসেছে cpu.max.burst-এ (৫.১৪ থেকে, অব্যবহৃত কোটার কিছুটা পরে খরচ), আর ২০১৯ নাগাদ ধরা পড়া একটি অপ্রয়োজনীয়-throttling বাগের সংশোধন এসেছে Linux ৫.৪-এ। Runtime-কেও (JVM, Go) জানতে হয় সে কত CPU-র কোটা পেয়েছে; না জানলে বেশি thread বানিয়ে ঠিক এই ফাঁদে পড়ে।
৯ · sched_ext, আর Windows ও macOS-এর scheduler
sched_ext — scheduler-ও এখন প্রোগ্রাম করা যায় (Linux ৬.১২)
বহু বছর Linux-এর নীতি ছিল একটিই সাধারণ scheduler; কিন্তু Meta, Google-এর মতো প্রতিষ্ঠান নিজস্ব scheduler নিয়ে রিবুট ছাড়া পরীক্ষা চালাতে চাইছিল। Tejun Heo আর David Vernet (Meta)-এর নেতৃত্বে তৈরি sched_ext একটি নতুন scheduling class দেয়, যার নিয়ম লেখা হয় eBPF প্রোগ্রামে (পর্ব ৫৫) — চলমান kernel-এ লোড করা যায়, রিবুট ছাড়াই বদলানো যায়। দীর্ঘ বিতর্কের পর এটি Linux ৬.১২-এ (নভেম্বর ২০২৪) মূলধারায় আসে।
নিরাপত্তা-জাল আছে: BPF verifier প্রোগ্রাম লোডের আগে যাচাই করে; আর BPF scheduler ভুল করলে — যেমন একটি runnable task-কে একটি সময়সীমার মধ্যে না চালালে (ডিফল্ট সর্বোচ্চ প্রায় ৩০ সেকেন্ড; BPF scheduler নিজে আরও কম ঠিক করতে পারে) — kernel তাকে সরিয়ে দিয়ে সব task fair class-এ ফিরিয়ে আনে। উদাহরণ: scx_rustland (নিয়ম user space-এ Rust-এ) আর গেমিংয়ের জন্য scx_lavd (Steam Deck-এর জন্য Igalia-র কাজ বলে পরিচিত)। সাধারণ ব্যবহারে ডিফল্ট fair class-ই থাকছে।
অন্য OS কী করে
| বিষয় | Linux (fair class) | Windows (NT kernel) | macOS / iOS (XNU) |
|---|---|---|---|
| মূল ধারণা | weight-অনুপাতে ভাগ (vruntime, EEVDF) | priority-ভিত্তিক preemptive; একই স্তরে quantum ধরে round robin | Mach থেকে আসা priority-ভিত্তিক বহুস্তর queue; timeshare thread-এর priority CPU ব্যবহারে ক্ষয় হয় |
| Priority-র পরিসর | nice −২০…১৯ (weight), rt ১–৯৯ | ০–৩১: ১–১৫ পরিবর্তনশীল, ১৬–৩১ real-time; process-এর priority class (Idle, Below normal, Normal, Above normal, High, Realtime) + thread-এর আপেক্ষিক priority | ০–১২৭, কয়েকটি band-এ ভাগ |
| ইন্টারঅ্যাকটিভ কাজকে সুবিধা | কম vruntime / আগের deadline — আলাদা অনুমান নেই | priority boost: I/O শেষে, GUI ইনপুটে, অনেকক্ষণ অপেক্ষায় থাকা thread-এ; foreground জানালায় লম্বা quantum | QoS শ্রেণি — user-interactive, user-initiated, utility, background — অ্যাপ নিজে জানায় |
| দলভিত্তিক ভাগ | autogroup, cgroup | Job object-এর CPU rate limit | thread group; সাম্প্রতিক সংস্করণে Clutch নামের scheduler |
| বড়-ছোট core (hybrid) | Energy Aware Scheduling (ARM), Intel hybrid-এ hardware-এর ইঙ্গিত | Intel Thread Director-এর ইঙ্গিত আর QoS/power throttling | QoS দেখে P-core আর E-core-এ পাঠানো (Apple silicon-এ “Edge” scheduler) |
দর্শনের পার্থক্য লক্ষ করুন: Windows আর macOS মূলত priority আর ইঙ্গিত-নির্ভর — Windows নিজে boost দেয়, macOS অ্যাপকে QoS জানাতে বলে; Linux-এর fair class দাঁড়িয়ে আনুপাতিক ভাগের গণিতে, যেখানে nice আসলে একটি অনুপাত। কোনোটি সার্বিকভাবে “সেরা” নয়। (ভেতরের সংখ্যা ও নাম সংস্করণভেদে বদলায়; Windows আরও দেখবেন পর্ব ৫৬-এ।)
আরও গভীরে: vruntime শুধু “কাকে চালাব” বলে; “কোন CPU-তে চালাব” ঠিক করতে Linux প্রতিটি task-এর গড় চাহিদা মাপে PELT (Per-Entity Load Tracking, Paul Turner, Linux ৩.৮) দিয়ে — প্রতি ১ ms-এ একটি জ্যামিতিক ক্ষয়, যাতে ৩২ ms আগের অবদান অর্ধেক হয়ে যায়; উপরের /proc/PID/sched-এ se.avg.util_avg সেই মাপ। Load balancer এই মাপ দিয়ে sched domain-এর স্তরে স্তরে (একই core-এর hyper-thread, একই cache, একই NUMA node) CPU-দের মধ্যে task সরায়, আর ARM-এর big.LITTLE-এ Energy Aware Scheduling একই মাপ দিয়ে শক্তি কম খরচের CPU বাছে। vruntime-এর তুলনাও সরাসরি a < b নয়, (s64)(a − b) < 0 — যাতে u64 সংখ্যা ঘুরে শূন্যে ফিরলেও (wraparound) ক্রম ঠিক থাকে।
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- CFS পরের task কীভাবে বাছে? — red-black tree-তে vruntime অনুযায়ী সাজানো runnable task-দের মধ্যে সবচেয়ে বাঁয়ের, অর্থাৎ সবচেয়ে কম vruntime-এর task; বাঁয়ের node আলাদা করে মনে রাখা থাকে বলে বাছাই O(1), insert/delete O(log n).
- nice 0 আর nice 5-এর দুটি CPU-নির্ভর task একটি CPU-তে কত ভাগ পায়? — weight 1024 আর 335, তাই 1024/1359 ≈ ৭৫.৩৫% আর 335/1359 ≈ ২৪.৬৫%.
- EEVDF-এ eligible মানে কী? — lag ≥ ০, অর্থাৎ task-এর vruntime runnable task-দের weight-ভারিত গড় vruntime-এর চেয়ে বেশি নয়; eligible-দের মধ্যে যার virtual deadline সবচেয়ে আগে, সে চলে।
- top-এ PR 25, NI 5 — সম্পর্ক? — সাধারণ task-এ PR = ২০ + NI; kernel-এর ভেতরে prio = ১২০ + nice = ১২৫।
- Container-এ CPU limit দিলে দেরি কেন বাড়তে পারে? — limit = CFS bandwidth control-এর কোটা; বহু thread কোটা দ্রুত শেষ করলে period-এর বাকি সময় পুরো দল throttled থাকে।
১০ · AI-এর যুগে scheduler-এর প্রশ্ন — উত্তর যাচাই
“আমার সার্ভারে latency বেশি, scheduler tune করব কীভাবে?” — AI সাথে সাথে sched_latency_ns আর sched_min_granularity_ns-এর মান বলে দেবে, প্রায়ই sysctl -w দিয়ে। সমস্যা: এই ফাইলগুলো ৫.১৩ থেকে /proc/sys-এ নেই, আর EEVDF-যুগের kernel-এ latency-র মানটিই নেই। AI-এর জানা তথ্যের বড় অংশ পুরোনো kernel-এর। সাধারণ ভুলগুলো:
| AI-এর ভুল | যা ঘটে | কীভাবে ধরবেন |
|---|---|---|
পুরোনো tunable (sched_latency_ns, sched_child_runs_first) বা ২.৬-যুগের /proc/sys/kernel/sched_* পথ | “No such file”, বা debugfs-এ অর্থহীন পরিবর্তন | uname -r দেখুন, kernel-এর Documentation/scheduler/ মিলিয়ে নিন |
| “nice −২০ দিলে real-time হয়ে যায়” বা “nice 19 কখনো চলে না” | ভুল ধারণায় ভুল সিদ্ধান্ত | weight সারণি থেকে ভাগ হিসাব করুন; উপরের C প্রোগ্রামের মতো মেপে দেখুন |
সমস্যা ছাড়াই sudo chrt -f 99 | ভুল লুপে একটি CPU আটকে যায়, kernel thread-ও পিছিয়ে পড়ে | real-time শুধু মাপা দরকারে; আগে SCHED_OTHER-এ সমস্যা প্রমাণ করুন |
| numerical-এ ভুল weight (যেমন nice 5 = 512) বা নিখুঁত ১০% ধরে হিসাব | ভুল শতাংশ, ভুল উত্তর | আসল সারণি (kernel/sched/core.c) থেকে হাতে হিসাব |
| Kubernetes-এ “limit = request দিলেই নিরাপদ”, বা throttling-এর কথা না বলা | p99 latency হঠাৎ বাড়ে, গড় ঠিক দেখায় | cpu.stat-এর nr_throttled, throttled_usec দেখুন; চাপের মধ্যে পরীক্ষা করুন |
AI-কে scheduler-সংক্রান্ত প্রশ্ন করার সময় পরিবেশ, প্রমাণ আর যাচাইয়ের শর্ত দিন:
AI-এর দেওয়া scheduler-পরামর্শ ব্যবহারের আগে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১O(1) scheduler প্রযুক্তিগতভাবে চমৎকার ছিল, তবু চার বছরের মধ্যে বাদ পড়ল। Heuristic বনাম নীতিভিত্তিক নকশার এই লড়াই থেকে কী শেখার আছে — আর Con Kolivas-এর ঘটনাটি kernel সম্প্রদায় সম্পর্কে কী বলে?
O(1) scheduler সমাধান করেছিল একটি পরিষ্কার, মাপা যায় এমন সমস্যা: বড় সার্ভারে scheduler নিজেই ধীর আর একটি lock-এ আটকে। কিন্তু desktop-এর সমস্যাটি ছিল ভিন্ন ধরনের — “কোন task ইন্টারঅ্যাকটিভ?” প্রশ্নের কোনো নিখুঁত উত্তর নেই। sleep_avg দিয়ে অনুমান করলে কিছু ক্ষেত্রে ভুল হবেই, আর প্রতিটি ভুল সারাতে নতুন একটি নিয়ম যোগ হয়। কয়েক বছরে নিয়মগুলো এমন জটিল হয় যে একটি পরিবর্তন কোথায় কী ভাঙবে, বলা কঠিন হয়ে পড়ে।
Con Kolivas-এর মূল অবদান ছিল একটি ধারণাগত মোড়: অনুমান বাদ দিয়ে কঠোর ন্যায্যতা আর পূর্বানুমেয়তা; তাহলে ইন্টারঅ্যাকটিভিটি আপনিই আসবে, কারণ যে কম খায় সে আগে পায়। তাঁর patch পরীক্ষা করতেন মূলত desktop-ব্যবহারকারীরা, যাঁদের অভিযোগকে তখন অনেকে “মাপা যায় না” বলে উড়িয়ে দিতেন। RSDL নিয়ে আলোচনা চলার সময় Ingo Molnár দ্রুত CFS লিখে ফেলেন, যা ন্যায্যতার ধারণা নিল কিন্তু ভিন্ন কাঠামোয় (vruntime আর tree), আর Molnár ঘোষণাতেই Kolivas-এর কাজের প্রভাব স্বীকার করেছিলেন। CFS গৃহীত হওয়ার পর Kolivas হতাশা নিয়ে সরে দাঁড়ান, আর সাক্ষাৎকারে বলেন desktop ব্যবহারকারীদের কথা kernel সম্প্রদায়ে যথেষ্ট গুরুত্ব পায় না।
দুই দিকেই যুক্তি আছে। kernel-এর মান রক্ষায় পরীক্ষিত, রক্ষণাবেক্ষণযোগ্য কোড দরকার; আবার বাইরের মানুষের ধারণা নিলে কৃতিত্ব কীভাবে ভাগ হবে — এই প্রশ্ন সব open-source প্রকল্পের। পরে latency মাপার টুল (perf sched-এর মতো) উন্নত হয়েছে, যাতে এমন অভিযোগও সংখ্যায় ধরা যায়। আর মজার ব্যাপার, ষোল বছর পর EEVDF-এ আবার একটি deadline-ধারণা ফিরে এসেছে — RSDL-এর নামেও “Deadline” ছিল।
মূল উপলব্ধি: যে সমস্যার নিখুঁত অনুমান সম্ভব নয়, সেখানে heuristic-এর স্তূপের চেয়ে একটি সরল, প্রমাণযোগ্য নীতি দীর্ঘমেয়াদে বেশি টেকে। আর প্রযুক্তিগত সিদ্ধান্ত কখনোই পুরোপুরি প্রযুক্তিগত নয় — মানুষ, আস্থা আর প্রক্রিয়াও তার অংশ।
প্র ০২CFS-এর শুধু “সবচেয়ে ছোট vruntime” দরকার — তাহলে min-heap কেন নয়, red-black tree কেন? আর এই পছন্দ EEVDF-এ কীভাবে কাজে লাগল?
প্রথম দেখায় binary min-heap আদর্শ মনে হয়: সবচেয়ে ছোট উপাদান O(1)-এ, insert আর বের করা O(log n), আর array-তে রাখা যায় বলে cache-বান্ধব (DSA কোর্সের heap পর্ব)। কিন্তু scheduler-এর কাজের তালিকা দেখুন। শুধু “ছোটটি বের করা” নয় — যেকোনো task যেকোনো মুহূর্তে ঘুমিয়ে পড়ে, অর্থাৎ মাঝখান থেকে বাদ যায়; অন্য CPU-তে সরানো হয়; nice বদলায়। Heap-এ মাঝখানের উপাদান মুছতে তার অবস্থান জানতে হয়, আর প্রতিটি অদলবদলে সেই অবস্থান হালনাগাদ রাখতে হয় — সম্ভব, কিন্তু জটিল। Red-black tree-তে প্রতিটি task-এর মধ্যে node নিজেই বসানো (run_node, intrusive data structure), তাই যেকোনো task মুছতে সরাসরি O(log n), কোনো অবস্থান খোঁজা নেই।
দ্বিতীয়ত, kernel-এ পরীক্ষিত rbtree library (lib/rbtree.c) আগে থেকেই নানা জায়গায় (epoll, I/O scheduler…) ব্যবহৃত — পুনর্ব্যবহার মানে কম বাগ। আর বাঁয়ের node cache করে রাখলে (পরে rb_root_cached হিসেবে library-তেই এল) heap-এর একমাত্র সুবিধা — ছোটটি O(1)-এ — tree-ও দেয়।
তৃতীয় সুবিধাটি টের পাওয়া গেল ষোল বছর পর। EEVDF-এর প্রশ্ন “সবচেয়ে ছোট” নয়: “যাদের vruntime ≤ V, তাদের মধ্যে সবচেয়ে ছোট deadline”. Heap এই দুই-মাত্রার প্রশ্নের উত্তর দিতে পারে না। কিন্তু একটি augmented balanced tree পারে — প্রতিটি node তার subtree-র একটি সারাংশ (সবচেয়ে ছোট deadline বা vruntime) রাখে, ঘূর্ণনের (rotation) সময় সেটি হালনাগাদ হয়, আর খোঁজা O(log n). Linux-এর rbtree-তে augmentation-এর সুবিধা আগে থেকেই ছিল, তাই EEVDF-এ শুধু callback যোগ করতে হয়েছে। একটি তুলনাও মনে রাখার মতো: Con Kolivas-এর BFS ইচ্ছা করে একটি সাধারণ তালিকা আর O(n) খোঁজা ব্যবহার করত — তাঁর যুক্তি ছিল desktop-এ runnable task সাধারণত কম, আর সরল কোডের cache-আচরণ ভালো। অর্থাৎ “সেরা” ডেটা স্ট্রাকচার নির্ভর করে n কত বড় আর কী কী প্রশ্ন করা হবে, তার ওপর।
মূল উপলব্ধি: ডেটা স্ট্রাকচার বাছাই হয় শুধু প্রধান কাজটি দেখে নয়, সব কাজ — মাঝখান থেকে মোছা, পুনর্ব্যবহার, ভবিষ্যতের নতুন প্রশ্ন — দেখে। Augmentation-যোগ্য balanced tree সেই নমনীয়তা দেয়, যা heap দেয় না।
প্র ০৩CFS ষোল বছর টিকে ছিল। EEVDF কেন এল — ১৯৯৫ সালের একটি গবেষণাপত্র ২০২৩ সালের kernel-এ কী সমাধান দিল, আর বড় scheduler বদলের ঝুঁকি কী?
CFS-এর ন্যায্যতা গাণিতিকভাবে পরিষ্কার ছিল, কিন্তু latency ছিল প্রায় “পার্শ্বফল”. একটি task কত তাড়াতাড়ি CPU পাবে, তা নির্ভর করত vruntime-এর অবস্থান, wakeup granularity, sleeper-এর বাড়তি আর buddy-নিয়মের জটিল মিশ্রণের ওপর। অ্যান্ড্রয়েডের UI thread, অডিও, গেমের দরকার বেশি ভাগ নয়, তাড়াতাড়ি পাওয়া; nice কমালে ভাগ বাড়ে, যা অপব্যবহারের পথ খোলে। “latency-nice” প্রস্তাব কয়েক বছর আলোচনা হয়েছে, কিন্তু CFS-এর কাঠামোয় এর অর্থ পরিষ্কার করে সংজ্ঞায়িত করা যাচ্ছিল না।
১৯৯৫ সালে Ion Stoica আর Hussein Abdel-Wahab proportional-share scheduling-এর জন্য EEVDF প্রস্তাব করেন। এর সৌন্দর্য হলো দুটি প্রশ্ন আলাদা করা: কতটা পাবে (weight, eligibility দিয়ে পাহারা) আর কত তাড়াতাড়ি পাবে (চাওয়া slice থেকে virtual deadline). পত্রে প্রমাণ ছিল যে প্রতিটি task-এর lag সীমিত থাকে — অর্থাৎ আদর্শ ভাগ থেকে বিচ্যুতির একটি নিশ্চয়তা, যা CFS-এর heuristic-গুলো দিতে পারত না। Stoica পরে Chord-এর মতো distributed system গবেষণা আর Spark ও Databricks-এর সাথে যুক্ত থাকার জন্য বিখ্যাত হন — এটি তাঁর শুরুর দিকের কাজ। Peter Zijlstra এই পত্রে ফিরে গিয়ে বাছাই নতুন করে লেখেন আর অনেক পুরোনো heuristic (buddy, sleeper-এর বিশেষ নিয়ম, latency period) মুছে ফেলেন — কোড কমেছে, নীতি পরিষ্কার হয়েছে।
ঝুঁকিও বাস্তব। Scheduler পৃথিবীর কোটি কোটি যন্ত্রে চলে, আর প্রতিটি workload পুরোনো আচরণের ওপর অজান্তে নির্ভর করে। ৬.৬-এর পর কিছু benchmark-এ গতি কমার খবর এসেছিল (কিছু ক্ষেত্রে বেশি preemption-এর কারণে বলে আলোচনা হয়েছে), আর পরের কয়েকটি সংস্করণে সংশোধন এসেছে — যেমন ঘুমন্ত task-এর lag সামলানোর নতুন উপায় (“delayed dequeue”). বড় distro আর Android সাধারণত LTS kernel ব্যবহার করে, তাই পরিবর্তন ধীরে ছড়ায় — এটিও একটি নিরাপত্তা-জাল। পুরোনো kernel-এর সার্ভার বহু বছর চলে, তাই CFS জানা আজও জরুরি।
মূল উপলব্ধি: পুরোনো গবেষণা প্রায়ই সময়ের আগে থাকে; প্রকৌশলের অভিজ্ঞতা যখন একই সমস্যায় ফিরে আসে, তখন তত্ত্ব কাজে লাগে। আর “ভালো” তত্ত্বও কোটি workload-এ নামাতে হলে মাপা, ধীর, সংশোধনযোগ্য পথে নামাতে হয়।
প্র ০৪Kubernetes-এ CPU limit বসাবেন কি না — প্রকৌশলীদের মধ্যে এ নিয়ে বিতর্ক কেন? CFS bandwidth control-এর ইতিহাস আর বহু-thread runtime-এর আচরণ থেকে যুক্তি দিন।
CFS bandwidth control তৈরি হয়েছিল একটি স্পষ্ট প্রয়োজনে: বহু ভাড়াটের (multi-tenant) যন্ত্রে প্রত্যেককে নির্দিষ্ট পরিমাণ CPU বিক্রি করা আর পূর্বানুমেয় রাখা। Google আর IBM-এর কয়েকজন প্রকৌশলী ২০১০ সালের দিকে এটি প্রস্তাব করেন, আর Linux ৩.২-এ এটি আসে। নকশাটি period-ভিত্তিক (ডিফল্ট ১০০ ms), তাই একটি অনিবার্য ফল আছে: কোটা যত দ্রুত খরচ হয়, period-এর বাকি অংশে তত লম্বা বিরতি। এক-thread প্রোগ্রামে ১ CPU limit-এ সমস্যা প্রায় নেই। বহু-thread প্রোগ্রাম কোটা কয়েক গুণ দ্রুত খায় — অংশ ৮-এর হিসাবে ৮টি thread ২ CPU-র কোটা ২৫ ms-এ শেষ করে, তারপর ৭৫ ms নিশ্চল।
এর সাথে যোগ হয়েছিল একটি বাস্তব বাগ। প্রতিটি CPU কোটার ছোট ছোট টুকরো স্থানীয়ভাবে ধার নেয়, আর পুরোনো কোডে সেই অব্যবহৃত টুকরো একটি নির্দিষ্ট সময় পর বাতিল হয়ে যেত। বহু-CPU node-এ কম ব্যবহারের অ্যাপও তাই কোটা “শেষ” দেখে throttled হতো। ২০১৯ সাল নাগাদ বিষয়টি বিস্তারিত বিশ্লেষণে ধরা পড়ে, আর Linux ৫.৪-এ সংশোধন আসে (পরে কিছু stable শাখাতেও)। সে সময়ের অনেক ব্লগ-পোস্ট “CPU limit সরিয়ে ফেলুন” পরামর্শ দেয়, অনেক দল আজও তা মানে।
বিপক্ষের যুক্তিও শক্ত। Limit না থাকলে একটি ভুল deployment (অসীম লুপ, হঠাৎ ট্রাফিক) পুরো node-এর অন্য pod-দের আনুপাতিক ভাগে নামিয়ে আনে — request-এর নিশ্চয়তা থাকে, কিন্তু অতিরিক্ত CPU-র ওপর যারা নির্ভর করছিল তারা হঠাৎ ধীর হয়। আর অলস node-এর benchmark-এর গতি ভিড়ের node-এ মেলে না। মাঝামাঝি পথ: request সঠিকভাবে মাপা; limit রাখলে runtime-কে কোটা জানানো (JVM-এর container support, Go-তে GOMAXPROCS কোটার সাথে মেলানো); cpu.max.burst; আর সবচেয়ে গুরুত্বপূর্ণ — nr_throttled আর p99 latency মনিটর করা, গড় CPU নয়।
মূল উপলব্ধি: একটি সীমা গড়ে যা বলে আর সময়ের ছোট জানালায় যা করে, তা আলাদা হতে পারে। CPU limit-এর সিদ্ধান্ত নিতে হয় workload-এর thread-সংখ্যা, latency-র লক্ষ্য আর node-এর ভাগাভাগির নীতি মিলিয়ে — আর kernel সংস্করণও দেখে।
প্র ০৫“একটিই scheduler সবার জন্য” — Linux-এর এই দীর্ঘ নীতি sched_ext দিয়ে ভাঙা কি ঠিক হলো? Pluggable scheduler-এর পক্ষে-বিপক্ষে যুক্তি কী?
২০০০-এর দশকের মাঝামাঝি “plugsched” নামে একটি প্রস্তাব ছিল, যাতে compile-এর সময় বা বুটে বিভিন্ন scheduler বেছে নেওয়া যায়; Con Kolivas-সহ কয়েকজন এর পক্ষে ছিলেন। Linus Torvalds আর Ingo Molnár-এর মত ছিল বিপরীত: একাধিক scheduler থাকলে প্রত্যেকে নিজের প্রিয় workload-এর জন্য একটি বানাবে, কেউ মূল scheduler-এর দুর্বলতা সারাতে আগ্রহী হবে না, আর বাগ রিপোর্ট ছড়িয়ে যাবে — “আপনি কোন scheduler চালাচ্ছেন?” একটি scheduler-কে সবার জন্য ভালো করতে বাধ্য হওয়াই মান রক্ষার পথ। এই নীতিতে CFS প্রায় সব workload-এ গ্রহণযোগ্য হয়ে উঠেছিল।
কিন্তু পৃথিবী বদলেছে। Hyperscaler কোম্পানির প্রতিটি workload (ad serving, ডেটাবেস, machine learning) লাখ লাখ CPU-তে চলে, আর কয়েক শতাংশ উন্নতিও বিশাল সাশ্রয়। Google ২০২১ সালে ghOSt নামে একটি গবেষণা প্রকাশ করে (SOSP সম্মেলনে), যেখানে scheduling-এর সিদ্ধান্ত user space-এর একটি agent-কে দেওয়া হয়। CPU-র গঠনও জটিল হয়েছে (বড়-ছোট core, chiplet), আর মূল scheduler-এ প্রতিটি পরীক্ষা ঢোকানো অসম্ভব। sched_ext-এর যুক্তি: BPF-এর নিরাপত্তা-জাল (verifier, watchdog, fair class-এ ফিরে যাওয়া) থাকায় ভুল scheduler যন্ত্র নষ্ট করবে না, আর ভালো ধারণা পরে মূল scheduler-এ আনা যাবে।
বিতর্ক তীব্র ছিল। Scheduler maintainer-দের কেউ কেউ আপত্তি তোলেন — কোম্পানিগুলো নিজের ব্যক্তিগত scheduler বানিয়ে মূল scheduler-এর উন্নতিতে অবদান রাখা বন্ধ করবে, আর kernel-এর ভেতরের API-কে স্থিতিশীল রাখার চাপ বাড়বে। শেষে Linus Torvalds ৬.১২-এ এটি গ্রহণ করেন, বলে জানা যায় এই যুক্তিতে যে আলোচনা আর আটকে রাখা উৎপাদনশীল নয়। এটি গবেষণার মঞ্চ হয়ে থাকবে, না কিছু distro-তে ডিফল্ট হবে — সময় বলবে।
মূল উপলব্ধি: “একটি নিয়ম সবার জন্য” মান রক্ষা করে, কিন্তু পরীক্ষার গতি কমায়; “সবাই নিজেরটা” পরীক্ষাকে মুক্ত করে, কিন্তু খণ্ডিত করে। নিরাপদ সীমার (sandbox) মধ্যে বিস্তারযোগ্যতা — BPF-এর মডেল — এই দ্বন্দ্বের একটি আধুনিক আপস।
অনুশীলন
প্রথম দুটি কাগজ-কলমে করুন; তৃতীয়টির উত্তর আগে অনুমান করে তারপর উপরের bash ব্লকে মিলিয়ে দেখুন; চতুর্থটি হাতে trace.
-
Weight থেকে ভাগ আর slice (numerical): একটি CPU-তে তিনটি সবসময়-ব্যস্ত task: P nice 0, Q nice 2, R nice −3. সারণি থেকে weight নিয়ে বের করুন: (ক) প্রত্যেকের CPU-র ভাগ; (খ) ক্লাসিক CFS-এ ৬ ms targeted latency period-এ প্রত্যেকের slice; (গ) আসল ১ ms চললে প্রত্যেকের vruntime কত বাড়ে; (ঘ) R একা nice −3 থেকে nice 0-এ গেলে P-এর ভাগ কত হবে?
Weight: P 1024, Q 655, R 1991. মোট W = 1024 + 655 + 1991 = 3670.
(ক) ভাগ: P 1024 ÷ 3670 ≈ 27.90%, Q 655 ÷ 3670 ≈ 17.85%, R 1991 ÷ 3670 ≈ 54.25%; যোগফল ১০০%.
(খ) Slice = 6 × ভাগ: P ≈ 1.674 ms, Q ≈ 1.071 ms, R ≈ 3.255 ms (যোগফল ৬ ms; সবই ০.৭৫ ms-এর তলার ওপরে, তাই period বাড়াতে হয়নি)।
(গ) Δvruntime = 1 × 1024 ÷ w: P 1.000 ms, Q 1024 ÷ 655 ≈ 1.563 ms, R 1024 ÷ 1991 ≈ 0.514 ms. যাচাই: period শেষে সবার vruntime বাড়ে ≈ 1.674 (1.071 × 1.563, 3.255 × 0.514).
(ঘ) নতুন W = 1024 + 655 + 1024 = 2703; P-এর ভাগ 1024 ÷ 2703 ≈ 37.88%. লক্ষ করুন, P নিজের nice বদলায়নি, তবু তার ভাগ ২৭.৯০% থেকে ৩৭.৮৮% হলো — ভাগ সবসময় আপেক্ষিক।
-
Throttling-এর হিসাব (numerical): একটি container-এর
cpu.max = 50000 100000. এর ভেতরের প্রোগ্রামের ৪টি thread, আর যন্ত্রে ৪টির বেশি CPU অলস। (ক) কোটা কত CPU-র সমান? (খ) চারটি thread একসাথে ব্যস্ত থাকলে প্রতিটি period-এর কতক্ষণ পর throttling শুরু হবে, আর কতক্ষণ চলবে? (গ) ১০ সেকেন্ড টানা এমন চললেcpu.stat-এnr_periods,nr_throttledআরthrottled_usecমোটামুটি কত দেখাবে? (ঘ) একটি অনুরোধে এক thread-এর ২০ ms CPU-কাজ লাগে, আর চারটি thread একসাথে চারটি অনুরোধ নিয়ে শুরু করে — প্রতিটি অনুরোধ কতক্ষণে শেষ হবে?(ক) ৫০ ms ÷ ১০০ ms = ০.৫ CPU.
(খ) চারটি thread প্রতি আসল ms-এ ৪ ms কোটা খরচ করে, তাই ৫০ ms কোটা শেষ হয় ১২.৫ ms-এ; বাকি ৮৭.৫ ms সবাই throttled.
(গ) ১০ সেকেন্ড = ১০০টি period.
nr_periods≈ 100,nr_throttled≈ 100, দল প্রতি period-এ ৮৭.৫ ms throttled, অর্থাৎ মোট ১০০ × ৮৭৫০০ µs = ৮.৭৫ সেকেন্ড wall-clock সময়; কিন্তুthrottled_usecসাধারণত প্রতিটি CPU-র throttled সময় যোগ করে, তাই ৪টি CPU-তে ≈ ৪ × ৮,৭৫০,০০০ = 35,000,000 µs (৩৫ সেকেন্ড) দেখাবে (খুঁটিনাটি kernel-সংস্করণভেদে বদলেছে)।(ঘ) প্রতিটি thread-কে ২০ ms চলতে হবে। প্রথম period-এ প্রত্যেকে ১২.৫ ms চলে, তারপর থামে (১০০ ms-এর সীমা পর্যন্ত)। দ্বিতীয় period-এর শুরুতে বাকি ৭.৫ ms চলে। তাই প্রতিটি অনুরোধ শেষ হয় প্রায় ১০০ + ৭.৫ = ১০৭.৫ ms-এ, যেখানে throttling না থাকলে লাগত ২০ ms — গড় CPU ০.৫-এর মধ্যে থেকেও দেরি পাঁচ গুণের বেশি। তুলনায়, একটি thread-এ চারটি অনুরোধ পরপর চালালে প্রথম দুটি শেষ হতো ২০ আর ৪০ ms-এ, তৃতীয়টি কোটা শেষ হয়ে থেমে ১১০ ms-এ, চতুর্থটি ১৩০ ms-এ — অর্থাৎ thread-সংখ্যা কোটার সাথে মেলালে অন্তত কিছু অনুরোধ দ্রুত শেষ হয়, আর দেরি অনুমান করা সহজ হয়।
-
আউটপুট অনুমান করুন: একটি সাধারণ Linux যন্ত্রে (শুরুর nice 0, autogroup-এর কথা ভুলে যান) এই কমান্ডগুলো কী ছাপাবে? (ক)
nice -n 5 nice -n 7 sh -c 'ps -o ni=,pri= -p $$'(খ)nice -n 30 sh -c 'ps -o ni= -p $$'(গ) সাধারণ user হিসেবেnice -n -5 true; echo $?(ঘ) প্রথমটির process top-এ কোন PR দেখাবে, আর/proc/PID/sched-এprioওse.load.weightকত (64-bit kernel)?(ক) nice জমা হয়: 0 + 5 + 7 = 12; ps-এর pri = 19 − 12 = 7. আউটপুট মোটামুটি
12 7.(খ) 0 + 30 সীমা ছাড়ায়, তাই আটকে যায় 19-এ, কোনো ত্রুটি ছাড়া। (উপরের অনলাইন সার্ভারে শুরু 10, সেখানে
nice -n 10-এর ফলও 19 দেখেছেন।)(গ) সাধারণ user nice কমাতে (priority বাড়াতে) পারেন না —
RLIMIT_NICEবাCAP_SYS_NICEনা থাকলে। GNUniceসতর্কবার্তা ছাপায় (nice: cannot set niceness: Permission denied), তবু কমান্ডটি অপরিবর্তিত nice-এ চালায়, তাই exit status 0 (coreutils-এর আচরণ;man niceদেখুন)।(ঘ) top-এর PR = 20 + 12 = 32. Kernel-এর prio = 120 + 12 = 132. nice 12-এর weight সারণিতে 70, আর 64-bit kernel ১০২৪ গুণ করে রাখে: 70 × 1024 = 71680. উপরের bash ব্লকে nice 10, 14, 19-এর জন্য ঠিক এই নিয়ম দেখেছেন।
-
EEVDF হাতে trace: একটি CPU-তে তিনটি task, সবার weight 1024 (তাই virtual slice = আসল slice), এককগুলো ms. P: vruntime 5, slice 3. Q: vruntime 6, slice 1.5. R: vruntime 9, slice 0.5. (ক) V, প্রত্যেকের lag, eligibility আর virtual deadline বের করুন। (খ) CFS আর EEVDF কাকে বাছবে? (গ) EEVDF-এর বাছাই করা task পুরো slice চলার পর আবার হিসাব করুন — এবার কে?
(ক) V = (5 + 6 + 9) ÷ 3 = 20 ÷ 3 ≈ 6.667. lag = V − v: P +1.667, Q +0.667, R −2.333 (যোগফল 0). Eligible: P আর Q. Deadline = v + slice: P 8, Q 7.5, R 9.5.
(খ) CFS: সবচেয়ে কম vruntime — P. EEVDF: eligible {P, Q}-এর মধ্যে আগের deadline — Q (7.5).
(গ) Q 1.5 ms চলল: v = 7.5, deadline পৌঁছে গেছে, নতুন deadline 7.5 + 1.5 = 9. নতুন V = (5 + 7.5 + 9) ÷ 3 = 21.5 ÷ 3 ≈ 7.167. lag: P +2.167, Q −0.333, R −1.833. Eligible শুধু P, তাই EEVDF এবার P-কে বাছবে (deadline 8). Q ছোট slice চেয়ে আগে পেল, কিন্তু ভাগের বেশি নয়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমক্লাসিক CFS-এ timer tick আসার পর থেকে নতুন task চলা পর্যন্ত যা যা ঘটে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ⏱️
update_curr(): চলমান task কত ns চলল, হিসাব - ➕ vruntime-এ যোগ: Δ × 1024 ÷ weight, আর min_vruntime হালনাগাদ
- ⚖️ slice পেরিয়েছে কি না পরীক্ষা, পেরোলে need_resched পতাকা
- 🌳
schedule(): পুরোনো task-কে আবার red-black tree-তে ঢোকানো - 👈 সবচেয়ে বাঁয়ের node — সবচেয়ে কম vruntime — বাছাই
- 🔁 context switch করে নতুন task চালানো
Linux-এর CFS scheduler একটি CPU-র runnable task-দের মধ্যে পরের task কীভাবে বাছে?
- সবচেয়ে উঁচু priority-র array-র প্রথম task
- vruntime অনুযায়ী সাজানো red-black tree-র সবচেয়ে বাঁয়ের task, অর্থাৎ সবচেয়ে কম vruntime
- যার পরের CPU burst সবচেয়ে ছোট
- যে সবচেয়ে বেশি সময় ধরে ready queue-তে অপেক্ষা করছে
একটি CPU-তে দুটি সবসময়-ব্যস্ত task, একটি nice 0 (weight 1024), অন্যটি nice 10 (weight 110). দীর্ঘ সময়ে nice 10-এর task মোটামুটি কত শতাংশ CPU পাবে?
- প্রায় ০%, কারণ nice 0 সবসময় আগে
- প্রায় ৪০%, প্রতি ধাপে ১০% কমে বলে
- প্রায় ৯.৭%
- ৫০%, কারণ CFS সম্পূর্ণ ন্যায্য
Linux ৬.৬-এ আসা EEVDF scheduler-এ একটি task “eligible” হওয়ার শর্ত কী?
- তার lag ≥ ০, অর্থাৎ vruntime runnable task-দের weight-ভারিত গড় vruntime-এর চেয়ে বেশি নয়
- তার virtual deadline সবার চেয়ে আগে
- তার nice মান ০ বা তার কম
- সে অন্তত একটি slice ধরে ঘুমিয়ে ছিল
Linux-এর top-এ একটি সাধারণ (SCHED_OTHER) process-এর NI −5 দেখাচ্ছে। তার PR কলামে কত দেখাবে?
- −5
- 115
- 24
- 15
Linux kernel-এর scheduling class-গুলোর অগ্রাধিকার, সবচেয়ে উঁচু থেকে নিচু — কোন ক্রমটি সঠিক?
- rt → deadline → fair → idle → stop
- stop → deadline → rt → fair → idle
- fair → rt → deadline → idle → stop
- idle → fair → rt → deadline → stop
একটি container-এর cpu.max = 100000 100000 (১ CPU), আর ভেতরের প্রোগ্রামের ৪টি thread একসাথে ৪টি অলস CPU-তে পুরো ব্যস্ত। প্রতিটি ১০০ ms period-এ কী ঘটবে?
- চারটি thread পুরো ১০০ ms চলবে, কারণ CPU অলস
- একটি thread চলবে, বাকি তিনটি কখনো চলবে না
- প্রায় ২৫ ms চলবে, তারপর period-এর বাকি প্রায় ৭৫ ms পুরো দল throttled থাকবে
- প্রতিটি thread মসৃণভাবে ২৫% গতিতে পুরো ১০০ ms চলবে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Linux scheduler তিন যুগ পেরিয়েছে: O(n) (২.৪ পর্যন্ত) → Ingo Molnár-এর O(1) (২.৬.০, ২০০৩; heuristic-নির্ভর) → CFS (২.৬.২৩, ২০০৭; ন্যায্যতার নীতি) → EEVDF (৬.৬, ২০২৩)।
- CFS: Δvruntime = Δt × 1024 ÷ weight; vruntime-এর red-black tree-র বাঁয়ের node চলে; দীর্ঘ সময়ে ভাগ = wi ÷ W.
- nice 0 = 1024, প্রতি ধাপে weight ≈ ১.২৫ গুণ, তাই এক ধাপে ≈ ১০% প্রভাব; nice 0 বনাম 10 ≈ ৯.৩ : ১. ঘুম থেকে ফেরা আর নতুন task min_vruntime-এর কাছে বসে।
- EEVDF: lag ≥ ০ (eligible) task-দের মধ্যে সবচেয়ে আগের virtual deadline; সব lag-এর যোগফল ০, তাই কেউ না কেউ সবসময় eligible; ছোট slice = দ্রুত সাড়া, কিন্তু ভাগের বেশি নয়।
- Class-ক্রম stop → deadline → rt → fair → idle; top-এর PR = ২০ + NI; autogroup ও
cpu.weightদলের ভাগ,cpu.maxকড়া কোটা — বহু-thread অ্যাপে throttling-এর দেরি আনতে পারে; sched_ext (৬.১২) BPF-এ scheduler লেখার পথ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২২ · Race condition আর critical section — Peterson-এর সমাধানপরবর্তী পর্বএকসাথে চলা thread-দের ভাগ করা ডেটার সমস্যা।
- অপারেটিং সিস্টেম · কোর্স হোম৬৪টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- DSA · Red-black treeপাশের ট্র্যাকCFS যে tree-তে task সাজায়, তার insert, delete আর ঘূর্ণন।
- DSA · Heap আর priority queueতুলনাকেন heap সহজ হলেও scheduler-এ red-black tree বেছে নেওয়া হলো।
- C প্রোগ্রামিং · process আর IPCহাতে-কলমেfork, pipe আর wait — এই পর্বের C পরীক্ষার ভিত্তি।