প্রসেস ও সিগন্যাল
একটা Linux প্রসেস আসলে কী, কীভাবে fork আর exec গোটা মহাবিশ্ব বানায়, আর যে সিগন্যালগুলো ঠিক করে আপনার অ্যাপ পরিষ্কারভাবে বন্ধ হবে নাকি চিৎকার করতে করতে মরবে।
বাস্তব জীবনের উপমা
প্রসেস আর সিগন্যাল অনেকটা একটা অফিসের কর্মচারীদের মতো — আপনি দেখতে পারেন কে কাজ করছে, কাউকে থামতে বলে একটা বার্তা পাঠাতে পারেন, আর সে উপেক্ষা করলে একটা কড়া সিগন্যাল দিয়ে চাপ বাড়াতে পারেন।
গল্পে বুঝি
ফাতিমা আল-ফিহরি একটা বড় কারখানার ম্যানেজার। ফ্লোরে অনেক কর্মী কাজ করছে, প্রত্যেকের বুকে একটা করে ব্যাজ নম্বর ঝোলানো — ব্যাজ ১২৩৪ কাঠ কাটছে, ব্যাজ ১৫৬৭ হিসাবের খাতা লিখছে, ব্যাজ ৮৯০ যন্ত্র চালাচ্ছে। একজন কর্মীকে কাজ থেকে ছুটি দিতে হলে ফাতিমা প্রথমে একটা ভদ্র চিরকুট পাঠায় — “হাতের কাজটা শেষ করে গুছিয়ে বেরিয়ে যান।” কর্মী তখন যন্ত্রটা নিরাপদে বন্ধ করে, খাতার লাইনটা শেষ করে, হাতিয়ার জায়গামতো রেখে, পরিষ্কারভাবে বেরিয়ে যায়। কোনো কাজ অসম্পূর্ণ থাকে না, কিছু নষ্টও হয় না।
কিন্তু মাঝেমধ্যে এমন হয় — কোনো কর্মী পুরোপুরি জমে গেছে, চিরকুট পাঠানোর পরেও নড়ছে না, কোনো সাড়া নেই। তখন আর অপেক্ষা করে লাভ নেই। ফাতিমা সিকিউরিটি ডাকে, আর তারা লোকটাকে কাজের মাঝখান থেকেই টেনে বের করে দেয় — হাতিয়ার মেঝেতে পড়ে থাকে, খাতা খোলা থাকে, গোছানোর কোনো সুযোগই থাকে না। কাজ হয়ে যায়, কিন্তু পেছনে একটা অগোছালো অবস্থা রেখে যায়।
এই গল্পটাই আসলে process আর signal। প্রতিটা কর্মী তার ব্যাজ নম্বর নিয়ে হলো একটা process, যার PID। ভদ্র চিরকুট “গুছিয়ে বেরিয়ে যান” হলো SIGTERM — process নিজের কাজ শেষ করে, connection বন্ধ করে, buffer flush করে পরিষ্কারভাবে বেরোয়, অর্থাৎ graceful shutdown। আর জমে-যাওয়া কর্মীকে সিকিউরিটি দিয়ে টেনে বের করা হলো SIGKILL — কার্নেল process-কে সঙ্গে সঙ্গে মেরে ফেলে, গোছানোর কোনো সুযোগ ছাড়াই। বাস্তবে তাই সবসময় আগে SIGTERM পাঠান, কয়েক সেকেন্ড অপেক্ষা করুন; process সত্যিই আটকে না গেলে SIGKILL-এ যাবেন না, নাহলে অসম্পূর্ণ transaction, করাপ্ট ফাইল আর পড়ে থাকা lock-এর মতো ঝামেলা হতে পারে।
একটা প্রসেস আপনার অ্যাপ নয়
একটা প্রসেস হলো একটা চলমান প্রোগ্রামের বর্ণনা দেওয়া একটা কার্নেল ডেটা স্ট্রাকচার: এর মেমরি, এর ফাইল ডেসক্রিপ্টর, এর ক্রেডেনশিয়াল, এক্সিকিউট করার পরের ইনস্ট্রাকশনের অ্যাড্রেস। আপনার Go বাইনারি, আপনার Node সার্ভার, আপনার Postgres — সবই প্রসেস। কার্নেল একই সঙ্গে কয়েকশো বা কয়েক হাজার প্রসেস ম্যানেজ করে, মাইক্রোসেকেন্ডে মাইক্রোসেকেন্ডে সেগুলোকে আপনার CPU কোরে শিডিউল করে।
একটা দেখুন:
$ ps -ef | head
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 09:12 ? 00:00:01 /sbin/init
root 2 0 0 09:12 ? 00:00:00 [kthreadd]
deploy 1234 1233 0 10:42 pts/0 00:00:00 -bash
deploy 1567 1234 0 10:43 pts/0 00:00:00 ps -ef পাঁচটা কলাম গুরুত্বপূর্ণ:
- PID — process ID, প্রসেস জীবিত থাকা অবস্থায় একটা ইউনিক সংখ্যা।
- PPID — parent PID।
init(PID 1) ছাড়া প্রতিটা প্রসেসের একটা parent আছে। - TTY — এটা যে টার্মিনালে যুক্ত, বা কোনোটা না থাকলে
?। - TIME — প্রসেস যত মোট CPU টাইম খরচ করেছে।
- CMD — কমান্ড লাইন।
প্রসেস কীভাবে জন্মায় — fork আর exec
একটা প্রসেস বানানোর Linux-এ একটাই উপায়: fork()। কার্নেল একটা বিদ্যমান প্রসেস নেয়, তার একটা হুবহু কপি বানায় (একই মেমরি, একই ফাইল ডেসক্রিপ্টর, একই সবকিছু), আর কপিটাকে একটা নতুন PID দেয়। দুটো কপিই fork() কল থেকে রিটার্ন করে — parent পায় child-এর PID, child পায় 0। সেখান থেকে তারা আলাদা হয়ে যায়।
একটা ভিন্ন প্রোগ্রাম চালাতে, child সঙ্গে সঙ্গে exec() কল করে, যা নিজের প্রোগ্রাম টেক্সট আর মেমরি নতুন বাইনারি দিয়ে প্রতিস্থাপন করে। তাই আপনি যখন টাইপ করেন:
ls /etc আসলে যা ঘটে:
- আপনার শেল (
bash)fork()কল করে। এখন দুটো bash আছে। - child bash
exec("/usr/bin/ls", ["ls", "/etc"])কল করে। এর মেমরিlsদিয়ে প্রতিস্থাপিত হয়। - parent bash
wait()কল করে আর child বের হওয়া পর্যন্ত থেমে থাকে। lsচলে, প্রিন্ট করে, বের হয়।- parent bash আবার শুরু হয় আর আপনাকে একটা প্রম্পট দেখায়।
এটাই গোটা Unix। প্রতিটা কমান্ড, প্রতিটা daemon, প্রতিটা systemctl start nginx — fork আর exec।
একটা কলের বদলে fork+exec কেন?
কারণ fork আর exec-এর মাঝখানে, child-এর জিনিসপত্র সেট আপ করার একটা সুযোগ থাকে — উত্তরাধিকারসূত্রে পাওয়া ফাইল ডেসক্রিপ্টর বন্ধ করা, stdin/stdout রিডাইরেক্ট করা, ইউজার বদলানো, এনভায়রনমেন্ট ভ্যারিয়েবল সেট করা। পাইপ, রিডাইরেক্ট, আর sudo — সবই এই ফাঁকটার জন্য কাজ করে।
init প্রসেস — PID 1
PID 1 বিশেষ। কার্নেল একে একদম প্রথম userspace প্রসেস হিসেবে শুরু করে। আধুনিক Linux-এ সেটা systemd। PID 1-এর কাজ:
- অন্য প্রতিটা প্রসেসের পূর্বপুরুষ হওয়া।
- অনাথ প্রসেস দত্তক নেওয়া (একটা parent মারা গেলে, child-দের PPID হয়ে যায় 1)।
- জম্বি প্রসেস reap করা (এ নিয়ে আরও নিচে)।
- বুট সিকোয়েন্স চালানো — ডিস্ক, নেটওয়ার্ক, সার্ভিস চালু করা।
PID 1 মারা গেলে, কার্নেল panic করে। এজন্যই systemd নিজের ক্র্যাশ নিয়ে এত সতর্ক।
প্রসেস স্টেট
$ ps -axo pid,state,comm | head
PID S COMMAND
1 S systemd
2 S kthreadd
12 I rcu_sched
1234 S bash
1567 R ps একক-অক্ষরের স্টেট:
| State | মানে |
|---|---|
R | Running বা runnable — সক্রিয়ভাবে একটা CPU ব্যবহার করছে বা একটার জন্য অপেক্ষা করছে। |
S | Interruptibly sleeping — I/O, একটা সিগন্যাল, বা একটা নেটওয়ার্ক ইভেন্টের জন্য অপেক্ষা করছে। |
D | Uninterruptibly sleeping — সাধারণত ডিস্কের জন্য অপেক্ষা করছে। kill করা যায় না। |
Z | Zombie — বের হয়ে গেছে, কিন্তু parent এখনো এর exit code সংগ্রহ করেনি। |
T | Stopped — Ctrl+Z দিয়ে বা একটা সিগন্যাল দিয়ে থামানো। |
I | Idle কার্নেল থ্রেড। |
মিনিটের পর মিনিট আটকে থাকা একটা D-স্টেট প্রসেস সাধারণত মানে ডিস্ক মরে যাচ্ছে বা নেটওয়ার্ক ফাইলসিস্টেম নাগালের বাইরে। আপনি এটাকে kill -9 করতে পারবেন না; কার্নেল একে ধরে রেখেছে। একমাত্র সমাধান হলো অন্তর্নিহিত I/O ঠিক করা বা রিবুট করা।
প্রসেস দেখার টুল
ps -ef # POSIX-ish, every process
ps auxf # BSD-style, with a tree
ps -axo pid,user,%cpu,%mem,comm # custom columns
top # live, sortable
htop # nicer top, with colors and tree view
# apt install htop
pgrep nginx # PIDs matching a name
pidof nginx # similar, single-line output
pstree # the whole family tree htop-ই আপনি দশবারের নয়বার হাতে তুলে নেন:
sudo apt install htop
htop tree view-এর জন্য t চাপুন, রিফ্রেশের জন্য F5, একটা সিগন্যাল পাঠাতে F9, বের হতে q।
সিগন্যাল — প্রসেস কীভাবে কার্নেল আর একে অপরের সঙ্গে কথা বলে
একটা সিগন্যাল হলো একটা প্রসেসে পাঠানো এক-বাইটের বার্তা। কার্নেল এটা ডেলিভার করে; প্রসেস হয় একটা handler চালায়, উপেক্ষা করে, বা মরে যায়। এদের প্রায় তিরিশটা আছে:
$ kill -l | head -3
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM আপনি আসলে যেগুলো ব্যবহার করেন:
| Signal | Number | যা চায় |
|---|---|---|
SIGTERM | 15 | “দয়া করে পরিষ্কারভাবে বন্ধ হও।” kill-এর ডিফল্ট। |
SIGINT | 2 | “ইউজার Ctrl+C চেপেছে।” |
SIGHUP | 1 | “তোমার config রিলোড করো।” (মূলত: টার্মিনাল hang up হয়েছিল।) |
SIGUSR1 / SIGUSR2 | 10 / 12 | অ্যাপ্লিকেশন-নির্ধারিত। |
SIGKILL | 9 | “এখনই মরো।” ধরা বা উপেক্ষা করা যায় না। |
SIGSTOP | 19 | “থামো।” ধরা যায় না। |
SIGCONT | 18 | “থামার পর আবার শুরু করো।” |
SIGCHLD | 17 | একটা child বের হলে parent-কে পাঠানো হয়। |
একটা সিগন্যাল পাঠান:
kill 1234 # SIGTERM by default
kill -TERM 1234 # explicit
kill -HUP 1234 # ask nginx to reload its config
kill -9 1234 # SIGKILL — only when the app is wedged
killall -HUP nginx # by name, all matching processes
pkill -f 'node server' # by command-line pattern SIGTERM বনাম SIGKILL — সবচেয়ে গুরুত্বপূর্ণ পার্থক্য
SIGTERM catchable। একটা ভালোভাবে লেখা অ্যাপ একটা handler ইনস্টল করে যা:
- নতুন রিকোয়েস্ট নেওয়া বন্ধ করে।
- চলমান রিকোয়েস্ট শেষ করে।
- ডেটাবেস কানেকশন বন্ধ করে, বাফার flush করে, PID ফাইল unlink করে।
- code 0 দিয়ে বের হয়।
SIGKILL catchable নয়। কার্নেল প্রসেসটাকে সঙ্গে সঙ্গে kill করে, পরিষ্কার করার কোনো সুযোগ ছাড়াই। চলমান ট্রানজ্যাকশন মরে যায়। খোলা ফাইল করাপ্ট হয়ে যেতে পারে। মেমরিতে ধরে রাখা লক চলে যায় কিন্তু ডেটাবেস বা ফাইলে ধরে রাখা লক থেকে যায়।
সবসময় আগে SIGTERM পাঠান। কয়েক সেকেন্ড অপেক্ষা করুন। প্রসেস সত্যিই আটকে থাকলে তবেই SIGKILL-এ চাপ বাড়ান।
# Graceful shutdown attempt:
kill 1234
sleep 5
kill -0 1234 2>/dev/null && kill -9 1234 systemd ঠিক এটাই করে। ডিফল্টভাবে এটা SIGTERM পাঠায়, TimeoutStopSec সেকেন্ড (ডিফল্ট 90) অপেক্ষা করে, তারপর SIGKILL পাঠায়।
আপনার অ্যাপে সিগন্যাল catch করা
প্রতিটা ভাষা সিগন্যাল handler ইনস্টল করতে পারে। এই যে Go-তে:
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGTERM, syscall.SIGINT)
defer stop()
log.Println("started, pid", os.Getpid())
<-ctx.Done()
log.Println("shutdown signal received, draining...")
// Pretend we have in-flight work to finish.
time.Sleep(2 * time.Second)
log.Println("clean exit")
} systemctl stop myservice চললে, এই বাইনারিটা SIGTERM পায়, drain করে, আর পরিষ্কারভাবে বের হয়। handler না থাকলে, সিগন্যাল পৌঁছানোর মুহূর্তেই প্রসেস মরে যায়।
জম্বি আর অনাথ
একটা প্রসেস বের হলে, parent তাদের সংগ্রহ করতে wait() কল না করা পর্যন্ত এর PID আর exit code কার্নেলে ঝুলে থাকে। parent যদি অসাবধান হয় আর কখনো wait() কল না করে, child একটা জম্বি হিসেবে থেকে যায় — ps-এ Z স্টেটে দেখা যায়। জম্বি কার্নেল struct-এর বাইরে CPU বা মেমরি খায় না, কিন্তু PID খায়। দশ লাখ জম্বির লিক PID টেবিল নিঃশেষ করবে আর বক্স fork করতে অস্বীকার করবে।
parent যদি child-এর আগে মারা যায়, child অনাথ হয়ে PID 1-এ re-parent হয়। systemd এদের সঠিকভাবে reap করে, তাই অনাথরা সাধারণত ঠিক থাকে। জম্বি হলো একটা parent-প্রসেস বাগ।
Foreground, background, jobs
একটা ইন্টারঅ্যাক্টিভ শেলে:
sleep 100 # foreground — your prompt is gone until it finishes
^Z # SIGSTOP — pauses it, prompt comes back
jobs # [1]+ Stopped sleep 100
bg # resume in background
fg # bring back to foreground
disown # detach from the shell so it survives logout আসল দীর্ঘ-চলা daemon-এর জন্য, nohup আর disown-এর উপর ভরসা করবেন না। systemd ব্যবহার করুন (পরের চ্যাপ্টার)।
রিক্যাপ
- একটা প্রসেস একটা কার্নেল অবজেক্ট: মেমরি, ফাইল ডেসক্রিপ্টর, ক্রেডেনশিয়াল, একটা PID।
- নতুন প্রসেস আসে
fork(কপি) আরexec(প্রতিস্থাপন) থেকে। PID 1 সবার পূর্বপুরুষ। - প্রসেস স্টেট গুরুত্বপূর্ণ —
Dভয়ংকরটা,Zএকটা parent-প্রসেস বাগ। SIGTERMভদ্রভাবে অনুরোধ করে।SIGKILLনিয়ে আপস চলে না। সবসময় আগেSIGTERMচেষ্টা করুন।- পরিষ্কারভাবে drain করতে আপনার অ্যাপে সিগন্যাল catch করুন। ধরা না-পড়া সিগন্যাল = নোংরা shutdown।
পরবর্তী: systemd — যে সুপারভাইজার “আমার একটা বাইনারি আছে” কে “এই জিনিসটা চিরকাল up” এ পরিণত করে।