Interrupt, Exception, Trap — যখন CPU স্বাভাবিক ক্রম ভাঙে
Interrupts, Exceptions, and Traps
তিনটা ভিন্ন কারণে CPU হঠাৎ তার স্বাভাবিক sequential পথ ছেড়ে দেয় — বাইরের ঘটনা (interrupt), নিজের ভুল (exception), আর ইচ্ছাকৃত অনুরোধ (trap) — কিন্তু ভেতরের mechanism-টা তিনটাতেই এক।
আগে এটা বুঝি
এই module-এর প্রায় প্রতিটা লেসন একটা নীরব অনুমানের উপর দাঁড়িয়ে ছিল: CPU একটার পর একটা instruction চালিয়ে যায়, PC (program counter) নিজে থেকেই এগোয় (branch instruction ছাড়া), আর প্রোগ্রাম নিজের গতিতে, নিজের জগতে সম্পূর্ণ একা চলে।
বাস্তবে তিনটা জিনিস এই সুন্দর গল্পটা ভেঙে দিতে পারে, যেকোনো মুহূর্তে।
দৃশ্য ১। আপনি একটা keyboard-এ চাপ দিলেন। CPU সেই মুহূর্তে হয়তো একটা সম্পূর্ণ ভিন্ন প্রোগ্রামের মাঝামাঝি কোনো loop-এর ৪৭তম iteration চালাচ্ছিল। Keypress-টার সাথে সেই প্রোগ্রামের কোনো সম্পর্কই নেই — তবু CPU-কে এখনই এটার প্রতি সাড়া দিতে হবে।
দৃশ্য ২। একটা প্রোগ্রাম x / y হিসাব করছে, আর y হঠাৎ
শূন্য বেরিয়ে আসে। এটা বাইরের কোনো ঘটনা না — এটা ঠিক এই
instruction-টার নিজের দোষ, ঠিক এই মুহূর্তে।
দৃশ্য ৩। একটা প্রোগ্রাম একটা ফাইল পড়তে চায়। ফাইল পড়া user-mode program নিজে করতে পারে না — hardware-এর সরাসরি নিয়ন্ত্রণ OS-এর হাতে। তাই প্রোগ্রাম ইচ্ছাকৃতভাবে একটা বিশেষ instruction চালায় যার একমাত্র কাজ হলো “আমাকে kernel-এ নিয়ে যাও।”
তিনটা দৃশ্যই CPU-কে তার স্বাভাবিক sequential path থেকে সরিয়ে দেয় — কিন্তু তিনটা সম্পূর্ণ আলাদা কারণে। প্রথমটা interrupt। দ্বিতীয়টা exception। তৃতীয়টা trap — যা আসলে exception-এরই একটা ইচ্ছাকৃত রূপ। এই লেসনের কাজ এই তিনটাকে নির্ভুলভাবে আলাদা করা, আর দেখানো তাদের ভেতরের প্রক্রিয়াটা কেন প্রায় একই রকম।
মূল ধারণা
তিনটা সংজ্ঞা, একটা তালিকায়
| কারণ | কখন ঘটে | উদাহরণ | |
|---|---|---|---|
| Interrupt | বাহ্যিক (external) | Asynchronous — বর্তমান instruction-এর সাথে সম্পর্কহীন, যেকোনো সময়ে | Keypress, network packet, timer tick, disk সম্পন্ন হওয়ার সংকেত |
| Exception | অভ্যন্তরীণ (internal) | Synchronous — বর্তমান instruction-এর সরাসরি ফলাফল | Divide by zero, page fault, illegal opcode |
| Trap | অভ্যন্তরীণ, ইচ্ছাকৃত | Synchronous, deliberately software-triggered | System call (syscall, ecall), breakpoint (int3) |
এই তিনটাকে গুলিয়ে ফেলা সবচেয়ে সহজ কারণ mechanism-টা প্রায় অভিন্ন — সবগুলোই CPU-কে একটা fixed handler-এ পাঠায়, state সংরক্ষণ করে, পরে ফিরে আসে। কিন্তু তাদের উৎস সম্পূর্ণ ভিন্ন, আর সেই পার্থক্যটাই ব্যবহারিকভাবে গুরুত্বপূর্ণ — বিশেষত কখন resume করা যাবে বা যাবে না, সেটা নির্ধারণে।
Interrupt — বাইরের জগৎ থেকে একটা ট্যাপ
Interrupt CPU-র বাইরের কোনো hardware device থেকে আসে — একটা
তারের মাধ্যমে (আক্ষরিক অর্থেই, ঐতিহাসিকভাবে একটা physical pin,
INTR) CPU-কে জানানো হয় “কিছু একটা ঘটেছে, মনোযোগ দাও।” এটা
সম্পূর্ণভাবে asynchronous — এই মুহূর্তে CPU কোন instruction
চালাচ্ছে তার সাথে interrupt-এর কোনো সম্পর্ক নেই। একটা keypress
interrupt একটা ADD instruction-এর মাঝখানেও আসতে পারে, একটা
LOAD-এর মাঝখানেও।
CPU সাধারণত instruction boundary-তে (একটা instruction পুরোপুরি শেষ হওয়ার পর, পরেরটা শুরুর আগে) interrupt চেক করে — তাই যদিও interrupt-এর ঘটনা যেকোনো সময় হতে পারে, তার প্রতি সাড়া দেওয়া হয় পরবর্তী সুবিধাজনক instruction boundary-তে।
Exception — বর্তমান instruction নিজেই দোষী
Exception ঘটে বর্তমানে execute হওয়া instruction-এর সরাসরি ফলাফল হিসেবে — এটা synchronous। একই প্রোগ্রাম, একই ইনপুট দিয়ে বারবার চালালে, ঠিক একই instruction-এ, ঠিক একই exception বারবার ঘটবে (interrupt-এর বিপরীতে, যা টাইমিং-নির্ভর, প্রোগ্রাম প্রতিবার একই জায়গায় থামবে এমন কোনো গ্যারান্টি নেই)।
Trap — ইচ্ছাকৃত exception
Trap আসলে একটা বিশেষ ধরনের exception — পার্থক্য শুধু এই যে এটা দুর্ঘটনা না, উদ্দেশ্যমূলক। প্রোগ্রামার (বা compiler generated code) সচেতনভাবে একটা নির্দিষ্ট instruction চালায় যার একমাত্র কাজ CPU-কে trap করানো — একে software interrupt-ও বলা হয় কিছু পুরনো সাহিত্যে, যদিও সেই নামকরণ বিভ্রান্তিকর কারণ এটা আদতে exception mechanism-ই ব্যবহার করে, interrupt না।
সবচেয়ে গুরুত্বপূর্ণ ব্যবহার: system call। একটা user-mode প্রোগ্রাম সরাসরি disk driver বা network card touch করতে পারে না — privilege ring-এর নিয়ম (Level 4-এ বিস্তারিত) সেটা আটকায়। তাই ফাইল পড়ার জন্য প্রোগ্রাম একটা বিশেষ instruction চালায়:
x86-64: syscall
RISC-V: ecall
ARM64: svc #0এই instruction চালানো মাত্র CPU ইচ্ছাকৃতভাবে trap করে — বাইরের কোনো ঘটনা ছাড়াই, বরং প্রোগ্রামের নিজস্ব সিদ্ধান্তে — আর নিয়ন্ত্রণ চলে যায় kernel-এ। Kernel তখন privileged mode-এ আসল ফাইল-পড়ার কাজটা করে, তারপর ফলাফল নিয়ে user program-এ ফিরে আসে। এই পুরো mechanism-টাই — trap, privilege transition, kernel handler — Level 4 (Operating Systems)-এ system call-এর লেসনে গভীরভাবে দেখা হবে; এখানে শুধু hardware primitive-টা চেনা যথেষ্ট।
বাহ্যিক device বর্তমান instruction প্রোগ্রামের
(keyboard, timer...) নিজেই সমস্যা তৈরি করল ইচ্ছাকৃত সিদ্ধান্ত
│ │ │
▼ ▼ ▼
INTERRUPT EXCEPTION TRAP
(asynchronous) (synchronous) (synchronous,
deliberate)
│ │ │
└──────────────┬───────────┴────────────────────────┘
▼
একই common mechanism:
state save → vector table lookup →
handler চলে → resume (বা terminate)ভেতরে কী ঘটছে
Common mechanism — চারটা ধাপ
তিনটা প্রক্রিয়াই — খুব সামান্য বৈচিত্র্য বাদে — একই চারটা ধাপে এগোয়।
- ১. State সংরক্ষণPC ও flags register (লেসন ৩-এর সেই condition code) সংরক্ষিত হয় — সাধারণত একটা বিশেষ স্ট্যাকে বা নির্দিষ্ট register-এ, যাতে পরে ঠিক এই মুহূর্তে ফেরা যায়
- ২. Vector table lookupএকটা fixed-address table (x86-এ IDT — Interrupt Descriptor Table, RISC-V-এ একটা mtvec register-নির্দেশিত টেবিল) থেকে ঠিক কোন handler চালাতে হবে তার ঠিকানা বের করা হয়
- ৩. Handler চলেনিয়ন্ত্রণ handler-এর কোডে যায় — এই handler OS-এর অংশ, উচ্চতর privilege-এ চলে (Level 4-এ বিস্তারিত)
- ৪. Resume বা terminateসংরক্ষিত state পুনরুদ্ধার করে বাধাপ্রাপ্ত instruction-এর ঠিক পরে (বা কিছু exception-এ ঠিক সেই instruction-এই) execution আবার শুরু হয় — অথবা, যদি সমস্যা অপুনরুদ্ধারযোগ্য হয়, প্রোগ্রাম বন্ধ করে দেওয়া হয়
কেন PC ও flags সংরক্ষণ জরুরি — লেসন ৩-এর সরাসরি সম্প্রসারণ। Register/PC/flags লেসনে আমরা দেখেছিলাম PC মানেই “পরবর্তী instruction কোথায়,” আর flags register মানেই সাম্প্রতিকতম অপারেশনের ফলাফল (zero, carry, overflow, ইত্যাদি)। একটা interrupt হ্যান্ডলার নিজেই তো instruction চালাবে — যা এই দুটোই বদলে দেবে। তাই handler শুরু হওয়ার আগেই এই মানগুলো নিরাপদে রাখা আবশ্যক, নাহলে handler শেষে ফেরার সময় জানার উপায় থাকবে না কোথা থেকে চালিয়ে যেতে হবে, বা বাধাপ্রাপ্ত instruction-এর ঠিক আগের flags অবস্থা কী ছিল।
Interrupt vector table — একটা ঠিকানার বই
CPU কীভাবে জানে ঠিক কোন handler চালাতে হবে — timer interrupt-এর জন্য একটা, keyboard-এর জন্য আরেকটা, divide-by-zero-এর জন্য সম্পূর্ণ ভিন্ন একটা? উত্তর: একটা fixed table, যাকে বলে interrupt vector table (x86-এ IDT — Interrupt Descriptor Table)। এই টেবিলের প্রতিটা entry একটা নির্দিষ্ট নম্বরের জন্য নির্দিষ্ট handler-এর ঠিকানা রাখে।
Vector নম্বর কারণ Handler
────────────────────────────────────────────────────────
0 Divide error (#DE) divide_error_handler
6 Invalid opcode (#UD) invalid_opcode_handler
13 General protection fault (#GP) gpf_handler
14 Page fault (#PF) page_fault_handler
32 Timer interrupt (IRQ0) timer_handler
33 Keyboard interrupt (IRQ1) keyboard_handler
0x80 (legacy) System call (int 0x80) syscall_handlerx86-এ ২৫৬টা এমন entry থাকে (৮-bit vector নম্বর)। প্রথম ৩২টা
CPU-reserved exception-এর জন্য (Intel-নির্ধারিত অর্থ), বাকিগুলো
device interrupt আর software-নির্ধারিত ব্যবহারের জন্য — যেমন
int 0x80, যা পুরনো ৩২-bit Linux-এ syscall entry point ছিল
(আধুনিক x86-64 Linux dedicated syscall instruction ব্যবহার
করে, যা আরও দ্রুত — table lookup-এর প্রয়োজনই এড়িয়ে যায়)।
Fault, trap, abort — exception-এর তিন উপশ্রেণি
x86 আরও একটা সূক্ষ্ম শ্রেণিবিভাগ করে, যা কোথায় resume হবে তা নির্ধারণ করে:
| শ্রেণি | resume হয় কোথা থেকে | পুনরুদ্ধারযোগ্য? | উদাহরণ |
|---|---|---|---|
| Fault | সেই একই instruction, পুনরায় চেষ্টা | হ্যাঁ, সাধারণত | Page fault — page allocate করে instruction পুনরায় চালানো হয় |
| Trap (x86-এর অর্থে) | পরের instruction | হ্যাঁ | Breakpoint (int3), debugger-এর single-step |
| Abort | নির্ধারিত না, সাধারণত অপুনরুদ্ধারযোগ্য | না | Machine check exception — hardware নিজেই ত্রুটিপূর্ণ অবস্থায় |
Maskable বনাম non-maskable interrupt
সব interrupt সমান গুরুত্বপূর্ণ না। CPU-তে একটা flag থাকে (x86-এ
IF — Interrupt Flag, EFLAGS-এর অংশ, লেসন ৩-এর flags register-এরই
সদস্য) যা দিয়ে বেশিরভাগ interrupt সাময়িকভাবে বন্ধ (mask)
করে রাখা যায় — যখন CPU একটা critical, বাধাগ্রস্ত-হওয়া-উচিত-না
এমন কাজ করছে (যেমন নিজেই একটা interrupt handler-এর মাঝামাঝি
থাকা)।
কিন্তু কিছু interrupt এত জরুরি (যেমন hardware failure সংকেত)
যে সেগুলো mask করা যায় না — এদের বলে NMI (Non-Maskable
Interrupt)। এগুলো IF flag-এর অবস্থা যাই হোক না কেন, সবসময়
সাথে সাথে সাড়া পায়।
ROB-এর সাথে যোগসূত্র — precise exception কেন সহজ কথা না
আগের লেসনে (Register Renaming ও Reorder Buffer) আমরা দেখেছি out-of-order CPU-তে instruction এলোমেলো ক্রমে execute হয়, কিন্তু commit হয় কঠোরভাবে program order-এ। এখন সেই ধারণাটা এই লেসনের বিষয়ের সাথে সরাসরি যুক্ত হয়।
ধরুন I5-এ একটা page fault ঘটে, কিন্তু ততক্ষণে I8, I9
(program order-এ পরে) ইতিমধ্যে out-of-order ভাবে execute হয়ে
গেছে, তাদের ফলাফল ROB-এ “সম্পন্ন” চিহ্নিত হয়ে বসে আছে (এখনো
commit হয়নি)। এখন exception handler-কে একটা নির্দিষ্ট, সুসংগত
প্রশ্নের উত্তর দিতে হবে: প্রোগ্রামের ঠিক কোন অংশ পর্যন্ত
“সত্যিই ঘটেছে” বলে ধরে নেওয়া উচিত?
ROB এই প্রশ্নের উত্তর তুচ্ছ করে দেয়: I5 যখন ROB-এর মাথায়
পৌঁছায় আর তার exception flag দেখা যায়, CPU ঘোষণা করে — I1
থেকে I4 commit হয়ে গেছে (সত্যিকারের architectural state), I5
থেকে exception, আর I8, I9-সহ পরের সব ROB entry flush
(বাতিল) করা হয় — তাদের ফলাফল architectural state-এ কখনো
পৌঁছায়ইনি, যেন তারা কখনো চলেইনি। এটাই precise exception:
বাইরে থেকে দেখতে মনে হয় CPU সরল, in-order ক্রমে চলছিল, ঠিক
I5 পর্যন্ত।
ROB ছাড়া (একটা সরল in-order CPU-তে) এই সমস্যাটাই তৈরি হয় না
— কারণ instruction ইতিমধ্যেই program order-এ execute হয়, তাই
I5-এর “পরে” কিছুই এখনো ঘটেইনি exception ধরা পড়ার সময়। কিন্তু
out-of-order CPU-তে এই গ্যারান্টি ROB ছাড়া রক্ষা করার কোনো
উপায় নেই — এটাই কেন প্রতিটা আধুনিক high-performance CPU-তে
ROB (বা তার সমতুল্য) থাকা আবশ্যিক, শুধু performance optimization
না।
উদাহরণ
একটা সম্পূর্ণ trace — page fault থেকে resume পর্যন্ত
একটা বাস্তব দৃশ্য পুরোপুরি ট্রেস করি।
int *p = (int *)0x1000; // একটা virtual address, এখনো কোনো physical page-এ map করা নেই
int x = *p; // এই instruction-এই সমস্যা হবেধাপ ১ — সমস্যার উৎপত্তি। *p করার সময় CPU-র memory
management unit (MMU) virtual address 0x1000-কে physical
address-এ অনুবাদ করার চেষ্টা করে (এই অনুবাদ প্রক্রিয়ার বিস্তারিত
Level 4-এ)। Page table-এ দেখা যায় এই virtual address-এর জন্য
কোনো valid mapping নেই।
ধাপ ২ — Exception তোলা (raise)। MMU CPU-কে জানায়: একটা
page fault হয়েছে (x86-এ vector ১৪, #PF)। এটা একটা fault-শ্রেণির
exception — resume হবে এই একই instruction থেকে, কারণ যদি
সমস্যা সমাধান হয় (page allocate করা হয়), instruction-টা তখন
স্বাভাবিকভাবেই সফল হবে।
ধাপ ৩ — State সংরক্ষণ। CPU সংরক্ষণ করে PC (এই *p
instruction-এর ঠিকানা — resume-এর জন্য প্রয়োজন), flags, আর
কোন virtual address-এ সমস্যা হয়েছিল তা (একটা বিশেষ register,
x86-এ CR2-তে) — handler-এর জানা দরকার কোন address fault
করেছে।
ধাপ ৪ — Vector table lookup, handler শুরু। IDT-এর entry ১৪ নির্দেশ করে page fault handler-এর ঠিকানা — এটা kernel-এর অংশ (আমরা এখনো OS বিস্তারিতভাবে দেখিনি, শুধু এটুকু জানা যথেষ্ট যে এটা privileged code, hardware-এর সরাসরি নিয়ন্ত্রণ যার আছে)।
ধাপ ৫ — Handler সিদ্ধান্ত নেয়। OS দেখে এই virtual address প্রোগ্রামের বৈধ address space-এর অংশ কি না।
- যদি বৈধ হয় (যেমন একটা lazily-allocated heap page, বা swap-করা page যা এখন ফিরিয়ে আনতে হবে) — OS একটা physical page বরাদ্দ করে, page table আপডেট করে, তারপর
- যদি অবৈধ হয় (প্রোগ্রামের নিজস্ব বাগ, একটা wild pointer) —
OS প্রোগ্রামকে একটা signal পাঠায় (
SIGSEGV), যা সাধারণত প্রোগ্রাম বন্ধ করে দেয়
ধাপ ৬ক — সফল হলে, resume। যদি page সফলভাবে allocate হয়,
সংরক্ষিত PC পুনরুদ্ধার হয় — মানে সেই একই *p instruction
আবার চালানো হয়। এবার MMU translation সফল হয় (page এখন map করা
আছে), আর প্রোগ্রাম এগিয়ে যায়, সম্পূর্ণ অজ্ঞাত যে মাঝখানে একটা
পুরো exception handling ঘটে গেছে।
ধাপ ৬খ — ব্যর্থ হলে, terminate। যদি address সত্যিই অবৈধ হয়, handler প্রোগ্রামকে বন্ধ করে দেয় — resume আর হয় না।
নিজে চালিয়ে দেখুন
একটা hardware exception-কে OS signal হিসেবে ধরুন
// fault_demo.c
#include \<stdio.h>
#include \<signal.h>
#include \<stdlib.h>
#include \<ucontext.h>
void handler(int sig, siginfo_t *info, void *ucontext_raw) {
printf("\n--- Signal ধরা পড়ল ---\n");
printf("Signal number : %d (%s)\n", sig, sig == SIGFPE ? "SIGFPE" : "SIGSEGV");
if (sig == SIGSEGV) {
printf("Faulting address : %p\n", info->si_addr);
}
ucontext_t *uc = (ucontext_t *)ucontext_raw;
#if defined(__x86_64__)
printf("সংরক্ষিত PC (RIP) : 0x%llx\n",
(unsigned long long)uc->uc_mcontext.gregs[REG_RIP]);
#endif
printf("প্রোগ্রাম এখানেই থামছে -- resume সম্ভব না এই ধরনের exception-এ।\n");
exit(1);
}
int main(int argc, char **argv) {
struct sigaction sa = {0};
sa.sa_sigaction = handler;
sa.sa_flags = SA_SIGINFO;
sigaction(SIGFPE, &sa, NULL);
sigaction(SIGSEGV, &sa, NULL);
if (argc \< 2) {
printf("ব্যবহার: ./fault_demo [div|segv]\n");
return 0;
}
if (argv[1][0] == 'd') {
printf("Divide by zero ট্রিগার করছি...\n");
volatile int a = 10, b = 0;
printf("%d\n", a / b); // hardware #DE exception -> kernel -> SIGFPE
} else {
printf("Null pointer dereference ট্রিগার করছি...\n");
volatile int *p = NULL;
printf("%d\n", *p); // hardware #PF exception (invalid) -> kernel -> SIGSEGV
}
return 0;
}gcc -O0 -o fault_demo fault_demo.c
./fault_demo div
./fault_demo segvসাধারণ আউটপুট:
$ ./fault_demo div
Divide by zero ট্রিগার করছি...
--- Signal ধরা পড়ল ---
Signal number : 8 (SIGFPE)
সংরক্ষিত PC (RIP) : 0x55a1b2c0116a
প্রোগ্রাম এখানেই থামছে -- resume সম্ভব না এই ধরনের exception-এ।
$ ./fault_demo segv
Null pointer dereference ট্রিগার করছি...
--- Signal ধরা পড়ল ---
Signal number : 11 (SIGSEGV)
Faulting address : (nil)
সংরক্ষিত PC (RIP) : 0x55a1b2c011a2
প্রোগ্রাম এখানেই থামছে -- resume সম্ভব না এই ধরনের exception-এ।যা লক্ষ্য করার: Faulting address ঠিক NULL (0x0)
দেখাচ্ছে — এটাই সেই virtual address যেটা MMU translate করতে
ব্যর্থ হয়েছিল, CR2-জাতীয় hardware register থেকে kernel এটা
পড়ে siginfo_t-তে ভরে দিয়েছে। আর RIP (x86-64-এর PC) দেখাচ্ছে
ঠিক সেই instruction-এর ঠিকানা যেখানে fault হয়েছিল — এটাই সেই
“state সংরক্ষণ” ধাপের সরাসরি, observable প্রমাণ।
strace ./fault_demo div চালিয়ে দেখুন — kernel signal delivery
mechanism-এর trace সরাসরি দেখা যাবে।
Divide-by-zero একটা user-space প্রোগ্রামে সরাসরি crash করে না — CPU একটা exception তোলে, kernel তা ধরে, আর একটা POSIX signal (SIGFPE) হিসেবে প্রোগ্রামের কাছে ফেরত পাঠায়। Signal handler ইনস্টল করে সেই পুরো hardware→OS→application যাত্রাটা সরাসরি observe করা যায়।
নিজে বানান
একটা মিনি Interrupt Vector Table সিমুলেটর
- একটা vector table তৈরি করুন -- সংখ্যা থেকে handler function map করা একটা dict
- একটা CPU state class লিখুন যাতে PC, flags, ও একটা saved-state stack থাকবে
- তিন ধরনের ঘটনা model করুন -- external interrupt (asynchronous, cycle সংখ্যা random), exception (synchronous, নির্দিষ্ট instruction-এ), trap (deliberate, একটা বিশেষ opcode)
- প্রতিটা ঘটনার জন্য state-save, vector lookup, handler-call, resume/terminate প্রবাহ বাস্তবায়ন করুন
- fault-শ্রেণির exception (একই instruction resume) বনাম interrupt (পরের instruction resume)-এর পার্থক্য দেখান
মূল ধারণা: একটা toy instruction stream চালান, মাঝে মাঝে random interrupt insert করুন, একটা নির্দিষ্ট instruction-এ exception রাখুন, আর একটা syscall-সদৃশ trap opcode রাখুন — তিনটার handling আলাদাভাবে trace করুন।
import random
VECTOR_TABLE = {
0: "divide_by_zero_handler",
14: "page_fault_handler",
32: "timer_interrupt_handler",
33: "keyboard_interrupt_handler",
0x80: "syscall_handler",
}
class CPU:
def __init__(self, program):
self.program = program # instruction list
self.pc = 0
self.flags = {}
self.saved_stack = [] # (pc, flags) জোড়ার স্ট্যাক -- nested handling সমর্থনে
self.halted = False
self.log = []
def save_state(self):
self.saved_stack.append((self.pc, dict(self.flags)))
def restore_state(self):
self.pc, self.flags = self.saved_stack.pop()
def raise_event(self, kind, vector, resume_mode):
"""kind: 'interrupt' | 'exception' | 'trap'
resume_mode: 'same' (fault-স্টাইল) | 'next' (trap/interrupt-স্টাইল)"""
self.log.append(f"[{kind.upper()}] vector={vector} at PC={self.pc} "
f"-> handler={VECTOR_TABLE.get(vector, 'unknown')}")
self.save_state()
handler = VECTOR_TABLE.get(vector)
if handler is None:
self.log.append(f" পরিচিত handler নেই -- প্রোগ্রাম terminate")
self.halted = True
return
self.log.append(f" handler '{handler}' চলছে...")
# handler নিজেই কিছু "কাজ" করে ধরে নিচ্ছি, এখানে সরলীকৃত
self.log.append(f" handler সম্পন্ন")
if resume_mode == "same":
self.restore_state() # PC অপরিবর্তিত -- একই instruction আবার চলবে
self.log.append(f" resume: একই instruction (PC={self.pc}) পুনরায় চেষ্টা")
elif resume_mode == "next":
saved_pc, saved_flags = self.saved_stack.pop()
self.pc = saved_pc + 1 # পরের instruction থেকে resume
self.flags = saved_flags
self.log.append(f" resume: পরের instruction (PC={self.pc})")
else: # terminate
self.halted = True
self.log.append(f" resume নেই -- প্রোগ্রাম terminate")
def step(self):
if self.halted or self.pc >= len(self.program):
self.halted = True
return
instr = self.program[self.pc]
self.log.append(f"EXEC PC={self.pc}: {instr}")
# এলোমেলো asynchronous interrupt -- বর্তমান instruction-এর সাথে সম্পর্কহীন
if random.random() \< 0.15:
self.raise_event("interrupt", 32, resume_mode="next")
return # interrupt handle হলো, এই instruction পরের ধাপে (resume PC-তে) চলবে
if instr == "DIV0":
self.raise_event("exception", 0, resume_mode="terminate")
return
if instr == "PAGEFAULT":
self.raise_event("exception", 14, resume_mode="same")
return
if instr == "SYSCALL":
self.raise_event("trap", 0x80, resume_mode="next")
return
self.pc += 1 # স্বাভাবিক instruction, শুধু এগিয়ে যাওয়া
program = ["ADD", "MOV", "PAGEFAULT", "MOV", "SYSCALL", "ADD", "SUB"]
random.seed(7)
cpu = CPU(program)
steps = 0
while not cpu.halted and steps \< 30:
cpu.step()
steps += 1
for line in cpu.log:
print(line)যাচাই করার বিষয়: PAGEFAULT-এর পরে log-এ দেখুন PC একই
থাকে (resume: একই instruction) — যতক্ষণ না সিমুলেশনে সেটা
সরাসরি “সমাধান” করে দিচ্ছি, বাস্তবে এটা অসীম loop-এ আটকে যেত
যদি handler সত্যিই সমস্যা সমাধান না করত (এটাই বাস্তব OS-এ যা
ঘটে page allocate করার আগে ও পরে)। SYSCALL-এর পরে PC পরের
instruction-এ যায়।
নিজে বাড়ান:
IFflag (interrupt mask) যোগ করুন — যখনTrue, asynchronous interrupt সাময়িকভাবে উপেক্ষা করা হবে (queue-তে জমা থাকবে)- NMI (non-maskable) সমর্থন করুন — mask flag-এর অবস্থা যাই হোক না কেন সবসময় সাথে সাথে handle হবে
- Nested exception সমর্থন করুন — একটা handler চলাকালীন যদি
আরেকটা exception ঘটে (
saved_stackইতিমধ্যেই এটার জন্য প্রস্তুত — যাচাই করুন এটা সঠিকভাবে কাজ করছে) - একটা
PRIORITYscheme যোগ করুন — একাধিক interrupt একসাথে pending থাকলে কোনটা আগে সাড়া পাবে তা নির্ধারণ করুন
বাস্তব সিস্টেমে
যেখানে এই তিনটা প্রতিদিন কাজ করে
Linux সিগন্যাল ব্যবস্থা। SIGSEGV (invalid memory access),
SIGFPE (arithmetic error), SIGILL (illegal instruction) —
এই সবই hardware exception-এর সরাসরি OS-level প্রতিফলন, যা এই
লেসনের experiment-এ সরাসরি observe করা হয়েছে। প্রতিটা crash
report, প্রতিটা “Segmentation fault” বার্তার পেছনে ঠিক এই
mechanism।
Timer interrupt আর preemptive scheduling। একটা hardware timer নিয়মিত বিরতিতে (সাধারণত প্রতি কয়েক মিলিসেকেন্ডে) একটা interrupt তোলে — এটাই OS-কে সুযোগ দেয় বর্তমান process থামিয়ে আরেকটা চালাতে, প্রতিটা প্রোগ্রামের “সমান সুযোগ” নিশ্চিত করতে। এই timer interrupt ছাড়া একটা infinite-loop program পুরো সিস্টেম আটকে দিত। Level 4-এ CPU scheduling নিয়ে বিস্তারিত আলোচনায় এই mechanism-ই কেন্দ্রীয় ভূমিকা পালন করবে।
gdb-এর breakpoint। যখন আপনি gdb-তে একটা breakpoint সেট
করেন, debugger আসলে সেই address-এর instruction-টা সাময়িকভাবে
int3 (x86-এ একটা এক-byte trap instruction) দিয়ে প্রতিস্থাপন
করে দেয়। প্রোগ্রাম সেখানে পৌঁছালে trap হয়, kernel debugger-কে
জানায়, আর আপনি program-এর অবস্থা পরিদর্শন করতে পারেন — মূল
instruction তখন পুনরুদ্ধার হয়ে থাকে যাতে আপনি “continue” করলে
স্বাভাবিকভাবে এগোতে পারেন।
NIC (Network Interface Card) interrupt। একটা network packet আসলে NIC hardware একটা interrupt তোলে, যাতে kernel জানতে পারে নতুন ডেটা এসেছে। উচ্চ-throughput নেটওয়ার্কে (প্রতি সেকেন্ডে লক্ষ লক্ষ packet) প্রতিটা packet-এর জন্য আলাদা interrupt একটা সিস্টেমকে অবশ করে দিতে পারে — পরের লেসনে এই সমস্যার সমাধান (interrupt coalescing) সরাসরি দেখা হবে।
x86 #GP (General Protection Fault)। যখন কোনো code
privilege violation করে (যেমন user-mode থেকে সরাসরি একটা
privileged instruction চালানোর চেষ্টা), CPU একটা #GP exception
তোলে — এটাই সেই hardware প্রক্রিয়া যা privilege ring আলাদা
রাখে, Level 4-এ kernel/user isolation-এর ভিত্তি।
RISC-V-এর ecall/mret। RISC-V-এর privileged architecture
ইচ্ছাকৃতভাবে ন্যূনতম — একটা মাত্র ecall instruction সব
ধরনের trap (user থেকে supervisor, supervisor থেকে machine mode)-এর
জন্য ব্যবহৃত হয়, আর mret/sret handler থেকে ফেরার জন্য।
এই সরলতা RISC দর্শনের (লেসন ২) সরাসরি প্রতিফলন — CISC x86-এর
তুলনায় অনেক কম, কিন্তু composable primitive।
Machine check exception (MCE) — abort-এর বাস্তব উদাহরণ। যখন CPU hardware নিজেই একটা সংশোধন-অযোগ্য ত্রুটি ধরে (যেমন একটা ECC memory error যা সংশোধন করা যাচ্ছে না), এটা একটা MCE তোলে — এই ধরনের exception থেকে কোনো নিরাপদ resume সম্ভব না, সিস্টেম সাধারণত সাথে সাথে বন্ধ (kernel panic/blue screen) হয়ে যায়, কারণ hardware-এর নির্ভরযোগ্যতা নিয়েই প্রশ্ন উঠেছে।
যে ভুলগুলো সবাই করে
“Interrupt আর exception একই জিনিস, শুধু নামের পার্থক্য।”
মেকানিজম একই রকম দেখতে হওয়ায় এই ভুল ধারণা সাধারণ, কিন্তু পার্থক্যটা মৌলিক, আর ব্যবহারিক ফলাফলে গুরুত্বপূর্ণ।
Interrupt asynchronous — বর্তমান instruction-এর সাথে সম্পর্কহীন, বাইরের জগৎ থেকে আসে, প্রতিবার প্রোগ্রাম চালালে একই জায়গায় ঘটবে এমন কোনো গ্যারান্টি নেই (একটা keypress ঠিক কোন instruction-এর সময় আসবে তা নির্ধারিত না)।
Exception synchronous — বর্তমান instruction নিজেই এর কারণ, একই ইনপুটে বারবার চালালে ঠিক একই জায়গায় ঘটবে, প্রতিবার।
এই পার্থক্যটাই নির্ধারণ করে কীভাবে debug করবেন — একটা exception reproducible (deterministic কারণ), একটা interrupt-জনিত সমস্যা (যেমন একটা race condition যেখানে interrupt টাইমিং প্রভাব ফেলে) প্রায়ই “heisenbug” — বারবার পরীক্ষা করলেও একই রকম নাও ঘটতে পারে।
“C-তে 'undefined behavior' মানে hardware-ও অনিশ্চিত/অপ্রত্যাশিতভাবে আচরণ করে।”
এই লেসনের “Under the Hood” ভাগে দেখানো হয়েছে — এটা একটা গুরুত্বপূর্ণ গুলিয়ে ফেলা। C/C++ standard-এর “undefined behavior” মানে ভাষার নিয়ম কোনো নির্দিষ্ট আচরণের প্রতিশ্রুতি দেয় না — এটা কম্পাইলারকে অপ্টিমাইজেশনের স্বাধীনতা দেওয়ার একটা কৌশল (যেমন “Integer Overflow” লেসনে দেখা compiler optimization যা signed overflow-কে “কখনো ঘটে না” ধরে নিয়ে কোড মুছে দেয়)।
কিন্তু hardware নিজে প্রায় সবসময় সুনির্দিষ্টভাবে আচরণ করে
— divide by zero একটা নির্দিষ্ট #DE exception তোলে, একটা
নির্দিষ্ট vector নম্বরে যায়, প্রতিবারই। CPU নিজে “যা খুশি তাই”
করে না। যা অনিশ্চিত তা হলো কম্পাইলার সেই hardware আচরণকে
কীভাবে ব্যবহার করবে অপ্টিমাইজেশনে — একটা optimizing compiler
হয়তো ধরে নেয় সেই exception-ঘটানো কোড path কখনো execute হবেই
না (যেহেতু UB), আর সেই অনুমানের ভিত্তিতে চারপাশের কোড পুনর্বিন্যাস
বা মুছে ফেলে। এটাই সেই বিপজ্জনক ফাঁক — hardware determinism
থাকা সত্ত্বেও, ভাষা-স্তরের UB-এর কারণে observable প্রোগ্রাম
আচরণ অপ্রত্যাশিত হয়ে যেতে পারে।
“একটা SIGSEGV মানেই মেমরি করাপশন হয়েছে।”
বেশিরভাগ ক্ষেত্রে SIGSEGV আসলে অনেক সরল একটা ঘটনা — প্রোগ্রাম এমন একটা virtual address access করার চেষ্টা করেছে যেটার জন্য কোনো বৈধ mapping নেই (একটা null pointer dereference, একটা বহুদূর out-of-bounds array access, বা একটা ইতিমধ্যে free করা memory-তে access করার চেষ্টা যার page mapping ইতিমধ্যেই সরানো হয়েছে)। MMU translation ব্যর্থ হয়, একটা page fault ওঠে, আর OS দেখে এই address প্রোগ্রামের বৈধ address space-এর অংশ না — তাই signal পাঠায়।
এটা “মেমরি করাপ্ট হয়ে গেছে” থেকে ভিন্ন একটা ঘটনা — কোনো bit flip বা ভুল ডেটা লেখা হয়নি, শুধু একটা ভুল ঠিকানা অ্যাক্সেসের চেষ্টা হয়েছিল, যা hardware/OS দ্রুত ধরে ফেলেছে। প্রকৃতপক্ষে, SIGSEGV হলো memory safety-এর একটা রক্ষাকবচ কাজ করছে — প্রোগ্রামকে অন্য প্রোগ্রামের memory এলোমেলোভাবে touch করা থেকে আটকাচ্ছে, একটা নীরব করাপশন এড়িয়ে বরং একটা তাৎক্ষণিক, স্পষ্ট crash দিচ্ছে। এই কারণেই memory-safety-সচেতন ভাষা ও টুল (যেমন AddressSanitizer) ইচ্ছাকৃতভাবে আরও বেশি SIGSEGV-সদৃশ crash তৈরি করার চেষ্টা করে — নীরব করাপশনের চেয়ে দ্রুত, স্পষ্ট ব্যর্থতা ভালো।
“System call একটা সাধারণ function call-এরই মতো, শুধু নাম আলাদা।”
বাস্তবে দুটোর খরচ ভিন্ন মাত্রার — একটা সাধারণ function call
মাত্র কয়েক cycle (একটা call/ret instruction জোড়া, stack-এ
return address push/pop), কিন্তু একটা system call একটা সম্পূর্ণ
privilege transition ঘটায়: trap instruction চালানো, state
সংরক্ষণ, vector table lookup, kernel-mode-এ ঢোকা (privilege
ring পরিবর্তন), handler execution, তারপর আবার user-mode-এ
ফেরা। এই পুরো যাত্রায় কয়েকশ CPU cycle লাগতে পারে, এমনকি হালকা
syscall-এও — একটা সাধারণ function call-এর তুলনায় দুই থেকে তিন
মাত্রার (order of magnitude) পার্থক্য।
এছাড়া privilege transition-এর সময় কিছু cache/TLB effect-ও
থাকে (ভিন্ন address space-এর কোড/ডেটা touch করায়), যা খরচ আরও
বাড়ায়। এই কারণেই performance-sensitive প্রোগ্রাম syscall
সংখ্যা কমানোর জন্য সচেতনভাবে ডিজাইন করে (যেমন একবারে বড় বাফার
পড়া, ছোট ছোট বহু read()-এর বদলে) — এই ব্যবহারিক পরামর্শ
সরাসরি এই লেসনের mechanism থেকে আসে, আর Level 4-এ syscall-এর
প্রকৃত খরচ পরিমাপ করে দেখা হবে।
বুঝেছেন কি না দেখুন
1নিচের প্রতিটা ঘটনাকে interrupt, exception, বা trap হিসেবে
চিহ্নিত করুন:
(ক) একটা network packet আসা
(খ) read() system call
(গ) array index বাউন্ডের বাইরে গিয়ে একটা invalid memory access
(ঘ) একটা mouse click
স্মরণ
read() system call
(গ) array index বাউন্ডের বাইরে গিয়ে একটা invalid memory access
(ঘ) একটা mouse click(ক) Interrupt। Network packet আসা সম্পূর্ণ বাহ্যিক ঘটনা, CPU যা করছিল তার সাথে সম্পর্কহীন, asynchronous — NIC hardware একটা interrupt তোলে।
(খ) Trap। read() একটা library function যা ভেতরে একটা
syscall/ecall instruction চালায় — এটা প্রোগ্রামের ইচ্ছাকৃত
সিদ্ধান্ত, দুর্ঘটনা না। Synchronous, deliberate — এটাই trap-এর
সংজ্ঞা।
(গ) Exception। Invalid memory access বর্তমান instruction-এরই (সেই load/store) সরাসরি ফলাফল — synchronous, কিন্তু ইচ্ছাকৃত না (প্রোগ্রামার চায়নি এটা ঘটুক, এটা একটা bug)। MMU translation ব্যর্থ হয়ে page fault (বা সরাসরি protection fault) তোলে।
(ঘ) Interrupt। ঠিক keypress-এর মতোই — বাহ্যিক hardware event, asynchronous।
2কেন একটা page fault-এর resume ঠিক সেই একই instruction থেকে
হয়, কিন্তু একটা timer interrupt-এর resume হয় পরের instruction
থেকে? দুটোর মধ্যে যুক্তিগত পার্থক্যটা ব্যাখ্যা করুন।
যুক্তি
পার্থক্যটা নির্ভর করে বাধাপ্রাপ্ত instruction-টা আসলে সফল হয়েছিল কি না তার উপর।
Page fault ঘটে instruction execute হওয়ার চেষ্টার সময়ই — MMU translation ব্যর্থ হয়, তাই instruction-টা কখনো সফলভাবে সম্পন্নই হয়নি। এটা যেন instruction-টা শুরুই হয়নি। তাই যদি handler সমস্যাটা সমাধান করে (page allocate করে), যুক্তিসঙ্গত পদক্ষেপ হলো একই instruction আবার চেষ্টা করা — এবার সফল হবে, কারণ এখন page valid। যদি hardware ভুল করে পরের instruction থেকে resume করত, মূল instruction-টা কখনোই সম্পন্ন হতো না — একটা সম্পূর্ণ error, যেন একটা লাইন কোড হারিয়ে গেল।
Timer interrupt সম্পূর্ণ ভিন্ন গল্প — এটা বর্তমান instruction-এর সাথে সম্পর্কহীন, বর্তমান instruction স্বাভাবিকভাবেই সম্পন্ন হচ্ছে (বা হয়ে গেছে) instruction boundary-তে যখন interrupt check হয়। Timer শুধু “এই মুহূর্তে ঢুকে পড়েছে” — বাধাপ্রাপ্ত instruction-টাকে পুনরায় চালানোর কোনো কারণ নেই, কারণ সেটা কখনো ব্যর্থই হয়নি। তাই resume হয় স্বাভাবিক পরের instruction থেকে, যেন কিছুই মাঝখানে ঘটেইনি (interrupt handler transparent)।
সাধারণ নিয়ম: যদি বাধাপ্রাপ্ত instruction ব্যর্থ হয়ে থাকে (এবং handler সমস্যা ঠিক করতে পারে), resume হয় সেই instruction থেকেই পুনরায়। যদি instruction সফলভাবে সম্পন্ন হওয়ার পরে বা তার সাথে সম্পর্কহীনভাবে ঘটনা ঘটে, resume হয় পরের instruction থেকে।
3একটা out-of-order CPU-তে, instruction I10 একটা exception তোলে
এই মুহূর্তে যখন I11, I12 ইতিমধ্যে out-of-order ভাবে execute
হয়ে গেছে আর তাদের ফলাফল ROB-এ “সম্পন্ন” চিহ্নিত। ঠিক কী কী
ঘটতে হবে যাতে precise exception বজায় থাকে?
প্রয়োগ
I10 একটা exception তোলে
এই মুহূর্তে যখন I11, I12 ইতিমধ্যে out-of-order ভাবে execute
হয়ে গেছে আর তাদের ফলাফল ROB-এ “সম্পন্ন” চিহ্নিত। ঠিক কী কী
ঘটতে হবে যাতে precise exception বজায় থাকে?আগের লেসনের ROB mechanism আর এই লেসনের exception mechanism একসাথে প্রয়োগ করে:
১. I10 ROB-এর মাথায় পৌঁছানো পর্যন্ত অপেক্ষা করতে হবে
commit-এর জন্য। যদিও I10 আগেই exception তুলেছে, সেই তথ্যটা
শুধু ROB entry-তে একটা “exception pending” flag হিসেবে সংরক্ষিত
থাকে — এটা সাথে সাথে handler চালু করে না, কারণ তখনও তার আগের
instruction-গুলো commit বাকি থাকতে পারে।
২. I10-এর আগের সব instruction (I1-I9) স্বাভাবিকভাবে
commit হবে, program order-এ। এই ধাপটা নিশ্চিত করে exception
handler যখন চলবে, architectural state ঠিক ততটুকুই প্রতিফলিত
করবে যতটা I10-এর ঠিক আগ পর্যন্ত হওয়া উচিত।
৩. I10 ROB-এর মাথায় পৌঁছালে, exception flag দেখা যায়।
এখন handler চালু হয় — কিন্তু commit হয় না I10-এর জন্য
(যেহেতু এটা ব্যর্থ হয়েছে, এর ফলাফল কখনো architectural state-এ
যাওয়া উচিত না)।
৪. I11, I12 (আর তাদের পরের সব ROB entry) সম্পূর্ণ flush
করতে হবে — এমনকি যদিও তারা ইতিমধ্যে “সম্পন্ন” চিহ্নিত। তাদের
ফলাফল কখনো architectural state-এ পৌঁছায়নি (কারণ commit শুধু
মাথা থেকে হয়, আর মাথা কখনো তাদের পর্যন্ত পৌঁছায়নি) — এই
flush নিশ্চিত করে তারা যেন কখনোই চলেইনি এমন অবস্থায় ফিরে যায়।
৫. এদের ব্যবহৃত physical register free list-এ ফিরে যায় (যদি সিস্টেমে register renaming ব্যবহৃত হয়, যা এই CPU-তে থাকার কথা যেহেতু এটা out-of-order), rename table পুনরুদ্ধার হয় সর্বশেষ committed mapping-এ।
ফলাফল: বাইরে থেকে দেখলে মনে হবে CPU ঠিক I9 পর্যন্ত সব
সফলভাবে চালিয়েছে, I10-এ থেমে গেছে, I11-এর পর থেকে কিছুই
ঘটেনি — precise, ঠিক যেন এটা একটা সরল in-order CPU ছিল। এই
পুরো প্রক্রিয়াটাই প্রমাণ করে কেন আগের লেসনে বলা হয়েছিল ROB
শুধু performance optimization না — এটা এই ধরনের সুসংগত
exception recovery-র আবশ্যিক পূর্বশর্ত।
4কেন একটা interrupt handler-এর প্রথম কাজ প্রায় সবসময় আরও
interrupt সাময়িকভাবে বন্ধ (mask) করে রাখা?
যুক্তি
মূল কারণ পুনঃপ্রবেশ (re-entrancy) সমস্যা এড়ানো আর state সংরক্ষণের সীমিত ক্ষমতা রক্ষা করা।
State সংরক্ষণের জন্য সাধারণত একটা নির্দিষ্ট, সীমিত সংখ্যক জায়গা (register বা একটা ছোট স্ট্যাক) ব্যবহৃত হয়। যদি একটা handler চলাকালীন আরেকটা interrupt এসে পড়ে (বিশেষত একই ধরনের, বা উচ্চ-frequency একটা interrupt source থেকে), আর সেটাও সাথে সাথে handle করার চেষ্টা করা হয়, দুটো সমস্যা হতে পারে:
১. State corruption। যদি দ্বিতীয় interrupt প্রথম handler-এর এখনো-ব্যবহৃত state (যেমন সেই সংরক্ষিত PC/flags) ওভাররাইট করে ফেলে, প্রথম handler শেষে সঠিক জায়গায় resume করতে পারবে না।
২. Stack overflow / অসীম nesting। যদি একই ধরনের interrupt বারবার দ্রুত আসতে থাকে (যেমন একটা faulty hardware device যা ক্রমাগত interrupt পাঠায়), আর প্রতিটা নতুন interrupt আগেরটার handler-কে বাধাগ্রস্ত করে নতুন nested handler শুরু করে, এটা কখনো শেষ না হওয়া nesting তৈরি করতে পারে — কার্যত একটা “interrupt storm” যা সিস্টেমকে অচল করে দেয় (এটাই পরের লেসনে DMA/interrupt coalescing আলোচনায় সরাসরি প্রাসঙ্গিক হবে)।
তাই একটা handler সাধারণত শুরুতেই IF flag (বা সমতুল্য) বন্ধ
করে দেয় — নিজের কাজ নিরাপদে, বাধাহীনভাবে শেষ করতে — আর শেষে
পুনরায় সক্ষম (re-enable) করে দেয়, নির্দিষ্ট, নিয়ন্ত্রিত সময়ের
জন্য নিজেকে “atomic” বানিয়ে। লক্ষ্য করুন এটা শুধু maskable
interrupt-এর ক্ষেত্রে প্রযোজ্য — NMI এই সুরক্ষার বাইরে, তাই
সেগুলোর handler বিশেষভাবে ছোট আর দ্রুত রাখা হয়, নেস্টিং এড়াতে
ভিন্ন কৌশল ব্যবহার করে (যেমন একটা “NMI চলাকালীন আরেকটা NMI
এলে তা কেবল pending থাকে, hardware-স্তরে”)।
5একটা নতুন CPU ডিজাইন করছেন যেখানে ROB নেই (কারণ এটা একটা সরল,
in-order CPU)। এই CPU কি precise exception দিতে পারবে? ব্যাখ্যা
করুন কেন বা কেন না।
ডিজাইন
হ্যাঁ — একটা সরল in-order CPU স্বাভাবিকভাবেই precise exception দিতে পারে, ROB ছাড়াই।
এটা প্রথম নজরে বিস্ময়কর মনে হতে পারে, কিন্তু কারণটা যুক্তিসঙ্গত: precise exception-এর সমস্যাটা তৈরিই হয় out-of-order execution-এর কারণে — যখন পরের instruction আগের instruction-এর আগে architectural state আপডেট করে ফেলতে পারে। একটা in-order CPU-তে, সংজ্ঞানুসারে, instruction-গুলো ঠিক program order-এই execute হয় (pipeline-এ হয়তো একে অপরের সাথে ওভারল্যাপ করে, কিন্তু কেউ প্রকৃত architectural state আপডেট করে তার আগের instruction-এর আগে না)।
তাই যখন I5-এ exception ঘটে একটা in-order CPU-তে, I6, I7
এখনো architectural state স্পর্শই করেনি — pipeline-এ তারা হয়তো
fetch/decode ধাপে আছে, কিন্তু তাদের write-back (architectural
state আপডেট) ধাপ এখনো আসেনি, কারণ pipeline-এর গঠনই নিশ্চিত করে
in-order write-back (হ্যাজার্ড আর stall মেকানিজম — লেসন ১০-১১
থেকে মনে করুন — ঠিক এটাই নিশ্চিত করত)। CPU শুধু pipeline-এর সেই
পরবর্তী instruction-গুলো flush (বাতিল) করে দেয়, ঠিক যেমন একটা
branch misprediction-এ করা হয় — কোনো architectural state ইতিমধ্যেই
নষ্ট হয়নি, তাই flush করাই যথেষ্ট।
তাহলে ROB কেন লাগে? শুধু তখনই যখন out-of-order execution
যোগ হয় — যখন I7 I5-এর আগেই সম্পন্ন হয়ে যেতে পারে, তখনই
architectural state-এ সময়ের ক্রম উল্টে যাওয়ার ঝুঁকি তৈরি হয়,
আর সেই ঝুঁকি সামলাতেই ROB-এর in-order commit গ্যারান্টি লাগে।
সংক্ষেপে: precise exception নিজে ROB-নির্ভর কোনো ধারণা না — এটা একটা general গ্যারান্টি (architectural state ঠিক program order মেনে আপডেট হওয়া) যা in-order CPU-তে স্বাভাবিকভাবেই আসে বিনামূল্যে, কিন্তু out-of-order CPU-তে সুস্পষ্টভাবে ROB (বা সমতুল্য কোনো mechanism) দিয়ে পুনর্নির্মাণ করতে হয়।
এরপর কী
যখন বাইরের একটা device সত্যিই ডেটা পাঠাতে চায়
Interrupt-এর পরিচয় এই লেসনে হলো, কিন্তু একটা বাস্তব প্রশ্ন এখনো অনুত্তরিত: keyboard-এর একটা keypress interrupt পাঠানো সহজ (মাত্র এক byte ডেটা), কিন্তু একটা disk বা network card-কে যখন মেগাবাইটের পর মেগাবাইট ডেটা memory-তে আনতে হয়, প্রতিটা byte-এর জন্য আলাদা interrupt পাঠানো (আর CPU-কে প্রতিটা byte নিজে copy করতে বাধ্য করা) স্পষ্টতই অবাস্তব — CPU-র প্রায় পুরো সময় শুধু ডেটা নড়াচড়া করাতেই যাবে, প্রকৃত হিসাবের জন্য কিছুই বাঁচবে না।
মডিউলের শেষ লেসনে আমরা দেখব কীভাবে এই সমস্যার সমাধান হয়েছে — DMA (Direct Memory Access), একটা dedicated hardware controller যে device আর memory-র মধ্যে সরাসরি বিশাল পরিমাণ ডেটা সরিয়ে দেয়, CPU-র প্রতিটা byte নিয়ে মাথা না ঘামিয়েই — আর কাজ শেষে, ঠিক এই লেসনেরই সেই mechanism ব্যবহার করে, একটা interrupt দিয়ে CPU-কে জানায় “কাজ শেষ।”
আরও পড়ুন
- Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3, Chapter 6 · Interrupt ও exception handling-এর প্রামাণ্য x86 রেফারেন্স — vector table, fault/trap/abort শ্রেণিবিভাগ
- The RISC-V Instruction Set Manual, Volume II: Privileged Architecture · ecall, mret, ও trap handling-এর ন্যূনতম, পরিষ্কার ডিজাইন
- Computer Organization and Design (RISC-V Edition), Chapter 5 — Patterson & Hennessy · Exception handling-এর pipeline-স্তরের বাস্তবায়ন
- signal(7) — Linux manual page · হার্ডওয়্যার exception কীভাবে POSIX signal-এ রূপান্তরিত হয় তার বাস্তব বিবরণ