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

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

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

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

  • একটি একক run_scheduler(processes, algorithm) ফাংশন কীভাবে একাধিক শিডিউলিং অ্যালগরিদম dispatch করতে পারে
  • FCFS, SJF (L10), প্রায়োরিটি ও Round Robin (L11)-এর প্রকৃত, কার্যকরী বাস্তবায়ন
  • একই প্রসেস সেটে সবকটি অ্যালগরিদম চালিয়ে গড় ওয়েটিং/টার্নঅ্যারাউন্ড টাইম হিসাব ও তুলনা করা
  • কেন কোনো একক অ্যালগরিদম সবসময় "সেরা" নয় — প্রতিটির নিজস্ব ট্রেড-অফ আছে

১ · একটি শেয়ার্ড প্রসেস সেট

L09-এ শেখা গড় ওয়েটিং টাইম ও গড় টার্নঅ্যারাউন্ড টাইম মেট্রিক ব্যবহার করে, চলুন M3-এর সবকটি অ্যালগরিদমকে একই ৪টি প্রসেসের ওপর চালাই — arrival ও burst time সচেতনভাবে এমনভাবে বেছে নেওয়া হয়েছে যাতে অ্যালগরিদমগুলোর পার্থক্য স্পষ্টভাবে দেখা যায় (ছোট প্রসেসগুলো arrival অর্ডারে প্রথমে নেই, তাই SJF-এর সুবিধা প্রকৃতপক্ষে দেখা যাবে)।

P1
arrival=0, burst=8, priority=4
P2
arrival=1, burst=4, priority=3
P3
arrival=2, burst=9, priority=1
P4
arrival=3, burst=5, priority=2

প্রায়োরিটিতে (L11-এর কনভেনশন অনুযায়ী) ছোট সংখ্যা = উচ্চ প্রায়োরিটি। লক্ষ্য করুন এই উদাহরণে প্রায়োরিটি অর্ডার (P3 > P4 > P2 > P1) burst-time অর্ডার (P2=4 সবচেয়ে ছোট) থেকে ইচ্ছাকৃতভাবে ভিন্ন — যাতে প্রায়োরিটি শিডিউলিং SJF-এর মতো "স্বয়ংক্রিয়ভাবে সঠিক" ফল না দেয়, বরং একটি বাস্তবসম্মত, স্বাধীন ফলাফল দেখা যায়।

২ · dispatcher বাস্তবায়ন

নিচের কোড সেলে চারটি অ্যালগরিদমই আলাদা ফাংশন হিসেবে বাস্তবায়ন করা হলো, এবং একটি run_scheduler() dispatcher ফাংশন সঠিক ফাংশনে কল পাঠায় — algorithm প্যারামিটার বদলালেই একই ইনপুটে ভিন্ন অ্যালগরিদম চালানো যায়, যেন এটি একটি বাস্তব "pluggable scheduler"।

Python
from collections import deque

processes_base = [
    {"pid": "P1", "arrival": 0, "burst": 8, "priority": 4},
    {"pid": "P2", "arrival": 1, "burst": 4, "priority": 3},
    {"pid": "P3", "arrival": 2, "burst": 9, "priority": 1},
    {"pid": "P4", "arrival": 3, "burst": 5, "priority": 2},
]

def avg(values):
    return sum(values) / len(values)

def fcfs(procs):
    order = sorted(procs, key=lambda p: (p["arrival"], p["pid"]))
    time, result = 0, []
    for p in order:
        start = max(time, p["arrival"])
        finish = start + p["burst"]
        result.append({"pid": p["pid"], "finish": finish,
                        "wait": start - p["arrival"], "turnaround": finish - p["arrival"]})
        time = finish
    return result

def sjf(procs):
    remaining = sorted(procs, key=lambda p: p["arrival"])
    time, done = 0, []
    while remaining:
        ready = [p for p in remaining if p["arrival"] <= time]
        if not ready:
            time = remaining[0]["arrival"]
            ready = [p for p in remaining if p["arrival"] <= time]
        nxt = min(ready, key=lambda p: (p["burst"], p["arrival"], p["pid"]))
        start = time
        finish = start + nxt["burst"]
        done.append({"pid": nxt["pid"], "finish": finish,
                      "wait": start - nxt["arrival"], "turnaround": finish - nxt["arrival"]})
        time = finish
        remaining.remove(nxt)
    return done

def priority_scheduling(procs):
    remaining = sorted(procs, key=lambda p: p["arrival"])
    time, done = 0, []
    while remaining:
        ready = [p for p in remaining if p["arrival"] <= time]
        if not ready:
            time = remaining[0]["arrival"]
            ready = [p for p in remaining if p["arrival"] <= time]
        nxt = min(ready, key=lambda p: (p["priority"], p["arrival"], p["pid"]))
        start = time
        finish = start + nxt["burst"]
        done.append({"pid": nxt["pid"], "finish": finish,
                      "wait": start - nxt["arrival"], "turnaround": finish - nxt["arrival"]})
        time = finish
        remaining.remove(nxt)
    return done

def round_robin(procs, quantum):
    order = sorted(procs, key=lambda p: p["arrival"])
    remaining_burst = {p["pid"]: p["burst"] for p in procs}
    arrival = {p["pid"]: p["arrival"] for p in procs}
    burst_total = {p["pid"]: p["burst"] for p in procs}
    finish_time = {}
    queue = deque()
    i, n, time = 0, len(order), 0
    while i < n and order[i]["arrival"] <= time:
        queue.append(order[i]["pid"]); i += 1
    while queue:
        pid = queue.popleft()
        run = min(quantum, remaining_burst[pid])
        time += run
        remaining_burst[pid] -= run
        while i < n and order[i]["arrival"] <= time:
            queue.append(order[i]["pid"]); i += 1
        if remaining_burst[pid] > 0:
            queue.append(pid)
        else:
            finish_time[pid] = time
    result = []
    for p in procs:
        pid = p["pid"]
        turnaround = finish_time[pid] - arrival[pid]
        result.append({"pid": pid, "finish": finish_time[pid],
                        "wait": turnaround - burst_total[pid], "turnaround": turnaround})
    return result

def run_scheduler(processes, algorithm, quantum=3):
    """একক dispatcher -- algorithm অনুযায়ী সঠিক শিডিউলারে কল পাঠায়।"""
    procs = [dict(p) for p in processes]   # মূল ডেটা অপরিবর্তিত রাখতে কপি
    if algorithm == "FCFS":
        return fcfs(procs)
    if algorithm == "SJF":
        return sjf(procs)
    if algorithm == "Priority":
        return priority_scheduling(procs)
    if algorithm == "RoundRobin":
        return round_robin(procs, quantum)
    raise ValueError(f"অজানা অ্যালগরিদম: {algorithm}")

summary = []
for algo in ["FCFS", "SJF", "Priority", "RoundRobin"]:
    result = run_scheduler(processes_base, algo, quantum=3)
    avg_wait = avg([r["wait"] for r in result])
    avg_turnaround = avg([r["turnaround"] for r in result])
    summary.append((algo, avg_wait, avg_turnaround))

print(f"{'অ্যালগরিদম':12} | {'গড় ওয়েটিং টাইম':18} | গড় টার্নঅ্যারাউন্ড টাইম")
print("-" * 60)
for algo, w, t in summary:
    print(f"{algo:12} | {w:18.2f} | {t:.2f}")

    
হাতে-হিসাব করে যাচাই করা এই নির্দিষ্ট প্রসেস সেটের জন্য: FCFS-এর গড় ওয়েটিং ৮.৭৫, SJF-এর ৭.৭৫ (সবচেয়ে কম — L10-এর দাবি অনুযায়ী), প্রায়োরিটি-এর ১০.২৫ (এই নির্দিষ্ট প্রায়োরিটি অ্যাসাইনমেন্টে খারাপ), আর quantum=3-এর Round Robin-এর ১৩.৫ (সবচেয়ে বেশি — কারণ ঘন ঘন কনটেক্সট সুইচ প্রতিটি প্রসেসের সমাপ্তি পিছিয়ে দেয়, যদিও এটি সবচেয়ে ভালো response time/ন্যায্যতা দেয়, L11)। কোড সেলটি রান করে এই সংখ্যাগুলো নিজে যাচাই করুন।
মূল কথা · Key takeaway

M3-এর পুরো মডিউলের ব্যবহারিক শিক্ষা এটাই — কোনো একক শিডিউলিং অ্যালগরিদম সব পরিস্থিতিতে "সেরা" নয়। SJF গড় ওয়েটিং টাইমের দিক থেকে তাত্ত্বিকভাবে অপ্টিমাল, কিন্তু ভবিষ্যতের burst time জানা দরকার (অবাস্তব)। Round Robin গড় ওয়েটিং টাইমে খারাপ হতে পারে, কিন্তু ইন্টারঅ্যাকটিভ সিস্টেমে ভালো response time দেয়। বাস্তব OS ডিজাইনাররা (L52-53 দেখুন) এই ট্রেড-অফগুলো মাথায় রেখেই তাদের শিডিউলার ডিজাইন করে।

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

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

প্র ০১ SJF-এর গড় ওয়েটিং টাইম সবচেয়ে কম হলো কেন — এটি কি সবসময় সত্য হবে?

SJF প্রমাণিতভাবে (L10) নন-প্রিএম্পটিভ অ্যালগরিদমগুলোর মধ্যে গড় ওয়েটিং টাইম মিনিমাইজ করার জন্য অপ্টিমাল — এটি সবসময়ই সত্য, যতক্ষণ সব প্রসেসের সঠিক burst time আগে থেকে জানা থাকে। বাস্তব সমস্যা হলো, একটি বাস্তব সিস্টেমে ভবিষ্যতের burst time নিশ্চিতভাবে জানা যায় না, শুধু আনুমানিক করা যায় — তাই SJF তাত্ত্বিকভাবে সেরা হলেও ব্যবহারিকভাবে সীমিত।

প্র ০২ Round Robin-এর গড় ওয়েটিং টাইম এই উদাহরণে সবচেয়ে বেশি হওয়া সত্ত্বেও এটি বাস্তবে ব্যাপকভাবে ব্যবহৃত হয় কেন?

কারণ গড় ওয়েটিং টাইম একমাত্র মেট্রিক নয় (L09 দেখুন) — Round Robin প্রতিটি প্রসেসকে নিয়মিত, বাউন্ডেড response time দেয় (একটি প্রসেস কখনোই খুব বেশিক্ষণ CPU-এর জন্য অপেক্ষা করে না), যা ইন্টারঅ্যাকটিভ সিস্টেমে (যেখানে ব্যবহারকারী দ্রুত প্রতিক্রিয়া আশা করে) গড় টার্নঅ্যারাউন্ড টাইমের চেয়ে অনেক বেশি গুরুত্বপূর্ণ।

প্র ০৩ যদি P3-এর প্রায়োরিটি ১ না হয়ে ৪ (সবচেয়ে কম) করা হতো, প্রায়োরিটি শিডিউলিং-এর ফলাফল কীভাবে বদলাত?

P3 (burst=9, সবচেয়ে বড় প্রসেস) তখন সবার শেষে চলত, তাই তার নিজের ওয়েটিং টাইম আরও বাড়ত, কিন্তু P2 ও P4 (ছোট প্রসেস) আগে সুযোগ পেয়ে তাদের ওয়েটিং টাইম কমত। এই পরিবর্তনের প্রকৃত সংখ্যা কোড সেলে processes_base-এ P3-এর priority বদলে আবার রান করলেই দেখা যাবে — এটাই প্রায়োরিটি শিডিউলিং-এর মূল বৈশিষ্ট্য: ফলাফল সম্পূর্ণভাবে প্রায়োরিটি অ্যাসাইনমেন্টের ওপর নির্ভরশীল।

অনুশীলন

  1. পরীক্ষা করুন: কোড সেলে quantum=3-কে quantum=2 করে আবার রান করুন। Round Robin-এর গড় ওয়েটিং টাইম বাড়ে না কমে?

    ছোট quantum মানে আরও ঘন ঘন কনটেক্সট সুইচ — এই নির্দিষ্ট উদাহরণে এটি সাধারণত গড় ওয়েটিং টাইমকে আরও পরিবর্তন করবে (বাড়াতেও পারে, কমাতেও পারে, প্রসেসগুলোর নির্দিষ্ট arrival/burst প্যাটার্নের ওপর নির্ভর করে) — তবে প্রতিটি প্রসেস আরও দ্রুত, আরও ঘন ঘন CPU-এর "পালা" পাবে (response time-এর দৃষ্টিকোণ থেকে ভালো), যা L11-এর quantum-সাইজ ট্রেড-অফ আলোচনার সাথে সরাসরি মেলে। কোড রান করে প্রকৃত সংখ্যাটি নিজে দেখুন।

  2. চিন্তা করুন: run_scheduler()-এ একটি নতুন algorithm="SRTF" (Shortest Remaining Time First, L10-এর প্রিএম্পটিভ ভ্যারিয়েন্ট) যোগ করতে হলে কী পরিবর্তন করতে হবে বলে মনে হয়?

    SJF-এর মতো একটি নতুন ফাংশন লিখতে হবে, কিন্তু সেটি প্রতিটি টাইম-ইউনিটে (বা প্রতিটি নতুন arrival-এ) পরীক্ষা করবে বর্তমানে চলা প্রসেসের বাকি সময়ের চেয়ে কোনো নতুন-আসা প্রসেসের বাকি সময় কম কি না — যদি হ্যাঁ, তাহলে বর্তমান প্রসেসকে preempt করে নতুনটি চালাবে। এটি SJF-এর মতোই run_scheduler()-এ একটি নতুন if algorithm == "SRTF": return srtf(procs) শাখা হিসেবে সহজেই যোগ করা যায় — dispatcher-এর মূল কাঠামো একই থাকে।

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

আগের পাঠ
কেস স্টাডি: Windows OS আর্কিটেকচার