最近遇到一个问题:机器深度休眠后唤醒,发现唤醒后系统时间与pc实际时间相比变慢,休眠时间越长,系统时间与实际时间偏差越大。
一、问题分析
linux时间有2种,rtc时钟与系统时钟。Rtc时钟是不断电的,一般由纽扣(锂)电池供电,关机后也处于供电状态。在开机时,会取rtc时间赋值给系统时间,之后系统时间独立运作。休眠时,会把系统时间与rtc时间的差值给保存起来,然后唤醒的时候把差值加上唤醒时的rtc时间再赋值给系统时间,也就是状态栏上显示的时间值。
所以明显是休眠的时候rtc时间变慢所致,说明rtc这里有问题。
Rtc硬件部份如下图所示,在确定了硬件vcc-pll、vcc-rtc、x32k信号后,明确应该是内部rtc控制器部份设置的问题。
对于平台来说,rtc时间的clk有2种,一种是32.768k的外部晶振;另一种是24M系统时间,内部PLL倍频再分频得到的,分频得到的大概是32k左右。
而sdk默认的是
tmp_data = REG_CLK32K_AUTO_SWT_EN | EXT_LOSC_GSM; writel(tmp_data, chip->base + SUNXI_LOSC_CTRL) |
也就是选的内部分频,但是由于spec上没有提供详细的clk tree,我也不知道分频出来为什么不准。又为什么不用外部32k的晶振作为时钟源?这里都不清楚。
只能说把rtc时钟源改成外部32k时钟后,就ok了,休眠24小时慢1s左右。一切正常。但是问题的本质原因是什么?还是不清楚。这里还没涉及到温漂,压差这些,一般的平台应该不会有这个小问题的。
二、Linux时间类型与rtc驱动分析
Linux rtc驱动在drvier/rtc/下面。内核已经把驱动架构搭好,各平台加入自己的驱动就可。
以rtc_sunxi.c为例。
chip->rtc = devm_rtc_device_register(&pdev->dev, "sunxi-rtc",
&sunxi_rtc_ops, THIS_MODULE);
向驱动架构注册rtc device。而平台驱动要做的,也就是实现rtc_class_ops操作。
static const struct rtc_class_ops sunxi_rtc_ops = { .read_time = sunxi_rtc_gettime, .set_time = sunxi_rtc_settime, .read_alarm = sunxi_rtc_getalarm, .set_alarm = sunxi_rtc_setalarm, .alarm_irq_enable = sunxi_rtc_alarm_irq_enable }; |
具体驱动架构与流程分析不表。
在此目录下面还有systohc.c/hctosys.c,也就是hwclock用来实现系统时间与rtc时间的同步工具。
1. hwclock命令
Hwclock用来显示/设置硬件rtc时间。具体用法如下:
hwclock --show 显示硬件时钟时间;
hwclock --hctosys 设置系统时间,使得系统时间变为跟硬件时间同步;
hwclock --systohc 设置硬件时间,使得硬件时钟变为跟系统时间同步;
查询系统时间:date
查询/设置硬件时间:
hwclock –r 显示硬件时钟与日期
hwclock –s 将系统时钟调整为与目前的硬件时钟一致。
hwclock –w 将硬件时钟调整为与目前的系统时钟一致
如:
date 042410302019.40
hwclock -w
date && hwclock
Wed Apr 24 11:27:03 CST 2019
Wed Apr 24 11:26:58 2019 0.000000 seconds
设置后可以通过cat /proc/driver/rtc查看,如下面
2. date命令
date显示或设置系统时间与日期
date命令并不从RTC获取时间,它获取的是内核xtime时间,也就是系统时间。
% H 小时(00..23)
% I 小时(01..12)
% k 小时(0..23)
% l 小时(1..12)
% M 分(00..59)
% p 显示出AM或PM
% r 时间(hh:mm:ss AM或PM),12小时
% s 从1970年1月1日00:00:00到目前经历的秒数
% S 秒(00..59)
% T 时间(24小时制)(hh:mm:ss)
% X 显示时间的格式(%H:%M:%S)
% Z 时区 日期域
% a 星期几的简称( Sun..Sat)
% A 星期几的全称( Sunday..Saturday)
% b 月的简称(Jan..Dec)
% B 月的全称(January..December)
% c 日期和时间( Mon Nov 8 14:12:46 CST 1999)
% d 一个月的第几天(01..31)
% D 日期(mm/dd/yy)
% h 和%b选项相同
% j 一年的第几天(001..366)
% m 月(01..12)
% w 一个星期的第几天(0代表星期天)
% W 一年的第几个星期(00..53,星期一为第一天)
% x 显示日期的格式(mm/dd/yy)
% y 年的最后两个数字( 1999则是99)
% Y 年(例如:1970,1996等)
格式:
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)是指位于英国伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线。
设置完系统时间后,还需要同步到硬件时钟上
A、在虚拟终端中使用date命令来查看和设置系统时间
查看系统时钟的操作: # date
设置系统时钟的操作: # date 091713272003.30
通用的设置格式: # date 月日时分年.秒
B、使用hwclock或clock命令查看和设置硬件时钟
查看硬件时钟的操作: # hwclock --show 或 # clock --show
2003年09月17日 星期三 13时24分11秒 -0.482735 seconds
设置硬件时钟的操作:
# hwclock --set --date="09/17/2003 13:26:00" 或者
# clock --set --date="09/17/2003 13:26:00"
通用的设置格式:hwclock/clock --set --date=“月/日/年 时:分:秒”
C、同步系统时钟和硬件时钟
# hwclock --hctosys 或者
# clock --hctosys
上面命令中,--hctosys表示Hardware Clock to SYStem clock
系统时钟和硬件时钟同步:
# hwclock --systohc 或者
# clock --systohc
三、linux取时间方式
1、linux时间函数总结
在linux下,常用的获取时间的函数有如下几个:
asctime, ctime, gmtime, localtime, gettimeofday,
mktime, asctime_r, ctime_r, gmtime_r, localtime_r,
clock_gettime
2、常用的结构体
2.1 struct tm
struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* day of the month */
int tm_mon; /* month */
int tm_year; /* year */
int tm_wday; /* day of the week */
int tm_yday; /* day in the year */
int tm_isdst; /* daylight saving time */
};
//int tm_sec 代表目前秒数,正常范围为0-59,但允许至61秒
//int tm_min 代表目前分数,范围0-59
//int tm_hour 从午夜算起的时数,范围为0-23
//int tm_mday 目前月份的日数,范围01-31
//int tm_mon 代表目前月份,从一月算起,范围从0-11
//int tm_year 从1900 年算起至今的年数
//int tm_wday 一星期的日数,从星期一算起,范围为0-6
//int tm_yday 从今年1月1日算起至今的天数,范围为0-365
//int tm_isdst 日光节约时间的旗标
2.2 struct timeval,struct timezone
struct timeval {
time_t tv_sec; /* seconds (秒)*/
suseconds_t tv_usec; /* microseconds(微秒) */
};
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of DST correction */
};
int tz_minuteswest; /* 格林威治时间往西方的时差 */
int tz_dsttime; /* 时间的修正方式*/
3、时间函数介绍
3.1 time() 函数获取当前时间
SYNOPSIS
#include <time.h>
time_t time(time_t *t);
DESCRIPTION
time() returns the time as the number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC).
//此函数会返回从公元1970年1月1日的UTC时间从0时0分0秒算起到现在所经过的秒数。如果t 并非空指针的话,此函数也会将返回值存到t指针所指的内存。
RETURN VALUE
On success, the value of time in seconds since the Epoch is returned. On error, ((time_t) -1) is returned, and errno is
set appropriately.
ERRORS
EFAULT t points outside your accessible address space.
//成功返回秒数,错误则返回(time_t) -1),错误原因存于errno中
用例:
#include <stdio.h>
#include <string.h>
#include <time.h>
int main()
{
time_t seconds;
seconds = time((time_t *)NULL);
printf("%d\n", seconds);
return 0;
}
3.2 localtime_r() localtime() 取得当地目前时间和日期
函数原型如下:
#include <time.h>
struct tm *localtime(const time_t *timep);
struct tm *localtime_r(const time_t *timep, struct tm *result);
/* 该函数将有time函数获取的值timep转换真实世界所使用的时间日期表示方法,然后将结果由结构tm返回 */
/** 需要注意的是localtime函数可以将时间转换本地时间,但是localtime函数不是线程安全的。多线程应用里面,应该用localtime_r函数替代localtime函数,因为 localtime_r是线程安全的 **/
用例:
#include <stdio.h>
#include <string.h>
#include <time.h>
int main()
{
time_t timep;
struct tm *p;
time(&timep);
p = localtime(&timep);
printf("%d-%d-%d %d:%d:%d\n", (1900 + p->tm_year), ( 1 + p->tm_mon), p->tm_mday,(p->tm_hour + 12), p->tm_min, p->tm_sec);
return 0;
}
3.3 asctime() asctime_r()将时间和日期以字符串格式返回
函数原型如下:
#include <time.h>
struct tm *gmtime(const time_t *timep);
struct tm *gmtime_r(const time_t *timep, struct tm *result);
char *asctime(const struct tm *tm);
char *asctime_r(const struct tm *tm, char *buf);
/** gmtime是把日期和时间转换为格林威治(GMT)时间的函数。将参数time 所指的time_t 结构中的信息转换成真实世界所使用的时间日期表示方法,然后将结果由结构tm返回 **/
/**asctime 将时间以换为字符串字符串格式返回 **/
用例:
#include <stdio.h>
#include <string.h>
#include <time.h>
int main()
{
time_t timep;
time(&timep);
printf("%s\n", asctime(gmtime(&timep)));
return 0;
}
3.4 ctime()/ctime_r() 将时间和日期以字符串格式表示
函数原型如下:
#include <time.h>
char *ctime(const time_t *timep);
char *ctime_r(const time_t *timep, char *buf);
/** ctime()将参数timep所指的time_t结构中的信息转换成真实世界所使用的时间日期表示方法,然后将结果以字符串形态返回 **/
用例:
#include <stdio.h>
#include <string.h>
#include <time.h>
int main(void)
{
time_t timep;
time(&timep);
printf("%s\n", ctime(&timep));
return 0;
}
3.5 mktime() 将时间结构体struct tm的值转化为经过的秒数
函数原型:
#include <time.h>
time_t mktime(struct tm *tm);
/**将时间结构体struct tm的值转化为经过的秒数**/
用例:
#include <stdio.h>
#include <string.h>
#include <time.h>
int main()
{
time_t timep;
struct tm *p;
time(&timep);
p = localtime(&timep);
timep = mktime(p);
printf("%d\n", timep);
return 0;
}
最后结果可以看出mktime转化后的时间与time函数获取的一样
3.6 gettimeofday() 获取当前时间
函数原型如下:
#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
struct timeval {
time_t tv_sec; /* seconds (秒)*/
suseconds_t tv_usec; /* microseconds(微秒) */
};
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of DST correction */
};
//gettimeofday函数获取当前时间存于tv结构体中,相应的时区信息则存于tz结构体中
//需要注意的是tz是依赖于系统,不同的系统可能存在获取不到的可能,因此通常设置为NULL
用例:
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
int main()
{
struct timeval tv;
gettimeofday(&tv, NULL);
printf("tv_sec: %d\n", tv.tv_sec);
printf("tv_usec: %d\n", tv.tv_usec);
return 0;
}
3.7 clock_gettime () 获取时间
int clock_gettime(clockid_t clk_id, struct timespect *tp); 参数说明: clockid_t clk_id 用于指定计时时钟的类型,有以下4种: CLOCK_REALTIME:系统实时时间,随系统实时时间改变而改变,即从UTC1970-1-1 0:0:0开始计时,中间时刻如果系统时间被用户该成其他,则对应的时间相应改变 CLOCK_MONOTONIC:从系统启动这一刻起开始计时,不受系统时间被用户改变的影响 CLOCK_PROCESS_CPUTIME_ID:本进程到当前代码系统CPU花费的时间 CLOCK_THREAD_CPUTIME_ID:本线程到当前代码系统CPU花费的 |
用例:
int main()
{
int timeElapse;
struct timespec ts_begin, ts_end;
clock_gettime(CLOCK_MONOTONIC, &ts_begin);
sleep(1);
clock_gettime(CLOCK_MONOTONIC, &ts_end);
timeElapse = (ts_end.tv_sec - ts_begin.tv_sec)*1000 + (ts_end.tv_nsec-ts_begin.tv_nsec)/1000000;
printf(“elapse=%d”, timeElapse);
return 0;
}
四、其他
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()函数。
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面两种状态之一,否则任务不会睡眠。调用代码绝对不能持有锁(因为持有锁的任务是不能睡眠的)。当任务被重新调度时,将返回代码进入睡眠前的位置继续执行。