搞嵌入式这几年,经常有人问我:Linux底层到底怎么学?驱动开发是不是特别难?说实话,这类问题我回答过不下几十遍,每次都要从头讲一遍学习路径、内核机制、实战坑点,讲完对方还是一脸懵。原因很简单——Linux设备驱动开发不是靠看几个命令、抄几段代码就能上手的东西,它需要一套完整的知识体系,从内核态和用户态的区别,到设备模型、中断处理、并发控制,再到具体硬件的外设操作,每一环都绕不开。
所以当我看到《手把手教你学Linux设备驱动开发》正式出版的消息时,第一反应是:终于有人愿意把这套东西系统讲清楚了。这本“硬核宝典”来得正是时候,正好把我这几年摸索出来的经验,结合新书的核心内容,从原理到实战、从入门到进阶,给还在门口徘徊的朋友捋一条清晰的路。这篇内容不卖书,纯粹是把Linux设备驱动开发这条路上的关键节点、核心机制、常见坑位,结合我自己的实操经验一次性讲透。
1. 设备驱动开发为什么被称作“硬核技能”
1.1 不是所有写Linux的人都会碰驱动
先泼一盆冷水:大部分Linux开发者,日常工作根本接触不到驱动。业务开发写的是应用层代码,调用系统API,操作文件描述符;运维工程师更关注系统部署、服务编排、性能调优;就算你做的是嵌入式方向,如果只是基于现成BSP(Board Support Package)做应用开发,驱动这层对你来说依然是个黑盒。
但为什么驱动开发还是被大家当成“硬核”技能?因为它是技术水平的分水岭。一个能独立完成驱动开发的工程师,意味着他对Linux内核的运行机制有真正的理解——他清楚系统调用如何陷入内核态,内核如何管理设备号,file_operations结构体是怎样把应用层的open/read/write和底层的硬件操作绑定起来的。这层能力在就业市场上极具含金量,尤其是嵌入式Linux、智能硬件、工控设备、车载系统这些领域,驱动开发几乎是核心岗位的必考项。
我见过不少做嵌入式应用开发的朋友,明明业务逻辑写得挺溜,但一遇到硬件适配问题就抓瞎。比如换了颗新传感器,I2C时序对不上;或者板子的串口驱动挂载失败,串口工具收不到数据。这些问题说大不大,但不会驱动开发就只能干瞪眼,要么求驱动工程师帮忙,要么去论坛灌水。自己会写驱动,和只会调驱动,职业天花板完全不一样。
1.2 驱动开发的“硬”到底硬在哪里
说它硬,不只是技术栈深,更是因为它横跨的知识面太宽了。一个典型的外设驱动,至少涉及三块知识:
- 操作系统原理:进程调度、内存管理、文件系统、虚拟文件系统(VFS)这些基础概念,不理解它们就没法理解驱动在内核中的位置。
- 内核编程模型:内核态的编程约束、
GFP_KERNEL之类的内存分配标志、自旋锁和信号量的选择、中断上下文和进程上下文的区别,这些规则和应用层编程完全不同。 - 硬件协议与寄存器操作:GPIO口的电平控制、I2C总线的时序写读、SPI的片选与时钟极性、DMA的搬运机制,每一类外设都是一套独立的硬件知识体系。
这三个维度叠加,让驱动开发的学习曲线非常陡峭。尤其对于自学的人来说,难点在于——不像应用开发,你能立刻看到printf的结果,驱动开发一旦写错,可能就是系统崩溃、内核Oops,甚至整块开发板直接变砖。这种“做错了就真的要命”的特性,劝退了很多人。
1.3 学会驱动开发意味着什么
不过换个角度看,一旦你跨过了这道门槛,收益也是巨大的。你会获得对Linux系统最底层的掌控力,写应用程序时,你能预判每一次系统调用在内核里做了什么;调板子时,你能从内核日志倒推出硬件状态;做性能优化时,你能发现中断和DMA的瓶颈在哪里。很多资深内核工程师常说:“写好驱动的人,写应用层代码是降维打击。”这句话不是没有道理。
这也是为什么新出版的《手把手教你学Linux设备驱动开发》这样一本系统性的书,对学习者是很有价值的。它把那些要花两三年从内核源码、硬件手册、论坛零散帖子里才能拼凑出来的知识,做了一次完整梳理。尤其是带着项目实战去学,比盲目啃内核源码高效太多。
2. 动手之前,先把这些内核机制吃透
2.1 内核态和用户态:两个世界的分界线
聊驱动开发,绕不开的第一个概念就是内核态和用户态。我习惯用一个类比来解释:内核态像图书馆的管理员,能直接拿到一本书(硬件资源)并记录在册(内存),而用户态像是坐在阅览室的读者,只能通过管理员(系统调用)索要图书,不能自己冲进书库乱翻。
应用层的进程运行在用户态,受CPU保护机制约束,不能直接操作硬件地址,不能随便执行特权指令。当进程需要读写硬件时,必须通过系统调用接口(比如open、read、write、ioctl)陷入内核态,由内核完成真正的工作。设备驱动,本质上就是内核中负责管理特定硬件设备的那段代码,它运行在内核态,是这个“图书馆管理员”身份的直接体现。
这个机制带来一个非常重要的推论:**驱动代码一旦出错,影响的可能不仅仅是当前进程,而是整个系统。**应用层的段错误顶多让一个进程崩溃,内核态的非法内存访问可能让系统直接宕机。所以写驱动的时候,心里要时刻绷着一根弦——这里的每一行代码,都运行在特权级。
2.2 设备文件与VFS:用户是怎么找到驱动的
熟悉Linux的朋友肯定用过/dev目录下的设备文件,比如/dev/ttyS0、/dev/mmcblk0。用户空间的程序读写这些设备文件,和读写普通文件的接口一模一样——open、close、read、write。这背后依赖的就是Linux的虚拟文件系统(VFS,Virtual File System)机制。
VFS是Linux文件系统的抽象层,它定义了一套统一的操作接口。所有的文件系统(ext4、proc、sysfs、设备文件系统devtmpfs等)都要实现这套接口,向上层用户进程暴露统一的行为。设备驱动也是这么接入的:驱动注册成功后,会创建设备节点;用户在应用层打开设备节点时,VFS根据设备号找到对应的驱动,再调用驱动注册时填入file_operations结构体的函数指针。
理解了这条链路,你就明白为什么驱动开发中file_operations结构体如此重要。它就像是驱动与应用层之间的“服务菜单”,应用层能对设备做什么操作,全看这个结构体里注册了哪些函数。常见的成员包括:
open:打开设备时调用,驱动在这里做初始化准备。release:关闭设备时调用,驱动在这里释放资源。read:从设备读取数据到用户空间。write:从用户空间写入数据到设备。ioctl:设备控制命令的统一入口,比如设置串口波特率、修改传感器采样频率。poll/mmap:提供多路复用和内存映射支持,高性能场景经常用到。
2.3 设备号与设备模型:驱动注册的地基
应用层通过设备文件名访问设备,内核则通过设备号定位驱动。设备号由主设备号和次设备号组成:主设备号标识设备对应的驱动程序,次设备号标识被驱动的具体设备实例。比如你电脑上有两块同型号的USB转串口芯片,它们的主设备号相同,次设备号不同。
驱动开发中有两种常见的注册方式:老式的register_chrdev方式需要我们手动指定主设备号(或者传0由内核动态分配),并且一次注册会占用整个主设备号下的所有次设备号范围,比较浪费;更推荐的是新内核中的cdev方式配合alloc_chrdev_region动态分配设备号,精确控制次设备号的分配范围。
这里有一个很多新手第一次写驱动都会踩的坑:注册了设备号、也创建了设备节点,但打开时就是提示No such device or address。这种情况十有八九是设备号对不上,或者设备节点的创建时机不对。现在内核通常通过device_create在驱动加载时自动创建设备节点,前提是系统已经挂载了devtmpfs,并且驱动正确填充了struct device相关的属性字段。
3. 从零写一个字符设备驱动:手把手实操
3.1 搭建最小开发环境
开始写代码之前,先把环境备齐。学习驱动开发,建议准备独立的Linux环境,虚拟机也可以,但最好保证内核头文件版本和运行内核一致。推荐使用Ubuntu或者Debian系发行版,原因很简单——apt install linux-headers-$(uname -r)一条命令就能装好内核头文件,省去编译内核的麻烦。
如果你的目标平台是ARM开发板(比如正点原子、韦东山系列,或者树莓派),需要先交叉编译配置好工具链,并确保开发板的内核源码树和正在运行的内核版本对应。调试驱动最理想的方式,其实是在开发板上直接编译加载,如果条件不允许,x86虚拟机也能完成大部分字符设备驱动的学习和验证。
检查内核头文件是否安装到位,可以执行:
ls /lib/modules/$(uname -r)/build如果有输出说明环境没问题。这个build目录实际上是指向内核源码树的软链接,里面包含了编译内核模块所需的头文件、Makefile和配置文件。编译驱动的本质,就是让内核的构建系统把我们写的源码编译成.ko内核模块文件。
3.2 一个最小可用的驱动框架
直接上代码。先写一个最小可用的字符设备驱动,这个驱动不操作任何真实硬件,只是注册一个设备节点,你可以在应用层通过open/read/write和它交互。为了简洁,这里用miscdevice(杂项设备)框架,它是对cdev的封装,适合单设备场景,无需手动管理主设备号。
#include <linux/module.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <linux/uaccess.h> #define MISC_DEVICE_NAME "misc_demo" static ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { char kbuf[64] = "hello from kernel!\n"; int len = strlen(kbuf); if (*ppos >= len) return 0; if (copy_to_user(buf, kbuf, len)) { pr_err("copy_to_user failed\n"); return -EFAULT; } *ppos = len; return len; } static ssize_t demo_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { char kbuf[128]; if (count > sizeof(kbuf) - 1) return -EINVAL; if (copy_from_user(kbuf, buf, count)) { pr_err("copy_from_user failed\n"); return -EFAULT; } kbuf[count] = '\0'; pr_info("recv from user: %s\n", kbuf); return count; } static const struct file_operations demo_fops = { .owner = THIS_MODULE, .read = demo_read, .write = demo_write, }; static struct miscdevice demo_miscdev = { .minor = MISC_DYNAMIC_MINOR, .name = MISC_DEVICE_NAME, .fops = &demo_fops, }; static int __init demo_init(void) { int ret = misc_register(&demo_miscdev); if (ret) { pr_err("misc_register failed: %d\n", ret); return ret; } pr_info("misc_demo init finished\n"); return 0; } static void __exit demo_exit(void) { misc_deregister(&demo_miscdev); pr_info("misc_demo exit finished\n"); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A simple misc device demo");这段代码的核心逻辑很简单:初始化时调用misc_register注册设备,退出时misc_deregister注销设备。file_operations结构体里只注册了read和write,正好够演示数据在用户态和内核态之间来回拷贝。
这里要特别强调两个我见过无数新人中招的关键点:
第一,copy_to_user和copy_from_user是必选项,千万不能直接用memcpy。内核态不能直接访问用户空间的指针,因为用户空间的地址可能尚未映射到内核地址空间,直接访问会导致缺页异常,甚至内核崩溃。copy_*_user系列函数会做地址合法性检查,并能处理页面换入,是最安全的使用方式。
第二,指针的合法性检查非常必要。即使使用了copy_*_user函数,也必须确认返回值是否为0。返回值非0表示有部分数据没有拷贝成功,这时候返回-EFAULT给应用层,应用层会得到Bad address的错误信息。
3.3 编写Makefile并编译加载
驱动模块的编译不能直接用gcc,要借助内核的构建系统。新建一个Makefile,内容如下:
obj-m := misc_demo.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) clean这段Makefile的核心逻辑是:obj-m := misc_demo.o告诉内核构建系统,把misc_demo.c编译成内核模块;-C选项切换到内核源码目录读取内核的Makefile;M=$(PWD)表示返回当前目录进行模块构建。
在源码目录下执行make,如果环境没问题,会生成misc_demo.ko文件。用modinfo misc_demo.ko可以查看模块的信息,然后加载:
sudo insmod misc_demo.ko dmesg | tailinsmod加载之后,内核日志会输出misc_demo init finished。因为miscdevice自动在/dev下创建了misc_demo设备节点,这时候你可以直接测试:
echo "hello user" > /dev/misc_demo cat /dev/misc_democat会输出内核态返回的字符串hello from kernel!,到这,你的第一个字符设备驱动就跑通了。不要小看这几步,它能跑通,说明你对设备号、设备节点、file_operations、内核模块的加载卸载机制都有了具体的感知,后面所有复杂驱动都是在这个框架上做加法。
3.4 驱动模块的应用层测试程序
驱动写完还不够,最好写一个简单的应用层测试程序来验证各个接口。这样能确认你写的驱动不仅是“加载不报错”,而是真的按照预期工作:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> int main(void) { int fd = open("/dev/misc_demo", O_RDWR); if (fd < 0) { perror("open"); return -1; } write(fd, "hello from user", 15); char buf[64] = {0}; int n = read(fd, buf, sizeof(buf)); if (n < 0) { perror("read"); close(fd); return -1; } printf("read from kernel: %s\n", buf); close(fd); return 0; }编译运行后,如果一切正常,应用层会依次触发驱动的write和read回调,你会在内核日志和终端上看到两条对应的消息。到了这一步,你已经摸清了“应用层 -> 系统调用 -> VFS -> 设备驱动”的完整数据流。
4. 驱动开发进阶:从会跑到会飞
4.1 阻塞与非阻塞I/O:处理硬件慢速响应
真实世界里的设备不是每次都“有数据就能读”的。比如串口要等数据到达,按键要等人按下。如果用轮询方式不断查询硬件状态,会白白占满CPU。这里就要引入阻塞I/O机制。
驱动开发中,阻塞I/O的经典实现是使用等待队列(wait_queue_head_t)。驱动在read回调里判断硬件是否有数据——如果暂时没有,就把当前进程放入等待队列,进入睡眠状态;当中断或者内核定时器发现数据到来时,再唤醒等待队列中的进程。
从应用层看,调用read的进程会被挂起,直到驱动唤醒它。这跟应用层用read读取HID设备输入、串口数据的体验是一致的——数据没来,read就不返回。虽然现在内核推荐使用wait_event_interruptible系列宏来简化等待队列的操作,但理解底层机制仍然重要。
自旋锁也是驱动开发躲不开的基础概念。它和信号量的选择,本质上是“能不能睡眠”的取舍。在中断上下文或临界区很短时,用自旋锁;在进程上下文且临界区可能较长的场景,用信号量。很多稳定性问题,追根溯源都和锁的使用不当有关——要么死锁,要么中断被长时间关闭导致实时性下降。
4.2 硬件操作:从GPIO到总线协议
当驱动的read/write回调需要真正操作硬件时,通常涉及寄存器读写。在内核中,寄存器操作有两种方式:一是通过ioremap把物理地址映射到内核虚拟地址空间,然后直接读写;二是使用内核提供的readl/writel接口。后者更安全,也更容易移植。
以I2C设备为例,一个新的I2C触摸屏驱动要做的第一件事就是在probe回调里拿到struct i2c_client和struct i2c_adapter,然后通过i2c_smbus_read_byte_data之类的接口读寄存器,完成设备初始化。这个过程中,I2C控制器外层框架已经帮你处理了总线仲裁、时钟时序等繁琐细节,你只需要关注你用到的设备自身的寄存器语义。SPI设备也是类似,通过struct spi_device和spi_message来组织一次传输。
学习硬件操作,我特别推荐从GPIO这类最简单的接口入手,然后在靠I2C或者SPI这种有明确协议的外设去练手。直接在树莓派或者一块带I2C接口的加速度计传感器模块上写驱动,比任何看书的效率都高。
4.3 中断与底半部:处理异步硬件事件
轮询效率低,所以真实驱动强烈依赖中断。硬件发生事件时,通过中断线通知CPU,CPU跳转到驱动注册的中断处理函数(top half)执行快速必要的操作,比如读取硬件状态寄存器和清除中断标志,然后立即返回;耗时的数据拷贝等工作交给底半部(bottom half)去完成。
Linux内核提供了多种底半部机制,从老的tasklet到新的workqueue,再到附加在现代内核性能调优里的threaded IRQ。选择哪种取决于你的需求:如果底半部需要睡眠(比如等待I2C总线传输),就需要workqueue;如果只是简单的延后处理小任务,tasklet也有它的适用场景。
写中断驱动的另一个关键点是要处理好共享中断。现在的硬件很多中断源都挂在同一根中断线上,注册中断时必须传入设备相关的dev_id参数,并在中断处理函数里首先判断“是不是我的设备产生了中断”,不是就返回IRQ_NONE,是就返回IRQ_HANDLED。这个细节在芯片引脚复用的场景下尤其常见,搞错会导致中断风暴。
4.4 调试工具链:驱动的“透视眼”
驱动开发最痛苦的不是写代码,是出了问题以后不知道怎么定位。好在Linux内核提供了足够强大的调试工具链:
printk/pr_info:最朴素的调试方式,配合dmesg查看输出。调试级别用KERN_DEBUG或者pr_debug时,需要确认内核CONFIG_DYNAMIC_DEBUG是否开启,否则看不到调试信息。/proc、/sys接口:在驱动里创建对应的读写节点,随时导出内部状态。这是我调试驱动时最常用的办法,相比printk,它的实时性更好,而且不对正常流程产生太多干扰。strace:当不确定应用层的系统调用到底失败在哪里时,用strace跟踪系统调用表象和返回码,能帮助快速区分问题出在应用层还是内核。- 内核Oops信息:遇到内核崩溃时,控制台或者
/var/log/kern.log里的Oops信息包含了出错的指令地址和函数调用栈,用addr2line结合内核符号表(vmlinux和System.map)就能定位到具体源码行。这是驱动工程师最核心的“战斗技能”。
调试驱动的经验我是真的踩过不少坑,后面单独整理了一章常见问题速查表,那些问题基本覆盖了新手到进阶的大部分场景,照着排查效率会高很多。
5. 常见问题与排查技巧实录
5.1 加载模块报错“Invalid module format”
这个错误大概率是内核源码版本和运行内核版本不匹配导致的。检查一下uname -r和/lib/modules/$(uname -r)/build是否是同一套源码树。还有一种情况是,你按照教程用自己的symvers编译过外部树,而内核的符号版本(CONFIG_MODVERSIONS)不一致,也会触发这个错误。
解决办法通常就是重新安装/重新编译与当前内核完全匹配的头文件或内核源码,清理后重新make。如果还不行,用modinfo的vermagic字段对比一下模块和内核的版本信息。
5.2 设备节点无法生成
misc_register成功返回了,但/dev下迟迟没有设备节点,或者设备节点生成了但一打开就报错。首先要确认两点:第一,/dev是不是devtmpfs挂载的(执行mount | grep devtmpfs),如果系统没有自动挂载,需要手动执行mdev -s或者udevadm trigger触发设备节点创建;第二,确认你的驱动是否在模块中被正确加载,用lsmod | grep 驱动名查看模块加载状态。
如果是自己手动用mknod创建设备节点,还要保证主设备号和次设备号与驱动注册的一致,否则打开时就会因为找不到驱动而报错。
5.3 一读写就“Oops”或者系统重启
这是驱动开发中最可怕的情况。出现Oops,基本就是内核态代码访问了非法内存,比如:
- 忘记用
copy_to_user,直接解引用用户空间指针。 - 缓冲区越界写。
- 在中断上下文里调用了可能睡眠的函数(比如
kmalloc(GFP_KERNEL))。 - 锁操作不当导致死锁或者锁顺序反转。
这时候的排查思路是:先把Oops信息完整看一遍,找到“RIP:”后面的函数名和偏移量,结合/proc/kallsyms或者vmlinux通过addr2line定位到出错代码行。然后仔细审查那行代码周围的操作,重点看指针来源和内存生命周期。
如果是开发板或者真实硬件,还有一个常见坑——忘了在硬件操作前通过设备树配置引脚复用功能,导致寄存器写不进去,或者中断完全触发不了。这类问题看日志往往没有直接报错,需要回到硬件本身的原理图和数据手册去找原因。
5.4 并发访问造成的数据错乱
驱动设备的read/write可能会被多个进程同时调用,如果驱动的内部状态(比如一个保存设备状态的缓冲区)不做保护,就会出现数据错乱。表现是调试单个进程完全正常,多进程并发一跑就出问题。解决办法就是在临界区加锁,或者改用无锁设计(比如atomic_t、READ_ONCE/WRITE_ONCE)。还有一个习惯我强烈建议养成:驱动中的全局变量越少越好,能不共享就不共享;如果必须共享,一定想清楚访问它的上下文是进程上下文还是中断上下文,再决定用什么锁。
5.5 常见问题速查表
| 现象 | 可能原因 | 优先排查方法 |
|---|---|---|
| 模块加载失败 | 内核版本不匹配 / 符号冲突 | modinfo查看vermagic,和uname -r对比 |
| 设备节点不存在 | devtmpfs未挂载 | `mount |
| 打开设备返回 ENODEV | 设备号不匹配 | cat /proc/devices查看主设备号,对比设备节点 |
| read 返回 EFAULT | 使用了非法的用户空间指针 | 检查是否使用copy_to_user且返回值是否为 0 |
| 驱动加载后系统卡死 | 初始化中死锁或死循环 | 检查init函数中锁的使用和循环条件 |
| 中断一直触发 | 中断标志未清除 / 共享中断误判 | 检查硬件中断状态寄存器、dev_id参数 |
| 数据间歇性错乱 | 并发访问未加锁 | 审查临界区,考虑加锁或原子变量 |
6. 怎么用好这本“硬核宝典”:学习路线与方法建议
6.1 从框架到驱动的三步走
我的建议是不要拿到书就一头扎进字符设备章节,先花两三天把Linux内核的基本工作方式理清楚,再去动手写代码。内核和驱动的知识,单纯靠记忆是记不住的,它是典型的“做中学”技能,最好的学习路径是:
- 第一步:搭好环境,编译第一个hello world级驱动,搞清楚
insmod、rmmod、dmesg、/proc/devices这些基础工具和虚拟文件。 - 第二步:熟练使用字符设备框架,自己能写出完整的
read/write/ioctl实现,理解file_operations每个常见回调什么时候被调用、返回值如何影响应用层。 - 第三步:找一块带真实外设的开发板,哪怕是几块钱的I2C温度传感器,把
probe、总线读写、中断处理完整走一遍。这一步是让你从“能写框架”变成“能调硬件”,也是驱动开发和嵌入式应用开发拉开差距的地方。
6.2 书是好书,但仍要配合源码和手册
新出版的这本《手把手教你学Linux设备驱动开发》内容很实战,书里提供了大量可复现的例程,比起自己啃内核源码要省力不少。但我建议任何一本驱动书籍都配合三样东西一起看:内核源码(/usr/src/linux-headers-$(uname -r))、芯片数据手册(Datasheet)、以及实际可操作的开发板电路原理图。书帮你建立知识地图,源码和数据手册才是最终权威,而板子是用来验证一切的最快路径。
还有一点,驱动开发的学习过程中,有相当多的时间应该花在内核文档(Documentation目录)和现成驱动源码的阅读上。Linux内核自带了几万个驱动,很多都是非常优秀的学习范本。比如你想学I2C驱动,就找内核里的drivers/i2c/busses,看看各家控制器的实现差异;想学平台驱动,去drivers/leds翻一翻,代码短小精悍,看完就能理解platform_driver注册与device tree匹配的整个流程。
6.3 面试和职业发展:驱动开发怎么帮你走得更远
写驱动的经验在职场上的优势非常明显。Linux内核相关的岗位、BSP工程师、嵌入式架构师,这些岗位都直接要求驱动开发能力。哪怕应聘通用后端开发,有内核态编程经验的人,对文件系统、网络协议栈、性能调优的理解深度也明显优于只会写应用层的人,因为你对整个系统的运行有了底层级的掌控。
另外提醒一下,Linux驱动开发面试中,面试官特别爱深入问几个问题:什么是用户态和内核态的区别?字符设备、块设备、网络设备的抽象模型分别是什么?自旋锁和信号量的区别?中断上下文为什么不能睡眠?如果一个驱动支持多个设备,file_operations里的私有数据是怎么管理的?这些问题你如果在学习过程中真的动手写过代码,而不是死记硬背,其实都很容易答出来。
7. 写在最后的一些真心话
《手把手教你学Linux设备驱动开发》这本“硬核宝典”上市,对正在学习Linux底层技术的人确实是个好消息。它在学习曲线上做了不少“削峰填谷”的工作,把那些我当年翻论坛翻到凌晨才能拼凑出来的知识点,系统地串成了一条可执行的学习路线。不过说到底,写驱动这件事,书只能带你上路,真正让你技术质变的,永远是手里那块开发板、一屏内核日志,和一遍遍调试到凌晨却终于跑通时的成就感。
如果你正打算进入Linux底层开发这个方向,我的建议很简单:环境搭起来,第一个hello world模块跑起来,剩下的路自然就一点点浮现出来了。驱动开发确实是座高山,但山脚的路,真的没有你想象的那么难。