这一章主要通过介绍字符设备scull(Simple Character Utility for Loading Localities,区域装载的简单字符工具)的驱动程序编写,来学习Linux设备驱动的基本知识。scull可以为真正的设备驱动程序提供样板。
一、主设备号和次设备号
主设备号表示设备对应的驱动程序;次设备号由内核使用,用于正确确定设备文件所指的设备。现代的Linux内核允许多个驱动程序共享主设备号。内核用dev_t类型()来保存设备编号,dev_t是一个32位的数,12位表示主设备号,20为表示次设备号。在实际使用中,是通过中定义的宏来转换格式。
(dev_t)-->主设备号、次设备号 |
MAJOR(dev_t dev) MINOR(dev_t dev) |
主设备号、次设备号-->(dev_t) |
MKDEV(int major,int minor) |
建立一个字符设备之前,驱动程序首先要做的事情就是获得设备编号。其这主要函数在中声明:
//指定设备编号
int register_chrdev_region(dev_t first, unsigned int count,char *name); first:要分配的设备编号范围的起始值。first的次设备号经常被置为0.count:所请求的连续设备编号的个数。name:和该编号范围关联的设备名称。该函数执行成功返回0,错误返回负的错误码。 int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name); //动态生成设备编号
dev:仅用于输出的参数,在成功完成调用后将保存已分配范围的第一个编号。firstminor:要使用的被请求的第一个次设备号,通常是0.count和name同register_chrdev_region一样。 void unregister_chrdev_region(dev_t first, unsigned int count); //释放设备编号 |
分配主设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。
以下是在scull.c中用来获取主设备号的代码:
if (scull_major)
{ dev = MKDEV(scull_major, scull_minor); result = register_chrdev_region(dev, scull_nr_devs, "scull"); }
else
{ result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,"scull"); scull_major = MAJOR(dev); } if (result < 0)
{ printk(KERN_WARNING "scull: can't get major %d\n", scull_major); return result; }
|
在上面代码中,书P52页说明了scull_major是一个全局变量,初始置为0,所以采用动态分配主设备号的方式。并且上述代码是本书中几乎所有的示例程序分配主设备号的方法。
在这部分中,比较重要的是在用函数获取设备编号后,其中的参数name是和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中。看到这里,就可以理解为什么mdev和udev可以动态、自动地生成当前系统需要的设备文件。udev就是通过读取sysfs下的信息来识别硬件设备的.
(请看《理解和认识udev》
URL:http://blog.chinaunix.net/u/6541/showart_396425.html)
二、一些重要的数据结构
大部分基本的驱动程序操作涉及到三个重要的内核数据结构,分别是file_operations、file和inode,它们的定义都在。file_operations:
file_operations是将驱动程序操作连接到设备编号的。file_operations结构或者指向这类结构的指针称为fops。这个结构中的每一个字段都必须指向驱动程序中实现特定操作的函数,对于不支持的操作,对应的字段可置为NULL值。具体的结构见P54。
file结构:struct file是一个内核结构,不会出现在用户程序。file结构代表一个打开的文件。它由内核在open时创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数。在内核源代码中,指向struct file的指针通常被称为filp。这样,file指的是结构本身,filp则是指向该结构的指针。 inode结构:内核用inode结构在内部表示文件。
dev_t i_rdev:对表示设备文件的inode结构,该字段包含了真正的设备编号。
struct cdev *i_cdev:struct cdev是表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。
下面两个可用来从一个inode中获得主设备号和次设备号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
三、字符设备的注册
内核内部使用struct cdev结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或多个struct cdev。代码应包含,它定义了struct cdev以及与其相关的一些辅助函数。
注册一个独立的cdev设备的基本过程如下:
1、为struct cdev 分配空间(如果已经将struct cdev 嵌入到自己的设备的特定结构体中,并分配了空间,这步略过!)
struct cdev *my_cdev = cdev_alloc();
2、初始化struct cdev
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
3、初始化cdev.owner
cdev.owner = THIS_MODULE;
4、cdev设置完成,通知内核struct cdev的信息(在执行这步之前必须确定你对struct cdev的以上设置已经完成!)
int cdev_add(struct cdev *dev, dev_t num, unsigned count)
这里,dev是cdev结构,num是该设备对应的第一个设备编号,count是和该设备关联的设备编号数量,经常取1.
从系统中移除一个字符设备:void cdev_del(struct cdev *p)
以下是scull中的初始化代码(之前已经为struct scull_dev 分配了空间):
/*Set up the char_dev structure for this device.*/ static void scull_setup_cdev(struct scull_dev *dev, int index) { int err, devno = MKDEV(scull_major, scull_minor + index); cdev_init(&dev->cdev, &scull_fops); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &scull_fops; //这句可以省略,在cdev_init中已经做过 err = cdev_add (&dev->cdev, devno, 1); /* Fail gracefully if need be 这步值得注意*/ if (err) printk(KERN_NOTICE "Error %d adding scull%d", err, index); }
|
四、scull模型的内存使用
以下是scull模型的结构体:
/*Representation of scull quantum sets.*/ struct scull_qset { void **data; struct scull_qset *next; }; struct scull_dev { struct scull_qset *data; /* Pointer to first quantum set */ int quantum; /* the current quantum size */ int qset; /* the current array size */ unsigned long size; /* amount of data stored here */ unsigned int access_key; /* used by sculluid and scullpriv */ struct semaphore sem; /* mutual exclusion semaphore */ struct cdev cdev; /* Char device structure */ };
|
scull驱动程序引入了两个Linux内核中用于内存管理的核心函数,它们的定义都在:
void *kmalloc(size_t size, int flags);
size:分配内存的大小,flags:内存的分配方法。执行成功返回指向该内存的指针,失败返回NULL。 void kfree(void *ptr);
只能是kmalloc分配的内存才可以使用kfree注销它
|
以下是scull模块中的一个释放整个数据区的函数(类似清零),将在scull以写方式打开和scull_cleanup_module中被调用:
int scull_trim(struct scull_dev *dev) { struct scull_qset *next, *dptr; int qset = dev->qset; /* 量子集中量子的个数*/ int i; for (dptr = dev->data; dptr; dptr = next)
{ /* 循环scull_set个数次,直到dptr为NULL为止。*/ if (dptr->data)
{ for (i = 0; i < qset; i++)/* 循环一个量子集中量子的个数次*/ kfree(dptr->data[i]);/* 释放其中一个量子的空间*/ kfree(dptr->data);/* 释放当前的scull_set的量子集的空间*/ dptr->data = NULL;/* 释放一个scull_set中的void **data指针*/ } next = dptr->next; /* 准备下个scull_set的指针*/ kfree(dptr);/* 释放当前的scull_set*/ } dev->size = 0; /* 当前的scull_device所存的数据为0字节*/ dev->quantum = scull_quantum;/* 初始化一个量子的大小*/ dev->qset = scull_qset;/* 初始化一个量子集中量子的个数*/ dev->data = NULL;/* 释放当前的scull_device的struct scull_qset *data指针*/ return 0; }
|
以下是scull模块中的一个沿链表前行得到正确scull_set指针的函数,将在read和write方法中被调用:
/*Follow the list*/ struct scull_qset *scull_follow(struct scull_dev *dev, int n) { struct scull_qset *qs = dev->data; /* Allocate first qset explicitly if need be */ if (! qs)
{ qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL); if (qs == NULL) return NULL; /* Never mind */ memset(qs, 0, sizeof(struct scull_qset)); } /* Then follow the list */ while (n--)
{ if (!qs->next)
{ qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL); if (qs->next == NULL) return NULL; /* Never mind */ memset(qs->next, 0, sizeof(struct scull_qset)); } qs = qs->next; continue; } return qs; }
|
其实这个函数的实质是:如果已经存在这个scull_set,就返回这个scull_set的指针。如果不存在这个scull_set,一边沿链表为scull_set分配空间一边沿链表前行,直到所需要的scull_set被分配到空间并初始化为止,就返回这个scull_set的指针。
五、open和release
open方法提供给驱动程序以初始化的能力,为以后的操作作准备。应完成的工作如下:
(1)检查设备特定的错误(如设备未就绪或硬件问题);
(2)如果设备是首次打开,则对其进行初始化;
(3)如有必要,更新f_op指针;
(4)分配并填写置于filp->private_data里的数据结构。
而根据scull的实际情况,他的open函数只要完成第四步(将初始化过的struct scull_dev dev的指针传递到filp->private_data里,以备后用)就好了,所以open函数很简单。但是其中用到了定义在中的container_of宏,源码如下:
#define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );})
|
其实从源码可以看出,其作用就是:通过指针ptr,获得包含ptr所指向数据(是member结构体)的type结构体的指针。即是用指针得到另外一个指针。
release方法提供释放内存,关闭设备的功能。应完成的工作如下:
(1)释放由open分配的、保存在file->private_data中的所有内容;
(2)在最后一次关闭操作时关闭设备。
由于前面定义了scull是一个全局且持久的内存区,所以他的release什么都不做。
六、read和write
read和write方法的主要作用就是实现内核与用户空间之间的数据拷贝。因为Linux的内核空间和用户空间隔离的,所以要实现数据拷贝就必须使用在中定义的:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count); unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
|
而值得一提的是以上两个函数和
#define __copy_from_user(to,from,n) (memcpy(to, (void __force *)from, n), 0) #define __copy_to_user(to,from,n) (memcpy((void __force *)to, from, n), 0)
|
之间的关系:通过源码可知,前者调用后者,但前者在调用前对用户空间指针进行了检查。
一个有1000个指针的数组,每个指针指向一个4000字节的区域,我们把每一个内存区域称为一个量子,而这个指针数组(或它的长度)称为量子集。
注意P70-P72的代码。仔细阅读。
七、模块实验
这次模块实验的使用是mini2440,使用Linux2.6.29内核。
模块程序链接:scull模块源程序
模块测试程序链接:模块测试程序
测试结果:
量子大小为6:
[root@FriendlyARM L3]# insmod scull.ko scull_quantum=6 scull_qset=2 [root@FriendlyARM L3]# cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 4 tty 5 /dev/tty 5 /dev/console 5 /dev/ptmx 7 vcs 10 misc 13 input 14 sound 29 fb 81 video4linux 89 i2c 90 mtd 116 alsa 128 ptm 136 pts 180 usb 188 ttyUSB 189 usb_device 204 s3c2410_serial 252 scull 253 usb_endpoint 254 rtc
Block devices: 259 blkext 7 loop 8 sd 31 mtdblock 65 sd 66 sd 67 sd 68 sd 69 sd 70 sd 71 sd 128 sd 129 sd 130 sd 131 sd 132 sd 133 sd 134 sd 135 sd 179 mmc [root@FriendlyARM L3]# mknod -m 666 scull0 c 252 0 [root@FriendlyARM L3]# mknod -m 666 scull1 c 252 1 [root@FriendlyARM L3]# mknod -m 666 scull2 c 252 2
[root@FriendlyARM L3]# mknod -m 666 scull3 c 252 3
启动测试程序
[root@FriendlyARM L3]# ./scull_test
write error! code=6
write error! code=6
write error! code=6
write ok! code=2
read error! code=6
read error! code=6
read error! code=6
read ok! code=2
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4
[5]=5 [6]=6 [7]=7 [8]=8 [9]=9
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
[15]=15 [16]=16 [17]=17 [18]=18 [19]=19
改变量子大小为默认值4000: [root@FriendlyARM L3]# rmmod scull [root@FriendlyARM L3]# insmod scull.ko
启动测试程序 [root@FriendlyARM L3]# ./scull_test write ok! code=20 read ok! code=20 [0]=0 [1]=1 [2]=2 [3]=3 [4]=4 [5]=5 [6]=6 [7]=7 [8]=8 [9]=9 [10]=10 [11]=11 [12]=12 [13]=13 [14]=14 [15]=15 [16]=16 [17]=17 [18]=18 [19]=19
改变量子大小为6,量子集大小为2: [root@FriendlyARM L3]# #cd /lib/modules/ [root@FriendlyARM L3]# #rmmod scull [root@FriendlyARM L3]# #insmod scull.ko scull_quantum=6 scull_qset=2
启动测试程序 [root@FriendlyARM L3]# ./scull_test write error! code=6 write error! code=6 write error! code=6 write ok! code=2 read error! code=6 read error! code=6 read error! code=6 read ok! code=2 [0]=0 [1]=1 [2]=2 [3]=3 [4]=4 [5]=5 [6]=6 [7]=7 [8]=8 [9]=9 [10]=10 [11]=11 [12]=12 [13]=13 [14]=14 [15]=15 [16]=16 [17]=17 [18]=18 [19]=19 |
实验不仅测试了模块的读写能力,还测试了量子读写是否有效。
阅读(1283) | 评论(0) | 转发(1) |