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 主设备号的分配方式

主设备号有两种分配方式:

  1. 静态分配:开发者手动指定一个未被占用的主设备号(如 240)。适用于私有驱动或实验场景。
  2. 动态分配:调用 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

设备文件、设备号、驱动三者之间的绑定关系如下:

  1. 设备文件通过 mknod 或 udev 关联一个主设备号和次设备号。
  2. 内核根据主设备号在已注册的驱动链表中查找对应的 cdev 结构体。
  3. cdev 结构体中保存了 file_operations 指针,指向驱动实现的回调函数。
  4. 用户对设备文件执行 open/read/write 时,VFS 通过上述链路最终调用驱动函数。
关键结论:设备文件是“门牌号”,设备号是“楼层+房间号”,驱动是“房间里的服务人员”。用户通过门牌号找到房间,房间号告诉系统该找哪个服务人员。

2.3.3 绑定关系流程图

设备文件与驱动绑定关系图 设备文件 → 设备号 → 驱动 → file_operations 绑定链路 用户空间 open("/dev/my_char") read(fd, buf, len) write(fd, buf, len) VFS(虚拟文件系统) 根据 inode 获取 dev_t (主:次) 内核驱动层(根据主设备号查找 cdev) file_operations { .open, .read, .write, .release } 驱动实现的回调函数被调用

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 可验证设备号注册与设备文件创建是否正确。