Skip to content

Latest commit

 

History

History
1359 lines (1018 loc) · 69.8 KB

File metadata and controls

1359 lines (1018 loc) · 69.8 KB

十三、定时器

定时器让我们能够设置一个工件,一旦指定的时间到期,操作系统就会通知我们,这是一个无处不在的应用(甚至是内核)功能。 当然,计时器通常只有在与应用逻辑并行运行时才有用;这种异步通知行为是通过不同的方式实现的,通常是通过让内核向相关进程发送信号来实现的。

在本章中,我们将探索 Linux 上可用于设置和使用计时器的接口。 这些接口分为两大类-较旧的 API(alarm(2)[get|set]itimer(2))和闪亮、较新的 POSIX API(timer_create(2)timer_[set|get]time(2),依此类推*)*。 当然,由于信号与计时器一起大量使用,我们也利用了信号接口。

我们还想指出,由于计时器的内在动态特性,在书中静态查看我们的示例程序的输出是不够的;像往常一样,我们肯定会敦促读者克隆本书的 GitHub 存储库并亲自尝试代码。

在本章中,读者将学习如何使用 Linux 内核提供的各种定时器接口(API)。 我们从较旧的版本开始,尽管它们有局限性,但随着需要的增加,它们仍然在系统软件中大量使用。 使用这些 API 编写和分析一个简单的命令行界面(CLI)专用的数字时钟程序。 然后,我们将读者转到更新、功能更强大的 POSIX 计时器 API 集。 我们展示并研究了两个非常有趣的示例程序--一个“你能有多快反应”游戏和一个跑步-步行间隔计时器应用。 最后,我们简要介绍一下通过文件抽象使用计时器 API,以及什么是看门狗计时器。

较旧的接口

如前所述,较旧的接口包括以下接口:

  • alarm(2)系统调用
  • 间隔计时器[get|set]itimer(2)系统调用 API

让我们从第一个开始。

好久不见的闹钟

alarm(2)系统调用允许进程设置简单的超时机制;其签名如下:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

事实上,这是不言而喻的。 让我们举一个简单的例子:一个进程想要设置一个计时器,该计时器将在三秒后到期,因此alarm(3)实质上是用于执行此操作的代码。

上述代码中到底发生了什么? 在发出警报和系统调用的三秒后(即,在定时器被激活之后),内核将向进程发送信号SIGALRM

The default action of SIGALRM (signal # 14 on x86) is to terminate the process.

因此,我们希望开发人员捕获信号(通过sigaction(2)系统调用)将是最好的,正如前面的第 11 章Signating-Part I第 12 章Signating-II中深入讨论的那样。

如果告警接口的参数输入为0,则取消任何挂起的alarm(2)(实际上,在调用告警接口时,任何情况下都会发生这种情况)。

请注意,对于系统调用而言,异常的 Alarm API 返回一个无符号整数(因此无法返回-1,这是常见的失败情况)。 相反,它返回任何先前编程超时的秒数,如果没有挂起,则返回零。

下面是一个演示alarm(2)基本用法的简单程序(ch13/alarm1.c);该参数指定超时的秒数。

For readability, only the key parts of the source code are displayed in the following; to view the complete source code, build it, and run it, the entire tree is available for cloning from GitHub here: https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux.

信号捕获和定时器报警代码如下所示:

[...]
/* Init sigaction to defaults via the memset,
 * setup 'sig_handler' as the signal handler function,
 * trap just the SIGALRM signal.
 */
 memset(&act, 0, sizeof(act));
 act.sa_handler = sig_handler;
 if (sigaction(SIGALRM, &act, 0) < 0)
     FATAL("sigaction on SIGALRM failed");

 alarm(n);
 printf("A timeout for %ds has been armed...\n", n);
 pause(); /* wait for the signal ... */

一旦内核将SIGALRM信号分派给进程,即一旦计时器超时,会发生什么情况? 当然,信号处理程序会运行。 这里是:

static void sig_handler(int signum)
{
    const char *str = " *** Timeout! [SIGALRM received] ***\n";
    if (signum != SIGALRM)
        return;
    if (write(STDOUT_FILENO, str, strlen(str)) < 0)
        WARN("write str failed!");
}

下面是一个快速构建和测试运行:

$ make alarm1
gcc -Wall -UDEBUG -c ../common.c -o common.o
gcc -Wall -UDEBUG -c alarm1.c -o alarm1.o
gcc -Wall -UDEBUG -o alarm1 alarm1.o common.o 
$ ./alarm1 
Usage: ./alarm1 seconds-to-timeout(>0)
$ ./alarm1 3
A timeout for 3s has been armed...
    *** Timeout! [SIGALRM received] ***            *<< 3 seconds later! >>*
$ 

现在,我们增强了前面的代码(ch13/alarm1.c),使超时持续重复(源文件是ch13/alarm2_rep.c);相关的代码片段(与前面的代码相比已更改)如下所示:

[...]
alarm(n);
printf("A timeout for %ds has been armed...\n", n);
/* (Manually) re-invoke the alarm every 'n' seconds */
while (1) {
    pause(); /* wait for the signal ... */
    alarm(n);
    printf(" Timeout for %ds has been (re)armed...\n", n);
}
[...]

虽然在这里不适用,但要知道调用alarm(2)会自动取消任何先前挂起的超时。 快速试运行如下:

$ ./alarm2_rep 1
A timeout for 1s has been armed...
 *** Timeout! [SIGALRM received] ***
 Timeout for 1s has been (re)armed...
 *** Timeout! [SIGALRM received] ***
 Timeout for 1s has been (re)armed...
 *** Timeout! [SIGALRM received] ***
 Timeout for 1s has been (re)armed...
 *** Timeout! [SIGALRM received] ***
 Timeout for 1s has been (re)armed...
^C
$ 

警报现在重复(在上面的示例运行中的每一秒)。 还要注意我们是如何用键盘Ctrl+C(传递SIGINT,因为我们没有捕获它,它只是终止前台进程)来终止进程的。

Alarm API-降落器

现在我们已经了解了如何使用(简单化)的alarm(2)API,重要的是要认识到它有几个缺点:

  • 非常粗粒度的超时(最少一秒,这在现代处理器上是非常长的时间!)
  • 不可能并行运行多个超时
  • 以后无法查询或修改超时值-尝试这样做将取消超时值
  • 混用以下接口可能会导致问题/冲突(下文中,后一种接口可能会使用前者在内部实现)
    • alarm(2)``alarm(2)setitimer(2)
    • alarm(2)``alarm(2)sleep(3)
  • 超时总是有可能晚于预期(超时)

随着本章的进展,我们将发现更强大的功能可以克服大多数这些问题。 (公平地说,穷人alarm(2)确实有一个优点:出于简单的目的,它非常快速且易于使用!)

间隔计时器

间隔定时器 API 允许进程设置和查询定时器,该定时器可以被编程为以固定的时间间隔自动重现。 相关的系统调用如下:

#include <sys/time.h>
int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,
                struct itimerval *old_value);

很明显,参数setitimer(2)用于设置新的计时器;参数getitimer(2)用于查询,并返回剩余时间。

两者的第一个参数是which-它指定要使用的计时器类型。Linux 允许我们使用三种类型的间隔计时器:

  • ITIMER_REAL:使用此计时器类型进行实时倒计时,也称为挂钟时间。 在计时器超时时,内核向调用进程发送信号SIGALRM
  • ITIMER_VIRTUAL:使用此计时器类型以虚拟时间进行倒计时;也就是说,计时器仅在调用进程(所有线程)在 CPU 上的用户空间中运行时才进行倒计时。 在计时器超时时,内核向调用进程发送信号SIGVTALRM
  • ITIMER_PROF:使用此计时器类型也可以在虚拟时间内倒计时;这一次,当调用进程(所有线程)在 CPU 的用户空间和/或内核空间中运行时,计时器也会倒计时。 在计时器超时时,内核向调用进程发送信号SIGPROF

因此,要让计时器在特定时间段到期时到期,请使用第一种计时器;可以使用剩下的两种类型来分析进程的 CPU 使用情况。 一次只能使用上述每种类型的一个计时器(更多信息将在后面介绍)。

下一个要检查的参数是itimerval数据结构(及其内部成员timeval和结构成员;两者都在time.h标题中定义):

struct itimerval {
    struct timeval it_interval;    /* Interval for periodic timer */
    struct timeval it_value;       /* Time until next expiration */
};

struct timeval {
    time_t      tv_sec;            /* seconds */
    suseconds_t tv_usec;           /* microseconds */
};

(仅供参考,内部类型定义time_t和类型定义suseconds_t都会转换为长整型(整数)值。)

正如我们看到的,这是setitimer(2)的第二个参数,它是一个指向 structitimerval的指针,称为new_value,它是我们指定新计时器的到期时间的地方,例如:

  • 在成员的it_value结构中,放置第一个初始超时值。 该值随着计时器用完而减小,并且在某个时刻将达到零;此时,与计时器类型相对应的适当信号将被传递到调用进程。
  • 在上一步之后,检查it_interval结构和成员。 如果它是非零值,则该值将被复制到it_value结构中,从而使计时器有效地自动重置并在该时间量内再次运行;换句话说,这就是 API 履行间隔计时器角色的方式。

此外,显然,超时时间以秒表示:微秒。

例如,如果我们希望每秒重复(间隔)超时,则需要按如下方式初始化结构:

struct itimerval mytimer;
memset(&mytimer, 0, sizeof(struct itimerval));
mytimer.it_value.tv_sec = 1;
mytimer.it_interval.tv_sec = 1;
setitimer(ITIMER_REAL, &mytimer, 0);

(为清楚起见,前面的代码中没有显示错误检查代码。)。 这正是在下面的简单数字时钟演示程序中完成的。

存在几种特殊情况:

  • 要取消(或解除)计时器,请将it_timer结构的两个字段都设置为零,然后调用setitimer(2)API。
  • 要创建单次计时器(即正好过期一次的计时器),请将参数it_interval函数结构的两个字段初始化为零,然后调用函数setitimer(2)函数 API。
  • 如果setitimer(2)的第三个参数非空,则在此返回上一个计时器值(就像调用了getitmer(2)API 一样)。

通常,这对系统调用在成功时返回0,在失败时返回-1(适当设置了errno)。

由于每种类型的计时器到期时都会生成一个信号,因此在给定进程中只能同时运行每种计时器类型的一个实例。 如果我们尝试设置相同类型的多个计时器(例如,ITIMER_REAL),则始终有可能将同一信号的多个实例(在本例中为SIGALRM)同时传递给进程和相同的处理程序例程。 正如我们在第 11 章信号-第一部分第 12 章信号-第二部分中了解到的,常规 Unix 信号不能排队,因此可能会丢弃信号实例。 实际上,在给定进程中同时使用每种类型的计时器中的一种是最好(也是最安全的)。

下表对比了我们之前看到的简单的 alarm(2)个系统调用 API 和我们刚刚看到的功能更强大的[set|get]itimer(2)个间隔计时器 API:

| 功能 | 简单计时器[alarm(2)] | 间隔计时器[setitimer(2)getitimer(2)] | | 粒度(分辨率) | 非常粗糙;1 秒 | 精细粒度;理论上为 1 微秒(实际上,通常比 2.6.16 HRT[1]早几毫秒) | | 查询剩余时间 | 不可能 | 是的,有getitimer(2) | | 修改超时 | 不可能 | 肯定的回答 / 赞成 / 是 | | 取消超时超时 | 肯定的回答 / 赞成 / 是 | 肯定的回答 / 赞成 / 是 | | 自动重复 | 不能,但可以手动设置 | 肯定的回答 / 赞成 / 是 | | 多个计时器 | 不可能 | 可以,但每个进程最多三个-每种类型(真实、虚拟和分析)各一个 |

Table 1 : A quick comparison of the simple alarm(2)API and interval timers

[1]高分辨率定时器(HRT);在 Linux 2.6.16 及更高版本中实现。 在有关 GitHub 存储库的进一步阅读部分中,请参阅有关这方面的详细论文的链接。

没有应用的知识是什么? 让我们来试用一下间隔计时器 API。

一个简单的 CLI 数字时钟

我们人类已经习惯于看着时钟滴答滴答地流逝,一次一秒。 为什么不写一个快速的 C 程序来模仿一个(非常简单的命令行)数字时钟,它必须每秒显示正确的日期和时间! (嗯,就我个人而言,我更喜欢看到老式的模拟时钟,但是,嘿,这本书没有涉及用 X11 进行图形绘制的秘密咒语。)

我们如何实现这一点非常简单,真的:我们设置了一个时间间隔计时器,每秒钟超时一次。 下面的程序(ch13/intv_clksimple.c)演示了相当强大的setitimer(2)API 的基本用法。

For readability, only key parts of the source code are displayed in the following; to view the complete source code, build it, and run it, the entire tree is available for cloning from GitHub here: https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux.

单秒间隔定时器的信号捕获和设置如下所示:

static volatile sig_atomic_t opt;
[...]
int main(int argc, char **argv)
{
    struct sigaction act;
    struct itimerval mytimer;
[...]
    memset(&act, 0, sizeof(act));
    act.sa_handler = ticktock;
    sigfillset(&act.sa_mask); /* disallow all signals while handling */
    /*
    * We deliberately do *not* use the SA_RESTART flag;
    * if we do so, it's possible that any blocking syscall gets
    * auto-restarted. In a timeout context, we don't want that
    * to happen - we *expect* a signal to interrupt our blocking
    * syscall (in this case, the pause(2)).
    * act.sa_flags = SA_RESTART;
    */
    if (sigaction(SIGALRM, &act, 0) < 0)
        FATAL("sigaction on SIGALRM failed");
    /* Setup a single second (repeating) interval timer */
    memset(&mytimer, 0, sizeof(struct itimerval));
    mytimer.it_value.tv_sec = 1;
 mytimer.it_interval.tv_sec = 1;
    if (setitimer(ITIMER_REAL, &mytimer, 0) < 0)
        FATAL("setitimer failed\n");
    while (1)
        (void)pause();

请注意我们在处理提供超时的信号时通常不使用SA_RESTART标志的原因的不言而喻的注释。

设置间隔计时器很简单:我们初始化itimerval结构,以便将内部timeval结构的秒成员设置为1(我们只需将微秒设置为零),并发出setitimer(2)的系统调用。 计时器已经准备好了-它开始倒计时。 当经过一秒后,内核将向进程发送信号SIGALRM(因为计时器类型为ITIMER_REAL)。 信号处理例程:ticktock将执行获取并打印出当前时间戳的任务(参见其代码如下)。 当间隔组件设置为1时,计时器将每隔一秒自动重复触发一次。

static void ticktock(int signum)
{
    char tmstamp[128];
    struct timespec tm;
    int myerrno = errno;

    /* Query the timestamp ; both clock_gettime(2) and
     * ctime_r(3) are reentrant-and-signal-safe */
    if (clock_gettime(CLOCK_REALTIME, &tm) < 0)
        FATAL("clock_gettime failed\n");
    if (ctime_r(&tm.tv_sec, &tmstamp[0]) == NULL)
        FATAL("ctime_r failed\n");

    if (opt == 0) {
        if (write(STDOUT_FILENO, tmstamp, strlen(tmstamp)) < 0)
            FATAL("write failed\n");
    } else if (opt == 1) {
      /* WARNING! Using the printf / fflush here in a signal handler is
       * unsafe! We do so for the purposes of this demo app only; do not
       * use in production.
       */
        tmstamp[strlen(tmstamp) - 1] = '\0';
        printf("\r%s", tmstamp);
        fflush(stdout);
    }
    errno = myerrno;
}

前一个信号处理程序例程每秒调用一次(当然,内核在计时器到期时将信号SIGALRM传递给进程)。 该例程的任务很明确:它必须查询和打印当前日期-时间;即时间戳。

获取当前时间

乍一看,查询当前时间很简单。 许多高级程序员使用以下 API 序列来实现它:

time(2)
localtime(3)
strftime(3)

我们没有。 这是为什么? 回想一下我们在第 11 章信号-第一部分、(在重入安全和信号一节中)中关于异步信号安全(重入)功能的讨论。 在上述三个 API 中,只有第一个time(2)API 被认为是信号安全的;其他两个则不是(也就是说,它们不应该在信号处理程序内使用)。 相关手册页(signal-safety(7))证实了这一点。

因此,我们使用文档记录的异步信号安全 API--time(2)clock_gettime(2)ctime_r(3)--来执行安全获取时间戳的角色。 下面让我们快速瞥一眼它们。

第一个clock_gettime(2)系统调用的签名如下:

int clock_gettime(clockid_t clk_id, struct timespec *tp);

第一个参数是要使用的时钟源或时钟类型;事实上,Linux OS(和 glibc)支持许多不同的内置时钟类型;其中包括:

  • CLOCK_REALTIME:全系统挂钟报时(实时),查询时间戳。
  • CLOCK_MONOTONIC:单调的时钟只在一个方向上计数(显然是向上的;穿越时间的倒退是 MAD 仍在研究的一个特征,是吗?)。 科学家)。 它通常计算自系统启动以来经过的时间。
  • CLOCK_BOOTTIME(来自 Linux 2.6.39):这与 CLOCK_MONTONTON 基本相同,不同之处在于它考虑了系统挂起的时间。
  • CLOCK_PROCESS_CPUTIME_ID:给定进程的所有线程在 CPU 上花费的 CPU 时间的度量(通过 PID;使用clock_getcpuclockid(3)API 查询)。
  • CLOCK_THREAD_CPUTIME_ID:特定线程在 CPU 上花费的 CPU 时间的度量(使用pthread_getcpuclockid(3)API 进行查询)。

还有更多信息;有关详细信息,请参阅clock_gettime(2)上的手册页。 就我们目前的目的而言,我们将使用CLOCK_REALTIME

clock_gettime(2)的第二个参数是值-结果样式的参数;实际上,这是一个返回值。 成功返回后,它将在timeval结构中保存时间戳;该结构在time.h头中定义,并以秒和纳秒为单位保存当前时间戳:

struct timespec {
    time_t tv_sec; /* seconds */
    long tv_nsec; /* nanoseconds */
 };

我们会对秒内的价值相当满意的。

但是,以秒和纳秒为单位的这个值究竟是如何解释的呢? 实际上,这在 Unix 世界中非常常见:Unix 系统将时间存储为自 1970 年 1 月 1 日午夜(00:00)以来经过的秒数--可以将其视为 Unix 的诞生! 这个时间值被称为自大纪元以来的时间或 Unix 时间。好的,所以今天会有相当多的秒数,对吗? 那么如何用人类可读的格式来表达呢? 我们很高兴您这么问,因为这正是ctime_r(3)API 的职责所在:

char *ctime_r(const time_t *timep, char *buf);

第一个参数将是我们从clock_gettime(2)函数 API 返回的time_t成员(指针);同样,第二个参数是一个值结果样式返回-成功完成时,它将保存人类可读的时间戳! 请注意,应用程序员的工作是为缓冲区buf分配内存(随后根据需要释放它)。 在我们的代码中,我们只使用静态分配的本地缓冲区。 (当然,我们会对所有 API 执行错误检查。)

最后,根据opt值(由用户传递),我们可以使用(Safe)的write(2)系统调用,也可以使用(UnSafe!)printf(3)/fflush(3)的 API 来打印当前时间。

The code printf("\r%s", tmstamp); has the printf(3) using the \r format—this is the carriage return, which effectively brings the cursor back to the beginning of the same line. This gives the appearance of a clock constantly updating. This is nice, except for the fact that using printf(3)itself is signal-unsafe!

试运行

以下是一次试运行,首先使用 Signal-Safewrite(2)方法:

$ ./intv_clksimple
Usage: ./intv_clksimple {0|1}
 0 : the Correct way (using write(2) in the signal handler)
 1 : the *Wrong* way (using printf(3) in the signal handler) *@your risk*
$ ./intv_clksimple 0
Thu Jun 28 17:52:38 2018
Thu Jun 28 17:52:39 2018
Thu Jun 28 17:52:40 2018
Thu Jun 28 17:52:41 2018
Thu Jun 28 17:52:42 2018
^C
$ 

现在,这里有一个使用信号不安全的方法printf(3)/fflush(3)方法:

$ ./intv_clksimple 1
 *WARNING* [Using printf in signal handler]
Thu Jun 28 17:54:53 2018^C
$ 

它看起来更好,因为时间戳在同一行上不断刷新,但不安全。 亲爱的读者,这本书不能向你展示回车风格的令人愉快的效果printf("\r...")。你一定要在你的 Linux 系统上尝试一下,亲眼看看这一点。

We understand that using the printf(3) and fflush(3)APIs within a signal handler is bad programming practice—they are not async-signal safe.

But what if the low-level design specification demands that we use exactly these APIs? Well, there's always a way: why not redesign the program to use one of the synchronous blocking APIs to wait upon and catch signal(s) wherever appropriate (Remember, when trapping  fatal signals such as SIGILL, SIGFPE, SIGSEGV, and SIGBUS, it's recommended to use the usual async sigaction(2) API): the sigwait(3), sigwaitinfo(2), sigtimedwait(2) or even the signalfd(2) API (that we covered in Chapter 12Signaling - Part II, section Synchronously blocking for signals via the sigwait APIs*). We leave this as an exercise for the reader.

关于使用性能分析计时器的一句话

我们已经比较详细地研究了ITIMER_REAL计时器类型的用法-它是实时倒计时的。 使用其他两个计时器(ITIMER_VIRTUALITIMER_PROF)怎么样? 嗯,代码样式非常相似;没有什么新东西。 对于不熟悉这一点的开发人员来说,他们面临的问题是:信号似乎永远都不会到达!

让我们使用ITIMER_VIRTUAL计时器获取一个简单的代码片段:

static void profalrm(int signum)
{
    /* In production, do Not use signal-unsafe APIs like this! */
    printf("In %s:%d sig=%d\n", __func__, __LINE__, signum);
}

[...]

// in main() ...

struct sigaction act;
struct itimerval t1;

memset(&act, 0, sizeof(act));
act.sa_handler = profalrm;
sigfillset(&act.sa_mask); /* disallow all signals while handling */
if (sigaction(SIGPROF, &act, 0) < 0)
    FATAL("sigaction on SIGALRM failed");

[...]

memset(&t1, 0, sizeof(struct itimerval));
t1.it_value.tv_sec = 1;
t1.it_interval.tv_sec = 1;
if (setitimer(ITIMER_PROF, &t1, 0) < 0)
    FATAL("setitimer failed\n");

while (1)
    (void)pause();

运行时,不显示任何输出-计时器似乎不工作。

但事实并非如此-它还在工作,但问题是:进程只是通过pause(2)休眠。休眠时,它不在 CPU 上运行;因此,内核几乎没有递减(前面提到的,逐秒)间隔计时器! 请记住,当进程在 CPU 上时,ITIMER_VIRTUALITIMER_PROF计时器都只会递减(或倒计时)。 因此,一秒计时器不会实际超时,也不会发送SIGPROF信号。

因此,现在,解决前面问题的方法变得显而易见:让我们在程序中引入一些 CPU 处理并减少超时值。 我们值得信任的DELAY_LOOP_SILENT宏(请参见源文件common.h)使进程绕过了一些愚蠢的逻辑-关键是它变得 CPU 密集型。 此外,我们还减少了进程在 CPU 上花费的每 10 毫秒的计时器过期时间:

[...]
memset(&t1, 0, sizeof(struct itimerval));
t1.it_value.tv_sec = 0;
t1.it_value.tv_usec = 10000;       // 10,000 us = 10 ms
t1.it_interval.tv_sec = 0;
t1.it_interval.tv_usec = 10000;    // 10,000 us = 10 ms
if (setitimer(ITIMER_PROF, &t1, 0) < 0)
    FATAL("setitimer failed\n");

while (1) {
    DELAY_LOOP_SILENT(20);
    (void)pause();
}

这一次,在运行时,我们看到以下内容:

In profalrm:34 sig=27
In profalrm:34 sig=27
In profalrm:34 sig=27
In profalrm:34 sig=27
In profalrm:34 sig=27
...

分析计时器确实在工作。

较新的 POSIX(间隔)计时器机制

在本章的前面部分,我们在表 1 中看到:简单的**Alarm(2)API 和间隔计时器的快速比较表明,虽然间隔计时器[get|set]itimer(2)API 优于简单的alarm(2)API,但它们仍然缺乏重要的现代功能。 现代 POSIX(间隔)计时器机制解决了几个缺点,其中一些缺点如下:

  • 通过添加纳秒粒度的定时器(添加了独立于 Arch 的 HRT 机制,该机制已集成到 2.6.16 Linux 内核中),分辨率提高了 1000 倍。
  • 一种通用的处理定时器超时的机制--这是一种处理异步事件的方法,比如定时器超时(我们的用例)、AIO 请求完成、消息传递等等。 我们现在不必强制将计时器超时与信号机制捆绑在一起。
  • 重要的是,一个进程(或线程)现在可以设置和管理任意数量的计时器。
  • 嗯,归根结底,总有一个上限:在这种情况下,它是资源限制RLIMIT_SIGPENDING。 (更严格地说,事实是操作系统为每个创建的定时器分配一个排队的实时信号,因此这是一个限制。)

这些要点如下所示,请继续阅读。

典型的应用工作流

设置和使用现代 POSIX 定时器的设计方法(和使用的 API)如下;顺序通常如下所示:

  • 信号设置。
    • 假设正在使用的通知机制是一个信号,则首先通过sigaction(2)捕获该信号。
  • 创建并初始化计时器。
    • 确定用于测量已用时间的时钟类型(或源)。
    • 决定应用要使用的计时器到期事件通知机制-通常是使用(通常的)信号还是(新产生的)线程。
    • 上述决定是通过timer_create(2)的系统调用实现的;因此,它允许创建一个计时器,当然,我们也可以通过多次调用它来创建多个计时器。
  • 使用timer_settime(2)解除特定计时器的武装(或解除武装)。 设置计时器意味着有效地启动计时器运行-倒计时;解除计时器设置则相反-在轨道上停止计时器。
  • 要查询特定定时器中剩余(到到期)的时间(及其间隔设置),请使用timer_gettime(2)
  • 使用timer_getoverrun(2)检查给定定时器的超时计数。
  • 使用timer_delete(2)删除计时器(显然还会解除计时器的武装)。

创建和使用 POSIX(间隔)计时器

如前所述,我们使用功能强大的timer_create(2)系统调用为调用进程(或线程,就此而言)创建计时器:

#include <signal.h>
#include <time.h>
int timer_create(clockid_t clockid, struct sigevent *sevp,
                 timer_t *timerid);
Link with -lrt.

We have to link with the real time (rt) library to make use of this API. The librtlibrary implements the POSIX.1b Realtime Extensions to POSIX interfaces. Find a link to the librtman page in the *Further Reading *section on the GitHub repository.

传递给timer_create(2)的第一个参数通知操作系统要使用的时钟源;我们避免重复此问题,请读者参阅本章前面介绍的获取当前时间一节,其中列举了 Linux 中几个常用的时钟源。 (此外,如上所述,有关更多详细信息,请参阅clock_gettime(2)上的手册页。)

传递给timer_create(2)的第二个参数很有趣:它提供了一种通用方法来指定应用要使用的计时器过期事件通知机制! 要理解这一点,让我们来看一下sigevent的结构:

#include <signal.h>

union sigval {     /* Data passed with notification */
    int sival_int;      /* Integer value */
    void *sival_ptr;    /* Pointer value */
 };

struct sigevent {
    int sigev_notify;         /* Notification method */
    int sigev_signo;          /* Notification signal */
    union sigval sigev_value; /* Data passed with notification */
    void (*sigev_notify_function) (union sigval);
    /* Function used for thread notification (SIGEV_THREAD) */
    void *sigev_notify_attributes; /* Attributes for notification 
    thread(SIGEV_THREAD) */
    pid_t sigev_notify_thread_id;
             /* ID of thread to signal (SIGEV_THREAD_ID) */
 };

(回想一下,我们已经在第 11 章信号-第 I 部分第 12 章信号-第 II 部分中遇到并使用了union sigval机制将值传递给信号处理程序。)

下面列举了sigev_notify成员的有效值:

| 通知方式sigevent.sigev_notify | 含义 | | SIGEV_NONE | 事件到达时不执行任何操作-空通知 | | SIGEV_SIGNAL | 通过向进程发送sigev_signo成员中指定的信号来通知 | | SIGEV_THREAD | 通过调用(实际上是派生)函数为sigev_notify_function的(新)线程来通知,传递给它的参数是sigev_value,如果sigev_notify_attributes不为空,则应该是新线程的pthread_attr_t 结构。 (读者请注意,我们将在后续章节详细介绍多线程。) | | SIGEV_THREAD_ID | 特定于 Linux,用于指定在计时器超时时运行的内核线程;实际上,只有线程库才使用此功能。 |

Table 2 : Using the sigevent(7) mechanism

在第一种情况下,SIGEV_NONE,总是可以通过timer_gettime(2)API 手动检查计时器是否超时。

更有趣、更常见的情况是第二种情况,SIGEV_SIGNAL。 在这里,一个信号被传递给计时器已过期的进程;进程的sigaction(2)处理程序的siginfo_t数据结构被适当填充;对于我们的用例(使用 POSIX 计时器),如下所示:

  • si_code(或信号来源字段)设置为值SI_TIMER,以表示 POSIX 计时器已过期(请查看sigaction上手册页中的其他可能性)
  • si_signo设置为信号号(sigev_signo)
  • si_value这将是在联盟中设置的值sigev_value

出于我们的目的(至少在本章中),我们将只考虑将第一sigevent的通知类型设置为值SIGEV_SIGNAL的情况(从而设置要在第二sigev_signo成员中传递的信号)。

传递给timer_create(2)timer_t *timerid的第三个参数是一个(现在通用的)值结果样式的参数;它实际上是新创建的 POSIX 计时器的返回 ID! 当然,系统调用在失败时返回-1(相应地设置了errno),在成功时返回0。 参数timerid是定时器的句柄-我们通常将其作为后续 POSIX 定时器 API 中的参数传递,以指定要操作的特定定时器。

军备竞赛-POSIX 定时器的武装和解除武装

如前所述,我们使用timer_settime(2)的系统调用来解除(启动)或解除(停止)计时器:

#include <time.h>
int timer_settime(timer_t timerid, int flags,
                    const struct itimerspec *new_value,
                    struct itimerspec *old_value);
Link with -lrt.

由于可以有多个并发的 POSIX 计时器同时运行,因此我们需要准确地指定我们引用的是哪个计时器;这是通过第一个参数timer_id(计时器的 ID)和前面看到的系统调用的有效返回来完成的。

这里使用的重要数据结构是itimerspec;其定义如下:

struct timespec {
    time_t tv_sec; /* Seconds */
    long tv_nsec;  /* Nanoseconds */
};

struct itimerspec {
    struct timespec it_interval; /* Timer interval */
    struct timespec it_value;    /* Initial expiration */
};

因此,应该很清楚:在第三个参数中,有一个指向itimerspec结构的指针,名为new_value

  • 我们可以将达到(理论)分辨率的时间指定到单个纳秒! 请注意,时间是根据(timer_create(2)API 指定的时钟源测量的。

    • 这提醒我们,始终可以通过clock_getres(2)接口查询时钟分辨率。
  • 关于初始化it_value结构(timespec结构):

    • 将其设置为非零值可指定初始计时器超时值。
    • 将其设置为零,以指定我们正在解除计时器的武装(停止)。
    • 如果此结构已经具有正值,该怎么办? 然后它会被覆盖,计时器也会用新的值重新武装起来。
  • 不仅如此,通过将时间it_interval(timespec 结构)设置为非零值,我们将设置一个重复间隔计时器(因此而得名为 POSIX 间隔计时器);时间间隔是它被初始化为的值。 计时器将继续无限期地开火,或者直到它被解除武装或删除。 相反,如果将此结构清零,则计时器将变成一个普通的一次性计时器(只在 it_value 成员中指定的时间过去时触发一次)。

通常,将参数flags的值设置为0--timer_settime(2)上的手册页指定了一个可以使用的附加标志。 最后,第四个参数old_value(同样是指向结构itimerspec的指针)的工作方式如下所示:

  • 如果为0,则它将被简单地忽略。
  • 如果非零,则查询给定计时器到期前的剩余时间。
  • 过期时间将在第一个old_value->it_value成员中返回(以秒和纳秒为单位),其设置的间隔将在第二个old_value->it_interval成员中返回。

不出所料,成功的返回值是0,失败的返回值是-1(适当设置了返回值errno)。

查询计时器

可以随时查询给定的 POSIX 定时器,通过(timer_gettime(2)系统调用 API 获取定时器到期前的剩余时间;其签名如下:

#include <time.h>
int timer_gettime(timer_t timerid, struct itimerspec *curr_value);

很明显,传递给timer_gettime(2)的第一个参数是要查询的特定计时器的 ID,传递的第二个参数是值 Result 样式的返回值-到期时间在其中返回(在类型为itimerspec的结构中)。

正如我们从前面了解到的,结构itimerval结构本身由两个类型为timespec的数据结构组成;到计时器到期的剩余时间将放在第二个curr_value->it_value成员中。 如果此值为 0,则表示计时器已停止(解除武装)。 如果放在第二个 curr_value->it_interval成员中的值为正,则表示计时器重复触发的间隔(在第一次超时之后);如果为 0,则表示计时器为单次计时器(没有重复超时)。

显示工作流的示例代码片段

在下面,我们将显示示例程序ch13/react.c中的代码片段(请参阅下一节中关于这个相当有趣的反应时间游戏应用的更多信息),它清楚地说明了前面描述的步骤顺序。

  • 信号设置:
    • 假设正在使用的通知机制是信号,则首先通过sigaction(2)捕获信号,如下所示:
struct sigaction act;
[...]
// Trap SIGRTMIN : delivered on (interval) timer expiry
memset(&act, 0, sizeof(act));
act.sa_flags = SA_SIGINFO | SA_RESTART;
act.sa_sigaction = timer_handler;
if (sigaction(SIGRTMIN, &act, NULL) == -1)
    FATAL("sigaction SIGRTMIN failed\n");
  • 创建并初始化计时器:
    • 确定用于测量运行时间的时钟类型(或源):
      • 我们使用实时时钟(系统范围的挂钟时间CLOCK_REALTIME)作为计时器源。
    • 决定应用要使用的计时器到期事件通知机制-通常是使用(通常的)信号还是(新产生的)线程。
      • 我们使用信号作为定时器过期事件通知机制。
    • 上述决定是通过系统调用timer_create(2)实现的,该系统调用允许创建计时器;当然,我们也可以通过多次调用来创建多个计时器:
struct sigevent sev;
[...]
/* Create and init the timer */
sev.sigev_notify = SIGEV_SIGNAL;
sev.sigev_signo = SIGRTMIN;
sev.sigev_value.sival_ptr = &timerid;
if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1)
    FATAL("timer_create failed\n");
  • 使用timer_settime(2)API 解除(或解除)特定计时器的武装(或解除武装)。 给计时器解除武装的意思是有效地启动它运行,或倒计时;解除计时器的武装正好相反--让它停在轨道上:
static struct itimerspec itv;    // global
[...]
static void arm_timer(timer_t tmrid, struct itimerspec *itmspec)
{
    VPRINT("Arming timer now\n");
    if (timer_settime(tmrid, 0, itmspec, NULL) == -1)
        FATAL("timer_settime failed\n");
    jumped_the_gun = 0;
}
[...]
printf("Initializing timer to generate SIGRTMIN every %ld ms\n",
 freq_ms);
memset(&itv, 0, sizeof(struct itimerspec));
itv.it_value.tv_sec = (freq_ms * 1000000) / 1000000000;
itv.it_value.tv_nsec = (freq_ms * 1000000) % 1000000000;
itv.it_interval.tv_sec = (freq_ms * 1000000) / 1000000000;
itv.it_interval.tv_nsec = (freq_ms * 1000000) % 1000000000;
[...]
arm_timer(timerid, &itv);
  • 要查询特定计时器中剩余(到到期)的时间(及其间隔设置),请使用timer_gettime(2)

在此特定应用中不执行此操作。

  • 使用timer_getoverrun(2)检查给定定时器的超时计数

下面的计算溢出部分将解释此 API 的用途,以及我们可能需要它的原因。

/* 
 * The realtime signal (SIGRTMIN) - timer expiry - handler.
 * WARNING! Using the printf in a signal handler is unsafe!
 * We do so for the purposes of this demo app only; do Not
 * use in production.
 */
static void timer_handler(int sig, siginfo_t * si, void *uc)
{
  char buf[] = ".";

  c++;
  if (verbose) {
      write(2, buf, 1);
#define SHOW_OVERRUN 1
#if (SHOW_OVERRUN == 1)
    {
          int ovrun = timer_getoverrun(timerid);
          if (ovrun == -1)
              WARN("timer_getoverrun");
          else {
              if (ovrun)
                  printf(" overrun=%d [@count=%d]\n", ovrun, c);
          }
    }
#endif
  }
}
  • 使用timer_delete(2)删除(并明显解除)计时器

这不是在这个特定的应用中执行的(因为进程退出当然会删除与该进程相关联的所有计时器)。

正如timer_create(2)上的手册页告诉我们的那样,关于 POSIX(间隔)计时器还有以下几点需要注意:

  • 在执行fork(2)时,所有计时器都会自动解除武装;换句话说,计时器在子进程中不会继续超时。
  • 在执行execve(2)时,所有计时器都将被删除,因此在后续进程中不可见。
  • 值得注意的是(从 Linux 3.10 内核开始),可以使用.proc 文件系统来查询进程拥有的计时器;只需在伪file /proc/<pid>/timers文件中查找 cat 即可查看它们(如果它们存在)。
  • 从 Linux 4.10 内核开始,POSIX 计时器是内核可配置的选项(在内核构建时,缺省情况下启用它们)。

正如我们反复提到的,手册页是开发人员可用的非常宝贵和有用的资源;同样,timer_create(2)上的手册页(https://linux.die.net/man/2/timer_create)提供了一个非常好的示例程序;我们敦促读者参考手册页,阅读它,构建它并试用该程序。

计算溢出的原因

假设我们使用信号作为事件通知机制来告诉我们 POSIX 计时器已经过期,假设计时器过期时间非常短(比如说,几十微秒);例如,100 微秒。 这意味着,每隔 100 微秒,信号就会被传递到目标进程!

在这种情况下,以如此高的速率传递相同的不断重复的信号的过程不可能处理它,这是非常合理的。 我们还从我们对信号的了解中知道,在恰恰像这样的情况下,使用实时信号将远远优于使用常规 Unix 信号,因为操作系统有能力对实时信号进行排队,但不能对常规信号进行排队-它们(常规信号)将被丢弃,并且只保留一个实例。

因此,我们将使用实时信号(比方说,SIGRTMIN)来表示计时器超时;然而,对于非常微小的计时器超时(例如,正如我们所说的,100 微秒),即使是这种技术也不够! 这一过程肯定会被相同信号的快速传递所溢出。 正是对于这些情况,没有人可以检索计时器超时和实际信号处理之间发生的实际溢出次数。 我们该怎么做呢? 有两种方式:

  • 一种是通过信号处理程序的siginfo_t->_timer->si_overrun成员(这意味着我们在使用 Sigaction 捕获信号时指定了SA_SIGINFO标志)-这是溢出计数。
  • 但是,该方法是特定于 Linux 的(并且不可移植)。 获取溢出计数的一种更简单、可移植的方法是使用timer_getoverrun(2)系统调用。 这里的缺点是系统调用比内存查找有更多的开销;就像生活中一样,当有好处时,也有坏处。

POSIX 间隔计时器-示例程序

编程最终是通过做来学习的,理解是通过做来深刻内化的,而不是简单的看或读。 让我们采纳我们自己的建议,截取几个像样的代码示例,来说明如何使用 POSIX(Interval)计时器 API。 (当然,亲爱的读者,这意味着你也可以这么做!)

第一个示例程序是一个小的 CLI 游戏,游戏名为“你能有多快反应”? 第二个示例程序是 Run-Walk 计时器的简单实现。 请继续阅读,了解血淋淋的细节。

反应时间游戏

我们都知道现代计算机速度很快! 当然,这是一个非常相对的说法。 具体有多快? 这是个有趣的问题。

多快是快?

第 2 章虚拟内存中,在内存金字塔部分,我们看到了表 2:内存层级编号。 在这里,我们对这些数字进行了代表性的查看-表中列举了不同类型的内存技术(嵌入式和服务器空间)的典型访问速度。

简要回顾一下典型的内存(和网络)访问速度。 当然,这些数字只是指示性的,最新的硬件很可能具有卓越的性能特征;这里的概念是重点关注的:

| CPU 寄存器 | CPU 缓存 | RAM | 闪光灯 | 磁盘 | 网络往返 | | 300-500 ps | 0.5 ns(L1)至 20 ns(L3) | 50-100 ns | 25-50 美元 | 5-10 毫秒 | >=100s 毫秒 |

Table 3 : Hardware memory speed summary table

这些潜伏值中的大多数都是如此之小,以至于作为人类,我们实际上无法将它们可视化(参见后面关于平均人类反应时间的信息框)。 所以,这就引出了一个问题。 我们人类甚至可以希望非常正确地可视化和关联到哪些最小的微小数字? 简短的答案是几百毫秒。

为甚麽我们要这样说呢? 那么,如果一个电脑程序告诉你看到一条信息后,要尽可能快地做出反应,并立即按下某个键盘组合键,那么需要多长时间呢? 所以,我们在这里真正试图测试的是人类对视觉刺激的反应时间。啊,这就是我们可以通过编写这个精确的程序来经验上回答的问题:反应计时器!

Do note that this simple visual stimulus reaction test is not considered to be scientific; we completely ignore important delay-inducing mechanisms such as the computer-system hardware and software itself. So don't beat yourself up on the results you get when you try it out!

我们的反应游戏-它是如何运作的

因此,在较高的层面上,下面是该程序的分步计划(实际代码显示在以下部分中;我们建议您先阅读本文,然后再检查代码):

  • 创建并初始化一个简单的警报;将其编程为在任意时间到期-程序启动后 1 到 5 秒之间的任何时间
  • 警报到期时,请执行以下操作:
    • 设置 POSIX(间隔)定时器(至第一个参数中指定的频率)。
    • 显示一条消息,要求用户按键盘上的Ctrl+C[T3
    • 取一个时间戳(让我们称它为tm_start)。
  • 当用户实际按下*^C*时(Ctrl+C;我们只需通过sigaction(2)捕获 SIGINT 即可获知),再次获取时间戳(我们称其为tm_end
  • 计算用户的反应时间(按tm_end-tm_start)并显示。

(请注意前面的步骤如何遵循我们在本章前面描述的典型应用工作流程。)

此外,我们要求用户指定间隔计时器的间隔(以毫秒为单位)(第一个参数),并将可选的 Verbose 选项指定为第二个参数。

进一步分解(更详细地),下面的初始化代码执行以下操作:

  • 通过sigaction(2)捕获信号:
    • SIGRTMIN:我们将使用信号通知来指定计时器超时;这是在 POSIX 间隔计时器超时时生成的信号。
    • SIGINT:用户按下键盘组合键*^C*进行反应时产生的信号。
    • SIGALRM:我们最初的随机警报到期时产生的信号
  • 设置 POSIX 间隔计时器:
    • 初始化表sigevent的结构。
    • 使用timer_create(2)创建计时器(带有实时时钟源)。
    • itimerspec参数结构初始化为用户指定的频率值(毫秒)

然后:

  • 向用户显示一条消息:
We shall start a timer anywhere between 1 and 5 seconds of starting this app.

GET READY ...
 [ when the "QUICK! Press ^C" message appears, press ^C quickly as you can ]
  • 在 1 到 5 秒之间的任意时间报警到期
  • 我们进入SIGALRM处理程序函数
    • 它显示*** QUICK! Press ^C !!! *** 消息
    • 它调用timer_settime(2)命令来启动计时器
    • 它需要tm_start个时间戳(带有clock_gettime(2)个 API)
    • POSIX 间隔计时器现在运行;它每隔freq_ms毫秒(用户提供的值)超时一次;在详细模式下运行时,我们为每个计时器超时显示**.**
  • 用户在近距离或远距离的某个时刻做出反应并按下*、Ctrl、+C(^C*);在 SIGINT 的信号处理程序代码中,我们执行以下操作:
    • 取第一个tm_end个时间戳(使用第二个clock_gettime(2)个 API)
    • 计算增量(反应时间!)。 通过tm_end-tm_start设置并显示
  • 出口。

反应-试验运行

最好是看到程序的实际运行情况;当然,读者会做得很好(并且更喜欢这个练习!)。 要真正为自己构建和试用它,请执行以下操作:

$ ./react 
Usage: ./react <freq-in-millisec> [verbose-mode:[0]|1]
  default: verbosity is off
  f.e.: ./react 100   => timeout every 100 ms, verbosity Off
      : ./react   5 1 => timeout every   5 ms, verbosity On

How fast can you react!?
Once you run this app with the freq-in-millisec parameter,
we shall start a timer anywhere between 1 and 5 seconds of
your starting it. Watch the screen carefully; the moment
the message "QUICK! Press ^C" appears, press ^C (Ctrl+c simultaneously)!
Your reaction time is displayed... Have fun!

$ 

我们首先以 10 毫秒的频率运行它,并且没有冗长:

$ ./react 10
Initializing timer to generate SIGRTMIN every 10 ms
[Verbose: N]
We shall start a timer anytime between 1 and 5 seconds from now...

GET READY ...
 [ when the "QUICK! Press ^C" message appears, press ^C quickly as you can ]

在 1 到 5 秒的随机间隔后,将出现此消息,用户必须做出反应:

*** QUICK! Press ^C !!! ***
^C
*** PRESSED ***
 Your reaction time is precisely 0.404794198 s.ns [~= 405 ms, count=40]
$ 

接下来,使用 10 毫秒的频率和详细模式打开:

$ ./react 10 1
Initializing timer to generate SIGRTMIN every 10 ms
timer struct ::
 it_value.tv_sec = 0 it_value.tv_nsec = 10000000
 it_interval.tv_sec = 0 it_interval.tv_nsec = 10000000
[SigBlk: -none-]
[Verbose: Y]
We shall start a timer anytime between 1 and 5 seconds from now...

GET READY ...
 [ when the "QUICK! Press ^C" message appears, press ^C quickly as you can ]

在 1 到 5 秒的随机间隔后,将出现此消息,用户必须做出反应:

react.c:arm_timer:161: Arming timer now

*** QUICK! Press ^C !!! *

现在,句点字符.迅速出现,对于 POSIX 间隔计时器的每个超时,它都会出现一次;也就是说,在这次运行中,每 10 毫秒出现一次。

.....................................^C
*** PRESSED ***
 Your reaction time is precisely 0.379339662 s.ns [~= 379 ms, count=37]
$ 

在我们之前的示例运行中,用户花费了 405ms 和 379ms 来做出反应;正如我们所提到的,它在数百毫秒的范围内。 接受挑战-你还能做得更好吗?

Research findings indicate the following numbers for average human reaction times:

| 刺激 | 视觉 | 听觉 | 触摸 | | 平均人体反应时间 | 250 毫秒 | 170 毫秒 | 150 毫秒 |

Source: https://backyardbrains.com/experiments/reactiontime.We have become used to using phrases such as "in the blink of an eye" to mean really quickly. Interestingly, how long does it actually take to blink an eye? Research indicates that it takes an average of 300 to 400 ms!

Reaction 游戏代码查看器

一些关键功能方面如下所示;首先是为SIGRTMIN设置信号处理程序并创建 POSIX 间隔(ch13/react.c)的代码:

For readability, only key parts of the source code are displayed in the following; to view the complete source code, build it, and run it, the entire tree is available for cloning from GitHub, here: https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux.

static int init(void)
{
 struct sigevent sev;
  struct rlimit rlim;
  struct sigaction act;

  // Trap SIGRTMIN : delivered on (interval) timer expiry
  memset(&act, 0, sizeof(act));
  act.sa_flags = SA_SIGINFO | SA_RESTART;
  act.sa_sigaction = timer_handler;
  if (sigaction(SIGRTMIN, &act, NULL) == -1)
    FATAL("sigaction SIGRTMIN failed\n");

[...]

/* Create and init the timer */
  sev.sigev_notify = SIGEV_SIGNAL;
  sev.sigev_signo = SIGRTMIN;
  sev.sigev_value.sival_ptr = &timerid;
  if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1)
    FATAL("timer_create failed\n");

  printf("Initializing timer to generate SIGRTMIN every %ld ms\n",
         freq_ms);
  memset(&itv, 0, sizeof(struct itimerspec));
  itv.it_value.tv_sec = (freq_ms * 1000000) / 1000000000;
  itv.it_value.tv_nsec = (freq_ms * 1000000) % 1000000000;
  itv.it_interval.tv_sec = (freq_ms * 1000000) / 1000000000;
  itv.it_interval.tv_nsec = (freq_ms * 1000000) % 1000000000;
[...]

意外启动的实施方式如下:

/* random_start
 * The element of surprise: fire off an 'alarm' - resulting in SIGALRM being
 * delivered to us - in a random number between [min..max] seconds.
 */
static void random_start(int min, int max)
{
    unsigned int nr;

    alarm(0);
    srandom(time(0));
    nr = (random() % max) + min;

#define CHEAT_MODE     0
#if (CHEAT_MODE == 1)
    printf("Ok Cheater :-) get ready; press ^C in %ds ...\n", nr);
#endif
 alarm(nr);
}

调用方式如下:

#define MIN_START_SEC 1
#define MAX_START_SEC 5
[...]
random_start(MIN_START_SEC, MAX_START_SEC);

报警(用于SIGALRM)的信号处理程序(函数startoff)和相关逻辑如下:

static void arm_timer(timer_t tmrid, struct itimerspec *itmspec)
{
  VPRINT("Arming timer now\n");
  if (timer_settime(tmrid, 0, itmspec, NULL) == -1)
      FATAL("timer_settime failed\n");
  jumped_the_gun = 0;
}

/*
 * startoff
 * The signal handler for SIGALRM; arrival here implies the app has
 * "started" - we shall arm the interval timer here, it will start
 * running immediately. Take a timestamp now.
 */
static void startoff(int sig)
{
  char press_msg[] = "\n*** QUICK! Press ^C !!! ***\n";

  arm_timer(timerid, &itv);
  write(STDERR_FILENO, press_msg, strlen(press_msg));

  //—- timestamp it: start time
  if (clock_gettime(CLOCK_REALTIME, &tm_start) < 0)
      FATAL("clock_gettime (tm_start) failed\n");
}

请记住,当用户游手好闲时,我们的 POSIX 间隔计时器继续以用户指定的频率设置和重置自身(作为第一个参数传递,我们将其保存在变量freq_ms中);因此,每隔freq_ms毫秒,我们的进程将接收信号SIGRTMIN。 下面是它的信号处理程序例程:

static volatile sig_atomic_t gTimerRepeats = 0, c = 0, first_time = 1,
    jumped_the_gun = 1; [...] static void timer_handler(int sig, siginfo_t * si, void *uc)
{
  char buf[] = ".";

  c++;
  if (verbose) {
      write(2, buf, 1);
#define SHOW_OVERRUN 1
#if (SHOW_OVERRUN == 1)
      {
          int ovrun = timer_getoverrun(timerid);
          if (ovrun == -1)
              WARN("timer_getoverrun");
          else {
              if (ovrun)
                  printf(" overrun=%d [@count=%d]\n", ovrun, c);
          }
    }
#endif
  }
}

当用户这样做时(终于!)。 按*^C,*调用 SIGINT 的信号处理程序(函数:userpress):

static void userpress(int sig)
{
  struct timespec res;

  // timestamp it: end time
  if (clock_gettime(CLOCK_REALTIME, &tm_end) < 0)
    FATAL("clock_gettime (tm_end) failed\n");

  [...]
      printf("\n*** PRESSED ***\n");
      /* Calculate the delta; subtracting one struct timespec
       * from another takes a little work. A retrofit ver of
       * the 'timerspecsub' macro has been incorporated into
       * our ../common.h header to do this.
       */
      timerspecsub(&tm_end, &tm_start, &res);
      printf
          (" Your reaction time is precisely %ld.%ld s.ns"
           " [~= %3.0f ms, count=%d]\n",
           res.tv_sec, res.tv_nsec,
           res.tv_sec * 1000 +
             round((double)res.tv_nsec / 1000000), c);
    }
   [...]
  c = 0;
  if (!gTimerRepeats)
    exit(EXIT_SUCCESS);
}

The Run:Walk and Interval Timer 应用

这本书的作者自称是一名休闲跑步者。 在我的拙见中,跑步者/慢跑者,尤其是刚开始的时候(经常,甚至是有经验的人),可以从始终如一的跑步:步行模式中受益(通常以分钟为单位)。

这背后的想法是,连续跑步很难,特别是对初学者来说。 通常,教练会让新手遵循一个有用的跑步策略:步行策略;跑一段给定的时间,然后在给定的时间段内休息一段时间,然后重复-再次跑步,再次步行-无限期地,或者直到你的目标距离(或时间)目标达到为止。

例如,当初学者跑步距离为 5 公里或 10 公里时,他可能会遵循一致的 5:2 跑:步行模式,即跑 5 分钟,走 2 分钟,不断重复这一点,直到跑完为止。 (另一方面,超级跑步者可能更喜欢类似于 25:5 的策略。)

为什么不写一个 Run:Walk 计时器应用来帮助我们的初学者和认真的跑步者。

我们就是这么做的。 不过,首先,从更好地理解这个程序的角度来看,让我们假设这个程序已经编写好并且正在运行--我们将给它一个简单的介绍。

几次试运行

当我们简单地运行程序而不传递任何参数时,将显示帮助屏幕:

$ ./runwalk_timer 
Usage: ./runwalk_timer Run-for[sec] Walk-for[sec] [verbosity-level=0|[1]|2]
 Verbosity Level :: 0 = OFF [1 = LOW] 2 = HIGH
$ 

可以看出,该程序至少需要两个参数:

  • 运行时间(秒)[必需]
  • *步行时间(秒)[必需]
  • 详细级别。[可选]

可选的第三个参数,详细级别,允许用户在程序执行时请求或多或少的信息(这始终是检测并帮助调试程序的有用方法)。 我们提供三种可能的详细级别:

  • OFF:只显示必填内容(传递第三个参数 0)
  • LOW:与调平相同,另外我们使用句点字符**.**表示时间流逝-每秒都会将一个**.** 打印到stdout和[默认]
  • HIGH:与调平相同,另外我们显示内部数据结构值、计时器超时时间等(传递第三个参数 2)

让我们首先尝试在默认详细级别(低)下运行,规范如下:

  • 运行 5 秒钟
  • 散步 2 秒钟

好的,好的,我们知道,你比那更合适--你可以跑步:步行超过 5 秒 2 秒。 原谅我们,但事情是这样的:为了演示的目的,我们真的不想等到 5 分钟后,然后又过了 2 分钟,只是为了看看它是否有效,对吗? (当你在跑步时使用这款应用时,请将分钟转换为秒,然后去做吧!)

说得够多了;让我们启动跑步:步行 POSIX 计时器进行大约 5 分 2 秒的跑步:步行间隔:

$ ./runwalk_timer 5 2
************* Run Walk Timer *************
                  Ver 1.0

Get moving... Run for 5 seconds
.....        *<< each "." represents 1 second of elapsed time >>*
*** Bzzzz!!! WALK! *** for 2 seconds
..
*** Bzzzz!!! RUN! *** for 5 seconds
.....
*** Bzzzz!!! WALK! *** for 2 seconds
..
*** Bzzzz!!! RUN! *** for 5 seconds
....^C
+++ Good job, bye! +++
$ 

是的,它可以工作;我们通过键入*^C*(Ctrl+*C)*来中断它。

前面的试运行处于默认的详细级别LOW;现在让我们使用相同的 5:2 Run:Walk 间隔重新运行它,但通过传递2作为第三个参数,将详细级别设置为HIGH

$ ./runwalk_timer 5 2 2
************* Run Walk Timer *************
                  Ver 1.0

Get moving... Run for 5 seconds
trun= 5 twalk= 2; app ctx ptr = 0x7ffce9c55270
runwalk: 4.999s                    *<< query on time remaining >>*
runwalk: 3.999s
runwalk: 2.999s
runwalk: 1.999s
runwalk: 0.999s
its_time: signal 34. runwalk ptr: 0x7ffce9c55270 Type: Run. Overrun: 0

*** Bzzzz!!! WALK! *** for 2 seconds
runwalk: 1.999s
runwalk: 0.999s
its_time: signal 34. runwalk ptr: 0x7ffce9c55270 Type: Walk. Overrun: 0

*** Bzzzz!!! RUN! *** for 5 seconds
runwalk: 4.999s
runwalk: 3.999s
runwalk: 2.999s
runwalk: 1.999s
runwalk: 0.999s
its_time: signal 34. runwalk ptr: 0x7ffce9c55270 Type: Run. Overrun: 0

*** Bzzzz!!! WALK! *** for 2 seconds
runwalk: 1.999s
runwalk: 0.999s
its_time: signal 34. runwalk ptr: 0x7ffce9c55270 Type: Walk. Overrun: 0

*** Bzzzz!!! RUN! *** for 5 seconds
runwalk: 4.999s
runwalk: 3.999s
runwalk: 2.999s
^C
+++ Good job, bye! +++
$ 

详细信息会显示出来;每秒钟都会显示我们的 POSIX 计时器超时的剩余时间(精确到毫秒)。 当计时器超时时,操作系统将实时信号SIGRTMIN传递给进程;我们进入信号处理程序its_time,然后打印出从结构siginfo_t指针获得的信号信息。 我们在联合si->si_value中接收信号号(34)和指针si->si_value,这是指向应用上下文数据结构的指针,这样我们就可以在不使用全局变量的情况下访问它(稍后将详细介绍)。(当然,正如多次提到的,在信号处理程序中使用printf(3)和变体是不安全的,因为它们是信号异步的。 我们在这里只是作为演示来做这件事;不要为生产用途编写这样的代码。 当然,Bzzzz!!!消息代表计时器开始计时的蜂鸣声;程序指示用户继续执行RUN!WALK!,并相应地指示用户继续执行RUN!WALK!,以及执行此操作的秒数。 整个过程无限重复。

低层设计和代码

这个简单的程序将允许您设置跑步和步行的秒数。 它将相应地超时。

在这个应用中,我们使用一个简单的单次 POSIX 定时器来完成这项工作。 我们将定时器设置为使用信号通知作为定时器超时通知机制。 我们将为 RT 信号设置一个信号处理程序(SIGRTMIN)。 接下来,我们首先将 POSIX 计时器设置为在运行周期后过期,然后,当信号确实到达信号处理程序时,我们可以将计时器重置为在遍历周期秒后过期。 这基本上会永远重复,或者直到用户按下*^C*中止程序。

For readability, only key parts of the source code are displayed in the following; to view the complete source code, build it, and run it, the entire tree is available for cloning from GitHub, here: https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux.

许多现实世界中的应用(实际上,任何软件)通常需要多条信息-状态或应用上下文-才能在任何给定的时间点对所有功能可用;换句话说,是全局的。 通常,只需将它们声明为全局(静态)变量并继续。 我们有一个建议:为什么不将它们全部封装到一个数据结构中呢? 事实上,为什么不通过类型化一个结构来使它成为我们自己的呢? 然后,我们可以给它分配内存,初始化它,然后以不要求它是全局的方式传递它的指针。 那将是高效而优雅的。

// Our app context data structure
typedef struct {
  int trun, twalk;
  int type;
  struct itimerspec *itmrspec;
  timer_t timerid;
} sRunWalk;

在我们的应用中,为了简单起见,我们只是静态地将内存分配给(此外,请注意,它是一个局部变量,而不是全局变量):

int main(int argc, char **argv)
{
  struct sigaction act;
  sRunWalk runwalk;
  struct itimerspec runwalk_curval;
[...]

初始化工作在这里进行:

/*————————— Our POSIX Timer setup
 * Setup a 'one-shot' POSIX Timer; initially set it to expire upon
 * 'run time' seconds elapsing.
 */
static void runwalk_timer_init_and_arm(sRunWalk * ps)
{
  struct sigaction act;
  struct sigevent runwalk_evp;

  assert(ps);

  act.sa_sigaction = its_time;
  act.sa_flags = SA_SIGINFO;
  sigfillset(&act.sa_mask);
  if (sigaction(SIGRTMIN, &act, 0) < 0)
      FATAL("sigaction: SIGRTMIN");
  memset(ps->itmrspec, 0, sizeof(sRunWalk));
  ps->type = RUN;
 ps->itmrspec->it_value.tv_sec = ps->trun;

  runwalk_evp.sigev_notify = SIGEV_SIGNAL;
  runwalk_evp.sigev_signo = SIGRTMIN;
  // Pass along the app context structure pointer
 runwalk_evp.sigev_value.sival_ptr = ps;

  // Create the runwalk 'one-shot' timer
  if (timer_create(CLOCK_REALTIME, &runwalk_evp, &ps->timerid) < 0)
      FATAL("timer_create");

  // Arm timer; will exire in ps->trun seconds, triggering the RT signal
  if (timer_settime(ps->timerid, 0, ps->itmrspec, NULL) < 0)
      FATAL("timer_settime failed");
}
[...]
runwalk_timer_init_and_arm(&runwalk);
[...]

在前面的代码中,我们执行以下操作:

  • 捕获实时信号(SIGRTMIN)(在定时器超时时传送)。
  • 初始化我们的应用上下文运行:遍历数据结构:
    • 特别地,我们在第一个参数中将超时类型设置为运行,将超时值(秒)设置为用户经过的时间。
  • 定时器超时事件通知机制被选为通过我们的代理sigevent结构的成员sigev_notify发送的信号。
    • 将通过sigev_value.sival_ptr成员传递的数据设置为指向我们的应用上下文的指针非常有用;这样,我们始终可以在信号处理程序中访问它(无需保持全局)。
  • 创建具有实时时钟源的 POSIX 计时器,并将其 ID 设置为我们的应用上下文 RunWalk 结构的第一个timerid成员
    • 启动或启动定时器。 (回想一下,它已被初始化为在运行几秒钟后过期。)

在我们之前的试运行中,运行时间设置为 5 秒,因此,从开始算起 5 秒后,我们将异步进入SIGRTMINits_time的运行信号处理程序,如下所示:

static void its_time(int signum, siginfo_t *si, void *uctx)
{
  // Gain access to our app context
 volatile sRunWalk *ps = (sRunWalk *)si->si_value.sival_ptr;

  assert(ps);
  if (verbose == HIGH)
    printf("%s: signal %d. runwalk ptr: %p"
           " Type: %s. Overrun: %d\n",
           __func__, signum,
           ps,
           ps->type == WALK ? "Walk" : "Run", 
           timer_getoverrun(ps->timerid)
        );

  memset(ps->itmrspec, 0, sizeof(sRunWalk));
  if (ps->type == WALK) {
    BUZZ(" RUN!");
    ps->itmrspec->it_value.tv_sec = ps->trun;
    printf(" for %4d seconds\n", ps->trun);
  }
  else {
    BUZZ(" WALK!");
    ps->itmrspec->it_value.tv_sec = ps->twalk;
    printf(" for %4d seconds\n", ps->twalk);
  }
  ps->type = !ps->type; // toggle the type

  // Reset: re-arm the one-shot timer
  if (timer_settime(ps->timerid, 0, ps->itmrspec, NULL) < 0)
    FATAL("timer_settime failed");
}

在信号处理代码中,我们执行以下操作:

  • (如前所述)访问我们的应用上下文和数据结构(通过将si->si_value.sival_ptr类型转换为我们的(sRunWalk *)数据类型)。
  • 在高冗长模式下,我们显示更多详细信息(同样,不要在生产中使用printf(3))。
  • 然后,如果刚刚到期的定时器是RUN定时器,我们使用WALK消息参数调用蜂鸣器函数BUZZ ,重要的是:
    • 将超时值(秒)重新初始化为行走持续时间(用户传递的第二个参数)。
    • 将类型从跑步切换为行走。
    • 通过接口timer_settime(2)重新启动计时器。
  • 反之亦然,当从刚到期的步行模式转换到跑步模式时。

这样,该进程将永远运行(或者直到用户通过*^C*终止它),为下一次 Run:Walk Interval 持续超时。

通过进程进行计时器查找

还有一件事:有趣的是,Linux 内核允许我们深入操作系统内部;这(通常)是通过强大的 Linux.proc 文件系统实现的。 在我们当前的上下文中,proc 允许我们查找给定进程拥有的所有计时器。 这是怎么做的? 通过读取伪文件/proc/<PID>/timers。 看看这个。 下面的屏幕截图说明了在runwalk_timer 流程中执行的操作:

左边的终端窗口是运行 procrunwalk_timer应用的地方;当它运行时,我们在右边的终端窗口中查找 proc 文件系统的伪文件:/proc/<PID>/timers。输出清楚地显示了以下内容:

  • 进程中只有一个(POSIX)计时器(ID0)。
  • 定时器到期事件通知机制正在发送信号,因为我们可以看到notify:signal/pid.<PID>和信号:34 与该定时器相关联(信号:34 是SIGRTMIN;使用信号kill -l34 来验证这一点)。
  • 与该定时器关联的时钟源是ClockID 0,即实时时钟。

略提一下

作为本章的总结,我们将简要介绍两种有趣的技术:通过文件抽象模型实现的计时器和看门狗计时器。 这些部分没有详细介绍;我们让感兴趣的读者进一步挖掘。

通过文件描述符的计时器

您还记得我们在本书的第 1 章Linux 系统体系结构中介绍的 Unix(以及 Linux)设计的一个关键哲学吗? 就是一切都是进程,不是进程就是文件,文件抽象在 Linux 上用得很多;这里也有计时器,我们发现有一种方法可以通过文件抽象来表示和使用计时器。

这是怎么做的? timerfd_*个 API 提供了所需的抽象。 在这本书中,我们不会试图深入研究错综复杂的细节;相反,我们希望读者意识到,如果需要,可以使用文件抽象-通过命令 read(2)和系统调用读取计时器。

下表快速概述了timerfd_*系列 API:

| 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 | 目的 | 相当于 POSIX 计时器 API | | timerfd_create(2) | 创建一个 POSIX 计时器;成功时的返回值是与该计时器关联的文件描述符。 | timer_create(2) | | timerfd_settime(2) | (Dis)解除第一个参数fd引用的定时器。 | timer_settime(2) | | timerfd_gettime(2) | 成功完成后,返回第一个参数fd引用的计时器的超时时间和时间间隔。 | timer_gettime(2) |

Table 4 : The timerfd_* APIs

include <sys/timerfd.h>

int timerfd_create(int clockid, int flags);

int timerfd_settime(int fd, int flags,
 const struct itimerspec *new_value, struct itimerspec *old_value);

int timerfd_gettime(int fd, struct itimerspec *curr_value);

使用文件描述符来表示各种对象的真正优势在于,可以使用一组统一的、功能强大的 API 来对它们进行操作。 在此特定情况下,我们可以通过read(2)poll(2)select(2)epoll(7)和类似 API 监控基于文件的定时器。

如果创建基于 FD 的计时器的进程派生或执行怎么办? 在调用fork(2)之后,子进程将通过 API 继承与父进程中创建的任何计时器相关的文件描述符的副本。 实际上,它与父进程共享相同的计时器。

在设置execve(2)时,计时器在后续进程中保持有效,并将在超时时继续过期;除非在创建时指定了 TFD_CLOEXEC 标志。

更多细节(以及一个示例)可以在这里的手册页中找到:https://linux.die.net/man/2/timerfd_create

关于看门狗定时器的快速说明

看门狗本质上是一种基于计时器的机制,用于定期检测系统是否处于健康状态,如果认为不是,则重新启动系统。

这是通过设置(内核)计时器(比如 60 秒超时)来实现的。 如果一切正常,看门狗守护程序进程将在计时器到期之前持续解除其防护,然后重新启用(解除防护);这称为抚摸狗。如果守护程序没有解除看门狗计时器的防护(由于出现严重错误),则看门狗会感到恼火,并重新启动系统。

A daemon is a system background process; more on daemons in Appendix B, Daemon Processes.

纯软件看门狗实现将不受内核错误和故障的保护;硬件看门狗(锁定在板重置电路中)将始终能够在需要时重新启动系统。

看门狗定时器经常用于嵌入式系统中,尤其是深度嵌入的系统(或人类出于任何原因无法访问的系统);在最坏的情况下,它可能会重新启动,并有望再次执行指定的任务。 一个著名的看门狗定时器导致重启的例子是探路者机器人,NASA 早在 1997 年就把它送到了火星表面(是的,就是在火星上遇到了优先级反转和并发错误的那个机器人)。 我们将在第 15 章使用 Pthread 多线程,第二部分-同步(关于多线程和并发)中对此进行一些探讨。 而且,是的,这就是在精彩的电影《火星救援》中饰演角色的探路者机器人! 有关这方面的更多信息,请参阅关于 GitHub 存储库的进一步阅读部分。

简略的 / 概括的 / 简易判罪的 / 简易的

在本章中,读者已经了解了 Linux 提供的有关创建和使用计时器的各种接口。 设置和管理超时是许多(如果不是大多数)系统应用的基本组件。 使用示例代码显示了较旧的接口-受人尊敬的alarm(2)接口 API,然后是[s|g]etitimer(2)的系统调用。 然后,我们深入研究了更新更好的 POSIX 计时器,包括它们提供的优势,以及如何以实际的方式使用它们。 这在很大程度上得益于两个相当复杂的示例程序-Reaction 游戏和 Run:Walk Timer 应用。 最后,向读者介绍了通过文件抽象使用计时器的概念,以及看门狗计时器。

下一章是我们开始我们关于理解和使用 Linux 上强大的多线程框架的漫长的三章之旅的地方。