面灵AI→

华为终端BG 嵌入式软件(存储方向)三轮面经

轮次
三轮技术面+HR面
base
深圳
时间
2026-09
来源
牛客网

《面试题目》

一面(技术面,约 55 分钟)

  1. 栈和堆分别放什么?局部变量一定在栈上吗?
  2. malloc 出来的内存没有释放,进程退出后会怎么样?
  3. memcpy 和 memmove 有什么区别?内存重叠时会发生什么?
  4. volatile 能保证什么?为什么不能拿它做线程同步?
  5. 编译器乱序和 CPU 乱序有什么区别?
  6. 内存屏障是干什么的?什么场景下需要?
  7. 中断上下文里为什么不能睡眠?
  8. 自旋锁和互斥锁怎么选?自旋锁一直拿不到会怎么样?
  9. 驱动里为什么不能直接用普通指针读写寄存器?
  10. 字符设备驱动从 open 到 read,大概会走哪些流程?
  11. 手撕:写一个固定大小的环形缓冲区,不允许动态申请内存,要支持写入、读取,还要能判断空和满。
  12. 追问:如果中断里写、任务里读,怎么保证数据不会乱?中断里能不能拿锁?
  13. 自我介绍,以及挑简历里参与度最高的项目讲一遍。

二面(技术面,约 70 分钟)

  1. NOR Flash 和 NAND Flash 有什么区别?
  2. Flash 为什么写之前要先擦除?
  3. 页、块分别是什么?为什么不能按字节随便擦?
  4. NAND 为什么会有坏块?坏块一般怎么管理?
  5. ECC 是干什么的?能纠正多少位错误由什么决定?
  6. 什么是磨损均衡?静态和动态磨损均衡有什么区别?
  7. 写放大是怎么来的?为什么小数据频繁写特别伤?
  8. MTD、UBI、UBIFS 分别解决什么问题?
  9. 文件调用 write 返回成功,数据就一定进存储介质了吗?
  10. page cache、脏页和后台回写大概是什么关系?
  11. fsync 到底保证了什么?突然掉电还能不能保证数据在?
  12. 日志文件系统为什么更抗异常掉电?
  13. DMA 读写缓冲区时,为什么要考虑 Cache 一致性?
  14. CPU 已经改了缓存里的数据,但 DMA 读到的还是旧数据,怎么处理?

三面(技术面,约 50 分钟:掉电场景现场设计)

  1. 设备每 10ms 产生一条数据,需要持续写入 Flash;存储空间有限,要循环覆盖旧数据;设备可能在任何时刻突然掉电,但重启后要尽量保住最近的数据。你会怎么设计?
  2. 数据先放 RAM 还是直接写 Flash,分别有什么问题?
  3. 小数据频繁写怎么减少擦除次数?
  4. 缓冲区攒满再写,突然掉电时缓存里的数据怎么办?
  5. 怎么判断一条记录是不是完整写入的?
  6. CRC 能发现数据损坏,但怎么判断哪份元数据是最新的?
  7. 索引写一半掉电,重启后怎么恢复?
  8. 双备份元数据如果两份都不一致,信哪一份?
  9. Flash 写满以后怎么回收?回收时又掉电怎么办?
  10. 某个块突然变成坏块,原来的数据怎么迁移?
  11. 重启后不能扫描整块 Flash,怎么把恢复时间压下来?
  12. 看门狗在写入中途复位,和直接断电有什么区别?
  13. 这种掉电场景你准备怎么测试,总不能真靠手拔电源吧?

《参考解析》

环形缓冲区:中断写、任务读怎么做对

固定大小数组加读写索引,不允许动态申请。空满判定常见两种写法:留一个空槽(写指针的下一个位置等于读指针即为满),或者额外维护一个 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 切换指针前后」这些点人为断电或返回错误,逐个验证恢复逻辑;三是每次重启后跑一致性检查(记录能否解析、序列号是否连续、有无重复或丢失),把不通过的断电点和现场记下来复现。面试时能把这三点说出来,比一句「多测几次」有说服力得多。