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

Exception আর interrupt — pipeline-এ ব্যতিক্রম সামলানো

Exceptions and Interrupts — Traps, RISC-V CSRs and mret, Precise Exceptions in the Pipeline, Interrupt Priority, Latency and Cost

Taught in Bangla, this lesson separates exceptions, interrupts and traps in RISC-V, x86 and ARM terms, and shows what hardware does on a trap using mepc, mcause, mtval, mstatus and mtvec, then mret. It walks a small trap handler, which pipeline stage detects which exception, precise versus imprecise exceptions, interrupt priority, masking, latency, nesting and controllers, the cost of a trap, and trap-and-emulate and virtualization, with a trap simulator.

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

  • Exception, interrupt আর trap-এর পার্থক্য — RISC-V, x86 আর ARM-এর পরিভাষা পাশাপাশি
  • RISC-V-এর machine-mode trap CSR, mtvec-এর direct ও vectored mode, mret আর CSR instruction
  • একটি trap handler কী কী করে — assembly-তে একটি ছোট উদাহরণসহ
  • Pipeline-এর কোন ধাপে কোন exception ধরা পড়ে, precise exception কীভাবে নিশ্চিত হয়, একই সময়ে দুটি হলে কোনটি আগে
  • Interrupt-এর priority, masking, latency, nesting আর interrupt controller; trap-এর খরচের হিসাব

১ · স্বাভাবিক ধারা যখন ভাঙে — exception, interrupt আর trap

এতদিন আমরা ধরে নিয়েছি CPU একটার পর একটা instruction চালায়, আর ধারা বদলায় শুধু branch আর jump-এ। পর্ব ৩৫ থেকে ৩৭-এ সেই ধারাকে pipeline-এ সাজানো দেখেছেন। কিন্তু বাস্তবে দুই রকম অপ্রত্যাশিত ঘটনা ঘটে। এক, চলমান instruction নিজেই এমন কিছু করতে চায় যা সে একা পারে না বা করার অনুমতি নেই — অজানা bit-pattern, মেমরিতে না থাকা page, operating system-এর কাছে অনুরোধ। দুই, প্রোগ্রামের বাইরে থেকে কেউ CPU-র মনোযোগ চায় — timer বলছে সময় শেষ, network card বলছে packet এসেছে, কি-বোর্ডে কেউ চাপ দিয়েছে।

একটি রেস্তোরাঁর রান্নাঘরের কথা ভাবুন। বাবুর্চি একটার পর একটা অর্ডার রান্না করছেন। হঠাৎ একটি অর্ডারে লেখা “কাচ্চি, কিন্তু চাল নেই” — এই অর্ডারের কারণেই কাজ থেমেছে; একে বলি exception. বাবুর্চি ঠিক কোন অর্ডারে থেমেছেন, সেটা লিখে রেখে ম্যানেজারকে ডাকেন। আবার কখনো অর্ডারের সাথে কোনো সম্পর্ক ছাড়াই দরজায় সরবরাহকারী এসে ঘণ্টা বাজান — এটি interrupt. বাবুর্চি হাতের তরকারিটা নামিয়ে (চলমান instruction শেষ করে) তবেই দরজায় যান, তারপর ফিরে ঠিক যেখানে ছিলেন সেখান থেকে শুরু করেন। দুই ক্ষেত্রেই রান্না থামিয়ে “ম্যানেজারের কাছে যাওয়া” — এই নিয়ন্ত্রণ-বদলের নাম trap.

RISC-V-এর privileged specification এই তিনটি শব্দ এভাবেই ব্যবহার করে। Exception হলো synchronous — একটি নির্দিষ্ট instruction-এর সাথে বাঁধা; একই প্রোগ্রাম একই ডেটা আর একই মেমরি-বিন্যাসে আবার চালালে একই জায়গায় আবার ঘটবে। Interrupt হলো asynchronous — কোনো instruction-এর দোষে নয়, বাইরের ঘটনায়; প্রোগ্রাম আবার চালালে অন্য কোনো instruction-এর ফাঁকে আসবে। আর trap হলো দুটোর যেকোনোটির ফলে CPU-র নিজে থেকে একটি trap handler-এ নিয়ন্ত্রণ পাঠানো।

ঘটনাধরনকার কারণেসাধারণত কী হয়
Illegal instruction — অজানা opcode, বা user mode-এ privileged instructionExceptionওই instructionOS প্রসেসটি বন্ধ করে, বা না-থাকা instruction software-এ অনুকরণ করে
Misaligned access — যেমন ঠিকানা 0x1002 থেকে lwException (chip hardware-এ না সামলালে)load/storeFirmware বা OS বাইট বাইট পড়ে অনুকরণ করে
Page fault — page মেমরিতে নেই বা অনুমতি নেইExceptionfetch, load বা storeOS page এনে instruction আবার চালায় (পর্ব ৫০)
ecallException (ইচ্ছাকৃত)ওই instructionSystem call — OS কাজটি করে দেয়
ebreak — breakpointException (ইচ্ছাকৃত)ওই instructionDebugger-এর কাছে নিয়ন্ত্রণ
Timer interruptInterrupttimer hardwareOS scheduler সিদ্ধান্ত নেয়, অন্য প্রসেস চালাতে পারে
External (device) interruptInterruptdisk, network, USB, কি-বোর্ডDriver ডেটা নেয়, যন্ত্রকে জানায় (পর্ব ৫২)

লক্ষ করুন, তালিকায় integer overflow, শূন্য দিয়ে ভাগ বা floating point-এর সমস্যা নেই। পর্ব ২০-এ দেখেছেন, MIPS-এর add overflow-তে trap করে, কিন্তু RISC-V-এর add চুপচাপ wrap করে। পর্ব ২২-এ দেখেছেন RISC-V-এ শূন্য দিয়ে ভাগে exception নেই (ফল নির্দিষ্ট: quotient-এর সব বিট 1), অথচ x86-এর div exception তোলে। আর পর্ব ২৪-এ দেখেছেন floating point-এর exception ডিফল্টে শুধু একটি sticky flag ওঠায়; RISC-V-এ এর জন্য trap-এর ব্যবস্থাই নেই, flag ওঠে fcsr-এ। কোন ঘটনা trap হবে, তা ISA-র সিদ্ধান্ত — hardware-এর অনিবার্যতা নয়।

একই শব্দ, ভিন্ন বইয়ে ভিন্ন অর্থ

পরীক্ষায় আর ইন্টারভিউতে সবচেয়ে বেশি গোলমাল হয় শব্দ নিয়ে। নিচের টেবিল মনে রাখার মতো:

যেখানেinstruction-এর কারণে (synchronous)বাইরের ঘটনা (asynchronous)“trap” শব্দের অর্থ
RISC-V (privileged spec)exceptioninterruptদুটোর যেকোনোটিতে handler-এ যাওয়া — ছাতা-শব্দ
x86 (Intel-এর manual)exception — তিন ভাগ: fault (আবার চালানো যায়, যেমন page fault), trap (instruction-এর পরে জানানো হয়, যেমন int3), abort (গুরুতর, যেমন double fault)interrupt (maskable আর NMI — non-maskable, যা বন্ধ করা যায় না); int n-কে বলে software interruptexception-এর একটি উপশ্রেণি
ARM (ARMv8-A)synchronous exception (যেমন svc, data abort)IRQ, FIQ, SError — এগুলোও “asynchronous exception”কোনো instruction-কে উঁচু exception level-এ (যেমন hypervisor-এ) পাঠানোর ব্যবস্থা; ছাতা-শব্দ হলো “exception”
Patterson–Hennessy (MIPS আর RISC-V সংস্করণ)exception (ভেতরের ঘটনা)interrupt (বাইরের ঘটনা)প্রায় ব্যবহার করে না; ছাতা-শব্দ হিসেবে exception বলে
পুরোনো OS বইtrap বা software interrupt (system call)interruptপ্রায়ই “ইচ্ছাকৃত exception”

তাই উত্তর লেখার সময় প্রথম লাইনেই বলে দিন কোন পরিভাষা ধরছেন। “Interrupt” শব্দটি অনেকে সব ধরনের trap-এর জন্য ঢিলেঢালাভাবে ব্যবহার করেন — যেমন “page fault interrupt”. RISC-V-এর অর্থে সেটি ভুল: page fault একটি exception. এই পর্বে সর্বত্র RISC-V-এর অর্থ ধরা হলো।

২ · Trap-এর মুহূর্তে hardware কী করে — RISC-V-এর CSR

পর্ব ২৫-এ privilege level আর পর্ব ২৬-এ ecall/ebreak দেখেছেন। এবার প্রশ্নটা hardware-এর: trap হলে CPU ঠিক কী কী নিজে থেকে করে? উত্তরের প্রতিটি অংশ একটি করে প্রশ্নের জবাব। ফিরব কোথায়? কেন এলাম? কোন ঠিকানা বা instruction দোষী? কে সামলাবে, কোন ক্ষমতায়? সামলানোর সময় আরেকটি interrupt এসে গোলমাল করবে না তো?

RISC-V এসব তথ্য রাখে কয়েকটি CSR-এ। ছোট embedded সিস্টেমে প্রায়ই শুধু দুটি mode থাকে — machine mode (M, সবচেয়ে ক্ষমতাবান, firmware) আর user mode (U). তাই আগে M-mode-এর CSR:

CSRকী রাখেtrap-এর সময় hardware
mepcফেরার ঠিকানাException হলে দোষী instruction-এরই PC; interrupt হলে যে instruction এখনো চলেনি তার PC
mcauseকারণ: সবচেয়ে উঁচু বিট = interrupt বিট, বাকিটা exception codeযেমন ২ = illegal instruction, ৮ = U-mode থেকে ecall, 0x80000007 = machine timer interrupt (RV32)
mtvalবাড়তি তথ্যMisaligned, access fault বা page fault-এ খারাপ ঠিকানা; illegal instruction-এ (chip চাইলে) instruction-এর বিটগুলো; অন্যথায় সাধারণত 0
mtvecHandler-এর ঠিকানা (BASE) আর MODE (নিচের ২ বিট)MODE 0 (direct): সব trap BASE-এ। MODE 1 (vectored): exception BASE-এ, interrupt BASE + ৪ × cause-এ
mstatusMIE (বিট ৩, global interrupt enable), MPIE (বিট ৭), MPP (বিট ১২–১১)MPIE ← MIE, MIE ← 0, MPP ← আগের mode, তারপর mode ← M
mie / mipপ্রতিটি interrupt উৎসের আলাদা enable আর pending বিট (যেমন MTIE/MTIP বিট ৭, MEIE/MEIP বিট ১১)Hardware pending বিট বসায়; handler কারণ দূর করলে নামে
mscratchHandler-এর নিজের জন্য একটি খালি ঘরHardware ছোঁয় না; handler সাধারণত এখানে নিজের stack-এর ঠিকানা রাখে

সব মিলিয়ে, একটি trap-এ hardware এক ধাপে (একই সাথে) এই কাজগুলো করে:

  1. mepc ← trap-এর instruction-এর PC
  2. mcause ← interrupt বিট আর code; mtval ← ঠিকানা বা instruction (অথবা 0)
  3. mstatus.MPIE ← MIE; MIE ← 0 (নতুন interrupt আপাতত বন্ধ); MPP ← আগের privilege mode; mode ← M
  4. PC ← mtvec-এর BASE (vectored mode-এ interrupt হলে BASE + ৪ × cause)

আর ফেরার instruction mret ঠিক উল্টো কাজ করে: PC ← mepc; MIE ← MPIE; MPIE ← 1; mode ← MPP; MPP ← সবচেয়ে নিচের সমর্থিত mode (সাধারণত U). লক্ষ করুন, hardware সাধারণ ৩১টি register-এর একটিও বাঁচায় না — সেটা software-এর কাজ (পরের অংশ)। Interrupt বিট আর code মিলিয়ে mcause-এর কয়েকটি মান:

Interrupt বিট = 0 (exception)Interrupt বিট = 1 (interrupt)
0 instruction address misaligned · 1 instruction access fault1 supervisor software · 3 machine software
2 illegal instruction · 3 breakpoint5 supervisor timer · 7 machine timer
4 load address misaligned · 5 load access fault9 supervisor external · 11 machine external
6 store address misaligned · 7 store access fault
8 ecall from U · 9 ecall from S · 11 ecall from M
12 instruction page fault · 13 load page fault · 15 store page fault

এখানে কোনো সংখ্যা মুখস্থ করার দরকার নেই; প্রয়োজন হলে spec-এর টেবিল দেখবেন। কিন্তু দুটি জিনিস বোঝা জরুরি: ecall-এর code নির্ভর করে কোন mode থেকে ডাকা হলো তার ওপর (handler এতে বোঝে অনুরোধকারী কে), আর interrupt বিট সবচেয়ে উঁচু বিটে বলে signed হিসেবে পড়লে interrupt-এর mcause ঋণাত্মক — handler একটি bltz দিয়েই দুই দল আলাদা করতে পারে।

CSR পড়া-লেখার instruction

CSR সাধারণ register নয়, তাই এদের জন্য Zicsr extension-এ আলাদা instruction. তিনটিই একটি কাজ atomically করে — পুরোনো মান rd-তে পড়া আর নতুন মান লেখা, মাঝখানে কিছু ঢুকতে পারে না:

Instructionকাজ (t = CSR-এর পুরোনো মান)প্রচলিত pseudo-instruction
csrrw rd, csr, rs1CSR ← rs1; rd ← t (অদলবদল)csrw csr, rs (rd = x0)
csrrs rd, csr, rs1CSR ← t OR rs1 (বিট বসানো); rd ← tcsrr rd, csr (rs1 = x0, শুধু পড়া)
csrrc rd, csr, rs1CSR ← t AND (NOT rs1) (বিট মোছা); rd ← tcsrc csr, rs

এদের immediate রূপও আছে (csrrwi, csrrsi, csrrci, ৫-বিট মান)। যেমন csrsi mstatus, 8 মানে বিট ৩ (MIE) বসানো — সব interrupt চালু; csrci mstatus, 8 মানে বন্ধ। User mode থেকে machine-mode CSR ছুঁতে চাইলে? সেটি নিজেই একটি illegal instruction exception — ক্ষমতার সীমা এভাবেই hardware পাহারা দেয়।

Supervisor mode — সংক্ষেপে

Linux-এর মতো OS চলে supervisor mode-এ (S), M-mode-এ থাকে ছোট firmware (যেমন OpenSBI). S-mode-এর নিজস্ব একই রকম CSR আছে: sepc, scause, stval, stvec, sstatus (SIE, SPIE, SPP) আর ফেরার জন্য sret. ডিফল্টে সব trap M-mode-এ যায়; M-mode চাইলে medeleg আর mideleg CSR-এ বিট বসিয়ে নির্দিষ্ট exception আর interrupt সরাসরি S-mode-এ পাঠাতে (delegate করতে) পারে — যেমন user প্রোগ্রামের ecall আর page fault, যাতে প্রতিবার firmware ঘুরে যেতে না হয়। খুঁটিনাটি (কোন বিট delegate করা যায়, Sstc-এর মতো extension) spec-এর version-ভেদে বদলায়, তাই নিজের chip-এর ডকুমেন্টেশন মিলিয়ে নেবেন।

এবার একটি ecall-এর পুরো যাত্রা ধাপে ধাপে দেখুন। এখানে একটি সরল সিস্টেম ধরা হলো যেখানে শুধু M আর U mode, তাই user প্রোগ্রামের ecall সরাসরি M-mode handler-এ যায়। দাগ দেওয়া লাইনটি এই ধাপে চলছে:

100010000 li    a7, 64      # সেবার নম্বর200010004 li    a0, 1       # argument300010008 ecall40001000c mv    s1, a0      # ফল5680000100 csrrw sp, mscratch, sp780000104 ...   # register সেভ, mcause পড়া8         call  do_syscall9         csrr  t0, mepc10         addi  t0, t0, 411         csrw  mepc, t012         ...   # register ফেরত13         mret
অবস্থা (mtvec = 0x80000100, direct)
mode
PC (পরের)
mepc
mcause
mstatus
a0
s1
  1. User কোড প্রস্তুত। প্রোগ্রাম U-mode-এ। a7-এ সেবার নম্বর (Linux-এর RISC-V ABI-তে 64 মানে write), a0-তে প্রথম argument. পরের instruction ecall, ঠিকানা 0x00010008. mstatus = 0x00000008, অর্থাৎ MIE (বিট ৩) = ১, MPIE = ০, MPP = ০০ (U).
  2. ecall — hardware-এর এক ধাপ। ecall নিজে কোনো register বদলায় না; সে শুধু exception তোলে। Hardware: mepc = 0x00010008 (ecall-এরই ঠিকানা, পরেরটির নয়), mcause = ৮ (ecall from U), mtval = 0. mstatus: MPIE ← MIE = ১, MIE ← ০, MPP ← U, তাই 0x00000080. Mode = M, PC = mtvec = 0x80000100.
  3. Handler শুরু। প্রথম instruction csrrw sp, mscratch, sp — user-এর sp আর handler-এর stack-এর ঠিকানা এক instruction-এ অদলবদল, কারণ user-এর sp-কে বিশ্বাস করা যায় না। তারপর register সেভ, csrr t0, mcause = ৮ দেখে system call-এর পথে। CSR-গুলো আগের মতোই আছে।
  4. সেবা আর mepc + ৪. do_syscall কাজটি করল, ফল a0 = ৫ (যেমন ৫ বাইট লেখা হলো) — সেভ করা a0-এর ঘরেই রাখা হয়, যাতে restore-এর পর user এটি পায়। এখন সবচেয়ে গুরুত্বপূর্ণ লাইন: mepc = 0x00010008 + ৪ = 0x0001000c. এটা না করলে mret আবার ecall-এ ফিরবে, আর সেবা বারবার হতে থাকবে।
  5. mret. PC = mepc = 0x0001000c; mode = MPP = U; MIE ← MPIE = ১; MPIE ← ১; MPP ← U. তাই mstatus = 0x00000088 (বিট ৭ আর বিট ৩). Privilege-এর পতন আর লাফ — একই instruction-এ, যাতে মাঝখানে user কোড M-mode-এ এক মুহূর্তও না চলে।
  6. User কোড ফিরে এসেছে। mv s1, a0: s1 = ৫. User প্রোগ্রামের চোখে ecall যেন একটি সাধারণ function call, যা a0-তে ফল দিল। Linux-এ একই নাচ হয় S-mode-এ: scause, sepc আর sret, কারণ firmware user-এর ecall-কে S-mode-এ delegate করে রাখে।

৩ · Handler-এর কাজ — register বাঁচানো থেকে mret

Hardware মাত্র কয়েকটি CSR লিখল আর লাফ দিল; বাকি সব দায়িত্ব handler software-এর। Handler-কে একজন ভালো অতিথির মতো আচরণ করতে হয় — যে ঘরে ঢুকেছে, বের হওয়ার সময় সব জিনিস ঠিক আগের জায়গায় রেখে যেতে হবে। কারণ interrupt-এর ক্ষেত্রে বাধা পাওয়া প্রোগ্রাম জানেই না যে তাকে থামানো হয়েছিল; একটি register-এর মান বদলে গেলে সে ভুল ফল দেবে, আর সেই বাগ কখনো একইভাবে আবার দেখা দেবে না।

একটি সাধারণ handler-এর ধাপ:

  1. নিজের stack নেওয়া: csrrw sp, mscratch, sp. Handler কোনো register ব্যবহারের আগেই এটা করে, কারণ এখনো কোনো register খালি নেই।
  2. Register সেভ: অন্তত যেগুলো handler বা তার ডাকা C কোড বদলাতে পারে — বাস্তবে প্রায়ই সব ৩১টি (OS-এর context switch-এর জন্যও এগুলো লাগে)।
  3. কারণ পড়া: mcause (আর দরকার হলে mtval, mepc). Interrupt হলে কোন উৎস; exception হলে কোন ধরন।
  4. সামলানো: system call চালানো, page আনা, timer নতুন করে বসানো, বা প্রসেস বন্ধ করা।
  5. ফেরার ঠিকানা ঠিক করা: ecall/ebreak হলে বা অনুকরণ (emulation) শেষ হলে mepc += ৪ (C extension-এ ২-বাইটের instruction হলে +২); page fault-এ যোগ নয় — একই instruction আবার চলবে, এবার সফলভাবে; interrupt-এ যোগ নয় — mepc আগেই না-চলা instruction-এ।
  6. Register ফেরত, sp ফেরত, mret.
    .align 2                        # mtvec-এর BASE অন্তত ৪-বাইট aligned
trap_entry:
    csrrw sp, mscratch, sp          # sp <-> mscratch: এখন sp = handler-এর stack
    addi  sp, sp, -64
    sw    ra,  0(sp)
    sw    t0,  4(sp)
    sw    t1,  8(sp)
    sw    a0, 12(sp)
    # ... বাকি caller-saved register (a1–a7, t2–t6) একইভাবে
    csrr  t0, mcause
    bltz  t0, is_interrupt          # সবচেয়ে উঁচু বিট 1 = interrupt
    li    t1, 8                     # 8 = ecall from U-mode
    bne   t0, t1, other_exception
    call  do_syscall                # a7 = সেবা, a0–a5 = argument, ফল a0-তে
    sw    a0, 12(sp)                # ফল সেভ করা a0-এর ঘরে, restore-এর পর user পাবে
    csrr  t0, mepc
    addi  t0, t0, 4                 # ecall-এর পরের instruction-এ ফেরা
    csrw  mepc, t0
    j     restore
is_interrupt:
    call  handle_interrupt          # timer হলে mtimecmp নতুন করে লেখা ইত্যাদি
    j     restore
other_exception:
    call  handle_fault              # illegal instruction, misaligned, page fault ...
restore:
    lw    ra,  0(sp)
    lw    t0,  4(sp)
    lw    t1,  8(sp)
    lw    a0, 12(sp)
    # ... বাকিগুলো ফেরত
    addi  sp, sp, 64
    csrrw sp, mscratch, sp          # user-এর sp ফেরত, mscratch-এ আবার handler-এর stack
    mret

এই উদাহরণ ধরে নিয়েছে trap সবসময় U-mode থেকে আসে। M-mode নিজেই trap করলে (যেমন handler-এর ভেতরে বাগ) mscratch-এর অদলবদল উল্টে যাবে; বাস্তব firmware তাই প্রথমে দেখে mscratch শূন্য কি না বা MPP কী। OS-এর দিক থেকে system call-এর পুরো যাত্রা — argument, errno, strace — আছে Operating Systems কোর্সের System call পর্বে; এখানে আমরা hardware-এর সীমানা পর্যন্তই থাকছি।

এবার নিজে চালিয়ে দেখুন। নিচের simulator একটি ছোট RV32I-ধাঁচের যন্ত্র, যার mstatus, mtvec, mepc, mcause, mtval আছে। প্রোগ্রামে তিনটি ফাঁদ: একটি ecall, একটি অবৈধ bit-pattern (0xffffffff — RISC-V-এ সব-1 encoding সংরক্ষিত ও অবৈধ), আর ঠিকানা 0x1002 থেকে একটি lw. এই যন্ত্র misaligned access hardware-এ সামলায় না, তাই handler সেটি বাইট বাইট পড়ে অনুকরণ করবে — trap-and-emulate-এর একটি ছোট উদাহরণ।

// Tiny RV32I-like machine with machine-mode trap CSRs: mstatus, mtvec, mepc, mcause, mtval
const hex = v => "0x" + (v >>> 0).toString(16).padStart(8, "0");
const prog = { 0x00: "li a7 1", 0x04: "li a0 42", 0x08: "ecall", 0x0c: "word 0xffffffff",
               0x10: "li t0 0x1002", 0x14: "lw t1 t0", 0x18: "halt" };
const mem = { 0x1000: 0x11, 0x1001: 0x22, 0x1002: 0x33, 0x1003: 0x44, 0x1004: 0x55, 0x1005: 0x66 };
const NAME = { 2: "illegal instruction", 4: "load address misaligned", 8: "ecall from U-mode" };
const r = {}, csr = { mstatus: 0x8, mtvec: 0x200, mepc: 0, mcause: 0, mtval: 0 }; let pc = 0, mode = "U";   // MIE=1, direct mode
function trap(cause, tval) {                                   // what the HARDWARE does, all in one step
  const mie = (csr.mstatus >> 3) & 1, mpp = mode === "M" ? 3 : 0;
  Object.assign(csr, { mepc: pc, mcause: cause, mtval: tval, mstatus: (csr.mstatus & ~0x1888) | (mie << 7) | (mpp << 11) });
  mode = "M"; pc = csr.mtvec & ~3;
  console.log(`TRAP  ${NAME[cause]}: mepc=${hex(csr.mepc)} mcause=${cause} mtval=${hex(tval)} mstatus=${hex(csr.mstatus)} -> pc=${hex(pc)}, M-mode`);
  handler();
}
function handler() {                                           // what the SOFTWARE at mtvec does
  if (csr.mcause === 8) console.log(`  handler: syscall a7=${r.a7} "print a0" -> ${r.a0}; mepc += 4 (do not repeat ecall)`);
  if (csr.mcause === 2) console.log(`  handler: bad encoding ${hex(csr.mtval)}; a real OS would kill the process, here we skip it: mepc += 4`);
  if (csr.mcause === 4) {                                               // trap-and-emulate: do the load byte by byte
    const a = csr.mtval, rd = prog[csr.mepc].split(" ")[1];
    r[rd] = (mem[a] | mem[a + 1] << 8 | mem[a + 2] << 16 | mem[a + 3] << 24) >>> 0;
    console.log(`  handler: emulate lw from ${hex(a)} with 4 byte loads -> ${rd}=${hex(r[rd])}; mepc += 4`);
  }
  csr.mepc += 4;                                               // mret: MIE=MPIE, MPIE=1, mode=MPP, MPP=U
  const mpie = (csr.mstatus >> 7) & 1, mpp = (csr.mstatus >> 11) & 3;
  csr.mstatus = (csr.mstatus & ~0x1888) | (mpie << 3) | 0x80; mode = mpp === 3 ? "M" : "U"; pc = csr.mepc;
  console.log(`MRET  pc=${hex(pc)} mstatus=${hex(csr.mstatus)} back to ${mode}-mode`);
}
while (prog[pc] !== "halt") {
  const [op, a, b] = prog[pc].split(" ");
  if (op === "li") { r[a] = +b; console.log(`${hex(pc)} U  li ${a}, ${b}`); pc += 4; }
  else if (op === "ecall") trap(8, 0);
  else if (op === "lw") { if (r[b] % 4) trap(4, r[b]); else pc += 4; }
  else trap(2, +a);                                            // unknown encoding -> illegal instruction
}
console.log(`halt at ${hex(pc)}: a0=${r.a0} t1=${hex(r.t1)}`);

আসল আউটপুট:

0x00000000 U  li a7, 1
0x00000004 U  li a0, 42
TRAP  ecall from U-mode: mepc=0x00000008 mcause=8 mtval=0x00000000 mstatus=0x00000080 -> pc=0x00000200, M-mode
  handler: syscall a7=1 "print a0" -> 42; mepc += 4 (do not repeat ecall)
MRET  pc=0x0000000c mstatus=0x00000088 back to U-mode
TRAP  illegal instruction: mepc=0x0000000c mcause=2 mtval=0xffffffff mstatus=0x00000080 -> pc=0x00000200, M-mode
  handler: bad encoding 0xffffffff; a real OS would kill the process, here we skip it: mepc += 4
MRET  pc=0x00000010 mstatus=0x00000088 back to U-mode
0x00000010 U  li t0, 0x1002
TRAP  load address misaligned: mepc=0x00000014 mcause=4 mtval=0x00001002 mstatus=0x00000080 -> pc=0x00000200, M-mode
  handler: emulate lw from 0x00001002 with 4 byte loads -> t1=0x66554433; mepc += 4
MRET  pc=0x00000018 mstatus=0x00000088 back to U-mode
halt at 0x00000018: a0=42 t1=0x66554433

তিনটি trap-এর প্রতিটিতে hardware-এর অংশ একই: mepc = দোষী instruction-এর ঠিকানা (0x08, 0x0c, 0x14), mstatus 0x00000008 থেকে 0x00000080 (MIE বিট ৩ নেমে MPIE বিট ৭-এ গেল), PC = 0x200. পার্থক্য শুধু mcause (৮, ২, ৪) আর mtval-এ: ecall-এ 0, illegal instruction-এ instruction-এর বিট 0xffffffff, misaligned load-এ খারাপ ঠিকানা 0x00001002. Handler তিনবারই mepc-তে ৪ যোগ করল — তবে তিনটি ভিন্ন কারণে: ecall আবার চালানো চলবে না; illegal instruction-টি এখানে বাদ দেওয়া হলো (আসল OS প্রসেসটিই বন্ধ করত, Linux-এ SIGILL); আর load-টি handler নিজেই সেরে ফেলেছে। Little-endian ক্রমে 0x1002 থেকে চার বাইট 33, 44, 55, 66 জুড়ে t1 = 0x66554433 — user প্রোগ্রাম জানলও না যে তার lw আসলে software-এ হয়েছে, শুধু অনেক ধীরে। প্রতিবার mret-এর পর mstatus = 0x00000088: MIE আবার ১, MPIE = ১।

৪ · Pipeline-এ exception — কোন ধাপে কী ধরা পড়ে, কোনটি আগে

Single-cycle CPU-তে (পর্ব ৩২) সমস্যাটি সহজ: একটি instruction পুরো চলে বা চলে না। কিন্তু পাঁচ ধাপের pipeline-এ (পর্ব ৩৫) একই সময়ে পাঁচটি instruction আলাদা আলাদা অবস্থায়। একটি instruction সমস্যা দেখালে তার আগের কয়েকটি তখনো শেষ হয়নি, আর পরের কয়েকটি ইতিমধ্যে শুরু হয়ে গেছে। তার ওপর ভিন্ন exception ভিন্ন ধাপে ধরা পড়ে:

ধাপযা ধরা পড়ে (RISC-V)মন্তব্য
IFinstruction page fault, instruction access faultPC-র ঠিকানা অনুবাদ বা অনুমতিতে সমস্যা (পর্ব ৫০)
IDillegal instruction, ecall, ebreak, অনুমতিহীন CSR accessDecode করলেই বোঝা যায়
EXjump/branch-এর লক্ষ্য misaligned (C extension না থাকলে); MIPS-এ add-এর overflowRISC-V-এ গাণিতিক overflow বা শূন্য দিয়ে ভাগে কিছু নেই
MEMload/store misaligned, access fault, page faultঠিকানা EX-এ হিসাব হয়, অনুবাদ ও পরীক্ষা MEM-এ
যেকোনো সময়interruptHardware একটি instruction বেছে তার আগে “ঢোকায়”

এখান থেকেই আসল সমস্যা। ধরুন I3 একটি lw, যার page fault ধরা পড়বে MEM-এ — cycle ৬-এ। আর ঠিক পরের instruction I4-এর opcode অবৈধ, যা ধরা পড়বে ID-তে — cycle ৫-এ! অর্থাৎ পরের instruction-এর exception সময়ের হিসাবে আগে চোখে পড়ে। CPU যদি দেখামাত্র I4-এর exception নিয়ে নিত, তাহলে mepc হতো I4-এর ঠিকানা, অথচ I3 তখনো অসম্পূর্ণ — handler-এর চোখে এমন অবস্থা যা প্রোগ্রাম-ক্রমে কখনো হওয়ার কথা নয়।

মূল ধারণা

Precise exception: trap নেওয়ার মুহূর্তে (১) দোষী instruction-এর আগের সব instruction সম্পূর্ণ — তাদের register আর মেমরি-লেখা দৃশ্যমান; (২) দোষী instruction আর তার পরের সব instruction-এর কোনো ছাপ নেই — কোনো register, মেমরি বা CSR বদলায়নি; (৩) mepc ঠিক দোষী instruction-এর ঠিকানা। অর্থাৎ handler এমন একটি অবস্থা দেখে, যেন CPU একটার পর একটা instruction চালাচ্ছিল আর ঠিক এখানে থেমেছে।

সমাধান: লিখে রাখা, নেওয়া পরে, প্রোগ্রামের ক্রমে

In-order pipeline-এ কৌশলটি সরল। Exception ধরা পড়লেই নেওয়া হয় না; শুধু সেই instruction-এর সাথে pipeline register-এ একটি “exception বিট” আর কারণ লিখে রাখা হয়, আর instruction-টি নিরীহ (কোনো লেখা ছাড়া) হয়ে সামনে এগোয়। একটি নির্দিষ্ট ধাপকে commit point ধরা হয় — সাধারণত MEM-এর শেষ, কারণ এর পরে আর কোনো exception ধরা পড়ে না। কোনো instruction commit point-এ পৌঁছে exception বিট দেখালে তখনই trap: ওই instruction আর তার পেছনের সবগুলোকে flush করা হয় (bubble বা nop বানানো, register file আর মেমরিতে লেখার অনুমতি বন্ধ), mepc-তে বসে তার PC, আর পরের cycle-এ fetch শুরু হয় mtvec থেকে।

এটি কেন precise — একটি ছোট যুক্তি। In-order pipeline-এ instruction-গুলো প্রোগ্রামের ক্রমেই ধাপগুলো পার হয়, তাই যেকোনো মুহূর্তে পুরোনো instruction সবসময় নতুনটির চেয়ে সামনের ধাপে। এখন দুটি শর্ত মিলিয়ে দেখুন: (ক) কোনো architectural অবস্থা (register file-এ লেখা WB-তে, মেমরিতে store MEM-এ) commit point-এর আগে বদলায় না, আর দোষী instruction-এর নিজের লেখা একই cycle-এ আটকে দেওয়া হয়; (খ) সব exception commit point-এর আগেই ধরা পড়ে। ধরুন instruction k commit point-এ trap নিল। k-এর আগের প্রতিটি j আগেই commit point পার হয়েছে — তাদের exception থাকলে তখনই trap হতো, তাই তারা নির্বিঘ্নে শেষ হয়েছে বা হচ্ছে (WB-তে থাকা instruction-কে শেষ করতে দেওয়া হয়)। k আর তার পরের প্রতিটি এখনো commit point পার হয়নি, তাই (ক) অনুযায়ী কিছুই লেখেনি; flush করলে কোনো ছাপ থাকে না। ফলে উপরের তিন শর্তই পূরণ। একই যুক্তি থেকে আসে “সবচেয়ে পুরোনোটি আগে” নিয়ম: একই cycle-এ বা ভিন্ন cycle-এ যতগুলো exception-ই ধরা পড়ুক, commit point-এ প্রথম পৌঁছায় প্রোগ্রাম-ক্রমে সবচেয়ে পুরোনোটি।

একই instruction-এর একাধিক সমস্যা থাকলে? যেমন একটি lw-এর ঠিকানা misaligned আর page-ও নেই। তখন ISA একটি অগ্রাধিকার-ক্রম ঠিক করে দেয় — RISC-V privileged spec-এ এর একটি টেবিল আছে (মোটামুটি: instruction-এর নিজের fetch-এর সমস্যা আগে, তারপর decode-এর, তারপর ডেটা-ঠিকানার)। তবে ঠিক এই জোড়ায় — misaligned বনাম page fault — spec কোনটি আগে, তা chip-এর ওপর ছেড়ে দিয়েছে। উত্তর মুখস্থের চেয়ে নীতিটি বোঝা বেশি কাজের: hardware যেন সবসময় একটিই, নির্ধারিত কারণ জানায়।

এবার I3-এর page fault আর I4-এর illegal opcode — দুটোর লড়াই cycle ধরে দেখুন:

Cycle ৪সব স্বাভাবিক
        c1   c2   c3   c4
I1 addi IF   ID   EX   MEM
I2 add       IF   ID   EX
I3 lw             IF   ID
I4 ???                 IF
x1 = 0, x2 = 0 (এখনো কেউ WB-তে পৌঁছায়নি)
Cycle ৫I4-এর illegal opcode — শুধু লেখা হলো
        c1   c2   c3   c4   c5
I1 addi IF   ID   EX   MEM  WB      x1 = 5 লেখা হলো
I2 add       IF   ID   EX   MEM
I3 lw             IF   ID   EX      ঠিকানা = x9 হিসাব
I4 ???                 IF   ID !    exc = illegal (2), নেওয়া হলো না
I5 addi                     IF
Cycle ৬I3-এর page fault, commit point-এ
        c1   c2   c3   c4   c5   c6
I1 addi IF   ID   EX   MEM  WB
I2 add       IF   ID   EX   MEM  WB      x2 = 10 লেখা হলো
I3 lw             IF   ID   EX   MEM !   load page fault (13)
I4 ???                 IF   ID ! EX
I5 addi                     IF   ID
I6 sw                            IF
MEM-এর শেষে সবচেয়ে পুরোনো exception: I3-এর
Cycle ৬-এর শেষেFlush আর CSR
I3  MEM  → flush   x3-তে লেখা হবে না
I4  EX   → flush   illegal-এর খবরও মুছে গেল
I5  ID   → flush
I6  IF   → flush   sw মেমরি ছোঁয়নি

mepc   = 0x108   (I3-এর PC)
mcause = 13      (load page fault)
mtval  = x9 + 0  (load-এর ঠিকানা)
Cycle ৭ আর পরেHandler, তারপর আবার I3
c7 : IF ← mtvec   (handler-এর প্রথম instruction)
     ... OS page এনে দেয়, mepc বদলায় না ...
mret → PC = 0x108 : I3 lw আবার, এবার সফল
       I4 আবার ID-তে → এবার I4-ই সবচেয়ে পুরোনো
       → trap: mcause = 2, mepc = 0x10c
  1. Cycle ৪ — সব স্বাভাবিক। Ik cycle k-তে IF-এ ঢোকে। I1 এখন MEM-এ, I2 EX-এ, I3 (lw x3, 0(x9)) ID-তে, আর I4 (অবৈধ bit-pattern) মাত্র fetch হলো। এখনো কোনো instruction WB পার হয়নি, তাই architectural register অপরিবর্তিত।
  2. Cycle ৫ — I4-এর সমস্যা আগে চোখে পড়ল। I1 WB-তে: x1 = ৫ লেখা হলো — I1 সম্পূর্ণ। I4 ID-তে decode হয়ে দেখা গেল opcode অবৈধ। কিন্তু CPU trap নিল না; শুধু ID/EX pipeline register-এ “exception = illegal (২)” লিখে রাখল। কারণ I4-এর চেয়ে পুরোনো I2 আর I3 এখনো চলছে — তাদের কারও exception থাকলে সেটিই আগে যাওয়ার কথা।
  3. Cycle ৬ — I3-এর page fault. I2 WB-তে: x2 = ১০. I3 MEM-এ পৌঁছে দেখল x9-এ রাখা ঠিকানার page নেই — load page fault (১৩). MEM-এর শেষই commit point; সেখানে সবচেয়ে পুরোনো exception-ওয়ালা instruction I3, তাই trap নেবে I3, I4 নয়। সময়ে আগে দেখা গেলেও প্রোগ্রাম-ক্রমে I4 পরে।
  4. Flush আর CSR লেখা। I3, I4, I5, I6 — চারটিকেই bubble বানানো হলো; register file আর মেমরির write enable বন্ধ, তাই x3 বদলাল না, I6-এর sw মেমরি ছুঁল না। I4-এর লিখে রাখা illegal-এর খবরও মুছে গেল। mepc = 0x108 (I3), mcause = ১৩, mtval = খারাপ ঠিকানা। Architectural অবস্থা: x1 = ৫, x2 = ১০ — ঠিক যেন I1, I2 চলে I3-এ থেমেছে।
  5. Handler আর পুনরায় চালানো। Cycle ৭-এ fetch শুরু mtvec থেকে। OS page এনে দেয় আর mepc-তে কিছু যোগ করে না, তাই mret ফেরে 0x108-এ — I3 আবার চলে, এবার সফল। তারপর I4 আবার decode হয়, আর এবার সে-ই সবচেয়ে পুরোনো: trap, mcause = ২, mepc = 0x10c. কোনো exception হারায় না; শুধু সঠিক ক্রমে আসে।

একই পরিস্থিতি simulator-এ। প্রতিটি instruction-এর সাথে কোথায় তার exception ধরা পড়বে তা দেওয়া আছে; প্রোগ্রাম cycle ধরে এগোয়, exception শুধু লিখে রাখে, আর trap নেয় MEM-এর শেষে:

// 5-stage pipeline with PRECISE exceptions: faults are only recorded where detected and taken in program order
const ST = ["IF", "ID", "EX", "MEM", "WB"];
const prog = [                                           // pc, text, register written, fault: [stage, cause]
  { pc: 0x100, t: "addi x1, x0, 5", rd: "x1", v: 5 },
  { pc: 0x104, t: "add  x2, x1, x1", rd: "x2", v: 10 },
  { pc: 0x108, t: "lw   x3, 0(x9)", rd: "x3", v: 99, fault: ["MEM", "load page fault (mcause 13)"] },
  { pc: 0x10c, t: ".word 0xffffffff", fault: ["ID", "illegal instruction (mcause 2)"] },
  { pc: 0x110, t: "addi x5, x0, 7", rd: "x5", v: 7 },
  { pc: 0x114, t: "sw   x5, 0(x0)" }];
const regs = { x1: 0, x2: 0, x3: 0, x5: 0 }, row = prog.map(() => []);
let trapAt = null, mepc = null;
for (let c = 1; trapAt === null && c <= 12; c++) {
  for (let i = 0; i < prog.length; i++) {                // instruction i entered IF in cycle i+1: stage = c - 1 - i
    const s = c - 1 - i, I = prog[i];
    if (s < 0 || s > 4 || I.flushed) continue;
    row[i][c] = ST[s];
    if (I.fault && I.fault[0] === ST[s]) { I.exc = I.fault[1]; console.log(`cycle ${c}: I${i + 1} ${I.t} -> ${I.exc} detected in ${ST[s]}, only RECORDED`); }
    if (ST[s] === "WB" && I.rd) { regs[I.rd] = I.v; console.log(`cycle ${c}: I${i + 1} writes ${I.rd} = ${I.v}  (commits)`); }
  }
  // commit point = end of MEM: every older instruction has already passed it
  const k = c - 4; const O = prog[k];
  if (O && O.exc) {
    trapAt = c; mepc = O.pc;
    prog.forEach((I, j) => { if (j >= k && c - 1 - j >= 0) { I.flushed = true; row[j][c] += "x"; } });
    console.log(`cycle ${c}: I${k + 1} is the OLDEST faulting instruction -> trap: ${O.exc}, mepc = 0x${mepc.toString(16)}`);
    console.log(`          flush I${k + 1}..I${prog.findLastIndex(I => I.flushed) + 1}; their writes never happen (I4's illegal opcode is forgotten)`);
  }
}
console.log("\n      " + Array.from({ length: trapAt }, (_, c) => String(c + 1).padEnd(5)).join(""));
prog.forEach((I, i) => console.log(`I${i + 1}    ` + Array.from({ length: trapAt }, (_, c) => (row[i][c + 1] || "").padEnd(5)).join("")));
console.log(`\ncycle ${trapAt + 1}: fetch handler at mtvec.  Architectural state: ${JSON.stringify(regs)}, mepc = 0x${mepc.toString(16)}`);

আসল আউটপুট:

cycle 5: I1 writes x1 = 5  (commits)
cycle 5: I4 .word 0xffffffff -> illegal instruction (mcause 2) detected in ID, only RECORDED
cycle 6: I2 writes x2 = 10  (commits)
cycle 6: I3 lw   x3, 0(x9) -> load page fault (mcause 13) detected in MEM, only RECORDED
cycle 6: I3 is the OLDEST faulting instruction -> trap: load page fault (mcause 13), mepc = 0x108
          flush I3..I6; their writes never happen (I4's illegal opcode is forgotten)

      1    2    3    4    5    6    
I1    IF   ID   EX   MEM  WB        
I2         IF   ID   EX   MEM  WB   
I3              IF   ID   EX   MEMx 
I4                   IF   ID   EXx  
I5                        IF   IDx  
I6                             IFx  

cycle 7: fetch handler at mtvec.  Architectural state: {"x1":5,"x2":10,"x3":0,"x5":0}, mepc = 0x108

প্রথম exception-বার্তা cycle ৫-এ — I4-এর, কিন্তু তাতে কিছু ঘটল না। Cycle ৬-এ I3-এর page fault ধরা পড়ল ঠিক commit point-এ, আর I3 তখন pipeline-এ থাকা সবচেয়ে পুরোনো exception-ওয়ালা instruction, তাই trap: mepc = 0x108. টেবিলে x চিহ্ন মানে flush — I3 MEM-এ, I4 EX-এ, I5 ID-তে, I6 IF-এ থাকা অবস্থায় বাতিল। কে শেষ হলো আর কে হলো না, তা শেষ লাইনে স্পষ্ট: x1 = 5, x2 = 10 (I1, I2 commit করেছে), x3 = 0 (I3-এর load-এর মান 99 কখনো লেখা হয়নি), x5 = 0 (I5 WB-তে পৌঁছানোর আগেই বাতিল)। Handler cycle ৭-এ শুরু। একটি জিনিস লক্ষ করুন: এখানে trap-এর খরচের একটি অংশ সরাসরি দেখা যাচ্ছে — চারটি instruction-এর কাজ ফেলে দেওয়া হলো।

৫ · Precise বনাম imprecise — কেন precise এত জরুরি

Precise exception বিনা খরচে আসে না: commit point পর্যন্ত exception বয়ে নিতে হয়, flush-এর তার টানতে হয়, আর লম্বা latency-র instruction (যেমন floating point-এর ভাগ, যা অনেক cycle নেয়) থাকলে পরের সহজ instruction-গুলোকে আগে শেষ হতে দেওয়া যায় না — অপেক্ষা করতে হয়। তাই ইতিহাসে কিছু দ্রুত মেশিন imprecise exception মেনে নিয়েছিল। বহুল উদ্ধৃত উদাহরণ IBM System/360 Model 91 (১৯৬০-এর দশকের শেষ ভাগ), যার floating point unit instruction-কে ক্রমের বাইরে শেষ করত; exception হলে OS জানত শুধু “এই এলাকার কোনো একটি instruction”-এ সমস্যা হয়েছে — কোনটি, আর আশপাশের কোনগুলো শেষ হয়েছে, তা নিশ্চিত নয়। পরে DEC Alpha-র মতো কিছু RISC-এর floating point exception-ও ডিফল্টে imprecise ছিল বলে জানা যায়; precise আচরণ দরকার হলে প্রোগ্রামে বিশেষ “trap barrier” instruction বসাতে হতো।

Imprecise হলে কী হারায়?

  • Virtual memory. Demand paging-এর পুরো ধারণা দাঁড়িয়ে আছে একটি প্রতিশ্রুতির ওপর: page fault হলে OS page এনে দেবে, আর দোষী instruction আবার শুরু থেকে চলবে, যেন কিছুই হয়নি। এর জন্য জানতে হবে ঠিক কোন instruction, আর নিশ্চিত হতে হবে যে সে বা তার পরের কেউ অর্ধেক কাজ করে রাখেনি। Imprecise হলে আবার চালানো মানে কিছু কাজ দুবার হওয়া, আর কিছু একবারও না। (Page fault-এর hardware-দিক আর TLB আসবে পর্ব ৫০-এ।)
  • Debugging. Debugger বলে “এই লাইনে crash হয়েছে, register-গুলো এই”. Imprecise হলে সেই লাইন আর register মিথ্যা হতে পারে।
  • Trap-and-emulate. Software-এ একটি instruction অনুকরণ করতে হলে তার আগের অবস্থা ঠিকঠাক থাকতে হবে।
  • Interrupt আর context switch. যেকোনো মুহূর্তে প্রসেস থামিয়ে পরে নিখুঁতভাবে চালু করতে একটি পরিষ্কার “এখানে থেমেছি” বিন্দু লাগে।

তাই আজকের প্রায় সব general-purpose CPU-তে অন্তত integer আর মেমরির exception precise. RISC-V সব exception-কেই precise রাখার নীতি নিয়েছে; floating point-এর ক্ষেত্রে আরও সহজ পথ — trap-ই নেই, শুধু flag (পর্ব ২৪), তাই কোনো সমস্যাই নেই।

আরও গভীরে: আধুনিক out-of-order CPU শত শত instruction একসাথে চালায়, ক্রমের বাইরে শেষ করে — তবু exception precise. কৌশলটি হলো reorder buffer (ROB): প্রতিটি instruction-এর ফল আর exception বিট প্রোগ্রাম-ক্রমে সাজানো একটি সারিতে থাকে, আর architectural অবস্থা বদলায় শুধু সারির মাথা থেকে, একে একে (retire বা commit). মাথায় exception-ওয়ালা instruction এলে পুরো সারি flush — ঠিক এই পর্বের commit point-এর বড় সংস্করণ। এর বিস্তারিত পর্ব ৪০-এ। একটি অস্বস্তিকর পরিণতিও আছে: flush হওয়া instruction-গুলো register-এ কিছু না লিখলেও cache-এ ছাপ রেখে যেতে পারে — Meltdown নামের দুর্বলতা ঠিক এই ফাঁক থেকে, যার ধারণা আর প্রতিরক্ষা পর্ব ৪১-এ।

৬ · Interrupt — কখন নেওয়া হয়, কাকে আগে, কত দ্রুত

Interrupt কোনো instruction-এর দোষ নয়, তাই CPU নিজে বেছে নেয় কোথায় সেটি ঢোকাবে — সবসময় দুটি instruction-এর মাঝখানে। Pipeline-এ এর অর্থ: একটি instruction বেছে নেওয়া হয় (সাধারণত commit point-এ পৌঁছানো পরের instruction), তার আগের সবগুলো শেষ হতে দেওয়া হয়, সে আর তার পরেরগুলো flush হয়, আর mepc-তে বসে তার ঠিকানা — যে instruction এখনো চলেনি। এভাবে interrupt-কেও precise রাখা হয়: mret-এর পর ঠিক সেখান থেকেই চলা শুরু, mepc-তে কিছু যোগ না করে।

কখন interrupt নেওয়া হবে — তিনটি তালা

RISC-V-এ একটি M-mode interrupt নেওয়া হয় তখনই, যখন একসাথে: (১) mip-এ তার pending বিট ১ (hardware বসিয়েছে); (২) mie-তে সেই উৎসের enable বিট ১; (৩) global অনুমতি আছে — CPU M-mode-এ থাকলে mstatus.MIE = ১ লাগবে, আর U বা S mode-এ থাকলে M-mode interrupt সবসময় অনুমোদিত (নিচের mode উঁচু mode-এর interrupt আটকাতে পারে না)। এটাই interrupt masking: MIE হলো সুইচবোর্ডের মেইন সুইচ, mie-র বিটগুলো প্রতিটি বাতির আলাদা সুইচ। Handler-এ ঢোকার সময় hardware MIE নামিয়ে দেয় বলেই একটি handler চলার মাঝে আরেকটি interrupt এসে তার register সেভের কাজ এলোমেলো করতে পারে না। OS-ও ছোট “critical section”-এ interrupt বন্ধ রাখে।

Priority: একসাথে কয়েকটি pending হলে? RISC-V spec M-mode-এর জন্য একটি স্থির ক্রম দেয়: machine external > machine software > machine timer, তারপর supervisor-এর একই তিনটি। অনেক যন্ত্রের (disk, network, USB) মধ্যে কে আগে, সেটা ঠিক করে interrupt controller — CPU-র বাইরে বা পাশে বসানো একটি সার্কিট।

ISAController-এর নামকাজ (সংক্ষেপে)
RISC-VCLINT (বা নতুন ACLINT); PLIC; নতুন নকশায় AIA (APLIC, IMSIC)CLINT: timer (সাধারণত সব core-এর জন্য একটি mtime, প্রতিটি core-এর নিজস্ব mtimecmp) আর software interrupt. PLIC: অনেক যন্ত্রের external interrupt-কে priority অনুযায়ী core-এ পাঠানো
x86APIC — প্রতি core-এ local APIC, যন্ত্রের দিকে I/O APIC; আধুনিক PCIe-তে MSILocal APIC-এ timer আর core-থেকে-core interrupt (IPI); vector দিয়ে IDT-র ২৫৬টি ঘরের একটি বাছাই
ARMGIC (Generic Interrupt Controller, কয়েকটি version); Cortex-M-এ NVICPriority, core-এ বিতরণ, masking; NVIC hardware-এই nesting সামলায়

যন্ত্র থেকে bus বেয়ে interrupt কীভাবে আসে, MSI কী, আর DMA কীভাবে interrupt-এর সংখ্যা কমায় — এসব hardware-দিক পর্ব ৫২-এ। OS-এর চোখে interrupt আর ISR-এর পরিচয় আছে Operating Systems কোর্সের OS-এর চোখে হার্ডওয়্যার পর্বের interrupt অংশে।

Vectored interrupt: mtvec-এর MODE = ১ হলে interrupt-এর জন্য আলাদা প্রবেশদ্বার — BASE + ৪ × cause. যেমন mtvec = 0x80000001 হলে BASE = 0x80000000, machine timer (cause ৭) গেলে 0x8000001c-তে, machine external (cause ১১) 0x8000002c-তে; প্রতিটি ঘরে সাধারণত একটি jump. লাভ: handler-কে mcause পড়ে খুঁজতে হয় না, latency কয়েক instruction কমে। Exception কিন্তু vectored mode-এও BASE-এ যায়। x86-এর IDT আর ARM-এর vector table একই ধারণার ভিন্ন রূপ।

Nested interrupt: একটি ধীর handler চলার সময় জরুরি আরেকটি interrupt এলে? RISC-V-এর মৌলিক নকশায় hardware MIE বন্ধ করে, তাই ডিফল্টে nesting নেই। চাইলে handler নিজেই mepc, mcause, mstatus stack-এ রেখে, কম জরুরি উৎসগুলো mie-তে বন্ধ করে, আবার csrsi mstatus, 8 দিয়ে MIE চালু করতে পারে — mepc আগে না রাখলে নতুন trap তা মুছে দেবে। ARM Cortex-M-এর NVIC এই কাজটি hardware-এ করে; RISC-V-এর জন্যও এমন controller-এর প্রস্তাব আছে (যেমন CLIC).

Interrupt latency — ঘণ্টা বাজা থেকে handler-এর কাজ শুরু

Interrupt latency = যন্ত্র সংকেত দেওয়া থেকে handler-এর প্রথম দরকারি instruction পর্যন্ত সময়। এর অংশগুলো:

  1. চিনতে পারা: সংকেত clock-এর সাথে মেলানো (synchronizer, পর্ব ১৮) আর controller-এর বাছাই — কয়েক cycle.
  2. Instruction-এর সীমা পর্যন্ত অপেক্ষা: চলমান instruction বা commit point পর্যন্ত — সাধারণত কয়েক cycle, তবে খুব লম্বা instruction থাকলে বেশি।
  3. Flush আর লাফ: pipeline-এর গভীরতার সমান মোটামুটি কয়েক থেকে কুড়ি-ত্রিশ cycle.
  4. Handler fetch: handler-এর কোড instruction cache-এ না থাকলে মেমরি থেকে আনতে শত cycle (পর্ব ৪৭)।
  5. Software prologue: stack নেওয়া, register সেভ — দরকারি কাজের আগে কয়েক ডজন instruction.
  6. Mask-এর সময়: interrupt বন্ধ থাকা অবস্থায় এলে, বন্ধ থাকার পুরো সময়টা অপেক্ষা — real-time সিস্টেমে worst-case latency-র সবচেয়ে বড় অংশ প্রায়ই এটি।

এবার একটি timer interrupt দুটি instruction-এর মাঝে কীভাবে ঢোকে, দেখুন। এখানে mtvec = 0x80000001 (vectored):

100010000 addi t0, t0, 1200010004 addi t1, t1, 2300010008 add  t2, t0, t1458000001c j    timer_isr   # BASE + 4 × 76timer_isr: ... register সেভ7           mtimecmp = mtime + 100008           ... ফেরত, mret
অবস্থা (mtimecmp শুরুতে 1000, mie.MTIE = 1)
mtime
mip.MTIP
mode
PC (পরের)
mepc
mcause
mstatus.MIE
  1. সাধারণ চলা। U-mode-এ addi t0, t0, 1 চলল। CLINT-এর counter mtime = ৯৯৯, তুলনার register mtimecmp = ১০০০ — এখনো সময় হয়নি, তাই mip.MTIP = ০।
  2. ঘণ্টা বাজল। mtime ১০০০-এ পৌঁছাতেই CLINT MTIP = ১ করল। এই মুহূর্তে addi t1 pipeline-এ commit point পার হয়ে গেছে, তাই তাকে শেষ হতে দেওয়া হলো। তিনটি তালা খোলা: MTIP = ১, MTIE = ১, আর CPU U-mode-এ, তাই M-mode interrupt-এর জন্য global অনুমতি স্বয়ংক্রিয়।
  3. দুটি instruction-এর মাঝে trap. add t2 (0x00010008) চলার আগেই trap: mepc = 0x00010008 — না-চলা instruction-এর ঠিকানা; mcause = 0x80000007 (interrupt বিট ১, code ৭); MIE = ০। Vectored mode, তাই PC = 0x80000000 + ৪ × ৭ = 0x8000001c, সেখানে একটি jump.
  4. Handler কারণ দূর করে। Handler mtimecmp = mtime + ১০০০০ লিখল — পরের tick-এর সময়। এতেই MTIP নেমে যায়। mip-এর এই বিটটি read-only — software সরাসরি মুছতে পারে না; কারণটি (mtime ≥ mtimecmp) দূর হলেই বিটটি নিজে নামে। কারণ দূর না করে mret করলে সঙ্গে সঙ্গে আবার একই interrupt — CPU আটকে যেত। এখানে OS-এর scheduler চাইলে অন্য প্রসেসে যাওয়ার সিদ্ধান্তও নিতে পারে।
  5. mret — যেন কিছুই হয়নি। PC = mepc = 0x00010008, mode = U, MIE = ১. এবার add t2, t0, t1 চলল। mepc-তে কিছু যোগ হয়নি, কারণ interrupt-এর mepc আগে থেকেই না-চলা instruction-এ। Register সব ঠিকঠাক ফেরানো হয়েছে, তাই প্রোগ্রামের ফল একই — শুধু কিছুটা দেরিতে।

৭ · একটি trap-এর দাম — হিসাব করে দেখা

Trap একটি অপ্রত্যাশিত branch-এর চেয়েও দামি। খরচের অংশগুলো: (১) flush — pipeline-এ থাকা পরের instruction-গুলোর কাজ ফেলে দেওয়া, প্রবেশে একবার আর mret-এর পর আবার ভরতে; (২) register সেভ ও ফেরত — ৩১টি করে store আর load; (৩) handler-এর আসল কাজ; (৪) পরোক্ষ খরচ — handler-এর কোড আর ডেটা cache থেকে প্রোগ্রামের জিনিস সরিয়ে দেয়, branch predictor-এর ইতিহাস এলোমেলো হয়। এগুলোকে CPI দিয়ে cycle-এ বদলানো যায়।

সূত্র

একটি trap-এর cycle = flushপ্রবেশ + flushফেরা + (সেভ-ফেরত instruction + handler instruction) × CPIhandler. পর্যায়ক্রমিক interrupt-এর overhead = হার (প্রতি সেকেন্ডে কতবার) × প্রতি trap-এর cycle ÷ clock rate.

উদাহরণ (হাতে): ১ GHz-এর একটি পাঁচ ধাপের CPU. প্রবেশে ৩ cycle আর ফেরায় ৩ cycle flush; সেভ-ফেরত ৬৪টি instruction; handler ৪০০টি instruction; handler-এর CPI ১.২৫. তাহলে একটি trap = ৩ + ৩ + (৬৪ + ৪০০) × ১.২৫ = ৬ + ৫৮০ = ৫৮৬ cycle = ৫৮৬ ns. Timer যদি সেকেন্ডে ১০০০ বার আসে: ১০০০ × ৫৮৬ = ৫,৮৬,০০০ cycle, ১ GHz-এর ১০০ কোটির মধ্যে — ০.০৫৮৬%. সেকেন্ডে ১০,০০০ বার হলে ০.৫৮৬%. অর্থাৎ timer interrupt নিজে সস্তা; কিন্তু হার বাড়লে বা handler ভারী হলে দ্রুত দামি। লক্ষ করুন, ২০ ধাপের গভীর pipeline-এ প্রতিটি flush ২০ cycle ধরলে মোট হয় ৬২০ cycle — গভীর pipeline-এ trap তুলনামূলক বেশি দামি।

নিচের calculator দুটি ক্ষেত্র তুলনা করে: একটি ছোট handler আর একটি OS timer tick, যেখানে scheduler-এর কাজ বেশি আর handler-এর কোড cache-এ না থাকায় প্রথম fetch-এ ১০০ cycle লাগে। সাথে latency — handler-এর প্রথম instruction পর্যন্ত:

// Cost of one trap/interrupt, and the CPU share eaten by a periodic timer interrupt
const GHz = 2;                                          // clock: 2 GHz -> 0.5 ns per cycle
function trapCycles(p) {                                // all parts in cycles
  const parts = {
    "wait for current instr": p.drain,                  // interrupt is taken between instructions
    "flush younger instrs":   p.flush,                  // refill of the pipeline after the jump
    "fetch at mtvec (miss?)": p.vecMiss,                // first handler instruction may miss in I-cache
    "save + restore regs":    p.saveRestore * p.cpi,    // e.g. 31 sw + 31 lw
    "handler body":           p.body * p.cpi,
    "mret + refill":          p.flush };
  return { parts, total: Object.values(parts).reduce((a, b) => a + b, 0) };
}
const small = { drain: 2, flush: 4, vecMiss: 0, saveRestore: 62, body: 100, cpi: 1.0 };
const tick  = { drain: 2, flush: 4, vecMiss: 100, saveRestore: 62, body: 2000, cpi: 1.5 };
for (const [name, p] of [["small handler, 5-stage", small], ["OS timer tick (scheduler work)", tick]]) {
  const { parts, total } = trapCycles(p);
  console.log(`${name}:`);
  for (const [k, v] of Object.entries(parts)) console.log(`   ${k.padEnd(24)} ${String(v).padStart(6)} cycles`);
  const latency = p.drain + p.flush + p.vecMiss;        // until the first handler instruction starts
  console.log(`   total ${total} cycles = ${(total / GHz).toFixed(1)} ns; latency to first handler instr = ${latency} cycles = ${(latency / GHz).toFixed(1)} ns`);
}
console.log("\ntimer Hz   small handler   OS tick   (share of CPU time at 2 GHz)");
for (const hz of [100, 250, 1000, 10000, 100000]) {
  const pct = p => (100 * hz * trapCycles(p).total / (GHz * 1e9)).toFixed(4) + " %";
  console.log(String(hz).padStart(8) + pct(small).padStart(16) + pct(tick).padStart(12));
}

আসল আউটপুট:

small handler, 5-stage:
   wait for current instr        2 cycles
   flush younger instrs          4 cycles
   fetch at mtvec (miss?)        0 cycles
   save + restore regs          62 cycles
   handler body                100 cycles
   mret + refill                 4 cycles
   total 172 cycles = 86.0 ns; latency to first handler instr = 6 cycles = 3.0 ns
OS timer tick (scheduler work):
   wait for current instr        2 cycles
   flush younger instrs          4 cycles
   fetch at mtvec (miss?)      100 cycles
   save + restore regs          93 cycles
   handler body               3000 cycles
   mret + refill                 4 cycles
   total 3203 cycles = 1601.5 ns; latency to first handler instr = 106 cycles = 53.0 ns

timer Hz   small handler   OS tick   (share of CPU time at 2 GHz)
     100        0.0009 %    0.0160 %
     250        0.0022 %    0.0400 %
    1000        0.0086 %    0.1601 %
   10000        0.0860 %    1.6015 %
  100000        0.8600 %   16.0150 %

ছোট handler-এ ১৭২ cycle (৮৬ ns), যার ১৬২ cycle-ই instruction চালানো — flush মাত্র ৮. OS tick-এ ৩২০৩ cycle (প্রায় ১.৬ µs), যার সিংহভাগ handler-এর ২০০০ instruction × CPI ১.৫ = ৩০০০ cycle. Latency আলাদা জিনিস: ছোট handler-এ ৬ cycle (৩ ns), কিন্তু handler-এর কোড cache-এ না থাকলে ১০৬ cycle (৫৩ ns) — একটি cache miss-ই latency-কে প্রায় ১৮ গুণ করল। Overhead-এর টেবিল বলছে, সাধারণ OS-এর timer হার (১০০ থেকে ১০০০ Hz) প্রায় বিনামূল্যে — ভারী tick-এও ০.১৬%-এর কাছাকাছি। কিন্তু সেকেন্ডে ১,০০,০০০ interrupt হলে ভারী handler CPU-র ১৬% খেয়ে নেয়। দ্রুত network card বা storage যন্ত্রের ক্ষেত্রে ঠিক এই সমস্যাই হয়, তাই interrupt coalescing (কয়েকটি ঘটনা জমিয়ে একটি interrupt), DMA (পর্ব ৫২) আর polling-এর মিশ্রণ ব্যবহার হয়।

আপনার সামনের যন্ত্রে interrupt কতবার আসছে? Linux-এ /proc/interrupts প্রতিটি উৎসের জন্য প্রতিটি CPU-তে কতবার interrupt এসেছে, তা দেখায়। নিচের script শুধু সংখ্যা ছাপে — শেষ কলামের যন্ত্রের নামগুলো বাদ দিয়ে — আর এক সেকেন্ডে কতটা বাড়ল তা মাপে। ফাইলটি container-এ বন্ধ থাকতে পারে, তাই আগে পরীক্ষা করে নেয়:

# Count interrupts from /proc/interrupts -- numbers only, no device names
f=/proc/interrupts
if [ ! -r "$f" ]; then echo "/proc/interrupts পড়া যায় না (container-এ বন্ধ)"; exit 0; fi
snap() {   # one line per interrupt source: label and total over all CPUs (device names are dropped)
    awk 'NR == 1 { ncpu = NF; next }
         $1 ~ /^(HYP|HRE|HVS):$/ { next }   # hypervisor-specific lines are not shown
         { s = 0; for (i = 2; i <= ncpu + 1 && i <= NF; i++) if ($i ~ /^[0-9]+$/) s += $i; print $1, s }' "$f"
}
echo "CPUs listed: $(head -1 "$f" | wc -w)   interrupt lines: $(($(wc -l < "$f") - 1))"
a=$(snap); sleep 1; b=$(snap)
both=$(awk 'NR == FNR { t[$1] = $2; next } { print $1, t[$1], $2 }' <(echo "$a") <(echo "$b"))
echo "-- six biggest counters: label, total since boot, increase in 1 s --"
echo "$both" | sort -k3 -n -r | head -6 | awk '{ printf "%-6s %12d   +%d\n", $1, $3, $3 - $2 }'
for l in LOC: 0:; do
    v=$(echo "$both" | awk -v L="$l" '$1 == L { print $3 - $2 }')
    if [ -n "$v" ]; then echo "$l increase in 1 s (all CPUs): $v"; else echo "$l line নেই"; fi
done

যাচাইয়ের সময় আউটপুট (প্রতিবার সংখ্যা বদলায়):

CPUs listed: 2   interrupt lines: 75
-- six biggest counters: label, total since boot, increase in 1 s --
CAL:     2602241615   +726
RES:     1074739049   +830
TLB:      838932761   +329
58:       444732560   +394
59:       440404720   +221
LOC:      101897779   +5569
LOC: increase in 1 s (all CPUs): 5569
0: increase in 1 s (all CPUs): 0

এই runner-এ ফাইলটি পড়া গেল — container নিজের kernel চালায় না, host-এর kernel ভাগ করে নেয়, তাই এই সংখ্যাগুলো আসলে পুরো যন্ত্রের (একটি cloud সার্ভারের x86-64 CPU, এখানে ২টি CPU দেখা যাচ্ছে), শুধু আমাদের script-এর নয়। ৭৫টি interrupt-উৎস। সবচেয়ে বড় তিনটি কোনো যন্ত্রের নয়, CPU-গুলোর নিজেদের মধ্যে পাঠানো interrupt (IPI): CAL — একটি core আরেকটিকে একটি function চালাতে বলছে, RES — অন্য core-এর scheduler-কে আবার সিদ্ধান্ত নিতে বলার rescheduling ডাক, TLB — page table বদলালে অন্য core-এর TLB মুছতে বলা (TLB shootdown, পর্ব ৫০)। 58: আর 59: সংখ্যাওয়ালা লাইন দুটি যন্ত্রের interrupt (নাম ইচ্ছা করে বাদ দেওয়া)। LOC হলো প্রতিটি core-এর local APIC timer: এক সেকেন্ডে ৫৫৬৯টি, অর্থাৎ CPU-প্রতি প্রায় ২৮০০টি — যেকোনো HZ মানের (সর্বোচ্চ ১০০০) চেয়ে বেশি, কারণ tickless kernel-এ এই গণনায় high-resolution timer-এর জন্য বসানো এককালীন interrupt-ও আছে, আর ভাগ করা যন্ত্রটি তখন ব্যস্ত ছিল। আরেকবার চালানোয় একই লাইন দেখিয়েছিল মাত্র +২৯১ — সংখ্যা প্রতিবার বদলায়। পুরোনো PC-র timer লাইন 0: এক সেকেন্ডে বাড়েনি (০): আধুনিক x86 Linux চালু হওয়ার পর local APIC timer-ই ব্যবহার করে। অংশ ৭-এর “OS tick”-এর মতো প্রতিটি interrupt-এ ৩২০৩ cycle আর ২ GHz ধরে নিলে ৫৫৬৯টি timer interrupt দুই CPU-র মোট সময়ের প্রায় ০.৪৫% — এটি শুধু একটি আনুমানিক হিসাব, এই যন্ত্রের আসল handler-খরচ মাপা হয়নি।

৮ · Trap দিয়ে কী কী বানানো হয়

এত খরচ আর জটিলতা কেন সহ্য করা হয়? কারণ trap হলো hardware আর software-এর মধ্যে একটি সাধারণ “দরজা”, যার ওপর আধুনিক কম্পিউটারের বড় বড় সুবিধা দাঁড়িয়ে আছে:

System call

User প্রোগ্রাম ecall (x86-64-এ syscall, ARM64-এ svc) দিয়ে নিয়ন্ত্রিত পথে kernel-এ ঢোকে। প্রবেশদ্বার একটাই — mtvec/stvec — যা kernel নিজে ঠিক করেছে, তাই user ইচ্ছামতো kernel-এর যেকোনো জায়গায় লাফাতে পারে না।

OS: System call পর্ব

Timer আর scheduling

Timer interrupt না থাকলে একটি অসীম লুপ পুরো কম্পিউটার আটকে রাখত। Timer-ই OS-কে নিয়মিত নিয়ন্ত্রণ ফিরিয়ে দেয় — preemptive multitasking-এর ভিত্তি।

OS: Context switch পর্ব

Breakpoint আর debugger

Debugger কোডের একটি instruction সাময়িকভাবে ebreak (x86-এ ১ বাইটের int3, 0xCC) দিয়ে বদলে দেয়। সেখানে পৌঁছালে trap → OS → debugger. CPU-র hardware breakpoint register-ও আছে (x86-এ DR0–DR3), যা কোড না বদলেই থামায়।

RISC-V debug spec, gdb

Page fault আর demand paging

প্রোগ্রাম শুরুতে পুরো মেমরিতে আসে না; যে page প্রথম ছোঁয়া হয়, তখন page fault → OS page আনে → instruction আবার চলে। Copy-on-write আর memory-mapped file-ও এই কৌশলে।

OS: Virtual memory পর্ব · hardware: পর্ব ৫০

Trap-and-emulate — না-থাকা hardware software-এ

ছোট chip-এ সব extension রাখা ব্যয়বহুল। তখন না-থাকা instruction চালালে illegal instruction trap হয়, আর handler সেই instruction-টি decode করে software-এ কাজটি সেরে mepc-তে ৪ যোগ করে ফেরে। প্রোগ্রামের চোখে instruction-টি “আছে”, শুধু খুব ধীর। RISC-V জগতে পরিচিত উদাহরণ: অনেক chip-এ misaligned load/store hardware-এ নেই, তখন M-mode firmware (যেমন OpenSBI) সেটি অনুকরণ করে — ঠিক উপরের simulator-এর মতো; আবার যেসব chip-এ time CSR সরাসরি পড়ার hardware নেই, সেখানে firmware rdtime অনুকরণ করে বলে জানা যায়। পুরোনো ARM Linux-এ floating point hardware না থাকলে FP instruction kernel-এ অনুকরণ হতো। খরচ বিশাল — একটি load-এর বদলে কয়েকশো cycle — তাই এটি সঠিকতার নিরাপত্তা-জাল, গতির সমাধান নয়। যে কোড বারবার misaligned access করে, তাকে ঠিক করাই সমাধান।

Virtualization — trap-এর ওপর আরেকটি কম্পিউটার

একটি hypervisor অতিথি OS-কে কম ক্ষমতায় চালায়। অতিথি যখনই privileged কোনো instruction চালায় (যেমন CSR লেখা, interrupt বন্ধ করা), trap হয় hypervisor-এ, যে সেটি অতিথির “কাল্পনিক” CPU-র ওপর অনুকরণ করে। ১৯৭৪ সালে Popek আর Goldberg এর শর্ত দিয়েছিলেন: যে সব instruction সিস্টেমের অবস্থা ছোঁয় (sensitive), সেগুলোকে কম ক্ষমতায় চালালে trap হতেই হবে। পুরোনো x86 এই শর্ত পূরণ করত না — কিছু instruction user mode-এ trap না করে চুপচাপ ভিন্ন আচরণ করত — তাই প্রথম দিকের VMware-কে binary translation করতে হয়েছিল, পরে Intel VT-x আর AMD-V hardware সহায়তা দিল। RISC-V শুরু থেকেই trap-and-emulate-বান্ধব ধরে নকশা করা, আর hypervisor (H) extension এর খরচ কমায়। বিস্তারিত Operating Systems কোর্সের Virtual machine আর hypervisor পর্বে।

৯ · AI-এর যুগে — trap-এর প্রশ্নে AI, আর যাচাইয়ের অভ্যাস

Exception আর interrupt নিয়ে AI-এর উত্তর প্রায়ই আত্মবিশ্বাসী, কিন্তু এই বিষয়ে ভুলের একটি পরিচিত ধরন আছে — বিশেষ করে বিভিন্ন ISA আর বইয়ের পরিভাষা আর CSR-এর খুঁটিনাটি মিশিয়ে ফেলা:

AI-এর সাধারণ ভুলকীভাবে ধরবেন
Page fault-কে “interrupt” বলা, বা x86-এর “trap”-কে RISC-V-এর “trap”-এর সমান ধরাকোন ISA-র পরিভাষা, প্রশ্নেই বলে দিন; spec-এর সংজ্ঞা মিলিয়ে নিন
“mepc-তে ecall-এর পরের ঠিকানা থাকে”RISC-V privileged spec: exception-এ দোষী instruction-এর ঠিকানা; ৪ যোগ software-এর কাজ
MIPS-এর EPC/Cause, eret RISC-V-তে চাপানো; বা mret আর sret গুলিয়ে ফেলাকোন mode-এ handler চলছে দেখুন; register-এর নাম manual-এ খুঁজুন
mcause-এর code বা mstatus-এর বিট-অবস্থান ভুল বলাSpec-এর টেবিল দেখুন; simulator (Spike, QEMU) চালিয়ে CSR ছাপুন
Pipeline-এ “যে exception আগে ধরা পড়ে সেটিই আগে নেওয়া হয়”Cycle ধরে diagram আঁকুন; প্রোগ্রাম-ক্রমে পুরোনোটি খুঁজুন
বানানো latency বা cycle সংখ্যা (“Cortex-X-এ interrupt latency ৫ cycle”)Datasheet বা technical reference manual দেখুন; না পেলে “অজানা” ধরুন

AI-কে trap নিয়ে প্রশ্ন করার সময় ISA, mode আর ঘটনার ক্রম স্পষ্ট দিন, আর যাচাইযোগ্য উত্তর চান:

আমি RISC-V privileged architecture (machine mode, শুধু M আর U mode আছে) নিয়ে কাজ করছি। MIPS বা x86-এর পরিভাষা ব্যবহার কোরো না। পরিস্থিতি: U-mode-এ PC = 0x00010008-এ একটি ecall, mstatus = 0x00000008, mtvec = 0x80000101. ১. ecall চলার ঠিক পরে mepc, mcause, mtval, mstatus আর PC-র মান কী হবে — প্রতিটির জন্য spec-এর কোন নিয়ম, তা বলো। ২. mret-এর পরে mstatus আর privilege mode কী হবে? ৩. handler-কে mepc-তে কিছু যোগ করতে হবে কি না, কেন — আর page fault বা timer interrupt হলে কী আলাদা? ৪. কোনো বিট-অবস্থান বা code নিশ্চিত না হলে "নিশ্চিত নই" লেখো, অনুমান কোরো না।

AI-এর দেওয়া trap-ব্যাখ্যা হাতে পেলে — চেকলিস্ট

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

  • Exception আর interrupt-এর পার্থক্য? — Exception synchronous, একটি instruction-এর কারণে; interrupt asynchronous, বাইরের ঘটনা থেকে; trap দুটোরই নিয়ন্ত্রণ-বদল।
  • Precise exception কী, আর pipeline-এ কীভাবে নিশ্চিত করা হয়? — আগের সব শেষ, দোষী আর পরের কোনোটির ছাপ নেই; exception লিখে রেখে commit point-এ প্রোগ্রাম-ক্রমে নেওয়া, পরেরগুলো flush.
  • ecall-এর পর handler mepc-তে ৪ যোগ করে কেন, page fault-এ করে না কেন? — ecall আবার চালানো যাবে না; page fault-এ একই instruction আবার চলে সফল হবে।
  • Interrupt latency-র অংশ? — চিনতে পারা, instruction-সীমা পর্যন্ত অপেক্ষা, flush, vector fetch, register সেভ, আর interrupt বন্ধ থাকার সময়।
  • Vectored interrupt-এ timer (cause ৭)-এর ঠিকানা, mtvec = 0x80000001 হলে? — 0x80000000 + ২৮ = 0x8000001c.

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

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

প্র ০১দ্রুততম মেশিনগুলো একসময় imprecise exception মেনে নিয়েছিল। তাহলে precise exception শেষ পর্যন্ত জিতল কীভাবে?

১৯৬০-এর দশকে বিজ্ঞানের হিসাবের জন্য যন্ত্রের গতি ছিল প্রধান লক্ষ্য। IBM System/360 Model 91-এর floating point unit-এ Robert Tomasulo-র বিখ্যাত পদ্ধতি instruction-কে ক্রমের বাইরে চালাত (পর্ব ৪০), আর ফল আসত যেটি আগে তৈরি হয় সেই ক্রমে। Exception হলে যন্ত্র জানাতে পারত শুধু একটি এলাকা — “এর আশপাশে কোথাও” — যাকে IBM-এর পরিভাষায় imprecise interrupt বলা হতো বলে উল্লেখ পাওয়া যায়। বৈজ্ঞানিক প্রোগ্রামে এটি সহনীয় ছিল: floating point-এর overflow হলে সাধারণত প্রোগ্রাম থামিয়ে দেওয়াই যথেষ্ট, সেখান থেকে নিখুঁতভাবে আবার শুরু করার দরকার নেই।

পরিস্থিতি বদলাল virtual memory আর multiprogramming. Demand paging-এ page fault কোনো “ভুল” নয়, প্রতিদিনের স্বাভাবিক ঘটনা — একটি প্রোগ্রাম সেকেন্ডে শত বা হাজারবার fault করতে পারে, আর প্রতিবার দোষী instruction-কে নিখুঁতভাবে আবার শুরু করতে হয়। Imprecise হলে কোনো instruction দুবার চলবে (যেমন একটি যোগ দুবার), কোনোটি একবারও না। একইভাবে time-sharing-এ যেকোনো মুহূর্তে প্রসেস থামিয়ে পরে চালাতে পরিষ্কার একটি থামার বিন্দু চাই। ফলে integer আর মেমরির exception-কে precise রাখা কার্যত বাধ্যতামূলক হয়ে গেল।

কিন্তু pipeline আর out-of-order যুগে precise রাখার খরচ? এর প্রকৌশল-সমাধান আসে ১৯৮৫ সালে James Smith আর Andrew Pleszkun-এর একটি গবেষণাপত্রে, যেখানে pipeline-এ precise interrupt বাস্তবায়নের কয়েকটি উপায় তুলনা করা হয় — এর মধ্যে reorder buffer, history buffer আর future file. ধারণাটা সরল: instruction যেকোনো ক্রমে চলুক, কিন্তু architectural অবস্থা বদলাক শুধু প্রোগ্রাম-ক্রমে। ১৯৯০-এর দশকের out-of-order CPU-গুলো (Intel-এর P6, MIPS R10000 ইত্যাদি) এই ধারণাকে বাণিজ্যিক করে। আজ শত শত instruction ক্রমের বাইরে উড়ে বেড়ায়, তবু একটি page fault হলে CPU নিখুঁতভাবে জানায় কোন instruction.

Floating point-এ ইতিহাস ভিন্ন পথ নিল। IEEE 754 (১৯৮৫) ডিফল্ট ফল আর sticky flag-এর ধারণা দিল, যাতে trap ছাড়াই চলা যায়। RISC-V পুরোপুরি সেই পথে গেছে — FP-তে trap-ই নেই, তাই precise-imprecise প্রশ্নই ওঠে না।

মূল উপলব্ধি: Precise exception জিতেছে কারণ virtual memory আর multitasking-এর মতো মৌলিক software ধারণা এর ওপর নির্ভর করে; hardware-কে তাই বাড়তি জটিলতা নিয়েও এই প্রতিশ্রুতি রাখতে হয়।

প্র ০২x86 কেন exception-কে fault, trap আর abort — তিন ভাগে ভাগ করে? RISC-V-এর একটি mepc-নিয়মের সাথে এর তুলনা কী?

x86-এ handler-কে জানতে হয় ফেরার ঠিকানা কোথায়, আর সেই ঠিকানা exception-এর ধরন অনুযায়ী আলাদা। Intel-এর manual তিন দল করে। Fault: stack-এ রাখা ঠিকানা দোষী instruction-এরই, আর instruction-এর আগের অবস্থা ফিরিয়ে দেওয়া হয়, তাই handler সমস্যা মিটিয়ে ফিরলে instruction আবার চলে। Page fault (#PF), general protection (#GP), invalid opcode (#UD), divide error (#DE) — সবই fault. Trap: instruction শেষ হওয়ার পরে জানানো হয়, ঠিকানা পরের instruction-এর। int3 breakpoint আর into (overflow) এই দলের, তাই debugger-কে breakpoint-এ ফিরে এসে নিজেই ঠিকানা এক বাইট পিছিয়ে নিতে হয়। Abort: এত গুরুতর যে নির্ভরযোগ্যভাবে ফেরা সম্ভব নয় — যেমন machine check (hardware ত্রুটি) বা double fault, যা একটি exception সামলাতে গিয়ে আরেকটি হলে ঘটে। Double fault-এর handler ডাকতে গিয়েও আরেকটি exception হলে “triple fault”, আর তখন CPU সাধারণত reset হয়ে যায়।

RISC-V একই প্রশ্নের উত্তর দিয়েছে সরলভাবে: সব exception-এ mepc = দোষী instruction-এর ঠিকানা — ecall আর ebreak-এও। কোনটি আবার চালাতে হবে আর কোনটি পার হতে হবে, সেই সিদ্ধান্ত পুরোপুরি software-এর: system call-এ ৪ যোগ, page fault-এ নয়। Hardware-এর নিয়ম একটি, ব্যতিক্রমের তালিকা নেই। এর একটি সুবিধা হলো debugger: ebreak-এর ঠিকানা সরাসরি mepc-তে, পিছিয়ে নেওয়ার হিসাব লাগে না।

x86-এর বিভাজনের পেছনে আছে ইতিহাস — ১৯৭৮ সালের 8086 থেকে ধাপে ধাপে protected mode, paging, ৬৪-বিট যোগ হয়েছে, আর প্রতিবার পুরোনো আচরণ ধরে রাখতে হয়েছে। x86-এর trap-প্রবেশও ভারী: CPU নিজেই stack-এ RIP, CS, RFLAGS, RSP, SS (আর কিছু exception-এ একটি error code) push করে, IDT-র ঘর থেকে handler খোঁজে, আর iret-এ ফেরে। System call-এর জন্য আলাদা দ্রুত পথ syscall/sysret আছে, যা stack-এ না লিখে register-এ ঠিকানা রাখে — RISC-V-এর CSR-পদ্ধতির কাছাকাছি। ARM64-এ ELR_ELx (ফেরার ঠিকানা), ESR_ELx (কারণ), FAR_ELx (খারাপ ঠিকানা) আর eret — নাম আলাদা, ধারণা RISC-V-এর মতোই।

মূল উপলব্ধি: ফেরার ঠিকানার নিয়ম ISA-র চুক্তির অংশ; RISC-V hardware-কে সরল রেখে সিদ্ধান্ত software-কে দিয়েছে, x86 ইতিহাসের দায়ে hardware-এ ধরনভেদ রেখেছে।

প্র ০৩Interrupt না polling? দ্রুত network-এ interrupt কেন উল্টো CPU-কে অচল করে দিতে পারে?

যন্ত্রের খবর নেওয়ার দুই উপায়। Polling: CPU বারবার যন্ত্রের status register পড়ে দেখে কাজ আছে কি না — রেলস্টেশনে বারবার গিয়ে দেখে আসার মতো ট্রেন এলো কি না। Interrupt: যন্ত্র নিজে ঘণ্টা বাজায়, CPU ততক্ষণ অন্য কাজ করে। কম হারের ঘটনায় (কি-বোর্ড, মাঝেমধ্যে আসা packet) interrupt নিশ্চিতভাবে ভালো — polling-এ অধিকাংশ পড়াই বৃথা।

কিন্তু হার বাড়লে হিসাব উল্টে যায়। অংশ ৭-এর calculator মনে করুন: প্রতি interrupt-এ কয়েক হাজার cycle হলে সেকেন্ডে লাখ লাখ interrupt CPU-র বড় অংশ খেয়ে ফেলে। ১৯৯০-এর দশকে Jeffrey Mogul আর K. K. Ramakrishnan একটি পরিচিত সমস্যা বিশ্লেষণ করেন, যার নাম “receive livelock”: packet এত দ্রুত আসে যে CPU-র পুরো সময় যায় interrupt handler-এ packet তুলে রাখতে, আর যে প্রোগ্রাম সেই packet ব্যবহার করবে সে কখনো চলারই সুযোগ পায় না। ফলে বেশি packet এলে কাজ হয় কম — throughput শূন্যের দিকে নামে, যদিও CPU একশো শতাংশ ব্যস্ত। Interrupt-কে priority-তে সাধারণ কাজের ওপরে রাখা হয় বলেই এই ফাঁদ।

সমাধান হলো মিশ্রণ। Linux-এর network stack-এ NAPI পদ্ধতিতে প্রথম packet-এ interrupt আসে, তারপর driver সেই interrupt বন্ধ করে একটি নির্দিষ্ট বাজেট পর্যন্ত polling করে; চাপ কমলে আবার interrupt চালু। Network card নিজেও interrupt coalescing করে — কয়েকটি packet বা কয়েক মাইক্রোসেকেন্ড জমিয়ে একটি interrupt. আর সবচেয়ে উচ্চ-গতির ক্ষেত্রে (DPDK-র মতো framework, দ্রুত NVMe storage-এর কিছু ব্যবস্থা) একটি পুরো core-কে শুধু polling-এর জন্য রেখে দেওয়া হয় — interrupt-এর latency আর খরচ পুরোপুরি এড়াতে।

এই trade-off সরাসরি latency বনাম throughput. Coalescing interrupt-এর সংখ্যা কমায়, কিন্তু প্রথম packet-কে অপেক্ষা করায়; polling latency কমায়, কিন্তু একটি core-এর পুরো শক্তি খরচ করে। Data center-এ এই সমন্বয় আজও একটি সক্রিয় প্রকৌশল-প্রশ্ন।

মূল উপলব্ধি: Interrupt কম হারের ঘটনার জন্য সেরা, polling বেশি হারের জন্য; বাস্তব সিস্টেম চাপ বুঝে দুটির মধ্যে বদলায়।

প্র ০৪Timer interrupt সেকেন্ডে কতবার হওয়া উচিত? Linux-এর HZ আর “tickless” kernel-এর গল্প কী শেখায়?

Timer interrupt-এর হার একটি টানাপোড়েন। বেশি হার মানে OS ঘন ঘন নিয়ন্ত্রণ পায় — scheduler দ্রুত সাড়া দেয়, ছোট sleep আরও নিখুঁত হয়, interactive প্রোগ্রাম মসৃণ লাগে। কম হার মানে overhead কম আর CPU বেশিক্ষণ গভীর ঘুমে থাকতে পারে — ব্যাটারির জন্য গুরুত্বপূর্ণ।

Linux-এ এই হারের নাম HZ. দীর্ঘদিন x86-এ HZ ছিল ১০০; ২০০৩-০৪ সালের দিকে 2.6 kernel-এর সাথে ডিফল্ট ১০০০ করা হয়, পরে ডিফল্ট আবার ২৫০-এ নামানো হয় বলে জানা যায়; আজ build-এর সময় ১০০, ২৫০, ৩০০ বা ১০০০ বেছে নেওয়া যায়, আর distribution-ভেদে এটি আলাদা। অংশ ৭-এর হিসাব বলছে overhead-এর দিক থেকে ১০০০ Hz-ও প্রায় বিনামূল্যে (ভারী tick-এও ০.১৬%-এর মতো)। তাহলে সমস্যা কোথায়? শক্তিতে। প্রতি tick-এ CPU ঘুম ভেঙে জাগে, cache আর power-state-এর খরচ দেয় — অলস ল্যাপটপে সেকেন্ডে হাজারবার অকারণে জাগা ব্যাটারি খেয়ে ফেলে। আবার virtual machine-এ শত শত অলস অতিথির প্রত্যেকের tick হোস্টের ওপর চাপ বাড়ায়।

উত্তর এলো “tickless” ধারণায়। ২০০৭ সালের দিকে Linux 2.6.21-এ “dynamic tick” (NO_HZ) যোগ হয়: CPU অলস হলে পর্যায়ক্রমিক tick বন্ধ, পরের যে কাজের সময় জানা আছে, timer সরাসরি সেই মুহূর্তে বসানো হয় — অংশ ৬-এর stepper-এ handler যেমন mtimecmp-এ পরের সময় লিখল। পরে (২০১৩ সালের দিকে) এমন ব্যবস্থাও এলো, যেখানে একটিমাত্র কাজ চলা core-এ চলন্ত অবস্থায়ও tick প্রায় বন্ধ রাখা যায় — real-time আর high-performance computing-এ jitter কমাতে।

এর ফলে /proc/interrupts-এর timer-সংখ্যা আর “HZ” সরাসরি মেলে না: অংশ ৭-এর script-এ যে local timer-গণনা দেখলেন, তাতে পর্যায়ক্রমিক tick ছাড়াও high-resolution timer-এর জন্য বসানো এককালীন interrupt-ও আছে। আর RISC-V-এ timer-এর কাঠামোও বদলেছে: শুরুতে S-mode-এর OS-কে timer বসাতে firmware-এর কাছে SBI call করতে হতো (আরেকটি trap!), পরে Sstc extension S-mode-কে নিজের তুলনা-register দিয়েছে বলে জানা যায় — একটি trap-এর দাম বাঁচাতে।

মূল উপলব্ধি: Interrupt-এর আসল খরচ সবসময় cycle-এ নয়; শক্তি, ঘুম ভাঙা আর jitter-ও খরচ — তাই আধুনিক OS “প্রয়োজন হলে তবেই interrupt” নীতিতে চলে।

প্র ০৫Virtual machine চালাতে trap কেন অপরিহার্য, আর পুরোনো x86 কেন “virtualize করা যায় না” বলে পরিচিত ছিল?

একটি hypervisor-এর লক্ষ্য: অতিথি OS ভাববে সে পুরো যন্ত্রের মালিক, অথচ আসলে সে কম ক্ষমতায় চলছে। এর সবচেয়ে সরল আর দ্রুত উপায় trap-and-emulate. অতিথির সাধারণ instruction (যোগ, load, branch) সরাসরি hardware-এ পূর্ণ গতিতে চলে। কিন্তু অতিথি kernel যখন privileged কিছু করে — interrupt বন্ধ করা, page table-এর ঠিকানা বদলানো, CSR লেখা — তখন CPU trap করে hypervisor-এ, কারণ অতিথি আসলে কম ক্ষমতায়। Hypervisor অতিথির জন্য রাখা একটি “ছায়া” CPU-অবস্থায় কাজটি অনুকরণ করে ফিরে যায়।

১৯৭৪ সালে Gerald Popek আর Robert Goldberg এর একটি আনুষ্ঠানিক শর্ত দেন: যে instruction যন্ত্রের নিয়ন্ত্রণ-অবস্থা পড়ে বা বদলায় (sensitive), সেগুলো যদি সবই privileged হয় — অর্থাৎ কম ক্ষমতায় চালালে trap হয় — তবে trap-and-emulate দিয়ে কার্যকর VMM বানানো যায়। ১৯৬০-৭০-এর IBM mainframe এই শর্ত মোটামুটি মানত, আর IBM-এর VM/370 এর ওপরই দাঁড়িয়ে ছিল।

পুরোনো x86 মানত না। কিছু sensitive instruction user mode-এ trap না করে চুপচাপ ভিন্ন ফল দিত। পরিচিত উদাহরণ popf: kernel mode-এ এটি interrupt-flag বদলায়, কিন্তু কম ক্ষমতায় সেই বিটের বদল নিঃশব্দে উপেক্ষা করে — কোনো trap নেই, তাই hypervisor জানতেই পারে না অতিথি interrupt বন্ধ করতে চেয়েছিল। ২০০০ সালের একটি বিশ্লেষণে এমন প্রায় ১৭টি instruction-এর তালিকা করা হয়েছিল বলে প্রায়ই উদ্ধৃত হয়। ১৯৯০-এর দশকের শেষে VMware এর সমাধান করল binary translation দিয়ে: অতিথি kernel-এর কোড চালানোর আগে পড়ে সমস্যাজনক instruction-গুলো নিরাপদ বিকল্পে বদলে দেওয়া। পরে Xen-এর paravirtualization অতিথি OS-কেই বদলে hypervisor-কে সরাসরি ডাকতে শেখাল। ২০০৫-০৬ সালে Intel VT-x আর AMD-V hardware-এ নতুন একটি মোড যোগ করল, যেখানে sensitive instruction নিশ্চিতভাবে hypervisor-এ “VM exit” ঘটায়।

কিন্তু প্রতিটি VM exit একটি দামি trap — কয়েকশো থেকে হাজারের বেশি cycle. তাই পরের প্রজন্মের hardware-এর লক্ষ্য হলো exit-ই কমানো: দ্বিতীয় স্তরের page table (Intel EPT, AMD NPT), interrupt সরাসরি অতিথিতে পাঠানো (posted interrupt). RISC-V-এর H extension একই দর্শনে অতিথির জন্য আলাদা CSR-সেট (vsstatus, vsepc ইত্যাদি) দেয়, যাতে অতিথির অনেক trap hypervisor না ঘুরেই অতিথির নিজের handler-এ যায়।

মূল উপলব্ধি: Virtualization-এর মূল চাবি হলো “প্রতিটি sensitive কাজ trap করবে” — ISA-র এই একটি বৈশিষ্ট্য ঠিক না থাকলে software-কে বিশাল জটিলতায় তা পূরণ করতে হয়।

অনুশীলন

পরীক্ষার ধাঁচে: শ্রেণিবিভাগ, pipeline-এ কোন exception আগে, overhead-এর হিসাব আর handler-এর ভুল ধরা। আগে নিজে চেষ্টা করুন।

  1. শ্রেণিবিভাগ: RISC-V-এর অর্থে প্রতিটি ঘটনা synchronous exception, asynchronous interrupt, না কোনো trap-ই নয়, বলুন। Trap হলে mcause আর mepc-তে কী থাকবে? (ক) U-mode-এ 0x2000-এর lw, যার ঠিকানার page মেমরিতে নেই (virtual memory চালু); (খ) U-mode-এ 0x2004-এর ecall; (গ) কি-বোর্ডে চাপ, যা PLIC হয়ে machine external interrupt দেয়, তখন 0x2008-এর instruction চলার অপেক্ষায়; (ঘ) RV32I-এ add-এর overflow; (ঙ) RV32M-এ div দিয়ে শূন্যে ভাগ; (চ) 0x200c-এ debugger-এর বসানো ebreak.

    (ক) synchronous exception — load page fault, mcause = ১৩, mepc = 0x2000, mtval = খারাপ ঠিকানা; OS page আনবে, mepc-তে কিছু যোগ নয়। (খ) synchronous exception — ecall from U, mcause = ৮, mepc = 0x2004 (handler ৪ যোগ করবে)। (গ) asynchronous interrupt — machine external, RV32-এ mcause = 0x8000000b (interrupt বিট + ১১), mepc = 0x2008, অর্থাৎ যে instruction এখনো চলেনি। (Linux-এর মতো সিস্টেমে PLIC সাধারণত S-mode-এর জন্য আলাদা supervisor external interrupt তোলে, যা S-mode-এ delegate করা থাকে; তখন scause = 0x80000009.) (ঘ) কোনো trap নয় — RISC-V-এ add wrap করে (পর্ব ২০)। (ঙ) কোনো trap নয় — ফল নির্দিষ্ট, quotient-এর সব বিট ১, অর্থাৎ −১ (পর্ব ২২)। (চ) synchronous exception — breakpoint, mcause = ৩, mepc = 0x200c.

  2. কোন exception আগে (হাতে trace): একটি পাঁচ ধাপের in-order pipeline (IF ID EX MEM WB), Ik cycle k-তে IF-এ ঢোকে, exception নেওয়া হয় MEM-এর শেষে। I3 (PC 0x108) একটি sw, যার ঠিকানা misaligned — MEM-এ ধরা পড়ে। I5 (PC 0x110)-এর opcode অবৈধ — ID-তে ধরা পড়ে। (ক) কোন cycle-এ কোনটি ধরা পড়ে? (খ) কোনটি trap হয়, mepc, mcause কী? কোন instruction-গুলো flush হয়? (গ) I3-এ যদি কোনো সমস্যা না থাকত, তাহলে কোন cycle-এর শেষে কোন trap হতো, আর handler-এর প্রথম fetch কোন cycle-এ?

    (ক) I3-এর MEM = cycle ৩ + ৩ = ৬; I5-এর ID = cycle ৫ + ১ = ৬. দুটিই একই cycle-এ ধরা পড়ে।

    (খ) একই cycle হলেও প্রোগ্রাম-ক্রমে I3 পুরোনো, আর সে-ই commit point-এ (MEM-এর শেষ) আছে — trap নেয় I3: mepc = 0x108, mcause = ৬ (store address misaligned), mtval = খারাপ ঠিকানা। Flush: I3 (তার store মেমরিতে লেখা হবে না), I4 (EX), I5 (ID — তার illegal-এর খবর মুছে যায়), I6 (IF). I1 আর I2 শেষ হয় (I2 cycle ৬-এ WB-তে)।

    (গ) তখন I5 নিজেই commit point-এ পৌঁছে trap নিত: I5-এর MEM = cycle ৫ + ৩ = ৮, তাই cycle ৮-এর শেষে trap, mcause = ২, mepc = 0x110. I3 আর I4 শেষ হতো (I4-এর WB cycle ৮-এ), I5, I6, I7, I8 flush. Handler-এর প্রথম fetch cycle ৯-এ। লক্ষ করুন, ID-তে cycle ৬-এই জানা ছিল, তবু precise রাখতে দুই cycle অপেক্ষা।

  3. Overhead-এর হিসাব: ২.৫ GHz-এর একটি CPU-তে network card সেকেন্ডে ৫০,০০০ interrupt দেয়। প্রতিটিতে: প্রবেশে ১০ cycle flush, ফেরায় ১০ cycle, সেভ-ফেরত ৭০টি instruction, handler ৮০০টি instruction, এই কোডের CPI ১.৫. (ক) প্রতি interrupt-এ কত cycle, CPU-র কত শতাংশ? (খ) Interrupt coalescing চালু করলে প্রতি ৮টি packet-এ একটি interrupt আসে (সেকেন্ডে ৬,২৫০টি), কিন্তু handler বড় হয়ে ৩০০০ instruction. এখন overhead কত?

    (ক) ১০ + ১০ + (৭০ + ৮০০) × ১.৫ = ২০ + ১৩০৫ = ১৩২৫ cycle. প্রতি সেকেন্ডে ৫০,০০০ × ১৩২৫ = ৬,৬২,৫০,০০০ cycle; ২.৫ GHz = ২৫০ কোটি cycle/সেকেন্ড, তাই ৬,৬২,৫০,০০০ ÷ ২,৫০,০০,০০,০০০ = ২.৬৫%.

    (খ) ২০ + (৭০ + ৩০০০) × ১.৫ = ২০ + ৪৬০৫ = ৪৬২৫ cycle. ৬,২৫০ × ৪৬২৫ = ২,৮৯,০৬,২৫০ cycle/সেকেন্ড → প্রায় ১.১৬% (১.১৫৬২৫%). Overhead অর্ধেকের বেশি কমল, কারণ প্রতি interrupt-এর নির্দিষ্ট খরচ (flush, সেভ-ফেরত আর handler-এর শুরু-শেষের কাজ) এখন ৮টি packet-এ ভাগ হচ্ছে — packet-প্রতি handler ৮০০ থেকে নেমে ৩০০০ ÷ ৮ = ৩৭৫ instruction. দাম: প্রথম packet-কে বাকিগুলোর জন্য অপেক্ষা করতে হতে পারে — latency বাড়ে।

  4. আউটপুট অনুমান আর কোড ঠিক করা: একজন শিক্ষার্থীর handler-এ ecall-এর পথে csrr t0, mepc / addi t0, t0, 4 / csrw mepc, t0 — এই তিন লাইন নেই। (ক) U-mode প্রোগ্রাম li a0, 1, li a7, 64, ecall চালালে কী ঘটবে? (খ) শুরুতে mstatus = 0x00000008 আর trap আসছে U-mode থেকে — handler-এর ভেতরে আর mret-এর পরে mstatus কত? (গ) একই trap যদি M-mode থেকে আসত (MIE = ১), handler-এর ভেতরে mstatus কত হতো?

    (ক) mret ফেরে mepc-তে, যা ecall-এরই ঠিকানা। তাই ecall আবার চলে, আবার trap, আবার সেবা — অসীম লুপ। এখানে সেবাটি write, তাই একই লেখা বারবার ছাপা হতে থাকবে; প্রোগ্রাম কখনো এগোবে না। (C extension থাকলেও ecall সবসময় ৪ বাইটের, তাই +৪-ই ঠিক।) ঠিক করার কোড, ফল সেভ করার পরে আর restore-এর আগে:

    csrr  t0, mepc
    addi  t0, t0, 4
    csrw  mepc, t0

    (খ) প্রবেশে MPIE ← MIE = ১, MIE ← ০, MPP ← U = ০০: mstatus = 0x00000080. mret-এ MIE ← MPIE = ১, MPIE ← ১, MPP ← U: 0x00000088, mode U. (গ) M-mode থেকে এলে MPP = ১১ (বিট ১২–১১), তাই 0x1800 + 0x80 = 0x00001880; mret-এর পরে mode M-এই থাকে আর mstatus আবার 0x00000088 (MPP নেমে U). এই মানগুলো একটি ছোট script দিয়ে বিট ধরে মিলিয়ে দেখা হয়েছে।

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

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

ক্রমRISC-V-এ একটি exception থেকে ফেরা পর্যন্ত ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ⚠️ Instruction commit point-এ exception দেখায়; আগেরগুলো শেষ, পরেরগুলো flush
  2. 🔒 Hardware, এক ধাপে: mepc, mcause, mtval লেখা; MPIE ← MIE, MIE ← 0, MPP ← আগের mode; mode ← M; PC ← mtvec
  3. 🧰 Handler: csrrw sp, mscratch, sp দিয়ে নিজের stack, তারপর register সেভ
  4. 🔍 Handler: mcause দেখে কারণ সামলানো; ecall হলে mepc += 4
  5. 📦 Handler: সেভ করা register আর user-এর sp ফেরত
  6. ↩️ mret: PC ← mepc, MIE ← MPIE, mode ← MPP

RISC-V-এর অর্থে কোনটি asynchronous interrupt?

  • একটি load-এর page fault
  • User প্রোগ্রামের ecall
  • Timer-এর সময় পূর্ণ হয়ে machine timer interrupt
  • Debugger-এর বসানো ebreak

U-mode-এ ঠিকানা 0x400-এর একটি ecall M-mode-এ trap করল। Hardware mepc-তে কী লেখে, আর handler সাধারণত mret-এর আগে কী করে?

  • mepc = 0x404; handler কিছু বদলায় না
  • mepc = 0x400; handler mepc-তে ৪ যোগ করে 0x404 করে
  • mepc = 0x3fc; handler ৮ যোগ করে
  • mepc বদলায় না; handler ra দিয়ে ফেরে

একটি RV32 CPU-তে trap-এর পর mcause = 0x80000007. এর অর্থ কী?

  • Machine timer interrupt
  • Store access fault
  • Machine external interrupt
  • M-mode থেকে ecall

একটি পাঁচ ধাপের in-order pipeline-এ একই cycle-এ I2 MEM-এ load page fault দেখায় আর I4 ID-তে illegal opcode দেখায়। Precise exception রাখতে CPU কী করবে?

  • I4-এর illegal instruction নেবে, কারণ সেটি decode-এ
  • দুটিই একসাথে mcause-এ জানাবে
  • I2-কে শেষ করতে দিয়ে I4-এর exception নেবে
  • I2-এর page fault নেবে, I2 আর পরের সব flush; I4-এর exception পরে আবার দেখা দেবে

mtvec = 0x80000101 (vectored mode). Machine external interrupt (cause ১১) এলে PC কোথায় যাবে?

  • 0x80000101
  • 0x8000012c
  • 0x80000100
  • 0x80000158

২ GHz-এর একটি CPU-তে timer interrupt সেকেন্ডে ১০০০ বার আসে, আর প্রতিটির মোট খরচ ২০০০ cycle. Timer interrupt CPU-র কত শতাংশ সময় নেয়?

  • ১%
  • ০.০১%
  • ০.১%
  • ২%

শব্দকোষ

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

মনে রাখুন

  1. Exception = synchronous (instruction-এর কারণে), interrupt = asynchronous (বাইরের), trap = দুটোর নিয়ন্ত্রণ-বদল — RISC-V-এর অর্থে; x86 আর ARM-এর শব্দ আলাদা।
  2. Trap-এ hardware: mepc, mcause (উঁচু বিট = interrupt), mtval, MPIE ← MIE, MIE ← 0, MPP ← mode, PC ← mtvec (vectored-এ interrupt BASE + ৪ × cause); ফেরা mret.
  3. Handler: mscratch দিয়ে stack, register সেভ, কারণ সামলানো, ecall-এ mepc + ৪ (page fault আর interrupt-এ নয়), ফেরত, mret.
  4. Pipeline-এ exception লিখে রাখা হয়, নেওয়া হয় commit point-এ, প্রোগ্রাম-ক্রমে; দোষী আর পরেরগুলো flush — এটাই precise, যার ওপর virtual memory আর debugging দাঁড়িয়ে।
  5. Interrupt নেওয়া হয় instruction-এর সীমায়, তিন তালা (pending, enable, global MIE) খোলা থাকলে; খরচ = flush + সেভ-ফেরত + handler × CPI; overhead = হার × cycle ÷ clock rate.

পরের পর্বে: মডিউল ৬-এ একটি পূর্ণ pipeline-এর নকশা শেষ — hazard, prediction আর exception সহ। এবার প্রশ্ন: একটির বদলে প্রতি cycle-এ দুটি, চারটি instruction শুরু করা যায় কি? Instruction-level parallelism, superscalar আর VLIW.

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

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