通過上節,基本了解了一個文件的訪問過程,user空間通過一系列的調用,將會創建了一個請求,該請求指明了要讀取的數據塊所在磁盤的位置、數據塊的數量以及拷貝該數據的目標位置,然后調將求提交給通用塊層處理,首先來看看塊設備通用層涉及到幾個重要的數據結構。
當一個塊被調用內存時,要儲存在一個緩沖區,每個緩沖區與一個塊對應,所以每一個緩沖區獨有一個對應的描述符,該描述符用buffer_head結構表示
1 struct buffer_head { 2 unsigned long b_state; /* 表示緩沖區狀態 */ 3 struct buffer_head *b_this_page; /* 當前頁中緩沖區 */ 4 struct page *b_page; /* 當前緩沖區所在內存頁 */ 5 6 sector_t b_blocknr; /* 起始塊號 */ 7 size_t b_size; /* buffer在內存中的大小 */ 8 char *b_data; /* 塊映射在內存頁中的數據 */ 9 10 struct block_device *b_bdev; /* 關聯的塊設備 */ 11 bh_end_io_t *b_end_io; /* I/O完成方法 */ 12 void *b_private; /* 保留的 I/O 完成方法 */ 13 struct list_head b_assoc_buffers; /* 關聯的其他緩沖區 */ 14 struct address_space *b_assoc_map; /* 相關的地址空間 */ 15 atomic_t b_count; /* 引用計數 */ 16 };
當完成了buffer的緩沖之后,會提交一個個bio請求,該描述符用bio結構來表示
struct bio { struct bio *bi_next; //請求鏈表 struct block_device *bi_bdev; //block設備 unsigned long bi_flags; //狀態和命令標志 unsigned long bi_rw; //讀寫 struct bvec_iter bi_iter; // unsigned int bi_phys_segments;//結合后的片段數 unsigned int bi_seg_front_size;//第一個可合並的段大小; unsigned int bi_seg_back_size;//最后一個可合並段大小 atomic_t bi_remaining; bio_end_io_t *bi_end_io; //bio完成 void *bi_private; //所有數據 #ifdef CONFIG_BLK_CGROUP struct io_context *bi_ioc; struct cgroup_subsys_state *bi_css; #endif union { #if defined(CONFIG_BLK_DEV_INTEGRITY) struct bio_integrity_payload *bi_integrity; /* data integrity */ #endif }; unsigned short bi_vcnt; //bio數目 unsigned short bi_max_vecs; //最大數 atomic_t bi_cnt; //使用計數 struct bio_vec *bi_io_vec; //bio_vec鏈表 struct bio_set *bi_pool; struct bio_vec bi_inline_vecs[0]; //內嵌在結構體末尾的 bio 向量,主要為了防止出現二次申請少量的 bio_vecs }
那么內核是怎么將這兩個結構關聯起來的呢?一個非常重要的函數出現了,ll_rw_block是文件系統對下訪問實際的塊設備驅動的接口,應用程序對實際文件(非設備文件)的操作,最終都是通過文件系統來調用ll_rw_block來操作實際的存儲設備的。
1 void ll_rw_block(int rw, int nr, struct buffer_head *bhs[]) 2 { 3 int i; 4 5 for (i = 0; i < nr; i++) { 6 struct buffer_head *bh = bhs[i]; 7 8 if (!trylock_buffer(bh)) 9 continue; 10 if (rw == WRITE) { 11 if (test_clear_buffer_dirty(bh)) { 12 bh->b_end_io = end_buffer_write_sync; 13 get_bh(bh); 14 submit_bh(WRITE, bh); 15 continue; 16 } 17 } else { 18 if (!buffer_uptodate(bh)) { 19 bh->b_end_io = end_buffer_read_sync; 20 get_bh(bh); 21 submit_bh(rw, bh); 22 continue; 23 } 24 } 25 unlock_buffer(bh); 26 } 27 }
對於這函數,其實主要是對於讀寫不同的操作經過對於b_end_io的處理后再提交請求
1 int submit_bh(int rw, struct buffer_head *bh, unsigned long bio_flags) 2 { 3 struct bio *bio; 4 int ret = 0; 5 ...... 6 bio = bio_alloc(GFP_NOIO, 1); //申請bio結構 7 8 bio->bi_iter.bi_sector = bh->b_blocknr * (bh->b_size >> 9); //起始大小 9 bio->bi_bdev = bh->b_bdev; //block dev訪問 10 bio->bi_io_vec[0].bv_page = bh->b_page; //訪問buffer的地址 11 bio->bi_io_vec[0].bv_len = bh->b_size; //訪問buffer的大小 12 bio->bi_io_vec[0].bv_offset = bh_offset(bh); //訪問buffer的偏移 13 14 bio->bi_vcnt = 1; 15 bio->bi_iter.bi_size = bh->b_size; 16 17 bio->bi_end_io = end_bio_bh_io_sync; //設定end后的回調接口 18 bio->bi_private = bh; //私有成員指向buufer head 19 bio->bi_flags |= bio_flags; //訪問方式 20 ...... 21 submit_bio(rw, bio); //提交BIO,設備對應的驅動程序進行進一步處理 22 ...... 23 24 return ret; 25 }
Submit_bh函數的主要任務是為分配一個bio對象,並且對其進行初始化,然后將bio提交給對應的塊設備對象。提交給塊設備的行為其實就是讓對應的塊設備驅動程序對其進行處理。在Linux中,每個塊設備在內核中都會采用bdev(block_device)對象進行描述。通過bdev對象可以獲取塊設備的所有所需資源,包括如何處理發送到該設備的IO方法。因此,在初始化bio的時候,需要設備目標bdev,在Linux的請求轉發層需要用到bdev對象對bio進行轉發處理。最后調用到submit_bio,這個函數就是為了引入generic_make_request,這個是塊通用層的核心。
1 void generic_make_request(struct bio *bio) 2 { 3 struct bio_list bio_list_on_stack; 4 5 if (!generic_make_request_checks(bio)) 6 return; 7 if (current->bio_list) { 8 bio_list_add(current->bio_list, bio); 9 return; 10 } 11 BUG_ON(bio->bi_next); 12 bio_list_init(&bio_list_on_stack); 13 current->bio_list = &bio_list_on_stack; 14 do { 15 struct request_queue *q = bdev_get_queue(bio->bi_bdev); 16 17 q->make_request_fn(q, bio); 18 19 bio = bio_list_pop(current->bio_list); 20 } while (bio); 21 current->bio_list = NULL; /* deactivate */ 22 }
下圖是generic_make_request的整個處理流程,最主要的操作是獲取請求隊列,然后調用對應的make_request_fn方法處理bio,make_request_fn方法會將bio放入請求隊列中進行調度處理,普通磁盤介質一個最大的問題是隨機讀寫性能很差。為了提高性能,通常的做法是聚合IO,因此在塊設備層設置請求隊列,對IO進行聚合操作,從而提高讀寫性能,對於I/O的調度將單獨進行分析。

此時,通用層的基本使命已經完成,從代碼流程上來看,通用層主要完成的是將應用層的讀寫請求構成一個或者多個I/O請求而已,后面的主要工作就交給I/O調度層。
