其他分享
首页 > 其他分享> > TLPI读书笔记第23章:定时器与休眠1

TLPI读书笔记第23章:定时器与休眠1

作者:互联网

定时器是进程规划自己在未来某一时刻接获通知的一种机制。休眠则能使进程(或线程)暂停执行一段时间。本章讨论了定时器设置以及休眠的接口,涵盖主题如下。 1.针对间隔式定时器设置的传统 UNIX API setitimer()和 alarm(),一经设定,会在特定的一段时间后通知进程。 2.允许进程休眠特定时间的API接口。 3.POSIX.1b时钟和定时器API接口。 4.Linux特有的 timerfd 功能,允许所创建定时器的到期信息可从文件描述符中读取。

23.1 间隔定时器

系统调用 setitimer()创建一个间隔式定时器(interval timer),这种定时器会在未来某个时间点到期,并于此后(可选择地)每隔一段时间到期一次。

#include<sys/time.h>
int settimer(int which,const struct itimerval *new_value,struct itimerval *old_value)
/*new_value=当前设置;old_value=前一设置*/

通过在调用 setitimer()时为 which 指定以下值,进程可以创建 3 种不同类型的定时器。 ITIMER_REAL 创建以真实时间倒计时的定时器。到期时会产生 SIGALARM 信号并发送给进程。 ITIMER_VIRTUAL 创建以进程虚拟时间(用户模式下的 CPU 时间)倒计时的定时器。到期时会产生信号SIGVTALRM。 ITIMER_PROF 创建一个 profiling 定时器,以进程时间(用户态与内核态 CPU 时间的总和)倒计时。到期时,则会产生 SIGPROF 信号。 对所有这些信号的默认处置(disposition)均会终止进程。除非真地期望如此,否则就需要针对这些定时器信号创建处理器函数。 参数 new_value 和 old_value 均为指向结构 itimerval 的指针,结构的定义如下:

struct itimeval{
   struct timeval it_interval;/*定时器间隔,其值决定是否为一次性或周期性的定时器*/
   struct timeval it_value;   /*距离定时器到期的延迟时间*/
}

struct timeval{
   time_t tv_sec;/*秒*/
   susseconds tv_usec; /*微秒*/
}

结构 itimerval 中的字段类型均为 timeval 结构, timeval 又由秒和微秒两部分组成: 参数 new_value 的下属结构 it_value 指定了距离定时器到期的延迟时间。另一下属结构it_interval 则说明该定时器是否为周期性定时器。如果 it_interval 的两个字段值均为 0,那么该定时器就属于在 it_value 所指定的时间间隔后到期的一次性定时器。

只要 it_interval 中的任一字段非 0,那么在每次定时器到期之后,都会将定时器重置为在指定间隔后再次到期。 进程只能拥有上述 3 种定时器中的一种。当第 2 次调用 setitimer()时,修改已有定时器的属性要符合参数 which 中的类型。

如果调用 setitimer()时将 new_value.it_value 的两个字段均置为 0,那么会屏蔽任何已有的定时器。若参数 old_value 不为 NULL, 则以其所指向的 itimerval 结构来返回定时器的前一设置。如果 old_value.it_value 的两个字段值均为 0,那么该定时器之前处于屏蔽状态。

如果old_value.it_interval 的两个字段值均为 0, 那么该定时器之前被设置为历经 old_value.it_value 指定时间而到期的一次性定时器。

对于需要在新定时器到期后将其还原的情况而言,获取定时器的前一设置就很重要。如果不关心定时器的前一设置,可以将 old_value 置为 NULL。定时器会从初始值(it_value)倒计时一直到 0 为止。递减为 0 时,会将相应信号发送给进程,随后,如果时间间隔值(it_interval)非 0,那么会再次将 it_value 加载至定时器,重新开始向 0 倒计时。 可以在任何时刻调用 getitimer(),以了解定时器的当前状态、距离下次到期的剩余时间。

#include<sys/time.h>
int gettimer(int which,struct itimeval *curr_value);

系统调用 getitimer()返回由 which 指定定时器的当前状态,并置于由 curr_value 所指向的缓冲区中。这与 setitimer()借参数 old_value 所返回的信息完全相同,区别则在于 getitimer()无需为了获取这些信息而改变定时器的设置。

子结构 curr_value.it_value 返回距离下一次到期所剩余的总时间。该值会随定时器倒计时而变化,如果设置定时器时将 it_interval 置为非 0 值,那么会在定时器到期时将其重置。子结构 curr_value.it_interval 返回定时器的间隔时间,除非再次调用 setitimer(),否则该值一直保持不变。 使用 setitimer()(和 alam(),稍后讨论)创建的定时器可以跨越 exec()调用而得以保存,但由 fork()创建的子进程并不继承该定时器

更为简单的定时器接口: alarm()

系统调用 alarm()为创建一次性实时定时器提供了一个简单接口。

#include<unistd.h>
unsigned int alarm(unsigned int seconds);
/*一次性实时定时器*/

参数 seconds 表示定时器到期的秒数。到期时,会向调用进程发送 SIGALRM 信号。调用 alarm()会覆盖对定时器的前一个设置。调用 alarm(0)可屏蔽现有定时器。 alarm()的返回值是定时器前一设置距离到期的剩余秒数,如未设置定时器则返回 0。

setitimer()和 alarm()之间的交互

Linux 中, alarm()和 setitimer()针对同一进程共享同一实时定时器,这也意味着,无论调用两者之中的哪个完成了对定时器的前一设置,同样可以调用二者中的任一函数来改变这一设置。

其他 UNIX 系统的情况可能会有所不同(也就是说,这两个函数可能分别控制着不同的定时器)。对于 setitimer()与 alarm()之间的交互,以及二者与 sleep()函数(23.4.1 节)之间的交互, SUSv3 均未加以规范。

为了确保应用程序可移植性的最大化,程序设置实时定时器的函数只能在二者中选择其一。

23.2 定时器的调度及精度

取决于当前负载和对进程的调度,系统可能会在定时器到期的瞬间(通常是几分之一秒)之后才去调度其所属进程。尽管如此,由 setitimer()或本章后续介绍的其他接口所创建的周期性定时器,在到期后依然会恪守其规律性。例如,假设设置一个实时定时器每两秒到期一次,虽然上述延迟可能会影响每个定时器事件的送达,但系统对后续定时器到期的调度依然会严格遵循两秒的时间间隔。换言之,间隔式定时器不受潜在错误左右。

虽然 setitimer()使用的 timeval 结构提供有微秒级精度, 但是传统意义上定时器精度还是受制于软件时钟(10.6 节)频率。如果定时器值未能与软件时钟间隔的倍数严格匹配,那么定时器值则会向上取整。也就是说,假如有一个间隔为 19100 微秒(刚刚超过 19 毫秒)的定时器,如果jiffy(软件时钟周期)为 4 毫秒,那么定时器实际上会每隔 20 毫秒过期一次。

高分辨率定时器

对于现代 Linux 内核而言,适才关于定时器分辨率受限于软件时钟频率的论断已经不再成立。自版本 2.6.21 开始, Linux 内核可选择是否支持高分辨率定时器。精度达到微秒级是司空见惯的事情。

23.3 为阻塞操作设置超时

实时定时器的用途之一是为某个阻塞系统调用设置其处于阻塞状态的时间上限。例如,当用户在一段时间内没有输入整行命令时,可能希望取消对终端的 read()操作。处理如下。 1. 调用 sigaction()为 SIGALRM 信号创建处理器函数,排除 SA_RESTART 标志以确保系统调用不会重新启动。 2. 调用 alarm()或setitimer()来创建一个定时器,同时设定希望系统调用阻塞的时间上限。 3. 执行阻塞式系统调用。 4. 系统调用返回后,再次调用 alarm()或 setitimer()以屏蔽定时器(以防止系统调用在定时器到期之前就已完成的情况)。 5. 检查系统调用失败时是否将 errno 置为 EINTR(系统调用遭到中断)。 程序清单 23-2 针对 read()调用展示了这一技术,创建定时器时使用的是 alarm()。

注意,程序清单 23-2 中程序理论上存在导致竞争条件的可能性。如果定时器到期时处于alarm()调用之后, read()调用之前,那么信号处理器函数将不会中断 read()。由于在这种场景下设定的超时值一般相对较大(至少几秒),故而发生上述情况的概率极低,因此这种技术实际上是可行的。

在处理 I/O 系统调用时,还有另一种备选方案,利用了系统调用 select()或 poll()(第 63 章)的超时特性,锦上添花的是还能同时等待多路描述符的 I/O。

23.4 暂停运行(休眠)一段固定时间

有时需要将进程挂起(固定的)一段时间。将前述定时器函数与 sigsuspend()相结合固然可以达到这一目的,但使用休眠函数会更为简单。

23.4.1 低分辨率休眠: sleep()

函数 sleep()可以暂停调用进程的执行达数秒之久(由参数 seconds 设置),或者在捕获到信号(从而中断调用)后恢复进程的运行

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

如果休眠正常结束, sleep()返回 0。如果因信号而中断休眠, sleep()将返回剩余(未休眠)的秒数。与 alarm()和 setitimer()所设置的定时器相同,由于系统负载的原因,内核可能会在完成 sleep()的一段(通常很短)时间后才对进程重新加以调度。 对于 sleep()和 alarm()以及 setitimer()之间的交互方式, SUSv3 并未加以规范。 Linux 将sleep()实现为对 nanosleep()的调用,其结果是 sleep()与定时器函数之间并无交互。不过,许多其他的实现,尤其是一些老系统,会使用 alarm()以及 SIGALRM 信号处理器函数来实现 sleep()。考虑到可移植性,应避免将 sleep()和 alarm()以及 setitimer()混用。

23.4.2 高分辨率休眠: nanosleep()

函数 nanosleep()的功用与 sleep()类似,但更具优势,其中包括能以更高分辨率来设定休眠间隔时间。

#include<time.h>
int nanosleep(const struct timespec *request,struct timespec *remain)

参数 request 指定了休眠的持续时间,是一个指向如下结构的指针

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

tv_nsec 字段为纳秒值,取值范围在 0~999999999 之间。 nanosleep()的更大优势在于, SUSv3 明文规定不得使用信号来实现该函数。这意味着,与sleep()不同,即使将 nanosleep()与 alarm()或 setitimer()混用,也不会危及程序的可移植性。 尽管 nanosleep()的实现并未使用信号,但还是可以通过信号处理器函数来将其中断。这时,nanosleep()将返回-1,并将 errno 置为 EINTR。同时,若参数 remain 不为 NULL,则该指针所指向的缓冲区将返回剩余的休眠时间。可利用这一返回值重启该系统调用以完成休眠。

程序清单 23-3演示了这一用途。程序从命令行参数中获取传入 nanosleep()的秒和纳秒值,并反复循环执行 nanosleep(),直至耗尽全部的休眠间隔时间。如果信号 SIGINT(按下 Ctrl-C 产生)的处理器函数将 nanosleep()中断,那么会以参数 remain 中的返回值重新调用 nanosleep()。其运行结果如下:

虽然 nanosleep()允许设定纳秒级精度的休眠间隔值,但其精度依然受制于软件时钟的间隔大小( 10.6 节)。如果指定的间隔值并非软件时钟间隔的整数倍,那么会对其向上取整。

当以高频率接收信号时,这一取整行为会给程序清单 23-3 中程序所采用的编程手法带来问题。由于返回的 remain 时间未必是软件时钟间隔的整数倍,故而 nanosleep()的每次重启都会遭遇取整错误。其结果是, nanosleep()每次重启后的休眠时间都要长于前一调用返回的remain 值。在信号接收频率极高的情况下(与软件时钟间隔的频率一致或更高),进程的休眠可能永远也完成不了。 Linux 2.6 中,使用带有 TIMER_ABSTIME 选项的 clock_nanosleep()可以避免这一问题。 23.5.4 节将对 clock_nanosleep()加以讨论

标签:调用,23,setitimer,alarm,读书笔记,TLPI,value,定时器,nanosleep
来源: https://www.cnblogs.com/wangbin2188/p/14803098.html