华为终端BG 嵌入式软件(存储方向)三轮面经
- 轮次
- 三轮技术面+HR面
- base
- 深圳
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
一面(技术面,约 55 分钟)
- 栈和堆分别放什么?局部变量一定在栈上吗?
malloc出来的内存没有释放,进程退出后会怎么样?memcpy和memmove有什么区别?内存重叠时会发生什么?volatile能保证什么?为什么不能拿它做线程同步?- 编译器乱序和 CPU 乱序有什么区别?
- 内存屏障是干什么的?什么场景下需要?
- 中断上下文里为什么不能睡眠?
- 自旋锁和互斥锁怎么选?自旋锁一直拿不到会怎么样?
- 驱动里为什么不能直接用普通指针读写寄存器?
- 字符设备驱动从
open到read,大概会走哪些流程? - 手撕:写一个固定大小的环形缓冲区,不允许动态申请内存,要支持写入、读取,还要能判断空和满。
- 追问:如果中断里写、任务里读,怎么保证数据不会乱?中断里能不能拿锁?
- 自我介绍,以及挑简历里参与度最高的项目讲一遍。
二面(技术面,约 70 分钟)
- NOR Flash 和 NAND Flash 有什么区别?
- Flash 为什么写之前要先擦除?
- 页、块分别是什么?为什么不能按字节随便擦?
- NAND 为什么会有坏块?坏块一般怎么管理?
- ECC 是干什么的?能纠正多少位错误由什么决定?
- 什么是磨损均衡?静态和动态磨损均衡有什么区别?
- 写放大是怎么来的?为什么小数据频繁写特别伤?
- MTD、UBI、UBIFS 分别解决什么问题?
- 文件调用
write返回成功,数据就一定进存储介质了吗? - page cache、脏页和后台回写大概是什么关系?
fsync到底保证了什么?突然掉电还能不能保证数据在?- 日志文件系统为什么更抗异常掉电?
- DMA 读写缓冲区时,为什么要考虑 Cache 一致性?
- CPU 已经改了缓存里的数据,但 DMA 读到的还是旧数据,怎么处理?
三面(技术面,约 50 分钟:掉电场景现场设计)
- 设备每 10ms 产生一条数据,需要持续写入 Flash;存储空间有限,要循环覆盖旧数据;设备可能在任何时刻突然掉电,但重启后要尽量保住最近的数据。你会怎么设计?
- 数据先放 RAM 还是直接写 Flash,分别有什么问题?
- 小数据频繁写怎么减少擦除次数?
- 缓冲区攒满再写,突然掉电时缓存里的数据怎么办?
- 怎么判断一条记录是不是完整写入的?
- CRC 能发现数据损坏,但怎么判断哪份元数据是最新的?
- 索引写一半掉电,重启后怎么恢复?
- 双备份元数据如果两份都不一致,信哪一份?
- Flash 写满以后怎么回收?回收时又掉电怎么办?
- 某个块突然变成坏块,原来的数据怎么迁移?
- 重启后不能扫描整块 Flash,怎么把恢复时间压下来?
- 看门狗在写入中途复位,和直接断电有什么区别?
- 这种掉电场景你准备怎么测试,总不能真靠手拔电源吧?
《参考解析》
环形缓冲区:中断写、任务读怎么做对
固定大小数组加读写索引,不允许动态申请。空满判定常见两种写法:留一个空槽(写指针的下一个位置等于读指针即为满),或者额外维护一个 count 计数。中断里写、任务里读是典型的单生产者单消费者(SPSC)场景,正确做法是不加锁:读索引只有任务在改、写索引只有中断在改,用 volatile 防止编译器把它优化进寄存器,再在「写完数据」和「更新写索引」之间加写屏障(如 smp_wmb()),读侧对应加读屏障,保证消费者不会先看到新索引、后看到数据。中断里能不能拿锁要分情况:自旋锁如果在任务上下文持有、又没有关中断,中断里再抢同一把锁就可能死锁;互斥锁会睡眠,中断上下文根本不能用。所以中断与任务共享数据时,要么设计成无锁 SPSC,要么在任务侧关中断保护临界区。
volatile、编译器乱序与 CPU 乱序
volatile 只保证每次访问都真的去内存读写、不被优化掉,它不提供原子性,也不提供顺序保证,所以既不能替代锁,也不能替代内存屏障。编译器乱序是编译期重排指令,CPU 乱序是运行期硬件为了填满流水线做的重排,两者都可能让别的核观察到与代码顺序不一致的写入顺序。屏障的作用是划出可见性与顺序的边界:写屏障保证它之前的写对其他核先可见,读屏障保证先读到别人在它之前写的值。典型场景就是上面的 SPSC 队列、双缓冲切换,以及 DMA 那种「先写描述符内容、再写启动位」的顺序敏感操作。
Flash 的读写擦粒度、坏块与 ECC
NOR 支持按字节随机读、读取速度快,适合存代码(可 XIP);NAND 按页读写、按块擦除,单位容量成本低、密度高,适合存数据,但只能整块擦。写之前必须先擦,是因为浮栅单元的编程只能把位从 1 变 0,只有擦除才能把整块恢复成全 1。擦除粒度远大于写粒度(常见页 2KB/4KB、块 128KB 以上),所以无法按字节擦。NAND 出厂和使用过程中都会出现坏块,管理方式是出厂坏块标记 + 预留块池替换 + 运行时坏块表,上层通过坏块管理把逻辑块映射到可用物理块。ECC 用来纠正随机位翻转,能纠几位取决于算法和校验位长度(BCH / LDPC 的强度、OOB 区留多少字节给 ECC),不是一个固定值。磨损均衡分动态和静态:动态只在新写入时挑擦除次数少的块,静态还会主动搬移长期不动的冷数据,避免冷块一直不被擦、热块先坏。写放大的根源是「改一小段要先擦一大块、再把整块搬走重写」,所以日志式追加、攒批写入、减少元数据更新频率都能降低写放大。
write 返回成功不等于落盘
write 只是把数据从用户态拷进内核 page cache,返回成功表示「内核接受了」,此时数据还在内存里,是脏页,由后台回写线程按 dirty_expire_centisecs、dirty_writeback_centisecs 等参数择机刷盘。要真正下发到设备,得调用 fsync / fdatasync,或者以 O_SYNC / O_DSYNC 打开文件。即使 fsync 返回成功,也只保证到这个文件的数据和必要元数据已经下发到设备,设备内部缓存是否刷完取决于它是否遵循 cache flush / FUA 语义。对掉电敏感的系统,要在记录里带 CRC 和单调序列号,重启后能判断哪条完整、哪条写了一半。日志文件系统(ext4 的 journal、F2FS 等)之所以抗掉电,是先通过日志或检查点记录「将要做什么」,再原地更新,掉电后按日志重做或回滚,把半更新的元数据拉回一致状态。
掉电场景设计:循环覆盖 + 随时断电
核心思路是把「一直原地覆盖」换成「日志式追加 + 逻辑指针提交」:
- 把 Flash 划成若干等长块当作环形日志区,每条记录 = 固定头部(magic、长度、序列号、CRC)+ 载荷;只有头部和载荷都写完、CRC 校验通过,这条记录才算有效。
- 每 10ms 来的数据先攒在 RAM 缓冲里,按页大小或攒够若干条成批写,写完再推进写指针,这样能明显减少擦除次数。
- 断电会丢掉 RAM 里还没落盘的部分,这属于需要向业务方明确的损失范围;关键是能识别「写到一半」的记录:靠单调递增的序列号 + CRC,重启后从尾部往前找最后一条完整记录即可。
- 元数据(当前写指针、最新记录位置)双份交替写,各带序列号和 CRC;重启时两份都有效就取序列号大的,都不一致就取 CRC 通过且序列号较大的那份,再退一步就沿记录链回溯重建。
- 空间回收时先挑擦除次数最少的块,把其中有效记录复制到新块并落盘,全部写完后才切换指针、最后擦旧块;回收过程中掉电,因为指针还没切换,旧块依然有效,重启最多重做一次回收。
- 恢复时间不能靠扫全片:每块头部保存「本块记录序号范围、是否有效」的摘要,或者持久化一个最后写入位置,恢复时只扫最后几个块和指针附近区域,把恢复时间压到与总块数无关的量级。
- 坏块处理:读写失败或擦除超时就标记该块、从预留池补一块,并把上面的有效记录迁移过去;迁移写入同样要带 CRC,避免搬一半的数据被当成有效。
- 看门狗复位和断电并不完全一样:复位可能留下尚未刷完的设备缓存,所以恢复路径要等价于掉电恢复,不要假设内存或缓存里的数据还在。
掉电测试怎么做
手拔电源不可重复、也覆盖不到关键窗口。工程上的做法有三类:一是用可编程电源或继电器做自动上下电循环,让设备反复经历「随机时刻断电 + 重启校验」,跑成千上万次;二是在写路径上做故障注入,在「擦除中、写完记录头部后、写完载荷一半、两份元数据之间、GC 切换指针前后」这些点人为断电或返回错误,逐个验证恢复逻辑;三是每次重启后跑一致性检查(记录能否解析、序列号是否连续、有无重复或丢失),把不通过的断电点和现场记下来复现。面试时能把这三点说出来,比一句「多测几次」有说服力得多。