主设备号,次设备号
主设备号表示设备对应的驱动程序;次设备号由内核使用,用于正确确定设备文件所指的设备。
内核用dev_t类型(<linux/types.h>)来保存设备编号,dev_t是一个32位的数,12位表示主设备号,20为表示次设备号。
在实际使用中,是通过<linux/kdev_t.h>中定义的宏来转换格式。
(dev_t)-->主设备号、次设备号
MAJOR(dev_t dev)
MINOR(dev_t dev)
主设备号、次设备号-->(dev_t)
MKDEV(int major,int minor)
分配和释放设备编号
#include<linux/fs.h>
静态指定
int register_chrdev_region(dev_t first,unsigned int count,char *name);
动态分配
int alloc_chrdev_region(dev *dev,unsigned int firstminor,unsigned int count,char *name);
释放设备号
void unregister_chrdev_region(dev_t first,unsigned int count);
分配之设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。
以下是在scull.c中用来获取主设备好的代码:
if(scull_major) { dev=MKDEV(scull_major,scull_minor); 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 not get scull major %d",scull_major); return result; }
在这部分中,比较重要的是在用函数获取设备编号后,其中的参数name是和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中。
看到这里,就可以理解为什么mdev和udev可以动态、自动地生成当前系统需要的设备文件。udev就是通过读取sysfs下的信息来识别硬件设备的.
一些重要的数据结构
file_operations,file,inode
大多数设备驱动程序都会用到的三个重要数据结构。file_operations结构保存了字符驱动程序的方法,struct file表示一个打开的文件,而struct inode表示一个磁盘上的文件。
字符设备的注册
1.获取一个cdev设备
struct cdev *my_dev = cdev_alloc();
my_cdev->ops = &my_fops;
2.初始化分配到的结构
void cdev_init(struct cdev *cdev,struct file_operation *fops);
另外还有一个struct cdev的字段需要初始化。和file_operations结构类似,struct cdev也有一个所有者字段,应被设置为THIS_MODULE。
cdev.owner = THIS_MODULE
3.cdev设置好之后,最后的步骤是通过下面的调用告诉内核该结构的信息:
int cdev_add(struct cdev *dev,dev_t num,unsigend int count);
dev是cdev结构,num是该设备对应的第一个设备 编号,count是应该和设备关联的设备编号的数量。
4.移除一个字符设备
void cdev_del(struct cdev *dev);
以下为scull中的设备注册
struct scull_dev{ struct scull_qset *data; int quantum; int qset; unsigned long size; unsigend int access_key; struct semaphore sem; struct cdev cdev; }
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;
err = cdev_add(&dev->cev,devno,1);
/*faile gracefully if need be*/
if(err)
printk(KERN_NOTICE "error %d adding scull %d",err,index);
}
open和release open方法
open方法提供给驱动程序以初始化的能力,从而为以后的操作完成初始化做准备。在大部分驱动程序中,open应完成以下工作:
1.检查设备特定的错误(诸如设备未就绪,或类似的硬件问题)
2.如果设备是首次打开,则对其进行初始化
3.如有必要,更新f_op指针
4.分配并填写置于filp->private_data里的数据结构
open方法原型:int (*open)(struct inode *inode,struct file *filp)
其中的inode参数在其i_cdev字段中包含了我们所需要的信息,即我们先前设置的cdev结构。唯一的问题是,我们通常不需要cdev结构本身,而是希望得到包含cdev结构的scull_dev结构。c语言可帮助程序员通过一些技巧完成这类转换,但不应滥用这类技巧。幸运的是,在这种情况下我们已经实现了这类技巧,它通过定义在<linux/kernel.h>中的container_of宏实现:
container_of(pointer,container_type,container_field);
这个宏要一个container_field字段的指针,该字段包含在container_type类型的结构中,然后返回包含该字段的结构指针。
在scull_open中,这个宏用来找到适当的设备:
struct scull_dev *dev; dev = container_of(inode->i_cdev,struct scull_dev,cdev); filp->private_data = dev; /*for other methods*/
而根据scull的实际情况,他的open函数只要完成第四步(将初始化过的struct scull_dev dev的指针传递到filp->private_data里,以备后用)就好了,所以open函数很简单。但是其中用到了定义在<linux/kernel.h>中的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的内核空间和用户空间隔离的,所以要实现数据拷贝就必须使用在<asm/uaccess.h>中定义的:
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)
之间的关系:通过源码可知,前者调用后者,但前者在调用前对用户空间指针进行了检查。
至于read和write 的具体函数比较简单,就在实验中验证好了。