LINUX时间管理
时间管理在内核中占有非常重要的地位。相对于事件驱动,内核中有大量的函数都是基于时间驱动的。内核必须管理系统的运行时间以及当前的日期和时间。
首先搞清楚RTC在kernel内的作用:
linux系统有两个时钟:实时时钟和系统定时器
一个是由纽扣电池供电的“Real Time Clock”也叫做RTC(实时时钟)或者叫CMOS时钟,硬件时钟。当操作系统关机的时候,用这个来记录时间,但是对于运行的系统是不用这个时间的。当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。所谓墙上时间也就是当前的实际时间。
另一个时间是 “System clock”也叫内核时钟或者软件时或者叫系统定时器,是由软件根据时间中断来进行计数的,系统定时器是内核时间机制中最重要的一部分,它提供了一种周期性触发中断机制,即系统定时器以HZ(时钟节拍率)为频率自行触发时钟中断。当时钟中断发生时,内核就通过时钟中断处理程序timer_interrupt()对其进行处理。
系统定时器完全由操作系统管理,因此也成为系统时钟或者软件时钟。当系统启动时,内核通过RTC初始化系统定时器,系统定时器接着由操作系统共掌管,进行固定频率的定时。可以看到,系统时间并不是传统意义上的那种计时时钟,而是通过定时这种特殊的方式来表现时间。 内核时钟在系统关机的情况下是不存在的,所以,当操作系统启动的时候,内核时钟是要读取RTC时间来进行时间同步。并且在系统关机的时候将系统时间写回RTC中进行同步。
全局变量jiffies用来记录自系统启动以来产生的节拍的总数。它被用来记录系统自开机以来,已经过了多少tick。每发生一次timer interrupt,Jiffies变数会被加一。启动时,内核将该变量初始化为0,此后,每次时钟中断处理程序都会增加该变量的值。因为一秒内时钟中断的次数等于Hz,所以jiffes一秒内增加的值也就为Hz,系统运行时间以秒为单位计算,就等于jiffes/Hz。
jiffies转换为秒可采用公式:(jiffies/HZ)计算,
将秒转换为jiffies可采用公式:(seconds*HZ)计算。
Tick是HZ的倒数,意即timer interrupt每发生一次中断的时间。如HZ为250时,tick为4毫秒(millisecond)。
jiffies仅是相对于系统启动的相对时间,如果想获取absolutetime或wall time,则需要使用RTC,
内核用变量xtime来记录,当系统启动时,读取RTC并记录在xtime中,当系统halt时,则将walltime写回RTC,函数do_gettimeofday()来读取wall time。
系统定时器及其中断处理程序是内核管理机制的中枢,下面是一些利用系统定时器周期执行的工作(中断处理程序所做的工作):
(1) 更新系统运行时间(uptime) jiffes
(2) 更新当前墙上时间(wall time) xtime
(3) 在对称多处理器系统(SMP)上,均衡调度各处理器上的运行队列
(4) 检查当前进程是否用完了时间片(time slice),如果用尽,则进行重新调度
如前所述,Linux内核与RTC进行互操作的时机只有两个:
1) 内核在启动时从RTC中读取启动时的时间与日期(LINUX系统时间的 初始化);
通过调用rtc_read_time(rtc, &tm);-读出RTC时间。调用 do_settimeofday(&tv);给系统时间xtime初始化。
Alarm.c (kernel\drivers\rtc):static int __init alarm_late_init(void)
Alarm.c (kernel\drivers\rtc):late_initcall(alarm_late_init);
static int __init alarm_late_init(void)
{
unsigned long flags;
struct timespec tmp_time, system_time;
/* this needs to run after the rtc is read at boot */
spin_lock_irqsave(&alarm_slock, flags);
/* We read the current rtc and system time so we can later calulate
* elasped realtime to be (boot_systemtime + rtc - boot_rtc) ==
* (rtc - (boot_rtc - boot_systemtime))
*/
getnstimeofday(&tmp_time);
ktime_get_ts(&system_time);
alarms[ANDROID_ALARM_ELAPSED_REALTIME_WAKEUP].delta =
alarms[ANDROID_ALARM_ELAPSED_REALTIME].delta =
timespec_to_ktime(timespec_sub(tmp_time, system_time));
spin_unlock_irqrestore(&alarm_slock, flags);
return 0;
}
Hctosys.c (kernel\drivers\rtc):int rtc_hctosys(void)
Hctosys.c (kernel\drivers\rtc):late_initcall(rtc_hctosys);
start_kernel()-->late_initcall(rtc_hctosys);-->
-->rtc_hctosys(void)-->err = rtc_read_time(rtc, &tm);---> do_settimeofday(&tv);
----》xtime = *tv;
int rtc_hctosys(void)
{
int err = -ENODEV;
struct rtc_time tm;
struct timespec tv = {
.tv_nsec = NSEC_PER_SEC >> 1,
};
struct rtc_device *rtc = rtc_class_open(CONFIG_RTC_HCTOSYS_DEVICE);
if (rtc == NULL) {
pr_err("%s: unable to open rtc device (%s)\n",
__FILE__, CONFIG_RTC_HCTOSYS_DEVICE);
goto err_open;
}
err = rtc_read_time(rtc, &tm);
if (err) {
dev_err(rtc->dev.parent,
"hctosys: unable to read the hardware clock\n");
goto err_read;
}
err = rtc_valid_tm(&tm);
if (err) {
dev_err(rtc->dev.parent,
"hctosys: invalid date/time\n");
goto err_invalid;
}
rtc_tm_to_time(&tm, &tv.tv_sec);
do_settimeofday(&tv);
dev_info(rtc->dev.parent,
"setting system clock to "
"%d-%02d-%02d %02d:%02d:%02d UTC (%u)\n",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec,
(unsigned int) tv.tv_sec);
err_invalid:
err_read:
rtc_class_close(rtc);
err_open:
rtc_hctosys_ret = err;
return err;
}
2) 内核在需要时将时间与日期回写到RTC中。 系统启动时,内核通过读取RTC来初始化内核时钟,又叫墙上时间,该时间放在xtime变量中。
系统睡眠的时候CPU要断电系统时钟不工作,所以RTC的睡眠函数rtc_suspend读出RTC和系统时钟的时间,计算它们之间的差,然后保存到静态变量。系统被唤醒后读出当前RTC的时间和系统的时间,系统时间应该接近0,因为cpu刚刚恢复上电,将原来保存的RTC和系统之间的时间差加上刚刚读到的RTC的时间就是最新的系统时间,将计算出来的时间通过rtc_resume函数调用 timekeeping_inject_sleeptime(&sleep_time);重新初始化系统时钟xtime。如果RTC的时间和系统时间都一样,那么他们之间的差为0.
但是有些系统硬件RTC时间是不可以写,只能够被读,那么他们之间的差就不为0了。
Class.c (kernel\drivers\rtc):static int rtc_suspend(struct device *dev, pm_message_t mesg)
static int rtc_suspend(struct device *dev, pm_message_t mesg)
{
struct rtc_device *rtc = to_rtc_device(dev);
struct rtc_time tm;
struct timespec delta, delta_delta;
if (strcmp(dev_name(&rtc->dev), CONFIG_RTC_HCTOSYS_DEVICE) != 0)
return 0;
/* snapshot the current RTC and system time at suspend*/
rtc_read_time(rtc, &tm);
getnstimeofday(&old_system);
rtc_tm_to_time(&tm, &old_rtc.tv_sec);
/*
* To avoid drift caused by repeated suspend/resumes,
* which each can add ~1 second drift error,
* try to compensate so the difference in system time
* and rtc time stays close to constant.
*/
delta = timespec_sub(old_system, old_rtc);
delta_delta = timespec_sub(delta, old_delta);
if (delta_delta.tv_sec < -2 || delta_delta.tv_sec >= 2) {
/*
* if delta_delta is too large, assume time correction
* has occured and set old_delta to the current delta.
*/
old_delta = delta;
} else {
/* Otherwise try to adjust old_system to compensate */
old_system = timespec_sub(old_system, delta_delta);
}
return 0;
}
static int rtc_resume(struct device *dev)
{
struct rtc_device *rtc = to_rtc_device(dev);
struct rtc_time tm;
struct timespec new_system, new_rtc;
struct timespec sleep_time;
if (strcmp(dev_name(&rtc->dev), CONFIG_RTC_HCTOSYS_DEVICE) != 0)
return 0;
/* snapshot the current rtc and system time at resume */
getnstimeofday(&new_system);
rtc_read_time(rtc, &tm);
if (rtc_valid_tm(&tm) != 0) {
pr_debug("%s: bogus resume time\n", dev_name(&rtc->dev));
return 0;
}
rtc_tm_to_time(&tm, &new_rtc.tv_sec);
new_rtc.tv_nsec = 0;
if (new_rtc.tv_sec < old_rtc.tv_sec) {
pr_debug("%s: time travel!\n", dev_name(&rtc->dev));
return 0;
}
/* calculate the RTC time delta (sleep time)*/
sleep_time = timespec_sub(new_rtc, old_rtc);
/*
* Since these RTC suspend/resume handlers are not called
* at the very end of suspend or the start of resume,
* some run-time may pass on either sides of the sleep time
* so subtract kernel run-time between rtc_suspend to rtc_resume
* to keep things accurate.
*/
sleep_time = timespec_sub(sleep_time,
timespec_sub(new_system, old_system));
if (sleep_time.tv_sec >= 0)
timekeeping_inject_sleeptime(&sleep_time);
return 0;
}
周期产生的事件都是由系统定时器驱动的。系统定时器是一种可编程硬件芯片,它已固定频率产生中断。该中断就是所谓的定时器中断,它所对应的中断处理程序负责更新系统时间,还负责执行需要周期性运行的任务。系统定时器和时钟中断处理程序是Linux系统内核管理机制中的中枢。
1 内核中的时间概念
硬件为内核提供了一个系统定时器用以计算流逝的时间,系统定时器以某种频率自行触发时钟中断,该频率可以通过编程预定,称节拍率。当时钟中断发生时,内核就通过一种特殊中断处理程序对其进行处理。内核知道连续两次时钟中断的间隔时间。这个间隔时间称为节拍(tick),内核就是靠这种已知的时钟中断来计算墙上时间和系统运行时间。墙上时间即实际时间,该时间放在xtime变量中,内核提供了一组系统调用以获取实际日期和实际时间。系统运行时间——自系统启动开始所经过的时间——对用户和内核都很有用,因为许多程序都必须清楚流逝过的时间。
2节拍率
系统定时器频率是通过静态预处理定义的,也就是HZ,为一秒内时钟中断的次数,在系统启动时按照Hz对硬件进行设置。体系结构不同,HZ的值也不同。内核在文件<asm/param.h>中定义了HZ的实际值,节拍率就是HZ,周期为1/HZ。i386的节拍率为1000,其它体系结构(包括ARM)的节拍率多数都等于100。
3 jiffies
全局变量jiffies用来记录自系统启动以来产生的节拍的总数。启动时,内核将该变量初始化为0,此后,每次时钟中断处理程序都会增加该变量的值。因为一秒内时钟中断的次数等于Hz,所以jiffes一秒内增加的值也就为Hz,系统运行时间以秒为单位计算,就等于jiffes/Hz。Jiffes=seconds*HZ。
Jiffs定义在文件linux/jiffs.h中
Extern unsigned long volatile jiffies;
关键字volatile指示编译器在每次访问变量时都重新从主内存中获得,而不是通过寄存器中的变量别名访问,从而确保前面的循环能按预期的方式执行。
3.1 jiffies的内部表示
jiffies变量总是无符号长整数(unsigned long),因此,在32位体系结构上是32位,在时钟频率为100的情况下,497天后会溢出,如果频率是1000,49.7天后会溢出
3.2用户空间和HZ
在2.6以前的内核中,如果改变内核中HZ的值会给用户空间中某些程序造成异常结果。这是因为内核是以节拍数/秒的形式给用户空间导出这个值的,在这个接口稳定了很长一段时间后应用程序便逐渐依赖于这个特定的HZ值了。所以如果在内核中更改了HZ的定义值,就打破了用户空间的常量关系——用户空间并不知道新的HZ值。
要想避免上面的错误,内核必须更改所有导出的jiffies值。因而内核定义了USER_HZ来代表用户空间看到的值。对于ARM体系结构,HZ = USR_HZ。
4硬实钟和定时器
体系结构提供了两种设备进行计时——一种是我们前面讨论过的系统定时器,另一种是实时时钟。实时时钟(RTC)是用来持久存放系统时间的设备,即便系统关闭后,它可以靠主板上的微型电池提供的电力保持系统的计时。当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。
系统定时器是内核定时机制中最为重要的角色。尽管不同体系结构中的定时器实现不尽相同,但是系统定时器的根本思想没有区别——提供一种周期性触发中断机制。
5时钟中断处理程序
下面我们看一下时钟中断处理程序是如何实现的。时钟中断处理程序可以划分为两个部分:体系结构相关部分和体系结构无关部分。
与体系结构相关的例程作为系统定时器的中断处理程序而注册到内核中,以便在产生时钟中断时,它能够相应的运行。虽然处理程序的具体工作依赖于特定的体系结构,但是绝大多数处理程序至少要执行如下工作:
(1)获得xtime_lock锁,以便对访问jiffies_64和墙上时间xtime进行保护。
(2)需要时应答或重新设置系统时钟。
(3)周期性的使用墙上时间更新实时时钟。
(4)调用体系结构无关的例程:do_timer。
中断服务程序主要通过调用与体系结构无关的例程do_timer执行下面的工作:
给jiffies_64变量增加1
更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间。
执行已经到期的动态定时器。
执行scheduler_tick()函数。
更新墙上时间,该时间存放在xtime变量中。
Do_timer()函数执行完毕后返回与体系结构相关的中断处理程序,继续执行后面的工作,释放xtime_lock锁,然后退出。
以上全部工作每1/HZ秒都要发生一次,也就是说在你的PC机上时钟中断处理程序每秒执行1000次。
6实际时间
当前实际时间(墙上时间)定义在文件kernel/timer.c中:
struct timespec xtime;
timespec数据结构定义在文件<linux/time.h>中,形式如下:
struct timespec{ time_t tv_sec; /*秒 */
long tv_nsec; /*纳秒 */
};
其中,xtime.tv_sec以秒为单位,存放着自1970年7月1日以来经过的时间。xtime.tv_nsec记录了自上一秒开始经过的纳秒数。读写xtime变量需要使用xtime_lock锁,它是一个seq锁。读取xtime时要使用read_seqbegin()和read_seqretry()函数:
SYSCALL_DEFINE2(gettimeofday, struct timeval __user *, tv,
struct timezone __user *, tz)---》
do_gettimeofday(&ktv);----》
Time.c (kernel\kernel):SYSCALL_DEFINE2(gettimeofday, struct timeval __user *, tv,
SYSCALL_DEFINE2(gettimeofday, struct timeval __user *, tv,
struct timezone __user *, tz)
{
if (likely(tv != NULL)) {
struct timeval ktv;
do_gettimeofday(&ktv);
if (copy_to_user(tv, &ktv, sizeof(ktv)))
return -EFAULT;
}
if (unlikely(tz != NULL)) {
if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
return -EFAULT;
}
return 0;
}
Timekeeping.c (kernel\kernel\time):void do_gettimeofday(struct timeval *tv)
void do_gettimeofday(struct timeval *tv)
{
struct timespec now;
getnstimeofday(&now);
tv->tv_sec = now.tv_sec;
tv->tv_usec = now.tv_nsec/1000;
}
Timekeeping.c (kernel\kernel\time):void getnstimeofday(struct timespec *ts)
该循环不断重复,直到读者确认读取数据时没有写操作介入。如果发现循环期间有时钟中断处理程序更新xtime,那么read_seqretry()函数就返回无效序列号,继续循环等待。从用户空间取得墙上时间的主要接口是gettimeofday()。
7 定时器
定时器,有时也称为动态定时器或内核定时器——是管理内核时间的基础。定时器的使用很简单。只需要执行一些初始化工作,设置一个超时时间,指定超时发生后执行的函数,然后激活定时器就可以了。指定的函数将在定时器到期时自动执行。定时器并不周期运行,它在超时后就自行销毁,这也正是这种定时器被称为动态定时器的一个原因。
7.1使用定时器
定时器由结构timer_list表示,定义在文件<linux/timer.h>中。
struct timer_list {
struct list_head entry; /*定时器链表的入口 */
unsigned long expiers; /*以jiffies为单位的定时器 */
spinlock_t lock; /*保护定时器的锁 */
void ( * function)(unsigned long); /*定时器处理函数 */
unsigned long data; /*传给处理函数的长整形参数 */
struct tvec_t_base_s *base; /*定时器内部值,用户不要使用 */
};
内核提供了一组与定时器相关的接口用来简化管理定时器的操作。所有这些接口都声明在文件<linux/timer.h>中,大多数接口都在kernel/timer.c中获得实现。
创建定时器时需要先定义它: struct timer_list my_timer;
初始化定时器数据结构,初始化必须在使用其它定时器管理函数对定时器进行操作之前完成。
init_timer(&my_timer); 然后就可以填充结构中需要的值了。
my_timer.expires = jiffies + delay; /*定时器超时时的节拍数 */
my_timer.data = 0; /*给定时器处理函数传入0值 */
my_timer.function = my_function; /*定时器超时时调用的函数 */
my_timer.expires表示超时时间,它是以节拍为单位的绝对计数值。如果当前jiffies计数等于或大于它,处理函数开始执行。处理函数必须符合下面的函数原形:
void my_timer_function(unsigned long data);
data参数使我们可以利用一个处理函数注册多个定时器,只需通过该参数就能区别它们。
激活定时器:add_timer(&my_timer);
有时可能需要更改已经激活的定时器超时时间,所以内核通过函数mod_timer()来实现该功能,该函数可以改变指定的定时器超时时间:
mod_timer(&my_timer, jiffies+new_delay);
mod_timer()函数也可以操作那些已经初始化,但还没有被激活的定时器,它会同时激活它。一旦从mod_timer()函数返回,定时器都将被激活而且设置了新的定时值。
如果需要在定时器超时前停止定时器,可以使用del_timer()函数:
del_timer(&my_timer); 或 del_timer_sync()(不能在中断上下文中使用)
8延迟执行
8.1忙等待
最简单的延迟方法是忙等待(或者说是忙循环)。但这种方法仅仅适用于延迟的时间是节拍的整数倍,或者精确度要求不高时。更好的方法是在代码等待时,允许内核重新调度执行其他任务:
unsigned long delay = jiffies + 5*HZ;
while(time_before(jiffies, delay)) cond_resched();
cond_resched()函数将调度一个新程序投入运行,但它只有在设置完need_resched标志后,才能生效。延迟执行不管在哪种情况下都不应该在持有锁或禁止中断时发生。
8.2 短延迟
有时内核代码(通常也是驱动程序)不但需要很短暂的延迟(比时钟节拍还短)而且还要求延迟的时间按很精确。这种情况多发生在和硬件同步时,内核提供了两个可以处理微秒和毫秒级别的延迟函数,它们都定义在<linux/delay.h>中,可以看到它们并不使用jiffies:
void udelay(unsigned long usecs)
void mdelay(unsigned long msecs)
经验证明,不要使用udelay()函数处理超过1毫秒的延迟。此时使用mdelay()更为安全。
纳秒,微妙,毫 秒延迟,必须是短延迟,时间过长会报错
头文件: delay.h
void ndelay(unsigned long nesec);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
void msleep(unsigned int millisecs);
void ssleep(unsigned int seconds);
头文件:jeffies.h / time.h
while(time_before(jiffies,jiffies+msecs_to_jiffies(delay_time)){
schedule();
}
8.3 schedule_timeout()
更理想的延迟执行方法是使用schedule_timeout()函数,用法如下:
set_current_state(TASK_INTERRUPTIBLE); /*将任务设置为可中断睡眠状态 */
schedule_timeout(s *HZ); /*小睡一会,s秒后唤醒 */
唯一的参数是延迟的相对时间,单位为jiffies。上例中将相应的任务推入可中断睡眠队列(注意了,这里的进入睡眠队列,就意味着可以去执行其他任务了),睡眠s秒。在调用schedule_timeout()函数前必须首先将任务设置成TASK_INTERRUPTILE和TASK_UNINTERRUPTIBLE面两种状态之一,否则任务不会睡眠。调用代码绝对不能持有锁(因为持有锁的任务是不能睡眠的)。
当任务被重新调度时,将返回代码进入睡眠前的位置继续执行。
时间相关的命令:
date显示或设置系统时间与日期
date命令并不从RTC获取时间,它获取的是内核xtime时间,同样它设置的也是内核xtime时间,而非RTC。
语法: date [-d <字符串>] [-u] [+格式参数]
date [-s <字符串>] [-u] [+格式参数]
补充说明:
第一种语法可用来显示系统日期或时间,以%为开头的参数为格式参数,可指定日期或时间的显示格式。
第二种语法可用来设置系统日期与时间。只有管理员才有设置日期与时间的权限。
若不加任何参数,data会显示目前的日期与时间。
该命令的各选项含义如下:
-d<字符串> 显示字符串所指的日期与时间。字符串前后必须加上双引号。
-s<字符串> 根据字符串来设置日期与时间。字符串前后必须加上双引号。
-u 显示GMT。
--help 在线帮助。
--version 显示版本信息。
该命令可用的格式参数如下:
命令中各选项的含义分别为:
格式:
date 月/日/时间/年.秒
也可以采用 date -s 月/日/年
date -s 时/分/秒
#date //查看系统时间
#date -s //设置当前时间,只有root权限才能设置,其他只能查看。
#date -s 20120608//设置成20120608,这样会把具体时间设置成空00:00:00
#date -s 12:23:23 //设置具体时间,不会对日期做更改
#date -s “12:12:23 2006-10-10″ //这样可以设置全部时间
#date 060812232012(月日时分年)(完整书写) //这样可以设置时间和日期
CST:中国标准时间(China Standard Time),这个解释可能是针对RedHat Linux。
UTC:协调世界时,又称世界标准时间,简称UTC,从英文国际时间/法文协调时间”Universal Time/Temps Cordonné”而来。中国大陆、香港、澳门、台湾、蒙古国、新加坡、马来西亚、菲律宾、澳洲西部的时间与UTC的时差均为+8,也就是UTC+8。
GMT:格林尼治标准时间(旧译格林威治平均时间或格林威治标准时间;英语:Greenwich Mean Time,GMT)是指位于英国伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线。
设置完系统时间后,还需要同步到硬件时钟上
查看RTC时间:
hwclock -r
把系统时间更新至RTC
hwclock -w
把RTC时间更新至系统
hwclock -s
设置后可以如下查看RTC信息:
cat /proc/driver/rtc
rtc_time : 09:32:13
rtc_date : 2011-03-24
alrm_time : **:**:**
alrm_date : 2063-**-31
alarm_IRQ : no
alrm_pending : no
24hr : yes