تفاوت Type در systemd: simple، forking و notify
سرویس شما فعال است اما دیمون متوقف شده؟ دلیل این مشکل انتخاب اشتباه Type در فایل unit است. با تنظیم درست simple، forking یا notify، پردازش اصلی را دقیق ردیابی کنید.
چرا systemd یک واحد را فعال گزارش میکند در حالی که پردازش متوقف شده است
یک واحد سرویس در systemd تا زمانی که پردازشی که systemd آن را به عنوان پردازش اصلی میشناسد زنده باشد، در وضعیت active باقی میماند و بخش [Service] در فایل Type= تعیین میکند که آن پردازش کدام است. اگر مقدار اشتباهی را انتخاب کنید، systemd در نهایت به جای دیمونی که مد نظر شماست، یک shell wrapper یا یک پردازش والد با عمر کوتاه را زیر نظر میگیرد که در همان واحد اجرا شده و متوقف میشود. این واحد در واقع دربارهٔ پردازشی که به آن دستور داده شده زیر نظر بگیرد، حقیقت را به شما میگوید.
تغییر سیاست راهاندازی مجدد در اینجا کمکی نمیکند. Restart= زمانی عمل میکند که پردازش اصلی خارج شود، بنابراین Restart=always هرگز تا زمانی که PID (شناسه پردازش) اصلی متعلق به چیزی است که هنوز در حال اجراست، فعال نمیشود. ابتدا Type= را اصلاح کنید. کاری که systemd پس از خروج واقعی پردازش اصلی انجام میدهد، یک تصمیم جداگانه است که در راهنمای Restart= و RestartSec= پوشش داده شده است.
پارامتر Type= دقیقاً چه چیزی را تعیین میکند
هر مقدار Type= به دو پرسش بهطور همزمان پاسخ میدهد. چه زمانی systemd این واحد را «شروعشده» تلقی میکند و کدام پردازش، پردازش اصلی (main) است.
پاسخ نخست، ترتیب اجرا (ordering) را کنترل میکند. واحدی که نام واحد شما را در After= ذکر کرده است، تا زمانی که systemd واحد شما را «شروعشده» اعلام نکند، منتظر میماند. یک Type= که وضعیت «شروعشده» را خیلی زود گزارش کند، باعث میشود واحدهای وابسته پیش از آنکه سرویس شما آمادهٔ پاسخگویی باشد، اجرا شوند.
پاسخ دوم، نظارت (supervision) را کنترل میکند. systemd هر پردازشی را که یک واحد ایجاد میکند، در یک cgroup (گروه کنترلی) قرار میدهد؛ قابلیتی در هسته که پردازشها را دستهبندی میکند تا بتوان آنها را محدود کرد یا بهصورت گروهی خاتمه داد. cgroup همان ابزاری است که systemctl stop برای پاکسازی استفاده میکند: KillMode= بهصورت پیشفرض روی control-group تنظیم شده است، بنابراین متوقف کردن یک واحد، به تمام پردازشهای درون آن سیگنال ارسال میکند. PID اصلی محدودتر است. این همان تکپردازشی است که خروجش به معنای پایان واحد است و وضعیت خروج آن، نتیجهٔ نهایی واحد را تعیین میکند. سردرگمی زمانی آغاز میشود که cgroup را به جای PID اصلی در نظر بگیرید.
نوع simple پیش از اجرای فایل باینری شروع میشود
Type=simple مقدار پیشفرض است، زمانی که ExecStart= تنظیم شده باشد و هیچکدام از Type= یا BusName= موجود نباشند. در این حالت systemd پردازش را ایجاد میکند، بلافاصله واحد (unit) را شروعشده تلقی کرده و آن پردازش را به عنوان PID اصلی در نظر میگیرد. واحدهای بعدی بلافاصله شروع میشوند، حتی پیش از آنکه فایل باینری سرویس اجرا شده باشد.
این جزئیات آخر، یک غافلگیری رایج را توضیح میدهد. وجود غلط تایپی در مسیر ExecStart= همچنان منجر به یک job شروع موفق میشود و خطا لحظاتی بعد، زمانی که اجرای برنامه با شکست مواجه میشود، بروز میکند. systemd این مورد را با کد خروج 203 ثبت میکند که در جدول داخلی خود با نام EXEC شناخته شده و به معنای عدم توانایی در اجرای فایل باینری سرویس است. بنابراین، موفقیتآمیز بودن systemctl start به معنای وجود داشتن فایل باینری شما نیست.
برای برنامهای که در پیشزمینه (foreground) باقی میماند و هرگز خود را به پسزمینه منتقل نمیکند، از simple استفاده کنید. این تنظیم اکثر دیمونهای مدرن و تقریباً هر برنامهای که خودتان مینویسید را پوشش میدهد.
نوع Type=exec منتظر میماند تا برنامه واقعاً اجرا شود
Type=exec همان simple است با یک گام اضافه. systemd تنها زمانی واحد را اجراشده در نظر میگیرد که هم fork و هم اجرای فایل باینری با موفقیت انجام شده باشند. نبود فایل باینری یا User= که قابل حل نباشد، اکنون باعث شکست خودِ عملیات شروع (start job) میشود؛ بهجای اینکه وضعیت موفقیت گزارش شود و لحظاتی بعد سرویس بیسروصدا از کار بیفتد.
Type=exec در نسخه 240 از systemd معرفی شد، بنابراین تمام توزیعهای سروری فعلی آن را دارند. تا اوت 2026، توزیع Ubuntu 24.04 با systemd 255 و Debian 13 با systemd 257 عرضه میشوند. نسخه خود را با systemctl --version بررسی کنید.
هزینه این کار، یک گام همگامسازی اضافه در هنگام شروع است. دستاورد آن، دریافت وضعیت خروجی دقیق از systemctl start است. برای برنامههایی که در پیشزمینه (foreground) اجرا میشوند، exec را به simple ترجیح دهید.
نوع Type=forking و نحوه گم شدن PID اصلی
Type=forking به systemd میگوید که فرآیند موجود در ExecStart= یک فرزند ایجاد (fork) کرده و سپس عمداً خاتمه مییابد. systemd منتظر میماند تا آن فرآیند اولیه خارج شود و تنها پس از آن، واحد (unit) را در وضعیت started قرار میدهد. فرزندی که باقی میماند، همان daemon است.
مشکل اصلی، تشخیص هویت است. فرآیندی که systemd راهاندازی کرده بود دیگر وجود ندارد، بنابراین systemd باید تشخیص دهد کدامیک از فرآیندهای باقیمانده، فرآیند اصلی است. مقدار PIDFile= را روی فایلی تنظیم کنید که daemon در آن مینویسد؛ این مسیر معمولاً در زیرشاخه /run قرار دارد و systemd، مقدار PID را از آن میخواند. همچنین systemd بررسی میکند که آیا PID موجود در آن فایل به فرآیندی تعلق دارد که قبلاً بخشی از این سرویس بوده است یا خیر؛ بنابراین، یک فایل قدیمی که به فرآیندی نامرتبط اشاره میکند، رد میشود و به آن اعتماد نمیشود.
بدون PIDFile=، گزینه GuessMainPID= اعمال میشود که مقدار پیشفرض آن yes است. این حدس زدن تنها زمانی قابلاطمینان است که سرویس به یک فرآیند واحد محدود شود. مستندات بهصراحت این محدودیت را بیان میکنند: اگر daemon شامل بیش از یک فرآیند باشد، حدس systemd ممکن است اشتباه باشد و تشخیص خطا از کار میافتد. یک واحد همچنین ممکن است در نهایت با PID اصلی 0 مواجه شود، که به این معنی است که systemd هیچ فرآیندی برای نظارت ندارد.
بیشتر daemonهایی که از قابلیت fork استفاده میکنند، سوئیچی دارند که آنها را در foreground نگه میدارد. از آن سوئیچ در Type=exec استفاده کنید و خط PIDFile= را حذف نمایید. قطعات متحرک کمتر، به معنای روشهای کمتر برای گم شدن PID است.
نوع Type=oneshot برای کارهایی که به پایان میرسند
Type=oneshot انتظار دارد که پردازش اجرا شده و سپس خارج شود. systemd واحد را تنها پس از خروج پردازش، «شروعشده» تلقی میکند؛ این ویژگی باعث میشود oneshot ساختار مناسبی برای هر کاری باشد که واحد دیگری باید منتظر اتمام آن بماند. همچنین، زمانی که واحد هیچکدام از مقادیر Type= یا ExecStart= را مشخص نکند، این حالت به صورت پیشفرض در نظر گرفته میشود.
دو رفتار خاص برای oneshot وجود دارد. این تنها نوعی است که بیش از یک خط ExecStart= را میپذیرد و آن خطوط به ترتیب اجرا میشوند. همچنین، زمان انتظار برای شروع (start timeout) در این حالت بهطور پیشفرض غیرفعال است؛ بنابراین اگر یک oneshot متوقف شود (hang)، تا ابد منتظر میماند مگر اینکه خودتان TimeoutStartSec= را تنظیم کنید.
پس از خروج پردازش، واحد به وضعیت غیرفعال (inactive) بازمیگردد. RemainAfterExit=yes آن را در وضعیت active نگه میدارد، بدون اینکه هیچ پردازشی در حال اجرا باشد. این نسخهٔ عمدی از نشانهای است که در ابتدای این صفحه ذکر شد و زمانی صحیح است که وظیفهٔ واحد، بهجای نگه داشتن یک سرویس در حال اجرا، ایجاد تغییر در وضعیت سیستم باشد؛ مانند بارگذاری مجموعهای از قوانین فایروال یا بالا آوردن یک stack از کانتینرها. این همان الگویی است که در یک Docker Compose stack که پس از reboot دوباره بالا میآید استفاده میشود؛ جایی که واحد دستور compose را اجرا کرده، خارج میشود و فعال باقی میماند، زیرا کانتینرهایی که راهاندازی کرده است همچنان به حیات خود ادامه میدهند. یک واحد oneshot همچنین همان چیزی است که توسط یک زمانبندی (schedule) فراخوانی میشود؛ این بخش دیگرِ اجرای یک job روی systemd timer بهجای cron است.
استفاده از Type=notify به سرویس اجازه میدهد زمان آمادهبهکار بودن خود را اعلام کند
Type=notify تصمیمگیری درباره زمان آمادهسازی را به خود سرویس واگذار میکند. systemd عملیات شروع (start job) را باز نگه میدارد تا زمانی که پردازش، پیام READY=1 را از طریق یک Unix socket ارسال کند؛ مسیر این سوکت در متغیر محیطی NOTIFY_SOCKET دریافت میشود. رابط C برای این کار sd_notify(3) است و بسیاری از سرورها از قبل از آن پشتیبانی میکنند.
این دقیقترین پاسخ به پرسش «آیا سرویس شروع شده است؟» محسوب میشود. simple و exec وضعیت شروع را پیش از آنکه سرویس فایل پیکربندی خود را خوانده یا سوکت شنود (listening socket) را باز کرده باشد گزارش میدهند؛ بنابراین یک واحد وابسته ممکن است خیلی زود شروع به کار کند و در اولین اتصال خود با شکست مواجه شود. notify وضعیت شروع را دقیقاً در لحظهای گزارش میدهد که خود سرویس اعلام میکند آماده است.
systemd این پیام را تنها از پردازش اصلی (main process) میپذیرد که این همان معنای NotifyAccess=main است و Type=notify نیز آن را تلویحاً بیان میکند. اگر پیام از یک پردازش فرزند یا کمکی ارسال میشود، باید NotifyAccess=all را تنظیم کنید. یک اسکریپت shell میتواند systemd-notify --ready را فراخوانی کند، اما چون این کار به عنوان یک پردازش مجزا و کوتاهمدت اجرا میشود، به NotifyAccess=all نیاز دارد و ممکن است systemd نتواند پیامی را که فرستندهاش قبلاً خاتمه یافته است، به سرویس اصلی نسبت دهد. سرویسی که خود مستقیماً از این پروتکل استفاده میکند، قابلاطمینانتر است.
دو تنظیم مرتبط دیگر نیز ارزش دانستن دارند. Type=notify-reload که از نسخه systemd 253 در دسترس است، همین دستدادن (handshake) را به عملیات reload نیز تعمیم میدهد؛ بنابراین دستور systemctl reload زمانی بازمیگردد که سرویس گزارش دهد عملیات reload به پایان رسیده است، نه زمانی که سیگنال ارسال شده است. WatchdogSec= از سرویسی که از قابلیت notify استفاده میکند میخواهد که در فواصل زمانی مشخص، یک پیام keep-alive ارسال کند و systemd در صورت از دست رفتن مهلت زمانی، آن را به عنوان یک خطا در نظر میگیرد.
Type=dbus و Type=idle
Type=dbus تا زمانی که سرویس نامی را روی D-Bus ثبت نکند، منتظر میماند؛ D-Bus همان message bus است که سرویسهای سیستم و دسکتاپ برای ارتباط با یکدیگر از آن استفاده میکنند. این گزینه به BusName= نیاز دارد و به محض تنظیم BusName=، به حالت پیشفرض تبدیل میشود. از این گزینه فقط برای سرویسی استفاده کنید که واقعاً یک نام روی bus ثبت میکند.
Type=idle مشابه simple عمل میکند، اما اجرای برنامه را تا زمانی که jobهای در صف پردازش شوند به تأخیر میاندازد (با محدودیت زمانی 5 ثانیه). این گزینه برای این وجود دارد که خروجی کنسول در زمان بوت با پیامهای وضعیت تداخل پیدا نکند. این یک ابزار برای تعیین ترتیب (ordering) نیست و نباید در سرویسهای معمولی استفاده شود.
چرا اسکریپتهای wrapper باعث میشوند systemd روی PID اشتباه باقی بماند
این ساختاری است که منجر به بروز مشکل اصلی میشود.
[Service]
Type=simple
ExecStart=/opt/app/run.sh#!/bin/bash
export APP_ENV=production
/opt/app/bin/server --config /etc/app.yaml &
/opt/app/bin/exporter --port 9101systemd پوسته (shell) را به عنوان PID اصلی ثبت میکند. پوسته تا زمانی که exporter در پیشزمینه اجرا میشود، زنده میماند. اگر server از کار بیفتد، پوسته متوجه نمیشود؛ بنابراین PID اصلی همچنان زنده است، وضعیت unit همچنان active باقی میماند و Restart= چیزی برای اقدام کردن ندارد. هر دو پردازش در تمام مدت در cgroup مربوط به unit باقی میمانند، بنابراین systemctl stop همچنان پاکسازی را به درستی انجام میدهد. آنچه دچار اختلال شده، نظارت (supervision) است، نه پاکسازی.
راهکار به تعداد پردازشهای طولانیمدت (long-running) موجود در unit بستگی دارد.
اگر تنها یک پردازش وجود دارد، پوسته را با آن جایگزین کنید.
#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yamlexec پوسته را با برنامه نامبرده جایگزین میکند و همان PID را حفظ میکند، بنابراین PID که systemd ثبت کرده است اکنون متعلق به daemon است. حتی بهتر است wrapper را حذف کنید. Environment= و EnvironmentFile= متغیرها را منتقل میکنند و ExecStartPre= مرحله راهاندازی را انجام میدهد، بنابراین systemd میتواند daemon را مستقیماً اجرا کند و PID آن را از همان ابتدا بداند.
اگر دو پردازش وجود دارد، هیچ PID واحدی نماینده کل unit نیست. آنها را به دو unit مجزا تقسیم کنید و با استفاده از After= و Wants= ترتیب اجرای آنها را مشخص کنید. یک unit برای هر پردازش، ساختاری است که systemd به خوبی بر آن نظارت میکند و این تنها راهی است که هر پردازش رفتار راهاندازی مجدد (restart) خاص خود را داشته باشد.
تغییرات ExitType=cgroup چیست
ExitType= در نسخه 250 از systemd اضافه شد. مقدار پیشفرض main است: واحد (unit) زمانی متوقف تلقی میشود که پردازش اصلی (main process) خاتمه یابد. با استفاده از ExitType=cgroup، واحد تا زمانی که هر پردازشی در cgroup آن زنده باشد، در حال اجرا در نظر گرفته میشود.
[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcherاین تنظیم یک مشکل خاص را حل میکند. تحت مقدار ExitType=main، اگر یک راهانداز (launcher) کار اصلی را شروع کرده و سپس خارج شود، systemd واحد را متوقفشده تلقی کرده و پردازشهای باقیمانده را میکشد. با استفاده از ExitType=cgroup، واحد وضعیت کل گروه را دنبال میکند.
دقت کنید که این تنظیم چه چیزی را حل نمیکند. ExitType=cgroup یک واحد را تا زمانی که حداقل یک پردازش زنده باشد فعال نگه میدارد، بنابراین واحدی که دو دیمون (daemon) را مدیریت میکند، پس از مرگ یکی از آنها همچنان فعال میماند. این تنظیم مشکل راهاندازها را حل میکند، اما یک واحد را به ناظر چندین پردازش مستقل تبدیل نمیکند. همچنین ExitType= را نمیتوان با Type=oneshot ترکیب کرد.
cgroup جایی است که حسابرسی منابع (resource accounting) نیز در آن انجام میشود، بنابراین محدودیتهایی مانند MemoryMax= و CPUQuota= بر تمام پردازشهایی که واحد ایجاد کرده است اعمال میشود، فارغ از اینکه Type= درباره PID اصلی چه میگوید. آن بخش از موضوع در محدود کردن حافظه و CPU سرویس با systemd آمده است.
How to find the process systemd is actually watching
Work through this on the unit you are debugging, in order. Read what systemd loaded, then read what it tracks, then compare that with the process table.
systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.servicesystemctl cat prints the unit file together with every drop-in that applies to it, so you are reading what systemd loaded rather than the file you remember editing. systemctl show prints the effective values, including the defaults you never wrote down. Note the value of MainPID before moving on.
systemd-cgls --unit=app.service
ps -o pid,ppid,stat,etime,args -p "$(systemctl show -p MainPID --value app.service)"systemd-cgls lists every process in the unit's cgroup. The ps line describes the single process systemd supervises. Read the two together. A MainPID of 0 means systemd has no process to watch. A MainPID that resolves to a shell while the cgroup also holds your daemon is the wrapper case above. A cgroup with more processes than you expected means a launcher or a forking daemon is involved.
systemctl status app.service
journalctl -u app.service -bsystemctl status prints the state line and the cgroup tree together, so it often answers both questions at once. journalctl -u limited to this boot with -b shows the start and stop events systemd recorded for the unit, with the exit codes it saw. If the daemon writes to its own log file instead of the journal, read that file as well, because systemd can only record what reached it.
When you change Type=, reload and restart.
systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.servicesystemd-analyze verify parses the file and reports settings it cannot accept. daemon-reload makes systemd re-read unit files from disk. A changed Type= does not apply to an already running unit, so the restart is required, not optional.
Then test the change. Take the PID of the process you actually care about from systemd-cgls and kill it. Run systemctl is-active app.service straight after. If Type= is right, the unit leaves the active state. If it stays active, systemd is still watching something else.
از کدام Type= در سرویس systemd استفاده کنید
- برنامهای که در پیشزمینه (foreground) باقی میماند:
Type=exec. - برنامهای که از اعلان آمادگی (readiness notification) پشتیبانی میکند:
Type=notify، و اگر بازخوانی (reload) را نیز تأیید میکند:notify-reload. - دیمونی که اصرار دارد به پسزمینه (background) برود:
Type=forkingبه همراهPIDFile=، یا استفاده از سوئیچ پیشزمینه آن باType=exec. - اسکریپتی که کاری را انجام داده و خارج میشود:
Type=oneshot، به علاوهRemainAfterExit=yesدر صورتی که هدف، باقی گذاشتن وضعیت (state) باشد. - لانچری که خارج میشود در حالی که فرزندانش به اجرا ادامه میدهند:
Type=simpleبه همراهExitType=cgroup.
اگر مطمئن نیستید که یک دیمون شخص ثالث به کدام نوع نیاز دارد، ابتدا فایل unit بستهبندیشده آن را بخوانید. اجرای systemctl cat روی یک unit که توزیع ارائه کرده است، Type= انتخابشده توسط توسعهدهنده اصلی را نشان میدهد؛ انتخابی که توسط افراد بیشتری نسبت به شما تست شده است.
FAQ
چرا وقتی پردازش متوقف میشود، unit در systemd همچنان active باقی میماند؟
زیرا پردازشی که systemd آن را به عنوان پردازش اصلی (main) میشناسد، همچنان زنده است. systemd به جای نظارت بر تمام پردازشهای موجود در cgroup مربوط به unit، تنها یک PID را بر اساس Type= زیر نظر میگیرد. دلیل معمول این اتفاق، استفاده از یک اسکریپت wrapper است که با Type=simple اجرا شده است: در این حالت، shell همان PID اصلی است، بنابراین وقتی daemonای که توسط shell در پسزمینه اجرا شده متوقف میشود، unit همچنان active باقی میماند. دستور systemctl show -p MainPID app.service را اجرا کنید، سپس cgroup مربوط به unit را با systemd-cgls --unit=app.service لیست کرده و این دو را با هم مقایسه کنید.
تفاوت بین Type=simple و Type=exec چیست؟
در Type=simple، به محض اینکه systemd پردازش را ایجاد کند، unit به عنوان شروعشده در نظر گرفته میشود؛ حتی پیش از آنکه فایل باینری اجرا شده باشد. به همین دلیل، اگر مسیر در ExecStart= اشتباه باشد، همچنان یک start job موفق گزارش میشود و پس از آن خطا رخ میدهد. در مقابل، Type=exec منتظر میماند تا اجرا با موفقیت انجام شود، بنابراین خطا مستقیماً توسط همان start job گزارش خواهد شد. هر دو گزینه، یک پردازش واحد را به عنوان PID اصلی در نظر میگیرند. Type=exec به نسخه 240 یا جدیدتر systemd نیاز دارد.
آیا در صورت استفاده از Type=forking همچنان به PIDFile= نیاز دارم؟
بله، هر زمان که daemon فایلی برای PID مینویسد، باید از آن استفاده کنید. بدون این گزینه، systemd به GuessMainPID= متکی میشود که صرفاً یک حدس است و تنها برای سرویسهایی که به یک پردازش واحد محدود میشوند، قابل اطمینان است. وقتی این حدس اشتباه باشد یا غیرممکن باشد، تشخیص خطا و راهاندازی مجدد خودکار برای آن unit از کار میافتد. مقدار PIDFile= را دقیقاً به مسیری که daemon در آن مینویسد اشاره دهید؛ این مسیر معمولاً در /run قرار دارد.
چه زمانی باید از RemainAfterExit=yes استفاده کنم؟
زمانی که هدف unit تغییر وضعیت سیستم باشد، نه حفظ یک پردازش در حال اجرا. یک unit از نوع Type=oneshot که قوانین فایروال را بارگذاری میکند یا یک stack از containerها را بالا میآورد، به محض پایان کارش خارج میشود. بدون RemainAfterExit=yes، وضعیت unit به inactive تغییر میکند که باعث میشود systemctl stop پردازشی برای متوقف کردن نداشته باشد و راهی برای اجرای ExecStop= جهت پاکسازی باقی نماند. با فعال کردن این گزینه، unit بدون داشتن هیچ پردازشی در وضعیت active باقی میماند که در این سناریو، رفتار مورد انتظار است.
آیا تغییر Type= نیاز به daemon-reload دارد؟
بله، علاوه بر آن نیاز به restart کردن خود unit نیز هست. دستور systemctl daemon-reload باعث میشود systemd فایلهای unit موجود روی دیسک را دوباره بخواند، اما نمونه در حال اجرا همچنان از Type=ای که با آن شروع شده استفاده میکند. پیش از تست، دستور sudo systemctl daemon-reload و سپس sudo systemctl restart app.service را اجرا کنید؛ در غیر این صورت، شما همچنان در حال مشاهده رفتار نظارتی قدیمی هستید.