POSIX定时器的是为了解决间隔定时器itimer的以下问题:
- 一个进程同一时刻只能有一个同一种类型(ITIMER_REAL, ITIMER_PROF, ITIMER_VIRT)的itimer。POSIX定时器在一个进程中可以创建任意多个timer。
- itimer定时器到期后,只能通过信号(SIGALRM,SIGVTALRM,SIGPROF)的方式通知进程,POSIX定时器到期后不仅可以通过信号进行通知,还可以使用自定义信号,还可以通过启动一个线程来进行通知。
- itimer支持us级别,POSIX定时器支持ns级别。
POSIX定时器提供的定时器API如下:
int timer_create(clockid_t clock_id, struct sigevent *evp, timer_t *timerid);
int timer_settime(timer_t timerid, int flags, const struct itimerspec *value, struct itimerspect *ovalue);
int timer_gettime(timer_t timerid,struct itimerspec *value);
int timer_getoverrun(timer_t timerid);
int timer_delete (timer_t timerid);
其中时间结构itimerspec定义如下:该结构和itimer的itimerval结构用处和含义类似,只是提供了ns级别的精度
struct itimerspec
{
struct timespec it_interval; // 时间间隔
struct timespec it_value; // 首次到期时间
};
struct timespec
{
time_t tv_sec //Seconds.
long tv_nsec //Nanoseconds.
};
it_value表示定时间经过这么长时间到时,当定时器到时候,就会将it_interval的值赋给it_value。如果it_interval等于0,那么表示该定时器不是一个时间间隔定时器,一旦it_value到期后定时器就回到未启动状态。
timer_create(clockid_t clock_id, struct sigevent *evp, timer_t *timerid)
创建一个POSIX timer,在创建的时候,需要指出定时器的类型,定时器超时通知机制。创建成功后通过参数返回创建的定时器的ID。
参数clock_id用来指定定时器时钟的类型,时钟类型有以下6种:
- CLOCK_REALTIME:系统实时时间,即日历时间;
- CLOCK_MONOTONIC:从系统启动开始到现在为止的时间;
- CLOCK_PROCESS_CPUTIME_ID:本进程启动到执行到当前代码,系统CPU花费的时间;
- CLOCK_THREAD_CPUTIME_ID:本线程启动到执行到当前代码,系统CPU花费的时间;
- CLOCK_REALTIME_HR:CLOCK_REALTIME的细粒度(高精度)版本;
- CLOCK_MONOTONIC_HR:CLOCK_MONOTONIC的细粒度版本;
struct sigevent设置了定时器到期时的通知方式和处理方式等,结构的定义如下:
struct sigevent
{
int sigev_notify; //设置定时器到期后的行为
int sigev_signo; //设置产生信号的信号码
union sigval sigev_value; //设置产生信号的值
void (*sigev_notify_function)(union sigval);//定时器到期,从该地址启动一个线程
pthread_attr_t *sigev_notify_attributes; //创建线程的属性
}
union sigval
{
int sival_int; //integer value
void *sival_ptr; //pointer value
}
如果sigevent传入NULL,那么定时器到期会产生默认的信号,对CLOCK_REALTIMER来说,默认信号就是SIGALRM,如果要产生除默认信号之外的其他信号,程序必须将evp->sigev_signo设置为期望的信号码。
如果几个定时器产生了同一个信号,处理程序可以用 sigev_value来区分是哪个定时器产生了信号。要实现这种功能,程序必须在为信号安装处理程序时,使用struct sigaction的成员sa_flags中的标志符SA_SIGINFO。
sigev_notify的值可取以下几种:
- SIGEV_NONE:定时器到期后什么都不做,只提供通过timer_gettime和timer_getoverrun查询超时信息。
- SIGEV_SIGNAL:定时器到期后,内核会将sigev_signo所指定的信号,传送给进程,在信号处理程序中,si_value会被设定为sigev_value的值。
- SIGEV_THREAD:定时器到期后,内核会以sigev_notification_attributes为线程属性创建一个线程,线程的入口地址为sigev_notify_function,传入sigev_value作为一个参数。
timer_settime(timer_t timerid, int flags, const struct itimerspec *value, struct itimerspect *ovalue)
创建POSIX定时器后,该定时器并没有启动,需要通过timer_settime()接口设置定时器的到期时间和周期触发时间。
flags字段标识到期时间是一个绝对时间还是一个相对时间。
https://github.com/lattera/glibc/blob/master/bits/time.h
/* Flag to indicate time is absolute. */
# define TIMER_ABSTIME 1
如果flags的值为TIMER_ABSTIME,则value的值为一个绝对时间。否则,value为一个相对时间。
timer_getoverrun(timer_t timerid)
取得一个定时器的超限运行次数:有可能一个定时器到期了,而同一定时器上一次到期时产生的信号还处于挂起状态。在这种情况下,其中的一个信号可能会丢失。这就是定时器超限。程序可以通过调 用timer_getoverrun来确定一个特定的定时器出现这种超限的次数。定时器超限只能发生在同一个定时器产生的信号上。由多个定时器,甚至是那 些使用相同的时钟和信号的定时器,所产生的信号都会排队而不会丢失。
执行成功时,timer_getoverrun()会返回定时器初次到期与通知进程(例如通过信号)定时器已到期之间额外发生的定时器到期次数。举例来说,在我们之前的例子中,一个1ms的定时器运行了10ms,则此调用会返回9。如果超限运行的次数等于或大于DELAYTIMER_MAX,则此调用会返回DELAYTIMER_MAX。
执行失败时,此函数会返回-1并将errno设定会EINVAL,这个唯一的错误情况代表timerid指定了无效的定时器。
timer_delete (timer_t timerid)
删除一个定时器:一次成功的timer_delete()调用会销毁关联到timerid的定时器并且返回0。执行失败时,此调用会返回-1并将errno设定会 EINVAL,这个唯一的错误情况代表timerid不是一个有效的定时器。
POSIX定时器通过调用内核的posix_timer进行实现,但glibc对POSIX timer进行了一定的封装,例如如果POSIX timer到期通知方式被设置为 SIGEV_THREAD 时,glibc 需要自己完成一些辅助工作,因为内核无法在 Timer 到期时启动一个新的线程。
https://github.com/lattera/glibc/blob/master/nptl/sysdeps/unix/sysv/linux/timer_create.c
int
timer_create (clock_id, evp, timerid)
clockid_t clock_id;
struct sigevent *evp;
timer_t *timerid;
{
if (evp == NULL || __builtin_expect (evp->sigev_notify != SIGEV_THREAD, 1))
{
...
}
else
{
...
/* Create the helper thread. */
pthread_once (&__helper_once, __start_helper_thread);
...
}
...
}
可以看到 GLibc 发现用户需要启动新线程通知时,会自动调用 pthread_once 启动一个辅助线程(__start_helper_thread),用 sigev_notify_attributes 中指定的属性设置该辅助线程。
然后 glibc 启动一个普通的 POSIX Timer,将其通知方式设置为:SIGEV_SIGNAL | SIGEV_THREAD_ID。这样就可以保证内核在 timer 到期时通知辅助线程。通知的 Signal 号为 SIGTIMER,并且携带一个包含了到期函数指针的数据。这样,当该辅助 Timer 到期时,内核会通过 SIGTIMER 通知辅助线程,辅助线程可以在信号携带的数据中得到用户设定的到期处理函数指针,利用该指针,辅助线程调用 pthread_create() 创建一个新的线程来调用该处理函数。这样就实现了 POSIX 的定义。