
做产测和嵌入式Linux的朋友一定遇到过那种让人后背发凉的问题流水线上的自动化老化测试脚本跑得飞快看板上十几个设备整整齐齐全是PASS厂长都准备签字放货了。结果这批设备刷完正式系统一开机操作系统读关键量产数据分区拿到的居然是一整片0x00——序列号没了、MAC地址没了、校准参数也全没了。脚本说PASSOS读全零到底是谁在撒谎今天聊的就是这个场景。我最近刚帮朋友排查过这种批量性问题从产测脚本、eMMC存储、OS驱动到刷机流程整个链路翻了个底朝天最后定位到的根因跟谁在撒谎其实都不太一样。这篇文章不只是讲解决思路更想帮大家建立一个理念PASS只是某个时刻的状态不是整个生命周期的承诺。如果你在做设备量产、老化测试、NAS刷机折腾或者嵌入式BSP相关的工作这篇内容应该能帮你省掉好几个通宵。1. 现场还原老化测试全绿正式OS读出一整片空白1.1 这批设备的完整经历朋友这批货是一批基于eMMC存储的嵌入式Linux小主机类似那种改了散热、加了老化测试工序的NAS盒子硬件配置不高但软件流程走得很完整。量产流程大致是先贴片焊接然后在工厂烧录基础引导固件和底层Bootloader接着通过产测软件往eMMC指定分区写入量产信息——包括设备序列号、MAC地址、无线校准参数、产测日期都会集中放在一个专门的数据分区里。写入完成后产测系统会调用一个全自动老化测试脚本跑一轮压力测试脚本会对网口、串口、温度、存储读写做循环检测。每台设备测试完成如果所有检测项都通过就会在产测记录里打一个PASS并记录当时的测试时间、操作员和测试版本号。朋友这批总共几十台设备老化测试脚本全部报PASS一条FAIL都没有。1.2 矛盾的第一现场真正的问题发生在这批设备刷入正式OS之后。刷完系统设备开机自检负责初始化的启动脚本需要去量产数据分区读序列号和MAC地址来配置网络接口结果读出来的数据乱套了。用命令行进去一看整个量产分区全是0x00用hexdump读出来是00000000 0000 0000 0000 0000 0000 0000 0000 0000 |................| 00000010 0000 0000 0000 0000 0000 0000 0000 0000 |................| 00000020 0000 0000 0000 0000 0000 0000 0000 0000 |................|类似这样的内容从头到尾几百个扇区都是零干净得像刚被安全擦除过一样。操作系统里对应的设备节点还在——比如 /dev/mmcblk0p7 这种——设备节点没有消失但读出来的内容就是一片空白。不只是个别设备这批设备里大部分都是这个表现。这就不是单板问题了而是整个批次的系统性问题。更让人费解的是产测脚本明明刚刚验证过这些数据还打了PASS怎么到了OS启动时就成了全零呢。1.3 为什么这个矛盾让人很难接受这种矛盾之所以让人头疼是因为两边看起来都很有道理。产测脚本给出的PASS是真实发生的它在测试那一刻确实从eMMC里读回了正确的数据而且做了比对并没有凭空捏造结果。OS这边也没有撒谎它确实是按正常方式通过eMMC驱动去读物理块设备读回来的内容就是0x00。两边都按自己的逻辑执行完毕但结果却是完全对立的。这种两边都合理却互相矛盾的现场最忌讳的就是急着下结论。我见过不少人上来就说肯定是脚本误判也见过人直接断定OS驱动有问题结果折腾半天都没找到方向。正确做法是顺着链路一层层拆先确认脚本的PASS到底证明了什么再分析OS读取路径里可能存在哪些坑。2. 第一怀疑对象产测脚本的PASS到底证明了什么2.1 复盘脚本的判定逻辑出了问题第一个被拉出来拷问的肯定是产测脚本。我把朋友那份老化测试脚本翻出来看了一遍发现它的存储校验部分长这样向量产分区写入SN和MAC然后等待一段时间再从同一个分区读出来拿读到的字符串和写入的字符串做比对一致就输出PASS。表面上看逻辑没什么问题写入、读回、比对标准的三段式校验。但这里藏着一个非常关键的盲区这个读回比对是在写入之后立刻做的中间几乎没有时间间隔。也就是说它验证的是写入瞬间之后那一小段时间内数据还能读出来而不是数据能不能长期保持。老化测试前面跑了几十分钟的压力负载到存储校验这一步eMMC内部可能还处在热状态电荷还没有明显流失弱块的问题根本来不及暴露。2.2 脚本PASS的三个隐蔽前提第一脚本读取到的数据可能来自eMMC内部的页面缓冲器page buffer而不是真正已经从电荷存储单元里稳定下来的内容。操作系统的块设备层有缓存eMMC控制器本身也有缓存如果你写入之后立即读回完全可能读到的是缓存里的副本压根没碰到物理介质。尤其是脚本里没有做sync和重新打开设备的操作时这种假读回非常常见。第二脚本只比对了一部分关键字段并没有做全量数据校验。如果产测程序只校验SN和MAC两个字符串那么其他区域的校准参数就算已经完全损坏脚本也发现不了照样PASS。第三脚本对eMMC命令的返回状态检查得不够严格只要Linux块设备层没报错就默认写入成功了可某些弱块的写入失败是被控制器静默吞掉的。2.3 一个反直觉的结论所以第一轮排查下来结论可能和很多人的直觉相反产测脚本并没有故意造假它在测试那一刻确实读到了正确的数据PASS是真实有效的。但真实有效不代表可靠它只证明了当时那一刻的状态压根没证明几十小时高温老化之后数据还在不在。这一点非常关键。脚本的PASS定义了整个测试流程的验收标准如果标准本身只测瞬时读写不测数据保持那PASS就是一个很脆弱的结论。就好比你在冰箱里放了一块肉放进去的时候拿出来看了一眼是好的你写了个PASS但你不能因此就保证肉在冰箱里放一个月之后还能吃——你应该做的是等到一个月之后再看一眼。3. 第二怀疑对象OS读全零的背后是访问方式还是介质本身3.1 先排除访问路径问题确认脚本那边有测试覆盖短板之后不能直接就把锅扣到eMMC头上还得先看看OS这边是不是读错了地方。排查OS读取全零第一步永远不是怀疑硬件而是确认系统访问的确实是你以为的那个物理地址。这一步要用通用的命令来做我当时的排查顺序是这样lsblk # 确认分区表看 /dev/mmcblk0p7 是不是量产数据分区 fdisk -l /dev/mmcblk0 # 打印分区起始扇区记录下量产数据分区的偏移量 dd if/dev/mmcblk0 of/tmp/sn_raw.bin bs512 count8 skip偏移扇区数 # 绕过分区节点直接从物理块设备读取数据 hexdump -C /tmp/sn_raw.bin # 确认物理层读出来的内容这套命令的意义在于跳过文件系统和分区解析直接从块设备层面读。如果物理块设备读出来全零那就说明问题在物理层以下而不是文件系统挂载方式的问题。还要顺手对比一下脚本当时写入的起始地址和OS这边读取的偏移量万一产测脚本是以绝对地址写入而OS是按分区表偏移读取两者错位了也会出现读不到数据的现象。3.2 检查eMMC的健康状态与内核报错物理层读出全零之后接下来要看eMMC的健康状态。Linux内核在驱动层会把很多问题记录下来先看dmesg里有啥dmesg | grep -i mmc dmesg | grep -i error如果看到类似 mmc0: error -110 或者 timeout 这样的输出说明控制器和卡之间的通信已经出现了超时妥妥的硬件问题信号。如果没有明确报错就要看eMMC自身的寿命信息。很多eMMC支持读取扩展寄存器我用mmc-utils看过mmc extcsd read /dev/mmcblk0 # 查看 Device life time estimation、Pre EOL information 等字段在 /sys/class/mmc0/mmc0:0001/ 下面也能看到不少属性。老化测试跑过高温高强度读写之后eMMC的寿命计数器一般都会有明显变化如果接近或者超过寿命线基本可以锁定存储介质本身老化。3.3 数据线损伤这个分支别忽略在排查过程中还有一个分支特别容易被忽略eMMC的数据线如果出现开路、虚焊或者对地短路读取时也会表现为全零或者固定值。这种问题通常是批量焊接工艺导致的比如钢网漏印、锡珠连锡或者BGA虚焊。区分介质损坏和数据线问题有个很直接的土办法重新把eMMC拆下来在编程器上读取或者换一块已知良好的主板来读同一片eMMC。如果换到好主板上能读出正常数据那问题就在主板的eMMC外围电路如果依然全零那才是介质本身的数据丢失。朋友这批设备后来抽样做了一次拆片读取结果在编程器上读出来的内容和OS读出来的一样全是零——这就把数据线问题排除了。4. 根因定位弱块在高温老化之后叛变测试时机决定了悲剧4.1 eMMC读全零的底层原因锁定了介质之后就得回到NAND Flash本身的物理特性上来解释为什么好好的数据会变成全零而不是抛出一个清晰的错误。NAND Flash里的每个存储单元靠电荷来记忆数据多级单元MLC/TLC在一个单元里存2个或3个bit的信息靠的是精确控制电荷的多少。数据写进去之后电荷会随着时间缓慢流失温度越高流失越快这个现象叫数据保持Data Retention衰减。当电荷流失到一定程度读取时单元状态的判定就会超过纠错码ECC的纠正能力。eMMC控制器在读出数据之后会做ECC校验如果发现错误超过可以纠正的bit数正常的做法是上报读错误。但很多eMMC控制器在检测到不可纠正错误时并不会总是返回一个标准的Error Code——有的实现会直接把数据缓冲区的内容返回给主机而缓冲区在读取失败时默认就是0x00。这就造成了读全零的现象而不是读失败。4.2 弱块和坏块的区别Flash存储里经常讲的坏块其实分两种。一种是出厂就有的坏块出厂时会做标记eMMC控制器出厂时就知道这些块不能用。另一种是使用过程中产生的块叫增长坏块其中最危险的是那些还没完全坏掉、但已经处于随时可能叛变状态的弱块。弱块的特点是写的时候能写进去读的时候短期内能读出来功能性测试全过但电荷保持能力已经大幅下降。高温老化测试就是弱块的催命符——在80度甚至更高的温度下跑几十个小时弱块里的电荷会加速流失写进去的1慢慢漂移成0最终OTL读出来就是一整片空白。这批设备里好几台机器在出厂前老化测试的时候数据还是完好的等老化结束、降温、刷完正式OS再读数据已经蒸发了。时间线拉直了看就非常清楚写入数据是在老化之前PASS也是在老化之前而OS读数据是在老化结束之后——中间隔了整整一个高温老化环节恰恰是测试设计没有覆盖到的环节悲剧就是这么发生的。4.3 到底是谁在撒谎把整条链路拉出来每个人其实都没撒谎。产测脚本读回的时候数据是真的对的OS读的时候数据也真的是空的eMMC控制器返回全零也是它的既定行为。真正撒谎的是测试设计本身——你只测了写入后即刻的读回并没有测老化之后的数据保持性于是大家各说各话制造出了这个看起来互相矛盾的现场。环节它做了什么它是否撒谎真实情况老化测试脚本写入后立即读回比对输出PASS没撒谎只验证了瞬时读写没有验证数据保持eMMC控制器读到不可纠正错误时返回全零缓冲没撒谎内部行为但表现得很像数据正常OS文件系统按正常流程读取分区返回0x00没撒谎看到的就是物理层读取的结果测试设计用老化前的PASS代表老化后的状态撒了谎PASS的定义没覆盖数据保持这个维度5. 修复与加固从脚本、系统到产线流程的一次全面整改5.1 脚本侧把PASS从瞬时状态升级为持续可靠经历了这次教训朋友那边的产测脚本做了几处硬性改动。第一存储校验从写入后立即读回比对改成写入后sync、卸载分区、重新挂载、再次打开设备读取确保读到的数据穿过所有缓存层真正来自物理介质。这一步一行命令就能实现但很多人容易忽略sync。第二校验范围从比对SN和MAC字符串升级为对整个量产数据分区做CRC32全量校验并且把校验值也写进一个固定区域。每次读回时重新计算CRC比对不通过直接FAIL绝不放过任何一个字节的变化。第三增加重试机制比如同一个数据块读五次超过一半的结果一致才算通过用来过滤偶发性读错误。sync # 先把缓存里的数据强制刷到物理介质 dd if/dev/mmcblk0p7 of/tmp/data.img bs512 count1024 # 重新读回整个量产数据分区 crc32 /tmp/data.img # 和写入时保存的基准CRC做比对不一致就FAIL最关键的一条改动是在老化测试结束之后、设备下线之前增加一道老化后复测环节。脚本不只是在前段跑PASS而是要求老化完成且温度回落到正常范围之后再完整读一次量产数据分区做CRC校验通过才算整台设备真正PASS。这道关口一加弱块几乎无处遁形。5.2 OS侧启动自检与关键数据冗余光靠产测脚本防还不够设备发给用户之后保不齐存储介质后续还会继续退化。OS层面的加固也要跟上。朋友的方案里启动脚本增加了一个自检逻辑系统起来之后先对量产数据分区做CRC校验校验失败就亮故障灯同时尝试读取备份数据区。关键量产信息不再只写一份而是写入两个不同的物理区域一份为主数据一份为备份启动时先读主数据CRC不过就自动切备份并且打印告警日志。同时他们在内核日志层面加了监控如果dmesg里反复出现CRC错误或者超时错误就会把这些信息上报到运维平台形成一条量化指标。这样数据区不是等到完全损坏才暴露而是刚出现苗头时就能被监控捕捉到。对用户设备来说只要还有备份区可以切业务就不会中断。5.3 产线流程与选型提前给数据保持留出余量这次事件最根本的教训还是在产线流程选型环节。采购eMMC时不能只看容量和价格要重点关注数据保持特性尤其是高温环境下的保持能力。老化测试设计要走先写数据、后烤机、烤完再验证的顺序也就是把最关键的数据放到老化之前写入让数据独自承受一次完整的高温考验那些弱块就会在出厂前原形毕露。另外还有一个很实用的技巧关键数据写两遍放到不同的物理区域。针对NAND这种弱块总会率先出现在某些损害较重的block上的特性一次写入触发坏块重映射后第二次写入往往会被分配到另一个block两个block同时坏掉的概率会低很多。有些方案甚至会把数据写到三个不同的位置做三冗余这在eMMC老化严重的二手设备上特别实用刷机爱好者如果折腾老矿渣NAS这条建议一样值得参考。6. 复测与复盘这套方案到底拦下来多少问题整改之后朋友那边又跑了一批同样的设备。老化前写入量产数据并计算CRC老化结束后复测时之前那种全零的问题果然在大约一成左右的设备上出现了全部被复测环节成功拦截没有一台带着坏数据流到刷机环节。这个结果完全印证了之前的根因判断——问题的确不是单块硬件彻底坏死而是弱块在高温老化过程中批量失守。更让我觉得值得写出来的是排查过程中的体验。遇到脚本说PASS、OS读全零这种矛盾最忌讳的是在脚本和OS之间二选一、急着站队。正确的方式是像破案一样把每个环节的视界都摸清楚——脚本看到了什么OS看到了什么介质在那个时刻做了什么然后你才能知道真正有空档的是哪一环。最后分享一个小建议这个问题排查完我直接在朋友的产测脚本里加了一行最终校验的shell判断逻辑很简单老化完成后读一次量产分区用cksum比对基准值不一致就标记为FAIL并禁止进入下一道工序。一行命令而已却能直接把瞬时PASS变成持久可用的验收标准。测试的本质不是跑通流程而是覆盖那些不显眼却很致命的时间窗口。