文件写入前的读取

一、写入前可能需要读取
在前一篇博客中看ramfs文件的时候想到一个问题:内存缓冲是以page为单位的,而许多磁盘设备是以block(sector)为单位的,当然这都不是重点。重点是现在假设有一个文件,它大概有两个页面,现在我只在文件的开始写入(修改)一点点数据,比方说10个字节,然后再seek到文件的开始进行读取操作,此时缓存如何管理?
问题看起来可能很简单,但是请允许我把它复杂化一下。根据缓冲的原则,如果要写入一个文件,首先要分配一个页面进行缓冲,这个缓冲页面将会作为实体设备在内存中的镜像,读取/写入操作都先在这个页面上完成,然后在折腾的差不多了之后再写回设备。就好像做硬件的时候,先用模拟器来仿真一段时间,最后修改的差不多了,再烧到真正的硬件中。在分配一个缓冲页面的时候,我只是修改了这个页面开始的10个字节,这个10个字节之后的所有内容都应该保持之前的内容。那么这个缓冲页的内容会是什么样子。假设只是修改了缓冲页开始的10个字节,之后的内容留空或者全部初始化为零,那么当下次再次读取的时候它如何判断这个页面中的哪些位置是已经被修改过的?从设备中读取的扇区将会覆盖内存页面的什么位置?假设说每次写入的时候都把所有将要蹂躏的扇区都读入内存,那么就更没有必要了。比方说我修改了10000字节,跨越接近20个扇区,如果每个扇区都读入,然后读入之后马上被修改为其它值,那这个读取明显是耗时而没有意义的。
二、linux的实现
我们就不从sys_write开始了,直接从ext2_file_operations开始,它的write接口为:
do_sync_write--->>>generic_file_aio_write---->>>__generic_file_aio_write_nolock---->>>generic_file_buffered_write--->>>ext2_prepare_write--->>>block_prepare_write--->>>__block_prepare_write
实现的核心就在这个文件里

static int __block_prepare_write(struct inode *inode, struct page *page,
        unsigned from, unsigned to, get_block_t *get_block)这里的get_block)变量为ext2_get_block,from和to都是页面内偏移量,而不是文件内偏移。
{
    unsigned block_start, block_end;
    sector_t block;
    int err = 0;
    unsigned blocksize, bbits;
    struct buffer_head *bh, *head, *wait[2], **wait_bh=wait;

    BUG_ON(!PageLocked(page));
    BUG_ON(from > PAGE_CACHE_SIZE);
    BUG_ON(to > PAGE_CACHE_SIZE);
    BUG_ON(from > to);

    blocksize = 1 << inode->i_blkbits;
    if (!page_has_buffers(page))
        create_empty_buffers(page, blocksize, 0);
    head = page_buffers(page);

    bbits = inode->i_blkbits;
    block = (sector_t)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;
        if (block_end <= from || block_start >= to) {
            if (PageUptodate(page)) {
                if (!buffer_uptodate(bh))
                    set_buffer_uptodate(bh);
            }
            continue;
        }
        if (buffer_new(bh))
            clear_buffer_new(bh);
        if (!buffer_mapped(bh)) {
            WARN_ON(bh->b_size != blocksize);
            err = get_block(inode, block, bh, 1);这里的get_block对于每个没有在内存中的页面都会被执行,但是这里不要被名字所迷惑,它不会启动对文件数据的真正读取(尽管可能会启动对inode节点及数据的读取),它只是对页面对应的buffer_head结构进行初始化,例如建立page和设备block之间的映射关系,这种映射关系根据不同的文件系统有不同的实现方式,例如经典的unix的三次间接寻址结构。而函数的最后一个参数get_block就是负责根据不同的文件系统来建立buffer_head和page的不同映射关系。再次强调,这里并不会读取文件具体内容,主要负责建立设备block和page之间的映射关系
            if (err)
                break;
            if (buffer_new(bh)) {
                unmap_underlying_metadata(bh->b_bdev,
                            bh->b_blocknr);
                if (PageUptodate(page)) {
                    set_buffer_uptodate(bh);
                    continue;
                }
                if (block_end > to || block_start < from) {
                    void *kaddr;

                    kaddr = kmap_atomic(page, KM_USER0);
                    if (block_end > to)
                        memset(kaddr+to, 0,
                            block_end-to);
                    if (block_start < from)
                        memset(kaddr+block_start,
                            0, from-block_start);
                    flush_dcache_page(page);
                    kunmap_atomic(kaddr, KM_USER0);
                }
                continue;
            }
        }
        if (PageUptodate(page)) {
            if (!buffer_uptodate(bh))
                set_buffer_uptodate(bh);
            continue; 
        }
        if (!buffer_uptodate(bh) && !buffer_delay(bh) &&
            !buffer_unwritten(bh) &&
             (block_start < from || block_end > to)) {这里是一个对文件数据的真正读取操作。一个页面中只有在 【from,to】之外(包括from和to所在的block)的block才会被读取,两者之间的block不需要被读取。现在假设从文件的第10个字节开始,写入512*8个字节的内容。则此时文件的第一个扇区是会被读取的,但是接下来的同一个页面缓冲内的7个扇区(假设一个block等于一个扇区,扇区大小为512字节)都不会被读取,因为此时的from为10,to为4096,满足(block_start < from || block_end > to)的只有第一个buffer_head,所以此时会启动对第一个扇区的读取。接下来的扇区都不会被读取,但是会被建立block和内存的映射关系(通过前面的get_block实现)以供之后把修改写回。对于最后一个扇区,同样需要读取,因为它的from为0,to为10,所以也只有第一个满足条件
            ll_rw_block(READ, 1, &bh);
            *wait_bh++=bh;
        }
    }
    ……
}
三、说明
这里的实现看起来很奇怪,在写入之前还要启动读取,但是从整体来说,还是对效率提高有好处的。如果直接使用write系统调用,明显可以看到,如果每次都是少量数据写入的话,将会触发对修改的所有扇区的读取。所以应该尽量使用一次性大量数据写入,从而避免读中间扇区的读取。例如,对于上面的例子,假设一次性写入512*1000,字节数据,也最多只进行开始和最后两个block的读取。但是大家也不用刻意做这个缓冲,因为C库的FILE结构已经进行了适当的缓冲,如果这个缓冲还不给力的话,同志们可以自己实现一个。据说数据库都是使用了非缓冲的文件操作,所有缓冲都在用户态完成,这样主要是为了完成数据库的事务性(transaction),另一方面,也可以利用这个属性提高写操作的效率。
对于赤裸裸的读操作,系统没有这个问题,但是一般会通过readahead机制来提前读取一些扇区内容,从而减少硬盘的寻道时间,这也是一个好的实现方法和思路,这些都是由内核完成的,所以不用担心。

原文地址:https://www.cnblogs.com/tsecer/p/10486037.html