面灵AI→

嵌入式面试的五个追问:这个锁到底保护了什么,不锁会出现什么现象

时间
2026-10
来源
牛客网

《面试题目》

  1. 你说你用过 FreeRTOS,这个锁到底保护了什么?
  2. 这个变量除了中断还有谁会访问?不锁会出现什么现象?
  3. 临界区应该包住哪几行?关中断和互斥量该怎么选?
  4. I2C 的时序是怎么走的?上拉电阻和总线电容会带来什么问题?
  5. 中断、DMA 和任务之间的共享数据,谁写谁读、什么时候能复用?
  6. HardFault 现场怎么读?CFSR、HFSR、BFAR、MMFAR 分别说明什么?
  7. 链接脚本里 .data 和 .bss 为什么要分开处理?

《参考解析》

「这个锁保护了什么」的正确答法,是先列访问者。 面试官问这句,是想听你说出「谁在写、谁在读、在什么上下文里」。一个共享变量如果只有任务 A 读写,那不需要锁;一旦中断服务程序也写它、或者任务 B 也读它,就有了竞争窗口。所以答案应该是这样的三段:这个变量被任务 A 的 update() 写、被中断里的采样回调写、被任务 B 读走显示;不加保护的后果是读到「半更新」的值或丢失一次更新。只答「保护了这个变量」等于没答。

不锁会出现的现象,要能举例。 最典型的是读—改—写被打断:counter++ 在汇编上是 load、add、store 三条指令,中断在 load 之后插进来也做了一次 ++,返回后把旧值加一写回去,两次自增只生效一次。第二种是结构体/多字节数据撕裂:一个 32 位变量在 8 位机上要写四次,中间被读到就是新旧混合的值;一个 {x, y} 结构体在 32 位机上也可能被读到 x 已更新、y 还是旧值。第三种是链表/环形缓冲的指针更新顺序错乱,导致遍历时跳到已释放的节点。这三种现象都能对应到具体的调试现场,比背「数据竞争」四个字有说服力。

顺带要能说清 volatile 的边界:它只保证每次都从内存读、不被编译器优化掉,不保证原子性,也不负责内存屏障。加了 volatile 仍然需要临界区。

临界区包住哪几行:只包共享的那一段。 判定方法是看「从读到写回」这条链路上有没有可能被打断的指令。如果只是 counter++,临界区就是这一行;如果是「判断队列非空 + 取元素 + 移动头指针」,那三步必须整体保护,否则两个消费者会拿到同一个元素。反过来,把整个函数包进临界区就是典型的扣分点——串口打印、浮点运算、malloc 都不该在临界区里。

选型上按「另一边是谁」来分:另一边是中断,用关中断(taskENTER_CRITICAL / portSET_INTERRUPT_MASK_FROM_ISR),因为中断不受调度器管;另一边是任务,用互斥量,让调度器去处理等待,不要为了保护任务间的共享数据去关中断——那会把整个系统的中断延迟拉长。互斥量还带优先级继承:低优先级任务持锁时,中优先级任务抢占会把高优先级任务间接拖住(优先级翻转),继承机制临时把持锁者的优先级提到等待者的水平,让它尽快跑完释放。二值信号量没有继承,所以「保护资源」要用互斥量,不能用信号量代替。

I2C 的时序要能顺着波形讲。 起始条件是 SCL 为高时 SDA 由高变低;随后每来一个 SCL 高电平周期送一位,先送 7 位从机地址加 1 位读写方向,第 9 个时钟由从机拉低 SDA 作为 ACK(不应答就是 NACK);然后按字节传数据,每个字节后同样跟一位 ACK;结束时 SCL 为高时 SDA 由低变高作为停止条件。中途要换方向或换寄存器地址,用重复起始条件(Sr)而不发停止位。

物理层的坑才是常考点:I2C 是开漏总线,靠上拉电阻把线拉高,所以上升时间由 R × C(上拉阻值乘总线电容)决定,总线电容上限一般按 400 pF 估;上拉太大上升沿变缓、高速下采不到高电平,上拉太小则灌电流超过器件的下拉能力(一般 3 mA 左右)。另外从机可以用时钟拉伸把 SCL 拉住不放来争取时间,多主场景还有仲裁——这些都在「时序为什么不匹配」的排查清单里。

共享数据的所有权和复用时机。 判断标准是「这次传输/这次中断真的结束了吗」。DMA 发送缓冲在传输完成标志置位前不能改写;接收缓冲在 DMA 写完之前不能读,而且要处理 cache 一致性(发送前 clean、接收后 invalidate,或把该区域配成非缓存)。中断与任务之间用环形缓冲或队列传递,队列按值拷贝最安全,传指针就要约定谁负责释放。这些约定写进注释和接口文档,比事后靠调试器找偶发问题便宜得多。

HardFault 现场怎么读。 先看压栈的异常帧:Cortex-M 硬件会自动压入 R0–R3、R12、LR、PC、xPSR,从 EXC_RETURN 的 bit2 判断当时用的是 MSP 还是 PSP(任务里出错多半是 PSP),再从对应的栈里取出 PC,那就是出错指令的地址,配合 .map/反汇编能定位到函数。然后看四个寄存器:CFSR 里 MMFSR 是取指/访存违例、BFSR 是总线错误(取指、读写、压栈出栈失败)、UFSR 是用法错误(未对齐访问、除零、非法状态、跳到非 Thumb 地址);HFSR 的 FORCED 位表示这是一个被升级上来的故障,说明真正的源头在上面几个子状态里;BFAR/MMFAR 记录出错的地址,但要先看对应状态位里的 VALID 位是否为 1,为 0 时这个地址没有意义。再往下才是常见原因清单:空指针、野指针、栈溢出、中断里用了阻塞 API、栈上的大数组、跳转到被擦掉的 Flash 区域。

.data 和 .bss 为什么分开。 .data 是「有初值且初值非零」的全局/静态变量,初值要存在 Flash 里(加载地址),运行时地址在 RAM(执行地址),所以启动代码必须把这段从 Flash 拷到 RAM;.bss 是「初值为零或未初始化」的,编译器只在链接脚本里保留一段 RAM 空间、不占 Flash,启动代码把它清零就行。分开的直接收益是省 Flash——一个 1 KB 的零初始化缓冲区如果放进 .data,Flash 里就要多存 1 KB 的零。追问还可能落到 .rodata(常量,留在 Flash 或拷到 RAM 取决于是否带 XIP)、.noinit(复位不清零,用来存复位原因或跨复位保留的数据)和栈的放置。

这五类追问的共同点: 面试官不是要你复述定义,而是要你给出「边界 + 现象 + 定位手段」三件套。平时写代码时把每个共享数据的访问者列一遍、把每个临界区的行数数一遍、把每次 HardFault 的寄存器截图存一份,这些题就不用背了。