一、基础部分
1、C/C++ 的内存分配?
- 静态分配:栈(局部变量 / 函数参数,自动分配释放)、全局 / 静态区(.data/.bss,程序启动时分配);
- 动态分配:堆(C 用
malloc/calloc/realloc/free,C++ 用 new/delete,手动分配释放)。
2、如何使用指针去操作内存?
- 指针指向内存:
int *p = &var;(变量地址)或 p = (int *)0x40000000;(物理地址映射);
- 读写内存:
*p = 10;(写)、int val = *p;(读);
- 连续内存:指针加减偏移(如
p++ 访问数组下一个元素)。
3、使用指针如何避免越界访问?
- 明确内存边界(如数组长度),校验下标 / 偏移量;
- 避免随意指针加减,使用安全函数(
strncpy 替代 strcpy);
- 调试开启内存检测(如
valgrind),用 volatile 监控共享内存。
4、栈内是保存什么的?malloc 出来的地址在栈还是堆?
- 栈保存:局部变量、函数参数、返回地址、栈帧信息;
malloc 分配的内存在堆,栈仅存储 malloc 返回的指针变量本身。
5、全局变量和静态变量保存在哪里?
- 均存于数据段(.data 区:初始化非 0 值)或 BSS 段(未初始化 / 初始化为 0 值);静态变量仅限制作用域,存储位置与全局变量一致。
6、哈希表的实现原理
- 哈希函数映射:将键映射到哈希桶下标,快速定位存储位置;
- 冲突解决:采用链地址法(冲突元素串成链表)或开放地址法;
- 核心特性:平均查询 / 插入时间复杂度 O (1)。
7、平衡二叉树的实现原理?
- 基于二叉搜索树,通过左旋 / 右旋维持左右子树高度差≤1(AVL 树),避免树退化为链表;保证查询、插入、删除的时间复杂度为 O (logn)。
8. 选择题
以下哪种线程同步机制可用于实现 “线程 A 等待线程 B 完成某操作后再执行” 的逻辑?()
A. 互斥锁(Mutex) B. 条件变量(Condition Variable) C. 二进制信号量 D. 计数信号量
答案:B
解析:条件变量通过 “等待 - 通知” 机制实现:线程 A 调用 pthread_cond_wait 等待,线程 B 完成后调用 pthread_cond_signal 唤醒 A。
9. 数据结构的了解
常用数据结构按逻辑分类:
- 线性结构:
- 数组:连续存储,随机访问快(O (1)),增删慢(O (n));
- 链表:非连续存储,增删快(O (1)),访问慢(O (n));
- 栈(FILO)、队列(FIFO):基于数组 / 链表实现,用于缓存、任务调度。
- 树形结构:
- 二叉树:每个节点最多两子树,用于排序、搜索;
- 红黑树:自平衡二叉树,O (log n) 操作,用于 map/set;
- 堆:完全二叉树,用于优先队列(如调度算法)。
- 哈希结构:哈希表通过键映射存储,平均 O (1) 操作,用于缓存、字典。
- 图结构:由节点和边组成,用于网络拓扑、路径规划(如 Dijkstra 算法)。
10、选择题
以下哪种进程间通信方式是最快的?()
A. 管道(Pipe) B. 消息队列 C. 共享内存 D. 套接字(Socket)
答案:C
解析:共享内存通过内核分配物理内存并映射到多进程地址空间,进程直接读写内存,无需内核中转(无数据拷贝),是所有 IPC 中效率最高的。
11、 简答题
fork() 和 vfork() 创建子进程的核心区别是什么?
参考答案:
- 地址空间处理:
fork() 采用写时复制(COW),子进程与父进程共享内存直到修改时复制;vfork() 子进程完全共享父进程地址空间,修改会直接影响父进程。
- 父进程状态:
fork() 父进程不阻塞,与子进程并发执行;vfork() 父进程阻塞,直到子进程调用 exec 或 exit 才恢复。
12、 分析题
某程序中,两个线程通过全局变量 int data = 0 交换数据,线程 A 负责写入 data = 100,线程 B 负责读取并打印 data。但运行时线程 B 有时打印 0,有时打印 100,原因是什么?如何解决?
参考答案:
- 原因:线程 A 和 B 同时访问
data 导致数据竞争(线程 B 可能在线程 A 写入前读取)。
- 解决:用互斥锁(
pthread_mutex_t)保护 data 的读写,确保同一时间只有一个线程访问。
13、选择题
Linux 中,以下哪个指令可用于查看进程占用的 CPU 和内存资源?()
A. ps -ef B. top C. netstat -tuln D. df -h
答案:B
解析:top 是动态进程监控工具,实时显示进程的 CPU 使用率、内存占用等;ps -ef 仅静态列出进程信息。
14、 TCP/IP 协议
TCP/IP 是互联网的核心协议族,采用分层模型(通常简化为 4 层):
- 链路层:处理物理传输(如以太网、Wi-Fi),封装 MAC 地址,协议有 ARP(地址解析)。
- 网络层:负责跨网络路由,核心协议是 IP(定义 IP 地址和数据包格式),辅助协议有 ICMP(ping 命令基于此)、IGMP(组播)。
- 传输层:提供端到端通信,核心协议:
- TCP:面向连接、可靠传输(三次握手建立连接,四次挥手断开,重传机制、流量控制、拥塞控制);
- UDP:无连接、不可靠但高效(适合实时通信,如视频、语音)。
- 应用层:直接为应用服务,协议有 HTTP(网页)、FTP(文件传输)、DNS(域名解析)、SSH(远程登录)等。
15、TCP4次握手
TCP 四次挥手的过程中,主动关闭方在第四次挥手后进入 TIME_WAIT 状态,等待 2MSL 的目的是什么?
参考答案:
- 确保最后一个 ACK 被被动关闭方收到:若被动方未收到,会重发 FIN,主动方可在
TIME_WAIT 期间再次回复。
- 防止旧连接的报文干扰新连接:
2MSL 是报文最大生存时间的 2 倍,确保本次连接的所有报文已从网络中消失。
16、工厂模式的理解以及为什么需要工厂模式?
- 工厂模式:创建型设计模式,定义一个工厂类负责创建其他类的实例,隐藏对象创建的细节(如具体类名、初始化参数)。分为简单工厂、工厂方法、抽象工厂。
- 为什么需要:
- 解耦:调用方无需知道具体类名,只需通过工厂接口获取对象,降低依赖。
- 扩展性:添加新类型时,只需扩展工厂类,无需修改调用方代码(符合开闭原则)。
- 统一管理:对象创建逻辑集中在工厂,便于维护(如统一初始化、日志记录)。
- 示例:日志系统中,工厂根据配置创建 “文件日志” 或 “控制台日志” 实例,调用方无需修改。
17、分析题
某设备 ping 通目标 IP(如 192.168.1.1),但无法解析域名(如 www.baidu.com),可能的原因有哪些?
参考答案:
- DNS 服务器配置错误:
/etc/resolv.conf 中 nameserver 未配置或指向无效 IP。
- 本地 Hosts 文件异常:
/etc/hosts 中错误映射目标域名,导致解析冲突。
- DNS 端口被拦截:防火墙封禁 UDP 53 端口(DNS 默认端口),无法向 DNS 服务器发送请求。
- DNS 缓存污染:本地 DNS 缓存记录错误,需清理(如
systemctl restart systemd-resolved)。
18、libjpeg 等库做了什么操作?
libjpeg 是处理 JPEG 图像的开源库,核心操作包括:
- 解码:将 JPEG 压缩格式(二进制流)转换为 RGB/BGR 等未压缩格式,步骤:解析文件头、霍夫曼解码、逆 DCT 变换、去量化、色彩空间转换(YCrCb→RGB)。
- 编码:将 RGB 等格式压缩为 JPEG,步骤:色彩空间转换(RGB→YCrCb)、DCT 变换、量化、霍夫曼编码、生成文件头。
- 提供 API 屏蔽底层算法细节(如
jpeg_read_header、jpeg_start_decompress),方便应用快速实现图像编解码,无需关注 JPEG 标准的复杂细节。
二、内核+驱动
1、简述嵌入式 Linux 的启动流程(从上电到应用程序运行)
核心要点:需经过 “硬件初始化→Bootloader→内核启动→根文件系统挂载→用户程序启动” 五个阶段:
- 硬件上电:CPU 从复位向量(如 0x0 地址)执行,初始化片内 SRAM、时钟等基础硬件。
- Bootloader 阶段:
- 初始化 DRAM、外设(如串口),将内核镜像(zImage)和设备树(.dtb)从 Flash 加载到 DRAM。
- 设置内核启动参数(如 bootargs),跳转到内核入口地址(如
start_kernel)。
- 内核启动:
- 初始化内核核心组件(进程管理、内存管理、中断系统),解析设备树,初始化驱动(平台设备、总线驱动匹配)。
- 挂载根文件系统(根据 bootargs 中的
root=/dev/mmcblk0p2 等参数)。
- 用户空间初始化:启动 init 进程(PID=1),解析
/etc/inittab 或 systemd 配置,启动服务(如网络、日志)。
- 运行应用程序:init 进程启动用户态应用(如 APP、脚本),系统进入正常运行状态。
2、什么是设备树(Device Tree)?它解决了什么问题?如何在驱动中获取设备树节点的属性?
核心要点:
3、使用设备树的优点是什么?
- 硬件与内核解耦:硬件信息(如引脚、寄存器地址)通过设备树描述,无需修改内核源码,同一内核可适配不同硬件(如同一芯片的不同开发板)。
- 简化内核维护:避免大量 “板级代码”(如传统的
platform_data),减少内核补丁数量。
- 动态适配:内核启动时解析设备树,自动匹配驱动(通过
compatible 属性),符合 Linux “设备 - 驱动分离” 模型。
- 跨平台兼容:统一的硬件描述格式,适配不同架构(ARM、RISC-V 等)。
4、编写一个简单的字符设备驱动(至少包含 open、read、write 操作),并说明如何测试
核心要点:字符设备驱动通过 cdev 结构体注册,依赖 file_operations 实现文件操作接口。
驱动代码框架:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#define DEV_NAME "mychar"
#define DEV_MAJOR 0 // 动态分配主设备号
static int major = DEV_MAJOR;
static struct cdev my_cdev;
static char data_buf[1024]; // 数据缓冲区
// open操作
static int my_open(struct inode *inode, struct file *filp)
{
printk("mychar device opened\n");
return 0;
}
// read操作:从内核缓冲区读数据到用户空间
static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
int ret = copy_to_user(buf, data_buf, count); // 内核→用户空间(必须用copy_to_user)
return count - ret;
}
// write操作:从用户空间写数据到内核缓冲区
static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
int ret = copy_from_user(data_buf, buf, count); // 用户→内核空间(必须用copy_from_user)
return count - ret;
}
// 文件操作集合
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
};
// 模块初始化
static int __init mychar_init(void)
{
dev_t dev;
// 分配设备号
if (major == 0) {
alloc_chrdev_region(&dev, 0, 1, DEV_NAME); // 动态分配
major = MAJOR(dev);
} else {
dev = MKDEV(major, 0);
register_chrdev_region(dev, 1, DEV_NAME); // 静态注册
}
// 初始化cdev并添加到内核
cdev_init(&my_cdev, &my_fops);
cdev_add(&my_cdev, dev, 1);
printk("mychar driver loaded, major=%d\n", major);
return 0;
}
// 模块退出
static void __exit mychar_exit(void)
{
cdev_del(&my_cdev);
unregister_chrdev_region(MKDEV(major, 0), 1);
printk("mychar driver unloaded\n");
}
module_init(mychar_init);
module_exit(mychar_exit);
MODULE_LICENSE("GPL");
测试方法:
- 编译驱动为 .ko 模块,加载:
insmod mychar.ko。
- 创建设备节点:
mknod /dev/mychar c <major> 0(major 为驱动打印的主设备号)。
- 编写测试程序(用户态):
#include <stdio.h>
#include <fcntl.h>
int main() {
int fd = open("/dev/mychar", O_RDWR);
write(fd, "hello", 5); // 写入数据
char buf[10];
read(fd, buf, 5); // 读取数据
printf("read: %s\n", buf); // 输出 "hello"
close(fd);
return 0;
}
5、中断的概念?
- 由硬件 / 软件触发的异步事件,用于暂停当前程序执行,转而去执行中断服务函数处理事件,处理完成后返回原程序继续执行。
6、用什么函数注册中断?
- 裸机(STM32):
HAL_NVIC_EnableIRQ()(使能中断)、HAL_GPIO_EXTI_Callback()(中断回调);
- Linux 内核:
request_irq()(注册中断服务函数)。
7、在中断中要注意什么?
- 执行时间尽量短,避免阻塞 / 休眠;
- 用自旋锁(而非互斥锁)保护共享资源;
- 禁止不必要的中断嵌套,用
volatile 修饰共享变量;
- 及时保存和恢复现场。
8、如何写一个中断?
步骤:
- 获取中断号:
- 设备树中指定:
interrupts = <GIC_SPI 16 IRQ_TYPE_EDGE_FALLING>;(边缘触发);
- 驱动中解析:
irq = platform_get_irq(pdev, 0);。
- 注册中断处理函数:
int ret = request_irq(irq, irq_handler, IRQF_TRIGGER_FALLING, "my_device", dev);
irq_handler:上半部函数(快速处理,如清中断标志),返回 IRQ_HANDLED。
- 处理下半部:若需耗时操作(如数据处理),在中断上半部中调度下半部(如 workqueue):
static irqreturn_t irq_handler(int irq, void *dev_id)
{
schedule_work(&dev->work); // 调度workqueue
return IRQ_HANDLED;
}
- 注销中断:
free_irq(irq, dev);
9、下半部里面 work_queue 和 irq_thread 的区别?
| 维度 |
work_queue |
irq_thread |
| 运行上下文 |
进程上下文(可睡眠) |
进程上下文(可睡眠) |
| 调度方式 |
由内核线程(如 kworker)调度 |
作为独立线程调度,优先级高于普通线程 |
| 触发方式 |
需手动调用 schedule_work |
中断触发后自动唤醒线程 |
| 适用场景 |
通用延迟任务(如数据上报) |
中断处理较复杂但需睡眠(如 I2C 数据读取) |
| 延迟性 |
可能被低优先级任务延迟 |
实时性较好(线程可设高优先级) |
10、解释 Linux 中断处理的 “顶半部(Top Half)” 和 “底半部(Bottom Half)”,并说明各自的实现方式
核心要点:中断处理分为两部分,平衡 “响应速度” 和 “处理效率”:
- 顶半部(TH):
- 功能:快速响应中断,完成最紧急的操作(如清除中断标志、保存硬件状态),禁止中断嵌套(避免干扰)。
- 实现:通过
request_irq 注册中断服务函数(ISR),在 ISR 中执行顶半部逻辑。
- 底半部(BH):
- 功能:处理耗时操作(如数据处理、唤醒任务),允许中断嵌套(不阻塞其他中断)。
- 实现方式:
tasklet:基于软中断,适合小任务(原子上下文,不能睡眠);
- 工作队列(
workqueue):基于内核线程,适合大任务(可睡眠,允许阻塞操作);
- 软中断:内核级异步处理(如网络、块设备,不建议驱动直接使用)。
示例:串口接收中断
- 顶半部:读取硬件 FIFO 数据到内核缓冲区,清除中断标志。
- 底半部(工作队列):解析缓冲区数据,发送到用户空间。
11、 Linux 内核的进程调度策略有哪些?嵌入式场景中如何选择?
核心要点:Linux 内核支持多种调度策略,按实时性分为:
- CFS(Completely Fair Scheduler):
- 非实时调度,默认策略(
SCHED_OTHER/SCHED_NORMAL),基于 “虚拟运行时间” 分配 CPU,确保各进程公平执行。
- 适用:普通应用程序(如后台服务、UI 界面)。
- 实时调度:
SCHED_FIFO(先进先出):高优先级进程一旦获取 CPU,一直运行直到主动放弃或被更高优先级进程抢占。
SCHED_RR(时间片轮转):同优先级实时进程按时间片轮流执行。
- 适用:嵌入式实时任务(如传感器数据采集、电机控制,需严格截止时间)。
- 其他策略:
SCHED_IDLE:最低优先级,仅当系统空闲时运行(如后台日志清理)。
嵌入式选择原则:
- 实时性要求高的任务(如 10ms 内响应)→ 用
SCHED_FIFO/SCHED_RR,并设置合适优先级。
- 普通任务→ CFS,避免抢占实时任务。
12、为什么中断不能堵塞,堵塞会引起什么?
- 中断不能堵塞的原因:中断是硬件触发的紧急事件(如按键按下、数据到达),需要快速响应;中断处理时会屏蔽同类型中断,若堵塞(如执行耗时操作),会导致后续同类型中断丢失。
- 堵塞的后果:
- 错过关键硬件事件(如传感器数据溢出、通信超时);
- 系统响应延迟,甚至卡顿(其他中断也可能被间接影响);
- 严重时导致 watchdog 超时(硬件复位),系统崩溃。
13、什么是 MMU(内存管理单元)?嵌入式 Linux 中 MMU 的作用是什么?
核心要点:
- 定义:MMU 是 CPU 内的硬件单元,负责虚拟地址到物理地址的转换,提供内存保护。
- 核心作用:
- 虚拟内存管理:每个进程拥有独立的 4GB 虚拟地址空间(32 位系统),通过页表(Page Table)映射到物理内存,进程间地址隔离(避免相互干扰)。
- 内存保护:设置内存页的访问权限(只读、读写、执行),防止非法访问(如用户态程序写内核空间→触发段错误)。
- 内存映射:支持将物理内存(如外设寄存器)、文件映射到虚拟地址空间(如
mmap 系统调用)。
- 嵌入式场景:
- 多进程系统必须启用 MMU(保证进程隔离);
- 部分极简嵌入式系统(如单任务)可禁用 MMU,直接使用物理地址(节省资源)。
14、 根文件系统的作用是什么?列举至少 3 种嵌入式常用的根文件系统类型,并说明特点
核心要点:
- 作用:根文件系统(rootfs)是 Linux 启动后挂载的第一个文件系统,提供用户空间基础环境(命令、库、配置文件、设备节点等),是用户态程序运行的载体。
- 常用类型及特点:
- squashfs:
- 特点:只读压缩文件系统,节省存储空间,适合固定固件(如路由器、IoT 设备),需配合 overlayfs 实现可写。
- tmpfs:
- 特点:基于内存的临时文件系统,读写速度快,重启后数据丢失,适合存储临时文件(如
/tmp)。
- yaffs2:
- 特点:专为 NAND Flash 设计,支持坏块管理、磨损均衡,适合嵌入式 Flash 设备(如旧款 Android 手机)。
- 缺点:不支持硬盘等块设备。
- ext4:
- 特点:支持大文件、日志功能(崩溃后可恢复),适合本地存储(如 eMMC、SSD)。
- 缺点:占用空间较大,不适合小容量 Flash。
15、如何在嵌入式 Linux 中实现用户空间与内核空间的数据交互?至少列举 3 种方式
核心要点:用户态(应用程序)与内核态(驱动 / 内核模块)隔离,需通过内核提供的接口交互:
- 系统调用(syscall):
- 应用通过库函数(如
open/read)触发系统调用,陷入内核态,内核处理后返回结果(如操作字符设备)。
- procfs/sysfs:
procfs(/proc):虚拟文件系统,内核通过文件暴露进程、内存等信息(如 /proc/meminfo),应用可读写。
sysfs(/sys):按设备层级组织的虚拟文件系统,驱动通过 sysfs 属性(如 /sys/class/leds/led0/brightness)暴露设备状态,应用直接读写文件交互。
- 共享内存(mmap):
- 驱动通过
remap_pfn_range 将内核内存(如外设缓冲区)映射到用户虚拟地址,应用通过 mmap 获取地址后直接读写,适合大数据量交互(如摄像头数据)。
- netlink 套接字:
- 基于网络协议的用户 - 内核通信机制,支持异步消息传递(如内核主动通知用户态事件,比系统调用更灵活)。
16、 嵌入式 Linux 中,如何配置网络(静态 IP、DNS)?简述流程
核心要点:配置网络需设置 IP 地址、子网掩码、网关、DNS 服务器,常用方式:
- 临时配置(重启失效):
- 设置 IP:
ifconfig eth0 192.168.1.100 netmask 255.255.255.0
- 设置网关:
route add default gw 192.168.1.1
- 设置 DNS:
echo "nameserver 8.8.8.8" > /etc/resolv.conf
- 永久配置(基于 systemd):
[Network]
Address=192.168.1.100/24 # IP+子网掩码
Gateway=192.168.1.1
DNS=8.8.8.8
DNS=114.114.114.114
- 重启网络服务:`systemctl restart systemd-networkd`
### 17、基于 BusyBox(极简系统):
- 在 `/etc/init.d/rcS` 脚本中添加临时配置命令,开机自动执行。
### 18、 列举 3 种嵌入式 Linux 常用的调试工具,并说明其用途
核心要点:
1. dmesg:
- 用途:查看内核日志(如驱动加载信息、中断触发、Oops 错误),定位内核 / 驱动问题(如 `dmesg | grep "mychar"` 查看自定义驱动日志)。
2. strace:
- 用途:跟踪用户态程序的系统调用(如 `open`/`read`/`write`)和信号,定位应用程序错误(如 “Permission denied” 是哪个系统调用失败)。
- 示例:`strace ./app` 打印 app 的所有系统调用。
3. gdb + gdbserver:
- 用途:远程调试嵌入式设备上的应用程序,支持断点、变量查看、堆栈跟踪(需设备端运行 gdbserver,开发机用 gdb 连接)。
- 示例:设备端 `gdbserver :1234 ./app`,开发机 `arm-linux-gnueabihf-gdb ./app`,然后 `target remote 192.168.1.100:1234`。
4. perf:
- 用途:性能分析工具,统计 CPU 使用率、函数调用次数、缓存命中率,定位程序性能瓶颈(如嵌入式设备中哪个函数占用 CPU 过高)。
### 19、Norflash、eMMC、Nand 的差异
| 维度 | Norflash | Nandflash | eMMC |
| :--- | :--- | :--- | :--- |
| 存储介质 | 或非门(NOR)结构 | 与非门(NAND)结构 | 封装 Nandflash + 控制器 |
| 读写特性 | 支持随机读取(像内存),写 / 擦除慢 | 顺序读写快,随机读慢(需页缓存) | 继承 Nand 特性,控制器优化读写 |
| 擦除单位 | 大(如 64KB) | 小(如 128KB) | 由控制器管理 |
| 可靠性 | 位反转少,无需 ECC | 易位反转,需 ECC 校验 | 控制器集成 ECC,可靠性高 |
| 用途 | 存启动代码(如 U-Boot) | 存大量数据(如系统镜像) | 嵌入式设备主存储(如手机、开发板) |
### 20、写一个按键驱动从内核到应用层的过程
1. **硬件与设备树**:在设备树中定义按键引脚(如 `key-gpio = <&gpio5 3 GPIO_ACTIVE_LOW>`),指定中断触发方式(如下降沿)。
2. 内核驱动实现:
- 注册字符设备或使用 input 子系统(更标准);
- 解析设备树,获取 GPIO 和中断号,申请 GPIO 为中断模式;
- 注册中断处理函数(上半部):检测按键按下 / 松开,记录状态;
- 下半部(如工作队列):通过 input 子系统上报事件(`input_report_key`)。
3. **驱动加载**:编译为 .ko 模块,`insmod` 加载,生成设备节点(如 `/dev/input/event1`)。
4. **应用层访问**:打开 `/dev/input/event1`,通过 `read` 读取 `input_event` 结构体,解析 `type`(`EV_KEY`)、`code`(按键码)、`value`(0/1 表示松开 / 按下)。
### 21、I2C 如何保证主从设备通信一致?
- 约定相同波特率、设备地址和帧格式;
- 通过起始 / 停止位同步通信时序;
- 从设备以 ACK 应答确认接收,时钟拉伸协调主从速率;
- 上拉电阻 + 开漏输出实现线与逻辑,避免总线冲突。
### 22、 I2C、UART、SPI 通信协议的差异
| 维度 | I2C | UART | SPI |
| :--- | :--- | :--- | :--- |
| 信号线 | SDA(数据)、SCL(时钟) | TX(发送)、RX(接收) | SCK(时钟)、MOSI(主发从收)、MISO(主收从发)、CS(片选) |
| 同步方式 | 同步(SCL 时钟) | 异步(波特率约定) | 同步(SCK 时钟) |
| 速率 | 低速(标准 100kbps,高速 400kbps) | 低速(通常≤1Mbps) | 高速(可达几十 Mbps) |
| 拓扑结构 | 多主多从(通过地址区分) | 点对点 | 一主多从(通过 CS 片选) |
| 应用场景 | 传感器(温湿度、陀螺仪) | 调试打印、模块通信(GPS) | 高速外设(显示屏、Flash) |
### 23、 Recovery 系统的理解,以及变砖了怎么处理?
- **Recovery 系统**:独立于主系统的小型引导环境,用于系统修复、升级、恢复出厂设置。通常存储在单独的分区(如 recovery 分区),由 bootloader 启动。核心功能:挂载系统分区、执行 OTA 升级脚本、清除数据。
- 变砖处理:
- 若能进入 Recovery:通过 SD 卡或 ADB 推送正确固件,执行 “从 SD 卡更新”。
- 若无法进入 Recovery(硬砖):使用线刷工具(如 SP Flash Tool、J-Link),通过 USB 或调试接口强制刷写 bootloader 和系统分区,恢复启动链。
### 24、pin 复用是怎么做的?
pin 复用指一个物理引脚可映射到多个功能(如 GPIO、UART_TX、SPI_CLK),实现步骤:
1. 硬件层面:芯片手册定义引脚的复用功能(如引脚 10 可复用为 GPIO5_3 或 UART2_TX),通过复用寄存器配置。
2. 设备树层面:在设备树中通过 pinctrl 节点指定引脚功能,例如:
pinctrl_uart2: uart2grp {
pins = "pin10";
function = "uart2"; // 复用为UART2_TX
};
3. 驱动层面:驱动通过 `devm_pinctrl_get_select` 绑定设备树的 pinctrl 配置,内核自动解析并配置复用寄存器,完成引脚功能切换。
### 25、复位高低电平不一样,怎么处理?
复位信号有 “高电平有效” 和 “低电平有效”,处理方式:
1. 设备树中声明极性:通过属性指定复位电平,例如:
reset-gpios = <&gpio1 2 GPIO_ACTIVE_HIGH>; // 高电平有效
2. 驱动中适配:解析设备树属性获取极性,复位时输出对应电平:
- 高电平有效:复位时 `gpio_set_value(reset_gpio, 1)`,结束后拉低;
- 低电平有效:复位时 `gpio_set_value(reset_gpio, 0)`,结束后拉高。
3. 复用通用函数:使用内核 gpio 接口(如 `gpio_set_active_low`)自动处理极性转换,无需关心硬件电平细节。
### 26、复位延时不一样,怎么处理?
不同外设复位所需延时不同(如有的需 10ms,有的需 100ms),处理方式:
1. 设备树动态配置:通过自定义属性指定延时,例如:
reset-delay-ms = <50>; // 复位延时50ms
2. 驱动中读取延时:驱动解析设备树属性,使用内核延时函数等待:
unsigned int delay;
of_property_read_u32(dev->of_node, "reset-delay-ms", &delay);
msleep(delay); // 毫秒级延时(进程上下文)
// 若在中断上下文,用udelay(微秒级,不睡眠)
3. 兼容固定延时:对无设备树配置的场景,设置默认延时(参考外设 datasheet),确保兼容性。
### 27、RTC 怎么和 Linux 进行时间同步?
Linux RTC 的时间同步通过 RTC 驱动和用户态工具实现:
1. 内核层面:RTC 驱动(如 `rtc-ds1307`)注册为 `/dev/rtc0`,内核启动时通过 `rtc_read_time` 读取 RTC 时间,同步到系统时间(`xtime`)。
2. 用户层面:
- 系统运行时,通过 `date` 命令修改系统时间后,用 `hwclock -w` 将系统时间写入 RTC;
- 定时同步:通过 cron 定时执行 `hwclock -w`,或驱动中实现周期性同步(如每小时)。
3. 自动同步:内核配置 `CONFIG_RTC_SYSTOHC` 后,系统会定期将系统时间同步到 RTC。
### 28、看门狗的原理?
看门狗(Watchdog)是硬件定时器,核心原理:
- 初始化时设置超时时间(如 500ms),定时器开始倒计时;
- 系统需定期 “喂狗”(通过寄存器或驱动接口重置定时器),否则超时后硬件触发系统复位(防止死机)。
分为:
- 硬件看门狗:独立芯片(如 MAX706)或 SOC 内置模块,可靠性高;
- 软件看门狗:内核模块模拟(如 softdog),依赖内核正常运行,可靠性低。
#### 喂狗的时间?
喂狗时间需满足:喂狗周期 < 看门狗超时时间 < 系统最大允许无响应时间。
- 例如:系统正常处理周期为 100ms,喂狗周期可设为 200ms,超时时间设为 500ms(预留容错空间)。
- 参考依据:外设 datasheet(看门狗最小 / 最大超时时间)、系统业务最大响应延迟(如传感器采集周期)。
#### 应用层是怎么喂狗的,定时器还是直接 while 1?
推荐用定时器喂狗,优势:
- 定时器方式:通过 `setitimer` 或 `timerfd` 创建定时事件(如 200ms 一次),定时向 `/dev/watchdog` 写入数据(如 `write(fd, "1", 1)`),不占用 CPU,效率高。
- 避免 `while(1)`:会持续占用 CPU(即使加 `sleep`,精度也较低),且无法处理线程异常(如崩溃后无法喂狗)。
- 增强可靠性:结合进程监控(如 systemd),若喂狗线程崩溃,监控进程重启线程或触发复位。
### 29、内核有没有打实时 patch?
判断方法:
- 查看内核配置:`zcat /proc/config.gz | grep CONFIG_PREEMPT_RT`,若为 y 则打了实时补丁(PREEMPT_RT)。
- 查看内核版本:`uname -a`,实时内核通常含 rt 标识(如 5.10.100-rt50)。
实时 patch 通过 “全抢占内核”“中断线程化” 等优化,将内核响应延时从毫秒级降至微秒级,适合工业控制、自动驾驶等低延迟场景。
### 30、SCHED_FIFO 线程一直占用怎么办?
SCHED_FIFO 是实时调度策略(优先级 1-99),高优先级线程会抢占 CPU,若一直占用会导致低优先级线程 “饿死”,解决方法:
1. 主动让出 CPU:线程中定期调用 `schedule_yield()`,允许同优先级线程运行。
2. 限制运行时长:通过定时器(如 timerfd)强制线程退出临界区,避免无限循环。
3. 合理设置优先级:非关键线程设低优先级(如 50 以下),预留高优先级给紧急任务。
4. 结合同步机制:用信号量、互斥锁限制线程运行范围,确保资源释放。
### 31、线程间的 `work_queue`(工作队列)和 `irq_thread`(中断线程)有什么核心区别?
**参考答案**:
- 调度方式:`work_queue` 由内核线程(kworker)统一调度,优先级较低;`irq_thread` 是独立线程,可设置高优先级。
- 触发方式:`work_queue` 需手动调用 `schedule_work` 触发;`irq_thread` 由中断自动唤醒(内核关联中断与线程)。
- 适用场景:`work_queue` 用于通用延迟任务;`irq_thread` 适合中断处理较复杂但需睡眠的场景(如 I2C 通信)。
### 32、实时操作系统(RTOS)与通用操作系统(如 Linux)的核心区别是什么?
**参考答案**:
- 核心目标:RTOS 聚焦 “实时性”(任务在截止时间内完成,响应时间可预测);通用 OS 优化吞吐量和资源利用率。
- 调度策略:RTOS 采用优先级抢占式调度,高优先级任务可立即抢占 CPU;通用 OS 采用复杂调度(如 Linux CFS),响应时间可能波动。
- 中断延迟:RTOS 中断延迟为微秒级(严格控制);通用 OS 默认中断延迟为毫秒级(非实时)。
这些题目覆盖了进程 / 线程通信、Linux 指令、网络协议、驱动开发、实时系统等核心考点,可用于检验对基础概念和实际应用的理解。
## 三、面试部分
### 1. 自我介绍
(结合岗位需求调整,以嵌入式开发岗为例)
“我叫 XX,毕业于 XX 学校 XX 专业,有 X 年嵌入式 Linux 开发经验。主要擅长 Linux 内核驱动开发(如字符设备、I2C/SPI 外设驱动)、设备树配置和应用层编程,熟悉 ARM 架构和常用通信协议(I2C/UART/SPI)。曾参与 XX 项目(如智能硬件传感器模块开发),负责从硬件驱动到应用层接口的全流程实现,解决过 XX 技术问题(如中断冲突、设备树兼容性)。熟练使用 GCC、Makefile、Git 等工具,了解数据结构和操作系统基础。期待加入贵团队,参与底层开发相关工作。”
### 2、之前做过哪些驱动开发?
(结合实际项目举例,突出技术栈和解决的问题)
“主要做过几类驱动开发:
- 字符设备驱动:如按键、LED、蜂鸣器,基于 GPIO 子系统实现,通过 input 子系统上报按键事件,支持中断触发;
- 总线设备驱动:I2C 接口的温湿度传感器(SHT30)、SPI 接口的显示屏(OLED),适配设备树,实现设备探测、数据读写逻辑;
- 外设驱动:UART 串口驱动(调试用)、RTC 实时时钟(DS3231),解决过时钟同步和中断冲突问题;
- 虚拟设备驱动:模拟一个字符设备用于进程间通信,通过 ioctl 提供控制接口。
开发中涉及设备树配置、中断处理、下半部机制(workqueue)、sysfs 接口封装等。”
### 更多学习资源
希望这份涵盖从[C/C++](https://yunpan.plus/f/25-1)基础到[操作系统](https://yunpan.plus/f/36-1)核心概念的面试题解析能对你的学习和求职有所帮助。如果你想与更多开发者交流或寻找更系统的学习路径,可以到[云栈社区](https://yunpan.plus)探索相关技术专题和讨论。