Linux文件系统从磁盘读页面

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

【转】http://tracymacding.blog.163.com/blog/static/21286929920130395934274/

1. 引言

在我前面的博客中详细分析了Linux页面缓存的实现机制,包括各种数据结构以及之间的关联。本篇专栏中我们将会详细讨论文件系统如何从磁盘上读出一个页面。
我们知道,文件系统以页面(page,默认大小4096字节)为单位缓存文件数据,而早期的Linux中是以buffer head结构组织文件缓存的。每个buffer head数据大小与文件系统块大小相同,在当前版本操作系统中,page和buffer_head的关系如下图描述(图例中的页幁大小为4096字节,buffer_head数据大小为1024字节):
Linux文件系统从磁盘读页面 - tracymacding - tracymacding的博客
图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. 释放串行化锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值