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

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

Process creation & termination
৮ মিনিট পড়া শুরু · Beginner Python কোডসহ সম্পূর্ণ বাংলায়

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

  • fork() ও exec()-এর বাস্তব Unix মডেল — parent থেকে child তৈরি হওয়া এবং child-এ নতুন প্রোগ্রাম লোড হওয়া
  • প্রসেস ট্রি — কীভাবে OS parent-child সম্পর্ক ট্র্যাক করে
  • স্বাভাবিক বনাম জোরপূর্বক টার্মিনেশন
  • জম্বি ও অনাথ প্রসেস — কেন এগুলো তৈরি হয় এবং OS কীভাবে এগুলো পরিষ্কার করে
  • Python দিয়ে একটি প্রসেস টেবিল সিমুলেশন — টার্মিনেশন, জম্বি অবস্থা, reaping, এবং orphan reparenting

১ · প্রসেস ক্রিয়েশন — fork()fork()একটি Unix সিস্টেম কল যা বর্তমান প্রসেসের প্রায় হুবহু একটি কপি (child) তৈরি করে — একই কোড, একই মেমরি অবস্থা, কিন্তু নিজস্ব PID। ও exec()exec()বর্তমান প্রসেসের মেমরি ইমেজকে সম্পূর্ণ নতুন একটি প্রোগ্রাম দিয়ে প্রতিস্থাপন করে — PID একই থাকে, কিন্তু ভেতরের কোড/ডেটা পুরোপুরি বদলে যায়।

Unix-পরিবারের অপারেটিং সিস্টেমে (Linux, macOS) একটি নতুন প্রসেস তৈরির ক্লাসিক মডেল দুটি ধাপে হয়। প্রথমে একটি বিদ্যমান parent প্রসেস fork() কল করে — এর ফলে একটি নতুন child প্রসেস তৈরি হয় যা parent-এর প্রায় হুবহু কপি (একই কোড, একই ভেরিয়েবলের মান, একই খোলা ফাইল), শুধু নিজস্ব একটি নতুন PID (Process ID) নিয়ে। এরপর child প্রায়ই exec() কল করে — এটি child-এর মেমরি ইমেজকে সম্পূর্ণ ভিন্ন একটি প্রোগ্রামের কোড দিয়ে প্রতিস্থাপন করে দেয় (PID অপরিবর্তিত থাকে)। এভাবেই, উদাহরণস্বরূপ, আপনার শেল (bash) fork() করে একটি child তৈরি করে, আর সেই child exec() করে আপনার টাইপ করা কমান্ডটি (যেমন ls) চালায়।

প্রসেস ট্রি

প্রতিটি প্রসেসের (init/PID 1 ছাড়া) একটি parent থাকে, ফলে সব প্রসেস মিলে একটি ট্রি (tree) গঠন করে — গোড়ায় init (PID 1), তার নিচে সিস্টেম সার্ভিস ও ব্যবহারকারীর শেল, তার নিচে শেল থেকে চালানো প্রোগ্রামগুলো। OS প্রতিটি প্রসেসের PCB-তে (L06) তার parent PID (PPID) সংরক্ষণ করে এই সম্পর্ক ট্র্যাক করে।

২ · প্রসেস টার্মিনেশন

স্বাভাবিক টার্মিনেশন (Normal exit)
প্রসেস নিজের কাজ শেষ করে স্বেচ্ছায় exit করে — একটি exit status কোড রেখে যায় (সফল হলে সাধারণত 0)।
জোরপূর্বক টার্মিনেশন (Killed)
OS বা parent প্রসেসটিকে জোর করে বন্ধ করে দেয় — যেমন রিসোর্স সীমা অতিক্রম, বা parent নিজেই সব child টার্মিনেট করছে।

৩ · জম্বি প্রসেস ও অনাথ (orphan) প্রসেস

একটি child প্রসেস টার্মিনেট হওয়ার পরও তার exit status তথ্য পুরোপুরি মুছে ফেলা হয় না — parent-কে সেই তথ্য সংগ্রহ (wait() কল) করার সুযোগ দিতে হয়। যতক্ষণ parent তা না করে, ততক্ষণ প্রসেসটি একটি জম্বি (Zombie)Zombie processটার্মিনেট হয়ে গেছে কিন্তু parent এখনো এর exit status collect করেনি — প্রসেস টেবিলে সামান্য জায়গা দখল করে "atকা" থাকে, বাস্তব CPU/মেমরি ব্যবহার করে না। হিসেবে প্রসেস টেবিলে থেকে যায় — এটি বাস্তব CPU বা মেমরি ব্যবহার করে না, শুধু প্রসেস টেবিলের একটি ছোট এন্ট্রি দখল করে রাখে।

উল্টো দিকে, যদি একটি parent নিজেই তার child টার্মিনেট হওয়ার আগে টার্মিনেট হয়ে যায়, সেই child হয়ে যায় একটি অনাথ (Orphan)Orphan processযার parent তার আগেই টার্মিনেট হয়ে গেছে — সাধারণত init/PID 1-এর কাছে reparent হয়। প্রসেস। বেশিরভাগ Unix সিস্টেমে এমন orphan-দের স্বয়ংক্রিয়ভাবে init (PID 1) প্রসেসের কাছে reparent ("দত্তক") করে দেওয়া হয় — init পর্যায়ক্রমে তার (দত্তক নেওয়া) orphan ও zombie child-দের wait() করে পরিষ্কার করতে থাকে, যাতে কোনো প্রসেস টেবিল-এন্ট্রি চিরকাল আটকে না থাকে।

init (PID 1) PID 10 (parent) PID 11 (zombie) PID 12 (running) যদি PID 10 টার্মিনেট হয় -> PID 12 init-এর কাছে reparent হবে
PID 10 টার্মিনেট হলে তার এখনো-চলমান child (PID 12) init-এর কাছে reparent হয়ে "অনাথ" থেকে "দত্তক-নেওয়া" প্রসেসে পরিণত হয়।

৪ · প্রসেস টেবিল সিমুলেশন — টার্মিনেশন, জম্বি ও রিপ্যারেন্টিং

নিচের কোড সেলে একটি ছোট্ট প্রসেস টেবিল সিমুলেট করা হয়েছে — সম্পূর্ণ ইন-মেমরি Python ডেটা, বাস্তব os.fork()/kill() নয়। লক্ষ্য করুন terminate() কীভাবে সন্তানদের reparent করে এবং নিজে জম্বি হয়ে যায়, আর wait() কীভাবে জম্বি সন্তানদের "রিপ" (reap) করে টেবিল থেকে সরিয়ে দেয়।

Python
# প্রসেস টেবিল সিমুলেশন -- টয় ইন-মেমরি ডেটা, বাস্তব OS প্রসেস/fork()/kill() নয়
process_table = {
    1:  {"ppid": None, "state": "running"},   # init / PID 1
    10: {"ppid": 1,    "state": "running"},   # parent প্রসেস
    11: {"ppid": 10,   "state": "running"},   # PID 10-এর child
    12: {"ppid": 10,   "state": "running"},   # PID 10-এর আরেকটি child
}

def terminate(pid, table):
    # যদি এই প্রসেসের নিজস্ব child থাকে, তারা এখন অনাথ -> init(PID 1)-এর কাছে reparent
    orphaned = [cpid for cpid, info in table.items() if info["ppid"] == pid]
    for cpid in orphaned:
        table[cpid]["ppid"] = 1
    if orphaned:
        print(f"PID {pid} টার্মিনেট হলো -> সন্তান {orphaned} এখন অনাথ, init(PID 1)-এর কাছে reparent হলো")
    table[pid]["state"] = "zombie"
    print(f"PID {pid}: টার্মিনেট হলো -> জম্বি অবস্থায় (parent wait() না করা পর্যন্ত)")

def wait(parent_pid, table):
    reaped = [pid for pid, info in table.items()
              if info["ppid"] == parent_pid and info["state"] == "zombie"]
    for pid in reaped:
        del table[pid]
    print(f"parent {parent_pid}: wait() কল করলো -> রিপড হলো {reaped if reaped else 'কিছুই না'}")
    return reaped

print("--- প্রাথমিক প্রসেস টেবিল ---")
for pid, info in process_table.items():
    print(pid, info)

print("\n--- ধাপ ১: PID 11 টার্মিনেট হলো ---")
terminate(11, process_table)
print("11 এখনো টেবিলে?", 11 in process_table, "| state:", process_table[11]["state"])

print("\n--- ধাপ ২: parent (PID 10) wait() করলো ---")
wait(10, process_table)
print("11 এখনো টেবিলে?", 11 in process_table)

print("\n--- ধাপ ৩: PID 10 নিজেই টার্মিনেট হলো (child 12 তখনো চলছে) ---")
terminate(10, process_table)
print("12-এর নতুন ppid:", process_table[12]["ppid"])

print("\n--- ধাপ ৪: init (PID 1) wait() করলো ---")
wait(1, process_table)

print("\n--- চূড়ান্ত প্রসেস টেবিল ---")
for pid, info in process_table.items():
    print(pid, info)

    
লক্ষ্য করুন — PID 10 টার্মিনেট হওয়ার মুহূর্তেই তার তখনো-চলমান child (PID 12) init-এর কাছে reparent হয়ে যায় (orphan হয়েই থাকে না), আর PID 10 নিজে জম্বি হয়ে থাকে যতক্ষণ না init তাকে wait() দিয়ে reap করে। বাস্তব Linux-এও ঠিক এই দুটি প্রক্রিয়াই (reparenting + reaping) init/systemd দ্বারা স্বয়ংক্রিয়ভাবে ঘটে।
মূল কথা · Key takeaway

fork()+exec() মডেল দিয়ে প্রসেস তৈরি হয় parent-child সম্পর্কের একটি ট্রি গঠন করে। একটি প্রসেস টার্মিনেট হলে তার exit status parent সংগ্রহ না করা পর্যন্ত জম্বি হয়ে থাকে, আর তার নিজস্ব child (যদি থাকে) সাথে সাথে অনাথ হয়ে init-এর কাছে reparent হয়ে যায় — OS-এর এই বুককিপিং নিশ্চিত করে কোনো প্রসেস কখনো "মালিকহীন" অবস্থায় প্রসেস টেবিলে চিরকাল আটকে না থাকে।

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

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

প্র ০১ একটি জম্বি প্রসেস কি বাস্তবে ক্ষতিকর? এটি কি CPU সময় বা মেমরি খরচ করে?

একটি একক জম্বি প্রসেস সাধারণত নিরীহ — এটি কোনো CPU সময় ব্যবহার করে না (এটি চলছে না) এবং শুধু প্রসেস টেবিলের একটি ছোট এন্ট্রি (exit status ও কিছু বুককিপিং তথ্য) দখল করে রাখে। কিন্তু যদি কোনো বাগযুক্ত parent কখনোই wait() না করে এবং বহু child বারবার টার্মিনেট হতে থাকে, শত শত/হাজার হাজার জম্বি জমে PID স্পেস বা প্রসেস টেবিলের সীমিত এন্ট্রি ফুরিয়ে ফেলতে পারে — এটিকে "জম্বি প্রসেস লিক" বলা হয়, একটি বাস্তব প্রোডাকশন বাগ প্যাটার্ন।

প্র ০২ উপরের কোড সেলে যদি ধাপ ২ (parent-এর wait()) স্কিপ করা হতো, ধাপ ৩-এ PID 10 টার্মিনেট হওয়ার সময় PID 11-এর কী হতো?

PID 11 তখনো টেবিলে জম্বি অবস্থায় থাকতো (ppid=10)। PID 10 টার্মিনেট হওয়ার সময় আমাদের terminate() ফাংশন PID 10-এর সব সন্তান (11 ও 12 উভয়ই, তাদের অবস্থা যাই হোক না কেন) খুঁজে reparent করে init-এর কাছে পাঠাতো — অর্থাৎ ইতিমধ্যে-জম্বি PID 11-ও init-এর সন্তান হয়ে যেতো, এবং init-কে তখন 11 ও 10 উভয়কেই wait() দিয়ে reap করতে হতো।

প্র ০৩ শেল-এ ls কমান্ড চালানোর সময় fork() ও exec() কী ভূমিকা পালন করে, ধাপে ধাপে ব্যাখ্যা করুন।

আপনি শেলে ls টাইপ করলে শেল প্রথমে fork() কল করে — নিজের একটি প্রায় হুবহু কপি (child) তৈরি করে। এই child তারপর exec() কল করে নিজের মেমরি ইমেজকে ls প্রোগ্রামের কোড দিয়ে প্রতিস্থাপন করে (PID অপরিবর্তিত থাকে, কিন্তু ভেতরের প্রোগ্রাম সম্পূর্ণ বদলে যায়)। মূল শেল প্রসেস (parent) সাধারণত child-এর সম্পূর্ণ হওয়ার জন্য wait() করে অপেক্ষা করে, তারপর পরবর্তী প্রম্পট দেখায়।

অনুশীলন

  1. চিন্তা করুন: আপনার কম্পিউটারে একটি টার্মিনালে ps aux (Linux/macOS) চালালে মাঝে মাঝে "Z" (zombie) স্টেটাসের প্রসেস দেখা যায় কেন, এবং এগুলো নিজে থেকে সাধারণত কতক্ষণ পর মুছে যায় বলে মনে করেন?

    একটি জম্বি সাধারণত তখনই দেখা যায় যখন parent এখনো wait() কল করেনি — বেশিরভাগ ভালোভাবে লেখা প্রোগ্রাম দ্রুতই এটি করে ফেলে, তাই দীর্ঘস্থায়ী জম্বি সাধারণত parent-এর কোডে একটি বাগ নির্দেশ করে (parent হয়তো wait() কল করতেই ভুলে গেছে, বা ব্যস্ত থাকায় দেরি করছে)।

  2. পরীক্ষা করুন: উপরের কোড সেলে process_table-এ একটি নতুন PID 13 যোগ করুন যার ppid হবে 12, এবং ধাপ ৩-এর আগে PID 12-কেও টার্মিনেট করে দেখুন PID 13-এর কী হয়।

    PID 12 টার্মিনেট হলে terminate(12, process_table) কল করলে এটি প্রথমে PID 12-এর সন্তান (PID 13) খুঁজে বের করবে, তার ppid-কে 1 (init) করে দেবে, এবং তারপর PID 12 নিজে জম্বি হয়ে যাবে — ঠিক যেভাবে মূল উদাহরণে PID 10 টার্মিনেট হওয়ার সময় PID 12 অনাথ হয়েছিল।

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

আগের পাঠ
প্রসেস কন্ট্রোল ব্লক (PCB) ও কনটেক্সট সুইচিং