Linux时区在应用程序未发送夏令时标志时也会自动应用夏令时
我正在开发一套嵌入式Linux系统,在该系统中应用程序根据用户选择的UTC偏移动态设置系统时区。
我们使用一个内部枚举与Linux时区名称之间的预定义映射,然后通过创建一个指向 /etc/localtime 的符号链接来应用时区:
static std::map<int, std::pair<string, string>> GmtValue = {
{ GMT_11, { "Etc/GMT+11", "GMT -11 Midway Islands, Samoa, Niue"}},
{ GMT_10, { "Etc/GMT+10", "GMT -10 Fakaofo, Hawaii, Tahiti"}},
{ GMT_9, { "US/Alaska", "GMT -9 Alaska, Gambier"}},
{ GMT_8, { "PST8PDT", "GMT -8 Pacific Time"}},
{ GMT_7, { "Etc/GMT+7", "GMT -7 Mountain Time"}},
{ GMT_6, { "CST6CDT", "GMT -6 Central Time"}},
{ GMT_5, { "EST5EDT", "GMT -5 Eastern Time"}},
{ GMT_4, { "Etc/GMT+4", "GMT -4 Atlantic Time"}},
{ GMT_3, { "Etc/GMT+3", "GMT -3 Rio de Janeiro, Buenos Aires"}},
{ GMT_2, { "Etc/GMT+2", "GMT -2 Mid-Atlantic"}},
{ GMT_1, { "Etc/GMT+1", "GMT -1 Azores, Cape Verde"}},
{ GMT0, { "GB", "GMT 0 London, Lisbon, Dublin"}},
{ GMT1, { "CET", "GMT +1 Paris, Zurich, Rome"}},
{ GMT2, { "EET", "GMT +2 Athens, Sofia, Cairo"}},
{ GMT3, { "Etc/GMT-3", "GMT +3 Moscow, Kuwait"}},
{ GMT33, { "Asia/Tehran", "GMT +3.30 Tehran"}},
{ GMT4, { "Etc/GMT-4", "GMT +4 Abu Dhabi"}},
{ GMT43, { "Asia/Kabul", "GMT +4.30 Kabul"}},
{ GMT5, { "Etc/GMT-5", "GMT +5 Islamabad"}},
{ GMT53, { "Asia/Colombo", "GMT +5.30 Colombo"}},
{ GMT6, { "Etc/GMT-6", "GMT +6 Dhaka"}},
{ GMT7, { "Etc/GMT-7", "GMT +7 Bangkok"}},
{ GMT8, { "Etc/GMT-8", "GMT +8 Beijing"}},
{ GMT9, { "Etc/GMT-9", "GMT +9 Tokyo"}},
{ GMT93, { "Australia/Darwin", "GMT +9.30 Darwin"}},
{ GMT10, { "Etc/GMT-10", "GMT +10 Sydney"}},
{ GMT11, { "Etc/GMT-11", "GMT +11 New Caledonia"}},
{ GMT12, { "Etc/GMT-12", "GMT +12 Auckland"}},
{ GMT13, { "Etc/GMT-13", "GMT +13 Nuku'alofa"}},
{ GMT14, { "Pacific/Apia", "GMT +14 Apia"}}
};
用于更新时区的函数:
void DeviceManagement::updateSystemZoneInfo(int zone)
{
printf("updateSystemZoneInfo!\n");
if (zone >= ApplicationDateTimeSt::DATETIME_NUM)
return;
const map<string, string> tzPaths =
{
{ "/etc/localtime", "/usr/share/zoneinfo/" },
{ "/etc/TZ", "/usr/share/zoneinfo/uclibc/" }
};
pair<string, string> tzPath;
struct stat buf;
for (auto &path : tzPaths)
{
if (!lstat(path.first.c_str(), &buf))
{
tzPath = path;
break;
}
}
if (tzPath.first.empty())
return;
unlink("/etc/comelit/localtime");
string newZonePath =
tzPath.second + ApplicationDateTimeSt::GmtValue[zone].first;
symlink(newZonePath.data(), "/etc/comelit/localtime");
}
问题在于,某些时区名称例如 CET、PST8PDT、EST5EDT 和 EET 会自动应用夏令时(DST),使系统时钟在夏季偏移+1小时。
然而,应用程序本身并不发送或管理任何DST标志。
系统仍然应用来自Linux时区数据库 (tzdata) 的DST规则,导致我们嵌入式产品的时间行为不正确。
我们不能直接修改映射,因为它在多个软件组件之间共享。
在不改变现有时区映射的前提下,在嵌入式Linux系统上强制固定的UTC偏移且不启用DST的正确做法是什么?
解决方案
然而,应用程序本身并不发送或管理任何DST标志。
这并不完全准确。应用程序“发送”了时区 US/Alaska,而这个时区被定义为在标准时间为UTC-9,夏令时为UTC-8。关于切换发生的时间规则也包含在tzdata对 US/Alaska 的定义中。因此,使用该时区会“标记”出使用DST的意图。若不想使用DST,应用程序需要指定一个不含DST成分的时区,比如 Etc/GMT+9(首选)或 Pacific/Gambier(不太鲁棒,因为未来DST可能被使用)。
维基百科维护着tz数据库时区列表。基于此,你的应用使用并被定义为使用DST的时区包括 US/Alaska、PST8PDT(并不意外,因为缩写中的“DT”代表“Daylight Time”)、CST6CDT(也不奇怪)、EST5EDT(不仅提到“DT”,而且 EST 被定义为一个不使用DST的独立时区)、GB、CET、和 EET。所以基本上,应用通过使用tz数据库中的这些时区定义来请求DST。
有趣的是,应用实现了“GMT +14 Apia”,你确实需要DST才能得到+14。萨摩亚(包括阿皮亚)的标准时区是GMT+13,因此名称“GMT +14 Apia”似乎表示应始终应用DST,而不是从不应用。对于应用来说不利的是,维基百科指出自2021年以来萨摩亚未再观察DST,tzdata将 Pacific/Apia 定义为GMT+13,而不是+14,适用于2021年之后的时间点。
在不改变现有时区映射的前提下,在嵌入式Linux系统上强制固定的UTC偏移且不启用DST的正确做法是什么?
正确做法取决于为何这点重要。你可能遇到一个所谓的XY问题,问题并非出在系统范围设置,而在于你如何使用它。修改系统范围的设置有可能带来比解决当前问题更多的问题。因此,可能需要进行一次深入的设计评审(别在有缺陷的设计上打补丁)。如果你坚持这种做法,请继续阅读。
1) 避免系统时间出现DST调整的正确做法是使用专门为此目的定义的时区,即 Etc/GMT-14 到 Etc/GMT+12。其他时区不可靠,因为它们可能根据各国政府的行动增减DST。为了保持现有映射不变,你可以定义一个从内部枚举映射到无DST时区的新映射。(如果在这里使用的映射与其他组件中的映射不同导致问题,那么你可能有更深层次的问题需要解决。其他组件是否假设会有DST调整?它们在多大程度上依赖具体的时区名称?)
2) 或者,你可以放弃修改系统时区的计划。DST是 tz数据库有用的一个重要原因(跟踪历史变更是另一大原因)。既然你不想要DST,这个数据库对你几乎没有价值,因此你可以在内部实现时区变更。让系统保持在UTC/GMT,并把当前偏移量存储在某处。定义一个使用偏移量的API,例如一个获取当前UTC时间再加上偏移量的函数,并让各软件组件改用这个API而不是系统时间。根据你为何需要时区信息以及你是否使用了 [std::chrono],实现起来可能相对简单。