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

কেস স্টাডি: একটি মেমরি অ্যালোকেটর সিমুলেটর বানানো

Case study: building a memory allocator simulator
১০ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • একটি বাস্তব MemoryAllocator ক্লাস — যা allocate ও free দুটোই সমর্থন করে, তিনটি স্ট্র্যাটেজি pluggable ভাবে
  • কীভাবে free() করার পর তৈরি হওয়া একাধিক হোল প্রতিটি স্ট্র্যাটেজিতে ভিন্নভাবে ব্যবহৃত হয়
  • external fragmentation পরিমাপ করার একটি সহজ, সঠিক পদ্ধতি
  • কেন ওয়ার্স্ট-ফিট তত্ত্বে "বড় হোল রেখে দেয়" বলা হলেও বাস্তবে প্রায়ই সবচেয়ে বেশি ফ্র্যাগমেন্টেশন তৈরি করে

১ · MemoryAllocator ক্লাস

L28-এ আমরা ফার্স্ট-ফিট, বেস্ট-ফিট ও ওয়ার্স্ট-ফিটকে আলাদা ফাংশন হিসেবে দেখেছিলাম, শুধু allocate অপারেশনের ওপর। এখানে সেগুলোকে একটি পূর্ণাঙ্গ ক্লাসে একত্রিত করা হলো — যেটি allocate() এবং free() দুটোই সমর্থন করে, এবং প্রতিটি হোলকে (start, size) আকারে ট্র্যাক করে (ঠিক L28-এর মতোই), সবসময় ঠিকানা-ক্রম অনুযায়ী সাজানো রাখে।

Python
class MemoryAllocator:
    def __init__(self, total_size, strategy):
        self.total_size = total_size
        self.strategy = strategy          # "first_fit" | "best_fit" | "worst_fit"
        self.free_holes = [(0, total_size)]   # (start, size), ঠিকানা-ক্রমে সাজানো
        self.occupied = {}                # name -> (start, size)

    def _choose_hole(self, size):
        candidates = [h for h in self.free_holes if h[1] >= size]
        if not candidates:
            return None
        if self.strategy == "first_fit":
            for h in self.free_holes:      # ঠিকানা-ক্রমে প্রথম যথেষ্ট বড় হোল
                if h[1] >= size:
                    return h
        elif self.strategy == "best_fit":
            return min(candidates, key=lambda h: (h[1], h[0]))   # সবচেয়ে ছোট, কিন্তু যথেষ্ট
        elif self.strategy == "worst_fit":
            return max(candidates, key=lambda h: (h[1], -h[0]))  # সবচেয়ে বড়
        raise ValueError("অজানা strategy")

    def allocate(self, name, size):
        hole = self._choose_hole(size)
        if hole is None:
            return None
        start, hole_size = hole
        self.free_holes.remove(hole)
        self.occupied[name] = (start, size)
        remainder = hole_size - size
        if remainder > 0:
            self.free_holes.append((start + size, remainder))
        self.free_holes.sort(key=lambda h: h[0])
        return start

    def free(self, name):
        start, size = self.occupied.pop(name)
        self.free_holes.append((start, size))
        self.free_holes.sort(key=lambda h: h[0])

    def total_free(self):
        return sum(size for _, size in self.free_holes)

    def largest_hole(self):
        return max((size for _, size in self.free_holes), default=0)

    def fragmentation_report(self):
        total = self.total_free()
        largest = self.largest_hole()
        return {
            "holes": sorted(self.free_holes),
            "hole_count": len(self.free_holes),
            "total_free": total,
            "largest_hole": largest,
            "wasted_fragmented": total - largest,   # ফ্রি, কিন্তু সবচেয়ে বড় একক অনুরোধের চেয়ে ছোট টুকরোয় ছড়ানো
        }

# একই allocate/free সিকোয়েন্স -- মাঝে ইচ্ছাকৃতভাবে "filler" ব্লক (F1, F2) রাখা হয়েছে
# যাতে A, B, C ফ্রি করার পর তিনটি পৃথক, ভিন্ন-সাইজের হোল তৈরি হয় (কোনো merge ছাড়াই)।
ops = [
    ("alloc", "A", 300), ("alloc", "F1", 50), ("alloc", "B", 500),
    ("alloc", "F2", 50), ("alloc", "C", 200),
    ("free", "A", None), ("free", "B", None), ("free", "C", None),
    ("alloc", "D", 150), ("alloc", "E", 80),
]

for strategy in ["first_fit", "best_fit", "worst_fit"]:
    allocator = MemoryAllocator(1200, strategy)
    print(f"\n=== স্ট্র্যাটেজি: {strategy} ===")
    for op, name, size in ops:
        if op == "alloc":
            start = allocator.allocate(name, size)
            if name in ("D", "E"):
                print(f"  {name}({size}) বরাদ্দ -> ঠিকানা {start}-তে শুরু হওয়া হোলে")
        else:
            allocator.free(name)
    report = allocator.fragmentation_report()
    print(f"  ফ্রি হোল লেআউট: {report['holes']}")
    print(f"  মোট ফ্রি: {report['total_free']}, সবচেয়ে বড় হোল: {report['largest_hole']}, "
          f"ফ্র্যাগমেন্টেড/অপচয়: {report['wasted_fragmented']}")

    
হাতে-হিসাব করে যাচাই করা: D(150) বরাদ্দের সময় তিনটি strategy তিনটি ভিন্ন হোল বেছে নেয় (first_fit: ঠিকানা 0 থেকে শুরু হওয়া হোল; best_fit: ঠিকানা 900-এর হোল, যা যথেষ্ট বড় হোলগুলোর মধ্যে সবচেয়ে ছোট; worst_fit: ঠিকানা 350-এর হোল, যা সবচেয়ে বড়)। শেষে মোট ফ্রি মেমরি তিনটিতেই সমান (৮৭০ ইউনিট — কারণ বরাদ্দকৃত মোট মেমরি একই), কিন্তু worst_fit-এ সবচেয়ে বড় হোলটি বারবার ভেঙে যাওয়ায় সবচেয়ে বেশি ফ্র্যাগমেন্টেড অবস্থায় শেষ হয়।
মূল কথা · Key takeaway

L28-এ আমরা তত্ত্বে বলেছিলাম ওয়ার্স্ট-ফিট "ইচ্ছাকৃতভাবে একটি বড় অবশিষ্ট রেখে দেয়, যা পরে কাজে লাগতে পারে" — কিন্তু এই কোড চালিয়ে দেখা যায় বাস্তবে এর উল্টো ঘটনাও ঘটতে পারে: বারবার সবচেয়ে বড় হোলটি বেছে নেওয়ায়, একটি সিস্টেমে কখনোই একটিও বড়, ব্যবহারযোগ্য হোল অবশিষ্ট থাকে না — এটাই কেন ওয়ার্স্ট-ফিট ব্যবহারিকভাবে প্রায় ব্যবহৃত হয় না।

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

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

প্র ০১ তিনটি স্ট্র্যাটেজিতেই মোট ফ্রি মেমরি (৮৭০) একই কেন, কিন্তু "সবচেয়ে বড় হোল" আলাদা?

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

প্র ০২ "wasted_fragmented = total_free − largest_hole" — এই মেট্রিকটি ঠিক কী বোঝায়?

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

প্র ০৩ এই সিমুলেটরে কোনো coalescing (পাশাপাশি দুটি ফ্রি হোলকে একটি বড় হোলে একত্র করা) বাস্তবায়ন করা হয়নি। এটি যোগ করলে ফলাফল কীভাবে বদলাতে পারত?

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

অনুশীলন

  1. পরীক্ষা করুন: কোড সেলে ops-এর একদম শেষে ("alloc", "F", 350) যোগ করে আবার রান করুন। কোন স্ট্র্যাটেজিতে এই বরাদ্দ ব্যর্থ হয় (None রিটার্ন করে)?

    first_fit ও best_fit-এ শেষ পর্যন্ত সবচেয়ে বড় হোল ৫০০ (অক্ষত), তাই ৩৫০-এর অনুরোধ সফল হবে। কিন্তু worst_fit-এ শেষে সবচেয়ে বড় হোল মাত্র ৩০০, যা ৩৫০-এর অনুরোধের চেয়ে ছোট — তাই worst_fit-এ এই বরাদ্দ ব্যর্থ হবে (allocate() None রিটার্ন করবে), যদিও মোট ফ্রি মেমরি (তিনটিতেই সমান) ৩৫০-এর চেয়ে ঢের বেশি — এক্সটার্নাল ফ্র্যাগমেন্টেশনের প্রভাব হাতেনাতে দেখা গেল।

  2. চিন্তা করুন: _choose_hole-এ best_fit-এর টাই-ব্রেকিং নিয়ম (h[1], h[0]) ব্যবহার করে — অর্থাৎ সমান সাইজের একাধিক হোল থাকলে ঠিকানা অনুযায়ী ছোটটি (প্রথমটি) বেছে নেয়। এই নিয়ম না থাকলে কী সমস্যা হতে পারত?

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

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

আগের পাঠ
কেস স্টাডি: একটি CPU শিডিউলার সিমুলেটর বানানো