در این فصل در میان انواع مباحثی که مطرح میکنیم به موارد زیر نیز خواهیم پرداخت:
SA_SIGINFO در سیستم کال
sigaction(2)
برای اینکه سیگنال هندلر بتواند اطلاعات بیشتری در مورد سیگنالی که باعث
فراخوانیاش شده است به دست بیاورد.به طور کلی مرجح است که سیگنال هندلرها کوچک و ساده باشند. یکی از دلایل مهم برای این کار، جلوگیری از شرایط رقابتی یا race condition است.
دو طرح رایج برای سیگنال هندلرها به صورت زیر است:
یک سیگنال در زمانی که سیگنال هندلر همان سیگنال
در حال اجراست در صورت رخداد مجدد
تحویل پراسس نمیشود یعنی آن سیگنال بلاک میشود،
در عوض در لیست
سیگنالهای pending قرار میگیرد و بعد از اینکه سیگنال هندلر return
کرد به پراسس تحویل داده میشود. این رفتار را میتوان با استفاده از فلگ
SA_NODEFER
در سیستم کال sigaction(2) تغییر داد.
سیگنالها صف نمیشوند. اگر سیگنالی بلاک شده باشد و چندین بار رخ دهد بعد از آنبلاک شدن فقط یکبار تحویل پراسس میشود به این خاطر که لیست سیگنالهای pending با bitmask پیادهسازی شده است و فقط وجود سیگنال را نشان میدهد و نه تعداد تکرار آن را.
همهی توابع کتابخانهای و یا سیستم کالها را نمیتوان به صورت ایمن در سیگنال هندلرها فراخوانی کرد. برای فهمیدن علت باید با دو مفهوم زیر آشنا شویم:
توابع reentrant and nonreentrant:
در برنامهی single thread در یک پراسس فقط یک جریان اجرای دستورات داریم
ولی در یک برنامهی multi thread چندین جریان اجرای مستقل و همزمان در
داخل یک پراسس وجود دارد. در فصل ۲۹ میبینیم که چگونه میتوان برنامههای
چند thread ی نوشت. در این جا فقط لازم است که بدانیم مفهوم چند thread در
مورد برنامههایی که برای سیگنالها، هندلر نوشتهاند نیز
صادق است چون سیگنال هندلرها نیز میتوانند جریان اجرای برنامه را در
هر لحظه تغییر دهند. در حقیقت در اینجا برنامهی اصلی و سیگنال هندلر،
تشکیل دو thread مستقل (اگر چه غیر همزمان) در داخل یک پراسس میدهند.
وقتی گفته میشود که یک تابع reentrant است یعنی چند thread در یک
پراسس میتوانند به طور همزمان آن را اجرا کنند و تابع نتیجهای که
واقعا قصد دارد را بگیرد فارغ از ترتیب اجرای thread ها و یا وضعیت هر کدام
از آنها. به توابعی که دارای این شرط هستند thread safe میگویند.
تابعی که یک دیتا استراکچر global و یا static را تغییر میدهد ممکن است
که reentrant نباشد ولی تابعی که فقط متغیرهای محلی را به کار میگیرد
قطعا reentrant است. (ر.ک ۴۲۳)
توابع async-signal-safe:
تابعی async-signal-safe است که پیادهسازی آن تضمین میکند که
فراخوانی آن از داخل یک سیگنال هندلر کاملا ایمن است و هیچ اثر جانبی پیشبینی
نشدهای ندارد. تابعی این ویژگی را دارد که یا reentrant باشد و یا
زمان اجرای تابع هیچ سیگنالی باعث وقفه در اجرای تابع نشود.
موقعی که یک سیگنال هندلر را طراحی میکنیم دو گزینه پیش رو داریم:
در یک برنامهی بزرگ پیادهسازی روش دوم و اطمینان از صحت بلاک شدن تمام سیگنالها در توابع 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) فلاش میکند.در فصل ۶ در مورد استفاده از توابع کتابخانهای
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 هندلر داشته باشد دو حالت پیش میآید:
abort(3)
همیشه پراسس را terminate میکند مگر زمانی که هندلر
نوشته شده برای SIGABRT با یک nonlocal goto کنترل را به قسمتی دیگری از برنامه
منتقل کند و هندلر return نکند.
دو روش برای خاتمه دادن به یک سیگنال هندلر وجود دارد:
در بسیاری از پیادهسازیهای یونیکس terminate شدن پراسس بعد از فراخوانی تابع abort(3) به صورت زیر تضمین میشود:
📝 اگر پراسس بعد از ساطع شدن سیگنال SIGABRT خاتمه نیابد یعنی یک هندلر، سیگنال
را catch کرده است و بعد از پایان کارش return کرده، در اینجا abort(3)
هندلر سیگنال SIGABRT را به SIG_DFL ست میکند و مجددا سیگنال
SIGABRT را ساطع میکند که این بار یقینا منجر به خاتمه یافتن
پراسس میشود.
پیادهسازی تابع abort(3)