通过上节,基本了解了一个文件的访问过程,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调度层。