本节摘要:一条写入命令怎样从终端一路走到 GPIO 寄存器。本节逐站拆解用户态命令、系统调用、VFS、字符设备驱动、硬件寄存器这五段路径,讲清每一站的数据形态与职责边界,为全册建立一张总地图。
站在开发板的终端前,敲下一行命令,板子上的灯应声而亮。这个瞬间由五段路径拼接而成:shell 解析命令、进程调用库函数、CPU 陷入内核、VFS 找到驱动、驱动改写寄存器。本节是全书的地基——后面七章里每一个主题,字符设备、中断、设备树、调试,都能挂在这条路径的某一站上。
你在终端里输入的命令本身就是一个进程。以向 LED 设备写入一个字符为例,用户态大致经历三步:shell 把参数交给目标程序;目标程序调用 C 库的写入函数;库函数把文件描述符、缓冲区指针、长度打包,请求内核服务。注意此时还没有任何一行驱动代码被执行——用户态只负责"把话说清楚"。
用系统调用跟踪工具观察这个过程,输出通常长这样:
$ strace -e write sh -c 'echo 1 > /dev/led0' openat(AT_FDCWD, "/dev/led0", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 fchmod(3, 0666) = 0 write(3, "1\n", 2) = 2 close(3) = 0 exit_group(0) = ?
这份跟踪记录信息量很大:打开设备文件得到描述符 3;写入两个字节,返回值 2 表示内核已接收;关闭描述符。对应用来说,LED 与普通文件没有区别——这正是 Unix"一切皆文件"哲学的落点,也是驱动开发者要兑现的承诺。
💡 关键直觉:设备文件是驱动递给用户态的名片。用户态拿着名片提请求,真正干活的是名片背后的驱动。
库函数触发一条陷入指令,CPU 从用户模式切到内核模式。这次切换做了三件事:保存用户态现场(寄存器压栈)、校验传入参数的合法性、跳转到内核的系统调用入口。此后控制权交给了内核。
这里有一个驱动开发者必须刻在脑子里的细节:用户传来的指针不可直接信任。用户缓冲区可能为空、可能指向无映射区域、也可能在系统调用执行到一半时被另一个线程改掉。所以内核提供了专用拷贝函数,在拷贝时逐页校验,失败则返回错误而不是崩溃。第 2 章实现 write 回调时会亲手用到它们。
进入内核后,请求先到达虚拟文件系统。VFS 是内核给所有"像文件的东西"画的统一接口:它拿到描述符 3,查到对应的索引节点,发现这是一个字符设备,主设备号指向我们已经注册的 LED 驱动。于是 VFS 调用该驱动挂在 file_operations 结构里的 write 函数——从这一刻起,舞台交给了驱动。

驱动的 write 回调被调用时,它面对的是四个参数:设备实例、用户缓冲区指针、长度、位置偏移。它的工作分三步:用安全拷贝函数把用户数据搬进内核;解析数据含义(字符 1 表示开灯);把语义翻译成对 GPIO 控制器的寄存器写入。写寄存器这一步,靠的是启动时映射好的 I/O 内存地址——物理地址到虚拟地址的翻译早在驱动初始化阶段就完成了,第 3 章会展开。
一个值得体会的对照:前四站里,每一站都只做自己那一小段翻译。用户态说"我要写文件",VFS 说"这是字符设备",驱动说"写 1 就是拉高引脚",硬件说"电平高了灯就亮"。驱动的本质工作就是最后一层翻译——把内核世界的抽象请求,翻译成硬件世界听得懂的电平操作序列。
GPIO 控制器内部有一组寄存器:方向寄存器决定引脚是输入还是输出,数据寄存器决定输出电平。驱动写入数据寄存器后,控制器在下一个时钟沿把引脚驱动到目标电平,LED 的供电回路导通,灯亮。整个过程没有软件参与——硬件按自己的时序规则行事,这也是驱动必须尊重硬件时序的原因,第 6 章讲延迟与定时时会再次遇到。
把路径拆开看的另一个收益,是排错时能快速定位责任段。常见故障与对应站点:
| 症状 | 责任站点 | 典型原因 |
|---|---|---|
| 打开设备报"不存在" | 设备节点 | 驱动未注册或节点未创建 |
| 写入返回错误码 | 驱动回调 | 拷贝用户数据失败或参数非法 |
| 写入成功但灯不亮 | 寄存器映射 | 映射地址错误或引脚配置不对 |
| 间歇性亮灭不稳 | 硬件时序 | 上下电时序未满足、去抖缺失 |
| 系统随写入死机 | 驱动并发 | 回调中睡眠或非法访问 |
第 7 章的排错实录里有两个案例就取自这张表。现在你只需要记住方法:先确定命令死在哪一站,再谈修复。
把旅程走通之后,值得回头看一眼每一段的时间量级,建立对系统的"延迟直觉":命令解析与库函数包装在微秒级;系统调用陷入与返回合计约一微秒上下;VFS 路径查找有目录项缓存加持,通常在微秒级;驱动的回调执行看复杂度,简单寄存器操作亚微秒;硬件响应由器件决定,GPIO 翻转在纳秒到微秒之间。整条链路的主导项往往是第一站的进程调度与输入输出缓冲——这也是为什么第 2 章实现驱动时,write 返回后灯"看起来立刻"就亮了:链路本身远快于人眼感知。
但同一张账单在别的场景会完全改观:网络卡的收包路径上,一次中断加上软中断处理是微秒级,可一旦流量把队列打满,尾延迟会以毫秒计;块设备的写入要过页缓存与请求队列,用户态"写入成功"到数据真正落盘之间隔着一次刷新周期。五段旅程是骨架,每段的时长由设备类型与框架决定——第 5 章讲吞吐框架时,这张账单会重新细算一遍。
追问一:写入返回正值,就代表硬件已经完成动作了吗? 对 LED 这类简单设备,回调同步完成寄存器写入,返回时动作确实已经发生。但对慢速设备(闪存编程、网络发送),回调可能只是"把请求挂进队列",真正的完成由中断异步通知——这是第 3 章中断与第 5 章吞吐框架要展开的世界,现在先立一个警惕:返回值只承诺"内核已接单"。
追问二:一次写入多个字节,驱动必须全收吗? 不必。回调可以只消费部分字节并返回消费量,这叫短写,用户态库会据此决定是否续写。LED 驱动只认第一个字符,返回消费一字节与返回请求全长都是合法选择,但行为要自洽且写进文档——上层脚本依赖哪种语义,就稳定提供哪种。
追问三:点个灯而已,为什么非要进内核,用户态程序直接操作硬件不行吗? 在通用操作系统上不行,原因是双重的。其一,用户态地址空间里根本看不见设备的物理地址——MMU 的地址翻译会把这个访问直接拦下,这是 1.2 节要展开的隔离机制;其二,就算绕开隔离,多个进程同时抢一个寄存器、设备中断无人应答,系统立刻失序。内核驱动存在的意义正是把"独占硬件"收编为"受调度、有权限、可并发的公共服务"。不过这个问题问得很有前瞻性——第 8 章的 UIO 与 VFIO 会展示,在特定约束下"驱动搬回用户态"如何成为可能,那是这条旅程的远端风景。
下一节把镜头对准路径中那条最关键的界线——用户态与内核态的分界,看看驱动代码到底活在一个什么样的世界里。