1. 程式人生 > >塊裝置剖析之塊設備註冊

塊裝置剖析之塊設備註冊

本文所有內容基於核心版本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合併、電梯演算法等一系列的內容,這裡不詳細討論,如果感興趣可參考這位仁兄的部落格 - 。這裡只說明一點: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
    
     斷斷續續分幾次才把這篇文章寫完,難免會有不足之處,如果發現歡迎批評指正。
     本文乃原創文章,請勿隨意轉載,如需轉載請詳細標明轉載出處。