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

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

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

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

Process Management

مدیریت پردازش

این صفحه هنوز در حال توسعه می‌باشد. احتمال دارد برخی قسمت‌ها کیفیت خیلی خوبی نداشته باشند.

مندرجات

  1. مبانی
  2. پرسش‌های ساده
    1. چگونه یک job پس‌زمینه اجرا کنم؟
    2. اسکریپت من یک job در پس‌زمینه اجرا می‌کند. چطور می‌توانم PID آن را به دست آورم؟
    3. خب، من PID آن را دارم. چگونه کنترل کنم که آیا هنوز در حال اجرا می‌باشد؟
    4. می‌خواهم با پردازشی که قبلاً شروع کرده‌ام، کاری انجام بدهم
    5. چگونه پردازش را توسط نام آن kill کنم؟ بیرون رفتن آن از ‎ps aux | grep‎ را لازم دارم ....
    6. اما من روی سیستم قدری قدیمی یونیکس سنتی هستم که pgrep ندارد! چکار کنم؟
    7. می‌خواهم موردی را در پس‌زمینه اجرا کنم و سپس قطع اتصال (log out) کنم.
    8. من سعی می‌کنم job خود را ‎kill -9‎ کنم، اما ...
    9. مطمئن شوید شما این فرمانها را اجرا کرده‌اید و فهمیده‌اید:
  3. پرسش‌های پیشرفته
    1. می‌خواهم دو job را در پس‌زمینه اجرا کنم، و سپس تا پایان یافتن هر دو منتظر بمانم.
    2. چگونه می‌توانم کنترل کنم که اگر سرویس‌دهنده game من هنوز در حال اجرا می‌باشد؟ من یک اسکریپت در crontab قرار بدهم، و اگر در حال اجرا نیست آن را مجدداً راه‌اندازی کنم...
    3. چگونه مطمئن شوم که در یک لحظه فقط یک نسخه از اسکریپت من می‌تواند اجرا گردد؟
    4. می‌خواهم یک دسته از فایلها را به طور موازی پردازش کنم، و موقعی که یکی تمام می‌شود، بعدی را شروع کنم. و می‌خواهم مطمئن شوم در هر لحظه دقیقاً پنج job در حال اجرا می‌باشد.
    5. اسکریپت من یک خط‌لوله را اجرا می‌کند. می‌خواهم وقتی اسکریپت کشته می‌شود، خط‌لوله نیز بمیرد.
  4. چگونگی کار کردن با پردازش ها
    1. PIDها و والدین
    2. ریسک اجازه مردن والد
    3. ریسک تجزیه درخت پردازش
    4. درست انجام بدهید
      1. شروع یک پردازش و به خاطر سپردن PID آن
      2. معاینه کامل پردازش خود یا خاتمه دادن آن
      3. شروع یک سرویس کمکی «daemon» و کنترل آنکه آیا به طور موفق شروع گردیده است
  5. در مورد پردازش‌ها، محیط‌ها و وراثت

مبانی

یک پردازش، نمونهِ در حال اجرایِ یک برنامه در حافظه است. هر پردازش به وسیله یک شماره تحت عنوان PID، یا تعیین کننده هویت پردازش شناخته می‌شود. هر پردازش دارای قطعهِ تخصیص یافته‌ای از حافظه است، که مخصوص خودش می‌باشد، و از سایر پردازشها قابل دستیابی نیست. این جایی است که پردازش متغیرها و سایر داده‌هایش را ذخیره می‌کند.

کرنل تمام این پردازشها را پیگردی می‌کند، و مقدار اندکی از فوق‌داده‌های اصلی در باره آنها را در جدول پردازش ذخیره می‌نماید. به هرحال، هر پردازش در حدود حقوق ویژه اعطایی توسط کرنل، خود مختار است. وقتی یک پردازش آغاز شده باشد، انجام هر کاری با آن غیر از تعلیق(pause)، یا خاتمه دادن آن دشوار است.

فوق‌داده‌های ذخیره شده توسط کرنل، یک «نام» پردازش و «سطر فرمان» را شامل می‌گردد. این موارد قابل اعتماد نیستند، «نام» پردازش آن است که شما وقتی برنامه را اجرا کردید، بیان نمودید، و ممکن است هیچگونه رابطه‌ای با نام فایل برنامه نداشته باشد. (در برخی سیستم‌ها، پردازشهای در حال اجرا نام خودشان را نیز می‌توانند تغییر بدهند. برای مثال، sendmail از این مورد برای نشان دادن وضعیت خود استفاده می‌کند.) بنابراین، موقع کار با یک پردازش، برای اینکه قادر باشید با آن کاری انجام بدهید باید PID آن را بدانید. جستجوی پردازش‌ها به وسیله نام، به شدت لغزش‌پذیر است.

پرسش‌های ساده

چگونه یک job پس‌زمینه اجرا کنم؟

 command &

ضمناً، در bash و سایر پوسته‌های بورن ‎'&'‎ یک جداکننده فرمان نیز هست. در هر جایی که ‎';'‎ بتواند استفاده شود می‌تواند به کار برود (اما افزون بر ‎';'‎ خیر -- شما باید یکی از آنها را انتخاب کنید). بدین ترتیب، می‌توانید بنویسید:

 command one & command two & command three &

که هر سه فرمان را به طور همزمان در پس‌زمینه اجرا می‌کند، و معادل است با:

 command one &
 command two &
 command three &

یا:

 for i in one two three; do command $i & done

در حالیکه هردو کاراکتر & و ; می‌توانند برای جداکردن فرمانها به کار بروند، & آنها را در پس‌زمینه اجرا می‌کند و ; آنها را به ترتیب اجرا می‌کند.

اسکریپت من یک job در پس‌زمینه اجرا می‌کند. چطور می‌توانم PID آن را به دست آورم؟

پارامتر ویژه ‎$!‎ شماره شناسایی آخرین پردازش اجرا شده در پس‌زمینه را نگهداری می‌کند. شما می‌توانید بعداً برای پیگردی job، خاتمه دادن آن، ثبت آن در یک فایل PID (لغزش‌پذیر)، یا هر مورد دیگری، از آن در اسکریپت خود استفاده نمایید.

 myjob &
 jobpid=$!

خب، من PID آن را دارم. چگونه کنترل کنم که آیا هنوز در حال اجرا می‌باشد؟

kill -0 $PID‎ کنترل خواهد نمود که ببیند آیا یک سیگنال قابل تحویل هست(یعنی، پردازش هنوز وجود دارد). اگر به کنترل یک پردازشِ فرزندِ منفرد به طور غیرهمزمان احتیاج دارید، قابل‌حمل‌ترین راه حل است. همچنین شاید شما بتوانید از فرمان wait پوسته برای مسدودسازی تا موقعی که فرزند (یا فرزندان) خاتمه یابد، استفاده کنید -- بستگی به آن دارد که برنامه شما چکار باید بکند.

معادل اسکریپتی پوسته برای فراخوان‌های سیستم ‎select(2)‎ یا ‎poll(2)‎ وجود ندارد. اگر شما به مدیریت یک مجموعه پیچیده پردازشهای فرزند و رویدادها نیاز دارید، سعی نکنید آن کار را در یک اسکریپت پوسته انجام بدهید. (چند شگرد در بخش advanced این صفحه وجود دارد.)

می‌خواهم با پردازشی که قبلاً شروع کرده‌ام، کاری انجام بدهم

وقتی پردازش را آغاز می‌کنید PID را ذخیره کنید و بعداً آن PID را به کار ببرید:

 # Bourne
 my child process &
 childpid=$!

اگر هنوز در پردازش والد و آغازکنندهِ پردازش فرزندی که می‌خواهید کاری با آن انجام بدهید، قرار دارید، کد فوق بدون نقص است. به دلایل تشریح شده در پایین، متضمن می‌شوید که PID، پردازش فرزندِ (زنده یا مرده) شماست. می‌توانید فرمان kill را برای ارسال سیگنال به آن، خاتمه دادن آن، یا فقط بررسی آنکه آیا هنوز در حال اجرا است، به کار ببرید. می‌توانید wait را برای انتظار جهت خاتمه یافتن آن یا به دست آوردن کد خروج آن در صورت خاتمه یافتن، به کار ببرید.

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

پردازش والدی که فرزند را ایجاد نموده باید PID آن را در جایی که شما بتوانید دستیابی کنید، نوشته باشد. احتمالاً یک فایل PID بهترین مکان است. شماره PID را از هر جا که پردازش والد ذخیره نموده است بخوانید و امیدوار باشید هنگامی که شما درحال نگریستن به آن نبودید، تصادفاً پردازش دیگری کنترل PID را نگرفته باشد. شما می‌توانید از kill برای ارسال سیگنال به آن، خاتمه دادنش، یا فقط بررسی آنکه آیا هنوز در حال اجرا هست، استفاده نمایید. شما نمی‌توانید wait را برای منتظر آن ماندن یا به دست آوردن کد خروجش به کار ببرید، این کار فقط از پردازش پدر آن فرزند امکان‌پذیر است. اگر واقعاً می‌خواهید برای پایان پردازش منتظر بمانید، می‌توانید از ‎kill -0‎ نظرخواهی کنید:

 while kill -0 $pid
 do
     sleep 1
 done

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

اگر نیاز دارید برنامه‌هایی بنویسید که یک پردازش را بدون نگهداری یک رابطه پدر-فرزند، مدیریت کند، بهترین امکانِ شما ایجاد اطمینان از آن است که تمام آن برنامه‌ها با یک شماره شناسایی کاربر(UID) که توسط هیچ برنامه دیگری در سیستم استفاده نمی‌گردد، اجرا می‌شوند. به این طریق، اگر PID بازیافت بشود، تلاش شما برای پرس و جو-کشتن آن ناموفق خواهد شد. این مورد نسبت به ارسال SIGTERM شما به برخی پردازشهای بی‌ضرر به مراتب برتر است.

چطور می‌توانم به واسطه نام یک پردازش را kill نمایم؟ نیاز دارم PID را از ‎ps aux | grep‎ بیرون بیاورم ....

خیر، نیاز ندارید. اولاً، شاید به هیچوجه لازم نداشته باشید به وسیله نام یک پردازش را پیدا کنید. اطمینان حاصل کنید که PID پردازش را دارید و آنچه پرسش فوق می‌گوید را انجام بدهید. اگر نمی‌دانید چگونه PID را به دست آورید: فقط پردازشی که پردازش شما را ایجاد نموده PID واقعی را می‌داند. باید آن را برای شما در یک فایل ذخیره کرده باشد. اگر در پردازش والد باشید حتی بهتر از این است. PID را در یک متغیر قرار بدهید ‎(process & mypid=$!)‎ و آن را به کار ببرید.

اگر بنا بر بعضی دلایل غیرمنطقی واقعاً بخواهید صرفاً به واسطه نام به یک پردازش برسید، متوجه می‌شوید که این یک روش ورشکسته است، شما علاقه ندارید که این بتواند موی شما را آتش بزند، و شما در هرحال بخواهید آن را انجام بدهید، احتمالاً باید از فرمانی به نام pkill استفاده کنید. همچنین اگر در یک سیستم گنو-لینوکس موروثی باشید، شاید بتوانید نگاهی به فرمان killall بیاندازید، اما آگاه باشید: killall در برخی سیستم‌ها هر پردازش در تمام سیستم را می‌کُشد. بهتر است از آن اجتناب نمایید مگر اینکه واقعاً آن را لازم داشته باشید.

(سیستم ‎Mac OS X‎ دارای killall هست، اما pkill ندارد. برای به دست آوردن pkill، به http://proctools.sourceforge.net/ بروید.)

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

لطفاً توجه کنید که کنترل یا کُشتن پردازشها به واسطه نام غیر مطمئن است، زیرا پردازشها می‌توانند در باره نامشان دروغ بگویند، و چیز منحصر به فردی در نام یک پردازش وجود ندارد.

اما من در سیستم یونیکس موروثی قدیمی هستم که فاقد pgrep است! چه کار کنم؟

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

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

دو فرمان کاملاً متفاوت ps در سیستم های موروثی یونیکس وجود دارد: سبکِ یونیکسِ سیستم V‏ (‎ps -ef)‏‎ و سبکِ یونیکس BSD ‏(‎ps auxw‎). در بعضی سیستم‌های یونیکسِ اندکی کمتر قدیمی، این دو ترکیب دستوریِ متفاوت باهم تلفیق می‌شوند، و حضور یا غیبت یک خط تیره به ps می‌گوید که کدام مجموعه حروفِ گزینه در حال استفاده است. (در هر صورت اگر شما ‎ps -auxw‎ با یک خط تیره را دیدید، فوراً برنامه را دور بیاندازید.) POSIX سبکِ سیستم V را به کار می‌برد، و یک گزینه ‎-o‎ برای گفتن به ps که شما کدام فیلدها را می‌خواهید، اضافه می‌کند، بنابراین دیگر لازم نیست شما مواردی مانند ‎ps ... | awk '{print $2}'‎ را بنویسید.

حال، دومین مشکل واقعی با ‎ps -ef | grep foo‎ (بعد از این واقعیت که نامهای پردازش به طور ذاتی غیر قابل اطمینان هستند) آن است که در خروجی یک وضعیت مسابقه وجود دارد. در این خط لوله هر دو فرمان ps و grep به طور همزمان یا تقریباً همزمان بذرپاشی می‌شوند. بسته به اینکه آنها چطور به صورت تقریباً همزمان بذر‌پاشی می‌شوند، پردازش grep می‌تواند یا نمی‌تواند در خروجی ps حضور یابد. و اگر هر دو حضور یابند، فرمان ‎grep foo‎ با هر دو پردازش انطباق خواهد یافت -- برنامه کمکی foo یا هر چه، و همچنین فرمان ‎grep foo‎. شاید هم فقط یکی از آنها را به دست آورید.

دو راهکارِ برطرف نمودنِ موردی برای این مشکل وجود دارد. نخست فیلتر نمودن فرمان grep است. این کار به طور نمونه با اجرای ‎ps -ef | grep -v grep | grep foo‎ انجام می‌شود. توجه نمایید که ‎grep -vاول عمل می‌کند برای اینکه فرمان پایانیِ خط لوله نیست. چنین است زیرا فرمان پایانی در خط لوله در حقیقت فرمانی است که وضعیت خروجش اهمیت دارد. این اجازه می‌دهد فرمانهایی مانند مورد زیر به طور صحیح کار کنند:

 if ps -ef | grep -v grep | grep -q foo; then

دومین راهکار یک ‎grep command‎ مهارت آمیز را در بر می‌گیرد که با پردازش foo منطبق می‌شود اما با خود grep منطبق نمی‌شود. گونه‌های بسیار متنوعی در این زمینه وجود دارد، اما یکی از رایج‌ترین آنها این مورد است:

 if ps -ef | grep '[f]oo'; then

احتمالاً چند مرتبه با این برخورد خواهید نمود. عبارت منظم[f]oo‎ فقط با رشته لفظی foo منطبق می‌گردد، با رشته لفظی ‎[f]oo‎ مطابقت نمی‌کند، و بنابراین فرمان grep نیز منطبق نخواهد شد. این راهکار یک انشعاب پردازش را صرفه‌جویی می‌کند(پردازش ‎grep -v‎ را)، و برخی اشخاص چابکی آن را تشخیص می‌دهند.

شخصی را دیده‌ام که انجام این مورد را امتحان می‌کرد:

 # THIS IS BAD!  DO NOT USE THIS!
 if ps -ef | grep -q -m 1 foo; then

این نه فقط الحاقیه غیر استاندارد گنو (‎grep -m‎ -- توقف پس از M بار انطباق) را استفاده می‌کند، بلکه برای اجتناب از وضعیت مسابقه کاملاً ناموفق است. اگر وضعیت مسابقه هر دو سطر grep و foo را تولید کند، تضمینی وجود ندارد که مورد foo اول خواهد بود! بنابراین، این مورد حتی وخیم‌تر از موردی است که با آن شروع کردیم.

به هرحال، اینها فقط تشریح شگردهایی است که شاید شما در کدهای اشخاص دیگر ببینید، برای اینکه بتوانید تشخیص بدهید آنها سعی در انجام دادن چه کاری دارند. امیدوارم که شما چنین مواردی نخواهید نوشت.

من می‌خواهم موردی را در پس‌زمینه اجرا و سپس قطع ارتباط نمایم.

اگر می‌خواهید بعداً قادر به ارتباط مجدد با آن باشید، screen یا tmux را به کار ببرید. یکی از آنها را راه‌اندازی کنید، آنوقت آنچه را مایل به اجرایش هستید در پیش‌زمینه اجرا کرده و انتقال بدهید(screen با ‎Ctrl-A d‎ و tmux با ‎Ctrl-B d‎). می‌توانید دوباره (به شرطی که سرویس دهنده را reboot نکرده‌اید) با ‎screen -x‎ به screen و با ‎tmux attach‎ به tmux متصل کنید. حتی می‌توانید چند نوبت متصل کنید، و هر ترمینال متصل شده همان چیز را خواهد دید. این مطلب برای وضعیت‌های آموزش راه دور نیز فوق‌العاده است.

اگر نمی‌خواهید یا نمی‌توانید این کار را انجام بدهید، رویکرد سنتی بازهم کار می‌کند: ‎nohup something &

اگر می‌خواهید با یک job در حال اجرا در پس‌زمینه قطع ارتباط کنید، و فراموش کرده‌اید در ابتدا آن را nohup نمایید، Bash همچنین دارای یک فرمان disown نیز هست.

 sleep 1000
 Ctrl-Z
 bg
 disown

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

من سعی در انجام ‎kill -9 my job‎ دارم اما...

واه! درست همین جا توقف کنید! هرگز از ‎kill -9‎ استفاده نکنید. به هر دلیل. مگر اینکه برنامه‌ای نوشتید که دارید SIGKILL به آن ارسال می‌کنید، و می‌دانید که می‌توانید آلودگی‌هایی که باقی می‌گذارد را پاک کنید. زیرا در حال اشکالزدایی آن هستید.

اگر یک پردازش به سیگنالهای معمول پاسخ نمی‌دهد، احتمالاً در«وضعیت D» است(به طوریکه در ‎ps u‎ نشان داده شده)، که به معنی «در حال اجرای یک فراخوان سیستم» خواهد بود. اگر این حالت است، شما با یک دیسک سخت بی‌حس، یا سرویس‌دهنده NFS مرده، یا یک باگ کرنل، یا موردی مشابه اینها روبرو هستید. و شما در هر صورت قادر نخواهید بود پردازش را kill کنید، یا SIGKILL نمایید.

اگر پردازش از سیگنالهای خاتمه دهنده معمول صرفنظر می‌کند، آنوقت کد منبع آن را به دست آورده و آن را اصلاح کنید!

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

اگر درک نمی‌کنید که چرا این حالتی مشابه برش دادن نان با اره برقی است، ‎Who's [sic] idea was this?‎ و ‎The UUOK9 Form Letter‎ را بخوانید.

اطمینان حاصل نمایید که این فرمانها را اجرا کرده‌اید و فهمیده‌اید:

  • help kill

  • help trap

  • man pkill

  • man pgrep

خوب، اکنون بیایید به موضوعات جالب بپردازیم....

پرسش‌های پیشرفته

می‌خواهم دو job را در پس‌زمینه اجرا کنم، و سپس تا پایان یافتن هر دو منتظر بمانم.

به طور پیش‌فرض، wait برای خارج شدن تمام پردازشهای فرزندِ پوسته شما در انتظار می‌ماند.

 job1 &
 job2 &
 wait

می‌توانید یک یا چند job را مشخص کنید (یا توسط PID، یا به وسیله jobspec -- کنترل Job را ببینید). صفحه ‎help wait‎ گمراه کننده است (اشاره دارد که تنها یک شناسه می‌تواند ارائه شود)، به جای آن به مستندات کامل Bash مراجعه نمایید.

هیچ راهی برای در انتظار «خاتمه یافتن پردازش foo، یا وقوع موردی دیگر» وجود ندارد، غیر از تنظیم یک trap، که فقط در صورتی کمک خواهد نمود که «وقوع موردی دیگر» یک سیگنالی باشد که به اسکریپت ارسال می‌شود.

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

چگونه می‌توانم کنترل کنم که اگر سرویس‌دهنده game من هنوز در حال اجرا هست؟ من یک اسکریپت در crontab قرار بدهم، و اگر در حال اجرا نیست آن را مجدداً راه‌اندازی کنم...

ما اغلب اوقات با این سَبک پرسش (به اشکال مختلف) مواجه می‌شویم. کاربر سرویس کمکی (daemon) دارد، و می‌خواهد هنگامیکه سرویس می‌میرد، آنرا مجدداً راه‌اندازی کند. بله، احتمالاً شخص می‌تواند یک اسکریپت bash بنویسد که خروجی ps (یا ترجیحاً pgrep در صورتیکه سیستم شما دارای آن هست) را تجزیه کند، و برای حدس زدن آن که کدام ID پردازش متعلق به سرویسی است که ما می‌خواهیم، و فهمیدن آنکه آیا هنوز وجود دارد تلاش کند. اما این بدون ضابطه و خطرناک است. روشهای بسیار مناسب‌تری وجود دارد.

اکثر سیستمهای یونیکس از قبل دارای یک ویژگی هستند که اجازه می‌دهد شما پردازشهای مرده را مجدداً تولید کنید: init و inittab. اگر می‌خواهید هنگامیکه یک سرویس کمکی مُرد، بلافاصله یک نمونه جدید از آن ایجاد کنید، به طور نمونه تمام آنچه لازم دارید آن است که یک سطر مناسب در فایل ‎/etc/inittab‎ قرار بدهید، با عمل ‎"respawn"‎ در ستون سوم و فراخوانی پردازش در ستون چهارم. سپس ‎telinit q‎ یا معادل آن در سیستم خودتان را برای وادار نمودن init به خواندن مجدد فایل inittab اجرا کنید.

برخی سیستم‌های یونیکس inittab ندارند، و شاید بعضی مدیران سیستم خواهان کنترل پالاینده‌ای بر سرویس‌ها و جزییات فعالیت‌هایشان باشند. ممکن است لازم باشد آن افراد نگاهی به daemontools، یا runit بیاندازند.

این مطلب به موضوع برنامه‌های self-daemonizing‎[1]‎ منجر می‌گردد. در سالهای دهه 80 گرایشی وجود داشت برای آنکه سرویس‌های کمکی یونیکس از قبیل inetd به طور خودکار خودشان را در پس‌زمینه قرار بدهند. اگرچه این مورد در تمام سیستم‌های با طعم یونیکس گسترده است، به نظر می‌رسد مخصوصاً در سیستم‌های BSD رایج باشد.

مشکل با این مطلب آن است که هر روش معقول مدیریت یک سرویس کمکی مستلزم آن است که شما بعد از شروع سرویس آن را پیگردی نمایید. اگر به init گفته شود، یک فرمان را مجدداً تولید کند، به سادگی آن فرمان را به عنوان یک فرزند راه‌اندازی کرده، سپس فراخوان سیستم ‎wait()‎ را به کار می‌برد، بنابراین، وقتی فرزند خارج می‌شود، والد می‌تواند یکی دیگر تولید مثل نماید. Daemontools به همین طریق کار می‌کند: یک اسکریپت run فراهم شدهِ کاربر، محیط رامستقر می‌سازد، و آنوقت پردازش را exec می‌کند، بدان وسیله به مدیر گروهِ daemontools صاحب اختیاری مستقیم پدرانه، شامل ورودی و خروجی استاندارد و غیره اعطا می‌کند.

اگر یک پردازش خودش را در پس‌زمینه (double-forks) دوشاخه کند (روشی که inetd و sendmail و named عمل می‌کنند)، به طور عمدی، ارتباط با والدش را قطع می‌کند. این باعث غیر قابل مدیریت شدن آن می‌گردد، دیگر پدر نمی‌تواند خروجی فرزند را دریافت کند، و دیگر نمی‌تواند برای فرزند جهت مطلع بودن از مرگش ‎wait()‎ کند. و پدر حتی از ID پردازش سرویس جدید آگاه نخواهد بود. فرزند بدون باقی گذاشتن حتی یک یادداشت، از خانه بیرون رفته است.

بنابراین، برای کاربران ‎Unix/BSD‎ راه‌حل های رفع و رجوع مطرح گردید... آنها «فایلهای PID» را ایجاد نمودند، که یک سرویس کمکی اجرا شده از دیر باز، شماره پردازشش را در آنها می‌نوشت، چون والد راه دیگری برای تعیین آن نداشت. اما فایلهای PID قابل اعتماد نیستند. یک پردازش می‌تواند مرده باشد، و سپس پردازش دیگری با جانشین کردن PID خود فایل بلااستفاده شده PID را ترجمه کرده باشد. یا فایل PID به سادگی می‌تواند حذف یا خراب شده باشد. آنها کوشش برای ردیابی نمودن پردازش‌ها توسط نام به جای شماره را مطرح کردند... اما اگر پردازش دارای یک نام منحصر بفرد نباشد چه؟ اگر در یک لحظه بیش از یک نمونه از آن وجود داشته باشد، مانند nfsd یا Apache چطور؟

این ترفندها و راه‌حل‌های موقت به علت hack ابتکاریِ «در پس‌زمینه قرارگفتن خودکار» فقط تک‌موردی هستند. ازدست آن خلاص بشوید، هر چیز دیگر راحت می‌شود! Init یا daemontools یا runit می‌توانند پردازش فرزند را به طور مستقیم کنترل کنند. و حتی اکثر نوآموزان بی‌تجربه می‌توانند wrapper scriptهای خودشان را بنویسند:

 #!/bin/sh
 while :; do
    /my/game/server -foo -bar -baz >> /var/log/mygameserver 2>&1
 done

آنوقت مدیریت آن به سادگی برای اجرا شدن موقع راه‌اندازی کامپیوتر، با یک & ساده برای قرار دادن آن در پس‌زمینه، و نگاه کنید! یک نمونه یگانه پس از مردن دوباره متولد می‌شود.

اکثر بسته‌های نرم‌افزاری مدرن دیگر به «در پس‌زمینه قرارگرفتن خودکار» نیاز ندارند، حتی برای آنها که (جهت سازگاری با نگارشهای قدیمی‌تر) این رفتارِ پیش‌فرض است، غالباً یک گزینه‌ای وجود دارد که کنترل پردازش را برای شخص میسر می‌سازد. به طور نمونه، smbd سامبا، اکنون مخصوصاً جهت استفاده با daemontools و برنامه‌های دیگری از این قبیل، دارای یک گزینه ‎-F‎ می‌باشد.

اگر ثمربخش نباشد، جهت پیش‌گیری از «در پس‌زمینه قرارگفتن خودکار» می‌توانید (برای بسته daemontools) استفاده از fghack را امتحان کنید.

چگونه مطمئن شوم که در یک لحظه فقط یک نسخه از اسکریپت من می‌تواند اجرا گردد؟

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

پاسخ خامی که به طور بسیار مکرر از طرف اسکریپت نویسان خیرخواه اما ناآزموده به این پرسش داده می‌شود، اجرای گونه‌هایی از این سطر فرمان ‎ps -ef | grep -v grep | grep "$(basename "$0")" | wc -l‎ برای شمارش آنکه در یک لحظه چند کپی از اسکریپت حضور دارد، خواهد بود. من حتی کوشش نخواهم نمود تشریح کنم که این رویکرد، چگونه اشتباه مهلکی می‌باشد... اگر خودتان نمی‌توانید آن را بفهمید، باید به سادگی گفتار من در باره آن را قبول کنید.

متأسفانه، bash وسیله‌ای برای قفل کردن یک فایل ندارد. پرسش و پاسخ شماره ۴۵ شامل مثالهایی از کاربرد یک دایرکتوری، پیوند نمادین، و غیره، به عنوان وسیله‌ ممانعت دوجانبه هست، اما شما به طور مستقیم نمی‌توانید یک فایل را قفل کنید.

  • من معتقد هستم شما می‌توانید ‎(set -C; >lockfile)‎ را برای ایجاد یک lockfile به طور خودکار، به کار ببرید، لطفاً این را بررسی نمایید. ( پرسش و پاسخ ۴۵) را ببینید. --Andy753421

شما همچنین می‌توانید برنامه یا اسکریپت پوسته‌تان را تحت برنامه setlock از بسته daemontools اجرا نمایید. اگر فرض شود که شما از همان lockfile برای پیش‌گیری از اجرای همگام یا همزمان اسکریپت خود استفاده می‌کنید، در آن صورت شما به طور مؤثری اطمینان ایجاد نموده‌اید که اسکریپت شما تنها یکبار اجرا خواهد شد. این هم یک مثال که در آن ما می‌خواهیم اطمینان حاصل نماییم در یک زمان مفروض فقط یک sleep در حال اجرا می‌باشد.

 $ setlock -nX lockfile sleep 100 &
 [1] 1169
 $ setlock -nX lockfile sleep 100
 setlock: fatal: unable to lock lockfile: temporary failure

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

می‌خواهم یک دسته از فایلها را به طور موازی پردازش کنم، و موقعی که یکی تمام می‌شود، بعدی را شروع کنم. و می‌خواهم مطمئن شوم در هر لحظه دقیقاً پنج job در حال اجرا می‌باشد.

بسیاری از xargsها اجرای وظایف به طور موازی را میسر می‌سازند، از جمله در FreeBSD، OpenBSD و گنو (اما در POSIX خیر):

 find . -print0 | xargs -0 -n 1 -P 5 command

همچنین ممکن است شخصی استفاده از Parallel گنو را (اگر در دسترس باشد) به جای xargs انتخاب کند، چون Parallel گنو اطمینان ایجاد می‌کند که خروجی jobهای مختلف مخلوط نمی‌شوند.

 find . -print0 | parallel -0 command | use_output_if_needed

یک برنامه C می‌تواند 5 پنج فرزند منشعب کرده و با کاربرد ‎select()‎ یا مشابه آن، با تخصیص فایل بعدی داخل صف به هر فرزندی که آمادهِ به کار بردن آن ‌باشد، آنها را به دقت مدیریت کند. اما bash دارای هیچ معادلی برای select یا poll نیست.

در یک اسکریپت جایی که حلقه خیلی بزرگ است می‌توانید sem از Parallel گنو را به کار ببرید. در اینجا 10 فقره job به طور موازی اجرا می‌شوند:

 for i in *.log ; do
   echo "$i"
   [...do other needed stuff...]
   sem -j10 gzip $i ";" echo done
 done
 sem --wait

اگر دارای Parallel گنوی نصب شده نیستید، به راه‌حل‌های کوچکتر تنزل داده می‌شوید. یک روش، تقسیم job به 5 قسمت مساوی، و سپس راه‌اندازی آنها به طور موازی است. این هم یک مثال:

   1 #!/usr/local/bin/bash
   2 # .خواندن تمام فایل (از یک فایل متن، در هر نوبت یک سطر) به داخل یک آرایه‎
   3 IFS=$'\n' read -r -d '' -a files < inputlist
   4 
   5 # .در اینجا آنچه ما برای عمل روی آنها طرح می‌کنیم‎
   6 do_it() {
   7    for f; do [[ -f $f ]] && my_job "$f"; done
   8 }
   9 
  10 # .تقسیم لیست به پنج لیست فرعی‎
  11 i=0 n=0 a=() b=() c=() d=() e=()
  12 while ((i < ${#files[*]})); do
  13     a[n]=${files[i]}
  14     b[n]=${files[i+1]}
  15     c[n]=${files[i+2]}
  16     d[n]=${files[i+3]}
  17     e[n]=${files[i+4]}
  18     ((i+=5, n++))
  19 done
  20 
  21 # پردازش لیست‌های فرعی به طور موازی‎
  22 do_it "${a[@]}" > a.out 2>&1 &
  23 do_it "${b[@]}" > b.out 2>&1 &
  24 do_it "${c[@]}" > c.out 2>&1 &
  25 do_it "${d[@]}" > d.out 2>&1 &
  26 do_it "${e[@]}" > e.out 2>&1 &
  27 wait

خواندن سطر به سطر یک فایل و آرایه‌ها و عبارت محاسباتی را برای توضیحاتِ ترکیب دستوریِ مورد استفاده در این مثال ملاحظه کنید.

حتی اگر لیست‌ها برحسب مقدار کار مورد نیاز کاملاً یکسان نیستند،این رویکرد برای هر مقصودی تقریباً کافی می‌باشد.

یک رویکرد دیگر کاربرد یک لوله با نام را شامل می‌شود، که به یک «مدیر» بگوید چه وقت یک job تمام می‌شود، به طوری که بتواند job بعدی را راه‌اندازی کند. این هم یک مثال از این رویکرد:

   1 #!/bin/bash
   2 
   3 #  به یک لوله با نام متصل خواهد شد‎ 3 شماره FD 
   4 mkfifo pipe; exec 3<>pipe
   5 
   6 # .است که ما اجرا می‌کنیم job این آن‎
   7 s() {
   8   echo Sleeping $1
   9   sleep $1
  10 }
  11 
  12 # .شروع کردن با ۳ نمونه از آن‎
  13 # .هر نوبت که یک نمونه خاتمه می‌یابد، یک سطرجدید در لوله با نام می‌نویسد‎
  14 { s 5; echo >&3; } &
  15 { s 7; echo >&3; } &
  16 { s 8; echo >&3; } &
  17 
  18 # .دیگر راه‌اندازی می‌شود job هر دفعه که ما یک سطر از لوله با نام دریافت می‌کنیم، یک ‎
  19 while read; do
  20   { s $((RANDOM%5+7)); echo >&3; } &
  21 done <&3

اگر شما به مورد سطح بالاتری از اینها نیاز دارید، احتمالاً زبان اشتباهی را در نظر گرفته‌اید.

اسکریپت من یک خط‌لوله را اجرا می‌کند. می‌خواهم وقتی اسکریپت کشته می‌شود، خط‌لوله نیز بمیرد.

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

 #!/bin/bash
 unset kids
 fifo=/tmp/foo$$
 trap 'kill "${kids[@]}"; rm -f "$fifo"' EXIT
 mkfifo "$fifo" || exit 1
 command 1 > "$fifo" & kids+=($!)
 command 2 < "$fifo" & kids+=($!)
 wait

این مثال یک FIFO با یک نویسنده و یک خواننده تنظیم می‌کند، و PIDهای آنها را در یک آرایه به نام kids ذخیره می‌کند. ‎EXIT trap‎ به تمام آنها SIGTERM را ارسال می‌کند، FIFO را حذف می‌کند، و خارج می‌شود. برای نکته‌های مربوط به استفاده از فایلهای موقت پرسش و پاسخ شماره ۶۲ را ببینید.

یک رویکرد دیگر، فعال کردن کنترل job است، که اجازه می‌دهد کل لوله‌هابه عنوان واحدها تلقی بشوند.

 #!/bin/bash
 set -m
 trap 'kill %%' EXIT
 command1 | command2 &
 wait

در این مثال با کاربرد ‎set -m‎ کنترل job را فعال می‌کنیم. قسمت %% در ‎EXIT trap‎ به job جاری (که آخرین خط‌لوله اجرا شده در پس‌زمینه برای آن در نظر گرفته می‌شود) ارجاع می‌دهد. با گفتن به bash برای کشتن job فعلی به جای فقط اخرین فرمان لوله (موردی که اگر در نظر بود از ‎$!‎ به جای %% استفاده می‌کردیم) کل آن خط‌لوله را پاک می‌کنیم.

چگونگی کار کردن با پردازش ها

بهترین روش انجام مدیریت پردازش در Bash، شروع کردن پردازش(های) مدیریت شده از اسکریپت شما، به خاطر سپردن PID آن، و بعدها به کار بردن آن PID برای انجام اموری با پردازش است.

اگر به هر شکلی میسر است، از ps و pgrep و killall، و هر ابزارِ دیگرِ تجزیه جدول پردازش پرهیز نمایید. این ابزارها هیچ سرنخی از پردازشی که شما می‌خواهید با آن صحبت کنید ندارند. آنها فقط بر اساس فیلتر کردنِ غیر قابل اعتمادِ اطلاعات تخمین می‌زنند. این ابزارها ممکن است در محیط تست کوچک شما به خوبی کار کنند، آنها ممکن است در ابتدای امر برای مدت کوتاهی خوب عمل کنند، اما به طور ناگزیر شکست خواهند خورد، زیرا یک رویکرد معیوب برای مدیریت پردازش هستند.

PIDها و والدین

در یونیکس، پردازشها بواسطه یک شماره که PID (برای ‎Process IDentifier‎) نامیده می‌شود، تعیین هویت می‌گردند. هر پردازشِ در حال اجرا دارای یک شناسه منحصر بفرد است. شما صرفاً بواسطه شماره شناسه نمی‌توانید به طور قابل اعتمادی تعیین کنید که پردازش چه وقت یا چطور شروع شده است: این شناسه تقریباً به کلی تصادفی است.

همچنین هر پردازش یونیکس دارای یک پردازش والد است. این پردازش والد(پدر) پردازشی است که آن را آغاز نموده، اما در صورتیکه پردازش والد قبل از اینکه پردازش انجام شود، خاتمه یابد می‌تواند به پردازش init تغییر یابد. (یعنی، init پردازش‌های یتیم را سرپرستی می‌کند.) فهمیدن رابطه پدر-فرزندی ضروری است، زیرا این مطلب کلید مدیریت پردازشِ قابل اطمینان در یونیکس است. یک PID پردازش بعد از آن که پردازش می‌میرد، تا هنگامی که پردازش والد آن جهت دیدن آنکه آیا پردازش خاتمه یافته و بازیابی کد خروج آن در حال wait برای PID است، هرگز قابل استفاده مجدد نمی‌شود. اگر والد خاتمه یابد، پردازش به init که این وظیفه را برای شما انجام می‌دهد بازگشت داده می‌شود.

به موجب یک علت عمده، این مطلب با اهمیتی است: اگر پردازش پدر پردازش فرزند را مدیریت کند، کاملاً می‌تواند مطمئن باشد که اگر حتی پردازش فرزند بمیرد، مادامیکه پدر برای آن wait کرده است، و تا ملتفت شود که فرزند مرده، پردازش جدید دیگری به طور تصادفی PID پردازش فرزند را بازیافت نمی‌کند. این مطلب به پردازش پدر تضمین می‌دهد که PID فرزند در نزد او، همواره به آن پردازش فرزند اشاره خواهد نمود، خواه زنده باشد یا یک «زامبی». هیچکس دیگر آن تضمین را ندارد.

ریسک اجازه مردن والد

چرا تمام اینها چنین مهم است؟ چرا شما باید مراقب باشید؟ در نظر بگیرید چه اتفاقی می‌افتد اگر یک«فایل PID» را به کار ببریم. توالی رویدادهای زیر را فرض کنید:

  1. شما یک اسکریپت boot باشید (برای مثال، یک اسکریپت در ‎/etc/init.d‎). به شما گفته می‌شود foodaemon را شروع کنید.

  2. شما پردازش فرزند foodaemon را در پس‌زمینه شروع می‌کنید و PID آن را به دست می‌آورید.
  3. شما این PID را در یک فایل می‌نویسید.
  4. خارج می‌شوید، با فرض اینکه job خود را انجام داده‌اید.
  5. بعداً، شما دوباره راه‌اندازی شده و به شما گفته می‌شود foodaemon را kill کنید.
  6. شما PID پردازش فرزند را در یک فایل جستجو می‌کنید.
  7. شما برای گفتن انجام پاکسازی و خروج، سیگنال SIGTERM را به آن PID ارسال می‌کنید.

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

ریسک تجزیه درخت پردازش

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

هنوز عده بسیار زیادی از افراد تصور می‌کنند که کامپیوترها و انسانها به یک روش عمل می کنند. آنها فکر می‌کنند که «من می‌توانم خروجی ps را بخوانم و اگر پردازش من در آنجا باشد ببینم، چرا اسکریپت من نباید همانطور عمل کند؟». چرای آن این است: شما (انشاالله) باهوش‌تر از اسکریپت خود هستید. شما خروجی ps و تمام انواع اطلاعات در مضمون آن را می‌بینید. ذکاوت شما مشخص می‌کند، «آیا این پردازشی است که من در جستجوی آن هستم؟» و نسبت به آنچه شما می‌بینید، گمان می‌کند «آهان، مثل اینکه همان است.». اولاً اسکریپت شما نمی‌تواند مضمون را به آن طریقی که مغز شما می‌تواند، پردازش کند (خیر، awk نمودن ستون چهارم و دیدن آنکه محتوی نام فرمان پردازش شما باشد، به قدر کافی مناسب نیست). ثانیاً، حتی اگر می‌توانست این وظیفه را به خوبی انجام دهد، به هیچوجه نباید تخمین زدن را انجام بدهد. نباید نیازی به آن داشته باشد.

خروجی ps غیر قابل پیش‌بینی است، بسیار وابسته به سیستم عامل است، و برای تجزیه شدن ساخته نشده. برای اسکریپت شما، تمایز قائل شدن میان ping به عنوان نام یک فرمان و پردازش دیگری از خط فرمان که ممکن است شامل کلمه مشابهی مانند piping باشد، یا کاربری به نام ping، و غیره، تقریباً نشدنی است.

این مطلب تقریباً برای هر ابزار دیگری که لیست پردازش‌ها را تجزیه میکند صادق است. برخی از آنها بدتر از سایرین هستند، اما سرانجام، تمام آنها اشتباه می‌کنند.

درست انجام بدهید

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

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

شروع یک پردازش و به خاطر سپردن PID آن

برای آغاز یک پردازش به طور ناهماهنگ (به این طریق در حالیکه پردازش در پس‌زمینه اجرا می‌شود، اسکریپت اصلی می‌تواند ادامه یابد)، عملگر & را به کار ببرید. برای به دست آوردن PID اختصاص داده شده به آن، پارامتر ! را بسط بدهید. برای مثال می‌توانید در یک متغیر ذخیره کنید:

 # Bourne shell
 myprocess -o myfile -i &
 mypid=$!

معاینه کامل پردازشِ شما یا خاتمه دادن آن

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

برای ارسال سیگنال به یک پردازش، فرمان kill را به کار می‌بریم. برای گفتن به پردازش که به موردی عمل کند، سیگنالها می‌توانند به کار بروند، اما kill نیز می‌تواند برای کنترل آنکه پردازش هنوز زنده است استفاده شود:

 # Bourne
 kill -0 $mypid && echo "My process is still alive."
 kill    $mypid ;  echo "I just asked my process to shut down."

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

جز اینکه قصد داشته باشید یک سیگنال خیلی خاص به پردازش ارسال کنید، هیچ یک از گزینه‌های دیگر kill را استفاده نکنید، مخصوصاً از به کار بردن ‎-9‎ یا SIGKILL به هر قیمت اجتناب کنید. سیگنال KILL یک سیگنال خیلی خطرناک برای ارسال به پردازش است و استفاده از آن تقریباً همیشه یک باگ است. به جای آن SIGTERM پیش‌فرض را ارسال کنید و شکیبا باشید.

جهت انتظار برای پایان یافتن یک پردازش فرزند یا خواندن کد خروج پردازشی که قبلاً خاتمه یافته (به عنوان مثال، چون یک کنترل ‎kill -0‎ انجام دادید)، فرمان داخلی wait را به کار ببرید:

 # Bash
 night() { sleep 10; } 
              # .را به عنوان تابعی تعریف می‌کند که ده ثانیه توقف می‌کند night‎‎
              # برای یک وضعیت بیشتر واقع‌گرایانه ثانیه‌ها را مطابق فصل‎
              # .و عرض جغرافیایی تنظیم کنید‎

 night & nightpid=$!
 sheep=0
 while sleep 1; do
     kill -0 $nightpid || break     # .قطع حلقه هنگامیکه می‌بیند پردازش از بین رفته‎
     echo "$(( ++sheep )) sheep jumped over the fence."
 done

 wait $nightpid; nightexit=$?
 echo "The night ended with exit code $nightexit.  We counted $sheep sheep."

شروع یک سرویس کمکی «daemon» و کنترل آنکه آیا به طور موفق شروع گردیده است

این یک در خواست بسیار متداول است. مشکل آن است که جوابی وجود ندارد! چیزی به عنوان « daemon شروع شده به طور موفقیت‌آمیز» وجود ندارد، و اگر سرویس کمکی ویژه شما یک تعریف مناسب برای آن جمله داشته باشد، به قدری کاملاً خاصِ سرویس خواهد بود که نزد ما یک روش عمومی که برای آن شرایط به شما بگوییم چطور کنترل کنید، وجود ندارد.

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

 # Bash
 mydaemon -i eth0 & daemonpid=$!
 sleep 2
 if kill -0 $daemonpid ; then
     echo "Daemon started successfully.  I think."
 else
     wait $daemonpid; daemonexit=$?
     echo "Daemon process disappeared.  I suppose something may have gone wrong.  Its exit code was $daemonexit."
 fi

صادقانه بگوییم، این مشکل با انجام یک کنترل خاصِ سرویس بسیار بهتر حل می‌شود. برای مثال، فرض کنیم شما در حال اجرای یک سرویس‌دهنده وب به نام httpd هستید. مورد محسوس برای کنترل به منظور تعیین آنکه آیا سرویس‌دهنده وب به طور موفقیت‌آمیز شروع شده... این است که آیا واقعاً در حال ارایه محتوای وب شما هست! کسی فکر کرده بود!

 # Bourne(?)
 httpd -h 127.0.0.1 & httpdpid=$!
 while sleep 1; do
     nc -z 127.0.0.1 80 && break   #  با پورت 80 برقرار کنیم TCP دیدن اینکه اگر می‌توانیم یک اتصال‎
 done

 echo "httpd ready for duty."

به هر حال، اگر چیزی اشتباه باشد برای همیشه در تلاش برای برقراری ارتباط با پورت 80 منتظر می‌ماند. بنابراین بیایید کنترل کنیم که آیا httpd به طور غیر منتظره مرده است یا زمان مورد نظر سپری شده است:

 # Bash
 httpd -h 127.0.0.1 & httpdpid=$!
 time=0 timeout=60
 while sleep 1; do
     nc -z 127.0.0.1 80 && break   #  با پورت 80 برقرار کنیم TCP دیدن اینکه اگر می‌توانیم یک اتصال‎

     # .هنوز ارتباط برقرار نیست‎
     if ! kill -0 $httpdpid; then
         wait $httpdpid; httpdexit=$?
         echo "httpd died unexpectedly with exit code: $httpdexit"
         exit $httpdexit
     fi
     if (( ++time > timeout )); then
         echo "httpd hasn't gotten ready after $time seconds.  Something must've gone wrong.."
         # kill $httpdpid; wait $httpdpid   # را خاتمه دهید httpd اگر مایل باشید در اینجا می‌توانید‎
         exit
     fi
 done

 echo "httpd ready for duty."

در مورد پردازش‌ها، محیط‌ها و وراثت

در سیستم یونیکس هر پردازش (به استثنای init) دارای یک پردازش پدر است که موارد معینی را از آن به ارث می‌برد. پردازش می‌تواند بعضی از این موارد را تغییر بدهد، و برخی دیگر را نمی‌تواند. شما نمی‌توانید این موارد را داخل پردازش دیگری تغییر بدهید، یا با یک اشکال‌یاب به آن ضمیمه کنید (ضمیمه کنید؟) مگر آنکه والدش باشید.

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

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

به همچنین، شاید یک مدیر سیستم بخواهد به in.ftpd خودش بگوید یک umask برابر با 002 به جای هر آنچه در حال حاضر به کار می‌رود، استفاده کند. رسیدن به آن هدف نیازمندِ شناختی از آن خواهد بود که in.ftpd چگونه در سیستم او راه‌اندازی می‌شود، به عنوان یک فرزند inetd یا به عنوان یک سرویس کمکی خود اِتکا با نوعی boot script، با انجام بهبودبخشی‌های متناسب، و راه‌اندازی مجدد سرویس‌های متناسب، اگر داشته باشد.

بنابراین، بیایید یک نگاه دقیق‌تری داشته باشیم به اینکه پردازشها چگونه ایجاد می‌شوند.

مدل تولید پردازش Unix متمرکز بر دو فراخوان سیستم است: ‎fork()‎ و ‎exec()‎. (در حقیقت یک خانواده فراخوان‌های سیستم مرتبط وجود دارند که با exec شروع می‌شوند و همگی در حالت‌هایی با اختلاف جزیی رفتار می‌کنند، اما فعلاً ما تمام آنها را به طور یکسان تلقی خواهیم نمود.) ‎fork()‎ پردازش فرزندی را ایجاد می‌کند که یک نسخه دوم از پردازش والدی است که ‎fork()‎ را فراخوانی نموده (با چند استثنا). والد شماره PID (یعنی ‎Process ID‎) پردازش فرزند را به عنوان مقدار برگشتی تابع ‎fork()‎ دریافت می‌کند، در حالیکه فرزند برای گفتن به او که فرزند است یک ‎"0"‎ به دست می‌آورد. ‎exec()‎ پردازش جاری را با یک برنامه متمایز تعویض می‌کند.

بنابراین، ترتیب معمول عبارت است از:

  • یک برنامه، ‎fork()‎ را فراخوانی می‌کند و مقدار برگشتی این فراخوان سیستم را کنترل می‌کند. اگر وضعیت بزرگتر از صفر باشد، آنوقت آن پردازش پدر است، بنابراین ‎wait()‎ را با ID پردازش فرزند فراخوانی می‌کند (مگر اینکه ما بخواهیم در حالیکه فرزند در پس‌زمینه اجرا می‌شود، او ادامه بدهد).

  • اگر وضعیت 0 باشد، آنوقت پردازش فرزند است، بنابراین ‎exec()‎ را برای انجام هر آنچه که برای عمل کردن به آن در نظر گرفته شده، فراخوانی می‌کند.

  • اما قبل از آن، شاید فرزند به اجرای ‎close()‎ با بعضی توصیف‌گرهای فایل، اجرای ‎open()‎ برای چند توصیف‌گر جدید، تنظیم متغیرهای محیط، تغییر محدودیت‌های منابع، و مانند آن اقدام نماید. تمام این تغییرات بعد از ‎exec()‎ به قوت خود باقی می‌مانند و روی وظیفه‌ای که انجام می‌شود اثر خواهند کرد.

  • اگر مقدار برگشتی ‎fork()‎ منفی باشد، مورد نامناسبی واقع گردیده (حافظه برای اجرای برنامه کم بوده، یا جدول پردازش لبریز شده، و غیره.).

بیاید یک مثال از یک فرمان پوسته را در نظر بگیریم:

 echo hello world 1>&2

پردازش اجراکننده این سطر یک پوسته است، که فرمانها را می‌خواند و آنها را اجرا می‌کند. برای فرمانهای خارجی، به منظور انجام این کار مدل استاندارد ‎fork()/exec()‎ را به کار می‌برد. اجازه بدهید آن را مرحله به مرحله نشان بدهیم:

  • پوسته پدر ‎fork()‎ را فراخوانی می‌کند.

  • پدر ID پردازش فرزند را به عنوان مقدار برگشتی از ‎fork()‎ تحصیل می‌کند و برای خاتمه یافتن آن منتظر می‌ماند.

  • فرزند یک 0 از ‎fork()‎ دریافت می‌کند، بنابراین می‌داند که فرزند است.

  • فرزند برای انجام تغییر مسیر خروجی استاندارد و خطای استاندارد در نظر گرفته می‌شود (به سبب حکم ‎1>&2‎). اکنون این کار را انجام می‌دهد:

    • بستن توصیف‌گر فایل 1.
    • دونسخه‌ای کردن توصیف‌گر فایل 2، و تضمین آنکه نسخه دوم، FD شماره 1 باشد.
  • فرزند ‎exec("echo", "echo", "hello", "world", (char *)NULL)‎ یا موردی مشابه آن را برای اجرای فرمان فراخوانی می‌کند. (اینجا، ما فرض کرده‌ایم که echo یک فرمان خارجی است.)

  • وقتی echo خاتمه می‌یابد، فراخوان wait پدر نیز پایان می‌پذیرد، و پدر عملیات عادی را از سر می‌گیرد.

موارد دیگری وجود دارد که شاید فرزند قبل از اجرای فرمان نهایی انجام بدهد. برای مثال، ممکن است متغیرهای محیط را تنظیم کند:

 http_proxy=http://tempproxy:3128/ lynx http://someURL/

در این حالت، فرزند ‎http_proxy=http://tempproxy:3128/‎ را قبل از فراخوانی ‎exec()‎ داخل محیط قرار می‌دهد. محیط پدر تأثیر نمی‌پذیرد.

پردازش فرزند موارد بسیاری را از والدش به ارث می‌برد:

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

  • دایرکتوری کاری فعلی. اگر فرزند دایرکتوری کاری‌اش را تغییر بدهد، پدر هرگز چیزی در باره آن نخواهد دانست.

  • ID کاربری, ID گروه و گروه‌های مکمل. پردازش فرزند با همان امتیازات والدش تولید مثل می‌گردد. مگر اینکه پردازش فرزند با UID کاربر ارشد ‎(UID 0)‎ اجرا بشود، او نمی‌تواند این امتیازات را تغییر بدهد.
  • محدودیت‌های منابع سیستم. فرزند محدودیت‌های پدرش را ارث می‌برد. پردازشی که به عنوان UID کاربر ارشد اجرا می‌شود می‌تواند محدودیت‌های منابعش را بالا ببرد ‎(setrlimit(2))‎. پردازشی که به عنوان کاربر غیر ارشد اجرا می‌شود فقط می‌تواند محدودیت‌های منابعش را کاهش بدهد، نمی‌تواند آنها را بالا ببرد.

  • umask.

یک سیستم یونیکس فعال می‌تواند به عنوان یک درخت پردازشها با خویشاوندی پدر-فرزندی نمایش داده شده مانند اتصالات (شاخه) عمودی میان گره‌ها مشاهده گردد. برای مثال

        (init)
           |
        (login)
           |
         startx
           |
         xinit
           |
     bash .xinitrc
     /     |    \
 rxvt    rxvt   fvwm2
  |        |        \
 bash   screen       \____________________
       /   |  \              |      |     \
    bash bash  bash        xclock  xload  firefox ...
           |     |
         mutt  rtorrent

این یک نگارش ساده شده از یک مجموعه پردازشهای واقعی اجرا شده توسط یک کاربر در یک سیستم حقیقی است. من برای حفظ خوانایی موارد بسیاری را از قلم انداخته‌ام. ریشه درخت به عنوان ‎(init)‎ نشان داده شده، همچنین پردازش فرزند نخست ‎(login)‎، به عنوان root (کاربر ارشد ‎UID 0‎) در حال اجرا می‌باشد. این هم چگونگی به انجام رسیدن این سناریو:

  • کرنل (در اینجا لینوکس) برای آنکه پس از به پایان رساندن فرایند startup، پردازش ‎/sbin/init‎ را به عنوان پردازش شماره 1 اجرا نماید hard-coded شده است (مترجم: منظور آنست که اجرای آن به طور مستقم در کُد کرنل قید گردیده است). init هرگز نمی‌میرد، بالاترین نیای هر پردازشِ داخلِ سیستم است.

  • init فایل ‎/etc/inittab‎ را می‌خواند که به او می‌گوید تعدادی پردازش getty در برخی دستگاه‌های ترمینال مجازی لینوکس (از بین بقیه) تولید مثل کند.

  • هر پردازش getty مقداری اطلاعات بعلاوه یک اعلان login نمایش می‌دهد.

  • پس از خواندن یک نام کاربری، getty فایل ‎/bin/login‎ را برای خواندن کلمه عبور ‎exec()‎ می‌کند. (بدینگونه getty دیگر در درخت ظاهر نمی‌شود، خودش را تعویض می‌کند.)

  • اگر کلمه عبور معتبر باشد، login به پوسته لاگین کاربر ( bash در اینجا) ‎fork()‎ می‌کند. احتمالاً سپس در اطراف معطل می‌ماند (به جای کاربرد ‎exec()‎) زیرا می‌خواهد بعد از آنکه پوسته کاربر خاتمه می‌یابد مقداری پاکسازی انجام بدهد.

  • کاربر ‎exec startx‎ را در اعلان پوسته bash تایپ می‌کند. این باعث می‌گردد پوسته ‎startx را ‎exec()‎ کند (و بنابراین پوسته لاگین دیگر در درخت ظاهر نمی‌شود).

  • startx یک wrapper است که یک جلسه X را راه‌اندازی می‌کند ، که یک پردازش سرویس‌دهنده X (نمایش نیافته -- به عنوان root اجرا می‌شود)، و تعداد بسیار زیادی برنامه‌های سرویس گیرنده را شامل می‌شود. در این سیستم خاص، ‎.xinitrc‎ در دایرکتوری خانگی کاربر یک اسکریپت است که می‌گوید کدام برنامه‌های سرویس‌گیرنده X اجرا شوند.

  • دو شبیه‌ساز ترمینال rxvt از فایل ‎.xinitrc‎ راه‌اندازی می‌شوند (با استفاده از & در پس‌زمینه)، و هر یک از آنها یک نسخه جدید از پوسته کاربر، یعنی bash را اجرا می‌کند.

    • در یکی از آنها، کاربر فرمان ‎exec screen‎ (یا چیزی مشابه آن) را برای تعویض bash با screen تایپ کرده است. Screen به نوبت خود دارای سه پردازش فرزند bash مال خودش می‌باشد، که دو تا از آنها دارای برنامه‌های در حال اجرای مبتنی بر ترمینال ‎(mutt, rtorrent)‎ در خودشان هستند.

  • مدیر پنجره کاربر، fvwm2، توسط اسکریپت ‎.xinitrc‎ در پیش‌زمینه در حال اجرا است. یک مدیر پنجره یا محیط میزکار معمولاً آخرین مورد اجرا شده توسط اسکریپت ‎.xinitrc‎ می‌باشد، موقعی که مدیر پنجره یا محیط میز کار خاتمه یابد، اسکریپت پایان می‌پذیرد، و کل جلسه ختم می‌شود.

  • مدیر پنجره چند پردازش خودش را اجرا می‌کند ‎(xclock, xload, firefox, ...)‎. به طور نوعی دارای یک منو، یا آیکون‌ها، یا یک نوار کنترل وظایف ‎(control panel)‎، یابرخی وسایل دیگر برای راه‌اندازی برنامه‌های جدید می‌باشد. ما در اینجا پیکربندی مدیر پنجره را پوشش نمی‌دهیم.

سایر بخشهای یک سیستم یونیکس درخت‌های پردازش مشابهی را برای انجام اهدافشان به کار می‌برند، به هر حال معدودی از آنها به عنوان یک جلسه X کاملاً عمیق یا بغرنج هستند. برای مثال، inetd سرویسی را راه‌اندازی می‌کند که روی چند درگاه UDP و TCP گوش می‌ایستد، و موقعی که ارتباطات شبکه‌ای دریافت می‌کند برنامه‌هایی (ftpd و telnetd، و غیره) را راه‌اندازی می‌کند. lpd یک سرویس کمکی مدیریت برای مشاغل چاپگر را اجرا می‌کند، و هنگامی که یک چاپگر آماده است فرزند را برای به کار بردن jobهای جداگانه مامور می‌کند. sshd به رسیدن ارتباطات SSH گوش فرا می‌دهد، و وقتی آنها می‌رسند فرزند را راه‌اندازی می‌کند. برخی سیستم‌های پست الکترونیک (مخصوصاً qmail) تعداد نسبتاً زیادی از پردازشهای مشغول کار با یکدیگر را استفاده می‌کنند.

درک وابستگی میان یک مجموعه پردازش برای مدیریت یک سیستم ضروری است. برای مثال، فرض کنید شما مایل هستید روشی را که سرویس FTP شما رفتار می‌کند تغییر دهید. شما یک فایل پیکربندی که جهت خوانده شدن در زمان راه‌اندازی اولیه شناخته می‌شود، مستقر نموده‌اید و آن را تغییر داده‌اید. اکنون چه می‌شود؟ شما می‌توانستید کل سیستم را برای مطمئن شدن از اینکه تغییرات اثر می‌کند reboot کنید، اما اکثریت اشخاص آنرا عمل افراط‌آمیزی می‌دانند. به طور کلی، اشخاص ترجیح می‌دهند فقط تعداد حداقلی از پردازشها را شروع مجدد کنند، بدین طریق موجب کمترین مقدار انقطاع در سرویسهای دیگر و برای سایر کاربران سیستم می‌گردند.

بنابراین، شما به فهمیدن آنکه سرویس FTP شما چگونه راه‌اندازی می‌گردد نیاز دارید. آیا یک سرویس خود اِتکا می‌باشد؟ اگر چنین است، احتمالاً دارای چند روش سیستم-وابسته برای راه‌اندازی مجدد آن می‌باشید (یا به وسیله اجرای یک BootScript، یا کشتن و شروع مجدد آن به طور دستی، یا شاید توسط صدور برخی فرمانهای مدیریت سرویس ویژه). به طور متداول‌تر، یک سرویس FTP تحت کنترل inetd اجرا می‌شود. اگراین حالت می‌باشد، به هیچوجه احتیاجی به راه‌اندازی مجدد چیزی نیاز ندارید،. inetd هر نوبت که یک ارتباط دریافت کند یک سرویس FTP تازه مامور می‌کند، و سرویس تازه در هر نوبت فایل پیکربندی جدید را خواهد خواند.

از طرف دیگر، فرض کنید سرویس FTP شما دارای فایل پیکربندی خودش نیست که ایجاد تغییرات مورد درخواست شما (برای مثال، تغییر umask آن برای مجوزهای فایلهای بارگذاری شده) را اجازه دهد. در این حالت، می‌دانید که umask خودش را از inetd ارث می‌برد، که این نیز به نوبت خود umaskش را از هرآنچه اسکریپت boot آن را راه‌اندازی کرده به دست می‌آورد. اگر شما مایل بودید در این سناریو umask سرویس FTP را تغییر بدهید، شما می‌بایست اسکریپت بوت inetd را ویرایش و سپس inetd را kill و restart می‌نمودید، برای اینکه سرویس‌های کمکی FTP (فرزندان inetd) مقدار جدید را ارث خواهند برد. و با انجام این کار، شما همچنین umask پیش‌فرض هر سرویس دیگری که inetd مدیریت می‌کند را تغییر می‌دهید! این موضوع قابل قبول است؟ تنها شما می‌توانید به آن پاسخ بدهید. اگر نیست، آنوقت ممکن است لازم باشد شماچگونگی اجرای سرویس FTP خود را تغییر بدهید، احتمالاً با تعویض آن با یک سرویس کمکی خوداِتکا. این کار مدیر سیستم است.


CategoryShell CategoryUnix

مدیریت پردازش (آخرین ویرایش ‎2013-06-26 08:22:51‎ توسط aim-177-116)


  1. مترجم: برنامه‌هایی که نیاز به احضار به طور دستی از طریق ‎ nohup "$name" &>/dev/null & ‎  نداشته باشند (1)