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

/proc আর /sys — Kernel-এর ভেতরটা যখন শুধুই একটা ফাইল পড়া

/proc and /sys: Kernel Introspection as File Reads

এই পুরো module জুড়ে আমরা বারবার /proc ব্যবহার করেছি — VmRSS পড়েছি, utime/stime পড়েছি, /proc/interrupts দেখেছি, /proc/PID/task/ ঘেঁটেছি — কিন্তু কখনো থেমে জিজ্ঞাসা করিনি এটা আসলে কী জিনিস। এই শেষ লেসনে কোনো নতুন mechanism নেই — শুধু পুরো module-এ যা বানানো হয়েছে (process, thread, memory, fd, namespace, cgroup) সেটাকে এখন সরাসরি, লাইভ চোখে দেখা, আর ps/top/lsof/ss-এর মতো টুল যে কোনো জাদু না — শুধু ফরম্যাট-করা ফাইল-পড়া — সেটা নিজের হাতে প্রমাণ করা।

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

  • /proc আর /sys-কে virtual filesystem হিসেবে নির্ভুলভাবে ব্যাখ্যা করতে পারবেন — ডিস্কে কোনো ব্লক না থেকেও read() syscall-এ kernel কীভাবে on-the-fly content generate করে, vfs-layer লেসনের pseudo-filesystem ধারণার সরাসরি প্রয়োগ হিসেবে
  • `/proc/<pid>/status`, `/proc/<pid>/maps`, `/proc/<pid>/fd/`, `/proc/<pid>/ns/`, `/proc/<pid>/task/` — প্রতিটা ফাইলকে এই module-এর একটা নির্দিষ্ট আগের লেসনের নির্দিষ্ট concept-এর সাথে সরাসরি যুক্ত করে দেখাতে পারবেন, যাতে বোঝা যায় এগুলো নতুন কিছু না — আগে যা শিখেছেন তারই দৃশ্যমান রূপ
  • procfs (পুরনো, খানিকটা ঐতিহাসিক দুর্ঘটনাক্রমে গঠিত, one-file-many-values) বনাম sysfs (নতুন, kobject-ভিত্তিক, one-value-per-file নিয়মে সুশৃঙ্খল) — এই দুই virtual filesystem-এর ডিজাইন দর্শনের পার্থক্য ব্যাখ্যা করতে পারবেন, আর একটা অচেনা ফাইল দেখেই সেটা কোন ধরনের বলতে পারবেন
  • `/sys/fs/cgroup/`-এ গিয়ে `memory.max`, `cpu.max`, `memory.current`-এর মতো cgroup v2 control file নিজে পড়তে ও লিখতে পারবেন, namespaces-and-cgroups লেসনের cgroup content-এর সাথে সরাসরি যুক্ত করে
  • `ps`, `top`, `lsof`, `ss` — এই টুলগুলো ঠিক কোন `/proc` ফাইল পড়ে, কোন field পার্স করে তা নির্দিষ্টভাবে বলতে পারবেন, আর নিজে একটা ন্যূনতম Python স্ক্রিপ্ট লিখে `ps`-এর একটা অংশ পুনর্নির্মাণ করতে পারবেন
  • `/proc` পড়ার সময় race condition ও non-atomicity-র সীমা চিনতে পারবেন — কেন একটা পুরো-সিস্টেম `ps` snapshot আসলে consistent না, আর কেন এটা race-conditions-and-deadlock লেসনের একই সমস্যার আরেকটা রূপ

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

আগে এটা বুঝি

এই পুরো module-এ আমরা একটা টুল বারবার ব্যবহার করেছি, কিন্তু কখনো থেমে ব্যাখ্যা করিনি এটা আসলে কী।

  • memory-allocation লেসনে rss_hole.c প্রোগ্রামটা RSS মাপার জন্য /proc/self/status খুলে VmRSS: %ld kB পার্স করেছিল।
  • linux-schedulers লেসনে আমরা /proc/$PID/stat-এর ১৪ আর ১৫ নম্বর field পড়ে utime আর stime বের করেছিলাম।
  • threads লেসনে ls /proc/$PID/task/ দিয়ে একটা process-এর সব thread গুনেছিলাম, আর diff /proc/$PID/task/A/maps /proc/$PID/task/B/maps দিয়ে প্রমাণ করেছিলাম তারা একই address space শেয়ার করে।
  • interrupts-and-drivers লেসনে /proc/interrupts দিয়ে IRQ counter বাড়তে দেখেছিলাম, আর /proc/irq/24/smp_affinity-তে লিখে একটা IRQ-কে একটা নির্দিষ্ট CPU-তে পিন করেছিলাম।
  • process-anatomy লেসনে /proc/self/maps দিয়ে নিজের প্রোগ্রামের memory layout যাচাই করেছিলাম।

প্রতিবারই /proc একটা প্রস্তুত সরঞ্জাম ছিল, যেন এটা এমনিতেই সেখানে আছে, কোনো ব্যাখ্যার দরকার নেই। এখন সময় এসেছে সেই ধরে-নেওয়া জিনিসটা নিজেই প্রশ্ন করার।

একটা ছোট পরীক্ষা দিয়ে শুরু করা যাক। টার্মিনালে চালান:

$ ls -la /proc/self/status
-r--r--r-- 1 rin rin 0 Aug 20 12:00 /proc/self/status
$ wc -c /proc/self/status
1287 /proc/self/status

ls -la বলছে ফাইলের আকার ০ বাইটwc -c বলছে এতে ১২৮৭ বাইট আছে। দুটোই একসাথে সত্যি — কারণ এটা আসলে কোনো “ফাইল” না, অন্তত সেই অর্থে না যেভাবে data.txt একটা ফাইল। এখানে ডিস্কে কোনো ব্লক নেই, কোনো inode-এ block map নেই, কোনো আকার আগে থেকে জানা নেই — read() কল করার মুহূর্তে kernel বসে সেই মুহূর্তের সত্য তথ্য দিয়ে content বানায়

আজকের লেসনে কোনো নতুন mechanism নেই। এই module-এর প্রতিটা লেসনে kernel-এর ভেতরে যা যা বানিয়েছি — process, thread, address space, fd table, namespace, cgroup — আজ সেগুলোর প্রতিটাই একটা ফাইলের ভেতর দিয়ে সরাসরি দেখা যাবে। এটাই এই পুরো module-এর payoff লেসন — নতুন কিছু শেখা না, বরং যা শিখেছেন তা দেখতে পারা

মূল ধারণা

Virtual filesystem — vfs-layer-এর প্রতিশ্রুতি রাখা

vfs-layer লেসনে একটা দাবি করা হয়েছিল: “একটা read() syscall যেভাবে ext4-এ কাজ করে, ঠিক সেভাবেই NFS, tmpfs বা /proc-এ কাজ করে — অ্যাপ্লিকেশন কোড কখনো জানেই না নিচে কোন filesystem চলছে।” আর সেই লেসনেই pseudo-filesystem-এর ধারণা প্রথম আনা হয়েছিল — “ফাইল যা আসলে ফাইল নয়, বরং kernel state-এর একটা live view।” আজ সেই প্রতিশ্রুতিটা বাস্তবে দেখব।

যা একটা সাধারণ (ext4) ফাইলে ঘটে:

open() → VFS dentry/inode lookup → inode-এ থাকা block map অনুযায়ী ডিস্ক থেকে ব্লক পড়া → page cache-এ কপি → read() সেই cache থেকে বাইট ফেরত দেয়। ফাইলের content ডিস্কে সংরক্ষিত — না পড়লেও সেটা সেখানে থাকে।

যা /proc/self/status-এ ঘটে:

open() → VFS dentry/inode lookup ঠিকই হয় (procfs-এরও নিজের superblock আর inode আছে) — কিন্তু সেই inode-এর file_operations-এ কোনো disk block-এর pointer নেই। এর বদলে একটা function pointer আছে, যেমন proc_pid_statusread() কল করলে VFS সেই function-টাই ডাকে — আর সেই function সেই মুহূর্তে task_struct থেকে সরাসরি field পড়ে, একটা string বানায়, আর সেটাই ফেরত দেয়। read() করার আগ পর্যন্ত এই content কোথাও অস্তিত্বই ছিল না।

cat /proc/1234/status -- একটা read() কোথায় যায়
  1. userspace: catopen("/proc/1234/status") তারপর read(fd, buf, 4096) — অন্য যেকোনো ফাইলের মতোই দুইটা সাধারণ syscall
  2. VFS: dentry/inode lookupprocfs-এর নিজস্ব superblock; /proc/1234 dentry-টা প্রতিবার lookup-এ dynamically বানানো হয় pid 1234 এখনো বেঁচে আছে কি না যাচাই করে
  3. inode->f_op->read_iter()এই function pointer টাই আসল ঠিকানা — procfs-এ এটা কোনো ext4_file_read_iter না, বরং seq_read()
  4. seq_file layer: proc_pid_status()fs/proc/array.c-এ একটা function যেটা task_struct থেকে সরাসরি pid, state, VmRSS, threads ইত্যাদি পড়ে একটা string বানায় -- এই মুহূর্তে, এই read()-এর জন্যই
  5. task_struct (RAM-এ ইতিমধ্যে আছে)কোনো নতুন ডেটা তৈরি হয়নি -- process-anatomy লেসনের সেই ~৭ KB struct-টাই, শুধু ভিন্নভাবে serialize হলো
  6. buffer → userspace, disk কখনো ছোঁয়া হয়নিblock layer, page cache backing store -- এসবের কিছুই এই পথে নেই, কারণ ডিস্কে কিছু সংরক্ষিতই ছিল না

তিনটা প্রত্যক্ষ ফলাফল, যেগুলো উপরের ls/wc পরীক্ষা ব্যাখ্যা করে:

  1. আকার অর্থহীন — তাই ০। ফাইল সিস্টেম কল করার আগে জানেই না content কত বড় হবে, তাই stat() একটা placeholder () দেয়। কিন্তু read() করলে ঠিক ততটাই বাইট আসে যতটা সেই মুহূর্তে দরকার।
  2. দুইবার পড়লে ভিন্ন content আসতে পারে। cat /proc/self/status দুইবার চালান — দ্বিতীয়বার voluntary_ctxt_switches বদলে গেছে, কারণ দুইটা cat আসলে আলাদা দুইটা process (প্রতিবার নতুন PID), আর প্রতিটা call একটা fresh generation।
  3. কোনো caching নেই, কোনো “stale” copy নেই। ডিস্ক থেকে পড়া একটা normal ফাইল page cache-এ থাকতে পারে (আগের কোনো write প্রতিফলিত না হয়ে), কিন্তু /proc ফাইলে প্রতিটা read() মানেই একটা fresh function call — চিরকাল বর্তমান, কখনো stale না (যদিও, এই লেসনের misconception অংশে দেখব, এটাই একটা ভিন্ন সমস্যার জন্ম দেয়: atomicity)।

ভেতরে কী ঘটছে

/proc — প্রতিটা আগের লেসন, এখন লাইভ

আসুন এই module-এর প্রতিটা বড় concept-এর জন্য ঠিক কোন /proc ফাইল সেটাকে উন্মুক্ত করে, তা এক এক করে দেখি। প্রতিটা এখানে সরাসরি callback — কোনোটাই নতুন ধারণা না।

/proc/[pid]/status — task_struct-এর মানব-পাঠযোগ্য অনুবাদ

process-anatomy লেসনে আমরা task_struct-কে “kernel-এর দৃষ্টিতে আপনার process” বলেছিলাম — PID থেকে page table pointer পর্যন্ত সবকিছু ধরে রাখা একটা ~৭ KB struct। /proc/<pid>/status সেই struct-এর একটা readable প্রতিচ্ছবি:

$ cat /proc/$$/status | head -20
Name:   bash
Umask:  0022
State:  S (sleeping)
Tgid:   48213
Ngid:   0
Pid:    48213
PPid:   48100
TracerPid:      0
Uid:    1000    1000    1000    1000
Gid:    1000    1000    1000    1000
FDSize: 256
Groups: 1000 27 100
VmPeak:    21504 kB
VmSize:    21504 kB
VmRSS:      5632 kB
VmData:     2048 kB
VmStk:       132 kB
VmExe:       772 kB
Threads:        1

$$ মানে চলমান shell-এর নিজের PID — এই মুহূর্তে ৪৮২১৩, ঠিক threads লেসনে যে PID-টা আমরা বারবার উদাহরণ হিসেবে ব্যবহার করেছিলাম। এই একটা ফাইল কতগুলো আগের concept একসাথে ধরছে দেখুন:

status-এর fieldকোন লেসনের concept
State: S (sleeping)process-anatomy-র state machine — running, interruptible sleep (S), uninterruptible sleep (D), zombie (Z)
Tgid / Pidthreads লেসনের TID বনাম thread-group-id পার্থক্য — একটা single-threaded process-এ দুটো সমান
PPidfork-exec-wait লেসনের parent-child সম্পর্ক
VmRSSmemory-allocation ও page-faults-and-demand-paging — কতটা physical RAM আসলে touch হয়েছে
VmSizevirtual address space-এর মোট আকার — touch হোক বা না হোক, শুধু mapping-এর হিসাব
Threadsthreads লেসনের thread group-এর সদস্য সংখ্যা
FDSizefile-descriptors লেসনের fd table-এর current capacity

/proc/[pid]/maps — address space-এর আক্ষরিক মানচিত্র

process-anatomy আর stack-and-heap লেসনে address space-এর একটা ছবি এঁকেছিলাম — text, rodata, data, bss, heap, mmap region, stack। /proc/<pid>/maps সেই ছবিটারই কার্নেলের সত্যিকারের সংস্করণ, প্রতিটা VMA (virtual memory area) একটা লাইনে:

$ cat /proc/$$/maps
55f2a1200000-55f2a12a0000 r-xp 00000000 08:01 1234  /usr/bin/bash
55f2a14a0000-55f2a14a8000 r--p 000a0000 08:01 1234  /usr/bin/bash
55f2a14a8000-55f2a14ac000 rw-p 000a8000 08:01 1234  /usr/bin/bash
55f2a3210000-55f2a3260000 rw-p 00000000 00:00 0     [heap]
7f8b4c000000-7f8b4c021000 rw-p 00000000 00:00 0
7f8b4c1c0000-7f8b4c1e5000 r-xp 00000000 08:01 5678  /usr/lib/libc.so.6
7ffe12340000-7ffe12361000 rw-p 00000000 00:00 0     [stack]
7ffe12390000-7ffe12394000 r--p 00000000 00:00 0     [vvar]
7ffe12394000-7ffe12396000 r-xp 00000000 00:00 0     [vdso]

প্রতিটা কলাম চিনে নেওয়া যাক — বাম থেকে ডানে: address range, permission (r/w/x/p=private বা s=shared), file offset, device, inode, আর শেষে path (থাকলে)।

পাঁচটা লাইন সরাসরি চেনা: [heap] — memory-allocation লেসনের brk দিয়ে বাড়া region। [stack] — stack-and-heap লেসনের growable stack, নিচের দিকে বাড়ে। libc.so.6-এর r-xp mapping — একটা shared library, assembly module-এর PIC/GOT/PLT লেসনের সাথে যুক্ত। [vdso] — kernel যে কয়েকটা syscall (যেমন gettimeofday) userspace-এই resolve করে দেয়, পুরো syscall trap এড়িয়ে। আর bash executable-এর নিজেই তিনটা আলাদা mapping — r-xp (text, executable কোড), r--p (rodata), rw-p (data+bss) — ঠিক ELF file-এর তিনটা আলাদা segment।

/proc/[pid]/fd/ — file-descriptors-এর তিন-স্তর, এখন বাস্তব ফাইল হিসেবে

file-descriptors লেসনের কেন্দ্রীয় ছবি ছিল তিন স্তরের indirection — per-process fd table → system-wide open file table → inode table। /proc/<pid>/fd/ সেই fd table-টাকেই আক্ষরিক symlink হিসেবে দেখায়:

$ ls -la /proc/$$/fd/
lrwx------ 1 rin rin 64 Aug 20 12:00 0 -> /dev/pts/3
lrwx------ 1 rin rin 64 Aug 20 12:00 1 -> /dev/pts/3
lrwx------ 1 rin rin 64 Aug 20 12:00 2 -> /dev/pts/3
lrwx------ 1 rin rin 64 Aug 20 12:00 255 -> /dev/pts/3

আর একটা pipe খোলা একটা প্রোগ্রামে (যেমন sleep 100 | cat-এর cat অংশ):

$ ls -la /proc/$(pgrep -f 'cat$')/fd/
lrwx------ 1 rin rin 64 Aug 20 12:01 0 -> 'pipe:[884213]'
lrwx------ 1 rin rin 64 Aug 20 12:01 1 -> /dev/pts/3

pipe:[884213] — এটা কোনো real path না, এটা pipe-এর inode নম্বর, বন্ধনীতে মোড়া। এখানে সরাসরি দেখা যাচ্ছে — fd 0 একটা সাধারণ ফাইলের পথে না গিয়ে সোজা একটা pipe-এর দিকে নির্দেশ করছে, ঠিক যেভাবে pipes-and-fifos লেসনে বলা হয়েছিল pipe-এর নিজের কোনো নাম নেই, শুধু একটা inode

readlink-এর মাধ্যমে এই symlink-গুলো পড়া যায় প্রোগ্রাম থেকেও:

$ readlink /proc/$$/fd/1
/dev/pts/3
$ readlink /proc/$$/fd/255
/dev/pts/3

এইখানেই lsof-এর পুরো secret লুকিয়ে — realworld অংশে দেখব।

/proc/[pid]/ns/ — namespace-এর লিঙ্ক

namespaces-and-cgroups লেসনে যে namespace-গুলো তৈরি হয়েছিল (PID, mount, net, UTS, IPC, user), সেগুলোর প্রতিটার একটা করে entry এখানে থাকে:

$ ls -la /proc/$$/ns/
lrwxrwxrwx 1 rin rin 0 Aug 20 12:00 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 rin rin 0 Aug 20 12:00 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 rin rin 0 Aug 20 12:00 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 rin rin 0 Aug 20 12:00 net -> 'net:[4026531992]'
lrwxrwxrwx 1 rin rin 0 Aug 20 12:00 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 rin rin 0 Aug 20 12:00 user -> 'user:[4026531837]'
lrwxrwxrwx 1 rin rin 0 Aug 20 12:00 uts -> 'uts:[4026531838]'

বন্ধনীর ভেতরের সংখ্যা প্রতিটা namespace-এর inode নম্বর — namespace নিজেই kernel-এর চোখে একটা বিশেষ ধরনের inode, যদিও কোনো ডিস্কে থাকে না। এখান থেকেই প্রমাণ হয় দুইটা process একই namespace-এ আছে কি না — namespace ID একই মানে সেই namespace শেয়ার করছে:

$ readlink /proc/1/ns/pid
pid:[4026531836]
$ readlink /proc/$$/ns/pid
pid:[4026531836]        # host-এর সব process-এর জন্য একই
$ sudo unshare --pid --fork --mount-proc readlink /proc/self/ns/pid
pid:[4026532841]        # সম্পূর্ণ ভিন্ন সংখ্যা -- নতুন namespace

এটাই ঠিক namespaces-and-cgroups লেসনের unshare demo-র সাথে যে container টুল (Docker, containerd) verify করে একটা process কোন namespace-এ আছে — nsenter --target <pid> --pid --mount ... আসলে ভেতরে ভেতরে এই /proc/<pid>/ns/* ফাইলগুলোর দিকেই setns() করে।

/proc/meminfo আর VmRSS — memory-allocation/page-faults, সিস্টেম-স্কেলে

/proc/<pid>/status-এর VmRSS process-স্তরের হিসাব দেয়; /proc/meminfo পুরো সিস্টেমের:

$ head -10 /proc/meminfo
MemTotal:       16384000 kB
MemFree:         2148392 kB
MemAvailable:    9823104 kB
Buffers:          412300 kB
Cached:          6234112 kB
SwapTotal:       2097148 kB
SwapFree:        2097148 kB
Active:          8123456 kB
Inactive:        3421000 kB
Dirty:              4212 kB

page-faults-and-demand-paging লেসনের active/inactive list, swappiness — এই সবকিছুর সিস্টেম-স্কেল সংখ্যা এখানেই। MemAvailable বিশেষভাবে গুরুত্বপূর্ণ — শুধু MemFree না, কারণ Cached-এর একটা অংশ চাইলেই reclaim করা যায় (page cache, dirty না এমন), তাই MemAvailable একটা বেশি বাস্তবসম্মত “কতটা RAM সত্যিই খালি” সংখ্যা।

/proc/interrupts — interrupts-and-drivers, লাইভ counter

আগের লেসনে যেভাবে দেখেছিলাম:

$ cat /proc/interrupts | head -5
           CPU0       CPU1       CPU2       CPU3
  24:      18452      18301      18398      18276   IO-APIC   24-fasteoi   eth0
  ...

এখানে নতুন কিছু বলার নেই — শুধু মনে করিয়ে দেওয়া যে এটাও একই /proc পরিবারের সদস্য, আর interrupts-and-drivers লেসন এটাকে ব্যাখ্যা ছাড়াই ব্যবহার করেছিল, ঠিক এই লেসনের ইন্ট্রোতে যেমন উল্লেখ করা হয়েছে।

/proc/[pid]/stat — cpu-scheduling ও linux-schedulers-এর কাঁচা সংখ্যা

/proc/<pid>/status মানব-পাঠযোগ্য; /proc/<pid>/stat একই তথ্যের একটা এক-লাইনের, স্পেস-দিয়ে-আলাদা সংস্করণ, প্রোগ্রামে পার্স করার জন্য বানানো:

$ cat /proc/$$/stat
48213 (bash) S 48100 48213 48213 34817 48213 4194560 4102 ...

linux-schedulers লেসনে ঠিক এই ফাইলের ১৪ আর ১৫ নম্বর field (utime, stime) পড়ে দুইটা প্রসেসের CPU-bound বনাম I/O-bound আচরণ তুলনা করেছিলাম। পুরো field তালিকার একটা কেন্দ্রীয় অংশ:

#নামঅর্থ
1pidprocess ID
2commexecutable-এর নাম, বন্ধনীতে
3stateR/S/D/Z/T — process-anatomy-র state machine
4ppidparent PID
14utimeuser-mode CPU time, clock tick-এ (সেকেন্ডে না!)
15stimekernel-mode CPU time, একই এককে
19priorityscheduling priority
20nicenice value — cpu-scheduling লেসনের সেই -20 থেকে +19 স্কেল
39processorএই মুহূর্তে কোন CPU core-এ শেষ চলেছিল

“/proc/pid/stat-এর utime মিলিসেকেন্ডে থাকে, তাই সরাসরি তুলনা করা যায়”

না — এককটা “clock tick”, সেকেন্ড না, মিলিসেকেন্ডও না। linux-schedulers লেসনে যেমন দেখানো হয়েছিল, sysconf(_SC_CLK_TCK) সাধারণত 100 রিটার্ন করে (Linux-এ প্রায় সবসময়), মানে প্রতি tick = ১০ মিলিসেকেন্ড। তাই utime = 1490 মানে 14.9 সেকেন্ড, 1490 * 10 মিলিসেকেন্ড না। কনভার্শন ভুলে গেলে সময়ের হিসাব ১০০ গুণ ভুল হয়ে যায় — বাস্তবে এটা একটা সাধারণ bug, বিশেষত মনিটরিং টুল লেখার সময়।

/proc/[pid]/task/ — threads, প্রতিটা এন্ট্রি একটা TID

threads লেসনের সবচেয়ে গুরুত্বপূর্ণ প্রমাণ ছিল এই ডিরেক্টরি:

$ ls /proc/48213/task/
48213  48214  48215  48216
$ grep Threads /proc/48213/status
Threads:        4

প্রতিটা সংখ্যা একটা TID — kernel-এর ভাষায় প্রতিটাই একটা আলাদা task_struct, কিন্তু একটাই mm_struct (address space) শেয়ার করছে। /proc/48213/task/48214/maps আর /proc/48213/task/48215/maps হুবহু অভিন্ন হবে, ঠিক threads লেসনে যা দেখানো হয়েছিল।

এখানে একটা সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ পয়েন্ট — /proc/<pid>/ নিজেই আসলে /proc/<tgid>/task/<tgid>/-এর একটা shortcut। যখন ls /proc/ করেন, শুধু thread group leader-এর TID-গুলোই দেখা যায় (কারণ getpid() সব thread-এ tgid দেয়), কিন্তু kernel-এর ভেতরে প্রতিটা thread-এর নিজস্ব entry আছে task/-এর নিচে।

/sys — যখন procfs-এর অগোছালোতা থেকে শেখা একটা শৃঙ্খলা

এতক্ষণ যা দেখলাম তাতে একটা প্যাটার্ন লক্ষ্য করার মতো — status-এ একসাথে অনেকগুলো field (key: value জোড়া, এক ফাইলে), maps-এ একটা টেবিল, stat-এ একটা মাত্র লাইনে ৫০+ field স্পেস দিয়ে আলাদা, কোনো header ছাড়াই। প্রতিটা ফাইলের নিজস্ব, অসামঞ্জস্যপূর্ণ ফরম্যাট

এটা কোনো দুর্ঘটনা না — এটা ইতিহাস। procfs শুরু হয়েছিল Linux-এর একদম প্রথম দিকে (৯০-এর দশকের গোড়ায়), যখন যে যেভাবে দরকার মনে করেছে সেভাবেই একটা নতুন /proc ফাইল যোগ করেছে — কোনো কেন্দ্রীয় নকশা-নিয়ম ছাড়াই। ফল হলো এমন একটা filesystem যেখানে কিছু ফাইল human-readable টেবিল, কিছু ফাইল একলাইনের কাঁচা সংখ্যা, কিছু ফাইল বাইনারি, আর কিছু ফাইলে (stat-এর মতো) নাম দেখেই বোঝা যায় না ভেতরে কী আছে।

দুই দর্শনের পাশাপাশি তুলনা:

procfs (/proc)sysfs (/sys)
জন্মLinux-এর একদম শুরু থেকে (১৯৯২ থেকে বিবর্তিত)Linux 2.6 (২০০৩), device model-এর জন্য
বিষয়বস্তুprocess/thread state + কিছু kernel-wide তথ্যdevice, driver, bus, module, block, class — kernel object (kobject)
ফাইল-প্রতি মানপ্রায়ই একাধিক (যেমন stat-এ ৫০+ field, status-এ ডজনখানেক key)কঠোরভাবে একটা (memory.max মানে শুধু একটা সংখ্যা বা max শব্দ)
গঠনঐতিহাসিক — নতুন ফাইল প্রয়োজনমতো যোগ হয়েছে, সামঞ্জস্যের নিশ্চয়তা নেইসচেতনভাবে kobject hierarchy-কে ডিরেক্টরি গাছে map করা
উদাহরণ/proc/meminfo, /proc/interrupts, /proc/<pid>/stat/sys/class/net/eth0/mtu, /sys/block/sda/size, /sys/fs/cgroup/.../memory.max

এই পার্থক্যটা শুধু ট্রিভিয়া না — এটা একটা বাস্তব, সৎ সফটওয়্যার-ইঞ্জিনিয়ারিং শিক্ষা: একটা interface যত বেশি বছর ধরে “যা দরকার তাই যোগ করো” নীতিতে বাড়ে, তত বেশি অসামঞ্জস্যপূর্ণ হয়ে যায় — আর একটা নতুন প্রজন্মের ডিজাইনার প্রায়ই আগের প্রজন্মের সেই অগোছালোতা দেখেই একটা কড়া নিয়ম চাপিয়ে ঠিক করার চেষ্টা করে। HTTP/1.1-এর অসংগতি থেকে HTTP/2-এর কড়া framing, বা JavaScript-এর ==-এর বিভ্রান্তি থেকে TypeScript-এর কড়া টাইপিং — একই প্যাটার্নের আরও উদাহরণ।

/sys-এর গঠন সংক্ষেপে

/sys/
├── class/          # ডিভাইস-টাইপ অনুযায়ী (net, block, tty, ...)
│   └── net/eth0/ → symlink আসল ডিভাইসে
├── devices/         # প্রকৃত hierarchy — bus topology অনুযায়ী
├── bus/             # bus টাইপ অনুযায়ী (pci, usb, ...)
├── block/           # block device (sda, nvme0n1, ...)
├── module/          # লোড হওয়া kernel module-এর parameter
└── fs/
    └── cgroup/      # cgroup v2 unified hierarchy -- নিচে দেখুন

লক্ষ করুন class/ আর devices/-এ প্রায়ই একই ডিভাইস দুইবার দেখা যায় — একবার bus topology অনুযায়ী (devices/), একবার টাইপ অনুযায়ী (class/, যেটা আসলে devices/-এর দিকে একটা symlink মাত্র)। এটাই kobject model-এর মূল সুবিধা — একই kernel object একাধিক “দৃষ্টিকোণ” থেকে দেখা যায়, কিন্তু আসল ডেটা একটাই জায়গায় থাকে।

/sys/fs/cgroup/ — namespaces-and-cgroups-এর control file, হাতে-কলমে

namespaces-and-cgroups লেসনে cgroup দিয়ে CPU, memory, IO সীমা বেঁধে দেওয়া হয়েছিল। সেই সীমাগুলো ঠিক এখানেই একটা ফাইলে লেখা থাকে — কোনো বিশেষ syscall লাগে না, একটা সাধারণ write():

$ cat /sys/fs/cgroup/mygroup/memory.max
209715200
$ cat /sys/fs/cgroup/mygroup/memory.current
104857600
$ cat /sys/fs/cgroup/mygroup/cpu.max
50000 100000
$ echo 100000000 | sudo tee /sys/fs/cgroup/mygroup/memory.max

memory.max — sysfs-এর “one value per file” নিয়ম মেনে ঠিক একটা সংখ্যা (বাইটে), memory.current তার পাশে বর্তমান ব্যবহার — একই নিয়মে আরেকটা ফাইল, একসাথে না মিশিয়ে। cpu.max ব্যতিক্রম মনে হতে পারে (দুইটা সংখ্যা, স্পেসে আলাদা) — কিন্তু এটাও আসলে একটামাত্র “মান”: “প্রতি 100000 মাইক্রোসেকেন্ডে সর্বোচ্চ 50000 মাইক্রোসেকেন্ড CPU” — একটাই logical ratio, শুধু দুইটা সংখ্যায় প্রকাশ করা।

উদাহরণ

একটা process, শুরু থেকে শেষ পর্যন্ত পুরো ছবিটা

এতক্ষণ ফাইলগুলো আলাদা আলাদা দেখেছি। এবার একটা একক, চলমান process নিয়ে সবগুলো একসাথে জোড়া লাগিয়ে দেখি — ঠিক সেই PID ৪৮২১৩, threads লেসনের বাশ শেল, যেটা এখনো চলছে ধরে নিচ্ছি।

ধাপ ১ — process টা কে, কী অবস্থায়, কার সন্তান:

$ cat /proc/48213/status | grep -E 'Name|State|Pid|PPid|Threads|VmRSS'
Name:   bash
State:  S (sleeping)
Pid:    48213
PPid:   48100
Threads:        4
VmRSS:      5632 kB

চার লাইনে পুরো process-anatomy লেসনের সারাংশ — নাম, অবস্থা, বংশ, আর memory footprint।

ধাপ ২ — তার address space-এ কী কী region আছে:

$ wc -l /proc/48213/maps
23 /proc/48213/maps
$ grep heap /proc/48213/maps
55f2a3210000-55f2a3260000 rw-p 00000000 00:00 0  [heap]

ধাপ ৩ — তার খোলা ফাইল/pipe/socket কী কী:

$ ls -la /proc/48213/fd/ | awk '{print $9, $10, $11}'
0 -> /dev/pts/3
1 -> /dev/pts/3
2 -> /dev/pts/3
3 -> 'socket:[991823]'
255 -> /dev/pts/3

fd 3-এ একটা socket — হয়তো একটা background SSH agent connection। socket:[991823]-এর ভেতরের সংখ্যাটা দিয়ে ss-এর সাহায্যে কোন connection সেটা বের করা যায় — realworld অংশে দেখব।

ধাপ ৪ — সে কোন namespace-এ:

$ readlink /proc/48213/ns/*
mnt:[4026531840]
net:[4026531992]
pid:[4026531836]
...

host-এর ডিফল্ট namespace ID-গুলো — মানে এই bash কোনো container-এর ভেতরে না, সরাসরি host-এ চলছে।

ধাপ ৫ — তার cpu ব্যবহার, আর কতগুলো thread:

$ awk '{print $14, $15}' /proc/48213/stat
1490 487
$ ls /proc/48213/task/
48213  48214  48215  48216

এই পাঁচটা ধাপ — status, maps, fd, ns, stat/task — একসাথে এই পুরো module-এর process/thread/memory/fd/namespace-এর প্রতিটা টুকরাকে একটা মাত্র সংখ্যা (PID ৪৮২১৩) দিয়ে সংযুক্ত করল। এটাই /proc-এর আসল শক্তি — একটা মাত্র নম্বর জানলে পুরো process-কে বাইরে থেকে সম্পূর্ণভাবে পরিদর্শন করা যায়, একটাও debugger ছাড়াই।

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

EXPERIMENT

demand paging লাইভ দেখা -- VmRSS বাড়তে দেখা, বাইরে থেকে, একটা লাইনও কোড না বদলে

Linux· ১৫ মিনিট

দুইটা টার্মিনাল লাগবে। প্রথমটায় একটা প্রোগ্রাম যেটা বড় একটা region allocate করে, কিন্তু ধীরে ধীরে touch করে:

/* grow_slowly.c -- ৫০০ MB mmap করে, তারপর প্রতি সেকেন্ডে ১০ MB touch করে */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>

#define TOTAL (500L * 1024 * 1024)
#define STEP  (10L  * 1024 * 1024)

int main(void) {
    char *p = mmap(NULL, TOTAL, PROT_READ | PROT_WRITE,
                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    printf("mmap হলো, PID = %d, এখনো কিছু touch হয়নি\n", getpid());
    for (long off = 0; off < TOTAL; off += STEP) {
        memset(p + off, 1, STEP);   /* এই ধাপেই page fault হবে */
        sleep(1);
    }
    printf("সব touch শেষ, ২০ সেকেন্ড অপেক্ষা করে বের হচ্ছি\n");
    sleep(20);
    return 0;
}
$ gcc -O0 -o grow_slowly grow_slowly.c
$ ./grow_slowly &
mmap হলো, PID = 51203, এখনো কিছু touch হয়নি

দ্বিতীয় টার্মিনালে সেই PID-এর VmSize (মোট mapping) আর VmRSS (আসল physical RAM) প্রতি সেকেন্ডে দেখুন:

$ watch -n1 'grep -E "VmSize|VmRSS" /proc/51203/status'

যা দেখবেন: VmSize শুরু থেকেই ~৫১২০০০ kBmmap() করার সাথে সাথেই পুরো ৫০০ MB virtual address space সংরক্ষিত। কিন্তু VmRSS শুরু হয় প্রায় -র কাছাকাছি, আর প্রতি সেকেন্ডে ~১০,২৪০ kB করে বাড়ে — ঠিক ততটাই যতটা memset() সেই মুহূর্তে touch করেছে।

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

page-faults-and-demand-paging লেসনের দাবি -- allocate করা মানেই resident হওয়া না -- সত্যিই সত্যি; আর /proc পড়েই, প্রোগ্রামের ভেতরে কোনো printf/instrumentation ছাড়াই, এই আচরণ বাইরে থেকে পর্যবেক্ষণযোগ্য।

এটা কী প্রমাণ করে, ধাপে ধাপে:

  1. mmap() মানে শুধু একটা address range সংরক্ষণ — কোনো physical frame এখনো বরাদ্দ হয়নি। VmSize তাই সাথে সাথেই পূর্ণ।
  2. প্রতিটা memset() কল আসলে একটা করে region-এ প্রথমবার write করছে — আর প্রতিটা নতুন page-এ প্রথম access একটা minor page fault তৈরি করে (page-faults-and-demand-paging লেসনের কেন্দ্রীয় ঘটনা), যা kernel-কে একটা নতুন physical frame allocate করতে বাধ্য করে।
  3. VmRSS-এর বৃদ্ধি এই page fault-গুলোর সরাসরি, বাইরে থেকে দৃশ্যমান প্রতিফলন — কোনো strace, কোনো perf, কোনো debugger দরকার হলো না, শুধু একটা /proc ফাইল প্রতি সেকেন্ডে পড়া।
  4. যদি প্রোগ্রামটা শেষে sleep(20) না করে সাথে সাথে বের হয়ে যেত, VmRSS তৎক্ষণাৎ আবার প্রায় -এ ফিরে যেত — process বের হয়ে গেলে তার সব physical frame মুক্ত হয়ে যায়, কিন্তু ততক্ষণ যতটা physical RAM সে ধরে রেখেছিল, ঠিক ততটাই VmRSS-এ দেখা গেছে, কোনো কম-বেশি না।

নিজে বানান

BUILD IT

tinyps -- ps-এর একটা অংশ পুনর্নির্মাণ করুন, শুধু /proc পড়ে

Python 3 · ●●○○○
  1. /proc/-এর নিচে যে এন্ট্রিগুলো সম্পূর্ণ সংখ্যা (PID) সেগুলো os.listdir দিয়ে বাছাই করুন -- অন্য এন্ট্রি (self, meminfo, ইত্যাদি) বাদ দিন
  2. প্রতিটা PID-এর জন্য /proc/PID/status থেকে Name আর State পড়ুন -- ফাইল না খুলতে পারলে (process মাঝপথে বের হয়ে গেছে) সেই PID স্কিপ করুন
  3. একই PID-এর /proc/PID/stat থেকে ১৪ ও ১৫ নম্বর field (utime, stime) পড়ে মোট CPU tick যোগ করুন
  4. SC_CLK_TCK (os.sysconf) দিয়ে tick-কে সেকেন্ডে রূপান্তর করুন
  5. PID, নাম, state, আর CPU সেকেন্ড -- চারটা কলামে সাজিয়ে প্রিন্ট করুন, আসল ps-এর আউটপুটের সাথে মিলিয়ে যাচাই করুন
#!/usr/bin/env python3
# tinyps.py -- ps-এর একটা ন্যূনতম, সৎ পুনর্নির্মাণ।
# কোনো C library কল না, কোনো syscall wrapper না -- শুধু open()/read(),
# ঠিক যেভাবে প্রকৃত procps-ng-র readproc.c কাজ করে।

import os

CLK_TCK = os.sysconf('SC_CLK_TCK')  # সাধারণত 100 -- linux-schedulers লেসনের সেই ধ্রুবক

def read_status(pid):
    name, state = None, None
    with open(f'/proc/{pid}/status') as f:
        for line in f:
            if line.startswith('Name:'):
                name = line.split(':', 1)[1].strip()
            elif line.startswith('State:'):
                state = line.split(':', 1)[1].strip().split()[0]
    return name, state

def read_cpu_ticks(pid):
    with open(f'/proc/{pid}/stat') as f:
        fields = f.read().split()
    # fields[1] হলো "(comm)" -- নামে স্পেস থাকলে split() ভেঙে যেতে পারে,
    # তাই আসল ps ')'-এর শেষ অবস্থান খুঁজে সেখান থেকে গোনে। এখানে সরলীকৃত।
    utime, stime = int(fields[13]), int(fields[14])
    return utime + stime

def main():
    print(f'{"PID":>7}  {"STATE":<6}  {"CPU(s)":>8}  CMD')
    for entry in sorted(os.listdir('/proc'), key=lambda s: s.zfill(10)):
        if not entry.isdigit():
            continue
        pid = int(entry)
        try:
            name, state = read_status(pid)
            ticks = read_cpu_ticks(pid)
        except (FileNotFoundError, ProcessLookupError):
            continue  # ৩ নম্বর misconception -- মাঝপথে process মরে যেতে পারে
        cpu_sec = ticks / CLK_TCK
        print(f'{pid:>7}  {state:<6}  {cpu_sec:>8.2f}  {name}')

if __name__ == '__main__':
    main()

চালিয়ে দেখুন আসল ps-এর সাথে তুলনা করে:

$ python3 tinyps.py | head -6
    PID  STATE     CPU(s)  CMD
      1  S           2.14  systemd
    412  S           0.31  systemd-journal
    891  S           1.02  sshd
  48213  S          14.90  bash
  51203  S           0.08  grow_slowly

$ ps -eo pid,state,cputime,comm | head -6
    PID S     TIME COMMAND
      1 S 00:00:02 systemd
    412 S 00:00:00 systemd-journal
    891 S 00:00:01 sshd
  48213 S 00:00:14 bash
  51203 S 00:00:00 grow_slowly

সংখ্যাগুলো মিলছে (সামান্য round-off বাদে)। এটাই এই লেসনের মূল দাবির প্রমাণ — ps জাদু কিছু করে না। এটা /proc-এর ঠিক ঐ ফাইলগুলোই পড়ে, ঠিক এই স্ক্রিপ্টের মতোই — শুধু বেশি field সামলায়, বেশি flag সমর্থন করে, আর C-তে লেখা বলে দ্রুত। procps-ng-এর proc/readproc.c ফাইল খুলে দেখলে দেখবেন মূল লজিক এই ৩০ লাইনের থেকে দর্শনগতভাবে আলাদা কিছু না।

এক ধাপ এগিয়ে (ঐচ্ছিক): /proc/PID/cmdline পড়ে (null বাইট দিয়ে আলাদা argument, .split('\0') দিয়ে ভাঙুন) পুরো command line দেখান, শুধু comm-এর সংক্ষিপ্ত নাম না — এটাই ps aux-এর COMMAND কলাম আর ps -o comm-এর COMM কলামের পার্থক্যের রহস্য।

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

প্রতিদিন ব্যবহার করা টুলগুলোর প্রায় সবগুলোই আসলে /proc বা /sys-এর একটা formatted view — নিচে আটটা উদাহরণ, প্রতিটাতে ঠিক কোন ফাইল পড়া হচ্ছে তা স্পষ্ট করে।

১. ps/proc/*/stat + /proc/*/status + /proc/*/cmdline ঠিক এই লেসনের build অংশে যা বানানো হলো। ps aux-এর প্রতিটা কলাম এই তিনটা ফাইলের কোনো না কোনো field-এর সরাসরি বা সামান্য-গণনা-করা মান — %CPU শুধু utime+stime-কে elapsed সময় দিয়ে ভাগ করা।

২. top/htop — একই ফাইল, কিন্তু বারবার, আর সাথে /proc/stat top প্রতি রিফ্রেশে (ডিফল্ট ৩ সেকেন্ড) সব PID-এর stat/status আবার পড়ে, দুই রিডিং-এর মাঝে utime+stime-এর পার্থক্য নিয়ে CPU% বের করে (একটা মুহূর্তের CPU tick সরাসরি অর্থবহ না, তার হার অর্থবহ)। সিস্টেম-স্তরের CPU% আসে /proc/stat-এর প্রথম লাইন থেকে — user, nice, system, idle, iowait — একই কৌশলে ডেল্টা নেওয়া। htop মূলত top-এরই ncurses UI-সহ সংস্করণ, একই ডেটা উৎস।

৩. free/proc/meminfo-র প্রথম কয়েক লাইনের পুনর্বিন্যাস। MemTotal, MemFree, MemAvailable, Buffers, Cached, SwapTotal, SwapFreefree -h এই field-গুলোই মানুষ-পাঠযোগ্য এককে (KB → MB/GB) রূপান্তর করে দেখায়, আর available কলাম গণনা করে MemAvailable থেকে (কার্নেল ৩.১৪+ থেকে সরাসরি পাওয়া যায়, তার আগে free নিজেই heuristic দিয়ে অনুমান করত)।

৪. lsof/proc/*/fd/-এর সব symlink readlink() করে। “list open files” আক্ষরিক অর্থেই এটাই করে — প্রতিটা PID-এর fd/-ডিরেক্টরিতে ঢুকে প্রতিটা symlink কোথায় নির্দেশ করছে পড়ে। lsof -p 48213 চালালে দেখবেন ঠিক সেই একই তথ্য যা আমরা ls -la /proc/48213/fd/ দিয়ে হাতে দেখেছিলাম, শুধু সুন্দর কলামে সাজানো, আর inode/device সংখ্যাগুলো /proc/*/maps-এর সাথে মিলিয়ে mapped ফাইলও দেখায় (lsof-এর কিছু আউটপুট আসলে maps থেকে আসে, fd/ থেকে না)।

৫. ss (আর পুরনো netstat) — /proc/net/tcp, /proc/net/udp, /proc/net/unix এই ফাইলগুলোতে প্রতিটা সক্রিয় socket-এর local/remote address, port, state, আর একটা inode নম্বর থাকে। সেই inode নম্বরটাই /proc/PID/fd/-এ socket:[991823]-এর ভেতরের সংখ্যা — ss -p এই দুইটা তথ্যকে cross-reference করে বলে দেয় কোন socket কোন process-এর দখলে। উদাহরণ থেকে মনে করুন — fd 3-এ socket:[991823] দেখেছিলাম; grep 991823 /proc/net/tcp-এর মতো (raw hex ফরম্যাটে) সেই কানেকশনের বিস্তারিত বের করা সম্ভব, ss -p ঠিক এটাই স্বয়ংক্রিয়ভাবে করে।

৬. docker stats / systemd-cgtop/sys/fs/cgroup/.../memory.current, cpu.stat, io.stat namespaces-and-cgroups লেসনের সরাসরি প্রয়োগ — প্রতিটা container একটা cgroup, আর “container-টা কত RAM/CPU খাচ্ছে” প্রশ্নের উত্তর সবসময় সেই cgroup-এর sysfs control file পড়েই আসে, কোনো বিশেষ Docker-only kernel API নেই। docker stats আসলে ভেতরে ভেতরে container-এর cgroup path বের করে সেই ফাইলগুলো পড়ে, ঠিক যেভাবে আমরা হুড অংশে memory.current হাতে পড়েছিলাম।

৭. strace -p PID / gdb attach — /proc/PID/mem, /proc/PID/syscall, আর ptrace। সরাসরি ফাইল-পড়া না হলেও একই পরিবার — /proc/PID/mem একটা বিশেষ ফাইল যেটার মাধ্যমে (ptrace-এর অনুমতি থাকলে) অন্য একটা process-এর memory-তে pread/pwrite করা যায়, lseek দিয়ে ঠিক maps-এ দেখা কোনো address-এ গিয়ে। gdb-র x/ কমান্ড (assembly module-এর debugging-with-gdb লেসনে দেখা) এই ফাইলটাই ব্যবহার করে, ptrace(PTRACE_PEEKDATA) এর আধুনিক, দ্রুততর বিকল্প হিসেবে।

৮. Security — hidepid mount option। ডিফল্টভাবে যেকোনো user ls /proc/-এ অন্য user-এর process-ও দেখতে পারে (যদিও fd/-এর মতো sensitive অংশে পড়ার অনুমতি থাকে না)। শুধু নিজের process ছাড়া কিছু লুকাতে চাইলে /proc-কে hidepid=2 mount option দিয়ে remount করা হয় (অনেক shared-hosting সার্ভারে ডিফল্ট) — তখন অন্য user-এর PID directory-ই ls-এ দেখা যায় না। এটা প্রমাণ করে /proc-ও একটা সাধারণ mount-point, তার নিজস্ব mount option থাকতে পারে, ঠিক vfs-layer লেসনের mount tree-র মতোই।

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

“/proc ফাইলগুলো ডিস্কে জায়গা নেয়, du করলে সেটা দেখা যাবে”

না — কোনো backing store নেই, তাই প্রকৃত অর্থে “জায়গা নেয়া” একটা প্রশ্নই অর্থহীন। concept অংশে দেখানো হয়েছে, stat() সাইজ হিসেবে রিপোর্ট করে কারণ content read()-এর মুহূর্তে বানানো হয়, আগে থেকে কোথাও সংরক্ষিত না। du -sh /proc চালালে একটা সংখ্যা দেখাবে ঠিকই, কিন্তু সেটা /proc-এর ভেতরের ফাইলগুলোর ঘোষিত (আসলে ভুয়া, সাধারণত ) সাইজের যোগফল না — এটা মূলত directory entry-গুলোর মেটাডেটা overhead গোনে, যেটা RAM-এ আছে, ডিস্কে না। মূল কথা — procfs/sysfs-এর জন্য “ডিস্ক স্পেস” ধারণাটাই প্রযোজ্য না, কারণ পুরো filesystem-টাই RAM-এ বাস করা একটা computation, কোনো block device-এর উপর না।

“ps বা top-এর আউটপুট একটা atomic, consistent snapshot -- সব প্রসেসের তথ্য একই মুহূর্তের”

না — এটা race-conditions-and-deadlock লেসনের সমস্যারই আরেকটা রূপ, শুধু ভিন্ন পরিসরে। ps যখন সিস্টেমের সব process তালিকাভুক্ত করে, সে /proc-এর প্রতিটা PID directory-তে একে একে, ক্রমানুসারে ঢুকে পড়ে — কোনো global lock নেই যা পুরো /proc-কে ওই মুহূর্তে freeze করে রাখে। এর মানে:

  • আপনার ps aux চলাকালীন, তালিকার প্রথম দিকের PID পড়া হওয়ার পরে কিন্তু শেষ দিকের PID পড়া হওয়ার আগে একটা নতুন process fork() হতে পারে — সেটা হয়তো তালিকায় থাকবে, হয়তো থাকবে না, নির্ভর করে ঠিক কখন directory listing নেওয়া হয়েছিল তার উপর।
  • একটা process ঠিক সেই মুহূর্তে exit করলে, ps তার stat/status খুলতে গিয়ে ENOENT পেতে পারে — ভালো tool (আর build অংশের tinyps.py) সেটা catch করে চুপচাপ স্কিপ করে, কিন্তু এই মুহূর্তে একটা সত্যিকারের TOCTOU (time-of-check-to-time-of-use) race ঘটে গেছে।
  • এমনকি একটা একক process-এর /proc/PID/status আর /proc/PID/stat — দুইটা আলাদা read() কল, দুইটার মাঝে kernel সেই process-এর state বদলে দিতে পারে। তাই দুইটা ফাইল থেকে পাওয়া তথ্য পুরোপুরি একই মুহূর্তের নাও হতে পারে।

/proc-এর প্রতিটা একক ফাইলের একটা read() call নিজে atomic (পুরো content একটা kernel function call-এই তৈরি হয়), কিন্তু একাধিক ফাইল বা একাধিক PID জুড়ে কোনো atomicity গ্যারান্টি নেই। ps/top তাই সবসময় একটা best-effort, সামান্য-পুরনো ছবি — মিলিসেকেন্ড-স্কেলে সঠিক, কিন্তু cycle-perfect snapshot না।

“/proc আর /sys মূলত একই জিনিস, শুধু নাম আলাদা”

না — hood অংশে বিস্তারিত দেখানো হয়েছে, দুইটার ডিজাইন দর্শন সরাসরি বিপরীত। procfs ঐতিহাসিকভাবে গঠিত, একটা ফাইলে অনেকগুলো field (stat-এর ৫০+ কলাম এক লাইনে) সাধারণ ব্যাপার — মূলত process আর কিছু kernel-wide পরিসংখ্যানের জন্য। sysfs সচেতনভাবে ডিজাইন করা, “one value per file” নিয়ম মেনে চলে, আর মূলত device/driver/bus/kobject-এর জন্য। একই সিস্টেমে দুইটাই একসাথে থাকে, একে অপরের replacement না, দুইটার কাজের পরিসর ভিন্ন — যদিও উভয়েই একই অর্থে “virtual filesystem”, আর উভয়েরই read() একই মেকানিজমে (kernel function → string) কাজ করে।

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

1

ls -la /proc/self/status ফাইলের সাইজ দেখায়, কিন্তু wc -c /proc/self/status কয়েকশো বাইট গোনে। এই দুইটা একসাথে সত্যি হওয়া কেন সম্ভব, আর একটা সাধারণ ext4 ফাইলে এমনটা কেন কখনো ঘটে না?

স্মরণ

কারণ /proc/self/status-এর কোনো পূর্ব-নির্ধারিত content নেইstat() syscall যখন সাইজ জিজ্ঞাসা করে, procfs-এর কাছে দেওয়ার মতো কোনো প্রকৃত সংখ্যা নেই (content এখনো বানানোই হয়নি), তাই এটা একটা placeholder ফেরত দেয়। কিন্তু wc -c আসলে ফাইলটা খুলে, read() করে, বাইট গুনে — আর read()-এর মুহূর্তে procfs-এর proc_pid_status function সত্যিই task_struct থেকে content generate করে, যেটার একটা বাস্তব দৈর্ঘ্য আছে।

একটা সাধারণ ext4 ফাইলে এমন হয় না কারণ সেখানে stat() inode-এ সংরক্ষিত প্রকৃত i_size পড়ে — একটা সংখ্যা যেটা প্রতিটা write() আপডেট করে রাখে, ডিস্কে। ext4-এ content আগে থেকেই ডিস্কে বসে আছে, তাই তার আকার আগে থেকেই জানা, কোনো generation লাগে না। এটাই এই লেসনের concept অংশের কেন্দ্রীয় পার্থক্য — সাধারণ ফাইলে “content” আর “আকার” দুটোই disk-এ আগে থেকে বিদ্যমান একটা সত্য; procfs ফাইলে কোনোটাই না, দুটোই read()-এর মুহূর্তে তৈরি।

2

ls -la /proc/48213/fd/3 দেখাচ্ছে 3 -> 'pipe:[884213]'। file-descriptors লেসনের তিন-স্তরের ছবি (fd table → open file table → inode table) মনে করে বলুন — এই একটা লাইন থেকে ঠিক কোন কোন তথ্য পাওয়া যায়, আর কোন কোন তথ্য এই symlink থেকে পাওয়া যায় না?

প্রয়োগ

যা পাওয়া যায়: এই symlink নিজেই fd table স্তরের একটা entry — process ৪৮২১৩-এর fd নম্বর ৩ একটা pipe object-কে নির্দেশ করছে। বন্ধনীর ভেতরের 884213 সংখ্যাটা সেই pipe-এর inode নম্বর — file-descriptors লেসনের তৃতীয় (inode) স্তরের identity। দুইটা ভিন্ন process-এর fd টেবিলে যদি একই pipe:[884213] দেখা যায়, তার মানে তারা একই pipe-এর দুই প্রান্তে বসে আছে (বা dup()-করা একই প্রান্তে) — এই একটা সংখ্যা মিলিয়েই সেটা নিশ্চিত করা যায়।

যা পাওয়া যায় না: এই symlink বলে না —

  • এই fd দিয়ে পড়া হচ্ছে না লেখা হচ্ছে (read end না write end) — সেটার জন্য /proc/48213/fdinfo/3 খুলতে হবে, যেখানে flags field-এ O_RDONLY/O_WRONLY দেখা যাবে।
  • pipe-টার ভেতরে কত বাইট buffered আছে — সেটাও fdinfo-তে (ino লাইনের পাশে, বা F_GETPIPE_SZ-এর মতো আলাদা তথ্য), symlink টার্গেটে না।
  • pipe-এর অন্য প্রান্ত কোন process-এর কাছে — শুধু inode নম্বর মিলিয়ে সিস্টেমের সব process-এর fd table স্ক্যান করলেই সেটা বের করা সম্ভব (lsof ঠিক এটাই করে)।

মূল কথা — মধ্যম স্তরের (open file table) নিজস্ব state, যেমন offset বা flag, একটা সাধারণ ফাইলের জন্য অর্থবহ (offset আছে), কিন্তু pipe-এর কোনো offset ধারণাই নেই (pipe-এ read মানেই সেই বাইট চিরতরে চলে যাওয়া, কোনো “কোথায় আছি” নেই) — তাই সেই তথ্য fd/-এর symlink-এ প্রকাশ করার কিছু নেই, fdinfo/-এ যা আছে তাও pipe-specific ভিন্ন ফরম্যাটে।

3

sysfs-এর নিয়ম “one value per file”, অথচ /sys/fs/cgroup/mygroup/cpu.max ফাইলে একসাথে দুইটা সংখ্যা থাকে (50000 100000)। এটা কি নিয়মের ব্যতিক্রম, নাকি নিয়মটাই ভুল বোঝা হচ্ছে? যুক্তি দিন।

যুক্তি

নিয়মের ব্যতিক্রম না — নিয়মটা “একটা logical value” বোঝায়, “একটা সংখ্যা” না। cpu.max-এর দুইটা সংখ্যা মিলে একটাই ধারণা প্রকাশ করে — একটা অনুপাত (quota/period), যেটাকে আলাদা করে দুইটা ফাইলে রাখলে বরং বিভ্রান্তিকর হতো: cpu.max.quota আর cpu.max.period দুইটা আলাদা ফাইলে থাকলে, একটা লেখার সময় আরেকটা পুরনো থেকে যাওয়ার সম্ভাবনা তৈরি হতো — একটা inconsistent intermediate state (quota আপডেট হয়ে গেছে, period এখনো পুরনো), যেটা race-conditions লেসনের সমস্যারই আরেকটা রূপ।

একটা মাত্র ফাইলে write("50000 100000") একটা atomic write() কল, তাই kernel একসাথেই দুইটা মান আপডেট করে, কখনো অর্ধেক-আপডেট অবস্থা বাইরে থেকে দেখা যায় না। sysfs-এর আসল নিয়ম তাই ঠিক এভাবে বলা ভালো: “একটা ফাইলে ঠিক একটা atomic, স্বয়ংসম্পূর্ণ semantic unit থাকবে” — যেটা প্রায়ই একটা সংখ্যা, কিন্তু যখন দুইটা সংখ্যা একসাথে না বদলালে অর্থহীন হয়ে যায়, তখন সেই জোড়াটাই “একটা মান”। তুলনা করুন memory.max/memory.current-এর সাথে — এই দুইটা সত্যিই স্বাধীন তথ্য (একটা সীমা, একটা পরিমাপ), তাই আলাদা ফাইলে থাকাই যুক্তিসঙ্গত।

4

আপনাকে একটা লাইটওয়েট মনিটরিং এজেন্ট লিখতে হবে যেটা প্রতি সেকেন্ডে সিস্টেমের প্রতিটা process-এর CPU% আর RSS রিপোর্ট করবে। /proc-ভিত্তিক ডিজাইনে কী কী সমস্যা এড়াতে হবে, আর কীভাবে?

ডিজাইন

এই লেসনের misconception অংশের তিনটা সমস্যাই এখানে সরাসরি প্রাসঙ্গিক — একটা বাস্তব ডিজাইন সিদ্ধান্তে রূপান্তর করা যাক।

সমস্যা ১ — CPU% এক-মুহূর্তের utime+stime থেকে বের করা যায় না, এটা একটা হার। সমাধান: প্রতিটা PID-এর জন্য আগের রিডিং সংরক্ষণ করুন (একটা dict-এ pid → (ticks, timestamp)), প্রতি সেকেন্ডে নতুন রিডিং নিয়ে Δticks / Δtime বের করুন, tick-কে SC_CLK_TCK দিয়ে সেকেন্ডে রূপান্তর করে percentage বানান। প্রথম রিডিং-এ কোনো ডেল্টা নেই, তাই প্রথম সেকেন্ডে সেই PID-এর CPU% স্কিপ করুন বা দেখান।

সমস্যা ২ — process মাঝপথে মরে যাওয়া (TOCTOU race)। os.listdir('/proc') আর প্রতিটা PID-এর ফাইল খোলার মাঝে সময় গ্যাপ আছে — সেই সময়ে process exit করতে পারে। প্রতিটা open()/read()-কে try/except (FileNotFoundError, ProcessLookupError)-এ মুড়ে সেই PID চুপচাপ স্কিপ করুন — এটা bug না, এটা প্রত্যাশিত আচরণ। এমনকি একটা নতুন PID পুরনো একটা exit-করা PID-এর সংখ্যা পুনর্ব্যবহার করতে পারে (PID পুনর্ব্যবহার, fork-exec-wait লেসনের বিষয়) — তাই দুই রিডিং-এর মাঝে PID মিললেও, starttime field (stat-এর ২২ নম্বর) তুলনা করে নিশ্চিত করুন এটা সত্যিই একই process, নতুন কেউ সেই PID দখল করেনি।

সমস্যা ৩ — RSS-এর জন্য শুধু status-এর VmRSS না, stat-এর utime/stime-ও লাগবে — দুইটা আলাদা ফাইল। দুইটা read()-এর মাঝে সামান্য সময়ের ফাঁক আছে, তাই পুরোপুরি একই ন্যানোসেকেন্ডের ছবি না — কিন্তু মনিটরিং-এর প্রয়োজনে (সেকেন্ড-স্কেল রেজোলিউশন) এটা সম্পূর্ণ গ্রহণযোগ্য। যদি সত্যিই কঠোর consistency দরকার হয়, stat ফাইলেই আসলে RSS-এর একটা field আছে (২৪ নম্বর, পেজ-এককে) — একটাই read()-এ দুটো তথ্য পাওয়া সম্ভব, দুইটা ফাইল না খুলে।

সমস্যা ৪ — /proc পুরোটা স্ক্যান করা নিজেই costly, high-frequency-তে। হাজার হাজার process থাকলে প্রতি সেকেন্ডে সবগুলোর stat+status পড়া নিজেই লক্ষণীয় CPU খরচ করে। বাস্তব agent-গুলো (যেমন Prometheus node_exporter) তাই sampling interval নিয়ে সতর্ক থাকে, আর প্রয়োজনে শুধু top-N process-এ ফোকাস করে।

5

থ্রেড গ্রুপে leader PID ৪৮২১৩, আর তিনটা অতিরিক্ত থ্রেড TID ৪৮২১৪, ৪৮২১৫, ৪৮২১৬। cat /proc/48213/task/48215/status চালালে Pid: ফিল্ডে কোন সংখ্যা দেখবেন — ৪৮২১৩ না ৪৮২১৫? আর Tgid: ফিল্ডে কোনটা? threads লেসনের task_struct.pid ব্যাখ্যার সাথে মিলিয়ে যুক্তি দিন।

যুক্তি

Pid: 48215, আর Tgid: 48213

threads লেসনের একটা objective ছিল ঠিক এই সূক্ষ্মতা ব্যাখ্যা করা: “কেন getpid() সব thread-এ একই মান দেয় অথচ gettid() আলাদা, আর kernel-এ task_struct.pid আসলে TID।” /proc/<tgid>/task/<tid>/status ফাইলটা সেই নির্দিষ্ট task-এর (thread-এর) task_struct থেকে সরাসরি পড়ে বানানো হয় — আর সেই struct-এর pid field সত্যিই সেই থ্রেডের নিজস্ব TID ধারণ করে, group leader-এর TID না। তাই task/48215/status-এ Pid: মানে “এই নির্দিষ্ট task-এর নিজের identity” — যেটা 48215

Tgid: field আলাদা — এটা সব থ্রেডে অভিন্ন, কারণ এটা group-এর পরিচয় বহন করে, individual task-এর না। এই ফাইলে Tgid: 48213 দেখাবে, ঠিক যেমন 48213, 48214, 48216-এর task/*/status-এও Tgid: 48213 দেখাবে — একটাই thread group, একটাই getpid()-এর ফেরত মান।

এখানেই userspace বনাম kernel-এর নামকরণের একটা সুন্দর অসামঞ্জস্য ধরা পড়ে: userspace getpid() (POSIX-এর দাবি অনুযায়ী “process ID”) আসলে kernel-এর tgid ফেরত দেয়, আর userspace gettid() (“thread ID”) আসলে kernel-এর pid field ফেরত দেয়। glibc-র wrapper এই ম্যাপিং লুকিয়ে রাখে বলে userspace থেকে এটা স্বাভাবিক মনে হয়, কিন্তু /proc/<tgid>/task/<tid>/status পড়লেই kernel-এর ভেতরের আসল নামকরণ সরাসরি দেখা যায় — যা threads লেসনে শুধু বলা হয়েছিল, আজ এই একটা cat কমান্ডে নিজের চোখে যাচাই করা গেল।

এরপর কী

ত্রিশ লেসন পর — Operating Systems শেষ

পিছনে তাকান। এই module শুরু হয়েছিল একটা প্রশ্ন দিয়ে: আমার program একা মনে করে পুরো machine তার — এই বিভ্রম OS কীভাবে বানায়? ত্রিশটা লেসনে আমরা সেই প্রশ্নের উত্তর একটা একটা স্তর করে খুলেছি — আর আজ, সেই পুরো নির্মাণটাকেই সরাসরি চোখে দেখলাম।

শুরু হয়েছিল ভিত্তি দিয়ে — “অপারেটিং সিস্টেম আসলে কী” bare metal থেকে kernel পর্যন্ত, তারপর kernel space বনাম user space-এর সেই দেয়াল, যেটা hardware নিজেই প্রয়োগ করে privilege ring দিয়ে। সেই দেয়ালে একটামাত্র বৈধ দরজা — system call — যেটা user program-কে নিয়ন্ত্রিতভাবে kernel-এ ঢুকতে দেয়। এরপর প্রশ্ন হলো, kernel “আপনার process” বলতে কী বোঝে — process-এর শরীর (task_struct, address space, state machine), আর সেই process কীভাবে জন্মায়-রূপ বদলায়-মরে (fork, exec, wait)। Threads দেখাল একই address space-এর ভেতরে একাধিক execution context কীভাবে সম্ভব, আর context switching সেই execution বদলানোর প্রকৃত খরচ সংখ্যায় মাপল। তারপর প্রশ্ন উঠল কে আগে চলবে — CPU scheduling-এর সাধারণ তত্ত্ব (FIFO, RR, MLFQ) থেকে Linux-এর বাস্তব scheduler (MLFQ থেকে CFS হয়ে EEVDF) পর্যন্ত।

এরপর মেমরির জগত। Virtual memory ও address translation প্রতিটা process-কে তার নিজস্ব, বিচ্ছিন্ন ঠিকানার বিভ্রম দিল — সেই বিভ্রমের driving question-এর সবচেয়ে সরাসরি বাস্তবায়ন। Paging আর page table সেই অনুবাদের যন্ত্র, TLB তার খরচ থেকে বাঁচার cache। Page fault ও demand paging দেখাল memory allocate করা আর memory ব্যবহার করা দুইটা আলাদা ঘটনা — আজকের experiment-এ যা নিজের চোখে VmRSS বাড়তে দেখে যাচাই করা হলো। Memory allocation (brk, mmap, malloc-এর ভেতরটা) আর stack বনাম heap memory-র এই তত্ত্বকে userspace-এর বাস্তব ব্যবহারে নামাল।

তারপর ফাইলের জগত। File descriptors সেই তিন-স্তরের indirection শেখাল যা আজ /proc/<pid>/fd/-এ আক্ষরিক symlink হিসেবে দেখলাম। Filesystem ও inode, ext4 আর journaling দেখাল ডেটা ডিস্কে কীভাবে টিকে থাকে আর ক্র্যাশ থেকে বাঁচে, আর VFS layer দেখাল কীভাবে একটাই read() ext4, NFS, tmpfs, আর — আজকের লেসনের ভিত্তি — procfs-এ সমানভাবে কাজ করে। I/O model (blocking থেকে epoll, C10K সমস্যা) আর io_uring দেখাল কীভাবে সেই I/O দ্রুত ও efficient করা যায়, আর interrupt ও device driver দেখাল hardware কীভাবে kernel-কে জানায় কাজ শেষ হয়েছে — আজ যার counter আমরা /proc/interrupts-এ আবার দেখলাম।

সমান্তরালতার জগত। Pipe ও FIFO, shared memory ও message queue-এর মতো IPC প্রক্রিয়াগুলোর মধ্যে যোগাযোগের পথ খুলল, signal asynchronous notification-এর প্রক্রিয়া দেখাল। Synchronization primitive (mutex, semaphore, condition variable) সেই যোগাযোগকে নিরাপদ করার হাতিয়ার দিল, আর race condition ও deadlock দেখাল কেন এই নিরাপত্তা এত জরুরি। Futex নামল সবচেয়ে নিচের স্তরে — কীভাবে একটা mutex-এর মতো high-level primitive আসলে বাস্তবায়িত হয় একটামাত্র syscall দিয়ে, শুধু contention হলে।

আর সবশেষে দুইটা লেসন যেগুলো একসাথে পুরো module-এর driving question-এর সবচেয়ে সরাসরি প্রকৌশলগত উত্তর: namespaces ও cgroups দেখাল কীভাবে আজকের container-জগৎ isolation (namespace — “কী দেখতে পাও”) আর resource limit (cgroup — “কতটুকু ব্যবহার করতে পারো”) মিলিয়ে সেই বিভ্রমটাকে industrial scale-এ বাস্তবায়ন করে। আর আজ, /proc ও /sys দেখাল সেই পুরো নির্মাণটাকে — process, thread, memory, fd, namespace, cgroup, সবকিছুকে — বাইরে থেকে, লাইভ, একটা সাধারণ read() দিয়ে পরিদর্শন করা যায়, কোনো বিশেষ API ছাড়াই।

একটা মাত্র বাক্যে পুরো মডিউল: OS একটামাত্র illusion তৈরি করে না — এটা একটা layered illusion-এর স্তূপ, প্রতিটা স্তর আগেরটার উপর দাঁড়িয়ে (scheduling illusion “তুমি একাই চলছ” পাওয়ার আগে virtual memory-র illusion “তোমার নিজস্ব address space আছে” দরকার, আর সেই দুটোর আগে system call-এর illusion “hardware সরাসরি ছুঁতে পারছ” দরকার) — আর /proc//sys হলো সেই illusion-এর পেছনের সত্যিকারের যন্ত্রপাতি দেখার একটা জানালা, যেটা kernel নিজেই স্বেচ্ছায় খুলে রেখেছে।

এরপর কী — Programming Languages

কিন্তু একটা প্রশ্ন এই পুরো module জুড়ে ইচ্ছাকৃতভাবে এড়ানো হয়েছে। আমরা বারবার C কোড লিখেছি — malloc, free, fork, pthread_create — আর প্রতিবার ধরে নিয়েছি এই ভাষাটাই “স্বাভাবিক” পছন্দ। কিন্তু কেন C? malloc/free-এর বদলে যদি garbage collector থাকত? pthread_create-এর বদলে যদি async/await থাকত? এই সিদ্ধান্তগুলো কোথা থেকে আসে, আর কতটা স্বেচ্ছাধীন?

Python আর Rust — দুটোই “programming language”, কিন্তু নিচে পার্থক্যটা ঠিক কোথায়?

এটাই পরের module, Programming Languages-এর driving question। লক্ষ্য করুন — এই module-এর প্রয়োজনীয়তা তালিকায় দুটো module আছে: operating-systems (যা আজ শেষ হলো) এবং algorithms (Data Structures & Algorithms)। এটা assembly→operating-systems bridge-এর মতো ঠিক প্রতিসম না — সেখানে দুইটা prerequisite-ই ততক্ষণে সম্পূর্ণ ছিল, এখানে সৎভাবেই বলতে হয়: algorithms module এখনো সামনে পড়ে আছে। কেন এটা লাগবে? কারণ একটা garbage collector-এর mark-sweep algorithm একটা graph traversal (BFS/DFS-এর সরাসরি প্রয়োগ), একটা language-এর symbol table প্রায়ই একটা hash table, আর type inference-এর efficient বাস্তবায়ন union-find-এর মতো data structure ব্যবহার করে — programming-languages module এই ভিত্তিগুলোকে ধরে নিয়েই এগোবে। তাই এই দুই module মিলেই — একটা “hardware কীভাবে কাজ করে” থেকে, আরেকটা “efficient computation কীভাবে সংগঠিত করতে হয়” থেকে — সেই ভিত্তি তৈরি করবে যার উপর একটা programming language-এর ভেতরটা বোঝা সম্ভব।

পরের module ঠিক আমাদের এই module-এর জমা করা প্রশ্নগুলো দিয়েই শুরু হবে:

  • “Syntax vs semantics” — আমরা C-তে {0} বনাম {1} লিখে দেখেছি একই-দেখতে syntax সম্পূর্ণ ভিন্ন memory layout তৈরি করে (.bss বনাম .data, process-anatomy লেসন) — semantics-ই আসল গল্প, syntax শুধু তার একটা প্রকাশ।
  • “Memory models — manual, RAII, ownership, GC” — এই module-এর malloc/free, stack-এর automatic cleanup, আর memory-allocation লেসনের সেই free() করার পরেও RSS না-কমার bug — এই সবকিছুই আসলে “manual” memory model-এর একটা কেস স্টাডি ছিল। এখন প্রশ্ন: বাকি মডেলগুলো (RAII, ownership, GC) কীভাবে এই একই সমস্যা ভিন্নভাবে সমাধান করে?
  • “Garbage collection — mark-sweep, copying, generational, tri-color” — মনে করুন synchronization-primitives লেসনের mutex আর race-conditions লেসনের সমস্যা — একটা concurrent GC-কে ঠিক এই একই সমস্যাগুলো সামলাতে হয়, allocator-এর সাথে race না করে।
  • “Concurrency models — threads, async/await, actors, CSP” — threads লেসনের raw clone(), synchronization-primitives-এর mutex/ condition variable — এগুলো OS-স্তরের প্রাইমিটিভ। async/await language-স্তরে এই একই সমস্যার একটা সম্পূর্ণ ভিন্ন সমাধান — কিন্তু নিচে নামলে io_uring/epoll লেসনের সেই একই non-blocking I/O-তেই গিয়ে ঠেকে।

fork() যেভাবে দুইবার return করে, বা mmap() যেভাবে virtual memory আর physical memory-র মাঝে একটা বিভ্রম তৈরি করে — এই module জুড়ে আমরা দেখেছি OS প্রতিটা abstraction-এর নিচে ঠিক কী আছে তা উন্মোচন করা সম্ভব। পরের module একই কাজ করবে, শুধু এক স্তর উপরে — একটা for loop, একটা class, একটা try/except block-এর নিচে ঠিক কী আছে, সেটাই এখন প্রশ্ন।

আরও পড়ুন

  • proc(5) — Linux manual page — Michael Kerrisk, man-pages project · প্রতিটা /proc/pid ফাইলের ফিল্ড-বাই-ফিল্ড প্রামাণ্য সংজ্ঞা — এই লেসনের হুড অংশের প্রাথমিক উৎস
  • The sysfs Filesystem — Linux kernel documentation · "one value per file" নিয়মের প্রামাণ্য উৎস — কেন sysfs procfs-এর অগোছালো অভ্যাসের বিপরীতে সচেতনভাবে ডিজাইন করা হয়েছিল
  • Control Group v2 — Linux kernel documentation · memory.max, cpu.max, io.max-সহ প্রতিটা cgroup control file-এর প্রামাণ্য সংজ্ঞা — namespaces-and-cgroups লেসনের সরাসরি continuation
  • The /proc Filesystem — Linux kernel documentation · procfs-এর ইতিহাস ও গঠন নিয়ে kernel maintainer-দের নিজেদের লেখা, একদম প্রথম প্যারাগ্রাফেই এর ঐতিহাসিক অসঙ্গতি স্বীকার করে
  • The Linux Programming Interface — Michael Kerrisk · প্রসেস, thread, memory-map, ও file descriptor সংক্রান্ত /proc ফাইলগুলোর প্রেক্ষাপট — এই module-এর বাকি সব লেসনের মতোই, প্রতিটা metadata-সংক্রান্ত অধ্যায়ে ছড়ানো
  • procps-ng source code — ps, top, free, vmstat · এই লেসনের "ps জাদু না" দাবির সরাসরি প্রমাণ — proc/readproc.c ফাইলে গিয়ে দেখুন, /proc/pid/stat আর /proc/pid/status পার্স করা ছাড়া আর কিছুই নেই