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

Interrupt আর DMA — hardware-এর দিক থেকে

Interrupts and DMA from the Device Side — Polling vs Interrupt Cost, Coalescing, IRQ Lines, PLIC Claim/Complete, MSI-X, Bus Mastering, Descriptor Rings, DMA Coherence and the IOMMU

Taught in Bangla, this lesson covers interrupts and DMA from the device side: polling versus interrupt cost and the crossover rate, coalescing, IRQ lines, level and edge triggering, interrupt controllers and RISC-V PLIC claim/complete, MSI and MSI-X, programmed I/O versus DMA, bus mastering, descriptor rings, scatter-gather, DMA cache coherence, pinned pages and the IOMMU, and a network packet's journey end to end, with simulators.

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

  • Polling, interrupt আর coalesced interrupt-এর CPU-খরচ ও latency হিসাব করা, আর কোন হারে কোনটি জেতে (crossover) বের করা
  • IRQ লাইন, level বনাম edge triggering, interrupt controller-এর কাজ, PLIC-এর claim/complete আর MSI/MSI-X
  • Programmed I/O বনাম DMA-র খরচ, bus mastering আর একটি DMA transfer-এর ধাপগুলো
  • Descriptor ring-এর head/tail, full/empty শর্ত আর scatter-gather
  • DMA-র সময় cache coherence, pinned page, IOMMU আর DMA আক্রমণ থেকে সুরক্ষা — আর একটি network packet-এর পুরো যাত্রা

১ · যন্ত্রের খবর কীভাবে নেবেন — polling বনাম interrupt, আর খরচের হিসাব

পর্ব ৫১-এ (I/O আর bus) দেখেছেন, CPU যন্ত্রের সাথে কথা বলে memory-mapped I/O register দিয়ে, আর সবচেয়ে সরল উপায়ে বারবার একটি status register পড়ে জানতে চায় “কাজ হলো?” — এর নাম polling. পর্ব ৩৮-এ (Exception আর interrupt) দেখেছেন CPU-র ভেতরের দিক: mcause, mepc, mtvec, vectored mode, nesting আর interrupt latency. সেগুলো এখানে আর বলব না। এই পর্বের প্রশ্ন যন্ত্রের দিক থেকে: যন্ত্র কীভাবে CPU-কে ডাকে, ডাকটা কোন পথে আসে, আর বিশাল ডেটা CPU-কে না খাটিয়ে কীভাবে মেমরিতে পৌঁছায়।

চায়ের দোকানে কেটলির কথা ভাবুন। এক দোকানি প্রতি আধা মিনিটে ঢাকনা তুলে দেখেন পানি ফুটল কি না — এটি polling. পানি না ফুটলেও প্রতিবার দেখার খরচ আছে। আরেক দোকানির কেটলিতে বাঁশি লাগানো; ফুটলে নিজেই বাজে — এটি interrupt. কিন্তু যদি দোকানে একসাথে চল্লিশটি কেটলি থাকে আর প্রতি সেকেন্ডে কোনো না কোনোটির বাঁশি বাজে? তখন প্রতিটি বাঁশিতে কাজ থামিয়ে দৌড়ানোর চেয়ে নিয়মিত এক চক্কর দিয়ে সবগুলো একসাথে দেখাই সস্তা। ঘটনার হার কম না বেশি — এটাই সিদ্ধান্তের চাবি।

খরচের মডেল

তিনটি খরচ ধরি, সবই CPU cycle-এ: প্রতিবার poll করার খরচ Cpoll (একটি uncached MMIO read আর লুপ), প্রতিটি interrupt-এর বাড়তি খরচ Cirq (পর্ব ৩৮-এর flush, register সেভ-ফেরত, controller-কে জানানো, cache-এর ক্ষতি), আর প্রতিটি ঘটনার আসল কাজ Cwork — যেটা যেকোনো পদ্ধতিতেই করতে হবে। যন্ত্রের ঘটনার হার r (প্রতি সেকেন্ডে কতবার), poll করা হয় প্রতি P সেকেন্ডে একবার, clock rate f.

সূত্র

Polling-এর CPU ভাগ = (Cpoll ÷ P + r × Cwork) ÷ f  ·  Interrupt-এর CPU ভাগ = r × (Cirq + Cwork) ÷ f
দুটি সমান হয় যখন r × Cirq = Cpoll ÷ P, অর্থাৎ crossover হার r* = Cpoll ÷ (P × Cirq). r < r* হলে interrupt সস্তা, r > r* হলে polling.
গড় latency: polling-এ প্রায় P ÷ ২ (সবচেয়ে খারাপ P); interrupt-এ শুধু interrupt latency.

যুক্তিটা সরল: Cwork দুই দিকেই একই, তাই তুলনায় বাদ যায়। Polling দেয় একটি স্থির কর — ঘটনা হোক বা না হোক, প্রতি সেকেন্ডে 1/P বার দেখতেই হবে। Interrupt দেয় ঘটনাপ্রতি কর — ঘটনা যত বেশি, খরচ তত বেশি। স্থির কর আর ঘটনাপ্রতি করের রেখা যেখানে মেলে, সেটাই r*.

উদাহরণ (হাতে): ২ GHz CPU. প্রতি ২০ µs-এ poll, অর্থাৎ সেকেন্ডে ৫০,০০০ বার, প্রতিটি ৪০০ cycle — মোট ২ কোটি cycle, CPU-র ১%, ঘটনা থাকুক বা না থাকুক। Interrupt-এর বাড়তি খরচ ৩০০০ cycle, আসল কাজ ১০০০ cycle. তাহলে r* = ৪০০ ÷ (২০ µs × ৩০০০) = ২ কোটি ÷ ৩০০০ ≈ সেকেন্ডে ৬,৬৬৭টি ঘটনা. কি-বোর্ড (সেকেন্ডে কয়েকটি চাপ) এর অনেক নিচে — interrupt-ই ঠিক। কিন্তু একটি network card সেকেন্ডে ৩,০০,০০০ packet দিলে interrupt-এ খরচ ৩,০০,০০০ × ৪০০০ = ১২০ কোটি cycle = ৬০%, আর polling-এ ১% + ৩,০০,০০০ × ১০০০ ÷ ২০০ কোটি = ১% + ১৫% = ১৬%.

কিন্তু polling-এর দাম latency-তে: ঘটনাটি গড়ে ১০ µs অপেক্ষা করে। মাঝামাঝি উপায় হলো interrupt coalescing (interrupt moderation): যন্ত্র কয়েকটি ঘটনা জমিয়ে একটি interrupt দেয় — হয় K-টি ঘটনা জমলে, নয়তো প্রথম ঘটনার পর T সময় পেরোলে, যেটা আগে হয়। নিচের simulator একটি যন্ত্রের এলোমেলো (Poisson) ঘটনার ধারা তৈরি করে তিনটি পদ্ধতিই চালায়। সব খরচ ধরে নেওয়া মান, কোনো নির্দিষ্ট chip-এর মাপা মান নয়:

// Polling vs interrupt vs coalesced interrupt for one device -- every cost below is an ASSUMPTION
const F = 2e9;                          // CPU clock: 2 GHz
const POLL_US = 20, POLL_CYC = 400;     // read the status register every 20 us, 400 cycles per poll
const IRQ_CYC = 3000, IRQ_LAT_US = 1;   // per interrupt: entry, ack, exit, cache damage; 1 us to the handler
const WORK_CYC = 1000;                  // handler work per event (same in every scheme)
const K = 8, T_US = 20;                 // coalescing: one interrupt per 8 events or 20 us, whichever first
const SIM_S = 0.2;                      // simulate 0.2 s
let seed = 42;                          // deterministic random arrivals (Park-Miller generator)
const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
const pct = c => (100 * c / (F * SIM_S)).toFixed(2).padStart(7);
const us = x => x.toFixed(1).padStart(6);
console.log("events/s |  poll: CPU%  lat us |  irq: CPU%  lat us | coalesce: CPU%  lat us  ev/irq");
for (const rate of [1000, 10000, 100000, 300000]) {
  const t = [];                         // arrival times in microseconds (Poisson process)
  for (let x = -Math.log(rnd()) / rate; x < SIM_S; x += -Math.log(rnd()) / rate) t.push(x * 1e6);
  const n = t.length;
  let pLat = 0;                         // polling: wait until the next poll tick
  for (const a of t) pLat += Math.ceil(a / POLL_US) * POLL_US - a;
  const pollCyc = (SIM_S * 1e6 / POLL_US) * POLL_CYC + n * WORK_CYC;
  const irqCyc = n * (IRQ_CYC + WORK_CYC);      // one interrupt per event
  let irqs = 0, cLat = 0;               // coalescing: batch closes at the K-th event or T_US after its first
  for (let i = 0; i < n; ) {
    let j = i;
    while (j + 1 < n && j + 1 - i < K && t[j + 1] <= t[i] + T_US) j++;
    const fire = (j - i + 1 === K) ? t[j] : t[i] + T_US;
    for (let m = i; m <= j; m++) cLat += fire - t[m] + IRQ_LAT_US;
    irqs++; i = j + 1;
  }
  const coCyc = irqs * IRQ_CYC + n * WORK_CYC;
  console.log(String(rate).padStart(8) + " |" + pct(pollCyc) + " " + us(pLat / n) + "   |" + pct(irqCyc) + " " +
    us(IRQ_LAT_US) + "   |    " + pct(coCyc) + " " + us(cLat / n) + "  " + (n / irqs).toFixed(1).padStart(6));
}

আসল আউটপুট:

events/s |  poll: CPU%  lat us |  irq: CPU%  lat us | coalesce: CPU%  lat us  ev/irq
    1000 |   1.05    9.9   |   0.19    1.0   |       0.18   20.6     1.0
   10000 |   1.50   10.0   |   1.99    1.0   |       1.73   19.3     1.2
  100000 |   5.95   10.0   |  19.80    1.0   |       9.95   14.3     3.0
  300000 |  15.89   10.0   |  59.54    1.0   |      21.87   10.9     6.4

সেকেন্ডে ১০০০টি ঘটনায় interrupt মাত্র ০.১৯%, আর polling ১.০৫% — যার প্রায় পুরোটা ফাঁকা poll-এর স্থির কর। ১০,০০০-এ (crossover ৬,৬৬৭-এর ওপরে) polling (১.৫০%) interrupt-কে (১.৯৯%) হারিয়ে দিল। ৩,০০,০০০-এ interrupt ৫৯.৫৪% — হাতের হিসাবের ৬০%-এর সাথে মেলে (এলোমেলো ধারায় ঘটনা সামান্য কম এসেছে)। Polling-এর latency সব হারে প্রায় ১০ µs = P ÷ ২, যেমন সূত্র বলেছিল। Coalescing মাঝখানে: উচ্চ হারে প্রতি interrupt-এ গড়ে ৬.৪টি ঘটনা, CPU ২১.৮৭%, latency ১০.৯ µs. কিন্তু কম হারে coalescing-এর লাভ নেই (প্রতি interrupt-এ ১টি ঘটনা) অথচ latency ২০.৬ µs — প্রতিটি একা ঘটনা টাইমারের পুরো ২০ µs অপেক্ষা করে। তাই বাস্তবের network card আর driver প্রায়ই adaptive moderation ব্যবহার করে: হার কম হলে T ছোট, বেশি হলে বড়।

মিশ্র পদ্ধতি — interrupt দিয়ে জাগা, polling দিয়ে খালি করা

উচ্চ হারে আরও একটি বিপদ: interrupt এত ঘন আসে যে CPU শুধু handler-ই চালায়, ডেটা অ্যাপ্লিকেশনে পৌঁছানোর সময়ই পায় না — একে বলে receive livelock. Linux-এর network stack-এর NAPI একটি পরিচিত সমাধান (খুঁটিনাটি version অনুযায়ী বদলায়): প্রথম packet-এ interrupt, তারপর driver সেই queue-র interrupt বন্ধ করে একটি বাজেট পর্যন্ত (প্রায়ই ৬৪টি packet) ring থেকে poll করে; ring খালি হলে আবার interrupt চালু। কম চাপে interrupt-এর কম latency, বেশি চাপে polling-এর কম খরচ। আরও চরমে (DPDK-র মতো framework) একটি পুরো core শুধু polling-এ রাখা হয়। OS-এর দিক থেকে top half আর bottom half আছে Operating Systems কোর্সের I/O সিস্টেম পর্বে।

Software-এ একটি উপমা — POSIX signal

Hardware interrupt সরাসরি user প্রোগ্রাম থেকে দেখা যায় না। তবে Linux-এর POSIX signal একই ধাঁচের একটি software ব্যবস্থা: kernel-এর একটি timer সময় পূর্ণ হলে প্রসেসকে SIGALRM পাঠায়, আর প্রোগ্রামের মূল লুপ কিছু না জেনেই হঠাৎ handler-এ চলে যায়, তারপর আবার যেখানে ছিল সেখানে ফেরে। নিচের প্রোগ্রামে handler শুধু একটি flag বসায় — ঠিক যেমন ভালো interrupt handler কম কাজ করে বাকিটা পরে করার জন্য রেখে দেয়:

// Software ANALOGY of an asynchronous interrupt: SIGALRM from an interval timer
#define _DEFAULT_SOURCE
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>

static volatile sig_atomic_t tick = 0;         // set by the "handler", read by the main loop

static void on_alarm(int sig) { (void)sig; tick = 1; }   // keep the handler tiny: just set a flag

int main(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = on_alarm;                  // like writing the handler address into mtvec
    sigaction(SIGALRM, &sa, NULL);
    struct itimerval it = { {0, 50000}, {0, 50000} };   // every 50 ms, first after 50 ms
    setitimer(ITIMER_REAL, &it, NULL);         // like programming a device timer

    unsigned long work = 0, last = 0;
    int seen = 0;
    while (seen < 5) {
        work++;                                // the "main program" keeps doing its own job
        if (tick) {                            // the flag is checked between pieces of work
            tick = 0;
            seen++;
            printf("tick %d: work done since previous tick = about %lu million\n",
                   seen, (work - last + 500000) / 1000000);
            last = work;
        }
    }
    printf("main loop stopped after %d ticks; it never called the handler itself\n", seen);
    return 0;
}

একটি run-এর আসল আউটপুট (কাজের সংখ্যা প্রতিবার আর প্রতি যন্ত্রে বদলায়):

tick 1: work done since previous tick = about 38 million
tick 2: work done since previous tick = about 37 million
tick 3: work done since previous tick = about 35 million
tick 4: work done since previous tick = about 29 million
tick 5: work done since previous tick = about 36 million
main loop stopped after 5 ticks; it never called the handler itself

মূল লুপ কোথাও on_alarm ডাকেনি, তবু প্রতি ৫০ ms-এ tick = ১ হয়েছে — বাইরের ঘটনা প্রোগ্রামের অজান্তে ঢুকেছে। এই run-এ দুটি tick-এর মাঝে লুপ প্রায় ৩–৪ কোটিবার ঘুরেছে; একটি shared virtual machine-এ সংখ্যাটি অন্য প্রসেসের চাপে ওঠানামা করে (চতুর্থবার ২৯ million). volatile sig_atomic_t জরুরি: volatile compiler-কে প্রতিবার মেমরি থেকে পড়তে বাধ্য করে, আর sig_atomic_t নিশ্চিত করে লেখাটি অর্ধেক অবস্থায় ধরা পড়বে না। এটি শুধু উপমা: এখানে কোনো যন্ত্র, IRQ লাইন বা interrupt controller নেই। Signal পৌঁছায় kernel থেকে, সাধারণত প্রসেসটি kernel থেকে user mode-এ ফেরার মুহূর্তে — আর kernel নিজে এই timer-এর খবর পায় একটি আসল hardware timer interrupt থেকে। Signal-এর নিয়মকানুন দেখুন Operating Systems কোর্সের Signal পর্বে।

২ · যন্ত্র কীভাবে ডাকে — IRQ লাইন, level বনাম edge, আর interrupt controller

সবচেয়ে পুরোনো আর সরল ব্যবস্থায় প্রতিটি যন্ত্র থেকে একটি তার যায় interrupt controller-এর দিকে — interrupt request (IRQ) লাইন. যন্ত্র কিছু জানাতে চাইলে লাইনের voltage বদলায়। কিন্তু “voltage বদলানো” দুইভাবে বোঝা যায়, আর এই পার্থক্য driver লেখার সময় সত্যিকারের bug তৈরি করে:

বিষয়Level-triggeredEdge-triggered
সংকেতের অর্থলাইন সক্রিয় স্তরে (যেমন low) থাকা পর্যন্ত “আমার সেবা দরকার”লাইনের একটি বদল (যেমন 0 → 1) মানে “একটি ঘটনা ঘটেছে”
কবে থামেDriver যন্ত্রের কারণ-বিট মুছলে যন্ত্র নিজে লাইন ছাড়েPulse নিজেই শেষ হয়; controller একটি pending বিট মনে রাখে
একাধিক যন্ত্রে একটি লাইন ভাগসহজ — wired-OR; যেকোনো একজন ধরে রাখলেই সক্রিয় (PCI-র INTA#–INTD# এভাবেই ভাগ হতো)কঠিন — দুটি edge কাছাকাছি এলে একটি হারিয়ে যেতে পারে
ঘটনা হারানোর ঝুঁকিকম — কারণ না মোছা পর্যন্ত লাইন জানাতেই থাকেআছে — pending থাকা অবস্থায় দ্বিতীয় edge আলাদা করে গোনা হয় না
বড় বিপদDriver কারণ না মুছলে লাইন সারাক্ষণ সক্রিয় — বারবার interrupt (storm)Driver-কে handler-এর শেষে আবার status দেখে নিতে হয়, নইলে নতুন ঘটনা চুপচাপ হারায়

পর্ব ১৫-এ latch আর flip-flop-এর level-sensitive আর edge-triggered আচরণ দেখেছিলেন; এখানে একই শব্দ, কিন্তু প্রশ্নটা সময়ের নয় — অনুরোধ কতক্ষণ বেঁচে থাকে, সেটির।

Interrupt controller-এর চারটি কাজ

শত শত যন্ত্রের লাইন সরাসরি CPU-তে আনা অসম্ভব। তাই মাঝখানে বসে interrupt controller, যার কাজ মোটামুটি চারটি: (১) জড়ো করা — প্রতিটি লাইন দেখে কোনটির অনুরোধ pending, তা মনে রাখা; (২) priority — একসাথে কয়েকটি এলে কোনটি আগে; (৩) masking — প্রতিটি উৎস আলাদা করে চালু বা বন্ধ রাখা, যাতে একটি বিগড়ে যাওয়া যন্ত্র বাকিদের না আটকায়; (৪) routing — কোন core-কে জানানো হবে, আর CPU-কে বলে দেওয়া কোন উৎস ডেকেছে (একটি ID বা vector), যাতে handler-কে সব যন্ত্রকে জিজ্ঞেস করতে না হয়। শেষে CPU জানায় “সেবা শেষ” — x86-এ একে বলা হয় EOI (end of interrupt), RISC-V PLIC-এ complete.

x86-এর পথ, সংক্ষেপে: ১৯৮১ সালের IBM PC-তে একটি Intel 8259A PIC ছিল — ৮টি IRQ লাইন; ১৯৮৪ সালের PC/AT-এ দুটি জুড়ে ব্যবহারযোগ্য লাইন হলো ১৫টি। লাইন এত কম ছিল যে ISA কার্ডে jumper দিয়ে IRQ বাছতে হতো, আর দুটি কার্ড একই IRQ নিলে “IRQ conflict”. Multiprocessor যুগে এল APIC: প্রতিটি core-এ local APIC আর যন্ত্রের দিকে I/O APIC, যা যেকোনো interrupt যেকোনো core-এ পাঠাতে পারে। আজকের PCIe যন্ত্র তার বাদ দিয়ে MSI ব্যবহার করে (অংশ ৩)। প্রতিটি chipset-এর খুঁটিনাটি আলাদা।

RISC-V-এর PLIC — claim আর complete

RISC-V-এর PLIC (Platform-Level Interrupt Controller) এই চারটি কাজ খুব পরিষ্কারভাবে ভাগ করে। প্রতিটি উৎসের (১ থেকে ১০২৩ পর্যন্ত; ০ মানে “কোনো interrupt নেই”) একটি gateway আছে, যা level বা edge সংকেতকে একটি “অনুরোধে” বদলায় আর একবারে একটির বেশি অনুরোধ পাঠায় না। প্রতিটি উৎসের একটি priority register (০ মানে কখনো নয়)। প্রতিটি context-এর — অর্থাৎ একটি hart আর একটি privilege mode-এর জোড়ার — আছে নিজস্ব enable বিট, একটি threshold (এর চেয়ে বেশি priority-র interrupt-ই শুধু পৌঁছায়), আর একটি claim/complete register. CPU-র দিকে PLIC শুধু একটি বিট তোলে — machine mode-এর জন্য mip.MEIP; তারপর পর্ব ৩৮-এর trap-যাত্রা শুরু হয়। Handler claim register পড়লে PLIC সবচেয়ে উঁচু priority-র pending উৎসের ID দেয় আর সেটির pending বিট মুছে দেয়; কাজ শেষে একই ID complete register-এ লিখলে gateway আবার নতুন অনুরোধ পাঠাতে পারে।

নিচের stepper-এ একটি network card উৎস ৭ (priority ৫) হিসেবে hart ০-এর machine-mode context-এ (threshold ২) interrupt দিচ্ছে। লাইনটি level-triggered:

1NIC: কাজ শেষ → IRQ 7 লাইন সক্রিয়2PLIC: pending[7] = 1; 5 > threshold 2 → MEIP3hart 0: trap → handler (পর্ব ৩৮)4id = read(claim)        // 75NIC-এর status পড়া, কারণ-বিট মোছা6write(complete, id); mret
অবস্থা (উৎস ৭: priority 5; hart 0 M-mode: threshold 2)
IRQ 7 লাইন
pending[7]
সেবায় আছে
hart 0 MEIP
hart 0 চালাচ্ছে
  1. যন্ত্র ডাকল। Network card একটি packet গ্রহণ শেষ করে তার নিজের status register-এ একটি কারণ-বিট বসাল, আর IRQ ৭ লাইন সক্রিয় করল। Level-triggered, তাই যতক্ষণ কারণ-বিট আছে, লাইন সক্রিয় থাকবে। এখনো CPU কিছু জানে না।
  2. PLIC বাছাই করল। Gateway লাইন দেখে একটি অনুরোধ তৈরি করল: pending[৭] = ১. উৎস ৭ hart ০-এর M-mode context-এ enabled, আর তার priority ৫ threshold ২-এর চেয়ে বেশি — তাই PLIC hart ০-এর MEIP তুলল। অন্য কোনো উৎস pending থাকলে PLIC সবচেয়ে উঁচু priority-রটিকে বেছে নিত।
  3. CPU-র ভেতরের trap. এবার পর্ব ৩৮-এর গল্প: তিনটি তালা খোলা থাকলে দুটি instruction-এর মাঝে trap, mcause = 0x8000000b (interrupt বিট + code ১১, machine external). কিন্তু mcause শুধু বলে “বাইরের কেউ ডেকেছে” — কোন যন্ত্র, তা জানতে PLIC-কে জিজ্ঞেস করতে হবে।
  4. Claim. Handler তার context-এর claim register পড়ল (একটি MMIO load) আর পেল ৭. একই সাথে PLIC pending[৭] মুছল, আর অন্য কিছু pending নেই বলে MEIP নামল। দুটি hart একই interrupt-এর জন্য একসাথে claim করলে একজনই ৭ পায়, অন্যজন পায় ০ — অর্থাৎ “কিছু নেই”; এভাবে একটি interrupt দুবার সেবা পায় না।
  5. যন্ত্রের সেবা। Driver card-এর status পড়ল, কারণ-বিট মুছল (প্রায়ই একটি “write 1 to clear” MMIO store), ring থেকে packet-এর খবর নিল। কারণ মোছায় যন্ত্র নিজে লাইন ছেড়ে দিল: লাইন = ০. লাইন এখনো সক্রিয় থাকা অবস্থায় complete করলে কী হতো? Gateway সাথে সাথে নতুন অনুরোধ পাঠাত — আবার interrupt.
  6. Complete আর ফেরা। Handler complete register-এ ৭ লিখল; এখন gateway উৎস ৭-এর পরের অনুরোধ নিতে প্রস্তুত। তারপর mret — user প্রোগ্রাম যেখানে ছিল সেখান থেকে চলল। পুরো যাত্রায় CPU-কে MMIO-তে অন্তত তিনবার যেতে হলো (claim, যন্ত্রের status, complete) — প্রতিটি uncached access, তাই interrupt-এর খরচের একটি বড় অংশ এগুলোই।

Interrupt storm আর spurious interrupt

Interrupt storm মানে একটি উৎস থেকে থামাথামিহীন interrupt, যাতে CPU আর কিছু করতে পারে না — সাধারণত level-triggered লাইনের কারণ-বিট না মোছা, ভুল driver বা বিকল যন্ত্রের ফল। Linux-এর মতো kernel কোনো লাইনে বারবার এমন interrupt দেখলে, যা কোনো handler “নিজের” বলে স্বীকার করেনি, লাইনটি বন্ধ করে log-এ “nobody cared” ধরনের বার্তা লেখে। Spurious interrupt উল্টো: interrupt এল, কিন্তু কেউ ডাকেনি — লাইনে সাময়িক noise, বা অন্য core আগেই সেবা দিয়েছে। PLIC-এ claim ০ ফেরালে handler কিছু না করে ফেরে।

৩ · MSI আর MSI-X — interrupt যখন একটি memory write

PCIe-তে প্রতিটি যন্ত্রের জন্য আলাদা interrupt-তার নেই; সব কথাবার্তা packet-এ (পর্ব ৫১)। পুরোনো INTx লাইন সেখানে বিশেষ message দিয়ে অনুকরণ করা হয়, কিন্তু আধুনিক যন্ত্রের আসল পদ্ধতি MSI (Message Signaled Interrupt): চালু হওয়ার সময় OS যন্ত্রকে একটি ঠিকানা আর একটি ডেটা মান বলে দেয়। Interrupt দিতে যন্ত্র ঠিক সেই ঠিকানায় সেই মানটি লেখে — একটি সাধারণ DMA memory write. ঠিকানাটি RAM-এর নয়, interrupt controller-এর: x86-এ এটি local APIC-এর জন্য রাখা একটি বিশেষ ঠিকানা-এলাকায় (সাধারণত 0xFEE… দিয়ে শুরু) যায়, আর ডেটার ভেতরে থাকে vector. RISC-V-এর নতুন AIA নকশায় এই কাজ করে প্রতিটি hart-এর IMSIC.

পুরোনো পদ্ধতি যেন প্রতিটি ফ্ল্যাট থেকে দারোয়ানের ঘর পর্যন্ত আলাদা কলিংবেলের তার — তার কম পড়লে দুই ফ্ল্যাট একটি বেল ভাগ করে, আর বাজলে দারোয়ানকে দুই জায়গায়ই খোঁজ নিতে হয়। MSI যেন প্রতিটি ফ্ল্যাট একটি নির্দিষ্ট নম্বরে একটি SMS পাঠায়, যার লেখাতেই বলা আছে কে আর কেন। তার লাগে না, আর নম্বর যত খুশি।

  • লাইন ভাগাভাগি নেই: প্রতিটি interrupt-এর নিজস্ব vector, তাই handler-কে “কে ডাকল” খুঁজতে হয় না।
  • অনেক vector: MSI-তে একটি যন্ত্র সর্বোচ্চ ৩২টি পর্যন্ত, আর MSI-X-এ সর্বোচ্চ ২০৪৮টি — প্রতিটির আলাদা ঠিকানা-ডেটা জোড়া, যন্ত্রের মেমরিতে একটি টেবিলে।
  • Queue-প্রতি interrupt আর core-এ পাঠানো: আধুনিক network card বা NVMe SSD-তে অনেক queue থাকে, প্রতিটির একটি MSI-X vector, যার ঠিকানা আলাদা core-এর দিকে। তাই queue ৩-এর কাজ core ৩-এ, যেখানে তার ডেটা cache-এ আছে। Linux-এ কোন IRQ কোন core-এ যাবে, তা /proc/irq/<n>/smp_affinity দিয়ে দেখা ও বদলানো যায় — এটিই IRQ affinity. Network card packet-এর header-এর hash দিয়ে queue বাছে — সাধারণত RSS (receive side scaling) নামে।
  • ক্রমের নিশ্চয়তা: PCIe-র নিয়মে একটি posted write আগের posted write-কে ডিঙাতে পারে না (relaxed ordering চালু না করলে)। যন্ত্র আগে ডেটা DMA করে, তারপর MSI লেখে — তাই interrupt পৌঁছানোর সময় ডেটাও মেমরিতে আছে। আলাদা তারের interrupt-এ এই নিশ্চয়তা ছিল না।

MSI-র আচরণ edge-এর মতো: একটি write একটি ঘটনা। তাই level-triggered লাইনের storm-এর সমস্যা নেই, কিন্তু driver-কে নিশ্চিত হতে হয় handler-এর শেষে নতুন কাজ জমে আছে কি না।

৪ · DMA — CPU-কে কুলির কাজ থেকে মুক্তি

Interrupt সমস্যার অর্ধেক সমাধান করে: CPU-কে আর অপেক্ষায় দাঁড়িয়ে থাকতে হয় না। কিন্তু ডেটা সরাবে কে? Programmed I/O-তে (PIO) CPU নিজেই প্রতিটি word যন্ত্রের data register থেকে load করে RAM-এ store করে। যন্ত্রের register uncached আর bus-এর ওপারে, তাই প্রতিটি load-এ CPU দীর্ঘ সময় (PCIe-তে কয়েকশো ns, এমনকি এক µs-এর কাছাকাছিও হতে পারে) থেমে থাকে। একটি দ্রুত SSD বা network card-এর ডেটা এভাবে সরালে CPU কার্যত কুলির কাজ করছে।

DMA (Direct Memory Access)-এ CPU শুধু কাজটি বুঝিয়ে দেয় — কোথা থেকে, কোথায়, কত বাইট — আর যন্ত্র (বা একটি DMA engine) নিজেই মেমরিতে পড়ে-লেখে। শেষ হলে একটি interrupt. CPU-র খরচ তাই transfer-এর আকারের ওপর প্রায় নির্ভর করে না: একটি নির্দিষ্ট setup খরচ আর একটি completion খরচ।

সূত্র

ডেটার হার R বাইট/সেকেন্ড, word-এর মাপ w বাইট, প্রতি word-এ cw cycle, block-এর মাপ B:
PIO-র CPU ভাগ = (R ÷ w) × cw ÷ f  ·  DMA-র CPU ভাগ = (R ÷ B) × (Csetup + Cdone) ÷ f
এক transfer-এ সমান খরচ: (B ÷ w) × cw = Csetup + Cdone, তাই break-even আকার B* = (Csetup + Cdone) × w ÷ cw.

// Programmed I/O (CPU copies every word) vs DMA (CPU only sets up and takes one interrupt)
// All costs are ASSUMPTIONS for a teaching example, not measurements of a real chip
const F = 2e9;            // CPU clock: 2 GHz
const WORD = 8;           // bytes moved per load/store pair in programmed I/O
const CPW = 50;           // cycles per word: uncached read of the device data register + store to RAM
const SETUP = 2000;       // DMA: driver fills a descriptor and writes the doorbell register
const DONE = 4000;        // DMA: completion interrupt + handler bookkeeping
const pioCycles = bytes => (bytes / WORD) * CPW;
const dmaCycles = () => SETUP + DONE;     // independent of the transfer size
const breakEven = (SETUP + DONE) * WORD / CPW;
console.log(`break-even size: ${breakEven} bytes (smaller transfers are cheaper with programmed I/O)`);
console.log("\nCPU cycles for ONE transfer:");
console.log("    size |      PIO |  DMA | winner");
for (const s of [64, 512, 960, 4096, 65536]) {
  const p = pioCycles(s), d = dmaCycles();
  console.log(String(s).padStart(8) + " |" + String(p).padStart(9) + " | " + d + " | " +
    (p < d ? "PIO" : p > d ? "DMA" : "tie"));
}
console.log("\nShare of one 2 GHz core at a steady device rate:");
console.log("   rate MB/s  block |  PIO CPU%  |  DMA CPU%");
for (const [mbps, block] of [[100, 4096], [100, 65536], [1000, 4096], [1000, 65536]]) {
  const bytesPerS = mbps * 1e6;
  const pio = 100 * pioCycles(bytesPerS) / F;
  const dma = 100 * (bytesPerS / block) * dmaCycles() / F;
  console.log(String(mbps).padStart(11) + String(block).padStart(7) + " |" +
    (pio.toFixed(2) + (pio > 100 ? " (!)" : "")).padStart(11) + " |" + dma.toFixed(2).padStart(9));
}

আসল আউটপুট:

break-even size: 960 bytes (smaller transfers are cheaper with programmed I/O)

CPU cycles for ONE transfer:
    size |      PIO |  DMA | winner
      64 |      400 | 6000 | PIO
     512 |     3200 | 6000 | PIO
     960 |     6000 | 6000 | tie
    4096 |    25600 | 6000 | DMA
   65536 |   409600 | 6000 | DMA

Share of one 2 GHz core at a steady device rate:
   rate MB/s  block |  PIO CPU%  |  DMA CPU%
        100   4096 |      31.25 |     7.32
        100  65536 |      31.25 |     0.46
       1000   4096 | 312.50 (!) |    73.24
       1000  65536 | 312.50 (!) |     4.58

Break-even ৬০০০ × ৮ ÷ ৫০ = ৯৬০ বাইট: এর চেয়ে ছোট transfer-এ DMA-র নির্দিষ্ট খরচই বেশি, তাই খুব ছোট কাজে (যেমন একটি status বাইট) PIO-ই ঠিক। ৪ KiB-এ DMA চার গুণের বেশি সস্তা, ৬৪ KiB-এ প্রায় ৬৮ গুণ। ১০০ MB/s-এর যন্ত্রে PIO একটি core-এর ৩১.২৫% খায় — ব্লকের মাপ যা-ই হোক, কারণ প্রতিটি বাইট CPU-কেই সরাতে হয়। ১০০০ MB/s-এ PIO-র দরকার ৩১২.৫% — একটি core-এ অসম্ভব (চিহ্ন (!))। কিন্তু লক্ষ করুন, DMA-তেও ১০০০ MB/s আর ৪ KiB block-এ ৭৩.২৪%: প্রতি সেকেন্ডে প্রায় ২,৪৪,১৪১টি transfer, প্রতিটির setup আর interrupt. DMA প্রতি-বাইটের খরচ দূর করে, প্রতি-transfer-এর খরচ নয় — সেটি কমাতে লাগে বড় block, একবারে অনেক কাজ জমা দেওয়ার descriptor ring (অংশ ৫) আর interrupt coalescing.

DMA controller আর bus mastering

পুরোনো PC আর বহু microcontroller-এ একটি আলাদা DMA controller থাকে (IBM PC-তে Intel 8237, ৪টি channel) — যন্ত্র আর মেমরির মাঝে এই তৃতীয় পক্ষ ডেটা সরায়। আধুনিক PCIe যন্ত্রের নিজের DMA engine থাকে, যা নিজেই bus-এ memory read/write শুরু করে — একে বলে bus mastering. দুই ক্ষেত্রেই মেমরির bandwidth CPU আর যন্ত্র ভাগ করে; ভারী DMA-র সময় CPU-র cache miss একটু বেশি অপেক্ষা করতে পারে — পুরোনো বইয়ে একে cycle stealing বলা হয়। DMA “বিনামূল্যে” নয়, শুধু CPU-র instruction খরচ ছাড়া।

একটি DMA transfer, ধাপে ধাপে

  1. Buffer প্রস্তুত: driver মেমরিতে একটি buffer নেয় আর যন্ত্রের বোঝার মতো ঠিকানা (physical বা IOMMU-র ঠিকানা, অংশ ৭) বের করে; non-coherent প্ল্যাটফর্মে cache সামলায় (অংশ ৬)।
  2. কাজ বুঝিয়ে দেওয়া: উৎস, গন্তব্য, দৈর্ঘ্য, দিক লেখে — সরল যন্ত্রে সরাসরি register-এ, আধুনিক যন্ত্রে একটি descriptor-এ।
  3. শুরু: একটি MMIO write — প্রায়ই একটি doorbell register-এ — যন্ত্রকে বলে “নতুন কাজ আছে”।
  4. Transfer: যন্ত্র bus-এ একের পর এক memory read বা write পাঠায়; CPU এ সময় অন্য কাজ করে।
  5. শেষের খবর: যন্ত্র descriptor-এ status লেখে, তারপর interrupt (MSI) দেয় — বা driver poll করে।
  6. গুছিয়ে নেওয়া: driver status দেখে, cache আর ম্যাপিং ঠিক করে, ডেটা উপরের স্তরে দেয়।

৫ · Descriptor ring — যন্ত্র আর driver-এর ভাগাভাগি করা খাতা

প্রতিটি transfer-এর জন্য register লিখে, শেষ হওয়ার অপেক্ষা করে, তারপর পরেরটি দেওয়া — এতে যন্ত্র প্রতিবার অলস বসে থাকে আর CPU প্রতিবার ডাক পায়। সমাধান: মেমরিতে একটি descriptor ring — N ঘরের একটি circular buffer, যার প্রতিটি ঘরে একটি descriptor: buffer-এর ঠিকানা, দৈর্ঘ্য, কিছু flag (যেমন “packet-এর শেষ”), আর একটি status অংশ যেখানে যন্ত্র “শেষ হয়েছে” (অনেক NIC-এ DD = descriptor done) লেখে। Driver অনেকগুলো কাজ একবারে জমা দিতে পারে, আর যন্ত্র নিজের গতিতে একটার পর একটা করে যায়।

রেস্তোরাঁর রান্নাঘরে ঘোরানো অর্ডার-চাকার কথা ভাবুন: ওয়েটার (driver) খালি ক্লিপে নতুন অর্ডার আটকে চাকা এগিয়ে দেন, বাবুর্চি (যন্ত্র) অন্য দিক থেকে একটার পর একটা অর্ডার নেন আর রান্না শেষে টিক দেন। কেউ কারও জন্য দাঁড়িয়ে থাকেন না। শুধু দুটি নিয়ম: ওয়েটার বাবুর্চির না-নেওয়া অর্ডারের ওপর নতুন অর্ডার আটকাতে পারবেন না (ভর্তি), আর বাবুর্চি খালি ক্লিপ থেকে অর্ডার নিতে পারবেন না (খালি)।

দুটি সূচক (index) এই নিয়ম রাখে। এই পর্বে Intel-এর পুরোনো e1000-ধাঁচের NIC-এর নিয়ম ধরছি (অন্য যন্ত্রে নাম ও দায়িত্ব আলাদা হতে পারে): tail বাড়ায় producer — যে নতুন কাজ দেয়; head বাড়ায় consumer — যে কাজ শেষ করে। Transmit ring-এ driver producer, যন্ত্র consumer. প্রতিটি সূচক N-এ পৌঁছে ০-তে ফেরে (mod N).

নিয়ম

ব্যবহৃত ঘর = (tail − head + N) mod N  ·  খালি: head = tail  ·  ভর্তি: (tail + ১) mod N = head, অর্থাৎ সর্বোচ্চ N − ১টি ঘর ব্যবহার হয়।
কেন এক ঘর খালি? ব্যবহৃত ঘরের সংখ্যা ০ থেকে N — মোট N + ১টি সম্ভাবনা, কিন্তু (tail − head) mod N-এর মান মাত্র N রকম; সব ঘর ভরলে head = tail “খালি” আর “ভর্তি” দুটোই বোঝাত। একটি ঘর ত্যাগ করলে সম্ভাবনা N-টি, সূচকেই ধরা যায়। বিকল্প: আলাদা count, mod N না করা free-running সূচক (virtio-র ring এমন বলে জানা যায়), বা NVMe-র মতো প্রতি চক্করে উল্টে যাওয়া “phase” বিট।

নিচের simulator একটি ৮ ঘরের transmit ring-এ driver আর যন্ত্রকে পালা করে চালায়। H আর T head আর tail-এর অবস্থান, . খালি ঘর, ছোট হাতের অক্ষর যন্ত্রের কাছে জমা থাকা packet:

// A transmit descriptor ring: the driver (producer) advances TAIL, the device (consumer) advances HEAD
// Convention used here: empty when head == tail, full when (tail + 1) % N == head (one slot kept empty)
const N = 8;
const ring = new Array(N).fill(".");      // "." = free slot, letter = descriptor owned by the device
let head = 0, tail = 0, next = 0;         // next = name of the next packet (a, b, c, ...)
const count = () => (tail - head + N) % N;
function show(what) {
  const marks = ring.map((s, i) => (i === head ? "H" : " ") + (i === tail ? "T" : " ") + s).join(" ");
  const st = count() === 0 ? "EMPTY" : count() === N - 1 ? "FULL" : `${count()} in use`;
  console.log(what.padEnd(22) + marks + "   h=" + head + " t=" + tail + "  " + st);
}
function post(k) {                        // driver writes k descriptors, then rings the doorbell once
  let done = 0;
  while (done < k && (tail + 1) % N !== head) {
    ring[tail] = String.fromCharCode(97 + next++); tail = (tail + 1) % N; done++;
  }
  show(`driver posts ${done}/${k}` + (done < k ? " !" : ""));
}
function device(k) {                      // device sends k packets (or fewer if the ring is empty)
  let done = 0;
  while (done < k && head !== tail) { ring[head] = "."; head = (head + 1) % N; done++; }
  show(`device sends ${done}/${k}`);
}
show("start");
post(3); device(2); post(5); post(2); device(4); post(4); device(9);

আসল আউটপুট:

start                 HT.   .   .   .   .   .   .   .   h=0 t=0  EMPTY
driver posts 3/3      H a   b   c  T.   .   .   .   .   h=0 t=3  3 in use
device sends 2/2        .   . H c  T.   .   .   .   .   h=2 t=3  1 in use
driver posts 5/5       T.   . H c   d   e   f   g   h   h=2 t=0  6 in use
driver posts 1/2 !      i  T. H c   d   e   f   g   h   h=2 t=1  FULL
device sends 4/4        i  T.   .   .   .   . H g   h   h=6 t=1  3 in use
driver posts 4/4        i   j   k   l   m  T. H g   h   h=6 t=5  FULL
device sends 7/9        .   .   .   .   . HT.   .   .   h=5 t=5  EMPTY

চতুর্থ লাইনে tail ৭ পেরিয়ে ০-তে ফিরেছে (wrap-around): (৩ + ৫) mod ৮ = ০. পঞ্চম লাইনে driver ২টি দিতে চেয়েছিল, পারল ১টি — tail = ১ হতেই (১ + ১) mod ৮ = ২ = head, ring ভর্তি (৭টি ঘর ব্যবহৃত, ১টি ইচ্ছা করে খালি)। Driver তখন অপেক্ষা করে বা packet-টি নিজের queue-তে রাখে; Linux-এর মতো সিস্টেমে উপরের স্তরকে সাময়িকভাবে থামতে বলা হয়। শেষ লাইনে যন্ত্র ৯টি পাঠাতে চেয়েছিল, কিন্তু ছিল ৭টি (h = ৬ থেকে t = ৫ পর্যন্ত: (৫ − ৬ + ৮) mod ৮ = ৭) — পাঠাল ৭টি, তারপর head = tail = ৫: খালি। লক্ষ করুন, driver প্রতিটি post-এ doorbell একবারই বাজায় — ৫টি descriptor-এর জন্য একটি MMIO write. Ring-এর আসল লাভ এখানেই: অনেক কাজ, এক ডাক।

এবার receive দিক, যেখানে যন্ত্র producer: driver আগে থেকে খালি buffer-ওয়ালা descriptor দিয়ে রাখে, packet এলে যন্ত্র DMA করে ভরে দেয়। নিচের stepper একটি ৪ ঘরের receive ring দেখায়। এখানে নিয়ম: head থেকে tail − ১ পর্যন্ত ঘরগুলো যন্ত্রের, যন্ত্র head বাড়ায়, driver tail বাড়িয়ে নতুন খালি buffer দেয়:

1driver: buf b0,b1,b2 → ঘর 0,1,2; tail = 32NIC: packet P1 এল; DMA read descriptor 03NIC: DMA write P1 → b04NIC: descriptor 0-তে len, DD = 1; head = 15NIC: P2 → b1, DD; head = 2; MSI-X6driver: 0,1 তুলল; b4,b5 → ঘর 3,0; tail = 1
Ring (N = 4) — কার হাতে, কী আছে
ঘর 0
ঘর 1
ঘর 2
ঘর 3
head / tail
  1. প্রস্তুতি। Driver তিনটি খালি buffer (b0, b1, b2)-এর ঠিকানা ঘর ০, ১, ২-এর descriptor-এ লিখে MMIO দিয়ে NIC-কে ring-এর ঠিকানা, মাপ আর tail = ৩ জানাল। head ০ থেকে tail − ১ = ২ পর্যন্ত তিনটি ঘর NIC-এর; ঘর ৩ নিয়ম অনুযায়ী খালি।
  2. Packet এল। NIC frame-টি নিজের ছোট on-chip buffer-এ রেখে মেমরি থেকে descriptor ০ পড়ল (একটি DMA read) আর জানল packet যাবে b0-তে। CPU এখনো কিছু জানে না।
  3. DMA write. NIC ১৫০০ বাইট bus দিয়ে সরাসরি b0-তে লিখল — কয়েকটি memory write TLP-তে (সংখ্যাটি TLP-র সর্বোচ্চ মাপের ওপর নির্ভর করে). CPU-র একটিও instruction খরচ হলো না।
  4. Status লেখা। ডেটা লেখার পরে NIC descriptor ০-এ দৈর্ঘ্য ১৫০০ আর DD = ১ লিখল, আর head = ১ করল। ক্রমটি জরুরি: driver যেন DD দেখে এমন ডেটা না পড়ে যা এখনো পৌঁছায়নি — PCIe-র posted write-এর ক্রম এটি নিশ্চিত করে।
  5. আরেকটি packet, একটি interrupt. ৬০ বাইটের P2 একইভাবে b1-এ গেল, head = ২. Coalescing টাইমার পূর্ণ হলে NIC একটি MSI-X পাঠাল — দুটি packet-এর জন্য একটি interrupt. এর পরের কাজ (handler, softirq) অংশ ৮-এ।
  6. তোলা আর refill. Driver ঘর ০ আর ১-এ DD দেখে packet দুটি উপরের স্তরে দিল। তারপর নতুন খালি buffer b4 আর b5 ঘর ৩ আর ০-তে (tail-এর অবস্থান থেকে) বসিয়ে tail = (৩ + ২) mod ৪ = ১ লিখল — একটি doorbell write. এখন NIC-এর ঘর: head ২ থেকে tail − ১ = ০ পর্যন্ত, অর্থাৎ ২, ৩, ০ — আবার তিনটি। ঘর ১ এখন সেই ইচ্ছা করে খালি রাখা ঘর।

Scatter-gather — টুকরো টুকরো মেমরি, এক transfer

একটি packet-এর header এক জায়গায় আর data অন্য জায়গায় থাকতে পারে; virtual memory-তে টানা ১ MiB physical memory-তে ২৫৬টি আলাদা ৪ KiB page-এ ছড়ানো থাকতে পারে। আগে সব এক টানা buffer-এ কপি করলে DMA-র লাভ নষ্ট। Scatter-gather DMA-তে driver (ঠিকানা, দৈর্ঘ্য)-এর একটি তালিকা দেয়, আর যন্ত্র টুকরোগুলো একটানা পড়ে (gather) বা ছড়িয়ে লেখে (scatter); NVMe-তে এর জন্য PRP তালিকা আর SGL আছে। পর্ব ৪২-এর vector gather-scatter নামে কাছাকাছি, কিন্তু সেটি CPU-র instruction.

৬ · DMA আর cache — পুরোনো তথ্যের ফাঁদ

পর্ব ৪৭-এ দেখেছেন, write-back cache-এ store শুধু cache-এ লেখা হয়, line dirty থাকে, RAM-এ যায় পরে। আর DMA কথা বলে মেমরির সাথে, CPU-র cache-এর সাথে নয় — যদি না hardware বিশেষ ব্যবস্থা রাখে। ফলে দুই দিকেই coherence-এর সমস্যা:

  • CPU → যন্ত্র (transmit): CPU buffer-এ নতুন packet লিখল, কিন্তু নতুন মান এখনো cache-এর dirty line-এ। যন্ত্র RAM থেকে পড়ে পুরোনো ডেটা পাঠাল।
  • যন্ত্র → CPU (receive): যন্ত্র RAM-এ নতুন ডেটা লিখল, কিন্তু CPU-র cache-এ সেই ঠিকানার পুরোনো একটি কপি আছে। CPU পড়ল — cache hit — পুরোনো মান।

সমাধান দুই রকম। Hardware-coherent DMA: যন্ত্রের প্রতিটি memory access cache-কে snoop করে — যন্ত্রের read dirty line থেকে নতুন মান পায়, যন্ত্রের write cache-এর পুরোনো কপি বাতিল করে। x86-এর PC আর server-এ PCIe DMA সাধারণত এভাবেই coherent; পর্ব ৫৩-এর multicore coherence-এর মতোই ধারণা, শুধু একটি “core” এখানে যন্ত্র। Software-managed (non-coherent) DMA: বহু embedded ARM SoC আর কিছু RISC-V chip-এ যন্ত্রের পথ cache এড়িয়ে যায় (area আর power বাঁচাতে)। তখন driver-কে যন্ত্র পড়ার আগে clean (dirty line RAM-এ লেখা) আর CPU পড়ার আগে invalidate (কপি বাতিল) করতে হয় — ARM64-এ DC CVAC, DC IVAC-এর মতো instruction, RISC-V-এর Zicbom extension-এ cbo.clean, cbo.inval, cbo.flush. Linux driver সাধারণত এগুলো নিজে লেখে না; DMA API (dma_map_single, dma_sync_single_for_cpu ইত্যাদি) প্ল্যাটফর্ম অনুযায়ী দরকার হলে কাজটি করে। কোন SoC কোন পথে, তা তার datasheet-এর বিষয়।

1CPU: buf_tx-এ নতুন packet লেখা (store)2✗ সরাসরি DMA শুরু → যন্ত্র RAM পড়ে3✓ clean(buf_tx)  // dirty line → RAM4DMA শুরু → যন্ত্র RAM পড়ে5যন্ত্র: DMA write → buf_rx; CPU পড়ে6✓ invalidate(buf_rx); CPU আবার পড়ে
একটি করে cache line: X (transmit), Y (receive)
cache[X]
RAM[X]
যন্ত্র পেল (X)
cache[Y]
RAM[Y]
CPU পেল (Y)
  1. CPU লিখল। Driver transmit buffer-এর line X-এ নতুন packet (NEW) লিখল। Write-back cache, তাই NEW আছে শুধু cache-এ, line dirty; RAM-এ এখনো আগের ডেটা (OLD).
  2. ভুল পথ: সোজা DMA. Non-coherent প্ল্যাটফর্মে যন্ত্র RAM থেকে পড়ে, cache দেখে না — পাঠাল OLD. কোনো error নেই, কোনো crash নেই; তারের ওপর ভুল packet গেল। এমন bug মাঝেমধ্যে দেখা দেয় (cache line কখনো আগেই evict হয়ে গেলে ঠিক কাজ করে), তাই খুঁজে পাওয়া কঠিন।
  3. ঠিক পথ: আগে clean. DMA শুরুর আগে driver line X clean করল: dirty মান RAM-এ লেখা হলো, cache-এর কপি থাকল কিন্তু এখন clean. Linux-এ DMA_TO_DEVICE দিকের ম্যাপিং এটিই করে।
  4. যন্ত্র ঠিক ডেটা পেল। এবার RAM[X] = NEW, তাই যন্ত্র NEW পাঠাল। ধাপ ২ আর ৪-এর পার্থক্য শুধু একটি cache-maintenance instruction-এর ক্রম।
  5. Receive-এর ফাঁদ। CPU আগে কোনো সময় line Y পড়েছিল, তাই cache-এ OLD2 আছে। যন্ত্র DMA দিয়ে RAM[Y]-এ নতুন packet (PKT) লিখল, কিন্তু cache জানল না। CPU পড়ল — hit — পেল OLD2.
  6. Invalidate, তারপর পড়া। Driver CPU-কে buffer দেওয়ার আগে line Y invalidate করল; পরের read miss হয়ে RAM থেকে PKT আনল। বাস্তবে driver DMA শুরুর আগেও একবার invalidate (বা clean) করে, যাতে DMA চলার মাঝে কোনো dirty line evict হয়ে যন্ত্রের লেখা ডেটার ওপর পুরোনো মান না বসিয়ে দেয়। Hardware-coherent প্ল্যাটফর্মে এই ছয় ধাপের পুরো সমস্যাই hardware নিজে মেটায়।

Cache line ভাগ করবেন না: Invalidate কাজ করে পুরো cache line-এ (সাধারণত ৬৪ বাইট)। একটি receive buffer যদি এমন line-এ শুরু বা শেষ হয় যেখানে অন্য কোনো ভ্যারিয়েবলও আছে, invalidate সেই ভ্যারিয়েবলের dirty, এখনো-না-লেখা মানটিও মুছে দেবে। তাই DMA buffer-কে cache line-এর মাপে aligned আর পূর্ণ line-এর মাপে বরাদ্দ করা হয় — Linux-এর মতো kernel-এর DMA-যোগ্য বরাদ্দ সাধারণত এটি নিশ্চিত করে।

৭ · DMA আর virtual memory — ঠিকানা, pinned page আর IOMMU

পর্ব ৫০-এ দেখেছেন, প্রোগ্রাম কাজ করে virtual address-এ, আর MMU প্রতিটি access-কে page table দিয়ে physical address-এ অনুবাদ করে। কিন্তু যন্ত্রের DMA পথে CPU-র MMU নেই। যন্ত্র বোঝে শুধু bus-এর ঠিকানা — IOMMU না থাকলে সেটি সরাসরি physical address. এর তিনটি ফল:

  1. টুকরো ঠিকানা: virtual-এ টানা buffer physical-এ ছড়ানো page — তাই scatter-gather (অংশ ৫)।
  2. Page সরানো যাবে না: DMA চলার মাঝে OS যদি page-টি swap করে বা অন্য জায়গায় সরায়, যন্ত্র পুরোনো physical ঠিকানায় লিখে অন্য কারও ডেটা নষ্ট করবে। তাই DMA-র buffer-কে কাজ শেষ না হওয়া পর্যন্ত pin করে রাখা হয় — OS প্রতিশ্রুতি দেয় এটি সরাবে না।
  3. সব ঠিকানা পৌঁছায় না: পুরোনো বা সস্তা কোনো যন্ত্র হয়তো শুধু ৩২-বিটের ঠিকানা (প্রথম ৪ GiB) বোঝে। Buffer তার ওপরে থাকলে OS নিচের মেমরিতে একটি bounce buffer রাখে আর CPU দিয়ে কপি করে — Linux-এ swiotlb এমন একটি ব্যবস্থা। কাজ হয়, কিন্তু DMA-র লাভের কিছুটা যায়।

IOMMU — যন্ত্রের জন্য MMU

IOMMU (Intel-এ VT-d, AMD-তে AMD-Vi, ARM-এ SMMU নামে পরিচিত) যন্ত্র আর মেমরির মাঝে বসে প্রতিটি DMA ঠিকানা অনুবাদ করে — পর্ব ৫০-এর page walk আর TLB-র মতোই, এখানে IOTLB. প্রতিটি যন্ত্রের (PCIe-তে তার bus/device/function পরিচয় দিয়ে চেনা) নিজস্ব page table থাকতে পারে। লাভ চারটি: (১) ছড়ানো physical page-কে যন্ত্রের চোখে একটানা দেখানো; (২) ৩২-বিটের যন্ত্রকে ওপরের মেমরিতে পৌঁছানো, bounce buffer ছাড়াই; (৩) virtual machine-কে একটি আসল যন্ত্র সরাসরি দেওয়া (device passthrough) — অতিথির “physical” ঠিকানা IOMMU আসল ঠিকানায় বদলায়; আর (৪) সবচেয়ে জরুরি — সুরক্ষা: যে ঠিকানা table-এ নেই, সেখানে DMA আটকে যায় আর একটি fault লেখা হয়। খরচও আছে: প্রতিটি map/unmap-এ table বদলানো আর IOTLB মোছা, যা উচ্চ হারের I/O-তে চোখে পড়ার মতো হতে পারে — তাই কিছু সিস্টেম ম্যাপিং দীর্ঘ সময় রেখে দেয় বা দলবদ্ধভাবে মোছে, সুরক্ষার সাথে একটু আপস করে।

কেন IOMMU নিরাপত্তার জন্য জরুরি (প্রতিরক্ষার দৃষ্টিতে): Bus master যন্ত্র IOMMU ছাড়া যেকোনো physical ঠিকানায় পড়তে-লিখতে পারে — OS-এর অনুমতি, page table-এর সুরক্ষা, user/kernel ভাগ কিছুই তাকে থামায় না। ফলে বাইরের port-এ লাগানো একটি ক্ষতিকর যন্ত্র, বা কোনো যন্ত্রের আপস-করা firmware, ধারণাগতভাবে মেমরি থেকে গোপন তথ্য পড়তে বা kernel বদলাতে পারে — একে বলা হয় DMA আক্রমণ. DMA-ক্ষমতার FireWire port আর পরে PCIe-বাহী Thunderbolt-এর মতো বাইরের port নিয়ে গবেষকেরা এমন দুর্বলতা বহুবার দেখিয়েছেন। প্রতিরক্ষা: IOMMU চালু রেখে প্রতিটি যন্ত্রকে শুধু তার নিজের buffer-এ সীমাবদ্ধ রাখা; আধুনিক OS-গুলো বাইরের port-এর যন্ত্রের জন্য এই সুরক্ষা ডিফল্টে চালু রাখার দিকে গেছে (খুঁটিনাটি version আর hardware অনুযায়ী আলাদা); firmware-এ boot-এর আগেই DMA সুরক্ষা; lock থাকা অবস্থায় নতুন যন্ত্রকে অনুমতি না দেওয়া; আর DMA buffer-কে আলাদা page-এ রাখা, যাতে যন্ত্র পাশের সংবেদনশীল ডেটা না দেখে।

Zero-copy — একটি কপিও কম

সাধারণ network পাঠানোয় ডেটা আগে CPU দিয়ে অ্যাপ্লিকেশনের buffer থেকে kernel-এর buffer-এ কপি হয়, তারপর DMA. Zero-copy-র ধারণা: কপি বাদ দিয়ে যন্ত্র সরাসরি ব্যবহারকারীর pinned page বা page cache থেকে DMA করুক — Linux-এর sendfile বা io_uring-এর নিবন্ধিত buffer এই দিকের উদাহরণ, প্রতিটির নিজস্ব শর্ত আছে। বড় transfer-এ লাভ স্পষ্ট; কিন্তু pin আর ম্যাপিংয়ের নিজস্ব খরচ আছে, তাই ছোট packet-এ সাধারণ কপিই প্রায়ই দ্রুত — অংশ ৪-এর break-even যুক্তি এখানেও খাটে।

৮ · শুরু থেকে শেষ — একটি network packet-এর যাত্রা

এবার সবগুলো টুকরো জোড়া দিই। Linux-এর মতো একটি সিস্টেমে, একটি আধুনিক multi-queue network card-এ একটি packet পৌঁছালে মোটামুটি এভাবে চলে (খুঁটিনাটি driver আর kernel version অনুযায়ী বদলায়):

  1. তার থেকে NICRSS hash → queue 3physical layer, MAC
  2. DMAbuffer + DDCPU-র কাজ নেই
  3. MSI-Xvector → core 3coalescing-এর পর
  4. HandlerNAPI scheduleছোট, দ্রুত
  5. Softirqpoll, IP, TCPতারপর অ্যাপ্লিকেশন
  1. আগে থেকে প্রস্তুত: driver প্রতিটি receive queue-র ring-এ খালি buffer দিয়ে রেখেছে (অংশ ৫-এর stepper-এর প্রথম ধাপ)।
  2. Queue বাছাই: NIC frame গ্রহণ করে header-এর hash থেকে ঠিক করে এটি queue ৩-এর — একই সংযোগের সব packet একই queue-তে, তাই একই core-এ।
  3. DMA: queue ৩-এর পরের descriptor পড়ে, packet buffer-এ লেখে, তারপর descriptor-এ দৈর্ঘ্য আর DD লেখে। Coherent প্ল্যাটফর্মে cache নিজে হালনাগাদ হয়।
  4. Interrupt: moderation-এর শর্ত পূর্ণ হলে NIC queue ৩-এর MSI-X vector-এর ঠিকানায় একটি write পাঠায়; interrupt controller সেটি core ৩-এ পৌঁছে দেয়; core ৩-এ পর্ব ৩৮-এর trap.
  5. Hard interrupt handler (top half): খুব ছোট — queue ৩-এর interrupt বন্ধ করে, NAPI poll নির্ধারণ করে আর network-এর receive softirq-এর অনুরোধ তোলে।
  6. Softirq (bottom half): interrupt চালু অবস্থায় NAPI poll বাজেট পর্যন্ত DD-ওয়ালা descriptor তোলে, প্রতিটি packet-এর জন্য kernel-এর কাঠামো (Linux-এ sk_buff) বানায়, ring-এ নতুন buffer দিয়ে tail doorbell লেখে, আর packet IP ও TCP স্তরে পাঠায়। চাপ খুব বেশি হলে এই কাজ ksoftirqd নামের kernel thread-এ সরে যায়।
  7. অ্যাপ্লিকেশন: TCP ডেটা socket-এর queue-তে রাখে আর অপেক্ষারত প্রসেসকে জাগায়; প্রসেস recv() করলে ডেটা kernel থেকে তার buffer-এ কপি হয় — পুরো যাত্রায় প্রায়ই এটিই একমাত্র CPU-কপি।
  8. শান্ত হলে: ring খালি হলে NAPI queue ৩-এর interrupt আবার চালু করে — পরের packet আবার প্রথম ধাপ থেকে।

লক্ষ করুন কোন ধাপে CPU-র কত কাজ: বাইট সরানো যন্ত্রের, ডাকার খরচ প্রতি দল-এ একবার, আর CPU-র আসল সময় যায় protocol-এর কাজে। OS-এর দিক থেকে driver, buffering আর read()-এর জীবনচক্র আছে Operating Systems কোর্সের I/O সিস্টেম পর্বে; frame কীভাবে তারে আসে, তা Computer Networks কোর্সের Ethernet আর switching পর্বে।

আরও গভীরে: কিছু server CPU-তে (Intel ২০১২ সালের দিকে Xeon-এ DDIO নামে চালু করেছিল বলে জানা যায়) NIC-এর DMA write সরাসরি শেষ স্তরের cache-এর একটি অংশে যায়, RAM ঘুরে নয় — তাই softirq packet পড়তে গিয়ে প্রায়ই cache hit পায়, তবে খুব বড় ring হলে সেই cache-অংশ উপচে পড়ে লাভ কমে। আরও এগিয়ে, CXL-এর মতো নতুন interconnect যন্ত্রকে CPU-র cache coherence-এর পূর্ণ অংশীদার করতে চায়, আর SmartNIC বা DPU network-এর অনেক কাজ CPU থেকে নিজের core-এ সরিয়ে নেয়। এসব পদ্ধতির নাম ও ক্ষমতা vendor অনুযায়ী ভিন্ন।

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

Driver বা firmware লেখায় AI কাজের, কিন্তু এই বিষয়ে তার ভুলগুলো প্রায়ই নীরব: কোড compile হয়, চলে, শুধু মাঝেমধ্যে ভুল ডেটা যায়। পরিচিত ধরনগুলো:

AI-এর সাধারণ ভুলকীভাবে ধরবেন
Head আর tail-এর দায়িত্ব উল্টে ফেলা, বা এক যন্ত্রের নিয়ম (e1000) অন্য যন্ত্রে (NVMe, virtio) চাপানোসেই যন্ত্রের datasheet বা spec-এ কে কোন সূচক লেখে দেখুন; ছোট simulator-এ full/empty চালিয়ে দেখুন
“DMA-তে cache নিয়ে ভাবতে হয় না” — সব প্ল্যাটফর্মের জন্যSoC-এর DMA coherent কি না datasheet বা device tree-তে দেখুন; DMA API ব্যবহার করুন, নিজে বাদ দেবেন না
Level-triggered লাইনে কারণ মোছার আগে complete/EOI লেখাInterrupt সংখ্যা গুনুন (Linux-এ /proc/interrupts) — হঠাৎ বিশাল হলে storm
MSI-কে “আলাদা interrupt-তার” বলা, বা MSI আর MSI-X-এর vector সীমা গুলিয়ে ফেলাPCI spec বা নির্ভরযোগ্য বইয়ের সংজ্ঞা মিলিয়ে নিন: MSI মানে memory write
CPU-শতাংশের হিসাবে clock rate দিয়ে ভাগ না করা, MB আর MiB গুলিয়ে ফেলাএকক লিখে হাতে হিসাব করুন; ছোট script দিয়ে মিলিয়ে নিন
বানানো সংখ্যা: “PCIe MMIO read সবসময় ১০০ ns”, “NAPI-র বাজেট সব kernel-এ একই”Datasheet, kernel-এর উৎস-কোড বা নিজের মাপ; না পেলে “অজানা” লিখুন

AI-কে এই বিষয়ে প্রশ্ন করার সময় প্ল্যাটফর্ম, যন্ত্রের নিয়ম আর সংখ্যা স্পষ্ট দিন, আর হিসাব দেখাতে বলুন:

আমি একটি RISC-V SoC-এর জন্য একটি network driver-এর receive অংশ বুঝতে চাই। ধরে নাও: DMA non-coherent, cache line 64 বাইট, ring-এ 256টি descriptor, যন্ত্র head বাড়ায় আর driver tail, এক ঘর খালি রাখার নিয়ম, interrupt আসে PLIC হয়ে (level-triggered). ১. ring খালি আর ভর্তির শর্ত লেখো, আর head = 250, tail = 10 হলে কয়টি descriptor যন্ত্রের হাতে — হিসাব দেখাও। ২. DMA শুরুর আগে আর packet পড়ার আগে cache-এর কোন কাজ (clean না invalidate) দরকার, কেন — Linux DMA API-র কোন call তা করে? ৩. handler-এ claim, যন্ত্রের কারণ মোছা আর complete — কোন ক্রমে, আর ক্রম ভুল হলে কী হয়? ৪. কোনো register-এর নাম বা সংখ্যা নিশ্চিত না হলে "নিশ্চিত নই" লেখো, বানিয়ো না।

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

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

  • Polling কখন interrupt-এর চেয়ে ভালো? — ঘটনার হার যখন crossover r* = Cpoll ÷ (P × Cirq)-এর চেয়ে বেশি, বা latency খুব কম রাখতে একটি core উৎসর্গ করা যায়।
  • Level আর edge-triggered interrupt-এর পার্থক্য? — Level: কারণ না মোছা পর্যন্ত সক্রিয়, লাইন ভাগ করা যায়, না মুছলে storm; edge: একটি বদল একটি ঘটনা, ঘটনা হারাতে পারে।
  • DMA থাকলে CPU-র কী কাজ থাকে? — Buffer আর descriptor প্রস্তুত, doorbell, completion interrupt সামলানো, cache আর ম্যাপিং — প্রতি transfer-এ, প্রতি বাইটে নয়।
  • Ring-এ এক ঘর খালি রাখা হয় কেন? — নইলে head = tail খালি আর ভর্তি দুটোই বোঝাত।
  • IOMMU কেন দরকার? — যন্ত্রের ঠিকানা অনুবাদ (scatter, ৩২-বিট যন্ত্র, VM passthrough) আর খারাপ যন্ত্রের DMA আটকানো।

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

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

প্র ০১পুরোনো PC-তে IRQ লাইন নিয়ে এত ঝামেলা কেন ছিল, আর MSI কীভাবে সমস্যাটি প্রায় মুছে দিল?

১৯৮১ সালের IBM PC-তে interrupt-এর জন্য ছিল একটি Intel 8259A — আটটি লাইন। Timer আর কি-বোর্ড দুটি নিত, বাকিগুলো serial port, printer, floppy, hard disk-এর মধ্যে ভাগ হতো। ১৯৮৪ সালের PC/AT-এ দ্বিতীয় একটি 8259A জুড়ে মোট ১৫টি ব্যবহারযোগ্য লাইন হলো। কিন্তু ISA-র লাইন edge-triggered, ভাগ করার জন্য নকশা করা নয়। তাই প্রতিটি sound card, network card বা modem-কে নিজস্ব IRQ দিতে হতো, প্রায়ই jumper দিয়ে হাতে; দুটি কার্ড একই IRQ নিলে একটি কাজ করত না, বা কম্পিউটার জমে যেত। ১৯৯০-এর দশকের “Plug and Play” বাছাই স্বয়ংক্রিয় করল, কিন্তু লাইন তো বাড়ল না।

PCI এল level-triggered, ভাগ করা যায় এমন লাইন নিয়ে (INTA# থেকে INTD#). সংঘর্ষ কমল, কিন্তু একটি লাইনে কয়েকটি যন্ত্র থাকলে প্রতিটি interrupt-এ kernel-কে সবার handler ডেকে খুঁজতে হতো কে ডেকেছে — প্রতিবার একটি ধীর MMIO read. Multiprocessor server-এ I/O APIC আরও লাইন আর যেকোনো core-এ পাঠানোর ক্ষমতা দিল, কিন্তু তারের জট রয়ে গেল।

মৌলিক পরিবর্তন আনে MSI, যা PCI-এর spec-এ ১৯৯০-এর দশকের শেষ দিকে যোগ হয় বলে জানা যায়, আর পরে MSI-X. ধারণাটি সরল কিন্তু শক্তিশালী: interrupt কোনো তার নয়, একটি memory write. তারের সংখ্যার সীমা উঠে গেল — একটি যন্ত্রই MSI-X-এ হাজারের বেশি vector নিতে পারে। ভাগাভাগি নেই, তাই “কে ডাকল” খোঁজার খরচ নেই। প্রতিটি vector আলাদা core-এ পাঠানো যায়, যা multi-queue network card আর NVMe-র মতো যন্ত্রকে সম্ভব করেছে। আর PCIe-র ক্রমের নিয়মে interrupt ডেটার পরে পৌঁছায়, তাই একটি সূক্ষ্ম race-ও দূর হলো। PCI Express তো শেষ পর্যন্ত আলাদা interrupt-তারই রাখেনি; পুরোনো INTx-কে message দিয়ে অনুকরণ করে শুধু পুরোনো software-এর জন্য।

মূল উপলব্ধি: সীমিত ভৌত সম্পদ (তার) দিয়ে শুরু হওয়া নকশা সফটওয়্যারে জটিলতা ঠেলে দেয়; interrupt-কে একটি সাধারণ memory write বানিয়ে ফেলায় সমস্যাটি ঠিকানা আর ডেটার প্রশ্নে নেমে এল, যা bus এমনিতেই ভালো পারে।

প্র ০২“Interrupt দ্রুত” — তাহলে বেশি চাপে interrupt-চালিত সিস্টেম কেন ভেঙে পড়ে, আর সমাধান কীভাবে এল?

Interrupt-এর জন্মই হয়েছিল polling-এর অপচয় দূর করতে: ১৯৫০-এর দশকের যন্ত্রগুলো ধীর tape আর printer-এর জন্য অপেক্ষা করে সময় নষ্ট করত, আর interrupt বলল “কাজ শেষে আমিই ডাকব”। কম হারের যন্ত্রে এই চুক্তি দারুণ। কিন্তু ১৯৯০-এর দশকে network-এর গতি CPU-র তুলনায় দ্রুত বাড়ল, আর একটি অদ্ভুত আচরণ চোখে পড়ল: বোঝা বাড়ালে সিস্টেমের কাজের হার একটি বিন্দু পর্যন্ত বাড়ে, তারপর কমতে থাকে — চরম অবস্থায় শূন্যের কাছে। Jeffrey Mogul আর K. K. Ramakrishnan ১৯৯৬ সালের একটি গবেষণাপত্রে (পরে ১৯৯৭ সালে journal-এ) এর নাম দেন receive livelock এবং বিশ্লেষণ করেন।

কারণটা priority-র উল্টো ফল। Interrupt handler সবচেয়ে উঁচু priority পায়; সে packet-টি একটি queue-তে রাখে, আর পরে নিচু priority-র protocol-প্রক্রিয়া আর অ্যাপ্লিকেশন সেটি নেয়। হার যথেষ্ট বেশি হলে CPU-র সব সময় যায় handler-এ; queue ভরে যায়, নতুন packet ফেলে দিতে হয় — অথচ সেগুলো গ্রহণ করতেই CPU খরচ হয়েছে। এই পর্বের সূত্রে: r × (Cirq + Cwork) যখন f-এর কাছে পৌঁছায়, আর কিছুর জন্য cycle থাকে না।

সমাধানগুলোর মূল কথা একটাই — চাপ বাড়লে interrupt কমানো। Mogul আর Ramakrishnan চাপের সময় interrupt বন্ধ করে polling-এ যাওয়ার প্রস্তাব দেন। Linux-এ এই ধারণা NAPI নামে ২০০১–২০০৩ সালের দিকে আসে বলে জানা যায়: প্রথম packet-এ interrupt, তারপর বাজেটসহ polling, ring খালি হলে আবার interrupt. NIC-গুলো hardware-এ interrupt moderation যোগ করে — সংখ্যা বা সময় ধরে জমানো, পরে adaptive. সাথে বড় receive ring, যাতে burst সামলানো যায়, আর multi-queue, যাতে কাজ কয়েকটি core-এ ভাগ হয়।

মূল উপলব্ধি: Interrupt কম হারে latency আর দক্ষতা দুটোই দেয়, কিন্তু ঘটনাপ্রতি খরচ বলে উচ্চ হারে তা নিজেই বোঝা হয়ে দাঁড়ায়; আধুনিক সিস্টেম তাই হার অনুযায়ী interrupt আর polling-এর মধ্যে নিজেকে বদলায়।

প্র ০৩একটি ছোট USB-C বা Thunderbolt যন্ত্র কেন নিরাপত্তার প্রশ্ন তোলে, আর IOMMU কতটা রক্ষা করে?

DMA-র পুরো উদ্দেশ্য হলো CPU-কে না জিজ্ঞেস করে মেমরিতে পৌঁছানো। বাক্সের ভেতরের যন্ত্রের ক্ষেত্রে এটা বিশ্বাসের ব্যাপার ছিল — মালিকই সেগুলো বসিয়েছেন। কিন্তু FireWire (IEEE 1394) বাইরের port থেকেই DMA-ক্ষমতা দিত, আর ২০০০-এর দশকের মাঝামাঝি থেকে গবেষকেরা জনসমক্ষে দেখিয়েছেন যে এমন port-এ লাগানো যন্ত্র ধারণাগতভাবে চলমান কম্পিউটারের মেমরি পড়তে পারে — password আর disk encryption-এর চাবিও মেমরিতেই থাকে। Thunderbolt (২০১১ সালের দিকে বাজারে) PCIe-কেই বাইরের তারে নিয়ে এল, তাই প্রশ্নটি নতুন করে উঠল।

প্রতিরক্ষার কেন্দ্র IOMMU. চালু থাকলে প্রতিটি যন্ত্রের DMA একটি অনুবাদ-টেবিল পার হয়; driver যে buffer ম্যাপ করেছে, শুধু সেগুলোতেই পৌঁছানো যায়, বাকি সব fault. কিন্তু দীর্ঘদিন IOMMU থাকলেও অনেক সিস্টেমে ডিফল্টে বন্ধ ছিল — পারফরম্যান্সের খরচ, পুরোনো driver-এর সাথে অসামঞ্জস্য, আর boot-এর শুরুতে, OS IOMMU চালু করার আগে, একটি অরক্ষিত সময় থাকত। ২০১৯ সালের দিকে Thunderclap নামে একটি গবেষণা দেখায় যে IOMMU চালু থাকলেও OS-গুলোর ব্যবহারের ধরনে ফাঁক থাকে: যেমন যন্ত্রের buffer আর অন্য সংবেদনশীল kernel ডেটা একই page-এ থাকলে যন্ত্র সেই page-এর সবটাই দেখে; IOTLB দেরিতে মোছার নীতিতে ছোট সময়ের জানালা খোলা থাকে।

এর জবাবে শিল্প কয়েক স্তরে এগিয়েছে, যার খুঁটিনাটি hardware আর OS-এর version অনুযায়ী ভিন্ন: Windows-এর Kernel DMA Protection, macOS আর Linux-এ বাইরের যন্ত্রের জন্য কঠোর IOMMU নীতি, firmware-এ boot-এর আগেই DMA সুরক্ষা, আর lock থাকা অবস্থায় নতুন যন্ত্রকে অনুমতি না দেওয়া। Driver লেখকদের শিক্ষা: DMA buffer আলাদা page-এ রাখা, যত কম সময় সম্ভব ম্যাপ রাখা, আর যন্ত্রের লেখা descriptor-কে অবিশ্বস্ত input ধরে যাচাই করা। ব্যবহারকারীর জন্য: OS আর firmware হালনাগাদ রাখা, IOMMU চালু রাখা, অচেনা যন্ত্র না লাগানো।

মূল উপলব্ধি: DMA গতির জন্য CPU-র নিরাপত্তা-চৌকি এড়িয়ে যায়; IOMMU সেই চৌকি যন্ত্রের পথে ফিরিয়ে আনে, কিন্তু সুরক্ষা তখনই কার্যকর যখন এটি শুরু থেকে চালু, সূক্ষ্মভাবে ম্যাপ করা, আর OS-এর নীতির সাথে মেলানো।

প্র ০৪x86 server-এ DMA নিজেই cache-coherent, তবু অনেক embedded chip-এ driver-কে cache flush/invalidate করতে হয় কেন?

Coherent DMA মানে যন্ত্রের প্রতিটি memory access CPU-র cache-ব্যবস্থাকে জানাতে হবে — snoop করতে হবে বা একটি directory দেখতে হবে। এর জন্য interconnect-এ বাড়তি পথ, cache-এ বাড়তি port বা tag দেখার ক্ষমতা, আর প্রতিটি DMA-তে বাড়তি সংকেত লাগে। x86 PC আর server-এ এই খরচ ঐতিহাসিকভাবে মেনে নেওয়া হয়েছে: বিশাল software-জগৎ ধরে নেয় DMA coherent, আর server-এর মানদণ্ডে কিছু বাড়তি transistor তুচ্ছ। Intel-এর মতো নকশায় NIC-এর লেখা সরাসরি শেষ স্তরের cache-এ পাঠানোর ব্যবস্থাও এসেছে, যা coherence ছাড়া সম্ভবই না।

একটি ছোট SoC-এর হিসাব আলাদা। ক্যামেরা, display, audio-র DMA engine প্রায়ই সরাসরি memory controller-এ যায়; তাদের ডেটা বড়, একটানা আর CPU হয়তো ছোঁয়ই না — যেমন ক্যামেরা থেকে video encoder. সেখানে সবকিছু CPU-র cache দিয়ে snoop করানো area, power আর bandwidth নষ্ট করে। ARM-ভিত্তিক বহু SoC তাই কিছু যন্ত্রকে coherent পথে (ARM-এর ACE বা ACP-এর মতো interconnect ব্যবস্থা) আর কিছুকে non-coherent পথে রাখে। RISC-V-এও প্রথম দিকের অনেক chip-এ non-coherent DMA ছিল বলে জানা যায়, আর cache-management-এর জন্য মানসম্মত instruction (Zicbom) পরে, ২০২১–২২ সালের দিকে, ratified হয়; তার আগে প্রতিটি vendor নিজস্ব উপায় দিত।

Software-কে প্ল্যাটফর্ম-নিরপেক্ষ রাখতে Linux-এর DMA API দুই রকম ম্যাপিং দেয়: “coherent” বরাদ্দ (যেমন descriptor ring-এর জন্য), যা non-coherent প্ল্যাটফর্মে uncached মেমরি দিয়ে বানানো হতে পারে; আর “streaming” ম্যাপিং (packet buffer-এর জন্য), যেখানে API প্রতিটি transfer-এর আগে-পরে দরকার হলে clean বা invalidate করে। যে driver শুধু x86-এ পরীক্ষা করা হয়েছে আর sync call বাদ দিয়েছে, সে ARM বোর্ডে নীরবে ভুল ডেটা দিতে পারে — embedded জগতের পরিচিত bug.

মূল উপলব্ধি: Coherence একটি প্রকৌশল-বাণিজ্য: hardware-এ দিলে software সরল আর নিরাপদ, কিন্তু chip বড় ও বেশি power-খেকো; software-এ ছাড়লে chip সস্তা, কিন্তু প্রতিটি driver-কে নির্ভুলভাবে cache সামলাতে হয়।

প্র ০৫একটি register-এ একটি transfer থেকে NVMe-র হাজার হাজার queue — DMA-র নকশা এভাবে বদলাল কেন, আর polling কেন আবার ফিরে এল?

IBM PC-র Intel 8237-এ প্রতিটি channel-এ একটি ঠিকানা আর একটি count register ছিল: একটি transfer, শেষে interrupt, তারপর পরেরটি। Floppy-র জন্য যথেষ্ট, কিন্তু দ্রুত যন্ত্রে প্রতিটি কাজের মাঝের CPU-হস্তক্ষেপ বড় অপচয়। Bus mastering আর descriptor ring আনল মৌলিক পরিবর্তন: কাজের তালিকা মেমরিতে, যন্ত্র নিজে পড়ে, CPU শুধু tail এগিয়ে দেয় — একটি doorbell-এ অনেক কাজ, একটি interrupt-এ অনেক ফল।

Flash storage এই নকশাকে চরমে নিল। পুরোনো SATA AHCI-তে একটি command queue-তে ৩২টি command ছিল — ঘোরা disk-এর জন্য যথেষ্ট, যার প্রতিটি seek মিলিসেকেন্ডের। কিন্তু SSD মাইক্রোসেকেন্ডে কাজ শেষ করে, আর ভেতরে অনেক flash chip একসাথে চালাতে পারে। NVMe (২০১১ সালের দিকে প্রথম spec) সাজানো হলো ring-কে কেন্দ্র করে: প্রায় ৬৫ হাজার পর্যন্ত I/O queue, প্রতিটিতে প্রায় ৬৫ হাজার পর্যন্ত entry, প্রতিটি submission queue-র সাথে completion queue, doorbell register, আর queue-প্রতি MSI-X. লক্ষ্য ছিল প্রতিটি core নিজের queue পাবে — core-দের মধ্যে lock বা ভাগাভাগি ছাড়াই।

আর তখনই পুরোনো সমীকরণ উল্টে গেল। Disk-এর মিলিসেকেন্ডের পাশে কয়েক মাইক্রোসেকেন্ডের interrupt latency তুচ্ছ ছিল; কিন্তু যন্ত্র নিজেই ১০ মাইক্রোসেকেন্ডে উত্তর দিলে interrupt-এর প্রবেশ, context switch আর cache-এর ক্ষতি মোট সময়ের বড় অংশ। তাই polling ফিরল, বুদ্ধিমানভাবে: Linux-এ NVMe-র জন্য poll-ভিত্তিক completion আর “hybrid” polling ২০১৬–১৭ সালের দিকে এসেছে বলে জানা যায়; DPDK আর SPDK-এর মতো framework কিছু core-কে পুরোপুরি polling-এ রেখে kernel-কেই পাশ কাটায়। দাম: সেই core-গুলো সবসময় ১০০% ব্যস্ত — power-এর দিক থেকে বড় খরচ।

মূল উপলব্ধি: যন্ত্রের গতি বদলালে “সেরা” I/O পদ্ধতিও বদলায় — ring কমিয়েছে প্রতি-কাজের CPU-হস্তক্ষেপ, আর যন্ত্র যখন interrupt-এর চেয়েও দ্রুত, তখন polling আবার যুক্তিসঙ্গত; এই পর্বের crossover সূত্র ঠিক এই সিদ্ধান্তই মাপে।

অনুশীলন

পরীক্ষার ধাঁচে: polling, interrupt আর DMA-র CPU-শতাংশ, ring-এর সূচক, coalescing-এর latency আর একটি আউটপুট অনুমান। আগে নিজে চেষ্টা করুন।

  1. CPU-শতাংশের হিসাব: ৩ GHz-এর একটি CPU. Polling: প্রতি ১০ µs-এ একবার, প্রতিটি ৩০০ cycle. Interrupt: প্রতিটিতে বাড়তি ২৫০০ cycle. প্রতিটি ঘটনার আসল কাজ ৫০০ cycle. (ক) একটি কি-বোর্ড (সেকেন্ডে ২০টি ঘটনা) আর একটি network card (সেকেন্ডে ২,০০,০০০ packet) — প্রতিটির জন্য polling আর interrupt-এ CPU-র কত শতাংশ? (খ) Crossover হার কত? (গ) একটি ৪০০ MB/s (৪০ কোটি বাইট/সেকেন্ড) disk-এর ডেটা PIO-তে ৪ বাইটের word-এ, প্রতি word-এ ২০ cycle-এ সরালে CPU-র কত শতাংশ? DMA-তে ১৬ KiB block-এ, প্রতি block-এ setup ১৫০০ আর completion ৩৫০০ cycle হলে? Break-even block-আকার কত?

    Polling-এর স্থির কর: সেকেন্ডে ১,০০,০০০ poll × ৩০০ = ৩ কোটি cycle = ৩০০ কোটির ১%.

    (ক) কি-বোর্ড — interrupt: ২০ × (২৫০০ + ৫০০) = ৬০,০০০ cycle = ০.০০২%; polling: ১% + ২০ × ৫০০ ÷ ৩০০ কোটি ≈ ১.০০০৩%. Interrupt প্রায় ৫০০ গুণ সস্তা। Network card — interrupt: ২,০০,০০০ × ৩০০০ = ৬০ কোটি cycle = ২০%; polling: ১% + ২,০০,০০০ × ৫০০ = ১০ কোটি cycle = ৩.৩৩%, মোট প্রায় ৪.৩৩%. এখানে polling প্রায় সাড়ে চার গুণ সস্তা।

    (খ) r* = Cpoll ÷ (P × Cirq) = ৩০০ ÷ (১০ µs × ২৫০০) = ৩০০ ÷ ০.০২৫ = সেকেন্ডে ১২,০০০টি ঘটনা. কি-বোর্ড এর অনেক নিচে, network card অনেক ওপরে — (ক)-এর ফলের সাথে মেলে।

    (গ) PIO: ৪০ কোটি ÷ ৪ = ১০ কোটি word × ২০ = ২০০ কোটি cycle = ৬৬.৬৭%. DMA: ৪০ কোটি ÷ ১৬৩৮৪ ≈ ২৪,৪১৪টি block/সেকেন্ড × ৫০০০ ≈ ১২.২১ কোটি cycle = প্রায় ৪.০৭%. Break-even: B* = ৫০০০ × ৪ ÷ ২০ = ১০০০ বাইট. (সব মান একটি ছোট script দিয়ে মিলিয়ে দেখা।)

  2. Ring-এর সূচক (হাতে trace): ১৬ ঘরের একটি transmit ring; driver tail বাড়ায়, যন্ত্র head; খালি: head = tail, ভর্তি: (tail + ১) mod ১৬ = head. এখন head = ১৩, tail = ৫. (ক) যন্ত্রের হাতে কয়টি descriptor? (খ) Driver আর কয়টি দিতে পারবে? (গ) Driver সেই সংখ্যক দিলে tail কত, ring কি ভর্তি? (ঘ) এরপর যন্ত্র ১০টি পাঠাতে চাইলে কয়টি পারবে, head আর ব্যবহৃত ঘর কত হবে? (ঙ) ১৬টি ঘরের সবগুলো ব্যবহার করতে চাইলে কী বদলাতে হবে?

    (ক) (৫ − ১৩ + ১৬) mod ১৬ = ৮টি (ঘর ১৩, ১৪, ১৫, ০, ১, ২, ৩, ৪)। (খ) সর্বোচ্চ ১৫টি ব্যবহার করা যায়, তাই ১৫ − ৮ = ৭টি. (গ) tail = (৫ + ৭) mod ১৬ = ১২; (১২ + ১) mod ১৬ = ১৩ = head, তাই ভর্তি. (ঘ) যন্ত্রের হাতে ১৫টি, তাই ১০টিই পারবে: head = (১৩ + ১০) mod ১৬ = ৭, ব্যবহৃত = (১২ − ৭ + ১৬) mod ১৬ = ৫টি. (ঙ) head = tail তখন খালি আর ভর্তি দুটোই হতে পারে, তাই বাড়তি তথ্য লাগবে: একটি আলাদা count, বা সূচক mod ১৬ না করে বড় পরিসরে বাড়তে দেওয়া (ব্যবহৃত = tail − head, ঘর = সূচক mod ১৬), বা NVMe-র মতো phase বিট।

  3. Coalescing-এর latency: একটি NIC-এ packet আসে ঠিক প্রতি ২ µs-এ একটি (সেকেন্ডে ৫,০০,০০০)। ২ GHz CPU-তে প্রতিটি interrupt-এর বাড়তি খরচ ৪০০০ cycle. (ক) Coalescing ছাড়া শুধু interrupt-এর বাড়তি খরচ CPU-র কত শতাংশ? (খ) নিয়ম “১৬টি packet জমলে, অথবা প্রথমটির ১০০ µs পরে” হলে সেকেন্ডে কয়টি interrupt, খরচ কত শতাংশ, আর একটি দলের প্রথম packet কত অপেক্ষা করে, গড়ে কত (interrupt-এর নিজস্ব latency বাদে)? (গ) রাতে traffic কমে প্রতি ১ ms-এ একটি packet হলে একই নিয়মে latency আর খরচ কত?

    (ক) ৫,০০,০০০ × ৪০০০ = ২০০ কোটি cycle = ২ GHz-এর ১০০% — শুধু ঢোকা-বেরোনোতেই পুরো CPU শেষ; আসল কাজের জন্য কিছু নেই। এটি receive livelock-এর অবস্থা।

    (খ) ১৬টি packet জমতে লাগে ১৫ × ২ = ৩০ µs, যা ১০০ µs-এর আগে — তাই সংখ্যার শর্তেই interrupt. সেকেন্ডে ৫,০০,০০০ ÷ ১৬ = ৩১,২৫০টি interrupt × ৪০০০ = ১২.৫ কোটি cycle = ৬.২৫%. প্রথম packet অপেক্ষা করে ৩০ µs, শেষটি ০; অপেক্ষাগুলো ৩০, ২৮, …, ০ — গড় ১৫ µs.

    (গ) এখন ১৬টি জমতে ১৫ ms লাগবে, তাই ১০০ µs-এর টাইমারই প্রতিবার আগে পূর্ণ হয়: প্রতিটি packet একা, latency ১০০ µs, সেকেন্ডে ১০০০টি interrupt × ৪০০০ = ৪০ লাখ cycle = ০.২%. খরচ সামান্য, কিন্তু latency অকারণে ১০০ µs — যেখানে coalescing ছাড়া হতো কয়েক µs. এজন্যই adaptive moderation: হার কম হলে টাইমার ছোট করা।

  4. আউটপুট অনুমান: অংশ ১-এর SIGALRM প্রোগ্রাম থেকে কেউ volatile শব্দটি মুছে ফেলল, আর gcc -O2 দিয়ে compile করল। (ক) কী ঘটতে পারে, কেন? (খ) আরেকজন handler-এর ভেতরেই printf ডাকল আর flag বাদ দিল — এতে সমস্যা কী? (গ) এই দুই ভুলের hardware-driver জগতে সমতুল্য কী?

    (ক) প্রোগ্রাম হয়তো একটি “tick”-ও ছাপবে না আর কখনো শেষ হবে না। volatile ছাড়া compiler দেখে লুপের ভেতরে tick বদলায় এমন কোনো কোড নেই (handler-কে লুপ ডাকে না), তাই সে tick একবার register-এ পড়ে রেখে লুপে আর মেমরি দেখতে না-ও পারে। Handler মেমরিতে ১ লিখলেও লুপ তা দেখবে না। এটা অবশ্যই ঘটবে, এমন নয় — compiler আর তার version-এর ওপর নির্ভর করে; কিন্তু ভাষার নিয়মে এমন optimization বৈধ, তাই কোডটি ভুল।

    (খ) printf async-signal-safe নয়: signal যদি মূল প্রোগ্রামের একটি printf-এর মাঝখানে আসে, handler-এর printf একই অভ্যন্তরীণ buffer আর lock-এ হাত দেবে — output এলোমেলো হতে পারে বা প্রোগ্রাম আটকে যেতে পারে। নিয়ম: handler-এ শুধু flag বসানো বা async-signal-safe function (যেমন write).

    (গ) (ক)-এর সমতুল্য: যন্ত্রের MMIO status register বা descriptor-এর DD বিট সাধারণ ভ্যারিয়েবলের মতো পড়া — তাই Linux-এর মতো kernel-এ MMIO-র জন্য বিশেষ accessor (readl ইত্যাদি) আর memory barrier ব্যবহার হয়। (খ)-এর সমতুল্য: interrupt handler-এ ঘুমাতে পারে বা ভারী lock নেয় এমন কাজ — সেজন্যই top half ছোট, বাকিটা softirq বা thread-এ।

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

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

ক্রমএকটি network card-এ DMA দিয়ে packet গ্রহণের ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🧺 Driver খালি buffer-এর ঠিকানা descriptor-এ লিখে tail doorbell বাজায়
  2. 📨 Packet আসে; NIC head-এর descriptor DMA দিয়ে পড়ে
  3. 🚚 NIC packet-এর বাইটগুলো DMA দিয়ে buffer-এ লেখে
  4. ✅ NIC descriptor-এ দৈর্ঘ্য আর DD লেখে, head বাড়ায়
  5. 🔔 NIC MSI-X পাঠায় (একটি memory write)
  6. 🧰 Driver DD দেখে packet তোলে, নতুন buffer দিয়ে tail বাড়ায়

PCIe-র MSI (Message Signaled Interrupt) আসলে কী?

  • যন্ত্র থেকে interrupt controller-এ একটি আলাদা তার
  • যন্ত্রের করা একটি memory write, যার ঠিকানা interrupt controller-এর আর ডেটায় vector
  • CPU-র নির্দিষ্ট সময় পরপর যন্ত্রের status পড়া
  • Kernel থেকে প্রসেসে পাঠানো একটি signal

৮ ঘরের একটি descriptor ring-এ নিয়ম: খালি হলে head = tail, ভর্তি হলে (tail + ১) mod ৮ = head. এখন head = ৩, tail = ২. Ring-এর অবস্থা কী?

  • খালি
  • ১টি ঘর ব্যবহৃত
  • ৮টি ঘরই ব্যবহৃত
  • ভর্তি — ৭টি ঘর ব্যবহৃত

প্রতি ১০ µs-এ poll করলে প্রতিবার ৫০০ cycle লাগে; প্রতিটি interrupt-এর বাড়তি খরচ ২৫০০ cycle. প্রতি ঘটনার আসল কাজ দুই পদ্ধতিতে একই। কোন ঘটনা-হারের ওপরে polling সস্তা?

  • সেকেন্ডে ২,০০,০০০
  • সেকেন্ডে ০.২
  • সেকেন্ডে ২০,০০০
  • সেকেন্ডে ২,০০০

একটি non-coherent DMA প্ল্যাটফর্মে যন্ত্র একটি receive buffer-এ DMA দিয়ে ডেটা লিখেছে। CPU সেই buffer পড়ার আগে driver-কে cache নিয়ে কী করতে হবে?

  • Buffer-এর cache line invalidate করা
  • শুধু cache line clean (write back) করা
  • কিছুই না — DMA শেষ হলেই cache নিজে হালনাগাদ হয়
  • সব interrupt বন্ধ করা

IOMMU-র প্রধান কাজ কোনটি?

  • CPU-র load/store-এর virtual address অনুবাদ করা
  • যন্ত্রের DMA ঠিকানা অনুবাদ করা আর অননুমোদিত ঠিকানায় DMA আটকানো
  • Interrupt-এর priority ঠিক করা
  • DMA-র সময় cache coherent রাখা

একটি ২ GHz CPU একটি যন্ত্রের ২০০ MB/s (২০ কোটি বাইট/সেকেন্ড) ডেটা programmed I/O-তে ৮ বাইটের word-এ সরায়, প্রতি word-এ ৪০ cycle. CPU-র কত শতাংশ এতে যায়?

  • ৪০০%
  • ৫%
  • ৫০%
  • ২৫%

একটি level-triggered IRQ লাইনে driver-এর handler PLIC-এ complete লিখল, কিন্তু যন্ত্রের status register-এর কারণ-বিট মুছতে ভুলে গেল। কী ঘটবে?

  • কিছুই না — একটি complete-ই যথেষ্ট
  • পরের ঘটনাটি হারিয়ে যাবে
  • PLIC নিজেই যন্ত্রের কারণ-বিট মুছে দেবে
  • সাথে সাথে আবার একই interrupt আসবে, বারবার — interrupt storm

শব্দকোষ

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

মনে রাখুন

  1. Polling = স্থির কর (Cpoll ÷ P), interrupt = ঘটনাপ্রতি কর (r × Cirq); crossover r* = Cpoll ÷ (P × Cirq). উচ্চ হারে coalescing আর NAPI-ধাঁচের মিশ্রণ; দাম latency.
  2. যন্ত্রের দিক: IRQ লাইন (level: কারণ মোছা পর্যন্ত, ভাগ করা যায়, storm-এর ঝুঁকি; edge: একটি বদল একটি ঘটনা) → controller (জড়ো, priority, mask, routing) → PLIC-এ claim → যন্ত্রের সেবা → complete.
  3. MSI/MSI-X = interrupt একটি memory write; ভাগাভাগি নেই, queue-প্রতি vector, যেকোনো core-এ (IRQ affinity), আর ডেটার পরে পৌঁছানোর নিশ্চয়তা।
  4. DMA: CPU-র খরচ প্রতি transfer-এ (setup + completion), প্রতি বাইটে নয়; break-even B* = (Csetup + Cdone) × w ÷ cw. Descriptor ring: খালি head = tail, ভর্তি (tail + ১) mod N = head; একটি doorbell-এ অনেক কাজ; scatter-gather.
  5. DMA-র ফাঁদ: cache (coherent hardware, নইলে যন্ত্র পড়ার আগে clean, CPU পড়ার আগে invalidate, line-aligned buffer) আর ঠিকানা (physical/IO address, pinned page, bounce buffer, IOMMU — অনুবাদ আর DMA আক্রমণ থেকে সুরক্ষা)।

পরের পর্বে: এই পর্বে যন্ত্র আর CPU-র cache-এর মিল রাখার প্রশ্ন এসেছে। এবার একাধিক core, প্রত্যেকের নিজের cache, একই ঠিকানা — MESI protocol কীভাবে সবাইকে একই মান দেখায়, আর memory consistency model কী প্রতিশ্রুতি দেয়।

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

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