块设备文件的readpage方法

块设备文件的readpage方法总是相同的。都是有blkdev_readpage( )函数实现的,它实际调用block_read_full_page( ):

static int blkdev_readpage(struct file * file, struct page * page)
{
    return block_read_full_page(page, blkdev_get_block);
}

page参数代表的含义是 要将块设备文件的内容读到page对应的页框中去,你可能好奇从块设备文件的哪个地方开始读?读多少?这些信息其实全在page里。
block_read_full_page的第二个参数指向一个函数,该函数把相对于文件开始处的文件块号转换为块设备开始处的逻辑块号。对于块设备文件来说,这两个数是一样的。

static int blkdev_get_block(struct inode *inode, sector_t iblock,
                            struct buffer_head *bh, int create)
{
    bh->b_bdev = I_BDEV(inode);
    bh->b_blocknr = iblock; 
//可以看出对于块设备文件来说,相对于文件开始处的文件块号和相对块设备开始处的逻辑块号是一样的

    set_buffer_mapped(bh);
    return 0;
}

可能读者对相对于文件开始处的文件块号和相对块设备开始处的逻辑块号有点迷惑,我来具体解释下:

  • 相对于文件开始处的文件块号:我们知道块是vfs和文件系统传送数据的基本单位。每个文件都是又很多个块组成的。我们要读写一个文件的某一个块,就得知道这个块是这个文件的第几个块,这就叫相对于文件开始处的文件块号。
    对设备文件直接读写是一种“原始”访问,它绕过了文件系统,内核通过使用最大的块(4096)执行该操作。
    每个块都需要自己的块缓冲区(块缓存),它是内核用来存放块内容的内存区。每个缓冲区对应一个缓冲区首部,用buffer_head描述(相当块缓存的元数据信息)。

  • 相对块设备开始处的逻辑块号:文件都是存在具体的物理设备上如磁盘,我们要往修改文件某块时,光知道该块相对文件是第几块是不够的,需要知道该块是在物理设备的第几个块才行。block_read_full_page的第二个参数就是一个实现块号转换的函数。对于块设备文件,它就是块设备的意思,所以两种块号相同,但对于普通文件就不同了。

    言归正传,block_read_full_page代表从块设备读取一整页,看看它的实现:

int block_read_full_page(struct page *page, get_block_t *get_block)  
{  
    struct inode *inode = page->mapping->host;  //找到inode我们才能找到文件
    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;  //块大小

    /*检查page的标志PG_private,如果置位,则该页与描述组成该页的块的缓冲区首部
    相关,否则调用create_empty_buffers来为该页所含的所有块缓冲区分配块缓冲区首
    部,页中第一个缓冲区首部地址放在page->private字段。每个缓冲区首部的
    b_this_page字段指向该页中下一个缓冲区的缓冲区首部*/
    if (!page_has_buffers(page))
        create_empty_buffers(page, blocksize, 0);  

    /*获得这些缓冲区,无论是新建的还是已经存在的 
    只是将page的private成员转换为buffer_head指针,因为按照 
    惯例,private指向与page关联的第一个缓冲头*/  
    head = page_buffers(page);  

  /*从相对于页的偏移量计算出页中第一块的文件块号(该页的第一块是文件的第几块,我觉得
  page->index在这的意思就是把文件划分成几个页,这个页是第几个页)
    iblock = (sector_t)page->index << (PAGE_CACHE_SHIFT - inode->i_blkbits);  
    //文件的结束块号
    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;  
            if (iblock < lblock) {  
                WARN_ON(bh->b_size != blocksize);  
                /*确定块在块设备上的位置,根据iblock得到相对于磁盘或者分区开始处
                的缓冲区逻辑块号*/  
                err = get_block(inode, iblock, bh, 0);  
                if (err)  
                    SetPageError(page);  
            }  
            /*超过文件范围的块,置0,并将对应的块缓冲区设为有效*/
            if (!buffer_mapped(bh)) {  
                zero_user(page, i * blocksize, blocksize);  
                if (!err)  
                    set_buffer_uptodate(bh);  
                continue;  
            }  
            /*因为依赖于文件系统的get_block函数可能已触发块IO操作而更新了缓冲区*/ 
            if (buffer_uptodate(bh))  
                continue;  
        }  
        /*如果缓冲区已经建立了与块的映射,但是其内容不是最新 
        的则将缓冲区放置到一个临时的数组中*/  
        arr[nr++] = bh;  
    } while (i++, iblock++, (bh = bh->b_this_page) != head);  

     //没有遇到文件洞,将该页的标志PG_mappedtodisk置位 
    if (fully_mapped)  
        SetPageMappedToDisk(page);  

    if (!nr) {  
        /* 页中所有的块缓冲区都是有效的,因此设置页描述符的PG_uptodate标志*/
        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);  
        /*将b_end_io设置为end_buffer_async_read,该函数将在数据传输结构时 
        调用*/  
        mark_buffer_async_read(bh);  
    }  


    for (i = 0; i < nr; i++) {  
        bh = arr[i];  
        /*再检查一遍块缓冲区是否有效,有效则不用从磁盘重复读了*/
        if (buffer_uptodate(bh))  
            end_buffer_async_read(bh, 1);  
        else  
            /*将所有需要读取的缓冲区转交给块层 
            也就是BIO层,在其中开始读操作*/  
            submit_bh(READ, bh);  
    }  
    return 0;  
}  

一页划分为几个数据单元,但缓冲头保存在独立的内存区中,与实际数据无关。与缓冲区的交互没有改变的页的内容,缓冲区只不过为页的数据提供了一个新的视图。
为支持页与缓冲区的交互,需要使用struct page的private成员。其类型为unsigned long,可用作指向虚拟地址空间中任何位置的指针。
Private成员还可以用作其他用途,根据页的具体用途,可能与缓冲头完全无关。但其主要的用途是关联缓冲区和页。这样的话,private指向将页划分为更小单位的第一个缓冲头。各个缓冲头通过b_this_page链接为一个环形链表。在该链表中每个缓冲头的b_this_page成员指向下一个缓冲头,而最后一个缓冲头的b_this_page成员指向第一个缓冲头。这使得内核从page结构开始,可以轻易地扫描与页关联的所有buffer_head实例。
内核提供cteate_empty_buffers函数关联page和buffer_head结构之间的关联:

void create_empty_buffers(struct page *page,  
            unsigned long blocksize, unsigned long b_state)  
{  
    struct buffer_head *bh, *head, *tail;  

    head = alloc_page_buffers(page, blocksize, 1);  
    bh = head;  
    /*遍历所有缓冲头,设置其状态,并建立一个环形链表*/  
    do {  
        bh->b_state |= b_state;  
        tail = bh;  
        bh = bh->b_this_page;  
    } while (bh);  
    tail->b_this_page = head;  

    spin_lock(&page->mapping->private_lock);  
    /*缓冲区的状态依赖于内存页面中数据的状态*/  
    if (PageUptodate(page) || PageDirty(page)) {  
        bh = head;  
        do {/*设置相关标志*/  
            if (PageDirty(page))  
                set_buffer_dirty(bh);  
            if (PageUptodate(page))  
                set_buffer_uptodate(bh);  
            bh = bh->b_this_page;  
        } while (bh != head);  
    }  
    /*将缓冲区关联到页面*/  
    attach_page_buffers(page, head);  
    spin_unlock(&page->mapping->private_lock);  
}  
static inline void attach_page_buffers(struct page *page,  
        struct buffer_head *head)  
{  
    page_cache_get(page);/*递增引用计数*/  
    /*设置PG_private标志,通知内核其他部分,page实例的private成员正在使用中*/  
    SetPagePrivate(page);  
    /*将页的private成员设置为一个指向环形链表中第一个缓冲头的指针*/  
    set_page_private(page, (unsigned long)head);  
}  
int submit_bh(int rw, struct buffer_head *bh)
{
    return _submit_bh(rw, bh, 0);
}

int _submit_bh(int rw, struct buffer_head *bh, unsigned long bio_flags)
{
    ...
    /*下面这个就是将相对于块设备开始处的逻辑块号转换成相对于块设备开始处的扇区号。
    一个扇区内核当做512字节,因此是9*/
    bio->bi_sector = bh->b_blocknr * (bh->b_size >> 9);
    bio->bi_bdev = bh->b_bdev;
    bio->bi_io_vec[0].bv_page = bh->b_page;
    bio->bi_io_vec[0].bv_len = bh->b_size;
    bio->bi_io_vec[0].bv_offset = bh_offset(bh);

    bio->bi_vcnt = 1;//从submit_bh构建的bio只含有一个段的
    ...
    submit_bio(rw, bio);//交给通用层去处理io了
    ...
}

你可能感兴趣的:(内核之页高速缓存)