Linux设备驱动开发实战:从字符设备到内核调试的完整入门指南
开门见山地说Linux设备驱动工程师这六个字放在招聘网站上确实自带几分神秘感——岗位描述里满屏的内核态中断上下文DMA设备树看得人头皮发麻薪水区间又通常挂得比普通后台开发高出一截外行第一反应多半是这是不是搞底层黑客的那帮人其实真入行了你就会发现这行当没有传说中那么玄乎但也绝不是随便搭个框架调个API就能混日子的活儿。它解决的是操作系统和真实硬件之间最后一公里的协作问题说白了就是让Linux内核认得出你的设备、叫得动你的硬件、转得好你的数据。这篇文章我就用自己的实际经历把这行的真实样貌、核心技能、入门路径和踩坑心得一次讲清楚想入行或者正在准备面试的朋友可以少走很多弯路。1. 先打破错觉高薪和神秘是怎么来的真实工作又在做什么先说神秘。这很大程度是信息差造成的。Linux驱动的工作对象是内核和硬件普通人日常接触的是Windows和Android看到开发者在终端里敲着一行行insmod、dmesg、cat /proc/interrupts天然会觉得这是系统底层黑魔法。另一方面驱动工程师写出来的代码大部分不直接交付给终端用户而是以.ko内核模块或固件的形式藏在系统里用户根本看不到它自然不知道它干了什么活。再加上内核里那一堆结构体、回调函数、宏定义外行一页源码都看不懂神秘感就来了。再说高薪。这行的薪水不是靠神秘感炒出来的而是由供需和门槛决定的。驱动开发要求一个人同时懂硬件、懂内核、懂操作系统原理还得会调试问题这种复合型人才本身就少。尤其在嵌入式、物联网、汽车电子、智能硬件这些行业设备种类千奇百怪厂商内核版本五花八门会写驱动的人永远是稀缺资源。稀缺自然溢价加上项目周期紧的时候一个能独立搞定外设接入的工程师往往能顶两三个应用层开发的工作量企业愿意花高薪养着本质是划算的。1.1 外界眼里的刻板印象 vs 真实的工作场景很多人以为驱动工程师每天都在写汇编、看芯片手册、跟示波器打交道像是硬件工程师和软件工程师的混合体。确实有一部分这种成分但真实工作远没有这么单纯。我入行这几年日常大概是这样的早上来了先看一眼邮件和工单系统可能有FAE转过来的客户问题——某个传感器在客户板上不工作logs抓回来一看i2c传输超时先初步判断是时序配置问题还是硬件上拉电阻没焊好然后打开源码树确认新需求要适配的芯片型号翻翻datasheet里的寄存器描述对照内核里已有的驱动框架看是改现有驱动还是新写一个下午可能要花大量时间看内核日志、调设备树、验证中断和DMA的行为等等。这个岗位和纯软件岗位最大的区别在于你不是和抽象的业务逻辑打交道而是和真实的物理世界打交道。硬件会出各种奇怪的毛病掉电时序不对、信号反射、时钟不准、寄存器被误写这些问题应用层根本感知不到但驱动层全都要管。所以这行很培养工程师的物理直觉——你知道代码最终会变成引脚上的电平、总线上的波形。1.2 驱动工程师一天到晚到底在解决什么问题抛开具体业务驱动工程师的核心任务可以归纳为三件事让内核认设备、让设备干工作、让系统别崩溃。让内核认设备对应的是设备的枚举和初始化。一个芯片接到系统总线上内核怎么知道它存在、它的地址和中断号是多少、该加载哪个驱动这些在PC上用PCI/ACPI自动完成在嵌入式上则是通过设备树Device Tree或ACPI表来描述。驱动工程师在这里的活就是写设备树节点、写probe函数把硬件信息和驱动逻辑绑定起来。让设备干工作则是对设备操作的完整实现。比如你驱动的是一个ADC芯片那你要实现读寄存器、启动转换、等待完成、读回数据、转成电压值这一系列逻辑并且把它封装成read/ioctl等接口提供给用户空间让上层的应用程序像读文件一样读电压值。让系统别崩溃是最累的部分。驱动跑在内核态一个空指针解引用就直接panic了不像应用程序崩了还能重启。你要时时刻刻想着并发、中断、内存屏障、锁的粒度、休眠与唤醒任何一个疏忽都可能导致整个系统挂掉这在生产环境是不可接受的。1.3 薪资水平背后的真实逻辑门槛、稀缺与试错成本高薪不是没有原因的。第一门槛高。Linux驱动要求扎实的C语言功底、操作系统原理、计算机体系结构还要懂硬件接口协议几样缺一不可这已经筛掉一大批人。第二稀缺性强。真正能独立完成驱动开发的人才供给一直不足尤其在国产芯片、工业控制、汽车电子领域招一个合格的驱动工程师往往要挂两三个月。第三试错成本高。驱动代码跑在kernel态一个bug可能让整台设备变砖尤其是量产后的现场问题排查起来动辄跑现场、抓日志、复现时间成本远超普通软件bug。所以说高薪不是行业自嗨是市场给出的定价。理解了这层底层逻辑你就明白为什么很多人挤破头想转过来却又有大量的人在面试环节就倒下了——因为考察的东西确实硬核。2. 设备驱动三兄弟字符设备、块设备、网络设备先分清楚再动手Linux下的设备驱动按设备类型和数据交换方式大体可以分为三大类字符设备、块设备、网络设备。这三者的核心差异、使用场景和驱动编写方式都截然不同。很多初学者一上来就急着抄代码结果连自己写的是什么设备都没搞清楚后面越学越乱。所以我建议你先把这三兄弟的底细摸清楚。2.1 三类设备的核心差异与典型场景设备类型数据交换方式典型代表核心特点字符设备按字节流逐个读写顺序访问串口、LED、按键、触摸屏、ADC、看门狗结构简单驱动的绝大部分工作都集中在这里块设备按固定大小的块读写支持随机访问硬盘、eMMC、U盘、SD卡有缓存机制涉及页缓存、I/O调度复杂度高网络设备以数据包为单位收发以太网卡、WiFi、4G/5G模块不通过/dev节点访问走网络协议栈主要实现发包收包用途上字符设备几乎涵盖了外设驱动的大半壁江山。你在嵌入式板上点个灯、读个温湿度、控制个电机基本都是字符设备驱动。块设备则跟文件系统深度绑定比如你用SD卡存数据整个读写路径要经过块设备层、文件系统层驱动只负责把请求翻译成具体的硬件操作。网络设备更特殊一点它不提供/dev/xxx这个节点用户程序用socket通过协议栈收发数据驱动只需要实现net_device_ops里的回调把sk_buff里的数据发出去或者收上来。2.2 为什么所有教程都让你从字符设备驱动入门原因很简单字符设备驱动是理解内核驱动框架最好的脚手架。它麻雀虽小五脏俱全从设备号申请、cdev注册、file_operations实现到与用户空间的数据拷贝、锁和并发控制几乎涵盖了驱动开发的所有核心知识点而且不需要牵扯块设备层的复杂缓存和调度机制也不需要网络协议栈的知识负担。你写一个字符设备驱动本质上就是在实现一套文件操作接口。open、read、write、release、ioctl这些函数你熟悉得不能再熟悉因为在用户空间你天天用。差别只在于你写的这些函数不是应用程序里的普通函数而是被内核在特定时机调用的回调函数。这个转变一旦想通驱动开发的黑盒感就消失了一半。我自己带过的新人里凡是能独立把一个小字符设备驱动写明白、能解释清楚open之后内核做了什么、读写时数据怎么从用户态到内核态、release时资源怎么释放的后面学块设备、网络设备、设备树都是水到渠成的事。相反那些急着研究eMMC、急着搞DMA的新人往往因为底层概念没打牢遇到问题根本无从下手。所以这条路真急不得。2.3 内核空间和用户空间驱动工程师必须拎清的边界写驱动和写应用最本质的区别是工作空间的不同。应用程序跑在用户态有独立的内存地址空间一个段错误最多崩掉自己驱动代码跑在内核态共享内核地址空间一个非法指针就可能把整个系统搞挂。这条边界直接带来两个实操层面的影响一是不能随便用C库函数。驱动里不能直接调用printf、malloc、strcpy这些库函数要用内核提供的printk、kmalloc、strscpy等替代。因为内核空间没有glibc可用你的代码是在一个更底层的环境里运行。二是不能直接访问用户态传进来的指针。比如write函数收到了一个用户空间缓冲区的地址千万不能在内核里直接解引用要用copy_from_user、copy_to_user或者get_user/put_user这类接口由内核帮你做地址合法性检查和跨空间数据拷贝。这不仅是规范问题更是安全问题——恶意的用户态指针可能导致内核崩溃或提权漏洞。很多编译通过但运行崩溃的驱动bug根源就是越过了这条边界。搞明白我在哪里、能碰什么、不能碰什么是驱动开发者心智模型成熟的第一步。3. 手写一个字符设备驱动从代码到加载的完整实操理论说了一大堆现在上手实操。这一节我带你写一个最简单的字符设备驱动并完整走一遍编译、加载、测试、卸载的流程。这个例子是我认为最合适的新手练手素材我在团队内部带人也是从这一步开始的。3.1 模块骨架module_init与module_exit背后的执行时机Linux驱动有两种编译方式编进内核镜像或者编译成模块.ko文件动态加载。开发阶段几乎都用模块方式方便反复修改测试。一个内核模块最基本的入口是module_init和module_exit两个宏它们告诉内核模块加载时执行哪个函数卸载时执行哪个函数。当你在终端敲insmod xx.ko时内核其实做了不少事比如分配module的内存、处理符号解析、调用模块的init函数。init函数里一般做资源初始化、设备号申请、注册设备等操作如果init失败insmod会直接报错并回滚所有已申请的资源。exit函数则负责释放init阶段申请的资源保证卸载后系统干干净净。这个谁申请、谁释放的原则看似简单但我在代码审查里见过太多次只申请不释放导致的资源泄漏。比如cdev_add成功了但后面device_create失败直接return错误把之前注册的cdev和申请的设备号晾在那儿系统里留下僵尸节点。真正的老手会把每一个错误分支的资源释放路径理得清清楚楚。3.2 file_operations驱动和应用程序握手的关键结构体file_operations是字符设备驱动里最重要的结构体。它就像一份菜单把用户在用户空间调用的open/read/write/release等系统调用映射到驱动实现的具体函数上。比如应用程序执行open(/dev/mychrdev, O_RDWR)内核会在设备对应驱动的file_operations里找到.open字段指向的函数并调用它。这个回调函数通常不做什么复杂事可能只是递增一个计数器、初始化一个私有数据结构但在某些场景下它承担着串口打开独占检查、设备初始化等关键逻辑。read和write则是数据交换的主战场。这两个函数的声明本身就能看出很多门道__user指针标记、size_t count、loff_t *offset分别对应了数据从哪来、要多少、当前文件位置在哪里。有个很容易被忽视的点是read不一定要返回用户请求的字节数返回0表示读到文件尾返回负数表示出错而write的返回值表示实际写入的字节数这些返回值会原样传给应用程序直接决定应用侧对读写结果的判断。3.3 完整驱动代码与逐段解读下面是我整理的示例驱动功能很简单用户空间写入一串字符驱动在内核日志里打印出来用户空间读取时驱动返回一段固定的字符串。麻雀虽小但设备号申请、字符设备注册、类创建、设备节点生成等关键流程全都覆盖了。#include linux/init.h #include linux/module.h #include linux/kernel.h #include linux/cdev.h #include linux/fs.h #include linux/device.h #include linux/uaccess.h #include linux/slab.h #define DEV_NAME mychrdrv static int major 0; static struct cdev my_cdev; static struct class *my_class; static dev_t dev_num; static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { char *data hello from kernel\n; size_t len strlen(data); if (*offset len) return 0; if (count len - *offset) count len - *offset; if (copy_to_user(buf, data *offset, count)) { pr_err(copy_to_user failed\n); return -EFAULT; } *offset count; return count; } static ssize_t my_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) { char *kbuf; if (count 0 || count PAGE_SIZE) return -EINVAL; kbuf kzalloc(count 1, GFP_KERNEL); if (!kbuf) return -ENOMEM; if (copy_from_user(kbuf, buf, count)) { kfree(kbuf); return -EFAULT; } kbuf[count] \0; pr_info(kernel received: %s\n, kbuf); kfree(kbuf); return count; } static int my_open(struct inode *inode, struct file *file) { pr_info(mychrdrv: open called\n); return 0; } static int my_release(struct inode *inode, struct file *file) { pr_info(mychrdrv: release called\n); return 0; } 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_init(void) { int ret; ret alloc_chrdev_region(dev_num, 0, 1, DEV_NAME); if (ret 0) { pr_err(alloc_chrdev_region failed\n); return ret; } major MAJOR(dev_num); cdev_init(my_cdev, my_fops); my_cdev.owner THIS_MODULE; ret cdev_add(my_cdev, dev_num, 1); if (ret 0) { unregister_chrdev_region(dev_num, 1); return ret; } my_class class_create(THIS_MODULE, DEV_NAME); if (IS_ERR(my_class)) { cdev_del(my_cdev); unregister_chrdev_region(dev_num, 1); return PTR_ERR(my_class); } device_create(my_class, NULL, dev_num, NULL, DEV_NAME); pr_info(mychrdrv: init success, major%d\n, major); return 0; } static void __exit my_exit(void) { device_destroy(my_class, dev_num); class_destroy(my_class); cdev_del(my_cdev); unregister_chrdev_region(dev_num, 1); pr_info(mychrdrv: exited\n); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Engineer Chen); MODULE_DESCRIPTION(A simple char device driver example);逐段看几个关键点。alloc_chrdev_region会在系统里动态分配一个未使用的主设备号和次设备号并把结果放在dev_num里这样就不用手工指定一个可能冲突的主设备号了。cdev_init负责把file_operations结构体绑到cdev上这是驱动和设备操作之间的关键连接。cdev_add真正把设备加入内核从这一刻起内核就知道有这么一个字符设备存在了。后续再通过class_create加上device_create在/sys和/dev下自动生成设备节点省掉了手动mknod这一步。read函数里用了copy_to_userwrite里用了copy_from_user这是用户态和内核态之间数据搬运的标准姿势。不要试图直接解引用用户态指针这是很多新手最容易踩的雷。还有一点read函数里用*offset来记录当前读位置当读到数据尾部时返回0用户态read就会认为到了文件末尾这个逻辑跟市面上的文件操作习惯完全一致。3.4 Makefile与编译加载测试全过程驱动编译依赖内核源码树不能像普通程序那样直接gcc。需要在目标机器上准备好内核头文件或完整的源码树然后用内核的构建系统来完成编译。我常用的Makefile模板如下obj-m : mychrdrv.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean解释一下obj-m表示最终生成一个内核模块KDIR指向当前内核的构建目录M指定模块源码所在目录。执行make之后内核构建系统会编译生成mychrdrv.ko。编译干净利落的话下一步就是加载和测试sudo insmod mychrdrv.ko lsmod | grep mychrdrv dmesg | tail -n 10 ls -l /dev/mychrdrv echo hello driver /dev/mychrdrv cat /dev/mychrdrv sudo rmmod mychrdrv如果一切顺利dmesg里能看到init success的日志echo写入的内容会在内核日志里看到kernel received: hello drivercat会输出hello from kernel。这里有两个常见问题值得提一下。一个是Secure Boot开启时会拒绝加载未签名模块收到Operation not permitted的错误调试机上建议关闭或者在编译时做模块签名。另一个是运行uname -r看到的内核版本如果和编译用的内核头文件版本不一致模块加载时会报version mismatch平台移植时尤其容易遇到。4. 驱动调试三板斧日志、设备节点和常用命令驱动开发离不开调试而在内核态调试手段和用户态完全不是一个画风。没有断点、没有单步、没有优雅的IDE界面很多时候你只能靠日志和系统暴露出来的各种信息去推断问题。所以我把自己这些年用得最顺手的调试方法总结成了三板斧。4.1 printk分级和dmesg的正确用法在内核里打印日志用的函数是printk跟应用层的printf长得像但多了日志等级。常见的等级有KERN_ERR错误、KERN_WARNING警告、KERN_INFO信息、KERN_DEBUG调试。等级决定日志是否显示在当前控制台以及是否进入内核缓冲区。默认情况下等级低于console_loglevel的信息才会打印到控制台而dmesg可以查看环形缓冲区里的所有日志。调试时我喜欢在关键路径上加上pr_info和pr_debug。pr_debug这类动态调试日志默认不输出需要开启内核配置CONFIG_DYNAMIC_DEBUG才可以通过debugfs动态打开某个文件的对应日志非常适合在客户现场不重新编译模块的情况下排查问题。一个血泪教训是不要在中断上下文中或者持锁状态下用printk做太多输出。printk本身可能触发调度和锁操作在硬中断里大量打印可能导致系统卡顿甚至死锁。真要在中断里记录信息应该把信息放到缓冲区延后到下半部或者内核线程里再统一打印。4.2 /dev、/sys和/proc调试时的地图这三个目录是驱动工程师必须烂熟于心的调试地图。 /dev是设备节点的所在地应用程序通过它访问设备ls -l可以看到主次设备号帮你确认设备是否注册成功。 /sys是内核对象在用户空间的映射特别是/sys/class和/sys/bus下面能看到设备和驱动的绑定情况读/sys/class/xxx/xxx/下的文件可以获得驱动暴露给用户空间的属性信息这是现代驱动框架的一个重要出口。 /proc则主要用来查看内核运行状态比如/proc/interrupts能看到每个中断号的触发次数/proc/devices能看到当前注册的字符设备和块设备/proc/meminfo能看到内存使用情况。遇到设备不工作的问题时我通常先看/dev下有没有节点、/sys下有没有设备目录、/proc/interrupts里中断有没有增加用这几步配合dmesg就能定位到八成的问题。这比直接上示波器要快得多也更有逻辑性。4.3 手动创建设备节点与权限踩坑记录示例代码里通过device_create自动创建了/dev设备节点但不是所有驱动都会这么做。很多老旧驱动或者特殊情况需要手动创建节点命令是mknodsudo mknod /dev/mychrdrv c 240 0 sudo chmod 666 /dev/mychrdrvmknod的c表示字符设备240是本例中动态分配的主设备号0是次设备号。不执行chmod的话普通用户没有权限访问设备节点程序运行时可能出现Permission denied这个错误在排查时要第一时间想到节点权限问题。另外一个容易踩的坑是手动创建节点时主设备号写错了应用层打开设备会一直报No such device or address。这时候用cat /proc/devices查一下实际分配的主设备号核对无误再创建就行。自动创建节点的方案因为有udev配合会相对省心但嵌入式环境里如果没有udev还是要回到手动创建这条路。5. 常见问题速查表和独家避坑心得驱动开发的坑五花八门我按自己的经验整理了一份带实战背景的速查表覆盖了从编译加载到运行调试的高频问题并附上我踩过之后的处理思路。如果面试或工作里遇到相似问题这份表可以直接拿来当排查手册。5.1 编译与加载阶段的频发问题问题现象可能原因处理思路insmod报Invalid module format内核版本不匹配或编译器不同用uname -r核对版本确认同一套工具链完成内核和模块编译insmod报Operation not permittedSecure Boot限制或权限不足尝试关闭Secure Boot确认确实加了sudo编译时报缺少头文件未安装内核开发包或源码树不完整安装linux-headers-$(uname -r)或建立匹配内核源码树的软链接加载后/dev没有对应节点类或设备节点创建失败或者udev未刷新看dmesg报错确认是否调用了device_create必要时mknod手动创建卸载模块时系统卡死有进程占用设备或release函数里有长时间阻塞先确认无进程打开设备检查release的退出路径是否在等待某个条件有一个问题特别值得单拎出来讲模块编译用的内核配置和目标运行内核配置不一致会导致很多莫名其妙的行为。比如你开了某款芯片的某些功能但运行内核的config里根本没开probe根本不会触发。所以做驱动开发一定要确认你是不是在用目标内核的源码树在编模块而不是拿了另一个版本的内核头文件在凑合。5.2 运行时的崩溃与数据异常怎么追运行时的bug最让人头疼因为它不像编译错有明确的行号。我梳理几个高频场景一是内核崩溃panic/Oops。dmesg里会留下调用栈用addr2line把地址翻译回源码行号是定位bug的第一步。很多时候问题出在空指针解引用或者访问了已释放的内存检查最近改动的代码里是否有释放后使用use-after-free的问题。二是数据错乱。如果驱动里访问了用户态指针或者没有正确使用copy_from_user就会有偶发的数据错误。这类问题的排查思路是先确认驱动里有没有直接解引用用户指针有就全部改成安全拷贝接口。三是并发问题导致的不稳定复现。这类bug最阴险往往跑十次挂一次加锁之后又不好复现。解决办法是用KASAN内存越界检测和KCSAN数据竞争检测这两个内核工具能在你完全无头绪的情况下快速圈定问题范围。调试内核问题用好内核自带的检测工具会事半功倍。5.3 一些文档里不会写的实操细节我额外总结几条平时不太能查到的细节都是实实在在换来的教训第一内核打印日志的级别要善用。开发调试用pr_debug加动态调试平时代码合并时不要留一堆pr_info刷屏否则dmesg里全是噪声真出问题时关键信息反而不容易被看到。第二、驱动里的延时要注意上下文。在进程上下文可以用msleep但在中断上下文或持有自旋锁时绝对不能用会睡眠的延时只能使用忙等的udelay、ndelay。不然系统直接崩溃或者在RT内核环境下被调度器警告。第三、在设备树里配置的寄存器地址、中断号一定要和芯片datasheet反复核对。我见过太多因为地址偏移多了一个0、中断号搞反导致设备怎么都点不亮的问题。这种问题排查起来最费时间所以写设备树的时候就得细心。第四、模块加载的依赖关系要理清。有些驱动依赖其他模块提供的符号insmod时要按依赖顺序加载否则会报Unknown symbol。这种问题用modprobe通常能自动解决但手动insmod时容易忽略。6. 面试考点梳理与进阶路线图既然说这个岗位高薪那高薪的岗位必然有对应的面试门槛。很多人在技术面试环节就挂了是因为只记住了代码模板不理解背后的原理。下面按我的经验梳理一下面试官比较看重的几类问题以及画一条从入门到进阶的学习路径。6.1 面试官最关注的知识块与典型问题我先列一个高频问题清单你会发现这些问题的考察点非常集中字符设备驱动的注册流程是什么alloc_chrdev_region、cdev_add、class_create、device_create每一步做了什么内核态和用户态如何交换数据为什么不能直接访问用户指针自旋锁和互斥锁的区别是什么什么场景用什么什么是中断上下文什么是进程上下文两者有什么区别内核内存分配有哪几种方式kmalloc和vmalloc的区别是什么设备树的作用是什么probe函数是如何被调用的什么是platform驱动模型它解决什么问题一个驱动的open/read/write调用链从应用层到硬件发生了什么面试官问这些问题核心是在考察你的心智模型是不是清晰。为什么这样讲你仔细想如果你能从头到尾说清楚一个设备从开机枚举到应用程序read数据的整个过程那你对设备模型、总线、驱动、文件系统、系统调用的理解一定是成体系的而不是只会照着模板抄代码。所以真正准备面试的方式不是背答案而是把一条主线彻底想透。6.2 从字符设备到驱动工程师的进阶路径我给新人的建议是分四步走第一步把字符设备驱动练到熟。能独立完成设备号分配、cdev注册、file_operations实现、应用层联调把read/write/ioctl这些接口都跑通这是所有后续基础。第二步搞懂设备树和platform驱动。现代Linux基本都是device和driver分离的设计设备树描述硬件信息驱动只负责匹配和操作。能用设备树描述一个外设并用platform驱动框架完成驱动开发才是真正进入现代主流的开发方式。第三步理解中断、内核并发与内存管理。中断上下半部、tasklet、workqueue、自旋锁、互斥锁、kmalloc与GFP标志这些概念决定了你写的驱动在复杂场景下稳不稳定。这一阶段可以多研究现有子系统的实现比如input子系统、gpio子系统、regmap框架看内核老手是怎么组织代码的。第四步选择一个细分方向深耕。比如网络驱动、音频驱动、存储驱动、显示驱动、USB/PCIe驱动每个方向都有大量独有的知识和经验。真正的高薪offer不是因为你什么都会一点而是在某个方向上有足够深的积累能在别人搞不定的现场问题上给出解决方案。7. 最后分享两个我坚持了多年的习惯延伸一下我认为对技术成长比较重要的两个工作习惯。第一个习惯是代码中写注释时不只写这段代码做了什么更要写为什么这样做。原因在于驱动代码的运行环境太隐蔽没有调试器的时候下一任维护者需要靠注释去理解intent。只在注释里说初始化寄存器完全不解释为什么设这个值、为什么时序要这样安排等于没写。第二习惯是遇到一个诡异的内核问题先不要急着改代码先把现场信息完整地记录下来——dmesg、/proc下的关键文件、设备树节点、硬件版本、内核版本、复现步骤这些信息齐全之后再动手排查往往能少走很多弯路。驱动开发是个越做越有味道的方向。你可能不会像做上层应用那样每天都在聊业务、改页面但你能真真切切感觉到自己写的代码在操纵硬件让一个设备从死的状态被激活、通信、工作。这种对底层运行机制的主导感是这个岗位最大的魅力。如果你正在学Linux不妨从写一个最简单的字符设备开始哪怕只是让一个LED亮起来你的技能树也会从此不一样。因为我当年就是这么入坑的。