سیگنال‌ها و trapها - آموزش اسکریپت نویسی
X
تبلیغات
رایتل

آموزش اسکریپت نویسی

آموزش اسکریپت نویسی پوسته گنو-لینوکس

#!/bin/bash

سیگنال‌ها و trapها

SignalTrap

ارسال و به دام انداختن سیگنالها

سیگنال‌ها ابزار اصلی برای ارتباطات میان‌پردازشی غیر همزمان می‌باشند. یعنی یک پردازش ‎(A)‎ می‌تواند به پردازش دیگر ‎(B)‎ بگوید در یک زمانی که به جای پردازش B توسط پردازش A انتخاب شده، کاری را انجام بدهد. (مانند جستجوی یک فایل توسط پردازش B هر چند ثانیه یکبار، این عمل polling نامیده می‌شود و زمان‌بندی آن به جای پردازش A توسط پردازش B کنترل می‌شود.)

سیستم عامل تعداد محدودی سیگنال فراهم می‌نماید که می‌توانند برای گفتن انجام کاری به یک پردازش ارسال بشوند. سیگنالها هیچ اطلاعات اضافی را حمل نمی‌کنند، تنها اطلاعاتی که پردازش به دست می‌آورد آن است که سیگنال دریافت شده است. پردازش حتی نمی‌داند که چه کسی سیگنال را ارسال نموده.

غیر از اینکه پردازش از قبل فرمان خاصی بگیرد، اکثر سیگنالها کُشنده هستند، یعنی عمل پیش‌فرضی که یک پردازش به مجرد دریافت یک سیگنال انجام خواهد داد، یک خروج بی‌درنگ است. (استثناها: SIGCHLD به طور پیش‌فرض صرفنظر می‌گردد، SIGSTOP پردازش را متوقف می‌سازد، و SIGCONT پردازش را از سر می‌گیرد.) برخی سیگنالها (از قبیل SIGQUIT) نیز باعث می‌شوند پردازش علاوه بر خروج یک فایل core باقی بگذارد.

1. تله‌ها، یا اداره‌کنندگان سیگنال

یک پردازش ممکن است به جای خروج بواسطه دریافت یک سیگنال، انجام عمل متفاوتی را انتخاب کند. این عمل توسط تنظیم یک گرداننده سیگنال (یا trap) انجام می‌شود. trap باید قبل از اینکه سیگنال دریافت شود تنظیم شده باشد. وقتی پردازش بواسطه اینکه یک trap برای آن تنظیم شده است یک سیگنال دریافت می‌کند، گفته می‌شود سیگنال را در تله انداخته است.

ساده‌ترین کاری که برای انجام دادن با یک پردازش می‌تواند انتخاب شود، نادیده گرفتن سیگنال است. قاعدتاً یک ایده نامناسب است، مگر اینکه برای یک مقصود خیلی خاص انجام گردد. صرفنظر نمودن از سیگنالها غالباً منجر به پردازشهای فراری می‌گردد که تمام قدرت CPU را مصرف می‌کنند.

به طور عمومی‌تر trapها می‌توانند برای جلوگیری کردن از یک سیگنال کُشنده، انجام پاکسازی، و سپس خروج موقرانه تنظیم گردند. برای مثال، برنامه‌ای که فایلهای موقتی ایجاد می‌کند و مایل است قبل از خروج آنها را حذف نماید. اگر برنامه توسط یک سیگنال مجبور به خروج بشود، قادر نخواهد شد فایلها را حذف کند مگر اینکه سیگنال را در تله بیاندازد.

در یک اسکریپت پوسته، trap فرمان مورد استفاده برای برقرار کردن یک گرداننده سیگنال است. فرمان trap به پنج روش مختلف می‌تواند استفاده شود:

  • trap 'some code' signal list‎ -- با استفاده از این ترکیب، یک گرداننده سیگنال برای هر موردِ داخل «لیستِ سیگنال» برقرار می‌شود. موقعی که یکی از این سیگنالها دریافت می‌شود، فرمانِ داخل اولین شناسه اجرا خواهد شد.

  • trap '' signal list‎ -- با کاربرد این ترکیب، هر سیگنال داخل لیست صرفنظر می‌گردد. اکثر اسکریپت‌ها نباید از این مورد استفاده نمایند.

  • trap - signal list‎ -- با استفاده از این روش، هر سیگنال داخل لیست به رفتار پیش فرضش باز گردانده خواهد شد.

  • trap signal‎ -- کاربرد این شکل، یک سیگنال نامبرده به رفتار پیش‌فرض برگردانده می‌شود. (این ترکیب موروثی است.)

  • trap -- بدون شناسه، یک لیست از نگهدارنده‌های سیگنال چاپ می‌کند.

سیگنالها می‌توانند با استفاده از یک عدد یا یک نام نمادین مشخص بشوند. به علت اینکه در میان سیستم‌عامل‌های مختلف طرح‌بندی شماره سیگنالها برای سیگنالهای واقعی می‌تواند اندکی متفاوت باشد، معمولاً در اسکریپت‌های POSIX یا Bash نام نمادین ترجیح داده می‌شود. برای پوسته‌های بورن، شاید اعداد لازم باشند.

برای تمام سیستم عامل‌های یونیکس-مانند یک مجموعه مرکزی سیگنال وجود دارد که شماره‌هایشان عملاً هرگز تغییر نمی‌کند، متداول‌ترین آنها عبارتند از:

نام

شماره

معنی

HUP

1

معلق ماندن. کنترل کردن ترمینال مرخص گردیده است.

INT

2

گسیختگی. کاربر کلید وقفه (معمولاً ‎ Ctrl-‎C یا DEL) را فشرده است.

QUIT

3

خروج. کاربر کلید خروج (معمولاً ‎ Ctrl-\‎) را فشرده است. خروج و رونوشت یک فایل core.

KILL

9

کشتن. این سیگنال نمی‌تواند صرفنظر یا محبوس گردد. کشنده بی قید و شرط. پاکسازی غیر ممکن.

TERM

15

خاتمه دادن. این سیگنال پیش فرض ارسالی توسط فرمان kill می‌باشد.

EXIT

0

در حقیقت سیگنال نیست. در یک اسکریپت پوسته، یک ‎EXIT trap‎ در هر گونه خروج، سیگنال شده یا نشده، اجرا می‌شود.

Bash نام سیگنالها را با یا بدون SIG مقدم برای هر دو فرمان داخلی trap و kill می‌پذیرد مگر در وضعیت POSIX که kill یک SIG پیشتاز را نمی‌پذیرد اما trap می‌پذیرد.

بدین طریق SIG مقدم، هرگز احتیاج نمی‌شود چون SIGHUP همیشه می‌تواند با استفاده از ‎trap ... HUP‎ محبوس گردد و با استفاده از ‎kill -HUP process_ID فرستاده شود.

نام ویژه EXIT توسط POSIX تعریف گردیده و برای گرداننده سیگنالی که می‌خواهد موقع خروج به سادگی پاکسازی انجام بدهد، نسبت به انجام هر کار پیچیده دیگری تقدم دارد. استفاده از 0 به جای EXIT نیز در فرمان trap مجاز است (اما 0 یک شماره سیگنال معتبر نیست، و ‎kill -0‎ دارای معنی کاملاً متفاوتی می‌باشد). بنابراین برای پاکسازی فقط EXIT را محبوس(trap) کنید و یک تابع پاکسازی در آنجا فراخوانی نمایید. یک گروه از سیگنالها را trap نکنید. یک هشدار: این مورد تنها در پوسته‌های غیر محاوره‌ای (اسکریپت‌ها) کار می‌کند، در صورتیکه پوسته‌های محاوره‌ای سیگنال دریافت کنند(یک باگ احتمالی) EXIT فراخوانی نمی‌شود.

اگر خواهان خاتمه دادن به یک برنامه هستید، همیشه باید از SIGTERM (در حقیقت ‎kill process_ID‎) استفاده کنید. این کار به برنامه شانسی برای در تله انداختن سیگنال و انجام پاکسازی خواهد داد. اگر شما SIGKILL را به کار ببرید، برنامه نمی‌تواند پاکسازی انجام بدهد، و ممکن است فایلها را در یک وضعیت مخدوش شده رها کند.

لطفاً برای یک توضیح کامل‌تر از چگونگی عملکرد و تأثیر متقابل پردازشها، مدیریت پردازش را ملاحظه نمایید.

2. مثالها

این یک روش اساسی برای تنظیم یک trap جهت پاکسازی فایلهای موقت می‌باشد:

#!/bin/sh
tempfile=$(mktemp)
trap 'rm -f "$tempfile"' EXIT
...

این مثال یک گرداننده سیگنال تعریف می‌کند که به علت SIGHUP یک فایل پیکربندی را بازخوانی می‌کند. این یک شگرد رایج مورد استفاده در پردازش‌های daemon است که به طور بلند مدت در حال اجرا هستند، بنابراین وقتی یک متغیر پیکربندی تغییر داده می‌شود، نیازی نیست آنها دوباره راه‌اندازی بشوند.

#!/bin/sh
config=/etc/myscript/config
read_config() { test -r "$config" && . "$config"; }
read_config
trap 'read_config' HUP
while true; do
  ...

تنظیم یک trap روی یک سیگنال، trap قبلی مربوط به آن را رونویسی می‌کند. راه مستقیمی جهت دسترسی به فرمان مرتبط شده با یک trap به صورت یک رشته، به منظور ذخیره و بازیابی وضعیت trap وجود ندارد. اگر چه، خروجی تولید شده trap که به طور صحیح پوشش‌یافته برای استفاده مجدد بی‌خطر می‌باشد. روش پیشنهاد شده POSIX این است:

traps="$(trap)"
...
eval "$traps"

3. چه وقت یک سیگنال به کار برده می‌شود؟

موقعی که bash یک دستور خارجی را در پیش‌زمینه اجرا می‌کند، تا موقعی که پردازش پیش‌زمینه خاتمه یابد هیچ سیگنال دریافت شده‌ای را به کار نمی‌برد. موقعی که یک اسکریپت مانند مورد زیر داشته باشید، این مطلب اهمیت دارد:

trap 'echo "doing some cleaning"' EXIT
echo waiting a bit
sleep 10000

اگر شما اسکریپت را kill کنید (با استفاده از ‎kill pid‎ از یک ترمینال دیگر، نه با ‎Ctrl-C‎ -- بعد بیشتر به آن می‌پردازیم)، bash قبل از فراخوانی trap برای خارج شدن sleep منتظر می‌ماند. احتمالاً این چیزی نیست که شما انتظار دارید. یک راه حل موقت به کار بردن یک دستور داخلی مانند wait است که منقطع خواهد شد:

trap 'echo "doing some cleaning"' EXIT
echo waiting a bit
sleep 10000 & wait $!

توجه نمایید که ‎sleep 10000‎ کشته نخواهد شد و اجرا را ادامه خواهد داد. اگر شما می‌خواهید وقتی اسکریپت kill خواهد شد، job پس‌زمینه آن کشته شود، آن را به trap اضافه کنید. این نوع پاکسازی دقیقاً موردی است که trapها برای آن هستند!

pid=
trap '[[ $pid ]] && kill $pid' EXIT
sleep 10000 & pid=$!
wait

هر فرمان داخلی bash توسط یک سیگنال وارده (صرف نظر نشده) منقطع خواهد شد. اگر wait را دوست ندارید read نیز وجود دارد. شما می‌توانید یک «sleep نامحدود» را به وسیله خواندن از لوله‌ای که هرگز هیچ اطلاعاتی ارایه نخواهد کرد، ایجاد نمایید، read بدون نیاز به یک sleep خارجی برای پیگردی ، برای همیشه مسدود خواهد نمود:

trap 'echo "we get signal"; rm -f ~/fifo' EXIT
mkfifo ~/fifo || exit
chmod 400 ~/fifo
echo "sleeping"
read < ~/fifo

4. ‎Ctrl-C‎ چه کار می‌کند؟

شاید تصور نمایید مثال اول در بخش قبلی صحیح نیست زیرا وقتی شما اسکریپت را در ترمینال خود امتحان نموده و Ctrl-C بزنید بلافاصله آشکارا پیغام چاپ می‌شود.تفاوت میان ارسال INT با استفاده از ‎kill -INT pid‎ و ‎Ctrl-C‎ آن است که ‎Ctrl-C‎ سیگنال INT را به گروه پردازش ارسال می‌کند، که یعنی آن sleep نیز سیگنال را دریافت خواهد کرد، و نه فقط اسکریپت.

برای ارسال INT به گروه پردازش، شما به استفاده از ID پردازش همراه با یک خط تیره جلوی آن نیاز دارید:

 kill -INT -123 # خواهد نمود kill برابر 123 را ID گروه پردازشی با ‎

برای یافتن ID گروه پردازشِ یک پردازش می‌توانید از ‎ps -p pid -o pgid‎ استفاده کنید (با فرض این که ps شما دارای این ترکیب باشد). در Linux شماره شناسایی گروه پردازش، PID سر گروه پردازش است (به طور نمونه اسکریپت)، اما همیشه این حالت نیست (برای مثال OpenBSD).

5. توجه خاص در مورد SIGINT

اگر شما تنظیم یک گرداننده برای SIGINT (به جای استفاده از یک ‎EXIT trap‎) را انتخاب کنید، باید مراقب باشید پردازشی که در پاسخ به SIGINT خارج می‌شود، به منظور اجتناب از ایجاد مشکلات برای فراخواننده‌اش، به جای اینکه به سادگی خارج شود خودش باید با SIGINT کشته شود. بدین گونه:

trap 'rm -f "$tempfile"; trap - INT; kill -INT $$' INT

می‌توانیم تفاوت میان یک رفتار صحیح پردازش و درست رفتار نکردن پردازش را مشاهده کنیم. در اکثر سیستم‌عامل‌ها، ping مثالی از یک سوءرفتار پردازش است. این فرمان به منظور نمایش خلاصه‌ای قبل از خروج، SIGINT را trap می‌کند. اما برای کشتن خودش با SIGINT ناموفق است، و بنابراین پوسته فراخواننده نیز نمی‌داند که باید انصراف دهد. برای مثال،

# Bash.  لینوکس ping ترکیب دستوری‎
for i in {1..254}; do
  ping -c 2 192.168.1.$i
done

در اینجا، اگر کاربر در اثنای حلقه ‎Ctrl-C‎ را بزند، فرمان جاری ping را خاتمه خواهد داد، اما حلقه را خاتمه نخواهد داد. این به علت آن است که فرمان ping لینوکس خودش را با SIGINT نمی‌کشد. (لینوکس از این حیث منحصر به فرد نیست، من سیستم عاملی نمی‌شناسم که فرمان ping آن رفتار صحیحی ارایه کند.)

یک پردازش دارای رفتار صحیح از قبیل sleep دارای این مشکل نیست:

i=1
while [ $i -le 100 ]; do
  printf "%d " $i
  i=$((i+1))
  sleep 1
done
echo

اگر ما در ضمن این حلقه ‎Ctrl-C‎ را بزنیم، sleep سیگنال SIGINT را دریافت خواهد نمود و بواسطه آن می‌میرد (sleep سیگنال SIGINT را در تله گرفتار نمی‌کند). پوسته می‌بیند که sleep بواسطه SIGINT مرده است. در وضعیت یک پوسته محاوره‌ای، این مطلب حلقه راخاتمه می‌دهد. در موقعیت یک اسکریپت، کل اسکریپت خارج خواهد شد، مگر اینکه اسکریپت خودش SIGINT را محبوس (trap) کند.


CategoryShell

SignalTrap (آخرین ویرایش ‎2013-07-19 11:25:11‎ توسط CharlesAtkinson)