Foundationপ্রথম নীতি থেকে
LEVEL 4লেসন ৪/২৯কঠিন১ ঘণ্টা ১০ মিনিট

Process-এর শরীর — task_struct থেকে address space পর্যন্ত

Process Anatomy

গত লেসনে দেখেছি syscall কীভাবে kernel-এ ঢোকে, কিন্তু এড়িয়ে গেছি একটা প্রশ্ন — kernel 'আপনার process' বলতে ঠিক কী বোঝে? এই লেসনে সেই প্রশ্নের উত্তর দুই দিক থেকে দেব। প্রথমে kernel-এর দৃষ্টিতে — `task_struct` নামের ~৭ KB-র একটা struct, যেখানে PID থেকে page table-এর ঠিকানা পর্যন্ত সবকিছু থাকে। তারপর process-এর নিজের দৃষ্টিতে — তার virtual address space, যেখানে code, data, heap আর stack নিজেদের জায়গা করে নেয়। শেষে দেখব process কীভাবে অবস্থা বদলায়, আর কেন কিছু অবস্থা থেকে ফেরা অসম্ভব।

এই লেসন শেষে আপনি পারবেন

  • একটা process-এর সংজ্ঞা নির্ভুলভাবে দিতে পারবেন — একটা প্রোগ্রাম নয়, একটা execution instance যার নিজস্ব virtual address space আছে
  • Address space-এর প্রতিটা অংশ (text, rodata, data, bss, heap, mmap region, stack) চিনতে পারবেন এবং সেগুলোকে ELF segment-এর সাথে মিলাতে পারবেন
  • `task_struct`-এ kernel একটা process সম্পর্কে ঠিক কী কী তথ্য রাখে তার একটা বাস্তব তালিকা দিতে পারবেন
  • Process state machine-এর প্রতিটা state (running, interruptible sleep, uninterruptible sleep, stopped, zombie) আলাদা করতে পারবেন, আর D state কেন `kill -9`-এও মরে না তা ব্যাখ্যা করতে পারবেন
  • PID, PPID, PGID, SID-এর মধ্যে সম্পর্ক বলতে পারবেন এবং `/proc/<pid>/status` থেকে সেগুলো পড়তে পারবেন
  • নিজের প্রোগ্রামের memory layout প্রিন্ট করে `/proc/self/maps`-এর সাথে হাতে মিলিয়ে যাচাই করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

আপনি ./a.out চালালেন। প্রশ্ন: এখন ঠিক কী “চলছে”?

সহজ উত্তর মনে হয় — “প্রোগ্রামটা চলছে”। কিন্তু একই প্রোগ্রাম তিনবার চালালে তিনটা আলাদা জিনিস চলে। তাদের PID আলাদা, তাদের memory আলাদা, একটা crash করলে বাকিরা অক্ষত থাকে। তাহলে “প্রোগ্রাম” শব্দটা যথেষ্ট নয়।

সঠিক শব্দ process — একটা running program-এর execution instance। Program হলো disk-এ পড়ে থাকা একটা নিষ্ক্রিয় ফাইল (ELF binary)। Process হলো সেই ফাইল থেকে তৈরি একটা জীবন্ত জিনিস — নিজস্ব memory, নিজস্ব state, নিজস্ব পরিচয়।

গত লেসনের প্রশ্নে ফিরি: getpid() ডাকলে kernel PID-টা কোথা থেকে পড়ে? উত্তর একটামাত্র struct-এ — kernel-এর কাছে আপনার process মানেই এই struct। আর process নিজে যেটা “নিজের memory” বলে বিশ্বাস করে, সেটা একটা সাজানো illusion — virtual address space।

এই লেসনে আমরা দুইটাই খুলে দেখব।

মূল ধারণা

Process কী — আর প্রোগ্রাম কী নয়

তিনটা জিনিস আলাদা রাখুন:

কোথায় থাকেউদাহরণ
ProgramDisk-এ, নিষ্ক্রিয়/usr/bin/python3
ProcessMemory-তে, জীবন্তPID 4821, python3 চালাচ্ছে
Process imageএকটা মুহূর্তের snapshotcore dump

একই program একাধিকবার চালালে একাধিক process — প্রতিটার আলাদা virtual address space, আলাদা PID, একে অপরের memory পড়তে পারে না (গত দুই লেসনের isolation)।

python3 &
python3 &
ps aux | grep python3

দুইটা আলাদা PID, একই binary।

Address space — process যা “নিজের memory” মনে করে

প্রতিটা process বিশ্বাস করে তার একটা flat, ব্যক্তিগত address space আছে, শূন্য থেকে শুরু। এটা একটা illusion — নিচে আসল physical memory-র সাথে virtual address-এর সম্পর্ক page table দিয়ে তৈরি (লেসন ১০-১১-এ বিস্তারিত)। আপাতত আমরা শুধু এই illusion-টার আকৃতি দেখব।

উঁচু address
┌─────────────────────────┐
│   kernel space           │  ← গত লেসনে দেখেছি, user মোডে অদৃশ্য
├─────────────────────────┤  0x00007fffffffffff
│   [stack]                │  ← নিচের দিকে বাড়ে, প্রতি function call-এ
│         ↓                │
│                          │
│   (ফাঁকা জায়গা, ASLR gap) │
│                          │
│         ↑                │
│   mmap region             │  ← shared library, mmap() করা ফাইল
├─────────────────────────┤
│         ↑ heap            │  ← উপরের দিকে বাড়ে, malloc/brk
├─────────────────────────┤
│   .bss                   │  ← শূন্যে initialised global (ফাইলে জায়গা নেয় না)
├─────────────────────────┤
│   .data                  │  ← initialised global
├─────────────────────────┤
│   .rodata                │  ← string literal, const
├─────────────────────────┤
│   .text                  │  ← machine code
└─────────────────────────┘  0x0000000000400000 (সাধারণত)
নিচু address
x86-64 Linux process-এর সাধারণ address space — নিচ থেকে উপরে address বাড়ছে।

Level 3-এর ELF লেসনের সাথে সরাসরি মিল — কারণ এটা সেই একই সংগঠন, শুধু এখন disk-এর বদলে memory-তে। .text, .rodata, .data, .bss — ELF section গুলো loader দিয়ে যখন memory-তে আসে, তখন সেগুলোকে বলে segment

task_struct — kernel-এর দৃষ্টিতে একটা process

আগের প্রশ্নের উত্তর: kernel-এর কাছে একটা process মানে একটা struct task_struct। Linux 6.x-এ এটা প্রায় ৭ কিলোবাইট

গুরুত্বপূর্ণ কিছু field, সরলীকৃত:

struct task_struct {
    pid_t pid;                    /* এই task-এর id */
    pid_t tgid;                   /* thread group id — গত লেসনের ধারণা */
    struct task_struct *parent;   /* PPID এখান থেকে আসে */
    struct list_head children;

    long state;                   /* running/sleeping/zombie/... */

    struct mm_struct *mm;         /* address space — page table-এর মূল */
    struct files_struct *files;   /* file descriptor table */
    struct signal_struct *signal; /* pending signal, handler */

    struct sched_entity se;       /* scheduler-এর জন্য — vruntime ইত্যাদি */
    int prio;

    struct cred *cred;            /* uid, gid, capability */
    struct fs_struct *fs;         /* cwd, root */

    u64 utime, stime;             /* user/kernel CPU time */
    struct thread_struct thread;  /* সংরক্ষিত register, architecture-নির্ভর */
};

এটা include/linux/sched.h-এ — আসল struct আরো অনেক বড় (২০০+ field), কিন্তু এই কয়েকটাই আমাদের প্রশ্নের উত্তর দেয়:

  • getpid()current->tgid পড়ে (current মানে বর্তমানে চলা task-এর pointer, প্রতিটা CPU-তে আলাদাভাবে রাখা)
  • write(1, ...)current->files-এ fd 1 খোঁজে (পরের লেসনগুলোয় বিস্তারিত)
  • Context switch-এ (লেসন ৭) → thread field-এ register সংরক্ষিত হয়

PID, PPID, PGID, session — চার স্তরের পরিচয়

Session (SID)
  └── Process Group (PGID)
        └── Process (PID)
              └── Parent (PPID)
কীব্যবহারিক প্রভাব
PIDনিজের পরিচয়kill <pid>
PPIDকে তৈরি করেছেzombie reaping-এর দায়িত্ব
PGIDএকই pipeline-এর অংশkill -<pgid> পুরো group-কে
SIDterminal-এর সাথে সম্পর্কlogout-এ কারা বন্ধ হবে

ls | grep foo চালালে ls আর grep আলাদা process, একই process group। তাই Ctrl-C চাপলে terminal driver পুরো group-কে SIGINT পাঠায় — দুটোই থামে, একসাথে।

ls | grep foo &
ps -o pid,ppid,pgid,sid,comm -j

ভেতরে কী ঘটছে

Process state — এবং একটা যা থেকে ফেরা যায় না

একটা process-এর জীবনচক্র
  1. R — Running/RunnableCPU-তে চলছে, বা চলার জন্য queue-তে
  2. S — Interruptible sleepকিছুর জন্য অপেক্ষা করছে, signal এলে জাগবে
  3. D — Uninterruptible sleepkernel operation-এর মাঝে, signal-ও উপেক্ষা করে
  4. T — StoppedSIGSTOP পেয়েছে, বা debugger থামিয়েছে
  5. Z — Zombieশেষ হয়েছে, কিন্তু exit status এখনো তোলা হয়নি

S আর D-র পার্থক্যটাই সবচেয়ে গুরুত্বপূর্ণ, আর সবচেয়ে কম বোঝা।

S state-এ একটা process ঘুমাচ্ছে কিন্তু signal দিয়ে জাগানো যায় — Ctrl-C কাজ করবে। D state-এ process একটা kernel operation-এর মাঝপথে — সাধারণত disk I/O — আর সেই মুহূর্তে kernel-এর ভেতরের data structure-গুলো একটা অসম্পূর্ণ অবস্থায় আছে। যদি এখন signal দিয়ে process থামিয়ে দেওয়া হয়, সেই data structure অসামঞ্জস্যপূর্ণ থেকে যাবে — filesystem corruption হতে পারে।

তাই kernel ইচ্ছাকৃতভাবে D state-এ সব signal উপেক্ষা করে, এমনকি SIGKILL-ও। এটা bug নয় — এটা correctness-এর জন্য প্রয়োজনীয় একটা সিদ্ধান্ত।

Zombie — মৃত কিন্তু কবর দেওয়া হয়নি

Process শেষ হলে সাথে সাথে অদৃশ্য হয় না। তার exit status কেউ পড়া পর্যন্ত সে থেকে যায় Z state-এ — একটা ছোট entry, প্রায় শুধু PID আর exit code।

pid_t pid = fork();
if (pid == 0) exit(42);
sleep(5);                 /* এই ৫ সেকেন্ড child একটা zombie */
wait(NULL);                /* এখন সে সম্পূর্ণ চলে যায় */

Parent যদি কখনো wait() না ডাকে, zombie চিরকাল থাকে — kill -9-ও কাজ করে না, কারণ সে তো ইতিমধ্যে মৃত। এভাবে অনেক zombie জমলে PID table ভরে যেতে পারে (গুরুতর কিন্তু বিরল সমস্যা)।

Parent নিজে মারা গেলে সন্তানরা orphan, আর তাদের PID 1 (init/systemd) reparent করে নেয় — যার একটা কাজই হলো zombie reap করা। Level 4-এর পরের একটা লেসনে (fork-exec-wait) আমরা এটা হাতে-কলমে দেখব।

ASLR — প্রতিবার আলাদা মানচিত্র

Address space-এর আকৃতি (উপরের figure) সবসময় একই, কিন্তু ঠিক কোথায় শুরু হয় তা প্রতিবার বদলায়:

for i in 1 2 3; do
  cat /proc/self/maps | grep '\[stack\]'
done

তিনবারই আলাদা address। এটাই ASLR (Address Space Layout Randomization) — heap, stack, mmap region, এমনকি executable-এর base address (PIE হলে) প্রতিটা exec-এ randomise হয়।

কেন: exploit লিখতে হলে attacker-কে জানতে হয় কোথায় কী আছে — একটা নির্দিষ্ট function-এর address, বা stack-এর অবস্থান। ASLR সেই অনুমানকে অবিশ্বস্ত করে দেয়। এটা memory safety bug ঠেকায় না, কিন্তু সেগুলোকে কাজে লাগানো কঠিন করে।

Level 10-এ আমরা দেখব কেন ASLR একা যথেষ্ট নয় (info leak দিয়ে বাইপাস করা যায়), আর কেন সেই সাথে stack canary আর NX bit লাগে।

উদাহরণ

/proc/self/maps-কে ELF-এর সাথে মেলানো

একটা সত্যিকারের প্রোগ্রাম চালিয়ে দেখি:

cat > /tmp/probe.c <<'EOF'
#include <stdio.h>
int global_init = 42;
int global_bss;
const char *msg = "hello";
int main(void) {
    int local = 1;
    static int stat_var = 7;
    void *heap = malloc(64);
    printf("code    : %p\n", (void *)main);
    printf("rodata  : %p\n", (void *)msg);
    printf("data    : %p\n", (void *)&global_init);
    printf("bss     : %p\n", (void *)&global_bss);
    printf("heap    : %p\n", heap);
    printf("stack   : %p\n", (void *)&local);
    while(1) pause();
}
EOF
gcc -o /tmp/probe /tmp/probe.c
/tmp/probe &
sleep 1
grep -E "r-xp|rw-p|r--p" /proc/$!/maps | grep probe
kill $!

Output-এর address গুলো /proc/<pid>/maps-এর নির্দিষ্ট range-এ পড়বে — r-xp permission-এর region-এ code, r--p-এ rodata, rw-p-এ data/bss/heap/stack

এইভাবে যাচাই করলাম যে theory আর বাস্তবতা মেলে — যা এই লেসনের সবচেয়ে গুরুত্বপূর্ণ শিক্ষা। .text, .data — এগুলো বিমূর্ত ধারণা নয়, প্রতিটার একটা প্রকৃত virtual address range আছে, আর সেটা দেখা যায়।

নিজে চালিয়ে দেখুন

EXPERIMENT

/proc/self/maps পড়ে ELF section-এর সাথে মেলান

Linux, gcc, readelf· ১৫ মিনিট
readelf -S /tmp/probe | grep -E "\.text|\.rodata|\.data|\.bss"
  [13] .text             PROGBITS         0000000000001060  00001060
  [15] .rodata           PROGBITS         0000000000002000  00002000
  [22] .data             PROGBITS         0000000000004000  00003000
  [23] .bss              NOBITS           0000000000004010  00004010

এই address গুলো file-এর ভেতরের offset, actual runtime address নয় (PIE binary-তে সবকিছু একটা base address যোগ করে সরে যায়)। তবু আপেক্ষিক ক্রম আর দূরত্ব ঠিক থাকে।

/tmp/probe &
PID=$!
sleep 1
cat /proc/$PID/maps | grep probe
kill $PID
55a1e2400000-55a1e2401000 r--p 00000000 ... /tmp/probe
55a1e2401000-55a1e2402000 r-xp 00001000 ... /tmp/probe
55a1e2402000-55a1e2403000 r--p 00002000 ... /tmp/probe
55a1e2403000-55a1e2404000 r--p 00003000 ... /tmp/probe
55a1e2404000-55a1e2405000 rw-p 00003000 ... /tmp/probe

লক্ষ্য করুন offset column (তৃতীয়টা) readelf-এর file offset-এর সাথে মেলে। r-xp region-টা .text-এর offset (00001000) দিয়ে শুরু হচ্ছে — এটাই প্রমাণ যে এই memory region সত্যিই .text section।

নিজে যাচাই করুন: .rodata-র offset (00002000) কোন r--p region-এ পড়ে? .data-র (00003000)?

এটা কী প্রমাণ করে

Process-এর memory layout কোনো তাত্ত্বিক diagram নয় — ELF file-এর section header-এ যা লেখা, memory-তে ঠিক তাই ঘটে, শুধু address ভিন্ন।

EXPERIMENT

ASLR বন্ধ করে নিশ্চিত হওয়া

Linux· ১০ মিনিট
for i in 1 2 3; do /tmp/probe & sleep 0.2; \
  grep -m1 '\[stack\]' /proc/$!/maps; kill $! 2>/dev/null; done

তিনটা ভিন্ন address।

cat /proc/sys/kernel/randomize_va_space

2 মানে সম্পূর্ণ ASLR (stack, heap, mmap, PIE base — সব randomised)। 1 মানে আংশিক। 0 মানে বন্ধ।

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
for i in 1 2 3; do /tmp/probe & sleep 0.2; \
  grep -m1 '\[stack\]' /proc/$!/maps; kill $! 2>/dev/null; done
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space   # ফিরিয়ে দিন

এবার তিনবারই একই address — প্রমাণ যে randomisation একটা সচেতন, বন্ধ-করা-যায় এমন feature, ভাগ্য নয়।

এটা কী প্রমাণ করে

Address randomisation সত্যিই ঘটছে, ঐচ্ছিক নয় — আর এটা একটা kernel-স্তরের সেটিং, প্রোগ্রামের নয়।

নিজে বানান

BUILD IT

নিজের address space-এর মানচিত্র

C · ●●●○○
  1. বিভিন্ন ধরনের variable-এর address নিন — global, static, heap, stack
  2. প্রোগ্রাম চলা অবস্থাতেই /proc/self/maps পড়ুন
  3. প্রতিটা address কোন region-এ পড়ে তা মিলিয়ে দেখান
  4. একটা recursive function দিয়ে stack বৃদ্ধি পাওয়া পর্যবেক্ষণ করুন
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int global_init = 42;
int global_bss;
const char rodata_str[] = "read-only string";

typedef struct { unsigned long lo, hi; char perm[8]; char path[256]; } region_t;

static region_t regions[256];
static int nregions;

static void load_maps(void) {
    FILE *f = fopen("/proc/self/maps", "r");
    char line[512];
    while (nregions < 256 && fgets(line, sizeof line, f)) {
        region_t *r = &regions[nregions];
        r->path[0] = 0;
        if (sscanf(line, "%lx-%lx %7s %*s %*s %*s %255[^\n]",
                   &r->lo, &r->hi, r->perm, r->path) >= 3)
            nregions++;
    }
    fclose(f);
}

static void identify(const char *label, void *addr) {
    unsigned long a = (unsigned long)addr;
    for (int i = 0; i < nregions; i++)
        if (a >= regions[i].lo && a < regions[i].hi) {
            printf("  %-14s %014lx  [%s]  %s\n", label, a,
                   regions[i].perm,
                   regions[i].path[0] ? regions[i].path : "(anonymous)");
            return;
        }
    printf("  %-14s %014lx  কোনো region-এ নেই!\n", label, a);
}

static int depth_probe(int depth, unsigned long *first_frame) {
    int local;
    if (!*first_frame) *first_frame = (unsigned long)&local;
    if (depth == 0) return (int)((*first_frame) - (unsigned long)&local);
    return depth_probe(depth - 1, first_frame);
}

int main(void) {
    load_maps();
    void *heap = malloc(128);
    static int stat_var = 7;
    int local = 1;

    printf("═══ address space পরিচিতি ═══\n\n");
    identify("code (main)",  (void *)main);
    identify("rodata",       (void *)rodata_str);
    identify("data",         &global_init);
    identify("bss",          &global_bss);
    identify("static local", &stat_var);
    identify("heap",         heap);
    identify("stack",        &local);

    unsigned long first = 0;
    int consumed = depth_probe(1000, &first);
    printf("\n═══ stack বৃদ্ধি ═══\n\n");
    printf("  ১০০০ গভীরতার recursion-এ stack ব্যবহার: ~%d byte\n", consumed);
    printf("  (প্রতি frame আনুমানিক %d byte)\n", consumed / 1000);

    free(heap);
    return 0;
}
gcc -O0 -o addrmap addrmap.c && ./addrmap

-O0 জরুরি — optimization-এ compiler local variable-কে register-এ রাখতে পারে, stack-এ নয়, আর তখন address মেলানো কঠিন হয়ে যায়।

নিজে বাড়ান:

  1. mmap() দিয়ে নিজে একটা region বানান, সেটাও identify করুন
  2. getrlimit(RLIMIT_STACK, ...) দিয়ে সর্বোচ্চ stack size বের করুন, আর সেই limit ছাড়িয়ে যাওয়ার চেষ্টা করে দেখুন কী ঘটে (লেসন ১৫-এ পুরোটা)
  3. একটা shared library-র (libc.so) কোনো function-এর address identify করুন — সেটা .text-এর বাইরে, নিজের আলাদা mapped region-এ থাকবে
  4. PIE binary বনাম non-PIE (gcc -no-pie) তুলনা করুন — কোনটায় .text-এর address স্থির থাকে?

বাস্তব সিস্টেমে

Process anatomy যেখানে সরাসরি কাজে লাগে

Container memory limit। docker run -m 512m আসলে cgroup-এ (লেসন ২৯) একটা সীমা বসায় — কিন্তু সেই সীমা মাপা হয় process-এর RSS-এর (আসলে ব্যবহৃত physical page) উপর, VSZ (virtual size)-এর উপর নয়। একটা process 100 GB virtual address space দাবি করতে পারে অথচ RSS মাত্র কয়েক MB — এই পার্থক্যটাই লেসন ১০-এর বিষয়।

Core dump analysis। একটা crash হওয়া process-এর core file আক্ষরিকভাবে তার address space-এর একটা snapshot — .data, heap, stack সব। gdb core ./program দিয়ে সেই মৃত মুহূর্তের প্রতিটা variable দেখা যায়, কারণ layout ঠিক এই লেসনে বর্ণিত নিয়মেই সাজানো।

ps aux-এর STAT column। R, S, D, Z, T — এই লেসনের state machine সরাসরি সেখানে দেখা যায়। একটা database server যদি D state-এ আটকে থাকে, সেটা প্রায় নিশ্চিতভাবে disk I/O-তে সমস্যা — CPU-র সমস্যা নয়।

Fork bomb defence। ulimit -u (max user processes) kernel-কে বলে একজন ব্যবহারকারী সর্বোচ্চ কতগুলো task_struct বানাতে পারবে — প্রতিটা struct ৭ KB হলেও, লক্ষ লক্ষ zombie বা fork bomb memory শেষ করে দিতে পারে।

Debugger breakpoint। gdb-তে break main কাজ করে কারণ compiler symbol table-এ main-এর address জানে, আর সেই address process-এর .text region-এই থাকে — ঠিক আজকের experiment-এ যা দেখলাম।

Chrome-এর one-process-per-tab। প্রতিটা tab আলাদা process কারণ তাহলে প্রতিটার আলাদা address space, আলাদা task_struct — একটা tab crash করলে বাকিরা অক্ষত। এটা process isolation-এর সবচেয়ে দৃশ্যমান দৈনন্দিন উদাহরণ।

যে ভুলগুলো সবাই করে

“প্রোগ্রাম আর process একই জিনিস, শব্দের পার্থক্য মাত্র।”

প্রোগ্রাম হলো disk-এ একটা নিষ্ক্রিয় ফাইল — কোনো memory নেই, কোনো PID নেই, কোনো state নেই। Process হলো সেই ফাইল থেকে তৈরি একটা জীবন্ত সত্তা।

একই প্রোগ্রাম একাধিকবার চালালে একাধিক process — প্রতিটার নিজস্ব task_struct, নিজস্ব address space। ps aux -এ firefox তিনবার দেখা যাওয়া মানে একটা প্রোগ্রাম, তিনটা process।

উল্টোটাও সত্য — একটামাত্র process একাধিক প্রোগ্রামের কোড চালাতে পারে (exec দিয়ে বদলে যায়, লেসন ৫-এ), যদিও PID একই থেকে যায়।

এই পার্থক্যটা না বুঝলে “মেমরি leak” জাতীয় আলোচনায় বিভ্রান্তি হয় — leak হয় process-এর memory-তে, প্রোগ্রামের ফাইলে নয়।

“`kill -9` সবসময় একটা process মেরে ফেলে।”

সাধারণত হ্যাঁ, কিন্তু D state-এ থাকা process-এ SIGKILLও উপেক্ষিত হয়

SIGKILL (-9) সাধারণ signal-এর মতো handler দিয়ে ধরা যায় না — এই অর্থে এটা “অপ্রতিরোধ্য”। কিন্তু kernel নিজেই এটা uninterruptible sleep-এ পৌঁছাতে দেয় না, কারণ সেই মুহূর্তে kernel-এর নিজের data structure একটা মধ্যবর্তী অবস্থায় আছে।

ps -eo pid,stat,comm | awk '$2 ~ /D/'

যদি কোনো process এখানে দেখা যায় আর kill -9-এ সাড়া না দেয়, সেটা bug নয় — সেটা সিস্টেম বলছে “আমি এখন একটা atomic operation মাঝপথে, নিরাপদে থামতে পারছি না”।

সমাধান সাধারণত প্রোগ্রাম-স্তরে নয়, hardware/driver-স্তরে — disk বা network filesystem-এর সমস্যা ঠিক করা।

“Zombie process CPU বা memory খরচ করে, তাই বিপজ্জনক।”

একটা zombie প্রায় কিছুই খরচ করে না — তার code, data, heap, stack সব মুক্ত হয়ে গেছে। যা বাকি থাকে তা শুধু task_struct-এর একটা কঙ্কাল সংস্করণ (PID, exit status, resource usage-এর হিসাব) — parent wait() ডাকা পর্যন্ত।

তাই একটা-দুইটা zombie সমস্যা নয়। সমস্যা হয় যখন parent কখনো wait() ডাকে না আর হাজার হাজার child তৈরি করতে থাকে — তখন প্রতিটা zombie সামান্য kernel memory ধরে রাখে, আর সবচেয়ে গুরুত্বপূর্ণ, PID namespace-এ সীমিত PID সংখ্যা ভরে যায়। একটা নতুন সংখ্যা /proc/sys/kernel/pid_max-এর বেশি process তৈরি করা যায় না।

cat /proc/sys/kernel/pid_max

সাধারণত ৪,১৯৪,৩০৩ — বিশাল, কিন্তু একটা buggy server যদি প্রতি সেকেন্ডে হাজারো zombie তৈরি করে আর কখনো reap না করে, কয়েক ঘণ্টায় সেটাও ফুরাতে পারে।

“ASLR চালু থাকলে buffer overflow আর কোনো সমস্যা নয়।”

ASLR কঠিন করে, অসম্ভব করে না।

Info leak দিয়ে বাইপাস: যদি প্রোগ্রামে এমন কোনো bug থাকে যা একটা pointer মান কোনোভাবে প্রকাশ করে দেয় (একটা error message-এ, একটা log-এ), attacker সেটা থেকে base address হিসাব করে ফেলতে পারে — তারপর ASLR অকার্যকর।

Brute force: ৩২-বিট system-এ randomisation-এর জন্য মাত্র কয়েক bit থাকে, তাই বারবার চেষ্টা করে অনুমান করা সম্ভব (বিশেষত এমন সার্ভিসে যা crash-এর পর restart করে একই randomisation state ফিরিয়ে আনে না, বা যেখানে fork() ASLR পুনরায় করে না)।

64-bit-এও একটা সূক্ষ্মতা: fork()-এর পর child একই address space layout পায় parent-এর — নতুন randomisation হয় না। শুধু exec()-এ নতুন randomisation ঘটে।

তাই ASLR একটা স্তর, একমাত্র প্রতিরক্ষা নয়। সাথে লাগে stack canary, NX bit, আর সবচেয়ে ভালো — memory-safe ভাষা বা bounds checking। Level 10-এ পুরো ছবিটা দেখব।

বুঝেছেন কি না দেখুন

1

.bss section-এর জন্য ELF ফাইলে কোনো byte সংরক্ষিত থাকে না, শুধু একটা size। কিন্তু .data-র জন্য প্রকৃত byte থাকে। এই পার্থক্যের ফলে কী সুবিধা হয়, আর কোন ধরনের variable কোথায় যায় তা কীভাবে নির্ধারিত হয়?

যুক্তি

সুবিধা — disk আর load time দুটোতেই।

একটা প্রোগ্রামে যদি ১০০ MB-র একটা বড় uninitialised array থাকে (static char buffer[100*1024*1024];), সেটা .data-তে গেলে ELF ফাইল নিজেই ১০০ MB বড় হতো, আর প্রতিবার load করার সময় সেই ১০০ MB disk থেকে পড়তে হতো।

.bss-তে গেলে ELF ফাইলে শুধু “এখানে ১০০ MB শূন্য রাখো” — কয়েক byte-এর একটা metadata। Loader শুধু একটা zero-filled anonymous mapping বানায় (mmap(MAP_ANONYMOUS) ঘরানার), যা kernel demand-paging দিয়ে lazily allocate করে (লেসন ১০-১৩)।

নির্ধারণের নিয়ম — compiler/linker করে, স্বয়ংক্রিয়ভাবে:

int a;              /* .bss — initial value নেই */
int b = 0;           /* .bss — শূন্যেই initialised, তাই একই কথা */
int c = 5;            /* .data — non-zero initial value */
const char *d = "x";   /* pointer .data-তে, string literal .rodata-তে */
static int e;          /* .bss */

Compiler প্রতিটা global/static variable-এর initial value দেখে সিদ্ধান্ত নেয়। = 0 লেখা আর কিছু না লেখা — দুটোই .bss-এ যায়, কারণ ফলাফল একই (linker/loader optimization)।

gcc -c test.c -o test.o
size test.o

size কমান্ড প্রতিটা section-এর আকার আলাদা করে দেখায় — নিজে যাচাই করুন।

ব্যবহারিক পরিণতি: এই কারণেই একটা বিশাল static array থাকা প্রোগ্রামের binary ফাইল ছোট থাকে, কিন্তু চালানোর সময় memory-তে বড় জায়গা নেয় — ls -l আর ps-এর RSS সংখ্যা মিলবে না, আর এটা bug নয়।

2

ps -eo pid,stat,comm চালিয়ে একটা process Ss state দেখাচ্ছে (কেবল S নয়)। আরেকটা R+। এই বাড়তি অক্ষরগুলোর অর্থ কী?

প্রয়োগ

ps-এর STAT column-এ প্রথম অক্ষরটাই মূল state (R/S/D/T/Z), তারপরের অক্ষরগুলো modifier:

Modifierঅর্থ
<উচ্চ priority (high-priority, “nice”-এর ঋণাত্মক)
Nনিম্ন priority
Lকিছু page memory-তে লক করা আছে (real-time-এর জন্য)
ssession leader — SID == PID
lmulti-threaded
+foreground process group-এ আছে (আপনার terminal-এ)

Ss — sleeping (S) এবং session leader (s)। এটা প্রায় নিশ্চিতভাবে একটা shell বা daemon যেটা নিজে একটা নতুন session শুরু করেছে (setsid() ডেকে) — যেমন আপনার login shell, বা sshd-এর একটা child।

R+ — running/runnable (R) এবং foreground group-এ (+)। এটা এমন একটা প্রোগ্রাম যা এই মুহূর্তে আপনার terminal-এ চলছে আর active — যেমন আপনি যদি এই মুহূর্তে ps কমান্ডটাই দেখতে পারতেন, সেটা নিজেই R+ হতো।

যাচাই করুন:

sleep 100 &
ps -eo pid,ppid,stat,comm | grep sleep

sleep 100 & ব্যাকগ্রাউন্ডে গেছে, তাই foreground নয় — STAT-এ + থাকবে না, শুধু S

sleep 100

(এবার & ছাড়া, অন্য terminal থেকে দেখুন) — এবার S+, কারণ এটা foreground-এ ঘুমাচ্ছে।

গুরুত্ব: + না থাকা মানে সেই process Ctrl-C দিয়ে থামানো যাবে না (কারণ terminal driver SIGINT শুধু foreground group-কে পাঠায়) — এটাই লেসন ২৩ ও ২৫-এর signal আলোচনায় গুরুত্বপূর্ণ হয়ে ফিরবে।

3

আপনার একটা production সার্ভিস মাঝেমধ্যে হ্যাং হয়ে যায়, আর তখন ps-এ STAT column-এ D দেখা যায়। কীভাবে root cause খুঁজে বের করবেন — কোন tool, কোন ক্রমে?

ডিজাইন

ধাপ ১ — নিশ্চিত করুন এটা সত্যিই D state, আর কতক্ষণ ধরে।

ps -eo pid,stat,etime,comm | grep " D"

etime দেখায় process কতক্ষণ চলছে — কিন্তু D state-এ কতক্ষণ আটকে সেটা আলাদা করে জানতে:

watch -n1 'ps -eo pid,stat,comm | grep " D"'

কয়েক সেকেন্ডের বেশি স্থায়ী D state সন্দেহজনক — সাধারণ disk read কয়েক মিলিসেকেন্ডে শেষ হয়।

ধাপ ২ — কোন kernel function-এ আটকে তা দেখুন।

cat /proc/<pid>/wchan; echo
cat /proc/<pid>/stack     # root লাগবে, kernel debug symbol দরকার

wchan একটা function নাম দেবে — যেমন wait_on_page_bit (disk page-এর জন্য অপেক্ষা), nfs_wait_bit_killable (NFS), __wait_rcu_gp। নামটাই root cause-এর দিকনির্দেশ করে।

ধাপ ৩ — কী resource-এ আটকে তা দেখুন।

sudo cat /proc/<pid>/syscall     # কোন syscall-এ আটকে
sudo ls -la /proc/<pid>/fd/      # কোন file/socket খোলা
iostat -x 1                       # disk latency অস্বাভাবিক কি না

wchan-এ NFS-সংক্রান্ত নাম থাকলে সেই NFS server-এর অবস্থা দেখুন। Local disk হলে iostat-এ %util আর await দেখুন — disk নিজে ধীর বা মৃত কি না।

ধাপ ৪ — dmesg-এ hardware error খুঁজুন।

dmesg -T | grep -iE "error|timeout|reset" | tail -50

Disk controller error, SCSI timeout, বা storage array-এর পাশ থেকে network সমস্যা — এগুলো প্রায়ই D state-এর প্রকৃত কারণ, প্রোগ্রামের কোড নয়।

যা করবেন না: এই অবস্থায় kill -9 বারবার চেষ্টা করা — এটা কাজ করবে না (D state signal উপেক্ষা করে), আর সময় নষ্ট করবে। D state-এ আটকে থাকা process-এর জন্য একমাত্র বাস্তব সমাধান হয় underlying I/O সমস্যা সমাধান, নয়তো পুরো মেশিন reboot।

দীর্ঘমেয়াদী প্রতিরোধ: NFS mount-এ soft,intr,timeo= option ব্যবহার করুন (hard mount-এর বদলে) যাতে timeout-এ অন্তত signal-এ সাড়া দেয়। Application-স্তরে I/O timeout সেট করুন যাতে একটা ধীর disk পুরো worker pool আটকে না দেয়।

এই diagnostic flow-টাই লেসন ৩০-এ (/proc ও observability) আমরা আরো সাধারণ toolkit হিসেবে ফিরে দেখব।

4

একটা প্রোগ্রামে int arr[1000000] = {0}; (global, সব শূন্যে initialised) লেখা আছে। এটা .data-তে যাবে না .bss-তে? আর int arr[1000000] = {1}; (শুধু প্রথম element non-zero, বাকিরা automatically শূন্য) কোথায় যাবে?

প্রয়োগ

প্রথমটা — {0} — যাবে .bss-তে।

C-এর নিয়মে {0}-দিয়ে initialise করা array-র সব element শূন্য হয় — আর সব-শূন্য মানে .bss-এর সংজ্ঞার সাথে হুবহু মিলে যায়। Compiler এটা চেনে আর optimize করে .bss-এ পাঠায়, .data-তে ৪ MB (১০ লক্ষ × ৪ byte) real zero byte সংরক্ষণ করে না।

cat > t1.c <<'EOF'
int arr[1000000] = {0};
int main(void) { return arr[0]; }
EOF
gcc -c t1.c -o t1.o && size t1.o
   text    data     bss     dec     hex filename
    ...       ...  4000000   ...     ... t1.o

data column প্রায় শূন্য, bss column ৪,০০০,০০০ — নিশ্চিত।

দ্বিতীয়টা — {1} — যাবে .data-তে, পুরোটাই।

C-তে {1} মানে প্রথম element 1, বাকি সব automatically শূন্যে initialised (এটা C standard-এর নিয়ম — partial initializer list-এর বাকিটা শূন্য হয়)। কিন্তু array-টা এখন আর “সব শূন্য” নয় — একটা non-zero byte আছে।

এখানে compiler-এর দুইটা বৈধ পথ আছে:

  1. সরল পথ: পুরো ৪ MB .data-তে রাখা (একটা non-zero element-এর জন্য পুরোটাই real byte সংরক্ষণ)
  2. চতুর পথ: সবচেয়ে ছোট continuous non-zero অংশটুকু .data-তে, বাকি .bss-তে (কিছু compiler/linker এটা করে, .data আর .bss-এর মাঝে বিভাজন করে)
cat > t2.c <<'EOF'
int arr[1000000] = {1};
int main(void) { return arr[0]; }
EOF
gcc -c t2.c -o t2.o && size t2.o

আপনার compiler কোন পথ নিয়েছে সেটা size-এর আউটপুট দেখেই বোঝা যাবে — data column বড় হলে প্রথম পথ, ছোট হয়ে bss বড় হলে দ্বিতীয় পথ।

সাধারণ শিক্ষা: compiler-এর decision কোনো রহস্য নয় — size, readelf -S, আর objdump -h দিয়ে সরাসরি যাচাই করা যায়। যখনই “compiler নিশ্চয়ই এটা করে” ভাবছেন, চালিয়ে দেখাই সবচেয়ে নির্ভরযোগ্য উত্তর।

5

Docker container-এর docker stats-এ একটা container-এর memory usage দেখাচ্ছে “50 MiB / 512 MiB”। কিন্তু সেই container-এর ভেতরে চলা প্রোগ্রাম যদি mmap() দিয়ে ১ GB virtual memory reserve করে রাখে (কিন্তু touch না করে), সেই সংখ্যাটা কি বদলাবে?

যুক্তি

না, প্রায় বদলাবে না।

docker stats cgroup-এর memory accounting থেকে সংখ্যা নেয়, আর cgroup memory controller physical page usage (RSS-এর কাছাকাছি একটা মেট্রিক) মাপে, virtual address space নয়।

mmap() দিয়ে address space reserve করা মানে শুধু page table-এ একটা entry আর task_struct-এর mm-এ একটা VMA (Virtual Memory Area) যোগ হওয়া — কোনো physical page এখনো বরাদ্দ হয়নি। এই লেসনে দেখেছি address space একটা illusion; সেই illusion-এর একটা অংশ দাবি করা মানে বাস্তব সম্পদ খরচ হওয়া নয়, যতক্ষণ না touch করা হয় (লেসন ১০-১৩-এ demand paging বিস্তারিত)।

পার্থক্যটা যাচাই করা যায়:

void *p = mmap(NULL, 1UL << 30, PROT_READ | PROT_WRITE,
              MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
/* এখানে docker stats: প্রায় অপরিবর্তিত */

memset(p, 1, 1UL << 30);   /* প্রতিটা byte touch */
/* এখানে docker stats: ~1 GB বেড়ে যাবে — যদি limit-এ না ঠেকে */

memset চালালে প্রতিটা page-এ page fault হবে, kernel সেগুলো physical frame দিয়ে ভরবে (লেসন ১৩), আর তখনই RSS বাড়বে — আর তখনই cgroup limit-এ ঠেকলে OOM killer container-টাকে মেরে ফেলবে।

ব্যবহারিক পরিণতি: এই কারণেই “আমার প্রোগ্রাম শুধু malloc করেছে, এখনো লেখেনি, তবু OOM হলো” — এটা স্ববিরোধী শোনায় কিন্তু আসলে ঠিক এই আচরণেরই একটা রূপ। যদি allocator বড় একটা chunk mmap করে আর zero-out করার জন্য touch করে (কিছু allocator security বা predictability-র জন্য করে), তখনই সমস্যা।

Overcommit-এর সাথে সংযোগ: /proc/sys/vm/overcommit_memory সেটিং নির্ধারণ করে kernel কি mmap()-এর সময়েই প্রত্যাখ্যান করবে (যদি মোট virtual demand physical+swap-এর চেয়ে বেশি হয়), নাকি allow করে দিয়ে পরে touch করার সময় OOM killer-এর উপর ছেড়ে দেবে। এটাই লেসন ১০-এর কেন্দ্রীয় বিষয়।

Level 12-এ আমরা এই cgroup accounting বিস্তারিত দেখব, আর docker stats-এর সংখ্যা ঠিক কোন /sys/fs/cgroup/ ফাইল থেকে আসে তা খুঁজে বের করব।

এরপর কী

পরের লেসন — fork, exec, wait

এই লেসনে আমরা একটা process-এর ভেতরটা দেখলাম — তার struct, তার address space, তার state machine। কিন্তু একটা মৌলিক প্রশ্ন এখনো বাকি: একটা নতুন process কোথা থেকে আসে?

আপনি shell-এ ls টাইপ করলেন। কেউ একজন task_struct বানাল, তাতে PID বসাল, তার জন্য একটা address space তৈরি করল, আর ls-এর কোড সেখানে লোড করল। কে করল এটা, আর কীভাবে?

উত্তরটা Unix-এর সবচেয়ে অদ্ভুত আর সবচেয়ে সুন্দর design decision-গুলোর একটা: fork() — একটা function যেটা দুইবার return করে। একবার parent-এ, একবার child-এ। এই একটা idea থেকে পুরো Unix process model তৈরি — shell pipeline, & দিয়ে background job, সার্ভার-এর worker process, সবকিছু।

পরের লেসনে আমরা দেখব fork() কীভাবে এত সস্তা হতে পারে যখন একটা পুরো address space কপি করার কথা (উত্তর: copy-on-write, যা এখন আমরা page table নিয়ে যা শিখেছি তার সরাসরি প্রয়োগ), exec() কীভাবে একটা process-এর “পরিচয়” বদলে দেয় তার PID অক্ষত রেখে, আর কেন zombie আজকের লেসনে যা দেখেছি তার একটা স্বাভাবিক পরিণতি — parent যদি কখনো wait() না ডাকে।

আরও পড়ুন

  • Linux kernel source — include/linux/sched.h (struct task_struct) · প্রকৃত সংজ্ঞা — এই লেসনের প্রতিটা field এখানে যাচাইযোগ্য, যদিও kernel version-ভেদে সামান্য বদলায়
  • The Linux Programming Interface, Chapter 6 — Process — Michael Kerrisk · Process-এর memory layout ও PID/PGID/session সম্পর্কের সবচেয়ে নির্ভুল বর্ণনা
  • proc(5) man page · /proc/<pid>/status ও /proc/<pid>/stat-এর প্রতিটা field-এর প্রামাণ্য অর্থ