Virtual Memory পরিচিতি — এক প্রোগ্রাম, এক বিভ্রম
Virtual Memory Intro
গত লেসনের শেষে একটা প্রশ্ন ঝুলিয়ে রাখা হয়েছিল — প্রতিটা process কীভাবে ভাবে সে পুরো machine-এর একমাত্র মালিক? এই লেসনে virtual memory-র মূল প্রতিশ্রুতি তিনটা — isolation, বেশি memory-র বিভ্রম, আর flexibility — ধারণাগতভাবে বোঝা, আর সেই বিভ্রম তৈরি করা হার্ডওয়্যার ইউনিট MMU-র সাথে পরিচিত হওয়া। প্রতিটা memory access কীভাবে ঠিক অনুবাদিত হয় তার বিস্তারিত যন্ত্রপাতি (page table) পরের লেসনের বিষয়।
আগে এটা বুঝি
গত লেসনের শেষে তিনটা সমস্যা নাম করে রাখা হয়েছিল, উত্তর না দিয়েই। আজ সেই তিনটার সাথে সরাসরি মুখোমুখি হব।
একটা ছোট চিন্তা-পরীক্ষা দিয়ে শুরু করি। আপনার মেশিনে একই প্রোগ্রাম দুইবার চালান — দুইটা আলাদা টার্মিনালে, দুইটা আলাদা process হিসেবে। দুইজনেই ধরুন malloc(100) কল করে একটা বাফার নেয়, আর তার ঠিকানা ছাপে। প্রায়ই দেখবেন দুইজনেই প্রায় একই সংখ্যা ছাপাচ্ছে — ধরুন 0x55a1b2c03010। দুইটা সম্পূর্ণ ভিন্ন process, ভিন্ন কাজ করছে, তবু একই ঠিকানা দাবি করছে। যদি সত্যিই দুইজনেই RAM-এর একই জায়গায় লিখত, একজনের লেখা মুহূর্তেই অন্যজনের ডেটা নষ্ট করে দিত — অথচ বাস্তবে দুইজনেই নিরাপদে, একে অপরের টেরও না পেয়ে কাজ করে যায়।
এই ঘটনাটা এই module-এর নিজস্ব driving question-এর ঠিক কেন্দ্রে বসে আছে — “আমার program একা মনে করে পুরো machine তার — এই বিভ্রম OS কীভাবে বানায়?” আজ পর্যন্ত আমরা process, scheduling, context switching নিয়ে অনেক কথা বলেছি, কিন্তু একটা মৌলিক প্রশ্ন এড়িয়ে গেছি: একটা process যখন &marker-এর মতো একটা ঠিকানা দেখে, সেই ঠিকানাটা আসলে কী? সরাসরি RAM-এর একটা physical স্লট? নাকি অন্য কিছু?
উত্তর: অন্য কিছু। আর সেই “অন্য কিছু”-র নাম virtual memory, আর আজকের লেসন তার প্রথম পরিচয়।
মূল ধারণা
তিনটা সমস্যা, একটা সমাধানের প্রতিশ্রুতি
কল্পনা করুন এমন একটা (কাল্পনিক, সরলীকৃত) OS যেখানে virtual memory বলে কিছু নেই — প্রতিটা process সরাসরি physical RAM ঠিকানা ব্যবহার করে, ঠিক যেমন computer-architecture/registers-pc-flags-এ single-program bare-metal মডেলে দেখেছিলাম। একটা mov instruction-এ যে ঠিকানা লেখা থাকে, সেটাই সরাসরি RAM-চিপের একটা তার-সংযোগে যায়। এই মডেলে তিনটা ভয়ংকর সমস্যা তৈরি হয়।
সমস্যা ১ — Isolation-এর অভাব
দুইটা process (ধরুন একটা ব্রাউজার, একটা টেক্সট এডিটর) একই সময়ে চলছে। কেউ একজন যদি একটা bug-এর কারণে ভুল ঠিকানায় লিখে ফেলে — বলুন 0x1000 — আর সেই ঠিকানাটা যদি সরাসরি physical RAM হয়, তাহলে দুইটা process একই ঠিকানা মানেই একই physical মেমরি স্লট ব্যবহার করছে। একটার bug অন্যটার ডেটা মুছে দিতে পারে, অথবা আরও খারাপ, kernel-এর নিজের মেমরিও লিখে ফেলতে পারে — একটা crashed ব্রাউজার-ট্যাব পুরো সিস্টেমকে অসাড় করে দিতে পারে। কোনো hardware-স্তরের বেড়া নেই যা একটা process-কে অন্যটার থেকে আলাদা রাখে, কারণ সবাই একই ঠিকানা-জগৎ শেয়ার করছে literally, রূপকভাবে না।
এটা শুধু bug-এর প্রশ্ন না, নিরাপত্তারও প্রশ্ন — একটা malicious process ইচ্ছাকৃতভাবে অন্য process-এর পাসওয়ার্ড বা encryption key যেই মেমরিতে বসে আছে সেই ঠিকানায় সরাসরি পড়তে পারত।
সমস্যা ২ — বেশি memory-র বিভ্রম
ধরুন মেশিনে ৮GB RAM আছে, আর ১০টা process চলছে, প্রতিটাই ভাবছে তার ২GB দরকার। সরাসরি physical ঠিকানা ব্যবহার করলে দুইটা প্রশ্ন সমাধান করতে হবে সবার আগে: কে কোন ২GB পাবে (আগে থেকে ভাগ করে রাখতে হবে, প্রতিটা process শুরুর আগেই), আর সব মিলিয়ে ২০GB চাহিদা ৮GB RAM-এ কীভাবে আঁটবে। বাস্তবে বেশিরভাগ process তাদের বরাদ্দকৃত মেমরির একটা ছোট অংশই সক্রিয়ভাবে ব্যবহার করে যেকোনো মুহূর্তে — কিন্তু bare-metal মডেলে এটা কাজে লাগানোর কোনো উপায় নেই, কারণ ঠিকানা মানেই সরাসরি RAM-এর একটা স্থায়ী দখল।
এখানেই এই module-এর driving question-টা আক্ষরিক অর্থে সত্যি হয়ে ওঠে — প্রতিটা process-কে ০ থেকে শুরু করে একটা বিশাল, ধারাবাহিক ঠিকানা-জগৎ দেওয়া যায়, যেন সে একাই পুরো machine-এর মালিক, যদিও physical RAM সীমিত আর সবার মধ্যে ভাগ হচ্ছে। প্রতিটা process ভাবে তার একার জন্য (আজকের 64-বিট মেশিনে) কয়েক টেরাবাইট ঠিকানা-জগৎ আছে — physical RAM যতই ৮GB বা ১৬GB হোক না কেন।
সমস্যা ৩ — Flexibility-র অভাব
একটা process যখন চালু হয়, তার code, heap, stack-এর জন্য কতটুকু জায়গা লাগবে তা আগে থেকে নিশ্চিতভাবে বলা কঠিন — heap বাড়তে পারে, stack বাড়তে পারে, নতুন লাইব্রেরি dlopen() দিয়ে মাঝপথে লোড হতে পারে। Bare-metal মডেলে একবার একটা ধারাবাহিক physical অঞ্চল বরাদ্দ করে দিলে, পাশের অঞ্চল অন্য কেউ দখল করে থাকলে বাড়ানো অসম্ভব — ঠিক paging-and-page-tables.mdx-এ যে external fragmentation সমস্যা দেখব, তার মূল কারণ।
আরও একটা নমনীয়তা দরকার: একটা process-এর ঠিকানা-জগতের একটা অংশ হয়তো এখনো RAM-এই নেই — একটা বড় ফাইল mmap করা হয়েছে, কিন্তু এখনো disk থেকে পড়া হয়নি। প্রোগ্রাম একটা ঠিকানা ব্যবহার করতে চাইলে তখনই সেই অংশটা RAM-এ আনা যেতে পারে, প্রয়োজনের আগে না — এই কৌশলটার নাম demand paging, যা page-faults-and-demand-paging.mdx-এ বিস্তারিত দেখব। কিন্তু সেটা সম্ভব হওয়ার পূর্বশর্ত হলো: প্রোগ্রামের ব্যবহৃত ঠিকানা আর RAM-এর প্রকৃত অবস্থান — এই দুইটার মধ্যে একটা পরোক্ষ স্তর (indirection) থাকতে হবে।
Virtual address বনাম physical address
এখন থেকে দুইটা আলাদা ধরনের ঠিকানা স্পষ্টভাবে আলাদা করে ভাবতে হবে।
Virtual address — যা একটা process নিজে ব্যবহার করে। প্রতিটা mov, প্রতিটা pointer, প্রতিটা function call-এর রিটার্ন ঠিকানা — সবই virtual address। প্রোগ্রামের দৃষ্টিকোণ থেকে এটাই “the” address, একমাত্র বাস্তবতা। প্রোগ্রাম কখনো জানে না (আর জানার দরকারও নেই) তার virtual address আসলে RAM-এর কোথায় বসে আছে।
Physical address — RAM চিপের ভেতরে একটা নির্দিষ্ট বিট-প্যাটার্নের প্রকৃত অবস্থান, digital-logic/memory-sram-dram-এ যে row/column decode দেখেছিলাম তার শেষ গন্তব্য। এটাই সেই ঠিকানা যা আসলে memory bus-এ যায়, DRAM চিপকে বলে “এই ঠিকানা থেকে পড়ো” বা “এখানে লেখো”।
একটা process যা “ঠিকানা” বলে জানে, সেটা সবসময় virtual। প্রোগ্রামের ভেতর থেকে physical address দেখার বা জানার কোনো সাধারণ উপায়ই নেই (ব্যতিক্রম: বিশেষ debugging টুল, যা এই লেসনের experiment-এ ব্যবহার করব)। এই আলাদা করাটাই সেই “একজন একাই মালিক” বিভ্রমের ভিত্তি — process নিজের virtual ঠিকানা-জগতের ভেতরে বাস করে, physical RAM-এর বাস্তবতা সম্পূর্ণ আড়ালে থাকে।
MMU — অনুবাদ করে এমন হার্ডওয়্যার
তাহলে virtual address কীভাবে physical address-এ পরিণত হয়? উত্তর: CPU-র ভেতরে বসে থাকা একটা বিশেষায়িত হার্ডওয়্যার ইউনিট, নাম MMU (Memory Management Unit)। এটা register file আর L1 cache-এর মাঝখানে বসে — computer-architecture/memory-hierarchy-তে যে path memory access নেয় তার একদম শুরুতে।
কাজটা ধারণাগতভাবে সহজ বলে ফেলা যায়: প্রতিটা একক memory access-এ, CPU যে virtual address তৈরি করে, MMU সেটাকে একটা physical address-এ অনুবাদ করে, তারপরই সেই physical address cache/RAM-এ পাঠানো হয়। এই পুরো প্রক্রিয়াটা প্রোগ্রামের কাছে সম্পূর্ণ অদৃশ্য — কোনো instruction নেই যা বলে “এখন অনুবাদ করো”, এটা প্রতিটা লোড, প্রতিটা স্টোর, প্রতিটা instruction-fetch-এর জন্য স্বয়ংক্রিয়ভাবে ঘটে।
অনুবাদের নিয়ম কে ঠিক করে? OS। প্রতিটা process-এর জন্য OS একটা মাপিং তৈরি করে রাখে — কোন virtual ঠিকানা কোন physical ঠিকানায় যাবে। MMU সেই মাপিং অনুসরণ করে, কিন্তু নিজে সিদ্ধান্ত নেয় না। এই মাপিং ঠিক কীভাবে সংরক্ষিত থাকে (একটা data structure, নাম page table) — সেটাই পরের লেসনের সম্পূর্ণ বিষয়, এখানে আমরা শুধু এইটুকু বুঝব: একটা মাপিং আছে, OS সেটা নিয়ন্ত্রণ করে, MMU সেটা প্রয়োগ করে।
Process A Process B
virtual address 0x1000 virtual address 0x1000
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ CPU core A │ │ CPU core (একই)│
│ (running A) │ │ (running B) │
└────────┬─────────┘ └────────┬────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────┐
│ MMU │
│ A-এর mapping ব্যবহার করে B-এর mapping ব্যবহার করে │
│ (A চলাকালীন সক্রিয়) (B চলাকালীন সক্রিয়) │
└────────┬─────────────────────────────────┬──────────┘
│ │
▼ ▼
physical 0x7a2b3000 physical 0x1f4c9000
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ RAM-এর একটা │ │ RAM-এর সম্পূর্ণ │
│ নির্দিষ্ট স্লট │ │ ভিন্ন স্লট │
│ (শুধু A-র) │ │ (শুধু B-র) │
└─────────────────┘ └─────────────────┘দুইজনেই 0x1000 লেখে, দুইজনেই ভাবে সেটাই “the” ঠিকানা — কিন্তু RAM-এ দুইটা সম্পূর্ণ ভিন্ন জায়গায় গিয়ে পড়ে। A কখনো B-র physical ঠিকানায় পৌঁছাতে পারবে না, কারণ A-র মাপিং-এ সেই ঠিকানার কোনো entry-ই নেই (অথবা থাকলেও access নিষিদ্ধ, পরের লেসনের permission bit-এ ফিরে আসব)। এটাই isolation সমস্যার সমাধান — শুধু নীতি না, একটা concrete hardware-প্রয়োগিত বেড়া।
Overcommit-এর ধারণাটাও এখান থেকেই আসে। যেহেতু মাপিং OS-এর নিয়ন্ত্রণে, OS প্রতিটা process-কে একটা বিশাল virtual ঠিকানা-জগৎ (যেমন x86-64-এ কয়েক টেরাবাইট) দিতে পারে — যদিও প্রতিটা মাপিং-এর পেছনে physical RAM থাকা বাধ্যতামূলক না, শুধু ব্যবহৃত অংশের জন্য দরকার। ফলে ১০টা process প্রতিটাই “নিজের” ২GB ভাবতে পারে, অথচ সব মিলিয়ে physical RAM ৮GB-ই যথেষ্ট, যতক্ষণ সবাই একসাথে তাদের পুরোটা সক্রিয়ভাবে ব্যবহার না করে (আর না করলে কী হয় — swapping — সেটাও page-faults-and-demand-paging.mdx-এর বিষয়)।
Flexibility-ও এখান থেকেই আসে। একটা process-এর কাছে ঠিকানা-জগৎ ধারাবাহিক দেখায় (0x1000 থেকে 0x2000 পর্যন্ত একটানা), কিন্তু মাপিং-এর কারণে এর পেছনের physical RAM scattered থাকতে পারে — এমনকি অংশবিশেষ RAM-এ নাই-ই থাকতে পারে, যতক্ষণ না প্রথমবার ব্যবহৃত হয়। প্রোগ্রামারের চোখে একটা পরিষ্কার, সরল, ধারাবাহিক জগৎ — নিচে যতই এলোমেলো বাস্তবতা থাকুক না কেন। এই “দেখতে সরল, ভেতরে জটিল” প্যাটার্নটাই abstraction-এর সংজ্ঞা, ঠিক এই module-এর description-এ যেমন বলা আছে।
কেন hardware — সফটওয়্যার-ভিত্তিক অনুবাদ কেন যথেষ্ট না
একটা স্বাভাবিক প্রশ্ন: এই অনুবাদটা কি OS নিজেই, প্রতিটা মেমরি-access-এ, software দিয়ে করতে পারত না? প্রযুক্তিগতভাবে সম্ভব, কিন্তু ব্যবহারিকভাবে ধ্বংসাত্মক ধীর।
computer-architecture/cache-organization-এ আমরা দেখেছিলাম কেন cache lookup-এর মতো speed-critical path-এ প্রতিটা ন্যানোসেকেন্ড গুরুত্বপূর্ণ — একটা L1 cache hit কয়েক cycle-এ শেষ হয়, আর পুরো CPU pipeline-এর গতি সরাসরি এই লেটেন্সির উপর নির্ভর করে। এখন চিন্তা করুন: একটা প্রোগ্রাম প্রতি সেকেন্ডে কোটি কোটি বার memory access করে — প্রতিটা load, প্রতিটা store, প্রতিটা instruction fetch। যদি প্রতিটা access-এর আগে OS-কে একটা function কল করে, একটা lookup table চেক করে, ফলাফল ফেরত পাঠাতে হতো — প্রতিটা memory access একটা function-call-এর খরচ বহন করত, যা একটা single cache access-এর চেয়ে শত-হাজার গুণ ধীর। পুরো CPU-র গতি কমে যেত মূলত মেমরি-অনুবাদের অপেক্ষায়, ঠিক সেই speed-critical path-এই যেখানে গতি সবচেয়ে বেশি জরুরি।
তাই অনুবাদটা hardware-এ থাকতে হবে — সরাসরি সেই একই সিলিকনে, cache-এর ঠিক পাশে, যাতে প্রতিটা access-এ কয়েক cycle-এর বেশি খরচ না হয়। MMU এই কাজটাই করে: প্রতিটা memory access-এ, pipeline-এর মধ্যেই, cache-access-এর সমান গতিতে অনুবাদ সম্পন্ন করে।
কিন্তু এখানে একটা ধাঁধা তৈরি হয়। OS মাপিং নিয়ন্ত্রণ করে, প্রতিটা process-এর জন্য আলাদা মাপিং সেট করে, mmap/brk-এর মতো syscall-এ মাপিং বদলায়। অথচ প্রতিটা access-এ MMU-কেই সেই মাপিং hardware-গতিতে প্রয়োগ করতে হয়, software-এর হস্তক্ষেপ ছাড়া। সমাধান: OS মেমরিতে একটা data structure বানিয়ে রাখে (মাপিং-এর টেবিল), আর একটা বিশেষ CPU register সবসময় বর্তমান process-এর টেবিলের দিকে নির্দেশ করে। MMU সরাসরি সেই টেবিল থেকে পড়ে, hardware-গতিতে — OS শুধু টেবিলটা আগে থেকে সাজিয়ে রাখে, প্রতিটা access-এ অংশ নেয় না।
এই টেবিলের নাম page table, আর সেই “টেবিল থেকে সরাসরি পড়া”-র প্রক্রিয়াটা — কতগুলো ধাপ লাগে, কীভাবে সংগঠিত, কেন এত দ্রুত — সেটাই পরের লেসনের সম্পূর্ণ বিষয়। এখানে শুধু এইটুকু মনে রাখুন: OS নিয়ন্ত্রণ করে, hardware প্রয়োগ করে — এই ভাগাভাগিই virtual memory-কে একইসাথে নমনীয় (software-নিয়ন্ত্রিত) আর দ্রুত (hardware-বাস্তবায়িত) রাখে।
ভেতরে কী ঘটছে
একটা memory access-এর জীবনচক্র — উচ্চ-স্তরে
গত কয়েকটা লেসনে LayerTrace দিয়ে দেখেছি কীভাবে একটা উচ্চ-স্তরের কাজ একের পর এক স্তর নেমে হার্ডওয়্যারে পৌঁছায়। আজকের বিষয়েও একই প্যাটার্ন — একটা সাধারণ mov instruction কার্যকর হওয়ার সময় ঠিক কোন কোন স্তর দিয়ে যায়, তা এখানে ধারণাগতভাবে দেখানো হলো (প্রতিটা ধাপের ভেতরের বিস্তারিত যন্ত্রপাতি পরের লেসনগুলোর বিষয়)।
- CPU একটা virtual address তৈরি করেrbx রেজিস্টারে যা আছে সেটাই একটা virtual address — process-এর নিজস্ব ঠিকানা-জগতের একটা সংখ্যা
- MMU সেই ঠিকানা ধরে অনুবাদ শুরু করেবর্তমান process-এর মাপিং (এখনো "কীভাবে" জানি না) ব্যবহার করে
- অনুমতি যাচাইএই process কি এই ঠিকানা পড়তে পারে? mapping না থাকলে বা নিষিদ্ধ হলে — page fault (পরের লেসনগুলোর বিষয়)
- একটা physical address বেরিয়ে আসেঅনুবাদ শেষ — এখন থেকে এটাই RAM-এর প্রকৃত ঠিকানা
- Cache hierarchy-তে যায়L1 → L2 → L3 → DRAM, memory-hierarchy লেসনের চেনা পথ
- ডেটা `rax`-এ ফিরে আসেপ্রোগ্রামের কাছে পুরো প্রক্রিয়াটা একটা একক, তাৎক্ষণিক instruction মনে হয়
লক্ষ্য করার মতো বিষয়: ধাপ ১-৩ (virtual address তৈরি, MMU অনুবাদ, অনুমতি যাচাই) প্রতিটা memory access-এর জন্য ঘটে, ব্যতিক্রম নেই — instruction fetch, data load, data store, স্ট্যাক push সব। প্রোগ্রামের দৃষ্টিকোণ থেকে ধাপ ২-৩ অস্তিত্বহীন; সে শুধু ধাপ ১ (একটা ঠিকানা চাওয়া) আর ধাপ ৬ (ফলাফল পাওয়া) দেখে। মাঝের অনুবাদটা সম্পূর্ণ স্বচ্ছ (transparent) — এই স্বচ্ছতাই virtual memory-র সবচেয়ে বড় ডিজাইন-অর্জন।
উদাহরণ
দুইটা process, একই কোড, ভিন্ন ফলাফল
একটা concrete উদাহরণ দিয়ে ধারণাটা মিলিয়ে দেখি। ধরুন নিচের C প্রোগ্রামটা দুইবার চালানো হলো, দুইটা ভিন্ন টার্মিনালে:
#include <stdio.h>
int global_var = 100;
int main(void) {
printf("virtual address: %p\n", (void *)&global_var);
printf("value: %d\n", global_var);
global_var += 1;
printf("new value: %d\n", global_var);
getchar(); // থামিয়ে রাখা, যাতে দুইটা একসাথে চলতে থাকে
return 0;
}দুইটা instance একসাথে চললে, সাধারণ ফলাফল:
টার্মিনাল ১:
virtual address: 0x55e2a1b04018
value: 100
new value: 101
টার্মিনাল ২:
virtual address: 0x55e2a1b04018
value: 100
new value: 101দুইটাই হুবহু একই virtual address ছাপায় (কারণ দুইটাই একই বাইনারি, একইভাবে load হয়েছে, আর ASLR বন্ধ থাকলে বা একই random offset পেলে)। দুইজনেই স্বাধীনভাবে 101-এ পৌঁছায় — কেউ কারো += 1-এর প্রভাব দেখে না। যদি সত্যিই একটাই physical মেমরি স্লট শেয়ার হতো, দ্বিতীয় প্রোগ্রামটা 101 না দেখে যেই মানই আগের প্রোগ্রাম শেষবার লিখেছিল সেটা দেখত — সম্ভবত race condition-এ ভরা একটা অপ্রত্যাশিত ফলাফল।
এখানেই MMU-র ভূমিকা concrete হয়ে ওঠে। দুইজনেরই virtual address 0x55e2a1b04018 — কিন্তু OS প্রতিটা process তৈরির সময় তার নিজস্ব, স্বাধীন মাপিং সেট করে দেয়। Process ১-এর জন্য 0x55e2a1b04018 হয়তো physical 0x3a01018-এ যায়; process ২-এর জন্য একই virtual ঠিকানা physical 0x5c02018-এ যায় — সম্পূর্ণ ভিন্ন RAM স্লট। দুইজনেই “নিজের” global_var লিখছে ভেবে সন্তুষ্ট, বাস্তবে দুইটা সম্পূর্ণ ভিন্ন physical মেমরি লোকেশন ব্যবহার করছে।
এই একই যুক্তি ব্যাখ্যা করে কেন fork()-এর পরে (fork-exec-wait.mdx-এ যা দেখেছি) parent আর child উভয়েই একই virtual ঠিকানায় নিজেদের ভেরিয়েবল দেখে, অথচ একজনের পরিবর্তন অন্যজনের কপিতে প্রতিফলিত হয় না (copy-on-write-এর বিস্তারিত প্রক্রিয়া অবশ্য পরের লেসনগুলোর বিষয়, বিশেষ করে PTE-র বিটে) — parent আর child-এর virtual address একই থাকলেও, তাদের মাপিং আলাদা physical পাতায় নির্দেশ করে।
নিজে চালিয়ে দেখুন
দুইটা process-এর একই virtual address, ভিন্ন physical বিষয়বস্তু — চোখে দেখা
উপরের C প্রোগ্রামটা marker.c নামে সেভ করুন আর কম্পাইল করুন:
gcc -O0 -no-pie -o marker marker.c(-no-pie দেওয়া হলো যাতে ASLR দুইটা রানের মধ্যে ভিন্ন base address না বেছে নেয় — তাহলে ভার্চুয়াল ঠিকানাও মিলে যাবে, তুলনাটা আরও পরিষ্কার হবে।)
দুইটা টার্মিনালে আলাদা করে চালান, দুইজনকেই getchar()-এ আটকে রাখুন:
./markerএখন উভয় প্রোগ্রামের virtual address মিলে যাওয়া নিশ্চিত করুন (দুইটাতেই একই সংখ্যা ছাপা উচিত)। এবার তৃতীয় একটা টার্মিনালে, প্রতিটা প্রোসেসের নিজস্ব address space mapping দেখুন:
ps aux | grep marker # দুইটা PID বের করুন
cat /proc/<PID_1>/maps | grep -A1 marker
cat /proc/<PID_2>/maps | grep -A1 markerসাধারণ ফলাফল — দুইটা PID-র জন্যই /proc/PID/maps-এ একই virtual range (যেমন 0x55e2a1b04000-0x55e2a1b05000) দেখাবে data segment হিসেবে — সম্পূর্ণ আলাদা process হওয়া সত্ত্বেও একই ঠিকানা-পরিসীমা claim করছে দুইজনেই। এখন দেখুন সেটা আসলে আলাদা physical মেমরিতে যায় কিনা — pagemap ব্যবহার করে (root লাগতে পারে):
# একটা ছোট সহায়ক স্ক্রিপ্ট — virtual address-কে physical frame-এ অনুবাদ করে
sudo python3 -c "
import struct, sys
def get_pfn(pid, vaddr):
page_size = 4096
with open(f'/proc/{pid}/pagemap', 'rb') as f:
f.seek((vaddr // page_size) * 8)
entry = struct.unpack('Q', f.read(8))[0]
if not (entry & (1 << 63)):
return None # present না
return entry & ((1 << 55) - 1)
pid = int(sys.argv[1])
vaddr = int(sys.argv[2], 16)
pfn = get_pfn(pid, vaddr)
print(f'PID {pid}, virtual 0x{vaddr:x} -> physical frame {pfn}')
" <PID_1> 0x55e2a1b04018
sudo python3 -c "..." <PID_2> 0x55e2a1b04018 # একই স্ক্রিপ্ট, PID_2 দিয়েসাধারণ ফলাফল:
PID 20142, virtual 0x55e2a1b04018 -> physical frame 245891
PID 20143, virtual 0x55e2a1b04018 -> physical frame 198234একই virtual address, দুইটা সম্পূর্ণ ভিন্ন physical frame number। এটাই এই লেসনের কেন্দ্রীয় দাবির প্রত্যক্ষ প্রমাণ — MMU সত্যিই প্রতি-process ভিত্তিতে ভিন্ন মাপিং ব্যবহার করছে, শুধু ধারণা না। (pagemap-এর ৬৪-বিট entry ফরম্যাট আর PFN বিটগুলোর পুরো বিস্তারিত ব্যাখ্যা paging-and-page-tables.mdx-এর experiment-এ আসবে — আজ শুধু এইটুকু নিশ্চিত করাই যথেষ্ট যে দুইটা সংখ্যা আলাদা।)
উপরের উদাহরণের দাবিটা তত্ত্ব না — একই virtual address সত্যিই দুইটা ভিন্ন, স্বাধীন physical মেমরি লোকেশনে যায়, নিজের মেশিনে যাচাইযোগ্য।
নিজে বানান
নিজের 'address space visualizer' — /proc/PID/maps থেকে একটা প্রোসেসের ভার্চুয়াল ঠিকানা-জগৎ ছবি করা
- একটা চলমান প্রোসেসের PID নিন (নিজের marker প্রোগ্রাম, অথবা bash/firefox যেকোনো কিছু)
- /proc/PID/maps পড়ে প্রতিটা mapping-এর virtual range, permission (r/w/x), আর কী map করা হয়েছে (ফাইল, [heap], [stack], anonymous) বের করুন
- প্রতিটা mapping-এর আকার (বাইটে) হিসাব করে একটা লগ-স্কেল বার-চার্ট আকারে টার্মিনালে ছাপুন — কোন অংশ সবচেয়ে বড়, কোনটা ছোট
- (ঐচ্ছিক) দুইটা ভিন্ন প্রোসেসের mapping পাশাপাশি তুলনা করুন — একই বাইনারির দুইটা instance-এর mapping কতটা মিল, কতটা আলাদা
#!/usr/bin/env python3
import sys
def parse_maps(pid):
mappings = []
with open(f"/proc/{pid}/maps") as f:
for line in f:
parts = line.split()
addr_range = parts[0]
perms = parts[1]
path = parts[5] if len(parts) > 5 else "[anonymous]"
start_s, end_s = addr_range.split('-')
start, end = int(start_s, 16), int(end_s, 16)
mappings.append((start, end, perms, path))
return mappings
def human_size(n):
for unit in ['B', 'KB', 'MB', 'GB']:
if n < 1024:
return f"{n:.1f}{unit}"
n /= 1024
return f"{n:.1f}TB"
def main():
pid = sys.argv[1]
mappings = parse_maps(pid)
total = sum(end - start for start, end, _, _ in mappings)
print(f"PID {pid} — মোট {len(mappings)}টা mapping, সর্বমোট {human_size(total)}\n")
print(f"{'virtual range':<28}{'perms':<7}{'size':>10} path")
print("-" * 70)
for start, end, perms, path in mappings:
size = end - start
bar_len = max(1, int(size / total * 30)) if total else 1
bar = '#' * bar_len
print(f"0x{start:012x}-0x{end:012x} {perms} {human_size(size):>8} {path}")
if size / total > 0.02: # শুধু উল্লেখযোগ্য অংশের জন্য bar
print(f"{'':<28}{'':<7}{'':>10} {bar}")
if __name__ == "__main__":
main()python3 addr_space.py $(pgrep -n bash)প্রত্যাশিত ধরনের আউটপুট:
PID 20142 — মোট 24টা mapping, সর্বমোট 4.2MB
virtual range perms size path
----------------------------------------------------------------------
0x000055e2a1b00000-0x000055e2a1b04000 r-xp 16.0KB /usr/bin/bash
0x000055e2a1d05000-0x000055e2a1e89000 rw-p 1.5MB [heap]
##########
0x00007f3a12000000-0x00007f3a12021000 r--p 132.0KB /usr/lib/libc.so.6
0x00007fffb2a01000-0x00007fffb2a22000 rw-p 132.0KB [stack]বাস্তব সিস্টেমে
Virtual memory যেখানে সরাসরি চোখে পড়ে
Docker/container isolation-এর ভিত্তি। একটা container-এর ভেতরের প্রোসেস host-এর অন্য container বা host নিজের memory একদম দেখতেই পায় না — এই isolation মূলত সেই একই MMU-ভিত্তিক প্রতি-process মাপিং-এর সম্প্রসারণ, namespaces/cgroups-এর (এই module-এর শেষের দিকের বিষয়) সাথে মিলিয়ে। Container আসলে জাদু কিছু না — শুধু OS-এর virtual memory isolation-কে আরও কড়াকড়িভাবে ব্যবহার করা।
Browser process-per-tab আর্কিটেকচার। Chrome-এর মতো ব্রাউজার প্রতিটা ট্যাবকে আলাদা OS process হিসেবে চালায় ঠিক এই isolation guarantee-র জন্য — একটা ট্যাবের compromised JavaScript engine অন্য ট্যাবের memory (যেমন ব্যাংকিং সেশনের কুকি) স্পর্শ করতে পারে না, কারণ virtual memory-স্তরেই তার কোনো মাপিং নেই সেই দিকে।
Overcommit আর OOM killer। Linux ডিফল্টভাবে “overcommit” করে — একাধিক process মিলিয়ে physical RAM-এর চেয়ে বেশি virtual memory বরাদ্দ দিতে দেয়, কারণ ধরে নেওয়া হয় সবাই একসাথে তাদের পুরোটা ব্যবহার করবে না। যখন এই ধারণা ভুল প্রমাণিত হয় (সত্যিই সবাই একসাথে বেশি ব্যবহার শুরু করে), kernel-এর OOM killer সবচেয়ে “খারাপ” process বেছে বন্ধ করে দেয় — এই পুরো ব্যবস্থাটাই সম্ভব শুধু virtual memory-র কারণে, কারণ বরাদ্দ (allocation) আর প্রকৃত ব্যবহার (physical backing) দুইটা আলাদা জিনিস।
mmap দিয়ে ফাইল-শেয়ারিং, কোনো কপি ছাড়াই। দুইটা আলাদা process একই shared library (যেমন libc.so) লোড করলে, প্রতিটার virtual address-এ সেই লাইব্রেরির জন্য আলাদা জায়গা থাকলেও, physical RAM-এ একটাই কপি থাকে — দুইজনের মাপিং একই physical frame-এ নির্দেশ করে (read-only অংশের জন্য)। এটাই কীভাবে ১০০টা প্রোসেস একই libc লোড করেও physical RAM-এ একাধিকবার সেটা কপি হয় না।
Emulator ও virtualization-এ nested translation। একটা virtual machine-এর ভেতরে আরেকটা OS চললে, guest OS নিজেই তার প্রোসেসগুলোর জন্য virtual-to-”physical” মাপিং করে, কিন্তু সেই “physical” আসলে host-এর দৃষ্টিতে আরেকটা virtual layer (নেস্টেড paging/EPT/NPT hardware সাপোর্ট দিয়ে ত্বরান্বিত) — Level 13-এর virtualization module-এ এই দুই-স্তরের অনুবাদ বিস্তারিত দেখব, কিন্তু ভিত্তিটা আজকের এই একই MMU ধারণা, শুধু স্তরে স্তরে সাজানো।
Debugger-এর breakpoint বসানো। gdb যখন একটা প্রোসেসে breakpoint বসায়, সে সেই প্রোসেসের virtual address-এ একটা instruction বদলে দেয় — নিজের (debugger-এর) প্রোসেসের মেমরি না ছুঁয়ে। এটা সম্ভব কারণ ptrace() syscall debugger-কে অন্য একটা process-এর virtual address space-এ (তার নিজের মাপিং দিয়ে) পড়া/লেখার বিশেষ অনুমতি দেয় — সাধারণ isolation-এর একটা সচেতন, নিয়ন্ত্রিত ব্যতিক্রম।
যে ভুলগুলো সবাই করে
“Virtual memory মানে হার্ড ডিস্ককে RAM-এর মতো ব্যবহার করা (swap-এর আরেক নাম)।”
এটা সম্ভবত সবচেয়ে সাধারণ ভুল ধারণা, বিশেষত যারা “virtual memory” শব্দটা প্রথম Windows-এর “virtual memory settings” মেনু থেকে শুনেছেন। Swap শুধু virtual memory-র একটা সম্ভাব্য প্রয়োগ, পুরো ধারণাটার সংজ্ঞা না।
Virtual memory-র মূল ধারণা হলো ঠিকানা-অনুবাদের স্তর — একটা process যে ঠিকানা ব্যবহার করে (virtual) আর RAM-এ যেখানে সেটা প্রকৃতপক্ষে থাকে (physical), এই দুইয়ের মধ্যে পার্থক্য। এই ধারণাটা সম্পূর্ণভাবে অর্থবহ এমনকি একটা মেশিনে যেখানে swap সম্পূর্ণ বন্ধ, কোনো disk-ব্যাকিং নেই। এমন একটা মেশিনেও প্রতিটা process আলাদা isolation পায়, নিজের flat address space পায়, MMU প্রতিটা access অনুবাদ করে — শুধু “RAM ফুরিয়ে গেলে disk-এ পাঠানো”-র অংশটা অনুপস্থিত। Swap virtual memory-র একটা সুবিধা, তার সংজ্ঞা না — এই লেসনের তিনটা সমস্যার মধ্যে “overcommit” অংশটার সাথে swap সম্পর্কিত, কিন্তু isolation বা flexibility অংশ দুটোর সাথে swap-এর কোনো সম্পর্কই নেই।
“প্রোগ্রাম চাইলে চেষ্টা করে অন্য process-এর 'physical' মেমরি ঠিকানায় সরাসরি পৌঁছাতে পারে, শুধু চেষ্টা করেনি।”
ভুল — এটা কোনো “চেষ্টা করলে হতো” প্রশ্ন না, এটা কাঠামোগতভাবে অসম্ভব সাধারণ userspace কোড থেকে। একটা প্রোগ্রাম যা লেখে বা পড়ে তার সবটাই virtual address — physical address তৈরি করা বা সরাসরি ব্যবহার করার কোনো instruction userspace কোডের কাছে নেই। এমনকি যদি একটা প্রোগ্রাম “physical ঠিকানা” ভেবে একটা সংখ্যা লেখে, MMU সেটাকেও একটা virtual address হিসেবেই পড়বে আর নিজের মাপিং দিয়ে অনুবাদ করবে — সেই সংখ্যাটা কখনো সরাসরি মেমরি বাসে যাবে না।
শুধু kernel mode-এ (আর সেখানেও নির্দিষ্ট, নিয়ন্ত্রিত পথে, যেমন device driver-এর DMA setup) সরাসরি physical address নিয়ে কাজ করা হয়, আর সেটাও একটা privileged প্রক্রিয়া, kernel-and-user-space.mdx-এর privilege ring আলোচনার সরাসরি প্রয়োগ। এই কাঠামোগত অসম্ভবতাই isolation-এর guarantee-কে “সাবধানে থাকলে হয়” থেকে “গাণিতিকভাবে নিশ্চিত” পর্যায়ে নিয়ে যায়।
“প্রতিটা process-এর virtual address space তার physical RAM-এর সমান আকারের একটা 'কপি'।”
সম্পূর্ণ ভুল একটা মানসিক মডেল। একটা process-এর virtual address space-এর আকার নির্ভর করে CPU architecture-এর ঠিকানা-প্রস্থের উপর (x86-64-এ কয়েক টেরাবাইট, paging-and-page-tables.mdx-এ ঠিক সংখ্যাটা দেখব) — physical RAM-এর প্রকৃত আকারের সাথে তার কোনো সরাসরি সম্পর্ক নেই।
একটা ৮GB RAM-এর মেশিনেও প্রতিটা process কয়েক টেরাবাইট virtual ঠিকানা-জগৎ “দেখতে” পারে — বেশিরভাগটাই কখনো ব্যবহৃত হয় না, আর যেটুকু ব্যবহৃত হয় তার সবটার জন্যও physical RAM-এ জায়গা বরাদ্দ থাকতে হবে না তাৎক্ষণিকভাবে (demand paging)। “Virtual address space” আর “physical RAM ব্যবহার” — দুইটা সম্পূর্ণ আলাদা মাত্রা, একটা আরেকটার আকারের প্রতিফলন না। এটাই এই লেসনের দ্বিতীয় সমস্যার (বেশি memory-র বিভ্রম) মূল কথা।
বুঝেছেন কি না দেখুন
1এই লেসনের শুরুর চিন্তা-পরীক্ষায় দুইটা process একই virtual address ছাপিয়েছিল, নিরাপদে। যদি virtual memory না থাকত (bare-metal মডেল), একই প্রোগ্রাম দুইবার চালালে কী ঘটত, আর কেন?
যুক্তি
দুইটা instance একই physical মেমরি স্লট নিয়ে সরাসরি প্রতিদ্বন্দ্বিতা করত, আর কার্যত এলোমেলো আচরণ দেখাত।
Bare-metal মডেলে প্রোগ্রামের ব্যবহৃত ঠিকানা সরাসরি physical RAM-এর ঠিকানা। দুইটা instance একই বাইনারি, তাই দুইজনেই একই ঠিকানায় (ধরুন global_var-এর জন্য) লিখতে চাইবে — আর সেই ঠিকানাটা এবার সত্যিই একই RAM স্লট, কোনো MMU অনুবাদ নেই যা তাদের আলাদা করবে।
ফলাফল হতো একটা race condition-জাতীয় বিভ্রান্তি: process ১ global_var = 100 লিখল, process ২ সেটা পড়ে 101 বানাল ও লিখল, কিন্তু ঠিক তখনই process ১-ও নিজের হিসাবে 101 লিখতে চেষ্টা করল — দুইজনেই ভাবছে তারা “নিজের” ভেরিয়েবল নিয়ে কাজ করছে, বাস্তবে একটাই শেয়ার্ড মেমরি লোকেশনে সংঘর্ষ করছে, কোনো lock বা coordination ছাড়াই (কারণ কেউই জানে না অন্যজন আছে)। এমনকি আরও খারাপ — দুইজনেরই code segment একই ঠিকানায় থাকলে, একজনের instruction stream অন্যজনের সাথে গুলিয়ে যেতে পারত, প্রায় নিশ্চিত crash।
এই কারণেই OS-এর মূল কাজগুলোর একটা হলো প্রতিটা process-কে তার নিজস্ব, স্বাধীন ঠিকানা-জগৎ দেওয়া — এটা কোনো “সুবিধা” ফিচার না, বহু-প্রোগ্রামিং কার্যত অসম্ভব হয়ে যেত এটা ছাড়া। প্রতিটা প্রোগ্রামারকে ভাবতে হতো “আমার ভেরিয়েবল কোন ঠিকানায় বসবে, আর সেটা কি অন্য কোনো একসাথে-চলা প্রোগ্রামের সাথে সংঘর্ষ করবে” — বাস্তবে virtual memory-র কারণে এই প্রশ্নটাই কখনো জিজ্ঞাসা করতে হয় না।
2কেউ যুক্তি দিতে পারে: “MMU-র দরকারই নেই, OS প্রতিটা process শুরুর সময় শুধু একটা আলাদা, non-overlapping physical মেমরি অঞ্চল বরাদ্দ করে দিক — তাহলেও তো isolation হয়ে যায়, কোনো hardware translation ছাড়াই।” এই যুক্তির সমস্যা কোথায়?
যুক্তি
এই পদ্ধতিতে isolation আংশিকভাবে সমাধান হয় (যদি OS ঠিকমতো non-overlapping অঞ্চল বরাদ্দ করে ও কোনো bug-এ সীমা ছাড়িয়ে না যায়), কিন্তু বাকি দুইটা সমস্যা — overcommit-এর বিভ্রম আর flexibility — একদমই সমাধান হয় না, আর isolation নিজেও দুর্বল থেকে যায়।
Isolation দুর্বল থাকে। শুধু “আলাদা অঞ্চল বরাদ্দ” যথেষ্ট না যদি কোনো hardware বাধ্যবাধকতা না থাকে সেই সীমার বাইরে গেলে কী হবে তার। একটা buggy প্রোগ্রাম একটা array-index ভুল করে তার নিজের বরাদ্দকৃত অঞ্চলের বাইরে লিখে ফেললে — এই মডেলে কিছুই তাকে থামায় না, সরাসরি পাশের process-এর মেমরিতে গিয়ে পড়ে। MMU-ভিত্তিক মডেলে প্রতিটা access-এ hardware নিজেই যাচাই করে ঠিকানাটা বৈধ কিনা — এটা একটা active guarantee, শুধু “ভদ্র থাকার আশা” না।
Overcommit-এর সুবিধা সম্পূর্ণ হারিয়ে যায়। যদি প্রতিটা process-কে “তার” ভাগের জন্য সত্যিকারের, আগে থেকে-বরাদ্দকৃত physical RAM দিতে হয়, তাহলে ১০টা process-এর প্রতিটাই “২GB চাই” বললে সত্যিই ২০GB physical RAM লাগবে — উপরের সমস্যা ২-এর সমাধানই নেই, কারণ ব্যবহৃত-না-হওয়া অংশও physical RAM দখল করে বসে থাকে।
Flexibility একদমই নেই। একটা fixed physical অঞ্চল একবার বরাদ্দ হয়ে গেলে, heap বা stack বাড়ানোর দরকার হলে পাশের জায়গা অন্য কারো দখলে থাকলে বাড়ানো অসম্ভব — ঠিক bare-metal মডেলের একই external-fragmentation সমস্যা যা paging-and-page-tables.mdx-এ বিস্তারিত দেখব।
মূল কথা: “non-overlapping physical বরাদ্দ” isolation-এর একটা দুর্বল, আংশিক আনুমানিক সংস্করণ দেয়, hardware-enforced guarantee ছাড়া, আর বাকি দুইটা সমস্যা একদমই স্পর্শ করে না। MMU-ভিত্তিক ঠিকানা-অনুবাদ তিনটা সমস্যাই একই সাথে, একই মেকানিজম দিয়ে সমাধান করে — এটাই virtual memory-র নকশাগত কেন্দ্রীয় সৌন্দর্য।
3আপনার নিজের experiment-এ (marker.c দুইবার চালানো) দেখা গেল দুইটা প্রোসেসের virtual address 0x55e2a1b04018 হুবহু মিলে গেছে, কিন্তু pagemap থেকে বের করা physical frame number আলাদা এসেছে (245891 বনাম 198234)। যদি দুইটা রান-এর মধ্যে virtual address মিলত না (ধরুন একজনের 0x55e2..., অন্যজনের 0x5a1f...), তাহলে কি এটা virtual memory ভেঙে গেছে বোঝাত?
প্রয়োগ
0x55e2a1b04018 হুবহু মিলে গেছে, কিন্তু pagemap থেকে বের করা physical frame number আলাদা এসেছে (245891 বনাম 198234)। যদি দুইটা রান-এর মধ্যে virtual address মিলত না (ধরুন একজনের 0x55e2..., অন্যজনের 0x5a1f...), তাহলে কি এটা virtual memory ভেঙে গেছে বোঝাত?না — বরং এটা virtual memory ঠিকমতো কাজ করছে তার আরেকটা প্রমাণ, শুধু ভিন্ন কারণে।
দুইটা প্রোসেসের virtual address মেলা বা না-মেলা, দুইটাই সম্পূর্ণ স্বাভাবিক — নির্ভর করে ASLR (Address Space Layout Randomization) চালু আছে কিনা তার উপর। ASLR একটা নিরাপত্তা ফিচার যা প্রতিটা প্রোসেস শুরুর সময় ইচ্ছাকৃতভাবে তার code/heap/stack-এর base virtual address এলোমেলো করে দেয় — যাতে আক্রমণকারী “আমি জানি stack ঠিক কোন ঠিকানায় আছে” ধরে নিয়ে আক্রমণ (যেমন return-oriented programming, paging-and-page-tables.mdx-এর NX বিট আলোচনায় যা উল্লেখ হবে) করতে না পারে। এই লেসনের experiment-এ -no-pie ফ্ল্যাগ দিয়ে কম্পাইল করার কারণটাই ছিল ASLR-এর একাংশ নিষ্ক্রিয় করা, যাতে তুলনাটা সহজ হয় — বাস্তব production বাইনারিতে সাধারণত ASLR চালুই থাকে, তাই দুইটা রানের virtual address প্রায়ই আলাদা হবে।
গুরুত্বপূর্ণ বিষয়টা হলো — virtual address মিলুক বা না মিলুক, তার সাথে isolation-এর কোনো সম্পর্ক নেই। Isolation guarantee করে physical frame। যদি virtual address ভিন্ন হতো, physical frame number-ও ভিন্নই থাকত (যেহেতু সেগুলো ইতিমধ্যেই ভিন্ন ছিল যখন virtual address মিলে গিয়েছিল) — কারণ প্রতিটা প্রোসেসের নিজস্ব, স্বাধীন মাপিং, virtual address জেনারেট হওয়ার পদ্ধতি (fixed বা randomized) যাই হোক না কেন।
তাহলে virtual memory কবে “ভেঙে গেছে” বলা যেত? শুধু তখনই, যদি দুইটা প্রোসেসের virtual address ভিন্ন থাকা সত্ত্বেও (বা একই থাকা সত্ত্বেও) তাদের physical frame number একই আসত অথচ কোনো ইচ্ছাকৃত shared mapping (যেমন shared library বা shm_open) না থাকত — তখন সেটা একটা প্রকৃত isolation ব্যর্থতা বোঝাত, সম্ভবত একটা kernel bug বা hardware ত্রুটি। আজকের experiment-এ দুইটা প্রোসেস স্বাধীন malloc/global-variable ব্যবহার করেছে, কোনো shared mapping অনুরোধ করেনি — তাই ভিন্ন physical frame-ই একমাত্র সঠিক, প্রত্যাশিত ফলাফল।
4একটা এমবেডেড সিস্টেম ডিজাইন করছেন যেখানে মাত্র একটাই প্রোগ্রাম কখনো চলবে (কোনো multi-tasking নেই, কোনো untrusted কোড নেই), আর memory মাত্র কয়েক KB। এই সিস্টেমে virtual memory (MMU-সহ পূর্ণ ব্যবস্থা) ব্যবহার করা কি মূল্যবান, নাকি সরাসরি physical addressing যথেষ্ট? এই লেসনের তিনটা সমস্যার আলোকে যুক্তি দিন।
ডিজাইন
এই নির্দিষ্ট পরিস্থিতিতে, সরাসরি physical addressing-ই বেশি যুক্তিসঙ্গত — এটাই কারণ বাস্তবে অনেক microcontroller (যেমন ছোট ARM Cortex-M সিরিজ) MMU ছাড়াই চলে।
তিনটা সমস্যা একে একে যাচাই করি:
Isolation। এই সমস্যাটা প্রাসঙ্গিকই না — যেহেতু মাত্র একটা প্রোগ্রাম কখনো চলে, আর সেটা trusted (কোনো untrusted কোড লোড হয় না), কারো থেকে কাউকে আলাদা রাখার দরকার নেই। MMU-র সবচেয়ে বড় সুবিধাটাই এখানে অকেজো।
বেশি memory-র বিভ্রম / overcommit। একটা single-program সিস্টেমে “বেশ কয়েকটা প্রোগ্রাম মিলিয়ে সীমিত RAM-এর বেশি চাওয়া” পরিস্থিতিই তৈরি হয় না — একটাই প্রোগ্রাম যা আছে তার পুরোটাই সরাসরি ব্যবহার করে। এই সমস্যাও অপ্রাসঙ্গিক।
Flexibility। এমবেডেড সিস্টেমে সাধারণত মেমরি লেআউট compile-time বা link-time-এই সম্পূর্ণ নির্দিষ্ট — dynamic loading, runtime-এ heap growth-এর অনিশ্চয়তা এসব সাধারণত ইচ্ছাকৃতভাবে এড়ানো হয় (predictability-র জন্য, বিশেষত hard real-time সিস্টেমে)। ফলে flexibility-র প্রয়োজনও ন্যূনতম।
উল্টো দিকে খরচ আছে। MMU নিজে একটা hardware component — সিলিকন এরিয়া, বিদ্যুৎ খরচ, আর প্রতিটা memory access-এ (যদিও দ্রুত) একটা সামান্য অতিরিক্ত লেটেন্সি যোগ করে। কয়েক KB মেমরির একটা সিস্টেমে page table-এর নিজস্ব overhead (একটা পূর্ণ page-ই তো ৪KB, পুরো সিস্টেমের চেয়ে বড়!) অসামঞ্জস্যপূর্ণ। কম দাম, কম বিদ্যুৎ খরচ, আর deterministic (predictable) টাইমিং-এর জন্য অনেক real-time embedded system ইচ্ছাকৃতভাবে MMU বাদ দেয়।
সাধারণ নীতি: virtual memory-র প্রতিটা সুবিধা একটা নির্দিষ্ট সমস্যার সমাধান — multi-tasking, untrusted কোড, dynamic/unpredictable memory ব্যবহার। যেই পরিস্থিতিতে এই সমস্যাগুলোর কোনোটাই বাস্তবে ঘটে না, সেখানে virtual memory-র পুরো যন্ত্রপাতি একটা সমাধান-ছাড়া-সমস্যার জন্য খরচ বহন করা। এটাই engineering trade-off-এর একটা সাধারণ শিক্ষা — একটা প্রযুক্তি “ভালো” কিনা তার উত্তর সবসময় প্রসঙ্গনির্ভর, universal না।
এরপর কী
পরের লেসন — Paging আর Page Table
আজ আমরা virtual memory-র প্রতিশ্রুতি দেখেছি — isolation, বেশি memory-র বিভ্রম, flexibility — আর জেনেছি MMU নামের একটা hardware ইউনিট প্রতিটা memory access-এ, অদৃশ্যভাবে, virtual address-কে physical address-এ অনুবাদ করে, OS-এর নির্ধারিত একটা মাপিং অনুসরণ করে। কিন্তু ইচ্ছাকৃতভাবে একটা প্রশ্ন এড়িয়ে গেছি: সেই মাপিং ঠিক কীভাবে সংরক্ষিত থাকে, আর MMU ঠিক কীভাবে সেটা লুকআপ করে?
পরের লেসনে সেই প্রশ্নের সম্পূর্ণ উত্তর — paging আর page table। দেখব কেন ঠিকানা-জগৎকে সমান-আকারের “page”-এ ভাগ করা হয়, x86-64-এর ৪৮-বিট ঠিকানা কীভাবে ঠিক পাঁচটা টুকরোয় ভেঙে যায়, একটা multi-level page table কীভাবে ৫১২ গিগাবাইটের একটা অবাস্তব টেবিলকে মাত্র কয়েক KB-তে নামিয়ে আনে, আর প্রতিটা table entry-র ভেতরের বিটগুলো (present, read/write, dirty, NX) ঠিক কী নিয়ন্ত্রণ করে। আজকের “একটা মাপিং আছে” ধারণাটা তখন একটা concrete, হাতে-কলমে-যাচাইযোগ্য ডেটা স্ট্রাকচারে রূপ নেবে।
আরও পড়ুন
- Operating Systems: Three Easy Pieces — Chapter 13: The Abstraction: Address Spaces — Remzi H. Arpaci-Dusseau, Andrea C. Arpaci-Dusseau · Address space abstraction-এর সবচেয়ে পরিষ্কার প্রাথমিক পরিচিতি — এই লেসনের কাঠামোর মূল অনুপ্রেরণা
- Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3A — Chapter 4: Paging — Intel Corporation · MMU ও paging hardware-এর প্রামাণ্য বিবরণ — পরের কয়েকটা লেসনে এই manual বারবার ফিরে আসবে
- Linux kernel documentation — Memory Management · প্রতিটা process-এর /proc/PID/maps কীভাবে তার address space বর্ণনা করে তার ব্যবহারিক রেফারেন্স