2. 字符设备驱动基础:字符设备的概念、主设备号与次设备号、设备文件与驱动的关系
字符设备驱动是 Linux 驱动开发中最基础、最核心的模块。本章将深入剖析字符设备的本质、设备号的编码机制以及设备文件与驱动之间的绑定关系,为后续实现完整的字符设备驱动打下坚实的理论基础。
2.1 字符设备的概念与特征
字符设备(Character Device)是 Linux 三大设备类型之一(另两类为块设备和网络设备)。它以字节流的形式进行数据传输,数据按顺序、逐个字节地读写,不支持随机访问(即不能像块设备那样通过扇区号直接定位数据块)。
核心特征:字符设备的数据流是串行的、无缓冲的,每次读写操作通常对应一次硬件交互或数据拷贝。
| 对比维度 | 字符设备 | 块设备 |
|---|---|---|
| 数据访问方式 | 字节流,顺序读写 | 块(512B/4KB),随机访问 |
| 缓冲机制 | 无内核缓冲(直接 I/O) | 有页缓存/块缓冲 |
| 典型设备 | 串口、键盘、鼠标、GPIO | 硬盘、U盘、SD卡 |
| 设备文件类型 | c(character) |
b(block) |
| 驱动接口 | file_operations 结构体 |
block_device_operations |
字符设备驱动通过实现 file_operations 结构体中的回调函数(如 open、read、write、release)来与用户空间交互。每次系统调用都会直接触发驱动中的对应函数。
2.2 主设备号与次设备号
Linux 使用设备号来唯一标识一个设备。设备号是一个 32 位的无符号整数,分为两部分:主设备号(Major Number)和次设备号(Minor Number)。
- 主设备号:标识设备对应的驱动程序。内核通过主设备号找到正确的驱动。
- 次设备号:由驱动内部使用,用于区分同一驱动管理的多个物理或逻辑设备。
类比理解:主设备号相当于“图书馆的楼层号”,次设备号相当于“该楼层的书架编号”。同一楼层(同一驱动)可以有多个书架(多个设备实例)。
2.2.1 设备号的编码与宏
设备号在内核中定义为 dev_t 类型(32 位)。高 12 位为主设备号,低 20 位为次设备号。内核提供了便捷的宏进行编码和解码:
#include <linux/types.h> // dev_t
#include <linux/kdev_t.h> // 宏定义
// 从主、次设备号合成 dev_t
dev_t dev = MKDEV(major, minor);
// 从 dev_t 中提取主设备号
unsigned int major = MAJOR(dev);
// 从 dev_t 中提取次设备号
unsigned int minor = MINOR(dev);
| 宏/函数 | 作用 | 示例 |
|---|---|---|
MKDEV(major, minor) |
合成设备号 | MKDEV(240, 0) → 0xF0000000 |
MAJOR(dev) |
提取主设备号 | MAJOR(0xF0000000) → 240 |
MINOR(dev) |
提取次设备号 | MINOR(0xF0000000) → 0 |
2.2.2 主设备号的分配方式
主设备号有两种分配方式:
- 静态分配:开发者手动指定一个未被占用的主设备号(如
240)。适用于私有驱动或实验场景。 - 动态分配:调用
alloc_chrdev_region()让内核自动分配未使用的主设备号。推荐用于正式产品,避免冲突。
避坑提示:静态分配的主设备号需要查询
/proc/devices 确认未被占用。动态分配的主设备号每次加载驱动可能不同,需通过 cat /proc/devices 查看。
2.3 设备文件与驱动的绑定关系
用户空间无法直接与内核驱动通信,必须通过设备文件(Device File)作为桥梁。设备文件位于文件系统中(通常为 /dev 目录),是用户态与驱动交互的入口点。
2.3.1 设备文件的创建
设备文件使用 mknod 命令创建,或由 udev 自动生成:
# 手动创建设备文件(需要 root 权限)
mknod /dev/my_char_device c 240 0
# 参数说明:
# c → 字符设备类型
# 240 → 主设备号
# 0 → 次设备号
使用 ls -l 查看设备文件时,第一个字符为 c 表示字符设备,b 表示块设备:
$ ls -l /dev/my_char_device
crw-r--r-- 1 root root 240, 0 Jan 15 10:30 /dev/my_char_device
2.3.2 绑定关系:设备号 → 驱动 → file_operations
设备文件、设备号、驱动三者之间的绑定关系如下:
- 设备文件通过
mknod或udev关联一个主设备号和次设备号。 - 内核根据主设备号在已注册的驱动链表中查找对应的
cdev结构体。 cdev结构体中保存了file_operations指针,指向驱动实现的回调函数。- 用户对设备文件执行
open/read/write时,VFS 通过上述链路最终调用驱动函数。
关键结论:设备文件是“门牌号”,设备号是“楼层+房间号”,驱动是“房间里的服务人员”。用户通过门牌号找到房间,房间号告诉系统该找哪个服务人员。
2.3.3 绑定关系流程图
2.4 设备号的注册与注销
在驱动初始化时,需要向内核注册设备号。Linux 提供了两组 API:
2.4.1 静态注册(指定主设备号)
#include <linux/fs.h>
int register_chrdev_region(dev_t from, unsigned count, const char *name);
// from: 起始设备号(MKDEV(major, first_minor))
// count: 要注册的次设备号数量
// name: 设备名称(显示在 /proc/devices)
// 返回值:0 成功,负值失败
2.4.2 动态注册(自动分配主设备号)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
// dev: 输出参数,返回分配的设备号
// baseminor: 起始次设备号
// count: 次设备号数量
// name: 设备名称
// 返回值:0 成功,负值失败
2.4.3 注销设备号
void unregister_chrdev_region(dev_t from, unsigned count);
// 在驱动卸载时调用,释放设备号
最佳实践:推荐使用
alloc_chrdev_region() 动态分配,避免主设备号冲突。在 /proc/devices 中查看分配结果后,再手动或通过 udev 创建设备文件。
2.5 查看设备号与设备文件
以下命令和文件用于调试和查看设备号信息:
| 命令/文件 | 作用 | 示例输出 |
|---|---|---|
cat /proc/devices |
查看已注册的设备号与驱动名称 | 240 my_char_driver |
ls -l /dev |
查看设备文件的主、次设备号 | crw------- 1 root root 240, 0 ... |
stat /dev/my_char |
查看设备文件的详细属性 | Device: 0,0 Inode: 12345 ... |
mknod /dev/xxx c major minor |
手动创建设备文件 | — |
2.6 本章小结
- 字符设备以字节流方式顺序读写,无内核缓冲,适合串口、GPIO 等设备。
- 设备号由主设备号(高 12 位)和次设备号(低 20 位)组成,使用
MKDEV/MAJOR/MINOR宏操作。 - 设备文件是用户空间与驱动交互的入口,通过设备号绑定到具体的
cdev和file_operations。 - 推荐使用
alloc_chrdev_region()动态分配设备号,避免冲突。 - 通过
/proc/devices和ls -l /dev可验证设备号注册与设备文件创建是否正确。