问题根因:
ext4_da_write_begin 前台使用计数的方式预留block(仅预留数据块),writepages的时候申请块,那么如果用户写入5个数据块,预留计数为5,但实际writepages的时候可能由于需要额外的extent块,实际申请的物理块数量6会超出前台计数。
ext4通过两种方式保证da_write通过引用计数方式不会出现上述情况:
- ext4_da_write_begin -> ext4_nonda_switch:
if (dirty_clusters && (free_clusters < 2 * dirty_clusters)) // 如果free块数小于2倍的dirty数量,dirty会基于前台写入的数据块数累加,在writepage申请到物理块后消减,同时free也再次之后被消减
try_to_writeback_inodes_sb(sb, WB_REASON_FS_FREE_SPACE); // 等待回写
if (2 * free_clusters < 3 * dirty_clusters ||
free_clusters < (dirty_clusters + EXT4_FREECLUSTERS_WATERMARK)) { // 如果free < dirty + EXT4_FREECLUSTERS_WATERMARK[4 * (percpu_counter_batch * nr_cpu_ids)],改变da_write方式为普通write方式(前台就分配unwritten extent block)
return 1;
}
return 0;
方式1主要是通过判断当前free减去dirty达不到阈值(阈值不小)时,触发回写更新准确的free,如果free确实不足改变da_write为write,前台分配块。
- ext4_da_write_begin -> ext4_da_get_block_prep -> ext4_da_map_blocks -> ext4_insert_delayed_block -> ext4_da_reserve_space -> ext4_claim_free_clusters -> ext4_has_free_clusters:
resv_clusters = atomic64_read(&sbi->s_resv_clusters);
if (free_clusters >= (nclusters + dirty_clusters + resv_clusters)) // ext4有预留块,不会让前台把所有的free计数消耗掉
return 1;
方式2通过预留一定数量块,保证文件系统空闲块不会被理论耗尽。
但是,在并发写多个不同文件的场景时上述条件无法保证计数预留的块 > 实际分配的块,流程如下:
- 消耗文件系统free_clusters,直到free_clusters > 2 * sbi->s_resv_clusters,并且free_clusters > EXT4_FREECLUSTERS_WATERMARK
- 重新挂载,dirty_clusters = 0
- 并发起free block - sbi->s_resv_clusters个任务,每个任务写不同的文件
由于任务对不同文件操作,可以并发进入ext4_nonda_switch,因此所有任务都是da_write
- 每个任务执行ext4_da_get_block_prep更新percpu_counter_add(&sbi->s_dirtyclusters_counter, 1),此时dirty_clusters > free_clusters
- 删除文件,释放新的块,free_clusters > 0
- ext4_writepages
mpage_map_and_submit_extent
mpage_map_one_extent // ret = ENOSPC
ext4_map_blocks -> ext4_ext_map_blocks -> ext4_mb_new_blocks -> ext4_claim_free_clusters :
if (free_clusters >= (nclusters + dirty_clusters)) // false,因为每个文件写触发了新的extent申请,开始预留的计数1 < 实际申请的块(数据块+extent块)
if (err == -ENOSPC && ext4_count_free_clusters(sb)) // true
// 陷入while (!mpd.scanned_until_end && wbc->nr_to_write > 0)循环