1、另一种写法引入
前面学习编写字符设备驱动的时候都是使用register_chrdev函数注册字符设备驱动程序,该函数的原型如下:
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
register_chrdev函数有三个参数:
- major:代表注册字符设备的主设备号,可以自己指定,也可以写为0让内核自动分配
- name:代表设备名称,如果register_chrdev操作成功,设备名就会出现在/proc/devices文件里
- fops:file_operations结构体,里面包含一系列函数指针,指向对硬件设备操作的函数
使用register_chrdev函数进行注册字符设备驱动程序时,有个很大的缺点:每注册一个字符设备,系统会连续注册0~255个次设备号,使它们绑定在同一个file_operations操作方法结构体上。在大多数情况下,我们都只用极少的次设备号,所以这样会浪费很多资源。
这里可以使用前面编写的第一个字符设备驱动程序来验证这种现象存在
在前面编写的第一个字符设备驱动程序中,我们使用register_chrdev函数创建了一个字符设备,在file_operation结构体的.open、.write、.read函数中只完成了打印语句的操作。根据上面的我们手动创建设备节点并对设备节点执行cat操作,由运行结果可以看出我们前面描述的现象的确存在。
下面我们从代码的角度分析,来看register_chrdev函数是怎么实现的?以及这种情况出现的原因
在分析register_chrdev函数之前,我们先来认识一下__register_chrdev_region、cdev_add两个函数
__register_chrdev_region:
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
函数各参数的表示内容:
- major:主设备号
- baseminor:次设备号基地址,对应要分配次设备号的起始地址
- minorct:分配次设备号的数量
- name:表示设备名称
__register_chrdev_region向内核注册一个字符设备区域,一个主设备号和能使用的次设备号的范围
cdev_add:
int cdev_add(unsigned cdev *p, dev_t dev, unsigned count)
cdev_add向内核添加一个字符设备,函数各参数的表示内容:
- p:cdev指针,指向要注册cdev结构体
- dev:该设备负责的第一个设备号
- count:与此设备对应的连续的次设备号的数量
好了,有了上面的基础,现在我们来看register_chrdev函数的代码
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) { ... cd = __register_chrdev_region(major, 0, 256, name);------------->① ... cdev = cdev_alloc();-------------------------------------------->② ... cdev->owner = fops->owner; cdev->ops = fops; ... err = cdev_add(cdev, MKDEV(cd->major, 0), 256);----------------->③ .... }
register_chrdev函数主要做了三件事情
① 调用__register_chrdev_region函数注册了一个单的主设备号和一个指定的次设备号范围
② 分配一个cdev结构体,并设置它
③ 调用cdev_add向内核添加一个字符设备
从register_chrdev函数源代码中可以看出,之所以该函数每注册一个字符设备,连续的0~255个次设备号绑定在同一个file_operations操作方法结构体,是因为该函数调用__register_chrdev_region函数向内核注册了一个主设备号并使用了所有的次设备号0~255。
因此,如果我们不想每注册一个字符设备,使0~255的次设备号绑定在同一个file_operations操作方法结构体上,可以通过减少注册字符设备时对应分配次设备号范围,这便引入了字符设备另一种写法。
2、另一种写法实现
2.1 驱动入口函数
static int new_chrdev_init(void) { dev_t devid; #if 0 major = register_chrdev(0, "new_chrdev", &new_chrdev_fops); #else cdev = cdev_alloc();--------------------------------->① cdev_init(cdev, &new_chrdev_fops); if(major == 0)--------------------------------------->② { alloc_chrdev_region(&devid, 0, 2, "new_chrdev"); major = MAJOR(devid); } else { devid = MKDEV(major, 0); register_chrdev_region(devid, 2, "new_chrdev"); } cdev_add(cdev, devid, 2);--------------------------->③ #endif new_chrdev_class = class_create(THIS_MODULE, "new_chrdev"); //led_drv_class_dev = class_device_create(led_drv_class, NULL, MKDEV(major, 0), NULL, "led"); new_chrdev_class_dev = device_create(new_chrdev_class, NULL, MKDEV(major, 0), NULL, "new_chrdev"); /* /dev/new_chrdev */ return 0; }
入口函数中不直接使用register_chrdev创建字符设备,而是按照register_chrdev函数实现内容,自己去创建字符设备,这样可以根据需要去分配注册设备对应的次设备号的数量
① 分配一个cdev结构体,cdev_init函数对分配的cdev结构体进行初始化操作
② 判断是否指定了主设备号,如果没有指定就自动分配主设备号,这里分配字符设备区域时只分配了0~1两个次设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name) //对于动态分配主设备号时使用 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name) //对应已经指定主设备号分别字符设备区域时使用
③ 使用cdev_add向内核添加字符设备
2.2 驱动出口函数
static void new_chrdev_exit(void) { #if 0 unregister_chrdev(major, "led_drv"); #endif //class_device_unregister(led_drv_class_dev); device_unregister(new_chrdev_class_dev); class_destroy(new_chrdev_class); unregister_chrdev_region(MKDEV(major, 0), 2); cdev_del(cdev); kfree(cdev); }
出口函数处和入口函数实现的功能相反,释放申请的字符设备区域、删除添加字符设备、释放动态分配的cdev结构体内存。
3、驱动测试
加载驱动程序,手动创建设备节点。由于注册的字符设备只分配了0~1这两个次设备号,所以打开次设备号为3的设备节点时失败,驱动程序修改成功。
完整驱动程序
#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/delay.h> #include <linux/device.h> #include <asm/uaccess.h> #include <asm/irq.h> #include <asm/io.h> #include <linux/cdev.h> #include <linux/slab.h> static int major = 0; static struct cdev *cdev; static struct class *new_chrdev_class; static struct class_device *new_chrdev_class_dev; static int new_chrdev_open(struct inode *inode, struct file *file); static ssize_t new_chrdev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos); static ssize_t new_chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos); struct file_operations new_chrdev_fops = { .owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */ .open = new_chrdev_open, .read = new_chrdev_read, .write = new_chrdev_write, }; static int new_chrdev_open(struct inode *inode, struct file *file) { printk("new_chrdev_open "); return 0; } static ssize_t new_chrdev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { printk("new_chrdev_read "); return 0; } static ssize_t new_chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { printk("new_chrdev_read "); return 0; } static int new_chrdev_init(void) { dev_t devid; #if 0 major = register_chrdev(0, "new_chrdev", &new_chrdev_fops); #else cdev = cdev_alloc(); cdev_init(cdev, &new_chrdev_fops); if(major == 0) { alloc_chrdev_region(&devid, 0, 2, "new_chrdev"); major = MAJOR(devid); } else { devid = MKDEV(major, 0); register_chrdev_region(devid, 2, "new_chrdev"); } cdev_add(cdev, devid, 2); #endif new_chrdev_class = class_create(THIS_MODULE, "new_chrdev"); //led_drv_class_dev = class_device_create(led_drv_class, NULL, MKDEV(major, 0), NULL, "led"); new_chrdev_class_dev = device_create(new_chrdev_class, NULL, MKDEV(major, 0), NULL, "new_chrdev"); /* /dev/new_chrdev */ return 0; } static void new_chrdev_exit(void) { #if 0 unregister_chrdev(major, "led_drv"); #endif //class_device_unregister(led_drv_class_dev); device_unregister(new_chrdev_class_dev); class_destroy(new_chrdev_class); unregister_chrdev_region(MKDEV(major, 0), 2); cdev_del(cdev); kfree(cdev); } module_init(new_chrdev_init); module_exit(new_chrdev_exit); MODULE_LICENSE("GPL");