聊聊Linux2038年问题

从Unix创世纪说起

创世纪一词来自于希伯来语:בראשית‎,意为“在开始之时”。每种文化都有它的创世纪一说,比如《创世纪》便是《圣纪》的第一卷,讲述了神创造,撒但败坏,人堕落,耶和华应许拯救的思想和故事。
对于Unix或类Unix系统,它关心时间从哪里开始,这便是它的创世纪。关于Unix和C语言创立背后的故事,我们在这里不重点介绍。
Unix操作系统的创世纪可以从中文Wikipedia(或英文wikipedia)中找到:

UNIX,一种计算机操作系统,具有多任务、多用户的特征。于1969年,在美国AT&T公司的贝尔实验室开发出来,参与开发的人有肯·汤普逊、丹尼斯·里奇等。
目前它的商标权由国际开放标准组织所拥有,只有匹配单一UNIX规范的UNIX系统才能使用UNIX这个名称,否则只能称为类UNIX(UNIX-like)。

Wikipedia中的图片更直观说明了整个Unix的发展和繁衍

由于Unix是从1970年开始广泛应用于商业和学术界,1970年被定义为Unix或类Unix系统的元纪。所有系统都以1970年1月1日 0点0分0秒作为时间的基准点,用秒数来表示系统时间,也即当前系统时间是从基准时间(1970年1月1日 0点0分0秒)走过多少秒之后的时间。
用简单公式来表即: 系统时间 = 基准时间 + 秒数

那么这个秒数应该保存在多宽的类型中呢?当时那个年代,16位字宽已是很大了。认为32位已经是“足够大”了,因此在POSIX标准中,将表示秒数的类型定义为time_t,而它是32位有符号整数类型。

下面是秒数与绝对时间的对照表:

time_t类型的秒数值 系统时间(绝对时间)
0 1970-01-01 00:00:00
1 1970-01-01 00:00:01
0x7ffffffff 2038-01-19 03:14:07

在32位系统上,time_t能表示的最大值为0x7ffffffff,当time_t取最大值时表示系统时间为2038-01-19 03:14:07,但时间再往后走时,那time_t会溢出变成一个负值,此时系统时间会倒流回到1901年,届时操作系统和上层软件都会运行错出。

下图同样来自于Wikipedia,它展示32系统time_t溢出前后系统时间的倒流:

如果时间将近2038年时,还存在32位机器在世界中运行,那将会受到2038年问题的影响。

2038年问题冲击波

当前世界时钟走到了2016年,离2038年还有21年有多,估计很多人会持乐观态度。也许到2038年,32位的机器早已不存在了,2038年问题自动消失。然而,世界没有这么美好。

操作系统运行时影响

对于服务器来说,早早就换到了64系统操作系统,2038年问题不复存在。而对于嵌入式设备来说,现在还有大量32位系统在全球各地运行,谁也无法保证这些系统在2038年之前就能光荣退役。

另外对于64位操作系统,上面还会运行着32位的应用程序,它的2038年问题一样对人们造成威胁,不可小视。

所以32位的time_t问题,必须要解决,无法自动消失。

持久化数据

一般听到2038年问题,想的最多的是time_t类型问题。事实上,2038年问题的范围远不止于此。
前面谈到的操作系统time_t类型问题是系统运行时表示数据的溢出,但还有一些数据是静静在躺在某个磁盘上,当时间走到2038之后再把它它们翻读出来,一样会出现问题。

我们知道类Unix下文件都有几种时间属性,比如创建时间,最后一次访问时间,最后一次修改时间。如果该时间类型也是32位有符号数(也即time_t的等价类型),那在2038之后的某个早晨,试想一下你和朋友喝着咖啡,回忆起2038年以前的某次旅游,你兴高采烈说着之前见闻,并拿出手提电脑打开之前拍下的照片,这时扫兴的事情将会发生,文件打不出或者出错。

下表是Linux社区对Linux下所有支持文件系统的时间类型和溢出时间作了分析和对比,为了不影响阅读,只取出重要的文件系统:

file system time type expiration year
btrfs signed 64-bit seconds, 32-bit ns never
cramfs fixed 1970
ext2 signed 32-bit seconds 2038
ext3 signed 32-bit seconds 2038
ext4 (good old inodes) signed 32-bit seconds 2038
ext4 (new inodes 34 bit seconds / 30-bit ns (but broken) 2038
fat 7-bit years since 1980, 2s resolution 2107
fuse 64-bit second/32-bit ns never
gfs2 u64 seconds/u32 ns never
jffs2 unsigned 32-bit seconds 2106
logfs signed 64-bit ns 2262
nfsv2,v3 unsigned 32-bit seconds/ns 2106
nfsv4 u64 seconds/u32 ns never
nfsd unsigned 32-bit seconds/ns 2106
ntfs 64-bit 100ns since 1601 30828
pstore ascii seconds 2106
squashfs unsigned 32-bit seconds 2106
sysv unsigned 32-bit seconds 2106
xfs signed 32-bit seconds/ns 2106

从上述表格中看到依然少数几个文件系统受2038年问题的影响。文件系统的时间类型和单位是由文件系统自己定义的,可以与系统的time_t以及基准时间不相同。如果一个文系统选定义的单位为秒数,并且使用32位有符号整数表示时间,那么尽管该文件系统运行在64位的操作系统之下,依然会有2038年问题。

Linux最广泛使用的文件系统要数ext系统(ext2/ext3/ext4),上表显示它们也存在2038年问题题。但我在查看系统代码时,发现表示时间的类型为32位无符号整数,所以它们的溢出时间应该是2016年。

无论如何,2038年问题要想彻底解决,文件系统脱不了干系。

协议交互

除于系统运行的数据表示,以及持久化数据,还有一类是需要关心的,那就是机器与机器之间通信约定是否有2038年问题,如果有那将会造成灾难。

前一段时间对部分开源软件代码做time_t搜索,没有发现协议相关的代码使用time_t作为协议类型,在google上搜索2038年问题也没有找到跟协议相关的说明。

从目前分析来看,协议交互不涉及2038年问题。

涅槃重生还是曲线解国

2038年问题的根源就是使用了32位有符号整数来表示时间,看起来它的解决方案非常的简单,直接粗暴地将time_t从32位有符号整数 修改成 64位有符号整数。

如果真的这样做,那对这个世界会产生什么影响呢? 在修复2038年问题那一天,估计全世界人已都在做同一件事情:

所有应用程序统统重新编写代码,至少得重新编译才能在新系统上运行
所有受影2038年影响的文件系统对应的分区,得统统格式化掉
在那天有的互联网服务都统统下线了,整个应用网络处于瘫痪状态
更离奇的是,你在银行的存款被清零了;对于那个贷款的家伙来说是个好事情,因为他们不用向银行还钱了

所以,解决方案不是这么简单的,无法创建一个新世界,直接抛弃旧世界。

Linux社区在讨论2038年问题时谈到OpenBSD的解决方案还真是这样干, 它直接将time_t类型从32位修改成64位。OpenBSD之所以能这么干,是因为整个系统是自包含的,内核和应用程序是一起编译的,不存在软件供应商发行二进制场景,所以没有二进制兼容性问题。

而Linux却无法这样做。一旦将time_t从32位修改成64位之后,在此之前发布的所有应用程序几乎不能在新系统上运行。对于文件系统来说,一旦存储格式上做修改(不是扩展),那磁盘上的老数据必须要统统格掉(格式化)才能在新文件系统上运行。

所以Linux的解决方案必须是要解决兼容性问题

对于ttime_t类型的改造,既要支持旧的应用程序可以在新系统上运行,也要支持新开发(或者新编译的)应用程 序能解决2038年问题,那意味着要保留老(32位time_t)的二进制应用程序接口(ABI),同时要新增一套64位time_t的ABI接口。

很明显Linux的解决方案如下图所示:

原来整个系统中原来32位time_t的所有系统调用都保留下来。新增一个time64_t类型,与之相关的系统调用都提供一套新的64位系统调用ABI。

老用户态程序由于在之前已经编译老了,在新系统运行时,它执行的系统调用依然还是原来老的32位系统调用。 而新编译的应用程序,尽管在源代码层面上看到还是time_t类型,但是在新系统上,它已变成64位了,编译出来的二进制程,它实际上调用的函数者是64位的系统调用。

其实在Linux时间相关的函数中,不单单只有time_t一个类型,还有struct timeval等等。所有这些类型,都需要提供64位的定义。

时间相关的系统调用其实也一大堆,比如直接为时间操作函数(gettimeofday/settimeofday/adjtimex/clock_gettime/clock_settime/clock_adjtime/clock_nanosleep),也有文件操作相关的函数也带上时间属性(stat/lstat/fstatat),同样还有些大杂烩函数诸如ioctl很多命令字出现时间相关的参数,都需要实现64位的版本。

当前Linux社区进展

正如前面所说,当前离2038时间还有20+年,Linux社区最近几年才慢慢开始着手解决2038年问题。

linux kernel newbies专项门有个2038年项目来跟踪此问题。整个项目有分成以下几部分:

  • 用户态glibc对64位新型time_t以及相应函数的支持
  • 内核态对64位新型time_t以及系统调用实现的支持
  • 文件系统解决2038年问题

上面3个部分之中,第2点是重为重要的,也是整个解决方案的基石所在。当前 Arnd Bergmann 正着手解决提出第2部分,可以从他的git看到他的解决方案。至文件系统部分,一直都有人有解决,但解决时间还有待进一步了解。

glibc部分支持必须要等内核的解决方案经过评审完成并合入到内核主线之后,glibc社区才能根据最终敲定的ABI来实现64位time_t和相关函数。

总结

2038年问题与之前的千年虫问题的杀伤力是不一样的,千年虫属于应用程序的问题,而2038年问题却是系统级的,有更大的杀伤力。幸好当前离2038还有20年时间,并且整个Linux社区已经开始解决的,离目标不远了,曙光在望。