systemd 服务编写实战
把程序做成正经服务:开机自启、崩了自动拉起、日志进 journal。Daemon 编写讲过进程侧(fork/Type 的选择),这篇讲unit 文件怎么写、怎么调。排查命令速查见常用命令与排查工具箱。
unit 文件放哪、长什么样
# /etc/systemd/system/myapp.service ← 自建服务放这(/usr/lib/systemd/system 是包管理器的地盘)
[Unit]
Description=My App Service
After=network-online.target # 排序:网络就绪后再启动(不等于强依赖)
Wants=network-online.target # 弱依赖:跟随启动,失败不拖累我
[Service]
Type=simple # 前台进程直接跑;forking/notify 等见 Daemon 篇
ExecStart=/opt/myapp/myapp --config /etc/myapp/config.toml # 【必须绝对路径】
Restart=on-failure # 异常退出自动拉起;always 连正常退出也拉
RestartSec=3
User=appuser # 别用 root 跑服务
Group=appuser
WorkingDirectory=/opt/myapp
Environment=LOG_LEVEL=info
EnvironmentFile=/etc/myapp/env # KEY=VALUE 格式,一行一个
LimitNOFILE=65536 # fd 上限(网络服务常见需求)
[Install]
WantedBy=multi-user.target # enable 时挂到哪个启动目标下
sudo systemctl daemon-reload # 改完 unit 必须执行,否则 systemd 不知道
sudo systemctl enable --now myapp # 开机自启 + 立即启动
systemctl status myapp # 状态(含主 PID、退出码、最近日志)
高频字段速查
| 字段 | 说明 |
|---|---|
Restart= |
no(默认)/on-failure/always/on-watchdog;配合 RestartSec= 防风暴 |
KillMode= |
control-group(默认,杀整组)/process/mixed |
TimeoutStopSec= |
stop 时的优雅退出宽限,超时 SIGKILL |
ExecStartPre=/Post= |
启动前/后辅助命令(建目录、迁移等,- 前缀忽略失败) |
ExecReload= |
systemctl reload 执行的动作(通常发 SIGHUP 重载配置) |
StandardOutput= |
默认进 journal;可 append:/var/log/x.log |
OOMScoreAdjust=-1000 |
OOM 时优先保命(见排查工具箱) |
Nice= / CPUWeight= / MemoryMax= |
资源约束;MemoryMax=2G 超 OOM |
安全加固:让服务跑在笼子里
[Service]
NoNewPrivileges=true # 禁止提权(setuid 等)
ProtectSystem=strict # 整个文件系统只读
ProtectHome=true # 隐藏 /home /root /run/user
PrivateTmp=true # 独立的 /tmp
ReadWritePaths=/var/lib/myapp # 只放开真正要写的目录
ProtectKernelTunables=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 # 只允许这些 socket 类型
DynamicUser=yes # 没有账号也能跑:临时分配 UID(配 StateDirectory= 持久化数据)
一条 systemd-analyze security myapp 能给当前配置打安全分(0~10),按提示逐项补。新服务默认就该带这些——暴露面小了,被打了也不至于直接 rooted。
日志:journalctl 配合
journalctl -u myapp -f # 跟踪
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp -p err # 只看错误级
journalctl -u myapp -o json-pretty # 结构化输出(含 _PID 等)
服务里 printf/std::cout 到 stdout/stderr 就自动进 journal——daemon 不用自己再写日志文件(Type=simple 前台跑时),除非要轮转策略。
timer:替代 cron 的官方姿势
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily
[Timer]
OnCalendar=*-*-* 03:00:00 # cron 表达式的 systemd 写法
Persistent=true # 错过的(关机期间)开机后补跑
RandomizedDelaySec=300 # 防止整点雪崩
[Install]
WantedBy=timers.target
# timer 需要“挂”在一个同名 .service 上(backup.service 定义要跑的命令)
sudo systemctl enable --now backup.timer
systemctl list-timers # 看所有定时任务与下次触发时间
比 cron 好在:依赖其他服务、失败自动记录、Persistent 补跑、日志齐全。cron 只剩一个优势——写法熟悉。
调试与常见坑
systemd-analyze verify /etc/systemd/system/myapp.service # 语法静态检查(不启动)
systemd-analyze blame # 上次启动各服务耗时
systemd-analyze critical-chain myapp # 启动依赖链分析
| 症状 | 原因 |
|---|---|
| 改了 unit 不生效 | 忘 daemon-reload |
Executable path is not absolute |
ExecStart 用了相对路径或没写全 |
| 服务反复重启 | Restart 风暴;看 status 里的退出码(如 203=EXEC 找不到程序,126/127=权限/缺库) |
| 程序找不到动态库 | 用户环境变量不生效——systemd 不经过你的 shell,库路径写进 unit 的 Environment= |
| 后台静默失败 | 标准输出没看:journalctl -u xxx -n 100 是第一现场 |
| 依赖的服务没起来 | After= 只是顺序,要"没它我不跑"用 Requires=;"没它最好有"用 Wants= |
延伸:程序侧怎么配合(优雅退出、sd_notify)见Daemon 编写;服务打不打了看排查工具箱;本篇 unit 示例在 WSL Ubuntu 22.04 用 systemd-analyze verify 验证过语法。