Course-এর সূচি

3.1

Process ও Thread

একটা process-এর নিজের কী থাকে, thread-রা কী ভাগ করে, আর প্রতিটা বানাতে কত খরচ।

55 মিনিট পড়া + Animation

টিপস: ডটেড-আন্ডারলাইন করা যেকোনো শব্দে ট্যাপ বা Hover করলে তার অর্থ দেখবে।

এই Lesson শেষে তুমি পারবে

  • বোঝাতে, Operating System আর তার Kernel কী করে, Program কেন User Mode-এ চলে, আর একটা System Call-এর দাম কত।
  • ঠিক করে বলতে, একটা Process-এর নিজের কী কী থাকে (Address Space, PID, File Descriptor, Security Context, Thread), আর Kernel Process Control Block-এ সেগুলোর হিসাব কীভাবে রাখে।
  • Thread-রা কী ভাগ করে আর প্রত্যেকে নিজের কাছে কী রাখে তা বলতে, আর Stack কেন আলাদা হয়েও সুরক্ষিত না, তা বোঝাতে।
  • fork, exec আর wait বর্ণনা করতে, Linux clone দিয়ে Thread কীভাবে বানায় তা বলতে, প্রতিটার দাম বলতে, আর Thread-ওয়ালা Program-এ fork কেন বিপজ্জনক, তা বোঝাতে।
  • Pipe থেকে Shared Memory থেকে TCP পর্যন্ত, Inter-Process Communication-এর ঠিক পদ্ধতিটা বাছতে, আর প্রতিটা কীভাবে ভাঙে তা বলতে।
  • ps-এ Process-এর অবস্থা পড়তে, আর Zombie, Orphan, D অবস্থায় আটকে থাকা Process আর Signal না-শোনা Container ধরতে।
  • SIGTERM দিয়ে Graceful Shutdown লিখতে, আর ১৩৭, ১৩৯, ১৪৩-এর মতো Exit Code পড়তে।
  • একটা Crash কতদূর ছড়াবে আগে থেকে বলতে, আর PostgreSQL, MySQL, Nginx, Chrome-এর মতো Multi-process ও Multi-thread Server তুলনা করতে।

#এই Lesson কোথায় বসে

Lesson ১.২-এ Server-কে দেখেছি লম্বা সময় ধরে চলা একটা Process হিসেবে, প্রতি Connection-এ Process, প্রতি Connection-এ Thread আর Event Loop, এই Design-গুলো কয়েক লাইনে করে তুলনা করেছি, আর Rolling Deploy ধাপে ধাপে দেখেছি। Lesson ১.১৩-এ দেখেছি Apache-র MPM, Nginx-এর Worker, PHP-FPM আর Gunicorn এই Design-গুলো কীভাবে ব্যবহার করে। দুটো Lesson-এই “Process” আর “Thread” শব্দ দুটো চলেছে, ভেতরটা না খুলেই। এই Lesson-এ আমরা ভেতরটা খুলব।

System Design এই জিনিসগুলোর ওপর যতটা নির্ভর করে, প্রথমে ততটা মনে হয় না। Backend Server, Database, Web Server, Network Programming আর High-concurrency Application, সবই এগুলো দিয়ে তৈরি, আর তাদের বেশিরভাগ সীমা আর ব্যর্থতা আসলে Operating System-এরই সীমা আর ব্যর্থতা। নোটে এই Chapter-এর নয়টা বিষয় আছে, আমরা সেগুলো পড়ব ছয়টা Lesson-এ:

বিষয়Lesson
Process বনাম Thread, Thread৩.১, এই Lesson
Concurrency, Context Switching৩.২
Thread Pool, Deadlock৩.৩
Virtual Memory৩.৪
I/O Model, epoll৩.৫
Event Loop৩.৬

আমরা থাকব Operating System-এর দিক থেকে। Scheduling আর Switching হলো Lesson ৩.২-এর বিষয়, আর Code-এ Data Race, Lock আর Memory Model আছে Chapter 14-এ।

#Operating System কী করে

একটা মেশিনে অনেক Program চলে, অথচ Disk আর Network Card একই। কাউকে তো এগুলো ন্যায্যভাবে ভাগ করে দিতে হবে, এক Program যেন আরেকটার ক্ষতি করতে না পারে সেটা দেখতে হবে, আর Hardware-এর ভিন্নতা আড়াল করতে হবে। এই কাজটাই Operating System-এর। এর কেন্দ্রে আছে Kernel: সেই একটা Program, যা মেশিন যতক্ষণ চালু ততক্ষণ Memory-তে থাকে, আর Hardware-এর পুরো নিয়ন্ত্রণ যার হাতে। OS-এর বাকিটা (Shell, Library, ls-এর মতো Tool) সাধারণ Program।

Kernel-এর কাজতোমার জন্য এর মানেLesson
অল্প Core-এ অনেক Program চালানোScheduling, Run Queue, Context Switch৩.২
প্রতিটা Program-কে নিজের Memory দেওয়াVirtual Address, Page Table, OOM Killer৩.৪
Disk-কে File হিসেবে দেখানোFile System, Page Cache, File Descriptor৩.৪, ৩.৫
Network-এর Data সরানোTCP/IP Stack, Socket, epoll১.৯, ৩.৫
Hardware চালানোDevice Driver, Interrupt
কে কী করতে পারবে তা ঠিক করাUser, Permission, Namespace, Cgroup৩.১, Chapter 4

#User Mode আর Kernel Mode

এই ভাগটা কোনো ভদ্রতার চুক্তি না, CPU নিজে এটা জোর করে মানায়। আজকের প্রতিটা CPU-তে অন্তত দুটো ক্ষমতার স্তর থাকে:

  • Kernel Mode (x86-এ Ring 0, ARM-এ EL1): যেকোনো Instruction, যেকোনো Memory, সব Hardware।
  • User Mode (x86-এ Ring 3, ARM-এ EL0): শুধু সাধারণ হিসাব, শুধু Program-এর নিজের Memory-তে।

User Mode-এ কোনো Program Device ছুঁতে, নিজের Memory-র ম্যাপ বদলাতে বা CPU থামাতে গেলে Hardware মানা করে দেয়, নিয়ন্ত্রণ যায় Kernel-এর হাতে, আর সাধারণত Program-টা শেষ। তাই Bug-ওয়ালা Program শুধু নিজেরই ক্ষতি করতে পারে। Chapter 4-এর Hypervisor এই সীমারেখাটাকেই আরও বাড়ায়; আর Container এটা ভাগ করে নেয়: একটা Host-এর সব Container একই Kernel ব্যবহার করে।

#System Call

Program যদি Disk বা Network ছুঁতেই না পারে, তাহলে কাজের কিছু করে কীভাবে? সে চায়। System Call হলো Kernel-এ ঢোকার নিয়ন্ত্রিত পথ: Program একটা Call নম্বর আর Argument Register-এ রেখে বিশেষ একটা Instruction চালায় (x86-64-এ syscall), CPU চলে যায় Kernel Mode-এ আর লাফ দেয় একটা নির্দিষ্ট জায়গায়, Kernel সব যাচাই করে কাজটা করে, তারপর ফেরে User Mode-এ। Linux-এ কয়েকশো System Call আছে: open, read, write, socket, accept, fork, mmap ইত্যাদি। তোমার ভাষার File.read বা fetch শেষমেশ এখানেই পৌঁছায়, সাধারণত C Library হয়ে।

System Call: Kernel-এর ভেতরে ঢোকা

তোমার Program চলে User Mode-এ, Hardware ছোঁয়ার ক্ষমতা তার নেই। বাইরের দুনিয়ার যা কিছু দরকার, Kernel-এর কাছে চাইতে হয়। ধাপে ধাপে দেখো একটা দ্রুত Read, অপেক্ষা করতে হয় এমন একটা Read, আর Kernel যে দুটো অনুরোধ ফিরিয়ে দেয়।

পরিস্থিতি

Data আগে থেকেই Kernel-এর Memory-তে আছে, তাই Disk-এর কোনো কাজ ছাড়াই Call ফিরে আসে।

তোমার ProgramUser Mode
KernelKernel Mode
DiskHardware
একটা File-এর ৪ KB চাই
ধাপ 1 / 5
1

File-টা File Descriptor ৩ হিসেবে খোলা আছে। Disk-এর সাথে কথা বলার কোনো Instruction Program-এর হাতে নেই, থাকলেও চালানোর অনুমতি পেত না।

দাম কেন গুরুত্বপূর্ণ। একটা Function Call-এ লাগে প্রায় এক ন্যানোসেকেন্ড। সস্তা একটা System Call-এ মোটামুটি ১০০ ns থেকে ১ µs, কারণ Mode বদলানো, যাচাই, আর Spectre ও Meltdown-এর পর যোগ হওয়া নিরাপত্তার ব্যবস্থা। এটা দ্রুতই, কিন্তু সেকেন্ডে লাখ লাখ Call হলে এটাই সময় খেয়ে ফেলে, আর এটা থেকেই Systems Design-এর অনেক কিছু বোঝা যায়:

  • Program Buffer করে: ১ KB করে ৬৪ বার না পড়ে একবারে ৬৪ KB, আর Logging Library একসাথে অনেক লাইন লেখে।
  • clock_gettime-এর মতো প্রচলিত শুধু-পড়ার Call vDSO দিয়ে User Mode-এই হয়ে যায়, Kernel-এ ঢুকতেই হয় না।
  • High-performance I/O হয় Call একসাথে করে, নয়তো এড়িয়েই যায়: epoll_wait একবারে অনেক Event ফেরত দেয়, আর আছে io_uring (Lesson ৩.৫)।

#Process কী

Process হলো একটা Program-এর চলমান রূপ। Program হলো Disk-এ রাখা একটা File, একটা রেসিপি। Process হলো সেই রেসিপি অনুযায়ী রান্না চলছে: Memory ব্যবহার হচ্ছে, File খোলা আছে, Code-এর কোথাও একটা জায়গায় আছে। তুমি Chrome চালু করলে OS তার জন্য এক বা একাধিক Process বানায়; python দুবার চালালে একই Program-এর দুটো Process।

Operating System
      |
      +---- Chrome process(es)
      |
      +---- VS Code process
      |
      +---- MySQL process
      |
      +---- Python process

প্রতিটা Process-এর নিজের যা থাকে:

যা আছেজিনিসটা কীকেন তোমার জানা দরকার
Virtual Address SpaceMemory-র দিকে তার নিজের দৃষ্টি: Code, Global Data, Heap, StackIsolation। একই Address দুই Process-এ আলাদা Memory (Lesson ৩.৪)
Process ID (PID)তার নম্বর, আর তার Parent-এর নম্বর (PPID)kill, ps, Log, Metric
Memoryআসলে ব্যবহার হচ্ছে যে Page-গুলো (Resident Memory)Capacity Planning, OOM Killer
খোলা File Descriptor (FD)ছোট ছোট নম্বরের একটা টেবিল, যা খোলা File, Socket, Pipe-এর নাম। ০, ১, ২ হলো Standard Input, Output, Errorপ্রতিটা Connection একটা করে খায়; Process প্রতি সীমাটা (ulimit -n, Lesson ১.২) আসল দেয়াল
Security ContextUID, GID, Capability, আর Container-এ Namespace ও CgroupHack হওয়া Process কতটা ক্ষতি করতে পারে
এক বা একাধিক Threadযে অংশগুলো আসলে CPU-তে চলেParallelism, আর পরের পুরো Section

এছাড়াও থাকে Current Directory, Environment Variable, Signal Handler আর Resource Limit। Linux-এ এ সবই /proc/<pid>/-এ দেখা যায়, “নিজে করে দেখো” অংশে দেখবে।

প্রতিটা Process-এর জন্য Kernel নিজের একটা খাতা রাখে, যাকে পাঠ্যবইয়ে বলে Process Control Block (PCB): অবস্থা, PID, Parent, না চলার সময়ের জন্য Save করা Register, Memory-র ম্যাপ, File-এর টেবিল, হিসাবপত্র। Linux-এ এই Structure-এর নাম task_struct, আর সেটা থাকে প্রতিটা Thread-এর জন্য একটা করে; একই Process-এর Thread-রা Field-গুলো ভাগ করে নেয়।

একটা সাধারণ Address Space, নিচু Address থেকে উঁচুর দিকে:

 low   +--------------------+
       | Code (read-only)   |  the program's machine code, shareable between processes
       | Global data        |  global and static variables
       | Heap        ↓ grows|  malloc, new, every object
       |                    |
       | Shared libraries,  |
       | memory maps        |
       |                    |
       | Stack       ↑ grows|  function calls and local variables
 high  +--------------------+
       | Kernel             |  reserved for the kernel, no access from user mode
       +--------------------+

#Process-দের মাঝের দেয়াল

এক Process আরেক Process-এর Memory পড়তে বা লিখতে পারে না, কারণ সেখানে যাওয়ার মতো কোনো Address তার নেই। প্রতিটা Address যায় সেই Process-এর নিজের Page Table দিয়ে, তাই অন্য Process-এ একই সংখ্যা মানে আলাদা আসল Byte; আর ম্যাপ না থাকলে সাথে সাথে Fault। এই দেয়াল তোমাকে একসাথে তিনটা জিনিস দেয়: Fault Isolation (Crash ভেতরেই থাকে), Security (এক User-এর Process আরেকজনের গোপন তথ্য পড়তে পারে না), আর আলাদা Resource (Limit, Restart, Upgrade)।

দেয়ালে কিছু ইচ্ছাকৃত দরজাও আছে। দুটো Process রাজি হলে Shared Memory ম্যাপ করতে পারে, আর ঠিক অনুমতি থাকলে Debugger ptrace ব্যবহার করতে পারে। নোটে আছে একটা Process অন্যটার Memory-তে “সরাসরি প্রবেশ করতে পারে না”, কথাটা ডিফল্ট হিসেবে ঠিক, তবে শেষ কথা না।

#Thread কী

Thread হলো Process-এর ভেতরে Execution-এর একটা আলাদা ধারা। প্রতিটা Process-এ অন্তত একটা থাকে, অনেকগুলোও থাকতে পারে। CPU-তে চলে শুধু Thread: Lesson ৩.২-এর Scheduler Thread বাছে, Process না।

             A program with three threads
                    |
          +---------+---------+
          |         |         |
          v         v         v
       Thread 1  Thread 2  Thread 3
       UI        Network   Rendering

এই Chapter-এ মুখস্থ রাখার মতো সবচেয়ে কাজের জিনিস এই ভাগটা:

Process-এর সব Thread যা ভাগ করেপ্রতিটা Thread-এর নিজের
Heap MemoryStack (Function Call, Local Variable)
Global VariableProgram Counter (পরের Instruction)
Code SegmentCPU Register
খোলা File DescriptorThread ID (TID)
Signal Handler, Current Directory, UIDSignal Mask, Thread-local Storage, errno

তার মানে সব Thread একই Object দেখে, একই Socket ব্যবহার করতে পারে, একই Code চালায়; কিন্তু সেই Code-এর কোথায় আছে, আর Local Variable, এগুলো প্রত্যেকের আলাদা। তাই দুটো Thread একই Function একসাথে চালালেও তাদের Local Variable ধাক্কা খায় না।

Process-এর ভেতরে: তার নিজের কী, Thread-রা কী ভাগ করে

Process হলো Resource-এর একটা বাক্স। Thread হলো সেই বাক্সের ভেতরে চলা Execution-এর একটা ধারা। নিরেট বাক্সগুলো Process-এর সব Thread মিলে ভাগ করে; প্রতিটা Dashed কলাম একটা Thread-এর নিজের।

পরিস্থিতি

Program একবার চালু করো, আর Kernel তার জন্য যা যা সাজিয়েছে, একে একে দেখো।

shop-apiPID 4121
Code
shop-api Program
Global
version = 7
Heap
খালি
খোলা FD
0 1 2 · 7 8
Security
UID 1000
Thread (প্রত্যেকের নিজের)
main
Stack: main()
Kernel PID 4121 বানাল
ধাপ 1 / 7
1

Program চালালেই একটা Process তৈরি হয়: তার একটা চলমান রূপ। একই Program দুবার চালালে দুটো Process, দুটো PID। Disk-এ থাকা Program File শুধু রেসিপি।

তিনটা জিনিসে মানুষ ধরা খায়:

১. সব Memory কার্যত ভাগের না, আর সবটা নিরাপদও না। Stack-এর Local Variable কার্যত ব্যক্তিগত, আর Register সত্যিই ব্যক্তিগত। কিন্তু সব Stack একই Address Space-এ থাকে, তাই এক Thread-এর ভুল Pointer আরেকজনের Stack মুছে লিখতে পারে। আলাদা, হ্যাঁ; সুরক্ষিত, না।

২. File Descriptor ভাগের। এক Thread একটা Socket বন্ধ করলে সবার জন্যই বন্ধ। দুবার Close করলে সেটা গিয়ে লাগতে পারে এমন Descriptor-এ, যা আরেক Thread এইমাত্র একই নম্বরে পেয়েছে; এই Bug খুঁজে পাওয়া ভয়াবহ কঠিন।

৩. ভাগাভাগিতে কথা বলা সহজ, ঠিক রাখা কঠিন। Thread-রা শুধু Memory-তে লিখেই কথা বলে, System Call লাগে না। তবে সমন্বয় ছাড়া দুজন একই Data-য় লিখলে হয় Race; এখানে শুধু নামটা জানলে, শিখবে Chapter 14-এ।

#Kernel Thread আর User-level Thread

এই Lesson-এর Thread হলো Kernel Thread: Kernel প্রত্যেকটাকে চেনে আর Schedule করে, একের বদলে এক। Linux-এ pthread_create আর Java-র Platform Thread এটাই দেয়। Goroutine, Java Virtual Thread আর অন্য Green Thread আরও হালকা: ভাষার Runtime অনেকগুলোকে অল্প কয়েকটা Kernel Thread-এ চালায়। এগুলো আছে Lesson ৩.৬-এ, কারণ কাজ করতে এদের Event Loop-এর যন্ত্রপাতি লাগে।

#Process বনাম Thread

বিষয়ProcessThread
অর্থচলমান Programতার ভেতরে Execution-এর ধারা
Memoryসাধারণত আলাদা Address SpaceProcess-এর Memory ভাগ করে, Stack নিজের
Resourceতুলনামূলক বেশিতুলনামূলক কম
তৈরি করাতুলনামূলক দামিতুলনামূলক হালকা
যোগাযোগIPC লাগেসরাসরি, Shared Memory দিয়ে
Failure Isolationশক্তদুর্বল: একই Process-এর Thread একে অন্যকে প্রভাবিত করতে পারে
Context Switchসাধারণত বেশি দামি (Address Space বদলায়)একই Process-এর ভেতরে সাধারণত কম দামি (Lesson ৩.২)
Security-র সীমানাহ্যাঁনা
মেশিন পেরিয়ে ছড়ানোহ্যাঁ, Network IPC দিয়েনা
যে সীমা দেখতে হবেProcess প্রতি Memory, Shared Store-এ ConnectionStack Memory, pids.max, Lock Contention

#Process আর Thread তৈরি

#fork, exec আর wait

Unix “একটা Program চালাও” কাজটাকে দুই ধাপে ভাগ করে, আর এই ভাগটাই অসাধারণ শক্তিশালী।

  1. fork: আমার কপি বানাও

    fork() এমন একটা Child বানায়, যা Parent-এর প্রায় হুবহু কপি। এটা দুবার ফেরে: Child-এ ০, আর Parent-এ Child-এর PID। এই সময়ে Memory আসলে কপি হয় না: Parent আর Child Copy-on-Write দিয়ে Page ভাগ করে, আর কেউ লিখলে তবেই সেই Page কপি হয়। কৌশলটা আছে Lesson ৩.৪-এ।

  2. exec: ওই Program হয়ে যাও

    execve() বর্তমান Memory-র জায়গায় নতুন Program বসায়। PID, Parent আর খোলা File Descriptor থেকে যায়, শুধু close-on-exec দাগানোগুলো বাদে। fork আর exec-এর মাঝে Child সব গুছিয়ে নিতে পারে: Shell এভাবেই > Redirection আর Pipe বানায়, যে Program চালাচ্ছে তার কোনো বিশেষ সাহায্য ছাড়াই। সফল হলে exec আর ফেরে না; ফিরলে বুঝবে কিছু একটা ব্যর্থ হয়েছে, আর Child-কে বের হয়ে যেতে হবে।

  3. wait: ফলাফল নাও

    wait() বা waitpid() Parent-কে Child-এর Exit Status দেয়, আর Kernel তার খাতা মুছে ফেলতে পারে। যে Parent এটা কখনো করে না, সে Zombie বানায়; সেই গল্পে একটু পরেই আসছি।

Windows-এ fork নেই; তার CreateProcess সরাসরি নতুন Program চালায়। Windows-এ Process বানানো অনেক ধীর হওয়ার এটা একটা কারণ, আর Windows Server-গুলো এজন্যই Thread-এর দিকে ঝোঁকে। Unix-এ posix_spawn একই রকম এক ধাপের পথ দেয়।

#Linux-এ Thread

Linux-এর Kernel-এ Thread-এর আলাদা কোনো ধারণা নেই। দুটোই Task, বানানো হয় clone() দিয়ে। Flag ঠিক করে কী ভাগ হবে: Memory, File টেবিল আর Signal Handler ভাগ করলে পাও একটা Thread, যার TID নতুন, কিন্তু PID (তার Thread Group) একই। এতে অনেক খুঁটিনাটি পরিষ্কার হয়: Thread-এর Stack দেখা যায় Process-এর Memory Map হিসেবে, আর pids.max-এর মতো সীমায় Process-এর সাথে Thread-ও গোনা হয়।

#কোনটার দাম কত

এগুলো একটা সাধারণ Linux Server-এর আনুমানিক মাপ। ভরসা করার আগে নিজের System-এ মেপে নাও।

কাজমোটামুটি দামকিসের ওপর নির্ভর করে
Function Callপ্রায় ১ ns
System Call১০০ ns থেকে ১ µsCPU, নিরাপত্তার ব্যবস্থা
Thread বানানোকয়েক দশ মাইক্রোসেকেন্ডStack সাজানো
ছোট Process-এ forkকয়েক দশ থেকে কয়েকশো মাইক্রোসেকেন্ডPage Table কপি
কয়েক দশ GB ব্যবহার করা Process-এ forkকয়েক মিলিসেকেন্ড থেকে আরও অনেকMemory-র সাথে Page Table বাড়ে
fork + exec + Runtime লোড (Python, Java)কয়েক দশ মিলিসেকেন্ড থেকে কয়েক সেকেন্ডProgram চালু হওয়া, Kernel না
Thread-এর MemoryLinux glibc-তে ডিফল্ট ৮ MB Address Space, ৬৪-bit JVM-এ প্রায় ১ MB; RAM লাগে শুধু ছোঁয়া Page-এর, প্রায়ই কয়েক দশ KBulimit -s, -Xss
Process-এর Memoryকয়েক MB, কোনো Runtime-এ কয়েকশো MB-ও; ভাগের Code আর Copy-on-Write Page একবারই গোনা হয়ভাষা

এ থেকে Design-এর তিনটা সিদ্ধান্ত আসে:

  • প্রতি Request-এ বানিও না। প্রতি Request-এ একটা Thread বা Process চালু করলে এই দাম বারবার দিতে হয়। Pool দিয়ে আবার ব্যবহার করো (Lesson ৩.৩), বা আগেই Worker Fork করে রাখো (Lesson ১.১৩)।
  • Process যত বড়, fork তত ধীর। Redis Snapshot লিখতে একটা Child Fork করে (BGSAVE), আর বড় Instance-এ শুধু এই Fork-এর জন্যই কয়েক দশ মিলিসেকেন্ড বা তারও বেশি Command চলা থেমে থাকতে পারে। Snapshot চলার সময় Write হলে Page কপি হয়, তাই Redis Server-এর Data-র চেয়ে অনেক বেশি Memory লাগতে পারে।
  • দশ হাজারের ঘরে না যাওয়া পর্যন্ত Thread সস্তা। এক লাখ অলস WebSocket Connection (Lesson ২.৬) মানে এক লাখ Stack আর বিপুল Scheduling। Lesson ৩.৫ আর ৩.৬ এই সমস্যারই সমাধান।

কাজ তৈরি: fork, exec, wait, আর একটা Thread

Unix-এ নতুন Process শুরু হয় চলমান একটার কপি হিসেবে, আর আলাদা একটা Call তাতে নতুন Program বসায়। Thread একই Kernel যন্ত্র ব্যবহার করে, কিন্তু কপি না করে সব ভাগ করে নেয়।

পরিস্থিতি

প্রতিবার Command লিখলে Shell যা করে, এখানে ls।

ParentPID 200
Kernel
নতুন TaskChild বা Thread
fork()
ধাপ 1 / 8
1

Shell নিজের একটা কপি চায়। খালি Process বানানোর উপায় নেই: Unix-এ Process সবসময় অন্য Process থেকে জন্মায়, পেছনে যেতে যেতে একদম PID 1 পর্যন্ত।

#Process-এর অবস্থা

একটা Process বা Thread সবসময় কোনো একটা অবস্থায় থাকে, আর Server-এ সেটা খুব কমই “চলছে”।

অবস্থামানেps stat
Newতৈরি হচ্ছে
Readyচলতে পারে, Core-এর অপেক্ষায়R
Runningএখন Core-এR
Waiting (Interruptible)কোনো Event-এর জন্য ঘুমাচ্ছে: Socket-এর Data, Timer, LockS
Waiting (Uninterruptible)Kernel-এর Disk-এর পথে ঘুমাচ্ছে; Signal-কে অপেক্ষা করতে হবেD
StoppedSIGSTOP বা Debugger থামিয়েছেT
Zombieবের হয়েছে, Status নেওয়া হয়নিZ

পড়ার কিছু কৌশল:

  • সুস্থ অলস Server-এ প্রায় সবই S। Database-এর অপেক্ষায় থাকা Thread-এ CPU খরচ নেই, তাই “Thread-এর সংখ্যা” আর “CPU ব্যবহার” সম্পর্কহীন দেখাতে পারে।
  • অল্প Core-এ অনেক R মানে CPU ভরে গেছে; Load Average আর Latency-তে Queue দেখবে।
  • অনেক D মানে Storage-এ গোলমাল: চেনার চিহ্ন হলো Load Average বেশি, অথচ CPU কম (Lesson ১.২-এ দেখেছ, Linux Load-এ D গোনে)। এমনকি SIGKILL-ও Thread না জাগা পর্যন্ত অপেক্ষা করে।

Process-এর অবস্থা: জীবনটা কোথায় কাটে

বেশিরভাগ সময় Server-এর Thread চলে না। Kernel প্রত্যেককে এই অবস্থাগুলোর মধ্যে সরায়, আর ps-এর stat কলাম বলে দেয় কে এখন কোথায়।

পরিস্থিতি

fork থেকে খাতা মুছে যাওয়া পর্যন্ত।

New
ReadyCore চায়
RunningCore-এ
Zombieবের হয়েছে
Waitingঘুমন্ত
শেষReap হয়েছে
ধাপ 1 / 5
1

fork এইমাত্র Task বানাল। Kernel তার খাতা আর Memory-র ম্যাপ সাজাচ্ছে। এখনো চলতে পারে না।

#Zombie আর Orphan

দুটো নাম শুনতে কাছাকাছি, আর Interview-তে প্রায়ই গুলিয়ে যায়।

Zombie হলো এমন Process, যে বের হয়ে গেছে, কিন্তু Parent এখনো wait ডাকেনি। তার Memory আর File আর নেই। আছে শুধু Exit Status-সহ Kernel-এর একটা খাতা, আর তার PID। প্রতিটা Process-ই এক মুহূর্তের জন্য Zombie হয়, এটা স্বাভাবিক। Bug হয় তখন, যখন Parent কখনো Reap করে না: ps ভরে যায় <defunct>-এ, আর একসময় fork ব্যর্থ হয়, কারণ PID-এর জায়গা (/proc/sys/kernel/pid_max) বা Container-এর pids.max শেষ।

  • Zombie-কে মারা যায় না: সে তো আগেই মৃত। kill -9 কিছুই করে না।
  • ঠিক করতে হয় Parent-কে: SIGCHLD সামলাও আর লুপে waitpid ডাকো, বা Status না লাগলে SIGCHLD Ignore করে দাও। জরুরি অবস্থায় Parent-কে মেরে দাও, তখন init দত্তক নিয়ে Zombie-গুলোকে Reap করে।

Orphan হলো এমন জীবিত Process, যার Parent মারা গেছে। তাকে মারা হয় না। Kernel তাকে PID 1-এর (বা কাছের কোনো Subreaper-এর) কাছে দেয়, আর সে বের হলে এই নতুন Parent তাকে Reap করে। Orphan কখনো ইচ্ছাকৃত (nohup, Daemon), কখনো চমক: Master-এর পরেও বেঁচে থাকা Worker উত্তরাধিকারে পাওয়া Listening Socket ধরে রাখে, তাই Restart করা Service ব্যর্থ হয় address already in use দিয়ে।

Container-এ এটা তোমার ঘাড়ে পড়ে। তোমার Application তার PID Namespace-এ PID 1, তাই ওই Container-এর Orphan-রা যায় তোমার App-এর কাছে, আর সে জানেই না যে তাদের Reap করতে হবে। ছোট্ট একটা init ব্যবহার করো: docker run --init, বা Entrypoint হিসেবে tini।

Zombie আর Orphan

Zombie হলো এমন Child, যে মারা গেছে অথচ Parent তার Exit Status পড়েনি। Orphan হলো এমন জীবিত Child, যার Parent মারা গেছে। সমস্যা আলাদা, সমাধানও আলাদা।

পরিস্থিতি

সুস্থ অবস্থা: মুহূর্তের জন্য Zombie।

ParentPID 300
ChildPID 301Zombie (Z)
initPID 1
exit(0)
ধাপ 1 / 3
1

Exit করলে প্রতিটা Process-ই Zombie হয়। এটা স্বাভাবিক, আর অল্প সময়ের: Parent চাইতে পারে ভেবে Kernel Exit Status রেখে দেয়।

#Inter-Process Communication

দেয়ালে Process-দের একটা দরজা লাগে। IPC হলো সেই দরজাগুলোর পরিবার।

পদ্ধতিকীভাবে কাজ করেকপিনাগালকোথায় ব্যবহারকীভাবে ভাঙে
PipeKernel Buffer দিয়ে একমুখী Byte-এর ধারা (Linux-এ ডিফল্ট ৬৪ KB)২সম্পর্কিত ProcessShell Pipeline, Parent-Child কথাবার্তাভরে গেলে লেখক আটকে যায়; পাঠক চলে গেলে SIGPIPE
Named Pipe (FIFO)File-এর নামওয়ালা Pipe২একই মেশিনসহজ, সম্পর্কহীন Processএকই, আর অপর পক্ষ না থাকলে খুলতেই আটকে যায়
Unix Domain SocketSocket API, Address হিসেবে File Path২একই মেশিনNginx → PHP-FPM, লোকাল Database Connection, Docker API, Sidecarপুরনো Socket File, ভুল File Permission
TCP / UDP Socketপুরো Network Stack২ বা বেশিযেকোনো জায়গায়Service, Database, মেশিনের মাঝের সবকিছুTimeout, আংশিক ব্যর্থতা
Shared Memoryএকই Page দুই পক্ষে ম্যাপ করা০একই মেশিনPostgreSQL-এর Shared Buffer, বিপুল হারের DataRace; Crash সবার জন্য এটা নষ্ট করতে পারে
Message Queue (POSIX, System V)Kernel-এ রাখা Message-এর Queue২একই মেশিনআজকাল কম; App সাধারণত Socket নেয়ভরে যায়, আকারের সীমা
SignalAsynchronous-ভাবে পৌঁছানো একটা নম্বরনেইএকই মেশিনখবর দেওয়া: থামো, Reload, Child বের হয়েছেData বয় না, মিলে যেতে পারে, Handler-এর Code সীমিত
File, Memory-mapped FileFile System হয়েভিন্ন ভিন্নএকই মেশিন, বা Shared StorageBatch হাতবদল, সহজ Lockআধা পড়া, Locking

কীভাবে বাছবে। একই মেশিন আর Request-এর ধারা: Unix Domain Socket, কারণ Socket API পরে সহজে TCP-তে নেওয়া যায়। একই মেশিন, বিপুল হার, আর নিজের Lock-এ ভরসা আছে: Shared Memory। কখনো অন্য মেশিনে যেতে পারে এমন যেকোনো কিছু: ঠিকঠাক Protocol-সহ TCP (HTTP, gRPC), বা Message Broker (Chapter ৩০–৩২)। বাস্তবে একটা System-এর Microservice মানে Network দিয়ে IPC করা Process, আর বাড়তি সব ব্যর্থতা আসে ওই “Network” শব্দটা থেকেই।

লোকাল IPC ভাঙেও সহজভাবে। Pipe হয় কাজ করে, নয় জানায় ভেঙেছে; Network Call-এ উত্তরই না আসতে পারে, আর কাজটা হলো কি না, তুমি বুঝতেই পারো না।

দেয়াল পেরিয়ে কথা: Inter-Process Communication

Process একে অন্যের Memory পড়তে পারে না, তাই প্রতিটা পদ্ধতিতেই Kernel হয় Data বয়ে নেয়, নয়তো একটা ভাগের জায়গা সাজিয়ে দেয়। দেখো কোনটার খরচ কত, আর কোনটা কীভাবে ভাঙে।

পরিস্থিতি

ls | wc -l: সম্পর্কিত Process-দের মধ্যে একমুখী Byte।

Process A
KernelBuffer: ৬৪ KB
Process B
ধাপ 1 / 3
1

Shell pipe ডাকে, আর Kernel দুটো File Descriptor সহ একটা Buffer বানায়: পড়ার মাথা আর লেখার মাথা। fork-এর পর দুই পক্ষ একেকটা পায়, তাই Program-গুলো সাধারণ Standard Output আর Input-ই ব্যবহার করে।

#Signal আর Graceful Shutdown

Signal হলো Kernel থেকে আসা ছোট্ট Asynchronous খবর, চেনা যায় শুধু নম্বর দিয়ে। আসতে পারে যেকোনো Instruction-এর মাঝে। তখন Process তার Handler চালায়, আর Handler না থাকলে ডিফল্ট কাজটা হয়।

Signalনম্বরডিফল্টমানে আর সাধারণ ব্যবহার
SIGHUP১শেষTerminal বন্ধ; রীতি অনুযায়ী Daemon-এর জন্য “Config আবার পড়ো”
SIGINT২শেষCtrl+C
SIGKILL৯শেষKernel এখনই শেষ করে দেয়। ধরা, আটকানো বা উপেক্ষা করা যায় না
SIGSEGV১১শেষ + Core Dumpঅবৈধ Memory ছোঁয়া
SIGPIPE১৩শেষবন্ধ Pipe বা Socket-এ লেখা; Server এটা উপেক্ষা করে Write Error সামলায়
SIGTERM১৫শেষ“দয়া করে থামো।” kill, systemctl stop, docker stop আর Kubernetes প্রথমে এটাই পাঠায়
SIGCHLDx86 Linux-এ ১৭উপেক্ষাএকটা Child বের হয়েছে
SIGSTOP / SIGCONTx86 Linux-এ ১৯ / ১৮থামা / চলাPause আর Resume। SIGSTOP ধরা যায় না

Exit Code বলে দেয় কী ঘটেছে। Signal-এ Process মরলে Shell আর Container Platform জানায় ১২৮ + Signal-এর নম্বর:

Exit Codeমানেকোথায় দেখবে
০সফল
১, ২, …Program-এর নিজের ErrorApplication Log
১২৬ / ১২৭চালানো যাচ্ছে না / Command নেইImage, Path, CMD
১৩৭ (১২৮ + ৯)SIGKILL: OOM Killer, বা Grace Period শেষdmesg, kubectl describe pod-এ কারণ
১৩৯ (১২৮ + ১১)SIGSEGV: Memory Bug, সাধারণত Native CodeCore Dump, সাম্প্রতিক Native Library বদল
১৪৩ (১২৮ + ১৫)Handler ছাড়া SIGTERMHandler যোগ করো; কাটা পড়া Request দেখো

#Graceful Shutdown লেখা

Lesson ১.২-এ Deploy-এর ধাপ দেখেছ। এবার Process-এর দিকটা, Node.js-এ:

const http = require('node:http');

const server = http.createServer((req, res) => res.end('ok\n'));
server.listen(8080);

process.on('SIGTERM', () => {
  console.log('SIGTERM: draining');
  // Stop accepting; the callback fires when in-flight requests are done.
  server.close(() => {
    console.log('drained, exiting');
    process.exit(0);
  });
  // Give up shortly before the orchestrator's SIGKILL would.
  setTimeout(() => process.exit(1), 25_000).unref();
});

যেকোনো ভাষায় ক্রমটা একই:

১. Readiness ব্যর্থ করো, যাতে Traffic থামে; তারপর একটু অপেক্ষা করো, কারণ Load Balancer সাথে সাথে বোঝে না। Kubernetes-এ Endpoint সরানো আর SIGTERM একই সময়ে হয়, পরপর না, তাই preStop Hook-এ কয়েক সেকেন্ড Sleep দেওয়া প্রচলিত।

২. নতুন কাজ নেওয়া বন্ধ করো: Listening Socket বন্ধ, Queue থেকে নেওয়া থামাও।

৩. চলমান কাজ শেষ করো, এমন সময়সীমায়, যা Grace Period-এর চেয়ে ছোট (Kubernetes-এ ডিফল্ট ৩০ সেকেন্ড, docker stop-এ ১০)।

৪. Resource ছেড়ে দাও: Log আর Metric Flush করো, অসমাপ্ত Message Queue-তে ফেরত দাও, Database Connection বন্ধ করো।

৫. Exit 0।

যে ভাষায় Native Code-এ Handler লেখো, সেখানে Signal Handler শুধু Async-signal-safe Function-এর ছোট্ট একটা তালিকাই ডাকতে পারে, কারণ সে যেকোনো কিছুকে থামিয়ে চলে আসতে পারে, এমনকি malloc-কেও। তাই Handler শুধু একটা Flag বসায় বা Pipe-এ লেখে, বাকিটা করে সাধারণ Code। Process-এ পাঠানো Signal যায় এমন যেকোনো একটা Thread-এ, যে সেটা Block করেনি; তাই Multithreaded Server সাধারণত সব জায়গায় Signal Block করে, আর একটা Thread রাখে sigwait-এর জন্য।

এগুলোর কিছুই না চললে কী হবে, সেভাবে Design করো। বিদ্যুৎ চলে যাওয়া, Kernel Panic বা SIGKILL কোনো সতর্কতা দেয় না। Graceful Shutdown Deploy-কে পরিচ্ছন্ন করে, কিন্তু Crash-কে নিরাপদ করে শুধু Idempotent Operation, Transaction আর Durable Log।

Signal আর Graceful Shutdown

প্রতিটা Deploy শেষ হয় পুরনো Process-এর SIGTERM পাওয়া দিয়ে। ব্যবহারকারী Error দেখবে কি না, সেটা নির্ভর করে Process এরপর কী করে তার ওপর। এখানে Grace Period হলো Kubernetes-এর ডিফল্ট ৩০ সেকেন্ড।

পরিস্থিতি

Handler নতুন কাজ বন্ধ করে, পুরনো কাজ শেষ করে, আর বের হয়।

OrchestratorKubernetes
Load Balancer
App Process৩টা Request চলমান
Target থেকে সরাও
ধাপ 1 / 5
1

প্রথমে Traffic সরানো হয়। Kubernetes-এ এটা Signal-এর আগে না, একই সময়ে হয়; তাই বন্ধ হতে থাকা Server-এ Request যাওয়া ঠেকাতে preStop Hook-এ অল্প Sleep দেওয়া একটা প্রচলিত উপায়।

#Crash Isolation: কার সাথে কে মরে

নিয়মটা সহজ: একটা Bug-এর ক্ষতির সীমা হলো তার সাথে Memory ভাগ করে যে Unit।

এক Thread ভুল Pointer ধরলে Kernel পাঠায় SIGSEGV, আর ডিফল্টে পুরো Process মরে: সব Thread, চলমান সব Request। এটা না হয়ে উপায় নেই। Bug-টা ভাগের Memory-র যা খুশি নষ্ট করে থাকতে পারে, তাই বাকি Thread-দের আর বিশ্বাস করা যায় না। আলাদা Process হলে ব্যাপারটা আলাদা: একটা Crash করলে বাকিদের কিছু হয় না, যদি না তারা Memory ভাগ করে।

এখানে তিনটা জিনিস প্রায়ই গুলিয়ে যায়:

ঘটনাকী মরে
Native Code-এ Segfault (যেকোনো Thread-এ)পুরো Process
Java বা Python-এ, Main ছাড়া অন্য Thread-এ না-ধরা Exceptionশুধু সেই Thread; Process চলে, হয়তো আধা-বদলানো Object বা ধরে রাখা Lock নিয়ে
Node.js-এ না-ধরা Exception, বা Go-তে যেকোনো Goroutine-এ Recover না করা Panicপুরো Process
C++-এ না-ধরা Exceptionপুরো Process, std::terminate হয়ে
Memory শেষ (Linux OOM Killer)Kernel যে Process বাছে সে পুরোটা, তারপর হয়তো আরও
গভীর Recursion-এ Stack OverflowNative Code-এ সাধারণত পুরো Process; Java আর Python-এ ধরা যায় এমন Error

Security-র চেহারাও একই। ২০১৪ সালের OpenSSL-এর Heartbleed Bug-এ Attacker Server-এর Process Memory পড়তে পেরেছিল, Private Key-সহ, কারণ সবকিছু একই Address Space-এ ছিল। SSH-এর Privilege Separation-এর মতো, স্পর্শকাতর কাজ নিজের Process-এ সরিয়ে নিলে Bug-এর নাগাল কমে।

#Multi-process বনাম Multi-thread Server

Lesson ১.২ Model-গুলোর সাথে পরিচয় করিয়েছে। এবার বোঝা যাবে আসল System-গুলো কেন যা বেছেছে তা-ই বেছেছে।

SystemModelকেন, আর দাম কী
PostgreSQLপ্রতি Connection-এ একটা OS Process, Postmaster থেকে Fork করা, Buffer-এর জন্য Shared Memory১৯৮০-র দশকের Design, খুব Portable। প্রতিটা Backend কয়েক MB খায়, তাই max_connections সাধারণত কয়েকশোর মধ্যে রাখা হয় (ডিফল্ট ১০০)। তাই PgBouncer। একটা Backend Crash করলে Postmaster বাকিদেরও Reset করে, কারণ Shared Memory নষ্ট হয়ে থাকতে পারে
MySQLএকটা mysqld Process, প্রতি Connection-এ একটা Threadপ্রতি Connection-এ সস্তা, হাজারো Connection বাস্তবসম্মত। Crash করলে পুরো Server যায়, Restart-এর পর InnoDB Redo Log দিয়ে Recovery করে
NginxMaster আর প্রতি Core-এ একটা Worker Process, প্রত্যেকে একটা Event LoopWorker-রা আলাদা; মরা Worker কয়েক মিলিসেকেন্ডে বদলে যায়। State ভাগ করতে স্পষ্ট Shared Memory Zone লাগে (Lesson ১.১৩)
Apacheprefork (Process), worker আর event (Process-এর ভেতরে Thread)একটা Product-এর ভেতরেই Trade-off: Thread-safe না এমন Library-র জন্য prefork, Scale-এর জন্য event
PHP-FPM, Gunicornআগে থেকে Fork করা Worker-এর Poolখারাপ Request আর Memory Leak আটকায়, CPU-র কাজে Python-এর GIL এড়ায় (Lesson ৩.২)। N-টা Request পর Worker বদলানো যায়
Tomcat, পুরনো ধাঁচের Java ও .NETএকটা Process, একটা Thread PoolCache আর Connection Pool Memory-তে ভাগ হয়, দ্রুত চালু হয়। একটা খারাপ Native Library বা Heap শেষ হওয়া সব ফেলে দেয়
Redisএকটা Process; Command একটা Thread-এ, ইচ্ছে হলে I/O Thread; Persistence-এর জন্য ForkData-র পথে Lock নেই; Persistence চলে Copy-on-Write-এ। Lesson ৩.৬ দেখো
ChromeBrowser, GPU, Network আর অনেক Renderer Process, ভেতরে Threadখারাপ Page অন্য Site-কে Crash করাতে বা পড়তে পারে না (Site Isolation)। দাম হলো Memory

কার সাথে কে মরে: Thread, Process আর আসল Server

একটা Bug-এর ক্ষতির সীমা হলো তার সাথে Memory ভাগ করে যে Unit। দেখো একটা Thread-এর Segfault, সাধারণ Exception, Worker Process, Chrome-এর Tab, PostgreSQL আর MySQL।

পরিস্থিতি

একটা Thread-এ একটা ভুল Pointer, একই Process-এ ২০০টা Request।

shop-apiPID 4121
ভাগের Heap
সব Request-এর Data
Thread
req A
req B
req C
২০০টা Request চলছে
ধাপ 1 / 4
1

তিনটা Thread দেখানো হলো। সবাই একই Heap ব্যবহার করে, আর এটাই তাদের দ্রুত ও সস্তা করে।

#কীভাবে বাছবে

তোমার যা দরকারযেদিকে ঝুঁকবেকারণ
যে Code-এ পুরো ভরসা নেই (Plugin, Native Library, User Content)Processদেয়াল Crash আর Exploit আটকায়
বড় ভাগের In-memory State (Cache, Connection Pool)Threadএকটাই Heap, কপি নেই
Global Lock-ওয়ালা ভাষায় সব Core-এ CPU-র কাজProcessThread ওই ভাষার Code Parallel চালাতে পারে না (CPython-এর ডিফল্ট Build)
বেশিরভাগ সময় অলস অনেক ConnectionEvent Loop বা Virtual ThreadLesson ৩.৫ আর ৩.৬ দেখো
User বা ক্ষমতার স্তরের মধ্যে Security-র ভাগProcess (বা Container, বা VM)Thread Credential আর Memory ভাগ করে
দ্রুত চালু, প্রতি Unit-এ কম MemoryThreadসাজানোর কাজ অনেক কম
একটা অংশ দ্রুত Restart, বা Leak আটকানোProcessWorker বদলানো রোজকার কাজ
এক মেশিনের বাইরে ScaleNetwork-এ কথা বলা ProcessThread মেশিনের সীমানা পার হতে পারে না

বাস্তবে আধুনিক System এগুলো মিলিয়ে ব্যবহার করে: মেশিনে কয়েকটা Process বা Container প্রতি একটা, প্রত্যেকের ভেতরে Thread-এর Pool বা Event Loop, ছড়ানো অনেক মেশিনে, আর State থাকে বাইরে। Process মরলে Container Platform Restart করে, তাই পুরো Fleet-এর ভরসা এই Process-স্তরের Isolation-এই। Bulkhead Pattern আলাদা Dependency-কে আলাদা Pool দিয়ে একই ধারণা বাড়ায় (Lesson ৩.৩)।

#Production-এর পরিচিত সমস্যা

লক্ষণসম্ভাব্য কারণকী দেখবেসমাধান
“fork: Resource temporarily unavailable”pids.max, ulimit -u বা pid_max-এ ঠেকেছে, প্রায়ই Thread Leak-এps -eLf | wc -l, cat /sys/fs/cgroup/pids.currentসীমিত Pool, Leak সারাও
Java: OutOfMemoryError: unable to create native threadএকই Task-এর সীমা, বা Address Space নেই, অথচ Heap ঠিক দেখায়Thread-এর সংখ্যা, pids.maxPool, ছোট -Xss
“too many open files”FD-এর সীমা, বা Connection বা File Leakls /proc/<pid>/fd | wc -lLimitNOFILE বাড়াও, যা খোলো তা বন্ধ করো
<defunct> বাড়ছেParent Reap করছে নাps -eo pid,ppid,stat,comm | grep ZParent ঠিক করো; Container-এ init দাও
থামতে ঠিক ১০ বা ৩০ সেকেন্ড, তারপর Exit ১৩৭Signal App-এ পৌঁছায়নি, বা Handler নেইCMD কি Shell Form?Exec Form, --init, Handler
প্রতি Deploy-এ ErrorGraceful Shutdown নেই, বা SIGTERM-এর পরেও Traffic আসছেExit Code ১৪৩, 502-এর ঝাপটাHandler, preStop, Drain
এলোমেলো Exit ১৩৯Native Memory BugCore Dump, dmesgLibrary Update বা আলাদা করো
Load Average বেশি, CPU অলসThread D-তে: Storage ঝুলে আছেps -eo stat,comm | grep ^DStorage বা Mount ঠিক করো
Service Restart হয় না: address already in useকোনো Orphan Socket পেয়ে বসে আছেss -tlnpতাকে মারো, close-on-exec দাও, systemd-র Cgroup Kill ব্যবহার করো
SIGTERM Handler মাঝে মাঝে আটকে যায়Handler অনিরাপদ কাজ করে Lock-এ Deadlock হয়Stack DumpHandler শুধু Flag বসাবে

#System Design-এ Process আর Thread

  • Process তোমার ব্যর্থতা আর Deploy-এর Unit; মেশিন Hardware ব্যর্থতার Unit। ধরে নাও যেকোনো Process যেকোনো মুহূর্তে মরতে পারে, State বাইরে রাখো (Lesson ১.৩), আর কয়েকটা চালাও।
  • Thread আর Process আসল Capacity খায়। ১ MB Stack-এর হাজারটা Thread, প্রত্যেকে ১০০ KB ছুঁলে প্রায় ১০০ MB Memory, সাথে Scheduling-এর খরচ। ৫,০০০ Process-ওয়ালা Database মানে Outage অপেক্ষা করছে। সামনে একটা Pool দাও।
  • Connection-এর সীমা আসলে Process আর FD-র সীমা। Database-এর Connection Cap এই দাম থেকেই আসে। Capacity-র হিসাবে গোনো: প্রতি App Instance-এ Connection × Instance-এর সংখ্যা।
  • Stateless Process সহজে Restart হয়। Memory-র গরম Cache Process-এর সাথেই যায়, তাই Restart-এ Cold Start-এর ঝাপটা আসতে পারে; ধীরে ধীরে Roll করো।
  • Isolation একটা নব: Thread → Process → Container → Virtual Machine → আলাদা মেশিন। প্রতি ধাপে দাম বাড়ে, আটকানোর ক্ষমতাও বাড়ে (একদম শেষ প্রান্তটা Chapter 4-এ)।
  • Interview-এর দিক: “Process বনাম Thread?” উত্তর দাও কী ভাগ হয়, ক্ষতির সীমা কত, আর দাম কত দিয়ে। “PostgreSQL-এর PgBouncer লাগে কেন, আর MySQL-এর সাধারণত লাগে না কেন?” “Zombie কী, মারা যায়?” “Container থামতে ৩০ সেকেন্ড লাগে কেন?” “Exit ১৩৭ মানে কী?” “Plugin Thread-এ চালাবে, না Process-এ?” “একটা ভুল Pointer কীভাবে ২০০টা Request ফেলে দেয়?”

#নিজে করে দেখো

Linux মেশিনে, বা procps আর python3 আছে এমন Container-এ:

# Look at processes: pid, parent, state, thread count, resident KB, name
ps -eo pid,ppid,stat,nlwp,rss,comm --sort=-nlwp | head

# Every thread of every process (LWP is the TID)
ps -eLf | head

# Everything the kernel knows about your shell
grep -E 'Name|State|Tgid|^Pid|PPid|Threads|Uid' /proc/$$/status
ls -l /proc/$$/fd        # its file descriptors
id                       # its security context

# Watch the system calls behind a command, and count them
strace -f -e trace=clone,clone3,execve,wait4 bash -c 'ls > /dev/null'
strace -c ls > /dev/null

# Make a zombie for 30 seconds, then see it
python3 -c "import os, time; pid = os.fork(); (os._exit(0) if pid == 0 else time.sleep(30))" &
sleep 1; ps -o pid,ppid,stat,comm --ppid $!

# Read exit codes: 143 and 137
sleep 100 & P=$!; kill -TERM $P; wait $P; echo "exit code $?"
sleep 100 & P=$!; kill -KILL $P; wait $P; echo "exit code $?"

# One thread segfaults and the whole process goes down: the print never runs, exit code 139
python3 -c 'import ctypes, threading, time
threading.Thread(target=lambda: ctypes.string_at(0)).start()
time.sleep(5); print("never printed")'; echo "exit code $?"

Thread Memory ভাগ করে, Process করে না। এটা share.py নামে Save করে চালাও:

import multiprocessing as mp
import threading

counter = 0

def bump():
    global counter
    counter = 100

if __name__ == '__main__':
    t = threading.Thread(target=bump)
    t.start(); t.join()
    print('after thread :', counter)   # 100: same memory

    counter = 0
    p = mp.Process(target=bump)
    p.start(); p.join()
    print('after process:', counter)   # 0: the child changed its own copy

যে Dockerfile Signal-কে তোমার App পর্যন্ত পৌঁছাতে দেয়:

# Shell form runs under sh, so the app does not get SIGTERM:
#   CMD node server.js
# Exec form makes the app PID 1, and it needs its own handler:
CMD ["node", "server.js"]

docker run --init দিয়ে চালাও, তাহলে Signal পৌঁছাবে আর Zombie Reap হবে; তারপর time docker stop <name>। ঠিক ১০ সেকেন্ড লাগলে বুঝবে Signal পৌঁছাচ্ছে না।

#সারসংক্ষেপ

মূল শিক্ষা

  • Kernel হলো OS-এর ক্ষমতাধর কেন্দ্র। Program চলে User Mode-এ, আর সবকিছু চায় System Call দিয়ে, যার দাম মোটামুটি ১০০ ns থেকে ১ µs; তাই Program Buffer আর Batch করে।
  • Process হলো চলমান Program, যার নিজের Virtual Address Space, PID, FD, Security Context আর Thread আছে, আর Kernel-এর PCB-তে যার হিসাব থাকে। Process-দের মাঝে দেয়াল, তাতে ইচ্ছাকৃত কিছু দরজা।
  • Thread হলো Process-এর ভেতরে Execution-এর ধারা। Thread-রা Heap, Global, Code আর FD ভাগ করে; প্রত্যেকের নিজের Stack, Program Counter, Register আর TID। Stack আলাদা, কিন্তু সুরক্ষিত না।
  • fork Copy-on-Write দিয়ে কপি করে, exec নতুন Program বসায়, wait Status নেয়। Linux-এ Thread হলো clone-এর এমন Task, যে সব ভাগ করে। তৈরির দাম এত যে Server Worker আবার ব্যবহার করে। Thread-ওয়ালা Program-এ fork করে কাজ চালিয়ে যেও না।
  • অবস্থা: Ready আর Running দুটোই R, S সস্তায় অপেক্ষা করে, D থামানো যায় না, আর CPU অলস অথচ Load বেশি মানে Storage-এ গোলমাল।
  • Zombie মৃত কিন্তু Reap হয়নি, তাকে মারা যায় না; Parent ঠিক করো। Orphan জীবিত, PID 1 দত্তক নেয়। Container-এ PID 1 তোমার App-ই, তাই একটা init দাও।
  • IPC: ধারার জন্য Pipe, লোকালে Unix Socket, গতির জন্য Shared Memory (দাম ভাগের ব্যর্থতা), মেশিন পেরোতে পারে এমন সবকিছুর জন্য TCP।
  • Graceful Shutdown-এর জন্য SIGTERM ধরা যায়; SIGKILL যায় না। Exit Code = ১২৮ + Signal: ১৩৭, ১৩৯, ১৪৩। Dockerfile-এ Shell Form, বা Handler-হীন PID 1, SIGTERM গিলে ফেলে।
  • Bug-এর ক্ষতির সীমা হলো যে তার Memory ভাগ করে: Segfault Process-এর সব Thread মারে। Shared Buffer-এর কারণে একটা Crash-এ PostgreSQL সব Backend Reset করে, MySQL পুরো Process হারায়, আর Chrome Site আলাদা রাখে।
  • আসল System মেলায় কয়েকটা Process, Thread-এর Pool বা Event Loop, আর অনেক মেশিন; State থাকে বাইরে।