击败夏令时错误并获取正确的文件修改时间






4.96/5 (26投票s)
2001年5月29日
13分钟阅读

516535

1353
Windows 报告的文件修改时间存在错误,会根据夏令时(DST)进行变化。本文将描述这一现象的原因,以及如何确定正确的文件修改时间并避免 DST bug。
引言
并非许多 Windows 开发者都意识到,Microsoft 在设计 Windows NT 时故意使其报告错误的文件创建、修改和访问时间。此行为在知识库文章 Q128126 和 Q158588 中有记录。对于大多数用途,这种行为无伤大雅,但正如 Microsoft 在 Q158588 中所述:
在自动进行夏令时校正后,监控程序在将当前时间/日期戳与未通过直接获取/调整到世界协调时 (UTC) 的 Win32 API 调用写入的参考数据进行比较时,将错误地报告文件的时/日期更改。受此问题影响的程序可能包括版本控制软件、数据库同步软件、软件分发包、备份软件……
这种行为导致了 CVS 的各种支持列表收到大量关于“4 月第一个星期日”和“10 月最后一个星期日”之后的问题,许多人抱怨 CVS 现在错误地报告他们的文件已被修改。这通常被称为“红色文件 bug”,因为 WinCVS shell 使用红色图标来指示修改过的文件。
在过去的两年里,有几个人做出了共同的努力来修复这个 bug,并确定 NTFS 和 FAT 卷上文件的正确修改时间。事实证明,正确解决这个问题非常困难。我相信我已经解决了所有问题,并希望与任何关心此问题的人分享我的解决方案。
问题的例子
下面的批处理文件示例,在一个 NTFS 卷为 C: 且 A: 为 FAT 格式软盘的计算机上运行以下批处理文件。您需要对 C:\ 和 A:\ 有写入权限。此脚本将更改您的系统时间和日期,请准备好手动恢复它们。
REM Test_DST_Bug.bat
REM File Modification Time Test
Date /T
Time /T
Date 10/27/2001
Time 10:00 AM
Echo Foo > A:\Foo.txt
Time 10:30 AM
Echo Foo > C:\Bar.txt
dir a:\Foo.txt c:\Bar.txt
Date 10/28/2001
dir a:\Foo.txt c:\Bar.txt
REM Prompt the user to reset the date and time.
date
time
结果看起来像这样(为节省空间已删节)
C:\>Date 10/27/2001
C:\>dir a:\Foo.txt c:\Bar.txt
Directory of a:\
10/27/01 10:00a 6 foo.txt
Directory of c:\
10/27/01 <font color="red">10:30a</font> 6 Bar.txt
C:\>Date 10/28/2001
C:\>dir a:\Foo.txt c:\Bar.txt
Directory of a:\
10/27/01 10:00a 6 foo.txt
Directory of c:\
10/27/01 <font color="red">09:30a</font> 6 Bar.txt
在 10 月 27 日,Windows 正确地报告 Bar.txt 比 Foo.txt 修改晚了半小时,但第二天,Windows 改变了主意,认为实际上 Bar.txt 比 Foo.txt 修改早了半小时。一个天真的程序员可能会认为这是一个 bug,但正如 Microsoft 所强调的,这就是他们希望 Windows 的行为方式。
问题的解决方案
我花了很多时间思考这个问题,我想与想彻底解决它的人分享信息,但我认识到这里的大多数读者只想获取我的解决方案并使用它们。因此,我在这里提供使用该解决方案的说明。
我提供的库包含一个导出的函数:BOOL GetUTCFileModTime ( LPCTSTR name, time_t * utc_mod_time )
。只需将文件名(可以是完全限定路径或当前目录中的文件名)传递给此函数。函数将返回 TRUE
表示成功,FALSE
表示失败,并将 UTC 文件修改时间存储在 * utc_mod_time
中。与 JmgStat.lib 库链接即可开始运行。哦,对了。我将该库包装在名为 Jonathan_M_Gilligan_95724E90_4A88_11d5_80F3_006008C7B14D
的命名空间中。其他人可能有一个名为 GetUTCFileModTime()
的函数,我不想与之冲突,因此我将我的首字母缩写与 GUID 连接起来,生成一个唯一但可识别的命名空间。为了避免每次调用函数时输入如此长的 string
,您可能希望分配一个命名空间别名,namespace jmg = Jonathan_M_Gilligan_95724E90_4A88_11d5_80F3_006008C7B14D;
然后您可以调用 jmg::GetUTCFileModTime();
示例
namespace jmg = Jonathan_M_Gilligan_95724E90_4A88_11d5_80F3_006008C7B14D;
time_t mod_time_1, mod_time_2;
if ( jmg::GetUTCFileModTime( _T("foo.txt"), & mod_time_1 )
&& jmg::GetUTCFileModTime( _T("bar.txt"), & mod_time_2 ) )
{
if (mod_time_1 > mod_time_2)
{
_tprintf( _T("foo is older.\n") );
}
}
Windows 出现此问题的原因
此文件命名问题的根源在于 MS-DOS 和 PC-DOS 的早期。Unix 和其他为持续使用和网络通信设计的操作系统长期以来倾向于以 GMT(后来是 UTC)格式存储时间,以便不同时区的计算机能够准确确定不同事件的顺序。然而,当 Microsoft 将 DOS 适配到 IBM PC 时,个人计算机并未设想在广域网环境中使用,而在此环境中,将 PC 上的文件修改时间与另一时区的另一台计算机上的文件进行比较很重要。
为了有效利用计算机非常有限的资源,Microsoft 明智地决定不浪费比特或处理器周期去关心时区。将此决定置于背景中,请记住,前两代 PC 没有电池备份的实时时钟,因此您通常会在 AUTOEXEC.BAT 文件中放置 DATE
和 TIME
命令,以便在计算机启动时提示您手动输入日期和时间。
关于时间测量系统的杂谈...
到了 WinNT 时代,广域网已变得足够普遍,以至于 Microsoft 意识到操作系统应该以某种通用格式测量时间,这将允许不同的计算机比较事件的顺序(和间隔),而不管它们各自的时区。尽管细节有所不同(不同的时间结构相对于不同的事件测量时间),但其净效应是 Win32 内部使用的所有时间都相对于 UTC(曾经称为 GMT)进行测量。
我曾在美国国家标准与技术研究院(位于博尔德)的原子钟阵列旁边工作过,我感到有义务就时间和报告时间的时间系统添加几句话。很久以前,我们用 GMT(格林威治标准时间)来指代时间,格林威治标准时间由英国皇家天文台维持,并最终参考了天文台测量的太阳位置。当原子钟成为计时标准时,出现了一个新标准,称为 UTC。UTC 是一个胡说八道的缩写。在英语中,它代表“Coordinated Universal Time”,而在法语中,它代表“le temps universel coordonné”。而不是使用 CUT 或 TUC,而是采用了无意义的折衷缩写 UTC。
要理解 UTC,我们必须首先理解更抽象的国际原子时 (TAI, le temps atomique international),它测量自大约 1958 年 1 月 1 日以来经过的秒数,由铯原子钟测量。秒被定义为铯超精细频率 9 192 631 770 个周所需要的时间。然而,无论是日还是年都不是这个数字的整数倍,所以我们取 TAI 并对其进行校正,使其与地球的实际运动相对应,方法是添加“闰秒”等校正。TAI 测量原始原子时间。UTC 测量与地球运动同步的时间(即,这样我们就不会在日出时出现午夜,或在盛夏出现一月)。UTC 真正含义的详细信息,以及更详细的计时历史,可以在这里找到。
UTC、时区和 Windows 文件时间
那么,这一切与 Windows 计算机上的文件修改时间有什么关系呢?Windows 在兼容地集成 FAT 和 NTFS 文件方面存在一些严重的问题。FAT 以本地时区为参考记录文件修改时间,而 NTFS 则以 UTC 记录文件修改时间(以及创建和访问时间,FAT 不记录)。您可能想问的第一个问题是:“Windows 应该如何报告这些文件时间?”显然,让 dir
和 Windows 资源管理器在本地时区报告 FAT 文件时间,而在 UTC 报告 NTFS 文件时间,这很愚蠢。如果使用不一致的格式,用户将很难确定两个文件中的哪个文件最近。因此,我们必须选择在向用户报告时转换两种文件时间格式之一。大多数用户可能希望以其本地时区了解文件修改时间。这与人们在 DOS 和 Win16 下的预期保持一致。对大多数用户来说也更有用,他们可能想知道他们修改文件有多久了,而无需查找本地时区与 UTC 的偏移量。
将 UTC 转换为本地时间很简单。查找本地时区与 UTC 之间的分钟偏移量,确定是否处于夏令时,然后将标准偏移量或夏令时偏移量添加到 UTC 时间。然而,如果我们尝试向后转换,夏令时会带来微妙的麻烦……
夏令时的问题
如果您想将本地时区的时间转换为 UTC,这似乎是一项直接的任务,只需确定本地是否处于夏令时,然后从本地时间中减去标准偏移量或夏令时偏移量即可得出 UTC。由于 UTC 到本地时间的映射不是一对一的,因此会出现一个微妙的问题。具体来说,当我们结束夏令时并将时钟拨回时,有两个不同的小时间隔的 UTC 时间映射到同一个小时间隔的本地时间。以 2001 年 10 月最后一个星期日凌晨 1:30 的具体情况为例。假设本地时区是美国中部时间(标准时间时偏移 UTC -6 小时,夏令时时偏移 UTC -5 小时)。在 10 月 28 日星期日 06:00 UTC,美国中部时区的当地时间将是 01:00(凌晨 1:00),并且处于夏令时。在 06:30 UTC,当地时间是 01:30。在 07:00 UTC,当地时间将是 01:00:00,并且不再处于夏令时。在 07:30 UTC,当地时间是 01:30。因此,对于所有本地时间 01:00 ≤ t < 02:00,都有两个不同的 UTC 时间对应于给定的本地时间。这种退化的映射意味着我们无法确定哪个 UTC 时间对应于凌晨 1:30 的本地时间。如果一个 FAT 文件被标记为在 2001 年 10 月 28 日凌晨 1:30 修改,我们无法确定 UTC 时间。
在转换本地文件时间到 UTC 和反之亦然时,Microsoft 做出了一个奇怪的决定。我们希望以下代码产生 out_time
等于 in_time
。
FILETIME in_time, local_time, out_time;
// assign in_time, then do this...
FileTimeToLocalFileTime(& in_time, & local_time);
LocalFileTimeToFileTime(& local_time, & out_time);
问题是,如果本地时区是美国中部(标准时间 UTC -6 小时,夏令时 UTC -5 小时),则 in_time
= 2001 年 10 月 28 日 06:30:00 和 in_time
= 2001 年 10 月 28 日 07:30:00 都映射到相同的本地时间,即 2001 年 10 月 28 日 01:30:00,我们不知道在执行 LocalFileTimeToFileTime()
时应选择哪条路径。Microsoft 选择了一个不正确但明确可逆的算法:当本地计算机处于夏令时状态时,将所有时间向上移动一小时,而不管转换时间的 DST 状态如何。因此,如果我的本地计算机处于 DST 状态,FileTimeToLocalFileTime
将 2001 年 10 月 28 日 06:30:00 UTC 转换为 01:30:00 CDT,将 2001 年 10 月 28 日 07:30:00 UTC 转换为 02:30:00 CDT。如果我使用相同的参数调用同一个函数,但当我的本地计算机不处于 DST 状态时,FileTimeToLocalFileTime
将 06:30:00 UTC 转换为 00:30:00 CDT,将 07:30:00 UTC 转换为 01:30:00 CDT。
这似乎很奇怪,为什么这会影响 C 库函数 stat
,它声称返回文件的 UTC 修改时间。如果您检查 Microsoft 的 C 库源代码,您会发现它是这样获取修改时间的:
// pseudocode listing
WIN32_FIND_DATA find_buf;
HANDLE hFile;
FILETIME local_ft;
time_t mod_time;
// FindFirstFile returns times in UTC.
//
// For NTFS files, it just returns the modification time
// stored on the disk.
//
// For FAT files, it converts the modification time from
// local (which is stored on the disk) to UTC using
// LocalFileTimeToFileTime()
//
hFile = FindFirstFile ( file_name, &find_buf );
// convert UTC mod time to local...
FileTimeToLocalFileTime ( &find_buf.ftLastWriteTime, &local_ft );
// Now use a private, undocumented function to convert local time to UTC
// time according to the DST settings appropriate to the time being
// converted!
mod_time = __secret_microsoft_converter(local_ft);
对于 FAT 文件,转换如下进行:
- 原始文件修改时间通过
LocalFileTimeToFileTime()
转换为 UTC - UTC 通过
FileTimeToLocalFileTime()
转换回本地时间。请注意,这完全逆转了步骤 1 的效果,因此我们得到了正确的本地修改时间。 - 本地时间通过私有函数转换为“正确”的 UTC
对于 NTFS 文件,转换如下进行:
- 原始文件修改时间已为 UTC,因此我们无需转换。
- UTC 通过
FileTimeToLocalFileTime()
转换为本地时间。此操作根据计算机系统时间的 DST 设置应用 DST 校正,而与文件修改时间的 DST 设置无关。 - 本地时间通过私有函数转换为“正确”的 UTC。请注意,这不会逆转步骤 2 的效果,因为在步骤 3 中,我们使用的是文件修改时间的 DST 设置,而不是系统时间的 DST 设置。
这解释了我在这篇文章开头展示的问题:NTFS 卷上文件的 dir
命令报告的时间会随着我们进入或离开夏令时而改变一个小时,尽管我没有触碰过文件。FAT 修改时间在 DST 期间保持稳定。
问题分类
我认为有 3 种可能的情况,在这种情况下,文件时间报告不一致可能会引起问题:
- 您可能正在将 NTFS 卷上的文件与存储在文件(或内存)中的
time_t
值进行比较。这在 CVS 中很常见,并在 4 月第一个星期日和 10 月最后一个星期日导致臭名昭著的“红色文件”问题。 - 您可能正在将 FAT 卷上的文件与
time_t
值进行比较。 - 您可能正在将 FAT 卷上的文件与 NTFS 卷上的文件进行比较。
解决方案
- 对于情况 (1),很简单。使用 Windows API 调用
GetFileTime()
而不是 C 库stat()
来获取文件时间,并通过减去原点(1600 年 1 月 1 日)并除以 10,000,000 将 100 纳秒单位转换为秒,将FILETIME
转换为time_t
。 -
对于情况 (2),
stat()
会起作用并返回一个time_t
值,您可以将其与存储的值进行比较。如果您必须使用GetFileTime()
,请不要使用LocalFileTimeToFileTime()
。此函数将应用当前系统时间的夏令时状态,而不是参数中文件时间的夏令时状态。幸运的是,C 库mktime()
函数将正确地转换时间如果您正确设置了tm
结构中的tm_isdst
字段。这里存在一个鸡生蛋还是蛋生鸡的问题。Windows 没有提供一个好的 API 调用来让您确定在特定时间是否启用了 DST。幸运的是,对于美国和其他使用相同逻辑(夏令时在 4 月第一个星期日的凌晨 2:00 开始,在 10 月最后一个星期日的凌晨 2:00 结束)的国家/地区的居民来说,您可以将
tm_isdst
设置为负数,mktime()
将自动确定是否应用夏令时。如果文件是在 10 月最后一个星期日的凌晨 1:00-2:00 之间修改的,mktime()
如何计算修改时间是模糊的。不遵循美国常规夏令时规则的时区的用户必须通过检索适用的
TIMEZONEINFO
结构(使用GetTimeZoneInformation
)并手动计算是否适用夏令时来强制解决夏令时问题。 - 对于情况 (3),最好的方法是遵循上述情况 2 的说明,并将生成的 UTC
time_t
与情况 (1) 中确定的 NTFS 文件时间进行比较。
本文开头下载链接提供的库实现了此解决方案,并检查了文件存储的文件系统。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。