文章目录
Linux中多设备驱动管理的实现(container_of访问的优点)
Linux操作系统中的应用大致可以分为用户应用和内核应用,分别运行在系统划分的用户空间和内核空间。用户空间应用运行在受限的环境中,具有较低的权限,无法直接访问系统的关键资源,这样可以避免因用户程序的错误或恶意行为影响到整个系统的稳定性。而内核空间应用则拥有最高的权限,能够直接操作硬件设备、管理系统内存等。
这两个空间是相互隔离的,应用程序无法直接访问内核空间的内存地址,也就不能直接调用内核函数。应用程序如果需要使用内核提供的功能,通常通过系统调用(System Call)来间接调用内核函数。系统调用是操作系统提供给用户程序的接口,它会触发一个从用户态到内核态的切换,让操作系统内核处理请求。例如,在C语言中,open、read、write等函数就是通过系统调用实现的。
众所周知Linux系统奉行“一切皆文件”原则,硬件设备驱动在Linux中同样被抽象为文件,统一由文件系统管理,这样应用程序对于硬件设备的操作,包括open、read、write等系统调用,实际上就是对由文件系统管理的设备文件的操作。
多设备管理
在 Linux 内核驱动开发中,一个驱动程序可以管理一个或多个设备实例。每个设备实例都有自己独立的状态和资源,驱动程序负责处理这些设备实例的各种操作。
- 设备号分配
- 设备数据结构定义
- 设备初始化
- 文件操作函数
- 驱动卸载
1. 设备号分配
需要为每个设备分配设备号,可采用静态分配(指定主设备号和次设备号)或动态分配(由内核自动分配主设备号)的方式。
静态分配
#define MAX_DEVICES 2 // 最大设备数量
#define MAJOR_NUM 11 // 主设备号
......
static dev_t devno;
......
static int __init multidev_init(void) {
int i, ret;
// 静态分配设备号
devno = MKDEV(MAJOR_NUM, 0);
ret = register_chrdev_region(devno, MAX_DEVICES, "multidev");
if (ret < 0) {
printk(KERN_ERR "Failed to register device numbers\n");
return ret;
}
......
}
`
动态分配
#define MAX_DEVICES 2 // 最大设备数量
......
static dev_t devno;
......
static int __init multidev_init(void) {
int i, ret;
// 动态分配设备号
ret = alloc_chrdev_region(&devno, 0, MAX_DEVICES, "multidev");
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device numbers\n");
return ret;
}
......
}
register_chrdev_region
与 alloc_chrdev_region
分别用于静态分配与动态分配,分配之前都要手动声明dev_t devno
变量
`
2. 设备数据结构定义
要定义一个设备数据结构,用来保存每个设备的私有信息,像设备号、字符设备对象、设备状态等。
// 设备私有数据结构
struct multidev {
struct cdev cdev;
int dev_id;
};
static dev_t devno;
static struct multidev *devices[MAX_DEVICES];
设备私有数据结构用于存储每个设备特有的信息,这些信息对驱动程序的正常运行和功能实现至关重要。不同类型的设备,其私有数据结构包含的内容也有所不同,下面分类介绍常见的设备私有数据。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/spinlock.h>
#include <linux/mutex.h>
#include <linux/atomic.h>
#define BUFFER_SIZE 1024
// 设备私有数据结构
struct example_dev {
//通用设备信息
dev_t devno; // 设备号
struct cdev cdev; // 字符设备对象
int dev_id; // 设备 ID ,用于区分同一驱动管理的多个设备
//硬件相关信息
void __iomem *reg_base; // 寄存器虚拟地址
int irq_num; // 中断号
int dma_channel; // DMA 通道号
//设备状态信息
atomic_t open_flag; // 打开标志
char buffer[BUFFER_SIZE]; // 数据缓冲区
size_t buffer_len; // 缓冲区数据长度
spinlock_t lock; // 自旋锁
struct mutex mutex_lock; // 互斥锁
//设备配置信息
unsigned int baud_rate; // 波特率
int work_mode; // 工作模式
};
3. 设备初始化
在驱动初始化函数里,对所有设备进行初始化,包括分配设备号、初始化字符设备对象、将字符设备添加到内核等操作。
for (i = 0; i < MAX_DEVICES; i++) {
devices[i] = kmalloc(sizeof(struct multidev), GFP_KERNEL);
if (!devices[i]) {
printk(KERN_ERR "Failed to allocate memory for device %d\n", i);
ret = -ENOMEM;
goto fail;
}
devices[i]->dev_id = i;
cdev_init(&devices[i]->cdev, &fops);
devices[i]->cdev.owner = THIS_MODULE;
ret = cdev_add(&devices[i]->cdev, MKDEV(MAJOR(devno), MINOR(devno) + i), 1);
if (ret) {
printk(KERN_ERR "Failed to add device %d\n", i);
kfree(devices[i]);
goto fail;
}
}
return 0;
4. 文件操作函数
实现 file_operations 结构体中的文件操作函数,像 open、read、write、release 等,这些函数要能处理不同设备的请求。
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = multidev_open,
.release = multidev_release,
};
static int multidev_open(struct inode *inode, struct file *filp) {
struct multidev *dev = container_of(inode->i_cdev, struct multidev, cdev);
filp->private_data = dev;
printk(KERN_INFO "Device %d opened\n", dev->dev_id);
return 0;
}
container_of宏
在 open
函数里使用 container_of
宏定位私有数据而非直接访问,主要出于数据封装、代码通用性、遵循内核设计规范和内存布局灵活等多方面考量,下面详细展开说明。
数据封装与模块化
- 直接访问的问题:若直接访问私有数据,意味着将设备的内部状态和数据暴露在外。当代码规模变大时,不同功能模块可能会随意修改这些数据,破坏数据的完整性和一致性,也会让代码的耦合度增加,不利于维护和扩展。
container_of
的优势:借助container_of
宏,能把设备私有数据封装在自定义结构体中。在open
函数里,通过该宏获取私有数据结构体指针,进而访问内部数据。这样其他模块只能通过驱动提供的接口访问私有数据,增强了数据的封装性和模块化程度
代码通用性
- 直接访问的局限:直接访问私有数据的代码会和特定的设备结构体紧密绑定。若要开发新的设备驱动,即便功能相似,也得重新编写大量访问私有数据的代码,缺乏通用性。
container_of
的灵活:container_of
宏是通用的,无论设备私有数据结构体如何定义,只要包含struct cdev
成员,就能用该宏获取私有数据结构体指针。这使得代码可移植性更强,不同驱动间能复用获取私有数据的逻辑。
遵循内核设计规范
- 内核接口的要求:内核的
struct inode
结构体只提供了i_cdev
成员来表示字符设备,并未直接关联设备私有数据。i_cdev
是通用的字符设备表示,为保证内核接口的通用性和一致性,没有直接暴露设备私有数据。 container_of
的适配:使用container_of
宏能依据i_cdev
成员,从struct cdev
指针推导出设备私有数据结构体的指针,符合内核的设计规范,便于与内核其他模块交互。
内存布局灵活
- 直接访问的限制:直接访问私有数据要求设备私有数据在内存中的布局固定不变。一旦结构体成员顺序或偏移量改变,所有直接访问的代码都要修改。
container_of
的适应:container_of
宏依据struct cdev
成员在结构体中的偏移量来计算私有数据结构体的起始地址。即便结构体成员发生变化,只要struct cdev
成员位置不变,获取私有数据指针的代码就无需修改。
综上所述,使用 container_of
宏定位私有数据能提升代码的封装性、通用性、可维护性,同时遵循内核设计规范,适应不同的内存布局。
5. 驱动卸载
static void __exit multidev_exit(void) {
int i; // 移除所有设备
for (i = 0; i < MAX_DEVICES; i++) {
if (devices[i]) {
cdev_del(&devices[i]->cdev);
kfree(devices[i]);
}
} // 释放设备号
unregister_chrdev_region(devno, MAX_DEVICES);
}
6. 附 container_of宏的更多解释
学习的时候一直对为什么不直接访问私有数据,而是要通过container_of宏来访问存在疑惑,以下是Deepseek解释的container_of宏的优点
问:为什么在Linux驱动开发的open函数中要使用container_of宏来定位私有数据,而不是直接访问
Deepseek答:
驱动架构全景图
用户空间
├─ 进程A打开 /dev/multidev0
└─ 进程B打开 /dev/multidev1
│
V
内核空间
├─ VFS 层
│ ├─ 通过设备号查找 inode
│ └─ 获取对应的 struct cdev
│
└─ 驱动层
├─ multidev0: struct multidev
│ ├─ cdev (已注册到内核)
│ └─ dev_id=0
│
└─ multidev1: struct multidev
├─ cdev (已注册到内核)
└─ dev_id=1
关键交互流程
当用户空间打开设备时:
- VFS 查找:通过设备号找到对应的 inode
- cdev 定位:
inode->i_cdev
指向注册时关联的struct cdev
- 逆向推导:通过
container_of
从cdev
反推父结构体multidev
- 私有数据绑定:将计算得到的
multidev
指针存入filp->private_data
为什么必须用 container_of?
1. 内核对象绑定机制
你的设备初始化代码中,每个设备都完成了这样的绑定:
devices[i]->cdev = 初始化后的字符设备对象
cdev_add(&devices[i]->cdev, 设备号)
此时内核会建立:
设备号 <--> cdev <--> multidev 结构体
container_of
正是利用了这种绑定关系。
2. 内存关系示意图
假设设备实例的内存分布如下:
plainText
0x1000: struct multidev (设备0)
+---------------+
| cdev | <-- 被内核记录为设备0的字符设备
| dev_id = 0 |
+---------------+
0x2000: struct multidev (设备1)
+---------------+
| cdev | <-- 被内核记录为设备1的字符设备
| dev_id = 1 |
+---------------+
当用户访问设备1时:
- 内核通过设备号找到位于 0x2000 的 cdev
container_of
计算:multidev地址 = 0x2000 - offsetof(cdev在multidev中的偏移)
3. 对比直接访问的缺陷
若尝试用次设备号直接访问:
static int unsafe_open(struct inode *inode, struct file *filp) {
int minor = iminor(inode);
filp->private_data = devices[minor]; // 危险!
}
这会引发:
- 数组越界风险(如设备号被伪造)
- 无法验证设备有效性
- 需维护额外的设备号映射表
完整数据流验证
以打开 /dev/multidev1 为例:
- 用户空间调用 open()
- 内核解析设备号得到主设备号 M 和次设备号 1
- 查找到对应的 cdev 位于地址 0x2000
- 执行驱动中的 open 函数:
dev = container_of(0x2000, struct multidev, cdev)
// 计算得到 dev = 0x2000 - offsetof(cdev在multidev中的偏移)
// 最终指向完整的 multidev 结构体
- 通过 dev->dev_id 可以确认这是设备1
这种机制保证了:
- 100% 准确的设备实例定位
- 无需维护额外映射表
- 天然防止设备号伪造
- 完美匹配内核的设备管理机制
这就是 Linux 驱动开发中必须使用 container_of
的根本原因。