面灵AI→

华为终端 BG 嵌入式(存储方向)三面:从 C 基础追到掉电一致性

轮次
三面
时间
2026-10
来源
牛客网

《面试题目》

一面(约 55 分钟)

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

二面(约 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 分钟)

场景:设备每 10ms 产生一条数据,需要持续写入 Flash;存储空间有限,要循环覆盖旧数据;设备可能在任何时刻突然掉电,但重启后要尽量保住最近的数据。请现场设计。

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

《参考解析》

一面:先把「未定义行为」讲清楚。 栈放函数调用上下文、局部变量、返回地址,堆放动态申请;局部变量通常在线程栈上,但被 static 修饰、被优化进寄存器、或是编译器分配的对象时不一定。malloc 不释放,进程退出后操作系统会回收整个地址空间,所以「不会泄漏到系统」,但长时间运行的服务会持续增长;更危险的是嵌入式/内核里没有进程退出这层兜底。memcpy 不处理重叠,重叠时用 memmove(正向/反向拷贝),否则结果依赖实现。

volatile 只保证每次访问都真的读写内存,不保证原子性和顺序;线程同步要用锁/原子操作/内存屏障。编译器乱序是编译优化重排指令,CPU 乱序是处理器动态调度,两者都可能让「代码顺序」与「内存可见顺序」不一致;内存屏障(smp_mb/rmb/wmb、__DMB/__DSB)用来约束这两层重排。中断上下文不能睡眠,因为中断栈没有任务上下文、无法被调度器安全挂起,睡眠会导致系统卡死;自旋锁在中断里可用但必须关中断/关抢占,一直拿不到会烧 CPU,临界区要极短;互斥锁会睡眠,不能用在中断。寄存器必须用 volatile 指针 + readl/writel 这类带屏障的访问器,普通指针可能被优化、合并或跨架构出错。

字符设备从 open 到 read:VFS 根据 inode 找到 cdev 的 file_operations,open 里初始化私有数据、申请资源;read 经 VFS 调到驱动的 read,驱动用 copy_to_user 把内核缓冲区数据拷给用户态,返回实际字节数;阻塞式驱动会挂等待队列,由中断唤醒。手写环形缓冲区时,核心是 head/tail 两个索引、size 为 2 的幂时用位掩码取模、空满判断(留一个空位或单独计数);单生产者单消费者场景下,写者只改 head、读者只改 tail,配合 volatile 和 acquire/release 屏障即可无锁;多生产者/多消费者才需要锁或原子 CAS。

二面:存储的答案要落到「物理限制 → 软件补偿」。 NOR 支持随机读、按块擦除,适合代码存储;NAND 按页读写、按块擦除,密度高、有坏块、需要 ECC,适合大容量数据。Flash 写之前必须擦除,是因为编程只能把位从 1 写成 0,擦除才能恢复为 1;擦除粒度是块,所以不能按字节擦。NAND 出厂和使用中都会产生坏块,靠坏块表标记、跳过、预留备用块;ECC 用冗余校验位纠错,能纠多少位由 ECC 算法强度和 OOB 空间决定(如 BCH 4/8/16 bit per 512B),超过纠错能力只能报不可纠。

磨损均衡是把擦写分散到全盘:动态只搬移空闲块,静态还会搬移冷数据,避免某些块先坏;写放大来自「改一页要擦一块、GC 要搬有效页」,小数据频繁写会放大得最厉害。MTD 是内核里对裸 Flash 的抽象,UBI 在 MTD 上做坏块管理、磨损均衡、逻辑卷,UBIFS 是跑在 UBI 上的文件系统。write 成功只代表数据进了 page cache,脏页由后台回写;fsync 才把文件数据和必要的元数据刷到稳定存储,但如果设备自带写缓存,还要发 flush 命令,否则掉电仍可能丢。日志文件系统先把元数据变更写日志再提交,掉电后重放/回滚,保证一致性。DMA 与 CPU 的 Cache 不一致,是因为 DMA 绕过 CPU 直接读写内存;要在传输前 clean(CPU 写的数据刷到内存)、传输后 invalidate(丢掉 CPU 里的旧缓存),或者把缓冲区设为非 cache、使用一致性 DMA API。

三面:方案的主线是「追加写 + 提交标记 + 双元数据 + 分段回收」。 10ms 一条数据,直接写 Flash 会导致每写一次就擦一块,必须先在 RAM 里攒批(比如 1~4KB 一页),攒满或定时刷;掉电时 RAM 里的数据会丢,所以要么接受「最多丢一个批次」,要么加超级电容/掉电检测,在断电瞬间把 RAM 刷下去。每条记录带 seq + len + payload + CRC,写入顺序是「先写数据页,再写提交标记」,重启时只认 CRC 正确且提交标记完整的记录,尾部半条丢弃。

元数据用两个 bank 轮流写,每个 bank 带 version + CRC,启动时选版本更大且 CRC 正确的那份;两份都不一致时,选「能解析出最后一个完整提交点」的那份,宁可回退一点也不能挂。索引也走追加写,索引项带指向数据页的物理地址和序号;恢复时从最后一个有效超级块开始,顺序扫描少量页找到最后提交点,不需要全盘扫。空间满了做 GC:选无效页最多的块,把有效页搬到新块,写新映射,最后才擦旧块;GC 中途掉电,旧块还在、新块未提交,重启后按映射回退。坏块出现时,把有效数据迁到预留块,更新坏块表和映射。恢复时间靠「固定超级块 + 日志式索引 + 预留扫描窗口」压到毫秒级。

看门狗复位和断电不完全一样:复位时外设可能已复位但 Flash 写操作可能仍在进行,复位后要按「未完成写」处理;断电则可能连缓存都丢了,所以两者都要做掉电注入测试,但断言不同。测试不能靠手拔电源,要用可编程电源/继电器做随机掉电,覆盖写入、擦除、GC、元数据提交各个窗口,每次重启后校验「最近 N 条完整、无错乱、能继续写」,跑几千次统计丢数据上界。