Linux 设备号管理 II

使用设备号的一个基本需求就是根据设备号索引能够找到设备,而在Linux设备模型中,所有的设备最后都归结到kobject,所以问题也就转变成 dev_t到kobject实例的mapping问题,内核中使用kobj_map来描述这种映射,具体地,内核创建了两个实例cdev_map和bdev_map来分别管理字符设备和块设备及其设备号的映射关系, 二者都是kobj_map类型, 可以用下图简要表示

本文主要讨论如下问题:

  1. kobj_map是如何实现这种映射的。
  2. kobj_map实现的这种映射是如何被使用的。

kobj_map

实际上, 这个kobj_map也是通过HASH来建立mapping的:以dev_t为HASH索引,通过每一个HASH表项(probe)上的链表来找到相应的kobject,和char_device_structblk_major_name完全不同,kobj_map有更大的自由度,它的每一条链表是完全根据range大小顺序排列的,链表中的每个probe对象负责的设备号区间可以重叠。此外,通过对大range的分割,保证了只要kobj_map的映射管理已经覆盖到了某个dev_t,那么就一定可以在相应的一条链表中找到,具体的表现是:HASH检索时用到的major与该链表中的某个MAJOR(dev_t)可能不一致。总的来说,相比于`char_device_struct`的”不重不漏”原则,kobj_map秉承**“优中取优,一次到位”**的原则,检索时会优先获取符合条件的最小range。接下来我们来分析一下这个结构。

//drivers/base/map.c
 19 struct kobj_map {
 20         struct probe {
 21                 struct probe *next;
 22                 dev_t dev;
 23                 unsigned long range;
 24                 struct module *owner;
 25                 kobj_probe_t *get;
 26                 int (*lock)(dev_t, void *);
 27                 void *data;
 28         } *probes[255];
 29         struct mutex *lock;
 30 };

–20–>每一个HASH表项链表的节点,每一个节点都表示一个设备号区间及其映射的kobject
–21–>链表节点连接处
–22–>该节点的设备号区间的起始设备号
–23–>该节点的设备号区间长度
–25–>函数指针实例,原型为typedef struct kobject *kobj_probe_t(dev_t, int *, void *);,保存根据dev_t获取kobject的方法
–26–>函数指针,保存锁定kobject实例,防止其被释放的方法。
–27–>与kobject关联的自定义结构,比如封装了kobject的cdev或gendisk实例
–29–>操作该结构使用的mutex

同样,内核也实现了针对该结构的操作方法.

kobj_map_init()

初始化kobj_map实例的函数,在相关子系统的初始化时期被调用,比如block subsys的__init genhd_device_init(void)或者char subsys的__init chrdev_init(void)

//drivers/base/map.c
136 struct kobj_map *kobj_map_init(kobj_probe_t *base_probe, struct mutex *lock)
137 {
138         struct kobj_map *p = kmalloc(sizeof(struct kobj_map), GFP_KERNEL);
139         struct probe *base = kzalloc(sizeof(*base), GFP_KERNEL);
140         int i;
141 
142         if ((p == NULL) || (base == NULL)) {
143                 kfree(p);
144                 kfree(base);
145                 return NULL;
146         }
147 
148         base->dev = 1;
149         base->range = ~0;
150         base->get = base_probe;
151         for (i = 0; i < 255; i++)
152                 p->probes[i] = base;
153         p->lock = lock;
154         return p;
155 }

–139–>分配链表”尾”
–148–>初始化probe实例base,作为”泛化”项,base映射[1,~0]区间的dev_t,即所有的设备号!base用来匹配无法匹配到该链表中任何其他项的设备号,它有自己的kobj_probe_t方法,该方法最终会尝试通过__request_module()>call_modprobe()来加载ko,并返回NULL,这样,在下一次检索的时候就可以通过设备号真正地获取kobj。
–151–>将链表”尾”链入每一个HASH表项,作为其初始值
–154–>初始化整个HASH表使用的Mutex

kobj_map()/kobj_ummap()

该函数实现的功能是向HASH表中插入/删除表项, 这里仅分析插入的代码。既然是插入,那么正确初始化一个probe实例所必需的信息是一定要提供的,这点从传入的参数即可看出。

32 int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
33              struct module *module, kobj_probe_t *probe,
34              int (*lock)(dev_t, void *), void *data)
35 {
36         unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
37         unsigned index = MAJOR(dev);
38         unsigned i;
39         struct probe *p;
40 
41         if (n > 255)
42                 n = 255;
43 
44         p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);
45 
46         if (p == NULL)
47                 return -ENOMEM;
48 
49         for (i = 0; i < n; i++, p++) {
50                 p->owner = module;
51                 p->get = probe;
52                 p->lock = lock;
53                 p->dev = dev;
54                 p->range = range;
55                 p->data = data;
56         }
57         mutex_lock(domain->lock);
58         for (i = 0, p -= n; i < n; i++, p++, index++) {
59                 struct probe **s = &domain->probes[index % 255];
60                 while (*s && (*s)->range < range)
61                         s = &(*s)->next;
62                 p->next = *s;
63                 *s = p;
64         }
65         mutex_unlock(domain->lock);
66         return 0;
67 }

–32–>要插入到哪个HASH表domain,需要建立映射的设备号区间dev和range,该HASH归属的模块,以及probe结构所需的其他成员…
–36–>为支持跨major做准备
–37–>根据major获取HASH索引index
–44–>有多少的major,就分配多少个probe对象
–49–>这些对象完全相同。–58–>遍历需要遍历的每一个链表。
–60–>每一个链表都按照range的大小,从前到后排列
–62–>找到合适的位置就插入probe对象。n>1时,会将probe对象分别根据range放在index和index+1两条链表中。如前文所述,对于一个dev_t,它被一个probe覆盖了,但这个probe的range是跨多个major的,那么怎么能保证无论这个dev_t属于哪个major链表,都能被一次遍历链表找到呢?就是将这个probe加入到每一个它覆盖到的major链表中!这种设计可以让lookup在一条链表中完成,是典型的空间换时间。

kobj_lookup()

这个函数是这组接口里被调用次数最多的一个函数,用于在kobj_map中检索需要的kobject实例。

 96 struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
 97 {
 98         struct kobject *kobj;
 99         struct probe *p;
100         unsigned long best = ~0UL;
101 
102 retry:
103         mutex_lock(domain->lock);
104         for (p = domain->probes[MAJOR(dev) % 255]; p; p = p->next) {
105                 struct kobject *(*probe)(dev_t, int *, void *);
106                 struct module *owner;
107                 void *data;
108 
109                 if (p->dev > dev || p->dev + p->range - 1 < dev)
110                         continue;
111                 if (p->range - 1 >= best)
112                         break;
113                 if (!try_module_get(p->owner))
114                         continue;
115                 owner = p->owner;
116                 data = p->data;
117                 probe = p->get;
118                 best = p->range - 1;
119                 *index = dev - p->dev;
120                 if (p->lock && p->lock(dev, data) < 0) {
121                         module_put(owner);
122                         continue;
123                 }
124                 mutex_unlock(domain->lock);
125                 kobj = probe(dev, index, data);
126                 /* Currently ->owner protects _only_ ->probe() itself. */
127                 module_put(owner);
128                 if (kobj)
129                         return kobj;
130                 goto retry;
131         }
132         mutex_unlock(domain->lock);
133         return NULL;
134 }

–104–>遍历根据MAJOR(dev_t)获取到的链表,如前文所述,只要这个dev_t已经被覆盖,那么一定可以在这个链表中找到。
–109–>当前的probe对象的dev不合适,继续循环遍历当前链表
–111–>range已经大于”泛化”probe对象,结束循环
–113–>对module的引用计数-1,如果为0则不能返回该module的kobj。
–119–>index不是HASH的索引,而是probe对象中的dev_t与待检索dev_t的差值。
–125–>回调probe方法,获取kobject并返回之。

Example

在具体的设备中,通常还会对上述方法进行封装,这里以block device为例讨论这种映射的使用方式。当前版本的block device使用kobj_map以及IDR机制对gendisk对象进行管理,kobj_map主要是对常规分区的设备号进行管理,IDR主要用来管理超出gendisk->minors数量的分区的设备号,参见add_disk()->blk_alloc_devt。本文仅讨论其kobj_map部分,kobj_map部分的相关的实现多在“block/genhd.c”。

genhd_device_init()

900 static int __init genhd_device_init(void)
901 {
902         int error;
903 
904         block_class.dev_kobj = sysfs_dev_block_kobj;
905         error = class_register(&block_class);
906         if (unlikely(error))
907                 return error;
908         bdev_map = kobj_map_init(base_probe, &block_class_lock); 
909         blk_dev_init();
910 
911         register_blkdev(BLOCK_EXT_MAJOR, "blkext");
912 
913         /* create top-level block dir */
914         if (!sysfs_deprecated)
915                 block_depr = kobject_create_and_add("block", NULL);
916         return 0;
917 }

–900–>__init 可以看出,这个函数在系统初始化的时候就会被调用
–908–>构建bdev_map供系统中所有的gendisk使用
–909–>通过slab分配request_queue等结构

blk_register_region()

“注册”设备号,看起来是”获取”设备号(register_chrdev_region()),其实是建立设备号dev_t的映射。

//block/genhd.c
 475 void blk_register_region(dev_t devt, unsigned long range, struct module *module,
 476                          struct kobject *(*probe)(dev_t, int *, void *),
 477                          int (*lock)(dev_t, void *), void *data)
 478 {
 479         kobj_map(bdev_map, devt, range, module, probe, lock, data);  
 480 }

get_gendisk()

681 struct gendisk *get_gendisk(dev_t devt, int *partno)
682 {
683         struct gendisk *disk = NULL;
684 
685         if (MAJOR(devt) != BLOCK_EXT_MAJOR) {
686                 struct kobject *kobj;
687 
688                 kobj = kobj_lookup(bdev_map, devt, partno);                                        
689                 if (kobj)
690                         disk = dev_to_disk(kobj_to_dev(kobj));
691         } else {
                   ...
701         }
702 
703         return disk;
704 }
705 EXPORT_SYMBOL(get_gendisk);

–685–>如果不是extended major,extended major用于分区,对应hd_struct而不是gendisk
–688–>使用kobj_lookup获取kobj
–690–>通过kobj获取的gendisk对象并返回

Leave a Reply

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