ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深入理解计算机存储器:组成、连接、时序与校验

深入理解计算机存储器:组成、连接、时序与校验 1. 为什么存储器的地位如此特殊它承载程序也承载数据1.1 冯·诺依曼体系里的“第五部件”到底负责什么前几篇文章我们把运算器、控制器和输入输出都过了个遍到了这一篇终于要面对计算机里最容易被低估、却一刻都离不开的部件——存储器。你打开任何一台现代计算机的主板目光扫过去CPU旁边那块最显眼的矩形插槽区域就是内存条的位置。它不像CPU那样集万千晶体管于一身也不像显卡那样自带光芒但整台机器的所有程序、所有数据、所有中间结果最终都要在它里面落脚。冯·诺依曼体系结构把计算机分成五个基本部件输入设备、输出设备、运算器、控制器和存储器。前四个部件的工作逻辑相对好理解但存储器的特殊之处在于它同时承担了两件性质完全不同的任务存放指令也存放数据。经典的“存储程序”思想就是这么来的——程序不再是硬接在电路里的逻辑而是作为一组数据先放进存储器再由CPU取出来逐条解释执行。这一步看似简单却是现代计算机与早期专用计算器的根本分水岭。理解这一点之后你就会明白为什么计算机系统基础这门课会把存储器放到这么靠前的位置来讲。没有它CPU再快也只能空转没有它输入设备送入的数据根本没有安放之处没有它运算器的中间结果会立刻丢失。存储器的容量、速度和可靠性从三个维度约束着整机性能。1.2 从延迟线到闪存存储器形态演变的底层逻辑如果你翻过计算机早期历史会发现第一代存储器居然是水银延迟线利用声波在水银里的传播时间来“拖住”数据绕一圈再读出来。后来又有了磁芯存储器、磁鼓再到半导体存储器的兴起。这些形态变化看似眼花缭乱背后的逻辑却始终一致用更小的体积、更低的成本、更快的速度去保存更多比特。半导体时代到来以后存储器市场基本被两类芯片瓜分RAM随机存取存储器和ROM只读存储器。RAM又分为SRAM和DRAM前者用触发器保存数据速度快但成本高后者靠电容充电保存数据密度大但需要定期刷新。ROM家族从掩膜ROM走到PROM、EPROM、EEPROM再到今天无处不在的Flash闪存核心差异在于写入是否方便、是否可以反复擦写。EEPROM的存储原理值得多说一句它利用浮栅晶体管保存电荷写入时需要较高电压让电子穿过绝缘层进入浮栅擦除时再通过量子隧穿效应把电子拉出来。电荷留下状态就是“0”电荷放掉状态就是“1”而且掉电不丢失。这种原理听起来和DRAM的电容存储有点像但最大区别是非易失和易失。我们在后面的层次结构里还会看到正是因为不同的存储器件拥有不同的速度、容量、成本和易失性计算机才需要把它们组合成一套存储体系而不是指望一种芯片搞定所有需求。2. 主存储器的内部构造地址、存储单元、数据线三种基本要素2.1 一次单元读写的过程是怎么走通的主存储器看起来是一整块内部却是高度规整的阵列结构。任意一个存储芯片拆开来看都有一排排存储单元每个单元能保存一位二进制数据。大量单元排列成矩阵横向有行地址译码器选出某一行纵向有列地址译码器选出某一列行和列的交点就是要访问的那个单元。这种二维阵列的好处很直观如果一万个单元排成一长条寻址逻辑会极其复杂排成100乘100的矩阵行译码加列译码就轻松得多。假设现在要读地址0x0042处的数据。CPU先把地址信号送到存储器的地址引脚经过译码后选中对应的存储单元存储单元里保存的电压状态被读出放大器感知并放大最终出现在数据引脚上。整个过程听起来顺理成章但这里有一个很容易被初学者忽略的细节存储单元的读出其实是“破坏性”的。DRAM的电容在读出过程中会放电读完一遍数据本身就没了所以DRAM必须在读出之后立刻把数据重新写回去这一动作叫“再生”或“恢复”。SRAM不存在这个问题所以速度更快代价是单个存储单元需要多只晶体管面积大、密度低。2.2 地址线、数据线、容量三者的数学关系存储器的容量不是随便标的它由两个参数直接决定地址线数量和每个存储单元的位数。一条地址线只能表达一个二进制位所以n条地址线最多产生2的n次方种地址组合也就最多选中2的n次方个存储单元位置。常见的内存条标称“容量2GB”背后就是足够多的地址线和足够密的单元阵列在支撑。这里有个需要理清的概念地址线数量决定的是“能访问多少个单元”数据线宽度决定“一次能同时读出多少位”。如果一个存储器有11条地址线、8条数据线它的容量写作2K×8位也就是2048个存储位置每个位置保存8位数据总容量为2048乘8等于16384比特。我们平时说的“内存条是8GB”指的是8G字节换算一下就是64G比特这中间的等效关系一定要彻底想清楚否则后续做存储系统设计时一定会算错。表格化整理一下一个存储芯片的容量参数解读表示方式含义总位容量16K×4位16384个单元每单元4位65536比特64K×8位65536个单元每单元8位524288比特512K×16位524288个单元每单元16位8388608比特实际系统中如果CPU数据总线是16位而存储芯片数据线只有8位就需要用两片芯片并联各提供高低8位同时工作。如果CPU需要更大的地址空间则可能需要多片芯片在地址上扩展这就涉及下一节要详细讲的连接问题。2.3 写入和读出的时序不是CPU直说“读”存储器就会立刻执行许多人第一次接触存储器接口时会本能地以为CPU发出地址、发出读信号数据就会瞬间出现在总线上。真实情况并非如此。RAM芯片内部从地址稳定到数据有效需要经历译码、单元访问、读出放大等一系列过程这一段时间就是存取时间。写入操作也有严格的建立时间和保持时间要求地址和数据必须在写信号有效前就稳定下来并且在写信号撤销后还要维持一小段时间否则数据就可能写错位置或者根本没写进去。这些时间参数在芯片手册上都有明确标注。以典型的SRAM为例读周期可能包括地址建立时间、读存取时间、数据保持时间几个阶段。设计者在连接CPU和存储器时必须对照CPU的时序图和存储芯片的时序图确认双方在时间上能否好好配合。CPU说“我要读”存储芯片说“我最快也要50纳秒才能给出数据”如果CPU在40纳秒时就把数据读走了那读回来的就是垃圾值。这时候要么插入等待周期让CPU慢一点要么换更快的存储器要么在中间插入Cache这就是后面章节的缓存思想的原始动机。3. CPU与存储器的连接接线只是表象协调才是最关键的3.1 译码逻辑存储器怎么才知道“CPU在叫我”把CPU和存储器芯片连接起来第一层工作当然是按图接线地址总线接到地址引脚数据总线接到数据引脚读信号、写信号接到对应控制引脚。但这只是最简单的部分。问题马上出现CPU的地址总线往往有16条、32条甚至更多而一片存储芯片可能只有11条地址引脚CPU发出的那些高位地址谁来管答案是需要译码逻辑。CPU发出的完整地址信号一部分直接进入存储芯片作为片内单元地址另一部分通过译码器来决定到底访问哪一片芯片。这个过程类似于一个大院里有好几栋楼门牌号的低位决定你在楼里的第几间房高位决定你进的哪栋楼。常用的译码器有74LS138这样的3-8译码器输入三位地址信号输出八路独立的使能信号每一路可以单独选中一片存储芯片。译码方式的选择值得琢磨。如果把所有高位地址都接入译码器得到的是全译码方式存储地址是连续且唯一映射的不会出现一块物理存储器对应多个CPU地址空间的情况。如果不接全部高位地址只用了部分地址参与译码这就是部分译码会出现地址重叠——多个地址值访问到同一个存储单元。部分译码能少用些器件代价是地址空间的浪费和软件上的模糊性做小系统时可以接受做正经设计时则要慎重。3.2 一个实际的小例子用8位CPU接6116纸上谈兵不太好理解我们看个具体的设计。假设现在要用8088这样带8位数据总线的CPU去接一片6116 SRAM。6116是2K×8位11条地址线所以CPU地址总线中的低11位直接连过去负责区分片内的2048个单元。CPU地址总线剩余的高位地址用来生成片选信号。8088的地址线在复位之后从0x00000开始线性编址。若我们希望这片6116占据从0x00000到0x007FF的空间也就是低2KB地址那么参与片选的地址信号就有讲究。CPU地址线中第11位到第19位如果全为0时选中6116这可以用一个与非门组合产生低有效的片选信号。更高位的地址译码越严格地址空间浪费越少但也需要更多门电路。数据线连接相对简单8位数据总线直接和6116的8个数据引脚对应相接。控制线方面CPU的存储器读信号接6116的输出使能OE存储器写信号接写使能WE译码器输出接到CE。把这几个信号的关系理清楚只有CE有效时芯片才被真正选中读时OE有效数据从芯片出来写时WE有效总线上的数据被写进单元。三者互为条件缺一不可这就是控制器设计里的“使能”思想。3.3 时序配合和总线竞争新手容易忽略的细节连接存储器时还有一个非常隐蔽的坑——总线冲突。如果多个芯片同时输出了数据数据总线就会短路打架。这种情况最容易出现在芯片的片选逻辑设计失误时本应只有一个CE有效结果两个芯片的CE同时变低两个芯片同时把数据放到总线上轻则读出数据错误重则烧毁芯片。排查办法是检查每一路片选信号是否互斥必要的时候在时序上再确认是否有毛刺导致瞬时误选中。另一个常见问题是CPU等待周期的计算。CPU在总线周期里发出读命令后会等待存储器在规定的时钟周期内送回数据。如果6116存取时间超过CPU的等待上限CPU读到的数据就是未稳定的。常用的应对手段是引脚Ready信号拉低让CPU延长总线周期。我做过的一个小系统里CPU主频从4.77兆赫兹往上超频到接近6兆赫兹存储器就开始间歇性出错加了一个等待周期后一切恢复正常。这说明时序问题不是理论上的推导而是实测中真实会踩到的坑。4. 6116型存储器测试原理如何证明芯片没毛病4.1 6116的基本结构和引脚功能6116是一块经典的静态RAM芯片容量2K×8位24个引脚的双列直插封装。它的引脚分配很有教学价值11根地址引脚A0到A108根双向数据引脚D0到D7控制引脚包括芯片使能CE、输出使能OE、写使能WE再加上电源VCC和地线VSS。为什么说它教学价值高因为几乎所有存储器接口的基本逻辑都能用这片小芯片讲明白。CE负责“选不选我”OE负责“数据能不能出来”WE负责“是不是要写进去”。芯片不使能时数据引脚处于高阻状态不会干扰总线使能但OE有效时数据引脚输出存储内容WE有效时数据总线上的电平被锁存进选中的单元。双向数据引脚的三态特性是总线型系统能够运行的基础之一。4.2 测试原理写模型、读回、比较6116型存储器的测试原理看起来简单做起来却比想象中复杂。最基本的测试流程是选一个地址写入一个已知数据再读出来与原值比较。一致就说明这个单元的基本读写功能正常不一致则说明该单元存在故障。但芯片测试真正难的地方在于单个单元读写正常不代表所有单元都正常。存储单元矩阵里某条地址线的断路可能让多个行或列的选择同时失效某根数据线的故障会在高阻状态下表现时好时坏。更隐蔽的是相邻单元之间的耦合故障——往一个单元写1会让邻近单元存储的内容意外翻转。这时候就需要更系统的测试算法。4.3 几种典型测试算法的差异覆盖面最基础的是全清零全置一的测试法先把所有单元写0全部读回验证再全部写1全部读回验证。这种方法能查出绝大多数固定型故障但测不出地址译码方面的毛病。稍好一点的做法是走步测试。从地址0开始写0到所有单元然后对第一个单元写1读其他单元确认是0再对第二个单元写1重复这个过程。核心思想是通过移动的测试数据来检查单元之间的干扰。走步测试的时间复杂度大约是O(n平方)级别存储容量增大后非常耗时。更实用的March算法把复杂度降到了线性级别。March算法是一套规定的读写操作序列对每个地址依次执行正序一遍、逆序一遍。例如March C测试就包括了多个“写、读、写、读”的组合能够以线性复杂度覆盖大部分固定故障、跳变故障和耦合故障。做嵌入式产品量产测试时内存自检程序通常就是用这类算法而不是傻乎乎地每个单元逐个测。做个简单的对比测试方法时间复杂度能覆盖的问题不足全0全1法O(n)固定型故障测不出耦合干扰走步测试O(n平方)固定、部分耦合速度慢March算法O(n)固定、跳变、部分耦合实现对新手稍复杂4.4 我的真实测试经历和踩坑记录早年我在实验室里折腾一块6116现象很怪写入0x55再读出来正常写入0xAA读出来也正常但写入0x55之后再立刻写入0xAA再读就会间隙性出现0xFF。排查了很久最后发现不是芯片问题而是我的测试程序里两次写操作之间的时序没有满足写周期的要求。写信号宽度不够导致第二次写入没有真正完成芯片内部还保留着上一轮的数据残影。另一个经验是高阻状态的检查。测试程序里必须有一项是“不使能芯片读总线确认所有数据引脚是高阻”否则如果CE逻辑失效芯片会一直在总线上输出数据其他设备就永远无法使用总线。我见过一位同事排查了整整一下午的“数据冲突”最后发现是译码器的输出引脚虚焊CE电平浮动不定芯片时好时坏。测试存储器时先测控制逻辑再测单元阵列顺序反过来只会浪费时间。5. 存储层次与虚拟存储器只有内存的机器活不下去5.1 寄存器、Cache、主存、外存的速度容量权衡把计算机里的存储资源整合起来看会发现它们构成了一个金字塔。最顶层是CPU内部的寄存器速度跟CPU主频同量级容量却只有几十到几百字节。往下是Cache从一级到三级容量从几十KB到几十MB速度比主存快一个数量级价格也高出许多。再往下是主存也就是DRAM组成的系统内存容量达到GB级别。最底层是SSD和机械硬盘这些外存容量最大速度最慢但掉电不丢数据。为什么计算机不能用几千GB的SRAM当主存因为成本会高到完全无法接受。SRAM的单个存储单元需要六只晶体管DRAM的单元只需要一只晶体管加一个电容。同样的芯片面积DRAM能做的容量大概是SRAM的四倍以上。所以设计师的选择很现实把速度最快的SRAM做成容量有限的Cache配合一个成本可控的大容量DRAM主存再配合更便宜的外存整体性能和成本才有最优平衡。5.2 局部性原理与缓存命中缓存能起作用依赖的不是复杂的硬件魔术而是一条很朴素的经验规律程序在短时间内访问的存储器地址往往集中在很小的范围内。这就是局部性原理。时间局部性指刚刚被访问过的数据很可能再次被访问比如循环体里的变量空间局部性指接近刚刚访问过的地址的数据也很可能被访问比如顺序遍历数组。Cache的工作方式正是基于这个规律CPU请求一个主存地址时先把包含该地址的一块数据整体搬入Cache。后续访问如果命中了缓存行就直接在Cache里完成不需要再访问慢速主存。如果没命中CPU就只能去主存取同时把整块数据更新进Cache希望接下来的访问能踩中局部性。这个设计之所以有效是因为大多数程序的访存行为都极度不均匀少部分数据承担了大多数访问。5.3 虚拟存储与地址变换实模式下怎么把地址算出来虚拟存储器的思想再往前走一步就是要让CPU能用的地址空间比实际物理内存更大。早期的x86实地址模式就是一个最简单的例子它只有20位物理地址但程序员写程序时用的是16位段地址加16位偏移量的组合。物理地址的计算方式是段地址乘以16再加上偏移量得到一个从0x00000到0xFFFFF之间的20位地址。这个“段基址左移四位加偏移”的操作经常让人一头雾水。其实它本质上就是一次分段地址变换两个16位的信息组合出一个20位的物理地址。一个数的16进制段地址0x1234左移一位16进制就变成0x12340加上偏移量0x0008就得到物理地址0x12348。这个过程没什么高深之处它告诉我们所谓的“地址变换”就是一组规范化运算而现代CPU里的MMU做的页式地址变换处理流程更复杂但逻辑结构是类似的逻辑地址经过页表映射换算成物理地址再用物理地址去访问主存。5.4 C语言视角看虚拟存储malloc分配的内存到底在哪写C语言的人天天用malloc却很少有人想过malloc返回的指针指向的地址到底是什么地址。它其实不是物理地址而是进程虚拟地址空间里的一个虚拟地址。操作系统为每个进程划定一块看似连续的虚拟地址空间malloc从这里面划分一块给程序用。真正把数据写入物理内存是在程序访问这个地址时由MMU配合页表完成映射的。所以“虚拟存储器管理”在C语言题目里经常考察的是malloc成功后操作系统只是修改了进程的堆管理数据结构并没有立刻分配物理页框。只有真正写入数据触发缺页异常操作系统才会分配物理页框并建立映射关系。这就是为什么一个大malloc不一定马上让物理内存占用上升写多少才会占多少NULL指针解引用崩溃的本质也是因为页表项不合法MMU触发异常。理解了这条链路你再去读操作系统的内存管理代码会发现每一步都能对上号。6. 存储器的校验数据翻转在现实中确实会发生6.1 奇偶校验低成本方案的真实局限存储器的可靠性没有大多数人想象的那么高。DRAM里的电容可能会因为辐射、温度变化或者信号串扰而丢失电荷导致某个比特悄悄翻转。对普通桌面应用偶发的单比特错误也许只会让程序崩溃一次。对服务器里的数据库、文件系统、科学计算来说一个不可检测的比特翻转可能造成整个文件损坏或者计算结论完全错误。最简单的检测手段是奇偶校验。每8位数据额外附1位校验位让这一组数据加上校验位之后“1”的个数保持奇数或者偶数。读出数据时重新计算校验位与存储的校验位比较不一致就报告出错。它的优点是谁都能做成本极低。局限性也很明显如果一组数据里有两位同时翻转奇偶关系保持不变错误会完全漏检。奇偶校验能告诉你“这组数据可能有问题”但无法告诉你是哪一位出了问题更无法自动修复。6.2 ECC与汉明码的纠错思路想进一步做到自动纠错就需要更聪明的编码方法。汉明码的基本思路是给数据增加多位冗余校验位每一位校验位覆盖数据中特定位置的若干位。错误发生时多个校验位的不一致情况组合起来就能定位出错在具体哪一位对二进制数据来说定位后修正它就是把这一位取反而已。以常见的SEC-DED编码为例它可以纠正单一比特错误同时检测出双比特错误。对于8位数据大约需要5位额外校验位64位数据则通常需要8位额外校验位。现代服务器内存条上那些多出来的颗粒就是在存储ECC校验数据。ECC不只是“检测前提”在我们讨论“存储器的效验”时经常看到的资料把纠错和检测混为一谈。实际工程判断很简单如果系统只需要知道“坏了没”奇偶校验够用如果系统必须在坏一个比特时继续正常运行就要上ECC。6.3 选择建议台式机要不要用ECC普通家用台式机基本不用ECC内存因为游戏和办公场景对单比特错误不敏感多花成本换不到可感知的收益。可一旦机器是用来跑数据库、代码构建、媒体处理之类长时间高负载任务的我的建议很直接能用ECC就上。消费级主板对ECC的支持参差不齐一般需要支持ECC的芯片组加服务器或工作站平台的CPU。采购的时候不仅要确认主板型式和CPU支持的型号还要确定内存条本身是真ECC还是“准ECC”后者往往只是校验没有纠错能力。判断内存是否支持ECC还可以从操作系统层面观察。Linux下执行dmidecode命令可以查看Memory Device条目里的Total Width和Data Width如果Total Width比Data Width大就说明多出来的部分就是校验位。这一步是验证ECC是否真实生效的最直接做法。在我的实际使用体会里存储器是所有计算机部件中最“闷头干活”的一个。你平时感觉不到它的存在可一旦它出错整台机器瞬间瘫痪数据可能一去不返。读懂存储器的组成和连接说到底是为了建立一种直觉容量从哪来速度卡在哪错误怎么防。这种直觉在同系列后续的Cache设计、主存扩展、总线仲裁等内容里还会反复用到。如果你手上正好有块6116或者类似的SRAM芯片强烈建议亲手接一次测试电路把读、写、片选、时序这几件事跑通一遍。踩过时序的坑看过总线冲突的现象再回头看这些基本概念你会突然发现它们全都能串起来了。
返回列表