一、内存映射文件的写操作(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之间的这种竞态关系的操作,难道说这种情景是绝对发生不了的?
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之间的这种竞态关系的操作,难道说这种情景是绝对发生不了的?