概述
在塊設備上的操作,涉及內核中的多個組成部分,如圖1所示。假設一個進程使用系統
調用read()讀取磁盤上的文件。下面步驟是內核響應進程讀請求的步驟;
- 系統調用read()會觸發相應的VFS(Virtual Filesystem Switch)函數,傳遞的參數
有文件描述符和文件偏移量。 - VFS確定請求的數據是否已經在內存緩沖區中;若數據不在內存中,確定如何執行讀操作。
- 假設內核必須從塊設備上讀取數據,這樣內核就必須確定數據在物理設備上的位置。這由映射層(Mapping Layer)來完成,主要執行兩步
- 內核確定該文件所在的文件系統的塊大小,并根據文件塊的大小計算所請求數據的長度。本質上,文件被看作拆分成許多塊,因此內核確定請求數據所在的塊號(文件開始位置的相對索引)。
- 接下來,映射層調用一個具體的文件系統的函數,它訪問文件的磁盤節點,然后根據邏輯塊號確定所請求數據在磁盤上的位置。事實上,磁盤也被看作分成許多塊,因此內核必須確定存放所請求數據的塊對應的號(磁盤或分區開始位置的相對索引)。由于一個文件可能存儲在磁盤上的不連續塊中,因此存放在磁盤索引節點中的數據結構將每個文件塊號映射為一個邏輯塊號。
- 內核可以對塊設備發出讀請求。內核利用通用塊層(generic block layer)啟動I/O操作來傳送所請求的數據。一般而言,
每個I/O操作只針對磁盤上一組連續的塊。由于請求的數據不必位于相鄰的塊中,所以通用塊層可能啟動幾次I/O操作。每次I/O操作是由一個“塊I/O”(簡單block io 即bio)的結構來描述,它收集底層組件需要的所有信息以滿足所發出的請求
。
通用塊層為所有的塊設備提供了一個抽象的視圖,因而隱藏了硬件塊設備間的差異性。幾乎所有的塊設備都是磁盤。所以通用塊層也提供了一些數據結構來描述“磁盤”或"磁盤分區" - 通用塊層的下面“I/O調度程序”根據預定義的內核策略將待處理的I/O數據傳送請求進行歸類。調度程序的作用是把物理介質上相鄰的數據請求聚集在一起。
-最后,塊設備驅動程序向磁盤控制器的三件接口發送適當的命令。從而進行實際的數據傳送。
對于(1)、(2)兩個步驟,在Linux虛擬文件系統中,我們討論了VFS(Virtual Filesystem Switch)主要數據結構和操作,結合相關系統調用(如sys_read()、sys_write()等) 的源碼,我們不難理解VFS層相關的操作和實現。
塊設備中的數據存儲涉及了許多內核的組件;每個組件采用不同長度的塊
來管理磁盤數據:
- 硬件塊設備控制器采用稱為
扇區
的固定長度的塊來傳遞數據。因此,I/O調度程序和塊驅動程序必須管理數據扇區
。 - 虛擬文件系統、映射層和文件系統將磁盤數據存放在稱為
塊
的邏輯單元中。一個塊對應文件系統中
一個最小
的磁盤存儲單元
。 - 塊設備驅動程序應該能夠處理數據的
段
:一個段就是一個內存頁或內存內的一部分
,它們包括磁盤上物理相鄰的數據塊。 - 磁盤高速緩存作用于磁盤數據的
頁
上,每頁正好裝在一個頁框中。 - 通用塊層將所有的上層和下層的組件組合在一起,因此它了解數據的扇區、塊、段以及頁。
注:但是,如果從原始塊設備文件進行讀訪問,映射層就不調用具體文件系統的方法,而是把塊設備文件中的偏移量轉換成磁盤或在對應該設備文件的磁盤分區中的位置。
即使有許多不同的數據塊,它們通常也是共享相同的物理RAM單元。例如:圖2顯示了一個具有4K的頁的構造
。上層內核組件將頁看成是由4個1K字節組成的塊緩沖區
。塊設備驅動程序正在傳送頁中的后3個塊,因此這3塊被插入到涵蓋了后3K字節的段
中。硬盤控制器將段看成由6個512字節的扇區組成。
page-disk-layout.png
相關概念
系統中能夠隨機訪問固定大小數據片(chunk)的設備稱為塊設備,這些數據片就稱作 塊。最常見的塊設備是硬盤,除此之外,還有CD-ROM驅動器和SSD等。它們通常安裝文 件系統的方式使用。
內核管理塊設備要比管理字符設備復雜。因為字符設備僅僅需要控制一個位置-當前位 置,而塊設備訪問的位置必須能夠在介質的不同區間前后移動。所以內核不必提供一個專門 的子系統來管理字符設備,但是對于塊設備的管理卻必須要有一個專門提供服務的子系統。 不僅僅是因為塊設備的復雜性遠遠高于字符設備,更重要的原因是塊設備對執行性能要求很 高;對硬盤每多一分利用都會對系統的整體性能帶來提升,其效果要遠遠比鍵盤吞吐速度成 倍的提高大得多。另外,塊設備的復雜性會為這種優化留下很大的空間。
- 扇區
為了達到可接受的性能,硬盤和類似的設備快速傳送幾個相鄰字節的數據。塊設備的每次傳送操作都作用于一組稱為扇區的相鄰字節。我們假定字節按相鄰的方式記錄在磁盤表面,這樣一次搜索操作就可以訪問到它們。盡管磁盤的物理構造很復雜,但是硬盤控制器
收到的命令將硬盤看成一個大組扇區
。
在大部分磁盤設備中,扇區
的大小是512字節
,但是一些設備使用更大的扇區(1K 2K)
。注意,應該把扇區作為數據傳送的基本單元;不允許傳送少于一個扇區的數據,盡管大多數磁盤設備都可以同時傳送幾個相鄰的扇區。
Linux中扇區的大小按慣例設為512字節
;如果一個塊設備使用更大的扇區,那么相應的底層塊設備驅動程序將做些必要的變換。因此,對存放在塊設備中的一組數據是通過它們在磁盤上的位置來標識,即首個512字節扇區的下標以及扇區的數目
。扇區的下標放在類型為sector_t的32或64位亦是中。
- 塊
扇區
是硬件設備傳送數據的基本單位
,而塊
是VFS和文件系統傳送數據的基本單位
。例如:內核訪問一個文件的內容時,它必須首先從磁盤上讀文件的磁盤索引節點所在塊。該塊對應磁盤上的一個或多個相鄰的扇區,而VFS
將其看成是一個單一的數據單元
。
在Linux中,塊大小必須是2的冪,而且不能超過一個頁框。此外,它必須是扇區大小的整數倍,因為每個塊必須包含整個扇區。因此在80x86體系結構中,它允許塊的大小為512 1024 2048 4096字節(linux 固定扇區大小為512
)。
塊設備的塊大小不是唯一的。創建一個磁盤文件系統時,管理員可以選擇合適的塊大小。因此,同一個磁盤上的幾個分區可能使用不同的塊大小。此外,對塊設備文件的每次讀或寫操作是一種"原始"訪問,因此它繞過了磁盤的文件系統;內核通過使用最大的塊(4096)執行該操作。
每個塊都需要自己的塊緩沖區,它是內核用來存放塊內容的RAM內存區。當內核從磁盤讀出一個塊時,就用從硬件設備中獲得的值來填充相應的塊緩沖區;同樣,當內核向磁盤寫入一個塊時,就用相關塊緩沖的實際值來更新硬件設備上相應的一組相鄰字節。塊緩沖區的大小 通常要與相應的塊大小相匹配。
緩沖區首部是一個與每個緩沖區相關的buffer_head
類型的描述符。它包含內核處理緩沖區需要了解的所有信息;因此,在對每個緩沖區進行操作之前,內核都要首先檢查其緩沖區首部。
其中b_page字段存放的是塊緩沖區所在頁框的頁描述符地址。如果頁框位于高端內存中,那么 b_data字段存放頁中塊緩沖區的偏移量;否則,b_data存放塊緩沖區本身的起始線性地址。b_blocknr字段存放的是邏輯塊號。最后,b_bdev字段標識使用緩沖區首部的塊設備。
- 段
磁盤的每個IO操作就是在磁盤與一些RAM單元之間相互傳送一些相鄰扇區的內容。大多數情況下,磁盤控制器直接采用DMA方式進行數據傳送。塊設備驅動程序只要向磁盤控制器發送一些適當的命令就可以觸發一次數據傳送,一旦完成數據的傳送,控制器就會發出一個中斷通知塊設備驅動程序。
DMA方式傳送的是磁盤上相鄰扇區的數據
。這是一個物理約束:磁盤控制器允許DMA傳送不相鄰的扇區數據,但是這種方式的傳送速率很低,因為在磁盤表面上移動讀、寫磁頭是相當慢的。
老式的磁盤控制器僅僅支持"簡單"的DMA傳送方式;在這種傳送方式中,磁盤必須與RAM中的連續內存單元相互傳送數據。但是,新的磁盤控制器也支持所謂的分散-聚集(scatter-gether)DMA傳送方式:此種方式中,磁盤可以與一些非連續的內存區相互傳送數據。
啟動一次分散-聚集DMA傳送,塊設備驅動程序需要向磁盤控制器發送:
- 要傳送的起始磁盤扇區號和總的扇區數。
- 內存區的描述符鏈表,其中鏈表的每項包含一個地址和一個長度。
磁盤控制器負責整個數據的傳送;例如,在讀操作中控制器從相鄰磁盤扇區中獲得數據,然后將它們存放到不同的內存區中。
為了使用分散-聚集DMA方式傳送方式,塊設備驅動程序必須能夠處理稱為段的數據存儲單元
。一個段就是一個內存頁或內存頁中的一部分,它們包含一些相鄰的磁盤扇區中的數據。因此一次分散-聚集DMA操作可以同時傳送幾個段。
注意:塊設備驅動程序
不需要知道塊,塊大小以及塊緩沖區。因此,即使高層將段看成是由幾個塊緩沖區組成的頁,塊設備驅動程序也不用對此給予關注。
如果不中的段在RAM中相應的頁框正好是連續的并且在磁盤上相應的數據塊也是相鄰的,那么通用塊層可以合并它們。通過這種合并方式產生的更大的內存區就稱為 物理段
。
然后,在多數體系結構上還允許另一種合并方式:通過使用一個專門的總結電路[IO-MMU]來處理總結地址與物理地址間的映射。通過這種合并方式產生的內存區稱為硬件段
。由于我們將注意力集中在80X86體系結構上,它的總結地址與物理地址之間不存在動態的映射,因此我們可以假定硬件段與物理段是對應的。
通用塊層
通用塊層是一個內核組件,它處理來自系統中的所有塊設備發出的請求。
- BIO結構
通用塊層的核心數據結構是一個稱為BIO的描述符,它描述了塊設備的IO操作。每個bio結構都包含一個磁盤存儲區標識符(存儲區中的起始扇區和扇區數目)和一個或多個描述符 與IO操作相關的內存區的段。bio由struct bio 數據結構描述,源代碼如下:
struct bio
https://github.com/sparrowzoo/linux/blob/master/include/linux/blk_types.h
bio中的每個段是一個由bio_vec數據結構描述的
源代碼如下:
https://github.com/sparrowzoo/linux/blob/master/include/linux/bvec.h
在塊IO操作期間,bio描述符的內容一直保持更新。例如,果塊設備驅動程序在一次分散-聚集DMA操作中不能完成全部的數據傳送,那么bio中的bi_idx字段會不斷更新來指向待傳送的第一個段。
struct bvec_iter {
sector_t bi_sector; /* device address in 512 byte
sectors */
unsigned int bi_size; /* residual I/O count */
unsigned int bi_idx; /* current index into bvl_vec */
unsigned int bi_bvec_done; /* number of bytes completed in
current bvec */
};
為了從索引bi_idx指向的當前段開始不斷重復bio中的段,設備驅動程序可以執行宏bio_for_each_segment。
當通用塊層
啟動一次新的IO操作時,調用bio_alloc函數分配一個新的bio
結構。通常,bio
結構是由slab
分配器分配的。但是,當內存不足時,內核也會使用一個備用的bio小內存池
。內核也為bio_vec結構分配內存池。畢竟,分配一個bio結構而不能分配其中的段描述符也是沒有什么意義的。相應地bio_put
函數減少bio中中引用計數器bi_cnt
的值,如果該值小于0,則釋放bio
結構以及相關的bio_vec
結構。
-
磁盤和磁盤分區的表示
磁盤是一個由通用塊層處理的邏輯塊設備。通常一個磁盤對應一個硬件塊設備,例如硬盤、軟盤或光盤。但是,磁盤也可以是一個虛擬設備,它建立在幾個物理磁盤分區之上或一些RAM專用頁中的內存頁上。在任何情形中,借助通用塊層提供的服務。上層內核組件可以以同樣的方式工作在所有磁盤上。磁盤由gendisk對象描述源碼注釋
https://github.com/sparrowzoo/linux/blob/master/include/linux/genhd.h塊設備操作源碼注釋
https://github.com/sparrowzoo/linux/blob/master/include/linux/blkdev.h
通常硬盤被劃分成幾個邏輯分區。每塊塊設備文件要么代表整個磁盤,要么代表磁盤中的某一個分區。例如,一個主設備號為3、次設備號為0的設備文件/dev/had代表的可能是一個主IDE磁盤;該磁盤中的前兩個分區分別由設備文件/dev/hda1和/dev/hda2代表,它們的主設備號都是3,而次設備號分別為1和2。一般而言,磁盤中的分區是由連續的次設備號
來區分的。
如果將一個磁盤分成了幾個分區,那么其分區表保存在hd_struct結構數組中,該數的地址存放在gendisk
對象的part
(struct disk_part_tbl __rcu *part_tbl; 源碼版本不一致
)字段中。通過磁盤內分區的相對索引對該數組進行索引。hd_struct數據結構如下:
struct disk_part_tbl {
struct rcu_head rcu_head;
int len;
struct hd_struct __rcu *last_lookup;
struct hd_struct __rcu *part[];
};
struct hd_struct {
sector_t start_sect;
/*
* nr_sects is protected by sequence counter. One might extend a
* partition while IO is happening to it and update of nr_sects
* can be non-atomic on 32bit machines with 64bit sector_t.
*/
sector_t nr_sects;
seqcount_t nr_sects_seq;
sector_t alignment_offset;
unsigned int discard_alignment;
struct device __dev;
struct kobject *holder_dir;
int policy, partno;
struct partition_meta_info *info;
#ifdef CONFIG_FAIL_MAKE_REQUEST
int make_it_fail;
#endif
unsigned long stamp;
atomic_t in_flight[2];
#ifdef CONFIG_SMP
struct disk_stats __percpu *dkstats;
#else
struct disk_stats dkstats;
#endif
struct percpu_ref ref;
struct rcu_head rcu_head;
};
當內核發現系統中一個新的磁盤時(在啟動階段,或將一個可移動介質插入到一個驅動器中時,或在運行期附加一個外置磁盤時),就調用alloc_disk()
函數,該函數分配并初始化一個新的gendisk
對象。如果新磁盤被分成了幾個分區,那么alloc_disk
還會分配并初始化一個適當的hd_struct類型的數組。然后,內核調用add_disk()函數將gendisk對象插入到通用塊層的數據結構中。
- 提交請求
我們介紹一下當向通用塊層提交一個IO操作請求時,內核所執行的步驟順序。我們假定(因為上文提到一個IO,如果數據不相鄰會被拆成多個請求
)被請求的數據塊在磁盤上是相鄰的,并且內核已經知道了它們的物理位置。
-
第一步是執行bio_alloc函數分配一個新的bio描述符。然后通過設置一些字段值來初始化bio描述符(bi_sector\bi_size\bi_bdev\bi_io_vec\bi_rw\bi_end_io)
一旦bio描述符被進行了適當的初始化,內核就調用generaic_make_request函數,該函數是通用塊層的主要入口點。- 獲取與塊設備相關的
請求隊列
- 調用blk_partition_remap()函數
至此,能用塊層 IO調度程序以及設備驅動程序將忘記磁盤分區的存在,直接作用于整個磁盤。
- 調用q_make_request_fn方法將bio請求插入到
請求隊列
中。
- 獲取與塊設備相關的