Linux 驱动开发核心自测题库(面试官问答版)
这份题库基于你提供的所有文档提炼,涵盖字符设备驱动、GPIO子系统、中断处理、I2C/SPI总线、设备树、Platform模型、并发控制、调试技巧八大核心领域。每道题都是"面试官真实会问"的高频考点,按难度分级标注。建议先尝试回答,再对照解析。
文章目录
- [Linux 驱动开发核心自测题库(面试官问答版)](#Linux 驱动开发核心自测题库(面试官问答版))
-
- 一、字符设备驱动框架(基础但必考)
-
- [Q1:`register_chrdev` 和 `cdev` 接口有什么区别?为什么推荐用 `cdev`?](#Q1:
register_chrdev和cdev接口有什么区别?为什么推荐用cdev?) - [Q2:`copy_to_user` 和 `copy_from_user` 能直接用 `memcpy` 替代吗?为什么?](#Q2:
copy_to_user和copy_from_user能直接用memcpy替代吗?为什么?) - [Q3:设备节点 `/dev/xxx` 是怎么自动创建的?需要手动 `mknod` 吗?](#Q3:设备节点
/dev/xxx是怎么自动创建的?需要手动mknod吗?) - [Q4:`module_init` 和 `module_exit` 的作用是什么?`__init` 和 `__exit` 修饰符的意义?](#Q4:
module_init和module_exit的作用是什么?__init和__exit修饰符的意义?)
- [Q1:`register_chrdev` 和 `cdev` 接口有什么区别?为什么推荐用 `cdev`?](#Q1:
- [二、GPIO 子系统与中断(高频考点)](#二、GPIO 子系统与中断(高频考点))
-
- [Q5:怎么查看一个 GPIO 在内核中的编号?以 `GPIO4_19` 为例。](#Q5:怎么查看一个 GPIO 在内核中的编号?以
GPIO4_19为例。) - [Q6:`request_irq` 的参数 `dev_id` 有什么作用?为什么 `free_irq` 需要传入相同的指针?](#Q6:
request_irq的参数dev_id有什么作用?为什么free_irq需要传入相同的指针?) - Q7:中断处理函数(ISR)中能做什么?不能做什么?为什么?
- Q8:按键驱动中为什么要用定时器(timer)?原理是什么?
- Q9:什么是中断上半部和下半部?有哪些下半部机制?
- [Q5:怎么查看一个 GPIO 在内核中的编号?以 `GPIO4_19` 为例。](#Q5:怎么查看一个 GPIO 在内核中的编号?以
- 三、等待队列、阻塞/非阻塞、Poll、异步通知(核心机制)
-
- [Q10:`wait_event_interruptible` 和 `wake_up_interruptible` 的配合原理是什么?](#Q10:
wait_event_interruptible和wake_up_interruptible的配合原理是什么?) - [Q11:阻塞 I/O 和非阻塞 I/O 在驱动层面如何实现?](#Q11:阻塞 I/O 和非阻塞 I/O 在驱动层面如何实现?)
- [Q12:驱动的 `.poll` 函数是怎么工作的?`poll_wait` 会让进程睡眠吗?](#Q12:驱动的
.poll函数是怎么工作的?poll_wait会让进程睡眠吗?) - Q13:异步通知(`SIGIO`)的实现步骤有哪些?驱动需要做什么?
- [Q10:`wait_event_interruptible` 和 `wake_up_interruptible` 的配合原理是什么?](#Q10:
- [四、设备树与 Platform 驱动(企业级开发核心)](#四、设备树与 Platform 驱动(企业级开发核心))
-
- [Q14:设备树中的 `compatible` 属性作用是什么?驱动如何匹配?](#Q14:设备树中的
compatible属性作用是什么?驱动如何匹配?) - [Q15:`probe` 函数一般要做哪些事情?](#Q15:
probe函数一般要做哪些事情?) - [Q16:`platform_driver` 的匹配机制有哪几种?](#Q16:
platform_driver的匹配机制有哪几种?) - [Q17:在设备树中描述 GPIO 引脚的标准格式是什么?](#Q17:在设备树中描述 GPIO 引脚的标准格式是什么?)
- [Q14:设备树中的 `compatible` 属性作用是什么?驱动如何匹配?](#Q14:设备树中的
- [五、I2C 驱动(AT24C02 案例)](#五、I2C 驱动(AT24C02 案例))
-
- [Q18:I2C 驱动的 `i2c_drv_read` 函数为什么需要两条消息(两条 `i2c_msg`)?一条读消息行不行?](#Q18:I2C 驱动的
i2c_drv_read函数为什么需要两条消息(两条i2c_msg)?一条读消息行不行?) - [Q19:AT24C02 的页写限制是什么?驱动中如何处理?](#Q19:AT24C02 的页写限制是什么?驱动中如何处理?)
- [Q20:为什么 I2C 写操作后要加 `mdelay(20)`?读操作不需要?](#Q20:为什么 I2C 写操作后要加
mdelay(20)?读操作不需要?)
- [Q18:I2C 驱动的 `i2c_drv_read` 函数为什么需要两条消息(两条 `i2c_msg`)?一条读消息行不行?](#Q18:I2C 驱动的
- [六、SPI 驱动(TLC5615 DAC 案例)](#六、SPI 驱动(TLC5615 DAC 案例))
-
- [Q21:SPI 的四种模式(Mode 0/1/2/3)由哪两个参数决定?如何区分?](#Q21:SPI 的四种模式(Mode 0/1/2/3)由哪两个参数决定?如何区分?)
- [Q22:Linux SPI 驱动中,`spi_transfer` 和 `spi_message` 的关系是什么?](#Q22:Linux SPI 驱动中,
spi_transfer和spi_message的关系是什么?) - [Q23:SPI 驱动中,片选信号(CS)需要驱动手动控制吗?](#Q23:SPI 驱动中,片选信号(CS)需要驱动手动控制吗?)
- 七、并发控制与竞态条件(进阶必考)
-
- Q24:自旋锁(spinlock)和互斥锁(mutex)的区别?分别在什么场景使用?
- [Q25:DS18B20 驱动中为什么要用 `spin_lock_irqsave` 而不是普通 `spin_lock`?](#Q25:DS18B20 驱动中为什么要用
spin_lock_irqsave而不是普通spin_lock?) - Q26:中断处理函数中能使用互斥锁吗?为什么?
- 八、调试技巧与常见错误排查(实战必备)
-
- [Q27:`insmod` 报 `File exists` 错误,可能原因有哪些?如何排查?](#Q27:
insmod报File exists错误,可能原因有哪些?如何排查?) - [Q28:`request_irq` 失败,返回 `-EBUSY` 或内核打印 `tried to flag a GPIO set as output for IRQ`,是什么原因?](#Q28:
request_irq失败,返回-EBUSY或内核打印tried to flag a GPIO set as output for IRQ,是什么原因?) - [Q29:模块加载后 `/dev/xxx` 没有自动创建,怎么排查?](#Q29:模块加载后
/dev/xxx没有自动创建,怎么排查?) - [Q30:编译内核模块时出现 `gnu/stubs-soft.h: No such file or directory` 或 `Relocations in generic ELF (EM: 40)` 错误,怎么解决?](#Q30:编译内核模块时出现
gnu/stubs-soft.h: No such file or directory或Relocations in generic ELF (EM: 40)错误,怎么解决?)
- [Q27:`insmod` 报 `File exists` 错误,可能原因有哪些?如何排查?](#Q27:
- 九、综合应用题(面试官最爱)
-
- Q31:如果让你设计一个支持多按键的驱动,每个按键有独立的去抖定时器和键值,你会怎么设计数据结构?
- Q32:一个驱动同时被多个进程打开,如何保证数据不会混乱?(以环形缓冲区为例)
- [Q33:I2C 和 SPI 总线的主要区别?什么场景选 I2C?什么场景选 SPI?](#Q33:I2C 和 SPI 总线的主要区别?什么场景选 I2C?什么场景选 SPI?)
- [Q34:DS18B20 驱动为什么用 `mdelay`?为什么不能直接用 `msleep`?而 DS18B20 中等待转换时又用 `schedule_timeout`?](#Q34:DS18B20 驱动为什么用
mdelay?为什么不能直接用msleep?而 DS18B20 中等待转换时又用schedule_timeout?)
- 十、快速记忆口诀
- 考核前最后的叮嘱
一、字符设备驱动框架(基础但必考)
Q1:register_chrdev 和 cdev 接口有什么区别?为什么推荐用 cdev?
难度:⭐⭐
点击查看答案
区别:
register_chrdev一次性分配主设备号并注册 0~255 共 256 个次设备号,资源浪费严重cdev接口分步操作:alloc_chrdev_region→cdev_init→cdev_add,可按需申请次设备号数量
推荐原因:
- 节省内核资源(尤其次设备号是稀缺资源)
- 一个驱动可管理多个不同类型的设备
- 符合 Linux 设备模型的分层设计思想
Q2:copy_to_user 和 copy_from_user 能直接用 memcpy 替代吗?为什么?
难度:⭐⭐
点击查看答案
绝对不能。原因:
- 用户空间地址可能非法或未映射,直接访问会导致内核崩溃(oops)
- 用户空间内存可能被换出到磁盘,
memcpy无法处理缺页异常 - 这两个函数会进行地址合法性检查,并处理缺页异常
正确用法 :检查返回值,非 0 表示拷贝失败,应返回 -EFAULT。
Q3:设备节点 /dev/xxx 是怎么自动创建的?需要手动 mknod 吗?
难度:⭐
点击查看答案
现代驱动通过以下两步自动创建:
class_create(THIS_MODULE, "xxx_class")→ 在/sys/class下创建类目录device_create(class, NULL, MKDEV(major, 0), NULL, "xxx")→ 触发udev/mdev自动在/dev下创建设备节点
不需要手动 mknod ,但如果 device_create 失败或系统没有 udev,可以手动创建调试。
Q4:module_init 和 module_exit 的作用是什么?__init 和 __exit 修饰符的意义?
难度:⭐
点击查看答案
module_init:指定模块加载时执行的入口函数module_exit:指定模块卸载时执行的出口函数__init:告诉内核该函数只在初始化阶段使用,执行完后可释放内存__exit:告诉内核该函数只在卸载时使用,如果编译进内核(非模块)可忽略
二、GPIO 子系统与中断(高频考点)
Q5:怎么查看一个 GPIO 在内核中的编号?以 GPIO4_19 为例。
难度:⭐
点击查看答案
三种方法:
- Debug 文件系统 :
cat /sys/kernel/debug/gpio - Sysfs 方式 :查看
/sys/class/gpio/gpiochip*/label确定每组的起始编号,再计算 - 公式计算(i.MX6ULL) :
(bank - 1) × 32 + offset。GPIO4_19→(4-1)×32 + 19 = 115
Q6:request_irq 的参数 dev_id 有什么作用?为什么 free_irq 需要传入相同的指针?
难度:⭐⭐
点击查看答案
- 作用 :当多个设备共享同一根中断线(
IRQF_SHARED)时,dev_id用于区分中断来源;同时也是中断处理函数获取设备私有数据的途径 - 释放时需要相同指针 :内核根据
irq+dev_id唯一标识一个中断处理函数,传入不同指针无法找到对应注册项,导致释放失败或误释放其他设备的中断
Q7:中断处理函数(ISR)中能做什么?不能做什么?为什么?
难度:⭐⭐⭐
点击查看答案
能做:
- 读取/写入硬件寄存器
- 修改只被中断访问的变量(注意加
volatile) - 调用
wake_up_interruptible唤醒等待队列 - 调用
kill_fasync发送信号 - 调度软中断、tasklet、工作队列
绝对不能做:
- 调用
copy_to_user/copy_from_user(可能睡眠) - 调用
kmalloc(..., GFP_KERNEL)(可能睡眠) - 调用
mutex_lock(互斥锁可能睡眠) - 任何可能导致调度的函数
原因:ISR 运行在中断上下文,不属于任何进程,无法被调度出去,睡眠会导致内核严重问题。
Q8:按键驱动中为什么要用定时器(timer)?原理是什么?
难度:⭐⭐
点击查看答案
原因:机械按键存在抖动(几毫秒到几十毫秒),若每次中断都上报,一次按下会触发几十次事件。
原理:
- 中断 ISR 中不直接读 GPIO,而是调用
mod_timer(&timer, jiffies + msecs_to_jiffies(20)) - 每次抖动中断都会"刷新"定时器,即把到期时间推迟 20ms
- 最后一次中断后 20ms 若无新中断,定时器到期执行回调,此时读取的 GPIO 电平已稳定
- 这样就实现了 去抖 + 只上报一次 的效果
Q9:什么是中断上半部和下半部?有哪些下半部机制?
难度:⭐⭐⭐
点击查看答案
上半部 :request_irq 注册的中断处理函数,要求快速执行,通常只做最少的事(如清除中断标志、启动下半部)
下半部:处理耗时工作的机制,允许被中断打断
常用下半部机制:
| 机制 | 特点 | 适用场景 |
|---|---|---|
| 软中断 | 最快,但复杂,驱动不能直接使用 | 网络子系统 |
| tasklet | 基于软中断,同一类型不会并行执行 | 多数驱动场景 |
| 工作队列 | 可睡眠,进程上下文 | 大量数据处理、需要睡眠的操作 |
| 定时器 | 延迟执行 | 去抖、轮询 |
三、等待队列、阻塞/非阻塞、Poll、异步通知(核心机制)
Q10:wait_event_interruptible 和 wake_up_interruptible 的配合原理是什么?
难度:⭐⭐
点击查看答案
原理:
wait_event_interruptible(wq, condition)检查condition,若为假则当前进程加入wq队列,状态设为TASK_INTERRUPTIBLE,然后调用schedule()交出 CPU- 中断或其他上下文中调用
wake_up_interruptible(&wq),将队列中所有进程状态改回TASK_RUNNING,等待调度器分配 CPU - 被唤醒的进程重新检查
condition,若为真则继续执行,否则可能再次睡眠
关键点 :wake_up 只负责唤醒,不负责条件判断;wait_event 被唤醒后会重新检查条件,这是防止"虚假唤醒"的核心机制。
Q11:阻塞 I/O 和非阻塞 I/O 在驱动层面如何实现?
难度:⭐⭐
点击查看答案
驱动通过检查 file->f_flags & O_NONBLOCK 来决定行为:
c
static ssize_t my_read(struct file *filp, char __user *buf, size_t size, loff_t *off)
{
if (is_buffer_empty() && (filp->f_flags & O_NONBLOCK))
return -EAGAIN; // 非阻塞,立即返回
wait_event_interruptible(wq, !is_buffer_empty()); // 阻塞,睡眠等待
// ... 读取数据
}
- 应用
open时带O_NONBLOCK标志 → 驱动走非阻塞路径 - 不带该标志或带
O_RDWR→ 驱动走阻塞路径
Q12:驱动的 .poll 函数是怎么工作的?poll_wait 会让进程睡眠吗?
难度:⭐⭐⭐
点击查看答案
.poll 不会让进程立即睡眠。流程如下:
- 应用调用
poll(fds, nfds, timeout) - 内核调用驱动的
.poll函数 .poll内部调用poll_wait------ 这只是将当前进程注册到等待队列,不调度- 内核检查返回值:若有数据(返回
POLLIN),立即返回;若无数据(返回 0),内核调用schedule_timeout让进程睡眠 - 超时或被
wake_up唤醒后,重新调用.poll检查状态
关键点 :poll_wait 不等于睡眠,它只是"告诉内核将来用什么队列来唤醒我"。真正的睡眠由内核的 do_poll 循环管理。
Q13:异步通知(SIGIO)的实现步骤有哪些?驱动需要做什么?
难度:⭐⭐⭐
点击查看答案
应用层(4 步):
signal(SIGIO, handler)注册信号处理函数fcntl(fd, F_SETOWN, getpid())设置文件所有者fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | FASYNC)设置异步标志- 在主循环做其他事,信号触发时在 handler 中
read
驱动层:
- 实现
.fasync函数,内部调用fasync_helper管理struct fasync_struct * - 在中断 ISR 或数据就绪时调用
kill_fasync(&fasync, SIGIO, POLL_IN)
四、设备树与 Platform 驱动(企业级开发核心)
Q14:设备树中的 compatible 属性作用是什么?驱动如何匹配?
难度:⭐⭐
点击查看答案
作用 :compatible = "厂商,型号" 是设备与驱动的"身份证",内核用它来匹配设备和驱动。
驱动匹配方式:
c
static const struct of_device_id my_ids[] = {
{ .compatible = "100ask,gpiodemo" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_ids);
static struct platform_driver my_driver = {
.driver = {
.name = "my_drv",
.of_match_table = my_ids, // 关键:匹配设备树
},
.probe = my_probe,
};
设备树节点中 compatible 与驱动 of_match_table 中任意一条匹配,内核就会调用该驱动的 probe 函数。
Q15:probe 函数一般要做哪些事情?
难度:⭐⭐
点击查看答案
- 获取硬件资源:从设备树或 platform 资源中读取 GPIO、中断号、寄存器地址等
- 初始化硬件 :
gpio_request、gpio_direction_*、request_irq、clk_enable等 - 注册字符设备 :
alloc_chrdev_region+cdev_add或register_chrdev - 创建设备节点 :
class_create+device_create - 保存私有数据 :通常用
platform_set_drvdata(pdev, private_data)
Q16:platform_driver 的匹配机制有哪几种?
难度:⭐⭐⭐
点击查看答案
按 platform_match 函数中的尝试顺序(源码 drivers/base/platform.c):
| 优先级 | 匹配方式 | 说明 |
|---|---|---|
| 1 | driver_override |
强制覆盖匹配,很少用 |
| 2 | 设备树(.of_match_table) |
现代驱动首选 ,比较 compatible |
| 3 | ACPI 匹配 | x86 平台使用 |
| 4 | id_table 匹配 |
平台设备 ID 表 |
| 5 | 名字直接匹配 | 比较 pdev->name 与 drv->driver.name(传统方式) |
Q17:在设备树中描述 GPIO 引脚的标准格式是什么?
难度:⭐⭐
点击查看答案
dts
gpios = <&gpio4 19 GPIO_ACTIVE_LOW>;
&gpio4:GPIO 控制器引用19:组内偏移(引脚号)GPIO_ACTIVE_LOW:有效电平(低有效),也可用GPIO_ACTIVE_HIGH
驱动中通过 of_get_gpio(np, index) 或 gpiod_get 获取实际 GPIO 编号。
五、I2C 驱动(AT24C02 案例)
Q18:I2C 驱动的 i2c_drv_read 函数为什么需要两条消息(两条 i2c_msg)?一条读消息行不行?
难度:⭐⭐
点击查看答案
不行。因为 AT24C02(以及其他大多数 EEPROM/传感器)的读取时序要求:
- 先发送内部地址(设备地址+写方向 + 寄存器/存储地址)
- 再发 Repeat Start,切换为读方向,读取数据
如果只用一条读消息,芯片不知道要从哪个地址读,会返回随机或无效数据。
两条消息通过 i2c_transfer 一次性提交,硬件会在它们之间插入 Repeat Start(不是 STOP),保证总线不被释放,操作原子化。
Q19:AT24C02 的页写限制是什么?驱动中如何处理?
难度:⭐⭐
点击查看答案
限制 :AT24C02 的页写缓冲区为 8 字节,一次最多写入 8 字节,超过 8 字节会发生"页翻转",覆盖同一页前面的数据。
处理 :驱动中的 i2c_drv_write 使用循环分页写入:
c
while (size > 0) {
len = min(size, 8);
// 构造消息:1字节地址 + len字节数据
i2c_transfer(adapter, &msg, 1);
mdelay(20); // 等待内部编程完成
addr += len;
size -= len;
}
Q20:为什么 I2C 写操作后要加 mdelay(20)?读操作不需要?
难度:⭐⭐
点击查看答案
- 写操作需要 :EEPROM 在写入后需要内部编程时间(典型 5ms,最大 10ms),在此期间芯片不响应 I2C 通信(返回 NACK)。
mdelay(20)确保足够等待时间 - 读操作不需要:读取只是从 EEPROM 中读出数据,不触发擦写过程,芯片随时可以响应
六、SPI 驱动(TLC5615 DAC 案例)
Q21:SPI 的四种模式(Mode 0/1/2/3)由哪两个参数决定?如何区分?
难度:⭐⭐
点击查看答案
由 CPOL(时钟极性) 和 CPHA(时钟相位) 决定:
| 模式 | CPOL | CPHA | 空闲 SCK | 数据采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低 | 上升沿 |
| 1 | 0 | 1 | 低 | 下降沿 |
| 2 | 1 | 0 | 高 | 下降沿 |
| 3 | 1 | 1 | 高 | 上升沿 |
设备树中用 spi-cpol 和 spi-cpha 属性指定(存在即表示 1)。
Q22:Linux SPI 驱动中,spi_transfer 和 spi_message 的关系是什么?
难度:⭐⭐
点击查看答案
spi_transfer:描述一次 传输,包含tx_buf(发送缓冲区)、rx_buf(接收缓冲区)、len(长度)、speed_hz(速度)等spi_message:由多个spi_transfer链接而成,保证它们被原子执行(CS 在整个消息期间保持有效)
常用简化函数 :spi_sync_transfer(spi, transfers, num) 内部自动创建 message 并提交。
Q23:SPI 驱动中,片选信号(CS)需要驱动手动控制吗?
难度:⭐
点击查看答案
不需要。Linux SPI 核心层和控制器驱动会自动管理 CS:
- 传输开始时,控制器驱动拉低 CS(根据
spi_device的chip_select和cs_gpio) - 传输结束时,拉高 CS
- 驱动只需调用
spi_sync_transfer或spi_async,无需关心 CS 的 GPIO 操作
七、并发控制与竞态条件(进阶必考)
Q24:自旋锁(spinlock)和互斥锁(mutex)的区别?分别在什么场景使用?
难度:⭐⭐⭐
点击查看答案
| 特性 | 自旋锁 | 互斥锁 |
|---|---|---|
| 获取锁失败时 | 忙循环等待(占用 CPU) | 进程睡眠,让出 CPU |
| 能否在中断中使用 | ✅ 可以(spin_lock_irqsave) |
❌ 不可以(可能睡眠) |
| 持锁时间 | 极短(微秒级) | 可长(毫秒级或更长) |
| 上下文 | 中断、软中断、进程上下文均可 | 只能进程上下文 |
选择原则:
- 持锁时间短 + 可能在中断中使用 → 自旋锁
- 持锁时间长 + 确定只在进程上下文 → 互斥锁
Q25:DS18B20 驱动中为什么要用 spin_lock_irqsave 而不是普通 spin_lock?
难度:⭐⭐⭐
点击查看答案
DS18B20 的单总线时序要求微秒级精度。如果在发送 480µs 复位脉冲的过程中被中断打断,回来时序已乱,通信失败。
spin_lock_irqsave 会保存当前中断状态并关闭本地 CPU 中断 ,保证临界区不被任何中断打扰。spin_lock 只防止其他 CPU 核心并发,不关中断,本 CPU 的中断仍可能打断时序。
为什么不用关中断的简单方式 :spin_lock_irqsave 会在释放时恢复原有中断状态,避免"永久关中断"的灾难。
Q26:中断处理函数中能使用互斥锁吗?为什么?
难度:⭐⭐
点击查看答案
绝对不能。因为:
- 互斥锁在获取不到时会导致当前进程睡眠
- 中断处理函数运行在中断上下文,不属于任何进程,无法被调度,睡眠会导致内核 panic
- 正确做法:在 ISR 中使用自旋锁(
spin_lock),或将需要睡眠的工作通过工作队列(workqueue)放到进程上下文执行
八、调试技巧与常见错误排查(实战必备)
Q27:insmod 报 File exists 错误,可能原因有哪些?如何排查?
难度:⭐⭐
点击查看答案
可能原因:
- 模块已加载 :
lsmod | grep xxx检查,rmmod卸载 - 设备节点残留 :
rm -f /dev/xxx - sysfs 类目录残留 :模块退出时
class_destroy未执行,手动rmdir /sys/class/xxx_class - 设备号仍被占用 :
cat /proc/devices | grep xxx,检查主设备号是否冲突
排查顺序 :lsmod → /dev/ → /sys/class/ → /proc/devices → 重启(最后手段)
Q28:request_irq 失败,返回 -EBUSY 或内核打印 tried to flag a GPIO set as output for IRQ,是什么原因?
难度:⭐⭐
点击查看答案
-EBUSY :中断号已被其他设备占用(且没有设置 IRQF_SHARED),使用 cat /proc/interrupts 查看中断占用情况。
GPIO 被设为输出 :GPIO 引脚当前是输出模式,不能作为中断源。解决方法:在 request_irq 之前必须调用 gpio_direction_input 将引脚设为输入模式。
c
gpio_request(gpio, name);
gpio_direction_input(gpio); // 关键:必须设为输入
irq = gpio_to_irq(gpio);
request_irq(irq, handler, flags, name, dev);
Q29:模块加载后 /dev/xxx 没有自动创建,怎么排查?
难度:⭐
点击查看答案
- 检查
class_create和device_create是否调用成功(查看dmesg有无错误打印) - 检查返回值:
IS_ERR(gpio_class)判断 class 是否创建成功 - 确认内核配置了
CONFIG_DEV_UREG或用户空间有udev/mdev - 临时解决方案:手动创建
mknod /dev/xxx c <主设备号> 0 - 查看
/proc/devices确认主设备号已分配
Q30:编译内核模块时出现 gnu/stubs-soft.h: No such file or directory 或 Relocations in generic ELF (EM: 40) 错误,怎么解决?
难度:⭐⭐⭐
点击查看答案
根本原因 :编译主机工具(如 modpost)时,环境变量 C_INCLUDE_PATH 或 LIBRARY_PATH 被指向了交叉编译器的路径,导致主机 GCC 去读 ARM 架构的头文件和库。
解决方案:
bash
unset C_INCLUDE_PATH
unset LIBRARY_PATH
unset LD_LIBRARY_PATH
unset CFLAGS LDFLAGS
原则:编译内核时不要 source 交叉编译环境脚本,通过 make 参数传递:
bash
make ARCH=arm CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
九、综合应用题(面试官最爱)
Q31:如果让你设计一个支持多按键的驱动,每个按键有独立的去抖定时器和键值,你会怎么设计数据结构?
难度:⭐⭐⭐
点击查看答案
c
struct button_desc {
int gpio;
int irq;
int key_code; // 键值(如 KEY_UP, KEY_DOWN)
struct timer_list debounce_timer; // 每个按键独立定时器
};
static struct button_desc *buttons;
static int button_count;
// probe 中动态分配
buttons = devm_kzalloc(dev, count * sizeof(*buttons), GFP_KERNEL);
for (i = 0; i < count; i++) {
buttons[i].gpio = of_get_gpio(np, i);
buttons[i].key_code = KEY_A + i; // 或其他映射
timer_setup(&buttons[i].debounce_timer, debounce_expire, 0);
request_irq(gpio_to_irq(buttons[i].gpio), isr,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"button", &buttons[i]);
}
关键点:
- 每个按键独立定时器,防止互相干扰
- 中断 ISR 中通过
dev_id获取对应的button_desc - 定时器回调中对
button_desc操作
Q32:一个驱动同时被多个进程打开,如何保证数据不会混乱?(以环形缓冲区为例)
难度:⭐⭐⭐
点击查看答案
方案:
- 环形缓冲区:生产者和消费者分离,数据不互相覆盖
- 互斥访问:如果有多进程写入,需要互斥锁保护
- 私有数据 :如果需要每个进程独立的数据,在
open中分配私有结构体,挂到filp->private_data
按键驱动案例(单生产者:中断;多消费者:不能同时读,因为只有一份数据):
- 使用
put_key(中断中)和get_key(read 中) - 如果允许多进程同时 read,需要加互斥锁防止多个进程同时取数据
- 常用做法:设置
O_EXCL或使用atomic_t标志禁止同时打开
c
static atomic_t open_count = ATOMIC_INIT(0);
static int my_open(struct inode *inode, struct file *filp)
{
if (atomic_inc_return(&open_count) > 1) {
atomic_dec(&open_count);
return -EBUSY;
}
return 0;
}
Q33:I2C 和 SPI 总线的主要区别?什么场景选 I2C?什么场景选 SPI?
难度:⭐⭐
点击查看答案
| 特性 | I2C | SPI |
|---|---|---|
| 信号线数量 | 2 根(SDA, SCL) | 4 根(MOSI, MISO, SCK, CS) |
| 速度 | 100kbps(标准),400kbps(快速),3.4Mbps(高速) | 几十 Mbps |
| 设备寻址 | 7/10 位地址 | 独立的 CS 线 |
| 全双工 | 半双工 | 全双工 |
| 功耗 | 低 | 较高 |
| 多设备 | 每个设备不同地址 | 每个设备独立 CS 线 |
选择建议:
- I2C:传感器、EEPROM、RTC、设备数量多但速度要求不高、引脚有限
- SPI:ADC/DAC、显示屏、SD 卡、Flash、需要高速或全双工传输
Q34:DS18B20 驱动为什么用 mdelay?为什么不能直接用 msleep?而 DS18B20 中等待转换时又用 schedule_timeout?
难度:⭐⭐⭐
点击查看答案
原因:
mdelay:忙等待(busy-wait),不会让出 CPU,但精度高(微秒级)。DS18B20 的复位脉冲要求 480µs 的精确低电平 ,期间不能被打断,只能用mdelay或自定义udelaymsleep:会调度其他进程,可能造成几十毫秒的误差,不适合微秒级时序
转换等待用 schedule_timeout:
- 温度转换需要 750ms ,如果用
mdelay(750),CPU 在 750ms 内空转,浪费严重 schedule_timeout让进程睡眠,交出 CPU,750ms 后被唤醒,高效且不影响系统响应
结论 :微秒级延时用 mdelay/udelay(需关中断);毫秒级以上延时用 schedule_timeout / msleep(可调度)。
十、快速记忆口诀
text
字符设备三要素:设备号、cdev、ops结构体
GPIO 操作四步走:请求、方向、读/写、释放
中断开发五步骤:申请 GPIO、设输入、转 IRQ、注册 ISR、free_irq
等待队列核心:wait_event 条件查,wake_up 中断发
设备树匹配靠:compatible 对 of_match_table
Platform 驱动:probe 拿资源,remove 要逆序
I2C 读写两消息:先写地址后读数据
SPI 传输用 message:多个 transfer 原子化
并发保护要记牢:自旋锁短中断用,互斥锁长进程行
考核前最后的叮嘱
- 动手胜过一切:把每个文档中的驱动代码自己敲一遍,编译、加载、测试
- 理解原理链:不要只记 API,要能讲出"APP → VFS → 驱动 → 硬件"的完整路径
- 十分钟速查:面试前用这份题库过一遍,确保每个问题都能说出 2-3 个关键点
祝你顺利通过考核!这些内容如果都能掌握,你已经具备 1-2 年嵌入式 Linux 驱动开发经验 的核心能力了。
本回答由 AI 生成,内容仅供参考,请仔细甄别。