4. 字符设备驱动回顾
在进入 Linux Platform 驱动实战之前,有必要把字符设备驱动的核心骨架重新梳理一遍。Platform 驱动最终仍要落到“用户空间如何访问硬件”这一根本问题上,而字符设备正是最基础、最直接的访问通道。理解主次设备号、file_operations、cdev 以及设备节点的创建过程,是后续将 Platform 设备与字符设备结合、实现 /dev 下可操作节点的前提。
一、主次设备号:内核识别设备的“身份证”
Linux 内核通过设备号区分不同的设备驱动与具体设备实例。设备号是一个 32 位整数,由主设备号(Major)和次设备号(Minor)组成。主设备号标识“由哪个驱动负责”,次设备号标识“该驱动管理的第几个设备”。例如 /dev/ttyS0 与 /dev/ttyS1 通常共享同一个主设备号,但次设备号不同。
内核中使用 dev_t 类型表示设备号,并提供了一组宏进行拆分与合成:
#include <linux/kdev_t.h>
dev_t dev = MKDEV(major, minor); // 合成设备号
unsigned int major = MAJOR(dev); // 提取主设备号
unsigned int minor = MINOR(dev); // 提取次设备号
设备号有两种分配方式:静态指定和动态分配。静态指定使用 register_chrdev_region(),需要开发者自己选定一个未被占用的主设备号;动态分配使用 alloc_chrdev_region(),由内核返回可用主设备号,更适合现代驱动开发,可避免冲突。释放时统一使用 unregister_chrdev_region()。
alloc_chrdev_region() 动态申请设备号,再通过 cat /proc/devices 查看内核实际分配的主设备号,避免与已有驱动冲突。
二、file_operations:用户空间与驱动的函数契约
file_operations 是字符设备驱动的核心结构体,它把用户空间的文件操作(open、read、write、ioctl、release 等)映射到驱动中的具体函数。用户空间调用 open("/dev/xxx") 时,内核最终会找到该设备对应的 file_operations,并调用其中的 .open 成员。
static int demo_open(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "demo_open\n");
return 0;
}
static ssize_t demo_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
return simple_read_from_buffer(buf, count, ppos,
"hello\n", 6);
}
static const struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = demo_open,
.read = demo_read,
.write = demo_write,
.release = demo_release,
};
其中 .owner = THIS_MODULE 用于模块引用计数,防止驱动在使用中被卸载。用户空间与内核空间的数据拷贝必须使用 copy_to_user() 与 copy_from_user(),不能直接解引用用户指针。
三、cdev:把 file_operations 注册进内核
仅有 file_operations 还不够,内核需要知道“哪个设备号对应哪组操作”。struct cdev 就是连接设备号与 file_operations 的桥梁。典型流程如下:
- 定义并初始化
struct cdev,可用cdev_init()绑定file_operations。 - 设置
cdev.owner = THIS_MODULE。 - 调用
cdev_add()将 cdev 加入内核,并指定设备号与设备数量。 - 模块卸载时调用
cdev_del()注销。
static struct cdev demo_cdev;
static dev_t demo_dev;
static int __init demo_init(void)
{
alloc_chrdev_region(&demo_dev, 0, 1, "demo");
cdev_init(&demo_cdev, &demo_fops);
demo_cdev.owner = THIS_MODULE;
cdev_add(&demo_cdev, demo_dev, 1);
return 0;
}
此时内核已经具备处理该设备号请求的能力,但 /dev 目录下还没有对应节点,用户空间仍无法直接访问。
四、设备节点创建与用户空间交互
设备节点是用户空间访问字符设备的入口。传统方式是手动执行 mknod:
mknod /dev/demo c 240 0
# c 表示字符设备,240 为主设备号,0 为次设备号
现代驱动更推荐在驱动中自动创建设备节点,流程为:class_create() 创建类,device_create() 创建设备,内核通过 udev/mdev 自动在 /dev 下生成节点。卸载时依次调用 device_destroy() 与 class_destroy()。
用户空间交互的完整链路可以概括为:应用程序调用 open/read/write/ioctl → 系统调用进入 VFS → VFS 根据设备号找到 cdev → cdev 找到 file_operations → 调用驱动函数 → 驱动通过 copy_to_user/copy_from_user 与用户空间交换数据。
五、常见误区与复盘要点
| 误区 | 现象 | 正确做法 |
|---|---|---|
| 只注册 cdev,不创建设备节点 | /dev 下无节点,应用无法 open | 使用 class_create + device_create 自动创建 |
| 直接解引用用户指针 | 内核 oops 或数据错误 | 必须使用 copy_to_user / copy_from_user |
| 忘记设置 .owner | 模块卸载时引用计数异常 | file_operations 中设置 .owner = THIS_MODULE |
| 设备号冲突 | insmod 失败,提示 busy | 使用 alloc_chrdev_region 动态分配 |
| 卸载时未释放资源 | 再次加载失败或内核告警 | 依次 cdev_del、device_destroy、class_destroy、unregister_chrdev_region |
回顾整条链路:设备号让内核找到驱动,file_operations 定义驱动能做什么,cdev 把两者绑定并注册进内核,设备节点让用户空间有入口,最终通过系统调用完成数据交互。这套机制是字符设备驱动的骨架,也是后续 Platform 驱动实现 /dev 节点、对接真实硬件的基础。掌握它,才能在 Platform 总线的框架下把设备、驱动与用户空间顺畅地串联起来。