驱动-兼容不同设备-container_of
驱动兼容不同类型设备
在 Linux 驱动开发中,container_of 宏常被用来实现一个驱动兼容多种不同设备的架构。这种设计模式在 Linux 内核中非常常见,特别
是在设备驱动模型中。linux内核的主要开发语言是C,但是现在内核的框架使用了非常多的面向对象的思想,这就面临了一个用C语言
来实现面向对象编程的问题
文章目录
- 参考资料
- 典型应用场景
- 原理:利用结构体中元素指针获取结构体指针
- container_of 函数
- 理解
- C程序例子
- 实验
- 源码程序 file.c
- 源码分析
- 基本步骤
- 知识点
- container_of 当前使用分析
- read、write
- Makefile 编译脚本
- 测试程序app.c
- 加载驱动 insmod file.ko
- 生成的驱动设备
- 运行程序 ./app
- 总结
参考资料
在字符设备这块内容,所有知识点都是串联起来的,需要整体来理解,缺一不可,建议多了解一下基础知识
驱动-申请字符设备号
驱动-注册字符设备
驱动-创建设备节点
驱动-字符设备驱动框架
驱动-杂项设备
驱动-内核空间和用户空间数据交换
驱动-文件私有数据
典型应用场景
- 同一厂商的不同型号设备
- 功能相似但寄存器布局不同的设备
- 需要维护设备特定数据的场合
原理:利用结构体中元素指针获取结构体指针
Kobject是linux设备驱动模型的基础,也是设备模型中抽象的一部分。
linux内核为了兼容各种形形色色的设备,就需要对各种设备的共性进行抽象,抽象出一个基类,其余的设备只需要继承此基类就可以了。
而此基类就是kobject(暂且把它看成是一个类),但是C语言没有面向对象语法。
在C++中这样的操作非常简单,继承基类就可以了,而在C语言中需要将基类的结构体指针嵌入到派生的类中,那么为什么将基类指针嵌入就可以得到派生类的指针呢?
这个实现是一个宏:container_of。
container_of 函数
container_of 在 Linux 内核中是一个常用的宏, 用于从包含在某个结构中的指针获得结构本
身的指针, 通俗地讲就是通过结构体变量中某个成员的首地址进而获得整个结构体变量的首地
址。 那么可以使用这个函数获取不同设备的地址, 来对不同的设备进行操作, 从而一个驱动可
以兼容不同的设备。
container_of 函数原型:container_of(ptr,type,member)
函数作用:通过结构体变量中某个成员的首地址获取到整个结构体变量的首地址。
参数含义:ptr 是结构体变量中某个成员的地址。type 是结构体的类型member 是该结构体变量的具体名字
container_of 宏的作用是通过结构体内某个成员变量的地址和该变量名, 以及结构体类型。
找到该结构体变量的地址
理解
C程序例子
#include <stdio.h>struct Base{ //定义一个Base类int var;char *string;};struct Derived{ //定义一个派生类int var;struct Base base; //派生类中包含了struct Base类};#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER) //offset_of宏#define container_of(ptr, type, member) ({ \ //阉割版container_of,省去了类型检查void *__mptr = (void *)(ptr); \((type *)(__mptr - offsetof(type, member))); })struct Base *base_p; struct Derived test_derived; struct Derived *get;int main(){base_p = &test_derived.base; //赋值给指针printf("Derived addr = %x\n",(unsigned int)&test_derived);printf("=================================\n");get = container_of(base_p,struct Derived,base); //使用base_p指针获取派生类的首地址printf("get Derived addr = %x\n",(unsigned int)get);return 0;}
输出结果:
输出如下:
Derived addr = 601050
=================================
get Derived addr = 601050
源码分析:最重要的就是理解 container_of 函数
get = container_of(base_p,struct Derived,base); //使用base_p指针获取派生类的首地址
-
base_p :ptr 是结构体变量中某个成员的地址。 这个base_p 是怎么定义的 struct Base *base_p; Base 又是在哪里定义的呢? 作为派生类Deviced 的成员变量 struct Base base
-
struct Derived: type 是结构体的类型,不就是派生类的结构体类型吗? 这个函数就是要返回这个结构体类型的指针
-
base: member 是该结构体变量的具体名字。 就是派生类里面 定义的成员变量名字。
我们这里讨论和接下来讨论的目的就是要理解这个函数container_of的作用,理解参数和返回类型 以及理解实际应用的价值。
实验
使用 container_of 函数编写一个驱动兼容不同设备
源码程序 file.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>struct device_test
{dev_t dev_num; // 设备号int major; // 主设备号int minor; // 次设备号struct cdev cdev_test; // cdevstruct class *class; // 类struct device *device; // 设备char kbuf[32];
};struct device_test dev1; // 定义一个device_test结构体变量dev1
struct device_test dev2; // 定义一个device_test结构体变量dev2/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{dev1.minor = 0; // 设置dev1的次设备号为0dev2.minor = 1; // 设置dev2的次设备号为1// inode->i_rdev 为该 inode 的设备号,使用container_of函数找到结构体变量dev1 dev2的地址// 然后设置私有数据file->private_data = container_of(inode->i_cdev, struct device_test, cdev_test);printk("This is cdev_test_open\r\n");return 0;
}/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{struct device_test *test_dev = (struct device_test *)file->private_data;// 如果次设备号是0,则为dev1if (test_dev->minor == 0){if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据{printk("copy_from_user error\r\n");return -1;}printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);}// 如果次设备号是1,则为dev2else if (test_dev->minor == 1){if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据{printk("copy_from_user error\r\n");return -1;}printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);}return 0;
}/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{struct device_test *test_dev = (struct device_test *)file->private_data;if (copy_to_user(buf, test_dev->kbuf, strlen(test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据{printk("copy_to_user error\r\n");return -1;}printk("This is cdev_test_read\r\n");return 0;
}static int cdev_test_release(struct inode *inode, struct file *file)
{printk("This is cdev_test_release\r\n");return 0;
}/*设备操作函数,定义file_operations结构体类型的变量cdev_test_fops*/
struct file_operations cdev_test_fops = {.owner = THIS_MODULE, // 将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块.open = cdev_test_open, // 将open字段指向chrdev_open(...)函数.read = cdev_test_read, // 将open字段指向chrdev_read(...)函数.write = cdev_test_write, // 将open字段指向chrdev_write(...)函数.release = cdev_test_release, // 将open字段指向chrdev_release(...)函数
};static int __init chr_fops_init(void) // 驱动入口函数
{/*注册字符设备驱动*/int ret;/*1 创建设备号,,这里注册2个设备号*/ret = alloc_chrdev_region(&dev1.dev_num, 0, 2, "alloc_name"); // 动态分配设备号if (ret < 0){printk("alloc_chrdev_region is error\n");}printk("alloc_chrdev_region is ok\n");dev1.major = MAJOR(dev1.dev_num); // 获取主设备号dev1.minor = MINOR(dev1.dev_num); // 获取次设备号printk("major is %d \r\n", dev1.major); // 打印主设备号printk("minor is %d \r\n", dev1.minor); // 打印次设备号// 对设备1进行操作/*2 初始化cdev*/dev1.cdev_test.owner = THIS_MODULE;cdev_init(&dev1.cdev_test, &cdev_test_fops);/*3 添加一个cdev,完成字符设备注册到内核*/cdev_add(&dev1.cdev_test, dev1.dev_num, 1);/*4 创建类*/dev1.class = class_create(THIS_MODULE, "test1");/*5 创建设备*/dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test1");dev2.major = MAJOR(dev1.dev_num + 1); // 获取主设备号dev2.minor = MINOR(dev1.dev_num + 1); // 获取次设备号printk("major is %d \r\n", dev2.major); // 打印主设备号printk("minor is %d \r\n", dev2.minor); // 打印次设备号// 对设备2进行操作/*2 初始化cdev*/dev2.cdev_test.owner = THIS_MODULE;cdev_init(&dev2.cdev_test, &cdev_test_fops);/*3 添加一个cdev,完成字符设备注册到内核*/cdev_add(&dev2.cdev_test, dev1.dev_num + 1, 1);/*4 创建类*/dev2.class = class_create(THIS_MODULE, "test2");/*5 创建设备*/dev2.device = device_create(dev2.class, NULL, dev1.dev_num + 1, NULL, "test2");return 0;
}static void __exit chr_fops_exit(void) // 驱动出口函数
{/*注销字符设备*/unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号unregister_chrdev_region(dev1.dev_num + 1, 1); // 注销设备号cdev_del(&dev1.cdev_test); // 删除cdevcdev_del(&dev2.cdev_test); // 删除cdevdevice_destroy(dev1.class, dev1.dev_num); // 删除设备device_destroy(dev2.class, dev1.dev_num + 1); // 删除设备class_destroy(dev1.class); // 删除类class_destroy(dev2.class); // 删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");
源码分析
这个代码程序就是在一个程序驱动程序适配了两个设备,里面有两套字符设备,所以字符设备驱动实践的基本步骤都是有的:
基本步骤
这里虽然在一个驱动程序里面,但是两个字符设备在里面生成,基本步骤是有的。
- 动态申请设备号:alloc_chrdev_region
- 初始化cdev:cdev_init
- 添加一个cdev,完成字符设备注册到内核:cdev_add
- 创建类:class_create
- 创建device device_create
知识点
- private_data:这里也用到了之前的私有数据结构体,file 相关的:file->private_data;
- device_test: 匹配private_data 就有了一个结构体,作为变量的封装。 搭配private_data 使用。 这里也体现了私有数据 private_data 和 结构体的意义。封装:设备号 dev_t dev_num ; 主 次设备号:major、minor;字符设备:cdev ;class:字符类 ;device 创建的字符设备:device
- alloc_chrdev_region(&dev1.dev_num, 0, 2, “alloc_name”): 看参数数据,动态申请设备号时候,主设备号一样的,从0 开始,申请两个次设备号来使用。
container_of 当前使用分析
// ,使用container_of函数找到结构体变量dev1 dev2的地址// 然后设置私有数据file->private_data = container_of(inode->i_cdev, struct device_test, cdev_test);
参数分析:
- struct device_test:struct device_test,不就是封装的结构体类型吗? 说明生成的结果就是封装结构体类型的指针,然后放到private_data里面。 这样在 系统调用中的read / write 就特别方便使用了。
- cdev_test : 第三个参数要求是 派生类里面,定义的基类的对象的名称。 在 结构体里面定义的如下: struct cdev cdev_test; // cdev 不就是动态注册的字符设备嘛。
- inode->i_cdev:第一个参数本身需要派生类中某个成员变量的的地址就可以了。 这里用如下 inode 就是传递过来节点的设备号。它本身也是一个结构体指针要,简要如说也如下所示:
cdev_test_open(struct inode *inode, struct file *file)
struct inode {// ...union {struct pipe_inode_info *i_pipe;struct block_device *i_bdev;struct cdev *i_cdev; // 指向字符设备结构的指针char *i_link;};// ...
};
所以,这里在open 方法中,使用了cdev_test_open 函数,最终将 封装的结构体存储在私有数据里面,传递不同的设备过来 比如打开不同设备节点,那么就会生成不同的封装的结构体类型的指针了。
read、write
核心逻辑还是上面的 知识点 container_of 函数的分析。既然封装的结构体指针都获取到了,那么在read/write 就可以通过不同的属性来适配不同的字符设备了。
/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{struct device_test *test_dev = (struct device_test *)file->private_data;// 如果次设备号是0,则为dev1if (test_dev->minor == 0){if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据{printk("copy_from_user error\r\n");return -1;}printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);}// 如果次设备号是1,则为dev2else if (test_dev->minor == 1){if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据{printk("copy_from_user error\r\n");return -1;}printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);}return 0;
}/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{struct device_test *test_dev = (struct device_test *)file->private_data;if (copy_to_user(buf, test_dev->kbuf, strlen(test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据{printk("copy_to_user error\r\n");return -1;}printk("This is cdev_test_read\r\n");return 0;
}
Makefile 编译脚本
#!/bin/bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
obj-m += file.o
KDIR :=/home/wfc123/Linux/rk356x_linux/kernel
PWD ?= $(shell pwd)
all:make -C $(KDIR) M=$(PWD) modulesclean:make -C $(KDIR) M=$(PWD) clean
测试程序app.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main(int argc, char *argv[])
{int fd1; //定义设备1的文件描述符int fd2; //定义设备2的文件描述符char buf1[32] = "nihao /dev/test1"; //定义写入缓存区buf1char buf2[32] = "nihao /dev/test2"; //定义写入缓存区buf2fd1 = open("/dev/test1", O_RDWR); //打开设备1:test1if (fd1 < 0){perror("open error \n");return fd1;}write(fd1,buf1,sizeof(buf1)); //向设备1写入数据close(fd1); //取消文件描述符到文件的映射fd2= open("/dev/test2", O_RDWR); //打开设备2:test2if (fd2 < 0){perror("open error \n");return fd2;}write(fd2,buf2,sizeof(buf2)); //向设备2写入数据close(fd2); //取消文件描述符到文件的映射return 0;
}
编译生成可执行程序:
aarch64-linux-gnu-gcc app.c -o app
加载驱动 insmod file.ko
如下,生成了两个不同的次设备号,主设备号一样的。
生成的驱动设备
驱动加载成功之后会生成/dev/test1 和/dev/test2 设备驱动文件,如下:
root@topeet:/mnt/sdcard]# ls /dev/test1 -al
crw------- 1 root root 236, 0 Jan 12 08:18 /dev/test1
[root@topeet:/mnt/sdcard]# ls /dev/test2 -al
crw------- 1 root root 236, 1 Jan 12 08:18 /dev/test2
运行程序 ./app
结果如下所示,一切按照程序逻辑来执行的。
总结
本篇其实还是对以前技术的总结,这里着重用到了函数 container_of,有点面向对象的意思。 这里适配不同的驱动设备只是一个案例而已。