admin 管理员组文章数量: 887021
目录:
目录:
一、9.9面经
1.1 什么是嵌入式?
1. 特点:
2. 应用领域:
3. 示例:
1.2 嵌入式设备开发与电脑上软件开发的区别
1. 硬件资源限制:
2. 操作系统:
3. 开发工具:
4. 应用场景:
1.3 单片机包含哪些部分,作用等
1. 中央处理器(CPU)
2. 存储器
3. 输入/输出(I/O)端口:
4. 定时器/计数器
5. 串行通信接口
6. 时钟电路
7. 中断系统
8. 模数转换器(ADC)和数模转换器(DAC)
9. 看门狗定时器
10. 电源管理模块
1.4 什么是中断,使用流程?
1. 中断流程
(1)中断请求(Interrupt Request,IRQ)
(2)中断响应(Interrupt Response)
(3)中断向量表(Interrupt Vector Table,IVT)
(4)执行中断服务程序(ISR)
(5)恢复现场并继续执行主程序
2. 中断的使用流程
(1)配置中断源:
(2)启用中断(使能中断):
(3)编写中断服务程序(ISR):
(4)中断优先级管理(可选):
(5) 中断处理结束:
1.5 一些通信协议的介绍与对比
1. UART(通用异步收发传输)
2. I2C(两线制串行总线)
3. SPI(串行外设接口)
4. CAN(控制器局域网络)
5. UART vs. I2C vs. SPI vs. CAN 对比
1.6 静态变量、局部变量、全局变量
1. 局部变量(Local Variable)
2. 全局变量(Global Variable)
3. 静态局部变量(Static Local Variable)
4. 应用场景:
1.7 结构体相关
1. 结构体的定义
2. 结构体变量的声明和使用
3. 结构体数组
4. 结构体指针
5. 嵌套结构体
1.8 OSI七层模型
1. 物理层(Physical Layer)
2. 数据链路层(Data Link Layer)
3. 网络层(Network Layer)
4. 传输层(Transport Layer)
5. 会话层(Session Layer)
6. 表示层(Presentation Layer)
7. 应用层(Application Layer)
8. 对比OSI七层模型
9. OSI模型的作用
1.9 TCP/IP协议,哪些使用了TCP?
1. HTTP(超文本传输协议)
2. HTTPS(安全超文本传输协议)
3. FTP(文件传输协议)
4. SFTP(安全文件传输协议)
5. SMTP(简单邮件传输协议)
6. SSH(安全外壳协议)
7. SQL协议(如MySQL、PostgreSQL等)
1.10 进程间通信方式
1. 管道(Pipe)
2. 消息队列(Message Queue)
3. 共享内存(Shared Memory)
4. 信号量(Semaphore)
5. 信号(Signal)
6. 套接字(Socket)
7. 匿名映射(mmap)
8. 内存映射文件(Memory-Mapped File)
9. 总结对比:
1.11 信号量是怎么传递的?
1. 信号量的工作机制:
2. 信号量的使用场景:
3. 信号量的传递方式
4. 信号量的类型
5. 使用信号量的注意事项
1.12 shell命令查找某个目录及子目录的某个文件
二、9.7面经
2.1 裸机和RTOS,如何挑选,包括Linux驱动
1. 裸机编程(Bare Metal Programming)
2. 实时操作系统(RTOS)
3. Linux 和 Linux 驱动开发
4. 如何挑选:
2.2 RTOS如何保证实时性
1. 抢占式多任务调度:
2. 确定性的中断处理:
3. 优先级调度算法:
4. 任务调度的确定性:
5. 实时定时器和定时中断
6. 内存管理的实时性
7. 任务的时间分配和资源管理:
8. 中断延迟管理:
2.3 FreeRTOS源码如何实现任务调度
1. 任务控制块(TCB)
2. 就绪列表:
3. 上下文切换:
4. 调度算法
5. 任务调度流程
2.4 FreeRTOS如何实现任务切换:
1. 任务切换的完整流程:
2. 中断中的任务切换
2.5 I2C驱动的时序
1. I2C基本时序
2. 通信过程
3. I2C时序图
4. 时序描述
5. 具体的时序要求 :
2.6 为什么需要头文件
1. 函数声明
2. 模块化编程
3. 类型定义和结构体定义
4. 宏定义和常量
2.7 头文件分尖括号和双引号,应该如何写引入的顺序
1. 尖括号 (<>) 用法:
2. 双引号 ("") 用法:
3. 推荐的引入顺序:
2.8 就是要先写双引号再写尖括号,为什么?
2.9 float可以移位吗
2.10 就是要float移位怎么办?
1. 模拟左移(乘以2的幂次)
2. 模拟右移(除以2的幂次)
2.11 new和malloc
1. new(C++)
2.malloc(C)
2.12 进程间、线程间通讯,管道的优势
1. 进程间通信(IPC)
2. 线程间通信
3. 管道的优势
2.13 同进程多线程,可以socket通讯吗
一、9.9面经
面经素材来源于牛客网:
1.1 什么是嵌入式?
嵌入式系统是指专门设计用于执行特定功能或任务的计算机系统,通常嵌入在更大的设备或系统中。与通用计算机相比,嵌入式系统通常具有以下几个特点:
1. 特点:
(1)专用性:嵌入式系统通常设计用于特定的应用或功能,例如控制设备、监测环境、处理信号等,而不是用于多种任务。
(2)资源受限:嵌入式系统通常在处理能力、内存、存储和电源方面有限,需在这些约束下高效运行。
(3)实时性:许多嵌入式系统要求在特定时间内完成任务,具有实时响应能力。比如,汽车安全气囊系统需要在碰撞发生的瞬间做出反应。
(4)可靠性和稳定性:嵌入式系统通常用于关键应用或长时间运行的设备,需要高可靠性和稳定性。
(5)硬件与软件紧密结合:嵌入式系统的硬件和软件通常是高度集成的,软件往往是专为特定硬件平台开发的。
2. 应用领域:
嵌入式系统广泛应用于各个领域,包括但不限于:
- 消费电子:如智能手机、平板电脑、电视机等。
- 家用电器:如微波炉、洗衣机、冰箱等。
- 汽车:如发动机控制单元、导航系统、安全系统等。
- 医疗设备:如监护仪、诊断设备等。
- 工业控制:如自动化设备、机器人控制系统等。
3. 示例:
- 单片机:例如,Arduino、Raspberry Pi等开发板常用于教育和原型开发。
- 实时操作系统(RTOS):如FreeRTOS、VxWorks等,用于管理嵌入式系统中的任务调度和资源分配。
嵌入式系统是专为特定任务设计和优化的计算机系统,广泛应用于现代生活的各个方面。
1.2 嵌入式设备开发与电脑上软件开发的区别
嵌入式设备开发与电脑上的软件开发有许多显著的区别,这些区别主要体现在开发环境、资源限制、系统架构和应用场景等方面。
1. 硬件资源限制:
- 嵌入式开发:嵌入式系统通常资源有限,如处理能力、内存、存储和电源。开发人员必须在这些限制条件下进行高效的编程和优化,确保系统能在有限的资源内稳定运行。
- 电脑软件开发:通常针对的是性能较强的计算机,资源相对丰富,开发者可以利用更多的内存和处理能力。
2. 操作系统:
- 嵌入式开发:嵌入式设备可能运行在实时操作系统(RTOS)、裸机或轻量级操作系统上,这些系统通常不具备完整的图形用户界面(GUI)和高级功能。
- 电脑软件开发:常在功能丰富的操作系统(如Windows、Linux、macOS)上运行,具有图形界面、复杂的库和工具支持。
3. 开发工具:
- 嵌入式开发:通常使用特定的开发工具链,包括交叉编译器、调试器和仿真器,以便在目标硬件上进行开发和测试。例如,Keil、IAR Embedded Workbench等。
- 电脑软件开发:使用IDE(集成开发环境)和其他工具,支持丰富的功能,如Visual Studio、Eclipse、PyCharm等。
4. 应用场景:
- 嵌入式开发:主要用于特定的应用,如消费电子、工业控制、汽车、医疗设备等,关注的是实时性、可靠性和稳定性。
- 电脑软件开发:可用于广泛的应用,包括桌面应用、网络应用、游戏等,通常关注用户体验和功能的丰富性。
1.3 单片机包含哪些部分,作用等
单片机(Microcontroller Unit,简称MCU)是一种集成了计算、存储和控制功能的芯片,广泛应用于嵌入式系统中。单片机是一个完整的微型计算机系统。
1. 中央处理器(CPU)
- 作用:CPU 是单片机的核心,负责执行指令、控制数据流动和进行运算。它从存储器中取出指令并执行,包括算术运算、逻辑运算、数据传输等。
- 工作原理:CPU 执行的指令通常来自存储器中的程序。它根据时钟信号的节拍完成指令周期,执行各种操作。
2. 存储器
单片机包含两种主要类型的存储器,分别用于存储程序和数据。
- 程序存储器(Flash 或 ROM):
- 作用:存储固化的程序代码,通常是不可修改的(ROM)或在系统运行时可擦写(Flash)。
- 用途:程序存储器保存用户编写的控制程序,这些程序在单片机上电后自动运行。
- 数据存储器(RAM):
- 作用:用于存储临时数据和变量。
- 用途:运行时CPU使用RAM进行数据的临时存储和处理,但掉电后数据会丢失。
3. 输入/输出(I/O)端口:
- 作用:用于与外部设备进行交互。I/O端口可以接收外部信号(输入),如传感器信号,也可以控制外部设备(输出),如驱动LED、继电器、显示屏等。
- 用途:I/O端口是单片机与外界通信的重要途径,它使得单片机能够控制和检测外部硬件。
4. 定时器/计数器
- 作用:定时器用于生成时钟脉冲或进行定时操作,计数器用于计数外部或内部事件。
- 用途:定时器常用于延时操作、PWM(脉宽调制)信号生成、实时计数等。计数器可以用于事件计数,如测量频率或脉冲个数。
5. 串行通信接口
- 作用:串行通信接口允许单片机与其他设备进行数据通信。
- 用途:常见的通信接口包括UART(通用异步收发器)、I2C(两线式总线)、SPI(串行外设接口)、CAN(控制器局域网)等,用于连接传感器、显示器、其他控制器或PC等。
6. 时钟电路
- 作用:时钟电路为单片机内部提供工作节拍(时钟信号)。
- 用途:时钟信号控制单片机的工作速度,所有操作都按照时钟的节拍进行。单片机可以使用外部晶振或内部振荡器来产生时钟信号。
7. 中断系统
- 作用:中断系统使单片机能够及时响应外部事件或内部定时器溢出等重要事件。
- 用途:当外部事件发生(如按钮按下或传感器变化)时,中断系统可以暂时打断CPU的当前任务,处理紧急任务后再恢复原来的任务。这提高了单片机的响应能力和效率。
8. 模数转换器(ADC)和数模转换器(DAC)
- ADC(模数转换器):
- 作用:将外部模拟信号(如温度、电压等)转换为数字信号,以便单片机处理。
- 用途:常用于采集传感器的模拟信号,如温度传感器、电压传感器的输出信号。
- DAC(数模转换器):
- 作用:将单片机内部处理的数字信号转换为模拟信号。
- 用途:用于控制外部设备,如音频输出或模拟控制信号的生成。
9. 看门狗定时器
- 作用:看门狗定时器用于监控单片机是否正常工作,如果系统出现故障或卡死,看门狗会重启单片机。
- 用途:看门狗可以防止单片机由于软件错误而陷入死循环或长时间停滞,确保系统的稳定性和可靠性。
10. 电源管理模块
- 作用:为单片机提供稳定的电源管理,包括电源开关、节能模式等功能。
- 用途:管理系统功耗,单片机可以进入低功耗模式以节省能量,特别是在电池供电的系统中。
1.4 什么是中断,使用流程?
1. 中断流程
中断是一种计算机系统中的机制,它允许系统在执行当前程序的过程中,暂时停止正常程序的执行,去处理一个紧急的事件或任务。当中断处理完毕后,系统会恢复之前的程序执行。这种机制在嵌入式系统和单片机中非常重要,能够提高系统的响应能力和效率。
在单片机中,中断常用于处理外部设备的输入、计时器溢出、通信事件等。中断可以在事件发生时立即响应,而不需要通过程序不断地轮询检测状态。
中断的基本工作流程包括以下几个步骤:
(1)中断请求(Interrupt Request,IRQ)
- 中断源(如外设、定时器或外部信号)发生了一个事件,并向CPU发出中断请求信号。这时,当前的程序不再继续执行,而是等待中断处理。
- 常见的中断源包括:
- 定时器溢出
- 外部设备输入(如按钮按下)
- 数据通信完成
- 传感器信号变化
(2)中断响应(Interrupt Response)
- CPU在检测到中断请求时,完成当前的指令周期后,暂停当前程序的执行。这时,CPU会自动执行一些保存当前程序状态的操作,包括保存程序计数器(PC)、寄存器等,以确保中断结束后可以恢复到中断前的状态。
(3)中断向量表(Interrupt Vector Table,IVT)
- CPU通过中断向量表找到对应中断源的中断服务程序(Interrupt Service Routine,ISR)入口地址。中断向量表是一个存储中断处理程序入口地址的特殊区域,每个中断源都有一个对应的地址。
(4)执行中断服务程序(ISR)
- CPU跳转到中断服务程序的地址,执行相应的中断处理任务。中断服务程序是开发者编写的一段用于处理特定中断事件的代码,通常包含对中断事件的处理逻辑,如读取数据、清除中断标志、控制外设等。
(5)恢复现场并继续执行主程序
- 当中断服务程序执行完毕后,CPU会恢复中断前保存的程序状态(如恢复程序计数器、寄存器等),并继续执行中断发生前暂停的主程序。
- CPU恢复到中断发生前的状态是自动完成的,确保在中断执行完后不会丢失之前的程序进度。
2. 中断的使用流程
(1)配置中断源:
- 根据需要配置中断源,如配置定时器、外部输入引脚或通信接口等。不同的中断源有不同的配置方式,例如,启用外部中断时需设置对应的引脚触发条件(上升沿、下降沿或高/低电平触发)。
(2)启用中断(使能中断):
- 开启CPU的全局中断开关,以及特定中断源的中断使能开关。全局中断开关通常是一个控制位,允许或禁止CPU响应任何中断。特定中断源的使能开关则允许相应的中断源发出中断信号。
(3)编写中断服务程序(ISR):
- 中断服务程序是用来处理中断事件的代码。当某个中断事件发生时,CPU会跳转到对应的中断服务程序执行。ISR应尽量简短,只处理必要的任务,并且在处理完后及时返回。
- 在ISR中,通常会:
- 读取或写入外设数据。
- 清除中断标志位,以防止重复触发中断。
- 调用后续逻辑或设置标志,通知主程序有数据或事件发生。
(4)中断优先级管理(可选):
- 某些单片机支持中断优先级管理,允许不同的中断源设置不同的优先级。优先级高的中断可以打断优先级低的中断,这确保了系统可以先处理更为紧急的事件。
- 如果系统不支持中断优先级机制,或者优先级不明确,可能会采用轮询的方式处理多个中断源。
(5) 中断处理结束:
- 中断服务程序执行结束后,系统通过恢复之前保存的现场状态,自动返回到中断前的主程序继续执行。
1.5 一些通信协议的介绍与对比
1. UART(通用异步收发传输)
- 描述:UART是一种异步串行通信协议,通常用于点对点的通信。它使用两个信号线进行数据传输:发送(TX)和接收(RX)。
- 优点:
- 简单易用,硬件需求少。
- 支持全双工通信。
- 可调波特率。
- 缺点:
- 通信距离较短(通常几米以内)。
- 无法进行多设备通信(需要额外的协议如RS-485实现多点通信)。
2. I2C(两线制串行总线)
- 描述:I2C是一种同步串行通信协议,使用两根线(SDA和SCL)进行数据传输。它支持多主机和多从机模式。
- 优点:
- 只需两根线,减少了布线复杂度。
- 支持多设备连接(最多127个设备)。
- 数据传输速度灵活(标准模式:100kHz;快速模式:400kHz;高速模式:3.4MHz)。
- 缺点:
- 数据传输速度相对较低(相比于SPI)。
- 支持的传输距离有限(通常在几米以内)。
3. SPI(串行外设接口)
- 描述:SPI是一种同步串行通信协议,使用四根线(MOSI、MISO、SCK和SS)进行数据传输。它通常用于点对点通信。
- 优点:
- 数据传输速度快(通常高达数MHz)。
- 支持全双工通信。
- 易于实现多设备通信(使用多个选择线)。
- 缺点:
- 需要更多的线路,特别是在多个从设备的情况下,布线复杂。
- 没有标准化的协议定义,设备之间的兼容性可能问题。
4. CAN(控制器局域网络)
- 描述:CAN是一种多主机、消息导向的串行通信协议,广泛应用于汽车和工业控制。
- 优点:
- 高可靠性和抗干扰能力,适合恶劣环境。
- 支持多主机和多从机,易于扩展。
- 内置错误检测和纠正机制。
- 缺点:
- 硬件和软件实现相对复杂。
- 数据传输速度相对较低(通常在1 Mbps以下)。
5. UART vs. I2C vs. SPI vs. CAN 对比
特性 | UART | I2C | SPI | CAN |
---|---|---|---|---|
通信方式 | 异步串行 | 同步串行 | 同步串行 | 同步串行 |
传输介质 | TX/RX线 | SDA/SCL线 | MOSI/MISO/SCK/SS线 | CAN_H/CAN_L |
最大设备数 | 1对1通信 | 127个设备 | 取决于选择线,理论无限 | 64个设备 |
数据传输速度 | 低(通常为9600-115200 bps) | 中(100kHz - 3.4MHz) | 高(几MHz到数十MHz) | 低(通常1 Mbps) |
全双工支持 | 是 | 否 | 是 | 否 |
复杂性 | 简单 | 中等 | 中等 | 复杂 |
错误检测 | 无 | 有(ACK) | 无 | 有(CRC) |
应用 | 设备调试、简单通信 | 传感器、EEPROM、LCD显示器等 | SD卡、显示屏、外设控制等 | 汽车、工业控制、自动化系统 |
1.6 静态变量、局部变量、全局变量
1. 局部变量(Local Variable)
定义:
局部变量是在函数或代码块内部定义的变量,它的作用范围(Scope)仅限于该函数或代码块。
特点:
- 作用域(Scope): 局部变量的作用范围是定义它的函数或代码块,离开该范围后,变量即失效。
- 生命周期(Lifetime): 局部变量的生命周期从变量定义开始,到离开定义它的代码块时结束(例如函数返回时)。当函数或代码块被调用时,局部变量会被重新创建。
- 存储位置: 局部变量通常存储在栈内存(Stack)中,栈内存的管理是自动的,函数调用结束后局部变量自动销毁。
- 初始化: 局部变量在定义时不进行初始化会导致未定义的行为(Undefined Behavior),因为栈内存中的初始值是不确定的。
示例:
void myFunction() {
int localVar = 10; // 局部变量
printf("%d\n", localVar);
} // localVar 离开此作用域后即失效
2. 全局变量(Global Variable)
定义:
全局变量是在所有函数之外定义的变量,它的作用范围是整个程序。
特点:
- 作用域(Scope): 全局变量的作用范围是整个程序文件(从定义的位置到文件结束)及其他包含该文件的文件(如果使用
extern
声明)。 - 生命周期(Lifetime): 全局变量的生命周期是从程序开始直到程序结束,它们在程序的整个运行期间都存在。
- 存储位置: 全局变量存储在全局数据区(数据段),分为已初始化的全局变量(在程序的初始化数据段)和未初始化的全局变量(在程序的未初始化数据段)。
- 初始化: 如果不显式初始化,全局变量会被初始化为其类型的默认值(例如,整型为0,浮点型为0.0,指针为NULL)。
示例:
int globalVar = 20; // 全局变量
void myFunction() {
printf("%d\n", globalVar); // 可以在函数内访问全局变量
}
3. 静态局部变量(Static Local Variable)
定义:
静态局部变量是在函数或代码块内部定义的局部变量,但使用 static
关键字修饰。这种变量在函数或代码块的作用范围内有效,但它的生命周期延续到程序结束。
特点:
- 作用域(Scope): 静态局部变量的作用范围和普通局部变量相同,只在定义它的函数或代码块内有效。
- 生命周期(Lifetime): 静态局部变量的生命周期是从定义时开始,到程序结束时结束(它不会因函数多次调用而被重新创建或销毁)。
- 存储位置: 静态局部变量存储在全局数据区(数据段),与全局变量类似,区别在于它的作用范围仅限于定义它的函数或代码块。
- 初始化: 如果不显式初始化,静态局部变量会被初始化为其类型的默认值(与全局变量相同)。
示例:
void myFunction() {
static int staticVar = 0; // 静态局部变量
staticVar++; // 每次调用函数时,staticVar 保持上一次的值
printf("%d\n", staticVar);
}
int main() {
myFunction(); // 输出:1
myFunction(); // 输出:2
myFunction(); // 输出:3
}
4. 总结比对:
特性 | 局部变量 | 全局变量 | 静态局部变量 |
---|---|---|---|
作用域(Scope) | 函数或代码块内部 | 整个程序 | 函数或代码块内部 |
生命周期(Lifetime) | 函数调用期间 | 程序运行期间 | 程序运行期间 |
存储位置 | 栈内存 | 全局数据区(数据段) | 全局数据区(数据段) |
默认初始化值 | 未定义 | 零值(如 0, 0.0, NULL 等) | 零值(如 0, 0.0, NULL 等) |
重新分配 | 每次调用都会重新分配 | 不会重新分配 | 第一次初始化后不再重新分配 |
4. 应用场景:
-
局部变量:适用于临时数据或仅在某个函数或代码块内需要的变量,使用栈内存管理,效率高,自动释放。
-
全局变量:适用于在多个函数之间共享数据,注意全局变量的过多使用会导致程序难以维护和调试(变量污染和命名冲突等问题)。
-
静态局部变量:适用于需要在函数调用之间保持状态的情况,例如计数器、缓存或需要在多次调用之间共享数据而不希望暴露在全局范围的情况。
1.7 结构体相关
结构体(struct
)是编程中的一种数据结构,用于将不同类型的数据组合在一起,使它们能够被作为一个整体进行处理。结构体在C、C++等编程语言中非常常见,尤其是在嵌入式系统编程(如STM32开发)中,结构体可以有效地组织和管理数据。
1. 结构体的定义
结构体的定义格式:
struct 结构体名称 {
数据类型 成员1;
数据类型 成员2;
...
数据类型 成员n;
};
struct Person {
char name[50]; // 名字
int age; // 年龄
float height; // 身高
};
上面的代码定义了一个名为Person
的结构体,包含了三个成员:name
(字符串类型,用于存储名字)、age
(整数类型,用于存储年龄)、height
(浮点数类型,用于存储身高)。
2. 结构体变量的声明和使用
定义结构体后,可以使用该结构体来声明结构体变量,并通过结构体的成员来访问或操作数据。
声明结构体变量:
struct Person person1;
给结构体成员赋值:
strcpy(person1.name, "John"); // 复制字符串到name
person1.age = 30; // 赋值给age
person1.height = 1.75; // 赋值给height
访问结构体成员:
printf("Name: %s\n", person1.name);
printf("Age: %d\n", person1.age);
printf("Height: %.2f\n", person1.height);
3. 结构体数组
如果你想管理多个相同类型的结构体,可以使用结构体数组。
struct Person people[3]; // 定义一个包含3个结构体的数组
// 给每个结构体成员赋值
strcpy(people[0].name, "Alice");
people[0].age = 25;
people[0].height = 1.68;
strcpy(people[1].name, "Bob");
people[1].age = 32;
people[1].height = 1.80;
通过结构体数组,能够轻松地管理和访问多个相同类型的结构化数据。
4. 结构体指针
在嵌入式系统中,由于内存资源有限,使用指针访问结构体数据是非常常见的做法。使用指针可以减少内存复制,提高效率。
定义结构体指针:
struct Person *pPerson;
pPerson = &person1; // 指针指向结构体变量person1
通过指针访问结构体成员:
通过指针访问结构体成员时,使用箭头运算符->
:
printf("Name: %s\n", pPerson->name);
pPerson->age = 40;
5. 嵌套结构体
结构体可以嵌套定义,即一个结构体可以包含另一个结构体作为其成员。嵌套结构体可以用于表示更复杂的数据结构。
struct Address {
char city[50];
char street[100];
};
struct Person {
char name[50];
int age;
struct Address address; // 嵌套的结构体
};
// 使用结构体嵌套
struct Person person1;
strcpy(person1.address.city, "New York");
strcpy(person1.address.street, "5th Avenue");
1.8 OSI七层模型
OSI模型的七层从下到上依次为:
1. 物理层(Physical Layer)
- 功能:物理层负责在网络设备之间传输原始的比特流,即0和1的二进制数据。它涉及网络通信的硬件方面,如电缆、交换机、网络接口卡、无线信号等。物理层规定了电气、机械、功能和规程标准,包括电压、电缆类型、连接器以及物理拓扑结构。
- 设备:集线器、网络接口卡(NIC)、光纤、双绞线、电缆等。
- 协议:例如IEEE 802.3(以太网标准),光纤通信协议等。
2. 数据链路层(Data Link Layer)
- 功能:数据链路层负责将物理层的比特流组织成数据帧(Frame),并提供节点间的可靠数据传输。它处理网络设备之间的错误检测和纠正,确保数据在网络中的正确传输。数据链路层还负责介质访问控制(MAC),管理设备如何访问物理介质。
- 子层:
- 逻辑链路控制(LLC)子层:负责帧的错误检测、流量控制等逻辑功能。
- 介质访问控制(MAC)子层:负责决定设备如何访问共享的传输介质。
- 设备:交换机、网桥、网络接口卡。
- 协议:以太网(Ethernet)、点对点协议(PPP)、HDLC、帧中继(Frame Relay)等。
3. 网络层(Network Layer)
- 功能:网络层负责数据包(Packet)的路由和转发,即决定数据从源节点传输到目标节点的路径选择。它还负责逻辑地址(IP地址)分配,管理主机到主机的通信。网络层是第一个涉及不同网络之间通信的层。
- 设备:路由器、三层交换机。
- 协议:IP(互联网协议)、ICMP(互联网控制消息协议)、IGMP(互联网组管理协议)等。
4. 传输层(Transport Layer)
- 功能:传输层负责在主机之间建立、维护和终止端到端的通信连接,并确保数据的可靠传输。它提供了错误检测、纠错、流量控制和数据重传等功能。传输层通常会分段传输数据,并在目标端重新组装数据。
- 设备:传输层主要是逻辑层面,没有物理设备。
- 协议:
- TCP(传输控制协议):提供面向连接的、可靠的通信。
- UDP(用户数据报协议):提供无连接的、快速但不可靠的通信。
5. 会话层(Session Layer)
- 功能:会话层负责建立、管理和终止应用程序之间的会话。它控制应用程序之间的对话,比如启动、维护和终止通信会话。会话层还提供会话恢复功能,确保因网络故障中断的会话可以恢复。
- 协议:常见的会话层协议包括RPC(远程过程调用协议)、SQL等。
6. 表示层(Presentation Layer)
- 功能:表示层负责数据格式的转换、编码和解码。它确保发送方和接收方之间的数据格式一致,解决数据的格式化和语法问题。表示层还提供数据加密和解密功能,保证数据的安全性。
- 典型功能:数据压缩、加密/解密、数据格式转换(如ASCII到EBCDIC,或JPEG到BMP)等。
- 协议:SSL(安全套接字层)、TLS(传输层安全协议)等。
7. 应用层(Application Layer)
- 功能:应用层是OSI模型中最接近用户的一层,它为用户和应用程序提供网络服务。应用层直接与用户交互,提供用户所需的各种网络服务,如电子邮件、文件传输、远程登录等。
- 协议:HTTP(超文本传输协议)、FTP(文件传输协议)、SMTP(简单邮件传输协议)、DNS(域名系统)、Telnet等。
8. 对比OSI七层模型
层级 | 名称 | 功能描述 | 相关设备 | 相关协议 |
---|---|---|---|---|
1 | 物理层 | 负责比特流的传输和物理硬件连接 | 集线器、网卡、交换机等 | IEEE 802.3(以太网)等 |
2 | 数据链路层 | 组织数据帧、介质访问控制、错误检测和纠正 | 交换机、网桥、网卡 | Ethernet、PPP等 |
3 | 网络层 | 路由选择、数据包传输、逻辑地址管理(如IP地址) | 路由器、三层交换机 | IP、ICMP、IGMP等 |
4 | 传输层 | 端到端的数据传输、流量控制、数据分段和重组 | (无硬件,主要是逻辑层) | TCP、UDP |
5 | 会话层 | 管理应用之间的会话,控制对话的建立、管理和终止 | (无硬件,逻辑层) | RPC、SQL等 |
6 | 表示层 | 数据格式转换、加密和解密、数据压缩 | (无硬件,逻辑层) | SSL、TLS等 |
7 | 应用层 | 提供应用程序访问网络的接口,用户与应用程序交互的层次 | (无硬件,逻辑层) | HTTP、FTP、SMTP、DNS等 |
9. OSI模型的作用
标准化通信协议:OSI模型为网络设备之间的通信提供了一个统一的标准,使得不同制造商的设备能够兼容工作。
分层设计:每一层有明确的职责,设计通信协议时可以集中处理某一层的功能,其他层次可独立开发。
简化故障诊断:由于网络通信被分为七层,问题可以根据不同层次来分析和解决,从而简化故障排查。
1.9 TCP/IP协议,哪些使用了TCP?
1. HTTP(超文本传输协议)
- 用途:用于在Web浏览器和Web服务器之间传输超文本(如网页)。HTTP通常在TCP端口80上运行。
- 特性:无状态的请求-响应协议,支持请求的多种方法(如GET、POST)。
2. HTTPS(安全超文本传输协议)
- 用途:是HTTP的安全版本,通过TLS/SSL加密传输数据,确保安全性和数据完整性。HTTPS通常在TCP端口443上运行。
- 特性:提供加密、身份验证和数据完整性。
3. FTP(文件传输协议)
- 用途:用于在客户端和服务器之间传输文件。FTP通常使用TCP端口21作为控制连接,同时会分配数据传输的端口(通常为20)。
- 特性:支持文件上传和下载,允许匿名访问。
4. SFTP(安全文件传输协议)
- 用途:是基于SSH的文件传输协议,提供文件传输的安全性和可靠性。
- 特性:在TCP端口22上运行,提供加密和安全认证。
5. SMTP(简单邮件传输协议)
- 用途:用于发送电子邮件。SMTP通常在TCP端口25上运行(或587端口用于安全连接)。
- 特性:提供邮件发送功能,支持邮件的转发和排队。
6. SSH(安全外壳协议)
- 用途:用于安全的远程登录和命令执行。SSH通常在TCP端口22上运行。
- 特性:提供加密和安全认证,是Telnet的安全替代方案。
7. SQL协议(如MySQL、PostgreSQL等)
- 用途:用于与数据库服务器进行通信。不同的数据库使用不同的TCP端口(如MySQL通常在3306端口,PostgreSQL在5432端口)。
- 特性:提供数据查询和管理功能,通常通过TCP进行可靠的数据传输。
TCP协议在许多网络应用中被广泛使用,特别是在需要可靠、顺序传输的场合。它提供了错误检测、重传和流量控制等功能,确保数据在不稳定的网络环境中仍能准确传输。
1.10 进程间通信方式
进程间通信(Inter-Process Communication, IPC)是指在操作系统中,多个进程之间相互交换数据的机制。由于进程具有独立的地址空间,彼此无法直接访问对方的内存,因此需要使用特定的通信方式。
1. 管道(Pipe)
管道是一种半双工的通信方式,数据只能单向流动(单向管道),需要两个管道才能实现双向通信。管道适合父进程和子进程之间的通信,因为它们共享相同的文件描述符。
- 命名管道(FIFO):命名管道可以实现无亲缘关系进程之间的通信。它与普通的管道类似,但它是一个系统文件,可以通过路径名进行访问。
- 特点:单向通信、数据流模型。
- 适用场景:常用于父子进程间的数据传输。
int pipefd[2];
pipe(pipefd); // 创建管道
write(pipefd[1], "Hello", 5); // 向管道写入数据
read(pipefd[0], buffer, 5); // 从管道读取数据
2. 消息队列(Message Queue)
消息队列是一种基于消息的通信机制,允许进程通过消息队列进行异步通信。消息队列支持随机读取消息,即根据消息的类型读取,而不一定按照先进先出的顺序读取。
- 特点:支持异步通信、可以对消息进行排序或优先级管理。
- 适用场景:当进程之间需要异步发送和接收消息时。
msgsnd(msgid, &message, sizeof(message), 0); // 发送消息到消息队列
msgrcv(msgid, &message, sizeof(message), type, 0); // 从消息队列接收消息
3. 共享内存(Shared Memory)
共享内存是一种高效的进程间通信方式,允许多个进程共享同一块内存区域。它是最快的IPC方式,因为数据直接存储在内存中,不需要经过内核来回拷贝。共享内存通常与信号量或互斥锁结合使用,以避免多个进程同时访问时产生竞争条件。
- 特点:高效、数据共享、需要同步机制(如信号量)来控制访问。
- 适用场景:大量数据需要在进程间频繁交换时。
int shmid = shmget(key, size, IPC_CREAT | 0666); // 创建共享内存段
void *shmaddr = shmat(shmid, NULL, 0); // 将共享内存附加到进程的地址空间
strcpy(shmaddr, "Hello"); // 写入共享内存
4. 信号量(Semaphore)
信号量是一种计数器,用于控制多个进程对共享资源的访问。信号量可以用来实现进程间的同步,防止竞争条件。通常与共享内存结合使用,确保多个进程在访问共享内存时的互斥和同步。
- 特点:用于同步进程操作,避免竞争条件。
- 适用场景:进程间的同步或互斥,特别是在使用共享资源时。
sem_wait(sem); // 等待信号量
sem_post(sem); // 释放信号量
5. 信号(Signal)
信号是一种用于进程间通信的机制,主要用于通知进程某些事件的发生。信号可以用于进程终止、暂停、继续执行等操作。虽然信号是一种简单的IPC方式,但它适合传递简短的、非实时的控制信息。
- 特点:用于传递简单的异步事件或通知。
- 适用场景:进程控制,处理中断或异常情况。
6. 套接字(Socket)
套接字是一种广泛使用的进程间通信方式,尤其适用于网络通信。套接字不仅用于网络中的进程间通信,也可以用于同一台主机上的不同进程之间的通信。可以实现双向通信,并且支持面向连接(TCP)或无连接(UDP)的通信。
- 特点:支持本地和远程通信、双向通信、基于网络协议。
- 适用场景:需要跨网络或不同主机进程间的通信。
socket(); // 创建套接字
bind(); // 绑定套接字
listen(); // 监听连接请求
accept(); // 接收连接请求
send(); // 发送数据
recv(); // 接收数据
7. 匿名映射(mmap)
mmap
是一种将文件或设备映射到进程的地址空间中的机制,不仅用于文件操作,也可以用于进程间通信。多个进程可以将同一个文件映射到它们的内存空间中,从而实现共享内存的功能。
- 特点:内存映射文件,允许进程直接通过内存地址访问文件内容。
- 适用场景:需要共享大量数据或文件时。
int fd = open("file", O_RDWR);
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
8. 内存映射文件(Memory-Mapped File)
内存映射文件允许多个进程将同一个文件映射到它们的地址空间,通过共享文件实现进程间通信。这种方式类似于共享内存,但共享的数据保存在文件中。
- 特点:允许通过内存地址直接访问文件内容,支持进程间共享数据。
- 适用场景:需要使用文件存储的共享数据时。
9. 总结对比:
通信方式 | 特点 | 适用场景 |
---|---|---|
管道 | 单向通信,父子进程间的常见通信方式 | 父子进程之间的数据传输 |
消息队列 | 异步通信,支持消息优先级 | 进程之间的异步消息传递 |
共享内存 | 高效数据共享,需要同步机制防止竞争 | 大量数据频繁交换,多个进程需要访问相同数据 |
信号量 | 进程同步或互斥,防止资源竞争 | 需要控制多个进程对共享资源的访问 |
信号 | 异步事件通知,传递简短控制信息 | 进程终止、暂停等操作 |
套接字 | 支持本地或远程通信,双向通信 | 跨网络或主机的进程通信 |
匿名映射 | 文件或设备映射到内存,支持数据共享 | 需要共享内存进行通信 |
D-Bus | 桌面环境中的消息总线系统 | 桌面应用程序之间的通信 |
内存映射文件 | 将文件映射到内存中,支持进程共享数据 | 需要通过文件存储共享数据 |
1.11 信号量是怎么传递的?
信号量可以看作是一个整数值,它表示资源的可用数量。当信号量的值大于0时,表示有资源可供使用;当信号量的值为0时,表示资源不可用,进程必须等待。信号量的两个主要操作是:
1. P操作(wait、down、decrement):信号量的值减1,如果值小于0,进程会阻塞等待资源变得可用。
2. V操作(signal、up、increment):信号量的值加1,如果有进程在等待资源(即信号量值小于0),则唤醒一个等待的进程。
1. 信号量的工作机制:
信号量本身不传递数据,而是通过控制访问来实现进程间的同步或互斥。它的工作原理如下:
(1)初始化信号量:信号量被初始化为一个非负整数值,通常表示可用资源的数量。这个值由系统管理员或程序设置。
- 对于互斥访问,信号量初始值为1,表示只有一个资源可用,确保同一时刻只能有一个进程访问共享资源(称为二进制信号量或互斥量)。
- 对于同步访问,信号量初始值可以大于1,表示有多个资源可以被多个进程同时访问。
(2)P操作(wait操作):当一个进程希望访问共享资源时,它执行P操作:
- 如果信号量值大于0,则进程将信号量值减1,表示它获取了资源,继续执行。
- 如果信号量值等于0或小于0,则进程会被阻塞,等待资源可用。当其他进程释放资源后,该进程将被唤醒。
(3)V操作(signal操作):当一个进程使用完共享资源后,它执行V操作:
- 信号量值加1,表示资源已被释放。如果有其他进程因等待资源而阻塞,系统将唤醒其中一个等待的进程,允许其继续执行。
2. 信号量的使用场景:
(1)互斥(Mutex)
当多个进程或线程需要访问一个共享的临界资源时,信号量可以保证同一时刻只能有一个进程或线程访问该资源。这种场景常见于并发编程中,需要确保数据的一致性和防止竞争条件。
- 信号量初始化为1,表示该资源只有一个可用,通常用于互斥量(Mutex)。
- 例子:多个线程需要写入同一个文件时,使用信号量确保同时只有一个线程在写入文件。
sem_wait(&mutex); // P操作:等待信号量
// 临界区代码
sem_post(&mutex); // V操作:释放信号量
(2)同步(Synchronization)
信号量也用于进程间的同步,确保一个进程的某些操作发生在另一个进程的某些操作之后。这种场景下,信号量的值可能初始化为0,进程在进行某些操作之前需要等待另一个进程的信号。
- 信号量初始化为0,表示进程需要等待另一个进程完成某些操作。
- 例子:进程A必须等待进程B完成数据准备后才能继续运行,此时信号量可以用于进程间的同步。
// 进程A
sem_wait(&sync); // 等待信号,只有当进程B发送信号时,才能继续
// 进程A继续运行
// 进程B
// 数据准备完毕
sem_post(&sync); // 发送信号,通知进程A可以继续执行
3. 信号量的传递方式
信号量的传递并不是通过直接发送数据,而是通过操作系统内核的调度和资源管理机制来实现的。信号量通常是由操作系统提供的一种内核对象,不同的进程可以通过系统调用访问同一个信号量:
(1)内核维护信号量的状态:信号量本质上是由内核管理的一个计数器,以及一个阻塞等待队列。当某个进程执行P操作并阻塞时,它会被挂起到信号量的等待队列中,直到信号量的值变为正数并唤醒等待的进程。
(2)进程通过系统调用操作信号量:每个进程可以通过系统调用(如sem_wait()
和sem_post()
)操作信号量。内核通过调度器来管理这些进程间的同步,当某个进程完成资源释放时,内核唤醒等待队列中的下一个进程。
4. 信号量的类型
(1)二进制信号量(Binary Semaphore):
- 这种信号量的值只有0或1,常用于互斥锁(Mutex)。当信号量为1时,表示资源可用;当信号量为0时,表示资源正在被使用。
(2)计数信号量(Counting Semaphore):
- 这种信号量可以有一个大于1的初始值,表示允许多个进程同时访问有限的资源(如有多个线程同时访问一定数量的相同资源)。它常用于资源池管理。
5. 使用信号量的注意事项
- 死锁问题:不当使用信号量可能导致死锁,比如两个进程或线程都在等待对方释放资源。
- 优先级反转:当低优先级进程持有信号量时,可能会导致高优先级进程长时间等待,导致优先级反转问题。
- 避免繁忙等待:信号量的实现通常依赖于操作系统的阻塞机制,避免让进程在等待时消耗CPU资源。
1.12 shell命令查找某个目录及子目录的某个文件
在Linux或UNIX系统中,可以使用find
命令来查找某个目录及其子目录中的某个文件。
find <directory> -name <filename>
<directory>
:要查找的起始目录。-name <filename>
:查找名称为filename
的文件。文件名可以是完整的,也可以包含通配符(如*
)。
假设你要在/home/user/documents
目录及其子目录中查找名为example.txt
的文件,命令如下:
find /home/user/documents -name "example.txt"
这个命令会从/home/user/documents
目录开始,递归地查找所有子目录中名为example.txt
的文件。
如果你要查找文件名匹配某个模式的文件,比如查找以.txt
结尾的所有文件,可以使用通配符*
:
find /home/user/documents -name "*.txt"
这个命令将查找/home/user/documents
目录及其子目录下的所有.txt
文件。
如果你想查找时忽略文件名的大小写,可以使用-iname
选项:
find /home/user/documents -iname "example.txt"
这将查找文件名为example.txt
、Example.txt
或其他大小写组合的文件。
-type f
:只查找文件,忽略目录。
-type d
:只查找目录。
-size +10M
:查找大于10MB的文件。
-exec
:对查找到的文件执行某个命令。
二、9.7面经
面经素材来源于牛客网:
2.1 裸机和RTOS,如何挑选,包括Linux驱动
在嵌入式系统开发中,选择裸机(Bare Metal)编程或实时操作系统(RTOS)涉及多个技术和设计层面的考量。每种方式都有自己的优点和适用场景,因此在做选择时需要根据系统需求、资源限制和应用特性做出权衡。
1. 裸机编程(Bare Metal Programming)
(1)定义:
裸机编程指的是没有任何操作系统支持,直接在硬件上运行代码。程序员手动管理硬件资源、时间调度和其他系统功能。
(2)特点:
- 性能高:因为没有操作系统的开销,代码直接运行在硬件上,能充分发挥硬件的性能。
- 可预测性:程序对系统行为有完全的控制,所有操作都是可控的,没有中断或调度器的干扰。
- 小资源占用:由于没有操作系统,程序的占用内存和CPU资源非常少,适用于资源极为受限的环境。
(3)适用场景:
- 简单应用:程序逻辑相对简单,功能有限的应用,比如小型传感器、LED控制器、简单通信设备等。
- 严格的实时性要求:由于没有操作系统,所有中断、定时、外设控制等都由开发人员直接编写,具有高精度的时间控制。
- 极限资源条件:当系统的RAM、ROM和CPU资源非常有限时,比如某些微控制器,裸机编程可以减少操作系统的开销。
(4)缺点:
- 难以维护和扩展:裸机编程没有操作系统抽象层,增加了硬件相关代码的复杂性。开发人员需要手动处理每个硬件外设和中断服务程序,代码往往难以扩展和复用。
- 多任务处理困难:如果应用需要处理多个任务,开发人员需要手动实现任务切换和优先级管理,增加了复杂性。
2. 实时操作系统(RTOS)
(1)定义:
RTOS是一种专门设计用于实时应用的操作系统,提供任务管理、调度、同步和中断处理等功能。RTOS可以保证任务在特定的时间约束内执行。
(2)特点:
- 任务管理:RTOS提供了多任务管理功能,支持任务优先级、抢占式调度等,简化了复杂任务的实现。
- 实时性:RTOS保证了任务的实时执行,通常具有确定的中断响应和任务调度时间,适合实时控制系统。
- 硬件抽象:RTOS通常包含硬件抽象层(HAL),使得应用代码和硬件解耦,增加了移植性和代码复用性。
(3)适用场景:
-
多任务并发应用:例如机器人控制、工业自动化、复杂传感器管理、网络设备等,RTOS使得处理多个任务变得容易。
- 实时性要求高的场景:RTOS在设计上确保了任务的实时性和可预测性,适合时间关键的应用,如汽车电子、医疗设备、航空航天等。
- 更复杂的嵌入式应用:需要外设的驱动、网络协议栈、文件系统等,RTOS提供了现成的库和服务。
(4)缺点:
- 资源占用高于裸机:RTOS占用了额外的内存和CPU资源,虽然一般比全功能操作系统要轻量,但对于极度受限的设备,RTOS的开销仍然是个问题。
- 增加了复杂性:需要学习和理解RTOS的API、任务调度和中断处理机制,增加了系统的复杂性。
3. Linux 和 Linux 驱动开发
(1)定义:
Linux是一种通用操作系统,通常用于嵌入式系统中具备相对强大计算和存储资源的设备。嵌入式Linux允许开发者基于Linux内核构建高度可定制的系统,并提供丰富的驱动支持。
(2)特点:
- 丰富的驱动支持:Linux内核包含了大量的现成驱动,几乎覆盖了所有常见的外设,开发者不需要重写底层驱动。
- 强大的开发生态:嵌入式Linux支持多种库、工具和应用程序,开发环境和调试工具非常完善。
- 网络和文件系统支持:Linux天然支持多种网络协议和文件系统,适合需要复杂通信和数据存储的应用。
- 可扩展性强:Linux可以支持更高层的操作系统功能,如多用户管理、安全机制、图形界面等。
(3)适用场景:
- 复杂嵌入式系统:需要多种外设、网络连接和丰富的用户应用支持,比如智能设备、网络设备和智能家居设备。
- 需要快速开发和部署的场景:Linux的丰富生态和开源软件支持,使得开发者可以快速构建和部署产品。
- 非严格实时性场景:虽然Linux可以通过实时扩展(如PREEMPT-RT)实现实时性,但它的实时性通常不如RTOS严格。适用于对时间精度要求相对宽松的应用。
(4)Linux 驱动开发:
- 内核模块开发:Linux内核通过模块化的驱动结构支持设备驱动程序的开发,开发者可以基于现有框架开发新的设备驱动。
- 用户态与内核态的交互:通过字符设备、块设备或网络设备接口,驱动程序允许用户态程序与硬件交互。
- 移植性和可扩展性:Linux内核的可移植性强,可以跨不同硬件平台运行,驱动代码在不同平台间的可移植性也较好。
(5)缺点:
- 开销大:嵌入式Linux对内存和存储资源的要求高于RTOS和裸机,适合资源较为充裕的设备。
- 实时性较差:虽然Linux可以通过内核配置增强实时性(如PREEMPT-RT),但它的实时性能不如RTOS高。
4. 如何挑选:
(1)系统复杂度:
- 如果系统逻辑简单,且不需要多任务并发处理,裸机编程可能是最佳选择。
- 如果系统涉及多个并发任务,RTOS提供了任务管理、定时器和中断等功能,能有效提高开发效率。
- 如果系统需要复杂的外设支持、文件系统、网络协议栈等,Linux通常是更好的选择。
(2)实时性要求:
- 如果系统对实时性要求非常高且严格控制执行时间,RTOS是更合适的选择。
- 裸机编程也可以满足极端的实时性需求,但维护和扩展的难度较大。
- 如果实时性要求不那么严格,Linux可以通过扩展(如PREEMPT-RT)满足一定的实时性需求。
(3)资源限制:
- 对于资源非常受限的系统(如微控制器),裸机编程或者轻量级RTOS是理想的选择。
- 对于资源较为丰富的系统,嵌入式Linux可以带来更多的功能和扩展性。
(4)开发周期和成本:
- 如果开发周期紧张,并且需要快速构建功能丰富的系统,嵌入式Linux或RTOS提供的现成驱动和库可以大大缩短开发时间。
- 裸机编程虽然性能高,但开发和维护成本较高,适合非常专用的应用场景。
裸机编程适合简单、资源受限、对时间精度要求严格的嵌入式应用。RTOS适合多任务并发、实时性要求高的嵌入式系统,且支持较复杂的应用和外设。嵌入式Linux适合资源丰富、需要复杂功能(如文件系统、网络、图形界面等)的嵌入式系统。
2.2 RTOS如何保证实时性
实时操作系统(RTOS)通过一系列关键机制来确保其任务可以在预定的时间约束内执行,从而保证实时性。
1. 抢占式多任务调度:
RTOS使用抢占式多任务调度来确保最高优先级的任务能够及时执行。抢占式调度意味着:
- 当一个任务正在运行时,如果有更高优先级的任务准备好执行,RTOS会立即暂停当前任务并切换到更高优先级的任务。
- 这种方式确保高优先级的任务可以在最短时间内响应,而不会被低优先级任务阻塞。
调度器通过实时分析任务的优先级,确保最高优先级的任务始终被分配到CPU上,避免了任务执行的不可预测性。
2. 确定性的中断处理:
RTOS确保中断处理时间确定,即从硬件中断触发到中断服务例程(ISR)开始执行的延迟是可预测的。为了实现这一点:
- 中断优先级管理:RTOS通常允许开发者设置中断的优先级,以便高优先级的中断可以打断低优先级的中断。
- 快速中断处理:RTOS中通常要求ISR尽量简洁、快速。复杂的处理可以在中断返回后通过任务来完成,这样中断处理的时间变得可控。
通过严格管理中断处理,RTOS可以降低任务和中断之间的干扰,从而实现高精度的响应时间。
3. 优先级调度算法:
RTOS通常使用基于优先级的调度算法来决定任务的执行顺序。常见的调度算法有:
-
固定优先级调度:每个任务在系统启动时被分配固定优先级,优先级高的任务会优先调度。常见的调度算法是优先级抢占调度(Priority Preemptive Scheduling),它确保高优先级任务始终可以中断低优先级任务。
-
时间片轮转调度(Round Robin):对于具有相同优先级的任务,RTOS可以通过时间片轮转的方式公平分配CPU时间,保证每个任务在一定时间内都可以运行。
-
优先级反转保护:为了防止优先级反转问题,RTOS会实现一些机制,如优先级继承,在任务由于资源竞争而阻塞时,低优先级任务可能会暂时继承高优先级任务的优先级,避免实时任务被意外延迟。
4. 任务调度的确定性:
RTOS调度器的行为是确定性的,即调度延迟是可预测的,通常是常数时间或在已知的时间范围内。这种确定性使得开发人员能够计算和保证任务的执行时限(deadline)。
例如,RTOS通常在每次任务调度切换时花费固定的时间,而且不管系统中有多少任务,调度开销都是已知的。
5. 实时定时器和定时中断
RTOS提供了高精度定时器和定时中断,用于精确控制任务的开始时间或执行周期。常见的机制包括:
- 周期性任务调度:RTOS可以通过定时器精确地按照预定的时间间隔触发任务执行。开发人员可以定义任务的周期,RTOS确保任务在指定的时间间隔内被调度执行。
- 延迟任务:允许任务在延迟指定时间后执行,RTOS会根据定时器计算出任务的启动时间并将其加入调度队列。
这种基于定时器的机制使得开发者能够实现精确的任务调度和执行。
6. 内存管理的实时性
RTOS通常避免使用复杂的内存管理机制(如动态内存分配),因为这些操作在执行时间上是不确定的。为了保证实时性,RTOS一般采用以下策略:
- 静态内存分配:任务的栈、全局变量等资源在系统启动时分配,避免了运行时的动态内存分配,从而保证任务的执行时间可预测。
- 简单且确定的内存管理机制:如果必须进行动态内存分配,RTOS会采用固定大小的内存块(如内存池),以确保分配和释放的时间是固定的。
这种内存管理方式避免了由于动态内存分配引起的不可预测延迟,从而增强了系统的实时性。
7. 任务的时间分配和资源管理:
RTOS通过有效的资源管理和任务调度策略来保证任务的时间需求:
-
硬实时和软实时:硬实时系统要求任务在严格的时间约束内完成,RTOS可以通过设定明确的时间限制来保证关键任务的执行。而软实时系统则允许一些时间偏差,但总体上仍然保证任务的实时性。
-
实时任务的优先级保护:RTOS通过优先级调度、任务排队和资源分配策略来防止低优先级任务阻塞高优先级任务的执行,确保关键任务可以按时完成。
8. 中断延迟管理:
RTOS通常通过限制和优化中断处理的方式来控制中断延迟,以避免中断过多占用CPU资源:
- 中断封装策略:大多数RTOS将中断处理分为快速的中断服务例程(ISR)和较慢的后处理部分(如延迟服务例程),从而避免长时间的中断服务影响任务调度。
- 优先级中断机制:RTOS允许对中断分配优先级,确保关键中断可以打断低优先级的中断,减少关键任务的延迟。
RTOS通过以下方式来保证实时性:
- 抢占式多任务调度,高优先级任务及时执行。
- 确定性的中断处理,确保快速和可预测的响应时间。
- 优先级调度算法,避免低优先级任务阻塞高优先级任务。
- 确定性的任务调度,确保调度行为在固定时间内完成。
- 高精度定时器和中断机制,实现精确的任务执行控制。
- 内存管理的确定性,避免不可预知的延迟。
- 轻量化的内核设计,减少内核操作的开销。
- 中断延迟管理,减少中断对任务调度的干扰。
2.3 FreeRTOS源码如何实现任务调度
FreeRTOS是一个流行的实时操作系统(RTOS),广泛用于嵌入式系统中。它的任务调度机制相对简单,但功能强大。
1. 任务控制块(TCB)
FreeRTOS使用任务控制块(Task Control Block, TCB)来管理任务的状态和信息。
每个任务都有一个TCB,TCB包含了以下信息:
- 任务的优先级
- 任务的堆栈指针
- 任务的状态(就绪、运行、阻塞等)
- 任务的名字
- 任务的时间片(用于时间片轮转调度)
- 其他与任务调度和管理相关的信息
2. 就绪列表:
FreeRTOS有一个就绪列表,它是一个优先级队列,按照任务的优先级来存储就绪任务。FreeRTOS支持不同的调度策略,最常用的是固定优先级调度。
具体实现中,FreeRTOS使用一个静态数组来存储不同优先级的任务就绪列表。
3. 上下文切换:
上下文切换是任务调度的核心部分。FreeRTOS通过保存和恢复任务的上下文来实现上下文切换。上下文通常包括以下内容:
- CPU寄存器的值
- 程序计数器(PC)
- 堆栈指针(SP)
FreeRTOS使用汇编语言和C语言混合的方式来实现上下文切换,以确保能够高效、快速地切换任务。
4. 调度算法
FreeRTOS使用以下调度算法来决定哪个任务可以运行:
(1)固定优先级调度
在FreeRTOS中,任务具有固定的优先级。每次系统调用调度时,RTOS会检查就绪列表,选择优先级最高的任务来运行。
(2)时间片轮转
对于同一优先级的任务,FreeRTOS使用时间片轮转策略。每个任务在被调度时会获得一个时间片,当时间片用尽时,调度器会将当前任务放回就绪列表,并调度下一个同优先级的任务。
5. 任务调度流程
(1)初始化: 在系统启动时,FreeRTOS会初始化任务控制块(TCB)和就绪列表。
(2)任务创建: 当任务被创建时,FreeRTOS会为该任务分配TCB,并将其添加到相应优先级的就绪列表中。
(3)调度器启动: 调度器启动后,FreeRTOS将运行优先级最高的就绪任务。调度器通常是在系统的主循环中定期调用的。
(4)上下文切换: 当发生某种事件(如任务阻塞、任务延时、或高优先级任务就绪)时, FreeRTOS会执行上下文切换,将当前任务的上下文保存到其TCB中,并从就绪列表中选择新的最高优先级任务。
(5)任务运行: 调度器将新的任务的上下文恢复并开始运行。
(6)任务结束: 当任务完成或被阻塞时,系统会再次进行任务调度。
2.4 FreeRTOS如何实现任务切换:
1. 任务切换的完整流程:
以下是一个 FreeRTOS 任务切换的完整流程:
(1)当前任务的上下文保存:
- 调用
portSAVE_CONTEXT
宏,保存当前任务的 CPU 寄存器、程序计数器等信息到该任务的堆栈中。 - 更新当前任务的 TCB,保存当前任务的堆栈指针。
(2)选择下一个任务:
- 调用
vTaskSwitchContext()
,调度器从就绪列表中选择下一个优先级最高的任务。 - 将下一个任务的 TCB 设置为当前任务。
(3) 恢复新任务的上下文:
- 调用
portRESTORE_CONTEXT
宏,从新任务的 TCB 中恢复任务的堆栈指针和寄存器状态。 - 新任务从它上次被切换时的位置继续执行。
2. 中断中的任务切换
在 FreeRTOS 中,中断服务例程(ISR)也可能引发任务切换。例如,在中断中唤醒一个高优先级的任务时,系统会在中断返回时执行任务切换。
通常,FreeRTOS 提供了 portYIELD_FROM_ISR()
或 portEND_SWITCHING_ISR()
这样的宏来在中断结束时触发任务切换。这些宏用于通知内核,当前中断中唤醒的任务可能是系统中优先级最高的任务,要求在中断结束后立即切换到该任务。
2.5 I2C驱动的时序
1. I2C基本时序
I2C协议使用两根信号线:
- SDA(Serial Data Line):串行数据线
- SCL(Serial Clock Line):串行时钟线
2. 通信过程
I2C通信分为两个主要阶段:起始条件(START)和停止条件(STOP)。在这两个条件之间,数据以字节为单位进行传输。
(1)起始条件(START Condition)
- 由主设备发出。
- SDA线在SCL线为高电平时由高变低。
(2)停止条件(STOP Condition)
- 由主设备发出。
- SDA线在SCL线为高电平时由低变高。
(3)数据传输
- 数据在SDA线上传输,SCL线提供时钟信号。
- 每次数据位的传输都要在时钟的上升沿或下降沿进行采样。数据在SCL为高电平时有效。
(4)ACK/NACK应答
- 在每个字节传输后,接收设备会发送一个应答信号(ACK),表示成功接收到数据。
- 若接收设备不想接收更多数据,会发送一个非应答信号(NACK)。
3. I2C时序图
以下是I2C通信的时序图(简化示例):
SCL: _____|‾‾‾|_____|‾‾‾|_____|‾‾‾|_____|‾‾‾|_____
| | | | | | | |
SDA: __|‾‾‾|_____|‾‾‾|_____|‾‾‾|_____|‾‾‾|____|‾
| | | | | | |
START ACK Byte ACK Byte ACK STOP
4. 时序描述
(1)起始条件:
- SDA从高到低的转换,表示开始传输。
(2) 数据字节:
- 每个数据字节为8位,SCL在每个时钟周期的上升沿传输数据,数据在SCL为高电平时有效。
- 例如,若传输的数据为
0xA5
,那么对应的位序列会依次通过SDA线发送。
(3)应答位:
- 接收方在每个字节之后会发送一个ACK(或NACK),ACK位在SCL的第9个时钟周期传输。
(4) 停止条件:
- SDA从低到高的转换,表示结束传输。
5. 具体的时序要求 :
- SCL频率:I2C的标准频率为100kHz(标准模式)和400kHz(快速模式),更高的频率(如高速模式)可达3.4MHz。
- SDA和SCL的稳定性:在SCL为高电平时,SDA必须保持稳定,直到SCL为低电平。
2.6 为什么需要头文件
头文件在C和C++编程中起着重要的作用。它们通常以`.h`扩展名结尾,包含了函数的声明、宏定义、常量、数据结构、类的定义等。在编写和维护程序时,使用头文件有以下几个主要原因:
1. 函数声明
头文件允许你在一个地方声明函数,这样可以在多个源文件中使用这些函数,而不需要重复声明。这有助于:
- 减少重复代码:避免在每个源文件中重复定义函数。
- 提供接口信息:给其他源文件说明如何调用这些函数,包括参数类型和返回类型。
2. 模块化编程
使用头文件可以帮助你将代码分成多个模块,每个模块可以有自己的功能和接口。这种结构化的方式使得程序更加清晰和可维护。
- 增强可读性:清晰分隔不同模块的代码,使得每个模块可以更容易地理解和管理。
- 便于团队合作:多个程序员可以在同一项目中并行工作,每个人专注于不同的模块,使用头文件来定义模块间的接口。
3. 类型定义和结构体定义
头文件可以包含结构体、类、枚举等类型的定义。这允许不同的源文件共享相同的数据结构,从而保持数据的一致性。
- 确保一致性:当结构体或类的定义在多个源文件中使用时,头文件确保所有源文件使用相同的定义。
- 简化数据管理:可以将数据结构的定义集中在一个地方,便于修改和维护。
4. 宏定义和常量
许多程序使用宏(通过`#define`定义)和常量(`const`关键字)来管理常量值和简化代码。头文件可以用来集中管理这些定义。
- 简化修改:当需要修改常量时,只需在头文件中进行更改,所有依赖该头文件的源文件都会自动更新。
- 提高可读性:使用有意义的名称而不是魔法数字可以提高代码的可读性。
头文件在C和C++编程中是一个不可或缺的组件。它们不仅提供了代码组织和分层的机制,还增强了代码的可读性和可维护性,促进了模块化编程和团队协作。
2.7 头文件分尖括号和双引号,应该如何写引入的顺序
在C和C++编程中,头文件的引入分为两种方式:使用尖括号 (<
和双引号 (""
)。这两种方式用于不同的情况,它们的搜索路径和目的有所不同,因此在编写程序时有一套推荐的引入顺序。
1. 尖括号 (<>
) 用法:
尖括号用于引入标准库头文件或者由编译器提供的系统头文件。
编译器首先会在系统标准库或预设的头文件路径中搜索这些文件,而不会去当前目录中查找。
2. 双引号 (""
) 用法:
双引号用于引入用户自定义的头文件,即你自己编写的项目中的头文件。
编译器首先会在当前源文件所在的目录中搜索这个头文件。如果在当前目录中没有找到,编译器才会去标准头文件路径中查找。
3. 推荐的引入顺序:
(1)标准库的头文件:首先引入C或C++的标准库头文件,它们使用尖括号<>
,例如 <stdio.h>
, <stdlib.h>
, <string>
, <vector>
等。这些文件是由编译器或标准库提供的,通常是安全且稳定的。
(2)第三方库的头文件:接着引入你可能使用的第三方库的头文件。这些库可能会放在特定的路径中,通常也使用尖括号<>
来表示。例如 <boost/algorithm/string.hpp>
或 <json/json.h>
等。
(3)项目内部的头文件:最后引入项目中的自定义头文件,使用双引号""
。这是因为你自己的头文件通常依赖标准库或第三方库提供的功能,所以应该在它们之后引入。
2.8 就是要先写双引号再写尖括号,为什么?
自定义头文件通常依赖于标准库或第三方库的功能。先引入标准库或第三方库的头文件可以确保自定义头文件在使用时能够正常找到其依赖的类型和函数。如果先引入自定义头文件,可能会导致编译错误,因为自定义头文件中的某些声明或定义可能依赖于这些库。
避免名称污染:如果先引入自定义头文件并且该文件中有与标准库或第三方库相同的命名,可能会造成命名冲突。在引入顺序中确保先引入标准库或第三方库,有助于最小化这种风险。
2.9 float可以移位吗
在C和C++中,浮点数类型(如 float
和 double
)不能直接进行移位操作。移位操作(如左移 <<
和右移 >>
)仅适用于整型数据类型(如 int
, unsigned int
, long
等)。如果你尝试对浮点数进行移位操作,编译器会报错或者产生未定义行为。
2.10 就是要float移位怎么办?
尽管不能直接对 float
进行移位操作,但你可以通过数学操作实现类似的功能。特别是通过乘法和除法来实现 "移位" 的效果:
- 左移(乘以2的幂次):左移
n
位通常相当于乘以2^n
。 - 右移(除以2的幂次):右移
n
位通常相当于除以2^n
。
1. 模拟左移(乘以2的幂次)
左移 n
位相当于将一个浮点数乘以 2^n
。例如,左移1位相当于乘以2,左移2位相当于乘以4,以此类推。
float x = 3.5;
int n = 2; // 假设要左移2位
x = x * (1 << n); // 等价于 x * 2^n,即乘以4
2. 模拟右移(除以2的幂次)
右移 n
位相当于将一个浮点数除以 2^n
。例如,右移1位相当于除以2,右移2位相当于除以4。
float x = 3.5;
int n = 2; // 假设要右移2位
x = x / (1 << n); // 等价于 x / 2^n,即除以4
2.11 new和malloc
new
和 malloc
是用于动态内存分配的两个不同机制,分别用于 C++ 和 C。
1. new
(C++)
-
用途:用于在堆上动态分配对象或数组。
-
语法:
int* ptr = new int; // 分配一个int
int* arr = new int[10]; // 分配一个int数组
-
功能:
- 自动调用构造函数来初始化对象。
- 支持类型安全,返回正确的对象类型指针。
-
功能:
- 自动调用构造函数来初始化对象。
- 支持类型安全,返回正确的对象类型指针。
-
释放内存:
- 使用
delete
释放单个对象,用delete[]
释放数组。
- 使用
delete ptr;
delete[] arr;
-
异常处理:
- 分配失败时,
new
会抛出std::bad_alloc
异常。
- 分配失败时,
2.malloc
(C)
-
用途:用于在堆上分配指定字节数的内存块。
-
语法:
int* ptr = (int*)malloc(sizeof(int)); // 分配一个int
int* arr = (int*)malloc(10 * sizeof(int)); // 分配一个int数组
-
功能:
- 不会调用构造函数,因此仅适用于基本数据类型或结构体。
- 返回
void*
,需要强制转换为目标类型。
-
释放内存:
- 使用
free
释放内存。
- 使用
free(ptr);
free(arr);
-
错误处理:
- 分配失败时返回
NULL
,需要手动检查。
- 分配失败时返回
2.12 进程间、线程间通讯,管道的优势
进程间和线程间的通信是软件开发中非常重要的概念,能够帮助不同的进程或线程交换数据和协调工作。
1. 进程间通信(IPC)
(1)共享内存:
- 进程可以直接访问同一块内存区域。
- 优势:速度快,适合大量数据传输。
- 缺点:需要同步机制,防止竞争条件。
(2)消息队列:
- 进程通过消息传递进行通信。
- 优势:避免了直接同步问题。
- 缺点:较为复杂的管理,性能稍逊于共享内存。
(3)信号:
- 用于异步通知进程某个事件发生。
- 优势:简单,适合简单的事件通知。
- 缺点:只能传递有限的信息。
(4)套接字:
- 主要用于网络通信,也可用于本地进程间通信。
- 优势:灵活,可以用于不同主机之间的通信。
- 缺点:相对复杂,性能不如共享内存。
(5)管道:
- 单向或双向数据流,常用于父子进程通信。
- 优势:简单,使用方便。
- 缺点:只能用于相关进程,适合数据流通信。
2. 线程间通信
(1)共享变量/内存:
- 线程共享进程的全局变量。
- 优势:高效,直接访问。
- 缺点:需小心同步,防止竞争条件。
(2)条件变量、互斥锁、读写锁:
- 用于线程同步和协调。
- 优势:同步效果好。
- 缺点:需要正确使用来防止死锁。
(3)信号量:
- 用于管理资源访问。
- 优势:灵活,有效控制资源使用。
- 缺点:复杂性高。
3. 管道的优势
(1)简单性:管道提供了简单的接口,方便快速实现进程间通信。
(2)标准化:管道是 UNIX 系统的基本功能,大多数系统都支持。
(3)数据流:适合连续的数据流,符合生产者-消费者模型。
(4)自动化:管道自动同步,无需手动管理锁。
2.13 同进程多线程,可以socket通讯吗
在同一个进程内,多线程之间通常不需要通过套接字(socket)进行通信,因为它们可以直接访问共享内存和进程的全局变量,这种方式更高效。
虽然通常不需要,但如果有特殊需求,可以在同一进程内的多个线程之间使用套接字进行通信。
逻辑隔离:如果你希望在代码结构上将线程间通信和网络通信统一处理。
模拟网络环境:在开发和测试中模拟不同主机之间的网络通信。
版权声明:本文标题:CVTE-嵌入式面经一面面经准备(已通过) 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1729001521h1305434.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论