fenlan

Everything gonna be fine in the end, if it's not fine, it's not the end.

0%

Unix 时间问题(非原创)

故事缘由

有时候不得不相信,有些东西就是刚刚好,刚刚好到来,刚刚好知道,刚刚好契合。因为这些刚刚好,就会不由自主的激动,感觉这些刚知道的东西发生在自己身上。这次要说的是unix时间问题,我最开始接触这个问题是我们云计算课程项目中一个小小的时间问题,当时项目中所有的时间都是通过unix时间记录。所谓unix时间,就是记录从unix元年1970年1月1日0点起到当前时间经过的秒数。这个问题最开始我并没有觉得有什么问题,但后来我在网上瞎逛的时候看见了一篇文章 十大著名的计算机发展史上的软件缺陷 ,这里就讲到了一个unix时间问题,我就很好奇的去了解这一个故事,而在这个时间点,我们正在进行的操作系统课程也讲到了unix时间,所以这一堆堆事情,就刚刚好地发生了。

2038问题

2038年问题的成因与20世纪90年代曾经名噪一时的千年虫问题很类似。千年虫问题是因为早期(早在20世纪50年前以前)程序员们使用两位十进制数字来表示年份(为了节约存储空间和书写程序的便利),并且在很长一段时间内,没有意识到这么做可能给后人带来的麻烦;当所有人都习惯这么做时,随着软件系统变得越来越复杂,小问题就逐渐变成大问题了。

说起Unix上的2038年问题,时间要追溯到1969年。当Ken Thompson和Dennis Ritchie在贝尔实验室里捣鼓他们的新玩意时,他们肯定没有想到他们的小发明将来会在全世界发挥如此大的影响力。他们决定把他们所创造的这个支持多用户多任务的新操作系统命名为UNIX,以一个32位二进制数所能表示的有符号整数范围(-2,147,483,648 ~ +2,147,483,647)所代表的秒数作为Unix纪元(Unix epoch)时间,把西元1970年1月1日0时0分0秒定为Unix纪元的元年。

在Unix历的约第100,000,000秒(或者,用人类的纪年法,大约在西元1972~1973年之间),一种叫做C的高级程序语言被发明出来了。于是,贝尔实验室的这帮人用C语言重写了他们的UNIX系统内核,很自然地,系统库中用于存储时间的time_t类型的typedef定义,就采用了C语言中对应的32位有符号整型(signed int32)来实现。

起初,一切都很好。UNIX走出了贝尔实验室中少数研究人员的小圈子,得到了工业界的青睐,好几家商业公司都发行了自己的Unix变体,其中包括最早的商业版UNIX System V以及后来的IBM AIX、HP-UX、SGI的IRIX和微软的Xenix,它们被广泛用在各式大中小型主机和服务器上。到了80年代初,当时在麻省理工大学人工智能实验室工作的一个大胡子黑客不满于Unix这种专有软件的闭源开发模式,他决定重写一个完全自由的仿Unix操作系统,但又不是Unix,这就是后来的GNU (GNU is Not Unix) Project;与此同时,在西海岸,UC Berkeley的另一群黑客获得了一部分来自最初贝尔实验室的Unix原始代码,他们在此基础上开发出属于自己的一套“伯克利软件发行包(Berkeley Software Distribution)”,这演变成了后来的BSD家族。很快,时间已经到了Unix纪元的第31年,在大洋彼岸的北欧国家芬兰,一个年轻大学生自己写了一个仿Unix内核,受到当时GNU发起的自由软件运动的影响,他决定把这个内核以GPL协议发放出来,让全世界的黑客们来共同使用、参与开发。恰好那时GNU操作系统还没有自己的成熟内核,这个后来被命名为“Linux”的仿Unix内核借着与GNU Project的结合获得了广泛的成功与关注。最终,就有了我们今天所使用的GNU/Linux。在很长一段时间里,不管是真正的Unix和由它直接衍生而来的BSD、Solaris,还是试图“仿造出”Unix的GNU/Linux,抑或是为了统一各种类Unix(*nix)系统而生的POSIX标准,所有的类Unix系统都把time_t类型理所当然地等价为C语言中的原生数据类型signed int32,无数的库和工具被基于它写出来,很多网络上的服务与应用也是基于这个事实的标准。

终于有一天,人们意识到,32 bits整数所能表示的数目大小毕竟是有限的,更何况这个数字是以秒作为单位。对于正在各行各业发挥着越来越重要作用的类Unix系统来说,当初的设定显然也太目光短浅了些。

32位有符号整型所能表示的最大数是+2,147,483,647。2147483647秒 = 24855天 = 68年。也就是说,自Unix纪元的元年(1970年)起,再过68年,所有现存的32位类Unix系统都将迎来历法上的终结——对+2,147,483,647加1将造成一个算术溢出:在大部分系统上,返回值会变成-2,147,483,648,在少数系统上,返回值可能是0。许多依赖于系统时间的程序将无法正常工作,计算机的时钟也将退回到1901年(或者1970年)。它们将无法正确处理那之后的时间。

这个准确的时刻是:协调世界时2038年1月19日3时14分7秒(未考虑闰秒)。你可以在维基百科上找到更多的信息。

更多的“xx”年问题

一个与Unix的2038年问题相类似的是NTP协议的2036年问题。NTP协议的时间戳采用了和Unix相似的32位整数表示,不同于Unix的1970 ± 68年范围,NTP使用的是无符号整型,并且以1900作为时间的起点,这意味着它的终点将是1900 + 136 = 2036年2月6日。

和2038年问题直接相关的两个历史事件是2001年9月9日问题和2004年日本银行ATM机故障:
2001年9月9日问题又被称作S1G(Second 1 Giga,一吉秒)或者S1B(Second 1 Billion)问题。在那一天,Unix纪元时间迎来了第1,000,000,000秒,由于某些软件中使用了字符串来存储时间戳,当字符串变成”1000000000”时,字典排序的结果会产生”999999999” > “1000000000”,这造成了相当一部分程序不能正常判断时间差,影响到正常工作。(我一直以为只有不上路子的2B程序员才会这么去做判断,没想到还真不少,包括KDE在内。。。)

2004年1月10日恰好是1970年到2038年时间轴上的中点——显然,如果你在程序中出于某种目的将Unix时间乘以了2,那么它将不能正常工作。这在日本某些银行使用了IBM软件的ATM机上确实发生了。(至于究竟为什么要把时间乘以2,我只能表示不明觉厉。)

可以肯定地说,全世界的程序员都是一群爱偷懒的货。

轻松一刻

在2001年S1B问题发生之前的4月19日,恰好迎来了Unix纪元的第987,654,321秒。虽然没什么实质意义(估计没有哪个程序会无聊到去计算时间戳中不同数字的个数),但这还是在Slashdot上激起了不少讨论。

2009年2月13日11:31:30,是Unix纪元的第1,234,567,890秒。这天刚好是星期五,又是一个13号,也就是西方文化中所谓的黑色星期五。世界各地不少Unix社区都在举办活动庆祝此事,包括Google的hackers们。他们甚至还做了一个Doodle:

从32位到64位

解决2038年问题的办法看似很简单:因为time_t类型本来就是依赖于C POSIX库的具体实现的,并没有哪个规范规定它必须是32位,直接把它改成64位不就行了吗?

问题是,直接这么改,会破坏很多现有程序(工具、服务……)的兼容性。几乎所有的32位系统都自然而然地采用了32位的time_t,这其中包括了现在绝大多数基于ARM处理器的电子产品(只要它们的底层系统是基于类Unix的——这包括iPad、iPhone、所有Android手机、PS Vita还有Raspberry Pi等等)

当然你大可不必担心你的手机会遭遇2038年问题,因为你现在的手机大概不会一直用到2038年(也许再过几个月你就会淘汰掉旧的去换新的了!)。剩下来的,就是桌面和服务器所要面临的问题了。

所幸的是,摆脱了嵌入式设备的局限性,我们在桌面和服务器上已经有了众多的64位体系架构可供选择。而且,目前绝大多数的64位操作系统,也都自然而然地采用了64位time_t类型(相当于long long int或者int64)。

问题解决了。只要我们可以预期现有的计算机都能在2038年之前迁移到64位系统(准确地说,是采用了64位time_t的系统),Unix纪元就不会迎来末日。到了那一天,所有的计算机系统仍然能正常工作,人类文明完好如初。

传统的32位Unix纪元时间会在2038年发生算术溢出,然后迎接末日。64位纪元当然也是会有这一天的,那将是在

15:30:08 UTC on Sun, 4 December 292,277,026,596

这个时间已经远远超过了预计太阳扩张成红巨星并吞噬地球的时间。所以,在此之前,已经没有什么好担心的了。我确信,要么人类文明要么根本都熬不到那一天,要么等到那一天,我们早就达到了拥有任意操纵时间和空间的能力的技术奇点,人类文明将获得永生(跑题了。。。);要么就是,所有的电脑都已经迁移到了128位。(这当然是最简单的解决办法)

你的系统如何?

想知道自己的Unix系统是否会在2038年1月19日这一天迎来末日,只要看系统能否正确显示Unix纪元第2,147,483,648秒的日期即可。

在GNU/Linux上,执行:(GNU date)

1
$ date -ud @2147483648

如果系统使用了64位时间,结果应该是正常的:(后文中假定时区一律设为export TZ=”UTC”)

1
2038年 01月 19日 星期二 03:14:08 UTC

那么恭喜,你的系统可以平安无恙地度过2038年末日。如果出现1901年或者其他神马奇怪结果的话,自己看着办吧。

另:Unix是否会发生2038年问题并不绝对取决于是32位系统还是64位系统。这由具体系统中对time_t类型的实现决定。NetBSD和OpenBSD的早期版本在amd64平台上仍然使用了32位time_t,因此仍然会发生2038年问题。如今也有一些32位系统开始使用64位的time_t类型(典型的例子:今年10月份发布的NetBSD 6.0,在32位和64位平台上一律改成了64位time_t)。64位Linux上已经在使用64位的time_t了,而32位Linux上则似乎仍然是32位。

那么我遇到了什么问题

在编写云计算项目程序的时候,我将unix时间转换为javaDate类型,但是我忘了java的时间默认是以ms为单位,所有我转换unix时间的时候原本单位是s,结果java把它当作ms,这样的结果就是原本时间应该是2016年6月1日的时间,变成了1970年1月的某一天。因此,在转换的时候要记得在unix时间上乘以1000

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void contextLoads() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Long time = Long.parseLong("1496246559")*1000;
String d = format.format(time);
try {
Date date = format.parse(d);
System.out.println(date);
} catch (ParseException e) {
e.printStackTrace();
}
}

参考资料/相关链接