一、基本简介
refer:http://blog.chinaunix.net/uid-11848011-id-96374.html
【简介】
这篇文章主要记录我在试图解决如何尽可能精确地在某个特定的时间间隔执行某项具体任务时的思路历程,并在后期对相关的API进行的归纳和总结,以备参考。【问题引出】
很多时候,我们会有类似“每隔多长时间执行某项任务”的需求,乍看这个问题并不难解决,实则并不容易,有很多隐含条件需要考虑,诸如:时间精度是多少?时间是否允许出现偏差,允许的偏差是多少,偏差之后如何处理?系统的负载如何?这个程序允许占用的系统资源是否有限制?这个程序运行的硬件平台如何?为了便于分析,我们锁定题目为“每隔2妙打印当前的系统时间(距离UNIX纪元的秒数)”。
【0】实现方法:可以调用上层调用函数/或者采用系统调用/或者直接采用超时实现
(只不过是精度不同而已)
--------------------------------------------------------------------------------------------------------------
sleep usleep(上层调用函数)
nanosleep poll和epoll_wait alarm和sigwaitinfo (系统调用)
timer_list (定时器的超时实现)
--------------------------------------------------------------------------------------------------------------
【1】基于sleep的朴素解法
看到这个题目,我想大家的想法和我一样,都是首先想到类似这样的解法:
|
如果对时间精度要求不高,以上代码确实能工作的很好。因为sleep的时间精度只能到1s:
sleep()和nanosleep()都是使进程睡眠一段时间后被唤醒,但是二者的实现完全不同。
Linux中并没有提供系统调用sleep(),sleep()是在库函数中实现的,它是通过调用alarm()来设定报警时间,调用sigsuspend()将进程挂起在信号SIGALARM上,sleep()只能精确到秒级上。
|
所以对于更高的时间精度(比如说毫秒)来说,sleep就不能奏效了。如果沿着这个思路走下去,还分别有精确到微妙和纳秒的函数usleep和nanosleep可用:
|
|
既然有了能精确到纳秒的nanosleep可用,上面的较低精度的函数也就可以休息了。实际上在Linux系统下,sleep和usleep就是通过一个系统调用nanosleep实现的。
nanosleep()则是Linux中的系统调用,它是使用定时器来实现的,该调用使调用进程睡眠,并往定时器队列上加入一个timer_list型定时器,time_list结构里包括唤醒时间以及唤醒后执行的函数,通过nanosleep()加入的定时器的执行函数仅仅完成唤醒当前进程的功能。系统通过一定的机制定时检查这些队列(比如通过系统调用陷入核心后,从核心返回用户态前,要检查当前进程的时间片是否已经耗尽,如果是则调用schedule()函数重新调度,该函数中就会检查定时器队列,另外慢中断返回前也会做此检查),如果定时时间已超过,则执行定时器指定的函数唤醒调用进程。当然,由于系统时间片可能丢失,所以nanosleep()精度也不是很高。
【2】用带有超时功能的API变相实现睡眠
如果开发者不知道有usleep和nanosleep,这个时候他可能会联想到select类的系统调用:
|
|
|
从函数原型和相关手册来看,poll和epoll_wait能提供的时间精度为毫秒,select比他们两个略胜一筹,为微秒,和前述的usleep相当。但是,果真如此么?这需要我们深入到Linux的具体实现,在内核里,这几个系统调用的超时功能都是通过内核中的动态定时器实现的,而动态定时器的时间精度是由当前内核的HZ数决定的。如果内核的HZ是100,那么动态定时器的时间精度就是1/HZ=1/100=10毫秒。目前,X86系统的HZ最大可以定义为1000,也就是说X86系统的动态定时器的时间精度最高只能到1毫秒。由此来看,select用来指示超时的timeval数据结构,只是看起来很美,实际上精度和poll/epoll_wait相当。
【3】基于定时器的实现
除了基于sleep的实现外,还有基于能用信号进行异步提醒的定时器实现:
alarm()也是通过定时器实现的,但是其精度只精确到秒级,另外,它设置的定时器执行函数是在指定时间向当前进程发送SIGALRM信号。
|
显然,上面的代码并没有利用信号进行异步提醒,而是通过先阻塞信号的传递,然后用sigwaitinfo等待并将信号取出的方法将异步化同步。这样做的目的是为了尽可能减少非必要的信号调用消耗,因为这个程序只需要执行这个简单的单一任务,所以异步除了带来消耗外,并无任何好处。
读者可能已经发现上面的代码无非是把最初的代码中的sleep换成了alarm和sigwaitinfo两个调用,除了复杂了代码之外,好像并没有什么额外的好处。alarm的时间精度只能到1s,并且alarm和sigwaitinfo的确也可以看成是sleep的一种实现,实际上有的sleep确实是透过alarm来实现的,请看sleep的手册页:
BUGS sleep() may be implemented using SIGALRM; mixing calls to alarm(2) and sleep() is a bad idea. Using longjmp(3) from a signal handler or modifying the handling of SIGALRM while sleeping will cause undefined results. |
但是,这只是表象,本质他们是不同的,sleep是拨了一个临时实时定时器并等待定时器到期,而alarm是用进程唯一的实时定时器来定时唤醒等待信号到来的进程执行。
如果需要更高的时间精度,可以采用精度为微秒的alarm版本ualarm:
|
或者是直接用setitimer操纵进程的实时定时器:
|
细心的你应该已经注意到了,ualarm和setitimer都额外提供了间隔时间的设置以便于间隔定时器用SIGALRM周期性的唤醒进程,这对于我们的需求有什么意义呢?请听我慢慢道来。一般来说,需要定时执行的任务所消耗的时间都很短,至少都会少于间隔时间,否则这个需求就是无法实现的。我们前面的程序实现,都是假设任务消耗时间为0,实际上的任务并不总是像打印当前系统时间这么简单,即便它们持续的时间真的短到相对来说可以忽略不计,如果这些小的忽略不计累积起来,也还是可能会造成长时间后的大偏差,所以我们有必要将这段时间计算进来。一种补救的措施是在任务执行的前后执行gettimeofday得到系统的时间,然后做差得到任务消耗时间并在接下来的“sleep”中将其扣除。问题看似解决了,但是我们毕竟没有将系统进行上下文切换的时间和计算消耗时间的时间考虑进来,这样的话,还是会存在较大的误差。另一种计算量相对小些的算法是:直接通过时间间隔计算下一次超时的绝对时间,然后根据当前的绝对时间算出需要等待的时间并睡眠。但是,这也只是修修补补而已,并没有从根本上解决问题。间隔定时器的出现从根本上解决了上面所提的问题,它自身就提供周期唤醒的功能,从而避免了每次都计算的负担。因为ualarm已经被放弃,所以用setitimer再次改写代码:
|
进程的间隔计时器能够提供的时间精度为微秒,对于大多数的应用来说,应该已经足够,如果需要更高的时间精度,或者需要多个定时器,那么每个进程一个的实时间隔定时器就无能为力了,这个时候我们可以选择POSIX实时扩展中的定时器:
|
它实际上就是进程间隔定时器的增强版,除了可以定制时钟源(nanosleep也存在能定制时钟源的版本:clock_nanosleep)和时间精度提高到纳秒外,它还能通过将evp->sigev_notify设定为如下值来定制定时器到期后的行为:
- SIGEV_SIGNAL: 发送由evp->sigev_sino指定的信号到调用进程,evp->sigev_value的值将被作为siginfo_t结构体中si_value的值。
- SIGEV_NONE:什么都不做,只提供通过timer_gettime和timer_getoverrun查询超时信息。
- SIGEV_THREAD: 以evp->sigev_notification_attributes为线程属性创建一个线程,在新建的线程内部以evp->sigev_value为参数调用evp->sigev_notification_function。
- SIGEV_THREAD_ID:和SIGEV_SIGNAL类似,不过它只将信号发送到线程号为evp->sigev_notify_thread_id的线程,注意:这里的线程号不一定是POSIX线程号,而是线程调用gettid返回的实际线程号,并且这个线程必须实际存在且属于当前的调用进程。
|
至于时钟源为什么是CLOCK_MONOTONIC而不是CLOCK_REALTIME,主要是考虑到系统的实时时钟可能会在程序运行过程中更改,所以存在一定的不确定性,而CLOCK_MONOTONIC则不会,较为稳定。
至此为止,我们已经找到了目前Linux提供的精度最高的定时器API,它应该能满足大多数情况的要求了。
【4】基于时钟中断进行系统定时的实现
硬件周期性的时间间隔产生,间隔(即频率)由内核根据Hz常数决定,可配置为50-1200,x86平台默认值为1000,即每秒产生1000次中断。每次时钟中断产生时,全局变量jiffies(unsigned long)就会加1,它记录着自linux启动之后和谐靠大家生的时钟中断的次数。
作用:把一些例行的和需要定时执行的程序放在时钟中断中;利用时钟中断协助主程序完成定时、延时等操作,但其定时精度不高!
例如: unsigned long j=jiffies+x_delay*Hz
{
//do nothing
}
//延时x_delay秒
【5】关于内核定时器
内核定时器:管理内核时间的基础,推后或执行时间执行某些代码。
用于控制某个函数(定时器处理函数)在未来的某个特定时间执行。
内核定时器注册的处理函数只执行一次(不是循环执行的)。它可用于驱动程序当中。
内核定时器被组织成双向链表,并使用struct timer_list来描述一个内核定时器!
(1)定时器数据结构:
struct timer_list {
struct list_head entry;
//定时值基于jiffies
unsigned long expires;
//定时器内部值
struct tvec_base *base;
//定时器处理函数
void (*function)(unsigned long);
//定时器处理函数参数
unsigned long data;
……
};
相关函数:
void init_timer(struct timer_list *timer)
vod add_timer(struct timer_list *timer)
int del_timer(struct timer_list *timer) 启动定时器前将它删除,因为在超时后系统会自动将它删除。
(2)定时器使用:
定时器的使用非常方便,只需要执行一些初始化的操作,设置一个超时时间,指定超时发生时执行的函数,然后激活定时器就可以了。它的处理和工作队列还是有点类似的。其实,在Linux内核开发中,很多的操作都是类似的。还有一点需要注意的,内核定时器并不是周期运行,它在超时后自动销毁。因此,如果要实现周期轮询,就需要在定时器执行函数返回前再次激活定时器。
struct timer_list my_timer;
//初始化定时器
init_timer(&my_timer);
……
//激活定时器
add_timer(&my_timer);
//删除定时器
del_timer(my_timer);
……
总的来说,主要需要以下步骤:
定义一个timer_list变量timer、
先初始化timer
init_timer(&timer);
then 对timer的相关参数赋值:
timer.function = fun;
timer.expires = jiffies + TIMER_DELAY;
add_timer(&timer);
在定时器时间到的时候,会执行fun,如果继续定时,可以通过在fun中执行
mod_timer(&timer, jiffies + TIMER_DELAY);
在不需要的时候通过调用del_timer(&timer);删除定时器。
(3)定时器使用代码实例1:
附程序:
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <linux/timer.h>
#include <asm/atomic.h>
#define SECOND_MAJOR 0
static int second_major = SECOND_MAJOR;
struct second_dev
{
struct cdev cdev;
atomic_t counter;
struct timer_list s_timer;
};
struct second_dev *second_devp;
static void second_timer_handle(unsigned long arg)
{
mod_timer(&second_devp->s_timer, jiffies + HZ);
atomic_inc(&second_devp->counter);
printk(KERN_ERR "current jiffies is %ld\n",jiffies);
}
int second_open(struct inode *inode, struct file *filp)
{
init_timer(&second_devp->s_timer);
second_devp->s_timer.function = &second_timer_handle;
second_devp->s_timer.expires = jiffies + HZ;
add_timer(&second_devp->s_timer);
atomic_set(&second_devp->counter, 0);
return 0;
}
int second_release(struct inode *inode, struct file *filp)
{
del_timer(&second_devp->s_timer);
return 0;
}
static ssize_t second_read(struct file *filp, char __user *buf, size_t count,
loff_t *ppos)
{
int counter;
counter = atomic_read(&second_devp->counter);
if (put_user(counter, (int *)buf))
{
return -EFAULT;
}else
{
return sizeof(unsigned int);
}
}
static const struct file_operations second_fops =
{
.owner = THIS_MODULE,
.open = second_open,
.release = second_release,
.read = second_read,
};
static void second_setup_cdev(struct second_dev *dev, int index)
{
int err, devno = MKDEV(second_major, index);
cdev_init(&dev->cdev, &second_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &second_fops;
err = cdev_add(&dev->cdev, devno, 1);
if (err)
{
printk(KERN_NOTICE "Error %d add second%d", err, index);
}
}
int second_init(void)
{
int ret;
dev_t devno = MKDEV(second_major, 0);
if (second_major)
{
ret = register_chrdev_region(devno, 1, "second");
}else
{
ret = alloc_chrdev_region(&devno, 0, 1, "second");
second_major = MAJOR(devno);
}
if (ret < 0)
{
return ret;
}
second_devp = kmalloc(sizeof(struct second_dev), GFP_KERNEL);
if (!second_devp)
{
ret = -ENOMEM;
goto fail_malloc;
}
memset(second_devp, 0, sizeof(struct second_dev));
second_setup_cdev(second_devp, 0);
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
}
void second_exit(void)
{
cdev_del(&second_devp->cdev);
kfree(second_devp);
unregister_chrdev_region(MKDEV(second_major, 0), 1);
}
MODULE_AUTHOR("Song Baohua");
MODULE_LICENSE("Dual BSD/GPL");
module_param(second_major, int, S_IRUGO);
module_init(second_init);
module_exit(second_exit);
附上用户端的测试程序:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(void)
{
int fd, i;
int data;
fd = open("/dev/second",O_RDONLY);
if (fd < 0)
{
printf("open /dev/second error\n");
}
for(i = 0; i < 20; i++)
{
read(fd, &data, sizeof(data));
printf("read /dev/second is %d\n",data);
sleep(1);
}
close(fd);
}
(4)定时器使用代码实例2:
例子--编写内核模块定时器:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/timer.h>
#include <asm/uaccess.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("David Xie");
MODULE_DESCRIPTION("Timer Module");
MODULE_ALIAS("timer module");
struct timer_list timer;//定义内核定时器
void timer_function(int para)//作为内核定时器的处理函数,此处para为形参
而实际值由timer.data传递过来。
{
printk("<0>Timer Expired and para is %d !!\n",para); //输出data参数
}
int timer_init() //模块初始化函数用于module_init()
{
init_timer(&timer);//初始化定时器链表
timer.data = 5;//参数为5,用于timer.function
timer.expires = jiffies + (20 * HZ);//设置定时时间为20秒
timer.function = timer_function;//设置操作函数
add_timer(&timer);//启动定时器
//注意以上几个函数的参数均为指针类型的,所以用&取地址
return 0;
}
void timer_exit()
{
del_timer( &timer );
}
module_init(timer_init);
module_exit(timer_exit);
#insmod之后会打印出:Timer Expired and para is 20
【其它问题】
传统信号的不可靠性
传统UNIX信号是不可靠的,也就是说如果当前的信号没有被处理,那么后续的同类信号将被丢失,而不是被排队,而实时信号则没有这个问题,它是被排队的。联系到当前应用,如果信号丢失,则是因为任务消耗了过多的处理器时间,而这个不确定性是那个任务带来的,需要改进的应该是那个任务。系统负载过高
如果系统的负载过高,使得我们的程序因为不能得到及时的调度导致时间精度降低,我们不妨通过nice提高当前程序的优先级,必要时可以通过sched_setscheduler将当前进程切换成优先级最高的实时进程已确保得到及时调度。硬件相关的问题
硬件配置也极大的影响着定时器的精度,有的比较老的遗留系统可能没有比较精确的硬件定时器,那样的话我们就无法期待它能提供多高的时钟精度了。相反,如果系统的配置比较高,比如说对称多处理系统,那么即使有的处理器负载比较高,我们也能通过将一个处理器单独分配出来处理定时器来提高定时器的精度。更高的时间精度
虽然,Linux的API暗示它能够提供纳秒级的时间精度,但是,由于种种不确定因素,它实际上并不能提供纳秒级的精度,比较脆弱。如果你需要更高强度的实时性,请考虑采用软实时系统、硬实时系统、专有系统,甚至是专业硬件。【注意:】
为了简便,以上所有代码都没有出错处理,请读者在现实的应用中自行加入出错处理,以提高程序的健壮性。尤其注意sleep类的返回值,它们可能没到期就返回,这个时候你应该手动计算需要再睡眠多长才能满足原始的睡眠时间要求,如果该API并没有返回剩余的时间的话。