嵌入式Linux驱动开发/BSP岗位面试,考察的核心无非三块:C语言功底、内核机制理解、驱动框架掌握。 日常工作中我们往往围绕业务逻辑进行开发而忽略这些看似简单却可能不能立马给出精确回答的。 整理了 15 道高频面试题,包含 问题 + 详细回答 + 代码示例/场景分析,希望对你有用。
问题: 请描述在 Linux 内核中注册一个字符设备驱动的完整流程。
回答:
以 Linux 5.15+ 推荐的新接口为例,分四步走:
// 1. 分配设备号(动态分配)dev_t dev_num;alloc_chrdev_region(&dev_num, 0, 1, "my_device");int major = MAJOR(dev_num);// 2. 初始化 cdev 结构体struct cdev my_cdev;cdev_init(&my_cdev, &my_fops);my_cdev.owner = THIS_MODULE;// 3. 添加字符设备到内核cdev_add(&my_cdev, dev_num, 1);// 4. 自动创建设备节点(推荐)struct class *my_class = class_create(THIS_MODULE, "my_class");device_create(my_class, NULL, dev_num, NULL, "my_device");
新旧接口区别:
register_chrdev | cdev_init + cdev_add | |
|---|---|---|
追问考点:
alloc_chrdev_region和register_chrdev_region区别? 前者由内核动态分配主设备号,后者手动指定。生产环境推荐动态分配,避免冲突。
问题: 请说明 file_operations 结构体中最重要的几个回调函数,并给出 open 和 release 的典型实现。
回答:
file_operations 是字符设备驱动的核心接口,关键成员:
struct file_operations {struct module *owner;loff_t (*llseek)(struct file *, loff_t, int);ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);int (*open)(struct inode *, struct file *);int (*release)(struct inode *, struct file *);long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);int (*mmap)(struct file *, struct vm_area_struct *);};
open 典型实现:
staticintmy_open(struct inode *inode, structfile *filp){struct my_dev *dev = container_of(inode->i_cdev, struct my_dev, cdev);filp->private_data = dev; // 保存设备结构体// 递增使用计数try_module_get(THIS_MODULE);return 0;}
release 典型实现:
staticintmy_release(struct inode *inode, structfile *filp){struct my_dev *dev = filp->private_data;// 清理资源// ...module_put(THIS_MODULE);return 0;}
追问考点:
filp->private_data的作用?—— 传递驱动私有数据结构给其他回调函数。
问题: 为什么说中断上下文不能睡眠?如果中断处理需要做大量工作,应该怎么办?
回答:
原因: 中断上下文没有进程的 task_struct,不参与内核调度。如果睡眠,内核无法找到哪个进程来唤醒,直接导致系统崩溃(BUG: scheduling while atomic)。
解决方案: 将中断处理分为上半部(Top Half) 和下半部(Bottom Half)。
上半部(硬中断上下文) 下半部(可延迟)┌──────────────┐ ┌──────────────┐│ 读寄存器 │ │ 数据处理 ││ 清除中断标志 │ ──────→ │ 唤醒等待队列 ││ 调度下半部 │ │ 拷贝数据 ││ ❌ 不能睡眠 │ │ ✅ 可以睡眠 │└──────────────┘ └──────────────┘
三种下半部实现机制对比:
| Tasklet | ||||
| Workqueue | ||||
| 软中断(Softirq) |
代码示例(Tasklet 方式):
// 上半部irqreturn_tmy_irq_handler(int irq, void *dev_id){// 读状态、清标志uint32_t status = readl(reg_base + STATUS_REG);writel(status, reg_base + STATUS_REG);// 调度 tasklettasklet_schedule(&my_tasklet);return IRQ_HANDLED;}// 下半部(tasklet)voidmy_tasklet_func(unsignedlong data){// 处理数据,不能睡眠process_data();wake_up_interruptible(&waitq);}DECLARE_TASKLET(my_tasklet, my_tasklet_func, 0);
代码示例(Workqueue 方式——可以睡眠):
// 上半部irqreturn_tmy_irq_handler(int irq, void *dev_id){schedule_work(&my_work); // 调度 workreturn IRQ_HANDLED;}// 下半部(workqueue,可以睡眠)voidmy_work_func(struct work_struct *work){msleep(10); // ✅ 可以睡眠copy_to_user(...); // ✅ 可以访问用户空间}DECLARE_WORK(my_work, my_work_func);
高频追问:
request_threaded_irq是什么?—— 注册 threaded IRQ,handler 中只做最小操作,threded_fn 运行在进程上下文,可以睡眠,相当于内核帮你实现了下半部。
问题: 自旋锁和信号量在互斥使用时有什么区别?中断服务程序里该用哪个?
回答:
核心区别:
中断中的选择:
// ✅ 正确:中断中用自旋锁spinlock_t my_lock;irqreturn_tmy_irq_handler(int irq, void *dev_id){unsigned long flags;spin_lock_irqsave(&my_lock, flags);// 操作共享资源spin_unlock_irqrestore(&my_lock, flags);return IRQ_HANDLED;}// ❌ 错误:中断中不能用信号量irqreturn_tmy_irq_handler(int irq, void *dev_id){down(&my_sem); // 可能导致睡眠!系统崩溃!// ...up(&my_sem);}
为什么用 spin_lock_irqsave 而不是 spin_lock?
spin_lock_irqsave 会保存当前中断状态并禁用本地中断,防止中断处理函数和进程上下文同时抢占锁,产生死锁。spin_lock 不会关中断,如果进程上下文持有锁时被中断打断,中断处理函数又去申请同一把锁 → 死锁!延伸考点:读写锁(rwlock)、RCU(Read-Copy-Update) 适用于读多写少的场景,性能优于自旋锁。
问题: 请说明设备树的作用,以及 platform 驱动是如何通过设备树匹配到设备的。
回答:
设备树的作用: 描述硬件信息的结构化数据,替代了旧的板级文件(board-xxx.c),实现 内核与硬件信息解耦。
设备树节点示例:my_led: led@2000000 {compatible = "my-company,my-led"; // ← 匹配的关键reg = <0x2000000 0x1000>; // 寄存器基址和大小interrupts = <0 31 4>; // 中断号status = "okay";};
platform 驱动匹配过程:
设备树节点 platform 驱动┌──────────────┐ ┌──────────────────┐│ compatible = │ │ of_match_table = ││ "my-company, │ ─────→ │ { .compatible = ││ my-led" │ 匹配 │ "my-company, ││ reg = ... │ │ my-led" }, ││ interrupts=...│ │ ... } │└──────────────┘ └──────────────────┘│ │└──────────┬───────────────┘▼probe() 函数被调用
驱动代码示例:
// 1. of_match_table 定义static const struct of_device_id my_of_match[] = {{ .compatible = "my-company,my-led" },{ /* sentinel */ }};MODULE_DEVICE_TABLE(of, my_of_match);// 2. platform_driver 定义static struct platform_driver my_driver = {.probe = my_probe,.remove = my_remove,.driver = {.name = "my_led",.of_match_table = my_of_match,},};module_platform_driver(my_driver);
probe 函数中获取设备资源:
staticintmy_probe(struct platform_device *pdev){struct resource *res;void __iomem *reg_base;int irq;// 获取寄存器地址res = platform_get_resource(pdev, IORESOURCE_MEM, 0);reg_base = devm_ioremap_resource(&pdev->dev, res);// 获取中断号irq = platform_get_irq(pdev, 0);// 获取设备树中的自定义属性u32 val;of_property_read_u32(pdev->dev.of_node, "my-value", &val);return 0;}
追问考点:
devm_系列函数(如devm_ioremap_resource、devm_kzalloc)的好处?—— 资源自动管理,probe 失败或 remove 时自动释放,避免资源泄漏。
问题: 内核中申请内存有哪几个函数?各自的区别和适用场景?
回答:
// 1. kmalloc — 分配连续的物理内存void *kmalloc(size_t size, gfp_t flags);// 2. kzalloc — kmalloc + 清零void *kzalloc(size_t size, gfp_t flags);// 3. vmalloc — 虚拟地址连续,物理地址不一定连续void *vmalloc(unsignedlong size);
对比:
kmalloc | kfree | ||||
kzalloc | kfree | ||||
vmalloc | vfree |
场景选择:
// ✅ DMA 缓冲区 → 必须用 kmalloc(物理地址连续)buf = kmalloc(4096, GFP_KERNEL | GFP_DMA);// ✅ 驱动私有数据结构 → 用 kzalloc(自动清零)struct my_dev *dev = kzalloc(sizeof(*dev), GFP_KERNEL);// ✅ 大块内存(如 framebuffer)→ 用 vmallocfb = vmalloc(1024 * 768 * 4); // 3MB// ❌ 中断中 → 只能用 kmalloc/kzalloc + GFP_ATOMICbuf = kmalloc(256, GFP_ATOMIC);
追问考点:
GFP_KERNEL和GFP_ATOMIC的区别?—— GFP_KERNEL 可能睡眠(页面回收/交换),GFP_ATOMIC 不会,用于中断上下文。
问题: 驱动中如何将数据从内核空间传到用户空间?为什么不能直接用 memcpy?
回答:
数据传输函数:
// 内核 → 用户空间unsignedlongcopy_to_user(void __user *to, constvoid *from, unsignedlong n);// 用户空间 → 内核unsignedlongcopy_from_user(void *to, constvoid __user *from, unsignedlong n);
为什么不能直接用 memcpy?
copy_to_user/copy_from_user 会检查用户地址是否合法(不在内核空间、已映射等),防止用户传入非法指针导致内核崩溃。典型用法(read/write 实现):
staticssize_tmy_read(struct file *filp, char __user *buf,size_t count, loff_t *f_pos){struct my_dev *dev = filp->private_data;ssize_t ret;if (count > dev->data_size)count = dev->data_size;if (copy_to_user(buf, dev->data, count))return -EFAULT;*f_pos += count;return count;}staticssize_tmy_write(struct file *filp, constchar __user *buf,size_t count, loff_t *f_pos){struct my_dev *dev = filp->private_data;if (count > MAX_DATA_SIZE)return -ENOMEM;if (copy_from_user(dev->data, buf, count))return -EFAULT;*f_pos += count;return count;}
注意: 返回值是未拷贝成功的字节数,返回 0 表示全部拷贝成功。
问题: 请简述 Linux 设备驱动模型中 platform 总线、I2C 总线、SPI 总线的驱动框架。
回答:
Linux 设备驱动模型的核心是 总线-设备-驱动 三者的关系:
┌─────────────────────────┐│ 总线 (Bus) ││ 负责匹配设备和驱动 │└──────┬──────────┬───────┘│ │┌──────▼──┐ ┌───▼────────┐│ 设备 │ │ 驱动 ││(device) │ │ (driver) ││ 描述硬件 │ │ 处理逻辑 │└─────────┘ └────────────┘
platform 总线驱动框架:
// 设备端(设备树或板级文件)// 驱动端static struct platform_driver my_pdrv = {.probe = my_probe,.remove = my_remove,.driver = {.name = "my_device",.of_match_table = my_of_match,},};module_platform_driver(my_pdrv);
I2C 总线驱动框架:
static const struct i2c_device_id my_i2c_id[] = {{ "my_sensor", 0 },{ }};MODULE_DEVICE_TABLE(i2c, my_i2c_id);static struct i2c_driver my_i2c_driver = {.probe = my_i2c_probe,.remove = my_i2c_remove,.id_table = my_i2c_id,.driver = {.name = "my_sensor",.of_match_table = my_of_match,},};module_i2c_driver(my_i2c_driver);// probe 中使用 i2c 通信staticintmy_i2c_probe(struct i2c_client *client){u8 buf[2];struct i2c_msg msg = {.addr = client->addr,.buf = buf,.len = 2,};i2c_transfer(client->adapter, &msg, 1);return 0;}
SPI 总线驱动框架:
static struct spi_driver my_spi_driver = {.probe = my_spi_probe,.remove = my_spi_remove,.driver = {.name = "my_spi_dev",.of_match_table = my_of_match,},};module_spi_driver(my_spi_driver);// probe 中使用 SPI 通信staticintmy_spi_probe(struct spi_device *spi){u8 tx_buf[] = {0x01, 0x02};u8 rx_buf[2];struct spi_transfer tr = {.tx_buf = tx_buf,.rx_buf = rx_buf,.len = 2,};spi_sync_transfer(spi, &tr, 1);return 0;}
追问考点: 三种总线的匹配方式有何不同?
platform: 通过设备树 compatible或驱动 name 匹配I2C: 通过设备树 compatible或i2c_device_id表匹配SPI: 通过设备树 compatible或spi_device_id表匹配
问题: 请对比 I2C 和 SPI 总线协议的区别和适用场景。
回答:
| 全双工 | ||
场景选择:
I2C 适合: SPI 适合:┌────────────────────┐ ┌────────────────────┐│ 传感器(温湿度、 │ │ 高速 ADC/DAC ││ 气压、光照等) │ │ LCD 显示屏 ││ RTC 时钟芯片 │ │ SD 卡 / TF 卡 ││ EEPROM 存储 │ │ 射频模块(NRF24L01)││ 低速、引脚受限场景 │ │ 音频编解码器 │└────────────────────┘ └────────────────────┘
追问考点: I2C 为什么需要上拉电阻?—— 开漏输出,只能拉低不能拉高,需要上拉电阻提供高电平。
问题: 在驱动中如何将一个 GPIO 引脚配置为中断输入?请写出完整流程。
回答:
#include<linux/gpio.h>#include<linux/interrupt.h>#include<linux/of_gpio.h>staticintmy_probe(struct platform_device *pdev){int gpio, irq, ret;// 1. 从设备树获取 GPIO 编号gpio = of_get_named_gpio(pdev->dev.of_node, "irq-gpios", 0);if (!gpio_is_valid(gpio)) {dev_err(&pdev->dev, "invalid GPIO\n");return -EINVAL;}// 2. 申请 GPIOret = devm_gpio_request_one(&pdev->dev, gpio,GPIOF_IN, "my_irq_gpio");if (ret) {dev_err(&pdev->dev, "gpio request failed\n");return ret;}// 3. GPIO 转中断号irq = gpio_to_irq(gpio);if (irq < 0) {dev_err(&pdev->dev, "gpio_to_irq failed\n");return irq;}// 4. 注册中断ret = request_irq(irq, my_irq_handler,IRQF_TRIGGER_RISING, // 上升沿触发"my_device", dev);if (ret) {dev_err(&pdev->dev, "request_irq failed\n");return ret;}return 0;}// 中断处理函数irqreturn_tmy_irq_handler(int irq, void *dev_id){struct my_dev *dev = dev_id;// 处理中断return IRQ_HANDLED;}staticintmy_remove(struct platform_device *pdev){// devm_ 系列自动释放,无需手动 free_irqreturn 0;}
设备树节点:
my_button {compatible = "my-company,my-button";irq-gpios = <&gpio0 15 GPIO_ACTIVE_HIGH>; // GPIO0_PB7interrupt-parent = <&gpio0>;interrupts = <15 IRQ_TYPE_EDGE_RISING>;};
追问考点:
devm_gpio_request_one和gpio_request的区别?—— devm_ 系列自动管理资源生命周期,remove 时自动释放。
问题: 驱动加载时内核报 Oops,如何根据打印信息定位问题?
回答:
Oops 是内核发现异常时的错误信息,类似用户态的段错误。典型 Oops 信息:
Unable to handle kernel NULL pointer dereference at virtual address 00000004pgd = c0004000[00000004] *pgd=00000000Internal error: Oops: 5 [#1] PREEMPT ARMModules linked in: my_driver(O)CPU: 0 PID: 123 Comm: insmod Tainted: G O 5.10.0Hardware name: MyBoardPC is at my_read+0x14/0x40 [my_driver] ← ★ 关键:出错的函数LR is at my_read+0x10/0x40 [my_driver]pc : [<bf000014>] lr : [<bf000010>] psr: 60000013sp : c7827e38 ip : c7827e50 fp : c7827e4cr10: 00000000 r9 : c7826000 r8 : bf000000r7 : c4a5c480 r6 : c4a5c480 r5 : 00000000 r4 : c4a5c480r3 : 00000000 r2 : 00000000 r1 : 00000000 r0 : c4a5c480Process insmod (pid: 123, stack limit = 0xc7826238)Stack: (0xc7827e38 to 0xc7828000)...Code: e5903004 e5931000 e3a00000 e5810000 (e5902004)
定位步骤:
PC is at my_read+0x14/0x40 | my_read 函数偏移 0x14 处 | |
Unable to handle NULL pointer dereference | ||
r5 : 00000000 | ||
Modules linked in: my_driver(O) | ||
Code: ... (e5902004) | objdump 反汇编定位 |
反汇编定位:
arm-linux-objdump -S my_driver.ko | grep -A 20 "my_read>"找到偏移 0x14 处的指令,就能精确定位到源码哪一行。
调试技巧: 开启
CONFIG_DEBUG_KERNEL、CONFIG_KALLSYMS、CONFIG_DEBUG_INFO可以获得更详细的 Oops 信息。
问题: 驱动开发中为什么会出现竞态(Race Condition)?如何解决?
回答:
竞态产生场景:
场景1:多核 CPU 同时访问共享资源场景2:进程上下文 vs 中断上下文抢占场景3:多个进程同时 open 同一个设备场景4:SMP 下同一个中断在不同 CPU 上触发
Linux 内核提供的同步机制:
| 自旋锁(spinlock) | ||
| 互斥锁(mutex) | ||
| 信号量(semaphore) | ||
| 读写锁(rwlock) | ||
| RCU | ||
| 原子操作(atomic_t) | ||
| 完成量(completion) |
典型代码示例:
// 原子操作atomic_t counter = ATOMIC_INIT(0);atomic_inc(&counter);int val = atomic_read(&counter);// 自旋锁(中断上下文安全)spinlock_t lock;unsigned long flags;spin_lock_irqsave(&lock, flags);// 临界区spin_unlock_irqrestore(&lock, flags);// 互斥锁(进程上下文)struct mutex mtx;mutex_init(&mtx);mutex_lock(&mtx);// 临界区(可以睡眠)mutex_unlock(&mtx);
追问考点: 自旋锁在单核 CPU 上会怎样?—— 单核上 spin_lock 退化为抢占禁用,不会死循环等待。
问题: 驱动中如何实现 mmap,让用户空间直接操作硬件寄存器?
回答:
mmap 的作用: 将内核空间的物理地址映射到用户空间,用户程序可以直接读写硬件寄存器,避免系统调用开销。
staticintmy_mmap(structfile *filp, struct vm_area_struct *vma){struct my_dev *dev = filp->private_data;unsigned long pfn = __pa(dev->reg_base) >> PAGE_SHIFT;// 将物理地址映射到用户空间if (remap_pfn_range(vma, vma->vm_start, pfn,vma->vm_end - vma->vm_start,vma->vm_page_prot))return -EAGAIN;return 0;}static const struct file_operations my_fops = {.mmap = my_mmap,// ...};
用户空间使用:
int fd = open("/dev/my_device", O_RDWR);void *map = mmap(NULL, 0x1000,PROT_READ | PROT_WRITE,MAP_SHARED, fd, 0);// 直接读写寄存器volatile uint32_t *reg = (volatile uint32_t *)map;*(reg + 1) = 0x1234; // 写寄存器uint32_t val = *reg; // 读寄存器munmap(map, 0x1000);close(fd);
注意事项:
pgprot_noncached) | |
追问考点:
remap_pfn_range和io_remap_pfn_range的区别?—— 后者用于映射 I/O 内存,保证禁用 CPU 缓存(适合寄存器)。
问题: 设备树源文件(DTS)的语法是怎样的?如何调试设备树问题?
回答:
DTS 基本语法:
/dts-v1/;#include<dt-bindings/gpio/gpio.h>#include<dt-bindings/interrupt-controller/irq.h>/ {model = "MyBoard";compatible = "my-company,myboard";chosen {stdout-path = &uart2;};memory@80000000 {device_type = "memory";reg = <0x80000000 0x20000000>; // 512MB};leds {compatible = "gpio-leds";led0: led@0 {label = "user-led";gpios = <&gpio0 15 GPIO_ACTIVE_HIGH>;linux,default-trigger = "heartbeat";};};&i2c1 {status = "okay";clock-frequency = <400000>;temp_sensor: sensor@48 {compatible = "ti,tmp102";reg = <0x48>;};};};
调试设备树的常用方法:
# 查看设备树是否被正确解析ls /proc/device-tree/ls /proc/device-tree/leds/# 查看特定节点的属性cat /proc/device-tree/leds/led@0/labelcat /proc/device-tree/leds/led@0/gpios | xxd# 编译设备树dtc -I dts -O dtb -o myboard.dtb myboard.dts# 反编译设备树(看实际加载的设备树)dtc -I dtb -O dts -o decompiled.dts /sys/firmware/fdt# 检查设备树语法dtc -I dts -O dtb myboard.dts 2>&1
追问考点:
status = "okay"和status = "disabled"的作用?—— 控制节点是否启用,常用于在 dtsi 中定义外设,在具体板级 dts 中启用。
问题: 驱动开发中遇到问题,你有哪些调试手段?
回答:
调试手段金字塔:
▲/ \ ① printk(最基础)/ \ ② dev_dbg / dev_err/ \ ③ /sys 和 /proc 文件系统/ \ ④ ftrace 内核跟踪/ \ ⑤ kprobe / kretprobe/ \ ⑥ KGDB 内核调试器/ \ ⑦ 逻辑分析仪/示波器(硬件)
① printk 调试:
printk(KERN_EMERG "level 0\n"); // 紧急,一定会输出printk(KERN_ALERT "level 1\n");printk(KERN_CRIT "level 2\n");printk(KERN_ERR "level 3\n"); // 常用printk(KERN_WARNING "level 4\n");printk(KERN_NOTICE "level 5\n");printk(KERN_INFO "level 6\n"); // 常用printk(KERN_DEBUG "level 7\n"); // 需要开启 DEBUG// 推荐使用 dev_xxx 系列(自动打印设备名)dev_err(&pdev->dev, "probe failed: %d\n", ret);dev_info(&pdev->dev, "device initialized\n");dev_dbg(&pdev->dev, "register value: 0x%08x\n", val);
② /sys 文件系统调试:
// 在 sysfs 中创建调试节点static DEVICE_ATTR(debug_reg, S_IRUGO | S_IWUSR,debug_reg_show, debug_reg_store);// 用户空间查看/设置echo 0x1000 > /sys/devices/.../debug_regcat /sys/devices/.../debug_reg
③ ftrace 跟踪:
# 跟踪函数调用echo function > /sys/kernel/debug/tracing/current_tracerecho my_driver_function > /sys/kernel/debug/tracing/set_ftrace_filtercat /sys/kernel/debug/tracing/trace
④ 逻辑分析仪:
对于 I2C/SPI/UART 等协议问题,逻辑分析仪是最直接的调试手段,一眼看出时序问题。
面试加分: 提到自己用过 JTAG + OpenOCD + GDB 调试内核,或者用 perf 做性能分析,会非常加分。
这 15 道题覆盖了驱动开发面试的 核心知识体系:
┌─────────────────────────────────────────┐│ 嵌入式Linux驱动面试 │├──────────────────┬──────────────────────┤│ 基础功 │ 进阶 │├──────────────────┼──────────────────────┤│ ① 字符设备注册 │ ⑨ I2C/SPI总线模型 ││ ② file_operations │ ⑩ GPIO中断映射 ││ ③ 中断上下半部 │ ⑪ Oops调试 ││ ④ 自旋锁vs信号量 │ ⑫ 竞态与同步 ││ ⑤ 设备树匹配 │ ⑬ mmap实现 ││ ⑥ 内存申请 │ ⑭ 设备树调试 ││ ⑦ 内核/用户交互 │ ⑮ 调试手段 ││ ⑧ platform驱动 │ │└──────────────────┴──────────────────────┘
面试建议:
end
一口Linux
关注,回复【1024】海量Linux资料赠送
精彩文章合集
文章推荐