歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

塊設備驅動編寫總結

塊設備與字符設備的區別
 
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、 回調結束之後,文件系統引發的讀寫操作過程結束。

Copyright © Linux教程網 All Rights Reserved