Shell-Script

如何讓 systemd 正確管理帶有後台程序的腳本?

  • April 23, 2019

我有一個 shell 腳本,它在後台執行三個程序,在前台執行幾個程序,然後是trapand wait,並且我已經設置了一個單元文件,因此systemd可以啟動它並在它失敗時重新啟動它。

然而,我發現,如果一個程序死了,它不會殺死該腳本中的所有內容並重新啟動它。對於此應用程序,如果其中任何一個死亡,則必須重新啟動它們。

我看到了兩條合理的路徑:

  1. 配置單元文件並可能更改腳本,以便它檢測到異常並殺死它們,重新執行腳本。我不知道該怎麼做。
  2. 將三個後台程序中的每一個配置為具有單獨.service文件的獨立單元。但是我不知道如何編寫.service文件以在其中任何一個失敗時殺死並重新啟動它們。我知道我可以安排他們的依賴關係,以便他們按順序開始,但我不知道如何讓它在 #2 死亡時殺死 #1,反之亦然。

我不想寫一個經理,或者讓程序自己解決問題並自己死去——這就是systemd目的——我希望我只是錯過了正確的咒語。

.服務文件:

[Unit]
Description=Foobar Interface
After=network.target

[Service]
Type=simple
WorkingDirectory=/home/user/scripts
ExecStart=/home/user/scripts/myscript.sh
Restart=always

[Install]
WantedBy=multi-user.target

bash 腳本:

#!/usr/bin/env bash

tty_port=/dev/ttyUSB0

#Clean up any old running processes
pkill -f "cat ${tty_port}"
pkill transport
pkill backgroundprogram

#Configure the target
source /home/user/somescript.sh
foregroundprogram

#Set up the serial port
stty -F $tty_port 115200 

#Read from the port in the background
cat $tty_port &
tty_pid=$!

#Wait for tty device to waken
sleep 15

#Send commands to tty device
echo "command1" > $tty_port
sleep 1
echo "command2" > $tty_port
sleep 1

#Start up the transport
/home/user/transport &>> /dev/null &
transport_pid=$!

#Wait a bit for the transport to start
sleep 1

#Start up the main process
/home/user/backgroundprogram &
background_pid=$!

#Wait a bit for it to start
sleep 1

#Finally, start the tty device
echo "command3" > $tty_port

trap "kill ${background_pid} ${tty_pid} ${transport_pid}; exit 1" INT
wait

一切正常,寫入日誌等,但是當三個程序中的任何一個失敗時,它會繼續執行,不會殺死並重新啟動一切。

然而,我發現,如果一個程序死了,它不會殺死該腳本中的所有內容並重新啟動它。對於此應用程序,如果其中任何一個死亡,則必須重新啟動它們。

systemd 正在監視您的 shell 腳本,而不是它的子腳本。您不希望systemd 響應孩子的退出,因為這會導致您每次執行 command 時都重新啟動。考慮一下,如果我有一個執行的 shell 腳本……

date

我只是產生了一個子程序,它執行,然後退出。我不希望這觸發我的流程主管的任何行動。

如果您希望 systemd 監控子程序,則為每個程序創建一個單獨的單元文件:

  1. 一個用於配置和從串口讀取的單元
  2. 一為/home/user/transport
  3. 一為/home/user/backgroundprogram

您可以使用 systemd 依賴項來確保服務的正確啟動順序(並確保如果您停止一個服務,它們都會停止),並且您可以使用EnvironmentFile指令$tty_port從文件中載入配置(如 )。

您可能會將一些設置命令(“向 tty 設備發送命令…”)放在ExecStartPre一行中,或者他們可能會獲得自己的Type=oneshot服務。

如果您可以將主腳本拆分為單獨的服務,則可以像這樣輕鬆解決它:

在下面的範例中,我擁有三個重生服務 s1、s2 和 s3,並通過目標 s.target 將它們作為一個組進行控制。

注意:

如果您Requires在 s.target 中配置這三個服務,那麼如果其中一個崩潰並重新生成,則該組中的所有參與程序都將重新啟動。

或者,如果您將它們配置為Wants在 s.target 中,那麼如果其中一個崩潰並重新生成,則僅重新啟動該單個程序。

為每個服務創建一個服務文件 s1, s2, s3:

/etc/systemd/system/s1.service :

[Unit]
Description=my worker s1
After=network.target
Before=foobar.service
PartOf=s.target

[Service]
Type=simple
ExecStart=/usr/local/bin/s1.sh
Restart=always

(注意:如果您的服務相同,您可以創建一個s1@.service 文件而不是多個文件。在手冊中查找使用 @ 和 %i 的服務實例。)

現在創建需要 s1、s2 和 s3 服務的主要目標(組)文件:

/etc/systemd/system/s.target :

[Unit]
Description=main s service
Requires=s1.service s2.service s3.service
# or
# Wants=s1.service s2.service s3.service

[Install]
WantedBy=multi-user.target

完畢。

像往常一樣,您現在必須執行systemctl daemon-reload.

現在您可以使用systemctl start s.target

s1 啟動您的服務,s2 和 s3 已啟動。

您可以停止所有三個服務,同時systemctl stop s.target

停止 s1、s2 和 s3。

當然,您可以像往常一樣啟動/停止/重新啟動/狀態各個服務:

systemctl status s1

如果您殺死 s1、s2 或 s3 程序,它將自動重生(Restart=always)。

如果使用Requires,則組中的所有程序都將重新啟動。

PS:systemctl enable s.target如果要在啟動時啟動服務,請執行。

PS:不幸的是,在使用 systemctl 時,您不能像使用“s1”那樣使用速記詞“s”來表示“s.target”,而不是輸入完整的“s1.service”。當你想管理組時,你必須輸入“s.target”。

引用自:https://unix.stackexchange.com/questions/499539