您的位置:首页 > 运维架构 > Linux

两种文件写操作的页缓存数据刷出操作和函数调用路径分析

2017-04-04 13:06 597 查看
一、内存映射文件的写操作(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之间的这种竞态关系的操作,难道说这种情景是绝对发生不了的?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐