标签 定时器 下的文章

这是一个演示如何创建 POSIX 兼容的间隔定时器的教程。

 title=

对开发人员来说,定时某些事件是一项常见任务。定时器的常见场景是看门狗、任务的循环执行,或在特定时间安排事件。在这篇文章中,我将演示如何使用 timer\_create(...) 创建一个 POSIX 兼容的间隔定时器。

你可以从 GitHub 下载下面样例的源代码。

准备 Qt Creator

我使用 Qt Creator 作为该样例的 IDE。为了在 Qt Creator 运行和调试样例代码,请克隆 GitHub 上的仓库,打开 Qt Creator,在 “ 文件 File -> 打开文件或项目…… Open File or Project... ” 并选择 “CMakeLists.txt”:

Qt Creator open project

在 Qt Creator 中打开项目

选择工具链之后,点击 “ 配置项目 Configure Project ”。这个项目包括三个独立的样例(我们在这篇文章中将只会用到其中的两个)。使用绿色标记出来的菜单,可以在每个样例的配置之间切换,并为每个样例激活在终端运行 “ 在终端中运行 Run in terminal ”(用黄色标记)。当前用于构建和调试的活动示例可以通过左下角的“ 调试 Debug ” 按钮进行选择(参见下面的橙色标记)。

Project configuration

项目配置

线程定时器

让我们看看 simple_threading_timer.c 样例。这是最简单的一个。它展示了一个调用了超时函数 expired 的间隔定时器是如何被创建的。在每次过期时,都会创建一个新的线程,在其中调用函数 expired:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

void expired(union sigval timer_data);

pid_t gettid(void);

struct t_eventData{
    int myData;
};

int main()
{
    int res = 0;
    timer_t timerId = 0;

    struct t_eventData eventData = { .myData = 0 };

    /*  sigevent 指定了过期时要执行的操作  */
    struct sigevent sev = { 0 };

    /* 指定启动延时时间和间隔时间 
    * it_value和it_interval 不能为零 */

    struct itimerspec its = {   .it_value.tv_sec  = 1,
                                .it_value.tv_nsec = 0,
                                .it_interval.tv_sec  = 1,
                                .it_interval.tv_nsec = 0
                            };

    printf("Simple Threading Timer - thread-id: %d\n", gettid());

    sev.sigev_notify = SIGEV_THREAD;
    sev.sigev_notify_function = &amp;expired;
    sev.sigev_value.sival_ptr = &amp;eventData;

    /* 创建定时器 */
    res = timer_create(CLOCK_REALTIME, &amp;sev, &amp;timerId);

    if (res != 0){
        fprintf(stderr, "Error timer_create: %s\n", strerror(errno));
        exit(-1);
    }

    /* 启动定时器 */
    res = timer_settime(timerId, 0, &amp;its, NULL);

    if (res != 0){
        fprintf(stderr, "Error timer_settime: %s\n", strerror(errno));
        exit(-1);
    }

    printf("Press ETNER Key to Exit\n");
    while(getchar()!='\n'){}
    return 0;
}

void expired(union sigval timer_data){
    struct t_eventData *data = timer_data.sival_ptr;
    printf("Timer fired %d - thread-id: %d\n", ++data->myData, gettid());
}

这种方法的优点是在代码和简单调试方面用量小。缺点是由于到期时创建新线程而增加额外的开销,因此行为不太确定。

中断信号定时器

超时定时器通知的另一种可能性是基于 内核信号。内核不是在每次定时器过期时创建一个新线程,而是向进程发送一个信号,进程被中断,并调用相应的信号处理程序。

由于接收信号时的默认操作是终止进程(参考 signal 手册页),我们必须要提前设置好 Qt Creator,以便进行正确的调试。

当被调试对象接收到一个信号时,Qt Creator 的默认行为是:

  • 中断执行并切换到调试器上下文。
  • 显示一个弹出窗口,通知用户接收到信号。

这两种操作都不需要,因为信号的接收是我们应用程序的一部分。

Qt Creator 在后台使用 GDB。为了防止 GDB 在进程接收到信号时停止执行,进入 “ 工具 ( Tools ) -> 选项 Options ” 菜单,选择 “ 调试器 Debugger ”,并导航到 “ 本地变量和表达式 Locals & Expressions ”。添加下面的表达式到 “ 定制调试助手 Debugging Helper Customization ”:

handle SIG34 nostop pass

Signal no stop with error

Sig 34 时不停止

你可以在 GDB 文档 中找到更多关于 GDB 信号处理的信息。

接下来,当我们在信号处理程序中停止时,我们要抑制每次接收到信号时通知我们的弹出窗口:

Signal 34 pop up box

Signal 34 弹出窗口

为此,导航到 “GDB” 标签并取消勾选标记的复选框:

Timer signal windows

定时器信号窗口

现在你可以正确的调试 signal_interrupt_timer。真正的信号定时器的实施会更复杂一些:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#define UNUSED(x) (void)(x)

static void handler(int sig, siginfo_t *si, void *uc);
pid_t gettid(void);

struct t_eventData{
    int myData;
};

int main()
{
    int res = 0;
    timer_t timerId = 0;

    struct sigevent sev = { 0 };
    struct t_eventData eventData = { .myData = 0 };

    /* 指定收到信号时的操作 */
    struct sigaction sa = { 0 };

    /* 指定启动延时的时间和间隔时间 */
    struct itimerspec its = {   .it_value.tv_sec  = 1,
                                .it_value.tv_nsec = 0,
                                .it_interval.tv_sec  = 1,
                                .it_interval.tv_nsec = 0
                            };

    printf("Signal Interrupt Timer - thread-id: %d\n", gettid());

    sev.sigev_notify = SIGEV_SIGNAL; // Linux-specific
    sev.sigev_signo = SIGRTMIN;
    sev.sigev_value.sival_ptr = &amp;eventData;

    /* 创建定时器 */
    res = timer_create(CLOCK_REALTIME, &amp;sev, &amp;timerId);

    if ( res != 0){
        fprintf(stderr, "Error timer_create: %s\n", strerror(errno));
        exit(-1);
    }

    /* 指定信号和处理程序 */
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = handler;

    /* 初始化信号 */
    sigemptyset(&amp;sa.sa_mask);

    printf("Establishing handler for signal %d\n", SIGRTMIN);

    /* 注册信号处理程序 */
    if (sigaction(SIGRTMIN, &amp;sa, NULL) == -1){
        fprintf(stderr, "Error sigaction: %s\n", strerror(errno));
        exit(-1);
    }

    /* 启动定时器 */
    res = timer_settime(timerId, 0, &amp;its, NULL);

    if ( res != 0){
        fprintf(stderr, "Error timer_settime: %s\n", strerror(errno));
        exit(-1);
    }

    printf("Press ENTER to Exit\n");
    while(getchar()!='\n'){}
    return 0;
}

static void
handler(int sig, siginfo_t *si, void *uc)
{
    UNUSED(sig);
    UNUSED(uc);
    struct t_eventData *data = (struct t_eventData *) si->_sifields._rt.si_sigval.sival_ptr;
    printf("Timer fired %d - thread-id: %d\n", ++data->myData, gettid());
}

与线程定时器相比,我们必须初始化信号并注册一个信号处理程序。这种方法性能更好,因为它不会导致创建额外的线程。因此,信号处理程序的执行也更加确定。缺点显然是正确调试需要额外的配置工作。

总结

本文中描述的两种方法都是接近内核的定时器的实现。不过,即使 timer\_create(...) 函数是 POSIX 规范的一部分,由于数据结构的细微差别,也不可能在 FreeBSD 系统上编译样例代码。除了这个缺点之外,这种实现还为通用计时应用程序提供了细粒度控制。


via: https://opensource.com/article/21/10/linux-timers

作者:Stephan Avenwedde 选题:lujun9972 译者:FigaroCao 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

systemd 提供定时器有一段时间了,定时器替代了 cron 功能,这一特性值得看看。本文将向你介绍在系统启动后如何使用 systemd 中的定时器来运行任务,并在此后重复运行。这不是对 systemd 的全面讨论,只是对此特性的一个介绍。

快速回顾:cron、anacron 与 systemd

cron 可以以几分钟到几个月或更长时间的粒度调度运行一个任务。设置起来相对简单,它只需要一个配置文件。虽然配置过程有些深奥,但一般用户也可以使用。

然而,如果你的系统在需要执行的时间没有运行,那么 cron 会失败。

anacron 克服了“系统没有运行”的问题。它确保任务将在你的系统再次启动时执行。虽然它旨在给管理员使用,但有些系统允许普通用户访问 anacron。

但是,anacron 的执行频率不能低于每天一次。

cron 和 anacron 都存在执行上下文一致性的问题。必须注意任务运行时有效的环境与测试时使用的环境完全相同。必须提供相同的 shell、环境变量和路径。这意味着测试和调试有时会很困难。

systemd 定时器提供了 cron 和 anacron 二者的优点,允许调度到分钟粒度。确保在系统再次运行时执行任务,即使在预期的执行时间内系统处于关闭状态。它对所有用户都可用。你可以在它将要运行的环境中测试和调试执行。

但是,它的配置更加复杂,至少需要两个配置文件。

如果你的 cron 和 anacron 配置可以很好地为你服务,那么可能没有理由改变。但是 systemd 至少值得研究,因为它可以简化任何当前的 cron/anacron 工作方式。

配置

systemd 定时器执行功能至少需要两个文件。这两个是“ 定时器单元 timer unit ”和“ 服务单元 service unit ”。(其执行的)“动作”不仅仅是简单的命令,你还需要一个“作业”文件或脚本来执行必要的功能。

定时器单元文件定义调度表,而服务单元文件定义执行的任务。有关的更多详细信息请参考 man systemd.timer 中提供的 .timer 单元。服务单元的详细信息可在 man systemd.service 中找到。

单元文件存放在几个位置(在手册页中有列出)。然而,对于普通用户来说,最容易找到的位置可能是 ~/.config/systemd/user。请注意,这里的 user 是字符串 user。

示例

此示例是一个创建用户调度作业而不是(以 root 用户身份运行的)系统调度作业的简单示例。它将消息、日期和时间打印到文件中。

1、首先创建一个执行任务的 shell 脚本。在你的本地 bin 目录中创建它,例如在 ~/bin/schedule-test.sh 中。

创建文件:

touch ~/bin/schedule-test.sh

然后将以下内容添加到你刚刚创建的文件中:

#!/bin/sh
echo "This is only a test: $(date)" >> "$HOME/schedule-test-output.txt"

记住赋予你的 shell 脚本执行权限。

2、创建 .service 单元调用上面的脚本。在以下位置创建目录与文件:~/.config/systemd/user/schedule-test.service:

[Unit]
Description=A job to test the systemd scheduler

[Service]
Type=simple
ExecStart=/home/<user>/bin/schedule-test.sh

[Install]
WantedBy=default.target

请注意 <user> 应该是你的家目录地址,但是单元文件路径名中的 user 实际上是字符串 user。

ExecStart 应该提供一个没有变量的绝对地址。例外情况是,对于用户单元文件,你可以用 %h 替换 $HOME。换句话说,你可以使用:

ExecStart=%h/bin/schedule-test.sh

这仅用于用户单元文件,而不适用于系统服务,因为在系统环境中运行时 %h 总是返回 /root。其他特殊符号可在 man systemd.unit 的 SPECIFIERS 中找到。因为它超出了本文的范围,所以这就是我们目前需要了解的关于特殊符号的全部内容。

3、创建一个 .timer 单元文件,该文件实际上调度你创建的 .service 单元文件。在 .service 单元文件相同位置创建它:~/.config/systemd/user/schedule-test.timer。请注意,文件名仅在扩展名上有所不同,例如一个是 .service,一个是 .timer。

[Unit]
Description=Schedule a message every 1 minute
RefuseManualStart=no  # Allow manual starts
RefuseManualStop=no   # Allow manual stops

[Timer]
#Execute job if it missed a run due to machine being off
Persistent=true
#Run 120 seconds after boot for the first time
OnBootSec=120
#Run every 1 minute thereafter
OnUnitActiveSec=60
#File describing job to execute
Unit=schedule-test.service

[Install]
WantedBy=timers.target

请注意,这个 .timer 单元文件使用了 OnUnitActiveSec 来指定调度表。OnCalendar 选项更加灵活。例如:

# run on the minute of every minute every hour of every day
 OnCalendar=*-*-* *:*:00
# run on the hour of every hour of every day
 OnCalendar=*-*-* *:00:00
# run every day
 OnCalendar=*-*-* 00:00:00
# run 11:12:13 of the first or fifth day of any month of the year
# 2012, but only if that day is a Thursday or Friday
 OnCalendar=Thu,Fri 2012-*-1,5 11:12:13

有关 OnCalendar 的更多信息参见 这里。

4、所有的部件都已就位,但你应该进行测试,以确保一切正常。首先,启用该用户服务:

$ systemctl --user enable schedule-test.service

这将导致类似如下的输出:

Created symlink /home/<user>/.config/systemd/user/default.target.wants/schedule-test.service → /home/<user>/.config/systemd/user/schedule-test.service.

现在执行测试工作:

$ systemctl --user start schedule-test.service

检查你的输出文件($HOME/schedule-test-output.txt),确保你的脚本运行正常。应该只有一个条目,因为我们还没有启动定时器。必要时进行调试。如果你需要更改 .service 单元文件,而不是更改它调用的 shell 脚本,请不要忘记再次启用该服务。

5、一旦作业正常运行,通过为服务启用、启动用户定时器来实时调度作业:

$ systemctl --user enable schedule-test.timer
$ systemctl --user start schedule-test.timer

请注意,你已经在上面的步骤 4 中启动、启用了服务,因此只需要为它启用、启动定时器。

enable 命令会产生如下输出:

Created symlink /home/<user>/.config/systemd/user/timers.target.wants/schedule-test.timer → /home/<user>/.config/systemd/user/schedule-test.timer.

start 命令将只是返回命令行界面提示符。

其他操作

你可以检查和监控服务。如果你从系统服务收到错误,下面的第一个命令特别有用:

$ systemctl --user status schedule-test
$ systemctl --user list-unit-files

手动停止服务:

$ systemctl --user stop schedule-test.service

永久停止并禁用定时器和服务,重新加载守护程序配置并重置任何失败通知:

$ systemctl --user stop schedule-test.timer
$ systemctl --user disable schedule-test.timer
$ systemctl --user stop schedule-test.service
$ systemctl --user disable schedule-test.service
$ systemctl --user daemon-reload
$ systemctl --user reset-failed

总结

本文以 systemd 定时器为出发点,但是 systemd 的内容远不止于此。这篇文章应该为你提供一个基础。你可以从 Fedora Magazine systemd 系列 开始探索更多。

参考

更多阅读:


via: https://fedoramagazine.org/systemd-timers-for-scheduling-tasks/

作者:Richard England 选题:lujun9972 译者:dcoliversun 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

定时器提供了比 cron 作业更为细粒度的事件控制。

 title=

我正在致力于将我的 cron 作业迁移到 systemd 定时器上。我已经使用定时器多年了,但通常来说,我的学识只足以支撑我当前的工作。但在我研究 systemd 系列 的过程中,我发现 systemd 定时器有一些非常有意思的能力。

与 cron 作业类似,systemd 定时器可以在特定的时间间隔触发事件(shell 脚本和程序),例如每天一次或在一个月中的特定某一天(或许只有在周一生效),或在从上午 8 点到下午 6 点的工作时间内每隔 15 分钟一次。定时器也可以做到 cron 作业无法做到的一些事情。举个例子,定时器可以在特定事件发生后的一段时间后触发一段脚本或者程序去执行,例如开机、启动、上个任务完成,甚至于定时器调用的上个服务单元的完成的时刻。

操作系统维护的计时器

当在一个新系统上安装 Fedora 或者是任意一个基于 systemd 的发行版时,作为系统维护过程的一部分,它会在 Linux 宿主机的后台中创建多个定时器。这些定时器会触发事件来执行必要的日常维护任务,比如更新系统数据库、清理临时目录、轮换日志文件,以及更多其他事件。

作为示例,我会查看一些我的主要工作站上的定时器,通过执行 systemctl status *timer 命令来展示主机上的所有定时器。星号的作用与文件通配相同,所以这个命令会列出所有的 systemd 定时器单元。

[root@testvm1 ~]# systemctl status *timer
● mlocate-updatedb.timer - Updates mlocate database every day
     Loaded: loaded (/usr/lib/systemd/system/mlocate-updatedb.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Fri 2020-06-05 00:00:00 EDT; 15h left
   Triggers: ● mlocate-updatedb.service

Jun 02 08:02:33 testvm1.both.org systemd[1]: Started Updates mlocate database every day.

● logrotate.timer - Daily rotation of log files
     Loaded: loaded (/usr/lib/systemd/system/logrotate.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Fri 2020-06-05 00:00:00 EDT; 15h left
   Triggers: ● logrotate.service
       Docs: man:logrotate(8)
             man:logrotate.conf(5)

Jun 02 08:02:33 testvm1.both.org systemd[1]: Started Daily rotation of log files.

● sysstat-summary.timer - Generate summary of yesterday's process accounting
     Loaded: loaded (/usr/lib/systemd/system/sysstat-summary.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Fri 2020-06-05 00:07:00 EDT; 15h left
   Triggers: ● sysstat-summary.service

Jun 02 08:02:33 testvm1.both.org systemd[1]: Started Generate summary of yesterday's process accounting.

● fstrim.timer - Discard unused blocks once a week
     Loaded: loaded (/usr/lib/systemd/system/fstrim.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Mon 2020-06-08 00:00:00 EDT; 3 days left
   Triggers: ● fstrim.service
       Docs: man:fstrim

Jun 02 08:02:33 testvm1.both.org systemd[1]: Started Discard unused blocks once a week.

● sysstat-collect.timer - Run system activity accounting tool every 10 minutes
     Loaded: loaded (/usr/lib/systemd/system/sysstat-collect.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Thu 2020-06-04 08:50:00 EDT; 41s left
   Triggers: ● sysstat-collect.service

Jun 02 08:02:33 testvm1.both.org systemd[1]: Started Run system activity accounting tool every 10 minutes.

● dnf-makecache.timer - dnf makecache --timer
     Loaded: loaded (/usr/lib/systemd/system/dnf-makecache.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Thu 2020-06-04 08:51:00 EDT; 1min 41s left
   Triggers: ● dnf-makecache.service

Jun 02 08:02:33 testvm1.both.org systemd[1]: Started dnf makecache –timer.

● systemd-tmpfiles-clean.timer - Daily Cleanup of Temporary Directories
     Loaded: loaded (/usr/lib/systemd/system/systemd-tmpfiles-clean.timer; static; vendor preset: disabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Fri 2020-06-05 08:19:00 EDT; 23h left
   Triggers: ● systemd-tmpfiles-clean.service
       Docs: man:tmpfiles.d(5)
             man:systemd-tmpfiles(8)

Jun 02 08:02:33 testvm1.both.org systemd[1]: Started Daily Cleanup of Temporary Directories.

每个定时器至少有六行相关信息:

  • 定时器的第一行有定时器名字和定时器目的的简短介绍
  • 第二行展示了定时器的状态,是否已加载,定时器单元文件的完整路径以及预设信息。
  • 第三行指明了其活动状态,包括该定时器激活的日期和时间。
  • 第四行包括了该定时器下次被触发的日期和时间和距离触发的大概时间。
  • 第五行展示了被定时器触发的事件或服务名称。
  • 部分(不是全部)systemd 单元文件有相关文档的指引。我虚拟机上输出中有三个定时器有文档指引。这是一个很好(但非必要)的信息。
  • 最后一行是计时器最近触发的服务实例的日志条目。

你也许有一些不一样的定时器,取决于你的主机。

创建一个定时器

尽管我们可以解构一个或多个现有的计时器来了解其工作原理,但让我们创建我们自己的 服务单元 和一个定时器去触发它。为了保持简单,我们将使用一个相当简单的例子。当我们完成这个实验之后,就能更容易理解其他定时器的工作原理以及发现它们正在做什么。

首先,创建一个运行基础东西的简单的服务,例如 free 命令。举个例子,你可能想定时监控空余内存。在 /etc/systemd/system 目录下创建如下的 myMonitor.server 单元文件。它不需要是可执行文件:

# This service unit is for testing timer units
# By David Both
# Licensed under GPL V2
#

[Unit]
Description=Logs system statistics to the systemd journal
Wants=myMonitor.timer

[Service]
Type=oneshot
ExecStart=/usr/bin/free

[Install]
WantedBy=multi-user.target

这大概是你能创建的最简单的服务单元了。现在我们查看一下服务状态同时测试一下服务单元确保它和我们预期一样可用。

[root@testvm1 system]# systemctl status myMonitor.service
● myMonitor.service - Logs system statistics to the systemd journal
     Loaded: loaded (/etc/systemd/system/myMonitor.service; disabled; vendor preset: disabled)
     Active: inactive (dead)
[root@testvm1 system]# systemctl start myMonitor.service
[root@testvm1 system]#

输出在哪里呢?默认情况下,systemd 服务单元执行程序的标准输出(STDOUT)会被发送到系统日志中,它保留了记录供现在或者之后(直到某个时间点)查看。(在本系列的后续文章中,我将介绍系统日志的记录和保留策略)。专门查看你的服务单元的日志,而且只针对今天。-S 选项,即 --since 的缩写,允许你指定 journalctl 工具搜索条目的时间段。这并不代表你不关心过往结果 —— 在这个案例中,不会有过往记录 —— 如果你的机器以及运行了很长时间且堆积了大量的日志,它可以缩短搜索时间。

[root@testvm1 system]# journalctl -S today -u myMonitor.service
-- Logs begin at Mon 2020-06-08 07:47:20 EDT, end at Thu 2020-06-11 09:40:47 EDT. --
Jun 11 09:12:09 testvm1.both.org systemd[1]: Starting Logs system statistics to the systemd journal...
Jun 11 09:12:09 testvm1.both.org free[377966]:               total        used        free      shared  buff/cache   available
Jun 11 09:12:09 testvm1.both.org free[377966]: Mem:       12635740      522868    11032860        8016     1080012    11821508
Jun 11 09:12:09 testvm1.both.org free[377966]: Swap:       8388604           0     8388604
Jun 11 09:12:09 testvm1.both.org systemd[1]: myMonitor.service: Succeeded.
[root@testvm1 system]#

由服务触发的任务可以是单个程序、一组程序或者是一个脚本语言写的脚本。通过在 myMonitor.service 单元文件里的 [Service] 块末尾中添加如下行可以为服务添加另一个任务:

ExecStart=/usr/bin/lsblk

再次启动服务,查看日志检查结果,结果应该看上去像这样。你应该在日志中看到两条命令的结果输出:

Jun 11 15:42:18 testvm1.both.org systemd[1]: Starting Logs system statistics to the systemd journal...
Jun 11 15:42:18 testvm1.both.org free[379961]:               total        used        free      shared  buff/cache   available
Jun 11 15:42:18 testvm1.both.org free[379961]: Mem:       12635740      531788    11019540        8024     1084412    11812272
Jun 11 15:42:18 testvm1.both.org free[379961]: Swap:       8388604           0     8388604
Jun 11 15:42:18 testvm1.both.org lsblk[379962]: NAME          MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
Jun 11 15:42:18 testvm1.both.org lsblk[379962]: sda             8:0    0  120G  0 disk
Jun 11 15:42:18 testvm1.both.org lsblk[379962]: ├─sda1          8:1    0    4G  0 part /boot
Jun 11 15:42:18 testvm1.both.org lsblk[379962]: └─sda2          8:2    0  116G  0 part
Jun 11 15:42:18 testvm1.both.org lsblk[379962]:   ├─VG01-root 253:0    0    5G  0 lvm  /
Jun 11 15:42:18 testvm1.both.org lsblk[379962]:   ├─VG01-swap 253:1    0    8G  0 lvm  [SWAP]
Jun 11 15:42:18 testvm1.both.org lsblk[379962]:   ├─VG01-usr  253:2    0   30G  0 lvm  /usr
Jun 11 15:42:18 testvm1.both.org lsblk[379962]:   ├─VG01-tmp  253:3    0   10G  0 lvm  /tmp
Jun 11 15:42:18 testvm1.both.org lsblk[379962]:   ├─VG01-var  253:4    0   20G  0 lvm  /var
Jun 11 15:42:18 testvm1.both.org lsblk[379962]:   └─VG01-home 253:5    0   10G  0 lvm  /home
Jun 11 15:42:18 testvm1.both.org lsblk[379962]: sr0            11:0    1 1024M  0 rom
Jun 11 15:42:18 testvm1.both.org systemd[1]: myMonitor.service: Succeeded.
Jun 11 15:42:18 testvm1.both.org systemd[1]: Finished Logs system statistics to the systemd journal.

现在你知道了你的服务可以按预期工作了,在 /etc/systemd/system 目录下创建 myMonitor.timer 定时器单元文件,添加如下代码:

# This timer unit is for testing
# By David Both
# Licensed under GPL V2
#

[Unit]
Description=Logs some system statistics to the systemd journal
Requires=myMonitor.service

[Timer]
Unit=myMonitor.service
OnCalendar=*-*-* *:*:00

[Install]
WantedBy=timers.target

在 myMonitor.timer 文件中的 OnCalendar 时间格式,*-*-* *:*:00,应该会每分钟触发一次定时器去执行 myMonitor.service 单元。我会在文章的后面进一步探索 OnCalendar 设置。

到目前为止,在服务被计时器触发运行时观察与之有关的日志记录。你也可以跟踪计时器,跟踪服务可以让你接近实时的看到结果。执行 journalctl 时带上 -f 选项:

[root@testvm1 system]# journalctl -S today -f -u myMonitor.service
-- Logs begin at Mon 2020-06-08 07:47:20 EDT. --

执行但是不启用该定时器,看看它运行一段时间后发生了什么:

[root@testvm1 ~]# systemctl start myMonitor.service
[root@testvm1 ~]#

一条结果立即就显示出来了,下一条大概在一分钟后出来。观察几分钟日志,看看你有没有跟我发现同样的事情:

[root@testvm1 system]# journalctl -S today -f -u myMonitor.service
-- Logs begin at Mon 2020-06-08 07:47:20 EDT. --
Jun 13 08:39:18 testvm1.both.org systemd[1]: Starting Logs system statistics to the systemd journal...
Jun 13 08:39:18 testvm1.both.org systemd[1]: myMonitor.service: Succeeded.
Jun 13 08:39:19 testvm1.both.org free[630566]:               total        used        free      shared  buff/cache   available
Jun 13 08:39:19 testvm1.both.org free[630566]: Mem:       12635740      556604    10965516        8036     1113620    11785628
Jun 13 08:39:19 testvm1.both.org free[630566]: Swap:       8388604           0     8388604
Jun 13 08:39:18 testvm1.both.org systemd[1]: Finished Logs system statistics to the systemd journal.
Jun 13 08:39:19 testvm1.both.org lsblk[630567]: NAME          MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
Jun 13 08:39:19 testvm1.both.org lsblk[630567]: sda             8:0    0  120G  0 disk
Jun 13 08:39:19 testvm1.both.org lsblk[630567]: ├─sda1          8:1    0    4G  0 part /boot
Jun 13 08:39:19 testvm1.both.org lsblk[630567]: └─sda2          8:2    0  116G  0 part
Jun 13 08:39:19 testvm1.both.org lsblk[630567]:   ├─VG01-root 253:0    0    5G  0 lvm  /
Jun 13 08:39:19 testvm1.both.org lsblk[630567]:   ├─VG01-swap 253:1    0    8G  0 lvm  [SWAP]
Jun 13 08:39:19 testvm1.both.org lsblk[630567]:   ├─VG01-usr  253:2    0   30G  0 lvm  /usr
Jun 13 08:39:19 testvm1.both.org lsblk[630567]:   ├─VG01-tmp  253:3    0   10G  0 lvm  /tmp
Jun 13 08:39:19 testvm1.both.org lsblk[630567]:   ├─VG01-var  253:4    0   20G  0 lvm  /var
Jun 13 08:39:19 testvm1.both.org lsblk[630567]:   └─VG01-home 253:5    0   10G  0 lvm  /home
Jun 13 08:39:19 testvm1.both.org lsblk[630567]: sr0            11:0    1 1024M  0 rom
Jun 13 08:40:46 testvm1.both.org systemd[1]: Starting Logs system statistics to the systemd journal...
Jun 13 08:40:46 testvm1.both.org free[630572]:               total        used        free      shared  buff/cache   available
Jun 13 08:40:46 testvm1.both.org free[630572]: Mem:       12635740      555228    10966836        8036     1113676    11786996
Jun 13 08:40:46 testvm1.both.org free[630572]: Swap:       8388604           0     8388604
Jun 13 08:40:46 testvm1.both.org lsblk[630574]: NAME          MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
Jun 13 08:40:46 testvm1.both.org lsblk[630574]: sda             8:0    0  120G  0 disk
Jun 13 08:40:46 testvm1.both.org lsblk[630574]: ├─sda1          8:1    0    4G  0 part /boot
Jun 13 08:40:46 testvm1.both.org lsblk[630574]: └─sda2          8:2    0  116G  0 part
Jun 13 08:40:46 testvm1.both.org lsblk[630574]:   ├─VG01-root 253:0    0    5G  0 lvm  /
Jun 13 08:40:46 testvm1.both.org lsblk[630574]:   ├─VG01-swap 253:1    0    8G  0 lvm  [SWAP]
Jun 13 08:40:46 testvm1.both.org lsblk[630574]:   ├─VG01-usr  253:2    0   30G  0 lvm  /usr
Jun 13 08:40:46 testvm1.both.org lsblk[630574]:   ├─VG01-tmp  253:3    0   10G  0 lvm  /tmp
Jun 13 08:40:46 testvm1.both.org lsblk[630574]:   ├─VG01-var  253:4    0   20G  0 lvm  /var
Jun 13 08:40:46 testvm1.both.org lsblk[630574]:   └─VG01-home 253:5    0   10G  0 lvm  /home
Jun 13 08:40:46 testvm1.both.org lsblk[630574]: sr0            11:0    1 1024M  0 rom
Jun 13 08:40:46 testvm1.both.org systemd[1]: myMonitor.service: Succeeded.
Jun 13 08:40:46 testvm1.both.org systemd[1]: Finished Logs system statistics to the systemd journal.
Jun 13 08:41:46 testvm1.both.org systemd[1]: Starting Logs system statistics to the systemd journal...
Jun 13 08:41:46 testvm1.both.org free[630580]:               total        used        free      shared  buff/cache   available
Jun 13 08:41:46 testvm1.both.org free[630580]: Mem:       12635740      553488    10968564        8036     1113688    11788744
Jun 13 08:41:46 testvm1.both.org free[630580]: Swap:       8388604           0     8388604
Jun 13 08:41:47 testvm1.both.org lsblk[630581]: NAME          MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
Jun 13 08:41:47 testvm1.both.org lsblk[630581]: sda             8:0    0  120G  0 disk
Jun 13 08:41:47 testvm1.both.org lsblk[630581]: ├─sda1          8:1    0    4G  0 part /boot
Jun 13 08:41:47 testvm1.both.org lsblk[630581]: └─sda2          8:2    0  116G  0 part
Jun 13 08:41:47 testvm1.both.org lsblk[630581]:   ├─VG01-root 253:0    0    5G  0 lvm  /
Jun 13 08:41:47 testvm1.both.org lsblk[630581]:   ├─VG01-swap 253:1    0    8G  0 lvm  [SWAP]
Jun 13 08:41:47 testvm1.both.org lsblk[630581]:   ├─VG01-usr  253:2    0   30G  0 lvm  /usr
Jun 13 08:41:47 testvm1.both.org lsblk[630581]:   ├─VG01-tmp  253:3    0   10G  0 lvm  /tmp
Jun 13 08:41:47 testvm1.both.org lsblk[630581]:   ├─VG01-var  253:4    0   20G  0 lvm  /var
Jun 13 08:41:47 testvm1.both.org lsblk[630581]:   └─VG01-home 253:5    0   10G  0 lvm  /home
Jun 13 08:41:47 testvm1.both.org lsblk[630581]: sr0            11:0    1 1024M  0 rom
Jun 13 08:41:47 testvm1.both.org systemd[1]: myMonitor.service: Succeeded.
Jun 13 08:41:47 testvm1.both.org systemd[1]: Finished Logs system statistics to the systemd journal.

别忘了检查下计时器和服务的状态。

你在日志里大概至少注意到两件事。第一,你不需要特地做什么来让 myMonitor.service 单元中 ExecStart 触发器产生的 STDOUT 存储到日志里。这都是用 systemd 来运行服务的一部分功能。然而,它确实意味着你需要小心对待服务单元里面执行的脚本和它们能产生多少 STDOUT。

第二,定时器并不是精确在每分钟的 :00 秒执行的,甚至每次执行的时间间隔都不是刚好一分钟。这是特意的设计,但是有必要的话可以改变这种行为(如果只是它挑战了你的系统管理员的敏感神经)。

这样设计的初衷是为了防止多个服务在完全相同的时刻被触发。举个例子,你可以用例如 Weekly,Daily 等时间格式。这些快捷写法都被定义为在某一天的 00:00:00 执行。当多个定时器都这样定义的话,有很大可能它们会同时执行。

systemd 定时器被故意设计成在规定时间附近随机波动的时间点触发,以避免同一时间触发。它们在一个时间窗口内半随机触发,时间窗口开始于预设的触发时间,结束于预设时间后一分钟。根据 systemd.timer 的手册页,这个触发时间相对于其他已经定义的定时器单元保持在稳定的位置。你可以在日志条目中看到,定时器在启动后立即触发,然后在每分钟后的 46 或 47 秒触发。

大部分情况下,这种概率抖动的定时器是没事的。当调度类似执行备份的任务,只需要它们在下班时间运行,这样是没问题的。系统管理员可以选择确定的开始时间来确保不和其他任务冲突,例如 01:05:00 这样典型的 cron 作业时间,但是有很大范围的时间值可以满足这一点。在开始时间上的一个分钟级别的随机往往是无关紧要的。

然而,对某些任务来说,精确的触发时间是个硬性要求。对于这类任务,你可以向单元文件的 Timer 块中添加如下声明来指定更高的触发时间跨度精确度(精确到微秒以内):

AccuracySec=1us

时间跨度可用于指定所需的精度,以及定义重复事件或一次性事件的时间跨度。它能识别以下单位:

  • usec,us,µs
  • msec,ms
  • seconds,second,sec,s
  • minutes,minute,min,m
  • hours,hour,hr,h
  • days,day,d
  • weeks,week,w
  • months,month,M(定义为 30.44 天)
  • years,year,y(定义为 365.25 天)

所有 /usr/lib/systemd/system 中的定时器都指定了一个更宽松的时间精度,因为精准时间没那么重要。看看这些系统创建的定时器的时间格式:

[root@testvm1 system]# grep Accur /usr/lib/systemd/system/*timer
/usr/lib/systemd/system/fstrim.timer:AccuracySec=1h
/usr/lib/systemd/system/logrotate.timer:AccuracySec=1h
/usr/lib/systemd/system/logwatch.timer:AccuracySec=12h
/usr/lib/systemd/system/mlocate-updatedb.timer:AccuracySec=24h
/usr/lib/systemd/system/raid-check.timer:AccuracySec=24h
/usr/lib/systemd/system/unbound-anchor.timer:AccuracySec=24h
[root@testvm1 system]#

看下 /usr/lib/systemd/system 目录下部分定时器单元文件的完整内容,看看它们是如何构建的。

在本实验中不必让这个定时器在启动时激活,但下面这个命令可以设置开机自启:

[root@testvm1 system]# systemctl enable myMonitor.timer

你创建的单元文件不需要是可执行的。你同样不需要启用服务,因为它是被定时器触发的。如果你需要的话,你仍然可以在命令行里手动触发该服务单元。尝试一下,然后观察日志。

关于定时器精度、事件时间规格和触发事件的详细信息,请参见 systemd.timer 和 systemd.time 的手册页。

定时器类型

systemd 定时器还有一些在 cron 中找不到的功能,cron 只在确定的、重复的、具体的日期和时间触发。systemd 定时器可以被配置成根据其他 systemd 单元状态发生改变时触发。举个例子,定时器可以配置成在系统开机、启动后,或是某个确定的服务单元激活之后的一段时间被触发。这些被称为单调计时器。“单调”指的是一个持续增长的计数器或序列。这些定时器不是持久的,因为它们在每次启动后都会重置。

表格 1 列出了一些单调定时器以及每个定时器的简短定义,同时有 OnCalendar 定时器,这些不是单调的,它们被用于指定未来有可能重复的某个确定时间。这个信息来自于 systemd.timer 的手册页,有一些不重要的修改。

定时器单调性定义
OnActiveSec=X定义了一个与定时器被激活的那一刻相关的定时器。
OnBootSec=X定义了一个与机器启动时间相关的计时器。
OnStartupSec=X定义了一个与服务管理器首次启动相关的计时器。对于系统定时器来说,这个定时器与 OnBootSec= 类似,因为系统服务管理器在机器启动后很短的时间后就会启动。当以在每个用户服务管理器中运行的单元进行配置时,它尤其有用,因为用户的服务管理器通常在首次登录后启动,而不是机器启动后。
OnUnitActiveSec=X定义了一个与将要激活的定时器上次激活时间相关的定时器。
OnUnitInactiveSec=X定义了一个与将要激活的定时器上次停用时间相关的定时器。
OnCalendar= 定义了一个有日期事件表达式语法的实时(即时钟)定时器。查看 systemd.time(7) 的手册页获取更多与日历事件表达式相关的语法信息。除此以外,它的语义和 OnActiveSec= 类似。

Table 1: systemd 定时器定义

单调计时器可使用同样的简写名作为它们的时间跨度,即我们之前提到的 AccuracySec 表达式,但是 systemd 将这些名字统一转换成了秒。举个例子,比如你想规定某个定时器在系统启动后五天触发一次事件;它可能看起来像 OnBootSec=5d。如果机器启动于 2020-06-15 09:45:27,这个定时器会在 2020-06-20 09:45:27 或在这之后的一分钟内触发。

日历事件格式

日历事件格式是定时器在所需的重复时间触发的关键。我们开始看下一些 OnCalendar 设置一起使用的格式。

与 crontab 中的格式相比,systemd 及其计时器使用的时间和日历格式风格不同。它比 crontab 更为灵活,而且可以使用类似 at 命令的方式允许模糊的日期和时间。它还应该足够熟悉使其易于理解。

systemd 定时器使用 OnCalendar= 的基础格式是 DOW YYYY-MM-DD HH:MM:SS。DOW(星期几)是选填的,其他字段可以用一个星号(*)来匹配此位置的任意值。所有的日历时间格式会被转换成标准格式。如果时间没有指定,它会被设置为 00:00:00。如果日期没有指定但是时间指定了,那么下次匹配的时间可能是今天或者明天,取决于当前的时间。月份和星期可以使用名称或数字。每个单元都可以使用逗号分隔的列表。单元范围可以在开始值和结束值之间用 .. 指定。

指定日期有一些有趣的选项,波浪号(~)可以指定月份的最后一天或者最后一天之前的某几天。/ 可以用来指定星期几作为修饰符。

这里有几个在 OnCalendar 表达式中使用的典型时间格式例子。

日期事件格式描述
DOW YYYY-MM-DD HH:MM:SS
*-*-* 00:15:30每年每月每天的 0 点 15 分 30 秒
Weekly每个周一的 00:00:00
Mon *-*-* 00:00:00同上
Mon同上
Wed 2020-*-*2020 年每个周三的 00:00:00
Mon..Fri 2021-*-*2021 年的每个工作日(周一到周五)的 00:00:00
2022-6,7,8-1,15 01:15:002022 年 6、7、8 月的 1 到 15 号的 01:15:00
Mon *-05~03每年五月份的下个周一同时也是月末的倒数第三天
Mon..Fri *-08~04任何年份 8 月末的倒数第四天,同时也须是工作日
*-05~03/2五月末的倒数第三天,然后 2 天后再来一次。每年重复一次。注意这个表达式使用了波浪号(~)。
*-05-03/2五月的第三天,然后每两天重复一次直到 5 月底。注意这个表达式使用了破折号(-)。

Table 2: OnCalendar 事件时间格式例子

测试日历格式

systemd 提供了一个绝佳的工具用于检测和测试定时器中日历时间事件的格式。systemd-analyze calendar 工具解析一个时间事件格式,提供标准格式和其他有趣的信息,例如下次“经过”(即匹配)的日期和时间,以及距离下次触发之前大概时间。

首先,看看未来没有时间的日(注意 Next elapse 和 UTC 的时间会根据你当地时区改变):

[student@studentvm1 ~]$ systemd-analyze calendar 2030-06-17
  Original form: 2030-06-17                
Normalized form: 2030-06-17 00:00:00        
    Next elapse: Mon 2030-06-17 00:00:00 EDT
       (in UTC): Mon 2030-06-17 04:00:00 UTC
       From now: 10 years 0 months left    
[root@testvm1 system]#

现在添加一个时间,在这个例子中,日期和时间是当作无关的部分分开解析的:

[root@testvm1 system]# systemd-analyze calendar 2030-06-17 15:21:16
  Original form: 2030-06-17                
Normalized form: 2030-06-17 00:00:00        
    Next elapse: Mon 2030-06-17 00:00:00 EDT
       (in UTC): Mon 2030-06-17 04:00:00 UTC
       From now: 10 years 0 months left    

  Original form: 15:21:16                  
Normalized form: *-*-* 15:21:16            
    Next elapse: Mon 2020-06-15 15:21:16 EDT
       (in UTC): Mon 2020-06-15 19:21:16 UTC
       From now: 3h 55min left              
[root@testvm1 system]#

为了把日期和时间当作一个单元来分析,可以把它们包在引号里。你在定时器单元里 OnCalendar= 时间格式中使用的时候记得把引号去掉,否则会报错:

[root@testvm1 system]# systemd-analyze calendar "2030-06-17 15:21:16"
Normalized form: 2030-06-17 15:21:16        
    Next elapse: Mon 2030-06-17 15:21:16 EDT
       (in UTC): Mon 2030-06-17 19:21:16 UTC
       From now: 10 years 0 months left    
[root@testvm1 system]#

现在我们测试下 Table2 里的例子。我尤其喜欢最后一个:

[root@testvm1 system]# systemd-analyze calendar "2022-6,7,8-1,15 01:15:00"
  Original form: 2022-6,7,8-1,15 01:15:00
Normalized form: 2022-06,07,08-01,15 01:15:00
    Next elapse: Wed 2022-06-01 01:15:00 EDT
       (in UTC): Wed 2022-06-01 05:15:00 UTC
       From now: 1 years 11 months left
[root@testvm1 system]#

让我们看一个例子,这个例子里我们列出了时间表达式的五个经过时间。

[root@testvm1 ~]# systemd-analyze calendar --iterations=5 "Mon *-05~3"
  Original form: Mon *-05~3                
Normalized form: Mon *-05~03 00:00:00      
    Next elapse: Mon 2023-05-29 00:00:00 EDT
       (in UTC): Mon 2023-05-29 04:00:00 UTC
       From now: 2 years 11 months left    
       Iter. #2: Mon 2028-05-29 00:00:00 EDT
       (in UTC): Mon 2028-05-29 04:00:00 UTC
       From now: 7 years 11 months left    
       Iter. #3: Mon 2034-05-29 00:00:00 EDT
       (in UTC): Mon 2034-05-29 04:00:00 UTC
       From now: 13 years 11 months left    
       Iter. #4: Mon 2045-05-29 00:00:00 EDT
       (in UTC): Mon 2045-05-29 04:00:00 UTC
       From now: 24 years 11 months left    
       Iter. #5: Mon 2051-05-29 00:00:00 EDT
       (in UTC): Mon 2051-05-29 04:00:00 UTC
       From now: 30 years 11 months left    
[root@testvm1 ~]#

这些应该为你提供了足够的信息去开始测试你的 OnCalendar 时间格式。systemd-analyze 工具可用于其他有趣的分析,我会在这个系列的下一篇文章来探索这些。

总结

systemd 定时器可以用于执行和 cron 工具相同的任务,但是通过按照日历和单调时间格式去触发事件的方法提供了更多的灵活性。

虽然你为此次实验创建的服务单元通常是由定时器调用的,你也可以随时使用 systemctl start myMonitor.service 命令去触发它。可以在一个定时器中编写多个维护任务的脚本;它们可以是 Bash 脚本或者其他 Linux 程序。你可以通过触发定时器来运行所有的脚本来运行服务,也可以按照需要执行单独的脚本。

我会在下篇文章中更加深入的探索 systemd 时间格式的用处。

我还没有看到任何迹象表明 cron 和 at 将被废弃。我希望这种情况不会发生,因为至少 at 在执行一次性调度任务的时候要比 systemd 定时器容易的多。

参考资料

网上有大量的关于 systemd 的参考资料,但是大部分都有点简略、晦涩甚至有误导性。除了本文中提到的资料,下列的网页提供了跟多可靠且详细的 systemd 入门信息。

  • Fedora 项目有一篇切实好用的 systemd 入门,它囊括了几乎所有你需要知道的关于如何使用 systemd 配置、管理和维护 Fedora 计算机的信息。
  • Fedora 项目也有一个不错的 备忘录,交叉引用了过去 SystemV 命令和 systemd 命令做对比。
  • 关于 systemd 的技术细节和创建这个项目的原因,请查看 Freedesktop.org 上的 systemd 描述。
  • Linux.com 的“更多 systemd 的乐趣”栏目提供了更多高级的 systemd 信息和技巧。

此外,还有一系列深度的技术文章,是由 systemd 的设计者和主要实现者 Lennart Poettering 为 Linux 系统管理员撰写的。这些文章写于 2010 年 4 月至 2011 年 9 月间,但它们现在和当时一样具有现实意义。关于 systemd 及其生态的许多其他好文章都是基于这些文章:


via: https://opensource.com/article/20/7/systemd-timers

作者:David Both 选题:lujun9972 译者:tt67wq 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

继续 systemd 教程,这些特殊的例子可以展示给你如何更好的利用 systemd 定时器单元。

在这个 systemd 系列教程中,我们已经在某种程度上讨论了 systemd 定时器单元。不过,在我们开始讨论 sockets 之前,我们先来看三个例子,这些例子展示了如何最佳化利用这些单元。

简单的类 cron 行为

我每周都要去收集 Debian popcon 数据,如果每次都能在同一时间收集更好,这样我就能看到某些应用程序的下载趋势。这是一个可以使用 cron 任务来完成的典型事例,但 systemd 定时器同样能做到:

# 类 cron 的 popcon.timer

[Unit]
Description= 这里描述了下载并处理 popcon 数据的时刻

[Timer]
OnCalendar= Thu *-*-* 05:32:07
Unit= popcon.service

[Install]
WantedBy= basic.target

实际的 popcon.service 会执行一个常规的 wget 任务,并没有什么特别之处。这里的新内容是 OnCalendar= 指令。这个指令可以让你在一个特定日期的特定时刻来运行某个服务。在这个例子中,Thu 表示 “在周四运行”,*-*-* 表示“具体年份、月份和日期无关紧要”,这些可以翻译成 “不管年月日,只在每周四运行”。

这样,你就设置了这个服务的运行时间。我选择在欧洲中部夏令时区的上午 5:30 左右运行,那个时候服务器不是很忙。

如果你的服务器关闭了,而且刚好错过了每周的截止时间,你还可以在同一个计时器中使用像 anacron 一样的功能。

# 具备类似 anacron 功能的 popcon.timer

[Unit]
Description= 这里描述了下载并处理 popcon 数据的时刻

[Timer]
Unit=popcon.service
OnCalendar=Thu *-*-* 05:32:07
Persistent=true

[Install]
WantedBy=basic.target

当你将 Persistent= 指令设为真值时,它会告诉 systemd,如果服务器在本该它运行的时候关闭了,那么在启动后就要立刻运行服务。这意味着,如果机器在周四凌晨停机了(比如说维护),一旦它再次启动后,popcon.service 将会立刻执行。在这之后,它的运行时间将会回到例行性的每周四早上 5:32.

到目前为止,就是这么简单直白。

延迟执行

但是,我们提升一个档次,来“改进”这个基于 systemd 的监控系统。你应该记得,当你接入摄像头的时候,系统就会开始拍照。假设你并不希望它在你安装摄像头的时候拍下你的脸。你希望将拍照服务的启动时间向后推迟一两分钟,这样你就有时间接入摄像头,然后走到画框外面。

为了完成这件事,首先你要更改 Udev 规则,将它指向一个定时器:

ACTION=="add", SUBSYSTEM=="video4linux", ATTRS{idVendor}=="03f0", 
ATTRS{idProduct}=="e207", TAG+="systemd", ENV{SYSTEMD_WANTS}="picchanged.timer", 
SYMLINK+="mywebcam", MODE="0666"

这个定时器看起来像这样:

# picchanged.timer

[Unit]
Description= 在摄像头接入的一分钟后,开始运行 picchanged

[Timer]
OnActiveSec= 1 m
Unit= picchanged.path

[Install]
WantedBy= basic.target

在你接入摄像头后,Udev 规则被触发,它会调用定时器。这个定时器启动后会等上一分钟(OnActiveSec= 1 m),然后运行 picchanged.path,它会监视主图片的变化。picchanged.path 还会负责接触 webcan.service,这个实际用来拍照的服务。

在每天的特定时刻启停 Minetest 服务器

在最后一个例子中,我们认为你决定用 systemd 作为唯一的依赖。讲真,不管怎么样,systemd 差不多要接管你的生活了。为什么不拥抱这个必然性呢?

你有个为你的孩子设置的 Minetest 服务。不过,你还想要假装关心一下他们的教育和成长,要让他们做作业和家务活。所以你要确保 Minetest 只在每天晚上的一段时间内可用,比如五点到七点。

这个跟之前的“在特定时间启动服务”不太一样。写个定时器在下午五点启动服务很简单…:

# minetest.timer

[Unit]
Description= 在每天下午五点运行 minetest.service

[Timer]
OnCalendar= *-*-* 17:00:00
Unit= minetest.service

[Install]
WantedBy= basic.target

…可是编写一个对应的定时器,让它在特定时刻关闭服务,则需要更大剂量的横向思维。

我们从最明显的东西开始 —— 设置定时器:

# stopminetest.timer

[Unit]
Description= 每天晚上七点停止 minetest.service

[Timer]
OnCalendar= *-*-* 19:05:00
Unit= stopminetest.service

[Install]
WantedBy= basic.target

这里棘手的部分是如何去告诉 stopminetest.service 去 —— 你知道的 —— 停止 Minetest. 我们无法从 minetest.service 中传递 Minetest 服务器的 PID. 而且 systemd 的单元词汇表中也没有明显的命令来停止或禁用正在运行的服务。

我们的诀窍是使用 systemd 的 Conflicts= 指令。它和 systemd 的 Wants= 指令类似,不过它所做的事情正相反。如果你有一个 b.service 单元,其中包含一个 Wants=a.service 指令,在这个单元启动时,如果 a.service 没有运行,则 b.service 会运行它。同样,如果你的 b.service 单元中有一行写着 Conflicts= a.service,那么在 b.service 启动时,systemd 会停止 a.service.

这种机制用于两个服务在尝试同时控制同一资源时会发生冲突的场景,例如当两个服务要同时访问打印机的时候。通过在首选服务中设置 Conflicts=,你就可以确保它会覆盖掉最不重要的服务。

不过,你会在一个稍微不同的场景中来使用 Conflicts=. 你将使用 Conflicts= 来干净地关闭 minetest.service:

# stopminetest.service

[Unit]
Description= 关闭 Minetest 服务
Conflicts= minetest.service

[Service]
Type= oneshot
ExecStart= /bin/echo "Closing down minetest.service"

stopminetest.service 并不会做特别的东西。事实上,它什么都不会做。不过因为它包含那行 Conflicts=,所以在它启动时,systemd 会关掉 minetest.service.

在你完美的 Minetest 设置中,还有最后一点涟漪:你下班晚了,错过了服务器的开机时间,可当你开机的时候游戏时间还没结束,这该怎么办?Persistent= 指令(如上所述)在错过开始时间后仍然可以运行服务,但这个方案还是不行。如果你在早上十一点把服务器打开,它就会启动 Minetest,而这不是你想要的。你真正需要的是一个确保 systemd 只在晚上五到七点启动 Minetest 的方法:

# minetest.timer

[Unit]
Description= 在下午五到七点内的每分钟都运行 minetest.service

[Timer]
OnCalendar= *-*-* 17..19:*:00
Unit= minetest.service

[Install]
WantedBy= basic.target

OnCalendar= *-*-* 17..19:*:00 这一行有两个有趣的地方:(1) 17..19 并不是一个时间点,而是一个时间段,在这个场景中是 17 到 19 点;以及,(2) 分钟字段中的 * 表示服务每分钟都要运行。因此,你会把它读做 “在下午五到七点间的每分钟,运行 minetest.service”

不过还有一个问题:一旦 minetest.service 启动并运行,你会希望 minetest.timer 不要再次尝试运行它。你可以在 minetest.service 中包含一条 Conflicts= 指令:

# minetest.service

[Unit]
Description= 运行 Minetest 服务器
Conflicts= minetest.timer

[Service]
Type= simple
User= <your user name>

ExecStart= /usr/bin/minetest --server
ExecStop= /bin/kill -2 $MAINPID

[Install]
WantedBy= multi-user.targe

上面的 Conflicts= 指令会保证在 minstest.service 成功运行后,minetest.timer 就会立即停止。

现在,启用并启动 minetest.timer:

systemctl enable minetest.timer
systemctl start minetest.timer

而且,如果你在六点钟启动了服务器,minetest.timer 会启用;到了五到七点,minetest.timer 每分钟都会尝试启动 minetest.service。不过,一旦 minetest.service 开始运行,systemd 会停止 minetest.timer,因为它会与 minetest.service “冲突”,从而避免计时器在服务已经运行的情况下还会不断尝试启动服务。

在首先启动某个服务时杀死启动它的计时器,这么做有点反直觉,但它是有效的。

总结

你可能会认为,有更好的方式来做上面这些事。我在很多文章中看到过“过度设计”这个术语,尤其是在用 systemd 定时器来代替 cron 的时候。

但是,这个系列文章的目的不是为任何具体问题提供最佳解决方案。它的目的是为了尽可能多地使用 systemd 来解决问题,甚至会到荒唐的程度。它的目的是展示大量的例子,来说明如何利用不同类型的单位及其包含的指令。我们的读者,也就是你,可以从这篇文章中找到所有这些的可实践范例。

尽管如此,我们还有一件事要做:下回中,我们会关注 sockets 和 targets,然后我们将完成对 systemd 单元的介绍。

你可以在 Linux 基金会和 edX 中,通过免费的 Linux 介绍课程中,学到更多关于 Linux 的知识。


via: https://www.linux.com/blog/intro-to-linux/2018/8/systemd-timers-two-use-cases-0

作者:Paul Brown 选题:lujun9972 译者:StdioA 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

学习使用 systemd 创建启动你的游戏服务器的定时器。

之前,我们看到了如何手动的、在开机与关机时、在启用某个设备时、在文件系统发生改变时 启用与禁用 systemd 服务。

定时器增加了另一种启动服务的方式,基于……时间。尽管与定时任务很相似,但 systemd 定时器稍微地灵活一些。让我们看看它是怎么工作的。

“定时运行”

让我们展开本系列前两篇文章中你所设置的 Minetest 服务器作为如何使用定时器单元的第一个例子。如果你还没有读过那几篇文章,可以现在去看看。

你将通过创建一个定时器来“改进” Minetest 服务器,使得在服务器启动 1 分钟后运行游戏服务器而不是立即运行。这样做的原因可能是,在启动之前可能会用到其他的服务,例如发邮件给其他玩家告诉他们游戏已经准备就绪,你要确保其他的服务(例如网络)在开始前完全启动并运行。

最终,你的 minetest.timer 单元看起来就像这样:

# minetest.timer
[Unit]
Description=Runs the minetest.service 1 minute after boot up

[Timer]
OnBootSec=1 m
Unit=minetest.service

[Install]
WantedBy=basic.target

一点也不难吧。

如以往一般,开头是 [Unit] 和一段描述单元作用的信息,这儿没什么新东西。[Timer] 这一节是新出现的,但它的作用不言自明:它包含了何时启动服务,启动哪个服务的信息。在这个例子当中,OnBootSec 是告诉 systemd 在系统启动后运行服务的指令。

其他的指令有:

  • OnActiveSec=,告诉 systemd 在定时器启动后多长时间运行服务。
  • OnStartupSec=,同样的,它告诉 systemd 在 systemd 进程启动后多长时间运行服务。
  • OnUnitActiveSec=,告诉 systemd 在上次由定时器激活的服务启动后多长时间运行服务。
  • OnUnitInactiveSec=,告诉 systemd 在上次由定时器激活的服务停用后多长时间运行服务。

继续 minetest.timer 单元,basic.target 通常用作 后期引导服务 late boot services 的 同步点 synchronization point 。这就意味着它可以让 minetest.timer 单元运行在安装完 本地挂载点 local mount points 或交换设备,套接字、定时器、路径单元和其他基本的初始化进程之后。就像在第二篇文章中 systemd 单元里解释的那样,targets 就像 旧的运行等级 old run levels 一样,可以将你的计算机置于某个状态,或像这样告诉你的服务在达到某个状态后开始运行。

在前两篇文章中你配置的 minetest.service 文件最终看起来就像这样:

# minetest.service
[Unit]
Description= Minetest server
Documentation= https://wiki.minetest.net/Main_Page

[Service]
Type= simple
User=

ExecStart= /usr/games/minetest --server
ExecStartPost= /home//bin/mtsendmail.sh "Ready to rumble?" "Minetest Starting up"

TimeoutStopSec= 180
ExecStop= /home//bin/mtsendmail.sh "Off to bed. Nightie night!" "Minetest Stopping in 2 minutes"
ExecStop= /bin/sleep 120
ExecStop= /bin/kill -2 $MAINPID

[Install]
WantedBy= multi-user.target

这儿没什么需要修改的。但是你需要将 mtsendmail.sh(发送你的 email 的脚本)从:

#!/bin/bash
# mtsendmail
sleep 20
echo $1 | mutt -F /home/<username>/.muttrc -s "$2" my_minetest@mailing_list.com
sleep 10

改成:

#!/bin/bash
# mtsendmail.sh
echo $1 | mutt -F /home/paul/.muttrc -s "$2" [email protected]

你做的事是去除掉 Bash 脚本中那些蹩脚的停顿。Systemd 现在来做等待。

让它运行起来

确保一切运作正常,禁用 minetest.service:

sudo systemctl disable minetest

这使得系统启动时它不会一同启动;然后,相反地,启用 minetest.timer:

sudo systemctl enable minetest.timer

现在你就可以重启服务器了,当运行 sudo journalctl -u minetest.* 后,你就会看到 minetest.timer 单元执行后大约一分钟,minetest.service 单元开始运行。

 title=

图 1:minetest.timer 运行大约 1 分钟后 minetest.service 开始运行

时间的问题

minetest.timer 在 systemd 的日志里显示的启动时间为 09:08:33 而 minetest.service 启动时间是 09:09:18,它们之间少于 1 分钟,关于这件事有几点需要说明一下:首先,请记住我们说过 OnBootSec= 指令是从引导完成后开始计算服务启动的时间。当 minetest.timer 的时间到来时,引导已经在几秒之前完成了。

另一件事情是 systemd 给自己设置了一个 误差幅度 margin of error (默认是 1 分钟)来运行东西。这有助于在多个 资源密集型进程 resource-intensive processes 同时运行时分配负载:通过分配 1 分钟的时间,systemd 可以等待某些进程关闭。这也意味着 minetest.service 会在引导完成后的 1~2 分钟之间启动。但精确的时间谁也不知道。

顺便一提,你可以用 AccuracySec= 指令修改误差幅度。

你也可以检查系统上所有的定时器何时运行或是上次运行的时间:

systemctl list-timers --all

 title=

图 2:检查定时器何时运行或上次运行的时间

最后一件值得思考的事就是你应该用怎样的格式去表示一段时间。Systemd 在这方面非常灵活:2 h,2 hours 或 2hr 都可以用来表示 2 个小时。对于“秒”,你可以用 seconds,second,sec 和 s。“分”也是同样的方式:minutes,minute,min 和 m。你可以检查 man systemd.time 来查看 systemd 能够理解的所有时间单元。

下一次

下次你会看到如何使用日历中的日期和时间来定期运行服务,以及如何通过组合定时器与设备单元在插入某些硬件时运行服务。

回头见!

在 Linux 基金会和 edx 上通过免费课程 “Introduction to Linux” 学习更多关于 Linux 的知识。


via: https://www.linux.com/blog/learn/intro-to-linux/2018/7/setting-timer-systemd-linux

作者:Paul Brown 选题:lujun9972 译者:LuuMing 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我最近在写一些执行备份工作的脚本,我决定使用systemd timers而不是对我而已更熟悉的cron jobs来管理它们。

在我使用时,出现了很多问题需要我去各个地方找资料,这个过程非常麻烦。因此,我想要把我目前所做的记录下来,方便自己的记忆,也方便读者不必像我这样,满世界的找资料了。

在我下面提到的步骤中有其他的选择,但是这里是最简单的方法。在此之前,请查看systemd.service, systemd.timer,和systemd.target的帮助页面(man),学习你能用它们做些什么。

运行一个简单的脚本

假设你有一个脚本叫:/usr/local/bin/myscript ,你想要每隔一小时就运行一次。

Service 文件

第一步,创建一个service文件,根据你Linux的发行版本放到相应的系统目录(在Arch中,这个目录是/etc/systemd/system/ 或 /usr/lib/systemd/system)

myscript.service

[Unit]
Description=MyScript

[Service]
Type=simple
ExecStart=/usr/local/bin/myscript

注意,务必将Type变量的值设置为"simple"而不是"oneshot"。使用"oneshot"使得脚本只在第一次运行,之后系统会认为你不想再次运行它,从而关掉我们接下去创建的定时器(Timer)。

Timer 文件

第二步,创建一个timer文件,把它放在第一步中service文件放置的目录。

myscript.timer

[Unit]
Description=Runs myscript every hour

[Timer]
# 首次运行要在启动后10分钟后 
OnBootSec=10min
# 每次运行间隔时间
OnUnitActiveSec=1h
Unit=myscript.service

[Install]
WantedBy=multi-user.target

授权 / 运行

授权并运行的是timer文件,而不是service文件。

# 以 root 身份启动定时器
systemctl start myscript.timer
# 在系统引导起来后就启用该定时器 
systemctl enable myscript.timer

在同一个Timer上运行多个脚本

现在我们假设你在相同时间想要运行多个脚本。这种情况,你需要在上面的文件中做适当的修改。

Service 文件

像我之前说过的那样创建你的service文件来运行你的脚本,但是在每个service 文件最后都要包含下面的内容:

[Install]
WantedBy=mytimer.target

如果在你的service 文件中有一些依赖顺序,确保你使用Description字段中的值具体指定After=something.service和Before=whatever.service中的参数。

另外的一种选择是(或许更加简单),创建一个包装脚本来使用正确的顺序来运行命令,并在你的service文件中使用这个脚本。

Timer 文件

你只需要一个timer文件,创建mytimer.timer,像我在上面指出的。

target 文件

你可以创建一个以上所有的脚本依赖的target文件。

mytimer.target

[Unit]
Description=Mytimer
# Lots more stuff could go here, but it's situational.
# Look at systemd.unit man page.

授权 / 启动

你需要将所有的service文件和timer文件授权。

systemctl enable script1.service
systemctl enable script2.service
...
systemctl enable mytimer.timer
systemctl start mytimer.service

Good luck.


via: http://jason.the-graham.com/2013/03/06/how-to-use-systemd-timers/

作者:Jason Graham 译者:johnhoow 校对:wxy

本文由 LCTT 原创翻译,Linux中国 荣誉推出