পাঠ ৩০ · ৫৭-এর মধ্যে · মডিউল ৭
Home / Courses / Microprocessors, Embedded Systems & IoT / প্রায়োরিটি ইনভার্সন

সেমাফোর, মিউটেক্স ও প্রায়োরিটি ইনভার্সন

Semaphores, mutexes and priority inversion
১২ মিনিট পড়া কঠিন · Advanced Python সিমুলেশনসহ সম্পূর্ণ বাংলায়

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

  • সেমাফোর ও মিউটেক্সের সংজ্ঞা ও পার্থক্য
  • প্রায়োরিটি ইনভার্সন সমস্যাটি ঠিক কীভাবে ঘটে — ধাপে ধাপে একটি genuine simulation-এ
  • প্রায়োরিটি ইনহেরিটেন্স কীভাবে এই সমস্যা সমাধান করে, একই সিমুলেশনে পাশাপাশি তুলনা করে
  • Mars Pathfinder মিশনের বাস্তব ঐতিহাসিক উদাহরণ — কেন এই বাগ ক্লাসটি RTOS ইঞ্জিনিয়ারিংয়ে এত গুরুত্বপূর্ণ

১ · সেমাফোর ও মিউটেক্স

যখন একাধিক RTOS টাস্ক একটি শেয়ার্ড রিসোর্স (একটি বাফার, একটি পেরিফেরাল, একটি ভ্যারিয়েবল) ব্যবহার করে, একসাথে দুটো টাস্ক সেটি স্পর্শ করলে ডেটা করাপশন হতে পারে। এই সমস্যা সমাধানে দুটো মূল প্রিমিটিভ ব্যবহৃত হয়:

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

মিউটেক্সMutex (Mutual Exclusion)N=১ বিশিষ্ট একটি বিশেষ সেমাফোর — একবারে শুধুমাত্র একটি টাস্ক এটি ধরে রাখতে পারে, এবং যে টাস্ক এটি acquire করেছে তাকেই release করতে হয় (ownership থাকে)। হলো N=১ বিশিষ্ট একটি বিশেষ সেমাফোর — একবারে শুধু একটি টাস্ক এটি ধরে রাখতে পারে (একটি ক্রিটিক্যাল সেকশন সুরক্ষিত করতে), এবং গুরুত্বপূর্ণভাবে এর মালিকানা থাকে — যে টাস্ক acquire() করেছে শুধু সেই টাস্কই release() করতে পারে। এই "মালিকানা" ধারণাটাই নিচে আলোচিত প্রায়োরিটি ইনভার্সন সমস্যার মূল কেন্দ্রবিন্দু।

২ · প্রায়োরিটি ইনভার্সন — একটি ক্লাসিক বাগ

কল্পনা করুন তিনটি টাস্ক — H (উচ্চ প্রায়োরিটি), M (মধ্যম প্রায়োরিটি), আর L (নিম্ন প্রায়োরিটি)। L একটি মিউটেক্স ধরে একটি ক্রিটিক্যাল সেকশনে কাজ করছে। ঠিক তখনই H রেডি হয়ে সেই একই মিউটেক্সের জন্য অপেক্ষা করা শুরু করে — যুক্তিসঙ্গতভাবে এটুকু সময় (L-এর ক্রিটিক্যাল সেকশন শেষ হওয়া পর্যন্ত) ব্লক থাকার কথা। কিন্তু সমস্যাটা হলো — M-এর মিউটেক্সের সাথে কোনো সম্পর্কই নেই, অথচ M-এর প্রায়োরিটি L-এর চেয়ে বেশি, তাই স্বাভাবিক শিডিউলিং নিয়মে (L29) M রেডি হলেই সে L-কে প্রি-এম্পট করে ফেলে। ফলে L তার ক্রিটিক্যাল সেকশন শেষ করতে পারে না, মিউটেক্স রিলিজ করতে পারে না — আর তাই H, যার প্রায়োরিটি সবার চেয়ে বেশি, পরোক্ষভাবে M-এর পেছনে আটকে থাকে। এটাই প্রায়োরিটি ইনভার্সনPriority Inversionএকটি মধ্যম-প্রায়োরিটি টাস্কের কারণে একটি উচ্চ-প্রায়োরিটি টাস্ক পরোক্ষভাবে অনির্দিষ্টকালের জন্য ব্লক থাকা — প্রায়োরিটি অর্ডারের একটি বিপরীতমুখী (inverted) পরিস্থিতি। — নিয়মমতো H > M > L, কিন্তু বাস্তবে M পরোক্ষভাবে H-কে ব্লক করে রাখছে।

৩ · সিমুলেশন ১ — ইনভার্সন ইনহেরিটেন্স ছাড়া

নিচের টাইমলাইনটি নিচের কোড সেলের প্রথম সিমুলেশনের (use_inheritance=False) প্রকৃত আউটপুট থেকে আঁকা। L tick ২-এ মিউটেক্স অ্যাকোয়ার করে, H tick ৫-এ ব্লক হয় — কিন্তু M (মিউটেক্সের সাথে কোনো সম্পর্ক নেই) tick ৩-৭ পর্যন্ত L-কে প্রি-এম্পট করে রাখায় L-এর ক্রিটিক্যাল সেকশন শেষ হতে দেরি হয়, ফলে H-কে tick ১০ পর্যন্ত (মোট ৬ টিক) অপেক্ষা করতে হয়।

H (prio ১) M (prio ২) L (prio ৩) ব্লকড (মিউটেক্স L-এর কাছে) রান M রান করছে (L-কে প্রি-এম্পট করে) রান ক্রিটিক্যাল সেকশন (আবার) 0 2 4 6 8 10 12 রান করছে মিউটেক্স ব্লকড
ইনহেরিটেন্স ছাড়া: M (prio ২) বারবার L-কে প্রি-এম্পট করে, ফলে সর্বোচ্চ-প্রায়োরিটি H-কে ৬ টিক অপেক্ষা করতে হয় — যদিও H-এর প্রায়োরিটি M-এর চেয়ে বেশি।

৪ · প্রায়োরিটি ইনহেরিটেন্স — সমাধান

প্রায়োরিটি ইনহেরিটেন্সPriority Inheritanceএকটি উচ্চ-প্রায়োরিটি টাস্ক একটি মিউটেক্সের জন্য ব্লক হলে, মিউটেক্স-হোল্ডারের effective priority সাময়িকভাবে সেই উচ্চ প্রায়োরিটির সমান বাড়িয়ে দেওয়া — মুক্তি দেওয়ার পর আবার আগের প্রায়োরিটিতে ফিরিয়ে আনা হয়। এর সমাধান সহজ কিন্তু কার্যকর — যখন H, L-এর ধরে রাখা মিউটেক্সের জন্য ব্লক হয়, তখন L-এর effective priority সাময়িকভাবে H-এর প্রায়োরিটির সমান বাড়িয়ে দেওয়া হয়। ফলে এখন M (prio ২) আর L-কে (temporarily prio ১) প্রি-এম্পট করতে পারে না — কারণ L-এর effective priority এখন M-এর চেয়ে বেশি। L বিনা বাধায় দ্রুত তার ক্রিটিক্যাল সেকশন শেষ করে, মিউটেক্স রিলিজ করে, আর তখনই তার প্রায়োরিটি আবার আগের (নিম্ন) মানে ফিরিয়ে আনা হয় — এরপর H মিউটেক্স পেয়ে চলতে শুরু করে।

H (prio ১) M (prio ২) L (prio ৩) ব্লকড রান M রান M রান রান প্রায়োরিটি-ইনহেরিট (prio ১) 0 2 4 6 8 10 12 রান করছে মিউটেক্স ব্লকড
ইনহেরিটেন্স সহ: H ব্লক হওয়ার পরের টিক থেকেই L-এর প্রায়োরিটি বেড়ে যাওয়ায় M আর প্রি-এম্পট করতে পারে না — H মাত্র ৪ টিক ব্লকড থেকে ২ টিক আগেই সম্পন্ন হয়।

৫ · একটি সত্যিকারের সিমুলেশন — দুটো দৃশ্যপট পাশাপাশি

নিচের কোডে একটি Mutex ক্লাস (মালিকানা ও ওয়েটিং কিউসহ) এবং তিনটি টাস্ক — H, M, L — বাস্তবায়ন করা হয়েছে। একই সিমুলেশন দুইবার চালানো হয় — একবার প্রায়োরিটি ইনহেরিটেন্স ছাড়া, একবার সহ — যাতে ঠিক একই দৃশ্যপটে সমাধানটির প্রকৃত প্রভাব সরাসরি তুলনা করা যায়।

Python
class RTOSTask:
    def __init__(self, name, priority, ready_tick, segments):
        self.name = name
        self.base_priority = priority
        self.effective_priority = priority
        self.ready_tick = ready_tick
        self.segments = segments  # [[duration, kind], ...]; kind in {'non', 'crit'}
        self.seg_index = 0
        self.seg_remaining = segments[0][0]
        self.finished = False
        self.blocked = False

    def current_kind(self):
        return self.segments[self.seg_index][1]


class Mutex:
    def __init__(self):
        self.owner = None
        self.wait_queue = []


def make_tasks():
    # H-এর পুরো কাজটাই মিউটেক্স-সুরক্ষিত ক্রিটিক্যাল সেকশন (২ টিক)
    H = RTOSTask("High_H", priority=1, ready_tick=5, segments=[[2, 'crit']])
    # M মিউটেক্সের সাথে সম্পর্কহীন -- শুধু CPU দখল করে
    M = RTOSTask("Med_M",  priority=2, ready_tick=3, segments=[[5, 'non']])
    # L আগে থেকেই চলছে, তারপর মিউটেক্স অ্যাকোয়ার করে ক্রিটিক্যাল সেকশনে ঢোকে
    L = RTOSTask("Low_L",  priority=3, ready_tick=0, segments=[[2, 'non'], [4, 'crit'], [1, 'non']])
    return H, M, L


def run_simulation(use_inheritance, total_ticks=15):
    H, M, L = make_tasks()
    tasks = [H, M, L]
    mutex = Mutex()
    trace = []

    for tick in range(total_ticks):
        ready = [t for t in tasks if t.ready_tick <= tick and not t.finished and not t.blocked]
        ready.sort(key=lambda t: t.effective_priority)

        chosen = None
        events = []
        for t in ready:
            if t.current_kind() == 'crit' and mutex.owner not in (None, t):
                t.blocked = True
                mutex.wait_queue.append(t)
                events.append(f"{t.name} মিউটেক্স ব্লকড (হোল্ডার={mutex.owner.name})")
                if use_inheritance and mutex.owner.effective_priority > t.base_priority:
                    mutex.owner.effective_priority = t.base_priority
                    events.append(f"প্রায়োরিটি-ইনহেরিট: {mutex.owner.name}-এর effective priority "
                                  f"{t.base_priority}-এ উন্নীত হলো")
                continue
            chosen = t
            break

        if chosen is None:
            trace.append((tick, None, events))
            continue

        if chosen.current_kind() == 'crit' and mutex.owner is None:
            mutex.owner = chosen
            events.append(f"{chosen.name} মিউটেক্স অ্যাকোয়ার করলো")

        chosen.seg_remaining -= 1
        events.append(f"{chosen.name} রান করছে (effective_priority={chosen.effective_priority}, "
                       f"segment={chosen.current_kind()}, বাকি={chosen.seg_remaining})")

        if chosen.seg_remaining == 0:
            finished_kind = chosen.current_kind()
            if finished_kind == 'crit':
                mutex.owner = None
                if chosen.effective_priority != chosen.base_priority:
                    events.append(f"{chosen.name}-এর প্রায়োরিটি {chosen.base_priority}-এ ফিরিয়ে আনা হলো")
                chosen.effective_priority = chosen.base_priority
                for w in mutex.wait_queue:
                    w.blocked = False
                mutex.wait_queue = []
                events.append(f"{chosen.name} মিউটেক্স রিলিজ করলো")
            chosen.seg_index += 1
            if chosen.seg_index >= len(chosen.segments):
                chosen.finished = True
            else:
                chosen.seg_remaining = chosen.segments[chosen.seg_index][0]

        trace.append((tick, chosen.name, events))

    return trace, H


for label, use_inh in [("প্রায়োরিটি ইনহেরিটেন্স ছাড়া", False), ("প্রায়োরিটি ইনহেরিটেন্স সহ", True)]:
    print("=" * 60)
    print(label)
    print("=" * 60)
    trace, H = run_simulation(use_inh)
    h_first_block = None
    h_complete = None
    h_blocked_ticks = 0
    for tick, name, events in trace:
        if name is None:
            print(f"tick {tick:>2}: CPU আইডল")
        for e in events:
            print(f"tick {tick:>2}: {e}")
            if "High_H মিউটেক্স ব্লকড" in e and h_first_block is None:
                h_first_block = tick
            if e.startswith("High_H রান") and "বাকি=0" in e:
                h_complete = tick
        if h_first_block is not None and h_complete is None and name != "High_H":
            h_blocked_ticks += 1
    print(f"\nHigh_H প্রথম ব্লক হলো tick={h_first_block}, মোট ব্লকড ছিল {h_blocked_ticks} টিক, "
          f"সম্পন্ন হলো tick={h_complete}\n")

    
দুটো রানের ফলাফল সরাসরি তুলনা করুন — ইনহেরিটেন্স ছাড়া, High_H tick ৫-এ ব্লক হয়ে ৬ টিক অপেক্ষা করে tick ১২-এ সম্পন্ন হয় (কারণ Med_M ঠিক এই সময়ে Low_L-কে বারবার প্রি-এম্পট করে রাখে)। ইনহেরিটেন্স সহ ঠিক একই দৃশ্যপটে, Low_L ব্লক হওয়ার সাথে সাথেই প্রায়োরিটি বেড়ে যাওয়ায় Med_M আর তাকে প্রি-এম্পট করতে পারে না — High_H মাত্র ৪ টিক অপেক্ষা করে tick ১০-এই সম্পন্ন হয়, অর্থাৎ ২ টিক আগে। এটাই প্রমাণ করে — প্রায়োরিটি ইনহেরিটেন্স শুধু তাত্ত্বিক ধারণা নয়, এটি সত্যিই H-এর ওয়েট টাইম কমায়, ঠিক যে সমস্যাটা (M-এর অপ্রাসঙ্গিক হস্তক্ষেপ) সমাধান করার কথা সেটাই করে দেখাচ্ছে।

৬ · বাস্তব উদাহরণ — Mars Pathfinder (১৯৯৭)

ঐতিহাসিক ঘটনা

প্রায়োরিটি ইনভার্সন কোনো নিছক তাত্ত্বিক সমস্যা নয় — ১৯৯৭ সালে NASA-র Mars Pathfinder মিশনে মঙ্গল গ্রহে অবতরণের পর রোভারটি বারবার অপ্রত্যাশিত ওয়াচডগ রিসেটের শিকার হচ্ছিল। তদন্তে দেখা যায় এর পেছনে ছিল ঠিক এই ধরনের একটি প্রায়োরিটি ইনভার্সন বাগ — একটি নিম্ন-প্রায়োরিটি টাস্ক একটি শেয়ার্ড ডেটা-বাসের মিউটেক্স ধরে রাখা অবস্থায় একটি মধ্যম-প্রায়োরিটি টাস্কের কাছে বারবার প্রি-এম্পটেড হচ্ছিল, ফলে একটি উচ্চ-প্রায়োরিটি টাস্ক দীর্ঘসময় ব্লক থেকে ওয়াচডগ টাইমার ট্রিগার করে ফেলছিল (ওয়াচডগ টাইমার নিয়ে বিস্তারিত M8/L33-এ)। NASA-র ইঞ্জিনিয়াররা মঙ্গল গ্রহে থাকা সফটওয়্যারে দূর থেকে প্রায়োরিটি ইনহেরিটেন্স সক্রিয় করে সমস্যাটি সমাধান করেন — এটি এমবেডেড/RTOS ইতিহাসের সবচেয়ে বিখ্যাত বাস্তব উদাহরণগুলোর একটি হয়ে আছে।

মূল কথা · Key takeaway

মিউটেক্সের "মালিকানা" থাকার কারণে একটি নিম্ন-প্রায়োরিটি টাস্ক অসাবধানে একটি উচ্চ-প্রায়োরিটি টাস্ককে পরোক্ষভাবে দীর্ঘসময় ব্লক করে রাখতে পারে, যদি মাঝে একটি সম্পর্কহীন মধ্যম-প্রায়োরিটি টাস্ক তাকে প্রি-এম্পট করতে থাকে — এটাই প্রায়োরিটি ইনভার্সন। প্রায়োরিটি ইনহেরিটেন্স — মিউটেক্স-হোল্ডারের প্রায়োরিটি সাময়িকভাবে বাড়িয়ে দেওয়া — এই সমস্যার একটি প্রমাণিত, ব্যাপকভাবে ব্যবহৃত সমাধান, যা FreeRTOS/Zephyর-এর মতো বাস্তব RTOS-এও বিল্ট-ইন হিসেবে পাওয়া যায়।

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

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

প্র ০১ যদি উপরের সিমুলেশনে Med_M টাস্কটিই না থাকতো, তাহলে কি প্রায়োরিটি ইনভার্সন সমস্যা দেখা দিত?

না, উল্লেখযোগ্যভাবে নয়। Med_M ছাড়া, High_H ব্লক হয়ে শুধু Low_L-এর ক্রিটিক্যাল সেকশন শেষ হওয়া পর্যন্তই অপেক্ষা করত (একটি স্বল্প, বাউন্ডেড সময়) — এটাই স্বাভাবিক ও প্রত্যাশিত ব্লকিং। সমস্যাটা তৈরি হয় ঠিক তখনই যখন একটি মিউটেক্সের সাথে সম্পর্কহীন কিন্তু মধ্যম-প্রায়োরিটির টাস্ক (M) এসে L-কে বারবার প্রি-এম্পট করে, L-এর ক্রিটিক্যাল সেকশন শেষ হতে অস্বাভাবিকভাবে দেরি করিয়ে দেয়।

প্র ০২ প্রায়োরিটি ইনহেরিটেন্স সিমুলেশনে Med_M tick ৫-এ কেন এক টিকের জন্য রান করে, অথচ Low_L-এর প্রায়োরিটি তখনই বেড়ে গিয়েছিল?

কারণ প্রতিটি টিকের শুরুতে রেডি টাস্কগুলো সেই টিক শুরুর সময়ের effective priority অনুযায়ী সাজানো হয় — tick ৫-এ যখন High_H ব্লক হয় ও ইনহেরিট ট্রিগার হয়, ততক্ষণে সেই টিকের জন্য "কে চলবে" এর সিদ্ধান্ত ইতিমধ্যে Med_M-এর পক্ষে নেওয়া হয়ে গেছে (আগের priority অনুযায়ী সাজানো তালিকায় Med_M-ই তখন এগিয়ে ছিল)। বুস্টটি tick ৬ থেকে কার্যকর হয় — এরপর থেকেই Low_L আর প্রি-এম্পটেড হয় না। এটি একটি discrete-tick সিমুলেশনের বাস্তবসম্মত বিস্তারিত আচরণ, মূল সিদ্ধান্তের দিক পরিবর্তন করে না।

প্র ০৩ মিউটেক্সের বদলে যদি একটি সাধারণ সেমাফোর (N>1) ব্যবহার করা হতো, প্রায়োরিটি ইনহেরিটেন্স কি একইভাবে প্রয়োগ করা সহজ হতো?

না, তুলনামূলক কঠিন। প্রায়োরিটি ইনহেরিটেন্সের জন্য জানা দরকার ঠিক কোন টাস্কটি রিসোর্স ধরে আছে, যাতে তারই প্রায়োরিটি বাড়ানো যায় — মিউটেক্সে এটা স্পষ্ট (একজনই মালিক)। কিন্তু N>1 সেমাফোরে একসাথে একাধিক টাস্ক রিসোর্স ধরে থাকতে পারে, তাই "কার প্রায়োরিটি বাড়ানো উচিত" প্রশ্নটি অস্পষ্ট হয়ে যায় — এই কারণেই প্রায়োরিটি ইনহেরিটেন্স সাধারণত মিউটেক্সের (N=1, স্পষ্ট মালিকানাসহ) সাথেই ব্যবহৃত হয়।

অনুশীলন

  1. চিন্তা করুন: উপরের কোড সেলে Med_M-এর segments যদি [[5, 'non']]-এর বদলে [[10, 'non']] করা হয় (আরও দীর্ঘ কাজ), ইনহেরিটেন্স ছাড়া দৃশ্যপটে High_H-এর অপেক্ষার সময় কীভাবে বদলাবে?

    ইনহেরিটেন্স ছাড়া দৃশ্যপটে Med_M আরও বেশি সময় ধরে Low_L-কে প্রি-এম্পট করে রাখবে, ফলে Low_L-এর ক্রিটিক্যাল সেকশন শেষ হতে আরও দেরি হবে, আর তাই High_H-এর অপেক্ষার সময়ও (blocked ticks) বাড়বে — প্রায়োরিটি ইনভার্সনের প্রভাব আরও প্রকট হয়ে দেখা যাবে। ইনহেরিটেন্স সহ দৃশ্যপটে অবশ্য কোনো পরিবর্তন হবে না, কারণ সেখানে Med_M মোটেও Low_L-কে প্রি-এম্পট করতে পারে না।

  2. পরীক্ষা করুন: কোড সেলে High_H-এর ready_tick ৫-এর বদলে ৩ করে Run চেপে দেখুন — ইনহেরিটেন্স ছাড়া ও সহ, দুই দৃশ্যপটেই High_H কখন প্রথম ব্লক হয় তা কীভাবে বদলায়।

    tick ৩-এ Low_L তখনও তার non-critical অংশে থাকবে (মিউটেক্স তখনও অ্যাকোয়ার করেনি, সেটা tick ২-এ শুরু হয়ে tick ৩-এও চলমান হতে পারে নির্ভর করে ঠিক কোন টিকে অ্যাকোয়ার হয়) — তাই High_H হয়তো একদমই ব্লক না হয়ে সরাসরি মিউটেক্স অ্যাকোয়ার করে ফেলতে পারে, নির্ভর করছে ঠিক কোন টিকে সে রেডি হচ্ছে তার উপর। এই পরিবর্তনটি নিজে Run করে সঠিক ফলাফলটি পর্যবেক্ষণ করাই সবচেয়ে ভালো উপায়।

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

আগের পাঠ
RTOS ফান্ডামেন্টাল — টাস্ক ও শিডিউলিং