引言:机器中的“时间之眼”
在个人计算机的日常使用中,一个常被忽视的事实是:当我们完全切断电源,主机内部的一切半导体存储(比如我们熟知的内存条 DRAM)都会因为失去电力供应而被彻底清零。然而,第二天你再次开机时,电脑的系统时间却是准确无误的。它既没有因为断电而倒退到 1970 年,也没有丢失系统的硬件配置信息。
这种“断电不失忆”的魔法,源自于计算机主板上一个不起眼、又极其关键的小芯片——CMOS RAM(互补金属氧化物半导体随机存取存储器)。在 Linux 0.11 内核的初始化流程中,系统最早的一批硬件操作,就是围绕着这颗芯片展开的。
一、 硬件解剖:被纽扣电池呵护的 MC146818
在早期的 IBM PC/AT 及其兼容机上,主板上集成了一颗名为 MC146818 的专用集成电路芯片。这颗芯片的功能非常独特,它被设计成需要由主板上的一颗 3V 纽扣锂电池进行持续供电,哪怕计算机电源被拔掉,这颗电池依然能保证芯片内部的数据不丢失。
这颗 MC146818 芯片内部被划分成两个主要区域:
重要注解:书中提到,后来一些机器的 CMOS 内存扩展到了 128 字节,甚至更大,但由于当时 Linux 0.11 开发的时代背景(1991 年),内核默认按照 64 字节的标准布局来操作。
二、 端口魔法:0x70 与 0x71 的对话
为了读取 CMOS RAM 里面的时间或配置信息,CPU 不能像访问普通内存那样直接使用 MOV 指令。因为 CMOS 的特殊物理设计,它的寄存器被映射到了 I/O 端口地址空间中。在 x86 架构下,我们必须使用 IN 和 OUT 指令进行端口操作。
具体来说,访问 CMOS 需要两个特定的 I/O 端口:
- 0x70(地址端口,Write-only):CPU 将要读取的 CMOS 内部偏移地址(比如想读“分钟”,就发送偏移量 0x02)写在这个端口。
- 0x71(数据端口,Read/Write):地址设定好后,CPU 从 0x71 端口读取到的字节,就是对应 CMOS 内部指定偏移地址所保存的数值。
在 init/main.c 中,Linux 0.11 内核原原本本地用 C 语言(通过内嵌汇编)实现了这个协议。我们来看代码:
// 原文 4.2 main.c 程序
#define CMOS_READ(addr) ({ \\
outb_p(0x80|addr, 0x70); \\
inb_p(0x71); \\
})
代码深度剖析:
- outb_p(val, port):这是内核自带的向指定端口输出一个字节的函数(封装了汇编指令 OUT)。后缀 _p 的意思是在输出操作后,加入一个微小的 I/O 延迟。在早期 PC 架构中,硬件的反应速度比 CPU 的指令执行速度慢得多。如果不加延时,连续对 I/O 端口进行读写操作,可能导致硬件来不及响应而产生错误数据。在这段代码中,outb_p 先写地址,inb_p 再读数据。
- | 0x80 掩码操作:将 addr 与 0x80(二进制 1000 0000)进行按位或操作。这是一个非常有趣的细节,下面我将为您详细解释这段“考古级”的代码逻辑。
三、 历史迷案:0x80 与 Linus 的探索之路
“之所以会有这样的操作是因为当时 Linus 手头缺乏有关 CMOS 方面的资料,CMOS 中时钟和日期的偏移地址都是他逐步实验出来的,也许在他实验中将偏移地址与 0x80 进行或操作(并且还修改了其他地方)后正好取得了所有正确的结果,因此他的代码中也就有了这步不必要的操作。不过从 1.0 版本之后,该操作就被去除了。”
0x80 到底是什么?
在早期的 PC 架构中,向 0x70 端口写入数据时,最高位(Bit 7)控制的是 NMI(非屏蔽中断)的开启与关闭。通常情况下,在读取 CMOS 时,我们需要先禁用 NMI 以防止中断干扰读取过程,其标准操作是将 0x80(即高位为 1)写入。
然而,当时的 Linus Torvalds 并没有这篇硬件手册。他可能是在屡次的实验尝试中,偶然发现加上 | 0x80 这段代码后,系统能够成功读取到时间。他以为这是必须的初始化步骤,于是这段本属于“手忙脚乱”的代码就留在了 Linux 0.11 的正式内核中。直到后来,随着内核的完善和资料齐全,在 Linux 1.0 版本中,drivers/block/hd.c 中的类似代码才被去掉这个掩码。
这不仅仅是一次“错误”,更是 Linux 内核诞生之初,一个人单枪匹马在没有手册的情况下攻克硬件的英雄史诗见证。
四、 时间的密码:解密 BCD 码与转换宏
经过上面的硬件握手,我们从端口读到了一个字节。但要注意,这个字节并不是我们习惯的十进制数字!为了简化 RTC 集成电路的内部电路设计,CMOS 芯片内部的时间(秒、分、时、月、日等)是以 BCD 码(二-十进制编码,Binary-Coded Decimal) 的格式保存的。
什么是 BCD 码?
举个例子:当前时间大概是 14 点 35 分。
- 如果我们直接用二进制存储 35,对应的十六进制表示是 0x23(即十进制的 35)。
- 但在 BCD 码中,它是这样存储的:
- 高 4 位:存储十位数字 3(二进制 0011)
- 低 4 位:存储个位数字 5(二进制 0101)
- 合起来,CMOS 中保存的字节是 0011 0101,也就是 十六进制的 0x35。
如果内核直接把这个 0x35 当成十进制来用,就会错乱。所以,我们必须写一段转换逻辑把它解码回十进制。Linux 0.11 使用了一个非常精妙的宏:
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
拆解这个宏的执行步骤:
五、 深入 CMOS 64 字节地图(解读表 4-1)
您提供的图片中列出了 表 4-1 CMOS 64 字节信息简表。这张简表堪称计算机硬件配置的“灵魂”。其中存储的数据,不仅给了内核开机的时间,还告诉了内核自己到底有多少内存。我们把这张表格对应的 64 个字节分块来解读:
1. 实时时钟与警报区(偏移 0x00 – 0x09)
这部分存储了系统当前的时间。
- 0x00 秒,0x02 分,0x04 时
- 0x06 星期,0x07 日,0x08 月,0x09 年
- 0x01, 0x03, 0x05 是报警用的秒、分、时。如果不需要定时唤醒电脑,这些位置通常都是 0。
2. RTC 状态寄存器区(偏移 0x0A – 0x0D)
- 0x0A (A), 0x0B (B), 0x0C ©, 0x0D (D)
这 4 个字节控制 RTC 芯片的工作模式。例如,寄存器 A 用来设置时钟更新的频率(比如 1024Hz);寄存器 B 决定是否使用 12 小时制还是 24 小时制,是否开启周期性中断等。在 Linux 0.11 中,这些寄存器并没有被直接操作,而是保留由早期 BIOS 初始化。
3. 系统配置与诊断区(偏移 0x0E – 0x11)
- 0x0E POST 诊断状态,记录了系统自检过程中是否发生了错误(如内存故障、键盘故障等)。
- 0x0F 停机状态字节。
- 0x10 磁盘驱动器类型:用来标注软盘驱动器 A 和 B 的规格(是 1.2MB 还是 1.44MB,或者 360KB 等)。
4. 关键系统信息:内存大小(偏移 0x15 – 0x18)
这 4 个字节(16 位)对内核来说是最重要的!
- 0x15, 0x16 存放的是 基本内存大小(Base Memory),单位是 KB。通常标准值是 0x0280,即 640KB。
- 0x17, 0x18 存放的是 扩展内存大小(Extended Memory),单位也是 KB。
在 Linux 0.11 的 setup.s 汇编程序中,内核会通过 BIOS 中断 INT 0x15 功能号 AH=0x88 读取扩展内存大小,把结果保存到物理地址 0x90002 处。随后,在 init/main.c 的 mem_init 初始化过程中,就会依赖这个数值来决定整个操作系统主内存区(Main Memory Area)的具体范围,从而划分出内存管理(MM)分页机制所需的内存页表。没有这个数值,Linux 0.11 根本不知道自己能管理多大的内存。
5. 保留与校验和(偏移 0x19 – 0x3F)
CMOS 中的大部分空间是保留给 BIOS 存放各类复杂配置信息的(比如硬盘的柱面数、磁头数等)。以及为了确保数据没出错,在 0x2E, 0x2F 位置处存放了校验和(Checksum)。如果电池没电,CMOS 里的值(包括校验和)就会乱掉,导致 BIOS 报 CMOS Checksum Error 错误。
六、 内核关键步骤流程复盘
至此,我们将硬件连接、内核代码读取、数据转换的整个流程串联起来。下面这张 Mermaid 流程图,能非常直观地展示这“硬件 -> 内核 -> 时间”的转化过程。
#mermaid-svg-etmai68ghUPmTNKM{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-etmai68ghUPmTNKM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-etmai68ghUPmTNKM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-etmai68ghUPmTNKM .error-icon{fill:#552222;}#mermaid-svg-etmai68ghUPmTNKM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-etmai68ghUPmTNKM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-etmai68ghUPmTNKM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-etmai68ghUPmTNKM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-etmai68ghUPmTNKM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-etmai68ghUPmTNKM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-etmai68ghUPmTNKM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-etmai68ghUPmTNKM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-etmai68ghUPmTNKM .marker.cross{stroke:#333333;}#mermaid-svg-etmai68ghUPmTNKM svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-etmai68ghUPmTNKM p{margin:0;}#mermaid-svg-etmai68ghUPmTNKM .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-etmai68ghUPmTNKM .cluster-label text{fill:#333;}#mermaid-svg-etmai68ghUPmTNKM .cluster-label span{color:#333;}#mermaid-svg-etmai68ghUPmTNKM .cluster-label span p{background-color:transparent;}#mermaid-svg-etmai68ghUPmTNKM .label text,#mermaid-svg-etmai68ghUPmTNKM span{fill:#333;color:#333;}#mermaid-svg-etmai68ghUPmTNKM .node rect,#mermaid-svg-etmai68ghUPmTNKM .node circle,#mermaid-svg-etmai68ghUPmTNKM .node ellipse,#mermaid-svg-etmai68ghUPmTNKM .node polygon,#mermaid-svg-etmai68ghUPmTNKM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-etmai68ghUPmTNKM .rough-node .label text,#mermaid-svg-etmai68ghUPmTNKM .node .label text,#mermaid-svg-etmai68ghUPmTNKM .image-shape .label,#mermaid-svg-etmai68ghUPmTNKM .icon-shape .label{text-anchor:middle;}#mermaid-svg-etmai68ghUPmTNKM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-etmai68ghUPmTNKM .rough-node .label,#mermaid-svg-etmai68ghUPmTNKM .node .label,#mermaid-svg-etmai68ghUPmTNKM .image-shape .label,#mermaid-svg-etmai68ghUPmTNKM .icon-shape .label{text-align:center;}#mermaid-svg-etmai68ghUPmTNKM .node.clickable{cursor:pointer;}#mermaid-svg-etmai68ghUPmTNKM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-etmai68ghUPmTNKM .arrowheadPath{fill:#333333;}#mermaid-svg-etmai68ghUPmTNKM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-etmai68ghUPmTNKM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-etmai68ghUPmTNKM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-etmai68ghUPmTNKM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-etmai68ghUPmTNKM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-etmai68ghUPmTNKM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-etmai68ghUPmTNKM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-etmai68ghUPmTNKM .cluster text{fill:#333;}#mermaid-svg-etmai68ghUPmTNKM .cluster span{color:#333;}#mermaid-svg-etmai68ghUPmTNKM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-etmai68ghUPmTNKM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-etmai68ghUPmTNKM rect.text{fill:none;stroke-width:0;}#mermaid-svg-etmai68ghUPmTNKM .icon-shape,#mermaid-svg-etmai68ghUPmTNKM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-etmai68ghUPmTNKM .icon-shape p,#mermaid-svg-etmai68ghUPmTNKM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-etmai68ghUPmTNKM .icon-shape .label rect,#mermaid-svg-etmai68ghUPmTNKM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-etmai68ghUPmTNKM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-etmai68ghUPmTNKM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-etmai68ghUPmTNKM :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
开启电脑,BIOS完成自检与设置
内核加载,执行init/main.c time_init()函数
准备读取硬件时间
向I/O端口 0x70 写入要读取的地址偏移量
延迟等待I/O同步
从I/O端口 0x71 读回一字节数据
得到BCD码格式字节
调用宏 BCD_TO_BIN
计算出正确的十进制秒、分、时、日、月、年
将这些数值赋值给 struct tm 结构体
调用 kernel_mktime() 计算Unix时间戳
将总秒数存入全局变量 startup_time
内核所有与时间相关的模块(定时器、文件修改时间)在此刻拥有了时间基准
七、 技术终章:从 CMOS 到对内存管理的启蒙
除了为系统提供时间,CMOS 芯片所保存的扩展内存(0x17, 0x18)信息,可以说是 Linux 0.11 内核正式开启物理内存分页管理的前置条件。
在 init/main.c 的 118 行,你会看到这样的代码:
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end = 0xfffff000;
系统根据读取的 EXT_MEM_K 和基本内存计算出总物理内存大小后,才会去调用 mem_init(main_memory_start, memory_end); 并将内存分成内核区、高速缓冲区、虚拟盘区、主内存区。
如果说启动引导程序 bootsect.s 是帮内核“把身体”搬进物理内存,那 CMOS 就是为内核指明了自己身体(物理内存)到底有多大的那个“度量衡”。
下篇预告:
在一系列硬件探测与内存初始化之后,Linux 0.11 终于迈出了它作为“多进程操作系统”的关键一步。在第 4 篇解析中,我们将聚焦于 if (!fork()) 这行神奇的代码,揭开进程复制(Clone)的奥秘,并探讨 init 进程是如何“诞生”出来的,敬请期待!