Blog:
Systemd 定时器应用

Thursday, February 13, 2020

定时器任务在 Linux 系统中是一项很普遍的应用,例如定期清理文件,在嵌入式系统则可以定时调用任务采集数据等。Cron 是一个广为人知的定时计划任务管理器,但随着systemd 兴起,systemd timers 也逐渐开始取代传统的 cron。本文就将基于Toradex Linux BSP在Colibri iMX8X 上如使用 systemd 定时器。

Systemd 定时器从触发时间上可以分为两种:

  • 单调定时器:从一个特定的时间开始后过一段时间触发,通常可以是系统启动或者systemd 服务单元执行的开始时间
  • 实时定时器:在某个特定时间触发,一般指时钟时间,这个就类似于cron定时任务

两种定时满足不同要求的任务,例如需要在某个明确并固定的时间来执行任务,如每周日晚上12点清理日志文件,则可以选择实时定时器。而对于有些无法实现预测执行时间,通常是要在系统启动后才定期执行,如某个任务在启动后每隔10分钟采样传感器数据,由于不确定系统启动时间,所有可以采用单调定时器。或者同时使用两个定时器功能。

Systemd 定时器的配置文件可以分为两部分,定时器单元以 .timer 后缀的systemd单元文件,以及服务单元以 .service 后缀的systemd单元文件。每个 .timer 文件通常对应一个同名的 .service 文件。定时器单元 .timer 文件中的 [Timer] 定义了该定时器何时以及如何触发。该定时器被触发后,执行对应的服务单元 .service,其中的 [Service] 定义了最终被执行的脚本或者应用。下面我们将演示如何使用。

单调定时器

我们将设置一个系统启动 2 分钟后启动并在此之后每隔 15 分钟定期执行的定时任务。下面是所需的定时器单元 test1.timer 和服务单元 test1.service。

test1.timer

[Unit]
Description=Run every 15min and on boot

[Timer]
OnBootSec=2min
OnUnitActiveSec=15min

[Install]
WantedBy=timers.target

test1.service

[Unit]
Description=Hello World
[Service]
ExecStart=/home/root/HelloWorld

在 .timer 中 OnBootSec= 设置了该定时器需要在系统启动 2 分钟后被执行,OnUnitActiveSec= 设置了该定时器在成功执行后,每隔 15 分钟需要再次被执行。服务单元 test1.service 中 [Service] 定义了 /home/root/HelloWorld 是需要被执行的应用。

把上面两个文件复制到 /etc/system/systemd 目录下,systemd-analyze 命令来核对文件是否有效。

root@colibri-imx8x:/etc/systemd/system# systemd-analyze verify test1*

启用 test1 定时任务,并在每次启动后都生效。设置完成后马上重启,查看该任务。

root@colibri-imx8x:/etc/systemd/system# systemctl start test1.timer
root@colibri-imx8x:/etc/systemd/system# systemctl enable test1.timer
root@colibri-imx8x:/etc/systemd/system# sync
root@colibri-imx8x:/etc/systemd/system# reboot

命令 systemctl list-timers 可以列出目前激活的定时任务。

root@colibri-imx8x:~# systemctl list-timers
NEXT                         LEFT          LAST PASSED UNIT                         ACTIVATES
Tue 2020-02-11 16:39:19 CST  1min 30s left n/a  n/a    test1.timer                  test1.service
Tue 2020-02-11 16:52:19 CST  14min left    n/a  n/a    systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service

2 timers listed.
Pass --all to see loaded but inactive timers, too.

可以看到在系统启动后,还剩下1分30秒执行test1.service,这个是由 OnBootSec=2min 参数所设置的。

在等待数分钟再次查看定时器任务,test1.service 在14分钟后会被执行,这是由 OnUnitActiveSec=15min 参数所设置。

root@colibri-imx8x:~# systemctl list-timers
NEXT                         LEFT       LAST                         PASSED  UNIT                         ACTIVATES
Tue 2020-02-11 16:52:19 CST  12min left n/a                          n/a     systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Tue 2020-02-11 16:54:28 CST  14min left Tue 2020-02-11 16:39:28 CST  26s ago test1.timer                  test1.service

从系统日志里也可以观察到自16:37:25 系统启动后,在 16:39:28 时 HelloWorld 被执行。

root@colibri-imx8x:~# journalctl -u test1.service
-- Logs begin at Tue 2020-02-11 16:37:25 CST, end at Tue 2020-02-11 16:40:49 CST. --
Feb 11 16:39:28 colibri-imx8x systemd[1]: Started Hello World.
Feb 11 16:39:28 colibri-imx8x HelloWorld[3982]: Hello world!

再次等待 15 分钟以后,重新查看日志和定时器任务,在 16:54:30 时HelloWorld 被执行,并且在 13 分钟后会再次执行。这符合定时器任务 test1 的设置预期。

root@colibri-imx8x:~# journalctl -u test1.service
-- Logs begin at Tue 2020-02-11 16:37:25 CST, end at Tue 2020-02-11 16:56:21 CST. --
Feb 11 16:39:28 colibri-imx8x systemd[1]: Started Hello World.
Feb 11 16:39:28 colibri-imx8x HelloWorld[3982]: Hello world!
Feb 11 16:54:30 colibri-imx8x systemd[1]: Started Hello World.
Feb 11 16:54:30 colibri-imx8x HelloWorld[4112]: Hello world!

root@colibri-imx8x:~# systemctl list-timers
NEXT                         LEFT       LAST                         PASSED       UNIT                         ACTIVATES
Tue 2020-02-11 17:09:30 CST  13min left Tue 2020-02-11 16:54:30 CST  1min 31s ago test1.timer                  test1.service
Wed 2020-02-12 16:52:20 CST  23h left   Tue 2020-02-11 16:52:20 CST  3min 42s ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service

实时定时器

这里我们将设置在每个小时第10分钟被执行的实时定时任务。下面是所需的定时器单元 test2.timer 和服务单元 test2.service。

test2.timer

[Unit]
Description=Run each 10min of hours

[Timer]
OnCalendar=*-*-* *:10:00

[Install]
WantedBy=timers.target

test2.service

[Unit]
Description=Hello Toradex

[Service]
ExecStart=/home/root/HelloToradex

在 test2.timer 中,OnCalendar= 设置定时器任务触发的时间。OnCalendar 的时间格式为 DayOfWeek Year-Month-Day Hour:Minute:Second。

其中 weekday 部分是可以省略的,定时器不匹配星期几,由后面的日期和时间部分来指定。如果填写则必须是英文中星期的缩写或者全称,如星期二是 Web 或者 Wednesday。可以使用 “,”,来指定多个星期天如Mon,Web,Fri。“..”用于指定连续的一段时间,如Mon..Fri 指从周一到周五。“,”和“..”还可以混合使用来指定一周的几天。

日期和时间部分,可以使用 “*” 表示任何符合的日期或者时间。如 2020-*-* 1:23:00 表示2020 年中每天的 1:23:00 时间触发定时器任务。也可以使用 “,” 来指定具体的而时间或日期,2020-1,2-* 1,2:23:00 表示 2020 年中1 月和 2 月每天的1:23:00 和 2:23:00 触发定时器任务。日期、时间还有多种表达方式,包括时区,具体可以查看文章最后的参考链接。

在 .timer 中 OnCalendar=*-*-* *:10:00 将定时器设置为每天每小时第 10 分钟触发。

同样将上述两个文件复制到 /etc/systemd/system 目录,并启动该定时器。

root@colibri-imx8x:/etc/systemd/system# systemctl enable test2.timer
root@colibri-imx8x:/etc/systemd/system# systemctl start test2.timer

系统当前时间是 17:29:28,距离18:10:00 还有 40 分钟,因此在 40 分钟后test2 定时器将被触发。

root@colibri-imx8x:~# date
Tue Feb 11 17:29:28 CST 2020
root@colibri-imx8x:~# systemctl list-timers
NEXT                         LEFT       LAST PASSED UNIT                         ACTIVATES
Tue 2020-02-11 17:41:57 CST  12min left n/a  n/a    systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Tue 2020-02-11 18:10:00 CST  40min left n/a  n/a    test2.timer                  test2.service

2 timers listed.
Pass --all to see loaded but inactive timers, too.

在等待一段时间后,查看日志,18:10:01 的时候 test2 定时器被触发,并执行 /home/root/HelloToradex 程序。

root@colibri-imx8x:~# journalctl -u test2.service
-- Logs begin at Tue 2020-02-11 17:27:02 CST, end at Tue 2020-02-11 18:11:11 CST. --
Feb 11 18:10:01 colibri-imx8x systemd[1]: Started Hello Toradex.
Feb 11 18:10:01 colibri-imx8x HelloToradex[4277]: Hello Toradex!

而距下一次触发的时间 19:10:00 还有一个小时。

root@colibri-imx8x:/etc/systemd/system# date
Tue Feb 11 18:10:43 CST 2020

root@colibri-imx8x:~# systemctl list-timers
NEXT                         LEFT       LAST                         PASSED    UNIT                         ACTIVATES
Tue 2020-02-11 19:10:00 CST  59min left Tue 2020-02-11 18:10:01 CST  47s ago   test2.timer                  test2.service
Wed 2020-02-12 17:41:58 CST  23h left   Tue 2020-02-11 17:41:58 CST  28min ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service

2 timers listed.
Pass --all to see loaded but inactive timers, too.

那么在一个小时再次查看日志文件,19:10:09 的时候 test2 定时器被触发。

root@colibri-imx8x:~# journalctl -u test2.service
-- Logs begin at Tue 2020-02-11 17:27:02 CST, end at Tue 2020-02-11 19:23:40 CST. --
Feb 11 18:10:01 colibri-imx8x systemd[1]: Started Hello Toradex.
Feb 11 18:10:01 colibri-imx8x HelloToradex[4277]: Hello Toradex!
Feb 11 19:10:09 colibri-imx8x systemd[1]: Started Hello Toradex.
Feb 11 19:10:09 colibri-imx8x HelloToradex[4689]: Hello Toradex!

混合定时器

除了上面单独使用单调或者实时定时器外,我们还可以同时利用两种定时器配置属性。

test3.timer

[Unit]
Description=Run each 10min of hours and on boot

[Timer]
OnBootSec=2min
OnCalendar=*-*-* *:10:00

[Install]
WantedBy=timers.target

test3.service

[Unit]
Description=Hello Torizon

[Service]
ExecStart=/home/root/HelloTorizon

这里 test3 定时器首先通过 OnBootSec=2min 设置为系统启动后 2 分钟触发,并且由 OnCalendar=*-*-* *:10:00 设置在每个小时第10分钟被执行的实时定时任务。这两个参数分别来自单调定时器和实时定时器配置。

启动该定时器后重启系统

root@colibri-imx8x:/etc/systemd/system# systemctl enable test3.timer
root@colibri-imx8x:/etc/systemd/system# sync
root@colibri-imx8x:/etc/systemd/system# reboot

系统启动后看到 test3 将会在 1 分 43 秒后触发。

root@colibri-imx8x:~# systemctl list-timers
NEXT                         LEFT          LAST PASSED UNIT                         ACTIVATES
Tue 2020-02-11 19:27:30 CST  1min 42s left n/a  n/a    test3.timer                  test3.service
Tue 2020-02-11 19:40:30 CST  14min left    n/a  n/a    systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service

在等待 2 分钟后,test3 已经被触发,并会在42 分钟后再次触发,即 20:10:00。

root@colibri-imx8x:~# systemctl list-timers
NEXT                         LEFT       LAST                         PASSED  UNIT                         ACTIVATES
Tue 2020-02-11 19:40:30 CST  12min left n/a                          n/a     systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Tue 2020-02-11 20:10:00 CST  42min left Tue 2020-02-11 19:27:31 CST  19s ago test3.timer                  test3.service

从日志中看到,系统在 19:25:35 左右启动,在其后 2 分钟 19:27:31 触发了test3 并执行 /home/root/HelloTorizon。

root@colibri-imx8x:~# journalctl -u test3.service
-- Logs begin at Tue 2020-02-11 19:25:35 CST, end at Tue 2020-02-11 19:28:09 CST. --
Feb 11 19:27:31 colibri-imx8x systemd[1]: Started Hello Torizon.
Feb 11 19:27:31 colibri-imx8x HelloTorizon[4002]: Hello Torizon!

在等到 20:10:00 后查看定时器任务,test3 如预期在 20:10:04 的时候被触发,44 分钟后下一次触发。

root@colibri-imx8x:~# systemctl list-timers
NEXT                         LEFT       LAST                         PASSED    UNIT                         ACTIVATES
Tue 2020-02-11 21:10:00 CST  44min left Tue 2020-02-11 20:10:04 CST  15min ago test3.timer                  test3.service
Wed 2020-02-12 19:40:31 CST  23h left   Tue 2020-02-11 19:40:31 CST  44min ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service

日志也记录了 20:10:04 时候test3 被触发并执行 /home/root/HelloTorizon。

root@colibri-imx8x:~# journalctl -u test3.service
-- Logs begin at Tue 2020-02-11 19:25:35 CST, end at Tue 2020-02-11 20:25:26 CST. --
Feb 11 19:27:31 colibri-imx8x systemd[1]: Started Hello Torizon.
Feb 11 19:27:31 colibri-imx8x HelloTorizon[4002]: Hello Torizon!
Feb 11 20:10:04 colibri-imx8x systemd[1]: Started Hello Torizon.
Feb 11 20:10:04 colibri-imx8x HelloTorizon[4299]: Hello Torizon!

到这里我们已经演示了不同类型定时器的使用方法,有些读者可能已经发现定时器任务被触发的时间并不是很严格得按照所设定的时间。例如上面 test3 在第二次被触发的时间是 20:10:04,不是 20:10:00。这是因为 Toradex Linux BSP 默认是普通的 Linux 内核,而非实时 Linux,这就导致在任务时间分配上会存在一定的时间抖动。其次,systemd 定时器中的 AccuracySec 参数默认是 1min,这设置了定时器的精度。定时器触发时间窗口为OnCalendar=、OnActiveSec=、OnBootSec=、OnStartupSec=、OnUnitActiveSec= 所设置的时间开始到 AccuracySec= 结束。这样做的目的是减少不必要的 CPU 唤醒,从而降低功耗。为了提高定时器精度,可以将 AccuracySec= 设置到最小的 1us。

为了避免在同一个时间触发多个定时器任务,从而导致系统资源紧张,可以使用 RandomizedDelaySec= 参数,为定时任务添加一个从 0 到 RandomizedDelaySec 的随机延时。

Systemd vs. Cron

上面的几个例子已经展示了 systemd 定时器的特点,其于传统的 Cron 比较如下:

优点:

  • Systemd 定时器可以利用 systemd 的依赖关系(如等待网络连接、某个文件的创建、其他应用就位后执行任务)
  • 定时任务会被记录到journald 日志
  • 定时器任务可以在系统/应用启动后触发
  • 可以利用cgroups
  • 可以单独执行任务而不依赖定时器(.timer 和 .service 分开配置)

缺点:

  • 相对复杂的配置过程,Cron 仅需一行命令即可
  • Systemd 定时器默认不支持 MAILTO 功能

总结

定时器任务作为嵌入式系统中常见的应用,systemd 定时器为用户提供更多的可配置功能以及优化选项。本文列举了 systemd 定时器基本操作方法,以及和cron 对比,帮助用户更快得使用。更多的技术细节和功能请参考下面的链接内容。

参考:

https://wiki.archlinux.org/index.php/Systemd/Timers
https://www.freedesktop.org/software/systemd/man/systemd.timer.html
https://jlk.fjfi.cvut.cz/arch/manpages/man/systemd.time.7
https://jlk.fjfi.cvut.cz/arch/manpages/man/systemd.timer.5

Author: 胡珊逢,FAE,韬睿(上海)
Share this on:

Leave a comment

Please login to leave a comment!
Have a Question?