linux下时间时区详解
首先我们要明白,“时间”和“时区”是两个东西。
时间是指从某个时间点开始到另一个时间点经过的“长度”,是“纵向”距离,一般在linux系统内有两个主要的时间,一是始于1970年(unix元年)至今的距离,二是系统启动后至今的距离。前者一般是由不断电的硬件维护(RTC)或者其他专门服务器授时(NTP),可修改;后者只能前进无法后退,不能修改
时区则是指世界范围内(国家/洲际之间)昼夜交替不同而导致的每天的相对时间不同造成的区域划分,是“横向”的“偏移”。比如北京会比纽约早12小时看到日出。时区一般以1小时划分,全球就有24个时区
linux操作系统内核只有“时间”概念,没有“时区”概念。对于linux操作系统本身来说,启动后时间为0,表示当前就是1970年。我们通过读取RTC或NTP时间后,会通过系统调用更新这个时间,然后cpu会自动对时间进行累加。后面其他进程就可以获取正确的时间了。
应用进程从系统内核读取到自1970年来经历过的时间后就会有一个问题,就是同样经过了一百年整,有些地区现处于旭日东升,另一些地区确黑夜蔽日。因此就要靠时区对这个时间进行一个“合理的描述”,比如在北京,现在就应该处于正午,日头正浓,而同样的时间点在纽约,大家就进入梦乡了。
这样大家更加理解了时区概念,它并不存在,只是为了让全球在24小时的尺度内(由于地球自转)对于同样一个时间点发生的日照情况比较合理。
接下来我们着重说说应用进程如何使用时间和时区。
一般我们会将这个过程分为3步,第一步是从系统获取时间(1970年来秒数),第二步是从文件系统读取时区配置(对于现代操作系统,还可能是GPS定位或通过出口网络进行ISP运营商定位等)以获取偏移量(也即一个秒数,可能为负),第三步则是将两者相加并格式化输出。
(这里还体现了我们程序编写中的一个数据和显示分离的思维,数据还是那个数据(1970年至今秒数),但展示是因人而异的)
上面代码中的tzset十分重要,它一般会从环境变量或/etc/localtime文件中读取时区配置并设置到进程全局变量。然后localtime,strftime等函数则会参考全局变量来计算当前地区应该显示的时间。
(tzset函数可以只调用一次,除非确定了配置更改,也可以不调用,因为strftime等函数内部有判定,如果进程对tzset函数调用次数为0,则会主动调用一次)
我们具体讲讲linux下时间日期的格式化输出和其背后的运行逻辑。
首先我们要认识一个前提:就是tzset为什么必须调用?
其实道理很简单,因为c库没有自动运行的权利。它不会在你加载*c.so时就自动调用某个函数,甚至遑论读取文件系统的文件,这个操作具有一定的侵入性,也会造成一些不必要的浪费(万一你的程序不需要格式化时间日期呢)。
我们根据一个实例来跟踪一下。
我们一般在系统内查看当前时间是使用date命令,以下是一般用法。
我们跟踪一下date命令的代码(以busybox为例)
我们可以看到,其实重要的就是time/localtime/strftime几个函数。
我们接着跟踪一下。
可以发现,localtime调用了tzset_internal。而实际上,tzset也是调用的tzset_internal。
tzset后,时区的偏移量就有了。后续就可以compute了
综上所述,其实所谓的时区并不复杂,就是一个秒数偏移量,用于不同地域的格式化输出。
我们再最后稍微深入分析一下tzset的内部实现。
可以看到,tzset首先从环境变量/编译时宏定义/运行时系统文件等地方读取配置进行分析。
当然,最终更新到的全局变量在外部也可访问
从tzset代码也可以看出,一般我们配置时区还是以配置文件为主。配置文件一般在/etc/localtime。
当然,这个配置文件是二进制的,并不容易编辑。
如果只需要单独进程具有正确的时区,可以使用setenv("TZ", "CST-8", 1);tzset();来更新。
setenv中还有一个坑,就是我们为什么设置的是CST-8(CST是什么意思我就不赘述了,大家百度一下即可,其实也没什么特别的意思,写ABC没区别,它只是你为当前地区取得别名),不是说中国在东8区吗,东为正,应该是+8啊!
这里是一个思维误区,就是我们设置的是UTC时区,即是“计算出UTC零点的算法”。所以CST=UTC+(+8小时) 或 UTC=CST-(+8小时)。
最后修改时间 2024-08-04 14:13:54
ps:虽然linux设置和获取系统时间现在的函数settimeofday和gettimeofday可以传递时区,但我还是建议不使用这部分功能,还是只用获取的unix秒数部分以及/etc/localtime配置文件。
毕竟官方已经建议settimeofday中的tz传递NULL
最后修改时间 2024-08-05 11:47:06
linux常用的时间日期函数如下:
time :用于从操作系统获取Unix时间戳
settimeofday : 设置unix时间戳到操作系统(stime已弃用)
mktime : 从本地时间(加了偏移量)转为unix时间戳
localtime : 从unix时间戳转为本地时间
如果我们的进程需要负责设置系统时间,一般是两种办法,一是从服务器获取unix时间戳直接通过settimeofday设置;二是使用时区和本地时间,先将时区通过setenv或/etc/localtime方式保存,然后tszet,而后通过mktime将本地时间转为unix时间戳,再通过settimeofday设置。
mktime/localtime都会在内部使用时区配置(tzset)。
以下是一个设置系统时间的参考代码:
#include <unistd.h> #include <sys/time.h> #include <cstdio> #include <cstdlib> #include <cstring> #include <ctime> #include <sstream> #include <iomanip> #include <iostream> #include <chrono> int main() { setenv("TZ", "CST-8", 1); tzset(); std::tm when = {0}; std::stringstream ss_when; ss_when <<"2024-08-09 12:00:23"; ss_when >> std::get_time(&when, "%Y-%m-%d %H:%M:%S"); std::time_t t = mktime(&when); timeval tv = {0}; tv.tv_sec = t; ::settimeofday(&tv, nullptr); std::system("hwclock -wu"); return 0; }
(对于c++来说,从字符串转为tm要方便很多)
注意:tm和time_t转换就涉及到时区
最后修改时间 2024-08-09 22:23:48