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

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

Page table structures — multilevel & TLB
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • কেন একটি ফ্ল্যাট, সিঙ্গেল-লেভেল পেজ টেবিল বড় (৩২/৬৪-বিট) অ্যাড্রেস স্পেসে অব্যবহারিক
  • মাল্টিলেভেল (হায়ারার্কিক্যাল) পেজ টেবিল কীভাবে স্পার্স অ্যাড্রেস স্পেসে জায়গা সাশ্রয় করে
  • TLB কী, কেন এটি প্রতিটি অ্যাক্সেসে প্রথমে চেক করা হয়, এবং TLB হিট/মিস-এর প্রকৃত পারফরম্যান্স প্রভাব
  • Python দিয়ে দুই-স্তরের পেজ টেবিল ও TLB (evict-on-full) সিমুলেশন — হিট/মিস কাউন্ট ট্র্যাক করাসহ

১ · সমস্যা — একটি ফ্ল্যাট পেজ টেবিল স্কেল করে না

L29-এ আমরা একটি সরল, ফ্ল্যাট (single-level) পেজ টেবিল ব্যবহার করেছি — একটি সাধারণ dict, প্রতিটি পেজ নাম্বারের জন্য একটি এন্ট্রি। কিন্তু একটি বাস্তব ৩২-বিট বা ৬৪-বিট অ্যাড্রেস স্পেসে সম্ভাব্য পেজ নাম্বারের সংখ্যা কোটি কোটি হতে পারে — এবং প্রতিটি প্রসেসের নিজস্ব পেজ টেবিল লাগে। বেশিরভাগ প্রসেস তাদের সম্পূর্ণ সম্ভাব্য অ্যাড্রেস স্পেসের সামান্য অংশই ব্যবহার করে — তাই একটি সম্পূর্ণ ফ্ল্যাট টেবিল আগে থেকেই বরাদ্দ করা রাখলে বিশাল পরিমাণ মেমরি অপচয় হবে, প্রায় সবটাই অব্যবহৃত এন্ট্রির জন্য।

২ · মাল্টিলেভেল পেজ টেবিল — স্পার্স, লেজি বরাদ্দ

মাল্টিলেভেল পেজ টেবিলMultilevel (Hierarchical) Page Tableপেজ নাম্বারকে একাধিক অংশে ভেঙে ছোট পেজ টেবিলের একটি ট্রি (গাছ) তৈরি করার কৌশল — অব্যবহৃত অঞ্চলের জন্য কোনো ইনার টেবিল তৈরিই হয় না। এই সমস্যা সমাধান করে পেজ নাম্বারকে একাধিক অংশে ভেঙে — একটি আউটার টেবিল, যার প্রতিটি এন্ট্রি একটি ইনার পেজ টেবিলের দিকে নির্দেশ করে। গুরুত্বপূর্ণ অংশ — একটি ইনার টেবিল তখনই তৈরি হয় যখন সেই অঞ্চলের অন্তত একটি পেজ প্রথমবার ম্যাপ করা হয় (lazy creation) — যে অঞ্চল কখনো ব্যবহৃতই হয় না, তার জন্য কোনো মেমরিই খরচ হয় না। মূল্য — প্রতিটি ট্রান্সলেশনে এখন একাধিক ধাপে টেবিল "ওয়াক" করতে হয় (একটির বদলে দুই বা তিন ধাপ), সামান্য বেশি সময় লাগে প্রতি অ্যাক্সেসে, বিনিময়ে বিশাল জায়গা সাশ্রয় হয়।

আউটার টেবিল ইনার টেবিল ০ (ইনার টেবিল তৈরি হয়নি) ফ্রেম নাম্বার
যে আউটার এন্ট্রির নিচে কখনো কোনো পেজ ম্যাপ হয়নি, তার জন্য কোনো ইনার টেবিল তৈরিই হয় না — এটাই স্পার্স/লেজি সাশ্রয়।

৩ · TLB — অনুবাদ ক্যাশ করার হার্ডওয়্যার

মাল্টিলেভেল টেবিল ওয়াক করা এখনও একাধিক মেমরি অ্যাক্সেস লাগে (প্রতিটি স্তরের জন্য একটি) — প্রতিটি প্রোগ্রাম অ্যাক্সেসের জন্য এত কাজ করলে পুরো সিস্টেম ধীর হয়ে যেত। এখানেই আসে TLBTranslation Lookaside BufferCPU-এর কাছাকাছি বসানো একটি ছোট, অত্যন্ত দ্রুত হার্ডওয়্যার ক্যাশ যা সাম্প্রতিক পেজ-নাম্বার→ফ্রেম-নাম্বার ট্রান্সলেশন সংরক্ষণ করে — প্রতিটি মেমরি অ্যাক্সেসে সবার আগে চেক করা হয়। — CPU-এর কাছাকাছি বসানো একটি ছোট, অত্যন্ত দ্রুত হার্ডওয়্যার ক্যাশ। প্রতিটি মেমরি অ্যাক্সেসে প্রথমে TLB চেক করা হয় — TLB হিট হলে ফুল পেজ-টেবিল ওয়াক এড়িয়ে সরাসরি ফ্রেম নাম্বার পাওয়া যায়; শুধুমাত্র TLB মিস হলে ধীর, সম্পূর্ণ টেবিল-ওয়াক হয় — এবং তার ফলাফল পরবর্তীর জন্য TLB-তে ক্যাশ করে রাখা হয়। বাস্তব সিস্টেমে TLB হিট রেট অত্যন্ত গুরুত্বপূর্ণ — উচ্চ হিট রেট মানে বেশিরভাগ অ্যাক্সেসই ধীর টেবিল-ওয়াকের খরচ পুরোপুরি এড়িয়ে যায়।

TLB-ও একটি ক্যাশ — তাই এভিকশন লাগে

TLB-এর আকার সীমিত (হার্ডওয়্যারে মাত্র কয়েকশ এন্ট্রি ধরে), তাই পূর্ণ হয়ে গেলে নতুন এন্ট্রি রাখতে পুরনো কোনো একটি এন্ট্রি সরাতে হয় — এটি ঠিক অন্য যেকোনো ক্যাশিং সিস্টেমের (যেমন L34-এর LRU পেজ-রিপ্লেসমেন্টের) মতোই একটি এভিকশন-পলিসি প্রয়োজন করে।

মাল্টিলেভেল টেবিল সাশ্রয় করে — স্পেস
অব্যবহৃত অ্যাড্রেস-অঞ্চলের জন্য কোনো ইনার টেবিল তৈরিই হয় না — মেমরি খরচ প্রকৃত ব্যবহারের সমানুপাতিক।
TLB সাশ্রয় করে — সময়
সাম্প্রতিক ট্রান্সলেশন হার্ডওয়্যারে ক্যাশ থাকায় বেশিরভাগ অ্যাক্সেসেই ধীর, বহু-স্তরের টেবিল-ওয়াক এড়ানো যায়।

৪ · বাস্তব বাস্তবায়ন — দুই-স্তরের পেজ টেবিল + TLB

নিচের কোড সেলে একটি দুই-স্তরের পেজ টেবিল (আউটার dict → ইনার dict, লেজি তৈরি) এবং একটি ছোট, সাইজ-সীমিত TLB (পূর্ণ হলে সবচেয়ে পুরনো এন্ট্রি সরিয়ে দেওয়া) সিমুলেট করা হয়েছে। একই কয়েকটি পেজের বারবার অ্যাক্সেস চালিয়ে হিট বনাম মিস গণনা করা হয়েছে।

Python
# দুই-স্তরের পেজ টেবিল (lazy inner tables) + TLB (size-limited, oldest-evicted)
from collections import OrderedDict

outer_table = {}  # outer_index -> {inner_index: frame_number}

def map_page(outer_index, inner_index, frame_number):
    """একটি পেজ ম্যাপ করে -- ইনার টেবিল না থাকলে তখনই (lazily) তৈরি করে।"""
    if outer_index not in outer_table:
        outer_table[outer_index] = {}
    outer_table[outer_index][inner_index] = frame_number

def split_page_number(page_number):
    inner_index = page_number & 0xF        # নিচু ৪ বিট
    outer_index = (page_number >> 4) & 0xF  # পরের ৪ বিট
    return outer_index, inner_index

def page_table_walk(page_number):
    outer_index, inner_index = split_page_number(page_number)
    inner = outer_table.get(outer_index)
    if inner is None:
        return None  # এই অঞ্চলে কোনো ইনার টেবিলই তৈরি হয়নি
    return inner.get(inner_index)

TLB_MAX_SIZE = 3
tlb = OrderedDict()  # page_number -> frame_number, সবচেয়ে পুরনোটা সামনে
hits, misses = 0, 0

def access(page_number):
    global hits, misses
    if page_number in tlb:
        tlb.move_to_end(page_number)
        hits += 1
        return tlb[page_number], "TLB HIT"
    misses += 1
    frame = page_table_walk(page_number)
    if frame is None:
        return None, "পেজ ফল্ট (আনম্যাপড)"
    tlb[page_number] = frame
    if len(tlb) > TLB_MAX_SIZE:
        tlb.popitem(last=False)  # সবচেয়ে পুরনো এন্ট্রি সরানো
    return frame, "TLB MISS (পেজ টেবিল ওয়াক)"

# প্রাথমিক ম্যাপিং সেট আপ
map_page(0, 0, 5)
map_page(0, 1, 8)
map_page(0, 2, 2)
map_page(1, 0, 11)

sequence = [0, 1, 2, 0, 1, 16, 0, 2, 1]
for page_number in sequence:
    frame, status = access(page_number)
    print(f"পেজ {page_number:>2}: ফ্রেম={frame} | {status} | TLB={dict(tlb)}")

print(f"\nমোট হিট: {hits}, মোট মিস: {misses}")
print(f"হিট রেট: {hits / (hits + misses):.2%}")

    
লক্ষ্য করুন — পেজ 16-এর জন্য split_page_number(16) দেয় inner_index = 16 & 0xF = 0, এবং outer_index = (16 >> 4) & 0xF = 1 — তাই এটি আসলে outer_table[1][0]-এ ম্যাপ করা হয়েছিল (frame 11), যা map_page(1, 0, 11)-এর সাথে মেলে। এভাবেই একই পেজ-নাম্বার-স্প্লিটিং লজিক নির্ভরযোগ্যভাবে সঠিক ইনার টেবিলে পৌঁছে দেয়। এছাড়াও TLB আকার মাত্র ৩ হওয়ায় বারবার নতুন পেজ অ্যাক্সেস হলে পুরনো এন্ট্রি সরে যায় — তাই একই পেজ পরে আবার অ্যাক্সেস করলেও কখনো কখনো তা আবার TLB মিস দেয়, যা হিট রেট ১০০% এর কম রাখে।
মূল কথা · Key takeaway

মাল্টিলেভেল পেজ টেবিল ও TLB — দুটোই একই মৌলিক সমস্যার সমাধান করে ভিন্ন দিক থেকে: মাল্টিলেভেল টেবিল স্পেস সাশ্রয় করে (স্পার্স, লেজি বরাদ্দ দিয়ে), আর TLB সময় সাশ্রয় করে (ক্যাশিং দিয়ে, বেশিরভাগ অ্যাক্সেসে ফুল টেবিল-ওয়াক এড়িয়ে)। এই দুটো মিলেই আধুনিক OS-এ পেজিং-কে বাস্তবসম্মতভাবে দ্রুত ও স্কেলযোগ্য করে তোলে। M8-এর ভার্চুয়াল মেমরি মডিউল (L32-এর ডিমান্ড পেজিং থেকে শুরু করে) ঠিক এই একই পেজ টেবিল কাঠামোর ওপর ভিত্তি করেই তৈরি — শুধু এখন প্রতিটি পেজ ফিজিক্যাল মেমরিতে আছে না ডিস্কে, তা-ও ট্র্যাক করা শুরু হবে।

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

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

প্র ০১ মাল্টিলেভেল পেজ টেবিল কীভাবে মেমরি সাশ্রয় করে, অথচ প্রতিটি ট্রান্সলেশনে একাধিক ধাপ লাগার মূল্য দিয়েও এটি লাভজনক?

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

প্র ০২ উপরের কোড সেলে TLB আকার মাত্র ৩ রাখার ফলে হিট রেটের ওপর কী প্রভাব পড়ল?

TLB আকার ছোট (৩) হওয়ায় নতুন কোনো পেজ (যেমন পেজ 16) অ্যাক্সেস হলে সবচেয়ে পুরনো এন্ট্রি সরিয়ে জায়গা করতে হয় — এমনকি সেই সরানো এন্ট্রিটি হয়তো একটু পরেই আবার দরকার পড়ত। ফলে যে পেজগুলো ধারাবাহিকভাবে অ্যাক্সেস না হয়ে মাঝে অন্য পেজ দিয়ে "বিঘ্নিত" হয়, তারা পুনরায় TLB মিস দিতে পারে। বাস্তব হার্ডওয়্যারে TLB আকার অনেক বড় (কয়েকশ এন্ট্রি) হওয়ায় এই সমস্যা কম হয়, কিন্তু নীতিগতভাবে একই — TLB আকার যত ছোট, এভিকশনের কারণে হিট রেট তত কম হওয়ার প্রবণতা।

প্র ০৩ TLB মিস হলে কী "স্থায়ীভাবে খারাপ" কিছু ঘটে, নাকি এটি স্বাভাবিক ও প্রত্যাশিত?

TLB মিস সম্পূর্ণ স্বাভাবিক এবং প্রত্যাশিত — এটি শুধু বোঝায় যে এবারের ট্রান্সলেশনের জন্য সম্পূর্ণ পেজ-টেবিল ওয়াক করতে হবে (ধীর, কিন্তু এখনও সঠিক ফলাফল দেয়)। মিস "ব্যর্থতা" নয়, বরং একটি ক্যাশ-মিস যা যেকোনো ক্যাশিং সিস্টেমে মাঝে মাঝে ঘটবেই। গুরুত্বপূর্ণ বিষয়টি হলো overall হিট রেট — একটি সিস্টেম যদি বেশিরভাগ সময় হিট পায় (উপরের উদাহরণে যেমন ৩৩% হিট রেট একটি ছোট, কৃত্রিম উদাহরণে দেখা গেছে), তাহলে গড়ে কর্মক্ষমতা ভালো থাকে, যদিও প্রতিটি একক অ্যাক্সেস মিস নাও হতে পারে।

অনুশীলন

  1. চিন্তা করুন: যদি একটি প্রসেস তার সম্পূর্ণ সম্ভাব্য অ্যাড্রেস স্পেসের প্রায় পুরোটাই ঘনভাবে ব্যবহার করে (স্পার্স নয়), তাহলে মাল্টিলেভেল পেজ টেবিলের স্পেস-সাশ্রয় সুবিধা কী হবে?

    এই ক্ষেত্রে প্রায় প্রতিটি আউটার এন্ট্রির নিচেই একটি ইনার টেবিল তৈরি হয়ে যাবে — তাই স্পেস-সাশ্রয়ের সুবিধা প্রায় থাকবেই না (বরং সামান্য বেশি মেমরিই লাগবে, আউটার+ইনার টেবিলের অতিরিক্ত কাঠামোর জন্য)। এটি দেখায় মাল্টিলেভেল টেবিলের সুবিধা নির্ভর করে অ্যাড্রেস-স্পেস ব্যবহারের ধরনের ওপর — বাস্তবে বেশিরভাগ প্রসেসই তাদের সম্পূর্ণ সম্ভাব্য স্পেসের সামান্য অংশ ব্যবহার করে বলেই এই কৌশলটি ব্যাপকভাবে কার্যকর।

  2. পরীক্ষা করুন: উপরের কোড সেলে TLB_MAX_SIZE-কে 3 থেকে 10 করে Run চাপুন — হিট রেট কীভাবে বদলায়?

    TLB আকার 10 করলে সিকোয়েন্সের সবগুলো ভিন্ন পেজ (0, 1, 2, 16 — মোট ৪টি ভিন্ন পেজ) একসাথে TLB-তে ধরে যাবে, তাই কোনো এন্ট্রিই এভিকশনের কারণে হারাবে না — প্রতিটি পেজের প্রথমবার অ্যাক্সেসের পরে তার সব পরবর্তী অ্যাক্সেসই হিট হবে। এতে হিট রেট আগের ৩৩%-এর তুলনায় উল্লেখযোগ্যভাবে বেড়ে যাবে — বাস্তব হার্ডওয়্যারে বড় TLB থাকার সুবিধা ঠিক এভাবেই কাজ করে।

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

আগের পাঠ
পাঠ ৩০ · সেগমেন্টেশন