使用 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 指定的时钟来进行时间度量。
设置参数 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()对其进行配备(启动)或解除(停止)。
函数 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 定时器的间隔以及剩余时间。
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()来移除定时器并释放这些资源。
参数 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(即无溢出)。
函数 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(),它会创建一个新的定时器对象,并返回一个指代该对象的文件描述符。
参数 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 所标识定时器的间隔及剩余时间。
同 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 文件描述符进行监控。如果定时器到期,会将对应的文件描述符标记为可读。