驱动开发

Linux驱动开发:字符设备驱动

从字符设备模型、file_operations、模块加载,到 RK3568 平台上板测试,梳理一个最小字符设备驱动的完整开发路径。

2026-04-28 30 分钟
  • RK3568
  • Linux Driver
  • Character Device
  • Kernel

为什么从字符设备开始

字符设备驱动是 Linux 驱动开发里最适合作为起点的一类模型。它足够简单,但又能把驱动开发中最关键的几件事串起来:

  • 驱动模块的加载与卸载
  • 设备号的管理
  • file_operations 的设计
  • 用户态与内核态之间的数据交互

如果这条链路理解清楚,后面继续学 platform 驱动、设备树匹配、中断和总线模型会顺很多。

先理解应用层和驱动层的关系

在 Linux 中,应用程序最终是通过操作 /dev 下的设备节点与驱动交互的。对于一个字符设备,最典型的调用链就是:

  • open
  • read
  • write
  • close

应用层看到的是文件接口,内核侧真正实现的是 struct file_operations 中对应的函数。

这也是字符设备驱动最值得练习的地方:你写的不是一个“抽象概念”,而是一组明确可映射到用户接口的内核函数。

应用层与驱动层调用关系示意

file_operations 该怎么看

第一次看到 struct file_operations 往往会觉得很长,但实际开发不需要一次理解全部成员。对一个最小可运行字符设备来说,先抓住四个核心函数就够了:

  • open
  • release
  • read
  • write

等到需要支持阻塞/非阻塞、ioctlpollmmap 时,再逐步扩展。

这种“先做最小闭环,再扩展能力”的方式,在驱动学习里非常重要。

模块加载和卸载做了什么

Linux 驱动常见有两种形态:

  • 编译进内核
  • 编译为可加载模块 .ko

对于学习和调试来说,模块形式更方便,因为你可以反复:

  • insmod / modprobe
  • 查看日志
  • rmmod
  • 修改后重新编译加载

对应入口通常就是:

module_init(xxx_init);
module_exit(xxx_exit);

可以把它理解成模块生命周期的起点和终点。

字符设备注册的两层含义

字符设备“注册”这件事,至少包含两层语义:

  1. 告诉内核:这是一个字符设备驱动
  2. 申请并绑定设备号,建立驱动与设备节点之间的映射关系

学习初期常见写法是直接使用 register_chrdev。它上手快,能迅速跑通流程;但从长期来看,也要理解 alloc_chrdev_region 这类更标准的设备号管理方式。

inodefileopen 里为什么重要

这是很多初学驱动时容易忽略,但又非常关键的点。

open 阶段,inode 主要可以帮助我们:

  • 获取主次设备号
  • 找到当前字符设备对应的 cdev
  • 进一步反推到自定义设备结构体

然后再把这个设备结构体保存到 filp->private_data 中,后续 read / write 就能直接复用。

这一步本质上是在做“设备上下文传递”。如果理解了这个思路,很多更复杂的驱动框架都会更容易读懂。

这次实战做了一个什么驱动

我写的是一个最小字符设备驱动:

  • 在内核中分配一个缓冲区
  • 允许用户态向缓冲区写入数据
  • 允许用户态从缓冲区读取数据

这个例子虽然简单,但它完整覆盖了:

  • 驱动注册
  • 设备读写
  • 用户态/内核态数据拷贝
  • 上板验证

最关键的两个数据拷贝函数

用户态和内核态不能直接互相访问对方内存,因此字符设备读写一定会频繁碰到:

  • copy_to_user
  • copy_from_user

这是字符设备驱动最基本、也最容易因为粗心出问题的部分。除了记住函数本身,更要养成几个习惯:

  • 明确缓冲区长度
  • 检查返回值
  • 不要默认用户传入的参数一定合法

在 RK3568 上板测试时我关注什么

驱动编译成 .ko 后,我会把它放到目标板对应的模块目录下,再通过 modprobe 加载,然后检查以下几件事:

  • 模块是否成功加载
  • 设备节点是否创建成功
  • 写入操作是否真的进入内核缓冲区
  • 读取结果是否符合预期
  • 卸载后节点与驱动状态是否一致

驱动编译生成 ko 文件

一个比较容易让初学者困惑的点是:驱动卸载了,为什么 /dev 下的节点还在?

原因很简单,因为如果这个节点是通过 mknod 手动创建的,它本身就是文件系统里的一个特殊文件,不会随着模块卸载自动消失。除非你显式删除,或者在驱动里补齐自动创建设备类与销毁逻辑。

上板验证结果

下面这几张图分别对应:创建设备节点、向缓冲区写入数据、从缓冲区读取数据。

创建设备节点

向字符设备写入测试数据

从字符设备读取测试数据

这份最小例子的价值在哪里

它最大的价值不是“可以读写一段字符串”,而是帮你建立一套驱动开发的骨架认知:

  1. 先有模块入口和出口
  2. 再有字符设备注册
  3. 再补齐 file_operations
  4. 最后通过用户态程序验证整个调用链

这套思路以后完全可以迁移到:

  • GPIO 设备控制
  • LED 驱动
  • 按键输入
  • 更复杂的平台设备驱动

小结

如果把字符设备驱动理解成 Linux 驱动开发的第一块拼图,那么这块拼图的核心并不是某个 API,而是三件事:

  • 明白用户态如何进入内核态
  • 明白设备号和驱动如何关联
  • 明白设备上下文如何在 open/read/write 之间传递

一旦这三件事理顺,后续学习内核驱动就会从“记笔记”逐渐变成“能自己搭框架”。