Linux字符设备驱动和杂项设备驱动对比

来源:互联网 发布:中国森林覆盖率 知乎 编辑:程序博客网 时间:2024/04/30 10:46

初学Linux驱动程序的时候,可能对什么是字符设备驱动(char device)和杂项设备驱动(misc device)并不是很了解,更谈不上如何区分了。我自己当初在学习Linux字符设备驱动的时候,也并没有特地去了解其两者之间的区别,尤其是在两种驱动设备注册的时候,没有意识到其不同之处,导致后来在项目中出现了很严重的问题,但却迟迟到找不到解决方案。所以今天就趁这个机会,好好分析一下两者之间的区别,以便有出现在我类似问题的朋友,以后不会再犯同样的错误。


普通字符型设备

对于字符型设备,可能学习过Linux设备驱动的,都应该是比较了解得,并且我们在项目中大多要自己编写的驱动设备也都是字符类型的。在普通的字符型设备驱动注册的过程中,需要经过以下这几个步骤:

  • 申请设备号
    • 动态申请设备号(alloc_chrdev_region)
    • 静态申请设备号(register_chrdev_region)
  • 设备注册
    • 为cdev分配空间(cdev_alloc)
    • 初始化cdev(cdev_init)
    • 将cdev添加进Kernel(cdev_add)
  • 生成设备节点
    • 创建class(class_create)
    • 通过class,创建设备节点(device_create)
以上这些步骤其实在Linux2.6版本之前,都是直接使用register_chrdev函数一步到位,全部完成的。虽然使用register_chrdev在驱动程序注册的过程中变得简单了,但是却存在很大局限性,尤其用户自定性不高。因此目前的Linux内核,这两种方式都提供,但通常采用的方式,就是我上面写的那几个步骤。在设备注册这一步骤需要注意的是cdev_alloc和cdev_init这两个函数之间的区别。关于相同点和不同点,我还是直接上它们的代码进行分析:
struct cdev *cdev_alloc(void) //无参数{   struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL); //为cdev申请内存   if (p) {   INIT_LIST_HEAD(&p->list);   kobject_init(&p->kobj, &ktype_cdev_dynamic); //这两步是初始化cdev,如将cdev加入链表中   }   return p;}
由上面的代码可以知道,cdev_alloc函数主要做了两件事情,第一件事在内核中,为cdev申请内存(这个是每一个驱动设备文件描述结构体需要做的事情,申请完cdev的内存之后,其cdev的地址将存放在inode结构中的i_cdev成员中)。第二件事情就是,初始化cdev,将其放入相应的链表中。最后返回申请到的cdev结构体的地址。下面我们再来看一下cdev_init函数:
void cdev_init(struct cdev *cdev, const struct file_operations *fops){   memset(cdev, 0, sizeof *cdev); //将cdev结构体里面的内容清零   INIT_LIST_HEAD(&cdev->list);   kobject_init(&cdev->kobj, &ktype_cdev_default); //上面这两步,与cdev_alloc一样,初始化cdev   cdev->ops = fops;  //将驱动程序的file_operation地址赋值给cdev的ops指针,这样cdev就真正具有了操作驱动的作用了。}
由上面的代码,我们可以看到cdev_init函数并没有申请cdev的内存空间,所以要使用cdev之前,应该自己为自己驱动的cdev变量申请内存空间,并用变量将其引用。例如:struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);然后将p传递给cdev_init。同时我们还看到,在cdev_init中,还包含了将file_operation的地址指向cdev的ops指针,这也是非常关键的一步,以后只要通过inode知道i_cdev了,那么也就知道file_operation了,从而就知道如何操作该驱动了。

杂项设备
杂项设备驱动,是对字符设备的一种封装,是一种特殊的字符型设备驱动,也是在Linux嵌入式设备中使用的比较多的一种驱动。之所以很大一部分驱动使用的是杂项设备驱动,主要有以下几个方面的原因(由知乎网友整理):
第一,节省主设备号:
使用普通字符设备,不管该驱动的主设备号是静态还是动态分配,都会消耗一个主设备号,这太浪费了。而且如果你的这个驱动最终会提交到内核主线版本上的话,需要申请一个专门的主设备号,这也麻烦。
如果使用misc驱动的话就好多了。因为内核中已经为misc驱动分配了一个主设备号。当系统中拥有多个misc设备驱动时,那么它们的主设备号相同,而用子设备号来区分它们。

第二,使用简单:
有时候驱动开发人员需要开发一个功能较简单的字符设备驱动,导出接口让用户空间程序方便地控制硬件,只需要使用misc子系统提供的接口即可快速地创建一个misc设备驱动。
当使用普通的字符设备驱动时,如果开发人员需要导出操作接口给用户空间的话,需要自己去注册字符驱动,并创建字符设备class以自动在/dev下生成设备节点,相对麻烦一点。而misc驱动则无需考虑这些,基本上只需要把一些基本信息通过struct miscdevice交给misc_register()去处理即可。

本质上misc驱动也是一个字符设备驱动,可能相对特殊一点而已。在drivers/char/misc.c的misc驱动初始化函数misc_init()中实际上使用了MISC_MAJOR(主设备号为10)并调用register_chrdev()去注册了一个字符设备驱动。同时也创建了一个misc_class,使得最后可自动在/dev下自动生成一个主设备号为10的字符设备。总的来讲,如果使用misc驱动可以满足要求的话,那么这可以为开发人员剩下不少麻烦。

正是因为杂项设备驱动不需要开发人员考虑普通字符型设备注册驱动程序时所需要的步骤,例如:设备号申请、添加设备cdev、在/dev下生成字符设备等等,这是因为杂项设备对这些流程都进行了封装,只需要使用misc_register函数即可以自动实现普通字符设备注册所需要的步骤,所以我自己在编写驱动程序的过程中也频繁使用到该类型的设备驱动。但是由于之前对于杂项设备驱动和普通字符型设备驱动不是很熟悉,所以导致我在编写驱动的过程中,也犯了一个非常严重的错误,并且这个错误所导致的原因一直困扰着好久。

下面我将这个错误记录下来:
首先我在编写驱动的时候,定义了一个全局的结构体,结构体是这样的:
struct chr_dev{char trig_flag1,trig_flag2; //发送数据标志位,char rise_flag1,rise_flag2;struct cir_buf rx_buf;wait_queue_head_t rd_waitq;struct cdev dev;struct timer_list s_timer;struct timeval tv1;  //用来获取当前时间struct tasklet_struct my_tasklet; int h_rise_time1;int l_rise_time1;int h_fall_time1;int l_fall_time1;int distance1;char irqflag;};
在这个chr_dev的结构体中,我也定义了cdev结构体,也就是说,按照我这样的编写,应该是采用普通字符设备驱动的注册方式进行注册。因为对于杂项设备驱动程序,是不需要为驱动设备程序定义cdev结构体的。因为只要使用到misc_register函数,对于cdev这些结构体的定义和内存分配,全部自己自动执行了。但是可以看到我们这里的chr_dev结构体里面不光包含有cdev,还有许多其他的变量和结构体,因此使用杂项设备驱动程序,自然是不行的。但是由于之前不了解杂项设备的内在原理,所以我在定义了chr_dev的这个全局结构体的情况下使用了杂项设备注册方式misc_register注册。并且最重要的是我并没有对chr_dev进行初始化,或者为其分配内存,直接在open等函数里面使用如下的机制:
static int Radar_open(struct inode *inode, struct file *file){//获取设备结构体的地址pdev= container_of(inode->i_cdev, struct chr_dev, dev);file->private_data=pdev;}
我使用这个机制的本意是通过inode结构体中的i_cdev地址获取到chr_dev结构体的地址,但此时的i_cdev与chr_dev结构体中的cdev半点关系都没有,并且我也没有为chr_dev分配内存。所以这里得到的pdev结构体的地址必然会有问题,要么就占用了其它变量的地址,要么就可能会有地址不存在的问题,从而引发如下错误:

[   71.824461] Unable to handle kernel paging request at virtual address 64bb7ed0
[   71.830217] pgd = c0004000
[   71.832908] [64bb7ed0] *pgd=00000000
[   71.836469] Internal error: Oops: 5 [#1] PREEMPT SMP
[   71.841414] Modules linked in:
[   71.844454] CPU: 0    Not tainted  (3.0.15 #4)
[   71.848887] PC is at __queue_work+0x8c/0x3d4
[   71.853134] LR is at __queue_work+0x70/0x3d4
[   71.857387] pc : [<c009a8ec>]    lr : [<c009a8d0>]    psr: 600000d3
[   71.857391] sp : d6301c68  ip : d632d584  fp : d6301ca4
[   71.868843] r10: ffff4dd0  r9 : 00000014  r8 : 00000001
[   71.874052] r7 : d6289ea0  r6 : d632d580  r5 : 200000d3  r4 : c09ac080
[   71.880562] r3 : 92dd8f00  r2 : e92dd8f0  r1 : e92dd8f4  r0 : c0041b00
[   71.887072] Flags: nZCv  IRQs off  FIQs off  Mode SVC_32  ISA ARM  Segment kernel
[   71.894537] Control: 10c5387d  Table: 548a804a  DAC: 00000015

或者是

[   77.292704] Unable to handle kernel paging request at virtual address aa6fb9a0
[   77.298503] pgd = d3ddc000
[   77.301194] [aa6fb9a0] *pgd=00000000
[   77.304761] Internal error: Oops: 805 [#1] PREEMPT SMP
[   77.309870] Modules linked in:
[   77.312921] CPU: 0    Not tainted  (3.0.15 #24)
[   77.317458] PC is at T.389+0x54/0xbc
[   77.320999] LR is at s3c_gpiolib_set+0x54/0x58
[   77.325427] pc : [<c025eae4>]    lr : [<c0067434>]    psr: 600001d3
[   77.325448] sp : d3f9bb00  ip : d3f9a018  fp : d3f9bb24
[   77.336872] r10: d4471e80  r9 : 00001414  r8 : 00000000
[   77.342081] r7 : 00000000  r6 : 00000001  r5 : 0000000f  r4 : aa6fb9a0
[   77.348591] r3 : 00000000  r2 : 40010002  r1 : 40010003  r0 : 0000000c
[   77.355107] Flags: nZCv  IRQs off  FIQs off  Mode SVC_32  ISA ARM  Segment user
[   77.362393] Control: 10c5387d  Table: 5494c04a  DAC: 00000015


总之每一次导致出错的地址和函数都不一样,而且也不好追踪到底是哪里出了问题。最后的解决方式当然是换回普通字符设备注册驱动的方式,首先在驱动的init函数中给chr_dev结构体分配内存,然后将che_dev->dev使用cdev_add函数加入Kernel中,这一问题才得以成功解决。


1 0