পাঠ ২৬ · ৫৬-এর মধ্যে · মডিউল ৬
Home / Courses / Operating Systems (OS) / ডিটেকশন ও রিকভারি

ডেডলক ডিটেকশন ও রিকভারি

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

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

  • কেন কিছু সিস্টেম প্রিভেনশন/অ্যাভয়ডেন্স এড়িয়ে ইচ্ছাকৃতভাবে ডিটেকশন-ও-রিকভারি বেছে নেয়
  • ডিটেকশন অ্যালগরিদম L25-এর সেফটি অ্যালগরিদম থেকে ঠিক কীভাবে আলাদা
  • রিকভারির দুটি প্রধান কৌশল এবং ভিকটিম বাছাইয়ের বাস্তব ট্রেড-অফ
  • Python-এ ডিটেকশন অ্যালগরিদম প্রয়োগ করে L21/L23-এর ডাইনিং ফিলোসফার্স-স্টাইল দৃশ্যে প্রকৃত ডেডলকড প্রসেস চিহ্নিত করা, এবং একটি প্রসেস টার্মিনেট করে রিকভারি যাচাই করা

১ · কখন ডিটেকশন-ও-রিকভারি বেছে নেওয়া হয়

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

২ · ডিটেকশন অ্যালগরিদম — L25-এর সেফটি-চেকের একটি রূপ

ডেডলক ডিটেকশন (Deadlock Detection)Deadlock Detectionএকটি অ্যালগরিদম যা বর্তমান Allocation ও outstanding Request পরীক্ষা করে যাচাই করে কোন প্রসেসগুলো ইতিমধ্যে চিরস্থায়ীভাবে আটকে (ডেডলকড) আছে — L25-এর সেফটি অ্যালগরিদমের একটি প্রয়োগ, কিন্তু হাইপোথেটিক্যাল Max/Need-এর বদলে বর্তমান বাস্তব Request নিয়ে। L25-এর সেফটি অ্যালগরিদমের গঠন প্রায় হুবহু পুনরায় ব্যবহার করে, কিন্তু একটি গুরুত্বপূর্ণ পার্থক্যে — এখানে Need (হাইপোথেটিক্যাল, Max থেকে গণনা করা) নেই; বরং প্রতিটি প্রসেসের বর্তমান, বাস্তব outstanding Request ব্যবহার করা হয় (সে এই মুহূর্তে ঠিক কী চেয়ে অপেক্ষা করছে)। ধাপগুলো —

  1. যে প্রসেসের কোনো Allocation নেই, তাকে শুরুতেই "সম্পন্ন" ধরে নিন (তার কোনো রিসোর্স আটকে নেই, তাই সে ডেডলকের অংশ হতে পারে না)।
  2. অসম্পূর্ণ প্রসেসগুলোর মধ্যে এমন একজনকে খুঁজুন যার outstanding Request ≤ Available (Work)।
  3. তাকে "চালিয়ে দিন" — তার Allocation যোগ করে দিন Work-এ, "সম্পন্ন" চিহ্নিত করুন।
  4. আর কাউকে এগিয়ে নেওয়া সম্ভব না হওয়া পর্যন্ত পুনরাবৃত্তি করুন।

শেষে যে প্রসেসগুলো এখনো "অসম্পূর্ণ" রয়ে গেলো, তারাই প্রকৃতপক্ষে ডেডলকড — তাদের জন্য কোনো বাস্তবসম্মত ভবিষ্যৎ নেই যেখানে তারা এগোতে পারবে।

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

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

৩ · কোড: L21/L23-এর দৃশ্যে ডিটেকশন ও রিকভারি

নিচে L23-এ ব্যবহৃত ঠিক একই ৫-ফিলোসফার/৫-ফর্ক দৃশ্যকে (প্রতিটি ফর্কের ১টি instance, প্রতিটি ফিলোসফার নিজের বাম ফর্ক ধরে ডান ফর্কের জন্য অপেক্ষমাণ) এবার Allocation/Request ম্যাট্রিক্স হিসেবে মডেল করে ডিটেকশন অ্যালগরিদম চালানো হলো, তারপর একটি প্রসেস টার্মিনেট করে রিকভারি যাচাই করা হলো।

Python
# ডেডলক ডিটেকশন -- L23-এর ৫ ফিলোসফার/৫ ফর্ক দৃশ্যকে Allocation/Request ম্যাট্রিক্স হিসেবে মডেল করা

n = 5
allocation = [[0] * n for _ in range(n)]
request = [[0] * n for _ in range(n)]
for i in range(n):
    allocation[i][i] = 1              # Pi ইতিমধ্যে fork i (তার বাম ফর্ক) ধরে আছে
    request[i][(i + 1) % n] = 1       # Pi fork (i+1)%n (তার ডান ফর্ক) চেয়ে অপেক্ষমাণ

total_instances = [1] * n             # প্রতিটি ফর্কের মাত্র ১টি instance
allocated_per_resource = [sum(allocation[i][j] for i in range(n)) for j in range(n)]
available = [total_instances[j] - allocated_per_resource[j] for j in range(n)]

def detect_deadlock(available, allocation, request):
    """L25-এর সেফটি অ্যালগরিদমেরই একটি রূপ -- Need-এর বদলে বর্তমান outstanding Request ব্যবহার করে,
    ইতিমধ্যে ডেডলক ঘটে গেছে কি না তা যাচাই করে। রিটার্ন করে ডেডলকড প্রসেসের index-তালিকা।"""
    n_proc = len(allocation)
    n_res = len(available)
    work = list(available)
    finish = [all(a == 0 for a in allocation[i]) for i in range(n_proc)]  # কোনো allocation নেই = সম্পন্ন ধরা হলো

    progress = True
    while progress:
        progress = False
        for i in range(n_proc):
            if finish[i]:
                continue
            if all(request[i][j] <= work[j] for j in range(n_res)):
                for j in range(n_res):
                    work[j] += allocation[i][j]
                finish[i] = True
                progress = True

    return [i for i in range(n_proc) if not finish[i]]

print("Available:", available)
deadlocked = detect_deadlock(available, allocation, request)
print("ডেডলকড প্রসেস:", [f"P{i}" for i in deadlocked] if deadlocked else "কেউ না")

print("\n--- রিকভারি: P0-কে ভিকটিম হিসেবে বেছে টার্মিনেট করা হলো ---")
victim = 0
rec_available = list(available)
rec_allocation = [row[:] for row in allocation]
rec_request = [row[:] for row in request]

for j in range(n):
    rec_available[j] += rec_allocation[victim][j]   # ভিকটিমের ধরে-রাখা রিসোর্স ফেরত এলো
    rec_allocation[victim][j] = 0
    rec_request[victim][j] = 0                      # ভিকটিমের অনুরোধও বাতিল

print(f"P{victim} টার্মিনেট করার পর Available: {rec_available}")
deadlocked_after = detect_deadlock(rec_available, rec_allocation, rec_request)
if deadlocked_after:
    print("তবুও ডেডলকড প্রসেস:", [f"P{i}" for i in deadlocked_after])
else:
    print("এখন কেউ ডেডলকড নেই -- বাকি সবাই ক্রমান্বয়ে সম্পন্ন হতে পারবে")

    
হাতে-যাচাই: প্রথম রানে Available=[0,0,0,0,0] (প্রতিটি ফর্ক কারো না কারো হাতে), আর প্রতিটি প্রসেসের outstanding Request-এ ঠিক ১টি ফর্কের জন্য অপেক্ষা — যেহেতু Available-এ কিছুই খালি নেই, কারো Request-ই ≤ Work হতে পারে না, তাই একজনও এগোতে পারে না — সবাই ডেডলকড হিসেবে চিহ্নিত হয় (L23-এর ফলাফলের সাথে সামঞ্জস্যপূর্ণ)। P0 টার্মিনেট করার পর তার ফর্ক (fork 0) ফেরত এসে Available=[1,0,0,0,0] হয় — এই একটিমাত্র খালি ফর্কই যথেষ্ট যাতে P4 (fork 0-এর জন্য অপেক্ষমাণ) এগিয়ে যেতে পারে, তারপর তার ফর্ক ছেড়ে দিলে P3, তারপর P2, তারপর P1 — একে একে সবাই সম্পন্ন হয়। রিকভারি সফল।
মূল কথা · Key takeaway

ডিটেকশন ও রিকভারি ডেডলক আগে থেকে ঠেকায় না — এটি স্বীকার করে নেয় যে ডেডলক মাঝে মাঝে ঘটতে পারে, এবং তার পরিবর্তে নিয়মিত পরীক্ষা করে দ্রুত ধরে ফেলার এবং ন্যূনতম ক্ষতিতে সমাধান করার কৌশল অবলম্বন করে — L24-এর প্রিভেনশন ও L25-এর অ্যাভয়ডেন্সের বিপরীতে একটি সম্পূর্ণ ভিন্ন দার্শনিক অবস্থান।

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

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

প্র ০১ একটি সিস্টেম ইচ্ছাকৃতভাবে প্রিভেনশন/অ্যাভয়ডেন্স এড়িয়ে ডিটেকশন-ও-রিকভারি বেছে নিতে পারে কেন — এটা কি অলসতা, নাকি একটি যুক্তিসঙ্গত ইঞ্জিনিয়ারিং সিদ্ধান্ত?

এটি একটি যুক্তিসঙ্গত খরচ-বনাম-লাভ সিদ্ধান্ত। প্রিভেনশন নমনীয়তা কমায় (যেমন রিসোর্স-অর্ডারিং প্রতিটি প্রসেসকে নির্দিষ্ট ক্রমে চাইতে বাধ্য করে), আর অ্যাভয়ডেন্স প্রতিটি অনুরোধে একটি সম্পূর্ণ সেফটি-চেক চালানোর গণনা-খরচ যোগ করে। যদি বাস্তব অভিজ্ঞতা বা পরিসংখ্যান দেখায় যে এই নির্দিষ্ট সিস্টেমে ডেডলক আদৌ বিরল, তাহলে প্রতিটি অনুরোধে এই খরচ বহন না করে মাঝে মাঝে সস্তায় ডিটেকশন চালিয়ে প্রয়োজনে রিকভার করা সামগ্রিকভাবে বেশি কার্যকর হতে পারে।

প্র ০২ ডিটেকশন অ্যালগরিদমে finish[i]-এর প্রাথমিক মান "Allocation শূন্য কি না" দিয়ে ঠিক করা হলো কেন — L25-এর সেফটি অ্যালগরিদমে এমন কিছু ছিল কি?

যে প্রসেসের কোনো রিসোর্সই বরাদ্দ নেই, সে সংজ্ঞা অনুযায়ী কোনো সার্কুলার-ওয়েট চেইনের অংশ হতে পারে না — তার কাছে এমন কিছু নেই যা অন্য কাউকে আটকে রাখতে পারে। তাই তাকে শুরু থেকেই "সম্পন্ন" ধরে নেওয়া নিরাপদ ও যৌক্তিক। L25-এর সেফটি অ্যালগরিদমে সব প্রসেসই finish = False দিয়ে শুরু হয়েছিল, কারণ সেখানে প্রশ্নটা ছিল "সবাই কি ভবিষ্যতে শেষ করতে পারবে" — এখানে প্রশ্নটা "কে এখনই আটকে আছে", তাই যাদের কিছুই ধরা নেই তাদের বাদ দিয়ে শুরু করাই স্বাভাবিক।

প্র ০৩ কোড সেলে P0-এর বদলে P2-কে ভিকটিম বানালে কি রিকভারি একইভাবে সফল হবে? নিজে যুক্তি দিন, তারপর কোড চালিয়ে যাচাই করুন।

হ্যাঁ। দৃশ্যটি প্রতিসম (symmetric) — যেকোনো একটি ফর্ক ফেরত এলেই একটি একক ব্রেক-পয়েন্ট তৈরি হয়ে যায়, যা থেকে ধারাবাহিকভাবে বাকি সবাই একে একে সম্পন্ন হতে পারে (ঠিক L24-এর রিসোর্স-অর্ডারিং সমাধানে যেমন একটি এজ ভাঙাই পুরো চক্র ভেঙে দিয়েছিল)। victim = 2 করে চালালে দেখা যাবে deadlocked_after আবারও খালি তালিকা — সব প্রসেস সম্পন্ন হয়।

অনুশীলন

  1. চিন্তা করুন: প্রথম রানে (রিকভারির আগে) কেন detect_deadlock() একজনকেও এগিয়ে নিতে পারলো না, অথচ L25-এর বেস উদাহরণে অন্তত একজন (P1) সহজেই এগিয়ে যেতে পেরেছিল?

    L25-এ Available ছিল (3,2) — যথেষ্ট বড়, তাই P1-এর ছোট Need (1,1) সহজেই মেটানো গিয়েছিল। কিন্তু এখানে প্রতিটি ফর্কের মাত্র ১টি instance, এবং সবগুলোই ইতিমধ্যে কারো না কারো হাতে — তাই Available সম্পূর্ণ শূন্য ([0,0,0,0,0])। কারো Request-ই, তা যত ছোটই হোক (এখানে প্রতিটি মাত্র ১ ইউনিট), শূন্য Available দিয়ে মেটানো সম্ভব নয় — তাই কেউই প্রথম রানে এগোতে পারে না।

  2. পরীক্ষা করুন: কোড সেলে victim = 0-এর বদলে victim = 3 করে Run চেপে দেখুন রিকভারি এখনও সফল হয় কি না।

    হ্যাঁ, সফল হবে। P3 টার্মিনেট করলে fork 3 ফেরত আসে, Available=[0,0,0,1,0] হয় — যা P2-কে (fork 3-এর জন্য অপেক্ষমাণ) এগোতে দেয়, তারপর ধারাবাহিকভাবে P1, P0, P4 — সবাই একে একে সম্পন্ন হয়ে যায়। দৃশ্যের প্রতিসাম্যের কারণে যেকোনো একটি ভিকটিম বেছে নিলেই একই ফলাফল আসবে — deadlocked_after খালি তালিকা।

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

আগের পাঠ
ডেডলক অ্যাভয়ডেন্স — ব্যাংকার'স অ্যালগরিদম