فصل ۲۶ − Monitoring Child Processes

در بسیاری موارد پراسس پدر نیاز دارد تا بفهمد چه موقع یکی از پراسس‌های فرزندش تغییر وضعیت می‌دهد، مثلا کی خاتمه می‌یابد و یا به وسیله‌ی یک سیگنال STOP می‌شود. در این فصل به بررسی دو تکنیک نظارت بر پراسس‌های فرزند می‌پردازیم:

  • استفاده از سیستم کال‌های خانواده‌ی wait
  • استفاده از سیگنال SIGCHLD

سیستم کال wait(2) منتظر می‌شود تا یکی از فرزندان پراسس صدا زننده‌ی این سیستم کال terminate شود و termination status فرزند را در بافری که status به آن اشاره می‌کند قرار می‌دهد.

#include <sys/wait.h>

pid_t wait(int *status);
        Returns process ID of terminated child, or -1 on error
  1. اگر قبلا پراسس فرزندی تمام شده باشد و پدر روی آن wait نکرده باشد، سیستم کال wait فورا برمی‌گردد، در غیر این صورت منتظر terminate شدن یکی از فرزندانش خواهد شد و بلاک می‌شود.
  2. اگر status مقدار NULL نداشته باشد، termination status پراسس فرزند خاتمه یافته در جایی که status به آن اشاره می‌کند قرار می‌گیرد.
  3. The kernel adds the process CPU time and resource usage statistics to running totals for all children at this parent process. #Think
  4. سیستم کال wait به عنوان نتیجه process ID فرزند خاتمه یافته را برمی‌گرداند.

در صورت خطا سیستم کال wait(2) مقدار -1 برمی‌گرداند. یک خطای محتمل این است که پراسس صدا زننده‌ی wait یا فرزند نداشته باشد یا فرزند داشته باشد ولی قبلا روی تمام آن wait کرده باشد. در این صورت متغیر errno با مقدار ECHILD پر می‌شود.

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

while ((childPid = wait(NULL)) != -1)
    continue;
if (errno != ECHILD)                /* an unexpected error... */
    err(EXIT_FAILURE, "wait");

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

مثالی دیگر در مورد سیستم کال wait

مثال متفرقه در مورد غیر فعال کردن buffering در stdout. ترمینال به طور پیش فرض line-buffered است یعنی در صورت رسیدن به کاراکتر خط جدید بافر stdio در بافر کرنل کپی می‌شود. در این مثال ما بافر stdout را غیر فعال کرده‌ایم و کاراکترهای نوشته شده توسط تابع printf فورا در بافر کرنل نوشته می‌شوند.

محدودیت‌های سیستم کالwait(2) به قرار زیر است:

  • اگر پراسس پدر چندین فرزند ایجاد کرده باشد با سیستم کال wait فقط می‌توانیم منتظر فرزند بعدی که terminate می‌شود باشیم. امکان اینکه منتظر خاتمه‌ی فرزند بخصوصی شویم با سیستم کال wait وجود ندارد.
  • اگر پراسس خاتمه‌یافته‌ای وجود نداشته باشد wait همیشه بلاک می‌شود. گاهی اوقات مرجح است که اگر فرزندی terminate نشده سیستم کال به سرعت بازگردد.
  • با سیستم کال wait فقط از رخداد خاتمه یافتن پراسس فرزند مطلع می‌شویم. گاهی اوقات لازم است از اتفاقاتی نظیر STOP شدن فرزند در نتیجه‌ی سیگنال‌هایی مثل SIGSTOP و SIGTTIN و یا resume شدن پراسس فرزند در نتیجه‌ی تحویل سیگنال SIGCONT نیز مطلع شویم.

محدودیت‌های فوق با سیستم کال waitpid(2) مرتفع می‌گردد.

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);
        Returns process ID of child, 0 (see text), or -1 on error

آرگومان pid می‌تواند بر مبنای قوانین زیر تفسیر شود:

  • pid > 0، در اینجا pid واقعا به معنای process ID است. سیستم کال منتظر وقوع رویدادی برای فرزندی با process ID مساوی pid می‌شود.
  • pid == 0، در این حالت، سیستم کال منتظر پیشامد رویدادی برای هر فرزندی در همان process group فراخواننده‌ی سیستم کال waitpid می‌شود. مفهوم process group را در فصل ۳۴ بیان خواهیم کرد.
  • pid == -1، در این حالت waitpid منتظر فرزند خاصی نخواهد نبود. وقوع یک رویداد برای هر فرزندی قابل قبول است.
wait(&status) == waitpid(-1, &status, 0)
  • pid < -1، در این حالت سیستم کال waitpid منتظر وقوع رویدادی برای هر فرزندی که process group identifier آن مساوی قدر مطلق pid باشد می‌شود.

آرگومان options یک bit mask است که می‌تواند از OR صفر یا ترکیبی از موارد زیر ساخته شود:

  • WUNTRACED: علاوه بر برگرداندن اطلاعات در مورد فرزندان terminate شده، اگر در نتیجه‌ی رخداد یک سیگنال فرزندی STOP شد آن را هم اطلاع می‌دهد.
  • WCONTINUED: از لینوکس ۲.۶.۱۰. اگر فرزند STOP شده‌ای در نتیجه‌ی تحویل سیگنال SIGCONT اجرا را از سر گرفت و به اصطلاح resume شد آن را هم اطلاع بده.
  • WNOHANG: اگر هنوز هیچ پراسس فرزندی که با pid مشخص شده است تغییر وضعیت نداده باشد به جای اینکه سیستم کال waitpid بلاک شود فورا برمی‌گردد (یعنی یک poll اجرا می‌کند. #Think) در این حالت waitpid صفر برمی‌گرداند.

اگر پراسس فراخوان سیستم کال waitpid هیچ فرزندی که با مقدار مشخص شده در pid مطابقت داشته باشد، نداشته باشد در این حالت waitpid ناموفق تمام می‌شود و errno مقدار ECHILD می‌گیرد.

#Think

با بررسی مقدار status برگشت داده شده از سیستم کال‌های wait و waitpid می‌توان وقوع رویدادهای زیر را تشخیص داد:

  • پراسس فرزند با سیستم کال _exit خاتمه یافته است.
  • پراسس فرزند به وسیله‌ی تحویل یک سیگنال catch نشده خاتمه داده شده است.
  • پراسس فرزند به وسیله‌ی یک سیگنال STOP شده است و سیستم کال waitpid با فلگ WUNTRACED فراخوانی شده است.
  • پراسس فرزند با سیگنال SIGCONT، resume شده است و سیستم کال waitpid با فلگ WCONTINUED صدا زده شده است.

در محیط shell با چاپ مقدار $? ، termination status آخرین دستور اجرا شده را خواهیم دید.

در سیستم کال‌های wait و waitpid اگرچه متغیر status از نوع integer است ولی فقط ۲ بایت کم ارزش آن مورد استفاده قرار می‌گیرد. به خاطر بیاورید که در سیستم کال _exit(status) نیز فقط کم ارزش‌ترین بایت status به پراسس فرزند ارسال می‌شد.

شکل زیر ساختار wait status را در linux/x86-32 نشان می‌دهد. جزئیات ماجرا در پیاده‌سازی‌های مختلف فرق می‌کند و SUSv3 نیز هیچ استانداردی را برای بیان این اطلاعات تعیین نکرده است و حتی نگفته است که wait status حتما باید در ۲ بایت کم ارزش status قرار گیرد. برنامه‌های portable همیشه باید برای استخراج اطلاعات wait status از ماکروهایی که در ادامه معرفی می‌شوند استفاده کنند و از تفسیر مستقیم بیت‌ها اجتناب کنند.

اگر ۴ ماکروی زیر را بر روی wait status ای که از سیستم کال wait و یا سیستم کال waitpid گرفته شده است استفاده کنیم فقط یکی از آن‌ها مقدار true برمی‌گرداند. این ماکروها در سرفایل <sys/wait.h> تعریف شده‌اند. ماکروهای استاندارد دیگری نیز در توضیحات این ۴ ماکروها معرفی شده‌اند:

  1. WIFEXITED(status): اگر پراسس فرزند به صورت طبیعی terminate شده باشد این ماکرو مقدار true برمی‌گرداند. در این حالت ماکروی WEXITSTATUS(status) مقدار exit status پراسس فرزند را برمی‌گرداند. همان طور که در فصل قبل خواندیم فقط بایت کم‌ارزش exit status پراسس فرزند در معرض دید پراسس پدر است.
  2. WIFSIGNALED(status): اگر پراسس فرزند با دریافت یک سیگنال کشته شده باشد این ماکرو true برمی‌گرداند. در این حالت ماکروی:
    1. WTREMSIG(status) شماره‌ی سیگنالی که باعث terminate شدن پراسس فرزند شده است را برمی‌گرداند.
    2. WCORDUMP(status) اگر پراسس فرزند فایل core dump ایجاد کرده باشد true برمی‌گرداند. ماکروی WCOREDUMP() در SUSv3 استاندارد نشده است ولی در بیشتر پیاده‌سازی‌های UNIX وجود دارد.
  3. WIFSTOPPED(status): اگر پراسس فرزند با تحویل یک سیگنال STOP شده باشد این ماکرو true برمی‌گرداند. در این حالت ماکروی WSTOPSIG(status) شماره‌ی سیگنالی که باعث STOP شدن پراسس فرزند شده است را به دست می‌دهد.
  4. WIFCONTINUED(status) اگر پراسس فرزند با دریافت سیگنال SIGCONT اجرا را از سر گرفته یا resume شده باشد این ماکرو مقدار true برمی‌گرداند. این ماکرو از لینوکس ۲.۶.۱۰ اضافه شده است.

برای بررسی مقدار wait status می‌توانید از این مثال استفاده کنید. چند نمونه کاربرد این برنامه در خط فرمان در ابتدای کد منبع برنامه آورده شده است.

مثالی دیگر از سیستم کال waitpid و ماکروهای مربوطه

پراسس فرزند به صورت پیش‌فرض رویداد‌های STOP شدن، resume شدن و terminate شدن خودش را با سیگنال SIGCHLD به پراسس پدر اطلاع می‌دهد. سیستم کال wait در سمت پراسس پدر فقط موقع terminate شدن پراسس فرزند برمی‌گردد. نکات گفته شده را در این مثال بررسی کنید.

کتاب در صفحه‌ی ۵۴۸ می‌گوید که با اجرای دستور $ ulimit -c unlimited در shell اجازه می‌دهیم که فایل core dump به هر اندازه‌ای که باشد در صورت لزوم ایجاد گردد. در مثال بالا با ساطع کردن سیگنال SIGABRT برای پراسس فرزند اگرچه خروجی می‌گوید که فایل core dump ایجاد شده است ولی در واقع در دایرکتوری فعلی چیزی وجود ندارد. اشکال کار کجاست؟ #Think

توجه کنید که اگر در داخل یک signal handler با استفاده از سیستم کال _exit(status) اقدام به خاتمه‌ی برنامه کنیم این کار به معنای terminate شدن طبیعی و نرمال پراسس است و status ارسال شده به سیستم کال _exit() در سمت پراسس پدر با سیستم کال‌های wait و waitpid قابل دسترسی است.

گاهی اوقات لازم است برای رخداد یک سیگنال یک cleanup handler بنویسیم. با توجه به نکته‌ی بالا نمی‌توان در داخل signal handler بعد از clean up اقدام به فراخوانی سیستم کال _exit() کنیم چون پراسس پدر فکر می‌کند که فرزندش به صورت طبیعی خاتمه یافته است. برای اینکه هم cleanup انجام دهیم و هم به پدر بفهمانیم که پراسس فرزند در نتیجه‌ی رخداد یک سیگنال، خاتمه‌ی غیر طبیعی داشته است، بعد از عمل cleanup باید دوباره مجددا همان سیگنال را با استفاده از تابع کتابخانه‌ای raise(3) ساطع کنیم در نتیجه سیگنال هندلر قالبی به شکل زیر خواهد داشت:

void
handler(int signo)
{
    /* perform cleanup steps */
    
    signal(signo, SIG_DFL);         /* disestablish handler */
    raise(sig);                     /* raise signal again */
}

مثال. چند نمونه از اجرای برنامه در بالای کد منبع آورده شده است. مثال نکته‌ای دارد که آن را در متن برنامه با #TIP نشان داده‌ایم.

مطالعه‌ی بیشتر

نوشته شده در: 1405-03-09 (1 هفته 2 روز 9 ساعت پیش)

من محسن هستم؛ برنامه‌نویس تفننی!

برای ارتباط با من یا در همین سایت کامنت بگذارید و یا به dokaj.ir(at)gmail.com ایمیل بزنید.

در مورد این مطلب یادداشتی بنویسید.