পাঠ ৩৬ · ৫৭-এর মধ্যে · মডিউল ৭
Home / Courses / Computer Architecture & Digital Logic / এক্সসেপশন ও ইন্টারাপ্ট

এক্সসেপশন ও ইন্টারাপ্ট হ্যান্ডলিং ইন পাইপলাইন

Exceptions & interrupt handling in pipeline
৮ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • এক্সসেপশন (synchronous) ও ইন্টারাপ্ট (asynchronous)-এর মধ্যে সুনির্দিষ্ট পার্থক্য
  • পাইপলাইনিং কেন এক্সসেপশন হ্যান্ডলিংকে জটিল করে তোলে — কোন ইনস্ট্রাকশন রাখতে হবে, কোনটা বাদ দিতে হবে
  • precise বনাম imprecise এক্সসেপশন হ্যান্ডলিং-এর সংক্ষিপ্ত পরিচিতি
  • কোড দিয়ে একটি division-by-zero এক্সসেপশনের সঠিক ফ্লাশ আচরণ সিমুলেট ও যাচাই করা

১ · এক্সসেপশন বনাম ইন্টারাপ্ট

এই দুটি টার্ম প্রায়ই গুলিয়ে ফেলা হয়, কিন্তু এদের উৎস আলাদা —

এক্সসেপশন (Synchronous)
বর্তমানে এক্সিকিউট হওয়া নির্দিষ্ট ইনস্ট্রাকশনের কারণে সরাসরি ঘটে — যেমন L21-এর division-by-zero, একটি অবৈধ অপকোড, বা মেমরি-প্রোটেকশন লঙ্ঘন। একই প্রোগ্রাম একই ইনপুটে চালালে সবসময় একই জায়গায় ঘটবে।
ইন্টারাপ্ট (Asynchronous)
একটি বাহ্যিক ইভেন্টের কারণে ঘটে, বর্তমান ইনস্ট্রাকশনের সাথে সরাসরি সম্পর্কহীন — যেমন কীবোর্ড প্রেস বা টাইমার এক্সপায়ার (M10/L48-এ হার্ডওয়্যার দৃষ্টিকোণ থেকে বিস্তারিত)। প্রায় যেকোনো সময় ঘটতে পারে।

মিল একটি জায়গায় — উভয় ক্ষেত্রেই পাইপলাইনকে স্বাভাবিক এক্সিকিউশন থামিয়ে বিশেষ হ্যান্ডলার কোডে নিয়ন্ত্রণ স্থানান্তর করতে হয়।

২ · পাইপলাইনিং কেন এক্সসেপশন হ্যান্ডলিংকে জটিল করে

একটি নন-পাইপলাইনড CPU-তে, একটি ইনস্ট্রাকশন সম্পূর্ণ শেষ হওয়ার আগে পরেরটি শুরুই হয় না — তাই এক্সসেপশন ঘটলে পরিষ্কার: শুধু বর্তমান ইনস্ট্রাকশনটাই থামাতে হবে। কিন্তু পাইপলাইনড CPU-তে, যে মুহূর্তে একটি ইনস্ট্রাকশনের Execute স্টেজে এক্সসেপশন সনাক্ত হয়, তার পরে প্রোগ্রাম-অর্ডারে ফেচ করা আরও কয়েকটি ইনস্ট্রাকশন তখনও Decode বা Fetch স্টেজে চলমান থাকতে পারে — অথচ এক্সসেপ্ট করা ইনস্ট্রাকশনটি নিজেই তখনও সম্পূর্ণ হয়নি। সঠিক হ্যান্ডলিংয়ের জন্য প্রয়োজন —

  • এক্সসেপ্ট করা ইনস্ট্রাকশনের পরের সব ইন-ফ্লাইট ইনস্ট্রাকশন ফ্লাশ করা (L35-এর ফ্লাশ ধারণার পুনর্ব্যবহার, ভিন্ন কারণে)।
  • ঠিক কোন ইনস্ট্রাকশন এক্সসেপ্ট করেছে তা রেকর্ড রাখা, যাতে পরে সম্ভব হলে সেখান থেকে পুনরায় শুরু করা যায়।
  • প্রোগ্রাম-অর্ডার বজায় রাখা — এক্সসেপ্ট করা ইনস্ট্রাকশনের আগের ইনস্ট্রাকশনগুলো স্বাভাবিকভাবে সম্পন্ন হতে দেওয়া (precise exception handling; পাইপলাইনের ভেতরের প্রসেসিং অর্ডার প্রোগ্রাম-অর্ডারের থেকে সামান্য ভিন্ন হলেও, এই নিয়ম মানতে হবে — imprecise handling একটি বাস্তব, কঠিন হার্ডওয়্যার-ডিজাইন চ্যালেঞ্জ, সংক্ষিপ্ত উল্লেখযোগ্য)।

৩ · কোড দিয়ে যাচাই — division-by-zero এক্সসেপশন

নিচের কোড সেলে ৫টি ইনস্ট্রাকশনের একটি সিকোয়েন্স সিমুলেট করা হয়েছে, যার তৃতীয়টি একটি division-by-zero ঘটায় তার Execute স্টেজে। কোড নিজে থেকেই গণনা করে ঠিক সাইকেল ৫-এ কোন ইনস্ট্রাকশন কোন স্টেজে আছে, তারপর সঠিক সেট ইনস্ট্রাকশন ফ্লাশ করে দেখায়।

Python
# পাইপলাইনে এক্সসেপশন ফ্লাশ -- শুধু পরের ইন-ফ্লাইট ইনস্ট্রাকশন বাদ, আগেরগুলো অক্ষত
IF, ID, EX, MEM, WB = "IF", "ID", "EX", "MEM", "WB"
STAGES = [IF, ID, EX, MEM, WB]

def stage_at_cycle(position, cycle):
    """position নম্বর ইনস্ট্রাকশন (IF@position সাইকেলে শুরু) নির্দিষ্ট cycle-এ কোন স্টেজে আছে -- না থাকলে None"""
    idx = cycle - position
    if 0 <= idx < len(STAGES):
        return STAGES[idx]
    return None

def detect_exception(instruction):
    """এই ইনস্ট্রাকশন এক্সসেপশন ঘটাবে কিনা -- এখানে division-by-zero সিমুলেট করা হলো"""
    return instruction["op"] == "DIV" and instruction.get("divisor") == 0

instructions = [
    {"id": 1, "op": "ADD"},
    {"id": 2, "op": "LOAD"},
    {"id": 3, "op": "DIV", "divisor": 0},   # এই ইনস্ট্রাকশনটি এক্সসেপশন ঘটাবে
    {"id": 4, "op": "ADD"},
    {"id": 5, "op": "SUB"},
]

# ধাপ ১: এক্সসেপশন সনাক্তকরণ -- এটি ঘটে excepting instruction-এর নিজের EX স্টেজে
exception_position = None
exception_cycle = None
for instr in instructions:
    if detect_exception(instr):
        exception_position = instr["id"]
        exception_cycle = instr["id"] + STAGES.index(EX)  # position-এর EX স্টেজের সাইকেল
        break

print(f"এক্সসেপশন সনাক্ত: ইনস্ট্রাকশন #{exception_position} "
      f"({instructions[exception_position - 1]['op']}) এর EX স্টেজে, সাইকেল {exception_cycle}\n")

# ধাপ ২: সাইকেল exception_cycle-এ ফ্লাশের ঠিক আগের পাইপলাইন স্টেট
print(f"--- সাইকেল {exception_cycle}-এ (ফ্লাশের আগে) পাইপলাইন স্টেট ---")
before_flush = {}
for instr in instructions:
    stage = stage_at_cycle(instr["id"], exception_cycle)
    if stage:
        before_flush[instr["id"]] = stage
        print(f"  ইনস্ট্রাকশন #{instr['id']} ({instr['op']}): {stage} স্টেজে")

# ধাপ ৩: ফ্লাশ প্রয়োগ -- এক্সসেপ্ট করা ইনস্ট্রাকশনের আগেরগুলো স্বাভাবিক, নিজেরটা হ্যান্ডলারে, পরেরগুলো ফ্লাশ
print(f"\n--- ফ্লাশ প্রয়োগ (এক্সসেপ্টিং ইনস্ট্রাকশন #{exception_position}) ---")
after_flush = {}
for position, stage in before_flush.items():
    if position < exception_position:
        after_flush[position] = stage
        print(f"  ইনস্ট্রাকশন #{position}: স্বাভাবিকভাবে সম্পন্ন হবে ({stage})")
    elif position == exception_position:
        after_flush[position] = "EXCEPTION"
        print(f"  ইনস্ট্রাকশন #{position}: এক্সসেপশন হ্যান্ডলারে নিয়ন্ত্রণ স্থানান্তরিত "
              f"({stage} স্টেজে সনাক্ত, স্বাভাবিক WB হবে না)")
    else:
        print(f"  ইনস্ট্রাকশন #{position}: ফ্লাশ করা হলো ({stage} স্টেজ থেকে বাতিল, প্রোগ্রাম-অর্ডারে পরে আসা)")

kept = set(after_flush.keys())
flushed = set(before_flush.keys()) - kept
print(f"\nনিশ্চিত হলো: রাখা হয়েছে {sorted(kept)}, ফ্লাশ হয়েছে {sorted(flushed)}")

assert flushed == {4, 5}, "শুধু এক্সসেপ্টিং ইনস্ট্রাকশনের পরেরগুলোই ফ্লাশ হওয়ার কথা"
assert kept == {1, 2, 3}, "এক্সসেপ্টিং ইনস্ট্রাকশন ও তার আগেরগুলো রক্ষা পাওয়ার কথা"
print("যাচাই সম্পন্ন: শুধু সঠিক ইনস্ট্রাকশন সেট ফ্লাশ হয়েছে।")

    
লক্ষ্য করুন — সাইকেল ৫-এ ইনস্ট্রাকশন #১ ইতিমধ্যে WB স্টেজে (প্রায় সম্পন্ন), #২ MEM-এ, দুটোই এক্সসেপ্টিং ইনস্ট্রাকশন #৩-এর আগে প্রোগ্রাম-অর্ডারে, তাই স্বাভাবিকভাবে চলতে দেওয়া হয়। কিন্তু #৪ (ID-তে) ও #৫ (IF-এ) #৩-এর পরে, তাই ফ্লাশ হয় — এমনকি তারা কোনো ভুল করেনি তা সত্ত্বেও, কারণ প্রোগ্রাম-অর্ডার অনুযায়ী এক্সসেপশন হ্যান্ডলার শেষ হওয়ার আগে তাদের এক্সিকিউট করা উচিত নয়।
মূল কথা · Key takeaway

এক্সসেপশন ও ইন্টারাপ্ট হ্যান্ডলিং আরেকটি জায়গা যেখানে পাইপলাইনিংয়ের ওভারল্যাপ-করা কাঠামো বাড়তি জটিলতা আনে — L33-এর স্ট্রাকচারাল হ্যাজার্ড, L34-এর ডেটা হ্যাজার্ড, L35-এর কন্ট্রোল হ্যাজার্ডের মতোই, এখানেও মূল চ্যালেঞ্জ হলো "কোন ইনস্ট্রাকশন সত্যিই সম্পন্ন হওয়ার যোগ্য" তা সঠিকভাবে নির্ধারণ করা। M7-এর এই পাঁচটি পাঠ মিলিয়ে দেখায় — বাস্তব পাইপলাইনড CPU শুধু L32-এর আদর্শ K+N-1 টাইমিং অর্জন করে না, বরং প্রতিটি ধরনের ব্যতিক্রম পরিস্থিতিতেও প্রোগ্রামের সঠিকতা বজায় রাখার জন্য সতর্কভাবে ডিজাইন করা হয়।

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

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

প্র ০১ উপরের কোডে ইনস্ট্রাকশন #৩ (এক্সসেপ্টিং) নিজেকে "রাখা" (kept) সেটে অন্তর্ভুক্ত করা হয়েছে, কিন্তু "স্বাভাবিকভাবে সম্পন্ন হবে" বলা হয়নি — কেন এই পার্থক্য গুরুত্বপূর্ণ?

কারণ #৩ আসলে স্বাভাবিকভাবে সম্পন্ন হয় না — এর ফলাফল ফেলে দেওয়া হয় এবং নিয়ন্ত্রণ এক্সসেপশন হ্যান্ডলারে স্থানান্তরিত হয় (তাই কোডে আলাদাভাবে "EXCEPTION" চিহ্নিত করা হয়েছে, কোনো স্বাভাবিক স্টেজ নাম নয়)। এটি "ফ্লাশ হওয়া" (#৪, #৫) থেকেও আলাদা — #৩ ইতিমধ্যে তার কাজ (এক্সসেপশন সনাক্তকরণ) সম্পন্ন করেছে এবং হার্ডওয়্যারের এটি মনে রাখা দরকার ঠিক কোন ইনস্ট্রাকশন এক্সসেপ্ট করেছে, যাতে হ্যান্ডলার সঠিক প্রোগ্রাম কাউন্টার (PC) দিয়ে পরবর্তীতে সিদ্ধান্ত নিতে পারে।

প্র ০২ যদি একটি ইন্টারাপ্ট (যেমন টাইমার) ঠিক একই সাইকেলে ঘটে যখন ইনস্ট্রাকশন #৩ এক্সসেপশন ঘটাচ্ছে, হার্ডওয়্যার কীভাবে সিদ্ধান্ত নেয় কোনটা আগে হ্যান্ডল করবে?

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

প্র ০৩ উপরের কোডে যদি এক্সসেপশন ঘটানো ইনস্ট্রাকশনটি #১ (প্রথম ইনস্ট্রাকশন) হতো, ফ্লাশ সেটটি কেমন দেখাত?

তাহলে exception_position=1, আর "রাখা" সেটে শুধু #১ (নিজেই, EXCEPTION হিসেবে চিহ্নিত) থাকত — কোনো আগের ইনস্ট্রাকশন নেই যা স্বাভাবিকভাবে সম্পন্ন হবে, কারণ #১ নিজেই প্রথম। আর #২ থেকে #৫ পর্যন্ত সব ইন-ফ্লাইট ইনস্ট্রাকশন ফ্লাশ হতো — একটি চরম কিন্তু সঠিক কেস, যা দেখায় প্রোগ্রামের একেবারে শুরুর ইনস্ট্রাকশনে এক্সসেপশন ঘটলেও একই ফ্লাশ-লজিক নির্ভরযোগ্যভাবে কাজ করে।

অনুশীলন

  1. চিন্তা করুন: উপরের কোডে instructions-এ যদি দুইটি ইনস্ট্রাকশন এক্সসেপশন ঘটানোর যোগ্য হতো (যেমন #৩ ও #৫ দুটোই DIV-by-zero), কোডের break স্টেটমেন্টের কারণে ফলাফল কী হতো?

    break-এর কারণে লুপ প্রথম মিলে যাওয়া এক্সসেপশনেই (প্রোগ্রাম-অর্ডারে প্রথম, অর্থাৎ #৩) থেমে যাবে, #৫-কে সম্পূর্ণ উপেক্ষা করে। এটি আসলে সঠিক আচরণ — যেহেতু #৩ প্রোগ্রাম-অর্ডারে আগে, তাকেই প্রথমে হ্যান্ডল করতে হবে এবং #৪, #৫ ফ্লাশ হয়ে যাবে যেভাবেই হোক (তারা এক্সসেপ্ট করত কিনা তা নিয়ে ভাবার সুযোগই আসত না) — এটিই precise exception handling-এর একটি মূল নীতি: শুধু প্রোগ্রাম-অর্ডারে সবচেয়ে আগের এক্সসেপশনটাই গণনা করা হয়।

  2. পরীক্ষা করুন: উপরের কোড সেলে {"id": 3, "op": "DIV", "divisor": 0}-এর বদলে {"id": 3, "op": "DIV", "divisor": 2} (বৈধ ভাজক) করে Run চাপুন — কী ঘটে?

    detect_exception এখন কোনো ইনস্ট্রাকশনের জন্যই True রিটার্ন করবে না (যেহেতু divisor != 0), তাই লুপ শেষ পর্যন্ত চলে যাবে এবং exception_position ও exception_cycle উভয়ই None থেকে যাবে। পরের কোড এই None মান নিয়ে ভুলভাবে চলার চেষ্টা করবে (একটি প্রকৃত এরর দেখাবে) — এটি স্পষ্টভাবে দেখায় বাস্তব হার্ডওয়্যারেও detect_exception-এর মতো একটি চেক অবশ্যই প্রতিটি ইনস্ট্রাকশনের জন্য চালাতে হয়, ধরে নেওয়া যায় না যে কোনো একটি অবশ্যই এক্সসেপ্ট করবে।

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

পূর্ববর্তী পাঠ
কন্ট্রোল হ্যাজার্ড ও ব্রাঞ্চ প্রেডিকশন