系统中能够随机(无序的)访问固定大小数据片(chunk)的设备被称作块设备,这些数据片就称为块。常见的块设备是磁盘,软盘驱动器,CD_ROM驱动器和闪存等。注意,它们都是以安装文件系统的方式使用的---这也是块设备通常的访问方式。
另一种基本的设备类型是字符设备。字符设备按照字符流的方式被有序访问。像串口和键盘都属于字符设备。
这两种类型的设备的根本区别在于它们是否可以被随机访问。
内核管理块设备比管理字符设备要细致得多。因为字符设备仅仅需要控制一个位置(当前位置),而块设备访问的位置必须能够在介质的不同区间前后移动。,因此对块设备的管理需要一个专门的提供服务的子系统。
- 解剖一个块设备
块设备中最小的可寻址单元是扇区。扇区大小一般是2的整数倍,最常见的大小是512个字节(很多CD_ROM盘的扇区都是2K大小)。扇区的大小是设备的物理属性,扇区是所有块设备的基本单元,块设备无法对比它还小的单元进行寻址和操作。
虽然各种软件的用途不同,但是它们都会用到自己的最小逻辑可寻址单元---块。块是文件系统的一种抽象,只能基于块来访问文件系统。虽然物理磁盘寻址是按照扇区来级进行的,但是内核执行的所有磁盘操作都是按照块进行的。由于扇区是设备的最小可寻址单元,所以块不能比扇区还小,只能数倍于扇区大小。对有扇区的硬件设备,内核还要求块大小是2的整数倍,且不能超过一个页的长度。
对块大小的要求最终如下:必须是扇区大小的2的整数倍,并且要小于页面大小。
扇区,设备的最小寻址单元,或称为“硬扇区”“设备块”。块,文件系统的最小寻址单元,或称为“文件块”“I/O块”。
和硬盘相关的常见术语有:簇,柱面,磁头等,这些都和具体的块设备邮箱,用户空间的软件一般用不到这些概念。
扇区对内核的重要性在与所有设备的I/O操作都必须基于扇区来进行。
- 缓冲区和缓冲区头
当一个块被调入内存时,它要存储在一个缓冲区中。每个缓冲区与一个块对应,它相当于是磁盘块在内存中的表示。一个块小于一个页,所以一页可以容纳一个或多个内存中的块。
由于内核在处理数据时需要一些相关的控制信息,每个缓冲区都有一个对应的描述符。该描述符用buffer_head结构体表示,被称作缓冲区头,它包含了内核操作缓冲区所需的全部信息:
- 在<Buffer_head.h(include/linux)>中
- struct page;
- struct buffer_head;
- struct address_space;
- typedef void (bh_end_io_t)(struct buffer_head *bh, int uptodate);
- /*
- * Historically, a buffer_head was used to map a single block
- * within a page, and of course as the unit of I/O through the
- * filesystem and block layers. Nowadays the basic I/O unit
- * is the bio, and buffer_heads are used for extracting block
- * mappings (via a get_block_t call), for tracking state within
- * a page (via a page_mapping) and for wrapping bio submission
- * for backward compatibility reasons (e.g. submit_bh).
- */
- struct buffer_head {
- unsigned long b_state; /* buffer state bitmap (see above) */
- struct buffer_head *b_this_page;/* circular list of page's buffers */
- struct page *b_page; /* the page this bh is mapped to */表示与缓冲区对应的内存物理页
- sector_t b_blocknr; /* start block number */与缓冲区对应的磁盘物理块由该域索引,该值是b_bdev指明的块设备中的逻辑块号
- size_t b_size; /* size of mapping 以字节为单位*/
- char *b_data; /* pointer to data within the page */直接指向相应的块(它位于b_page域所指的页面中的某个位置),块大小为b_size,块在内存中的起始位置在b_data处,结束位置在b_data+b_size
- struct block_device *b_bdev; /*块设备*/
- bh_end_io_t *b_end_io; /* I/O completion; io完成方法*/
- void *b_private; /* reserved for b_end_io */
- struct list_head b_assoc_buffers; /* associated with another mapping */
- struct address_space *b_assoc_map; /* mapping this buffer is
- associated with */
- atomic_t b_count; /* users using this buffer_head */
- };
缓冲区头的目的在于描述磁盘块和物理内存缓冲区(在特定页面上的字节序列)之间的映射关系。这个结构体在内核中只扮演一个描述符的角色,说明从缓冲区到块的映射关系。
b_state域中表示缓冲区的状态。合法标志存放在bh_state_bits枚举中:
- enum bh_state_bits {
- BH_Uptodate, /* Contains valid data */该缓冲区包含可用数据
- BH_Dirty, /* Is dirty */该缓冲区是脏的(缓存中的内容比磁盘中的块内容新所以缓冲区内容必须被写回磁盘)
- BH_Lock, /* Is locked */该缓冲区正在被IO操作使用,被锁定以防被并发访问
- BH_Req, /* Has been submitted for I/O */该缓冲区有IO请求操作
- BH_Uptodate_Lock,/* Used by the first bh in a page, to serialise
- * IO completion of other buffers in the page
- */
- BH_Mapped, /* Has a disk mapping */该缓冲区是映射磁盘块的可用缓冲区
- BH_New, /* Disk mapping was newly created by get_block */缓冲区是通过get_block()刚刚映射的,尚且不能访问
- BH_Async_Read, /* Is under end_buffer_async_read I/O */该缓冲区正通过end_buffer_async_read()被异步IO读操作使用
- BH_Async_Write, /* Is under end_buffer_async_write I/O */该缓冲区正通过end_buffer_async_write()被异步IO写操作使用
- BH_Delay, /* Buffer is not yet allocated on disk */该缓冲区尚未和磁盘块关联
- BH_Boundary, /* Block is followed by a discontiguity */该缓冲区处于连续块去的边界,下一个块不在连续
- BH_Write_EIO, /* I/O error on write */
- BH_Ordered, /* ordered write */
- BH_Eopnotsupp, /* operation not supported (barrier) */
- BH_Unwritten, /* Buffer is allocated on disk but not written */
- BH_PrivateStart,/* not a state bit, but the first bit available
- * for private allocation by other entities
- */该标志不是可用状态标志,使用它是为了指明可被其他代码使用的起始位。块IO层不会使用BH_PrivateStart或更高的位。
- };
某个驱动程序希望通过b_state域存储信息时就可以安全的使用这些位。驱动程序可以在这些位中定义自己的状态标志,只要保证自己的状态标志不与块IO层的专用位发生冲突就OK了。
b_count域表示缓冲区的使用计数,可以通过下面的函数进行增减:
- static inline void get_bh(struct buffer_head *bh)
- {
- atomic_inc(&bh->b_count);
- }
- static inline void put_bh(struct buffer_head *bh)
- {
- smp_mb__before_atomic_dec();
- atomic_dec(&bh->b_count);
- }
在操作缓冲区头之前,应该先使用get_bh()函数增加缓冲区头的引用计数确保该缓冲区头不会再被分配出去;当完成对缓冲区头的操作之后,还必须使用put_bh()函数减少引用计数。
在2.6之前,缓冲区头的作用很重要,缓冲区头作为内核中IO操作单元,不仅仅描述了从哦哦那个磁盘块到物理内存的映射,而且还是所有开IO操作的容器。 将缓冲区头作为IO操作单元的弊端如下:
1. 缓冲区头是一个很大且不易控制的数据结构体(2.6之前),而且缓冲区头对数据的操作既不方便也不清晰。在2.6版本中,许多IO操作都是通过内核直接对页面或地址空间进行操作来完成,不再使用缓冲区头。
2. 缓冲区头仅能描述单个缓冲区,当作为所有IO的容器使用时,缓冲区头会迫使内核打断对大块数据的IO操作,使其成为对多个buffer_head结构体进行操作。2.5版本以后的内核的主要目标就是为块IO操作引入一直新型。灵活并且轻量级的容器,bio结构体。
- bio结构体
目前内核中块IO操作的基本容器由bio结构体表示。该结构体代表了正在现场的(活动的)以片段(segment)链表形式组织的块IO操作。一个片段是一小块连续的内存缓冲区。这样就不需要保证单个缓冲区一定要连续。通过片段来描述缓冲区,即使一个缓冲区分散在内存的多个位置上,bio结构体也能对内核保证IO操作的执行。像这样的向量IO就是所谓的聚散IO。
- 在<Bio.h(include/linux)>中
- struct bio_set;
- struct bio;
- typedef int (bio_end_io_t) (struct bio *, unsigned int, int);
- typedef void (bio_destructor_t) (struct bio *);
- /*
- * main unit of I/O for the block layer and lower layers (ie drivers and
- * stacking drivers)
- */
- struct bio {
- sector_t bi_sector; /* device address in 512 byte
- sectors */磁盘上相关的扇区
- struct bio *bi_next; /* request queue link */请求链表
- struct block_device *bi_bdev; /* 相关的块设备信息 */
- unsigned long bi_flags; /* status, command, etc */
- unsigned long bi_rw; /* bottom bits READ/WRITE,
- * top bits priority
- */
- unsigned short bi_vcnt; /* how many bio_vec's */
- unsigned short bi_idx; /* current index into bvl_vec */
- /* Number of segments in this BIO after
- * physical address coalescing is performed.
- */结合后的片段数目
- unsigned short bi_phys_segments;
- /* Number of segments after physical and DMA remapping
- * hardware coalescing is performed.
- */
- unsigned short bi_hw_segments; /* 重映射后的片断数目 */
- unsigned int bi_size; /* residual I/O count */
- /*
- * To keep track of the max hw size, we account for the
- * sizes of the first and last virtually mergeable segments
- * in this bio
- */
- unsigned int bi_hw_front_size; /* 第一个可合并的段大小 */
- unsigned int bi_hw_back_size; /* 最后一个可合并的段大小 */
- unsigned int bi_max_vecs; /* max bvl_vecs we can hold */
- struct bio_vec *bi_io_vec; /* the actual vec list */
- bio_end_io_t *bi_end_io; /* IO完成方法 */
- atomic_t bi_cnt; /* pin count */使用计数
- void *bi_private; /*拥有者的私有方法*/
- bio_destructor_t *bi_destructor; /* destructor */
- };
使用bio结构体的目的主要是代表正在现场执行的IO操作,所以该结构体中的主要域都是用来管理相关信息的。其中最重要的几个域是bi_io_vecs,bi_vcnt和bi_idx。
bi_io_vecs域指向一个bio_vec结构体数组,该结构体链表包含了一个特定的IO操作所需要使用到的所有片段。
在每个给定的块IO操作中,bi_vcnt域用来描述bi_io_vec所指向的bio_vec数组中的向量数目。
当块IO操作执行完了,bi_idx指向数组的当前索引。
每个bio_vec结构都是一个形式为<page,offset,len>的向量,它描述的是一个特定的片断:片段所在的物理页、在物理页中的偏移位置、从给定偏移量开始的块长度。整个bio_io_vec结构体指向的bio_vec结构体数组表示了一个完整的缓冲区:
- /*
- * was unsigned short, but we might as well be ready for > 64kB I/O pages
- */
- struct bio_vec {
- struct page *bv_page;
- unsigned int bv_len;
- unsigned int bv_offset;
- };
总而言之,每一个块IO请求都通过一个bio结构体表示。每个请求包括一个或多个块,这些块存储在bio_vec结构体数组中。这些结构体描述了每个片段在物理页中的实际位置,并且像向量一样被组织在一起。IO操作的第一个片段由bio_io_vecs指针所指向,其他的片段在其后一次防止,共有bi_vcnt个片段。当块IO层开始执行请求、需要使用各个片段时,bi_idx域会不断更新,指向当前片段。块IO层通过bi_idx可以跟踪IO操作的完成进度。但该域更重要的作用在于分割bio结构体。
bi_cnt域记录bio结构体的使用计数,如果为0就销毁该结构体,并释放内存。通过下面的函数管理使用计数:
- 在<Bio.h(include/linux)>
- /*
- * get a reference to a bio, so it won't disappear. the intended use is
- * something like:
- *
- * bio_get(bio);
- * submit_bio(rw, bio);
- * if (bio->bi_flags ...)
- * do_something
- * bio_put(bio);
- *
- * without the bio_get(), it could potentially complete I/O before submit_bio
- * returns. and then bio would be freed memory when if (bio->bi_flags ...)
- * runs
- */
- #define bio_get(bio) atomic_inc(&(bio)->bi_cnt)
- 在<Bio.c(fs)>中
- /**
- * bio_put - release a reference to a bio
- * @bio: bio to release reference to
- *
- * Description:
- * Put a reference to a &struct bio, either one you have gotten with
- * bio_alloc or bio_get. The last put of a bio will free it.
- **/
- void bio_put(struct bio *bio)
- {
- BIO_BUG_ON(!atomic_read(&bio->bi_cnt));
- /*
- * last put frees it
- */
- if (atomic_dec_and_test(&bio->bi_cnt)) {
- bio->bi_next = NULL;
- bio->bi_destructor(bio);
- }
- }
在操作正在活动的bio结构体时,一定要首先增加它的使用计数,以免在操作过程中该bio结构体被释放。
bi_private域,这是一个属于拥有者(创建者)的私有域,只有创建了bio结构的拥有者可以读写该域。
- 新老方法的对比
缓冲区头和新的bio结构体之间存在明显的差别。
bio结构体代表的是IO操作,它可以包括内存中的一个或多个页;buffer_head结构体代表的是一个缓冲区,它描述的仅仅是磁盘中的一个块。因为缓冲区头关联的是单独页中的单独磁盘块,所以它会引起不必要的分割,将请求按块为单位划分,只能靠以后重新组合。bio结构体是轻量级的,它描述的块可以不需要连续存储区,且不需要分割IO操作。
bio代替buffer_head的好处如下:
1. bio很容易处理高度内存,因为它处理的是物理页而不是直接指针。
2. bio既可以代表普通页IO,也可以代表直接IO(那些不通过页高速缓存的IO操作)
3. bio便于执行分散--集中(向量化)块IO操作,操作中的数据可取自多个物理页面。
4. bio相比缓冲区头属于轻量级的结构体。它只需要包含块IO操作所需的信息就行了,不用包含与缓冲区本身相关的不必要信息。
缓冲区头负责描述磁盘块到页面的映射。bio结构体不包括任何和缓冲区相关的状态信息,它仅仅是一个向量组,描述一个或多个单独块IO操作的数据片段和相关信息。
在当前设置中,当bio描述当前正在使用的IO操作时,buffer_head需要包含缓冲区信息。