سیگنال و کد خروج؛ توقف تمیز و جمعکردن پردازهٔ فرزند
در این درس ۷ بخش
بستن پنجره یا ارسال فرمان توقف به معنی جمعشدن همهٔ کارهای برنامه نیست. یک فرایند ممکن است فرصت پاکسازی داشته باشد، پیام پایان بنویسد یا پیش از آن قطع شود. این درس به رابطهٔ والد و فرزندِ خودمان میپردازد: چه سیگنالی ارسال میکنیم، چقدر صبر میکنیم و چگونه مطمئن میشویم وضعیت پایان را دریافت کردهایم. هیچ PID حدسی یا پردازهٔ کاربر دیگری هدف آزمایش نیست.
سیگنال درخواستِ رویداد است، کد خروج گزارش پایان
سیگنال سازوکاری برای اطلاعدادن رخداد به فرایند است. رفتار نهایی به نوع سیگنال، تنظیمات و برنامه بستگی دارد. SIGTERM معمولاً برای درخواست پایان مناسب است و برنامه میتواند برایش handler داشته باشد. SIGKILL قابل گرفتن یا نادیدهگرفتن نیست؛ بنابراین فرصتی برای اجرای پاکسازیِ اختصاصی برنامه نمیدهد. ارسال سیگنال با موفقیت، تأیید تمامشدن عملیات کسبوکار یا ذخیرهشدن داده نیست.
کد خروج را بعد از پایان میخوانیم و معنای کدهای عادی از قرارداد برنامه میآید. مثلاً خروج هفت در یک ابزار آموزشی میتواند «ورودی رد شد» باشد، ولی در ابزار دیگری معنای دیگری داشته باشد. عدد errno هم چیز دیگری است: یک عملیات سیستمی میتواند شکست بخورد، برنامه آن را مدیریت کند و سرانجام با کد صفر تمام شود. #linux-signal و #linux-exit-status این مرزها را کنار هم نگه میدارند.
دو فرزند، یک نام سیگنال، دو پایان
در پوشهٔ project فرمان زیر را اجرا کنید. والد دو فرزند کنترلشده میسازد؛ پیش از ارسال سیگنال از آمادهبودن آنها مطلع میشود. فرزند نخست SIGTERM را مدیریت میکند و پیام مشخصی مینویسد؛ فرزند دوم رفتار پیشفرض دارد. این هماهنگی مهم است: ارسال سیگنال پیش از نصب handler میتواند همان آزمایش را به سناریوی دیگری تبدیل کند.
python3 lab.py signalانتظار آموزشی، handled_returncode برابر صفر، handled_message برابر TERM handled و children_reaped=true است. default_returncode باید با expected_default_returncode همان اجرا برابر باشد. این مقدار در POSIX منفیِ شمارهٔ SIGTERM است؛ برای نوشتن آزمون از ثابت نمادین signal.SIGTERM استفاده میکنیم. شمارهٔ رایج را به قانون ثابت همهٔ سیستمها تبدیل نمیکنیم.
اگر هر دو فرزند با کد صفر تمام شدند، ممکن است رفتار پیشفرض ناخواسته تغییر کرده باشد. اگر فرزند نخست با سیگنال پایان یافت، آمادهبودن، نصب handler و خطای داخل آن را بررسی کنید. خروجی JSON، مشاهدهٔ همین فرزندان است و سلامت سرویس واقعی را نتیجه نمیدهد. هنگام شکست غیرمنتظره، زمان و خروجی را نگه دارید و برای سبزشدن آزمایش، assertion را حذف نکنید.
والد باید وضعیت فرزند را جمع کند
فرزند تمامشده ممکن است تا دریافت وضعیت توسط والد، ورودی کوچکی در جدول فرایندها داشته باشد؛ این با فرایندِ درحالمصرف CPU یکی نیست. wait یا communicate مسیر معمول دریافت پایان در این نمونه است. sleep والد جای این دریافت را نمیگیرد. همچنین والد باید لولههای خروجی را درست مصرف کند؛ صبرکردن روی فرزندی که خروجی زیادی در pipe پرشده مینویسد، میتواند به انتظار ناخواسته منجر شود.
در آزمایش، communicate با مهلت محدود به کار میرود. اگر فرصت توقف تمام شود، مسیر جایگزین فقط همان فرزندِ ثبتشده در شیء Popen را هدف میگیرد و سپس وضعیتش را جمع میکند. این منطق مجوز پایاندادن به تمام Pythonها یا همهٔ پردازههای یک نام نیست. از فرمان گروهی برای حل مشکلی که مالکیت فرایندش روشن نیست استفاده نکنید؛ شناسایی درست باید پیش از مداخله انجام شده باشد.
درخواست پایان → فرصت محدود برای همکاری → دریافت خروجی و وضعیت
اگر فرصت تمام شد: پایان اجباری فقط برای فرزند خودمان → انتظار و دریافت وضعیت
در هر دو مسیر: بستهشدن منابع والد و بررسی نتیجهسه رابط گزارش را با هم اشتباه نگیریم
در Popen پایتون روی POSIX، returncode منفیِ N نشان میدهد فرزند مستقیم بهوسیلهٔ سیگنال N پایان یافته است. Bash برای فرمانی که با سیگنال مرگبار تمام شده، از قرارداد ۱۲۸ بهعلاوهٔ شماره استفاده میکند. اگر shell=True داشته باشیم، وضعیت پوسته وارد زنجیره میشود. برای همین آزمایش از فهرست آرگومان و بدون پوستهٔ واسط استفاده میکنیم تا معلوم باشد وضعیت متعلق به کدام فرزند است.
فایل مستقل 06-explicit-exit.py زیر هیچ سیگنالی ارسال نمیکند: فرزند خودش هفت را انتخاب میکند. خروجی 7 است و هر دو جریان متن خالیاند. این مثال نشان میدهد نبود پیام stderr موفقیت را تضمین نمیکند و عدد خروج هم بدون قرارداد برنامه دلیل کامل شکست را نمیگوید. check=False در اینجا عمدی است، زیرا میخواهیم نتیجهٔ ناموفقِ تعریفشده را بررسی کنیم.
import subprocess
import sys
result = subprocess.run(
[sys.executable, '-c', 'raise SystemExit(7)'],
capture_output=True, text=True, timeout=2, check=False
)
assert result.returncode == 7
assert result.stdout == '' and result.stderr == ''
print(result.returncode)
if python3 -c 'raise SystemExit(7)'; then
app_status=0
else
app_status=$?
fi
printf 'status=%s\n' "$app_status"در بلوک Bash، مقدار $? بلافاصله در شاخهٔ شکست گرفته میشود؛ یک فرمان موفق میان آن و فرمان اصلی میتوانست آن را عوض کند. چاپ متن بعدی وضعیت قبلی را به خاطر نمیسپارد. خروجی این بلوک status=7 است. اگر دستور را در یک pipeline بگذارید، قرارداد گزارش pipeline را هم باید بررسی کنید؛ برای مرور آن به درس Bash قبلی برگردید و اینجا چند رفتار را یکجا مخلوط نکنید.
توقف تمیز را متناسب با برنامه تعریف کنیم
در برنامهٔ وب، توقف تمیز میتواند شامل نپذیرفتن درخواست تازه و فرصتدادن به کار جاری باشد. در ابزار خط فرمان ممکن است بستن یک فایل کافی باشد. زمان مناسب را از نیاز و رفتار واقعی انتخاب کنید؛ انتظار نامحدود قابل دفاع نیست و توقف فوری هم همیشه مناسب نیست. handler پایتون در رشتهٔ اصلی اجرا میشود و جای پیامرسانی آزاد میان رشتهها نیست؛ برای آن مسئله ابزارهای همگامسازی لازماند.
چرخهٔ عمر سرویس را پیشتر در این درس ساختهایم:
https://www.darsavand.com/courses/linux-advanced-administration?lesson=0a675015-d758-4018-aa4e-a6f9baf96230
رابطهٔ تنظیمات اجرای بکاند با توقف و restart در این درس آمده است:
https://www.darsavand.com/courses/nodejs-backend-ubuntu-deployment?lesson=78a3a92a-f850-4744-9a6f-0b4fade2d5a6
برای واژهها #linux-process، #linux-signal و #linux-exit-status را مرور کنید. معیار تحویل، ثبت هویت فرزند، قرارداد پایان، مهلت همکاری، وضعیت واقعی و نتیجهٔ جمعکردن فرزند است. «فرمان kill خطا نداد» هنوز گزارش کامل نیست. همچنین مشخص کنید پایانِ پردازه چه چیزی را دربارهٔ کار انجامشده ثابت میکند و چه چیزهایی نیازمند آزمون مستقلاند.
تمرین و پاسخ تشریحی
پرسش 1: همان SIGTERM برای دو فرزند ارسال شده است. یکی returncode صفر و دیگری مقدار منفیِ شمارهٔ SIGTERM دارد. آیا فرزند اول سیگنال را دریافت نکرده است؟
پاسخ تشریحی: الزاماً نه. در سناریوی ما فرزند نخست handler نصب کرده، پیام TERM handled نوشته و آگاهانه با کد صفر خارج شده است. فرزند دوم عمل پیشفرض پایانیافتن با سیگنال را دارد و Popen در POSIX آن را با -N گزارش میکند. برای نتیجه باید قرارداد برنامه، پیام و نحوهٔ انتظار والد را کنار هم بخوانیم، نه فقط یک عدد را.
پرسش 2: Popen.communicate با TimeoutExpired برگشته است. آیا همین استثنا ثابت میکند فرزند دیگر زنده نیست؟
پاسخ تشریحی: خیر. این روش هنگام تمامشدن مهلت، خودکار فرزند را نمیکشد. والد باید تصمیم بگیرد؛ در آزمایش کنترلشده پس از فرصت توقف، فقط فرزندِ متعلق به خودش را پایان میدهد و دوباره communicate یا wait میکند. رفتار subprocess.run با timeout را با Popen.communicate یکی نگیرید؛ run فرزند مستقیم را در مسیر timeout میکشد و انتظار میکشد.
پرسش 3: در Bash پس از فرمانِ پایانیافته با SIGTERM، کد ۱۴۳ دیده شده است. آیا هر برنامهای که ۱۴۳ برگرداند حتماً با همان سیگنال کشته شده است؟
پاسخ تشریحی: خیر. ۱۴۳ در سیستم رایج با قرارداد ۱۲۸ بهعلاوهٔ شمارهٔ ۱۵ سازگار است، اما برنامه میتواند خودش همان عدد را بهعنوان کد خروج انتخاب کند. عدد بهتنهایی تاریخچهٔ کامل را بازسازی نمیکند. در ابزار و سیستم دیگر، شمارهٔ سیگنال را از نام آن بخوانید و روش گزارش والد را نیز مشخص کنید؛ برای پایانیافتن با سیگنال از شاهد متناسب استفاده کنید.
منابع رسمی برای بررسی و مطالعهٔ بیشتر
Python 3.12 — subprocess: https://docs.python.org/3.12/library/subprocess.html
Python 3.12 — signal: https://docs.python.org/3.12/library/signal.html
GNU Bash — Exit Status: https://www.gnu.org/s/bash/manual/html_node/Exit-Status.html
Linux man-pages — signal(7): https://man7.org/linux/man-pages/man7/signal.7.html
متن، سناریوها و پرسشهای این درس تألیف مستقل تحریریه درسآوند است. منابع برای بررسی رفتار ابزار و ادامهٔ مطالعه معرفی شدهاند؛ این آموزش ترجمهٔ یک منبع واحد نیست.
فایل آزمایشگاه، راهنمای اجرا، آزمونها و فرم گزارش رخداد:
https://www.darsavand.com/resources/linux-application-errors-practice-kit
برای دیدن ترتیب دورههای مقدماتی، پیشرفته و این مسیر، نقشهٔ راه لینوکس را بخوانید:
https://www.darsavand.com/resources/linux-learning-roadmap
تألیف و مثالهای اختصاصی: تحریریه درسآوند.
متن درسهای عمومی را بدون ورود بخوانید.