এই পর্বে যা শিখবেন
- Shell-এর ভেতরের লুপ, আর কেন কিছু কমান্ড shell-এর ভেতরেই চলতে বাধ্য — নিজের হাতে C-তে একটি ছোট shell চালিয়ে
- Foreground/background job,
jobs,fg,bg,wait, Ctrl+C আর Ctrl+Z-এর পেছনের যন্ত্রপাতি - Process group, session, controlling terminal — SSH logout-এ job কেন মরে, আর
nohup,disown, daemon দিয়ে কীভাবে বাঁচাবেন ps,pstree,top/htop-এর প্রতিটি কলাম আর load average-এর সঠিক মানে/procদিয়ে kernel-এর ভেতরে উঁকি,kill/nice-এর ভদ্র ব্যবহার, আর Windows-এ একই কাজ
১ · Shell আসলে একটি লুপ — পড়া, ভাঙা, fork, exec, অপেক্ষা
পর্ব ০৩-এ আপনি terminal-এ কমান্ড চালিয়েছেন, আর একটি pipeline-এর ভেতরে shell কীভাবে fd জোড়ে, তা দেখেছেন। পর্ব ১০-এ দেখেছেন fork, exec আর wait — process তৈরি আর শেষের তিনটি মূল system call. এই পর্বে দুটি জোড়া লাগাব: shell নিজে কীভাবে এই তিনটি দিয়ে তৈরি, আর একসাথে অনেক process চললে আপনি সেগুলো কীভাবে দেখবেন, থামাবেন, আবার চালাবেন।
যেকোনো shell — bash, zsh, dash, এমনকি পর্ব ৫৯-এর ক্যাপস্টোনে আপনার নিজের লেখা shell — মূলত একটি অন্তহীন লুপ:
- পড়া (read): prompt দেখিয়ে stdin থেকে একটি লাইন পড়া। ইনপুট শেষ হলে (Ctrl+D, অর্থাৎ EOF) লুপ শেষ।
- ভাঙা (parse): লাইনটিকে শব্দে ভাঙা, উদ্ধৃতিচিহ্ন,
|,>,&চেনা, variable আর wildcard বদলানো। - Builtin কি না দেখা: কমান্ডটি shell-এর নিজের অবস্থা বদলায় এমন কিছু হলে (
cd,exit,export) shell নিজেই কাজটি করে, কোনো নতুন process ছাড়া। - fork: না হলে নিজের একটি কপি, অর্থাৎ child process তৈরি।
- exec (child-এ): child নিজের ভেতরে নতুন প্রোগ্রামটি বসায় — PATH-এ খুঁজে
/usr/bin/ls. - wait (parent-এ): shell waitpid দিয়ে child শেষ হওয়ার অপেক্ষা করে, exit status নিয়ে
$?-এ রাখে, তারপর আবার ধাপ ১-এ।
ব্যাংকের টোকেন-কাউন্টারের কর্মকর্তার কথা ভাবুন। তিনি আপনার কাগজ নেন (পড়া), দেখে বোঝেন কী চাই (ভাঙা)। কাজটি তাঁর নিজের খাতার হলে — যেমন “আমার ঠিকানা বদলে দিন” — নিজেই করে ফেলেন (builtin). অন্য কাজ হলে একটি নতুন ফাইল খুলে (fork) সেটিকে নির্দিষ্ট ডেস্কে পাঠান (exec), তারপর ডেস্ক থেকে ফল না আসা পর্যন্ত পরের টোকেন ডাকেন না (wait). আর যদি বলেন “এটা পরে দেখলেও চলবে, আমি অন্য কাজ করি” — সেটিই &, অর্থাৎ background.
কোন কাজ shell-এর ভেতরেই করতে হয়
পর্ব ০৩-এ দেখেছেন, cd আলাদা প্রোগ্রাম হলে শুধু child-এর ফোল্ডার বদলাত। নিয়মটি আরও সাধারণ: যে কাজ shell process-এর নিজের অবস্থা বদলায়, অথবা যে তথ্য শুধু shell-এর মেমরিতে আছে, সেটি builtin হতেই হবে। কারণ fork-এর পর child যা-ই বদলাক, তা child-এর কপিতে; parent কখনো দেখে না।
| Builtin | shell-এর কোন অবস্থা ছোঁয় | আলাদা প্রোগ্রাম হলে কী হতো |
|---|---|---|
cd, pushd | process-এর working directory | child-এর ফোল্ডার বদলে child মরে যেত |
export, unset, VAR=x | shell-এর environment, যা পরের child-রা পাবে | শুধু ওই child-এর কপি বদলাত |
exit | shell process নিজেই শেষ হয় | শুধু child শেষ হতো, shell চলতেই থাকত |
umask, ulimit | process-এর ফাইল-অনুমতির mask, resource-সীমা | child-এর মান বদলাত, পরের কমান্ডে প্রভাব নেই |
jobs, fg, bg, wait | shell-এর job table আর তার child-রা | অন্য process সেই তালিকা দেখতেই পায় না; আর শুধু parent-ই নিজের child-এর জন্য wait করতে পারে |
source (বা .), exec | একটি ফাইল shell-এর ভেতরেই চালানো; shell-কেই নতুন প্রোগ্রামে বদলানো | নতুন process-এ চলত — উদ্দেশ্যই নষ্ট |
শেষ সারির builtin exec মজার: exec ls লিখলে shell fork করে না, সরাসরি নিজের ভেতরে ls বসায়। ls শেষ হলে আর shell নেই — terminal জানালা বন্ধ হয়ে যায়। অর্থাৎ shell-এর লুপের ধাপ ৪ বাদ দিলে ঠিক এটাই ঘটে।
২ · ৩৫ লাইনে নিজের shell — C-তে mysh
তত্ত্ব যথেষ্ট; এবার লুপটি নিজে লিখে চালাই। নিচের প্রোগ্রামটি একটি সত্যিকারের, কাজের shell — pipe, redirection বা উদ্ধৃতিচিহ্ন বোঝে না, কিন্তু যেকোনো বাইরের কমান্ড চালাতে পারে, আর cd ও exit নিজে সামলায়। ব্লকটির stdin-এ আগে থেকে সাতটি লাইন দেওয়া আছে, যেন কেউ টাইপ করছেন: echo hello from mysh, uname -s, cd /tmp, pwd, false, nosuchcmd -x, exit — আর exit-এর পরে একটি অতিরিক্ত লাইন।
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
char line[256], *argv[16];
for (;;) {
printf("mysh> ");
fflush(stdout); /* flush BEFORE fork */
if (!fgets(line, sizeof line, stdin)) break; /* 1. read; EOF = Ctrl+D */
if (!isatty(STDIN_FILENO)) { fputs(line, stdout); fflush(stdout); }
int argc = 0; /* 2. parse into words */
for (char *t = strtok(line, " \t\n"); t && argc < 15; t = strtok(NULL, " \t\n"))
argv[argc++] = t;
argv[argc] = NULL;
if (argc == 0) continue;
if (strcmp(argv[0], "exit") == 0) break; /* builtins run HERE */
if (strcmp(argv[0], "cd") == 0) {
if (chdir(argc > 1 ? argv[1] : "/") != 0) printf("cd: failed\n");
continue;
}
pid_t pid = fork(); /* 3. fork */
if (pid == 0) { /* child */
execvp(argv[0], argv); /* 4. exec */
printf("mysh: %s: command not found\n", argv[0]);
fflush(stdout);
_exit(127);
}
int status;
waitpid(pid, &status, 0); /* 5. parent waits */
if (WIFEXITED(status) && WEXITSTATUS(status) != 0)
printf("[exit %d]\n", WEXITSTATUS(status));
}
return 0;
}আসল রানের আউটপুট: mysh> echo hello from mysh → hello from mysh; mysh> uname -s → Linux; mysh> cd /tmp কিছু ছাপায় না, কিন্তু পরের mysh> pwd দেখায় /tmp — অর্থাৎ builtin cd shell-এর নিজের ফোল্ডার বদলেছে, তাই পরের child pwd জন্ম থেকেই /tmp-এ। mysh> false → [exit 1]; mysh> nosuchcmd -x → mysh: nosuchcmd: command not found আর [exit 127] — bash-এর মতোই ১২৭. শেষে mysh> exit-এ লুপ ভাঙল; তার পরের echo never কখনো পড়াই হলো না।
তিনটি জিনিস খেয়াল করুন। (১) fflush(stdout) fork-এর আগে। এখানে stdout একটি pipe, তাই C library লেখা জমিয়ে রাখে; flush না করলে "mysh> " লেখাটি parent আর child দুজনের বাফারেই কপি হতো, আর দুবার ছাপা হতে পারত। (২) execvp ফিরে আসা মানেই ব্যর্থতা — সফল হলে পুরোনো কোড আর থাকেই না। তাই তার পরের লাইনে ত্রুটি ছাপিয়ে _exit(127); এটি ভুলে গেলে ব্যর্থ child নিজেই লুপে ফিরে আরেকটি “shell” হয়ে যেত — নতুনদের খুব পরিচিত বাগ। (৩) isatty: সত্যিকারের terminal-এ আপনার টাইপ করা অক্ষর পর্দায় দেখায় terminal driver নিজে (echo); এখানে কোনো terminal নেই, তাই লাইনটি আমরা নিজেরা ছাপিয়েছি।
এই ছোট shell-এ কী নেই, তার তালিকাই আসলে এই পর্বের বাকি অংশের সূচি: & দিয়ে background (অংশ ৩), Ctrl+C চাপলে shell নিজে না মরে শুধু কমান্ডটি থামানো (অংশ ৪), job table আর fg/bg (অংশ ৪–৫)। Pipe আর redirection যোগ করার কৌশল পর্ব ০৩-এর ডায়াগ্রামে দেখেছেন — pipe(), dup2(), তারপর exec; পর্ব ৫৯-এর ক্যাপস্টোনে সবগুলো জুড়ে পূর্ণ shell লিখবেন।
৩ · Foreground আর background — &, jobs, wait আর $!
সাধারণভাবে কমান্ড চালালে shell অপেক্ষা করে — কমান্ডটি foreground-এ চলে, আর আপনার কিবোর্ড তার দখলে। লাইনের শেষে & দিলে shell fork আর exec করে, কিন্তু wait করে না; সাথে সাথে prompt ফিরিয়ে দেয়। কমান্ডটি তখন background-এ চলে। Shell প্রতিটি এমন কমান্ড (বা পুরো pipeline)-কে একটি job হিসেবে মনে রাখে — job নম্বর %1, %2… আর এই তালিকাই job control-এর ভিত্তি।
jobs— shell-এর job table: নম্বর, অবস্থা (Running, Stopped, Done), কমান্ড।+চিহ্ন “বর্তমান” job,-তার আগেরটি;jobs -lPID-ও দেখায়।$!— সবচেয়ে শেষে background-এ পাঠানো process-এর PID. Script-এ পরেkillবাwaitকরতে কাজে লাগে।wait— সব background child শেষ হওয়া পর্যন্ত অপেক্ষা;wait $pidবাwait %1নির্দিষ্ট একটির জন্য, আর তার exit status ফেরত দেয়।
নিচের ডায়াগ্রামে একই কমান্ড দুইভাবে চালানো হয়েছে — sleep 5 আর sleep 5 &. প্রতিটি ধাপে shell আর child কী করছে, আর terminal কার হাতে, পাশাপাশি তুলনা করুন। (PID-গুলো উদাহরণ)
| sleep 5 — shell | |
| sleep 5 — child | |
| sleep 5 & — shell | |
| sleep 5 & — child | |
| terminal কার হাতে |
- পড়া — দুটিতেই এক। Enter চাপার পর shell লাইনটি পড়ল। এখনো কোনো process তৈরি হয়নি।
- ভাঙা — পার্থক্য এখানেই জন্মায়। দ্বিতীয় লাইনের শেষে
&আছে। Shell এটিকে argument হিসেবে দেয় না; শুধু একটি পতাকা মনে রাখে: “এর জন্য অপেক্ষা করব না”। দুটিতেইsleepপাবে একইargv:["sleep", "5"]. - fork — দুটিতেই এক। Job control চালু থাকলে (interactive shell-এ চালু থাকে) shell প্রতিটি job-কে একটি নতুন process group-এ রাখে: child-এর PID আর group নম্বর (PGID) একই — 4242 আর 4243. দৌড়-প্রতিযোগিতা এড়াতে parent আর child দুজনেই
setpgid()ডাকে। - Terminal কার? Foreground job হলে shell
tcsetpgrp()দিয়ে terminal-কে জানায়: “এখন থেকে foreground group 4242”. তখন কিবোর্ডের ইনপুট আর Ctrl+C সেই group পাবে। Background job হলে terminal shell-এর group-এই থাকে — তাই আপনি টাইপ চালিয়ে যেতে পারেন। - exec, তারপর মূল পার্থক্য। দুই child-ই exec করে
sleepহয়ে গেল। প্রথম ক্ষেত্রে shellwaitpid(4242)-এ ঘুমিয়ে আছে — prompt নেই। দ্বিতীয় ক্ষেত্রে shell job table-এ লিখল[1] 4243, ছাপাল, আর সাথে সাথে prompt দিল; আপনিlsচালাতে পারলেন। (ps-এর STAT-এ foreground group-এর process-এর পাশে+থাকে —S+; background-এরটির থাকে না) - ৫ সেকেন্ড পর। প্রথমটিতে
waitpidফিরল, shell terminal নিজের কাছে ফেরত নিল (tcsetpgrp),$?-এ 0 রাখল, prompt দিল। দ্বিতীয়টিতে child শেষ হয়ে কিছুক্ষণ zombie হয়ে রইল — kernel তার exit status ধরে রেখেছে; kernel shell-কেSIGCHLDপাঠাল, shell সেটি reap করল (status সংগ্রহ), আর পরের prompt-এর আগে জানাল[1]+ Done sleep 5.
এবার আসল Linux-এ দেখা যাক। নিচের ব্লকটি background job চালায়, job table আর process-এর group দেখায়, job control চালু-বন্ধ করে, একটি job-কে ভদ্রভাবে থামায়, আর একটির priority কমায়:
echo "this shell: PID $$"
sleep 1 & # & : start it, do not wait
echo "started a job, \$! = $!"
sleep 1 &
jobs -l # the shell's job table
ps -o pid,ppid,pgid,ni,stat,comm # job control is OFF in a script
set -m # turn job control ON
sleep 1 &
ps -o pid,pgid,stat,comm
wait # wait for ALL background children
echo "wait returned $?, jobs left: $(jobs | wc -l)"
set +m
sleep 30 &
kill $! # polite: SIGTERM (15)
wait $!
echo "killed sleep: status $? = 128 + $(kill -l TERM)"
nice -n 5 sleep 0.2 &
ps -o pid,ni,stat,comm -p $!
waitএকটি আসল রানে: this shell: PID 2, started a job, $! = 3, তারপর jobs -l দেখাল [1]- 3 Running sleep 1 & আর [2]+ 4 Running sleep 1 & — নতুনতম job-এ +. প্রথম ps-এ bash (PID 2), দুটি sleep (3, 4) আর ps নিজে (5) — চারজনেরই PGID 2. কারণ script-এ (non-interactive shell) job control ডিফল্টভাবে বন্ধ; background কমান্ডও shell-এর group-এই থাকে। set -m-এর পর নতুন sleep পেল PID 6, PGID 6 — নিজের group; এমনকি ps-ও (7, 7). এরপর wait returned 0, jobs left: 0, আর kill-এর পর killed sleep: status 143 = 128 + 15 — signal-এ মরলে exit status ১২৮ + signal নম্বর। শেষে nice -n 5-এর sleep দেখাল NI 15.
PID-এর মান আপনার রানে আলাদা হতে পারে; সম্পর্কগুলোই আসল। আর এই container-এর তিনটি বৈশিষ্ট্য লক্ষ করুন: PID 1 হলো podman-init — container-এর নিজের ছোট init, যে zombie reap করে আর signal পৌঁছে দেয় (পর্ব ৫১); সব process-এর NI আগে থেকেই 10 — সার্ভারটি অতিথিদের কম priority-তে চালায়, তাই nice -n 5-এ হলো 15, আর STAT-এ N (low priority); আর কোনো terminal নেই — তাই Ctrl+Z, fg এখানে পরীক্ষা করা যায় না, সেগুলো পরের অংশে ডায়াগ্রামে দেখব।
Job control বন্ধ থাকা অবস্থায় background কমান্ডের stdin POSIX নিয়মে /dev/null থেকে আসে, যাতে কিবোর্ডের ইনপুট নিয়ে কাড়াকাড়ি না হয়। Interactive shell-এ job control চালু থাকলে নিয়ম ভিন্ন: background job terminal থেকে পড়তে চাইলে kernel তাকে SIGTTIN পাঠিয়ে থামিয়ে দেয়, আর jobs-এ দেখায় Stopped (tty input). তখন fg করে ইনপুট দিতে হয়।
৪ · Ctrl+C, Ctrl+Z, fg আর bg — কে কাকে থামায়
একটি প্রচলিত ভুল ধারণা: Ctrl+C চাপলে shell প্রোগ্রামটিকে মেরে ফেলে। আসলে shell তখন waitpid-এ ঘুমিয়ে আছে, সে কিছুই দেখে না। কাজটি করে kernel-এর terminal driver (tty-এর line discipline). আপনার চাপা অক্ষরগুলো প্রোগ্রামে পাঠানোর আগে driver দেখে নেয় কোনো বিশেষ অক্ষর এসেছে কি না; এলে সে terminal-এর foreground process group-এর প্রতিটি process-কে একটি signal পাঠায় — kernel-এর একটি ছোট সংকেত, যা process-কে জানায় “কিছু একটা ঘটেছে”। Signal ধরা, উপেক্ষা করা আর নিজের handler লেখা পর্ব ১৩-এর বিষয়; এখানে শুধু কে, কখন, কাকে পাঠায়।
| কি | যা ঘটে | ডিফল্ট ফল |
|---|---|---|
| Ctrl+C | foreground group-কে SIGINT (নম্বর ২) | process শেষ; shell-এ $? = ১৩০ |
| Ctrl+Z | foreground group-কে SIGTSTP | process থেমে থাকে (মরে না); shell prompt ফিরে পায় |
| Ctrl+\ | foreground group-কে SIGQUIT | process শেষ, অনেক সিস্টেমে core dump-সহ |
| Ctrl+D | কোনো signal নয় — লাইনের শুরুতে চাপলে পাঠক read() থেকে ০ byte পায়, অর্থাৎ EOF | shell-এ চাপলে shell বেরিয়ে যায় |
কেন “পুরো group”? কারণ sort big.txt | uniq -c | head তিনটি আলাদা process, অথচ আপনার কাছে এটি একটি কাজ। Shell তিনটিকে একটি process group-এ রাখে, তাই একবার Ctrl+C-তে তিনটিই থামে। আর shell নিজে অন্য group-এ থাকে বলে সে বেঁচে যায় — এটিই আপনার mysh-এর সাথে bash-এর বড় পার্থক্য (mysh-এ সবাই এক group-এ, তাই Ctrl+C-তে mysh-ও মরে)।
থামানো, পেছনে পাঠানো, সামনে আনা
- Ctrl+Z — চলমান foreground job থামানো (Stopped). Process মেমরিতে আছে, শুধু CPU পাচ্ছে না।
bgবাbg %1— থেমে থাকা job-কেSIGCONTপাঠিয়ে background-এ চালু করা।fgবাfg %1— job-কে foreground-এ আনা: terminal তার group-কে দেওয়া, দরকার হলেSIGCONT, তারপর অপেক্ষা।kill %1— job নম্বর দিয়ে signal পাঠানো (shell%1-কে PGID-তে বদলে নেয়)।
ধরুন আপনি একটি লম্বা কাজ (sleep 100) foreground-এ শুরু করে ফেলেছেন, এখন অন্য কাজ করতে চান। নিচের ধারায় job table, process-এর STAT আর terminal-এর মালিক কীভাবে বদলায় দেখুন। (bash-এর PID/PGID 4000, sleep-এর 5001 — উদাহরণ)
| signal | |
| sleep-এর STAT | |
| terminal-এর foreground group | |
| bash-এর job table | |
| bash কী করছে |
- Foreground-এ চলছে।
sleep 100নিজের group 5001-এ, আর terminal-এর foreground group 5001. bashwaitpid-এ অপেক্ষা করছে; prompt নেই। ps-এ STATS+— ঘুমিয়ে (টাইমারের অপেক্ষায়), আর+মানে foreground group-এর সদস্য। - Ctrl+Z. Terminal driver অক্ষরটি চিনে foreground group 5001-কে
SIGTSTPপাঠাল; sleep থেমে গেল (STATT). bash-এরwaitpid(WUNTRACEDপতাকা দিয়ে ডাকা) ফিরে জানাল “child থেমেছে”। bash terminal নিজের group 4000-কে ফেরত দিল, job table-এStoppedলিখল, prompt দিল। Driver^Zলেখাটিও দেখায়। jobs. Builtin, তাই কোনো fork নেই — bash নিজের মেমরির তালিকা ছাপাল:[1]+ Stopped sleep 100. Process বেঁচে আছে, মেমরিও ধরে রেখেছে, কিন্তু CPU পাচ্ছে না।bg. bash group 5001-কেSIGCONTপাঠাল; sleep আবার চলতে লাগল, কিন্তু terminal bash-এর কাছেই রইল। bash লিখল[1]+ sleep 100 &— যেন শুরু থেকেই&দিয়ে চালানো। STAT এখনS,+ছাড়া।- আবার
jobs. এবারRunning. এই সময়ে আপনি অন্য কমান্ড চালাতে পারেন; sleep পেছনে নিজের কাজ করছে। fg. bash কমান্ডটির নাম ছাপাল (sleep 100),tcsetpgrp()দিয়ে terminal-কে group 5001-এর হাতে দিল, নিয়ম অনুযায়ীSIGCONTপাঠাল (চলমান process-এ এটি নিরীহ), তারপর আবারwaitpid-এ। STAT আবারS+.- Ctrl+C. এবার driver group 5001-কে
SIGINTপাঠাল; sleep-এর ডিফল্ট আচরণ হলো মরে যাওয়া। bash-এরwaitpidজানাল “signal ২-এ মারা গেছে”, bash terminal ফেরত নিল,$?-এ রাখল ১২৮ + ২ = ১৩০, আর job table খালি হলো। খেয়াল করুন, bash নিজে group 4000-এ ছিল, তাই SIGINT তার কাছে পৌঁছায়ইনি।
/* child, fork()-এর ঠিক পরে */
setpgid(0, 0); /* নিজের নতুন process group */
if (foreground) tcsetpgrp(tty_fd, getpid());
execvp(argv[0], argv);
/* parent shell */
setpgid(pid, pid); /* একই কাজ — race এড়াতে দুদিকেই */
if (foreground) {
tcsetpgrp(tty_fd, pid); /* terminal দিলাম job-কে */
waitpid(-pid, &status, WUNTRACED); /* শেষ হলো, না থামল? */
tcsetpgrp(tty_fd, getpgrp()); /* terminal ফেরত নিলাম */
if (WIFSTOPPED(status)) mark_job_stopped(pid);
}
/* bg: kill(-pgid, SIGCONT); fg: tcsetpgrp(...) + kill(-pgid, SIGCONT) + waitpid */kill(-pgid, …) আর waitpid(-pgid, …)-এর ঋণাত্মক সংখ্যাটি লক্ষ করুন — POSIX-এ এর মানে “এই group-এর সবাই”। পুরো job control এই কয়েকটি system call দিয়েই তৈরি; বাকিটা shell-এর হিসাবরক্ষণ।
৫ · Process group, session আর controlling terminal — SSH কাটলে job কেন মরে
এ পর্যন্ত তিনটি স্তর দেখা হলো। এবার নামগুলো ঠিক করে নিই:
- Process — একটি চলমান প্রোগ্রাম, PID দিয়ে চেনা; তার parent-এর PID হলো PPID.
- Process group — একসাথে signal পাওয়া process-দের দল, সাধারণত একটি job বা pipeline; PGID = দলনেতার PID.
- Session — একটি login-এর অধীনের সব process group; SID = session leader-এর PID, সাধারণত login shell. Session leader
setsid()দিয়ে নতুন session খোলে। - Controlling terminal — একটি session-এর সাথে যুক্ত একটিমাত্র terminal (যেমন
/dev/pts/0). এর একটি foreground group থাকে; বাকি group background.
Session 4000 ── controlling terminal: /dev/pts/0 (SSH-এর pseudo-terminal)
│
├── group 4000 bash session leader, prompt-এ অপেক্ষা
├── group 5001 sleep 100 & background job [1]
└── group 5100 sort big.txt | uniq -c | head ← foreground job: terminal এদের হাতে
├── 5100 sort
├── 5101 uniq
└── 5102 head
একটি অফিস ভবন ভাবুন। Session হলো একটি অফিস — একজন কর্মকর্তা (session leader) সকালে ঢুকে খুলেছেন। Process group হলো অফিসের একেকটি দল — হিসাব দল, কেনাকাটা দল। Controlling terminal হলো রিসেপশনের একমাত্র মাইক্রোফোন আর দরজা: একসময়ে শুধু একটি দল (foreground) মাইকে কথা বলতে আর বাইরের অতিথির কথা শুনতে পারে। আর বাড়ির দারোয়ান (kernel) মাইক থেকে “সবাই থামুন!” (Ctrl+C) ঘোষণা এলে শুধু সেই দলটিকেই জানান। অফিসের চাবি ফেরত দিলে (logout) দারোয়ান ঘোষণা করেন “অফিস বন্ধ হচ্ছে” — সেটিই SIGHUP.
SIGHUP — “লাইন কেটে গেছে”
SIGHUP নামটি এসেছে টেলিফোনের “hang up” থেকে। ১৯৭০-এর দশকে terminal দূরের কম্পিউটারে modem দিয়ে যুক্ত হতো; লাইন কেটে গেলে kernel সেই terminal-এর process-দের জানিয়ে দিত, যাতে কেউ অনন্তকাল এমন কিবোর্ডের অপেক্ষায় না থাকে যা আর নেই। আজকের SSH সংযোগও একটি pseudo-terminal-এর মাধ্যমে চলে, তাই নিয়ম একই। নিচের ডায়াগ্রামে Wi-Fi কেটে যাওয়ার পর সার্ভারে কী ঘটে দেখুন — তিনটি background কাজ তিনভাবে চালানো ছিল:
- sshd
সংযোগpty বন্ধ করে - kernel · tty
/dev/pts/0hangup - bash
session leaderSIGHUP পায়, job-দের পাঠায় - ফল
বাঁচল / মরলকার কী হলো
| ./report.sh & | |
| nohup ./backup.sh & | |
| tmux-এর ভেতরে ./train.sh | |
| bash |
- আগের অবস্থা। আপনি SSH দিয়ে সার্ভারে ঢুকেছেন। sshd একটি pseudo-terminal বানিয়ে child-এ
setsid()করেছে, তাই bash একটি নতুন session-এর leader আর/dev/pts/0তার controlling terminal. তিনটি কাজ চলছে: একটি সাধারণ&দিয়ে, একটিnohupদিয়ে, আর একটি tmux-এর ভেতরে। - সংযোগ কাটল। Wi-Fi চলে গেল বা ল্যাপটপের ঢাকনা বন্ধ হলো। কিছুক্ষণ পর sshd বুঝল সংযোগ মৃত (TCP timeout বা keepalive দিয়ে), তাই সে pty-র নিজের দিকের (master) মুখটি বন্ধ করল।
- Kernel-এর hangup. Master বন্ধ হওয়া মানে terminal-এর “লাইন কেটে গেছে”। Kernel terminal-এর controlling process — session leader bash — কে
SIGHUPপাঠায়। এখন থেকে এই terminal-এ লেখার চেষ্টা ব্যর্থ হবে (EIO). - bash job-দের জানায়। SIGHUP-এ বেরিয়ে যাওয়ার আগে interactive bash নিজের job table-এর প্রতিটি job-কে SIGHUP পাঠায় (থেমে থাকা job-কে সাথে SIGCONT-ও, যাতে সংকেতটি পৌঁছায়)।
report.sh-এর ডিফল্ট আচরণ মারা যাওয়া।nohupচালানোর সময়ই SIGHUP-কে “উপেক্ষা” করে রেখেছিল, তাইbackup.shকিছু টের পায় না। tmux-এর কাজটি bash-এর job-ই নয় — tmux server নিজে আলাদা session-এ চলে। - ফল।
report.shশেষ; অর্ধেক লেখা রিপোর্ট পড়ে রইল।backup.shবেঁচে আছে, parent মারা যাওয়ায় সে এখন init (বা কোনো subreaper)-এর child, আর তার আউটপুট যাচ্ছেnohup.outফাইলে।train.shনিশ্চিন্তে চলছে; আবার SSH করেtmux attachলিখলে ঠিক আগের পর্দা ফিরে পাবেন।
বাঁচানোর চারটি উপায়
| উপায় | কীভাবে কাজ করে | কখন |
|---|---|---|
nohup cmd > out.log 2>&1 & | SIGHUP উপেক্ষা করে cmd চালায়; stdout terminal হলে আউটপুট nohup.out-এ পাঠায় | আগে থেকে জানেন কাজটি লম্বা |
disown %1 (বা disown -h %1) | job-টি bash-এর তালিকা থেকে সরায় (বা “এটিকে SIGHUP পাঠানো যাবে না” চিহ্ন দেয়), তাই bash আর তাকে SIGHUP পাঠায় না | শুরু করে ফেলেছেন, এখন মনে পড়ল: Ctrl+Z, bg, disown |
| tmux বা screen | terminal-টিই একটি আলাদা server process-এর ভেতরে থাকে; SSH কাটলে শুধু “দর্শক” চলে যায় | interactive কাজ, পরে ফিরে দেখতে চান |
systemd সার্ভিস, systemd-run | কাজটি login session-এর বাইরেই চলে, systemd নজরদারি করে, দরকারে আবার চালায় | স্থায়ী সার্ভিস — ওয়েব সার্ভার, worker |
দুটি সূক্ষ্মতা: (১) exit লিখে স্বাভাবিকভাবে বের হলে bash ডিফল্টভাবে চলমান job-দের SIGHUP পাঠায় না (shopt huponexit বন্ধ থাকে); থেমে থাকা job থাকলে শুধু সতর্ক করে There are stopped jobs. তবুও সেই job terminal-এ লিখতে গেলে ব্যর্থ হবে। (২) কিছু Linux distro-তে systemd-logind-এর KillUserProcesses সেটিং চালু থাকলে logout-এ পুরো session-এর সব process মারা যায় — nohup দিয়েও বাঁচে না। আপনার সার্ভারে কী সেটিং আছে, /etc/systemd/logind.conf-এ দেখে নিন।
Daemon — terminal ছাড়া জন্ম থেকে
যে process কোনো terminal ছাড়া পেছনে স্থায়ীভাবে চলে, তাকে বলে daemon — sshd, cron, nginx; নামের শেষে প্রায়ই d. পুরোনো Unix-এ daemon নিজেকে নিজে “আলাদা” করত: fork করে parent বেরিয়ে যেত (যাতে shell ভাবে কাজ শেষ), child setsid() করে নতুন session খুলত (কোনো controlling terminal নেই, SIGHUP-এর ভয় নেই), আবার fork করত যাতে ভুলেও কোনো terminal তার controlling terminal না হয়, তারপর chdir("/"), stdin/stdout/stderr-কে /dev/null-এ পাঠানো। আধুনিক Linux-এ systemd এই কাজ নিজেই করে: সার্ভিসটি সাধারণ প্রোগ্রামের মতো foreground-এ চলে, stdout-এ log লেখে, আর systemd তাকে আলাদা রাখে, নজর রাখে, পড়ে গেলে আবার চালায়। ps-এ daemon-এর TTY কলামে দেখবেন ? — কোনো controlling terminal নেই।
৬ · ps আর pstree — process-এর তালিকা পড়া
ps (process status) একটি মুহূর্তের ছবি তোলে: সেই মুহূর্তে কোন process আছে, কে চালিয়েছে, কী অবস্থায়। ইতিহাসের কারণে এর option-এর দুটি ঘরানা আছে, আর Linux-এর ps (procps প্যাকেজ) দুটিই বোঝে:
| ঘরানা | সাধারণ রূপ | মানে |
|---|---|---|
| BSD (dash ছাড়া) | ps aux | a সব user-এর terminal-যুক্ত process, x terminal-ছাড়াগুলোও, u user-কেন্দ্রিক কলাম (USER, %CPU, %MEM, VSZ, RSS, STAT, START) |
| System V (dash-সহ) | ps -ef | -e সব process, -f পূর্ণ তালিকা (UID, PID, PPID, C, STIME, TTY, CMD) |
| নিজের কলাম | ps -eo pid,ppid,pgid,stat,ni,comm | ঠিক যে কলাম দরকার; script-এর জন্য সবচেয়ে নির্ভরযোগ্য |
| গাছ | ps -ejH, ps axjf, pstree -p | parent-child সম্পর্ক গাছ আকারে |
সাবধান: ps aux আর ps -aux এক নয় — dash দিলে -u মানে “এই user-এর process”, আর x নামে user খোঁজা হতে পারে। BSD রূপ সবসময় dash ছাড়া লিখুন। আরেকটি পার্থক্য: ps-এর %CPU process-এর পুরো জীবনের গড়, আর top-এরটি শেষ কয়েক সেকেন্ডের — তাই একই process-এর দুটি সংখ্যা মেলে না। একটি সাধারণ ল্যাপটপে ps aux-এর কয়েকটি লাইন দেখতে এমন (উদাহরণ, ছাঁটা):
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.1 167712 12840 ? Ss 09:02 0:03 /sbin/init splash root 812 0.0 0.0 15428 8960 ? Ss 09:02 0:00 sshd: /usr/sbin/sshd -D rahim 4000 0.0 0.0 11284 5320 pts/0 Ss 10:11 0:00 -bash rahim 5001 0.0 0.0 8392 1020 pts/0 T 10:20 0:00 sleep 100 rahim 5100 98.7 1.2 210440 99812 pts/0 R+ 10:21 1:12 sort big.txt rahim 4321 12.4 5.1 3914560 412300 ? Sl 09:15 12:03 /usr/lib/firefox/firefox rahim 6060 0.0 0.0 0 0 ? Z 10:25 0:00 [python3] <defunct>
VSZ (virtual size) হলো process-এর পুরো ঠিকানা-জায়গার আকার KiB-তে — যা সে “দাবি” করেছে, ব্যবহার না করলেও। RSS (resident set size) হলো এই মুহূর্তে আসল RAM-এ থাকা অংশ। Firefox-এর VSZ প্রায় ৩.৭ GiB, অথচ RSS মাত্র ৪০০ MiB-এর মতো — কেন এত পার্থক্য, পর্ব ৩৫ আর ৩৮-এ virtual memory দেখার পর পরিষ্কার হবে। -bash-এর আগের dash মানে এটি login shell. আর শেষ লাইনের <defunct> একটি zombie — শেষ হয়ে গেছে, কিন্তু parent এখনো wait করেনি।
STAT কলাম — এক-দুই অক্ষরে পুরো গল্প
| অক্ষর | মানে | কখন দেখবেন |
|---|---|---|
R | Running বা runnable — CPU-তে চলছে অথবা run queue-তে অপেক্ষা | হিসাবের কাজ, এই মুহূর্তের ps নিজে |
S | Interruptible sleep — কোনো ঘটনার অপেক্ষা; signal এলে জাগবে | বেশিরভাগ process বেশিরভাগ সময় |
D | Uninterruptible sleep — সাধারণত ডিস্ক বা নেটওয়ার্ক ফাইল সিস্টেমের I/O; signal-এও জাগে না, kill -9-ও তখনই কাজ করে না | ধীর ডিস্ক, আটকে যাওয়া NFS |
T | Stopped — job control signal-এ থামানো (Ctrl+Z) | jobs-এ Stopped |
t | debugger-এর জন্য থামানো (tracing stop) | gdb দিয়ে চালালে |
Z | Zombie — শেষ, কিন্তু parent exit status নেয়নি | parent-এর বাগ (পর্ব ১০) |
I | Idle kernel thread | [kworker/…] জাতীয় kernel thread |
| পরের অক্ষর | s session leader, + foreground group, l বহু-thread, < উঁচু priority, N নিচু priority (nice), L মেমরিতে আটকানো page | Ss, R+, Sl, SN |
এখন উপরের তালিকা পড়ে ফেলতে পারেন: init আর sshd Ss — ঘুমিয়ে আছে, নিজের session-এর leader; sleep 100 T — কেউ Ctrl+Z চেপেছে; sort R+ — CPU-তে চলছে, foreground-এ; firefox Sl — ঘুমন্ত, অনেক thread. এগুলোই পর্ব ০৮-এর process state diagram-এর বাস্তব রূপ।
pstree -p একই তথ্য গাছ আকারে দেখায় — কে কার parent, এক নজরে (উদাহরণ, ছাঁটা):
systemd(1)─┬─sshd(812)───sshd(3990)───sshd(3998)───bash(4000)─┬─sleep(5001)
│ └─sort(5100)
├─cron(640)
└─tmux: server(7100)───bash(7101)───train.sh(7150)
লক্ষ করুন, tmux-এর ভেতরের কাজটি sshd-এর শাখায় নয়, systemd-এর সরাসরি নিচে — ঠিক এজন্যই SSH কাটলেও সে বাঁচে। নাম দিয়ে PID খুঁজতে pgrep -a nginx (-a পুরো কমান্ড লাইনসহ দেখায়) — ps aux | grep nginx-এর চেয়ে নির্ভরযোগ্য, কারণ grep নিজেকেও তালিকায় ধরে ফেলে।
৭ · top আর htop — চলমান ছবি, আর load average-এর আসল মানে
ps যদি ছবি হয়, top ভিডিও — প্রতি কয়েক সেকেন্ডে পর্দা হালনাগাদ করে, ডিফল্টভাবে সবচেয়ে বেশি CPU খরচ করা process ওপরে রেখে। কয়েকটি কাজের কি: P CPU অনুযায়ী, M মেমরি অনুযায়ী সাজানো, 1 প্রতিটি CPU আলাদা, k signal পাঠানো, r renice, q বেরোনো। Script-এ বা log-এর জন্য top -b -n 1 (batch mode, একবার)। htop একই তথ্য রঙিন বার, গাছ-দৃশ্য (F5) আর মাউস দিয়ে দেখায়; অনেক distro-তে আলাদা ইনস্টল করতে হয়।
Load average — সবচেয়ে বেশি ভুল বোঝা সংখ্যা
top-এর প্রথম লাইনে, uptime-এ আর /proc/loadavg-এ তিনটি সংখ্যা থাকে, যেমন 2.15, 1.40, 0.90 — গত ১, ৫ আর ১৫ মিনিটের load average. এটি শতাংশ নয়। এটি “গড়ে কতগুলো task কাজ চাইছিল” তার একটি মাপ:
- Linux-এ যা গোনা হয়: যে task-গুলো
Rঅবস্থায় (CPU-তে চলছে বা CPU-র অপেক্ষায় run queue-তে) এবং যেগুলোDঅবস্থায় (uninterruptible sleep, সাধারণত I/O). প্রতিটি thread আলাদা task হিসেবে গোনা হয়। - গড় কীভাবে: সাধারণ গড় নয় — kernel প্রায় প্রতি ৫ সেকেন্ডে একবার সংখ্যাটি নমুনা নেয় আর একটি exponentially-damped moving average হালনাগাদ করে:
load = load × e−5/(60m) + n × (1 − e−5/(60m)), যেখানে m = ১, ৫ বা ১৫ মিনিট আর n = এই মুহূর্তের R + D task-এর সংখ্যা। ফলে পুরোনো নমুনার প্রভাব ধীরে ধীরে মিলিয়ে যায়। - কীসের সাথে তুলনা: CPU-র সংখ্যার সাথে। ৪টি CPU-তে load ৪.০ মানে গড়ে সব CPU ব্যস্ত, কেউ অপেক্ষায় নেই; ৮.০ মানে প্রায় সমান সংখ্যক task লাইনে দাঁড়িয়ে (অথবা I/O-তে আটকে)। ১টি CPU-র যন্ত্রে ৪.০ একই সংখ্যা হলেও অবস্থা অনেক খারাপ।
- প্রবণতা: ১ মিনিটেরটি ১৫ মিনিটেরটির চেয়ে বড় হলে চাপ সম্প্রতি বাড়ছে; ছোট হলে কমছে।
D-অবস্থার ফাঁদ। Linux-এ load-এর ভেতরে D-task থাকায় একটি আটকে যাওয়া NFS সার্ভার বা ধীর ডিস্ক load ২০-৩০ বানিয়ে দিতে পারে, অথচ CPU প্রায় অলস (top-এর id বেশি, wa বেশি)। তখন “CPU বাড়াতে হবে” সিদ্ধান্ত ভুল — সমস্যা I/O-তে। আলাদা করতে দেখুন: top-এর %Cpu(s) লাইনে wa, S কলামে D, আর /proc/stat-এর procs_running বনাম procs_blocked. অনেক BSD-ঘরানার সিস্টেমে load-এ শুধু runnable task গোনা হয়, তাই অন্য OS-এর সাথে সংখ্যা সরাসরি তুলনা করবেন না।
এবার একটি পূর্ণ top পর্দা লাইন ধরে ধরে পড়ি। যন্ত্রটি একটি ৪-CPU ল্যাপটপ, ৮ GB RAM (উদাহরণ):
1top - 10:15:02 up 3 days, 2:41, 1 user, load average: 2.15, 1.40, 0.902Tasks: 231 total, 2 running, 228 sleeping, 0 stopped, 1 zombie3%Cpu(s): 23.5 us, 4.1 sy, 0.0 ni, 70.2 id, 1.9 wa, 0.0 hi, 0.3 si, 0.0 st4MiB Mem : 7837.4 total, 512.3 free, 4210.8 used, 3114.3 buff/cache5MiB Swap: 2048.0 total, 1900.0 free, 148.0 used. 3205.6 avail Mem6 7 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND8 4321 rahim 20 0 3914560 412300 98200 S 87.5 5.1 12:03.44 firefox9 1187 rahim 20 0 812344 95120 60480 R 12.0 1.2 0:48.10 code10 498 root 20 0 0 0 0 D 0.3 0.0 0:05.12 jbd2/sda2-811 5570 rahim 39 19 20480 4096 3500 S 2.0 0.1 0:01.02 backup.sh
| পড়ছি | |
| মূল সংখ্যা | |
| সিদ্ধান্ত |
- সময় আর uptime. এখন সকাল ১০:১৫, যন্ত্রটি ৩ দিন ২ ঘণ্টা ৪১ মিনিট ধরে চালু, আর ১ জন user login করে আছেন। একই লাইন
uptimeকমান্ডও দেখায়। - Load average 2.15, 1.40, 0.90. CPU ৪টি, তাই ১ মিনিটের গড়ে প্রতি CPU-তে 2.15 / 4 ≈ 0.54 — মোটামুটি অর্ধেক সামর্থ্য ব্যবহার হচ্ছে, কেউ লাইনে দাঁড়িয়ে নেই বললেই চলে। তবে ১৫ মিনিটে 0.90, ৫ মিনিটে 1.40, ১ মিনিটে 2.15 — চাপ সম্প্রতি বাড়ছে। এটি শতাংশ নয়; আর Linux-এ D-অবস্থার task-ও এর ভেতরে।
- Tasks. মোট 231 = 2 running + 228 sleeping + 0 stopped + 1 zombie. বেশিরভাগ সময় বেশিরভাগ task ঘুমিয়ে থাকে — এটাই স্বাভাবিক। ১টি zombie মানে কোনো parent তার child-এর exit status নিচ্ছে না; একটি-দুটি ক্ষতিকর নয়, কিন্তু সংখ্যা বাড়তে থাকলে parent প্রোগ্রামে বাগ আছে।
- CPU-র সময় কোথায় যাচ্ছে। সব CPU মিলিয়ে:
us23.5% user code-এ,sy4.1% kernel-এ (system call, driver),ninice করা process-এ,id70.2% অলস,wa1.9% অলস কিন্তু I/O-র অপেক্ষায়,hi/sihardware ও software interrupt,st“stolen” — virtual machine-এ hypervisor অন্য অতিথিকে দিয়েছে। যোগফল ১০০।waকম, তাই এখানে load-এর পেছনে মূলত CPU-র কাজ। - মেমরি। 7837.4 total = 512.3 free + 4210.8 used + 3114.3 buff/cache. “free মাত্র ৫১২!” দেখে ভয় পাবেন না — Linux খালি RAM ফাইলের cache হিসেবে কাজে লাগায়, দরকার হলে ছেড়ে দেয়। আসল প্রশ্নের উত্তর
avail Mem3205.6 MiB: নতুন প্রোগ্রাম swap ছাড়াই মোটামুটি এতটা পেতে পারে। Swap-এ 148 MiB ব্যবহৃত — সামান্য। - Process-এর সারি।
PRscheduler-এর priority, সাধারণ process-এ ২০ + NI (backup.sh: NI 19, তাই PR 39);NInice মান;VIRT/RES/SHRKiB-তে virtual, RAM-এ থাকা, আর তার মধ্যে শেয়ার করা অংশ;Sঅবস্থা — jbd2 (ext4-এর journal thread)D-তে, তাই load-এ গোনা হচ্ছে;%CPUএকটি CPU-র শতাংশ, তাই বহু-thread firefox ১০০ ছাড়াতেও পারে (top-এ Shift+I চাপলে সব CPU মিলিয়ে ভাগ করে দেখায়);%MEM= RES / মোট RAM = 412300 KiB / (7837.4 × 1024) KiB ≈ 5.1%.
৮ · /proc — kernel-এর দিকে খোলা জানালা
ps আর top কোনো জাদু জানে না। তারা kernel-এর বানানো একটি বিশেষ ফোল্ডার /proc পড়ে। এটি procfs — একটি virtual file system: ডিস্কে এর কোনো ফাইল নেই; আপনি কোনো “ফাইল” পড়লে kernel সেই মুহূর্তে নিজের ভেতরের তথ্য থেকে লেখাটি বানিয়ে দেয়। প্রতিটি process-এর জন্য একটি ফোল্ডার /proc/<pid>/, আর /proc/self সবসময় যে process পড়ছে তার নিজের ফোল্ডার।
| ফাইল | কী থাকে | কোন টুল ব্যবহার করে |
|---|---|---|
/proc/<pid>/cmdline | পুরো argv, প্রতিটি শব্দের পর একটি NUL (\0) byte | ps -f-এর CMD, pgrep -a |
/proc/<pid>/status | মানুষের পড়ার মতো সারাংশ: Name, State, PPid, Uid, VmRSS, Threads… | ps, top |
/proc/<pid>/fd/ | প্রতিটি খোলা file descriptor একটি symlink — কোন ফাইল, pipe বা socket | lsof |
/proc/<pid>/maps | memory map: প্রতিটি অঞ্চলের ঠিকানা, অনুমতি (r/w/x), কোন ফাইল থেকে | pmap, debugger (পর্ব ৩৫, ৩৮) |
/proc/loadavg | তিনটি load average, চলমান/মোট task, শেষ তৈরি PID | uptime, top |
/proc/stat | বুট থেকে প্রতিটি CPU-র সময় (user, nice, system, idle, iowait…), context switch, তৈরি process-এর সংখ্যা | top-এর %Cpu, vmstat |
এবার এই ব্লকটি যে bash চালাচ্ছে, তাকেই ভেতর থেকে দেখি:
echo "shell PID $$, parent PID $PPID"
tr '\0' ' ' < /proc/$$/cmdline; echo # NUL-separated argv
grep -E '^(Name|State|PPid|Threads|VmRSS)' /proc/$$/status
ls -l /proc/$$/fd | awk 'NR>1 {print $9, $10, $11}'
read one five fifteen procs last < /proc/loadavg
echo "load average: $one $five $fifteen (running/total: $procs)"
echo "CPUs seen: $(nproc)"
echo "PID 1 is: $(cat /proc/1/comm)"
top -b -n 1 | head -12একটি আসল রানে: shell PID 2, parent PID 1; cmdline থেকে /bin/bash prog.sh — অর্থাৎ সার্ভারটি আপনার কোড prog.sh নামে সংরক্ষণ করে bash-কে দিয়েছে। status দেখাল Name: bash, State: S (sleeping) — কারণ ঠিক তখন bash grep-এর শেষ হওয়ার অপেক্ষায় ঘুমিয়ে ছিল; তারপর PPid: 1, VmRSS: 3384 kB, Threads: 1. fd-গুলো: 0 -> pipe:[…], 1 -> pipe:[…], 2 -> pipe:[…] — terminal নয়, সার্ভারের সাথে তিনটি pipe; আর 255 -> /home/wandbox/prog.sh — bash script ফাইলটি পড়ার জন্য একটি উঁচু নম্বরের fd খুলে রাখে।
এরপর load average: 0.52 1.06 1.04 (running/total: 5/289), CPUs seen: 2, PID 1 is: podman-init. আর top-এর প্রথম লাইন up 79 days, অথচ Tasks: 4 total আর তালিকায় মাত্র চারটি process — podman-init, bash, top, head; সবার PR 30, NI 10 (২০ + ১০)। এই অমিলটিই শিক্ষণীয়: container নিজের PID দেখে আলাদা (PID namespace, পর্ব ৫১), কিন্তু /proc/loadavg, uptime আর মেমরির সংখ্যা পুরো host মেশিনের — ২৮৯টি task আসলে সার্ভারের সবার। তাই container-এর ভেতরে top-এর load বা free মেমরি দেখে নিজের অ্যাপ নিয়ে সিদ্ধান্ত নেওয়া বিভ্রান্তিকর হতে পারে। সংখ্যাগুলো প্রতি রানে বদলাবে।
lsof — কে কোন ফাইল ধরে রেখেছে
lsof (list open files) সব process-এর /proc/<pid>/fd ঘুরে একটি তালিকা বানায়। Linux-এ “সবকিছুই ফাইল” — তাই সাধারণ ফাইল, ফোল্ডার, pipe, network socket সবই এখানে আসে। এই সাইটের সার্ভারে এটি ইনস্টল নেই, তাই উদাহরণ:
$ lsof -i :8080 # কে 8080 পোর্টে শুনছে?
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 9120 rahim 21u IPv4 73412 0t0 TCP *:8080 (LISTEN)
$ lsof +L1 # মুছে ফেলা কিন্তু এখনো খোলা ফাইল
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
java 3301 app 7w REG 259,2 8589934592 0 13321 /var/log/app.log (deleted)
$ lsof -p 9120 | head -3 # একটি process-এর সব খোলা ফাইলদ্বিতীয় উদাহরণটি পর্ব ০৩-এর একটি রহস্যের সমাধান: log ফাইল rm করার পরও ডিস্ক ভরা, কারণ java এখনো ৮ GiB-এর ফাইলটি খুলে রেখেছে। প্রোগ্রামটি restart করলে (বা তাকে log ফাইল নতুন করে খুলতে বললে) জায়গা খালি হয়। lsof না থাকলে ls -l /proc/*/fd 2>/dev/null | grep deleted একই কাজ করে, আর পোর্টের জন্য ss -ltnp.
৯ · kill, pkill, nice — আর Windows-এ একই কাজ
নাম যতই ভয়ংকর হোক, kill আসলে শুধু একটি signal পাঠায়। কোনটি পাঠাবেন, সেখানেই ভদ্রতা:
kill 1234— ডিফল্ট SIGTERM (১৫): “দয়া করে শেষ করুন”। প্রোগ্রাম চাইলে এটি ধরে গুছিয়ে নিতে পারে — খোলা ফাইল flush, temp ফাইল মোছা, অর্ধেক লেনদেন শেষ করা বা বাতিল।kill -9 1234বাkill -KILL 1234— SIGKILL (৯): kernel সরাসরি process শেষ করে; ধরা বা উপেক্ষা করা যায় না, তাই গোছানোর সুযোগও নেই। এটি শেষ উপায় — আগে SIGTERM দিয়ে কয়েক সেকেন্ড অপেক্ষা।kill -0 1234— কিছুই পাঠায় না, শুধু যাচাই করে process আছে কি না আর signal পাঠানোর অনুমতি আছে কি না।kill -l— সব signal-এর নাম আর নম্বর। পূর্ণ তালিকা আর আচরণ পর্ব ১৩-এ।pkill nginx— নামের সাথে মেলে এমন সব process-কে signal;pkill -fপুরো কমান্ড লাইনে খোঁজে। আগে সবসময়pgrep -aদিয়ে দেখে নিন কারা মিলছে।killall nginx— Linux-এ (psmisc প্যাকেজ) ঠিক এই নামের সব process. সতর্কতা: Solaris-এর মতো কিছু পুরোনো Unix-এkillallমানে “সব process মেরে ফেলা” — shutdown-এর জন্য তৈরি। অন্য OS-এ অভ্যাসবশত লিখবেন না।
সাধারণ user শুধু নিজের process-এ signal পাঠাতে পারেন; অন্যের process-এ পাঠাতে root লাগে (পর্ব ৪৭)। অন্যের process মারতে sudo kill -9 লেখার আগে ভাবুন — সেটি হয়তো কোনো ডেটাবেস, যার অর্ধেক লেখা ডেটা নষ্ট হবে।
nice আর renice — ভদ্র process
প্রতিটি process-এর একটি nice মান থাকে, −২০ থেকে ১৯; ডিফল্ট ০. মান যত বেশি, process তত “ভদ্র” — CPU নিয়ে কাড়াকাড়ির সময় অন্যদের বেশি সুযোগ দেয়। nice -n 10 ./backup.sh কম priority-তে চালায়; renice -n 15 -p 5570 চলমান process-এর মান বদলায়। সাধারণ user শুধু মান বাড়াতে (ভদ্রতর করতে) পারেন; কমাতে, মানে নিজের priority বাড়াতে, সাধারণত root লাগে। উপরের runnable ব্লকেই দেখেছেন, NI 10 থেকে nice -n 5-এ 15. মনে রাখুন, nice শুধু CPU-র ভাগ বদলায়; ডিস্কের জন্য আলাদা টুল ionice. Linux scheduler nice মানকে কীভাবে CPU-র ভাগে বদলায়, পর্ব ২১-এ দেখবেন।
Windows-এ একই কাজ
Windows-এ fork নেই (process তৈরি হয় CreateProcess দিয়ে), SIGHUP বা SIGTSTP-ও নেই, তাই Unix-এর job control-এর হুবহু রূপ নেই। কিন্তু process দেখা আর থামানোর টুল আছে:
| কাজ | Linux | Windows |
|---|---|---|
| গ্রাফিক্যাল দৃশ্য | System Monitor, htop | Task Manager (Ctrl+Shift+Esc) — Details ট্যাবে PID; আরও গভীরে Resource Monitor, Sysinternals Process Explorer |
| তালিকা | ps aux | tasklist, tasklist /FI "IMAGENAME eq notepad.exe"; PowerShell-এ Get-Process |
| বেশি CPU খরচকারী | top | Get-Process | Sort-Object CPU -Descending | Select-Object -First 5 |
| ভদ্রভাবে থামানো | kill PID (SIGTERM) | taskkill /PID 1234 — GUI অ্যাপকে বন্ধের অনুরোধ পাঠায় |
| জোর করে থামানো | kill -9 PID | taskkill /PID 1234 /F, /T দিলে child-সহ; Stop-Process -Id 1234 -Force |
| নাম দিয়ে | pkill name | taskkill /IM notepad.exe, Stop-Process -Name notepad |
| priority | nice, renice | Task Manager-এ Set priority; start /low cmd; (Get-Process -Id 1234).PriorityClass = 'BelowNormal' |
| “load average” | /proc/loadavg | সরাসরি নেই; কাছাকাছি হলো Performance Monitor-এর Processor Queue Length |
Get-Process -Name notepad | Select-Object Id, CPU, WorkingSet64, StartTime
Stop-Process -Name notepad # -Force ছাড়া আগে চেষ্টা করুন
# উদাহরণ: WorkingSet64 মোটামুটি Linux-এর RSS-এর মতো — RAM-এ থাকা অংশ (byte-এ)macOS-এ ps, top, kill, job control সবই আছে (BSD ঘরানার, তাই top-এর option ভিন্ন, যেমন top -o cpu), কিন্তু /proc নেই — তথ্য আসে sysctl আর বিশেষ system call থেকে; গ্রাফিক্যাল দৃশ্য Activity Monitor.
আরও গভীরে: Linux kernel load average floating-point ছাড়াই হিসাব করে — ১১-bit fixed-point সংখ্যায় (FIXED_1 = 2048), প্রতি ৫ সেকেন্ডের একটু বেশি পরপর; ১ মিনিটের ক্ষয়-গুণক EXP_1 = 1884, অর্থাৎ 1884/2048 ≈ 0.920 ≈ e−5/60 (kernel/sched/loadavg.c). বহু-CPU যন্ত্রে প্রতিটি CPU-র run queue নিজের অংশ জমা রাখে আর মাঝে মাঝে বৈশ্বিক যোগফলে মেশায়, যাতে প্রতিটি tick-এ সব CPU একটি lock নিয়ে কাড়াকাড়ি না করে। আর terminal-এর Ctrl+C-কে signal-এ বদলানো হয় tty-এর line discipline (n_tty)-এ — stty -a দেখালে intr = ^C; susp = ^Z লেখা থাকে, আর চাইলে stty intr ^X দিয়ে কি-টিই বদলানো যায়।
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Shell একটি কমান্ড কীভাবে চালায়? — পড়ে, শব্দে ভাঙে, builtin না হলে fork করে, child-এ exec, parent-এ waitpid; শেষে exit status
$?-এ। - Ctrl+C আর Ctrl+Z-এর পার্থক্য? — দুটিই terminal driver foreground process group-কে পাঠায়: SIGINT (ডিফল্টে শেষ) আর SIGTSTP (থামায়, পরে
fg/bg). - SSH logout-এর পর background job কেন মরে, কীভাবে বাঁচাবেন? — terminal hangup-এ shell SIGHUP পায় আর job-দের পাঠায়;
nohup,disown, tmux/screen বা systemd সার্ভিস। - Load average ৮ মানে কী? — গত ১ মিনিটে গড়ে ৮টি task চলছিল, CPU-র অপেক্ষায় ছিল বা (Linux-এ) D-অবস্থায় I/O-তে আটকে ছিল; CPU-র সংখ্যার সাথে তুলনা করে বিচার করতে হয়।
killআরkill -9-এর পার্থক্য? — SIGTERM ধরা যায়, প্রোগ্রাম গুছিয়ে বের হতে পারে; SIGKILL ধরা যায় না, kernel সাথে সাথে শেষ করে — শেষ উপায়।
১০ · AI-এর যুগে process সামলানো — কমান্ড আর কোড যাচাই
“সার্ভার ধীর, কী করব?” — AI সাথে সাথে পাঁচটি কমান্ড দেবে। কিন্তু AI আপনার সার্ভার দেখে না: কোন distro, কোন container, কতগুলো CPU, কোন process আসলে জরুরি — কিছুই জানে না। এই বিষয়ে AI-এর ভুলগুলো বেশ নির্দিষ্ট ধাঁচের:
| AI-এর ভুল | যা ঘটে | কীভাবে ধরবেন |
|---|---|---|
প্রথমেই kill -9 বা sudo pkill -9 -f … | ডেটাবেস বা অ্যাপ গোছাতে পারে না; -f-এর বিস্তৃত নকশায় ভুল process মরে | আগে pgrep -a-এ তালিকা দেখুন; প্রথমে SIGTERM, অপেক্ষা, তারপর বিবেচনা |
ps aux | grep x | awk '{print $2}' | xargs kill | grep নিজেকেও ধরে; একই শব্দওয়ালা অন্য process-ও মরে | pgrep/pkill ব্যবহার, নির্দিষ্ট নাম; আগে শুধু তালিকা ছাপিয়ে দেখা |
| “load 4 মানে CPU 400%” বা load দেখেই CPU বাড়ানোর পরামর্শ | I/O সমস্যায় টাকা খরচ, সমস্যা থেকে যায় | CPU-র সংখ্যা, wa, D-অবস্থার task মিলিয়ে দেখা |
macOS-এ /proc, Linux-এ BSD top-এর option, Windows-এ ps aux | “No such file” বা ভুল আউটপুট | প্রশ্নে OS বলুন; man ps, man top মিলিয়ে নিন |
| নিজের double-fork daemon কোড লিখে দেওয়া | PID ফাইল, log, restart — সব হাতে, বাগে ভরা | systemd unit (Type=simple) বা systemd-run-এর কথা জিজ্ঞেস করুন |
C shell কোডে fflush নেই, exec ব্যর্থ হলে _exit নেই, background child-এর waitpid নেই | দুবার ছাপা আউটপুট, দুটি shell, zombie জমা | stdout pipe-এ পাঠিয়ে চালানো, ভুল কমান্ড দিয়ে পরীক্ষা, ps-এ Z খোঁজা |
AI-কে process-সংক্রান্ত প্রশ্ন করার সময় পরিবেশ, প্রমাণ আর নিরাপত্তার শর্ত দিন:
AI-এর দেওয়া process কমান্ড বা shell কোড ব্যবহারের আগে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Linux-এর load average-এ D-অবস্থার (uninterruptible) task কেন গোনা হয়? এটি কি ভালো নকশা, নাকি ইতিহাসের একটি দুর্ঘটনা?
Load average-এর ধারণা Unix-এর চেয়েও পুরোনো — ১৯৭০-এর দশকের শুরুর TENEX-এর মতো time-sharing সিস্টেমেও এর মতো একটি মাপ ছিল, আর মূল ভাবনা ছিল সহজ: CPU-র জন্য কতজন লাইনে দাঁড়িয়ে আছে। বেশিরভাগ Unix সিস্টেম আজও শুধু runnable task গোনে। Linux আলাদা পথ নিয়েছে ১৯৯৩ সালের একটি পরিবর্তন থেকে, যখন D-অবস্থার task-কেও যোগ করা হয়। ২০১৭ সালে performance-বিশেষজ্ঞ Brendan Gregg পুরোনো mailing list ঘেঁটে সেই patch খুঁজে বের করেন; তাতে যুক্তি ছিল মোটামুটি এই: ধীর swap ডিস্কে আটকে থাকা যন্ত্রে শুধু CPU গুনলে load কমে যায়, অথচ ব্যবহারকারীর অভিজ্ঞতায় যন্ত্রটি আরও ধীর — তাই load-এর মাপা উচিত “সিস্টেমের সম্পদের চাহিদা”, শুধু CPU-র নয়।
এই যুক্তির শক্তি আছে। ব্যবহারকারী জানতে চান “যন্ত্র কতটা চাপে আছে”, আর ডিস্কের অপেক্ষাও চাপ। একটি সংখ্যায় CPU আর I/O দুটোর চাপ ধরা পড়ে। দুর্বলতাও স্পষ্ট: সংখ্যাটি আর “CPU-র লাইন” নয়, তাই CPU-র সংখ্যার সাথে তুলনা করে সিদ্ধান্ত নেওয়া সবসময় ঠিক নয়। পরে kernel-এ আরও অনেক কারণে D-অবস্থা ব্যবহার শুরু হয় — কিছু lock-এর অপেক্ষাও এতে পড়ে — ফলে load আরও মিশ্র একটি মাপ হয়ে গেছে। Linux পরে TASK_IDLE নামে একটি অবস্থা যোগ করে, যাতে কিছু kernel thread uninterruptible ঘুমেও load বাড়ায় না — ছোট একটি সংশোধন।
বাস্তব শিক্ষা: load average একটি সতর্কসংকেত, রোগনির্ণয় নয়। Load বেশি দেখলে পরের প্রশ্ন — CPU (us, sy বেশি, R-task বেশি) না I/O (wa বেশি, D-task বেশি)? আধুনিক Linux (৪.২০ থেকে) এর জন্য আরও স্পষ্ট মাপ দেয়: /proc/pressure/cpu, io, memory — Pressure Stall Information, যা বলে কত শতাংশ সময় task-রা কোন সম্পদের অভাবে আটকে ছিল। পর্ব ৫৭-এ পারফরম্যান্স তদন্তের পদ্ধতিতে এগুলো কাজে লাগাবেন।
মূল উপলব্ধি: একটি সংখ্যার মানে ঠিক করে তার হিসাবের নিয়ম, নাম নয়। “Load” একই শব্দ হলেও Linux আর BSD-তে ভিন্ন জিনিস মাপে — কোনো metric বিশ্বাস করার আগে জানুন সেটি কী গোনে।
প্র ০২Job control-এর আগে মানুষ কীভাবে কাজ করতেন? Process group আর session-এর মতো জটিল কাঠামো কেন দরকার হলো, আর GUI-এর যুগে এগুলো কি এখনো প্রাসঙ্গিক?
প্রথম দিকের Unix-এ & ছিল, কিন্তু job control ছিল না। একবার কোনো কমান্ড foreground-এ চালালে তাকে পেছনে পাঠানোর উপায় ছিল না; হয় শেষ হওয়ার অপেক্ষা, নয়তো interrupt দিয়ে মেরে ফেলা। Background কাজের দেখাশোনা মানে PID মনে রাখা আর ps চালানো। একটি terminal-এ একসাথে কয়েকটি কাজ সামলাতে চাইলে কয়েকটি আলাদা terminal যন্ত্র লাগত — যা তখন দামি জিনিস।
Job control প্রথম আসে C shell-এ; Jim Kulp নামের একজন প্রোগ্রামার অস্ট্রিয়ার IIASA গবেষণা প্রতিষ্ঠানে কাজ করার সময় এটি তৈরি করেন, আর তা ৪.১BSD kernel-এর সুবিধা ব্যবহার করে BSD-তে যুক্ত হয় (১৯৮০-এর দশকের শুরুতে)। সমস্যাটি ছিল কঠিন: একটি terminal, অনেক process, আর কিবোর্ডের ইনপুট বা Ctrl+C কাকে যাবে? সমাধান হলো process group — একটি job-এর সব process একটি দল, আর terminal মনে রাখে কোন দল এখন foreground. পরে System V আর BSD-এর পার্থক্য মেটাতে POSIX মানদণ্ড (১৯৮৮) session-এর ধারণা যুক্ত করে: একটি login, একটি controlling terminal, তার অধীনে অনেক group — যাতে এক login-এর job অন্য login-এর terminal দখল করতে না পারে।
GUI-এর যুগেও এই কাঠামো প্রতিদিন কাজ করে, শুধু চোখে পড়ে না। প্রতিটি SSH সংযোগ একটি session; VS Code-এর ভেতরের terminal একটি pseudo-terminal, আর সেখানে Ctrl+C ঠিক এই নিয়মেই আপনার npm run dev থামায়। docker run -it container-এর ভেতরে pseudo-terminal দেয়, আর docker stop SIGTERM পাঠিয়ে ১০ সেকেন্ড অপেক্ষা করে। tmux আর screen পুরোপুরি এই ধারণার ওপর দাঁড়ানো। এমনকি Android-এর প্রতিটি অ্যাপ process আর তাদের group Linux-এর একই kernel কাঠামো ব্যবহার করে।
Windows ভিন্ন পথ নিয়েছে: সেখানে console-এর নিজস্ব “console process group” আর Ctrl+C-এর জন্য CTRL_C_EVENT আছে, আর process-দের একসাথে সামলাতে “Job Object” নামের kernel বস্তু — যা সীমা বেঁধে দেওয়া আর একসাথে বন্ধ করার কাজে লাগে, Unix-এর shell job-এর মতো foreground/background বদলের জন্য নয়।
মূল উপলব্ধি: process group আর session জটিল মনে হয়, কিন্তু প্রতিটি স্তর একটি বাস্তব প্রশ্নের উত্তর — “Ctrl+C কে পাবে?”, “কিবোর্ড কে পড়বে?”, “লাইন কাটলে কাকে জানাব?” কোনো কাঠামো দেখে প্রথমে জিজ্ঞেস করুন, এটি কোন সমস্যা সমাধান করে।
প্র ০৩Linux kernel-এর তথ্য দেখায় /proc-এর “ফাইল” দিয়ে, Windows দেয় API আর WMI দিয়ে, FreeBSD আর macOS মূলত sysctl দিয়ে। “সবকিছুই ফাইল” নকশার সুবিধা আর দাম কী?
/proc-এর ধারণা এসেছে Bell Labs থেকে। ১৯৮৪ সালে Tom Killian “Processes as Files” নামের একটি লেখায় দেখান, Eighth Edition Unix-এ প্রতিটি process-কে একটি ফাইল হিসেবে দেখালে debugger সাধারণ open, read, write দিয়েই অন্য process-এর মেমরি পড়তে পারে। Plan 9 এই ভাবনাকে আরও দূরে নেয় — সেখানে প্রায় সব kernel সেবাই ফাইল। Linux-এর /proc process-এর বাইরেও বিস্তৃত হয়েছে: /proc/meminfo, /proc/cpuinfo, /proc/sys-এর মাধ্যমে kernel-এর সেটিং বদলানো পর্যন্ত। পরে device-এর তথ্যের জন্য আলাদা /sys (sysfs) আসে, যাতে /proc আরও এলোমেলো না হয়।
সুবিধা বিশাল। কোনো বিশেষ library বা নতুন system call ছাড়াই cat, grep, shell script, যেকোনো ভাষা kernel-এর তথ্য পড়তে পারে — এই পর্বের runnable ব্লকেই দেখেছেন। ফাইলের অনুমতির নিয়মেই নিরাপত্তা আসে: অন্যের /proc/<pid>/fd আপনি দেখতে পান না। নতুন তথ্য যোগ করতে kernel ডেভেলপার শুধু একটি নতুন ফাইল যোগ করেন।
দামও আছে। (১) লেখা parse করা ধীর আর ভঙ্গুর — একটি কলাম যোগ হলে পুরোনো parser ভাঙতে পারে, তাই kernel অনেক ফাইলের ফরম্যাট কখনো বদলায় না। হাজার process-এর যন্ত্রে top প্রতি সেকেন্ডে হাজার হাজার ফাইল খোলে-পড়ে। (২) Race: আপনি /proc/1234/status পড়ছেন, এর মধ্যে 1234 মরে গিয়ে একই PID-এ নতুন process জন্মাতে পারে; Linux পরে pidfd যোগ করে এই সমস্যার সমাধান দেয়। (৩) তথ্য ফাঁস: ডিফল্টভাবে সব user সবার কমান্ড লাইন দেখতে পান — তাই কমান্ড লাইনে password দেওয়া বিপজ্জনক; /proc mount করার সময় hidepid option দিয়ে এটি সীমিত করা যায়।
অন্যরা অন্য পথ বেছেছে। FreeBSD-এ procfs ঐচ্ছিক, প্রধান উপায় sysctl আর libprocstat-এর মতো library — গঠনবদ্ধ binary তথ্য, দ্রুত কিন্তু shell থেকে কম সহজ। macOS-এ /proc নেই। Windows-এ NtQuerySystemInformation-এর মতো API, WMI আর ETW — শক্তিশালী আর গঠনবদ্ধ, তবে একটি লাইনের script-এ ব্যবহার কঠিন; PowerShell-এর object-pipe অনেকটা এই ফাঁক পূরণ করে।
মূল উপলব্ধি: interface সহজ হলে সবাই ব্যবহার করতে পারে, কিন্তু গতি, গঠন আর নিরাপত্তার কিছু দাম দিতে হয়। পর্ব ০৩-এর “লেখা বনাম object pipe” প্রশ্নটি এখানে kernel-এর স্তরে ফিরে এসেছে।
প্র ০৪Docker, Kubernetes আর systemd — সবাই কোনো কিছু থামানোর সময় আগে SIGTERM, পরে SIGKILL পাঠায়। কেন এই দুই ধাপ? আর container-এ PID 1 হিসেবে আপনার নিজের অ্যাপ চালালে কী সমস্যা হতে পারে?
একটি চলমান প্রোগ্রামের অবস্থা শুধু মেমরিতে থাকে না, বাইরের জগতেও ছড়ানো থাকে: অর্ধেক লেখা ফাইল, ডেটাবেসের খোলা লেনদেন, ক্লায়েন্টের সাথে চলমান HTTP অনুরোধ, lock ফাইল, queue থেকে নেওয়া কিন্তু শেষ না হওয়া কাজ। SIGKILL হঠাৎ সব থামিয়ে দেয় — kernel মেমরি আর fd পরিষ্কার করে, কিন্তু বাইরের এই অর্ধেক কাজগুলো গোছানোর কেউ থাকে না। SIGTERM বলে “আপনার হাতে কিছু সময় আছে”: ভালো প্রোগ্রাম নতুন অনুরোধ নেওয়া বন্ধ করে, চলমানগুলো শেষ করে, flush করে, তারপর নিজেই বেরিয়ে যায় — একে বলে graceful shutdown.
কিন্তু কোনো প্রোগ্রাম আটকে থাকলে বা SIGTERM উপেক্ষা করলে চিরকাল অপেক্ষা করা যায় না। তাই সবাই একটি সময়সীমা রাখে: docker stop ডিফল্টভাবে ১০ সেকেন্ড, Kubernetes-এর terminationGracePeriodSeconds ডিফল্ট ৩০ সেকেন্ড, systemd-এর ডিফল্ট stop timeout ৯০ সেকেন্ড; সময় পেরোলে SIGKILL. আপনার অ্যাপের দায়িত্ব এই সময়ের মধ্যে গুছিয়ে নেওয়া।
Container-এ একটি বিশেষ ফাঁদ আছে। Linux-এ একটি PID namespace-এর PID 1-কে kernel বিশেষভাবে রক্ষা করে: যে signal-এর জন্য PID 1 কোনো handler বসায়নি, namespace-এর ভেতর থেকে আসা সেই signal-এর ডিফল্ট আচরণ (মারা যাওয়া) তার ওপর খাটে না। ফলে আপনার অ্যাপ PID 1 হলে আর SIGTERM-এর handler না থাকলে docker stop ১০ সেকেন্ড বৃথা অপেক্ষা করে SIGKILL পাঠায় — প্রতিবার অগোছালো বন্ধ। দ্বিতীয় দায়িত্ব: PID 1 অনাথ process-দের দত্তক নেয়, তাই zombie reap করার কাজও তার; সাধারণ অ্যাপ সেটি করে না। সমাধান হলো একটি ছোট init — docker run --init (tini), বা podman-এর podman-init, যা এই পর্বের runnable ব্লকে PID 1 হিসেবে দেখেছেন। এরা signal সন্তানের কাছে পৌঁছে দেয় আর zombie পরিষ্কার করে।
আরেকটি পরিচিত ভুল: Dockerfile-এ CMD ./start.sh (shell form) লিখলে অনেক ক্ষেত্রে PID 1 হয় /bin/sh, যে SIGTERM অ্যাপের কাছে পৌঁছে দেয় না। CMD ["./start.sh"] (exec form) আর script-এর শেষে exec ./app লিখলে অ্যাপ নিজেই signal পায় — এই পর্বের প্রথম অংশের builtin exec-এর বাস্তব প্রয়োগ।
মূল উপলব্ধি: “থামানো” একটি কথোপকথন — অনুরোধ, সময়, তারপর জোর। নিজের প্রোগ্রামে SIGTERM-এর উত্তর দিতে শিখুন (পর্ব ১৩), আর container-এ PID 1 কে, তা সবসময় জানুন।
প্র ০৫পুরোনো daemon নিজেকে double-fork করে, setsid করে “আলাদা” করত; systemd বলে daemon-কে এসব করতেই হবে না। কোন সমস্যার কারণে এই পরিবর্তন এল?
ক্লাসিক Unix-এ init (PID 1) শুধু বুটের সময় কিছু script চালাত। Script থেকে চালানো সার্ভিসকে নিজেই “daemon” হয়ে উঠতে হতো: fork করে parent বেরিয়ে যায় যাতে script পরের লাইনে যেতে পারে; child setsid() করে নতুন session খোলে, কোনো controlling terminal নেই, তাই SIGHUP-এর ভয় নেই; আবার fork করে, যাতে session leader না থাকায় কোনো terminal খুললেও সেটি তার controlling terminal না হয়ে যায়; তারপর chdir("/") (কোনো ডিস্ক unmount আটকে না রাখতে), umask ঠিক করা, সব fd বন্ধ আর stdin/stdout/stderr /dev/null-এ, শেষে একটি PID ফাইল লেখা, যাতে পরে kill $(cat /var/run/x.pid) করা যায়।
সমস্যাগুলো জমতে থাকে। প্রতিটি প্রোগ্রাম এই আচারটি নিজের মতো করে লিখত, অনেকে ভুল করত। PID ফাইল পুরোনো হয়ে যেত — সার্ভিস ক্র্যাশ করার পর একই PID-এ অন্য কেউ জন্মালে ভুল process মরত। Double-fork-এর পর কে কার সন্তান, ট্র্যাক করা কঠিন; একটি সার্ভিসের ছড়ানো সব child খুঁজে থামানো প্রায় অসম্ভব। Log যেত syslog-এ বা হারিয়ে যেত। আর সার্ভিস পড়ে গেলে কেউ জানতেই পারত না, আবার চালানো তো দূরের কথা।
২০১০ সালে Lennart Poettering আর Kay Sievers systemd প্রকাশ করেন, আর এর নকশা অন্যভাবে ভাবে: সার্ভিস সাধারণ প্রোগ্রামের মতো foreground-এ চলুক (Type=simple বা exec), stdout-এ log লিখুক — systemd সেটি journal-এ রাখবে। প্রতিটি সার্ভিস তার নিজের cgroup-এ (পর্ব ৫১) রাখা হয়, তাই যত fork-ই করুক, kernel জানে কোন process কোন সার্ভিসের; থামানোর সময় পুরো cgroup থামে। Restart=on-failure দিলে পড়ে গেলে আবার চালায়। Linux ৩.৪ থেকে “child subreaper” সুবিধাও আছে, যাতে PID 1 ছাড়া অন্য একটি process-ও অনাথ বংশধরদের দত্তক নিতে পারে। এই নকশা নিয়ে বিতর্কও কম হয়নি — systemd অনেক কাজ এক জায়গায় জড়ো করেছে বলে সমালোচনা আছে, আর কিছু distro অন্য init ব্যবহার করে। তবু “সার্ভিসকে নজরদারি করবে একজন supervisor, সার্ভিস নিজে নয়” — এই নীতিটি আজ প্রায় সর্বজনীন; Docker, Kubernetes আর macOS-এর launchd-ও একই ধারণায় চলে।
মূল উপলব্ধি: যে দায়িত্ব প্রতিটি প্রোগ্রাম আলাদাভাবে ভুল করে, তা একটি কেন্দ্রীয় জায়গায় সরিয়ে আনলে সিস্টেম নির্ভরযোগ্য হয়। পুরোনো আচার জানা দরকার — পুরোনো কোড আর ইন্টারভিউর প্রশ্ন বুঝতে — কিন্তু নতুন সার্ভিস লিখুন systemd unit দিয়ে।
অনুশীলন
প্রথম তিনটি কাগজ-কলমে করুন; চতুর্থটি কোড লিখে উপরের mysh-এর ব্লকে পরীক্ষা করুন।
-
Load average হাতে হিসাব (numerical): একটি ৪-CPU Linux সার্ভার অনেকক্ষণ পুরো অলস ছিল (তিনটি load-ই ০)। হঠাৎ ৪টি CPU-নির্ভর process চালু হলো আর টানা চলতে থাকল (প্রতি মুহূর্তে n = ৪)। সূত্র
load(t) = n × (1 − e−t/m)(শুরুতে load = ০ ধরে, t আর m মিনিটে) ব্যবহার করে বের করুন: (ক) ১ মিনিট পর তিনটি load average কত? (খ) ৫ মিনিট পর? (গ) এ থেকে ১৫ মিনিটের সংখ্যা সম্পর্কে কী শিখলেন?(ক) t = ১: ১-মিনিট = 4 × (1 − e−1) = 4 × 0.632 ≈ 2.53; ৫-মিনিট = 4 × (1 − e−1/5) = 4 × 0.181 ≈ 0.73; ১৫-মিনিট = 4 × (1 − e−1/15) = 4 × 0.0645 ≈ 0.26. top দেখাবে মোটামুটি
2.53, 0.73, 0.26.(খ) t = ৫: ১-মিনিট = 4 × (1 − e−5) ≈ 3.97; ৫-মিনিট = 4 × (1 − e−1) ≈ 2.53; ১৫-মিনিট = 4 × (1 − e−1/3) ≈ 4 × 0.283 ≈ 1.13.
(গ) “১৫ মিনিটের load” মানে গত ১৫ মিনিটের সাধারণ গড় নয় — t = m হলে সংখ্যাটি আসল মানের মাত্র প্রায় ৬৩% (1 − e−1)-এ পৌঁছায়, আর প্রায় ৯৯%-এ পৌঁছাতে লাগে ৫m সময় (১-মিনিটেরটি ৫ মিনিটে 3.97 — তাই)। এজন্যই হঠাৎ চাপ এলে ১-মিনিটের সংখ্যা দ্রুত ওঠে আর ১৫-মিনিটেরটি ধীরে — তিনটির তুলনা থেকে প্রবণতা বোঝা যায়। (kernel আসলে ৫ সেকেন্ড পরপর ধাপে হিসাব করে, তাই ফল এর খুব কাছাকাছি হয়।)
-
/proc/statথেকে CPU ব্যবহার (numerical):/proc/stat-এরcpuলাইনের প্রথম আটটি ঘর হলো user, nice, system, idle, iowait, irq, softirq, steal (একক: clock tick). দুটি মুহূর্তের মান — A:cpu 1000 0 400 8000 100 0 50 0, B:cpu 1600 0 640 8600 140 0 70 0. এই সময়ে CPU কত শতাংশ ব্যস্ত ছিল? iowait কত শতাংশ? top-এরusআরsyকত দেখাত?পার্থক্য (B − A): user 600, nice 0, system 240, idle 600, iowait 40, irq 0, softirq 20, steal 0. মোট Δ = 600 + 240 + 600 + 40 + 20 = 1500 tick.
অলস = idle + iowait = 600 + 40 = 640. ব্যস্ত = 1500 − 640 = 860 → 860 / 1500 ≈ 57.3%. iowait = 40 / 1500 ≈ 2.7%. top দেখাত মোটামুটি
us= 600/1500 = 40.0,sy= 240/1500 = 16.0,si= 20/1500 ≈ 1.3,id= 40.0,wa≈ 2.7 — যোগফল ১০০. মূল শিক্ষা:/proc/stat-এর সংখ্যা বুট থেকে জমা হওয়া মোট, তাই একটি মুহূর্তের মান দিয়ে কিছু বোঝা যায় না — দুটি নমুনার পার্থক্য নিতে হয়;topআরvmstatঠিক এটাই করে। -
আউটপুট অনুমান করুন: একটি interactive bash-এ পরপর টাইপ করা হলো —
sleep 100 &,sleep 200 &,jobs,kill %1,jobs,fg, তারপর Ctrl+Z, তারপরjobs. প্রতিটিjobsকী দেখাবে?fgকোন job-কে সামনে আনবে? (PID-গুলো[1] 7001,[2] 7002ধরুন)প্রথম দুটি লাইন ছাপায়
[1] 7001আর[2] 7002. প্রথমjobs:[1]- Running sleep 100 &আর[2]+ Running sleep 200 &— নতুনতমটি বর্তমান (+).kill %1job ১-কে SIGTERM পাঠায়। দ্বিতীয়jobs:[1]- Terminated sleep 100আর[2]+ Running sleep 200 &— bash শেষ হওয়া job-এর খবর একবার দেখিয়ে তালিকা থেকে সরিয়ে দেয়। (sleep যদি পরের prompt আসার আগেই মরে যায়, bashTerminatedলাইনটি prompt-এর ঠিক আগেই দেখিয়ে ফেলে; তখন দ্বিতীয়jobs-এ শুধু[2]+ Runningথাকে — সময়ের ওপর নির্ভর করে।)fg(কোনো নম্বর ছাড়া) বর্তমান job, অর্থাৎ%2-কে আনে, আর ছাপায়sleep 200. Ctrl+Z-এ তা থেমে যায়:[2]+ Stopped sleep 200. শেষjobs: শুধু[2]+ Stopped sleep 200— job নম্বর ২-ই থাকে, পুনরায় সংখ্যায়ন হয় না। এখনbgদিলে চলবে,exitলিখলে bash সতর্ক করবেThere are stopped jobs. -
কোড লিখুন — mysh-এ
&: উপরের C shell-কে এমনভাবে বদলান, যাতে লাইনের শেষ শব্দ&হলে shell অপেক্ষা না করে[bg] <pid>ছাপিয়ে পরের prompt দেয়। শর্ত: শেষ হয়ে যাওয়া background child যেন zombie হয়ে জমে না থাকে। কোথায় কী যোগ করবেন?তিনটি পরিবর্তন: (১) parse-এর পর
&চিনে সরানো; (২) fork-এর পর background হলেwaitpidনা করা; (৩) প্রতিবার prompt দেখানোর আগেWNOHANGদিয়ে শেষ হয়ে যাওয়া child-দের reap করা।/* লুপের শুরুতে, prompt-এর আগে: শেষ হওয়া সব background child reap */ pid_t done; while ((done = waitpid(-1, NULL, WNOHANG)) > 0) printf("[done] %d\n", (int)done); /* parse-এর পরে */ int bg = 0; if (argc > 0 && strcmp(argv[argc - 1], "&") == 0) { bg = 1; argv[--argc] = NULL; /* "&" exec-এর argument নয় */ } /* fork-এর পরে, parent-এ */ if (bg) { printf("[bg] %d\n", (int)pid); continue; } waitpid(pid, &status, 0);WNOHANGমানে: কেউ শেষ না হলে অপেক্ষা না করে সাথে সাথে ০ ফেরত দেওয়া, তাই prompt আটকায় না। আরও ভালো সমাধান SIGCHLD-এর handler-এ reap করা (পর্ব ১৩)। পরীক্ষা: stdin-এsleep 1 &,echo hi,sleep 2,echo afterদিন —[bg], তারপরhi, তারপর ২ সেকেন্ড পর পরের prompt-এর আগে[done]আসা উচিত। background-এর জন্য argc ০ হয়ে গেলে (শুধু&লেখা) সেটিও সামলাতে ভুলবেন না।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি বাইরের কমান্ড (যেমন ls -l) foreground-এ চালাতে shell যা যা করে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ⌨️ prompt দেখিয়ে একটি লাইন পড়া
- ✂️ লাইনটিকে শব্দে ভাঙা
- 🔍 কমান্ডটি builtin কি না দেখা
- 🍴 fork করে child process তৈরি
- 🚀 child-এ exec দিয়ে নতুন প্রোগ্রাম বসানো
- ⏳ parent-এ waitpid, exit status রেখে আবার prompt
Unix-ধরনের shell-এ cd কেন আলাদা প্রোগ্রাম না হয়ে builtin হতে বাধ্য?
- কারণ fork খুব ধীর
- কারণ chdir ডাকতে root লাগে
- কারণ আলাদা প্রোগ্রাম শুধু নিজের (child-এর) working directory বদলাতে পারত, shell-এরটি নয়
- কারণ PATH-এ cd নামের ফাইল রাখা যায় না
Linux terminal-এ একটি pipeline (sort big.txt | uniq -c | head) চলার সময় Ctrl+Z চাপলে কী ঘটে?
- shell শুধু sort-কে SIGKILL পাঠায়
- terminal driver foreground process group-এর সব process-কে SIGTSTP পাঠায়, তারা থেমে যায়
- তিনটি process-ই মারা যায় আর $? হয় ১৩০
- pipeline-এর stdin-এ EOF পাঠানো হয়
একটি ৪-CPU Linux সার্ভারে load average 9.5, অথচ top-এ CPU-র id প্রায় ৬০% আর wa ৩৫%, আর অনেক process-এর S কলামে D. সবচেয়ে সম্ভাব্য ব্যাখ্যা কোনটি?
- CPU যথেষ্ট নয়, আরও CPU লাগবে
- load 9.5 মানে CPU ৯৫০% ব্যবহৃত
- অনেক zombie process জমে আছে
- অনেক task I/O-র অপেক্ষায় uninterruptible (D) অবস্থায় আটকে আছে, আর Linux সেগুলোও load-এ গোনে
ps-এর STAT কলামে একটি process-এর পাশে T দেখা গেলে তার মানে কী?
- process-টি থামানো (stopped), যেমন Ctrl+Z চাপার পর
- process-টি zombie
- process-টি terminal-এর সাথে যুক্ত
- process-টিতে অনেক thread আছে
কোনো signal নাম না দিয়ে kill 1234 লিখলে process 1234 কোন signal পায়?
- SIGKILL (৯)
- SIGINT (২)
- SIGTERM (১৫)
- SIGHUP (১)
SSH দিয়ে সার্ভারে ঢুকে একটি লম্বা script চালাতে চান, যা সংযোগ কেটে গেলেও চলতে থাকবে। নিচের কোনটি কাজ করবে?
./long.sh &nohup ./long.sh > long.log 2>&1 &./long.shচালিয়ে terminal খোলা রাখা./long.shচালিয়ে Ctrl+Z চাপা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Shell = পড়া → ভাঙা → builtin? → fork → exec → waitpid-এর লুপ; shell-এর নিজের অবস্থা বা job table ছোঁয় এমন কমান্ড (
cd,export,exit,fg) builtin হতে বাধ্য। &মানে শুধু waitpid না করা; job control চালু থাকলে প্রতিটি job নিজের process group-এ, আর terminal-এর foreground group ঠিক করে কিবোর্ড ও Ctrl+C কে পাবে।- Ctrl+C = SIGINT, Ctrl+Z = SIGTSTP — পাঠায় kernel-এর terminal driver, পুরো foreground group-কে;
bg/fgSIGCONT আরtcsetpgrpদিয়ে কাজ করে। - Session → process group → process; terminal hangup-এ session leader SIGHUP পায় আর job-দের পাঠায় — বাঁচাতে
nohup,disown, tmux, systemd. ps,topপড়ে/procথেকে; STAT-এ R/S/D/T/Z; Linux-এর load average = R + D task-এর exponentially-damped গড়, CPU-র সংখ্যার সাথে তুলনা করুন;killআগে SIGTERM, SIGKILL শেষ উপায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১২ · Inter-process communication — pipe, shared memory, message queue আর socketপরবর্তী পর্বprocess-দের মধ্যে তথ্য আদান-প্রদানের সব পথ।
- অপারেটিং সিস্টেম · কোর্স হোম৬৪টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- C প্রোগ্রামিং · process আর IPCপাশের ট্র্যাকfork, exec, wait আর pipe নিজের C কোডে।
- C প্রোগ্রামিং · signal আর low-level I/OগভীরেSIGINT, SIGTERM ধরা আর নিজের handler লেখা।
- কম্পিউটার ফান্ডামেন্টালস · OS-এর ভেতরেসহজ শুরুএকেবারে নতুনদের জন্য process আর OS-এর পরিচয়।