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

posted @ 2024-08-04 14:15  云中双月  阅读(432)  评论(0编辑  收藏  举报