linux驱动模型学笔记——字符设备号

 

内核版本:linux-2.6.34.1

 

字符设备号

 

一、简介

Linux下的应用程序在访问字符设备时,一般都是通过设备节点访问的。设备节点一般都在/dev目录下。字符设备文件的第一个标志是c,如下所示:

[machi@localhost dev]$ ll | grep tty

……..

    crw--w----. 1 root root      4,   0 Sep 26 18:05 tty0

crw--w----. 1 root root      4,   1 Sep 25 19:04 tty1

crw--w----. 1 root tty       4,  10 Sep 26 18:05 tty10

crw--w----. 1 root tty       4,  11 Sep 26 18:05 tty11

crw--w----. 1 root tty       4,  12 Sep 26 18:05 tty12

……..

在上面的输出中,每一个文件代表一个设备,在时间前面有两个用逗号隔开的数字,第一个数字是主设备号,第二个数字是次设备号。一般认为一个主设备号对应一个驱动程序,可以看到,这里列出的TTY设备都由驱动程序4管理。不过,也可以一个主设备号对应多个驱动程序。一个次设备号对应一个设备, 所以上面输出中表示的0110等数字代表不同的设备。一个驱动程序,可以管理多个此类型的设备,设备数可以有2^20个,原因是次设备号有20位,不过不可能有这么多设备的。

 

二、设备号的内部表示

设备号的类型定义在内核代码树的文件/include/linux/Types.h中:

typedef __u32 __kernel_dev_t;

typedef __kernel_dev_t         dev_t;

__u32代表一个无符号的32位整形数。

32位的设备号中,主设备号占高12位,次设备号占低20位。在操作设备号时,可以使用内核提供的宏:

#define MINORBITS     20

#define MINORMASK   ((1U << MINORBITS) - 1)

 

#define MAJOR(dev)     ((unsigned int) ((dev) >> MINORBITS))

#define MINOR(dev)     ((unsigned int) ((dev) & MINORMASK))

#define MKDEV(ma,mi)       (((ma) << MINORBITS) | (mi))

记得看过一本比较老的linux教材,上面说linux只能注册255个主设备号和255个次设备号。不过,现在的内核可以注册2^12个主设备号和2^20个次设备号,但内核具体是如何做的,下面再详细介绍。

 

三、设备号的申请与释放

1. 静态申请设备号

int register_chrdev_region(dev_t from, unsigned count, const char *name)

此函数就是用于静态申请一个设备号,所谓的静态申请,就是在程序编码时,就已经确定了设备号是多少,并在程序中申请。当驱动程序装载前,设备号就是已知的了。如果静态申请时,主设备号没有和系统中某个字符设备的主设备号相同,则此函数肯定会成功返回;如果系统中某个已注册的字符设备的主设备号与你申请的主相同,则内核还需要检查这两个设备的次设备号范围有没有重合,若有重合部分,则register_chrdev_region返回-EBUSY,表示系统正忙,你申请的设备号已被占用,若无重合部分,则register_chrdev_region返回0,表示成功。

2. 动态分配设备号

上面的静态申请设备号会使自己编写的驱动程序与其它驱动程序发生冲突的可能性变大,所以,这里介绍动态分配设备号。动态分配设备号的函数是:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

所谓的动态分配设备号,是指在驱动程序中通过调用此函数,系统将为驱动程序动态分配一个主设备号,将分配到的主设备号与参数baseminor组合成一个设备号,通过输出参数dev返回给用户。这样做的好处是,大大减小了设备号冲突的情况。但是,如果用户使用了静态申请设备号,只要用户申请的主设备号在0~2^12-1之间,次设备号在0~2^20-1之间,并且设备号不冲突,内核就会成功注册该设备号;而如果用户使用了动态分配设备号,内核为用户分配的主设备号只会在1~254之间,可以想像,如果这254个设备号全部被占用,则动态分配设备号会失败,不过,常用的设备只有三十多个,所以基本上不用担心动态分配的时候内核会返回失败。

3. 设备号的释放

设备号的释放只有一个函数,比较简单:

void unregister_chrdev_region(dev_t from, unsigned count)

第一个参数from,表示要释放的设备号范围的起始号码,第二个参数count,表示要释放的设备号个数。比如,现在要释放主设备号为248,次设备号分别为567这三个设备,则unregister_chrdev_region的参数应该设置为from = 248 << 20 | 5count = 3

 

四、字符设备号的内核实现

1. 字符设备号的结构表示

字符设备号在内核中由一个结构体表示:

#define CHRDEV_MAJOR_HASH_SIZE     255

static struct char_device_struct {

       struct char_device_struct *next;

       unsigned int major;

       unsigned int baseminor;

       int minorct;

       char name[64];

       struct cdev *cdev;         /* will die */

} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

与字符设备号相关的信息会保存在上面的结构中,而chrdevs是内核维护的一个指向struct char_device_struct的指针数组。此数组可看作一个散列表,其散列函数是:

static inline int major_to_index(int major)

{

       return major % CHRDEV_MAJOR_HASH_SIZE;

}

由此可见,现在的Linux内核可以容纳2^12个主设备号,而不是以前版本的Linux内核只允许注册255个主设备号。比如,主设备号为4259514这四个驱动程序,在数组chrdevs中的下标都是4,以4259514为顺序,即主设备号从小到大的顺序形成一个链表,表头指针保存在chrdevs[4]中。数组chrdevs的形式如下图:

linux驱动模型学笔记——字符设备号_第1张图片

2. 字符设备号的操作函数

A.申请函数

1. int register_chrdev_region(dev_t from, unsigned count, const char *name)

2. int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,

                     const char *name)

3. int __register_chrdev(unsigned int major, unsigned int baseminor,

                    unsigned int count, const char *name,

                    const struct file_operations *fops)

以上三个函数均是向内核为字符设备申请设备号,前两个函数上面已经介绍过了,这里不再赘述。函数__register_chrdevLDD3中没有介绍,不过这个函数用上去很方便,但是有些非主流。__register_chrdev,从字面意思来看,就是注册一个字符设备,而不只是字符设备号。函数__register_chrdev会为用户自动生成一个struct cdev结构体,并通过给定的参数初始化该结构。如果用户传入的major0,则__register_chrdev会为用户动态分配一个主设备号。然后,__register_chrdev调用cdev_add(),将刚才动态生成的struct cdev注册到内核中。最后,__register_chrdev将刚才生成的struct cdev结构的首地址保存在与刚才申请的设备号相对应的结构struct char_device_struct的成员cdev中(需要说明的是,在此三个申请设备号的函数中,只有__register_chrdev才会设置struct char_device_struct的成员cdev,因为只有__register_chrdev是内核为用户动态生成struct cdev)。__register_chrdev执行成功,返回0,若失败,则返回一个负值。

从上面对__register_chrdev的介绍来看,此函数非常方便,只需要用户指定好与字符设备相关的文件操作,即为结构fops赋值,并且设定好名称,只须一次调用,便可以完成对struct cdev的生成及初始化,设备号的申请,以及设备的注册。但是,这样做只是单独生成了一个struct cdev结构,并没有将与该设备相关的其它数据和struct cdev结构关联起来。若此驱动对应的设备存在多个时,并且每个设备都有自己的私有数据,则只调用函数__register_chrdev显然是达不到要求的。

函数__register_chrdev的流程如下图:

linux驱动模型学笔记——字符设备号_第2张图片

上面提到的三个函数,虽然从表面看来,各有各的特点,但是,它们在申请设备号时,调用的是同一个内部函数:

static struct char_device_struct *

__register_chrdev_region(unsigned int major, unsigned int baseminor,

                        int minorct, const char *name)

此函数的流程图如下:

linux驱动模型学笔记——字符设备号_第3张图片

函数__register_chrdev_region需要四个参数,如果major0,则此函数为用户动态分配一个主设备号,不过动态分配的设备号只可能在1~254之间,因为,内核只是在chrdevs中从2541查找元素为NULL的项,如果找到,则分配该值,如果找不到,则返回-EBUSY。当分配到一个主设备号之后,__register_chrdev_region会根据该主设备号,和从参数传入的次设备号起始值,以及次设备号个数生成一个struct char_device_struct,并且根据该主设备号找到对应的链表。如果链表已经存在与刚才分配的主设备号相同的节点,则检查该节点的次设备号范围是否与刚才分配的次设备号范围重合,若不重合则将生成的struct char_device_struct按照主设备升序插入到该链表中,否则返回-EBUSY

B.释放函数

1. void unregister_chrdev_region(dev_t from, unsigned count)

2. void __unregister_chrdev(unsigned int major, unsigned int baseminor,

                      unsigned int count, const char *name)

第一个函数在上面已经介绍过了。这里要讲的是第二个函数。__unregister_chrdev从字面意思就可以看出,它与__register_chrdev是一对函数,是取消对一个字符设备的注册。此函数先通过用户给定的主设备号,次设备号初始值,和次设备号个数(需要说明的是,这里传入的name在函数内部根本没有用,所以这个字段可以设为NULL,至于为什么还要保留这个字段,可能是因为以前版本的内核会用到name这个参数),在chrdevs中找到对应的struct char_device_struct,若找到,则将该结构从链表中移出,并通过struct char_device_struct的成员cdev释放内核动态为设备分配的内存,最后,释放此struct char_device_struct结构。

可以看出,用于设备号释放的两个函数的返回值都是void,这表示如果用户传入了一个无效的设备号信息,这两个函数也只是什么也不做,不会对系统造成什么影响,所以没有必要检查其返回值。

对比以上两个函数,__unregister_chrdevunregister_chrdev_region多删除一个struct cdev,而unregister_chrdev_region可以删除一个指定范围的设备号。这两个函数在内部实现时,都调用了内部函数:

static struct char_device_struct *

__unregister_chrdev_region(unsigned major, unsigned baseminor, int minorct)

此函数的作用是,根据参数指定的主设备号,次设备号初始值,以及次设备号个数,在chrdevs中找到对应的struct char_device_struct,将该节点从链表中移出,并返回该节点的首地址。函数流程如下:

linux驱动模型学笔记——字符设备号_第4张图片

    至此,字符设备号的介绍就结束了。总结一下,常用的字符设备号的申请和释放过程如下:

1. alloc_chrdev_region

2. unregister_chrdev_region

 

你可能感兴趣的:(c,linux,struct,File,null,linux内核)