এই পর্বে যা শিখবেন
- প্রোগ্রাম ও প্রসেসের পার্থক্য, PID, parent-child সম্পর্ক, আর অপারেটিং সিস্টেম কীভাবে সবাইকে সামলায়
fork(),execওwaitpid()— কোনটি কী করে, আর shell কীভাবে এদের দিয়ে কমান্ড চালায়- Zombie ও orphan প্রসেস কেন তৈরি হয়, আর কীভাবে এড়াবেন
- Pipe, file descriptor ও
dup2দিয়েls | grep txtআসলে কীভাবে কাজ করে - Chrome, ওয়েব সার্ভার, Android ও Docker-এ প্রসেসের ব্যবহার, আর AI-এর লেখা প্রসেস-কোড যাচাইয়ের চেকলিস্ট
১ · প্রোগ্রাম আর প্রসেস — রেসিপি বনাম রান্না
আপনার ফোনে এখন কয়টি অ্যাপ খোলা? Facebook, bKash, YouTube, হয়তো একটি গেমও। সবগুলো একসাথে চলছে, কেউ কারো মেমরিতে হাত দিচ্ছে না, একটি crash করলে বাকিগুলো দিব্যি চলতে থাকে। এর পেছনে যে ধারণাটি কাজ করছে, তার নাম process. এই পর্বে আমরা সেটিই খুলে দেখব।
প্রথমে একটি পার্থক্য পরিষ্কার করি। প্রোগ্রাম হলো ডিস্কে রাখা একটি ফাইল — যেমন আপনার compile করা hello বা /bin/ls. এটি চুপচাপ পড়ে থাকে, কিছুই করে না। আপনি যখন সেটি চালান, তখন অপারেটিং সিস্টেম ফাইলটিকে মেমরিতে আনে, একটি stack ও heap বানিয়ে দেয়, CPU-র সময় বরাদ্দ করে — আর তখনই তৈরি হয় একটি প্রসেস। প্রসেস মানে চলমান অবস্থায় থাকা প্রোগ্রাম।
মায়ের রান্নার খাতায় লেখা বিরিয়ানির রেসিপি হলো প্রোগ্রাম — শুধু নির্দেশনা। কিন্তু যখন চুলা জ্বলে, হাঁড়িতে চাল-মাংস পড়ে, কেউ নাড়তে থাকেন — সেই চলমান রান্নাটিই প্রসেস। একই রেসিপি দিয়ে একসাথে তিন বাড়িতে তিনটি রান্না হতে পারে। প্রতিটির নিজস্ব হাঁড়ি, নিজস্ব চুলা, নিজস্ব অবস্থা — এক বাড়িতে লবণ বেশি পড়লে অন্য বাড়ির বিরিয়ানি নষ্ট হয় না।
ঠিক তেমনি, আপনি যদি তিনটি terminal খুলে তিনবার একই প্রোগ্রাম চালান, তাহলে তিনটি আলাদা প্রসেস তৈরি হবে। প্রতিটি প্রসেস অপারেটিং সিস্টেমের কাছ থেকে কয়েকটি জিনিস পায়:
🪪 একটি পরিচয় নম্বর
প্রতিটি প্রসেসের একটি অনন্য PID (Process ID) থাকে, যেমন 4821. ক্লাসের রোল নম্বরের মতো — এই নম্বর দিয়েই OS প্রসেসকে চেনে, থামায় বা বন্ধ করে।
🧠 নিজস্ব মেমরি
পর্ব ২০-এ দেখা code, data, heap, stack — পুরো memory map প্রতিটি প্রসেসের আলাদা। Virtual memory-র কল্যাণে একটি প্রসেস আরেকটির মেমরি দেখতেই পায় না।
নিজস্ব ঠিকানা-জগৎ📂 খোলা ফাইলের তালিকা
কোন কোন ফাইল, pipe বা নেটওয়ার্ক সংযোগ খোলা আছে — তার একটি তালিকা, যার প্রতিটির একটি file descriptor নম্বর। 0, 1, 2 যথাক্রমে stdin, stdout, stderr.
fd table👨👦 একজন অভিভাবক
প্রতিটি প্রসেসকে অন্য কোনো প্রসেস জন্ম দিয়েছে। জন্মদাতা হলো parent process, নতুনটি child process. Parent-এর PID-কে বলে PPID.
getppid() দিয়ে জানা যায়এই সবকিছুর হিসাব রাখে অপারেটিং সিস্টেম, আরও নির্দিষ্ট করে বললে তার ভেতরের kernel. Kernel হলো গার্মেন্টস কারখানার প্রোডাকশন ম্যানেজারের মতো — কোন লাইন কখন মেশিন পাবে, কে কতটা কাপড় নেবে, কেউ অন্যের টেবিলে হাত দিচ্ছে কি না, সব তাঁর নজরে। এক মুহূর্তে CPU-র একটি কোরে একটিই প্রসেস চলে; kernel সেকেন্ডে বহুবার প্রসেস বদলায় বলে মনে হয় সব একসাথে চলছে।
আপনার কম্পিউটারে এখন কী কী প্রসেস চলছে, নিজেই দেখতে পারেন। Windows-এ Task Manager খুলে “Details” ট্যাবে PID কলাম দেখুন। Linux বা Mac-এ terminal-এ লিখুন:
ps -ef # সব প্রসেসের তালিকা, PID ও PPID সহ
pstree -p # কে কার parent — গাছের আকারে
echo $$ # এই shell-এর নিজের PIDUID PID PPID CMD
root 1 0 /sbin/init
rafi 500 1 -bash
rafi 812 500 vim notes.c
rafi 813 500 ps -ef ← ps নিজেও একটি প্রসেস, তার parent হলো bash
আপনি terminal-এ যা-ই চালান, shell সেটির parent হয়। কীভাবে, সেটাই এই পর্বের মূল গল্প।
প্রসেস, fork, pipe — এই ধারণাগুলোর আধুনিক রূপ এসেছে Unix থেকে। ১৯৬৯ সালের দিকে Bell Labs-এ Ken Thompson ও Dennis Ritchie Unix লেখা শুরু করেন, আর ১৯৭১ সালে এর প্রথম সংস্করণ ব্যবহার শুরু হয়। পরে Ritchie Unix-কে নতুন করে লেখার জন্যই C ভাষা তৈরি করেন। এজন্যই C আর প্রসেসের system call-গুলো এত ঘনিষ্ঠভাবে জড়ানো — দুটি একই পরিবারে জন্ম নিয়েছে। আজকের Linux, macOS, Android সবই এই নকশার উত্তরসূরি।
২ · System call — kernel-এর দরজায় কড়া নাড়া
একটি প্রশ্ন ভাবুন: আপনার প্রোগ্রাম যদি নিজের ইচ্ছামতো যেকোনো মেমরিতে লিখতে পারত, ডিস্কের যেকোনো জায়গা মুছতে পারত — তাহলে একটি ভুল প্রোগ্রাম বা একটি ভাইরাস পুরো কম্পিউটার নষ্ট করে দিত। তাই আধুনিক CPU দুটি আলাদা অধিকারের স্তরে চলে।
👤 User space
আপনার প্রোগ্রাম, browser, গেম — সবাই চলে user space-এ, সীমিত অধিকার নিয়ে। সরাসরি হার্ডওয়্যার ছোঁয়া নিষেধ, অন্যের মেমরি দেখা নিষেধ।
ব্যাংকের গ্রাহক কাউন্টার🔐 Kernel space
Kernel চলে kernel space-এ, পূর্ণ অধিকার নিয়ে। ডিস্ক, নেটওয়ার্ক, মেমরি, নতুন প্রসেস তৈরি — সব কাজ শুধু এখান থেকেই করা যায়।
ব্যাংকের ভল্টব্যাংকে গিয়ে আপনি নিজে ভল্টে ঢুকে টাকা তুলতে পারেন না। কাউন্টারে একটি চেক জমা দেন, কর্মকর্তা ভেতরে গিয়ে কাজটি করে দেন। প্রোগ্রামের জগতে এই চেকের নামই system call, আর kernel-এর কাছে যাওয়ার এটিই একমাত্র বৈধ পথ। আপনি যখন printf ডাকেন, C library শেষ পর্যন্ত write নামের একটি system call করে — CPU তখন অল্প সময়ের জন্য kernel space-এ যায়, kernel লেখাটি স্ক্রিনে পাঠায়, তারপর আবার আপনার প্রোগ্রামে ফিরে আসে।
| System call | কাজ | ব্যাংকের তুলনা |
|---|---|---|
fork() | চলমান প্রসেসের একটি কপি তৈরি | একটি অ্যাকাউন্টের হুবহু যুগ্ম কপি খোলা |
execve() | প্রসেসের ভেতরে নতুন প্রোগ্রাম বসানো | একই অ্যাকাউন্ট নম্বর, কিন্তু নতুন ধরনের অ্যাকাউন্ট |
waitpid() | child শেষ হওয়ার অপেক্ষা ও ফলাফল নেওয়া | লেনদেনের রসিদ বুঝে নেওয়া |
pipe() | দুই প্রসেসের মাঝে একমুখী নল তৈরি | দুই শাখার মাঝে একটি ক্যাশ পাঠানোর বাক্স |
kill() | কোনো প্রসেসকে signal পাঠানো | গ্রাহককে জরুরি নোটিশ পাঠানো |
_exit() | প্রসেস শেষ করা, exit status জানানো | অ্যাকাউন্ট বন্ধ করা |
System call সাধারণ ফাংশন ডাকার চেয়ে বেশ ব্যয়বহুল, কারণ প্রতিবার CPU-কে অধিকারের স্তর বদলাতে হয়। এজন্যই printf প্রতিটি অক্ষরে write ডাকে না — লেখা কিছুক্ষণ buffer-এ জমিয়ে একবারে পাঠায়। এই buffer-এর কথা মনে রাখুন, পরের অংশেই এটি একটি মজার গোলমাল বাধাবে। Linux-এ কোনো প্রোগ্রাম কোন কোন system call করছে, দেখতে চাইলে strace ./hello চালিয়ে দেখুন।
এখানে একটি কথা স্পষ্ট করে রাখি: এই পর্বের কোড POSIX মান অনুযায়ী — Linux, macOS, Android ও অন্যান্য Unix-ধাঁচের সিস্টেমে চলে। Windows-এ এগুলো সরাসরি নেই; সেখানে WSL (Windows Subsystem for Linux) বা এই পাতার অনলাইন রানার ব্যবহার করুন। পাতার রানারটি Linux-এ চলে, তাই নিচের কোডগুলো এখানেই চালাতে পারবেন।
৩ · fork() — একটি প্রসেস থেকে দুটি
এবার Unix-এর সবচেয়ে বিখ্যাত, আর প্রথম দেখায় সবচেয়ে অদ্ভুত ফাংশনটি। fork() ডাকলে kernel আপনার চলমান প্রসেসের একটি প্রায় হুবহু কপি তৈরি করে — একই কোড, ভ্যারিয়েবলের একই মান, একই খোলা ফাইল, এমনকি প্রোগ্রাম ঠিক কোন লাইনে আছে সেটিও একই। পার্থক্য শুধু PID-তে, আর fork()-এর ফেরত মানে।
দুটি আলাদা প্রসেসে, দুটি আলাদা মান নিয়ে।
| কোথায় ফিরল | ফেরত মান | মানে |
|---|---|---|
| Child প্রসেসে | 0 | “আমি নতুন জন্ম নেওয়া কপি” |
| Parent প্রসেসে | child-এর PID (যেমন 1201) | “আমার সন্তানের নম্বর এটি” |
| Parent-এ, ব্যর্থ হলে | -1 | কোনো child তৈরি হয়নি — প্রসেসের সীমা পূর্ণ বা মেমরি নেই |
একই কোড দুই প্রসেসে চলছে — তাহলে তারা আলাদা কাজ করে কীভাবে? ফেরত মান দেখে। if (pid == 0) হলে আমি child, না হলে parent. নিচের ডায়াগ্রামে একটি ছোট প্রোগ্রাম ধাপে ধাপে চালিয়ে দেখুন। ডান পাশে দুই প্রসেসের অবস্থা খেয়াল করুন:
1int main(void) {2 printf("Before fork: only one process\n");3 fflush(stdout);4 pid_t pid = fork();5 if (pid == 0) { /* child */6 printf("Child: ...\n");7 exit(7);8 }9 waitpid(pid, &status, 0); /* parent */10 printf("Parent: ... %d\n", WEXITSTATUS(status));11}
| stdout buffer | |
| Parent 1200: pid | |
| Parent-এর কাজ | |
| Child 1201: pid | |
| Child-এর কাজ |
- এখনো একটিই প্রসেস। PID
1200-এর প্রসেসটিprintfচালাল। লেখাটি সাথে সাথে স্ক্রিনে যায়নি — আগে বসেছে stdout-এর buffer-এ, প্রসেসের নিজস্ব মেমরিতে। (Terminal-এ লাইনের শেষে নতুন লাইন পেলেই সাধারণত ছাপা হয়ে যায়, কিন্তু আউটপুট ফাইল বা pipe-এ গেলে buffer-এই বসে থাকে।) - Buffer খালি করা হলো।
fflush(stdout)জোর করে buffer-এর লেখা kernel-এ পাঠিয়ে দিল। এখন buffer খালি — fork-এর আগে এটি জরুরি, কারণ একটু পরেই পুরো মেমরি কপি হবে। - fork() — kernel কপি বানাচ্ছে। প্রসেসটি kernel-কে অনুরোধ করল। Kernel নতুন PID
1201দিল, parent-এর মেমরি, খোলা ফাইলের তালিকা আর বর্তমান অবস্থান — সব নতুন প্রসেসে কপি করল। - দুই প্রসেসে দুটি ফেরত মান। এখন দুটি প্রসেসই লাইন ৪-এর ঠিক পরে দাঁড়িয়ে। Parent-এর
pidভ্যারিয়েবলে এসেছে1201, child-এর নিজের কপিতে এসেছে0. কোড এক, মান আলাদা — এখান থেকেই পথ ভাগ হয়। - দুজন দুই পথে। Child দেখল
pid == 0, তাই ভেতরে ঢুকে ছাপল এবংexit(7)দিয়ে শেষ হলো। একই সময়ে parent শর্ত মিথ্যা পেয়ে লাইন ৯-এwaitpid-এ গিয়ে ঘুমিয়ে পড়ল — child শেষ না হওয়া পর্যন্ত সে আর কিছু ছাপবে না। এই অপেক্ষার কারণেই আউটপুটের ক্রম সবসময় একই থাকে। - Child শেষ, কিন্তু পুরোপুরি মোছেনি। Child-এর মেমরি মুক্ত হয়ে গেছে, কিন্তু kernel তার exit status
7একটি ছোট এন্ট্রিতে রেখে দিয়েছে, যতক্ষণ না parent সেটি নেয়। এই অবস্থার নাম zombie — একটু পরে বিস্তারিত দেখব। Kernel এবার parent-কে জাগাল। - Parent ফলাফল নিল।
waitpidফিরে এলো,WEXITSTATUS(status)থেকে পাওয়া গেল7. Child-এর শেষ চিহ্নটুকুও এখন মুছে গেছে। আবার একটিই প্রসেস, আর সে ফল ছাপছে।
পুরো মেমরি কি সত্যিই কপি হয়? — Copy-on-write
একটি প্রসেস যদি ২ GB মেমরি ব্যবহার করে, প্রতিবার fork-এ ২ GB কপি করা তো ভয়াবহ ধীর হতো! বাস্তবে kernel একটি চতুর কৌশল ব্যবহার করে, নাম copy-on-write. Fork-এর সময় আসল মেমরি কপি হয় না; শুধু “কোন ঠিকানা কোন RAM-পাতায়” সেই মানচিত্রটি কপি হয়, আর দুই প্রসেস একই পাতাগুলো ভাগ করে নেয়। পাতাগুলো “শুধু পড়া যাবে” চিহ্নিত থাকে। কেউ কোনো পাতায় লিখতে গেলেই একটি page fault হয়, kernel তখন কেবল সেই একটি পাতার আলাদা কপি বানিয়ে দেয়।
দুই ভাই একটি গাইড বই ভাগ করে পড়ছে। যতক্ষণ দুজনই শুধু পড়ছে, একটি বই-ই যথেষ্ট। যেই একজন কোনো পাতায় নিজের নোট লিখতে চাইল, শুধু সেই পাতাটি ফটোকপি করে তাকে দেওয়া হলো। পুরো বই কখনো ফটোকপি করতে হলো না।
এবার নিজে চালিয়ে দেখুন। প্রোগ্রামটি fork করে, child একটি লাইন ছাপে আর exit(7) দিয়ে শেষ হয়, parent অপেক্ষা করে exit status পড়ে:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
printf("Before fork: only one process\n");
fflush(stdout); /* empty the buffer before cloning */
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) { /* child: fork() returned 0 */
printf("Child: fork() gave me 0, I will exit with 7\n");
exit(7);
}
int status; /* parent: fork() returned child's PID */
waitpid(pid, &status, 0); /* wait until the child is finished */
if (WIFEXITED(status))
printf("Parent: child exited with status %d\n", WEXITSTATUS(status));
return 0;
}Child-এর pid-এ 0, তাই সে if-এর ভেতরে ঢুকে ছাপে এবং exit(7) করে — exit থাকায় সে কখনো নিচের parent-এর কোডে পৌঁছায় না। Parent waitpid-এ অপেক্ষা করে, তারপর WIFEXITED দিয়ে নিশ্চিত হয় child স্বাভাবিকভাবে শেষ হয়েছে, আর WEXITSTATUS দিয়ে সেই 7 বের করে। fork()-এর -1 যাচাইটিও খেয়াল করুন — বাস্তবে প্রসেসের সীমা পূর্ণ হলে fork ব্যর্থ হয়, আর তা না দেখলে parent ভাববে -1 নম্বরের একটি child আছে।
Buffer দ্বিগুণ হওয়ার ফাঁদ: উপরের কোড থেকে fflush(stdout); লাইনটি মুছে আবার চালান। দেখবেন Before fork লাইনটি দুবার ছাপা হয়েছে! কারণ এই রানারে আউটপুট একটি pipe-এ যায়, তাই লেখাটি fork-এর সময়ও buffer-এ বসে ছিল। Fork সেই buffer-সহ পুরো মেমরি কপি করেছে, আর দুই প্রসেস শেষ হওয়ার সময় যার যার buffer খালি করেছে। নিজের কম্পিউটারে terminal-এ চালালে হয়তো একবারই দেখবেন, কিন্তু ./fork_wait > out.txt করলে আবার দুবার। নিয়মটি সহজ: fork-এর ঠিক আগে fflush(stdout), আর child-এ exec-এর পর ব্যর্থ হলে exit-এর বদলে _exit, যাতে buffer আরেকবার খালি না হয়।
৪ · exec — একই প্রসেসে নতুন প্রোগ্রাম
fork দিয়ে আমরা নিজেরই একটি কপি পেলাম। কিন্তু shell-এ ls লিখলে তো bash-এর কপি চলে না, চলে ls প্রোগ্রাম! এর জন্য লাগে দ্বিতীয় অস্ত্র — exec পরিবার। exec চলমান প্রসেসের ভেতরের কোড, ডেটা, heap, stack সব ফেলে দিয়ে ডিস্ক থেকে নতুন একটি প্রোগ্রাম এনে বসায়, তারপর সেটির main থেকে চালানো শুরু করে।
fork নতুন প্রসেস বানায়, কিন্তু প্রোগ্রাম একই রাখে। exec প্রোগ্রাম বদলায়, কিন্তু প্রসেস একই রাখে। exec-এর পর PID, parent, আর খোলা file descriptor-গুলো আগের মতোই থাকে — শুধু ভেতরের প্রোগ্রামটি নতুন। আর exec সফল হলে কখনো ফেরে না, কারণ যে কোড তাকে ডেকেছিল, সেটির আর অস্তিত্বই নেই।
exec-এর কয়েকটি রূপ আছে, নামের অক্ষর দেখেই চেনা যায়। সবগুলো শেষ পর্যন্ত একটিই system call ডাকে — execve.
| নামের অক্ষর | মানে | উদাহরণ |
|---|---|---|
l (list) | argument আলাদা আলাদা করে দেওয়া, শেষে NULL | execl("/bin/ls", "ls", "-l", (char *)NULL) |
v (vector) | argument একটি array-তে দেওয়া | execv("/bin/ls", argv) |
p (PATH) | প্রোগ্রামের পুরো ঠিকানা না দিলে PATH-এ খুঁজে নেয় | execvp("ls", argv) |
e (environment) | নতুন প্রোগ্রামের environment নিজে ঠিক করে দেওয়া | execve(path, argv, envp) |
নিচে fork আর exec একসাথে — shell-এর মূল কাজের ছোট একটি রূপ। এটি পড়ার জন্য; চালানোর দরকার নেই, পরের ডায়াগ্রামেই এর প্রতিটি ধাপ দেখবেন:
pid_t pid = fork();
if (pid == 0) { /* child */
char *argv[] = { "ls", "-l", NULL };
execvp("ls", argv); /* success: never returns */
perror("execvp"); /* reached only on failure */
_exit(127); /* 127 = "command not found" */
}
int status;
waitpid(pid, &status, 0); /* parent waits, like a shell */খেয়াল করুন execvp-এর পরের দুটি লাইন। exec সফল হলে এগুলো কখনো চলবে না। চলবে কেবল তখনই, যখন প্রোগ্রামটি খুঁজে পাওয়া যায়নি বা চালানোর অনুমতি নেই। এই লাইন দুটি না থাকলে ব্যর্থ child নিচে নেমে parent-এর কোড চালাতে শুরু করবে — তখন একই কাজ দুই প্রসেসে দুবার হবে। AI-এর লেখা কোডে এই ভুল প্রায়ই দেখা যায়।
এবার দেখুন আপনি terminal-এ ls -l লিখে Enter চাপলে shell ভেতরে ঠিক কী করে:
- bash পড়ল
ls -lPID 500 - fork()
bash → bashনতুন PID 501 - execvp()
bash → lsPID 501-ই আছে - ls চলছে
exit(0)তালিকা ছাপল - waitpid()
$? = 0আবার prompt
PID PPID প্রোগ্রাম অবস্থা
500 1 bash আপনার লেখা লাইন পড়ছে: "ls -l"
PID PPID প্রোগ্রাম অবস্থা
500 1 bash waitpid(501)-এ অপেক্ষা
501 500 bash সবে জন্ম, fork() ফেরত দিল 0
PID PPID প্রোগ্রাম অবস্থা
500 1 bash waitpid(501)-এ অপেক্ষা
501 500 ls bash-এর কোড মুছে /bin/ls বসেছে
-rw-r--r-- 1 rafi rafi 220 notes.txt
-rw-r--r-- 1 rafi rafi 1450 report.c
← ls শেষ, exit status 0; kernel bash-কে জাগাল
$ echo $? 0 $ ← পরের কমান্ডের অপেক্ষা
- Shell লাইন পড়ল। bash (PID
500) নিজেও একটি সাধারণ প্রসেস। আপনিls -lলিখে Enter চাপলেন। bash লাইনটিকে টুকরো করল: প্রোগ্রামের নামls, argument-l. - fork() — নিজের কপি। bash fork করল। এখন দুটি bash: parent
500আর child501. Parent সাথে সাথেwaitpid-এ অপেক্ষায় গেল। Child-এর হাতে আছে parent-এর সব খোলা ফাইলের কপি — বিশেষ করে stdout, যা এখনো terminal-এর দিকে। - exec() — শরীর বদল। Child
execvp("ls", argv)ডাকল। Kernel child-এর ভেতর থেকে bash-এর কোড সরিয়ে/bin/lsবসাল। PID কিন্তু এখনো501, parent এখনো500. এই ফাঁকটুকু — fork আর exec-এর মাঝখান — খুব গুরুত্বপূর্ণ; একটু পরে দেখবেন shell এখানেই redirection সাজায়। - ls কাজ করল। ls ফাইলের তালিকা লিখল তার stdout-এ, অর্থাৎ fd 1-এ — যা bash-এর কাছ থেকে উত্তরাধিকারে পাওয়া, তাই সরাসরি আপনার স্ক্রিনে এলো। কাজ শেষে
exit(0). - wait — ফলাফল সংগ্রহ। Kernel parent bash-কে জাগাল।
waitpidফেরত দিল child-এর exit status 0, bash সেটি$?-এ রেখে দিল এবং আবার prompt দেখাল। আপনার প্রতিটি কমান্ড ঠিক এই পাঁচ ধাপে চলে — দিনে শত শত বার।
অন্য পথ — posix_spawn আর Windows-এর CreateProcess
Fork-এর পর প্রায় সাথে সাথেই exec — অর্থাৎ যে মেমরির মানচিত্র কপি করা হলো, তা প্রায় সাথে সাথেই ফেলে দেওয়া হচ্ছে। এই অপচয় এড়াতে POSIX মানে আছে posix_spawn(), যা এক ডাকেই নতুন প্রোগ্রাম চালু করে। Windows-এর মূল প্রোগ্রামিং ব্যবস্থা কখনো fork-এর পথে যায়নি; সেখানে CreateProcess() একটি ফাংশন কলেই প্রোগ্রামের নাম, argument, কোন ফাইল খোলা থাকবে ইত্যাদি সব প্যারামিটার নিয়ে নতুন প্রসেস তৈরি করে। কোন নকশা ভালো, সেই বিতর্ক ভাবনার প্রশ্নে।
৫ · wait, exit status — আর zombie ও orphan প্রসেস
পর্ব ০৪-এ দেখেছিলেন main-এর return 0 আসলে অপারেটিং সিস্টেমকে একটি সংখ্যা জানায় — exit status. সেই সংখ্যাটি কে পড়ে? উত্তর: parent, waitpid() দিয়ে। Shell-এ make && ./app লিখলে দ্বিতীয় কমান্ডটি তখনই চলে, যখন প্রথমটির exit status 0 — অর্থাৎ পুরো ব্যবস্থাটি এই ছোট সংখ্যার ওপর দাঁড়িয়ে।
| Macro | প্রশ্ন | উদাহরণ |
|---|---|---|
WIFEXITED(s) | child কি স্বাভাবিকভাবে (exit/return দিয়ে) শেষ হয়েছে? | exit(7) করলে সত্য |
WEXITSTATUS(s) | তাহলে কোন সংখ্যা দিয়ে? (0 থেকে 255) | 7 |
WIFSIGNALED(s) | কোনো signal কি তাকে মেরে ফেলেছে? | segmentation fault হলে সত্য |
WTERMSIG(s) | কোন signal? | SIGSEGV (11), SIGKILL (9) |
এবার একটি গুরুত্বপূর্ণ প্রশ্ন: child শেষ হয়ে গেল, কিন্তু parent যদি কখনো waitpid না ডাকে? Kernel তো জানে না parent কখন ফল চাইবে, তাই child-এর exit status ফেলে দিতে পারে না। তখন child-এর মেমরি মুক্ত হয়, কিন্তু process table-এ তার একটি ছোট এন্ট্রি থেকে যায়। এই আধামরা অবস্থার নাম zombie process. উল্টোটাও ঘটে — child চলছে, কিন্তু parent আগেই শেষ হয়ে গেল। তখন child হয় orphan process. নিচে দুটিই দেখুন:
PID PPID STAT CMD
700 500 S ./server
701 700 S ./server ← child, একটি কাজ করছে
PID PPID STAT CMD 700 500 S ./server 701 700 Z [server] <defunct> ← zombie: exit status 3 জমা
PID PPID STAT CMD 700 500 S ./server 701 700 Z [server] <defunct> 702 700 Z [server] <defunct> 703 700 Z [server] <defunct> … হাজারো এন্ট্রি জমলে নতুন প্রসেস তৈরিই বন্ধ
PID PPID STAT CMD 700 500 S ./server ← সব status সংগ্রহ করা হলো (701, 702, 703 — তালিকা থেকে মুছে গেছে)
PID PPID STAT CMD
1 0 S /sbin/init
704 1 S ./server ← অনাথ, এখন PPID = 1
- স্বাভাবিক অবস্থা। একটি সার্ভার প্রসেস (PID
700) একটি child তৈরি করেছে কোনো কাজের জন্য। দুজনেই চলছে বা অপেক্ষা করছে —STATকলামেSমানে ঘুমিয়ে অপেক্ষা। - Child শেষ — zombie জন্ম। Child
exit(3)করল। তার মেমরি, খোলা ফাইল সব মুক্ত। কিন্তু parent এখনোwaitpidডাকেনি, তাই kernel তার PID আর exit status ধরে রেখেছে।STAT-এZ, নামের পাশে<defunct>. Zombie-কেkillদিয়েও মারা যায় না — সে তো আগেই মৃত! - Zombie জমছে। Parent প্রতিটি কাজের জন্য নতুন child বানাচ্ছে, কিন্তু কখনো wait করছে না। প্রতিটি zombie ছোট, কিন্তু প্রতিটি একটি PID আটকে রাখে। দিনের পর দিন চলা সার্ভারে এভাবে হাজারো zombie জমলে একসময় PID শেষ হয়ে যায়, আর নতুন প্রসেস তৈরিই ব্যর্থ হতে থাকে।
- wait — সমাধান। Parent
waitpidডাকল (বাSIGCHLDsignal পেয়ে নিয়মিত ডাকে)। প্রতিটি zombie-এর exit status সংগ্রহ হলো, এন্ট্রিগুলো মুছে গেল। একে বলে child-কে reap করা — ফসল তোলার মতো। নিয়ম: যে fork করে, সে-ই wait করে। - Orphan — অনাথের দত্তক। উল্টো ঘটনা: parent আগেই crash করল, child তখনো চলছে। Kernel child-কে অনাথ রাখে না — তাকে init প্রসেসের (PID
1, আজকের বেশিরভাগ Linux-এ systemd) সন্তান বানিয়ে দেয়। কিছু সিস্টেমে বিশেষ দায়িত্বপ্রাপ্ত অন্য একটি প্রসেসও এই দত্তক নিতে পারে। init নিয়মিত wait করে, তাই অনাথ শেষ হলে zombie হয়ে পড়ে থাকে না।
Zombie নিজে ক্ষতিকর নয় — এটি একটি লক্ষণ, যে parent-এর কোডে wait বাদ পড়েছে। আর orphan সবসময় ভুল নয়; Linux-এ ব্যাকগ্রাউন্ডে চলা অনেক সার্ভিস (daemon) ইচ্ছা করেই parent থেকে আলাদা হয়ে init-এর অধীনে চলে যায়।
৬ · IPC ও pipe — প্রসেসেরা কীভাবে কথা বলে
এখন একটি সমস্যা দেখুন। Fork-এর পর parent যদি একটি global ভ্যারিয়েবল count = 5 করে, child কি সেটি দেখবে? না! Copy-on-write-এর কারণে parent লিখতেই তার জন্য নতুন পাতা তৈরি হয়েছে; child-এর কপিতে মান আগের মতোই। প্রসেসের নিজস্ব মেমরি — এই দেয়ালটিই নিরাপত্তার ভিত্তি, কিন্তু একই দেয়াল কথা বলাও কঠিন করে দেয়। তাই প্রসেসের মধ্যে তথ্য আদান-প্রদানের জন্য kernel কিছু বিশেষ পথ দেয়, যাদের একসাথে বলে IPC (Inter-Process Communication).
সবচেয়ে পুরোনো ও সরল পথটি হলো pipe. pipe(fd) ডাকলে kernel নিজের মেমরিতে একটি ছোট buffer বানায় (Linux-এ সাধারণত ৬৪ KB) আর আপনাকে দুটি file descriptor দেয়: fd[0] দিয়ে পড়া যায়, fd[1] দিয়ে লেখা যায়। এক মুখে যা ঢালবেন, অন্য মুখে সেটাই একই ক্রমে বের হবে।
ওয়াসার পানির পাইপের কথা ভাবুন। পানি এক দিকেই যায়। ট্যাংকি থেকে কলের দিকে। কলের মুখ থেকে কেউ উল্টো পাঠাতে পারে না — দুই দিকে কথা বলতে চাইলে দুটি পাইপ লাগবে। আর একটি জরুরি বিষয়: কলের পাশে দাঁড়ানো মানুষ কীভাবে জানবেন পানি আসা পুরোপুরি থেমে গেছে, নাকি একটু পরে আবার আসবে? জানবেন কেবল তখনই, যখন ট্যাংকির দিকের সব ভালভ বন্ধ হবে।
ওই শেষ কথাটিই pipe-এর সবচেয়ে বড় নিয়ম। পড়ার দিক EOF পায় শুধু তখন, যখন pipe-এর লেখার মুখ যতগুলো প্রসেসে খোলা আছে, সব বন্ধ হয়। Fork-এর পর দুই প্রসেসের হাতেই দুই মুখ থাকে। তাই প্রত্যেককে নিজের অপ্রয়োজনীয় মুখটি বন্ধ করতে হয়। নিচের কোডে child লেখে, parent পড়ে:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int fd[2]; /* fd[0] = read end, fd[1] = write end */
if (pipe(fd) == -1) { perror("pipe"); return 1; }
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) { /* child = writer */
close(fd[0]); /* child does not read */
const char msg[] = "Salam from the child";
if (write(fd[1], msg, sizeof msg - 1) < 0) _exit(1);
close(fd[1]); _exit(0); /* last write end closed: reader gets EOF */
}
close(fd[1]); /* parent does not write */
char buf[64], c; int len = 0;
while (len < 63 && read(fd[0], &c, 1) == 1) buf[len++] = c;
buf[len] = '\0';
close(fd[0]); waitpid(pid, NULL, 0);
printf("Parent got %d bytes: \"%s\"\n", len, buf);
return 0;
}Fork-এর আগে pipe তৈরি হয়েছে, তাই দুই প্রসেসই দুই মুখের কপি পেয়েছে। Child পড়বে না বলে fd[0] বন্ধ করে, ২০ বাইট লেখে, তারপর fd[1]-ও বন্ধ করে বেরিয়ে যায়। Parent লিখবে না বলে নিজের fd[1] বন্ধ করে, তারপর একটি একটি করে বাইট পড়তে থাকে যতক্ষণ না read 0 ফেরত দেয় — অর্থাৎ EOF. এবার পরীক্ষাটি করুন: parent-এর close(fd[1]); লাইনটি মুছে দিন। ২০ বাইট পড়ার পর parent-এর read চিরকাল অপেক্ষা করবে, কারণ তার নিজের হাতেই একটি লেখার মুখ খোলা — kernel ভাবছে “এই প্রসেস হয়তো আরও লিখবে।” কোনো error আসবে না, রানারটি শুধু সময়সীমা পেরিয়ে থেমে যাবে।
dup2 — shell-এর আসল জাদু
পর্ব ১০-এ আপনি ls | grep txt বা ./app > out.txt-এর মতো কমান্ড দেখেছেন। কিন্তু ls তো pipe-এর কথা কিছুই জানে না — সে শুধু fd 1-এ লেখে! তাহলে লেখা pipe-এ যায় কীভাবে? এখানেই dup2(). dup2(p[1], 1) মানে “fd 1 নম্বরটি এখন থেকে pipe-এর লেখার মুখের দিকে নির্দেশ করবে।” fork আর exec-এর মাঝখানে child এটি করে নেয়। আর যেহেতু exec খোলা file descriptor মুছে দেয় না, নতুন প্রোগ্রামটি না জেনেই pipe-এ লিখতে থাকে। নিচে প্রতিটি প্রসেসের file descriptor তালিকা ধাপে ধাপে দেখুন:
1/* shell: ls | grep txt */2pipe(p); /* p[0]=3, p[1]=4 */3fork(); fork(); /* two children */4child1: dup2(4, 1); close(3); close(4);5 execlp("ls", ...);6child2: dup2(3, 0); close(3); close(4);7 execlp("grep", "grep", "txt", ...);8shell: close(3); close(4);9 waitpid(...); waitpid(...);
| Shell | |
| Child ১ | |
| Child ২ | |
| লেখার মুখ খোলা যাদের হাতে |
- শুরু। Shell-এর হাতে আছে fd 0 (কিবোর্ড), fd 1 ও 2 (স্ক্রিন)। আপনি লিখেছেন
ls | grep txt— দুটি প্রোগ্রাম, মাঝে একটি নল। - pipe() — নল তৈরি। Kernel একটি pipe বানাল আর shell-কে দুটি নতুন নম্বর দিল:
3পড়ার মুখ,4লেখার মুখ। সবচেয়ে ছোট খালি নম্বরগুলোই দেওয়া হয়। - দুবার fork. দুটি child তৈরি হলো, আর প্রত্যেকে shell-এর পুরো fd তালিকার কপি পেল। খেয়াল করুন নিচের সারি: লেখার মুখ এখন তিনটি প্রসেসের হাতে খোলা।
- Child ১: stdout-কে pipe-এ জোড়া।
dup2(4, 1)— fd 1 এখন pipe-এর লেখার মুখ। তারপর 3 আর 4 বন্ধ (কাজ শেষ, আর লাগবে না), তারপরexec ls. ls জানেই না যে তার “স্ক্রিন” আসলে একটি pipe. - Child ২: stdin-কে pipe-এ জোড়া।
dup2(3, 0)— fd 0 এখন pipe-এর পড়ার মুখ। 3 আর 4 বন্ধ, তারপরexec grep txt. grep ভাবছে সে কিবোর্ড থেকে পড়ছে। - Shell নিজের মুখ বন্ধ করল। Shell নিজে pipe ব্যবহার করবে না, তাই 3 ও 4 বন্ধ করে দুই child-এর জন্য অপেক্ষায় গেল। এখন লেখার মুখ শুধু ls-এর হাতে। Shell এই কাজটি ভুলে গেলে grep কখনো EOF পেত না — terminal আটকে থাকত।
- ls শেষ — EOF — grep শেষ। ls তালিকা লিখে শেষ হলো; তার fd 1 বন্ধ হতেই লেখার মুখ আর কারো হাতে নেই। grep বাকি লেখা পড়ে EOF পেল,
txtআছে এমন লাইন ছাপল, শেষ হলো। Shell দুটি exit status নিয়ে আবার prompt দেখাল।
Bell Labs-এ Doug McIlroy ১৯৬০-এর দশকেই একটি নোটে লিখেছিলেন, বাগানের হোসপাইপের মতো প্রোগ্রামগুলোকেও জোড়া লাগানো যাওয়া উচিত। কয়েক বছর ধরে তিনি এটি নিয়ে চাপ দিতে থাকেন, আর শেষে ১৯৭৩ সালে Ken Thompson Unix-এ pipe যোগ করেন — | চিহ্নসহ। এর সাথেই জন্ম নেয় বিখ্যাত “Unix দর্শন”: প্রতিটি প্রোগ্রাম একটি কাজ ভালোভাবে করবে, আর লেখার স্রোত দিয়ে অন্য প্রোগ্রামের সাথে মিলে কাজ করবে। আজকের cat log.txt | grep ERROR | sort | uniq -c সেই চিন্তারই ফল।
৭ · আরও IPC — signal, shared memory, message queue, socket
Pipe সরল ও নির্ভরযোগ্য, কিন্তু সব কাজের জন্য উপযুক্ত নয়। এটি একমুখী, সাধারণত শুধু একই পরিবারের (fork-সম্পর্কিত) প্রসেসের মধ্যে চলে, আর প্রতিটি বাইট দুবার কপি হয় — পাঠানোর প্রসেস থেকে kernel-এ, kernel থেকে গ্রহণকারী প্রসেসে। তাই আরও কয়েকটি পথ আছে:
📛 Named pipe (FIFO)
নাম থাকা pipe. mkfifo /tmp/chat দিয়ে একটি বিশেষ ফাইল তৈরি হয়; সম্পর্কহীন দুটি প্রোগ্রামও এই নাম ধরে খুলে কথা বলতে পারে। এটি named pipe.
🚨 Signal
একটি ছোট সংকেত — ডেটা নয়, শুধু “কিছু একটা ঘটেছে।” Ctrl+C চাপলে terminal চলমান প্রোগ্রামকে SIGINT পাঠায়। kill -9 1234 পাঠায় SIGKILL, যা ধরা বা উপেক্ষা করা যায় না। এটি signal.
🧠 Shared memory
একই RAM-অংশ দুই প্রসেসের ঠিকানা-জগতে জুড়ে দেওয়া। কোনো কপি নেই, তাই সবচেয়ে দ্রুত। কিন্তু দুজন একসাথে লিখলে race condition — শৃঙ্খলা নিজেকেই রাখতে হয়। এটি shared memory.
ভিডিও, ডেটাবেস, গেম ইঞ্জিন📬 Message queue
Kernel-এর রাখা একটি সারি, যেখানে গোটা গোটা বার্তা জমা হয় ও একে একে তোলা হয়। Pipe-এর মতো অবিরাম বাইটের স্রোত নয়, বার্তার সীমানা বজায় থাকে। এটি message queue.
কাজের তালিকা ভাগ করা🌐 Socket
দুই মুখী সংযোগ, যা একই কম্পিউটারে তো বটেই, ইন্টারনেট পেরিয়ে অন্য দেশের কম্পিউটারের প্রসেসের সাথেও কথা বলতে পারে। ব্রাউজার আর ওয়েব সার্ভারের কথোপকথন এভাবেই। পর্ব ৫০-এর পুরো বিষয় এটি।
নেটওয়ার্ক পেরিয়ে| পদ্ধতি | দিক | গতি | কার মধ্যে | ঝামেলা |
|---|---|---|---|---|
| Pipe | একমুখী | মাঝারি (দুবার কপি) | parent-child পরিবার | মুখ বন্ধ করার শৃঙ্খলা |
| Named pipe | একমুখী | মাঝারি | একই কম্পিউটারের যে কেউ | ফাইলের অনুমতি সামলানো |
| Signal | শুধু সংকেত | দ্রুত, কিন্তু ডেটা নেই | PID জানা যে কেউ (অনুমতি সাপেক্ষে) | handler-এ অল্প কাজই নিরাপদ |
| Shared memory | দুই দিক | সবচেয়ে দ্রুত | একই কম্পিউটার | নিজে lock/শৃঙ্খলা রাখতে হয় |
| Message queue | দুই দিক | মাঝারি | একই কম্পিউটার | সারির আকারের সীমা |
| Socket | দুই দিক | মাঝারি থেকে ধীর | নেটওয়ার্কের যেকোনো জায়গা | সংযোগ ছিন্ন, দেরি, নিরাপত্তা |
৮ · একটি mini-shell-এর নকশা, আর বাস্তব জগতে প্রসেস
এতক্ষণ যা শিখলেন, তা জুড়লেই একটি কাজ চালানোর মতো shell তৈরি হয়ে যায়। মূল লুপটি মাত্র কয়েকটি ধাপের:
চিরকাল:
prompt দেখাও ("$ ")
এক লাইন পড়ো ← fgets; EOF হলে লুপ শেষ
লাইনকে শব্দে ভাঙো ← "ls" "-l" NULL
"cd" বা "exit" হলে নিজেই করো ← built-in: child-এ করলে কাজ হবে না!
pid = fork()
child হলে: execvp(শব্দ[0], শব্দ); ব্যর্থ হলে _exit(127)
parent হলে: waitpid(pid, &status, 0); status মনে রাখো
একটি মজার প্রশ্ন: cd কেন আলাদা? কারণ বর্তমান ফোল্ডার প্রতিটি প্রসেসের নিজস্ব সম্পত্তি। Child-এ cd করলে শুধু child-এর ফোল্ডার বদলাবে, আর child শেষ হলে সেই পরিবর্তনও হারিয়ে যাবে — shell যেখানে ছিল সেখানেই থাকবে। তাই cd shell-কে নিজের প্রসেসেই করতে হয়। প্রসেসের দেয়াল কতটা বাস্তব, এটি তার সুন্দর প্রমাণ। এর সাথে pipe ও dup2 যোগ করলে |, >, < সবই সম্ভব — অনুশীলনের শেষ প্রশ্নটি এটি নিয়েই।
বাস্তব জগতে প্রসেসের ব্যবহার
🌍 Chrome — প্রতি সাইট আলাদা প্রসেস
Chrome শুরু থেকেই multi-process: browser-এর মূল অংশ এক প্রসেসে, আর ওয়েবপেজগুলো আলাদা আলাদা “renderer” প্রসেসে। একটি ট্যাব crash করলে বাকিগুলো চলে। Task Manager-এ Chrome-এর অনেকগুলো এন্ট্রি দেখার কারণ এটাই। এটি process isolation-এর বড় উদাহরণ।
নিরাপত্তা ও স্থিতিশীলতা🖥️ ওয়েব সার্ভার — worker প্রসেস
Nginx একটি master প্রসেস চালায়, যে কয়েকটি worker প্রসেস তৈরি করে — সাধারণত প্রতি CPU কোরে একটি। Apache-র “prefork” পদ্ধতি আগেই অনেকগুলো child fork করে রাখে, প্রতিটি একটি করে অনুরোধ সামলায়। একটি worker মরলে master নতুন একটি বানায়।
master + workers📱 Android — Zygote থেকে প্রতিটি অ্যাপ
Android চালু হওয়ার সময় “Zygote” নামের একটি প্রসেস দরকারি library আগেই লোড করে রাখে। আপনি কোনো অ্যাপ খুললে Zygote fork হয় — copy-on-write-এর কারণে অ্যাপ দ্রুত খোলে। প্রতিটি অ্যাপ আলাদা প্রসেসে, আলাদা ব্যবহারকারী-পরিচয়ে চলে।
fork-এর চতুর ব্যবহার🐳 Docker — বেড়া দেওয়া প্রসেস
একটি container কোনো আলাদা কম্পিউটার নয় — এটি host-এর kernel-এ চলা সাধারণ প্রসেস। Linux namespace তাকে নিজস্ব PID তালিকা, ফাইল সিস্টেম ও নেটওয়ার্ক দেখায়, আর cgroups তার CPU ও মেমরির সীমা বেঁধে দেয়।
namespaces + cgroupsখেয়াল করুন, চারটি উদাহরণেই একই নীতি: আলাদা প্রসেস মানে আলাদা দেয়াল। ভুল, crash বা আক্রমণ এক প্রসেসে আটকে থাকে। দাম হলো বেশি মেমরি আর IPC-র খরচ।
৯ · AI-এর যুগে প্রসেস-কোড — যেখানে AI সবচেয়ে বেশি ভুল করে
AI-কে “fork দিয়ে একটি প্রোগ্রাম লেখো” বললে সাধারণত এমন কোড আসে, যা প্রথমবার চালালে ঠিকঠাক দেখায়। সমস্যা ধরা পড়ে পরে — সার্ভার এক সপ্তাহ চলার পর, বড় ইনপুটে, বা কেউ অদ্ভুত ইনপুট দিলে।
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
| fork করেছে, কিন্তু wait নেই | Zombie জমে, শেষে নতুন প্রসেস তৈরি বন্ধ | প্রতিটি fork-এর জন্য একটি waitpid খুঁজুন; ps-এ Z দেখুন |
| pipe-এর অব্যবহৃত মুখ বন্ধ করেনি | পড়ার দিক EOF পায় না, প্রোগ্রাম আটকে থাকে | fork-এর পর প্রতিটি প্রসেসে কোন মুখ বন্ধ হলো, গুনে দেখুন |
| লুপে fork, কিন্তু child-এ exit নেই | child-ও লুপ চালিয়ে আবার fork করে — ৩ বার লুপে ৮টি প্রসেস, ২০ বারে ১০ লাখের বেশি | child-এর শাখা exit/_exit দিয়ে শেষ হয় কি না |
| fork-এর -1 যাচাই নেই | ব্যর্থ হলে parent ভাবে -1 একটি child; kill(-1, …) হলে বিপর্যয় | if (pid < 0) শাখা আছে কি না |
| exec ব্যর্থ হলে কিছু করেনি | child parent-এর কোড চালাতে থাকে, কাজ দুবার হয় | exec-এর ঠিক পরে perror + _exit |
fork-এর আগে fflush নেই | আউটপুট ফাইলে গেলে লেখা দুবার | ./app > out.txt করে দেখুন |
ব্যবহারকারীর ইনপুট system()-এ | Command injection — আক্রমণকারী যেকোনো কমান্ড চালাতে পারে | system/popen-এ বাইরের ইনপুট আছে কি না |
শেষ ভুলটি সবচেয়ে বিপজ্জনক, তাই আলাদা করে দেখি। ধরুন AI একটি “ping টুল” লিখে দিল:
snprintf(cmd, sizeof cmd, "ping -c 1 %s", host); /* host এসেছে ব্যবহারকারীর কাছ থেকে */
system(cmd);
ব্যবহারকারী লিখল: 8.8.8.8; rm -rf ~/projects
system() আসলে /bin/sh -c চালায়, আর shell-এর কাছে ; মানে “এরপর আরেকটি কমান্ড।” ফলে ping-এর পর ব্যবহারকারীর ফোল্ডার মুছে যাবে! এটি ওয়েবসাইটের ফর্মে থাকলে আক্রমণকারী আপনার সার্ভারেই কমান্ড চালাবে। সমাধান: shell একেবারে বাদ দিন — fork + execvp("ping", argv), যেখানে ইনপুট argv-র একটি আলাদা ঘরে বসে, ফলে ; শুধু একটি সাধারণ অক্ষর। সাথে ইনপুট যাচাই করুন (শুধু অঙ্ক আর বিন্দু)। বিস্তারিত পাবেন Cybersecurity কোর্সে।
AI-কে প্রসেস-কোড লিখতে বললে শুরুতেই নিয়মগুলো বলে দিন, আর কোডের সাথে ব্যাখ্যা চান। এতে AI-এর ভুল আপনার চোখে পড়বে:
AI-এর লেখা প্রসেস-কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১নতুন প্রোগ্রাম চালাতে Unix কেন দুটি আলাদা ডাক (fork + exec) ব্যবহার করে? একটি “spawn” ডাকই কি ভালো হতো না?
প্রথমে Unix-এর পক্ষের যুক্তি। fork আর exec-এর মাঝখানে একটি ফাঁক থাকে, যেখানে child তখনো আপনার নিজের কোড চালাচ্ছে। এই ফাঁকে সে সাধারণ system call দিয়েই নতুন প্রোগ্রামের পরিবেশ সাজিয়ে নিতে পারে:
dup2দিয়ে stdin/stdout অন্যদিকে জোড়া — redirection ও pipe.- অপ্রয়োজনীয় ফাইল বন্ধ, বর্তমান ফোল্ডার বদল, environment বদল।
- অধিকার কমানো — যেমন root থেকে সাধারণ ব্যবহারকারীতে নেমে তারপর exec.
অর্থাৎ নতুন কোনো সুবিধা লাগলে নতুন ফাংশন বানাতে হয় না — আগের ছোট ছোট system call জুড়লেই হয়। Windows-এর CreateProcess-এ ঠিক এই কাজগুলোর জন্য অনেকগুলো প্যারামিটার আর আলাদা কাঠামো লাগে।
সমালোচনা: ২০১৯ সালে Microsoft Research ও কয়েকটি বিশ্ববিদ্যালয়ের গবেষকেরা “A fork() in the road” নামে একটি আলোচিত গবেষণাপত্রে যুক্তি দেন যে fork আজকের দিনে একটি সমস্যা। তাঁদের মূল কথাগুলো মোটামুটি এমন: অনেক GB মেমরির প্রসেসে copy-on-write থাকলেও মেমরির মানচিত্র কপি করা ধীর; thread থাকা প্রোগ্রামে fork শুধু ডাকা thread-টিকে কপি করে, ফলে অন্য thread-এর ধরে রাখা lock চিরকাল আটকে থাকতে পারে; child parent-এর সব গোপন তথ্য আর মেমরির বিন্যাস উত্তরাধিকারে পায়, যা নিরাপত্তার দিক থেকে ভালো নয়; আর kernel-এর নকশাও জটিল হয়। তাঁদের পরামর্শ ছিল নতুন কোডে posix_spawn-এর মতো সরাসরি পদ্ধতি ব্যবহার করা।
বাস্তবে দুই পক্ষই কিছুটা ঠিক। ছোট টুল ও shell-এ fork + exec আজও সরল ও কার্যকর। বিশাল, বহু-thread প্রোগ্রামে (যেমন একটি browser বা ডেটাবেস) নতুন প্রসেস চালাতে posix_spawn বা Linux-এর বিশেষ ব্যবস্থা বেশি নিরাপদ।
মূল উপলব্ধি: যে নকশা ১৯৭০-এর দশকের ছোট কম্পিউটারে চমৎকার ছিল, তা আজকের বিশাল ও বহু-thread সফটওয়্যারে নতুন খরচ তৈরি করতে পারে। ভালো ইঞ্জিনিয়ার পুরোনো নকশাকে অন্ধভাবে পূজা বা বাতিল করেন না — তার অনুমানগুলো আজও সত্য কি না, সেটা যাচাই করেন।
প্র ০২Chrome প্রতিটি সাইটের জন্য আলাদা প্রসেস ব্যবহার করে, যদিও এতে অনেক বেশি RAM লাগে। কেন এই দাম দেওয়া হয়?
পুরোনো browser-গুলো সব ট্যাব একটি প্রসেসে চালাত। একটি পাতার JavaScript বা plugin crash করলে পুরো browser বন্ধ হয়ে যেত, সব ট্যাবসহ। ২০০৮ সালে Chrome শুরু থেকেই ভিন্ন পথ নেয় — browser-এর মূল অংশ এক প্রসেসে, আর ওয়েবপেজ আঁকার কাজ আলাদা “renderer” প্রসেসে।
- স্থিতিশীলতা: একটি renderer crash করলে শুধু সেই ট্যাবে “Aw, Snap!” দেখায়, বাকিগুলো চলে।
- নিরাপত্তার স্যান্ডবক্স: Renderer প্রসেস খুব কম অধিকার নিয়ে চলে — সরাসরি ফাইল খুলতে বা নেটওয়ার্কে যেতে পারে না, সব কাজ IPC দিয়ে মূল প্রসেসের কাছে চাইতে হয়। কোনো ক্ষতিকর পাতা renderer-এর বাগ কাজে লাগালেও সে একটি খাঁচায় আটকে থাকে।
- সাইট আলাদা রাখা: ২০১৮ সালে Spectre নামের CPU-র দুর্বলতা প্রকাশিত হয়, যাতে একই প্রসেসের ভেতরের অন্য মেমরি গোপনে পড়া সম্ভব। এর পরে Chrome ডেস্কটপে “Site Isolation” চালু করে — ভিন্ন ভিন্ন সাইট ভিন্ন প্রসেসে, যাতে একটি ক্ষতিকর সাইট আপনার ব্যাংকের পাতার মেমরি একই প্রসেসে না পায়।
দাম: প্রতিটি প্রসেসের নিজস্ব মেমরি লাগে, তাই RAM বেশি খরচ হয়। কম মেমরির ফোনে Chrome সব সাইটকে আলাদা না-ও করতে পারে, শুধু বেশি সংবেদনশীল সাইটগুলোকে আলাদা রাখে।
মূল উপলব্ধি: প্রসেস হলো অপারেটিং সিস্টেমের দেওয়া সবচেয়ে শক্ত দেয়াল। যখন ভুল বা আক্রমণের দাম অনেক বেশি, তখন বাড়তি মেমরি দিয়ে সেই দেয়াল কেনা লাভজনক।
প্র ০৩বলা হয় “container আসলে বেড়া দেওয়া প্রসেস মাত্র।” কথাটির মানে কী, আর virtual machine-এর সাথে পার্থক্য কোথায়?
Docker দিয়ে একটি container চালানোর পর host কম্পিউটারে ps -ef চালালে দেখবেন, container-এর ভেতরের প্রোগ্রামটি সেখানে একটি সাধারণ প্রসেস হিসেবেই আছে — নিজস্ব PID সহ। আলাদা কোনো কম্পিউটার বা আলাদা kernel নেই। তাহলে container-এর ভেতর থেকে সব আলাদা মনে হয় কেন? কারণ kernel কয়েকটি বেড়া দিয়ে রাখে:
- PID namespace: container-এর প্রথম প্রসেস নিজেকে PID 1 হিসেবে দেখে, host-এর বাকি প্রসেস তার চোখে পড়েই না — যদিও host-এর চোখে তার PID হয়তো 48213.
- Mount namespace: নিজস্ব ফাইল সিস্টেম দেখায় — যেন পুরো একটি আলাদা Linux.
- Network namespace: নিজস্ব নেটওয়ার্ক কার্ড ও IP ঠিকানা।
- cgroups: CPU, মেমরি ও প্রসেসের সংখ্যার সীমা — একটি container পুরো সার্ভারের RAM খেয়ে ফেলতে পারে না।
Virtual machine-এর সাথে পার্থক্য: VM-এ সম্পূর্ণ আলাদা একটি অপারেটিং সিস্টেম তার নিজের kernel নিয়ে চলে, নকল হার্ডওয়্যারের ওপর। তাই VM চালু হতে বেশি সময় ও মেমরি লাগে, কিন্তু দেয়াল অনেক মোটা। Container একই kernel ভাগ করে, তাই খুব হালকা ও দ্রুত চালু হয় — একটি সার্ভারে শত শত container চলতে পারে।
নিরাপত্তার দিক: সবাই একই kernel ভাগ করে বলে kernel-এর কোনো গুরুতর বাগ কাজে লাগিয়ে container থেকে বেরিয়ে host-এ ঢোকার (container escape) ঝুঁকি থাকে। তাই বড় cloud প্রতিষ্ঠানগুলো অপরিচিত গ্রাহকের কোড চালাতে প্রায়ই container-কে আবার একটি হালকা VM-এর ভেতরে রাখে — দুই স্তরের দেয়াল।
মূল উপলব্ধি: নতুন প্রযুক্তি প্রায়ই পুরোনো ধারণার নতুন বিন্যাস। এই পর্বের প্রসেস, PID, file descriptor বুঝলেই আপনি Docker-এর ভেতরে কী ঘটছে, তার অর্ধেক বুঝে গেছেন।
প্র ০৪Fork bomb কী, এটি কীভাবে একটি সার্ভার অচল করে, আর অপারেটিং সিস্টেম কীভাবে নিজেকে রক্ষা করে?
Fork bomb হলো এমন একটি প্রোগ্রাম, যা একটি অসীম লুপে fork করতে থাকে — আর প্রতিটি child-ও একই লুপ চালায়। প্রতি ধাপে প্রসেসের সংখ্যা দ্বিগুণ: ১, ২, ৪, ৮… মাত্র ৩০ ধাপে একশো কোটির বেশি। বাস্তবে তার অনেক আগেই process table ভরে যায়, CPU-র সময় এত টুকরো হয় যে আসল কাজ চলে না, নতুন login বা কমান্ডও চালানো যায় না, কারণ সেগুলোর জন্যও তো নতুন প্রসেস লাগবে! অনেক সময় একমাত্র উপায় কম্পিউটার রিস্টার্ট করা।
ইচ্ছাকৃত আক্রমণ ছাড়াও এটি দুর্ঘটনায় ঘটে। সবচেয়ে সাধারণ কারণ: লুপের ভেতরে fork, কিন্তু child-এর শাখায় exit নেই। Child তখন লুপের বাকি অংশ চালিয়ে নিজেও fork করে। AI-এর লেখা “১০টি worker তৈরি করো” কোডে এই ভুল প্রায়ই দেখা যায় — ১০টির বদলে ১০২৪টি প্রসেস!
প্রতিরক্ষা:
ulimit -u: প্রতিটি ব্যবহারকারী একসাথে সর্বোচ্চ কতগুলো প্রসেস চালাতে পারবে, তার সীমা। সীমা পূর্ণ হলে fork -1 ফেরত দেয় (errnoEAGAIN), ফলে বোমা নিজেই থেমে যায়, আর অন্য ব্যবহারকারীরা (বিশেষ করে admin) কাজ চালিয়ে যেতে পারেন।- cgroups-এর pids সীমা: container বা সার্ভিস ধরে প্রসেসের সংখ্যা বেঁধে দেওয়া। systemd-এর
TasksMaxসেটিংও এর ওপর দাঁড়িয়ে। - ভালো কোড: fork-এর -1 সবসময় যাচাই করা, আর child-কে নির্দিষ্ট পথে শেষ করা।
এটি একরকম denial of service — কিছু চুরি হয় না, কিন্তু সবার জন্য সেবা বন্ধ হয়ে যায়।
মূল উপলব্ধি: যেকোনো ভাগ করা সম্পদের — CPU, মেমরি, PID — একটি সীমা থাকা উচিত। ভালো সিস্টেম ধরে নেয় যে কোনো একটি প্রোগ্রাম একদিন ভুল করবেই, আর সেই ভুল যেন সবার সমস্যা না হয়, সেভাবে সাজানো থাকে।
প্র ০৫দুই প্রসেসের মধ্যে ডেটা পাঠাতে pipe, সাধারণ ফাইল আর shared memory — কখন কোনটি বেছে নেবেন?
তিনটিই কাজ করে, কিন্তু প্রত্যেকে আলাদা জিনিসের বিনিময়ে আলাদা সুবিধা দেয়।
- ফাইল: একটি প্রসেস লিখে রাখল, অন্যটি পরে পড়ল। সুবিধা — ডেটা থেকে যায়, কম্পিউটার বন্ধ হলেও; দুই প্রসেসকে একই সময়ে চলতেও হয় না। অসুবিধা — ডিস্ক ধীর, আর “লেখা কি শেষ হয়েছে?” সেটা বোঝার জন্য আলাদা ব্যবস্থা লাগে।
- Pipe: Kernel-এর মেমরিতে একটি ছোট buffer. সবচেয়ে বড় সুবিধা — শৃঙ্খলা নিজে থেকেই আসে। Buffer খালি হলে পাঠক অপেক্ষা করে, ভরে গেলে লেখক অপেক্ষা করে, লেখক চলে গেলে পাঠক EOF পায়। তাই দ্রুত লেখক ধীর পাঠককে ডুবিয়ে দিতে পারে না। অসুবিধা — একমুখী, প্রতিটি বাইট দুবার কপি হয়, আর ডেটা থেকে যায় না।
- Shared memory: একই RAM দুজনের চোখে। কোনো কপি নেই, তাই বিশাল ডেটার জন্য (যেমন ভিডিওর ফ্রেম, ডেটাবেসের cache) সবচেয়ে দ্রুত। অসুবিধা — শৃঙ্খলা পুরোপুরি আপনার দায়িত্ব। একজন লিখতে লিখতে অন্যজন পড়ে ফেললে আধা-নতুন আধা-পুরোনো ডেটা পাবে — race condition. এজন্য lock বা semaphore লাগে, যা পরের পর্বে thread-এর সাথে দেখবেন।
বাস্তব সিস্টেম প্রায়ই এগুলো মেশায়। যেমন বড় ডেটা shared memory-তে রেখে, “নতুন ডেটা তৈরি” — এই ছোট খবরটি একটি pipe বা socket দিয়ে পাঠানো। আর যদি প্রসেস দুটি আলাদা কম্পিউটারে থাকে, তাহলে socket ছাড়া উপায় নেই — পর্ব ৫০-এর বিষয়।
মূল উপলব্ধি: যোগাযোগের নকশায় তিনটি প্রশ্ন করুন — কত ডেটা, কত দ্রুত, আর শৃঙ্খলা কে রাখবে? যে পদ্ধতি শৃঙ্খলার দায় যত বেশি নিজে নেয়, সেটি তত নিরাপদ কিন্তু তত ধীর।
অনুশীলন
এগুলোর বেশিরভাগে কোড লিখতে হবে না — হাতে-কলমে দেখা আর ভাবাই আসল কাজ।
-
প্রসেসের গাছ খুঁজুন: Linux/Mac-এ (বা WSL-এ) একটি terminal খুলে
echo $$লিখুন, তারপরsleep 100 &চালিয়েps -o pid,ppid,cmdদেখুন।sleep-এর PPID কত? Windows-এ Task Manager-এর Details ট্যাবে Chrome-এর কতগুলো এন্ট্রি আছে, গুনে দেখুন।sleep-এর PPID হবে ঠিকecho $$-এ দেখা নম্বরটি — অর্থাৎ আপনার shell. Shell fork করে, child-এsleepexec করেছে;&থাকায় এবার shell অপেক্ষা না করে সাথে সাথে prompt দিয়েছে। আর Chrome-এর এন্ট্রি সাধারণত খোলা ট্যাবের সংখ্যার চেয়েও বেশি — browser-এর মূল প্রসেস, GPU প্রসেস, নেটওয়ার্ক প্রসেস, আর সাইট অনুযায়ী আলাদা renderer প্রসেস। -
Buffer-এর ফাঁদ নিজের চোখে: প্রথম কোড থেকে
fflush(stdout);মুছে চালান। কয়বারBefore forkদেখলেন? নিজের কম্পিউটারে terminal-এ সরাসরি চালালে, আর./fork_wait > out.txtকরে ফাইলে পাঠালে — ফল আলাদা কেন?এই পাতার রানারে দুবার দেখবেন। Terminal-এ stdout সাধারণত line-buffered —
\nপেলেই ছাপা হয়, তাই fork-এর আগেই buffer খালি, আর লাইনটি একবার আসে। ফাইল বা pipe-এ stdout পুরোপুরি buffered — লেখা fork-এর সময়ও মেমরিতে বসে থাকে, fork সেটি কপি করে, আর দুই প্রসেস শেষে নিজের নিজের কপি লিখে দেয়। তাই ফাইলে দুবার। শিক্ষা: একই প্রোগ্রাম আউটপুট কোথায় যাচ্ছে তার ওপর নির্ভর করে ভিন্ন আচরণ করতে পারে — fork-এর আগে সবসময়fflush. -
গুনে বলুন: কোনো error ছাড়া নিচের কোড চললে
Hiকয়বার ছাপা হবে?fork(); fork(); fork(); printf("Hi\n");— আর যদি এটিfor (int i = 0; i < 3; i++) { fork(); }লুপে থাকে, তাহলে মোট কয়টি প্রসেস তৈরি হয়?প্রতিটি fork চলমান প্রতিটি প্রসেসকে দুটিতে পরিণত করে। প্রথম fork-এর পর ২টি, দ্বিতীয়টির পর ৪টি, তৃতীয়টির পর ৮টি প্রসেস — প্রত্যেকে একবার
Hiছাপে, তাই ৮ বার। লুপের ক্ষেত্রেও একই: শেষে মোট ৮টি প্রসেস (মূলটিসহ), অর্থাৎ ৭টি নতুন। লুপ ৩-এর বদলে ২০ বার ঘুরলে ২২০ — দশ লাখের বেশি! এজন্যই লুপে fork করলে child-এর শাখা অবশ্যইexitদিয়ে শেষ করতে হয়। -
আটকে যাওয়া pipe (কোড লেখার কাজ): দ্বিতীয় কোডটি বদলে উল্টো দিকে কথা বলান — parent লিখবে, child পড়ে ছাপবে। তারপর ইচ্ছা করে child-এর
close(fd[1])বাদ দিন এবং পড়ার লুপটি সীমাহীন করুন। কী ঘটে, আর কেন?সঠিক সংস্করণে parent
fd[0]বন্ধ করে, বার্তা লেখে,fd[1]বন্ধ করে, তারপরwaitpidকরে। Childfd[1]বন্ধ করে, EOF পর্যন্ত পড়ে, ছাপে,_exit(0)করে। এবার child-এরclose(fd[1])বাদ দিলে child নিজের হাতেই একটি লেখার মুখ খোলা রাখল। Parent লিখে বন্ধ করলেও kernel দেখে একটি লেখার মুখ এখনো খোলা — তাই child-এরreadকখনো 0 পায় না, চিরকাল অপেক্ষা করে। আর parent অপেক্ষা করছে child-এর জন্য। দুজনেই একে অপরের জন্য আটকে — একটি deadlock, যা কোনো error মেসেজ দেয় না, শুধু থেমে থাকে। এই চুপচাপ থেমে থাকা ভুলটিই AI-এর কোডে সবচেয়ে বেশি দেখা যায়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমShell একটি কমান্ড চালানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ⌨️ ব্যবহারকারীর লেখা লাইন পড়া ও শব্দে ভাঙা
- 🧬
fork()— নিজের একটি কপি তৈরি - 🔄 Child-এ
execvp()— নতুন প্রোগ্রাম বসানো - 📋 নতুন প্রোগ্রাম কাজ করে
exitকরে - ✅ Parent-এর
waitpid()ফিরে আসে, আবার prompt
প্রোগ্রাম আর প্রসেসের সম্পর্ক কোনটি?
- প্রসেস ডিস্কে থাকে, প্রোগ্রাম মেমরিতে চলে
- প্রোগ্রাম হলো ফাইল; সেটি চালালে প্রসেস তৈরি হয়, আর একই প্রোগ্রামের অনেক প্রসেস একসাথে চলতে পারে
- একটি প্রোগ্রাম থেকে একসাথে কেবল একটিই প্রসেস চলতে পারে
- দুটি একই জিনিসের দুটি নাম
pid_t pid = fork(); সফল হলে child প্রসেসে pid-এর মান কত?
- child-এর নিজের PID
- parent-এর PID
- 0
- -1
execvp("ls", argv) সফল হলে কী ঘটে?
- একই প্রসেসে (একই PID) ls বসে যায়, আর execvp কখনো ফেরে না
- একটি নতুন PID-সহ নতুন প্রসেস তৈরি হয়, আগেরটিও চলতে থাকে
- ls শেষ হলে execvp-এর পরের লাইন থেকে আগের প্রোগ্রাম চলতে থাকে
- সব খোলা file descriptor বন্ধ হয়ে যায়
একটি সার্ভার চলার কয়েক দিন পর ps-এ শত শত <defunct> এন্ট্রি দেখা গেল। সবচেয়ে সম্ভাব্য কারণ?
- Child প্রসেসগুলো kill করা হয়নি
- Pipe-এর মুখ বন্ধ করা হয়নি
- Parent প্রসেস crash করেছে
- Parent প্রসেস শেষ হওয়া child-দের জন্য waitpid ডাকছে না
Parent pipe থেকে পড়ছে, child লিখে বেরিয়ে গেছে, তবু parent-এর read চিরকাল অপেক্ষা করছে। কারণ কী?
- Pipe-এর buffer ভরে গেছে
- Parent নিজের হাতে থাকা pipe-এর লেখার মুখ (fd[1]) বন্ধ করেনি
- Child exit করলেও তার fd বন্ধ হয় না
- Pipe একমুখী, তাই parent পড়তে পারে না
AI একটি ওয়েব টুলে লিখেছে: sprintf(cmd, "ping %s", user_input); system(cmd); — সবচেয়ে বড় সমস্যা কী?
- ping কমান্ড ধীর
- system() zombie তৈরি করে
- Command injection — ব্যবহারকারী ; দিয়ে নিজের কমান্ড জুড়ে চালাতে পারে
- কোড compile-ই হবে না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- প্রোগ্রাম = ফাইল, প্রসেস = চলমান রূপ। প্রতিটি প্রসেসের নিজস্ব PID, মেমরি আর file descriptor তালিকা; kernel সবাইকে সামলায়।
fork()ফেরে দুবার: child-এ 0, parent-এ child-এর PID, ব্যর্থ হলে -1. Copy-on-write একে দ্রুত করে; fork-এর আগেfflush.execপ্রোগ্রাম বদলায়, PID নয়; সফল হলে ফেরে না। Shell = পড়ো → fork → exec → wait.- যে fork করে, সে-ই wait করে — নইলে zombie. Parent আগে মরলে child অনাথ, তাকে init দত্তক নেয়।
- Pipe একমুখী; অব্যবহৃত মুখ বন্ধ না করলে EOF আসে না।
dup2দিয়ে stdin/stdout জোড়া লাগে। বাইরের ইনপুট কখনোsystem()-এ নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৯ · মাল্টিথ্রেডিং — একসাথে অনেক কাজপরবর্তী পর্বএকই প্রসেসের ভেতরে একাধিক thread, ভাগ করা মেমরি আর তার শৃঙ্খলা।
- C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Operating Systemsগভীরে যানProcess scheduling, virtual memory, copy-on-write আর kernel-এর ভেতরের নকশা।
- Cybersecurityপাশের ট্র্যাকCommand injection, sandbox আর প্রসেসের অধিকার কমিয়ে নিরাপদ রাখার কৌশল।
- Cloud & DevOpsপাশের ট্র্যাকDocker container, namespace ও cgroups — প্রসেস থেকে cloud পর্যন্ত।