এই পর্বে যা শিখবেন
- একটি প্রসেসের “context”-এ ঠিক কী কী থাকে, আর কোনটি কোথায় সংরক্ষিত হয় (PCB, kernel stack)
- Context switch কখন ঘটে, voluntary আর involuntary switch-এর পার্থক্য, আর Linux-এ নিজের চোখে সেই সংখ্যা দেখা
- Dispatcher-এর প্রতিটি ধাপ, register ধরে ধরে — আর mode switch, process switch ও thread switch-এর পার্থক্য
- Switch-এর প্রত্যক্ষ ও পরোক্ষ খরচ: TLB flush, ASID/PCID, cache-এর দূষণ; আর C প্রোগ্রাম দিয়ে খরচ মাপা
- Overhead আর CPU efficiency-র সব রকম numerical, cooperative বনাম preemptive multitasking, আর Linux-এর
schedule()ওswitch_to
১ · “Context” কী — CPU-র মনে রাখার খাতা
একটি ব্যাংকের শাখায় একজন কর্মকর্তা আছেন, কাউন্টার একটিই। সামনে তিনজন গ্রাহক — একজনের ঋণের ফাইল, একজনের নতুন অ্যাকাউন্ট, একজনের চেক। কর্মকর্তা প্রথম ফাইলে কাজ করতে করতে ম্যানেজারের ফোন পেলেন: “দ্বিতীয় গ্রাহককে আগে দেখুন।” তিনি কী করবেন? প্রথম ফাইলটি এলোমেলো রেখে উঠে গেলে পরে কিছুই মনে থাকবে না। তাই তিনি একটি চিরকুটে লেখেন — “পৃষ্ঠা ৭, তৃতীয় লাইন পর্যন্ত যাচাই শেষ, হিসাবে ৪২ হাজার টাকা পর্যন্ত মিলেছে, ক্যালকুলেটরে এখন ১২৫০ লেখা” — আর চিরকুটসহ ফাইলটি তাকে রাখেন। তারপর দ্বিতীয় গ্রাহকের ফাইল নামান, তার পুরোনো চিরকুট পড়েন, আর ঠিক যেখানে থেমেছিলেন সেখান থেকে শুরু করেন।
CPU-র ক্ষেত্রেও একই ব্যাপার। একটি CPU core একসময়ে একটিমাত্র নির্দেশ-ধারা চালায়, অথচ আপনার ফোনে বা ল্যাপটপে শত শত process আর thread “একসাথে” চলছে। এই ভ্রম তৈরি হয় দ্রুত পালাবদলে। আর পালাবদলের সময় চিরকুটে যা লিখতে হয়, সেটিই context — CPU-র ভেতরের সেই সব মান, যা ফিরিয়ে দিলে থেমে থাকা প্রোগ্রাম বুঝতেই পারে না যে সে থেমেছিল।
| Context-এর অংশ | x86-64-এ উদাহরণ | না রাখলে কী হতো |
|---|---|---|
| সাধারণ register | RAX, RBX … R15 (১৬টি) | অর্ধেক করা হিসাবের মান হারিয়ে যেত |
| Program counter | RIP | কোন নির্দেশ থেকে আবার শুরু, জানা যেত না |
| Stack pointer | RSP | ফাংশন-কলের শৃঙ্খল আর লোকাল ভ্যারিয়েবল হারাত |
| Flags | RFLAGS | শেষ তুলনার ফল (zero, carry) ভুল হতো, if উল্টো চলত |
| FPU/SIMD অবস্থা | x87, SSE, AVX register (XSAVE এলাকা) | ভিডিও, গেম বা ML-এর ভাসমান-বিন্দু হিসাব নষ্ট হতো |
| Thread-local storage-এর ঠিকানা | FS/GS base | errno-এর মতো প্রতি-thread ভ্যারিয়েবল গুলিয়ে যেত |
| Page table-এর ঠিকানা | CR3 | একজনের ঠিকানায় আরেকজনের মেমরি দেখা যেত |
| Kernel stack | প্রতি thread-এর নিজস্ব (Linux x86-64-এ ১৬ KB) | Kernel-এর ভেতরে অর্ধেক হওয়া কাজ (যেমন একটি read) ফেরত আসত না |
খেয়াল করুন, প্রসেসের খোলা ফাইলের তালিকা, তার মেমরির পাতাগুলো বা PID এই তালিকায় নেই। এগুলো আগে থেকেই মেমরিতে, প্রসেসের নিজস্ব কাঠামোয় রাখা থাকে; switch-এর সময় এগুলো কপি করতে হয় না, শুধু “এখন কোন প্রসেস চলছে” নির্দেশকটি বদলায়। Context switch-এ যা সত্যিই সরাতে হয়, তা হলো CPU-র ভেতরে থাকা অবস্থা — কারণ CPU-তে register-এর একটিই সেট, দুটো প্রসেস একসাথে সেখানে বসতে পারে না।
সংরক্ষিত context কোথায় যায়? পাঠ্যবইয়ের উত্তর: প্রসেসের PCB (process control block)-এ, যা পর্ব ৮-এ দেখেছেন — প্রসেসের PID, অবস্থা, অগ্রাধিকার, খোলা ফাইল আর সংরক্ষিত register রাখার kernel-এর কাঠামো। বাস্তব kernel-এ ব্যাপারটি একটু ভাগ করা। Linux-এ PCB-র নাম task_struct. User mode-এর register-গুলো kernel-এ ঢোকার মুহূর্তেই thread-এর নিজস্ব kernel stack-এর মাথায় একটি কাঠামোয় (pt_regs) জমা হয়; আর kernel-এর stack pointer-টি রাখা হয় task_struct-এর ভেতরে। Windows-এ scheduling-এর একক thread, তাই এই তথ্য থাকে প্রতিটি thread-এর কাঠামোয় (KTHREAD). Thread-এর এমন কাঠামোকে সাধারণভাবে বলে thread control block.
নিচের ডায়াগ্রামে দুটি প্রসেস P1 আর P2 দেখুন। মানগুলো ছোট করে বানানো, কিন্তু কোন মান কখন কোথায় যায়, তা বাস্তবের মতোই। ডান দিকে CPU-র register আর দুই প্রসেসের সংরক্ষণ-এলাকা (save area) পাশাপাশি রাখা হয়েছে।
1P1 (user): total = total + x2timer interrupt! CPU pushes RIP, RSP, RFLAGS3kernel entry: push RAX..R15 (pt_regs)4schedule(): P1 -> ready, pick P25switch_mm: CR3 = P2's page table6switch_to: RSP = P2's kernel stack, pop regs7iretq -> P2 (user) continues
| mode | |
| RIP | |
| RSP | |
| RAX | |
| CR3 | |
| P1 save area | |
| P2 save area |
- P1 নিজের কাজ করছে। CPU user mode-এ। RIP = 0x1040 (পরের নির্দেশ), RSP = 0x7f00, RAX-এ চলমান হিসাব 42. CR3 দেখাচ্ছে P1-এর page table (0x5000). P2-এর পুরোনো অবস্থা তার save area-য় ঘুমিয়ে আছে: RIP 0x2210, RSP 0x6e80, RAX 7.
- Timer interrupt — হার্ডওয়্যার নিজেই তিনটি জিনিস রাখে। P1-এর সময় শেষ। CPU মোড kernel-এ তোলে, P1-এর kernel stack-এ চলে যায়, আর সেখানে RIP, RSP, RFLAGS (আরও কয়েকটি) push করে। এখন P1-এর save area-য় 0x1040 আর 0x7f00 আছে, RIP দেখাচ্ছে kernel-এর handler.
- Kernel-এর প্রবেশ-কোড বাকি register রাখে। Handler-এর প্রথম কাজ RAX থেকে R15 পর্যন্ত সব সাধারণ register একই kernel stack-এ push করা (Linux-এ এই কাঠামোর নাম
pt_regs). এখন RAX = 42-ও নিরাপদ। এর পরে kernel নিজের কাজে register ইচ্ছামতো ব্যবহার করতে পারে। - Scheduler পরের জনকে বাছে। Kernel দেখল P1-এর সময় শেষ, তাই
schedule()ডাকল। P1-এর অবস্থা “running” থেকে “ready”, আর সে ready queue-এর পেছনে গেল। Scheduler বাছল P2. RAX-এ এখন kernel-এর নিজের কোনো মান — P1-এর 42 তো আগেই সংরক্ষিত। - Address space বদল। P2 আলাদা প্রসেস, তাই তার মেমরি-মানচিত্র আলাদা। Kernel CR3-এ লেখে 0x9000 — P2-এর page table. এখন থেকে user-এর একই ঠিকানা (যেমন 0x6e80) P2-এর physical মেমরি দেখাবে। Kernel-এর নিজের অংশ সব প্রসেসের page table-এ একই থাকে, তাই kernel কোড চলতেই থাকে।
- Stack বদল আর register ফেরত।
switch_toRSP-তে বসায় P2-এর kernel stack-এর ঠিকানা — এই এক নির্দেশেই “আমি কে” বদলে গেল। তারপর kernel থেকে ফেরার পথে সেই stack থেকে P2-এর সংরক্ষিত register pop হয়: RAX আবার 7. - User mode-এ ফেরা — P2 কিছুই টের পায় না।
iretqনির্দেশ RIP = 0x2210, RSP = 0x6e80, RFLAGS ফিরিয়ে দিয়ে mode user করে। P2 ঠিক সেই নির্দেশ থেকে চলে, যেখানে আগেরবার থেমেছিল। P1-এর সবকিছু তার save area-য় অপেক্ষা করছে, পরের পালার জন্য।
Context switch-এর পুরো কৌশল আসলে একটি সরল সত্যের ওপর দাঁড়িয়ে: প্রতিটি thread-এর নিজস্ব kernel stack আছে, আর stack pointer বদলালেই “কোন thread চলছে” বদলে যায়। বাকি সব — register push/pop, CR3 লেখা — সেই বদলের আগে-পরের গোছগাছ।
২ · কখন context switch হয় — চারটি কারণ
Context switch নিজে থেকে হয় না; কোনো একটি ঘটনা kernel-কে নিয়ন্ত্রণ দেয়, আর kernel সিদ্ধান্ত নেয় এখন অন্য কাউকে CPU দেওয়া দরকার কি না। মনে রাখুন পর্ব ৫-এর কথা — kernel-এ ঢোকার পথ তিনটি: system call, interrupt আর exception. তাই switch-এর কারণগুলোও এই পথ দিয়েই আসে।
| কারণ | কীভাবে kernel-এ ঢোকে | ধরন | বাস্তব উদাহরণ |
|---|---|---|---|
| ১. সময় শেষ (quantum ফুরিয়েছে) | Timer interrupt | Involuntary | একটি হিসাব-ভারী লুপ, যে নিজে থেকে কখনো থামে না |
| ২. Blocking system call | System call | Voluntary | খালি pipe বা socket থেকে read(), ডিস্কের অপেক্ষা, sleep(), mutex না পাওয়া |
| ৩. নিজে থেকে ছেড়ে দেওয়া (yield) | System call (sched_yield()) | Voluntary | “আমার এখন জরুরি কিছু নেই, অন্য কেউ চলুক” |
| ৪. বেশি অগ্রাধিকারের কেউ জেগে উঠল | I/O interrupt বা অন্য প্রসেসের system call | Involuntary (preemption) | কিবোর্ডে চাপ পড়ায় editor জেগে উঠল, আর পটভূমির compile থামল |
| ৫. প্রসেস শেষ | exit() system call বা মারাত্মক exception | — | প্রোগ্রাম শেষ হলে CPU পরের জনের কাছে যায় (এটি switch, তবে ফেরার কেউ নেই) |
এখানে দুটি শব্দ খুব গুরুত্বপূর্ণ। Voluntary context switch মানে প্রসেস নিজেই এমন কিছু চেয়েছে যার জন্য অপেক্ষা করতে হবে, তাই সে blocked (waiting) অবস্থায় গিয়ে CPU ছেড়ে দিয়েছে। Involuntary context switch মানে প্রসেস চলতে প্রস্তুত ছিল, তবু kernel জোর করে তাকে সরিয়েছে — সময় শেষ বা অগ্রাধিকারের কারণে। প্রথমটি I/O-bound প্রোগ্রামের স্বভাব, দ্বিতীয়টি CPU-bound প্রোগ্রামের।
রেস্তোরাঁর রান্নাঘরে একটিই চুলা। বাবুর্চি ডাল বসিয়েছেন, কিন্তু ডাল ভিজতে আরও আধা ঘণ্টা লাগবে — তিনি নিজেই হাঁড়ি নামিয়ে পাশের অর্ডার ধরেন। এটি voluntary: অপেক্ষার কাজ, তাই নিজে ছাড়লেন। আবার কাবাব ভাজতে ভাজতে ম্যানেজার এসে বললেন, “পাঁচ মিনিট হয়ে গেছে, এবার টেবিল ৪-এর চা।” কাবাব তখনো ভাজার মতো অবস্থায় ছিল — তবু নামাতে হলো। এটি involuntary. আর যদি মন্ত্রীর গাড়ির ড্রাইভার এসে জরুরি অর্ডার দেন, সবকিছু থামিয়ে সেটি আগে — উচ্চ অগ্রাধিকারের preemption.
নিজের চোখে দেখা — Linux-এর দুটি গণক
Linux প্রতিটি প্রসেস (আসলে প্রতিটি thread)-এর জন্য দুটি সংখ্যা গুনে রাখে, আর /proc/<pid>/status ফাইলে দেখায়: voluntary_ctxt_switches আর nonvoluntary_ctxt_switches. নিচের script একটি bash shell-এর নিজের গণক তিনবার মাপে — অপেক্ষার কাজের আগে-পরে, হিসাব-ভারী লুপের আগে-পরে, আর একই CPU-তে একজন প্রতিদ্বন্দ্বী রেখে লুপের আগে-পরে। একটি সূক্ষ্ম বিষয়: /proc/self লিখলে “self” হতো grep প্রসেস নিজেই, তাই shell-এর নিজের PID $$ ব্যবহার করা হয়েছে।
snap() { # this shell's own counters: /proc/$$ (not /proc/self, which is grep)
v=$(grep '^voluntary_ctxt' /proc/$$/status | tr -dc 0-9)
n=$(grep '^nonvoluntary_ctxt' /proc/$$/status | tr -dc 0-9)
}
echo "cpus: $(nproc)"
snap; v0=$v; n0=$n
for i in $(seq 20); do sleep 0.01; done # 20 waits for a child
snap; echo "sleep x20 : voluntary +$((v - v0)), nonvoluntary +$((n - n0))"
snap; v0=$v; n0=$n
for ((i = 0; i < 200000; i++)); do :; done # pure CPU, no waiting
snap; echo "busy loop : voluntary +$((v - v0)), nonvoluntary +$((n - n0))"
taskset -cp "$(taskset -cp $$ | grep -o '[0-9]*$')" $$ >/dev/null 2>&1
( while :; do :; done ) & hog=$! # a rival on the same CPU
snap; v0=$v; n0=$n
for ((i = 0; i < 200000; i++)); do :; done
snap; echo "busy+rival: voluntary +$((v - v0)), nonvoluntary +$((n - n0))"
kill $hog; wait $hog 2>/dev/null; echo "rival stopped"একটি আসল রানের আউটপুট:
cpus: 2
sleep x20 : voluntary +26, nonvoluntary +0
busy loop : voluntary +4, nonvoluntary +8
busy+rival: voluntary +3, nonvoluntary +416
rival stopped
প্রথম লাইন: এই container-এ ২টি CPU. sleep x20: shell প্রতিটি sleep-এর জন্য একটি child প্রসেস চালায় আর তার শেষ হওয়ার অপেক্ষায় block হয় — ২০টি অপেক্ষা, সঙ্গে $(seq 20) আর snap-এর ভেতরের $(grep …)-এর অপেক্ষা মিলিয়ে voluntary বাড়ল ২৬. Involuntary একটিও নয় — shell কখনো চলতে চেয়ে সরানো হয়নি।
busy loop: লুপে কোনো অপেক্ষা নেই, তবু voluntary +৪ — সেগুলো এসেছে মাপার কাজ থেকেই (snap-এর দুটি $(…), প্রতিটিতে একটি child-এর অপেক্ষা)। মাপার যন্ত্র নিজেও ফলাফলে ছাপ ফেলে, এটি মনে রাখার মতো। Involuntary +৮ — মাঝে মাঝে container-এর অন্য কোনো কাজ এই CPU-তে এসে shell-কে সরিয়েছে। আরেকটি রানে একই ফাঁকা লুপেই involuntary এসেছিল +৮১৯, কারণ তখন সেই মেশিনের CPU-তে অন্যদের কাজ চলছিল — প্রতিদ্বন্দ্বী আপনার নিজের প্রোগ্রাম না হলেও থাকতে পারে।
busy+rival: taskset দিয়ে shell-কে একটি CPU-তে বাঁধা হয়েছে, আর একটি অসীম লুপ (hog) সেই একই CPU-তে চলছে। এবার involuntary লাফিয়ে +৪১৬ — দুজনই সবসময় চলতে প্রস্তুত, তাই scheduler তাদের বারবার জোর করে পালা বদলায়। এটিই timer-চালিত preemption. সংখ্যাগুলো প্রতিবার বদলাবে (CPU-র সংখ্যা, লোড, scheduler-এর সেটিং অনুযায়ী), কিন্তু প্রবণতা একই থাকবে: অপেক্ষা → voluntary, কাড়াকাড়ি → involuntary.
৩ · Dispatcher — পালাবদলের কারিগর
দুটি শব্দ প্রায়ই গুলিয়ে যায়। Scheduler ঠিক করে কে পরের পালা পাবে — এটি নীতি (policy), যেমন FCFS, Round Robin বা Linux-এর EEVDF (মডিউল ৩-এ বিস্তারিত)। Dispatcher হলো সেই প্রক্রিয়া (mechanism), যা সিদ্ধান্তটি কাজে পরিণত করে — আসলেই CPU এক প্রসেসের হাত থেকে নিয়ে অন্যের হাতে তুলে দেয়। ট্রাফিক মোড়ের উপমায়: কোন রাস্তার গাড়ি এখন যাবে, সেই নিয়ম scheduler; আর সিগন্যালের বাতি বদলানো, পুলিশের হাত তোলা — dispatcher.
Dispatcher-এর কাজকে পাঠ্যবই তিন বাক্যে বলে: context switch করা, user mode-এ ফেরানো, আর নতুন প্রোগ্রামের সঠিক জায়গায় লাফ দেওয়া। আরেকটু ভেঙে দেখলে ধাপগুলো এমন:
- পুরোনো প্রসেসের অবস্থা সংরক্ষণ — register, PC, SP, flags যায় PCB বা kernel stack-এ; প্রসেসের অবস্থা হয় ready বা blocked.
- পরের জনকে বাছাই — scheduler-কে ডাকা; সে ready queue থেকে একজনকে দেয়।
- Address space বদল — নতুনটি আলাদা প্রসেস হলে page table-এর ঠিকানা (CR3) বদল; একই প্রসেসের thread হলে এই ধাপ বাদ।
- নতুনের অবস্থা ফিরিয়ে আনা — kernel stack বদল, register ফেরত, FPU/SIMD অবস্থা ফেরত।
- User mode-এ ফেরা — বিশেষ return নির্দেশ দিয়ে mode নামিয়ে নতুন প্রসেসের সংরক্ষিত PC-তে লাফ।
এক প্রসেস থামানো থেকে আরেক প্রসেস চালু হওয়া পর্যন্ত সময়কে বলে dispatch latency. এই সময়ে কোনো user প্রোগ্রামের কাজ হয় না — CPU শুধু গোছগাছ করে। তাই dispatcher যত দ্রুত, তত ভালো; প্রতিটি সেকেন্ডে এটি শত থেকে হাজারবার চলতে পারে। Real-time সিস্টেমে (পর্ব ২০) dispatch latency-র ঊর্ধ্বসীমা জানা থাকা জরুরি, কারণ সেখানে “গড়ে দ্রুত” যথেষ্ট নয়, “কখনো দেরি নয়” দরকার।
এবার timer interrupt থেকে শুরু করে পুরো ঘটনাটি ধাপে ধাপে দেখুন। প্রতিটি ধাপে নিচের প্যানেলে ready queue আর ঘড়ির অবস্থা দেওয়া আছে।
- P1 চলছে
user modequantum শেষের পথে - Timer IRQ
kernel modeহার্ডওয়্যারের ঘণ্টা - অবস্থা রাখা
pt_regsP1-এর চিরকুট - বাছাই
schedule()কে পরের পালা - মেমরি-জগৎ বদল
CR3নতুন page table - ফেরত আনা
switch_toP2-এর stack আর register - P2 চলছে
user modeযেখানে থেমেছিল
running : P1 (quantum 4 ms, used 3.9 ms)
ready : [P2, P3]
blocked : [P4 — waiting for disk]
CPU : mode -> kernel, stack -> P1's kernel stack pushed : RIP, RSP, RFLAGS (by hardware) tick : P1 used 4.0 of 4 ms -> need_resched = 1
pt_regs : RAX RBX RCX ... R15 (by entry code) P1 state: running -> ready ready : [P2, P3, P1]
policy : round robin -> take the head
picked : P2
ready : [P3, P1]
CR3 : P1's table -> P2's table TLB : old entries useless (flushed, or kept with a PCID tag)
save : P1's kernel RSP -> P1's task_struct
load : RSP <- P2's kernel stack
restore : P2's registers, FPU/SIMD state
iretq : mode -> user, RIP = P2's saved PC
running : P2 (fresh quantum 4 ms)
ready : [P3, P1]
- P1 চলছে, quantum প্রায় শেষ। P1 পেয়েছে ৪ মিলিসেকেন্ড, ৩.৯ পার। Ready queue-তে অপেক্ষা করছে P2 আর P3; P4 ডিস্কের অপেক্ষায় blocked, তাই সে এই প্রতিযোগিতায় নেই।
- Timer interrupt. হার্ডওয়্যারের ঘড়ি সংকেত দিল। CPU সঙ্গে সঙ্গে kernel mode-এ গিয়ে P1-এর kernel stack-এ RIP, RSP আর RFLAGS রাখল। Tick-এর handler হিসাব করল P1-এর সময় ফুরিয়েছে, আর একটি পতাকা তুলল: “ফেরার আগে reschedule করতে হবে” (Linux-এ need_resched).
- অবস্থা সংরক্ষণ। প্রবেশ-কোড বাকি সব register একই stack-এ রেখেছে। P1 এখনো চলার মতো প্রস্তুত — শুধু সময় শেষ — তাই তার অবস্থা হলো ready, আর সে queue-এর শেষে গেল: [P2, P3, P1].
- Scheduler বাছাই করে। Round Robin নীতিতে queue-এর মাথা থেকে নেওয়া হয় — P2. বাছাইয়ের এই অংশটুকুই scheduling; বাকি সব dispatching. Queue এখন [P3, P1].
- Address space বদল। P2 আলাদা প্রসেস, তাই CR3-এ P2-এর page table বসল। TLB-তে P1-এর অনুবাদগুলো এখন অকেজো — হয় মুছে ফেলা হয়, নয়তো PCID ট্যাগ থাকায় রেখে দেওয়া যায় (অংশ ৫)।
- Stack আর register বদল।
switch_toP1-এর kernel stack pointer তারtask_struct-এ রেখে P2-এর kernel stack pointer CPU-তে বসায়। এরপর যা pop হয়, সবই P2-এর — তার register, তার FPU/SIMD অবস্থা। - P2 চলছে।
iretqদিয়ে user mode-এ ফেরা, ঠিক P2-এর সংরক্ষিত PC-তে। ধাপ ২ থেকে এ পর্যন্ত সময়টুকুই dispatch latency — আধুনিক কম্পিউটারে সাধারণত কয়েক মাইক্রোসেকেন্ড বা তারও কম, তবে হার্ডওয়্যার আর kernel-ভেদে বদলায়।
একটি প্রচলিত ভুল ধারণা: “প্রতিটি timer interrupt মানেই context switch.” না। Tick আসার পর kernel দেখে সময় শেষ হয়েছে কি না, বা ready queue-তে আরও কেউ আছে কি না। কেউ না থাকলে, বা সময় বাকি থাকলে, একই প্রসেসে ফিরে যায় — সেটি কেবল দুটি mode switch, কোনো context switch নয়।
৪ · Mode switch, context switch, process switch, thread switch
পরীক্ষায় আর ইন্টারভিউতে এই চারটি শব্দের পার্থক্য প্রায়ই জিজ্ঞেস করা হয়, আর অনেকে এগুলোকে এক মনে করেন। আসলে এরা একটি সিঁড়ির মতো — প্রতিটি ধাপে আগেরটির সব খরচ, সঙ্গে বাড়তি কিছু।
Mode switch
একই প্রসেস user mode থেকে kernel mode-এ যায় আর ফেরে — যেমন getpid(). কোনো প্রসেস বদলায় না। Kernel stack-এ register রাখা আর ফেরত — এটুকুই।
Thread switch (একই প্রসেস)
একই প্রসেসের এক thread থেকে আরেক thread. Register, stack, FPU অবস্থা বদলায়; কিন্তু page table একই, তাই CR3 লেখা হয় না, TLB অটুট থাকে।
মাঝারিProcess switch
এক প্রসেসের thread থেকে অন্য প্রসেসের thread. সবকিছু বদলায়, সঙ্গে address space — CR3, TLB-এর অনুবাদ, cache-এ থাকা ডেটার কাজে লাগা।
সবচেয়ে দামি“Context switch”
সাধারণ শব্দ — শেষ দুটির যেকোনোটি। Kernel-এর কাছে আসল প্রশ্ন একটাই: পরের thread কি একই address space-এর? হ্যাঁ হলে সস্তা পথ, না হলে পুরো পথ।
ছাতার মতো শব্দMode switch আর context switch-এর পার্থক্য মনে রাখার সহজ উপায়: mode switch-এ অভিনেতা একই, মঞ্চ বদলায়; context switch-এ অভিনেতাই বদলায়। প্রতিটি context switch-এর আগে অন্তত একটি mode switch থাকে (kernel-এ না ঢুকে তো dispatcher চলতে পারে না), কিন্তু বেশিরভাগ mode switch কোনো context switch ঘটায় না।
Linux-এ একটি মজার মিল আছে: kernel-এর চোখে process আর thread দুটিই task_struct — একই প্রসেসের thread-গুলো শুধু একই মেমরি-কাঠামো (mm) ভাগ করে। তাই Linux-এ thread switch আর process switch-এর কোড একই; পার্থক্য হয় একটি তুলনায় — আগের আর পরের task-এর mm এক কি না। এক হলে page table বদলানোর কাজ বাদ পড়ে। Windows-এও scheduler process নয়, thread-কেই বেছে নেয়; প্রসেস সেখানে মূলত একটি ধারক (container) — address space, handle-এর তালিকা আর নিরাপত্তা-পরিচয় রাখার জায়গা। পরের thread অন্য প্রসেসের হলে তবেই address space বদলায়।
1A: getpid() (Chrome tab, thread T1)2B: T1 -> T2 (same process: Chrome tab)3C: Chrome -> bash (another process, no PCID)4D: Chrome -> bash (another process, PCID on)5E: T2 -> kernel thread (e.g. kworker)
| user↔kernel mode | |
| register ও stack | |
| FPU/SIMD | |
| CR3 (page table) | |
| TLB | |
| cache-এর উষ্ণতা |
- শুধু mode switch. Chrome-এর একটি tab-এর thread T1
getpid()ডাকল। Kernel-এ ঢোকা, register kernel stack-এ রাখা, কাজ সেরে সেই একই register ফেরত দিয়ে T1-এই ফেরা। কেউ বদলায়নি, CR3 আর TLB ছোঁয়া হয়নি। এটি context switch নয়। - একই প্রসেসের thread switch. T1 একটি lock-এর অপেক্ষায় block হলো, একই প্রসেসের T2 চলল। Register, stack আর FPU অবস্থা বদলাল, কিন্তু দুজনের page table একই — kernel
mmমিলিয়ে দেখে CR3 লেখা বাদ দেয়। TLB-র অনুবাদ কাজে লাগে, আর দুজনের ভাগের ডেটা cache-এ থাকলে সেটাও। - Process switch, PCID ছাড়া। এবার পরের thread অন্য প্রসেস bash-এর। CR3 বদলাতেই হবে। পুরোনো x86 নিয়মে CR3 লেখা মানে TLB-এর user অংশের সব অনুবাদ মুছে যাওয়া — bash-এর প্রতিটি নতুন ঠিকানায় আবার page table হাঁটতে হবে। Cache-এ Chrome-এর ডেটা, bash-এর কাছে অকেজো।
- Process switch, PCID সহ। আধুনিক x86-এ প্রতিটি TLB entry-তে একটি address-space নম্বর (PCID) ট্যাগ থাকতে পারে; ARM-এ এর নাম ASID. তখন CR3 বদলালেও Chrome-এর অনুবাদ মুছতে হয় না — ট্যাগ আলাদা, তাই গুলিয়ে যাওয়ার ভয় নেই। Chrome-এ ফিরলে সেগুলো আবার কাজে লাগতে পারে। Cache-এর সমস্যা অবশ্য থেকেই যায়।
- Kernel thread-এ switch. Linux-এর kernel thread (যেমন
kworker)-এর নিজের কোনো user মেমরি নেই; সে kernel-এর অংশ ব্যবহার করে, যা সব page table-এ একই। তাই Linux আগের প্রসেসের page table “ধার করে” রেখে দেয় (lazy TLB) — CR3 বদলায় না। পরে আবার সেই প্রসেসে ফিরলে আরও সস্তা।
৫ · খরচ — প্রত্যক্ষ আর পরোক্ষ
Context switch-এর খরচকে দুই ভাগে ভাবুন। প্রত্যক্ষ খরচ হলো dispatcher-এর নিজের কাজ: kernel-এ ঢোকা, register রাখা ও ফেরানো, scheduler-এর বাছাই, CR3 লেখা, user mode-এ ফেরা। পরোক্ষ খরচ ধরা পড়ে switch শেষ হওয়ার পরে, নতুন প্রসেসের গতিতে — কারণ CPU-র সব “স্মৃতি” তখনো আগের প্রসেসের।
পরোক্ষ খরচ ১ — TLB
প্রতিটি মেমরি-ঠিকানা virtual থেকে physical-এ অনুবাদ করতে হয় (মডিউল ৫)। এই অনুবাদের ছোট, দ্রুত cache হলো TLB. পুরোনো x86-এ CR3-এ নতুন মান লিখলে TLB-র (global চিহ্ন ছাড়া) সব entry মুছে যায়। নতুন প্রসেসের প্রথম কয়েকশো মেমরি-ছোঁয়ার প্রতিটিতে তখন MMU-কে মেমরি থেকে page table পড়তে হয় — x86-64-এ চার স্তরের টেবিল। এই ক্ষতি কমাতে এসেছে address-space ট্যাগ: ARM-এর ASID বহু আগে থেকে, আর x86-এর PCID (Intel-এ প্রায় ২০১০ সালের প্রসেসর থেকে)। Linux ২০১৭-১৮ সাল থেকে PCID ব্যাপকভাবে ব্যবহার শুরু করে — বিশেষ করে Meltdown-এর পর KPTI চালু হওয়ায়, যেখানে প্রতিটি kernel-প্রবেশেই page table বদলায় (ভাবনার প্রশ্ন ৩)।
পরোক্ষ খরচ ২ — cache-এর দূষণ
P1 চলার সময় L1, L2 cache ভরে ছিল P1-এর ডেটা আর কোডে। P2 এসে নিজের ডেটা আনতে আনতে P1-এর অংশ সরিয়ে দেয় — একে বলে cache pollution. P1 পরে ফিরলে আবার cache miss, আবার মেমরি থেকে আনা। প্রসেসের working set (সক্রিয়ভাবে ব্যবহৃত মেমরি) যত বড়, ক্ষতি তত বেশি। এই খরচ মাপা কঠিন, কারণ এটি switch-এর মধ্যে নয়, পরের কয়েক মাইক্রো বা মিলিসেকেন্ডে ছড়িয়ে থাকে। Branch predictor-এর মতো CPU-র অন্য “শেখা” অবস্থাও একইভাবে আংশিক নষ্ট হয়, আর কিছু নিরাপত্তা-প্রতিরোধ (Spectre-এর পর) switch-এর সময় ইচ্ছা করেই এগুলো মুছে দেয়।
| ঘটনা | আনুমানিক মাত্রা (আধুনিক x86 ডেস্কটপ/সার্ভার) |
|---|---|
| একটি সাধারণ ফাংশন কল | ন্যানোসেকেন্ডের এক ভগ্নাংশ থেকে কয়েক ns |
| একটি mode switch (ছোট syscall) | প্রায় ১০০ ns থেকে কয়েকশো ns; নিরাপত্তা-প্রতিরোধ চালু থাকলে বেশি |
| Context switch-এর প্রত্যক্ষ খরচ | প্রায় ১ µs থেকে কয়েক µs |
| পরোক্ষ খরচ (cache/TLB আবার ভরা) | কয়েক µs থেকে, বড় working set-এ শত µs বা তারও বেশি সমতুল্য ক্ষতি |
সংখ্যাগুলো ইচ্ছা করেই মোটা দাগের — CPU-র প্রজন্ম, kernel-এর সংস্করণ, নিরাপত্তা-সেটিং, virtual machine-এ চলছে কি না — সব কিছুতে এগুলো কয়েক গুণ বদলায়। মনে রাখার মতো বিষয় হলো অনুপাত: একটি switch-এর সময়ে CPU কয়েক হাজার সাধারণ নির্দেশ চালাতে পারত।
নিজের হাতে মাপা — দুই প্রসেসের ping-pong
Context switch-এর খরচ মাপার একটি পুরোনো, সরল কৌশল: দুটি প্রসেস, দুটি pipe. Parent এক বাইট পাঠায়, তারপর উত্তরের জন্য read()-এ block হয়। Child বাইটটি পড়ে ফেরত পাঠায়, আবার পরের বাইটের জন্য block হয়। দুটোই একই CPU-তে বাঁধা থাকলে (CPU affinity) প্রতিটি আসা-যাওয়ায় অন্তত দুটি context switch ঘটতেই হবে — কারণ একজন না ঘুমালে আরেকজন চলতে পারে না। fork-এর বিস্তারিত পর্ব ১০-এ, pipe-এর পর্ব ১২-এ; এখানে শুধু মাপার কাজ।
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <sys/wait.h>
#define N 20000
static double now_us(void) {
struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t);
return t.tv_sec * 1e6 + t.tv_nsec / 1e3;
}
int main(void) {
int to_child[2], to_parent[2]; char b = 'x';
cpu_set_t one; /* keep both processes on ONE cpu */
CPU_ZERO(&one); CPU_SET(sched_getcpu(), &one);
int pinned = sched_setaffinity(0, sizeof one, &one) == 0;
if (pipe(to_child) < 0 || pipe(to_parent) < 0) { perror("pipe"); return 1; }
fflush(stdout);
pid_t pid = fork();
if (pid == 0) { /* child: echo every byte back */
for (int i = 0; i < N; i++)
if (read(to_child[0], &b, 1) != 1 || write(to_parent[1], &b, 1) != 1) _exit(1);
_exit(0);
}
double t0 = now_us();
for (int i = 0; i < N; i++) /* parent: send, then wait for the echo */
if (write(to_child[1], &b, 1) != 1 || read(to_parent[0], &b, 1) != 1) return 1;
double us = (now_us() - t0) / N;
waitpid(pid, NULL, 0);
printf("pinned to one CPU: %s, round trips: %d\n", pinned ? "yes" : "no", N);
printf("avg round trip : %.2f us\n", us);
printf("per switch (~1/2): %.2f us\n", us / 2);
return 0;
}একটি আসল রানের আউটপুট (সংখ্যা প্রতিবার আর প্রতি মেশিনে আলাদা হবে):
pinned to one CPU: yes, round trips: 20000
avg round trip : 18.12 us
per switch (~1/2): 9.06 us
pinned ... yes মানে দুটি প্রসেসই একই CPU-তে বাঁধা ছিল (child fork-এর সময় parent-এর affinity পায়)। ২০,০০০ বার আসা-যাওয়ায় গড়ে একটি round trip লেগেছে প্রায় ১৮ µs, তাই এক দিকের পালাবদল প্রায় ৯ µs.
কিন্তু এই ৯ µs খাঁটি context switch নয়। একটি round trip-এ আছে চারটি system call (দুটি write, দুটি read), প্রতিটিতে mode switch; pipe-এর ভেতরে ঘুমন্ত প্রসেসকে জাগানোর কাজ; scheduler-এর বাছাই; আর অন্তত দুটি পূর্ণ process switch (CR3 বদলসহ)। তাই সংখ্যাটিকে বলা উচিত “switch-সহ পুরো পথের খরচের ঊর্ধ্বসীমা”. এই রানটি হয়েছে একটি অনলাইন container-এ, সম্ভবত virtual machine-এর ভেতরে, যেখানে নিরাপত্তা-প্রতিরোধ আর virtualization-এর বাড়তি খরচ থাকে; সাধারণ bare-metal Linux ডেস্কটপে একই প্রোগ্রাম প্রায়ই কয়েক µs দেখায়। দুটি প্রসেস দুই আলাদা CPU-তে থাকলে (affinity না দিলে) সংখ্যা আরও বদলাত — তখন switch-এর বদলে মাপা হতো এক CPU থেকে আরেক CPU-কে জাগানোর খরচ। lmbench-এর lat_ctx আর perf bench sched pipe ঠিক এই কৌশলেই মাপে।
$ perf bench sched pipe # the same ping-pong, built into perf
# Running 'sched/pipe' benchmark:
# Executed 1000000 pipe operations between two processes
Total time: 4.871 [sec]
4.871203 usecs/op
205287 ops/sec
$ vmstat 1 3 # "cs" column = context switches per second (whole system)
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 812340 90412 2412220 0 0 3 11 402 911 3 1 96 0 0
2 0 0 812100 90412 2412236 0 0 0 0 618 1430 4 1 95 0 0৬ · হিসাব — overhead আর CPU efficiency
এবার পরীক্ষার সবচেয়ে প্রিয় অংশ। ধরুন time-sharing সিস্টেমে প্রতিটি প্রসেস একবারে q সময় পায় (time quantum), আর প্রতিটি context switch-এ s সময় লাগে, যখন কোনো উপকারী কাজ হয় না। প্রতিটি প্রসেস যদি পুরো quantum ব্যবহার করে (অর্থাৎ CPU-bound), তাহলে প্রতিটি চক্র দেখতে এমন: q সময় কাজ, s সময় switch, আবার q, আবার s…
Overhead-এর ভাগ = s / (q + s)
সূত্রটি থেকে দুটি সীমা পরিষ্কার: q → ∞ হলে efficiency → ১০০% (কিন্তু তখন Round Robin আসলে FCFS হয়ে যায়, ছোট কাজও দীর্ঘ অপেক্ষা করে); q → 0 হলে efficiency → 0 (CPU-র প্রায় সব সময় যায় পালাবদলে, আসল কাজ হয় না)। আর q = s হলে ঠিক অর্ধেক, ৫০%. তাই quantum বাছাই একটি দর-কষাকষি — response time বনাম efficiency (বিস্তারিত পর্ব ১৮-এ)।
সমাধান করা উদাহরণ ১ — সরাসরি সূত্র
প্রশ্ন: q = ১০ ms, s = ০.১ ms. (ক) CPU efficiency কত? (খ) q কমিয়ে ১ ms করলে? (গ) Efficiency অন্তত ৯৫% রাখতে q কমপক্ষে কত হতে হবে?
সমাধান: (ক) ১০ ÷ (১০ + ০.১) = ১০ ÷ ১০.১ ≈ ৯৯.০%. (খ) ১ ÷ ১.১ ≈ ৯০.৯% — quantum দশ ভাগের এক ভাগ করায় overhead প্রায় ১% থেকে ৯%-এ উঠল। (গ) q ÷ (q + ০.১) ≥ ০.৯৫ ⇒ q ≥ ০.৯৫q + ০.০৯৫ ⇒ ০.০৫q ≥ ০.০৯৫ ⇒ q ≥ ১.৯ ms.
সমাধান করা উদাহরণ ২ — প্রসেস quantum-এর আগেই block হলে
বাস্তবে অনেক প্রসেস পুরো quantum ব্যবহার করে না — I/O-র জন্য আগেই block হয়। ধরুন গড়ে একটি প্রসেস T সময় চলে তারপর block হয়, switch-এর খরচ s, আর quantum q. পাঠ্যবইগুলোর একটি পরিচিত অনুশীলন:
| পরিস্থিতি | প্রতি T কাজের জন্য switch | Efficiency |
|---|---|---|
| q = ∞ | ১টি (block হওয়ার সময়) | T / (T + s) |
| q > T | ১টি — quantum ফুরানোর আগেই block | T / (T + s) |
| s < q < T | প্রায় T/q টি | T / (T + s·T/q) = q / (q + s) |
| q = s | T/q টি | q / (q + q) = ৫০% |
| q → 0 | অসীমের দিকে | → 0 |
খেয়াল করুন, q যখন T-এর চেয়ে বড়, তখন quantum আরও বাড়িয়ে লাভ নেই — efficiency আটকে থাকে T/(T+s)-এ, কারণ switch-টা হচ্ছে block হওয়ার কারণে, quantum-এর কারণে নয়। সংখ্যায়: T = ২০ ms, s = ১ ms হলে q = ১০০ ms-এ efficiency = ২০/২১ ≈ ৯৫.২%; q = ৫ ms-এ ৫/৬ ≈ ৮৩.৩%; q = ১ ms-এ ৫০%.
সমাধান করা উদাহরণ ৩ — Gantt chart-এ overhead ধরা
তিনটি প্রসেস একসাথে (t = 0) আসে: burst time P1 = ৭, P2 = ৪, P3 = ৫ ms. Round Robin, q = ৪ ms, আর ভিন্ন প্রসেসে যেতে প্রতিবার s = ১ ms. (একই প্রসেস পরপর চললে switch নেই; প্রথম প্রসেস চালু করার খরচ ধরা হয়নি — প্রশ্নে অন্য নিয়ম বলা থাকলে সেটিই মানবেন।) নিচে ধাপে ধাপে দেখুন।
1 0- 4 P1 runs 42 4- 5 switch P1 -> P23 5- 9 P2 runs 4 (done)4 9-10 switch P2 -> P3510-14 P3 runs 4614-15 switch P3 -> P1715-18 P1 runs 3 (done)818-19 switch P1 -> P3919-20 P3 runs 1 (done)
| t | |
| CPU-তে | |
| ready queue | |
| বাকি P1/P2/P3 | |
| উপকারী সময় | |
| overhead |
- ০–৪: P1 পুরো quantum চলে। P1-এর বাকি ৭ − ৪ = ৩. সময় শেষ, তাই সে queue-এর পেছনে: [P2, P3, P1]. উপকারী সময় ৪, overhead ০.
- ৪–৫: প্রথম switch. Dispatcher P1-এর অবস্থা রেখে P2-কে আনে। এই ১ ms-এ কারও কাজ হয় না। Queue: [P3, P1]; overhead ১.
- ৫–৯: P2 শেষ। P2-এর burst ৪, quantum ৪ — ঠিক শেষ হলো, t = ৯-এ। সে আর queue-তে ফেরে না। উপকারী সময় ৮.
- ৯–১০: দ্বিতীয় switch. P2 শেষ হলেও CPU-কে পরের প্রসেসে নিতে switch লাগে — P3. Queue: [P1]; overhead ২.
- ১০–১৪: P3 চার ms চলে। P3-এর বাকি ৫ − ৪ = ১; সে queue-এর পেছনে: [P1, P3]. উপকারী সময় ১২.
- ১৪–১৫: তৃতীয় switch, P1-এ। Queue: [P3]; overhead ৩.
- ১৫–১৮: P1 শেষ। বাকি ছিল ৩, quantum ৪-এর কম, তাই ৩ ms চলেই শেষ, t = ১৮. উপকারী সময় ১৫.
- ১৮–১৯: চতুর্থ switch, P3-এ। Queue খালি; overhead ৪.
- ১৯–২০: P3 শেষ। ফল: মোট সময় ২০, উপকারী ১৬, overhead ৪ → efficiency = ১৬/২০ = ৮০%, যা q/(q+s) = ৪/৫-এর সাথে মেলে। Turnaround: P1 = ১৮, P2 = ৯, P3 = ২০, গড় ৪৭/৩ ≈ ১৫.৬৭ ms. Switch-এর খরচ শূন্য ধরলে একই চার্টে সমাপ্তি হতো ১৫, ৮, ১৬ — গড় ১৩ ms. চারটি switch গড় turnaround প্রায় ২.৬৭ ms বাড়িয়েছে।
সূত্র আর আসল চার্ট কি সবসময় মেলে? নিচের simulator একই তিনটি প্রসেস বিভিন্ন quantum-এ চালিয়ে দুটো পাশাপাশি দেখায়। burst, s বা quantum-এর তালিকা বদলে আবার চালিয়ে দেখুন।
// Round Robin, all arrive at t=0. A switch to a DIFFERENT process costs s ms.
const burst = { P1: 7, P2: 4, P3: 5 }; // CPU time each process needs (ms)
const s = 1; // fixed switch overhead (ms)
function rr(q) {
const left = { ...burst }, queue = Object.keys(burst), line = [];
let useful = 0, overhead = 0, switches = 0, last = null;
while (queue.length) {
const p = queue.shift();
if (last !== null && last !== p) { overhead += s; switches++; line.push("|cs|"); }
const run = Math.min(q, left[p]);
useful += run; left[p] -= run; line.push(p + ":" + run);
if (left[p] > 0) queue.push(p);
last = p;
}
return { q, useful, overhead, switches, line: line.join(" ") };
}
console.log("timeline q=4: " + rr(4).line);
console.log(" q | switches | useful | overhead | efficiency | q/(q+s)");
for (const q of [1, 2, 4, 8, 16]) {
const r = rr(q), eff = (100 * r.useful) / (r.useful + r.overhead);
console.log(String(q).padStart(2) + " | " + String(r.switches).padStart(8) + " | " +
String(r.useful).padStart(6) + " | " + String(r.overhead).padStart(8) + " | " +
(eff.toFixed(1) + "%").padStart(10) + " | " + ((100 * q) / (q + s)).toFixed(1) + "%");
}আউটপুট (simulator নির্ধারিত, তাই প্রতিবার একই):
timeline q=4: P1:4 |cs| P2:4 |cs| P3:4 |cs| P1:3 |cs| P3:1
q | switches | useful | overhead | efficiency | q/(q+s)
1 | 14 | 16 | 14 | 53.3% | 50.0%
2 | 8 | 16 | 8 | 66.7% | 66.7%
4 | 4 | 16 | 4 | 80.0% | 80.0%
8 | 2 | 16 | 2 | 88.9% | 88.9%
16 | 2 | 16 | 2 | 88.9% | 94.1%
প্রথম লাইনটি উপরের stepper-এর Gantt chart — চারটি |cs|. টেবিলে q = ২ আর ৪-এ সূত্র আর simulator হুবহু মেলে, কারণ প্রায় সব slice পুরো quantum-এর। q = ১-এ simulator সামান্য বেশি (৫৩.৩% বনাম ৫০%) — শেষে শুধু P1 বাকি থাকায় সে পরপর দুবার চলে, মাঝে switch লাগে না, তাই ১৬টি slice-এ ১৪টি switch. q = ১৬-এ উল্টো: প্রতিটি প্রসেস quantum-এর অনেক আগেই শেষ, ফলে switch কমে মাত্র ২টিতে থেমে যায়, আর efficiency ৮৮.৯%-এ আটকে থাকে — সূত্রের ৯৪.১% এখানে খাটে না। শিক্ষা: q/(q+s) সূত্রটি তখনই সঠিক, যখন প্রসেসগুলো পুরো quantum ব্যবহার করে; burst ছোট হলে সংখ্যা গুনে Gantt chart থেকেই হিসাব করতে হয়।
সমাধান করা উদাহরণ ৪ — সবচেয়ে বেশি অপেক্ষা আর প্রতি সেকেন্ডের খরচ
প্রশ্ন: ৫টি CPU-bound প্রসেস Round Robin-এ, q = ২০ ms, s = ২ ms. (ক) একটি প্রসেস তার এক পালা শেষে আবার CPU পাওয়ার আগে সর্বোচ্চ কত অপেক্ষা করে? (খ) প্রতি সেকেন্ডে কতগুলো switch হয়, আর কত সময় overhead-এ যায়?
সমাধান: (ক) বাকি (n − 1) = ৪টি প্রসেসের প্রত্যেকে q চলে, আর প্রতিবার একটি করে switch — নিজের কাছে ফেরার switch-সহ মোট ৫টি: ৪ × ২০ + ৫ × ২ = ৯০ ms. (সাধারণ সূত্র: (n − 1)·q + n·s; অনেক বই সরলভাবে (n − 1)(q + s) লেখে, যেখানে শেষ switch-টি বাদ পড়ে = ৮৮ ms — প্রশ্নে কোনটি চাওয়া হয়েছে তা দেখুন।) (খ) একটি চক্র q + s = ২২ ms, তাই প্রতি সেকেন্ডে ১০০০ ÷ ২২ ≈ ৪৫টি switch; overhead = s/(q+s) = ২/২২ ≈ ৯.১%, অর্থাৎ প্রতি সেকেন্ডে প্রায় ৯১ ms.
বাস্তবের সাথে তুলনা: পরীক্ষার প্রশ্নে s সাধারণত ১ ms-এর মতো বড় ধরা হয়, যাতে হিসাব সহজ হয়। আধুনিক কম্পিউটারে প্রত্যক্ষ খরচ মাইক্রোসেকেন্ডে আর quantum মিলিসেকেন্ডে, তাই সূত্র অনুযায়ী overhead এক শতাংশেরও অনেক কম। তবু বাস্তবে প্রতি সেকেন্ডে লক্ষাধিক switch করে এমন সার্ভারে (অনেক ছোট অনুরোধ, অনেক thread) পরোক্ষ খরচসহ ক্ষতি চোখে পড়ার মতো হয়।
৭ · Cooperative বনাম preemptive multitasking
এ পর্যন্ত আমরা ধরে নিয়েছি, kernel timer interrupt দিয়ে যেকোনো সময় CPU কেড়ে নিতে পারে। এটিই preemptive multitasking. কিন্তু আরেকটি পথ আছে, যা ইতিহাসে বহু বছর চলেছে: cooperative multitasking — প্রতিটি প্রোগ্রাম নিজে থেকে মাঝে মাঝে CPU ছেড়ে দেবে (yield), এই ভরসায়। Switch হয় শুধু voluntary পথে।
Cooperative
প্রোগ্রাম নিজে yield করে, বা কোনো সিস্টেম-ফাংশন ডাকলে তখন পালা বদলায়। সরল, আর switch ঘটে জানা জায়গায় — তাই lock কম লাগে। কিন্তু একটি প্রোগ্রাম অসীম লুপে আটকে গেলে পুরো সিস্টেম জমে যায়।
Preemptive
Timer interrupt-এ kernel যেকোনো সময় CPU কেড়ে নেয়। একটি খারাপ প্রোগ্রাম কেবল নিজেকে ধীর করে, বাকিরা চলতে থাকে। বিনিময়ে কোড যেকোনো জায়গায় থামতে পারে বলে synchronization (মডিউল ৪) জরুরি।
Unix, Windows NT/95-এর পর থেকে, macOS, Linux, Android, iOS১৯৯০-এর দশকের শুরুর Windows 3.x-এ Windows অ্যাপগুলো cooperative ভাবে চলত: প্রতিটি অ্যাপ বার্তা-লুপে (GetMessage বা PeekMessage) ফিরলে তবেই অন্য অ্যাপ পালা পেত। একটি অ্যাপ দীর্ঘ হিসাবে আটকে গেলে মাউসের ঘড়ি-আইকন ঘুরত, আর পুরো ডেস্কটপ সাড়া দিত না। Windows 95 ৩২-বিট অ্যাপের জন্য preemptive multitasking আনে (পুরোনো ১৬-বিট অ্যাপগুলো নিজেদের মধ্যে তখনো cooperative থাকত), আর Windows NT শুরু থেকেই preemptive. Apple-এর Classic Mac OS-এ অ্যাপগুলোর মধ্যে cooperative multitasking চলেছে ২০০১ সালে Unix-ভিত্তিক Mac OS X আসা পর্যন্ত। অন্যদিকে Unix ১৯৭০-এর দশক থেকেই time-sharing-এর জন্য preemptive.
Cooperative ধারণা কিন্তু হারিয়ে যায়নি — জায়গা বদলেছে। আজ এটি থাকে প্রোগ্রামের ভেতরে: JavaScript-এর event loop, Python-এর asyncio, Kotlin-এর coroutine — এরা একটি thread-এর ভেতরে কাজ ভাগ করে, আর await-এ পৌঁছালে তবেই পালা বদলায়। ব্রাউজারের একটি tab-এ কেউ অসীম লুপ লিখলে সেই tab জমে যায় — ঠিক Windows 3.x-এর মতো — কিন্তু OS preemptive বলে বাকি কম্পিউটার চলতে থাকে। আধুনিক ব্যবস্থা তাই দুই স্তরের: বাইরে preemptive kernel, ভেতরে চাইলে সস্তা cooperative switch (ভাবনার প্রশ্ন ৪)।
Kernel নিজে কি preemptive? এটি আলাদা প্রশ্ন। পুরোনো Linux-এ (২.৪ পর্যন্ত) kernel কোড চলার সময় অন্য কেউ CPU নিতে পারত না, শুধু user mode-এ ফেরার সময় switch হতো। Linux ২.৬ থেকে kernel preemption বিকল্প হিসেবে এসেছে (CONFIG_PREEMPT); নতুন সংস্করণে (৬.x) কোন মাত্রার preemption চলবে তা বুট-সময়েও বেছে নেওয়া যায়, আর real-time-এর PREEMPT_RT অংশ মূল kernel-এ যুক্ত হয়েছে ২০২৪ সালে (Linux 6.12).
৮ · বাস্তব kernel-এ — Linux, Windows, macOS
Linux: schedule() → context_switch() → switch_to
Linux-এ পালাবদলের কেন্দ্র kernel/sched/core.c ফাইলের __schedule() ফাংশন। দুইভাবে এটি ডাকা হয়: কোনো task নিজে block হতে গিয়ে (voluntary), অথবা interrupt বা system call থেকে ফেরার পথে need_resched পতাকা ওঠা দেখে (involuntary). নিচের অংশটি বহু লাইন বাদ দিয়ে সরল করা — শুধু কাঠামো দেখানোর জন্য, চালানোর জন্য নয়।
static void __schedule(...)
{
struct rq *rq = this_rq(); /* this CPU's run queue */
struct task_struct *prev = rq->curr, *next;
if (prev is going to sleep) /* e.g. blocked in read() */
deactivate_task(rq, prev); /* take it off the run queue */
next = pick_next_task(rq, prev); /* POLICY: fair, RT, deadline... */
clear_tsk_need_resched(prev);
if (prev != next) {
rq->nr_switches++;
rq = context_switch(rq, prev, next); /* MECHANISM */
}
}
static struct rq *context_switch(struct rq *rq,
struct task_struct *prev, struct task_struct *next)
{
if (!next->mm) /* kernel thread: no user memory */
enter_lazy_tlb(prev->active_mm, next); /* keep the old page table */
else
switch_mm_irqs_off(prev->active_mm, next->mm, next); /* CR3; skipped if same mm */
switch_to(prev, next, prev); /* kernel stack + registers */
return finish_task_switch(prev); /* runs in NEXT's context */
}switch_to একটি macro, যা x86-64-এ assembly-র __switch_to_asm ডাকে (arch/x86/entry/entry_64.S). এর মূল অংশ কয়েক লাইনের:
__switch_to_asm: ; rdi = prev, rsi = next
push rbp, rbx, r12, r13, r14, r15 ; callee-saved regs onto PREV's kernel stack
mov [rdi + thread.sp], rsp ; remember where prev's stack is
mov rsp, [rsi + thread.sp] ; ---- from here we are on NEXT's stack ----
pop r15, r14, r13, r12, rbx, rbp ; next's callee-saved regs
jmp __switch_to ; C part: FPU state, FS/GS base, per-CPU stack topলক্ষ করুন, এখানে মাত্র ছয়টি register রাখা হয়। কারণ এটি একটি সাধারণ C ফাংশন-কলের মতো: বাকি register রাখার দায় C-এর নিয়মে ডাকার পক্ষের, আর user mode-এর সব register তো kernel-এ ঢোকার মুহূর্তেই pt_regs-এ রাখা হয়ে গেছে (অংশ ১-এর stepper). __switch_to() C ফাংশন বাকি কাজ করে: FPU/SIMD অবস্থার ব্যবস্থা (আধুনিক x86 Linux-এ এটি XSAVE দিয়ে রাখা হয়, আর নতুন thread-এর অবস্থা user mode-এ ফেরার আগে ফিরিয়ে আনা হয়), thread-local storage-এর FS/GS base, আর পরের interrupt-এ কোন kernel stack ব্যবহার হবে তা CPU-কে জানানো।
Windows: thread-ই scheduling-এর একক
Windows NT পরিবার (আজকের Windows 10/11-সহ) শুরু থেকেই thread ধরে schedule করে; process সেখানে address space, handle table আর নিরাপত্তা-token-এর ধারক। Kernel-এর যে অংশ thread বাছাই আর পালাবদল করে, Microsoft-এর নথি ও Windows Internals বইয়ে তাকে বলা হয় dispatcher — এখান থেকেই পাঠ্যবইয়ের শব্দটি পরিচিত। প্রতিটি thread-এর context থাকে তার KTHREAD কাঠামো আর kernel stack-এ; Windows-এ ৩২টি অগ্রাধিকার-স্তর (০–৩১) আছে, আর সবচেয়ে উঁচু অগ্রাধিকারের ready thread-ই চলে (পর্ব ৫৬-এ বিস্তারিত)। পুরো সিস্টেমে প্রতি সেকেন্ডে কতগুলো switch হচ্ছে, দেখা যায় Performance Monitor-এর একটি counter দিয়ে:
PS> Get-Counter '\System\Context Switches/sec' -SampleInterval 1 -MaxSamples 2
Timestamp CounterSamples
--------- --------------
25-10-2026 10:15:01 \\mypc\system\context switches/sec :
8412.37
25-10-2026 10:15:02 \\mypc\system\context switches/sec :
9105.88macOS/iOS-এর kernel XNU-এর scheduling অংশ এসেছে Mach থেকে; সেখানেও scheduling-এর একক Mach thread, আর প্রসেস (BSD-র দিক থেকে proc, Mach-এর দিক থেকে task) মূলত ধারক। তিনটি বড় OS তাই একই জায়গায় মিলেছে: CPU পায় thread, address space-এর মালিক process.
আরও গভীরে: শিক্ষণ-OS xv6 (পর্ব ৫৮)-এ context switch-এর পুরো assembly মাত্র কয়েক লাইনের swtch — RISC-V সংস্করণে ra, sp আর s0–s11 রাখে, আর প্রতিটি CPU-র একটি আলাদা “scheduler thread” থাকে, যার মধ্য দিয়ে প্রতিটি switch যায় (P1 → scheduler → P2). Linux-এ এক সময় FPU অবস্থা “lazy” ভাবে বদলানো হতো — নতুন thread FPU ছুঁলে তবেই। আধুনিক CPU-তে XSAVE দ্রুত হওয়ায় Linux ২০১৬ সালের দিকে eager পদ্ধতিকে ডিফল্ট করে, আর পরে lazy পদ্ধতি পুরোপুরি সরিয়ে দেয়; ২০১৮ সালে প্রকাশিত LazyFP ত্রুটি (আগের প্রসেসের FPU মান speculative execution দিয়ে আঁচ করা) দেখাল, সিদ্ধান্তটি নিরাপত্তার দিক থেকেও ঠিক ছিল। গবেষণায় বারবার দেখা গেছে, switch-এর আসল খরচের বড় অংশ পরোক্ষ — cache আর TLB আবার ভরা — যা working set-এর আকারে অনেক গুণ বাড়তে পারে।
৯ · AI-এর যুগে context switch
“আমার সার্ভার ধীর, context switch অনেক বেশি — কী করব?” — AI-কে জিজ্ঞেস করলে লম্বা তালিকা পাবেন। কিছু পরামর্শ কাজের, কিছু ভুল OS-এর, কিছু পুরোনো, আর কিছু ক্ষতিকর। এই পর্বের ধারণাগুলো দিয়ে সেগুলো যাচাই করুন:
| AI-এর ভুল | কেন ভুল | কীভাবে যাচাই করবেন |
|---|---|---|
| “Context switch সবসময় কয়েক মিলিসেকেন্ড লাগে” | প্রত্যক্ষ খরচ সাধারণত মাইক্রোসেকেন্ডে; পরীক্ষার s = ১ ms একটি সহজ-হিসাবের ধারণা | perf bench sched pipe বা উপরের ping-pong নিজের মেশিনে চালান |
| “Thread switch আর process switch একই খরচ” | একই প্রসেসের thread-এ CR3 বদলায় না, TLB অটুট থাকে | দুটি thread আর দুটি process দিয়ে একই ping-pong মেপে তুলনা করুন |
| “Thread বাড়ালেই দ্রুত হবে” — CPU core ৮টি, thread ৫০০টি | CPU-bound কাজে বাড়তি thread মানে বাড়তি involuntary switch আর cache দূষণ | pidstat -w বা /proc/<pid>/status-এ nonvoluntary গণক দেখুন |
| Linux-এর কমান্ড Windows-এ (বা উল্টো) দেওয়া | vmstat Windows-এ নেই; Windows-এ Performance Monitor counter | প্রম্পটে OS আর সংস্করণ স্পষ্ট লিখুন |
| “Spinlock দিয়ে switch এড়ান” — সব জায়গায় | এক core-এ বা দীর্ঘ অপেক্ষায় spin করলে CPU নষ্ট হয়, উল্টো ধীর হয় | লোড দিয়ে মাপুন; core সংখ্যা আর অপেক্ষার সময় দেখুন (পর্ব ২৩) |
সমস্যা সারাতে sudo দিয়ে kernel-এর scheduler সেটিং বদলানো বা নিরাপত্তা-প্রতিরোধ (যেমন KPTI) বন্ধ করা | ভুল সেটিং পুরো সিস্টেম অস্থির করে; প্রতিরোধ বন্ধ করা মানে জানা ত্রুটির দরজা খোলা | আগে মেপে কারণ খুঁজুন; নিরাপত্তা-প্রতিরোধ বন্ধ করার পরামর্শ মানবেন না |
| Efficiency-র অঙ্কে ভুল — শেষ প্রসেসের switch গোনা, বা burst ছোট হলেও q/(q+s) বসানো | সূত্র খাটে শুধু পুরো quantum ব্যবহার হলে | Gantt chart এঁকে switch হাতে গুনুন, উপরের simulator দিয়ে মেলান |
AI-কে performance-এর প্রশ্ন করলে পরিবেশ, মাপা তথ্য আর যাচাইয়ের শর্ত দিন:
AI-এর উত্তর মানার আগে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Context switch কী, আর একে overhead বলা হয় কেন? — এক প্রসেস/thread-এর CPU অবস্থা সংরক্ষণ করে আরেকটির অবস্থা ফিরিয়ে আনা; এই সময়ে কোনো user প্রোগ্রামের কাজ হয় না।
- Scheduler আর dispatcher-এর পার্থক্য? Dispatch latency কী? — scheduler বাছে কে চলবে (policy), dispatcher তাকে আসলে CPU-তে বসায় (mechanism); এক প্রসেস থামানো থেকে অন্যটি শুরু পর্যন্ত সময় dispatch latency.
- Thread switch কেন process switch-এর চেয়ে সস্তা? — একই address space, তাই page table (CR3) বদলায় না, TLB আর ভাগের cache-ডেটা কাজে লাগে।
- q = ৪ ms, s = ১ ms হলে CPU efficiency? — q/(q+s) = ৪/৫ = ৮০% (প্রসেসগুলো পুরো quantum ব্যবহার করলে)।
- Mode switch কি context switch? — না; একই প্রসেস user↔kernel যায়-আসে, কেউ বদলায় না।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Linux, Windows আর macOS — তিনটিই কেন process নয়, thread-কে scheduling-এর একক বানিয়েছে?
শুরুর Unix-এ (১৯৭০-এর দশক) thread বলে কিছু ছিল না; প্রতিটি প্রসেসে একটিই নির্বাহ-ধারা, তাই “process schedule করা” আর “thread schedule করা” একই কথা ছিল। ১৯৮০-৯০-এর দশকে multiprocessor কম্পিউটার আর GUI প্রোগ্রাম আসায় একটি প্রোগ্রামের ভেতরে একাধিক ধারা দরকার হলো — একটি ধারা স্ক্রিন আঁকবে, একটি নেটওয়ার্ক থেকে পড়বে, একটি হিসাব করবে। এখন প্রশ্ন দাঁড়াল: CPU কাকে দেওয়া হবে? প্রসেসকে দিলে ভেতরের কোন ধারা চলবে, সেটি আবার আলাদা করে ঠিক করতে হয়, আর একটি ধারা block হলে পুরো প্রসেস আটকে যেতে পারে।
Windows NT (১৯৯৩) এই প্রশ্নের পরিষ্কার উত্তর দিয়ে শুরু করেছিল: process হলো সম্পদের ধারক — address space, খোলা handle, নিরাপত্তা-পরিচয় — আর CPU পায় thread. Mach microkernel-ও (যা থেকে macOS-এর XNU-র বড় অংশ এসেছে) একই ভাগ করেছিল: task হলো ধারক, thread হলো চলার একক। Linux একটু অন্য পথে গেল: kernel-এ আলাদা “thread” কাঠামো নেই, সবকিছু task (task_struct). clone() system call দিয়ে নতুন task বানানোর সময় বলে দেওয়া যায় কোন কোন সম্পদ (মেমরি, ফাইল টেবিল, signal handler) আগের task-এর সাথে ভাগ হবে। সব ভাগ করলে সেটিই “thread”. শুরুর LinuxThreads লাইব্রেরিতে POSIX-এর সাথে অনেক অমিল ছিল; ২০০৩ সালের দিকে NPTL (Native POSIX Thread Library) আর kernel-এর সহায়তায় সেগুলো ঠিক হয়।
এই নকশার লাভ সরাসরি এই পর্বের বিষয়ের সাথে যুক্ত। Scheduler-কে শুধু একটি প্রশ্নের উত্তর দিতে হয়: “কোন runnable thread পরের পালা পাবে?” Dispatcher দেখে নেয়, পরেরটি একই address space-এর কি না — হলে CR3 আর TLB-এর খরচ বাদ। একটি thread block হলে একই প্রসেসের অন্য thread চলতে পারে, আর একাধিক core-এ একই প্রসেসের thread-গুলো আসলেই সমান্তরালে চলে। দামও আছে: thread-গুলো মেমরি ভাগ করে, তাই data race-এর ঝুঁকি (মডিউল ৪)। আর একটি প্রসেসে হাজারো thread থাকলে scheduler “ন্যায্যতা” কার মধ্যে মাপবে — thread-এ, নাকি প্রসেস বা ব্যবহারকারীর দলে? Linux এর উত্তর দেয় cgroup দিয়ে (পর্ব ২১)।
মূল উপলব্ধি: “কে CPU পায়” আর “কে মেমরি ও সম্পদের মালিক” — দুটি আলাদা প্রশ্ন। Thread আর process আলাদা করে আধুনিক OS দুটোকেই সরল করেছে, আর switch-এর খরচ কমানোর পথ খুলেছে।
প্র ০২Time quantum কত হওয়া উচিত? ছোট quantum-এর response আর বড় quantum-এর efficiency — বাস্তব OS এই টানাপোড়েন কীভাবে মেটায়?
সূত্র q/(q+s) বলছে quantum বড় হলেই ভালো। কিন্তু Round Robin-এ n টি প্রসেস থাকলে একজনের পরের পালার জন্য অপেক্ষা প্রায় (n − 1)(q + s). ১০টি প্রসেস আর q = ১০০ ms হলে editor-এ একটি অক্ষর টাইপ করার পর সাড়া পেতে প্রায় এক সেকেন্ড লাগতে পারে — মানুষের চোখে স্পষ্ট ধীরতা। আবার q = ০.০১ ms হলে, s যদি ০.০০৫ ms হয়, CPU-র এক-তৃতীয়াংশ সময় যায় পালাবদলে। পাঠ্যবইয়ের একটি প্রচলিত পরামর্শ: quantum এমন হোক যাতে বেশিরভাগ (প্রায় ৮০%) CPU burst এক quantum-এর মধ্যেই শেষ হয় — তাহলে বেশিরভাগ switch হয় প্রসেস নিজে block হওয়ায়, quantum ফুরানোয় নয়।
বাস্তব OS আরও চতুর। প্রথমত, তারা I/O-bound আর CPU-bound প্রসেস আলাদা করে চেনে। যে প্রসেস বারবার নিজে থেকে block হয় (editor, shell), সে জেগে উঠলে দ্রুত CPU পায়; আর হিসাব-ভারী প্রসেস লম্বা slice পায় কিন্তু কম ঘন ঘন। মাল্টিলেভেল ফিডব্যাক কিউ (পর্ব ১৯) এই ধারণার পাঠ্যবই-রূপ। দ্বিতীয়ত, Linux-এর CFS (২০০৭ সালে Linux 2.6.23) কোনো স্থির quantum ব্যবহার করত না: একটি “target latency” সময়ের মধ্যে সব runnable task অন্তত একবার চলবে, এই লক্ষ্যে প্রতিটির slice হিসাব হতো; task বেশি হলে slice ছোট হতো, কিন্তু একটি ন্যূনতম সীমার নিচে নয় — ঠিক efficiency ধসে পড়া ঠেকাতে। Linux 6.6 (২০২৩) থেকে EEVDF নামের নতুন নীতি এসেছে, যেখানে task নিজের পছন্দের slice জানাতে পারে (পর্ব ২১)। Windows-এ quantum কয়েকটি clock tick-এর সমান, server সংস্করণে লম্বা আর ডেস্কটপ সংস্করণে ছোট, আর সামনের জানালার (foreground) প্রসেসের thread-কে বাড়তি সুবিধা দেওয়া যায়।
তৃতীয়ত, আধুনিক Linux “tickless” হতে পারে: একটি core-এ মাত্র একটি runnable task থাকলে নিয়মিত timer tick বন্ধ রাখা যায় (NO_HZ), কারণ কাকে দেবে, এমন কেউ নেই — এতে অপ্রয়োজনীয় mode switch আর বিদ্যুৎ-খরচ দুটোই কমে। ল্যাপটপ আর ফোনের ব্যাটারির জন্য এটি গুরুত্বপূর্ণ।
মূল উপলব্ধি: একটি স্থির “সঠিক” quantum নেই। প্রসেসের আচরণ, সংখ্যা আর লক্ষ্য (response, throughput, বিদ্যুৎ) অনুযায়ী slice বদলানোই আধুনিক scheduler-এর আসল কাজ; q/(q+s) শুধু নিচের সীমাটি দেখায়।
প্র ০৩২০১৮ সালের Meltdown ত্রুটি কীভাবে mode switch আর context switch-এর খরচ বদলে দিল, আর PCID কেন হঠাৎ গুরুত্বপূর্ণ হলো?
২০১৮ সালের জানুয়ারিতে প্রকাশিত Meltdown ত্রুটি দেখাল, অনেক Intel প্রসেসরে user mode-এর প্রোগ্রাম speculative execution-এর সুযোগ নিয়ে kernel-এর মেমরি আঁচ করতে পারে — যদি সেই মেমরি প্রসেসের page table-এ থাকে, অনুমতি না থাকলেও। তখন পর্যন্ত প্রায় সব OS প্রতিটি প্রসেসের page table-এর উপরের অংশে পুরো kernel রেখে দিত, যাতে system call বা interrupt-এ CR3 বদলাতে না হয় — এটিই mode switch সস্তা রাখার কৌশল ছিল।
প্রতিরোধ হিসেবে Linux আনল KPTI (Kernel Page-Table Isolation), Windows আনল একই ধরনের KVA Shadow. এখন প্রতিটি প্রসেসের দুটি page table: user mode-এর জন্য একটি, যেখানে kernel-এর প্রায় কিছুই নেই, আর kernel mode-এর জন্য পূর্ণ একটি। প্রতিটি system call আর interrupt-এ CR3 দুবার বদলায় — ঢোকার সময় আর ফেরার সময়। অর্থাৎ যে খরচ আগে শুধু process switch-এ ছিল, তা এখন প্রতিটি mode switch-এও চলে এল। PCID না থাকলে প্রতিটি CR3 লেখায় TLB মুছে যেত, আর syscall-ভারী কাজ (ডেটাবেস, নেটওয়ার্ক সার্ভার) অনেক ধীর হয়ে যেত — প্রথম দিকের মাপে কিছু কাজে উল্লেখযোগ্য ধীরতা দেখা গিয়েছিল, যদিও সাধারণ ডেস্কটপ কাজে প্রভাব কম ছিল।
এখানেই PCID-এর ভূমিকা। Linux-এর ২০১৭ সালের শেষ দিকের সংস্করণগুলোতে (4.14) PCID-এর ব্যাপক ব্যবহার এসেছিল, আর KPTI-তে user ও kernel page table-কে আলাদা PCID দেওয়া হয় — ফলে CR3 বদলালেও TLB-র অনুবাদ টিকে থাকে। যেসব CPU-তে PCID নেই (পুরোনো প্রজন্ম), সেখানে KPTI-র খরচ বেশি। পরের প্রজন্মের Intel আর বেশিরভাগ AMD প্রসেসরে এই ত্রুটি হার্ডওয়্যারেই নেই, তাই Linux সেখানে KPTI স্বয়ংক্রিয়ভাবে বন্ধ রাখে। কোন প্রতিরোধ চালু আছে, তা দেখা যায় /sys/devices/system/cpu/vulnerabilities/ ফোল্ডারের ফাইলগুলো পড়ে — কিন্তু “গতি বাড়াতে” এগুলো বন্ধ করা নিরাপত্তার জন্য বিপজ্জনক, বিশেষত যেখানে অন্যের কোড চলে (ক্লাউড, ব্রাউজার)।
মূল উপলব্ধি: Context switch-এর খরচ শুধু kernel-এর কোডের ব্যাপার নয় — হার্ডওয়্যারের নকশা আর নিরাপত্তার চাহিদা একে বদলে দেয়। আর address-space ট্যাগ (PCID/ASID)-এর মতো একটি “ছোট” হার্ডওয়্যার সুবিধা পুরো শিল্পের গতি বাঁচাতে পারে।
প্র ০৪Go-র goroutine, async/await, coroutine — এগুলো কি cooperative multitasking-এর প্রত্যাবর্তন? Kernel-এর switch এড়িয়ে লাভ কী, ঝুঁকি কী?
একটি নেটওয়ার্ক সার্ভারে লাখো সংযোগ থাকতে পারে, প্রতিটি বেশিরভাগ সময় অপেক্ষায়। প্রতিটির জন্য একটি OS thread রাখলে প্রতিটির নিজস্ব kernel stack আর user stack (সাধারণত কয়েক MB পর্যন্ত সংরক্ষিত), আর প্রতিটি অপেক্ষা-জাগরণে একটি পূর্ণ kernel context switch. এর বিকল্প হলো user-space-এ “হালকা thread” — একটি OS thread-এর ভেতরে অনেক কাজ, আর তাদের মধ্যে পালাবদল প্রোগ্রামের নিজের runtime করে। এই switch-এ kernel-এ ঢুকতে হয় না, CR3 বা TLB-এর প্রশ্নই নেই; শুধু কয়েকটি register আর stack pointer বদলানো — তাই সাধারণত kernel switch-এর চেয়ে অনেক গুণ সস্তা।
JavaScript-এর event loop, Python-এর asyncio, C#-এর async/await — এগুলো সরাসরি cooperative: কাজটি await-এ পৌঁছালে তবেই অন্য কাজ চলে। ঝুঁকিও Windows 3.x-এর মতো: একটি কাজ await ছাড়া লম্বা হিসাব করলে বা ভুল করে blocking কল (যেমন সাধারণ time.sleep) ডাকলে পুরো event loop থেমে যায়, সব সংযোগ সাড়া দেওয়া বন্ধ করে। Node.js-এ এ কারণে ভারী হিসাব worker thread-এ পাঠানোর পরামর্শ দেওয়া হয়।
Go-র ইতিহাস শিক্ষণীয়। Go runtime goroutine-গুলোকে অল্প কয়েকটি OS thread-এ চালায় (M:N মডেল, পর্ব ১৪)। শুরুর দিকে goroutine-এর switch হতো শুধু নির্দিষ্ট জায়গায় — ফাংশন কল, channel, I/O — অর্থাৎ কার্যত cooperative. ফলে কোনো ফাংশন-কল ছাড়া একটি টানা লুপ পুরো thread আটকে রাখতে পারত, এমনকি garbage collector-কেও অপেক্ষা করাত। ২০২০ সালে Go 1.14 এই সমস্যার সমাধানে asynchronous preemption আনে: runtime একটি OS signal পাঠিয়ে লুপে আটকে থাকা goroutine-কে থামাতে পারে। অর্থাৎ Go-কেও শেষ পর্যন্ত OS-এর মতো preemption নিজের ভেতরে বানাতে হলো। Java 21-এর virtual thread-ও একই লক্ষ্যে এসেছে।
আরেকটি সূক্ষ্মতা: user-space switch kernel-এর চোখে অদৃশ্য। একটি goroutine সত্যিকারের blocking syscall-এ আটকে গেলে runtime-কে নতুন OS thread দিয়ে বাকিদের চালাতে হয়; আর OS-এর scheduler পুরো প্রসেসকে একটি দল হিসেবে দেখে, ভেতরের অগ্রাধিকার জানে না।
মূল উপলব্ধি: Cooperative switch সস্তা কিন্তু সবার ভদ্রতার ওপর নির্ভর করে; preemptive switch দামি কিন্তু নিশ্চয়তা দেয়। আধুনিক নকশা দুটোকে স্তরে স্তরে মেশায় — আর সময়ের সাথে ভেতরের স্তরেও preemption ফিরে আসছে।
প্র ০৫একটি সার্ভার প্রতি সেকেন্ডে কয়েক লাখ context switch করছে। এটি কি সমস্যা? কমানোর কৌশলগুলো কী, আর প্রতিটির দাম কী?
প্রথম প্রশ্ন: সংখ্যাটি নিজে সমস্যা নয়, ফল দেখতে হয়। একটি ব্যস্ত ওয়েব সার্ভারে প্রতি সেকেন্ডে লক্ষাধিক switch স্বাভাবিক হতে পারে, যদি latency আর throughput ঠিক থাকে। সমস্যার লক্ষণ হলো: CPU-র বড় অংশ kernel-এ (vmstat-এর sy), throughput বাড়াতে thread বাড়ালে উল্টো কমে যাওয়া, আর perf-এ scheduler-এর ফাংশন উপরের দিকে। তারপর voluntary আর involuntary আলাদা করুন। Involuntary বেশি মানে core-এর চেয়ে বেশি CPU-bound thread কাড়াকাড়ি করছে — সমাধান thread-সংখ্যা core-সংখ্যার কাছাকাছি আনা (thread pool), বা affinity দিয়ে কাজ আলাদা core-এ রাখা। Voluntary বেশি মানে thread-গুলো বারবার অপেক্ষায় যাচ্ছে — lock নিয়ে টানাটানি, ছোট ছোট I/O, বা thread-এর মধ্যে খুব ঘন ঘন বার্তা আদান-প্রদান।
কৌশলগুলোর প্রতিটির দাম আছে। Batching — একবারে অনেক কাজ বা ডেটা পাঠানো — switch কমায়, কিন্তু প্রতিটি অনুরোধের latency একটু বাড়ায়। Spin-then-block — lock না পেলে কিছুক্ষণ ঘুরে দেখা, তারপর ঘুমানো — Linux-এর futex-ভিত্তিক mutex আর অনেক runtime এটি করে; ছোট অপেক্ষায় switch বাঁচে, কিন্তু spin-এর সময় CPU আর বিদ্যুৎ খরচ হয় (পর্ব ২৩)। Event-driven I/O (epoll, io_uring) — একটি thread অনেক সংযোগ দেখে — thread আর switch দুটোই কমায়, কিন্তু কোড জটিল হয়। চরম পথে আছে busy polling: DPDK-এর মতো নেটওয়ার্ক ফ্রেমওয়ার্ক একটি core পুরোপুরি দখল করে প্যাকেট খোঁজে, কখনো ঘুমায় না — switch প্রায় শূন্য, latency খুব কম, কিন্তু সেই core সারাক্ষণ ১০০% চলে, অন্য কাজে লাগে না। Linux-এ isolcpus বা nohz_full-এর মতো boot option দিয়ে কিছু core-কে সাধারণ scheduling থেকে আলাদাও রাখা যায়, যা কম-latency ট্রেডিং বা টেলিকম সিস্টেমে ব্যবহৃত হয়।
মাপার পদ্ধতিও গুরুত্বপূর্ণ: একবারে একটি জিনিস বদলান, একই লোডে আগে-পরে মাপুন, আর শুধু গড় নয়, ধীরতম অনুরোধগুলো (p99 latency)-ও দেখুন — switch-এর ক্ষতি প্রায়ই সেখানেই ধরা পড়ে।
মূল উপলব্ধি: Context switch কমানো নিজে লক্ষ্য নয়; লক্ষ্য latency, throughput আর সম্পদের ভারসাম্য। প্রতিটি কৌশল switch-এর খরচকে অন্য কোনো খরচে (latency, CPU, জটিলতা) বদলে নেয় — মেপে দেখে তবেই বাছুন।
অনুশীলন
হাতে হিসাব, Gantt chart, আউটপুট অনুমান আর কমান্ড — আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।
-
হাতে হিসাব: একটি সিস্টেমে q = ৫ ms, প্রতি context switch-এ s = ০.৫ ms, আর ৬টি CPU-bound প্রসেস Round Robin-এ চলছে। (ক) CPU efficiency কত? (খ) প্রতি সেকেন্ডে আনুমানিক কতটি switch? (গ) Efficiency ৯৮% করতে q কত লাগবে?
(ক) q/(q+s) = ৫ ÷ ৫.৫ ≈ ৯০.৯%.
(খ) একটি চক্র = ৫.৫ ms, তাই ১০০০ ÷ ৫.৫ ≈ ১৮২টি switch প্রতি সেকেন্ডে (প্রসেস-সংখ্যা ৬ হলেও প্রতি slice-এর পর একটি switch — সংখ্যা n-এর ওপর নির্ভর করে না)।
(গ) q ÷ (q + ০.৫) ≥ ০.৯৮ ⇒ ০.০২q ≥ ০.৪৯ ⇒ q ≥ ২৪.৫ ms. দাম: একটি প্রসেস তার পালা শেষে আবার পেতে প্রায় (৬ − ১) × ২৪.৫ + ৬ × ০.৫ = ১২৫.৫ ms অপেক্ষা করতে পারে (সমাধান করা উদাহরণ ৪-এর সূত্র)।
-
Gantt chart আঁকুন: P1 = ৬, P2 = ৩, P3 = ৪ ms, সবাই t = 0-তে আসে, ক্রম P1, P2, P3. Round Robin, q = ২ ms, ভিন্ন প্রসেসে যেতে s = ১ ms. Gantt chart, মোট সময়, switch-এর সংখ্যা, efficiency আর গড় turnaround time বের করুন। Switch-এর খরচ শূন্য হলে গড় turnaround কত হতো?
│P1 0-2│cs│P2 3-5│cs│P3 6-8│cs│P1 9-11│cs│P2 12-13│cs│P3 14-16│cs│P1 17-19│Switch ৬টি, overhead ৬ ms, উপকারী সময় ৬ + ৩ + ৪ = ১৩ ms, মোট ১৯ ms. Efficiency = ১৩/১৯ ≈ ৬৮.৪% (q/(q+s) = ৬৬.৭%-এর কাছাকাছি, কিন্তু সমান নয় — P2-এর শেষ slice মাত্র ১ ms).
সমাপ্তি: P1 = ১৯, P2 = ১৩, P3 = ১৬ → গড় turnaround = ৪৮/৩ = ১৬ ms. Waiting time (turnaround − burst): ১৩, ১০, ১২ → গড় ≈ ১১.৬৭ ms.
s = 0 হলে: │P1 0-2│P2 2-4│P3 4-6│P1 6-8│P2 8-9│P3 9-11│P1 11-13│ → সমাপ্তি ১৩, ৯, ১১ → গড় turnaround = ৩৩/৩ = ১১ ms. ছয়টি switch গড়ে ৫ ms যোগ করেছে।
-
আউটপুট অনুমান: দুটি প্রোগ্রাম, প্রতিটি ৫ সেকেন্ড চলে, একটি ব্যস্ত ৪-core Linux মেশিনে যেখানে আরও অনেক CPU-bound কাজ চলছে। প্রোগ্রাম A একটি লুপে ১০০ বার
read()দিয়ে নেটওয়ার্ক থেকে অল্প ডেটার অপেক্ষা করে, মাঝে প্রায় কোনো হিসাব নেই। প্রোগ্রাম B কোনো I/O ছাড়া টানা হিসাব করে। শেষে/proc/<pid>/status-এ কারvoluntary_ctxt_switchesবেশি, আর কারnonvoluntary_ctxt_switchesবেশি দেখার কথা?A: voluntary প্রায় ১০০ বা তার বেশি (প্রতিটি অপেক্ষায় block হয়; ডেটা আগেই এসে থাকলে কিছু read block না-ও করতে পারে), nonvoluntary খুব কম — সে CPU-তে এত অল্প সময় থাকে যে quantum ফুরানোর আগেই নিজে ছেড়ে দেয়।
B: voluntary প্রায় 0 (কখনো অপেক্ষা করে না), কিন্তু nonvoluntary অনেক — ব্যস্ত মেশিনে scheduler তাকে বারবার সরিয়ে অন্যদের সময় দেয়; ৫ সেকেন্ডে শত থেকে হাজারের ঘরে হতে পারে। মেশিন ফাঁকা থাকলে B-এর nonvoluntary গণকও কম থাকত — অংশ ২-এর script-এ ঠিক এটাই দেখা গেছে।
-
কমান্ড লিখুন: (ক) Linux-এ PID 1234-এর দুটি switch-গণক দেখার এক লাইনের কমান্ড। (খ) পুরো সিস্টেমে প্রতি সেকেন্ডে কতগুলো context switch হচ্ছে, তা ৫ সেকেন্ড ধরে দেখার কমান্ড। (গ) প্রতিটি প্রসেসের প্রতি-সেকেন্ডের voluntary আর involuntary switch আলাদা করে দেখার কমান্ড। (ঘ) Windows-এ (খ)-এর সমতুল্য।
grep ctxt_switches /proc/1234/status # (a) vmstat 1 5 # (b) look at the "cs" column pidstat -w 1 # (c) cswch/s = voluntary, nvcswch/s = involuntary (sysstat package) Get-Counter '\System\Context Switches/sec' -SampleInterval 1 -MaxSamples 5 # (d) PowerShellএর কোনোটিতেই
sudoলাগে না।perf stat -e context-switches -p 1234-ও একটি পথ, তবে অনেক সিস্টেমে perf-এর জন্য বাড়তি অনুমতি লাগে — তখন আগের কমান্ডগুলোই যথেষ্ট। Windows-এ প্রতিটি thread-এর জন্য\Thread(*)\Context Switches/seccounter বা Sysinternals-এর Process Explorer-ও কাজে লাগে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমTimer interrupt-এ P1 থেকে P2-তে context switch-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Timer interrupt আসে, CPU kernel mode-এ যায়
- P1-এর register kernel stack বা PCB-তে সংরক্ষণ হয়
- Scheduler ready queue থেকে P2-কে বাছে
- P2-এর page table-এর ঠিকানা CR3-এ বসে
- P2-এর kernel stack আর register ফিরিয়ে আনা হয়
- User mode-এ ফিরে P2 তার সংরক্ষিত PC থেকে চলে
Context switch-এর সময় নিচের কোনটি CPU থেকে সংরক্ষণ (save) করার দরকার হয় না?
- Program counter
- Stack pointer
- Flags register (x86-এ RFLAGS)
- প্রসেসের খোলা একটি ফাইলের ডিস্কে থাকা বিষয়বস্তু
একই প্রসেসের দুটি thread-এর মধ্যে switch সাধারণত দুটি আলাদা প্রসেসের মধ্যে switch-এর চেয়ে সস্তা হয় কেন?
- Thread switch-এ কোনো register সংরক্ষণ করতে হয় না
- Address space একই থাকে, তাই page table বদলায় না আর TLB অটুট থাকে
- Thread switch kernel mode-এ না গিয়েই হয়
- Thread-এর time quantum বেশি লম্বা
Round Robin-এ time quantum q = ৪ ms আর প্রতি context switch-এ s = ১ ms. সব প্রসেস পুরো quantum ব্যবহার করলে CPU efficiency কত?
- ৭৫%
- ২০%
- ৮০%
- ৯৬%
নিচের কোন ঘটনাটি একটি voluntary context switch ঘটায়?
- একটি প্রসেস খালি pipe থেকে
read()করে ডেটার অপেক্ষায় block হলো - টানা লুপে থাকা প্রসেসের time quantum শেষ হলো
- বেশি অগ্রাধিকারের একটি প্রসেস জেগে উঠে চলতিটিকে সরাল
- কিবোর্ড interrupt-এর handler শেষে একই প্রসেসে ফেরা
Cooperative multitasking-এর প্রধান দুর্বলতা কোনটি?
- প্রতি সেকেন্ডে অনেক বেশি context switch হয়
- একটি প্রোগ্রাম CPU না ছাড়লে (যেমন অসীম লুপে) পুরো সিস্টেম জমে যায়
- Timer interrupt বেশি ঘন ঘন দিতে হয়
- Synchronization-এর জন্য বেশি lock লাগে
x86-এর PCID (বা ARM-এর ASID) কোন সমস্যা কমায়?
- Register সংরক্ষণের সময়
- Scheduler-এর ভুল বাছাই
- Page table বদলানোর পর পুরো TLB মুছে যাওয়া আর তার ফলে TLB miss
- L1 data cache-এর দূষণ
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Context = register, PC, SP, flags, FPU/SIMD, TLS base, page table-এর ঠিকানা (CR3), kernel stack; থাকে PCB/TCB আর kernel stack-এ (Linux-এ
task_struct+pt_regs). - Switch-এর কারণ: timer interrupt, blocking syscall, yield, উচ্চ-অগ্রাধিকারের জাগরণ; অপেক্ষায় নিজে ছাড়লে voluntary, কেড়ে নিলে involuntary.
- Dispatcher: সংরক্ষণ → বাছাই (scheduler) → address space বদল → ফেরত আনা → user mode; মাঝের সময় dispatch latency.
- Mode switch < thread switch < process switch — process switch-এ CR3, TLB (PCID/ASID না থাকলে) আর cache-এর পরোক্ষ খরচ।
- পুরো quantum ব্যবহার হলে efficiency = q/(q+s); burst ছোট হলে Gantt chart থেকে গুনে হিসাব। আধুনিক OS preemptive, thread-ই scheduling-এর একক।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১০ · fork, exec আর wait — প্রসেস তৈরি আর শেষপরবর্তী পর্বপ্রসেসের জন্ম, প্রোগ্রাম বদল আর শেষ — হাতে-কলমে।
- অপারেটিং সিস্টেম · কোর্স হোম৬৪টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ০৪ · OS-এর চোখে হার্ডওয়্যারপেছনে ফিরেRegister, interrupt, cache আর TLB-এর ভিত্তি — এই পর্বের পূর্বশর্ত।
- C প্রোগ্রামিং · Process আর IPCহাতে-কলমেfork, pipe আর প্রসেসের যোগাযোগ C-তে আরও অনুশীলন।
- C প্রোগ্রামিং · Multithreadingপাশের ট্র্যাকThread তৈরি, join আর ভাগের ডেটা — thread switch-এর অন্য দিক।
- DSA · Queue আর dequeডেটা স্ট্রাকচারReady queue-এর পেছনের ডেটা স্ট্রাকচার।