时钟,计时器和踢脚

本章内容包括:

时钟和计时器

现在该看看与Neutrino中的时间有关的所有事情了。我们将看到您如何以及为什么使用计时器及其背后的理论。然后,我们来看看获取和设置实时时钟。

本章使用的滴答大小为10毫秒,但是QNX Neutrino现在在大多数系统上默认使用1毫秒的滴答大小。这不会影响所讨论问题的实质。
ticksize 滴答

让我们看一个典型的系统,例如汽车。在这辆车上,我们有很多程序,其中大多数程序以不同的优先级运行。其中一些需要响应实际的外部事件(例如制动器或无线电调谐器),而其他一些则需要定期运行(例如诊断系统)。

这里依然没有解释优先级的作用。

定期运行

那么,诊断系统如何“定期运行”?您可以想象汽车CPU中的某个过程执行与以下操作类似的操作:

// Diagnostics Process

int
main (void)     // ignore arguments here
{
    for (;;) {
        perform_diagnostics ();
        sleep (15);
    }

    // You'll never get here.
    return (EXIT_SUCCESS);
}

在这里,我们看到诊断过程将永远运行。它执行一轮诊断,然后进入睡眠状态15秒钟,醒来,再次经过循环,然后……

回到单任务的黑暗,黑暗的日子,那里一个CPU专供一个用户使用,这些程序是通过让sleep (15);代码执行繁忙等待循环来实现的。您将计算出CPU的速度,然后编写自己的sleep()函数:

void
sleep (int nseconds)
{
    long    i;

    while (nseconds--) {
        for (i = 0; i < CALIBRATED_VALUE; i++) ;
    }
}

在那些日子里,由于计算机上没有其他任何东西在运行,所以这并没有太大的问题,因为没有其他进程关心您在_sleep()_函数中占用了100%的CPU 。


即使在今天,我们有时仍会占用100%的CPU来执行定时功能。值得注意的是,_nanospin()_函数用于获得非常精细的时序,但这样做的代价是以优先级燃烧CPU为代价。请谨慎使用!

如果您确实必须执行某种形式的“多任务”,则通常是通过中断例程来完成的,该例程会挂断硬件计时器,或者在“繁忙等待”时间内执行,这在一定程度上影响了时间校准。这通常不是问题。

幸运的是,我们已经取得了很大的进步。回顾“ 进程和线程”一章中的“调度和现实世界”,是什么导致内核重新调度线程:

  • 硬件中断
  • 内核调用
  • 故障(异常)
    在本章中,我们关注列表中的前两项:硬件中断和内核调用。

当线程调用sleep()时,C库包含最终进行内核调用的代码。该调用告诉内核“将线程搁置固定的时间。” 该调用从正在运行的队列中删除线程并启动计时器。

同时,内核一直在从计算机的时钟硬件接收常规的硬件中断。假设出于争论的考虑,这些硬件中断恰好在10毫秒的间隔内发生。

让我们重述一下:每次由内核的时钟中断服务程序(ISR)处理这些中断之一时,都意味着已经过去了10毫秒。内核通过在每次ISR运行时将其每日时间变量增加对应于10毫秒的量来跟踪每日时间。

因此,当内核实现一个15秒的计时器时,它真正要做的就是:

  1. 将变量设置为当前时间加上15秒。
  2. 在时钟ISR中,将此变量与一天中的时间进行比较。
  3. 当一天中的时间与变量相同(或大于)时,将线程放回READY队列。

如果有多个计时器未完成(例如,需要在不同时间唤醒多个线程),内核将简单地将请求排队,按时间顺序对它们进行排序-最接近的一个位于队列的开头,并且以此类推。ISR所查看的变量是此队列开头的变量。

计时器5分巡回赛到此结束。

实际上,除了第一次见到眼外,还有更多的东西。

时钟中断源

那么时钟中断从哪里来?下图显示了负责生成这些时钟中断的硬件组件(以及PC的一些典型值):

PC时钟中断源。

如您所见,PC中的电路产生了一个高速(MHz范围)时钟。然后,该高速时钟由硬件计数器(图中的82C54组件)分频,这会将时钟速率降低到kHz或几百Hz范围(即,ISR可以实际处理)。时钟ISR是内核的组成部分,直接与内核本身的数据结构和代码交互。在非x86架构(MIPS,PowerPC)上,会发生类似的事件序列。有些芯片在处理器中内置了时钟。

注意,高速时钟除以整数 除数。这意味着速率不会精确到 10 ms,因为高速时钟的速率不是 10 ms的整数倍。因此,上面示例中的内核ISR实际上可能在9.9999296004 ms之后被中断。

没关系吧?好吧,当然,这对我们的15秒计数器来说很好。15秒是1500个计时器滴答声-通过数学计算可知,与标记相距大约106 µs:

  • 15 s-1500×9.9999296004 ms = 15000毫秒-14999.8944006毫秒= 0.1055994毫秒= 105.5994微秒

不幸的是,继续进行数学运算,这相当于每天608毫秒,或每月约18.5秒,或每年近3.7分钟!

您可以想象,使用其他除数,误差可能会更大或更小,具体取决于引入的舍入误差。幸运的是,内核知道了这一点并对其进行了纠正。

这个故事的重点是,无论显示的是不错的取整值, 实际值都将被选择为下一个更快的值。

基本时序分辨率

假设计时器滴答声的运行时间比10 ms快一点。我可以可靠地睡眠3毫秒吗?

不。

考虑一下内核中发生了什么。您发出C库的delay()调用进入睡眠状态,持续3毫秒。内核必须将ISR中的变量设置为某个值。如果将其设置为当前时间,则意味着计时器已过期,您应该立即唤醒。如果将其设置为比当前时间多一刻,则意味着您应在下一刻(最多 10毫秒)内醒来。

这个故事的寓意是:“不要期望计时分辨率会比输入计时器的滴答频率更好。”

获得更高的精度

在Neutrino下,程序可以与内核一起调整硬件除数组件的值(以便内核知道调用ISC计时器滴答的速率)。我们将在下面的“获取和设置实时时钟”部分中进行介绍。

定时抖动

您还需要担心另一件事。假设时序分辨率为10毫秒,而您想要20毫秒的超时时间。

从发出_delay()_调用的时间到函数调用返回的时间,您总是会得到正好20毫秒的延迟吗?

绝对不。

原因有两个。第一个非常简单:当您阻塞时,您将退出运行队列。这意味着优先级高的另一个线程现在可能正在使用CPU。20毫秒到期后,您将被置于该优先级的READY队列的末尾,因此您将不受任何正在运行的线程的支配。这也适用于正在运行的中断处理程序或正在运行的优先级更高的线程—仅因为您处于“就绪”状态并不意味着您正在消耗CPU。

第二个原因有些微妙。下图将帮助解释原因:

时钟抖动。

问题是您的请求与时钟源异步。您无法将硬件时钟与您的请求同步。因此,根据从硬件时钟周期中开始请求的位置,您将获得从仅仅20多个毫秒到不到30毫秒的延迟。

这是关键。时钟抖动是生活中令人难过的事实。解决该问题的方法是提高系统的时序分辨率,以使您的时序在公差范围内。(我们将在下面的“获取和设置实时时钟”部分中了解如何执行此操作。)请记住,抖动仅发生在第一个刻度上–延迟100秒和10毫秒时钟将延迟大于100秒且小于100.01秒。

计时器类型

我上面显示的计时器类型是相对计时器。选择的超时时间是相对于当前时间的。如果您希望计时器将线程延迟到美国东部时间2005年1月20日12:04:33,则必须计算从“现在”到此的秒数,并为该秒数设置一个相对计时器。因为这是一个相当普遍的功能,中微子实现了一个绝对定时器,将延迟_,直到在指定的时间(而不是对于_在指定的时间,喜欢的相对时间计时器)。

如果你想要做的东西,而你等待该日恢复过来?或者,如果您想做某事并每27秒获得一次“click”怎么办?您当然无法负担沉睡!

正如我们在“进程和线程”一章中讨论的那样,您可以简单地启动另一个线程来完成工作,并且您的线程可能会花费一些时间。但是,由于我们在谈论计时器,因此我们将研究另一种方式。

您可以根据自己的目标使用定期或单次计时器来执行此操作。甲周期定时器是一个熄灭周期性,通知线程(一遍一遍),该一定的时间间隔是否已经过去。一个单次计时器是一个熄灭一次。

内核中的实现仍然基于与我们在第一个示例中使用的延迟计时器相同的原理。内核会花费绝对时间(如果您以这种方式指定的话)并进行存储。在时钟ISR中,以通常的方式将存储的时间与一天中的时间进行比较。

但是,在调用内核时,不会从运行队列中删除线程,而是继续运行线程。当一天中的时间达到存储的时间时,内核会通知您的线程已达到指定的时间。

通知方案

您如何收到超时通知?使用延迟计时器,您可以通过再次准备就绪来接收通知。

使用定期计时器和一次性计时器,您可以选择:

  • 发送一个脉冲
  • 发送信号
  • 创建一个线程

我们已经在“ 消息传递”一章中讨论了脉冲。signal是一种标准的UNIX风格的机制,稍后我们将看到线程创建通知类型。

如何填写 struct sigevent

系统通过传递sigevent来传递时钟,所以必须要对其的参数进行配置

让我们快速看一下如何填写struct sigevent结构。

无论您选择哪种通知方案,都需要填写一个 struct sigevent结构:

struct sigevent {
    int                 sigev_notify;

    union {
        int             sigev_signo;
        int             sigev_coid;
        int             sigev_id;
        void          (*sigev_notify_function) (union sigval);
    };

    union sigval        sigev_value;

    union {
        struct {
            short       sigev_code;
            short       sigev_priority;
        };
        pthread_attr_t *sigev_notify_attributes;
    };
};
请注意,以上定义使用匿名union和struct。仔细检查头文件,您将了解如何在不支持这些功能的编译器上实现此技巧。基本上,有一个 #define使用命名联合和结构使它看起来像一个匿名联合。查看<sys/siginfo.h>详细信息。

您必须填写的第一个字段是sigev_notify 成员。这将确定您选择的通知类型:

  • SIGEV_PULSE

将发送一个脉冲。

  • SIGEV_SIGNAL,SIGEV_SIGNAL_CODE或SIGEV_SIGNAL_THREAD

将发送信号。

  • SIGEV_UNBLOCK

在这种情况下不使用;与内核超时一起使用(请参见下面的“内核超时”)。

  • SIGEV_INTR

在这种情况下不使用;与中断一起使用(请参阅“ 中断”一章)。

  • SIGEV_THREAD

创建一个线程。

因为我们将要使用的struct sigevent与定时器,我们仅关注SIGEV_PULSE,为SIGEV_SIGNAL 和SIGEV_THREAD值sigev_notify* ; 我们将看到上面列表中提到的其他类型。

脉冲通知

要发送一个脉冲时,计时器火灾,设置sigev_notify 领域SIGEV_PULSE,并提供一些额外的信息:

领域 价值和意义
_sigev_coid_ 将脉冲发送到与此连接ID关联的通道。
_sigev_value_ 一个32位值,该值发送到_sigev_coid_字段中标识的连接。
_sigev_code_ 一个8位值,该值发送到_sigev_coid_字段中标识的连接。
_sigev_priority_ 脉冲的传送优先级。不允许使用零值(太多的人在收到脉冲时被以零优先级运行而被咬了-零优先级是空闲任务运行时的优先级,因此实际上他们正在与Neutrino的IDLE进程竞争,而没有占用太多CPU时间:-)) 。

请注意,sigev_coid可以是与任何通道的连接 (通常,但不一定是与启动事件的流程相关联的通道)。

信号通知

要发送信号,请将sigev_notify字段设置为以下之一:

  • SIGEV_SIGNAL

向该过程发送常规信号。

  • SIGEV_SIGNAL_CODE

将包含8位代码的信号发送到进程。

  • SIGEV_SIGNAL_THREAD

将包含8位代码的信号发送到特定线程。

对于SIGEV_SIGNAL *,您必须填写的其他字段是:

领域 价值和意义
_sigev_signo_ 要发送的信号号(<signal.h>例如,SIGALRM)。
_sigev_code_ 8位代码(如果使用SIGEV_SIGNAL_CODE或SIGEV_SIGNAL_THREAD)。

线程通知

要创建一个线程,每当定时器触发时,设置sigev_notify领域SIGEV_THREAD并填写以下字段:

领域 价值和意义
_sigev_notify_function_ 事件触发时void _接受void _被调用的函数的地址。
_sigev_value_ 作为参数传递给_sigev_notify_function()_函数的值。
_sigev_notify_attributes_ 线程属性结构(有关详细信息,请参见“线程和线程结构”下的“进程和线程”一章)。
此通知类型有点吓人!如果计时器触发得足够多,则可能会创建大量线程,并且如果有更高优先级的线程在等待运行,这可能会消耗系统上的所有可用资源!请谨慎使用!

通知的一般技巧

其中包含一些便利宏,<sys/siginfo.h>可简化通知结构的填充(请参阅sigevent《 Neutrono库参考》中的条目):

  • SIGEV_SIGNAL_INIT (*eventp*, *signo*)

用SIGEV_SIGNAL和适当的信号编号signo填充eventp。

  • SIGEV_SIGNAL_CODE_INIT (*eventp*, *signo*, *value*, *code*)

用SIGEV_SIGNAL_CODE,信号编号signo以及值和code填充eventp。

  • SIGEV_SIGNAL_THREAD_INIT (*eventp*, *signo*, *value*, *code*)

用SIGEV_SIGNAL_THREAD,信号编号signo以及值和code填充eventp。

SIGEV_PULSE_INIT (_eventp_, _coid_, _priority_, _code_, _value_)

填充eventp与SIGEV_SIGNAL_PULSE,在连接到通道COID和一个优先级,代码,和 值。请注意,SIGEV_PULSE_PRIO_INHERIT的优先级有一个特殊值,该值会使接收线程以进程的初始优先级运行。

  • SIGEV_UNBLOCK_INIT (*eventp*)

填写eventp与SIGEV_UNBLOCK。

  • SIGEV_INTR_INIT (*eventp*)

填写eventp与SIGEV_INTR。

  • `SIGEV_THREAD_INIT (eventp, func, val, attributes)

用线程函数(func)和属性结构(attribute)填充eventp。执行线程时,将val中的值传递给func中的函数。

这个函数可以直接调用生成一个线程。

脉冲通知

假设您正在设计一个服务器,该服务器的大部分生命都被RECEIVE阻塞,等待消息。收到一条特别的消息,告诉您您等待的时间终于到来,这不是理想的做法吗?

这种情况正是您应使用脉冲作为通知方案的地方。在下面的“使用计时器”部分,我将向您展示一些示例代码,这些代码可用于获取周期性脉冲消息。

信号通知 Signal notification

另一方面,假设您正在执行某种工作,但不希望该工作永远持续下去。例如,您可能正在等待某些函数调用返回,但是您无法预测需要多长时间。

在这种情况下,使用信号作为通知方案(可能带有信号处理程序)是一个不错的选择(我们稍后将讨论的另一个选择是使用内核超时;也请参见“消息传递”一章中的_NTO_CHF_UNBLOCK)。在下面的“使用计时器” 部分,我们将看到一个使用信号的示例。

另外,如果您仍然不打算在应用程序中接收消息,则使用sigwait()的信号比创建一个接收脉冲的通道便宜。

这一点需要注意。

使用计时器

看完所有这些奇妙的理论之后,让我们将注意力转向一些特定的代码示例,以了解您可以使用计时器做什么。

要使用计时器,您必须:

  1. 创建计时器对象。
  2. 确定您希望收到通知的方式(创建信号,脉冲或线程),并创建通知结构(struct sigevent)。
  3. 确定您希望使用哪种计时器(相对计时器和绝对计时器,单次计时器与定期计时器)。
  4. 启动它。
    让我们按顺序看一下这些。

创建一个计时器

第一步是使用timer_create()创建计时器:

#include <time.h>
#include <sys/siginfo.h>

int
timer_create (clockid_t clock_id,
              struct sigevent *event,
              timer_t *timerid);

该clock_id参数告诉timer_create()函数它要创建这个定时器时间基准。这是POSIX的事– POSIX表示在不同的平台上可以有多个时基,但是每个平台都必须至少支持CLOCK_REALTIME时基。在Neutrino下,有 三个时基可供选择:

  • CLOCK_REALTIME重点是这个
  • CLOCK_SOFTTIME
  • CLOCK_MONOTONIC

现在,我们将忽略CLOCK_SOFTTIME和CLOCK_MONOTONIC,但我们将在下面的“其他时钟源” 部分中再次介绍它们。

Signal, pulse, or thread?

第二个参数是指向struct sigevent 数据结构的指针。该数据结构用于通知内核计时器在“触发”时应传递的事件类型。我们struct sigevent在信号与脉冲与线程创建的讨论中讨论了如何填写以上内容。

因此,您将使用CLOCK_REALTIME和指向您的数据结构的指针调用timer_create()struct sigevent内核将为您创建一个计时器对象(该对象将在最后一个参数中返回)。这个计时器对象只是一个小的整数,它充当内核计时器表的索引;将其视为“句柄”。

在这一点上,没有其他事情发生。您仅 创建了计时器;您尚未触发它。

什么样的计时器?What kind of timer?

在创建了定时器,你现在必须决定什么样的计时器是。这是通过使用timer_settime()的参数组合来完成的,该参数实际上是用来启动计时器的:

创建定时器,然后定义计时器

#include <time.h>

int
timer_settime (timer_t timerid,
               int flags,
               struct itimerspec *value,
               struct itimerspec *oldvalue);

该的timerId说法是价值,你有从后面 timer_create()函数调用-你可以创建一批定时器,然后调用_timer_settime()_分别对他们设置和在您方便的启动它们。

该标志的说法是你指定的绝对与相对。

如果您传递常数TIMER_ABSTIME,则它是绝对的,几乎与您期望的一样。然后,当您希望计时器关闭时,您传递实际的日期和时间。

如果传递零,则认为计时器是相对于当前时间的。

让我们看看如何指定时间。以下是两个数据结构(在中<time.h>)的关键部分:

struct timespec {
    long    tv_sec,
            tv_nsec;
};

struct itimerspec {
    struct timespec it_value,
                    it_interval;
};

中有两个成员struct itimerspec这两个量很重要

  • it_value
    the one-shot value
  • it_interval
    the reload value

该it_value指定是如何从现在开始不久计时器应熄灭(在一个相对定时器的情况下),或者当定时器应熄灭(在绝对定时器的情况下)。计时器启动后,it_interval值会指定一个相对值以重新加载计时器,以便它可以再次触发。请注意,为it_interval指定零值 会使它成为单触发计时器。您可能希望创建一个“纯”定期计时器,只需将it_interval设置为重载值,然后将it_value设置为零。不幸的是,该语句的最后一部分是false —将it_value设置为零会禁用计时器。如果要创建纯周期计时器,请将it_value设置为等于it_interval并将该计时器创建为相对计时器。这将触发一次(针对it_value延迟),然后继续使用it_interval延迟重新加载。

无论是it_value和it_interval成员实际上类型的结构struct timespec,另一个POSIX的事情。该结构使您可以指定亚秒分辨率。第一个成员tv_sec是秒数;第二个成员tv_nsec是当前秒的纳秒数。(这意味着您永远不要将tv_nsec设置为超过10亿的值,这意味着偏移量将超过一秒钟。)

这里有些例子:

it_value.tv_sec = 5;//开始
it_value.tv_nsec = 500000000;//延续时间?
it_interval.tv_sec = 0;//间隔时间
it_interval.tv_nsec = 0;

这将创建一个单响计时器,该计时器将在5.5秒后关闭。(因为有500,000,000纳秒的值,所以得到了“ .5”。)

我们假设将其用作相对计时器,因为如果不使用该计时器,则该时间早已过去(格林尼治标准时间1970年1月1日00:00以后5.5秒)。

这是另一个例子:

it_value.tv_sec = 987654321;
it_value.tv_nsec = 0;
it_interval.tv_sec = 0;
it_interval.tv_nsec = 0;

这将创建一个一次性计时器,该计时器在美国东部时间2001年4月19日(星期四)00:25开始计时。(有许多函数可帮助您在人类可读的日期和“自1970年1月1日,格林尼治标准时间0:00:00开始的秒数”之间进行转换。在C库中查看time()asctime()ctime()mktime()strftime()等)

对于此示例,我们假设它是一个绝对计时器,因为如果相对的话我们要等待的秒数很大(987654321秒大约是31.3年)。

请注意,在这两个例子中,我说,“我们假设…”没有什么的代码为_timer_settime()_是检查这些假设和做“正确”的事!您必须自己指定计时器是绝对计时器还是相对计时器。内核将愉快地计划未来31.3年。

最后一个例子:

it_value.tv_sec = 1;
it_value.tv_nsec = 0;
it_interval.tv_sec = 0;
it_interval.tv_nsec = 500000000;

假设它是相对的,此计时器将在一秒钟后关闭,然后每隔半秒再次关闭。绝对没有要求重载值看起来像一次性值。

这四个数的含义是什么?不是很清楚。

具有周期性脉冲的服务器

为什么服务器生产周期脉冲?

我们首先要看的是要获取定期消息的服务器。最典型的用途是:

  • 服务器维护的客户端请求超时
  • 定期服务器维护周期

当然,这些东西还有其他特殊用途,例如需要定期发送的网络“保持活动”消息,重试请求等。

服务器维护的超时

在这种情况下,服务器正在向客户端提供某种服务,并且客户端可以指定超时。在很多地方都使用此功能。例如,您可能希望告诉服务器,“让我获得15秒钟的数据价值”,或“让我知道10秒钟何时结束”,或“等待数据显示,但如果没有出现2分钟之内,超时。”

这些都是服务器维护超时的示例。客户端向服务器发送一条消息,然后进行阻止。服务器从计时器接收周期性的消息(也许每秒一次,也许或多或少的频率),并计算接收到的消息数量。当超时消息的数量超过客户端指定的超时时,服务器将使用某种超时指示或到目前为止累积的数据回复客户端,这实际上取决于客户端/服务器关系的结构。

这是一个服务器的完整示例,该服务器接受来自客户端的两个消息之一和来自脉冲的超时消息。第一种客户消息类型是:“让我知道是否有可用数据,但请不要让我停留5秒钟以上。” 第二种客户端消息类型是:“这里有一些数据。” 服务器应允许在其上阻止多个客户端,等待数据,因此必须将超时与客户端关联。这是脉冲消息进入的地方。它说:“一秒钟过去了。”

为了使代码示例免于压倒性的麻烦,我在每个主要部分之前都添加了一些文本。您可以time1.c在示例程序附录中找到的完整版本。

声明书

这里的代码的第一部分设置了我们将要使用的各种清单常量,数据结构,并包括了所有所需的头文件。我们将在不加评论的情况下进行介绍。:-)

/*
 *  time1.c
 *
 *  Example of a server that receives periodic messages from
 *  a timer, and regular messages from a client.
 *
 *  Illustrates using the timer functions with a pulse.
*/

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/siginfo.h>
#include <sys/neutrino.h>

// message send definitions

// messages
#define MT_WAIT_DATA        2       // message from client
#define MT_SEND_DATA        3       // message from client

// pulses
#define CODE_TIMER          1       // pulse from timer

// message reply definitions
#define MT_OK               0       // message to client
#define MT_TIMEDOUT         1       // message to client

// message structure
typedef struct
{
    // contains both message to and from client
    int messageType;
    // optional data, depending upon message
    int messageData;
} ClientMessageT;

typedef union
{
    // a message can be either from a client, or a pulse
    ClientMessageT  msg;
    struct _pulse   pulse;
} MessageT;

// client table
#define MAX_CLIENT 16       // max # of simultaneous clients

struct
{
    int in_use;             // is this client entry in use?
    int rcvid;              // receive ID of client
    int timeout;            // timeout left for client
}   clients [MAX_CLIENT];   // client table

int     chid;               // channel ID (global)
int     debug = 1;          // set debug value, 1=on, 0=off
char    *progname = "time1.c";

// forward prototypes
static  void setupPulseAndTimer (void);
static  void gotAPulse (void);
static  void gotAMessage (int rcvid, ClientMessageT *msg);

主要()

下一部分代码是主线。它负责:

  • 创建频道(通过ChannelCreate()),
  • 调用_setupPulseAndTimer()_例程(设置一个每秒一次的计时器,以脉冲作为事件传递方法),然后
  • 坐在“永远做”的循环中,等待脉冲或消息并对其进行处理。

请注意,对MsgReceive()的返回值进行了检查-零表示它是一个脉冲(并且我们不做任何强力检查以确保它是我们的脉冲),非零表示它是一条消息。脉冲或消息的处理由gotAPulse()gotAMessage()完成

为什么0是脉冲,非0就是消息呢?
MsgReceive在间隔一段时间t之后收到时钟的脉冲。

int
main (void)                 // ignore command-line arguments
{
    int rcvid;              // process ID of the sender
    MessageT msg;           // the message itself

    if ((chid = ChannelCreate (0)) == -1) {
        fprintf (stderr, "%s:  couldn't create channel!\n",
                 progname);
        perror (NULL);
        exit (EXIT_FAILURE);
    }

    // set up the pulse and timer
    setupPulseAndTimer ();

    // receive messages
    for (;;) {
        rcvid = MsgReceive (chid, &msg, sizeof (msg), NULL);

        // determine who the message came from
        if (rcvid == 0) {
            // production code should check "code" field...
            gotAPulse ();
        } else {
            gotAMessage (rcvid, &msg.msg);
        }
    }

    // you'll never get here
    return (EXIT_SUCCESS);
}

setupPulseAndTimer()

在setupPulseAndTimer()中,您将看到我们在其中定义计时器和通知方案类型的代码。当我们在上面的文本中谈到定时器函数调用时,我说定时器可以传递信号,脉冲或导致创建线程。该决定是在这里(在setupPulseAndTimer()中)做出的。注意,我们使用了宏SIGEV_PULSE_INIT()。通过使用宏,我们有效地分配值SIGEV_PULSE到sigev_notify成员。(如果我们改用SIGEV_SIGNAL * _INIT()宏之一,它将传递指定的信号。)请注意,对于脉冲,我们通过ConnectAttach()将连接设置回自己调用,并为其提供一个唯一标识它的代码(我们选择了清单常量CODE_TIMER;这是我们定义的内容)。事件结构初始化中的最后一个参数是脉冲的优先级。我们选择了SIGEV_PULSE_PRIO_INHERIT(常数-1)。这告诉内核在脉冲到达时不要更改接收线程的优先级。

在此函数底部附近,我们调用timer_create()在内核中创建一个计时器对象,然后用数据填充该数据,该数据说它应该在一秒钟内消失(it_value成员),并且应该重新加载一个-second重复(it_interval 成员)。请注意,仅当我们调用timer_settime()时才激活计时器,而不是在创建计时器时将其激活。

SIGEV_PULSE通知方案是Neutrino扩展-POSIX没有脉冲的概念。
/*
 *  setupPulseAndTimer
 *
 *  This routine is responsible for setting up a pulse so it
 *  sends a message with code MT_TIMER.  It then sets up a
 *  periodic timer that fires once per second.
*/

void
setupPulseAndTimer (void)
{
    timer_t             timerid;    // timer ID for timer
    struct sigevent     event;      // event to deliver
    struct itimerspec   timer;      // the timer data structure
    int                 coid;       // connection back to ourselves

    // create a connection back to ourselves
    coid = ConnectAttach (0, 0, chid, 0, 0);
    if (coid == -1) {
        fprintf (stderr, "%s:  couldn't ConnectAttach to self!\n",
                 progname);
        perror (NULL);
        exit (EXIT_FAILURE);
    }

    // set up the kind of event that we want to deliver -- a pulse
    SIGEV_PULSE_INIT (&event, coid,
                      SIGEV_PULSE_PRIO_INHERIT, CODE_TIMER, 0);

    // create the timer, binding it to the event
    if (timer_create (CLOCK_REALTIME, &event, &timerid) == -1) {
        fprintf (stderr, "%s:  couldn't create a timer, errno %d\n", 
                 progname, errno);
        perror (NULL);
        exit (EXIT_FAILURE);
    }

    // setup the timer (1s delay, 1s reload)
    timer.it_value.tv_sec = 1;
    timer.it_value.tv_nsec = 0;
    timer.it_interval.tv_sec = 1;
    timer.it_interval.tv_nsec = 0;

    // and start it!
    timer_settime (timerid, 0, &timer, NULL);
}

gotAPulse()

在gotAPulse()中,您可以看到我们如何实现服务器使客户端超时的功能。我们沿着客户端列表进行浏览,并且由于我们知道每秒触发一次脉冲,因此我们只是减少了客户端在超时之前离开的秒数。如果该值达到零,我们将通过一条消息“ Sorry,timed out”(MT_TIMEDOUT消息类型)回复该客户端。您会注意到,我们提前(在for循环之外)准备了此消息,然后根据需要发送。这只是样式/使用问题-如果您希望进行很多答复,那么一次设置设置开销可能很有意义。如果您不希望做出太多答复,那么根据需要进行设置可能更有意义。

如果超时值尚未达到零,则我们不对其做任何事情-客户端仍然处于阻塞状态,等待消息显示。

/*
 *  gotAPulse
 *
 *  This routine is responsible for handling the fact that a
 *  timeout has occurred.  It runs through the list of clients
 *  to see which client has timed out, and replies to it with
 *  a timed-out response.
 */

void
gotAPulse (void)
{
    ClientMessageT  msg;
    int             i;

    if (debug) {
        time_t  now;

        time (&now);
        printf ("Got a Pulse at %s", ctime (&now));
    }

    // prepare a response message
    msg.messageType = MT_TIMEDOUT;

    // walk down list of clients
    for (i = 0; i < MAX_CLIENT; i++) {

        // is this entry in use?
        if (clients [i].in_use) {

            // is it about to time out?
            if (--clients [i].timeout == 0) {

                // send a reply
                MsgReply (clients [i].rcvid, EOK, &msg,
                          sizeof (msg));

                // entry no longer used
                clients [i].in_use = 0;
            }
        }
    }
}

gotAMessage()

在gotAMessage()中,您将看到功能的另一半,在该功能中,我们将客户端添加到等待数据的客户端列表中(如果是MT_WAIT_DATA消息),或者将客户端与刚刚到达的消息进行匹配(如果它是MT_SEND_DATA消息)。请注意,为简单起见,我们没有添加等待发送数据的客户端队列,但是尚无可用的接收器队列,这是队列管理问题,供读者练习!

队列管理? queue management issue?是指目前没有传递消息吗?
这里的time_out是什么意思?翻译成超时肯定不准确。

/*
 *  gotAMessage
 *
 *  This routine is called whenever a message arrives.  We
 *  look at the type of message (either a "wait for data"
 *  message, or a "here's some data" message), and act
 *  accordingly.  For simplicity, we'll assume that there is
 *  never any data waiting.  See the text for more discussion
 *  about this.
*/

void
gotAMessage (int rcvid, ClientMessageT *msg)
{
    int i;

    // determine the kind of message that it is
    switch (msg -> messageType) {

    // client wants to wait for data
    case    MT_WAIT_DATA:

        // see if we can find a blank spot in the client table
        for (i = 0; i < MAX_CLIENT; i++) {

            if (!clients [i].in_use) {

                // found one -- mark as in use, save rcvid, set timeout
                clients [i].in_use = 1;
                clients [i].rcvid = rcvid;
                clients [i].timeout = 5;
                return;
            }
        }

        fprintf (stderr, "Table full, message from rcvid %d ignored, "
                         "client blocked\n", rcvid);
        break;

    // client with data
    case    MT_SEND_DATA:

        // see if we can find another client to reply to with
        // this client's data
        for (i = 0; i < MAX_CLIENT; i++) {

            if (clients [i].in_use) {

                // found one -- reuse the incoming message
                // as an outgoing message
                msg -> messageType = MT_OK;

                // reply to BOTH CLIENTS!
                MsgReply (clients [i].rcvid, EOK, msg,
                          sizeof (*msg));
                MsgReply (rcvid, EOK, msg, sizeof (*msg));

                clients [i].in_use = 0;
                return;
            }
        }

        fprintf (stderr, "Table empty, message from rcvid %d ignored, "
                         "client blocked\n", rcvid);
        break;
    }
}

笔记

有关代码的一些一般说明:

  • 如果没有人在等待并且有数据消息到达,或者列表中没有空间容纳新的服务员客户端,则我们将消息打印为标准错误,但从不答复客户端。这意味着有些客户可能坐在那里,

回复

-永远被阻止-我们丢失了他们的接收ID,因此我们无法在以后回复他们。

这是设计中有意的。您可以对其进行修改,以分别添加MT_NO_WAITERS和MT_NO_SPACE消息,只要检测到这些错误,就可以返回这些消息。

  • 当服务端客户端正在等待,并且有数据提供客户端发送给它时,我们将回复这两个客户端。这是至关重要的,因为我们希望两个 客户端都能解除阻塞。

  • 我们将提供数据的客户端缓冲区重新用于两次回复。这又是一个样式问题-在较大的应用程序中,您可能必须具有多种类型的返回值,在这种情况下,您可能不想重复使用同一缓冲区。

  • 此处显示的实现使用带有“使用中”标志(clients[i].in_use)的“低俗”固定长度数组。由于我的目标不是展示所有者列表的技巧和用于单链列表管理的技术,因此,我展示了最容易理解的版本。当然,在生产代码中,您可能会使用动态管理的存储块的链表。

  • 当消息到达MsgReceive()时,我们将根据弱检查来确定是否实际上是“我们的”脉冲-我们假定(根据注释)所有脉冲都是CODE_TIMER脉冲。同样,在生产代码中,您需要检查脉冲的代码值并报告任何异常情况。

请注意,上面的示例仅显示了一种实现客户端超时的方法。在本章的后面(“ 内核超时 ”中),我们将讨论内核超时,这是实现几乎完全相同的事情的另一种方式,除了它是由客户端而不是计时器驱动的。

定期服务器维护周期

在这里,我们对于定期超时消息有稍微不同的用法。这些消息纯粹是供服务器内部使用的,通常与客户端完全无关。

例如,某些硬件可能要求服务器定期轮询它,就像网络连接一样—服务器应该查看连接是否仍处于“启动”状态,而不管来自客户端的任何指示。

如果硬件具有某种“非活动关闭”计时器,则可能会发生另一种情况。例如,由于长时间使一块硬件通电可能会浪费电源,因此,如果没有人使用该硬件达10秒钟,则可以将其断电。同样,这与客户端无关(除了客户端请求将取消此非活动电源关闭)—这只是服务器必须能够为其硬件提供的功能。

在代码方面,这将与上面的示例非常相似,不同的是,除了拥有一个正在等待的客户端列表之外,您只有一个超时变量。每当计时器事件到达时,此变量将减小。如果为零,则将导致硬件关闭(或此时您希望执行的其他任何活动)。如果它仍然大于零,则不会发生任何事情。

设计中唯一的“难题”是,每当来自使用硬件的客户端收到消息时,您都必须将超时变量重置为其完整值-让某人使用该资源重置“倒数”。相反,硬件可能需要一定的“预热”时间才能从掉电中恢复。在这种情况下,一旦关闭硬件电源,则一旦客户端发出请求,您就必须设置其他计时器。此计时器的目的是延迟客户端对硬件的请求,直到硬件再次上电为止。

计时器传送信号

到目前为止,除了一件小事情之外,我们几乎已经看到了计时器的所有功能。我们一直在传递消息(通过脉冲),但是您也可以传递POSIX信号。让我们看看如何做到这一点:

timer_create (CLOCK_REALTIME, NULL, &timerid);

这是创建向您发送信号的计时器的最简单方法。计时器触发时,此方法将引发SIGALRM。如果我们实际提供了struct sigevent,则可以指定我们实际想要获得的信号:

struct sigevent event;

SIGEV_SIGNAL_INIT (&event, SIGUSR1);
timer_create (CLOCK_REALTIME, &event, &timerid);

这给我们打了SIGUSR1而不是SIGALRM。

您可以使用普通的信号处理程序捕获计时器信号,它们没有什么特别的。

计时器创建线程

如果您想在每次触发计时器时创建一个新线程,则可以使用struct sigevent我们刚刚讨论过的及所有其他计时器来实现:

struct sigevent event;
SIGEV_THREAD_INIT (&event, maintenance_func, NULL);

您需要特别注意这一点,因为如果指定的间隔太短,就会被新线程淹没!这可能会耗尽您所有的CPU和内存资源!

获取和设置实时时钟等

除了使用计时器外,您还可以获取并设置当前的实时时钟,并对其进行逐步调整。以下功能可用于这些目的:

功能 类型? 描述
_ClockAdjust()_ 中微子 逐渐调整时间
_ClockCycles()_ 中微子 高分辨率快照
_clock_getres()_ POSIX 获取基本时序分辨率
_clock_gettime()_ POSIX 获取当前时间
_ClockPeriod()_ 中微子 获取或设置基本定时分辨率
_clock_settime()_ POSIX 设置当前时间
_时钟时间()_ 中微子 获取或设置当前时间

获取和设置

函数clock_gettime()clock_settime()是基于内核函数ClockTime()的POSIX函数。这些功能可用于获取或设置当前时间。不幸的是,将其设置为“硬”调整,意味着您在缓冲区中指定的任何时间都将立即作为当前时间。这可能会产生惊人的后果,尤其是当时间似乎在“向后”移动时,因为时间早于“真实”时间。通常,仅在上电期间或时间与实时不同步时才应使用此方法设置时钟。

也就是说,要使当前时间逐渐变化,可以使用函数ClockAdjust()

int
ClockAdjust (clockid_t id,
             const struct _clockadjust *new,
             const struct _clockadjust *old);

该参数是时钟源(总是使用CLOCK_REALTIME),以及新的和旧的参数。无论是新的和旧的参数是可选的,并且可以为NULL。在旧参数只是返回当前的调整。时钟调整操作通过新参数控制,该参数是指向包含两个元素tick_nsec_inc和tick_count的结构的指针。基本上,ClockAdjust()的操作非常简单。在下一个tick_count时钟滴答声中,tick_nsec_inc中包含的调整被添加到当前系统时钟。这意味着要向前移动时间(实时“赶上”),您可以为tick_nsec_inc指定一个正值。请注意,您永远不会倒退时间!相反,如果您的时钟太快,则可以为tick_nsec_inc指定一个较小的负数,这将导致当前时间不如预期的那样快。如此有效,您已经放慢了时钟直到它与现实相符。一条经验法则是,您调整时钟的时间不应超过系统基本计时分辨率的10%(如我们将在下面讨论的函数 _ClockPeriod()_和朋友所示)。

调整时基

正如我们在本章中一直说的那样,系统中所有组件的时序分辨率都不会比进入系统的基本时序分辨率更准确。因此,显而易见的问题是,如何设置基本时序分辨率?您可以为此使用ClockPeriod()函数:

int
ClockPeriod (clockid_t id,
             const struct _clockperiod *new,
             struct _clockperiod *old,
             int reserved);

与上述ClockAdjust()函数一样,新参数和旧参数是如何获取和/或设置基本时序分辨率的值。在新的和旧的参数是指向的结构struct _clockperiod,其中包含两个成员,纳秒和FRACT。目前,FRACT成员必须设置为零(这是飞秒的数量,我们可能不会使用这种分辨率的一小会儿呢!)的纳秒成员指示基本计时时钟的刻度之间经过了多少纳秒。默认值为10毫秒(在CPU速度大于40 MHz的计算机上为1毫秒),因此nsec成员(如果通过指定旧参数使用调用的“ get”形式)将显示大约1000万毫微秒。(正如我们上面所讨论的,在“时钟中断源”中,它不会 精确到 10毫秒。)

尽管您当然可以随意尝试将系统的基本时序分辨率设置为小得离谱,但内核会介入并阻止您这样做。通常,您可以将大多数系统设置在1毫秒到数百微秒的范围内。

准确的时间戳

您的处理器上可能有一个不符合我们刚刚描述的“基本时序分辨率”规则的时基。一些处理器中内置有一个高频(高精度)计数器,Neutrono可以让您通过ClockCycles()调用来访问它。例如,在以200 MHz运行的Pentium处理器上,该计数器也以200 MHz递增,因此它可以为您提供定时采样,低至5纳秒。如果您想弄清楚一段代码执行需要多长时间(当然,假设您没有被抢占),这将特别有用。您可以在代码之前和代码之后调用ClockCycles(),然后计算增量。有关更多详细信息,请参见《中微子库参考》。

请注意,在SMP系统上,您可能会遇到一些小问题。如果您的线程从一个CPU 获取_ClockCycles()_值,然后最终在另一个CPU上运行,则可能会得到不一致的结果。这是因为_ClockCycles()_使用的计数器存储在CPU芯片本身中,并且在CPU之间不同步。解决方案是使用线程亲和力来强制线程在特定CPU上运行。

进阶主题

既然我们已经了解了计时器的基础知识,我们将介绍一些高级主题:

  1. CLOCK_SOFTTIME和CLOCK_MONOTONIC计时器类型,以及
  2. 内核超时

其他时钟源

我们已经看到了时钟源CLOCK_REALTIME,并提到符合POSIX的实现可以提供尽可能多的时钟源,只要它至少提供CLOCK_REALTIME。

什么是时钟源?简而言之,它是时序信息的抽象来源。如果您想将其应用于现实生活中,那么您的个人手表就是时钟源。它测量时间流逝的速度。您的手表与其他人的手表会有不同的准确性。您可能会忘记给手表上弦,或者给它换新电池,并且时间似乎会“冻结”一段时间。或者,您可能会调整手表,突然间似乎“跳了起来”。这些都是时钟源的特征。

在Neutrino下,CLOCK_REALTIME基于Neutrino提供的“当前时间”时钟。(在下面的示例中,我们将此称为“ Neutrino时间”。)这意味着,如果系统正在运行,并且突然有人将时间向前调整了5秒,则更改可能会或可能不会对您的程序产生不利影响(取决于你在做什么)。让我们来看一个sleep (30);电话:

即时的 中微子时间 活动
11:22:05 11:22:00 sleep (30);
11:22:15 11:22:15 时钟调整为11:22:15;太慢了5秒!
11:22:35 11:22:35 sleep (30); 醒来

美丽!该线程完全符合您的预期:在11:22:00进入睡眠状态三十秒钟,在11:22:35(三十秒后经过30秒)醒来。请注意,_sleep()_如何“出现”于睡眠35秒而不是30秒;但实际上,经过的时间只有30秒,因为Neutrino的时钟提前了五秒(11:22:15)。

内核知道_sleep()_调用是一个相对的计时器,因此要确保经过指定的“实时”时间。

现在,另一方面,如果我们使用了绝对计时器,并且在“中性时间”的11:22:00告诉内核在11:22:30唤醒我们该怎么办?

即时的 中微子时间 活动
11:22:05 11:22:00 于11:22:30醒来
11:22:15 11:22:15 时钟像以前一样调整
11:22:30 11:22:30 醒来

这也和您期望的一样—您想在11:22:30醒来,并且(尽管调整了时间)。

但是,这里有一个小转折。例如,如果看一下pthread_mutex_timedlock()函数,您会注意到它具有绝对超时值,而不是相对值:

int
pthread_mutex_timedlock (pthread_mutex_t *mutex,
                         const struct timespec *abs_timeout);

可以想象,如果我们尝试实现一个在30秒内超时的互斥锁,可能会出现问题。让我们逐步进行。在11:22:00(Neutrino时间),我们决定要尝试锁定互斥锁,但是我们最多只希望阻塞30秒。由于_pthread_mutex_timedlock()_函数占用绝对时间,因此我们执行了一个计算:将当前时间加30秒,得出11:22:30。如果遵循上面的示例,我们将在11:22:30醒来,这意味着我们只将互斥锁锁定了25秒,而不是整个30秒。

CLOCK_MONOTONIC

POSIX人们对此进行了思考,他们想出的解决方案是使_pthread_mutex_timedlock()函数基于CLOCK_MONOTONIC而不是CLOCK_REALTIME。它内置在pthread_mutex_timedlock()_函数中,您不能更改。

CLOCK_MONOTONIC的工作方式是永远不会调整其时基。这样做的影响是,无论现实世界中的时间是什么,如果您将计时器设置为CLOCK_MONOTONIC并为其添加30秒(然后对时间进行任何调整),则计时器将在30秒后过期秒。

时钟源CLOCK_MONOTONIC具有以下特征:

  • 总是增加数量
  • 基于实时时间
  • 从零开始
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Id9d93Fs-1607647324405)(…/…/…/…/…/…/…/…/pointing.gif)] 关于时钟从零开始的重要一点是,这是不同于CLOCK_REALTIME 1970年1月1日格林尼治标准时间00:00:00的时期。因此,即使两个时钟以相同的速率运行,它们的值 _也不_可互换。

那么CLOCK_SOFTTIME是做什么的呢?

如果我们想按“硬度”对时钟源进行排序,我们将有以下顺序。您可以将CLOCK_MONOTONIC视为一列货运火车,它对任何人都不会停止。列表中的下一个是CLOCK_REALTIME,因为它可以被推动一点(如我们在时间调整中所看到的)。最后,我们有CLOCK_SOFTTIME,可以推 很多。

CLOCK_SOFTTIME的主要用途是用于“软”的东西-如果不做就不会造成严重故障的东西。仅当CPU运行时,CLOCK_SOFTTIME才处于“活动”状态。(是的,这听起来确实很明显,:-)但是请稍等!)当由于电源管理检测到一段时间内没有任何反应而导致CPU断电时,CLOCK_SOFTTIME也将断电!

这是显示三个时钟源的时序图:

即时的 中微子时间 活动
11:22:05 11:22:00 在“现在” + 00:00:30醒来(如下所示)
11:22:15 11:22:15 时钟像以前一样调整
11:22:20 11:22:20 电源管理关闭CPU
11:22:30 11:22:30 CLOCK_REALTIME醒来
11:22:35 11:22:35 CLOCK_MONOTONIC唤醒
11:45:07 11:45:07 电源管理打开了CPU,CLOCK_SOFTTIME唤醒了

这里有几件事要注意:

  • 我们将唤醒时间预先计算为“现在”加30秒,并使用绝对计时器在计算出的时间唤醒我们。这 不同于使用相对计时器在 30秒内唤醒。
  • 请注意,为了方便将示例放在一个时间轴上,我们撒了一点谎言。如果确实确实唤醒了CLOCK_REALTIME线程,(随后对CLOCK_MONOTONIC也是如此),它将导致我们当时退出电源管理模式,这将导致CLOCK_SOFTTIME唤醒。

当CLOCK_SOFTTIME“过度睡眠”时,它会尽快唤醒-它不会在CPU掉电时停止“计时”,只是在CPU通电后才可以唤醒。除此之外,CLOCK_SOFTTIME就像CLOCK_REALTIME。

使用不同的时钟源

要指定不同的时钟源之一,请使用接受时钟ID的POSIX计时功能。例如:

#include <time.h>

int
clock_nanosleep (clockid_t clock_id,
                 int flags,
                 const struct timespec *rqtp,
                 struct timespec *rmtp);

所述clock_nanosleep()函数接受clock_id参数(告诉它该时钟源来使用),一个标志(它决定如果时间是相对的或绝对的)的,“请求的睡眠时间”参数(rqtp),以及一个指针,指向函数可以填充剩余时间的区域(在rmtp参数中,如果您不在乎,则可以为NULL)。

内核超时

Neutrino让您拥有与所有内核阻塞状态关联的超时。我们在“ 进程和线程”一章的“内核状态”一节中讨论了阻塞状态。大多数情况下,您需要在传递消息时使用它。客户端将向服务器发送消息,但是客户端不希望“永远”等待服务器响应。在这种情况下,内核超时是合适的。内核超时对于pthread_join()函数也很有用。您可能要等待线程完成,但是您可能不想等待太久。

这是TimerTimeout()函数调用的定义,它是负责内核超时的内核函数:

#include <sys/neutrino.h>

int
TimerTimeout (clockid_t id,
              int flags,
              const struct sigevent *notify,
              const uint64_t *ntime,
              uint64_t *otime);

这表示TimerTimeout()返回一个整数(通过/失败指示,-1表示调用失败并设置errno,零表示成功)。时间源(CLOCK_REALTIME等)在id中传递,并且flags参数给出相关的内核状态。该通知应该总是类型SIGEV_UNBLOCK的通知事件,并且n时间是当内核调用应该超时的相对时间。该otime参数表示超时前值-这不是在绝大多数情况下使用(你可以传递NULL)。

这是需要注意的是超时重要_武装_通过_TimerTimeout()_ ,并_触发_对进入由指定的内核状态的一个_标志_。它是_清除_在从任何内核调用返回。这意味着您必须在_每个_希望超时的内核调用之前重新设防超时 。在内核调用_之后_,您不必清除超时;这是自动完成的。

使用pthread_join()的内核超时

要考虑的最简单情况是与pthread_join()调用一起使用的内核超时。设置方法如下:

/*
 * part of tt1.c
*/

#include <sys/neutrino.h>

// 1 billion nanoseconds in a second
#define SEC_NSEC 1000000000LL 

int
main (void) // ignore arguments
{
    uint64_t        timeout;
    struct sigevent event;
    int             rval;

    …
    // set up the event -- this can be done once

    // This or event.sigev_notify = SIGEV_UNBLOCK:
    SIGEV_UNBLOCK_INIT (&event);

    // set up for 10 second timeout
    timeout = 10LL * SEC_NSEC;

    TimerTimeout (CLOCK_REALTIME, _NTO_TIMEOUT_JOIN,
                  &event, &timeout, NULL);

    rval = pthread_join (thread_id, NULL);
    if (rval == ETIMEDOUT) {
        printf ("Thread %d still running after 10 seconds!\n",
                thread_id);
    }
    …

(您可以tt1.c在示例程序附录中找到的完整版本。)

我们使用SIGEV_UNBLOCK_INIT()宏来初始化事件结构,但我们也能设置sigev_notify成员SIGEV_UNBLOCK自己。更优雅的是,我们可以传递NULL,因为struct sigevent— _TimerTimeout()_理解为这意味着它应该使用SIGEV_UNBLOCK。

如果线程(在thread_id中指定)仍在10秒后仍在运行,则内核调用将超时— pthread_join()将以错误码ETIMEDOUT 返回。

您可以使用另一种快捷方式-通过为超时值(在上面的正式声明中为_ntime)指定NULL ,可以告诉内核不要在给定状态下阻塞。这可以用于轮询。(虽然通常不建议使用轮询,但是在使用pthread_join()_的情况下,您可以非常有效地使用它-您可以定期轮询以查看您感兴趣的线程是否已经完成。否则,您可以执行其他工作。 )

这是一个代码示例,显示了非阻塞的pthread_join():

int
pthread_join_nb (int tid, void **rval)
{
    TimerTimeout (CLOCK_REALTIME, _NTO_TIMEOUT_JOIN, 
                  NULL, NULL, NULL);
    return (pthread_join (tid, rval));
}

通过消息传递的内核超时

当您将内核超时与消息传递一起使用时,事情会变得有些棘手。回想一下“ 消息传递”一章(在“消息传递和客户端/服务器”部分中),当客户端发送消息时,服务器可能正在或可能未在等待消息。这意味着客户端可能以SEND阻止状态(如果服务器尚未收到消息)或REPLY阻止状态(如果服务器已收到消息并且尚未回复)被阻止。 。这意味着您应该为TimerTimeout()的flags参数指定两种阻塞状态,因为客户端可能在两种状态下都被阻塞。

要指定多个状态,只需将它们或在一起:

TimerTimeout (… _NTO_TIMEOUT_SEND | _NTO_TIMEOUT_REPLY, …);

每当内核进入SEND阻止状态或REPLY阻止状态时,这都会导致超时活动。进入SEND阻止状态并超时没有什么特别的-服务器尚未收到消息,因此服务器没有代表客户端进行任何活动。这意味着,如果内核使阻止SEND的客户端超时,则不必通知服务器。客户端的MsgSend() 函数返回ETIMEDOUT指示,并且该超时的处理已完成。

但是,如“消息传递”一章(在“ _NTO_CHF_UNBLOCK”下)所述,如果服务器已经收到客户端的消息,并且客户端希望取消阻止,则服务器有两种选择。如果服务器没有在接收消息的通道上指定_NTO_CHF_UNBLOCK,则客户端将立即被解除阻止,并且服务器将不会收到任何解除阻止的指示。我见过的大多数服务器 始终启用_NTO_CHF_UNBLOCK标志。在这种情况下,内核会向服务器发送一个脉冲,但是客户端将保持阻塞状态,直到服务器回复为止!正如在“消息传递”一章的上面引用的部分中提到的那样,这样做是为了使服务器指示它应该对客户端的取消阻止请求执行某些操作。

摘要

我们研究了Neutrino的基于时间的功能,包括计时器及其使用方式,以及内核超时。相对计时器“在一定秒数内”提供某种形式的事件,而绝对计时器“在一定时间”内提供该事件。计时器(通常说来是struct sigevent)会导致脉冲,信号或线程的传递开始。

内核通过将代表下一个“event”的绝对时间存储在已排序的队列中,并将当前时间(由计时器滴答中断服务程序得出)与已排序队列的开头进行比较,来实现计时器。当当前时间大于或等于队列的第一个成员时,将对队列进行处理(针对所有匹配的条目),并且内核将调度事件或线程(取决于队列条目的类型)并(可能)重新调度。

为了提供对省电功能的支持,您应该在不需要定期计时器时将其禁用–否则,省电功能将无法实现省电,因为它认为需要定期执行某些操作。您也可以使用CLOCK_SOFTTIME时钟源,除非您当然确实希望计时器破坏节电功能。

给定不同类型的时钟源,您可以灵活地确定时钟和计时器的基础。从“实际的经过时间”一直到基于电源管理活动的时间源。