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

Signal — প্রসেসকে বার্তা পাঠানো আর সামলানো

Unix Signals — Generation, Masks, Handlers, Async-Signal Safety and Graceful Shutdown

Taught in Bangla, this lesson explains Unix signals: their three sources, common signals and default actions, why SIGKILL cannot be caught, generation, pending and delivery, signal masks and sigprocmask, why ordinary signals do not queue, safe handlers with sigaction, async-signal safety and sig_atomic_t, EINTR, reaping zombies with SIGCHLD, SIGTERM-based graceful shutdown inside docker stop and systemctl stop, and Windows alternatives.

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

  • Signal কী, কোথা থেকে আসে, আর Ctrl+C চাপলে কিবোর্ড থেকে প্রসেস পর্যন্ত কী কী ঘটে
  • সাধারণ signal-গুলোর নম্বর আর default action, আর কেন SIGKILL ধরা যায় না
  • Generation → pending → delivery, signal mask, sigprocmask, আর কেন সাধারণ signal জমা হয় না
  • sigaction দিয়ে নিরাপদ handler লেখা, EINTR, আর SIGCHLD দিয়ে zombie পরিষ্কার
  • SIGTERM দিয়ে graceful shutdown, docker stop ও systemctl stop-এর ভেতরের ক্রম, আর Windows-এ এর বিকল্প

১ · Signal কী — প্রসেসের দরজায় হঠাৎ টোকা

আগের পর্বে (পর্ব ১২) দেখেছেন, প্রসেসেরা pipe, shared memory বা socket দিয়ে অনেক ডেটা আদান-প্রদান করে। কিন্তু অনেক সময় ডেটা নয়, শুধু একটি ছোট খবর পাঠানো দরকার: “থামুন”, “আপনার কাজের সময় শেষ”, “আপনার child প্রসেস মারা গেছে”, “আপনি নিষিদ্ধ মেমরিতে হাত দিয়েছেন”। এই ধরনের ছোট, হঠাৎ আসা বার্তার নাম signal.

ধরুন আপনি বাসায় মন দিয়ে একটি হিসাব মেলাচ্ছেন। হঠাৎ বাড়ির দারোয়ান দরজায় টোকা দিলেন — “স্যার, আপনার পার্সেল এসেছে”। আপনি জানতেন না টোকাটি কখন আসবে। এখন আপনার সামনে তিনটি পথ: হিসাব থামিয়ে পার্সেল নেওয়া (নিজস্ব handler), টোকা না শোনার ভান করা (ignore), অথবা আগে থেকে ঠিক করা নিয়মে চলা (default action). আর যদি দারোয়ান বলেন “আগুন লেগেছে, এখনই বের হন” — সেখানে আপনার পছন্দের প্রশ্নই নেই। এটিই SIGKILL.

OS-এর ভাষায়, signal হলো kernel-এর মাধ্যমে একটি প্রসেসকে পাঠানো একটি ছোট সংখ্যা (১, ২, ৯, ১৫…), প্রতিটি সংখ্যার একটি নাম আছে (SIGINT, SIGKILL, SIGTERM…). Signal-এর সাথে সাধারণত আর কোনো ডেটা যায় না — শুধু “কোন signal” এই তথ্যটুকু। দুটি বৈশিষ্ট্য একে আলাদা করে:

  • Asynchronous (অসমলয়): প্রসেস যেকোনো instruction-এর মাঝখানে থাকতে পারে, আর signal ঠিক তখনই এসে তার স্বাভাবিক কাজ থামিয়ে দিতে পারে। প্রসেস একে “অপেক্ষা করে পড়ে” না, যেমন pipe থেকে read() করে।
  • সংক্ষিপ্ত: signal শুধু একটি ঘটনা জানায়। কে পাঠাল, কেন — এসব বাড়তি তথ্য চাইলে বিশেষ ব্যবস্থা লাগে (অংশ ৫-এ SA_SIGINFO).

Software interrupt — হার্ডওয়্যার interrupt-এর ছোট ভাই

পর্ব ৪-এ দেখেছেন, হার্ডওয়্যার interrupt এলে CPU চলতি কাজ থামিয়ে kernel-এর একটি নির্দিষ্ট handler চালায়, তারপর আগের জায়গায় ফেরে। Signal হুবহু একই ধারণা, শুধু এক স্তর ওপরে — CPU-র বদলে প্রসেস, আর হার্ডওয়্যারের বদলে kernel. তুলনাটি পাশাপাশি দেখুন:

বিষয়হার্ডওয়্যার interruptSignal
কে পায়CPUএকটি প্রসেস
কে পাঠায়ডিভাইস (টাইমার, কিবোর্ড, ডিস্ক)kernel, বা kernel-এর মাধ্যমে অন্য প্রসেস
পরিচয়interrupt vector নম্বরsignal নম্বর (১–৬৪, Linux)
সাড়াkernel-এর ISRপ্রসেসের signal handler, অথবা default action
সাময়িক বন্ধ রাখাinterrupt disable (cli)signal mask-এ block করা
শেষেiret দিয়ে আগের কাজেhandler ফিরলে আগের কাজে (sigreturn)
মূল ধারণা

Signal প্রসেসের জন্য “interrupt”: প্রসেস নিজের কাজ করছে, kernel মাঝখানে থামিয়ে একটি ছোট ফাংশন (handler) চালায়, তারপর আগের জায়গা থেকে আবার চালু করে। প্রসেস চাইলে নির্দিষ্ট signal কিছুক্ষণ আটকে রাখতে পারে, যেমন kernel interrupt বন্ধ রাখে। এই মিলটি মাথায় রাখলে signal-এর প্রায় সব নিয়ম নিজেই আন্দাজ করতে পারবেন।

একটি সূক্ষ্ম পার্থক্যও আছে। কিছু signal আসলে synchronous — প্রসেসের নিজের একটি instruction-এর কারণে তৈরি, যেমন শূন্য দিয়ে ভাগ করলে SIGFPE, ভুল ঠিকানায় পড়লে SIGSEGV. CPU তখন একটি exception তোলে, kernel সেটিকে signal-এ রূপ দেয়। আর Ctrl+C বা অন্য প্রসেসের kill() থেকে আসা signal asynchronous — প্রসেসের কাজের সাথে কোনো সম্পর্ক নেই। এজন্যই অনেক বই signal-কে বলে “software interrupt”, যদিও পর্ব ৫-এর trap-কেও কখনো একই নামে ডাকা হয় — প্রসঙ্গ দেখে বুঝে নিতে হয়।

Signal Unix-এর একেবারে প্রথম দিকের ধারণা — ১৯৭০-এর দশকে Bell Labs-এর Unix-এ signal() ছিল। কিন্তু তখনকার নিয়মে একটি বড় ত্রুটি ছিল: handler একবার চলার পর আবার default-এ ফিরে যেত, আর handler আবার বসানোর আগের মুহূর্তে দ্বিতীয় signal এলে প্রসেস মারা যেত। একে বলা হয় “unreliable signals”. ১৯৮৩ সালের 4.2BSD নির্ভরযোগ্য signal আনে, আর ১৯৮৮ সালের POSIX মানদণ্ড sigaction() দিয়ে নিয়মগুলো স্থির করে। সেই ইতিহাসের কারণেই আজও পরামর্শ — signal() নয়, sigaction().

২ · Signal কোথা থেকে আসে — তিনটি উৎস

⚙️ Kernel নিজে

প্রসেসের কোনো ভুল বা কোনো ঘটনা kernel টের পেলে: অবৈধ মেমরি (SIGSEGV), শূন্য দিয়ে পূর্ণসংখ্যা ভাগ (SIGFPE), child শেষ (SIGCHLD), পাঠক-বিহীন pipe-এ লেখা (SIGPIPE), alarm()-এর সময় পূর্ণ (SIGALRM).

exception বা kernel-এর ঘটনা

⌨️ টার্মিনাল

বিশেষ কি চাপলে টার্মিনালের driver সামনের প্রসেসগুলোকে signal দেয়: Ctrl+C → SIGINT, Ctrl+Z → SIGTSTP, Ctrl+\ → SIGQUIT. টার্মিনাল বন্ধ হলে SIGHUP, আকার বদলালে SIGWINCH.

tty driver

📨 অন্য প্রসেস

kill(pid, sig) system call, বা shell-এর kill কমান্ড। নাম “kill” হলেও এটি যেকোনো signal পাঠাতে পারে। নিজের কাছে পাঠাতে raise(sig).

অনুমতি লাগে: একই user বা root

Kernel-এর তৈরি কয়েকটি signal বাস্তবে খুব চেনা। একটি C প্রোগ্রাম NULL পয়েন্টার থেকে পড়লে CPU page fault তোলে; kernel দেখে ঠিকানাটি প্রসেসের কোনো বৈধ এলাকায় নেই (পর্ব ৩৫), তাই SIGSEGV পাঠায় — আর আপনি দেখেন “Segmentation fault (core dumped)”. আবার yes | head -n 1 চালালে head একটি লাইন পড়েই বেরিয়ে যায়; এরপর yes আর লেখার জায়গা পায় না, kernel তাকে SIGPIPE দেয় আর সে চুপচাপ শেষ হয়। এজন্যই pipeline-এর শেষ অংশ থামলে পুরো pipeline নিজে থেকে গুটিয়ে যায়।

অন্য প্রসেসের signal পাঠানোর অনুমতি নিয়ে একটি কথা: আপনি শুধু নিজের প্রসেসগুলোকেই signal পাঠাতে পারেন। অন্য ব্যবহারকারীর প্রসেসকে পাঠাতে root বা Linux-এর CAP_KILL ক্ষমতা লাগে (permission নিয়ে বিস্তারিত পর্ব ৪৭-এ)। নইলে যে কেউ সার্ভারের ডেটাবেস বন্ধ করে দিতে পারত। আর kill(pid, 0) কোনো signal পাঠায় না, শুধু যাচাই করে প্রসেসটি আছে কি না আর পাঠানোর অনুমতি আছে কি না।

Ctrl+C চাপলে আসলে কী ঘটে

টার্মিনালে একটি লম্বা কাজ চলছে: sort big.txt | uniq. আপনি Ctrl+C চাপলেন, দুটো প্রোগ্রামই থেমে গেল, shell আবার prompt দেখাল। এর পেছনে কয়েকটি স্তর কাজ করে — আর সবচেয়ে গুরুত্বপূর্ণ খেলোয়াড় হলো tty driver-এর line discipline আর process group. ধাপে ধাপে দেখুন:

  1. কিবোর্ডCtrl+Cbyte 0x03
  2. tty line disciplineintr = ^Cইনপুট নয়, নির্দেশ
  3. Foreground grouppgid 4100sort + uniq
  4. SIGINTsignal 2দুজনেরই pending
  5. Shell$? = 130prompt ফেরত
stty -a (অংশবিশেষ, উদাহরণ)কোন কি কোন signal
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; susp = ^Z;
... isig icanon echo ...
ps -o pid,pgid,tpgid,stat,comm (উদাহরণ)+ মানে foreground
  PID  PGID TPGID STAT COMMAND
 4012  4012  4100 Ss   bash
 4100  4100  4100 R+   sort
 4101  4100  4100 S+   uniq
দুই প্রসেসের সিদ্ধান্তhandler নেই → default
sort (4100): SIGINT, no handler -> terminate
uniq (4101): SIGINT, no handler -> terminate
Terminalshell আবার সামনে
$ sort big.txt | uniq
^C
$ echo $?
130
  1. আপনি Ctrl+C চাপলেন। কিবোর্ড একটি interrupt দেয়; GUI-তে terminal emulator কি-চাপটি পায় আর একটি pseudo-terminal (pty)-তে byte 0x03 লেখে। এই মুহূর্তে এটি নিছক একটি অক্ষর — এখনো কোনো signal তৈরি হয়নি।
  2. tty line discipline অক্ষরটি চিনে ফেলে। Kernel-এর tty স্তর প্রতিটি ইনপুট অক্ষর দেখে। isig চালু থাকলে আর অক্ষরটি intr (সাধারণত ^C) হলে সেটি প্রোগ্রামের ইনপুটে যায় না; বরং signal তৈরির নির্দেশ হয়ে যায়। stty intr ^X দিলে Ctrl+X-ই এই কাজ করত।
  3. লক্ষ্য: foreground process group. প্রতিটি টার্মিনালের একটি “সামনের” process group থাকে, যা shell tcsetpgrp() দিয়ে ঠিক করে দেয়। এখানে pipeline-এর দুই প্রসেস sort (4100) আর uniq (4101) একই group 4100-এ; TPGID 4100 আর STAT-এ + সেটিই বলছে। Shell (4012) নিজের আলাদা group-এ।
  4. Kernel পুরো group-কে SIGINT দেয়। শুধু একটি প্রসেস নয়, group-এর প্রত্যেককে — তাই pipeline-এর সব অংশ একসাথে থামে। দুজনের pending তালিকায় SIGINT (২) বসে গেল। Shell এই group-এ নেই, তাই সে signal পায় না।
  5. পৌঁছানো আর সিদ্ধান্ত। প্রতিটি প্রসেস kernel থেকে user mode-এ ফেরার মুহূর্তে signal পায়। sort বা uniq কেউ SIGINT-এর handler বসায়নি, তাই default action — প্রসেস শেষ। কোনো প্রোগ্রাম handler বসালে (যেমন vim বা python-এর interactive mode) সে থামত না, নিজের মতো সাড়া দিত।
  6. Shell আবার দায়িত্বে। Shell waitpid() দিয়ে জানতে পারে child দুটো SIGINT-এ মারা গেছে, টার্মিনালের foreground নিজের group-এ ফিরিয়ে নেয়, আর prompt দেখায়। $? = ১২৮ + ২ = ১৩০. Job control-এর বাকি খুঁটিনাটি পর্ব ১১-এ পেয়েছেন।

Ctrl+Z-এর পথ একই, শুধু signal আলাদা: SIGTSTP (Terminal SToP). এর default action প্রসেসকে থামিয়ে রাখা, শেষ করা নয়। পরে fg বা bg দিলে shell সেই group-কে SIGCONT পাঠায়, আর কাজ যেখানে থেমেছিল সেখান থেকে চলে। Ctrl+\ দেয় SIGQUIT, যা প্রসেস শেষ করার সাথে একটি core dump-ও তৈরি করতে পারে — debugging-এর জন্য মেমরির ছবি।

৩ · সাধারণ signal-এর তালিকা আর default action

প্রতিটি signal-এর একটি default action আছে — প্রসেস নিজে কিছু না বললে kernel যা করে। পাঁচ রকম default action:

  • Term: প্রসেস শেষ।
  • Core: প্রসেস শেষ, সাথে core dump (চালু থাকলে)।
  • Ign: কিছুই না, signal ফেলে দেওয়া হয়।
  • Stop: প্রসেস থেমে থাকে (মারা যায় না)।
  • Cont: থেমে থাকা প্রসেস আবার চলে।

নিচের টেবিলের নম্বরগুলো Linux x86-64-এর (ARM64 Linux-এও এগুলো একই)। macOS আর BSD-তে ১, ২, ৩, ৬, ৯, ১৪, ১৫-এর মতো পুরোনো নম্বর মেলে, কিন্তু অনেকগুলো আলাদা — যেমন macOS-এ SIGUSR1 = 30, SIGCHLD = 20. তাই কোডে সবসময় নাম লিখুন, নম্বর নয়।

নম্বরনামDefaultকখন আসে
1SIGHUPTermটার্মিনাল বন্ধ; অনেক daemon এটিকে “config আবার পড়ুন” হিসেবে ব্যবহার করে
2SIGINTTermCtrl+C
3SIGQUITCoreCtrl+\
4SIGILLCoreঅবৈধ instruction
6SIGABRTCoreabort(), যেমন assert ব্যর্থ হলে
7SIGBUSCoreকিছু ধরনের ভুল মেমরি-প্রবেশ, যেমন ছেঁটে ফেলা mmap ফাইলের বাইরে পড়া
8SIGFPECoreপূর্ণসংখ্যা শূন্য দিয়ে ভাগ (নামে “floating point” থাকলেও)
9SIGKILLTermkill -9; Linux-এর OOM killer — ধরা যায় না
10SIGUSR1Termব্যবহারকারীর নিজস্ব অর্থে; যেমন GNU dd এটি পেলে অগ্রগতি দেখায়
11SIGSEGVCoreঅবৈধ মেমরি-ঠিকানা (segmentation fault)
12SIGUSR2Termব্যবহারকারীর নিজস্ব অর্থে
13SIGPIPETermএমন pipe বা socket-এ লেখা, যার পড়ার দিক বন্ধ
14SIGALRMTermalarm()-এর টাইমার শেষ
15SIGTERMTermkill কমান্ডের ডিফল্ট — “ভদ্রভাবে শেষ করুন”
17SIGCHLDIgnchild শেষ হলো বা থামল
18SIGCONTContfg, bg, kill -CONT
19SIGSTOPStopkill -STOP — ধরা যায় না
20SIGTSTPStopCtrl+Z (এটি ধরা যায়)
28SIGWINCHIgnটার্মিনাল-জানালার আকার বদল
34–64SIGRTMIN…SIGRTMAXTermreal-time signal, প্রোগ্রামের নিজস্ব কাজে (অংশ ৪)

Linux kernel-এ real-time signal শুরু ৩২ থেকে; কিন্তু glibc ৩২ আর ৩৩ নিজের থ্রেড-লাইব্রেরির জন্য রেখে দেয়, তাই প্রোগ্রাম SIGRTMIN দেখে ৩৪ — অংশ ৪-এর প্রোগ্রামের আউটপুটেই এটি দেখবেন। এজন্যই SIGRTMIN একটি ধ্রুবক নয়, glibc-তে একটি ফাংশন-কল।

SIGKILL আর SIGSTOP কেন ধরা যায় না

বাকি প্রায় সব signal-এর জন্য প্রসেস নিজের handler বসাতে পারে, বা উপেক্ষা করতে পারে। শুধু দুটি ব্যতিক্রম: SIGKILL (৯) আর SIGSTOP (১৯). এদের জন্য sigaction() ডাকলে kernel EINVAL দিয়ে প্রত্যাখ্যান করে, mask-এ রাখলেও কাজ হয় না। কারণটি নকশাগত:

ট্রাফিক মোড়ে সাধারণ সিগন্যাল বাতি চালকেরা মানেন, কিন্তু চাইলে কেউ উপেক্ষাও করতে পারেন। তাই অ্যাম্বুলেন্স বা পুলিশের জন্য এমন একটি ক্ষমতা রাখা হয়, যা কোনো চালক “না” বলতে পারেন না। কম্পিউটারেও প্রশাসকের হাতে একটি নিশ্চিত উপায় থাকতেই হবে — একটি বাগওয়ালা বা দুষ্ট প্রোগ্রাম যদি সব signal ধরে উপেক্ষা করে, তাহলে তাকে থামানোর আর কোনো পথ থাকবে না।

তাই SIGKILL পুরো কাজটি kernel-এর ভেতরেই সারে: প্রসেসের কোনো কোড আর চলে না, handler নেই, cleanup নেই। এর মানে ঝুঁকিও আছে — অর্ধেক লেখা ফাইল, মুছে না-ফেলা temp ফাইল, ছেড়ে না-দেওয়া lock ফাইল রয়ে যায়। তবে kernel প্রসেসের সব মেমরি আর খোলা file descriptor ঠিকই ফেরত নেয়। একইভাবে SIGSTOP প্রশাসককে যেকোনো প্রসেস নিশ্চিতভাবে থামিয়ে রাখার ক্ষমতা দেয় — debugger-ও এটি ব্যবহার করে।

SIGKILL-ও তিনটি ক্ষেত্রে “কাজ করে না” মনে হতে পারে। (১) প্রসেস একটি zombie — সে আগেই মারা গেছে, শুধু parent-এর wait()-এর অপেক্ষায়; মারার কিছু নেই, parent-কে ঠিক করতে হবে। (২) প্রসেস uninterruptible sleep-এ (ps-এ STAT D), সাধারণত ধীর বা আটকে যাওয়া ডিস্ক বা নেটওয়ার্ক ফাইল সিস্টেমের অপেক্ষায়; signal pending থাকে, kernel-এর কাজ শেষ হলে তবেই পৌঁছায়। (৩) container-এর ভেতরের PID 1-এর বিশেষ নিয়ম (অংশ ৮)।

৪ · Signal-এর জীবনচক্র — তৈরি, অপেক্ষা আর পৌঁছানো

একটি signal-এর জীবনের তিনটি অবস্থা আলাদা করে চেনা জরুরি, কারণ পরীক্ষা আর বাস্তব বাগ — দুই জায়গাতেই প্রশ্ন আসে এই ফাঁক নিয়ে:

  1. Generation (তৈরি): কোনো ঘটনা ঘটল — kill() ডাকা হলো, Ctrl+C চাপা হলো, child শেষ হলো। Kernel লক্ষ্য-প্রসেসের রেকর্ডে (PCB, পর্ব ৮) signal-টি লিখে রাখে।
  2. Pending (অপেক্ষমাণ): signal তৈরি হয়েছে কিন্তু এখনো প্রসেসের হাতে পৌঁছায়নি। প্রতিটি প্রসেসের একটি pending set থাকে — সাধারণ signal-এর জন্য প্রতি signal-এ মাত্র এক বিট.
  3. Delivery (পৌঁছানো): kernel signal-টির ব্যবস্থা নেয় — handler চালায়, উপেক্ষা করে, বা default action করে। সাধারণত এটি ঘটে যখন প্রসেস kernel থেকে user mode-এ ফেরে (system call শেষে, interrupt শেষে, বা CPU পেয়ে)।

এর সাথে আছে signal mask (blocked set): প্রসেস যে signal-গুলো আপাতত নিতে চায় না, তার তালিকা। Mask-এ থাকা signal তৈরি হতে পারে, pending-ও হয়, কিন্তু যতক্ষণ block থাকে ততক্ষণ পৌঁছায় না। মনে রাখুন, block মানে ignore নয়: ignore করা signal ফেলে দেওয়া হয়, block করা signal অপেক্ষা করে আর unblock হলেই পৌঁছায়। Mask বদলানোর system call sigprocmask() (থ্রেডের জন্য pthread_sigmask()).

অফিসের একজন কর্মকর্তা গুরুত্বপূর্ণ মিটিংয়ে ঢোকার আগে দারোয়ানকে বললেন, “এক ঘণ্টা কোনো ফোন দেবেন না” (mask). এর মধ্যে তিনজন ফোন করে একই কথা বললেন — “ফাইলটা পৌঁছেছে”। দারোয়ান নোটপ্যাডে একটি টিক দিলেন: “ফাইল-বিষয়ক ফোন এসেছিল” (pending বিট)। তিনটি আলাদা টিক নয়, একটিই — কারণ তাঁর খাতায় প্রতিটি বিষয়ের জন্য একটিমাত্র ঘর। মিটিং শেষে (unblock) কর্মকর্তা একবারই খবরটি পেলেন। এটিই সাধারণ signal-এর আচরণ।

নিচের ডায়াগ্রামে একটি প্রসেস SIGUSR1 block করে, তারপর দুবার SIGUSR1 আর একবার SIGINT পায়। প্রতিটি ধাপে mask আর pending set কীভাবে বদলায়, আর কোন handler কতবার চলে, খেয়াল করুন।

1main(): handlers set for USR1, INT2sigprocmask(SIG_BLOCK, {USR1})3kill(getpid(), SIGUSR1);  // #14kill(getpid(), SIGUSR1);  // #25kill(getpid(), SIGINT);6kernel -> user: deliver INT7on_int() returns (sigreturn)8sigprocmask(SIG_UNBLOCK, {USR1})9back in main()
প্রসেসের signal-অবস্থা
blocked (mask)
pending
USR1 পাঠানো
on_usr1 চলেছে
on_int চলেছে
এখন চলছে
  1. শুরু। প্রসেস SIGUSR1 আর SIGINT-এর জন্য handler বসিয়েছে। Mask খালি, pending set খালি — যেকোনো signal এলে সঙ্গে সঙ্গে পৌঁছাবে।
  2. Mask-এ USR1. sigprocmask(SIG_BLOCK, …) দিয়ে প্রসেস বলল, “আপাতত USR1 দেবেন না”। হয়তো সে একটি গুরুত্বপূর্ণ ডেটা-কাঠামো বদলাচ্ছে, যা handler-ও ছোঁয়। Pending এখনো খালি।
  3. প্রথম USR1 — generation. Kernel pending set-এ USR1-এর বিট 1 করল। কিন্তু USR1 mask-এ আছে, তাই delivery হলো না। Handler চলেনি; main নিজের কাজ চালিয়ে যাচ্ছে।
  4. দ্বিতীয় USR1 — হারিয়ে গেল। বিটটি আগেই 1, আবার 1 করায় কিছু বদলায় না। দুবার পাঠানো হয়েছে, কিন্তু pending-এ চিহ্ন একটিই। সাধারণ signal গোনা হয় না — এটিই “signals are not queued”.
  5. SIGINT — block করা নয়। Pending-এ এখন USR1 আর INT. প্রসেস এই মুহূর্তে kill system call-এর ভেতরে, kernel mode-এ। INT mask-এ নেই, তাই ফেরার পথেই পৌঁছাবে।
  6. Delivery: on_int চলে। User mode-এ ফেরার ঠিক আগে kernel pending-এর মধ্যে unblocked signal খোঁজে, পায় INT. INT-এর বিট মুছে handler চালায়। Handler চলাকালীন kernel নিজে INT-কে mask-এ যোগ করে, যাতে একই handler নিজের ভেতরে আবার না ঢোকে। USR1 এখনো অপেক্ষায়।
  7. Handler ফিরল। Handler শেষে একটি বিশেষ system call sigreturn আগের register আর আগের mask ফিরিয়ে আনে। Mask আবার শুধু USR1, main ঠিক যেখানে থেমেছিল সেখান থেকে চলল।
  8. Unblock — জমে থাকা USR1 পৌঁছায়। SIG_UNBLOCK-এর system call থেকে ফেরার মুহূর্তেই kernel দেখে USR1 pending আর এখন unblocked. বিট মুছে on_usr1 চালায় — একবারই, যদিও পাঠানো হয়েছিল দুবার।
  9. শেষ হিসাব। USR1 পাঠানো ২ বার, handler চলেছে ১ বার; INT ১ বার, handler ১ বার। Mask আর pending দুটোই খালি। শিক্ষা: signal দিয়ে “কতবার ঘটেছে” গোনা যায় না, শুধু “অন্তত একবার ঘটেছে” জানা যায়।

Real-time signal — যেগুলো সারি বেঁধে থাকে

“Signal হারিয়ে যায়” সমস্যার সমাধান হিসেবে ১৯৯০-এর দশকে POSIX real-time extension-এ আসে real-time signal (SIGRTMIN থেকে SIGRTMAX). এদের তিনটি বাড়তি গুণ: প্রতিটি পাঠানো signal আলাদাভাবে সারিতে জমা থাকে (একটি সীমা পর্যন্ত), sigqueue() দিয়ে signal-এর সাথে একটি ছোট সংখ্যা বা পয়েন্টারও পাঠানো যায়, আর একাধিক real-time signal pending থাকলে ছোট নম্বরেরটি আগে পৌঁছায়। এবার আসল মেশিনে দেখি — একটি সাধারণ আর একটি real-time signal, দুটোই block করে তিনবার করে পাঠানো:

#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

static volatile sig_atomic_t n_usr1 = 0, n_rt = 0;
static int RT;                                   /* SIGRTMIN, set in main */

static void on_sig(int sig) { if (sig == SIGUSR1) n_usr1++; else n_rt++; }

static void show(const char *when) {
    sigset_t p;
    sigpending(&p);
    printf("%-22s pending: USR1=%s RT=%s | handler ran: USR1 %d, RT %d\n", when,
           sigismember(&p, SIGUSR1) ? "yes" : "no ", sigismember(&p, RT) ? "yes" : "no ",
           (int)n_usr1, (int)n_rt);
}

int main(void) {
    RT = SIGRTMIN;
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = on_sig;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);  sigaction(RT, &sa, NULL);

    sigset_t block, old;
    sigemptyset(&block);  sigaddset(&block, SIGUSR1);  sigaddset(&block, RT);
    sigprocmask(SIG_BLOCK, &block, &old);       /* put both in the mask */
    show("blocked, nothing sent");

    for (int i = 0; i < 3; i++) { kill(getpid(), SIGUSR1); kill(getpid(), RT); }
    show("sent each 3 times");
    sigprocmask(SIG_SETMASK, &old, NULL);       /* unblock: delivery now */
    show("after unblocking");
    printf("SIGUSR1 = %d, SIGRTMIN = %d, SIGRTMAX = %d\n", SIGUSR1, RT, SIGRTMAX);
    return 0;
}

একটি আসল রানের আউটপুট:

blocked, nothing sent  pending: USR1=no  RT=no  | handler ran: USR1 0, RT 0
sent each 3 times      pending: USR1=yes RT=yes | handler ran: USR1 0, RT 0
after unblocking       pending: USR1=no  RT=no  | handler ran: USR1 1, RT 3
SIGUSR1 = 10, SIGRTMIN = 34, SIGRTMAX = 64

দ্বিতীয় লাইনে দুটো signal-ই pending, কিন্তু কোনো handler চলেনি — block-এর কারণে। sigpending() শুধু বলতে পারে “আছে কি নেই”, কতগুলো তা নয়। আসল পার্থক্য তৃতীয় লাইনে: unblock করার পর SIGUSR1-এর handler চলেছে ১ বার (তিনটি মিলে একটি হয়ে গেছে), আর real-time signal-এর handler ৩ বার — প্রতিটি আলাদাভাবে সারিতে ছিল। শেষ লাইন অংশ ৩-এর টেবিল মিলিয়ে দেয়: এই Linux x86-64 মেশিনে SIGUSR1 = 10, আর glibc-র দেখানো SIGRTMIN = 34. লক্ষ করুন handler-এ শুধু counter বাড়ানো হয়েছে, printf নয় — কেন, তা অংশ ৬-এ।

এই “জমা না হওয়া”র সবচেয়ে চেনা ফল দেখা যায় SIGCHLD-এ। একটি সার্ভারের দুটি child প্রায় একসাথে শেষ হলে parent একটিমাত্র SIGCHLD পেতে পারে। Handler যদি ধরে নেয় “একটি signal = একটি child” আর একবারই wait করে, দ্বিতীয় child zombie হয়ে পড়ে থাকবে। সমাধান অংশ ৬-এ।

৫ · Signal ধরা — signal() বনাম sigaction()

প্রতিটি signal-এর জন্য প্রসেস তিনটি “disposition”-এর একটি বেছে নিতে পারে: SIG_DFL (default action), SIG_IGN (উপেক্ষা), অথবা নিজের একটি ফাংশন — signal handler. এটি বসানোর দুটি API আছে।

signal() — পুরোনো, সরল

ISO C-র অংশ, এক লাইনে লেখা যায়: signal(SIGINT, on_int). কিন্তু এর আচরণ সিস্টেম-ভেদে আলাদা: handler চলার পর default-এ ফিরবে কি না, বাধা পাওয়া system call আবার শুরু হবে কি না — নির্দিষ্ট নয়। glibc ডিফল্টে BSD-ধাঁচের নির্ভরযোগ্য আচরণ দেয়, কিন্তু কিছু compiler option-এ পুরোনো System V আচরণ পায়।

শুধু SIG_IGN / SIG_DFL বসাতে চলে

sigaction() — নির্ভরযোগ্য, স্পষ্ট

POSIX-এর API. একটি struct sigaction-এ আপনি নিজে ঠিক করেন: handler (sa_handler বা sa_sigaction), handler চলাকালীন আর কোন signal block হবে (sa_mask), আর আচরণের flag (sa_flags). সব POSIX সিস্টেমে একই অর্থ।

নতুন কোডে সবসময় এটি

গুরুত্বপূর্ণ কয়েকটি flag:

  • SA_RESTART: handler চলার সময় প্রসেস যদি কোনো “ধীর” system call-এ (টার্মিনাল বা pipe থেকে read, wait) আটকে থাকে, handler শেষে kernel সেই call নিজে থেকে আবার শুরু করে। না দিলে call ফেরে −1, errno = EINTR (অংশ ৬)।
  • SA_SIGINFO: তিন-argument-এর handler void h(int sig, siginfo_t *info, void *ctx) চলে। info-তে থাকে কে পাঠাল (si_pid, si_uid), কেন (si_code), আর SIGSEGV-এর ক্ষেত্রে কোন ঠিকানায় ভুল হয়েছিল (si_addr).
  • SA_NOCLDSTOP, SA_NOCLDWAIT: SIGCHLD-এর জন্য — child থামলে signal নয়, শুধু শেষ হলে; অথবা child-কে zombie-ই না হতে দেওয়া।
  • SA_RESETHAND, SA_NODEFER: পুরোনো unreliable আচরণ ইচ্ছে করে চাইলে — সাধারণত দরকার হয় না।

এবার একটি সম্পূর্ণ উদাহরণ। প্রোগ্রামটি একটি লুপে “কাজ” করে, আর তৃতীয় কাজের পর নিজেকেই SIGINT পাঠায় — যেন কেউ Ctrl+C চেপেছে। Handler শুধু একটি flag বসায়; লুপ সেই flag দেখে ভদ্রভাবে থামে। শেষে SIG_IGN বসিয়ে দেখায়, উপেক্ষিত signal কিছুই করে না।

#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

static volatile sig_atomic_t stop = 0;      /* the only thing we share */

static void on_sigint(int sig) {
    (void)sig;
    static const char msg[] = "  [handler] SIGINT arrived, setting stop = 1\n";
    write(STDOUT_FILENO, msg, sizeof msg - 1);  /* write() is async-signal-safe */
    stop = 1;
}

int main(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = on_sigint;
    sigemptyset(&sa.sa_mask);           /* block nothing extra in the handler */
    sa.sa_flags = SA_RESTART;           /* restart slow syscalls after it */
    if (sigaction(SIGINT, &sa, NULL) == -1) { perror("sigaction"); return 1; }

    printf("SIGINT is signal number %d\n", SIGINT);
    for (int job = 1; !stop; job++) {   /* main loop checks the flag */
        printf("working on job %d\n", job);
        fflush(stdout);
        if (job == 3) kill(getpid(), SIGINT);   /* as if Ctrl+C was pressed */
    }
    printf("main: flag seen, saving state and exiting cleanly\n");

    signal(SIGINT, SIG_IGN);            /* now ignore it completely */
    raise(SIGINT);
    printf("main: raise(SIGINT) while ignored -> nothing happened\n");
    return 0;
}

একটি আসল রানের আউটপুট:

SIGINT is signal number 2
working on job 1
working on job 2
working on job 3
  [handler] SIGINT arrived, setting stop = 1
main: flag seen, saving state and exiting cleanly
main: raise(SIGINT) while ignored -> nothing happened

তৃতীয় কাজের পর kill(getpid(), SIGINT) চলল। একটি থ্রেডের প্রসেসে POSIX নিশ্চিত করে যে নিজেকে পাঠানো unblocked signal kill ফেরার আগেই পৌঁছাবে — তাই handler-এর লাইনটি ঠিক “job 3”-এর পরে এসেছে। Handler নিজে কোনো বড় কাজ করেনি: একটি লাইন write দিয়ে লিখেছে আর stop = 1 করেছে। লুপের শর্ত !stop পরের বার মিথ্যা হলো, তাই “job 4” আর আসেনি, আর cleanup-এর কাজটি হয়েছে সাধারণ কোডে, যেখানে printf বা ফাইল লেখা নিরাপদ। শেষ লাইন দেখায়, SIG_IGN থাকলে raise(SIGINT) প্রসেসকে থামায় না। এখানে signal() ব্যবহার ঠিক আছে, কারণ শুধু SIG_IGN বসানো হচ্ছে; handler বসাতে sigaction. প্রতিটি printf-এর পর fflush রাখা হয়েছে, কারণ এই রানারে stdout একটি pipe — নইলে handler-এর write করা লাইন buffer-এ বসে থাকা printf-এর আগে চলে আসত (পর্ব ৫-এর সেই buffering).

৬ · Handler-এর ভেতরে কী করা নিরাপদ

Signal handler সাধারণ ফাংশনের মতো দেখতে, কিন্তু সে চলে একটি অদ্ভুত মুহূর্তে — main-এর যেকোনো instruction-এর মাঝখানে. main হয়তো তখন printf-এর ভেতরে, stdout-এর buffer অর্ধেক বদলেছে আর buffer-এর lock ধরে রেখেছে। বা malloc-এর ভেতরে heap-এর তালিকা অর্ধেক জোড়া। এখন handler-ও যদি printf বা malloc ডাকে?

রেস্তোরাঁর রান্নাঘরে একটিই চুলা, বাবুর্চি একটি হাঁড়িতে বিরিয়ানি নাড়ছেন, হাতা হাঁড়ির ভেতর। হঠাৎ ম্যানেজার এসে বললেন, “এখনই এক কাপ চা বানান”। বাবুর্চি একই চুলা আর একই হাতা দিয়ে চা বানাতে গেলে বিরিয়ানির হাঁড়ি সরাতে হবে — অর্ধেক রান্না করা অবস্থায়, যেখানে সবকিছু এলোমেলো। বুদ্ধিমান বাবুর্চি একটি চিরকুটে লিখে রাখেন “চা চাই”, বিরিয়ানি নামিয়ে তারপর চা করেন। Handler-এ flag বসানো ঠিক সেই চিরকুট।

বাস্তবে যা হতে পারে: (১) Deadlock — glibc-র printf stdout-এর একটি lock নেয়; main সেই lock ধরে থাকা অবস্থায় handler আবার একই lock চাইলে প্রসেস চিরকাল আটকে থাকে। (২) নষ্ট ডেটা — lock না থাকলেও buffer-এর অর্ধেক-বদলানো অবস্থা handler আরও এলোমেলো করে দেয়, লেখা দুবার আসে বা হারায়। malloc-এর ক্ষেত্রে heap-এর কাঠামো নষ্ট হয়ে পরে অদ্ভুত crash. সমস্যাটি সবচেয়ে বিপজ্জনক কারণ এটি কদাচিৎ ঘটে: হাজারবার ঠিক চলে, একবার — signal ঠিক ভুল মুহূর্তে এলে — সার্ভার আটকে যায়।

মূল ধারণা

Handler থেকে শুধু async-signal-safe ফাংশন ডাকা যায় — যেগুলো POSIX নিশ্চিত করে যে যেকোনো মুহূর্তে বাধা পেয়ে আবার ঢুকলেও ঠিক চলবে। তালিকাটি man 7 signal-safety-তে আছে। নিরাপদ: write, read, _exit, kill, waitpid, sigprocmask, open, close. নিরাপদ নয়: printf ও পুরো stdio, malloc/free, exit (এটি stdio flush করে), বেশিরভাগ pthread ফাংশন। সবচেয়ে ভালো নকশা: handler-এ শুধু একটি flag বসান, আসল কাজ main-এ।

Flag-এর ধরন: volatile sig_atomic_t

Flag-টি সাধারণ int নয়, volatile sig_atomic_t — দুটি শব্দেরই কাজ আছে। sig_atomic_t এমন একটি পূর্ণসংখ্যার ধরন, যা একটি instruction-এই পড়া বা লেখা হয়; signal এসে অর্ধেক-লেখা মান দেখতে পায় না। volatile compiler-কে বলে, “এই ভ্যারিয়েবল বাইরে থেকে যেকোনো মুহূর্তে বদলাতে পারে, তাই প্রতিবার মেমরি থেকেই পড়তে হবে”। না দিলে optimizer দেখে লুপের ভেতরে stop কেউ বদলায় না, তাই মানটি একবার register-এ তুলে চিরকাল সেটিই দেখে — লুপ কখনো থামে না। -O0-এ ঠিক চলে, -O2-এ আটকে যায় — এমন বাগ প্রায়ই এভাবেই জন্মায়। (মনে রাখুন, থ্রেডের মধ্যে ভাগ করা ডেটার জন্য volatile যথেষ্ট নয়; সেখানে atomic আর lock লাগে — পর্ব ২৩।)

EINTR — handler-এর পর বাধা পাওয়া system call

প্রসেস ধরুন read(0, buf, 100)-এ ব্যবহারকারীর ইনপুটের অপেক্ষায় ঘুমিয়ে আছে। এমন সময় একটি signal এল আর handler চলল। Handler ফেরার পর read-এর কী হবে? দুটি সম্ভাবনা:

  • SA_RESTART ছাড়া: read ফেরে −1, errno = EINTR (“Interrupted system call”). প্রোগ্রামকে নিজে আবার ডাকতে হয়, নইলে সে ভাববে আসল ত্রুটি ঘটেছে।
  • SA_RESTART দিলে: kernel নিজে থেকে read আবার শুরু করে, প্রোগ্রাম কিছুই টের পায় না। তবে কিছু call — যেমন poll, select, epoll_wait আর nanosleep-এর মতো ঘুমের call — Linux-এ SA_RESTART থাকলেও আবার শুরু হয় না, EINTR দেয়।

এজন্য নির্ভরযোগ্য systems কোডে এই ধাঁচ খুব চেনা: while ((n = read(fd, buf, len)) == -1 && errno == EINTR) ;. আর মাঝে মাঝে EINTR-ই কাম্য: একটি সার্ভার SA_RESTART ছাড়া SIGTERM ধরলে তার accept() বা poll() EINTR দিয়ে ফেরে, তখন লুপ flag দেখে বন্ধের প্রস্তুতি নিতে পারে।

SIGCHLD দিয়ে zombie পরিষ্কার

পর্ব ১০-এ দেখেছেন, child শেষ হলে parent wait না করা পর্যন্ত সে zombie হয়ে থাকে। একটি সার্ভার, যে অনেক child চালায়, প্রতিবার আটকে থেকে wait করতে পারে না। তাই সে SIGCHLD-এর handler বসায়, আর handler-এর ভেতরে যতগুলো child শেষ হয়েছে সবাইকে তোলে। এটি হাতে-লেখা কোড (চালানো যায় না, তবে বাস্তবে এভাবেই লেখা হয়):

#include <errno.h>
#include <signal.h>
#include <sys/wait.h>

static void on_sigchld(int sig) {
    (void)sig;
    int saved = errno;                 /* waitpid may change errno: save it */
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;                              /* reap EVERY finished child, not one */
    errno = saved;                     /* main must not see a changed errno */
}

/* in main():
   struct sigaction sa = {0};
   sa.sa_handler = on_sigchld;
   sigemptyset(&sa.sa_mask);
   sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
   sigaction(SIGCHLD, &sa, NULL);                                   */

তিনটি বিষয় খেয়াল করুন। লুপ: দুটি child প্রায় একসাথে শেষ হলে SIGCHLD একবারই আসতে পারে (অংশ ৪), তাই একবার নয়, যতক্ষণ পাওয়া যায় ততক্ষণ waitpid. WNOHANG: আর কোনো শেষ-হওয়া child না থাকলে আটকে না থেকে 0 ফেরত দেয় — handler-এ ঘুমিয়ে পড়া চলবে না। errno সংরক্ষণ: main হয়তো ঠিক তখন একটি ব্যর্থ call-এর errno পড়তে যাচ্ছিল; handler-এর waitpid সেটি বদলে দিলে main ভুল কারণ দেখবে। আর যদি child-এর exit status একেবারেই দরকার না হয়, POSIX-এ আরও সহজ পথ আছে: SIGCHLD-কে স্পষ্টভাবে SIG_IGN করলে বা SA_NOCLDWAIT দিলে child-রা zombie-ই হয় না।

৭ · প্রসেস থামানো — kill, SIGTERM বনাম SIGKILL, আর exit status

Shell-এর kill কমান্ড শুধু kill() system call-এর একটি সহজ রূপ। কয়েকটি ব্যবহার:

kill 4100            # default: SIGTERM (15), a polite request
kill -TERM 4100      # same thing, written out
kill -INT 4100       # like Ctrl+C for that process
kill -9 4100         # SIGKILL: last resort, no cleanup possible
kill -STOP 4100      # freeze it; kill -CONT 4100 to continue
kill -TERM -- -4100  # negative number: the whole process group 4100
kill -l              # list all signal names and numbers
pkill -TERM nginx    # by name (procps); killall does the same

প্রথম নিয়মটি পেশাদার সিস্টেম প্রশাসকেরা মুখস্থ রাখেন: আগে SIGTERM, অপেক্ষা, তারপরই কেবল SIGKILL. SIGTERM প্রোগ্রামকে সুযোগ দেয় — অর্ধেক লেখা ফাইল শেষ করা, ডেটাবেসের লেনদেন (transaction) গুছিয়ে নেওয়া, temp ফাইল মোছা, client-কে “বন্ধ হচ্ছি” জানানো। সরাসরি kill -9 সেই সুযোগ কেড়ে নেয়।

১২৮ + n — signal-এ মৃত্যুর চিহ্ন

পর্ব ১০-এ দেখেছেন, parent waitpid() দিয়ে child-এর শেষ হওয়ার খবর পায়, আর WIFEXITED / WEXITSTATUS দিয়ে স্বাভাবিক exit status পড়ে। Child signal-এ মারা গেলে WIFSIGNALED সত্য হয়, আর WTERMSIG বলে কোন signal. Shell সেই তথ্য একটি সংখ্যায় বসায়: $? = ১২৮ + signal নম্বর.

Signalনম্বরShell-এ $?সাধারণত মানে
SIGINT2130কেউ Ctrl+C চেপেছে
SIGABRT6134প্রোগ্রাম নিজে abort করেছে (assert, double free ধরা পড়া)
SIGKILL9137kill -9, OOM killer, বা timeout-এর পর জোর করে বন্ধ
SIGSEGV11139মেমরির বাগ
SIGPIPE13141পাঠক-বিহীন pipe-এ লেখা
SIGTERM15143ভদ্র অনুরোধে বন্ধ, কিন্তু প্রোগ্রাম নিজে সামলায়নি

এজন্যই Kubernetes বা Docker-এ “exit code 137” দেখলে অভিজ্ঞ প্রকৌশলী প্রথমেই জিজ্ঞেস করেন: “মেমরি সীমা ছাড়িয়েছিল কি (OOM killer, পর্ব ৩৮), নাকি বন্ধ হতে বেশি সময় নিয়েছিল?” — দুটোই SIGKILL. আবার মনে রাখুন, এই ১২৮ + n নিয়মটি shell-এর রীতি। Kernel-এর আসল wait status আলাদা একটি সংখ্যা, যা macro দিয়ে পড়তে হয়; আর কোনো প্রোগ্রাম চাইলে নিজেই exit(137) দিয়ে একই সংখ্যা দেখাতে পারে।

এবার নিজের চোখে দেখি। প্রোগ্রামটি SIGTERM-এর একটি handler বসিয়ে দুটি child তৈরি করে (fork-এর পর child handler উত্তরাধিকারসূত্রে পায়)। প্রথম child-কে SIGTERM, দ্বিতীয়টিকে SIGKILL পাঠায়, আর waitpid দিয়ে দুজনের পরিণতি পড়ে। শুরুতেই চেষ্টা করে SIGKILL-এর handler বসাতে:

#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
static void on_term(int sig) {
    (void)sig;
    static const char m[] = "  child: got SIGTERM, closing files, exit(3)\n";
    write(STDOUT_FILENO, m, sizeof m - 1);
    _exit(3);                              /* _exit is async-signal-safe */
}

static void run_child(const char *name, int sig) {
    fflush(stdout);                        /* empty the buffer before fork */
    pid_t pid = fork();
    if (pid == 0) for (;;) pause();        /* child: just wait for signals */
    int st;
    kill(pid, sig);  waitpid(pid, &st, 0);
    if (WIFEXITED(st))
        printf("%s: exited normally, status %d\n", name, WEXITSTATUS(st));
    else if (WIFSIGNALED(st))
        printf("%s: killed by signal %d (%s); a shell shows $? = %d\n",
               name, WTERMSIG(st), strsignal(WTERMSIG(st)), 128 + WTERMSIG(st));
}

int main(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = on_term;  sigemptyset(&sa.sa_mask);
    sigaction(SIGTERM, &sa, NULL);         /* children inherit the handler */
    if (sigaction(SIGKILL, &sa, NULL) == -1)
        printf("sigaction(SIGKILL) refused: %s\n", strerror(errno));
    run_child("child A (SIGTERM)", SIGTERM);   /* polite request */
    run_child("child B (SIGKILL)", SIGKILL);   /* cannot be caught */
    return 0;
}

একটি আসল রানের আউটপুট:

sigaction(SIGKILL) refused: Invalid argument
  child: got SIGTERM, closing files, exit(3)
child A (SIGTERM): exited normally, status 3
child B (SIGKILL): killed by signal 9 (Killed); a shell shows $? = 137

প্রথম লাইনেই kernel-এর প্রত্যাখ্যান: SIGKILL-এর handler বসানো যায় না, EINVAL (“Invalid argument”). Child A-র ক্ষেত্রে handler চলেছে — নিজের বার্তা লিখে _exit(3) করেছে — তাই parent দেখছে স্বাভাবিক exit, status ৩. অর্থাৎ যে প্রোগ্রাম SIGTERM সামলায়, তার exit code সে নিজেই বেছে নেয়। Child B-র কাছেও একই handler ছিল, কিন্তু SIGKILL-এ কোনো handler চলে না: কোনো বার্তা নেই, WIFSIGNALED সত্য, signal ৯ (“Killed”), আর shell হলে $? হতো ১৩৭. দুটি ছোট খুঁটিনাটি: handler-এ exit নয়, _exit, কারণ exit stdio flush করে, যা async-signal-safe নয়; আর প্রতিটি fork-এর আগে fflush(stdout), যাতে buffer-এ বসে থাকা লেখা child-এ কপি হয়ে দুবার না আসে (পর্ব ১০)। Child-রা pause()-এ অপেক্ষা করছিল; সেটি এমন একটি call, যা signal আসা পর্যন্ত ঘুমায়।

Shell script-এ signal — trap

Bash script-ও signal ধরতে পারে trap দিয়ে। trap 'কমান্ড' TERM মানে “SIGTERM এলে এই কমান্ড চালানো হবে”; আর EXIT একটি ছদ্ম-signal, যা script যেভাবেই শেষ হোক (SIGKILL ছাড়া) একবার চলে — temp ফাইল মোছার আদর্শ জায়গা। নিচের script-এ একটি “worker” trap বসিয়ে চলে, আর মূল script তাকে SIGTERM পাঠায়; তারপর trap ছাড়া প্রসেসের exit code দেখায়:

echo "== names and numbers =="
echo "INT=$(kill -l INT) TERM=$(kill -l TERM) KILL=$(kill -l KILL) CHLD=$(kill -l CHLD)"
echo "exit code 143 means: SIG$(kill -l 143), 137 means: SIG$(kill -l 137)"

echo "== a worker that traps TERM and EXIT =="
bash -c '
  trap "echo \"  worker: SIGTERM received, saving progress\"; exit 0" TERM
  trap "echo \"  worker: EXIT trap, temp files removed\"" EXIT
  echo "  worker: started"
  while true; do sleep 0.1; done
' &
w=$!
sleep 0.3
kill -TERM "$w"                 # same as plain: kill $w
s=0; wait "$w" || s=$?
echo "worker exit status: $s (handled, so it chose 0)"

echo "== processes with no trap =="
( sleep 5 & p=$!; kill -TERM "$p"; s=0; wait "$p" || s=$?
  echo "sleep + SIGTERM -> exit status $s"
  sleep 5 & p=$!; kill -KILL "$p"; s=0; wait "$p" || s=$?
  echo "sleep + SIGKILL -> exit status $s" ) 2>/dev/null

একটি আসল রানের আউটপুট:

== names and numbers ==
INT=2 TERM=15 KILL=9 CHLD=17
exit code 143 means: SIGTERM, 137 means: SIGKILL
== a worker that traps TERM and EXIT ==
  worker: started
  worker: SIGTERM received, saving progress
  worker: EXIT trap, temp files removed
worker exit status: 0 (handled, so it chose 0)
== processes with no trap ==
sleep + SIGTERM -> exit status 143
sleep + SIGKILL -> exit status 137

kill -l দুই দিকেই কাজ করে: নাম দিলে নম্বর, আর ১২৮-এর বেশি exit code দিলে সেটি থেকে signal-এর নাম — debugging-এ খুব কাজের কৌশল। Worker SIGTERM পেয়ে প্রথমে TERM-এর trap চালিয়েছে, সেখানে exit 0 থেকে EXIT trap চলেছে — ক্রমটি লক্ষ করুন। একটি সূক্ষ্মতা: bash একটি foreground কমান্ড (এখানে sleep 0.1) চলার সময় signal পেলে trap চালায় সেই কমান্ড শেষ হওয়ার পর; তাই লম্বা sleep 60 থাকলে trap ৬০ সেকেন্ড দেরিতে চলত। trap ছাড়া sleep-এর দুই পরিণতি ঠিক নিয়মমাফিক: SIGTERM-এ ১৪৩, SIGKILL-এ ১৩৭. শেষ অংশে 2>/dev/null আছে, কারণ bash signal-এ মারা যাওয়া job-এর জন্য stderr-এ “Terminated” / “Killed” ধরনের বার্তা লেখে।

৮ · Graceful shutdown — docker stop আর systemctl stop-এর ভেতরে

বাস্তব সার্ভারে প্রসেস বন্ধ করতে প্রায় কেউ হাতে kill দেন না; দেন service manager বা container runtime. সবার পদ্ধতি মূলত এক: SIGTERM → নির্দিষ্ট সময় অপেক্ষা → তখনও বেঁচে থাকলে SIGKILL. একে বলে graceful shutdown.

টুলপ্রথম signalডিফল্ট অপেক্ষাবদলানোর উপায়
docker stopcontainer-এর STOPSIGNAL (ডিফল্ট SIGTERM), container-এর PID 1-কে১০ সেকেন্ডdocker stop -t 30; Dockerfile-এ STOPSIGNAL
systemctl stopKillSignal= (ডিফল্ট SIGTERM), service-এর cgroup-এর প্রসেসগুলোকেTimeoutStopSec, সাধারণত ৯০ সেকেন্ডunit ফাইলে TimeoutStopSec=, KillMode=
Kubernetes pod বন্ধ(preStop hook থাকলে আগে সেটি) তারপর SIGTERM৩০ সেকেন্ডterminationGracePeriodSeconds
Linux shutdownsystemd সব service-কে উপরের নিয়মেপ্রতিটি unit-এর নিজস্ব—

একটি ভালো আচরণের সার্ভার SIGTERM পেলে সাধারণত এই কাজগুলো করে: নতুন সংযোগ নেওয়া বন্ধ (listening socket বন্ধ, বা load balancer-কে “আমি আর প্রস্তুত নই” জানানো), চলমান অনুরোধগুলো শেষ করা, লগ আর ডেটা flush, ডেটাবেসের সংযোগ বন্ধ, তারপর exit 0. Kubernetes-এর মতো পরিবেশে এটি না করলে প্রতিটি deploy-এর সময় কিছু ব্যবহারকারী ভাঙা অনুরোধ পান।

নিচের ডায়াগ্রামে দুটি container বন্ধ করা দেখুন — একটি ঠিকভাবে বানানো, আরেকটি একটি পরিচিত ভুলসহ:

1$ docker stop web        # PID 1 = node server.js2dockerd -> PID 1: SIGTERM3handler: stopping = 1; close listener4finish 12 requests, flush logs, close DB5exit(0)  -> container exited (0)6$ docker stop old-web    # PID 1 = /bin/sh -c "node ..."7sh (PID 1): no handler -> SIGTERM dropped810 s timeout -> SIGKILL9container exited (137)
অবস্থা
সময়
পাঠানো signal
অ্যাপ
exit code
  1. docker stop web, সময় ০. এই container-এর প্রথম প্রসেস, অর্থাৎ container-এর ভেতরের PID 1, সরাসরি node server.js — কারণ Dockerfile-এ exec রূপে লেখা: CMD ["node", "server.js"]. ডিফল্ট অপেক্ষার সীমা ১০ সেকেন্ড।
  2. SIGTERM পৌঁছায়। Docker daemon container-এর PID 1-কে SIGTERM পাঠায়। অ্যাপের কাছে এটি এখন pending, পরের মুহূর্তেই handler চলবে।
  3. Handler শুধু flag বসায়। অংশ ৬-এর নিয়মে handler-এ বড় কাজ নয়: stopping = 1, আর ইভেন্ট-লুপ নতুন সংযোগ নেওয়া বন্ধ করে। (Node.js-এর process.on('SIGTERM', …) ভেতরে ঠিক এই ধাঁচে কাজ করে — আসল callback চলে সাধারণ ইভেন্ট-লুপে।)
  4. শেষ কাজ গোছানো, ০–৩ সেকেন্ড। চলমান ১২টি অনুরোধের উত্তর দেওয়া, লগ flush, ডেটাবেস সংযোগ বন্ধ। সময়সীমা ১০ সেকেন্ড, কাজ শেষ ৩ সেকেন্ডে — যথেষ্ট সময় হাতে।
  5. পরিষ্কারভাবে শেষ। অ্যাপ exit(0) করল, container-এর exit code 0. কোনো ব্যবহারকারীর অনুরোধ মাঝপথে ভাঙেনি, SIGKILL লাগেনি। docker stop container-এর নাম ছাপিয়ে ফেরে।
  6. দ্বিতীয় container, সময় ০. এবার Dockerfile-এ shell রূপে লেখা: CMD node server.js. Docker এটিকে /bin/sh -c "node server.js" হিসেবে চালায়, তাই PID 1 হলো sh, আর node তার child. (কিছু shell একটিমাত্র কমান্ড পেলে নিজেই সেটি exec করে ফেলে, তখন সমস্যাটি দেখা যায় না; কিন্তু সব shell বা সব image-এ এমন হয় না, তাই এর ওপর ভরসা না করাই নিরাপদ।)
  7. SIGTERM হারিয়ে যায়। Linux-এর একটি বিশেষ নিয়ম: একটি PID namespace-এর PID 1 (init) যে signal-এর handler বসায়নি, সেই signal-এর default action তার ওপর খাটে না — signal-টি ফেলে দেওয়া হয়। (SIGKILL আর SIGSTOP বাইরের namespace থেকে এলে ব্যতিক্রম।) sh handler বসায়নি, আর node-কে signal পৌঁছেও দেয় না। ফলে node কিছুই জানে না, ১০ সেকেন্ড ধরে চলতেই থাকে।
  8. ১০ সেকেন্ড পূর্ণ — SIGKILL. সময়সীমা পেরোতেই Docker SIGKILL পাঠায়। এটি ধরা যায় না, PID 1-এর জন্যও বাইরে থেকে এলে খাটে। চলমান অনুরোধ মাঝপথে ভাঙে, লগের শেষ অংশ হারাতে পারে।
  9. Exit code ১৩৭. ১২৮ + ৯. “প্রতিবার ঠিক ১০ সেকেন্ড লাগে আর 137 আসে” — এই লক্ষণ দেখলেই PID 1 আর signal-এর সমস্যা সন্দেহ করুন। সমাধান: exec রূপের CMD, entrypoint script-এ শেষ কমান্ডটি exec node server.js, অথবা docker run --init (একটি ছোট init, যা signal পৌঁছে দেয় আর zombie তোলে)।

systemd-এও একই গল্প, শুধু শব্দ আলাদা। systemctl stop ডিফল্টে service-এর cgroup-এর সব প্রসেসকে SIGTERM দেয় (পরে SIGCONT-ও, যাতে থেমে থাকা প্রসেস signal-টি সামলাতে পারে), TimeoutStopSec পর্যন্ত অপেক্ষা করে, তারপর SIGKILL. সময় পেরোলে journal-এ এমন লাইন দেখা যায় (উদাহরণ; সংস্করণভেদে ভাষা সামান্য আলাদা):

myapp.service: State 'stop-sigterm' timed out. Killing.
myapp.service: Killing process 2311 (myapp) with signal SIGKILL.
myapp.service: Main process exited, code=killed, status=9/KILL
myapp.service: Failed with result 'timeout'.

অনেক daemon আরও কিছু signal-কে নিজস্ব অর্থ দেয়। যেমন nginx-এ SIGHUP মানে “config আবার পড়ুন” (চলমান সংযোগ না ভেঙে), আর SIGQUIT মানে graceful shutdown; nginx -s reload ভেতরে আসলে master প্রসেসকে SIGHUP-ই পাঠায়। কোন signal-এ কী হয়, তা প্রতিটি প্রোগ্রামের ডকুমেন্টেশনে দেখে নিতে হয় — সবার নিয়ম এক নয়।

৯ · Windows আর macOS-এ — সবখানে কি signal আছে?

macOS আর FreeBSD-এর মতো BSD-বংশের সিস্টেমে signal প্রায় হুবহু একই — একই নাম, একই sigaction, তবে অংশ ৩-এ যেমন বলেছি, অনেক নম্বর আলাদা। কিছু ছোট পার্থক্যও আছে, যেমন BSD-তে SIGINFO নামে একটি বাড়তি signal (টার্মিনালে Ctrl+T), যা Linux-এ নেই।

Windows-এর গল্প সম্পূর্ণ আলাদা। Windows NT kernel-এ Unix-ধাঁচের signal নেই। একই কাজগুলো সেখানে ভিন্ন ভিন্ন ব্যবস্থায় হয় (বিস্তারিত পর্ব ৫৬-এ):

Unix-এWindows-এ কাছাকাছি ব্যবস্থা
Ctrl+C → SIGINTConsole control event: CTRL_C_EVENT, CTRL_BREAK_EVENT, CTRL_CLOSE_EVENT; প্রোগ্রাম SetConsoleCtrlHandler() দিয়ে handler বসায়, যা Windows একটি নতুন থ্রেডে চালায় — Unix-এর মতো চলমান কোডে বাধা দিয়ে নয়
SIGSEGV, SIGFPEStructured Exception Handling (SEH): EXCEPTION_ACCESS_VIOLATION ইত্যাদি, C-তে __try/__except দিয়ে ধরা যায়
SIGKILLTerminateProcess(); taskkill /F বা PowerShell-এর Stop-Process সাধারণত এটিই করে
SIGTERM (ভদ্র অনুরোধ)নির্দিষ্ট কিছু নেই: GUI অ্যাপকে window message (WM_CLOSE), service-কে Service Control Manager-এর stop অনুরোধ, console অ্যাপকে Ctrl event
SIGCHLDনেই; parent প্রসেসের handle-এ WaitForSingleObject() করে

Microsoft-এর C runtime একটি signal() ফাংশন দেয় (SIGINT, SIGTERM, SIGSEGV ইত্যাদি নামসহ), কারণ ISO C এটি দাবি করে; কিন্তু এটি ওপরের ব্যবস্থাগুলোর ওপর একটি পাতলা অনুকরণ, POSIX-এর পূর্ণ আচরণ নয় — যেমন Windows নিজে কখনো কোনো প্রসেসকে SIGTERM পাঠায় না। Cygwin বা পুরোনো WSL1-এর মতো স্তর Windows-এর ওপর POSIX signal অনুকরণ করেছে; WSL2-এ ভেতরে একটি আসল Linux kernel চলে, তাই সেখানে signal পুরোপুরি Linux-এর মতো। তাই cross-platform প্রোগ্রাম লিখলে “Ctrl+C-তে ভদ্রভাবে বন্ধ” কাজটি দুই OS-এ আলাদাভাবে লিখতে হয়, অথবা এমন library ব্যবহার করতে হয় যা দুটোকে এক interface-এ আনে।

আরও গভীরে: Linux-এ প্রতিটি থ্রেডের task_struct-এ নিজের pending তালিকা আর blocked mask থাকে, আর পুরো প্রসেসের জন্য একটি ভাগ করা pending তালিকা (signal_struct-এ)। Kernel থেকে user mode-এ ফেরার পথে (exit_to_user_mode) kernel TIF_SIGPENDING flag দেখে, তারপর handler-এর জন্য user stack-এ একটি “signal frame” বানায় — সেখানে সংরক্ষিত register আর পুরোনো mask — এবং instruction pointer handler-এর দিকে ঘোরায়; handler ফিরলে একটি ছোট trampoline rt_sigreturn syscall করে frame থেকে সব ফিরিয়ে আনে। লক্ষ্য-প্রসেস অন্য CPU-তে চলতে থাকলে kernel সেই CPU-কে একটি inter-processor interrupt পাঠিয়ে kernel-এ ঢুকতে বাধ্য করে, যাতে signal দেরিতে না পৌঁছায়। আধুনিক Linux signal-কে আরও নিরাপদ করার উপায়ও দেয়: signalfd (২০০৭, Linux 2.6.22) signal-কে একটি file descriptor থেকে পড়ার ঘটনা বানায়, আর pidfd_send_signal (Linux 5.1) PID পুনর্ব্যবহারের race ছাড়াই সঠিক প্রসেসকে signal পাঠায়।

১০ · AI-এর যুগে signal

“আমার C সার্ভারে Ctrl+C-তে ভদ্রভাবে বন্ধ হওয়ার কোড লিখে দিন” — AI প্রায় সবসময় একটি চলনসই উত্তর দেবে, আর আপনার ল্যাপটপে সেটি হাজারবার ঠিকঠাক চলবে। Signal-এর বাগ ঠিক এই কারণেই বিপজ্জনক: এরা সময়-নির্ভর, কদাচিৎ ঘটে, আর ছোট পরীক্ষায় ধরা পড়ে না। AI-এর লেখা কোডে যা প্রায়ই দেখা যায়:

AI-এর ভুলফলকীভাবে ধরবেন
Handler-এর ভেতরে printf, malloc, লগ-library বা exit()কদাচিৎ deadlock বা নষ্ট আউটপুটHandler-এর প্রতিটি কল man 7 signal-safety-র তালিকায় আছে কি না মেলান
Flag সাধারণ int বা bool, volatile নেই-O2-এ লুপ কখনো থামে নাvolatile sig_atomic_t দেখুন; -O2 দিয়ে compile করে পরীক্ষা করুন
sigaction-এর বদলে signal() দিয়ে handlerOS বা compiler option-ভেদে আলাদা আচরণhandler বসাতে sigaction চান
SIGCHLD handler-এ একবার wait()zombie জমে, বা handler আটকে যায়while (waitpid(-1, …, WNOHANG) > 0) লুপ আছে কি না
EINTR সামলানো নেইsignal এলে read “ব্যর্থ” হয়ে প্রোগ্রাম ভুলভাবে বন্ধপ্রতিটি ধীর syscall-এর −1-এ errno == EINTR দেখা হচ্ছে কি না
“signal দিয়ে ঘটনা গুনুন” — প্রতিটি SIGUSR1-এ counter++signal মিশে গিয়ে হিসাব কম আসেসাধারণ signal জমা হয় না; গোনার জন্য pipe, queue বা real-time signal
সমস্যা হলেই kill -9 বা pkill -9 পরামর্শঅর্ধেক লেখা ডেটা, lock ফাইল থেকে যাওয়াআগে SIGTERM, কারণ খুঁজুন; SIGKILL শেষ উপায়
Windows-এর প্রোগ্রামে SIGTERM handler-এর ওপর ভরসা, বা Dockerfile-এ shell-রূপের CMDবন্ধের সংকেত কখনো পৌঁছায় নালক্ষ্য-OS আর PID 1 কে, তা প্রম্পটে বলুন; docker stop-এর সময় মাপুন

AI-কে signal-সংক্রান্ত কোড লিখতে দিলে পরিবেশ আর নিরাপত্তার শর্ত স্পষ্ট করে দিন:

আমার পরিবেশ: Ubuntu 24.04, x86-64, gcc 13, glibc. প্রোগ্রামটি Docker container-এর ভেতরে PID 1 হিসেবে চলবে। একটি C সার্ভার-লুপের জন্য graceful shutdown কোড লিখে দাও: ১. শুধু sigaction ব্যবহার করবে; SIGTERM আর SIGINT ধরবে, SA_RESTART ছাড়া, যাতে poll() EINTR দিয়ে ফেরে। ২. Handler-এ শুধু একটি volatile sig_atomic_t flag বসাবে; printf, malloc, exit ডাকবে না। দরকার হলে শুধু write(). ৩. Child প্রসেস থাকলে SIGCHLD handler-এ errno সংরক্ষণ করে waitpid(-1, ..., WNOHANG) লুপে সবাইকে তুলবে। ৪. প্রতিটি ধীর syscall-এ EINTR আলাদাভাবে সামলাবে। ৫. gcc -Wall -Wextra -O2-এ কোনো warning আসবে না। শেষে বলো: handler-এর প্রতিটি কল কেন async-signal-safe, docker stop দিলে কত সেকেন্ডে আর কোন exit code-এ বন্ধ হওয়ার কথা, আর কোন পরীক্ষা চালিয়ে আমি এটি যাচাই করব।

AI-এর লেখা signal-কোড বা কমান্ড চালানোর আগে — চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • Signal কী, interrupt-এর সাথে মিল কোথায়? — প্রসেসকে পাঠানো asynchronous ঘটনা-সংকেত; interrupt যেমন CPU-কে থামিয়ে ISR চালায়, signal তেমনি প্রসেসকে থামিয়ে handler চালায়।
  • SIGTERM আর SIGKILL-এর পার্থক্য? — SIGTERM (১৫) ধরা যায়, প্রোগ্রাম cleanup করে বন্ধ হতে পারে; SIGKILL (৯) ধরা বা উপেক্ষা করা যায় না, kernel সঙ্গে সঙ্গে শেষ করে।
  • Blocked আর ignored signal-এর পার্থক্য? — blocked signal pending থাকে আর unblock হলে পৌঁছায়; ignored signal ফেলে দেওয়া হয়।
  • Handler-এ printf কেন নিষেধ? — printf async-signal-safe নয়; main printf-এর মাঝে থাকলে lock বা buffer-এর কারণে deadlock বা নষ্ট আউটপুট; write ব্যবহার করুন বা flag বসান।
  • Exit code ১৩৭ মানে কী? — ১২৮ + ৯, অর্থাৎ প্রসেস SIGKILL-এ মারা গেছে (প্রায়ই OOM killer বা timeout).

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১পুরোনো Unix-এর signal-কে “unreliable” বলা হয় কেন, আর সেই ইতিহাস আজকের কোডে কীভাবে টিকে আছে?

১৯৭০-এর দশকের Unix-এ (Version 7 পর্যন্ত) signal(SIGINT, h) দিয়ে handler বসানো যেত, কিন্তু signal পৌঁছানোর মুহূর্তে kernel disposition আবার SIG_DFL-এ ফিরিয়ে দিত। তাই প্রোগ্রামাররা handler-এর প্রথম লাইনেই আবার signal(SIGINT, h) লিখতেন। সমস্যা হলো handler শুরু হওয়া আর সেই লাইন চলার মাঝের ছোট্ট ফাঁক: ঠিক তখন দ্বিতীয় SIGINT এলে default action খাটত, আর প্রসেস মারা যেত। আরও ছিল: signal সাময়িকভাবে আটকে রাখার কোনো উপায় ছিল না — শুধু “ignore”, যাতে signal হারিয়ে যেত। আর “signal আসা পর্যন্ত অপেক্ষা” করতে গেলে flag দেখা আর pause() ডাকার মাঝে signal এসে পড়লে প্রসেস চিরকাল ঘুমিয়ে থাকত।

এগুলো সব race condition — পর্ব ২২-এর ভাষায়, এমন বাগ যা ঘটনার সময়ক্রমের ওপর নির্ভর করে। ১৯৮৩ সালের 4.2BSD এর সমাধান দেয়: handler চলার পর বসানোই থাকে, handler চলাকালীন একই signal আপনা থেকে block থাকে, আর প্রসেস নিজের signal mask বদলাতে পারে। AT&T-র System V আলাদা পথে কিছু সমাধান আনে, ফলে দুই ধারার আচরণ মেলেনি। ১৯৮৮ সালের POSIX.1 কমিটি নতুন একটি API বানিয়ে বিরোধ মেটায় — sigaction(), যেখানে প্রতিটি আচরণ flag দিয়ে স্পষ্ট করে বলতে হয়। sigsuspend() পুরোনো pause-race-এর সমাধান দেয়: mask বদলানো আর ঘুমানো একটিমাত্র atomic কাজে।

কিন্তু signal() মুছে ফেলা যায়নি, কারণ ISO C মানদণ্ডে এটি আছে আর লক্ষ লক্ষ পুরোনো প্রোগ্রাম এটি ব্যবহার করে। তাই আজও এর আচরণ “implementation-defined”: Linux-এ glibc ডিফল্টে BSD আচরণ দেয়, কিন্তু কঠোর ISO মোডে compile করলে (-std=c99-এর মতো option-এ feature macro বদলে গেলে) পুরোনো System V আচরণ ফিরে আসতে পারে। একই কোড এক মেশিনে ঠিক, আরেকটিতে মাঝে মাঝে মারা যায় — এমন বাগ খুঁজে পাওয়া কঠিন। Linux kernel-এর man 2 signal নিজেই এজন্য পরামর্শ দেয় handler বসাতে signal() এড়িয়ে চলার।

মূল উপলব্ধি: asynchronous ঘটনার API-তে প্রতিটি “ছোট ফাঁক” একদিন race হয়ে ধরা দেয়। sigaction-এর স্পষ্টতা সেই ইতিহাসের শিক্ষা; আর পুরোনো API টিকে থাকে সামঞ্জস্যের দায়ে — তাই নতুন কোডে সচেতনভাবে নতুনটি বাছতে হয়।

প্র ০২SIGKILL “অবশ্যই” প্রসেস শেষ করে — তবু কেন কখনো কখনো kill -9 দেওয়ার পরও প্রসেস ps-এ থেকে যায়?

SIGKILL-এর প্রতিশ্রুতি হলো, প্রসেস এটি ধরতে, উপেক্ষা করতে বা block করতে পারবে না। কিন্তু signal পৌঁছানোর জন্য প্রসেসকে kernel-এর এমন একটি জায়গায় থাকতে হয় যেখান থেকে signal দেখা সম্ভব। তিনটি পরিচিত ব্যতিক্রম আছে।

Zombie (STAT Z): প্রসেস আগেই মারা গেছে; kernel তার মেমরি আর ফাইল ফেরত নিয়েছে, রেখে দিয়েছে শুধু একটি ছোট রেকর্ড — PID আর exit status, যাতে parent wait() দিয়ে পড়তে পারে। মৃতকে আবার মারা যায় না। সমাধান parent-এর দিকে: parent-কে ঠিক করা, অথবা parent শেষ হলে zombie init-এর (বা subreaper-এর) কাছে যায়, যে নিয়মিত wait করে পরিষ্কার করে।

Uninterruptible sleep (STAT D): কিছু kernel-এর কাজ মাঝপথে থামানো নিরাপদ নয় — যেমন একটি ডিস্ক-ব্লক পড়া, যার ফলের ওপর kernel-এর ডেটা-কাঠামো নির্ভর করে। Kernel তখন প্রসেসকে এমন ঘুমে রাখে যেখান থেকে signal জাগাতে পারে না। সাধারণত এটি কয়েক মিলিসেকেন্ড; কিন্তু ডিস্ক নষ্ট হতে থাকলে বা NFS-এর মতো নেটওয়ার্ক ফাইল সিস্টেমের সার্ভার হারিয়ে গেলে প্রসেস মিনিটের পর মিনিট D-তে আটকে থাকে, আর SIGKILL pending হয়ে বসে থাকে। Linux 2.6.25 (২০০৮)-এ এর আংশিক সমাধান আসে TASK_KILLABLE অবস্থা দিয়ে: এমন ঘুম, যা সাধারণ signal-এ ভাঙে না কিন্তু মারাত্মক signal-এ ভাঙে; ধীরে ধীরে অনেক kernel-পথ এটি ব্যবহার শুরু করেছে।

Container-এর PID 1: অংশ ৮-এর নিয়মে namespace-এর ভেতর থেকে পাঠানো SIGKILL-ও PID 1 উপেক্ষা করে — নইলে ভেতরের যেকোনো প্রসেস পুরো container-এর init মেরে ফেলতে পারত। বাইরের (host-এর) namespace থেকে পাঠালে অবশ্য কাজ করে।

এর বাইরে একটি পুরোনো সমস্যাও আছে: PID পুনর্ব্যবহার। আপনি PID 4100 দেখে kill দেওয়ার আগের মুহূর্তে সেই প্রসেস শেষ হয়ে নতুন একটি প্রসেস একই PID পেলে ভুল প্রসেস মারা যায়। Linux 5.1 থেকে pidfd দিয়ে নির্দিষ্ট প্রসেসের একটি “handle” রেখে signal পাঠানো যায়, এই race ছাড়াই।

মূল উপলব্ধি: “ধরা যায় না” আর “তৎক্ষণাৎ ঘটে” এক কথা নয়। SIGKILL প্রসেসের ইচ্ছাকে হারাতে পারে, কিন্তু kernel-এর নিজের নিরাপত্তা-নিয়মকে নয় — আর ps-এ টিকে থাকা প্রসেস দেখলে আগে STAT কলামটি পড়ুন।

প্র ০৩Multi-thread প্রোগ্রামে signal কোন থ্রেডে যায়? অনেক অভিজ্ঞ প্রকৌশলী কেন বলেন “থ্রেড আর signal একসাথে মেশাবেন না”?

Signal-এর নকশা হয়েছিল এক-থ্রেডের প্রসেসের যুগে। থ্রেড (পর্ব ১৪) আসার পর POSIX নিয়ম ঠিক করে: disposition (handler, ignore, default) পুরো প্রসেসের — সব থ্রেডের এক; কিন্তু signal mask প্রতিটি থ্রেডের আলাদা (pthread_sigmask). আর signal দুই রকম: thread-directed, যেমন একটি থ্রেডের নিজের ভুলে তৈরি SIGSEGV বা pthread_kill() দিয়ে পাঠানো, যা সেই থ্রেডেই যায়; আর process-directed, যেমন kill() বা Ctrl+C, যা kernel এমন যেকোনো একটি থ্রেডকে দেয় যে signal-টি block করেনি। কোন থ্রেড পাবে, তা আগে থেকে বলা যায় না।

এতে সমস্যা বাড়ে। একটি থ্রেড হয়তো তখন একটি mutex ধরে রেখেছে; handler সেই থ্রেডেই চলে আর একই mutex চায় — deadlock. বেশিরভাগ pthread ফাংশন async-signal-safe নয়, তাই handler থেকে অন্য থ্রেডকে “জাগানো” যায় না (POSIX semaphore-এর sem_post একটি ব্যতিক্রম)। আর SA_RESTART না থাকলে যেকোনো থ্রেডের যেকোনো ধীর syscall EINTR পেতে পারে।

তাই প্রচলিত পরামর্শ: signal-কে asynchronous থেকে synchronous বানিয়ে ফেলুন। প্রোগ্রাম শুরুতেই, থ্রেড বানানোর আগে, main থ্রেডে সব গুরুত্বপূর্ণ signal block করে — নতুন থ্রেডেরা mask উত্তরাধিকারসূত্রে পায়। তারপর একটি আলাদা থ্রেড sigwait() বা sigwaitinfo()-এ বসে থাকে, আর signal এলে সেটি সাধারণ ফাংশনের ফেরত মান হিসেবে পায় — handler নেই, safe-unsafe-এর ঝামেলা নেই। Linux-এ signalfd আরও এক ধাপ এগিয়ে signal-কে একটি file descriptor-এ পড়ার ঘটনা বানায়, যা epoll-এর ইভেন্ট-লুপে বাকি সব ইভেন্টের সাথে মেলে। পুরোনো পোর্টেবল কৌশল “self-pipe trick”: handler একটি pipe-এ এক byte লেখে (write নিরাপদ), আর ইভেন্ট-লুপ সেই pipe-ও দেখে। Go, Rust-এর tokio বা Node.js-এর মতো runtime ভেতরে এসব কৌশলই ব্যবহার করে, তাই সেখানে আপনি signal পান একটি channel বা ইভেন্ট হিসেবে।

মূল উপলব্ধি: signal-এর সবচেয়ে কঠিন অংশ তার “যেকোনো মুহূর্তে বাধা দেওয়া”। আধুনিক নকশা সেই বাধাকে একটি নির্দিষ্ট জায়গায় নিয়ে আসে, যেখানে সাধারণ কোডের সব নিয়ম খাটে।

প্র ০৪ক্লাউডের যুগে graceful shutdown এত গুরুত্বপূর্ণ হয়ে উঠল কেন? একটি deploy-এর সময় signal-এর ভূমিকা কী?

আগে একটি সার্ভার মাসের পর মাস চলত, বন্ধ হতো কদাচিৎ। আজকের ক্লাউড-ব্যবস্থায় উল্টো: Kubernetes বা অনুরূপ ব্যবস্থায় প্রতিটি নতুন সংস্করণ deploy মানে পুরোনো container বন্ধ করে নতুন চালু করা; autoscaling ভিড় কমলে container কমায়; node রক্ষণাবেক্ষণে সরানো হয়। একটি ব্যস্ত দলে দিনে বহুবার এমন বন্ধ হওয়া সাধারণ ঘটনা। প্রতিবার যদি চলমান অনুরোধ মাঝপথে ভাঙে, তাহলে মোবাইল ব্যাংকিং বা ই-কমার্স অ্যাপের ব্যবহারকারী দেখবেন “Something went wrong” — অথচ কোনো বাগ নেই, শুধু বন্ধের নিয়ম মানা হয়নি।

ক্রমটি এমন: Kubernetes একটি pod বন্ধ করতে চাইলে সেটিকে service-এর endpoint তালিকা থেকে সরানোর প্রক্রিয়া শুরু করে, preStop hook থাকলে চালায়, তারপর container-এর PID 1-কে SIGTERM দেয়, আর terminationGracePeriodSeconds (ডিফল্ট ৩০ সেকেন্ড) অপেক্ষা করে। এর পরও বেঁচে থাকলে SIGKILL. সূক্ষ্ম বিষয়: endpoint সরানো আর SIGTERM প্রায় একসাথে ঘটে, তাই load balancer-এর কাছে খবর পৌঁছানোর আগেই কিছু নতুন অনুরোধ আসতে পারে। এজন্য অনেক দল SIGTERM পেয়েও কয়েক সেকেন্ড নতুন অনুরোধ নিতে থাকে, বা preStop-এ ছোট অপেক্ষা রাখে।

সবচেয়ে সাধারণ ভুল অংশ ৮-এ দেখেছেন: shell-রূপের CMD বা entrypoint script-এ exec না দেওয়া, ফলে PID 1 হয় shell, যে SIGTERM অ্যাপকে পৌঁছায় না। লক্ষণ: প্রতিবার বন্ধ হতে ঠিক grace period লাগে, আর exit code ১৩৭. দ্বিতীয় ভুল: অ্যাপ SIGTERM ধরে কিন্তু কাজ গোছাতে grace period-এর চেয়ে বেশি সময় নেয়। তৃতীয়: PID 1 হিসেবে চলা অ্যাপ zombie তোলে না, কারণ সে জানেই না যে অনাথ প্রসেসেরা এখন তার দায়িত্ব — এজন্যই tini-র মতো ছোট init তৈরি হয়েছে, যা Docker-এর --init দিয়ে যুক্ত করা যায়।

একই চিন্তা ডেটাবেসেও: PostgreSQL-এর মতো সিস্টেম আলাদা আলাদা signal-কে আলাদা মাত্রার বন্ধ হিসেবে ধরে — ভদ্রভাবে সবার শেষ হওয়ার অপেক্ষা, চলমান লেনদেন বাতিল করে দ্রুত বন্ধ, বা জরুরি বন্ধ যার পর recovery লাগে (DBMS কোর্সের recovery পর্বের ধারণা)।

মূল উপলব্ধি: SIGTERM হলো সিস্টেম আর অ্যাপের মধ্যে একটি চুক্তি — “আপনাকে সময় দিচ্ছি, গুছিয়ে নিন”। চুক্তিটি তখনই কাজ করে, যখন signal ঠিক প্রসেসে পৌঁছায় আর অ্যাপ নির্ধারিত সময়ের মধ্যে সাড়া দেয়; দুটোই মেপে যাচাই করতে হয়।

প্র ০৫Windows signal ছাড়াই চলে — তাহলে কি signal একটি অপরিহার্য OS-ধারণা, নাকি শুধু Unix-এর একটি ঐতিহাসিক নকশা?

Signal আসলে কয়েকটি আলাদা প্রয়োজন একটি যন্ত্রে মিশিয়ে মেটায়: (১) প্রসেসের নিজের ভুলের খবর (SIGSEGV, SIGFPE), (২) বাইরের নিয়ন্ত্রণ-আদেশ (SIGTERM, SIGSTOP, SIGINT), (৩) ঘটনার খবর (SIGCHLD, SIGALRM, SIGPIPE), আর (৪) প্রসেসে প্রসেসে ছোট বার্তা (SIGUSR1). Unix-এর দর্শন ছিল একটি সরল, সার্বজনীন ব্যবস্থা — একটি সংখ্যা আর একটি handler. এর দাম অংশ ৬-এর সব জটিলতা: যেকোনো মুহূর্তে বাধা, async-signal-safety, EINTR.

১৯৮০-এর দশকের শেষে Dave Cutler-এর দল যখন Windows NT নকশা করে, তারা এই চারটি প্রয়োজন চারটি আলাদা ব্যবস্থায় ভাগ করে। ভুলের খবর গেল structured exception handling-এ, যা প্রোগ্রামিং ভাষার exception-এর মতো stack ধরে ওপরের দিকে handler খোঁজে — অনেক বেশি কাঠামোবদ্ধ। নিয়ন্ত্রণ-আদেশ গেল আলাদা পথে: console-এর Ctrl event (নতুন থ্রেডে handler), service-এর Service Control Manager, GUI-এর window message. ঘটনার খবরের জন্য প্রায় সব kernel object-এ “অপেক্ষা” করা যায় — প্রসেসের handle-এ WaitForSingleObject দিলে child শেষ হওয়ার খবর মেলে। আর থ্রেডকে asynchronous কাজ দেওয়ার জন্য আছে APC (asynchronous procedure call), যা কেবল থ্রেড নিজে “alertable” অপেক্ষায় গেলে চলে — অর্থাৎ বাধা আসে শুধু নিরাপদ মুহূর্তে।

অন্য গবেষণা-OS-ও ভিন্ন পথ নিয়েছে। Bell Labs-এর Plan 9 সংখ্যার বদলে ছোট লেখা-বার্তা (“note”) ব্যবহার করত। আধুনিক Linux-ও ধীরে ধীরে Windows-ধাঁচের দিকে যাচ্ছে বলা যায়: signalfd, pidfd আর ইভেন্ট-লুপ-ভিত্তিক নকশা signal-কে “অপেক্ষা করে পড়া যায় এমন ঘটনা”-য় রূপ দেয়। তবু POSIX signal অপরিহার্য থেকে গেছে, কারণ এটি সর্বজনীন সামঞ্জস্যের ভিত্তি — Linux, macOS, Android, প্রতিটি container — আর shell, job control, systemd, Docker সবই এর ওপর দাঁড়িয়ে।

মূল উপলব্ধি: প্রয়োজনগুলো সার্বজনীন — ভুলের খবর, বাইরের আদেশ, ঘটনার খবর; signal তার একটি নির্দিষ্ট, ঐতিহাসিক সমাধান। কোন OS কোন সমাধান বেছে নিয়েছে জানলে cross-platform কোড লেখা আর AI-এর ভুল OS-এর উত্তর ধরা দুটোই সহজ হয়।

অনুশীলন

হাতে হিসাব, আউটপুট অনুমান, কোড লেখা আর সমস্যা নির্ণয় — আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।

  1. হাতে হিসাব — wait status পড়া: Linux-এ waitpid যে কাঁচা status সংখ্যা দেয়, তার নিয়ম (সরলীকৃত): নিচের ৭ বিট (st & 0x7f) 0 হলে প্রসেস স্বাভাবিকভাবে শেষ, আর exit code = (st >> 8) & 0xff; নইলে নিচের ৭ বিটই signal নম্বর, আর 0x80 বিট বলে core dump হয়েছে কি না। নিচের কাঁচা status-গুলোর অর্থ বলুন, আর প্রতিটির জন্য shell-এ $? কত হতো: (ক) 768, (খ) 9, (গ) 139, (ঘ) 15, (ঙ) 256.
    • (ক) 768 = 0x300. নিচের ৭ বিট 0 → স্বাভাবিক exit; exit code = 0x300 >> 8 = ৩. $? = ৩.
    • (খ) 9 = 0x09. নিচের ৭ বিট ৯ → SIGKILL, core নেই। $? = ১২৮ + ৯ = ১৩৭.
    • (গ) 139 = 0x8B = 0x80 + 11. Signal ১১ (SIGSEGV), core dump-এর বিট চালু। $? = ১২৮ + ১১ = ১৩৯. লক্ষ করুন, এখানে কাঁচা status আর $? কাকতালীয়ভাবে একই সংখ্যা — কারণ core বিটের মান ১২৮. দুটিকে গুলিয়ে ফেলবেন না।
    • (ঘ) 15 → SIGTERM, core নেই। $? = ১৪৩.
    • (ঙ) 256 = 0x100 → স্বাভাবিক exit, code ১. $? = ১.

    বাস্তব কোডে এই বিট নিজে হাতে না খুলে WIFEXITED, WEXITSTATUS, WIFSIGNALED, WTERMSIG, WCOREDUMP macro ব্যবহার করুন — বিন্যাস OS-ভেদে আলাদা হতে পারে।

  2. আউটপুট অনুমান: একটি প্রোগ্রাম SIGUSR2 আর SIGRTMIN+1-এর জন্য counter-বাড়ানো handler বসায়, দুটোই block করে, তারপর SIGUSR2 পাঁচবার আর SIGRTMIN+1 চারবার নিজেকে পাঠায়, শেষে unblock করে দুটো counter ছাপে। কী ছাপা হবে? আর block না করে পাঠালে?

    Block করে: SIGUSR2 = 1, RT = 4. সাধারণ signal-এর pending বিট একটি, তাই পাঁচটি মিলে একটি; real-time signal প্রতিটি সারিতে থাকে, তাই চারটিই পৌঁছায়।

    Block না করে: এক-থ্রেডের প্রসেসে নিজেকে পাঠানো unblocked signal kill ফেরার আগেই পৌঁছায়, তাই প্রতিটি আলাদাভাবে handler চালায় — SIGUSR2 = 5, RT = 4. কিন্তু অন্য প্রসেস দ্রুত পরপর পাঠালে ফল নিশ্চিত নয়: লক্ষ্য-প্রসেস CPU পাওয়ার আগে কয়েকটি মিশে যেতে পারে। সাধারণ signal দিয়ে গোনা তাই অবিশ্বাস্য।

  3. কোড লিখুন: একটি লম্বা হিসাবের প্রোগ্রাম লিখুন, যা SIGUSR1 পেলে (GNU dd-এর মতো) “এখন পর্যন্ত কত শেষ” ছাপবে, কিন্তু থামবে না; আর SIGTERM পেলে ফলাফল সেভ করে বেরিয়ে যাবে। Handler-এর ভেতরে printf ব্যবহার করবেন না।

    দুটি flag, দুটি handler, আর সব আসল কাজ main লুপে:

    static volatile sig_atomic_t want_report = 0, want_stop = 0;
    static void on_usr1(int s) { (void)s; want_report = 1; }
    static void on_term(int s) { (void)s; want_stop = 1; }
    
    int main(void) {
        struct sigaction sa;
        memset(&sa, 0, sizeof sa);
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        sa.sa_handler = on_usr1; sigaction(SIGUSR1, &sa, NULL);
        sa.sa_handler = on_term; sigaction(SIGTERM, &sa, NULL);
    
        long done = 0;
        while (!want_stop) {
            do_one_step();  done++;            /* the real work */
            if (want_report) {                 /* printf is safe HERE */
                want_report = 0;
                printf("progress: %ld steps\n", done);
                fflush(stdout);
            }
        }
        save_results(done);                    /* normal code: files, malloc OK */
        return 0;
    }

    অন্য টার্মিনাল থেকে kill -USR1 <pid> দিলে অগ্রগতি দেখাবে, kill <pid> দিলে সেভ করে বের হবে। want_report = 0 আগে, তারপর printf — যাতে printf চলাকালীন নতুন অনুরোধ এলে সেটি হারিয়ে না যায়।

  4. সমস্যা নির্ণয়: একটি দলের Docker container প্রতিবার docker stop-এ প্রায় ঠিক ১০ সেকেন্ড নেয় আর exit code ১৩৭ দেখায়। Dockerfile-এর শেষ লাইন CMD ./start.sh, আর start.sh-এর শেষ লাইন python app.py. অ্যাপে SIGTERM-এর handler আছে। কী ঘটছে, আর তিনটি সমাধান বলুন। systemd-এ একই অ্যাপ TimeoutStopSec=20 দিয়ে চালালে আর অ্যাপের গোছাতে ২৫ সেকেন্ড লাগলে কী হবে?

    নির্ণয়: shell-রূপের CMD তাই PID 1 = /bin/sh -c ./start.sh, তার child আরেকটি shell (start.sh), তার child python. SIGTERM যায় শুধু PID 1-এ; handler না থাকায় PID 1-এর নিয়মে সেটি ফেলে দেওয়া হয়, আর shell এটি child-কে পৌঁছায়ও না। অ্যাপের handler তাই কখনো চলে না; ১০ সেকেন্ড পর SIGKILL, exit ১২৮ + ৯ = ১৩৭.

    সমাধান: (১) exec রূপ: CMD ["./start.sh"] আর start.sh-এর শেষ লাইন exec python app.py — তখন python নিজেই PID 1 হয়ে SIGTERM পায় (python handler বসিয়েছে, তাই PID 1-এর নিয়মে বাধা নেই)। (২) docker run --init বা image-এ tini — init signal পৌঁছে দেয় আর zombie তোলে। (৩) একান্তই script রাখতে হলে script-এ trap দিয়ে SIGTERM ধরে child-কে kill -TERM পাঠানো আর wait করা।

    systemd-এর হিসাব: t = ০-তে SIGTERM, অ্যাপ গোছানো শুরু করে; t = ২০ সেকেন্ডে সময়সীমা শেষ, তখনও ৫ সেকেন্ডের কাজ বাকি — systemd SIGKILL পাঠায়, শেষ ৫ সেকেন্ডের কাজ হারায়, unit-এর ফল 'timeout'. সমাধান: TimeoutStopSec অন্তত ৩০ সেকেন্ড করা, অথবা অ্যাপের বন্ধ-প্রক্রিয়া দ্রুত করা।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি block করা signal-এর জীবনের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. প্রসেস sigprocmask দিয়ে signal-টি mask-এ রাখে
  2. অন্য প্রসেস kill() দিয়ে signal পাঠায় — generation
  3. Kernel pending set-এ বিট বসায়, কিন্তু delivery হয় না
  4. প্রসেস signal-টি unblock করে
  5. User mode-এ ফেরার মুহূর্তে kernel handler চালায়
  6. Handler ফেরে, sigreturn আগের অবস্থা ফিরিয়ে দেয়

Linux-এ নিচের কোন signal-এর জন্য প্রসেস handler বসাতে বা উপেক্ষা করতে পারে না?

  • SIGTERM
  • SIGKILL
  • SIGINT
  • SIGSEGV

একটি প্রসেস SIGUSR1 block করে রাখল; এ অবস্থায় অন্য প্রসেস তাকে তিনবার SIGUSR1 পাঠাল। এরপর unblock করলে handler কতবার চলবে?

  • একবারও না, block-এর সময় পাঠানো signal হারিয়ে যায়
  • ৩ বার
  • ২ বার
  • ১ বার

Bash-এ একটি প্রসেস SIGTERM (signal 15)-এ মারা গেলে, আর সে signal-টি সামলায়নি, echo $? কী দেখাবে?

  • 15
  • 1
  • 143
  • 137

Signal handler-এর ভেতর থেকে নিচের কোনটি ডাকা নিরাপদ (async-signal-safe)?

  • write()
  • printf()
  • malloc()
  • exit()

Linux টার্মিনালে একটি চলমান foreground প্রোগ্রামে Ctrl+Z চাপলে tty driver কোন signal পাঠায়, আর তার default action কী?

  • SIGINT — প্রসেস শেষ
  • SIGSTOP — প্রসেস থামে, ধরা যায় না
  • SIGTSTP — প্রসেস থেমে থাকে
  • SIGQUIT — core dump সহ শেষ

docker stop ডিফল্ট সেটিংয়ে একটি container বন্ধ করে কীভাবে?

  • সঙ্গে সঙ্গে সব প্রসেসকে SIGKILL দেয়
  • PID 1-কে SIGTERM দেয়, ১০ সেকেন্ড পর তখনও চললে SIGKILL
  • SIGINT দেয় আর প্রসেস নিজে না থামা পর্যন্ত অপেক্ষা করে
  • Container-এর kernel shutdown করে দেয়

শব্দকোষ

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

মনে রাখুন

  1. Signal = প্রসেসের কাছে asynchronous ছোট বার্তা, প্রসেসের জন্য “software interrupt”; উৎস kernel, টার্মিনাল আর অন্য প্রসেস (kill).
  2. প্রতিটির default action: Term, Core, Ign, Stop, Cont; SIGKILL আর SIGSTOP ধরা, উপেক্ষা বা block করা যায় না।
  3. Generation → pending → delivery; mask-এ থাকা signal pending থাকে; সাধারণ signal জমা হয় না, real-time signal হয়।
  4. Handler বসান sigaction দিয়ে; ভেতরে শুধু async-signal-safe কাজ আর volatile sig_atomic_t flag; EINTR আর SIGCHLD-এ waitpid লুপ সামলান।
  5. বন্ধের নিয়ম: SIGTERM → অপেক্ষা → SIGKILL; shell-এ $? = ১২৮ + n (১৩০, ১৩৭, ১৪৩); container-এ PID 1 ঠিক আছে কি না দেখুন; Windows-এ signal নেই, আলাদা ব্যবস্থা।

পরের পর্বে: এতক্ষণ প্রতিটি প্রসেসে একটিই নির্বাহ-ধারা ধরে নিয়েছি। একটি প্রসেসের ভেতরে যদি একসাথে অনেকগুলো ধারা চলে? থ্রেড কী, প্রসেস থাকতে থ্রেড কেন, আর kernel আর user-space-এ থ্রেড কীভাবে বানানো হয়।

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

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