قصه: Linux System Programming

در این قصه

  1. فهرست کتاب Linux System Programming
  2. فصل ۱۰ - Time (همین پست)

فصل ۱۰ - Time

کرنل گذشت زمان را در سه حالت مختلف اندازه‌گیری می‌کند:

  1. Wall time (or real time)
  2. Process time
  3. Monotonic time

Real time یا Wall time دقیقا مانند زمانی است که از روی ساعت دیواری می‌خوانیم.

Process time زمانی است که پروسس CPU مصرف کرده است، خواه به صورت مستقیم در user space و یا زمانی که کرنل در مد کرنل برای انجام کار پروسس CPU را در اختیار داشته است. از این زمان برای profiling و فهمیدن اینکه یک عملیات چقدر طول می‌کشد استفاده می‌کنیم. به خاطر ماهیت multitask بودن لینوکس برای پروفایلینگ نمی‌توانیم از Wall time استفاده کنیم چون Process time می‌تواند بسیار کمتر از Wall time باشد.

  • uptime = Time since boot
  • UTC = Universal Time, Coordinated
  • GMT = Greenwich Mean Time
  • UTC = GMT = ZULU time
  • RFC = Request For Comment

در مورد Monotonic time نکته‌ی مهم این است که مبدا ثابت دارد و دائما به مقدار فعلی آن با روند ثابتی افزوده می‌شود و مقدار آن به هیچ طریق دیگری جز به طریق پیش‌گفته تغییر نمی‌کند. در حالی که Wall time را می‌توان به راحتی تغییر داد یا دوباره تنظیم کرد.

Time را به دو صورت زیر نیز می‌توان تقسیم‌بندی کرد.

  1. Relative time: نسبت به یک مبدا مشخص اندازه گیری می‌شود، مثلا ۵ ثانیه از حالا یا ۱۰ دقیقه قبل.
  2. Absolute time: نقطه‌ای دقیق در زمان مثل ۲۵ مارس ۱۹۶۸.

مبدا اندازه‌گیری Absolute time در سیستم یونیکس ساعت صفر ۱ ژانویه‌ی ۱۹۷۰ در تایم زون UTC است. یعنی در یونیکس حتی Absolute time هم Relative time است!

سیستم‌های عامل پیشرفت زمان را از طریق software clock که توسط کرنل مدیریت می‌شود ثبت و رهگیری می‌کنند. کرنل system timer را راه اندازی‌میکند که در فواصل زمانی مشخص سیگنال می‌دهد. هر وقت سیگنال رسید یک واحد به زمان سپری شده اضافه می‌شود. هر واحد زمانی یک tick یا jiffy نامیده می‌شود. شمارنده‌ی تیک‌ها یا جیفی‌ها امروزه یک متغیر ۶۴ بیتی است.

در لینوکس فرکانس system timer را HZ می‌نامند و مقدار آن بستگی به معماری CPU دارد لذا برنامه‌ها نباید وابسته به آن باشند یا انتظار مقدار خاصی را داشته باشند. مقدار ۱۰۰ به معنای آن است که تایمر سیستم در یک ثانیه ۱۰۰ بار اجرا می‌شود یعنی فرکانس ۱۰۰ هرتز دارد. این به معنای آن است که هر جیفی ۱/۱۰۰ ثانیه است یعنی هر جیفی 1/HZ است. در کرنل ۲.۶ فرکانس تایمر سیستم یا همان HZ به یکباره به ۱۰۰۰ افزایش یافت ولی در نسخه‌ی ۲.۶.۱۳ به ۲۵۰ کاهش یافت. فرکانس‌های بالاتر باعث context switch های بیشتر می‌شوند و هزینه‌های سربار بالاتری را به CPU تحمیل می‌کنند. (پاورقی صفحه‌ی ۳۰۹ مطالعه شود.)

POSIX می‌گوید که باید با استفاده از تابع sysconf فرکانس تایمر سیستم یا همان HZ را در زمان اجرا run time پیدا کرد.

پیدا کردن فرکانس تایمر سیستم در run time

یعنی چی؟

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

$ sudo apt install util-linux-extra
$ hwclocw

در این فصل به بررسی مباحث زیر می‌پردازیم:

  1. Setting and retrieving the current wall time.
  2. Calculating elapsed time.
  3. Sleeping for a given amount of time.
  4. Performing high-precision measurements of time.
  5. Controlling timers.

time_t تعداد ثانیه‌های گذشته از epoch را نگهداری می‌کند. time_t عددی علامتدار است و از نوع long تعریف می‌شود. در یک سیستم ۳۲ بیتی در سال ۲۰۳۸ overflow می‌کند. دقت time_t برحسب ثانیه است.

استراکچر struct timeval دقت بر حسب میکروثانیه را فراهم می‌کند و در سر فایل <sys/time.h> تعریف شده است.

#include <sys/time.h>

struct timeval {
   time_t      tv_sec;     /* seconds */
   suseconds_t tv_usec;    /* microseconds, normally an integer type */
}

استراکچر struct timespec که در سر فایل time.h تعریف شده است زمان را با دقت نانوثانیه نگهداری می‌کند.

#include <time.h>
    
struct timespec {
    time_t  tv_sec;     /* seconds */
    long    tv_nsec;    /* nanoseconds */
}

در عمل چون تایمر سیستم دقت میکروثانیه و نانوثانیه را فراهم نمی‌کند هیچ کدام از این دو structure عدد صحیح را ذخیره نمی‌کنند. با این حال بهتر است از این استراکچرها استفاده کنیم تا اگر سیستم این دقت‌ها را فراهم کرده بود مشکلی در ذخیره و بازیابی نداشته باشیم. (ر.ک. ۳۱۱)

Miliseconds  = 0 .. 999
Microseconds = 0 .. 999,999
Nanoseconds  = 0 .. 999,999,999

struct timespec نوع داده‌ی احمقانه‌ی suseconds_t را دور انداخته است و از نوع داده‌ی ساده long استفاده می‌کند.

C استاندارد struct tm را برای نگهداری broken down time معرفی کرده است. این استراکت اطلاعات تاریخ و زمان رابه تفکیک در فیلدهای human readable نگهداری می‌کند.

#include <time.h>

struct tm {
    int tm_sec;             /* seconds [0-60] 60 for leap seconds */
    int tm_min;             /* minutes [0-59] */
    int tm_hour;            /* hours [0-23] */

    int tm_mday;            /* the day of the month [0-31]
                               0 = last day of previous month */
    int tm_mon;             /* the month [0-11] */
    int tm_year;            /* the year [number of years since 1900] */

    int tm_wday;            /* the day of the week [0-6] 0=sunday */
    int tm_yday;            /* the day in the year [0-365] */
    int tm_isdst;           /* daylight saving time? */

#ifdef _BSD_SOURCE
    long tm_gmtoff;         /* time zone's offset from GMT */
    const char *tm_zone;    /* time zone abbr */
#endif /* _BSD_SOURCE */
}

در مورد فیلد tm_mday استاندارد POSIX مقدار صفر را مشخص نکرده است با این حال لینوکس عدد صفر را برای آخرین روز ماه قبل به کار می‌برد.

عدد قرار گرفته در فیلد tm_isdst می‌گوید که در محاسبه‌ی سایر فیلدهای این استراکت آیا DST لحاظ شده است یا خیر.

  1. tm_isdst < 0: the state of DST is unknown.
  2. tm_isdst == 0: DST is not in effect.
  3. tm_isdst > 0: DST is in effect.

broke down کردن time_t داده شده به struct tm

نوع داده‌ی clock_t نشان دهنده‌ی تعداد clock tickها است و از نوع integer و معمولا یک عدد long است. بسته به نوع اینترفیس عددی که در داخل نوع داده‌ی clock_t قرار می‌گیرد فرکانس تایمر سیستم HZ و یا CLOCKS_PER_SEC را نشان می‌دهد.

نوع داده‌ی clockid_t نشان‌دهنده‌های داده‌های نوع POSIX clock است. لینوکس ۴ POSIX clock را پشتیبانی می‌کند.

  1. CLOCK_MONOTONIC ساعتی که با نرخ یکنواخت رشد می‌کند و قابل تنظیم توسط هیچ پروسسی نیست و زمان گذشته شده از یک نقطه‌ی مشخص نشده را اندازه‌گیری می‌کند.
  2. CLOCK_PROCESS_CPUTIME_ID یک ساعت مخصوص خود پروسس. به عنوان مثال در معماری i386 این ساعت از رجیستر timestamp counter (TSC) استفاده می‌کند.
  3. CLOCK_REALTIME ساعت realtime یا همان wall time سیستم. این ساعت با داشتن دسترسی‌های ویژه قابل ست کردن است.
  4. CLOCK_THREAD_CPUTIME_ID مثل ساعت مخصوص هر پروسس ولی این بار در مخصوص هر thread داخل یک پروسس

گرچه استاندارد POSIX این ۴ نوع منبع زمان (time sources) را تعریف کرده است ولی فقط به CLOCK_REALTIME احتیاج دارد. لذا کدهای portable فقط باید به CLOCK_REALTIME اتکا کنند.

سیستم کال clock_getres برای به دست آوردن resolution مربوط به time source داده شده به کار می‌رود. تابع در صورت عدم موفقیت errno را ست می‌کند. در صورت موفقیت خروجی در struct timespec فراهم شده به عنوان آرگومان دوم قرار می‌گیرد.

#include <time.h>
 
int clock_getres(clockid_t clock_id, struct timespec *res);
            returns 0 on success, -1 on error

به نظر منظور از resolution دقت ساعت است که مثلا هر ۴ میلی‌ثانیه یک تیک می‌زند یا هر ۱ نانوثانیه. بدیهی است که در مورد دوم دقت ساعت بالاتر است.

مثال از ۴ منبع time

کتاب می‌گوید که این برنامه‌ها باید با کتابخانه librt لینک شوند ولی در هنگام کامپایل و لینک نیازی به این کتابخانه نشد.

#include <time.h>

time_t time(time_t *t);
        returns elapsed seconds since the Epoch on success,
                       (time_t) -1 on error and sets errno.

تنها خطای ممکن در مورد سیستم کال time اشاره‌گر نامعتبر به عنوان آرگومان اول است که میشود به راحتی آن را NULL گذاشت.

مثال از سیستم کال time

در مورد time_t نکته این نیست که دقیق است بلکه این است که محاسبه‌ی پایدار و ساده‌ای دارد.

اینترفیس gettimeofday دقت بیشتری در حد میکروثانیه فراهم می‌کند.

#include <sys/time.h>

int gettimeofday(struct timeval *tv, struct timezone *tz);
                         returns 0 on success, -1 on error

استفاده از آرگومان دوم منسوخ شده است و همیشه باید با NULL مقداردهی شود چون کرنل دیگر timezone را manage نمی‌کند. تنها خطایی که این سیستم کال ممکن است با آن روبرو شود اشاره‌گرهای نامعتبر به عنوان آرگومان ورودی است. در این صورت متغیر errno مقدار EFAULT می‌گیرد.

سیستم کال clock_gettime برای به دست آوردن زمان یک time source مشخص استفاده می‌شود. نکته‌ی مهم این است که دقت در حد نانو ثانیه را فراهم می‌کند. در صورت بروز خطا علاوه بر برگرداندن -1 متغیر سراسری errno را هم ست می‌کند.

#include <time.h>
 
int clock_gettime(clockid_t id, struct timespec *ts);
                    returns 0 on success, -1 on error

مشاهده‌ی مقادیر time source های مختلف

با منبع زمان CLOCK_REALTIME و سیستم کال clock_gettime می‌توان زمان فعلی را مشابه سیستم کال time و gettimeofday این بار با دقت در حد نانو محاسبه کرد. مثال

سیستم کال times زمان مصروف شده‌ی CPU توسط پروسس فراخوان و فرزندانش را به تفکیک user cpu time و system cpu time برمی‌گرداند.

#include <sys/times.h>

struct tms {
    clock_t tms_utime;      /* user time consumed */
    clock_t tms_stime;      /* system time consumed */
    clock_t tms_cutime;     /* user time consumed by children */
    clock_t tms_cstime;     /* system time consumed by children */
};

clock_t times(struct tms *buf);
    returns number of clock ticks since an arbitrary point
                      in the past on success, -1 on error.

user time زمانی است که مصروف اجرای کد در مد کاربر CPU می‌شود. بالعکس system time زمانی است که CPU در حال اجرای کد کرنل از طرف پروسس کاربر است یعنی در مد سوپروایزر CPU قرار دارد مثلا در هنگام اجرای یک سیستم کال یا هندل کردن یک نقص صفحه.

اطلاعاتی که در مورد مصرف CPU توسط فرزندان گزارش می‌شود فقط مربوط به child های terminate شده‌ای است که پروسس پدر روی آن‌ها wait کرده است.

مقداری که سیستم کال times برمی‌گرداند تعداد کلاک تیک‌هایی است که نسبت به یک زمان دلخواه سنجیده شده است. این زمان دلخواه موقعی هنگام بوت سیستم بود یعنی خروجی سیستم کال times در واقع uptime سیستم را به صورت clock tick برمی‌گرداند. اما امروزه مبدا این زمان در حدود ۴۲۹ میلیون ثانیه قبل از بوت سیستم است (ر.ک. ۳۱۸) لذا مقدار برگشتی سیستم کال times بی ارزش است ولی اختلاف دو مقدار برگشتی times کماکان مفید است چون مقدار برگشتی times به صورت یکنواخت و monotonic افزایش می‌یابد.

برای تنظیم ساعت و تاریخ سیستم قبلا از سیستم کال stime استفاده می‌شود که امروزه منسوخ شده است. به جای آن از clock_settime(2) استفاده کنید.

سیستم کال متناظر gettimeofday سیستم کال settimeofday است که زمان سیستم را با دقت میکروثانیه ست می‌کند.

#include <sys/time.h>

int settimeofday(const struct timeval *tv, const struct timezone *tz);
                                     returns 0 on success, -1 on error

همانند سیستم کال gettimeofday در سیستم کال settimeofday هم tz را NULL قرار می‌دهیم.

پروسس فراخوان تابع settimeofday باید دارای توانایی CAP_SYS_TIME باشد در غیر این صورت سیستم کال با شکست مواجه می‌شود و errno با EPERM مقداردهی می‌شود.

از نسخه‌ی ۴.۳ لینوکس به بعد در سیستم کال settimeofday امکان مقداردهی فیلد ثانیه tv_sec در استراکت timeval به مقداری کمتر از CLOCK_MONOTONIC که uptime سیستم را ثبت می‌کند وجود ندارد و با خطای EINVAL روبرو می‌شویم. مثال

همان طور که سیستم کال clock_gettime امکانات بیشتری نسبت به gettimeofday فراهم می‌کند سیستم کال clock_settime نیز عملکرد settimeofday را ارتقا می‌دهد.

#include <time.h>

int clock_settime(clockid_t clock_id, const struct timespec *ts);
                returns 0 on success, -1 on error (and set errno)

clock_id در واقع time source ما است. خطاهایی که ممکن است این سیستم کال با آن روبرو شود عبارتند از EFAULT و EINVAL و EPERM

در بسیاری از سیستم‌ها تنها time source قابل ست کردن CLOCK_REALTIME است. لذا تنها مزیت این سیستم کال نسبت به settimeofday این است که دقت نانوثانیه در ست کردن زمان فراهم می‌کند.

مثال از سیستم کال clock_settime

تابع asctime یک struct tm که در واقع broken-down time است را گرفته و رشته‌ی اسکی human readable معادل آن را برمی‌گرداند.

#include <time.h>

char *asctime(const struct tm *tm);
char *asctime_r(const struct tm *tm, char *buf);
            both functions returns NULL on error

واضح است که تابع asctime یک تابع thread safe نیست و اشاره‌گری به یک string که به صورت statically allocated توسط تابع گرفته شده است برمی‌گرداند. در فراخوانی بعدی این string رونویسی می‌شود. برای thread safe بودن (برنامه‌های mutlithread) از نسخه‌ی reentrant این تابع به نام asctime_r استفاده کنید. در این صورت به جای استفاده از بافر statically allocated خودمان بافری به طول حداقل ۲۶ کاراکتر برای تابع فراهم می‌کنیم.

تابع mktime مشابه تابع asctime یک اشاره‌گر به struct tm می‌گیرد ولی time_t برمی‌گرداند. دقت کنید که پارامتر struct tm پیشوند const ندارد چون mktime محتویات این استراکت را در صورت نیاز اصلاح می‌کند.

#include <time.h>

time_t mktime(struct tm *tm);
        returns (time_t) -1 on error

تابع ctime اشاره‌گر به time_t می‌گیرد و اسکی معادل آن time_t را برمی‌گرداند. خروجی تابع به صورت statically allocated است و thread safe نیست و در فراخوانی بعدی تابع آن فضا رونویسی می‌شود.

#include <time.h>

char *ctime(const time_t *timep);
char *ctime_r(const time_t *timep, char *buf);
          both functions returns NULL on error

هم تابع asctime و هم تابع ctime به انتهای رشته‌ی خروجی خود کاراکتر \n اضافه می‌کنند. مشابه تابع asctime_r تابع ctime_r نیز باید متوجه باشد که بافری که برای دریافت خروجی تابع فراهم می‌کند باید حداقل ۲۶ کاراکتر طول داشته باشد.

مثال از تابع ctime

ctime در خروجی خودش timezone را لحاظ می‌کند ولی asctime با timezone کاری ندارد.

time_t = calendar time

تابع asctime در ساختن خروجی خود به صحت اطلاعات موجود در struct tm داده شده کاری ندارد! مثال

تابع gmtime به عنوان ورودی اشاره‌گری به نوع داده‌ی time_t می‌گیرد و اشاره‌گری به struct tm که در واقع همان broken down time است بر‌می‌گرداند. این تابع timezone را در محاسبات خود دخالت نمی‌دهد. تابع localtime دقیقا مانند بالا عمل می‌کند با این تفاوت که timezone را لحاظ می‌کند.

#include <time.h>

struct tm *gmtime(const time_t *timep);
struct tm *gmtime_r(const time_t *timep, struct tm *result);

strcut tm *localtime(const time_t *timep);
struct tm *localtime_r(const time_t *timep, struct tm *result);

                             all functions return NULL on error

توابع gmtime_r و localtime_r در صورت موفقیت آدرس struct tm واقع در result را برمی‌گردانند. بررسی این موضوع

در تمام سیستم‌های POSIX نوع داده‌ی time_t یک نوع داده‌ی arithmetic است. در لینوکس time_t یک نوع داده‌ی integer است. برای portable بودن و بررسی مساله‌ی overflow در تفریق بهتر است همیشه از تابع difftime استفاده کنیم. دقت کنید خروجی تابع difftime از نوع double است.

#include <time.h>

double difftime(time_t time1, time_t t0);
          returns (double)(time1 - time0)

مثال از تابع difftime

تغییرات بزرگ و ناگهانی و غیرمنتظره در wall clock می‌تواند بر عملکرد برنامه‌هایی که بر زمان مطلق (absolute) وابسته هستند مثل برنامه‌ی make تاثیر منفی و نامطلوب بگذارند. (توضیحات بیشتر صفحه‌ی ۳۲۱)

برای تنظیم wall clock به صورت تدریجی از تابع adjtime(3) استفاده می‌کنیم. با این تابع هیچ وقت زمان سیستم به عقب برنمی‌گردد. اگر زمان سیستم باید به جلو برود ساعت سیستم را سریع می‌کند و اگر زمان سیستم باید به عقب برگردد ساعت سیستم را کند می‌کند. این امکان مناسب سرویس‌هایی مانند Network Time Protocol (NTP) است.

#define _BSD_SOURCE
#include <sys/time.h>

int adjtime(const struct timeval *delta, struct timeval *olddelta);
                  returns 0 on success, -1 on error (and set errno)

اگر delta مساوی NULL نباشد، چنانچه adjtime قبلی‌ای ثبت شده باشد آنرا کنسل می‌کند. در این صورت اگر olddelta برابر NULL نباشد مقدار زمان باقی مانده از تصحیح زمان قبلی که هنوز اعمال نشده است در struct timeval داده شده برگردانده می‌شود.

اگر delta برابر NULL باشد و olddelta مقدار معتبری داشته باشد در این صورت می‌توانیم مقدار زمان باقی‌مانده از تصحیح زمان ثبت شده را بازیابی کنیم.

تصحیح زمان به وسیله‌ی تابع adjtime باید کوچک باشد. مناسب‌ترین مورد استفاده از آن NTP است که تصحیح‌های کوچکی در زمان انجام می‌دهد.

لینوکس از الگوریتم تصحیح زمان پیچیده‌تری استفاده می‌کند که توسط سیستم کال adjtimex(2) پیاده‌سازی شده است. توضیحات زیر علی‌رغم مفید بودن کامل نیست. برای اطلاعات بیشتر و دقیق‌تر به adjtimex(2) مراجعه کنید. تابع adjtime روی سیستم کال adjtimex پیاده‌سازی شده است.

#include <sys/timex.h>

struct timex {
    int modes;              /* mode selector */
    long offset;            /* time offset (usec) */
    long freq;              /* frequency offset (scaled ppm) */
    long maxerror;          /* maximum error (usec) */
    long esterror;          /* estimated error (usec) */
    int status;             /* clock status */
    long constant;          /* PLL time constant */
    long precision;         /* clock precision (usec) */
    long tolerance;         /* clock frquency tolerance (ppm) */
    struct timeval time;    /* current time */
    long tick;              /* usecs between clock ticks */
};

int adjtimex(struct timex *adj);
        returns the current clock state (below) on success, -1 on error
        (and set errno)

فیلد mode یک فیلد OR بیتی است که باید مقداری از OR کردن صفر یا چند فلگ زیر داشته باشد. فقط کاربری با توانایی CAP_SYS_TIME می‌تواند مدی غیر صفر انتخاب کند.

  1. ADJ_OFFSET: set the time offset via offset.
  2. ADJ_FRQUENCY: set the frequency offset via freq.
  3. ADJ_MAXERROR: set the maximum error via maxerror.
  4. ADJ_ESTERROR: set the estimated error via esterror.
  5. ADJ_STATUS: set the clock status via status.
  6. ADJ_TIMECONST: set the phase-locked loop (PLL) time constant via constant.
  7. ADJ_TICK: set the tick value via tick.
  8. ADJ_OFFSET_SINGLEHOST: set the time offset via offset once, with a simple algorithm, like adjtime().

اگر mode مساوی صفر باشد چیزی ست نمی‌شود بلکه تمام پارامترهای struct timex بازیابی می‌شود.

در صورت موفقیت، سیستم کال adjtimex مقدار clock state جاری را برمی‌گرداند که یکی از موارد زیر خواهد بود:

  1. TIME_OK: The clock is synchronized.
  2. TIME_INS: A leap second will be inserted.
  3. TIME_DEL: A leap second will be deleted.
  4. TIME_OOP: A leap second is in progress.
  5. TIME_WAIT: A leap second just occured.
  6. TIME_BAD or TIME_ERROR: The clock is not synchronized.

adjtimex مخصوص لینوکس است. برای portable بودن برنامه بهتر است از adjtime استفاده کنید.

#include <unistd.h>

unsigned int sleep(unsigned int seconds);
    returns 0 on success, a positive number on failure

تابع sleep تعداد ثانیه‌هایی که از خوابش باقی‌مانده است را برمی‌گرداند. لذا در یک فراخوانی موفق این تابع مقدار صفر را برمی‌گرداند و یک فراخوانی ناموفق مقداری بین (0-seconds] را برمی‌گرداند.

بسیاری از برنامه‌نویسان مقدار برگشتی sleep را چک نمی‌کنند. اگر مدت زمان خواب برنامه دقیقا برایتان مهم است مداوما روی مقدار برگشتی sleep دوباره sleep کنید تا این مقدار به صفر برسد. (مثال)

برای sleep کردن پروسس یا suspend کردن آن برای تعداد مشخصی میکروثانیه از تابع usleep استفاده می‌کنیم. متاسفانه prototype تابع در BSD و SUS با هم فرق دارد. در نسخه‌ی BSD تابع usleep خروجی ندارد ولی در SUS خروجی آن از نوع int است.

/* BSD version */
#include <unistd.h>

void usleep(unsigned long usec);

/* SUSv2 version */
#define _XOPEN_SOURCE 500
#include <unistd.h>

int usleep(useconds_t usec);
        returns 0 on success, -1 on error (and set errno)

مثال از تابع usleep (با فشردن دگمه‌ی Control-C حالت اینتراپت و عدم موفقیت تابع usleep را بررسی کنید.)

در لینوکس حداکثر عدد قابل ذخیره در useconds_t عددی قابل قبول است و لذا خطای EINVAL داده نمی‌شود.

According to the specification, the useconds_t type is an unsigned integer capable of holding values as high as 1,000,000.

علی رغم اینکه من در FreeBSD و NetBSD چک کردم و پروتوتایپ usleep دقیقا مثل لینوکس بود ولی کتاب می‌گوید برای portability بهتر شایسته است که از نوع داده useconds_t استفاده نکنیم . همیشه از unsinged int استفاده کنیم و بر مقدار خروجی usleep تکیه نکنیم. در این صورت باز هم بررسی خطا همچنان میسر است:
errno = 0;
usleep(1000);
if (errno)
    perror("usleep");

لینوکس تابع usleep را به نفع سیستم کال nanosleep(2) منسوخ می‌کند که دقتی در حد نانوثانیه و اینترفیسی هوشمندانه‌تر فراهم می‌کند.

#define _POSIX_C_SOURCE 199309
#include <time.h>

int nanosleep(const struct timespec *req, struct timespec *rem);
               returns 0 on success, -1 on error (and set errno)

اگر در هنگام nanosleep یک سیگنال interrupt بیاید، nanosleep قبل از زمان مقرر با مقدار -1 که نشانه‌ی بروز خطاست برمی‌گردد و errno مقدار EINTR می‌گیرد. اگر rem برابر NULL نباشد مقدار زمان باقیمانده از nanosleep در استراکت timespec فراهم شده قرارداده می‌شود.

مثال از سیستم کال nanosleep (همین مثال به صورت efficient تر ولی قدری ناخوانا در صفحه‌ی ۳۲۷)

سیستم کال nanosleep جزو POSIX است و نکته‌ی مهم این که با استفاده از سیگنال‌ها پیاده‌سازی نمی‌شود. به همین خاطر نسبت به sleep و usleep بسیار ارجحیت دارد.

تابع sleep و سیستم کال nanosleep مقدار زمان باقی مانده از خوابشان را برمی‌گردانند ولی تابع usleep فقط موفقیت یا عدم موفقیتش را برمی‌گرداند.

مهمترین و پیشرفته‌ترین اینترفیس در مورد sleep را خانواده‌ی POSIX clock فراهم کرده است:

#include <time.h>

int clock_nanosleep(clockid_t clock_id,
                    int flags,
                    const struct timespec *req,
                    struct timespec *rem);

    returns 0 on success, or a positive error number (and not(!) set errno)

متاسفانه سیستم کال clock_nanosleep در هنگام خطا errno را ست نمی‌کند بلکه آن را به عنوان مقدار سیستم کال برمی‌گرداند. (به مثال مراجعه کنید.)

انتخاب منبع زمان CLOCK_PROCESS_CPUTIME_ID به عنوان clock_id بی‌معنی است برای اینکه فراخوانی این سیستم کال باعث ساسپند شدن اجرای پروسس می‌شود و لذا زمان پروسس افزایش نمی‌یابد!

مقدار flags باید یا صفر باشد به معنای زمان relative و یا TIMER_ABSTIME به معنای زمان مطلق. (توضیحات بیشتر صفحه ۳۲۸) اگر flags برابر TIMER_ABSTIME باشد زمان قرارگرفته در req به عنوان زمان absoulte یا مطلق در نظر گرفته می‌شود.

همیشه یک race condition بالقوه در سناریوی «به دست آوردن زمان فعلی، محاسبه‌ی اختلاف بین زمان فعلی و زمان دلخواه، و واقعا sleep کردن» وجود دارد. این مشکل با فلگ TIMER_ABSTIME مرتفع می‌شود. این فلگ باعث می‌شود تا به صورت مستقیم زمان پایان sleep را مشخص کنیم. در این صورت کرنل اجرای برنامه را تا زمانی که time source داده شده به زمان دلخواه نرسیده است متوقف می‌کند.

مثالی تقریبا جامع از مباحث سیستم کال clock_nanosleep

یک روش portable در سیستم‌های قدیمی وقتی که usleep همه‌گیر نشده بود و nanosleep هنوز نوشته نشده بود استفاده از سیستم کال select(2) بود.

#include <sys/select.h>

int select(int n,
           fd_set *readfds,
           fd_set *writefds,
           fd_set *exceptfds,
           struct timeval *timeout);

شیوه‌ی کار به صورت زیر بود:

strcut timeval tv = { .tv_sec = 0, .tv_usec = 757 };

/* sleep for 757 micro seconds */
select(0, NULL, NULL, NULL, &tv);

تمامی اینترفیس‌های مطرح شده در اینجا تضمین می‌کنند که پروسس فراخوان حداقل به مدت داده شده به خواب خواهد رفت. اما ممکن است مثلا به خاطر الگوریتم زمانبندی CPU پروسس بیشتر از درخواست ما اجرا نشود. یعنی کرنل حتی پروسس را به موقع بیدار کند ولی scheduler پروسس دیگری را برای اجرا در آن لحظه انتخاب کند.

اما اینجا مبحث دیگری به نام ==timer ticks ==مطرح می‌شود. فرض کنید اگر تایمر سیستم هر ۱۰ میلی ثانیه یک تیک بزند در این صورت فقط قادر به اندازه‌گیری بازه‌های زمانی و پاسخ‌گویی به رویدادهای زمانی با دقت ۱۰ میلی ثانیه خواهد بود. اگر برنامه درخواست یک sleep در حد یک میلی ثانیه داشته باشد، با فرض اینکه در ابتدای نوبت CPU این درخواست را مطرح کند تا ۹ میلی ثانیه بعد هیچ تیک زمانی وجود ندارد تا کرنل کنترل را به دست بگیرد و پروسس را بیدار کند لذا در این صورت ۹ میلی ثانیه overrun وجود خواهد داشت.

The use of high-precision time sources, such as those provided by POSIX clocks, and higher values for HZ, minimize timer overrun.

Timer ها سازوکاری برای اطلاع دادن به پروسس مبنی بر این که مقدار مشخصی از زمان گذشته است فراهم می‌کنند. اینکه کرنل چگونه به پروسس اطلاع می‌دهد که زمان تایمر منقصی شده است بستگی به نوع تایمر دارد. لینوکس چند نوع تایمر را پشتیبانی می‌کند.

دو نمونه کاربرد مفهوم تایمر:

  1. رفرش کردن صفحه نمایش ۶۰ بار در ثانیه
  2. کنسل کردن یک تراکنش pend شده اگر بعد از ۵۰۰ میلی ثانیه همچنان pend است.

سیستم کال alarm برنامه‌ریزی می‌کند که بعد از گذشت تعداد ثانیه‌های درخواست شده از CLOCK_REALTIME یک سیگنال SIGALRM به پروسس فراخوان داده شود. اگر یک سیگنال pending برای آن پروسس وجود داشته باشد آن سیگنال SIGALRM کنسل می‌شود و سیگنال جدید ثبت می‌شود و تعداد ثانیه‌های باقی‌مانده تا SIGALRM قبلی بازگردانده می‌شود.

#include <unistd.h>

unsigned int alarm(unsigned int seconds);
        returns 0 or a positive number (description above)

مثال از سیستم کال ‌های alarm و pause

فراخوانی مجدد سیستم کال alarm و مشاهده‌ی خروجی

خانواده‌ی سیستم کال‌های interval timer کنترل و امکانات بیشتری نسبت به سیستم کال alarm در اختیار ما می‌گذارد. این inetrval timer ها به صورت دلخواه می‌توانند به طور اتوماتیک خودشان را rearm کنند.

#include <sys/time.h>

struct itimerval {
    struct timeval it_interval;     /* next value */
    struct timeval it_value;        /* current value */
};

int getitimer(int which, struct itimerval *value);

int setitimer(int which,
              const struct itimerval *value,
              struct itimerval *ovalue);

    both functions returns 0 on success, -1 on error (and set errno)

این سیستم کال‌های interval timer در یکی از سه مد زیر عمل می‌کنند:

  1. ITIMER_REAL: Measures real time. When the specified amount of real time has elapsed, the kernel sends the process a SIGALRM signal.
  2. ITIMER_VIRTUAL: Decrements only while the process user space code is exceuting. When the amount of process time has elapsed, the kernel sends the process a SIGVTALRM.
  3. ITIMER_PROF: Decrements both while the process is exceuting, and while the kernel is executing on behalf of the process(e.g., completing a system call). When the specified amount of time has elapsed, the kernel sends the process a SIGPROF signal. This mode is usually coupled with ITIMER_VIRTUAL, so that the program can measure user and kernel time spent by the process.

ITIMER_REAL مثل alarm کار می‌کند و دو مد بعدی برای پروفایلینگ مفید هستند.

سیستم کال setitimer یک تایمر از نوع type با زمان انقضایی که در فیلد it_value قرار گرفته است ست می‌کند. زمانی که تایمر منقضی شود کرنل دوباره تایمری این بار با زمان it_interval رجیستر می‌کند. بنابر این it_value زمان باقی‌مانده در تایمر فعلی است.

اگر تایمر منقضی شود و مقدار قرار گرفته در it_interval صفر باشد تایمر کلا خاتمه می‌یابد و زمانبندی مجدد نمی‌شود. به طور مشابه اگر فیلد it_value به صفر مقداردهی شود تایمر متوقف می‌شود و دوباره زمان‌بندی نمی‌شود.

If ovalue is not NULL, the previous value for the interval timer of type which is returned.

مثال از سیستم کال setitimer

بعضی از سیستم‌های یونیکس sleep و usleep را با SIGALRM پیاده‌سازی می‌کنند و به طور مشخص سیستم کال‌های alarm و setitimer نیز از SIGALRM استفاده می‌کنند. بنابر این برنامه‌نویسان باید دقت کافی داشته باشند تا فراخوانی این توابع و سیستم کال‌ها با هم همپوشانی نداشته باشند که در این صورت نتیجه مشخص نیست.

برای wait های مختصر باید از سیستم کال nanosleep استفاده کرد که POSIX دیکته می‌کند که از سیگنال‌ها استفاده نمی‌کند. برای تایمرها از alarm و setitimer استفاده کنید.

در لینوکس sleep(3) با nanosleep(2) پیاده‌سازی شده است. بررسی این موضوع

عجیب نیست که قوی‌ترین اینترفیس timer از خانواده‌ی POSIX clock بیاید. با تایمرهای خانواده‌ی POSIX clock فرآیند ایجاد، initialize، و حذف یک تایمر در سه سیستم کال مجزای timer_create(2) و timer_settime(2) و timer_delete(2) انجام می‌شود.

#include <signal.h>
#include <time.h>

struct sigevent {
    union sigval    sigev_value;
    int             sigev_signo;
    int             sigev_notify;
    void (*sigev_notify_function)(union sigval);
    pthread_attr_t  *sigev_notify_attributes;
};

union sigval {
    int     sival_int;
    void    *sival_ptr;
};

int timer_create(clockid_t clockid,
                 struct sigevent *evp,
                 timer_t *timerid);

        returns 0 on success, -1 on error (and set errno)

A successful call to timer_create(2) creates a new timer associated with the POSIX clock clockid, stores a unique timer identification in timerid, and returns 0. This call merely sets up the conditions for running the timer; nothing actually happens until the timer is armed, as shown in the following section.

The evp parameter, if non-NULL, defines the asynchronous notification that occurs when the timer expires.

تایمرهای خانواده‌ی POSIX clocks کنترل بیشتری در مورد اینکه کرنل چگونه پروسس را مطلع کند که تایمر منقضی شده است به ما می‌دهند. مثلا دقیقا مشخص می‌کنند که کرنل چه سیگنالی ساطع کند یا حتی اجازه می‌دهند تا کرنل یک thread را راه‌اندازی کند و در آن تابعی را که ما مشخص کرده‌ایم در پاسخ به منقضی شدن تایمر اجرا کند.

رفتار کرنل در زمان منقضی شدن تایمر توسط فیلد sigev_notify مشخص می‌شود که یکی از مقادیر زیر را می‌گیرد:

  1. SIGEV_NONE: A "null" notification. On timer expiration, nothing happens.
  2. SIGEV_SIGNAL: On timer expiration, the kernel sends the process the signal specified by sigev_signo. In the signal handler, si_value is set to sigev_value.
  3. SIGEV_THREAD: On timer expiration, the kernel spawns a new thread (within this process), and has it execute sigev_notify_function, passing sigev_value as its sole argument. The thread terminates when it returns from this function. If sigev_notify_attributes is not NULL, the provided pthread_attr_t structure defines the behaviour of the new thread.

اگر evp با NULL مقداردهی شود معادل تنظیمات زیر است:

sigev_notify = SIGEV_SIGNAL
sigev_signo = SIGALRM
sigev_value = timer's ID

تایمری که به وسیله‌ سیستم کال timer_create ایجاد شده است هنوز عملیاتی نیست. برای انتساب یک زمان انقضا و راه انداختن تایمر از سیستم کال timer_settime استفاده می‌کنیم. این که زمان انقضای کدام تایمر مشخص می‌شود و راه اندازی می‌گردد توسط فیلد timer_id تعیین می‌شود.

#include <time.h>

struct itimerspec {
    struct timepsec it_interval;    /* next value */
    struct timespec it_value;       /* current value */
};

int timer_settime(timer_t timer_id,
                  int flags,
                  const struct itimerspec *value,
                  struct itimerspec *ovalue);

        returns 0 on success, -1 on error (and set errno)

توضیح فیلدهای struct itimerspec مانند struct itimerval در سیستم کال setitimer است.

If it_interval is 0, the timer is not an interval timer, and will disarm once it_value expires.

آرگومان flag می‌تواند مانند سیستم کال clock_nanosleep مقدار TIMER_ABSTIME داشته باشد که در این صورت مقدار مشخص شده توسط آرگومان value زمان مطلق است. در حالت عادی زمان مشخص شده زمان نسبی یا relative است که نسبت به زمان فعلی سنجیده می‌شود. این فلگ باعث جلوگیری از تاثیرات race condition می‌شود.

اگر آرگومان ovalue مقدار NULL نداشته باشد زمان انقضای تایمر قبلی در استراکچر فراهم شده ذخیره می‌شود. اگر تایمر قبلی disarm شده باشد فلیدهای استراکچر داده شده با صفر پر می‌شوند.

با سیستم کال timer_gettime می‌توانیم بدون ریست کردن تایمر اطلاعات انقضای تایمر داده شده را به دست بیاوریم.

#include <time.h>

int timer_gettime(timer_t timerid, struct itimerspec *value);

           returns 0 on success, -1 on error (and sets errno)

POSIX یک اینترفیس برای مشخص کردن overrun احتمالی یک تایمر فراهم کرده است:

#include <time.h>

int timer_getoverrun(timer_t timerid);

    returns 0 or a positive number on success, or -1 on error (and sets errno)

On success, timer_getoverrun(2) returns the number of additional timer expirations that have occurred between the initial expiration of a timer and notification to the process for example, via a signal that the timer expired.

اگر تعداد overrun های بازگشتی از سیستم کال timer_getoverrun بزرگتر یا مساوی DELAYTIMER_MAX باشد، سیستم کال مقدار DELAYTIMER_MAX برمی‌گرداند.

حذف کردن یک تایمر با آی‌دی timerid با استفاده از سیستم کال timer_delete به صورت زیر صورت می‌گیرد. تنها خطایی که ممکن است رخ دهد این است که timerid صحیح نباشد. در این صورت errno مقدار EINVAL می‌گیرد.

#include <time.h>

int timer_delete(timer_t timerid);

    returns 0 on success, -1 on error (and sets errno)

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

نوشته شده در: 1405-03-11 (1 هفته 36 دقیقه 7 ثانیه پیش)

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

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

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