পাঠ ৩২ · ৫৬-এর মধ্যে · মডিউল ৮
Home / Courses / Operating Systems (OS) / ডিমান্ড পেজিং

ডিমান্ড পেজিং

Demand paging
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ভার্চুয়াল মেমরি ও ডিমান্ড পেজিং কীভাবে একটি প্রসেসকে ফিজিক্যাল মেমরির সীমার বাইরে "বড়" মনে করায়
  • valid/invalid বিট, পেজ ফল্ট এবং OS-এর ট্র্যাপ→লোড→রিজিউম চক্র ধাপে ধাপে
  • Effective Access Time (EAT) হিসাব করে দেখা কেন সামান্য পেজ-ফল্ট রেটও পারফরম্যান্সের জন্য বিশাল ব্যাপার
  • Python দিয়ে একটি বাস্তব ডিমান্ড-পেজিং সিমুলেশন — হিট/ফল্ট ট্র্যাক করা

১ · ভার্চুয়াল মেমরি ও ডিমান্ড পেজিং কী

ভার্চুয়াল মেমরি (Virtual Memory)Virtual Memoryএকটি প্রসেসের লজিক্যাল অ্যাড্রেস স্পেসকে প্রকৃত ফিজিক্যাল মেমরির চেয়ে বড় হতে দেওয়ার কৌশল — বাকি অংশ ডিস্কে (swap space) থাকে। হলো এমন একটি কৌশল যা একটি প্রসেসের লজিক্যাল অ্যাড্রেস স্পেসকে কম্পিউটারে আসলে থাকা ফিজিক্যাল RAM-এর চেয়েও বড় হতে দেয়। M7-এর পেজিং (L29) আমাদের দেখিয়েছিল কীভাবে একটি প্রসেসের পেজগুলো ফিজিক্যাল ফ্রেমে ম্যাপ হয় — কিন্তু সেখানে ধরে নেওয়া হয়েছিল প্রসেসের সবগুলো পেজ একসাথে মেমরিতে থাকে। ভার্চুয়াল মেমরি এই ধারণা ভেঙে দেয়: যেকোনো মুহূর্তে শুধু প্রসেসটি এখন সক্রিয়ভাবে যা ব্যবহার করছে তা-ই মেমরিতে থাকা যথেষ্ট, বাকিটা ডিস্কের একটি সংরক্ষিত এলাকায় (swap space/page file) অপেক্ষা করে।

ডিমান্ড পেজিং (Demand Paging)Demand Pagingএকটি পেজ ডিস্ক থেকে ফিজিক্যাল মেমরিতে লোড হয় শুধু তখনই, যখন প্রসেস সেই পেজে প্রথমবার সত্যিকারের অ্যাক্সেস করে — আগে থেকে নয়। হলো ভার্চুয়াল মেমরি বাস্তবায়নের প্রধান কৌশল — "চাহিদা অনুযায়ী" পেজিং। একটি পেজ ডিস্ক থেকে মেমরিতে লোড হয় শুধু প্রথমবার সত্যিকারের অ্যাক্সেস হলে, প্রসেস শুরু হওয়ার সময় সবকিছু আগেভাগে (eagerly) লোড করে নয়। এর ফলে একটি প্রসেস দ্রুত শুরু হতে পারে (প্রথম কয়েকটি প্রয়োজনীয় পেজ ছাড়া আর কিছু লোড করার দরকার নেই) এবং যে পেজগুলো কখনোই ব্যবহৃত হয় না (যেমন একটি প্রোগ্রামের এরর-হ্যান্ডলিং কোড যা কখনো ট্রিগার হয় না) সেগুলো ডিস্ক থেকে মেমরিতে আনার খরচও লাগে না।

প্রচলিত (eager) পেজিং
প্রসেস শুরু হওয়ার সময়েই তার সবগুলো পেজ ফিজিক্যাল মেমরিতে লোড করা হয় — সরল কিন্তু অপচয়ী, বিশেষত বড় প্রোগ্রামে যার বেশিরভাগ অংশ হয়তো কখনোই ব্যবহৃত হবে না।
ডিমান্ড পেজিং
শুধু এখন প্রয়োজন এমন পেজ লোড হয় — মেমরি ব্যবহার কার্যকর হয়, কিন্তু প্রতিটি নতুন পেজ প্রথমবার অ্যাক্সেসে একটি "পেজ ফল্ট" ও তার সাথে ডিস্ক I/O-এর খরচ আনে।

২ · Valid/Invalid বিট, পেজ ফল্ট (Page Fault) ও রিজিউম

ডিমান্ড পেজিং বাস্তবায়ন করতে প্রতিটি পেজ টেবিল এন্ট্রিতে একটি অতিরিক্ত valid/invalid বিট যোগ করা হয়। valid মানে পেজটি এখন সত্যিই কোনো ফিজিক্যাল ফ্রেমে আছে — MMU সরাসরি ফ্রেম নাম্বার পেয়ে যায় (L29/L31-এর স্বাভাবিক অনুবাদ)। invalid মানে পেজটি এখনো ডিস্কেই আছে, ফিজিক্যাল মেমরিতে আনা হয়নি।

যখন একটি প্রসেস একটি invalid পেজে অ্যাক্সেস করার চেষ্টা করে, হার্ডওয়্যার একটি পেজ ফল্ট (Page Fault)Page Faultএকটি ইনভ্যালিড (মেমরিতে অনুপস্থিত) পেজে অ্যাক্সেসের চেষ্টা — হার্ডওয়্যার OS-কে ট্র্যাপ করায়, যাতে OS পেজটি ডিস্ক থেকে লোড করে দেয়। তৈরি করে এবং OS-কে ট্র্যাপ করায়। তারপর OS নিচের ধাপগুলো অনুসরণ করে:

প্রসেস পেজ N অ্যাক্সেস করে MMU পেজ টেবিল দেখে -- valid বিট চেক করে invalid -> পেজ ফল্ট! OS-কে ট্র্যাপ (kernel mode) OS: ফ্রি ফ্রেম যোগাড়, ডিস্ক থেকে পেজ লোড, টেবিল আপডেট প্রসেস রিজিউম -- একই ইন্সট্রাকশন আবার, এবার HIT
পুরো চক্রটি প্রসেসের কাছে অদৃশ্য — এটি শুধু জানে ইন্সট্রাকশনটি (কিছুক্ষণ দেরিতে হলেও) সফল হয়েছে।
মূল ধারণা · Transparent resume

পেজ ফল্ট হ্যান্ডলিংয়ের সবচেয়ে গুরুত্বপূর্ণ বৈশিষ্ট্য হলো এটি প্রসেসের কাছে সম্পূর্ণ স্বচ্ছ (transparent) — প্রসেস কখনো জানতে পারে না যে একটি ফল্ট ঘটেছিল (টাইমিং ছাড়া)। OS যে ইন্সট্রাকশনটি ফল্ট ঘটিয়েছিল, ঠিক সেই একই ইন্সট্রাকশন থেকে প্রসেসকে পুনরায় শুরু করায় — নতুন করে ডেটা লোড হয়ে যাওয়ার পর এবার এটি সফলভাবে সম্পন্ন হয় (হিট)।

৩ · পেজ ফল্টের রেট ও প্রকৃত খরচ — Effective Access Time

পেজ ফল্ট কেন এত গুরুত্বপূর্ণ একটি বিষয়? কারণ একটি ফল্ট হ্যান্ডল করতে একটি ডিস্ক I/O লাগে — যা সাধারণ মেমরি অ্যাক্সেসের চেয়ে কয়েক হাজার থেকে কয়েক লক্ষ গুণ ধীর। এই বাস্তব খরচ গণনা করার একটি প্রমিত সূত্র হলো Effective Access Time (EAT) — যদি $p$ পেজ-ফল্ট রেট (০ থেকে ১-এর মধ্যে একটি ভগ্নাংশ), $ma$ সাধারণ মেমরি অ্যাক্সেস টাইম, এবং $fault\_time$ একটি ফল্ট হ্যান্ডল করতে যে সময় লাগে:

$$ EAT = (1-p) \times ma + p \times fault\_time $$

একটি বাস্তব উদাহরণ দেখা যাক: ধরা যাক সাধারণ মেমরি অ্যাক্সেস $ma = 100$ ন্যানোসেকেন্ড, একটি পেজ ফল্ট হ্যান্ডল করতে $fault\_time = 8{,}000{,}000$ ন্যানোসেকেন্ড (৮ মিলিসেকেন্ড, একটি ডিস্ক সিকের বাস্তবসম্মত মান) লাগে, এবং পেজ-ফল্ট রেট মাত্র $p = 0.001$ (অর্থাৎ প্রতি ১০০০ অ্যাক্সেসে গড়ে ১টি ফল্ট)। তাহলে —

$$ EAT = (0.999 \times 100) + (0.001 \times 8{,}000{,}000) = 99.9 + 8000 = 8099.9 \text{ ন্যানোসেকেন্ড} $$

মাত্র ০.১% পেজ-ফল্ট রেটেই গড় অ্যাক্সেস টাইম ১০০ ন্যানোসেকেন্ড থেকে বেড়ে প্রায় ৮১০০ ন্যানোসেকেন্ড হয়ে যায় — অর্থাৎ সিস্টেম প্রায় ৮১ গুণ ধীর হয়ে যায়! এই কারণেই OS ডিজাইনাররা পেজ-ফল্ট রেট যতটা সম্ভব কম রাখার জন্য এত গুরুত্ব দেন — সামান্য বৃদ্ধিও প্রকৃত পারফরম্যান্সে বিশাল প্রভাব ফেলে (সরাসরি L35-এর থ্র্যাশিং আলোচনার ভিত্তি)।

৪ · সিমুলেশন: ডিমান্ড পেজিং বাস্তবে কীভাবে কাজ করে

নিচের কোড সেলে একটি ছোট্ট, বাস্তব ডিমান্ড-পেজিং সিমুলেশন — একটি পেজ টেবিল (valid/invalid এন্ট্রিসহ), একটি ফিজিক্যাল মেমরি (৪টি ফ্রেম) এবং একটি access_page() ফাংশন যা প্রতিটি অ্যাক্সেসে হিট না ফল্ট তা ঠিক করে, ফল্ট হলে প্রকৃতপক্ষে একটি ফ্রি ফ্রেমে পেজ "লোড" করে।

Python
# ডিমান্ড পেজিং সিমুলেশন -- বাস্তব ডিস্ক/RAM নয়, শুধুই ধারণা বোঝানোর টয় ডেটা স্ট্রাকচার

NUM_FRAMES = 4

# page_table: page_number -> {"valid": bool, "frame": frame_number or None}
page_table = {}

# physical_memory: frame_number -> page_number (কোন ফ্রেমে এখন কোন পেজ আছে)
physical_memory = {}
free_frames = list(range(NUM_FRAMES))

fault_count = 0
hit_count = 0

def access_page(page_number, page_table, physical_memory, free_frames):
    global fault_count, hit_count
    entry = page_table.get(page_number, {"valid": False, "frame": None})

    if entry["valid"]:
        hit_count += 1
        print(f"পেজ {page_number}: HIT  (আগে থেকেই ফ্রেম {entry['frame']}-এ আছে)")
        return entry["frame"]

    # --- পেজ ফল্ট ---
    fault_count += 1
    if not free_frames:
        print(f"পেজ {page_number}: FAULT -> কিন্তু কোনো ফ্রি ফ্রেম নেই! "
              f"(কাকে সরানো হবে তা ঠিক করা page-replacement অ্যালগরিদমের কাজ -- দেখুন পরবর্তী পাঠ, L33)")
        return None

    frame = free_frames.pop(0)
    physical_memory[frame] = page_number
    page_table[page_number] = {"valid": True, "frame": frame}
    print(f"পেজ {page_number}: FAULT -> ডিস্ক থেকে লোড, ফ্রেম {frame}-এ বসানো হলো")
    return frame

# ৪টি ফ্রেম, কিন্তু মাত্র ৪টি ভিন্ন পেজ ব্যবহৃত হচ্ছে -- তাই কখনো রিপ্লেসমেন্টের দরকার হবে না
reference_sequence = [1, 2, 3, 1, 4, 2, 1, 3, 4, 2]

for pg in reference_sequence:
    access_page(pg, page_table, physical_memory, free_frames)

print("\n--- ফলাফল ---")
print("মোট অ্যাক্সেস:", len(reference_sequence))
print("পেজ ফল্ট:", fault_count)
print("পেজ হিট:", hit_count)
print(f"পেজ-ফল্ট রেট: {fault_count/len(reference_sequence):.2f}")

    
লক্ষ্য করুন — মাত্র ৪টি ভিন্ন পেজ (1, 2, 3, 4) থাকায় ৪টি ফ্রেমেই সবকিছু ফিট হয়ে যায়, ফলে প্রতিটি পেজের প্রথম অ্যাক্সেসে ফল্ট (মোট ৪টি) এবং পরের প্রতিটি পুনরাবৃত্ত অ্যাক্সেসে হিট (মোট ৬টি) হয়। ফ্রি ফ্রেম শেষ হয়ে গেলে কী হয় তা যাচাই করতে নিচের অনুশীলনে একটি ৫ম পেজ যোগ করে দেখুন।
মূল কথা · Key takeaway

ডিমান্ড পেজিং ভার্চুয়াল মেমরির ভিত্তি — এটি ঠিক করে কখন একটি পেজ মেমরিতে আনা হবে (শুধু প্রথম প্রকৃত অ্যাক্সেসে)। কিন্তু মেমরি পূর্ণ থাকলে কাকে সরিয়ে জায়গা করা হবে তা এই পাঠের সিমুলেশন এড়িয়ে গেছে — ঠিক এই প্রশ্নের উত্তর নিয়েই পরবর্তী পাঠ, L33 (পেজ রিপ্লেসমেন্ট অ্যালগরিদম) শুরু হবে।

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

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

প্র ০১ ডিমান্ড পেজিং কেন সব পেজ আগে থেকে লোড করার চেয়ে ভালো, যদি শেষ পর্যন্ত প্রতিটি পেজই একবার না একবার অ্যাক্সেস হয়?

বাস্তবে অনেক প্রোগ্রামের একটি বড় অংশ (যেমন এরর-হ্যান্ডলিং কোড, খুব কম ব্যবহৃত ফিচার) আসলে কখনোই অ্যাক্সেস হয় না একটি নির্দিষ্ট রান-এ। এছাড়া ডিমান্ড পেজিং প্রসেসকে অনেক দ্রুত শুরু করতে দেয় — প্রথম কয়েকটি প্রয়োজনীয় পেজ লোড হলেই প্রসেস চালু হয়ে যেতে পারে, বাকিটা প্রয়োজন অনুযায়ী ধীরে ধীরে আসে। ফলে গড়ে মেমরি ও সময় দুটোই সাশ্রয় হয়, এমনকি যদি প্রতিটি পেজ শেষমেশ একবার অ্যাক্সেস হয়ও।

প্র ০২ একটি পেজ ফল্ট কেন "ট্র্যাপ" হিসেবে হ্যান্ডল করা হয়, সাধারণ ফাংশন কলের মতো নয়?

একটি ট্র্যাপ হলো এমন একটি হার্ডওয়্যার-লেভেল মেকানিজম যা প্রসেসকে জানার সুযোগ না দিয়েই CPU-কে user mode থেকে kernel mode-এ পাঠিয়ে দেয় (সরাসরি L03-এর সিস্টেম-কল আলোচনার প্যাটার্ন) — কারণ শুধুমাত্র kernel mode-এ OS-এর প্রকৃত ফ্রেম বরাদ্দ ও ডিস্ক I/O-এর মতো প্রিভিলেজড কাজ করার অনুমতি আছে। প্রসেসের নিজের কোডে একটি সাধারণ ফাংশন কল দিয়ে এটি করা সম্ভব নয়, কারণ প্রসেসের কাছে সেই প্রিভিলেজই নেই।

প্র ০৩ উপরের EAT হিসাবে পেজ-ফল্ট রেট ০.০০১ থেকে ০.০১ (১%) করলে EAT-এর ওপর কী প্রভাব পড়বে, মোটামুটি অনুমান করুন।

$p=0.01$ হলে $EAT = 0.99 \times 100 + 0.01 \times 8{,}000{,}000 = 99 + 80{,}000 = 80{,}099$ ন্যানোসেকেন্ড — অর্থাৎ ফল্ট রেট ১০ গুণ বাড়ানোয় EAT-ও প্রায় ১০ গুণ বেড়ে যায় (যেহেতু $fault\_time$ পদটিই EAT-তে প্রায় সম্পূর্ণ প্রভাব ফেলে, $ma$ পদ তুলনায় নগণ্য)। এটি দেখায় কেন পেজ-ফল্ট রেট নিয়ন্ত্রণে রাখা এত গুরুত্বপূর্ণ — এটি প্রায় সরাসরি রৈখিকভাবে (linearly) সামগ্রিক পারফরম্যান্সকে প্রভাবিত করে।

অনুশীলন

  1. পরীক্ষা করুন: উপরের কোড সেলে reference_sequence-এর শেষে 5 যোগ করুন (যেমন [1, 2, 3, 1, 4, 2, 1, 3, 4, 2, 5]) এবং Run চেপে দেখুন কী প্রিন্ট হয়।

    পেজ 5-এর জন্য কোনো ফ্রি ফ্রেম বাকি নেই (৪টি ফ্রেমই 1, 2, 3, 4-এ ভরা), তাই কোড "কোনো ফ্রি ফ্রেম নেই! ... page-replacement অ্যালগরিদমের কাজ" বার্তাটি প্রিন্ট করবে এবং None রিটার্ন করবে। এটিই ঠিক সেই মুহূর্ত যেখানে একটি বাস্তব OS-কে সিদ্ধান্ত নিতে হয় কোন পেজ সরানো হবে — যা পরের পাঠ L33-এর বিষয়।

  2. চিন্তা করুন: EAT সূত্রে fault_time যদি SSD-ভিত্তিক সিস্টেমে HDD-এর চেয়ে ১০ গুণ কম হয় (ধরুন ৮০০,০০০ ন্যানোসেকেন্ড, ৮ মিলিসেকেন্ডের বদলে), তাহলে একই $p=0.001$-এ EAT কেমন হবে?

    $EAT = 0.999 \times 100 + 0.001 \times 800{,}000 = 99.9 + 800 = 899.9$ ন্যানোসেকেন্ড — এখনও সাধারণ অ্যাক্সেসের (১০০ ন্যানোসেকেন্ড) চেয়ে প্রায় ৯ গুণ ধীর, কিন্তু HDD-ভিত্তিক সিস্টেমের ৮১ গুণের তুলনায় অনেক কম খারাপ। এটি দেখায় কেন দ্রুত স্টোরেজ (M10-এর SSD বনাম HDD পাঠ, L45) পেজ-ফল্টের বাস্তব খরচ কমাতে সরাসরি সাহায্য করে।

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

আগের পাঠ
পেজ টেবিল স্ট্রাকচার — মাল্টিলেভেল ও TLB