关于C:Linux夏令时通知

Linux daylight savings notification

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

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

由于用户在本地时间提供这些时间,并且Linux在UTC上运行,因此需要在每次夏时制发生时重新调整时区调整计时器fd。(当符合夏令时的闹钟唤醒他时,用户希望喝咖啡…)

正如我想象的那样,实现这一点的智能方法是在应用夏令时向系统/kernel/init/注册任何需要通知的内容,避免陷入自己试图确定这些日期和时间的混乱事务中,并希望系统同意您的结果(即,您的重新同步操作和实际夏令时保存NGS同时发生)。

是否有任何方法通知DST变更?或者对当地时间的任何更改(假设DST更改会修改这一点)?


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ 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文件中添加一些规则:

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

每年3月24日至30日(3月30日至10月30日)每星期六(5日)00:00(当地时间)向您发送一封邮件。我相信您能够将这个例子适应您的本地性或提前时间(所以,在时间变化发生的前一天)。


Consider a program sits on a pselect() waiting for a number of timer fd's, all which have exactly 24-hour intervals, but differing start times

这就是你的根本问题。所有的日子都不完全是24小时长——有时是一小时(夏令时)或几秒(闰秒),就像不是每个二月都有28天一样。

一种更简单、更轻(消耗的资源更少)的方法是在UTC中使用一个最小的未来事件堆,类似于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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中的eventc99灵活数组成员是一个包含num_events事件(分配给max_events的内存;如果需要更多事件,可以重新分配)的数组,它位于每个event项中由when字段键控的最小堆中。也就是说,最早的事件总是在根目录。

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

(如果这是一个多用户服务,那么您可以支持多个并发时区,每个触发器一个,方法是将TZ环境变量设置为各自的时区定义,并在调用mktime()之前调用tzset()。因为环境由进程中的所有线程共享,所以如果您有多线程进程,那么一次只需要确保一个线程这样做。通常,像这样的东西可以使用单线程过程完美地实现。)

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

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

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

现在,如果您不喜欢这种方法(同样,这种方法使用的资源比您在问题中概述的方法少),请与SystemD开发人员联系。如果你对他们有足够的恭维,我相信他们会为你提供一个DBUS信号。它不像在它的当前设计中有任何健全,而且一个疣肯定不会使它更糟。切换到C可能被认为是一个优势。

重要的是要理解,mktime()计算指定时刻的unix epoch time(time_t),如果适用于该特定时刻,则应用夏令时。调用函数时,夏令时是否有效并不重要!

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#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("
"
);
    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 ]
"
, argv[0]);
        fprintf(stderr,"       %s [ :REGION/CITY | =TIMEZONE ] @EPOCH | YYYYMMDD-HHMMSS[+-] ...
"
, argv[0]);
        fprintf(stderr,"Where:
"
);
        fprintf(stderr,"       EPOCH is in UTC seconds since 19700101T000000,
"
);
        fprintf(stderr,"       + after time indicates you prefer daylight savings time,
"
);
        fprintf(stderr,"       - after time indicates you prefer standard time.
"
);
        fprintf(stderr,"
"
);
        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.
"
, argv[arg]);
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

使用例如

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

运行它时不带参数,以查看命令行的用法。跑

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

在芬兰赫尔辛基研究2016年夏令时结束前后的Unix时间戳。命令将输出

1
2
3
4
5
(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,库会发现;但是如果同时存在有效的标准时间和DST时间,C库会选择一个(你应该假设它是随机的)。上面的epoch()功能在必要时对此进行纠正,如果用户对dst不正确,则取消调整时间。