嵌入式秋招面经八股(三)

嵌入式秋招面经八股(三)

经纬恒润

1. 宏定义

宏定义在编译过程的预处理阶段生效。处理以#开头的指令,如#include、#define等,宏定义在此阶段展开,替换为相应的代码和值,生成一个预处理的源文件,供后续编译使用。

2. 指针和数组的异同

  • 数组:连续内存块的直接表示,大小固定,数组名是常量指针
  • 指针:存储地址的变量,可以指向任意内存位置,灵活性更高

3. 结构体描述

结构体(struct)是C/C++中一种用户自定义的数据类型,用于将不同类型的数据组合在一起,形成一个复合数据类型。结构体可以包含多个成员变量,每个成员变量可以是不同的数据类型(如 int、float、char 等)。

优点:

  1. 数据封装:将相关的数据组合在一起,便于管理和使用
  2. 代码可读性:通过结构体可以更清晰地表示复杂的数据结构
  3. 灵活性:结构体可以包含不同类型的成员,适应多种需求

4. 结构体内存对齐规则

结构体中的内存对齐是为了提高内存访问效率,确保数据在内存中的存储位置符合硬件的要求。

  • 对齐值:每个数据类型都有一个对齐值,通常是其大小(以字节为单位)。例如,int 的对齐值通常是 4 字节,double 的对齐值通常是 8 字节
  • 结构体成员的对齐:结构体中的每个成员都按照其对齐值进行对齐。成员的存储位置必须是其对齐值的整数倍
  • 结构体整体的对齐:结构体的整体大小必须是其最大成员对齐值的整数倍。结构体的对齐值通常等于其最大成员的对齐值

5. 结构体和联合体的关系与区别

结构体(struct)和联合体(union)都是C/C++中用于定义自定义数据类型的方式。二者语法相似,并且可以嵌套使用。

区别:

  • 存储方式
    • 结构体中每个成员各自占据内存,结构体的大小通常为所有成员大小的和
    • 联合体中所有成员共享一块内存区域,联合体的大小等于其最大成员的大小

系统编程

6. I/O多路复用:epoll、poll、select

三者都是Linux下用于I/O多路复用的系统调用,用于同时监视多个文件描述符(fd)的状态,从而避免阻塞式I/O造成的性能损失。

  • select:最早的I/O复用机制,受到文件描述符数量的限制,并且每次调用都要复制fd的集合,性能较低
  • poll:改进了select,去除了fd的数量限制,但仍然是线性遍历所有的fd,性能较差
  • epoll:由Linux引入,使用事件通知机制,无需遍历所有的fd,大幅提高性能

7. C++虚函数

在 C++ 中,虚函数是一种支持多态的机制,使得基类的指针或引用可以调用派生类的重写(override)方法。定义方式是在基类的成员函数前加上 virtual 关键字。

实现机制

C++中通过虚函数表和虚表指针来实现虚函数的动态绑定机制:

  • 每个包含虚函数的类都有一张虚表,表中存储的是该类的虚函数的地址,虚表是静态存储的,在编译时就已经生成了
  • 每个对象都有一个虚表指针(编译阶段产生),指向该类的虚表
  • 当通过基类指针调用虚函数时,程序会根据虚表指针查找对应的虚函数表,然后调用其中存储的函数

8. C/C++编译流程及产生文件的后缀

  1. 预处理(.i):展开头文件、处理宏定义和条件编译,删除注释
  2. 编译(.s):将高级语言转换为平台相关的汇编代码
  3. 汇编(.o):将汇编代码转换为机器码,生成未链接的可重定位的目标文件
  4. 链接:合并所有的目标文件和库,生成最终的可执行文件

9. 可执行文件分段

  • 代码段:可执行的机器指令
  • 数据段:包含已经初始化的全局变量和静态变量
  • BSS段:未初始化的全局变量和静态变量
  • 堆段:动态分配的内存
  • 栈段:局部变量和函数调用信息

10. 未初始化的变量存储

  • 全局变量和静态变量:存储在BSS段,不占用可执行文件大小,程序运行的时候自动初始化为0
  • 局部变量:存储在栈段,占用的内存大小和相应的数据类型相同,值是随机的

11. U-Boot启动流程

在 ARM 架构下运行 Linux 操作系统时,系统启动阶段需要将内核加载到内存并启动执行,这一过程通常由 Bootloader 完成,常用的 Bootloader 是 U-Boot。

  1. BootROM:系统上电后,SoC 内部固化的 BootROM 代码会首先运行,它负责检测启动引导设备(如 SD 卡、eMMC、SPI Flash 等),并从中加载下一阶段的启动程序(通常是 SPL:Secondary Program Loader)到片内 SRAM 执行
  2. SPL执行:SPL 的任务是对关键硬件(尤其是 DDR 内存)进行初始化
  3. U-Boot加载:一旦 DDR 初始化完成,SPL 会将完整的 U-Boot 镜像从存储介质中加载到 DDR 中并跳转过去运行
  4. 系统移交:完整的 U-Boot 随后会初始化更多外设(如串口、网络、USB 等),完成环境设置,最终负责将 Linux 内核、设备树(Device Tree)和其他的部分加载到内存中,并跳转到内核入口,交由 Linux 接管系统控制权

启动流程至此结束,而后linux内核开始启动:解压自身->解析设备树【创建设备节点】->初始化内核子系统->匹配并加载驱动->启动第一个用户空间进程(init)

12. sizeof运算时机

sizeof的运算结果大多数情况下在编译时就能确定(基本类型,固定大小的数组,指针,结构体/类),少数情况当操作数为可变长度数组时,发生在运行阶段(没用过,只在C中使用)。

示例:

c 复制代码
int a = 0; 
int i = sizeof(a++);

结果:i = 4,a = 0

解释:sizeof是一个编译阶段指令,所以sizeof实际就是sizeof(int)为4字节。a++在运行时才会生效,但在实际运行的时候由于sizeof是在编译阶段,所以会被忽略,所以a仍然是0。

并发编程

13. 死锁

死锁发生的条件:

  • 互斥
  • 占有并等待
  • 不可抢占
  • 循环等待

死锁发生的情况:

如果线程A持有资源R1并等待资源R2,而线程B持有资源R2并等待资源R1,就会形成一个循环等待的状态,导致死锁。

14. 互斥锁、读写锁

互斥锁:

  • 保证在任何时刻,只有一个线程可以访问某一共享资源
  • 互斥锁允许线程在访问临界区时锁定资源,其他线程必须等待当前线程释放锁才能继续访问该资源
  • 特点:独占性、阻塞性

读写锁:

  • 允许多个线程同时读共享资源,但在写资源时必须独占访问
  • 读时共享、写时独占
  • 读锁:共享锁;写锁:独占锁

15. 线程间同步方式

  • 锁机制
  • 条件变量
  • 信号量

16. 线程的状态

  • 新建状态
  • 就绪状态
  • 运行状态
  • 阻塞状态
  • 死亡状态

高频考点

17. volatile关键字

volatile 关键字在 C 和 C++ 中用于修饰变量,指示编译器该变量的值可能会在程序的任何时刻发生变化,即使程序的其他部分没有显式地修改它。

使用场景:

  • 与硬件相关的编程
  • 并发编程或者多线程编程

作用:

通常编译器会将一个局部变量的值保存在寄存器中,每次读数据都从寄存器中进行读取(速度快),当加了volatile关键字后,编译器就会知道该变量可能会在其他地方改变(如硬件),编译器每次都会从内存中读取这个值。

18. const关键字

const的主要作用是表示某个变量值在程序中不可被修改,根据修饰的对象不同,const的用法不同。

(1) 修饰普通变量:

表明该值在定义后不可以再次修改。

(2) 修饰指针:

根据const的位置不同,有不同的含义:

a、指向常量的指针:

c 复制代码
const int *ptr = &a;  // ptr 是指向常量的指针
*ptr = 20;  // 编译错误:不能修改 ptr 指向的值
ptr = &b;   // 合法:指针可以指向其他地址

b、常量指针:

c 复制代码
int *const ptr = &a;  // ptr 是常量指针
*ptr = 20;  // 合法:可以修改 ptr 指向的值
ptr = &b;   // 编译错误:不能修改指针 ptr 的地址

c、常量指针指向常量:

c 复制代码
const int *const ptr = &a;  // const 指针指向 const 对象
*ptr = 20;  // 编译错误:不能修改 ptr 指向的值
ptr = &b;   // 编译错误:不能修改 ptr 的地址

(3) 修饰函数参数:

确保参数在函数内不能被修改,通常用于引用或指针类型的参数。

(4) 修饰返回值:

表示返回的值不能被修改。

(5) 修饰成员函数:

const放在函数声明的尾部,表示函数不会修改类的成员变量,也不能调用其他非const的成员函数。可以访问成员变量,不能修改,可以调用const成员函数。

19. 空指针和野指针

空指针:

指不指向任何有效内存位置的指针,通常是NULL(宏:值通常为0)或nullptr(C++11中表示一个类型安全的空指针)。

野指针:

指一个指针仍然指向原本的有效位置,但该内存位置已经被释放或者不再有效。可能引发程序崩坏、数据损坏。

悬空指针:

是野指针的一种情况,即释放指针后没有将其置空。

野指针的场景:

  1. 释放内存后指针未被置空:
c 复制代码
int* ptr = new int(10);  // 动态分配内存
delete ptr;  // 释放内存
// 此时 ptr 成为野指针,指向已释放的内存区域
  1. 指向局部变量:
    指向已出作用域的局部变量的指针也会成为野指针。

避免野指针的方法:

  1. 及时置空指针
  2. 使用智能指针
  3. 避免指向局部变量的指针
  4. 使用nullptr初始化指针

20. 位域

位域是一种允许我们精确控制每个数据成员占用内存位数的技术。它通常用于存储标志位或小范围的整数,可以节省内存。尽管如此,位域在内存对齐和跨平台行为方面可能会存在一些不确定性,因此在使用时需要小心。

21. 指针函数和函数指针

指针函数:

是指返回值为指针的函数。

函数指针:

是指向函数的指针,它保存一个函数的地址,通过函数指针可以间接地调用一个函数。

例如:

c 复制代码
int (*fptr)(int, int);  // 定义一个函数指针 fptr,指向一个返回 int 类型并且参数为两个 int 类型的函数

函数指针也可以作为参数。

22. 指针数组和数组指针

指针数组:

是指每个元素都是指针的数组。

数组指针:

是指向数组的指针,指向整个数组的起始位置。

23. 函数传参的方式

值传递:

将实参复制到形参中,函数内部操作不会影响到实参。

指针传递:

将参数的地址传递给函数,函数可以直接访问和修改实参的值。

引用传递:

将实参的引用(别名)传递给函数,函数内部操作的就是实参本身,可以直接修改实参。可以使用常量引用传递来避免修改实参。

24. 位运算操作

如何取出一个32位数的高16位和低16位?

  • 取低16位:与操作0x0000FFFF
  • 取高16位:将高16位右移16位至低16位上,与操作0x0000FFFF

25. 构造函数和析构函数调用时机

构造函数:

在对象创建的时候被调用,包括:

  • 对象的局部作用域内
  • 动态分配内存时
  • 对象数组创建时
  • 函数返回对象时等

析构函数:

在对象被销毁的时候调用,包括:

  • 对象的局部作用域结束时
  • 动态内存释放时
  • 对象数组销毁时
  • 临时对象销毁时等

26. 内存分区

内存分区(Memory Partitioning)是操作系统管理内存的一种方式,它将内存分成不同的区域或"分区",以便更有效地分配和管理系统资源。通常会将其分为代码段、数据段、堆、栈、内核空间、用户空间。

内存分区技术:

  • 静态分区
  • 动态分区(堆、栈实现)
  • 分页
  • 分段
  • 虚拟内存

优点:

  • 提高内存利用率:合理的内存分区可以避免浪费,使得不同的任务可以共享内存
  • 提高程序的稳定性:通过保护每个分区,操作系统能够有效避免程序之间的干扰和数据冲突
  • 更好的资源管理:操作系统可以根据需要动态调整内存分配,从而有效利用内存资源

27. 头文件中定义变量和函数

实际上是可以的,但是并不建议这样操作。可以在头文件声明变量(用关键字extern,告知编译器该变量在其他地方定义)和函数,而在源文件中定义变量和函数。

28. 多态的实现方法

  • 虚函数
  • 函数指针
  • 运算符重载
  • 模板(静态多态)

29. 大端和小端

大端和小端是数据在内存中两种不同的存储方式。

大端:

数据的高位字节在低地址,低位字节在高地址。

小端:

数据的低位字节在低地址,高位字节在高地址。

应用:

主机字节序通常是小端,而网络字节序是大端。所以在进行socket通信的时候,通常需要大小端的转换。

检测方法:

使用C语言实现大端和小端可以定义一个无符号整型变量,在内存中以4个字节(32位)进行存储,将x的地址强制转换为unsigned char*类型,此时这个指针就指向内存中的第一个字节,就可以根据结果来得到大端和小端。

网络编程

30. TCP的滑动窗口机制

滑动窗口是TCP实现流量控制的关键机制,允许发送方在等待确认前连续发送多个数据包,而不是每发一个就等一次确认,从而提高传输效率。

简单点说就是接收方有一个缓冲区,通常是以字节为单位,只要这个缓冲区没有满,发送方就可以一直发送数据,当接收方回复时,TCP报文中的window窗口里面会有相关的信息,告诉发送方我还可以接收多少字节的数据。

31. TCP的拥塞控制

拥塞控制的目的就是避免因为网络过载导致丢包、延迟增加、带宽浪费。就是说发送方在发送数据的时候,不能一下发送的数据太多,直接"塞爆"网络,而是通过动态控制发送速率,使得发送速率和网络恰好合适。

32. 拥塞控制的四个经典阶段

  1. 慢启动:

    • 初始时,发送速率要慢一点,通常为1或较小值
    • 每收到一个ACK,拥塞窗口就增长(指数增长)
    • 当达到慢启动阈值(ssthresh),进入下一阶段
  2. 拥塞避免:

    • 当拥塞窗口大于等于慢启动阈值后,不再按照指数增长,而是按照线性增长
  3. 快速重传:

    • 如果收到连续3个重复的ACK,不用等待超时,立刻重传那个丢失的数据包
    • 如果收到2个ACK,可能是网络抖动,乱序导致的
  4. 快速恢复:

    • 重传数据丢失后,将拥塞窗口设置为ssthresh / 2,以缓解网络的压力,然后重新慢启动

注意:

发送方可发送的数据量是滑动窗口和拥塞窗口中的较小值。

33. IP协议

IP协议用于在不同网络之间传输数据包。它提供了无连接、不可靠的传输服务。

特点:

  • **无连接:**发送数据前不建立连接
  • **不可靠:**不保证数据一定送达,也不保证顺序和完整性

版本:

  • **IPV4:**使用32位地址,地址格式是点分十进制
  • **IPV6:**使用128位地址,采用冒号十六进制

相关概念:

  • 子网掩码是IPV4的概念,主要用来确认划分网络位和主机位
  • 每个计算机或者路由器内部都会有一个路由表,在进行数据传输的时候是按照一跳一跳来实现的

过渡技术:

为了从IPV4平稳的过渡到IPV6,设计了多种技术:

  • 双协议栈(同时运行IPV4和IPV6协议栈)
  • 隧道机制(IPV6数据包封装在IPV4中进行传输)等

Linux内核与驱动

34. 内核模块

内核模块是编译好的内核代码片段(.ko文件),可以在内核运行时动态的加载和卸载,从而扩展内核功能,它和静态编译进内核不同,它是需要"拔插的",就像USB插件一样。有些不常用的功能(比如某些硬件驱动)不想在启动的时候全部加载,就可以使用内核模块,而且不用重启内核就可以调试模块代码。

35. 内核模块加载的流程

当用户使用insmod加载模块的时候,实际上完成了从用户空间到内核空间的一系列动作:

  1. Insmod是用户空间的命令,它最终会调用Linux内核暴露出来的系统调用sys_init_module()
  2. .ko是一个ELF格式的可链接目标文件,类似与普通的.o文件,内核会检查ELF文件格式,分配内核空间内存,解析重定位和符号依赖
  3. 执行初始化函数module_init
  4. 注册符号/添加模块表项

硬件通信协议

36. I2C协议

硬件组成:

SDA和SCL以及一个上拉电阻组成。在空闲状态下,二者都是高电平,通过改变两根线的高低电平来进行数据传输。

特点:

同步、串行、半双工的通信方式。不管多少设备,都是两根线,挂载的设备数量有限。

信号定义:

  • 起始信号:SCL为高电平,SDA由高至低
  • 终止信号:SCL为高电平,SDA由低至高
  • 每次发送8个字节,收到一个ACK

速率:

  • 低速:100kbps
  • 快速:400kbps
  • 最快可达3.4Mbps

适用场景:

设备多,通信速率低,系统简单的应用场景。

37. SPI协议

硬件组成:

有四根线,分别是SS、MISO、MOSI、SCLK。

特点:

一主多从的通信方式,串行,同步,全双工的通信方式,主从通信实际上就是二者相互交换自己的数据,通过移位寄存器实现。

优势:

速度快,可达几十Mbps,无ACK。

38. 串口通信

特点:

串行、异步、全双工的通信方式。

硬件组成:

由三根线组成,分别是地线、发送线、接收线。

数据格式:

一帧数据由11位组成,其中1位起始位,8位数据位,1位奇偶校验位,一位停止位。

  • 起始位:拉低开始传输数据
  • 停止位:拉高停止传输数据

电平标准:

  • TTL电平
  • RS232协议: -5-15为逻辑1,+5+15为逻辑0
  • **RS485:**使用差分信号进行传输,两线之间的电压差为+2到+6V表示逻辑1,两线之间的电压差为-2到-6V表示逻辑0

39. CAN协议

特点:

一种串行通信协议,目标是高可靠性、实时性和多主通信。所有节点地位平等,都可以发起通信,使用差分信号传输,抗干扰能力强。

帧类型:

数据帧、遥控帧、错误帧、过载帧、间隔帧。

帧结构:

每帧主要由以下几个部分组成:起始位、标识符、控制字段、数据字段、CRC校验、应答位、帧结束位。

驱动开发

40. 字符设备驱动开发流程

主要包含四个部分:

  1. 分配主次设备号
  2. 文件操作函数fops设置
  3. 字符设备结构的分配和初始化
  4. 创建设备节点

音视频编码

41. H264编码

H264是一种高效的视频压缩标准,压缩的主要步骤包含:

  • 使用当前帧使用周围像素预测【只保存残差,不保存完整的数据】,压缩空间冗余
  • 使用前后帧数据,压缩时间冗余
  • 变换、量化、熵编码等一些手段

H265 vs H264:

H265相比于H264具有更高的帧率,并且更加节省空间,但是H265编码速度慢(H264比H265快5-10倍),所以在要求实时性的场景下,优先考虑H264。

内存操作函数

42. memcpy、memset和memmove

memcpy:

c 复制代码
memcpy(void *dest, const void *src, size_t n)

从 src 拷贝 n 个字节到 dest,常用于结构体或数组的复制。底层通常是使用char*逐字节拷贝。

memset:

c 复制代码
memset(void *s, int c, size_t n)

将 s 指向的内存区域的前 n 个字节设置为值 c,常用于清零或初始化内存。

memmove:

c 复制代码
memmove(void *dest, const void *src, size_t n)

从 src 拷贝 n 个字节到 dest,与memcpy相比,它的源地址和目标地址可以重叠。

重叠处理:

比如源目标地址是0,目标地址是2,需要拷贝的字节为5,则此时发生重叠,必须使用memmove,使用memcpy会发生未定义的错误,因为memcpy是逐字节拷贝的,在拷贝到第三个字节的时候会用已经覆盖的字节继续拷贝。使用memmove可以避免的原因是会自动判断是否重叠,如果重叠的话就会从后向前拷贝来避免。

相关推荐
Liugh2 分钟前
图解 Linux 内核网络栈
linux
yuren_xia27 分钟前
Java 公平锁与非公平锁详解
java·网络
ZaaaaacK1 小时前
Linux系统远程操作和程序编译
linux·运维·postgresql
Y_3_71 小时前
Netty实战:从核心组件到多协议实现(超详细注释,udp,tcp,websocket,http完整demo)
linux·运维·后端·ubuntu·netty
测试专家2 小时前
ARINC653系统架构
大数据·linux·运维
昵称什么的不存在2 小时前
腾讯云轻量级服务器Ubuntu系统与可视化界面
服务器·ubuntu·腾讯云
怪只怪满眼尽是人间烟火2 小时前
SQL分片工具类
网络·数据库·sql
国际云2 小时前
腾讯云搭建web服务器的方法
服务器·数据库·云计算·区块链
deeper_wind2 小时前
配置DHCP服务(小白的“升级打怪”成长之路)
运维·服务器·智能路由器
睡觉z2 小时前
LVS+Keepliaved高可用群集
网络·智能路由器·lvs