本文所有内容基于内核版本Linux-v3.2.40。
add_disk()是块设备注册的内核接口,是块设备驱动的最后一步,也是最关键的一步,下面就分析一下该函数实现的具体细节。
当申请完一个gendisk并进行初始化之后,就可以借助add_diak将之注册到通用块层。表面上看,add_diak似乎是一个平淡无奇的函数,其实不然,它涉及到了后备存储器、kobj_map、分区、请求队列等一大堆东西,但在这里我们主要关心块设备的注册过程,所以会省略掉一些内容,有兴趣的读者可以去阅读源码,源码位置在block/genhd.c。
下面先把add_disk分段贴出,并加以我自己的理解,如有不准确或不恰当的地方,欢迎批评指正。
-
void add_disk(struct gendisk *disk)
-
{
-
struct backing_dev_info *bdi;
-
dev_t devt;
-
int retval;
-
-
/* 申请设备号 */
-
retval = blk_alloc_devt(&disk->part0, &devt);
-
if (retval) {
-
WARN_ON(1);
-
return;
-
}
-
disk_to_dev(disk)->devt = devt; /* 记录gendisk的设备号 */
-
-
/* ->major and ->first_minor aren't supposed to be
-
* dereferenced from here on, but set them just in case.
-
*/
-
disk->major = MAJOR(devt);
-
disk->first_minor = MINOR(devt);
-
-
disk_alloc_events(disk);
-
-
/* Register BDI before referencing it from bdev */
-
bdi = &disk->queue->backing_dev_info;
-
bdi_register_dev(bdi, disk_devt(disk));
-
-
/* 将gendisk添加到kobj_map中 */
-
blk_register_region(disk_devt(disk), disk->minors, NULL,
-
exact_match, exact_lock, disk);
-
register_disk(disk); /* 注册gendisk到通用块层 */
-
blk_register_queue(disk); /* 注册请求队列到通用块层 */
-
-
/*
-
* Take an extra ref on queue which will be put on disk_release()
-
* so that it sticks around as long as @disk is there.
-
*/
-
WARN_ON_ONCE(blk_get_queue(disk->queue));
-
-
retval = sysfs_create_link(&disk_to_dev(disk)->kobj, &bdi->dev->kobj,
-
"bdi");
-
WARN_ON(retval);
-
-
disk_add_events(disk);
-
}
Line7~13:
这段代码很简单,主要是获得gendisk的设备号并记录到gendisk内嵌的decive中。这里唯一需要说明的一点是块设备的次设备号与分区号并不是相等的,有的设备其起始次设备号可能就是一个比较大的值,如:
disk 202, 64 /dev/sda
disk 202, 65 /dev/sda1
disk 202, 66 /dev/sda2
因此次设备号的计算一般为:disk->first_minor + part->partno
Line27~29:
接下来将申请的gendisk管理起来。如何管理呢?内核采用了与字符设备相似的方法,使用了一个全局的struct kobj_map结构体bdev_map,它其实是一个struct probe指针数组,用主设备号作为数组的索引,如下所示:
-
struct kobj_map {
-
struct probe *probes[255];
-
struct mutex *lock;
-
};
-
-
struct probe {
-
struct probe *next; /* 下一个probe */
-
dev_t dev; /* 起始设备号 */
-
-
/* 设备号的范围,如起始设备号是12,range是10,
-
* 那么该结构体关联设备号为[12, 22)的所有设备
-
*/
-
unsigned long range;
-
struct module *owner;
-
kobj_probe_t *get; /* 用于获取该设备内嵌的kobj */
-
int (*lock)(dev_t, void *);
-
void *data; /* 一般指向设备的实际结构体 */
-
};
从上面可以看到,该指针数组最多可容纳255个指针,那对于主设备号大于255的设备应该放到哪里呢?细心的读者应该会发现,在probe结构体中有一个next指针,该指针就是为了链接索引值相同的不同probe,因此数组struct probe *probes[255]索引值的计算应该为:major % 256。
对于一个块设备,它唯一的对应于一个struct probe结构体,该结构体包含了它所有必需的信息,如以上代码所示。所以,我们只需要知道设备的设备号就可以从bdev_map中还原回真正的设备结构体(probe结构体中的data),这也真是函数kobj_lookup完成的功能。
Line30:register_disk(disk)注册gendisk到通用块层,如果深究这将是一个相对复杂的函数。
-
void register_disk(struct gendisk *disk)
-
{
-
struct device *ddev = disk_to_dev(disk);
-
struct block_device *bdev;
-
struct disk_part_iter piter;
-
struct hd_struct *part;
-
int err;
-
-
/* No minors to use for partitions */
-
if (!disk_part_scan_enabled(disk)) {
-
goto exit; /* 该设备不支持分区或强制不扫描分区 */
-
}
-
-
/* No such device (e.g., media were just removed) */
-
if (!get_capacity(disk))
-
goto exit;
-
-
bdev = bdget_disk(disk, 0);
-
if (!bdev)
-
goto exit;
-
-
bdev->bd_invalidated = 1;
-
err = blkdev_get(bdev, FMODE_READ, NULL);
-
if (err < 0)
-
goto exit;
-
blkdev_put(bdev, FMODE_READ);
-
-
exit:
-
/* announce disk after possible partitions are created */
-
dev_set_uevent_suppress(ddev, 0);
-
kobject_uevent(&ddev->kobj, KOBJ_ADD);
-
-
/* announce possible partitions */
-
disk_part_iter_init(&piter, disk, 0);
-
while ((part = disk_part_iter_next(&piter)))
-
kobject_uevent(&part_to_dev(part)->kobj, KOBJ_ADD);
-
disk_part_iter_exit(&piter);
-
}
它主要由两个关键函数实现:
1. bdget_disk():为gendisk分配block_device结构体,作为gendisk在bdevfs中的抽象。该函数最终借助bdget()实现,具体的实现细节可参考我的另一篇博文-bdget()函数详解 。
2. blkdev_get():以只读方式打开该设备,进行分区扫描,并设置block_device与gendisk、hd_struct之间的关联,以及gendisk的block_device与hd_struct的block_device之间的关联。关于该函数的详细分析,感兴趣的读者可自行分析。
我们再回到add_disk()函数:
Line31:通过blk_register_queue()将该gendisk的请求队列request_queue注册到通用块层。请求队列主要在数据的读写时用到,涉及到request合并、电梯算法等一系列的内容,这里不详细讨论,如果感兴趣可参考这位仁兄的博客 - blk_register_queue()函数学习。这里只说明一点:request_queue里面包含了许多与底层设备相关的内容,比如扇区大小,该queue可容纳的最大扇区数等等,可通过blk_queue_logical_block_size()设置扇区的大小,比如blk_queue_logical_block_size(gendisk->queue, 4096),设置gendisk使用4k大小的扇区,该设置对新型设备(如flash等)往往是必须的,因为它们可操作的最小单元就是4k。
这样整个函数的主要部分就分析完了。当该函数执行完成后,你期望的设备就会乖乖的出现在/dev目录下,比如/dev/sdb, /dev/ramdisk,而不需要像注册字符设备那样再搞一个class。
断断续续分几次才把这篇文章写完,难免会有不足之处,如果发现欢迎批评指正。
本文乃原创文章,请勿随意转载,如需转载请详细标明转载出处。
阅读(7684) | 评论(0) | 转发(0) |