跳到主要内容

锁住,还是续命——把一块 RAM 读到物理层

读底
读底

· 阅读约 24 分钟

volatile uint32_t counter;

这一行 C,编译器给你的东西是一个 SRAM 单元。不是比喻,就是一个 SRAM 单元。你把某个工艺库的 standard cell 手册翻出来,找到那个六管单元,看它的电路图:两个反相器首尾咬住,两条位线从外侧切进来。然后你再去翻 1918 年英国那份专利,Eccles 和 Jordan 交上去的双稳态真空管电路。把两张图叠在一起看——除了工艺,它们是一个东西。

一百零八年。这个电路没被换掉过。

我平时写的东西基本都在软件调用链上打转,这次想换一换。因为读到某个份上你会发现,malloc 返回给你的那块内存、memcpy 搬的那些字节、你 debug 时在 GDB 里敲 x/16xb 打出来的那十六个字节,它们底下压着的不是抽象,是物理。而这一层物理里真正值得读穿的东西,只有三条路,从继电器一直排到硅片底下。

先把地图铺开。

第一条,锁住。 找一个本来就是稳定的物理平衡点,把状态放上去,只要不断电它就在那儿。继电器自锁、Eccles-Jordan 触发器、SRAM 里那对交叉耦合反相器,全在这条路上。好处是你随便读,读到天荒地老值不变;代价是每个比特要占的元件多。

第二条,续命。 找一个会自己消失的物理量,把状态变成它,然后不停地把它救回来。电容上的电荷、延迟线里的声波、CRT 荧光层上那片静电场,全在这条路上。好处是每个比特只要极少的元件;代价是你得一直付税。

第三条,磁性。 磁芯、磁鼓、今天 Everspin 卖的那种 MRAM。这条路两头不靠:磁化方向本身是稳的、断电磁不倒,但你没法"看"它,你只能让它变一次、去感应它变的那一下。所以磁性主存天生带着一个别扭——读就是写。

这三条路不是按年份排开的,也不是谁取代谁的关系。磁芯在这三条路里同时活了好些年,和延迟线、和 Williams 管、和最早那批 SRAM 同台过。你记住"哪条路"比记住"哪一年"有用得多。

一张地图三条路,这张地图上真正的问句只有一个:状态放在什么上、这个东西多久会跑掉、为了不让它跑掉,人类多花了多少东西。 这个问题问到底,你回头再看今天内存条的 datasheet,会认出里面每一条参数的来处。

两千个继电器做存储,六百个做计算

先跳到 1938 年前后的德国,看一台具体的机器:楚泽的 Z3。

这台机器在中文材料里常被一句"第一台可编程计算机"带过去,然后大家就去争它和 ENIAC 谁更早。争这个没什么意思。我想说的是它里面那个比例:Z3 的主存用了大约两千个继电器,而整台机器其余部分——真正做运算、做控制的那部分——只用了大约六百个。 两千比六百,三倍多。

这个比例是整篇的第一个钉子,慢一点看。它说的最朴素的一件事是:在那个年代,存东西比算东西贵三倍以上。 你今天写代码的直觉刚好是反的——内存是便宜的、是白来的,CPU 才是贵的那个。1938 年是倒过来的。你手上只有一批珍贵的、笨重的、通断一次要几十毫秒的机电开关,你先拿它去干什么?先拿去算,还是先拿去存?楚泽的选择是存。因为算不出来的机器只是慢,存不住的机器根本不能叫可编程。

那继电器怎么"锁住"一个比特?两条做法,都很土,但都直通今天。

一条是普通继电器自供电锁存。继电器里有个线圈,通电就把衔铁吸过去,衔铁上带着一组常开触点;你把这组触点里的一个,接回线圈自己的供电回路。它一吸合,就自己给自己供电了——从这一刻起,即使你松开按下去的那个按钮,线圈里的电流也不会断,因为它靠的是自己刚接通的那条路。 这就是自锁。要断开它,你得另给一条串联的断开回路,把它切断。

另一条是闭锁继电器,靠一块永磁体保持衔铁位置。通一下电翻过去,断电了磁铁还摁着,再反向通一下才翻回来。更省电,工艺更麻烦。

现在拿这两条和你今天在 MCU 里写的那种几十行状态机对一下——if (flag) { flag = 0; ... }。你写的每一个 flag,在电路上就是这么个东西:一个自己喂自己的回路。锁住这条路,从第一步起就已经定性了。

那楚泽知不知道继电器没前途?他知道,他非常清楚继电器就几十赫兹,没有长期前景。那他为什么还用?为了拿到钱。真空管那时候更贵、更不可靠、灯丝还烧,你要拿一台"给元首看的、能算出航空结构强度的机器"去申请资助,你只能在机电这一条路上做到极致。这不是技术选择,这是资助选择。

这里我插一句,因为它太值得插了。你在读任何一段"古人怎么会选这么蠢的方案"的代码或图纸时,先问一句:他当时的约束是什么。 楚泽的约束是"三年内拿出一台能跑的机器、并且让一个不懂技术的拨款人相信它值钱"。在这个约束下,继电器是唯一解。换你写,你也得选继电器。你甚至不一定能像他那样,把整台机器的继电器数控制在两千六。

(顺便,这次资助最后没拿到。这条我觉得比 Z3 本身更值得记住。)

1918 年那个专利,和今天那块 SRAM

Z3 之后不到三十年,同一条路走到了真空管上。

1918 年,英国人 Eccles 和 Jordan 申请了一份专利,内容是用双稳态真空管电路取代机电继电器——注意"取代"这个词,他们要取代的就是楚泽后来用的那种东西。这个双稳态电路的骨架,你第一次见会觉得有点怪:两个真空管(或者后来是两个三极管),每管的输出接另一个管的输入,彼此咬住。咬住之后,整个回路只有两个稳定的平衡点:左通右断,或者左断右通。中间那个"两个都通一半"的状态是不稳的,任何一点扰动都会把它推到其中一个平衡点上去。

这就是置位-复位锁存器。它和继电器自锁,和今天 SRAM 单元里那对交叉耦合反相器,是同一个东西的三种工艺实现。

            VDD
         ┌───┴───┐
         │       │
       [ P1 ]  [ P2 ]          两条上拉
         │   ╲╱   │
         ├────╳────┤            ← 交叉耦合点:
         │   ╱╲   │               P1 的栅接 P2 的漏,
       [ N1 ]  [ N2 ]             P2 的栅接 N1 的漏
         │       │
         └───┬───┘
            GND

         ▲       ▲
        BL      BL̄          ← 两条位线从这里切进来(各经一个访问管)

(结构示意,不是哪个具体器件的仿真图。)

上面那四个管子是内核:两个 PMOS 上拉、两个 NMOS 下拉,分成两组,组成两个反相器,然后把反相器的输出交叉接回对方输入。中间那个 ╳ 就是"咬住"的地方。下面伸出去的两条线是位线,各经过一个访问晶体管切进来——加上这两个访问管,就是今天教科书上那个 6T SRAM。

读到这儿,我想把一个判断说死:SRAM 不是"发明"出来的,它是"缩"出来的。 它的电路在 1918 年就定型了,中间经过真空管、分立晶体管,直到把六个管子做进同一块硅里,电路本身一步没动。你要问它为什么能活到今天,答案不是"它好",而是——在这条"锁住"的路上,这个结构已经是每比特元件数的下限了。四个管子做双稳态内核,你没法再省;两个管子做访问,你也省不掉。省掉任何一个,锁存就不成立。

这就是不变量。不变量这东西你读源码时会遇到(比如"Redis 单线程不加锁"那条),读硬件也会遇到,而且硬件的更硬——软件的不变量是约定,硬件的不变量是物理。

顺着"锁住"这条路往下走,你一定会问:1950 年前后真空管机器满天飞,为什么没有一台是拿真空管堆出主存的?

答案是算得出来的。一个双稳态触发器两个管,一千个比特就是两千个管,加上外围的电阻、耦合电容、门电路,一颗比特的成本要几毛到几块钱这个量级的当时货币;更要命的是灯丝。每个管子里的灯丝都在持续耗电,一千个比特的存储阵列光让灯丝亮着,功耗就能到一台机器整机的几倍。 你不是在花钱买存储,你是在给一个永远点着的电阻阵列交电费。

所以那一代机器的做法是分裂的:逻辑用真空管做,主存用别的做。 延迟线、磁鼓、Williams 管,这些名字后面会一个一个讲到。真空管在存储器史上只留下了一个短暂的位置,就是那几个、几十个比特的寄存器,以及 1918 年那份专利给整条"锁住"之路定下的模子。

这个分裂是有后果的。它意味着从 1950 年到 1970 年代中期、也就是集成电路真的成熟之前,主存这个东西必须靠别的技术撑着——不是设计者不想统一,是真空管这条路线撑不起存储的功耗预算。你如果把这条当成"历史的弯路",就又想错了;它不是弯路,它是被物理约束逼出来的唯一走法。约束在这儿,代价就在这儿。

换路:电荷会跑掉

现在从"锁住"切到"续命"。

故事要先拐到 1939 年的爱荷华州立学院,看一台叫 Atanasoff-Berry 的机器。这台机器在中文互联网上被捧得很高,常被说成"第一台电子计算机"。这个说法得纠正一下:它实际上是一台很复杂、但不可编程的计算器。 它只能解线性方程组这一类特定问题——电子管几百个,做的是固定算术。它不是通用机器,没有指令流,没有可编程的存储程序。

那为什么还要读它?因为它的存储方式,是今天你手上这条内存条的直接祖先。

Atanasoff-Berry 用的是一个旋转鼓加一片机械开关控制的电容器阵列。每个比特存在一个小电容上,电容里有没有电荷,代表 0 还是 1。就这么简单。但它立刻带来一个问题:电容是会漏电的。 电荷自己会从介质里跑掉,你不用它,过一阵它也没了。所以这个存储阵列必须被周期性扫描一遍、把该有电荷的重新充满,也就是刷新。

读到这里先别往下滑。这一刻,正是"续命"这条路的出生证明。 你现在写 C 基本感觉不到"内存需要刷新"这件事,因为内存条上的刷新控制器替你在你不知道的地方、每几毫秒扫一遍所有行。但这条路上的所有代价——刷新税、读放大、读破坏性、行缓冲——都是从这个"电荷会跑掉"里长出来的。Atanasoff-Berry 的电容鼓是现代 DRAM 的前身,现代 DRAM 依然是电容存比特,只是把那堆电容从旋转的鼓上搬进了硅片,用电子方式寻址,把容量从几百比特做到几十亿比特。

物理量没换,换的只是工艺。

那为什么不全都做 SRAM?为什么不锁住,非要续命?

因为划算不划算。

DRAM 相比 SRAM,慢,而且耗电——很多人不理解"DRAM 耗电"这件事从哪儿来,其实主要的耗电项不是读写,是刷新。那笔税,只要通电就在交。但 DRAM 每个比特需要的元件数少得多:理想情况下,一个晶体管加一个电容,就是一个比特。SRAM 是六个晶体管一个比特,差六倍。芯片面积按元件数走,六倍面积就是六倍成本,还要算良率、走线、功耗密度。这道算题里你选的不是"哪个更好",你选的是**"我可以忍受一个周期性的、永不停止的续命动作,来换回六分之五的面积"**。

这个交易从 1970 年代一直做到今天,一次都没被推翻过。

把比特放进时间里

电容鼓的思路是把比特摊在一个二维的物理面上。还有一条更邪门、我私心也更喜欢的路:把比特放进时间里。

延迟线存储器干的事情是:把一串数据编码成声波(更早的版本里是编码成电脉冲),让它在一段介质里传播,从这头走到那头,另一端有个检测器,检测到之后把波形重新整形成数字,再重新发回这头。于是这一串比特就在一个电声回路里循环不息,只要回路转着,数据就在。 一个比特不是停在那儿不动,它在跑。

跟电容鼓比,优势在速度。声波在材料里传播的速率,比一个机械鼓转一圈快太多了。电容鼓的延迟取决于电机转速,你转得再快也就那么多转;声波的延迟取决于介质里的声速,两者根本不在一个数量级上。所以在磁芯成熟之前,延迟线在那些"等着它转一圈"的场合,是能打的。

最出名的一种介质是汞。汞延迟线——一根长长的、充满汞的管子,两端各有一个压电换能器,电信号一进一出就变成声波。1950 年代初的机器,很多主存就是一根到几根这样的管子。

扭线存储器走的是另一个极端:它把波做成弹簧丝上的扭转波,而不是沿着丝轴线方向的纵向波。这个差别看起来很小,但带来一个很实用的后果——扭转波允许你把丝松散地盘绕起来,用橡胶间隔件固定住,而不必让丝绷得笔直。 纵向波的延迟线,丝得拉紧;扭转波的,你可以把它盘进一个小盒子里。1970 年,Monroe 925 计算器用的就是这种技术。一台桌面计算器的存储,是一根盘起来的弹簧丝。

这里我得停下来泼一点冷水。我见过不少材料把汞延迟线讲得很浪漫,说汞的声阻抗刚好匹配当时的压电换能器,所以是"被物理选中"的。lcamtuf 在那篇原文里明确不认这个说法,他倾向于汞可能只是在当时那个容量需求下,几个方案里算下来最优的那个——不排除当年也试过别的液体甚至固体,只是没做出来。

我站他这边。这类"必然性叙事"我在读源码时见得太多了:某个设计被写进教材,就被追认成"因为 X 所以只能这样"。但你翻开它的历史,常常是几个方案摆在那儿,掐指一算成本,选了一个。别把工程妥协读成物理宿命,这是我看这段时最想说的一句话。

延迟线这条路的代价也很清楚:它是串行的。 你要读第 500 个比特,就得等它转到第 500 个位置,没有随机访问这回事。当机器主频到了兆赫兹量级,延迟线每转一圈的时间就显得长了。所以它后来被磁芯顶掉,是必然的——不是它慢得离谱,是计算机快得太快,它追不上。

Williams 管:用眼睛存,但不靠眼睛读

同一个年代,还有一类存储器件长得像示波管。代表性的两个是 Williams 管和 RCA 的 Selectron 管。它们没有活动部件——这在那时候是个大卖点,延迟线和磁鼓都在转,磁芯还没成熟,一个不用转的存储器件意味着更少的机械故障。

工作原理说穿了不复杂:管子正面有一层荧光材料,电子束打上去会留下一个亮点图案。这个图案就存了几百个比特。 当然,你得能寻址、能刷新,图案才立得住。

但真正让我读得停下来的,是它的读取方式。

它不是靠"看亮点"来读的。 你把电子束打到某个位置,去检测的其实是那一点上静电场的变化。为什么?因为荧光是有余辉的,衰减得很快,你一路扫描过来,等扫到那一点的时候,亮点可能已经暗了不少,靠亮度区分 0 和 1 不可靠。可静电场不一样,电荷留在那儿,场还在。所以读的时候,你看的不是"那儿亮不亮",而是"那儿的场怎么变的"。

这又是一个"物理量会消失、所以得换个角度去读"的例子。 和电容鼓的电荷自放电是同一类问题,只是换了一种物理现象。而它的容量——几百个比特,一个管子。几百个。你拿它当主存,得并联多少个?所以它最后也就在早期几个实验机里用过,然后被磁芯送进博物馆。

这一段我承认讲得有点杂,把 Williams 管和 Selectron 混在一块说了。严格讲它们是两条不同的实验路线,做的机构也不是一家。但它们在"用电场读、不用亮点读"这个点上是共通的,而我就是想说这一个点。那两条岔路的差别,值得单独一篇文章来拆。

磁芯:读就是写

现在钻到全文最厚的一段。你要是只读这一篇里的一个器件,读磁芯。

从 1950 年到 1970 年代中期,磁性主存在计算机里相当常见,而磁芯存储器是其中最经典、也最值得拆开看的一个。它的基本单元是微型铁氧体磁环——一个个比芝麻还小的环形磁铁,排成一片二维阵列,穿织其间的是一根根绝缘铜线。

先说写。

核心的设计叫半选择。假设你要写某一个比特。你不能只在它的行上通一根线给一个强电流,那会把这一整行同一条线上的磁芯全都翻一遍。它的做法是:水平方向一根线(X 线)和垂直方向一根线(Y 线)各通一个电流,两个电流单独产生的磁场都低于磁化阈值,只有交叉点上那两个磁场叠加起来,才超过阈值,把那个磁芯翻转。

这是一个非常漂亮的设计,但它同时是一份非常危险的契约。为什么危险?因为同一条 X 线上的所有磁芯都承受了那个半场,同一条 Y 线上的所有磁芯也一样。 你每写一个比特,都在拿半场去扰动一整条线上所有其他比特。它们必须挺住,不能被翻。所以这条线上每一个磁芯,它的矫顽力、它的磁滞回线的方度、它的尺寸公差、它穿线的角度,全都被这个"半场不能翻"的不变量卡死了。你今天设计存储单元要跟工艺漂移搏斗,1960 年的人也一样,只是他们搏斗的介质是铁氧体。换你写,你敢让整条线上的邻居都吃半个翻转场、还指望它们一个都不动吗?半选择就是从这份不敢里长出来的。

再说读。这里有个反直觉的点,我觉得它是整段最该讲透的。

读取是破坏性的。 你没法"感应"一个磁芯现在朝哪边——磁化了的磁环本身不发出任何持续的、能被读头察觉的信号。你只能把它覆写一遍,看它翻不翻。 具体做法是:往被选中的单元里写一个已知值(比方说 0),同时检测它的感应线圈。如果这个单元原来是 1,那这次写入就发生了极性翻转,翻转的瞬间会在感应线上打出一个电流脉冲;如果它原来是 0,那它本来就是 0,这次写入不引起翻转,也就没有脉冲。检测到脉冲 = 原值是 1,没检测到 = 原值是 0。

问题是,不管你读到的是 0 还是 1,读完之后那个单元里现在都是 0 了。 原来存 1 的,被你翻掉了。所以每一次读后面,必须紧跟着一次写回,把读到的值重新写回去。这叫读后重写(read-restore),是磁芯存储器的标准动作。这个"读 = 写 + 比较"的结构,直接决定了磁芯的访问时序:它的读周期比写周期长,因为读包含了写。

我知道你在这儿可能会想起什么。DRAM 也是破坏性读,也得写回。这两条路在这一点上撞车了。 一个靠磁化方向存,一个靠电容电荷存,最后都被"你必须以破坏它的方式来读它"这件事绑在同一个工程问题上:读回写的时钟怎么排、什么时候可以开始下一条指令。

现在把密度摊出来看。lcamtuf 那篇里引了一张显微照片,拍的是一片高密度磁芯模块——照片上那一小片里能看到大约 63 个比特。 而整个模块是 8×8 英寸,容量 32 kB。你不用去算 8×8 英寸换算成平方厘米是多少,你只要知道这是两万六千多个比特摊在一块 8 英寸见方的板上——平均下来一个比特占的面积,得用小半个平方厘米来数。 你今天拿一颗内存颗粒,同样面积能装下的比特数,多到我不想去算这个比值,因为算了会显得荒唐。

但你得公平地看它。磁芯当年是又快又小的——相对于延迟线和磁鼓,它没有活动部件,访问时间到微秒量级,容量到几十、上百 KB,可靠性也高。它统治了二十多年,这不是侥幸。

两条路同时在硅上重做一遍

1970 年代中期的某一个时刻,这两条路同时被搬进了硅里。

按 lcamtuf 在置顶评论里补的(这条不在正文里,是他自己下场说的,我觉得比正文还值得记):Intel 3101 是第一颗单芯片 SRAM,也是 Intel 这家公司的第一款产品,容量 64 比特,主要用途是数据寄存器。

六十四比特。一颗芯片,六十四比特。你要拿它当主存,得铺一墙。

紧接着出来的是 Intel 1103,第一颗 DRAM,容量 1 千比特,刷新周期 2 毫秒。 作者自己的比较是:1103 比 3101 复杂得多,需要两种电源电压,但很快就开始取代磁芯存储器。

把这两颗放在一起看,这一节我想说的东西就出来了:3101 是把 1918 年 Eccles-Jordan 那个双稳态电路缩到一颗硅片上的结果;1103 是把 1939 年 Atanasoff 电容鼓上那堆会漏电的小电容缩到一颗硅片上的结果。 两条路走了三十多年,各自绕了一大圈,最后在同一个年代、同一家公司、同一间工厂里,以两颗芯片的形式汇合。

1103 那个"需要两种电源电压",不是设计缺陷,是这条路本来就要交的代价。电容要写出足够的电荷量,需要比逻辑电路更高的电压;逻辑电路又要跑在更低的电压上省电、提速度。 两套电压意味着更麻烦的电源设计、更多的管脚、更多的板级器件。这个代价在后来的 DRAM 里一直没消失过——你想用 1T1C 换那六分之五的面积,你就得接受它的供电比 SRAM 更麻烦。这是一揽子交易,没有拆开买的选项。

而 1103 开始取代磁芯,也让前面那段里那个"读就是写"的问题,从一个硬件时序问题变成了控制器固件里的一个状态。读破坏性这件事没被解决,只是被一层一层抽象上去,最后你在 C 里写 p = *q; 的时候已经感觉不到它了。 你今天在 CPU 里看不到磁芯那套读后重写,是因为刷新、预充电、行激活这一整套动作被塞进了内存控制器的一个小状态机里,你在汇编里都看不到。这就是抽象层干的事。

磁性这条路今天的样子

最后看一眼第三条路今天长什么样——因为它绕回来了。

Everspin Technologies 在卖磁阻式 MRAM 芯片,用在嵌入式应用里,容量最高 16 MB。它的行为像 SRAM,但断电之后数据不丢——这句话你得嚼一下:像 SRAM 一样用,但它是非易失的,中间没有电池、没有超级电容、没有 flash 那套擦写周期。 它靠的是磁隧道结里的磁化方向,不用电就一直保持。

lcamtuf 自己的判断是:MRAM 目前的竞争力不算强,它可能的机会在抗辐射、抗电磁脉冲这些场合——航空航天和国防。我对这个判断没异议,而且想把它往回接到前面的地图上。

MRAM 这条路每隔几年就被吹一次,每次都有人写文章说它要取代 DRAM。它没取代。你要问为什么,技术不行只是一半——更根本的是 DRAM 那条路已经把"每比特最少的元件数"顶到物理下限了。 1 个晶体管、1 个电容,一个比特。你不可能比这个更少。任何新存储技术要跟 DRAM 争,它至少得在这个成本结构上打平,而这几乎意味着它得发明一套比 1T1C 更省的物理机制。磁隧道结的读出要过一层薄薄的隧道势垒,它的单元结构比 1T1C 复杂。它不是没戏,但它的戏不在"便宜"这个战场上。

我可能是太较真了。但这一段我想替它说实话:一件事能不能成,经常不取决于它技术有多好,取决于它有没有撞上一条已经在跑的、把成本压到地板上的老路。DRAM 就是那条老路,从 1939 年一台计算器的电容鼓开始跑,跑到今天还没停过。

下一站

结尾我不复述前面讲过的东西,那没意思。给你几张下一站的地图。

第一站,把 1918 年那份 Eccles-Jordan 专利翻出来。 不用读全文,就看它那两张电路图,然后再去你的工艺库里找那个 6T SRAM 单元的标准单元图,两张图并排放。你要是做数字后端,这一步值得花你半个小时——从这一天起,你 layout 上那些 bitcell 阵列,在你脑子里就不再是 ▩▩▩▩ 的一个黑盒图块。

第二站,Intel 3101 和 1103 的原始 datasheet。 这两颗芯片的 datasheet 是活的——它们会把"64 比特""1 千比特""2 毫秒刷新周期""两种电源电压"这些数字,原原本本摊在你面前。你看一份 1970 年代初的 datasheet,会明白那个年代的"内存系统设计"是怎么被这些参数逼出来的。

第三站,这一站是我最想推的:去找一份磁芯存储器的存储板实物照片,最好是显微特写。 不是看它有多"复古",是看那些线怎么穿过去。绞线法、三线法、二线半选,这些名字在磁芯手册里都有,你去核一下它们和半选择原理怎么配合。看完再回到今天内存条的行激活时序图,你会认出同一套问题——半选扰动在磁芯里是"同一线上的邻居会不会被翻",在 DRAM 里是"刷新间隔内的电荷还能不能读出",物理不一样,问题的形状是一样的。

我最近一直在读这一类东西,越读越觉得,软件里最容易被当成"理所当然"的那几层——内存、缓存、可见性、原子性——它们的根都扎在这种几毫米厚的板子和几个管子的物理里。Andre 那篇博客下面有人在留言里催他多写点这类东西,我懂那个感觉。

地图给你了,剩下的路,自己顺着物理走到底。

读底
读底

万字导读大型开源项目源码:关键函数逐段讲、画数据流,读到你能自己定位。

查看主页 →