Demonizacja procesu – linux

 
Ech… blog miał być o cyfrowym audio/wideo, a tu linux i demony.

Ukończyłem wstępną implementację pewnej aplikacji, która docelowo ma działać jako demon w systemie, czyli działać bezustannie jako usługa, bez bezpośredniej interakcji z poziomu terminala. Typowe aplikacje “konsolowe”, czy to interaktywne, czy nie, po uruchomieniu przypisane są po pierwsze do procesu macierzystego, czyli shella (np bash), a po drugie do terminala sterującego (controlling terminal), którym może być terminal lokalny (tty, konsole w trybie tekstowym komputera), terminal realizowany przez port szeregowy lub modem, czy też pseudoterminal kontrolowany czy to przez serwer zdalnego shella (telnet, ssh), czy przez okienkową aplikację terminala (np. gnome-terminal). Taka aplikacja jest aplikacją interaktywną, zamknięcie czy to shella, czy też terminala poprzez np. przerwanie połączenia powoduje, że shell wysyła do swoich potomków sygnał hang up (czyli SIGHUP) historycznie symbolizujący odłożenie słuchawki telefonu-modemu. Demon, to proces działający bez “pana”, jedynym zwierzchnikiem jest proces init, a demon nie korzysta z terminala, czyli nie posiada standardowego wyjścia, wejścia czy strumienia błędów (stdout, stdin, stderr). Jak przygotować aplikację do roli demona?

Bieda-demonizacja

  • Jeśli init obsługuje plik inittab, można zamieścić tam wpis z akcją ‘once’ albo ‘respawn’. W przypadku tej drugiej init będzie nieustannie wywoływał daną aplikację, podobnie jak wywołuje standardowe konsole tekstowe ilekroć zechcemy je ubić. Doświadczony sysadmin uzna coś takiego za bluźnierczą aberrację. Ja z kolei odczuwam pewien wstręt do inita w obecnym kształcie i wcale nie cieszą mnie wynalazki mające godnie go zastępować, typu upstart. Co w takim razie, gdyby zachciało się nam zawołać naszą aplikację z shella, w postaci ‘demonicznej’?
  • Na pewno jest do tego jakieś narzędzie, w końcu to linux. Zatem mamy coś takiego jak nohup albo shellowe disown. Nohup przekierowuje standardowe wyjście do pliku i przeistacza się w wołaną aplikację poprzez exec, jednocześnie maskuje SIGHUP, przez co zamknięcie terminala nie powoduje zamknięcia aplikacji. Disown działa identycznie, poza tym, że jest poleceniem shella i tutaj to shell zostaje skłoniony do tego, żeby nie wysyłał SIGHUP do procesu, na którym wywołano disown. W obu przypadkach, po zamknięciu shella, aplikacja jako sierotka trafia pod skrzydła inita, zachowując ID grupy procesów i ID sesji. Nie jest w związku z tym ani liderem sesji ani liderem grupy. Nie dość, że sierotka, to do tego wyrzutek.
    Tutaj drzewko procesu atop uruchomionego z basha, któremu gnome-terminal przydzielił pseudoterminal pts/0: 

    Co się dzieje w przypadku wywołania atop przez nohup?

    Nic. Poza tym, że nohup zamaskował odbieranie sygnału SIGHUP po czym wywołał za pomocą jednej z funkcji exec() atop, przez co przeistoczył się w atop, przekazując mu podmienione deskryptory stdin i stdout. I teraz atop nadaje bezustannie do nohup.out jako stdout i “przyjmuje” na stdin to co w pliku /dev/null, czyli nic.
    No to teraz siup z terminala (exit). Jak teraz wygląda atop 1 w drzewie?

    I teraz nasz atop 1 wisi sobie pod procesem init. Jak pisałem wcześniej, nie jest liderem sesji i ma przypisaną grupę swojego macierzystego basha, zatem jest to taki bieda-demon, jak kto lubi. Analogicznie rzecz wygląda w przypadku komendy basha disown.
  • screen – mój ulubiony sposób bieda-demonizacji procesów. Screen udaje przed wołanym aplikacjami, że jest nieśmiertelnym terminalem, z którym mogą sobie konwersować do końca świata. Screen powołuje tyle pseudo-terminali, ile użytkownik sobie zażyczy i odpala dla każdego shella. A tak to wygląda na drzewku:

    Jednym zwinnym skrótem klawiszowym można się od screena odpiąć i cieszyć się z hasających w systemie bieda-demonów. Ale zanim się odepniemy, możemy zaobserwować na drzewie procesów naszego terminalowego klienta screena (screen -r) i serwer (SCREEN) z dwoma “oknami” basha.

Demonizacja

Można również napisać aplikację tak, aby demonem po prostu była i to jest najlepsze rozwiązanie, jeśli panuje się nad kodem źródłowym aplikacji.
W najbiedniejszym wydaniu wystarczy fork(), po którym nastąpi błyskawiczny exit() procesu macierzystego. Ale to strasznie nieeleganckie. Niestety w tym wydaniu niewiele się to różni od nohup i disown. Zatem poniżej przedstawię krótką listę kroków, które wypadałoby wykonać, żeby przemienić aplikację w demona w sposób dość przyzwoity.

  1. Fork procesu. Po forku proces macierzysty natychmiast kończy działanie.
    Init natychmiast przygarnia osierocony proces potomny.
  2. Utworzenie własnej sesji i grupy procesów  – setsid(). To nie tylko kwestia estetyki. Po pierwsze do grupy procesów można wysyłać sygnały. Po co nam ewentualne sygnały kierowane do naszego basha? Z drugiej strony może chcielibyśmy forkować procesy potomne w ramach naszej własnej grupy i wpływać na nie za pomocą sygnałów. Warto mieć wtedy własną sesję i grupę, żeby nie wpływać przypadkiem na macierzystą grupę.
  3. Zrobić coś z stdin/stdout. Najlepiej żeby skojarzyć je z /dev/null . Jeśli proces macierzysty (ten który zakończył pracę tuż po forku) posiadał jakieś otwarte pliki, to może wypadałoby je zamknąć.
  4. Być może chcemy, żeby nasz demon był publicznie osiągalnym serwerem TCP, wtedy aby zabezpieczyć się przed złośliwym użytkownikiem, chcielibyśmy, aby demon działał jako użytkownik nieuprzywilejowany. Stąd potrzeba wywołania setuid() .  Ale uwaga – pierwsze 1024 porty TCP może zająć jedynie superużytkownik, stąd warto zabindować sockety zanim odbierze się sobie generalskie dystynkcje.
  5. Być może warto kontrolować liczbę instancji procesu, stąd aby ograniczyć liczbę procesów do np. jednego, przydałby się tzw. lockfile. Niektórzy uważają, że jest to toporny koncept, ja do nich nie należę. Lock na pliku zakłada się za pomocą flock().

A teraz po kolei:

  1. Funkcja fork() może zakończyć się trojako:

    w zależności od wartości zwracanej zmiennej pid

    fork() zakończył się błędem. No cóż, nie powinno się to zdarzyć. No cóż, exit z błędem jest tutaj chyba jedynym wyjściem.

    Po forku jesteśmy nadal w procesie macierzystym. W zmiennej pid zapisany jest PID procesu potomnego, który właśnie został przez system powołany do życia. Zgodnie z planem, proces macierzysty woła exit().

    Po forku obudziliśmy się w procesie potomnym. Proces potomny jest wierną kopią macierzystego. Oba procesy wykonują instrukcje bezpośrednio następujące po fork() i w momencie wyjścia z forka żaden z procesów nie zdaje sobie sprawy, czy jest procesem macierzystym czy potomnym, dopóki nie zbada wartości zmiennej pid. Podsumowując, do forka wchodzi jeden proces, ale następną instrukcję po forku wykonują już dwa procesy jednocześnie. Na drzewie procesów jeden jest rodzicem drugiego.

    A tak to wygląda razem:

    A jak to po kolei wygląda na drzewie procesów (do kodu dodam sleep’y – jeden przed forkiem i drugi, zanim wywołam exit())

    Proces potomny został osierocony (jest teraz podpięty do inita).

  2. Proces potomny zawołał setsid(). Jest teraz liderem sesji (charakterystyczna flaga s w zestawie fflag stanu Ssl w ps), nie ma już przypisanego terminala (? w miejscu nazwy pliku terminala pts/0) i liderem grupy. Świadczy o tym następująca zależność: SID = PID = PGID = 26074. PPID to PID rodzica i wskazuje na init.

    Tutaj mała dygresja – kiedyś, gdy init miał niezawodnie PID = 1, niektórzy programiści skracali sobie dróżkę sprawdzając, czy getppid() == 1, wtedy proces już na starcie w zasadzie był demonem, co pozwalało zrezygnować z forka. Obecnie nie należy robić takiego założenia, jak obrazują powyższe zrzuty z ps’a (init ma pid 1825).

  3. Odziedziczyliśmy otwarte stdin, stdout i stderr. Co z tym fantem zrobić? Proces wprawdzie formalnie odłączył się od terminala (ps pokaże ? w miejscu nazwy terminala), ale pamiętajmy, że stdout to w istocie otwarty do zapisu plik urządzenia terminala /dev/pts/0 . Pisanie do niego (np. printf()) spowoduje niezawodnie wypisanie tego czegoś na terminalu pts/0, czyli pomimo sforkowania, pomimo setsid, demon dalej będzie zaśmiecał swój macierzysty terminal. A nuż chcielibyśmy jeszcze z niego pokorzystać? A nuż jest to terminal na porcie szeregowym i nie możemy go ot tak sobie zamknąć. Wówczas może okazać się wręcz nieużywalny, jeśli nasz demon wpadnie w jakiś debugowy słowotok. Dlatego:

    No dobra, ale nie wystarczyło jedynie zamknąć 0, 1 i 2? Hmm, no nie wiem, a co jeśli gdzieś dalej w trzewiach naszej aplikacji coś/ktoś zrobi open? Jeśli z open’a wyskoczy numer deskryptora odpowiadający stdout a nasza aplikacja namiętnie printfuje, to może być to kłopotliwe.

  4. Mój demon nie jest serwerem, który nasłuchiwałby na jakimś porcie, zatem nie będę się tutaj o tym rozpisywał. To temat na osobny wpis. Jeśli go popełnię, postaram się tutaj zalinkować.
  5. Lockfile w sposób atomowy zapewni nam szeregowanie wywołań naszego demona, dzięki czemu możemy chociażby utrzymywać wyłącznie jedną instancję procesu. W przypadku na przykład serwera sieciowego nasłuchującego na jednym określonym porcie TCP taki zabieg nie jest bezwzględnie konieczny. Rolę takiego lockfile’a mógłby wtedy spełniać socket, któremu nie powiedzie się bind() na już zajętym porcie. Zgodnie z pewną konwencją do lockfile’u można wpisać pid procesu, dzięki czemu można łatwo z poziomu terminala ustalić PID demona. Lockfile (albo pidfile) miałby też inne zalety, spróbuję je kiedyś opisać przy okazji opisywania koncepcji spawnera.

    Powstał swego rodzaju singleton, jeśli proces przejdzie całą tę ścieżkę, wówczas można powiedzieć, że jest jedynym demonem swojego rodzaju w systemie i może w tej chwili spokojnie rozpocząć realizację swoich głównych zadań.

 

Spawner

Jednak programiści popełniają błędy. Czasem program kończy się niespodziewanie. Na przykład taki segmentation fault, czyli spadający jak grom z jasnego nieba sygnał SIGSEGV, a może dzielenie przez 0, tudzież abort() z wewnątrz jakiejś niezbyt stabilnej biblioteki? Wtedy demon przewraca się i znika. Kto miałby nad nim czuwać?
Otóż sam mógłby nad sobą czuwać. Wystarczyłoby, żeby demon jeszcze raz się sforkował i dopiero zanurkował w mroczne i niebezpieczne odmęty kodu realizującego właściwą funkcjonalność. A demon macierzysty czekałby na potomka, aż ten nabroi, żeby spłodzić kolejnego, z nadzieją, że pożyje dłużej niż poprzednik.
Ale o tym w innym wpisie…

Kod źródłowy

Żeby nie było, że wszystko sobie tak od początku sam wymyśliłem – tutaj artykuł, który można uznać za główne źródło.

2 przemyślenia nt. „Demonizacja procesu – linux

  1. Dodam od siebie, że istnieje kilka bardzo wygodnych narzędzi do wspomagania pisania demonów. Od czasu kiedy pisałem kiedyś pewien demon-serwer usług sieciowych, absolutnie zakochałem się w boost-asio (przykład demona z użyciem async socketów: http://www.boost.org/doc/libs/1_47_0/doc/html/boost_asio/example/fork/daemon.cpp) oraz boostowej puli wątków.
    Boost ma pewien próg wejścia, na początku potrafi nieco odpychać – ale warto wytrzymać.

  2. Muszę się kiedyś przeprosić z bioostem, mam wrażenie, że jestem zwyczajnie uprzedzony do tego suite’a. Ale chyba trudniej byłoby mi sie przekonac do C++11, który powoli staje się powszechny.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *