در این فصل در میان انواع مباحثی که مطرح میکنیم به موارد زیر نیز خواهیم پرداخت:
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)
به صورت طبیعی وقتی یک سیگنال هندلر فراخوانی میشود، کرنل یک frame روی
process stack
برای آن میسازد. اما همیشه این عمل ممکن نیست. به عنوان مثال اندازهی
استک به قدری رشد کرده است که با heap که به سمت بالا پر میشود برخورد
کرده و یا استک با حافظهی map شده تداخل پیدا کرده و یا اصلا به مقدار
RLIMIT_STACK
رسیده است که یک resource limit است.
وقتی که یک پراسس سعی در بزرگتر کردن استک
از حداکثر اندازهی ممکن را دارد
کرنل سیگنال SIGSEGV را به آن پراسس ارسال میکند ولی از آنجا که فضای
استک پراسس، پر شده است کرنل نمیتواند برای هیچ هندلر نصب شدهی
سیگنال SIGSEGV فریمی در استک بسازد لذا هندلر صدا زده نمیشود و پراسس
terminate میشود.
اگر بخواهیم مطمئن شویم که در چنین شرایطی حتما هندلر نصب شده برای سیگنال
SIGSEGV
اجرا خواهد شد میتوانیم کارهای زیر را انجام دهیم:
SA_ONSTACK را ست کنید تا کرنل بفهمد که فریم
مربوط به این هندلر باید روی alternate stack ساخته شود.#include <signal.h>
typedef struct {
void *ss_sp; /* Starting address of the alternate stack */
int ss_flags; /* Flags: SS_ONSTACK, SS_DISABLE */
size_t ss_size;
} stack_t;
int sigaltstack(const stack_t *sigstack, stack_t *old_sigstack);
Returns 0 on success, or -1 on error.
سیستم کال sigaltstack(2) نیز مانند بسیاری از سیستم کالها و توابع دیگر چند کار انجام میدهد. در این مورد سیستم کال sigaltstack(2) علاوه بر معرفی signal alternate stack به کرنل، signal alternate stack قبلی را نیز برمیگرداند. در صورت عدم نیاز به هر کدام از این اطلاعات، آرگومان مربوطه را میتوانیم به NULL مقدار دهیم.
#Think

alternate signal stack
یا به صورت statically allocated و یا dynamically allocated در heap ساخته
میشود. SUSv3 ثابت SIGSTKSZ را به عنوان اندازهی معمول،
و ثابت MINSIGSTKSZ را به عنوان حداقل اندازهی
alternate stack
برای فراخوانی یک سیگنال هندلر مشخص کرده است.
دیدن مقادیر دو ثابت فوق که در سر فایل <signal.h> تعریف شدهاند.
کرنل alternate signal stack را تغییر اندازه نمیدهد لذا در صورت پر شدن این مکان، حافظهی مربوط به متغیرهای دیگر، که در مجاورت این مکان قرار گرفتهاند را رونویسی میکند و یک آشفتگی رخ میدهد. این موضوع به طور کلی مشکل بزرگی نیست چون ما به طور معمول alternate signal stack را زمانی به کار میبریم که استک استاندارد ما سر ریز کرده باشد و عموما یک یا چند فریم محدود روی این استک ایجاد میشود.
The job of the SIGSEGV handler is either to perform some cleanup and terminate
the process or to unwind the standard stack using a nonlocal goto.
فیلد ss_flags یکی از دو مقدار زیر را خواهد داشت:
SS_ONSTACK: اگر این فیلد موقعی که اطلاعات alternate signal stack فعلی
را بازیابی میکنیم ست شده باشد (یعنی آرگومان old_sigstack) به این
معنی است که پراسس هم اکنون روی alternate signal stack اجرا میشود.
در چنین موقعیتی که خود پراسس روی alternate signal stack مشغول اجراست
تلاش برای ایجاد یک alternate signal stack جدید توسط سیستم کال
sigaltstack(2)
با خطای EPERM مواجه میشود.SS_DISABLE: در آرگومان old_sigstack به معنی این است که هیچ
alternate signal stack ای ست نشده است.
در آرگومان sigstack کاری که میکند این است که
alternate signal stack
نصب شده را غیر فعال میکند.
دیدن مقادیر ثابتهای بالا
مثال از سیستم کال sigaltstack(2)
مثال همراه با استفاده از ثوابت فوق و غیر فعال کردن
alternate signal stack
بعد از تعریف آن