LV05-07-Linux系统-02-系统时间
本文主要是用C语言获取Linux系统时间的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
开发板 | 正点原子 i.MX6ULL Linux阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
linux内核 | linux-4.15(NXP官方提供) |
STM32开发板 | 正点原子战舰V3(STM32F103ZET6) |
点击查看本文参考资料
参考方向 | 参考原文 |
--- | --- |
一、日期与时间
1. 时间的基本概念
1.1 时区
地球是自西向东自转,东边比西边先看到太阳,东边的时间也比西边的早。东边时刻与西边时刻的差值不仅要以时计,而且还要以分和秒来计算,这给人们带来不便。
为了克服时间上的混乱,1884
年在华盛顿召开的一次国际经度会议(又称国际子午线会议)上,规定将全球划分为24
个时区(东、西各12
个时区)。
以格林尼治天文台旧址的本初子午线作为零度经线,将全球划分为东西两个半球,24
个时区就为东1—12
区,西1—12
区。每个时区横跨经度15
度,时间正好是1
小时。最后的东、西第12
区其实是一个时区,就是12
区,它们各跨经度7.5
度,以东、西经180
度为界。而本初子午线所在时区被称为中时区(零时区)。
每个时区的中央经线上的时间就是这个时区内统一采用的时间,称为区时。相邻两个时区的时间相差1
小时。例如,我国东8
区的时间总比泰国东7
区的时间早1
小时,而比日本东9
区的时间晚1小时。因此,出国旅行的人,必须随时调整自己的手表,才能和当地时间相一致。凡向西走,每过一个时区,就要把表向前拨1
小时(比如2
点拨到1
点);凡向东走,每过一个时区,就要把表向后拨1
小时(比如1
点拨到2
点)。
实际上,世界上不少国家和地区都不严格按时区来计算时间。为了在全国范围内采用统一的时间,一般都把某一个时区的时间作为全国统一采用的时间。例如,我国把首都北京所在的东8
区的时间作为全国统一的时间,称为北京时间,北京时间就作为我国使用的本地时间,我们电脑上显示的时间就是北京时间,我国国土面积广大,由东到西横跨了5
个时区,也就意味着我国最东边的地区与最西边的地区实际上相差了4
、5
个小时。又例如,英国、法国、荷兰和比利时等国,虽地处中时区,但为了和欧洲大多数国家时间相一致,则采用东1
区的时间。
1.2 GMT 时间
GMT
(Greenwich Mean Time
)中文全称是格林尼治标准时间,这个时间系统的概念在1884
年被确立,由英国伦敦的格林威治皇家天文台计算并维护,并在之后的几十年向欧陆其它国家扩散。
由于从19
世纪开始,因为世界各国往来频繁,而欧洲大陆、美洲大陆以及亚洲大陆都有各自的时区,所以为了避免时间混乱,1884
年,各国代表在美国华盛顿召开国际大会,通过协议选出英国伦敦的格林威治作为全球时间的中心点,决定以通过格林尼治的子午线作为划分东西两半球的经线零度线(本初子午线、零度经线),由此格林威治标准时间因而诞生。GMT
时间就是英国格林威治当地时间,也就是零时区(中时区)所在时间,例如GMT 12:00
就是指英国伦敦的格林威治皇家天文台当地的中午12:00
,与我国的标准时间北京时间(东8
区)相差8
个小时,即早8
个小时,所以GMT 12:00
对应的北京时间是20:00
。
1.3 UTC 时间
UTC
(Coordinated Universal Time
)指的是世界协调时间(又称世界标准时间、世界统一时间),是经过平均太阳时(以格林尼治时间GMT
为准)、地轴运动修正后的新时标以及以秒为单位的国际原子时所综合精算而成的时间,计算过程相当严谨精密,因此若以世界标准时间的角度来说,UTC
比GMT
更加精准。
GMT
与UTC
这两者几乎是同一概念,它们都是指格林威治标准时间,也就是国际标准时间,只不过UTC
时间比GMT
时间更加精准。
1.4 CST 时间
在Ubuntu
中,我们还可能会看到CST
时间,这又是什么呢?CST
可视为美国、澳大利亚、古巴或中国的标准时间。CST
可以为如下4
个不同的时区的缩写:
美国中部时间:
Central Standard Time (USA) UT-6:00
澳大利亚中部时间:
Central Standard Time (Australia) UT+9:30
中国标准时间:
China Standard Time UT+8:00
古巴标准时间:
Cuba Standard Time UT-4:00
2. Ubuntu 中的时间
2.1 相关命令
在Ubuntu
中,我们可以通过date
命令来查看系统当前的本地时间,我们在终端输入以下命令:
1 | date |
然后便会出现以下信息:
1 | 2022年 06月 21日 星期二 07:07:53 CST |
CST
在这里其实指的是China Standard Time
(中国标准时间)的缩写,表示当前查看到的时间是中国标准时间,也就是我国所使用的标准时间——北京时间,一般在安装Ubuntu
系统的时候会提示我们设置所在城市,那么系统便会根据我们所设置的城市来确定系统的本地时间对应的时区,就像我设置的城市为上海,那么系统的本地时间就是北京时间,这是因为我国统一使用北京时间作为本国的标准时间。
我们在终端输入以下命令:
1 | date -u |
然后便会看到输出的时间信息如下:
1 | 2022年 06月 20日 星期一 23:07:49 UTC |
这样显示的就是UTC
时间了,可以看到与上边的CST
时间相差了8
小时。
2.2 时区信息文件
在Ubuntu
系统,时区信息通常以标准格式保存在一些文件当中,这些文件通常位于/usr/share/zoneinfo
目录下,该目录下的每一个文件(包括子目录下的文件)都包含了一个特定国家或地区内时区制度的相关信息,且往往根据其所描述的城市或地区缩写来加以命名,例如,EST
(美国东部标准时间)、CET
(欧洲中部时间)、UTC
(世界标准时间)、Hongkong
、等,也把这些文件称为时区配置文件,我们可以通过以下命令查看相关文件名称:
1 | ls /usr/share/zoneinfo |
然后便会看到以下信息输出:
系统的本地时间由时区配置文件/etc/localtime
定义,通常链接到/usr/share/zoneinfo
目录下的某一个文件(或其子目录下的某一个文件),我们可以查看一下这个文件:
1 | ls -l /etc/localtime |
然后便会看到以下信息:
这样的话,如果我们要修改Ubuntu
系统本地时间的时区信息,可以直接将/etc/localtime
链接到/usr/share/zoneinfo
目录下的任意一个时区配置文件:
1 | sudo rm -rf localtime # 删除原有链接文件 |
3. Linux 系统中的时间
3.1 实时时钟RTC
操作系统中一般会有两个时钟,一个系统时钟(system clock
),一个实时时钟(Real time clock
),也叫RTC
。系统时钟由系统启动之后由内核来维护,例如,使用date
命令查看到的就是系统时钟,它在系统关机情况下是不存在的;而实时时钟一般由RTC
时钟芯片提供,RTC
芯片有相应的电池供电,以保证系统在关机情况下RTC
能够继续工作、继续计时。
3.2 Linux 如何记录时间
Linux
系统在开机启动之后首先会读取RTC
硬件获取实时时钟作为系统时钟的初始值,之后内核便开始维护自己的系统时钟。所以,RTC
硬件只有在系统开机启动时会读取一次,目的是用于对系统时钟进行初始化操作,之后的运行过程中便不会再对其进行读取操作了。而在系统关机时,内核会将系统时钟写入到RTC
硬件。
3.3 jiffies
jiffies
是内核中定义的一个全局变量,内核使用jiffies
来记录系统从启动以来的系统节拍数,所以这个变量用来记录以系统节拍时间为单位的时间长度。
Linux
内核在编译配置时定义了一个节拍时间,使用节拍率(一秒钟多少个节拍数)来表示,例如如常用的节拍率为100Hz
(1
秒钟100
个节拍数,节拍时间为1s / 100
)、200Hz
(1
秒钟200
个节拍,节拍时间为1s / 200
)、250Hz
(1
秒钟250
个节拍,节拍时间为1s / 250
)、300Hz
(1
秒钟300
个节拍,节拍时间为1s / 300
)、500Hz
(1
秒钟500
个节拍,节拍时间为1s / 500
)等。由此可以发现配置的节拍率越低,每一个系统节拍的时间就越短,也就意味着jiffies
记录的时间精度越高,当然,高节拍率会导致系统中断的产生更加频繁,频繁的中断会加剧系统的负担,一般默认情况下都是采用100Hz
作为系统节拍率。
内核其实是通过jiffies
来维护系统时钟,全局变量jiffies
在系统开机启动时会设置一个初始值,RTC
实时时钟会在系统开机启动时读取一次,目的是用于对系统时钟进行初始化,这里说的初始化其实指的就是对内核的jiffies
变量进行初始化操作,具体如何将读取到的实时时钟换算成jiffies
数值,后边用到了再说吧。
操作系统使用jiffies
这个全局变量来记录当前时间,当我们需要获取到系统当前时间点时,就可以使用jiffies
变量去计算,当然并不需要我们手动去计算,Linux
系统提供了相应的系统调用或C
库函数用于获取当前时间,例如如系统调用time()
、gettimeofday()
,其实质上就是通过jiffies
变量换算得到。
【说明】系统的节拍率可以使用下边的函数获取:
1 | sysconf(_SC_CLK_TCK); |
3.4 日历时间
自1970-01-01 00:00:00 +0000 (UTC)
以来经过的总秒数,我们把这个称之为日历时间或time_t
时间。为什么是1970-01-01
开始嘛,可以上网搜一下,这里就不写了。
4. 获取时间
4.1 time() 函数
4.1.1 函数说明
在linux
下可以使用man 2 time
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于获取当前时间,以秒为单位。
【函数参数】
tloc
:time_t
类型指针变量,如果tloc
参数不是NULL
,则返回值也存储在tloc
指向的内存中。
【返回值】time_t
类型,成功则返回自1970-01-01 00:00:00 +0000 (UTC)
以来的时间值(以秒为单位);失败则返回-1
,并会设置errno
表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)time
函数获取得到的是一个时间段,也就是从1970-01-01 00:00:00 +0000 (UTC)
到现在这段时间所经过的秒数,所以我们要计算现在这个时间点,只需要使用time()
得到的秒数加1970-01-01 00:00:00
即可。
(2)我们要是想打印出这个总的秒数,使用printf
的时候,格式字符需要使用%ld
。
4.1.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | nowTime: 1655778106 |
4.2 gettimeofday() 函数
4.2.1 函数说明
在linux
下可以使用man 2 gettimeofday
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于获取当前时间,以微秒为单位。
【函数参数】
tv
:struct timeval
类型的结构体指针变量,获取得到的时间值存储在该所指向的struct timeval
结构体变量中,该结构体包含了两个成员变量tv_sec
和tv_usec
,分别用于表示秒和微秒,所以获取得到的时间值就是tv_sec
(秒)+
tv_usec
(微秒),同样获取得到的秒数与time()
函数一样,也是自1970-01-01 00:00:00 +0000 (UTC)
到现在这段时间所经过的秒数,也就是日历时间,time()
返回得到的值和函数gettimeofday()
所返回的tv
参数中tv_sec
字段的数值相同。
点击查看 struct timeval 结构体成员
在使用man
手册的时候会有说明:
1 | struct timeval |
tz
:struct timezone
类型结构体指针变量,早期实现用其来获取系统的时区信息,目前已遭废弃,在调用gettimeofday()
函数时应将参数tz
设置为NULL
。
点击查看 struct timeval 结构体成员
在使用man
手册的时候会有说明:
1 | struct timezone |
【返回值】time_t
类型,成功则返回自0
;失败则返回-1
,并会设置errno
表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
4.2.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | nowTime: 1655778991s+882392us |
5. 时间转换
上边两个函数可以获取到时间,但是呢,获取的都是相对于1970-01-01 00:00:00 +0000 (UTC)
这个时间点所经过时间(日历时间),所以获取得到的是一个时间段的长度秒数,我们总不能真的自己去换算吧,C
语言中已经为我们提供了相关函数来帮助我们做这个事情。
5.1 ctime() 函数
5.1.1 函数说明
在linux
下可以使用man 3 ctime
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数可以将日历时间转换为可打印输出的字符串形式,ctime_r()
为ctime()
函数的可重入版本,具体什么是可重入,什么是不可重入,现在就大概知道可重入版本的函数可以避免一些安全隐患,更加推荐使用。
【函数参数】
timep
:time_t
类型指针变量,该参数包含了一个日历时间,也就是上边获取时间函数获取的那个秒数。buf
:char
类型指针变量,该参数为可重入版本的函数ctime()
所有,它指向存放转换后的时间字符串的缓冲区首地址。
【返回值】char
类型指针变量,成功则返回指向转换后得到的字符串;失败将返回NULL
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
5.1.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | nowTime: 1655781498s |
5.2 localtime() 函数
5.2.1 函数说明
在linux
下可以使用man 3 localtime
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数可以把time()
或gettimeofday()
得到的秒数(time_t
时间或日历时间)变成一个struct tm
结构体所表示的时间,该时间对应的是本地时间。localtime_r()
为localtime()
函数的可重入版本,更加推荐使用。
【函数参数】
timep
:time_t
类型指针变量,该参数包含了一个日历时间,也就是上边获取时间函数获取的那个秒数。result
:struct tm
类型结构体指针变量,该参数为可重入版本的函数localtime_r()
所有,转换后的本地时间将存放在该参数指向的结构体中。
【返回值】struct tm
类型的结构体指针,成功则返回一个有效的struct tm
结构体指针,而对于可重入版本localtime_r()
来说,成功执行情况下,返回值将会等于参数result
;失败则返回NULL
。
点击查看 struct tm 结构体成员
在使用man
手册的时候,下边会有这个结构体成员的说明:
1 | struct tm |
【注意事项】要想得到实际的时间,那么年份需要加上1900
,月份需要加上1
。这是因为这个函数内部转换的时候,年份从1900
开始算,月份从0
开始算了。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
5.2.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | tm: 1655782709s |
6. 时间相关API
总结
在Linux
系统下常用的时间相关的系统调用和库函数,主要有下边几个:
1 | time_t time(time_t *tloc); |
这些函数总结如下:
三、进程时间
1. 什么是进程时间
进程时间指的是进程从创建后(也就是程序运行后)到目前为止这段时间内使用CPU
资源的时间总数,出于记录的目的,内核把CPU
时间(进程时间)分为以下两个部分:
用户
CPU
时间:进程在用户空间(用户态)下运行所花费的CPU
时间,有时也称为虚拟时间(virtual time
)。这对于程序来说,是它已经得到CPU
的时间系统
CPU
时间:进程在内核空间(内核态)下运行所花费的CPU
时间。这是内核执行系统调用或代表进程执行的其它任务(例如,服务页错误)所花费的时间。
一般来说,进程时间指的是用户CPU
时间和系统CPU
时间的总和,也就是总的CPU
时间。
【注意事项】进程时间不等于程序的整个生命周期所消耗的时间,如果进程一直处于休眠状态(进程被挂起、不会得到系统调度),那么它并不会使用CPU
资源,所以休眠的这段时间并不计算在进程时间中。
2. times() 函数
2.1 函数说明
在linux
下可以使用man 2 times
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于获取当前进程的时间。
【函数参数】
buf
:struct tms
类型接结构体指针变量,该参数指向一个保存了当前进程时间信息的struct tms
结构体变量。
点击查看 struct tms 结构体变量成员
在使用man
手册的时候,会有该结构体的相关说明:
1 | struct tms |
上边的四个成员说明如下:
tms_utime | user time, 进程的用户CPU时间, tms_utime个系统节拍数 |
tms_stime | system time, 进程的系统CPU时间, tms_stime个系统节拍数 |
tms_cutime | user time of children, 已死掉子进程的tms_utime + tms_cutime时间总和 |
tms_cstime | system time of children, 已死掉子进程的tms_stime + tms_cstime时间总和 |
【返回值】clock_t
类型(实质是long
类型),成功时将返回从过去任意的一个时间点(例如系统启动时间)所经过的时钟滴答数(其实就是系统节拍数),将(节拍数 / 节拍率)便可得到秒数,返回值可能会超过clock_t
所能表示的范围(溢出);调用失败返回(clock_t) -1
,并设置errno
表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | 系统节拍率:100 |
可以看到用户CPU
时间为0.17
秒,系统CPU
时间为0
秒,也就是说测试的这段代码并没有进入内核态运行,所以
1 | 总的进程时间 = 用户CPU时间 + 系统CPU时间 = 0.17秒 |
上边显示的时间总和并不是总的进程时间,这个时间总和指的是从起点到终点所经过的时间,并不是进程时间。时间总和包括了进程处于休眠状态时消耗的时间(sleep
等会让进程挂起、进入休眠状态),可以发现时间总和比进程时间多1
秒,其实这一秒就是进程处于休眠状态的时间。至于还多出来了0.01
秒,我自己感觉是语句执行耗费的时间,但也可能不是,这里我就没有追究那么深入了,后边万一用到了,搞明白了,再在这里补充修改吧。
3. clock() 函数
3.1 函数说明
在linux
下可以使用man 2 clock
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数也用于获取进程时间,使用方式比times
更加简单。
【函数参数】none
【返回值】clock_t
类型,返回的值是到目前为止使用CPU
的时间,这个时间并不是系统节拍数。要获得使用的秒数,还需要除以CLOCKS_PER_SEC
。如果所用的处理器时间不可用,或者它的值无法表示,那么这个函数将返回值(clock_t) -1
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】clock()
函数虽然可以很方便的获取总的进程时间,但并不能获取到单独的用户CPU
时间和系统CPU
时间。
3.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | 总CPU时间: 0.197372秒, CLOCKS_PER_SEC = 1000000 |