zoukankan      html  css  js  c++  java
  • TLPI读书笔记第23章:定时器与休眠3

    23.6 POSIX 间隔式定时器

    使用 setitimer()来设置经典 UNIX 间隔式定时器,会受到如下制约。

    1.针对 ITIMER_REAL、 ITIMER_VIRTUAL 和 ITIMER_PROF 这 3 类定时器,每种只能设置一个。

    2.只能通过发送信号的方式来通知定时器到期。另外,也不能改变到期时产生的信号。

    3.如果一个间隔式定时器到期多次,且相应信号遭到阻塞时,那么会只调用一次信号处理器函数。换言之,无从知晓是否出现过定时器溢出(timer overrun)的情况。

    4.定时器的分辨率只能达到微秒级。不过,一些系统的硬件时钟提供了更为精细的时钟分辨率,软件此时应采用这一较高分辨率。

    POSIX.1b 定义了一套 API 来突破这些限制, Linux 2.6 实现了这一 API。

    POSIX 定时器 API 将定时器生命周期划分为如下几个阶段。 1.以系统调用 timer_create()创建一个新定时器,并定义其到期时对进程的通知方法。

    2.以系统调用 timer_settime()来启动或停止一个定时器。

    3.以系统调用 timer_delete()删除不再需要的定时器。

    由 fork()创建的子进程不会继承 POSIX 定时器。 调用 exec()期间亦或进程终止时将停止并删除定时器。 Linux 上,调用 POSIX 定时器 API 的程序编译时应使用-lrt 选项,从而与 librt(实时)函数库相链接。

    23.6.1 创建定时器: timer_create()

    函数 timer_create()创建一个新定时器,并以由 clockid 指定的时钟来进行时间度量。

    #include<signal.h>
    #include<time.h>
    int timer_create(clockid_t clockid,struct sigevent *evp,timer_t *timerid);
    unoin sigval{
       int sival_int;
       void *sival_ptr;
    };
    struct sigevent{
       int sigev_notify;
       int sigev_signo;
       union sigval sigev_value;
       union {
           pid_t _tid;
           struct {
               void (*_function)(union sigval);
               void *_attribute;
          } _sigev_thread;
      } _sigev_un;
    }

    设置参数 clockid,可以使用表 23-1 中的任意值,也可以采用 clock_getcpuclocid()或pthread_getcpuclockid()返回的 clockid 值。函数返回时会在参数 timerid 所指向的缓冲区中放置定时器句柄(handle),供后续调用中指代该定时器之用。这一缓冲区的类型为 timer_t,是一 种由 SUSv3 定义的数据类型,用于标识定时器。 参数 evp 可决定定时器到期时对应用程序的通知方式,指向类型为 sigevent 的数据结构, 具体定义如下:

    关于 sigev_notify 常量值的更多细节,以及 sigval 结构中与每个常量值相关的字段,特做如下说明。 SIGEV_NONE 不提供定时器到期通知。进程可以使用 timer_gettime()来监控定时器的运转情况。 SIGEV_SIGNAL 定时器到期时,为进程生成指定于 sigev_signo 中的信号。如果 sigev_signal 为实时信号, 那么 sigev_value 字段则指定了信号的伴随数据(整型或指针)(22.8.1 节)。通过 siginfo_t 结构的 si_value 可获取这一数据,至于 siginfo_t 结构,既可以直接传递给该信号的处理器函数,也可以由调用 sigwaitinfo()或 sigtimerdwait()返回。 SIGEV_THREAD 定时器到期时,会调用由 sigev_notify_function 字段指定的函数。调用该函数类似于调用新线程的启动函数。上述措词摘自 SUSv3,即允许系统实现以如下两种方式为周期性定时器

    产生通知:要么将每个通知分别传递给一个唯一的新线程,要么将通知成系列发送给单个新线程。可将 sigev_notify_attribytes 字段置为 NULL,或是指向 pthread_attr_t 结构的指针,并在结构中定义线程属性。在 sigev_value 中设定的联合体 sigval 值是传递给函数的唯一参数。 SIGEV_THREAD_ID 这与 SIGEV_SIGNAL 相类似,只是发送信号的目标线程 ID 要与 sigev_notify_thread_id 相匹配。该线程应与调用线程同属一个进程。(伴随 SIGEV_SIGNAL 通知,会将信号置于针对整个进程的一个队列中排队,并且,如果进程包含多条线程,那么可将信号传递给进程中的任意线程。)可用 clone()或 gettid()的返回值对sigev_notify_thread_id 赋值。设计 SIGEV_THREAD_ID 标志,意在供线程库使用。(要求线程实现使用 28.2.1 节描述的 CLONE_THREAD 选项。现代NPTL线程实现采用了CLONE_THREAD,但较老的LinuxThreads 线程则没有。) 除去 Linux 系统特有的 SIGEV_THREAD_ID 之外, SUSv3 定义了上述所有常量。 将参数 evp 置为 NULL, 这相当于将 sigev_notify 置为 SIGEV_SIGNAL, 同时将 sigev_signo置为 SIGALRM(这与其他系统可能会有出入,因为 SUSv3 的措词是:一个缺省的信号值),并将 sigev_value.sival_int 置为定时器 ID。 在当前实现中,内核会为每个用 timer_create()创建的 POSIX 定时器在队列中预分配一个实时信号结构。之所以要采取预分配,旨在确保当定时器到期时,至少有一个有效结构可服务于所产生的队列化信号。这也意味着可以创建的 POSIX 定时器数量受制于排队实时信号的数量(参考 22.8 节)。

    23.6.2 配备和解除定时器: timer_settime()

    一旦创建了定时器,就可以使用 timer_settime()对其进行配备(启动)或解除(停止)。

    #include<time.h>
    int timer_settime(timer_t timeid,int flags,const struct itimespec *value,struct itimerspec *oldvalue);

    struct itimespec{
       struct timespec it_interval;/*周期*/
       struct timespec it_value;   /*首次到期时间*/
    }

    struct timespec{
       time_t tv_sec;/*秒*/
       long tv_nsec; /*纳秒*/
    }

    函数 timer_settime()的参数 timerid 是一个定时器句柄(handle),由之前对 timer_create()的调用返回。 参数 value 和 old_value 则类似于函数 setitimer()的同名参数: value 中包含定时器的新设置, old_value 则用于返回定时器的前一设置。如果对定时器的前一设置不感兴趣,可将 old_value 设为 NULL。参数 value 和 old_value 都是指向结构itimerspec 的指针,该结构定义如下: 结构 itimerspec 中的所有字段都是 timespec 类型的结构,用秒和纳秒来指定时间:

    it_value 指定了定时器首次到期的时间。如果 it_interval 的任一子字段非 0,那么这就是一个周期性定时器,在经历了由 it_value 指定的初次到期后,会按这些子字段指定的频率周期性到期。如果 it_interval 的下属字段均为 0,那么这个定时器将只到期一次。

    若将 flags 置为 0,则会将 value.it_value 视为始于 timer_settime()(与 setitimer()类似)调用时间点的相对值。如果将 flags 设为 TIMER_ABSTIME,那么 value.it_value 则是一个绝对时间(从时钟值 0 开始)。一旦时钟过了这一时间,定时器会立即到期。

    为了启动定时器,需要调用函数 timer_settime(),并将 value.it_value 的一个或全部下属字段设为非 0 值。如果之前曾经配备过定时器, timer_settime()会将之前的设置替换掉。

    如果定时器的值和间隔时间并非对应时钟分辨率(由 clock_getres()返回)的整数倍,那么会对这些值做向上取整处理。

    定时器每次到期时,都会按特定方式通知进程,这种方式由创建定时器的 timer_create()定义。如果结构 it_interval 包含非 0 值,那么会用这些值来重新加载 it_value 结构。

    要解除定时器,需要调用 timer_settime(),并将 value.it_value 的所有字段指定为 0。

    23.6.3 获取定时器的当前值: timer_gettime()

    系统调用 timer_gettime()返回由 timerid 指定 POSIX 定时器的间隔以及剩余时间。

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

    curr_value 指针所指向的 itimerspec 结构中返回的是时间间隔以及距离下次定时器到期的时间。即使是以 TIMER_ABSTIME 标志创建的绝对时间定时器,在 curr_value.it_value 字段中返回的也是距离定时器下次到期的时间值。 如果返回结构 curr_value.it_value 的两个字段均为 0,那么定时器当前处于停止状态。如果返回结构 curr_value.it_interval 的两个字段都是 0,那么该定时器仅在 curr_value.it_value 给定的时间到期过一次。

    23.6.4 删除定时器: timer_delete()

    每个 POSIX 定时器都会消耗少量系统资源。所以,一旦使用完毕,应当用 timer_delete()来移除定时器并释放这些资源。

    #include<time.h>
    int timer_delete(timer_t timerid);

    参数 timerid 是之前调用 timer_create()时返回的句柄。对于已启动的定时器,会在移除前自动将其停止。如果因定时器到期而已经存在待定(pending)信号,那么信号会保持这一状态。(SUSv3 对此并未加以规范,所以其他的一些 UNIX 实现可能会有不同行为。)当进程终止时,会自动删除所有定时器

    23.6.5 通过信号发出通知

    如果选择通过信号来接收定时器通知,那么处理这些信号时既可以采用信号处理器函数,也可以调用 sigwaitinfo()或是 sigtimerdwait()。接收进程借助于这两种方法可以获得一个siginfo_t 结构(21.4 节),其中包含与信号相关的深入信息。(要在信号处理器函数中使用这种特性,创建信号处理器函数时需设置 SA_SIGINFO 标志。)在结构 siginfo_t 中设置如下字段。

    1.si_signo:包含由定时器产生的信号。

    2.si_code:置为 SI_TIMER,表示这是因 POSIX 定时器到期而产生的信号。

    3.si_value:将该字段置为以 timer_create()创建定时器时在 evp.sigev_value 中提供的值。

    为 evp.sigev_value 指定不同的值, 可以将到期时发送同类信号的不同定时器区分开来。 调用 timer_create()时,通常将 evp.sigev_value.sival_ptr 赋值为当前调用中参数 timerid 的 地址(见程序清单 23-5)。从而允许信号处理器函数(或 sigwaitinfo()调用)获得产生信号的 定时器 ID。(另外,也可以将调用函数 timer_create()时给定的 timerid 参数置于一结构中,并 将结构地址赋予 evp.sigev_value.sival_ptr。) Linux 还为 siginfo_t 结构提供了如下非标准字段。 1.si_overrun:包含了定时器溢出个数(在 23.6.6 节中说明)。

    程序清单 23-5 所演示的是使用信号作为 POSIX 定时器的通知机制。

    程序清单 23-5 程序的每个命令行参数都为定时器指定了初始值及间隔时间。程序的“用法”输出中描述了这些参数的语法,并在后面的 shell 会话中做了演示。程序执行的步骤如下。

    1.为用于定时器通知的信号创建处理器函数②。

    2.为每一个命令行参数,创建④并配备⑤一个使用 SIGEV_SIGNAL 通知机制的 POSIX 定时器。至于将命令行参数转换③为 itimerspec 结构的函数 itimerspecFromStr(),请参考程序清单 23-6。 3.每当一个定时器到期时,都将发送由 sev.sigev_signo 指定的信号给进程。信号处理器函数会将 sev.sigev_value.sival_ptr 中提供的值(定时器 ID, tidlist[j])以及定时器溢出值①显示出来。

    4.创建并配备定时器之后,在循环中反复调用 pause(),以等待定时器到期⑥。

    程序清单 23-6 中函数可将程序 23-5 的命令行参数转化为相应的 itimerspec 结构。函数可识别的字符串参数格式在源码文件开始的注释中做了说明(并在下面的 shell 会话中做了演示

    23.6.6 定时器溢出

    假设已经选择通过信号(即 sigev_notify 为 SIGEV_SIGNAL) 传递的方式来接收定时器到期通知。进一步假设,在捕获或接收相关信号之前,定时器到期多次。这可能是因为进程再次获得调度前的延时所致。另外,不论是直接调用 sigprocmask(),还是在信号处理器函数里暗中处理,也都有可能堵塞相关信号的发送。如何知道发生了这些定时器溢出呢? 也许会认为使用实时信号有助于解决这个问题,因为可以对实时信号的多个实例进行排队。不过, 由于对排队实时信号有数量上的限制, 结果证明这种方法也无法奏效。所以 POSIX.1b委员会选用了另一种方法:一旦选择通过信号来接收定时器通知,那么即便用了实时信号,也绝不会对该信号的多个实例进行排队。相反,在接收信号后(无论是通过信号处理器函数还是调用 sigwaitinfo()),可以获取定时器溢出计数,即在信号生成与接收之间发生的定时器到期额外次数。如果上次收到信号后定时器发生了 3 次到期,那么溢出计数是 2。 接收到定时器信号之后,有两种方法可以获取定时器溢出值。

    1.调用 timer_getoverrun(),稍后将会讨论。这是由 SUSv3 指定去获取溢出计数的方法。

    2.使用随信号一同返回的结构 siginfo_t 中的 si_overrun 字段值。这种方法可以避免timer_getoverrun()的系统调用开销,但同时也是一种 Linux 扩展方法,无法移植。

    每次收到定时器信号后,都会重置定时器溢出计数。若自处理或接收定时器信号之后,定时器仅到期一次,则溢出计数为 0(即无溢出)。

    #include<time.h>
    int timer_getoverrun(timer_t timerid);

    函数 timer_getoverrun()返回由参数 timerid 指定定时器的溢出值。 根据 SUSv3 规定(表 21-1),函数 timer_getoverrun()是异步信号安全的函数之一,故而在信号处理器函数内部调用也是安全的。

    23.6.7 通过线程来通知

    SIGEV_THREAD 标志允许程序从一个独立的线程中调用函数来获取定时器到期通知。 要理解这一标志的含义,需要具备第 29 章和第 30 章中关于 POSIX 线程的知识。如果不了解POSIX 线程,那么在查看本节示例程序前,可能需要预先阅读一下这些章节。 程序清单 23-7 演示了 SIGEV_THREAD 的使用。 该程序的命令行参数与程序清单 23-5 相同。所执行的步骤如下。 1.针对每个命令行参数,程序都创建⑥并配备⑦一个使用了 SIGEV_THREAD 通知机制③的 POSIX 定时器。

    2.每当定时器到期时,会在一条独立线程中调用由 sev.sigev_notify_function 指定的函数。调用函数时,使用由 sev.sigev_value.sival_ptr 指定的值作为参数。程序中会将定时器ID(tidlist[j])的地址赋给该字段⑤,以便在调用通知函数时可以获得定时器 ID。

    3.创建和配备所有定时器之后,主程序进入循环并等待定时器到期⑧。每次循环,程序都会调用pthread_cond_wait(),等待处理定时器通知的线程就条件变量(cond)发出信号。

    4.每次定时器到期都会调用函数 threadFunc()①。在打印消息后,增加全局变量 expireCnt的值。考虑到定时器可能溢出,会将 timer_getoverrun()的返回值也加入 expireCnt 变量中。(23.6.6 节解释了定时器溢出与SIGEV_SIGNAL 通知机制之间的关系。定时器溢出还可以与 SIGEV_THREAD 机制协作使用,因为在调用通知函数前,定时器可能会多次到期。)通知函数就条件变量(cond)发出信号,告知主程序定时器到期。

    下面的 shell 会话日志展示了对程序清单 23-7 中程序的调用。在本例中,程序创建了两个定时器:一个定时器首次到期时间为 5 秒,并设置了 5 秒的时间间隔;另一个初次到期时间为 10 秒,并设置了 10 秒的时间间隔。

    23.7 利用文件描述符进行通知的定时器: timerfd API

    始于版本 2.6.25, Linux 内核提供了另一种创建定时器的 API。 Linux 特有的 timerfd API,可从文件描述符中读取其所创建定时器的到期通知。因为可以使用 select()、 poll()和 epoll()将这种文件描述符会同其他描述符一同进行监控,所以非常实用。 这组 API 中的 3 个新系统调用,其操作与 23.6 节所述的 timer_create()、 timer_settime()和timer_gettime()相类似。 新加入的第 1 个系统调用是 timerfd_create(),它会创建一个新的定时器对象,并返回一个指代该对象的文件描述符。

    #include<sys/timerfd.h>
    int timerfd_create(int clockid,int flags);

    参数 clockid 的值可以设置为 CLOCK_REALTIME 或 CLOCK_MONOTONIC(参考表 23-1)。timerfd_create()的最初实现将参数 flags 预留供未来使用,必须设置为 0。不过, Linux 内核从 2.6.27 版本开始支持下面两种 flags 标志。

    TFD_CLOEXEC 为新的文件描述符设置运行时关闭标志(FD_CLOEXEC)。与 4.3.1 节介绍的 open()标志O_CLOEXEC 适用于相同情况。 TFD_NONBLOCK 为底层的打开文件描述设置 O_NONBLOCK 标志,随后的读操作将是非阻塞式的。这样设置省却了对 fcntl()的额外调用,却能达到相同效果。 timerfd_create()创建的定时器使用完毕后,应调用 close()关闭相应的文件描述符,以便于内核能够释放与定时器相关的资源。 系统调用 timerfd_settime()可以配备(启动) 或解除(停止) 由文件描述符 fd 所指代的定时器。 参数 new_value 为定时器指定新设置。参数 old_value 可用来返回定时器的前一设置。 如果不关心定时器的前一设置, 可将 old_value 置为 NULL。 两个参数均指向 itimerspec 结构,用法与 timer_settime()(参考 23.6.2 节)相同。 参数 flags 与 timer_settime()中的对应参数类似。可以是 0,此时将 new_value.it_value 的值视为相对于调用 timerfd_settime()时间点的相对时间,也可以设为 TFD_TIMER_ABSTIME,将其视为一个绝对时间(从时钟的 0 点开始测量)。 系统调用 timerfd_gettime()返回文件描述符 fd 所标识定时器的间隔及剩余时间。

    #include<sys/timerfd.h>
    int timerfd_settime(int fd,int flags,struct itimerspec *new_value,struct itimerspec *old_value);
    int timerfd_gettime(int fd,struct itimerspec *curr_value);

    同 timer_gettime()一样,间隔以及距离下次到期的时间均返回 curr_value 指向的结构itimerspec 中。即使是以 TFD_TIMER_ABSTIME 标志创建的绝对时间定时器, curr_vallue.it_value 字段中返回值的意义也会保持不变。 如果返回的结构 curr_value.it_value 中所有字段值均为 0,那么该定时器已经被解除。如果返回的结构 curr_value.it_interval 中两字段值均为 0,那么定时器只会到期一次,到期时间在 curr_value.it_value 中给出。

    timerfd 与 fork()及 exec()之间的交互

    调用 fork()期间,子进程会继承 timerfd_create()所创建文件描述符的拷贝。这些描述符与父进程的对应描述符均指代相同的定时器对象,任一进程都可读取定时器的到期信息。

    timerfd_create()创建的文件描述符能跨越 exec()得以保存(除非将描述符置为运行时关闭,如 27.4 节所述),已配备的定时器在 exec()之后会继续生成到期通知。

    从 timerfd 文件描述符读取

    一旦以 timerfd_settime()启动了定时器, 就可以从相应文件描述符中调用 read()来读取定时器的到期信息。出于这一目的,传给 read()的缓冲区必须足以容纳一个无符号 8 字节整型(uint64_t)数。 在上次使用 timerfd_settime()修改设置以后,或是最后一次执行 read()后,如果发生了一起到多起定时器到期事件, 那么 read()会立即返回, 且返回的缓冲区中包含了已发生的到期次数。 如果并无定时器到期, read()会一直阻塞直至产生下一个到期。也可以执行 fcntl()的 F_SETFL 操作(5.3 节)为文件描述符设置 O_NONBLOCK 标志,这时的读动作是非阻塞式的,且如果没有定时器到期,则返回错误,并将 errno 值置为 EAGAIN。 如前所述,可以利用 select()、 poll()和 epoll()对 timerfd 文件描述符进行监控。如果定时器到期,会将对应的文件描述符标记为可读。

  • 相关阅读:
    UVALive 7509 Dome and Steles
    HDU 5884 Sort
    Gym 101194H Great Cells
    HDU 5451 Best Solver
    HDU 5883 The Best Path
    HDU 5875 Function
    卡特兰数
    UVa 11729 Commando War 突击战
    UVa 11292 The Dragon of Loowater 勇者斗恶龙
    Spark Scala Flink版本对应关系
  • 原文地址:https://www.cnblogs.com/wangbin2188/p/14837066.html
Copyright © 2011-2022 走看看