您的位置:首页 > 其它

主设备号和次设备号

2014-06-26 09:46 155 查看
Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在/dev目录下,称为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这些设备,系统为设备编了号,每个设备号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。
     一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。
     设备文件通常都在 /dev 目录下。如:
beyes@linux-beyes:~/C/kernel/memory> ll /dev |more

总计 0

crw-rw----  1 root uucp    4,  70 04-14 18:16 ttyS6

crw-rw----  1 root uucp    4,  71 04-14 18:16 ttyS7

crw-rw----  1 root tty         7,   0 08-08 18:58 vcs

crw-rw----  1 root tty         7,   1 08-08 18:58 vcs1

crw-rw-rw-  1 root root    1,   7 08-08 18:58 full

crw-rw-rw-  1 root root    1,   3 04-14 18:16 null
 
如上,前面第一个字符为c 的表示字符设备。在字符设备里,有主设备号和次设备号。如上1,4,7 分别是主设备号,0,1,3,7,70,71都是次设备号。一般的,主设备号标识出与设备关联的设备驱动。如 /dev/null 和 /dev/full 由 1 号驱动来管理,/dev/vcs 和/dev/vcs1由
7 号驱动来管理,/dev/ttyS6 由 4 号驱动来管理。
现在的 Linux 内核允许多个驱动共享一个主设备号,但更多的设备都遵循一个驱动对一个主设备号的原则。

内核由次设备号确定当前所指向的是哪个设备。根据所编写的驱动程序,可以从内核那里得到一个直接指向设备的指针,或者使用次设备号作为一个设备本地数组的索引。但不论如何,内核自身几乎不知道次设备号的什么事情。 

设备号的内部表示
在内核中,dev_t  类型( 在 头文件有定义 ) 用来表示设备号,包括主设备号和次设备号两部分。对于 2.6.x 内核,dev_t 是个 32 位量,其中 12 位用来表示主设备号,20 位用来表示次设备号。
在 linux/types.h 头文件里定义有
typedef __kernel_dev_t          dev_t;
typedef __u32 __kernel_dev_t;
 
主设备号和次设备号的获取

为了写出可移植的驱动程序,不能假定主设备号和次设备号的位数。不同的机型中,主设备号和次设备号的位数可能是不同的。应该使用MAJOR宏得到主设备号,使用MINOR宏来得到次设备号。下面是两个宏的定义:(linux/kdev_t.h)
 
#define MINORBITS   20                                  /*次设备号*/  
#define MINORMASK   ((1U << MINORBITS) - 1)             /*次设备号掩码*/  
#define MAJOR(dev)  ((unsigned int) ((dev) >> MINORBITS))   /*dev右移20位得到主设备号*/  
#define MINOR(dev)  ((unsigned int) ((dev) & MINORMASK))   /*与次设备掩码与,得到次设备号*/ 
 
MAJOR宏将dev_t向右移动20位,得到主设备号;MINOR宏将dev_t的高12位清零,得到次设备号。相反,可以将主设备号和次设备号转换为设备号类型(dev_t),使用宏MKDEV可以完成这个功能。
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
MKDEV宏将主设备号(ma)左移20位,然后与次设备号(mi)相或,得到设备号
 
静态分配设备号
静态分配设备号,就是驱动程序开发者,静态地指定一个设备号。对于一部分常用的设备,内核开发者已经为其分配了设备号。这些设备号可以在内核源码documentation/ devices.txt文件中找到。如果只有开发者自己使用这些设备驱动程序,那么其可以选择一个尚未使用的设备号。在不添加新硬件的时候,这种方式不会产生设备号冲突。但是当添加新硬件时,则很可能造成设备号冲突,影响设备的使用。

动态分配设备号
由于静态分配设备号存在冲突的问题,所以内核社区建议开发者使用动态分配设备号的方法。动态分配设备号的函数是alloc_chrdev_region()。
 
查看设备号
当静态分配设备号时,需要查看系统中已经存在的设备号,从而决定使用哪个新设备号。可以读取/proc/devices文件获得设备的设备号。/proc/devices文件包含字符设备和块设备的设备号,如下所示:
[root@tom /]# cat /proc/devices /*cat命令查看/proc/devices文件的内容*/  
Character devices:                  /*字符设备*/  
   1 mem  
   4 /dev/vc/0  
   7 vcs  
   13 input  
   14 sound  
   21 sg  
Block devices:                      /*块设备*/  
   1 ramdisk  
   2 fd  
   8 sd  
   253 device-mapper  
   254 mdp 
 
申请和释放设备号
内核维护着一个特殊的数据结构,用来存放设备号与设备的关系。在安装设备时,应该给设备申请一个设备号,使系统可以明确设备对应的设备号。设备驱动程序中的很多功能,是通过设备号来操作设备的。下面,首先对申请设备号进行简述。
1.申请设备号
在构建字符设备之前,首先要向系统申请一个或者多个设备号。完成该工作的函数是register_chrdev_region(),该函数在中定义:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
 
其中,from是要分配的设备号范围的起始值。一般只提供from的主设备号,from的次设备号通常被设置成0。count是需要申请的连续设备号的个数。最后name是和该范围编号关联的设备名称,该名称不能超过64字节。
和大多数内核函数一样,register_chrdev_region()函数成功时返回0。错误时,返回一个负的错误码,并且不能为字符设备分配设备号。 
     在Linux中有非常多的字符设备,在人为的为字符设备分配设备号时,很可能发生冲突。Linux内核开发者一直在努力将设备号变为动态的。可以使用alloc_chrdev_region()函数达到这个目的。(linux/fs.h)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
在上面的函数中,dev作为输出参数,在函数成功返回后将保存已经分配的设备号。函数有可能申请一段连续的设备号,这是dev返回第一个设备号。baseminor表示要申请的第一个次设备号,其通常设为0。count和name与register_chrdev_region()函数的对应参数一样。count表示要申请的连续设备号个数,name表示设备的名字。
 
2.释放设备号
使用上面两种方式申请的设备号,都应该在不使用设备时,释放设备号。设备号的释放统一使用下面的函数:
void unregister_chrdev_region(dev_t from, unsigned count);  
在上面这个函数中,from表示要释放的设备号,count表示从from开始要释放的设备号个数。通常,在模块的卸载函数中调用unregister_chrdev_region()函数。

次设备号的主要用途

1、区分设备驱动程序控制的实际设备;

2、区分不同用途的设备 (misc 系列设备)

3、区分块设备的分区 (partition)

通常,为了使应用程序区分所控制设备的类型,内核使用主设备号。而存在多台同类设备时,为了选择其中的一种,设备驱动程序就使用次设备号。

区分块设备的分区
块设备具有被称为分区的分配领域。例如,硬盘在物理上是一个设备,从内核的角度,硬盘被分为多个分区,而以这些分区为对象则形成了文件系统,此时,次设备号既表示设备,也表示分区
brw-rw----  1 root disk    8,  16 2009-09-24 sdb

brw-rw----  1 root disk    8,  17 2009-09-24 sdb1

brw-rw----  1 root disk    8,  18 2009-09-24 sdb2

brw-rw----  1 root disk    8,  21 2009-09-24 sdb5

brw-rw----  1 root disk    8,  22 2009-09-24 sdb6

brw-rw----  1 root disk    8,  23 2009-09-24 sdb7

brw-rw----  1 root disk    8,  24 2009-09-24 sdb8

主设备号找驱动、次设备号找设备的内核实现
   Linux内核允许多个驱动共享一个主设备号,但更多的设备都遵循一个驱动对一个主设备号的原则。

内核维护着一个以主设备号为key的全局哈希表,而哈希表中数据部分则为与该主设备号设备对应的驱动程序(只有一个次设备)的指针或者多个同类设备驱动程序组成的数组的指针(设备共享主设备号)。根据所编写的驱动程序,可以从内核那里得到一个直接指向设备驱动的指针,或者使用次设备号作为索引的数组来找到设备驱动程序。但无论哪种方式,内核自身几乎不知道次设备号的什么事情。如下图所示:
   


                            图1:应用程序调用open时通过主次设备号找到相应驱动
   来看内核中一个简单的字符设备驱动的例子,其主设备号为1,根据LANANA标准,该设备有10个不同的次设备号。每个都提供了一个不同的功能,这些都与内存访问操作有关。下面列出一些次设备号,以及相关的文件名和含义。
                                                表1 用于主设备号1的各个从设备号

    从设备号                                               文件                                                           含义

         1                                                 /dev/mem                                                    物理内存

         2                                                 /dev/kmem                                                内核虚拟地址空间

         3                                                 /dev/null                                                       比特位桶

         4                                                 /dev/port                                                     访问I/O端口

         5                                                 /dev/zero                                                 WULL字符源

         8                                                 /dev/random                                          非确定性随机数发生器
    一些设备是我们熟悉的,特别是/dev/null。根据设备描述我们可以很清楚地知道尽管这些从设备都涉及到内存访问,但所实现功能有很大差别。然后来看下图1中主设备号为1的memory_fops中定义了哪些函数指针。代码如下:
driver/char/mem.c
 

[cpp] view
plaincopy

 

static const struct file_operations memory_fops = {  

    .open = memory_open,  

    .llseek = noop_llseek,  

};  

其中函数memory_open最为关键,其作用是根据次设备号找到次设备的驱动程序。

 

[cpp] view
plaincopy

 

static int memory_open(struct inode *inode, struct file *filp)  

{  

    int minor;  

    const struct memdev *dev;  

  

    minor = iminor(inode); /* get the minor device number commented by guoqingbo */  

    if (minor >= ARRAY_SIZE(devlist))  

        return -ENXIO;  

  

    dev = &devlist[minor];/* select the specific file_operations */  

    if (!dev->fops)  

        return -ENXIO;  

  

    filp->f_op = dev->fops;  

    if (dev->dev_info)  

        filp->f_mapping->backing_dev_info = dev->dev_info;  

  

    /* Is /dev/mem or /dev/kmem ? */  

    if (dev->dev_info == &directly_mappable_cdev_bdi)  

        filp->f_mode |= FMODE_UNSIGNED_OFFSET;  

  

    if (dev->fops->open)  //open the device  

        return dev->fops->open(inode, filp);  

  

    return 0;  

}  

该函数用到的图1中的devlist数组定义如下:

[cpp] view
plaincopy

 

static const struct memdev {  

    const char *name;  

    mode_t mode;  

    const struct file_operations *fops;  

    struct backing_dev_info *dev_info;  

} devlist[] = {  

     [1] = { "mem", 0, &mem_fops, &directly_mappable_cdev_bdi },  

#ifdef CONFIG_DEVKMEM  

     [2] = { "kmem", 0, &kmem_fops, &directly_mappable_cdev_bdi },  

#endif  

     [3] = { "null", 0666, &null_fops, NULL },  

#ifdef CONFIG_DEVPORT  

     [4] = { "port", 0, &port_fops, NULL },  

#endif  

     [5] = { "zero", 0666, &zero_fops, &zero_bdi },  

     [7] = { "full", 0666, &full_fops, NULL },  

     [8] = { "random", 0666, &random_fops, NULL },  

     [9] = { "urandom", 0666, &urandom_fops, NULL },  

    [11] = { "kmsg", 0, &kmsg_fops, NULL },  

#ifdef CONFIG_CRASH_DUMP  

    [12] = { "oldmem", 0, &oldmem_fops, NULL },  

#endif  

};  

通过上面代码及图1可看出,memory_open实际上实现了一个分配器(根据次设备号区分各个设备,并且选择适当的file_operations),图2说明了打开内存设备时,文件操作是如何改变的。所涉及的函数逐渐反映了设备的具体特性。最初只知道用于打开设备的一般函数,然后由打开与内存相关设备文件的具体函数所替代。接下来根据选择的次设备号,进一步细化函数指针
,为不同的次设备号最终选定函数指针。



                                    图2:设备驱动程序函数指针的选择过程


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息