华为 终端 BG 嵌入式软件面经:三轮技术面,存储与掉电异常路径深挖
- 轮次
- 多轮面试合集
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
一面 技术面(约 55 min)
- 自我介绍,然后挑简历里参与度最高的项目讲。
- 栈和堆分别放什么,局部变量一定在栈上吗?
- malloc 出来的内存没有释放,进程退出后会怎么样?
- memcpy 和 memmove 有什么区别,内存重叠时会发生什么?
- volatile 能保证什么,为什么不能拿它做线程同步?
- 编译器乱序和 CPU 乱序有什么区别?
- 内存屏障是干什么的,什么场景下需要?
- 中断上下文里为什么不能睡眠?
- 自旋锁和互斥锁怎么选,自旋锁一直拿不到会怎么样?
- 驱动里为什么不能直接用普通指针读写寄存器?
- 字符设备驱动从 open 到 read,大概会走哪些流程?
- 手撕:写一个固定大小的环形缓冲区,不允许动态申请内存,要支持写入、读取,还要能判断空和满。
- 追问:如果中断里写、任务里读,怎么保证数据不会乱?中断里这个锁能不能拿?
二面 技术面(约 70 min)
- 挑项目里的存储部分直接提问。
- NOR Flash 和 NAND Flash 有什么区别?
- Flash 为什么写之前要先擦除?
- 页、块分别是什么,为什么不能按字节随便擦?
- NAND 为什么会有坏块,坏块一般怎么管理?
- ECC 是干什么的,能纠正多少位错误由什么决定?
- 什么是磨损均衡,静态和动态磨损均衡有什么区别?
- 写放大是怎么来的,为什么小数据频繁写特别伤?
- MTD、UBI、UBIFS 分别解决什么问题?
- 文件调用 write 返回成功,数据就一定进存储介质了吗?
- page cache、脏页和后台回写大概是什么关系?
- fsync 到底保证了什么,突然掉电还能不能保证数据在?
- 日志文件系统为什么更抗异常掉电?
- DMA 读写缓冲区时,为什么要考虑 Cache 一致性?
- CPU 已经改了缓存里的数据,但 DMA 读到的还是旧数据,怎么处理?
三面 技术面(约 50 min)
- 场景设计:设备每 10 ms 产生一条数据,需要持续写入 Flash;存储空间有限,要循环覆盖旧数据;设备可能在任何时刻突然掉电,但重启后要尽量保住最近的数据。
追问:
- 数据先放 RAM 还是直接写 Flash,分别有什么问题?
- 小数据频繁写怎么减少擦除次数?
- 缓冲区攒满再写,突然掉电时缓存里的数据怎么办?
- 怎么判断一条记录是不是完整写入的?
- CRC 能发现数据损坏,但怎么判断哪份元数据是最新的?
- 索引写一半掉电,重启后怎么恢复?
- 双备份元数据如果两份都不一致,信哪一份?
- Flash 写满以后怎么回收,回收时又掉电怎么办?
- 某个块突然变成坏块,原来的数据怎么迁移?
- 重启后不能扫描整块 Flash,怎么把恢复时间压下来?
- 看门狗在写入中途复位,和直接断电有什么区别?
- 这种掉电场景你准备怎么测试,总不能真靠手拔电源吧?
《参考解析》
-
先看清三轮的分工,复习的重点跟着变:一面查基础(C、操作系统、驱动),二面往存储底层挖,三面基本不给背题的机会——面试官抓着一个异常场景一直追,你说加锁他问突然掉电怎么办,你说落盘他问设备缓存没刷完怎么办。所以备考时不能只背 Flash、文件系统这些定义,真正拉开差距的是异常路径:写到一半怎么办、掉电怎么办、数据不一致怎么办。每准备一个机制,都顺手想一遍「它在哪一步会被打断、打断后怎么恢复」。
-
一面的 C 与并发细节要答到点上:局部变量不一定在栈上——被优化进寄存器、加 static 进静态区、或者被编译器提升位置都可能;堆上是 malloc 出来的对象,生命周期由程序管理;栈是自动分配、连续、有大小上限。malloc 不释放时进程退出,操作系统会回收全部地址空间,所以「进程退出会不会泄漏」的答案是:单次进程退出不会,但长生命周期进程里反复申请不释放会持续增长,服务类程序必须靠 RAII 或显式释放。memcpy 与 memmove 的差别只在重叠场景:memcpy 不保证重叠正确(实现可能按字批量拷贝导致数据被覆盖),memmove 会判断方向做前向或后向拷贝。
-
乱序、屏障与中断上下文这组题是驱动岗的筛选线:编译器乱序是编译期指令重排(受语言内存模型与 volatile 约束),CPU 乱序是运行期的动态调度与缓存可见性,两者都可能让多核或 DMA 场景看到不一致的中间状态,所以需要内存屏障(编译屏障约束编译器,硬件屏障保证读写顺序与可见性)。中断上下文不能睡眠,是因为它没有可被调度挂起的任务上下文——一旦睡下去就没有人来唤醒调度它,内核会直接报错,所以中断里只能用自旋锁、原子操作和 FromISR 类接口。自旋锁一直拿不到会持续占用 CPU,单核上还会因持锁者无法被调度而死锁,因此临界区要极短;互斥锁可睡眠、适合可能长时间持有的场景。
-
驱动读写寄存器必须用带 volatile 语义的访问函数:普通指针读写在编译期可能被优化掉或合并,运行期还可能因乱序导致读写顺序与预期不符,位域写法在不同编译器下布局也不一致。正确做法是 ioremap 映射后使用 readl/writel 之类的访问器(它们内含屏障语义),并注意端序与位操作的可读性。字符设备从 open 到 read 的主链是:用户态系统调用进入 VFS,VFS 按设备号找到 cdev 并调用注册的 file_operations,open 完成私有数据初始化,read 从内核缓冲区把数据 copy_to_user 回用户空间;这条链路里错误返回、并发保护和用户指针校验都是考点。
-
环形缓冲区的追问是这一面最见功底的地方:基础版用 head、tail 两个索引加容量取模,判断空用 head == tail、判断满通常留一个空位((tail+1)%size == head)或用计数变量,注意不能用动态内存。追问「中断里写、任务里读」时,标准答案是识别出这是单生产者单消费者场景,可以不加锁:读写索引各自只被一方修改,用 volatile 保证不被优化,用编译屏障或读写屏障保证索引更新与数据写入的顺序(先写数据再更新索引,读侧先读索引再读数据),并让索引宽度与访问是原子的;中断里绝对不能拿互斥锁(会睡眠),用自旋锁也要评估关中断时间。这题答得顺,一面基本就稳了。
-
二面的 Flash 与文件系统要按层讲:NOR 支持随机访问、可执行代码、擦写寿命较低;NAND 单位成本低、容量大,但只能按页读写、按块擦除且有坏块。写之前必须擦除,是因为擦除状态是全 1、写只能把 1 变成 0,要重新写入 1 就必须整块擦回全 1,这也解释了为什么不能按字节擦、以及为什么擦除是性能瓶颈。坏块来自工艺与磨损,管理方式是在出厂与运行时标记坏块并建立映射(把坏块从可用块池里剔除)。ECC 用于检出与纠正位翻转,纠错能力由校验码位数与块大小决定。磨损均衡分动态(写入时优先挑擦写次数少的块,只均衡被更新的数据)与静态(主动迁移长期不动的冷数据,均衡更彻底但带来额外搬移与写放大)。写放大来自「改一页要搬一整个块」,加上 UBI 的开销,所以小数据频繁写会加速块消耗。
-
write 返回成功不等于数据落盘,这一串问题本质是缓存层次:write 只把数据写进 page cache 并标脏,真正落盘由后台回写线程异步完成;fsync 会强制把该文件的数据与必要的元数据刷到介质,fdatasync 只保证数据部分。即使 fsync 返回,如果底层设备缓存没刷,掉电仍可能丢——这就是日志文件系统与屏障/FUA 存在的原因。日志文件系统把元数据变更先顺序写进日志,崩溃后用重放保证一致;所以它抗的是「元数据不一致」而不是「数据全不丢」。答题时把这三层分清(page cache、设备缓存、介质),比笼统说「刷盘了就行」可信得多。
-
DMA 与 Cache 一致性是嵌入式存储岗的常青题:CPU 通过 cache 访问内存,DMA 直接读写物理内存,两者看到的数据可能不同。对策要看架构架构:非一致性(non-coherent)平台上,发送前要 clean(把 cache 里的新数据写回内存),接收后要 invalidate(丢弃 cache 里的旧副本再读内存);同时也别把 DMA 缓冲区放在会被 CPU 预取或被 cache 覆盖的普通可缓存区域,用一致性映射(dma_alloc_coherent)或流式 DMA 映射接口来管理。这题能答到「clean 与 invalidate 各自在什么时机做」,就说明真写过驱动。
-
三面场景题的答题框架:日志 + 元数据 + 恢复:这类「持续写、循环覆盖、随时掉电」的题有成熟套路——把 Flash 当环形日志用,每条记录带记录头(序号、长度、CRC)并顺序追加;另设固定位置的双份元数据扇区(记录当前写指针、有效区间、版本计数),写入时先写数据再更新元数据。恢复时用 CRC 判断记录完整性,用元数据里的序号或版本计数选出最新且一致的一份;两份元数据不一致时,靠版本号加记录序号交叉验证,宁可回退到上一个自洽状态也不猜。块写满后回收最旧块,回收过程中的掉电靠「先标记、后擦除、再更新元数据」的顺序保证可恢复。减少擦除次数的手段是攒批、压缩记录、以及把冷数据固定下来做静态均衡。测试不能真靠拔电源,要用断电注入(可编程电源或继电器按随机时刻切断)、看门狗复位注入、坏块注入工具和长时间老化,重点覆盖「写入中途掉电」这类窗口。