Linux驱动开发三.驱动框架重构
通过前面两章内容我们先后做了个虚拟设备驱动,还成功共过驱动文件操作GPIO的点亮了LED,但是那个驱动的架构是有些问题的:
- 需要自己设定主次设备号,并且要在去驱动中定义好设备号。移植性差,在A机子开发的驱动放在B设备上可能设备号被占用,需要重新i修改驱动,并且要手动查询哪些设备号可以被使用。
- 每次模块加载完成后还要手动创建设备节点,操作复杂
- 在注册模块时候用到函数为register_chrdev,这个注册函数使用的时候只传了主设备号,由于设备号是32位的,其中高12位为主设备号,低20位为次设备号。问题就是我们注册了设备号以后这个主设备号下面所有次设备号都被占用了,比较浪费资源
针对上面几点,我们需要对前面的驱动进行修改。
指定设备号
Linux为我们提供了专门的函数用来指定设备号。相当于每当我们有个新的设备需要驱动,就向内核申请一个组设备号,内核根据当前状态给出一个合理的设备号供我们使用。等到需要卸载设备时将其释放掉即可,整个过程是动态的,不需要开发人员认为干预。
这里就要用到下面的函数
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
struct new_device { dev_t dev_id; //设备号 int major; //主设备号 int minor; //次设备号 }; struct new_device led;
static int main(void){ int ret = 0; ret = alloc_chrdev_region(&led.dev_id,0,1,DEV_NAME); return 0; }
我们先定义一个结构体,里面内容为设备号、主设备号和次设备号。然后声明一个该结构体的变量(led),在主函数中我们使用alloc_chrdev_region函数获取设备号,由于函数第一个参数是指针类型,所以我们传参的时候用了取址符。调用函数的时候回将设备号赋值给dev_id,需要设备号时就可以使用这个dev_id。Linux还为我们提供了新的函数换算设备号
主要是major和minor的用法,下面是内核里对几个宏的说明
#define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
主设备号就是将设备号右移20位(取高12位),次设备号也是是通过相应计算直接拿到,可以通过下面的代码调试
ret = alloc_chrdev_region(&led.dev_id,0,1,DEV_NAME); led.major = MAJOR(led.dev_id); led.minor = MINOR(led.dev_id); printk("dev_t = %d,major = %d,minor = %d\r\n",led.dev_id,led.major,led.minor);
主次设备号就可以通过上面的方法拿到,写到这可以make成ko文件放在rootfs里加载一下
设备号为261095424,换算后高12位就是249,低20位都是0,没问题!
设备号释放
前面说过,Linux内大部分功能函数都是成对出现的,有设备号申请,肯定有个释放和其配对使用
void unregister_chrdev_region(dev_t from, unsigned count)
用上面的函数就可以释放掉我们申请的设备号。当然,和前面的用法一样,设备号的释放要放在模块卸载对应的函数中。
unregister_chrdev_region(led.dev_id,1);
这样就行了!
注册设备
这里的注册设备要考虑到保留手动设置设备号和自动设置设备号两种方法;并且手动设置设备号的过程和前面的注册方式不同(使用register_chrdev函数),我们采用另外的一个函数
extern int register_chrdev_region(dev_t, unsigned, const char *);
三个参数第一个是设备号,第二个是数量,第三个是设备名称,我们结合下面的cdev结构体使用
dev.dev_id = MKDEV(dev.major,0); //调用MKDEV函数构建设备号 ret = register_chrdev_region(dev.dev_id,1,DEV_NAME); //注册设备
首先用MKDIEV构建设备号,再调用注册函数注册设备。
自动申请设备注册
通过alloc_chrdev_region函数获取的设备号,需要另外的方式注册——借助cdev结构体,以及cdev_init和cdev_add函数向Linux系统直接添加设备。这三者结合起来就是实现register_chrdev的功能,下面我们大致讲一下。
字符设备cdev结构体
我们在前面的驱动框架中,我们在调用设备注册函数时
ret = register_chrdev(DEV_MAJOR, DEV_NAME, &led_fops);
给了一个参数led_fops,也就是文件操作结构体,在这个结构体中我们关联了驱动文件打开、关闭以及读写时对应的函数。但是这个新的驱动架构中我们没有使用这个函数,那么怎么告知文件打开、关闭及读写时对应的函数呢?这里就要用到一个新的结构体——cdev。cdev主要用来描述一个字符设备,主要包含了设备号(dev_t)和文件操作VFS接口函数(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; //隶属于该主设备号下次设备号的个数 };
上面是内核中定义的cdev数据类型,可以看出来里面包含了file_operations这个元素,所以我们需要使用cdev来关联文件操作结构体。
本章一开始第二个程序段里我们定义了一个包含设备号信息的新设备结构体,下面我们要把cdev声明到这个结构体中
struct new_device { struct cdev cdev; //字符设备 dev_t dev_id; //设备号 int major; //主设备号 int minor; //次设备号 }; static const struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .write = led_write, .release = led_release }; struct new_device led;
我们声明的变量名称和数据类型一样,这个是没问题的, 使用的时候可以直接调用,后面声明的led_fops变量对应的文件VFS接口函数,。
cdev和ops之间关联
虽然我们声明了cdev和led_fops,但是这两个之间还没有直接关联,二者的关联需要一个函数执行:
void cdev_init(struct cdev *, const struct file_operations *);
函数很简单,我们只需要将cdev的地址和led_fops传过去就行了。
cdev_init(&led.cdev, &led_fops);
注意参数都是指针变量,要加取址符
cdev添加和删除
关联好文件操作接口的cdev还需要用一个函数将其添加到Linux系统中
int cdev_add(struct cdev *, dev_t, unsigned count);
参数为cdev,设备号及要增加设备的数量。
ret = cdev_add(&led.cdev,led.dev_id, 1);
由于我们要添加到设备只有1个,所以直接给了个1,就可以了。
有添加就肯定有卸载,卸载的函数要简单些
void cdev_del(struct cdev *);
只需要把cdev传给他就可以了。
static int __exit led_exit(void){ cdev_del(&led.cdev); //注销设备号 unregister_chrdev_region(led.dev_id,1); return 0; }
要注意应该先删除设备,再释放设备号。
创建设备节点
上面的过程我们解决了设备号的问题,但是还有个问题是每次加载模块以后还要在/dev路径下手动添加设备节点。2.6版本以后的Linux内核为我们提供了一套设备管理器——udev。
udev
百度上查到关于udev的概念:Linux 传统上使用静态设备创建方法,因此大量设备节点在 /dev 下创建(有时上千个),而不管相应的硬件设备是否真正存在。通常这由一个MAKEDEV脚本实现,这个脚本包含了许多通过世界上(有幽默意味,注)每一个可能存在的设备相关的主设备号和次设备号对mknod程序的调用。采用udev的方法,只有被内核检测到的设备才会获取为它们创建的设备节点。因为这些设备节点在每次系统启动时被创建,他们会被贮存在ramfs(一个内存中的文件系统,不占用任何磁盘空间).设备节点不需要大量磁盘空间,因此它使用的内存可以忽略。总之就是通过udev可以实现设备在/dev目录下创建相对应的设备文件节点。
我们在构建根文件系统的时候使用的是busybox,busybox创建了一个简化版本的udev——mdev。包括我们最常用的热插拔功能都是依靠mdev来管理的。下面我们看一下如何通过mdev来实现设备文件节点的创建和删除。学习的时候一定是知道整个过程的流程就可以了,千万不要纠结细节!不要纠结细节!整个过程就是两个:创建类、创建设备。
创建、删除类
设备节点的创建是在驱动模块加载的时候就要完成的,也就是在那个init断函数中。一般是放在cdev_add函数后面,由于还是要操作描述这个设备,原先那个描述设备的结构体要修改一下,加新的元素
struct new_device { struct cdev cdev; //字符设备 dev_t dev_id; //设备号 struct class *class; //类 struct device *device; //设备 int major; //主设备号 int minor; //次设备号 };
主要就是那个class和device两个指针类型的数据。在device.h库里,内核提供了创建类的函数
#define class_create(owner, name) \ ({ \ static struct lock_class_key __key; \ __class_create(owner, name, &__key); \ })
使用的时候很简单,owner就是THIS_MODULE,name就给个设备名称就可以了。
led.class = class_create(THIS_MODULE,DEV_NAME);
在卸载模块的时候,把这个类删除就行了,删除也就一句代码
class_destroy(led.class);
直接销毁。
创建、删除设备
创建类完了以后j就要创建对应的设备,设备的创建及删除也是使用给定的函数
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...); extern void device_destroy(struct class *cls, dev_t devt);
删除设备不用细讲了,创建设备函数是可变参数,主要用的参数就是第一个类指针、父设备我们平时不用,直接给NULL,第三个参数dev_t就是设备号,第四个是devdata就是设备可能会用到一些数据,一般也为NULL第5个fmt就是设备的名字,在射着fmt=xxx的时候,就会生成/dev/xxx这个文件。放到代码里就是这样
led.device = device_create(led.class, NULL,led.dev_id,NULL,DEV_NAME);
make以后复制到根目录系统下,加载一下
这样就OK了。
整个框架代码如下:
我们要使用的话直接修改初始化和文件操作函数就可以了!
借用goto实现异常处理
由于我们这个驱动框架几乎完成了注册和生成对应文件所有过程,这里有必要加上异常处理。在每个步骤下面根据返回值进行判断,如果出现异常做出相对应的操作。