পাঠ ০৮ · ৫৬-এর মধ্যে · মডিউল ২

ইন্টার-প্রসেস কমিউনিকেশন (IPC)

Inter-process communication — shared memory & message passing
৮ মিনিট পড়া শুরু · Beginner Python কোডসহ সম্পূর্ণ বাংলায়

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

  • Shared memory মডেল — কী, কেন দ্রুত, এবং কেন কোঅর্ডিনেশন দরকার
  • Message passing মডেল — কী, কেন নিরাপদ, এবং কেন ধীর
  • দুই মডেলের সরাসরি তুলনা এবং কোনটি কখন উপযোগী
  • Python দিয়ে দুটি সিমুলেশন — একটি আনপ্রোটেক্টেড শেয়ার্ড কাউন্টারে race condition, আর একটি deque-ভিত্তিক মেইলবক্স

১ · দুটি মৌলিক IPC মডেল

Shared MemoryShared memory IPCOS একটি মেমরি অঞ্চল একাধিক প্রসেসের অ্যাড্রেস স্পেসে ম্যাপ করে দেয়; প্রসেসগুলো সরাসরি সেই অঞ্চলে read/write করে যোগাযোগ করে।
OS একটি মেমরি অঞ্চল একাধিক প্রসেসের কাছে ম্যাপ করে দেয়; সেটআপের পর প্রসেসগুলো সরাসরি সেই মেমরিতে read/write করে — কোনো OS ইনভলভমেন্ট ছাড়াই, তাই দ্রুত। কিন্তু কোঅর্ডিনেশনের দায়িত্ব প্রসেসদের নিজেদের।
Message PassingMessage passing IPCপ্রসেসরা OS-প্রদত্ত send()/receive() প্রিমিটিভ দিয়ে discrete বার্তা আদান-প্রদান করে; OS প্রতিটি ট্রান্সফার সরাসরি সামলায়।
প্রসেসরা OS-এর send()/receive() কল দিয়ে discrete বার্তা আদান-প্রদান করে; OS প্রতিটি ট্রান্সফার সামলায় — সঠিকতা যুক্তি করা সহজ (কোনো shared-state race নেই), কিন্তু প্রতি বার্তায় OS ইনভলভমেন্টের কারণে ধীর। মাইক্রোকার্নেল ডিজাইনে (L04) এই মডেলই প্রধান।

২ · শেয়ার্ড মেমরির ঝুঁকি — race condition

Shared memory-র সমস্যা হলো — OS শুধু মেমরি অঞ্চলটি সেটআপ করে দেয়, কিন্তু কোন প্রসেস কখন সেটি পড়বে/লিখবে তা নিয়ন্ত্রণ করে না। যদি দুটি প্রসেস কোনো কোঅর্ডিনেশন ছাড়াই একই শেয়ার্ড ডেটা একসাথে পড়তে/লিখতে যায়, ফলাফল নির্ভর করবে তাদের এক্সিকিউশনের অনির্দেশ্য টাইমিং/ইন্টারলিভিং-এর উপর — এটিই race condition, এবং এটি M5 (প্রসেস সিনক্রোনাইজেশন)-এর কেন্দ্রীয় সমস্যা।

৩ · সিমুলেশন — শেয়ার্ড মেমরি রেস কন্ডিশন বনাম মেসেজ পাসিং

নিচের কোড সেলে প্রথমে একটি আনপ্রোটেক্টেড শেয়ার্ড কাউন্টার দেখানো হয়েছে — দুটি "প্রসেস" কোনো লক ছাড়াই কাউন্টার +1 করতে চায়, কিন্তু increment আসলে তিনটি সাব-স্টেপ (read, add, write) — ম্যানুয়ালি এই সাব-স্টেপগুলো ইন্টারলিভ করে দেখানো হয়েছে একটি আপডেট কীভাবে হারিয়ে যায়। এরপর একটি collections.deque-ভিত্তিক মেইলবক্স দিয়ে message passing দেখানো হয়েছে।

Python
# সম্পূর্ণ ইন-মেমরি সিমুলেশন -- বাস্তব থ্রেড/প্রসেস বা OS shared-memory কল নয়

shared = {"counter": 0}

def read_counter(mem):
    return mem["counter"]

def write_counter(mem, value):
    mem["counter"] = value

print("--- Shared memory: দুটি প্রসেস কোনো কোঅর্ডিনেশন ছাড়াই counter += 1 করতে চায় ---")
# increment আসলে ৩টি সাব-স্টেপ: read -> add -> write। এখানে ম্যানুয়ালি ইন্টারলিভ করা হলো:
val_a = read_counter(shared)        # প্রসেস A পড়লো
val_b = read_counter(shared)        # প্রসেস B পড়লো (A এখনো লিখেনি!)
print(f"A পড়লো: {val_a}  |  B পড়লো: {val_b} (দুজনেই একই পুরনো মান পেলো)")
val_a += 1                          # A হিসাব করলো
val_b += 1                          # B হিসাব করলো
write_counter(shared, val_a)        # A লিখলো
print(f"A লিখলো: {val_a}  ->  counter = {shared['counter']}")
write_counter(shared, val_b)        # B লিখলো -- A-এর আপডেট মুছে গেলো!
print(f"B লিখলো: {val_b}  ->  counter = {shared['counter']}  (A-এর আপডেট হারিয়ে গেলো!)")

print("\nপ্রত্যাশিত মোট (দুইবার +1) =", 2)
print("প্রকৃত counter =", shared["counter"])
print("রেস কন্ডিশনের কারণে একটি আপডেট হারিয়ে গেলো?", shared["counter"] < 2)

print("\n--- Message passing: deque-ভিত্তিক মেইলবক্স দিয়ে send()/receive() ---")
from collections import deque
mailbox = deque()

def send(mb, msg):
    mb.append(msg)
    print(f"send(): '{msg}' পাঠানো হলো, মেইলবক্সে এখন {len(mb)}টি বার্তা")

def receive(mb):
    if not mb:
        print("receive(): মেইলবক্স খালি, কিছু নেই")
        return None
    msg = mb.popleft()
    print(f"receive(): '{msg}' গ্রহণ করা হলো, মেইলবক্সে এখন {len(mb)}টি বার্তা বাকি")
    return msg

send(mailbox, "প্রসেস A থেকে: হ্যালো")
send(mailbox, "প্রসেস A থেকে: ডেটা রেডি")
receive(mailbox)
receive(mailbox)
receive(mailbox)

print("\n--- তুলনা ---")
print("Shared memory: দ্রুত, কিন্তু M5 (সিনক্রোনাইজেশন) ছাড়া উপরের মতো race condition ঘটে")
print("Message passing: প্রতি বার্তায় OS জড়িত থাকায় ধীর, কিন্তু কোনো shared state নেই বলেই নিরাপদ")

    
লক্ষ্য করুন — উভয় প্রসেসের increment সফলভাবে "সম্পন্ন" হয়েছে বলে মনে হলেও চূড়ান্ত counter মান প্রত্যাশিত ২-এর বদলে ১ — কারণ B যখন পড়েছিল তখনো A-এর লেখা মান মেমরিতে বসেনি। এটিই lost update নামক ক্লাসিক race condition। মেসেজ পাসিং-এ এই সমস্যা একেবারেই ঘটে না, কারণ কোনো shared mutable state-ই নেই — প্রতিটি বার্তা মেইলবক্সে একটি atomic যোগ/অপসারণ অপারেশন।
মূল কথা · Key takeaway

Shared memory ও message passing একে অপরের বিপরীত trade-off — গতি বনাম নিরাপত্তা। বাস্তব সিস্টেমে দুটোই ব্যবহৃত হয়: performance-critical জায়গায় shared memory (সাথে M5-এর সিনক্রোনাইজেশন টুল), আর simplicity/ isolation গুরুত্বপূর্ণ জায়গায় (যেমন মাইক্রোকার্নেল, L04) message passing।

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

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

প্র ০১ যদি প্রসেস A এবং B-এর সাব-স্টেপগুলো এমনভাবে ইন্টারলিভ হতো যে A সম্পূর্ণ read-add-write শেষ করার পরই B শুরু করতো, তাহলে কী হতো?

তাহলে কোনো race condition ঘটতো না — B যখন read করতো, ততক্ষণে A ইতিমধ্যে তার নতুন মান (1) লিখে ফেলেছে, তাই B সেই 1 পড়ে তাতে +1 করে 2 লিখতো, যা প্রত্যাশিত ফলাফল। এটিই দেখায় race condition কোনো নিশ্চিত ফলাফল নয় — এটি টাইমিং-নির্ভর এবং অনির্দেশ্য, যা একে ডিবাগ করা এত কঠিন করে তোলে।

প্র ০২ মেসেজ পাসিং মডেলে কি কখনো race condition ঘটতে পারে?

মেইলবক্সের নিজের অপারেশন (append/popleft) OS/রানটাইম দ্বারা atomic হিসেবে সামলানো হয় বলে ধরে নেওয়া হয়, তাই মূল IPC মেকানিজমে race থাকে না। তবে যদি একাধিক প্রসেস বার্তার ভেতরের কোনো shared রিসোর্স নিয়ে আলাদাভাবে সিদ্ধান্ত নেয় (যেমন দুটি প্রসেস একই বার্তা দেখে একই কাজ দুইবার করে ফেলা), সেটি একটি ভিন্ন ধরনের কোঅর্ডিনেশন বাগ — কিন্তু ক্লাসিক shared-memory lost-update সমস্যাটি নয়।

প্র ০৩ মাইক্রোকার্নেল ডিজাইন (L04) কেন সাধারণত message passing-কে প্রাধান্য দেয়, monolithic kernel-এর তুলনায়?

মাইক্রোকার্নেলে ফাইল সিস্টেম, ড্রাইভার ইত্যাদি আলাদা user-mode প্রসেস হিসেবে চলে (L04) — তারা একে অপরের মেমরিতে সরাসরি অ্যাক্সেস পায় না (fault isolation বজায় রাখতে)। তাই তাদের যোগাযোগের জন্য OS-নিয়ন্ত্রিত message passing স্বাভাবিক পছন্দ — এটি ধীর হলেও একটি component ক্র্যাশ করলে বাকিদের নিরাপদ রাখে, যা মাইক্রোকার্নেলের মূল লক্ষ্য।

অনুশীলন

  1. চিন্তা করুন: দুই ধরনের বাস্তব অ্যাপ্লিকেশনের কথা ভাবুন — একটি যেখানে shared memory উপযোগী হবে (গতির জন্য) এবং একটি যেখানে message passing উপযোগী হবে (নিরাপত্তা/সরলতার জন্য)।

    উদাহরণ: একটি ভিডিও এডিটিং সফটওয়্যার যেখানে একাধিক থ্রেড/প্রসেস একই বড় ফ্রেম বাফারে কাজ করে — shared memory (কপি করার খরচ বাঁচাতে, দ্রুততা জরুরি)। অন্যদিকে, একটি ব্রাউজারের ট্যাব-প্রসেস ও মূল প্রসেসের মধ্যে যোগাযোগ — message passing (একটি ট্যাব ক্র্যাশ করলেও ব্রাউজার বা অন্য ট্যাব যেন নিরাপদ থাকে)।

  2. পরীক্ষা করুন: উপরের কোড সেলে write_counter(shared, val_a) এবং write_counter(shared, val_b)-এর ক্রম উল্টে দিন (B আগে লিখুক)। চূড়ান্ত counter মান কী হবে, এবং কেন এখনো race condition-ই থাকবে?

    ক্রম উল্টালেও চূড়ান্ত মান এখনো 1 থাকবে (এবার A-এর লেখা মান শেষে থাকবে, B-এরটা হারাবে) — সমস্যাটি কোন প্রসেস শেষে লিখলো তা নয়, বরং উভয়েই একই পুরনো মান (0) পড়ে ফেলেছিল লেখার আগে। এটিই দেখায় সমাধান ক্রম পরিবর্তন নয়, বরং M5-এর মতো প্রকৃত কোঅর্ডিনেশন (lock/semaphore) দরকার।

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

আগের পাঠ
প্রসেস ক্রিয়েশন ও টার্মিনেশন