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.

Discover more from sketch2sky

Subscribe now to keep reading and get access to the full archive.

Continue reading