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

ডেডলক প্রিভেনশন

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

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

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

১ · প্রিভেনশনের মূল ধারণা

ডেডলক প্রিভেনশন (Deadlock Prevention)Deadlock PreventionL23-এর চারটি প্রয়োজনীয় শর্তের অন্তত একটিকে কখনোই সত্যি হতে না দেওয়ার জন্য সিস্টেমকে এমনভাবে ডিজাইন করা, যাতে ডেডলক কাঠামোগতভাবে অসম্ভব হয়ে যায়। মানে হলো, L23-এর চারটি শর্তের অন্তত একটিকে কখনোই সত্যি হতে না দেওয়ার জন্য সিস্টেম-ডিজাইন করা — যাতে ডেডলক ঘটার কোনো সুযোগই তৈরি না হয়। যেহেতু চারটি শর্তই একসাথে সত্যি হলে তবেই ডেডলক সম্ভব, একটি ভাঙলেই যথেষ্ট।

মিউচুয়াল এক্সক্লুশন আক্রমণ
সাধারণত ব্যবহারিক নয় — একটি প্রিন্টার বা একটি এক্সক্লুসিভ লকের মতো কিছু রিসোর্স স্বভাবতই এক্সক্লুসিভ; জোর করে "শেয়ারেবল" ঘোষণা করা যায় না।
হোল্ড-অ্যান্ড-ওয়েট আক্রমণ
একটি প্রসেসকে তার প্রয়োজনীয় সব রিসোর্স একসাথে, আগে থেকেই চাইতে বাধ্য করা — হয় সবটা পাবে, নয়তো অপেক্ষা করবে, কখনো কিছু ধরে রেখে বাকিটার জন্য অপেক্ষা করবে না। সহজ, কিন্তু রিসোর্স-অপচয়ী (এমন রিসোর্স আগেভাগে ধরে রাখা যা অনেক পরে দরকার হবে) এবং আগে থেকে সব চাহিদা জানা দরকার।
নো-প্রিএম্পশন আক্রমণ
OS-কে একটি অপেক্ষমাণ প্রসেসের রিসোর্স জোরপূর্বক কেড়ে নেওয়ার অনুমতি দেওয়া (পরে ফেরত দেওয়া হবে) — কেবল সেসব রিসোর্সের জন্য সম্ভব যাদের অবস্থা নিরাপদে সেভ/রিস্টোর করা যায় (যেমন CPU রেজিস্টার, L06-এর কনটেক্সট সুইচের মতো) — একটি প্রিন্টারের মাঝপথে প্রিন্ট জব "প্রিএম্পট" করা বাস্তবে অসম্ভব বা ক্ষতিকর।
সার্কুলার ওয়েট আক্রমণ · সবচেয়ে ব্যবহারিক
প্রতিটি রিসোর্স টাইপকে একটি সংখ্যাগত rank দিয়ে একটি টোটাল অর্ডারিং বসানো, এবং প্রতিটি প্রসেসকে শুধু বাড়তি-ক্রমে রিসোর্স চাইতে বাধ্য করা — এতে একটি বৃত্তাকার চেইন গাণিতিকভাবেই অসম্ভব হয়ে যায়। বাস্তব সিস্টেমে সবচেয়ে বেশি ব্যবহৃত কৌশল।

২ · কেন রিসোর্স-অর্ডারিং কাজ করে

ধরুন প্রতিটি রিসোর্সকে একটি অনন্য rank দেওয়া হলো, এবং নিয়ম হলো — একটি প্রসেস rank X ধরে রাখা অবস্থায় rank Y < X-এর কোনো রিসোর্স চাইতে পারবে না। এখন যদি একটি সার্কুলার-ওয়েট চেইন গঠনের চেষ্টা হয় (P1 অপেক্ষা P2-এর রিসোর্সের জন্য, P2 অপেক্ষা P3-এর, ..., Pk অপেক্ষা P1-এর রিসোর্সের জন্য) — চেইনের প্রতিটি ধাপে rank কঠোরভাবে বাড়তে থাকবে, কিন্তু চেইনটি শেষে আবার P1-এ ফিরে আসছে মানে rank-কে আবার প্রথম মানে ফিরতে হবে — এটি গাণিতিকভাবে অসম্ভব (একটি সংখ্যা কঠোরভাবে বেড়ে আবার নিজের কাছেই ফিরে আসতে পারে না)। ফলে চক্র গঠনই অসম্ভব হয়ে যায়।

L21/L23-এর সাথে সরাসরি সংযোগ

L23-এর অনুশীলনে আমরা দেখেছি একটি মাত্র এজ সরিয়ে দিলেই ডাইনিং ফিলোসফার্সের চক্রটি ভেঙে যায়। ফর্কগুলোকে সংখ্যায়িত করে "সবসময় ছোট-rank ফর্ক আগে চাও" নিয়ম বসালে ঠিক এই এজ-ভাঙাটাই স্বয়ংক্রিয়ভাবে ঘটে — সীমানায় থাকা ফিলোসফার (যার দুটি ফর্ক rank ৪ ও rank ০, "wrap around" করে) বাধ্য হয় তার প্রচলিত বাম-আগে নিয়ম উল্টে rank ০ ফর্কটি আগে চাইতে, যা প্রতিসাম্য ভেঙে দেয়।

৩ · কোড: রিসোর্স-অর্ডারিং দিয়ে ডাইনিং ফিলোসফার্স সমাধান

নিচে প্রথমে request_resource() ফাংশনটি দেখানো হলো, যা বাড়তি-ক্রম লঙ্ঘনকারী অনুরোধ প্রত্যাখ্যান করে। তারপর সেই একই নিয়ম দিয়ে L21/L23-এর ৫-ফিলোসফার/৫-ফর্ক দৃশ্যটি রাউন্ড-ভিত্তিক সিমুলেশনে চালিয়ে দেখানো হলো — ফর্কগুলো rank অনুযায়ী নম্বরিত, প্রতিটি ফিলোসফার সবসময় ছোট-rank ফর্ক আগে চায়।

Python
# রিসোর্স-অর্ডারিং -- ডেডলকের "সার্কুলার ওয়েট" শর্ত আক্রমণ করা

def request_resource(held_ranks, new_rank):
    """বর্তমানে ধরে-রাখা সর্বোচ্চ rank-এর চেয়ে কম rank-এর রিসোর্স চাইলে প্রত্যাখ্যাত --
    এভাবেই প্রতিটি প্রসেসকে বাড়তি-ক্রমে রিসোর্স চাইতে বাধ্য করা হয়।"""
    if held_ranks and new_rank < max(held_ranks):
        return False
    return True

print("out-of-order অনুরোধ (rank 3 ধরে রেখে rank 1 চাওয়া):",
      "গ্রহণযোগ্য" if request_resource([3], 1) else "প্রত্যাখ্যাত")
print("সঠিক-ক্রম অনুরোধ (rank 3 ধরে রেখে rank 4 চাওয়া):",
      "গ্রহণযোগ্য" if request_resource([3], 4) else "প্রত্যাখ্যাত")

print("\n--- এখন পুরো ডাইনিং ফিলোসফার্স দৃশ্য: ফর্ক rank অনুযায়ী, সবসময় ছোট rank আগে ---")

n = 5
# philosopher i-র দুটি ফর্ক rank -- সবসময় (ছোট, বড়) হিসেবে সাজানো, wrap-around ফিলোসফার (i=4)-এর
# ক্ষেত্রে এটি স্বয়ংক্রিয়ভাবেই প্রচলিত বাম-আগে নিয়ম উল্টে দেয়
low_high = []
for i in range(n):
    a, b = i, (i + 1) % n
    low_high.append((min(a, b), max(a, b)))

fork_owner = [None] * n     # fork_owner[f] = কোন ফিলোসফার fork f ধরে আছে, বা None
held = [set() for _ in range(n)]
done = [False] * n
round_num = 0

while not all(done) and round_num < 20:
    round_num += 1
    ate_this_round = []
    for i in range(n):
        if done[i]:
            continue
        low, high = low_high[i]
        if len(held[i]) == 0:
            if fork_owner[low] is None and request_resource(list(held[i]), low):
                fork_owner[low] = i
                held[i].add(low)
        elif len(held[i]) == 1:
            if fork_owner[high] is None and request_resource(list(held[i]), high):
                fork_owner[high] = i
                held[i].add(high)
                # দুটো ফর্কই পাওয়া গেছে -- খেয়ে নিয়ে সাথে সাথেই দুটো ফর্ক ছেড়ে দিলো
                fork_owner[low] = None
                fork_owner[high] = None
                held[i] = set()
                done[i] = True
                ate_this_round.append(i)
    print(f"রাউন্ড {round_num}: fork_owner={fork_owner}, এই রাউন্ডে খেলো={ate_this_round}")

print(f"\nসবাই ডেডলক ছাড়াই খেয়েছে: {all(done)} ({round_num} রাউন্ডে)")

    
লক্ষ্য করুন — প্রথম রাউন্ডে P4 (ফর্ক ০ ও ৪-এর মালিকানা চায়) তার "ছোট rank" ফর্ক ০ পায় না, কারণ P0 ইতিমধ্যে সেটি নিয়ে নিয়েছে। কিন্তু এই একটি বাধাই যথেষ্ট চক্রটি ভাঙার জন্য — P3 প্রথম তার দুটি ফর্ক পেয়ে খেয়ে নেয় ও ছেড়ে দেয়, তারপর ধারাবাহিকভাবে P2, P1, P0 একইভাবে খেয়ে নেয়, এবং সবশেষে P4 উভয় ফর্ক খালি পেয়ে খায় — মোট ৬ রাউন্ডে সবাই ডেডলক ছাড়াই খেয়ে নেয়, L21-এর নাইভ সমাধানের বিপরীতে।
মূল কথা · Key takeaway

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

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

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

প্র ০১ মিউচুয়াল এক্সক্লুশন শর্তটি আক্রমণ করা কেন সাধারণত অন্য তিনটির চেয়ে বেশি অব্যবহারিক?

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

প্র ০২ হোল্ড-অ্যান্ড-ওয়েট আক্রমণ (সব রিসোর্স একসাথে আগে চাওয়া) কেন "রিসোর্স-অপচয়ী" — একটি বাস্তব উদাহরণ দিন।

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

প্র ০৩ P4-এর ফর্ক-জোড়া কেন (0, 4) হিসেবে গণ্য হলো, (4, 0) নয় — এবং এই ছোট্ট পার্থক্যটাই কীভাবে পুরো চক্র ভাঙলো?

কোডে low, high = min(a, b), max(a, b) ব্যবহার করা হয়েছে, তাই P4-এর ফর্ক ৪ ও ০-এর মধ্যে rank অনুযায়ী ছোটটি (০) সবসময় "low" হিসেবে প্রথমে চাওয়া হয় — এটিই নিয়মের মূল কথা: rank দিয়ে সাজানো, নিজের বাম/ডান দিয়ে নয়। বাকি সব ফিলোসফারের জন্য এটি তাদের প্রচলিত বাম-ফর্ক-আগে আচরণের সাথে মিলে যায়, কিন্তু P4-এর জন্য এটি উল্টে যায় (তাকে "ডান" ফর্ক ০ আগে চাইতে হয়) — এই একমাত্র ব্যতিক্রমটিই L23-এর সার্কুলার-ওয়েট চেইনের একটি এজ প্রতিসাম্যচ্যুত করে দেয়, যা পুরো চক্রকে গাণিতিকভাবে অসম্ভব করে তোলে।

অনুশীলন

  1. চিন্তা করুন: কোড সেলের রাউন্ড ১-এর আউটপুটে কেন P4 কোনো ফর্ক পায়নি, অথচ P0-P3 প্রত্যেকেই একটি করে ফর্ক পেয়েছে?

    P4-এর low fork হলো ০, যেটি P0-এরও low fork। যেহেতু P0 তালিকায় আগে আসে (index ০) এবং রাউন্ডের ভেতরে ক্রমান্বয়ে প্রসেস করা হয়, P0 প্রথমেই fork ০ দখল করে ফেলে P4 এই একই রাউন্ডে পৌঁছানোর আগেই। ফলে P4 তার প্রথম ফর্কের জন্যই অপেক্ষায় থেকে যায় — কিন্তু কখনো স্থায়ীভাবে আটকায় না, কারণ P0 পরে ফর্ক ০ ছেড়ে দিলে P4 সেটি পেয়ে যায় (রাউন্ড ৫)।

  2. পরীক্ষা করুন: কোড সেলে request_resource([2], 2) কল করে দেখুন সমান rank-এর ক্ষেত্রে কী ফলাফল আসে, এবং ভাবুন দুটো ভিন্ন রিসোর্সের rank সমান হলে কী সমস্যা হতে পারে।

    request_resource([2], 2) রিটার্ন করবে True (গ্রহণযোগ্য), কারণ শর্তটি হলো new_rank < max(held_ranks) — সমান হলে এটি সত্যি নয়, তাই প্রত্যাখ্যাত হয় না। কিন্তু বাস্তবে যদি দুটি ভিন্ন রিসোর্সের rank ইচ্ছাকৃতভাবে সমান রাখা হয়, তাহলে দুই দিক থেকেই একে অপরকে "বৈধভাবে" চাওয়া সম্ভব হয়ে যেতে পারে — যা একটি সূক্ষ্ম সার্কুলার-ওয়েট চেইন আবার তৈরি করার ঝুঁকি রাখে। এই কারণেই রিসোর্স-অর্ডারিং কৌশলে প্রতিটি রিসোর্স টাইপের rank অবশ্যই অনন্য (একটি প্রকৃত টোটাল অর্ডার) হতে হবে, নইলে নিয়মটি নিজেই দুর্বল হয়ে পড়ে।

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

আগের পাঠ
ডেডলক — চারটি প্রয়োজনীয় শর্ত