(嵌入式面经)第11章 20+公司面经杂谈(五):Momenta、瑞晟、OPPO、VIVO
本篇涉及的所有问题概要:大家可以试试在看参考答案前,提前尝试解答,以便明确自身知识点的不足部分!
1.中断里面通常可以睡眠吗,为什么?
2.什么是栈溢出,有什么方法检测?
3.MCU 与 MPU 的区别是什么?
4.讲一下Linux中断分为上半部和下半部的原因?
5.如果给线程访问加锁,是在驱动层实现还是应用层实现?
6.现在需要一个常量5来做一些运算,你会使用const还是define?为什么?
7.项目里的ADC多通道采集是具体采用什么方式?
8.说一下互斥锁,自旋锁的区别,在中断中可以用哪个锁?
9.怎么保证原子性操作?
10.专栏订阅奖励(支持模仿)——个人创新点问答:在你设计的FreeRTOS PLUS中提及修改任务控制块,增加优先级阶段继承属性,来进一步降低“优先级反转”的影响,展开说说?
---------------------------------------------------------------------------------------------------
1.中断里面通常可以睡眠吗,为什么?
在 Linux 内核或嵌入式 RTOS(如 FreeRTOS)中,中断处理程序(ISR) 不能进入 睡眠(sleep/wait) 状态。
主要有 两大原因:
- 中断上下文的限制(缺乏调度支持)
- 中断设计的初衷——高实时性、不可阻塞
1. 中断上下文限制,缺乏调度支持
📌 什么是中断上下文?
- 内核中的中断处理程序不属于某个特定进程,而是独立的执行实体,没有进程调度信息。
- 没有进程的内核栈、调度信息、文件描述符 等数据结构,无法像普通进程一样被调度。
- 缺少进程控制块(PCB),无法进行任务切换。
✅ 具体分析
- 进程上下文(Process Context) → 进程有自己的 调度信息、内核栈、寄存器保存,可以被挂起并恢复。
- 中断上下文(Interrupt Context) → 没有这些信息,如果 ISR 进入睡眠,就无法被调度器重新唤醒。
🚨 问题:
- 进入睡眠后 找不到中断的上下文信息,唤醒后无法恢复,系统可能会崩溃。
2. 中断设计的初衷:高实时性,不可阻塞
📌 中断的核心任务
- 中断的目的是 快速响应硬件事件,执行最小化的任务,然后尽快恢复系统运行。
- 如果中断中调用
sleep()
,会导致 CPU 挂起整个系统,影响其他任务的执行。
✅ 为什么要避免阻塞?
- 中断优先级比普通进程高,如果中断中进入睡眠,会导致 CPU 被占用,进程调度器无法调度其他任务。
- 高优先级中断可能会阻塞低优先级进程,影响系统性能。
3. 实例:为什么 ISR 不能调用 sleep()
📌 错误示例
🚨 在中断处理中调用 sleep()
msleep()
会导致任务挂起,但中断上下文没有进程调度信息,无法恢复。📌 正确做法:使用 workqueue
或 tasklet
在 中断处理程序中,只做必要的工作,然后将复杂任务交给其他线程(如 workqueue
)。
✅ 解决方案:使用 workqueue
结论
✅ 使用 workqueue
或 tasklet
处理复杂任务
✅ 避免在 ISR 中使用 sleep()
,改为调度后台线程处理
2.什么是栈溢出,有什么方法检测?
1. 什么是栈溢出?
栈溢出(Stack Overflow)是指程序在运行过程中,函数调用栈空间超出系统分配的范围,导致程序崩溃或行为异常。
由于栈空间是有限的,如果程序需要的栈空间超过了系统分配的最大限制,就会发生栈溢出。
2. 栈溢出的常见原因
📌(1)函数调用层数过深
无限递归/深度递归 会不断地向栈中压入新的栈帧,最终超过栈的最大空间,导致栈溢出。
✅ 示例:递归导致的栈溢出
由于 stack_overflow_function()
没有终止条件,递归会无限增长,最终导致栈空间被耗尽,程序崩溃。
📌(2)占用栈空间过大的局部变量
如果一个函数中的局部变量过大,尤其是大数组或结构体,也可能导致栈溢出。
✅ 示例:局部变量过大导致栈溢出
🚨 问题:
- 局部数组
buffer
占用了 10MB 的栈空间,大于默认栈空间,导致栈溢出。
3. 栈溢出的后果
4. 栈溢出的检测方法
📌(1)哨兵检测(Stack Sentinel)
- 在栈的起始地址或结束地址放置特定的标志值(哨兵值),然后在任务切换或关键点检查这些值是否被修改。
- 如果哨兵值被改写,说明发生了栈溢出!
✅ 示例:哨兵检测
- 只适用于单任务环境,如果多个任务共用栈空间,可能检测不到问题。
📌(2)栈指针范围检测
- 在任务切换时检查
SP
(栈指针) 是否越界 - 如果
SP
低于分配的栈空间,说明栈溢出
✅ 示例:FreeRTOS 栈溢出检测
📌 FreeRTOS 需要在 FreeRTOSConfig.h
启用检测
📌(3)使用 ulTaskCheckFreeStackSpace()
监测任务栈
✅ FreeRTOS 提供的栈检查 API
📌 如果 freeStack
过小(如 <20),可能需要增加栈空间!
📌(4)GCC 编译器保护(-fstack-protector
)
- 启用
-fstack-protector
选项,可以在函数返回时检测栈溢出 - 原理:编译器在栈上存放"Canary" 保护值函数返回时检查 Canary 是否被修改如果被篡改,则触发 stack smashing detected
✅ 启用 -fstack-protector
gcc -fstack-protector -o test test.c
5. 如何预防栈溢出
✅ (1)避免深度递归
- 用循环代替递归
- 尾递归优化
✅ (2)减少局部变量的栈空间
- 大数组使用
malloc()
在堆上分配 - 减少函数嵌套调用
✅ (3)合理分配栈大小
- 在 FreeRTOS/Linux 任务创建时,合理设置
STACK_SIZE
- 监测
uxTaskGetStackHighWaterMark()
确保栈足够
✅ (4)使用 -fstack-protector
进行编译器保护
gcc -fstack-protector -o my_program my_program.c
✅ (5)启用 configCHECK_FOR_STACK_OVERFLOW
结论
🚀 防止栈溢出,需要合理分配栈大小、优化代码结构、并启用硬件和软件监测机制! 🎯
3.MCU 与 MPU 的区别是什么?
- MPU(Microprocessor Unit) 适用于复杂运算的大型程序,通常需要外挂 RAM 和 ROM。
- MCU(Microcontroller Unit) 适用于中小型控制程序,内部集成 RAM 和 ROM,无需外挂存储器。
1. MCU(微控制器)
特点:
- 集成度高:MCU 典型地将 CPU、RAM、ROM、GPIO、定时器、ADC、PWM、串口(UART/I2C/SPI) 等外设集成在一颗芯片内。
- 成本低:由于 MCU 的所有组件都集成在同一芯片中,硬件设计成本较低。
- 低功耗:适用于低功耗设备(如 IoT 设备、工业控制、汽车电子)。
- 高实时性:一上电即可运行,无需操作系统,也可以跑 RTOS(如 FreeRTOS)。
- 典型应用:
✅ 例子:
- STM32F4(Cortex-M4)
- AVR ATmega328(Arduino 用的芯片)
- GD32F3(国产 MCU)
2. MPU(微处理器)
特点:
- 高性能:MPU 适用于复杂计算,如 AI、图像处理、多任务操作。
- 需要外挂 RAM 和 ROM:
运行完整的操作系统:
适用于需要 UI、多任务、复杂运算的设备:
典型应用:
✅ 例子:
- 树莓派(Raspberry Pi)
- NXP i.MX 6/8(Cortex-A 系列)
- Allwinner H3/H5(电视盒子芯片)
- Rockchip RK3399(安卓平板/开发板)
3. STM32(MCU) vs 树莓派(MPU)
4. 为什么导弹、工业控制用 MCU?
- 高实时性:MCU 上电即运行,而 MPU 需要加载操作系统,延迟高。
- 功耗低:MCU 可以低功耗运行,而 MPU 需要较大功耗运行。
- 稳定可靠:MCU 运行裸机或 RTOS,没有 OS 崩溃的风险。
总结:
- MPU 适合需要操作系统、复杂运算(如 AI、Linux 设备)
- MCU 适合实时控制、低功耗应用(如 STM32 控制电机、传感器)
4.讲一下Linux中断分为上半部和下半部的原因?
在 Linux 内核中,中断处理被分为 上半部(Top Half) 和 下半部(Bottom Half),
主要目的是提高系统响应速度,减少中断禁用时间,避免影响系统的实时性。
1. 为什么要分为上半部和下半部?
中断处理的挑战:
1.中断发生时,CPU 需要立即响应
2.中断处理会影响其他任务
3.中断是优先级最高的任务
4.解决方案:
- 上半部(Top Half):快速执行关键任务,完成最小化的硬件操作,然后尽快返回,释放 CPU。
- 下半部(Bottom Half):在稍后的合适时间,延迟执行较慢的任务,降低系统负担。
2. 上半部(Top Half)
主要作用:
- 处理紧急的硬件相关任务,确保系统快速响应中断。
- 代码通常执行极少的操作,然后立即退出。
典型任务:
- 读取中断状态(ISR 读取寄存器)
- 清除中断标志,防止重复触发
- 将数据存入缓冲区
- 触发下半部处理
上半部的限制:
✅ 不能使用 sleep()
(不能阻塞)
✅ 不能访问 用户空间(只能访问内核空间)
✅ 不能执行 耗时操作(如磁盘 I/O、复杂计算)
3. 下半部(Bottom Half)
主要作用:
- 执行耗时的任务,但不影响中断响应速度。
- 避免在中断上下文中执行过长的操作,以提高系统性能。
典型任务:
- 数据处理(如网络包解析)
- 唤醒等待的进程
- 调用文件系统、磁盘 I/O
- 复杂计算
Linux 实现下半部的方法:
- 软中断(SoftIRQ) → 适合高吞吐量任务(如网络协议栈、定时器)
- 任务队列(Tasklet) → 适合较短的任务,可以在多个 CPU 上并行执行
- 工作队列(Workqueue) → 适合能睡眠的任务,在进程上下文中执行
4. 例子:网络数据包处理
假设网卡收到数据包,触发中断:
上半部(ISR)
下半部(SoftIRQ / Workqueue)
socket
读取数据)总结
🚀 结论:分离上半部和下半部可以减少 CPU 负担,提高系统的实时性和响应速度。
5.如果给线程访问加锁,是在驱动层实现还是应用层实现?
✅ 回答:可以在驱动层实现,也可以在应用层实现,具体取决于需求和并发控制的粒度。
- 驱动层加锁:适用于多个进程/线程访问同一个驱动资源(如设备寄存器、共享内存)。
- 应用层加锁:适用于多个线程访问同一资源,但不涉及内核驱动(如数据库、全局变量)。
1. 在驱动层实现加锁(
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
作者简介:仅用几个月时间0基础天坑急转嵌入式开发,逆袭成功拿下华为、vivo、小米等15个offer,面试经验100+,收藏20+面经,分享求职历程与学习心得。 专栏内容:这是一份覆盖嵌入式求职过程中99%问题指南,详细讲解了嵌入式开发的学习路径、项目经验分享、简历优化技巧、面试心得及实习经验,从技术面,HR面,AI面,主管面,谈薪一站式服务,助你突破技术瓶颈、打破信息差,争取更多大厂offer。