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

I/O আর bus — memory-mapped I/O, port I/O, PCIe আর USB

I/O and Buses — Device Registers, Port-Mapped vs Memory-Mapped I/O, Polling, Bus Bandwidth, PCI Express Lanes and Encoding, USB Enumeration and Little's Law

Taught in Bangla, this lesson covers I/O and buses: device controllers with status, control and data registers, port-mapped versus memory-mapped I/O, uncached regions, volatile and fences, polling a UART in C, RISC-V assembly and a simulator, polling cost, bus timing, arbitration and bandwidth, the move to serial links, PCI Express lanes, encoding, packets and BARs, USB enumeration, SATA, NVMe, I2C and CXL, and Little's law.

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

  • I/O কেন কঠিন, device controller আর তার status, control ও data register কী কাজ করে
  • Port-mapped আর memory-mapped I/O-র পার্থক্য, uncached অঞ্চল, volatile আর RISC-V-এর fence-এর ভূমিকা
  • একটি 16550-ধাঁচের UART-এ polling দিয়ে byte পাঠানো — C, RISC-V assembly আর চালানো যায় এমন simulator দিয়ে
  • Polling-এর খরচ, bus-এর bandwidth, PCIe-এর encoding-সহ কার্যকর bandwidth আর Little-এর সূত্রের পরীক্ষার ধাঁচের হিসাব
  • PCIe-এর গঠন (lane, TLP, root complex, BAR), USB-এর enumeration আর transfer-এর ধরন, আর SATA, NVMe, I2C, CXL-এর জায়গা

১ · I/O-র সমস্যা — যন্ত্র ভিন্ন, গতি ভিন্ন, আর আসে অসময়ে

এ পর্যন্ত আমরা CPU আর মেমরির ভেতরে ছিলাম। কিন্তু কাজ শুরু হয় বাইরে থেকে — কীবোর্ডের চাপ, network-এর packet — আর শেষ হয় বাইরে: পর্দা, স্পিকার, SSD. CPU আর মেমরির বাইরের এই যোগাযোগই I/O (input/output). মডিউল ৯-এর এই প্রথম পর্বে দেখব, CPU কীভাবে যন্ত্রের সাথে কথা বলে, আর তাদের যুক্ত করার পথ — bus আর serial link — কীভাবে কাজ করে।

I/O কেন আলাদা মনোযোগ চায়? তিনটি কারণ:

  • যন্ত্রগুলো খুবই ভিন্ন। কীবোর্ড, mouse, প্রিন্টার, network card, GPU, SSD, তাপমাত্রার sensor — প্রত্যেকের নিজস্ব নিয়ম। CPU-র ISA প্রতিটি যন্ত্রের জন্য আলাদা instruction রাখতে পারে না; দরকার একটি সাধারণ পদ্ধতি।
  • গতির পার্থক্য বিশাল। একজন মানুষ সেকেন্ডে বড়জোর ১০টির মতো key চাপেন — কয়েক byte/s. একটি আধুনিক NVMe SSD কয়েক GB/s দিতে পারে। মাঝখানে প্রায় ১০৯ গুণের ফারাক। কিছু যন্ত্র CPU-র তুলনায় ভীষণ ধীর, কিছু এত দ্রুত যে CPU-ই হয়ে যায় বাধা।
  • ঘটনা আসে অসময়ে (asynchronous). কখন key চাপা হবে বা packet আসবে, প্রোগ্রাম জানে না। CPU-র clock আর যন্ত্রের clock আলাদা; যন্ত্রের নিজের সময়ে কাজ শেষ হয়।

একটি ব্যস্ত রেস্তোরাঁর রান্নাঘর ভাবুন। প্রধান রাঁধুনি (CPU) খুব দ্রুত কাজ করেন। বাইরের সাথে যোগাযোগ চলে কয়েকটি জানালা দিয়ে — অর্ডার নেওয়ার কাউন্টার, বাজার থেকে মাল আসার দরজা, ডেলিভারির জানালা। প্রতিটি জানালায় একজন সহকারী (controller) আছেন, যাঁর সামনে তিনটি জিনিস: একটি বাতি (“তৈরি / ব্যস্ত” — status), একটি নির্দেশের খাতা (“আজ ২০ প্লেট বিরিয়ানি” — control), আর একটি ট্রে (যেখানে মাল রাখা বা নেওয়া হয় — data). রাঁধুনি কোনো জানালার ভেতরের কাজ জানেন না; তিনি শুধু বাতি দেখেন, খাতায় লেখেন, ট্রেতে রাখেন বা তোলেন। আর সমস্যা হলো — চা-দোকানের ছেলেটি কখন অর্ডার নিয়ে আসবে, রাঁধুনি জানেন না।

তিনটি প্রশ্ন: (১) CPU একটি যন্ত্রের register-কে কোন ঠিকানায় চেনে? (২) যন্ত্র কখন তৈরি, CPU কীভাবে জানে — এখানে polling, পরের পর্বে interrupt ও DMA. (৩) ডেটা তারে কীভাবে যায় — bus, PCIe, USB. Driver আর buffering-এর software দিক OS কোর্সের I/O সিস্টেম পর্বে আছে।

২ · Device controller আর device register — status, control, data

প্রতিটি যন্ত্রের সাথে CPU সরাসরি কথা বলে না; বলে তার device controller-এর সাথে। Controller হলো যন্ত্রের পাশে বা chip-এর ভেতরে বসানো একটি ছোট digital circuit — কখনো সাধারণ FSM (পর্ব ১৭), কখনো নিজস্ব firmware চালানো ছোট একটি processor. সে যন্ত্রের ভেতরের জটিলতা (motor ঘোরানো, flash-এর block মোছা, ethernet-এর signal) সামলায়, আর CPU-র দিকে কয়েকটি সরল device register দেখায়। সাধারণত তিন ধরনের:

Status register

যন্ত্রের অবস্থা — তৈরি কি না, ডেটা এসেছে কি না, ভুল হয়েছে কি না। CPU সাধারণত শুধু পড়ে। যেমন UART-এর “পাঠানোর জায়গা খালি” বিট।

Control register

CPU যন্ত্রকে নির্দেশ দেয় — গতি (baud rate), কোন mode, interrupt চালু কি না, “কাজ শুরু” বিট। CPU লেখে।

Data register

আসল ডেটার জানালা — CPU এখানে byte লেখে পাঠাতে, বা পড়ে নিতে। প্রায়ই পেছনে একটি FIFO buffer থাকে।

Doorbell ও queue

দ্রুত যন্ত্রে কাজের তালিকা থাকে মেমরিতে; CPU শুধু একটি “doorbell” register-এ লিখে জানায় (পর্ব ৫২-এর DMA).

একটি গুরুত্বপূর্ণ পার্থক্য মনে রাখুন: device register মেমরির মতো আচরণ করে না. RAM-এর একটি ঘরে ৫ লিখলে পরে পড়লে ৫-ই পাবেন। কিন্তু device register-এ:

  • পড়া মান নিজে থেকে বদলায় — যন্ত্র কাজ শেষ করলে status বিট বসে যায়, CPU কিছু না লিখলেও।
  • পড়ারও পার্শ্বপ্রতিক্রিয়া থাকতে পারে — UART-এর receive register পড়লেই FIFO থেকে byte সরে যায়; দুবার পড়লে দ্বিতীয় byte পাবেন।
  • একই ঠিকানায় পড়া আর লেখা দুটি ভিন্ন register হতে পারে (অংশ ৪-এ দেখবেন)।
  • লেখার ক্রম গুরুত্বপূর্ণ — “আগে ঠিকানা, তারপর start বিট” — এই ক্রম উল্টে গেলে যন্ত্র ভুল কাজ শুরু করবে।

এই চারটি বৈশিষ্ট্যই পরের অংশে ঠিক করে দেবে, cache, compiler আর CPU-র out-of-order execution এই register-গুলোর সাথে কীভাবে আচরণ করবে।

৩ · যন্ত্রের ঠিকানা — port-mapped I/O বনাম memory-mapped I/O

CPU-র কাছে সব কিছু ঠিকানা। প্রশ্ন হলো, device register-এর ঠিকানা মেমরির ঠিকানার সাথে একই জগতে, না আলাদা জগতে?

Port-mapped I/O — আলাদা ঠিকানা-জগৎ (x86)

Port-mapped I/O-তে (isolated I/O-ও বলে) যন্ত্রের register থাকে একটি আলাদা “port” ঠিকানা-জগতে, আর তাদের ছোঁয়ার জন্য বিশেষ instruction. Intel 8080 থেকে এই ধারা x86-এ এসেছে: in আর out instruction, ১৬-বিটের port নম্বর — মোট ৬৫,৫৩৬টি byte-port. Bus-এ একটি আলাদা সংকেত (বা আলাদা ধরনের transaction) জানায় যে এটি মেমরি নয়, I/O. PC-র ঐতিহ্যগত প্রথম serial port (COM1) port 0x3F8 থেকে ৮টি port জুড়ে থাকে — এটি PC-যুগের প্রচলিত বিন্যাস।

        mov  dx, 0x3FD      ; COM1-এর LSR = 0x3F8 + 5
poll:   in   al, dx         ; port থেকে এক byte পড়া (মেমরি নয়!)
        test al, 0x20       ; বিট 5: THR খালি?
        jz   poll           ; না হলে আবার দেখা
        mov  dx, 0x3F8      ; THR = 0x3F8 + 0
        mov  al, 'A'
        out  dx, al         ; port-এ এক byte লেখা

Memory-mapped I/O — একই ঠিকানা-জগৎ (RISC-V, ARM)

Memory-mapped I/O (MMIO)-তে physical ঠিকানা-জগতের কিছু অংশ RAM নয়, যন্ত্রের জন্য রাখা। Address decoder (পর্ব ১২-এর address decoding) ঠিকানা দেখে ঠিক করে, অনুরোধটি RAM-এ যাবে না কোনো device-এ। ISA-তে কোনো বিশেষ I/O instruction লাগে না — সাধারণ lb/sb/lw/sw. RISC-V-এ port I/O বলে কিছুই নেই; ARM-এও নেই। এমনকি x86-এর আধুনিক যন্ত্রেও (PCIe GPU, NVMe, network card) প্রায় সব register MMIO দিয়ে; port I/O টিকে আছে মূলত পুরোনো সামঞ্জস্যের জন্য।

ঠিকানার পরিসর (উদাহরণ)কী আছেধরন
0x0000_1000 – 0x0000_FFFFboot ROMশুধু পড়া, cache করা যায়
0x0C00_0000 – …interrupt controller (PLIC-ধাঁচের, পর্ব ৩৮)I/O, uncached
0x1000_0000 – 0x1000_00FFUART (16550-ধাঁচের)I/O, uncached
0x4000_0000 – 0x7FFF_FFFFPCIe-এর MMIO জানালা (BAR-গুলো এখানে বসে)I/O, uncached
0x8000_0000 – …DRAMসাধারণ মেমরি, cache করা যায়

এই বিন্যাস একটি শেখানোর উদাহরণ। এটি QEMU-র RISC-V “virt” যন্ত্রের বিন্যাসের কাছাকাছি (সেখানে UART 0x10000000-এ আর RAM 0x80000000 থেকে বলে xv6-riscv-এর মতো শিক্ষামূলক OS-এর কোডে দেখা যায়), কিন্তু প্রতিটি বাস্তব board-এর নিজস্ব map থাকে — device tree বা datasheet-এ লেখা।

বিষয়Port-mapped I/OMemory-mapped I/O
Instructionবিশেষ: in, outসাধারণ load/store
ঠিকানা-জগৎআলাদা, ছোট (x86-এ ৬৪ Ki port)মেমরির সাথে ভাগ করা
Addressing modeসীমিত (x86-এ port ধ্রুবক বা dx-এ)যেকোনো load/store-এর mode
সুরক্ষাআলাদা ব্যবস্থা (x86-এ IOPL, I/O permission bitmap)সাধারণ page table-এর অনুমতি (পর্ব ৫০)
C থেকে ব্যবহারinline assembly বা compiler-এর বিশেষ ফাংশনvolatile pointer
অসুবিধাISA-তে বাড়তি instruction, ছোট জগৎঠিকানা-জগতের কিছু অংশ খরচ; cache ও ক্রমের সতর্কতা লাগে

MMIO-র তিনটি সতর্কতা — cache, compiler আর CPU-র ক্রম

MMIO-তে load/store সাধারণ instruction, কিন্তু অংশ ২-এর চারটি বৈশিষ্ট্যের জন্য তিন স্তরে বিশেষ ব্যবস্থা লাগে।

  1. Cache নয় (uncached). Status register cache-এ থাকলে CPU চিরকাল পুরোনো মান পড়বে, আর লেখা data register-এ না গিয়ে cache-এ বসে থাকবে। তাই device অঞ্চল চিহ্নিত থাকে uncached হিসেবে — প্রতিটি প্রবেশ সত্যিই যন্ত্র পর্যন্ত যায়। RISC-V-এ এটি ঠিক করে প্ল্যাটফর্মের physical memory attributes (PMA) — কোন ঠিকানা-পরিসর মেমরি, কোনটি I/O; ARM-এ page table-এর “Device” memory type (যেমন Device-nGnRnE); x86-এ MTRR আর PAT দিয়ে “UC” (uncacheable) ধরন।
  2. Compiler যেন পড়া-লেখা বাদ না দেয় — volatile. লুপের ভেতরে কেউ status বদলায় না দেখে compiler -O2-এ পড়াটি একবারেই সেরে লুপকে অসীম বানাতে পারে। volatile বলে: প্রতিটি পড়া-লেখা সত্যিই আর ক্রমে করতে হবে। তবে এটি শুধু compiler-কে বাঁধে — CPU-কে নয়, আর এটি lock বা barrier নয় (Embedded C পর্বে ব্যবহার আছে)।
  3. CPU-র ক্রম — fence. RISC-V-এর memory model (RVWMO) শিথিল: ভিন্ন ঠিকানার load/store CPU আগে-পরে দেখাতে পারে। ধরুন driver প্রথমে RAM-এ একটি কাজের বিবরণ (descriptor) লেখে, তারপর device-এর doorbell register-এ লেখে। Device যদি doorbell আগে দেখে, পুরোনো descriptor পড়বে। RISC-V-এর fence instruction-এ আগের আর পরের সেট আলাদা করে বলা যায় — I (device input, অর্থাৎ device-পড়া), O (device output, device-লেখা), R (মেমরি-পড়া), W (মেমরি-লেখা)। যেমন fence w, o মানে “আগের সব মেমরি-লেখা পরের যেকোনো device-লেখার আগে দৃশ্যমান হবে”। Linux-এর RISC-V port writel()-জাতীয় ফাংশনে এ ধরনের fence বসায় বলে জানা যায়; ঠিক কোন fence লাগবে, তা specification আর SoC-এর নথি দেখে নিশ্চিত হওয়া উচিত।

প্রচলিত ভুল: “volatile দিলেই device-এর সাথে ক্রম ঠিক থাকবে।” না। volatile শুধু নিশ্চিত করে যে compiler প্রতিটি volatile প্রবেশ instruction হিসেবে রাখবে আর নিজেদের মধ্যে ক্রম বদলাবে না। সাধারণ (non-volatile) মেমরি-লেখার সাথে ক্রম, বা CPU-র hardware স্তরের ক্রম — সেটি barrier/fence-এর কাজ। বাস্তব driver তাই OS-এর দেওয়া accessor ফাংশন (readl/writel-জাতীয়) ব্যবহার করে, যেগুলোর ভেতরে দুটিই থাকে।

৪ · হাতে-কলমে MMIO — একটি UART-এ polling দিয়ে byte পাঠানো

সবচেয়ে সরল আর সবচেয়ে প্রচলিত যন্ত্র দিয়ে শুরু করি: UART, যা একটি একটি বিট করে serial লাইনে byte পাঠায় (ভেতরের shift register পর্ব ১৬-এ দেখেছেন)। ১৯৮০-এর দশকের National Semiconductor-এর 16550 chip-এর register-বিন্যাস এত ব্যাপকভাবে অনুকরণ করা হয়েছে যে আজও বহু SoC আর emulator “16550-ধাঁচের” UART দেয়। আমরা শুধু দরকারি অংশ নেব:

Offsetপড়লেলিখলেদরকারি বিট
+0RBR (receive buffer)THR (transmit holding)৮ বিটের ডেটা
+1IER (interrupt enable)কোন ঘটনায় interrupt
+2IIR (interrupt identification)FCR (FIFO control)FIFO চালু/মোছা
+3LCR (line control)ডেটা বিট, parity, stop বিট
+5LSR (line status)বিট ০ = ডেটা এসেছে; বিট ৫ = THRE (THR খালি); বিট ৬ = TEMT (THR আর shift register দুটিই খালি)

(+4, +6, +7-এ modem control, modem status আর একটি scratch register; আর LCR-এর একটি বিট বদলে +0/+1-এ baud rate-এর divisor লেখা হয় — এখানে লাগবে না।) Base ঠিকানা আমরা ধরব 0x10000000. তাহলে LSR-এর ঠিকানা = 0x10000000 + 5 = 0x10000005.

Polling-এ byte পাঠানোর নিয়ম দুই লাইনের: “LSR-এর THRE বিট ১ না হওয়া পর্যন্ত বারবার পড়ুন, তারপর THR-এ byte লিখুন।” C আর RISC-V-এ:

/* C */                                   # RV32I
#define UART ((volatile unsigned char *)0x10000000)
void uart_putc(char c) {                  uart_putc:
                                              lui   a5, 0x10000      # a5 = 0x10000000
    while ((UART[5] & 0x20) == 0)         1:  lbu   a4, 5(a5)        # LSR পড়া (uncached)
        ;              /* busy-wait */        andi  a4, a4, 0x20     # THRE বিট
                                              beqz  a4, 1b           # ০ হলে আবার
    UART[0] = c;                              sb    a0, 0(a5)        # THR-এ লেখা
}                                             ret

লক্ষ করুন, lbu a4, 5(a5) একটি সম্পূর্ণ সাধারণ load — পর্ব ২৬-এর base + offset ঠিকানা। পার্থক্য শুধু ঠিকানায়: decoder এটিকে RAM-এ না পাঠিয়ে UART-এ পাঠায়। এই ধরনের polling-কে busy-waiting-ও বলে: CPU কাজের কাজ কিছু না করে শুধু অপেক্ষা করে।

এখন stepper-এ দেখুন, একটি byte পাঠানোর সময় register-গুলোর ভেতরে কী ঘটে। আগের byte 'O' shift register-এ বেরোচ্ছে, তার পরের 'K' THR-এ অপেক্ষায়, আর CPU এখন '\n' (0x0A) পাঠাতে চায়। “পরে” চাপুন:

1while ((UART[5] & 0x20) == 0)   // LSR পড়া2    ;                           // আবার3    // THRE = 1, লুপ শেষ4UART[0] = '\n';                  // THR-এ লেখা5// (CPU এখন অন্য কাজে)
UART-এর ভেতরে
LSR (পড়া মান)
THR
Shift register
ব্যর্থ poll
  1. Poll ১ — ব্যস্ত। CPU 0x10000005 থেকে LSR পড়ে: 0x00. বিট ৫ (THRE) = ০, কারণ THR-এ 'K' এখনো অপেক্ষা করছে — shift register আগের 'O' পাঠাতে ব্যস্ত। লুপ আবার ঘোরে; ব্যর্থ poll = ১।
  2. Poll ২ — এখনো ব্যস্ত। আবার 0x00. প্রতিটি poll একটি uncached load — cache hit-এর চেয়ে অনেক বেশি cycle. ব্যর্থ poll = ২।
  3. Poll ৩ — জায়গা খালি। এর মধ্যে 'O'-এর stop বিট শেষ; hardware নিজে 'K'-কে THR থেকে shift register-এ সরিয়েছে, আর THRE = ১ বসিয়েছে। LSR = 0x20 (বিট ৬ TEMT এখনো ০, কারণ shift register ব্যস্ত)। এবারের পড়ায় লুপ শেষ।
  4. THR-এ লেখা। sb দিয়ে 0x0A যায় 0x10000000-এ। THR আবার ভরা, তাই hardware THRE = ০ করে — LSR = 0x00. CPU-র কাজ শেষ; byte-টি তারে যাবে নিজের গতিতে।
  5. পরে — 'K' শেষ। 'K'-এর ১০টি বিট (start + ৮ ডেটা + stop) বেরোলে '\n' shift register-এ যায়; THR খালি, LSR = 0x20.
  6. সব শেষ। দুটিই খালি: THRE = ১, TEMT = ১, LSR = 0x60. ১১৫২০০ baud-এ প্রতি byte প্রায় ৮৬.৮ µs — ১ GHz CPU-তে প্রায় ৮৬,৮০০ cycle, যার পুরোটা polling-এ কাটতে পারে।

এবার একই ধারণা চালানো যায় এমন রূপে। নিচের JS-এ একটি ছোট “bus” আছে: 0x10000000-এর নিচে RAM, ওপরে ৮টি register-এর UART. প্রতিটি load/store সময় বাড়ায় (RAM ১ cycle — যেন cache hit; device-প্রবেশ ৬ cycle), আর THR-এ লেখার পর UART K = ৪০ cycle ব্যস্ত থাকে। (আসল UART-এর চেয়ে বহু গুণ ছোট সংখ্যা):

// A tiny bus: RAM below 0x10000000, a 16550-style UART at 0x10000000 (THR = +0, LSR = +5).
const UART = 0x10000000, THR = UART + 0, LSR = UART + 5, THRE = 0x20, K = 40;
const ram = new Map(); let now = 0, busyUntil = 0, sent = "";
function load8(a) {                                  // every access advances simulated time
  if (a >= UART && a < UART + 8) {                   // address decoder: device window, 8 regs
    now += 6;                                        // uncached device read: ~6 cycles here
    return a === LSR ? (now >= busyUntil ? THRE : 0) : 0;
  }
  now += 1; return ram.get(a) ?? 0;                  // RAM hit (pretend cache): 1 cycle
}
function store8(a, v) {
  if (a === THR) { now += 6; sent += String.fromCharCode(v); busyUntil = now + K; return; }
  now += 1; ram.set(a, v & 0xFF);
}
function putc(ch) {                                  // the polling transmit loop
  const t0 = now; let polls = 0;
  while ((load8(LSR) & THRE) === 0) polls++;         // busy-wait until THR is empty
  const waited = now - t0;
  store8(THR, ch.charCodeAt(0));
  return [polls, waited];
}
const msg = "Hi!\n";
for (let i = 0; i < msg.length; i++) store8(0x2000 + i, msg.charCodeAt(i));  // message in RAM
console.log("byte  polls_failed  cycles_waiting");
let total = 0;
for (let i = 0; i < msg.length; i++) {
  const ch = String.fromCharCode(load8(0x2000 + i));
  const [p, w] = putc(ch); total += w;
  console.log(JSON.stringify(ch).padEnd(6), String(p).padStart(12), String(w).padStart(15));
}
console.log(`sent ${JSON.stringify(sent)} in ${now} cycles; ${total} spent polling (${(100 * total / now).toFixed(1)}%)`);

আউটপুট (নির্ধারিত):

byte  polls_failed  cycles_waiting
"H"               0               6
"i"               6              42
"!"               6              42
"\n"              6              42
sent "Hi!\n" in 164 cycles; 132 spent polling (80.5%)

প্রথম byte-এ UART অলস, তাই প্রথম poll-ই সফল — শুধু ৬ cycle. পরের প্রতিটি byte-এ আগের byte-এর ৪০ cycle শেষ হওয়ার অপেক্ষা: লেখার পর RAM থেকে পরের অক্ষর আনতে ১ cycle, তারপর প্রতি poll ৬ cycle. ১ + ৬n ≥ ৪০ হতে n = ৭টি poll লাগে — ৬টি ব্যর্থ, সপ্তমটি সফল, মোট ৪২ cycle. পুরো বার্তায় ১৬৪ cycle-এর ১৩২টি (৮০.৫%) কেটেছে শুধু LSR পড়ে। একই load8 ফাংশন ঠিকানা দেখে RAM বা UART বেছে নিচ্ছে — memory-mapped I/O-র address decoder ঠিক এটাই করে।

এবার C-তে একই নকশা, তবে বাস্তব driver যেভাবে লেখে — register-গুলোকে একটি struct-এর field হিসেবে, আর সেই struct-এর দিকে একটি volatile pointer. এখানে আসল UART নেই, তাই struct থাকে সাধারণ মেমরিতে আর একটি ছোট ফাংশন (hw_tick) hardware-এর ভূমিকা নেয়। বাস্তব board-এ pointer-টি হতো (void *)0x10000000, আর hw_tick-এর কাজ যন্ত্র নিজেই করত:

// Device registers as a volatile struct. Here the "device" is ordinary memory and a
// helper plays the hardware's part; on a real board the pointer would be (void *)0x10000000.
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
struct uart16550 {                     /* 16550-style layout, one byte per register */
    uint8_t thr;                       /* +0 transmit holding (write)               */
    uint8_t ier, fcr, lcr, mcr;        /* +1..+4 interrupt enable, FIFO, line, modem */
    uint8_t lsr, msr, scr;             /* +5 line status (bit 5 = THR empty), +6, +7 */
};
#define LSR_THRE 0x20u
_Static_assert(offsetof(struct uart16550, lsr) == 5, "LSR must sit at offset 5");
static struct uart16550 fake;          /* stands in for the device's address window  */
static unsigned busy;                  /* polls left until the fake byte is "sent"   */
static void hw_tick(void) {            /* what the hardware would do on its own      */
    if (busy && --busy == 0) fake.lsr |= LSR_THRE;
}
static unsigned uart_putc(volatile struct uart16550 *u, char c) {
    unsigned polls = 0;
    while ((u->lsr & LSR_THRE) == 0) { polls++; hw_tick(); }   /* re-read every time */
    u->thr = (uint8_t)c;               /* the store that starts transmission         */
    u->lsr &= (uint8_t)~LSR_THRE;      /* (the real chip clears THRE by itself)      */
    busy = 3;
    return polls;
}
int main(void) {
    volatile struct uart16550 *uart = &fake;
    fake.lsr = LSR_THRE;               /* idle: ready for the first byte             */
    printf("sizeof = %zu, offset of lsr = %zu\n", sizeof *uart, offsetof(struct uart16550, lsr));
    for (const char *p = "OK\n"; *p; p++) {
        unsigned n = uart_putc(uart, *p);
        printf("byte 0x%02X: %u failed polls, LSR now 0x%02X\n", (unsigned)(uint8_t)*p, n, (unsigned)uart->lsr);
    }
    return 0;
}

আউটপুট:

sizeof = 8, offset of lsr = 5
byte 0x4F: 0 failed polls, LSR now 0x00
byte 0x4B: 3 failed polls, LSR now 0x00
byte 0x0A: 3 failed polls, LSR now 0x00

প্রথম লাইন দেখায়, struct-এর বিন্যাস যন্ত্রের register-বিন্যাসের সাথে হুবহু মেলে: মোট ৮ byte, আর lsr ঠিক offset ৫-এ। _Static_assert compile-এর সময়েই এটি যাচাই করে — কেউ ভুল করে একটি field সরালে কোড compile-ই হবে না, যা driver-এ খুব কাজের অভ্যাস। প্রথম byte 'O' (0x4F)-এর সময় যন্ত্র অলস, তাই ০টি ব্যর্থ poll. লেখার পর THRE মুছে যায় (LSR = 0x00), আর “hardware” তিন poll পরে আবার THRE বসায় — তাই 'K' (0x4B) আর '\n' (0x0A)-এ ৩টি করে ব্যর্থ poll. লুপের ভেতরে u->lsr প্রতিবার আবার পড়া হয় কারণ pointer-টি volatile; এখানে hw_tick() একই ফাইলে থাকায় compiler এমনিতেও বদলটি দেখতে পেত, কিন্তু আসল যন্ত্রে বদল আসে program-এর বাইরে থেকে — সেখানে volatile ছাড়া -O2-এ লুপটি ভেঙে যেতে পারত। u->lsr &= ... লাইনটি শুধু অনুকরণের জন্য; আসল chip THRE নিজেই মোছে।

৫ · Polling-এর দাম — কখন চলে, কখন চলে না; আর interrupt ও DMA

পাঠ্যবইয়ের প্রচলিত হিসাব: ডেটা না হারাতে CPU-কে সেকেন্ডে নির্দিষ্ট সংখ্যক বার poll করতে হবে, আর প্রতিটি poll-এ (call, uncached পড়া, তুলনা, ফেরা) নির্দিষ্ট cycle লাগে। তাহলে:

CPU-র অংশ = (প্রতি সেকেন্ডে poll × প্রতি poll-এ cycle) ÷ clock rate

প্রতি সেকেন্ডে poll কত? ধীর যন্ত্রে একটি নির্দিষ্ট হার (mouse-এর অবস্থান সেকেন্ডে কয়েকশো বার দেখলেই যথেষ্ট); দ্রুত যন্ত্রে ডেটার হার ÷ প্রতি poll-এ কত byte সরানো যায়। হাতে একটি উদাহরণ: ২ GHz CPU, প্রতি poll ৪০০ cycle (ধরে নেওয়া মান)।

  • Mouse, সেকেন্ডে ১০০০ বার: ১০০০ × ৪০০ = ৪ × ১০৫ cycle/s → ৪ × ১০৫ ÷ ২ × ১০৯ = ০.০২%. নগণ্য।
  • ১ Gbit/s network card, প্রতি poll-এ ৬৪ byte: ১২৫ × ১০৬ byte/s ÷ ৬৪ = ১৯,৫৩,১২৫ poll/s × ৪০০ = ৭.৮১ × ১০৮ → ৩৯.১%. শুধু অপেক্ষায় CPU-র এক-তৃতীয়াংশের বেশি!

নিচের calculator আরও কয়েকটি যন্ত্রে একই হিসাব করে। সব মান শেখানোর জন্য ধরে নেওয়া — বাস্তবের যন্ত্র ভিন্ন হবে:

// Fraction of CPU time spent polling. ALL device figures are ASSUMED teaching values.
const CLOCK = 2e9, POLL = 400;                        // 2 GHz CPU, 400 cycles per poll (assumed)
const devices = [
  // name, polls per second needed (rate / bytes per poll, or a fixed poll rate)
  ["mouse (1000 polls/s)",            1000],
  ["keyboard (100 polls/s)",           100],
  ["1 Gbit/s NIC, 64 B per poll",     125e6 / 64],
  ["SSD 2 GB/s, 4 KiB per poll",      2e9 / 4096],
  ["SSD 2 GB/s, 16 B per poll",       2e9 / 16],
];
const pct = x => (100 * x).toPrecision(3) + "%";
console.log(`CPU ${CLOCK / 1e9} GHz, ${POLL} cycles per poll`);
console.log("device".padEnd(30), "polls/s".padStart(12), "cycles/s".padStart(14), "CPU share".padStart(10));
for (const [name, pps] of devices) {
  const cyc = pps * POLL;
  console.log(name.padEnd(30), pps.toFixed(0).padStart(12), cyc.toExponential(2).padStart(14),
              (cyc > CLOCK ? "> 100%!" : pct(cyc / CLOCK)).padStart(10));
}
// Polling only while the device is busy: disk active 10% of the time
const active = 0.10, disk = 2e9 / 4096 * POLL / CLOCK;
console.log(`SSD 4 KiB polling, busy ${active * 100}% of the time -> ${pct(disk * active)}`);

আউটপুট (নির্ধারিত):

CPU 2 GHz, 400 cycles per poll
device                              polls/s       cycles/s  CPU share
mouse (1000 polls/s)                   1000        4.00e+5    0.0200%
keyboard (100 polls/s)                  100        4.00e+4   0.00200%
1 Gbit/s NIC, 64 B per poll         1953125        7.81e+8      39.1%
SSD 2 GB/s, 4 KiB per poll           488281        1.95e+8      9.77%
SSD 2 GB/s, 16 B per poll         125000000       5.00e+10    > 100%!
SSD 4 KiB polling, busy 10% of the time -> 0.977%

তিনটি শিক্ষা। (১) Mouse-কীবোর্ডে খরচ ০.০২% বা তারও কম — তাই ছোট microcontroller-এ polling-ই যথেষ্ট। (২) একই SSD-তে প্রতি poll-এ কত ডেটা সরে, তার ওপর সব নির্ভর করে: ৪ KiB-এর টুকরোয় ৯.৭৭%, কিন্তু ১৬ byte-এর টুকরোয় প্রয়োজন ৫ × ১০১০ cycle/s — ২ GHz CPU-র ২৫ গুণ, অর্থাৎ অসম্ভব। এখান থেকেই বোঝা যায়, দ্রুত যন্ত্রে CPU byte-ধরে ডেটা সরাতে পারে না — ডেটা সরাবে যন্ত্র নিজে (DMA). (৩) শুধু ব্যস্ত সময়ে (১০%) poll করলে খরচ ০.৯৭৭%-এ নামে — কিন্তু “কখন ব্যস্ত” জানতেই একটি সংকেত লাগে।

দুটি বিকল্প, এক অনুচ্ছেদে — বিস্তারিত পর্ব ৫২-এ

Interrupt: CPU জিজ্ঞেস করে না; যন্ত্র কাজ শেষ হলে একটি তারে (বা PCIe-তে একটি বিশেষ বার্তায়) সংকেত পাঠায়, CPU দুটি instruction-এর মাঝে থেমে handler চালায়। অলস সময়ে খরচ শূন্য, কিন্তু প্রতিটি ঘটনায় context বাঁচানো-ফেরানোর খরচ (কয়েকশো থেকে কয়েক হাজার cycle) — খুব ঘন ঘটনায় এটিই আবার বোঝা। CPU-র দিক থেকে trap-এর পদ্ধতি পর্ব ৩৮-এ (Exception আর interrupt) দেখেছেন।

DMA (direct memory access): CPU যন্ত্রকে শুধু বলে “এই ঠিকানার ৬৪ KiB disk-এ লিখতে হবে” (বা মেমরিতে একটি descriptor রেখে doorbell বাজায়); যন্ত্রের DMA engine নিজেই bus-এর master হয়ে মেমরি থেকে পড়ে বা লেখে, আর শেষে একটি interrupt দেয়। CPU-র খরচ এখন প্রতি byte-এ নয়, প্রতি কাজে। Interrupt-এর বিস্তারিত, interrupt controller, MSI, DMA-র descriptor ring, cache coherence আর IOMMU — সব পর্ব ৫২-এ (Interrupt আর DMA — hardware-এর দিক থেকে)।

মূল ধারণা

Polling-এর খরচ যন্ত্রের নামমাত্র গতির সমানুপাতিক নয়, প্রতি সেকেন্ডে কতবার poll করতে হয় তার সমানুপাতিক। ধীর যন্ত্রে polling সস্তা ও সরল; দ্রুত যন্ত্রে বড় টুকরোয় কাজ (DMA) আর কাজ শেষের সংকেত (interrupt) ছাড়া চলে না। মজার ব্যাপার, খুব দ্রুত যন্ত্রে, যেখানে ঘটনা প্রায় অবিরাম, আবার polling ফিরে আসে — ভাবনার প্রশ্ন ৩ দেখুন।

৬ · Bus — তার, সময়, arbitration আর bandwidth; parallel থেকে serial

Bus হলো কয়েকটি অংশের মধ্যে ভাগ করা যোগাযোগের পথ — এক গুচ্ছ তার আর তাদের ব্যবহারের নিয়ম (protocol). ক্লাসিক bus-এর তারগুলো তিন দলে:

  • Address লাইন: কোন ঠিকানা (কোন মেমরি-ঘর বা কোন device register).
  • Data লাইন: আসল ডেটা — ৮, ১৬, ৩২ বা ৬৪টি তার একসাথে; এই সংখ্যাই bus-এর width.
  • Control লাইন: পড়া না লেখা, মেমরি না I/O, clock, “ডেটা বৈধ”, “তৈরি”, অনুরোধ আর অনুমতির সংকেত।

Synchronous বনাম asynchronous bus

Synchronous bus-এ সবাই একটি সাধারণ clock মেনে চলে: “clock-এর তৃতীয় edge-এ ডেটা তৈরি থাকবে”। নকশা সরল আর দ্রুত, কিন্তু সবচেয়ে ধীর যন্ত্রকেও সেই সময়ে উত্তর দিতে হয় (নাহলে “wait” সংকেত দিয়ে বাড়তি cycle চাইতে হয়), আর লম্বা তারে clock skew (পর্ব ১৮) বাড়ে বলে দৈর্ঘ্য সীমিত। Asynchronous bus-এ কোনো সাধারণ clock নেই; দুই পক্ষ handshake-এ কথা বলে — চার ধাপের REQ/ACK:

          (1)          (2)          (3)          (4)
REQ   ____/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\_______________________
ACK   _________________/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\__________
DATA  ====<        বৈধ ডেটা          >=========================
(1) পাঠানো পক্ষ ডেটা বসিয়ে REQ তোলে   (2) গ্রহীতা ডেটা ধরে ACK তোলে
(3) পাঠানো পক্ষ REQ নামায়            (4) গ্রহীতা ACK নামায় — পরের চক্র তৈরি

ধীর যন্ত্র ধীরে ACK দেয়, দ্রুত যন্ত্র দ্রুত — সময় আপনা থেকে মিলে যায়। দাম: প্রতিটি transfer-এ তারের দুই দিকে চারবার যাওয়া-আসা।

Bus arbitration — কে চালাবে?

একাধিক অংশ (CPU, DMA-সক্ষম যন্ত্র) bus-এর master হতে চাইলে bus arbitration লাগে। দুটি ক্লাসিক পদ্ধতি:

  • Daisy chain: অনুমতির (grant) একটি তার যন্ত্র থেকে যন্ত্রে শিকলের মতো যায়। যে যন্ত্র অনুরোধ করেছে, সে grant আটকে রাখে; না করলে পরের জনকে দেয়। তার কম, কিন্তু অগ্রাধিকার জায়গায় বাঁধা — শিকলের শুরুর যন্ত্র সবসময় জেতে, শেষের যন্ত্র অনাহারে (starvation) থাকতে পারে।
  • কেন্দ্রীয় (centralised) arbiter: প্রতিটি যন্ত্রের নিজস্ব request ও grant তার একটি arbiter-এ যায়, যে নীতি (নির্দিষ্ট অগ্রাধিকার, round-robin) মেনে বেছে নেয়। ন্যায্য, কিন্তু তার বেশি। ক্লাসিক PCI এই ধরনের ছিল।

Bandwidth-এর হিসাব

Bandwidth = (width ÷ ৮) byte × clock rate × প্রতি clock-এ transfer

ক্লাসিক PCI: ৩২ বিট = ৪ byte, প্রায় ৩৩.৩৩ MHz, প্রতি clock-এ ১টি transfer → ৪ × ৩৩.৩৩ × ১০৬ ≈ ১৩৩ MB/s — এটি সর্বোচ্চ (peak) মান। বাস্তবে address-এর cycle, অপেক্ষা আর arbitration-এ কিছু cycle যায়: যদি প্রতি ৪টি data cycle-এর জন্য ১টি address cycle আর ১টি turnaround লাগে, তাহলে কার্যকর মান peak-এর ৪/৬. DDR মেমরি (পর্ব ৪৯) clock-এর দুই edge-এই transfer করে, তাই “প্রতি clock-এ ২”।

Parallel থেকে serial — কেন?

PC-তে ছিল ISA bus (১৯৮১ সালের IBM PC-তে ৮ বিট, পরে ১৬ বিট), তারপর ১৯৯০-এর দশকে PCI. গতি বাড়ানোর স্বাভাবিক পথ — আরও চওড়া, আরও দ্রুত clock — শত MHz-এ তিনটি দেয়ালে ঠেকে:

  • Skew: সব তারের সংকেত ঠিক একসাথে পৌঁছায় না; clock যত দ্রুত, এই আগে-পিছে বিটের সময়ের তত বড় অংশ খায়।
  • Crosstalk: পাশাপাশি তারে দ্রুত বদলানো সংকেত একে অপরের ওপর বৈদ্যুতিক প্রভাব ফেলে।
  • Pin আর জায়গা: প্রতিটি বাড়তি তার মানে chip-এ pin, board-এ পথ, connector-এ পিন — খরচ আর শক্তি।

সমাধান: কম তার, অনেক দ্রুত। আধুনিক serial link-এ প্রতিটি দিকে একটি differential pair (দুটি তারে উল্টো সংকেত, যাতে বাইরের গোলমাল কাটাকাটি হয়), আর clock আলাদা তারে যায় না — গ্রহীতা ডেটার বিট-বদল থেকেই clock পুনরুদ্ধার করে (clock recovery). তার জন্য ডেটায় যথেষ্ট ০↔১ বদল থাকা চাই — সেটিই line encoding-এর কাজ: 8b/10b মানে প্রতি ৮ বিট ডেটা পাঠানো হয় ১০ বিটের code হিসেবে (তারের বিটের ২০% বাড়তি), 128b/130b মানে প্রতি ১২৮ বিটে মাত্র ২ বিটের মাথা (তারের বিটের প্রায় ১.৫৪%, তবে ডেটা scramble করে বিট-বদল নিশ্চিত করা হয়)। Signalling আর encoding-এর মৌলিক ধারণা Computer Networks কোর্সের Physical layer পর্বে আছে। প্রতিটি lane-এর clock আলাদাভাবে পুনরুদ্ধার হয়, আর lane-গুলোর মধ্যের skew গ্রহীতা নিজে মেলায়। এর hardware অংশ SerDes (serializer/deserializer).

নিচের calculator দুই ধরনের সংযোগের raw আর কার্যকর bandwidth হিসাব করে। Serial-এর ক্ষেত্রে GT/s মানে প্রতি সেকেন্ডে কত বিলিয়ন transfer (তারে বিট), তার থেকে encoding বাদ দিয়ে আসল ডেটা। সব মান প্রতি দিকে, আর packet-এর header-এর খরচ ধরা হয়নি:

// Raw vs effective bandwidth of parallel buses and serial links (per direction).
// Parallel: width (bits) x clock (MHz) x transfers per clock.  Serial: GT/s x lanes x encoding.
const MBs = x => x >= 1000 ? (x / 1000).toFixed(2) + " GB/s" : x.toFixed(1) + " MB/s";
console.log("Parallel bus                 width  clock MHz  xfer/clk  bandwidth");
for (const [name, bits, mhz, xfer] of [
  ["PCI (classic)",              32,  33.33, 1],
  ["PCI-X 133",                  64, 133.33, 1],
  ["DDR4-3200 channel",          64, 1600,   2],
]) console.log(name.padEnd(28), String(bits).padStart(5), String(mhz).padStart(10),
               String(xfer).padStart(9), " " + MBs(bits / 8 * mhz * xfer));

console.log("\nSerial link                  GT/s lanes encoding      raw      effective");
for (const [name, gt, lanes, enc, [d, t]] of [
  ["PCIe Gen1 x1",          2.5,  1, "8b/10b",    [8, 10]],
  ["PCIe Gen2 x8",          5,    8, "8b/10b",    [8, 10]],
  ["PCIe Gen3 x4",          8,    4, "128b/130b", [128, 130]],
  ["PCIe Gen3 x16",         8,   16, "128b/130b", [128, 130]],
  ["PCIe Gen4 x4",         16,    4, "128b/130b", [128, 130]],
  ["SATA 6 Gb/s",           6,    1, "8b/10b",    [8, 10]],
  ["USB 5 Gb/s (Gen 1)",    5,    1, "8b/10b",    [8, 10]],
  ["USB 10 Gb/s (Gen 2)",  10,    1, "128b/132b", [128, 132]],
]) {
  const raw = gt * lanes * 1000 / 8, eff = raw * d / t;          // MB/s, before packet overhead
  console.log(name.padEnd(26), String(gt).padStart(6), String(lanes).padStart(4), enc.padStart(10),
              MBs(raw).padStart(11), MBs(eff).padStart(13), `(${(100 * d / t).toFixed(2)}%)`);
}

আউটপুট (নির্ধারিত):

Parallel bus                 width  clock MHz  xfer/clk  bandwidth
PCI (classic)                   32      33.33         1  133.3 MB/s
PCI-X 133                       64     133.33         1  1.07 GB/s
DDR4-3200 channel               64       1600         2  25.60 GB/s

Serial link                  GT/s lanes encoding      raw      effective
PCIe Gen1 x1                  2.5    1     8b/10b  312.5 MB/s    250.0 MB/s (80.00%)
PCIe Gen2 x8                    5    8     8b/10b   5.00 GB/s     4.00 GB/s (80.00%)
PCIe Gen3 x4                    8    4  128b/130b   4.00 GB/s     3.94 GB/s (98.46%)
PCIe Gen3 x16                   8   16  128b/130b  16.00 GB/s    15.75 GB/s (98.46%)
PCIe Gen4 x4                   16    4  128b/130b   8.00 GB/s     7.88 GB/s (98.46%)
SATA 6 Gb/s                     6    1     8b/10b  750.0 MB/s    600.0 MB/s (80.00%)
USB 5 Gb/s (Gen 1)              5    1     8b/10b  625.0 MB/s    500.0 MB/s (80.00%)
USB 10 Gb/s (Gen 2)            10    1  128b/132b   1.25 GB/s     1.21 GB/s (96.97%)

DDR4-3200-এর একটি ৬৪-বিট channel: ৮ byte × ১৬০০ MHz × ২ = ২৫.৬ GB/s — নামের “3200” আসলে প্রতি সেকেন্ডে ৩২০০ মিলিয়ন transfer. Serial অংশে সবচেয়ে বড় শিক্ষা encoding-এ: Gen1–Gen2-এর 8b/10b-এ তারের বিটের ২০% চলে যায়, তাই Gen1 x1-এ ২.৫ GT/s মানে ২৫০ MB/s. Gen3-এ transfer হার ৫ থেকে মাত্র ৮ GT/s (১.৬ গুণ), কিন্তু 128b/130b-এ কার্যকর হার প্রায় দ্বিগুণ (প্রতি lane ৫০০ MB/s থেকে প্রায় ৯৮৫ MB/s). তাই Gen2 x8 (৪.০০ GB/s) আর Gen3 x4 (৩.৯৪ GB/s) প্রায় সমান — অর্ধেক lane-এ একই কাজ। SATA-র ৬ Gb/s-ও ৬০০ MB/s-এ নামে। Packet-এর header আর acknowledgement-এর খরচে বাস্তব throughput আরও কিছুটা কম।

৭ · PCI Express — lane, packet, root complex আর BAR

PCI Express (PCIe) আজকের PC, server আর অনেক SoC-এর প্রধান দ্রুত সংযোগ: GPU, NVMe SSD, network card, এমনকি chip-এর ভেতরের কিছু অংশ। ২০০০-এর দশকের শুরুতে এটি PCI-র উত্তরসূরি হিসেবে আসে। নাম PCI-র মতো, আর software-এর দিক থেকে (configuration-এর নিয়ম, BAR) PCI-র সাথে সামঞ্জস্য রাখা হয়েছে — কিন্তু তারের স্তরে এটি সম্পূর্ণ ভিন্ন: ভাগ করা bus নয়, প্রতিটি যন্ত্রের সাথে নিজস্ব point-to-point serial link.

Lane আর link-এর চওড়া

একটি lane = দুটি differential pair — একটি পাঠানোর, একটি গ্রহণের; তাই দুই দিকে একসাথে পূর্ণ গতি (full duplex). একটি link-এ ১, ২, ৪, ৮ বা ১৬টি lane থাকতে পারে, লেখা হয় x1, x4, x8, x16 (“by sixteen”). ডেটা lane-গুলোতে byte ধরে ভাগ করে (striping) পাঠানো হয়। Graphics card সাধারণত x16 slot-এ, NVMe SSD সাধারণত x4-এ, ছোট network বা USB controller x1-এ বসে। শুরুতে দুই প্রান্ত link training-এ ঠিক করে কত lane আর কোন গতি।

প্রজন্মপ্রতি lane-এ transferEncodingপ্রতি lane, প্রতি দিকে (প্রায়)x16, প্রতি দিকে (প্রায়)
Gen1২.৫ GT/s8b/10b২৫০ MB/s৪ GB/s
Gen2৫ GT/s8b/10b৫০০ MB/s৮ GB/s
Gen3৮ GT/s128b/130b≈ ৯৮৫ MB/s≈ ১৫.৭৫ GB/s
Gen4১৬ GT/s128b/130b≈ ১.৯৭ GB/s≈ ৩১.৫ GB/s
Gen5৩২ GT/s128b/130b≈ ৩.৯৪ GB/s≈ ৬৩ GB/s

প্রতিটি প্রজন্মে প্রতি lane-এর কার্যকর হার মোটামুটি দ্বিগুণ হয়েছে। Gen6-এ (৬৪ GT/s) signalling বদলে PAM4 (প্রতি চিহ্নে ২ বিট) আর error correction-সহ নির্দিষ্ট মাপের “FLIT” packet আসে, যেখানে 128b/130b আর প্রযোজ্য নয় বলে জানা যায় — তাই এই টেবিলের সরল হিসাব সেখানে খাটে না। বিজ্ঞাপনে প্রায়ই দুই দিক যোগ করা সংখ্যা থাকে; উত্তরে বলবেন, প্রতি দিকে না দুই দিক মিলিয়ে।

Packet দিয়ে কথা — তিন স্তর

PCIe-তে কোনো address বা data “লাইন” নেই; সব কিছু packet. স্তরগুলো নেটওয়ার্কের মতো:

  • Transaction layer: TLP (transaction layer packet) বানায় — memory read, memory write, configuration read/write, completion (পড়ার উত্তর), আর বার্তা বা message TLP (যেমন পুরোনো INTx interrupt-তারের অনুকরণ). লক্ষ করুন, MSI interrupt কোনো বিশেষ বার্তা নয় — আসলে একটি সাধারণ memory write. Memory write সাধারণত posted — পাঠিয়েই শেষ, উত্তরের অপেক্ষা নেই। Memory read non-posted — উত্তর আসে আলাদা completion TLP-তে, তাই MMIO-পড়া লেখার চেয়ে অনেক দেরিতে শেষ হয়।
  • Data link layer: প্রতিটি TLP-তে sequence নম্বর আর একটি CRC (LCRC) যোগ করে; গ্রহীতা ঠিক পেলে ACK, ভুল পেলে NAK পাঠায়, আর পাঠানো পক্ষ একটি replay buffer থেকে আবার পাঠায়। Software জানে না (CRC-র ধারণা Error detection পর্বে)।
  • Physical layer: encoding (128b/130b), scrambling, lane-এ ভাগ, serialize করে তারে পাঠানো।

Topology — root complex, switch, endpoint

PCIe একটি গাছ। গোড়ায় root complex — CPU/মেমরির দিক আর PCIe-র দিকের মাঝের সেতু, আজকাল সাধারণত CPU chip বা SoC-এর ভেতরেই। তার এক বা একাধিক root port; প্রতিটিতে একটি link. Link-এর ওপাশে হয় একটি endpoint (আসল যন্ত্র — GPU, SSD), নয়তো একটি switch, যা একটি ওপরের (upstream) port থেকে কয়েকটি নিচের (downstream) port বানায়, যেন বিদ্যুতের মাল্টিপ্লাগ, আর প্রতিটি port-এর ঠিকানা-পরিসর (base/limit) দেখে TLP পাঠায়।

Enumeration আর BAR — MMIO জানালা কোথায় বসবে

প্রতিটি PCIe function-এর একটি configuration space আছে (PCI-তে ২৫৬ byte, PCIe-তে ৪ KiB): vendor ID, device ID, class code আর BAR (base address register). প্রতিটি function-এর ঠিকানা BDF — bus (৮ বিট), device (৫ বিট), function (৩ বিট)। Boot-এর সময় firmware বা OS enumeration করে: প্রতিটি সম্ভাব্য BDF-এর config space পড়ে দেখে কে আছে, switch পেলে নিচে নতুন bus নম্বর দেয়, আর প্রতিটি BAR ঠিক করে।

BAR বলে, যন্ত্রটি তার register-এর জন্য কত বড় ঠিকানা-জানালা চায়। আকার জানার কৌশলটি চতুর: software BAR-এ সব বিট ১ (0xFFFFFFFF) লেখে, তারপর পড়ে। যন্ত্র যে বিটগুলো বদলাতে দেয় না, সেগুলো ০ ফেরে। Memory BAR-এর নিচের ৪টি বিট flag (বিট ০ = ০ মানে memory, বিট ২–১ ধরন — ৩২ বা ৬৪ বিট, বিট ৩ prefetchable); সেগুলো মুছে বাকিটার উল্টো + ১ = আকার। যেমন পড়া মান 0xFFFFC000 → আকার = ~0xFFFFC000 + 1 = 0x4000 = ১৬ KiB. তারপর software একটি ১৬ KiB-aligned ঠিকানা (যেমন 0xFE000000) BAR-এ লেখে — এখন থেকে CPU-র 0xFE000000–0xFE003FFF-এ load/store সরাসরি যন্ত্রের register-এ যায়। অংশ ৩-এর memory-mapped I/O এভাবেই PCIe যন্ত্রে বসে।

এবার stepper-এ একটি NVMe-ধাঁচের যন্ত্রের doorbell register-এ CPU-র একটি store-এর পুরো যাত্রা (ধারণাগত; বাস্তব chip-এ fabric-এর নাম আর খুঁটিনাটি ভিন্ন):

  1. Coreswuncached store
  2. Decode0xFE001008PCIe জানালা
  3. RootMWr TLPposted
  4. Linkseq + LCRCreplay buffer
  5. Lanes128b/130bx4
  6. Switchport 2range মিলেছে
  7. EndpointBAR0 + 0x1008doorbell
Coredriver-এর store
sw   a1, 8(a0)        # a0 = doorbell অঞ্চলের শুরুর VA, a1 = নতুন tail = 5
MMU: VA -> PA 0xFE001008, page-টি I/O (uncached)
cache-এ যাবে না; store buffer থেকে ক্রমমতো বেরোবে
Address decodechip-এর fabric
0x80000000.. : DRAM            না
0xFE000000.. : PCIe MMIO জানালা  হ্যাঁ
অনুরোধ যায় root complex-এ (memory controller-এ নয়)
Transaction layerTLP
ধরন    : Memory Write (MWr), 32-bit ঠিকানা
ঠিকানা : 0xFE001008   দৈর্ঘ্য: 1 DW (4 byte)
ডেটা   : 0x00000005
posted : উত্তর (completion) আসবে না
Data link layerনির্ভরযোগ্যতা
[seq 0x2A1][ TLP header + data ][LCRC 32-bit]
কপি থাকে replay buffer-এ, ACK না আসা পর্যন্ত
NAK বা timeout হলে আবার পাঠানো হয়
Physical layerGen3 x4
byte 0 -> lane 0, byte 1 -> lane 1, ... (striping)
প্রতি lane: scramble, 128b/130b block, serialize
৮ GT/s প্রতি lane, দুই দিকে আলাদা জোড়া
Switchঠিকানা দিয়ে পথ
downstream port 1: 0xE0000000-0xEFFFFFFF  না
downstream port 2: 0xFE000000-0xFE0FFFFF  হ্যাঁ
TLP যায় port 2-এ; ACK ফেরে প্রতিটি link-এ আলাদা
EndpointBAR মিলিয়ে register
BAR0 = 0xFE000000, আকার 16 KiB  -> মিলেছে
offset = 0x1008 -> submission queue 1-এর tail doorbell
যন্ত্র মেমরি থেকে নতুন কাজ পড়তে শুরু করে (DMA, পর্ব ৫২)
  1. ধাপ ১ — core-এর store. Driver একটি সাধারণ sw চালায়। MMU (পর্ব ৫০) ঠিকানাকে physical 0xFE001008-এ অনুবাদ করে; page-টি I/O, তাই cache এড়িয়ে ক্রম রেখে বেরোবে।
  2. ধাপ ২ — ঠিকানা দেখে পথ। Chip-এর interconnect ঠিকানাটি DRAM-এ নয়, PCIe-র MMIO জানালায় পায় — অনুরোধ যায় root complex-এ। এটি অংশ ৩-এর address decoder-এরই বড় রূপ।
  3. ধাপ ৩ — TLP তৈরি। Root complex একটি Memory Write TLP বানায়: ঠিকানা 0xFE001008, ১ DW, ডেটা 5. Write posted, তাই core উত্তরের অপেক্ষা করে না; MMIO load হলে completion-এর জন্য অপেক্ষা করতে হতো।
  4. ধাপ ৪ — data link layer. সামনে sequence নম্বর, পেছনে ৩২-বিটের LCRC. একটি কপি replay buffer-এ থাকে; ACK এলে মোছে, NAK এলে hardware আবার পাঠায়।
  5. ধাপ ৫ — তারে। Gen3 x4: byte-গুলো চার lane-এ ভাগ হয়, প্রতিটিতে scramble আর 128b/130b করে ৮ GT/s-এ যায়; গ্রহীতা lane-গুলোর skew মিলিয়ে byte জোড়া লাগায়।
  6. ধাপ ৬ — switch. Switch ঠিকানাটি downstream port-গুলোর base/limit-এর সাথে মেলায় — port 2-এর পরিসর 0xFE000000 থেকে, তাই সেখানে পাঠায়। ACK আর replay প্রতিটি link-এ আলাদা।
  7. ধাপ ৭ — endpoint. ঠিকানাটি BAR0-এর (0xFE000000, ১৬ KiB) ভেতরে, offset 0x1008 — একটি doorbell. ৫ লেখা মানে “queue-র ৫ নম্বর ঘরের আগ পর্যন্ত নতুন কাজ জমা আছে” (tail দেখায় পরের খালি ঘর)। এরপর যন্ত্র নিজেই DMA দিয়ে কাজ পড়বে (পর্ব ৫২)।

৮ · USB, আর অন্যান্য সংযোগ — SATA, NVMe, I2C, CXL; chipset আর SoC

USB — host-নিয়ন্ত্রিত tiered star

USB (Universal Serial Bus) এসেছিল ১৯৯০-এর দশকের মাঝামাঝি, PC-র পেছনের নানা রকম port (serial, parallel, কীবোর্ড-mouse-এর আলাদা port) একটি সাধারণ, plug-and-play সংযোগে বদলাতে। তার নকশার মূল কথাগুলো:

  • Host সব নিয়ন্ত্রণ করে। Host controller সব transfer-এর সময়সূচি বানায়; device শুধু জিজ্ঞেস করলে উত্তর দেয়। “Interrupt transfer”-ও আসলে host-এর নির্দিষ্ট বিরতিতে polling.
  • Tiered star. Host-এর root hub থেকে hub, hub থেকে আরও hub বা device — গাছের মতো স্তরে স্তরে। একটি bus-এ ৭-বিট ঠিকানা, তাই সর্বোচ্চ ১২৭টি device; স্তরের সংখ্যাও মান অনুযায়ী সীমিত।
  • Endpoint আর চার ধরনের transfer. প্রতিটি device-এর ভেতরে কয়েকটি endpoint — ডেটার আলাদা “জানালা”; endpoint 0 সবসময় control-এর জন্য।
Transferকী নিশ্চিত করেউদাহরণ
Controlছোট নির্দেশ ও উত্তর, ভুল হলে আবার পাঠানোenumeration, সেটিং বদলানো
Bulkনির্ভুল ডেটা, কিন্তু সময়ের নিশ্চয়তা নেই — ফাঁকা bandwidth-এ যায়pen drive, প্রিন্টার
Interruptছোট ডেটা, host নির্দিষ্ট বিরতিতে জিজ্ঞেস করবে — latency-র সীমাকীবোর্ড, mouse, game controller
Isochronousনির্দিষ্ট bandwidth প্রতি বিরতিতে সংরক্ষিত, কিন্তু ভুল হলে আবার পাঠানো হয় নাwebcam, USB মাইক, অডিও

গতি আর নামের জট. নামগুলো সত্যিই বিভ্রান্তিকর, তাই সংখ্যা দিয়ে মনে রাখাই নিরাপদ। USB 1.x: Low Speed ১.৫ Mbit/s আর Full Speed ১২ Mbit/s. USB 2.0 (২০০০ সালের দিকে): High Speed ৪৮০ Mbit/s. USB 3.0 নিয়ে আসে ৫ Gbit/s (“SuperSpeed”, 8b/10b encoding) — পরে এই একই গতির নাম বদলে হয় “USB 3.1 Gen 1”, তারপর “USB 3.2 Gen 1”. ১০ Gbit/s (128b/132b) = “USB 3.1 Gen 2” = “USB 3.2 Gen 2”; দুই lane মিলিয়ে ২০ Gbit/s = “USB 3.2 Gen 2x2”. USB4 ২০ বা ৪০ Gbit/s, আর তার পরের সংস্করণে আরও বেশি বলে ঘোষণা হয়েছে। তাই বাক্সে শুধু “USB 3.2” দেখলে গতি নিশ্চিত নয় — সংখ্যায় লেখা গতি (“USB 10Gbps”) খুঁজুন।

USB-C সংযোগকারী, protocol নয়. USB-C শুধু প্লাগ আর সকেটের আকার (দুই দিকেই ঢোকানো যায়)। একটি USB-C তার বা port USB 2.0-এর গতিতে চলতে পারে, ১০ Gbit/s-এ চলতে পারে, বা Thunderbolt/USB4, DisplayPort-এর সংকেতও বহন করতে পারে — নির্ভর করে port, তার আর device তিনটিই কী সমর্থন করে তার ওপর।

একটি USB mouse লাগালে host কীভাবে তাকে চেনে — enumeration-এর ধাপগুলো ধারণাগত স্তরে:

  1. লাগানোport 3hub টের পায়
  2. Resetaddr 0default
  3. Device desc.VID:PIDEP0-তে
  4. ঠিকানাSET_ADDRESS 5নতুন নাম
  5. Config desc.HID, EP1 INinterface
  6. ConfigureSET_CONFIG 1driver
  7. চালুIN, প্রতি 1 mshost polls
  1. ধাপ ১ — সংযোগ টের পাওয়া। Mouse লাগালে hub-এর port 3-এ বৈদ্যুতিক পরিবর্তন হয়। Hub নিজে host-কে ডাকতে পারে না — host নিয়মিত hub-এর status জিজ্ঞেস করলে hub জানায় “port 3-এ পরিবর্তন”।
  2. ধাপ ২ — reset. Host hub-কে port 3 reset করতে বলে। Mouse এখন “default” অবস্থায়, ঠিকানা ০ — তাই host নতুন device এক এক করে enumerate করে।
  3. ধাপ ৩ — device descriptor. ঠিকানা ০, endpoint 0-এ GET_DESCRIPTOR (Device): উত্তরে USB সংস্করণ, endpoint 0-এর packet-এর মাপ, vendor ID আর product ID (VID:PID), class.
  4. ধাপ ৪ — নিজস্ব ঠিকানা। SET_ADDRESS(5): এখন থেকে mouse শুধু ঠিকানা ৫-এ সাড়া দেয়, ঠিকানা ০ পরের device-এর জন্য খালি।
  5. ধাপ ৫ — configuration descriptor. GET_DESCRIPTOR (Configuration): interface আর তার class (এখানে HID), আর endpoint — যেমন endpoint 1, IN, interrupt, কত byte, কত বিরতিতে।
  6. ধাপ ৬ — configure আর driver. SET_CONFIGURATION(1): device চালু। OS class (HID) দেখে সাধারণ mouse driver বসায়, বা VID:PID দেখে নির্দিষ্ট driver খোঁজে।
  7. ধাপ ৭ — নিয়মিত polling. Host controller এখন endpoint 1-কে নিয়মিত (ধরা যাক প্রতি ১ ms-এ) জিজ্ঞেস করে। Mouse নড়লে উত্তরে কয়েক byte, না নড়লে NAK. এই polling CPU নয়, host controller নিজে করে, আর ডেটা DMA দিয়ে মেমরিতে রেখে CPU-কে জানায়।

অন্যান্য সংযোগ, সংক্ষেপে

  • SATA আর NVMe: SATA হার্ড ডিস্কের যুগের serial সংযোগ (১.৫, ৩, ৬ Gbit/s; 8b/10b-এ সর্বোচ্চ প্রায় ৬০০ MB/s), সাধারণত AHCI নামের controller-নিয়মে, যেখানে একটি command queue-তে সর্বোচ্চ ৩২টি কাজ। NVMe কোনো আলাদা তার নয় — PCIe-র ওপর চলা একটি protocol, flash-এর জন্য নতুন করে নকশা করা: মেমরিতে submission আর completion queue-র জোড়া, প্রতিটি CPU core-এর নিজস্ব queue রাখা যায়, আর মান অনুযায়ী queue-র সংখ্যা ও গভীরতা দুটিই হাজারের ঘরে (প্রায় ৬৪ হাজার পর্যন্ত বলে উল্লেখ করা হয়)।
  • I2C, SPI, UART — embedded জগৎ: sensor, EEPROM, ছোট display-এর জন্য কয়েক তারের ধীর bus. I2C-তে দুটি তার (ডেটা আর clock), ঠিকানা দিয়ে অনেক যন্ত্র একই জোড়ায়; SPI-তে আলাদা clock আর chip-select, দ্রুত কিন্তু তার বেশি। বিস্তারিত পর্ব ৫৬-এ (Power, energy আর embedded সিস্টেম)।
  • Thunderbolt আর USB4: একটি তারের ভেতরে PCIe, DisplayPort আর USB-এর ট্রাফিক packet আকারে “tunnel” করে নেওয়ার প্রযুক্তি; Thunderbolt 3 থেকে USB-C সংযোগকারী, আর USB4 তার protocol-এর ভিত্তিতে তৈরি বলে জানা যায়।
  • CXL: PCIe-এর ভৌত স্তরের ওপর তিনটি protocol (CXL.io, CXL.cache, CXL.mem) — accelerator-কে CPU-র cache-এর সাথে coherent রাখা, আর বাইরের মেমরি load/store দিয়ে ব্যবহার। মেমরি হায়ারার্কির নতুন স্তর হিসেবে এর ধারণা পর্ব ৪৪-এ (মেমরি হায়ারার্কি আর locality).

Chipset থেকে SoC — একটি আধুনিক যন্ত্রের মানচিত্র

২০০০-এর দশকের PC-তে CPU-র পাশে থাকত “northbridge” (memory controller, গ্রাফিক্স) আর “southbridge” (USB, SATA, অডিও). পরে memory controller আর দ্রুত PCIe lane CPU chip-এর ভেতরে চলে আসে; Intel-এর PC-তে ধীর I/O থাকে একটি platform controller hub (PCH)-এ। ফোন আর অনেক ল্যাপটপে প্রায় সবকিছু একটি SoC-এ। নির্মাতাভেদে ভাগ ভিন্ন, তবে মোটামুটি ছবিটি এমন:

CPU / SoC chip core + cache memory controller interconnect + PCIe root complex (MMIO decode) DRAM GPU PCIe x16 NVMe SSD PCIe x4 platform controller hub USB · SATA · network · অডিও (SoC-এ এগুলোও chip-এর ভেতরে)
ছবি ৫১.১ — দ্রুত অংশ (DRAM, GPU, NVMe) সরাসরি CPU chip-এ; ধীর I/O একটি hub-এর মাধ্যমে। ছবিটি ধারণাগত — বাস্তব প্ল্যাটফর্মে lane-এর সংখ্যা আর ভাগ ভিন্ন।

৯ · I/O-র পারফরম্যান্স — latency, throughput আর Little-এর সূত্র

I/O-র গতি মাপার দুটি আলাদা প্রশ্ন আছে (পর্ব ০৩-এর latency বনাম throughput):

  • Latency: একটি অনুরোধের শুরু থেকে শেষ — যেমন একটি ৪ KiB পড়া SSD থেকে কত µs-এ আসে। মানুষ যে “ধীর” অনুভব করে, তা সাধারণত latency.
  • Throughput: প্রতি সেকেন্ডে কত কাজ বা কত byte — যেমন IOPS (প্রতি সেকেন্ডে I/O অনুরোধ) বা MB/s. Server-এর ক্ষমতা সাধারণত throughput-এ মাপা হয়।

দুটিকে জোড়ে একটি সরল কিন্তু খুব শক্তিশালী সূত্র — Little-এর সূত্র:

L = λ × W   (গড়ে ব্যবস্থার ভেতরে কাজ = আগমনের হার × প্রতিটির গড় সময়)

শর্ত শুধু স্থিতিশীল অবস্থা (গড়ে যত কাজ ঢোকে, তত বেরোয়); আগমনের ধরন বা সেবার নিয়ম যাই হোক, সূত্র খাটে। একটি সহজ যুক্তি: দীর্ঘ T সময়ে মোট λT-টি কাজ আসে, প্রতিটি গড়ে W সময় ভেতরে থাকে, তাই “কাজ × সময়”-এর মোট যোগফল λT × W; এটিকে T দিয়ে ভাগ করলে যেকোনো মুহূর্তে গড়ে ভেতরে থাকা কাজ = λW.

উদাহরণ ১ — queue depth. একটি NVMe SSD-র একটি ৪ KiB পড়ার latency প্রায় ১০০ µs (ধরে নেওয়া)। একটি প্রোগ্রাম যদি একটি করে অনুরোধ পাঠায় আর উত্তরের অপেক্ষা করে (L = ১), তাহলে λ = L ÷ W = ১ ÷ ১০০ µs = ১০,০০০ IOPS = ১০,০০০ × ৪০৯৬ byte ≈ ৪০.৯৬ MB/s — যে SSD কয়েক GB/s দিতে পারে, তার একটি ক্ষুদ্র অংশ। ৫,০০,০০০ IOPS পেতে হলে একসাথে চালু থাকা অনুরোধ L = ৫ × ১০৫ × ১০০ × ১০−৬ = ৫০টি (তখন ২.০৪৮ GB/s) — অবশ্যই যদি latency না বাড়ে। এ কারণেই NVMe-তে গভীর, বহু queue; আর এ কারণেই একটি thread-এর synchronous পড়া দ্রুত SSD-কেও ধীর দেখায়।

উদাহরণ ২ — link-এ. PCIe-র প্রতি দিকে ১৫.৭৫ GB/s আর একটি read-এর আসা-যাওয়া ১ µs ধরলে, link ব্যস্ত রাখতে একসাথে উড়ন্ত ডেটা লাগে প্রায় ১৫.৭৫ KB — তাই যন্ত্র একসাথে অনেক read অনুরোধ পাঠায় (network-এর একই ধারণা: Delay আর throughput).

আরও গভীরে: দ্রুত I/O-তে আজকের বড় প্রবণতা হলো CPU-কে পথ থেকে সরানো আর যন্ত্রকে মেমরির কাছে আনা। Server-এর network card-এ নিজস্ব processor (“SmartNIC” বা DPU) packet-এর কাজ নিজে সারে; কিছু প্ল্যাটফর্মে যন্ত্রের DMA-লেখা সরাসরি CPU-র শেষ স্তরের cache-এ পৌঁছায়, যাতে driver DRAM থেকে না পড়ে; আর CXL মেমরিকে PCIe-র তারে CPU থেকে দূরে বসানোর সুযোগ দেয়। ঠিক কোন chip কোন কৌশল কতটা নিয়েছে, তা নির্মাতার নথিভেদে ভিন্ন; প্রকাশিত পরিমাপগুলো আন্দাজ হিসেবে নেবেন।

১০ · AI-এর যুগে — I/O আর bus-এর হিসাবে AI, আর যাচাইয়ের অভ্যাস

AI টুল driver-এর কাঠামো আর ধারণা ভালো ব্যাখ্যা করে, কিন্তু I/O-তে সংখ্যা, একক আর register-এর খুঁটিনাটিতে প্রায়ই আত্মবিশ্বাসী ভুল করে। এই বিষয়ে সবচেয়ে বেশি দেখা যায়:

AI-এর প্রচলিত ভুলঠিক কথাকীভাবে যাচাই
GT/s-কে সরাসরি GB/s বা Gbit/s-এর ডেটা হার ধরাGT/s তারের বিট; encoding বাদ দিয়ে, ৮ দিয়ে ভাগ করে byteঅংশ ৬-এর calculator-এ বসানো
Gen3-এও 8b/10b ধরা, বা Gen1-এ 128b/130bGen1–2 8b/10b; Gen3–5 128b/130bPCI-SIG-এর প্রকাশিত সারসংক্ষেপ
“volatile মানে memory barrier”volatile শুধু compiler-কে বাঁধে; CPU-র ক্রমে fence লাগেISA manual-এর memory model অংশ, OS-এর accessor
16550-এর LSR-কে offset ৪ বা THRE-কে বিট ৬ বলাLSR = +5, THRE = বিট ৫ (0x20), TEMT = বিট ৬UART-এর datasheet বা emulator-এর উৎস
USB-এর নামকে গতি ধরা (“USB 3.2 মানে ২০ Gbit/s”)USB 3.2 Gen 1 = ৫, Gen 2 = ১০, Gen 2x2 = ২০ Gbit/sসংখ্যা দিয়ে লেখা গতি খোঁজা

AI-কে প্রশ্ন করার সময় একক, দিক আর ISA স্পষ্ট করে চান:

আমি কম্পিউটার আর্কিটেকচারে I/O আর bus শিখছি। ISA: RISC-V (RV32I), memory-mapped I/O. ১. একটি 16550-ধাঁচের UART base 0x10000000-এ। LSR-এর ঠিকানা, THRE বিটের মান আর polling দিয়ে একটি byte পাঠানোর C ও RV32I কোড দাও; কোথায় volatile আর কোথায় fence লাগতে পারে, কারণসহ বলো। ২. PCIe Gen3 x4 আর Gen2 x8-এর কার্যকর bandwidth প্রতি দিকে কত? GT/s থেকে encoding বাদ দিয়ে byte-এ যাওয়ার প্রতিটি ধাপ দেখাও। ৩. 2 GHz CPU, প্রতি poll 400 cycle, 1 Gbit/s ডেটা 64 byte করে — polling-এ CPU-র কত শতাংশ যায়? ৪. যে সংখ্যা কোনো নির্দিষ্ট মান বা chip-এর ওপর নির্ভর করে, তা আলাদা করে চিহ্নিত করো আর বলো কোন নথিতে মিলিয়ে দেখব। x86-এর port I/O-র নিয়ম RISC-V-এর সাথে মিশিও না।

AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

  • Memory-mapped আর port-mapped I/O-র পার্থক্য কী? — MMIO-তে device register সাধারণ ঠিকানায়, load/store দিয়ে; port I/O-তে আলাদা ঠিকানা-জগৎ আর বিশেষ instruction (x86-এর in/out).
  • MMIO-র status register পড়তে volatile কেন লাগে? — নাহলে compiler পড়াটি লুপের বাইরে তুলে বা বাদ দিয়ে দিতে পারে, কারণ সে জানে না মান বাইরে থেকে বদলায়।
  • PCIe Gen3 x16-এর কার্যকর bandwidth? — ৮ × ১৬ × ১২৮/১৩০ ÷ ৮ ≈ ১৫.৭৫ GB/s প্রতি দিকে।
  • Polling কখন interrupt-এর চেয়ে ভালো? — ঘটনা খুব ঘন হলে বা latency খুব কম রাখতে হলে, যখন interrupt-এর প্রতি-ঘটনা খরচ বেশি পড়ে।

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

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

প্র ০১x86 আজও in/out instruction রেখেছে, অথচ Motorola 68000, ARM আর RISC-V শুরু থেকেই শুধু memory-mapped I/O বেছে নিয়েছে। এই দুই পথের পেছনের যুক্তি আর ইতিহাস কী?

১৯৭০-এর দশকের ৮-বিট microprocessor-এর ঠিকানা-জগৎ ছিল মাত্র ৬৪ KiB (১৬-বিটের ঠিকানা)। Intel 8080-এর নকশাকারেরা I/O-র জন্য আলাদা ২৫৬টি port রেখেছিলেন — যাতে ছোট মেমরির একটি byte-ও যন্ত্রের জন্য খরচ না হয়, আর ঠিকানা decode করার circuit সরল থাকে (port নম্বর মাত্র ৮ বিট)। 8086 সেই ধারা নেয়, port বাড়িয়ে ৬৪ Ki করে। ১৯৮১ সালের IBM PC এই port-গুলোতে কীবোর্ড controller, timer, serial আর parallel port বসায়, আর হাজারো software সেই ঠিকানা ধরে লেখা হয়। x86-এর সবচেয়ে বড় শক্তি — পুরোনো software চালানো — তাই port I/O-কে আজও টিকিয়ে রেখেছে।

একই সময়ে Motorola 68000 (১৯৭৯) ভিন্ন পথ নেয়: বড় (২৪-বিট) ঠিকানা-জগৎ থাকায় কিছু ঠিকানা যন্ত্রকে দিলে ক্ষতি নেই, আর আলাদা instruction না রাখলে ISA পরিষ্কার থাকে — সব addressing mode, সব instruction যন্ত্রের register-এও চলে। RISC আন্দোলন (পর্ব ২৫) এই যুক্তিকে আরও জোর দেয়: load/store architecture-এ মেমরি ছোঁয়ার একমাত্র পথ load আর store; I/O-র জন্য আলাদা পথ মানে অপ্রয়োজনীয় জটিলতা। সুরক্ষাও সহজ — সাধারণ page table-এর অনুমতিই যথেষ্ট।

মজার ব্যাপার, ব্যবহারে x86-ও অনেক আগেই MMIO-র দিকে চলে গেছে। PCI-যুগ থেকে বড় যন্ত্রগুলো memory BAR দিয়ে register দেখায়; ৬৪ Ki port GPU-র কয়েক MiB register বা NVMe-র doorbell-এর জন্য একেবারেই যথেষ্ট নয়। Port I/O আজ মূলত boot-এর প্রথম মুহূর্ত, পুরোনো যন্ত্রের অনুকরণ (emulation) আর কিছু virtual machine-এর সরল যন্ত্রে দেখা যায়।

মূল উপলব্ধি: port I/O ছিল ছোট ঠিকানা-জগতের যুগের যুক্তিসঙ্গত সমাধান, আর সামঞ্জস্য তাকে বাঁচিয়ে রেখেছে। বড় ঠিকানা-জগতে memory-mapped I/O সরল, নমনীয় আর সুরক্ষায় সুবিধাজনক — তবে দাম হিসেবে cache আর ক্রমের নিয়ম স্পষ্ট করে ভাবতে হয়।

প্র ০২Hard disk-এর চওড়া ribbon cable থেকে সরু SATA তার, PCI slot থেকে PCIe, প্রিন্টারের parallel port থেকে USB — প্রায় সব জায়গায় parallel হেরে গেছে serial-এর কাছে। “বেশি তারে বেশি গতি” ধারণাটি কোথায় ভুল?

সরল অঙ্কে parallel-ই জেতার কথা: ৩২টি তারে একসাথে ৩২ বিট, একটি তারে একটি। কম clock-এ সেটি সত্যিই ছিল, তাই ১৯৮০ আর ৯০-এর দশকে ISA, PCI, PATA (IDE) — সব parallel. সমস্যা শুরু হয় যখন clock শত MHz ছাড়ায়। একটি বিটের সময়কাল তখন কয়েক nanosecond-এর নিচে, আর ৩২টি তারের সংকেতের পৌঁছানোর সময়ের সামান্য পার্থক্য (skew) সেই সময়ের বড় অংশ। সব বিট একই clock edge-এ ধরতে হয়, তাই সবচেয়ে ধীর তারই পুরো bus-এর গতি ঠিক করে। পাশাপাশি তারে crosstalk বাড়ে — PATA-র দ্রুত mode-এর জন্য ৪০ তারের cable-এর জায়গায় ৮০ তারের cable আনা হয়েছিল, যেখানে বাড়তি তারগুলো ছিল শুধু ground, সংকেতের মাঝে আড়াল হিসেবে। আর প্রতিটি তার chip-এ একটি pin, board-এ একটি পথ, connector-এ একটি পিন — চওড়া bus দামি আর ভারী।

Serial link এই সমস্যাগুলো এড়ায় তিনটি কৌশলে। প্রথম, differential signalling: দুটি তারে উল্টো সংকেত, গ্রহীতা পার্থক্য মাপে, তাই বাইরের গোলমাল দুই তারে সমান পড়ে কাটাকাটি হয়, আর খুব ছোট voltage-এ খুব দ্রুত বদলানো যায়। দ্বিতীয়, embedded clock: আলাদা clock তার নেই, গ্রহীতা ডেটার বিট-বদল থেকে নিজের clock মেলায় — তাই clock আর ডেটার মধ্যে skew-এর প্রশ্নই নেই। এজন্যই 8b/10b-এর মতো encoding, যা নিশ্চিত করে যে ডেটায় যথেষ্ট বদল আছে আর ০ ও ১ মোটামুটি সমান (DC balance). তৃতীয়, গতি বাড়াতে lane যোগ: প্রতিটি lane স্বাধীন, গ্রহীতা lane-গুলোর মধ্যের skew buffer দিয়ে মেলায় — parallel bus-এর মতো সব বিট এক edge-এ ধরার কঠোরতা নেই।

দাম আছে: SerDes circuit জটিল আর শক্তি খায়, latency কয়েক nanosecond বাড়ে, আর encoding কিছু bandwidth খায়। তবুও উচ্চ গতিতে এই দাম পুরো লাভের তুলনায় ছোট। লক্ষ করুন, DDR মেমরি এখনো parallel — তার ছোট, সাবধানে মাপা, আর latency সেখানে প্রধান।

মূল উপলব্ধি: উচ্চ গতিতে সীমা তারের সংখ্যা নয়, সময়ের মিল। Serial link সময়ের সমস্যাকে প্রতিটি lane-এর ভেতরে স্থানীয় করে দেয় — তাই কম তারে বেশি গতি সম্ভব।

প্র ০৩পাঠ্যবই বলে polling অপচয়, interrupt ভালো। অথচ খুব দ্রুত network আর storage-এর software-এ আবার polling ফিরে এসেছে। এই উল্টো যাত্রার কারণ কী?

অংশ ৫-এর হিসাব একটি অনুমানের ওপর দাঁড়িয়ে: যন্ত্র বেশিরভাগ সময় অলস, তাই polling-এর বেশিরভাগ চেষ্টা ব্যর্থ। Interrupt সেই অপচয় সরায়, কিন্তু প্রতিটি ঘটনায় নিজের দাম নেয়: pipeline থামানো, context বাঁচানো, handler-এ যাওয়া, cache আর branch predictor-এর অবস্থা নষ্ট হওয়া, ফিরে আসা — আধুনিক CPU-তে সব মিলিয়ে কয়েক µs পর্যন্ত হতে পারে। যখন একটি ১০ বা ১০০ Gbit/s network card সেকেন্ডে লক্ষ লক্ষ packet আনে, তখন প্রতি packet-এ একটি interrupt মানে CPU শুধু interrupt সামলাতেই ব্যস্ত — একে বলা হয় “interrupt livelock”: যন্ত্র ক্রমাগত কাজ দিচ্ছে, কিন্তু কোনো কাজ শেষ হচ্ছে না।

তাই বাস্তব ব্যবস্থাগুলো মিশ্র পথ নেয়। Linux-এর network stack-এ (NAPI নামে পরিচিত) প্রথম packet-এ interrupt আসে, তারপর driver interrupt বন্ধ করে polling-এ চলে যায় — যতক্ষণ packet আসতে থাকে, একবারে অনেকগুলো তুলে নেয়; queue খালি হলে আবার interrupt চালু করে। যন্ত্রগুলোও interrupt coalescing করে: কয়েকটি ঘটনা বা কয়েক µs জমিয়ে একটি interrupt. আরও চরম প্রান্তে, DPDK আর SPDK-এর মতো user-space কাঠামো একটি বা কয়েকটি CPU core-কে পুরোপুরি polling-এ বসিয়ে রাখে — সেই core-এর ১০০% polling-এ যায়, বিনিময়ে প্রতি packet বা প্রতি I/O-তে latency খুব কম আর throughput খুব বেশি।

এখানে Little-এর সূত্রও কাজ করে: SSD-র latency যত কমে (দশ µs-এর ঘরে), interrupt-এর কয়েক µs খরচ মোট latency-র তত বড় অংশ হয়ে যায়। দশ ms-এর hard disk-এ interrupt-এর খরচ ছিল নগণ্য; দশ µs-এর SSD-তে তা চোখে পড়ার মতো। দাম হলো শক্তি: polling core কখনো ঘুমায় না, তাই ল্যাপটপে বা ব্যাটারির যন্ত্রে এটি সাধারণত গ্রহণযোগ্য নয়।

মূল উপলব্ধি: polling আর interrupt-এর মধ্যে “ভালো” বলে কিছু নেই — আছে ঘটনার হার আর প্রতিটি পদ্ধতির স্থির খরচের তুলনা। ঘটনা বিরল হলে interrupt, ঘটনা অবিরাম হলে polling, আর মাঝখানে দুটির বুদ্ধিমান মিশ্রণ।

প্র ০৪USB-তে device কখনো নিজে থেকে কথা বলে না, সব host নিয়ন্ত্রণ করে। এই নকশার লাভ-ক্ষতি কী, আর USB-র নাম ও সংযোগকারীর বিভ্রান্তি কীভাবে তৈরি হলো?

১৯৯০-এর দশকের মাঝামাঝি একটি PC-র পেছনে ছিল নানা রকম port: কীবোর্ড আর mouse-এর আলাদা port, serial port, প্রিন্টারের parallel port, game port. প্রতিটির আলাদা connector, আলাদা driver, প্রায়ই যন্ত্র লাগানোর আগে কম্পিউটার বন্ধ করতে হতো আর interrupt নম্বর হাতে বসাতে হতো। কয়েকটি বড় কম্পিউটার আর chip কোম্পানির একটি জোট USB নকশা করে, যার প্রথম সংস্করণ ১৯৯৬ সালে আসে। লক্ষ্য ছিল সস্তা device: একটি mouse-এর chip যত সরল, তত ভালো।

Host-কেন্দ্রিক নকশা সেই লক্ষ্যেরই ফল। Device-কে arbitration জানতে হয় না, কখন কথা বলবে ভাবতে হয় না — শুধু জিজ্ঞেস করলে উত্তর দেয়। Bandwidth-এর হিসাব host রাখে, তাই isochronous transfer-এর জন্য নির্দিষ্ট অংশ আগেই সংরক্ষণ করা যায়, আর webcam আর অডিও একই bus-এ শান্তিতে চলে। Enumeration-এর মাধ্যমে যন্ত্র নিজের পরিচয় দেয়, তাই plug-and-play. ক্ষতি হলো latency আর দক্ষতা: mouse নড়লেও host-এর পরের poll পর্যন্ত অপেক্ষা (ধরুন ১ ms পর্যন্ত), আর অলস device-কেও নিয়মিত জিজ্ঞেস করতে হয়, যা শক্তি খরচ করে।

নামের জট এসেছে মান-সংস্থার সংস্করণ-নম্বর আর গতির নাম একসাথে বদলানোর কারণে: ৫ Gbit/s-এর একই গতি তিনবার তিন নামে (USB 3.0, 3.1 Gen 1, 3.2 Gen 1). সাথে যোগ হয়েছে USB-C — একটি সংযোগকারী, যা USB 2.0 থেকে USB4, DisplayPort, এমনকি শুধু চার্জিং — সবই বহন করতে পারে। ফলে একই রকম দেখতে দুটি তারের ক্ষমতা সম্পূর্ণ ভিন্ন হতে পারে। প্রকৌশলীর জন্য শিক্ষা: সংস্করণের নাম নয়, প্রকৃত signalling হার, lane-এর সংখ্যা আর encoding জিজ্ঞেস করতে হয়।

মূল উপলব্ধি: নিয়ন্ত্রণ এক জায়গায় রাখলে প্রান্তের যন্ত্র সস্তা আর সরল হয়, আর সময়ের নিশ্চয়তা দেওয়া সহজ হয় — দাম হলো latency আর polling-এর খরচ। আর একটি মানের সাফল্য তার নামকরণের স্পষ্টতার ওপরও নির্ভর করে।

প্র ০৫SATA SSD আর NVMe SSD-তে একই ধরনের flash থাকতে পারে, তবু NVMe অনেক দ্রুত। শুধু তারের গতি, না queue-র নকশাও এর কারণ? Little-এর সূত্র দিয়ে ব্যাখ্যা করুন।

SATA আর তার controller-নিয়ম AHCI নকশা হয়েছিল ঘূর্ণায়মান hard disk-এর জন্য। একটি hard disk-এর একটি মাথা, একবারে একটি জায়গায় পড়ে; একটি অনুরোধে কয়েক ms লাগে। সেখানে একটি command queue আর তাতে সর্বোচ্চ ৩২টি কাজ (NCQ) যথেষ্টের চেয়েও বেশি — disk সেগুলোর ক্রম সাজিয়ে মাথার নড়াচড়া কমাত। Flash-এর গঠন সম্পূর্ণ ভিন্ন: একটি SSD-তে অনেকগুলো flash chip আর channel, প্রতিটি স্বাধীনভাবে কাজ করতে পারে, আর একটি পড়ায় লাগে দশ থেকে শত µs.

Little-এর সূত্র বলে, throughput = (একসাথে চালু কাজ) ÷ latency. ধরুন একটি SSD-র ভেতরে ৬৪টি স্বাধীন flash অংশ, প্রতিটি পড়ায় ১০০ µs. সব অংশ ব্যস্ত রাখতে একসাথে অন্তত ৬৪টি অনুরোধ লাগবে — তাতে সর্বোচ্চ ৬৪ ÷ ১০০ µs = ৬,৪০,০০০ IOPS. AHCI-র ৩২টি কাজের সীমায় সর্বোচ্চ ৩২ ÷ ১০০ µs = ৩,২০,০০০ IOPS — আর তারের ৬০০ MB/s সীমায় ৪ KiB পড়ায় আরও কম, প্রায় ১,৪৬,০০০. অর্থাৎ SATA SSD দুই দিক থেকেই আটকে: queue-র গভীরতা আর link-এর bandwidth.

NVMe (প্রথম মান ২০১১ সালের দিকে) দুটিই সরায়। PCIe x4 Gen3-এ প্রায় ৩.৯৪ GB/s, আর queue-র নকশা বহু-core CPU-র জন্য: প্রতিটি core-এর নিজস্ব submission/completion queue জোড়া, তাই একাধিক core-কে একটি queue-র জন্য lock নিয়ে লড়তে হয় না; একটি কাজ জমা দিতে শুধু মেমরিতে একটি entry লেখা আর একটি doorbell register-এ একটি MMIO store — অংশ ৭-এর stepper-এর ঠিক সেই পথ। তবে সূত্রটি উল্টো দিকেও সতর্ক করে: application যদি একটি করে অনুরোধ পাঠিয়ে অপেক্ষা করে (queue depth ১), তাহলে NVMe-র সব queue অলস থাকে, আর throughput = ১ ÷ latency — SATA আর NVMe-র পার্থক্য তখন অনেক কম মনে হয়।

মূল উপলব্ধি: দ্রুত যন্ত্রের পূর্ণ গতি পেতে দুটি জিনিস লাগে — চওড়া পথ আর যথেষ্ট সমান্তরাল কাজ। Interface-এর নকশা যদি সমান্তরালতাকে আটকে রাখে, তারের গতি বাড়িয়েও লাভ সীমিত।

অনুশীলন

পরীক্ষার ধাঁচে লেখা — আগে খাতায় সমাধান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. Polling-এর খরচ: একটি CPU ৩ GHz-এ চলে, প্রতিটি poll-এ ৩০০ cycle লাগে। (ক) একটি যন্ত্র ২০০ MB/s হারে ডেটা দেয়, প্রতি poll-এ ৬৪ byte সরানো যায়। CPU-র কত শতাংশ polling-এ যায়? (খ) প্রতি poll-এ মাত্র ১৬ byte হলে? (গ) একটি কীবোর্ড সেকেন্ডে ৫০ বার poll করা হয় — শতাংশ কত? (ঘ) (খ)-এর ফল থেকে কী সিদ্ধান্ত নেবেন?
    • (ক) poll/s = ২০০ × ১০৬ ÷ ৬৪ = ৩১,২৫,০০০. Cycle/s = ৩১,২৫,০০০ × ৩০০ = ৯.৩৭৫ × ১০৮. অংশ = ৯.৩৭৫ × ১০৮ ÷ ৩ × ১০৯ = ৩১.২৫%.
    • (খ) poll/s = ১.২৫ × ১০৭; cycle/s = ৩.৭৫ × ১০৯ > ৩ × ১০৯ → ১২৫% — অসম্ভব; CPU পুরো সময় poll করেও তাল রাখতে পারবে না, ডেটা হারাবে।
    • (গ) ৫০ × ৩০০ = ১৫,০০০ cycle/s ÷ ৩ × ১০৯ = ৫ × ১০−৬ = ০.০০০৫%.
    • (ঘ) দ্রুত যন্ত্রে DMA আর interrupt লাগবে (পর্ব ৫২); কীবোর্ডে polling-ই যথেষ্ট।
  2. Bus আর PCIe-এর bandwidth: (ক) একটি synchronous bus ৬৪ বিট চওড়া, ১০০ MHz. প্রতি transaction-এ ১টি address cycle, তারপর ৪টি data cycle (প্রতিটিতে ৮ byte), তারপর ১টি বিশ্রামের (turnaround) cycle. Peak আর কার্যকর bandwidth কত? (খ) Data cycle-গুলো DDR হলে (প্রতি cycle-এ ২ transfer, তাই ৪টি transfer ২ cycle-এ) কার্যকর bandwidth? (গ) PCIe Gen3 x16-এর কার্যকর bandwidth প্রতি দিকে কত? একটি GPU-তে ১২ GB ডেটা পাঠাতে কমপক্ষে কত সময়? (ঘ) প্রতিটি ২৫৬ byte payload-এর TLP-তে আরও ২৪ byte বাড়তি (header, sequence, LCRC, framing — ধরে নেওয়া) থাকলে বাস্তব সর্বোচ্চ হার কত?
    • (ক) Peak = ৮ byte × ১০০ MHz = ৮০০ MB/s. প্রতি transaction-এ ৩২ byte, ৬ cycle → ৩২ × ১০৮ ÷ ৬ ≈ ৫৩৩.৩ MB/s (peak-এর ৬৬.৭%).
    • (খ) এখন ১ + ২ + ১ = ৪ cycle-এ ৩২ byte → ৩২ × ১০৮ ÷ ৪ = ৮০০ MB/s.
    • (গ) ৮ GT/s × ১৬ × ১২৮/১৩০ ÷ ৮ ≈ ১৫.৭৫ GB/s. সময় = ১২ ÷ ১৫.৭৫৪ ≈ ০.৭৬ s (packet-এর খরচ বাদ দিয়ে)।
    • (ঘ) দক্ষতা = ২৫৬ ÷ (২৫৬ + ২৪) = ২৫৬/২৮০ ≈ ৯১.৪৩%. হার ≈ ১৫.৭৫৪ × ০.৯১৪৩ ≈ ১৪.৪০ GB/s. ছোট TLP-তে দক্ষতা আরও কমে।
  3. MMIO ঠিকানা decode আর BAR: একটি RISC-V board-এর map: RAM 0x80000000 থেকে, 16550-ধাঁচের UART 0x10000000-এ ২৫৬ byte-এর জানালায় (register-গুলো offset ০–৭-এ, প্রতিটি ১ byte). (ক) জানালাটি পুরোপুরি decode করতে ঠিকানার কোন বিটগুলো তুলনা করতে হবে, কোন মানের সাথে? (খ) lbu t1, 5(t0), যেখানে t0 = 0x10000000 — কোন register পড়া হলো? sb দিয়ে 0x10000000-এ লিখলে আর lbu দিয়ে একই ঠিকানা পড়লে কি একই register? (গ) সস্তা নকশায় decoder শুধু বিট ৩১–২৮ দেখে (= 0x1). তাহলে 0x10000105-এ পড়লে কী হবে? (ঘ) একটি PCIe memory BAR-এ 0xFFFFFFFF লিখে পড়লে 0xFFF00008 এল। জানালার আকার কত, আর বিট ৩ কী বলে?
    • (ক) ২৫৬ = 28, তাই নিচের ৮ বিট জানালার ভেতরের offset; বিট ৩১–৮ (২৪টি বিট) তুলনা করতে হবে 0x100000-এর সাথে। (Register বাছতে আসলে লাগে শুধু বিট ২–০.)
    • (খ) ঠিকানা 0x10000005 → offset ৫ → LSR. না — offset ০-তে লেখা যায় THR-এ, পড়া আসে RBR থেকে; একই ঠিকানা, দুটি ভিন্ন register.
    • (গ) বিট ৩১–২৮ = 0x1 মিলে যায়, তাই UART সাড়া দেয়, আর register বাছাই বিট ২–০ = ৫ দিয়ে — অর্থাৎ আবার LSR. এটি address aliasing: একই register অনেক ঠিকানায় দেখা যায়, আর ভুল ঠিকানায় লেখা নীরবে যন্ত্রে পৌঁছায় (পর্ব ১২)।
    • (ঘ) নিচের ৪টি flag বিট মুছলে 0xFFF00000; আকার = ~0xFFF00000 + 1 = 0x00100000 = ১ MiB. বিট ৩ = ১ → prefetchable (পড়ার পার্শ্বপ্রতিক্রিয়া নেই); বিট ২–১ = ০০ → ৩২-বিটের BAR.
  4. Simulator-এর ফল আন্দাজ আর Little-এর সূত্র: (ক) অংশ ৪-এর mmio_uart.js-এ K = 40-এর বদলে K = 20 করলে টেবিলের প্রতিটি সারি আর শেষ লাইন কী ছাপবে, আগে হাতে আন্দাজ করুন, তারপর চালিয়ে মেলান। (খ) একটি SSD-তে গড়ে একটি পড়ার latency ১০০ µs. ৫,০০,০০০ IOPS পেতে গড়ে কতগুলো অনুরোধ একসাথে চালু থাকতে হবে? (গ) Queue depth ১-এ সর্বোচ্চ IOPS আর ৪ KiB পড়ায় MB/s কত?
    • (ক) প্রথম byte আগের মতো: "H" 0 6. পরের byte-গুলোতে শর্ত ১ + ৬n ≥ ২০ → n = ৪টি poll: ৩টি ব্যর্থ, অপেক্ষা ২৪ cycle. তাই "i" 3 24, "!" 3 24, "\n" 3 24. মোট সময়: ৪ (বার্তা RAM-এ লেখা) + ৪ × ১ (অক্ষর আনা) + ৬ + ৩ × ২৪ (poll) + ৪ × ৬ (THR-এ লেখা) = ১১০ cycle; polling = ৬ + ৭২ = ৭৮. শেষ লাইন: sent "Hi!\n" in 110 cycles; 78 spent polling (70.9%).
    • (খ) L = λW = ৫ × ১০৫ × ১০০ × ১০−৬ = ৫০টি.
    • (গ) λ = ১ ÷ ১০০ µs = ১০,০০০ IOPS; × ৪০৯৬ byte = ৪০.৯৬ MB/s.

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

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

ক্রমএকটি driver PCIe যন্ত্রের একটি MMIO register-এ store করল। ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Core uncached store চালায়, MMU physical ঠিকানা দেয়
  2. Address decoder ঠিকানাটি PCIe-র MMIO জানালায় পেয়ে root complex-এ পাঠায়
  3. Root complex একটি Memory Write TLP বানায়
  4. Data link layer sequence নম্বর আর LCRC যোগ করে
  5. Switch ঠিকানার পরিসর মিলিয়ে সঠিক downstream port-এ পাঠায়
  6. Endpoint BAR মিলিয়ে offset অনুযায়ী register-এ লেখে

RISC-V-এর মতো যে ISA-তে কোনো বিশেষ I/O instruction নেই, সেখানে CPU একটি device register কীভাবে পড়ে?

  • in instruction দিয়ে আলাদা port ঠিকানা থেকে
  • CSR পড়ার instruction দিয়ে
  • সাধারণ load instruction দিয়ে, যন্ত্রের জন্য রাখা একটি ঠিকানা থেকে
  • শুধু interrupt handler-এর ভেতর থেকে

C-তে while ((*lsr & 0x20) == 0) ; লুপে lsr একটি device-এর status register-এর pointer. Pointer-টি volatile না হলে -O2-এ কী ঝুঁকি?

  • Compiler register-টিকে cache-এ রেখে দেবে
  • Compiler পড়াটি লুপের বাইরে তুলে নিতে পারে, ফলে লুপ বাইরের বদল আর দেখবে না
  • কোড compile-ই হবে না
  • CPU অন্য store-এর সাথে ক্রম বদলে ফেলবে, যা volatile ঠিক করত

PCIe Gen3-এ প্রতি lane ৮ GT/s, encoding 128b/130b. একটি x4 link-এর প্রতি দিকে কার্যকর bandwidth প্রায় কত (packet-এর খরচ বাদে)?

  • ৩.৯৪ GB/s
  • ৪.০০ GB/s
  • ৩.২০ GB/s
  • ৩২ GB/s

একটি ১ GHz CPU একটি যন্ত্রকে সেকেন্ডে ২০০০ বার poll করে, প্রতিটি poll-এ ৫০০ cycle. CPU-র কত শতাংশ সময় polling-এ যায়?

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

একটি USB webcam-এর ভিডিওর জন্য কোন ধরনের transfer সবচেয়ে উপযুক্ত, যেখানে নির্দিষ্ট bandwidth দরকার কিন্তু হারানো ফ্রেম আবার পাঠানোর দরকার নেই?

  • Bulk
  • Control
  • Isochronous
  • Interrupt

একটি storage ব্যবস্থা স্থিতিশীলভাবে সেকেন্ডে ২,০০,০০০টি I/O সামলায়, আর প্রতিটি I/O গড়ে ৫০ µs ব্যবস্থার ভেতরে থাকে। Little-এর সূত্র অনুযায়ী গড়ে কতগুলো I/O একসাথে চালু?

  • ১
  • ১০
  • ১০০
  • ৪০০০

শব্দকোষ

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

মনে রাখুন

  1. Controller আর register: CPU যন্ত্রের controller-এর status, control আর data register-এ পড়ে-লেখে; এগুলো মেমরির মতো নয় — মান নিজে বদলায়, পড়ারও পার্শ্বপ্রতিক্রিয়া আছে।
  2. দুই ঠিকানা-পদ্ধতি: port-mapped (x86 in/out, আলাদা ৬৪ Ki port) বনাম memory-mapped (RISC-V/ARM, সাধারণ load/store); MMIO-তে uncached অঞ্চল, C-তে volatile, আর ক্রমের জন্য fence (যেমন RISC-V-এর fence w, o).
  3. Polling-এর হিসাব: CPU অংশ = poll/s × cycle/poll ÷ clock. ধীর যন্ত্রে নগণ্য, দ্রুত যন্ত্রে অসম্ভব — তাই interrupt আর DMA (পর্ব ৫২)।
  4. Bandwidth: parallel = width/৮ × clock × transfer/clock; serial = GT/s × lane × encoding ÷ ৮. Gen1–2 8b/10b (৮০%), Gen3–5 128b/130b (≈ ৯৮.৪৬%); Gen3 x4 ≈ ৩.৯৪, x16 ≈ ১৫.৭৫ GB/s প্রতি দিকে।
  5. PCIe, USB, Little: PCIe = root complex → switch → endpoint, TLP, enumeration, BAR-এ MMIO জানালা; USB = host-নিয়ন্ত্রিত tiered star, চার transfer, enumeration; L = λW বলে কত কাজ একসাথে চালু রাখতে হবে।

পরের পর্বে: যন্ত্র কীভাবে CPU-কে নিজে ডাকে আর নিজেই মেমরিতে ডেটা সরায় — interrupt controller, MSI, DMA engine, descriptor ring, cache coherence আর IOMMU, hardware-এর দিক থেকে।

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

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