Linux裝置驅動-塊裝置之通用塊層
通用塊層簡介
Linux中的通用塊層是一個核心元件,它負責處理來自系統中的所有塊裝置訪問,並將塊裝置的訪問轉換為請求下發到IO排程層。這個過程中會涉及到多種資料結構的轉換,下面我們來討論通用塊層所涉及的資料結構以及通用塊層所做的工作。
通用塊層資料結構
注:本文所涉及的資料結構均為Linux 3.0核心中的資料結構
1、bio結構
bio描述符是通用塊層的核心資料結構,它描述了塊裝置的IO操作,包含了IO操作所設計的磁碟的儲存區識別符號,與IO操作相關的記憶體區的段資訊等。bio秒資料結構如下:
struct bio { sector_tbi_sector;/* 塊IO操作的第一個磁碟扇區 */ struct bio*bi_next;/* 連結到請求佇列的下一個bio */ struct block_device*bi_bdev;/* 指向塊裝置描述符的指標 */ unsigned longbi_flags;/* bio的狀態標誌 */ unsigned longbi_rw;/* IO操作標誌 */ unsigned shortbi_vcnt;/* bio中bio_vec陣列中段的數目,即bio_vec陣列長度 */ unsigned shortbi_idx;/* bio中bio_vec陣列當前索引值 */ /* Number of segments in this BIO after * physical address coalescing is performed. */ unsigned intbi_phys_segments;/* 合併之後bio中的物理段 */ unsigned intbi_size;/* 需要傳送的位元組數 */ /* * To keep track of the max segment size, we account for the * sizes of the first and last mergeable segments in this bio. */ unsigned intbi_seg_front_size;/* 段合併演算法使用 */ unsigned intbi_seg_back_size;/* 段合併演算法使用 */ unsigned intbi_max_vecs;/* bio中bio_vec陣列允許的最大段數 */ unsigned intbi_comp_cpu;/* completion CPU */ atomic_tbi_cnt;/* pin count */ struct bio_vec*bi_io_vec;/* 指向bio的bio_vec陣列中的第一個段的指標 */ bio_end_io_t*bi_end_io;/* bio的IO操作結束時呼叫的方法 */ void*bi_private;/* bio私有資料 */ #if defined(CONFIG_BLK_DEV_INTEGRITY) struct bio_integrity_payload *bi_integrity;/* data integrity */ #endif bio_destructor_t*bi_destructor;/* 釋放bio時呼叫的析構方法*/ /* * We can inline a number of vecs at the end of the bio, to avoid * double allocations for a small number of bio_vecs. This member * MUST obviously be kept at the very end of the bio. */ struct bio_vecbi_inline_vecs[0]; };
bio中的每個段都是由bio_vec結構表示,其中個欄位如下所示。bio中的bio_io_vec欄位存放bio_vec陣列中的第一個元素地址,bi_vcnt欄位存放bio_vec陣列當前的元素個數
struct bio_vec { struct page*bv_page;/* 指向段的頁框中頁描述符的指標 */ unsigned intbv_len;/* 段的長度,以位元組為單位 */ unsigned intbv_offset;/* 頁框中段資料的偏移量 */ };
2、gendisk結構
gendisk描述一個磁碟或磁碟分割槽。磁碟是由通用塊層梳理的邏輯裝置,通常一個磁碟對應一個硬體裝置。gendisk具體欄位如下所示:
struct gendisk { /* major, first_minor and minors are input parameters only, * don't use directly.Use disk_devt() and disk_max_parts(). */ int major;/* 磁碟主裝置號 */ int first_minor;/* 與磁碟關聯的第一個次裝置號 */ int minors;/* 與磁碟關聯的此裝置號範圍 */ char disk_name[DISK_NAME_LEN];/* 磁碟標準名稱 */ char *(*devnode)(struct gendisk *gd, mode_t *mode); unsigned int events;/* supported events */ unsigned int async_events;/* async events, subset of all */ /* Array of pointers to partitions indexed by partno. * Protected with matching bdev lock but stat and other * non-critical accesses use RCU.Always access through * helpers. */ struct disk_part_tbl __rcu *part_tbl;/* 磁碟分割槽表 */ struct hd_struct part0;/* 磁碟第一個分割槽 */ const struct block_device_operations *fops;/* 磁碟操作方法 */ struct request_queue *queue;/* 指向磁碟請求佇列的指標 */ void *private_data;/* 塊裝置驅動的私有資料 */ int flags;/* 磁碟型別的標誌 */ struct device *driverfs_dev;// FIXME: remove struct kobject *slave_dir; struct timer_rand_state *random; atomic_t sync_io;/* RAID */ struct disk_events *ev; #ifdefCONFIG_BLK_DEV_INTEGRITY struct blk_integrity *integrity; #endif int node_id; };
gendisk結構中fops欄位指向磁碟操作方法的結構提指標,這些方法包括open、release、ioctl等,類似於字元裝置驅動程式中的fops結構;
gendisk結構中part_tbl欄位指向磁碟的分割槽表,分割槽表結構具體的欄位如下:
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;/* 磁碟中分割槽的起始扇區 */ sector_t nr_sects;/* 分割槽的扇區數 */ 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]; #ifdefCONFIG_SMP struct disk_stats __percpu *dkstats; #else struct disk_stats dkstats; #endif atomic_t ref; struct rcu_head rcu_head; };
通用塊層工作流程
本節以do_erase函式為例討論Linux中當向通用塊層提交了一個IO操作時通用塊層的處理流程。
首先附上do_erase函式程式碼:
static int do_erase(struct super_block *sb, u64 ofs, pgoff_t index, size_t nr_pages) { struct logfs_super *super = logfs_super(sb); struct bio *bio; struct request_queue *q = bdev_get_queue(sb->s_bdev); unsigned int max_pages = queue_max_hw_sectors(q) >> (PAGE_SHIFT - 9); int i; if (max_pages > BIO_MAX_PAGES) max_pages = BIO_MAX_PAGES; bio = bio_alloc(GFP_NOFS, max_pages);//申請bio結構 BUG_ON(!bio); for (i = 0; i < nr_pages; i++) { if (i >= max_pages) {//當請求的資料大於磁碟一次資料傳輸大小時,會將請求分成多個bio提交 /* Block layer cannot split bios :( */ bio->bi_vcnt = i; bio->bi_idx = 0; bio->bi_size = i * PAGE_SIZE; bio->bi_bdev = super->s_bdev; bio->bi_sector = ofs >> 9; bio->bi_private = sb; bio->bi_end_io = erase_end_io; atomic_inc(&super->s_pending_writes); submit_bio(WRITE, bio); ofs += i * PAGE_SIZE; index += i; nr_pages -= i; i = 0; bio = bio_alloc(GFP_NOFS, max_pages);//申請新的bio結構 BUG_ON(!bio); } bio->bi_io_vec[i].bv_page = super->s_erase_page; bio->bi_io_vec[i].bv_len = PAGE_SIZE; bio->bi_io_vec[i].bv_offset = 0; } bio->bi_vcnt = nr_pages; bio->bi_idx = 0; bio->bi_size = nr_pages * PAGE_SIZE; bio->bi_bdev = super->s_bdev; bio->bi_sector = ofs >> 9; bio->bi_private = sb; bio->bi_end_io = erase_end_io; atomic_inc(&super->s_pending_writes); submit_bio(WRITE, bio); return 0; }
上述程式碼可有如下流程圖解釋:
![]() |
圖1 do_erase流程圖 |
在do_erase函式中,如果像磁碟請求的資料大小大於一次bio操作允許的最大值(i>max_pages),則會將磁碟資料請求分成多個bio進行,先完善並提交當前bio,然後申請新的bio結構並將剩餘的資料請求填充到新的bio中。
接下來討論一下提交bio請求函式submit_bio,原始碼如下:
void submit_bio(int rw, struct bio *bio) { int count = bio_sectors(bio); bio->bi_rw |= rw; /* * If it's a regular read/write or a barrier with data attached, * go through the normal accounting stuff before submission. */ if (bio_has_data(bio) && !(rw & REQ_DISCARD)) { if (rw & WRITE) { count_vm_events(PGPGOUT, count); } else { task_io_account_read(bio->bi_size); count_vm_events(PGPGIN, count); } if (unlikely(block_dump)) { char b[BDEVNAME_SIZE]; printk(KERN_DEBUG "%s(%d): %s block %Lu on %s (%u sectors)\n", current->comm, task_pid_nr(current), (rw & WRITE) ? "WRITE" : "READ", (unsigned long long)bio->bi_sector, bdevname(bio->bi_bdev, b), count); } } generic_make_request(bio); }
submit_bio完善一下bio資訊後會呼叫generic_make_request函式提交bio。
generic_make_request函式原始碼如下:
void generic_make_request(struct bio *bio) { struct bio_list bio_list_on_stack; if (current->bio_list) { /* make_request is active */ bio_list_add(current->bio_list, bio); return; } BUG_ON(bio->bi_next); bio_list_init(&bio_list_on_stack); current->bio_list = &bio_list_on_stack; do { __generic_make_request(bio); bio = bio_list_pop(current->bio_list); } while (bio); current->bio_list = NULL; /* deactivate */ }
generic_make_request函式將bio連線到current->bio_list連結串列中,並呼叫__generic_make_request函式提交連結串列中所有的bio。__generic_make_request函式最終會呼叫塊裝置的請求佇列中的make_request_fn成員函式將bio請求傳送給I/O排程層,至此對磁碟的資料請求離開通用塊層,進入下一層——I/O排程層
通用塊層總結
綜上,一個磁碟資料請求在通用塊層經過的流程為:
- 上層下發磁碟資料請求
- 通用塊層申請bio結構,將請求的資料分段記錄到bio中
- 如果請求的資料大於一個bio允許的最大資料量,則將請求分成多個bio
- 呼叫submit_bio提交bio請求
- submit_bio函式經過層層呼叫,最終呼叫塊裝置請求佇列中的make_request_fn成員函式將bio提交給I/O排程層進行處理