
简介这份资源是《Linux设备驱动开发详解基于最新的Linux 4.0内核》一书的配套样例代码包面向具备一定C语言与操作系统基础、希望系统学习Linux内核驱动开发的工程师与高校学生。内容围绕设备模型、中断处理、I/O调度器、字符与块设备驱动、网络设备驱动、设备文件、内存管理、模块化设计及电源管理等核心主题展开可帮助读者对照书中章节动手实践理解驱动与硬件交互的完整流程。压缩包为rar格式共122个文件约659KB以c源码与cmd编译脚本为主辅以makefile构建文件、o目标文件、ko内核模块及mod、symvers等模块元数据另含少量头文件与测试程序结构紧凑便于按模块查阅。目前已有197人学习下载适合作为驱动开发入门与项目排错的参考素材。1. 从一份 drivers 源码包说起Linux 内核驱动到底该怎么读、怎么编、怎么跑很多人拿到一个叫drivers_drivers_linux_Kernel_的包第一反应是「这不就是内核源码里的 drivers 目录吗」然后直接扔进内核树里make modules结果编译报错、模块加载失败、设备节点出不来最后怀疑包是坏的。我拆过不少这类驱动源码包它本质上是把 Linux 内核drivers/下的若干子系统源码单独抽出来方便你脱离完整内核树去研究字符设备、平台设备、I2C/SPI 从设备驱动的注册流程。它解决的不是「一键装驱动」而是让你能在一个可控的目录里看清file_operations、probe/remove、module_init/exit这套骨架怎么落地。适合已经会写简单内核模块、但一进真实驱动就迷路的嵌入式 Linux 开发和运维。下面按「先立住原理、再动手编译、最后排坑」的顺序拆开讲。2. 驱动源码包的目录结构与内核编译模型先搞懂它凭什么能编2.1 为什么驱动不能像普通 C 文件那样直接 gcc内核驱动跑在内核态它调用的printk、kmalloc、register_chrdev这些符号都不是用户态 glibc 提供的而是内核自身导出的。所以驱动编译必须依赖内核头文件、Module.symvers以及一套叫 kbuild 的构建系统。你直接gcc -o test test.c一定失败报linux/module.h: No such file or directory。常见做法是写一个Makefile用-C指向内核源码目录让内核的顶层 Makefile 接管编译。这就是为什么驱动包通常带一个Makefile和一堆.c/.h而不是一个独立可执行文件。理解这一点后你就能判断手里的包属于哪一类如果它带完整Kconfig、Makefile且目录层级和内核drivers/一致那是「可回填内核树」的如果只有零散.c和一个简易Makefile那是「外部模块」写法。两者编译命令不同混用就是翻车现场。2.2 目录里各文件到底管什么一个典型的抽取式驱动包结构大致如下路径/文件作用是否必须Makefile定义obj-m或obj-$(CONFIG_XXX)驱动编译入口必须Kconfig配置菜单项决定编进内核还是编成模块回填内核树时必须xxx_drv.c驱动主体含probe/remove、file_operations必须xxx_drv.h寄存器定义、结构体声明视驱动而定Module.symvers内核导出符号表外部模块编译时校验符号外部模块建议有obj-m : xxx_drv.o表示编成可加载模块.koobj-y表示直接编进vmlinux。新手最容易忽略的是Kconfig里的tristate和bool区别tristate才有 M 选项bool只能 Y/N。如果你在菜单里找不到 M多半是写成了bool。2.3 外部模块编译的标准动作假设你拿到的包放在~/drv_pkg内核源码在/lib/modules/$(uname -r)/build标准编译流程如下# 进入驱动包目录 cd ~/drv_pkg # 查看当前内核版本确认 build 软链接存在 uname -r ls -l /lib/modules/$(uname -r)/build # 清理上次编译产物避免残留 .o 干扰 make clean # 指定内核路径编译模块M 指向当前驱动目录 make -C /lib/modules/$(uname -r)/build M$(pwd) modules-C让 make 先切到内核源码目录读取顶层规则M$(pwd)告诉 kbuild 实际要编的外部模块在当前目录。编译成功后目录里会出现xxx_drv.ko。如果报ERROR: modpost: xxx undefined说明你的驱动调用了内核没导出的符号或者内核版本和驱动不匹配这时候要看Module.symvers是否缺失。2.4 加载与验证别只看 insmod 没报错# 加载模块 sudo insmod xxx_drv.ko # 确认已加载查看引用计数和依赖 lsmod | grep xxx_drv # 查看内核日志probe 是否成功、有没有 request_region 冲突 dmesg | tail -30 # 卸载 sudo rmmod xxx_drvinsmod成功不代表驱动工作正常它只说明模块被内核接受。真正的验证看dmesg如果probe里register_chrdev返回负值或者ioremap失败日志里会有明确错误码。常见做法是先在module_init里只留printk确认能加载再逐步加注册逻辑这样排错范围小。3. 字符设备与平台设备驱动把注册流程拆成可复现的代码3.1 字符设备驱动的三段式骨架字符设备是最适合入门的驱动模型核心就三件事申请设备号、实现file_operations、注册到内核。下面是一个最小可编译的字符设备驱动#include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/uaccess.h #define DEV_NAME demo_chr static dev_t dev_num; static struct cdev demo_cdev; static char kbuf[128]; /* 读接口把内核缓冲区数据拷到用户态 */ static ssize_t demo_read(struct file *f, char __user *buf, size_t len, loff_t *off) { if (*off sizeof(kbuf)) return 0; if (copy_to_user(buf, kbuf, sizeof(kbuf))) return -EFAULT; *off sizeof(kbuf); return sizeof(kbuf); } /* 写接口把用户态数据拷进内核缓冲区 */ static ssize_t demo_write(struct file *f, const char __user *buf, size_t len, loff_t *off) { if (len sizeof(kbuf)) len sizeof(kbuf); if (copy_from_user(kbuf, buf, len)) return -EFAULT; return len; } static struct file_operations demo_fops { .owner THIS_MODULE, .read demo_read, .write demo_write, }; static int __init demo_init(void) { /* 动态申请一个设备号 */ if (alloc_chrdev_region(dev_num, 0, 1, DEV_NAME) 0) return -1; cdev_init(demo_cdev, demo_fops); if (cdev_add(demo_cdev, dev_num, 1) 0) { unregister_chrdev_region(dev_num, 1); return -1; } printk(KERN_INFO demo_chr loaded, major%d\n, MAJOR(dev_num)); return 0; } static void __exit demo_exit(void) { cdev_del(demo_cdev); unregister_chrdev_region(dev_num, 1); printk(KERN_INFO demo_chr unloaded\n); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL);alloc_chrdev_region让内核动态分配主设备号避免和已有设备冲突cdev_init把file_operations绑到cdev上cdev_add才是真正注册。copy_to_user/copy_from_user不能换成memcpy因为用户态和内核态地址空间不同直接访问会触发 oops。MODULE_LICENSE(GPL)不写会污染内核并限制部分符号调用。3.2 平台设备驱动的匹配机制真实嵌入式驱动大多是平台设备模型设备树或板级文件描述硬件资源驱动通过compatible匹配后拿到probe。核心结构是platform_driver#include linux/platform_device.h #include linux/of.h static int my_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; /* 从设备树或平台数据里取寄存器地址 */ 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); dev_info(pdev-dev, probe ok, reg%pr\n, res); return 0; } static int my_remove(struct platform_device *pdev) { dev_info(pdev-dev, removed\n); return 0; } static const struct of_device_id my_of_match[] { { .compatible vendor,demo-dev }, { } }; MODULE_DEVICE_TABLE(of, my_of_match); static struct platform_driver my_driver { .probe my_probe, .remove my_remove, .driver { .name demo_plat, .of_match_table my_of_match, }, }; module_platform_driver(my_driver); MODULE_LICENSE(GPL);of_device_id里的compatible必须和设备树节点里的字符串完全一致大小写、逗号位置错一个字符都匹配不上probe根本不会执行。devm_ioremap_resource带devm_前缀资源会在设备卸载时自动释放省掉手动iounmap这是推荐写法。module_platform_driver宏把module_init/exit都封装了比手写简洁。3.3 设备树节点与驱动参数的对应关系平台驱动能不能起来一半看设备树。一个匹配上面驱动的节点长这样demo_dev: demo10000000 { compatible vendor,demo-dev; reg 0x10000000 0x1000; interrupts 0 32 4; status okay; };reg的第一个值0x10000000是寄存器基地址第二个0x1000是长度platform_get_resource取的就是它。interrupts里的三个数分别对应中断控制器、中断号和触发类型驱动里用platform_get_irq取。status okay不能少写disabled内核直接忽略该节点。改完设备树要重新编译 dtb 并重启光重新加载模块没用因为设备树是启动时解析的。3.4 编译进内核树 vs 外部模块的取舍如果你的驱动要参与内核启动流程、依赖其他子系统初始化顺序回填内核树更稳如果只是验证功能、快速迭代外部模块编译更快。回填时把目录拷到drivers/下对应位置改父级Makefile和Kconfig然后make menuconfig选中。外部模块则每次都要指定-C内核路径。两者不能混回填后还用M编译可能因为Module.symvers不一致报符号错误。4. 驱动调试与常见故障排查这些坑我基本都踩过4.1 模块加载报 Unknown symbol现象insmod提示Unknown symbol in moduledmesg里列出具体符号名。原因通常是驱动调用了当前内核未导出的函数或者编译时用的内核版本和运行内核不一致。解决先用modinfo xxx.ko看vermagic是否和uname -r匹配再用grep 符号名 /proc/kallsyms确认符号是否存在。不匹配就重新用目标内核的build目录编译。4.2 probe 函数不执行现象模块加载成功lsmod能看到但dmesg里没有probe ok。原因多半是compatible不匹配、设备树节点status不是okay或者驱动没注册of_match_table。解决cat /proc/device-tree/.../compatible看实际值和驱动里的字符串逐字符比对确认of_match_table已赋值而不是只写了driver.name。4.3 设备号冲突导致注册失败现象register_chrdev或cdev_add返回-EBUSY。原因是静态指定的主设备号已被占用。解决改用alloc_chrdev_region动态分配或者cat /proc/devices查已用主设备号避开冲突段。静态设备号在多个驱动共存时特别容易撞车。4.4 用户态 read/write 返回 EFAULT现象应用层调用read报Bad address。原因是驱动里用了memcpy直接操作用户指针或者copy_to_user的地址没校验。解决所有用户态指针必须走copy_to_user/copy_from_user并且检查返回值access_ok在较新内核里已不推荐单独调用交给 copy 函数即可。4.5 卸载模块时内核 oops现象rmmod时系统卡死或打印 oops。原因是module_exit里漏了释放资源或者有内核线程还在引用模块。解决确保cdev_del、unregister_chrdev_region、iounmap成对出现有工作队列或定时器的先停掉再卸载。rmmod前先lsmod看引用计数是否为 0。5. 进阶用 printk 分级与 ftrace 把驱动行为看透驱动调试最原始也最有效的手段是printk但很多人只会printk(here\n)日志级别混乱一屏刷过去找不到重点。printk支持分级KERN_ERR、KERN_WARNING、KERN_INFO、KERN_DEBUG写法是printk(KERN_ERR fail: %d\n, ret)。控制台默认只打印比console_loglevel更严重的消息你可以用echo 8 /proc/sys/kernel/printk临时放开全部级别调试完再调回去否则日志量会拖慢系统。比printk更精细的是 ftrace。它能跟踪函数调用链不用改代码。比如你想看probe到底有没有被调用# 挂载 tracefs mount -t tracefs none /sys/kernel/tracing # 设置要跟踪的函数 echo my_probe /sys/kernel/tracing/set_ftrace_filter echo function /sys/kernel/tracing/current_tracer # 打开跟踪 echo 1 /sys/kernel/tracing/tracing_on # 触发操作后读取结果 cat /sys/kernel/tracing/trace # 关闭 echo 0 /sys/kernel/tracing/tracing_onset_ftrace_filter只跟踪你关心的函数避免全量输出淹没关键信息。functiontracer 记录函数调用function_graph还能显示调用耗时定位慢在哪一段特别有用。这套方法在没法加printk的生产环境里是救命稻草。还有一个习惯每次改完驱动先make clean再编别信增量编译。我遇到过.o残留导致新旧代码混编probe行为诡异查了半天才发现是编译产物没清干净。从那以后我每次编译驱动都强制走一遍make clean make -C ... M$(pwd) modules加载前先dmesg -C清空日志这样dmesg里出现的每一条都是本次操作的不会被历史日志干扰。希望帮到你。本文还有配套的精品资源点击获取