SSD Nodes Learn Hosting plans →
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-28

systemd Type: simple, forking czy notify? Wybór typu

Dowiedz się, jak poprawnie skonfigurować dyrektywę Type w pliku unit, aby uniknąć błędnego statusu active przy wyłączonym procesie. Wyjaśniamy różnice między simple, forking i notify.

Why does systemd report a unit as active when the process died

A systemd service unit stays active while the one process systemd calls the main process is alive, and Type= in the [Service] section decides which process that is. Pick the wrong value and systemd ends up watching a shell wrapper or a short lived parent while the daemon you care about dies inside the same unit. The unit is telling you the truth about the process it was told to watch.

Changing the restart policy will not help here. Restart= acts when the main process exits, so Restart=always never fires while the main PID (process identifier) belongs to something that is still running. Fix Type= first. What systemd does after the main process really exits is a separate decision, covered in the guide to Restart= and 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, która wskazuje Twoją w After=, czeka, aż systemd uzna Twoją za uruchomioną. Type=, który zgłasza stan "uruchomiono" zbyt wcześnie, pozwala jednostkom zależnym na start, zanim Twoja usługa będzie w stanie obsłużyć żądania.

Druga odpowiedź kontroluje nadzór. systemd umieszcza każdy proces wywołany przez jednostkę w cgroup (grupie kontrolnej). Jest to funkcja jądra, która grupuje procesy, umożliwiając ich wspólne ograniczanie i kończenie. cgroup jest mechanizmem, dzięki któremu systemctl stop sprząta po usłudze: KillMode= domyślnie przyjmuje control-group, więc zatrzymanie jednostki wysyła sygnał do każdego procesu wewnątrz niej. Główny 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 jednostki. Interpretowanie cgroup jako głównego PID jest źródłem nieporozumień.

Type=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, jeszcze zanim plik binarny usługi zostanie wykonany.

Ten ostatni 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 jest definiowany jako 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 nigdy nie przenoszą się w tło. Dotyczy to większości nowoczesnych demonów oraz niemal każdego oprogramowania tworzonego samodzielnie.

Typ=exec czeka na faktyczne uruchomienie programu

Type=exec to simple z jednym 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 rzetelny kod wyjścia z systemctl start. W przypadku programów działających na pierwszym planie, należy preferować exec zamiast simple.

Type=forking i problem utraty głównego PID

Type=forking informuje systemd, że proces określony 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. Ten nawyk wywodzi się z ery SysV, kiedy żaden mechanizm nie nadzorował daemona po zakończeniu działania skryptu init, a plik PID był jedynym zapisem stanu uruchomienia – ograniczenie to stanowi sedno dlaczego systemd zastąpił skrypty init.

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 zapisywany przez daemona, zazwyczaj ścieżkę wewnątrz /run, z którego systemd odczyta PID. systemd sprawdza również, czy PID w tym pliku odnosi się do procesu należącego do tej usługi, dzięki czemu nieaktualny plik wskazujący na niepowiązany proces jest odrzucany zamiast uznawany za wiarygodny.

Bez PIDFile= stosowane jest GuessMainPID=, które domyślnie przyjmuje wartość yes. To domniemanie jest wiarygodne tylko wtedy, gdy usługa ogranicza się do pojedynczego procesu. Dokumentacja techniczna jasno określa to ograniczenie: jeśli daemon składa się z więcej niż jednego procesu, domniemanie może być błędne, a wykrywanie awarii przestaje działać. Jednostka może również zakończyć pracę z głównym PID równym 0, co oznacza, że systemd nie ma żadnego procesu do nadzorowania.

Większość daemonów korzystających z forkowania posiada również przełącznik utrzymujący proces 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 się

Type=oneshot oczekuje, że proces zostanie uruchomiony i zakończy działanie. systemd uznaje jednostkę za uruchomioną dopiero po jej zakończeniu, co czyni oneshot odpowiednim 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 (start timeout) 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 wraca do stanu nieaktywnego. RemainAfterExit=yes utrzymuje ją w stanie active, mimo że żaden proces nie jest uruchomiony. Jest to zamierzona 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 czegoś w działaniu: ładowanie zestawu reguł zapory sieciowej 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żka jest przekazywana w zmiennej środowiskowej NOTIFY_SOCKET. Interfejs C to sd_notify(3) i wiele serwerów obsługuje go natywnie.

Jest to precyzyjna 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 prowadzić do zbyt wczesnego startu jednostek zależnych i błędów 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 wymusza takie zachowanie. Jeśli komunikat pochodzi od procesu potomnego lub pomocniczego, należy ustawić NotifyAccess=all. Skrypt powłoki może wywołać systemd-notify --ready, jednak jest to proces krótkotrwały, 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 komunikująca się z protokołem bezpośrednio jest bardziej niezawodna.

Warto znać dwa powiązane ustawienia. Type=notify-reload, dostępne od systemd 253, rozszerza ten sam mechanizm uzgadniania na przeładowania, dzięki czemu systemctl reload kończy działanie dopiero po zgłoszeniu przez usługę zakończenia przeładowania, a nie natychmiast po wysłaniu sygnału. WatchdogSec= wymaga od usługi wysyłania komunikatu podtrzymującego w określonych odstępach czasu, a systemd traktuje przekroczenie terminu jako awarię.

Type=dbus oraz Type=idle

Type=dbus czeka, aż usługa zarejestruje nazwę w D-Bus, magistrali komunikatów używanej przez usługi systemowe i desktopowe do komunikacji między sobą. Wymaga to 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. Istnieje po to, aby komunikaty wyjściowe konsoli podczas rozruchu nie mieszały się 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, że systemd śledzi niewłaściwy PID

Oto struktura, która 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 9101

systemd 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 istnieje, jednostka jest nadal 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. Zepsuł się nadzór, a nie czyszczenie.

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.yaml

exec 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= przenoszą 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 procesy są 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.

Zmiany w 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 pozostaje aktywna tak długo, jak żywy jest jakikolwiek proces w jej cgroup.

[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcher

Rozwiązuje to konkretny problem. Launcher, który uruchamia właściwe zadanie i kończy pracę, przy ExitType=main powodowałby, że systemd uznałby jednostkę za zatrzymaną i zabił pozostałe procesy. Dzięki ExitType=cgroup jednostka monitoruje całą grupę.

Należy jasno określić, czego to rozwiązanie nie naprawia. ExitType=cgroup utrzymuje jednostkę aktywną, dopóki żyje przynajmniej jeden proces, więc jednostka zarządzająca dwoma demonami pozostanie aktywna po awarii jednego z nich. Rozwiązuje to problem launchera. Nie zmienia to jednostki w nadzorcę kilku niezależnych procesów. ExitType= nie może być również łączone z Type=oneshot.

Cgroup jest miejscem, w którym naliczane jest również zużycie 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 opisano w ograniczanie pamięci i procesora usługi za pomocą systemd.

Jak znaleźć proces, który faktycznie jest monitorowany przez systemd

Należy wykonać poniższe kroki na jednostce, która jest diagnozowana. Najpierw należy odczytać konfigurację załadowaną przez systemd, następnie sprawdzić, co jest śledzone, 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.service

systemctl cat wyświetla plik jednostki wraz ze wszystkimi zastosowanymi plikami typu drop-in, dzięki czemu czytana jest konfiguracja załadowana przez systemd, a nie tylko plik, który był edytowany. 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 wypisuje wszystkie procesy znajdujące się w cgroup 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 skryptu opakowującego (wrapper). Cgroup zawierająca więcej procesów niż oczekiwano oznacza, że w działanie zaangażowany jest program uruchamiający lub demon typu forking.

systemctl status app.service
journalctl -u app.service -b

systemctl 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ć usługę.

systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.service

systemd-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 w Type= nie jest stosowana 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 Type= w systemd należy wybrać

  • Program działający w pierwszym planie: Type=exec.
  • Program obsługujący powiadomienie o gotowości: Type=notify, a także notify-reload, jeśli potwierdza on również przeładowania.
  • Demon, który wymusza przejście w tło: Type=forking wraz z PIDFile= lub jego przełącznik pierwszego planu z Type=exec.
  • Skrypt wykonujący zadanie i kończący działanie: Type=oneshot, oraz RemainAfterExit=yes, gdy celem było pozostawienie stanu.
  • Program uruchamiający, który kończy działanie, podczas gdy jego procesy potomne nadal pracują: Type=simple wraz z ExitType=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, a ten wybór został przetestowany przez większą liczbę osób niż własna konfiguracja.

FAQ

Dlaczego jednostka systemd pozostaje aktywna, mimo że proces zakończył działanie?

Ponieważ proces, który systemd uznaje za główny, nadal żyje. systemd monitoruje jeden PID na usługę, wybrany zgodnie z Type=, a nie wszystkie procesy w cgroup jednostki. Częstą przyczyną jest skrypt opakowujący uruchomiony przez Type=simple: powłoka jest głównym PID, więc jednostka pozostaje aktywna, gdy demon uruchomiony przez tę powłokę w tle zakoń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. Dlatego błędna ścieżka w ExecStart= nadal skutkuje sukcesem zadania startowego, 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 muszę używać PIDFile= przy Type=forking?

Tak, jeśli demon zapisuje taki plik. Bez niego systemd polega na GuessMainPID=, co jest jedynie szacowaniem i działa poprawnie tylko dla usług, które stabilizują się w jednym procesie. Gdy szacowanie jest błędne lub niemożliwe, wykrywanie awarii i automatyczne restarty przestają działać dla tej jednostki. Wskaż w PIDFile= dokładną ścieżkę, do której demon zapisuje plik, zazwyczaj wewnątrz /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 zapory sieciowej lub uruchamia stos kontenerów, kończy działanie zaraz po wykonaniu zadania. Bez RemainAfterExit=yes jednostka przechodzi w stan nieaktywny, co sprawia, że systemctl stop nie ma czego zatrzymać i nie ma możliwości wykonania czyszczenia ExecStop=. Dzięki tej opcji jednostka pozostaje aktywna bez żadnych procesów, co jest w tym przypadku zamierzonym zachowaniem.

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 nadzorcy.