সরাসরি পর্বে যান
পর্ব ১১ · ৬৪-এর মধ্যে · মডিউল ২

Shell কীভাবে কাজ করে — job control, ps, top আর /proc

How the Shell Works — the fork/exec Loop, Job Control, Sessions, ps, top, Load Average and /proc

Taught in Bangla, this lesson shows how a shell works: the read-parse-fork-exec-wait loop, a working mini shell in about 35 lines of C, foreground and background jobs with &, jobs, fg and bg, how Ctrl+C and Ctrl+Z reach process groups, sessions and controlling terminals and why SSH logout kills jobs, ps, pstree, top and htop columns, the real meaning of load average, /proc, kill, nice and Windows equivalents.

এই পর্বে যা শিখবেন

  • 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 — মূলত একটি অন্তহীন লুপ:

  1. পড়া (read): prompt দেখিয়ে stdin থেকে একটি লাইন পড়া। ইনপুট শেষ হলে (Ctrl+D, অর্থাৎ EOF) লুপ শেষ।
  2. ভাঙা (parse): লাইনটিকে শব্দে ভাঙা, উদ্ধৃতিচিহ্ন, |, >, & চেনা, variable আর wildcard বদলানো।
  3. Builtin কি না দেখা: কমান্ডটি shell-এর নিজের অবস্থা বদলায় এমন কিছু হলে (cd, exit, export) shell নিজেই কাজটি করে, কোনো নতুন process ছাড়া।
  4. fork: না হলে নিজের একটি কপি, অর্থাৎ child process তৈরি।
  5. exec (child-এ): child নিজের ভেতরে নতুন প্রোগ্রামটি বসায় — PATH-এ খুঁজে /usr/bin/ls.
  6. wait (parent-এ): shell waitpid দিয়ে child শেষ হওয়ার অপেক্ষা করে, exit status নিয়ে $?-এ রাখে, তারপর আবার ধাপ ১-এ।

ব্যাংকের টোকেন-কাউন্টারের কর্মকর্তার কথা ভাবুন। তিনি আপনার কাগজ নেন (পড়া), দেখে বোঝেন কী চাই (ভাঙা)। কাজটি তাঁর নিজের খাতার হলে — যেমন “আমার ঠিকানা বদলে দিন” — নিজেই করে ফেলেন (builtin). অন্য কাজ হলে একটি নতুন ফাইল খুলে (fork) সেটিকে নির্দিষ্ট ডেস্কে পাঠান (exec), তারপর ডেস্ক থেকে ফল না আসা পর্যন্ত পরের টোকেন ডাকেন না (wait). আর যদি বলেন “এটা পরে দেখলেও চলবে, আমি অন্য কাজ করি” — সেটিই &, অর্থাৎ background.

কোন কাজ shell-এর ভেতরেই করতে হয়

পর্ব ০৩-এ দেখেছেন, cd আলাদা প্রোগ্রাম হলে শুধু child-এর ফোল্ডার বদলাত। নিয়মটি আরও সাধারণ: যে কাজ shell process-এর নিজের অবস্থা বদলায়, অথবা যে তথ্য শুধু shell-এর মেমরিতে আছে, সেটি builtin হতেই হবে। কারণ fork-এর পর child যা-ই বদলাক, তা child-এর কপিতে; parent কখনো দেখে না।

Builtinshell-এর কোন অবস্থা ছোঁয়আলাদা প্রোগ্রাম হলে কী হতো
cd, pushdprocess-এর working directorychild-এর ফোল্ডার বদলে child মরে যেত
export, unset, VAR=xshell-এর environment, যা পরের child-রা পাবেশুধু ওই child-এর কপি বদলাত
exitshell process নিজেই শেষ হয়শুধু child শেষ হতো, shell চলতেই থাকত
umask, ulimitprocess-এর ফাইল-অনুমতির mask, resource-সীমাchild-এর মান বদলাত, পরের কমান্ডে প্রভাব নেই
jobs, fg, bg, waitshell-এর 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 -l PID-ও দেখায়।
  • $! — সবচেয়ে শেষে 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 কার হাতে
  1. পড়া — দুটিতেই এক। Enter চাপার পর shell লাইনটি পড়ল। এখনো কোনো process তৈরি হয়নি।
  2. ভাঙা — পার্থক্য এখানেই জন্মায়। দ্বিতীয় লাইনের শেষে & আছে। Shell এটিকে argument হিসেবে দেয় না; শুধু একটি পতাকা মনে রাখে: “এর জন্য অপেক্ষা করব না”। দুটিতেই sleep পাবে একই argv: ["sleep", "5"].
  3. fork — দুটিতেই এক। Job control চালু থাকলে (interactive shell-এ চালু থাকে) shell প্রতিটি job-কে একটি নতুন process group-এ রাখে: child-এর PID আর group নম্বর (PGID) একই — 4242 আর 4243. দৌড়-প্রতিযোগিতা এড়াতে parent আর child দুজনেই setpgid() ডাকে।
  4. Terminal কার? Foreground job হলে shell tcsetpgrp() দিয়ে terminal-কে জানায়: “এখন থেকে foreground group 4242”. তখন কিবোর্ডের ইনপুট আর Ctrl+C সেই group পাবে। Background job হলে terminal shell-এর group-এই থাকে — তাই আপনি টাইপ চালিয়ে যেতে পারেন।
  5. exec, তারপর মূল পার্থক্য। দুই child-ই exec করে sleep হয়ে গেল। প্রথম ক্ষেত্রে shell waitpid(4242)-এ ঘুমিয়ে আছে — prompt নেই। দ্বিতীয় ক্ষেত্রে shell job table-এ লিখল [1] 4243, ছাপাল, আর সাথে সাথে prompt দিল; আপনি ls চালাতে পারলেন। (ps-এর STAT-এ foreground group-এর process-এর পাশে + থাকে — S+; background-এরটির থাকে না)
  6. ৫ সেকেন্ড পর। প্রথমটিতে 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+Cforeground group-কে SIGINT (নম্বর ২)process শেষ; shell-এ $? = ১৩০
Ctrl+Zforeground group-কে SIGTSTPprocess থেমে থাকে (মরে না); shell prompt ফিরে পায়
Ctrl+\foreground group-কে SIGQUITprocess শেষ, অনেক সিস্টেমে core dump-সহ
Ctrl+Dকোনো signal নয় — লাইনের শুরুতে চাপলে পাঠক read() থেকে ০ byte পায়, অর্থাৎ EOFshell-এ চাপলে 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 — উদাহরণ)

kernel আর shell-এর হিসাব
signal
sleep-এর STAT
terminal-এর foreground group
bash-এর job table
bash কী করছে
  1. Foreground-এ চলছে। sleep 100 নিজের group 5001-এ, আর terminal-এর foreground group 5001. bash waitpid-এ অপেক্ষা করছে; prompt নেই। ps-এ STAT S+ — ঘুমিয়ে (টাইমারের অপেক্ষায়), আর + মানে foreground group-এর সদস্য।
  2. Ctrl+Z. Terminal driver অক্ষরটি চিনে foreground group 5001-কে SIGTSTP পাঠাল; sleep থেমে গেল (STAT T). bash-এর waitpid (WUNTRACED পতাকা দিয়ে ডাকা) ফিরে জানাল “child থেমেছে”। bash terminal নিজের group 4000-কে ফেরত দিল, job table-এ Stopped লিখল, prompt দিল। Driver ^Z লেখাটিও দেখায়।
  3. jobs. Builtin, তাই কোনো fork নেই — bash নিজের মেমরির তালিকা ছাপাল: [1]+ Stopped sleep 100. Process বেঁচে আছে, মেমরিও ধরে রেখেছে, কিন্তু CPU পাচ্ছে না।
  4. bg. bash group 5001-কে SIGCONT পাঠাল; sleep আবার চলতে লাগল, কিন্তু terminal bash-এর কাছেই রইল। bash লিখল [1]+ sleep 100 & — যেন শুরু থেকেই & দিয়ে চালানো। STAT এখন S, + ছাড়া।
  5. আবার jobs. এবার Running. এই সময়ে আপনি অন্য কমান্ড চালাতে পারেন; sleep পেছনে নিজের কাজ করছে।
  6. fg. bash কমান্ডটির নাম ছাপাল (sleep 100), tcsetpgrp() দিয়ে terminal-কে group 5001-এর হাতে দিল, নিয়ম অনুযায়ী SIGCONT পাঠাল (চলমান process-এ এটি নিরীহ), তারপর আবার waitpid-এ। STAT আবার S+.
  7. 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 কাজ তিনভাবে চালানো ছিল:

  1. sshdসংযোগpty বন্ধ করে
  2. kernel · tty/dev/pts/0hangup
  3. bashsession leaderSIGHUP পায়, job-দের পাঠায়
  4. ফলবাঁচল / মরলকার কী হলো
তিনটি কাজের অবস্থা
./report.sh &
nohup ./backup.sh &
tmux-এর ভেতরে ./train.sh
bash
  1. আগের অবস্থা। আপনি SSH দিয়ে সার্ভারে ঢুকেছেন। sshd একটি pseudo-terminal বানিয়ে child-এ setsid() করেছে, তাই bash একটি নতুন session-এর leader আর /dev/pts/0 তার controlling terminal. তিনটি কাজ চলছে: একটি সাধারণ & দিয়ে, একটি nohup দিয়ে, আর একটি tmux-এর ভেতরে।
  2. সংযোগ কাটল। Wi-Fi চলে গেল বা ল্যাপটপের ঢাকনা বন্ধ হলো। কিছুক্ষণ পর sshd বুঝল সংযোগ মৃত (TCP timeout বা keepalive দিয়ে), তাই সে pty-র নিজের দিকের (master) মুখটি বন্ধ করল।
  3. Kernel-এর hangup. Master বন্ধ হওয়া মানে terminal-এর “লাইন কেটে গেছে”। Kernel terminal-এর controlling process — session leader bash — কে SIGHUP পাঠায়। এখন থেকে এই terminal-এ লেখার চেষ্টা ব্যর্থ হবে (EIO).
  4. bash job-দের জানায়। SIGHUP-এ বেরিয়ে যাওয়ার আগে interactive bash নিজের job table-এর প্রতিটি job-কে SIGHUP পাঠায় (থেমে থাকা job-কে সাথে SIGCONT-ও, যাতে সংকেতটি পৌঁছায়)। report.sh-এর ডিফল্ট আচরণ মারা যাওয়া। nohup চালানোর সময়ই SIGHUP-কে “উপেক্ষা” করে রেখেছিল, তাই backup.sh কিছু টের পায় না। tmux-এর কাজটি bash-এর job-ই নয় — tmux server নিজে আলাদা session-এ চলে।
  5. ফল। 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 বা screenterminal-টিই একটি আলাদা 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 auxa সব 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 -pparent-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 কলাম — এক-দুই অক্ষরে পুরো গল্প

অক্ষরমানেকখন দেখবেন
RRunning বা runnable — CPU-তে চলছে অথবা run queue-তে অপেক্ষাহিসাবের কাজ, এই মুহূর্তের ps নিজে
SInterruptible sleep — কোনো ঘটনার অপেক্ষা; signal এলে জাগবেবেশিরভাগ process বেশিরভাগ সময়
DUninterruptible sleep — সাধারণত ডিস্ক বা নেটওয়ার্ক ফাইল সিস্টেমের I/O; signal-এও জাগে না, kill -9-ও তখনই কাজ করে নাধীর ডিস্ক, আটকে যাওয়া NFS
TStopped — job control signal-এ থামানো (Ctrl+Z)jobs-এ Stopped
tdebugger-এর জন্য থামানো (tracing stop)gdb দিয়ে চালালে
ZZombie — শেষ, কিন্তু parent exit status নেয়নিparent-এর বাগ (পর্ব ১০)
IIdle kernel thread[kworker/…] জাতীয় kernel thread
পরের অক্ষরs session leader, + foreground group, l বহু-thread, < উঁচু priority, N নিচু priority (nice), L মেমরিতে আটকানো pageSs, 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
যা বোঝা গেল
পড়ছি
মূল সংখ্যা
সিদ্ধান্ত
  1. সময় আর uptime. এখন সকাল ১০:১৫, যন্ত্রটি ৩ দিন ২ ঘণ্টা ৪১ মিনিট ধরে চালু, আর ১ জন user login করে আছেন। একই লাইন uptime কমান্ডও দেখায়।
  2. Load average 2.15, 1.40, 0.90. CPU ৪টি, তাই ১ মিনিটের গড়ে প্রতি CPU-তে 2.15 / 4 ≈ 0.54 — মোটামুটি অর্ধেক সামর্থ্য ব্যবহার হচ্ছে, কেউ লাইনে দাঁড়িয়ে নেই বললেই চলে। তবে ১৫ মিনিটে 0.90, ৫ মিনিটে 1.40, ১ মিনিটে 2.15 — চাপ সম্প্রতি বাড়ছে। এটি শতাংশ নয়; আর Linux-এ D-অবস্থার task-ও এর ভেতরে।
  3. Tasks. মোট 231 = 2 running + 228 sleeping + 0 stopped + 1 zombie. বেশিরভাগ সময় বেশিরভাগ task ঘুমিয়ে থাকে — এটাই স্বাভাবিক। ১টি zombie মানে কোনো parent তার child-এর exit status নিচ্ছে না; একটি-দুটি ক্ষতিকর নয়, কিন্তু সংখ্যা বাড়তে থাকলে parent প্রোগ্রামে বাগ আছে।
  4. CPU-র সময় কোথায় যাচ্ছে। সব CPU মিলিয়ে: us 23.5% user code-এ, sy 4.1% kernel-এ (system call, driver), ni nice করা process-এ, id 70.2% অলস, wa 1.9% অলস কিন্তু I/O-র অপেক্ষায়, hi/si hardware ও software interrupt, st “stolen” — virtual machine-এ hypervisor অন্য অতিথিকে দিয়েছে। যোগফল ১০০। wa কম, তাই এখানে load-এর পেছনে মূলত CPU-র কাজ।
  5. মেমরি। 7837.4 total = 512.3 free + 4210.8 used + 3114.3 buff/cache. “free মাত্র ৫১২!” দেখে ভয় পাবেন না — Linux খালি RAM ফাইলের cache হিসেবে কাজে লাগায়, দরকার হলে ছেড়ে দেয়। আসল প্রশ্নের উত্তর avail Mem 3205.6 MiB: নতুন প্রোগ্রাম swap ছাড়াই মোটামুটি এতটা পেতে পারে। Swap-এ 148 MiB ব্যবহৃত — সামান্য।
  6. Process-এর সারি। PR scheduler-এর priority, সাধারণ process-এ ২০ + NI (backup.sh: NI 19, তাই PR 39); NI nice মান; VIRT/RES/SHR KiB-তে 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) byteps -f-এর CMD, pgrep -a
/proc/<pid>/statusমানুষের পড়ার মতো সারাংশ: Name, State, PPid, Uid, VmRSS, Threads…ps, top
/proc/<pid>/fd/প্রতিটি খোলা file descriptor একটি symlink — কোন ফাইল, pipe বা socketlsof
/proc/<pid>/mapsmemory map: প্রতিটি অঞ্চলের ঠিকানা, অনুমতি (r/w/x), কোন ফাইল থেকেpmap, debugger (পর্ব ৩৫, ৩৮)
/proc/loadavgতিনটি load average, চলমান/মোট task, শেষ তৈরি PIDuptime, 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 দেখা আর থামানোর টুল আছে:

কাজLinuxWindows
গ্রাফিক্যাল দৃশ্যSystem Monitor, htopTask Manager (Ctrl+Shift+Esc) — Details ট্যাবে PID; আরও গভীরে Resource Monitor, Sysinternals Process Explorer
তালিকাps auxtasklist, tasklist /FI "IMAGENAME eq notepad.exe"; PowerShell-এ Get-Process
বেশি CPU খরচকারীtopGet-Process | Sort-Object CPU -Descending | Select-Object -First 5
ভদ্রভাবে থামানোkill PID (SIGTERM)taskkill /PID 1234 — GUI অ্যাপকে বন্ধের অনুরোধ পাঠায়
জোর করে থামানোkill -9 PIDtaskkill /PID 1234 /F, /T দিলে child-সহ; Stop-Process -Id 1234 -Force
নাম দিয়েpkill nametaskkill /IM notepad.exe, Stop-Process -Name notepad
prioritynice, reniceTask 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 killgrep নিজেকেও ধরে; একই শব্দওয়ালা অন্য 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-সংক্রান্ত প্রশ্ন করার সময় পরিবেশ, প্রমাণ আর নিরাপত্তার শর্ত দিন:

আমার Ubuntu 24.04 সার্ভারে (৪টি CPU, ৮ GB RAM, systemd আছে) load average 9.8, 7.5, 4.1. top-এর %Cpu লাইনে id 60, wa 35, আর কয়েকটি process-এর S কলামে D. নিচে top -b -n 1-এর প্রথম ২০ লাইন দিলাম। ১. এই প্রমাণ থেকে সমস্যা CPU-তে না I/O-তে, ধাপে ধাপে যুক্তি দিয়ে বলো; load average-এর হিসাবে D-অবস্থার ভূমিকা ব্যাখ্যা করো। ২. শুধু পড়ার (read-only) কমান্ড দাও যা দিয়ে আরও নিশ্চিত হতে পারি — কোনো kill, sudo বা সেটিং বদল নয়। ৩. কোনো process থামাতে হলে আগে SIGTERM, কতক্ষণ অপেক্ষা, তারপর কী — সেই ক্রম বলো, আর কোন process (যেমন ডেটাবেস) থামানো ঝুঁকিপূর্ণ জানাও। ৪. তোমার কোনো কমান্ড macOS বা container-এ ভিন্ন আচরণ করলে উল্লেখ করো।

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-এর ব্লকে পরীক্ষা করুন।

  1. 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 আসলে ৫ সেকেন্ড পরপর ধাপে হিসাব করে, তাই ফল এর খুব কাছাকাছি হয়।)

  2. /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 ঠিক এটাই করে।

  3. আউটপুট অনুমান করুন: একটি 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 %1 job ১-কে SIGTERM পাঠায়। দ্বিতীয় jobs: [1]- Terminated sleep 100 আর [2]+ Running sleep 200 & — bash শেষ হওয়া job-এর খবর একবার দেখিয়ে তালিকা থেকে সরিয়ে দেয়। (sleep যদি পরের prompt আসার আগেই মরে যায়, bash Terminated লাইনটি 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.

  4. কোড লিখুন — 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 যা যা করে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ⌨️ prompt দেখিয়ে একটি লাইন পড়া
  2. ✂️ লাইনটিকে শব্দে ভাঙা
  3. 🔍 কমান্ডটি builtin কি না দেখা
  4. 🍴 fork করে child process তৈরি
  5. 🚀 child-এ exec দিয়ে নতুন প্রোগ্রাম বসানো
  6. ⏳ 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 চাপা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Shell = পড়া → ভাঙা → builtin? → fork → exec → waitpid-এর লুপ; shell-এর নিজের অবস্থা বা job table ছোঁয় এমন কমান্ড (cd, export, exit, fg) builtin হতে বাধ্য।
  2. & মানে শুধু waitpid না করা; job control চালু থাকলে প্রতিটি job নিজের process group-এ, আর terminal-এর foreground group ঠিক করে কিবোর্ড ও Ctrl+C কে পাবে।
  3. Ctrl+C = SIGINT, Ctrl+Z = SIGTSTP — পাঠায় kernel-এর terminal driver, পুরো foreground group-কে; bg/fg SIGCONT আর tcsetpgrp দিয়ে কাজ করে।
  4. Session → process group → process; terminal hangup-এ session leader SIGHUP পায় আর job-দের পাঠায় — বাঁচাতে nohup, disown, tmux, systemd.
  5. ps, top পড়ে /proc থেকে; STAT-এ R/S/D/T/Z; Linux-এর load average = R + D task-এর exponentially-damped গড়, CPU-র সংখ্যার সাথে তুলনা করুন; kill আগে SIGTERM, SIGKILL শেষ উপায়।

পরের পর্বে: দুটি process-কে একসাথে কাজ করাতে হলে তারা কথা বলবে কীভাবে? Pipe, shared memory, message queue আর socket — inter-process communication.

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান