Linux 夏令时通知

Posted

技术标签:

【中文标题】Linux 夏令时通知【英文标题】:Linux daylight savings notification 【发布时间】:2017-02-02 04:44:37 【问题描述】:

我试图找到一种在应用夏令时时从系统 (Linux) 接收通知的方法,但我似乎找不到类似的东西。

假设一个程序位于pselect() 上等待多个计时器fd,所有这些计时器都有24 小时的间隔,但开始时间不同,由用户定义; “07:00 ON, 07:25 OFF”(例如,如果是咖啡机)。

因为用户在本地时间提供这些时间并且 Linux 在 UTC 上运行,所以每次夏令时发生时都需要重新调整时区调整计时器 fd。 (当他的夏令时兼容的闹钟把他叫醒时,用户希望喝咖啡......)

正如我想象的那样,解决这个问题的智能方法是注册到系统/内核/初始化/应用夏令时时要通知的任何内容,并避免陷入尝试确定此类日期的混乱事务并为自己计时,并希望系统与您的结果一致(即,您的重新同步操作和实际夏令时同时发生)。

有什么方法可以收到有关 DST 更改的通知?或者可能对本地时间有任何更改(假设 DST 更改会修改)?

【问题讨论】:

【参考方案1】:

假设一个程序位于 pselect() 上,等待多个计时器 fd,所有计时器 fd 的间隔正好是 24 小时,但开始时间不同

这就是你的根本问题。所有的日子并不完全是 24 小时——有时它们会相差一个小时(夏令时)或秒(闰秒);就像不是每个二月都有 28 天一样。

一种更简单和轻量级(消耗资源更少)的方法是使用 UTC 中的未来事件的最小堆,例如

struct trigger 
    /* Details on how the event is defined;
       for example, "each day at 07:00 local time".
    */
;

struct utc_event 
    struct trigger  *trigger;
    time_t           when;
;

struct event_min_heap 
    size_t           max_events;
    size_t           num_events;
    struct utc_event event[];
;

struct event_min_heap 中的event C99 灵活数组成员是一个带有num_events 事件的数组(为max_events 分配的内存;如果需要更多事件,可以重新分配)在min heap 中,由@987654329 键入每个 event 条目中的 @ 字段。也就是说,最早的事件总是在根。

只要当前时间至少为event[0].when,它就会被“触发”——意味着要采取的任何行动,都会被采取——并且基于它所指的struct trigger,下一次发生的时间该事件的更新为event[0],然后在堆中向下渗透到适当的位置。请注意,您只需使用 mktime() 从分解的本地时间字段中获取 UTC 时间。

(如果这是一个多用户服务,那么您可以支持多个并发时区,每个触发器一个,方法是将TZ 环境变量设置为相应的时区定义,并在调用@之前调用tzset() 987654337@。因为进程中的所有线程共享环境,所以如果您有一个多线程进程,您需要确保一次只有一个线程执行此操作。通常,使用单线程进程可以完美实现这样的事情.)

当根 (event[0]) 中的事件被删除或渗透(筛选)时,具有下一个最小 when 的事件将位于根。如果when 等于或小于当前 UTC 时间,它也会被触发。

当下一个when在未来时,进程可以休眠剩余的时间间隔。

这就是它的全部内容。您不需要多个计时器(系统范围内的有限资源),并且您无需担心某个本地时间是否为夏令时; C 库mktime() 将为您处理这些细节。


现在,如果您不喜欢这种方法(同样,它使用的资源比您在问题中概述的方法少),请联系 SystemD 开发人员。如果你足够恭顺地亲吻他们,我相信他们会为你提供 dbus 信号。它目前的设计并没有任何理智,多一个疣肯定不会让它变得更糟。切换到 C# 可能会被认为是一个加分项。


了解mktime() 计算指定时刻的 Unix 纪元时间 (time_t) 至关重要,如果适用于该特定时刻,则应用夏令时。调用函数时夏令时是否生效无关紧要!

此外,UTC 时间是协调世界时,不受时区或夏令时的影响。

考虑以下程序,mktime-example.c

#define  _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>

static time_t epoch(struct tm *const tm,
                    const int year, const int month, const int day,
                    const int hour, const int minute, const int second,
                    const int isdst)

    struct tm  temp;
    time_t     result;

    memset(&temp, 0, sizeof temp);
    temp.tm_year = year - 1900;
    temp.tm_mon = month - 1;
    temp.tm_mday = day;
    temp.tm_hour = hour;
    temp.tm_min = minute;
    temp.tm_sec = second;
    temp.tm_isdst = isdst;

    result = mktime(&temp);

    if (isdst >= 0 && isdst != temp.tm_isdst) 
        /* The caller is mistaken about DST, and mktime()
         * adjusted the time. We readjust it. */
        temp.tm_year = year - 1900;
        temp.tm_mon = month - 1;
        temp.tm_mday = day;
        temp.tm_hour = hour;
        temp.tm_min = minute;
        temp.tm_sec = second;
        /* Note: tmp.tm_isdst is kept unchanged. */

        result = mktime(&temp);
    

    if (tm)
        memcpy(tm, &temp, sizeof temp);

    return result;


static void show(const time_t t, const struct tm *const tm)

    printf("(time_t)%lld = %04d-%02d-%02d %02d:%02d:%02d",
           (long long)t, tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday,
           tm->tm_hour, tm->tm_min, tm->tm_sec);

    if (tm->tm_isdst == 1)
        printf(", DST in effect");
    else
    if (tm->tm_isdst == 0)
        printf(", DST not in effect");
    else
    if (tm->tm_isdst == -1)
        printf(", Unknown if DST in effect");

    if (tzname[0] && tzname[0][0])
        printf(", %s timezone", tzname[0]);

    printf("\n");
    fflush(stdout);


int main(int argc, char *argv[])

    struct tm  tm;
    time_t     t;
    long long  secs;
    int        arg, year, month, day, hour, min, sec, isdst, n;
    char       ch;

    if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) 
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s [ :REGION/CITY | =TIMEZONE ] @EPOCH | YYYYMMDD-HHMMSS[+-] ...\n", argv[0]);
        fprintf(stderr, "Where:\n");
        fprintf(stderr, "       EPOCH is in UTC seconds since 19700101T000000,\n");
        fprintf(stderr, "       + after time indicates you prefer daylight savings time,\n");
        fprintf(stderr, "       - after time indicates you prefer standard time.\n");
        fprintf(stderr, "\n");
        return EXIT_FAILURE;
    

    for (arg = 1; arg < argc; arg++) 

        if (argv[arg][0] == ':') 
            if (argv[arg][1])
                setenv("TZ", argv[arg], 1);
            else
                unsetenv("TZ");
            tzset();
            continue;
        

        if (argv[arg][0] == '=') 
            if (argv[arg][1])
                setenv("TZ", argv[arg] + 1, 1);
            else
                unsetenv("TZ");
            tzset();
            continue;
        

        if (argv[arg][0] == '@') 
            if (sscanf(argv[arg] + 1, " %lld %c", &secs, &ch) == 1) 
                t = (time_t)secs;
                if (localtime_r(&t, &tm)) 
                    show(t, &tm);
                    continue;
                 
            
        

        n = sscanf(argv[arg], " %04d %02d %02d %*[-Tt] %02d %02d %02d %c",
                              &year, &month, &day, &hour, &min, &sec, &ch);
        if (n >= 6) 
            if (n == 6)
                isdst = -1;
            else
            if (ch == '+')
                isdst = +1; /* DST */
            else
            if (ch == '-')
                isdst = 0;  /* Not DST */
            else
                isdst = -1;

            t = epoch(&tm, year, month, day, hour, min, sec, isdst);
            if (t != (time_t)-1) 
                show(t, &tm);
                continue;
            
        

        fflush(stdout);
        fprintf(stderr, "%s: Cannot parse parameter.\n", argv[arg]);
        return EXIT_FAILURE;
    

    return EXIT_SUCCESS;

使用例如编译它

gcc -Wall -O2 mktime-example.c -o mktime-example

不带参数运行它以查看命令行用法。运行

./mktime-example :Europe/Helsinki 20161030-035959+ 20161030-030000- 20161030-030000+ 20161030-035959- 20161030-040000-

检查 2016 年 DST 在芬兰赫尔辛基结束时的 Unix 时间戳。命令会输出

(time_t)1477789199 = 2016-10-30 03:59:59, DST in effect, EET timezone
(time_t)1477789200 = 2016-10-30 03:00:00, DST not in effect, EET timezone
(time_t)1477785600 = 2016-10-30 03:00:00, DST in effect, EET timezone
(time_t)1477792799 = 2016-10-30 03:59:59, DST not in effect, EET timezone
(time_t)1477792800 = 2016-10-30 04:00:00, DST not in effect, EET timezone

无论在运行时此 DST 是否在某个时区生效,输出都是相同的!

当用.tm_isdst = 0.tm_isdst = 1 调用mktime() 并且mktime() 更改它时,它也会更改指定的时间(按夏令时)。当.tm_isdst = -1时,表示调用者不知道是否应用了夏令时,库会发现;但如果同时存在有效的标准时间和 DST 时间,C 库将选择一个(您应该假设它是随机选择的)。上面的epoch() 函数会在必要时对此进行更正,如果用户对 DST 不正确,则取消调整时间。

【讨论】:

如果我完全理解,主要的要点是在每个事件到期后重新安排,但只有一个(下一个触发)以节省计时器。重新安排时间将使事件保持在有效的当地时间,在最坏的情况下,我会查看 23-25 小时的周期,一年两次,当事件没有在正确的时间触发时。 (仅运行一个触发 23:59 的事件并在当地时间跳到 DST 前一分钟重新安排的情况)。如果我确定 DST 总是在午夜跳跃,无论 TZ 是什么,如果我发现 .tm_gmtoff 发生变化,我可以进行每日检查并重新调整。 @Tammi:否! 每当触发事件时,您都可以使用mktime() 找出下次触发该事件的 UTC 时间。 C 库时区处理足够智能,可以确定 UTC 时间,包括是否适用夏令时。 所有事件都会在正确的时间触发。我将尝试在我的答案中添加一个实际示例。 @Tammi:在 POSIXy 系统中,包括 Linux 和 Mac OS,夏令时不是由某些操作系统服务打开和关闭的全局标志。这是一个时区属性。 C 库 mktime() 函数采用分解的本地时间字段,并将其转换为 UTC(纪元),应用当前时区规则,包括 DST 目标时间。呼叫时是否适用 DST 无关紧要。当您使用 UTC 时间戳时,“DST 跳转”是无关紧要的:我们会提前做好准备,所以根本没有“跳转”。 我想我在这里学到了宝贵的一课(虽然它第一次让我忘记了)有了mktime() 的功能(直到现在我还没有意识到)确实不需要 DST 通知(也重新安排)。这比我想象的要更好。非常感谢您一直以来的帮助和出色的示例代码。 后来我发现“比我想象的更好”可能被错误地理解 - 我的意思是 @nominal-animal 的解决方案比我正在寻找或期望的最佳解决方案要好得多找。 :-)【参考方案2】:

Unix/linux 系统只处理 UTC,它们使用 time_t 数据(从 1970 年 1 月 1 日 00:00h 到现在的秒数)作为内部时间。只有在向用户显示信息时才会转换为本地时间(由于例外情况、夏季和冬季期间的变化等导致的复杂性),因此只有在转换为本地时间时才会完成。如前所述,在 unix 系统中没有安排任何事情或为此做准备。

zdump(1),您可以获得每个时区所需的所有信息,并使用它来构建一个 crontab,以便在进行切换时通知您。它会查询本地时区数据库,并提取有关从冬季切换到夏季(包括历史)或相反的所有信息。

$ zdump -v Europe/Madrid
Europe/Madrid  Fri Dec 13 20:45:52 1901 UTC = Fri Dec 13 20:45:52 1901 WET isdst=0 gmtoff=0
Europe/Madrid  Sat Dec 14 20:45:52 1901 UTC = Sat Dec 14 20:45:52 1901 WET isdst=0 gmtoff=0
Europe/Madrid  Sat May  5 22:59:59 1917 UTC = Sat May  5 22:59:59 1917 WET isdst=0 gmtoff=0
Europe/Madrid  Sat May  5 23:00:00 1917 UTC = Sun May  6 00:00:00 1917 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sat Oct  6 22:59:59 1917 UTC = Sat Oct  6 23:59:59 1917 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sat Oct  6 23:00:00 1917 UTC = Sat Oct  6 23:00:00 1917 WET isdst=0 gmtoff=0
Europe/Madrid  Mon Apr 15 22:59:59 1918 UTC = Mon Apr 15 22:59:59 1918 WET isdst=0 gmtoff=0
Europe/Madrid  Mon Apr 15 23:00:00 1918 UTC = Tue Apr 16 00:00:00 1918 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sun Oct  6 22:59:59 1918 UTC = Sun Oct  6 23:59:59 1918 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sun Oct  6 23:00:00 1918 UTC = Sun Oct  6 23:00:00 1918 WET isdst=0 gmtoff=0
Europe/Madrid  Sat Apr  5 22:59:59 1919 UTC = Sat Apr  5 22:59:59 1919 WET isdst=0 gmtoff=0
Europe/Madrid  Sat Apr  5 23:00:00 1919 UTC = Sun Apr  6 00:00:00 1919 WEST isdst=1 gmtoff=3600
Europe/Madrid  Mon Oct  6 22:59:59 1919 UTC = Mon Oct  6 23:59:59 1919 WEST isdst=1 gmtoff=3600
Europe/Madrid  Mon Oct  6 23:00:00 1919 UTC = Mon Oct  6 23:00:00 1919 WET isdst=0 gmtoff=0
Europe/Madrid  Wed Apr 16 22:59:59 1924 UTC = Wed Apr 16 22:59:59 1924 WET isdst=0 gmtoff=0
Europe/Madrid  Wed Apr 16 23:00:00 1924 UTC = Thu Apr 17 00:00:00 1924 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sat Oct  4 22:59:59 1924 UTC = Sat Oct  4 23:59:59 1924 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sat Oct  4 23:00:00 1924 UTC = Sat Oct  4 23:00:00 1924 WET isdst=0 gmtoff=0
Europe/Madrid  Sat Apr 17 22:59:59 1926 UTC = Sat Apr 17 22:59:59 1926 WET isdst=0 gmtoff=0
Europe/Madrid  Sat Apr 17 23:00:00 1926 UTC = Sun Apr 18 00:00:00 1926 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sat Oct  2 22:59:59 1926 UTC = Sat Oct  2 23:59:59 1926 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sat Oct  2 23:00:00 1926 UTC = Sat Oct  2 23:00:00 1926 WET isdst=0 gmtoff=0
Europe/Madrid  Sat Apr  9 22:59:59 1927 UTC = Sat Apr  9 22:59:59 1927 WET isdst=0 gmtoff=0
...

顺便说一句,如果你想知道本地时间即将发生变化,你可以使用以前的信息来构建一个 crontab 文件,包括所有信息,或者简单地构建一个包含适用于你的规则的 crontab 文件地方性。例如,如果我想在西班牙的开关更改前一天收到通知(它在 3 月/10 月的最后一个星期日,02/03h 更改),您可以在您的 crontab 文件中添加一些规则:

0 0 24-30 3,10 5 echo Time daylight savings change scheduled for tomorrow | mail $USER@your.domain.com

并且将在每年 3 月 24 日至 30 日和 10 月 3,10 日这一周的每个星期六 (5) 00:00(当地时间)向您发送一封邮件。我相信您将能够根据您所在的地区或提前的时间调整此示例(因此,在时间更改发生的前一天)。

【讨论】:

在这个问题下有很好的信息 - 其他搜索类似信息的人可能仍然需要实际通知。 如何知道哪些人会对此信息感兴趣?一个很好的了解方法是对其进行投票:) "Unix/linux 系统只处理 UTC,并且 .... time_t 数据(从 1970 年 1 月 1 日 1 月 00:00 到现在的秒数)作为内部时间。"不对劲。 UTC considers leap seconds、time_t 很少这样做。 @chux,应该很容易在内部使用 TAI 而不是 UTC 并在时区校正数据库中实现闰秒校正(我在 tz 数据库文件中看到了相反的示例)(并且通过这就是 TAY->UTC 转换的原因)允许内部时间在不受干扰的情况下流动。但我没有看到这种可能性。 @LuisColorado UTC 的一个奇异问题是time_t 是闰秒列表是历史性的,不适合未来的计算。 TAI 听起来很棒,除了民用时间确实计算闰秒。相反,我怀疑 UNIX time_t, struct tm 等会继续假装闰秒不存在。

以上是关于Linux 夏令时通知的主要内容,如果未能解决你的问题,请参考以下文章

Linux系统时间时区夏令时杂谈

Linux系统时间时区夏令时杂谈

Linux下C语言恢复夏令时

通知极客晨星™Minecraft夏令营增加新班

在 Linux 下使用 gcc 在 C 中的夏令时和 mktime

有没有啥简单的方法可以在 C/C++ 中获得 Linux 下的夏令时转换时间