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

Virtual memory — demand paging, page fault আর copy-on-write

Virtual Memory — Demand Paging, Page-Fault Handling, Effective Access Time, Copy-on-Write, mmap and Overcommit

Taught in Bangla, this lesson explains why virtual memory lets programs exceed RAM, demand paging, the step-by-step handling of a page fault, the effective access time calculation with page faults, copy-on-write and fast fork, memory-mapped files and anonymous memory, the free-frame pool, overcommit, and what happens when RAM runs out.

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

  • Virtual memory-র চারটি বড় লাভ, আর demand paging কেন কাজ করে (locality)
  • Page fault সামলানোর পুরো ধাপক্রম — trap থেকে instruction restart পর্যন্ত, আর কোন fault minor, কোনটি major
  • Effective access time-এর সূত্র দিয়ে পরীক্ষার numerical সমাধান — p-এর সীমা বের করা সহ
  • fork-এর copy-on-write, vfork-এর সাথে তুলনা, zero-fill-on-demand, আর mmap-এর private বনাম shared ম্যাপিং
  • Overcommit, free-frame pool, আর RAM ফুরোলে কী হয় — page replacement-এর প্রয়োজন কোথা থেকে আসে

১ · RAM-এর চেয়ে বড় প্রোগ্রাম — virtual memory কেন

আপনার ল্যাপটপে হয়তো ৮ GB RAM. অথচ একসাথে খোলা আছে Chrome-এর বিশটি ট্যাব, VS Code, Zoom আর একটি গেম। Task Manager-এ সবার “মেমরি” যোগ করলে অনেক সময় ৮ GB ছাড়িয়ে যায়, তবু সব চলছে। আবার একটি 64-bit প্রসেস তাত্ত্বিকভাবে ১২৮ TiB (x86-64-এ user অংশ, 4-level page table-এ) পর্যন্ত ঠিকানা ব্যবহার করতে পারে — পৃথিবীর কোনো ল্যাপটপে এত RAM নেই। এই ধাঁধার উত্তর virtual memory.

পর্ব ৩০ থেকে ৩৪-এ দেখেছেন, প্রসেস যে ঠিকানা দেখে (logical বা virtual address) আর RAM-এর আসল ঠিকানা (physical address) আলাদা; মাঝখানে page table অনুবাদ করে, আর TLB সেই অনুবাদ মনে রাখে। এই পর্বের নতুন কথাটি হলো: page table-এর প্রতিটি ঘরে যে RAM-এর frame থাকতেই হবে, এমন কোনো নিয়ম নেই। কিছু page থাকতে পারে ডিস্কে, কিছু হয়তো এখনো তৈরিই হয়নি। প্রসেস সেটা টেরও পায় না — শুধু প্রথম ছোঁয়ায় একটু দেরি হয়।

একটি বড় পাবলিক লাইব্রেরির কথা ভাবুন। ক্যাটালগে লাখখানেক বই — এটি আপনার address space. কিন্তু আপনার পড়ার টেবিলে জায়গা মাত্র দশটি বইয়ের — এটি RAM. লাইব্রেরিয়ান (OS) আপনাকে পুরো লাইব্রেরি টেবিলে এনে দেন না; আপনি যে বইটি চান, তখনই তাক থেকে (ডিস্ক থেকে) এনে দেন। টেবিল ভরে গেলে এমন একটি বই তাকে ফেরত পাঠান যেটি এখন আর পড়ছেন না। আপনার মনে হয় সব বই হাতের কাছেই আছে — শুধু নতুন বই চাইলে একটু অপেক্ষা।

চারটি বড় লাভ

RAM-এর চেয়ে বড় প্রোগ্রাম

প্রোগ্রামের পুরোটা একসাথে লাগে না — error সামলানোর কোড, কখনো না-খোলা মেনু, বড় অ্যারের অব্যবহৃত অংশ। যতটুকু চলছে, ততটুকুই RAM-এ থাকলে চলে।

প্রোগ্রামারের ঝামেলা কমে

একসাথে বেশি প্রসেস

প্রত্যেকের অল্প অংশ RAM-এ থাকায় বেশি প্রসেস একসাথে রাখা যায় — degree of multiprogramming বাড়ে, CPU কম অলস থাকে।

CPU utilisation বাড়ে

ভাগাভাগি সহজ

একই library-র কোড, একই ফাইল, fork-এর পরের parent-child — সবাই একই physical frame দেখতে পারে। ১০০টি প্রসেস libc ব্যবহার করলেও RAM-এ তার কোড একবারই থাকে।

shared library, COW, mmap

ফাঁকা ঠিকানার খরচ নেই

Heap নিচে, stack ওপরে, মাঝে বিশাল ফাঁকা — একে বলে sparse address space. ফাঁকা অংশের জন্য RAM বা ডিস্ক কিছুই খরচ হয় না।

heap আর stack বাড়ার জায়গা

Virtual memory-র আগে বড় প্রোগ্রাম চালাতে প্রোগ্রামারকে নিজে overlay বানাতে হতো — কোন অংশ কখন একই মেমরি-জায়গায় আনা হবে, তার পুরো পরিকল্পনা হাতে লিখে। ১৯৬২ সালে ইংল্যান্ডের University of Manchester-এ Tom Kilburn-এর দলের Atlas কম্পিউটার প্রথম দেখায় যে কাজটি হার্ডওয়্যার আর OS নিজেরাই করতে পারে — তারা একে বলেছিল “one-level store”: ছোট দ্রুত core মেমরি আর বড় ধীর drum মিলে প্রোগ্রামারের চোখে একটিই মেমরি। ১৯৬০-এর দশকের শেষে Peter Denning-এর working set তত্ত্ব (পর্ব ৩৭) দেখায় কেন এটি দক্ষভাবে চলে, আর ১৯৭০-এর দশকে IBM-এর System/370 ও DEC-এর VAX-এর হাত ধরে এটি সাধারণ কম্পিউটারের নিয়মে পরিণত হয়।

একটি প্রসেসের page কোথায় কোথায় থাকতে পারে

Virtual page → page table → RAM-এর frame, অথবা ডিস্ক, অথবা কোথাও না

  1. Virtual page0x7f3a…প্রসেস যা দেখে
  2. Page tablevalid / invalidMMU প্রতিবার দেখে
  3. RAM-এর framevalidদ্রুত, ন্যানোসেকেন্ড
  4. ডিস্ক / এখনো নেইinvalidছুঁলে page fault
ছবি ৩৫.১ — একই virtual ঠিকানা কখনো RAM-এ, কখনো ডিস্কে, কখনো একেবারে অস্তিত্বহীন; প্রসেস শুধু ঠিকানাটাই জানে।

২ · Demand paging — দরকার পড়লে তবেই আনা

প্রোগ্রাম চালু করার দুটি উপায় ভাবা যায়। এক, পুরো প্রোগ্রামটি ডিস্ক থেকে RAM-এ এনে তারপর চালানো। দুই, কিছুই না এনে চালানো শুরু করা, আর যে page প্রথমবার লাগবে, শুধু সেটিই তখন আনা। দ্বিতীয়টির নাম demand paging — সফটওয়্যারের সাধারণ কৌশল lazy loading-এর মেমরি-রূপ। আধুনিক সব সাধারণ-কাজের OS (Linux, Windows, macOS, Android, iOS) এটিই করে।

চরম রূপটির নাম pure demand paging: exec-এর পর প্রসেসের একটি page-ও RAM-এ থাকে না। CPU প্রথম নির্দেশটি আনতে গেলেই প্রথম page fault. বাস্তবের OS পুরোপুরি “pure” নয় — Linux ফাইল পড়ার সময় আশপাশের কয়েকটি page আগেভাগে পড়ে রাখে (readahead), Windows-এর prefetcher আগের রানের হিসাব দেখে কিছু page আনে। এই আগাম আনার নাম prepaging. তবে মূল নীতি একই: না ছোঁয়া পর্যন্ত আনা নয়।

Valid–invalid bit আর backing store

প্রতিটি page table entry-তে একটি বিট থাকে — valid–invalid bit (x86-এ “present” বিট)। Valid মানে page-টি এই মুহূর্তে একটি frame-এ আছে, ঘরে লেখা frame নম্বর ঠিক। Invalid মানে দুটির একটি:

  • ঠিকানাটি বৈধ, কিন্তু page এখন RAM-এ নেই — এটি আছে তার backing store-এ: কোড আর read-only ডেটার জন্য প্রোগ্রামের নিজের ফাইল, সরিয়ে রাখা heap/stack-এর জন্য swap space, অথবা এখনো কখনো ছোঁয়াই হয়নি (তাহলে প্রথম ছোঁয়ায় শূন্য-ভরা নতুন page).
  • ঠিকানাটিই অবৈধ — প্রসেসের কোনো এলাকার মধ্যে পড়ে না, যেমন NULL pointer বা মুক্ত করা মেমরি।

হার্ডওয়্যার দুটিকে আলাদা করে না; MMU শুধু দেখে বিটটি 0, আর exception তোলে। পার্থক্য করে kernel — প্রসেসের মেমরি-এলাকার তালিকা দেখে। Linux-এ প্রতিটি এলাকা একটি VMA (virtual memory area); /proc/self/maps-এর প্রতিটি লাইন একটি VMA.

মূল ধারণা

Demand paging কাজ করে কারণ প্রোগ্রাম locality of reference মেনে চলে — একটি সময়ে অল্প কয়েকটি page-ই বারবার ছোঁয়া হয় (একটি লুপ, তার ডেটা, stack-এর মাথা)। তাই প্রথম কয়েকটি fault-এর পর অনেকক্ষণ আর fault হয় না। প্রোগ্রাম যদি প্রতিটি নির্দেশে নতুন page ছুঁত, demand paging অচল হতো।

নিচের ডায়াগ্রামে একটি ছোট প্রোগ্রাম report-এর ৬টি virtual page (0 থেকে 5) আর RAM-এ ৪টি খালি frame. “v, F3” মানে valid আর frame 3-এ; “i” মানে invalid. প্রোগ্রাম চলার সাথে সাথে কোন page কখন আসে, খেয়াল করুন:

1exec ./report2fetch first instruction (page 0)3read global counter (page 2)4call: push on stack (page 5)5loop x1000 on pages 0, 2, 56first write to malloc buffer (page 3)7call print_report() (page 1)
Page table আর free frame
page 0 · code
page 1 · code
page 2 · data
page 3 · heap
page 4 · অব্যবহৃত
page 5 · stack
খালি frame
মোট page fault
  1. exec শেষ, RAM-এ কিছুই নেই। Kernel শুধু ঠিক করে রেখেছে কোন virtual এলাকা কোন ফাইল থেকে আসবে — code আর data আসবে report ফাইল থেকে, heap আর stack হবে anonymous. ৬টি page-ই invalid, চারটি frame (F3, F6, F8, F9) খালি তালিকায়। Fault এখনো ০টি।
  2. প্রথম নির্দেশেই fault. CPU page 0 থেকে নির্দেশ আনতে গেল, MMU দেখল invalid — page fault. Kernel প্রোগ্রামের ফাইল থেকে page 0 পড়ে F3-এ বসাল, valid করল। এটিই pure demand paging-এর বৈশিষ্ট্য: প্রথম নির্দেশটিও দাবি করে আনা হয়। মোট fault ১।
  3. Global ভ্যারিয়েবল পড়া। নির্দেশটি page 2-এর একটি global ভ্যারিয়েবল পড়তে চায় — আবার fault. ফাইলের data অংশ থেকে page 2 এল F6-এ। মোট fault ২, খালি frame বাকি দুটি: F8, F9.
  4. ফাংশন ডাকা — stack-এ লেখা। Page 5 (stack) এর আগে কখনো ছোঁয়া হয়নি, তাই পড়ার মতো কোনো ফাইল নেই। Kernel F8 নিয়ে পুরোটা শূন্য দিয়ে ভরে বসিয়ে দিল — ডিস্কে যেতেই হলো না। মোট fault ৩।
  5. Locality-র সুফল। এবার লুপ চলল এক হাজার বার, কিন্তু শুধু page 0, 2, 5-এর মধ্যে — তিনটিই valid. হাজার হাজার মেমরি-অ্যাক্সেস, একটিও নতুন fault নয়। মোট fault এখনো ৩।
  6. malloc-এর buffer-এ প্রথম লেখা। malloc ঠিকানা দিয়েছিল আগেই, কিন্তু frame দেয়নি। প্রথম লেখায় page 3-এর fault, শূন্য-ভরা F9. মোট fault ৪, আর খালি frame-এর তালিকা এখন শূন্য। Page 4 কখনো ছোঁয়া হয়নি, তাই সেটি কখনো আসেওনি — sparse address space-এর লাভ।
  7. পঞ্চম page, কিন্তু frame নেই। print_report() আছে page 1-এ — fault. কিন্তু খালি frame একটিও নেই। এখন kernel-কে চলমান কোনো page সরিয়ে জায়গা করতে হবে: কাকে সরাবে? এই প্রশ্নটিই page replacement, যার algorithm (FIFO, Optimal, LRU, Clock) পর্ব ৩৬-এর বিষয়। মোট fault ৫।

৩ · Page fault — ধাপে ধাপে কী ঘটে

“Page fault” নামে ভুল আছে মনে হলেও এটি কোনো ভুল নয় — এটি একটি স্বাভাবিক exception, যার মাধ্যমে হার্ডওয়্যার kernel-কে বলে “এই page-টি নিয়ে কিছু একটা করতে হবে।” একটি সাধারণ Linux ডেস্কটপে প্রতি সেকেন্ডে হাজার হাজার page fault হয়, আর বেশিরভাগই কেউ টেরও পায় না। কিন্তু যে fault-এ ডিস্ক থেকে পড়তে হয়, সেটির ধাপগুলো জানা জরুরি — পরীক্ষায় এটি প্রায় নিশ্চিত প্রশ্ন, আর কর্মক্ষমতা বুঝতেও এটিই ভিত্তি।

নিচে প্রসেস P একটি নির্দেশে ঠিকানা 0x5123 থেকে পড়তে চায়। 4 KiB page-এ এটি page 5, offset 0x123. Page 5 আগে swap-এ সরানো হয়েছিল (swap slot 812):

1MMU: PTE(5).valid == 0  → trap2kernel: 0x5123 inside a VMA? rights ok?3kernel: frame = take(free_list)4kernel: disk read swap[812] → frame5scheduler: run another process6disk interrupt: PTE(5) = frame, valid7P scheduled: restart same instruction8mov eax, [0x5123] completes
অবস্থা
CPU-তে চলছে
P-এর অবস্থা
PTE(5)
খালি frame
ডিস্ক
eax
  1. Trap. MMU page table-এ (বা আগে TLB-তে) page 5 খুঁজে দেখল valid বিট 0. নির্দেশটি মাঝপথে থামিয়ে CPU একটি page-fault exception তুলল: P-এর register বাঁচিয়ে kernel mode-এ handler-এ গেল। x86-এ fault-এর ঠিকানাটি CR2 register-এ থাকে, আর একটি error code বলে দেয় পড়া না লেখা, user না kernel.
  2. বৈধতা যাচাই। Handler প্রসেসের VMA তালিকায় দেখল 0x5123 P-এর heap এলাকায় পড়ে, আর সেখানে পড়ার অনুমতি আছে — অর্থাৎ বৈধ ঠিকানা, শুধু RAM-এ নেই। কোনো VMA-তে না পড়লে বা অনুমতি না থাকলে (যেমন শুধু-পড়ার কোডে লেখা) এখানেই শেষ: P পায় SIGSEGV, পরিচিত “Segmentation fault”.
  3. খালি frame নেওয়া। Kernel তার free-frame pool থেকে F7 নিল। তালিকা খালি থাকলে এখানেই একটি victim page বেছে সরাতে হতো, আর সেটি dirty হলে আগে ডিস্কে লিখতে হতো (পর্ব ৩৬)।
  4. ডিস্ক থেকে পড়ার অনুরোধ। PTE-তে লেখা আছে page-টি swap slot 812-এ। Kernel ডিস্ককে বলল ওই slot F7-এ পড়তে (DMA দিয়ে)। হার্ড ডিস্কে এতে কয়েক মিলিসেকেন্ড লাগবে — CPU-র হিসাবে লক্ষ লক্ষ নির্দেশের সময়। তাই P-কে blocked করা হলো।
  5. অপেক্ষার সময় অন্যের পালা। CPU অলস বসে থাকে না: scheduler context switch করে ready queue থেকে প্রসেস Q চালাল। Demand paging-এর খরচের বড় অংশ লুকিয়ে যায় এভাবেই — অন্য প্রসেস কাজ করে।
  6. পড়া শেষ — page table ঠিক করা। ডিস্ক interrupt দিল। Kernel PTE(5)-এ F7 লিখে valid করল, swap-এর কপিটির হিসাব রাখল, আর P-কে ready queue-তে ফেরাল। প্রসেস Q আপাতত চলতে থাকে।
  7. একই নির্দেশ আবার। P-এর পালা এলে তার register ফিরিয়ে আনা হলো, আর program counter আগের মতোই সেই নির্দেশে — পরের নির্দেশে নয়। একে বলে instruction restart: নির্দেশটি যেন কখনো শুরুই হয়নি।
  8. এবার সফল। MMU দেখল PTE(5) valid, F7-এর offset 0x123 থেকে মান এল (ধরা যাক 0x2A). অনুবাদটি TLB-তেও বসল, তাই পরের অ্যাক্সেস ন্যানোসেকেন্ডে। P-এর দৃষ্টিতে কিছুই ঘটেনি, শুধু একটি নির্দেশে কয়েক মিলিসেকেন্ড লেগেছে।

Instruction restart — সব নির্দেশ সহজে আবার চালানো যায় না

একটি সাধারণ load নির্দেশ আবার চালানো সহজ — কিছুই বদলায়নি। সমস্যা হয় সেই নির্দেশে, যা কাজের মাঝখানে fault করতে পারে। উদাহরণ: IBM System/360-এর MVC নির্দেশ এক চালেই ২৫৬ বাইট পর্যন্ত কপি করে। উৎস বা গন্তব্য যদি page-সীমা পেরোয়, আর অর্ধেক কপির পরে দ্বিতীয় page-এ fault হয় — আর উৎস ও গন্তব্য যদি একে অপরের ওপর পড়ে — তাহলে উৎসের কিছু অংশ ইতিমধ্যে মুছে গেছে, আবার শুরু করলে ভুল ফল। সমাধান দুটি: (১) নির্দেশ শুরুর আগেই microcode উভয় প্রান্ত ছুঁয়ে দেখে নেয়, fault হলে কিছু বদলানোর আগেই হবে; (২) মুছে যাওয়া মান সাময়িক register-এ রেখে fault হলে ফিরিয়ে দেওয়া। Auto-increment ঠিকানা-পদ্ধতির নির্দেশেও (যা register-এর মান বদলায়) একই সমস্যা। সারকথা: demand paging শুধু OS-এর কৌশল নয়, CPU-কেও নিশ্চিত করতে হয় যে যেকোনো fault-এর পর নির্দেশ নিরাপদে আবার চালানো যায়।

Minor বনাম major fault

উপরের উদাহরণে ডিস্কে যেতে হয়েছে — এটি major (hard) page fault. কিন্তু বেশিরভাগ fault এর চেয়ে অনেক সস্তা, কারণ ডেটা হয় RAM-এ আগে থেকেই আছে, নয়তো দরকারই নেই। এগুলো minor (soft) page fault:

ঘটনাধরনkernel কী করেমোটামুটি খরচ
malloc বা stack-এর নতুন page-এ প্রথম লেখাminorখালি frame নিয়ে শূন্য দিয়ে ভরে বসায়এক বা কয়েক µs
fork-এর পর ভাগ করা page-এ লেখা (COW)minorনতুন frame-এ কপি করে বসায়কয়েক µs
ফাইলের page, যা অন্য প্রসেস আগেই পড়েছে (page cache-এ আছে)minorশুধু page table-এ frame বসায়এক µs-এর আশপাশে
প্রোগ্রাম বা ফাইলের page, RAM-এ নেইmajorফাইল থেকে পড়েSSD-তে দশ থেকে কয়েকশো µs, HDD-তে কয়েক ms
swap-এ সরানো heap/stack pagemajorswap থেকে পড়েএকই
অবৈধ ঠিকানা বা অনুমতির বাইরেfault নয়, ত্রুটিপ্রসেসকে SIGSEGV (map করা ফাইলের শেষের পরে হলে SIGBUS)—

খরচের সংখ্যাগুলো হার্ডওয়্যার আর kernel ভেদে অনেক বদলায় — এগুলো শুধু মাত্রা বোঝানোর জন্য। Linux-এ দুটি গণনাই পাওয়া যায়: ps -o min_flt,maj_flt, /proc/PID/stat, আর C থেকে getrusage()-এর ru_minflt ও ru_majflt. Windows-এর Resource Monitor-এ “Hard Faults/sec” কলামটি major fault-ই দেখায়। এবার নিজের চোখে দেখুন — ১৬ MiB মেমরি নিয়ে প্রতিটি page-এ একবার করে লিখে fault গোনা:

#include <stdio.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <unistd.h>

static long minor_faults(void) {          /* faults served without disk I/O */
    struct rusage ru;
    getrusage(RUSAGE_SELF, &ru);
    return ru.ru_minflt;
}

int main(void) {
    size_t pg = (size_t)sysconf(_SC_PAGESIZE);
    size_t n = 4096, len = n * pg;              /* 16 MiB with 4 KiB pages */
    long f0 = minor_faults();
    char *p = mmap(NULL, len, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (p == MAP_FAILED) { perror("mmap"); return 1; }
    madvise(p, len, MADV_NOHUGEPAGE);           /* keep 4 KiB pages */
    long f1 = minor_faults();
    for (size_t i = 0; i < n; i++) p[i * pg] = 1;   /* first touch */
    long f2 = minor_faults();
    for (size_t i = 0; i < n; i++) p[i * pg] = 2;   /* second touch */
    long f3 = minor_faults();

    printf("page size             : %zu bytes\n", pg);
    printf("mmap of %zu MiB        : %ld faults\n", len >> 20, f1 - f0);
    printf("1st touch, %zu pages : %ld faults = %.3f per page\n",
           n, f2 - f1, (double)(f2 - f1) / (double)n);
    printf("2nd touch, same pages : %ld faults\n", f3 - f2);
    munmap(p, len);
    return 0;
}

আসল রানের আউটপুট (আপনার রানে সংখ্যা সামান্য আলাদা হতে পারে):

page size             : 4096 bytes
mmap of 16 MiB        : 0 faults
1st touch, 4096 pages : 4096 faults = 1.000 per page
2nd touch, same pages : 0 faults

তিনটি শিক্ষা। (১) mmap নিজে কোনো fault করে না — ১৬ MiB চাইলেও kernel শুধু একটি VMA লিখে রাখে, একটি frame-ও দেয় না। এটিই demand paging. (২) প্রথম ছোঁয়ায় ঠিক page প্রতি একটি fault — 4096টি page, 4096টি minor fault; প্রতিটিতে kernel একটি শূন্য-ভরা frame দিয়েছে, ডিস্কে যায়নি। (৩) দ্বিতীয়বার কোনো fault নেই — page এখন valid, MMU সরাসরি অনুবাদ করে। madvise(MADV_NOHUGEPAGE) লাইনটি রাখা হয়েছে কারণ transparent huge page চালু থাকলে kernel 2 MiB-এর বড় page দিতে পারে, তখন ৫১২টি ছোট page-এর জন্য একটিই fault হতো আর হিসাব মিলত না। আর সব printf শেষে রাখা হয়েছে, যাতে stdout-এর buffer তৈরির fault আমাদের গোনায় না ঢোকে।

৪ · Demand paging-এর খরচ — effective access time-এর হিসাব

প্রতিটি মেমরি-অ্যাক্সেস দুই রকম হতে পারে: page valid (খরচ ma, অর্থাৎ memory access time), অথবা page fault (খরচ পুরো page-fault service time). Page-fault rate p (0 ≤ p ≤ 1) হলে গড় খরচ, অর্থাৎ effective access time:

EAT = (1 − p) × ma + p × (page-fault service time)

Page-fault service time-এ তিনটি অংশ: (১) trap সামলানো, (২) page পড়া — হার্ড ডিস্কে seek, ঘূর্ণনের অপেক্ষা আর transfer মিলিয়ে কয়েক মিলিসেকেন্ড, আর victim dirty হলে আগে তাকে লেখা, (৩) প্রসেসকে আবার চালানো। প্রথম আর তৃতীয়টি মাইক্রোসেকেন্ডের; দ্বিতীয়টিই প্রধান। এখানে TLB আর page table-এর নিজের অ্যাক্সেস (পর্ব ৩৩) আলাদা করে ধরা হয়নি — পরীক্ষায় সেটা প্রশ্নে বলা থাকে।

উদাহরণ ১ — ma = 200 ns, service time = 8 ms

প্রথমেই একক মেলান: 8 ms = 8,000,000 ns. তাহলে

EAT = (1 − p) × 200 + p × 8,000,000 = 200 + 7,999,800 × p  (ns)

  • p = 1/1000 (হাজারে একটি fault): EAT = 200 + 7,999.8 = 8,199.8 ns ≈ 8.2 µs. মেমরি ৪১ গুণ ধীর! হাজারে একটি শুনতে কম, কিন্তু একটি fault (8 ms) একটি সাধারণ অ্যাক্সেসের (200 ns) চল্লিশ হাজার গুণ সময় খায়।
  • সর্বোচ্চ ১০% ধীর মানতে হলে: EAT < 220 ns হতে হবে, অর্থাৎ 200 + 7,999,800 × p < 220 ⇒ 7,999,800 × p < 20 ⇒ p < 20 / 7,999,800 = 1 / 399,990 ≈ 2.5 × 10−6. অর্থাৎ প্রায় চার লক্ষ অ্যাক্সেসে একটির বেশি fault চলবে না।

উদাহরণ ২ — dirty page সহ (পরীক্ষার পরিচিত ধরন)

ma = 100 ns. খালি frame পাওয়া গেলে বা বাদ দেওয়া page পরিষ্কার হলে service time 8 ms; বাদ দেওয়া page dirty হলে (আগে লিখতে হয়) 20 ms. ৭০% ক্ষেত্রে বাদ দেওয়া page dirty. EAT সর্বোচ্চ 200 ns রাখতে p কত হতে পারে?

  1. গড় service time = 0.3 × 8 + 0.7 × 20 = 2.4 + 14 = 16.4 ms = 16,400,000 ns.
  2. 200 ≥ (1 − p) × 100 + p × 16,400,000 = 100 + 16,399,900 × p.
  3. p ≤ 100 / 16,399,900 = 1 / 163,999 ≈ 6.1 × 10−6.

এখানে dirty bit-এর গুরুত্ব বোঝা যায়: পরিষ্কার page ফেলে দিলেই চলে (ডিস্কে আগের কপি আছে), dirty page আগে লিখতে হয় — service time আড়াই গুণ।

নিচের simulator একই সূত্রে বিভিন্ন p-এর জন্য EAT আর ধীরতার গুণফল দেখায়, আর তিনটি লক্ষ্যের জন্য p-এর সীমা বের করে — একটি SSD-র (service ≈ 0.1 ms) জন্যও:

// Effective access time with demand paging: EAT = (1 - p) * ma + p * service
const ma = 200;              // memory access time, ns
const service = 8e6;         // page-fault service time: 8 ms = 8,000,000 ns (HDD)
const eat = p => (1 - p) * ma + p * service;

console.log("     p (fault rate) | one fault per |  EAT (ns) | slowdown");
for (const n of [Infinity, 1e6, 399990, 1e5, 1e4, 1e3, 100]) {
  const p = 1 / n;
  console.log(p.toExponential(3).padStart(19) + " | " +
    (n === Infinity ? "never" : n.toLocaleString("en-US")).padStart(13) + " | " +
    eat(p).toFixed(1).padStart(9) + " | " + (eat(p) / ma).toFixed(2).padStart(7) + "x");
}

// Largest p for a given slowdown: (1 - p) * ma + p * service < ma * (1 + s)
// => p < ma * s / (service - ma)
for (const [label, s, svc] of [["10% slower, HDD 8 ms", 0.10, service],
                               [" 1% slower, HDD 8 ms", 0.01, service],
                               ["10% slower, SSD 0.1 ms", 0.10, 1e5]]) {
  const p = ma * s / (svc - ma);
  console.log(label + ": p < " + p.toExponential(4) +
              " = 1 / " + Math.round(1 / p).toLocaleString("en-US"));
}

আউটপুট (নির্ধারিত হিসাব, প্রতিবার একই):

     p (fault rate) | one fault per |  EAT (ns) | slowdown
           0.000e+0 |         never |     200.0 |    1.00x
           1.000e-6 |     1,000,000 |     208.0 |    1.04x
           2.500e-6 |       399,990 |     220.0 |    1.10x
           1.000e-5 |       100,000 |     280.0 |    1.40x
           1.000e-4 |        10,000 |    1000.0 |    5.00x
           1.000e-3 |         1,000 |    8199.8 |   41.00x
           1.000e-2 |           100 |   80198.0 |  400.99x
10% slower, HDD 8 ms: p < 2.5001e-6 = 1 / 399,990
 1% slower, HDD 8 ms: p < 2.5001e-7 = 1 / 3,999,900
10% slower, SSD 0.1 ms: p < 2.0040e-4 = 1 / 4,990

টেবিলটি দেখায় EAT প্রায় p-এর সরলরৈখিক: p দশগুণ বাড়লে বাড়তি সময়ও দশগুণ। ১০০টিতে একটি fault হলে মেমরি চারশো গুণ ধীর — কার্যত সিস্টেম থেমে যাওয়া, যাকে পর্ব ৩৭-এ thrashing বলা হবে। শেষ লাইনটি আধুনিক বাস্তবতা: SSD-তে service time আশি গুণ ছোট বলে একই ১০% সীমায় p প্রায় আশি গুণ বড় হতে পারে (১/৪,৯৯০)। তবু RAM আর ডিস্কের ফারাক এখনো শত বা হাজার গুণ — তাই page fault কম রাখাই virtual memory-র সাফল্যের শর্ত।

Numerical-এ সবচেয়ে সাধারণ ভুল একক। ms আর ns মিশিয়ে ফেললে উত্তর দশ লক্ষ গুণ ভুল হয়। আগে সব ns-এ আনুন (1 ms = 106 ns, 1 µs = 103 ns). দ্বিতীয় ভুল: (1 − p) বাদ দিয়ে EAT = ma + p × service লেখা — p ছোট হলে ফল প্রায় একই, কিন্তু সীমা বের করার প্রশ্নে হর (service − ma)-র বদলে service লিখলে শেষ অঙ্কে গরমিল হয়। প্রশ্নে “fault হলেও পরে মেমরি-অ্যাক্সেস লাগে” বলা থাকলে service time-এর সাথে ma যোগ করুন — প্রশ্নের শর্তই চূড়ান্ত।

৫ · Copy-on-write — fork দ্রুত হয় যেভাবে

পর্ব ১০-এ দেখেছেন, fork() parent-এর একটি প্রায় হুবহু কপি বানায়, আর বেশিরভাগ সময় child প্রায় সাথে সাথে exec করে সব ফেলে দেয়। ২ GB-এর একটি প্রসেস fork করতে যদি সত্যিই ২ GB কপি করতে হতো, প্রতিটি shell কমান্ডে সেকেন্ড চলে যেত। সমাধান copy-on-write (COW): fork শুধু page table কপি করে, আর লেখার-যোগ্য private page-গুলো দুই দিকেই শুধু-পড়া করে দেয়। যতক্ষণ কেউ শুধু পড়ে, সবাই একই frame ভাগ করে। কেউ লিখতে গেলে protection fault — kernel তখন শুধু সেই একটি page কপি করে।

নিচের ডায়াগ্রামে দুটি page: A (কোড, সবসময় শুধু-পড়া) আর D (ডেটা, মান 10). R মানে শুধু-পড়া, RW মানে পড়া-লেখা। “refcount” হলো frame-টি কতজনের page table-এ আছে:

1parent: D = 102pid = fork()3child: x = D        (read)4child: D = 99       → write fault5child: write retried on F56parent: D = 11      → write fault
Page table → frame
parent A
parent D
child A
child D
F2-এর refcount
D: parent / child
মোট page fault
কপি হওয়া page
  1. fork-এর আগে। Parent-এর কোড page A আছে F1-এ (শুধু-পড়া), ডেটা page D আছে F2-এ (পড়া-লেখা), D-র মান 10. F2 শুধু parent-এর, refcount ১।
  2. fork — কপি হলো শুধু page table. Child পেল parent-এর page table-এর কপি: A আর D দুজনেরই F1 আর F2-তে। Kernel D-কে দুই দিকেই শুধু-পড়া করে “COW” চিহ্ন দিল; F2-এর refcount ২। একটি বাইট ডেটাও কপি হয়নি — fork-এর খরচ page table-এর আকারের অনুপাতে, মেমরির আকারের নয়।
  3. Child পড়ল — কিছুই ঘটল না। পড়া শুধু-পড়ার অনুমতিতেই চলে, তাই কোনো fault নেই। দুজনেই F2 থেকে 10 দেখছে। যে প্রসেস fork-এর পর শুধু পড়ে বা সাথে সাথে exec করে, তার জন্য COW প্রায় বিনামূল্যে।
  4. Child লিখতে গেল — write fault. F2 শুধু-পড়া, তাই MMU protection fault তুলল। Handler দেখল VMA-তে লেখার অনুমতি আছে কিন্তু page COW, আর refcount ২ — অর্থাৎ ভাগ করা। তাই নতুন frame F5 নিয়ে F2-এর পুরো 4 KiB কপি করল, child-এর D-কে F5 RW করল, F2-এর refcount নেমে ১। মোট fault ১, কপি ১।
  5. লেখাটি আবার চালানো হলো। Instruction restart — এবার child-এর D F5-এ, লেখার অনুমতি আছে, মান হলো 99. Parent-এর F2-এ এখনো 10: দুজনের মেমরি এখন সত্যিই আলাদা, কিন্তু শুধু এই একটি page-এর জন্য খরচ হয়েছে।
  6. Parent লিখল — fault, কিন্তু কপি নয়। Parent-এর D এখনো শুধু-পড়া চিহ্নিত, তাই আবার fault. কিন্তু এবার handler দেখল F2-এর refcount ১ — আর কেউ ভাগ করছে না। তাই কপি না করে শুধু অনুমতি RW করে দিল (Linux-এ একে page “reuse” বলে)। D = 11. মোট fault ২, কিন্তু কপি এখনো ১টিই।

এবার আসল Linux-এ মেপে দেখুন। Parent ৮ MiB (2048টি page) ভরে fork করে; child প্রথমে সব page পড়ে, তারপর সব page-এ লেখে, আর প্রতিবার নিজের minor fault গোনে। শেষে parent যাচাই করে তার ডেটা অক্ষত কি না:

#include <stdio.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <unistd.h>

static long minor_faults(void) {
    struct rusage ru; getrusage(RUSAGE_SELF, &ru); return ru.ru_minflt;
}

int main(void) {
    size_t pg = (size_t)sysconf(_SC_PAGESIZE), n = 2048;   /* 8 MiB */
    char *buf = mmap(NULL, n * pg, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (buf == MAP_FAILED) { perror("mmap"); return 1; }
    madvise(buf, n * pg, MADV_NOHUGEPAGE);
    for (size_t i = 0; i < n; i++) buf[i * pg] = 'P';   /* parent fills it */
    fflush(stdout);                                     /* nothing buffered at fork */
    pid_t pid = fork();
    if (pid == 0) {
        long f0 = minor_faults(), sum = 0;
        for (size_t i = 0; i < n; i++) sum += buf[i * pg];  /* read only */
        long f1 = minor_faults();
        for (size_t i = 0; i < n; i++) buf[i * pg] = 'C';   /* now write */
        long f2 = minor_faults();
        printf("child : read  %zu pages -> %ld faults (sum ok: %d)\n", n, f1 - f0, sum == 'P' * (long)n);
        printf("child : write %zu pages -> %ld faults\n", n, f2 - f1);
        fflush(stdout); _exit(0);
    }
    waitpid(pid, NULL, 0);
    size_t still_p = 0;
    for (size_t i = 0; i < n; i++) still_p += (buf[i * pg] == 'P');
    printf("parent: %zu of %zu pages still hold 'P'\n", still_p, n);
    return 0;
}

আসল রানের আউটপুট (fault-এর সংখ্যা রান ভেদে সামান্য বদলাতে পারে, সম্পর্কটি নয়):

child : read  2048 pages -> 0 faults (sum ok: 1)
child : write 2048 pages -> 2048 faults
parent: 2048 of 2048 pages still hold 'P'

ডায়াগ্রামের প্রতিটি ধাপ এখানে সংখ্যায়: পড়ায় ০টি fault — fork-এর পর child-এর page table-এ parent-এর সব frame শুধু-পড়া হিসেবে বসানো ছিল, পড়া সরাসরি চলেছে। লেখায় page প্রতি একটি fault — প্রতিটি লেখা একটি COW কপি ঘটিয়েছে, মোট 2048টি। আর parent-এর 2048টি page-এই এখনো 'P' — child-এর লেখা তার নিজস্ব কপিতে গেছে। fflush(stdout) fork-এর আগে রাখা অভ্যাসবশত (পর্ব ১০): buffer-এ কিছু থাকলে সেটিও কপি হয়ে দুবার ছাপা হতো। আর child _exit ব্যবহার করেছে, যাতে parent-এর stdio-র কিছু আবার flush না হয়।

vfork — COW-এর আগের সমাধান

COW সব হার্ডওয়্যারে সহজ ছিল না। তাই BSD Unix-এ (3.0BSD, ১৯৭০-এর দশকের শেষে) আসে vfork(): child parent-এর মেমরি কপি না করে সরাসরি ব্যবহার করে, আর parent থেমে থাকে যতক্ষণ না child exec বা _exit করে। Page table কপির খরচটুকুও নেই, কিন্তু নিয়ম কঠোর — child কোনো ভ্যারিয়েবল বদলালে সেটা parent-এর মেমরিতেই বদলায়, আর ফাংশন থেকে return করলে parent-এর stack নষ্ট হয়। COW-এর তুলনা:

বিষয়fork (COW)vfork
মেমরিযৌক্তিকভাবে আলাদা; লিখলে page কপিএকই মেমরি, কোনো কপি নয়
Page tableকপি হয় (বড় প্রসেসে কিছুটা খরচ)কপি হয় না
Parentসাথে সাথে চলতে থাকেchild exec বা _exit না করা পর্যন্ত থেমে
Child যা করতে পারেযা খুশিকার্যত শুধু exec বা _exit
আজকের ব্যবহারসাধারণ নিয়মবিরল; glibc-এর posix_spawn ভেতরে একই ধারণা (CLONE_VM + CLONE_VFORK) নিরাপদে ব্যবহার করে

Zero-fill-on-demand

COW-এর একটি আত্মীয় হলো zero-fill-on-demand: নতুন heap, stack বা BSS-এর page প্রথম লেখার মুহূর্তে একটি খালি frame নিয়ে শূন্য দিয়ে ভরে দেওয়া হয় — যা প্রথম C প্রোগ্রামে দেখেছেন। শূন্য করা বাধ্যতামূলক: frame-টিতে হয়তো আগে অন্য কারও পাসওয়ার্ড ছিল। Linux আরেক ধাপ এগোয়: অছোঁয়া anonymous মেমরি যদি শুধু পড়া হয়, kernel সবার page table-এ একটিমাত্র শেয়ার করা zero page শুধু-পড়া হিসেবে বসায়; লিখলে তবেই আসল frame (আরেকটি fault). তাই Linux-এ “আগে পড়ে পরে লিখলে” page প্রতি সাধারণত দুটি minor fault হয়। Windows-এ এই ধরনের page-কে বলে “demand-zero” page, আর kernel-এর একটি থ্রেড অবসরে খালি frame আগেভাগে শূন্য করে রাখে।

৬ · Memory-mapped file আর anonymous memory

Demand paging-এর যন্ত্রটি তৈরি হয়ে গেলে একটি চমৎকার জিনিস বিনামূল্যে পাওয়া যায়: ফাইলকেও মেমরির মতো ব্যবহার করা। mmap দিয়ে একটি ফাইল map করলে kernel শুধু একটি VMA লেখে — “এই ঠিকানা-পরিসর অমুক ফাইলের অমুক অংশ”। প্রথম ছোঁয়ায় page fault, kernel ফাইলের সেই page এনে দেয়। read() লাগে না, buffer-এ কপি লাগে না। পর্ব ১২-এ দেখেছেন MAP_SHARED দিয়ে দুই প্রসেসের shared memory বানানো যায়; এখানে দেখব ফাইলের দিক থেকে।

ফাইল-ভিত্তিক (file-backed)

পেছনে একটি ফাইল। প্রোগ্রামের কোড, shared library, mmap করা ডেটা ফাইল। পরিষ্কার page সরাতে হলে শুধু ফেলে দিলেই হয় — ফাইলে কপি আছে।

backing store = ফাইল

Anonymous

পেছনে কোনো ফাইল নেই: heap, stack, MAP_ANONYMOUS. প্রথম ছোঁয়ায় শূন্য-ভরা page; সরাতে হলে লিখতে হয় swap-এ। Swap না থাকলে এগুলো সরানোই যায় না।

backing store = swap

ফাইল-ম্যাপিংয়ের আবার দুই রূপ: MAP_PRIVATE — লিখলে COW, নিজের কপি বদলায়, ফাইল অক্ষত (প্রোগ্রামের data অংশ এভাবেই map হয়: global ভ্যারিয়েবল বদলালে executable ফাইল বদলায় না)। আর MAP_SHARED — লেখা সরাসরি ফাইলের page-এ যায়। দুটিই পাশাপাশি দেখুন:

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

static void show(int fd, const char *when) {      /* read() the file itself */
    char buf[64] = {0};
    if (pread(fd, buf, sizeof buf - 1, 0) < 0) perror("pread");
    printf("%-22s file says: %s", when, buf);
}

int main(void) {
    const char *text = "exam on sunday at 10 am\n";
    size_t len = strlen(text);
    int fd = open("notice.txt", O_RDWR | O_CREAT | O_TRUNC, 0600);
    if (fd < 0 || write(fd, text, len) != (ssize_t)len) { perror("setup"); return 1; }
    show(fd, "start:");
    char *pv = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
    char *sh = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (pv == MAP_FAILED || sh == MAP_FAILED) { perror("mmap"); return 1; }
    memcpy(pv + 8, "friday", 6);                 /* private copy only */
    show(fd, "after MAP_PRIVATE:");
    memcpy(sh + 8, "monday", 6);                 /* goes to the page cache */
    if (msync(sh, len, MS_SYNC) != 0) perror("msync");   /* and to disk */
    show(fd, "after MAP_SHARED:");
    printf("private mapping sees  : %.*s", (int)len, pv);

    munmap(pv, len);
    munmap(sh, len);
    close(fd);
    unlink("notice.txt");
    return 0;
}

আসল রানের আউটপুট:

start:                 file says: exam on sunday at 10 am
after MAP_PRIVATE:     file says: exam on sunday at 10 am
after MAP_SHARED:      file says: exam on monday at 10 am
private mapping sees  : exam on friday at 10 am

Private ম্যাপিংয়ে “friday” লেখায় একটি COW fault হয়েছে — প্রসেস ফাইলের page-এর নিজস্ব কপি পেয়েছে, তাই pread দিয়ে ফাইল পড়লে এখনো “sunday”. Shared ম্যাপিংয়ে “monday” লেখা গেছে সরাসরি ফাইলের page-এ, আর pread সেটিই দেখছে। শেষ লাইনটি খেয়াল করুন: private ম্যাপিং এখনো নিজের “friday” দেখছে — shared-এর পরের বদলটি সে পায়নি, কারণ লেখার মুহূর্তেই তার page আলাদা হয়ে গেছে। ফাইলটি চলমান ফোল্ডারে তৈরি, শেষে মুছে ফেলা হয়েছে; অনুমতি 0600 — শুধু মালিক।

Page cache — read আর mmap একই page দেখে

কেন pread সাথে সাথে বদলটি দেখল? কারণ Linux-এ ফাইলের ডেটা RAM-এ থাকে একটি জায়গাতেই — page cache-এ। read() সেখান থেকে কপি করে, আর mmap সেই একই frame সরাসরি page table-এ বসায়। ফলে MAP_SHARED-এ লেখামাত্র যেকোনো read() তা দেখে — msync ছাড়াই। তাহলে msync কেন? দেখার জন্য নয়, টিকিয়ে রাখার জন্য। Dirty page kernel নিজের সময়মতো ডিস্কে লেখে (সাধারণত কয়েক সেকেন্ডের মধ্যে); তার আগে বিদ্যুৎ চলে গেলে বদল হারাবে। msync(MS_SYNC) নিশ্চিত করে লেখা ডিস্কে পৌঁছেছে — ঠিক যেমন write-এর পর fsync. সব Unix-এ read আর mmap-এর এই মিল নিশ্চিত ছিল না (পুরোনো কিছু সিস্টেমে আলাদা buffer cache ছিল), তাই বহনযোগ্য কোডে msync-এর পরেই অন্যের দেখার ওপর ভরসা করুন।

mmap-এর দুটি ফাঁদ। (১) ফাইলের আকারের বাইরের page ছুঁলে Linux-এ SIGBUS আসে — অন্য প্রসেস ফাইলটি ছোট (truncate) করে দিলে চলমান প্রোগ্রামও মরতে পারে। (২) ডিস্কের পড়ার ত্রুটি read()-এ একটি error code হয়ে আসে, কিন্তু mmap-এ সেটি আসে signal হয়ে, যেকোনো সাধারণ pointer-পড়ার মাঝখানে। এজন্য ডেটাবেস-নকশাকারীরা mmap নিয়ে দ্বিধায় থাকেন — ভাবনার প্রশ্ন ৪ দেখুন।

৭ · Free-frame pool, overcommit আর RAM ফুরোলে

Free-frame pool

Page fault সামলাতে খালি frame লাগে, আর fault-এর মুহূর্তে victim খুঁজে তাকে ডিস্কে লেখা ধীর। তাই kernel সবসময় কিছু খালি frame-এর একটি তালিকা — free-frame pool — ভরা রাখার চেষ্টা করে। Linux-এ প্রতিটি মেমরি-zone-এর তিনটি “watermark” আছে (min, low, high): খালি frame low-এর নিচে নামলে kswapd নামের kernel থ্রেড পটভূমিতে page সরিয়ে তালিকা high পর্যন্ত ভরে; min-এর নিচে নামলে মেমরি চাওয়া প্রসেসকে নিজেই সরানোর কাজ করতে হয় (direct reclaim), যা আরও ধীর। Windows-এ একই কাজ হয় free, zeroed আর standby তালিকা দিয়ে।

Overcommit — প্রতিশ্রুতি বেশি, মজুত কম

প্রথম C প্রোগ্রামে mmap-এ ১৬ MiB চেয়ে ০টি frame পেয়েছেন। তাহলে কেউ যদি RAM আর swap মিলিয়ে যা আছে তার চেয়ে বেশি চায়? Linux সাধারণত “হ্যাঁ” বলে — এর নাম overcommit. যুক্তি: বেশিরভাগ প্রোগ্রাম যা চায় তার সবটা কখনো ছোঁয় না (বড় buffer, fork-এর পর COW page যা কখনো লেখা হয় না, thread-এর ৮ MiB stack-এর অল্প অংশ)। Linux-এ নিয়ন্ত্রণ /proc/sys/vm/overcommit_memory (sysctl vm.overcommit_memory) দিয়ে — আচরণ kernel সংস্করণে কিছুটা বদলেছে, তাই সঠিক বিবরণ kernel-এর ডকুমেন্টেশন (Documentation/mm/overcommit-accounting.rst) মিলিয়ে নিন:

মাননামমোটামুটি আচরণ
0 (ডিফল্ট)heuristicস্পষ্টতই অসম্ভব চাওয়া (যেমন RAM + swap-এর চেয়ে বহুগুণ বড় এক চাওয়া) ফেরায়, বাকিটায় সাধারণত রাজি
1alwaysকখনো ফেরায় না — কিছু বৈজ্ঞানিক প্রোগ্রাম বিশাল sparse অ্যারের জন্য এটি চায়
2strict (don't overcommit)মোট প্রতিশ্রুতি (Committed_AS) একটি সীমা (CommitLimit ≈ swap + RAM × overcommit_ratio/100, ডিফল্ট ratio ৫০) ছাড়ালে malloc/mmap ব্যর্থ হয়

Windows ভিন্ন পথে চলে: প্রতিটি “commit” করা বাইটের জন্য RAM বা pagefile-এ জায়গা রাখতে হয়। মোট commit charge commit limit (মোটামুটি RAM + pagefile-এর আকার) ছাড়াতে পারে না, ছাড়ালে VirtualAlloc ব্যর্থ হয় (বা pagefile বড় হয়)। অর্থাৎ Windows প্রতিশ্রুতির হিসাব রাখে, Linux-এর ডিফল্ট আশাবাদী। macOS আর iOS-এ মেমরি চাপের সময় প্রথম ভরসা memory compression — অল্প ব্যবহৃত page RAM-এর ভেতরেই চেপে ছোট করে রাখা (macOS-এ OS X Mavericks, ২০১৩ থেকে); macOS দরকারে /private/var/vm-এ swap ফাইলও বানায়, আর iOS সাধারণত disk swap-এর বদলে অ্যাপ বন্ধ করে দেয়।

RAM সত্যিই ফুরোলে

Overcommit-এর দাম আসে পরে: প্রতিশ্রুত মেমরি একসাথে ছোঁয়া হলে page fault-এর সময় খালি frame নেই। Kernel তখন পরপর চেষ্টা করে:

  1. Page cache-এর পরিষ্কার ফাইল page ফেলে দেওয়া — সবচেয়ে সস্তা।
  2. Dirty ফাইল page ডিস্কে লিখে ফেলা, আর anonymous page swap-এ পাঠানো। কাকে সরাবে, সেটাই page replacement algorithm — পর্ব ৩৬-এর বিষয়।
  3. এতেও না কুলালে, আর বারবার সরানো-আনায় সিস্টেম প্রায় থেমে গেলে (thrashing, পর্ব ৩৭), শেষ উপায় OOM killer: একটি প্রসেস বেছে SIGKILL. Malloc ততক্ষণে বহু আগেই সফল হয়ে ফিরেছিল — তাই Linux-এ malloc-এর NULL পরীক্ষা মেমরি-স্বল্পতা ধরার নির্ভরযোগ্য উপায় নয়। OOM killer কাকে বাছে, oom_score_adj আর cgroup-এর মেমরি-সীমা কীভাবে কাজ করে — পর্ব ৩৮-এ।

আরও গভীরে: Linux-এ x86-এর page fault ঢোকে exc_page_fault() দিয়ে, তারপর do_user_addr_fault() VMA খোঁজে (৬.৪ থেকে অনেক ক্ষেত্রে পুরো address space-এর lock ছাড়া, per-VMA lock দিয়ে) আর handle_mm_fault()-কে ডাকে, যা অবস্থা দেখে বেছে নেয় do_anonymous_page() (zero-fill), do_fault() (ফাইল), do_swap_page() (swap) বা do_wp_page() (COW). ফাইলের read fault-এ kernel “fault-around” করে — page cache-এ থাকা আশপাশের কয়েকটি page-ও (ডিফল্টে ৬৪ KiB পর্যন্ত) একসাথে map করে দেয়, যাতে পরের fault এড়ানো যায়। আর userfaultfd দিয়ে একটি user-space প্রোগ্রাম নিজেই অন্য এলাকার page fault সামলাতে পারে; QEMU-র VM-এর “post-copy” live migration এর ওপর দাঁড়িয়ে — VM আগে নতুন মেশিনে চালু হয়, page আসে fault হলে তবেই।

৮ · AI-এর যুগে virtual memory

“আমার সার্ভারে মেমরি কম, কী করব?” — AI-কে জিজ্ঞেস করলে দ্রুত একগাদা কমান্ড আসবে, যার কিছু আপনার OS-এর জন্য নয়, কিছু বিপজ্জনক, আর কিছু শুধু পুরোনো ব্লগের প্রতিধ্বনি। Virtual memory-র ক্ষেত্রে বিশেষ সমস্যা হলো, ভুল পরামর্শও অনেক সময় “কাজ করছে” বলে মনে হয় — যতক্ষণ না চাপের মুহূর্তে OOM killer আসে। AI-এর সাধারণ ভুলগুলো:

AI-এর ভুলফলকীভাবে ধরবেন
“malloc NULL না দিলে মেমরি আছে”Linux-এ overcommit-এর কারণে পরে ছোঁয়ার সময় OOM kill, কোনো error নয়overcommit_memory দেখুন; বড় buffer ছুঁয়ে পরীক্ষা করুন; cgroup সীমায় চালিয়ে দেখুন
OOM ঠেকাতে vm.overcommit_memory=1 বা swap বন্ধ করার পরামর্শসমস্যা আরও খারাপ বা অন্য জায়গায় সরে যায়আগে মাপুন (free, vmstat, /proc/meminfo); পরিবর্তন production-এ নয়, পরীক্ষার মেশিনে
EAT-এ ms আর ns মেশানো, (1 − p) বাদ, বা সীমায় ভুল হরউত্তর হাজার বা লাখ গুণ ভুলসব ns-এ এনে নিজে হিসাব করুন; ছোট script-এ মিলিয়ে নিন
“MAP_PRIVATE-এ লিখলে ফাইলে যায়” বা “msync ছাড়া read বদল দেখবে না”ভুল নকশা, হারানো ডেটাছোট পরীক্ষা চালান (উপরের তৃতীয় প্রোগ্রামের মতো); man 2 mmap, man 2 msync
RSS-কে “প্রসেস কত মেমরি নিচ্ছে”-র চূড়ান্ত মাপ ধরাshared library আর page cache দুবার গোনা, ভুল সিদ্ধান্ত/proc/PID/smaps_rollup-এর PSS দেখুন; virtual (VSZ) আর resident (RSS) আলাদা করুন
Windows-এ /proc বা Linux-এ pagefile-এর নির্দেশনা; অনেক পুরোনো kernel-এর তথ্যকমান্ড চলে না বা ভুল সেটিং বদলায়প্রম্পটে OS, distro আর kernel সংস্করণ (uname -r) দিন

মেমরি-সমস্যায় AI-এর সাহায্য নিতে প্রেক্ষাপট, মাপা তথ্য আর যাচাইয়ের দাবি — তিনটিই দিন:

আমার পরিবেশ: Ubuntu 24.04 সার্ভার, kernel 6.8, 8 GB RAM, 2 GB swap. একটি Python ওয়েব অ্যাপ মাঝে মাঝে হঠাৎ মারা যায়, dmesg-এ "Out of memory: Killed process" দেখি। নিচে free -m, vmstat 1 5 আর /proc/meminfo-র Commit লাইনগুলো দিলাম: [এখানে আউটপুট পেস্ট করুন] ১. এই সংখ্যা থেকে সমস্যাটি minor fault, major fault/swap, না overcommit-এর — ধাপে ধাপে ব্যাখ্যা দাও। ২. যেকোনো sysctl বদলের পরামর্শ দিলে তার ঝুঁকি বলো, আর কোন কমান্ডে আগে-পরে মেপে আমি ফল যাচাই করব তা লেখো। ৩. chmod, sudo দিয়ে সব খুলে দেওয়া, swap বন্ধ বা overcommit_memory=1-এর মতো "সহজ সমাধান" দেবে না যদি না কারণসহ প্রমাণ দেখাও। ৪. কোন তথ্য kernel সংস্করণ-নির্ভর, আলাদা করে চিহ্নিত করো।

AI-এর পরামর্শ মানার আগে — চেকলিস্ট

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

  • Page fault হলে OS কী কী করে? — trap; ঠিকানা বৈধ কি না যাচাই (অবৈধ হলে প্রসেস শেষ); খালি frame নেওয়া (না থাকলে replacement); ডিস্ক থেকে পড়া, অপেক্ষার সময় অন্য প্রসেস চালানো; page table আপডেট; একই নির্দেশ আবার চালানো।
  • Demand paging আর pure demand paging-এর পার্থক্য? — দুটিতেই প্রথম ছোঁয়ায় page আসে; pure-এ শুরুতে একটিও page থাকে না, এমনকি প্রথম নির্দেশটিও fault করে আসে।
  • ma = 200 ns, service = 8 ms; ১০%-এর কম ধীর রাখতে p? — p < 20 / 7,999,800 = 1/399,990 ≈ 2.5 × 10−6.
  • Copy-on-write কী, fork-এ কেন লাগে? — fork-এ page table কপি, page দুই দিকে শুধু-পড়া; লিখলে fault আর শুধু সেই page কপি; ফলে fork+exec সস্তা।
  • Minor আর major page fault? — minor ডিস্ক ছাড়াই মেটে (zero-fill, COW, page cache-এ থাকা page), major-এ ডিস্ক বা swap থেকে পড়তে হয়।

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

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

প্র ০১Overlay দিয়ে প্রোগ্রামাররা তো কাজ চালাচ্ছিলেন — তাহলে virtual memory কেন জিতল, আর শুরুতে কেন অনেকে একে সন্দেহ করতেন?

১৯৫০-এর দশকে মেমরি ছিল খুব দামি আর ছোট। বড় প্রোগ্রাম লিখতে প্রোগ্রামাররা overlay পরিকল্পনা করতেন — কোন সাবরুটিন কোন সময়ে ড্রাম বা টেপ থেকে কোন ঠিকানায় আসবে, তার নিখুঁত হিসাব। প্রোগ্রামের একটি অংশ বদলালে পুরো পরিকল্পনা আবার করতে হতো, আর হিসাবে ভুল হলে প্রোগ্রাম নিজের কোড নিজেই মুছে দিত। সে যুগের নানা হিসাব অনুযায়ী প্রোগ্রামারের সময়ের একটি বড় অংশ যেত এই মেমরি-ব্যবস্থাপনায়।

১৯৬২ সালে Manchester-এর Atlas দেখাল কাজটি স্বয়ংক্রিয় করা যায়: page, page table-এর মতো একটি হার্ডওয়্যার কাঠামো, আর অনুপস্থিত page ছুঁলে একটি interrupt — আজকের page fault. কিন্তু সন্দেহও ছিল প্রবল। প্রথম দিকের অনেক virtual memory সিস্টেম ভয়ংকর ধীর হয়ে যেত — প্রোগ্রাম বেশি, মেমরি কম হলে সিস্টেম বেশিরভাগ সময় শুধু page সরাত আর আনত। প্রথম দিকের কিছু time-sharing সিস্টেমে এই সমস্যা বেশ পরিচিত ছিল; প্রকৌশলীরা প্রশ্ন তুলেছিলেন, একটি সাধারণ algorithm কি সত্যিই একজন দক্ষ প্রোগ্রামারের হাতে-করা overlay-কে হারাতে পারে?

উত্তর এসেছিল তত্ত্ব আর পরিমাপ থেকে। Peter Denning ১৯৬৮ সালে working set মডেল দিয়ে দেখালেন, প্রতিটি প্রসেসের সাম্প্রতিক ব্যবহৃত page-গুলো RAM-এ রাখতে পারলে fault খুব কমে যায়, আর না পারলে thrashing — অর্থাৎ সমস্যাটি virtual memory-র নয়, বেশি প্রসেস ঢোকানোর। ১৯৭০ সালে তাঁর জরিপ-প্রবন্ধ “Virtual Memory” এই জ্ঞানকে এক জায়গায় আনে। সেই বছরগুলোতেই IBM System/370 (১৯৭২ সালে virtual memory ঘোষণা) আর পরে DEC VAX (১৯৭৭) একে মূলধারায় নিয়ে আসে।

শেষ পর্যন্ত জয়ের কারণ অর্থনীতি: মেমরি সস্তা হচ্ছিল, প্রোগ্রামারের সময় দামি হচ্ছিল। আর virtual memory শুধু “বড় প্রোগ্রাম” নয়, আরও দিয়েছে — প্রসেসের মধ্যে সুরক্ষা, shared library, COW fork, mmap.

মূল উপলব্ধি: একটি বিমূর্ত ধারণা (একটি বড় সমতল মেমরি) গ্রহণযোগ্য হয় তখনই, যখন তার খরচ মাপা ও নিয়ন্ত্রণ করা যায়। Virtual memory জিতেছিল locality আর working set-এর তত্ত্ব তার খরচকে ব্যাখ্যা করতে পারার পরে।

প্র ০২Copy-on-write fork এত চমৎকার হলে Redis-এর মতো ডেটাবেস fork-এর সময় মেমরি দ্বিগুণ হওয়ার সতর্কতা দেয় কেন? Huge page এখানে কী করে?

Redis সব ডেটা RAM-এ রাখে। ডিস্কে snapshot নেওয়ার (BGSAVE) জন্য সে fork করে: child নিজের দেখা “জমে যাওয়া” মেমরির ছবি ধীরে ধীরে ফাইলে লেখে, আর parent ক্লায়েন্টদের সেবা চালিয়ে যায়। COW-এর কারণে fork-এর মুহূর্তে কোনো ডেটা কপি হয় না — কৌশলটির সৌন্দর্য এখানেই: snapshot-টি নিখুঁতভাবে একটি মুহূর্তের, অথচ কোনো lock লাগে না।

কিন্তু snapshot চলাকালীন parent যে page-এ লেখে, প্রতিটির একটি কপি হয়। লেখার চাপ বেশি হলে আর snapshot লম্বা হলে প্রায় সব page-ই একবার করে লেখা হয়ে যায় — তখন মেমরি ব্যবহার প্রায় দ্বিগুণ হতে পারে। তাই Redis-এর ডকুমেন্টেশন বলে, লেখার চাপ বেশি হলে ডেটার আকারের তুলনায় যথেষ্ট বাড়তি RAM রাখতে, আর Linux-এ vm.overcommit_memory = 1 রাখার পরামর্শও দেয় — কারণ strict accounting-এ (মান 2) fork-ই ব্যর্থ হতে পারে, যদিও আসলে সব page কখনো কপি হবে না।

Transparent huge page এখানে বিষয়টি আরও খারাপ করে। Page যদি 2 MiB-এর হয়, একটি ছোট লেখাতেই পুরো 2 MiB কপি হয় — 4 KiB-এর তুলনায় ৫১২ গুণ। তাতে মেমরি দ্রুত বাড়ে আর প্রতিটি COW fault-এর সময়ও বাড়ে, ফলে latency-তে হঠাৎ লাফ দেখা যায়। এজন্য Redis আর অনেক ডেটাবেস THP বন্ধ বা “madvise” মোডে রাখার পরামর্শ দেয়।

আরেকটি সূক্ষ্ম খরচ: fork নিজেও বিনামূল্যে নয়। কয়েক দশ GB-এর প্রসেসের page table-ই কয়েক দশ MB, আর সেটি কপি করতে fork মিলিসেকেন্ড থেকে শত মিলিসেকেন্ড পর্যন্ত নিতে পারে — সেই সময় parent থেমে থাকে।

মূল উপলব্ধি: COW খরচ মুছে দেয় না, পিছিয়ে দেয় — আর খরচটি আসে লেখার পরিমাণ অনুযায়ী। নকশার সময় জিজ্ঞেস করুন: fork-এর পর কতটা মেমরি লেখা হবে, আর page কত বড়?

প্র ০৩Linux আশাবাদী overcommit করে, Windows প্রতিটি commit-এর হিসাব রাখে। কোন দর্শন ঠিক?

দুটিই একটি সত্যিকারের trade-off-এর দুই প্রান্ত।

Linux-এর যুক্তি: Unix-এর প্রসেস তৈরির পদ্ধতি fork-ভিত্তিক। ১০ GB-এর একটি প্রসেস fork করলে কঠোর হিসাবে আরও ১০ GB-এর প্রতিশ্রুতি লাগে, যদিও child সাথে সাথে exec করে কিছুই লিখবে না। Thread-এর stack (প্রায়ই ৮ MiB সংরক্ষিত), বড় কিন্তু sparse অ্যারে, JVM-এর মতো runtime-এর বিশাল heap সংরক্ষণ — সবখানে প্রতিশ্রুতি আর বাস্তব ব্যবহারের ফারাক বিশাল। কঠোর হিসাব রাখলে অনেক বেশি RAM বা swap লাগত, বা প্রোগ্রাম অকারণে ব্যর্থ হতো। তাই Linux-এর ডিফল্ট heuristic: স্পষ্টতই অসম্ভব চাওয়া ফেরানো, বাকিতে বিশ্বাস রাখা।

দাম: ব্যর্থতা আসে ভুল জায়গায়, ভুল সময়ে। malloc সফল, কিন্তু অনেক পরে একটি সাধারণ লেখার নির্দেশে মেমরি নেই — প্রোগ্রাম কিছু সামলানোর সুযোগই পায় না। OOM killer যাকে মারে, সে হয়তো দোষী নয়। নির্ভরযোগ্যতা-প্রধান সিস্টেমে (কিছু embedded, ব্যাংকিং) তাই মান 2 রাখা হয়, সাথে যথেষ্ট swap.

Windows-এর দর্শন: প্রসেস তৈরি হয় CreateProcess দিয়ে, fork নেই, তাই প্রতিশ্রুতির বিস্ফোরণও কম। প্রতিটি commit-এর জন্য RAM বা pagefile-এ জায়গা নিশ্চিত, ফলে ব্যর্থতা আসে VirtualAlloc-এর ফেরত মানে — সামলানো যায়। দাম: pagefile বড় রাখতে হয়, আর “commit” না করে শুধু “reserve” করার মতো আলাদা ধাপ প্রোগ্রামারকে বুঝতে হয়।

আধুনিক মাঝামাঝি পথ: container-এ cgroup-এর মেমরি-সীমা — পুরো মেশিন নয়, প্রতিটি সার্ভিসের নিজস্ব সীমা; সীমা ছাড়ালে শুধু সেই দলের ভেতরে OOM (পর্ব ৩৮, ৫১)।

মূল উপলব্ধি: প্রশ্নটি “কোনটা ঠিক” নয়, বরং “ব্যর্থতা কোথায় দেখতে চান” — বরাদ্দের মুহূর্তে, সামলানোর সুযোগসহ, নাকি ব্যবহারের মুহূর্তে, বেশি দক্ষতার বিনিময়ে।

প্র ০৪mmap দিয়ে ফাইলকে মেমরির মতো পড়া এত সহজ — তাহলে অনেক ডেটাবেস-গবেষক কেন বলেন “DBMS-এ mmap ব্যবহার করবেন না”?

mmap-এর আকর্ষণ স্পষ্ট: আলাদা buffer pool লিখতে হয় না, OS নিজেই page আনে-সরায়, কপি কম, কোড সরল। LMDB-র মতো কিছু সফল key-value স্টোর এর ওপরই দাঁড়িয়ে, আর MongoDB-র প্রথম দিকের স্টোরেজ ইঞ্জিন MMAPv1-ও ছিল mmap-ভিত্তিক।

কিন্তু ২০২২ সালে CIDR কনফারেন্সে Andrew Crotty, Viktor Leis আর Andrew Pavlo-র প্রবন্ধ “Are You Sure You Want to Use MMAP in Your Database Management System?” কয়েকটি মৌলিক সমস্যা তুলে ধরে:

  • লেখার ক্রম নিয়ন্ত্রণে নেই: kernel যেকোনো সময় dirty page ডিস্কে লিখতে পারে। কিন্তু ডেটাবেসের নিয়ম (write-ahead logging, পর্ব ৪৪ ও DBMS কোর্সের recovery অংশ) বলে, log আগে, ডেটা পরে। mmap-এ এটি নিশ্চিত করতে জটিল কৌশল লাগে।
  • I/O ত্রুটি signal হয়ে আসে: ডিস্কের সমস্যা read()-এ error code, কিন্তু mmap-এ SIGBUS — যেকোনো pointer-পড়ার মাঝখানে।
  • page fault-এর অনিশ্চিত দেরি: কোন অ্যাক্সেস ন্যানোসেকেন্ডে আর কোনটি মিলিসেকেন্ডে, প্রোগ্রাম আগে জানে না; অপেক্ষার সময় থ্রেডটি আটকে থাকে, অন্য কাজ করতে পারে না।
  • বড় মেশিনে scalability: page বের করে দেওয়ার সময় TLB shootdown (সব core-কে অনুবাদ মুছতে বলা) আর kernel-এর ভেতরের lock-বিবাদ বেশি thread-এ গতি আটকে দেয় — প্রবন্ধে মাপা হয়েছে।

MongoDB পরে WiredTiger ইঞ্জিনকে ডিফল্ট করে (৩.২ সংস্করণ, ২০১৫) আর MMAPv1 সরিয়ে দেয়। অন্যদিকে LMDB-র মতো read-প্রধান, single-writer নকশায় mmap এখনো ভালো কাজ করে।

মূল উপলব্ধি: virtual memory একটি সাধারণ-উদ্দেশ্যের নীতি নেয় — কোন page রাখবে, কখন লিখবে। যে প্রোগ্রাম নিজের ডেটার আচরণ OS-এর চেয়ে ভালো জানে, তার জন্য নিয়ন্ত্রণ হাতে রাখাই প্রায়ই ভালো।

প্র ০৫একটি page fault-এ যদি মিলিসেকেন্ড যায়, তাহলে শেয়ারবাজারের trading সিস্টেম বা লাইভ অডিওর মতো latency-সংবেদনশীল প্রোগ্রাম কীভাবে টিকে থাকে?

Demand paging গড় সময়ের জন্য চমৎকার, কিন্তু অনিশ্চয়তা আনে: কোন অ্যাক্সেসে হঠাৎ fault হবে, আগে জানা যায় না। একটি অডিও প্রোগ্রামের কাছে প্রতি কয়েক মিলিসেকেন্ডে buffer ভরার দায়, একটি major fault মানেই শ্রোতার কানে “ক্লিক”। Trading সিস্টেমে কয়েক মাইক্রোসেকেন্ডের দেরিও প্রতিযোগিতায় হার। তাই এমন প্রোগ্রাম কয়েকটি কৌশল একসাথে ব্যবহার করে:

  • আগেই fault করিয়ে রাখা (prefault): চালু হওয়ার সময়ই সব buffer আর stack-এর গভীর অংশ একবার ছুঁয়ে আসা, বা mmap-এ MAP_POPULATE দেওয়া — যাতে আসল কাজের সময় কোনো zero-fill fault না হয়। প্রথম C প্রোগ্রামের প্রথম লুপটি ঠিক এই কাজ করে।
  • RAM-এ আটকে রাখা: mlock() বা mlockall(MCL_CURRENT | MCL_FUTURE) দিয়ে page-গুলো swap-এ যাওয়া নিষেধ করা। Linux-এ এতে সাধারণত অনুমতি বা RLIMIT_MEMLOCK-এর সীমা লাগে; Windows-এ কাছাকাছি কাজ করে VirtualLock. PREEMPT_RT-এর মতো real-time Linux-এর নির্দেশিকা প্রায় সবসময় এটি দিয়ে শুরু হয়।
  • বড় page: huge page-এ TLB miss কমে, আর একবার বসলে fault-ও কম।
  • গরম পথে বরাদ্দ নয়: চালু হওয়ার পর malloc/free এড়িয়ে আগে থেকে বানানো pool থেকে বস্তু নেওয়া।
  • swap না রাখা বা খুব কম রাখা: কিছু সার্ভারে এটি নীতি, যদিও তখন anonymous page সরানোর পথ বন্ধ, তাই OOM-এর ঝুঁকি বাড়ে।

EAT-এর ভাষায়: এই প্রোগ্রামগুলো গড় নয়, সবচেয়ে খারাপ ক্ষেত্রটি (tail latency) নিয়ে চিন্তিত, তাই তারা p-কে কাজের সময় শূন্যের কাছে নামিয়ে দেয় — খরচটা আগে, চালু হওয়ার সময় মিটিয়ে।

মূল উপলব্ধি: virtual memory-র lazy নীতি গড় দক্ষতা দেয়, নিশ্চয়তা নয়। নিশ্চয়তা দরকার হলে প্রোগ্রামকে স্পষ্টভাবে বলতে হয় “এখনই আনতে হবে, আর কখনো সরানো যাবে না”।

অনুশীলন

খাতা-কলম নিন — একটি হিসাব, একটি হাতে-ট্রেস, একটি আউটপুট অনুমান আর একটি কমান্ড।

  1. EAT হিসাব: ma = 100 ns, page-fault service time = 10 ms, আর গড়ে ৫০,০০০ অ্যাক্সেসে একটি page fault. (ক) EAT কত? মেমরি কত গুণ ধীর? (খ) সর্বোচ্চ ১০% ধীর মানতে হলে p-এর সীমা কত?

    (ক) 10 ms = 10,000,000 ns, p = 1/50,000 = 0.00002. EAT = (1 − 0.00002) × 100 + 0.00002 × 10,000,000 = 99.998 + 200 = 299.998 ns ≈ 300 ns. অর্থাৎ প্রায় ৩ গুণ ধীর — পঞ্চাশ হাজারে একটি fault-ও সহনীয় নয়।

    (খ) EAT < 110 ⇒ 100 + (10,000,000 − 100) × p < 110 ⇒ 9,999,900 × p < 10 ⇒ p < 1/999,990 ≈ 1.0 × 10−6 — প্রায় দশ লক্ষে একটি।

  2. হাতে ট্রেস — pure demand paging: একটি প্রসেসের জন্য ৪টি খালি frame আছে, শুরুতে কোনো page RAM-এ নেই। প্রসেস এই ক্রমে page ছোঁয়: 0 2 0 5 2 0 3 5 1 0. প্রথম ৯টি অ্যাক্সেসের প্রতিটি fault কি না লিখুন। এই ৯টিতে মোট কতটি fault? কোন অ্যাক্সেসে প্রথমবার page replacement লাগবে? আর শেষ অ্যাক্সেসটি (page 0) fault হবে কি না, তা কিসের ওপর নির্ভর করে?

    0 → fault (প্রথম frame), 2 → fault (দ্বিতীয়), 0 → hit, 5 → fault (তৃতীয়), 2 → hit, 0 → hit, 3 → fault (চতুর্থ, খালি frame শেষ), 5 → hit, 1 → fault, কিন্তু খালি frame নেই — ৯ নম্বর অ্যাক্সেসে (page 1) প্রথম replacement লাগবে।

    প্রথম ৯টিতে মোট fault = ৫টি — প্রতিটি ভিন্ন page-এর প্রথম ছোঁয়া (0, 2, 5, 3, 1). শেষ অ্যাক্সেস page 0 fault হবে শুধু যদি page 1-এর জন্য victim হিসেবে page 0-কেই সরানো হয়; কাকে সরানো হবে, সেটা replacement algorithm-এর সিদ্ধান্ত (পর্ব ৩৬)। মনে রাখুন: pure demand paging-এ fault-এর সংখ্যা কমপক্ষে ভিন্ন page-এর সংখ্যার সমান — কোনো algorithm এগুলো এড়াতে পারে না (compulsory miss).

  3. আউটপুট অনুমান: প্রথম C প্রোগ্রামে (16 MiB, 4096টি page) দুটি বদল ভাবুন। (ক) প্রথম লুপ p[i * pg]-এর বদলে p[i * (pg / 2)]-এ লেখে, i < 2 * n পর্যন্ত — অর্থাৎ ৮১৯২টি লেখা। প্রথম ছোঁয়ার fault কত? (খ) প্রথম লুপটি লেখার বদলে শুধু পড়ে (sum += p[i * pg]), তারপর দ্বিতীয় লুপ লেখে। Linux-এ দুই লুপে মোটামুটি কত fault আশা করবেন?

    (ক) এখনো প্রায় 4096টি। প্রতিটি page-এ দুটি লেখা, কিন্তু fault শুধু প্রথমটিতে — দ্বিতীয়টির সময় page ইতিমধ্যে valid. লেখা প্রতি 0.5 fault, page প্রতি 1.0.

    (খ) Linux-এ সাধারণত প্রথম লুপে প্রায় 4096টি (প্রতিটি page-এ শেয়ার করা zero page শুধু-পড়া হিসেবে বসে) আর দ্বিতীয় লুপে আবার প্রায় 4096টি (লেখার জন্য আসল frame দিতে হয়) — মোট প্রায় দ্বিগুণ। Kernel-এর কনফিগারেশন ও সংস্করণে এটি বদলাতে পারে, তাই নিজে চালিয়ে মিলিয়ে নিন। শিক্ষা: “শুধু পড়লাম” মানেই fault-মুক্ত নয়।

  4. কমান্ড লিখুন: Linux-এ (ক) বর্তমান shell প্রসেসের minor ও major fault আর RSS দেখার একটি ps কমান্ড, (খ) overcommit-এর মোড আর মোট প্রতিশ্রুতি দেখার কমান্ড, (গ) একটি কমান্ড (যেমন sort) চালিয়ে তার fault-সংখ্যা জানার উপায়।
    $ ps -o pid,min_flt,maj_flt,rss,cmd -p $$
        PID  MINFL  MAJFL   RSS CMD
       4121   1873      2  5320 bash
    $ cat /proc/sys/vm/overcommit_memory
    0
    $ grep -E 'CommitLimit|Committed_AS' /proc/meminfo
    CommitLimit:     8104052 kB
    Committed_AS:    6230416 kB
    $ /usr/bin/time -v sort big.txt > /dev/null 2> t.txt; grep -i 'page faults' t.txt
    	Major (requiring I/O) page faults: 0
    	Minor (reclaiming a frame) page faults: 2614

    $$ হলো বর্তমান shell-এর PID. Committed_AS CommitLimit ছাড়িয়ে গেলেও মোড 0-এ সমস্যা নেই — সীমাটি কঠোরভাবে মানা হয় শুধু মোড 2-এ। /usr/bin/time হলো GNU time প্রোগ্রাম (shell-এর বিল্ট-ইন time নয়), কিছু distro-তে আলাদা করে ইনস্টল করতে হয়। Windows-এ একই তথ্য Resource Monitor-এর Memory ট্যাবে (Hard Faults/sec, Commit) পাওয়া যায়।

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

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

ক্রমএকটি major page fault সামলানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. MMU দেখে page table entry invalid — trap করে kernel-এ যায়
  2. Kernel যাচাই করে ঠিকানাটি প্রসেসের কোনো বৈধ এলাকায় (VMA) আছে কি না
  3. Free-frame pool থেকে একটি খালি frame নেওয়া হয়
  4. ডিস্ক থেকে page পড়ার অনুরোধ, প্রসেস blocked, CPU অন্য প্রসেস চালায়
  5. পড়া শেষে page table-এ frame বসিয়ে valid করা হয়
  6. যে নির্দেশে fault হয়েছিল, সেটি আবার শুরু থেকে চালানো হয়

Memory access time 100 ns, page-fault service time 10 ms, আর page-fault rate p = 0.001 হলে effective access time কত?

  • 100.9 ns
  • 1,099.9 ns
  • 10,099.9 ns
  • 10 ms

নিচের কোনটি সাধারণত major page fault?

  • malloc করা নতুন মেমরিতে প্রথম লেখা
  • আগে swap-এ সরানো একটি heap page আবার ছোঁয়া
  • fork-এর পর ভাগ করা page-এ child-এর প্রথম লেখা
  • অন্য প্রসেস সদ্য পড়েছে এমন একটি ফাইলের page mmap দিয়ে ছোঁয়া

একটি প্রসেসের ১০০০টি লেখার-যোগ্য page আছে। fork-এর পর child শুধু ৩টি আলাদা page-এ লিখে exit করল, parent কিছুই লিখল না। Copy-on-write-এ মোট কতটি page কপি হয়েছে?

  • ৩টি
  • ১০০০টি, fork-এর মুহূর্তেই
  • ০টি
  • ৬টি — প্রতিটি লেখায় দুটি করে

একটি page table entry-র valid–invalid bit “invalid” — এ থেকে নিশ্চিতভাবে কী বলা যায়?

  • ঠিকানাটি অবৈধ, প্রসেস SIGSEGV পাবে
  • Page-টি অবশ্যই swap-এ আছে
  • Page RAM-এ আছে, কিন্তু শুধু-পড়ার অনুমতিতে
  • ছুঁলে page fault হবে; ঠিকানা বৈধ কি না kernel ঠিক করবে

একটি ফাইল MAP_PRIVATE দিয়ে map করে তার page-এ লেখা হলো। এরপর অন্য একটি প্রসেস read() দিয়ে ফাইলটি পড়লে কী দেখবে?

  • নতুন লেখা, কারণ page cache ভাগ করা
  • ফাইলের পুরোনো বিষয়বস্তু
  • msync ডাকার পর নতুন লেখা, তার আগে পুরোনো
  • কিছুই না — লেখার চেষ্টায় প্রসেস SIGBUS পেয়েছে

Linux-এ vm.overcommit_memory = 2 রাখলে কী হয়?

  • Kernel কখনো মেমরির অনুরোধ ফেরায় না
  • শুধু স্পষ্টতই অসম্ভব বড় অনুরোধ ফেরায়
  • মোট প্রতিশ্রুতি একটি সীমা (CommitLimit) ছাড়ালে malloc/mmap ব্যর্থ হয়
  • Swap বন্ধ হয়ে যায়

শব্দকোষ

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

মনে রাখুন

  1. Virtual memory = বড় নিজস্ব address space, যার page RAM-এ, ডিস্কে বা কোথাও না থাকতে পারে; লাভ: বড় প্রোগ্রাম, বেশি প্রসেস, ভাগাভাগি, sparse ঠিকানা।
  2. Demand paging: প্রথম ছোঁয়ায় page fault; invalid বিট মানে “এখন নেই” বা “অবৈধ” — kernel VMA দেখে আলাদা করে।
  3. Page fault-এর ধাপ: trap → বৈধতা → খালি frame → ডিস্ক পড়া (অন্য প্রসেস চলে) → page table → একই নির্দেশ আবার। Minor ডিস্ক ছাড়া, major ডিস্কসহ।
  4. EAT = (1 − p) × ma + p × service; ma 200 ns, service 8 ms-এ ১০%-এর সীমা p < 1/399,990. আগে একক মেলান।
  5. COW fork-কে সস্তা করে (লিখলে তবেই কপি), mmap ফাইলকে মেমরি বানায় (PRIVATE = নিজের কপি, SHARED = ফাইলে, msync = টেকসই), আর overcommit-এর দাম আসে replacement আর OOM হয়ে।

পরের পর্বে: খালি frame ফুরিয়ে গেছে, নতুন page চাই — কাকে সরাবেন? FIFO, Optimal, LRU আর Clock algorithm দিয়ে reference string ধরে ধরে fault গোনা, আর frame বাড়ালেও fault বাড়ার অদ্ভুত ঘটনা — Belady-র anomaly.

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

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