只有在“打开”了文件以后,或者说建立了进程与文件的“连接”之后,才能对文件进行读写。为了提高效率,Linux的读写操作都是带缓冲的,即写的时候先写到缓冲副本中,读的时候也从缓冲副本中读入。在多进程的系统中,由于同一文件可为多个进程共享,缓冲的作用就更加显著。
Linux文件缓冲设置在文件层的inode结构中。它里面有一个指针i_mapping,它指向一个address_space数据结构(通常这个结构就是inode中的i_data),缓冲区队列就在这个数据结构中。不过,挂载缓冲区队列中的并不是记录块(逻辑磁盘块)而是内存页面。也就是说,文件的内容并不是以逻辑磁盘块为单位而是以页面为单位进行缓冲的。如果一个记录块的大小为1K字节,那么一个页面就相当于4个逻辑磁盘块(记录块)。至于为什么这么做,是为了把文件内容的缓冲与文件的内存映射结合在一起(这也是为什么取名叫i_mapping、address_space,详细参考情景分析P580)。
在文件层是以页面为单位缓冲,但在设备层则是以逻辑磁盘块为单位缓冲。在一个记录块的缓冲区头部即buffer_head结构中有一个指针b_data指向记录块缓冲区,而buffer_head结构本身则不在缓冲区中。
以一个缓冲页面为例,在文件层它通过一个page数据结构挂入所属inode结构的缓冲页面队列,并且同时又可以通过各个进程的页面映射表映射到这些进程的内存空间;而在设备层则有通过若干buffer_head结构挂入其所在设备的缓冲区队列。
数据缓冲区的大小等于逻辑磁盘块(记录块)的大小,为物理磁盘块(扇区)大小的整数倍;同时内存页(文件层缓冲页page)的大小又为逻辑磁盘块的整数倍。在从磁盘读取数据时,文件系统一次读取若干个磁盘扇区大小的数据存放在记录块中,若干个记录块再组成一个内存页。如下图所示:
上面这些都是为了讲明白内存页page与逻辑磁盘块在文件系统所处的位置以及它们之间的关系(因此说到文件缓冲,要分清楚是文件层的缓冲页page还是设备层的逻辑磁盘块缓冲区buffer_head)。缓冲页面page结构除链入附属于inode结构的缓冲页面队列外,同时也链入到一个杂凑表page_hash_table中的杂凑队列中,所以寻找目标页面的的操作也是很高效的。
除了通过缓冲来提高文件读写效率外,还有个措施是“预读”。如果一个进程发动了对某一个缓冲页面的读写操作,并且该页面上不再内存中而需要从设备读入,那么就可以预测,通常情况下它接下去可能会继续往下读写,因此不妨预先将后面几个页面也一起读进来。其实以页面单位的缓冲本身就隐含着预读,因为一个页面包含着多个记录块(通常是4块),只不过预读的量很小而已。现在file结构中其实要维持两个上下文了。一个就是由“当前位置”f_pos代表的真正的读写上下文,而另一个就是预读的上下文。为此目的,在file结构中增设了f_reada、f_ramax、f_raend、f_rawin等几个字段(ra表示read ahead)。
注意,在调用参数中并不指明在文件中写的位置,因为文件的file结构代表着上下文,记录着在文件中的“当前位置”。
根据打开文件号fd找到该已打开文件的file结构。而它实质上就是通过调用下面函数实现的。
struct fdtable *fdt = files_fdtable(files);
if (fd < fdt->max_fds)
file = rcu_dereference(fdt->fd[fd]);
即通过fdtable,根据数组下标fd得到file结构。
fget_file()返回打开的文件file结构后,便开始为写做准备。
很简单,就是返回file中的文件“当前位置”file->f_pos。再就是通过vfs_write()开始了真正的写流程。
if (!(file->f_mode & FMODE_WRITE))
return -EBADF;
if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))
return -EINVAL;
一个进程要对一个已打开的文件进行写操作,应该满足几个必要条件。其一是相应file结构里f_mode字段中的标志位FMODE_WRITE为1.这个字段的内容是在打开文件时根据对系统调用open()的参数flags经过变换而来的。若标志位FMODE_WRITE为0,则表示这个文件是按“只读”方式打开的,所以该标志位为1是写操作的一个必要条件。另外file结构必须包含有具体文件系统的写操作函数。
这是检查文件是否加锁以及是否允许使用强制锁。
检查了锁之后,就是写操作本身了。具体的文件系统通过其中file_operation数据结构提供用于写操作的函数指针。在2.6内核中,Ext2文件系统的写操作函数指针指向的就是do_sync_write()。
struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
struct kiocb kiocb;
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = *ppos;
kiocb.ki_left = len;
首先这段代码是对iovec结构和kiocb结构的初始化。 iovec 主要是用于存放两个内容:用来接收所读取数据的用户地址空间缓冲区的地址(iov_base)和缓冲区的大小(iov_len)。kiocb描述符用来跟踪正在运行的同步和异步I/O操作的完成状态。在Linux内核中,每个IO请求都对应一个kiocb结构体,其ki_filp成员指向对应的file指针,通过is_sync_kiocb可以判断某Kiocb是否为同步IO请求,如果非真,表示是异步IO请求。块设备和网络设备本身就是异步的。调用宏init_sync_kiocb来初始化描述符kiocb,并设置一个同步操作对象的有关字段。主要设置ki_filp字段和ki_obj字段以及在kiocb中设置io读写的位置和长度。
接下来又是调用ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos),进行真正的写,参数就是kiocb和iovec结构变量。可见linux的块设备也是异步IO。
struct file *file = iocb->ki_filp;
struct address_space * mapping = file->f_mapping;
先从kiocb中获得file结构和address_space结构。
针对iovec段做一些检查。传下来的nr_segs值为1。
/* Performs necessary checks before doing a write
* @iov: io vector request
* @nr_segs: number of segments in the iovec
* @count: number of bytes to write
* @access_flags: type of access: %VERIFY_READ or %VERIFY_WRITE
*
* Adjust number of segments and amount of bytes to write (nr_segs should be
* properly initialized first). Returns appropriate error code that caller
* should return or zero in case that write should be allowed.
*/
针对写文件位置和长度做一些检查。
/*
* Performs necessary checks before doing a write
*
* Can adjust writing position or amount of bytes to write.
* Returns appropriate error code that caller should return or
* zero in case that write should be allowed.
*/
如在打开文件时的参数中将O_APPEND标志位设为1,则表示对此文件的写操作只能是在尾端添加,所以要讲当前位置pos调整到文件的尾端。
(if (file->f_flags & O_APPEND)*pos = i_size_read(inode);)
进程的task_struct结构中有个数组rlim就规定了对该进程使用各种资源的上限。其中有一项,即下标为RLIMT_FSIZE处的元素,就表示对该进程的文件大小的限制。如果企图写入的位置超出了这个限制,就要给这个进程发一个SIGXFSZ,并让系统调用失败而返回错误代码-EFBIG。
unsigned long limit = current->signal->rlim[RLIMIT_FSIZE].rlim_cur;
if (limit != RLIM_INFINITY) {
if (*pos >= limit) {
send_sig(SIGXFSZ, current, 0);
return -EFBIG;
}
… …
做完了这些检查还会调用下这个函数。
这是说,如果当前进程并无设置“set uid”,即S_ISUID标志位的特权,而目标文件的set uid标志位S_ISUID和S_ISGID为1,则应将inode中的这些标志位清0,也就是剥夺该文件的set uid和set gid特性。
在inode结构中打上世间印记并将该inode标志成“脏”后,开始操作。
if (a_ops->write_begin)
status = generic_perform_write(file, &i, pos);
这个write_begin函数是存在的。于是调用generic_perform_write()。file->f_mapping是从对应inode->i_mapping而来,inode->i_mapping->a_ops是由对应的文件系统类型在生成这个inode时赋予的。而各个文件系统类型提供的file->f_mapping->a_ops->write_begin函数一般是block_write_begin函数的封装、file->f_mapping->a_ops->write_end函数一般是generic_write_end函数的封装。
这里传给它的参数包括一个iov_iter结构指针,它实际上是包含着iovec变量以及读写长度等信息。写操作的主体是由一个do-while循环实现的,循环的次数取决于写的长度和位置。在每一次循环中,只往一个缓冲页面中写,并将当前位置pos相应的向前推进,而剩下的长度iov_iter.count则逐次减少。
offset = (pos & (PAGE_CACHE_SIZE - 1)); /* Offset into pagecache page */
index = pos >> PAGE_CACHE_SHIFT; /* Pagecache index for current page */
bytes = min_t(unsigned long, PAGE_CACHE_SIZE - offset,iov_iter_count(i)); /* Bytes to write to page */
这是首先计算出当前位置是从第几个page开始,以及它在page内部的偏移和要写的长度。然后就调用write_begin函数,即ext2_write_begin()
根据当前位置pos计算出本次循环中要写的缓冲页面index、在该页面中的起点start、以及写入长度bytes。计算将整个文件的内容当作一个连续线性存储空间,将pos右移PAGE_CACHE_SHIFT位跟将pos被页面大小所整除是等价的(但是更快)。计算出了缓冲页面在目标文件中的逻辑序号index后,就通过__grab_cache_page()找到该缓冲页面。
这个函数首先通过find_lock_page()在mapping对应的基树中查找页面,如果找不到就通过page_cache_alloc()分配一个缓冲页面,并用add_to_page_cache_lru()将其插入到mapping的基树中。
从_grab_cache_page()返回到block_write_begin()中,已经有了一个缓冲页面。在开始写入之前还要做一些准备工作。
参数里的get_block变量为ext2_get_block,from和to都是页面内偏移量,而不是文件内偏移,from为该页面起点,to为起点+写入长度。
为什么要有准备工作,原因是这样的。通过前面分析知道,ext2在文件系统层面是以页面为单位缓冲的,在设备层次上却是以记录块为单位缓冲的。_grab_cache_page()返回的可能是已经存在的缓冲页面,也可能是个新分配的空白页面,它们之间有两点根本性的区别。第一点是在结构上,缓冲页面一方面与一个page结构相联系,另一方面又要与若干记录块缓冲区的头部,即buffer_head数据结构相联系,已经存在的缓冲页面是具备这个关系的,而新分配的页面则尚无buffer_head结构与之挂钩。第二点是在内容上,新分配的空白页面要将目标页面的内容首先从设备中读入(因为写操作未必是整个页面的写入,这一点在设计MS的对象层时也深有体会)。不仅如此,就是业已存在的老页面也有个缓冲页面中的内容是否“up_to_date”,即是否一致的问题。所以如果一个缓冲页面内容是一致的,就意味着构成这个页面的所有记录块的内容都一致。反过来,如果一个缓冲页面不一致,则未必每个记录块都不一致。因此,要根据写入的位置和长度找到具体设计的记录块,针对这些记录块做写入准备。
要理解后面的代码,先看下面这个问题。
现在假设有一个文件,它大概有两个页面,现在我只在文件的开始写入(修改)一点点数据,比方说10个字节,然后再seek到文件的开始进行读取操作,此时缓存如何管理?在分配一个缓冲页面的时候,我只是修改了这个页面开始的10个字节,这个10个字节之后的所有内容都应该保持之前的内容。那么这个缓冲页的内容会是什么样子。假设只是修改了缓冲页开始的10个字节,之后的内容留空或者全部初始化为零,那么当下次再次读取的时候它如何判断这个页面中的哪些位置是已经被修改过的?从设备中读取的扇区将会覆盖内存页面的什么位置?假设说每次写入的时候都把所有将要蹂躏的扇区都读入内存,那么就更没有必要了。比方说我修改了10000字节,跨越接近20个扇区,如果每个扇区都读入,然后读入之后马上被修改为其它值,那这个读取明显是耗时而没有意义的。
后面的代码就是__block_prepare_write解决这个问题的方法。对于已经建立起缓冲页面和物理记录块映射的页面,则需要做的只是检查一下记录块内容是否一致,如果不一致就调用ll_rw_block()将设备上记录块读到缓冲区中。如果缓冲页面是新的,即尚未建立起到物理记录块的映射,则需要通过get_block()先建立映射。由此可见,对文件的写操作是“写中有读,欲写先读”。关键就是读多少,怎么读,这也是解决上面问题的关键。_block_prepare_write()的机制就是以记录块为单位(buffer_head,也即逻辑磁盘块)读,并且只对满足某些特定条件的块才需要读。
blocksize = 1 << inode->i_blkbits; /*块大小,也即页内缓冲区大小*/
if (!page_has_buffers(page))
create_empty_buffers(page, blocksize, 0);
head = page_buffers(page);
调用create_empty_buffers为该页建立缓冲区队列,然后对队列进行初始化。没有涉及bh的state标志,调用的create_buffers设置bh->state=0且把新分配的缓冲区放入BUF_CLEAN链表(BH_dirty标志为0)缓冲区头赋给page->buffer。
bbits = inode->i_blkbits; /*块位数*/
block = (sector_t)page->index << (PAGE_CACHE_SHIFT - bbits);
当前页所在的块号假设块大小为1k,则bbits = inode->i_blkbits, bbits为10 则一页占2^(PAGE_CACHE_SHIFT-bbits)=2^(12-10)=2^2=4个块因此该页的逻辑起始块号为: page->index*每页块数即page->index*2^(PAGE_CACHE_SHIFT-bbits)=page->index<<(PAGE_CACHE_SHIFT-bbits)
for(bh = head, block_start = 0; bh != head || !block_start;block++, block_start=block_end, bh = bh->b_this_page)
{ block_end = block_start + blocksize;
对当前页的每个块缓冲区对应的bh和受写影响的每个bh,block_start记录循环写入的总的块大小。
if (block_end <= from || block_start >= to) {
if (PageUptodate(page)) {
if (!buffer_uptodate(bh))
set_buffer_uptodate(bh);
}
continue;
}
对于页内块完全不在from~to之间的区域,所谓完全不在,就是说这个块的整块都在from~to之外:终止地址小于from,或是块的起始地址大于to。这些块与写入范围完全无关,既不用从设备上读数据,也不会有数据写入这些块,所以可以直接跳过。
对页内form~to之间的区域,则有的块可能需要从设备读数据,有的块不需要读,于是进行下列转换、检查或设置。
检查BH_Mapped标志,未设置时,调用get_block完成从文件逻辑块号到磁盘逻辑块号的转换,磁盘逻辑块号存放在bh->b_blocknr字段,且设置BH_Mapped标志。这里的get_block对于每个没有在内存中的页面都会被执行,但是这里不要被名字所迷惑,它不会启动对文件数据的真正读取(尽管可能会启动对inode节点及数据的读取),它只是对页面对应的buffer_head结构进行初始化,例如建立page和设备block之间的映射关系,这种映射关系根据不同的文件系统有不同的实现方式,例如经典的unix的三次间接寻址结构。而函数的最后一个参数get_block就是负责根据不同的文件系统来建立buffer_head和page的不同映射关系。再次强调,这里并不会读取文件具体内容,主要负责建立设备block和page之间的映射关系。对于get_block的代码后面还要详细分析,这里先跳过。
if (PageUptodate(page)) {
if (!buffer_uptodate(bh))
set_buffer_uptodate(bh);
continue;
}
如果page的读操作完成,PG_uptodate标志被设置,则将其缓冲区的BH_uptodate也设置。也就是说如果一个缓冲页面内容是一致的,就意味着构成这个页面的所有记录块的内容都一致。
if (!buffer_uptodate(bh) && !buffer_delay(bh) &&!buffer_unwritten(bh) &&(block_start < from || block_end > to))
{
ll_rw_block(READ, 1, &bh);
*wait_bh++=bh;
}
如果不对整个块进行重写,且它的BH_Delay和BH_Uptodate标志未置位(即块缓冲区没有有效数据的影响),调用ll_rw_block函数从磁盘读入块的内容。注意加亮语句的含义!由于前面对完全在from~to范围内的块已经跳过了,那么这里的块说明在from~to范围内,或者部分在from~to范围内。而不对整个块进行重写说明就是后者,即这个块需要重写,但又只有一部分需要重写,对于这样的块,就需要先冲设备中把原来的物理块数据先读上来,然后再将需要重写的那部分重写。这也就是__block_prepare_write()解决上面所提出问题的关键所在。一个页面中的块,完全在from~to之外的不需要读取,完全在from~to之间的也不需要读取(因为这一整块即使读上来也马上要全部写入新数据),只有部分在from~to之间的块才需要读取。ll_rw_block函数中定义了I/O完成后的处理函数end_buffer_io_sync。
while(wait_bh > wait) {
wait_on_buffer(*--wait_bh);
if (!buffer_uptodate(*wait_bh))
err = -EIO;
}
阻塞当前进程,直到for循环中的ll_rw_block读操作全部完成。对于这部分缓存刷新同步的细节,留作内存缓存线程代码分析后再来研究。
__block_prepare_write()函数至此结束,下面回头来看之前get_block()
的函数实现。
参数iblock表示所处理的记录块在文件中的逻辑块号,inode则指向文件的inode结构。这个函数的基础就是ext2中文件内块号到设备上块号的映射,即经典的三层寻址结构(文件的记录块的直接寻址、间接寻址、双重间接寻址和三重间接寻址)。
在ext2文件系统的ext2_inode_info结构中(由EXT2_I(inode)宏得来),有个大小为15的整型数组i_data[](与设备上索引节点ext2_inode结构中的i_block[]相对应),其开头12个元素是直接寻址,第13个元素是间接寻址,它指向一个记录块,依次类推。对于1K的块大小,则ext2的一个inode所支持的最大文件大小为256*256*256+256*256+256+12个记录块(即再*1K)。
这里还要注意,在struct inode结构中有个成分名为i_data,这是一个address_space数据结构。而作为struct inode 结构一部分的ext2_inode_info结构中,也有个名为i_data的数组,它是记录块映射表,二者毫无关系。
有了这些背景知识,就可以接着看下面的函数。
这个函数的作用就是根据文件内的块号计算出这个记录块落在i_data[]的哪个区间,要采用几重映射。
int ptrs = EXT2_ADDR_PER_BLOCK(inode->i_sb);
int ptrs_bits = EXT2_ADDR_PER_BLOCK_BITS(inode->i_sb);
const long direct_blocks = EXT2_NDIR_BLOCKS,
indirect_blocks = ptrs,
double_blocks = (1 << (ptrs_bits * 2));
这些定义中的EXT2_NDIRBLOCKS为12,表示直接映射的记录块数量。EXT2_IND_BLOCK的值也是12,表示在i_data[]数组中用于一次间接映射的元素下标。而EXT2_DIND_BLOCK和EXT2_TIND_BLOCK则分别为用于二次间接和三次间接的元素下标,值为13、14.至于EXT2_N_BLOCKS则为i_data[]数组的大小。根据这些宏定义,在记录块大小为1K时,ptrs的值为256,从而indirect_blocks的值也是256,ptrs_bits的值为8。
if (i_block < 0) {
ext2_warning (inode->i_sb, "ext2_block_to_path", "block < 0");
} else if (i_block < direct_blocks) {
offsets[n++] = i_block;
final = direct_blocks;
} else if ( (i_block -= direct_blocks) < indirect_blocks) {
offsets[n++] = EXT2_IND_BLOCK;
offsets[n++] = i_block;
final = ptrs;
} else if ((i_block -= indirect_blocks) < double_blocks) {
offsets[n++] = EXT2_DIND_BLOCK;
offsets[n++] = i_block >> ptrs_bits;
offsets[n++] = i_block & (ptrs - 1);
final = ptrs;
} else if (((i_block -= double_blocks) >> (ptrs_bits * 2)) < ptrs) {
offsets[n++] = EXT2_TIND_BLOCK;
offsets[n++] = i_block >> (ptrs_bits * 2);
offsets[n++] = (i_block >> ptrs_bits) & (ptrs - 1);
offsets[n++] = i_block & (ptrs - 1);
final = ptrs;
} else {
ext2_warning (inode->i_sb, "ext2_block_to_path", "block > big");
}
这段代码仔细品读一下就能理解其含义,它就是三层映射路径分解的精髓之处。它就是根据文件内的块号,得到这个逻辑块的映射深度,并且算出这个逻辑块在每一层映射中的偏移量,并将计算的结果放在数组offset[]中。
由于每一层占用的位数是8位(1K的逻辑块,数组元素u32站2位,则一个逻辑块的间接寻址容量为256,即8位),于是把要解析的地址按每8位分层,每一层的8位表示它所在层的位移量(即它是属于所在层的第几个块)。
这里要特别注意的是,在ext2的块地址解析中,要先确定这个块号是属于第几层映射深度(每个深度是独立的),然后根据相应的深度减掉前面所有层深度的满块数之和(好好理解i_block -=,这些if-else-if语句是一层层往下检测的,每一层检测后都通过i_block-=将这一层的满块数减去了,因此无论到那一层都是已经把前面层所有的满块数和减去了的),这样得到的块号才是真正属于这层深度映射的地址,然后才能用上面所说的每8位分层解析出每层的偏移量。即右移得到某层的8位,然后按位与255得到这8位的值。这样结束后便得到这个块号所属的映射深度的地址,从i_data[]开始一直到真正的逻辑磁盘块号的每一层的偏移量(第几个块),即每一层的路径分量。这也是函数名block to path 的含义。
这个函数是从磁盘上逐层读入用于间接映射的记录块。
根据数组offset[]的指引,这个函数逐层将用于记录块号映射的记录块读入内存,并将指向缓冲区的指针保存在数组chain[]的相应元素,即Indirect结构中。同时还要使该Indirect结构中的指针p指向本层记录块号映射表(数组)中的相应表项,并使key字段持有该表项的内容,也就是所映射设备上块号。
将记录块读入到内存中,存放在buffer_head所指向的缓存。
读入一个记录块后再调用此函数检验一下映射链的有效性,实质上是检查隔层映射表中有关的内容是否改变了(from->key==*from->p)。
14→→→→→→→→→→→→→→add_chain ()
将读入的buffer地址以及逻辑块号等内容记录到chain[]的相应元素中,即Indirect结构中( p->key = *(p->p = v);p->bh = bh;)。
举个例子来看。假设要写的是文件内块号为10的块,则不需要间接映射,所以只用chain[0]一个Indirect结构。其指针bh为NULL,因为没有用于间接映射的记录块;指针p指向映射表中直接映射部分下标为10处,即&inode->u.ext2_i.i_data[10];而key则持有该表项的内容,即所映射的设备上块号。想比之下,文件内块号为20的块则需要一次间接映射,所以要用chain[0]和chain[1]两个表项。第一个表项chain[0]中的指针bh仍为NULL,因为在这一层没有用于间接映射的记录块;指针p指向映射表中下标为12处,即&inode->u.ext2_i.i_data[12],这是用于这一层间接映射的表项;而key则持有该表项的内容,即用于这一层间接映射的记录块的设备上块号。第二个表项chain[1]中的指针bh则指向该记录块的缓冲区,这个缓冲区的内容就是用作映射表的一个整数数组。所以chain[1]中的指针p指向这个数组中下标为8处,而key则持有该表项的内容,即经过间接映射后的设备上块号。这样,根据具体映射深度depth,数组chain[]中的最后一个元素,更确切的说是chain[depth-1].key,总是持有目标记录块的物理块号。而冲chain[]中的第一个元素chain[0]到具体映射的最后一个元素chain[depth-1]则提供了具体映射的整个路径,构成了一条映射链,这也是数组名chain的由来。如果把映射的过程看成爬树的过程,则一条映射链也可看成决定着书上的一个分支,所以叫ext2_get_branch()。
总结上面的分析,对于chain[]数组中Indirect元素的内容要尤其注意理解,它是文件内从逻辑块到物理块映射的载体。一个文件的整个逻辑结构由i_data[]表示,某个逻辑块在每一层的偏移由offset[]记录。在chain[]数组中,对于它的每一个元素即一个Indirect结构中,bh就是这个Indirect结构所代表的物理块映射到内存中后的首地址(这个物理块就是一个映射表数组);p则是bh+offset[]中响应的偏移得到的地址,也就是这个Indirect结构所代表的物理块中的某个地址(从这可看出就是p完成了文件从逻辑地址到物理地址的转换,因为它把逻辑偏移offset+块首址bh得到物理块中的地址);key就是这个地址中的内容,即块号(key就是下一层逻辑块的实际物理块号,也就是下一层Indirect结构中bh的来源)。每一层Indirect结构中的三个元素bh、p、key的含义和之间的关系就是这样的,然后如此往复。理解这三者的关系对后面的代码分析很有帮助。
从ext2_get_branch()返回到ext2_get_block(),返回值有两种可能。首先如果顺利完成映射,则返回值为NULL。其次,如果在某一层上发现映射表内的相应项为0,则说明这个表项(记录块)原来不存在(也就是说,在这一层的Indirect结构中,bh是有的,因为此层映射表已经读入内存,相应p也是有的,它就是bh+offset中的逻辑偏移,但是p地址中的内容为0,也就是代表下层块号key为0,这样一来,下一层的bh就无法读出。这也是为什么分配新块的时候会zero out 归零),现在因为写操作而需要扩充文件的大小。此时返回指向该层Indirect结构的指针,表示映射在此断裂了。此外如果映射的过程中出了错,例如读记录块失败,则返回一个错误码。
/* Simplest case - block found, no allocation needed */
if (!partial) {
first_block = le32_to_cpu(chain[depth - 1].key);
clear_buffer_new(bh_result); /* What's this do? */
… …
对于这种顺利完成了映射的情况,就把所得的结果填入作为参数传下来的缓冲区结构bh_result中。
要是ext2_get_branch()返回了一个非0指针,那就说明映射在某一层上断裂了。根据映射的深度和断裂的位置,这个记录也许还只是个中间的用于间接映射的记录块,也许就是最终的目标记录块。总之在这种情况下,要在设备上为目标记录块以及可能需要的中间记录块分配空间。
这个函数是对文件的磁盘预留窗口进行初始化,这是在2.6版本的内核中新加入的。
在磁盘上组织文件时,我们想将文件的数据尽可能存放在连续的磁盘块上,这样读写文件时,因为磁头移动的距离比较短,故速度会有很大提高。块预留机制的核心思想是文件系统应该提前考虑如果文件增长,可以从哪块空间分配磁盘块,并将这些磁盘块预留。采用这种方法,当文件增长时,会在磁盘的合适位置有空闲磁盘块供使用。为了达到这个目的,ext2块分配器被改为基于预留机制了。当一个文件第一次需要分配一个新块时,文件系统为它创建一个预留窗口,该窗口中保留了一些磁盘块(初始值为8个),然后从预留窗口中分配磁盘块。当预留窗口中的块用完时,尽量会在旧的预留窗口周围创建一个扩展的预留窗口,以代替旧的预留窗口。预留窗口会持续到写文件的进程关闭文件,然后,这些预留块又重新变为空闲块。
ext2块预留机制主要数据结构如下:
一、主要数据结构
预留块的信息是有一棵红黑树管理的,如图1:
struct rb_node
{
unsigned long rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
};
这个结构表示树中的一个节点。
struct rb_root
{
struct rb_node *rb_node;
};
这个结构只是个封装,用来指向一颗树的根节点。
注意上面节点中并没有数据,那么数据存放在哪里?
struct ext2_reserve_window {
ext2_fsblk_t _rsv_start; /* First byte reserved */
ext2_fsblk_t _rsv_end; /* Last byte reserved or 0 */
};
这个结构用于表示一个块预留区间,[_rsv_start,_rsv_end]。
struct ext2_reserve_window_node {
struct rb_node rsv_node;
__u32 rsv_goal_size;
__u32 rsv_alloc_hit;
struct ext2_reserve_window rsv_window;
};
这个结构既包含一个struct rb_node,又包含一个struct ext2_reserve_window,将两者结合起来了。
struct ext2_block_alloc_info {
struct ext2_reserve_window_node rsv_window_node;
__u32 last_alloc_logical_block;
ext2_fsblk_t last_alloc_physical_block;
};
这个结构描述了一个inode的预留窗口,以及上一次分配的逻辑磁盘块号和物理磁盘块号。
/*
* second extended file system inode data in memory
*/
struct ext2_inode_info {
……
/* block reservation info */
struct ext2_block_alloc_info *i_block_alloc_info;
};
这个结构是根据磁盘上的inode信息建立起来的,每个文件一个。
/*
* second extended-fs super-block data in memory
*/
struct ext2_sb_info {
/* root of the per fs reservation window tree */
spinlock_t s_rsv_window_lock;
struct rb_root s_rsv_window_root;
struct ext2_reserve_window_node s_rsv_window_head;
};
这个结构是根据磁盘中的超级块在内存中建立起来的,每个文件系统一个。其中s_rsv_window_root就是红黑树的根节点,s_rsv_window_head就是包含红黑树的根节点的struct ext2_reserve_window_node。
二、主要数据结构之间的关系
1、 一个文件系统对应一个ext2_sb_info结构,一个ext2_sb_info结构指向一棵红黑树。
2、 每个文件inode对应一个ext2_inode_info结构,每个ext2_inode_info结构指向一个ext2_block_alloc_info结构。
3、 每个ext2_block_alloc_info结构包含一个ext2_reserve_window_node结构。
4、 每个ext2_reserve_window_node结构包含一个rb_node结构和一个ext2_reserve_window结构。
5、 rb_node结构用于组成一个树状结构。
6、 每个ext2_reserve_window结构描述一个区间,并且多个ext2_reserve_window结构之间不会重叠。
ext2块预留机制主要数据结构转载自:
http://gbk.chinaunix.net/uid-52662-id-2107875.html
参数block为文件内逻辑块号,goal则用来返回所建议的设备上目标块号。如前所诉,ext2_inode_info结构中的i_block_alloc_info结构中设置了两个字段,last_alloc_logical_block和last_alloc_physical_block。前者用来记录下一次要分配的文件内块号,后者则用来记录希望下一次能分配的设备上块号。
正常情况下对文件的扩充是顺序的,所以每次的文件内块号都与前一次的连续,而理想的设备上块号也同样连续,二者平行的向前推进。当然这只是建议值,内核会尽量满足要求。可是文件内逻辑块号也有可能是不连续的,也就是说对文件的扩充是跳跃的。这种情况发生在通过系统调用lseek()将已经打开文件的当前读写位置推进到了超出文件末尾之后,新的逻辑块号与文件原有的最后一个逻辑块号之间留下了“空洞”。
这种情况下是通过ext2_find_near(),根据空洞的不同位置返回对设备上记录块号的建议值。要注意这两个函数返回的都是建议块号,设备上具体记录块的分配,包括目标记录块和可能需要的用于间接映射的中间记录块以及映射的建立,是由ext2_alloc_branch()完成的。调用之前先要算出映射断裂点离终点的距离,也就是还有几层映射需要建立。
/* the number of blocks need to allocate for [d,t]indirect blocks */
indirect_blks = (chain + depth) - partial - 1;
这就是计算出需要分配的间接映射块的数目。depth是总的块数目,partial是chain中断裂的那一层的地址,因此chain+depth-partial就是断裂层后还需要分配的块数,最后一块是直接块,因此再减一就是需要分配的间接块数。再然后就是调用ext2_blks_to_allocate()计算出总共需要分配的块数。
ext2_alloc_blocks一次性地就把我们需要的数据块都申请到了,并把它存放在数组new_blocks[]中。
创建的工作都是在ext2_new_blocks()中完成的,主要是处理一下预留窗口或者是查看位图。参数goal是建议分配的设备上记录块号,分配时,首先视图满足建议要求,如果所建议的记录块还空闲着,就把它分配出去,否则如果所建议的记录块已经分配掉了,就试图在它附近32个记录块的范围内分配。还不行就向前在本块组的位图中搜索,先找位图整个字节都是0,即至少有连续8个记录块空闲的区间,若实在找不到就在整个设备的范围内寻找和分配。
返回到ext2_alloc_branch (),是一个for循环。
for (n = 1; n <= indirect_blks; n++) {
/*
* Get buffer_head for parent block, zero it out
* and set the pointer to new one, then send
* parent to disk.
*/
bh = sb_getblk(inode->i_sb, new_blocks[n-1]);
branch[n].bh = bh;
lock_buffer(bh);
memset(bh->b_data, 0, blocksize);
branch[n].p = (__le32 *) bh->b_data + offsets[n];
branch[n].key = cpu_to_le32(new_blocks[n]);
*branch[n].p = branch[n].key;
… …
在for循环中,每一个间接块通过getblock()为其在内存中分配缓冲区,并通过memset就爱那个其缓冲区清成全0(zero out,前面有说过),然后在缓冲区中建立起本层的映射,即将p指向的地址内容填上缓冲区的块号key,即用p指向key,继而得到此块块号。
要注意一点的是,这个for循环是从branch[1]开始的,而chain[]数组断裂的开始处是branch[0],因此从branch[1]开始的p、key、逻辑块之间的映射已经建立好,但是branch[0]处的映射并未建立。在for循环的前面有这样一行代码:branch[0].key = cpu_to_le32(new_blocks[0]);即在映射开始断开的那一层上(branch[0]),所分配的记录块号只是记录了这一层Indirect结构中的key字段,却并未写入相应的映射表项中(由指针p所指之处)。也就是说我们的那根树枝已经建立好了,但是在断开部分还没连上,没有把这根树枝接在树上。
*where->p = where->key;这个函数一开始就是把原来映射开始断开的那一层所分配的记录块号写入了相应的映射表中。如果相应的Indirect结构中的指针bh为0(必定是chain[0]),则映射表就在inode结构中。否则,就是一个间接映射表。然后在修改了inode的相关字段包括最后分配的逻辑块号(last_alloc_logical_block),最后分配的物理块号(last_alloc_physical_block)等之后,将inode标志成脏。
回到ext2_get_block()中,把映射后的记录块连同设备号置入bh_result所指的缓冲区结构中,就完成了任务。从ext2_get_block()返回到_block_prepare_write(),for循环结束时,所有设计本次写操作的物理记录块(缓冲区)都已找到,需要从设备上读的读取完毕,写操作的准备工作就完成了。所以就返回到generic_perform_write()。
在generic_perform_write()中是一个while循环,通过具体文件系统所提供的函数为写操作做准备的。准备好了以后就可以从用户空间把待写的内容复制到缓冲区中,实际上是缓冲页面中。
为写操作准备好了以后,从缓冲区(缓冲页面)到设备上的记录块这条道路畅通了。这样才可以从用户空间把待写的内容复制过来。如前所述,目标记录块的缓冲区在文件层是作为缓冲页面的一部分存在的,所以这是从用户空间到缓冲页面的拷贝,具体就是通过这个函数完成。参数iov_iter *i中的iovec字段记录了指向用户空间的缓冲区buf以及待拷贝的长度(char __user *buf = i->iov->iov_base + i->iov_offset;)。对于i386处理器,flush_dcache_page是空操作。
写入缓冲页面后,调用write_end函数,ext2文件系统没有专门的write_end,就是generic_write_end()函数。
函数中的for循环扫描缓冲页面中的每个记录块,如果一个记录块与写入的范围(从from到to)相交,就把该记录块的缓冲区设成“up to date”,即与设备上的记录块一致,并将其设成dirty,下面的事就交给kflushd了。
值得注意的是,这里已经将缓冲区的BH_Update标志位设成1,表示缓冲区的内容已经与设备上相一致。可是,实际上此时缓冲区的内容尚未写会设备,所以从物理上说显然是不一致的。但是由于写操作本身已接近完成,涉及的缓冲区即将提交给kflushd,从逻辑角度上缓冲区中的内容与设备上的内容已经一致了。
所以所谓“一致”或“不一致”只是一个逻辑上的概念,并非物理上的概念。只要写入的内容已经“提交(commit)”,就认为已经一致了。而不一致的状态发生在写操作的中途,即改变了缓冲区的内容而尚未提交之前。在写入的准备阶段,遇有不一致的缓冲区就要从设备上重新读入,就是因为有未完成的写操作存在而破坏了缓冲区的内容。
完成了_block_commit_write()之后generic_perform_write()中的一轮循环,也就是对一个缓冲页面的写入就完成了 。这样循环结束返回到generic_file_buffered_write()也随之结束,进而整个写文件操作的主体generic_file_aio_write()就告结束。
最后将新的当前位置写入file中。Sys_write()结束。
sys_read()函数与write函数几乎一样,只是在sys_write()中要验证用户空间的缓冲区可读,并使用file_operations结构中的函数指针write,而在sys_read()中则要验证用户空间的缓冲区可写,并使用file_operations函数指针read。由于涉及到较多内核页、缓冲区中的内容,以后再看。