免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 2389 | 回复: 0
打印 上一主题 下一主题

[文件系统] 两种文件写操作的页缓存数据刷出操作和函数调用路径分析 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2017-04-04 13:06 |只看该作者 |倒序浏览
                                                     
一、内存映射文件的写操作(MAP_SHARED模式):
1、写内存时按以下流程标记页为脏:pte_mkdirty(pte),swap_out->……->try_to_swap_out时set_page_dirty(page)
2、文件映射内存同步到磁盘(调用sys_msync)
(1)sys_msync->msync_interval:调用filemap_sync、filemap_fdatasync、ext2_sync_file。
(2)filemap_sync扫描指定范围的内存页表项,对于页表项标记为脏的,将相关页标记为脏;
(3)filemap_fdatasync将脏页的缓冲区加入脏缓冲区链表(inode->i_dirty_data_buffers和lru_list[BUF_DIRTY]链表)。
(4)ext2_sync_file->ext2_fsync_inode:先后调用3个函数,fsync_inode_buffers、fsync_inode_data_buffers、ext2_sync_inode,见一.2.(5)的描述。

(5)ext2_sync_file->ext2_fsync_inode:调用fsync_inode_buffers、fsync_inode_data_buffers、ext2_sync_inode。
fsync_inode_buffers对inode->i_dirty_buffers(元数据,如文件中间指针块)链表中的脏缓冲区调用ll_rw_block刷出到磁盘;
fsync_inode_data_buffers对inode->i_dirty_data_buffers(实际文件数据)链表中的脏缓冲区调用ll_rw_block刷出到磁盘;
ext2_sync_inode调用ext2_update_inode把inode节点所在块写回磁盘。
(6)1个文件元数据脏块的产生场景举例:ext2_get_block->ext2_alloc_branch->buffer_insert_inode_queue

注意:
buffer_insert_inode_queue是把文件元数据脏块加入inode->i_dirty_buffers链表。
buffer_insert_inode_data_queue是把文件本身的脏数据块加入inode->i_dirty_data_buffers链表。

二、普通文件写操作sys_write->generic_file_write:
(1)__grab_cache_page->__find_lock_page
(2)prepare_write、__copy_from_user、commit_write。
(3)最后脏数据块由后台内核线程bdflush或kupdate等异步写回磁盘。

struct address_space_operations ext2_aops = {
    readpage: ext2_readpage,
    writepage: ext2_writepage,
    sync_page: block_sync_page,
    prepare_write: ext2_prepare_write,
    commit_write: generic_commit_write,
    bmap: ext2_bmap,
    direct_IO: ext2_direct_IO,
};


三、sys_fsync刷新文件脏页到磁盘:
(1)sys_fsync主要有两个步骤: 首先调用filemap_fdatasync(对脏页的块缓冲区打BH_Dirty标记),然后调用ext2_sync_file(刷出脏缓冲区到磁盘)。
(2)filemap_fdatasync先后调用三个函数:lock_page(page),ClearPageDirty(page),write_page(page)
write_page->ext2_writepage->block_write_full_page:调用两个函数, prepare_write(建立页块映射)、commit_write(块缓冲区标记为脏,加入相应链表)。
prepare_write->ext2_prepare_write
commit_write->generic_commit_write
(3)ext2_sync_file见一.2.(5)的描述。


四、综述
(1)3个层面的操作互斥
以上几种方式凡是在文件级别进行操作时一般通过inode->i_sem信号量进行互斥;
对页缓存操作时通过自旋锁pagecache_lock进行互斥;
对页操作时,给页上锁lock_page(page)
给块缓冲区调整lru_list时,通过自旋锁lru_list_lock互斥。

(2)重点关注下几个方式操作同一块缓冲区时的互斥同步问题
后台内核线程kupdate、bdflush刷出脏块,用户进程调用sys_fsync或sys_msync刷出脏块。主要涉及块缓存的状态一致性、所在链表的一致性。
kupdate->……->sync_old_buffers->write_some_buffers->write_locked_buffers->submit_bh->……->do_rw_disk->……->ide_output_data
bdflush->write_some_buffers->write_locked_buffers->submit_bh->……->do_rw_disk->……->ide_output_data

根据kupdate和bdflush的路径,可知它们都调用了write_some_buffers,该函数是自身同步互斥的,保证了数据一致性。
sys_msync和sys_fsync也有相似点,它们都调用了ext2_sync_file进行文件脏块缓冲区的刷出。
最后两边的互斥同步落在write_some_buffers和ll_rw_block之间。代码贴在下面,这里边的关键在于atomic_set_buffer_clean这个原子操作。
它保证了两个函数不会同时操作同一个块缓冲区。


---------------------------------write_some_buffers
#define NRSYNC (32)
static int write_some_buffers(kdev_t dev)
{
    struct buffer_head *next;
    struct buffer_head *array[NRSYNC];
    unsigned int count;
    int nr;

    next = lru_list[BUF_DIRTY];
    nr = nr_buffers_type[BUF_DIRTY];
    count = 0;
    while (next && --nr >= 0) {
        struct buffer_head * bh = next;
        next = bh->b_next_free;

        if (dev && bh->b_dev != dev)
            continue;
        if (test_and_set_bit(BH_Lock, &bh->b_state))
            continue;
        if (atomic_set_buffer_clean(bh)) {
            __refile_buffer(bh);
            get_bh(bh);
            array[count++] = bh;
            if (count < NRSYNC)
                continue;

            spin_unlock(&lru_list_lock);
            write_locked_buffers(array, count);
            return -EAGAIN;
        }
        unlock_buffer(bh);
        __refile_buffer(bh);
    }
    spin_unlock(&lru_list_lock);

    if (count)
        write_locked_buffers(array, count);
    return 0;
}

---------------------------------ll_rw_block
void ll_rw_block(int rw, int nr, struct buffer_head * bhs[])
{
    unsigned int major;
    int correct_size;
    int i;

    if (!nr)
        return;

    major = MAJOR(bhs[0]->b_dev);

    /* Determine correct block size for this device. */
    correct_size = get_hardsect_size(bhs[0]->b_dev);

    /* Verify requested block sizes. */
    for (i = 0; i < nr; i++) {
        struct buffer_head *bh = bhs[i];
        if (bh->b_size % correct_size) {
            printk(KERN_NOTICE "ll_rw_block: device %s: "
                   "only %d-char blocks implemented (%u)\n",
                   kdevname(bhs[0]->b_dev),
                   correct_size, bh->b_size);
            goto sorry;
        }
    }

    if ((rw & WRITE) && is_read_only(bhs[0]->b_dev)) {
        printk(KERN_NOTICE "Can't write to read-only device %s\n",
               kdevname(bhs[0]->b_dev));
        goto sorry;
    }

    for (i = 0; i < nr; i++) {
        struct buffer_head *bh = bhs[i];

        /* Only one thread can actually submit the I/O. */
        if (test_and_set_bit(BH_Lock, &bh->b_state))
            continue;

        /* We have the buffer lock */
        atomic_inc(&bh->b_count);
        bh->b_end_io = end_buffer_io_sync;

        switch(rw) {
        case WRITE:
            if (!atomic_set_buffer_clean(bh))
                /* Hmmph! Nothing to write */
                goto end_io;
            __mark_buffer_clean(bh);
            break;

        case READA:
        case READ:
            if (buffer_uptodate(bh))
                /* Hmmph! Already have it */
                goto end_io;
            break;
        default:
            BUG();
    end_io:
            bh->b_end_io(bh, test_bit(BH_Uptodate, &bh->b_state));
            continue;
        }

        submit_bh(rw, bh);
    }
    return;

sorry:
    /* Make sure we don't get infinite dirty retries.. */
    for (i = 0; i < nr; i++)
        mark_buffer_clean(bhs[i]);
}


(3)关于修改缓冲区数据和异步磁盘IO操作的数据一致性问题:
在request_queue中的bh写到磁盘IO端口的同时generic_file_write修改bh数据内容的问题,其实不存在数据不一致的问题,因为
generic_file_write会设置缓冲区的BH_Dirty标志,因此该缓冲区以后至少还会再写到磁盘一次。

相关论坛帖子:
---------------------------------2.4内核中文件写操作和缓冲区刷新到磁盘之间的竞态存在吗?
既然写文件是异步的,是否有可能一个缓冲区刷新到磁盘的过程中,另一个文件写操作正在改变缓冲区的内容?怎么避免generic_file_write和bdflush、kupdate之间的这种竞态关系?

---------------------------------重温2.4内核的文件写操作和缓冲区时,关于竞态问题的疑惑。
读2.4.18内核的generic_file_write函数,发现里边写page内的块缓冲区时没有使用任何关于块缓冲区buffer层面的同步互斥措施,也没检查缓冲区上锁情况。写文件是异步的,也就是说,将来的某个不确定的时机这个dirty的buffer将被submit_bh到硬盘的request_queue中。假设这样一个情景,即page中的buffer(不妨称之为bufferA)写脏后,在某个时刻提交给request_queue,然后某个时刻do_rw_disk将其中数据拷贝到硬盘接口的IO端口中。正在拷贝的过程中,某个进程调用了文件写操作,也操作这个bufferA中的数据,那么就可能造成数据不一致的情况。两个路径如下:

//1、用户数据拷贝到buffer中时没有检查buffer是不是正写到IO
generic_file_write->__copy_from_user(kaddr+offset, buf, bytes)
//2、缓冲区写到硬盘接口IO(比如可以假设是由kupdate启动的)
kupdate->……->sync_old_buffers->write_some_buffers->write_locked_buffers->submit_bh->……->do_rw_disk->……->ide_output_data
bdflush->write_some_buffers->write_locked_buffers->submit_bh->……->do_rw_disk->……->ide_output_data
是否有可能一个缓冲区刷新到磁盘的过程(路径2执行)中,另一个文件写操作(路径1也执行)正在改变缓冲区的内容?内核中似乎没有避免generic_file_write和bdflush、kupdate之间的这种竞态关系的操作,难道说这种情景是绝对发生不了的?



您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP