众所周知,linux中可以采用灵活的多层次的驱动架构来对接口进行统一与抽象,最低层次的驱动总是直接面向硬件的,而最高层次的驱动在linux中
被划分为“面向字符设备、面向块设备、面向网络接口”三大类来进行处理,前两类驱动在文件系统中形成类似文件的“虚拟文件”,又称为“节点node”,
这些节点拥有不同的名称代表不同的设备,在目录/dev下进行统一管理,系统调用函数如open、close、read等也与普通文件的操作有相似之处,这种接
口的一致性是由VFS(虚拟文件系统层)抽象完成的。面向网络接口的设备仍然在UNIX/Linux系统中被分配代表设备的名称(如eth0),但是没有映射入
文件系统中,其驱动的调用方式也与文件系统的调用open、read等不同。
首先,对字符设备的访问时通过文件系统内的设备名称(设备节点)进行的,他们通常在/dev目录下,利用 ls -l 命令可以查看当前目录下
的文件类型c代表是字符设备,b代表块设备。
主设备号:标识的是设备对应的驱动程序,例如 :/dev/null和/dev/zero由驱动程序1管理,虚拟控制台和串口终端由驱动程序4管理,vcsl和vcsal
都是由设备驱动程序7管理。现在的Linux内核允许多个驱动程序共享主设备号,但我们看到的大多数设备任然按照“一个设备号对应一个驱动程序”的
原则进行组织。
次设备号:由内核使用,用于正确确定设备文件所指的设备。除了知道次设备号用来指向驱动程序所实现的设备外,内核本身基本不关心关于次设
备号的任何其他信息。
在内核中,dev_t类型(linux/types.h)用来保存设备编号----包括主设备号和次设备号(12(主)+ 20(次)),我们应该使用
中定义的宏,例如:
获取主设备号:MAJOR(dev_t dev);
获取主设备号:MINOR(dev_t dev);
如果要将主设备号和次设备号转换成dev_t类型:
MKDEV(int major,int minor);
静态分配设备编号:
在建立一个字符设备之前,我们的驱动程序首先要做的工作就是获得一个或者多个设备编号,完成该工作的函数(<linux/fs.h>)
int register_chrdev_region(dev_t first,unsigned int count,char *name);
first:要分配的设备编号范围的起始值,其次设备号经常被置为0,但并不是必须的;
count:请求的连续编号的个数,如果count非常大,则请求的范围可能和下一个主设备号重叠,但只要我们所请求的编号范围是可用的,
则不会带来任何问题;
name:是和该编号范围管理的设备名称,将会出现在/proc/devices和sysfs中,
返回值:分配成功返回0,失败返回负的错误码;
如果我们预先已经知道所需要的设备编号,则 register_chrdev_region()会工作的很好,但是我们经常不知道设备将要使用哪些主设备号。
动态分配设备编号:
int alloc_chrdev_region(dev_t* dev,unsigned int firstminor,unsigned int count,char *name);
dev:仅用于输出的参数,在成功完成调用后将保存已分配范围的第一个参数;
firstminor:请求的第一个次设备号,通常置为0;
count、name:和register_chrdev_region函数相同。
返回值:分配成功返回0,失败返回负的错误码;
无论采用哪种方式分配设备号,都应该在不适用它的时候释放掉,通常会在模块的清除函数中使用释放函数。
释放设备编号:
void unregister_chrdev_region(dev_t first,unsigned int count);
上述的函数为驱动程序的使用分配设备编号,但是他们并没有告诉内核关于拿来这些编号做什么工作,在用户空间程序可访问上述设备编号之前,
驱动程序需要将设备编号和内部函数连接起来。这些内部函数用来实现设备的操作。
一部分主设备号已经静态的分配给了大部分常见的设备(Documentation/devices.txt中有清单),将某个已经分配好的静态编号用于新驱动程序的
机会很小,但尚未分配的新编号是可用的。因此,作为驱动开发者有两种选择:1. 选定一个尚未使用的编号;2. 动态分配主设备号。如果驱动的使用者只
有你自己的话则第一种方法将永远行不通,但是当一个驱动程序如果被广泛应用,则第一种方法则可能造成冲突和麻烦。因此对于一个新的驱动程序建议使用
第二种方法动态的分配主设备号。动态的分配缺点是:由于分配的主设备号不能保证始终一致,所以无法预先创建设备节点,这道不是问题,因为一旦分配了
设备号,既可以从/proc/devices中读取到,因此,为了加载一个使用动态分配主设备号的设备驱动程序,对insmod的调用可用一个脚本替换,该脚本在调
用insmod之后读取/proc/devices以霍地新分配的主设备号,然后创建对应的设备文件。
一种最佳的分配方法:默认采用动态分配,同时保留在加载甚至是编译时制定设备编号的余地。例如下面这段程序:它使用了一个全局变量char_major,
用来爆出所选择的设备号,同时char_major用来保存次设备号。其中char_major的初始化值是CHAR_MAJOR(初始化值为0,即选择动态分配方式),这个宏
定义在char.h中,用户可以使用这个默认值或选择某个特定的主设备号,而且既可以在编译前修改宏定义,也可以通过insmod命令行制定char_major的值,
最后通过使用char_load脚本,用户可以在char_load的命令行中将参数传递insmod。
下面是char.c中用来获取主设备号的代码:
-
if(char_major) {
-
dev = MKDEV( char_major, char_minor);
-
result = register_chrdev_region(dev,char_nr_devs,"char");
-
} else {
-
result = alloc_chrdev_region(&dev,char_minor,char_nr_devs,"char");
-
scull_major = MAJOR(dev);
-
}
-
if(result < 0){
-
printk(KERN_WARNING"char:canot get major %d\n",char_major);
-
return result;
-
}
字符设备驱动中最重要的3个结构体:file_operations、file、inode,大部分基本的驱动程序操作都会涉及到这三个结构体
file_operations:
作用是将驱动程序的操作连接到设备号上,结构中包含了一组函数指针,其中每个打开的文件(用file表示)与一组这样的函数相连,即:
struct file {
... ...
struct file_operations * f_ops;
... ...
}
struct file_operations 结构体中的函数主要用来实现系统调用(open、read等),也就是说这个结构体中的方法是用来操作打开的文件的(struct file)。而在这个
结构中的每个字段(函数指针)都必须指向驱动程序中已经实现的特定的操作函数,例如:{ .read = scull_read},则这个scull_read就是用来操作打开的scull文件的
read方法,对scull文件的度操作就通过scull_read函数实现,但是在上层(应用层)则任然通过read方法实现对open的scull文件读操作。也就是说在上层中,虽然
使用相同的read方法,但如果open打开文件时打开的是不同的文件,则对应的read在顶层的实现是不同的。
而对于 file_operations 结构体中不支持的操作则对应的字段可置为NULL。但是对应于不同的字段即使都被置为NULL,但具体的处理却不尽相同,这部分在
《Linux设备驱动程序》的54页(第三版)有具体的介绍。具体的实现实例可以如下(引用Linux设备驱动程序中的例子):
-
struct file_operations{
-
.owner = THIS_MODULE, //几乎是固定格式
-
.llseek = scull_llseek,
-
.read = scull_read,
-
.write = scull_write,
-
.ioctl = scull_ioctl,
-
.open = scull_open,
-
.release = scull_release,
-
-
};
file:
与用户空间中的FILE没有丝毫关系,file结构代表一个打开的文件(并不局限于驱动程序,系统中每个打开的文件在内核空间都有一个对应的file结构),
由内核在open时创建,并传递给在该文件上进行操作的所有函数,直至close,在文件的所有实例都被关闭之后内核会释放这个数据结构。如下是其内部的部
分重要字段:
mode_t f_mode;
loff_t f_ops;
unsigned int f_flags;
struct file_operations *f_ops;
void *private_data;
struct dentry *f_dentry;
inode:
内核用inode结构在内部标识文件,和file结构不同,file结构标识打开的文件描述符。对于单个文件,可能有许多个表示打开的文件描述符(file结构),
但是这些所有的file结构都指向表示文件的单个inode结构。inode中包含大量有关文件的信息,但只有下面这两个字段对编写驱动程序有用
dev_t i_rdev; //该字段包含了真正的设备号
struct cdev * i_cdev: //struct cdev是表示字符设备的内核的内部结构,当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的字段。
可移植的方法获取i_rdev设备号:
unsigned int iminor(struct inode * inode);
unsigned int imajor(struct inode * inode);
字符设备的注册:
内核内部使用struct cdev()表示字符设备,在内核调用设备的操作之前,必须分配并注册一个或者多个上述结构。
这是你可以将cdev结构嵌入到自己的设备的特定结构中,我们用下面的代码初始化已经分配到的结构:
struct cdev和struct file_operations一样有一个字段需要初始化owner = THIS_MOUDLE
在字符设备中用到的结构体:
-
struct cdev {
-
struct kobject kobj;//每个cdev都是一个kobject
-
struct module *owner;//指向实现驱动的模块
-
const struct file_operations *ops;//操作这个字符设备文件的方法
-
struct list_head list;//与cdev对应的字符设备文件的inode->i_devices的链表头
-
dev_t dev;//起始设备编号
-
unsigned int count;//设备编号范围
-
}
一个 cdev 一般它有两种定义初始化方式:静态的和动态的。
静态内存定义初始化:
-
struct cdev my_cdev;
-
cdev_init(&my_cdev, &fops);
-
my_cdev.owner = THIS_MODULE;
动态内存定义初始化:
-
struct cdev *my_cdev = cdev_alloc();
-
my_cdev->ops = &fops;
-
my_cdev->owner = THIS_MODULE;
两种使用方式的功能是一样的,只是使用的内存区不一样,一般视时机的数据结构需求而定
下面贴出了两个函数的代码,以具体看一下它们之间的差异。
-
struct cdev *cdev_alloc(void)
-
{
-
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
-
if (p) {
-
INIT_LIST_HEAD(&p->list);
-
kobject_init(&p->kobj, &ktype_cdev_dynamic);
-
}
-
return p;
-
}
-
-
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
-
{
-
memset(cdev, 0, sizeof *cdev);
-
INIT_LIST_HEAD(&cdev->list);
-
kobject_init(&cdev->kobj, &ktype_cdev_default);
-
cdev->ops = fops;
-
}
由此可见,两个函数完成的功能基本一致,只是cdev_init()还多赋值了一个cdev->ops的值。
在cdev结构设置好后,最后通过下面调用告诉内核该结构的信息(即注册):
初始化cdev后,需要把它添加到系统中去,为此可以调用cdev_add()函数。传入cdev结构的指针,起始设备好,以及设备编号范围。
-
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
-
{
-
p->dev = dev;
-
p->count = count;
-
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
-
}
dev:该设备对应的第一个设备编号
count:和该设备关联的设备编号的数量,经常取1,但有些时候会有多个设备编号对应一个特定的设备(SCSI)。
这个调用可能会失败,返回负的错误码,则设备不会添加到系统中。但几乎总是成功返回,只要cdev_add返回了,我们的设备就活了,他的操作就会被内核调用,
因此在驱动程序还没有完全准备好处理设备上的操作时,就不能调用cdev_add。
从系统中移除一个字符设备:
-
void cdev_del(struct cdev *p)
-
{
-
cdev_unmap(p->dev, p->count);
-
kobject_put(&p->kobj);
-
}
其中 cdev_unmap() 调用 kobj_unmap() 来释放 cdev_map 散列表中的对象。kobject_put() 释放 cdev 结构本身。
2. 上述方法是新式的注册方法,也有很多利用到了老的方法:
int register_chrdev(unsigned int major,const char *name,struct file_operations *fops);
int unregister_chrdev(unsigned int major,const char *name);
/********************************************************************************************************/
阅读(1393) | 评论(0) | 转发(0) |