Chinaunix首页 | 论坛 | 博客
  • 博客访问: 134117
  • 博文数量: 49
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 182
  • 用 户 组: 普通用户
  • 注册时间: 2013-03-14 13:22
个人简介

Linux的热爱者

文章存档

2014年(7)

2013年(42)

我的朋友

分类: LINUX

2013-09-25 11:14:35

众所周知,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中用来获取主设备号的代码:

点击(此处)折叠或打开

  1. if(char_major) {
  2.     dev = MKDEV( char_major, char_minor);
  3.     result = register_chrdev_region(dev,char_nr_devs,"char");
  4. } else {
  5.     result = alloc_chrdev_region(&dev,char_minor,char_nr_devs,"char");
  6.     scull_major = MAJOR(dev);
  7. }
  8. if(result < 0){
  9.     printk(KERN_WARNING"char:canot get major %d\n",char_major);
  10.     return result;
  11. }

字符设备驱动中最重要的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设备驱动程序中的例子):

  1. struct file_operations{
  2.     .owner = THIS_MODULE, //几乎是固定格式
  3.     .llseek = scull_llseek,
  4.     .read = scull_read,
  5.     .write = scull_write,
  6.     .ioctl = scull_ioctl,
  7.     .open = scull_open,
  8.     .release = scull_release,

  9. };
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
在字符设备中用到的结构体:
  1. struct cdev {
  2.        struct kobject kobj;//每个cdev都是一个kobject
  3.        struct module *owner;//指向实现驱动的模块
  4.        const struct file_operations *ops;//操作这个字符设备文件的方法
  5.        struct list_head list;//与cdev对应的字符设备文件的inode->i_devices的链表头
  6.        dev_t dev;//起始设备编号
  7.        unsigned int count;//设备编号范围
  8. }
一个 cdev 一般它有两种定义初始化方式:静态的和动态的。
静态内存定义初始化:
  1. struct cdev my_cdev;
  2. cdev_init(&my_cdev, &fops);
  3. my_cdev.owner = THIS_MODULE;
动态内存定义初始化:
  1. struct cdev *my_cdev = cdev_alloc();
  2. my_cdev->ops = &fops;
  3. my_cdev->owner = THIS_MODULE;
两种使用方式的功能是一样的,只是使用的内存区不一样,一般视时机的数据结构需求而定
下面贴出了两个函数的代码,以具体看一下它们之间的差异。


  1. struct cdev *cdev_alloc(void)
  2. {
  3.     struct cdev *= kzalloc(sizeof(struct cdev), GFP_KERNEL);
  4.     if (p) {
  5.         INIT_LIST_HEAD(&p->list);
  6.         kobject_init(&p->kobj, &ktype_cdev_dynamic);
  7.     }
  8.     return p;
  9. }

  10. void cdev_init(struct cdev *cdev, const struct file_operations *fops)
  11. {
  12.     memset(cdev, 0, sizeof *cdev);
  13.     INIT_LIST_HEAD(&cdev->list);
  14.     kobject_init(&cdev->kobj, &ktype_cdev_default);
  15.     cdev->ops = fops;
  16. }
由此可见,两个函数完成的功能基本一致,只是cdev_init()还多赋值了一个cdev->ops的值。

在cdev结构设置好后,最后通过下面调用告诉内核该结构的信息(即注册):
初始化cdev后,需要把它添加到系统中去,为此可以调用cdev_add()函数。传入cdev结构的指针,起始设备好,以及设备编号范围
  1. int cdev_add(struct cdev *p, dev_t dev, unsigned count)
  2. {
  3.    p->dev = dev;
  4.    p->count = count;
  5.    return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
  6. }
dev
:该设备对应的第一个设备编号
count:和该设备关联的设备编号的数量,经常取1,但有些时候会有多个设备编号对应一个特定的设备(SCSI)。
这个调用可能会失败,返回负的错误码,则设备不会添加到系统中。但几乎总是成功返回,只要cdev_add返回了,我们的设备就活了,他的操作就会被内核调用,
因此在驱动程序还没有完全准备好处理设备上的操作时,就不能调用cdev_add。
从系统中移除一个字符设备:
  1. void cdev_del(struct cdev *p)
  2. {
  3.    cdev_unmap(p->dev, p->count);
  4.    kobject_put(&p->kobj);
  5. }
其中 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);

/********************************************************************************************************/

阅读(1322) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~