塊設備讀寫流程

 

 

塊設備與字符設備的區別

1、  從字面上理解,塊設備和字符設備最大的區別在於讀寫數據的基本單元不同。塊設備讀寫數據的基本單元爲塊,例如磁盤通常爲一個 sector ,而字符設備的基本單元爲字節。所以 Linux 中塊設備驅動往往爲磁盤設備的驅動,但是由於磁盤設備的 IO 性能與 CPU 相比很差,因此,塊設備的數據流往往會引入文件系統的 Cache 機制。

2、 從實現角度來看, Linux 爲塊設備和字符設備提供了兩套機制。字符設備實現的比較簡單,內核例程和用戶態 API 一一對應,用戶層的 Read 函數直接對應了內核中的 Read 例程,這種映射關係由字符設備的 file_operations 維護。塊設備接口相對於字符設備複雜, read write API 沒有直接到塊設備層,而是直接到文件系統層,然後再由文件系統層發起讀寫請求。

 

塊設備讀寫流程

 

在學習塊設備原理的時候,我最關係塊設備的數據流程,從應用程序調用 Read 或者 Write 開始,數據在內核中到底是如何流通、處理的呢?然後又如何抵達具體的物理設備的呢?下面對一個帶 Cache 功能的塊設備數據流程進行分析。

 

1、  用戶態程序通過 open() 打開指定的塊設備,通過 systemcall 機制陷入內核,執行 blkdev_open() 函數,該函數註冊到文件系統方法( file_operations )中的 open 上。在 blkdev_open 函數中調用 bd_acquire() 函數, bd_acquire 函數完成文件系統 inode 到塊設備 bdev 的轉換,具體的轉換方法通過 hash 查找實現。得到具體塊設備的 bdev 之後,調用 do_open() 函數完成設備打開的操作。在 do_open 函數中會調用到塊設備驅動註冊的 open 方法,具體調用如下: gendisk->fops->open(bdev->bd_inode, file)

2、  用戶程序通過 read write 函數對設備進行讀寫,文件系統會調用相應的方法,通常會調用如下兩個函數: generic_file_read blkdev_file_write 。在讀寫過程中採用了多種策略,首先分析讀過程。

3、  用戶態調用了 read 函數,內核執行 generic_file_read ,如果不是 direct io 方式,那麼直接調用 do_generic_file_read->do_generic_mapping_read() 函數,在 do_generic_mapping_read (函數位於 filemap.c )函數中,首先查找數據是否命中 Cache ,如果命中,那麼直接將數據返回給用戶態;否則通過 address_space->a_ops->readpage 函數發起一個真實的讀請求。在 readpage 函數中,構造一個 buffer_head ,設置 bh 回調函數 end_buffer_async_read ,然後調用 submit_bh 發起請求。在 submit_bh 函數中,根據 buffer_head 構造 bio ,設置 bio 的回調函數 end_bio_bh_io_sync ,最後通過 submit_bio bio 請求發送給指定的快設備。

4、  如果用戶態調用了一個 write 函數,內核執行 blkdev_file_write 函數,如果不是 direct io 操作方式,那麼執行 buffered write 操作過程,直接調用 generic_file_buffered_write 函數。 Buffered write 操作方法會將數據直接寫入 Cache ,並進行 Cache 的替換操作,在替換操作過程中需要對實際的快設備進行操作, address_space->a_ops 提供了塊設備操作的方法。當數據被寫入到 Cache 之後, write 函數就可以返回了,後繼異步寫入的任務絕大部分交給了 pdflush daemon (有一部分在替換的時候做了)

5、  數據流操作到這一步,我們已經很清楚用戶的數據是如何到內核了。與用戶最接近的方法是 file_operations ,每種設備類型都定義了這一方法(由於 Linux 將所有設備都看成是文件,所以爲每類設備都定義了文件操作方法,例如,字符設備的操作方法爲 def_chr_fops ,塊設備爲 def_blk_fops ,網絡設備爲 bad_sock_fops )。每種設備類型底層操作方法是不一樣的,但是通過 file_operations 方法將設備類型的差異化屏蔽了,這就是 Linux 能夠將所有設備都理解爲文件的緣由。到這裏,又提出一個問題:既然這樣,那設備的差異化又該如何體現呢?在文件系統層定義了文件系統訪問設備的方法,該方法就是 address_space_operations ,文件系統通過該方法可以訪問具體的設備。對於字符設備而言,沒有實現 address_space_operations 方法,也沒有必要,因爲字符設備的接口與文件系統的接口是一樣的,在字符設備 open 操作的過程中,將 inode 所指向的 file_operations 替換成 cdev 所指向的 file_operations 就可以了。這樣用戶層讀寫字符設備可以直接調用 cdev file_operations 方法了。

6、  截至到步驟( 4 ),讀操作在沒有命中 Cache 的情況下通過 address_space_operations 方法中的 readpage 函數發起塊設備讀請求;寫操作在替換 Cache 或者 Pdflush 喚醒時發起塊設備請求。發起塊設備請求的過程都一樣,首先根據需求構建 bio 結構, bio 結構中包含了讀寫地址、長度、目的設備、回調函數等信息。構造完 bio 之後,通過簡單的 submit_bio 函數將請求轉發給具體的塊設備。從這裏可以看出,塊設備接口很簡單,接口方法爲 submit_bio (更底層函數爲 generic_make_request ),數據結構爲 struct bio

7、  submit_bio 函數通過 generic_make_request 轉發 bio generic_make_request 是一個循環,其通過每個塊設備下注冊的 q->make_request_fn 函數與塊設備進行交互。如果訪問的塊設備是一個有 queue 的設備,那麼會將系統的 __make_request 函數註冊到 q->make_request_fn 中;否則塊設備會註冊一個私有的方法。在私有的方法中,由於不存在 queue 隊列,所以不會處理具體的請求,而是通過修改 bio 中的方法實現 bio 的轉發,在私有 make_request 方法中,往往會返回 1 ,告訴 generic_make_request 繼續轉發比 bio Generic_make_request 的執行上下文可能有兩種,一種是用戶上下文,另一種爲 pdflush 所在的內核線程上下文。

8、  通過 generic_make_request 的不斷轉發,最後請求一定會到一個存在 queue 隊列的塊設備上,假設最終的那個塊設備是某個 scsi disk /dev/sda )。 generic_make_request 將請求轉發給 sda 時,調用 __make_request ,該函數是 Linux 提供的塊設備請求處理函數。在該函數中實現了極其重要的操作,通常所說的 IO Schedule 就在該函數中實現。在該函數中試圖將轉發過來的 bio merge 到一個已經存在的 request 中,如果可以合併,那麼將新的 bio 請求掛載到一個已經存在 request 中。如果不能合併,那麼分配一個新的 request ,然後將 bio 添加到其中。這一切搞定之後,說明通過 generic_make_request 轉發的 bio 已經抵達了內核的一個站點—— request ,找到了一個臨時歸宿。此時,還沒有真正啓動物理設備的操作。在 __make_request 退出之前,會判斷一個 bio 中的 sync 標記,如果該標記有效,說明請求的 bio 是一個是實時性很強的操作,不能在內核中停留,因此調用了 __generic_unplug_device 函數,該函數將觸發下一階段的操作;如果該標記無效的話,那麼該請求就需要在 queue 隊列中停留一段時間,等到 queue 隊列觸發鬧鐘響了之後,再觸發下一階段的操作。 __make_request 函數返回 0 ,告訴 generic_make_request 無需再轉發 bio 了, bio 轉發結束。

9、  到目前爲止,文件系統( pdflush 或者 address_space_operations )發下來的 bio 已經 merge request queue 中,如果爲 sync bio ,那麼直接調用 __generic_unplug_device ,否則需要在 unplug timer 的軟中斷上下文中執行 q->unplug_fn 。後繼 request 的處理方法應該和具體的物理設備相關,但是在標準的塊設備上如何體現不同物理設備的差異性呢?這種差異性就體現在 queue 隊列的方法上,不同的物理設備, queue 隊列的方法是不一樣的。舉例中的 sda 是一個 scsi 設備,在 scsi middle level scsi_request_fn 函數註冊到了 queue 隊列的 request_fn 方法上。在 q->unplug_fn (具體方法爲: generic_unplug_device )函數中會調用 request 隊列的具體處理函數 q->request_fn Ok ,到這一步實際上已經將塊設備層與 scsi 總線驅動層聯繫在了一起,他們的接口方法爲 request_fn (具體函數爲 scsi_request_fn )。

10、 明白了第( 9 )點之後,接下來的過程實際上和具體的 scsi 總線操作相關了。在 scsi_request_fn 函數中會掃描 request 隊列,通過 elv_next_request 函數從隊列中獲取一個 request 。在 elv_next_request 函數中通過 scsi 總線層註冊的 q->prep_rq_fn scsi 層註冊爲 scsi_prep_fn )函數將具體的 request 轉換成 scsi 驅動所能認識的 scsi command 。獲取一個 request 之後, scsi_request_fn 函數直接調用 scsi_dispatch_cmd 函數將 scsi command 發送給一個具體的 scsi host 。到這一步,有一個問題: scsi command 具體轉發給那個 scsi host 呢?祕密就在於 q->queuedata 中,在爲 sda 設備分配 queue 隊列時,已經指定了 sda 塊設備與底層的 scsi 設備( scsi device )之間的關係,他們的關係是通過 request queue 維護的。

11、  scsi_dispatch_cmd 函數中,通過 scsi host 的接口方法 queuecommand scsi command 發送給 scsi host 。通常 scsi host queuecommand 方法會將接收到的 scsi command 掛到自己維護的隊列中,然後再啓動 DMA 過程將 scsi command 中的數據發送給具體的磁盤。 DMA 完畢之後, DMA 控制器中斷 CPU ,告訴 CPU DMA 過程結束,並且在中斷上下文中設置 DMA 結束的中斷下半部。 DMA 中斷服務程序返回之後觸發軟中斷,執行 SCSI 中斷下半部。

12、      SCSi 中斷下半部中,調用 scsi command 結束的回調函數,這個函數往往爲 scsi_done ,在 scsi_done 函數調用 blk_complete_request 函數結束請求 request ,每個請求維護了一個 bio 鏈,所以在結束請求過程中回調每個請求中的 bio 回調函數,結束具體的 bio Bio 又有文件系統的 buffer head 生成,所以在結束 bio 時,回調 buffer_head 的回調處理函數 bio->bi_end_io (註冊爲 end_bio_bh_io_sync )。自此,由中斷引發的一系列回調過程結束,總結一下回調過程如下: scsi_done->end_request->end_bio->end_bufferhead

13、   回調結束之後,文件系統引發的讀寫操作過程結束。

 

初始化一個塊設備

 

每個塊設備都擁有一個操作接口: struct block_device_operations ,該接口定義了 open close ioctl 等函數接口,但沒有,也沒有必要定義 read write 函數接口。

 

初始化一個塊設備的過程如下:

int setup_device(block_dev_t *dev, int minor)

{

       int hardsect_size = HARDSECT_SIZE;

       int chunk_size;

       sector_t dev_size;

      

       /* 分配一個請求隊列 */

       dev->queue = blk_alloc_queue(GFP_KERNEL);

       if (dev->queue == NULL) {

              printk(ERROR, "blk_alloc_queue failure!/n");

              return -ENOMEM;

       }

 

       chunk_size = dev->chunk_size >> 9;    //sectors

 

       /* block_make_request 註冊到 q->make_request */

       blk_queue_make_request(dev->queue, block_make_request);

      

       blk_queue_max_sectors(dev->queue, chunk_size);

       blk_queue_hardsect_size(dev->queue, hardsect_size);

       blk_queue_merge_bvec(dev->queue, block_mergeable_bvec);

 

       dev->queue->queuedata = dev;

       /* block_unplug 註冊到 q->unplug_fn */

       dev->queue->unplug_fn = block_unplug;

      

       /* 分配一個 gendisk */

       dev->gd = alloc_disk(1);

       if (!dev->gd) {

              prink(ERROR, "alloc_disk failure!/n");

              blk_cleanup_queue(dev->queue);

              return -ENOMEM;

       }

      

       dev->gd->major = block_major;               /* 設備的 major */

       dev->gd->first_minor = minor;            /* 設備的 minor */

       dev->gd->fops = &block_ops;                 /* 塊設備的操作接口, open close ioctl */

       dev->gd->queue = dev->queue;           /* 塊設備的請求隊列 */

       dev->gd->private_data = dev;                    

       snprintf(dev->gd->disk_name, 32, dev->block_name);

 

       dev_size = (sector_t) dev->dev_size >> 9;  

       set_capacity(dev->gd, dev_size);         /* 設置塊設備的容量 */

 

       add_disk(dev->gd);                                   /* 添加塊設備 */

 

       return 0;

}

 

註冊 / 釋放一個塊設備

       通過 register_blkdev 函數將塊設備註冊到 Linux 系統。示例代碼如下:

 

static int blockdev_init(void)

{

      

 

block_major = register_blkdev(block_major, "blockd");

       if (block_major <= 0) {

              printk(ERROR, "blockd: cannot get major %d/n", block_major);

              return -EFAULT;

       }

      

}

 

       通過 unregister_blkdev 函數清除一個塊設備。示例代碼如下:

static int blockdev_cleanup(void)

{

      

 

unregister_blkdev(block_major, "blockd");

 

}

make_request 函數

make_request 函數是塊設備中最重要的接口函數,每個塊設備都需要提供 make_request 函數。如果塊設備爲有請求隊列的實際設備,那麼 make_request 函數被註冊爲 __make_request ,該函數由 Linux 系統提供;反之,需要用戶提供私有函數。 __make_request 函數功能在前文已述。

 

在用戶提供的私有 make_request 函數中往往對 bio 進行過濾處理,這樣的驅動在 Linux 中有 md raid0 raid1 raid5 ),過濾處理完畢之後,私有 make_request 函數返回 1 ,告訴 generic_make_request 函數進行 bio 轉發。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章