23 定时器与休眠

定时器 API

1. setitimergetitimer(不建议)

SYNOPSIS
       #include <sys/time.h>

       int getitimer(int which, struct itimerval *curr_value);
       int setitimer(int which, const struct itimerval *restrict new_value,
                     struct itimerval *_Nullable restrict old_value);

which 参数指定创建的定时器的类型

  • 可以是真实时间,到期的信号是 SIGALARM。
  • 可以是用户 CPU 时间(进程虚拟时间),到期的信号为 SIGVTALRM。
  • 可以是内核 + 用户 CPU 时间(profiling 定时器),到期的信号是 SIGPROF。

以上三种信号默认行为是终止进程,所以定时器要结合信号处理函数使用。

struct itimerval 参数解释

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

其中 timeval 是带有秒和微秒的结构体,可以参考 10.01 时间类型 tm time_t timeval timespec

it_interval 指的是定时器的发生周期,如果是 0 μs,则表示这是一次性定时器。it_value 是定时器的首次触发时间,可以和 it_interval 不同,如果为 0 μs 则表示清除(已有的)定时器而不是设置定时器。

定时器的数量限制

以上三种定时器每个进程都只能最多各一个。

其他

使用 setitimeralarm 创建的定时器可以不会被 exec 清理,但是不会被 fork 继承到子进程。

SUSv4 废止了 getitimer() 和 setitimer(),同时推荐使用 POSIX 定时器 API。

2. alarm(不建议)

SYNOPSIS
       #include <unistd.h>

       unsigned int alarm(unsigned int seconds);

alarm 函数能设置一次性定时器,计时单位是秒。alarmsetitimer 比较相似:

  1. 会覆盖上一个设置。
  2. 用时间 0 作为参数表示清除定时器。
  3. 一个进程只能有一个 alarm 定时器。

SUSv3 没有规定 alarmsetitimer 混用时的情况,在 Linux 实现中 alarmsetitimer 对真实时间的计时设定会相互覆盖。为了可移植性最好不要混用两套 API。

3. 为阻塞操作设置超时

有些系统调用不能设置超时时间,这个时候可以用定时器来完成这个功能。

  1. sigaction 设置好对应信号的处理器函数,防止超时信号使得进程终止。创建处理器函数的时候还要排除 SA_RESTART 标志,防止系统调用被重启。
  2. 创建定时器。
  3. 执行阻塞系统调用。
  4. 检查系统调用的返回值是否是 EINTR(遭遇中断而失败)。
  5. 消除定时器。

一个风险是:如果计时器在执行阻塞系统调用之前就已经到期,那么系统调用将可能一直阻塞。但是一般计时器设置的时间会比较长,所以这种问题很少发生。不过,如果使用的是 I/O 系统调用, select / poll 的超时功能是更好的方案。

4. 定时器的精度

以前定时器的精度受限于软件时钟周期(内核 jiffy)的精度。从 Linux 版本 2.6.21 开始,如果内核配置了 CONFIG_HIGH_RES_TIMERS 选项,那么定时器的精度可以达到硬件级精度。

如果定时器的时间不能刚好被内核所支持精度的最小单位表示,又或者系统负载高,定时器到期时进程就不能马上得到调度。但是尽管有这种延迟,定时器的下一次触发不会因此而被延后,也不会有累积延迟。

睡眠 API

之前介绍的阻塞函数都是等待某种条件。如果想要用之前的函数实现等待若干时间,可以用 sigsuspend 和定时器函数,但这稍嫌麻烦。

sleepusleep(不建议)

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

sleep 函数的接口比较简单,Linux 将其实现为对 nanosleep 的调用。此函数和 alarmsetitimer 之间如何相互影响,SUSv3 没有规定,所以为了移植性最好避免使用。

如果有信号在此期间到达,则 sleep 函数也会提前返回,返回值表示还剩下没有休眠够的秒数。

usleep 除了精度比 sleep 更高之外,其他的特点非常相似,也应该避免使用。

nanosleep

#include <time.h>

int nanosleep(const struct timespec *req,
              struct timespec *_Nullable rem);

Compared to sleep(3) and usleep(3), nanosleep() has the following advantages: it provides a higher resolution for specifying the sleep interval; POSIX.1 explicitly specifies that it does not interact with signals; and it makes the task of resuming a sleep that has been interrupted by a signal handle easier.

SUSv3 明文规定不得使用信号来实现该系统调用,因此不会担心它和 alarm/setitimersleep/usleep 相互影响。如果进程收到信号 nanosleep 也会提前返回。

Caution

nanosleep 虽然精度很高,但是仍然受到系统软件时钟的限制,参考之前的小节 定时器的精度。如果高频率接受信号,nanosleep 每次设置的时间可能会遭遇取整困境,即被信号中断时过去的纳秒数还不如当前剩余时间取整到下一个软件时钟的整倍数带来的时间增量多。如果反复重试 nanosleep,它也可能永远完不成。Linux 2.6 中,使用带有 TIMER_ABSTIME 选项的 clock_nanosleep() 可以避免这一问题。

POSIX 时钟 API

在 glibc 2.17 之前,想要使用 POSIX 时钟必须使用 -lrt 编译选项,以便和 librt 库链接。在 glibc 2.17 之后,只要和标准 C 链接(-lc)就可以了,我想这个行为应该是 gcc 默认的。

定时器 API 和睡眠 API 应该是 POSIX 时钟 API 的一部分,所以有必要先介绍一下 POSIX 时钟 API 基础部分。

获取和设定时钟时间

#include <time.h>

int clock_getres(clockid_t clockid, struct timespec *_Nullable res); // 获取时钟精度

int clock_gettime(clockid_t clockid, struct timespec *tp);
int clock_settime(clockid_t clockid, const struct timespec *tp);

clockid_t 是 SUSv3 规定的一种类型,该类型有一些系统定义的常量(可以用 man clock_getres 来查看):

CLOCK_REALTIME  可设定的系统实时时钟
CLOCK_MONOTONIC 不可设置的稳定时钟,Linux 是参照系统开机后的时间来设置的
CLOCK_PROCESS_CPUTIME_ID 当前进程 CPU 时间
CLOCK_THREAD_CPUTIME_ID  当前线程 CPU 时间
// ... 自成书以来 Linux 增加了不少时钟类型,这里略去

clock_settime 设置系统时钟时,进程需要有响应的特权。

获取其他进程或线程的 CPU 时间

我们已经知道用 CLOCK_PROCESS_CPUTIME_IDCLOCK_THREAD_CPUTIME_ID 可以分别获取当前进程和当前线程的 CPU 时间。如果想要获取其他进程或线程的 CPU 时间,需要先获取对应的时钟 id。

#include <time.h>

int clock_getcpuclockid(pid_t pid, clockid_t *clockid);
#include <pthread.h>
#include <time.h>

int pthread_getcpuclockid(pthread_t thread, clockid_t *clockid);

POSIX 定时器 API

  • 创建定时器:timer_create
  • 设置定时器:timer_settime
  • 销毁定时器:timer_delete

创建定时器

int timer_create(clockid_t clockid,
                struct sigevent *_Nullable restrict sevp,
                timer_t *restrict timerid);

其中类型为 struct sigevent * 的参数 sevp 功能就比较多了。其中 sevp.sigev_notify 支持四种字段:

  • SIGEV_NONE:到期不异步通知。需要用 timer_gettime 主动检查。
  • SIGEV_SIGNAL:到期产生信号。具体是什么信号由 sevp 中的其他字段指定。
  • SIGEV_THREAD:到期执行函数。该函数应该被假设在新线程执行。(这个说法有点含糊,意思是说可以不在新的线程执行吗?)
  • SIGEV_THREAD_ID:Linux 专有,到期产生信号发给特定的线程

目前 Linux 中的 POSIX 定时器是用实时信号实现的,会占用用户可用实时信号的数量,同时定时器数量还受到排队实时信号数量的限制。

设置定时器

除了和特定的 timer 关联,以及有个 flags(目前是可以用来确定计时按照绝对时间还是相对时间)之外,其他的和 setitimer 的接口相差不大。

删除定时器

在 Linux 中删除定时器后,如果因定时器之前的到期已经有 pending 的信号,那么信号将保持这一状态不被清理。SUSv3 没有对此行为做出规范。

什么是定时器溢出

定时器相关的实时信号是特殊的(可以不用实时信号而去用标准信号):

一旦选择通过信号来接收定时器通知,那么即便用了实时信号, 也绝不会对该信号的多个实例进行排队。相反,在接收信号后(无论是通过信号处理器函数还是调用 sigwaitinfo()),可以获取定时器溢出计数,即在信号生成与接收之间发生的定时器到期额外次数。

POSIX 睡眠 API

clock_nanosleep ~~曾经是 Linux 特有(?不确定),~~现在也被加入到 POSIX 标准中来了。

#include <time.h>

int clock_nanosleep(clockid_t clockid, int flags,
                   const struct timespec *request,
                   struct timespec *_Nullable remain);

从签名上面来看,clock_nanosleepnanosleep 多出来两个参数:一个是时钟 id,一个是标志。标志可以填 TIMER_ABSTIME,这表示将使用绝对时间睡眠,这样可以规避软件时钟精度导致的、在循环中重启按相对时间睡眠的嗜睡(oversleeping)问题。

小结:老 API 和 POSIX API 的功能比较

可以看到 POSIX 标准带来的新的 API 都纠正了之前 API 只考虑单例(因为设计之初线程模型还没有普及)的缺陷。

定时器timer_* (POSIX) > setitimer > alarm

  1. POSIX 定时器不会被 fork、不会跨越 exec。老的定时器 setitimeralarm 虽然不会被 fork 但是会跨越 exec
  2. POSIX 定时器可以通过指定 flags 来使用绝对时间。

Important

老定时器跨越 exec 的特性大概是使用信号实现导致的?

休眠clock_nanosleep (POSIX) > nanosleep > usleep > sleep。其中 nanosleepclock_nanosleep 不用信号实现。

Note

Man 手册对于 clock_nanosleep 的说法是:POSIX.1 specifies that clock_nanosleep() has no effect on signals dispositions or the signal mask,而对于 nanosleep 的说法是:POSIX.1 explicitly specifies that it does not interact with signals

Linux 专有的 timerfd_* API

始于版本 2.6.25,Linux 特有的 timerfd API,可从文件描述符中读取其所创建定时器的到期通知。由于形式上是文件描述符,所以可以和 selectpollepoll 等结合使用。

#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 *_Nullable old_value);
int timerfd_gettime(int fd, struct itimerspec *curr_value);

以上 API 和 POSIX timer API 还是比较相似的。目前支持的 flags 有两个,一个是在 exec 时关闭文件描述符,另外一个是决定文件描述符在读取时是否阻塞。删除一个和定时器关联的 fd 则是使用 close 系统调用。

Note

由于使用的是文件描述符,所以 timerfd API 创建的定时器可以跨越 forkexec(除非有 TFD_CLOEXEC 标志)。