এই পর্বে যা শিখবেন
- von Neumann মডেল আর fetch–decode–execute চক্র — ধাপে ধাপে register-এর মান দেখে
- PC, SP, general register আর flags কেন context switch-এর কেন্দ্রে, আর privilege mode কীভাবে OS-কে রক্ষা করে
- Memory hierarchy-র আনুমানিক latency, locality, cache-এর ধারণা আর AMAT / effective access time-এর পুরো হিসাব
- Interrupt বনাম polling, interrupt vector table, ISR, timer interrupt, আর trap/exception-এর সাথে পার্থক্য
- Device controller, memory-mapped বনাম port I/O, DMA, multicore, SMP, hyper-threading আর cache coherence-এর প্রাথমিক ধারণা
১ · OS কেন হার্ডওয়্যার জানবে — von Neumann মডেল
পর্ব ০১-এ দেখেছেন, অপারেটিং সিস্টেম কম্পিউটারের ব্যবস্থাপক। কিন্তু ব্যবস্থাপক যদি না জানেন কারখানায় কোন যন্ত্র কী পারে, কোনটি ধীর, কোনটি দ্রুত — তিনি ভালো সিদ্ধান্ত নিতে পারবেন না। OS-এর প্রায় প্রতিটি নকশা আসলে হার্ডওয়্যারের কোনো একটি বৈশিষ্ট্যের উত্তর। Scheduler আছে কারণ CPU একটি সময়ে একটিই নির্দেশ চালায় (প্রতি core-এ)। Virtual memory আছে কারণ RAM সীমিত আর ডিস্ক ধীর। Device driver আছে কারণ প্রতিটি ডিভাইসের ভাষা আলাদা। তাই এই পর্বে আমরা হার্ডওয়্যারকে দেখব একজন OS ডিজাইনারের চোখে — সার্কিট নয়, শুধু সেটুকু যা OS-এর সিদ্ধান্তকে প্রভাবিত করে।
আজকের প্রায় সব সাধারণ কম্পিউটার একটি পুরোনো নকশা মেনে চলে। ১৯৪৫ সালে গণিতবিদ John von Neumann EDVAC নামের একটি কম্পিউটারের খসড়া প্রতিবেদনে (“First Draft of a Report on the EDVAC”) যে কাঠামো বর্ণনা করেন, তাকেই আমরা বলি von Neumann architecture. এর মূল কথা stored-program ধারণা: প্রোগ্রামের নির্দেশ আর ডেটা — দুটোই একই মেমরিতে, একই রকম সংখ্যা হিসেবে রাখা থাকে। নতুন প্রোগ্রাম চালাতে তার বদলানোর দরকার নেই, শুধু মেমরিতে নতুন সংখ্যা লিখলেই হয়। OS নিজেও এভাবেই একটি প্রোগ্রাম, যে অন্য প্রোগ্রামকে মেমরিতে এনে চালায়।
von Neumann মডেলের চারটি অংশ
CPU, memory, I/O and the bus that connects them
- CPU
ALU + Control + Registersহিসাব আর নির্দেশ চালানো - Bus
address · data · controlসবার মাঝের রাস্তা - Memory
নির্দেশ + ডেটাএকই জায়গায় দুটোই - I/O
disk · NIC · keyboardবাইরের দুনিয়া
একটি রেস্তোরাঁর রান্নাঘর ভাবুন। একজন বাবুর্চি (CPU) একটি চুলায় রাঁধেন। রেসিপির খাতা আর কাঁচামাল — দুটোই থাকে একই স্টোররুমে (memory). একজন সহকারী স্টোর থেকে জিনিস এনে দেন (bus), আর অর্ডার আসে, খাবার যায় সামনের কাউন্টার দিয়ে (I/O). বাবুর্চি যত দ্রুতই হন, সহকারী ধীর হলে রান্না আটকে থাকবে। এই সমস্যাটিরই নাম von Neumann bottleneck — CPU আর মেমরির মাঝের সরু রাস্তা। এই পর্বের অর্ধেক জুড়ে আমরা দেখব, হার্ডওয়্যার আর OS এই রাস্তার জট কমাতে কী কী কৌশল নেয়।
OS-এর তিনটি প্রধান সম্পদ এই মডেল থেকেই আসে: CPU-র সময় (কে কখন চালাবে — মডিউল ৩), মেমরির জায়গা (কে কোথায় থাকবে — মডিউল ৫), আর I/O ডিভাইস (কে কখন ডিস্ক বা নেটওয়ার্ক পাবে — মডিউল ৬)। হার্ডওয়্যার যা দেয়, OS তা ভাগ করে, রক্ষা করে আর সহজ করে তোলে।
২ · CPU-র ছন্দ — fetch, decode, execute আর register
CPU দেখতে যত জটিলই হোক, তার মূল কাজ একটি চক্র, যা সে সেকেন্ডে শতকোটি বার চালায়। একে বলে fetch–decode–execute cycle:
- Fetch (আনা): program counter (PC) নামের register-এ লেখা আছে পরের নির্দেশের ঠিকানা। CPU সেই ঠিকানা থেকে নির্দেশটি এনে instruction register-এ (IR) রাখে, আর PC-কে পরের নির্দেশের দিকে এগিয়ে দেয়।
- Decode (বোঝা): নির্দেশটি কী চায় — যোগ, মেমরি থেকে পড়া, নাকি অন্য ঠিকানায় লাফ (jump)?
- Execute (চালানো): ALU হিসাব করে, ফল register বা মেমরিতে যায়, দরকার হলে flags বদলায়। তারপর আবার fetch.
নিচের খেলনা-কম্পিউটারে মেমরির ১০০, ১০১, ১০২ ঠিকানায় তিনটি নির্দেশ আছে: ২০০ নম্বর ঘরের মান (7) R1-এ আনা, তার সাথে ২০১ নম্বর ঘরের মান (5) যোগ করা, আর ফলটি ২০২ নম্বর ঘরে রাখা। “পরে” চেপে প্রতিটি register-এর মান খেয়াল করুন:
100LOAD R1, [200]101ADD R1, [201]102STORE R1, [202]103...2007 ; data2015 ; data2020 ; result goes here
| PC | |
| IR | |
| R1 | |
| Z flag | |
| mem[202] |
- শুরু। PC-তে আছে 100 — অর্থাৎ পরের নির্দেশ ১০০ নম্বর ঠিকানায়। IR খালি, R1-এ 0, আর ২০২ নম্বর ঘরেও 0.
- Fetch, নির্দেশ ১। CPU ১০০ ঠিকানা থেকে
LOAD R1, [200]এনে IR-এ রাখল, আর PC বাড়িয়ে 101 করল। খেয়াল করুন, নির্দেশ চালানোর আগেই PC এগিয়ে গেছে। - Decode আর execute, নির্দেশ ১। নির্দেশটির মানে: ২০০ নম্বর ঘর পড়ে মানটি R1-এ রাখা। মেমরি থেকে 7 এলো — R1 এখন 7. PC আগের মতোই 101.
- Fetch, নির্দেশ ২। ১০১ থেকে
ADD R1, [201]IR-এ এলো, PC হলো 102. - Execute, নির্দেশ ২। ২০১ নম্বর ঘরের 5 যোগ হলো: R1 = 7 + 5 = 12. ফল শূন্য নয়, তাই Z (zero) flag হলো 0. পরের কোনো শর্তযুক্ত jump এই flag দেখেই সিদ্ধান্ত নেবে।
- Fetch, নির্দেশ ৩।
STORE R1, [202]IR-এ এলো, PC হলো 103. - Execute, নির্দেশ ৩। R1-এর 12 লেখা হলো ২০২ নম্বর ঘরে। তিনটি নির্দেশে মোট ছয়বার মেমরিতে যেতে হলো — তিনবার নির্দেশ আনতে, তিনবার ডেটার জন্য (LOAD আর ADD-এ একবার করে পড়া, STORE-এ একবার লেখা)। মেমরির এই ঘন ঘন ব্যবহারই অংশ ৪-এর cache-এর গল্পের কারণ।
আসল CPU এর চেয়ে অনেক চালাক। আধুনিক processor pipeline ব্যবহার করে — একটি নির্দেশ execute হওয়ার সময় পরেরটি decode আর তার পরেরটি fetch হয়, ঠিক গার্মেন্টসের সেলাই লাইনের মতো। অনেক CPU একাধিক নির্দেশ একসাথে, এমনকি ক্রম বদলে (out-of-order) চালায়। কিন্তু বাইরে থেকে ফল এমনভাবে দেখায় যেন নির্দেশগুলো একে একে চলেছে। OS ডিজাইনারের জন্য এই “যেন একে একে” চুক্তিটুকুই যথেষ্ট — অন্তত একটি core-এর ভেতরে। (একাধিক core-এ এই চুক্তি দুর্বল হয়ে যায়; সেই গল্প পর্ব ২৩-এ।)
Register — CPU-র পকেট খাতা
Register হলো CPU-র ভেতরের অল্প কয়েকটি ঘর, যেগুলো সবচেয়ে দ্রুত পড়া-লেখা যায়। x86-64-এ সাধারণ কাজের ১৬টি ৬৪-বিটের register আছে; ARM64-এ ৩১টি। OS-এর চোখে চার ধরনের register বিশেষ গুরুত্বপূর্ণ:
| Register | কী রাখে | x86-64 / ARM64-এ নাম | OS-এর কাছে কেন জরুরি |
|---|---|---|---|
| Program counter (PC) | পরের নির্দেশের ঠিকানা | RIP / PC | প্রসেস থামালে “কোথায় ছিল” — এটিই |
| Stack pointer (SP) | stack-এর মাথার ঠিকানা | RSP / SP | ফাংশন কল, লোকাল ভ্যারিয়েবল; প্রতিটি থ্রেডের নিজস্ব stack |
| General-purpose | হিসাবের মাঝের মান | RAX…R15 / X0…X30 | অর্ধেক হিসাব এখানেই থাকে — হারালে ভুল ফল |
| Flags / status | শেষ হিসাবের ফল-চিহ্ন, interrupt চালু কি না, mode | RFLAGS / PSTATE | শর্তযুক্ত jump-এর ভিত্তি; interrupt চালু-বন্ধের বিট |
কেন এত গুরুত্ব? কারণ একটি চলমান প্রোগ্রামের পুরো “এই মুহূর্ত” লুকিয়ে আছে এই register-গুলোতে। ধরুন OS প্রসেস A থামিয়ে প্রসেস B চালাতে চায়। A-র মেমরি যেখানে আছে সেখানেই থাকবে, কিন্তু register তো একটাই সেট — B চালালে সেগুলো B-র মানে ভরে যাবে। তাই OS প্রথমে A-র সব register তার নিজের খাতায় (process control block, পর্ব ০৮) লিখে রাখে, তারপর B-র আগের মানগুলো register-এ ফিরিয়ে আনে। এই কাজের নাম context switch, যার বিস্তারিত পর্ব ০৯-এ। একটি register ভুল ফেরালেই — বিশেষ করে PC বা SP — প্রোগ্রাম ভুল জায়গায় লাফ দেবে বা crash করবে।
ব্যাংকের একজন ক্যাশিয়ার একজন গ্রাহকের টাকা গুনছেন, এমন সময় ম্যানেজার জরুরি কাজে ডাকলেন। ভালো ক্যাশিয়ার উঠে যাওয়ার আগে একটি চিরকুটে লিখে রাখেন: “রহিম সাহেব, ৩৭ নম্বর নোট পর্যন্ত গোনা, মোট ১৮,৫০০ টাকা।” ফিরে এসে চিরকুট দেখে ঠিক সেখান থেকে শুরু করেন। চিরকুটটিই register-এর সংরক্ষিত কপি; “৩৭ নম্বর নোট” হলো PC.
৩ · CPU-র দুই চেহারা — privilege mode আর protection ring
একটি প্রশ্ন ভাবুন: আপনার ফোনে একটি গেম চলছে। গেমটি যদি চায়, সে কি সরাসরি ডিস্কের সব ফাইল মুছে দিতে পারে? বা অন্য অ্যাপের মেমরি পড়ে নিতে পারে? পারে না — আর সেটি কোনো সফটওয়্যারের ভদ্রতার জন্য নয়, হার্ডওয়্যারের একটি নিয়মের জন্য।
আধুনিক CPU অন্তত দুটি অবস্থায় চলতে পারে। User mode-এ সাধারণ প্রোগ্রাম চলে; সেখানে কিছু নির্দেশ নিষিদ্ধ। Kernel mode-এ (supervisor mode-ও বলে) OS-এর kernel চলে, আর সব নির্দেশ ব্যবহার করা যায়। যেসব নির্দেশ শুধু kernel mode-এ চলে, তাদের বলে privileged instruction — যেমন CPU থামানো (x86-এ HLT), interrupt table-এর ঠিকানা বসানো (LIDT), page table বদলানো (CR3 register-এ লেখা), timer সেট করা, অনেক ক্ষেত্রে সরাসরি I/O port-এ লেখা। User mode-এ এমন নির্দেশ চালাতে গেলে CPU নিজেই থেমে যায় আর OS-কে ডাকে (একটি exception — অংশ ৭)। OS সাধারণত তখন প্রোগ্রামটি বন্ধ করে দেয়।
👤 User mode
অ্যাপ, ব্রাউজার, গেম, আপনার C প্রোগ্রাম। শুধু নিজের মেমরি দেখে, ডিভাইস সরাসরি ছুঁতে পারে না। কিছু দরকার হলে OS-কে অনুরোধ করে — system call (পর্ব ০৫)।
x86: ring 3 · ARM: EL0🛡️ Kernel mode
OS-এর kernel. সব মেমরি, সব ডিভাইস, সব নির্দেশ। এখানে একটি বাগ পুরো সিস্টেম ফেলে দিতে পারে — Windows-এর নীল পর্দা বা Linux-এর “kernel panic”.
x86: ring 0 · ARM: EL1Mode কোথায় লেখা থাকে? একটি register-এর কয়েকটি বিটে। x86-এ এটি current privilege level (CPL), যা ০ থেকে ৩ — চারটি protection ring. Intel ১৯৮২ সালের 80286 প্রসেসরে protected mode-এর সাথে এই চার রিং আনে। মজার ব্যাপার, Linux আর Windows — দুটোই কার্যত শুধু ring 0 আর ring 3 ব্যবহার করে; মাঝের দুটি প্রায় অব্যবহৃত (কেন, তা ভাবনার প্রশ্ন ৪-এ)। ARM-এ নাম exception level: EL0 অ্যাপ, EL1 kernel, EL2 hypervisor, EL3 secure firmware. RISC-V-এ U, S আর M mode.
User mode থেকে kernel mode-এ যাওয়ার দরজা মাত্র কয়েকটি, আর সবগুলো হার্ডওয়্যার-নিয়ন্ত্রিত: interrupt, exception আর system call-এর বিশেষ নির্দেশ (x86-64-এ syscall, ARM-এ svc). প্রতিবার CPU নিজেই mode বদলায় আর OS-এর আগে থেকে ঠিক করে রাখা ঠিকানায় লাফ দেয় — প্রোগ্রাম নিজে ঠিকানা বেছে নিতে পারে না। বাড়ির দারোয়ানের মতো: ভেতরে ঢোকার গেট একটিই, আর কে কোন ঘরে যাবে তা দারোয়ান ঠিক করেন, অতিথি নন।
Windows, Linux, macOS — তিনটিই এই দুই-mode নকশা মেনে চলে, যদিও kernel-এর গঠন আলাদা (পর্ব ০৬)। পুরোনো কিছু সিস্টেম, যেমন MS-DOS, এমন সুরক্ষা ব্যবহার করত না: যেকোনো প্রোগ্রাম মেমরির যেকোনো জায়গায় লিখতে পারত, আর একটি বাগে পুরো কম্পিউটার আটকে যেত। Privilege ring-এর নিরাপত্তা-দিক বিস্তারিত আসবে পর্ব ৪৮-এ।
৪ · Memory hierarchy — দ্রুত-ছোট-দামি থেকে ধীর-বড়-সস্তা
প্রকৌশলীরা চান এমন মেমরি, যা একসাথে অনেক বড়, খুব দ্রুত আর সস্তা। দুঃখের বিষয়, এমন কিছু নেই। দ্রুত মেমরি (যেমন SRAM, যা দিয়ে cache তৈরি) প্রতি বাইটে দামি আর চিপে বেশি জায়গা নেয়; বড় আর সস্তা মেমরি (ডিস্ক) ধীর। তাই কম্পিউটার কয়েক স্তরের একটি সিঁড়ি বানায় — memory hierarchy. ওপরের স্তর ছোট আর দ্রুত, নিচের স্তর বড় আর ধীর। প্রতিটি স্তর তার নিচের স্তরের একটি অংশের কপি রাখে।
নিচের টেবিলের সংখ্যাগুলো আনুমানিক — বহুল প্রচলিত “Latency numbers every programmer should know” ধরনের তালিকা আর প্রসেসর-নির্মাতাদের সাধারণ তথ্য থেকে মাত্রা (order of magnitude) বোঝানোর জন্য। নির্দিষ্ট CPU, ঘড়ির গতি, মাদারবোর্ড আর ডিভাইসভেদে এগুলো কয়েক গুণ পর্যন্ত বদলায়। মুখস্থ করার বিষয় সংখ্যা নয়, অনুপাত: প্রতি ধাপে মোটামুটি দশ গুণ বা তার বেশি পার্থক্য।
| স্তর | সাধারণ আকার | আনুমানিক latency | যদি L1 = ১ সেকেন্ড হয় | কে সামলায় |
|---|---|---|---|---|
| Register | কয়েকশো বাইট | < 1 ns (প্রায় ১ cycle) | এক সেকেন্ডেরও কম | compiler |
| L1 cache | প্রতি core-এ 32–64 KB | ~1 ns (প্রায় ৪ cycle) | ১ সেকেন্ড | হার্ডওয়্যার |
| L2 cache | প্রতি core-এ 256 KB – 2 MB | ~3–5 ns | ৩–৫ সেকেন্ড | হার্ডওয়্যার |
| L3 cache | কয়েক MB থেকে কয়েক ডজন MB, core-গুলোর মধ্যে ভাগ | ~10–40 ns | ১০–৪০ সেকেন্ড | হার্ডওয়্যার |
| Main memory (DRAM) | কয়েক GB থেকে শত GB | ~50–100 ns | ১–২ মিনিট | OS |
| SSD (NVMe) | কয়েকশো GB থেকে কয়েক TB | ~10–100 µs | কয়েক ঘণ্টা থেকে এক দিন | OS (file system, swap) |
| HDD | কয়েক TB | ~5–10 ms | দুই থেকে চার মাস | OS |
“যদি L1 = ১ সেকেন্ড হয়” কলামটি খেয়াল করুন। L1 থেকে একটি মান আনতে যদি এক সেকেন্ড লাগত, RAM থেকে আনতে লাগত দেড় মিনিটের মতো, SSD থেকে প্রায় এক দিন, আর হার্ড ডিস্ক থেকে কয়েক মাস! এই কারণেই OS ডিস্কের ডেটা RAM-এ জমিয়ে রাখে (page cache, পর্ব ৩৮), একটি প্রসেস ডিস্কের জন্য অপেক্ষা করলে CPU অন্য প্রসেসকে দিয়ে দেয় (পর্ব ১৬), আর page fault এত ব্যয়বহুল (পর্ব ৩৫)।
অফিসে একজন কর্মকর্তার কথা ভাবুন। হাতের কাগজ (register) — সাথে সাথে পড়া যায়। টেবিলের ওপর কয়েকটি ফাইল (L1) — হাত বাড়ালেই। টেবিলের ড্রয়ার (L2) আর পাশের তাক (L3) — কয়েক সেকেন্ড। অফিসের ফাইল ক্যাবিনেট রুম (RAM) — উঠে হেঁটে যেতে হয়। আর অন্য শহরের মহাফেজখানা (ডিস্ক) — চিঠি পাঠিয়ে দিনের পর দিন অপেক্ষা। বুদ্ধিমান কর্মকর্তা যে ফাইল বারবার লাগে তা টেবিলেই রাখেন। Cache ঠিক এই কাজ করে, আর নিজে থেকেই।
Cache-এর ধারণা — hit, miss আর cache line
Cache হলো একটি দ্রুত স্তর, যা ধীর স্তরের সাম্প্রতিক ব্যবহৃত অংশের কপি রাখে। CPU কোনো ঠিকানা চাইলে আগে cache দেখে। পেয়ে গেলে cache hit — দ্রুত উত্তর। না পেলে cache miss — নিচের স্তর থেকে আনতে হয়, আর আনার সময় কপি cache-এও রেখে দেওয়া হয়, পরের বারের জন্য। মোট অনুরোধের কত ভাগ hit হলো, তা হলো hit ratio.
একটি গুরুত্বপূর্ণ বিস্তারিত: cache একটি একটি বাইট আনে না, আনে একটি নির্দিষ্ট আকারের খণ্ড — cache line. x86-এর প্রায় সব আধুনিক প্রসেসরে এর আকার ৬৪ বাইট (অংশ ৯-এর bash প্রোগ্রামে নিজেই দেখবেন)। অর্থাৎ একটি int (৪ বাইট) পড়লে আশপাশের আরও ১৫টি int বিনামূল্যে cache-এ চলে আসে। পরের অংশে দেখবেন, এই একটি তথ্য প্রোগ্রামের গতি কয়েক গুণ বদলে দিতে পারে।
Caching — শুধু CPU-র ব্যাপার নয়
“দ্রুত জায়গায় ধীর জায়গার কপি রাখা” — এই কৌশলটি কম্পিউটারের প্রতিটি স্তরে আছে। RAM ডিস্কের cache (OS-এর page cache); TLB হলো page table-এর cache (পর্ব ৩৩); ব্রাউজার ওয়েবসাইটের cache রাখে; CDN সারা বিশ্বের ব্যবহারকারীর কাছাকাছি ভিডিওর cache রাখে। প্রতিটি ক্ষেত্রে একই তিনটি প্রশ্ন: কোন জিনিস রাখব, জায়গা ফুরালে কোনটি সরাব (replacement — পর্ব ৩৬), আর মূল কপি বদলালে cache-এর কপি কীভাবে ঠিক রাখব (consistency — অংশ ৯)।
Storage structure — volatile আর non-volatile
হিসাবের দিক থেকে আরেকটি বিভাজন জরুরি। Register, cache আর DRAM হলো volatile — বিদ্যুৎ চলে গেলে সব মুছে যায়। লোডশেডিংয়ে ডেস্কটপ হঠাৎ বন্ধ হলে না-সেভ করা Word ফাইল হারায় এই কারণেই। SSD, HDD, USB ড্রাইভ, ম্যাগনেটিক টেপ হলো non-volatile — ডেটা টিকে থাকে। পাঠ্যবইয়ের পরিভাষায় CPU সরাসরি যা পড়তে পারে (RAM) তা primary storage, ডিস্ক secondary storage, আর ব্যাকআপের টেপ বা অপটিক্যাল ডিস্ক tertiary storage. CPU ডিস্ক থেকে সরাসরি নির্দেশ চালাতে পারে না; আগে RAM-এ আনতে হয় — এই আনা-নেওয়াই OS-এর memory আর file system অংশের অর্ধেক কাজ। কোন ডেটা কখন volatile থেকে non-volatile-এ যাবে, তা ঠিক করাও OS-এর দায়িত্ব; crash-এর পরে ডেটা অক্ষত রাখার কৌশল আসবে পর্ব ৪৪-এ।
৫ · Locality — কেন cache আসলেই কাজ করে
একটি প্রশ্ন স্বাভাবিক: L1 cache মাত্র কয়েক ডজন KB, অথচ প্রোগ্রাম ব্যবহার করে কয়েকশো MB. তাহলে cache-এ hit হয় কীভাবে? উত্তর হলো প্রোগ্রামের একটি স্বভাব — locality of reference. বাস্তবের প্রোগ্রাম মেমরির সব জায়গা সমানভাবে ব্যবহার করে না; একটি সময়ে একটি ছোট অংশে ঘোরাফেরা করে। Locality দুই রকম:
⏱️ Temporal locality
যে জিনিস এইমাত্র ব্যবহার হলো, শিগগিরই আবার হবে। লুপের ভেতরের নির্দেশ, লুপের counter i, একটি sum ভ্যারিয়েবল — বারবার একই ঠিকানা।
📏 Spatial locality
যে ঠিকানা ব্যবহার হলো, তার পাশের ঠিকানাও শিগগিরই লাগবে। Array পরপর পড়া, নির্দেশগুলো পরপর চলা — এজন্যই পুরো cache line আনা লাভজনক।
স্থানের নৈকট্যএবার একটি পরীক্ষা। C ভাষায় দুই-মাত্রার array মেমরিতে row-major ক্রমে থাকে: আগে সারি ০-এর সব ঘর পাশাপাশি, তারপর সারি ১-এর সব ঘর, এভাবে। তাই সারি ধরে পড়লে (a[i][0], a[i][1]…) আমরা মেমরিতে পরপর এগোই। কলাম ধরে পড়লে (a[0][j], a[1][j]…) প্রতিবার পুরো একটি সারি পরিমাণ লাফ দিই। নিচে একটি খেলনা-cache দিয়ে দুটি পথ তুলনা করুন: array int a[4][4], একটি cache line-এ ধরে ৪টি int (একটি পুরো সারি), আর cache-এ জায়গা মাত্র ২টি line-এর (জায়গা ফুরালে সবচেয়ে পুরোনো-ব্যবহৃত line সরে — LRU).
| ক্রম | |
| পড়া হয়েছে | |
| miss | |
| hit | |
| hit ratio |
- সারি ধরে, প্রথম ঘর।
a[0][0]cache-এ নেই — miss. হার্ডওয়্যার পুরো সারি ০ (একটি line) এনে cache-এ রাখল। - একই সারির বাকি তিনটি।
a[0][1],a[0][2],a[0][3]একই line-এ, তাই তিনটিই hit. এটাই spatial locality-র পুরস্কার: একবার আনার খরচে চারটি ঘর। - সারি ধরে পুরো array. প্রতি সারিতে ১টি miss আর ৩টি hit — মোট ৪ miss, ১২ hit, hit ratio ৭৫%. আসল cache line-এ ১৬টি
intধরে, তাই সেখানে hit ratio আরও বেশি (প্রায় ১৫/১৬)। - কলাম ধরে, খালি cache থেকে।
a[0][0]miss (সারি ০ আনা হলো), তারপরa[1][0]— অন্য সারি, অন্য line — আবার miss. cache-এর দুটি জায়গাই এখন ভরা। - জায়গা ফুরাল।
a[2][0]miss — LRU নিয়মে সবচেয়ে পুরোনো সারি ০ সরে গেল।a[3][0]miss — সারি ১ সরে গেল। - দুর্ভাগ্যের চক্র। এবার
a[0][1]দরকার — কিন্তু সারি ০ এইমাত্র সরানো হয়েছে! আবার miss. পুরো array-তে ১৬টি পড়ায় ১৬টিই miss, একটিও hit নয়। একই ডেটা, একই যোগফল — শুধু পড়ার ক্রম বদলেছে।
খেলনা-উদাহরণ থেকে আসল মেশিনে যাই। নিচের প্রোগ্রাম ৪০৯৬ × ৪০৯৬টি int-এর (৬৪ MB) একটি array দুইভাবে যোগ করে আর সময় মাপে। সময় মাপতে ব্যবহার করা হয়েছে clock_gettime(CLOCK_MONOTONIC) — এমন একটি ঘড়ি যা কখনো পেছনে যায় না। চালিয়ে দেখুন:
#include <stdio.h>
#include <time.h>
#define N 4096
static int a[N][N]; /* 4096 x 4096 ints = 64 MB */
static double now_ms(void) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return t.tv_sec * 1e3 + t.tv_nsec / 1e6;
}
int main(void) {
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
a[i][j] = 1;
long long s1 = 0, s2 = 0;
double t0 = now_ms();
for (int i = 0; i < N; i++) /* row-major: a[i][0], a[i][1], ... */
for (int j = 0; j < N; j++)
s1 += a[i][j];
double t1 = now_ms();
for (int j = 0; j < N; j++) /* column-major: a[0][j], a[1][j], ... */
for (int i = 0; i < N; i++)
s2 += a[i][j];
double t2 = now_ms();
double row = t1 - t0, col = t2 - t1;
printf("sums equal? %s (%lld)\n", s1 == s2 ? "yes" : "no", s1);
printf("row-major : %7.1f ms\n", row);
printf("column-major : %7.1f ms\n", col);
printf("faster: %s, about %.1fx\n",
row < col ? "row-major" : "column-major",
row < col ? col / row : row / col);
return 0;
}একবার চালানোর আসল ফল:
sums equal? yes (16777216)
row-major : 30.4 ms
column-major : 166.8 ms
faster: row-major, about 5.5x
দুটি যোগফলই 16777216 (= ৪০৯৬ × ৪০৯৬), অর্থাৎ কাজ হুবহু এক। তবু কলাম ধরে যোগ করতে প্রায় সাড়ে পাঁচ গুণ সময় লেগেছে। মিলিসেকেন্ডের সংখ্যা প্রতিবার আর প্রতি মেশিনে আলাদা আসবে — অনলাইন সার্ভারে অন্য কাজও চলে, আর CPU-ভেদে cache-এর আকার আলাদা। স্থির থাকে শুধু সম্পর্কটি: row-major দ্রুত, আর অনুপাত সাধারণত কয়েক গুণ। কারণ তিনটি। এক, সারি ধরে পড়লে প্রতি ৬৪-বাইট line-এ ১৬টি int কাজে লাগে; কলাম ধরে পড়লে প্রতিটি পড়া আলাদা line-এ, কারণ পরপর দুটি ঘরের দূরত্ব ৪০৯৬ × ৪ = ১৬ KB. দুই, ১৬ KB-র মতো ২-এর ঘাতের লাফে অনেক line cache-এর একই “সেট”-এ গিয়ে পড়ে আর একে অপরকে সরিয়ে দেয় (conflict miss). তিন, হার্ডওয়্যারের prefetcher পরপর পড়ার ধরন চিনে আগেই পরের line এনে রাখে — সারি ধরে পড়লে এটি নিখুঁত কাজ করে। এ ছাড়া প্রতিটি কলাম-লাফ নতুন একটি ৪ KB page-এ পড়ে, ফলে TLB-ও বারবার miss হয় (পর্ব ৩৩)। প্রোগ্রামটি optimization ছাড়া compile হয়েছে; -O2 দিলে row-major লুপ আরও দ্রুত হয়, তাই অনুপাত সাধারণত আরও বাড়ে।
Big-O-তে দুটি লুপই O(N²). অথচ বাস্তবে একটি পাঁচ গুণ ধীর। অ্যালগরিদমের বিশ্লেষণ ধরে নেয় প্রতিটি মেমরি-পড়ার খরচ সমান — memory hierarchy সেই অনুমান ভেঙে দেয়। OS-এর নিজের ডেটা-কাঠামোও (scheduler-এর run queue, page cache-এর index) এই কারণে cache-বান্ধব করে লেখা হয়। C-তে cache নিয়ে আরও বিস্তারিত পাবেন C প্রোগ্রামিং কোর্সের পারফরম্যান্স ও cache পর্বে।
৬ · AMAT আর effective access time — হিসাবের পদ্ধতি
Cache কতটা লাভ দিচ্ছে, তা একটি সংখ্যায় প্রকাশ করা যায়: গড়ে একটি মেমরি-পড়ায় কত সময় লাগে। কম্পিউটার আর্কিটেকচারে একে বলে AMAT (average memory access time):
এখানে hit time হলো cache-এ পেলে কত সময়, miss rate = ১ − hit ratio, আর miss penalty হলো না পেলে নিচের স্তর থেকে আনতে বাড়তি কত সময়। দুই স্তরের cache-এ সূত্রটি নিজের ভেতরে নিজেকে বসায়: L1-এর miss penalty হলো L2-এর AMAT.
এখানে m₂ হলো L2-এর local miss rate — যত অনুরোধ L2 পর্যন্ত পৌঁছায়, তার কত ভাগ সেখানেও মেলে না। আর m₁ × m₂ হলো global miss rate — সব অনুরোধের কত ভাগ শেষ পর্যন্ত RAM-এ যায়। নিচের ছোট simulator-এ ধরা হয়েছে L1 hit = ১ cycle, L2 hit = ১০ cycle, L2-এর local miss rate ২০%, আর RAM থেকে আনতে ১০০ cycle. L1-এর miss rate বদলালে কী হয়, দেখুন:
// Two-level cache: AMAT = L1 hit + L1 miss rate x (L2 hit + L2 miss rate x memory)
const L1_HIT = 1; // cycles
const L2_HIT = 10; // cycles
const L2_MISS = 0.20; // local miss rate of L2 (of the accesses that reach L2)
const MEM = 100; // cycles to fetch from DRAM
const amat = (m1) => L1_HIT + m1 * (L2_HIT + L2_MISS * MEM);
const noL2 = (m1) => L1_HIT + m1 * MEM;
const f = (x, w) => x.toFixed(2).padStart(w);
console.log('L1 miss | AMAT with L2 | AMAT L1 only | reach RAM');
for (const m1 of [0.01, 0.02, 0.05, 0.10, 0.20]) {
console.log(
`${(m1 * 100).toFixed(0).padStart(6)}% | ${f(amat(m1), 8)} cyc | ${f(noL2(m1), 8)} cyc |` +
` ${(m1 * L2_MISS * 100).toFixed(1).padStart(5)}%`);
}
const ex = amat(0.05);
console.log(`No cache: every access = ${MEM} cycles`);
console.log(`Example: 1 + 0.05 x (10 + 0.20 x 100) = ${ex.toFixed(2)} cycles`);
console.log(`Speed-up over no cache: ${(MEM / ex).toFixed(0)}x`);আউটপুট:
L1 miss | AMAT with L2 | AMAT L1 only | reach RAM
1% | 1.30 cyc | 2.00 cyc | 0.2%
2% | 1.60 cyc | 3.00 cyc | 0.4%
5% | 2.50 cyc | 6.00 cyc | 1.0%
10% | 4.00 cyc | 11.00 cyc | 2.0%
20% | 7.00 cyc | 21.00 cyc | 4.0%
No cache: every access = 100 cycles
Example: 1 + 0.05 x (10 + 0.20 x 100) = 2.50 cycles
Speed-up over no cache: 40x
L1-এ ৫% miss হলে গড় সময় ২.৫ cycle — cache না থাকলে যা হতো (১০০ cycle), তার চল্লিশ ভাগের এক ভাগ। L2 না থাকলে একই অবস্থায় ৬ cycle; অর্থাৎ L2 প্রতিটি L1-miss-এর খরচ ১০০ থেকে কমিয়ে গড়ে ৩০ করছে। আর শেষ কলাম দেখায়, ৫% L1 miss-এর মধ্যে মাত্র ১% অনুরোধ শেষ পর্যন্ত RAM-এ যায়। আরেকটি শিক্ষা: miss rate ১% থেকে ২০%-এ গেলে AMAT ১.৩ থেকে ৭ — পাঁচ গুণের বেশি। ছোট মনে হওয়া miss rate-এর পার্থক্যই গতিতে বড় পার্থক্য আনে।
পরীক্ষার হিসাব — effective access time, পুরোটা
বিশ্ববিদ্যালয় আর চাকরির পরীক্ষায় একই ধারণা আসে effective access time (EAT) নামে, hit ratio h দিয়ে। এখানে একটি ফাঁদ আছে: প্রশ্নে miss হলে মোট সময় কীভাবে ধরা হবে, তার দুটি প্রচলিত রীতি।
- রীতি ক (hierarchical / look-through): আগে cache দেখা হয়, না পেলে তারপর মেমরি। miss-এর সময় = Tc + Tm. সূত্র: EAT = h × Tc + (1 − h) × (Tc + Tm), যা সরল করলে Tc + (1 − h) × Tm — হুবহু AMAT-এর সূত্র।
- রীতি খ (simultaneous / look-aside): cache আর মেমরিতে একসাথে খোঁজ শুরু হয়। miss-এর সময় = শুধু Tm. সূত্র: EAT = h × Tc + (1 − h) × Tm.
প্রশ্নে স্পষ্ট বলা না থাকলে কোন রীতি ধরছেন তা লিখে দিন — পরীক্ষকেরা সাধারণত সেটিই দেখতে চান।
Cache access time ১০ ns, main memory access time ১০০ ns, hit ratio ০.৯. EAT কত?
রীতি ক: ০.৯ × ১০ + ০.১ × (১০ + ১০০) = ৯ + ১১ = ২০ ns.
রীতি খ: ০.৯ × ১০ + ০.১ × ১০০ = ৯ + ১০ = ১৯ ns.
Cache না থাকলে প্রতিবার ১০০ ns লাগত; তাই cache গতি প্রায় পাঁচ গুণ বাড়িয়েছে।
একই মেশিনে (Tc = ১০ ns, Tm = ১০০ ns, রীতি ক) EAT ১৫ ns-এর বেশি হতে দেওয়া যাবে না। ন্যূনতম hit ratio কত?
EAT = ১০ + (১ − h) × ১০০ ≤ ১৫ ⟹ (১ − h) × ১০০ ≤ ৫ ⟹ ১ − h ≤ ০.০৫ ⟹ h ≥ ০.৯৫.
আর h = ০.৯৯ হলে EAT = ১০ + ০.০১ × ১০০ = ১১ ns. Hit ratio ০.৯ থেকে ০.৯৯-এ তুললে সময় ২০ থেকে ১১ ns — প্রায় অর্ধেক। শেষের কয়েক শতাংশ hit-ই সবচেয়ে মূল্যবান।
L1: ২ ns, hit ratio ০.৯. L2: ১০ ns, L2-তে পৌঁছানো অনুরোধের hit ratio ০.৮. Main memory: ১০০ ns. রীতি ক ধরে EAT কত?
EAT = ২ + (১ − ০.৯) × [১০ + (১ − ০.৮) × ১০০] = ২ + ০.১ × [১০ + ২০] = ২ + ৩ = ৫ ns.
সমমানের বিস্তৃত রূপ: ০.৯ × ২ + ০.১ × ০.৮ × (২ + ১০) + ০.১ × ০.২ × (২ + ১০ + ১০০) = ১.৮ + ০.৯৬ + ২.২৪ = ৫ ns. দুই পদ্ধতিতে একই উত্তর — হাতে মিলিয়ে দেখার ভালো উপায়।
পর্ব ৩৩-এ এই একই কাঠামো ফিরে আসবে TLB-র effective access time হিসেবে, আর পর্ব ৩৫-এ page fault-এর হিসাবে, যেখানে miss penalty ন্যানোসেকেন্ড নয়, মিলিসেকেন্ড — ফলে সামান্য fault rate-ও গতি নাটকীয়ভাবে কমিয়ে দেয়।
৭ · Interrupt — হার্ডওয়্যার কীভাবে CPU-কে ডাকে
CPU সেকেন্ডে শতকোটি নির্দেশ চালায়, আর কিবোর্ডে আপনি হয়তো সেকেন্ডে পাঁচটি অক্ষর টাইপ করেন। নেটওয়ার্ক কার্ডে প্যাকেট আসে অনিয়মিত সময়ে, ডিস্ক একটি পড়া শেষ করে কয়েক মাইক্রোসেকেন্ড বা মিলিসেকেন্ড পরে। CPU কীভাবে জানবে কখন কোন ডিভাইস প্রস্তুত? দুটি পথ আছে।
🔁 Polling
CPU বারবার ডিভাইসের status register পড়ে জিজ্ঞেস করে, “হয়েছে? হয়েছে?” সহজ, কিন্তু ডিভাইস ধীর হলে CPU-র বেশিরভাগ সময় শুধু জিজ্ঞেস করতেই চলে যায়।
polling · busy-waiting🔔 Interrupt
CPU নিজের কাজ করে; ডিভাইস প্রস্তুত হলে একটি বৈদ্যুতিক সংকেত পাঠিয়ে CPU-কে থামায়। CPU তখন একটি বিশেষ ফাংশন চালিয়ে আবার আগের কাজে ফেরে।
interrupt · event-drivenআপনি অনলাইনে খাবার অর্ডার করেছেন। Polling হলো প্রতি দুই মিনিটে দরজা খুলে দেখা ডেলিভারিম্যান এলো কি না — অন্য কোনো কাজ হয় না। Interrupt হলো কলিং বেল: আপনি নিশ্চিন্তে পড়াশোনা করছেন, বেল বাজলে বইয়ের পাতায় আঙুল রেখে (state সংরক্ষণ) দরজায় যান, খাবার নেন (ISR), তারপর ঠিক সেই পাতায় ফিরে আসেন। তবে ডেলিভারিম্যান যদি প্রতি পাঁচ সেকেন্ডে আসতে থাকে, বারবার উঠে যাওয়ার চেয়ে দরজার পাশেই বসে থাকা ভালো — ভাবনার প্রশ্ন ৩-এ এই trade-off দেখব।
Interrupt vector table আর ISR
প্রতিটি interrupt-এর একটি নম্বর থাকে — vector. বুটের সময় OS মেমরিতে একটি টেবিল সাজায়, যার প্রতিটি ঘরে লেখা থাকে “এই নম্বরের interrupt এলে এই ঠিকানার কোড চালাও।” এটিই interrupt vector table (x86-এ নাম Interrupt Descriptor Table, IDT; সেখানে ২৫৬টি ঘর)। প্রতিটি ঘরের কোডকে বলে interrupt service routine (ISR) বা interrupt handler. টেবিলের ঠিকানা বসানো একটি privileged instruction — তাই কোনো অ্যাপ নিজের কোডকে handler বানিয়ে নিতে পারে না। নিচের ডায়াগ্রামে দেখুন, একটি প্রোগ্রাম চলার মাঝে কিবোর্ডের একটি চাপ এলে ঠিক কী ঘটে। (vector নম্বর আর ঠিকানাগুলো উদাহরণ মাত্র।)
| mode | |
| PC | |
| interrupt pending | |
| নতুন interrupt নেবে? | |
| kernel stack-এ |
- প্রসেস P চলছে। User mode-এ P একটি গুণ (
imul) করছে, PC = 0x4010. CPU interrupt নিতে প্রস্তুত (FLAGS-এর interrupt বিট চালু)। - Interrupt এলো। আপনি একটি key চাপলেন। কিবোর্ড controller একটি interrupt লাইন (IRQ) সক্রিয় করল। CPU চলতি নির্দেশটি শেষ করে, তারপর দেখে কোনো interrupt অপেক্ষায় আছে কি না।
imulশেষ, তাই PC এখন 0x4014 — পরের নির্দেশ। - হার্ডওয়্যার state রাখল আর mode বদলাল। এই কাজ কোনো সফটওয়্যার করে না, CPU নিজে করে: FLAGS আর PC (x86-64-এ SP আর CS-ও) kernel stack-এ রাখে, mode user থেকে kernel করে, নতুন interrupt সাময়িক বন্ধ করে, তারপর vector table-এর ঘর (এখানে ৩৩ নম্বর) দেখে ISR-এর ঠিকানায় লাফ দেয়।
- ISR চলছে। ISR প্রথমে যে register-গুলো নিজে ব্যবহার করবে সেগুলো stack-এ রাখে, যাতে P-র মান নষ্ট না হয়। তারপর controller-এর data register (এখানে port 0x60) থেকে scancode পড়ে, একটি buffer-এ রাখে, আর controller-কে জানায় “পেয়েছি” (EOI). ভালো ISR খুব ছোট; ভারী কাজ পরে করার জন্য সরিয়ে রাখে।
- ফেরা। ISR register ফিরিয়ে দিয়ে
iretচালায়। CPU stack থেকে PC = 0x4014 আর আগের FLAGS তুলে নেয়, mode আবার user. FLAGS ফেরত আসায় interrupt আবার চালু। - P যেন কিছুই হয়নি। P ঠিক 0x4014 থেকে চলতে থাকে, সব register অক্ষত। এই “স্বচ্ছতা”-ই interrupt-এর জাদু। তবে OS চাইলে ফেরার আগে অন্য প্রসেসও চালাতে পারত — যেমন key-টির জন্য অপেক্ষায় থাকা text editor. তখন সেটিই হতো একটি context switch.
Timer interrupt — OS কেন সবসময় CPU ফিরে পায়
এবার একটি গভীর প্রশ্ন। আপনার প্রোগ্রামে যদি থাকে while (1) { } — এমন একটি লুপ যা কখনো system call করে না, কোনো ডিভাইস ছোঁয় না — তাহলে OS কীভাবে CPU ফিরে পাবে? OS তো নিজেও একটি প্রোগ্রাম; CPU যখন অন্য প্রোগ্রাম চালাচ্ছে, OS-এর কোড তখন চলছেই না!
উত্তর: হার্ডওয়্যারের একটি timer. বুটের সময় OS (kernel mode-এ) একটি timer সেট করে, যা নির্দিষ্ট সময় পরপর interrupt পাঠায় — Linux-এ সাধারণত সেকেন্ডে ১০০ থেকে ১০০০ বার (kernel-এর HZ সেটিং, যা compile-এর সময় ঠিক হয়), Windows-এ ডিফল্ট প্রায় ১৫.৬ ms পরপর। প্রতিটি timer interrupt-এ CPU বাধ্যতামূলকভাবে kernel-এর handler-এ যায়। Handler হিসাব করে, চলমান প্রসেস তার ভাগের সময় (time slice) শেষ করেছে কি না; শেষ হলে scheduler অন্য প্রসেস বেছে নেয়। Timer বদলানো বা বন্ধ করা privileged instruction, তাই কোনো অ্যাপ এটি থামাতে পারে না। এই ব্যবস্থার নাম preemptive multitasking, আর পুরো মডিউল ৩-এর scheduling এর ওপরেই দাঁড়িয়ে।
OS-এর নিয়ন্ত্রণ ফিরে পাওয়ার মাত্র তিনটি পথ: প্রোগ্রাম নিজে ডাকে (system call), প্রোগ্রাম ভুল করে (exception), অথবা বাইরের কোনো ঘটনা ঘটে (interrupt — বিশেষ করে timer). Timer interrupt হলো এদের মধ্যে একমাত্র নিশ্চিত পথ, যা প্রোগ্রামের সহযোগিতা ছাড়াই কাজ করে।
Interrupt, trap আর exception — পার্থক্য কোথায়
সব “থামিয়ে kernel-এ যাওয়া” এক জিনিস নয়। পার্থক্যটি উৎস আর সময়ে:
| ধরন | কে ঘটায় | সময়ের সাথে সম্পর্ক | উদাহরণ |
|---|---|---|---|
| Hardware interrupt | বাইরের ডিভাইস | Asynchronous — চলমান নির্দেশের সাথে সম্পর্কহীন, যেকোনো সময় | কিবোর্ড, নেটওয়ার্ক প্যাকেট, ডিস্ক শেষ, timer |
| Trap (software interrupt) | প্রোগ্রাম নিজে, ইচ্ছাকৃত নির্দেশে | Synchronous — নির্দিষ্ট নির্দেশেই, প্রতিবার | System call (syscall, পুরোনো Linux-এ int 0x80), debugger-এর breakpoint (int3) |
| Exception / fault | প্রোগ্রামের নির্দেশ, অনিচ্ছাকৃত | Synchronous — ভুল নির্দেশটি চালানোর সময় | শূন্য দিয়ে ভাগ (x86 vector 0), অবৈধ নির্দেশ, page fault (vector 14), user mode-এ privileged instruction |
পরিভাষা বইভেদে একটু আলাদা — কেউ trap বলতে সব synchronous ঘটনা বোঝান, Intel-এর ম্যানুয়ালে exception-কে আবার fault, trap আর abort-এ ভাগ করা আছে। একটি সূক্ষ্ম তফাত মনে রাখার মতো: fault-এর পরে CPU একই নির্দেশ আবার চালায় (যেমন page fault — OS page এনে দিলে নির্দেশটি এবার সফল হয়, পর্ব ৩৫), আর trap-এর পরে পরের নির্দেশ থেকে চলে। Linux-এ exception প্রায়ই প্রসেসের কাছে একটি signal হয়ে পৌঁছায় — শূন্য দিয়ে ভাগে SIGFPE, ভুল মেমরিতে SIGSEGV (পর্ব ১৩); Windows-এ একই ঘটনা structured exception হিসেবে আসে।
৮ · I/O ডিভাইস — controller, device register, port I/O আর DMA
CPU সরাসরি ডিস্কের চাকতি ঘোরায় না, Wi-Fi-র রেডিও তরঙ্গও বোঝে না। প্রতিটি ডিভাইসের সাথে থাকে একটি ছোট নিজস্ব কম্পিউটার — device controller (SSD-র controller, নেটওয়ার্ক কার্ডের চিপ, USB host controller). Controller-এর নিজস্ব buffer আর কয়েকটি device register থাকে, সাধারণত:
- Status register — ডিভাইস ব্যস্ত, প্রস্তুত, নাকি কোনো ত্রুটি হয়েছে।
- Command / control register — CPU এখানে লিখে আদেশ দেয়: “ব্লক ৫০০ পড়ো।”
- Data register (বা buffer) — আদান-প্রদানের ডেটা।
OS-এর ভেতরে যে কোডটি একটি নির্দিষ্ট controller-এর register-এর ভাষা জানে, সেটিই device driver (পর্ব ৩৯)। কিন্তু CPU এই register-এ পৌঁছায় কীভাবে? দুটি পদ্ধতি আছে:
| বিষয় | Port-mapped I/O | Memory-mapped I/O (MMIO) |
|---|---|---|
| ঠিকানা | মেমরি থেকে আলাদা একটি “I/O ঠিকানার জগৎ” (x86-এ ৬৫,৫৩৬টি port) | মেমরির ঠিকানার একটি অংশ ডিভাইসের জন্য সংরক্ষিত |
| নির্দেশ | বিশেষ নির্দেশ: x86-এর in / out | সাধারণ load / store — যেন মেমরিতে পড়া-লেখা |
| কোথায় দেখা যায় | x86-এর পুরোনো ডিভাইস (যেমন PS/2 কিবোর্ড controller-এর port 0x60) | প্রায় সব আধুনিক ডিভাইস (PCIe কার্ড, GPU, NVMe); ARM আর RISC-V-এ একমাত্র পদ্ধতি |
| সুরক্ষা | user mode-এ সাধারণত নিষিদ্ধ | OS ঠিকানাগুলো user প্রসেসের page table-এ রাখে না |
MMIO-তে একটি সতর্কতা আছে: ডিভাইসের register সাধারণ মেমরির মতো আচরণ করে না — একই ঠিকানা দুবার পড়লে দুটি আলাদা মান আসতে পারে (status বদলে গেছে)। তাই এই ঠিকানাগুলো cache-এ রাখা হয় না, আর C-তে driver লেখার সময় compiler যেন পড়া-লেখা বাদ না দেয়, সেজন্য volatile ব্যবহার করা হয়।
DMA — CPU-কে কুলির কাজ থেকে মুক্তি
কিবোর্ডের এক বাইটের জন্য interrupt ঠিক আছে। কিন্তু ডিস্ক থেকে ৪ KB বা নেটওয়ার্ক থেকে মেগাবাইটের পর মেগাবাইট ডেটা আনতে হলে? CPU যদি প্রতিটি শব্দ (word) controller-এর data register থেকে পড়ে RAM-এ লেখে — একে বলে programmed I/O — তাহলে দ্রুততম অংশটি দিনভর কুলির কাজ করবে। সমাধান DMA (direct memory access): একটি আলাদা DMA engine (অনেক সময় ডিভাইসের controller-এর ভেতরেই) CPU-কে না জড়িয়ে সরাসরি ডিভাইস আর RAM-এর মধ্যে ডেটা সরায়। CPU শুধু শুরুতে কাজটি বুঝিয়ে দেয়, আর শেষে একটি interrupt পায়। নিচে দুটি পথ পাশাপাশি:
| বাঁয়ে (CPU কপি) | |
| ডানে (DMA) | |
| কপি হয়েছে |
- অনুরোধ। প্রসেস A ফাইল থেকে ৪০৯৬ বাইট পড়তে চায়। দুই দিকেই system call দিয়ে kernel-এ যায়, driver ডিস্ক controller-কে আদেশ দেয়, আর A অপেক্ষার অবস্থায় যায়। এখান থেকে পথ আলাদা।
- প্রস্তুতি। বাঁ দিকে CPU নিজেই কপির লুপ চালাতে শুরু করে — ৮-বাইটের ৫১২টি word, প্রতিটির জন্য একটি পড়া আর একটি লেখা। ডান দিকে CPU DMA engine-এর কয়েকটি register-এ লেখে: কোথা থেকে, কোথায়, কত বাইট, কোন দিকে। তারপর CPU মুক্ত — scheduler প্রসেস B চালায়।
- মাঝপথে। দুই দিকেই অর্ধেক কপি শেষ। কিন্তু বাঁ দিকে CPU-র পুরো সময় গেছে কপিতে, B বসে আছে। ডান দিকে DMA engine bus ব্যবহার করে ডেটা সরাচ্ছে আর CPU B-র হিসাব করছে। DMA মাঝে মাঝে bus দখল করে বলে CPU সামান্য ধীর হতে পারে — একে বলে cycle stealing — কিন্তু CPU-র cache থেকে চলা কাজ প্রায় অপ্রভাবিত।
- কপি শেষ। বাঁ দিকে CPU ১০২৪টি মেমরি-অপারেশন আর লুপের খরচ দিয়ে কাজ শেষ করল। ডান দিকে DMA-র count শূন্যে পৌঁছাল, আর DMA engine CPU-কে একটি interrupt পাঠাল।
- ফল। দুই দিকেই A এখন ready, ডেটা তার buffer-এ। পার্থক্য: বাঁ দিকে এতক্ষণ B কিছুই করতে পারেনি; ডান দিকে B পুরো সময় চলেছে, আর CPU-র খরচ ছিল শুধু কয়েকটি register লেখা আর একটি ছোট ISR. বড় ডেটায় এই পার্থক্য বিশাল — এজন্যই ডিস্ক, নেটওয়ার্ক কার্ড, GPU সবাই DMA ব্যবহার করে। DMA-র বিস্তারিত, scatter-gather আর IOMMU আসবে পর্ব ৩৯-এ।
DMA শক্তিশালী, তাই বিপজ্জনকও: একটি ডিভাইস CPU-কে না জিজ্ঞেস করে RAM-এ লিখতে পারে। কোনো ক্ষতিকর বা ত্রুটিপূর্ণ ডিভাইস (যেমন বাইরের পোর্টে লাগানো) তাত্ত্বিকভাবে kernel-এর মেমরিও বদলে দিতে পারে। এর প্রতিরক্ষা হলো IOMMU — DMA-র জন্য একটি “page table”, যা ঠিক করে কোন ডিভাইস RAM-এর কোন অংশ ছুঁতে পারবে। আধুনিক Windows, Linux আর macOS এটি ব্যবহার করে বাইরের ডিভাইস থেকে সুরক্ষা দেয় (যেমন Windows-এর Kernel DMA Protection).
৯ · একাধিক core — multicore, SMP, hyper-threading আর cache coherence
প্রায় ২০০৫ সালের কাছাকাছি সময়ে চিপ-নির্মাতারা একটি দেয়ালে ঠেকে যান: ঘড়ির গতি (GHz) আর বাড়ানো যাচ্ছিল না, কারণ তাপ আর বিদ্যুৎ-খরচ সামলানো অসম্ভব হয়ে উঠছিল। তাই তাঁরা পথ বদলান — একটি চিপে একাধিক সম্পূর্ণ CPU বসানো। প্রতিটির নাম core, আর এমন চিপ multicore. আজ অনেক কম দামের স্মার্টফোনেও ৮টি core থাকে।
OS-এর চোখে প্রায় সব সাধারণ multicore কম্পিউটার SMP (symmetric multiprocessing): সব core সমান, সবাই একই RAM দেখে, যেকোনো core kernel চালাতে পারে, যেকোনো প্রসেস যেকোনো core-এ চলতে পারে। এতে OS-এর কাজ বাড়ে: প্রতিটি core-এর জন্য আলাদা run queue, প্রসেসকে কোন core-এ রাখলে তার cache গরম থাকে (affinity, পর্ব ২০), আর দুটি core একসাথে kernel-এর একই ডেটা বদলাতে গেলে lock (মডিউল ৪)। বড় সার্ভারে আরেকটি বৈশিষ্ট্য যোগ হয় — NUMA: প্রতিটি CPU socket-এর নিজের কাছের RAM দ্রুত, অন্য socket-এর RAM তুলনামূলক ধীর; OS তাই প্রসেসের মেমরি তার কাছের RAM-এ রাখতে চেষ্টা করে।
Hyper-threading (সাধারণ নাম SMT, simultaneous multithreading) আরেকটু ভিন্ন জিনিস। এখানে একটি physical core-এর ভেতরে দুই সেট register থাকে, ফলে OS-এর কাছে একটি core দুটি “logical CPU” হিসেবে দেখায়। কিন্তু ALU, cache-এর মতো আসল কাজের অংশ দুই থ্রেড ভাগ করে নেয়। একটি থ্রেড মেমরির জন্য অপেক্ষা করলে অন্যটি সেই ফাঁকা সময় কাজে লাগায়। Intel ২০০২ সালে Xeon প্রসেসরে Hyper-Threading নামে এটি আনে; AMD-র Zen প্রসেসরেও (২০১৭ থেকে) SMT আছে। লাভ কাজভেদে সাধারণত দুই গুণের অনেক কম — দুটি logical CPU মানে দুটি পূর্ণ core নয়।
Cache coherence — দুই core, একই ঠিকানা
প্রতিটি core-এর নিজস্ব L1 আর L2 cache আছে। ধরুন একটি ভ্যারিয়েবল x = 5 দুই core-এর cache-এই কপি হয়ে আছে। Core 0 লিখল x = 6. Core 1 এখন x পড়লে পুরোনো 5 পাবে? তা হলে প্রোগ্রাম ভুল চলবে। এই সমস্যার সমাধান cache coherence: হার্ডওয়্যার একটি protocol চালায় (সবচেয়ে পরিচিত MESI — Modified, Exclusive, Shared, Invalid), যাতে কোনো core একটি line-এ লেখার আগে অন্য সব core-এর কপি বাতিল (invalidate) হয়। ফলে Core 1 পরের বার পড়লে miss হবে আর নতুন মান 6 পাবে।
এটি নিজে থেকেই হয়, তবে বিনামূল্যে নয়। দুটি core যদি একই cache line-এর দুটি আলাদা ভ্যারিয়েবলে বারবার লেখে, line-টি দুই cache-এর মধ্যে ক্রমাগত ছোটাছুটি করে — একে বলে false sharing, আর এটি multithreaded প্রোগ্রামকে রহস্যজনকভাবে ধীর করে দেয়। আরও সূক্ষ্ম বিষয় হলো, coherence শুধু “একটি ঠিকানার” মান মেলায়; ভিন্ন ঠিকানায় লেখা অন্য core কোন ক্রমে দেখবে, তা ঠিক করে memory ordering, যার আলোচনা পর্ব ২৩-এ।
আপনার (বা অনলাইন সার্ভারের) CPU সম্পর্কে Linux কী জানে, দেখুন। Linux এই তথ্য দেখায় দুটি ভার্চুয়াল ফাইল-ব্যবস্থায়: /proc/cpuinfo আর /sys/. নিচের script কোনো ফাইল না পেলে ভেঙে না পড়ে বার্তা দেয়:
echo "== CPU =="
grep -m1 'model name' /proc/cpuinfo || echo "(model name not listed)"
echo "logical CPUs we may use: $(nproc)"
flags=$(grep -m1 -E '^(flags|Features)' /proc/cpuinfo | cut -d: -f2)
echo "feature flags: $(echo $flags | wc -w)"
for f in lm nx ht sse2 avx2 vmx svm; do
if echo " $flags " | grep -qw "$f"; then echo " $f : yes"; else echo " $f : no"; fi
done
echo "== cache of cpu0 =="
found=0
for d in /sys/devices/system/cpu/cpu0/cache/index*; do
[ -r "$d/size" ] || continue
found=1
echo " L$(cat "$d/level") $(cat "$d/type"): $(cat "$d/size")"
done
[ "$found" -eq 1 ] || echo " (cache info is not exposed here)"
line=$(cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size 2>/dev/null)
echo "cache line: ${line:-unknown} bytes"একবার চালানোর আসল ফল:
== CPU ==
model name : AMD EPYC-Rome Processor
logical CPUs we may use: 2
feature flags: 80
lm : yes
nx : yes
ht : yes
sse2 : yes
avx2 : yes
vmx : no
svm : no
== cache of cpu0 ==
L1 Data: 32K
L1 Instruction: 32K
L2 Unified: 512K
L3 Unified: 16384K
cache line: 64 bytes
সার্ভারটি একটি AMD EPYC (Rome প্রজন্ম) — নামের ধরন দেখে মনে হয় এটি নিজেই একটি virtual machine-এর ভেতরে, আর আমাদের container ব্যবহার করতে পারে মাত্র ২টি logical CPU (nproc container-এর সীমা মানে, পুরো মেশিনের নয়)। Flag-গুলো CPU-র সামর্থ্যের তালিকা: lm = ৬৪-বিট (long mode), nx = মেমরির কোনো page-কে “চালানো যাবে না” চিহ্ন দেওয়ার ক্ষমতা (নিরাপত্তায় জরুরি, পর্ব ৪৮), ht মানে CPU একাধিক logical CPU-র ক্ষমতা ঘোষণা করছে — এটি দেখে নিশ্চিত হওয়া যায় না যে hyper-threading চালু আছে। vmx/svm (Intel/AMD-র hardware virtualization) দেখা যাচ্ছে না, কারণ VM-এর ভেতরে এগুলো সাধারণত লুকানো থাকে (পর্ব ৫০)। Cache: L1 ডেটা আর নির্দেশের জন্য আলাদা, প্রতিটি ৩২ KB — von Neumann-এর এক মেমরির ভেতরেও CPU-র কাছে নির্দেশ আর ডেটার আলাদা রাস্তা। L2 ৫১২ KB, L3 ১৬ MB, আর cache line ঠিক ৬৪ বাইট — অংশ ৫-এর হিসাবের সাথে মিলে গেল। আপনার ল্যাপটপে চালালে মান ভিন্ন আসবে; ARM-এ (যেমন Raspberry Pi) flag-এর লাইনের নাম Features, তাই script দুটোই খোঁজে।
অন্য OS-এ একই তথ্য অন্য পথে পাওয়া যায় — /proc শুধু Linux-এর জিনিস:
lscpu # সংক্ষিপ্ত সারাংশ: core, thread, cache, NUMA
cat /proc/interrupts # কোন interrupt কতবার এসেছে, প্রতিটি CPU-তে আলাদাGet-CimInstance Win32_Processor |
Select-Object Name, NumberOfCores, NumberOfLogicalProcessors, L2CacheSize, L3CacheSizeTask Manager-এর Performance → CPU ট্যাবেও core, logical processor আর L1/L2/L3 cache-এর আকার দেখা যায়।
sysctl -n machdep.cpu.brand_string
sysctl hw.ncpu hw.l1dcachesize hw.l2cachesize hw.cachelinesizeApple silicon-এ hw.cachelinesize সাধারণত 128 দেখায় — অর্থাৎ ৬৪ বাইট সর্বজনীন নিয়ম নয়।
আরও গভীরে: আধুনিক x86-এ interrupt আর পুরোনো 8259 PIC চিপ দিয়ে আসে না; প্রতিটি core-এর থাকে একটি local APIC, আর PCIe ডিভাইস সাধারণত MSI/MSI-X ব্যবহার করে — অর্থাৎ interrupt আসলে একটি বিশেষ ঠিকানায় মেমরি-লেখা, ফলে একটি নেটওয়ার্ক কার্ড আলাদা আলাদা queue-র interrupt আলাদা core-এ পাঠাতে পারে। ARM-এ একই কাজ করে GIC (Generic Interrupt Controller). Linux-এ interrupt handler-এর ভারী কাজ “top half / bottom half”-এ ভাগ হয় (softirq, tasklet, workqueue), আর কয়েক হাজার লাইনের ছোট শিক্ষণ-OS xv6-এ পুরো পথটি পড়া যায়: x86 সংস্করণে trapasm.S state সংরক্ষণ করে, trap.c vector দেখে সিদ্ধান্ত নেয়; RISC-V সংস্করণে একই কাজ করে trampoline.S, kernelvec.S আর trap.c (পর্ব ৫৮)।
১০ · AI-এর যুগে — হার্ডওয়্যার-প্রশ্নে AI-কে কীভাবে ব্যবহার করবেন
“আমার সার্ভারের L3 cache কত?”, “এই লুপ ধীর কেন?”, “EAT-এর এই অঙ্কটা করে দাও” — এমন প্রশ্নে AI দ্রুত উত্তর দেয়। কিন্তু হার্ডওয়্যারের প্রশ্নে AI-এর ভুলগুলো একটি নির্দিষ্ট ধাঁচের, আর সেগুলো চেনা থাকলে যাচাই সহজ:
| AI প্রায়ই যা করে | কেন সমস্যা | কীভাবে যাচাই করবেন |
|---|---|---|
| Latency-কে নির্দিষ্ট সত্য হিসেবে বলে (“RAM = 100 ns”) | মান CPU, প্রজন্ম আর মেশিনভেদে কয়েক গুণ বদলায় | নিজের মেশিনে মাপুন — অংশ ৫-এর মতো ছোট benchmark |
| EAT-এ রীতি ক আর খ মিশিয়ে ফেলে | উত্তর ২০ বনাম ১৯ ns — পরীক্ষায় নম্বর কাটা | নিজে হাতে দুই রীতিতে হিসাব করুন, বিস্তৃত রূপেও মিলিয়ে দেখুন |
| Linux-এর কমান্ড দেয়, আপনি Windows বা macOS-এ | /proc/cpuinfo macOS-এ নেই; wmic এখন Microsoft-এর অবচয়িত টুল | প্রশ্নে OS আর version লিখুন; কমান্ড চালানোর আগে man page বা অফিশিয়াল ডকুমেন্টেশন দেখুন |
“ht flag আছে, তাই hyper-threading চালু” বলে | flag শুধু সামর্থ্য বোঝায় | lscpu-এর “Thread(s) per core” দেখুন |
volatile-কে থ্রেড-নিরাপদ বলে | volatile শুধু MMIO-র মতো ক্ষেত্রে পড়া-লেখা বাদ পড়া ঠেকায়; atomicity বা ordering দেয় না | পর্ব ২৩-এর atomic আর lock দেখুন; চাপের মধ্যে বহুবার চালিয়ে পরীক্ষা করুন |
AI-কে হার্ডওয়্যার বা পারফরম্যান্সের প্রশ্ন করার সময় নিজের প্রেক্ষাপট দিন, আর উত্তরের সাথে যাচাইয়ের পথও চান:
AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Interrupt আর polling-এর পার্থক্য? — Polling-এ CPU বারবার ডিভাইসের status পড়ে; interrupt-এ ডিভাইস প্রস্তুত হলে নিজেই CPU-কে সংকেত দেয়। ধীর বা অনিয়মিত ডিভাইসে interrupt ভালো, খুব দ্রুত ডিভাইসে polling.
- Cache ১০ ns, memory ১০০ ns, hit ratio ০.৯ — EAT? — Hierarchical ধরলে ০.৯ × ১০ + ০.১ × ১১০ = ২০ ns; simultaneous ধরলে ১৯ ns.
- DMA কী, কেন দরকার? — ডিভাইস আর RAM-এর মধ্যে CPU ছাড়া ডেটা সরানোর ব্যবস্থা; বড় ডেটায় CPU মুক্ত থাকে, শেষে একটি interrupt আসে।
- Dual-mode operation কেন? — user প্রোগ্রাম যেন privileged instruction (I/O, timer, page table) চালাতে না পারে; OS আর অন্য প্রোগ্রাম সুরক্ষিত থাকে।
- Trap আর interrupt-এর পার্থক্য? — trap synchronous, চলমান নির্দেশ থেকে (system call, ভুল); interrupt asynchronous, বাইরের ডিভাইস থেকে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো হার্ডওয়্যার আর OS-নকশার সম্পর্কটি গভীরভাবে দেখায়।
প্র ০১CPU প্রতি বছর দ্রুত হয়েছে, অথচ মেমরি সেই হারে হয়নি। এই “memory wall” OS আর প্রোগ্রামের নকশা কীভাবে বদলে দিয়েছে?
১৯৭৭ সালের Turing Award ভাষণে (প্রকাশ ১৯৭৮) FORTRAN-এর স্রষ্টা John Backus প্রথম “von Neumann bottleneck” কথাটি জনপ্রিয় করেন — তাঁর অভিযোগ ছিল, প্রোগ্রাম চিন্তা করে একটি একটি শব্দ CPU আর মেমরির মাঝে আনা-নেওয়ার ভাষায়। পরের দুই দশকে সমস্যাটি আরও তীব্র হয়: CPU-র গতি প্রতি বছর মোটামুটি ৫০% করে বাড়ছিল, আর DRAM-এর latency কমছিল অনেক ধীরে। ১৯৯৫ সালে William Wulf আর Sally McKee “Hitting the Memory Wall” প্রবন্ধে হিসাব করে দেখান, এভাবে চললে একসময় CPU-র প্রায় পুরো সময় মেমরির অপেক্ষাতেই যাবে।
হার্ডওয়্যারের উত্তর ছিল cache-এর স্তর বাড়ানো (L2, তারপর L3), prefetcher, out-of-order execution — যাতে একটি miss-এর অপেক্ষায় অন্য নির্দেশ চলতে পারে — আর hyper-threading. L1-এ নির্দেশ আর ডেটার আলাদা cache (যাকে অনেকে “modified Harvard” বলেন) একই সমস্যার উত্তর: দুই রাস্তা একসাথে খোলা।
OS-এর নকশাও বদলেছে। Linux-এর slab allocator (পর্ব ৩৮) kernel-এর বস্তুগুলো cache line-এর সাথে মিলিয়ে সাজায়; scheduler প্রসেসকে একই core-এ রাখতে চায় যাতে তার cache “গরম” থাকে; huge page (পর্ব ৩৮) TLB miss কমায়; NUMA-সচেতন allocator মেমরি কাছের socket-এ রাখে। প্রোগ্রামাররাও শিখেছেন — ডেটাবেস ইঞ্জিন column-store ব্যবহার করে, গেম ইঞ্জিন “data-oriented design” মেনে একই ধরনের ডেটা পাশাপাশি রাখে।
মূল উপলব্ধি: আধুনিক কম্পিউটারে গণনা সস্তা, ডেটা সরানো দামি। OS আর প্রোগ্রামের অনেক “চালাক” নকশার পেছনে আসল লক্ষ্য একটাই — ধীর স্তরে যাওয়া যত কম সম্ভব।
প্র ০২Timer interrupt ছাড়া কি multitasking সম্ভব? পুরোনো OS-গুলো কী করত, আর তাতে কী সমস্যা হতো?
সম্ভব, এবং বহু বছর এভাবেই চলেছে — নাম cooperative multitasking. এতে প্রতিটি প্রোগ্রাম নিজে থেকে মাঝে মাঝে OS-কে CPU ফিরিয়ে দেয় (“yield”), সাধারণত যখন সে ইনপুটের জন্য অপেক্ষা করে। Windows 3.x-এর ১৬-বিট অ্যাপ আর Mac OS X-এর আগের classic Mac OS এভাবে চলত। নকশাটি সহজ, আর প্রোগ্রাম জানে কখন তাকে থামানো হবে — তাই কিছু synchronization-এর ঝামেলাও কম।
সমস্যা হলো, পুরো ব্যবস্থাটি প্রতিটি প্রোগ্রামের ভদ্রতার ওপর নির্ভরশীল। একটি প্রোগ্রাম অসীম লুপে আটকে গেলে বা ভারী হিসাবের মাঝে yield করতে ভুলে গেলে পুরো কম্পিউটার জমে যেত — মাউস নড়ত, কিন্তু কিছুই সাড়া দিত না। অনেক ব্যবহারকারী তখন রিস্টার্ট ছাড়া উপায় পেতেন না।
Preemptive multitasking এই নির্ভরতা ভেঙে দেয়। Unix শুরু থেকেই timer-নির্ভর preemption ব্যবহার করত; Windows NT (১৯৯৩) আর Windows 95-এর ৩২-বিট অ্যাপ এবং ২০০১ সালের Mac OS X এটি সাধারণ ব্যবহারকারীর কাছে নিয়ে আসে। এখন একটি অ্যাপ আটকে গেলে শুধু সেটিই “Not Responding” হয়, বাকি সিস্টেম চলে।
আধুনিক মোড়টি আরও আকর্ষণীয়। নিয়মিত timer interrupt ব্যাটারি খরচ করে — CPU ঘুমাতে পারে না। তাই Linux ২০০৭ সালের ২.৬.২১ version থেকে “tickless” (NO_HZ) মোড আনে: কোনো কাজ না থাকলে CPU পরের দরকারি ঘটনা পর্যন্ত timer বন্ধ রাখে। অর্থাৎ timer এখন “নিয়মিত টিক” নয়, “দরকার মতো অ্যালার্ম”।
মূল উপলব্ধি: ভালো সিস্টেম কারও ভদ্রতার ওপর নির্ভর করে না। Timer interrupt হলো সেই হার্ডওয়্যার-নিশ্চয়তা, যা OS-কে চূড়ান্ত নিয়ন্ত্রণ দেয় — আর সেটি privileged হওয়াতেই কোনো অ্যাপ তা কেড়ে নিতে পারে না।
প্র ০৩Interrupt তো polling-এর চেয়ে কার্যকর — তাহলে আধুনিক দ্রুত নেটওয়ার্ক আর NVMe ড্রাইভার আবার polling-এ ফিরছে কেন?
কারণ প্রতিটি interrupt-এর একটি নির্দিষ্ট খরচ আছে: pipeline খালি করা, state সংরক্ষণ, mode বদল, ISR চালানো, cache-এর ডেটা নষ্ট হওয়া, ফেরা। ডিভাইস ধীর হলে এই খরচ নগণ্য, কারণ মাঝের সময়ে CPU অন্য কাজ করে। কিন্তু ১০ বা ১০০ Gbit/s নেটওয়ার্ক কার্ডে সেকেন্ডে লক্ষ লক্ষ প্যাকেট আসতে পারে — প্রতিটির জন্য interrupt হলে CPU-র পুরো সময় যায় interrupt সামলাতে, আসল কাজ হয় না।
১৯৯৬-৯৭ সালে Jeffrey Mogul আর K. K. Ramakrishnan এই অবস্থার নাম দেন receive livelock: সিস্টেম ব্যস্ত, কিন্তু কোনো অগ্রগতি নেই। তাঁদের প্রস্তাবিত কৌশলের ধাঁচেই Linux-এ NAPI তৈরি হয় — হালকা চাপে interrupt, ভারী চাপে interrupt বন্ধ করে polling, চাপ কমলে আবার interrupt. নেটওয়ার্ক কার্ডগুলোও interrupt coalescing করে — কয়েকটি প্যাকেট জমিয়ে একটি interrupt.
আরও চরম পথ হলো DPDK-র মতো লাইব্রেরি, যেখানে একটি পুরো core শুধু নেটওয়ার্ক কার্ড poll করে — বিদ্যুৎ বেশি খরচ হয়, কিন্তু latency সবচেয়ে কম। স্টোরেজে NVMe SSD এত দ্রুত যে interrupt-এর খরচ ডিভাইসের সময়ের তুলনায় আর নগণ্য নয়; তাই Linux-এর io_uring (২০১৯ থেকে) polling-ভিত্তিক মোডও দেয়।
অর্থাৎ নিয়মটি “interrupt সবসময় ভালো” নয়, বরং: ঘটনা বিরল হলে interrupt, ঘটনা অবিরাম হলে polling. কল সেন্টারের উদাহরণে — দিনে পাঁচটি কল এলে বেল যথেষ্ট, মিনিটে পঞ্চাশটি এলে একজনকে হেডসেট পরে সবসময় বসিয়ে রাখাই ভালো।
মূল উপলব্ধি: OS-এর কৌশল বেছে নেওয়া হয় কাজের চাপ মেপে। একই সিস্টেম চাপভেদে দুই কৌশলের মধ্যে বদলাতে পারে — hybrid নকশাই বাস্তবের উত্তর।
প্র ০৪x86-এ চারটি protection ring, অথচ Linux আর Windows ব্যবহার করে মাত্র দুটি। কেন? আর হার্ডওয়্যারের সুরক্ষা কি নিখুঁত?
চার রিংয়ের ধারণা এসেছে ১৯৬০-এর দশকের Multics থেকে, যেখানে আরও বেশি রিং ছিল আর ভাবনা ছিল — kernel-এর কেন্দ্র ring 0-এ, driver ring 1-এ, system service ring 2-এ, অ্যাপ ring 3-এ — যাতে একটি driver-এর বাগ kernel-কে না ছোঁয়। Intel 80286-এ চার রিং এনেছিল এই চিন্তা থেকেই।
বাস্তবে OS-নির্মাতারা মাঝের রিং প্রায় ব্যবহার করেননি, কয়েকটি কারণে। প্রথমত, portability: Unix আর Windows NT শুরু থেকেই একাধিক ধরনের CPU-তে চলার জন্য লেখা, আর অনেক CPU-তে (MIPS, Alpha, পরে ARM-এর প্রচলিত নকশা) কার্যত দুটি স্তরই ছিল। সবচেয়ে কম সাধারণ বৈশিষ্ট্যটিই ব্যবহার করা সহজ। দ্বিতীয়ত, x86-এর মাঝের রিংয়ের সুরক্ষা segmentation-এর সাথে জড়িত, আর paging-ভিত্তিক মেমরি-সুরক্ষা কার্যত শুধু “user” আর “supervisor” — এই দুই ভাগ চেনে। তৃতীয়ত, রিং বদলের খরচ আছে।
পরে নতুন স্তরের দরকার হয়েছে ঠিকই, কিন্তু অন্যভাবে এসেছে: ২০০৫-০৬ সালে Intel VT-x আর AMD-V hypervisor-এর জন্য একটি আলাদা মোড যোগ করে, যাকে অনেকে অনানুষ্ঠানিকভাবে “ring −1” বলেন (পর্ব ৫০)। ARM-এ এর সমতুল্য EL2.
আর সুরক্ষা নিখুঁত নয়। ২০১৮ সালে প্রকাশিত Meltdown আর Spectre দেখায়, CPU-র অনুমাননির্ভর (speculative) execution cache-এ চিহ্ন রেখে যেতে পারে, যা দিয়ে সুরক্ষিত মেমরির তথ্য পরোক্ষভাবে বোঝা সম্ভব। প্রতিরক্ষা হিসেবে OS-গুলো kernel আর user-এর page table আরও আলাদা করে (Linux-এ KPTI), আর CPU-নির্মাতারা microcode ও নতুন প্রজন্মের চিপে সংশোধন আনে। নিজের সিস্টেমে কোন প্রতিরক্ষা চালু আছে, Linux-এ তা দেখা যায় /sys/ ফোল্ডারে।
মূল উপলব্ধি: হার্ডওয়্যার যত বৈশিষ্ট্য দেয়, OS সব ব্যবহার করে না — portability আর সরলতা প্রায়ই জেতে। আর সুরক্ষার সীমারেখা শুধু নিয়মে নয়, বাস্তবায়নের খুঁটিনাটিতেও ভাঙতে পারে।
প্র ০৫“CPU-তে ৮টি core মানে প্রোগ্রাম ৮ গুণ দ্রুত” — এই ধারণা কোথায় ভুল, আর OS-কে এজন্য কী কী সামলাতে হয়?
২০০৫ সালে Herb Sutter “The Free Lunch Is Over” প্রবন্ধে লেখেন: এতদিন প্রোগ্রাম কিছু না বদলেই প্রতি বছর দ্রুত হতো, কারণ CPU-র ঘড়ি দ্রুত হচ্ছিল। তাপ আর বিদ্যুতের সীমায় সেই যুগ শেষ (Dennard scaling ভেঙে পড়া)। নতুন core-এর সুবিধা পেতে প্রোগ্রামকেই একাধিক থ্রেডে ভাগ হতে হবে।
কিন্তু ভাগ করলেও ৮ গুণ হয় না, কয়েকটি কারণে। এক, প্রোগ্রামের কিছু অংশ ধারাবাহিক — Amdahl-এর সূত্র বলে, ১০% অংশ ধারাবাহিক হলে অসীম core-এও সর্বোচ্চ ১০ গুণ। দুই, থ্রেডগুলোকে ভাগ করা ডেটায় lock নিতে হয়, আর lock-এর জন্য অপেক্ষা সময় খায়। তিন, cache coherence-এর খরচ: একটি শেয়ার করা counter-এ সব core লিখলে cache line-টি core থেকে core-এ ছুটতে থাকে; false sharing হলে আলাদা ভ্যারিয়েবলেও একই দুর্দশা। চার, সব core একই memory bus আর L3 ভাগ করে — মেমরি-ভারী কাজে bandwidth-ই সীমা হয়ে দাঁড়ায়। পাঁচ, hyper-threading-এর “logical CPU” আসলে অর্ধেক-ভাগ করা core.
OS-কেও নিজেকে বদলাতে হয়েছে। পুরোনো Linux-এ পুরো kernel-এর জন্য একটি বড় lock ছিল (“Big Kernel Lock”), যা ধাপে ধাপে সরিয়ে সূক্ষ্ম lock আনা হয় — শেষ চিহ্ন সরানো হয় ২০১১ সালের কাছাকাছি। Kernel-এর অনেক counter এখন per-CPU: প্রতিটি core নিজের কপিতে লেখে, পড়ার সময় যোগ করা হয় — coherence-এর ছোটাছুটি এড়াতে। Scheduler প্রতিটি core-এর আলাদা run queue রাখে আর মাঝে মাঝে ভার ভাগাভাগি করে (পর্ব ২০, ২১)। RCU-র মতো কৌশল পড়াকে প্রায় lock-মুক্ত করে (পর্ব ২৯)।
মূল উপলব্ধি: core বাড়ালে সম্ভাবনা বাড়ে, গতি নিজে থেকে বাড়ে না। সমান্তরাল গতির আসল শত্রু ভাগ করা ডেটা — আর OS-নকশার বড় অংশই হলো যতটা সম্ভব কম ভাগ করা।
অনুশীলন
প্রথমে খাতায় নিজে করুন, তারপর উত্তর মিলিয়ে নিন।
-
Effective access time (হাতে হিসাব): Cache access time ৫ ns, main memory ৮০ ns, hit ratio ০.৯৫. (ক) hierarchical রীতিতে EAT কত? (খ) simultaneous রীতিতে? (গ) hierarchical রীতিতে EAT ৭ ns বা কম রাখতে ন্যূনতম hit ratio কত লাগবে?
(ক) ০.৯৫ × ৫ + ০.০৫ × (৫ + ৮০) = ৪.৭৫ + ৪.২৫ = ৯ ns. (সরল রূপে: ৫ + ০.০৫ × ৮০ = ৯ ns.)
(খ) ০.৯৫ × ৫ + ০.০৫ × ৮০ = ৪.৭৫ + ৪ = ৮.৭৫ ns.
(গ) ৫ + (১ − h) × ৮০ ≤ ৭ ⟹ (১ − h) ≤ ২/৮০ = ০.০২৫ ⟹ h ≥ ০.৯৭৫. অর্থাৎ miss মাত্র ২.৫%-এ নামাতে হবে — ৫% থেকে অর্ধেক।
-
Fetch–decode–execute হাতে চালান: PC = 100, R1 = 0, Z = 0. মেমরি:
100: LOAD R1, [300],101: SUB R1, [301],102: JZ 108(Z = 1 হলে ১০৮-এ লাফ),103: …;300 = 9,301 = 9. প্রতিটি নির্দেশের fetch আর execute-এর পরে PC, R1, Z কত? শেষে পরের নির্দেশ কোন ঠিকানা থেকে আসবে?- LOAD fetch: PC = 101. Execute: R1 = 9 (Z ধরা হলো বদলায় না, 0).
- SUB fetch: PC = 102. Execute: R1 = 9 − 9 = 0, ফল শূন্য, তাই Z = 1.
- JZ fetch: PC = 103. Execute: Z = 1, তাই PC-তে 108 বসানো হলো।
পরের নির্দেশ আসবে ১০৮ থেকে, ১০৩ থেকে নয়। শিক্ষা: jump নির্দেশ আসলে শুধু PC-তে নতুন মান লেখে। আর flags register কেন context switch-এ সংরক্ষণ করতে হয়, তাও দেখা গেল — SUB আর JZ-এর মাঝে প্রসেস বদলে Z হারালে প্রোগ্রাম ভুল পথে যেত।
-
ফল অনুমান করুন:
static int a[4096][4096];ধরে তিনটি লুপ — (ক)for i, for j: s += a[i][j](খ)for j, for i: s += a[i][j](গ)for j, for i: s += a[j][i]. কোনগুলো দ্রুত, কোনটি ধীর? কেন?(ক) আর (গ) দ্রুত, (খ) ধীর। (গ)-তে লুপের ভ্যারিয়েবলের নাম বদলেছে, কিন্তু ভেতরের লুপের ভ্যারিয়েবল
iবসেছে শেষ সূচকে (a[j][i]), তাই মেমরিতে পরপর এগোনো হচ্ছে — row-major. (খ)-তে ভেতরের লুপ প্রথম সূচক বদলায়, তাই প্রতিবার ১৬ KB লাফ। নিয়মটি মনে রাখুন: C-তে সবচেয়ে ভেতরের লুপের ভ্যারিয়েবল যেন সবচেয়ে ডানের সূচক হয়। (Fortran আর MATLAB column-major — সেখানে নিয়মটি উল্টো।) -
নিজের মেশিনে interrupt গুনুন: Linux (বা Windows-এর WSL) হলে এমন একটি কমান্ড লিখুন, যা
/proc/interrupts-এর প্রথম কয়েকটি লাইন দুই সেকেন্ডের ব্যবধানে দুবার দেখায়। কোন লাইনের সংখ্যা সবচেয়ে দ্রুত বাড়ে? মাউস নাড়ালে বা কিছু ডাউনলোড করলে কী বদলায়?head -5 /proc/interrupts; sleep 2; head -5 /proc/interrupts # অথবা প্রতি সেকেন্ডে স্বয়ংক্রিয়ভাবে, বদলানো অংশ চিহ্নিত করে: watch -n1 -d 'head -25 /proc/interrupts'প্রতিটি কলাম একটি CPU, প্রতিটি সারি একটি interrupt-এর উৎস। x86-এ সাধারণত
LOC(local timer interrupt) সারিটি প্রতিটি CPU-তে নিয়মিত বাড়ে — timer interrupt-এর প্রমাণ (tickless kernel-এ অলস CPU-তে কম বাড়ে)। ডাউনলোড করলে নেটওয়ার্ক কার্ডের সারি (নামেeth,enp,iwlwifiইত্যাদি) বাড়বে, মাউস নাড়ালে USB বা i8042-এর সারি। ঠিক কোন নাম দেখবেন তা হার্ডওয়্যারভেদে আলাদা। Windows-এ এই তালিকা সরাসরি পাওয়া যায় না; Performance Monitor-এর “Interrupts/sec” কাউন্টার দিয়ে মোট হার দেখা যায়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি hardware interrupt সামলানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🔔 ডিভাইস controller interrupt লাইন সক্রিয় করে
- ⏹️ CPU চলতি নির্দেশটি শেষ করে
- 💾 হার্ডওয়্যার PC আর flags সংরক্ষণ করে, kernel mode-এ যায়
- 📋 Interrupt vector table দেখে ISR-এর ঠিকানায় লাফ দেয়
- 🛠️ ISR ডিভাইসের কাজ সেরে acknowledge করে
- ↩️ State ফিরিয়ে আগের প্রোগ্রামে ফেরা
CPU-র কোন register-এ পরের যে নির্দেশটি চালানো হবে তার ঠিকানা থাকে?
- Stack pointer (SP)
- Program counter (PC)
- Instruction register (IR)
- Flags / status register
Cache access time ২০ ns, main memory access time ২০০ ns, hit ratio ০.৮. Miss হলে আগে cache, তারপর memory দেখা হয় (hierarchical). Effective access time কত?
- ৫৬ ns
- ৪৪ ns
- ৬০ ns
- ৭৬ ns
একটি প্রোগ্রাম while(1){} লুপে আটকে আছে, কোনো system call করছে না। তবু OS অন্য প্রসেস চালাতে পারে প্রধানত কোন হার্ডওয়্যার-ব্যবস্থার কারণে?
- Timer interrupt
- DMA
- Cache memory
- System call
একটি প্রোগ্রাম শূন্য দিয়ে ভাগ করার নির্দেশ চালাল। CPU-র দৃষ্টিতে এটি কোন ধরনের ঘটনা?
- বাইরের ডিভাইসের hardware interrupt
- Polling
- DMA completion
- Synchronous exception (trap/fault)
DMA ব্যবহারের প্রধান সুবিধা কোনটি?
- ডিস্কের ঘোরার গতি বাড়ে
- CPU-কে প্রতিটি শব্দ কপি করতে হয় না, সে অন্য কাজ করতে পারে
- Interrupt-এর আর কোনো দরকার থাকে না
- User প্রোগ্রাম kernel ছাড়াই ডিভাইস চালাতে পারে
C-তে int a[1000][1000] array-র সব ঘর a[0][0], a[0][1], a[0][2], … ক্রমে পড়লে দ্রুত হয় মূলত কোন কারণে?
- Temporal locality — একই ঘর বারবার পড়া
- Cache coherence protocol
- Spatial locality — পাশাপাশি ঠিকানা, একই cache line
- Memory-mapped I/O
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- CPU-র চক্র: fetch → decode → execute; চলমান প্রোগ্রামের অবস্থা থাকে PC, SP, general register আর flags-এ — context switch এগুলোই সংরক্ষণ করে।
- User mode বনাম kernel mode: privileged instruction শুধু kernel-এ; mode বদলের দরজা কেবল interrupt, exception আর system call.
- Memory hierarchy-তে প্রতি ধাপে মোটামুটি দশ গুণ বা তার বেশি পার্থক্য; locality-র জোরে cache কাজ করে; AMAT = hit time + miss rate × miss penalty, আর EAT-এ hierarchical/simultaneous রীতি স্পষ্ট লিখুন।
- Interrupt asynchronous, trap/exception synchronous; vector table → ISR → ফেরা; timer interrupt OS-কে সবসময় নিয়ন্ত্রণ ফিরিয়ে দেয়।
- ডিভাইসের সাথে কথা হয় controller-এর register দিয়ে (MMIO বা port I/O); বড় ডেটা সরায় DMA; multicore-এ cache coherence কপিগুলো মেলায়, কিন্তু ভাগ করা ডেটার খরচ থেকে যায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৫ · System call — user mode থেকে kernel modeপরবর্তী পর্বTrap দিয়ে kernel-এ ঢোকা, system call table আর ফেরা।
- অপারেটিং সিস্টেম · কোর্স হোম৬৪টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- Computer Fundamentals · Memory hierarchyভিত্তিএকেবারে শুরু থেকে register, cache, RAM আর storage.
- C প্রোগ্রামিং · পারফরম্যান্স ও cacheহাতে-কলমেCache-বান্ধব C কোড লেখা আর মাপা।
- DBMS ও SQL · Storage internalsপাশের ট্র্যাকডেটাবেস কীভাবে ডিস্ক আর buffer pool-এর latency সামলায়।