systemd-timer 能替代 crontab,但需手动处理环境变量、权限、路径、shell 特性等隐式依赖,否则易静默失败;必须配对 timer/service 单元文件,显式声明 path、workingdirectory 等,并用 journalctl 查日志。

systemd-timer 能不能直接替代 crontab
能,但不是“一换就跑”,尤其涉及环境变量、用户权限、路径解析时容易静默失败。systemd-timer 不读 /etc/crontab,也不自动加载用户的 crontab -e 条目,它只认自己那一套 unit 文件。
常见错误现象:systemctl start myjob.timer 显示 active,但 myjob.service 从不执行;或执行了但日志里报 Command not found——多半是 PATH 没继承、shell 特性不兼容、或工作目录不对。
- crontab 默认用
/bin/sh解析命令,而 systemd service 默认不走 shell,ExecStart=写echo $HOME | mail -s "hi" user会失败(变量不展开、管道不生效) - 用户级 timer 必须放在
~/.config/systemd/user/,且需先运行systemctl --user daemon-reload,否则systemctl --user list-timers看不见 - root 级 timer 的
EnvironmentFile=或Environment=必须显式声明,不会自动加载/etc/environment或用户的~/.bashrc
写一个最简可用的 daily backup.timer
核心是两个文件配对:timer + service。timer 只管“什么时候触发”,service 管“具体干什么”。两者必须同名(不含后缀),且 service 类型必须是 Type=oneshot。
示例:每天凌晨 2:15 执行备份脚本 /usr/local/bin/backup.sh
[Unit] Description=Daily backup <p>[Timer] OnCalendar=<em>-</em>-* 02:15:00 Persistent=true</p><p>[Install] WantedBy=timers.target</p>
对应 backup.service:
[Unit] Description=Run backup script <p>[Service] Type=oneshot ExecStart=/usr/local/bin/backup.sh</p><h1>关键:显式指定环境和工作目录</h1><p>Environment="PATH=/usr/local/bin:/usr/bin:/bin" WorkingDirectory=/root</p><h1>防止因脚本 exit code 非 0 导致 timer 停摆(默认行为)</h1><p>RemainAfterExit=yes</p>
-
Persistent=true表示:如果机器关机错过触发时间,开机后立刻补执行一次(类似 cron 的 @reboot + 时间补偿) -
OnCalendar支持多种格式,但避免用hourly这类模糊词——它实际对应*-*-* *:00:00,可能和你预期的“每小时整点”有偏差 - 不要在
ExecStart=里写sh -c "xxx"来兼容 bash 特性,应把逻辑移到脚本里,保持 service 定义干净
systemd-timer 日志怎么看、怎么调
出问题时别翻 /var/log/syslog,直接用 journal:
- 查 timer 状态:
systemctl list-timers --all(注意看LEFT和UNIT列) - 查某次执行是否成功:
journalctl -u backup.service --since "2 hours ago" - 查 timer 本身有没有报错(比如语法错):
journalctl -u backup.timer - 如果 service 启动就退出,加
StandardOutput=journal+console和StandardError=journal+console把输出也刷进 journal
注意:journalctl 默认只存当前 boot 的日志。如需跨重启查,确保 /var/log/journal/ 目录存在且 Storage=persistent 在 /etc/systemd/journald.conf 中已启用。
crontab 里带 % 的行怎么转成 systemd
crontab 的 % 表示换行并把后续内容作为标准输入传给命令,systemd 没这机制。必须拆解:
原 crontab 行:0 3 * * * echo "report" | mail -s "daily" admin@example.com
错误写法:ExecStart=/bin/sh -c 'echo "report" | mail -s "daily" admin@example.com'(mail 可能收不到 stdin)
正确做法是写个 wrapper 脚本,或用 ExecStartPre + 临时文件:
[Service] Type=oneshot ExecStartPre=/bin/sh -c 'echo "report" > /tmp/mail-body.txt' ExecStart=/usr/bin/mail -s "daily" admin@example.com
- 别依赖
/tmp持久性,ExecStartPost必须清理,否则下次执行会读到旧内容 - 更稳妥的方式是把整个逻辑封装进独立脚本,service 只调用它——这样调试、权限、环境都可控
- systemd 对重定向(
、<code>>>)支持有限,复杂 IO 流程一律交给脚本处理
真正麻烦的从来不是语法转换,而是 cron 里那些隐式依赖:当前 shell、用户 HOME、ssh-agent、dbus session、甚至某个挂载点是否就绪。这些全得手动补全,漏一条,timer 就哑火。










