You are here : safarionline.ir / books / lpi / ch21

فصل ۲۱- Siganls: Signal Handlers

در این فصل در میان انواع مباحثی که مطرح می‌کنیم به موارد زیر نیز خواهیم پرداخت:

  • بحث در مورد بازگشت کردن طبیعی از یک signal handler و به طور ویژه به استفاده از nonlocal goto برای این منظور
  • هندل کردن سیگنال‌ها روی یک alternate stack
  • استفاده از فلگ SA_SIGINFO در سیستم کال sigaction(2) برای اینکه سیگنال هندلر بتواند اطلاعات بیشتری در مورد سیگنالی که باعث فراخوانی‌اش شده است به دست بیاورد.
  • چطور یک سیستم کال بلاک شده ممکن است توسط یک سیگنال هندلر وقفه بخورد و چگونه اگر علاقمند بودیم سیستم کال interrupt خورده را دوباره از نو آغاز کنیم.

به طور کلی مرجح است که سیگنال هندلرها کوچک و ساده باشند. یکی از دلایل مهم برای این کار، جلوگیری از شرایط رقابتی یا race condition است.

دو طرح رایج برای سیگنال هندلرها به صورت زیر است:

  1. سیگنال هندلر یک فلگ سراسری را ست می‌کند و سپس return می‌کند. برنامه‌ی اصلی به صورت دوره‌ای این فلگ را بررسی می‌کند و اگر ست شده باشد عملیات تعیین شده را انجام می‌دهد. در فصل ۶۳ در مورد این تکنیک و روش‌های پیاده‌سازی آن بیشتر بحث می‌کنیم.
  2. سیگنال هندلر cleanup انجام می‌دهد و بعد یا پراسس را terminate می‌کند و یا با استفاده از یک nonlocal goto استک را unwind می‌کند و کنترل به یک نقطه‌ی از پیش تعریف شده در برنامه‌ی اصلی برمی‌گردد. #Think

یک سیگنال در زمانی که سیگنال هندلر همان سیگنال در حال اجراست در صورت رخداد مجدد تحویل پراسس نمی‌شود یعنی آن سیگنال بلاک می‌شود، در عوض در لیست سیگنال‌های pending قرار می‌گیرد و بعد از اینکه سیگنال هندلر return کرد به پراسس تحویل داده می‌شود. این رفتار را می‌توان با استفاده از فلگ SA_NODEFER در سیستم کال sigaction(2) تغییر داد.

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

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

  1. توابع reentrant and nonreentrant:
    در برنامه‌ی single thread در یک پراسس فقط یک جریان اجرای دستورات داریم ولی در یک برنامه‌ی multi thread چندین جریان اجرای مستقل و همزمان در داخل یک پراسس وجود دارد. در فصل ۲۹ می‌بینیم که چگونه میتوان برنامه‌های چند thread ی نوشت. در این جا فقط لازم است که بدانیم مفهوم چند thread در مورد برنامه‌هایی که برای سیگنال‌ها، هندلر نوشته‌اند نیز صادق است چون سیگنال هندلرها نیز می‌توانند جریان اجرای برنامه را در هر لحظه تغییر دهند. در حقیقت در اینجا برنامه‌ی اصلی و سیگنال هندلر، تشکیل دو thread مستقل (اگر چه غیر همزمان) در داخل یک پراسس می‌دهند.
    وقتی گفته می‌شود که یک تابع reentrant است یعنی چند thread در یک پراسس می‌توانند به طور همزمان آن را اجرا کنند و تابع نتیجه‌ای که واقعا قصد دارد را بگیرد فارغ از ترتیب اجرای thread ها و یا وضعیت هر کدام از آن‌ها. به توابعی که دارای این شرط هستند thread safe می‌گویند.
    تابعی که یک دیتا استراکچر global و یا static را تغییر می‌دهد ممکن است که reentrant نباشد ولی تابعی که فقط متغیرهای محلی را به کار می‌گیرد قطعا reentrant است. (ر.ک ۴۲۳)

    • چنین کاربردهایی از متغیرهای global و در توابع کتابخانه‌ای C استاندارد بسیار شایع و فراوان است. برای مثال توابع کتابخانه‌ای malloc(3) و free(3) از linked list برای نگهداری بلاک‌های حافظه‌ی آزادی که در heap قرار دارند و آماده‌ی اختصاص یافتن هستند استفاده می‌کنند. اگر اجرای تابع malloc(3) با فراخوانی یک سیگنال هندلر که آن هم تابع malloc(3) را فراخوانی می‌کند دچار وقفه شود آنگاه این linked list ممکن است دچار آسیب شود و در پایان مقدار صحیحی نداشته باشد. به همین خاطر خانواده‌ی توابع malloc(3) و سایر توابع کتابخانه‌ای که از آن‌ها استفاده می‌کنند reentrant نیستند.
    • بعضی دیگر از توابع کتابخانه‌ای reentrant نیستند چون از statically allocated memory برای برگرداندن اطلاعات به تابع فراخوان استفاده می‌کنند. اگر سیگنال هندلر از یکی از این توابع استفاده کند آنگاه اطلاعات قرار گرفته در آن حافظه که در نتیجه‌ی فراخوانی آن تابع در برنامه‌ی اصلی بوده است رونویسی می‌شود. مثال از این نوع توابع فراوان است: crypt(3) و getpwnam(3) و gethostbyname(3) و getserverbyname(3) و ...
    • بعضی توابع دیگر reentrant نیستند چون از متغیرهای static برای عملیات داخلی خود استفاده می‌کنند. مثال آشکار از این نوع کاربرد توابع کتابخانه‌ی stdio مثل printf(3) و scanf(3) و نظایر آن‌ها هستند که دیتا استراکچرهای داخلی خودشان را برای نگهداری I/O بافر شده آپدیت می‌کنند. به همین خاطر به عنوان مثال وقتی در داخل یک سیگنال هندلر از تابع کتابخانه‌ای printf(3) استفاده می‌کنیم بعضی وقت‌ها خروجی‌های عجیب و غریبی را می‌بینیم. (ر.ک ۴۲۳)
    • حتی اگر از هیچ تابع کتابخانه‌ای nonreentrant ای هم استفاده نکنیم باز ممکن است با پدیده‌ی nonreentrancy مواجه شویم. اگر سیگنال هندلر یک متغیر global را که خود برنامه‌نویس تعریف کرده است و برنامه‌ی اصلی آن را آپدیت می‌کند تغییر دهد باز هم سیگنال هندلر نسبت به برنامه‌ی اصلی nonreentrant است.

      مثال (توضیحات این برنامه را در صفحه‌ی ۴۲۴ کتاب مطالعه کنید.)
      یک مثال ساده‌تر. تابع getpwuid(3) یک تابع nonreentrant است زیرا به عنوان نتیجه‌ی تابع یک مقدار statically allocated برمی‌گرداند.
  2. توابع async-signal-safe:
    تابعی async-signal-safe است که پیاده‌سازی آن تضمین می‌کند که فراخوانی آن از داخل یک سیگنال هندلر کاملا ایمن است و هیچ اثر جانبی پیش‌بینی نشده‌ای ندارد. تابعی این ویژگی را دارد که یا reentrant باشد و یا زمان اجرای تابع هیچ سیگنالی باعث وقفه در اجرای تابع نشود.

موقعی که یک سیگنال هندلر را طراحی می‌کنیم دو گزینه پیش رو داریم:

  1. مطمئن شویم که کد خود سیگنال هندلر reentrant باشد و فقط توابع async-signal-safe را فراخوانی کند.
  2. وقتی در برنامه‌ی اصلی هستیم و تابعی ناایمن یا unsafe function را فراخوانی می‌کنیم یا با یک ساختار داده‌ی سراسری کار می‌کنیم قبلش حتما تمام سیگنال‌ها را بلاک کنیم تا اجرای تابع با رخداد یک سیگنال و اجرای سیگنال هندلر دچار وقفه نشود.

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

اگر یک هندلر را برای چند سیگنال ست کنیم و یا در موقع نصب یک هندلر با استفاده از سیستم کال sigaction(2) از فلگ SA_NODEFER استفاده کرده باشیم، آنگاه اجرای هندلر می‌تواند با رخداد یک سیگنال مشابه دچار وقفه شود. در این صورت اگر هندلر اقدام به بروزرسانی متغیرهای global یا static کند، یک تابع nonreentrant است حتی اگر این متغیرها توسط برنامه‌ی اصلی استفاده نشوند. (مطالعه‌ی بیشتر signal-safety(7))

در لیست توابع بالا به هر حال اکثر آن‌ها ممکن است متغیر سراسری errno را تغییر دهند و این عمل باعث می‌شود که این توابع دیگر reentrant نباشند. راه حل فوری این مساله آن است که errno را ابتدای سیگنال هندلر ذخیره کنیم و در پایان هندلر آن مقدار را دوباره در متغیر errno بنشانیم:

void
handler(int sig)
{
    int savedErrno;

    savedErrno = errno;

    /* Now we can execute a function that might modify errno */

    errno = savedErrno;
}

در بسیاری از مثال‌های کتاب، توابع stdio را در داخل سیگنال هندلرها استفاده کرده‌ایم. در اپلیکیشن‌های واقعی باید جدا از این کار پرهیز کرد چون توابع این کتابخانه async-signal-safe نیستند.

به هر حال اگر طراحی برنامه می‌گفت که یک متغیر باید global تعریف شود تا هم برنامه‌ی اصلی و هم سیگنال هندلر همزمان به آن دسترسی داشته باشند لازم است که آن را با volatile attribute تعریف کنیم تا جلوی این که کامپایلر روی آن optimization انجام دهد و آن را در رجیستر ذخیره کند را بگیریم.

خواندن و نوشتن یک متغیر global ممکن است بیشتر از یک دستور زبان ماشین باشد و همان طور که می‌دانیم سیگنال هندلر هر لحظه ممکن است جریان برنامه‌ی اصلی را با وقفه روبرو سازد. به همین خاطر استانداردهای زبان C و SUSv3 یک نوع داده‌ی integer به نام sig_atomic_t را تعریف کرده‌اند که خواندن و نوشتن در آن به صورت اتمیک تضمین شده است. بنابر این متغیر سراسری share شده بین برنامه‌ی اصلی و سیگنال هندلر باید به صورت زیر تعریف شود:

volatile sig_atomic_t flag;

دقت کنید که operator های ++ و -- در بعضی معماری‌های سخت‌افزار به صورت اتمیک اجرا نمی‌شوند و این opertaor ها جزو تضمین‌های نوع داده‌ی sig_atomic_t نیستند. (برای اصلاعات بیشتر به صفحه‌ی ۶۳۱ و مباحث آن رجوع کنید.)

All that we are guaranteed to be safely allowed to do with a sig_atomic_t variable is set it whitin the signal handler, and check it in the main program (or vice versa).

استاندارد C99 و SUSv3 ذکر کرده‌اند که پیاده‌سازی‌های یونیکس باید دارای دو ثابت تعریف شده در سر فایل <stdint.h> به نام‌های SIG_ATOMIC_MIN و SIG_ATOMIC_MAX باشند که حدود مقادیر مجاز برای ذخیره در نوع داده‌ی sig_atomic_t را مشخص می‌کنند.

مشاهده‌ی مقدار ثوابت فوق

تا اینجا اکثر سیگنال هندلرهایی که تعریف کرده‌ایم return می‌کنند و برنامه‌ی اصلی از نقطه‌ی دریافت سیگنال دوباره شروع به اجرا می‌کند ولی بسته به نوع اپلیکیشن سناریوهای دیگری هم وجود دارند:

  • با سیستم کال _exit(2) می‌توان پراسس را terminate کرد. توجه داشته باشید که نمی‌توانیم از تابع exit(3) در سیگنال هندلر استفاده کنیم چون async-signal-safe نیست؛ و بافرهای stdio را قبل از فراخوانی _exit(2) فلاش می‌کند.
  • استفاده از سیستم کال kill(2) و یا تابع کتابخانه‌ای raise(3) جهت ارسال سیگنالی به خود پراسس که رفتار پیش‌فرض در مقابل آن سیگنال process termination باشد.
  • انجام یک nonlocal goto از داخل سیگنال هندلر
  • استفاده از تابع کتابخانه‌ای abort(3) برای خاتمه‌ی برنامه همراه با ایجاد یک فایل core dump.

در فصل ۶ در مورد استفاده از توابع کتابخانه‌ای setjmp(3) و longjmp(3) برای انجام یک nonlocal goto از یک تابع به یکی از توابع فراخواننده‌اش صحبت کردیم. همین تکنیک را می‌توانیم در یک سیگنال هندلر استفاده کنیم. این روش راهی برای برون رفت از سیگنالی که از منشا exception های سخت‌افزاری مثل memory access error نشئت می‌گیرد فراهم می‌کند. کاربرد دیگر این تکنیک بردن کنترل به مکان مشخصی از برنامه بعد از دریافت سیگنال است. این روشی است که shell در هنگام دریافت سیگنال SIGINT انجام می‌دهد یعنی یک nonlocal goto به ابتدای حلقه‌ی اصلی برنامه می‌کند و منتظر دریافت دستور بعدی می‌شود.

ایده‌های فوق خیلی خوب هستند ولی مشکلی وجود دارد. قبلا دیدیم که وقتی سیگنال هندلر می‌خواهد اجرا شود کرنل سیگنال مربوط به آن هندلر را به اضافه‌ی سیگنال‌های لیست شده در فیلد act.sa_mask را به لیست سیگنال‌های mask شده‌ی پراسس اضافه می‌کند و وقتی که هندلر یک return طبیعی انجام داد این سیگنال‌ها را از لیست سیگنال‌های mask شده حذف می‌کند. در هنگام انجام longjmp(3) رفتار پیاده‌سازی‌های مختلف یونیکس در مورد سیگنال‌های mask شده یکسان نیست و لذا استفاده از longjmp(3) روش قابل حملی برای خروج از یک سیگنال هندلر نیست. (فرق بین پیاده‌سازی System V و BSD ها را در صفحه‌ی ۴۲۹ مطالعه کنید.)

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

#include <setjmp.h>

int sigsetjmp(sigjmp_buf env, int savesigs);
            Returns 0 on initial call, nonzero on return via siglongjmp()

void siglongjmp(sigjmp_buf env, int val);

توابع sigsetjmp(3) و siglongjmp(3) مانند توابع نظیرشان setjmp(3) و longjmp(3) عمل می‌کنند. تنها تفاوت آن‌ها در نوع آرگومان env است که اینجا از نوع sigjmp_buf تعریف شده است. علاوه بر این sigsetjmp(3) آرگومان دومی نیز دارد. اگر این آرگومان مقداری غیر صفر داشته باشد در هنگام فراخوانی تابع، سیگنال mask های فعلی پراسس در آرگومان env ذخیره می‌شوند و در هنگام صدا زدن تابع siglongjmp(3) با همان env قبلی، این سیگنال‌ها restore می‌شوند. اگر این مقدار صفر باشد دیگر process signal mask نه ذخیره می‌شود و نه restore می‌گردد.

#Think

The fundtion siglongjmp(3) restores the signal mask to the value it had at the time of the sigsetjmp(3) was called.

استاندارد SUSv3 اجازه نمی‌دهد تا توابع setjmp(3) و sigsetjmp(3) در انتساب‌ها (assignments ها) به کار بروند.

s = sigsetjmp(senv, 1);     /* incorrect */

مثال. اگر این مثال را با ماکروی USE_SIGSETJMP کامپایل کنید از توابع sigsetjmp(3) و siglongjmp(3) استفاده می‌کند. این موضوع را می‌توانید با فشردن دگمه Control-C و تولید سیگنال SIGINT بعد از بازگشت کنترل از سیگنال هندلر بررسی کنید. در اینجا با بازگشت کنترل به وسیله‌ی تابع siglongjmp(3) به خارج از سیگنال هندلر، سیگنال mask های پراسس به سیگنال mask های زمان فراخوانی تابع sigsetjmp(3) بازگردانده می‌شود. اگر برنامه را بدون ماکروی USE_SIGSETJMP کامپایل کنید از توابع setjmp(3) و longjmp(3) استفاده می‌کند و سیگنال‌های بلاک شده در زمان اجرای سیگنال هندلر بعد از longjmp(3) بلاک شده باقی می‌مانند.

برای کامپایل برنامه با ماکروی داده شده به صورت زیر عمل می‌کنیم:

gcc -D USE_SIGSETJMP sigmask_longjmp.c

تابع کتابخانه‌ای abort(3) پراسس صدا زننده‌ی خودش را با ساطع کردن یک سیگنال SIGABRT خاتمه می‌دهد و یک فایل core dump نیز ایجاد می‌کند. می‌دانیم که terminate کردن پراسس و ایجاد core dump رفتار پیش‌فرض در مواجهه با سیگنال SIGABRT است.

#include <stdlib.h>

void abort(void);
  • اگر SIGABRT قبلا ignore شده باشد تابع abort(3) ابتدا disposition آن را به حالت پیش‌فرض برمی‌گرداند و سپس سیگنال SIGABRT را ساطع می‌کند. مثال
  • اگر SIGABRT هندلر داشته باشد دو حالت پیش می‌آید:
    • اگر هندلر به صورت طبیعی return کند abort(3) پراسس را خاتمه می‌دهد و فایل core dump ایجاد می‌کند. مثال
    • اگر هندلر برنگردد و یا به دیگر کنترل با یک nonlocal goto از هندلر فرار کند abort(3) کاری انجام نمی‌دهد. مثال

abort(3) همیشه پراسس را terminate می‌کند مگر زمانی که هندلر نوشته شده برای SIGABRT با یک nonlocal goto کنترل را به قسمتی دیگری از برنامه منتقل کند و هندلر return نکند.

دو روش برای خاتمه دادن به یک سیگنال هندلر وجود دارد:

  1. return کردن آشکار در هندلر و یا رسیدن کنترل به پایان تابع هندلر
  2. استفاده از یک nonlocal goto در هندلر و فرستادن کنترل به جای دیگری در برنامه.

در بسیاری از پیاده‌سازی‌های یونیکس terminate شدن پراسس بعد از فراخوانی تابع abort(3) به صورت زیر تضمین می‌شود:

📝 اگر پراسس بعد از ساطع شدن سیگنال SIGABRT خاتمه نیابد یعنی یک هندلر، سیگنال را catch کرده است و بعد از پایان کارش return کرده، در اینجا abort(3) هندلر سیگنال SIGABRT را به SIG_DFL ست می‌کند و مجددا سیگنال SIGABRT را ساطع می‌کند که این بار یقینا منجر به خاتمه یافتن پراسس می‌شود.

پیاده‌سازی تابع abort(3)

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

کلیه‌ی حقوق برای safarionline.ir محفوظ است.