块设备剖析之块设备注册 - add_disk()函数解析

本文深入解析了Linux内核中块设备注册的过程,重点介绍了add_disk()函数的工作原理,包括设备号分配、kobj_map管理、注册到通用块层等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://blog.chinaunix.net/uid-30282771-id-5113192.html



分类: LINUX

本文所有内容基于内核版本Linux-v3.2.40。

    add_disk()是块设备注册的内核接口,是块设备驱动的最后一步,也是最关键的一步,下面就分析一下该函数实现的具体细节。

    当申请完 一个 gendisk并进行初始化之后,就可以借助add_diak将之注册到通用块层。表面上看,add_diak似乎是一个平淡无奇的函数,其实不然,它涉及到了后备存储器、kobj_map、分区、请求队列等一大堆东西,但在这里我们主要关心块设备的注册过程,所以会省略掉一些内容,有兴趣的读者可以去阅读源码,源码位置在block/genhd.c。
    下面先把add_disk分段贴出,并加以我自己的理解,如有不准确或不恰当的地方,欢迎批评指正。
    

点击(此处)折叠或打开

  1. void add_disk(struct gendisk *disk)
  2. {
  3.     struct backing_dev_info *bdi;
  4.     dev_t devt;
  5.     int retval;

  6.     /* 申请设备号 */
  7.     retval = blk_alloc_devt(&disk->part0, &devt);
  8.     if (retval) {
  9.         WARN_ON(1);
  10.         return;
  11.     }
  12.     disk_to_dev(disk)->devt = devt/* 记录gendisk的设备号 */

  13.     /* ->major and ->first_minor aren't supposed to be
  14.      * dereferenced from here on, but set them just in case.
  15.      */
  16.     disk->major = MAJOR(devt);
  17.     disk->first_minor = MINOR(devt);

  18.     disk_alloc_events(disk);

  19.     /* Register BDI before referencing it from bdev */
  20.     bdi = &disk->queue->backing_dev_info;
  21.     bdi_register_dev(bdi, disk_devt(disk));

  22.     /* 将gendisk添加到kobj_map中 */
  23.     blk_register_region(disk_devt(disk), disk->minors, NULL,
  24.              exact_match, exact_lock, disk);
  25.     register_disk(disk); /* 注册gendisk到通用块层 */
  26.     blk_register_queue(disk); /* 注册请求队列到通用块层 */

  27.     /*
  28.      * Take an extra ref on queue which will be put on disk_release()
  29.      * so that it sticks around as long as @disk is there.
  30.      */
  31.     WARN_ON_ONCE(blk_get_queue(disk->queue));

  32.     retval = sysfs_create_link(&disk_to_dev(disk)->kobj, &bdi->dev->kobj,
  33.                  "bdi");
  34.     WARN_ON(retval);

  35.     disk_add_events(disk);
  36. }
     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指针数组,用 主设备号 作为数组的索引,如下所示:
    

点击(此处)折叠或打开

  1. struct kobj_map {
  2.     struct probe *probes[255];
  3.     struct mutex *lock;
  4. };

  5. struct probe {
  6.     struct probe *next; /* 下一个probe */
  7.     dev_t dev; /* 起始设备号 */

  8.     /* 设备号的范围,如起始设备号是12,range是10,
  9.      * 那么该结构体关联设备号为[12, 22)的所有设备
  10.      */
  11.     unsigned long range; 
  12.     struct module *owner;
  13.     kobj_probe_t *get; /* 用于获取该设备内嵌的kobj */
  14.     int (*lock)(dev_t, void *);
  15.     void *data; /* 一般指向设备的实际结构体 */
  16. };
    从上面可以看到,该指针数组最多可容纳255个指针,那对于主设备号大于255的设备应该放到哪里呢?细心的读者应该会发现,在probe结构体中有一个next指针,该指针就是为了链接索引值相同的不同probe,因此数组struct probe *probes[255]索引值的计算应该为:major % 256。
    对于一个块设备,它唯一的对应于一个struct probe结构体,该结构体包含了它所有必需的信息,如以上代码所示。所以,我们只需要知道设备的设备号就可以从 bdev_map中还原回真正的设备结构体(probe结构体中的data),这也真是函数kobj_lookup完成的功能。

    Line30register_disk(disk)注册gendisk到通用块层,如果深究这将是一个相对复杂的函数
    

点击(此处)折叠或打开

  1. void register_disk(struct gendisk *disk)
  2. {
  3.     struct device *ddev = disk_to_dev(disk);
  4.     struct block_device *bdev;
  5.     struct disk_part_iter piter;
  6.     struct hd_struct *part;
  7.     int err;

  8.     /* No minors to use for partitions */
  9.     if (!disk_part_scan_enabled(disk)) { 
  10.         goto exit/* 该设备不支持分区或强制不扫描分区 */
  11.     }

  12.     /* No such device (e.g., media were just removed) */
  13.     if (!get_capacity(disk))
  14.         goto exit;

  15.     bdev = bdget_disk(disk, 0);
  16.     if (!bdev)
  17.         goto exit;

  18.     bdev->bd_invalidated = 1;
  19.     err = blkdev_get(bdev, FMODE_READ, NULL);
  20.     if (err < 0)
  21.         goto exit;
  22.     blkdev_put(bdev, FMODE_READ);

  23. exit:
  24.     /* announce disk after possible partitions are created */
  25.     dev_set_uevent_suppress(ddev, 0);
  26.     kobject_uevent(&ddev->kobj, KOBJ_ADD);

  27.     /* announce possible partitions */
  28.     disk_part_iter_init(&piter, disk, 0);
  29.     while ((part = disk_part_iter_next(&piter)))
  30.         kobject_uevent(&part_to_dev(part)->kobj, KOBJ_ADD);
  31.     disk_part_iter_exit(&piter);
  32. }
     它主要由两个关键函数实现:
    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
    
     断断续续分几次才把这篇文章写完,难免会有不足之处,如果发现欢迎批评指正。
     本文乃原创文章,请勿随意转载,如需转载请详细标明转载出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值