ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

嵌入式Linux驱动开发实战:从字符设备到设备树与并发控制

嵌入式Linux驱动开发实战:从字符设备到设备树与并发控制 1. 嵌入式驱动开发到底在做什么刚入行那会儿我对“驱动开发”这四个字的理解特别朴素——不就是写代码让硬件动起来吗后来在项目里摸爬滚打了几年踩过一堆坑之后才明白驱动开发真正做的事情是在硬件寄存器和操作系统内核之间架一座桥。这座桥要足够稳稳到应用程序调用open、read、write的时候完全感觉不到底层硬件的存在同时又要足够灵活灵活到能适配不同厂商、不同型号的芯片和外设。嵌入式驱动开发的核心关键词就两个嵌入式和驱动开发。前者决定了你的代码跑在资源受限的环境里内存可能只有几十兆CPU 主频可能不到 1GHz甚至没有 MMU后者决定了你必须跟寄存器、中断、DMA、时钟树这些底层玩意儿打交道。两者叠加在一起就注定了这个方向不是纯写业务逻辑那么轻松。这篇文章适合谁看如果你正在学嵌入式 Linux想从“会点灯”进阶到“能写一个完整的字符设备驱动”如果你在面试中被问过“platform 总线和 I2C 总线的区别”却答得磕磕巴巴如果你手里有一块开发板想跑通从设备树到驱动 probe 的完整链路——那这篇内容就是写给你的。我会从整体设计思路讲到具体实操把踩过的坑和总结出来的经验都摊开来说。2. 驱动开发的整体设计思路与架构选型2.1 为什么驱动要分层从“一坨代码”到“可维护架构”我见过不少初学者写驱动习惯把所有逻辑塞进一个.c文件里寄存器操作、中断处理、文件操作接口、硬件初始化全混在一起。这种写法在单一设备上确实能跑但一旦项目里要支持多个相似设备或者硬件版本迭代了代码就会变成一坨谁都不敢动的“屎山”。Linux 内核给出的答案是分层与分离。以字符设备为例内核把驱动拆成了几个层次最上层是文件操作接口file_operations负责和用户空间交互中间是设备模型device、driver、bus负责设备与驱动的匹配和管理最下层是硬件操作直接读写寄存器或调用子系统 API。这样分层的好处很直接换硬件时只需要改最下层的硬件操作部分上层接口和用户空间程序完全不用动。我做过一个项目同一套应用代码要跑在三个不同厂商的触摸屏上正是因为驱动层做了分离应用层一行代码都没改。2.2 总线模型的选择platform、I2C、SPI 还是 USB选总线模型是驱动开发里第一个关键决策。很多新手会问我的设备到底该挂在哪条总线上这里给一个实用的判断逻辑。platform 总线适合那些“直接映射到内存地址”或者“没有物理总线可枚举”的设备。比如 SoC 内部的 GPIO 控制器、时钟控制器、DMA 控制器这些设备在芯片内部通过 AHB/AXI 总线连接不需要热插拔地址固定。用 platform 总线的好处是设备信息可以通过设备树描述驱动和设备的匹配由内核自动完成。I2C 和 SPI适合外挂的传感器、EEPROM、显示屏等。I2C 两根线能挂多个设备适合低速、低引脚数的场景SPI 速度快、全双工适合需要高吞吐的外设比如 TFT 屏、高速 ADC。选哪个主要看外设本身支持什么接口以及你的引脚资源够不够。USB则适合需要热插拔、标准化的设备。但 USB 驱动开发的复杂度明显更高涉及枚举、端点、URB 等概念新手不建议一上来就啃。我个人的建议是先从 platform 驱动入手因为它最能帮你理解设备树、probe 机制、资源获取这些核心概念。等 platform 驱动写熟了再去看 I2C/SPI 驱动你会发现它们只是在 platform 的基础上多了一层总线通信的封装。2.3 设备树驱动和硬件的“合同”设备树Device Tree是嵌入式 Linux 驱动开发绕不开的东西。你可以把它理解成驱动和硬件之间的一份“合同”硬件工程师在设备树里描述“我有什么设备、地址是多少、中断号是多少、时钟怎么接”驱动工程师在代码里说“我要什么资源、怎么操作”。为什么要有设备树因为 ARM 架构不像 x86 有 ACPI 和 BIOS 来枚举硬件。在设备树出现之前每个板子的硬件信息都硬编码在内核的board-xxx.c文件里导致内核里堆满了各种板级代码。设备树把这些信息从内核代码里抽出来变成独立的.dts文件编译成.dtb后由 bootloader 传给内核。写驱动时你需要关注设备树里的几个关键属性compatible用于匹配驱动reg描述寄存器地址范围interrupts描述中断信息clocks和pinctrl描述时钟和引脚配置。这些属性在驱动里通过of_系列函数读取比如of_get_named_gpio、irq_of_parse_and_map。注意设备树里的compatible字符串必须和驱动里的of_device_id表完全一致包括大小写和连字符。我见过有人因为把fsl,imx6q-uart写成了fsl,imx6q_uart调试了一整天才发现匹配不上。3. 核心细节解析与实操要点3.1 字符设备驱动的骨架从 module_init 到 file_operations字符设备驱动是最基础也是最常见的驱动类型。它的核心结构可以用一句话概括注册一个设备号实现一组文件操作接口把硬件操作封装在这些接口里。先看模块的入口和出口。每个驱动模块都需要module_init和module_exit来告诉内核“我什么时候加载、什么时候卸载”。在入口函数里通常要做三件事申请设备号、初始化硬件、注册字符设备。static int __init my_driver_init(void) { int ret; dev_t devno; /* 1. 申请设备号主设备号设为0表示由内核动态分配 */ ret alloc_chrdev_region(devno, 0, 1, my_device); if (ret 0) { pr_err(alloc_chrdev_region failed\n); return ret; } /* 2. 初始化 cdev 并添加到内核 */ cdev_init(my_cdev, my_fops); my_cdev.owner THIS_MODULE; ret cdev_add(my_cdev, devno, 1); if (ret 0) { unregister_chrdev_region(devno, 1); return ret; } /* 3. 创建设备节点 /dev/my_device */ my_class class_create(THIS_MODULE, my_class); device_create(my_class, NULL, devno, NULL, my_device); return 0; }这里有几个细节值得展开说。alloc_chrdev_region和register_chrdev_region的区别在于前者让内核动态分配主设备号后者需要你指定一个固定的主设备号。动态分配的好处是不会和其他驱动冲突但缺点是每次加载主设备号可能不同所以一定要配合class_create和device_create自动创建设备节点而不是让用户手动mknod。file_operations结构体是驱动和用户空间之间的接口定义。最常用的几个成员是open、release、read、write、unlocked_ioctl。其中unlocked_ioctl是控制接口的主力用来传递命令和参数。注意新内核已经不再使用带 BKL大内核锁的ioctl而是用unlocked_ioctl你需要自己在驱动里处理并发保护。3.2 并发与竞态自旋锁、互斥锁和原子操作怎么选驱动开发里最容易被忽视、也最容易出问题的就是并发控制。用户空间可能有多个进程同时打开同一个设备中断处理函数可能随时打断你的代码内核还支持 SMP 多核并行。如果你的驱动里共享数据没有保护轻则数据错乱重则内核崩溃。Linux 内核提供了几种并发控制机制选哪种取决于你的场景。自旋锁spinlock适合保护临界区很短的场景尤其是在中断上下文里。自旋锁的特点是“忙等待”拿不到锁就原地打转所以临界区里绝对不能睡眠。我一般用自旋锁保护那些只是读写几个寄存器的操作。互斥锁mutex适合临界区可能睡眠的场景比如需要拷贝大量数据到用户空间、需要等待硬件响应。互斥锁拿不到锁时会睡眠让出 CPU 给其他任务。但互斥锁不能在中断上下文里使用因为中断处理函数不能睡眠。原子操作atomic适合简单的计数器、标志位。比如统计中断次数用atomic_inc就够了不需要上锁。完成量completion适合“一个线程等待另一个线程完成某件事”的场景。比如驱动里启动 DMA 传输后等待 DMA 完成中断来唤醒。实操心得我刚开始写驱动时觉得加锁麻烦经常偷懒不加。结果在一次压力测试中两个进程同时读写同一个寄存器导致设备直接挂死。从那以后我养成了一个习惯只要有两个以上的执行路径可能访问同一份数据就先想清楚用什么锁。3.3 中断处理上半部和下半部的分工中断是驱动开发里另一个核心话题。硬件产生中断后CPU 会跳转到中断处理函数。但中断处理函数有一个硬性要求必须尽快返回。因为中断处理期间当前 CPU 上的其他中断可能被屏蔽如果处理时间太长系统响应就会变差。所以 Linux 把中断处理拆成了两部分上半部top half和下半部bottom half。上半部就是中断处理函数本身它只做最紧急的事情比如读取中断状态寄存器、清除中断标志、记录数据到缓冲区。下半部则负责耗时的处理比如解析数据、唤醒等待队列、拷贝数据到用户空间。下半部的实现方式有几种softirq、tasklet、工作队列workqueue。softirq 性能最高但使用最复杂一般驱动开发者用不到tasklet 基于 softirq 实现运行在中断上下文不能睡眠工作队列运行在进程上下文可以睡眠适合需要调用可能睡眠的函数的场景。我一般的选择逻辑是如果下半部只是简单处理数据用 tasklet如果需要调用msleep、mutex_lock或者访问用户空间用工作队列。/* 中断处理函数示例 */ static irqreturn_t my_isr(int irq, void *dev_id) { struct my_dev *dev dev_id; u32 status; /* 读取中断状态 */ status readl(dev-base REG_STATUS); if (!(status IRQ_PENDING)) return IRQ_NONE; /* 清除中断标志 */ writel(status, dev-base REG_STATUS); /* 调度下半部 */ tasklet_schedule(dev-my_tasklet); return IRQ_HANDLED; }注意IRQ_NONE和IRQ_HANDLED的返回值。如果中断不是你的设备产生的必须返回IRQ_NONE否则内核会认为中断处理有问题。共享中断线上尤其要注意这一点。3.4 设备树匹配与 probe 函数的执行流程platform 驱动的核心是probe函数。当内核启动或者驱动模块加载时内核会遍历 platform 总线上的设备和驱动通过compatible属性进行匹配。匹配成功后调用驱动的probe函数。probe函数里通常要做这些事获取设备树中的资源寄存器地址、中断号、GPIO、时钟初始化硬件注册字符设备或其他子系统接口。static int my_probe(struct platform_device *pdev) { struct my_dev *dev; struct resource *res; /* 分配设备私有数据结构 */ dev devm_kzalloc(pdev-dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; /* 获取寄存器地址 */ res platform_get_resource(pdev, IORESOURCE_MEM, 0); dev-base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(dev-base)) return PTR_ERR(dev-base); /* 获取中断号 */ dev-irq platform_get_irq(pdev, 0); if (dev-irq 0) return dev-irq; /* 注册中断处理函数 */ ret devm_request_irq(pdev-dev, dev-irq, my_isr, 0, my_device, dev); if (ret) return ret; platform_set_drvdata(pdev, dev); return 0; }这里大量使用了devm_前缀的函数这是内核的“设备资源管理”机制。用devm_申请的资源会在设备卸载或驱动移除时自动释放省去了手动清理的麻烦也避免了资源泄漏。我强烈建议在probe函数里优先使用devm_系列函数。4. 实操过程与核心环节实现4.1 环境搭建交叉编译工具链和内核源码准备动手写驱动之前环境搭建是第一步。嵌入式开发通常需要交叉编译因为目标板的 CPU 架构和你的开发机不一样。比如你的开发机是 x86_64目标板是 ARM Cortex-A7就需要用arm-linux-gnueabihf-前缀的工具链。工具链的获取方式有几种从芯片厂商的 SDK 里拿用 Buildroot 或 Yocto 自己构建或者用 Linaro 发布的通用工具链。我一般推荐用厂商 SDK 里的工具链因为兼容性最有保障。内核源码的准备同样重要。驱动编译需要内核头文件和配置信息。你需要先获取和目标板运行的内核完全一致的内核源码然后执行make defconfig或使用厂商提供的defconfig文件再执行make modules_prepare来准备编译环境。# 设置交叉编译环境变量 export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- export KERNEL_DIR/path/to/kernel/source # 准备内核模块编译环境 cd $KERNEL_DIR make imx_v6_v7_defconfig make modules_prepare驱动模块的 Makefile 写法也有讲究。一个典型的驱动 Makefile 长这样obj-m my_driver.o my_driver-objs : main.o hardware.o KERNEL_DIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KERNEL_DIR) M$(PWD) modules clean: $(MAKE) -C $(KERNEL_DIR) M$(PWD) cleanobj-m表示编译成可加载模块my_driver-objs指定模块由哪些目标文件组成。编译时用make -C $(KERNEL_DIR) M$(PWD) modules-C切换到内核目录M指定模块源码目录。4.2 一个完整的 GPIO 驱动实例从设备树到用户空间下面用一个完整的 GPIO 驱动例子把前面讲的东西串起来。这个驱动控制一个 LED用户空间可以通过write来点亮或熄灭。先看设备树节点my_led: my-led { compatible mycompany,my-led; led-gpios gpio1 3 GPIO_ACTIVE_HIGH; status okay; };驱动代码的核心部分#include linux/module.h #include linux/platform_device.h #include linux/gpio/consumer.h #include linux/fs.h #include linux/cdev.h #include linux/uaccess.h struct my_led_dev { struct gpio_desc *led_gpio; struct cdev cdev; dev_t devno; struct class *cls; }; static int my_led_open(struct inode *inode, struct file *filp) { struct my_led_dev *dev container_of(inode-i_cdev, struct my_led_dev, cdev); filp-private_data dev; return 0; } static ssize_t my_led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { struct my_led_dev *dev filp-private_data; char kbuf[8]; if (count sizeof(kbuf) - 1) count sizeof(kbuf) - 1; if (copy_from_user(kbuf, buf, count)) return -EFAULT; kbuf[count] \0; if (kbuf[0] 1) gpiod_set_value(dev-led_gpio, 1); else if (kbuf[0] 0) gpiod_set_value(dev-led_gpio, 0); return count; } static const struct file_operations my_led_fops { .owner THIS_MODULE, .open my_led_open, .write my_led_write, }; static int my_led_probe(struct platform_device *pdev) { struct my_led_dev *dev; int ret; dev devm_kzalloc(pdev-dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; /* 从设备树获取 GPIO */ dev-led_gpio devm_gpiod_get(pdev-dev, led, GPIOD_OUT_LOW); if (IS_ERR(dev-led_gpio)) { dev_err(pdev-dev, Failed to get LED GPIO\n); return PTR_ERR(dev-led_gpio); } /* 申请设备号 */ ret alloc_chrdev_region(dev-devno, 0, 1, my_led); if (ret) return ret; cdev_init(dev-cdev, my_led_fops); dev-cdev.owner THIS_MODULE; ret cdev_add(dev-cdev, dev-devno, 1); if (ret) goto err_cdev; dev-cls class_create(THIS_MODULE, my_led_class); if (IS_ERR(dev-cls)) { ret PTR_ERR(dev-cls); goto err_class; } device_create(dev-cls, NULL, dev-devno, NULL, my_led); platform_set_drvdata(pdev, dev); dev_info(pdev-dev, my_led probed successfully\n); return 0; err_class: cdev_del(dev-cdev); err_cdev: unregister_chrdev_region(dev-devno, 1); return ret; } static int my_led_remove(struct platform_device *pdev) { struct my_led_dev *dev platform_get_drvdata(pdev); device_destroy(dev-cls, dev-devno); class_destroy(dev-cls); cdev_del(dev-cdev); unregister_chrdev_region(dev-devno, 1); return 0; } static const struct of_device_id my_led_of_match[] { { .compatible mycompany,my-led }, { } }; MODULE_DEVICE_TABLE(of, my_led_of_match); static struct platform_driver my_led_driver { .probe my_led_probe, .remove my_led_remove, .driver { .name my_led, .of_match_table my_led_of_match, }, }; module_platform_driver(my_led_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple GPIO LED driver);这个例子里有几个关键点。devm_gpiod_get的第二个参数led对应设备树里的led-gpios属性名。GPIOD_OUT_LOW表示初始化为输出低电平。container_of宏用于从cdev指针反推出包含它的设备结构体指针这是内核里非常常用的技巧。用户空间的操作就很简单了echo 1 /dev/my_led # 点亮 LED echo 0 /dev/my_led # 熄灭 LED4.3 调试手段printk、ftrace 和动态调试驱动调试比应用调试难得多因为驱动跑在内核空间不能随便用 gdb 打断点。我常用的调试手段有几种。printk是最基础的通过printk或pr_info、dev_dbg等宏输出日志。日志级别从 0 到 7数字越小优先级越高。dev_dbg默认不会输出需要开启DEBUG宏或者在运行时通过dynamic_debug控制。ftrace是内核自带的跟踪工具可以跟踪函数调用、中断、调度等事件。通过/sys/kernel/debug/tracing/目录下的文件控制。比如想看某个函数的调用栈可以设置function_graph跟踪器。动态调试dynamic debug可以在运行时开启或关闭某条pr_debug语句不需要重新编译内核。通过/sys/kernel/debug/dynamic_debug/control文件控制。# 开启某个文件的动态调试 echo file my_driver.c p /sys/kernel/debug/dynamic_debug/control # 使用 ftrace 跟踪函数调用 echo function_graph /sys/kernel/debug/tracing/current_tracer echo my_probe /sys/kernel/debug/tracing/set_ftrace_filter echo 1 /sys/kernel/debug/tracing/tracing_on踩坑记录有一次驱动 probe 一直失败但printk输出的信息太少看不出问题在哪。后来用devm_ioremap_resource的返回值判断发现是设备树里的reg属性地址范围写错了导致ioremap失败。从那以后我在probe里每个可能失败的步骤后面都加了dev_err输出方便定位问题。5. 常见问题与排查技巧实录5.1 驱动加载失败从 insmod 报错到问题定位insmod或modprobe加载驱动失败是最常见的问题。错误信息通常比较隐晦需要结合dmesg来看。错误现象可能原因排查方法insmod: ERROR: could not insert module: Invalid parameters模块参数不匹配或内核版本不一致检查modinfo输出的 vermagic 是否和当前内核一致insmod: ERROR: could not insert module: Unknown symbol依赖的符号未导出或依赖模块未加载用nm查看符号用modprobe自动加载依赖probe failed with error -517依赖的资源未就绪如时钟、 regulator检查设备树里的依赖关系确认-EPROBE_DEFER的处理probe failed with error -22参数无效通常是设备树属性解析失败检查of_get_*函数的返回值确认设备树属性名和类型-EPROBE_DEFER是新手最容易困惑的错误码。它的意思是“我现在还不能初始化等依赖的资源就绪后再试”。内核会把驱动放到延迟探测队列里稍后重试。如果你在probe里获取时钟、GPIO、regulator 失败应该返回-EPROBE_DEFER而不是直接返回错误否则驱动可能永远无法加载。5.2 设备树匹配不上compatible 属性的那些坑设备树匹配失败是另一个高频问题。现象是驱动加载了但probe函数根本没被调用。排查步骤是这样的首先确认设备树是否被正确编译和加载。在目标板上执行ls /proc/device-tree/看看你的设备节点是否存在。如果不存在说明设备树没有编译进去或者 bootloader 没有传递正确的 dtb。然后确认compatible属性是否匹配。在/proc/device-tree/下找到你的节点用hexdump -C compatible查看属性值。注意设备树里的字符串是以\0结尾的比较时要去掉结尾的空字符。最后确认驱动是否注册到了正确的总线。platform 驱动注册到 platform 总线I2C 驱动注册到 I2C 总线。如果设备树节点在 I2C 控制器下面但驱动注册成了 platform 驱动那肯定匹配不上。经验之谈我习惯在probe函数的第一行加一句dev_info(pdev-dev, probe called\n)。这样只要probe被调用了就能在dmesg里看到。如果没看到说明匹配环节出了问题不用往下查了。5.3 内存泄漏与资源释放devm 机制的正确使用驱动里的内存泄漏比应用层更危险因为内核内存是有限的泄漏多了会导致系统 OOM。常见的泄漏点包括kmalloc后忘记kfreeioremap后忘记iounmaprequest_irq后忘记free_irqclass_create后忘记class_destroy。devm_机制就是为解决这个问题而生的。所有devm_前缀的函数申请的资源都会绑定到设备上设备卸载时自动释放。我现在的习惯是只要能用devm_的地方绝不用非devm_版本。但devm_也不是万能的。有些资源没有devm_版本比如cdev_add对应的cdev_delalloc_chrdev_region对应的unregister_chrdev_region。这些还是需要手动在remove函数里释放。另外devm_资源的释放顺序和申请顺序相反如果资源之间有依赖关系需要注意释放顺序是否正确。5.4 中断不触发从硬件到软件的排查链路中断不触发的问题排查起来比较麻烦因为涉及硬件和软件两个层面。我一般按照以下顺序排查先确认硬件是否真的产生了中断。用示波器或逻辑分析仪测量中断引脚看有没有电平变化。如果硬件没有中断信号那问题在硬件侧检查外设配置、中断引脚连接、上拉电阻等。如果硬件有中断信号但驱动里没反应检查中断号是否正确。在设备树里interrupts属性描述的是中断号和触发方式但中断号需要经过中断控制器映射。用irq_of_parse_and_map或platform_get_irq获取映射后的虚拟中断号。然后检查request_irq的返回值。如果返回-EBUSY说明中断线被其他驱动占用了需要确认是否用了IRQF_SHARED标志。如果返回-EINVAL说明中断号无效或处理函数为空。最后检查中断是否被屏蔽。在/proc/interrupts里可以看到每个中断号的触发次数。如果次数一直是 0说明中断根本没有到达 CPU。如果次数在增加但你的处理函数没执行说明中断被其他处理函数抢占了或者你的IRQ_NONE返回值有问题。6. 驱动开发的进阶方向与学习路线6.1 从字符设备到子系统输入、显示、音频框架字符设备驱动是入门但实际项目里你更多是跟各种子系统打交道。比如做触摸屏驱动你需要了解Input 子系统把触摸事件通过input_report_abs上报做显示屏驱动你需要了解DRM 框架或Framebuffer 框架做音频驱动你需要了解ASoC 框架理解 DAI、DAPM、codec 这些概念。这些子系统的学习曲线比字符设备陡峭得多但掌握之后你会发现它们本质上还是“注册设备、实现回调、处理中断”这套逻辑只是框架帮你处理了很多通用的事情。我的建议是先精通一个子系统再横向扩展。比如你先把 Input 子系统吃透写一个完整的触摸屏驱动然后再去看 DRM 或 ASoC会发现很多设计思想是相通的。6.2 嵌入式 AI 与驱动开发的结合点最近两年嵌入式 AI 很火驱动开发和 AI 的结合点主要在异构计算上。比如 SoC 里集成了 NPU神经网络处理单元驱动需要负责 NPU 的初始化、内存分配、任务调度、中断处理。这类驱动通常比普通外设驱动复杂因为涉及 DMA 缓冲区管理、多核通信、固件加载等。另一个结合点是传感器数据采集。AI 模型需要摄像头、麦克风、IMU 等传感器的数据这些传感器的驱动质量直接影响数据质量。比如摄像头驱动如果丢帧或者曝光控制不准再好的模型也跑不出好结果。如果你有驱动开发基础想往嵌入式 AI 方向转我建议先补一下计算机体系结构的知识理解 DMA、Cache 一致性、内存屏障这些概念然后找一个带 NPU 的开发板从跑通官方 demo 开始逐步深入到驱动层。6.3 面试中高频出现的驱动开发问题嵌入式驱动开发的面试八股文主要集中在几个方向总线模型platform、I2C、SPI 的区别和适用场景、并发控制自旋锁和互斥锁的区别、中断上下文为什么不能睡眠、内存管理kmalloc 和 vmalloc 的区别、DMA 一致性内存、设备树compatible 匹配机制、常用属性。我整理了一个高频问题速查表问题回答要点platform 总线和 I2C 总线的区别platform 用于内存映射设备无物理总线I2C 用于外挂低速设备有物理总线自旋锁和互斥锁的区别自旋锁忙等待可用于中断上下文互斥锁睡眠等待只能用于进程上下文中断上半部和下半部的区别上半部快速响应下半部处理耗时操作tasklet 不能睡眠工作队列可以kmalloc 和 vmalloc 的区别kmalloc 物理连续适合 DMAvmalloc 虚拟连续物理不连续适合大内存分配设备树 compatible 的作用用于驱动和设备匹配驱动里的 of_device_id 表和设备树节点比较面试里除了八股还会问项目经验。我建议准备一两个你真正做过的驱动项目能说清楚这个驱动控制什么硬件用了什么总线怎么处理中断和并发遇到了什么问题怎么解决的。面试官更看重你解决实际问题的能力而不是背了多少概念。6.4 持续学习内核源码和开源项目驱动开发的学习离不开读内核源码。但内核源码几千万行不可能全读。我的方法是按需阅读写字符设备驱动时去看drivers/char/下的简单驱动写 I2C 驱动时去看drivers/i2c/下的框架代码和具体芯片驱动遇到不认识的 API用grep在内核源码里搜看别人怎么用的。开源项目也是很好的学习资源。比如 Linux 内核本身、U-Boot、Buildroot 这些项目里都有大量驱动代码。我经常在 GitHub 上搜platform_driver或i2c_driver看别人是怎么组织代码的。但要注意开源项目的代码质量参差不齐有些驱动写得很规范有些则是“能跑就行”。读的时候要有判断力学习好的设计避免坏的实践。最后分享一个我个人的习惯每写一个新驱动都先找一个内核里类似的驱动作为参考。比如写 GPIO 驱动就参考drivers/gpio/gpio-*.c写 I2C 传感器驱动就参考drivers/iio/下的同类传感器。这样不仅能加快开发速度还能保证代码风格和内核社区一致减少被 maintainer 打回的概率。
返回列表