এই পর্বে যা শিখবেন
- Polling-এর busy আর command-ready বিটের handshake ধাপে ধাপে, আর কখন polling interrupt-এর চেয়ে ভালো — হিসাব করে
- Interrupt controller (PIC, APIC, MSI-X), vector, priority, masking, top half আর bottom half, interrupt storm ও livelock
- DMA-র descriptor, scatter-gather, cycle stealing, IOMMU-র নিরাপত্তা-ভূমিকা আর cache coherence
- Block, character ও network device,
/devআর major/minor নম্বর; blocking, non-blocking ও asynchronous I/O — আসল প্রোগ্রাম চালিয়ে - Single, double আর circular buffering-এর throughput-এর হিসাব, device driver-এর গঠন, আর একটি
read()-এর পুরো যাত্রা
১ · I/O হার্ডওয়্যারের এক ঝলক — bus, port, controller আর PCIe
পর্ব ০৪-এ (OS-এর চোখে হার্ডওয়্যার) আপনি দেখেছেন: প্রতিটি ডিভাইসের সাথে একটি device controller থাকে, যার status, command আর data নামের কয়েকটি device register আছে। CPU সেগুলো ছোঁয় দুইভাবে — port-mapped I/O (x86-এর in/out) অথবা memory-mapped I/O. সেখানে interrupt আর DMA-র মূল ধারণাও এসেছে। এই পর্ব সেখান থেকে শুরু করে এক ধাপ নিচে নামবে।
একটি বড় জেলা হাসপাতালের কথা ভাবুন। ডাক্তার (CPU) একজন, কিন্তু বিভাগ অনেক — প্যাথলজি ল্যাব, এক্স-রে, ফার্মেসি। প্রতিটি বিভাগের নিজের ইনচার্জ আছেন (controller), দরজার পাশে একটি নোটিশ বোর্ড (register) — “ব্যস্ত”, “রিপোর্ট তৈরি”, “নতুন অনুরোধ”. হাসপাতালের করিডোর হলো bus. ডাক্তার নিজে রক্ত পরীক্ষা করেন না; তিনি শুধু অনুরোধ লেখেন আর রিপোর্ট পড়েন। প্রশ্ন হলো, রিপোর্ট তৈরি হয়েছে তা তিনি কীভাবে জানবেন — নিজে বারবার গিয়ে দেখবেন, নাকি ল্যাব থেকে কেউ এসে খবর দেবে? এই পর্বের পুরো গল্প এই প্রশ্ন ঘিরে।
PCIe — আধুনিক যন্ত্রের করিডোর
আজকের ল্যাপটপ আর সার্ভারে প্রায় সব দ্রুত ডিভাইস — NVMe SSD, GPU, নেটওয়ার্ক কার্ড, Wi-Fi — বসে PCIe (PCI Express)-এ। পুরোনো PCI ছিল সবার ভাগের একটি সমান্তরাল bus; PCIe হলো point-to-point সিরিয়াল সংযোগ, যার প্রতিটি “lane” আলাদা জোড়া তারে দুই দিকে একসাথে ডেটা পাঠায়। একটি ডিভাইস x1, x4 বা x16 lane পেতে পারে। প্রতি lane-এ মোটামুটি গতি: Gen3-এ প্রায় ১ GB/s, Gen4-এ প্রায় ২ GB/s, Gen5-এ প্রায় ৪ GB/s (প্রতি দিকে). তাই একটি Gen4 x4 NVMe SSD তাত্ত্বিকভাবে প্রায় ৮ GB/s পর্যন্ত যেতে পারে।
বুটের সময় firmware আর OS প্রতিটি PCIe ডিভাইসের configuration space পড়ে জানে কোন নির্মাতা, কোন ধরন, কতটা register-এর জায়গা লাগবে; তারপর physical ঠিকানার একটি অংশ দেয়, যা লেখা থাকে ডিভাইসের BAR (Base Address Register)-এ। এরপর থেকে ওই ঠিকানায় load/store মানেই ডিভাইসের register পড়া-লেখা। Linux-এ lspci -v দিয়ে এটি দেখা যায়:
$ lspci -v -s 01:00.0
01:00.0 Non-Volatile memory controller: Samsung Electronics Co Ltd NVMe SSD Controller
Flags: bus master, fast devsel, latency 0, IRQ 16, NUMA node 0
Memory at fb000000 (64-bit, non-prefetchable) [size=16K]
Capabilities: [b0] MSI-X: Enable+ Count=33 Masked-
Kernel driver in use: nvmeচারটি লাইন চারটি গল্প বলে: bus master মানে ডিভাইসটি নিজে DMA করতে পারে (অংশ ৪), Memory at fb000000 … size=16K হলো BAR — ১৬ KiB-এর register এলাকা, MSI-X … Count=33 মানে ৩৩টি আলাদা interrupt vector পাঠাতে পারে (অংশ ৩), আর nvme নামের driver এটি চালাচ্ছে (অংশ ৮)। Windows-এ একই তথ্য Device Manager-এর Properties → Resources ট্যাবে দেখায়।
| ডিভাইস | মোটামুটি গতি | ডেটার ধরন | OS-এর চ্যালেঞ্জ |
|---|---|---|---|
| কিবোর্ড, মাউস | সেকেন্ডে কয়েক থেকে কয়েকশো ঘটনা | একটি একটি অক্ষর বা নড়াচড়া | দেরি কম রাখা, CPU নষ্ট না করা |
| হার্ড ডিস্ক (HDD) | প্রায় ১০০–২৫০ MB/s, seek কয়েক ms | ৫১২ বাইট বা ৪ KiB-এর ব্লক | অনুরোধ সাজিয়ে seek কমানো (পর্ব ৪০) |
| NVMe SSD | কয়েক GB/s, এক পড়ায় দশ মাইক্রোসেকেন্ডের ঘরে | ব্লক, বহু queue-তে একসাথে | software-এর খরচ যেন ডিভাইসের চেয়ে বেশি না হয় |
| নেটওয়ার্ক কার্ড | ১–১০০ Gb/s বা বেশি | প্যাকেট, অনিয়মিত সময়ে | প্রতি প্যাকেটে interrupt দিলে CPU ডুবে যায় |
একই kernel-কে সেকেন্ডে পাঁচটি কী-চাপ আর কয়েক কোটি প্যাকেট — দুটোই সামলাতে হয়; এটিই I/O subsystem-এর মূল সমস্যা।
২ · Polling-এর handshake — busy আর command-ready বিট
সবচেয়ে সরল কথোপকথন দিয়ে শুরু করা যাক: CPU একটি controller-এ একটি বাইট লিখবে (যেমন একটি সিরিয়াল পোর্টে). Controller-এর status register-এ একটি busy বিট আছে — controller কাজ করার সময় সেটি ১. Command register-এ আছে একটি write বিট (আদেশটি লেখা না পড়া) আর একটি command-ready বিট — CPU যখন আদেশ সাজিয়ে প্রস্তুত, তখন সেটি ১ করে। দুই পক্ষ এই বিটগুলো দিয়ে পালাক্রমে কথা বলে — একে বলে handshake. নিচের stepper-এ CPU একটি বাইট 0x41 ('A') লিখছে:
| status.busy | |
| command.write | |
| command.ready | |
| data-out | |
| status.error | |
| CPU কী করছে | |
| Controller কী করছে |
- CPU busy বিট পড়ছে। CPU একটি লুপে বারবার status register পড়ে: busy ১, ১, ১… Controller আগের কাজ শেষ করে busy ০ করলে লুপ ভাঙে। এই অপেক্ষাটিই busy waiting — CPU কাজের কিছুই করছে না।
- আদেশ সাজানো। CPU command register-এর write বিট ১ করে (মানে “লিখতে চাই”), আর data-out register-এ বাইট
0x41রাখে। Controller এখনো কিছু জানে না। - “প্রস্তুত” সংকেত। CPU command-ready বিট ১ করে। এটিই আসল ঘণ্টা: “আদেশ সাজানো শেষ, এবার শুরু করুন।” ক্রম গুরুত্বপূর্ণ — data আগে, command-ready পরে; নইলে controller অর্ধেক-লেখা আদেশ পড়ে ফেলতে পারত।
- Controller খেয়াল করল। Controller command-ready দেখে busy ১ করে — “কাজ চলছে, বিরক্ত করবেন না।” CPU চাইলে আবার busy-র লুপে ফিরতে পারে।
- আসল I/O. Controller command register পড়ে বোঝে এটি write, data-out থেকে বাইটটি নিয়ে ডিভাইসে পাঠায়। ডিভাইস ধীর হলে এই ধাপ CPU-র হিসাবে খুব দীর্ঘ — হাজার হাজার নির্দেশের সময়।
- শেষের সংকেত। Controller command-ready ০ করে, error ০ রাখে (সফল), তারপর busy ০ করে। CPU-র পরের পড়ায় busy ০ — এবার পরের বাইট। প্রতিটি বাইটে এই চক্র পুরো চলে।
কোডে লুপটি আসলে তিনটি নির্দেশ: status পড়া, busy বিট আলাদা করা (AND), আর ১ হলে আবার শুরুতে লাফ। ডিভাইস দ্রুত হলে লুপ এক-দুবারেই শেষ — তখন polling দারুণ দক্ষ। ডিভাইস ধীর হলে লুপ লাখবার ঘোরে। মাঝামাঝি একটি কৌশল আছে: অল্প কিছুক্ষণ poll করে না পেলে ঘুমিয়ে interrupt-এর অপেক্ষা।
#define BUSY 0x01 /* status register bit */
#define CMD_WRITE 0x01 /* command register bits */
#define CMD_READY 0x80
void put_byte(volatile uint8_t *status, volatile uint8_t *cmd,
volatile uint8_t *data_out, uint8_t b)
{
while (*status & BUSY) /* step 1: busy-wait */
cpu_relax(); /* x86 "pause": spin politely */
*data_out = b; /* step 2: data first */
*cmd = CMD_WRITE;
*cmd = CMD_WRITE | CMD_READY; /* step 3: then ring the bell */
}volatile না দিলে compiler একবার পড়েই চিরকালের লুপ বানাতে পারত — পর্ব ০৪-এর MMIO সতর্কতা।
কখন polling interrupt-কে হারায়
প্রতিটি interrupt-এর একটি স্থির দাম আছে: state রাখা, kernel-এ ঢোকা, handler, ফেরা, আর cache নষ্ট হওয়া। ধরা যাক এই দাম ৪ µs (উদাহরণ মাত্র; যন্ত্রভেদে এক থেকে কয়েক µs). তাহলে:
- সেকেন্ডে ১০,০০০ ঘটনা → ১০,০০০ × ৪ µs = ৪০ ms, অর্থাৎ একটি core-এর ৪% — interrupt সস্তা, polling-এ একটি পুরো core নষ্ট হতো।
- সেকেন্ডে ২,৫০,০০০ ঘটনা → ২,৫০,০০০ × ৪ µs = ১ সেকেন্ড, অর্থাৎ ১০০% — শুধু interrupt সামলাতেই একটি core শেষ। এর ওপরে interrupt আর টিকতেই পারে না (অংশ ৩-এর livelock).
দ্বিতীয় কারণ দেরি (latency). একটি আধুনিক NVMe SSD একটি ৪ KiB পড়া মোটামুটি ১০ µs-এর ঘরে শেষ করতে পারে। Interrupt-এর পর ঘুমন্ত thread-কে জাগিয়ে context switch করাতে কয়েক µs লাগলে মোট দেরির বড় অংশই হয়ে যায় software. তাই আধুনিক সিস্টেম মিশ্র পথ নেয় (খুঁটিনাটি kernel সংস্করণ আর ডিভাইসভেদে আলাদা):
- NAPI (Linux নেটওয়ার্ক): প্রথম প্যাকেটে interrupt, তারপর interrupt বন্ধ রেখে polling; queue খালি হলে আবার interrupt চালু। অংশ ৩-এর stepper-এ পুরোটা দেখবেন।
- io_uring-এর polling:
IORING_SETUP_SQPOLL-এ একটি kernel thread অনুরোধের ring poll করে, তাই অ্যাপের system call-ই লাগে না;IORING_SETUP_IOPOLL-এ completion-এর জন্য interrupt-এর বদলে NVMe-র queue poll করা হয় (সাধারণতO_DIRECTফাইলে). - Poll-mode driver: DPDK (নেটওয়ার্ক) আর SPDK (স্টোরেজ) পুরো driver user space-এ চালায় আর কয়েকটি core শুধু poll-এর জন্য রেখে দেয় — সর্বোচ্চ গতি, কিন্তু সেই core সারাক্ষণ ১০০% আর বিদ্যুৎ-খরচ বেশি।
৩ · Interrupt-চালিত I/O — controller, MSI-X, top half আর bottom half
পর্ব ০৪-এর stepper-এ আপনি দেখেছেন, একটি interrupt এলে CPU state রেখে IDT থেকে ISR-এর ঠিকানা নেয় আর iret দিয়ে ফেরে। এবার প্রশ্ন: শত শত ডিভাইসের সংকেত CPU-র কাছে পৌঁছায় কীভাবে, কে আগে যাবে, আর কোনটি আটকে রাখা যায়? এই কাজ interrupt controller-এর।
IBM PC (১৯৮১) ব্যবহার করত Intel-এর 8259A PIC — ৮টি ইনপুট লাইন (IRQ 0–7), প্রতিটির নির্দিষ্ট priority. PC/AT (১৯৮৪)-এ দুটি 8259 শিকলের মতো জোড়া দেওয়া হলো, একটির লাইন অন্যটিতে ঢুকিয়ে — মোট ১৫টি ব্যবহারযোগ্য IRQ. আজও x86-এ timer-কে IRQ 0 আর PS/2 কিবোর্ডকে IRQ 1 বলা সেই যুগের উত্তরাধিকার। মাল্টিপ্রসেসর যুগে এলো APIC: প্রতিটি core-এ একটি local APIC, আর ডিভাইসের লাইন সংগ্রহ করে একটি I/O APIC, যা interrupt যেকোনো core-এ পাঠাতে পারে।
লাইন থেকে বার্তা — MSI আর MSI-X
পুরোনো পদ্ধতিতে interrupt মানে একটি তারে ভোল্টেজ — IRQ লাইন। লাইন কম, তাই কয়েকটি ডিভাইস একটি লাইন ভাগ করত, আর handler-কে প্রতিটি ডিভাইস জিজ্ঞেস করে দেখতে হতো “এটি কি আপনার?” PCI 2.2 আনে MSI (Message Signaled Interrupts): ডিভাইস কোনো তার নাড়ে না, বরং একটি বিশেষ ঠিকানায় একটি ছোট মেমরি-লেখা পাঠায়; সেই লেখার ডেটাই বলে দেয় কোন vector আর কোন core. MSI-তে একটি ডিভাইস সর্বোচ্চ ৩২টি vector পায়; PCI 3.0-এর MSI-X-এ ২০৪৮টি পর্যন্ত, প্রতিটি আলাদা core-এ আলাদা ঠিকানায়।
এর বাস্তব লাভ বিশাল। একটি NVMe SSD প্রতিটি CPU core-এর জন্য আলাদা অনুরোধ-queue রাখতে পারে, আর প্রতিটি queue-এর completion interrupt সেই core-এই আসে — core-গুলোর মধ্যে lock বা cache-এর টানাটানি নেই। নেটওয়ার্ক কার্ডও একইভাবে প্যাকেট কয়েকটি queue-তে ভাগ করে (receive-side scaling).
Vector, priority আর masking
x86-এ ২৫৬টি vector আছে: ০–৩১ CPU-র নিজের exception-এর জন্য সংরক্ষিত (যেমন ১৪ = page fault), ৩২–২৫৫ বাইরের interrupt আর OS-এর নিজের ব্যবহারের জন্য (যেমন core-দের মধ্যে বার্তা)। Local APIC vector-এর উপরের চার বিটকে priority-র শ্রেণি হিসেবে ধরে; একটি উঁচু শ্রেণির interrupt নিচু শ্রেণির handler-কে থামাতে পারে।
- Masking: kernel একটি নির্দিষ্ট লাইন বা ডিভাইসের interrupt বন্ধ রাখতে পারে (controller-এ mask), অথবা এই core-এ সব interrupt সাময়িক বন্ধ করতে পারে (x86-এ
cli/sti, Linux-এlocal_irq_save()). Critical section-এ handler-এর সাথে ডেটা ভাগ হলে এটি দরকার (পর্ব ২৩)। কিন্তু যত বেশি সময় বন্ধ, তত বেশি interrupt latency. - NMI (non-maskable interrupt): vector ২, যা সাধারণ masking-এ বন্ধ হয় না — হার্ডওয়্যারের গুরুতর ত্রুটি, watchdog বা profiler-এর জন্য।
- Windows-এর IRQL: Windows NT মাস্কিংকে একটি সংখ্যায় প্রকাশ করে — IRQL. Core যে স্তরে চলছে, তার সমান বা নিচের interrupt অপেক্ষা করে: সাধারণ thread PASSIVE_LEVEL-এ (০), DPC DISPATCH_LEVEL-এ (২), ISR আরও উঁচুতে।
Interrupt controller তিনটি কাজ করে: অনেক উৎসের সংকেত এক জায়গায় আনে, প্রতিটিকে একটি vector নম্বরে অনুবাদ করে, আর priority ও mask মেনে ঠিক করে কোনটি এখন কোন core-এ যাবে। MSI-X এই সিদ্ধান্তকে ডিভাইসের ভেতরের queue পর্যন্ত নামিয়ে এনেছে।
Top half আর bottom half — interrupt-কে দুই ভাগে
ISR চলার সময় অন্তত সেই লাইনের (প্রায়ই পুরো core-এর) নতুন interrupt অপেক্ষায় থাকে, আর ISR ঘুমাতে (sleep) পারে না — কোন process-এর হয়ে ঘুমাবে? তাই নিয়ম: interrupt-এর মুহূর্তে শুধু জরুরি কাজ, বাকিটা পরে নিরাপদ জায়গায়।
রেস্তোরাঁর রান্নাঘরে একটি চুলা, এক বাবুর্চি। ওয়েটার অর্ডারের কাগজ নিয়ে ঢুকলে বাবুর্চি হাতের কড়াই না নামিয়েই কাগজটি হুকে গেঁথে বলেন “পেয়েছি” — এটুকু top half, কয়েক সেকেন্ডের ব্যাপার। কাগজ পড়ে মশলা মাপা, রান্না সাজানো — সেটি পরে, হাতের কাজ শেষ হলে — bottom half. বাবুর্চি যদি প্রতিটি অর্ডার আসামাত্র পুরো রান্না শুরু করতেন, চুলার আগের কড়াই পুড়ে যেত।
- Top half (hard IRQ handler): ডিভাইসকে জানানো “পেয়েছি” (acknowledge), দরকার হলে ডিভাইসের interrupt mask করা, ছোট একটি তথ্য তুলে রাখা, আর bottom half-কে সূচিত করা। Linux-এ
request_irq()দিয়ে নিবন্ধিত ফাংশন; ফেরত দেয়IRQ_HANDLEDবা ভাগের লাইনে “আমার নয়” বোঝাতেIRQ_NONE. - Softirq: kernel-এ স্থির কয়েকটি ধরন (নেটওয়ার্ক পাঠানো/গ্রহণ, block, timer, RCU ইত্যাদি)। Hard IRQ শেষে, interrupt আবার চালু রেখে চলে; একই ধরন একসাথে কয়েকটি core-এ চলতে পারে, তাই কোড per-CPU ডেটা আর lock-এ সতর্ক। এখনো ঘুমানো চলে না। বেশি জমলে প্রতি core-এর ksoftirqd thread-এ যায়।
- Tasklet: softirq-এর ওপর বানানো সহজ রূপ, একসাথে একটি core-এই চলে। পুরোনো driver-এ বহুল ব্যবহৃত; সাম্প্রতিক kernel-এ এর বিকল্প হিসেবে BH workqueue এসেছে।
- Workqueue আর threaded IRQ: কাজ চলে একটি সাধারণ kernel thread-এ — process context, তাই ঘুমানো, mutex নেওয়া, ধীর I/O করা সব চলে।
request_threaded_irq()-এ top half শুধু thread জাগায়, পুরো handler thread-এ চলে। PREEMPT_RT kernel প্রায় সব interrupt এভাবে চালায়, যাতে real-time কাজকে আগে রাখা যায় (পর্ব ২০)। - Windows-এ DPC: ISR একটি Deferred Procedure Call queue করে, যা পরে DISPATCH_LEVEL-এ চলে; ঘুমানোর দরকার হলে work item, যা PASSIVE_LEVEL-এর thread-এ চলে। নাম আলাদা, নকশা একই।
একটি নেটওয়ার্ক কার্ডের driver-এ দুই ভাগ কেমন দেখায় (ডিভাইস-নির্দিষ্ট ফাংশনগুলো কাল্পনিক, kernel-এর API আসল):
static irqreturn_t mynic_irq(int irq, void *dev_id) /* top half */
{
struct mynic *nic = dev_id;
u32 st = readl(nic->regs + INT_STATUS); /* MMIO read */
if (!(st & RX_PENDING))
return IRQ_NONE; /* shared line: not ours */
writel(st, nic->regs + INT_ACK); /* acknowledge */
mynic_mask_rx_irq(nic); /* no more IRQs for now */
napi_schedule(&nic->napi); /* raise NET_RX softirq */
return IRQ_HANDLED;
}
static int mynic_poll(struct napi_struct *napi, int budget) /* bottom half */
{
struct mynic *nic = container_of(napi, struct mynic, napi);
int done = mynic_clean_rx_ring(nic, budget); /* at most budget packets */
if (done < budget && napi_complete_done(napi, done))
mynic_unmask_rx_irq(nic); /* ring empty: IRQs back on */
return done;
}
/* probe(): request_irq(irq, mynic_irq, IRQF_SHARED, "mynic", nic);
netif_napi_add(netdev, &nic->napi, mynic_poll); */নিচের stepper-এ একটি CPU core-এ একটি প্যাকেটের ঢেউ এই কোডের ভেতর দিয়ে যাচ্ছে:
| এই core-এ কী চলছে | |
| core-এর interrupt | |
| NIC-এর RX interrupt | |
| ring-এ জমা প্যাকেট | |
| ঘুমানো যায়? | |
| সময়ের মাপ |
- প্যাকেট এলো। NIC প্যাকেটটি DMA দিয়ে RAM-এর একটি ring buffer-এ লিখল, descriptor-এ “done” বিট দিল, তারপর MSI-X বার্তা পাঠাল। Core-এ তখন একটি user process P চলছে। Ring-এ ১টি প্যাকেট।
- হার্ডওয়্যারের কাজ। Core চলতি নির্দেশ শেষ করে P-এর state kernel stack-এ রাখল, এই core-এর interrupt বন্ধ করল, vector দেখে kernel-এর সাধারণ entry কোডে গেল, সেখান থেকে
mynic_irq(). এর মধ্যেই আরও ২টি প্যাকেট এসেছে — ring-এ ৩। - Top half — কয়েক মাইক্রোসেকেন্ড। Handler status পড়ে নিশ্চিত হলো interrupt এর নিজের, NIC-কে ack করল, NIC-এর RX interrupt mask করল, আর
napi_schedule()দিয়ে NET_RX softirq চিহ্নিত করল। কোনো প্যাকেট প্রক্রিয়া করেনি! Ring-এ এখন ১২টি, কিন্তু mask থাকায় নতুন interrupt আসেনি। - Bottom half — softirq. Hard IRQ থেকে বেরোনোর মুখে kernel জমে থাকা softirq চালায়, interrupt চালু রেখে।
mynic_poll()সর্বোচ্চ budget ৬৪টি প্যাকেট তুলে IP আর TCP স্তরে পাঠাল। ঢেউ চলছে বলে নতুন প্যাকেট এসেছে — ৯০টি বাকি। Budget শেষ, তাই poll ফেরে, NIC-এর interrupt বন্ধই থাকে। - চাপ বেশি — ksoftirqd. একবারে softirq-এর সময় (প্রায় ২ ms) বা পুনরাবৃত্তির সীমা পেরোলে kernel বাকি কাজ এই core-এর
ksoftirqd/3thread-কে দেয়। সাধারণ thread বলে scheduler P-কেও সময় দেয়। Thread বাকি ৯০টি শেষ করল। - শান্ত — interrupt আবার চালু। Ring খালি, budget-এর কম কাজ হয়েছে, তাই
napi_complete_done()আর NIC-এর interrupt unmask. TCP স্তর socket-এ ডেটা রেখেrecv()-এ ঘুমিয়ে থাকা receiver process-কে ready queue-তে তুলেছে। P আবার চলছে, যেন কিছুই হয়নি।
Interrupt storm আর receive livelock
ধরুন NAPI নেই, আর প্রতিটি প্যাকেটে একটি interrupt. চাপ বাড়লে CPU-র সব সময় যায় top half-এ — প্যাকেট ring থেকে তুলে queue-তে রাখতে। কিন্তু সেই queue থেকে প্যাকেট নিয়ে অ্যাপ পর্যন্ত পৌঁছানোর কাজ (নিচু priority) কখনো সময় পায় না; queue ভরে প্যাকেট ফেলে দেওয়া হয়। CPU ১০০% ব্যস্ত, অথচ কাজের ফল শূন্য — একে বলে receive livelock (সাধারণ livelock-এর একটি রূপ)। ১৯৯৬-৯৭ সালে Jeffrey Mogul আর K. K. Ramakrishnan-এর বিখ্যাত গবেষণাপত্র এটি মেপে দেখায়, আর সমাধান প্রস্তাব করে: চাপ বাড়লে interrupt বন্ধ করে polling, আর প্রতি দফায় কাজের সীমা — NAPI ঠিক এই ধারণার বাস্তব রূপ।
আরেক বিপদ interrupt storm: একটি ত্রুটিপূর্ণ ডিভাইস বা ack না পাওয়া level-triggered লাইন অবিরাম interrupt দেয়। Linux এটি ধরে: কোনো handler বারবার IRQ_HANDLED না বললে log-এ “nobody cared” লিখে লাইনটি বন্ধ করে। প্রতিরোধের আরেকটি হাতিয়ার interrupt coalescing: ডিভাইস কয়েকটি ঘটনা জমিয়ে, অথবা কয়েক মাইক্রোসেকেন্ড অপেক্ষা করে, একটি interrupt দেয়। Linux-এ ethtool -C eth0 rx-usecs 50 ধরনের কমান্ডে এটি বদলানো যায় — throughput বাড়ে, প্রতিটি প্যাকেটের দেরি সামান্য বাড়ে।
৪ · DMA গভীরে — descriptor, scatter-gather, IOMMU আর cache
পর্ব ০৪-এর stepper-এ DMA-র মূল ছবি দেখেছেন: CPU কয়েকটি register-এ উৎস, গন্তব্য আর আকার লিখে দেয়, DMA engine কপি করে, শেষে একটি interrupt. আধুনিক ডিভাইস এক ধাপ এগিয়ে — তারা একটির বদলে শত শত অনুরোধ একসাথে নেয়, RAM-এ রাখা একটি তালিকা থেকে।
Descriptor ring — RAM-এ রাখা কাজের খাতা
Driver RAM-এ একটি বৃত্তাকার অ্যারে বানায় — descriptor ring. প্রতিটি descriptor একটি ছোট struct: buffer-এর ঠিকানা, আকার, কিছু flag (যেমন “এটি এখন ডিভাইসের”). Driver ring-এর মাথায় descriptor যোগ করে, তারপর ডিভাইসের একটি doorbell register-এ (MMIO) নতুন মাথার নম্বর লেখে। ডিভাইস নিজেই DMA দিয়ে descriptor পড়ে, কাজ করে, আর ফল লেখে আরেকটি ring-এ বা descriptor-এর status-এ:
RAM-এ RX descriptor ring (৮টি ঘর)
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ done │ done │ dev │ dev │ dev │ dev │ free │ free │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
▲ driver এখান থেকে পড়ে ▲ ডিভাইস এখানে লিখবে
done = ডিভাইস ভরেছে, driver-এর তোলার অপেক্ষায় · dev = খালি buffer, ডিভাইসের হাতে
NVMe-তে একই ধারণার নাম submission queue আর completion queue — প্রতিটি core-এর নিজের জোড়া। এভাবে এক doorbell লেখায় অনেক অনুরোধ যায়, আর এক interrupt-এ অনেক ফল আসে।
Scatter-gather — ছড়ানো page এক অনুরোধে
একটি ১৬ KiB buffer virtual ঠিকানায় একটানা হলেও physical RAM-এ চারটি আলাদা ৪ KiB frame-এ ছড়ানো থাকতে পারে (পর্ব ৩২)। পুরোনো DMA শুধু একটানা physical এলাকা বুঝত — তখন হয় চারটি আলাদা অনুরোধ, নয়তো একটি একটানা “bounce buffer”-এ কপি। Scatter-gather DMA-তে descriptor-এ থাকে (ঠিকানা, দৈর্ঘ্য) জোড়ার একটি তালিকা; ডিভাইস নিজেই ছড়ানো টুকরো থেকে জড়ো করে (gather) বা টুকরোগুলোতে ছড়িয়ে (scatter) লেখে। NVMe-র PRP list আর SGL, নেটওয়ার্ক কার্ডের একাধিক-টুকরোর প্যাকেট — সবই এর রূপ। User space-এ একই ধারণা readv()/writev().
Cycle stealing বনাম burst
DMA engine আর CPU একই মেমরি-পথ ভাগ করে। Cycle stealing-এ DMA একবারে একটি শব্দ সরিয়ে bus ছেড়ে দেয় — CPU মাঝে মাঝে এক-আধ cycle হারায়। Burst mode-এ পুরো ব্লক একটানা — দ্রুত, কিন্তু সেই সময় CPU-র মেমরি-প্রবেশ আটকে থাকতে পারে। আধুনিক PCIe সিস্টেমে পার্থক্যটি কম, তবে ছোট embedded সিস্টেমে আর পরীক্ষায় এই তুলনা প্রায়ই আসে।
IOMMU — DMA-র জন্য page table
একটি bus-master ডিভাইস CPU-কে না জিজ্ঞেস করে RAM-এ লিখতে পারে। Driver-এর একটি বাগ (ভুল ঠিকানা), ত্রুটিপূর্ণ firmware, বা বাইরের পোর্টে লাগানো ক্ষতিকর ডিভাইস — যেকোনোটি kernel-এর মেমরি নষ্ট করতে পারত। IOMMU (Intel-এ VT-d, AMD-তে AMD-Vi, ARM-এ SMMU) ডিভাইস আর RAM-এর মাঝখানে বসে ঠিক MMU-র মতো কাজ করে: ডিভাইস দেখে একটি “I/O virtual address”, IOMMU-র টেবিল সেটিকে physical ঠিকানায় অনুবাদ করে — আর যে ঠিকানা ওই ডিভাইসের জন্য map করা হয়নি, সেখানে লেখা আটকে দিয়ে ত্রুটি জানায়।
তিনটি সুবিধা: (১) নিরাপত্তা — Thunderbolt বা USB4-এর মতো বাইরের পোর্টে লাগানো ডিভাইস শুধু তার জন্য খোলা buffer-ই ছুঁতে পারে; (২) ভুল ধরা — driver-এর ভুল ঠিকানা চুপচাপ মেমরি নষ্ট না করে ধরা পড়ে; (৩) virtualization — একটি PCIe ডিভাইস সরাসরি একটি VM-কে দেওয়া যায় (পর্ব ৫০), কারণ ডিভাইসটি সেই VM-এর মেমরির বাইরে যেতে পারে না। দাম: প্রতিটি map/unmap-এ টেবিল হালনাগাদ আর IOTLB পরিষ্কারের খরচ।
নিজের যন্ত্রে যাচাই (প্রতিরক্ষার জন্য): Windows-এ msinfo32-এর System Summary-তে “Kernel DMA Protection: On” দেখুন; Off হলে firmware সেটিংসে VT-d/AMD-Vi (IOMMU) চালু আছে কি না দেখুন। Linux-এ dmesg | grep -i -e DMAR -e IOMMU আর ls /sys/kernel/iommu_groups — ফাঁকা হলে IOMMU সক্রিয় নয়। কোনো ফোরাম “গতির জন্য” iommu=off বা firmware-এ VT-d বন্ধ করতে বললে, সুরক্ষা হারানোর দামটি আগে বুঝে নিন।
DMA আর cache coherence
CPU-র cache-এ একটি buffer-এর পুরোনো কপি থাকতে পারে, অথচ ডিভাইস সরাসরি RAM-এ নতুন ডেটা লিখেছে — CPU তখন পুরোনো (stale) ডেটা পড়বে। উল্টো দিকে, CPU-র লেখা cache-এ বসে আছে, RAM-এ পৌঁছায়নি, আর ডিভাইস RAM থেকে পুরোনোটা পড়ছে। x86-এ PCIe DMA সাধারণত cache-coherent: হার্ডওয়্যার cache “snoop” করে মিলিয়ে নেয় (পর্ব ০৪-এর cache coherence-এর আত্মীয়)। কিন্তু অনেক ARM SoC-এ DMA coherent নয়; সেখানে software-কে DMA-র আগে cache “flush” (লেখা RAM-এ পাঠানো) আর পরে “invalidate” (পুরোনো কপি ফেলা) করতে হয়।
Linux driver-কে এই পার্থক্য মনে রাখতে হয় না — সে শুধু DMA API ডাকে, আর API প্রতিটি আর্কিটেকচারে ঠিক কাজটি করে (IOMMU map, cache রক্ষণাবেক্ষণ, দরকারে bounce buffer):
dma_addr_t h = dma_map_single(dev, buf, len, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, h)) /* IOMMU full, or address unusable */
return -ENOMEM;
desc->addr = cpu_to_le64(h); /* device sees h, not the CPU address */
desc->len = cpu_to_le16(len);
desc->flags = DESC_OWNED_BY_DEVICE;
writel(tail, nic->regs + RX_DOORBELL); /* ring the doorbell */
/* ... later, in the completion path: */
dma_unmap_single(dev, h, len, DMA_FROM_DEVICE); /* invalidate caches if needed */
process_packet(buf); /* only now may the CPU read buf */দীর্ঘস্থায়ী ভাগের এলাকার জন্য (যেমন descriptor ring নিজেই) আছে dma_alloc_coherent(), যা এমন মেমরি দেয় যেখানে CPU আর ডিভাইস দুজনেই সবসময় একই জিনিস দেখে।
৫ · Application I/O interface — block, character, network আর /dev
অ্যাপ কখনো NVMe-র doorbell বা কিবোর্ডের scancode দেখে না। Kernel-এর I/O subsystem হাজার রকম ডিভাইসকে কয়েকটি সাধারণ শ্রেণিতে সাজায়, প্রতিটির একটি মানক interface — অফিসের ফাইল ক্যাবিনেটে যেমন হাজার কাগজ কয়েকটি ড্রয়ারে, প্রতিটি ড্রয়ার খোলার নিয়ম এক।
🧱 Block device
নির্দিষ্ট আকারের ব্লকে পড়া-লেখা, যেকোনো ব্লকে সরাসরি যাওয়া যায় (random access). ডিস্ক, SSD, USB ড্রাইভ। ওপরে সাধারণত file system আর page cache বসে।
block device ·/dev/sda, /dev/nvme0n1
🔤 Character device
বাইটের স্রোত, সাধারণত ক্রম মেনে — কিবোর্ড, terminal, সিরিয়াল পোর্ট, /dev/null, /dev/urandom. read/write-এর বাইরে বিশেষ আদেশের জন্য ioctl.
/dev/tty
🌐 Network device
প্যাকেট পাঠানো-নেওয়া; read/write-এর মডেলে মেলে না, তাই Unix-এ এর interface socket. Linux-এ /dev-এ এর কোনো ফাইল নেই — ip link-এ নাম দেখা যায় (eth0, wlan0).
Unix-এর দর্শন “সবকিছুই ফাইল”: block আর character device-কে /dev-এর একটি device file দিয়ে খোলা যায়, সাধারণ open/read/write-এ। ফাইলটির ভেতরে কোনো ডেটা থাকে না; থাকে দুটি সংখ্যা — major number (কোন driver) আর minor number (সেই driver-এর কোন ডিভাইস বা অংশ)। আধুনিক Linux-এ এই ফাইলগুলো devtmpfs আর udev ডিভাইস খোঁজার সাথে সাথে বানায়। এবার অনলাইন রানারের container-টিতেই দেখি (কোনো ফাইল পড়া না গেলে script থামে না, শুধু জানায়):
echo "== three character devices =="
ls -l /dev/null /dev/zero /dev/urandom
echo "== type and major:minor via stat (hex) =="
stat -c '%-13n %F major=%t minor=%T' /dev/null /dev/zero /dev/urandom
echo "== reading them =="
printf 'zero: '; head -c 8 /dev/zero | od -An -tx1
printf 'urandom: '; head -c 8 /dev/urandom | wc -c
echo "== /proc/interrupts (first lines, cut) =="
if [ -r /proc/interrupts ]; then head -n 6 /proc/interrupts | cut -c1-100
else echo "not readable here"; fi
echo "== /proc/devices (first lines) =="
head -n 12 /proc/devicesএকটি আসল রানের আউটপুট:
== three character devices ==
crw-rw-rw- 1 nobody nogroup 1, 3 Aug 6 17:54 /dev/null
crw-rw-rw- 1 nobody nogroup 1, 9 Aug 6 17:54 /dev/urandom
crw-rw-rw- 1 nobody nogroup 1, 5 Aug 6 17:54 /dev/zero
== type and major:minor via stat (hex) ==
/dev/null character special file major=1 minor=3
/dev/zero character special file major=1 minor=5
/dev/urandom character special file major=1 minor=9
== reading them ==
zero: 00 00 00 00 00 00 00 00
urandom: 8
== /proc/interrupts (first lines, cut) ==
CPU0 CPU1
0: 180 0 IO-APIC 2-edge timer
1: 9 0 IO-APIC 1-edge i8042
8: 0 1 IO-APIC 8-edge rtc0
9: 0 0 IO-APIC 9-fasteoi acpi
12: 0 15 IO-APIC 12-edge i8042
== /proc/devices (first lines) ==
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
5 ttyprintk
7 vcs
10 misc
13 input
(১) প্রথম অক্ষর c মানে character device; আকারের জায়গায় major, minor — তিনটিরই major ১ (driver “mem”, /proc/devices-এর প্রথম লাইন), minor ৩, ৫, ৯ বলে দেয় কোনটি null, zero আর urandom. মালিক nobody দেখাচ্ছে কারণ container-এর user namespace host-এর root-কে চেনে না। (২) /dev/zero যতবার পড়ুন শূন্য বাইট দেয়, /dev/urandom দেয় এলোমেলো বাইট (৮টি পড়া হয়েছে, মান প্রতিবার আলাদা বলে শুধু গোনা)। (৩) /proc/interrupts-এ প্রতিটি IRQ-এর প্রতি core-এ আসার গণনা, কোন controller (IO-APIC), কোন trigger (edge বা fasteoi) আর কোন driver — timer, i8042 (PS/2 কিবোর্ড ও মাউস controller), rtc0. রানারটি সম্ভবত একটি virtual machine-এর ভেতরে, আর যন্ত্রটি অল্প সময় আগে চালু হয়েছে, তাই সংখ্যা ছোট; আসল সার্ভারে নিচের দিকে MSI-X-এর PCI-MSIX … nvme0q1 ধরনের লাইন আর LOC (local timer), NMI দেখবেন। (৪) 10 misc — অংশ ৮-এর driver ঠিক এই major-এ বসবে।
Windows আর macOS-এ
Windows-এ major/minor নেই। প্রতিটি ডিভাইস একটি device object (যেমন \Device\HarddiskVolume3); অ্যাপ সেটি খোলে CreateFile দিয়ে, বিশেষ আদেশ পাঠায় DeviceIoControl দিয়ে (ioctl-এর সমতুল্য), আর driver প্রতিটি অনুরোধ পায় একটি IRP (I/O Request Packet) হিসেবে। macOS-এ BSD-ধাঁচের /dev আছে, কিন্তু driver-এর কাঠামো Apple-এর IOKit.
I/O protection — অ্যাপ কেন সরাসরি port ছুঁতে পারে না
সব I/O নির্দেশ privileged instruction: x86-এর in/out user mode-এ চালালে exception হয় (যদি না kernel বিশেষভাবে অনুমতি দেয়), আর ডিভাইসের MMIO ঠিকানা কোনো user process-এর page table-এ রাখা হয় না। তাই I/O করার একমাত্র পথ system call, যেখানে kernel অনুমতি (ফাইলের permission, পর্ব ৪৭), ঠিকানার বৈধতা আর সীমা যাচাই করে। Linux-এ ioperm() দিয়ে নির্দিষ্ট port খোলা যায়, কিন্তু তাতে CAP_SYS_RAWIO ক্ষমতা লাগে — সাধারণ ব্যবহারকারী পারেন না।
৬ · Blocking, non-blocking আর asynchronous I/O
ব্যাংকের টোকেন-কাউন্টারের কথা ভাবুন। তিনভাবে অপেক্ষা করা যায়: কাউন্টারের সামনে দাঁড়িয়ে থাকা যতক্ষণ না কাজ হয় (blocking); একবার জিজ্ঞেস করে “এখনো হয়নি” শুনে চলে গিয়ে অন্য কাজ, পরে আবার জিজ্ঞেস (non-blocking); অথবা ফর্ম জমা দিয়ে চলে যাওয়া, কাজ শেষে ব্যাংক SMS পাঠাবে (asynchronous).
- Blocking:
read()ডেটা না আসা পর্যন্ত process-কে wait queue-তে ঘুম পাড়িয়ে রাখে (পর্ব ০৮-এর blocked অবস্থা)। লেখা সহজ, কিন্তু একটি thread একসাথে একটিই অপেক্ষা করতে পারে। - Non-blocking:
O_NONBLOCKদেওয়া fd-তে ডেটা না থাকলেread()সাথে সাথে −১ ফেরায় আরerrnoহয় EAGAIN (একই মানEWOULDBLOCK). কিন্তু এর ওপর লুপ ঘোরালে আবার busy waiting! - I/O multiplexing: সমাধান — kernel-কে বলা “এই fd-গুলোর যেকোনোটি প্রস্তুত হলে জাগাবেন”. select (4.2BSD, ১৯৮৩) আর poll প্রতিবার পুরো তালিকা kernel-এ দেয়, kernel প্রতিটি দেখে — fd হাজার হলে খরচ O(n). Linux-এর epoll (২.৫/২.৬, ২০০২-এর আশেপাশে) আর BSD/macOS-এর kqueue (FreeBSD, ২০০০) তালিকাটি kernel-এ একবার নিবন্ধন করে, আর শুধু প্রস্তুতগুলো ফেরত দেয়। nginx, Node.js-এর libuv, Redis — সবাই এর ওপর দাঁড়িয়ে।
- Asynchronous I/O: অ্যাপ অনুরোধ জমা দিয়ে চলে যায়; kernel কাজটি শেষ করে জানায়। “প্রস্তুত হলে জানাও” (readiness, epoll) আর “শেষ হলে জানাও” (completion) — এই পার্থক্যই মূল। Windows-এর IOCP (I/O completion port) completion মডেলের পুরোনো উদাহরণ; Linux-এ আধুনিক উত্তর io_uring (৫.১, ২০১৯)। POSIX-এর
aio_read()-ও আছে, তবে glibc এটি ভেতরে thread দিয়ে চালায়।
এবার প্রথম তিনটি আচরণ আসল kernel-এ দেখি — একটি pipe-এ: খালি pipe-এ non-blocking read, ৩০০ ms timeout-এর poll, একটি child ২০০ ms পরে লিখলে blocking read কতক্ষণ ঘুমায়, আর ডেটা আগেই থাকলে poll কী করে। সময় ঠিক সংখ্যায় না ছেপে সম্পর্ক ছাপা হয়েছে, যাতে প্রতি রানে একই আউটপুট আসে:
#define _GNU_SOURCE
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <time.h>
#include <unistd.h>
#include <sys/wait.h>
static struct timespec t0;
static void mark(void) { clock_gettime(CLOCK_MONOTONIC, &t0); }
static long ms(void) {
struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t);
return (t.tv_sec - t0.tv_sec) * 1000 + (t.tv_nsec - t0.tv_nsec) / 1000000;
}
int main(void) {
int fd[2]; char buf[32]; if (pipe(fd) != 0) return 1;
int fl = fcntl(fd[0], F_GETFL);
fcntl(fd[0], F_SETFL, fl | O_NONBLOCK); /* 1: non-blocking */
ssize_t n = read(fd[0], buf, sizeof buf);
printf("1) non-blocking read, empty pipe: n=%zd errno=%s\n", n, errno == EAGAIN ? "EAGAIN" : "other");
struct pollfd p = { .fd = fd[0], .events = POLLIN };
mark(); int r = poll(&p, 1, 300); /* 2: timeout 300 ms */
printf("2) poll, nothing written: returned %d, waited >= 290 ms: %s\n", r, ms() >= 290 ? "yes" : "no");
fflush(stdout); /* before fork! */
if (fork() == 0) { usleep(200000); _exit(write(fd[1], "hello", 5) == 5 ? 0 : 1); }
fcntl(fd[0], F_SETFL, fl); /* 3: blocking again */
mark(); n = read(fd[0], buf, sizeof buf);
printf("3) blocking read: n=%zd \"%.5s\", slept >= 190 ms: %s\n", n, buf, ms() >= 190 ? "yes" : "no");
wait(NULL);
if (write(fd[1], "x", 1) != 1) return 1; /* 4: data already there */
mark(); r = poll(&p, 1, 300);
printf("4) poll, data ready: returned %d, POLLIN=%s, waited < 50 ms: %s\n",
r, (p.revents & POLLIN) ? "yes" : "no", ms() < 50 ? "yes" : "no");
return 0;
}আউটপুট:
1) non-blocking read, empty pipe: n=-1 errno=EAGAIN
2) poll, nothing written: returned 0, waited >= 290 ms: yes
3) blocking read: n=5 "hello", slept >= 190 ms: yes
4) poll, data ready: returned 1, POLLIN=yes, waited < 50 ms: yes
(১) খালি pipe, O_NONBLOCK — kernel অপেক্ষা না করে সাথে সাথে −১ আর EAGAIN দিল। (২) poll ঘুমিয়ে ছিল, কেউ লেখেনি, তাই ৩০০ ms পরে ০ ফেরত — ০ মানে timeout, কোনো fd প্রস্তুত নয়। (৩) Blocking read-এ process wait queue-তে ঘুমাল; child ২০০ ms পরে লিখতেই kernel তাকে জাগাল, আর read ৫ বাইট নিয়ে ফিরল। এই ঘুমের সময় CPU অন্য কাজ করতে পারত — busy waiting-এর সাথে এটাই পার্থক্য। (৪) ডেটা আগে থেকেই থাকায় poll অপেক্ষা না করে ১ (প্রস্তুত fd-এর সংখ্যা) দিল, revents-এ POLLIN. fork-এর আগে fflush(stdout) একটি অভ্যাস: stdout pipe-এ গেলে পুরো buffered থাকে; fflush না করলে, আর child _exit-এর বদলে exit করলে, না-ছাপা লাইনগুলো দুবার ছাপা হতো।
epoll-এর একটি পরিচিত ফাঁদ: edge-triggered (EPOLLET) মোডে kernel শুধু অবস্থা বদলানোর মুহূর্তে জানায়। একটি ঘটনায় আপনি অর্ধেক ডেটা পড়ে থামলে, নতুন ডেটা না আসা পর্যন্ত আর কোনো খবর আসবে না — প্রোগ্রাম চুপচাপ আটকে থাকবে। নিয়ম: edge-triggered-এ fd অবশ্যই non-blocking, আর প্রতিটি ঘটনায় EAGAIN না পাওয়া পর্যন্ত পড়তে হয়।
৭ · Buffering, caching, spooling — আর buffering-এর হিসাব
Kernel I/O-র পথে ডেটা সাময়িক রাখে তিনটি কারণে: (১) গতির অমিল — নেটওয়ার্ক থেকে ধীরে আসা ফাইল জমিয়ে একবারে ডিস্কে লেখা; (২) আকারের অমিল — নেটওয়ার্কের ছোট প্যাকেট জুড়ে বড় বার্তা, বা অ্যাপের ১০০ বাইটের লেখা ৪ KiB ব্লকে মেলানো; (৩) copy semantics — write() ফেরার পর অ্যাপ buffer বদলালেও যা লেখার কথা ছিল সেটিই যাবে, কারণ kernel আগেই নিজের কাছে কপি রেখেছে।
| কৌশল | কী রাখে | উদ্দেশ্য | উদাহরণ |
|---|---|---|---|
| Buffer | ডেটার একমাত্র কপি, পথে থাকা অবস্থায় | উৎপাদক আর ভোক্তার গতি/আকার মেলানো | pipe-এর buffer, NIC-এর ring, socket buffer |
| Cache | অন্য কোথাও থাকা ডেটার দ্রুত কপি | বারবার পড়া দ্রুত করা | page cache (একই page অনেক সময় buffer আর cache দুটোই) |
| Spool | একবারে একজনই ব্যবহার করতে পারে এমন ডিভাইসের পুরো কাজ, ডিস্কে | ভাগ করা যায় না এমন ডিভাইসকে সারিতে ভাগ করা | প্রিন্টারের queue — spooling (Linux-এ CUPS, Windows-এ Print Spooler) |
Single, double আর circular buffer
একটি প্রোগ্রাম ডিস্ক থেকে n টি ব্লক পড়ে, প্রতিটিতে কিছু হিসাব করে। তিনটি সময় ধরা যাক: T = একটি ব্লক ডিভাইস থেকে buffer-এ আসার সময়, C = একটি ব্লকে হিসাবের সময়, M = system buffer থেকে process-এর মেমরিতে কপির সময়।
- Buffer ছাড়া: ডিভাইস সরাসরি process-এর মেমরিতে লেখে; পড়া আর হিসাব পালাক্রমে, কোনো overlap নেই। প্রতি ব্লকে T + C.
- Single buffer: kernel-এর একটি buffer. ডিভাইস buffer ভরে, kernel সেটি process-এ কপি করে (M), তারপর process হিসাব করার সময়েই ডিভাইস পরের ব্লক দিয়ে buffer আবার ভরে। স্থিত অবস্থায় প্রতি ব্লকে max(C, T) + M.
- Double buffer: দুটি buffer. Process একটিতে কাজ করে, ডিভাইস তখন অন্যটি ভরে, তারপর দুজন জায়গা বদল করে। প্রতি ব্লকে max(C, T) — ধীরতর পক্ষটিই গতি ঠিক করে।
- Circular buffer: দুইয়ের বেশি buffer একটি বৃত্তে (অংশ ৪-এর descriptor ring-এর মতো)। গড় গতি double-এর মতোই, কিন্তু হঠাৎ ঢেউ (burst) সামলাতে পারে — যেমন নেটওয়ার্কের প্যাকেট বা ভিডিওর ফ্রেম।
শুরু আর শেষের হিসাবসহ মোট সময়: buffer ছাড়া n(T + C); single = T + M + C + (n − 1)(max(C, T) + M); double = T + C + (n − 1)·max(C, T). একটি পরীক্ষা-ধাঁচের হিসাব: n = ১০, T = ৫ ms, C = ৩ ms, M = ১ ms. Buffer ছাড়া ১০ × ৮ = ৮০ ms. Single: ৫ + ১ + ৩ + ৯ × (৫ + ১) = ৯ + ৫৪ = ৬৩ ms. Double: ৫ + ৩ + ৯ × ৫ = ৫৩ ms. Throughput-এর ভাষায়, স্থিত অবস্থায় প্রতি ব্লক ৮, ৬ আর ৫ ms — সেকেন্ডে ১২৫, প্রায় ১৬৭ আর ২০০টি ব্লক। নিচের simulator ধাপে ধাপে ঘটনা মিলিয়ে এই সংখ্যা বের করে, আর আরও দুটি ক্ষেত্র দেখায়:
// Total time to read and process n blocks: T = transfer, C = compute, M = move (buffer -> user)
function simulate(n, T, C, M) {
const none = n * (T + C); // read and compute strictly alternate
let full = T, done = 0; // single: one kernel buffer
for (let k = 1; k <= n; k++) {
const moved = Math.max(full, done) + M; // copy out when buffer full AND process free
done = moved + C; // process computes block k
full = moved + T; // device refills the freed buffer meanwhile
}
const single = done;
const fillEnd = [], compEnd = []; // double: blocks alternate between 2 buffers
for (let k = 0; k < n; k++) {
const devFree = k > 0 ? fillEnd[k - 1] : 0;
const bufFree = k > 1 ? compEnd[k - 2] : 0; // buffer k%2 is free once block k-2 is done
fillEnd[k] = Math.max(devFree, bufFree) + T;
compEnd[k] = Math.max(fillEnd[k], k > 0 ? compEnd[k - 1] : 0) + C;
}
return [none, single, compEnd[n - 1]];
}
const cases = [[10, 5, 3, 1], [10, 2, 4, 1], [10, 5, 5, 0]];
console.log("case (ms) none single double");
for (const [n, T, C, M] of cases) {
const [a, b, c] = simulate(n, T, C, M);
const tag = `n=${n} T=${T} C=${C} M=${M}`;
console.log(tag.padEnd(20) + String(a).padStart(5) + String(b).padStart(8) + String(c).padStart(8));
}
console.log("steady state per block: none T+C, single max(C,T)+M, double max(C,T)");আউটপুট:
case (ms) none single double
n=10 T=5 C=3 M=1 80 63 53
n=10 T=2 C=4 M=1 60 52 42
n=10 T=5 C=5 M=0 100 55 55
steady state per block: none T+C, single max(C,T)+M, double max(C,T)
(১) প্রথম সারি ওপরের হাতের হিসাবের সাথে হুবহু মেলে: ৮০, ৬৩, ৫৩. (২) দ্বিতীয় সারিতে C > T, তাই হিসাবই bottleneck: double = ২ + ৪ + ৯ × ৪ = ৪২, single = ২ + ১ + ৪ + ৯ × (৪ + ১) = ৫২. (৩) তৃতীয় সারিটি সূক্ষ্ম: M = ০ হলে single আর double সমান (৫৫), কারণ এই মডেলে কপি শেষ হলেই single buffer খালি হয়। বাস্তবে কপি কখনো শূন্য নয়, আর process ডেটা buffer-এ রেখেই ব্যবহার করলে single buffer-এ ডিভাইসকে থামতে হয় — তখন double অপরিহার্য। তবু buffer ছাড়ার (১০০) তুলনায় দুটোই প্রায় দ্বিগুণ দ্রুত — I/O আর হিসাবের overlap-ই আসল জয়। নিজের সংখ্যা দিয়ে cases-এ সারি যোগ করে দেখুন।
I/O scheduling আর error handling — সংক্ষেপে
I/O scheduling মানে জমে থাকা অনুরোধগুলোর ক্রম ঠিক করা — পাশাপাশি ব্লকের অনুরোধ জুড়ে দেওয়া (merge), HDD-তে মাথার নড়াচড়া কমানো, আর কোনো process যেন অনন্তকাল অপেক্ষা না করে। Linux-এর block layer এখন blk-mq (multi-queue) ভিত্তিক, আর scheduler হিসেবে mq-deadline, bfq, kyber বা none (দ্রুত NVMe-তে প্রায়ই) বেছে নেওয়া যায়। FCFS, SSTF, SCAN, C-SCAN-এর হিসাবসহ পুরো আলোচনা পর্ব ৪০-এ।
Error handling: ডিভাইস ব্যর্থ হতে পারে সাময়িকভাবে (নেটওয়ার্কে প্যাকেট হারানো, ডিস্কের একটি পড়া ব্যর্থ) বা স্থায়ীভাবে (ডিস্কের নষ্ট sector, controller মরে যাওয়া)। Kernel সাধারণত কয়েকবার আবার চেষ্টা করে, না হলে system call-কে −১ আর errno = EIO ফেরত দেয়, আর বিস্তারিত লেখে kernel log-এ (dmesg, Windows-এ Event Viewer). ডিস্ক নিজেও নিজের স্বাস্থ্যের খাতা রাখে — SMART বা NVMe-র health log, যা smartctl দিয়ে পড়া যায়।
৮ · Device driver — kernel আর হার্ডওয়্যারের দোভাষী
Driver-এর দুটি মুখ: ওপরের দিকে kernel-এর মানক interface (যেমন Linux-এর file_operations বা block device-এর queue), নিচের দিকে একটি নির্দিষ্ট controller-এর register, ring আর interrupt-এর ভাষা। এই স্তর-ভাগের কারণেই একই cat কমান্ড SATA ডিস্ক, NVMe আর USB pen drive — সবখানে চলে। একটি ফাইল পড়ার স্তরগুলো:
Linux-এ একটি ফাইল পড়ার স্তর
From the system call down to the controller
- System call
read()user → kernel - VFS + file system
ext4পর্ব ৪২, ৪৩ - Page cache
RAMhit হলে এখানেই শেষ - Block layer
bio, blk-mqmerge, scheduler - Driver → controller
nvmedoorbell, DMA, IRQ
একটি character driver-এর কঙ্কাল
নিচের Linux module একটি /dev/hello বানায়, যা পড়লে একটি লাইন দেয়। এখানে misc device কাঠামো ব্যবহার করা হয়েছে (major ১০, minor kernel ঠিক করে) — সবচেয়ে ছোট বৈধ উদাহরণ। এটি এই পাতায় চলবে না: kernel module লোড করতে root আর kernel-এর header লাগে।
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
static const char msg[] = "hello from the kernel\n";
static ssize_t hello_read(struct file *f, char __user *ubuf,
size_t len, loff_t *ppos)
{
size_t left;
if (*ppos >= sizeof(msg) - 1)
return 0; /* EOF */
left = sizeof(msg) - 1 - *ppos;
if (len > left)
len = left;
if (copy_to_user(ubuf, msg + *ppos, len)) /* never memcpy to a user pointer */
return -EFAULT;
*ppos += len;
return len;
}
static const struct file_operations hello_fops = {
.owner = THIS_MODULE,
.read = hello_read, /* .open, .release, .write, .unlocked_ioctl ... optional */
};
static struct miscdevice hello_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "hello", /* devtmpfs/udev creates /dev/hello */
.fops = &hello_fops,
};
module_misc_device(hello_dev); /* misc_register() on load, deregister on unload */
MODULE_LICENSE("GPL");লক্ষ করুন: (১) driver কোনো ফাইলের নাম বা path জানে না — VFS fd থেকে struct file খুঁজে f_op->read ডাকে; (২) copy_to_user ঠিকানাটি user-এর অংশে কি না যাচাই করে, আর page fault হলেও নিরাপদে -EFAULT দেয় — kernel-এর সবচেয়ে গুরুত্বপূর্ণ নিরাপত্তা-নিয়মগুলোর একটি; (৩) *ppos বাড়িয়ে driver জানায় পরের পড়া কোথা থেকে, আর ০ ফেরত মানে EOF. Build আর পরীক্ষা নিজের যন্ত্রে এমন দেখায়:
$ make -C /lib/modules/$(uname -r)/build M=$PWD modules
$ sudo insmod hello_misc.ko
$ ls -l /dev/hello
crw------- 1 root root 10, 123 Oct 26 10:15 /dev/hello
$ sudo cat /dev/hello
hello from the kernel
$ sudo rmmod hello_miscUser space-এর driver
সব driver kernel-এ থাকতে হবে, এমন নয়। Linux-এর UIO একটি ডিভাইসের register এলাকা user process-এ mmap করতে দেয়, আর interrupt আসে একটি fd-এর read হিসেবে; VFIO এর সাথে IOMMU যোগ করে, যাতে user-space driver (DPDK, SPDK) বা একটি VM ডিভাইসের পূর্ণ নিয়ন্ত্রণ পায় কিন্তু নিজের মেমরির বাইরে DMA করতে পারে না। File system-ও user space-এ লেখা যায় — FUSE (যেমন sshfs; পর্ব ৪২)। Windows-এ একই ধারণা UMDF (user-mode) বনাম KMDF (kernel-mode) driver; macOS ১০.১৫ থেকে Apple DriverKit দিয়ে অনেক driver user space-এ সরাচ্ছে।
Driver — নির্ভরযোগ্যতার সবচেয়ে বড় ঝুঁকি
Linux-এর সোর্স কোডের অর্ধেকের বেশি drivers/ ফোল্ডারে। কোডগুলো লেখেন হাজারো নির্মাতা, প্রায়ই এমন হার্ডওয়্যারের জন্য যা মূল ডেভেলপারদের হাতে নেই, আর চলে সর্বোচ্চ অধিকারে — একটি ভুল pointer পুরো kernel ফেলে দেয়। প্রতিরক্ষার পথ: স্থির-বিশ্লেষণ টুল, চলার সময় যাচাই (Windows-এর Driver Verifier, Linux-এর KASAN), driver signing, user-space driver আর নিরাপদ ভাষা (Linux ৬.১ থেকে Rust-এর প্রাথমিক সমর্থন) — ইতিহাস ভাবনার প্রশ্ন ২-এ।
৯ · একটি read()-এর জীবনচক্র, আর পারফরম্যান্সের কৌশল
এবার সব টুকরো এক জায়গায়। একটি process একটি ফাইলের প্রথম ৪ KiB পড়ছে, ফাইলটি NVMe SSD-তে, আর page cache-এ নেই (cache miss). প্রতিটি ধাপে কোন স্তর কাজ করছে, process কোন অবস্থায়, আর ডেটা কোথায় — দেখুন:
- অ্যাপ
read() - VFS, ext4
page cache - Block layer
bio - NVMe driver
doorbell - SSD + DMA
MSI-X
| কোন স্তরে | |
| process A | |
| ডেটা কোথায় | |
| CPU-র বাইট-কপি |
- System call. A
read(fd, buf, 4096)ডাকল।syscallনির্দেশে CPU kernel mode-এ গেল (পর্ব ০৫)। Kernel fd থেকেstruct fileখুঁজে নিল। - VFS আর page cache. VFS ফাইল সিস্টেমের (ext4) read পথ ডাকল। ওই অংশের page page cache-এ নেই — miss. তাই একটি খালি page বরাদ্দ করে lock করা হলো, আর ext4 হিসাব করল ডেটা ডিস্কের কোন ব্লকে (পর্ব ৪৩)। Cache hit হলে এখান থেকে সোজা ধাপ ৭।
- Block layer. একটি bio তৈরি হলো: “ডিভাইসের এই sector থেকে ৮টি sector, এই page-এ”. blk-mq এটি এই core-এর queue-তে রাখল; পাশাপাশি অনুরোধ জোড়া দেওয়া বা ক্রম বদলানো এখানেই (পর্ব ৪০)।
- Driver আর doorbell. NVMe driver এই core-এর submission queue-তে একটি ৬৪ বাইটের command লিখল — opcode “read”, শুরুর ব্লক, আর page-এর DMA ঠিকানা (IOMMU থাকলে তার অনুবাদিত ঠিকানা) — তারপর doorbell register-এ MMIO লেখা। A-কে wait queue-তে ঘুম পাড়িয়ে scheduler অন্য process চালাল।
- ডিভাইসের কাজ — CPU ছাড়া। SSD controller DMA দিয়ে command পড়ল, flash থেকে ডেটা আনল, DMA দিয়ে সরাসরি page cache-এর page-এ লিখল, completion queue-তে একটি entry লিখল, তারপর এই queue-এর MSI-X interrupt পাঠাল। এই পুরো সময় CPU অন্য কাজে।
- Interrupt আর completion. Top half completion queue পড়ে bio-টি শেষ ঘোষণা করল; block layer page-কে “uptodate” চিহ্নিত করে unlock করল, আর সেই page-এর অপেক্ষায় থাকা A-কে জাগিয়ে ready queue-তে তুলল।
- শেষ কপি আর ফেরা. Scheduler A-কে চালাল। Kernel page cache থেকে ৪০৯৬ বাইট
copy_to_userদিয়ে A-এরbuf-এ কপি করল — পুরো পথে CPU-র একমাত্র বাইট-কপি।read()৪০৯৬ ফেরত দিল, CPU user mode-এ। পরের বার একই অংশ পড়লে ধাপ ২-এই hit.
খরচ কমানোর তিন দিক
উপরের পথে খরচ আসে তিন জায়গা থেকে, আর আধুনিক কৌশলগুলো এক-একটিকে লক্ষ্য করে:
- Context switch আর mode switch কমানো: একবারে বড় ব্লক পড়া,
readv/writev, epoll-এ এক ঘুমে অনেক ঘটনা, io_uring-এ এক system call-এ অনেক অনুরোধ (বা SQPOLL-এ কোনো system call-ই নয়)। - কপি কমানো — zero-copy: একটি ওয়েব সার্ভার ফাইল পাঠাতে সাধারণত
read()(page cache → user) আরwrite()(user → socket buffer) করে — দুটি CPU-কপি, দুটি system call. sendfile() (Linux ২.২ থেকে) kernel-কে বলে সরাসরি page cache থেকে socket-এ পাঠাতে; NIC scatter-gather DMA পারলে ডেটা CPU দিয়ে একবারও কপি হয় না। - Interrupt কমানো: coalescing (অংশ ৩), NAPI, আর খুব দ্রুত ডিভাইসে polling (অংশ ২)।
আরও গভীরে: Linux-এ প্রতিটি IRQ কোন core-এ যাবে তা /proc/irq/N/smp_affinity_list-এ লেখা যায়, আর irqbalance daemon চাপ দেখে এটি বদলায়; managed IRQ-তে (যেমন NVMe-র per-CPU queue) kernel নিজেই বণ্টন ঠিক করে। নেটওয়ার্কে RSS হার্ডওয়্যারে প্যাকেটের hash দেখে queue বাছে, আর RPS/RFS একই কাজ software-এ করে, যাতে একটি connection-এর প্যাকেট সেই core-এ যায় যেখানে অ্যাপের thread চলছে। io_uring-এ buffer আর fd আগে থেকে “register” করলে প্রতিটি অনুরোধে kernel-কে page pin বা fd খোঁজার কাজ আবার করতে হয় না।
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Polling আর interrupt-এর পার্থক্য, কোনটি কখন? — polling-এ CPU বারবার status পড়ে, interrupt-এ ডিভাইস ডাকে; ঘটনা বিরল হলে interrupt, খুব ঘন বা ডিভাইস খুব দ্রুত হলে polling (বা NAPI-র মতো মিশ্রণ)।
- DMA কেন, আর CPU-র ভূমিকা কী থাকে? — বড় ব্লক সরাতে CPU-কে প্রতিটি শব্দ কপি করতে হয় না; CPU শুধু descriptor সাজায় আর শেষে interrupt সামলায়।
- Top half আর bottom half কেন আলাদা? — interrupt context-এ ঘুমানো যায় না আর অন্য interrupt আটকে থাকে, তাই জরুরি অংশ ছোট রেখে বাকিটা softirq/workqueue-তে।
- Block আর character device-এর পার্থক্য? — block-এ নির্দিষ্ট আকারের ব্লকে random access আর ওপরে file system; character-এ বাইটের স্রোত; network device-এর interface socket.
- Double buffering-এ প্রতি ব্লকের সময়? — max(C, T); single-এ max(C, T) + M; buffer ছাড়া T + C.
১০ · AI-এর যুগে I/O-র কোড আর পরামর্শ — যাচাই
“epoll দিয়ে একটি TCP সার্ভার লিখে দাও” — AI কয়েক সেকেন্ডে কোড দেবে, আর তা প্রথম পরীক্ষায় প্রায়ই চলেও। সমস্যা দেখা দেয় চাপের মধ্যে, অন্য kernel-এ, বা নিরাপত্তায়। I/O-র ক্ষেত্রে AI-এর সাধারণ ভুল:
| AI-এর ভুল | যা ঘটে | কীভাবে ধরবেন |
|---|---|---|
Non-blocking fd-তে EAGAIN পেলে সাথে সাথে আবার read-এর লুপ | একটি core ১০০% — busy waiting | top-এ CPU দেখুন; poll/epoll-এ অপেক্ষা করুন |
| Edge-triggered epoll, কিন্তু একবার read করেই থামা | চাপের সময় connection চুপচাপ আটকে যায় | বড় ডেটা আর অনেক client দিয়ে load test; EAGAIN পর্যন্ত পড়ার নিয়ম মিলিয়ে নিন |
EINTR বা আংশিক read/write (short count) না সামলানো | signal বা বড় ডেটায় হঠাৎ ডেটা হারানো | ফেরত মান আর errno-র প্রতিটি পথ পরীক্ষা; man 2 read |
পুরোনো kernel API-র driver কোড (যেমন সরানো .ioctl ক্ষেত্র, বদলে যাওয়া class_create-এর আর্গুমেন্ট) | compile হয় না বা ভুল আচরণ | uname -r-এর সংস্করণের kernel docs আর header মিলিয়ে নিন |
ডিভাইস খুলতে chmod 777 /dev/… বা সব কিছু sudo-তে | যেকোনো ব্যবহারকারী ডিভাইস নিয়ন্ত্রণ করতে পারে; রিবুটে হারায়ও | সঠিক group (যেমন dialout) বা নির্দিষ্ট udev rule |
“গতির জন্য” iommu=off, interrupt coalescing সর্বোচ্চ, বা Windows-এর পরামর্শ Linux-এ | DMA-সুরক্ষা হারানো, latency বেড়ে যাওয়া, ভুল OS-এর কমান্ড | আগে-পরে মাপুন; OS আর distro স্পষ্ট বলে প্রশ্ন করুন |
| Buffering বা interrupt-খরচের হিসাবে ভুল সূত্র | ভুল উত্তর আত্মবিশ্বাসের সাথে | অংশ ৭-এর মতো হাতে বা ছোট simulator-এ মিলিয়ে নিন |
AI-কে I/O-সংক্রান্ত কোড চাওয়ার সময় পরিবেশ, সীমা আর যাচাইয়ের দাবি দিন:
AI-এর দেওয়া I/O কোড বা কমান্ড ব্যবহারের আগে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১কম্পিউটারের ইতিহাসে interrupt এসেছিল polling-এর অপচয় দূর করতে। তাহলে আজকের সবচেয়ে দ্রুত নেটওয়ার্ক আর স্টোরেজ সিস্টেম আবার polling-এ ফিরছে কেন?
১৯৫০-এর দশকের যন্ত্রে CPU ছিল সবচেয়ে দামি সম্পদ, আর টেলিটাইপ বা টেপ ড্রাইভ ছিল তার তুলনায় অসম্ভব ধীর। প্রতিটি অক্ষরের জন্য CPU-কে লুপে দাঁড় করিয়ে রাখা মানে লাখ লাখ cycle নষ্ট। Interrupt এই সমস্যার মার্জিত সমাধান: ডিভাইস প্রস্তুত হলে নিজেই ডাকবে, CPU ততক্ষণ অন্য প্রোগ্রাম চালাবে। Multiprogramming আর time-sharing-এর পুরো ধারণা (পর্ব ০২) এর ওপর দাঁড়িয়ে।
কিন্তু interrupt-এর হিসাব একটি অনুমানের ওপর নির্ভর করে: ঘটনা বিরল, আর ঘটনার মাঝের সময় interrupt-এর খরচের চেয়ে অনেক বড়। ১৯৯০-এর দশকে নেটওয়ার্কের গতি বাড়তেই এই অনুমান ভাঙে। ১৯৯৬-৯৭ সালে Mogul আর Ramakrishnan দেখান, প্রতি প্যাকেটে interrupt দিলে একটি নির্দিষ্ট চাপের পর রাউটারের কার্যকর throughput কমতে কমতে শূন্যের কাছে যায় — receive livelock. তাঁদের প্রস্তাব, চাপের সময় polling আর কাজের সীমা, পরে Linux-এ NAPI হয়ে আসে।
আজ ছবিটা আরও চরম। ১০০ Gb/s কার্ডে ছোট প্যাকেট এলে প্রতি প্যাকেটের জন্য সময় থাকে মাত্র কয়েক ন্যানোসেকেন্ড — একটি interrupt-এর খরচেরও অনেক কম। তাই Intel-এর উদ্যোগে শুরু হওয়া DPDK আর স্টোরেজের SPDK পুরো driver user space-এ চালিয়ে কয়েকটি core শুধু polling-এ রাখে। NVMe SSD-তেও একটি পড়া কয়েক মাইক্রোসেকেন্ডে শেষ হতে পারে, তাই Linux-এ polled I/O আর io_uring-এর IOPOLL এসেছে।
দাম আছে: polling core সারাক্ষণ ১০০% চলে, বিদ্যুৎ খায়, তাপ বাড়ায়, আর চাপ কম থাকলে পুরোটাই অপচয়। তাই বাস্তব সমাধান প্রায় সবসময় অভিযোজনশীল — চাপ কম হলে interrupt, বেশি হলে polling, কখনো অল্প সময় poll করে তারপর ঘুম।
মূল উপলব্ধি: polling বনাম interrupt কোনো চিরস্থায়ী সঠিক উত্তর নয়, বরং ঘটনার হার আর প্রতি-ঘটনার খরচের অনুপাতের প্রশ্ন। হার্ডওয়্যারের গতি বদলালে সেরা নকশাও বদলায়।
প্র ০২OS-এর crash-এর বড় অংশের জন্য বহু বছর ধরে device driver দায়ী বলে গবেষণায় দেখা গেছে। কেন driver এত দুর্বল জায়গা, আর OS-জগৎ এর বিরুদ্ধে কী কী পথে লড়েছে?
Driver-এর চারটি বৈশিষ্ট্য একসাথে বিপজ্জনক। এক, সংখ্যায় বিশাল — Linux-এর সোর্সের অর্ধেকের বেশি driver. দুই, লেখেন অসংখ্য নির্মাতা, ভিন্ন দক্ষতা আর সময়ের চাপে। তিন, হার্ডওয়্যারের সব অবস্থা (হঠাৎ খুলে ফেলা, firmware-এর বাগ, বিরল ত্রুটি) পরীক্ষা করা কঠিন। চার, প্রচলিত monolithic kernel-এ driver kernel mode-এ চলে — একটি ভুল pointer গোটা যন্ত্র ফেলে দেয়।
২০০১ সালে Stanford-এর Andy Chou, Dawson Engler ও সহকর্মীরা স্বয়ংক্রিয় বিশ্লেষণে Linux-এর কোড পরীক্ষা করে দেখান, driver-এ ত্রুটির হার kernel-এর বাকি অংশের চেয়ে কয়েক গুণ বেশি। ২০০৩ সালে University of Washington-এর Nooks প্রকল্প (Michael Swift ও সহকর্মী) Microsoft-এর তখনকার তথ্য উদ্ধৃত করে লেখে, Windows XP-এর সাম্প্রতিক crash-এর বিশাল অংশের উৎস driver. Microsoft-এর প্রতিক্রিয়া ছিল বহুমুখী: SLAM গবেষণা থেকে Static Driver Verifier, চলার সময় যাচাইয়ের Driver Verifier, driver signing আর পরীক্ষার কর্মসূচি, আর UMDF-এ অনেক driver user space-এ সরানো।
Microkernel-এর পথ আরও মৌলিক: MINIX 3 প্রতিটি driver আলাদা user process-এ চালায়, আর একটি “reincarnation server” মরে যাওয়া driver আবার চালু করে — যন্ত্র চলতেই থাকে (পর্ব ০৬)। Linux-এ সাম্প্রতিক পথ হলো নিরাপদ ভাষা: ৬.১ থেকে Rust-এ driver লেখার প্রাথমিক সমর্থন, যাতে memory-safety-র একটি বড় শ্রেণির বাগ compile-এর সময়েই আটকে যায়।
২০২৪ সালের জুলাইয়ে CrowdStrike-এর একটি নিরাপত্তা-সফটওয়্যারের kernel-mode অংশে একটি ত্রুটিপূর্ণ হালনাগাদ বিশ্বজুড়ে লাখ লাখ Windows যন্ত্র (Microsoft-এর হিসাবে প্রায় ৮৫ লাখ) একসাথে crash করায় — বিমানবন্দর, ব্যাংক আর হাসপাতাল ক্ষতিগ্রস্ত হয়। এটি মনে করিয়ে দেয়, kernel mode-এ চলা যেকোনো কোড — driver হোক বা নিরাপত্তা-সফটওয়্যার — পুরো যন্ত্রের ভাগ্য বহন করে, আর হালনাগাদ ধাপে ধাপে ছাড়া জরুরি।
মূল উপলব্ধি: নির্ভরযোগ্যতা আসে সবচেয়ে দুর্বল অংশ থেকে, আর kernel-এ সেটি প্রায়ই driver. প্রতিরক্ষা একটি নয়, স্তরে স্তরে: যাচাই টুল, বিচ্ছিন্নকরণ, নিরাপদ ভাষা আর সাবধানী হালনাগাদ।
প্র ০৩select থেকে poll, তারপর epoll আর kqueue, এখন io_uring — একই কাজের জন্য Unix-জগৎ বারবার নতুন interface বানাল কেন? আর io_uring নিয়ে নিরাপত্তার বিতর্কটা কী?
select() এসেছিল 4.2BSD-তে (১৯৮৩), যখন একটি সার্ভারে কয়েক ডজন connection-ই অনেক। প্রতিবার ডাকার সময় অ্যাপ fd-এর একটি bitmap দেয়, kernel সবগুলো পরীক্ষা করে, ফেরার পর অ্যাপ আবার সবগুলো ঘুরে দেখে কোনটি প্রস্তুত। আর glibc-এ সাধারণ সীমা FD_SETSIZE = ১০২৪. poll() bitmap-এর বদলে struct-এর অ্যারে দিয়ে সীমা তুলে দিল, কিন্তু প্রতি ডাকে O(n) খরচ রয়ে গেল।
১৯৯৯ সালে Dan Kegel “C10K problem” নামে একটি লেখায় প্রশ্ন তোলেন: একটি সার্ভার একসাথে দশ হাজার client সামলাবে কীভাবে? দশ হাজার fd-এর মধ্যে হয়তো দশটি প্রস্তুত, অথচ প্রতিবার দশ হাজারই পরীক্ষা। সমাধান এলো আগ্রহের তালিকা kernel-এ স্থায়ীভাবে রাখায়: Jonathan Lemon-এর kqueue (FreeBSD, ২০০০) আর Davide Libenzi-র epoll (Linux, ২০০২-এর আশেপাশে)। এখন kernel নিজেই প্রস্তুত fd-এর তালিকা বানায়, খরচ প্রায় প্রস্তুত fd-এর সংখ্যার অনুপাতে। nginx-এর সাফল্যের পেছনে এই মডেল।
কিন্তু epoll শুধু বলে “এখন পড়া যাবে”; সাধারণ ফাইলে এটি কাজই করে না, আর প্রতিটি পড়ার জন্য আলাদা system call লাগে। Windows NT শুরু থেকেই completion মডেল (IOCP) নিয়েছিল। Linux-এর পুরোনো AIO শুধু O_DIRECT-এ ভালো চলত। ২০১৯ সালে Jens Axboe-র io_uring দুটি shared ring দিয়ে প্রায় সব ধরনের অনুরোধ asynchronous করে, system call-এর সংখ্যা নাটকীয়ভাবে কমায়।
বিপরীত দিকও আছে। io_uring kernel-এ একটি বিশাল নতুন আক্রমণের পৃষ্ঠ খুলেছে, আর কয়েক বছরে এতে বেশ কিছু গুরুতর নিরাপত্তা-ত্রুটি ধরা পড়ে। ২০২৩ সালে Google জানায়, তাদের kernel bug bounty-তে জমা পড়া exploit-এর বড় অংশ io_uring-কে লক্ষ্য করেছিল, তাই তারা ChromeOS-এ, Android-এর সাধারণ অ্যাপের জন্য আর নিজেদের production সার্ভারে এটি বন্ধ বা সীমিত করে। Linux-এ এখন sysctl দিয়ে io_uring বন্ধ রাখা যায়, আর কিছু container runtime-এর ডিফল্ট seccomp নীতিও এটি আটকায়।
মূল উপলব্ধি: interface-এর বিবর্তন চলে workload-এর মাপ বদলানোর সাথে। কিন্তু প্রতিটি নতুন দ্রুত পথ নতুন ঝুঁকিও আনে — গতির সাথে আক্রমণের পৃষ্ঠও মাপতে হয়।
প্র ০৪DMA একটি ডিভাইসকে CPU-কে না জিজ্ঞেস করেই RAM-এ লিখতে দেয়। বাইরের পোর্ট (FireWire, Thunderbolt) দিয়ে এ থেকে কী ঝুঁকি তৈরি হয়েছিল, আর আজকের OS কীভাবে নিজেকে রক্ষা করে?
সমস্যাটি নকশাগত। DMA-র উদ্দেশ্যই CPU-কে পাশ কাটানো, আর PCIe-র মতো bus-এ একটি bus-master ডিভাইস নীতিগতভাবে যেকোনো physical ঠিকানায় লিখতে পারে। ডিভাইস যখন মাদারবোর্ডের ভেতরে বসানো, তখন ঝুঁকি কম মনে হতো। কিন্তু FireWire (২০০০-এর দশক) আর পরে Thunderbolt-এর মতো বাইরের পোর্ট PCIe বা তার সমতুল্য প্রবেশাধিকার বাইরে এনে দিল — একটি ছোট তার লাগানো ডিভাইসই তখন “ভেতরের” ডিভাইসের মতো আচরণ করতে পারে।
নিরাপত্তা-গবেষকেরা ২০০০-এর দশকেই প্রকাশ্যে দেখান, এমন পোর্ট দিয়ে লক করা কম্পিউটারের মেমরি পড়া সম্ভব। ২০১৯ সালে Cambridge-এর গবেষকদের “Thunderclap” গবেষণা দেখায়, IOMMU চালু থাকলেও অনেক OS-এর কনফিগারেশনে ফাঁক ছিল — যেমন একই IOMMU page-এ ডিভাইসের buffer আর অন্য সংবেদনশীল kernel ডেটা পাশাপাশি থাকা। ২০২০ সালে “Thunderspy” Thunderbolt controller-এর firmware-স্তরের দুর্বলতা নিয়ে আলোচনা আনে।
প্রতিরক্ষা এখন কয়েক স্তরে। প্রথমত IOMMU: Windows ১০-এর ১৮০৩ সংস্করণ থেকে “Kernel DMA Protection” বাইরের PCIe ডিভাইসকে IOMMU-র বিচ্ছিন্ন এলাকায় রাখে, আর ব্যবহারকারী লগইন না করা পর্যন্ত নতুন ডিভাইসকে DMA করতে দেয় না; macOS আর আধুনিক Linux-ও Thunderbolt ডিভাইসে IOMMU ব্যবহার করে। দ্বিতীয়ত firmware: বুটের সময়ের DMA সুরক্ষা আর Thunderbolt-এর security level (ব্যবহারকারীর অনুমোদন ছাড়া ডিভাইস চালু না করা)। তৃতীয়ত driver-স্তর: buffer আলাদা page-এ রাখা, map-এর আকার যতটুকু দরকার ততটুকু, আর strict IOTLB invalidation.
ব্যবহারকারীর দিক থেকে যাচাইয়ের কাজ সরল: firmware-এ VT-d/AMD-Vi চালু, Windows-এর msinfo32-এ Kernel DMA Protection On, Linux-এ dmesg-এ IOMMU সক্রিয়, আর অচেনা USB-C/Thunderbolt ডিভাইস না লাগানো।
মূল উপলব্ধি: কর্মদক্ষতার জন্য দেওয়া প্রতিটি সরাসরি পথ (DMA) একটি বিশ্বাসের সীমা ভাঙে। IOMMU সেই সীমা হার্ডওয়্যারেই আবার টেনে দেয় — “ডিভাইসও ব্যবহারকারীর মতো, শুধু তার জন্য বরাদ্দ মেমরিটুকুই।”
প্র ০৫মেমরি-কপি তো খুব দ্রুত — তাহলে Netflix বা Kafka-র মতো সিস্টেম zero-copy নিয়ে এত মাথা ঘামায় কেন? আর zero-copy কখন কোনো লাভই দেয় না?
একটি কপি একা দ্রুত, কিন্তু পরিমাণ যখন সেকেন্ডে দশ বা শত গিগাবাইট, তখন হিসাব বদলায়। প্রতিটি বাইট কপি মানে মেমরি থেকে পড়া আর মেমরিতে লেখা — মেমরি bandwidth দ্বিগুণ খরচ, CPU-র cache ভরে যাওয়া (অন্য দরকারি ডেটা বেরিয়ে যায়), আর প্রতিটি read/write জোড়ায় দুটি system call. একটি ফাইল-সার্ভারের সাধারণ পথে ডেটা page cache থেকে user buffer, তারপর socket buffer — দুটি CPU-কপি, অথচ অ্যাপ ডেটাটি একবারও দেখে না!
Linux ২.২ (১৯৯৯)-এ আসা sendfile() এই অপচয় সরায়: kernel page cache-এর page সরাসরি নেটওয়ার্ক স্তরে দেয়, আর NIC scatter-gather DMA পারলে সেখান থেকে সোজা তারে। Apache Kafka-র নকশা-নথিতে স্পষ্ট বলা আছে, consumer-কে ডেটা পাঠাতে তারা sendfile-এর পথ নেয়, কারণ log-এর ডেটা ডিস্ক থেকে নেটওয়ার্কে যায় অপরিবর্তিত। Netflix তাদের Open Connect সার্ভারে FreeBSD-র sendfile উন্নত করে, এবং পরে kernel TLS যোগ করে (এনক্রিপশন kernel-এ বা NIC-এ, যাতে sendfile-এর পথ না ভাঙে), একটি সার্ভার থেকে শত শত Gb/s ভিডিও পাঠানোর কথা প্রকাশ্যে জানিয়েছে।
কিন্তু zero-copy সব জায়গায় লাভ দেয় না। ছোট বার্তায় কপির খরচ নগণ্য, আর zero-copy-র হিসাব (page pin করা, শেষ হলে notification, IOMMU map) কপির চেয়েই বেশি হতে পারে — Linux-এর MSG_ZEROCOPY-এর ডকুমেন্টেশনই বলে, এটি সাধারণত শুধু বড় (প্রায় ১০ KB-র বেশি) লেখায় কার্যকর। আর অ্যাপকে যদি ডেটা বদলাতে হয় (compression, এনক্রিপশন user space-এ, HTML বানানো), তাহলে ডেটা user space-এ আনতেই হবে। কেউ আবার আরও এগিয়ে kernel পুরোপুরি পাশ কাটায় — DPDK বা RDMA — কিন্তু তখন kernel-এর সুরক্ষা আর সুবিধাও হারায়।
মূল উপলব্ধি: পারফরম্যান্সের কাজ শুরু হয় “ডেটা কতবার, কোথায় কোথায় সরছে” প্রশ্ন দিয়ে। যে কপির ফল কেউ দেখে না, সেটিই প্রথম বাদ দেওয়ার প্রার্থী — তবে মেপে নিয়ে।
অনুশীলন
প্রথম দুটি কাগজ-কলমে হিসাব; তৃতীয়টির আউটপুট আগে অনুমান করুন; চতুর্থটিতে কমান্ড লিখুন।
-
Buffering-এর হিসাব (numerical): একটি প্রোগ্রাম ৮টি ব্লক পড়ে। প্রতিটি ব্লক ডিভাইস থেকে আসতে T = ৪ ms, প্রতিটিতে হিসাব C = ৬ ms, system buffer থেকে user-এ কপি M = ১ ms. (ক) buffer ছাড়া, single buffer আর double buffer-এ মোট সময় কত? (খ) স্থিত অবস্থায় প্রতিটির throughput (ব্লক/সেকেন্ড)? (গ) এখানে কোনটি bottleneck, আর ডিভাইস দ্বিগুণ দ্রুত হলে double-এর সময় কতটা কমবে?
(ক) buffer ছাড়া: ৮ × (৪ + ৬) = ৮০ ms. Single: T + M + C + (n − ১)(max(C, T) + M) = ৪ + ১ + ৬ + ৭ × (৬ + ১) = ১১ + ৪৯ = ৬০ ms. Double: T + C + (n − ১)·max(C, T) = ৪ + ৬ + ৭ × ৬ = ৫২ ms.
(খ) প্রতি ব্লক: ১০, ৭ আর ৬ ms — সেকেন্ডে ১০০, প্রায় ১৪৩ আর প্রায় ১৬৭টি ব্লক।
(গ) C = ৬ > T = ৪, তাই হিসাবই (CPU) bottleneck. ডিভাইস দ্বিগুণ দ্রুত হলে (T = ২) double = ২ + ৬ + ৭ × ৬ = ৫০ ms — মাত্র ২ ms কম, শুধু শুরুর অংশে। গতি বাড়াতে হলে C কমাতে হবে। (simulator-এ
[8, 4, 6, 1]আর[8, 2, 6, 1]দিয়ে মিলিয়ে নিন।) -
Interrupt বনাম polling (numerical): একটি নেটওয়ার্ক কার্ড সেকেন্ডে ৫০,০০০টি প্যাকেট দেয়। প্রতিটি interrupt সামলাতে মোট ৬ µs লাগে। (ক) প্রতি প্যাকেটে interrupt দিলে একটি core-এর কত শতাংশ যায়? (খ) কোন হারে interrupt একাই একটি core পুরো খেয়ে ফেলবে? (গ) কার্ডটি ৩২টি প্যাকেট জমিয়ে একটি interrupt দিলে (coalescing) (ক)-এর উত্তর কত, আর এর দাম কী?
(ক) ৫০,০০০ × ৬ µs = ৩,০০,০০০ µs = ০.৩ সেকেন্ড প্রতি সেকেন্ডে — একটি core-এর ৩০%.
(খ) ১ সেকেন্ড ÷ ৬ µs ≈ ১,৬৬,৬৬৭টি প্যাকেট/সেকেন্ড. এর কাছাকাছি গেলে বাকি কাজ সময় পায় না — receive livelock-এর দিকে।
(গ) interrupt ৫০,০০০ ÷ ৩২ ≈ ১৫৬২টি, খরচ ≈ ১৫৬২ × ৬ µs ≈ ৯.৪ ms, অর্থাৎ প্রায় ০.৯৪% (প্রতিটি প্যাকেটের প্রক্রিয়ার নিজস্ব খরচ আলাদা থাকে)। দাম: প্রথম প্যাকেটটিকে বাকি ৩১টির জন্য অপেক্ষা করতে হতে পারে, তাই latency বাড়ে — এজন্য বাস্তবে একটি সময়সীমাও (rx-usecs) থাকে।
-
আউটপুট অনুমান করুন: Linux-এ একটি প্রোগ্রাম
pipe(fd)করে, পড়ার মুখেO_NONBLOCKদেয়, তারপর ক্রমে: (১)write(fd[1], "abc", 3); (২)n = read(fd[0], buf, 10); (৩) আবারn = read(fd[0], buf, 10); (৪)close(fd[1]); (৫) আবারn = read(fd[0], buf, 10). প্রতিটি read-এ n আর errno কী হবে? পড়ার মুখ blocking হলে (৩) নম্বরে কী ঘটত?(২) n = ৩ — যা আছে তা-ই ফেরত, ১০ চাওয়া হলেও (short read স্বাভাবিক)। (৩) pipe খালি, কিন্তু লেখার মুখ খোলা — তাই n = −১, errno = EAGAIN. (৫) লেখার মুখ আর কেউ ধরে নেই — n = ০, অর্থাৎ EOF; EAGAIN নয়।
Blocking হলে (৩) নম্বরে read চিরকাল ঘুমাত — কারণ লেখার মুখ খোলা, আর সেটি এই process-এরই হাতে, যে নিজেই ঘুমাচ্ছে। পর্ব ১২-এর pipe-এর ক্লাসিক আটকে যাওয়া।
-
কমান্ড লিখুন: নিজের Linux যন্ত্রে (ক)
/dev/sdaবা/dev/nvme0n1কোন ধরনের ডিভাইস আর এর major:minor কত, তা দেখার কমান্ড; (খ) এক সেকেন্ডে কোনো একটি IRQ (যেমনnvme0q1) কতবার এসেছে, তা হিসাবের কমান্ড; (গ) একটি PCIe ডিভাইস কোন driver ব্যবহার করছে, তা দেখার কমান্ড লিখুন।(ক)
ls -l /dev/nvme0n1— প্রথম অক্ষরb(block), আর মাপের জায়গায় যেমন259, 0. অথবাstat -c '%F %t:%T' /dev/nvme0n1(hex) বাlsblk -o NAME,MAJ:MIN,TYPE.(খ) একটি সমাধান — সব core-এর কলাম যোগ করে দুবার মাপা:
cnt() { awk '/nvme0q1/ {s=0; for(i=2;i<=NF;i++) if($i~/^[0-9]+$/) s+=$i; print s}' /proc/interrupts; } a=$(cnt); sleep 1; b=$(cnt); echo "nvme0q1: $((b-a)) interrupts/s"(গ)
lspci -k— প্রতিটি ডিভাইসের নিচে “Kernel driver in use”. অথবাreadlink /sys/bus/pci/devices/0000:01:00.0/driver. নেটওয়ার্কের জন্যethtool -i eth0.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি process এমন ফাইলের অংশ read() করে, যা page cache-এ নেই। সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📞 system call দিয়ে kernel mode-এ প্রবেশ, VFS-এ page cache miss
- 🧱 block layer একটি bio বানিয়ে queue-তে রাখে
- 🔔 driver command সাজিয়ে doorbell-এ লেখে, process ঘুমায়
- 💾 controller DMA দিয়ে ডেটা RAM-এর page-এ লেখে
- ⚡ completion interrupt — handler process-কে জাগায়
- 📋 kernel ডেটা user buffer-এ কপি করে read ফেরত দেয়
Polling-ভিত্তিক handshake-এ CPU একটি আদেশ সাজানো শেষ হয়েছে বলে controller-কে কীভাবে জানায়?
- status register-এর busy বিট ১ করে
- command register-এর command-ready বিট ১ করে
- controller-কে একটি interrupt পাঠায়
- error বিট ০ করে দেয়
পুরোনো pin-ভিত্তিক IRQ লাইনের তুলনায় MSI-X-এর প্রধান সুবিধা কোনটি?
- এতে CPU-কে আর কখনো poll করতে হয় না
- এর interrupt কখনো mask করা যায় না
- ডিভাইস মেমরি-লেখার মাধ্যমে অনেক আলাদা vector পাঠাতে পারে, প্রতিটি পছন্দের core-এ
- এতে DMA-র আর দরকার হয় না
একটি Linux driver-এর interrupt-পরবর্তী কাজে mutex নিতে হবে আর ধীর একটি I/O-র জন্য ঘুমাতে হতে পারে। কাজটি কোথায় করা উচিত?
- Top half (hard IRQ handler)-এর ভেতরেই
- একটি softirq-এ
- একটি tasklet-এ
- একটি workqueue বা threaded IRQ-এ
একটি প্রোগ্রাম ১০টি ব্লক পড়ে; প্রতিটি ব্লক আসতে T = ৫ ms, প্রতিটিতে হিসাব C = ৩ ms. Double buffering-এ (process একটি buffer-এ কাজ করে, ডিভাইস অন্যটি ভরে) মোট সময় কত?
- ৫৩ ms
- ৮০ ms
- ৫০ ms
- ৩০ ms
Linux-এ O_NONBLOCK দেওয়া একটি খালি pipe-এর পড়ার মুখে read() করলে, যখন লেখার মুখ এখনো খোলা, কী ফেরত আসে?
- ০ (EOF)
- −১, আর errno = EAGAIN
- ডেটা না আসা পর্যন্ত process ঘুমায়
- −১, আর errno = EBADF
একটি ল্যাপটপে IOMMU (Intel VT-d) চালু রাখার নিরাপত্তা-সুবিধা কোনটি?
- ডিস্কের সব ডেটা স্বয়ংক্রিয়ভাবে এনক্রিপ্ট হয়
- কোনো ডিভাইস আর DMA করতে পারে না
- প্রতিটি ডিভাইস শুধু তার জন্য map করা মেমরিতেই DMA করতে পারে
- একটি user process অন্য process-এর মেমরি পড়তে পারে না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Polling-এর handshake: CPU busy ০ হওয়া পর্যন্ত পড়ে → data ও write বিট সাজায় → command-ready ১ → controller busy ১, কাজ, তারপর command-ready, error, busy ০. ঘটনা খুব ঘন বা ডিভাইস খুব দ্রুত হলে polling interrupt-এর চেয়ে সস্তা (NAPI, io_uring polling).
- Interrupt controller (8259 PIC → APIC → MSI/MSI-X) সংকেত জড়ো করে, vector দেয়, priority ও mask মানে। x86-এ vector ০–৩১ exception, NMI mask হয় না; Windows-এ IRQL.
- Top half ছোট, ঘুমায় না; bottom half: softirq/tasklet (ঘুমায় না), workqueue/threaded IRQ (ঘুমাতে পারে), Windows-এ DPC. প্রতি ঘটনায় interrupt চাপে livelock আনে — coalescing আর NAPI তার ওষুধ।
- DMA: descriptor ring আর doorbell, scatter-gather, cycle stealing বনাম burst; IOMMU ডিভাইসকে নিজের map করা মেমরিতে আটকায়; non-coherent সিস্টেমে DMA API cache সামলায়।
- I/O subsystem: block / character / network device,
/devআর major:minor; blocking, non-blocking (EAGAIN), multiplexing (select/poll/epoll/kqueue) আর async (IOCP, io_uring); buffering — প্রতি ব্লক none T + C, single max(C, T) + M, double max(C, T); driver =file_operations-এর মতো মানক interface + হার্ডওয়্যারের ভাষা।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪০ · ডিস্ক আর SSD — গঠন আর disk schedulingপরবর্তী পর্বস্টোরেজ ডিভাইসের ভেতর আর অনুরোধ সাজানোর অ্যালগরিদম।
- অপারেটিং সিস্টেম · কোর্স হোম৬৪টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- পর্ব ০৪ · OS-এর চোখে হার্ডওয়্যারভিত্তিinterrupt, device register, port ও memory-mapped I/O আর DMA-র শুরুর ধারণা।
- C প্রোগ্রামিং · signal আর low-level I/Oহাতে-কলমেopen, read, write আর file descriptor দিয়ে C-তে সরাসরি I/O.
- DBMS · storage-এর ভেতরতুলনাডেটাবেস কীভাবে ডিস্কের ব্লক, buffer pool আর direct I/O ব্যবহার করে।