Interrupt এবং Device Driver — hardware কীভাবে kernel-কে বাধা দেয় ঠিক সময়ে
Interrupts and Device Drivers
গত লেসনের শেষে একটা প্রশ্ন ঝুলে ছিল -- io_uring-এ একটা read জমা দেওয়ার পর disk থেকে ডেটা এলে kernel সেটা কীভাবে জানে? উত্তর: interrupt, একটা hardware সংকেত যা CPU-কে যা করছিল তা মাঝপথে থামিয়ে একটা নির্দিষ্ট handler-এ পাঠায়। এই লেসনে সেই পুরো পথটা device থেকে IDT পর্যন্ত খুলে দেখব, top half/bottom half বিভাজনের যুক্তি বুঝব, আর নিজের হাতে একটা character device driver লিখে kernel-এ লোড করব।
আগে এটা বুঝি
গত লেসনের একদম শেষ লাইনে একটা প্রশ্ন ইচ্ছাকৃতভাবে খোলা রেখেছিলাম। io_uring-এ একটা read জমা দিলেন, disk controller-কে কাজ করতে হলো, আর কয়েক মিলিসেকেন্ড পরে ডেটা এসে পৌঁছাল। প্রশ্নটা ছিল — kernel কীভাবে জানল যে ডেটা এসেছে? কেউ কি অনন্তকাল ধরে disk controller-কে জিজ্ঞাসা করতে থাকল “হয়ে গেছে? হয়ে গেছে? হয়ে গেছে?”
সেই প্রশ্নের সরল উত্তরটাই — লুপে জিজ্ঞাসা করা — আসলে একটা বৈধ কৌশল, নাম polling। আর সমস্যাটা কল্পনা করা কঠিন না: ডেটা যদি ৫ মিলিসেকেন্ড পরে আসে, আর আপনি প্রতি ১ মাইক্রোসেকেন্ডে একবার জিজ্ঞাসা করেন, তাহলে ৫০০০ বার নিরর্থক প্রশ্ন করলেন। CPU-টা পুরোটা সময় এই একটা কাজেই আটকে রইল, অন্য কিছু করতেই পারল না। এটাই ঠিক সেই সমস্যা যা গত লেসনে SQPOLL নিয়ে আলোচনায় দেখেছিলাম — একটা পুরো core পুড়িয়ে ফেলার খরচ।
Hardware-এর জগতে এর বিপরীত সমাধানটার নাম interrupt: device নিজে থেকে CPU-কে বলে “আমার হয়ে গেছে” — CPU যা করছিল তা মাঝপথে থামিয়ে একটা নির্দিষ্ট কোড চালাতে বাধ্য হয়, তারপর যেখানে ছিল সেখানেই ফিরে যায়। কেউ জিজ্ঞাসা করে না, device নিজে জানায়। এই মডেলটাই আধুনিক OS-এর প্রায় সব asynchronous ব্যবহারের ভিত্তি — keyboard press থেকে network packet, timer tick থেকে disk completion পর্যন্ত।
কিন্তু গল্পটা এত সরল না। একটা আধুনিক NIC যখন সেকেন্ডে ১৫ লাখ প্যাকেট আনে, তখন প্রতিটা প্যাকেটের জন্য একটা interrupt পাঠালে CPU নিজেই ডুবে যাবে — polling-এর উল্টো সমস্যা, কিন্তু একই রকম মারাত্মক। তাই বাস্তব সিস্টেম কোনো একটা বিশুদ্ধ মডেলে থাকে না — NAPI নামের একটা কৌশল লোড অনুযায়ী দুটোর মধ্যে সুইচ করে, ঠিক যেমন io_uring লোড অনুযায়ী SQPOLL চালু-বন্ধ করার সিদ্ধান্ত আপনার হাতে ছেড়ে দিয়েছিল। এই লেসনে আমরা পুরো interrupt-এর যন্ত্রপাতি খুলে দেখব — device থেকে handler পর্যন্ত পথ, top half/bottom half-এর বিভাজন, আর শেষে নিজের হাতে একটা kernel module লিখে সত্যিকারের driver-এর স্বাদ পাব।
মূল ধারণা
Polling বনাম Interrupt — একটা মৌলিক trade-off
| Polling | Interrupt | |
|---|---|---|
| CPU কে জিজ্ঞাসা করে | হ্যাঁ, বারবার | না, device নিজে জানায় |
| Idle অবস্থায় CPU খরচ | সবসময় ব্যস্ত (busy-wait) | প্রায় শূন্য |
| Response latency | polling interval-এর উপর নির্ভর | প্রায় তাৎক্ষণিক (কয়েকশ ns) |
| উঁচু event rate-এ আচরণ | ধ্রুব খরচ (rate-নির্ভর নয়) | প্রতিটা event-এ একটা mode switch — rate বাড়লে খরচও বাড়ে |
| সবচেয়ে ভালো কোথায় | খুব উঁচু, ধারাবাহিক rate (কখনো idle হয় না) | নিম্ন-থেকে-মাঝারি, বিরতিময় rate |
শেষ দুই সারিই এই লেসনের কেন্দ্রীয় উপলব্ধি। একটা interrupt নিছক বিনামূল্যে আসে না — প্রতিটা interrupt মানে একটা পূর্ণ privilege-level transition (ring 3 → ring 0, বা যদি CPU আগে থেকেই kernel-এ ছিল তবু একটা context save/restore), IDT lookup, আর handler entry/exit। kernel-and-user-space লেসনে দেখা সেই ~২০০-৪০০ ns-এর খরচটা এখানেও প্রযোজ্য — সেখানে খরচটা ছিল software-চালিত (syscall instruction), এখানে হার্ডওয়্যার-চালিত (asynchronous সংকেত), কিন্তু transition-এর যন্ত্রটা একই।
Interrupt, Exception, আর Trap — তিনটা আলাদা জিনিস, একই যন্ত্র ব্যবহার করে
তিনটাই CPU-কে স্বাভাবিক instruction-ক্রম থেকে সরিয়ে একটা handler-এ পাঠায়, তিনটাই IDT/vector table ব্যবহার করে — কিন্তু উৎস আর সময় আলাদা:
| উৎস | Synchronous? | উদাহরণ | |
|---|---|---|---|
| Interrupt | CPU-র বাইরে (device, timer) | না — CPU যেকোনো instruction-এর মাঝখানে ধরা খেতে পারে | keyboard press, NIC-এ প্যাকেট আসা, timer tick |
| Exception (fault) | CPU নিজে, একটা instruction চালাতে গিয়ে ব্যর্থ | হ্যাঁ — নির্দিষ্ট instruction-এর কারণে | page fault, divide-by-zero, general protection fault |
| Trap | CPU নিজে, ইচ্ছাকৃতভাবে | হ্যাঁ — একটা নির্দিষ্ট instruction-ই এটা চাইছিল | syscall/int 0x80, int3 (breakpoint) |
Fault আর trap-এর মধ্যে সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ পার্থক্য — fault-এর পর CPU সেই instruction-টাতেই ফেরত যায় (page fault হ্যান্ডল হওয়ার পর সেই মেমরি-অ্যাক্সেস instruction আবার চলে, এবার সফল হয়), trap-এর পর পরের instruction-এ যায় (syscall শেষে যেখান থেকে শুরু হয়েছিল তার ঠিক পরের লাইনে)। x86-64-তে এই তিনটাই একই IDT ব্যবহার করে, কিন্তু vector সংখ্যা দিয়ে আলাদা করা হয় — 0-31 architecture-reserved exception/trap (0 = divide error, 14 = page fault, 3 = breakpoint), 32-255 device interrupt আর software-defined vector।
Device থেকে CPU পর্যন্ত — interrupt controller
একটা device সরাসরি CPU-কে “থামো” বলতে পারে না — মাঝখানে একটা interrupt controller থাকে যেটার কাজ একাধিক device-এর সংকেত সংগ্রহ করা, priority ঠিক করা, আর সঠিক CPU-তে পাঠানো।
x86-64: APIC (Advanced Programmable Interrupt Controller)। দুই ভাগে বিভক্ত:
| উপাদান | কোথায় | কাজ |
|---|---|---|
| I/O APIC | মাদারবোর্ডে, সব CPU-র জন্য একটা (সাধারণত) | Legacy IRQ line বা device থেকে সংকেত পায়, routing table অনুযায়ী কোন CPU-তে পাঠাবে ঠিক করে |
| Local APIC (LAPIC) | প্রতিটা CPU core-এর ভেতরে, একটা করে | I/O APIC থেকে (বা সরাসরি MSI-তে) সংকেত পায়, CPU-কে interrupt signal দেয়, timer/IPI-ও পরিচালনা করে |
পুরনো PC-তে ছিল শুধু PIC (8259A) — একটাই, cascade করা দুইটা চিপ, ১৫টা IRQ line, কোনো multi-core সমর্থন ছিল না। APIC এসেছে multi-core যুগে “কোন CPU-তে এই interrupt যাবে” প্রশ্নের উত্তর দিতে — এটাই IRQ affinity-র হার্ডওয়্যার ভিত্তি, নিচে বিস্তারিত।
ARM64: GIC (Generic Interrupt Controller)। ধারণাগতভাবে সমান্তরাল কিন্তু নামকরণ আলাদা — Distributor (I/O APIC-এর সমতুল্য, সব interrupt সংগ্রহ করে prioritize করে), Redistributor/CPU interface (LAPIC-এর সমতুল্য, প্রতি core-এ)। GICv3-এ Redistributor প্রতিটা core-এর কাছাকাছি রাখা হয়েছে latency কমাতে।
IDT — vector থেকে handler-এ যাওয়ার মানচিত্র
CPU-র কাছে interrupt সংকেত পৌঁছালে সে জানে শুধু একটা সংখ্যা — vector (0-255)। কোন কোড চালাতে হবে তা জানতে CPU IDT (Interrupt Descriptor Table) দেখে — একটা ২৫৬-এন্ট্রির টেবিল যেখানে প্রতিটা এন্ট্রি একটা gate descriptor: handler-এর ঠিকানা (code segment selector + offset), আর কিছু flag (gate type, DPL)।
vector (0-255) ──▶ IDT[vector] ──▶ { segment selector, offset, type, DPL }
│
▼
CPU: RIP = offset, CS = selector
(mode switch যদি DPL অনুমতি দেয়)দুই ধরনের gate গুরুত্বপূর্ণ — interrupt gate (ঢোকার সময় স্বয়ংক্রিয়ভাবে IF flag ক্লিয়ার করে, অর্থাৎ handler চলাকালীন আরেকটা maskable interrupt আসতে পারবে না, যদি না handler নিজে থেকে আবার চালু করে) আর trap gate (IF অপরিবর্তিত রাখে, syscall entry-তে ব্যবহৃত)। এই পার্থক্যটাই বলে দেয় কেন device driver-এর top half-এ interrupt সাধারণত disabled থাকে — gate-টাই সেটা নিশ্চিত করে, প্রোগ্রামারকে আলাদা করে মনে রাখতে হয় না।
DEVICE (NIC, disk, keyboard...)
│ ডেটা প্রস্তুত -- সংকেত পাঠাও (wire IRQ, বা MSI হলে সরাসরি মেমরি-write)
▼
I/O APIC / GIC Distributor
│ routing table দেখে ঠিক করে কোন CPU
▼
Local APIC / GIC CPU interface (নির্দিষ্ট CPU core-এ)
│ সেই core-কে vector number সহ সংকেত দেয়
▼
CPU: বর্তমান instruction শেষ হওয়ার পরই (atomically) থামে
│ privilege level যাই থাকুক (ring 3 বা ring 0), IDT lookup
▼
IDT[vector] ──▶ gate descriptor ──▶ handler-এর ঠিকানা
│ ring 3-এ থাকলে: mode switch ring 3 → ring 0 (KPTI থাকলে CR3 সুইচ)
│ IF flag ক্লিয়ার (interrupt gate হলে)
▼
KERNEL: interrupt handler (top half) চলে -- দ্রুত, ঘুমাতে পারে না
│ device-কে "পেয়েছি" জানাও (ack), APIC/GIC-কে EOI পাঠাও
▼
CPU: iret/eret -- আগে যা করছিল সেখানেই ফিরে যায়, IF পুনরুদ্ধারTop half / Bottom half — কেন handler-কে দুই ভাগে ভাঙা হয়
Top half-এর উপর একটা কঠোর নিয়ম আছে: দ্রুত হও, ঘুমাবে না, lock নেবে না যা sleep করতে পারে, বেশি কাজ করবে না। কারণটা কাঠামোগত — interrupt gate-এ ঢোকার সাথে সাথেই সেই CPU-তে (বা x86-এ পুরনো ডিজাইনে পুরো সিস্টেমে) maskable interrupt বন্ধ হয়ে যায়। Top half যত বেশি সময় নেয়, তত বেশি সময় অন্য device-এর interrupt (timer সহ!) সেই CPU-তে আটকে থাকে। একটা ধীর top half মানে scheduler নিজেও দেরিতে চলে, latency পুরো সিস্টেমে ছড়িয়ে পড়ে।
কিন্তু বাস্তব কাজ — একটা network প্যাকেট প্রোটোকল স্ট্যাকে ঠেলে দেওয়া, একটা disk completion-এর জন্য অপেক্ষারত process জাগানো — প্রায়ই দ্রুত নয়, আর কখনো কখনো ঘুমানোর দরকার হয় (মেমরি বরাদ্দ, mutex)। সমাধান: handler-কে দুই ভাগে ভাঙা।
| Top half (hard IRQ) | Bottom half | |
|---|---|---|
| কখন চলে | সাথে সাথে, interrupt context-এ | পরে, সুবিধাজনক সময়ে |
| Interrupt অবস্থা | disabled (আংশিক বা পূর্ণ) | সাধারণত enabled |
| ঘুমাতে পারে? | না, কখনোই না | নির্ভর করে কোন মেকানিজম (নিচে) |
| কাজ | ack করা, minimal state সংরক্ষণ, bottom half তফসিল করা | প্রকৃত ভারী কাজ |
চারটা bottom-half মেকানিজম — কোনটা কখন
| মেকানিজম | Context | ঘুমাতে পারে? | Serialization | সাধারণ ব্যবহার |
|---|---|---|---|---|
| Softirq | Interrupt context (কিন্তু interrupt enabled) | না | একই softirq একই সময়ে একাধিক CPU-তে চলতে পারে — ডেভেলপারকে নিজে lock করতে হয় | Network Rx/Tx (NAPI ঠিক এখানে বসে), timer, block I/O completion — সর্বোচ্চ performance দরকার এমন পথ |
| Tasklet | Softirq-এর উপর বানানো | না | একই tasklet কখনো একসাথে দুই CPU-তে চলে না (কিন্তু ভিন্ন tasklet চলতে পারে) | পুরনো driver code — আধুনিক kernel-এ deprecated, নতুন কোডে ব্যবহার নিষেধ |
| Workqueue | Process context, kernel thread-এ | হ্যাঁ | নিজের thread, স্বাধীনভাবে schedule হয় | যেকোনো কাজ যেখানে sleep/block/mutex দরকার — filesystem I/O, USB device reset |
| Threaded IRQ | Process context, dedicated kernel thread (request_threaded_irq) | হ্যাঁ | primary handler ছোট (hard IRQ-তেই), secondary handler নিজের thread-এ, priority scheduler দিয়ে নিয়ন্ত্রণযোগ্য | আধুনিক driver-এর ডিফল্ট প্যাটার্ন, বিশেষত PREEMPT_RT kernel-এ |
IRQ sharing এবং IRQ affinity
Sharing। পুরনো PCI bus-এ IRQ line সীমিত ছিল (মাত্র ৪টা: INTA-INTD), তাই একাধিক device একই line শেয়ার করত। একই vector-এ একাধিক handler রেজিস্টার করা যায় (IRQF_SHARED flag দিয়ে), আর interrupt এলে kernel প্রতিটা registered handler-কে ক্রমান্বয়ে ডাকে — প্রতিটা handler নিজেই পরীক্ষা করে “এটা কি আমার device?” (একটা status register পড়ে)। এটাই কারণ শেয়ার্ড IRQ-তে handler-এর প্রথম কাজ প্রায়ই একটা দ্রুত “আমার না হলে সাথে সাথে ফিরে যাও” চেক।
Affinity। Multi-core সিস্টেমে একটা IRQ যেকোনো CPU-তে (বা CPU-দের সেটে) পাঠানো যায় — এটা নিয়ন্ত্রিত হয় /proc/irq/<n>/smp_affinity (bitmask) দিয়ে। কেন এটা গুরুত্বপূর্ণ:
- একটা device-এর সব interrupt যদি সবসময় একই CPU-তে যায়, সেই CPU-র L2/L3 cache-এ device-সম্পর্কিত data structure গরম থাকে — প্রতিবার নতুন CPU-তে গেলে cache miss।
- উঁচু-throughput multi-queue NIC-এ প্রতিটা RX queue-র IRQ আলাদা CPU-তে পাঠিয়ে (RSS — Receive Side Scaling-এর সাথে মিলিয়ে) সমান্তরালভাবে প্যাকেট প্রসেস করা যায় — একটা core bottleneck হয় না।
- ব্যবহারকারী-স্পেসে
irqbalanceডেমন by default এই ভারসাম্যটা স্বয়ংক্রিয়ভাবে করে, কিন্তু latency-sensitive workload (HFT, উঁচু-throughput ডেটাবেস)-এ প্রায়ই এটা বন্ধ করে নিজে হাতে pin করা হয়, যাতে একটা নির্দিষ্ট predictable আচরণ পাওয়া যায়।
MSI/MSI-X — তার ছাড়া interrupt
Legacy IRQ line একটা ভৌত তার, সীমিত সংখ্যায় (x86-এ ঐতিহাসিকভাবে ১৬টা), আর shared হলে উপরের ambiguity-সমস্যা আছে। MSI (Message Signaled Interrupts) সমাধানটা elegant — device কোনো তার সংকেত দেয় না, বরং একটা নির্দিষ্ট মেমরি ঠিকানায় একটা ছোট write করে (PCI configuration-এ নির্ধারিত address+data), আর সেই write-টাকেই APIC ইন্টারপ্রেট করে “এটা একটা interrupt, এই vector-এ পাঠাও।”
| Legacy (pin-based) IRQ | MSI | MSI-X | |
|---|---|---|---|
| Vector সংখ্যা | সীমিত (~১৬) | ডিভাইস প্রতি ১-৩২ | ডিভাইস প্রতি ২০৪৮ পর্যন্ত |
| Sharing দরকার | প্রায়ই, ambiguity-সহ | কম | না — প্রতিটা vector-এর নিজস্ব address/data |
| প্রতি vector আলাদা CPU-তে পাঠানো যায়? | কঠিন | সীমিত | হ্যাঁ, প্রতিটা আলাদাভাবে configurable |
এটাই কেন আধুনিক multi-queue NIC আর NVMe drive-এ প্রতিটা queue-র নিজস্ব MSI-X vector থাকে — একটা ৩২-queue NVMe SSD ৩২টা আলাদা interrupt vector ব্যবহার করতে পারে, প্রতিটা একটা নির্দিষ্ট CPU core-এ পিন করা, যাতে কোনো cross-core lock ছাড়াই সমান্তরাল সম্পূর্ণভাবে সম্পন্ন হয়। io_uring লেসনে দেখা “registered files” ধারণার মতোই — একটা পুরনো সীমাবদ্ধতা (limited শেয়ার্ড resource) সরিয়ে per-work-item আলাদা resource দেওয়া।
DMA আর cache coherency
Interrupt শুধু “শেষ হয়েছে” বলে, প্রকৃত ডেটা কীভাবে RAM-এ পৌঁছায়? পুরনো পদ্ধতিতে (PIO — Programmed I/O) CPU নিজে প্রতিটা বাইট device থেকে register-এ পড়ে RAM-এ লিখত — ধীর, CPU পুরোটা সময় আটকে। DMA (Direct Memory Access) এটাকে ঘুরিয়ে দেয়: device নিজেই সরাসরি RAM-এ লেখে (একটা DMA controller বা device-এর নিজস্ব DMA engine দিয়ে), CPU শুধু শুরুতে ঠিকানা আর দৈর্ঘ্য বলে দেয়, তারপর সম্পূর্ণ স্বাধীন — কাজ শেষে একটা interrupt আসে।
কিন্তু এখানে একটা সূক্ষ্ম সমস্যা লুকিয়ে আছে — cache coherency। CPU-র নিজের L1/L2/L3 cache-এ একটা মেমরি অঞ্চলের একটা কপি থাকতে পারে। DMA device সরাসরি RAM-এ লিখলে CPU-র cache-এ পুরনো কপি রয়ে যায় — CPU সেই মেমরি পড়লে stale data দেখবে, নতুন DMA-লেখা ডেটা নয়। উল্টো দিকেও সমস্যা: CPU যদি cache-এ কিছু লিখে রাখে (এখনো RAM-এ flush হয়নি) আর device DMA দিয়ে সেই সময়ে RAM থেকে পড়ে, তবে device পুরনো ডেটা পাবে।
দুইটা সমাধান বাস্তবে আছে:
| পদ্ধতি | কীভাবে কাজ করে | কোথায় |
|---|---|---|
| Software cache management | Driver-কে explicit-ভাবে dma_sync_* বা cache flush/invalidate কল করতে হয় DMA-র আগে/পরে | কিছু embedded/ARM প্ল্যাটফর্ম, non-coherent DMA |
| Hardware cache coherency | Bus protocol নিজেই (x86-এ সবসময়, বেশিরভাগ আধুনিক ARM SoC-তে) DMA write-কে cache-এ সংকেত পাঠায়, CPU স্বয়ংক্রিয়ভাবে দেখে | x86-64-এর সব আধুনিক প্ল্যাটফর্ম, ARM-এর “coherent DMA” |
Linux-এর DMA API (dma_alloc_coherent, dma_map_single) এই দুইটার পার্থক্য driver থেকে লুকিয়ে রাখে — driver শুধু সঠিক API কল করে, platform-নির্দিষ্ট বাস্তবায়ন ঠিক করে দেয় flush আসলে দরকার কি না। Level 11-এর memory-model লেসনে এই একই cache-coherency প্রশ্ন আবার আসবে, তখন multi-core CPU-দের নিজেদের মধ্যে coherency protocol (MESI) হিসেবে।
IOMMU — device-এর জন্য একটা page table
CPU-র নিজস্ব MMU যেমন virtual address-কে physical address-এ অনুবাদ করে (paging-and-page-tables লেসন), IOMMU (I/O Memory Management Unit) একই কাজ করে device-এর জন্য। device একটা DMA শুরু করার সময় যে ঠিকানা ব্যবহার করে (IOVA — I/O Virtual Address), IOMMU সেটাকে প্রকৃত physical address-এ অনুবাদ করে — ঠিক যেভাবে CPU-র MMU virtual-কে physical-এ করে।
দুইটা কারণে এটা জরুরি:
- নিরাপত্তা। IOMMU ছাড়া একটা device (বা একটা compromised/buggy device driver) DMA দিয়ে যেকোনো physical address-এ লিখতে পারে — kernel memory সহ। IOMMU device-কে শুধু নির্দিষ্ট, mapped অঞ্চলে সীমাবদ্ধ রাখে, ঠিক যেমন MMU একটা user process-কে অন্য process-এর মেমরিতে যেতে বাধা দেয়। একটা compromised NIC firmware DMA আক্রমণ (যেমন Thunderbolt/FireWire-এর মাধ্যমে ঐতিহাসিক DMA আক্রমণ) IOMMU সক্রিয় থাকলে ঠেকানো যায় — Level 10-এর security module-এ এই আক্রমণ-শ্রেণি বিস্তারিত আসবে।
- Virtualization। VFIO/SR-IOV দিয়ে একটা physical device সরাসরি একটা guest VM-কে দেওয়া যায় (device passthrough) নিরাপদে, কারণ IOMMU guest-এর IOVA-কে host physical address-এ অনুবাদ করে আর guest-কে অন্য কোনো VM বা host memory-তে DMA করতে দেয় না। Level 12-এর cloud module-এ এটা GPU passthrough-এর প্রসঙ্গে ফিরে আসবে।
NAPI — আসল হাইব্রিড
এখন সব টুকরো একসাথে জোড়া যায়। নেটওয়ার্ক ড্রাইভারের জন্য বিশুদ্ধ interrupt-driven ডিজাইনে প্রতিটা প্যাকেটে একটা interrupt — সেকেন্ডে ১৫ লাখ প্যাকেটে এটা কার্যত সিস্টেমকে অচল করে দেয় (নিচের example সেকশনে হিসাব)। বিশুদ্ধ polling-এ CPU সবসময় ব্যস্ত, idle load-এ অপচয়। NAPI (New API, Linux 2.4-এ প্রবর্তিত) দুটোর সেরাটা নেয়:
নিম্ন লোড: প্যাকেট আসে ──▶ interrupt ──▶ softirq schedule ──▶ প্রসেস ──▶ ঘুম
(interrupt আবার চালু, পরের প্যাকেট আবার interrupt দেবে)
উঁচু লোড: প্যাকেট আসে ──▶ interrupt (একবার) ──▶ NIC-এর interrupt বন্ধ করো
──▶ softirq loop: "budget"-এর মধ্যে যত পার প্যাকেট পোল করো
──▶ queue খালি বা budget শেষ ──▶ NIC-এর interrupt আবার চালুমূল কৌশল: প্রথম প্যাকেটে একটা interrupt আসে, কিন্তু handler সাথে সাথে সেই device-এর জন্য interrupt বন্ধ করে দেয় আর একটা softirq তফসিল করে যা polling mode-এ ঢোকে। যতক্ষণ queue-তে প্যাকেট থাকে, ততক্ষণ polling চলতে থাকে (কোনো নতুন interrupt লাগে না) — একটা budget (ডিফল্ট ৩০০ প্যাকেট প্রতি পোল-চক্রে) দ্বারা সীমাবদ্ধ যাতে একটা busy NIC পুরো সিস্টেমকে starve না করে। Queue খালি হয়ে গেলে আবার interrupt চালু হয়ে যায় — ফিরে যায় idle-বান্ধব মোডে।
এটা ঠিক io_uring-এর SQPOLL-এর দর্শনগত আয়না — SQPOLL সবসময় polling (একটা core সবসময় ব্যয়), NAPI অভিযোজিত: idle-এ interrupt (সস্তা), busy-তে polling (দক্ষ)। এই adaptive switching-এর ধারণা এতটাই মৌলিক যে DPDK-র মতো kernel-bypass framework একই যুক্তি চরমে নিয়ে গেছে — realworld সেকশনে বিস্তারিত।
ভেতরে কী ঘটছে
একটা keyboard press-এর পূর্ণ যাত্রা
সবচেয়ে সহজবোধ্য concrete উদাহরণ দিয়ে পুরো path-টা আবার দেখি — একটা key চাপার মুহূর্ত থেকে terminal-এ character দেখা পর্যন্ত।
- কীবোর্ড কন্ট্রোলারkey-press ডিটেক্ট করে, USB/PS2 controller-এর মাধ্যমে ইন্টারাপ্ট সংকেত পাঠায় (USB-তে আসলে polling-based transfer, কিন্তু host controller নিজেই CPU-কে interrupt দেয়)
- I/O APICrouting table দেখে ঠিক করে কোন CPU core-এ এই vector যাবে -- সাধারণত boot CPU বা affinity-configured core
- Local APIC (সেই CPU-তে)CPU-কে vector number সহ সংকেত দেয় -- CPU যে instruction চালাচ্ছিল তা atomically শেষ করে থামে
- IDT lookup + mode switchযদি CPU তখন user-space code চালাচ্ছিল (ring 3), এখন ring 0-এ চলে যায় -- KPTI সক্রিয় থাকলে CR3 সুইচ, kernel-and-user-space লেসনের ঠিক সেই খরচ
- Top half (interrupt handler)কীবোর্ড controller থেকে scancode পড়ে (দ্রুত I/O port read), একটা ring buffer-এ রাখে, ack পাঠায়, EOI দেয় -- মোট কয়েক microsecond-এর কম
- Bottom half (tty layer, workqueue/softirq)scancode-কে keymap দিয়ে character-এ অনুবাদ, line discipline প্রসেসিং, tty buffer-এ রাখা -- এখানে বেশি কাজ, ঘুমাতে পারে
- প্রক্রিয়া জাগেযে process tty থেকে read()-এ ব্লক করে বসেছিল (bash), তার wait queue-তে wake_up() -- scheduler তাকে runnable করে
- iret/eret + সাধারণ execution পুনরায়CPU যেখানে ছিল সেখানে ফেরত, ততক্ষণে হয়তো কয়েক microsecond পার হয়েছে -- ব্যবহারকারীর কাছে অদৃশ্য
শেষ দুই ধাপ লক্ষ করুন — এখানেই lesson 23-এর pipe blocking-এর সাথে সরাসরি যোগসূত্র। যখন একটা process কোনো fd থেকে read() করে ব্লক হয়ে বসে (কোনো ডেটা নেই বলে), সে kernel-এর একটা wait queue-তে যোগ হয় আর scheduler তাকে runnable তালিকা থেকে সরিয়ে দেয় (CPU সময় নষ্ট করে না)। যখনই সেই fd-তে ডেটা আসে — কীবোর্ড হোক, নেটওয়ার্ক প্যাকেট হোক, বা pipe-এ অন্য প্রান্তের write() — bottom half-এর শেষ কাজটাই হলো সেই wait queue-তে wake_up() ডাকা। Interrupt-এর পুরো প্রক্রিয়াটার আসল লক্ষ্যই এটা: hardware event-কে software-এর কাছে একটা “জাগো” সংকেতে রূপান্তর করা।
IDT entry-র প্রকৃত গঠন (x86-64)
একটা IDT gate descriptor সরলীকৃতভাবে দেখতে এরকম:
struct idt_gate {
uint16_t offset_low; /* handler ঠিকানার নিচের ১৬ বিট */
uint16_t selector; /* কোন code segment -- সবসময় kernel-এর */
uint8_t ist; /* Interrupt Stack Table index -- 0 মানে বর্তমান stack */
uint8_t type_attr; /* gate type (interrupt/trap), DPL, present bit */
uint16_t offset_mid;
uint32_t offset_high;
uint32_t reserved;
} __attribute__((packed)); /* মোট ১৬ বাইট, তাই ২৫৬ এন্ট্রি = ৪ KB */ist ফিল্ডটা একটা সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ ব্যাপার সমাধান করে — কিছু exception (যেমন double fault, NMI) এত গুরুতর যে বর্তমান stack pointer-ই corrupt হয়ে থাকতে পারে। IST দিয়ে সেই নির্দিষ্ট vector-এর জন্য একটা আলাদা, পূর্বনির্ধারিত stack ব্যবহার করা যায়, বর্তমান (হয়তো ভাঙা) stack-এর উপর নির্ভর না করে। type_attr-এর একটা বিট ঠিক করে এটা interrupt gate (IF ক্লিয়ার করে) না trap gate (করে না)।
উদাহরণ
NAPI ছাড়া বনাম সহ — সংখ্যায় হিসাব
ধরা যাক একটা ১০ Gbps NIC, ৬৪-বাইট প্যাকেট (সবচেয়ে খারাপ ক্ষেত্র — সর্বোচ্চ প্যাকেট-রেট), যা তাত্ত্বিকভাবে প্রায় ১.৫ কোটি প্যাকেট/সেকেন্ড (14.88 Mpps) আনতে পারে। ধরি একটা interrupt-এর top-half entry+exit খরচ (mode switch, IDT lookup, EOI) ৩০০ ns।
বিশুদ্ধ interrupt-per-packet মডেলে:
অর্থাৎ শুধু interrupt handling-ই একটা পুরো CPU core-এর ৪.৪৬৪ গুণ সময় দাবি করছে — একটা core দিয়ে এটা সম্ভবই না, এমনকি একাধিক core-এও বেশিরভাগ ক্ষমতা শুধু interrupt entry/exit-এ পুড়ে যাবে, প্যাকেট প্রসেস করার সময় প্রায় কিছুই বাঁচবে না। এটাকে বলে interrupt storm/livelock — সিস্টেম “ব্যস্ত” কিন্তু প্রকৃত throughput প্রায় শূন্যের কাছাকাছি নেমে যায়, কারণ CPU শুধু interrupt সামলাতেই ব্যস্ত।
NAPI মডেলে (budget = ৩০০ প্যাকেট/পোল-চক্র):
একটা মাত্র প্রথম interrupt-এর পর polling শুরু হয়, প্রতি চক্রে ৩০০টা প্যাকেট batch-এ প্রসেস হয় (ঠিক io_uring-এর batching-এর মতোই যুক্তি — fixed overhead বহু কাজের উপর ভাগ হয়ে যায়):
প্রতি চক্রে overhead ধরি ২০০ ns (softirq schedule + loop entry, interrupt নয়):
| মডেল | overhead/সেকেন্ড | কতগুলো core সমতুল্য |
|---|---|---|
| Interrupt-per-packet | ৪.৪৬৪ সেকেন্ড কাজ | ~৪.৫টা core শুধু overhead-এ |
| NAPI (budget ৩০০) | ৯.৯২ ms কাজ | ~১%-এরও কম একটা core |
পার্থক্যটা ~৪৫০×। এই একটা হিসাবই ব্যাখ্যা করে কেন প্রতিটা আধুনিক Linux network driver (ixgbe, mlx5, virtio_net) NAPI ব্যবহার করে, আর কেন উঁচু-throughput নেটওয়ার্কিং-এ interrupt কোনোভাবেই “বিশুদ্ধ” রাখা যায় না।
নিজে চালিয়ে দেখুন
/proc/interrupts -- লোডের নিচে counter বাড়তে দেখা
প্রথমে বিশ্রামে থাকা অবস্থায় একবার দেখুন:
cat /proc/interrupts | head -20 CPU0 CPU1 CPU2 CPU3
0: 24 0 0 0 IO-APIC 2-edge timer
1: 9 0 0 0 IO-APIC 1-edge i8042
8: 1 0 0 0 IO-APIC 8-edge rtc0
9: 0 0 0 0 IO-APIC 9-fasteoi acpi
24: 18422 9103 21044 15887 PCI-MSI 32768-edge eth0-TxRx-0
25: 12203 20981 8877 19442 PCI-MSI 32769-edge eth0-TxRx-1
30: 102934 0 0 0 PCI-MSI 45056-edge nvme0q0
LOC: 5921034 5918822 5920103 5917756 Local timer interrupts
RES: 82013 81440 80112 79988 Rescheduling interruptsলক্ষ করুন: eth0-TxRx-0 আর eth0-TxRx-1 দুইটা আলাদা MSI-X vector, আর তাদের counter-ই বিভিন্ন CPU-তে ছড়ানো — এটাই RSS + IRQ affinity-র প্রমাণ, উপরে যা আলোচনা হলো। LOC (local timer) সংখ্যা সবচেয়ে বেশি — প্রতিটা CPU নিজের scheduler tick-এর জন্য নিয়মিত interrupt পায়।
এখন load তৈরি করুন আর counter-এর পরিবর্তন live দেখুন:
# টার্মিনাল ১ -- network load তৈরি করুন
ping -f -c 200000 8.8.8.8 2>/dev/null & # flood ping, রুট প্রয়োজন হতে পারে
# অথবা: iperf3 -c <server> -t 30
# টার্মিনাল ২ -- প্রতি সেকেন্ডে counter-এর পার্থক্য দেখুন
watch -n1 'grep eth0 /proc/interrupts'Every 1.0s: grep eth0 /proc/interrupts
24: 18507 9180 21044 15887 PCI-MSI eth0-TxRx-0
25: 12203 21310 8877 19442 PCI-MSI eth0-TxRx-1সংখ্যাগুলো দ্রুত বাড়ছে দেখতে পাবেন, কিন্তু NAPI চালু থাকলে বৃদ্ধির হারটা প্যাকেট-রেটের সমানুপাতিক নয় — সেটাই আসল পরীক্ষা:
# softirq counter-ও দেখুন -- NAPI polling এখানে গণনা হয়
watch -n1 'grep -E "NET_RX|NET_TX" /proc/softirqs' CPU0 CPU1 CPU2 CPU3
NET_RX: 892341 451022 398765 512034
NET_TX: 34521 12099 9887 15234NET_RX সংখ্যা /proc/interrupts-এর eth0 counter-এর চেয়ে অনেক বেশি বাড়ছে flood-এর সময়। এটাই প্রমাণ যে polling-mode-এ প্রতিটা প্যাকেটে নতুন interrupt লাগছে না — একটা interrupt একটা softirq schedule করছে, আর সেই softirq বহু প্যাকেট প্রসেস করে যাচ্ছে budget পর্যন্ত (প্রতিটা napi_poll call NET_RX বাড়ায়)।
শেষে ksoftirqd থ্রেড দেখুন, যা তখন সক্রিয় হয় যখন softirq load এত বেশি যে সাথে সাথে শেষ করা যায় না:
top -H -b -n1 | grep ksoftirqd 45 root 20 0 0 0 0 R 38.2 0.0 0:12.44 ksoftirqd/2ksoftirqd/2 চলছে মানে CPU2-তে softirq load এত বেশি যে kernel সেটাকে একটা normal-priority kernel thread-এ ঠেলে দিয়েছে, যাতে softirq একটা busy loop-এ পুরো CPU দখল না করে রাখে (fairness রক্ষা)।
প্রতিটা interrupt একটা গণনাযোগ্য, per-CPU ঘটনা -- আর কোন device কোন CPU-তে ইন্টারাপ্ট পাঠাচ্ছে তা সরাসরি পর্যবেক্ষণযোগ্য, অনুমান নয়।
IRQ affinity বদলানো -- /proc/irq/<n>/smp_affinity
প্রথমে NIC-এর IRQ নম্বরগুলো খুঁজে বের করুন:
grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'24
25বর্তমান affinity mask দেখুন (hexadecimal bitmask, প্রতিটা বিট একটা CPU):
cat /proc/irq/24/smp_affinity
cat /proc/irq/24/smp_affinity_list # মানুষ-পড়ার-যোগ্য সংস্করণf
0-3f মানে বাইনারিতে 1111 — CPU0, 1, 2, 3 সবগুলোই এই IRQ পেতে পারে (kernel/irqbalance যেকোনোটা বেছে নেবে)। এখন irqbalance বন্ধ করুন, নাহলে সে আপনার পরিবর্তন কয়েক সেকেন্ডের মধ্যে উল্টে দেবে:
sudo systemctl stop irqbalanceএখন IRQ 24-কে শুধুমাত্র CPU2-তে পিন করুন (bit 2 = mask 4):
echo 4 | sudo tee /proc/irq/24/smp_affinity
cat /proc/irq/24/smp_affinity_list2লোড আবার তৈরি করে দেখুন সব interrupt এখন শুধু CPU2-তে যাচ্ছে:
watch -n1 'grep "24:" /proc/interrupts' 24: 45102 0 892341 0 PCI-MSI eth0-TxRx-0CPU1, CPU3-এর কলাম আর বাড়ছে না — সব interrupt CPU2-তেই জমা হচ্ছে। এই pin-করা অবস্থার সুবিধা মাপতে perf stat দিয়ে cache miss তুলনা করুন:
perf stat -e cache-misses,cache-references -p $(pgrep -f "network-app") -- sleep 10 Performance counter stats for process id '1234':
1,203,441 cache-misses # 12.4% of all cache refs
9,721,003 cache-references
10.001834143 seconds time elapsedAffinity ছড়ানো থাকলে cache-miss শতাংশ সাধারণত বেশি দেখা যায় (একই ডেটা বারবার ভিন্ন core-এর cache-এ ferry হয়), pin করা থাকলে কম — ঠিক পরিমাণ workload-নির্ভর, কিন্তু দিকটা predictable। কাজ শেষে irqbalance ফিরিয়ে দিন:
sudo systemctl start irqbalanceএকটা IRQ কোন CPU-তে যাবে সেটা runtime-এ নিয়ন্ত্রণযোগ্য, আর এই নিয়ন্ত্রণ irqbalance ডেমনের সাথে দ্বন্দ্বে যেতে পারে -- তাই আগে সেটা বন্ধ করতে হয়।
নিজে বানান
একটা ন্যূনতম character device driver -- loadable kernel module
- একটা VM প্রস্তুত করুন -- কখনো host machine-এ চেষ্টা করবেন না
- linux-headers ইনস্টল করুন, আপনার kernel সংস্করণের সাথে মিলিয়ে
- hello_chardev.c লিখুন -- open/read/write/release সহ file_operations
- Makefile লিখুন kbuild সিস্টেম ব্যবহার করে, make চালান
- insmod করুন, dmesg দেখুন, /dev node দিয়ে read/write টেস্ট করুন, rmmod করুন
ধাপ ১ — সঠিক headers ইনস্তল
Module compile করতে আপনার চলমান kernel-এর ঠিক সেই সংস্করণের header ফাইল লাগবে (build system, Module.symvers, কনফিগারেশন) — অন্য সংস্করণে compile হলেও load করার সময় “version magic” mismatch-এ ব্যর্থ হবে।
uname -r
# Debian/Ubuntu:
sudo apt update && sudo apt install -y build-essential linux-headers-$(uname -r)
# Fedora/RHEL:
sudo dnf install -y kernel-devel-$(uname -r) gcc make
# Arch:
sudo pacman -S base-devel linux-headersধাপ ২ — ড্রাইভার কোড
/* hello_chardev.c -- একটা ন্যূনতম character device
* insmod করলে /dev/hello_chardev তৈরি হয় (udev-এর মাধ্যমে),
* লেখা যায়, যা লেখা হয়েছে তাই পড়া যায় (echo চেম্বার) */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define DEVICE_NAME "hello_chardev"
#define BUF_SIZE 256
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static char kbuf[BUF_SIZE];
static size_t kbuf_len;
static int dev_open(struct inode *inode, struct file *file)
{
pr_info("hello_chardev: open() -- pid %d\n", current->pid);
return 0;
}
static int dev_release(struct inode *inode, struct file *file)
{
pr_info("hello_chardev: release()\n");
return 0;
}
static ssize_t dev_read(struct file *file, char __user *ubuf,
size_t len, loff_t *offset)
{
size_t n = min(len, kbuf_len - (size_t)*offset);
if (n <= 0) return 0; /* EOF */
if (copy_to_user(ubuf, kbuf + *offset, n))
return -EFAULT; /* userspace ঠিকানা খারাপ */
*offset += n;
pr_info("hello_chardev: read() -- %zu বাইট দেওয়া হলো\n", n);
return n;
}
static ssize_t dev_write(struct file *file, const char __user *ubuf,
size_t len, loff_t *offset)
{
size_t n = min(len, (size_t)(BUF_SIZE - 1));
if (copy_from_user(kbuf, ubuf, n))
return -EFAULT;
kbuf[n] = '\0';
kbuf_len = n;
pr_info("hello_chardev: write() -- %zu বাইট নেওয়া হলো: \"%s\"\n", n, kbuf);
return n;
}
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = dev_open,
.release = dev_release,
.read = dev_read,
.write = dev_write,
};
static int __init hello_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) { pr_err("hello_chardev: major নম্বর পাওয়া গেল না\n"); return ret; }
cdev_init(&my_cdev, &fops);
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
return ret;
}
my_class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(my_class)) {
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_class);
}
device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);
pr_info("hello_chardev: লোড হলো -- major=%d minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
return 0;
}
static void __exit hello_exit(void)
{
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("hello_chardev: আনলোড হলো\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("os-curriculum");
MODULE_DESCRIPTION("ন্যূনতম character device -- interrupts-and-drivers লেসনের build");ধাপ ৩ — Makefile (kbuild)
obj-m += hello_chardev.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) cleanধাপ ৪ — build, load, টেস্ট, unload
make
ls *.kohello_chardev.kosudo insmod hello_chardev.ko
dmesg | tail -3[ 1234.567] hello_chardev: লোড হলো -- major=240 minor=0ls -l /dev/hello_chardev # device_create + udev স্বয়ংক্রিয়ভাবে node বানিয়েছে
echo "প্রথম বার্তা" > /dev/hello_chardev
cat /dev/hello_chardev
dmesg | tail -5crw------- 1 root root 240, 0 ... /dev/hello_chardev
প্রথম বার্তা
[ 1234.789] hello_chardev: open() -- pid 5821
[ 1234.789] hello_chardev: write() -- 16 বাইট নেওয়া হলো: "প্রথম বার্তা"
[ 1234.801] hello_chardev: open() -- pid 5823
[ 1234.801] hello_chardev: read() -- 16 বাইট দেওয়া হলোsudo rmmod hello_chardev
dmesg | tail -1
lsmod | grep hello # খালি -- সফলভাবে আনলোড হয়েছে[ 1235.100] hello_chardev: আনলোড হলোনিজে বাড়ান
১. একটা প্রকৃত interrupt handler যোগ করুন। যদি আপনার VM-এ একটা virtual serial port বা GPIO থাকে, request_irq()/request_threaded_irq() দিয়ে একটা হ্যান্ডলার রেজিস্টার করুন, আর pr_info দিয়ে interrupt আসার সময় লগ করুন। QEMU-তে -serial দিয়ে একটা virtual UART সহজেই পাওয়া যায়।
২. একটা wait queue যোগ করে blocking read বানান। এখন dev_read কখনো ব্লক করে না। wait_event_interruptible ব্যবহার করে read()-কে ব্লক করান যতক্ষণ না নতুন ডেটা write() হয় — এটাই lesson 23-এর pipe blocking-এর ভেতরের মেকানিজম, নিজের হাতে বানানো।
৩. ioctl যোগ করুন। একটা কাস্টম command (যেমন buffer clear করা) .unlocked_ioctl দিয়ে হ্যান্ডল করুন — driver-এর সাথে কথা বলার একটা তৃতীয় পথ, শুধু read/write ছাড়াও।
৪. /proc বা /sys entry যোগ করুন। proc_create() বা একটা sysfs attribute দিয়ে driver-এর ভেতরের counter (কতবার read/write হয়েছে) বাইরে থেকে দেখার ব্যবস্থা করুন — ঠিক যেভাবে /proc/interrupts কাজ করে।
৫. একাধিক minor number সমর্থন করুন। alloc_chrdev_region-এ count=4 দিয়ে চারটা independent device (/dev/hello_chardev0-3) বানান, প্রতিটার নিজস্ব buffer, file->private_data-তে কোন minor সেটা রেখে।
বাস্তব সিস্টেমে
যেখানে এই যন্ত্রপাতি সত্যিই চলছে
NVMe SSD-র MSI-X queue। একটা আধুনিক NVMe drive-এ প্রতিটা I/O queue-র (৬৪+ পর্যন্ত হতে পারে) নিজস্ব MSI-X vector, নিজস্ব CPU affinity — এই লেসনের MSI-X আলোচনার সরাসরি প্রয়োগ। এটাই কারণ NVMe SSD একই সাথে লাখ লাখ IOPS দিতে পারে একটা single-queue SATA SSD-র তুলনায় — প্রতিটা core সম্পূর্ণ স্বাধীনভাবে নিজের queue-র completion পায়, কোনো lock contention ছাড়াই।
DPDK — interrupt সম্পূর্ণভাবে বাদ দেওয়া। যেসব সিস্টেমে NAPI-ও যথেষ্ট নয় (টেলিকম প্যাকেট প্রসেসিং, ১০০ Gbps+ লাইন রেট), DPDK (Data Plane Development Kit) NIC-কে সম্পূর্ণভাবে polling mode-এ রাখে, kernel driver বাইপাস করে (UIO/VFIO দিয়ে সরাসরি userspace থেকে অ্যাক্সেস), একটা dedicated core চিরস্থায়ীভাবে busy-loop-এ চালিয়ে। এটা এই লেসনের polling-বনাম-interrupt trade-off টেবিলের চরম প্রান্ত — যখন rate এত বেশি এবং ধারাবাহিক যে interrupt-এর কোনো সুবিধাই নেই, শুধু খরচ।
PREEMPT_RT-এ threaded IRQ ডিফল্ট। Linux 6.12 (২০২৪)-এ PREEMPT_RT patch-set mainline-এ merge হয়েছে — এখন real-time কনফিগারেশনে প্রায় সব IRQ ডিফল্ট-ভাবে threaded, যাতে latency-critical process-কে bottom-half কাজের চেয়ে বেশি priority দেওয়া যায়। অটোমোটিভ (engine control), industrial control, আর অডিও প্রোডাকশন সিস্টেমে এটা directly ব্যবহৃত হয়।
IOMMU আর Spectre-শ্রেণির device আক্রমণ। Thunderbolt-এর মতো hot-pluggable, DMA-capable port একটা ঐতিহাসিক আক্রমণ-পথ ছিল (“DMA attack” — একটা compromised device প্লাগ করেই RAM থেকে সরাসরি secret পড়ে ফেলা, কোনো driver বা OS চেক এড়িয়ে)। আধুনিক OS (Windows, Linux, macOS) এখন ডিফল্টভাবে IOMMU সক্রিয় রাখে Thunderbolt port-এর জন্য (kernel IOMMU passthrough না, বরং কড়া isolation mode) — ঠিক এই লেসনের IOMMU isolation-আলোচনার বাস্তব প্রয়োগ।
irqbalance আর NUMA। মাল্টি-সকেট সার্ভারে (২+ physical CPU) IRQ affinity শুধু cache locality নয়, NUMA locality-ও প্রভাবিত করে — একটা NIC যদি socket 0-এ থাকে আর তার IRQ socket 1-এর একটা CPU-তে পিন করা থাকে, প্রতিটা প্যাকেট প্রসেসিং cross-socket memory access করবে, যা local access-এর চেয়ে ৫০-১০০% বেশি ধীর হতে পারে। উঁচু-throughput ডেটাবেস (Cassandra, ScyllaDB) deployment guide-এ প্রায়ই স্পষ্টভাবে বলা থাকে IRQ affinity-কে NIC-এর NUMA node-এর সাথে মেলাতে।
যে ভুলগুলো সবাই করে
“Interrupt সবসময় polling-এর চেয়ে ভালো, কারণ CPU সময় বাঁচায়।”
Example সেকশনের হিসাবটা এর সরাসরি খণ্ডন — ১৪.৮৮ Mpps-এ বিশুদ্ধ interrupt-per-packet মডেল একাই ~৪.৫টা CPU core-এর সমতুল্য overhead তৈরি করে, যেখানে NAPI (polling-এ সুইচ করে) সেটাকে ১%-এর নিচে নামিয়ে আনে। উঁচু, ধারাবাহিক event rate-এ polling বেশি দক্ষ, কারণ প্রতিটা event-এর জন্য আলাদা mode-switch খরচ হয় না। সঠিক নিয়ম: idle-এর কাছাকাছি বা বিরতিময় লোডে interrupt, চরম-উঁচু ধারাবাহিক লোডে polling বা হাইব্রিড — ঠিক যেমন io_uring লেসনে দেখা SQPOLL শুধু উঁচু, ধারাবাহিক I/O rate-এই যুক্তিসঙ্গত ছিল।
“একটা interrupt handler-ই device driver-এর 'আসল কাজ' করে -- মানে ডেটা প্রসেস করা।”
Top half/bottom half বিভাজনটাই এই ধারণার বিপরীত। Top half (hard IRQ handler) ইচ্ছাকৃতভাবে ন্যূনতম কাজ করে — device ack করা, সামান্য state সংরক্ষণ, বাকিটা তফসিল করা। প্রকৃত ভারী কাজ (প্রোটোকল প্রসেসিং, buffer copy, filesystem update) প্রায় সবসময় bottom half-এ (softirq/tasklet/workqueue/threaded IRQ) চলে, প্রায়ই interrupt চালু অবস্থায়, কখনো কখনো ঘুমাতেও পারে। একটা driver-এর top half যদি “প্রকৃত কাজ” করে, সেটা প্রায় নিশ্চিতভাবে একটা bug — অন্য সব interrupt (এমনকি timer!) সেই সময়টুকু আটকে থাকবে।
“একটা device-এর সব interrupt একই IRQ নম্বর দিয়ে চিহ্নিত -- IRQ নম্বর, vector নম্বর, আর device একই জিনিস।”
তিনটা আলাদা স্তর। IRQ নম্বর kernel-এর দৃষ্টিতে একটা logical identifier, যা একাধিক device শেয়ার করতে পারে (legacy pin-based, IRQF_SHARED)। Vector নম্বর CPU-র IDT-তে সরাসরি ব্যবহৃত সংখ্যা, যা kernel runtime-এ IRQ-র সাথে map করে — এবং MSI-X-এ একটা single device-এর একাধিক vector থাকতে পারে (উপরের NVMe উদাহরণে ৬৪টা পর্যন্ত), প্রতিটা আলাদা IRQ নম্বরে ম্যাপ হয়। অর্থাৎ একটা device একাধিক IRQ ব্যবহার করতে পারে, একটা IRQ একাধিক device শেয়ার করতে পারে — সম্পর্কটা many-to-many, one-to-one নয়।
“DMA মানে CPU সম্পূর্ণভাবে বাইরে -- আর ডেটা RAM-এ লেখা হয়ে গেলেই CPU সেটা সাথে সাথে সঠিকভাবে দেখতে পায়।”
DMA CPU-কে বাইট-বাই-বাইট কপি করা থেকে মুক্ত করে ঠিকই, কিন্তু cache coherency স্বয়ংক্রিয়ভাবে গ্যারান্টিড নয় সব প্ল্যাটফর্মে। Non-coherent DMA-তে CPU-র cache-এ থাকা পুরনো কপি DMA-লেখা নতুন ডেটাকে “ঢেকে” রাখতে পারে যতক্ষণ না driver স্পষ্টভাবে cache invalidate করে (dma_sync_single_for_cpu)। x86-64-এর বেশিরভাগ আধুনিক প্ল্যাটফর্মে হার্ডওয়্যার coherency থাকায় এই সমস্যা কার্যত অদৃশ্য, কিন্তু অনেক embedded/ARM প্ল্যাটফর্মে এটা বাস্তব এবং driver bug-এর একটা পরিচিত উৎস — “device থেকে পড়া ডেটা মাঝেমধ্যে পুরনো দেখাচ্ছে” ধরনের রিপোর্টের পেছনে প্রায়ই এই ভুলটাই থাকে।
বুঝেছেন কি না দেখুন
1একটা device driver লেখক ভুল করে তাদের top half-এ একটা mutex_lock() কল রেখে দিয়েছেন, যেটা মাঝেমধ্যে অন্য একটা thread ইতিমধ্যে ধরে আছে। ঠিক কী ঘটতে পারে, আর কেন এটা শুধু “একটু ধীর” হওয়ার চেয়ে অনেক বেশি বিপজ্জনক?
যুক্তি
mutex_lock() কল রেখে দিয়েছেন, যেটা মাঝেমধ্যে অন্য একটা thread ইতিমধ্যে ধরে আছে। ঠিক কী ঘটতে পারে, আর কেন এটা শুধু “একটু ধীর” হওয়ার চেয়ে অনেক বেশি বিপজ্জনক?এটা একটা সম্ভাব্য deadlock/system hang, নিছক ধীরগতি নয়।
কেন mutex sleep করতে পারে: যদি mutex ইতিমধ্যে অন্য thread ধরে আছে, mutex_lock() caller-কে sleep অবস্থায় পাঠায় (scheduler-কে ডেকে অন্য কিছু চালানোর সুযোগ দেয়), যতক্ষণ না মালিক unlock করে caller-কে জাগায়। এটাই mutex-এর স্বাভাবিক আচরণ (synchronization-primitives লেসনের বিষয়) — কিন্তু interrupt context-এ এটা নিষিদ্ধ।
কেন interrupt context-এ sleep করা যায় না: top half চলার সময় CPU-তে interrupt gate-এর কারণে maskable interrupt বন্ধ (বা অন্তত সেই vector) — আর কোনো “current process” নেই যাকে scheduler ঠিকভাবে suspend করে অন্য কিছু চালাতে পারবে বলে ধরে নেওয়া যায়। mutex_lock()-এর ভেতরের sleep-path schedule() ডাকতে চাইলে kernel সেটা ধরে ফেলে (CONFIG_DEBUG_ATOMIC_SLEEP চালু থাকলে একটা spectacular warning সহ BUG: scheduling while atomic), কিন্তু production kernel-এ ধরা নাও পড়তে পারে — ফলাফল undefined, বাস্তবে প্রায়ই একটা সম্পূর্ণ CPU lockup।
সবচেয়ে খারাপ বাস্তব দৃশ্য — deadlock: ধরুন mutex-টা একটা normal-priority thread ধরে আছে, আর সেই thread-টাকে চলতে দিতে হলে ঠিক সেই CPU-তে একটা timer interrupt (scheduler tick) দরকার — যেটা top half শেষ না হওয়া পর্যন্ত আটকে আছে। এখন সিস্টেম চিরস্থায়ীভাবে আটকে গেছে: top half মালিকের জাগার অপেক্ষায়, মালিক CPU সময় পাওয়ার অপেক্ষায়, CPU সময় পেতে হলে top half শেষ হতে হবে।
সঠিক সমাধান: এই কাজটা (যা lock দরকার) top half থেকে সরিয়ে একটা workqueue বা threaded IRQ-তে নিতে হবে — process context, যেখানে sleep করা নিরাপদ। এটাই ঠিক কেন এই লেসনে বারবার জোর দেওয়া হয়েছে: top half শুধু atomic, non-sleeping কাজ, বাকি সব bottom half-এ। Level 6-এর concurrency module-এ deadlock detection algorithm (wait-for graph) এই একই সমস্যার সাধারণ রূপ বিশ্লেষণ করবে।
2cat /proc/interrupts-এ আপনি দেখলেন একটা IRQ-এর counter শুধু CPU0-তেই বাড়ছে, বাকি ৩টা CPU-তে সবসময় শূন্য — যদিও smp_affinity mask f (সব CPU অনুমোদিত)। এটা কি একটা bug? কী কী কারণ থাকতে পারে, আর কীভাবে যাচাই করবেন?
প্রয়োগ
cat /proc/interrupts-এ আপনি দেখলেন একটা IRQ-এর counter শুধু CPU0-তেই বাড়ছে, বাকি ৩টা CPU-তে সবসময় শূন্য — যদিও smp_affinity mask f (সব CPU অনুমোদিত)। এটা কি একটা bug? কী কী কারণ থাকতে পারে, আর কীভাবে যাচাই করবেন?বাগ নাও হতে পারে — smp_affinity mask শুধু অনুমোদিত CPU-দের সেট বলে, kernel/hardware কে বাধ্য করে না প্রতিটাতে সমানভাবে পাঠাতে। কয়েকটা বৈধ কারণ থাকতে পারে:
| কারণ | ব্যাখ্যা | কীভাবে যাচাই |
|---|---|---|
| Legacy pin-based IRQ, single-target hardware | কিছু পুরনো I/O APIC routing শুধু একটা “lowest priority” CPU বেছে নেয় প্রতিটা interrupt-এ, যেটা প্রায়ই একই CPU হয়ে যায় যদি সেটাই সবচেয়ে কম busy থাকে | cat /proc/irq/<n>/smp_affinity_list দিয়ে allowed সেট দেখুন, তারপর কয়েক মিনিট পর্যবেক্ষণ করুন প্যাটার্ন বদলায় কি না |
| irqbalance আসলে pin করে রেখেছে | irqbalance heuristic অনুযায়ী একটামাত্র CPU বেছে স্থির রাখতে পারে, বিশেষত কম-লোড সিস্টেমে যেখানে সে “ছড়ানোর” দরকার দেখে না | systemctl status irqbalance; বন্ধ করে নিজে affinity সেট করে দেখুন পরিবর্তন হয় কি না |
| MSI vector CPU0-এ boot-টাইমে হার্ডকোড হয়েছে | কিছু device firmware/driver init-এর সময় CPU0-কেই default টার্গেট রাখে যতক্ষণ না explicitly পরিবর্তন করা হয় | `dmesg |
| প্রকৃত bug — affinity write silently ignored | কিছু driver/platform combination-এ affinity change আসলে কার্যকর হয় না (hardware routing-এর সীমাবদ্ধতা) | affinity পরিবর্তন করে সরাসরি smp_affinity_list আবার পড়ুন — read-back যদি পুরনো মান দেখায়, লেখা ব্যর্থ হয়েছে |
যাচাই করার পদ্ধতি নিয়মতান্ত্রিকভাবে: প্রথমে irqbalance বন্ধ করুন (dynamic interference বাদ দিতে), তারপর affinity explicitly CPU1-এ সেট করুন, sync করুন, লোড তৈরি করুন, আর দেখুন counter সত্যিই CPU1-এ যাচ্ছে কি না। যদি তাও CPU0-এ থেকে যায়, সেটা একটা hardware/firmware সীমাবদ্ধতা — অনেক embedded/legacy platform-এ সত্যিই ঘটে, আর এটা কার্যত driver/kernel-এর নিয়ন্ত্রণের বাইরে। এই ডায়াগনস্টিক পদ্ধতি — “প্রথমে variable কমাও, তারপর একটা করে টেস্ট করো” — Level 11-এর performance-debugging module-এর কেন্দ্রীয় শৃঙ্খলা।
3একটা IOMMU সক্রিয় সিস্টেমে একটা malicious/buggy NIC driver ভুল একটা physical address-এ DMA লিখতে চেষ্টা করে — যেটা IOMMU mapping-এর বাইরে। ঠিক কী ঘটবে, আর IOMMU ছাড়া একই পরিস্থিতিতে কী ঘটত?
যুক্তি
IOMMU-সহ: device শুধু তার নিজস্ব IOVA (I/O virtual address) স্পেসে কাজ করে, যেটা driver dma_map_single()/dma_alloc_coherent()-এর মাধ্যমে explicitly ম্যাপ করে দিয়েছে। device যদি একটা un-mapped বা অন্য কারো owned IOVA-তে লেখার চেষ্টা করে, IOMMU সেই transaction-টা প্রত্যাখ্যান করে — অনেকটা MMU-র page fault-এর মতো, কিন্তু device-এর জন্য। এই ঘটনা সাধারণত kernel log-এ একটা IOMMU fault হিসেবে রিপোর্ট হয় (dmesg-এ “DMAR: DRHD” বা “IOMMU: Reserved region…” ধরনের বার্তা), driver-টা হয়তো crash করে বা reset হয়, কিন্তু বাকি সিস্টেম নিরাপদ থাকে — kernel memory বা অন্য process-এর ডেটা ছোঁয়া যায় না।
IOMMU ছাড়া: device সরাসরি physical address ব্যবহার করে DMA করে — কোনো translation বা permission check নেই। একটা buggy বা malicious device (compromised firmware, বা physically প্লাগ করা একটা rogue Thunderbolt device) যেকোনো physical address-এ লিখতে পারে, যার মধ্যে kernel code/data, অন্য process-এর মেমরি, এমনকি page table নিজেই অন্তর্ভুক্ত। এটা কার্যত একটা সম্পূর্ণ, unrestricted memory-corruption primitive — privilege escalation বা secret exfiltration-এর জন্য যথেষ্ট।
তুলনাটা এত গুরুত্বপূর্ণ কেন: এটা ঠিক MMU-বিহীন একটা সিস্টেমের সাথে সমান্তরাল — যেমন MMU ছাড়া একটা user process অন্য process-এর মেমরি পড়তে-লিখতে পারত (paging-and-page-tables লেসন), IOMMU ছাড়া একটা device একই কাজ করতে পারে, শুধু ভিন্ন দিক থেকে (CPU-র বদলে device চালক)। এই কারণেই cloud provider-রা VFIO device passthrough-এর সময় IOMMU সক্রিয়তা কড়াভাবে বাধ্যতামূলক করে — এটা ছাড়া একটা VM-কে একটা physical device দেওয়া মানে সেই VM-কে কার্যত host memory-তে unrestricted access দেওয়া। Level 10-এর security module-এ IOMMU bypass-ভিত্তিক আক্রমণ-শ্রেণি (এবং তাদের বিরুদ্ধে IOMMU গ্রুপিং-এর ভূমিকা) বিস্তারিত আসবে।
4আপনি একটা উঁচু-throughput ডেটাবেস সার্ভার ডিজাইন করছেন, ৩২-কোর, ২-সকেট মেশিনে, একটা ১০০ Gbps NIC-সহ। ল্যাটেন্সি স্পাইক (p99.9) আপনার সবচেয়ে বড় সমস্যা — গড় throughput ইতিমধ্যেই যথেষ্ট। IRQ affinity, NAPI budget, আর CPU isolation নিয়ে আপনার কৌশল কী হবে?
ডিজাইন
লক্ষ্যটা throughput নয়, predictability — তাই সিদ্ধান্তগুলো একটা সাধারণ throughput-অপ্টিমাইজড সেটআপ থেকে আলাদা হবে।
১. NUMA-সচেতন IRQ affinity, কিন্তু সীমিত core-এ। NIC যে socket-এ physically বসানো, সেই socket-এর CPU-দেরই শুধু IRQ affinity দিন (cross-NUMA memory access-এর অনির্দেশ্য latency এড়াতে) — কিন্তু সব core নয়, একটা subset (যেমন ৪-৬টা core)। বাকি core-গুলো ডেটাবেস worker thread-এর জন্য সংরক্ষিত রাখুন, যাতে interrupt handling কখনো worker thread-কে preempt না করে।
২. isolcpus/nohz_full দিয়ে সেই worker core-গুলোকে scheduler থেকে আলাদা করুন। Kernel boot parameter দিয়ে নির্দিষ্ট core-কে সাধারণ scheduler-এর “load balancing”-এর বাইরে রাখা যায় — সেখানে শুধু pinned worker thread চলবে, কোনো random kernel thread বা timer tick এসে latency spike তৈরি করবে না (nohz_full timer tick-ও বন্ধ করে দেয় যখন একটাই runnable task থাকে)।
৩. IRQ core আর worker core আলাদা রাখুন, ওভারল্যাপ নয়। যদি IRQ handling আর ডেটাবেস worker একই core-এ থাকে, প্রতিটা interrupt worker-কে কয়েক microsecond preempt করবে — সেটাই ঠিক p99.9 spike-এর উৎস। আলাদা রাখলে interrupt handling worker-এর কাজে হস্তক্ষেপ করে না, শুধু IRQ-নির্ধারিত core-গুলোতেই “শব্দ” থাকে।
৪. NAPI budget কমান, বাড়াবেন না। ডিফল্ট ৩০০ থেকে কমিয়ে (যেমন ৬৪) প্রতিটা softirq চক্র ছোট রাখুন — একটা busy period-এ একটা দীর্ঘ polling loop অন্য softirq/scheduling-কে বেশিক্ষণ আটকে রাখতে পারে, যা p99.9-কে সরাসরি আঘাত করে। Throughput সামান্য কমতে পারে, কিন্তু latency-tail উন্নত হয় — এই ডিজাইনের ঘোষিত লক্ষ্যের সাথে মেলে।
৫. threaded IRQ + বাস্তব-সময় priority বিবেচনা করুন, বিশেষত যদি PREEMPT_RT kernel চালান — IRQ thread-কে একটা নির্দিষ্ট, পরিমিত priority দিয়ে, worker thread-এর priority-র সাথে সচেতনভাবে সাজিয়ে।
৬. মাপুন, অনুমান করবেন না। perf sched বা cyclictest দিয়ে actual scheduling-latency distribution মাপুন প্রতিটা পরিবর্তনের আগে-পরে — IRQ-related spike vs GC/compaction-related spike vs lock-contention spike আলাদা করা critical, কারণ প্রতিটার সমাধান আলাদা।
এই পুরো কৌশলটা এক কথায়: throughput-অপ্টিমাইজেশন “সব core-এ ছড়াও”, latency-অপ্টিমাইজেশন “predictable ভাগ করে রাখো, ওভারল্যাপ এড়াও”। Level 11-এর performance module-এ tail-latency vs average-latency-র এই বিভাজন এবং CPU isolation-এর সম্পূর্ণ কৌশল বিস্তারিত আসবে।
5একটা legacy PCI device দুইটা vector-এ MSI ব্যবহার করে, কিন্তু ড্রাইভার লেখক ভুলে দুইটা vector-এর জন্য একই handler function রেজিস্টার করেছেন IRQF_SHARED flag ছাড়া, আর handler ভেতরে কোনো “এটা কি আমার vector” চেক নেই। কী সমস্যা হতে পারে?
প্রয়োগ
IRQF_SHARED flag ছাড়া, আর handler ভেতরে কোনো “এটা কি আমার vector” চেক নেই। কী সমস্যা হতে পারে?এখানে দুটো ভিন্ন প্রশ্ন গুলিয়ে ফেলা হচ্ছে, আর সেটাই সমস্যার মূল।
প্রথম বিভ্রান্তি — IRQF_SHARED এখানে দরকারই না। MSI/MSI-X-এ প্রতিটা vector-এর নিজস্ব, unique address/data pair থাকে (concept সেকশনের টেবিল) — অর্থাৎ কোনো physical wire শেয়ার হচ্ছে না, প্রতিটা vector kernel-এর কাছে একটা আলাদা IRQ নম্বর। তাই IRQF_SHARED (যেটা legacy pin-sharing-এর জন্য) এখানে প্রাসঙ্গিকই না — প্রতিটা vector-এর জন্য একবার করে request_irq() ডাকাই স্বাভাবিক, প্রতিটা তার নিজের IRQ নম্বরে।
দ্বিতীয় সমস্যা, যেটা বাস্তবে ঘটবে — একই handler, কিন্তু কোন vector সেটা না জানা। যদি একই C function দুইটা request_irq() কলে পাস করা হয় (দুইটা আলাদা IRQ নম্বরের জন্য), handler-টাকে জানতে হবে কোন vector-এ ডাকা হয়েছে — না জানলে সে ভুল queue/ভুল ring buffer প্রসেস করতে পারে। সমাধান সহজ: request_irq(irq, handler, flags, name, dev_id)-এর শেষ argument dev_id-কে ব্যবহার করে প্রতিটা vector-এ আলাদা একটা context pointer পাস করা (যেমন কোন queue index এই vector-এর জন্য), আর handler ভেতরে সেই dev_id থেকে সঠিক queue বের করা।
/* ভুল -- দুইটা vector একই dev_id দিয়ে, handler জানে না কোনটা */
request_irq(vec0, my_handler, 0, "nic-q0", NULL);
request_irq(vec1, my_handler, 0, "nic-q1", NULL);
/* ঠিক -- প্রতিটা vector-এর জন্য নিজস্ব context */
request_irq(vec0, my_handler, 0, "nic-q0", &queue[0]);
request_irq(vec1, my_handler, 0, "nic-q1", &queue[1]);
/* handler-এ: struct queue *q = dev_id; -- এখন জানে কোন queue */সবচেয়ে খারাপ পরিণতি: যদি handler ভুল করে সবসময় queue[0] প্রসেস করে (dev_id উপেক্ষা করে), তাহলে vector 1-এ আসা interrupt-এও queue 0 প্রসেস হবে — queue 1-এর ডেটা কখনো drain হবে না, ring buffer পূর্ণ হয়ে যাবে, আর device শেষমেশ নতুন ডেটা DMA করার জায়গাই পাবে না। এই bug-টা প্রায়ই শুধু উঁচু লোডে ধরা পড়ে (queue 1 পূর্ণ হতে সময় লাগে), যা একে debug করা কঠিন বানায় — ঠিক io_uring লেসনে দেখা সেই একই প্যাটার্ন: ভুল ধরনের bug যেটা শুধু production-এর নির্দিষ্ট শর্তে দেখা যায়, dev মেশিনে না।
এরপর কী
পরের লেসন — Pipes এবং FIFOs
এই লেসনে আমরা দেখলাম hardware কীভাবে kernel-কে জানায় “কিছু হয়েছে” — আর হুড-সেকশনের keyboard trace-এ একটা জিনিস বিশেষভাবে লক্ষণীয় ছিল: bottom half-এর শেষ কাজ প্রায়ই একটা ব্লক-হওয়া process-কে wait queue থেকে জাগানো। এই “একটা প্রান্ত অপেক্ষা করছে, অন্য প্রান্ত ডেটা দিলে জাগে” প্যাটার্নটাই এখন আমরা সবচেয়ে সরল রূপে দেখব — দুইটা process-এর মধ্যে, কোনো hardware ছাড়াই।
পরের লেসনে pipe-এ যাচ্ছি — সেই | চিহ্ন যা আপনি প্রতিদিন শেলে টাইপ করেন। দেখব কেন এর buffer ঠিক ৬৪ KB, কেন লেখার প্রান্ত বন্ধ না করলে পড়ার প্রান্ত চিরকাল অপেক্ষা করে (ঠিক এই লেসনের wait-queue প্যাটার্নের একটা বাস্তব উদাহরণ), SIGPIPE/EPIPE কী বলে, PIPE_BUF-এর atomicity গ্যারান্টি, আর কেন একটা পূর্ণ pipe-এ writer-কে block করে রাখাটা bug নয় — এটাই backpressure, একটা design choice যা পরে TCP-র flow control-এ (Level 7) আবার দেখা যাবে।
আরও পড়ুন
- The genirq subsystem — Linux kernel documentation · top half/bottom half, threaded IRQ, আর IRQ affinity-র প্রামাণ্য বর্ণনা সরাসরি kernel maintainer-দের লেখা
- NAPI — Linux kernel networking documentation · interrupt-থেকে-polling সুইচের প্রকৃত mechanism, `napi_poll` আর budget-এর সংজ্ঞা এখান থেকেই
- Linux Device Drivers, 3rd Edition — Jonathan Corbet, Alessandro Rubini, Greg Kroah-Hartman · character device, file_operations আর interrupt handler লেখার ধ্রুপদী মুক্ত রেফারেন্স -- এই লেসনের build সেকশন এর অধ্যায় ৩ ও ১০-এর সরলীকরণ
- Arm Generic Interrupt Controller Architecture Specification (GICv3/v4) — Arm Ltd. · x86-এর APIC-এর সমতুল্য ARM-এর দিক -- Distributor, Redistributor, CPU interface-এর প্রামাণ্য সংজ্ঞা