嵌入式驱动岗的秋招面试,和普通软件开发岗有一个明显区别:面试官很少只问“你熟悉Linux吗”,而是习惯用一串连续问题把整个驱动开发链路串起来。很多候选人简历里写着“熟悉字符设备驱动、掌握设备树、能写platform驱动”,但十个问题下来,是从“注册接口”讲到“probe调用顺序”,再从“中断下半部”问到“poll机制和自旋锁”,哪一层理解不牢,都会立刻暴露。
这篇文章以“27届秋招嵌入式驱动岗面试十连问”为线索,逐题拆解答题思路、底层原理、代码示例和追问方向。适合正在准备秋招驱动岗的学生,也适合想系统梳理Linux驱动知识树的初级工程师。学完后,你不只会背接口名,还能把“模块加载、设备号分配、设备树匹配、中断处理、锁选择、阻塞IO、内核调试”这条完整链路讲清楚。
1. 先看全景:驱动岗面试十连问为什么这么问
1.1 一套有代表性的驱动岗十连问
下面这十个问题是驱动岗面试中最常见的组合,顺序基本按照驱动从“加载”到“工作”再到“调试”的生命周期排列。每个问题都能继续往下追问两三层,所以不要指望背答案就能过关。
| 序号 | 面试问题 | 直接考察点 | 再追问方向 |
|---|---|---|---|
| 1 | Linux内核模块加载和卸载流程是什么 | module_init与module_exit机制 | initcall段如何排列,模块参数如何传递 |
| 2 | 字符设备驱动应该用哪个注册接口 | register_chrdev与cdev体系 | 设备节点谁创建,mknod和devtmpfs关系 |
| 3 | 设备号如何分配,主设备号和次设备号有什么作用 | alloc_chrdev_region与register_chrdev_region | 设备号冲突如何解决 |
| 4 | 设备树节点如何描述设备,compatible是什么意思 | DTS语法与of_match_table | reg、interrupts属性如何解析 |
| 5 | platform总线如何把设备和驱动匹配上 | platform_match匹配顺序 | 没有设备树时怎么匹配 |
| 6 | probe函数里通常要做什么 | 资源获取、ioremap、中断注册 | 失败回滚和devm_*帮助函数 |
| 7 | 中断处理函数为什么分上下半部 | tasklet、工作队列、中断线程化 | 中断上下文为什么不能睡眠 |
| 8 | 自旋锁和mutex怎么选 | 锁的睡眠行为、临界区长度 | 中断里能不能用mutex |
| 9 | 驱动如何支持阻塞读和poll | wait_queue与poll回调 | 用户层select/epoll在内核里对应谁 |
| 10 | 驱动出问题后怎么调试 | printk、oops、动态调试 | 看到oops后第一步做什么 |
1.2 面试官真正在评估什么
这十问表面上是考语法和API,实际上评估的是三件事。
第一是“链路感”。字符设备不是孤立概念,它和设备号、设备节点、class_create、probe、file_operations都是一条链路。候选人如果能从“驱动加载”讲到“用户open设备”再讲到“驱动read回调”,说明知识是成网的。第二是“取舍能力”。例如register_chrdev和cdev接口都能注册字符设备,但为什么现代驱动推荐cdev,这需要理解历史和演进,而不是只记结论。第三是“工程意识”。比如probe失败要不要清理、中断程里能不能睡眠、锁的粒度如何控制,这些都是实际项目里最容易出问题的地方。
所以准备面试时,不要只刷“驱动八股”,要把十问串成一个完整故事:一个设备从设备树描述,到platform驱动匹配,到probe注册,到中断和并发处理,再到用户态通过文件接口访问。能讲通这个故事,十连问自然变得连贯起来。
2. 第1问到第3问:模块、字符设备与设备号的底层链路
2.1 第一问:module_init之后发生了什么
这个问题看起来简单,但丢分的关键在于把“模块加载”讲成了一个黑盒。
模块的基本写法是:
#include <linux/init.h> #include <linux/module.h> static int __init my_driver_init(void) { printk(KERN_INFO "my_driver: loaded\n"); return 0; } static void __exit my_driver_exit(void) { printk(KERN_INFO "my_driver: unloaded\n"); } module_init(my_driver_init); module_exit(my_driver_exit); MODULE_LICENSE("GPL");用insmod加载时,内核会先做依赖检查和符号解析,然后执行模块的init函数;用rmmod卸载时执行exit函数。这里要补一句:如果驱动被编译进内核而非模块,module_init会展开为initcall,由内核启动阶段的do_initcalls统一调用,而不是“加载瞬间调用”。
回答的重点在于说明“__init和module_init到底做了什么”。module_init将函数指针放到特定的initcall段中,链接脚本对这个段进行排序;加载模块时通过系统调用进入内核,由内核完成段装载后调用对应函数。面试官听到这一层,基本能判断你不是只写过hello world。
常见坑:遗漏MODULE_LICENSE导致内核标记为污染内核;没有static限制函数作用域造成全局符号冲突;或者只写init不写exit,导致模块无法卸载。
2.2 第二问:register_chrdev和cdev接口怎么选
第二问直指新旧接口的差异。老接口是这样:
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);这个接口传入主设备号、设备和fops,内核自动注册一个0到255范围的次设备号区间。缺点是无法控制次设备号个数,也无法注册多个用途不同的fops子设备,内部实现相当于创建一个主设备号覆盖全部次设备号范围。
现代推荐写法是cdev思路:
static dev_t dev_num; static struct cdev my_cdev; static const struct file_operations my_fops = { .owner = THIS_MODULE, .open = my_open, .read = my_read, .write = my_write, .release = my_release, }; static int __init my_driver_init(void) { alloc_chrdev_region(&dev_num, 0, 1, "my_device"); cdev_init(&my_cdev, &my_fops); my_cdev.owner = THIS_MODULE; cdev_add(&my_cdev, dev_num, 1); return 0; }回答时要说清楚:register_chrdev是历史接口,它内部也会走cdev机制,但把设备号范围和file_operations绑定得很粗糙;cdev接口把“设备号注册”和“字符设备对象管理”分开,更适合控制次设备号范围和多个设备文件。面试中可以补充一句:真正让用户态看到设备文件,还需要mknod或依赖udev/devtmpfs在/dev下创建节点。
2.3 第三问:主设备号和次设备号为什么要分开
设备号在Linux中是一个dev_t类型,高12位是主设备号,低20位是次设备号。主设备号通常表示设备种类或对应的驱动,次设备号表示同一类驱动管理下的第几个设备。
#include <linux/kdev_t.h> #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma, mi) (((ma) << MINORBITS) | (mi))分配设备号有两种方式:已知主设备号用register_chrdev_region,动态分配用alloc_chrdev_region。
| 分配方式 | 使用条件 | 优点 | 风险 |
|---|---|---|---|
| register_chrdev_region | 有明确主设备号 | 地址固定,适合已知编号 | 主设备号可能被占用,易冲突 |
| alloc_chrdev_region | 不关心主设备号 | 由内核分配,冲突少 | 主设备号不确定,需要动态创建节点 |
2.4 这一段的工程坑
第一个坑是只注册设备号,不创建设备节点,然后用户态open总是报No such file or directory。解决方式是在驱动里用class_create和device_create自动创建设备,或者依赖udev规则。第二个坑是设备号冲突,驱动加载返回-EBUSY。动态分配能避免这种问题,但需要动态节点配合。第三坑是cdev_add成功后忘记保存dev_t,卸载时无法正确unregister设备,导致模块卸载后设备节点仍指向不存在的驱动。
3. 第4问到第6问:设备树、platform总线和probe从哪里开始
3.1 第四问:设备树里的compatible为什么是匹配钥匙
设备树用文本描述硬件信息,编译成dtb后由bootloader传给内核。一个GPIO按键可能像下面这样:
/ { gpio_keys { compatible = "gpio-keys"; pinctrl-names = "default"; pinctrl-0 = <&key_pinctrl>; key_enter { label = "enter"; linux,code = <KEY_ENTER>; gpios = <&gpio1 12 GPIO_ACTIVE_LOW>; }; }; };compatible是设备和驱动之间“约定接口”的字符串,一般格式是“厂商,型号”。驱动侧通过of_match_table声明自己支持哪些compatible:
static const struct of_device_id my_of_match[] = { { .compatible = "vendor,my-device" }, { } }; MODULE_DEVICE_TABLE(of, my_of_match); static struct platform_driver my_platform_driver = { .probe = my_probe, .remove = my_remove, .driver = { .name = "my_device", .of_match_table = my_of_match, }, };设备树中compatible的值必须和of_match_table中的字符串完全一致,否则不会触发probe。这是驱动开发里高频排错点:设备树编译通过、驱动加载成功,但probe不执行,先查compatible和of_match_table是否匹配。
3.2 第五问:platform总线匹配顺序是怎样的
platform总线是Linux内核对“挂载在简单总线上的设备”抽象出的虚拟总线。设备端可以是设备树节点,也可以是静态注册的platform_device。
匹配时,platform_match会按以下顺序判断:
- 设备树匹配:用设备节点的compatible字段与driver的of_match_table比较。
- ACPI匹配:在ACPI平台上有对应匹配表。
- id_table匹配:用platform_driver中的id_table里的name字段和设备name比较。
- 最后尝试driver.driver.name和设备name比较。
面试时只要把“设备树匹配优先、id_table兜底”讲清楚即可。这里常见坑是:写驱动时只设置了driver.name,没有设置of_match_table,结果设备树找到驱动后仍然无法匹配probe。
3.3 第六问:probe函数里到底做什么
probe是驱动真正开始“拥有设备”的函数。典型顺序如下:
static int my_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int irq; int ret; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENODEV; base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(base)) return PTR_ERR(base); irq = platform_get_irq(pdev, 0); if (irq < 0) return irq; ret = devm_request_irq(&pdev->dev, irq, my_irq_handler, 0, "my_device", base); if (ret) return ret; return 0; }关键不是列API,而是说明两点。第一,probe里要处理“失败回滚”,前面资源申请成功但后面失败时,要把已获得的资源全部释放。devm_*系列帮助函数能自动完成释放,减少忘记释放的泄漏。第二,中断请求一般放在probe中完成,因为此时设备资源已经就绪。回答时如果能提到“用devm_ioremap_resource可以同时完成request_mem_region和ioremap”,会给面试官留下经验印象。
3.4 这一段的工程坑
设备树改完不生效是最常见的一类问题。很多情况下改了dts,但忘记重新编译dtb,或bootloader加载的还是旧dtb。还有一个坑是compatible写错大小写或厂商前缀,导致silent匹配失败。另一个是platform_get_irq返回0和负数的问题:新版内核中获取失败返回负的错误码,返回0也可能是合法中断号,判断时建议用“< 0”判断错误。
4. 第7问到第8问:中断处理、下半部与并发控制
4.1 第七问:为什么中断要分上下半部,哪种下半部合适
中断处理函数的执行上下文很特殊:它会打断当前进程,在原子上下文中运行,不能调用可能睡眠的函数。如果中断处理时间太长,系统会丢中断,甚至无法及时响应其他重要中断。所以Linux把中断工作分成上半部和下半部。
上半部由request_irq注册的处理函数执行,主要负责快速确认中断、记录状态、唤醒下半部。耗时操作推迟到下半部执行。面试中要把主流机制对比清楚:
| 下半部机制 | 运行上下文 | 是否可睡眠 | 适用场景 |
|---|---|---|---|
| softirq | 软中断上下文 | 否 | 内核高频率网络收发 |
| tasklet | 软中断上下文 | 否 | 简单、执行时间短的中断后续处理 |
| workqueue | 进程上下文 | 是 | 可能睡眠的耗时工作 |
| threaded irq | 内核线程上下文 | 是 | 中断处理本身需要等待、读写慢设备 |
回答时不要只背定义,要给一个选择逻辑:临界代码短且不需要睡眠,优先tasklet;需要访问I2C、SPI这类可能调度的总线,必须用workqueue或者threaded irq。再补充一句“request_threaded_irq可以注册threaded irq,把整个处理函数放到内核线程里执行”。
// 线程化中断示例 static irqreturn_t my_threaded_handler(int irq, void *data) { // 这里可以调用可能睡眠的API return IRQ_HANDLED; } ret = request_threaded_irq(irq, NULL, my_threaded_handler, IRQF_TRIGGER_RISING, "my_device", dev_data);4.2 第八问:自旋锁和mutex为什么不能乱用
锁的选择和上下文强相关。自旋锁在抢不到锁时会原地自旋,同时关闭抢占,等价于“忙等”;mutex在抢不到锁时会把当前任务放到等待队列,调度其他任务运行,等价于“睡眠等待”。
因为mutex会睡眠,它只能在可以睡眠的进程上下文中使用。中断处理函数、软中断上下文、硬中断上下文都不能用mutex。自旋锁可以用于中断上下文,但要注意使用spin_lock_irqsave保存和恢复中断状态,避免在持锁时被同CPU上的中断打断,造成死锁。
| 场景 | 合适选择 | 原因 |
|---|---|---|
| 中断上下文共享数据 | 自旋锁 + irqsave | 不能睡眠,还要屏蔽本CPU中断 |
| 进程上下文短临界区 | 自旋锁 | 自旋时间短,睡眠成本更高 |
| 进程上下文长临界区 | mutex | 睡眠等待更省CPU,允许调度 |
| 只保护单个计数 | atomic_t或原子位操作 | 开销最小,避免锁 |
面试中容易丢分的是说不清“自旋锁持有时间为什么不能太长”。因为自旋锁等待本身不释放CPU,如果临界区太长,其他核上的任务只能干等,严重降低系统并发度;如果临界区里又发生调度或睡眠,后果更严重,可能直接触发内核警告。
还有一个经典追问:“同一个设备的中断函数和主线程都访问一个变量,怎么保护?”正确思路是分析访问上下文:中断上下文和进程上下文共享数据,要使用spin_lock_irqsave/spin_unlock_irqrestore,而不是普通spin_lock,否则进程在持锁时被中断打断,中断函数再抢同一把锁,就会死锁。
4.3 这一段的工程坑
中断函数里调用printk过多会导致系统卡顿,原因是串口输出非常慢,影响中断响应。更多典型问题包括:申请中断时flags设错了触发方式,设备是低电平触发但写成了上升沿触发;中断处理函数没有及时清除中断状态寄存器,导致中断风暴不断触发;以及使用workqueue时没有注意取消排队的工作,模块卸载时崩溃。这些坑在面试中可以作为“实际项目遇到过”的素材,但前提是要能解释原因。
5. 第9问到第10问:阻塞IO、poll回调与内核调试入口
5.1 第九问:驱动如何支持阻塞读
用户态的read在数据未准备好时会被阻塞,对应驱动里就是read回调让当前进程睡眠。内核用等待队列实现这个机制。
static wait_queue_head_t my_wq; static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off) { int ret; /* 等待数据就绪 */ ret = wait_event_interruptible(my_wq, data_ready); if (ret) return -ERESTARTSYS; /* 数据就绪后拷贝到用户空间 */ if (copy_to_user(buf, &my_data, sizeof(my_data))) return -EFAULT; data_ready = 0; return sizeof(my_data); }中断或者工作队列产生数据后,调用wake_up_interruptible(&my_wq)唤醒read进程。回答这一题时,要能画出调用链:用户read -> sys_read -> vfs_read -> 驱动file_operations.read -> 睡眠等待,数据到达 -> 唤醒 -> 拷贝数据 -> 返回用户态。
poll机制是阻塞读的扩展。用户态select/poll/epoll会调用驱动file_operations.poll回调,回调里调用poll_wait把当前等待队列关联到事件等待机制,并返回当前设备是否可读可写的掩码。
static unsigned int my_poll(struct file *filp, poll_table *wait) { unsigned int mask = 0; poll_wait(filp, &my_wq, wait); if (data_ready) mask |= POLLIN | POLLRDNORM; return mask; }面试时能说出“epoll底层也是通过poll回调把文件事件挂到epoll的监控链上”,基本就证明你理解IO多路复用的内核侧实现,而不是只会用用户态API。
5.2 第十问:驱动出问题后从哪里开始调试
驱动调试的第一个入口永远是内核日志。先执行dmesg或查看串口输出,重点看有没有oops、warning、call trace。初学者最容易犯的错误是直接改代码重编译,不先分析日志。
排查oops的顺序可以固定下来:
- 记录问题现象和复现条件。
- 保存完整dmesg,尤其是oops段和call trace。
- 从oops中提取PC指针、函数名和寄存器。
- 用addr2line或gdb将PC地址换算到源码行号。
- 检查访问的地址、锁、中断是否异常。
- 用二分定位:先屏蔽最近改动,再看是否复现。
常用调试手段如下表:
| 手段 | 场景 | 关键命令或接口 |
|---|---|---|
| printk | 快速确认执行路径 | dmesg、cat /proc/kmsg |
| dev_info/dev_dbg | 驱动开发时打印 | 需要开启DEBUG宏 |
| /proc和/sys | 查看运行状态 | cat /proc/devices、/proc/interrupts |
| debugfs | 驱动内部节点调试 | mount -t debugfs none /sys/kernel/debug |
| ftrace | 追踪函数调用 | trace-cmd record、trace-cmd report |
| oops解析 | panic和崩溃定位 | addr2line -e vmlinux 地址 |
| gdb | 源码级调试内核 | kgdb / qemu + gdb |
这些工具不要求全部熟练,但至少要有一条“printk -> dmesg -> 定位路径 -> 确认根因”的完整调试思路。面试官更希望看到候选人能描述一次真实的排查过程,而不是只会列工具名字。
6. 从“会答”到“会做”:学习环境与生产环境的差距
6.1 面试中怎么体现“真做过的样子”
很多同学面试时说得最多的词是“会”“熟悉”,但一追问细节就没有了。区分“看过教程”和“真做过项目”的关键信号有三个:能否画出调用链、能否主动说失败处理、能否说出自己踩过的坑。
例如回答字符设备注册时,主动补一句“cdev_add之后还要考虑设备节点如何生成,否则用户态打不开设备文件”。回答中断申请时,主动说“devm_request_irq失败时会自动释放资源,但非devm接口要在remove里手动free”。这些细节不是八股,而是真实编码时才会注意到的事情。
6.2 学习环境怎么验证这些知识点
如果没有开发板,也可以用qemu模拟ARM或x86环境。学习阶段可以这样分配任务:
- 用qemu启动一个带设备树的最小内核,练习模块编译和加载。
- 在内核源码drivers/char下写一个简单字符驱动,注册设备节点,用echo/cat测试读写。
- 在qemu的virtio设备或者虚拟平台设备上,写platform驱动,观察probe是否执行。
- 在设备树里增加新节点,验证compatible匹配流程。
- 用gpio模拟按键中断,运行时查看/proc/interrupts确认中断是否注册成功。
这套练习不需要买很贵的硬件,但能覆盖十连问中的大部分知识点。关键是要动手编译内核,而不是只看源码。编译一次内核,你才能理解模块编译、设备树编译、内核启动日志这些最基础又最容易被问倒的环节。
6.3 生产环境还要额外做什么
学习环境里modprobe加载失败可以随便reboot,生产环境却不能这样。真实项目里驱动要额外考虑:
- 开机自动加载:把模块放到/lib/modules/$(uname -r)/,并配置modprobe配置文件或initramfs。
- 卸载安全性:remove回调要取消工作队列、释放中断、注销设备,顺序不能乱。
- 设备树兼容性:compatible要稳定,不要随意更改,否则会影响旧设备升级。
- 多设备支持:不能把设备数据都放在全局变量里,要用设备的私有数据指针挂到struct device上。
- 日志和监控:生产环境日志可能非常多,要用dev_dbg和动态调试而不是大量printk。
- 固件和DTS版本管理:设备树和驱动版本要一起发布,否则现场设备会出现“驱动加载成功但不工作”的怪问题。
这些点只要在面试里自然提到,就能明显拉开和“只写demo”候选人的差距。
6.4 一个可复用的驱动面试自检清单
| 检查项 | 自问 | 过关标准 |
|---|---|---|
| 模块加载 | module_init和initcall关系能讲清吗 | 能说出编译进内核和动态加载的差异 |
| 字符设备 | cdev注册流程完整吗 | 能说出device_create创建节点的作用 |
| 设备号 | 主次设备号、动态分配 | 能解释设备号与/dev节点关系 |
| 设备树 | compatible匹配链路 | 能画出设备树节点到of_match_table的匹配流程 |
| platform | 匹配优先级清楚吗 | 能说出设备树优先、id_table兜底 |
| probe | 失败回滚思路 | 能说出devm_*的自动释放优势 |
| 中断 | 上下半部选择 | 能分析中断上下文与睡眠冲突 |
| 并发 | 锁选择依据 | 能区分自旋锁和mutex的上下文约束 |
| 阻塞IO | wait_queue和poll | 能画出用户select到驱动poll的链路 |
| 调试 | 有固定排查顺序 | 看到oops知道下一步做什么 |
7. 十连问之外的常见丢分点与追问应对
7.1 丢分点一:API记忆混乱,说不清参数
典型表现是cdev_add、cdev_init、cdev_del顺序不清楚,或者alloc_chrdev_region和register_chrdev_region混用。这类问题不是靠死记,而是靠理解生命周期:先申请设备号,再初始化cdev,再把cdev加到内核,卸载时逆序注销。记住“申请、注册、使用、注销”的顺序,比背参数更有效。
7.2 丢分点二:中断和锁的关系讲不透彻
很多人知道“中断里不能用mutex”,但说不清原因。问题根源在于“原子上下文能否睡眠”。中断处理打断的是另一段逻辑,如果当前持锁线程被打断,而中断处理里又去抢锁,就会形成“死锁闭环”。回答时补一句“自旋锁要配合local_irq_save使用,防止本CPU中断打断持锁代码”,这一问就过关了。
7.3 丢分点三:设备树匹配流程混乱
有同学把platform_match直接理解为“名字相同就匹配”,没有意识到设备树设备主要靠compatible匹配。丢分原因是把驱动模型和打开设备文件的流程混在一起了。建议画清楚两条链:设备树节点被内核解析成platform_device,驱动注册为platform_driver,之后platform总线匹配;匹配成功后调用probe。这是硬件检测驱动,和用户open设备节点是完全两回事。
7.4 丢分点四:被追问时当场乱了阵脚
面试十连问不是为了难住人,而是看候选人在压力下如何组织知识。遇到不会的问题,不要直接说“我不知道”,可以这样回答:“这个问题我还没有深入验证过,但根据我对xxx机制的理解,它可能是……”然后给出推理路径。这种方式至少能展示思考能力。也可以退一步说:“我目前只确定到讨论xxx为止,再往下的细节我需要查内核源码确认。”坦诚且有限定性的回答,比乱编更安全。
7.5 换一版更稳的开场回答
如果面试官说“先从Linux驱动讲起”,不要直接冒出register_chrdev。更好的开法是:“我比较习惯按设备生命周期来理解驱动:设备在设备树或总线上被描述,内核为它创建device,驱动注册driver后由总线匹配,匹配成功调用probe,probe申请资源并注册中断,用户态通过file_operations访问设备。char driver是其中最常用的一种类型。”这段开场能把十连问需要的所有线索都埋进去。
8. 27届秋招准备路线:别把时间全部花在背题上
8.1 时间安排主次
距离秋招时间越紧张,越要把时间花在“一整套驱动链路”上,而不是零散API。建议按以下顺序推进:
- 第一阶段:掌握内核模块和字符设备驱动,跑通insmod/rmmod、mknod、echo/read。
- 第二阶段:理解platform总线、设备树匹配,学会定位probe不执行的问题。
- 第三阶段:加上中断、等待队列、锁,编写一个完整按键中断驱动。
- 第四阶段:掌握printk、dmesg、oops定位、动态调试。
- 第五阶段:阅读一个真实驱动源码,比如drivers/input/keyboard/gpio_keys.c或drivers/misc/eeprom/at24.c。
这套路线对应十连问的覆盖顺序。每个阶段都要有代码产出,不能只做笔记。
8.2 最能拉差距的三个练习项目
第一,GPIO按键中断驱动。要求使用设备树描述按键,platform_driver匹配,注册中断,处理去抖,用等待队列实现按键事件读取,并用poll支持select。这一个项目就覆盖第4问到第9问。
第二,虚拟字符设备计数器。要求动态分配设备号,使用cdev注册,实现read/write/ioctl,通过device_create自动生成节点,并正确处理copy_to_user和copy_from_user。这个项目覆盖第2问到第3问。
第三,在qemu里给虚拟平台增加一个简单外设节点,实现驱动读取寄存器、注册中断并使用threaded irq。这个项目覆盖设备树、ioremap、中断下半部,还能练习oops调试。
8.3 读源码的建议
读源码不要从内核最复杂的地方开始。可以先读一个最简字符驱动的历史代码,再读drivers/char/mem.c这类老驱动,最后读一个带有设备树匹配和中断的真实驱动。读的时候带着问题走:这个驱动谁注册的、谁匹配的、probe做了什么、数据从设备到用户态的路径是什么。把这个路径画出来,比记住源码行数有用得多。
8.4 秋招最该守住的一条底线
驱动岗面试可以允许你某条API不记得,但不允许你把“调用链”和“上下文约束”搞错。宁可少背十道八股,也要能完整讲清“设备树片段 -> platform_probe -> 中断处理 -> 等待队列唤醒 -> 用户read返回”这条主线。这条主线一旦立住,十连问就只是在这条主线上加分支。
准备过程中保持一个习惯:每做一个小实验,都记录下来现象、结论和踩坑过程。这些记录在面试中比“我熟悉Linux驱动”更有说服力,也是后续进入工程岗位后最宝贵的第一手经验。