Learning Objectives
- 寫出 systemd 的 unit 檔,讓 Python 程式開機自動執行、意外結束時自動重新啟動
- 用 systemctl 與 journalctl 啟用、查看狀態與讀取程式的輸出
- 把程式改寫成適合長時間執行的服務:處理感測器錯誤、輸出立刻送出
- 讀懂並寫出 cron 的五個時間欄位,安排定時執行的工作
- 依需求選擇 systemd 服務或 cron
拔掉螢幕之後,程式誰來啟動
第 14 課的記錄程式、第 15 課的網頁、第 16 課的 MQTT 程式,到目前為止都是你連上終端機、手動執行的。 真的放到教室裡的樹莓派沒有人顧,會遇到:
- 停電又復電:開機之後,程式要自己跑起來。
- 程式意外結束:感測器鬆脫、網路斷線、程式有 bug,要能自動重新啟動。
- 定時工作:每天凌晨把資料匯出備份、每 10 分鐘拍一次快照。
Linux 有兩個常用的工具:
| 工具 | 適合 |
|---|---|
| systemd 服務 | 一直執行的程式(記錄程式、網頁伺服器、MQTT 用戶端),開機啟動、當掉重啟、統一看日誌 |
| cron | 在固定時間執行一次就結束的工作(每天備份、每小時統計) |
Raspberry Pi OS 和 Ubuntu 一樣用 systemd 管理開機流程(「Linux 系統管理」軌也會用到 systemctl)。
systemd 的 unit 檔
要讓 systemd 管理你的程式,寫一個 unit 檔放在 /etc/systemd/system/,檔名就是服務的名稱,例如 envmon.service:
[Unit]
Description=Classroom environment logger
After=network-online.target
Wants=network-online.target
[Service]
User=student
WorkingDirectory=/home/student/envmon
ExecStart=/home/student/envmon/.venv/bin/python /home/student/envmon/logger.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
| 段落與設定 | 意思 |
|---|---|
Description |
說明文字,systemctl status 會顯示 |
After/Wants=network-online.target |
等網路連上之後再啟動(程式要連 MQTT broker 時需要) |
User |
用哪個使用者的身分執行;不寫就是 root,權限太大 |
WorkingDirectory |
工作目錄:程式裡寫相對路徑的 log.csv 會存在這裡 |
ExecStart |
要執行的指令。用絕對路徑;用了第 04 課的虛擬環境,就寫虛擬環境裡 python 的完整路徑 |
Restart=on-failure |
程式以非 0 的結束碼結束、被訊號終止等異常情況時,自動重新啟動 |
RestartSec=5 |
重啟前等 5 秒(預設只等 0.1 秒),避免一直失敗時瘋狂重啟 |
WantedBy=multi-user.target |
enable 之後,開機進到一般的多使用者模式時就啟動 |
範例裡的使用者是 student、資料夾是 /home/student/envmon,換成你自己的。
把記錄程式變成服務
在樹莓派的終端機操作。輸出經過節錄,日期與程序編號(PID)每台機器都不一樣。
-
1
先確認程式手動執行沒問題
服務只是「替你執行同一個指令」。手動執行就出錯的程式,交給 systemd 也一樣會出錯。看到輸出後按 Ctrl+C 停止。
-
2
建立 unit 檔
/etc/systemd/system/只有管理員能寫,所以用sudo。在 nano 裡貼上上一段的內容,Ctrl+O 存檔、Ctrl+X 離開,再用cat確認。 -
3
讓 systemd 讀新的設定,啟用並立刻啟動
新增或修改 unit 檔之後都要
daemon-reload。enable設定開機自動啟動,加上--now同時立刻啟動。 -
4
看服務的狀態
Loaded那一行的enabled表示開機會啟動;Active: active (running)表示正在執行。 -
5
讀程式的輸出(日誌)
服務的
print輸出會進到 systemd 的日誌。-u指定服務,-f持續顯示新的內容(Ctrl+C 離開)。 -
6
試試看:程式被強制結束,會自己回來
用
kill -9強制結束程式(模擬當機)。等 5 秒(RestartSec)再看狀態,PID 換了一個新的號碼,表示 systemd 已經重新啟動它。 -
7
其他常用的指令
指令 作用 sudo systemctl restart envmon.service改了程式之後重新啟動 sudo systemctl stop envmon.service停止(開機時仍會啟動) sudo systemctl disable envmon.service取消開機啟動
適合當服務的程式
交給 systemd 的程式沒有人盯著看,寫法要注意兩件事:
- 輸出要立刻送出:Python 發現輸出不是終端機時,會先把
print的內容存在緩衝區,累積一段才一次寫出, 日誌裡就會很久看不到新的內容。print(..., flush=True)(或在 unit 檔加Environment=PYTHONUNBUFFERED=1)讓每一行立刻送出。 - 能處理的錯誤自己處理:感測器鬆脫(smbus2 會丟出 OSError)、接觸不良偶爾讀到離譜的數值,不應該讓整個記錄程式停掉。
用
try/except接住、記下錯誤、略過這一筆,繼續下一輪。Restart=on-failure是最後一道防線,不是第一道。
下一個範例每秒讀一次溫度、寫進 env.csv。第 2 次讀取時感測器接觸不良,讀到 118.6°C: 程式檢查讀值合不合理,不合理就丟出 ValueError,主迴圈接住它、略過這一筆。另外加一顆心跳燈,每跑一輪就切換一次, 站在教室裡看一眼就知道程式還活著。
逐步看:讀值異常,程式繼續跑
溫度感測器在 1 秒左右出現一次異常的讀值。看第 12 行丟出的例外怎麼一路傳回第 19 行的呼叫處、被第 23 行接住,以及心跳燈每一輪都換一次狀態。
cron:在固定的時間執行
cron 是 Linux 的排程服務,依照每個使用者的 crontab(排程表)在指定的時間執行指令。 每一行是五個時間欄位加上要執行的指令:
┌───── 分(0~59)
│ ┌───── 時(0~23)
│ │ ┌───── 日(1~31)
│ │ │ ┌───── 月(1~12)
│ │ │ │ ┌───── 星期(0~7,0 與 7 都是星期日)
│ │ │ │ │
*/10 * * * * 指令
| 寫法 | 意思 | 例子 |
|---|---|---|
* |
每一個值 | 時欄寫 *:每個小時 |
a-b |
範圍 | 8-17:8 點到 17 點 |
a,b |
列舉 | 0,30:0 分與 30 分 |
*/n |
每隔 n | */15:0、15、30、45 分 |
例如 30 7 * * 1-5 是「星期一到星期五的 7:30」,0 2 * * * 是「每天凌晨 2:00」。
另外 @reboot 可以代替五個欄位,表示 cron 啟動時(開機)執行一次。
cron 用的是系統的時區;樹莓派設成台灣時間,0 2 * * * 就是台灣的凌晨兩點。
下一個範例寫一個小函式,把一個欄位展開成實際的數字,看 cron 是怎麼理解這些符號的。
逐步看:展開 cron 欄位
expand 先用逗號切開,每一段再判斷有沒有斜線(間隔)與連字號(範圍)。看 */15 怎麼變成 0、15、30、45。
用 crontab 排定時工作
在樹莓派的終端機操作,排兩個工作:每 10 分鐘拍一次環境快照、每天凌晨 2 點匯出 CSV。輸出經過節錄。
-
1
編輯自己的排程表
第一次執行
crontab -e會請你選編輯器(選 nano 最簡單)。在檔案最後加上兩行,存檔離開後就生效,不用重新開機。 -
2
列出排程表確認
兩個重點:
- 全部用絕對路徑。cron 執行時的環境很陽春,
PATH只有/usr/bin:/bin,也不會進入你的虛擬環境;寫python或相對路徑常常找不到。 - 把輸出導向記錄檔:
>> 檔案 2>&1把一般輸出與錯誤訊息都附加到 cron.log,出問題時才查得到原因。
- 全部用絕對路徑。cron 執行時的環境很陽春,
-
3
過一段時間檢查記錄檔
沒有出現預期的輸出時,先看記錄檔裡的錯誤訊息,再手動執行同一行指令比對。
systemd 還是 cron?
| 需求 | 選擇 |
|---|---|
| 程式一直執行(迴圈、網頁伺服器、MQTT 訂閱) | systemd 服務 |
| 當掉要自動重新啟動 | systemd 服務(Restart=on-failure) |
| 在固定時間做一件事就結束 | cron |
| 開機時執行一次的小工作 | cron 的 @reboot 也可以;但要等網路或其他服務就緒時,用 systemd 比較好控制順序 |
不要用 cron 每分鐘「檢查程式還在不在、不在就啟動」:那正是 systemd 已經幫你做好的事。 systemd 也有計時器(timer)可以取代 cron,功能更多,之後有需要再學。
小測驗:systemd
新增 /etc/systemd/system/envmon.service 之後,下面哪一組指令能讓它「現在就啟動,而且以後開機也自動啟動」?
服務設定 Restart=on-failure、RestartSec=5,程式因為未處理的例外結束(結束碼不是 0),systemd 會怎麼做?
服務正常執行,但 journalctl -u envmon.service 很久都看不到 print 的新內容,最可能的原因是?
小測驗:cron
crontab 裡的 30 7 * * 1-5 指令 什麼時候執行?
用本課的 expand 函式,下面的程式會印出什麼?
def expand(field, low, high):
values = []
for part in field.split(","):
step = 1
if "/" in part:
part, step = part.split("/")
step = int(step)
if part == "*":
start, end = low, high
elif "-" in part:
start, end = [int(x) for x in part.split("-")]
else:
start = end = int(part)
values += list(range(start, end + 1, step))
return values
print(expand("0-20/10,45", 0, 59))
crontab 寫 */10 * * * * python logger.py,手動執行沒問題,cron 卻一直沒跑出結果。最該先改什麼?
重點整理
- 一直執行的程式交給 systemd:unit 檔放在
/etc/systemd/system/,寫好ExecStart(絕對路徑)、User、Restart=on-failure、WantedBy=multi-user.target。 daemon-reload→enable --now→status;輸出用journalctl -u 服務名 -f看。- 服務程式要
print(..., flush=True),能處理的錯誤用 try/except 自己處理,重啟是最後一道防線。 - 定時工作交給 cron:五個欄位是分、時、日、月、星期,
*、a-b、a,b、*/n;@reboot開機執行一次。 - cron 裡全部用絕對路徑,並把輸出導向記錄檔。
下一課把前面學過的感測、顯示、記錄、網頁與自動執行整合成一個教室環境監測站。