<acronym id="xonnx"></acronym>
      <td id="xonnx"></td>
    1. <pre id="xonnx"></pre>

      1. 專注電子技術學習與研究
        當前位置:單片機教程網 >> MCU設計實例 >> 瀏覽文章

        塊設備驅動學習過程

        作者:佚名   來源:本站原創   點擊數:  更新時間:2014年08月18日   【字體:

        塊設備傳輸數據的固定長度為一個sector,因此,輸入、輸出調度器和塊設備的驅動必須管理扇區的數據。

        虛擬文件系統->磁盤設備的中間映射層,它使用一種邏輯單元來管理磁盤數據,這種邏輯單元被稱之為“Block”,一個塊相當于一個文件系統里面的最小磁盤存儲單元。
        塊設備驅動能夠拷貝一個segment的數據:每個段是一個內存頁或者一個內存頁的一個部分包含了磁盤上的在物理上相鄰的block【即segment由一個或多個block組成,而內存page由一個或多個磁盤上相鄰的segment組成】;
        【注意】這里的segment只是處理數據的單位,并不是操作系統中段頁式管理內存中的那個“段的概念”
        磁盤緩沖工作于磁盤數據的“頁page”上,每一頁適合于一頁的框架內。
        因為Generic block層連接上層和下層的所有組件,因此在這一層里定義了sectors blocks  segments 和數據頁

        sector: 扇區   block:塊  segment:段  page:頁
        sector:硬件(磁盤)上的最小的操作單位,是操作系統和塊設備(硬件、磁盤)之間傳送數據的單位
        block由一個或多個sector組成,是軟件(OS、文件系統)中最小的操作單位;操作系統的虛擬文件系統從硬件設備上讀取一個block,實際為從硬件設備讀取一個或多個sector.對于文件管理來說,每個文件對應的多個block可能是不連續的;block最終要映射到sector上,所以block的大小一般是sector的整數倍。不同的文件系統block可使用不同的大小,操作系統會在內存中開辟內存,存放block到所謂的block buffer中。
        segment由磁盤上的在物理上相鄰的一個或多個block組成(從硬盤中將block獨到內存中)
        在linux內核中,內存以4KB/頁 進行管理
         
        塊設備的工作流程如下所示:
         
        block_device_operations:
        與字符設備的file_operations類似,塊設備有一個block_device_operations結構體;該block_device_operations結構體主要綁定open、close、iocrl、compat_ioctl、media_changged、revalidate_disk、getgeo等函數。
        gendisk:
        在linux內核中,使用gendisk結構體來表示一個獨立的磁盤設備或者一個分區。
        同一個磁盤的各個分區共享一個主設備號,而次設備號則不同。
        在OS中,gendisk屬于通用塊設備層,格式化在該層進行處理。
        gendisk的struct disk_part_tbl part_tbl成員指向分區表。
        gendisk的struct hd_struct part0成員表示一個分區。
        disk->part_tbl->part[0] = &disk->part0;
        針對該結構體的相關函數有:
            alloc_disk:分配gendisk
            add_disk:注冊gendisk
            del_gendisk:釋放gendisk
            getdisk、putdisk:引用計數
        block_device:
        類似于字符設備的描述結構體cdev,塊設備(或它的分區)也有一個塊設備描述結構體【類似于gendisk既可以描述磁盤設備也可以描述分區】,但是不像字符設備驅動需要注冊cdev,塊設置直接注冊gendisk即可。
        struct block_device {
            dev_t     bd_dev; 
            struct inode *     bd_inode;
            ....
            struct block_device *   bd_contains;
            unsigned     bd_block_size;
            struct hd_struct *     bd_part;
            ....
            struct gendisk *     bd_disk;
            struct list_head     bd_list;
            ....
        };
        下面的圖片有一個磁盤disk,磁盤有個一分區partition; 磁盤的塊設備描述符block_device bd_contains成員指向自己,而
        分區partition的bd_contains成員指向該分區所屬的磁盤設備即disk,以表示一種從屬的關系。
        通過add_disk(struct gendisk *)就可以注冊一個設備【并不是注冊一個分區】,就一個設備與gendisk綁定;而在linux內部分區時也用gendisk表示分區,我們不用管。
        圖中gendisk中的part是gendisk結構體中的part_table成員,指向設備的分區表;在塊設備中用hd_struct結構體表示一個分區,所以圖中表示有四個分區,其中partition分區的描述結構體block_device中的bd_part指向第二分區處。
        通過圖片分析知在程序中如果

        request:
        在linux內核的塊設備中,使用request結構體來表征等待進行的IO請求。
        request_queue:
        表示IO請求隊列的結構體,一個塊請求隊列是一個塊IO請求隊列。
         
        Disk Caches,磁盤高速緩存。
        在linux中,程序對硬盤設備的寫操作實際上是寫到硬盤在內存中的高速緩存,內黑中有pdflush守護進程,什么時候高速緩存中的數據寫入到硬盤中由pdflush決定,可通過sync() fsync()進行強制更新。
        將磁盤上的數據緩存在內存中,加速文件的讀寫。實際上,在一般情況下,read/write是只跟緩存打交道的。(當然,存在特殊情況。下面會說到。)
        read就直接從緩存讀數據。如果要讀的數據還不在緩存中,則觸發一次讀盤操作,然后等待磁盤上的數據被更新到磁盤高速緩存中;write也是直接寫到緩存里去,然后就不用管了。后續內核會負責將數據寫回磁盤。

        BIO :塊IO
        通常一個bio對應一個 上層傳遞給塊層的I/O請求,I/O請求算法可將連續的bio合并成一個request。request是bio經由塊層進行調整后的結果。

        上圖中request的成員q指向自己所屬的等待隊列,request通過quelist成員組成request鏈表;
        request的bio與bio_tail指向request包含的bio。
        在IO調度器中,上層提交的bio被構造成request結構,一個request結構包含了一組順序的bio。而每個物理設備會對應一個request_queue,里面順序存放著相關的request。
        新的bio可能被合并到request_queue中已有的request結構中(甚至合并到已有的bio中),也可能生成新的request結構并插入到request_queue的適當位置上。具體怎么合并、怎么插入,取決于設備驅動程序選擇的IO調度算法。大體上可以把IO調度算法就想象成“電梯算法”,盡管實際的IO調度算法有所改進。

        bio結構中通過bi_next組成bio鏈表。bio的核心是一個稱為bi_io_vec的數組,它由bio_vec結構體組成。
        bio結構體中的成員bv_page是一個內存的頁指針。

        bio通過IO調度算法這一層層的排序、合并等,生產IO請求,IO請求存放到IO請求隊列中去。


        為了實現這樣的緩存,每個文件的inode內嵌了一個address_space結構,通過inode->i_mapping來訪問。address_space結構中維護了一棵radix樹,用于磁盤高速緩存的內存頁面就掛在這棵樹上。而既然磁盤高速緩存是跟文件的inode關聯上的,則打開這個文件的每個進程都共用同一份緩存。
        radix樹的具體實現細節這里可以不用關心,可以把它理解成一個數組。數組中的每個元素就是一個頁面,文件的內容就順序存放在這些頁面中。

        于是,通過要讀寫的文件pos,可以換算得到要讀寫的是第幾頁(pos是以字節為單位,只需要除以每個頁的字節數即可)。
        inode被載入內存的時候,對應的磁盤高速緩存是空的(radix樹上沒有頁面)。隨著文件的讀寫,磁盤上的數據被載入內存,相應的內存頁被掛到radix樹的相應位置上。
        如果文件被寫,則僅僅是對應inode的radix樹上的對應頁上的內容被更新,并不會直接寫回磁盤。這樣被寫過,但還沒有更新到磁盤的頁稱為臟頁。
        內核線程pdflush定期將每個inode上的臟頁更新到磁盤,也會適時地將radix上的頁面回收,這些內容都不在這里深入探討了。

        當需要讀寫的文件內容尚未載入到對應的radix樹時,read/write的執行過程會向底層的“通用塊層”發起讀請求,以便將數據讀入。
        而如果文件打開時指定了O_DIRECT選項,則表示繞開磁盤高速緩存,直接與“通用塊層”打交道。
        既然磁盤高速緩存提供了有利于提高讀寫效率的緩存機制,為什么又要使用O_DIRECT選項來繞開它呢?一般情況下,這樣做的應用程序會自己在用戶態維護一套更利于應用程序使用的專用的緩存機制,用以取代內核提供的磁盤高速緩存這種通用的緩存機制。(數據庫程序通常就會這么干。)
        既然使用O_DIRECT選項后,文件的緩存從內核提供的磁盤高速緩存變成了用戶態的緩存,那么打開同一文件的不同進程將無法共享這些緩存(除非這些進程再創建一個共享內存什么的)。而如果對于同一個文件,某些進程使用了O_DIRECT選項,而某些又沒有呢?沒有使用O_DIRECT選項的進程讀寫這個文件時,會在磁盤高速緩存中留下相應的內容;而使用了O_DIRECT選項的進程讀寫這個文件時,需要先將磁盤高速緩存里面對應本次讀寫的臟數據寫回磁盤,然后再對磁盤進行直接讀寫。
        關于O_DIRECT選項帶來的direct_IO的具體實現細節,說來話長,在這里就不做介紹了。

        3、Generic Block Layer,通用塊層。
        linux內核為塊設備抽象了統一的模型,把塊設備看作是由若干個扇區組成的數組空間。扇區是磁盤設備讀寫的最小單位,通過扇區號可以指定要訪問的磁盤扇區。
        上層的讀寫請求在通用塊層被構造成一個或多個bio結構,這個結構里面描述了一次請求--訪問的起始扇區號?訪問多少個扇區?是讀還是寫?相應的內存頁有哪些、頁偏移和數據長度是多少?等等……

        這里面主要有兩個問題:要訪問的扇區號從哪里來?內存是怎么組織的?
        前面說過,上層的讀寫請求通過文件pos可以定位到要訪問的是相應的磁盤高速緩存的第幾個頁,而通過這個頁index就可以知道要訪問的是文件的第幾個扇區,得到扇區的index。
        但是,文件的第幾個扇區并不等同于磁盤上的第幾個扇區,得到的扇區index還需要由特定文件系統提供的函數來轉換成磁盤的扇區號。文件系統會記載當前磁盤上的扇區使用情況,并且對于每一個inode,它依次使用了哪些扇區。
        于是,通過文件系統提供的特定函數,上層請求的文件pos最終被對應到了磁盤上的扇區號。
        可見,上層的一次請求可能跨多個扇區,可能形成多個非連續的扇區段。對應于每個扇區段,一個bio結構被構造出來。而由于塊設備一般都支持一次性訪問若干個連續的扇區,所以一個扇區段(不止一個扇區)可以包含在代表一次塊設備IO請求的一個bio結構中。

        接下來談談內存的組織。既然上層的一次讀寫請求可能跨多個扇區,它也可能跨越磁盤高速緩存上的多個頁。于是,一個bio里面包含的扇區請求可能會對應一組內存頁。而這些頁是單獨分配的,內存地址很可能不連續。
        那么,既然bio描述的是一次塊設備請求,塊設備能夠一次性訪問一組連續的扇區,但是能夠一次性對一組非連續的內存地址進行存取嗎?
        塊設備一般是通過DMA,將塊設備上一組連續的扇區上的數據拷貝到一組連續的內存頁面上(或將一組連續的內存頁面上的數據拷貝到塊設備上一組連續的扇區),DMA本身一般是不支持一次性訪問非連續的內存頁面的。
        但是某些體系結構包含了io-mmu。就像通過mmu可以將一組非連續的物理頁面映射成連續的虛擬地址一樣,對io-mmu進行編程,可以讓DMA將一組非連續的物理內存看作連續的。所以,即使一個bio包含了非連續的多段內存,它也是有可能可以在一次DMA中完成的。當然,不是所有的體系結構都支持io-mmu,所以一個bio也可能在后面的設備驅動程序中被拆分成多個設備請求。

        每個被構造的bio結構都會分別被提交,提交到底層的IO調度器中。

        4、I/O Scheduler Layer,IO調度器。
        我們知道,磁盤是通過磁頭來讀寫數據的,磁頭在定位扇區的過程中需要做機械的移動。相比于電和磁的傳遞,機械運動是非常慢速的,這也就是磁盤為什么那么慢的主要原因。
        IO調度器要做的事情就是在完成現有請求的前提下,讓磁頭盡可能少移動,從而提高磁盤的讀寫效率。最有名的就是“電梯算法”。
        在IO調度器中,上層提交的bio被構造成request結構,一個request結構包含了一組順序的bio。而每個物理設備會對應一個request_queue,里面順序存放著相關的request。
        新的bio可能被合并到request_queue中已有的request結構中(甚至合并到已有的bio中),也可能生成新的request結構并插入到request_queue的適當位置上。具體怎么合并、怎么插入,取決于設備驅動程序選擇的IO調度算法。大體上可以把IO調度算法就想象成“電梯算法”,盡管實際的IO調度算法有所改進。
        除了類似“電梯算法”的IO調度算法,還有“none”算法,這實際上是沒有算法,也可以說是“先來先服務算法”。因為現在很多塊設備已經能夠很好地支持隨機訪問了(比如固態磁盤、flash閃存),使用“電梯算法”對于它們沒有什么意義。

        IO調度器除了改變請求的順序,還可能延遲觸發對請求的處理。因為只有當請求隊列有一定數目的請求時,“電梯算法”才能發揮其功效,否則極端情況下它將退化成“先來先服務算法”。
        這是通過對request_queue的plug/unplug來實現的,plug相當于停用,unplug相當于恢復。請求少時將request_queue停用,當請求達到一定數目,或者request_queue里最“老”的請求已經等待很長一段時間了,這時候才將request_queue恢復。
        在request_queue恢復的時候,驅動程序提供的回調函數將被調用,于是驅動程序開始處理request_queue。
        一般來說,read/write系統調用到這里就返回了。返回之后可能等待(同步)或是繼續干其他事(異步)。而返回之前會在任務隊列里面添加一個任務,而處理該任務隊列的內核線程將來會執行request_queue的unplug操作,以觸發驅動程序處理請求。

        5、Device Driver,設備驅動程序。
        到了這里,設備驅動程序要做的事情就是從request_queue里面取出請求,然后操作硬件設備,逐個去執行這些請求。

        除了處理請求,設備驅動程序還要選擇IO調度算法,因為設備驅動程序最知道設備的屬性,知道用什么樣的IO調度算法最合適。甚至于,設備驅動程序可以將IO調度器屏蔽掉,而直接對上層的bio進行處理。(當然,設備驅動程序也可實現自己的IO調度算法。)
        可以說,IO調度器是內核提供給設備驅動程序的一組方法。用與不用、使用怎樣的方法,選擇權在于設備驅動程序。

        于是,對于支持隨機訪問的塊設備,驅動程序除了選擇“none”算法,還有一種更直接的做法,就是注冊自己的bio提交函數。這樣,bio生成后,并不會使用通用的提交函數,被提交到IO調度器,而是直接被驅動程序處理。
        但是,如果設備比較慢的話,bio的提交可能會阻塞較長時間。所以這種做法一般被基于內存的“塊設備”驅動使用(當然,這樣的塊設備是由驅動程序虛擬的)。



        下面大致介紹一下read/write的執行流程:
        sys_read。通過fd得到對應的file結構,然后調用vfs_read;
        vfs_read。各種權限及文件鎖的檢查,然后調用file->f_op->read(若不存在則調用do_sync_read)。file->f_op是從對應的inode->i_fop而來,而inode->i_fop是由對應的文件系統類型在生成這個inode時賦予的。file->f_op->read很可能就等同于do_sync_read;
        do_sync_read。f_op->read是完成一次同步讀,而f_op->aio_read完成一次異步讀。do_sync_read則是利用f_op->aio_read這個異步讀操作來完成同步讀,也就是在發起一次異步讀之后,如果返回值是-EIOCBQUEUED,則進程睡眠,直到讀完成即可。但實際上對于磁盤文件的讀,f_op->aio_read一般不會返回-EIOCBQUEUED,除非是設置了O_DIRECT標志aio_read,或者是對于一些特殊的文件系統(如nfs這樣的網絡文件系統);
        f_op->aio_read。這個函數通常是由generic_file_aio_read或者其封裝來實現的;
        generic_file_aio_read。一次異步讀可能包含多個讀操作(對應于readv系統調用),對于其中的每一個,調用do_generic_file_read;
        do_generic_file_read。主要流程是在radix樹里面查找是否存在對應的page,且該頁可用。是則從page里面讀出所需的數據,然后返回,否則通過file->f_mapping->a_ops->readpage去讀這個頁;
        file->f_mapping是從對應inode->i_mapping而來,inode->i_mapping->a_ops是由對應的文件系統類型在生成這個inode時賦予的。而各個文件系統類型提供的a_ops->readpage函數一般是mpage_readpage函數的封裝;
        mpage_readpage。調用do_mpage_readpage構造一個bio,再調用mpage_bio_submit將其提交;
        do_mpage_readpage。根據page->index確定需要讀的磁盤扇區號,然后構造一組bio。其中需要使用文件系統類型提供的get_block函數來對應需要讀取的磁盤扇區號;
        mpage_bio_submit。設置bio的結束回調bio->bi_end_io為mpage_end_io_read,然后調用submit_bio提交這組bio;
        submit_bio。調用generic_make_request將bio提交到磁盤驅動維護的請求隊列中;
        generic_make_request。一個包裝函數,對于每一個bio,調用__generic_make_request;
        __generic_make_request。獲取bio對應的塊設備文件對應的磁盤對象的請求隊列bio->bi_bdev->bd_disk->queue,調用q->make_request_fn將bio添加到隊列;
        q->make_request_fn。設備驅動程序在其初始化時會初始化這個request_queue結構,并且設置q->make_request_fn和q->request_fn(這個下面就會用到)。前者用于將一個bio組裝成request添加到request_queue,后者用于處理request_queue中的請求。一般情況下,設備驅動通過調用blk_init_queue來初始化request_queue,q->request_fn需要給定,而q->make_request_fn使用了默認的__make_request;
        __make_request。會根據不同的調度算法來決定如何添加bio,生成對應的request結構加入request_queue結構中,并且決定是否調用q->request_fn,或是在kblockd_workqueue任務隊列里面添加一個任務,等kblockd內核線程來調用q->request_fn;
        q->request_fn。由驅動程序定義的函數,負責從request_queue里面取出request進行處理。從添加bio到request被取出,若干的請求已經被IO調度算法整理過了。驅動程序負責根據request結構里面的描述,將實際物理設備里面的數據讀到內存中。當驅動程序完成一個request時,會調用end_request(或類似)函數,以結束這個request;
        end_request。完成request的收尾工作,并且會調用對應的bio的的結束方法bio->bi_end_io,即前面設置的mpage_end_io_read;
        mpage_end_io_read。如果page已更新則設置其up-to-date標記,并為page解鎖,喚醒等待page解鎖的進程。最后釋放bio對象;

        sys_write。跟sys_read一樣,對應的vfs_write、do_sync_write、f_op->aio_write、generic_file_aio_write被順序調用;
        generic_file_aio_write。調用__generic_file_aio_write_nolock來進行寫的處理,將數據寫到磁盤高速緩存中。寫完成之后,判斷如果文件打開時使用了O_SYNC標記,則再調用sync_page_range將寫入到磁盤高速緩存中的數據同步到磁盤(只同步文件頭信息);
        __generic_file_aio_write_nolock。進行一些檢查之后,調用generic_file_buffered_write;
        generic_file_buffered_write。調用generic_perform_write執行寫,寫完成之后,判斷如果文件打開時使用了O_SYNC標記,則再調用generic_osync_inode將寫入到磁盤高速緩存中的數據同步到磁盤(同步文件頭信息和文件內容);
        generic_perform_write。一次異步寫可能包含多個寫操作(對應于writev系統調用),對于其中牽涉的每一個page,調用file->f_mapping->a_ops->write_begin準備好需要寫的磁盤高速緩存頁面,然后將需要寫的數據拷入其中,最后調用file->f_mapping->a_ops->write_end完成寫;
        file->f_mapping是從對應inode->i_mapping而來,inode->i_mapping->a_ops是由對應的文件系統類型在生成這個inode時賦予的。而各個文件系統類型提供的file->f_mapping->a_ops->write_begin函數一般是block_write_begin函數的封裝、file->f_mapping->a_ops->write_end函數一般是generic_write_end函數的封裝;
        block_write_begin。調用grab_cache_page_write_begin在radix樹里面查找要被寫的page,如果不存在則創建一個。調用__block_prepare_write為這個page準備一組buffer_head結構,用于描述組成這個page的數據塊(利用其中的信息,可以生成對應的bio結構);
        generic_write_end。調用block_write_end提交寫請求,然后設置page的dirty標記;
        block_write_end。調用__block_commit_write為page中的每一個buffer_head結構設置dirty標記;
        至此,write調用就要返回了。如果文件打開時使用了O_SYNC標記,sync_page_range或generic_osync_inode將被調用。否則write就結束了,等待pdflush內核線程發現radix樹上的臟頁,并最終調用到do_writepages寫回這些臟頁;
        sync_page_range也是調用generic_osync_inode來實現的,而generic_osync_inode最終也會調用到do_writepages;
        do_writepages。調用inode->i_mapping->a_ops->writepages,而后者一般是mpage_writepages函數的包裝;
        mpage_writepages。檢查radix樹中需要寫回的page,對每一個page調用__mpage_writepage;
        __mpage_writepage。這里也是構造bio,然后調用mpage_bio_submit來進行提交;
        后面的流程跟read幾乎就一樣了……
        關閉窗口
        欧美性色欧美精品视频,99热这里只有精品mp4,日韩高清亚洲日韩精品一区二区,2020国自产拍精品高潮