初学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)
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驱动可以满足要求的话,那么这可以为开发人员剩下不少麻烦。
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.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