systemd Type: simple, forking, notify - jak wybrać?
Usługa ma status active, mimo że proces nie działa? Wyjaśniamy, jak poprawnie dobrać dyrektywę Type w systemd, aby uniknąć błędnego monitorowania PID i zapewnić poprawne restarty.
Dlaczego systemd zgłasza jednostkę jako aktywną, mimo że proces zakończył działanie
Jednostka usługi systemd pozostaje w stanie active, dopóki proces, który systemd uznaje za główny, jest uruchomiony. Sekcja [Service] oraz dyrektywa Type= określają, który to proces. Wybór niewłaściwej wartości powoduje, że systemd monitoruje skrypt powłoki lub krótkotrwały proces nadrzędny, podczas gdy właściwy demon wewnątrz tej samej jednostki kończy działanie. Jednostka przekazuje informacje o procesie, który została skonfigurowana do monitorowania.
Zmiana polityki restartu nie rozwiąże tego problemu. Restart= reaguje dopiero po zakończeniu głównego procesu, więc Restart=always nigdy nie zostanie wyzwolone, dopóki główny PID (identyfikator procesu) należy do działającego obiektu. Należy najpierw poprawić Type=. Działania podejmowane przez systemd po faktycznym zakończeniu głównego procesu są odrębną kwestią, opisaną w przewodniku po Restart= oraz RestartSec=.
Co określa dyrektywa Type=
Każda wartość Type= odpowiada jednocześnie na dwa pytania. Kiedy systemd uznaje jednostkę za uruchomioną oraz który proces jest procesem głównym.
Pierwsza odpowiedź kontroluje kolejność startu. Jednostka wskazująca na Twoją usługę w After= czeka, aż systemd uzna Twoją usługę za uruchomioną. Wartość Type=, która zbyt wcześnie zgłasza stan "uruchomiono", pozwala jednostkom zależnym na start, zanim Twoja usługa będzie w stanie je obsłużyć.
Druga odpowiedź kontroluje nadzór. systemd umieszcza każdy proces wywołany przez jednostkę w cgroup (grupie kontrolnej). Jest to funkcja jądra systemu, która grupuje procesy w celu wspólnego ograniczania zasobów i kończenia ich działania. cgroup jest mechanizmem, dzięki któremu systemctl stop czyści zasoby: KillMode= domyślnie przyjmuje control-group, więc zatrzymanie jednostki wysyła sygnał do wszystkich procesów wewnątrz niej. Główny PID (Main PID) ma węższe znaczenie. Jest to pojedynczy proces, którego zakończenie kończy działanie jednostki, a jego kod wyjścia staje się wynikiem pracy jednostki. Odczytywanie cgroup tak, jakby była głównym PID-em, jest źródłem nieporozumień.
Typ=simple zgłasza uruchomienie przed wykonaniem pliku binarnego
Type=simple jest wartością domyślną, gdy ustawiono ExecStart=, a nie zdefiniowano Type= ani BusName=. systemd tworzy proces, uznaje jednostkę za natychmiast uruchomioną i traktuje ten proces jako główny PID. Kolejne jednostki startują niezwłocznie, zanim plik binarny usługi zostanie faktycznie wykonany.
Ten szczegół wyjaśnia częste zaskoczenie. Literówka w ścieżce ExecStart= nadal skutkuje powodzeniem zadania startowego, a błąd pojawia się chwilę później, gdy wykonanie kończy się niepowodzeniem. systemd rejestruje taki przypadek z kodem wyjścia 203, który w jego własnej tabeli nosi nazwę EXEC i oznacza błąd wykonania pliku binarnego usługi. Zatem polecenie systemctl start kończące się bez błędu nie stanowi dowodu na istnienie pliku binarnego.
Należy używać simple dla programów, które pozostają na pierwszym planie i nie przenoszą się samodzielnie w tło. Dotyczy to większości nowoczesnych demonów oraz niemal każdego oprogramowania tworzonego we własnym zakresie.
Typ=exec czeka na faktyczne uruchomienie programu
Type=exec to simple z dodatkowym krokiem. systemd uznaje jednostkę za uruchomioną dopiero po pomyślnym zakończeniu procesu fork oraz wykonaniu pliku binarnego. Brak pliku binarnego lub User=, którego nie można rozwiązać, powoduje teraz niepowodzenie zadania startowego, zamiast zgłaszania sukcesu i cichego wyłączenia chwilę później.
Type=exec pojawiło się w systemd 240, więc każda obecna dystrybucja serwerowa je posiada. Ubuntu 24.04 dostarcza systemd 255, a Debian 13 dostarcza systemd 257, według stanu na sierpień 2026. Wersję własnego systemu można sprawdzić za pomocą systemctl --version.
Koszt stanowi jeden dodatkowy krok synchronizacji podczas startu. Zyskiem jest wiarygodny kod wyjścia z systemctl start. W przypadku programów działających na pierwszym planie, należy preferować exec zamiast simple.
Type=forking oraz problem utraty głównego PID
Type=forking informuje systemd, że proces wskazany w ExecStart= utworzy proces potomny (fork), a następnie celowo zakończy działanie. systemd oczekuje na zakończenie tego pierwszego procesu i dopiero wtedy uznaje jednostkę za uruchomioną. Pozostały proces potomny jest właściwym daemonem.
Trudność polega na identyfikacji. Proces uruchomiony przez systemd już nie istnieje, więc systemd musi ustalić, który z pozostałych procesów jest główny. Należy ustawić PIDFile= na plik, do którego daemon zapisuje swój PID, zazwyczaj jest to ścieżka w /run, z której systemd odczytuje wartość. systemd sprawdza również, czy PID zawarty w tym pliku odnosi się do procesu należącego do tej usługi, dzięki czemu nieaktualny plik wskazujący na niepowiązany proces zostaje odrzucony zamiast uznany za wiarygodny.
Bez PIDFile= stosowane jest GuessMainPID=, którego domyślną wartością jest yes. Zgadywanie jest wiarygodne tylko wtedy, gdy usługa ogranicza się do pojedynczego procesu. Dokumentacja jasno określa ograniczenie: jeśli daemon składa się z więcej niż jednego procesu, zgadnięcie może być błędne, a wykrywanie awarii przestaje działać. Jednostka może również otrzymać główny PID o wartości 0, co oznacza, że systemd nie ma czego nadzorować.
Większość daemonów korzystających z fork posiada przełącznik utrzymujący je na pierwszym planie (foreground). Należy użyć tego przełącznika w Type=exec i usunąć linię PIDFile=. Mniejsza liczba elementów składowych oznacza mniejsze ryzyko utraty PID.
Type=oneshot dla zadań kończących działanie
Type=oneshot oczekuje, że proces zostanie uruchomiony i zakończy pracę. systemd uznaje jednostkę za uruchomioną dopiero po zakończeniu jej działania, co czyni oneshot właściwym wyborem dla zadań, na które muszą oczekiwać inne jednostki. Jest to również domyślny typ, gdy jednostka nie określa ani Type=, ani ExecStart=.
Dwa zachowania są specyficzne dla oneshot. Jest to jedyny typ, który akceptuje więcej niż jedną linię ExecStart=, a linie te są wykonywane w kolejności. Limit czasu uruchomienia jest również domyślnie wyłączony, więc oneshot, który zawiesza się, będzie oczekiwał w nieskończoność, chyba że samodzielnie ustawisz TimeoutStartSec=.
Po zakończeniu procesu jednostka powraca do stanu nieaktywnego. RemainAfterExit=yes utrzymuje ją w stanie active, mimo że żaden proces nie jest uruchomiony. Jest to celowa wersja objawu opisanego na początku tej strony i jest to poprawne zachowanie, gdy zadaniem jednostki było pozostawienie określonego stanu, a nie utrzymywanie działającego procesu: ładowanie zestawu reguł firewalla lub uruchamianie stosu kontenerów. Jest to wzorzec stojący za stosem Docker Compose, który powraca po restarcie, gdzie jednostka wykonuje polecenie compose, kończy działanie i pozostaje aktywna, ponieważ uruchomione przez nią kontenery działają nadal. Jednostka oneshot jest również tym, co wyzwala harmonogram, co stanowi drugą połowę uruchamiania zadania za pomocą systemd timer zamiast cron.
Typ=notify pozwala usłudze zasygnalizować gotowość do pracy
Type=notify przenosi decyzję o gotowości na samą usługę. systemd wstrzymuje zadanie uruchomienia do momentu, aż proces wyśle READY=1 przez gniazdo Unix, którego ścieżkę otrzymuje w zmiennej środowiskowej NOTIFY_SOCKET. Interfejs C to sd_notify(3) i wiele serwerów już go obsługuje.
Jest to dokładna odpowiedź na pytanie, czy usługa została uruchomiona. simple oraz exec zgłaszają uruchomienie, zanim usługa odczyta konfigurację lub otworzy gniazdo nasłuchujące, co może spowodować, że zależna jednostka wystartuje zbyt wcześnie i nie nawiąże połączenia. notify zgłasza uruchomienie w momencie, gdy sama usługa potwierdzi gotowość.
systemd akceptuje ten komunikat wyłącznie od procesu głównego, co oznacza NotifyAccess=main, a Type=notify to wymusza. Jeśli komunikat pochodzi od procesu potomnego lub pomocniczego, należy ustawić NotifyAccess=all. Skrypt powłoki może wywołać systemd-notify --ready, ale działa on jako osobny, krótkotrwały proces, więc wymaga NotifyAccess=all, a systemd może nie być w stanie przypisać komunikatu, którego nadawca już zakończył działanie. Usługa obsługująca protokół samodzielnie jest bardziej niezawodna.
Warto znać dwa powiązane ustawienia. Type=notify-reload, dostępne od systemd 253, rozszerza ten sam mechanizm na przeładowania, dzięki czemu systemctl reload kończy działanie dopiero po zgłoszeniu przez usługę zakończenia przeładowania, zamiast zwracać wynik natychmiast po wysłaniu sygnału. WatchdogSec= wymaga od usługi wysyłania komunikatu keep-alive w określonych odstępach czasu, a przekroczenie terminu systemd traktuje jako awarię.
Type=dbus oraz Type=idle
Type=dbus czeka, aż usługa zarejestruje nazwę w D-Bus, czyli magistrali komunikatów używanej przez usługi systemowe i desktopowe do komunikacji między sobą. Wymaga to użycia BusName= i staje się ustawieniem domyślnym, gdy tylko BusName= zostanie zdefiniowane. Należy stosować to ustawienie wyłącznie dla usług, które faktycznie rejestrują nazwę w magistrali.
Type=idle działa podobnie do simple, ale opóźnia uruchomienie programu do momentu przetworzenia zakolejkowanych zadań, z limitem czasowym wynoszącym pięć sekund. Mechanizm ten służy zapobieganiu przeplataniu się komunikatów wyjściowych konsoli podczas rozruchu z komunikatami o stanie usługi. Nie jest to narzędzie do zarządzania kolejnością uruchamiania i nie powinno być stosowane w standardowych usługach.
Dlaczego skrypt opakowujący powoduje błędne rozpoznanie PID przez systemd
Oto schemat, który wywołuje ten objaw.
[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 rejestruje powłokę jako główny PID. Powłoka pozostaje aktywna, podczas gdy exporter działa na pierwszym planie. Jeśli server ulegnie awarii, powłoka tego nie zauważy, więc główny PID nadal figuruje jako aktywny, jednostka pozostaje w stanie active, a Restart= nie ma na co reagować. Oba procesy przez cały czas znajdują się w cgroup jednostki, więc systemctl stop nadal poprawnie przeprowadza czyszczenie. Awarii uległ nadzór, a nie proces czyszczenia.
Rozwiązanie zależy od tego, ile długo działających procesów faktycznie posiada jednostka.
Jeśli jest tylko jeden, należy zastąpić nim powłokę.
#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yamlexec zastępuje powłokę wskazanym programem i zachowuje ten sam PID, dzięki czemu PID zarejestrowany przez systemd należy teraz do demona. Jeszcze lepiej jest usunąć skrypt opakowujący. Environment= oraz EnvironmentFile= przekazują zmienne, a ExecStartPre= realizuje krok konfiguracji, dzięki czemu systemd może uruchomić demona bezpośrednio i znać jego PID z samej konstrukcji.
Jeśli procesów jest dwa, żaden pojedynczy PID nie reprezentuje jednostki. Należy rozdzielić je na dwie jednostki i uporządkować za pomocą After= oraz Wants=. Jedna jednostka na proces to układ, który systemd nadzoruje poprawnie i jest to jedyny sposób, aby każdy proces otrzymał własną politykę restartu.
Czym są zmiany ExitType=cgroup
ExitType= wprowadzono w systemd 250. Wartością domyślną jest main: jednostka jest uznawana za zatrzymaną, gdy kończy działanie główny proces. Przy ExitType=cgroup jednostka jest uznawana za uruchomioną tak długo, jak żyje jakikolwiek proces w jej cgroup.
[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcherRozwiązuje to konkretny problem. Program uruchamiający, który inicjuje właściwe zadanie, a następnie kończy działanie, przy ExitType=main powodowałby uznanie jednostki przez systemd za zatrzymaną i zabicie pozostałych procesów. Dzięki ExitType=cgroup jednostka monitoruje całą grupę.
Należy jasno określić, czego to rozwiązanie nie naprawia. ExitType=cgroup utrzymuje jednostkę jako aktywną, dopóki żyje przynajmniej jeden proces, więc jednostka obsługująca dwa demony pozostanie aktywna po awarii jednego z nich. Rozwiązuje to problem programu uruchamiającego. Nie zmienia to jednak jednostki w nadzorcę kilku niezależnych procesów. ExitType= nie może być również łączone z Type=oneshot.
Cgroup jest również miejscem, w którym odbywa się rozliczanie zasobów, więc limity takie jak MemoryMax= i CPUQuota= mają zastosowanie do każdego procesu uruchomionego przez jednostkę, niezależnie od tego, co Type= mówi o głównym PID. Ten aspekt został opisany w ograniczanie pamięci i procesora usługi za pomocą systemd.
Jak znaleźć proces, który jest faktycznie monitorowany przez systemd
Należy wykonać poniższe kroki na jednostce, która jest diagnozowana. Najpierw należy odczytać konfigurację wczytaną przez systemd, następnie sprawdzić, co jest monitorowane, a na końcu porównać to z tablicą procesów.
systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.servicesystemctl cat wyświetla plik jednostki wraz ze wszystkimi zastosowanymi plikami typu drop-in, dzięki czemu można sprawdzić, co faktycznie wczytał systemd, a nie tylko to, co pamiętamy z edycji pliku. systemctl show wyświetla efektywne wartości, w tym wartości domyślne, które nie zostały jawnie zdefiniowane. Przed przejściem dalej należy zanotować wartość MainPID.
systemd-cgls --unit=app.service
ps -o pid,ppid,stat,etime,args -p "$(systemctl show -p MainPID --value app.service)"systemd-cgls wymienia wszystkie procesy w cgroup danej jednostki. Linia ps opisuje pojedynczy proces nadzorowany przez systemd. Należy przeanalizować obie te informacje łącznie. Wartość MainPID równa 0 oznacza, że systemd nie monitoruje żadnego procesu. Jeśli MainPID wskazuje na powłokę (shell), podczas gdy w cgroup znajduje się również demon, oznacza to przypadek użycia wrappera opisany powyżej. Cgroup zawierająca więcej procesów niż oczekiwano oznacza, że w działanie zaangażowany jest skrypt uruchamiający lub demon typu forking.
systemctl status app.service
journalctl -u app.service -bsystemctl status wyświetla linię stanu oraz drzewo cgroup, co często pozwala odpowiedzieć na oba pytania jednocześnie. journalctl -u ograniczone do bieżącego uruchomienia systemu za pomocą -b pokazuje zdarzenia uruchomienia i zatrzymania zarejestrowane przez systemd dla danej jednostki, wraz z zaobserwowanymi kodami wyjścia. Jeśli demon zapisuje logi do własnego pliku zamiast do journal, należy również sprawdzić ten plik, ponieważ systemd rejestruje tylko to, co do niego dotarło.
Po zmianie Type= należy przeładować konfigurację i zrestartować jednostkę.
systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.servicesystemd-analyze verify analizuje plik i zgłasza ustawienia, których nie może zaakceptować. daemon-reload wymusza na systemd ponowne odczytanie plików jednostek z dysku. Zmiana Type= nie ma zastosowania do już działającej jednostki, dlatego restart jest wymagany, a nie opcjonalny.
Następnie należy przetestować zmianę. Należy pobrać PID procesu, który jest istotny, za pomocą systemd-cgls i zakończyć go. Bezpośrednio po tym należy wykonać systemctl is-active app.service. Jeśli Type= jest poprawna, jednostka przejdzie w stan nieaktywny. Jeśli pozostanie aktywna, oznacza to, że systemd nadal monitoruje inny proces.
Który typ usługi systemd Type= należy wybrać
- Program działający w pierwszym planie:
Type=exec. - Program obsługujący powiadomienie o gotowości:
Type=notify, a takżenotify-reload, jeśli potwierdza również przeładowania. - Demon, który wymusza przejście w tło:
Type=forkingzPIDFile=lub jego przełącznik pierwszoplanowy zType=exec. - Skrypt wykonujący zadanie i kończący działanie:
Type=oneshotorazRemainAfterExit=yes, gdy celem było pozostawienie stanu. - Uruchamiacz, który kończy działanie, podczas gdy jego procesy potomne nadal pracują:
Type=simplezExitType=cgroup.
W przypadku braku pewności, jakiego typu wymaga demon zewnętrznego dostawcy, należy najpierw przeczytać dostarczony plik jednostki. Uruchomienie systemctl cat dla jednostki dostarczonej przez dystrybucję pokazuje Type= wybrane przez twórców oprogramowania; wybór ten został przetestowany przez większą liczbę osób niż własna konfiguracja.
FAQ
Dlaczego moja jednostka systemd pozostaje aktywna, mimo że proces zakończył działanie?
Ponieważ proces, który systemd uznaje za główny, nadal żyje. systemd monitoruje pojedynczy PID dla usługi, wybrany zgodnie z Type=, a nie każdy proces w cgroup jednostki. Częstą przyczyną jest skrypt opakowujący uruchomiony za pomocą Type=simple: powłoka jest głównym PID, więc jednostka pozostaje aktywna, gdy demon uruchomiony przez powłokę w tle kończy działanie. Uruchom systemctl show -p MainPID app.service, następnie wyświetl cgroup jednostki za pomocą systemd-cgls --unit=app.service i porównaj oba wyniki.
Jaka jest różnica między Type=simple a Type=exec?
Type=simple uznaje jednostkę za uruchomioną, gdy tylko systemd utworzy proces, jeszcze przed wykonaniem pliku binarnego, więc błędna ścieżka w ExecStart= nadal skutkuje udanym zadaniem startowym, po którym następuje błąd. Type=exec czeka, aż wykonanie zakończy się powodzeniem, dzięki czemu błąd jest zgłaszany bezpośrednio przez zadanie startowe. Oba typy traktują ten sam proces jako główny PID. Type=exec wymaga systemd w wersji 240 lub nowszej.
Czy nadal potrzebuję PIDFile= przy Type=forking?
Tak, zawsze gdy demon go zapisuje. Bez niego systemd korzysta z GuessMainPID=, co jest jedynie szacowaniem i działa niezawodnie tylko dla usług, które działają jako pojedynczy proces. Gdy szacowanie jest błędne lub niemożliwe, wykrywanie awarii i automatyczne restarty przestają działać dla tej jednostki. Wskaż PIDFile= dokładną ścieżkę, do której zapisuje demon, zazwyczaj w /run.
Kiedy należy użyć RemainAfterExit=yes?
Gdy celem jednostki była zmiana stanu systemu, a nie utrzymywanie procesu w działaniu. Jednostka Type=oneshot, która ładuje reguły firewalla lub uruchamia stos kontenerów, kończy działanie zaraz po wykonaniu zadania, a bez RemainAfterExit=yes jednostka przechodzi w stan nieaktywny. Pozostawia to systemctl stop bez procesu do zatrzymania i bez możliwości wykonania czyszczenia ExecStop=. Dzięki tej opcji jednostka pozostaje aktywna bez żadnych procesów, co jest w tym przypadku zamierzone.
Czy zmiana Type= wymaga wykonania daemon-reload?
Tak, a także restartu jednostki. systemctl daemon-reload sprawia, że systemd ponownie odczytuje pliki jednostek z dysku, ale działająca instancja zachowuje Type=, z którym została uruchomiona. Przed testowaniem wykonaj sudo systemctl daemon-reload, a następnie sudo systemctl restart app.service, w przeciwnym razie nadal będziesz obserwować stare zachowanie nadzorcze.