正点原子RK3568学习日志12-注册字符设备
1.注册字符设备
驱动编译成驱动模块ko 运行驱动编译成模块insmod
“内核源码/include/linux/cdev.h”文件——struct cdev 结构体
dev 记录了字符设备号、内核对象、文件操作file_operations结构体(设备的打开、读写、关闭等操作接口)等信息struct cdev { struct kobject kobj; //内嵌的内核对象.struct module *owner; //该字符设备所在的内核模块的对象指针.const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.unsigned int count; //隶属于同一主设备号的次设备号的个数. };
“内核源码/include/linux/cdev.h”文件,“内核源码/include/fs/char_dev.c”文件——设备初始化所用到的函数为cdev_init(),
void cdev_init(struct cdev *cdev, const struct file_operations *fops) {memset(cdev, 0, sizeof *cdev);//将整个结构体清零;INIT_LIST_HEAD(&cdev->list);//初始化list成员使其指向自身;kobject_init(&cdev->kobj, &ktype_cdev_default);//初始化kobj成员;cdev->ops = fops;//初始化ops成员,建立cdev 和 file_operations之间的连接 }
内核源码/include/linux/cdev.h”文件——字符设备添加所用到的函数为cdev_add()
1.字符设备初始化
设备初始化所用到的函数为cdev_init()
void cdev_init(struct cdev *, const struct file_operations *);
初始化传入的cdev 类型的结构体,并与自定义的file_operations * 类型的结构体进行链接。
void cdev_init(struct cdev *cdev, const struct file_operations *fops) {memset(cdev, 0, sizeof *cdev);//将整个结构体清零;INIT_LIST_HEAD(&cdev->list);//初始化list成员使其指向自身;kobject_init(&cdev->kobj, &ktype_cdev_default);//初始化kobj成员;cdev->ops = fops;//初始化ops成员,建立cdev 和 file_operations之间的连接 }
要传入的cdev类型结构体,为要初始化的字符设备
要传入的file_operations * file_operations结构体
2.字符设备的添加
该函数向内核注册一个struct cdev结构体
int cdev_add(struct cdev *, dev_t, unsigned);
struct cdev 类型的结构体
申请的字符设备号
和该设备关联的设备编号的数量
*添加成功返回0,添加失败返回负数
字符设备的注销该函数会向内核删除一个struct cdev 类型结构体
void cdev_del(struct cdev *);
要删除的struct cdev 类型的结构体
卸载,先删注册设备,再删设备号
struct cdev cdev_test; //定义cdev结构体类型的变量cdev_test struct file_operations cdev_test_ops = { //owner字段指向本模块,定义file_operations结构体类型的变量cdev_test_ops.owner = THIS_MODULE };
//使用cdev_init()初始化cdev_test结构体,并且连接到cdev_test_ops结构体cdev_init(&cdev_test, &cdev_test_ops); cdev_test.owner = THIS_MODULE; //将owner字段指向本模块,防止模块被卸载ret = cdev_add(&cdev_test, dev_num, 1); //注册字符设备到内核
cdev_del(&cdev_test); //注销字符设备 先删除设备,再删除设备号
2.实验:05_cdev 注册字符设备
动态申请设备号的方式进行设备号的申请,然后对设备进行注册
cdev.c Makefile
cdev.c
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/kdev_t.h> #include <linux/cdev.h>static dev_t dev_num; //定义32位的变量dev_num,用于存放设备号struct cdev cdev_test; //定义cdev结构体类型的变量cdev_test struct file_operations cdev_test_ops = { //owner字段指向本模块,定义file_operations结构体类型的变量cdev_test_ops.owner = THIS_MODULE };static int __init module_cdev_init(void) {int ret; //判断函数返回值int major; //定义主设备号变量int minor; //定义次设备号变量//动态申请设备号ret = alloc_chrdev_region(&dev_num, 0, 1, "chrdev_name"); if(ret < 0) //申请失败{printk("alloc_chrdev_region is error\n");}printk("alloc_chrdev_region is ok\n"); major = MAJOR(dev_num); //获取主设备号minor = MINOR(dev_num); //获取次设备号printk("major = %d\n", major); //打印设备号printk("minor = %d\n", minor); //使用cdev_init()初始化cdev_test结构体,并且连接到cdev_test_ops结构体cdev_init(&cdev_test, &cdev_test_ops); cdev_test.owner = THIS_MODULE; //将owner字段指向本模块,防止模块被卸载ret = cdev_add(&cdev_test, dev_num, 1); //注册字符设备到内核if (ret < 0) //注册失败{printk("cdev_add is error\n");}printk("cdev_add is ok\n");return 0; } static void __exit module_cdev_exit(void) {cdev_del(&cdev_test); //注销字符设备 先删除设备,再删除设备号unregister_chrdev_region(dev_num, 1); //释放设备号printk("module exit. \n");} module_init(module_cdev_init); module_exit(module_cdev_exit); MODULE_LICENSE("GPL v2"); //声明模块许可证 MODULE_AUTHOR("AFANFAN"); //声明模块作者
Makefile
export ARCH=arm64#设置平台架构 export CROSS_COMPILE=/opt/atk-dlrk3568-5_10_sdk-toolchain/bin/aarch64-buildroot-linux-gnu-#交叉编译器前缀 obj-m += cdev.o #此处要和你的驱动源文件同名 KDIR :=/home/alientek/rk3568_linux5.10_sdk/kernel #这里是你的内核目录 PWD ?= $(shell pwd) all:make -C $(KDIR) M=$(PWD) modules #make操作 clean:make -C $(KDIR) M=$(PWD) clean #make clean操作
驱动编译成驱动模块ko
使用命令“make”进行驱动的编译,编译完生成cdev.ko目标文件
运行驱动编译成模块insmod
开发板启动之后, insmod cdev.ko
注册设备号的查看 cat /proc/devices
对驱动进行卸载 rmmod cdev.ko
3.问题:
1.__init 优化内存使用
在Linux内核源代码中,__init 是一个特殊的宏,它用来告诉编译器,被它修饰的函数(在这里是 dev_t_init)或数据结构只在内核初始化期间被使用
内核在编译和链接时会将它们统一放置在一个特殊的内存区域(.init.text 段)。在内核启动过程的最后阶段,内核会释放这部分内存区域,将其回收给系统使用
与 __init 类似的,还有一些其他的宏:
__exit:用于修饰模块卸载时执行的函数。对于静态编译进内核的代码,如果配置了允许卸载,它也会被特殊处理。
__initdata:用于修饰只在初始化阶段使用的数据。这些数据也会被放在特殊的内存段,并在初始化后被释放。
__exitdata:用于修饰模块卸载时使用的数据。