পাঠ ৫৬ · ৫৬-এর মধ্যে · মডিউল ১৪
Home / Courses / Operating Systems (OS) / চূড়ান্ত প্রকল্প

চূড়ান্ত প্রকল্প — একটি সম্পূর্ণ OS কার্নেল সিমুলেশন

Capstone — a full OS kernel simulation
১৮ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • কীভাবে একটি প্রসেস টেবিল, শিডিউলার, মেমরি ম্যানেজার, সিনক্রোনাইজেশন প্রিমিটিভ ও ফাইল সিস্টেম একটি একক OS-এর ভেতরে একসাথে কাজ করে
  • একটি সম্পূর্ণ TinyKernel ক্লাস, যার প্রতিটি মেথড এই কোর্সের একটি নির্দিষ্ট আগের পাঠের ধারণা বাস্তবায়ন করে
  • একটি বাস্তব, নির্ভুলভাবে যাচাই করা "কার্নেল লগ" যা প্রসেস তৈরি থেকে শুরু করে টার্মিনেশন পর্যন্ত প্রতিটি সাবসিস্টেমের কার্যক্রম ক্রমানুসারে দেখায়
  • কেন OS-এর সাবসিস্টেমগুলো বিচ্ছিন্নভাবে নয়, বরং একে অপরের সাথে সচেতনভাবে সহযোগিতা করে কাজ করতে হয়

১ · TinyKernel-এর নকশা — পাঁচটি সাবসিস্টেম, একটি ক্লাস

এখন পর্যন্ত আমরা প্রতিটি OS ধারণা আলাদাভাবে শিখেছি এবং L54-L55-এ দুটি সাবসিস্টেমকে (শিডিউলার, মেমরি অ্যালোকেটর) নিজ নিজ ছোট টুলে একত্রিত করেছি। এখন সময় এসেছে সবকিছুকে একটি একক কার্নেলে একত্রিত করার — একটি TinyKernel ক্লাস, যেখানে প্রতিটি সাবসিস্টেম একই সময়ে, একই প্রসেসগুলোর ওপর কাজ করে।

TinyKernel প্রসেস টেবিল + PCB M2 CPU শিডিউলার M3 · L54 পেজিং মেমরি ম্যানেজার (LRU) M7-M8 সেমাফোর (শেয়ার্ড রিসোর্স) M5 · L18 ইনডেক্সড ফাইল সিস্টেম M9 · L39 একটি সমন্বিত সিনারিও প্রসেস তৈরি -> শিডিউল -> পেজ ফল্ট/LRU evict -> সেমাফোর wait/signal -> ফাইল তৈরি/পড়া -> টার্মিনেট
পাঁচটি সাবসিস্টেম একই কার্নেল অবজেক্টের ভেতরে থাকে এবং একই প্রসেসগুলোর জন্য একসাথে কাজ করে — এটাই একটি বাস্তব OS-এর প্রকৃত চিত্র।

২ · প্রসেস টেবিল ও PCB (M2/L05-06)

প্রতিটি প্রসেসের জন্য একটি PCB থাকে (L06), যা তার অবস্থা (state, L05-এর ৫-স্টেট মডেল অনুযায়ী), প্রায়োরিটি এবং নিজস্ব পেজ টেবিল (নিচে দেখুন) ধরে রাখে। TinyKernel.processes হলো L06-এ শেখা "প্রসেস টেবিল"-এর সরাসরি বাস্তবায়ন — pid থেকে PCB-তে একটি ম্যাপিং।

৩ · Pluggable CPU শিডিউলার (M3, L54-এর ধারণা পুনর্ব্যবহার)

schedule_next() মেথডটি L54-এর dispatcher ধারণার একটি সরলীকৃত রূপ — এটি রেডি প্রসেসগুলোর মধ্যে থেকে প্রায়োরিটি অনুযায়ী (L11-এর কনভেনশনে, ছোট সংখ্যা = উচ্চ প্রায়োরিটি) পরবর্তী প্রসেস বেছে নেয় এবং তার অবস্থা Ready থেকে Running-এ পরিবর্তন করে (L05-এর বৈধ ট্রানজিশন)।

৪ · পেজিং মেমরি ম্যানেজার — ডিমান্ড পেজিং + LRU (M7-M8/L29,L32,L34)

প্রতিটি PCB-এর নিজস্ব পেজ টেবিল আছে (L29)। access_page() ঠিক L32-এর ডিমান্ড পেজিং মডেল অনুসরণ করে — একটি পেজ প্রথমবার অ্যাক্সেস হলেই কেবল লোড হয় (page fault)। ফ্রেম পুল ফুরিয়ে গেলে, L34-এর collections.OrderedDict-ভিত্তিক LRU কৌশল ব্যবহার করে সবচেয়ে কম-সাম্প্রতিক ব্যবহৃত ফ্রেমটি evict করা হয় — এবং একটি প্রসেস টার্মিনেট হলে তার সবকটি ফ্রেম মুক্ত হয়ে যায় (অন্য প্রসেসের জন্য পুনরায় ব্যবহারযোগ্য)।

৫ · সেমাফোর-সুরক্ষিত শেয়ার্ড রিসোর্স (M5/L18)

L18-এর ঠিক একই Semaphore ক্লাস (wait/signal, একটি অপেক্ষমাণ-তালিকা সহ) এখানে একটি শেয়ার্ড কাউন্টার সুরক্ষিত করতে ব্যবহৃত হয়েছে — L18-19-এর মতোই, একাধিক প্রসেসের অ্যাক্সেস স্পষ্টভাবে ইন্টারলিভ করে (আসল থ্রেড ছাড়াই) দেখানো হয়েছে একটি প্রসেস ব্লক হওয়া এবং সেমাফোর রিলিজের পর জেগে ওঠা (L05-এর Waiting→Ready ট্রানজিশন)।

৬ · ইনডেক্সড ফাইল সিস্টেম (M9/L39)

create_file() ও read_file_block() ঠিক L39-এর ইনডেক্সড অ্যালোকেশন — প্রতিটি ফাইলের একটি ইনডেক্স ব্লক (ডিস্ক ব্লক নম্বরের তালিকা) থাকে, যা যেকোনো লজিক্যাল ব্লকে O(1) সরাসরি অ্যাক্সেস দেয়, কোনো লিংকড-লিস্ট ট্র্যাভার্সাল ছাড়াই।

৭ · কোড: সম্পূর্ণ TinyKernel এবং একটি সমন্বিত সিনারিও

নিচের কোড সেলে পুরো TinyKernel ক্লাস এবং একটি একক সিনারিও আছে যা পাঁচটি সাবসিস্টেমকেই ক্রমানুসারে ব্যবহার করে: ৩টি প্রসেস তৈরি → শিডিউল → মেমরি অ্যাক্সেস (fault, hit, ও LRU eviction সহ) → সেমাফোর-সুরক্ষিত শেয়ার্ড রিসোর্স (একটি ব্লক ও জাগরণ সহ) → ফাইল তৈরি ও পড়া → টার্মিনেশন — সবশেষে একটি সমন্বিত কার্নেল লগ প্রিন্ট।

Python
from collections import OrderedDict

class PCB:
    """M2/L06 -- প্রতি প্রসেসের প্রোসেস কন্ট্রোল ব্লক।"""
    def __init__(self, pid, priority):
        self.pid = pid
        self.state = "New"
        self.priority = priority
        self.page_table = {}   # page_number -> frame_number (শুধু resident পেজ)

class Semaphore:
    """M5/L18 -- ঠিক সেই সেমাফোর ক্লাস, wait()/signal()।"""
    def __init__(self, value=1):
        self.value = value
        self.waiting = []
    def wait(self, pid):
        self.value -= 1
        if self.value < 0:
            self.waiting.append(pid)
            return False       # ব্লকড
        return True            # সাথে সাথে ঢুকলো
    def signal(self):
        self.value += 1
        if self.value <= 0 and self.waiting:
            return self.waiting.pop(0)   # যাকে জাগানো হলো, তার pid
        return None

class TinyKernel:
    def __init__(self, num_frames, disk_block_count):
        self.processes = {}                     # M2/L06 -- প্রসেস টেবিল
        self.ready_queue = []
        self.free_frames = list(range(num_frames))
        self.frame_table = OrderedDict()         # frame -> (pid, page) ; অর্ডার = LRU রিসেন্সি
        self.shared_counter = 0                  # M5/L18
        self.shared_mutex = Semaphore(1)
        self.files = {}                          # M9/L39 -- filename -> index_block
        self.disk = [None] * disk_block_count
        self.free_disk_blocks = list(range(disk_block_count))
        self.log = []

    def _log(self, msg):
        self.log.append(msg)

    # ---------- M2/L05-06: প্রসেস ও PCB ----------
    def create_process(self, pid, priority):
        pcb = PCB(pid, priority)
        pcb.state = "Ready"
        self.processes[pid] = pcb
        self.ready_queue.append(pid)
        self._log(f"[প্রসেস] {pid} তৈরি (priority={priority}) -> Ready")

    def schedule_next(self):
        """M3/L54 -- pluggable dispatcher, এখানে প্রায়োরিটি-ভিত্তিক।"""
        ready = [pid for pid in self.ready_queue if self.processes[pid].state == "Ready"]
        if not ready:
            self._log("[শিডিউলার] কোনো Ready প্রসেস নেই")
            return None
        chosen = min(ready, key=lambda pid: self.processes[pid].priority)
        self.processes[chosen].state = "Running"
        self._log(f"[শিডিউলার] {chosen} নির্বাচিত হলো -> Running")
        return chosen

    def terminate(self, pid):
        pcb = self.processes[pid]
        for page_number, frame in list(pcb.page_table.items()):
            del self.frame_table[frame]
            self.free_frames.append(frame)
            self._log(f"[মেমরি] {pid} টার্মিনেট -> ফ্রেম {frame} মুক্ত হলো")
        pcb.page_table.clear()
        pcb.state = "Terminated"
        self._log(f"[প্রসেস] {pid} টার্মিনেটেড")

    # ---------- M7-M8/L29,L32,L34: ডিমান্ড পেজিং + LRU ----------
    def access_page(self, pid, page_number):
        pcb = self.processes[pid]
        if page_number in pcb.page_table:
            frame = pcb.page_table[page_number]
            self.frame_table.move_to_end(frame)
            self._log(f"[মেমরি] {pid} পেজ {page_number} -> HIT (ফ্রেম {frame})")
            return frame
        self._log(f"[মেমরি] {pid} পেজ {page_number} -> PAGE FAULT")
        if self.free_frames:
            frame = self.free_frames.pop(0)
        else:
            evict_frame, (evict_pid, evict_page) = self.frame_table.popitem(last=False)
            del self.processes[evict_pid].page_table[evict_page]
            frame = evict_frame
            self._log(f"[মেমরি] LRU evict -> {evict_pid}-এর পেজ {evict_page}, ফ্রেম {frame} খালি করা হলো")
        pcb.page_table[page_number] = frame
        self.frame_table[frame] = (pid, page_number)
        self.frame_table.move_to_end(frame)
        self._log(f"[মেমরি] {pid}-এর পেজ {page_number} -> ফ্রেম {frame}-এ লোড হলো")
        return frame

    # ---------- M5/L18: সেমাফোর-সুরক্ষিত শেয়ার্ড রিসোর্স ----------
    def sem_wait(self, pid):
        proceeded = self.shared_mutex.wait(pid)
        if proceeded:
            self._log(f"[সিনক্রোনাইজেশন] {pid} সেমাফোর পেলো -> critical section-এ ঢুকলো")
        else:
            self.processes[pid].state = "Waiting"
            self._log(f"[সিনক্রোনাইজেশন] {pid} সেমাফোর না পেয়ে Waiting-এ গেলো")
        return proceeded

    def modify_shared_resource(self, pid, amount):
        self.shared_counter += amount
        self._log(f"[শেয়ার্ড রিসোর্স] {pid} কাউন্টার পরিবর্তন করলো -> এখন {self.shared_counter}")

    def sem_signal(self, pid):
        woken = self.shared_mutex.signal()
        self._log(f"[সিনক্রোনাইজেশন] {pid} সেমাফোর রিলিজ করলো")
        if woken:
            self.processes[woken].state = "Ready"
            self._log(f"[সিনক্রোনাইজেশন] {woken} জেগে উঠে Ready-তে ফিরলো")
        return woken

    # ---------- M9/L39: ইনডেক্সড ফাইল সিস্টেম ----------
    def create_file(self, filename, blocks_needed):
        if len(self.free_disk_blocks) < blocks_needed:
            self._log(f"[ফাইল সিস্টেম] {filename} ব্যর্থ -- ফ্রি ব্লক অপর্যাপ্ত")
            return False
        index_block = []
        for i in range(blocks_needed):
            block = self.free_disk_blocks.pop(0)
            self.disk[block] = f"{filename}:block{i}"
            index_block.append(block)
        self.files[filename] = index_block
        self._log(f"[ফাইল সিস্টেম] {filename} তৈরি -> ইনডেক্স ব্লক {index_block}")
        return True

    def read_file_block(self, filename, logical_block_number):
        physical_block = self.files[filename][logical_block_number]
        content = self.disk[physical_block]
        self._log(f"[ফাইল সিস্টেম] {filename} লজিক্যাল ব্লক {logical_block_number} -> ফিজিক্যাল ব্লক {physical_block} ({content})")
        return content


# ============ একটি সমন্বিত সিনারিও -- সবকটি সাবসিস্টেম একসাথে ============
kernel = TinyKernel(num_frames=3, disk_block_count=8)

kernel.create_process("P1", priority=2)
kernel.create_process("P2", priority=1)
kernel.create_process("P3", priority=3)

kernel.schedule_next()                 # প্রায়োরিটি ১ -> P2 নির্বাচিত

kernel.access_page("P2", 0)             # fault -> frame 0
kernel.access_page("P2", 1)            # fault -> frame 1
kernel.access_page("P2", 0)            # hit
kernel.access_page("P2", 2)            # fault -> frame 2 (পুল পূর্ণ)
kernel.access_page("P2", 3)            # fault -> ৩ ফ্রেমই ব্যস্ত -> LRU (পেজ ১) evict

kernel.sem_wait("P2")
kernel.modify_shared_resource("P2", 5)
kernel.sem_signal("P2")

kernel.create_file("notes_p2", 2)
kernel.read_file_block("notes_p2", 0)
kernel.read_file_block("notes_p2", 1)

kernel.terminate("P2")                 # P2-এর ৩টি ফ্রেম মুক্ত হলো

kernel.schedule_next()                 # এখন P1 (priority=2) নির্বাচিত
kernel.access_page("P1", 0)            # fault -> এখন খালি ফ্রেম পুনর্ব্যবহার

kernel.sem_wait("P1")                  # P1 সেমাফোর পেলো
kernel.schedule_next()                 # P3 (একমাত্র বাকি Ready) -> Running
kernel.sem_wait("P3")                  # সেমাফোর ইতিমধ্যে P1-এর কাছে -> P3 ব্লকড (Waiting)

kernel.modify_shared_resource("P1", 10)
kernel.sem_signal("P1")                # P3 জেগে উঠলো

kernel.modify_shared_resource("P3", 3)
kernel.sem_signal("P3")

kernel.create_file("log_p1", 3)
kernel.read_file_block("log_p1", 2)

kernel.terminate("P1")
kernel.terminate("P3")

print("=== TinyKernel সমন্বিত কার্নেল লগ ===")
for i, entry in enumerate(kernel.log, start=1):
    print(f"{i:02d}. {entry}")

print("\n--- চূড়ান্ত অবস্থা ---")
for pid, pcb in kernel.processes.items():
    print(f"{pid}: state={pcb.state}, priority={pcb.priority}")
print(f"শেয়ার্ড কাউন্টার শেষ মান: {kernel.shared_counter}")
print(f"ফাইল সিস্টেমে ফাইল: {list(kernel.files.keys())}")

    
হাতে-হিসাব করে যাচাই করা: ৩টি ফ্রেমের পুলে P2 চারটি ভিন্ন পেজ (0,1,0,2,3 — ক্রমানুসারে) অ্যাক্সেস করলে চতুর্থ স্বতন্ত্র পেজ (page 3) অ্যাক্সেসের সময় পুল পূর্ণ থাকায় সবচেয়ে কম-সাম্প্রতিক ব্যবহৃত পেজ (page 1, যেটি একবারও দ্বিতীয়বার অ্যাক্সেস হয়নি) evict হয় — ঠিক LRU-এর সংজ্ঞা অনুযায়ী। সেমাফোরের মান ১ থেকে শুরু হয়ে P1-এর wait-এ ০, P3-এর wait-এ -১ (ব্লকড) হয়ে, P1-এর signal-এ ০ (P3 জেগে ওঠে) এবং P3-এর নিজের signal-এ আবার ১-এ ফিরে আসে — সম্পূর্ণ সুষম হিসাব, কোনো লিক নেই।
মূল কথা · Key takeaway

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

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

এই কোর্সের শেষ তিনটি প্রশ্ন — মডিউলের সীমানা পেরিয়ে সামগ্রিকভাবে চিন্তা করুন। প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ শিডিউলার (M3) ও মেমরি ম্যানেজার (M7-M8) কেন সম্পূর্ণ বিচ্ছিন্নভাবে কাজ করতে পারে না — যদি একটি প্রসেসকে শিডিউল করা হয় কিন্তু তার দরকারি পেজ শিডিউলারের অজান্তে evict হয়ে যায়, তাহলে কী সমস্যা হতে পারে?

এটি ঠিক M8/L35-এর থ্র্যাশিং পরিস্থিতি — শিডিউলার একটি প্রসেসকে বারবার CPU দিচ্ছে, কিন্তু প্রতিবারই সে তার দরকারি পেজ খুঁজে পাচ্ছে না (কারণ সেটি ইতিমধ্যে evict হয়ে গেছে), তাই প্রতিবার একটি page fault হচ্ছে, প্রসেসটি Waiting-এ চলে যাচ্ছে, আবার Ready হচ্ছে, আবার শিডিউল হচ্ছে, আবার fault — কোনো প্রকৃত কাজ এগোচ্ছে না। সহযোগিতা (অন্তত পারস্পরিক সচেতনতা) ছাড়া, শিডিউলার এমন একটি প্রসেসকে বারবার CPU দিতে পারে যেটি আসলে তখনও চালানোর জন্য প্রস্তুত নয় — এটাই কেন বাস্তব OS-এ এই সাবসিস্টেমগুলো সম্পূর্ণ আলাদা, স্বাধীন মডিউল হিসেবে ডিজাইন করা হয় না।

প্র ০২ উপরের কোডে P3 সেমাফোরের জন্য Waiting অবস্থায় গেলো, তারপর Ready-তে ফিরলো। এটি M2/L05-এর প্রসেস স্টেট মডেলের সাথে ঠিক কীভাবে মেলে?

L05-এ আমরা শিখেছিলাম Running→Waiting ট্রানজিশন ঘটে যখন একটি প্রসেস কোনো ইভেন্টের (I/O, বা এখানে একটি সেমাফোরের) জন্য অপেক্ষা করে যা তখনও উপলব্ধ নয়, আর Waiting→Ready ঘটে সেই ইভেন্ট ঘটার পর (এখানে, P1 সেমাফোর রিলিজ করার পর)। P3-এর সেমাফোরে ব্লক হওয়া ঠিক এই একই সাধারণ প্যাটার্নের একটি নির্দিষ্ট উদাহরণ — সিনক্রোনাইজেশন ব্লকিং আসলে L05-এর সাধারণ "রিসোর্স উপলব্ধ না থাকলে Waiting-এ যাও" নিয়মেরই একটি বিশেষ ক্ষেত্র।

প্র ০৩ এই TinyKernel-এ ফাইল সিস্টেম ও মেমরি ম্যানেজার সম্পূর্ণ আলাদা কোড হিসেবে লেখা হয়েছে, অথচ দুটোই কাঠামোগতভাবে খুব একই রকম (একটি ইনডেক্স/টেবিল দিয়ে লজিক্যাল নম্বরকে ফিজিক্যাল অবস্থানে ম্যাপ করা)। এই মিলটি কেন গুরুত্বপূর্ণ?

এটি দেখায় OS ডিজাইনে একই মৌলিক প্যাটার্ন (ইনডাইরেকশন — একটি লজিক্যাল আইডেন্টিফায়ারকে একটি টেবিলের মাধ্যমে ফিজিক্যাল অবস্থানে ম্যাপ করা) বারবার ফিরে আসে: L27-এর MMU ঠিকানা অনুবাদ, L29-এর পেজ টেবিল, L31-এর মাল্টিলেভেল পেজ টেবিল, এবং L39-এর ফাইল ইনডেক্স ব্লক — সবগুলোই একই মূল কৌশলের ভিন্ন প্রয়োগ। একবার এই প্যাটার্নটি গভীরভাবে বুঝলে, OS-এর অনেক আলাদা-দেখতে অংশ আসলে একই ধারণার পুনরাবৃত্তি বলে মনে হবে — এটাই এই পুরো কোর্সের সবচেয়ে বড়, পুনরাবৃত্ত শিক্ষা।

অনুশীলন

  1. পরীক্ষা করুন: কোড সেলে TinyKernel(num_frames=3, ...)-কে num_frames=4 করে আবার রান করুন। P2-এর পাঁচটি মেমরি অ্যাক্সেসের (0,1,0,2,3) মধ্যে এখন কি কোনো LRU eviction ঘটে?

    না — P2 সর্বমোট ৪টি স্বতন্ত্র পেজ (0,1,2,3) অ্যাক্সেস করে, এবং ৪টি ফ্রেম থাকলে প্রতিটির জন্য একটি খালি ফ্রেম পাওয়া যাবে, কোনো eviction ছাড়াই। এটি প্রত্যক্ষভাবে দেখায় কীভাবে ফ্রেম পুলের আকার (M8/L36-এর ফ্রেম অ্যালোকেশন আলোচনা) সরাসরি নির্ধারণ করে কতবার page fault-এর ফলে সত্যিকারের eviction লাগবে।

  2. চিন্তা করুন: এই TinyKernel-এ ডেডলক (M6) সরাসরি বাস্তবায়ন করা হয়নি। যদি দুটি প্রসেস একে অপরের সেমাফোরের জন্য অপেক্ষা করতে থাকে (যেমন L21-এর ডাইনিং ফিলোসফার্স), TinyKernel-এর বর্তমান sem_wait/sem_signal ডিজাইনে কী হবে?

    বর্তমান ডিজাইনে একটিমাত্র শেয়ার্ড সেমাফোর আছে, তাই এই নির্দিষ্ট কাঠামোতে সরাসরি ডেডলক তৈরি করা কঠিন (একটি প্রসেস সবসময় বাকি একমাত্র সেমাফোরের জন্যই অপেক্ষা করে, কোনো "চক্র" তৈরি হওয়ার সুযোগ নেই)। কিন্তু L21/L23-এর মতো যদি একাধিক সেমাফোর/লক থাকত (যেমন একটি ফাইল-লক এবং একটি মেমরি-লক, দুটি প্রসেস বিপরীত ক্রমে চাইলে), তাহলে ঠিক M6-এর চারটি শর্ত (mutual exclusion, hold-and-wait, no preemption, circular wait) পূরণ হয়ে সত্যিকারের ডেডলক তৈরি হতে পারত — এটাই কেন M6-এর প্রতিরোধ/এড়ানো/সনাক্তকরণ কৌশলগুলো একাধিক-রিসোর্স বাস্তব সিস্টেমে অপরিহার্য।

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

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