华为终端 BG 嵌入式(存储方向)三面:从 C 基础追到掉电一致性
- 轮次
- 三面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
一面(约 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,怎么把恢复时间压下来?
- 看门狗在写入中途复位,和直接断电有什么区别?
- 这种掉电场景你准备怎么测试,总不能真靠手拔电源吧?
《参考解析》
一面:先把「未定义行为」讲清楚。 栈放函数调用上下文、局部变量、返回地址,堆放动态申请;局部变量通常在线程栈上,但被 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 条完整、无错乱、能继续写」,跑几千次统计丢数据上界。