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从何而来。