اینجا هستید : 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)

به صورت طبیعی وقتی یک سیگنال هندلر فراخوانی می‌شود، کرنل یک frame روی process stack برای آن می‌سازد. اما همیشه این عمل ممکن نیست. به عنوان مثال اندازه‌ی استک به قدری رشد کرده است که با heap که به سمت بالا پر می‌شود برخورد کرده و یا استک با حافظه‌ی map شده تداخل پیدا کرده و یا اصلا به مقدار RLIMIT_STACK رسیده است که یک resource limit است.

مشاهده‌ی مقدار RLIMIT_STACK

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

اگر بخواهیم مطمئن شویم که در چنین شرایطی حتما هندلر نصب شده برای سیگنال SIGSEGV اجرا خواهد شد می‌توانیم کارهای زیر را انجام دهیم:

  1. فضایی از حافظه را allocate کنید. به این فضا سیگنال استک جایگزین یا alternate signal stack می‌گوییم و از آن به عنوان فریم مربوط به سیگنال هندلر، در استک استفاده می‌کنیم.
  2. با استفاده از سیستم کال sigaltstack(2) به کرنل اطلاع دهید که چنین فضایی را به عنوان alternate signal stack رزرو کرده‌اید.
  3. موقع نصب یک سیگنال هندلر فلگ 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.

🤔 unwind کردن استک چیست؟

فیلد ss_flags یکی از دو مقدار زیر را خواهد داشت:

دیدن مقادیر ثابت‌های بالا
مثال از سیستم کال sigaltstack(2)
مثال همراه با استفاده از ثوابت فوق و غیر فعال کردن alternate signal stack بعد از تعریف آن

وقتی از سیستم کال sigaction(2) برای نصب یک سیگنال هندلر استفاده می‌کنیم اگر فلگ SA_SIGINFO را ست کنیم کرنل در موقع تحویل سیگنال اطلاعات بیشتری در اختیار هندلر قرار می‌دهد. برای این که هندلر این اطلاعات را دریافت کند تابع هندلر را باید به صورت زیر تعریف کنیم:

void handler(int sig, siginfo_t *siginfo, void *ucontext);

از آنجا که تابع سیگنال هندلر بالا پروتوتایپ متفاوتی از تابع سیگنال هندلر استاندارد دارد بر مبنای قوانین زبان C دیگر نمی‌توانیم از فیلد sa_handler در struct sigaction استفاده کنیم تا آدرس handler را مشخص کرده باشیم، بجای آن باید از فیلد sa_sigaction استفاده کنیم. در حقیقت تعریف struct sigaction اندکی از آنچه در فصل ۲۰ نشان دادیم پیچیده‌تر است.

struct sigaction {
    union {
        void (*sa_handler)(int);
        void (*sa_sigaction)(int, siginfo_t *, void *);
    } __sigaction_handler;
    sigset_t sa_mask;
    int      sa_flags;
    void   (*sa_restorer)(void);
};

/* Following defines make the union fields look like simple fields
   in the parent structure */

#define sa_handler __sigaction_handler.sa_handler
#define sa_sigaction __sigaction_handler.sa_sigaction

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

struct sigaction از یک union برای یکجا نگهداری کردن فیلدهای sa_sigaction و sa_handler استفاده می‌کند. در اینجا استفاده از union مفید و امکان‌پذیر است چون در فراخوانی سیستم کال sigaction(2) فقط به وجود یکی از این دو فیلد نیاز داریم. مثال

استراکچر siginfo_t که به عنوان دومین آرگومان به سیگنال هندلری که توسط فلگ SA_SIGINFO نصب شده ارسال می‌شود به مانند زیر است:

typedef struct {
    int     si_signo;       /* signal number */
    int     si_code;        /* signal code */
    int     si_trapno;      /* trap number for hardware-generated signal
                               (unused on most architectures) */

    union sigval si_value;  /* accompanying data from sigqueue() */
    pid_t   si_pid;         /* Process ID of sending process */
    uid_t   si_uid;         /* real user ID of sender */
    int     si_errno;       /* error number (generally unused) */
    void   *si_addr;        /* address that generated signal
                               (hardware generated signals only) */
    
    int     si_overrun;     /* overrun count (linux 2.6, POSIX timers) */
    int     si_timerid;     /* (kernel-internal) Timer ID,
                               (Linux 2.6, POSIX timers) */

    long    si_band;        /* band event (SIGPOLL/SIGIO) */
    int     si_fd;          /* file descriptor (SIGPOLL/SIGIO) */
    int     si_status;      /* exit status or signal (SIGCHLD) */
    clock_t si_utime;       /* user CPU time (SIGCHLD) */
    clock_t si_stime;       /* system CPU time (SIGCHLD) */
} siginfo_t;

❗ کتاب می‌گوید که به منظور مشاهده‌ی struct siginfo از سر فایل <signal.h> «فیچر تست ماکرو»ی _POSIX_C_SOURCE باید با مقداری بزرگتر یا مساوی 199309 تعریف شده باشد. در مثال بالا نیازی به این کار نشد!

در بسیاری از پیاده‌سازی‌های یونیکس از جمله لینوکس، از آن جا که تمام فیلدها برای همه‌ی سیگنال‌ها مورد استفاده قرار نمی‌گیرند، بخش زیادی از فیلدهای استراکچر siginfo_t در یک union با هم نگهداری می‌شوند. فایل <siginfo.h> را ببینید.

به محض ورود کنترل به سیگنال هندلر فیلدهای استراکچر siginfo_t به صورت زیر مقدار می‌گیرند:

  1. si_signo : این فیلد برای همه‌ی سیگنال‌ها ست می‌شود. مقدارش برابر sig، اولین آرگومان ارسالی به هندلر، است. مثال
  2. si_code : این فیلد نیز برای همه‌ی سیگنال‌ها ست می‌شود و حاوی یک کد است که اطلاعات بیشتری را در مورد منشا سیگنال آشکار می‌کند.
  3. si_value : این فیلد دیتایی که به عنوان همراه از طریق تابع کتابخانه‌ای sigqueue(3) ارسال می‌شود را نگهداری می‌کند.
  4. si_pid : اگر سیگنال توسط سیستم کال kill(2) و یا تابع کتابخانه‌ای sigqueue(3) ارسال شده باشد این فیلد با PID پراسس ارسال کننده‌ی سیگنال پر می‌شود.
  5. si_uid : اگر سیگنال توسط سیستم کال kill(2) و یا تابع کتابخانه‌ای sigqueue(3) ارسال شده باشد این فیلد به real user ID پراسس ارسال کننده‌ی سیگنال مقدار داده می‌شود. RUID حاوی اطلاعات مفیدتری نسبت به EUID است. #Think
  6. si_errno : این فیلد که در لینوکس عموما مورد استفاده قرار نمی‌گیرد در صورتی که مقداری غیر از صفر داشته باشد علت رخداد سیگنال را نشان می‌دهد.
  7. si_addr: این فیلد فقط برای سیگنال‌های SIGBUS و SIGSEGV و SIGILL و SIGFPE که منشا سخت‌افزاری دارند ست می‌شود:
    • برای سیگنال‌های SIGBUS و SIGSEGV این فیلد با آدرس جایی که باعث invalid memory reference شده است پر می‌شود.
    • برای سیگنال‌های SIGILL و SIGFPE این فیلد با آدرس program instruction که باعث سیگنال شده پر می‌شود. (مثال از SIGFPE)

دو فیلد غیر استاندارد زیر که مخصوص لینوکس هستند فقط هنگام تحویل سیگنالی که در موقع انقضای یک POSIX timer تولید می‌شود ست می‌شوند:

  1. si_timerid : این فیلد حاوی ID ای است که کرنل به صورت داخلی برای شناسایی timer به کار می‌برد.
  2. si_overrun : این فیلد با مقدار overrun count تایمر پر می‌شود.

دو فیلد زیر فقط در زمان تحویل سیگنال SIGIO مقداردهی می‌شوند:

  1. si_band : This field contains the "band event" value associated with the I/o event.
  2. si_fd : این فیلد که در SUSv3 مشخص نشده است ولی در بسیاری از پیاده‌سازی‌های یونیکس وجود دارد شماره‌ی file descriptor مربوط با رخداد I/O را نگهداری می‌کند.

سه فیلد زیر فقط در زمان تحویل سیگنال SIGCHLD پر می‌شوند:

  1. si_status : اگر فیلد si_code برابر با CLD_EXITED باشد این فیلد به معنای exit status پراسس فرزند است در غیر این صورت به معنی شماره‌ی سیگنال ارسال شده به پراسس فرزند است که حال یا فرزند را خاتمه داده است یا آن را stop کرده است.
  2. si_utime : این فیلد user CPU time ای که به وسیله‌ی پراسس فرزند مصرف شده است را برمی‌گرداند.
  3. si_stime : این فیلد system CPU time ای که به وسیله‌ی پراسس فرزند مصرف شده است را برمی‌گرداند.

فیلد si_code اطلاعات تکمیلی در مورد منشا سیگنال برمی‌گرداند. تمام ثابت‌های زیر در لینوکس تعریف شده و بیشتر آن‌ها نیز در SUSv3 استاندارد شده‌اند ولی تمام آن‌ها در تمام پیاده‌سازی‌های یونیکس و معماری‌های سخت‌افزاری رخ نمی‌دهند (به خصوص در مورد چهار سیگنال SIGBUS و SIGSEGV و SIGILL و SIGFPE که توسط سخت‌افزار تولید می‌شوند.)

در جدول بالا دو مقدار SI_KERNEL و SI_SIGIO مخصوص لینوکس هستند؛ در SUSv3 مشخص نشده‌اند و در پیاده‌سازی‌های دیگر یونیکس وجود ندارند. خود SI_SIGIO فقط در لینوکس ۲.۲ به کار گرفته شد و از کرنل ۲.۴ به بعد لینوکس از ثوابت POLL_* که در جدول نشان داده شده‌اند استفاده می‌کند.

تابع psiginfo(3) عملکردی مشابه تابع‌های perror(3) و psignal(3) دارد. این تابع اشاره‌گری به متغیری از نوع siginfo_t می‌گیرد و اطلاعات آن را تا حدودی نمایش می‌دهد:

#include <signal.h>

void psiginfo(const siginfo_t *pinfo, const char *s);

مثال از تابع psiginfo(3)

اگر سیگنال هندلر را با فلگ SA_SIGINFO نصب کنیم سومین آرگومان هندلر اشاره‌گر به ساختاری از نوع ucontext_t است که در سر فایل <ucontext.h> تعریف شده است. SUSv3 برای این آرگومان از نوع اشاره‌گر به void استفاده کرده چون این استاندارد هیچ جزيیاتی از این آگومان را مشخص نکرده است. این آرگومان user-context-information که مشخص کننده‌ی وضعیت پراسس قبل از فراخوانی سیگنال هندلر است را نگهداری می‌کند از جمله سیگنال mask پراسس و مقادیر ذخیره شده‌ی رجیسترها را. این اطلاعات به ندرت در سیگنال هندلرها مورد استفاده قرار می‌گیرند لذا وارد این مبحث نمی‌شویم.

🚫 کاربرد دیگر استراکچر ucontext_t همراه با توابع getcontext(3) و makecontext(3) و setcontext(3) و swapcontext(3) است که execution context ها را بازیابی، ایجاد، تغییر و یا swap می‌کنند. این عملیات‌ها تا حدودی شبیه setjmp() و longjmp() ولی عمومی‌تر هستند. از این توابع می‌توان برای پیاده‌سازی coroutine ها استفاده کرد، جایی که جریان اجرای برنامه بین دو یا چند تابع می‌چرخد. SUSv3 این توابع را با «منسوخ» علامت‌گذاری کرده است و SUSv4 آن‌ها را حذف کرده است و پیشنهاد می‌کند که اپلیکیشن‌ها باید با POSIX thread ها بازنویسی شوند.

سناریوی زیر را در نظر بگیرید:

  1. برای چند سیگنال، هندلر نصب می‌کنیم.
  2. یک سیستم کال بلاک شونده را صدا می‌زنیم، مثلا read()، تا از یک ترمینال بخواند و تا زمانی که ورودی فراهم شود سیستم کال بلاک می‌شود.
  3. در حالی که سیستم کال بلاک شده است یک سیگنالی که برای آن هندلر نصب کرده‌ایم از راه می‌رسد و تحویل پراسس می‌شود و سیگنال هندلر فراخوانده می‌شود.

به طور معمول بعد از سناریوی بالا، سیستم کال، ناموفق تمام می‌شود و متغیر errno با خطای EINTR ست می‌شود. این ویژگی گاهی اوقات مفید است. بعدا خواهیم دید که چگونه با استفاده از یک تایمر (که در زمان پایان یافتنش یک سیگنال SIGALRM تحویل پراسس می‌شود) می‌توان یک timeout روی یک سیستم کال بلاک‌شونده مثل read() گذاشت.

گذاشتن timeout روی سیستم کال read

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

while ((cnt = read(fd, buf, BUF_SIZE)) == -1 && errno == EINTR)
    continue;           /* resume read() system call */

if (cnt == -1)          /* read() failed with other than EINTR */
    err(EXIT_FAILURE, "read");

مثال از restart کردن سیستم کال در صورت رخداد سیگنال

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

#define NO_EINTR(stmt) while ((stmt) == -1 && errno == EINTR);

// usage of this macro
NO_EINTR(cnt = read(fd, buf, BUF_SIZE));
if (cnt == -1)          /* read() failed with other than EINTR */
    err(EXIT_FAILURE, "read");

پیاده‌سازی نکته‌ی بالا

📝 کتابخانه‌ی C پروژه‌ی GNU در سرفایل <unistd.h> ماکروی غیر استانداردی به نام TEMP_FAILURE_RETRY() دارد که با #define _GNU_SOURCE در دسترس برنامه قرار می‌گیرد. این ماکرو عملکردی شبیه ماکروی NO_EINTR(stmt) دارد که اینجا نوشته‌ایم.
سعی کنید از این ماکرو در برنامه‌ی بالا استفاده کنید.

این که بخواهیم برای تمام سیستم‌کال‌های بلاک شونده به شیوه‌ی بالا عمل کنیم تا در صورت وقوع سیگنال و اجرا شدن سیگنال هندلر، دوباره پراسس سیستم کال را اجرا کند و روی آن بلاک شود قدری مشکل است به همین خاطر از فلگ SA_RESTART هنگام نصب سیگنال هندلر با سیستم کال sigaction(2) استفاده می‌کنیم. با این کار کرنل خودش سیستم کال اینتراپت خورده را از طرف پراسس دوباره اجرا می‌کند؛ یعنی دیگر لازم نیست تا برنامه‌نویس خطای EINTR احتمالی این سیستم کال‌ها را چک کند.

🔔 فلگ SA_RESTART بر مبنای سیگنال عمل می‌کند یعنی سیستم کال را در صورتی restart می‌کند که هندلرِ سیگنالِ از راه رسیده با فلگ SA_RESTART نصب شده باشد ولی در پیاده‌سازی‌های بالا هر سیگنالی که catch می‌شد سیستم کال restart می‌شد. در این مثال پراسس در صورت دریافت سیگنال SIGUSR1 سیستم کال read(2) را restart می‌کند و در صورت دریافت سیگنال SIGUSR2 خیر.

متاسفانه در نتیجه‌ی ست کردن فلگ SA_RESTART تمام سیستم کال‌های بلاک شونده restart نمی‌شوند. علت این موضوع تا حدود زیادی تاریخی است. (ر.ک ۴۴۳)

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

  1. سیستم کال‌هایی که برای wait کردن روی پراسس فرزند به کار می‌روند:
  2. سیستم کال‌های I/O وقتی که با slow device ها به کار برده شوند: #Think
  3. سیستم کال open(2) در موقعیت‌هایی که می‌تواند بلاک شود مانند باز کردن فایل‌های FIFO.
  4. چندین سیستم کالی که با سوکت‌ها استفاده می‌شوند:
  5. سیستم کال‌هایی(؟) که برای I/O روی POSIX message queue ها به کار می‌روند:
  6. سیستم کال‌ها و توابع کتابخانه‌ای که برای قرار دادن قفل روی فایل‌ها استفاده می‌شوند:
  7. عملیات FUTEX_WAIT مربوط سیستم کال مخصوص لینوکس futex(2)
  8. توابع زیر که برای کاهش (قفل کردن) یک POSIX semaphore استفاده می‌شوند:
  9. توابعی که برای همزمان کردن POSIX thread ها استفاده می‌شوند:

سیستم کال‌های بلاک شونده‌ی زیر (و توابع کتابخانه‌ای که روی این سیستم کال‌ها پیاده‌سازی شده‌اند) فارغ از این که سیگنال هندلر نصب شده از فلگ SA_RESTART استفاده کرده باشد یا نه در صورت سیگنال خوردن restart نمی‌شوند:

  1. سیستم کال‌های I/O multiplexing زیر:
    • poll(2)
    • ppoll(2)
    • select(2)
    • pselect(2)
      SUSv3 صراحتا ذکر می‌کند که رفتار select(2) و pselect(2) وقتی توسط یک سیگنال هندلر وقفه می‌خورند، فارغ از وضعیت SA_RESTART، نامشخص است.
  2. دو سیستم کال‌های مخصوص لینوکس:
  3. سیستم کال مخصوص لینوکس io_getevents(2)
  4. سیستم کال‌های بلاک شونده که توسط message queue ها و سمافورهای System V استفاده می‌شوند:
    • semop(2)
    • semtimedop(2)
    • msgsnd(2)
    • msgrcv(2)
      (اگر چه System V به صورت سنتی امکان restat شدن سیستم کال‌ها را فراهم نمی‌کند در بعضی از پیاده‌سازی‌های یونیکس این سیستم کال‌ها در صورت ست شدن فلگ SA_RESTART ری‌استارت می‌شوند.)
  5. read(2) از یک inotify file descriptor.
  6. سیستم کال‌ها و توابع کتابخانه‌ای که به منظور ساسپند کردن اجرای یک برنامه برای مدت مشخصی طراحی شده‌اند:
  7. سیستم کال‌هایی که طراحی شده‌اند تا منتظر رسیدن یک سیگنال باشند:

با تابع siginterrupt(3) می‌توان وضعیت فلگ SA_RESTART مربوط به یک سیگنال را تغییر داد:

#include <signal.h>

int siginterrupt(int sig, int flag);
            Returns 0 on success, or -1 on error

اگر flag دارای ارزش درستی باشد (یک) در آن صورت هندلرِ مربوط به سیگنال داده شده اجازه دارد که سیستم کال بلاک شونده را با وقفه روبرو سازد و اجرای آن را کاملا متوقف کند ولی اگر فلگ نادرست باشد (صفر) در آن صورت کرنل بعد از اجرای هندلر مربوط به سیگنال داده شده، سیستم کال بلاک شونده‌ی وقفه خورده را restart می‌کند.

📝 تابع siginterrupt(3) به این صورت پیاده‌سازی می‌شود که با سیستم کال sigaction(2) یک کپی از disposition فعلی سیگنال را در oldact بازیابی می‌کنیم و سپس فیلد sa_flags آن را با توجه به نیاز تغییر می‌دهیم یعنی فلگ SA_RESTART را ست یا unset می‌کنیم و بار دیگر با سیستم کال sigaction(2)، این بار disposition سیگنال را آپدیت می‌کنیم. SUSv4 تابع siginterrupt(3) را منسوخ علامت‌گذاری کرده است و توصیه کرده که به جای آن از سیستم کال sigaction(2) استفاده کنیم.

پیاده سازی تابع کتابخانه‌ای siginterrupt(3)

#Think

موضوع reentrancy باعث می‌شود که مجموعه‌ی توابعی که بتوانیم در داخل یک سیگنال هندلر استفاده کنیم کاهش بیابد.

یک سیگنال هندلر علاوه بر return کردن به طرق دیگری نیز می‌تواند خاتمه بیابد:

با استفاده از توابع sigsetjmp(3) و siglongjmp(3) می‌توانیم کنترل آشکاری روی process signal mask در یک nonlocal goto داشته باشیم.

با سیستم کال sigaltstack(2) می‌توانیم برای پراسس یک alternate signal stack تعریف کنیم. این استک فضایی از حافظه است که از آن می‌توان بجای استک استاندارد پراسس در زمان فراخوانی یک سیگنال هندلر استفاده کرد. alternate signal stack برای زمانی مفید است که استک استاندارد برنامه پر شده باشد. در چنین حالتی کرنل به پراسس سیگنال SIGSEGV ارسال می‌کند ولی در نبود alternate signal stack بخاطر اینکه استک استاندارد پر شده است امکان اجرای هندلر سیگنال SIGSEGV وجود ندارد.

فلگ SA_SIGINFO در سیستم کال sigaction(2) اجازه می‌دهد هندلری نصب کنیم که اطلاعات بیشتری در مورد سیگنال رخ داده شده بگیرد. این اطلاعات از طریق یک استراکچر از نوع siginfo_t که آدرسش به عنوان آرگومان به هندلر ارسال می‌شود در اختیار هندلر قرار می‌گیرد.

وقتی یک سیگنال هندلر، یک سیستم کال بلاک شده را با وقفه روبرو سازد، سیستم کال با خطای EINTR خاتمه می‌باید. سیستم کال‌های وقفه خورده می‌توانند به صورت دستی restart شوند. با استفاده از فلگ SA_RESTART در هنگام نصب یک سیگنال هندلر با سیستم کال sigaction(2) می‌توان بسیاری از سیستم کال‌ها (ولی نه همه‌ی آن‌ها) را به صورت خودکار بعد از اجرای سیگنال هندلر restart کرد.

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

  1. sigaction(2)
  2. strdup(3)
  3. signal-safety(7)
  4. async-signal-safe functions (image)
  5. kill(2)
  6. raise(3)
  7. process termination functions:
  8. nonlocal goto functions:
  9. sigaltstack(2)
  10. sbrk(2)
  11. system_data_types(7)
  12. sigqueue(3)
  13. psiginfo(3)
  14. signal(7)
  15. setsockopt(2)
© کلیه‌ی حقوق برای safarionline.ir محفوظ است.