根据面经准备面试第五期-2026Linux嵌入式软开-上海繁易

#秋招笔面试记录##我的秋招日记##还记得你第一次面试吗?#

细节没答上来,反问第一次遇见,太菜了

1.volatie,用了有什么好处,不用有什么坏处

2.static用了有什么好处,不用有什么坏处

3.static为什么值不能修改

4.堆和栈

5.内存碎片、第一次听、怎么解决

6.栈溢出是怎么导致的、怎么解决

7.设备驱动的接口,字符设备

8.波特率字节怎么计算

9.串口中断,流程

10.自旋锁和互斥锁

11.缓冲区

12.gpio上的电阻

13.进程间通讯和线程间通讯,依旧记不全

1. volatile 的好处与坏处

用了的好处:确保变量直接从内存地址中读取、防止编译器优化,比如缓存到寄存器、保证对特殊地址,如硬件寄存器

多线程共享变量、访问总是最新的

不用的坏处:若该用而未用,编译器可能进行错误优化,导致读取的值不是最新值(如硬件状态已变但代码读到的是旧值),引发程序逻辑错误。

2.static 的好处与坏处

用了的好处:

修饰变量:延长生命周期至整个程序运行期(位于静态区),局部静态变量保持值不变;限制作用域为本文件,提高封装性。

修饰函数:限制函数作用域为本文件,避免命名冲突。

不用的坏处:若该用而未用,可能导致变量被意外修改(全局变量)、函数命名冲突,或局部变量每次重新初始化无法保持状态。

3. static 为什么值不能修改?

误解澄清:static 修饰的变量可以修改(如静态局部变量可多次赋值),并非不可修改。提问者可能混淆了 const。

我就记混了

核心特点:static 关键的是生命周期和作用域,而非不可变性

4. 堆和栈的区别

栈:编译器自动管理,存放局部变量、函数参数等,分配/回收速度快,但空间有限,溢出会导致崩溃。

一般用于函数调用、保存函数参数、局部变量、返回地址

基本数据类型的局部变量存储

由操作系统自动分配和释放,无需手动干预

栈,先进后出,栈内存是连续的,操作是在栈顶进行的

堆:程序员手动管理(malloc/free),空间大但分配/回收开销大,使用不当易产生内存泄漏或碎片。

需程序员手动申请释放

主要存储对象、数组动态分配的数据生命周期由代码逻辑控制

内存地址不连续,通过指针查找,可能产生内存碎片,第一次遇到,

5. 内存碎片

定义:内存碎片是动态内存分配后产生的,无法被利用的空闲内存块,核心是,内存总量足够,单个空闲内存块太小,无法满足程序分配需求,多次分配和释放不同大小内存块后,空闲内存被分割成不连续的小块,导致无法满足大块内存申请,即使总空闲足够。

影响:降低内存利用率,可能引起分配失败。

解决:使用内存池、定制分配算法或垃圾回收(如某些RTOS提供方案)。

内部碎片(Internal Fragmentation)

产生原因:内存分配器为了简化管理,通常会按固定大小的 “块” 分配内存。当程序申请的内存大小小于分配块的实际大小时,块内未使用的部分就成为内部碎片。

示例:若分配器最小分配单位是 8 字节,程序仅申请 3 字节,分配后剩余的 5 字节就是内部碎片,归该程序所有但无法使用。

常见场景:使用固定大小内存块的分配策略(如早期的分区分配)、或内存对齐机制(如结构体对齐)。

外部碎片(External Fragmentation)

产生原因:频繁的内存分配与释放(“分配 - 释放 - 再分配”)后,原本连续的空闲内存被已使用的内存块分割成多个不连续的小空闲块,形成外部碎片。

示例:内存中依次分配了 100 字节、200 字节、300 字节的块,随后释放中间 200 字节的块,此时空闲的 200 字节块被前后已使用的块夹住。若程序需申请 250 字节,即使总空闲内存足够,也因无连续 250 字节块而失败。

常见场景:使用动态大小内存块的分配策略(如堆内存分配),是编程语言中更需关注的碎片类型

内存整理(Compaction):将所有已使用的内存块移动到一端,使分散的空闲块合并成一个大的连续块,适用于支持内存移动的环境(如部分垃圾回收机制)。

伙伴系统(Buddy System):将内存按 2 的幂次大小划分成块,分配和释放时通过合并 “伙伴块”(大小相同、地址连续的空闲块)减少外部碎片,常见于操作系统内核。

内存池(Memory Pool):提前分配一块固定大小的内存池,按程序需求划分成统一大小的小块,避免频繁动态分配导致的碎片,适用于频繁申请 / 释放同类型小对象的场景(如网络编程中的数据包)。

6. 栈溢出的原因与解决

原因:栈内存使用超出其容量限制时触发的错误、核心原因是栈中存储的内容、函数调用、局部变量、耗尽了预分配的栈空间,

深递归、大局部变量、函数调用层次太深、中断嵌套过深等耗尽栈空间。

栈的大小在程序编译或运行初期由系统(或虚拟机)固定,无法动态扩展。当以下操作导致栈内存占用超出限制时,就会发生溢出:

过多的函数嵌套调用:即使非递归,函数调用层级过深(如 A 调用 B、B 调用 C、C 调用 D…… 嵌套数十层甚至上百层),也会因每层调用的参数、局部变量占用栈空间而溢出。

栈中存储超大局部变量:在栈上定义过大的数组、结构体等局部变量,直接超出栈的剩余容量

最常见原因。函数自身或间接调用自身时没有终止条件,导致调用栈不断叠加,最终撑满栈空间。

典型表现与危害

程序崩溃:是最直接的后果,通常伴随错误提示(如 “Stack Overflow”“段错误(Segmentation Fault)”)。

行为异常:部分情况下可能不直接崩溃,但会破坏栈中存储的函数返回地址等关键数据,导致程序跳转到错误地址执行,出现逻辑混乱、数据损坏等问题。

解决:

优化代码:减少递归深度、改用迭代;避免大数组在栈上(改到堆或静态区)。

调整栈大小:在链接脚本或RTOS配置中增大栈空间。

使用工具:静态分析栈深度(如Keil的Stack Usage分析)。

常见解决与预防手段

避免无限递归:确保递归函数有明确的终止条件(如递归深度限制、递归变量达到目标值时退出)。

减少函数嵌套层级:将过深的嵌套逻辑拆分为多个独立函数,降低单次调用栈的深度。

转移大对象到堆内存:对于数组、大型结构体等,改用new(C++)、malloc(C)或动态数组(如 Python 的list)在堆上分配,避免占用栈空间。

调整栈大小(谨慎操作):部分编译器或操作系统支持通过参数调整栈的默认大小(如 GCC 的-Wl,--stack,<size>选项),但需注意过大的栈可能浪费系统资源。

7. 字符设备驱动接口

Linux 设备驱动是运行在 Linux 内核空间、用于管理硬件设备并为用户空间程序提供操作接口的核心软件,其本质是内核与硬件之间的 “翻译官”,让操作系统能识别和控制具体硬件(如网卡、U 盘、传感器等)

核心接口:实现 file_operations 结构体中的函数指针,如:

open():初始化设备。

read()/write():数据读写。

ioctl():控制命令。

release():释放资源。

注册方式:register_chrdev() 注册主设备号,mknod 创建设备节点。

核心作用:为不同厂商的同类硬件,提供统一软件接口,实现硬件控制,直接操作硬件寄存器、中断底层资源,完成设备初始化、数据读写、状态查询操作

衔接内核与硬件:遵循Linux内核规范、将硬件功能集成到内核子系统,块设备子系统、字符设备子系统

字符设备驱动:按字节流顺序读写的设备,如串口、键盘、LED 灯等,需实现open/read/write等文件操作接口,对应/dev下的字符设备文件(如/dev/ttyS0)。

块设备驱动:按固定大小 “块”(如 512 字节、4KB)读写的设备,如硬盘、U 盘、SD 卡等,依赖内核块设备子系统,支持随机访问,对应/dev下的块设备文件(如/dev/sda1)。

网络设备驱动:用于网络通信的设备,如网卡、Wi-Fi 模块,不对应/dev文件,而是通过内核网络子系统(如 TCP/IP 协议栈)提供网络接口(如eth0)。

杂项设备驱动:简化版字符设备,适用于功能简单的硬件(如 GPIO 按键),内核自动分配主设备号,无需手动注册设备文件。

驱动代码运行在内核态,拥有最高权限(可直接操作硬件、访问内核资源),但需严格遵循内核规范(如避免内存泄漏、不能直接调用用户态函数)。

***与用户空间通信:通过 “系统调用” 间接交互,用户程序通过操作/dev下的设备文件(如open("/dev/led", O_RDWR)),触发内核驱动的对应接口。

设备号:驱动的唯一标识,分为主设备号(区分设备类型,如字符设备、块设备)和次设备号(区分同类型下的具体设备),通过register_chrdev(字符设备)等函数注册。

设备文件:用户空间操作硬件的 “入口”,通过mknod命令或驱动自动创建(如class_create+device_create),位于/dev目录下。

中断处理:硬件触发事件(如按键按下、数据接收)的响应机制,驱动通过request_irq注册中断处理函数,内核在中断发生时调用。

内存映射(mmap):将硬件寄存器或缓冲区的物理地址映射到用户空间虚拟地址,让用户程序可直接访问硬件内存,减少数据拷贝开销(如显存操作)。

内核模块机制:大多数驱动以 “内核模块(.ko 文件)” 形式存在,可通过insmod加载、rmmod卸载,无需重启系统(如insmod led_drv.ko加载 LED 驱动)。

驱动运行在内核态,bug 可能导致内核崩溃(如空指针访问、死锁),需严格调试(如使用printk、gdb内核调试)。

多个进程同时访问驱动时,需通过自旋锁(spinlock_t)、互斥体(mutex_t)等机制避免数据竞争。

驱动开发流程(简化)

环境搭建:安装交叉编译工具链(针对嵌入式 Linux)、内核源码(需与目标系统内核版本一致)、开发板 SDK。

驱动代码编写:

包含内核头文件(如linux/module.h、linux/fs.h)。

实现驱动核心接口:初始化函数(__init)、退出函数(__exit)、文件操作接口(file_operations结构体,如read/write)。

注册设备号、创建设备文件、注册中断(按需)。

编译驱动:编写 Makefile,指定内核源码路径和编译规则,生成.ko模块文件。

加载与测试:

加载驱动:insmod xxx.ko,通过lsmod查看已加载模块,dmesg查看驱动打印信息。

测试驱动:用户程序通过open/read/write操作/dev下的设备文件,验证硬件功能(如控制 LED 亮灭、读取传感器数据)。

卸载驱动:rmmod xxx.ko,清理驱动占用的内核资源(如释放设备号、注销中断)。

并发与同步:多个进程同时访问驱动时,需通过自旋锁(spinlock_t)、互斥体(mutex_t)等机制避免数据竞争。

8.波特率字节传输时间限制

波特率” 和 “字节” 是串行通信中两个关联但不同的概念,前者描述信号传输速率,后者是数据存储的基本单位,二者通过 “位(bit)” 建立换算关系。

波特率表示单位时间内串行通讯线路上的信号状态变化的次数,即码元速率,串口通信中,通常一个码元对应一个数据位,可以理解成,每秒传输的二进制位数bit/s,波特,实际通信等同于比特/秒 (bit/s) 衡量串口通信的速度快慢

字节 ,计算机中数据存储的基本单位,一个字节固定等于8个二进制位,表示实际传输的数据量大小,本质是传输若干字节的数据

1 字节数据 = 8 位数据位 + 1 位起始位 + 1 位停止位 = 10 位传输位。

波特率 → 字节传输速率换算公式:

每秒传输字节数 ≈ 波特率 ÷ 10(因帧格式固定为 10 位 / 字节)。

公式:传输1字节(8位)时间 = (1 / 波特率) × (1起始位 + 8数据位 + 1停止位) × 1000 ms。

示例:9600波特率传输1字节时间 ≈ (1/9600) × 10 ≈ 1.04 ms。

波特率 → 字节传输速率换算公式:

每秒传输字节数 ≈ 波特率 ÷ 10(因帧格式固定为 10 位 / 字节)。

9. 串口中断流程

串口中断是串口设备(如 UART)在发生特定事件(如收到数据、发送完成)时,主动向 CPU 发送请求,暂停当前任务并转而执行中断服务程序(ISR)的机制,其核心作用是高效处理串口数据的收发,避免 CPU 轮询等待造成的资源浪费。

释放 CPU 资源:CPU 可执行其他任务,仅在串口有事件时才被 “打断” 处理,解决了轮询方式下 CPU 资源被占用的问题。

实时响应数据:对高速或突发的串口数据(如传感器实时数据),中断能快速触发处理,避免数据丢失或延迟。

串口中断的本质是 “硬件主动通知 CPU 处理事件”,而非 CPU 持续查询(轮询),能显著提升系统效率。

数据到达串口硬件,触发接收中断(或发送缓冲区空触发发送中断)。

CPU跳转到中断服务程序(ISR)。

ISR读取状态寄存器判断中断源。

若是接收中断,从数据寄存器读取字节存入缓冲区;若是发送中断,写入下一字节或关闭中断。

清除中断标志,退出ISR。

常见触发事件(以 UART 为例)

接收中断(RX Interrupt):串口接收缓冲区收到新数据时触发,是最常用的中断类型,用于及时读取接收的数据。

发送中断(TX Interrupt):串口发送缓冲区数据发送完成、为空时触发,常用于连续发送数据(如发送大段数据时,触发后填充下一批数据)。

接收错误中断(Error Interrupt):接收数据时出现异常(如帧错误、奇偶校验错误、数据溢出)时触发,用于处理通信错误。

空闲中断(Idle Interrupt):接收线在一定时间内无数据(即空闲)时触发,常用于判断一帧数据接收结束(如 Modbus 协议中的帧边界检测)。

中断初始化:

配置串口控制器:使能对应中断(如接收中断),设置触发条件(如接收 1 个字节即触发)。

注册中断服务程序(ISR):将处理该串口中断的函数(如uart_rx_isr)与 CPU 的中断控制器(如 ARM 的 GIC、x86 的 PIC)关联,指定中断号。

使能 CPU 中断:允许 CPU 响应该串口的中断请求。

中断触发与响应:

硬件触发:外部设备通过串口发送数据,串口接收缓冲区收到数据后,向 CPU 的中断控制器发送中断请求(IRQ)。

CPU 响应:CPU 暂停当前正在执行的任务,保存现场(如寄存器值),根据中断号找到对应的 ISR 并执行。

中断服务程序(ISR)执行:

核心操作:读取串口接收缓冲区的数据,存入内存(如环形缓冲区),避免数据被后续数据覆盖。

清理中断:清除串口控制器的中断标志位,避免 CPU 重复响应同一中断。

恢复现场与任务切换:

ISR 执行完毕后,CPU 恢复之前保存的现场,回到被中断的任务继续执行;若有更高优先级任务等待,触发任务调度。

关键技术点与注意事项

1. 中断服务程序(ISR)的核心要求

快速执行:ISR 运行时会阻塞其他任务(尤其是低优先级中断),需尽量简短,仅做 “数据搬运”(如读数据到缓冲区),复杂处理(如数据解析)应交给后续线程。

避免阻塞操作:禁止在 ISR 中调用睡眠、互斥锁等可能阻塞的函数(如sleep()、mutex_lock()),否则会导致系统死锁或崩溃。

使用原子操作:若 ISR 与其他线程共享数据(如环形缓冲区),需用原子操作(如atomic_t)或自旋锁(spinlock_t)保证数据一致性,避免并发冲突。

2. 常见问题与解决方案

数据丢失:若 ISR 处理不及时,新数据覆盖接收缓冲区未读数据,需增大缓冲区(如使用环形缓冲区)或提高中断优先级。

中断嵌套:高优先级中断可打断低优先级中断,需合理配置中断优先级(如串口接收中断优先级高于普通任务),避免关键中断被阻塞。

中断风暴:若中断触发条件未正确清理(如未清除中断标志),会导致 CPU 反复进入 ISR,需在 ISR 中确保中断标志被有效清除。

10. 自旋锁与互斥锁

  • 自旋锁:请求不到锁时,CPU循环等待(忙等),适用于临界区很短、多核场景,避免上下文切换开销。

核心行为:忙等待。线程会一直循环检查锁是否被释放,占着CPU不放。

优点:在等待时间非常短的情况下,效率极高,避免了切换线程的开销。

缺点:如果等待时间很长,它会白白浪费大量的CPU时间(就像那个急性子的人累得满头大汗,啥正事也没干)。

适用场景:多核系统上,预计锁只会被持有非常短的时间(比如只是给一个变量加个1)

  • 互斥锁:请求不到锁时,线程睡眠(阻塞),让出CPU,适用于临界区较长场景,节省CPU但切换有开销。

核心行为:阻塞等待。线程如果拿不到锁,就去睡觉,把CPU让给别人。

优点:在等待时间较长的情况下,非常节省CPU资源。

缺点:“回家”和“再赶来”需要额外的时间(两次上下文切换的开销),反应慢。

适用场景:锁可能被持有较长时间的情况(比如需要从硬盘读取文件)。

短期等待使用自旋锁,长期等待使用互斥锁

11. 缓冲区(Buffer)

缓冲区是地方,缓冲队列是管理这个地方的方法。 我们绝大多数时候提到的“缓冲区”,其实指的就是用队列方式管理的缓冲区。

  • 作用:暂存数据,平衡生产者与消费者速度差异(如UART接收),平滑数据处理,提高效率。
  • 实现:通常为循环队列(FIFO),配以读写指针和互斥保护。

为了解决 “生产者”(网盘/发送数据)和 “消费者”(播放器/接收处理数据)之间的速度不匹配和处理时机不匹配的问题。

  • 串口(UART)通信:数据是一个字节一个字节传过来的,速度很快。你的主程序可能正在处理别的事情,不能马上处理每一个字节。所以串口接收器会有一个缓冲区,先把数据存起来,等你的主程序有空了再来一次性读取处理。
  • 文件读写:从硬盘读文件时,系统会一次读一大块数据到内存的缓冲区,而不是每次应用程序要一个字节就读一次硬盘(硬盘操作极慢)。
  • 视频播放:如上所述,预先下载一段视频数据到缓冲区,防止网络波动导致视频卡顿(这就是“缓冲中...”的由来)。

为什么要用队列这种结构?为了保持数据的顺序!数据到来的顺序很重要,先来的数据必须先被处理。

存一个数据,写指针后移;取一个数据,读指针后移。通过比较这两个指针,我们就知道缓冲区是空了、满了、还是有多少数据。

实现方式:

在编程中,我们通常用两个指针(或索引)来管理一个循环队列缓冲区:

  1. 写指针:指向下一个要存入数据的位置。(就像快递入库,仓库管理员记录下一个空货架在哪)
  2. 读指针:指向下一个要取出数据的位置。(就像取件人记录下一个该取哪个货架的包裹)

12.gpio上的电阻

GPIO常用的电阻有上拉、下拉和串联电阻。上拉和下拉电阻用于保证空闲状态下的电平稳定,防止引脚浮空引入干扰,比如按键电路就必须用上拉或下拉。串联电阻主要用于限流保护GPIO口,以及进行阻抗匹配,改善信号质量。”

13.进程间通讯和线程间通讯,依旧记不全

进程和线程通信的最大区别在于进程有独立的地址空间,而线程共享地址空间。”

  • 进程间通信(IPC) 需要内核的支持,因为操作系统要在进程之间搭建桥梁。常见方式有:管道/命名管道用于字节流传输。消息队列用于格式消息传输。共享内存是最快的方式,但需要自行同步。信号量用于同步。Socket是最通用的方式。
  • 线程间通信 则简单得多,因为它们天然共享全局变量、堆内存等资源。所以通信的核心不在于传递数据,而在于如何安全地访问共享数据,因此重点在于同步机制,主要是互斥锁和条件变量,防止出现竞态条件。”

14.死锁的四个条件

死锁是指两个或多个进程在执行过程中,由于争夺资源而造成的一种相互等待的现象,从而使得这些进程都无法继续执行。死锁产生的原因主要包括以下四个条件:

  1. 互斥条件:至少有一个资源必须以排他模式占用,即某时刻只能有一个进程使用该资源。
  2. 保持与等待条件:一个进程在保持某些资源的同时,正在等待其他资源。
  3. 不剥夺条件:已经分配给进程的资源在进程使用完之前不能被剥夺。
  4. 循环等待条件:存在一种进程资源的循环链,其中每个进程都在等待下一个进程所持有的资源。

解决死锁的策略

解决死锁问题的方法主要有以下几种:

  1. 预防死锁:破坏死锁四个必要条件中的一个或多个条件。例如,可以通过对资源请求进行限制来破坏“保持与等待”条件。
  2. 避免死锁:使用资源分配图(如银行家算法等)来判断资源分配的安全性。如果申请资源后会导致系统进入不安全状态,则拒绝该请求。
  3. 检测与恢复:定期检查系统中的进程和资源状态,识别死锁。如果发现死锁,可以采取措施,例如:终止某些进程以释放资源。强制剥夺某些资源,使其他进程能够继续执行。
  4. 忽略死锁:在某些系统中,可以认为死锁很少发生,因此可以选择忽略它。在这种策略下,系统可能会遭遇死锁,但通常会通过重启或其他方式进行恢复。

全部评论

相关推荐

评论
3
8
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务