Unix时间戳揭秘:为什么计算机时间从1970-01-01开始?

发布时间:2026/9/18 4:46:56
Unix时间戳揭秘:为什么计算机时间从1970-01-01开始? 1. 系统时间突然回到1970-01-01说明机器被“清零”了先说个我实际遇到的场景。前几年有个同事抱着一台笔记本找我说系统时间变成了1970年1月1日后台日志也全乱了他怀疑是中了顽固病毒。我当时第一反应是先看一眼时间戳别急着查毒。进BIOS一看主板RTC时间其实还在走但Windows里显示的日期就是1970-01-01这类现象十有八九不是病毒而是系统没有拿到合法时间直接把Unix时间戳的“零点”当成当前时间用了。计算机里几乎所有主流操作系统、编程语言、数据库内部都不是直接存“2025年某月某日”而是存一个数字——从1970年1月1日0点0分0秒UTC开始流逝的秒数。这个数字就是经常听到的“时间戳”或者“timestamp”。1970-01-01就是整个时间体系的坐标原点也就是Unix时间戳的epoch纪元。epoch这个单词听着玄乎说白了就是“第0秒”的位置。1.1 最常见的两类触发原因系统时间突然变成1970-01-01通常是这两种情况第一种CMOS电池没电或主板RTC实时时钟初始化失败。电脑开机时如果读不到有效的RTC时间BIOS会返回一个无效值操作系统接管后可能就把时间初始化为Unix时间戳0也就是1970-01-01。这个时候进入系统你看到的时间很可能不是“1970-01-01 00:00:00”而是“1970-01-01 08:00:00”——因为东八区比UTC早8个小时。这其实是同一时刻的不同显示不是系统又算错了。第二种某些程序解析时间戳时遇到非法值、字段缺失、32位溢出库函数会把结果归零。比如很多语言里new Date(0)、date -d 0出来的结果就是1970-01-01。在数据库里非法时间戳也可能被转成0000-00-00或者直接报警原因也差不多。1.2 为什么偏偏是1970年不是1900年也不是2000年很多人会追问既然要选一个“起点”凭什么不选个更整齐的年份比如1900年或者2000年这就要说到历史了。NTP网络时间协议确实用了1900年作为起点Windows的FILETIME用的甚至是1601年Excel也有自己的一套“日期序列号”。但今天绝大多数开发者和运维人员打交道的时间戳继承的是Unix那一套体系起点就是1970年1月1日。至于2000年用来当未来时间可以但没法表示1969年而Unix在1969年就已经开始原型开发了选一个“现在”当起点对当时的人来说最自然。所以1970-01-01不是一个物理规律是一个人为约定。只是这个约定太成功了以至于成了全世界的默认“公元纪年”。2. 1970年1月1日这个“起点”是Unix定下的规矩要搞清楚为什么是1970年得回到上世纪六七十年代看Unix是怎么来的。1969年前后贝尔实验室的Ken Thompson和Dennis Ritchie在PDP-7、PDP-11这类小型机上捣鼓出了一个多用户操作系统也就是Unix的原型。系统里总要记录文件创建时间、进程启动时间、日志时间吧当时很多老系统是把“年、月、日、时、分、秒”拆成好几个字段存的比较两个时间谁更晚得逐字段比大小非常痛苦。Unix的思路很直接用“从某个固定参考点到现在经过的秒数”来表示时间。这样时间就变成了一个单调递增的整数比较、加减、排序全变成普通整数运算又快又不容易出错。这个参考点Unix最终定在了1970年1月1日 00:00:00 UTC。2.1 从Unix操作系统的诞生说起为什么偏偏落在1970年而不是1968年或者1971年最直接的原因是Unix的第一版正式文档里time系统调用已经规定“返回从1970年1月1日00:00:00开始经过的秒数”。1970年1月1日正好在Unix诞生初期离开发者“现在”足够近取整秒也方便。后来POSIX标准化时把这个约定固化了下来就成了今天C语言里time_t的基础。我们写C语言的时候只要#include time.h调time(NULL)拿到的就是当前时刻的Unix时间戳调ctime、gmtime、localtime就能把它还原成人类可读的日期。这套API已经沿用了半个世纪几乎所有语言里都有对应版本。所以与其说“计算机起始时间是1970年1月1日”不如说“Unix时间戳的参考点是1970年1月1日”后者更准确。2.2 time_t是怎么记录时间的time_t本质上是一个整数。早期Unix用32位有符号整数最大值是2147483647对应2038年1月19日03:14:07 UTC。32位能覆盖1970年到2038年当时看来绰绰有余。后来64位系统普及time_t在64位下可以表示到约2920亿年之后基本等于“用不完”。但有个容易被忽略的点Unix时间戳只是“从1970-01-01 00:00:00 UTC到现在的秒数”它本身不包含时区信息。无论你在北京、纽约还是伦敦同一个瞬间的Unix时间戳数字都是一样的。只有当你把它转换成本地日期字符串时才会加上东八区或者西五区的偏移。理解这一点后面很多“差8小时”的坑就都能想明白了。2.3 其他系统的时间起点各玩各的知道Unix是1970年还不够。Windows的FILETIME是从1601年1月1日开始计数的单位是100纳秒Excel的日期序列号是从1899年12月30日开始的NTP协议的时间戳从1900年1月1日开始macOS的一些底层时间基准也有自己的历史。所以不管做开发还是排查问题拿到一个“时间戳”第一件事永远是确认它用的是哪个epoch、什么单位。单位错了可能差几十年epoch错了可能差几百年。这也是为什么我在实际工作中从不“猜时间戳”先问系统、问协议、问库再动手转换。3. 时间戳的计算逻辑以及三个常年踩坑的细节Unix时间戳的计算公式很简单当前时间戳 从1970-01-01 00:00:00 UTC到当前时刻经过的UTC秒数。注意是“UTC秒数”和本地时区无关。比如2024年1月1日0点0分0秒UTC对应的时间戳是1704067200。你可以自己验算从1970年到2024年中间有54个年头其中包含13个闰年总天数就是54×3651319723天再乘86400秒就是1704067200。3.1 从UTC到秒数手动换算是这样做的实际开发中很少需要手动算秒数但你需要能看懂那串数字。在Linux或者macOS终端里随手就能验证# 看当前时间戳 date %s # 把时间戳转成UTC时间 date -u -d 1704067200 # 把时间戳转成本地时间 date -d 1704067200在Windows上用PowerShell也有对应的命令或者直接用Pythonimport datetime # 秒时间戳转UTC时间 print(datetime.datetime.fromtimestamp(1704067200, tzdatetime.timezone.utc)) # 当前时间戳秒 print(int(datetime.datetime.now(tzdatetime.timezone.utc).timestamp()))我自己排查问题时最常用的就是Python这一行又快又准而且能明确指定时区不会被系统的默认时区带偏。3.2 时区不参与存储但参与展示很多新人第一次被坑就是发现“明明存的是0读出来却变成了1970-01-01 08:00:00”。原因不复杂时间戳0对应的UTC时间是1970-01-01 00:00:00但你在东八区系统按本地时区显示就成了早上8点。这不是bug是展示层的时区设置问题。反过来如果你开发一个全球用户都能用的系统最稳妥的做法是所有服务端统一存UTC也就是存时间戳只有到前端展示时才根据用户时区转成本地时间。千万不要“东八区存一次西五区再存一次”那基本等于自埋地雷。我在代码评审里看到过太多次因为TimeZone.getDefault()隐式参与转换导致凌晨之后的数据莫名其妙少8小时的情况。3.3 秒、毫秒、微秒单位错一位就是几十年时间戳的单位是另一个重灾区。Java的System.currentTimeMillis()返回毫秒JavaScript的Date.now()也是毫秒而很多数据库、接口协议返回的是秒。后端返回一个13位的毫秒时间戳前端当成秒去new Date(1472483070)算出来的多半是1970年附近的日期反之前端把10位秒数当毫秒也会得到1970年1月18日之类的离谱结果。我习惯在项目里定一个规矩所有内部接口的时间戳统一用Long型毫秒对外API再根据场景转成秒或字符串日期。并且每个接口文档里都要写清楚单位。一个小数点或者几位数的差别很容易变成只在深夜上线的“定时炸弹”。3.4 2038年问题32位时间戳的倒计时既然提到了32位time_t就得说说2038年问题。2038年1月19日03:14:07 UTC之后32位有符号整数就装不下了继续累加会变成负数很多老系统的时间会直接“穿越”回1901年或者1970年。这不是科幻而是和Y2K类似的技术债。现在手机、电脑基本都是64位但我们身边还有大量32位嵌入式设备、老内核、旧数据库、老格式文件tar、zip里都藏着时间戳它们的32位time_t仍然有风险。我的建议是新项目一律用64位整型存时间戳老系统如果还在维护尽早检查底层代码有没有用int接收time_t。别看这个问题表面很远物联网设备一多2038年其实一眨眼就到。4. 开发与运维里和1970-01-01正面相遇的5个场景热搜词里那些“js时间戳”“excel时间戳转时间”“时间戳转localdatetime”“fastjson时间转时间戳”“crt日志时间戳”之类的问题我几乎都在生产环境里碰过。它们本质上是同一个问题拿到一个时间戳不知道怎么在特定技术栈里正确转换。4.1 JavaScriptDate.now()返回的是毫秒JavaScript里最容易犯的错就是单位混淆。Date.now()返回的是自1970-01-01 00:00:00 UTC以来的毫秒数new Date().getTime()同样返回毫秒。而很多后端接口返回的是秒。正确换算方式// 当前时间的毫秒时间戳 const nowMs Date.now(); // 秒转毫秒再传给new Date const tsSec 1472483070; const date new Date(tsSec * 1000); // 格式化输出 console.log(date.toISOString()); // UTC字符串 console.log(date.toLocaleString()); // 本地时间字符串我复盘过不少线上问题最后都是13位毫秒被前端当成10位秒导致时间全变成1970年附近。所以拿到接口的时间戳第一眼先数一下位数10位是秒13位是毫秒16位是微秒。4.2 Excel序列号和时间戳的换算Excel里存的日期本质上是一个“从1900年1月1日实际是1899年12月30日开始的天数”也叫Excel序列号。比如1970-01-01在Excel里对应25569。要把Excel日期转成Unix秒时间戳公式是(A1-25569)*86400反过来已知Unix秒时间戳转成Excel日期序列号B1/8640025569注意大部分Excel默认用的是“1900日期系统”Mac上的老Excel可能用“1904日期系统”起点不一样转换结果会差4年。我处理财务系统导出数据时吃过这个亏从那以后每次做Excel时间转换都要先确认文件是用哪个日期系统生成的。4.3 JavaLocalDateTime与Instant互转Java 8之后推荐用java.time包。LocalDateTime本身不带时区要转成时间戳必须指定时区// LocalDateTime转时间戳指定UTC LocalDateTime ldt LocalDateTime.parse(2024-08-17T12:00:00); long epochSecond ldt.toEpochSecond(ZoneOffset.UTC); System.out.println(epochSecond); // 时间戳转LocalDateTime指定系统默认时区 LocalDateTime fromTs LocalDateTime.ofEpochSecond(epochSecond, 0, ZoneOffset.UTC); System.out.println(fromTs); // Instant与LocalDateTime互转 Instant instant Instant.ofEpochSecond(epochSecond); System.out.println(LocalDateTime.ofInstant(instant, ZoneId.systemDefault()));热搜里那句“时间戳转localdatetime”多半是忘了给ofEpochSecond传时区参数结果转出来的时间差了8个小时。记住一点时间戳本身是UTC的转LocalDateTime时你不明确指定时区代码就会去拿系统默认时区结果自然变来变去。4.4 fastjson为什么序列化出来是一串数字Java项目里用fastjson时经常发现JSON.toJSONString(obj)把一个Date字段输出成一串毫秒时间戳比如1472483070000。这是fastjson的默认行为之一java.util.Date会被序列化为时间戳。如果前端想要的是“2024-08-17 12:00:00”这种字符串有两个办法// 方式一在字段上加注解 public class MyDto { JSONField(format yyyy-MM-dd HH:mm:ss) private Date createdAt; } // 方式二全局配置序列化 SerializeConfig config new SerializeConfig(); config.put(Date.class, new SimpleDateFormatSerializer(yyyy-MM-dd HH:mm:ss)); String json JSON.toJSONString(obj, config);需要提醒的是fastjson老版本存在不少安全隐患项目里如果还在用旧版我建议尽早升级或者干脆换成Jackson/Gson做序列化。时间戳格式化是一回事依赖库本身的健康度是另一回事。4.5 终端与日志CRT、MobaXterm的时间戳设置运维排查串口或SSH日志时经常需要精确到秒甚至毫秒的时间。SecureCRT的日志功能默认不记录时间或者只记录会话开始时间需要在“Session Options - Log File”里自定义日志格式把%H:%M:%S这样的占位符加进去。MobaXTerm则更直观在Terminal设置里可以开启“Display timestamp”让每一行终端输出前面自动带上时间。但这些“时间戳”是人读的到了日志分析阶段我建议在采集端就把时间统一成两种格式之一要么是ISO8601字符串比如2024-08-17T12:00:00Z要么是UTC毫秒数。最怕的就是一台机器日志用本地时间另一台用UTC两边合到一起排查问题时前后顺序完全对不上。踩过这个坑之后我在所有日志格式规范里都写了同一句话时间戳一律用UTC展示时再转本地。5. 一个真实案例explorer.exe错误模块里的0x57c44efe是什么Windows系统报错弹窗里经常出现这样的信息错误应用程序名称: explorer.exe版本: 6.1.7601.23537时间戳: 0x57c44efe错误模块名称: explorer.exe版本: 6.1.7601.23537时间戳: 0x57c44efe很多人会盯着“时间戳”三个字发懵这不就是我们前面说的Unix时间戳吗对但它不是“当前时间”而是这个explorer.exe文件在被编译器链接生成时写进PE文件头里的TimeDateStamp字段。这个字段的起点还是1970-01-01 00:00:00 UTC。5.1 先把十六进制时间戳解开0x57c44efe是一个32位十六进制数转成十进制就是1472483070。用前面提到的Python一行代码解import datetime print(datetime.datetime.fromtimestamp(1472483070, tzdatetime.timezone.utc))输出结果是2016-08-29 15:04:30 UTC换算成东八区是2016-08-29 23:04:30。也就是说这个explorer.exe文件是在2016年8月底编译出来的。明白了这一点报错信息里的“时间戳”就不再神秘它是文件编译更准确说是链接生成时留下的时间印章。5.2 用时间戳判断版本新旧版本号6.1.7601.23537里的23537是build号而时间戳可以帮你确认这个文件究竟是不是某个补丁之后的版本。比如你手上同时有旧版explorer.exe和新版explorer.exe分别查看它们的文件时间戳哪个数字大哪个就是后生成的。想知道某个exe/dll的时间戳不用专门去读PE头。Windows下直接用PowerShellGet-Item C:\Windows\explorer.exe | Select-Object -ExpandProperty VersionInfo | Format-List *更省事的办法是用Python的pefile库直接解析import pefile pe pefile.PE(rC:\Windows\explorer.exe) print(pe.FILE_HEADER.TimeDateStamp)在大规模排查异常进程、补丁一致性、文件是否被替换时这个时间戳是很有价值的线索。很多安全分析工具也会用这个字段关联“这个文件是什么时候生成的、和哪个补丁版本对应”。5.3 时间戳排错也有不靠谱的时候但是别把PE时间戳当成绝对真相。有几个现实原因会导致它失真第一有些构建系统为了保证“可复现构建”会把TimeDateStamp固定成一个常量或者0这样同一个源码每次构建出来的文件都一样时间戳就不能反映真实构建时间。第二一些加壳、签名、二次打包工具会在文件生成后重写这个字段。第三如果文件时间戳显示为0说明链接时就没有写入有效时间。所以真正定位Windows崩溃问题时正确姿势是“文件版本号PE时间戳文件哈希”三件套一起看。版本号说大版本时间戳说构建时机哈希说文件内容到底变没变。只看时间戳能判断方向但别当铁证。从1970-01-01这个小小的原点出发一路看到系统时间异常、Unix历史、程序里的各种转换、Windows报错信息里的十六进制时间戳你会发现时间戳其实已经成了计算机世界里一张巨大的“全局坐标系”。以后再看到系统日期变成1970-01-01不用慌张那大概率只是某个地方把时间戳清零了而已。