تصویر مقاله
عکس از Rachael Gorjestani در Unsplash

پردازه‌های Daemon

پردازه Daemon به پردازه‌ای گفته می‌شود که در پس‌زمینه (Background) اجرا شده و به هیچ ترمینال کاربری متصل نیست. نداشتن ترمینال متصل، از ویژگی‌های بارز این پردازه‌هاست، چرا که معمولاً توسط اسکریپت‌های راه‌اندازی اولیه سیستم آغاز به کار می‌کنند.

از آنجایی که پردازه‌های Daemon ترمینال کنترلی ندارند، هنگام وقوع هر رویدادی به روشی برای ثبت و نمایش پیام‌های خود نیاز خواهند داشت. این پیام‌ها ممکن است صرفاً جنبه اطلاع‌رسانی داشته باشند یا پیام‌های اضطراری باشند که نیازمند رسیدگی فوری مدیر سیستم هستند. تابع syslog استانداردترین روش برای ثبت این پیام‌ها به شمار می‌رود و آن‌ها را برای پردازش به پردازه Daemon به نام syslogd ارسال می‌کند.

پردازه Daemon به نام syslogd

دیمن syslogd پردازه‌ای است که وظیفه مدیریت لاگ‌ها را بر عهده دارد؛ این پردازه در زمان بوت سیستم شروع به کار کرده و تا زمان فعال بودن سیستم، پیوسته در حال اجرا می‌ماند. نحوه عملکرد syslogd در سیستم‌های Unix مشتق‌شده از برکلی (BSD) به شرح زیر است:

  1. ابتدا فایل پیکربندی (معمولاً /etc/syslog.conf) را می‌خواند. این فایل تعیین می‌کند که با توجه به نوع پیام لاگِ دریافتی، چه اقدامی باید صورت گیرد؛ برای مثال، ممکن است پیام را به یک فایل خاص اضافه کند، آن را برای یک کاربر واردشده (Login) به سیستم بفرستد یا حتی به پردازه syslogd در یک میزبان (Host) دیگر ارسال نماید.
  2. یک سوکت دامنه Unix ایجاد شده و به فایلی در مسیر /var/run/log (یا در برخی سیستم‌ها /dev/log) متصل (Bind) می‌شود.
  3. یک سوکت UDP ایجاد شده و به پورت ۵۱۴ متصل می‌شود (سرویس syslog).
  4. فایل دستگاه /dev/klog ایجاد می‌شود؛ تمامی پیام‌های خطای هسته (Kernel) به عنوان ورودی از طریق این دستگاه دریافت می‌شوند.

پردازه دیمن syslog یک حلقه بی‌نهایت اجرا کرده و با فراخوانی تابع select، منتظر می‌ماند تا یکی از سه توصیف‌گر فایل (File Descriptor) ذکر شده در مراحل قبل آماده خواندن شود. به محض خواندن پیام لاگ، اقدامات مشخص‌شده در فایل پیکربندی را انجام می‌دهد. همچنین در صورتی که این پردازه سیگنال SIGHUP را دریافت کند، فایل پیکربندی را مجدداً بازخوانی خواهد کرد.

ما می‌توانیم با ایجاد یک سوکت دیتاگرام دامنه Unix و ارسال پیام به مسیری که دیمن به آن متصل شده، پیام‌ها را از دیمنِ خود به پردازه syslogd بفرستیم. همچنین ایجاد یک سوکت UDP و ارسال پیام به آدرس Loopback و پورت ۵۱۴ نیز امکان‌پذیر است؛ با این حال، ساده‌ترین راهکار، فراخوانی تابع syslog است که در ادامه به آن می‌پردازیم.

تابع syslog

روش اصلی برای ثبت لاگ‌ها در یک پردازه Daemon، فراخوانی تابع syslog است. این تابع در ابتدا برای سیستم‌های BSD توسعه یافته بود، اما امروزه تقریباً در تمام سیستم‌های مشتق‌شده از Unix پشتیبانی می‌شود.

C

آرگومان priority در واقع ترکیبی از دو پارامتر level و facility است که مفهوم هر یک در ادامه توضیح داده شده است:

  • level
تصویر مقاله
سطح (level) پیام لاگ
پارامتر level در پیام‌های لاگ مقداری بین ۰ تا ۷ دارد. در صورتی که مقداری برای آن تعیین نشود، مقدار پیش‌فرض LOG_NOTICE در نظر گرفته خواهد شد.
  • facility
تصویر مقاله
منشأ (facility) پیام لاگ
پارامتر facility برای شناسایی نوع و منشأ پردازه‌ای که پیام را ارسال می‌کند به کار می‌رود. اگر مقداری برای آن مشخص نکنید، مقدار پیش‌فرض LOG_USER اعمال خواهد شد.

هنگامی که برنامه برای اولین بار تابع syslog() را فراخوانی می‌کند، یک سوکت دامنه Unix ایجاد کرده و با اجرای تابع connect() به سوکت ایجادشده توسط دیمن syslogd (مثلاً در مسیر /var/run/log) متصل می‌شود. این سوکت تا پایان کار برنامه باز خواهد ماند.

علاوه بر این، برنامه می‌تواند توابع openlog() و closelog() را به صورت صریح فراخوانی کند.

C

تابع openlog() پیش از فراخوانی syslog() استفاده می‌شود و آرگومان‌های آن به شرح زیر هستند:

  • آرگومان ident: رشته‌ای است که توسط syslog به ابتدای هر پیام لاگ اضافه می‌شود و معمولاً نام برنامه را نشان می‌دهد.
  • آرگومان options: ترکیبی از ثابت‌های مشخص‌شده در تنظیمات سیستم است.
  • آرگومان facility: مقدار پیش‌فرض این پارامتر را برای فراخوانی‌های بعدی تابع syslog تعیین می‌کند.
تصویر مقاله

تابع daemon_init()

این تابع برای تبدیل یک پردازه معمولی به یک پردازه Daemon مورد استفاده قرار می‌گیرد.

C

مراحل عملکرد این تابع به ترتیب زیر است:

C

با فراخوانی تابع fork()، پردازه والد (Parent) خاتمه یافته و پردازه فرزند (Child) شروع به کار می‌کند. فرزند شناسه گروه پردازه (Process Group ID) را به ارث می‌برد اما شناسه پردازه (PID) اختصاصی خود را دارد؛ بنابراین پردازه فرزند نمی‌تواند رهبر گروه پردازه (Process Group Leader) باشد.

C

سپس تابع POSIX به نام setsid() فراخوانی می‌شود تا یک نشست (Session) جدید ایجاد کند. با این کار، پردازه فرزند رهبر نشست و رهبر گروه پردازه جدید می‌شود.

C

سیگنال SIGHUP نادیده گرفته می‌شود. این اقدام برای فراخوانی دومین تابع fork() در مرحله بعد ضروری است.

CSS

با فراخوانی دومین fork()، پردازه فرزند جدیدی ایجاد شده و فرزند اول خاتمه می‌یابد. هدف این است که تضمین شود دیمن، حتی در صورت باز کردن یک دستگاه ترمینال (مانند /dev/tty یا /dev/console)، هرگز نمی‌تواند یک ترمینال کنترلی به دست آورد؛ زیرا اگر رهبر نشستی که ترمینال کنترلی ندارد دستگاه ترمینالی را باز کند، آن ترمینال به طور خودکار ترمینال کنترلی آن خواهد شد.

از آنجا که فرزند دوم رهبر نشست نیست، امکان تصاحب ترمینال کنترلی را نخواهد داشت. با این حال، هنگام خاتمه فرزند اول، سیگنال SIGHUP به فرزند دوم ارسال می‌شود و به همین دلیل، نادیده گرفتن این سیگنال در مرحله قبل الزامی بود.

C

مقدار ۱ به متغیر سراسری daemon_proc اختصاص می‌یابد. این متغیر در توابع err_XXX() این کتاب تعریف شده و اگر مقداری غیر از صفر داشته باشد، پیام‌های خطا به جای fprintf() از طریق تابع syslog() ثبت می‌شوند.

C

دایرکتوری کاری تغییر می‌کند (معمولاً به دایرکتوری ریشه). البته برخی دیمن‌ها مانند دیمن پرینتر ممکن است به مسیر دیگری نیاز داشته باشند. همچنین دایرکتوری اولیه دیمن را در حین اجرای آن نمی‌توان Unmount کرد، بنابراین تغییر مسیر از قبل اهمیت دارد.

C

تمامی توصیف‌گرهای باز و به ارث رسیده بسته می‌شوند. چالش کار در این است که حداکثر تعداد توصیف‌گرهای در حال استفاده مشخص نیست و تابع استاندارد Unix برای به دست آوردن آن وجود ندارد؛ به همین دلیل در این کتاب، ۶۴ توصیف‌گر اول بسته می‌شوند.

C

سپس ورودی استاندارد، خروجی استاندارد و خطای استاندارد به مسیر /dev/null هدایت (Redirect) می‌شوند. این کار تضمین می‌کند که توصیف‌گرهای پایه باز هستند و از خطاهای غیرمنتظره در کتابخانه‌های وابسته جلوگیری می‌کند.

علاوه بر این، اگر توصیف‌گرهای پایه بسته باشند و سوکت جدیدی برای کلاینت باز شود، ممکن است شماره توصیف‌گر ۱ یا ۲ را بگیرد؛ در این حالت، فراخوانی توابعی مانند perror() ممکن است ناخواسته داده‌هایی را به سمت کلاینت ارسال کند که خطرساز خواهد بود.

C

در نهایت، تنظیمات لازم برای استفاده از syslogd انجام می‌شود و با فراخوانی تابع openlog()، نام برنامه و مقدار پیش‌فرض facility تعیین می‌گردد.

مثال کاربردی از تابع daemon_init(): سرور Daytime

کد زیر، نسخه ارتقایافته سرور Daytime از فصل ۱۱ است که با فراخوانی تابع daemon_init() به عنوان یک پردازه Daemon اجرا می‌شود. تغییرات اعمال‌شده شامل دو مورد زیر است:

  • تابع daemon_init() درست در ابتدای برنامه فراخوانی می‌شود.
  • برای نمایش آدرس IP و پورت کلاینت، به جای printf() از تابع err_msg() استفاده می‌شود.
C

پیش از اجرای daemon_init()، مقدار argc بررسی می‌شود تا راهنمای استفاده صحیح به کاربر نمایش داده شود. این بررسی زودهنگام باعث می‌شود کاربر در صورت وارد کردن آرگومان‌های اشتباه فوراً مطلع شود؛ چرا که پس از اجرای daemon_init()، پیام‌ها به syslogd منتقل شده و کاربر متوجه خطای لحظه‌ای نخواهد شد.

پردازه inetd

در یک سیستم متداول Unix، معمولاً سرویس‌های متعددی مانند FTP ،Telnet ،Rlogin و TFTP منتظر اتصال کلاینت‌ها هستند. این پردازه‌ها غالباً در زمان بوت سیستم اجرا شده و الگوی رفتاری بسیار مشابهی دارند.

  • ابتدا یک سوکت ایجاد کرده،
  • پورت مشخص و شناخته‌شده‌ی سرور را به سوکت متصل (Bind) نموده،
  • در صورت استفاده از TCP، منتظر اتصال کلاینت و در صورت استفاده از UDP، منتظر دریافت دیتاگرام مانده و سپس
  • تابع fork() را فراخوانی می‌کند.

با این حال، چنین پیاده‌سازی و ساختاری با دو چالش اساسی همراه است:

  1. نخست اینکه تمامی این پردازه‌های Daemon، کدهای راه‌اندازی تقریباً یکسانی دارند.
  2. دوم اینکه هر پردازه‌ی Daemon یک پردازه مجزا ایجاد می‌کند، اما بیشتر این پردازه‌ها بخش عمده‌ای از زمان را در حالت انتظار و خواب (Sleep) به سر می‌برند.

برای برطرف کردن این چالش‌ها، در نگارش 4.3 BSD پردازه‌ای موسوم به «ابر سرور» (Super-server) به نام inetd معرفی شد. پردازه‌ی inetd این دو مشکل را به شیوه زیر حل می‌کند:

  1. از آنجا که سرویس inetd مدیریت جزئیات مراحل راه‌اندازی را بر عهده می‌گیرد، توسعه و نوشتن پردازه‌های Daemon بسیار ساده‌تر می‌شود؛ در نتیجه، دیگر نیازی نیست که هر سرور به طور مجزا تابع daemon_init را فراخوانی کند.
  2. به جای اینکه هر سرویس به طور جداگانه منتظر درخواست‌های کلاینت بماند، پردازه‌ی inetd به عنوان نماینده منتظر تمام درخواست‌ها می‌نشیند. این کار باعث کاهش چشمگیر تعداد کل پردازه‌های فعال در سیستم می‌شود.

پردازه‌ی inetd ابتدا با استفاده از همان روش‌هایی که در تابع daemon_init() توضیح داده شد، خود را به عنوان یک پردازه‌ی Daemon اجرا می‌کند. سپس فایل پیکربندی (معمولاً /etc/inetd.conf) را می‌خواند و پردازش می‌کند؛ فایلی که مشخص می‌سازد چه سرویس‌هایی باید مدیریت شوند و در صورت رسیدن درخواست برای هر سرویس، چه واکنشی باید نشان داده شود. هر خط از این فایل شامل فیلدهایی است که در ادامه آمده است:

تصویر مقاله
فیلدهای فایل inetd.conf

اگر به محتوای فایل inetd.conf نگاهی بیندازیم، با ساختاری به شکل زیر روبرو می‌شویم:

Terminal / Bash

در اینجا، ویژگی مهمی که باید به آن دقت کرد، فلگ wait-flag است. این مشخصه تعیین می‌کند که آیا inetd باید پیش از پذیرش اتصالات جدید برای یک سرویس، منتظر اتمام کار پردازه‌ی فرزند بماند یا خیر. در سرورهای معمولی TCP، این مقدار روی nowait تنظیم می‌شود؛ بدین معنی که سیستم بدون معطل ماندن برای پایان کار پردازه‌ی فرزند (که در حال پاسخ به درخواست قبلی است)، اتصالات جدید را می‌پذیرد و فرزندان جدیدی ایجاد می‌کند.

مراحل عملکرد inetd

تصویر زیر چگونگی عملکرد پردازه‌ی inetd را نشان می‌دهد. بیایید مراحل آن را گام‌به‌گام بررسی کنیم.

تصویر مقاله
عملکرد مرحله‌به‌مرحله‌ی inetd
  1. در زمان راه‌اندازی اولیه، این پردازه فایل /etc/inetd.conf را می‌خواند و برای تمام سرویس‌های مشخص‌شده در آن، سوکت‌هایی از نوع مناسب ایجاد می‌کند. حداکثر تعداد سرویس‌هایی که inetd می‌تواند مدیریت کند، به حداکثر تعداد دیسکریپتورهایی بستگی دارد که این پردازه قادر به ایجاد آن‌هاست. سپس هر سوکت جدیدی که ایجاد می‌شود، به مجموعه‌ی دیسکریپتورهای تابع select افزوده خواهد شد.
  2. در ادامه، تابع bind() فراخوانی می‌شود تا پورت سرور و آدرس IP عمومی (Wildcard) را مشخص کند. شماره پورت TCP/UDP مورد نظر نیز با ارسال نام سرویس و فیلد پروتکل از فایل پیکربندی به عنوان ورودی به تابع getservbyname() به دست می‌آید.
  3. در مورد سوکت‌های TCP، تابع listen() فراخوانی می‌شود تا امکان پذیرش درخواست‌های اتصال ورودی فراهم گردد؛ البته برای سوکت‌های UDP نیازی به طی کردن این مرحله نیست.
  4. پس از ساخته شدن تمامی سوکت‌ها، تابع select() صدا زده می‌شود تا منتظر آماده شدن یکی از سوکت‌ها برای خواندن بماند. همان‌طور که در فصل ۶ اشاره شد، سوکت TCP زمانی قابل خواندن می‌شود که آماده‌ی پذیرش اتصال جدید باشد، و سوکت UDP نیز با رسیدن یک دیتاگرام آماده‌ی خواندن می‌گردد. پردازه‌ی inetd بیشتر زمان خود را در این تابع select() می‌گذراند و منتظر آماده شدن یکی از سوکت‌ها می‌ماند.
  5. هنگامی که تابع select() سوکت آماده به خواندن را برمی‌گرداند، اگر آن سوکت از نوع TCP بوده و فلگ nowait برای آن تنظیم شده باشد، با فراخوانی accept() اتصال جدید پذیرفته می‌شود.
  6. سپس پردازه‌ی inetd با فراخوانی تابع fork()، یک پردازه‌ی جدید برای پاسخ‌گویی به درخواست سرویس ایجاد می‌کند که دقیقاً مشابه روند همزمانی (Concurrency) در سرورهای معمول است.
  7. اگر سوکت از نوع UDP باشد، پردازه‌ی والد باید ارتباط سوکت متصل‌شده را قطع کند (همانند آنچه در سرورهای همزمان معمول اتفاق می‌افتد). با فراخوانی مجدد تابع select() توسط والد، سیستم دوباره تا زمان آماده شدن سوکت‌ها برای خواندن به انتظار می‌نشیند.

پردازه‌ی فرزندی که توسط inetd ایجاد شده است، درخواست کاربر را از سوکت خوانده و آن را پردازش می‌کند. بیایید مراحل کار این پردازه‌ی فرزند را دقیق‌تر مرور کنیم.

  1. نخست تمامی دیسکریپتورها به جز دیسکریپتور سوکتی که قرار است پردازش شود، بسته می‌شوند.
  2. پردازه‌ی فرزند سه بار تابع dup2 را فراخوانی می‌کند تا سوکت را روی دیسکریپتورهای 0، 1 و 2 کپی کند؛ سپس سوکت اصلی بسته می‌شود. بدین ترتیب، تنها دیسکریپتورهای باز در پردازه‌ی فرزند، دیسکریپتورهای 0، 1 و 2 خواهند بود.
  3. پردازه‌ی فرزند با فراخوانی تابع getpwnam()، اطلاعات رمز عبور مربوط به نام کاربری مشخص‌شده در فایل پیکربندی را به دست می‌آورد. اگر نام کاربری تعیین‌شده چیزی غیر از root باشد، پردازه‌ی فرزند توابع setgid() و setuid() را اجرا می‌کند تا تحت هویت کاربر مشخص‌شده به فعالیت ادامه دهد.
  4. از آنجا که پردازه‌ی inetd با شناسه کاربری 0 (User ID 0 یا همان root) اجرا می‌شود، پردازه‌ی فرزندی که با تابع fork() ایجاد شده، این امکان را دارد تا هویت هر کاربر دلخواهی را به خود بگیرد.
  5. در نهایت، پردازه‌ی فرزند با فراخوانی تابع exec() برنامه سرور مورد نظر را برای رسیدگی به درخواست اجرا کرده و همزمان، آرگومان‌های تعریف‌شده در فایل پیکربندی را نیز به آن منتقل می‌کند.

نمونه‌ای از عملکرد inetd: سرویس FTP

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

تصویر مقاله
هنگام رسیدن درخواست اتصال به پورت ۲۱ پروتکل TCP

درخواست اتصال مستقیماً به پورت ۲۱ ارسال می‌شود، اما با فراخوانی تابع accept()، یک سوکت متصل‌شده‌ی جدید ایجاد می‌گردد. تصویر زیر وضعیتی را نشان می‌دهد که پس از فراخوانی تابع fork()، تمامی دیسکریپتورها به جز سوکت متصل‌شده بسته شده‌اند.

تصویر مقاله
پس از نخستین فراخوانی ()Fork

اکنون پردازه‌ی فرزند تابع exec() را فراخوانی می‌کند. همان‌طور که در بخش ۴.۷ بیان شد، تمامی دیسکریپتورهایی که پیش از فراخوانی exec() باز بوده‌اند، پس از اجرای آن نیز باز باقی می‌مانند. بنابراین پردازه‌ی اصلی سرور که توسط exec() فراخوانی شده است، می‌تواند از هر یک از دیسکریپتورهای 0، 1 و 2 برای ارتباط با کلاینت استفاده کند و این موارد باید تنها دیسکریپتورهای باز در پردازه‌ی سرور باشند.

تصویر مقاله
پس از فراخوانی تابع ()dup2

سناریویی که بررسی کردیم مربوط به زمانی بود که سرویس در فایل پیکربندی به صورت nowait تعریف شده باشد. اما اگر برای یک سرویس UDP فلگ wait تنظیم شود، بدین معناست که پردازه‌ی فرزند باید پیش از فراخوانی مجدد select() روی سوکت توسط inetd، کار خود را به پایان برساند. در نتیجه، تغییرات زیر رخ می‌دهد:

  • پس از بازگشت تابع fork()، پردازه‌ی والد شناسه‌ی پردازه‌ی فرزند (Process ID) را ذخیره می‌کند. این موضوع به والد امکان می‌دهد تا با بررسی مقدار بازگشتی تابع waitpid()، از پایان کار فرزند مربوطه اطمینان حاصل کند.
  • پردازه‌ی والد با استفاده از ماکروی FD_CLR، بیت مربوط به آن سوکت را در مجموعه‌ی دیسکریپتورها غیرفعال می‌کند؛ به این معنی که سوکت تا زمان اتمام کار پردازه‌ی فرزند در حالت اشغال باقی می‌ماند.
  • با به پایان رسیدن کار پردازه‌ی فرزند، سیگنال SIGCHLD به والد ارسال می‌شود و Handler سیگنال، شناسه‌ی فرزند خاتمه‌یافته را دریافت می‌کند. سپس با فعال‌سازی مجدد بیت مربوط به سوکت در مجموعه‌ی دیسکریپتورها، آن سوکت برای تابع select() مجدداً در دسترس قرار می‌گیرد.

علت اشغال ماندن سوکت تا پایان کار سرور دیتاگرام این است که از انتظار کشیدن inetd در تابع select() برای خواندن از سوکت (به امید دریافت دیتاگرام کلاینت دیگر) جلوگیری شود. بر خلاف سرورهای TCP که یک سوکت برای اتصال و به ازای هر کلاینت یک سوکت مجزا برای تبادل داده دارند، سرور UDP تنها از یک سوکت واحد بهره می‌برد.

اگر پردازه‌ی inetd سوکت UDP را از دامنه‌ی پایش select() خارج نکند و خودِ inetd زودتر از پردازه‌ی فرزند اجرا شود، دیتاگرامِ ورودی کلاینت جدید همچنان در بافر دریافت سوکت باقی خواهد ماند؛ در نتیجه inetd از حالت انتظار در select() خارج شده و یک پردازه‌ی فرزند اضافی (و غیرضروری) دیگر ایجاد می‌کند. از این رو، inetd باید سوکت دیتاگرام را تا زمانی که مطمئن شود فرزند دیتاگرام را از صف دریافت خوانده است، نادیده بگیرد. دریافت سیگنال SIGCHLD دقیقاً به همین منظور است تا نشان دهد کلاینت کارش با سوکت تمام شده است؛ موضوعی که در بخش ۲۲.۷ به طور مفصل به آن خواهیم پرداخت.

پردازه‌ی inetd معمولاً برای سرورهای با بار ترافیکی سنگین، به‌ویژه سرورهای ایمیل و وب به کار گرفته نمی‌شود. به عنوان مثال، سرور sendmail اغلب به عنوان یک سرور همزمان استاندارد اجرا می‌شود؛ چرا که در این حالت، هزینه‌ی مدیریت هر اتصال کلاینت تنها به یک فراخوانی fork() محدود می‌شود، در حالی که inetd ملزم به فراخوانی هر دو تابع fork() و exec() است. وب‌سرورها نیز برای به حداقل رساندن سربار ناشی از پردازه‌ها تکنیک‌های متعددی را به کار می‌بندند که در فصل ۳۰ به شرح آن‌ها می‌پردازیم.

تابع daemon_inetd

در ادامه، کد تابعی با نام daemon_inetd() را مشاهده می‌کنید که توسط سرورهای فراخوانی‌شده از طریق inetd قابل استفاده است.

C

از آنجا که تمام مراحل تبدیل شدن به Daemon از پیش توسط inetd انجام شده است، این تابع بسیار سرراست‌تر و ساده‌تر از تابع daemon_init() عمل می‌کند. تمام کاری که این تابع انجام می‌دهد، تغییر مقدار daemon_proc به ۱ و فراخوانی تابع openlog است.

بیایید با استفاده از این تابع، سرور Daytime را که قبلاً معرفی کردیم، به عنوان یک پردازه‌ی Daemon اجرا کنیم. با جایگزین کردن تابع daemon_init() با daemon_inetd()، پیاده‌سازی به صورت زیر درمی‌آید:

C

در این برنامه دو تغییر بنیادین و مهم دیده می‌شود:

  1. نخست اینکه تمامی کدهای مربوط به ساخت سوکت حذف شده‌اند؛ یعنی دیگر نیازی به فراخوانی توابع tcp_listen() و accept() نیست.

۲. از آنجا که این تابع به ازای هر اتصال کلاینت فقط یک‌بار فراخوانی می‌شود، حلقهٔ بی‌نهایت از بین رفته است.

خلاصه

فرآیند دیمن (Daemon Process) فرآیندی است که مستقل از کنترل هرگونه ترمینال و در پس‌زمینه اجرا می‌شود. بسیاری از سرورهای شبکه به‌صورت دیمن کار می‌کنند. معمولاً تمام خروجی‌های یک فرآیند دیمن توسط تابع syslog به فرآیند دیمن syslogd منتقل می‌شوند.

برای اجرای هر برنامه‌ای در قالب یک فرآیند دیمن، انجام چند مرحله ضروری است:

  1. فراخوانی fork() به منظور اجرا در پس‌زمینه،
  2. فراخوانی setsid() برای تبدیل شدن به لیدر یک سشن جدید POSIX،
  3. فراخوانی مجدد fork() جهت جلوگیری از اختصاص ترمینال کنترلی جدید،
  4. و در نهایت، تغییر دایرکتوری کاری، تنظیم ماسک ایجاد مود فایل (File Mode Creation Mask) و بستن فایل‌های غیرضروری.

تابع daemon_init() تمامی این مراحل را مدیریت و اجرا می‌کند.

تمرین‌ها

۱۳.۱ در مثال مربوط به تابع daemon_init()، اگر فراخوانی daemon_init() را به قبل از بررسی آرگومان‌های خط فرمان منتقل کنیم به‌طوری‌که err_quit() پس از آن قرار گیرد، چه اتفاقی رخ می‌دهد؟

پیام خطای مربوط به آرگومان‌های نامعتبر توسط syslog ثبت (Log) خواهد شد.

۱۳.۲ سرویس‌های TCP/IP موجود در جدول زیر، نمونه‌هایی از سرویس‌های رایجی هستند که توسط inetd مدیریت می‌شوند. با در نظر گرفتن نسخه‌های TCP و UDP هر سرویس، کدام‌یک از آن‌ها با استفاده از fork() پیاده‌سازی شده‌اند و کدام سرورها نیازی به fork() ندارند؟
تصویر مقاله
سرویس‌های استاندارد TCP/IP
  • نسخه‌های TCP سرویس‌های echo، discard و chargen پس از اجرای fork() در فرآیند فرزند اجرا می‌شوند؛ چراکه این سه سرویس تا زمان قطع اتصال توسط کلاینت به فعالیت خود ادامه می‌دهند.
  • سرورهای باقی‌مانده یعنی time و daytime نیازی به fork() ندارند؛ چراکه پیاده‌سازی ساده‌ای دارند و مستقیماً توسط inetd پردازش می‌شوند.
  • تمامی سرویس‌های UDP بدون نیاز به fork() پردازش می‌شوند، زیرا در پاسخ به هر دیتاگرام کلاینت، حداکثر یک دیتاگرام برمی‌گردانند.
۱۳.۳ اگر یک سوکت UDP ایجاد کنیم، آن را به پورت ۷ (سرور استاندارد echo) متصل (Bind) کرده و یک دیتاگرام UDP به سرور chargen ارسال نماییم، چه رخ خواهد داد؟
  • این سناریو یک روش برای حملهٔ DoS است. نخستین دیتاگرام ارسال‌شده از پورت ۷ باعث می‌شود سرویس chargen دیتاگرامی را مجدداً به پورت ۷ ارسال کند. این فرآیند دوباره بازتاب یافته و دیتاگرام دیگری را به سرویس chargen می‌فرستد (و این چرخه به طور مداوم تکرار می‌شود).
  • برای جلوگیری از بروز این مشکل، در FreeBSD اگر پورت مبدأِ دیتای دریافتی متعلق به یک سرویس داخلی باشد، از ارسال دیتاگرام به سرویس‌های داخلی ممانعت به عمل می‌آید.
  • روش دیگر این است که چنین سرویس‌های داخلی را در inetd هر هاست یا در روترهای اینترنت درون‌سازمانی غیرفعال کنیم.
۱۳.۴ در صفحهٔ راهنما (man page) مربوط به inet در سولاریس 2.x، فلگ -t توضیح داده شده است که آدرس IP و پورت کلاینت را برای تمامی سرویس‌های TCP تحت مدیریت inetd ثبت می‌کند. inetd این اطلاعات را چگونه به دست می‌آورد؟ همچنین در این صفحه ذکر شده که چنین امکانی برای سرویس‌های UDP وجود ندارد؛ دلیل آن چیست؟ آیا راهکاری برای برطرف کردن این محدودیت در سرویس‌های UDP وجود دارد؟
  • آدرس IP و پورت کلاینت را می‌توان از ساختار آدرس سوکت که توسط تابع accept() برگردانده می‌شود، استخراج کرد.
  • اما در مورد UDP، از آنجا که تابع recvfrom() برای خواندن دیتاگرام توسط فرآیند اصلی سرور (که با استفاده از تابع fork() ایجاد می‌شود) اجرا می‌گردد، inetd از این اطلاعات بی‌خبر می‌ماند.
  • در عوض، فرآیند inetd می‌تواند با مشخص کردن فلگ MSG_PEEK، دیتاگرام را بخواند تا صرفاً آدرس IP و پورت کلاینت را به دست آورد و دیتاگرام را برای پردازش نهایی در اختیار فرآیند سرور باقی بگذارد.

منابع و مراجع

Python
Terminal / Bash