【转】http://tracymacding.blog.163.com/blog/static/21286929920130395934274/
1. 引言
在我前面的博客中详细分析了Linux页面缓存的实现机制,包括各种数据结构以及之间的关联。本篇专栏中我们将会详细讨论文件系统如何从磁盘上读出一个页面。
我们知道,文件系统以页面(page,默认大小4096字节)为单位缓存文件数据,而早期的Linux中是以buffer head结构组织文件缓存的。每个buffer head数据大小与文件系统块大小相同,在当前版本操作系统中,page和buffer_head的关系如下图描述(图例中的页幁大小为4096字节,buffer_head数据大小为1024字节):
图1 page与buffer head关系图
因为Linux使用内存缓存文件数据,每次应用程序读写文件时首先必然在内存中进行,读时会首先从内存中查找当前读页面是否存在,若页面不存在或者当前页面的数据并非出于uptodata状态,那么VFS必须启动一次读页面流程。
若文件系统处理流程检测当前读页面不存在(尚未缓存)或者页面状态与磁盘不一致,此时需要从磁盘上读出页面内容。具体来说,调用具体文件系统的struct address_space_operations中的readpage方法,对于ext2文件系统来说,该方法被实例化为ext2_readpage,而其又是mpage_readpage的封装。mpage_readpage对于页面的读出会根据页面中的块在磁盘中是否连续而不同。具体来说,如果一个页面中的buffer_head对应的磁盘块在磁盘上连续,那么其实该page是无需创建buffer_head与其相关联,只有当页面中保存的磁盘块物理位置不连续,此时才需要创建多个buffer_head并在buffer_head结构中记录每一个逻辑块在磁盘中的物理块号。
mpage_readpage调用do_mpage_readpage完成具体的读页面工作。函数的参数一为需读出的页面信息,参数二为文件逻辑块至物理磁盘块的映射方法,因具体文件系统而异。
int mpage_readpage(struct page *page, get_block_t get_block)
{
struct bio *bio = NULL;
sector_t last_block_in_bio = 0;
struct buffer_head map_bh;
unsigned long first_logical_block = 0;
map_bh.b_state = 0;
map_bh.b_size = 0;
bio = do_mpage_readpage(bio, page, 1, &last_block_in_bio,&map_bh, &first_logical_block, get_block);
if (bio)
mpage_bio_submit(READ, bio);
return 0;
}
struct bio是文件系统与底层IO子系统连接件, 文件系统读/写页面其实就是向底层IO子系统发送struct bio请求。关于IO子系统会在别的章节中讨论,此处略过。mpage_readpage向调用者传入了多个参数,“1”表示仅读入一个页面。
static struct bio * do_mpage_readpage(struct bio *bio, struct page *page, unsigned nr_pages,
sector_t *last_block_in_bio, struct buffer_head *map_bh,
unsigned long *first_logical_block, get_block_t get_block)
{
struct inode *inode = page->mapping->host;
const unsigned blkbits = inode->i_blkbits;
const unsigned blocks_per_page = PAGE_CACHE_SIZE >> blkbits;
const unsigned blocksize = 1 << blkbits;
sector_t block_in_file;
sector_t last_block;
sector_t last_block_in_file;
sector_t blocks[MAX_BUF_PER_PAGE];
unsigned page_block;
unsigned first_hole = blocks_per_page;
struct block_device *bdev = NULL;
int length;
int fully_mapped = 1;
unsigned nblocks;
unsigned relative_block;
if (page_has_buffers(page))
goto confused;
......
confused:
if (bio)
bio = mpage_bio_submit(READ, bio);
if (!PageUptodate(page))
block_read_full_page(page, get_block);
else
unlock_page(page);
goto out;
}
该函数的处理流程是异常复杂的,但我们这里仅仅关注该page已经与buffer_head关联过的场景,当函数检测到该条件成立时,转入confused分支处理。
该分支中检测page是否是“Uptodata”状态,如果已经是Uptodata,直接解锁页面,那些等待在该页面的进程可以被唤醒,如果否,那么只能乖乖地调用block_read_full_page()从磁盘上读出页面数据。
这里需要注意的是,page不处于Uptodata并不意味着page中的所有block都是非Uptodata,可能是只有某些block是非Uptodata,而部分是Uptodata,因此,block_read_full_page只需读出那么非Uptodata的block即可,千万不可被函数名欺骗了双眼。
int block_read_full_page(struct page *page, get_block_t *get_block)
{
struct inode *inode = page->mapping->host;
sector_t iblock, lblock;
struct buffer_head *bh, *head, *arr[MAX_BUF_PER_PAGE];
unsigned int blocksize;
int nr, i;
int fully_mapped = 1;
BUG_ON(!PageLocked(page));
blocksize = 1 << inode->i_blkbits;
if (!page_has_buffers(page))
create_empty_buffers(page, blocksize, 0);
head = page_buffers(page);
//block number of a page
iblock = (sector_t)page->index << (PAGE_CACHE_SHIFT - inode->i_blkbits);
//last block number of this file
lblock = (i_size_read(inode)+blocksize-1) >> inode->i_blkbits;
bh = head;
nr = 0;
i = 0;
do {
if (buffer_uptodate(bh))
continue;
if (!buffer_mapped(bh)) {
int err = 0;
fully_mapped = 0;
//current block doesn't exceed file end
if (iblock < lblock) {
WARN_ON(bh->b_size != blocksize);
err = get_block(inode, iblock, bh, 0);
if (err)
SetPageError(page);
}
if (!buffer_mapped(bh)) {
zero_user(page, i * blocksize, blocksize);
if (!err)
set_buffer_uptodate(bh);
continue;
}
/*
* get_block() might have updated the buffer,some file system may do like this
* synchronously
*/
if (buffer_uptodate(bh))
continue;
}
arr[nr++] = bh;
} while (i++, iblock++, (bh = bh->b_this_page) != head);
if (fully_mapped)
SetPageMappedToDisk(page);
if (!nr) {
/*
* All buffers are uptodate - we can set the page uptodate
* as well. But not if get_block() returned an error.
*/
if (!PageError(page))
SetPageUptodate(page);
unlock_page(page);
return 0;
}
/* Stage two: lock the buffers */
for (i = 0; i < nr; i++) {
bh = arr[i];
lock_buffer(bh);
mark_buffer_async_read(bh);
}
/*
* Stage 3: start the IO. Check for uptodateness
* inside the buffer lock in case another process reading
* the underlying blockdev brought it uptodate (the sct fix).
*/
for (i = 0; i < nr; i++) {
bh = arr[i];
if (buffer_uptodate(bh))
end_buffer_async_read(bh, 1);
else
submit_bh(READ, bh);
}
return 0;
}
这个函数读页面的逻辑是极其清晰的:
1. 搜集判断该页面中有多少个block处于非Uptodata状态,只需判断buffer_head的标志位即可,对于非Uptodata状态的block还需要判断该block是否已映射,即该逻辑块是否已经与物理磁盘块建立了映射关系,如果没有,那么说明可能别的进程解除了该block的映射关系(谁?为什么会解除一个页面的某些block的映射关系?),此时需要作一个简单判断,如果当前读的位置并没有超过文件末尾,那么,建立映射,否则,将内存block填充0,并设置其为Uptodata状态,至于为什么会读超过文件末尾的位置,我想这是因为,可能有别的进程在本程序读之前删除了文件的部分数据,导致文件变小,而读进程并未感知这种变化。映射完成以后,还需要判断该block是否处于Uptodata状态,这是因为某些文件系统可能在映射期间读出块数据。
2.检查完成页面中的所有块状态以后,如果某些块处于非Uptodata状态,那么需要锁住这些block(lock_buffer()),并设置读完成以后的回调函数mark_buffer_async_read().
3.一切就绪后,向底层IO子系统提交每一个待读出的buffer_head。
在上面的步骤2中,首先锁定每个非Uptodata的buffer_head,即对每个buffer_head设置一个BH_Locked标志位,此时其他进程无法再操作该buffer_head对应的块直到其被解锁。锁定以后,调用函数mark_buffer_async_read()来设置读完成以后的回调函数。
static void mark_buffer_async_read(struct buffer_head *bh)
{
bh->b_end_io = end_buffer_async_read;
set_buffer_async_read(bh);
}
可以看到在改函数中不仅设置了bh完成时的回调函数end_buffer_async_read,同时调用了set_buffer_async_read()为该buffer_head设置一个标记BH_Async_Read,表示该buffer_head目前正处于读过程中,那么,为什么需要设置这样一个标志位?
要弄清楚这个问题,我们就必须深入到buffer_head完成时的回调函数
end_buffer_async_read
中:
static void end_buffer_async_read(struct buffer_head *bh, int uptodate)
{
unsigned long flags;
struct buffer_head *first;
struct buffer_head *tmp;
struct page *page;
int page_uptodate = 1;
BUG_ON(!buffer_async_read(bh));
page = bh->b_page;
if (uptodate) {
set_buffer_uptodate(bh);
} else {
clear_buffer_uptodate(bh);
if (!quiet_error(bh))
buffer_io_error(bh);
SetPageError(page);
}
/*
* Be _very_ careful from here on. Bad things can happen if
* two buffer heads end IO at almost the same time and both
* decide that the page is now completely done.
*/
//serialize page's buffer_head's callback function
first = page_buffers(page);
local_irq_save(flags);
bit_spin_lock(BH_Uptodate_Lock, &first->b_state);
clear_buffer_async_read(bh);
//now the buffer is Uptodata,we can unlock it
unlock_buffer(bh);
tmp = bh;
do {
if (!buffer_uptodate(tmp))
page_uptodate = 0;
if (buffer_async_read(tmp)) {
BUG_ON(!buffer_locked(tmp));
goto still_busy;
}
tmp = tmp->b_this_page;
} while (tmp != bh);
bit_spin_unlock(BH_Uptodate_Lock, &first->b_state);
local_irq_restore(flags);
/*
* If none of the buffers had errors and they are all
* uptodate then we can set the page uptodate.
*/
if (page_uptodate && !PageError(page))
SetPageUptodate(page);
unlock_page(page);
return;
still_busy:
bit_spin_unlock(BH_Uptodate_Lock, &first->b_state);
local_irq_restore(flags);
return;
}
当某个block被读出后,最终会进入该完成函数,传入的uptodata表明本次读是否正确,如果读正确,那么设置该buffer_head状态为BH_Uptodata,然后进入一个串行化处理流程,之所以要串行化是为了以下考虑:
假如pageX的两个页面A和B都被提交至IO子系统进行读,当他们完成以后都会进入该完成函数,在函数中会检查页面中的其他block状态,因此,需要串行化,避免多个函数同时处理page这个临界资源。
明白了这个问题,我们上述的问题也就比较容易理解了,每个block在读完成以后都会在完成函数中判断block所在页面其余的block状态,阅读上述代码可知,完成函数的处理流程是首先将buffer_head的Uptodata置位,然后获取串行处理的锁,在获取锁之后解锁buffer_head并清除BH_Async_Read标志。现在我们来考虑如果不设置BH_Async_Read的处理流程:
如果没有BH_Async_Read这类标志,那么每个buffer_head唯一设置的标志位就是BH_Locked。那完成函数的处理流程可如下图描述:
1. 设置buffer_head BH_Uptodata;
2. 获取串行化锁;
3. 对buffer_head解锁;
4. 遍历buffer_head所属page的所有block,检查其是否锁定,若锁定跳转至步骤6;
5. 根据上述遍历结果决定是否将page设置为PG_Uptodata,解锁page;
6. 释放串行化锁。
对于pageX中的块A和B,A和B在读之前都被加了锁,即设置了BH_Locked,同时pagex也被加锁,设置标志位PG_Locked,此时假如A读完成,进入完成函数,函数中首先设置A状态为BH_Uptodata,然后获取串行锁,假设获取成功,完成函数中遍历pageX其他的block,发现B被锁定,那么A就认定此时B可能正被锁定读写,因此,根据上述流程,此时尚不能解锁页面pageX,但A的BH_Locked已被清除。
假如此时某个线程T锁定了A,而正好B读完成进入完成函数
end_buffer_async_read(),它完成固定工作后获取串行锁遍历pageX所有block,发现A此时被锁定,那么直接释放串行锁并返回。
此时便产生问题了,被加在pageX上的锁PG_Locked永远没有机会得到释放,形成死锁。
因此,问题出现在一个BH_Locked并不能满足buffer_head的所有应用情况,需要为其增加一个标志位来标志是否正在进行Read/Write。因此,添加BH_Async_Read便可解决上述问题,此时处理流程变为:
1. 设置buffer_head BH_Uptodata;
2. 获取串行化锁;
3. 清除buffer_head的BH_Async_Read标志;
4. 对buffer_head解锁;
5. 遍历buffer_head所属page的所有block,检查其标志位BH_Async_Read是否设置,若锁定跳转至步骤7,此时页面尚有块正在进行IO,不可解锁;
6. 根据上述遍历结果决定是否将page设置为PG_Uptodata,解锁page;
7. 释放串行化锁。

本文详细解析Linux文件系统如何从磁盘读取页面。介绍了page与buffer_head的关系,阐述了mpage_readpage函数的工作原理及block_read_full_page函数如何处理非Uptodate状态的block。
2745

被折叠的 条评论
为什么被折叠?



