Chapter 09 · 树莓派与嵌入式 Linux

systemd 服务与开机自启

脚本能跑只是第一步。真正的嵌入式设备要求:断电重启后程序自动拉起、崩溃了自动重来、日志有据可查。systemd 就是让树莓派"永不掉线"的守护者。

课程进度75%

关键概念

systemd 与 init 系统
Linux 开机后第一个启动的进程(PID 1)叫 init 系统,负责拉起所有其他服务、管理它们的生命周期。现代发行版(含 Raspberry Pi OS)用 systemd。它掌管开机流程、服务启停、依赖顺序、日志收集。让程序开机自启、常驻后台,就是把它交给 systemd 管理。
Unit(单元)与 .service 文件
systemd 把它管理的一切抽象成 Unit(单元),服务是最常见的一种(.service 文件)。一个单元文件用 INI 格式描述:程序在哪、怎么启动、何时启动、崩溃怎么办。写好放到 /etc/systemd/system/,systemd 就接管它。
systemctl 命令
控制 systemd 的主命令。start/stop/restart 启停服务,enable/disable 设置/取消开机自启,status 查看运行状态,daemon-reload 在改了单元文件后重新加载。管理服务的日常全靠它。
Restart 策略(崩溃自愈)
单元文件里 Restart=always 让服务崩溃或退出后自动重启,RestartSec=5 设重启间隔。这是嵌入式设备可靠性的关键——程序偶发 bug 崩了也能自己爬起来,无需人工干预。配合 StartLimitBurst 防止无限崩溃重启。
journalctl 与 journal 日志
systemd 自带的日志系统。服务的标准输出/错误自动被 journal 收集,用 journalctl 查看,支持按服务、时间、优先级过滤,-f 实时跟踪。不用自己写日志文件——print 出来的东西都能在 journal 里查到,排障利器。
Target(运行目标)与依赖
Target 是一组单元的集合,代表系统状态(如 multi-user.target 命令行多用户、network-online.target 网络就绪)。单元用 After=/Wants= 声明依赖顺序——比如联网的服务要 After=network-online.target,确保网络起来后才启动。
cron 定时任务
Unix 经典的定时任务工具,按时间表周期性执行命令。crontab -e 编辑任务,五个字段(分 时 日 月 周)定义频率。适合简单周期任务:每天备份、每小时抓拍、每分钟上报。语法紧凑但可读性一般。
systemd timer(定时器)
systemd 提供的定时任务机制,是 cron 的现代替代。用 .timer 单元触发 .service,优点是:日志进 journal、支持"错过补运行"(Persistent)、依赖管理、精确到秒。复杂/需可靠性的定时任务用它,简单的 cron 也够。

为什么需要 systemd 托管

直接 SSH 里 python3 app.py 跑,一断开连接程序就死了,重启更是无影无踪。对比看清 systemd 的价值:

裸跑脚本 vs systemd 托管 ═══════════════════════════════════════════════════════════════ ❌ SSH 里直接跑 python3 app.py ┌─────────────────────────────────────────┐ │ SSH 断开 → 进程被杀,程序停止 │ │ 程序崩溃 → 没人管,永久死亡 │ │ 树莓派重启 → 不会自动跑 │ │ 日志 → 刷屏即失,无处可查 │ └─────────────────────────────────────────┘ ✓ systemd 托管为服务 ┌─────────────────────────────────────────┐ │ SSH 断开 → 照常运行(后台守护) │ │ 程序崩溃 → Restart=always 自动拉起 │ │ 树莓派重启 → enable 后开机自启 │ │ 日志 → journalctl 随时可查 │ └─────────────────────────────────────────┘ 嵌入式设备 = 无人值守,systemd 托管是标配

编写第一个 service 单元

假设有个传感器上报脚本 /home/pi/project/sensor.py,用虚拟环境跑。创建单元文件:

# 创建服务单元文件(需 sudo)
sudo nano /etc/systemd/system/sensor.service
# /etc/systemd/system/sensor.service
[Unit]
Description=传感器数据采集服务
After=network-online.target       # 等网络就绪后启动
Wants=network-online.target

[Service]
Type=simple                        # 前台进程,最常用
User=pi                            # 以 pi 用户运行(不用 root)
WorkingDirectory=/home/pi/project
# 用虚拟环境里的 python(绝对路径)
ExecStart=/home/pi/project/env/bin/python /home/pi/project/sensor.py
Restart=always                     # 崩溃/退出后自动重启
RestartSec=5                       # 重启前等 5 秒

[Install]
WantedBy=multi-user.target         # 开机(多用户模式)时启动

三段结构:[Unit] 描述与依赖、[Service] 怎么运行、[Install] 开机自启目标。保存后启用:

# 改了单元文件后必须 reload
sudo systemctl daemon-reload

# 立即启动服务
sudo systemctl start sensor

# 设为开机自启(关键!否则重启后不跑)
sudo systemctl enable sensor

# 查看运行状态(绿色 active (running) 表示正常)
sudo systemctl status sensor

# 一步到位:启用并立即启动
sudo systemctl enable --now sensor
ExecStart 必须用绝对路径

systemd 启动服务时环境极简,没有你 shell 里的 PATH。ExecStart 里的 python、脚本、参数都要写绝对路径(如 /home/pi/project/env/bin/python,不能只写 python3)。用虚拟环境时更要指向 env 里的解释器。写相对路径或裸命令是新手最常见的启动失败原因。

日常管理与查看日志

服务跑起来后,用 systemctl 控制、journalctl 看日志:

# === 服务控制 ===
sudo systemctl restart sensor    # 重启服务
sudo systemctl stop sensor       # 停止
sudo systemctl disable sensor    # 取消开机自启
systemctl is-active sensor       # 查是否在运行
systemctl is-enabled sensor      # 查是否开机自启

# === 查看日志(journalctl)===
# 看某服务的全部日志
journalctl -u sensor

# 实时跟踪(最常用,像 tail -f)
journalctl -u sensor -f

# 只看最近 50 行
journalctl -u sensor -n 50

# 看今天的、按时间过滤
journalctl -u sensor --since today
journalctl -u sensor --since "2026-07-07 08:00"

# 只看错误级别
journalctl -u sensor -p err
让 print 立即进日志:关闭缓冲

Python 的 print 默认有缓冲,日志可能延迟出现在 journal 里。在单元文件 [Service] 段加 Environment=PYTHONUNBUFFERED=1,或运行时用 python -u,强制无缓冲输出,日志实时可见。这对调试常驻服务非常重要。

限制资源与防崩溃风暴

树莓派资源有限,给服务加资源上限和重启限流,防止一个坏服务拖垮整机:

# 在 [Service] 段追加这些指令:

# 内存上限 200M,超了会被 OOM 杀掉(保护系统)
MemoryMax=200M

# CPU 最多用 50%
CPUQuota=50%

# 防崩溃风暴:10 秒内崩溃超过 5 次就停止重试
[Unit]
StartLimitIntervalSec=10
StartLimitBurst=5

定时任务:cron vs systemd timer

周期性任务(每天备份、每小时抓拍)有两种做法。先看简单的 cron:

# 编辑当前用户的 crontab
crontab -e

# 五字段格式:分 时 日 月 周  命令
# ┌─分(0-59)  ┌─时(0-23)  ┌─日  ┌─月  ┌─周(0-7)

# 每天凌晨 3 点执行备份脚本
0 3 * * * /home/pi/backup.sh

# 每 10 分钟抓拍一张
*/10 * * * * /home/pi/env/bin/python /home/pi/snap.py

# 每小时整点记录温度到日志
0 * * * * /usr/bin/vcgencmd measure_temp >> /home/pi/temp.log

# 开机时执行一次(@reboot)
@reboot /home/pi/startup.sh

需要日志、可靠性、错过补运行时,用 systemd timer。它由一对 .service + .timer 组成:

# /etc/systemd/system/backup.service(要执行的任务,Type=oneshot)
[Unit]
Description=每日数据备份

[Service]
Type=oneshot
User=pi
ExecStart=/home/pi/backup.sh
# /etc/systemd/system/backup.timer(何时触发)
[Unit]
Description=每天 3 点触发备份

[Timer]
OnCalendar=*-*-* 03:00:00          # 每天 3 点
Persistent=true                     # 关机错过了,开机补跑

[Install]
WantedBy=timers.target
# 启用定时器(注意是 enable .timer 不是 .service)
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer

# 查看所有定时器和下次触发时间
systemctl list-timers
对比项cronsystemd timer
配置复杂度低(一行)高(两个文件)
日志需自己重定向自动进 journal
错过补运行不支持Persistent=true 支持
依赖管理可依赖其他单元
适用简单周期任务需可靠性/日志/依赖
本章小结

嵌入式设备无人值守,程序必须常驻、自愈、开机自启——这是 systemd 的活。写一个 .service 单元([Unit] 依赖 + [Service] 运行方式 + [Install] 自启目标),放到 /etc/systemd/system/,关键项:ExecStart绝对路径(虚拟环境指向 env/bin/python)、Restart=always 崩溃自愈、User=pi 非 root 运行、After=network-online.target 等网络。用 systemctl daemon-reload 加载、enable --now 启用并启动、status 看状态。日志全进 journal,journalctl -u 服务名 -f 实时跟踪(记得设 PYTHONUNBUFFERED=1)。资源上限用 MemoryMax/CPUQuota 保护整机。定时任务简单用 croncrontab -e),要可靠性/日志/补运行用 systemd timer(.timer + .service)。下一章我们用 Docker 容器化部署,一键跑起 Home Assistant 等应用。