f2fs文件系统 CP介绍

前言

在前面的介绍中,f2fs保证一致性有两种方法:前滚恢复和后滚恢复。前滚恢复需要配合fsync流程使用,先不进行介绍。而后滚恢复就是本章节需要介绍的CheckPoint相关内容。所谓的文件系统一致性,可以简单类比交易操作,即交易双方中一方资金减少,另一方资金增加,任何时候(即使系统崩溃)都不应该出现一方资金减少,另一方资金没有增加,或者反过来,一方资金没有减少,另一方资金却增加了。这些都是违背了一致性。交易操作中常使用原子事务原理,只有最终提交交易完成操作,才算最终完成交易。如果没有提交交易完成操作,那么这笔交易可以被撤销。

CP介绍

CP是Checkpoint的缩写。上面将CP原理和交易流程做了类比,如果具体来说CP功能就是:将当前内存缓存的管理数据,写入到存储介质,一旦发生掉电或者宕机等事件,还可以恢复到上一次CP的状态。当然,还没有来得及做CP的修改,最终会丢失,所以掌握CP的时机显得尤为重要。因为CP可能需要做大量IO操作,非常耗时,但是不做CP的话,刚修改的数据可能会丢失,所以需要平衡好两者。

CP数据结构

盘上布局

struct f2fs_checkpoint {
    __le64 checkpoint_ver;      /* checkpoint block version number */
    __le64 user_block_count;    /* # of user blocks */
    __le64 valid_block_count;   /* # of valid blocks in main area */
    __le32 rsvd_segment_count;  /* # of reserved segments for gc */
    __le32 free_segment_count;  /* # of free segments in main area */
​
    /* information of current node segments */
    __le32 cur_node_segno[MAX_ACTIVE_NODE_LOGS];
    __le16 cur_node_blkoff[MAX_ACTIVE_NODE_LOGS];
    /* information of current data segments */
    __le32 cur_data_segno[MAX_ACTIVE_DATA_LOGS];
    __le16 cur_data_blkoff[MAX_ACTIVE_DATA_LOGS];
    __le32 ckpt_flags;      /* Flags : umount and journal_present */
    __le32 cp_pack_total_block_count;   /* total # of one cp pack */
    __le32 cp_pack_start_sum;   /* start block number of data summary */
    __le32 valid_node_count;    /* Total number of valid nodes */
    __le32 valid_inode_count;   /* Total number of valid inodes */
    __le32 next_free_nid;       /* Next free node number */
    __le32 checksum_offset;     /* checksum offset inside cp block */
    __le64 elapsed_time;        /* mounted time */
    /* allocation type of current segment */
    unsigned char alloc_type[MAX_ACTIVE_LOGS];
​
    /* SIT and NAT version bitmap */
    unsigned char sit_nat_version_bitmap[];
} __packed;

一个f2fs_checkpoint代表CP pack,其作用是记录本次CP的基本信息,包括版本号、sit和nat的bitmap使用情况、block的使用情况等等。

  • checkpoint_ver:本次CP的版本号。每次写CP时,都会将版本号递增,版本号越大,表示数据越新。在恢复时,优先选择版本号大的CP。
  • user_block_count:当前用户已经使用的block数量。
  • valid_block_count:Main Area区域已经使用的block数量。
  • rsvd_segment_count:为GC流程保留的segment数量。
  • free_segment_count:Main Area区域空闲的segment数量。
  • cur_node_segno:当前正在使用的node segment id号,对应curseg的数据。
  • cur_node_blkoff:当前正在使用node segment的下一个待使用的node id。
  • cur_data_segno:当前正在使用的data segment id号,对应curseg的数据。
  • cur_data_blkoff:当前正在使用data segment的下一个待使用的node id。
  • ckpt_flags:本次CP的标记,例如umount、resize、fsck等。
  • cp_pack_total_block_count:本CP pack所占用的block数,每次CP的长度不固定。
  • cp_pack_start_sum:本CP中,summary信息的开始位置。
  • valid_node_count:本次CP中,已经使用的node数量。
  • valid_inode_count:本次CP中,已经使用的inode数量。
  • next_free_nid:下一个待使用的free node,对应NAT的管理。
  • checksum_offset:本CP的checksum偏移位置。
  • elapsed_time:系统挂载的时间戳。
  • alloc_type:记录各个curseg对应的分配器类型,最多能记录16个。
  • sit_nat_version_bitmap:记录SIT和NAT的bitmap,用于判断本次使用的时主区还是备区。

以上就是一个CP pack的全部内容,这只是记录了内存中的一些基础信息,curseg中的内容则附加在CP pack后。

CP的管理

写CP全流程

如下是CP的全流程图,下面将对照着代码逐一展开。

f2fs写CP全流程

f2fs_write_checkpoint

int f2fs_write_checkpoint(struct f2fs_sb_info *sbi, struct cp_control *cpc)
{
    ...
​
    // 阻塞文件系统写操作
    err = block_operations(sbi);
​
    // 提交sbi中缓存的bio(可能经过合并了的)
    f2fs_flush_merged_writes(sbi);
​
    // 获取当前的CP版本号
    ckpt_ver = cur_cp_version(ckpt);
    // CP版本号递增
    ckpt->checkpoint_ver = cpu_to_le64(++ckpt_ver);
​
    /* write cached NAT/SIT entries to NAT/SIT area */
    // 下刷journal中的nat entry cache
    err = f2fs_flush_nat_entries(sbi, cpc);
​
    // 下刷journal中的sit entry cache
    f2fs_flush_sit_entries(sbi, cpc);
​
    /* save inmem log status */
    // 将pinned文件sum信息下刷
    f2fs_save_inmem_curseg(sbi);
​
    // 写CP
    err = do_checkpoint(sbi, cpc);
​
    // TODO:恢复pinned文件的sum信息?
    f2fs_restore_inmem_curseg(sbi);
}

这是写CP全流程的操作,对照流程图可以看出有如下步骤:

  1. 阻塞系统写操作,将脏数据落盘,包括脏inline数据、脏dentry、脏node数据等等,这些数据会影响系统的一致性。
  2. 将sbi中缓存的bio下发。
  3. 将当前CP版本号递增,作为本次CP的版本号。版本号可以检验本次CP是否可用,以及哪一份数据是最新的。
  4. 将对NAT和SIT的修改下刷。
  5. 完成前面的脏数据处理后,开始真正写CP区域,也就是说提交一次“交易”完成的记录。

block_operations

/*
 * Freeze all the FS-operations for checkpoint.
 */
static int block_operations(struct f2fs_sb_info *sbi)
{
    /*
     * Let's flush inline_data in dirty node pages.
     */
    // 将inline data回写到inode page中
    f2fs_flush_inline_data(sbi);
​
    /* write all the dirty dentry pages */
    // 下刷所有脏dentry页,fsync并没有处理dir的data数据,只下刷了文件的data
    // 数据,在此处将dir的data数据下刷
    if (get_pages(sbi, F2FS_DIRTY_DENTS)) {
        f2fs_unlock_all(sbi);
        err = f2fs_sync_dirty_inodes(sbi, DIR_INODE, true);
    }
​
    /*
     * POR: we should ensure that there are no dirty node pages
     * until finishing nat/sit flush. inode->i_blocks can be updated.
     */
    f2fs_down_write(&sbi->node_change);
​
    // 将管理元数据下刷(不属于文件、目录、链接的inode,应该就是元数据inode,例如:
    // acl、attr等等)
    if (get_pages(sbi, F2FS_DIRTY_IMETA)) {
        f2fs_up_write(&sbi->node_change);
        f2fs_unlock_all(sbi);
        err = f2fs_sync_inode_meta(sbi);
    }
​
    f2fs_down_write(&sbi->node_write);
​
    // fsync流程只下刷了普通文件的node page,配合前滚恢复可以保证一致性;
    // 在CP流程中,将其他类型的noage page下刷,配合后滚恢复保证一致性
    if (get_pages(sbi, F2FS_DIRTY_NODES)) {
        f2fs_up_write(&sbi->node_write);
        atomic_inc(&sbi->wb_sync_req[NODE]);
        err = f2fs_sync_node_pages(sbi, &wbc, false, FS_CP_NODE_IO);
        atomic_dec(&sbi->wb_sync_req[NODE]);
    }
    ...
}

在写CP的时候,会冻结所有文件系统操作,这也是为啥CP流程开销很大的原因,需要尽可能减少CP次数。在这个流程有如下步骤:

  1. 遍历所有脏direct node(主要是为了找到inode page),如果该node存在inline data,则将page cache(已经被修改的inline data)数据回写到inode page,等待后续inode page落盘。
    注:f2fs使用inline data来优化空间,一个inode page占4KB大小,但是元数据只占用一小部分空间。如果data数据小于一定范围,则存放在inline区域,省去分配一个data block的开销。同时由于inline区域的存在,所以在写数据之前,需要将inline data转为0号page cache,等待需要回写时,在将其写入inode page中,如果超过inline data范围,则不再回写。
  2. 通过遍历inode_list链表,将脏dirty dentry页下刷。
  3. 将脏元数据页下刷。
  4. 将脏node页下刷。

以上流程是将内存中缓存的脏页下刷,这是在写CP区域之前需要完成的动作。

f2fs_flush_nat_entries

int f2fs_flush_nat_entries(struct f2fs_sb_info *sbi, struct cp_control *cpc)
{
    ...
​
    /*
     * if there are no enough space in journal to store dirty nat
     * entries, remove all entries from journal and merge them
     * into nat entry set.
     */
    // 如果是umount流程,就将journal下刷(但前面已经下刷过了?)
    // 如果当前journal没有空间,也会释放journal的空间。
    if (cpc->reason & CP_UMOUNT ||
        !__has_cursum_space(journal,
            nm_i->nat_cnt[DIRTY_NAT], NAT_JOURNAL))
        remove_nats_in_journal(sbi);
​
    // 遍历nat_set_root,将set中所有nat_entry下刷,这里需要考虑net_entry的合并
    while ((found = __gang_lookup_nat_set(nm_i,
                    set_idx, SETVEC_SIZE, setvec))) {
        unsigned idx;
​
        set_idx = setvec[found - 1]->set + 1;
        for (idx = 0; idx < found; idx++)
            __adjust_nat_entry_set(setvec[idx], &sets,
                        MAX_NAT_JENTRIES(journal));
    }
​
    /* flush dirty nats in nat entry set */
    // 将nat_entry_set放到journal或者下刷到存储介质。
    list_for_each_entry_safe(set, tmp, &sets, set_list) {
        err = __flush_nat_entry_set(sbi, set, cpc);
        if (err)
            break;
    }
    ...
}

将内存中缓存的NAT表项修改下刷,而该信息是缓存在CURSEG_HOT_DATA类型的journal中。首先判断journal剩余空间是否满足缓存脏NAT表项,如果不足,则将当前journal中的表项放入到nat_entry_set中,等待下刷。如果本次是umount或者journal空间不足,则直接下刷到NAT区域中,否则存放在journal中即可。

f2fs_flush_sit_entries

void f2fs_flush_sit_entries(struct f2fs_sb_info *sbi, struct cp_control *cpc)
{
    ...
    // 如果没有dirty sentry entry,则直接跳过,不处理
    if (!sit_i->dirty_sentries)
        goto out;
​
    /*
     * add and account sit entries of dirty bitmap in sit entry
     * set temporarily
     */
    // 将dirty sentry entry放入到set中
    // 修改过的sit entry都放入到dirty_sentries_bitmap中
    add_sits_in_set(sbi);
​
    /*
     * if there are no enough space in journal to store dirty sit
     * entries, remove all entries from journal and add and account
     * them in sit entry set.
     */
    // 如果journal没有足够的空间,则将journal放到set中,并清空journal的内容
    if (!__has_cursum_space(journal, sit_i->dirty_sentries, SIT_JOURNAL) ||
                                !to_journal)
        remove_sits_in_journal(sbi);
​
    /*
     * there are two steps to flush sit entries:
     * #1, flush sit entries to journal in current cold data summary block.
     * #2, flush sit entries to sit page.
     */
    list_for_each_entry_safe(ses, tmp, head, set_list) {
        ...
        // 如果本次要写journal,但是空间不足,则直接落盘
        if (to_journal &&
            !__has_cursum_space(journal, ses->entry_cnt, SIT_JOURNAL))
            to_journal = false;
​
        /* flush dirty sit entries in region of current sit set */
        // 遍历bitmap,获取脏sit entry,并写入journal或者存储介质
        for_each_set_bit_from(segno, bitmap, end) {
            int offset, sit_offset;
​
            se = get_seg_entry(sbi, segno);
​
            /* add discard candidates */
            if (!(cpc->reason & CP_DISCARD)) {
                cpc->trim_start = segno;
                add_discard_addrs(sbi, cpc, false);
            }
​
            if (to_journal) {
                // 找到可以写本次journal的偏移,如果journal中有相应的记录,
                // 修改对应的位置
                offset = f2fs_lookup_journal_in_cursum(journal,
                            SIT_JOURNAL, segno, 1);
                segno_in_journal(journal, offset) =
                            cpu_to_le32(segno);
                seg_info_to_raw_sit(se,
                    &sit_in_journal(journal, offset));
            } else {
                // 直接写到sit表中(前面获取sit页的时候已经写过了???)
                sit_offset = SIT_ENTRY_OFFSET(sit_i, segno);
                seg_info_to_raw_sit(se,
                        &raw_sit->entries[sit_offset]);
            }
        }
​
    }
    ...
}
​

SIT缓存的下刷与NAT类似,将脏的SIT表项放入到sit_entry_set集合中,遍历该集合,如果journal中有空间,则存放在journal中,否则直接下刷到SIT区域。不同的是,SIT对应的是CURSEG_COLD_DATA类型的journal。

do_checkpoint

static int do_checkpoint(struct f2fs_sb_info *sbi, struct cp_control *cpc)
{
    ...
    /* Flush all the NAT/SIT pages */
    f2fs_sync_meta_pages(sbi, META, LONG_MAX, FS_CP_META_IO);
    ...
    start_blk = __start_cp_next_addr(sbi);
​
    /* write nat bits */
    if ((cpc->reason & CP_UMOUNT) &&
            is_set_ckpt_flags(sbi, CP_NAT_BITS_FLAG)) {
        __u64 cp_ver = cur_cp_version(ckpt);
        block_t blk;
​
        cp_ver |= ((__u64)crc32 << 32);
        *(__le64 *)nm_i->nat_bits = cpu_to_le64(cp_ver);
​
        blk = start_blk + sbi->blocks_per_seg - nm_i->nat_bits_blocks;
        for (i = 0; i < nm_i->nat_bits_blocks; i++)
            f2fs_update_meta_page(sbi, nm_i->nat_bits +
                    (i << F2FS_BLKSIZE_BITS), blk + i);
    }
​
    /* write out checkpoint buffer at block 0 */
    f2fs_update_meta_page(sbi, ckpt, start_blk++);
​
    for (i = 1; i < 1 + cp_payload_blks; i++)
        f2fs_update_meta_page(sbi, (char *)ckpt + i * F2FS_BLKSIZE,
                            start_blk++);
​
    if (orphan_num) {
        write_orphan_inodes(sbi, start_blk);
        start_blk += orphan_blocks;
    }
​
    f2fs_write_data_summaries(sbi, start_blk);
    start_blk += data_sum_blocks;
​
    ...
​
    /* flush all device cache */
    err = f2fs_flush_device_cache(sbi);
    if (err)
        return err;
​
    /* barrier and flush checkpoint cp pack 2 page if it can */
    commit_checkpoint(sbi, ckpt, start_blk);
    ...
}

该接口是才是最终写CP区域的,其可以总结为如下步骤:

  1. 等待NAT/SIT的修改落盘。
  2. 将文件系统状态信息填入CP pack #1中,包括各个curseg的segno号、next_blkoff、分配策略、SIT/NAT bitmap等等,最终计算CP pack #1的checksum。
  3. 计算CP的起始地址,由于CP区域存在主备区,采用“乒乓”策略写入。
  4. 如果支持NAT_BITS,将nat_bits写入CP区域的结尾处。
  5. 在CP区域开头处写入CP pack #1的block,然后写入填充内容(cp_payload)。
  6. 如果存在“孤儿”节点,需要在其后写入孤儿节点的内容。
  7. 写入data的summary,如果允许压缩(只写有实际内容的SSA表项),还需要将压缩后的数据写入。
  8. 写入node的summary,不支持压缩。
  9. 等待上述的数据真正落盘后,才最终写入CP pack #2。(注:如果不等待,可能出现CP pack #1和#2都已经落盘,但其他元数据还没有落盘,这是后发生宕机,那么其他数据是受损的。在写入CP pack #2前等待,可以保证所有元数据已经落盘,即使宕机,由于CP pack #2还没写入,最终该CP会被丢弃。)、

以上是一次写CP的完整流程,CP区域的具体内容参考如下:

f2fs的CP布局

如上就是一个CP的具体布局,分为主备两个segment。每次轮流使用主备区域,确保总有一个区域的内容是可用的。

总结

本章节介绍了f2fs一致性中的后滚恢复部分,虽然只是介绍了写CP的流程,但是读操作只是写操作的反向流程,就不过多赘述。

猜你喜欢

转载自blog.csdn.net/a13821684483/article/details/132076945