Linux 设备号管理 I

Linux使用设备号——dev_t类型的一个数值来标识一个字符设备或块设备对象,虽然实际开发中通常使用对cdev或gendisk的更高层的封装接口,诸如输入子系统,MTD,MutiDisk等设备模型,但万丈高楼平地起,管理这些复杂设备的根基都是对cdev或gendisk对象的管理,其中,设备号的获取以及管理就是重要的一个方面,这就是本文主要讨论问题。

和所有其他资源一样,计算机酷爱使用数字id来标识一个资源,从页面到进程,从中断到地址,自然而然,linux对设备的标识也不例外,这就是设备号dev_t。设备号和PID,中断号一样,Device Number本身就是系统资源,在用户空间,我们可以通过`$cat /proc/devices`来查看主设备号的分配情况,只要一个设备注册的设备号,就可以在/proc中体现,但仅限于major。

$cat /proc/devices 
Character devices:
  1 mem
  4 /dev/vc/0
Block devices:
259 blkext
  7 loop

也可以通过`$ls -l /dev`查看设备文件的设备号占用情况,/dev下的设备文件需要使用sysfs接口并通过udevd来创建,一旦创建完毕我们可以同时查看major和minor。下例中的10就是major,235就是minor

$ls -l /dev/
total 0
crw-------  1 root root     10, 235 Sep  9 22:19 autofs

对于驱动开发者,内核文档“Documentation/devices.txt”对设备号有比较准确的叙述。字符设备和块设备各享有0-255个设备号,其中0是保留设备号,用来表示无名设备,同时HASH值为0的255也是保留设备号。

//Documentation/devices.txt
  86   0             Unnamed devices (e.g. non-device mounts)
3111 255 char        RESERVED
3113 255 block       RESERVED

下面的这个设备号区间是留给普通开发用的,比如如果动态分配,获取到的设备号常常是250…

//Documentation/devices.txt
3104 240-254 char    LOCAL/EXPERIMENTAL USE
3106 240-254 block   LOCAL/EXPERIMENTAL USE
//include/uapi/asm-generic/int-l64.h
 26 typedef unsigned int __u32;  
//include/linux/types.h
 12 typedef __u32 __kernel_dev_t;
 15 typedef __kernel_dev_t          dev_t;   
 
//include/linux/kdev_t.h
  6 #define MINORBITS       20
  7 #define MINORMASK       ((1U << MINORBITS) - 1)
  8 
  9 #define MAJOR(dev)      ((unsigned int) ((dev) >> MINORBITS))                                       
 10 #define MINOR(dev)      ((unsigned int) ((dev) & MINORMASK))
 11 #define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

字符设备的Device number

既然设备号本身就是一种资源,那么在使用之前,就需要向内核申请。对字符设备来说,内核使用chrdevs[]这个全局HASH表来标记已经注册的设备号,我们使用内核的相关API来注册设备号,使用获取的设备号初始化cdev实例(include/linux/cdev.h +12)。当然,从内核注销一个cdev也要将其占用的设备号释放。

//fs/char_dev.c
 31 static struct char_device_struct {
 32         struct char_device_struct *next;
 33         unsigned int major;
 34         unsigned int baseminor;
 35         int minorct;
 36         char name[64];
 37         struct cdev *cdev;              /* will die */
 38 } *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

–32–>该HASH表的每一个表项都是一条链表。
–37–>那个`/*will die*/`从2.6开始就在内核了[汗],现在没有函数需要这个成员,只是没有剔除而已。这张HASH表可以用下图简要的表示。

__register_chrdev_region()

内核提供的下述接口来注册/注销一个设备号区间给字符设备

//include/linux/fs.h +2267
2266 #define CHRDEV_MAJOR_HASH_SIZE  255
2267 extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
2268 extern int register_chrdev_region(dev_t, unsigned, const char *);
2269 extern int __register_chrdev(unsigned int major, unsigned int baseminor,unsigned int count, const char *name,const struct file_operations *fops);

本节讨论的这个函数是alloc_chrdev_region()和register_chrdev_region实现的核心函数,无论是手动注册(alloc_chrdev_region)还是自动注册(register_chrdev_region),最终都会操作相应的管理结构,这部分核心工作就在这个__register_chrdev_region中完成。 这个函数设计的十分巧妙。该函数在申请设备号的时候,秉承“不成功便成仁”的意志,要么完成注册任务,要么返回错误。

//fs/char_dev.c +74
 73 static struct char_device_struct *
 74 __register_chrdev_region(unsigned int major, unsigned int baseminor,int minorct, const char *name)
 76 {
 77         struct char_device_struct *cd, **cp;
 78         int ret = 0;
 79         int i;
 81         cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
 88         if (major == 0) {
 89                 for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
 90                         if (chrdevs[i] == NULL)
 91                                 break;
 92                 }
 98                 major = i;
 99         }
101         cd->major = major;
102         cd->baseminor = baseminor;
103         cd->minorct = minorct;
104         strlcpy(cd->name, name, sizeof(cd->name));
105 
106         i = major_to_index(major);
107 
108         for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
109                 if ((*cp)->major > major ||
110                     ((*cp)->major == major &&
111                      (((*cp)->baseminor >= baseminor) ||
112                       ((*cp)->baseminor + (*cp)->minorct > baseminor))))
113                         break;
115         /* Check for overlapping minor ranges.  */
116         if (*cp && (*cp)->major == major) {
117                 int old_min = (*cp)->baseminor;
118                 int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
119                 int new_min = baseminor;
120                 int new_max = baseminor + minorct - 1;
121 
122                 /* New driver overlaps from the left.  */
123                 if (new_max >= old_min && new_max <= old_max) {
124                         ret = -EBUSY;
125                         goto out;
126                 }
127 
128                 /* New driver overlaps from the right.  */
129                 if (new_min <= old_max && new_min >= old_min) {
130                         ret = -EBUSY;
131                         goto out;
132                 }
133         }
135         cd->next = *cp;
136         *cp = cd;
138         return cd;
139 out:
140         mutex_unlock(&chrdevs_lock);
141         kfree(cd);
142         return ERR_PTR(ret);
143 }

该函数是申请/注册设备号的核心函数
–88–>在major==0时,则自行寻找最小未用的HASH键值进行动态分配,注意,这里仅从chrdevs[i]==NULL处开始分配,即整条链表都为空,试想一种情况:chrdevs[255]没有一个元素为NULL,即不存在某个major或major%255未被使用,但是某个major中有满足要求的minor区间,那么这种分配也不会成功的。
–101–>初始化一个struct char_device_struct*
–106–>计算HASH键值
–108-113–>遍历chrdevs[]一个index上的链表,这段判断就是确定cp的位置,以便之后插入新的char_device_struct对象cd, 其实,在找cp的过程中,如果通过`(*cp)->baseminor + (*cp)->minorct > baseminor`来找到cp,那么这个cd注定是失败的,只是这里我们只关注cp的确定,能否成功由后面的代码判断,图例如下,数轴中的灰色块表示一段已注册的设备号区间,由一个char_struct_device实例表示,蓝色表示描述期待注册设备号区间的char_struct_device实例(cd):

–116–>检查cd的minor区间是否与之前有重叠(overlap),一旦有overlap即报错,即上图中的A、B。 当然,overlap只会发生在major相同的情况下
–135-136–>这两行就是将新的char_device_struct对象插入到HASH中,具体的说是cp之前,其中对二级指针的使用很巧妙

register_chrdev_region()

//fs/char_dev.c +174
174 int register_chrdev_region(dev_t from, unsigned count, const char *name)
175 {
176         struct char_device_struct *cd;
177         dev_t to = from + count;
178         dev_t n, next;
179 
180         for (n = from; n < to; n = next) {
181                 next = MKDEV(MAJOR(n)+1, 0);
182                 if (next > to)
183                         next = to;
184                 cd = __register_chrdev_region(MAJOR(n), MINOR(n),
185                                next - n, name);
188         }
189         return 0;
197 }

–180–>__register_chrdev_region()这个接口只能在一个HASH键上注册一段设备区间,而这个接口使用这个简单for(),实现了跨major的注册,比如,我们可以注册起始major:minor=10:20 长度为2^20的设备号,最终可以注册到从10:20~11:20的区间,当然,这是在有足够空闲区间的情况下,否则某一次循环体中__register_chrdev_region()就会报错了

块设备的Device number

块设备的设备号管理和字符设备有一些不同。在块设备中,申请主设备号同样是可以通过类似的HASH表,也有支持手动注册和自动注册major,相应的实例gendisk中同样保存了设备号信息。但minor是与该块设备驱动的partition支持情况有关的,并不像字符设备一样怎么注册都可以(如果不冲突的话)。 gendisk中的minors类似于char_dev_struct中的minorct,但不完全相同,它表示disk允许的包括disk本身在内的最大partition的数量,例如,/dev/sdb不允许分区,那么它的minors已经是1,允许分一个区/dev/sdb1,则它的minors就是2…,相关的还有partno,即”partition number”,对于disk /dev/sdb来讲,它至少有一个partno就是0,类似的,/dev/sdb1的partno就是1… 本文不对Block Device minor进行深入讨论,相关内容会在Block Device驱动框架中介绍 在Block Device中,使用blk_major_name来作为HASH表项,相当于Char Device中的char_device_struct。

//include/linux/fs.h
2194 extern int register_blkdev(unsigned int, const char *);
2293 #define BLKDEV_MAJOR_HASH_SIZE  255
//block/genhd.c
 243 static struct blk_major_name {
 244         struct blk_major_name *next;
 245         int major;
 246         char name[16];
 247 } *major_names[BLKDEV_MAJOR_HASH_SIZE];

Block Device中的Device Number HASH只考虑major,并不理会minor的问题,minor的问题必须要交给驱动中的分区策略来处理,内核也是这种设计,所以minor的相关API都与gendisk的alloc或init直接相关。

register_blkdev()

这个接口对标到cdev机制中是__register_chrdev_region(),二者有相当多的实现是一样的。此外,由于这个函数只负责major,所以比__register_chrdev_region()要简单很多,这里仅分析二者不同的部分。

//block/genhd.c
 285 int register_blkdev(unsigned int major, const char *name)
 286 {
 287         struct blk_major_name **n, *p;
 288         int index, ret = 0;
 293         if (major == 0) {                                              
 294                 for (index = ARRAY_SIZE(major_names)-1; index > 0; index--) {
 295                         if (major_names[index] == NULL)
 296                                 break;
 297                 }
 298 
 299                 if (index == 0) {
 300                         printk("register_blkdev: failed to get major for %s\n",
 301                                name);
 302                         ret = -EBUSY;
 303                         goto out;
 304                 }
 305                 major = index;
 306                 ret = major;
 307         }
 308 
 309         p = kmalloc(sizeof(struct blk_major_name), GFP_KERNEL);
 310         if (p == NULL) {
 311                 ret = -ENOMEM;
 312                 goto out;
 313         }
 315         p->major = major;
 316         strlcpy(p->name, name, sizeof(p->name));
 317         p->next = NULL;
 318         index = major_to_index(major);
 319 
 320         for (n = &major_names[index]; *n; n = &(*n)->next) {
 321                 if ((*n)->major == major)
 322                         break;
 323         }
 324         if (!*n)
 325                 *n = p;
 326         else
 327                 ret = -EBUSY;
 336         return ret;
 337 }

–293–>和__register_chrdev_region()一样,如果传入的major==0,那么会自动在HASH表中搜索可用的major,但这种搜索也会漏掉一些可能的值,参见__register_chrdev_region()
–318–>计算HASH键值
–320–>遍历某个键值对应的链表
–324-327–>如果和major表示的主设备号已经被注册,返回EBUSY,否则将major插入链表尾。

alloc_disk()

看名字也知道这是用来分配的,可正如很多linux内核API一样,名为alloc的函数可能连带着将initialization的工作也做了,

//block/genhd.c
1254 struct gendisk *alloc_disk(int minors)    
1255 {
1256         return alloc_disk_node(minors, NUMA_NO_NODE);
1257 }
1258 EXPORT_SYMBOL(alloc_disk);

//block/genhd.c
1260 struct gendisk *alloc_disk_node(int minors, int node_id)
1261 {
1262         struct gendisk *disk;
1264         disk = kzalloc_node(sizeof(struct gendisk), GFP_KERNEL, node_id);
1265         if (disk) {
                    ...
1290                disk->minors = minors;
                    ...
1295         }
1296         return disk;
1297 }
1298 EXPORT_SYMBOL(alloc_disk_node);

–1256–>gendisk实例中的minors成员是在调用alloc_disk()时由调用者传入的,毕竟这个成员表示一个disk的最大partition数量,只有驱动开发人员才有权指定。 –1264–>这就是不让使用kmalloc自己分配gendisk实例的原因,内核使用slab/slub/slat机制来管理这种经常使用的对象,而kzalloc_node就是其中一个相关API,后续的文章会有详细分析
–1290–>alloc_disk_node做了很多初始化工作,本文只关注这一点,对minors成员的初始化,有了这句,就可以知道gendisk中的minors从何而来。

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.