├── F2FS-Data-Recovery ├── Checkpoint流程.md ├── Recovery的流程.md └── 数据恢复的原理以及方式.md ├── F2FS-Experiment └── 实验环境搭建.md ├── F2FS-GC ├── GC流程介绍.md └── 选择victim segment.md ├── F2FS-Layout ├── Checkpoint结构.md ├── Node Address Table结构.md ├── Segment Infomation Table结构.md ├── Segment Summary Area结构.md ├── Superblock结构.md └── 总体结构.md ├── File-Creation-and-Deletion ├── 文件创建.md ├── 文件删除.md ├── 目录创建.md └── 目录删除.md ├── ImportantDataStructure ├── curseg.md ├── f2fs_do_fsync_file.md ├── f2fs_fill_super.md ├── f2fs_journal.md ├── f2fs_map_blocks.md ├── f2fs_rename.md ├── f2fs_summary.md ├── get_dnode.md ├── node_footer.md └── segment.md ├── Outline.md ├── README.md ├── Reading-and-Writing ├── file_data_structure.md ├── 写流程.md └── 读流程.md └── img └── F2FS-Layout ├── cp_layout.png ├── cp_layout2.png ├── f2fs-layout.png ├── inode-blockmap.png ├── nat_layout.png ├── node-table.png ├── sb_layout.png ├── sb_layout2.png ├── sit_layout.png ├── sit_layout2.png ├── ssa_layout.png └── ssa_layout2.png /F2FS-Data-Recovery/Checkpoint流程.md: -------------------------------------------------------------------------------- 1 | ## Checkpoint的作用与实现 2 | 后滚恢复即恢复到上一个Checkpoint点的元数据状态,因此F2FS需要在特定的时刻将Checkpoint的数据写入到磁盘中。 3 | 4 | ### Checkpoint的时机 5 | CP是一个开销很大的操作,因此合理选取CP时机,能够很好地提高性能。CP的触发时机有: 6 | >前台GC(FG_GC) 7 | >FASTBOOT 8 | >UMOUNT 9 | >DISCARD 10 | >RECOVERY 11 | >TRIM 12 | >周期进行 13 | 14 | 因此F2FS有几个宏表示CP的触发原因: 15 | 16 | ```c 17 | #define CP_UMOUNT 0x00000001 18 | #define CP_FASTBOOT 0x00000002 19 | #define CP_SYNC 0x00000004 20 | #define CP_RECOVERY 0x00000008 21 | #define CP_DISCARD 0x00000010 22 | #define CP_TRIMMED 0x00000020 23 | ``` 24 | 25 | 大部分情况下,都是触发 `CP_SYNC` 这个宏的CP。 26 | 27 | ### Checkpoint的核心流程 28 | Checkpoint的入口函数以及核心数据结构 29 | ```c 30 | struct cp_control { 31 | int reason; /* Checkpoint的原因,大部分情况下是CP_SYNC */ 32 | __u64 trim_start; /* CP处理后的数据的block起始地址 */ 33 | __u64 trim_end; /* CP处理后的数据的block结束地址 */ 34 | __u64 trim_minlen; /* CP处理后的最小长度 */ 35 | }; 36 | 37 | int f2fs_write_checkpoint(struct f2fs_sb_info *sbi, struct cp_control *cpc) 38 | ``` 39 | 下面分段分析 `CP_SYNC` 原因的 `f2fs_write_checkpoint` 函数的核心流程。 40 | 41 | ```c 42 | f2fs_write_checkpoint(struct f2fs_sb_info *sbi, struct cp_control *cpc) 43 | { 44 | struct f2fs_checkpoint *ckpt = F2FS_CKPT(sbi); //从sbi读取当前CP的数据结构 45 | ... 46 | err = block_operations(sbi); //将文件系统的所有操作都停止 47 | ... 48 | f2fs_flush_merged_writes(sbi); // 将暂存的所有BIO刷写到磁盘 49 | ... 50 | ckpt_ver = cur_cp_version(ckpt); // 获取当前CP的version 51 | ckpt->checkpoint_ver = cpu_to_le64(++ckpt_ver); // 给当前CP version加1 52 | 53 | // 更新元数据的NAT区域 54 | f2fs_flush_nat_entries(sbi, cpc); // 刷写所有nat entries到磁盘 55 | 56 | // 更新元数据的SIT区域 57 | f2fs_flush_sit_entries(sbi, cpc); // 刷写所有sit entries到磁盘,处理dirty prefree segments 58 | 59 | // 更新元数据的Checkpoint区域以及Summary区域 60 | err = do_checkpoint(sbi, cpc); // checkpoint核心流程 61 | 62 | 63 | f2fs_clear_prefree_segments(sbi, cpc); // 清除dirty prefree segments的dirty标记 64 | unblock_operations(sbi); //恢复文件系统的操作 65 | ... 66 | f2fs_update_time(sbi, CP_TIME); // 更新CP的时间 67 | ... 68 | } 69 | ``` 70 | ### Checkpoint涉及的子函数的分析 71 | 72 | #### 暂存BIO的回写 73 | 一般情况下,文件系统与设备的交互的开销是比较大的,因此一些文件系统为了减少交互的开销,都会尽可能将更多的page合并在一个bio中,再提交到设备,进而减少交互的次数。F2FS中,在sbi中使用了`struct f2fs_bio_info`结构用于减少交互次数,它的核心是缓存一个bio,将即将回写的page都保存到这个bio中,等到bio尽可能满再回写进入磁盘。它在sbi的声明如下: 74 | ```c 75 | struct f2fs_sb_info { 76 | ... 77 | struct f2fs_bio_info *write_io[NR_PAGE_TYPE]; // NR_PAGE_TYPE表示HOW/WARM/COLD不同类型的数据 78 | ... 79 | } 80 | ``` 81 | 在Checkpoint流程中,必须要回写暂存的page,以获得系统最新的稳定状态信息,它调用了函数是 `f2fs_flush_merged_writes`。`f2fs_flush_merged_writes` 函数调用了`f2fs_submit_merged_write`分别回写了DATA、NODE、META的信息。然后会调用`__submit_merged_write_cond`函数,这个函数会遍历HOW/WARM/COLD对应的`sbi->write_io`进行回写,最后调用`__submit_merged_bio`函数,从`sbi->write_io`得到bio,submit到设备中。 82 | ```c 83 | void f2fs_flush_merged_writes(struct f2fs_sb_info *sbi) 84 | { 85 | f2fs_submit_merged_write(sbi, DATA); 86 | f2fs_submit_merged_write(sbi, NODE); 87 | f2fs_submit_merged_write(sbi, META); 88 | } 89 | 90 | void f2fs_submit_merged_write(struct f2fs_sb_info *sbi, enum page_type type) 91 | { 92 | __submit_merged_write_cond(sbi, NULL, 0, 0, type, true); 93 | } 94 | 95 | static void __submit_merged_write_cond(struct f2fs_sb_info *sbi, 96 | struct inode *inode, nid_t ino, pgoff_t idx, 97 | enum page_type type, bool force) 98 | { 99 | enum temp_type temp; 100 | 101 | if (!force && !has_merged_page(sbi, inode, ino, idx, type)) 102 | return; 103 | 104 | for (temp = HOT; temp < NR_TEMP_TYPE; temp++) { // 遍历不同的HOT/WARM/COLD类型就行回写 105 | 106 | __f2fs_submit_merged_write(sbi, type, temp); 107 | 108 | /* TODO: use HOT temp only for meta pages now. */ 109 | if (type >= META) 110 | break; 111 | } 112 | } 113 | 114 | static void __f2fs_submit_merged_write(struct f2fs_sb_info *sbi, 115 | enum page_type type, enum temp_type temp) 116 | { 117 | enum page_type btype = PAGE_TYPE_OF_BIO(type); 118 | struct f2fs_bio_info *io = sbi->write_io[btype] + temp; // temp可以计算属于HOT/WARM/COLD对应的sbi->write_io 119 | 120 | down_write(&io->io_rwsem); 121 | 122 | /* change META to META_FLUSH in the checkpoint procedure */ 123 | if (type >= META_FLUSH) { 124 | io->fio.type = META_FLUSH; 125 | io->fio.op = REQ_OP_WRITE; 126 | io->fio.op_flags = REQ_META | REQ_PRIO | REQ_SYNC; 127 | if (!test_opt(sbi, NOBARRIER)) 128 | io->fio.op_flags |= REQ_PREFLUSH | REQ_FUA; 129 | } 130 | __submit_merged_bio(io); 131 | up_write(&io->io_rwsem); 132 | } 133 | 134 | static void __submit_merged_bio(struct f2fs_bio_info *io) 135 | { 136 | struct f2fs_io_info *fio = &io->fio; 137 | 138 | if (!io->bio) 139 | return; 140 | 141 | bio_set_op_attrs(io->bio, fio->op, fio->op_flags); 142 | 143 | if (is_read_io(fio->op)) 144 | trace_f2fs_prepare_read_bio(io->sbi->sb, fio->type, io->bio); 145 | else 146 | trace_f2fs_prepare_write_bio(io->sbi->sb, fio->type, io->bio); 147 | 148 | __submit_bio(io->sbi, io->bio, fio->type); // 从f2fs_io_info得到bio,提交到设备 149 | io->bio = NULL; 150 | } 151 | ``` 152 | 153 | 154 | #### NAT区域的脏数据回写 155 | `f2fs_flush_nat_entries` 和 `f2fs_flush_sit_entries` 的作用是将暂存在ram的nat entry合sit entry都回写到Journal或磁盘当中: 156 | ##### f2fs_flush_nat_entries函数 157 | 修改node的信息会对对应的`nat_entry`进行修改,同时`nat_entry`会被设置为脏,加入到`nm_i->nat_set_root`的radix tree中。Checkpoint会对脏的`nat_entry`进行回写,完成元数据的更新。 158 | 159 | 首先声明了一个list变量`LIST_HEAD(sets)`,然后通过一个while循环,将`nat_entry_set`按一个set为单位,对脏的`nat_entry`进行提取,每次提取SETVEC_SIZE个,然后保存到`setvec[SETVEC_SIZE]`中,然后对`setvec`中的每一个`nat_entry_set`,按照一定条件加入到`LIST_HEAD(sets)`的链表中。最后针对`LIST_HEAD(sets)`的`nat_entry_set`,执行`__flush_nat_entry_set`函数,对脏数据进行回写。`__flush_nat_entry_set`有两种回写方法,第一种是写入到curseg的journal中,第二种是直接找到对应的nat block,回写到磁盘中。 160 | ```c 161 | void f2fs_flush_nat_entries(struct f2fs_sb_info *sbi, struct cp_control *cpc) 162 | { 163 | struct f2fs_nm_info *nm_i = NM_I(sbi); 164 | struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_HOT_DATA); 165 | struct f2fs_journal *journal = curseg->journal; 166 | struct nat_entry_set *setvec[SETVEC_SIZE]; 167 | struct nat_entry_set *set, *tmp; 168 | unsigned int found; 169 | nid_t set_idx = 0; 170 | LIST_HEAD(sets); 171 | 172 | if (!nm_i->dirty_nat_cnt) 173 | return; 174 | 175 | down_write(&nm_i->nat_tree_lock); 176 | 177 | /* 178 | * __gang_lookup_nat_set 这个函数就是从radix tree读取set_idx开始, 179 | * 连续读取SETVEC_SIZE这么多个nat_entry_set,保存在setvec中 180 | * 然后按照一定条件,通过__adjust_nat_entry_set函数加入到LIST_HEAD(sets)链表中 181 | * */ 182 | while ((found = __gang_lookup_nat_set(nm_i, 183 | set_idx, SETVEC_SIZE, setvec))) { 184 | unsigned idx; 185 | set_idx = setvec[found - 1]->set + 1; 186 | for (idx = 0; idx < found; idx++) 187 | __adjust_nat_entry_set(setvec[idx], &sets, 188 | MAX_NAT_JENTRIES(journal)); 189 | } 190 | 191 | /* 192 | * flush dirty nats in nat entry set 193 | * 遍历这个list所有的nat_entry_set,然后写入到curseg->journal中 194 | * */ 195 | list_for_each_entry_safe(set, tmp, &sets, set_list) 196 | __flush_nat_entry_set(sbi, set, cpc); 197 | 198 | up_write(&nm_i->nat_tree_lock); 199 | /* Allow dirty nats by node block allocation in write_begin */ 200 | } 201 | ``` 202 | 203 | `__flush_nat_entry_set`有两种回写的方式,第一种是写入到curseg的journal中,第二种是回写到nat block中。 204 | 205 | 第一种写入方式通常是由于curseg有足够的journal的情况下的写入,首先遍历`nat_entry_set`中的所有`nat_entry`,然后根据nid找到curseg->journal中对应的nat_entry的位置,跟着将被遍历的`nat_entry`的值赋予给curseg->journal的`nat_entry`,通过`raw_nat_from_node_info`完成curseg的nat_entry的更新。 206 | 207 | 第二种写入方式在curseg没有足够的journal的时候触发,首先根据nid找到NAT区域的对应的`f2fs_nat_block`,然后通过`get_next_nat_page`读取出来,然后通过`raw_nat_from_node_info`进行更新。 208 | 209 | 210 | ```c 211 | static void __flush_nat_entry_set(struct f2fs_sb_info *sbi, 212 | struct nat_entry_set *set, struct cp_control *cpc) 213 | { 214 | struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_HOT_DATA); 215 | struct f2fs_journal *journal = curseg->journal; 216 | nid_t start_nid = set->set * NAT_ENTRY_PER_BLOCK; // 根据set number找到对应f2fs_nat_block 217 | bool to_journal = true; 218 | struct f2fs_nat_block *nat_blk; 219 | struct nat_entry *ne, *cur; 220 | struct page *page = NULL; 221 | 222 | /* 223 | * there are two steps to flush nat entries: 224 | * #1, flush nat entries to journal in current hot data summary block. 225 | * #2, flush nat entries to nat page. 226 | */ 227 | if (enabled_nat_bits(sbi, cpc) || 228 | !__has_cursum_space(journal, set->entry_cnt, NAT_JOURNAL)) //当curseg的journal空间不够了,就刷写到磁盘中 229 | to_journal = false; 230 | 231 | if (to_journal) { 232 | down_write(&curseg->journal_rwsem); 233 | } else { 234 | page = get_next_nat_page(sbi, start_nid); /* 根据nid找到管理这个nid的f2fs_nat_block */ 235 | nat_blk = page_address(page); 236 | f2fs_bug_on(sbi, !nat_blk); 237 | } 238 | 239 | /* 240 | * flush dirty nats in nat entry set 241 | * 遍历所有的nat_entry 242 | * 243 | * nat_entry只存在于内存当中,具体在磁盘保存的是f2fs_entry_block 244 | * */ 245 | list_for_each_entry_safe(ne, cur, &set->entry_list, list) { 246 | struct f2fs_nat_entry *raw_ne; 247 | nid_t nid = nat_get_nid(ne); 248 | int offset; 249 | 250 | f2fs_bug_on(sbi, nat_get_blkaddr(ne) == NEW_ADDR); 251 | 252 | if (to_journal) { 253 | // 搜索当前的journal中nid所在的位置 254 | offset = f2fs_lookup_journal_in_cursum(journal, 255 | NAT_JOURNAL, nid, 1); 256 | f2fs_bug_on(sbi, offset < 0); 257 | raw_ne = &nat_in_journal(journal, offset); // 从journal中取出f2fs_nat_entry的信息 258 | nid_in_journal(journal, offset) = cpu_to_le32(nid); // 更新journal的nid 259 | } else { 260 | raw_ne = &nat_blk->entries[nid - start_nid]; /* 拿到nid对应的nat_entry地址,下面开始填数据 */ 261 | } 262 | raw_nat_from_node_info(raw_ne, &ne->ni); // 将node info的信息更新到journal中后者磁盘中 263 | nat_reset_flag(ne); // 清除需要CP的标志 264 | __clear_nat_cache_dirty(NM_I(sbi), set, ne); // 从dirty list清除处理后的entry 265 | if (nat_get_blkaddr(ne) == NULL_ADDR) { // 如果对应nid已经是被无效化了,则释放 266 | add_free_nid(sbi, nid, false, true); 267 | } else { 268 | spin_lock(&NM_I(sbi)->nid_list_lock); 269 | update_free_nid_bitmap(sbi, nid, false, false); // 更新可用的nat的bitmap 270 | spin_unlock(&NM_I(sbi)->nid_list_lock); 271 | } 272 | } 273 | 274 | if (to_journal) { 275 | up_write(&curseg->journal_rwsem); 276 | } else { 277 | __update_nat_bits(sbi, start_nid, page); 278 | f2fs_put_page(page, 1); 279 | } 280 | 281 | /* Allow dirty nats by node block allocation in write_begin */ 282 | if (!set->entry_cnt) { 283 | radix_tree_delete(&NM_I(sbi)->nat_set_root, set->set); 284 | kmem_cache_free(nat_entry_set_slab, set); 285 | } 286 | } 287 | 288 | ``` 289 | 290 | #### SIT区域的脏数据回写 291 | ##### f2fs_flush_sit_entries函数 292 | 293 | 主要过程跟 `f2fs_flush_nat_entries` 类似,将dirty的seg_entry刷写到journal或sit block中 294 | 295 | ```c 296 | void f2fs_flush_sit_entries(struct f2fs_sb_info *sbi, struct cp_control *cpc) 297 | { 298 | struct sit_info *sit_i = SIT_I(sbi); 299 | unsigned long *bitmap = sit_i->dirty_sentries_bitmap; 300 | struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_COLD_DATA); 301 | struct f2fs_journal *journal = curseg->journal; 302 | struct sit_entry_set *ses, *tmp; 303 | struct list_head *head = &SM_I(sbi)->sit_entry_set; 304 | bool to_journal = true; 305 | struct seg_entry *se; 306 | 307 | down_write(&sit_i->sentry_lock); 308 | 309 | if (!sit_i->dirty_sentries) 310 | goto out; 311 | 312 | /* 313 | * add and account sit entries of dirty bitmap in sit entry 314 | * set temporarily 315 | * 316 | * 遍历所有dirty的segment的segno, 317 | * 找到对应的sit_entry_set,然后保存到sbi->sm_info->sit_entry_set 318 | */ 319 | add_sits_in_set(sbi); 320 | 321 | /* 322 | * if there are no enough space in journal to store dirty sit 323 | * entries, remove all entries from journal and add and account 324 | * them in sit entry set. 325 | */ 326 | if (!__has_cursum_space(journal, sit_i->dirty_sentries, SIT_JOURNAL)) 327 | remove_sits_in_journal(sbi); 328 | 329 | /* 330 | * there are two steps to flush sit entries: 331 | * #1, flush sit entries to journal in current cold data summary block. 332 | * #2, flush sit entries to sit page. 333 | * 遍历list中的所有segno对应的sit_entry_set 334 | */ 335 | list_for_each_entry_safe(ses, tmp, head, set_list) { 336 | struct page *page = NULL; 337 | struct f2fs_sit_block *raw_sit = NULL; 338 | unsigned int start_segno = ses->start_segno; 339 | unsigned int end = min(start_segno + SIT_ENTRY_PER_BLOCK, 340 | (unsigned long)MAIN_SEGS(sbi)); 341 | unsigned int segno = start_segno; /* 找到 */ 342 | 343 | if (to_journal && 344 | !__has_cursum_space(journal, ses->entry_cnt, SIT_JOURNAL)) 345 | to_journal = false; 346 | 347 | if (to_journal) { 348 | down_write(&curseg->journal_rwsem); 349 | } else { 350 | page = get_next_sit_page(sbi, start_segno); /* 访问磁盘,从磁盘获取到f2fs_sit_block */ 351 | raw_sit = page_address(page); /* 根据segno获得f2fs_sit_block,然后下一步将数据写入这个block当中 */ 352 | } 353 | 354 | /* 355 | * flush dirty sit entries in region of current sit set 356 | * 遍历segno~end所有dirty的seg_entry 357 | * */ 358 | for_each_set_bit_from(segno, bitmap, end) { 359 | int offset, sit_offset; 360 | 361 | se = get_seg_entry(sbi, segno); /* 根据segno从SIT缓存中获取到seg_entry,这个缓存是F2FS初始化的时候,将全部seg_entry读入创建的 */ 362 | 363 | if (to_journal) { 364 | offset = f2fs_lookup_journal_in_cursum(journal, 365 | SIT_JOURNAL, segno, 1); 366 | f2fs_bug_on(sbi, offset < 0); 367 | segno_in_journal(journal, offset) = 368 | cpu_to_le32(segno); 369 | seg_info_to_raw_sit(se, 370 | &sit_in_journal(journal, offset)); // 更新journal的数据 371 | check_block_count(sbi, segno, 372 | &sit_in_journal(journal, offset)); 373 | } else { 374 | sit_offset = SIT_ENTRY_OFFSET(sit_i, segno); 375 | seg_info_to_raw_sit(se, 376 | &raw_sit->entries[sit_offset]); // 更新f2fs_sit_block的数据 377 | check_block_count(sbi, segno, 378 | &raw_sit->entries[sit_offset]); 379 | } 380 | 381 | __clear_bit(segno, bitmap); // 从dirty map中除名 382 | sit_i->dirty_sentries--; 383 | ses->entry_cnt--; 384 | } 385 | 386 | if (to_journal) 387 | up_write(&curseg->journal_rwsem); 388 | else 389 | f2fs_put_page(page, 1); 390 | 391 | f2fs_bug_on(sbi, ses->entry_cnt); 392 | release_sit_entry_set(ses); 393 | } 394 | 395 | f2fs_bug_on(sbi, !list_empty(head)); 396 | f2fs_bug_on(sbi, sit_i->dirty_sentries); 397 | out: 398 | up_write(&sit_i->sentry_lock); 399 | 400 | /* 401 | * 通过CP的时机,将暂存在dirty_segmap的dirty的segment信息,更新到free_segmap中 402 | * 而且与接下来的do_checkpoint完成的f2fs_clear_prefree_segments有关系,因为 这里处理完了 403 | * dirty prefree segments,所以在f2fs_clear_prefree_segments这个函数将它的dirty标记清除 404 | * */ 405 | set_prefree_as_free_segments(sbi); 406 | } 407 | 408 | static inline struct seg_entry *get_seg_entry(struct f2fs_sb_info *sbi, 409 | unsigned int segno) 410 | { 411 | struct sit_info *sit_i = SIT_I(sbi); 412 | return &sit_i->sentries[segno]; 413 | } 414 | 415 | static void set_prefree_as_free_segments(struct f2fs_sb_info *sbi) 416 | { 417 | struct dirty_seglist_info *dirty_i = DIRTY_I(sbi); 418 | unsigned int segno; 419 | 420 | mutex_lock(&dirty_i->seglist_lock); 421 | /* 422 | * 遍历dirty_seglist_info->dirty_segmap[PRE],然后执行__set_test_and_free 423 | * */ 424 | for_each_set_bit(segno, dirty_i->dirty_segmap[PRE], MAIN_SEGS(sbi)) 425 | __set_test_and_free(sbi, segno); /* 根据segno更新free_segmap的可用信息 */ 426 | mutex_unlock(&dirty_i->seglist_lock); 427 | } 428 | 429 | static inline void __set_test_and_free(struct f2fs_sb_info *sbi, 430 | unsigned int segno) 431 | { 432 | struct free_segmap_info *free_i = FREE_I(sbi); 433 | unsigned int secno = GET_SEC_FROM_SEG(sbi, segno); 434 | unsigned int start_segno = GET_SEG_FROM_SEC(sbi, secno); 435 | unsigned int next; 436 | 437 | spin_lock(&free_i->segmap_lock); 438 | /* 439 | * free_i->free_segmap用这个bitmap表示这个segment是否是dirty 440 | * 如果这个segno对应的segment位置等于0,代表不是dirty,不作处理 441 | * 如果这个segno对应的位置等于1,表示这个segment是dirty的,那么在当前的free_segment+1,更新最新的free_segment信息 442 | * */ 443 | if (test_and_clear_bit(segno, free_i->free_segmap)) { 444 | free_i->free_segments++; 445 | 446 | next = find_next_bit(free_i->free_segmap, 447 | start_segno + sbi->segs_per_sec, start_segno); 448 | if (next >= start_segno + sbi->segs_per_sec) { 449 | if (test_and_clear_bit(secno, free_i->free_secmap)) 450 | free_i->free_sections++; 451 | } 452 | } 453 | spin_unlock(&free_i->segmap_lock); 454 | } 455 | 456 | ``` 457 | 458 | #### Checkpoint区域的回写 459 | 上述分别描述了对NAT和SIT的回写与更新,而`do_checkpoint`是针对Checkpoint区域的更新。Checkpoint主要涉及两部分,第一部分`f2fs_checkpoint`结构的更新,第二部分是curseg的summary数据的回写。在分析这个函数之前,需要知道元数据的Checkpoint区域在磁盘中是如何保存的,磁盘的保存结构如下: 460 | ``` 461 | +---------------------------------------------------------------------------------------------------+ 462 | | f2fs_checkpoint | data summaries | hot node summaries | warm node summaries | cold node summaries | 463 | +---------------------------------------------------------------------------------------------------+ 464 | . . 465 | . . 466 | . compacted summaries . 467 | +----------------+-------------------+----------------+ 468 | | nat journal | sit journal | data summaries | 469 | +----------------+-------------------+----------------+ 470 | 471 | . normal summaries . 472 | +----------------+-------------------+----------------+ 473 | | data summaries | 474 | +----------------+-------------------+----------------+ 475 | ``` 476 | 其中f2fs_checkpoint、hot/warm/cold node summaries都分别占用一个block的空间。f2fs为了减少Checkpoint的写入开销,将data summaries被设计为可变的。它包含两种写入方式,一种是compacted summaries写入,另一种是normal summaries写入。compacted summaries可以在一次Checkpoint中,减少1~2个page的写入。 477 | 478 | 479 | ##### do_checkpoint函数 480 | 下面是简化的`do_checkpoint`函数核心流程,如下所示: 481 | ```c 482 | static int do_checkpoint(struct f2fs_sb_info *sbi, struct cp_control *cpc) 483 | { 484 | // 第一部分,根据curseg,修改f2fs_checkpoint结构的信息 485 | ... 486 | for (i = 0; i < NR_CURSEG_NODE_TYPE; i++) { 487 | ckpt->cur_node_segno[i] = 488 | cpu_to_le32(curseg_segno(sbi, i + CURSEG_HOT_NODE)); 489 | ckpt->cur_node_blkoff[i] = 490 | cpu_to_le16(curseg_blkoff(sbi, i + CURSEG_HOT_NODE)); 491 | ckpt->alloc_type[i + CURSEG_HOT_NODE] = 492 | curseg_alloc_type(sbi, i + CURSEG_HOT_NODE); 493 | } 494 | //printk("[do-checkpoint] point 3\n"); 495 | for (i = 0; i < NR_CURSEG_DATA_TYPE; i++) { 496 | ckpt->cur_data_segno[i] = 497 | cpu_to_le32(curseg_segno(sbi, i + CURSEG_HOT_DATA)); 498 | ckpt->cur_data_blkoff[i] = 499 | cpu_to_le16(curseg_blkoff(sbi, i + CURSEG_HOT_DATA)); 500 | ckpt->alloc_type[i + CURSEG_HOT_DATA] = 501 | curseg_alloc_type(sbi, i + CURSEG_HOT_DATA); 502 | } 503 | 504 | // 第二部分,根据curseg,修改summary的信息 505 | ... 506 | 507 | data_sum_blocks = f2fs_npages_for_summary_flush(sbi, false); 508 | 509 | if (data_sum_blocks < NR_CURSEG_DATA_TYPE) 510 | __set_ckpt_flags(ckpt, CP_COMPACT_SUM_FLAG); 511 | else 512 | __clear_ckpt_flags(ckpt, CP_COMPACT_SUM_FLAG); 513 | 514 | f2fs_write_data_summaries(sbi, start_blk); // 将data summary以及里面的journal写入磁盘 515 | 516 | /* 517 | * node summaries的写回只有在启动和关闭F2FS的时候才会执行, 518 | * 如果出现的宕机的情况下,就会失去了UMOUNT的标志,也会失去了所有的NODE SUMMARY 519 | * F2FS会进行根据上次checkpoint的情况进行恢复 520 | */ 521 | if (__remain_node_summaries(cpc->reason)) { 522 | f2fs_write_node_summaries(sbi, start_blk); // 将node summary以及里面的journal写入磁盘 523 | start_blk += NR_CURSEG_NODE_TYPE; 524 | } 525 | 526 | commit_checkpoint(sbi, ckpt, start_blk); // 将修改后的checkpoint区域的数据提交到设备,对磁盘的元数据进行更新 527 | 528 | ... 529 | 530 | return 0; 531 | } 532 | ``` 533 | 534 | 首先,第一部分主要是针对元数据区域的`f2fs_checkpoint`结构的修改,其实包括将curseg的当前segno,blkoff等写入到`f2fs_checkpoint`中,以便下次重启时可以根据这些信息,重建curseg。 535 | 536 | 接下来重点讨论,Checkpoint区域的summary的回写,在分析流程之前,需要分析compacted summaries和normal summaries的差别。 537 | 538 | **compacted summaries和normal summaries** 539 | 通过查看curseg的结构可以知道,curseg管理了(NODE,DATA) X (HOT,WARM,COLD)总共6个的segment,因此也需要管理这6个segment对应的`f2fs_summary_block`。 540 | 541 | 因此一般情况下,每一次checkpoint时候,应该需要回写6种类型的`f2fs_summary_block`,即6个block到磁盘。 542 | 543 | 为了减少这部分回写的开销,f2fs针对**DATA**类型`f2fs_summary_block`设计了一种compacted summary block。一般情况下,DATA需要回写3个`f2fs_summary_block`到磁盘(HOT,WARM,COLD),但是如果使用了compacted summary block,大部分情况下只需要回写1~2个block。 544 | 545 | compacted summary block被设计为通过1~2个block保存当前curseg所有的元信息,它的核心设计是**将HOW WARM COLD DATA的元信息混合保存**: 546 | 547 | **混合类型Journal保存** 548 | compacted summary block分别维护了一个公用的nat journal,以及sit journal,HOT WARM COLD类型的Journal都会混合保存进入两个journal结构中。 549 | 550 | 在满足COMPACTED的条件下,系统启动时,F2FS会从磁盘中读取这两个Journal到内存中,分别保存在HOT以及COLD所在的curseg->journal中。 551 | 552 | 不同类型的journal会在CP时刻,通过`f2fs_flush_sit_entries`函数写入到HOT或者COLD对应的curseg->journal区域中。如果HOT或者COLD对应的curseg->journal区域的空间不够了,就将不同类型的journal保存的segment的信息,直接写入到对应的sit entry block中。 553 | 554 | 接下来将HOT或者COLD对应的curseg->journal包装为compacted block回写到cp区域中。 555 | 556 | **混合类型Summary保存** 557 | b) 以及将HOT,WARM,COLD三种类型的summary混合保存同一个data summaries数组中,它们的差别如下: 558 | ``` 559 | compacted summary block (4KB) 560 | +------------------+ 561 | |nat journal | 562 | |sit journal | 563 | |data sum[439] | data summaries数组大小是439 564 | +------------------+ 565 | | | 如果需要,会接上一个纯summary数组的block 566 | | data sum[584] | data summaries数组大小是584 567 | | | 568 | +------------------+ 569 | 570 | 571 | normal summary block,表示三种类型的DATA的summary 572 | +--------------------+ 573 | |hot data journal | 574 | |hot data summaries | data summaries数组大小是512 575 | | | 576 | +--------------------+ 577 | |warm data journal | 578 | |warm data summaries | data summaries数组大小是512 579 | | | 580 | +--------------------+ 581 | |cold data journal | 582 | |cold data summaries | data summaries数组大小是512 583 | | | 584 | +--------------------+ 585 | ``` 586 | 587 | 根据上面的描述,不同类型的summary block的可以保存的summary的大小,可以得到 588 | HOT,WARM,COLD DATA这三种类型,如果目前**加起来**仅使用了 589 | 1. 少于439的block(只修改了439个f2fs_summary),那么可以通过compacted回写方式进行回写,即通过一个compacted summary block完成回写,需要回写1个block。 590 | 2. 大于439,少于439+584=1023个block,那么可以通过compacted回写方式进行回写,即可以通过compacted summary block加一个纯summary block的方式保存所有信息,需要回写2个block。 591 | 3. 大于1023的情况下,即和normal summary block同样的回写情况,那么就会使用normal summary block的回写方式完成回写,即回写3个block。(因为大于1023情况下,如果继续使用compacted回写,最差的情况下要回写4个block) 592 | 593 | 接下来进行代码分析: 594 | ```c 595 | 596 | // 根据需要回写的summary的数目,返回需要写回的block的数目,返回值有1、2、3 597 | data_sum_blocks = f2fs_npages_for_summary_flush(sbi, false); 598 | 599 | // 如果data_sum_blocks = 1 或者 2,则表示回写1个或者2个block,则设置CP_COMPACT_SUM_FLAG标志 600 | if (data_sum_blocks < NR_CURSEG_DATA_TYPE) // NR_CURSEG_DATA_TYPE = 3 601 | __set_ckpt_flags(ckpt, CP_COMPACT_SUM_FLAG); 602 | else 603 | __clear_ckpt_flags(ckpt, CP_COMPACT_SUM_FLAG); 604 | 605 | // 然后将summary写入磁盘 606 | f2fs_write_data_summaries(sbi, start_blk); // 将data summary以及里面的journal写入磁盘 607 | ``` 608 | `f2fs_write_data_summaries`函数会判断一下是否设置了CP_COMPACT_SUM_FLAG标志,采取不同的方法写入磁盘 609 | ```c 610 | void f2fs_write_data_summaries(struct f2fs_sb_info *sbi, block_t start_blk) 611 | { 612 | if (is_set_ckpt_flags(sbi, CP_COMPACT_SUM_FLAG)) 613 | write_compacted_summaries(sbi, start_blk); 614 | else 615 | write_normal_summaries(sbi, start_blk, CURSEG_HOT_DATA); 616 | } 617 | ``` 618 | `write_compacted_summaries`函数会根据上述的compacted block的数据分布,将数据写入到磁盘中 619 | ```c 620 | static void write_compacted_summaries(struct f2fs_sb_info *sbi, block_t blkaddr) 621 | { 622 | struct page *page; 623 | unsigned char *kaddr; 624 | struct f2fs_summary *summary; 625 | struct curseg_info *seg_i; 626 | int written_size = 0; 627 | int i, j; 628 | int datatypes = CURSEG_COLD_DATA; 629 | #ifdef CONFIG_F2FS_COMPRESSION 630 | datatypes = CURSEG_BG_COMPR_DATA; 631 | #endif 632 | 633 | page = f2fs_grab_meta_page(sbi, blkaddr++); 634 | kaddr = (unsigned char *)page_address(page); 635 | memset(kaddr, 0, PAGE_SIZE); 636 | 637 | /* Step 1: write nat cache */ 638 | seg_i = CURSEG_I(sbi, CURSEG_HOT_DATA); // 第一步写nat的journal 639 | memcpy(kaddr, seg_i->journal, SUM_JOURNAL_SIZE); 640 | written_size += SUM_JOURNAL_SIZE; 641 | 642 | /* Step 2: write sit cache */ 643 | seg_i = CURSEG_I(sbi, CURSEG_COLD_DATA); 644 | memcpy(kaddr + written_size, seg_i->journal, SUM_JOURNAL_SIZE); // 第二步写sit的journal 645 | written_size += SUM_JOURNAL_SIZE; 646 | 647 | /* Step 3: write summary entries */ 648 | for (i = CURSEG_HOT_DATA; i <= datatypes; i++) { // 开始写summary 649 | unsigned short blkoff; 650 | seg_i = CURSEG_I(sbi, i); 651 | if (sbi->ckpt->alloc_type[i] == SSR) 652 | blkoff = sbi->blocks_per_seg; 653 | else 654 | blkoff = curseg_blkoff(sbi, i); 655 | 656 | for (j = 0; j < blkoff; j++) { 657 | if (!page) { // 如果f2fs compacted block写不下,则创建一个纯summary的block 658 | page = f2fs_grab_meta_page(sbi, blkaddr++); 659 | kaddr = (unsigned char *)page_address(page); 660 | memset(kaddr, 0, PAGE_SIZE); 661 | written_size = 0; 662 | } 663 | summary = (struct f2fs_summary *)(kaddr + written_size); 664 | *summary = seg_i->sum_blk->entries[j]; 665 | written_size += SUMMARY_SIZE; 666 | 667 | if (written_size + SUMMARY_SIZE <= PAGE_SIZE - 668 | SUM_FOOTER_SIZE) 669 | continue; 670 | 671 | set_page_dirty(page); // 如果超过了compaced sum block可以承载的极限,就设置这个block是脏,等待回写 672 | f2fs_put_page(page, 1); 673 | page = NULL; 674 | } 675 | } 676 | if (page) { 677 | set_page_dirty(page); 678 | f2fs_put_page(page, 1); 679 | } 680 | } 681 | ``` 682 | 683 | `write_normal_summaries`函数则是简单地将按照HOT/WARM/COLD的顺序写入到checkpoint区域中 684 | ```c 685 | static void write_normal_summaries(struct f2fs_sb_info *sbi, 686 | block_t blkaddr, int type) 687 | { 688 | int i, end; 689 | if (IS_DATASEG(type)) 690 | end = type + NR_CURSEG_DATA_TYPE; 691 | else 692 | end = type + NR_CURSEG_NODE_TYPE; 693 | 694 | for (i = type; i < end; i++) // 根据 HOW WARM COLD 都写入磁盘 695 | write_current_sum_page(sbi, i, blkaddr + (i - type)); 696 | } 697 | 698 | static void write_current_sum_page(struct f2fs_sb_info *sbi, 699 | int type, block_t blk_addr) 700 | { 701 | struct curseg_info *curseg = CURSEG_I(sbi, type); 702 | struct page *page = f2fs_grab_meta_page(sbi, blk_addr); 703 | struct f2fs_summary_block *src = curseg->sum_blk; 704 | struct f2fs_summary_block *dst; 705 | 706 | dst = (struct f2fs_summary_block *)page_address(page); 707 | memset(dst, 0, PAGE_SIZE); 708 | 709 | mutex_lock(&curseg->curseg_mutex); 710 | 711 | down_read(&curseg->journal_rwsem); 712 | memcpy(&dst->journal, curseg->journal, SUM_JOURNAL_SIZE); 713 | up_read(&curseg->journal_rwsem); 714 | 715 | memcpy(dst->entries, src->entries, SUM_ENTRY_SIZE); 716 | memcpy(&dst->footer, &src->footer, SUM_FOOTER_SIZE); 717 | 718 | mutex_unlock(&curseg->curseg_mutex); 719 | 720 | set_page_dirty(page); 721 | f2fs_put_page(page, 1); 722 | } 723 | ``` 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | -------------------------------------------------------------------------------- /F2FS-Data-Recovery/Recovery的流程.md: -------------------------------------------------------------------------------- 1 | ## 数据恢复的代码的实现与分析 2 | 3 | 4 | ### 后滚恢复 5 | 后滚恢复的代码实现主要是对Checkpoint数据的读取与恢复。Checkpoint的数据会在F2FS启动的时候,分两阶段进行恢复,第一阶段恢复`f2fs_checkpoint`相关的数据,第二阶段恢复与curseg相关的数据,下面进行分析。 6 | 7 | F2FS的初始化函数`f2fs_fill_super`中与后滚恢复相关的代码有 8 | ``` 9 | static int f2fs_fill_super(struct super_block *sb, void *data, int silent) 10 | { 11 | ... 12 | err = f2fs_get_valid_checkpoint(sbi); // 恢复f2fs_checkpoint 13 | ... 14 | err = f2fs_build_segment_manager(sbi); // 恢复curseg 15 | ... 16 | } 17 | ``` 18 | 19 | #### 恢复f2fs_checkpoint 20 | 具体的恢复流程是: 21 | 1. 分配f2fs_checkpoint的堆空间 22 | 2. 然后根据sbi记录的checkpoint的起始地址,读取cp1,cp2的数据出来。 23 | 24 | ```c 25 | int f2fs_get_valid_checkpoint(struct f2fs_sb_info *sbi) 26 | { 27 | 28 | sbi->ckpt = f2fs_kzalloc(sbi, array_size(blk_size, cp_blks), 29 | GFP_KERNEL); // 分配空间 30 | if (!sbi->ckpt) 31 | return -ENOMEM; 32 | 33 | cp_start_blk_no = le32_to_cpu(fsb->cp_blkaddr); // 从sbi获得checkpoint的起始地址 34 | cp1 = validate_checkpoint(sbi, cp_start_blk_no, &cp1_version); // 读取 & 检查合法性 35 | 36 | /* The second checkpoint pack should start at the next segment */ 37 | cp_start_blk_no += ((unsigned long long)1) << 38 | le32_to_cpu(fsb->log_blocks_per_seg); 39 | cp2 = validate_checkpoint(sbi, cp_start_blk_no, &cp2_version); 40 | 41 | if (cp1 && cp2) { // 根据版本的老旧决定使用哪个版本的cp 42 | if (ver_after(cp2_version, cp1_version)) 43 | cur_page = cp2; 44 | else 45 | cur_page = cp1; 46 | } else if (cp1) { 47 | cur_page = cp1; 48 | } else if (cp2) { 49 | cur_page = cp2; 50 | } else { 51 | goto fail_no_cp; 52 | } 53 | 54 | cp_block = (struct f2fs_checkpoint *)page_address(cur_page); // 这个cur_page表示正在使用的cp 55 | memcpy(sbi->ckpt, cp_block, blk_size); // 复制数据到sbi中,用于运行中的管理 56 | 57 | /* Sanity checking of checkpoint */ 58 | if (f2fs_sanity_check_ckpt(sbi)) 59 | goto free_fail_no_cp; 60 | 61 | if (cur_page == cp1) 62 | sbi->cur_cp_pack = 1; 63 | else 64 | sbi->cur_cp_pack = 2; 65 | 66 | if (cp_blks <= 1) // 一般情况下,cp_blks=1,所以这里就是完成了cp的初始化了 67 | goto done; 68 | 69 | cp_blk_no = le32_to_cpu(fsb->cp_blkaddr); 70 | if (cur_page == cp2) 71 | cp_blk_no += 1 << le32_to_cpu(fsb->log_blocks_per_seg); 72 | 73 | for (i = 1; i < cp_blks; i++) { // 注意这里构建bitmap,存疑,如何构建,理论上会占用journal 74 | void *sit_bitmap_ptr; 75 | unsigned char *ckpt = (unsigned char *)sbi->ckpt; 76 | 77 | cur_page = f2fs_get_meta_page(sbi, cp_blk_no + i); 78 | sit_bitmap_ptr = page_address(cur_page); 79 | memcpy(ckpt + i * blk_size, sit_bitmap_ptr, blk_size); 80 | f2fs_put_page(cur_page, 1); 81 | } 82 | done: 83 | f2fs_put_page(cp1, 1); 84 | f2fs_put_page(cp2, 1); 85 | return 0; 86 | 87 | free_fail_no_cp: 88 | f2fs_put_page(cp1, 1); 89 | f2fs_put_page(cp2, 1); 90 | fail_no_cp: 91 | kfree(sbi->ckpt); 92 | return -EINVAL; 93 | } 94 | ``` 95 | 96 | #### 恢复curseg 97 | 这一个步骤主要在`f2fs_build_segment_manager`函数的`build_curseg`函数中完成: 98 | 1. 分配空间给curseg_info数组,默认是6个,表示(HOT,WARM,COLD) X (DATA,NODE)的6种分配方式。 99 | 2. 执行`restore_curseg_summaries`函数,从`f2fs_checkpoint`中读取数据,以及根据compacted summary block和normal summary block恢复数据。 100 | 101 | ```c 102 | static int build_curseg(struct f2fs_sb_info *sbi) 103 | { 104 | struct curseg_info *array; 105 | int i; 106 | 107 | array = f2fs_kzalloc(sbi, array_size(NR_CURSEG_TYPE, sizeof(*array)), 108 | GFP_KERNEL); 109 | if (!array) 110 | return -ENOMEM; 111 | 112 | SM_I(sbi)->curseg_array = array; 113 | 114 | // 初始化curseg的空间 115 | for (i = 0; i < NR_CURSEG_TYPE; i++) { 116 | mutex_init(&array[i].curseg_mutex); 117 | array[i].sum_blk = f2fs_kzalloc(sbi, PAGE_SIZE, GFP_KERNEL); 118 | if (!array[i].sum_blk) 119 | return -ENOMEM; 120 | init_rwsem(&array[i].journal_rwsem); 121 | array[i].journal = f2fs_kzalloc(sbi, 122 | sizeof(struct f2fs_journal), GFP_KERNEL); 123 | if (!array[i].journal) 124 | return -ENOMEM; 125 | array[i].segno = NULL_SEGNO; 126 | array[i].next_blkoff = 0; 127 | } 128 | 129 | // 从磁盘中,读取恢复curseg的信息 130 | return restore_curseg_summaries(sbi); 131 | } 132 | ``` 133 | 134 | **restore_curseg_summaries函数** 135 | 136 | `restore_curseg_summaries`第一步先检查CP_COMPACT_SUM_FLAG的标志,这个标志用于检查是否按COMPACTED的方式读取data summary。第二步就是通过NORMAL的方式读取NODE的summary。 137 | ```c 138 | static int restore_curseg_summaries(struct f2fs_sb_info *sbi) 139 | { 140 | struct f2fs_journal *sit_j = CURSEG_I(sbi, CURSEG_COLD_DATA)->journal; 141 | struct f2fs_journal *nat_j = CURSEG_I(sbi, CURSEG_HOT_DATA)->journal; 142 | int type = CURSEG_HOT_DATA; 143 | int err; 144 | 145 | if (is_set_ckpt_flags(sbi, CP_COMPACT_SUM_FLAG)) { 146 | int npages = f2fs_npages_for_summary_flush(sbi, true); // 检查需要读取一个block还是2个block 147 | 148 | if (npages >= 2) 149 | f2fs_ra_meta_pages(sbi, start_sum_block(sbi), npages, 150 | META_CP, true); // 先预读出来 151 | 152 | /* restore for compacted data summary */ 153 | read_compacted_summaries(sbi); //恢复 compacted summary 154 | type = CURSEG_HOT_NODE; 155 | } 156 | 157 | if (__exist_node_summaries(sbi)) // 如果没有出现宕机,则预测这几个block 158 | f2fs_ra_meta_pages(sbi, sum_blk_addr(sbi, NR_CURSEG_TYPE, type), 159 | NR_CURSEG_TYPE - type, META_CP, true); 160 | 161 | /* 162 | * 如果没有COMPACTED标识,则DATA和NODE都使用NORMAL的方式进行恢复 163 | * */ 164 | for (; type <= CURSEG_COLD_NODE; type++) { 165 | err = read_normal_summaries(sbi, type); 166 | if (err) 167 | return err; 168 | } 169 | 170 | /* sanity check for summary blocks */ 171 | if (nats_in_cursum(nat_j) > NAT_JOURNAL_ENTRIES || 172 | sits_in_cursum(sit_j) > SIT_JOURNAL_ENTRIES) 173 | return -EINVAL; 174 | 175 | return 0; 176 | } 177 | ``` 178 | **read_normal_summaries函数** 179 | 这个函数对于F2FS的正常关闭,重新启动时读取的summary的方式都是类似的,都是根据HOT/WARM/COLD的顺序,读取对应的block,然后将数据保存到curseg对应的类型当中。这里**重点考虑出现了宕机的情况的恢复**。 180 | 181 | F2FS可以通过判断`f2fs_checkpoint`的`CP_UMOUNT_FLAG`标志,可以知道系统是否出现了宕机的情况,**由于F2FS只会在启动和关闭的时候回写node summaries,因此宕机下毫无疑问会丢失所有的node summary**。对函数的DATA部分进行简化后,函数如下。 182 | 183 | ```c 184 | static int read_normal_summaries(struct f2fs_sb_info *sbi, int type) 185 | { 186 | 187 | segno = le32_to_cpu(ckpt->cur_node_segno[type - 188 | CURSEG_HOT_NODE]); // 获取上次最后时刻使用的segno 189 | blk_off = le16_to_cpu(ckpt->cur_node_blkoff[type - 190 | CURSEG_HOT_NODE]); // 以及用到这个segno的第几个block 191 | 192 | if (__exist_node_summaries(sbi)) 193 | blk_addr = sum_blk_addr(sbi, NR_CURSEG_NODE_TYPE, 194 | type - CURSEG_HOT_NODE); // 如果没有宕机的情况下,从checkpoint中恢复 195 | else 196 | blk_addr = GET_SUM_BLOCK(sbi, segno); // 出现了宕机,则从SSA中恢复 197 | 198 | new = f2fs_get_meta_page(sbi, blk_addr); // 根据地址读取出f2fs_summary_block 199 | sum = (struct f2fs_summary_block *)page_address(new); // 转换结构 200 | 201 | if (__exist_node_summaries(sbi)) { // 如果没有宕机的情况下,将每一个ns重新置为0 202 | struct f2fs_summary *ns = &sum->entries[0]; 203 | int i; 204 | for (i = 0; i < sbi->blocks_per_seg; i++, ns++) { 205 | ns->version = 0; 206 | ns->ofs_in_node = 0; 207 | } 208 | } else { 209 | f2fs_restore_node_summary(sbi, segno, sum); // 如果出现了宕机,则执行这个这个函数 210 | } 211 | 212 | /* set uncompleted segment to curseg */ 213 | curseg = CURSEG_I(sbi, type); 214 | mutex_lock(&curseg->curseg_mutex); 215 | 216 | /* 复制journal到curseg */ 217 | /* update journal info */ 218 | down_write(&curseg->journal_rwsem); 219 | memcpy(curseg->journal, &sum->journal, SUM_JOURNAL_SIZE); 220 | up_write(&curseg->journal_rwsem); 221 | 222 | /* 复制summaries到curseg */ 223 | memcpy(curseg->sum_blk->entries, sum->entries, SUM_ENTRY_SIZE); 224 | memcpy(&curseg->sum_blk->footer, &sum->footer, SUM_FOOTER_SIZE); 225 | curseg->next_segno = segno; 226 | reset_curseg(sbi, type, 0); 227 | curseg->alloc_type = ckpt->alloc_type[type]; 228 | curseg->next_blkoff = blk_off; 229 | mutex_unlock(&curseg->curseg_mutex); 230 | f2fs_put_page(new, 1); 231 | return 0; 232 | } 233 | ``` 234 | 235 | 然后分析 `f2fs_restore_node_summary` 函数,这个函数主要是根据宕机时,根据最后一次checkpoint的node正在使用的segno找到对应的保存在这个segno的所有node page。进而遍历整个segment的node page恢复改summary对应的nid。 236 | 237 | !!!需要注意的时候这种恢复方式仅仅可以对node使用,不能对data使用,这是因为node summary仅需要记录一个nid,但是对于data的summary,需要同时记录ofs_in_node,这个信息无法从node中直接恢复出来,因此不能使用这种方式。 238 | ```c 239 | void f2fs_restore_node_summary(struct f2fs_sb_info *sbi, 240 | unsigned int segno, struct f2fs_summary_block *sum) 241 | { 242 | struct f2fs_node *rn; 243 | struct f2fs_summary *sum_entry; 244 | block_t addr; 245 | int i, idx, last_offset, nrpages; 246 | 247 | /* scan the node segment */ 248 | last_offset = sbi->blocks_per_seg; 249 | addr = START_BLOCK(sbi, segno); // node block所在的segment 250 | sum_entry = &sum->entries[0]; 251 | 252 | for (i = 0; i < last_offset; i += nrpages, addr += nrpages) { 253 | nrpages = min(last_offset - i, BIO_MAX_PAGES); 254 | 255 | /* readahead node pages */ 256 | f2fs_ra_meta_pages(sbi, addr, nrpages, META_POR, true); // 预读NODE PAGES 257 | 258 | for (idx = addr; idx < addr + nrpages; idx++) { // 遍历这个segment的node,然后将nid赋值改curseg 259 | struct page *page = f2fs_get_tmp_page(sbi, idx); 260 | 261 | rn = F2FS_NODE(page); 262 | sum_entry->nid = rn->footer.nid; 263 | sum_entry->version = 0; 264 | sum_entry->ofs_in_node = 0; 265 | sum_entry++; 266 | f2fs_put_page(page, 1); 267 | } 268 | 269 | invalidate_mapping_pages(META_MAPPING(sbi), addr, 270 | addr + nrpages); 271 | } 272 | } 273 | ``` 274 | 275 | ## 原理未名 276 | ### 前滚恢复 277 | 前滚恢复的主要作用是恢复那些执行了fsync等同步操作写入了磁盘,但是在CP回写之前就宕机的数据(F2FS为了提高效率,不会在fsync之后马上就回写CP,而是加上了某些标志然后回写fsync关联的数据,用于前滚恢复,从而避免大开销的写CP操作)。 278 | 279 | **典型Crash场景分析:**dnode block指的是直接保存data block的node page,包含`f2fs_inode`以及`direct_node`这两种node page。参考下图,在blkaddr=2和3之间发生了checkpoint,cpver表示的checkpoint版本号由11增加为12,nat bitmap也写入到checkpoint中。一个文件进行修改了之后,执行fsync,使用了blkaddr=4和blkaddr=5的dnode block用于准备node page数据的写入,但是此时出现了Crash,导致目前的nat bitmap丢失了。因此node page的索引都会丢失,即使node page已经在crash之前写入了磁盘。 280 | 281 | ``` 282 | DNODE BLOCK 283 | +-----------+ 284 | |blkaddr=2 | <-node.footer->cpver=11, curseg->next_blkoff=3 285 | +-----------+ <-Checkpoint, cpver 11=>12,cp是先增加版本号再回写到磁盘,因此stable cpver=12 286 | |blkaddr=3 | <-node.footer->cpver=12, curseg->next_blkoff=4 287 | +-----------+ <-fsync 288 | |blkaddr=4 | <-node.footer->cpver=12, curseg->next_blkoff=5 289 | +-----------+ 290 | |blkaddr=5 | <-node.footer->cpver=12, curseg->next_blkoff=6 291 | +-----------+ <-Crash 292 | |blkaddr=6 | 293 | +-----------+ 294 | ``` 295 | **典型前滚恢复流程分析:**系统在宕机后重新启动时,恢复到上一次的稳定的checkpoint点,即cpver=12的时刻。此时根据curseg->next_blkoff的值即=,即3,找到下一个即将使用的blkaddr。然后找到blkaddr=3,然后读取出来,然后根据一定的条件判断(如通常情况,必须node和checkpoint的cpver一致才可以恢复),是否是可以恢复的node page,然后继续判断下一个curseg->next_blkoff的数据,直到为空。 296 | 297 | 298 | 前滚恢复的具体实现在recovery.c,核心函数是 `f2fs_recover_fsync_data` ,下面进行分析。 299 | 300 | ### Recovery的代码分析 301 | F2FS启动的时候,会调用 `f2fs_recover_fsync_data` 函数开始恢复数据,步骤如下: 302 | 1. 先通过`find_fsync_dnodes`找出所有的可以恢复的dnode对应的inode(有可能dnode就是inode本身),放入到一个list里面。 303 | 2. 恢复inode list里面的所有的node page。 304 | 305 | ```c 306 | int f2fs_recover_fsync_data(struct f2fs_sb_info *sbi, bool check_only) 307 | { 308 | ... 309 | // 找出所有的可以恢复的dnode对应的inode(有可能dnode就是inode本身),放入到一个list里面 310 | err = find_fsync_dnodes(sbi, &inode_list, check_only); 311 | 312 | ... 313 | // 恢复inode list里面的数据 314 | err = recover_data(sbi, &inode_list, &dir_list); 315 | ... 316 | } 317 | ``` 318 | 319 | **f2fs_recover_fsync_data函数** 320 | ```c 321 | static int find_fsync_dnodes(struct f2fs_sb_info *sbi, struct list_head *head, bool check_only) 322 | { 323 | ... 324 | curseg = CURSEG_I(sbi, CURSEG_WARM_NODE); // 因为是基于dnode进行恢复,因此是WARM NODE 325 | 326 | blkaddr = NEXT_FREE_BLKADDR(sbi, curseg); // 遍历next_blkoff组成的node list 327 | while (1) { 328 | 329 | page = f2fs_get_tmp_page(sbi, blkaddr); // 根据blkaddr读取该地址对应的node page的数据 330 | 331 | if (!is_recoverable_dnode(page)) // 比较cpver的版本 332 | break; 333 | 334 | if (!is_fsync_dnode(page)) // 前滚恢复只能恢复被fsync的node page 335 | goto next; 336 | 337 | /** 338 | * 如果是NULL,则表示inode不在list中 339 | * 如不是NULL,则表示这个inode已经在list中,不需要加入了 340 | **/ 341 | entry = get_fsync_inode(head, ino_of_node(page)); // 342 | if (!entry) { 343 | bool quota_inode = false; 344 | 345 | if (!check_only && 346 | IS_INODE(page) && is_dent_dnode(page)) { // 如果是dentry的inode,则先恢复 347 | err = f2fs_recover_inode_page(sbi, page); 348 | if (err) 349 | break; 350 | quota_inode = true; 351 | } 352 | 353 | /* 354 | * CP | dnode(F) | inode(DF) 355 | * For this case, we should not give up now. 356 | */ 357 | entry = add_fsync_inode(sbi, head, ino_of_node(page), 358 | quota_inode); 359 | if (IS_ERR(entry)) { 360 | err = PTR_ERR(entry); 361 | if (err == -ENOENT) { 362 | err = 0; 363 | goto next; 364 | } 365 | break; 366 | } 367 | } 368 | entry->blkaddr = blkaddr; 369 | 370 | if (IS_INODE(page) && is_dent_dnode(page)) 371 | entry->last_dentry = blkaddr; // 将恢复得inode串成一个列 372 | next: 373 | /* sanity check in order to detect looped node chain */ 374 | if (++loop_cnt >= free_blocks || 375 | blkaddr == next_blkaddr_of_node(page)) { 376 | f2fs_msg(sbi->sb, KERN_NOTICE, 377 | "%s: detect looped node chain, " 378 | "blkaddr:%u, next:%u", 379 | __func__, blkaddr, next_blkaddr_of_node(page)); 380 | err = -EINVAL; 381 | break; 382 | } 383 | 384 | /** 385 | * check next segment 386 | * 387 | * F2FS分配物理地址的时候,会将本次分配的blkaddr和下一次分配的blkaddr,通过footer连接成一个list, 388 | * 前滚恢复通过这个list找到下一个被分配的blkaddr,直到没有分配为止 389 | * */ 390 | blkaddr = next_blkaddr_of_node(page); 391 | f2fs_put_page(page, 1); 392 | 393 | f2fs_ra_meta_pages_cond(sbi, blkaddr); // 预读下一个blkaddr 394 | } 395 | f2fs_put_page(page, 1); 396 | return err; 397 | } 398 | ``` 399 | 400 | **recover_data函数** 401 | ```c 402 | static int recover_data(struct f2fs_sb_info *sbi, struct list_head *inode_list, 403 | struct list_head *dir_list) 404 | { 405 | struct curseg_info *curseg; 406 | struct page *page = NULL; 407 | int err = 0; 408 | block_t blkaddr; 409 | 410 | /* get node pages in the current segment */ 411 | curseg = CURSEG_I(sbi, CURSEG_WARM_NODE); 412 | blkaddr = NEXT_FREE_BLKADDR(sbi, curseg); 413 | 414 | while (1) { 415 | struct fsync_inode_entry *entry; 416 | 417 | if (!f2fs_is_valid_meta_blkaddr(sbi, blkaddr, META_POR)) 418 | break; 419 | 420 | f2fs_ra_meta_pages_cond(sbi, blkaddr); 421 | 422 | page = f2fs_get_tmp_page(sbi, blkaddr); 423 | 424 | if (!is_recoverable_dnode(page)) { 425 | f2fs_put_page(page, 1); 426 | break; 427 | } 428 | 429 | entry = get_fsync_inode(inode_list, ino_of_node(page)); // 从inodelist中取出一个entry 430 | if (!entry) 431 | goto next; 432 | /* 433 | * inode(x) | CP | inode(x) | dnode(F) 434 | * In this case, we can lose the latest inode(x). 435 | * So, call recover_inode for the inode update. 436 | */ 437 | if (IS_INODE(page)) 438 | recover_inode(entry->inode, page); 439 | if (entry->last_dentry == blkaddr) { 440 | err = recover_dentry(entry->inode, page, dir_list); 441 | if (err) { 442 | f2fs_put_page(page, 1); 443 | break; 444 | } 445 | } 446 | err = do_recover_data(sbi, entry->inode, page); // 进行恢复 447 | if (err) { 448 | f2fs_put_page(page, 1); 449 | break; 450 | } 451 | 452 | if (entry->blkaddr == blkaddr) 453 | del_fsync_inode(entry); 454 | next: 455 | /* check next segment */ 456 | blkaddr = next_blkaddr_of_node(page); 457 | f2fs_put_page(page, 1); 458 | } 459 | if (!err) 460 | f2fs_allocate_new_segments(sbi); 461 | return err; 462 | } 463 | 464 | ``` 465 | -------------------------------------------------------------------------------- /F2FS-Data-Recovery/数据恢复的原理以及方式.md: -------------------------------------------------------------------------------- 1 | ## F2FS文件系统的数据恢复 2 | ### 数据恢复的简介 3 | F2FS数据恢复的核心结构是Checkpoint结构,它保存了很多元数据用于维护系统一致性。数据恢复方式有两种,分别是后滚恢复(Roll-Back Recovery),前滚恢复(Roll-Forward Recovery)。 4 | 5 | ### Checkpoint的作用 6 | 从F2FS的磁盘布局可以了解到,F2FS有一个元数据区域,用于集中管理磁盘所有的block的信息,因此F2FS使用了检查点(checkpointing)机制去维护文件系统的恢复点(recovery point),这个恢复点用于在系统突然崩溃的时候,元数据区域依然能够正确地将数据重新读取出来。因此保证元数据区域的有效性以及恢复性。当F2FS需要通过 `fsync` 或 `umount` 等命令对系统进行同步的时候,F2FS会触发一次Checkpoint机制,它会完成以下的工作: 7 | 1. 页缓存的脏node和dentry block会刷写回到磁盘; 8 | 2. 挂起系统其他的写行为,如create,unlink,mkdir; 9 | 3. 将系统的meta data,如NAT、SIT、SSA的数据写回磁盘; 10 | 4. 更新checkpoint的状态,包括checkpoint的版本,NAT和SIT的bitmaps以及journals,SSA,Orphan inode 11 | 12 | ### 后滚恢复-Roll-Back Recovery 13 | 当遇到突然失去电源等情况,F2FS会回滚到最近的Checkpoint点中。为了保持一个稳定可用的Checkpoint(防止Chcekpoint在创建或者更新的时候宕机),F2FS维护了两个Checkpoint。如果一个Checkpoint在header和footer包含可以区分的内容,那么F2FS就会认为是可用的,可以使用这个Checkpoint,否则就会被丢弃掉。 14 | 15 | 与之相似,每一个Checkpoint都包含一份NAT bitmap和SIT bitmap,用于管理NAT Blocks和SIT Blocks。F2FS会选择其中一组Checkpoint数据进行更新,然后标记为最新。 16 | 17 | 如果有少量的NAT和SIT entries被频繁更新,可能会导致频繁大量的IO读写。因此F2FS在Checkpoint使用了SIT journals和NAT journals去缓存一些频繁更改。通过这种方式可以减少CP和读写的Overheads。 18 | 19 | 在F2FS进行挂载的时候,F2FS需要根据Headers和footers找到合适的可用的Checkpoint,如果两个Checkpoint都是可用的,那么F2FS会挑选version最新的Checkpoint进行恢复。在F2FS得到可用的Checkpoint之后,就会检查是否存在Orphan Inodes,如果找到了orphan inode,就会truncate它们的所有数据。然后,F2FS会根据SIT Blocks和NAT Blocks的bitmap恢复meata data和建立映射表。 20 | 21 | 22 | ## 原理未名 23 | ### 前滚恢复-Roll-Forward Recovery 24 | 应用程序常常会调用fsync完成一些同步性的请求,为了满足这个同步性的请求,一个简单的方法是每次调用fsync都写一次Checkpoint。然而,由于Checkpoint需要同时写入大量与调用fsync的应用程序无关的node、 dentry block等数据,因此这是一个大开销的操作,会影响到手机的流畅性。 25 | 26 | 因此,F2FS使用了高效的前滚恢复方式以优化fsync的性能。核心的概念是**只写入data block和direct node到磁盘**,但是而不写入其他元数据。F2FS具体实现是在fsync操作完成之后,给每一个相关的direct node加上一个特殊的标记位。当系统后滚恢复到了一个稳定的Checkpoint后,F2FS会选择性地(通过标记位)通过前滚恢复恢复一些已经fsync到磁盘,但是没有被元数据索引的一些数据。 27 | 28 | 一个典型的场景是:当完成一次写操作后,需要更新node page和data block,然后执行fsync,node page以及data block在submit bio后就完成了数据写入磁盘。此时需要注意的是nat bitmap是在checkpoit的地方更新的,优化后的fsync,不会每一次fsync都会执行checkpoint。**但是如果在checkpoint之前出现了宕机**,虽然node page和data page本身已经写入了磁盘,但是丢失了nat bitmap,因此无法索引这些已经写入磁盘的node page和data page,这时就要利用前滚恢复机制,重新建立索引。 29 | 30 | 前滚恢复的原理如下: 31 | 假设上一次稳定的Checkpoint的日志位置是N 32 | 1) F2FS收集了日志位置是N+n的,含有特殊标记位的direct node block,这些block之间组成一个list用于表示node information,其中n表示上一次稳定的Checkpoint之后写入了多少个block。 33 | 2) 通过这个list中的node information,系统将距离上一次稳定的Checkpoint最靠近的被更新的node block(日志位置是N-n),载入到cache当中。 34 | 3) 比较日志位置是N-n和N+n的数据是否一致,如果不一致则使用N+n的数据覆盖已经缓存的N-n的node block,然后标记为脏。 35 | 4) 执行写Checkpoint流程,刷新元数据。 -------------------------------------------------------------------------------- /F2FS-Experiment/实验环境搭建.md: -------------------------------------------------------------------------------- 1 | # 实验环境搭建 2 | ## 一、搭建Qemu虚拟机 3 | 使用Qemu虚拟机可以方便的调试内核代码,可以跟着以下步骤进行搭建(参考自[笨叔叔的环境搭建样章](https://gitee.com/benshushu/runninglinuxkernel_4.0/raw/master/%E5%A5%94%E8%B7%91%E5%90%A7-linux%E5%86%85%E6%A0%B8-qemu%E8%B0%83%E8%AF%95%E5%86%85%E6%A0%B8-%E6%A0%B7%E7%AB%A0.pdf) ): 4 | ### **第一步: 安装编译内核所需要的运行环境** 5 | ```shell 6 | $ sudo apt-get install qemu libncurses5-dev build-essential gcc-arm-linux-gnueabi 7 | ``` 8 | 9 | ### **第二步: 下载qemu运行内核需要使用到内核源码以及busybox源码** 10 | 可以到以下网址进行下载: 11 | ``` 12 | 下载5.x的源码: https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/ 13 | 下载busybox源码: https://busybox.net/downloads/ 14 | ``` 15 | 16 | ### **第三步: 编译Busybox,制作一个最小文件系统** 17 | 解压busybox压缩包,然后进入其根目录,执行: 18 | ```shell 19 | $ export ARCH=arm 20 | $ export CROSS_COMPILE= arm-linux-gnueabi- 21 | $ make menuconfig 22 | ``` 23 | 进入menuconfig以后,进行如下配置: 24 | ``` 25 | Busybox Settings ---> 26 | Build Options ---> 27 | [*] Build BusyBox as a static binary (no shared libs) 28 | ``` 29 | 保存并退出menuconfig以后,执行`make install`完成编译,最后会得到一个`_install`目录,里面包含了一些编译好的系统命令可执行文件。 30 | 31 | ### **第四步: 制作根文件系统所需要的目录和工具** 32 | 先把编译生成的`_install`目录拷贝到解压后的Linux源码的根目录,然后进入`_install`目录,通过`mkdir`命令创建`etc`和`dev`等目录。 33 | ``` 34 | $ sudo mkdir etc 35 | $ sudo mkdir dev 36 | $ sudo mkdir mnt 37 | $ sudo mkdir –p etc/init.d/ 38 | ``` 39 | 在`_install /etc/init.d/`目录下新创建一个叫 rcS 的文件,并且写入如下内容: 40 | ```shell 41 | #!/bin/sh 42 | mkdir –p /proc 43 | mkdir –p /tmp 44 | mkdir -p /sys 45 | mkdir –p /mnt 46 | /bin/mount -a 47 | mkdir -p /dev/pts 48 | mount -t devpts devpts /dev/pts 49 | echo /sbin/mdev > /proc/sys/kernel/hotplug 50 | mdev –s 51 | ``` 52 | 然后给rcS文件赋予权限: 53 | ```shell 54 | $ sudo chmod 777 rcS 55 | ``` 56 | 在`_install /etc`目录下新创建一个叫fstab的文件,并且写入如下内容: 57 | ```shell 58 | proc /proc proc defaults 0 0 59 | tmpfs /tmp tmpfs defaults 0 0 60 | sysfs /sys sysfs defaults 0 0 61 | tmpfs /dev tmpfs defaults 0 0 62 | debugfs /sys/kernel/debug debugfs defaults 0 0 63 | ``` 64 | 在`_install /etc`目录下新创建一个叫inittab的文件,并且写入如下内容: 65 | ```shell 66 | ::sysinit:/etc/init.d/rcS 67 | ::respawn:-/bin/sh 68 | ::askfirst:-/bin/sh 69 | ::ctrlaltdel:/bin/umount -a -r 70 | ``` 71 | 最后在`_install /dev`目录下创建如下的设备节点: 72 | ```shell 73 | $ sudo mknod console c 5 1 74 | $ sudo mknod null c 1 3 75 | ``` 76 | 到这里就完成了根目录运行环境的创建。 77 | 78 | ### **第五步: 编译Linux内核** 79 | 首先是进入linux源码目录,然后通过menuconfig配置qemu运行参数: 80 | ```shell 81 | $ export ARCH=arm 82 | $ export CROSS_COMPILE= arm-linux-gnueabi- 83 | $ export make vexpress_defconfig 84 | $ make menuconfig 85 | ``` 86 | 进入menuconfig界面以后,修改几个内核选项: 87 | 第一个是修改initramfs的位置,即`_install`目录,这样系统就会将`_install`目录作为启动的根目录,还需要把boot option的command string清空。 88 | ``` 89 | General setup ---> 90 | [*] Initial RAM filesystem and RAM Disk (initramfs/initrd) support 91 | (_install) Initramfs source file(s) 92 | Boot options --> 93 | ()Default kernel command string 94 | ``` 95 | 第二个选项是内存选项,配置 memory split 为“3G/1G user/kernel split”以及打开高端内存。 96 | ``` 97 | Kernel Features ---> 98 | Memory split (3G/1G user/kernel split) ---> 99 | [ *] High Memory Support 100 | ``` 101 | 然后就可以开始编译内核: 102 | ```shell 103 | $ make bzImage –j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- 104 | $ make dtbs 105 | ``` 106 | 107 | ## 二、F2FS工具的准备 108 | 主要使用到用于格式化的mkfs.f2fs工具,ubuntu下可以执行以下命令进行安装 109 | ```shell 110 | sudo apt-get install f2fs-tools 111 | ``` 112 | ## 三、编译可运行F2FS的内核 113 | 经过上述步骤的编译,会在Linux的根目录生成一个`.config`文件,打开这个文件,找到以下的内核选项,并设置为y。 114 | ``` 115 | CONFIG_F2FS_FS=y 116 | CONFIG_F2FS_STAT_FS=y 117 | CONFIG_F2FS_FS_XATTR=y 118 | CONFIG_F2FS_FS_POSIX_ACL=y 119 | ``` 120 | 然后重新编译 121 | ```shell 122 | $ make bzImage –j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- 123 | $ make dtbs 124 | ``` 125 | 编译结束后,创建一个文件作为F2FS的磁盘空间 126 | ```shell 127 | dd if=/dev/zero of=a9rootfs.f2fs bs=1M count=250 # 创建250MB的F2FS空间 128 | mkfs.f2fs a9rootfs.f2fs #使用F2FS格式化工具进行格式化 129 | ``` 130 | 131 | 接下来,通过执行如下命令启动Qemu虚拟机,需要使用-sd选项将刚刚创建的作为F2FS磁盘空间的文件挂载到系统中: 132 | ```shell 133 | qemu-system-arm \ 134 | -M vexpress-a9 \ 135 | -m 512M \ 136 | -kernel /home/xxx/kernels/linux4/linux-4.18/arch/arm/boot/zImage \ 137 | -dtb /home/xxx/kernels/linux4/linux-4.18/arch/arm/boot/dts/vexpress-v2p-ca9.dtb \ 138 | -nographic \ 139 | -append "rdinit=/linuxrc console=ttyAMA0 loglevel=8" \ 140 | -sd a9rootfs.f2fs 141 | ``` 142 | 143 | 最后,Qemu完成启动之后,在Qemu的linux系统执行如下命令将F2FS挂载到linux中: 144 | ```shell 145 | mount -t f2fs /dev/mmcblk0 /mnt/ -o loop 146 | ``` 147 | 然后就可以在/mnt目录下通过F2FS对文件进行操作和测试。 148 | 149 | -------------------------------------------------------------------------------- /F2FS-GC/GC流程介绍.md: -------------------------------------------------------------------------------- 1 | # F2FS的GC流程 2 | 垃圾回收(Garbage Collection)在F2FS中,主要作用是回收无效的block,以供用户重新使用。在详细介绍GC之前,需要先分析一下为什么需要GC。 3 | 4 | ## Log-structured文件系统的特性 5 | F2FS是一个基于Log-structured的文件系统,而垃圾回收则是Log-structured文件系统一个非常重要的特征,因为其独特的数据分配方式: 6 | 1. 这种类型的文件系统运行时会一直维护一个segment manager的元数据结构。 7 | 2. 用户使用在写入流程中,分配的每一个block都是从一个segment manager中取出来,并根据block的地址更新segment manager对应位置的数据,标记该block为已使用。 8 | 3. 当用户需要进行更新文件数据时,文件系统会通过异地更新的方式进行数据更新,即文件系统将用户更新后的数据写入一个新的block中,并且将旧的block无效掉(invalid)。 9 | 10 | ## F2FS GC的简介 11 | 基于Log-structured文件系统的特征,GC的主要作用是回收这些invalid的block,以供文件系统继续使用。F2FS的GC分为前台GC和后台GC: 前台GC一般在系统空间紧张的情况下运行,目的是尽快回收空间; 而后台GC则是在系统空闲的情况下进行,目的是在不影响用户体验的情况回收一定的空间。前台GC一般情况下是在checkpoint或者写流程的时候触发,因为F2FS能够感知空间的使用率,如果空间不够了会常触发前台GC加快回收空间,这意味着文件系统空间不足的时候,性能可能会下降。后台GC则是被一个线程间隔一段时间进行触发。而接下来我们主要讨论的都是后台GC。 12 | 13 | ## GC线程创建以及GC的触发时机 14 | GC的启动函数是`start_gc_thread`,它在f2fs进行挂载的时候执行,作用是创建一个gc线程。 15 | ```c 16 | int start_gc_thread(struct f2fs_sb_info *sbi) 17 | { 18 | struct f2fs_gc_kthread *gc_th; 19 | dev_t dev = sbi->sb->s_bdev->bd_dev; 20 | int err = 0; 21 | 22 | // 分配gc线程所需要的内存空间 23 | gc_th = kmalloc(sizeof(struct f2fs_gc_kthread), GFP_KERNEL); 24 | if (!gc_th) { 25 | err = -ENOMEM; 26 | goto out; 27 | } 28 | 29 | // 设置最小后台gc触发间隔,DEF_GC_THREAD_MIN_SLEEP_TIME=30秒 30 | gc_th->min_sleep_time = DEF_GC_THREAD_MIN_SLEEP_TIME; 31 | // 设置最大后台gc触发间隔,DEF_GC_THREAD_MAX_SLEEP_TIME=60秒 32 | gc_th->max_sleep_time = DEF_GC_THREAD_MAX_SLEEP_TIME; 33 | // 设置没有gc的间隔,DEF_GC_THREAD_NOGC_SLEEP_TIME=300秒 34 | gc_th->no_gc_sleep_time = DEF_GC_THREAD_NOGC_SLEEP_TIME; 35 | 36 | // 判断系统是否为空间状态(idle) 37 | gc_th->gc_idle = 0; 38 | 39 | // 启动线程 40 | sbi->gc_thread = gc_th; 41 | init_waitqueue_head(&sbi->gc_thread->gc_wait_queue_head); 42 | sbi->gc_thread->f2fs_gc_task = kthread_run(gc_thread_func, sbi, 43 | "f2fs_gc-%u:%u", MAJOR(dev), MINOR(dev)); 44 | if (IS_ERR(gc_th->f2fs_gc_task)) { 45 | err = PTR_ERR(gc_th->f2fs_gc_task); 46 | kfree(gc_th); 47 | sbi->gc_thread = NULL; 48 | } 49 | out: 50 | return err; 51 | } 52 | ``` 53 | 从上面分析可以知道,gc的触发间隔会根据实际情况进行变化,下面根据gc线程的关于时间的变化的代码,分析是如何进行间隔变化的: 54 | ```c 55 | static inline void increase_sleep_time(struct f2fs_gc_kthread *gc_th, 56 | long *wait) 57 | { 58 | if (*wait == gc_th->no_gc_sleep_time) 59 | return; 60 | 61 | *wait += gc_th->min_sleep_time; 62 | if (*wait > gc_th->max_sleep_time) 63 | *wait = gc_th->max_sleep_time; 64 | } 65 | 66 | static inline void decrease_sleep_time(struct f2fs_gc_kthread *gc_th, 67 | long *wait) 68 | { 69 | if (*wait == gc_th->no_gc_sleep_time) 70 | *wait = gc_th->max_sleep_time; 71 | 72 | *wait -= gc_th->min_sleep_time; 73 | if (*wait <= gc_th->min_sleep_time) 74 | *wait = gc_th->min_sleep_time; 75 | } 76 | ``` 77 | `increase_sleep_time`以及`decrease_sleep_time`是调整gc间隔的函数,其中入参`long *wait`即为gc的间隔时间,而指针类型的原因是等待时间可以在该函数进行变化。 78 | 79 | 对于`increase_sleep_time`函数而言,如果目前的等待时间等于`no_gc_sleep_time`,则不做变化,表示系统处于不需要频繁做后台GC的情况,继续维持这种状态。如果不是,则增加30秒的gc间隔时间。 80 | 81 | 对于`decrease_sleep_time`函数而言,如果目前的等待时间等于`no_gc_sleep_time`,则不做变化,表示系统处于不需要频繁做后台GC的情况,继续维持这种状态。如果不是,则减少30秒的gc间隔时间。 82 | 83 | 这里有一个疑问,如果两个函数的末尾都增加限制范围的判断,限制了gc的间隔时间在30秒~60秒之间,为什么gc间隔可以增加到300秒以满足第一个if的条件呢? 解答这个问题,我们需要分析gc线程的主函数`gc_thread_func`,如下所示,我们可以知道关键位置是`f2fs_gc`的if判断条件。当f2fs_gc返回值不为0的时候,表示系统无法找到可以找到足够的invalid的block,因此间隔一段较长的时间,积累多一点invalid block再进行gc。 84 | 85 | ```c 86 | static int gc_thread_func(void *data) 87 | { 88 | struct f2fs_sb_info *sbi = data; 89 | struct f2fs_gc_kthread *gc_th = sbi->gc_thread; 90 | wait_queue_head_t *wq = &sbi->gc_thread->gc_wait_queue_head; 91 | long wait_ms; 92 | 93 | wait_ms = gc_th->min_sleep_time; // wait_ms初始化为最小的gc间隔,即30秒 94 | 95 | do { 96 | 97 | if (try_to_freeze()) // 如果线程被挂起了,则continue 98 | continue; 99 | else 100 | wait_event_interruptible_timeout(*wq, 101 | kthread_should_stop(), 102 | msecs_to_jiffies(wait_ms)); // 阻塞while循环wait_ms毫秒 103 | 104 | ... 105 | 106 | if (sbi->sb->s_writers.frozen >= SB_FREEZE_WRITE) { // 如果f2fs冻结了写操作,则表示没有新分配block,因此增加gc间隔,不做gc 107 | increase_sleep_time(gc_th, &wait_ms); 108 | continue; 109 | } 110 | 111 | if (!is_idle(sbi)) { // 如果系统处于不是idle的状态,则表示系统忙,因此为了不影响用户体验,增加间隔的同时也不进行gc 112 | increase_sleep_time(gc_th, &wait_ms); 113 | continue; 114 | } 115 | 116 | if (has_enough_invalid_blocks(sbi)) // 如果系统空间有很多无效(invalid)的block,则减少触发间隔,增加gc的次数 117 | decrease_sleep_time(gc_th, &wait_ms); 118 | else 119 | increase_sleep_time(gc_th, &wait_ms); // 反之则减少gc的次数 120 | /** 121 | * 这里解答了上面的疑问,什么时候会间隔300秒后再gc 122 | * 因为当f2fs_gc返回值不为0的时候,表示系统无法找到可以找到足够的invalid的block,因此间隔一段较长的时间, 123 | * 积累多一点invalid block再进行gc。 124 | **/ 125 | if (f2fs_gc(sbi)) 126 | wait_ms = gc_th->no_gc_sleep_time; 127 | 128 | ... 129 | 130 | } while (!kthread_should_stop()); 131 | return 0; 132 | } 133 | ``` 134 | 135 | ## GC的主流程 136 | 在分析如何回收之前,我们需要知道gc是以什么单位进行回收的。从第一章的f2fs layout的分析可以知道,f2fs的最小单位是block,往上的是segment,再往上是section,最高是zone。gc的回收单位是section,在默认情况下,一个section等于一个segment,因此每回收一个section就回收了512个block。 137 | 138 | 从上面的gc线程主函数`gc_thread_func`可以知道,gc的核心是`f2fs_gc`函数的执行,代码如下。 139 | 140 | 141 | 变量初始化中需要特别关注的是`struct gc_inode_list gc_list `,它的作用是将被gc的section所影响到的inode加入到这个list里面。因为一个section里面并不是所有的block都是invalid的,f2fs也只是会挑选相对比较多的invalid block的section进行gc。因此有一些valid的block需要进行迁移时会影响到它的inode,因此需要将这些inode串起来,集中处理。 142 | 143 | 需要关注的是函数是`__get_victim`函数以及`do_garbage_collect`函数,其中`__get_victim`是根据invalid block的数目以及其它因素等挑选出最适合进行gc的segment,然后传入到`do_garbage_collect`进行gc。`__get_victim`我们用单独一个小节[如何选择victim segment](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-GC/%E9%80%89%E6%8B%A9victim%20segment.md)进行描述,这里先不做分析,只需要理解为挑选出一个合适的segment进行gc。 144 | 145 | 146 | 147 | ```c 148 | int f2fs_gc(struct f2fs_sb_info *sbi) 149 | { 150 | unsigned int segno, i; 151 | int gc_type = BG_GC; 152 | int nfree = 0; 153 | int ret = -1; 154 | struct cp_control cpc; 155 | struct gc_inode_list gc_list = { 156 | .ilist = LIST_HEAD_INIT(gc_list.ilist), 157 | .iroot = RADIX_TREE_INIT(GFP_NOFS), 158 | }; 159 | 160 | gc_more: 161 | 162 | 163 | if (!__get_victim(sbi, &segno, gc_type)) // 挑选出合适的segment,通过segno段号的方式返回 164 | goto stop; // 注意ret现在的值是-1,因此如果f2fs_gc返回不是0的结果,意味着找不出适合的victim segment,对应了上面300秒等待时间的分析 165 | ret = 0; 166 | 167 | for (i = 0; i < sbi->segs_per_sec; i++) // 将整个section里面的segment进行gc,其实1 sec = 1 seg,只会执行一次 168 | do_garbage_collect(sbi, segno + i, &gc_list, gc_type); // 进行gc的主要操作 169 | 170 | 171 | if (has_not_enough_free_secs(sbi, nfree)) 172 | goto gc_more; 173 | 174 | stop: 175 | put_gc_inode(&gc_list); 176 | return ret; 177 | } 178 | ``` 179 | `do_garbage_collect`函数是gc流程的主要操作,作用是根据入参的段号segno找到对应的segment,然后将整个segment读取出来,通过异地更新的方式写入迁移到其他segment中。这样操作以后,被gc的segment会变为一个全新的segment进而可以被系统重新使用,如代码所示: 180 | 181 | 上面提及到f2fs需要使用一个inode list去记录被影响到的inode,那么是如何根据物理地址找到对应的inode呢? 答案是通过`f2fs_summary_block`结构。每一个segment都对应一个`f2fs_summary_block`结构。segment中每一个block都对应了`f2fs_summary_block`结构中的一个entry,记录了这个block地址属于哪个node(通过node id)以及属于这个node的第几个block,更详细的描述参看[f2fs_summary和f2fs_summary_block的介绍和应用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/f2fs_summary.md)。 182 | 183 | 因此,`do_garbage_collect`函数的第一步是根据segno找到对应的`f2fs_summary_block`结构。第二步则是根据gc的数据类型选择`gc_node_segment`函数或者`gc_data_segment`函数实现数据的迁移。 184 | 185 | ```c 186 | static void do_garbage_collect(struct f2fs_sb_info *sbi, unsigned int segno, 187 | struct gc_inode_list *gc_list, int gc_type) 188 | { 189 | struct page *sum_page; 190 | struct f2fs_summary_block *sum; 191 | struct blk_plug plug; 192 | 193 | sum_page = get_sum_page(sbi, segno); // 根据segno获取f2fs_summary_block 194 | 195 | blk_start_plug(&plug); 196 | 197 | sum = page_address(sum_page); 198 | 199 | switch (GET_SUM_TYPE((&sum->footer))) { // 根据类型迁移数据 200 | case SUM_TYPE_NODE: 201 | gc_node_segment(sbi, sum->entries, segno, gc_type); 202 | break; 203 | case SUM_TYPE_DATA: 204 | gc_data_segment(sbi, sum->entries, gc_list, segno, gc_type); 205 | break; 206 | } 207 | blk_finish_plug(&plug); 208 | 209 | stat_inc_seg_count(sbi, GET_SUM_TYPE((&sum->footer))); 210 | stat_inc_call_count(sbi->stat_info); 211 | 212 | f2fs_put_page(sum_page, 1); 213 | } 214 | ``` 215 | `gc_node_segment`函数或者`gc_data_segment`函数的数据迁移步骤大同小异,因此这里只分析`gc_data_segment`函数。 216 | 217 | 函数的第一步是根据segno获取这个segment里面的第一个block的地址,保存在`start_addr`中。然后从这个地址开始,便利这个segment所有的block。`entry = sum`同理,表示第一个block对应的第一个entry。 218 | 219 | 核心循环由于`next_step`的跳转,执行了4次,每一次循环都对应了一个阶段,分别是phase=0~3,通过continue关键词使每一次循环只执行一部分操作。我们每一阶段进行分析。 220 | 221 | **第一阶段(phase=0):** 根据`entry`记录的nid,通过`ra_node_page`函数可以将这个nid对应的node page读入到内存当中。 222 | 223 | **第二阶段(phase=1):** 根据`start_addr`以及`entry`,通过`check_dnode`函数,找到了对应的`struct node_info *dni`,它记录这个block是属于哪一个inode(inode no),然后将对应的inode page读入到内存当中。 224 | 225 | **第三阶段(phase=2):** 首先通过`entry->ofs_in_node`获取到当前block属于node的第几个block,然后通过`start_bidx_of_node`函数获取到当前block是属于从`inode page`开始的第几个block,其实本质上就是`start_bidx + ofs_in_node = page->index`的值。然后根据`page->index`找到对应的`data page`,读入到内存中以便后续使用。最后就是将该inode加入到上面提及过的inode list中。 226 | 227 | **第三阶段(phase=3):** 从inode list中取出一个inode,然后根据`start_bidx + ofs_in_node`找到对应的`page->index`,然后通过`move_data_page`函数,将数据写入到其他segment中。 228 | 229 | 需要注意的是,上述很多的操作都是为了将数据读入内存中,这样系统可以快速地进行下一个步骤。 230 | 231 | 经过上述四个步骤,一个segment的所有数据被迁移了出去,系统就可以将segment回收(即从segment manager中设置这个segment对应的标志位为全新可用状态)。 232 | 233 | 234 | ```c 235 | static void gc_data_segment(struct f2fs_sb_info *sbi, struct f2fs_summary *sum, 236 | struct gc_inode_list *gc_list, unsigned int segno, int gc_type) 237 | { 238 | struct super_block *sb = sbi->sb; 239 | struct f2fs_summary *entry; 240 | block_t start_addr; 241 | int off; 242 | int phase = 0; 243 | 244 | start_addr = START_BLOCK(sbi, segno); 245 | 246 | next_step: 247 | entry = sum; 248 | 249 | for (off = 0; off < sbi->blocks_per_seg; off++, entry++) { 250 | struct page *data_page; 251 | struct inode *inode; 252 | struct node_info dni; /* dnode info for the data */ 253 | unsigned int ofs_in_node, nofs; 254 | block_t start_bidx; 255 | 256 | if (phase == 0) { 257 | ra_node_page(sbi, le32_to_cpu(entry->nid)); // 预读node page 258 | continue; 259 | } 260 | 261 | if (check_dnode(sbi, entry, &dni, start_addr + off, &nofs) == 0) // 找到ino 262 | continue; 263 | 264 | if (phase == 1) { 265 | ra_node_page(sbi, dni.ino); // 预读inode page 266 | continue; 267 | } 268 | 269 | ofs_in_node = le16_to_cpu(entry->ofs_in_node); 270 | 271 | if (phase == 2) { 272 | inode = f2fs_iget(sb, dni.ino); 273 | 274 | start_bidx = start_bidx_of_node(nofs, F2FS_I(inode)); 275 | 276 | data_page = find_data_page(inode, 277 | start_bidx + ofs_in_node, false); // 预读data page 278 | 279 | f2fs_put_page(data_page, 0); 280 | add_gc_inode(gc_list, inode); // 加入到inode list 281 | continue; 282 | } 283 | 284 | /* phase 3 */ 285 | inode = find_gc_inode(gc_list, dni.ino); 286 | if (inode) { 287 | start_bidx = start_bidx_of_node(nofs, F2FS_I(inode)); 288 | data_page = get_lock_data_page(inode, 289 | start_bidx + ofs_in_node); 290 | move_data_page(inode, data_page, gc_type); // 迁移数据 291 | } 292 | } 293 | 294 | if (++phase < 4) 295 | goto next_step; 296 | } 297 | ``` 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | -------------------------------------------------------------------------------- /F2FS-GC/选择victim segment.md: -------------------------------------------------------------------------------- 1 | # victim segment的选择 2 | 在[垃圾回收流程分析](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-GC/GC%E6%B5%81%E7%A8%8B%E4%BB%8B%E7%BB%8D.md)这一节提及到了gc会通过`__get_victim`函数去选择一个需要被gc的segment。怎么样的segment才适合被gc,而F2FS是如何实现这一个流程的呢? 本节对这部分源码进行分析。 3 | 4 | 我们首先分析`__get_victim`函数,如下所示: 5 | ```c 6 | static int __get_victim(struct f2fs_sb_info *sbi, unsigned int *victim, 7 | int gc_type) 8 | { 9 | struct sit_info *sit_i = SIT_I(sbi); 10 | int ret; 11 | 12 | mutex_lock(&sit_i->sentry_lock); 13 | ret = DIRTY_I(sbi)->v_ops->get_victim(sbi, victim, gc_type, 14 | NO_CHECK_TYPE, LFS); 15 | mutex_unlock(&sit_i->sentry_lock); 16 | return ret; 17 | } 18 | ``` 19 | 这里f2fs通过接口的方式提供给用户一个实现自己的垃圾回收算法能力。在默认情况下f2fs通过`get_victim_by_default`实现victim segment的选择: 20 | ```c 21 | static const struct victim_selection default_v_ops = { 22 | .get_victim = get_victim_by_default, 23 | }; 24 | 25 | ``` 26 | 27 | 我们先浏览一个简化的`get_victim_by_default`函数的结构,再逐步分析: 28 | 29 | ```c 30 | static int get_victim_by_default(struct f2fs_sb_info *sbi, 31 | unsigned int *result, int gc_type, int type, char alloc_mode) 32 | { 33 | struct dirty_seglist_info *dirty_i = DIRTY_I(sbi); 34 | struct victim_sel_policy p; 35 | unsigned int secno, max_cost; 36 | int nsearched = 0; 37 | 38 | /* 初始化选择策略 */ 39 | p.alloc_mode = alloc_mode; 40 | select_policy(sbi, gc_type, type, &p); 41 | 42 | p.min_segno = NULL_SEGNO; 43 | p.min_cost = max_cost = get_max_cost(sbi, &p); 44 | 45 | /* 前台gc模式要求快速释放空间,因此不做循环寻找,直接找到之前BG GC的时候所记录下来适合gc的的segment进行gc */ 46 | if (p.alloc_mode == LFS && gc_type == FG_GC) { 47 | p.min_segno = check_bg_victims(sbi); 48 | if (p.min_segno != NULL_SEGNO) 49 | goto got_it; 50 | } 51 | 52 | /* 进入循环,找到一个最小cost的segment */ 53 | while (1) { 54 | unsigned long cost; 55 | unsigned int segno; 56 | 57 | /* 从map里面找到一个dirty的segment所以对应segno出来 */ 58 | segno = find_next_bit(p.dirty_segmap, MAIN_SEGS(sbi), p.offset); 59 | 60 | p.offset = segno + p.ofs_unit; 61 | if (p.ofs_unit > 1) 62 | p.offset -= segno % p.ofs_unit; 63 | 64 | secno = GET_SECNO(sbi, segno); 65 | 66 | /* 计算当前segment的cost */ 67 | cost = get_gc_cost(sbi, segno, &p); 68 | 69 | /* 判断更新最小cost */ 70 | if (p.min_cost > cost) { 71 | p.min_segno = segno; 72 | p.min_cost = cost; 73 | } else if (unlikely(cost == max_cost)) { 74 | continue; 75 | } 76 | 77 | /* 达到了最大搜索次数即退出 */ 78 | if (nsearched++ >= p.max_search) { 79 | sbi->last_victim[p.gc_mode] = segno; 80 | break; 81 | } 82 | } 83 | if (p.min_segno != NULL_SEGNO) { 84 | got_it: 85 | if (p.alloc_mode == LFS) { 86 | secno = GET_SECNO(sbi, p.min_segno); 87 | if (gc_type == FG_GC) 88 | sbi->cur_victim_sec = secno; 89 | else 90 | set_bit(secno, dirty_i->victim_secmap); // BG_GC的情况下设定这个map,给FG_GC快速寻找segment进行gc 91 | } 92 | *result = (p.min_segno / p.ofs_unit) * p.ofs_unit; // 返回结果 93 | } 94 | 95 | return (p.min_segno == NULL_SEGNO) ? 0 : 1; 96 | } 97 | ``` 98 | ### 设置victim segment选择策略 99 | 第一步设置`victim_sel_policy`,即victim segment选择策略: 100 | ```c 101 | struct victim_sel_policy p; 102 | p.alloc_mode = alloc_mode; // 设置分配模式,一般为LFS 103 | select_policy(sbi, gc_type, type, &p); // 根据gc类型等信息,设定选择策略 104 | p.min_segno = NULL_SEGNO; 105 | p.min_cost = max_cost = get_max_cost(sbi, &p); // 根据不同的policy,设定最大回收cost 106 | ``` 107 | 其中 108 | ```c 109 | static void select_policy(struct f2fs_sb_info *sbi, int gc_type, 110 | int type, struct victim_sel_policy *p) 111 | { 112 | struct dirty_seglist_info *dirty_i = DIRTY_I(sbi); 113 | 114 | if (p->alloc_mode == SSR) { 115 | p->gc_mode = GC_GREEDY; 116 | p->dirty_segmap = dirty_i->dirty_segmap[type]; 117 | p->max_search = dirty_i->nr_dirty[type]; 118 | p->ofs_unit = 1; 119 | } else { 120 | p->gc_mode = select_gc_type(sbi->gc_thread, gc_type); // 赋予gc算法类型,默认有两种算法,即greedy算法(GC_GREEDY),以及cost-benefit算法(GC_CB) 121 | p->dirty_segmap = dirty_i->dirty_segmap[DIRTY]; // 即脏的segment所对应的segmap,它的作用是标记了这个segmen里面的block有效无效信息 122 | p->max_search = dirty_i->nr_dirty[DIRTY]; // 表示最大搜索次数,等于当前有多少个dirty的segment 123 | p->ofs_unit = sbi->segs_per_sec; // 1 sec = 1 seg ,所以等于1 124 | } 125 | 126 | if (p->max_search > sbi->max_victim_search) 127 | p->max_search = sbi->max_victim_search; 128 | 129 | p->offset = sbi->last_victim[p->gc_mode]; // 上一个被回收的segment 130 | } 131 | ``` 132 | 133 | ### 查找最小cost的segment 134 | 在分析下一步循环之前,我们要明确循环的目的是什么。f2fs通过变量**cost**表示gc这个segment所带来的开销,cost的值根据不同的算法也不一样。`get_victim_by_default`函数目的是找到一个cost最低的segment进行回收,因此在找到之前需要设定一个最大cost,用于一步步遍历降低cost。 135 | ```c 136 | while (1) { 137 | unsigned long cost; 138 | unsigned int segno; 139 | 140 | /* 从map里面找到一个dirty的segment所以对应segno出来 */ 141 | segno = find_next_bit(p.dirty_segmap, MAIN_SEGS(sbi), p.offset); 142 | 143 | p.offset = segno + p.ofs_unit; 144 | if (p.ofs_unit > 1) 145 | p.offset -= segno % p.ofs_unit; 146 | 147 | secno = GET_SECNO(sbi, segno); 148 | 149 | /* 计算当前segment的cost */ 150 | cost = get_gc_cost(sbi, segno, &p); 151 | 152 | /* 判断更新最小cost */ 153 | if (p.min_cost > cost) { 154 | p.min_segno = segno; 155 | p.min_cost = cost; 156 | } else if (unlikely(cost == max_cost)) { 157 | continue; 158 | } 159 | 160 | /* 达到了最大搜索次数即退出 */ 161 | if (nsearched++ >= p.max_search) { 162 | sbi->last_victim[p.gc_mode] = segno; 163 | break; 164 | } 165 | } 166 | ``` 167 | 首先通过`find_next_bit` 函数,从`p.dirty_segmap`找到第一个dirty的segment出来,然后通过`get_gc_cost`函数计算这个segment的cost。然后判断是否少于`p.min_cost`,然后赋值。最后循环到达了最大搜索次数以后旧退出。此时的segment及时本次选择过程中得到的cost最小的segment,最后返回到*result中,完成这个victim segment选择流程。 168 | 169 | ### cost算法 170 | F2FS使用了两种计算cost的算法,分别是Greedy算法和Cost-Benefit算法。 171 | 172 | ```c 173 | static inline unsigned int get_gc_cost(struct f2fs_sb_info *sbi, 174 | unsigned int segno, struct victim_sel_policy *p) 175 | { 176 | if (p->alloc_mode == SSR) 177 | return get_seg_entry(sbi, segno)->ckpt_valid_blocks; 178 | 179 | /* alloc_mode == LFS */ 180 | if (p->gc_mode == GC_GREEDY) 181 | return get_valid_blocks(sbi, segno, sbi->segs_per_sec); // Greedy算法,valid block越多表示cost越大,越不值得gc 182 | else 183 | return get_cb_cost(sbi, segno); // Cost-Benefit算法,这个是考虑了访问时间和valid block开销的算法 184 | } 185 | ``` 186 | 由于是通过cost值表示是否值得被gc,因此cost值是越小越好。 187 | 188 | **Greedy算法** 189 | 顾名思义,会选择invalid block最多(valid block最少)的segment进行gc,实现如下: 190 | ```c 191 | static inline unsigned int get_valid_blocks(struct f2fs_sb_info *sbi, 192 | unsigned int segno, int section) 193 | { 194 | if (section > 1) 195 | return get_sec_entry(sbi, segno)->valid_blocks; 196 | else 197 | return get_seg_entry(sbi, segno)->valid_blocks; 198 | } 199 | ``` 200 | 由于是通过cost来描述开销,因此这里valid_block越多,表示开销越大。 201 | 202 | **Cost-Benefit算法** 203 | Cost-Benefit算法是一个同时考虑最近一次修改时间以及invalid block个数的算法。因为相当于频繁修改的数据而言,不值得进行GC,因为GC完很快就修改了,同时由于异地更新的特性,导致继续产生invalid block。较长时间未作修改的数据,可以认为迁移以后也相对没那么频繁继续产生invalid block。Cost-Benefit算法的核心是: 204 | ``` 205 | cost = (1 - u) / 2u * age 206 | 其中 207 | u: 表示valid block在该section中的比例,age代表该section最近一次修改的时间 208 | 1-u: 表示对这个section进行gc后的收益 209 | 2u: 则表示开销(读+写) 210 | age: 表示上一次修改时间 211 | ``` 212 | 因此我们可以将Cost-Benefit算法理解为一个平衡invalid block数目以及修改时间的的一个算法,在f2fs的实现如下: 213 | ```c 214 | static unsigned int get_cb_cost(struct f2fs_sb_info *sbi, unsigned int segno) 215 | { 216 | struct sit_info *sit_i = SIT_I(sbi); 217 | unsigned int secno = GET_SECNO(sbi, segno); 218 | unsigned int start = secno * sbi->segs_per_sec; 219 | unsigned long long mtime = 0; 220 | unsigned int vblocks; 221 | unsigned char age = 0; 222 | unsigned char u; 223 | unsigned int i; 224 | 225 | for (i = 0; i < sbi->segs_per_sec; i++) 226 | mtime += get_seg_entry(sbi, start + i)->mtime; // 计算section里面的每一个segment最近一次访问时间 227 | vblocks = get_valid_blocks(sbi, segno, sbi->segs_per_sec); // 获取当前的section有多少个valid block 228 | 229 | mtime = div_u64(mtime, sbi->segs_per_sec); // 计算平均每一segment的最近一次访问时间 230 | vblocks = div_u64(vblocks, sbi->segs_per_sec); // 计算平均每一个segment的valid block个数 231 | 232 | u = (vblocks * 100) >> sbi->log_blocks_per_seg; // 百分比计算所以乘以100,然后计算得到了valid block的比例 233 | 234 | /* sit_i->min_mtime以及sit_i->max_mtime计算的是每一次gc的时候的最小最大的修改时间,因此通过这个比例计算这个section的修改时间在总体的情况下的表现 */ 235 | if (mtime < sit_i->min_mtime) 236 | sit_i->min_mtime = mtime; 237 | if (mtime > sit_i->max_mtime) 238 | sit_i->max_mtime = mtime; 239 | if (sit_i->max_mtime != sit_i->min_mtime) 240 | age = 100 - div64_u64(100 * (mtime - sit_i->min_mtime), 241 | sit_i->max_mtime - sit_i->min_mtime); 242 | 243 | return UINT_MAX - ((100 * (100 - u) * age) / (100 + u)); 244 | } 245 | ``` 246 | 第一步先计算一个section平均每一个segment的valid block数目是多少,然后计算valid block的比例。 247 | 第二步计算上一次修改时间占据最大最小的修改时间的比例。 248 | 最后一步,公式`((100 * (100 - u) * age) / (100 + u))`即对应`(1 - u) / 2u * age`,使用UINT_MAX减去这个值的原因是f2fs要维持cost越高,越不值得被gc的特征。 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | -------------------------------------------------------------------------------- /F2FS-Layout/Checkpoint结构.md: -------------------------------------------------------------------------------- 1 | # Checkpoint区域 2 | Checkpoint是维护F2FS的数据一致性的结构,它维护了系统当前的状态,例如segment的分配情况,node的分配情况,以及当前的active segment的状态等。F2FS在满足一定的条件的情况下,会将当前系统的分配状态写入到Checkpoint中,万一系统出现突然宕机,这个是F2FS可以从Checkpoint中恢复到上次回写时的状态,以保证数据的可恢复性。F2FS维护了两个Checkpoint结构,互为备份,其中一个是当前正在使用的Checkpoint,另外一个上次回写的稳定的Chcekpoint。如果系统出现了宕机,那么当前的Checkpoint就会变得不可信任,进而使用备份Checkpoint进行恢复。 3 | 4 | 5 | ## Checkpoint在元数据区域的物理结构 6 | ![cp_layout](../img/F2FS-Layout/cp_layout2.png) 7 | 8 | 根据上述的结构图,Checkpoint区域由几个部分构成,分别是checkpoint元数据区域(f2fs_checkpoint)、orphan node区域、active segments区域。同时active segments区域在不同的情况下,会有不同的形式,目的是减少IO的写入。接下来分别讨论Checkpoint不同的部分。 9 | 10 | ### Checkpoint元数据区域 11 | F2FS使用数据结构`f2fs_checkpoint`表示Checkpoint结构,它保存在磁盘中`f2fs_super_block`之后区域中,数据结构如下。需要特别注意的是`cur_node_segno`、`cur_node_blkoff`、`cur_data_segno`、`cur_data_blkoff`这几个变量。第一节提到,F2FS分为了6个log区域,分别对应hot node/data、warm node/data、cold node/data。F2FS必须定时执行Checkpoint去记录当前系统的log分配到哪个位置,否则在系统宕机的时候,会出现数据丢失等一致性问题,因此`cur_xxx_segno`以及`cur_xxx_blkoff`记录了上次Checkpoint时,系统正在使用的log的segment number,以及分配到这个segment的哪个位置。 12 | ```c 13 | struct f2fs_checkpoint { 14 | __le64 checkpoint_ver; /* CP版本,用于比较新旧版本进行恢复 */ 15 | __le64 user_block_count; /* # of user blocks */ 16 | __le64 valid_block_count; /* # of valid blocks in main area */ 17 | __le32 rsvd_segment_count; /* # of reserved segments for gc */ 18 | __le32 overprov_segment_count; /* # of overprovision segments */ 19 | __le32 free_segment_count; /* # of free segments in main area */ 20 | 21 | /* information of current node segments */ 22 | __le32 cur_node_segno[MAX_ACTIVE_NODE_LOGS]; 23 | __le16 cur_node_blkoff[MAX_ACTIVE_NODE_LOGS]; 24 | /* information of current data segments */ 25 | __le32 cur_data_segno[MAX_ACTIVE_DATA_LOGS]; 26 | __le16 cur_data_blkoff[MAX_ACTIVE_DATA_LOGS]; 27 | __le32 ckpt_flags; /* Flags : umount and journal_present */ 28 | __le32 cp_pack_total_block_count; /* total # of one cp pack */ 29 | __le32 cp_pack_start_sum; /* start block number of data summary */ 30 | __le32 valid_node_count; /* Total number of valid nodes */ 31 | __le32 valid_inode_count; /* Total number of valid inodes */ 32 | __le32 next_free_nid; /* Next free node number */ 33 | __le32 sit_ver_bitmap_bytesize; /* Default value 64 */ 34 | __le32 nat_ver_bitmap_bytesize; /* Default value 256 */ 35 | __le32 checksum_offset; /* checksum offset inside cp block */ 36 | __le64 elapsed_time; /* mounted time */ 37 | /* allocation type of current segment */ 38 | unsigned char alloc_type[MAX_ACTIVE_LOGS]; 39 | 40 | /* SIT and NAT version bitmap */ 41 | unsigned char sit_nat_version_bitmap[1]; 42 | } __packed; 43 | ``` 44 | 45 | ### Orphan node区域 46 | 这是一个动态的区域,如果没有orphan node list则不会占用空间。 47 | 48 | 49 | ### Active Segments区域 50 | #### Active Segments的定义 51 | Active Segments,又称current segment(CURSEG),即当前正在用于进行数据分配的log对应的segment,如用户需要写入8KB数据,那么就会从active segments分配两个block提供给用户写入到磁盘中。F2FS为了提高数据分配的效率,根据数据的特性,一共定义了6个active segment。如[总体结构](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/%E6%80%BB%E4%BD%93%E7%BB%93%E6%9E%84.md)这一节提到的multi-head logging特性所描述,这6个active segments对应了(how, warm, cold) X (node, data)的数据。 52 | 53 | #### Active Segment与恢复相关的数据结构 54 | CP的主要任务是维护数据一致性,因此CP的active segment区域的主要任务是维护Active Segment的分配状态,使系统宕机时候可以恢复正常。维护active segment需要维护三种信息,分别是`f2fs_checkpoint`的信息,以及该segment对应的journal和summary的信息。 55 | 56 | - **f2fs_checkpoint中Active Segment信息**:从上面给出的`f2fs_checkpoint`定义,`cur_node_segno[MAX_ACTIVE_NODE_LOGS]`和`cur_data_segno[MAX_ACTIVE_DATA_LOGS]`表示node和data当前的Active Segment的编号(segment number, segno),系统可以通过这个编号找到对应的segment。`MAX_ACTIVE_NODE_LOGS`以及`MAX_ACTIVE_NODE_LOGS`分别表示data和node有多少种类型,F2FS默认情况下都等于3,表示、、即HOT、WARM、COLD类型数据。`cur_node_blkoff[MAX_ACTIVE_NODE_LOGS]`以及`cur_data_blkoff[MAX_ACTIVE_DATA_LOGS]`则分别表示当前active segment分配到哪一个block(一个segment包含了512个block)。 57 | 58 | - **Segment对应的Journal信息**:Journal在两处地方都有出现,分别是CP区域以及SSA区域。CP区域的journal主要用来保存**active segment**的修改信息,而SSA区域的则是持久化保存的**所有的segment**的journal信息。如系统分配出一个block给用户,那么就要将这个block所在的segment的bitmap中标记为已分配,防止其他写请求使用。分两个区域存放journal是为了减轻频繁更新导致的系统性能下降。例如,当系统写压力很大的时候,bitmap就会频繁被更新,如果这个时候频繁将bitmap写入SSA,就会加重写压力。因此CP区域的Journal的作用就是维护这些经常修改的数据,等待CP被触发的时候才回写到闪存设备,从而减少写压力,提高闪存寿命。 59 | - **Segment对应的Summary信息**:summary同样在CP区域和SSA区域有出现,它表示的是逻辑地址和物理地址的映射关系,这个映射关系会使用到GC流程中。summary与segment是一对一的关系,一个summary保存了一个segment所有的block的物理地址和逻辑地址的映射关系。summary保存在CP区域中同样是出于减少IO的写入。 60 | 61 | 62 | ## Checkpoint内存管理结构 63 | Checkpoint的内存管理结构是`struct f2fs_checkpoint`本身,因为Checkpoint一般只在F2FS启动的时候被读取数据,用于数据恢复,而在运行过程中大部分情况都是被写,用于记录恢复信息。因此,Checkpoint不需要过于复杂的内存管理结构,因此使用`struct f2fs_checkpoint`本身即可以满足需求。 64 | 65 | 另一方面,active segments,即F2FS的log,主要用于系统free block的分配,因此需要特定的管理结构`struct curseg_info`进行管理,它的定义如下: 66 | ```c 67 | struct curseg_info { 68 | struct mutex curseg_mutex; 69 | struct f2fs_summary_block *sum_blk; /* 每一个segment对应一个summary block */ 70 | struct rw_semaphore journal_rwsem; 71 | struct f2fs_journal *journal; /*每一个segment对应一个 info */ 72 | unsigned char alloc_type; 73 | unsigned int segno; /* 当前segno */ 74 | unsigned short next_blkoff; /* 记录当前segment用于分配的下一个给block号 */ 75 | unsigned int zone; /* current zone number */ 76 | unsigned int next_segno; /* 当前segno用完以后,下个即将用来分配的segno */ 77 | }; 78 | ``` 79 | 从结构分析可以直到,`curseg_info`记录当前的segment的分配信息,当系统出现宕机的时候,可以从CP记录的`curseg_info`恢复当上一次CP点的状态。 80 | 81 | 每一种类型的active segment就对应一个`struct curseg_info`结构。在F2FS中,使用一个数组来表示: 82 | ```c 83 | struct f2fs_sm_info { 84 | ... 85 | struct curseg_info *curseg_array; // 默认是分配6个curseg_info,分别对应不同类型 86 | ... 87 | } 88 | ``` 89 | `struct f2fs_sm_info`是SIT的管理结构,它也管理了CP最终的active segment的信息,是一个跨区域的管理结构。 90 | 91 | 92 | `struct f2fs_checkpoint`通过`get_checkpoint_version`函数从磁盘读取出来: 93 | ```c 94 | static int get_checkpoint_version(struct f2fs_sb_info *sbi, block_t cp_addr, 95 | struct f2fs_checkpoint **cp_block, struct page **cp_page, 96 | unsigned long long *version) 97 | { 98 | unsigned long blk_size = sbi->blocksize; 99 | size_t crc_offset = 0; 100 | __u32 crc = 0; 101 | 102 | *cp_page = f2fs_get_meta_page(sbi, cp_addr); // 根据CP所在的地址cp_addr从磁盘读取一个block 103 | *cp_block = (struct f2fs_checkpoint *)page_address(*cp_page); // 直接转换为数据结构 104 | 105 | crc_offset = le32_to_cpu((*cp_block)->checksum_offset); 106 | if (crc_offset > (blk_size - sizeof(__le32))) { 107 | f2fs_msg(sbi->sb, KERN_WARNING, 108 | "invalid crc_offset: %zu", crc_offset); 109 | return -EINVAL; 110 | } 111 | 112 | crc = cur_cp_crc(*cp_block); 113 | if (!f2fs_crc_valid(sbi, crc, *cp_block, crc_offset)) { // 比较CRC的值,进而知道是否成功读取出来 114 | f2fs_msg(sbi->sb, KERN_WARNING, "invalid crc value"); 115 | return -EINVAL; 116 | } 117 | 118 | *version = cur_cp_version(*cp_block); 119 | return 0; 120 | } 121 | ``` 122 | 123 | `struct curseg_info`则是通过`build_curseg`函数进行初始化: 124 | ```c 125 | static int build_curseg(struct f2fs_sb_info *sbi) 126 | { 127 | struct curseg_info *array; 128 | int i; 129 | 130 | array = f2fs_kzalloc(sbi, array_size(NR_CURSEG_TYPE, sizeof(*array)), 131 | GFP_KERNEL); // 根据active segment类型的数目分配空间 132 | if (!array) 133 | return -ENOMEM; 134 | 135 | SM_I(sbi)->curseg_array = array; // 赋值到f2fs_sm_info->curseg_array 136 | 137 | for (i = 0; i < NR_CURSEG_TYPE; i++) { // 为curseg的其他信息分配空间 138 | mutex_init(&array[i].curseg_mutex); 139 | array[i].sum_blk = f2fs_kzalloc(sbi, PAGE_SIZE, GFP_KERNEL); 140 | if (!array[i].sum_blk) 141 | return -ENOMEM; 142 | init_rwsem(&array[i].journal_rwsem); 143 | array[i].journal = f2fs_kzalloc(sbi, 144 | sizeof(struct f2fs_journal), GFP_KERNEL); 145 | if (!array[i].journal) 146 | return -ENOMEM; 147 | array[i].segno = NULL_SEGNO; 148 | array[i].next_blkoff = 0; 149 | } 150 | return restore_curseg_summaries(sbi); // 从f2fs_checkpoint恢复上一个CP点CURSEG的状态 151 | } 152 | 153 | static int restore_curseg_summaries(struct f2fs_sb_info *sbi) 154 | { 155 | struct f2fs_journal *sit_j = CURSEG_I(sbi, CURSEG_COLD_DATA)->journal; 156 | struct f2fs_journal *nat_j = CURSEG_I(sbi, CURSEG_HOT_DATA)->journal; 157 | int type = CURSEG_HOT_DATA; 158 | int err; 159 | 160 | ... 161 | for (; type <= CURSEG_COLD_NODE; type++) { // 按类型逐个恢复active segment的信息 162 | err = read_normal_summaries(sbi, type); 163 | if (err) 164 | return err; 165 | } 166 | ... 167 | 168 | return 0; 169 | } 170 | 171 | static int read_normal_summaries(struct f2fs_sb_info *sbi, int type) 172 | { 173 | struct f2fs_checkpoint *ckpt = F2FS_CKPT(sbi); 174 | struct f2fs_summary_block *sum; 175 | struct curseg_info *curseg; 176 | struct page *new; 177 | unsigned short blk_off; 178 | unsigned int segno = 0; 179 | block_t blk_addr = 0; 180 | 181 | ... 182 | segno = le32_to_cpu(ckpt->cur_data_segno[type]); // 从CP读取segno 183 | blk_off = le16_to_cpu(ckpt->cur_data_blkoff[type - CURSEG_HOT_DATA]); // 从CP读取blk_off 184 | blk_addr = sum_blk_addr(sbi, NR_CURSEG_DATA_TYPE, type); // 获取summary block地址 185 | 186 | // 读取&转换结构 187 | new = f2fs_get_meta_page(sbi, blk_addr); 188 | sum = (struct f2fs_summary_block *)page_address(new); 189 | 190 | curseg = CURSEG_I(sbi, type); // 根据type找到对应的curseg 191 | mutex_lock(&curseg->curseg_mutex); 192 | 193 | /* 复制&恢复数据 */ 194 | down_write(&curseg->journal_rwsem); 195 | memcpy(curseg->journal, &sum->journal, SUM_JOURNAL_SIZE); 196 | up_write(&curseg->journal_rwsem); 197 | 198 | memcpy(curseg->sum_blk->entries, sum->entries, SUM_ENTRY_SIZE); 199 | memcpy(&curseg->sum_blk->footer, &sum->footer, SUM_FOOTER_SIZE); 200 | curseg->next_segno = segno; 201 | reset_curseg(sbi, type, 0); 202 | curseg->alloc_type = ckpt->alloc_type[type]; 203 | curseg->next_blkoff = blk_off; // 恢复上次的分配状态 204 | mutex_unlock(&curseg->curseg_mutex); 205 | f2fs_put_page(new, 1); 206 | return 0; 207 | } 208 | ``` 209 | 210 | 211 | -------------------------------------------------------------------------------- /F2FS-Layout/Node Address Table结构.md: -------------------------------------------------------------------------------- 1 | # Node Address Table区域-NAT结构 2 | 3 | Node Address Table,简称NAT,是F2FS用于集中管理node的结构。它的主要维护了一张表(如下图),记录了每一个node在flash设备的物理地址。F2FS给每一个node分配了一个node ID(nid),系统可以根据nid从NAT查找到该node在flash设备上的物理地址,然后从flash设备读取出来。 4 | 5 | ![node-table](../img/F2FS-Layout/node-table.png) 6 | 7 | ## NAT在元数据区域的物理结构 8 | 9 | ![node-table](../img/F2FS-Layout/nat_layout.png) 10 | 11 | 如上图所示,NAT区域由N个`struct f2fs_nat_block`组成,每一个`struct f2fs_nat_block`包含了455个`struct f2fs_nat_entry`。每一个nid对应了一个entry,每一个entry记录了这个node的在flash设备上的物理地址block_addr。同时entry也记录了一个ino的值,这个值用于找到这个node的parent node,如果nid=ino则表示这个node是inode,如果nid != ino,则表示这是一个direct_node或者indrect_node(node的概念参考第二章的第一节)。version变量用于系统恢复。 12 | 13 | 14 | 15 | ## NAT内存管理结构 16 | 17 | NAT在内存中对应的管理结构是`struct f2fs_nm_info`,它在` build_node_manager`函数进行初始化(如下)。`struct f2fs_nm_info`不会将所有的NAT的数据都读取出来,而是读取NAT的一部分,然后构建free nid表,用于给新的node分配nid。 18 | 19 | ```c 20 | int build_node_manager(struct f2fs_sb_info *sbi) 21 | { 22 | int err; 23 | 24 | /* 分配空间 */ 25 | sbi->nm_info = kzalloc(sizeof(struct f2fs_nm_info), GFP_KERNEL); 26 | if (!sbi->nm_info) 27 | return -ENOMEM; 28 | 29 | /* 初始化sbi->nm_info的信息 */ 30 | err = init_node_manager(sbi); 31 | if (err) 32 | return err; 33 | 34 | /* 构建free nids表,用于给新的node分配nid */ 35 | build_free_nids(sbi); 36 | return 0; 37 | } 38 | ``` 39 | 40 | `init_node_manager`函数主要用于初始化`sbi->nm_info`内的变量信息: 41 | 42 | ```c 43 | static int init_node_manager(struct f2fs_sb_info *sbi) 44 | { 45 | struct f2fs_super_block *sb_raw = F2FS_RAW_SUPER(sbi); 46 | struct f2fs_nm_info *nm_i = NM_I(sbi); 47 | unsigned char *version_bitmap; 48 | unsigned int nat_segs, nat_blocks; 49 | 50 | /* NAT表在flash设备的起始物理地址 */ 51 | nm_i->nat_blkaddr = le32_to_cpu(sb_raw->nat_blkaddr); 52 | 53 | /* segment_count_nat includes pair segment so divide to 2. */ 54 | nat_segs = le32_to_cpu(sb_raw->segment_count_nat) >> 1; 55 | 56 | /* NAT区域包含了多少个block,从nm_i->nat_blkaddr开始的nat_blocks就是NAT表的存储空间 */ 57 | nat_blocks = nat_segs << le32_to_cpu(sb_raw->log_blocks_per_seg); 58 | 59 | /* 最大的nid的值 */ 60 | nm_i->max_nid = NAT_ENTRY_PER_BLOCK * nat_blocks; 61 | 62 | /* not used nids: 0, node, meta, (and root counted as valid node) */ 63 | nm_i->available_nids = nm_i->max_nid - F2FS_RESERVED_NODE_NUM; 64 | nm_i->fcnt = 0; 65 | nm_i->nat_cnt = 0; 66 | nm_i->ram_thresh = DEF_RAM_THRESHOLD; 67 | nm_i->ra_nid_pages = DEF_RA_NID_PAGES; 68 | nm_i->dirty_nats_ratio = DEF_DIRTY_NAT_RATIO_THRESHOLD; 69 | 70 | /* list + radix tree就是构建了类似 HashMap的索引结构,用于缓存free nid的entry */ 71 | INIT_RADIX_TREE(&nm_i->free_nid_root, GFP_ATOMIC); 72 | INIT_LIST_HEAD(&nm_i->free_nid_list); 73 | INIT_RADIX_TREE(&nm_i->nat_root, GFP_NOIO); 74 | INIT_RADIX_TREE(&nm_i->nat_set_root, GFP_NOIO); 75 | INIT_LIST_HEAD(&nm_i->nat_entries); 76 | 77 | mutex_init(&nm_i->build_lock); 78 | spin_lock_init(&nm_i->free_nid_list_lock); 79 | init_rwsem(&nm_i->nat_tree_lock); 80 | 81 | /* 每次一次分配nid的时候,就会更新这个next_scan_nid的值,下次就可以从这里开始搜索 */ 82 | nm_i->next_scan_nid = le32_to_cpu(sbi->ckpt->next_free_nid); 83 | nm_i->bitmap_size = __bitmap_size(sbi, NAT_BITMAP); 84 | version_bitmap = __bitmap_ptr(sbi, NAT_BITMAP); 85 | if (!version_bitmap) 86 | return -EFAULT; 87 | 88 | nm_i->nat_bitmap = kmemdup(version_bitmap, nm_i->bitmap_size, 89 | GFP_KERNEL); 90 | if (!nm_i->nat_bitmap) 91 | return -ENOMEM; 92 | return 0; 93 | } 94 | ``` 95 | 96 | `build_free_nids`主要用于构建free nid表,用于给新的node分配nid。 为了节省内存,F2FS不会将NAT中所有的free nid读取出来,只会读取一部分,因此使用`nm_i->fcnt`表示缓存了多少个free nid。然后会读取一定的数目的`f2fs_nat_block`出来,并遍历其中的每一个`f2fs_nat_entry`,加入到free nid的管理结构中。最后还会搜索一下log区域的free nid信息(参考Checkpoint区域一节),也加入到free nid管理结构中。 97 | 98 | ```c 99 | void build_free_nids(struct f2fs_sb_info *sbi) 100 | { 101 | struct f2fs_nm_info *nm_i = NM_I(sbi); 102 | struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_HOT_DATA); 103 | struct f2fs_journal *journal = curseg->journal; 104 | int i = 0; 105 | nid_t nid = nm_i->next_scan_nid; // next_scan_nid的含义上面有介绍,从这里开始搜索free nid 106 | 107 | /* * 108 | * 为了节省内存,F2FS不会将NAT中所有的free nid读取出来,只会读取一部分 109 | * fcnt表示目前缓存了多少个free nid,如果大于NAT_ENTRY_PER_BLOCK,则不再缓存了 110 | */ 111 | if (nm_i->fcnt >= NAT_ENTRY_PER_BLOCK) 112 | return; 113 | 114 | /* 115 | * 因为准备开始读取NAT的page(block),因此根据nid(next_scan_nid)的所在的block开始, 116 | * 读取FREE_NID_PAGES(=8)个page进入内存 117 | */ 118 | ra_meta_pages(sbi, NAT_BLOCK_OFFSET(nid), FREE_NID_PAGES, 119 | META_NAT, true); 120 | 121 | down_read(&nm_i->nat_tree_lock); 122 | 123 | while (1) { 124 | struct page *page = get_current_nat_page(sbi, nid); 125 | 126 | /* 127 | * 读取当前nid所在的f2fs_nat_block(page), 128 | * 然后将free nid加入到nm_i->free_nid_list/root中 129 | */ 130 | scan_nat_page(sbi, page, nid); 131 | 132 | /* 释放当前的f2fs_nat_block对应的page */ 133 | f2fs_put_page(page, 1); 134 | 135 | /* 已经读取了一个f2fs_nat_block,自然要跳到下一个f2fs_nat_block的第一个nid */ 136 | nid += (NAT_ENTRY_PER_BLOCK - (nid % NAT_ENTRY_PER_BLOCK)); 137 | if (unlikely(nid >= nm_i->max_nid)) 138 | nid = 0; 139 | 140 | /* 所有block读完之后就跳出循环 */ 141 | if (++i >= FREE_NID_PAGES) 142 | break; 143 | } 144 | 145 | /* 更新next_scan_nid,前面的已经扫描过了,下一次从这个nid开始扫描 */ 146 | nm_i->next_scan_nid = nid; 147 | 148 | /* 遍历log的nat_journal记录的nat_entry信息,从中寻找free nid */ 149 | down_read(&curseg->journal_rwsem); 150 | for (i = 0; i < nats_in_cursum(journal); i++) { 151 | block_t addr; 152 | /* 从journal中获取nid信息 */ 153 | nid = le32_to_cpu(nid_in_journal(journal, i)); 154 | /* 从journal中获取该nid对应的物理地址 */ 155 | addr = le32_to_cpu(nat_in_journal(journal, i).block_addr); 156 | 157 | /* addr==NULL_ADDR 表示这个nid没有被文件使用,因此加入free nid,否则去除free nid */ 158 | if (addr == NULL_ADDR) 159 | add_free_nid(sbi, nid, true); 160 | else 161 | remove_free_nid(nm_i, nid); 162 | } 163 | up_read(&curseg->journal_rwsem); 164 | up_read(&nm_i->nat_tree_lock); 165 | 166 | ra_meta_pages(sbi, NAT_BLOCK_OFFSET(nm_i->next_scan_nid), 167 | nm_i->ra_nid_pages, META_NAT, false); 168 | } 169 | ``` 170 | 171 | `scan_nat_page`函数的作用是扫描当前的`f2fs_nat_block`的每一个entry,并找到其中的free nid,加入到`nm_i`的free nid管理结构中。 172 | 173 | ```c 174 | static void scan_nat_page(struct f2fs_sb_info *sbi, 175 | struct page *nat_page, nid_t start_nid) 176 | { 177 | struct f2fs_nm_info *nm_i = NM_I(sbi); 178 | struct f2fs_nat_block *nat_blk = page_address(nat_page); 179 | block_t blk_addr; 180 | int i; 181 | 182 | /* 找到start_nid的值是属于f2fs_nat_block的第几个entry */ 183 | i = start_nid % NAT_ENTRY_PER_BLOCK; 184 | 185 | /* 186 | * 从这个entry开始遍历, 187 | * 如果blk_addr == NULL_ADDR则通过add_free_nid函数加入free nid的管理结构中, 188 | * 同时增加nm_i->fcnt的值 189 | */ 190 | for (; i < NAT_ENTRY_PER_BLOCK; i++, start_nid++) { 191 | 192 | if (unlikely(start_nid >= nm_i->max_nid)) 193 | break; 194 | 195 | blk_addr = le32_to_cpu(nat_blk->entries[i].block_addr); 196 | f2fs_bug_on(sbi, blk_addr == NEW_ADDR); 197 | if (blk_addr == NULL_ADDR) { 198 | if (add_free_nid(sbi, start_nid, true) < 0) 199 | break; 200 | } 201 | } 202 | } 203 | ``` 204 | 205 | -------------------------------------------------------------------------------- /F2FS-Layout/Segment Infomation Table结构.md: -------------------------------------------------------------------------------- 1 | # Segment Infomation Table区域-SIT结构 2 | Segment Infomation Table,简称SIT,是F2FS用于集中管理segment状态的结构。它的主要作用是维护的segment的分配信息,它的作用可以使用两个常见例子进行描述: 3 | 4 | - 用户进行写操作,那么segment会根据用户写入的数据量分配特定数目的block给用户进行数据写入,SIT会将这些已经被分配的block标记为"已经使用(valid状态)",那么之后的写操作就不会再使用这些block。 5 | - 用户进行了**覆盖写**操作以后,由于F2FS**异地更新**的特性,F2FS会分配新block给用户写入,同时会将旧block置为"无效状态(invalid状态)",这样gc的时候可以根据segment无效的block的数目,采取某种策略进行回收。 6 | 7 | 综上所述,SIT的作用是维护每一个segment的block的使用状态以及有效无效状态。 8 | 9 | ## SIT在元数据区域的物理结构 10 | ![cp_layout](../img/F2FS-Layout/sit_layout2.png) 11 | 12 | 如上图所示,SIT区域由N个`struct f2fs_sit_block`组成,每一个`struct f2fs_sit_block`包含了55个`struct f2fs_sit_entry`,每一个entry对应了一个segment的管理状态。每一个entry包含了三个变量: vblocks(记录这个segment有多少个block已经被使用了),valid_map(记录这个segment里面的哪一些block是无效的),mtime(表示修改时间)。 13 | 14 | ### SIT物理存放区域结构 15 | 16 | 从上图所示,SIT的基本存放单元是`struct f2fs_sit_block`,它结构如下: 17 | 18 | ```c 19 | struct f2fs_sit_block { 20 | struct f2fs_sit_entry entries[SIT_ENTRY_PER_BLOCK]; 21 | } __packed; 22 | ``` 23 | 24 | 由于一个block的尺寸是4KB,因此跟根据`sizeof(struct f2fs_sit_entry entries)`的值,得到`SIT_ENTRY_PER_BLOCK`的值为55。`struct f2fs_sit_entry entries`用来表示每一个segment的状态信息,它的结构如下: 25 | 26 | ```c 27 | struct f2fs_sit_entry { 28 | __le16 vblocks; /* reference above */ 29 | __u8 valid_map[SIT_VBLOCK_MAP_SIZE]; /* bitmap for valid blocks */ 30 | __le64 mtime; /* segment age for cleaning */ 31 | } __packed; 32 | ``` 33 | 34 | 第一个参数`vblocks`表示当前segment有多少个block已经被使用,第二个参数`valid_map`表示segment内的每一个block的有效无效信息; 由于一个segment包含了512个block,因此需要用512个bit去表示每一个block的有效无效状态,因此`SIT_VBLOCK_MAP_SIZE`的值是64(8*64=512)。最后一个参数`mtime`表示这个entry被修改的时间,用于挑选GC时需要使用的segment。 35 | 36 | 37 | 38 | ## SIT内存管理结构 39 | 40 | SIT在内存中对应的管理结构是`struct f2fs_sm_info`,它在` build_segment_manager`函数进行初始化: 41 | 42 | ```c 43 | int build_segment_manager(struct f2fs_sb_info *sbi) 44 | { 45 | struct f2fs_super_block *raw_super = F2FS_RAW_SUPER(sbi); 46 | struct f2fs_checkpoint *ckpt = F2FS_CKPT(sbi); 47 | struct f2fs_sm_info *sm_info; 48 | int err; 49 | 50 | /* 分配空间 */ 51 | sm_info = kzalloc(sizeof(struct f2fs_sm_info), GFP_KERNEL); 52 | 53 | /* 初始化一些地址信息,基础信息 */ 54 | sbi->sm_info = sm_info; 55 | INIT_LIST_HEAD(&sm_info->wblist_head); 56 | spin_lock_init(&sm_info->wblist_lock); 57 | sm_info->seg0_blkaddr = le32_to_cpu(raw_super->segment0_blkaddr); 58 | sm_info->main_blkaddr = le32_to_cpu(raw_super->main_blkaddr); 59 | sm_info->segment_count = le32_to_cpu(raw_super->segment_count); 60 | sm_info->reserved_segments = le32_to_cpu(ckpt->rsvd_segment_count); 61 | sm_info->ovp_segments = le32_to_cpu(ckpt->overprov_segment_count); 62 | sm_info->main_segments = le32_to_cpu(raw_super->segment_count_main); 63 | sm_info->ssa_blkaddr = le32_to_cpu(raw_super->ssa_blkaddr); 64 | 65 | /* 初始化内存中的entry数据结构 */ 66 | err = build_sit_info(sbi); 67 | 68 | /* 初始化可用segment的数据结构 */ 69 | err = build_free_segmap(sbi); 70 | 71 | /* 恢复checkpoint active segment区域的信息,参考checkpoint结构那一节 */ 72 | err = build_curseg(sbi); 73 | 74 | /* 从磁盘中将SIT物理区域记录的 物理区域sit_entry与只存在于内存的sit_entry建立联系 */ 75 | build_sit_entries(sbi); 76 | 77 | /* 根据checkpoint记录的恢复信息,恢复可用segment的映射关系 */ 78 | init_free_segmap(sbi); 79 | 80 | /* 恢复脏segment的映射关系 */ 81 | err = build_dirty_segmap(sbi); 82 | 83 | /* 初始化最大最小的修改时间 */ 84 | init_min_max_mtime(sbi); 85 | return 0; 86 | } 87 | ``` 88 | 89 | `build_sit_info`用于初始化内存区域的entry,这里需要注意的是注意区分内存entry以及物理区域的entry: 90 | 91 | ```c 92 | static int build_sit_info(struct f2fs_sb_info *sbi) 93 | { 94 | struct f2fs_super_block *raw_super = F2FS_RAW_SUPER(sbi); 95 | struct f2fs_checkpoint *ckpt = F2FS_CKPT(sbi); 96 | struct sit_info *sit_i; 97 | unsigned int sit_segs, start; 98 | char *src_bitmap, *dst_bitmap; 99 | unsigned int bitmap_size; 100 | 101 | /* 分配空间给sit_info */ 102 | sit_i = kzalloc(sizeof(struct sit_info), GFP_KERNEL); 103 | 104 | /* 将sit_info归于sbi->sm_info进行管理 */ 105 | SM_I(sbi)->sit_info = sit_i; 106 | 107 | /* 根据main area的segment的数目,给每一个segment在内存中分配一个entry结构 */ 108 | sit_i->sentries = vzalloc(TOTAL_SEGS(sbi) * sizeof(struct seg_entry)); 109 | 110 | /* 这个bitmap是segment的bitmap,作用是当segment全部block都没有使用过, 111 | * 这个segment就需要标记free 112 | */ 113 | bitmap_size = f2fs_bitmap_size(TOTAL_SEGS(sbi)); 114 | 115 | /* 这个bitmap是记录segment是否为脏的bitmap,作用是当segment分配了一个block之后, 116 | * 这个segment对应的entry信息就会改变,因此将这个segment标记为脏,之后需要通过某种策略 117 | * 将数据写回到SIT区域 118 | */ 119 | sit_i->dirty_sentries_bitmap = kzalloc(bitmap_size, GFP_KERNEL); 120 | 121 | /* 这里给每一个内存entry的记录block状态的bitmap分配空间,SIT_VBLOCK_MAP_SIZE=64 */ 122 | for (start = 0; start < TOTAL_SEGS(sbi); start++) { 123 | sit_i->sentries[start].cur_valid_map 124 | = kzalloc(SIT_VBLOCK_MAP_SIZE, GFP_KERNEL); 125 | sit_i->sentries[start].ckpt_valid_map 126 | = kzalloc(SIT_VBLOCK_MAP_SIZE, GFP_KERNEL); 127 | } 128 | 129 | /* 获取SIT区域包含了多少个segment去存放f2fs_sit_block */ 130 | sit_segs = le32_to_cpu(raw_super->segment_count_sit) >> 1; 131 | 132 | /* 从checkpoint中恢复bitmap的状态 */ 133 | bitmap_size = __bitmap_size(sbi, SIT_BITMAP); 134 | src_bitmap = __bitmap_ptr(sbi, SIT_BITMAP); 135 | 136 | dst_bitmap = kmemdup(src_bitmap, bitmap_size, GFP_KERNEL); 137 | 138 | /* 初始化其他信息 */ 139 | sit_i->s_ops = &default_salloc_ops; 140 | 141 | sit_i->sit_base_addr = le32_to_cpu(raw_super->sit_blkaddr); 142 | sit_i->sit_blocks = sit_segs << sbi->log_blocks_per_seg; 143 | sit_i->written_valid_blocks = le64_to_cpu(ckpt->valid_block_count); 144 | sit_i->sit_bitmap = dst_bitmap; 145 | sit_i->bitmap_size = bitmap_size; 146 | sit_i->dirty_sentries = 0; 147 | sit_i->sents_per_block = SIT_ENTRY_PER_BLOCK; 148 | sit_i->elapsed_time = le64_to_cpu(sbi->ckpt->elapsed_time); 149 | sit_i->mounted_time = CURRENT_TIME_SEC.tv_sec; 150 | mutex_init(&sit_i->sentry_lock); 151 | return 0; 152 | } 153 | ``` 154 | 155 | `build_free_segmap`用于初始化segment的分配状态: 156 | 157 | ```c 158 | static int build_free_segmap(struct f2fs_sb_info *sbi) 159 | { 160 | struct f2fs_sm_info *sm_info = SM_I(sbi); 161 | struct free_segmap_info *free_i; 162 | unsigned int bitmap_size, sec_bitmap_size; 163 | 164 | /* 给管理segment分配状态的free_segmap_info分配内存空间 */ 165 | free_i = kzalloc(sizeof(struct free_segmap_info), GFP_KERNEL); 166 | 167 | /* 将sit_info归于sbi->sm_info进行管理 */ 168 | SM_I(sbi)->free_info = free_i; 169 | 170 | /* 根据segment的数目初始化free map的大小 */ 171 | bitmap_size = f2fs_bitmap_size(TOTAL_SEGS(sbi)); 172 | free_i->free_segmap = kmalloc(bitmap_size, GFP_KERNEL); 173 | 174 | /* 由于1 section = 1 segment,将sec map看作为根据segment map同等作用就好 */ 175 | sec_bitmap_size = f2fs_bitmap_size(TOTAL_SECS(sbi)); 176 | free_i->free_secmap = kmalloc(sec_bitmap_size, GFP_KERNEL); 177 | 178 | /* 在从checkpoint恢复数据之前,将所有的segment设置为dirty */ 179 | memset(free_i->free_segmap, 0xff, bitmap_size); 180 | memset(free_i->free_secmap, 0xff, sec_bitmap_size); 181 | 182 | /* 初始化其他信息 */ 183 | free_i->start_segno = 184 | (unsigned int) GET_SEGNO_FROM_SEG0(sbi, sm_info->main_blkaddr); 185 | free_i->free_segments = 0; 186 | free_i->free_sections = 0; 187 | rwlock_init(&free_i->segmap_lock); 188 | return 0; 189 | } 190 | ``` 191 | 192 | `build_sit_entries`的作用是从SIT的物理区域存放的物理entry与内存的entry建立联系,首先看看物理entry和内存entry的差异在哪里。 193 | 194 | ```c 195 | // 物理entry 196 | struct f2fs_sit_entry { 197 | __le16 vblocks; /* reference above */ 198 | __u8 valid_map[SIT_VBLOCK_MAP_SIZE]; /* bitmap for valid blocks */ 199 | __le64 mtime; /* segment age for cleaning */ 200 | } __packed; 201 | 202 | // 内存entry 203 | struct seg_entry { 204 | unsigned short valid_blocks; /* # of valid blocks */ 205 | unsigned char *cur_valid_map; /* validity bitmap of blocks */ 206 | unsigned short ckpt_valid_blocks; 207 | unsigned char *ckpt_valid_map; 208 | unsigned char type; /* segment type like CURSEG_XXX_TYPE */ 209 | unsigned long long mtime; /* modification time of the segment */ 210 | }; 211 | ``` 212 | 213 | 两者之间的差异主要是多了表示segment类型的type变量,以及多了两个与checkpoint相关的内容。 214 | 215 | 216 | 217 | 其实物理entry也包含了segment type的信息,但是为了节省空间,将segment type于vblocks存放在了一起,及vblocks的前10位表示数目,后6位表示segment type,他们的关系可以用`f2fs_fs.h`找到: 218 | 219 | ```c 220 | #define SIT_VBLOCKS_SHIFT 10 221 | #define SIT_VBLOCKS_MASK ((1 << SIT_VBLOCKS_SHIFT) - 1) 222 | #define GET_SIT_VBLOCKS(raw_sit) \ 223 | (le16_to_cpu((raw_sit)->vblocks) & SIT_VBLOCKS_MASK) 224 | #define GET_SIT_TYPE(raw_sit) \ 225 | ((le16_to_cpu((raw_sit)->vblocks) & ~SIT_VBLOCKS_MASK) \ 226 | >> SIT_VBLOCKS_SHIFT) 227 | ``` 228 | 229 | 因此,内存entry实际上仅仅多了2个与checkpoint相关的信息,即`ckpt_valid_blocks`与`ckpt_valid_map`。在系统执行checkpoint的时候,会将`valid_blocks`以及`cur_valid_map`的值分别写入`ckpt_valid_blocks`与`ckpt_valid_map`,当系统出现宕机的时候根据这个值恢复映射信息。 230 | 231 | 232 | 233 | 继续分析`build_sit_entries`的代码, 234 | 235 | ```c 236 | static void build_sit_entries(struct f2fs_sb_info *sbi) 237 | { 238 | struct sit_info *sit_i = SIT_I(sbi); 239 | struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_COLD_DATA); 240 | struct f2fs_summary_block *sum = curseg->sum_blk; 241 | unsigned int start; 242 | 243 | /* 建立物理entry以及内存entry的关系 */ 244 | for (start = 0; start < TOTAL_SEGS(sbi); start++) { 245 | struct seg_entry *se = &sit_i->sentries[start]; // 内存entry 246 | struct f2fs_sit_block *sit_blk; 247 | struct f2fs_sit_entry sit; 248 | struct page *page; 249 | int i; 250 | 251 | // 先尝试在journal恢复 252 | mutex_lock(&curseg->curseg_mutex); 253 | for (i = 0; i < sits_in_cursum(sum); i++) { 254 | if (le32_to_cpu(segno_in_journal(sum, i)) == start) { 255 | sit = sit_in_journal(sum, i); 256 | mutex_unlock(&curseg->curseg_mutex); 257 | goto got_it; 258 | } 259 | } 260 | mutex_unlock(&curseg->curseg_mutex); 261 | 262 | // 如果恢复不了就从SIT恢复 263 | page = get_current_sit_page(sbi, start); // 读取 f2fs_sit_block 264 | sit_blk = (struct f2fs_sit_block *)page_address(page); // 转换为block 265 | sit = sit_blk->entries[SIT_ENTRY_OFFSET(sit_i, start)]; // 物理entry 266 | f2fs_put_page(page, 1); 267 | got_it: 268 | check_block_count(sbi, start, &sit); 269 | seg_info_from_raw_sit(se, &sit); // 将物理entry的数据赋予到内存entry 270 | } 271 | } 272 | 273 | ``` 274 | 275 | `init_free_segmap` 从内存entry以及checkpoint中恢复free segment的信息: 276 | 277 | ```c 278 | static void init_free_segmap(struct f2fs_sb_info *sbi) 279 | { 280 | unsigned int start; 281 | int type; 282 | 283 | for (start = 0; start < TOTAL_SEGS(sbi); start++) { // 根据segment编号遍历每一个内存entry 284 | struct seg_entry *sentry = get_seg_entry(sbi, start); 285 | if (!sentry->valid_blocks) // 如果这个segment一个block都没有用过,则设置为free 286 | __set_free(sbi, start); 287 | } 288 | 289 | /* 从checkpoint的curseg中恢复可用信息 */ 290 | for (type = CURSEG_HOT_DATA; type <= CURSEG_COLD_NODE; type++) { 291 | struct curseg_info *curseg_t = CURSEG_I(sbi, type); 292 | __set_test_and_inuse(sbi, curseg_t->segno); // 设置为正在使用的状态 293 | } 294 | } 295 | ``` 296 | 297 | `init_dirty_segmap`函数恢复脏segment的信息 298 | 299 | ```c 300 | static void init_dirty_segmap(struct f2fs_sb_info *sbi) 301 | { 302 | struct dirty_seglist_info *dirty_i = DIRTY_I(sbi); 303 | struct free_segmap_info *free_i = FREE_I(sbi); 304 | unsigned int segno = 0, offset = 0; 305 | unsigned short valid_blocks; 306 | 307 | while (segno < TOTAL_SEGS(sbi)) { 308 | /* find dirty segment based on free segmap */ 309 | segno = find_next_inuse(free_i, TOTAL_SEGS(sbi), offset); // 找出所有已经使用过的seg 310 | if (segno >= TOTAL_SEGS(sbi)) 311 | break; 312 | offset = segno + 1; 313 | valid_blocks = get_valid_blocks(sbi, segno, 0); // 得到了使用了多少个block 314 | if (valid_blocks >= sbi->blocks_per_seg || !valid_blocks) 315 | continue; 316 | mutex_lock(&dirty_i->seglist_lock); 317 | __locate_dirty_segment(sbi, segno, DIRTY); // 将其设置为dirty 318 | mutex_unlock(&dirty_i->seglist_lock); 319 | } 320 | } 321 | ``` 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | -------------------------------------------------------------------------------- /F2FS-Layout/Segment Summary Area结构.md: -------------------------------------------------------------------------------- 1 | # Segment Summary Area区域-SSA结构 2 | Segment Summary Area,简称SSA,是F2FS用于集中管理物理地址到逻辑地址的映射关系的结构,同时它也具有通过journal缓存sit或者nat的操作用于数据恢复的作用。映射关系的主要作用是当给出一个物理地址的时候,可以通过SSA索引得到对应的逻辑地址,主要应用在GC流程中; SSA所包含的journal可以缓存一些sit或者nat的操作,用于避免频繁的元数据更新,以及宕机时候的数据恢复。 3 | 4 | ## SSA在元数据区域的物理结构 5 | ![cp_layout](../img/F2FS-Layout/ssa_layout2.png) 6 | 7 | 从结构图可以知道,SSA区域由N个`struct f2fs_summary_block`组成,每一个`struct f2fs_summary_block`包含了512个`struct f2fs_summary_entry`,刚好对应一个segment。segment里面的每一个block(物理地址)对应一个的`struct f2fs_summary_entry`,它记录了物理地址到逻辑地址的映射信息。它包含了三个变量: nid(该物理地址是属于哪一个node的),version(用于数据恢复),ofs_in_node(该物理地址属于该nid对应的node的第ofs_in_node个block)。 8 | 9 | `f2fs_journal`属于journal的信息,它的作用是减少频繁地对NAT区域以及SIT区域的更新。例如,当系统写压力很大的时候,segment bitmap更新就会很频繁,就会对到SIT章节所提到的`struct f2fs_sit_entry`结构进行频繁地改动。如果这个时候频繁将新的映射关系写入SIT,就会加重写压力。此时可以将数据先写入到journal中,因此journal的作用就是维护这些经常修改的数据,等待CP被触发的时候才写入磁盘,从而减少写压力。也许这里会有疑问,为什么将journal放在SSA区域而不是NAT区域以及SIT区域呢?这是因为这种存放方式可以减少元数据区域空间的占用。关于journal的描述以及作用可以参考[journal章节](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/f2fs_journal.md)以及[checkpoint章节](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/Checkpoint%E7%BB%93%E6%9E%84.md),本节不做描述。 10 | 11 | ### SSA物理存放区域结构 12 | 13 | 从上图所示,SSA的基本存放单元是`struct f2fs_summary_block`,它结构如下: 14 | 15 | ```c 16 | struct f2fs_summary_block { 17 | struct f2fs_summary entries[ENTRIES_IN_SUM]; // ENTRIES_IN_SUM = 512 18 | struct f2fs_journal journal; 19 | struct summary_footer footer; 20 | } __packed; 21 | ``` 22 | 23 | 与summary直接相关的是`struct f2fs_summary`以及`struct summary_footer`。`ENTRIES_IN_SUM`的值512,因此每一个entry对应一个block,记录了从物理地址到逻辑地址的映射关系,entry的结构如下: 24 | 25 | ```c 26 | struct f2fs_summary { 27 | __le32 nid; /* parent node id */ 28 | union { 29 | __u8 reserved[3]; 30 | struct { 31 | __u8 version; /* node version number */ 32 | __le16 ofs_in_node; /* block index in parent node */ 33 | } __packed; 34 | }; 35 | } __packed; 36 | ``` 37 | 38 | 用了一个union结构进行表示,但是核心信息是`nid`、`version`以及`ofs_in_node`。根据[文件数据的保存以及物理地址的映射](https://github.com/RiweiPan/F2FS-NOTES/blob/master/Reading-and-Writing/file_data_structure.md) 可以知道,数据的索引是通过node来进行。文件访问某一个页的数据时,需要首先根据页的索引,找到对应的nid以及offset(两者构成逻辑地址),从而根据nid得到node page,再根据offset得到了该页的物理地址,然后从磁盘中读取出来。`f2fs_summary`则是记录物理地址到逻辑地址的映射,即根据物理地址找到对应的nid以及offset。例如,现在需要根据物理地址为624的block,找到对应的nid以及offset。那么物理地址为624,可以得到该地址位于第二个segment,然后属于第二个segment的第113个block(block的编址从0开始)。因此根据属于第二个segment的信息,找到第二个`struct f2fs_summary_block`,然后根据偏移量为113的信息,找到对应的`struct f2fs_summary`结构,从而得到`nid`以及`ofs_in_node`。 39 | 40 | 41 | 42 | `struct summary_footer`结构记录了校验信息,以及这个summary对应的segment是属于保存data数据的segment还是node数据的segment。 43 | 44 | 45 | 46 | ## SSA内存管理结构 47 | SSA在内存没有单独的管理结构,summary以及journal在内存中主要存在于`CURSEG`中,可以从[Checkpoint章节](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Data-Recovery/Checkpoint%E6%B5%81%E7%A8%8B.md)找到相关的描述。 48 | -------------------------------------------------------------------------------- /F2FS-Layout/Superblock结构.md: -------------------------------------------------------------------------------- 1 | # Superblock区域 2 | Superblock保存了F2FS的核心元数据的结构,包括磁盘大小,元区域的各个部分的起始地址等。 3 | 4 | ## Superblock在元数据区域的物理结构 5 | 6 | ![sb_layout](../img/F2FS-Layout/sb_layout2.png) 7 | 8 | Superblock区域是由两个`struct f2fs_super_block`结构组成,互为备份。 9 | ## Superblock物理存放区域结构 10 | `struct f2fs_super_block`是F2FS对Superblock的具体数据结构实现,它保存在磁盘的最开始的位置,F2FS进行挂载时从磁盘的前端直接读取出来,然后转换为`struct f2fs_super_block`结构。它的定义如下: 11 | 12 | ```c 13 | 14 | struct f2fs_super_block { 15 | __le32 magic; /* Magic Number */ 16 | __le16 major_ver; /* Major Version */ 17 | __le16 minor_ver; /* Minor Version */ 18 | __le32 log_sectorsize; /* log2 sector size in bytes */ 19 | __le32 log_sectors_per_block; /* log2 # of sectors per block 一般是3,因为1<<3 = 8 */ 20 | __le32 log_blocksize; /* log2 block size in bytes 一般是12,因为 1<<12 = 4096 */ 21 | __le32 log_blocks_per_seg; /* log2 # of blocks per segment 一般是8,因为 1<<8 = 512 */ 22 | __le32 segs_per_sec; /* # of segments per section */ 23 | __le32 secs_per_zone; /* # of sections per zone */ 24 | __le32 checksum_offset; /* checksum offset inside super block */ 25 | __le64 block_count; /* total # of user blocks */ 26 | __le32 section_count; /* total # of sections */ 27 | __le32 segment_count; /* total # of segments */ 28 | __le32 segment_count_ckpt; /* # of segments for checkpoint */ 29 | __le32 segment_count_sit; /* # of segments for SIT */ 30 | __le32 segment_count_nat; /* # of segments for NAT */ 31 | __le32 segment_count_ssa; /* # of segments for SSA */ 32 | __le32 segment_count_main; /* # of segments for main area */ 33 | __le32 segment0_blkaddr; /* start block address of segment 0 */ 34 | __le32 cp_blkaddr; /* start block address of checkpoint */ 35 | __le32 sit_blkaddr; /* start block address of SIT */ 36 | __le32 nat_blkaddr; /* start block address of NAT */ 37 | __le32 ssa_blkaddr; /* start block address of SSA */ 38 | __le32 main_blkaddr; /* start block address of main area */ 39 | __le32 root_ino; /* root inode number */ 40 | __le32 node_ino; /* node inode number */ 41 | __le32 meta_ino; /* meta inode number */ 42 | __u8 uuid[16]; /* 128-bit uuid for volume */ 43 | __le16 volume_name[MAX_VOLUME_NAME]; /* volume name */ 44 | __le32 extension_count; /* # of extensions below */ 45 | __u8 extension_list[F2FS_MAX_EXTENSION][F2FS_EXTENSION_LEN];/* extension array */ 46 | __le32 cp_payload; 47 | __u8 version[VERSION_LEN]; /* the kernel version */ 48 | __u8 init_version[VERSION_LEN]; /* the initial kernel version */ 49 | __le32 feature; /* defined features */ 50 | __u8 encryption_level; /* versioning level for encryption */ 51 | __u8 encrypt_pw_salt[16]; /* Salt used for string2key algorithm */ 52 | struct f2fs_device devs[MAX_DEVICES]; /* device list */ 53 | __le32 qf_ino[F2FS_MAX_QUOTAS]; /* quota inode numbers */ 54 | __u8 hot_ext_count; /* # of hot file extension */ 55 | __u8 reserved[314]; /* valid reserved region */ 56 | } __packed; 57 | 58 | ``` 59 | 60 | 61 | 对于一个50MB大小的磁盘,格式化后的`f2fs_super_block` 的信息如下: 62 | ``` 63 | magic = -218816496 64 | major_ver = 1 65 | minor_ver = 10 66 | log_sectorsize = 9 67 | log_sectors_per_block = 3 68 | log_blocksize = 12 69 | log_blocks_per_seg = 9 70 | segs_per_sec = 1 71 | secs_per_zone = 1 72 | checksum_offset = 0 73 | block_count = 12800 # 50MB / 4KB = 12800 74 | section_count = 17 # section只在main area应用,因此和main area一样 75 | segment_count = 24 76 | segment_count_ckpt = 2 # checkpoint用了2个segment 77 | segment_count_sit = 2 # SIT也用了2个segment 78 | segment_count_nat = 2 # NAT也用了2个segment 79 | segment_count_ssa = 1 # SSA用了1个segment 80 | segment_count_main = 17 # main area一共有17个可用的segment 81 | segment0_blkaddr = 512 82 | cp_blkaddr = 512 # checkpoint的地址 83 | sit_blkaddr = 1536 # sit的地址 84 | nat_blkaddr = 2560 # nat的地址 85 | ssa_blkaddr = 3584 # ssa的地址 86 | main_blkaddr = 4096 # main area的地址 87 | root_ino = 3 88 | node_ino = 1 89 | meta_ino = 2 90 | extension_count = 27 91 | cp_payload = 0 92 | feature = 0 93 | encryption_level = 94 | ``` 95 | 96 | ## Superblock内存管理结构 97 | 如上一节所述,`f2fs_super_block`在内存中的对应的结构是`struct f2fs_sb_info`,它除了包含了`struct f2fs_super_block`的信息以外,还包含了一些额外的功能,如锁、SIT、NAT对应的内存管理结构等,简单如下所述: 98 | ```c 99 | struct f2fs_sb_info { 100 | struct super_block *sb; /* pointer to VFS super block */ 101 | struct f2fs_super_block *raw_super; /* raw super block pointer */ 102 | struct rw_semaphore sb_lock; /* lock for raw super block */ 103 | 104 | /* for node-related operations */ 105 | struct f2fs_nm_info *nm_info; /* node manager */ 106 | struct inode *node_inode; /* cache node blocks */ 107 | 108 | /* for segment-related operations */ 109 | struct f2fs_sm_info *sm_info; /* segment manager */ 110 | 111 | /* for checkpoint */ 112 | struct f2fs_checkpoint *ckpt; /* raw checkpoint pointer */ 113 | 114 | /* for orphan inode, use 0'th array */ 115 | unsigned int max_orphans; /* max orphan inodes */ 116 | 117 | struct f2fs_mount_info mount_opt; /* mount options */ 118 | 119 | /* for cleaning operations */ 120 | struct mutex gc_mutex; /* mutex for GC */ 121 | struct f2fs_gc_kthread *gc_thread; /* GC thread */ 122 | unsigned int cur_victim_sec; /* current victim section num */ 123 | unsigned int gc_mode; /* current GC state */ 124 | }; 125 | ``` 126 | 它的初始化在`init_sb_info`函数完成: 127 | ```c 128 | static void init_sb_info(struct f2fs_sb_info *sbi) 129 | { 130 | // raw_supaer就是F2FS在从磁盘读取出来的信息,再初始化对应的内存结构struct f2fs_sb_info 131 | struct f2fs_super_block *raw_super = sbi->raw_super; 132 | int i, j; 133 | 134 | sbi->log_sectors_per_block = 135 | le32_to_cpu(raw_super->log_sectors_per_block); 136 | sbi->log_blocksize = le32_to_cpu(raw_super->log_blocksize); 137 | sbi->blocksize = 1 << sbi->log_blocksize; 138 | sbi->log_blocks_per_seg = le32_to_cpu(raw_super->log_blocks_per_seg); 139 | sbi->blocks_per_seg = 1 << sbi->log_blocks_per_seg; 140 | sbi->segs_per_sec = le32_to_cpu(raw_super->segs_per_sec); 141 | sbi->secs_per_zone = le32_to_cpu(raw_super->secs_per_zone); 142 | sbi->total_sections = le32_to_cpu(raw_super->section_count); 143 | sbi->total_node_count = 144 | (le32_to_cpu(raw_super->segment_count_nat) / 2) 145 | * sbi->blocks_per_seg * NAT_ENTRY_PER_BLOCK; 146 | sbi->root_ino_num = le32_to_cpu(raw_super->root_ino); 147 | sbi->node_ino_num = le32_to_cpu(raw_super->node_ino); 148 | sbi->meta_ino_num = le32_to_cpu(raw_super->meta_ino); 149 | sbi->cur_victim_sec = NULL_SECNO; 150 | sbi->max_victim_search = DEF_MAX_VICTIM_SEARCH; 151 | 152 | sbi->dir_level = DEF_DIR_LEVEL; 153 | sbi->interval_time[CP_TIME] = DEF_CP_INTERVAL; 154 | sbi->interval_time[REQ_TIME] = DEF_IDLE_INTERVAL; 155 | clear_sbi_flag(sbi, SBI_NEED_FSCK); 156 | 157 | for (i = 0; i < NR_COUNT_TYPE; i++) 158 | atomic_set(&sbi->nr_pages[i], 0); 159 | 160 | for (i = 0; i < META; i++) 161 | atomic_set(&sbi->wb_sync_req[i], 0); 162 | 163 | INIT_LIST_HEAD(&sbi->s_list); 164 | mutex_init(&sbi->umount_mutex); 165 | for (i = 0; i < NR_PAGE_TYPE - 1; i++) 166 | for (j = HOT; j < NR_TEMP_TYPE; j++) 167 | mutex_init(&sbi->wio_mutex[i][j]); 168 | init_rwsem(&sbi->io_order_lock); 169 | spin_lock_init(&sbi->cp_lock); 170 | 171 | sbi->dirty_device = 0; 172 | spin_lock_init(&sbi->dev_lock); 173 | 174 | init_rwsem(&sbi->sb_lock); 175 | } 176 | 177 | ``` -------------------------------------------------------------------------------- /F2FS-Layout/总体结构.md: -------------------------------------------------------------------------------- 1 | # 总体介绍 2 | F2FS全称为Flash Friendly File System,是专门为flash设备设计的一个日志结构型文件系统(Log-structured File System, LFS)。相对于传统的日志结构型文件系统,F2FS在wandering tree和gc的高时间开销等问题,有一定的改进和优化。 3 | 4 | - **wandering tree问题:** 传统的LFS,在文件数据被更新或者写入到日志的末端的时候,指向文件数据保存地址的直接指针指针会被更新(flash设备的异地更新特性),同时指向这个直接指针的间接指针也会更新,然后保存间接指针的inode、inode blockmap等结构也需要更新,这样就导致了频繁metadata的更新。这种问题称为wandering tree问题。 5 | 6 | - **高gc开销问题:** 由于LFS对于修改的数据是执行异地更新的特性,因此数据更新到一个新地址后,旧地址的回收和再利用的过程称为垃圾回收过程(GC),这个过程会导致不可预期的高延迟,对于用户感知有很大影响。 7 | 8 | 9 | 10 | # 系统特性 11 | 12 | ### F2FS的基本数据单位 13 | 1. **block:** F2FS的数据存储的基本单位是block,大小为4KB,整个flash设备被格式化为多个block组成的结构。很多数据结构也被设计为4KB的大小,这是因为很多flash设备单次IO的读写都是基于4KB的倍数进行。 14 | 2. **segment:** segment是管理block的结构,一个segment的大小是512个block,也就是2MB。 15 | 3. **section:** 默认情况下一个segment等于一个section,section是GC的基本操作单位,每次GC都会从section中选出特定的segment进行回收。F2FS将section分为了6类,分别是hot-node,warm-node,cold-node,hot-data,warm-data,cold-data,hot->cold表示了数据的从高到低的修改频率,通过不同类型的section,进行gc的时候可针对使用hot的section进行gc,以降低gc的时间开销。 16 | 4. **zone:** 默认情况一个zone等于一个section,与物理设备有关,大部分情况下用不上。 17 | 18 | 19 | 20 | ### LFS异地更新特性 21 | 22 | F2FS是一个Log-structured File System(LFS),因此会使用**异地更新**策略。我们可以i用一个简单的例子去说明什么是异地更新。假设有一个文件,它的文件数据保存在物理地址100的位置中,此时用户对文件内容进行更新: 23 | 24 | **非LFS:** 使用就地更新策略,将更新后的数据写入到物理地址100中。 25 | 26 | **LFS:** 使用异地更新策略,首先会分配一个新的物理地址101,然后将数据写入新物理地址101中,接着将文件指针指向新的物理地址101,最后将旧的物理地址100进行回收再利用。 27 | 28 | 这种设计的好处是: 29 | 30 | 1. 可以将随机写转换为顺序写以获得更好的性能提升; 31 | 2. flash的颗粒program寿命是有限的,通过LFS的异地更新特性,可以自带磨损均衡。 32 | 33 | 但是LFS也有一些缺点,一个最明显的缺点是F2FS要对旧的物理地址进行回收,这个过程称为垃圾回收过程(GC),不适当的GC时机会影响到系统的性能表现;另外一个缺点是LFS极端情况的安全性不像JFS(journal file system)那么好,因为LFS依赖Checkpoint保证一致性,但是Checkpoint不是每次写入数据都会进行(带来很大的开销),而是隔一段时间才会进行一次Checkpoint,因此可能在Checkpoint之前系统宕机,会带来部分数据的丢失。 34 | 35 | 36 | 37 | ### Multi-head Logging特性 38 | 39 | Log区域指的是文件系统中用于分配free block(空闲的且没有写入数据的block)的区域,例如F2FS的一个文件需要写入新数据,它就要去Log区域请求free block,然后再将数据写入这个free block中。传统的LFS往往会维护一个大的日志区域,一切数据的分配都从这个大的日志区域中进行处理,而F2FS则根据数据的访问冷热特性差异,维护了6个Log区域,分别是: 40 | 41 | - **HOT NODE区域**:给目录的direct node使用,因为打开目录、读取目录是最频繁的操作。 42 | - **WARM NODE区域**:给普通文件的direct node使用 43 | - **COLD NODE区域**:给indirect node使用,一般而言只有较大的文件才会使用到这个log区域。 44 | - **HOT DATA区域**:给目录的数据使用,目录数据记录了当前目录有多少个子文件、子文件夹。 45 | - **WARM DATA区域**:给普通文件的数据使用,常规的fwrite/write函数写入的数据都是在这里分配。 46 | - **COLD DATA区域**:给不频繁修改的数据使用,如多媒体文件(多为只读文件),或者用户指定的只读文件,如GC产生写的数据(gc会挑热度最低的数据)。 47 | 48 | *注: direct node、indirect node的概念和作用会在第二章进行介绍。* 49 | 50 | 51 | 52 | # F2FS的闪存设备物理区域布局 53 | 54 | ![](../img/F2FS-Layout/f2fs-layout.png) 55 | 56 | 通过mkfs.f2fs工具,可以将整个flash设备格式化成特定的格式。整个存储设备区域被F2FS格式化为6个区域,分别是Superblock,Checkpoint,Segment Info Table,Node Address Table,Segment Summary Area,以及Main Area。前5个区域总称为元数据区域(Metadata Area),保存的是跟F2FS直接相关的元信息,而最后一个区域是保存文件数据的主要区域,主要保存了node数据、文件data数据、目录数据。它们的作用分别是: 57 | 58 | **Superblock:** 记录整个文件系统的分区信息,包括一共有多少个block,使用了多少block这些最基本同时也是最重要的信息。F2FS在挂载的时候,会创建一个内存数据结构`struct f2fs_sb_info`,然后从设备的Superblock区域读取相关数据。 59 | 60 | **Checkpoint:** 记录了上次卸载F2FS的时刻,系统的block、node等分配状态(如当前free block分配了到哪个位置),用于给下次挂载F2FS的时候,复原整个系统的block、node的分配状态。在F2FS的运行过程中,F2FS会定时将当前的block、node分配状态写入Checkpoint区域,用于由于F2FS被关闭/崩溃时恢复数据。Checkpoint在内存中对应的数据结构是`struct f2fs_checkpoint`。 61 | 62 | **Segment Information Table(SIT):** 保存了每一个segment的信息,例如这个segment已经分配了多少个block、哪一个block正在使用,哪一个block是无效的需要回收。通过这些信息去管理已经被使用了的block和未使用的block,使系统可以合理分配block。每一个segment都对应了一个segment number(segno),系统可以通过segno快速地查询到该segment的分配信息。SIT在内存中对应的数据结构是`struct f2fs_sm_info`。 63 | 64 | **Node Address Table(NAT):** 建立了一张表保存了每一个node的物理地址信息。F2FS的每一个node都有一个node id(nid),系统可以通过nid在NAT找到对应node的物理地址,从而在闪存设备读取出来。NAT在内存中对应的数据结构是`struct f2fs_nm_info`。 65 | 66 | **Segment Summary Area(SSA):** 这个区域主要保存了jounal(SIT/NAT临时的修改信息)以及summary(记录了逻辑地址和物理地址关系的结构,主要用与GC)。SSA区域在内存中没有专门的数据结构。 67 | 68 | **Main Area:** Main Area被4KB大小的block所填充,这些block可以分配给文件的data或者文件的node,是F2FS的主要数据保存区域。 69 | 70 | 71 | -------------------------------------------------------------------------------- /File-Creation-and-Deletion/文件创建.md: -------------------------------------------------------------------------------- 1 | # F2FS的文件创建流程 2 | 3 | ## 文件创建流程介绍 4 | linux的文件的创建可以抽象为两个流程 5 | 6 | 1. 创建一个inode,使得包含文件的元数据信息; 7 | 2. 将这个新创建的inode加入父目录的管理当中,可以理解建立父目录与这个新inode的关系。 8 | 9 | 到具体代码,上述两个抽象流程在F2FS中主要包含了以下几个子流程: 10 | 11 | 1. 调用vfs_open函数 12 | 2. 调用f2fs_create函数: 创建文件inode,并连接到父目录 13 | 1. f2fs_new_inode函数创建inode 14 | 2. f2fs_add_link函数链接到父目录 15 | 16 | 第一步的vfs_open函数是VFS层面的流程,下面仅针对涉及F2FS的文件创建流程,且经过简化的主要流程进行分析。 17 | 18 | ## 前置概念: inode和f2fs_inode_info 19 | 20 | 众所周知,`inode`结构是linux的vfs层最核心的结构之一,反应了文件的应该具有的基础信息,但是对于一些文件系统,原生的`inode`结构的信息并不够,还需要增加一些额外的变量去支持文件系统的某些功能,同时为了保证vfs层对所有文件系统的兼容性,我们直接修改`inode`结构不是一个明智的方法。针对这种场景,f2fs使用了一种叫`f2fs_inode_info`的结构去扩展原有的`inode`的功能。 21 | 22 | ### 相互转换 23 | 24 | 从`inode`到`f2fs_inode_info`: 25 | 26 | ```c 27 | static inline struct f2fs_inode_info *F2FS_I(struct inode *inode) 28 | { 29 | return container_of(inode, struct f2fs_inode_info, vfs_inode); 30 | } 31 | ``` 32 | 33 | 从`f2fs_inode_info`到`inode`: 34 | 35 | ```c 36 | // vfs的inode其实是f2fs_inode_info结构体的一个内部变量 37 | struct f2fs_inode_info { 38 | struct inode vfs_inode; /* serve a vfs inode */ 39 | ... 40 | }; 41 | 42 | // 因此访问可以直接指向 43 | struct f2fs_inode_info *fi = F2FS_I(inode); 44 | fi->vfs_inode // 这里 fi->vfs_inode == inode 45 | ``` 46 | 47 | 从上面代码我们可以看出,f2fs中的`inode`是`f2fs_inode_info`当中的一个内部变量,因此可以用container_of这个函数直接获得,也可以通过指针获得。 48 | 49 | ### F2FS中的VFS inode的创建和销毁 50 | 51 | 我们一般使用VFS提供的`new_inode`函数创建一个新inode。这个`new_inode`函数内部会调用new_inode_pseudo函数,然后再调用alloc_inode函数,最后调用`f2fs_alloc_inode`函数,我们从这里开始分析: 52 | 53 | 如下代码,显然就是通过内存分配函数先创建一个`f2fs_inode_info`然后返回给上层: 54 | 55 | ```c 56 | static struct inode *f2fs_alloc_inode(struct super_block *sb) 57 | { 58 | struct f2fs_inode_info *fi; 59 | 60 | fi = kmem_cache_alloc(f2fs_inode_cachep, GFP_F2FS_ZERO); //简单直接创建f2fs_inode_info 61 | if (!fi) 62 | return NULL; 63 | 64 | init_once((void *) fi); // 这个函数初始化vfs inode部分的原始信息 65 | 66 | // 下面开始初始化f2fs_inode_info部分的原始信息 67 | atomic_set(&fi->dirty_pages, 0); 68 | init_rwsem(&fi->i_sem); 69 | ... 70 | return &fi->vfs_inode; // 返回的vfs_inode给上层 71 | } 72 | ``` 73 | 74 | 当vfs inode的link是0的时候,它应当被销毁。由于vfs inode是f2fs_inode_info的内部变量,它如何被销毁呢: 75 | 76 | ```c 77 | // 用户传入一个inode销毁 78 | static void f2fs_destroy_inode(struct inode *inode) 79 | { 80 | call_rcu(&inode->i_rcu, f2fs_i_callback); 81 | } 82 | 83 | ``` 84 | 85 | 同样简单直接,free掉这块内存就行 86 | 87 | ```c 88 | static void f2fs_i_callback(struct rcu_head *head) 89 | { 90 | struct inode *inode = container_of(head, struct inode, i_rcu); 91 | kmem_cache_free(f2fs_inode_cachep, F2FS_I(inode)); 92 | } 93 | ``` 94 | 95 | ## f2fs_create函数 96 | 这个函数的主要作用是创建vfs_inode,并链接到对应的目录下,核心流程就是先创建该文件的基于f2fs的inode结构(参考xxx),以及它对应的f2fs的inode page,即`f2fs_inode`。然后设置函数指针,最后将这个f2fs的inode page链接到对应的目录下。 97 | ```c 98 | static int f2fs_create(struct inode *dir, struct dentry *dentry, umode_t mode, 99 | bool excl) 100 | { 101 | struct f2fs_sb_info *sbi = F2FS_I_SB(dir); 102 | struct inode *inode; 103 | nid_t ino = 0; 104 | int err; 105 | 106 | inode = f2fs_new_inode(dir, mode); // 创建f2fs特定的inode结构 107 | 108 | inode->i_op = &f2fs_file_inode_operations; // 然后赋值对应的函数指针 109 | inode->i_fop = &f2fs_file_operations; 110 | inode->i_mapping->a_ops = &f2fs_dblock_aops; 111 | ino = inode->i_ino; // 记录该inode的ino 112 | 113 | err = f2fs_add_link(dentry, inode); // 将该inode链接到用户传入的父目录dir中 114 | if (err) 115 | goto out; 116 | 117 | f2fs_alloc_nid_done(sbi, ino); // 在f2fs_new_inode函数内分配了ino,在这里完成最后一步 118 | 119 | return 0; 120 | } 121 | 122 | ``` 123 | ### f2fs_new_inode函数 124 | 125 | 下面继续分析`f2fs_new_inode`函数(只显示主干部分),这个函数创建inode结构,**还没**创建对应的f2fs inode page 126 | 127 | ```c 128 | static struct inode *f2fs_new_inode(struct inode *dir, umode_t mode) 129 | { 130 | struct f2fs_sb_info *sbi = F2FS_I_SB(dir); 131 | nid_t ino; 132 | struct inode *inode; 133 | bool nid_free = false; 134 | int xattr_size = 0; 135 | int err; 136 | 137 | inode = new_inode(dir->i_sb); // 先创建出来一个没有ino的inode结构,参考前面提及的创建流程 138 | 139 | if (!f2fs_alloc_nid(sbi, &ino)) { // 然后给这个inode分配一个nid,即ino 140 | goto fail; 141 | } 142 | 143 | nid_free = true; 144 | 145 | inode_init_owner(inode, dir, mode); // 初始化从属信息: 访问模式、父目录等 146 | 147 | inode->i_ino = ino; // 初始化一些元数据信息,例如ino 148 | inode->i_blocks = 0; 149 | inode->i_mtime = inode->i_atime = inode->i_ctime = current_time(inode); 150 | F2FS_I(inode)->i_crtime = inode->i_mtime; 151 | inode->i_generation = sbi->s_next_generation++; 152 | 153 | err = insert_inode_locked(inode); // 将这个inode插入到全局的inode table(VFS行为) 154 | 155 | set_inode_flag(inode, FI_NEW_INODE); // 注意这个标志位后面会用到 156 | 157 | ...... 158 | // 上面省略代码都在设置法f2fs_inode_info的flag,并在这个函数将部分flag设置到vfs inode中 159 | f2fs_set_inode_flags(inode); 160 | return inode; 161 | } 162 | ``` 163 | ### f2fs_add_link函数 164 | 165 | 经过上面的函数,我们已经创建了一个f2fs使用的vfs inode,接下来我们要将这个inode链接到父目录的inode当中,建立联系,`f2fs_add_link`函数直接会调用`f2fs_do_add_link`函数,因此我们直接分析这个函数。其中`f2fs_dir_entry`代表是目录项,具体的作用含义在目录项的作用相关章节(新坑待填)介绍,这里可以理解为**父目录包含了多个子文件/目录项,每一个目录项对应一个子文件/子目录的关联信息。我们将上一节新创建的inode加入到父目录的管理,也就是在父目录中为这个新inode下创建一个目录项。** 166 | 167 | ```c 168 | static inline int f2fs_add_link(struct dentry *dentry, struct inode *inode) 169 | { 170 | // 这里的dentry就是新inode的dentry 171 | return f2fs_do_add_link(d_inode(dentry->d_parent), &dentry->d_name, 172 | inode, inode->i_ino, inode->i_mode); 173 | } 174 | 175 | // dir是父目录 176 | int f2fs_do_add_link(struct inode *dir, const struct qstr *name, 177 | struct inode *inode, nid_t ino, umode_t mode) 178 | { 179 | struct f2fs_dir_entry *de = NULL; // 父目录dir的目录项,初始化为NULL 180 | int err; 181 | // 如果文件已经加密,则获得解密后的名字fname 182 | err = fscrypt_setup_filename(dir, name, 0, &fname); 183 | if (de) { // 如果找到目录项 184 | f2fs_put_page(page, 0); 185 | err = -EEXIST; 186 | } else if (IS_ERR(page)) { 187 | err = PTR_ERR(page); 188 | } else { // 对于一个新inode,它对应的父目录的目录项f2fs_dir_entry应该是不存在的 189 | err = f2fs_add_dentry(dir, &fname, inode, ino, mode); 190 | } 191 | return err; 192 | } 193 | ``` 194 | 195 | 这个`f2fs_add_dentry`函数提取了文件名字的字符串以及字符串长度: 196 | 197 | ```c 198 | int f2fs_add_dentry(struct inode *dir, struct fscrypt_name *fname, 199 | struct inode *inode, nid_t ino, umode_t mode) 200 | { 201 | struct qstr new_name; 202 | int err = -EAGAIN; 203 | 204 | new_name.name = fname_name(fname); // 将文件名的字符串格式保存在这里 205 | new_name.len = fname_len(fname); // 将文件名的长度保存在这里 206 | 207 | // 在这个函数实现新inode和父inode的链接 208 | err = f2fs_add_regular_entry(dir, &new_name, fname->usr_fname, 209 | inode, ino, mode); 210 | 211 | f2fs_update_time(F2FS_I_SB(dir), REQ_TIME); // 更新修改时间 212 | return err; 213 | } 214 | ``` 215 | 216 | 新inode的`f2fs_dir_entry`应该是不存在的,注意我们f2fs_new_inode函数一节提到的`FI_NEW_INODE`的flag。 217 | 218 | ```c 219 | int f2fs_add_regular_entry(struct inode *dir, const struct qstr *new_name, 220 | const struct qstr *orig_name, 221 | struct inode *inode, nid_t ino, umode_t mode) 222 | { 223 | ... 224 | // 上面的机制比较复杂,在这里不提,在目录项的作用相关章节再提 225 | // 上面做了一大堆事情可以理解为,根据[文件名的长度]创建一个新的f2fs_dir_entry,然后加入到父目录当中 226 | // 需要注意的是这个f2fs_dir_entry还没有包含新inode的信息 227 | 228 | // 接下来就是要做的就是 229 | // 1. 为新的vfs inode创建inode page,初始化与父目录有关的信息 230 | // 2. 基于新inode的信息(名字,ino等)更新f2fs_dir_entry 231 | 232 | if (inode) { 233 | // 这个函数就是创建inode page,初始化与父目录有关的信息 234 | page = f2fs_init_inode_metadata(inode, dir, new_name, 235 | orig_name, NULL); 236 | } 237 | 238 | 239 | // 基于新inode的信息(名字,ino等)更新f2fs_dir_entry 240 | f2fs_update_dentry(ino, mode, &d, new_name, dentry_hash, bit_pos); 241 | 242 | set_page_dirty(dentry_page); 243 | f2fs_update_parent_metadata(dir, inode, current_depth); // 清除FI_NEW_INODE的flag 244 | return err; 245 | } 246 | ``` 247 | 248 | 由于新inode设置了`FI_NEW_INODE`,因此`f2fs_init_inode_metadata`函数就是完成了两个功能: 249 | 250 | 1. 创建一个新的inode page,然后初始化acl、security等信息。 251 | 2. 然后初始化新创建的inode page的名字 252 | 3. 再增加inode的引入链接。 253 | 254 | ```c 255 | struct page *f2fs_init_inode_metadata(struct inode *inode, struct inode *dir, 256 | const struct qstr *new_name, const struct qstr *orig_name, 257 | struct page *dpage) 258 | { 259 | struct page *page; 260 | int err; 261 | 262 | // 由于新inode设置了FI_NEW_INODE 263 | if (is_inode_flag_set(inode, FI_NEW_INODE)) { 264 | // 创建一个新的inode page,然后初始化acl、security等信息。 265 | page = f2fs_new_inode_page(inode); 266 | 267 | err = f2fs_init_acl(inode, dir, page, dpage); 268 | if (err) 269 | goto put_error; 270 | 271 | err = f2fs_init_security(inode, dir, orig_name, page); 272 | if (err) 273 | goto put_error; 274 | } 275 | } else { 276 | page = f2fs_get_node_page(F2FS_I_SB(dir), inode->i_ino); 277 | if (IS_ERR(page)) 278 | return page; 279 | } 280 | 281 | if (new_name) { // 然后初始化新创建的inode page的名字 282 | init_dent_inode(new_name, page); 283 | if (f2fs_encrypted_inode(dir)) 284 | file_set_enc_name(inode); 285 | } 286 | // 再增加inode的引入链接。 287 | if (is_inode_flag_set(inode, FI_INC_LINK)) 288 | f2fs_i_links_write(inode, true); 289 | return page; 290 | } 291 | ``` 292 | 将新的inode链接到父目录后,后续用户访问时,可以通过父目录找到新创建的文件的inode,即完成了整个文件的创建流程。 293 | -------------------------------------------------------------------------------- /File-Creation-and-Deletion/文件删除.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/File-Creation-and-Deletion/文件删除.md -------------------------------------------------------------------------------- /File-Creation-and-Deletion/目录创建.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/File-Creation-and-Deletion/目录创建.md -------------------------------------------------------------------------------- /File-Creation-and-Deletion/目录删除.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/File-Creation-and-Deletion/目录删除.md -------------------------------------------------------------------------------- /ImportantDataStructure/curseg.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/ImportantDataStructure/curseg.md -------------------------------------------------------------------------------- /ImportantDataStructure/f2fs_do_fsync_file.md: -------------------------------------------------------------------------------- 1 | ## F2FS的sync流程 2 | 应用程序很多时候会调用fsync同步文件,在f2fs中会调用`f2fs_do_sync_file`函数完成文件的同步过程,简化后的代码代码如下,fsync的过程涉及了很多inode的flag相关的信息,因此接下来分开几个部分分析: 3 | 4 | ```c 5 | static int f2fs_do_sync_file(struct file *file, loff_t start, loff_t end, 6 | int datasync, bool atomic) 7 | { 8 | enum cp_reason_type cp_reason = 0; 9 | struct writeback_control wbc = { 10 | .sync_mode = WB_SYNC_ALL, 11 | .nr_to_write = LONG_MAX, 12 | .for_reclaim = 0, 13 | }; 14 | 15 | /* 1. FI_NEED_IPU-fdatasyn以及少量文件改动使用就地更新 */ 16 | if (datasync || get_dirty_pages(inode) <= SM_I(sbi)->min_fsync_blocks) 17 | set_inode_flag(inode, FI_NEED_IPU); 18 | ret = file_write_and_wait_range(file, start, end); 19 | clear_inode_flag(inode, FI_NEED_IPU); 20 | 21 | /* 2. f2fs_skip_inode_update这个函数用于判断inode是否为脏,如果是脏,这个函数会返回false */ 22 | if (!f2fs_skip_inode_update(inode, datasync)) { 23 | f2fs_write_inode(inode, NULL); 24 | goto go_write; 25 | } 26 | 27 | if (!is_inode_flag_set(inode, FI_APPEND_WRITE) && 28 | !f2fs_exist_written_data(sbi, ino, APPEND_INO)) { 29 | 30 | /* it may call write_inode just prior to fsync */ 31 | if (need_inode_page_update(sbi, ino)) 32 | goto go_write; 33 | 34 | if (is_inode_flag_set(inode, FI_UPDATE_WRITE) || 35 | f2fs_exist_written_data(sbi, ino, UPDATE_INO)) 36 | goto flush_out; 37 | goto out; 38 | } 39 | go_write: 40 | down_read(&F2FS_I(inode)->i_sem); 41 | cp_reason = need_do_checkpoint(inode); 42 | up_read(&F2FS_I(inode)->i_sem); 43 | 44 | if (cp_reason) { 45 | ret = f2fs_sync_fs(inode->i_sb, 1); 46 | try_to_fix_pino(inode); 47 | clear_inode_flag(inode, FI_APPEND_WRITE); 48 | clear_inode_flag(inode, FI_UPDATE_WRITE); 49 | goto out; 50 | } 51 | 52 | sync_nodes: 53 | atomic_inc(&sbi->wb_sync_req[NODE]); 54 | ret = f2fs_fsync_node_pages(sbi, inode, &wbc, atomic); 55 | atomic_dec(&sbi->wb_sync_req[NODE]); 56 | if (ret) 57 | goto out; 58 | 59 | if (f2fs_need_inode_block_update(sbi, ino)) { 60 | f2fs_mark_inode_dirty_sync(inode, true); 61 | f2fs_write_inode(inode, NULL); 62 | goto sync_nodes; 63 | } 64 | 65 | if (!atomic) { 66 | ret = f2fs_wait_on_node_pages_writeback(sbi, ino); 67 | if (ret) 68 | goto out; 69 | } 70 | 71 | f2fs_remove_ino_entry(sbi, ino, APPEND_INO); 72 | clear_inode_flag(inode, FI_APPEND_WRITE); 73 | flush_out: 74 | if (!atomic && F2FS_OPTION(sbi).fsync_mode != FSYNC_MODE_NOBARRIER) 75 | ret = f2fs_issue_flush(sbi, inode->i_ino); 76 | if (!ret) { 77 | f2fs_remove_ino_entry(sbi, ino, UPDATE_INO); 78 | clear_inode_flag(inode, FI_UPDATE_WRITE); 79 | f2fs_remove_ino_entry(sbi, ino, FLUSH_INO); 80 | } 81 | f2fs_update_time(sbi, REQ_TIME); 82 | out: 83 | trace_f2fs_sync_file_exit(inode, cp_reason, datasync, ret); 84 | f2fs_trace_ios(NULL, 1); 85 | printk("[hubery] end f2fs_do_sync_file (%d,%d)thread=%s\n", current->pid, current->tgid, current->comm); 86 | return ret; 87 | } 88 | ``` 89 | 90 | ### FI_NEED_IPU-fdatasyn以及少量文件改动使用就地更新 91 | 当改动文件少量的数据时,为了减少IO量,使用了会使用就地更新的策略:在fsync函数中,给inode设置`FI_NEED_IPU`标记,再writepages函数中,检测到该标记会使用就地更新的策略。 92 | ```c 93 | if (datasync || get_dirty_pages(inode) <= SM_I(sbi)->min_fsync_blocks) 94 | set_inode_flag(inode, FI_NEED_IPU); // 当dirty pages少于min_fsync_blocks时,设置标记 95 | ret = file_write_and_wait_range(file, start, end); // 调用writepages刷写入磁盘 96 | clear_inode_flag(inode, FI_NEED_IPU); // 再清除标记 97 | ``` 98 | writepages会调用`f2fs_do_write_data_page`进行写入,该函数有会调用`need_inplace_update`判断是否包含`FI_NEED_IPU`标记,如果包含则进行就地更新。 99 | ```c 100 | int f2fs_do_write_data_page(struct f2fs_io_info *fio) 101 | { 102 | ... 103 | 104 | if (ipu_force || (is_valid_blkaddr(fio->old_blkaddr) && need_inplace_update(fio))) { 105 | ... 106 | err = f2fs_inplace_write_data(fio); // 进行就地写入 107 | ... 108 | return err; 109 | } 110 | ... 111 | } 112 | ``` 113 | 114 | ### FI_AUTO_RECOVER-可以判断inode是否为脏 115 | 如果对一个不是没有`FI_DIRTY_INODE`标记的inode进行size等信息进行修改,会加上`FI_AUTO_RECOVER`等信息,当inode被设置`FI_DIRTY_INODE`标记的时候或者inode被fsync之后,就会被清除。 116 | ```c 117 | static inline bool f2fs_skip_inode_update(struct inode *inode, int dsync) 118 | { 119 | ... 120 | if (!is_inode_flag_set(inode, FI_AUTO_RECOVER) || 121 | file_keep_isize(inode) || 122 | i_size_read(inode) & ~PAGE_MASK) // 这个if判断了没有设置FI_AUTO_RECOVER标记,则认为是脏的 123 | return false; 124 | } 125 | 126 | ``` 127 | 128 | 129 | ### FI_APPEND_WRITE & FI_UPDATE_WRITE-用于判断inode已经没有脏数据可以写 130 | FI_APPEND_WRITE----->在异地更新的时候设置 131 | FI_UPDATE_WRITE----->在就地更新的时候设置 132 | 这两个标记在fsync的时候的都会清除,因此fsync会使用这两个标记判断是否已经没有dirty pages 133 | ```c 134 | if (!is_inode_flag_set(inode, FI_APPEND_WRITE) && 135 | !f2fs_exist_written_data(sbi, ino, APPEND_INO)) { // 判断FI_APPEND_WRITE标记 136 | 137 | if (need_inode_page_update(sbi, ino)) 138 | goto go_write; 139 | 140 | if (is_inode_flag_set(inode, FI_UPDATE_WRITE) || 141 | f2fs_exist_written_data(sbi, ino, UPDATE_INO)) // 判断FI_UPDATE_WRITE标记,如果还是存在就要flush 142 | goto flush_out; // 如果 143 | goto out; // 如果都没有就直接退出fsync函数 144 | } 145 | ``` 146 | 147 | ### fsync的回写 148 | 如果无法在`f2fs_skip_inode_update`函数跳过,则要进行各个page的回写 149 | 150 | 第一步检查是否需要回写checkpoint,如果需要则写完checkpoint后退出fsync 151 | 152 | ```c 153 | down_read(&F2FS_I(inode)->i_sem); 154 | cp_reason = need_do_checkpoint(inode); 155 | up_read(&F2FS_I(inode)->i_sem); 156 | 157 | if (cp_reason) { 158 | ret = f2fs_sync_fs(inode->i_sb, 1); // 这个函数用于调用write_checkpoint,回写一次checkpoint 159 | try_to_fix_pino(inode); 160 | clear_inode_flag(inode, FI_APPEND_WRITE); 161 | clear_inode_flag(inode, FI_UPDATE_WRITE); 162 | goto out; 163 | } 164 | ``` 165 | 166 | 如果不需要回写checkpoint,则回写所有dirty的node pages。 167 | ```c 168 | atomic_inc(&sbi->wb_sync_req[NODE]); 169 | ret = f2fs_fsync_node_pages(sbi, inode, &wbc, atomic); // 回写所有的dirty node pages 170 | atomic_dec(&sbi->wb_sync_req[NODE]); 171 | 172 | if (f2fs_need_inode_block_update(sbi, ino)) { 173 | f2fs_mark_inode_dirty_sync(inode, true); 174 | f2fs_write_inode(inode, NULL); 175 | goto sync_nodes; 176 | } 177 | 178 | if (!atomic) { 179 | ret = f2fs_wait_on_node_pages_writeback(sbi, ino); 180 | if (ret) 181 | goto out; 182 | } 183 | 184 | f2fs_remove_ino_entry(sbi, ino, APPEND_INO); 185 | clear_inode_flag(inode, FI_APPEND_WRITE); 186 | ``` 187 | 188 | -------------------------------------------------------------------------------- /ImportantDataStructure/f2fs_fill_super.md: -------------------------------------------------------------------------------- 1 | # f2fs_fill_super的流程 2 | `f2fs_fill_super` 是加载F2FS文件系统的第一步,主要作用是读取磁盘的前端区域的数据,对F2FS元区域数据进行初始化。 3 | 4 | ```c 5 | 6 | ``` 7 | -------------------------------------------------------------------------------- /ImportantDataStructure/f2fs_journal.md: -------------------------------------------------------------------------------- 1 | ## F2FS Journal机制 2 | ### Journal机制的介绍 3 | 当F2FS进行文件读写的时候,根据 `f2fs_node` 的设计以及闪存设备异地更新的特性,每修改一个数据块,都需要改动 `f2fs_node` 的地址映射,以及NAT,SIT等信息。但是如果仅仅因为一个小改动,例如修改一个块,就需要改动这么多数据,然后再写入磁盘,这样既会导致性能下降,也会导致SSD寿命的下降。因此F2FS设计了journal机制,用于将这些对数据的修改会暂存在 `f2fs_journal` ,等待系统进行checkpoint的时候,再写入磁盘当中。 4 | 部分内容参考: https://blog.csdn.net/sunwukong54/article/details/45669017 5 | ### Journal涉及到的数据结构 6 | ```c 7 | struct f2fs_journal { 8 | union { 9 | __le16 n_nats; /* 这个journal里面包含多少个nat_journal对象 */ 10 | __le16 n_sits; /* 这个journal里面包含多少个sit_journal对象 */ 11 | }; 12 | /* spare area is used by NAT or SIT journals or extra info */ 13 | union { 14 | struct nat_journal nat_j; 15 | struct sit_journal sit_j; 16 | struct f2fs_extra_info info; 17 | }; 18 | } __packed; 19 | ``` 20 | `f2fs_journal` 可以保存NAT的journal也可以保存SIT的journal,以下分别分析: 21 | 22 | **NAT Journal** 23 | NAT类型的journal主要保存的每一个node是属于哪一个inode,以及它的地址是什么,这样设计的原始访问某一个node的时候,只要根据nid找到对应的 `nat_journal_entry` ,然后就可以找到 `f2fs_nat_entry` ,最后找到blkaddr。 24 | ```c 25 | struct nat_journal { 26 | struct nat_journal_entry entries[NAT_JOURNAL_ENTRIES]; 27 | __u8 reserved[NAT_JOURNAL_RESERVED]; 28 | } __packed; 29 | 30 | struct nat_journal_entry { 31 | __le32 nid; 32 | struct f2fs_nat_entry ne; 33 | } __packed; 34 | 35 | struct f2fs_nat_entry { 36 | __u8 version; /* latest version of cached nat entry */ 37 | __le32 ino; /* inode number */ 38 | __le32 block_addr; /* block address */ 39 | } __packed; 40 | ``` 41 | **SIT Journal** 42 | SIT类型的Journal和segment一一对应。segment管理着512个block,需要一种机制去记录每一个它所管理的block是否已经被分配出去。 通过 `f2fs_sit_entry` 可以发现,`f2fs_journal` 保存的是有效block的数目 `vblocks` 以及 它的bitmap `valid_map`。 43 | ```c 44 | struct sit_journal { 45 | struct sit_journal_entry entries[SIT_JOURNAL_ENTRIES]; 46 | __u8 reserved[SIT_JOURNAL_RESERVED]; 47 | } __packed; 48 | 49 | struct sit_journal_entry { 50 | __le32 segno; 51 | struct f2fs_sit_entry se; 52 | } __packed; 53 | 54 | struct f2fs_sit_entry { 55 | __le16 vblocks; /* reference above */ 56 | __u8 valid_map[SIT_VBLOCK_MAP_SIZE]; /* SIT_VBLOCK_MAP_SIZE = 64,64 * 8 = 512 可以表示每一个块的valid状态 */ 57 | __le64 mtime; /* segment age for cleaning */ 58 | } __packed; 59 | ``` 60 | 61 | ### Journal一些机制的具体实现 62 | 63 | #### 通过Journal获取Node的地址 64 | ```c 65 | void f2fs_get_node_info(struct f2fs_sb_info *sbi, nid_t nid, 66 | struct node_info *ni) 67 | { 68 | struct f2fs_nm_info *nm_i = NM_I(sbi); 69 | struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_HOT_DATA); 70 | struct f2fs_journal *journal = curseg->journal; 71 | nid_t start_nid = START_NID(nid); 72 | struct f2fs_nat_block *nat_blk; 73 | struct page *page = NULL; 74 | struct f2fs_nat_entry ne; 75 | struct nat_entry *e; 76 | pgoff_t index; 77 | int i; 78 | 79 | ni->nid = nid; 80 | 81 | /* Check nat cache */ 82 | down_read(&nm_i->nat_tree_lock); 83 | e = __lookup_nat_cache(nm_i, nid); // 从cache里面找nid 84 | if (e) { // 如果有就返回 85 | ni->ino = nat_get_ino(e); 86 | ni->blk_addr = nat_get_blkaddr(e); 87 | ni->version = nat_get_version(e); 88 | up_read(&nm_i->nat_tree_lock); 89 | return; 90 | } 91 | 92 | memset(&ne, 0, sizeof(struct f2fs_nat_entry)); // 初始化为0 93 | 94 | /* Check current segment summary */ 95 | down_read(&curseg->journal_rwsem); 96 | i = f2fs_lookup_journal_in_cursum(journal, NAT_JOURNAL, nid, 0); // 从NAT_JOURNAL里面找这个nid在journal中的offset 97 | if (i >= 0) { 98 | ne = nat_in_journal(journal, i); // 将nat_entry返回出来 99 | node_info_from_raw_nat(ni, &ne); // 读到node_info中 100 | } 101 | up_read(&curseg->journal_rwsem); 102 | if (i >= 0) { 103 | up_read(&nm_i->nat_tree_lock); 104 | goto cache; 105 | } 106 | 107 | /* 108 | * Fill node_info from nat page 109 | * start_nid是根据nid找到管理这个nid的nat block偏移 110 | * */ 111 | index = current_nat_addr(sbi, nid); 112 | up_read(&nm_i->nat_tree_lock); 113 | 114 | page = f2fs_get_meta_page(sbi, index); // 从磁盘读取出f2fs_nat_block 115 | nat_blk = (struct f2fs_nat_block *)page_address(page); 116 | ne = nat_blk->entries[nid - start_nid]; 117 | node_info_from_raw_nat(ni, &ne); 118 | f2fs_put_page(page, 1); 119 | cache: 120 | /* cache nat entry */ 121 | cache_nat_entry(sbi, nid, &ne); // 缓存这个node_entry 122 | } 123 | ``` 124 | 125 | #### 通过Checkpoint将journal的信息写入到磁盘中 126 | [具体参考Checkpoint的流程](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Checkpoint/Checkpoint%E6%B5%81%E7%A8%8B.md), 简略的流程如下: 127 | 1. `f2fs_flush_nat_entries` 和 `f2fs_flush_sit_entries` 函数将entry都写入到 `curseg_info->f2fs_summary->journal` 的变量中。 128 | 2. `do_checkpoint函数` 读取 ``curseg_info->f2fs_summary` ,然后通过函数 `f2fs_write_node_summaries` 或 `f2fs_write_data_summaries` 刷写到磁盘中。 -------------------------------------------------------------------------------- /ImportantDataStructure/f2fs_map_blocks.md: -------------------------------------------------------------------------------- 1 | ## f2fs_map_blocks的作用与源码分析 2 | 函数 `f2fs_map_blocks` 启到了地址映射的作用,主要作用是通过逻辑地址找到可以访问磁盘的物理地址。 3 | 4 | ### f2fs_map_blocks的读写流程的作用 5 | 1. 对读的作用: 通过该函数根据逻辑地址找到物理地址,然后从磁盘读取出数据。 6 | 2. 对写的作用: 文件在写入数据之前,会执行一个preallocate的过程,这个过程会调用 `f2fs_map_blocks` 函数对即将要写入数据的逻辑块进行预处理,如果是append的方式写入数据,则将物理地址初始化为NEW_ADDR; 如果是rewrite的方式写入数据,则不作改变。 7 | 8 | ### f2fs_map_blocks的核心数据结构 9 | `f2fs_map_blocks` 函数的核心是 `f2fs_map_blocks` 数据结构,如下所示,它保存了一系列映射信息。 10 | ```c 11 | struct f2fs_map_blocks { 12 | block_t m_pblk; // 保存的是物理地址,可以通过这个物理地址访问磁盘读取信息 13 | block_t m_lblk; // 保存的逻辑地址,即文件的page->index 14 | unsigned int m_len; // 需要读取的长度 15 | unsigned int m_flags; // flags表示获取数据状态,如F2FS_MAP_MAPPED 16 | pgoff_t *m_next_pgofs; // 指向下一个offset 17 | }; 18 | ``` 19 | 20 | ### 读流程下的f2fs_map_blocks的核心逻辑 21 | 22 | 一般的读流程,会进行如下的数据结构初始化: 23 | ```c 24 | map.m_lblk = block_in_file; // 设置逻辑地址page->index 25 | map.m_len = len; // 设置需要读取的长度 26 | f2fs_map_blocks(inode, &map, 0, F2FS_GET_BLOCK_READ); // 0设定非创建模式,F2FS_GET_BLOCK_READ设定搜索模式 27 | ``` 28 | 即通过逻辑地址和读取长度找到对应的物理地址,与**读流程相关的核心逻辑**如下面代码所示: 29 | ```c 30 | int f2fs_map_blocks(struct inode *inode, struct f2fs_map_blocks *map, int create, int flag) 31 | { 32 | unsigned int maxblocks = map->m_len; // 设定最大搜索长度 33 | int mode = create ? ALLOC_NODE : LOOKUP_NODE_RA; // LOOKUP_NODE_RA模式 34 | 35 | map->m_len = 0; // 将len重新设置为0 36 | map->m_flags = 0; 37 | pgofs = (pgoff_t)map->m_lblk; // page->index 38 | 39 | // 第一步:先从extent找,如果在extent找到,就可以马上返回 40 | if (!create && f2fs_lookup_extent_cache(inode, pgofs, &ei)) { 41 | map->m_pblk = ei.blk + pgofs - ei.fofs; 42 | map->m_len = min((pgoff_t)maxblocks, ei.fofs + ei.len - pgofs); 43 | map->m_flags = F2FS_MAP_MAPPED; 44 | goto out; 45 | } 46 | 47 | // 第二步:根据page->index找到对应的dn,dn是一个包含了物理地址的数据结构 48 | set_new_dnode(&dn, inode, NULL, NULL, 0); 49 | err = get_dnode_of_data(&dn, pgofs, mode); 50 | 51 | // 第三步:从dn获取物理地址 52 | blkaddr = datablock_addr(dn.node_page, dn.ofs_in_node); 53 | ... 54 | map->m_pblk = blkaddr; 55 | ... 56 | return err; 57 | } 58 | ``` 59 | 60 | ### 写流程下的f2fs_map_blocks的核心逻辑 61 | 一般的读流程,会进行如下的数据结构初始化: 62 | ```c 63 | map.m_lblk = F2FS_BLK_ALIGN(iocb->ki_pos); // 计算得到页偏移 64 | map.m_len = F2FS_BYTES_TO_BLK(iocb->ki_pos + iov_iter_count(from)); // 计算得到需要读取的页数 65 | f2fs_map_blocks(inode, &map, 1, F2FS_GET_BLOCK_PRE_AIO); // 1设定创建模式,F2FS_GET_BLOCK_PRE_AIO表示用于预分配物理页 66 | ``` 67 | 写流程下的 `f2fs_map_blocks` 函数作用是先根据逻辑地址读取物理地址出来,如果这个物理地址没有被分配过(NULL_ADDR),则初始化为新地址(NEW_ADDR),用于下一步的写入磁盘的操作,与**写流程相关的核心逻辑**如下面代码所示: 68 | ```c 69 | 70 | int f2fs_map_blocks(struct inode *inode, struct f2fs_map_blocks *map, 71 | int create, int flag) 72 | { 73 | unsigned int maxblocks = map->m_len; 74 | int mode = create ? ALLOC_NODE : LOOKUP_NODE; 75 | 76 | map->m_len = 0; 77 | map->m_flags = 0; 78 | 79 | pgofs = (pgoff_t)map->m_lblk; 80 | end = pgofs + maxblocks; 81 | 82 | // 第一步:根据page->index找到对应的dn,dn是一个包含了物理地址的数据结构 83 | set_new_dnode(&dn, inode, NULL, NULL, 0); 84 | err = f2fs_get_dnode_of_data(&dn, pgofs, mode); 85 | 86 | // 第二步:从dn获取物理地址 87 | blkaddr = datablock_addr(dn.inode, dn.node_page, dn.ofs_in_node); 88 | 89 | // 第三步:如果blkaddr == NULL_ADDR表示这个是从来未使用过的物理页,即目前运行的是append写, 90 | // 因此将其记录下来。 91 | if (flag == F2FS_GET_BLOCK_PRE_AIO) { 92 | if (blkaddr == NULL_ADDR) { 93 | prealloc++; // 记录需要与分配的物理页的数目 94 | last_ofs_in_node = dn.ofs_in_node; 95 | } 96 | } 97 | 98 | if (flag == F2FS_GET_BLOCK_PRE_AIO && 99 | (pgofs == end || dn.ofs_in_node == end_offset)) { 100 | 101 | dn.ofs_in_node = ofs_in_node; 102 | // 第四步:根据prealloc记录的从未被使用过的块的数目, 103 | // 通过函数f2fs_reserve_new_blocks,将他们的值由NULL_ADDR转换为NEW_ADDR,用于下一步写入磁盘 104 | err = f2fs_reserve_new_blocks(&dn, prealloc); 105 | if (err) 106 | goto sync_out; 107 | 108 | map->m_len += dn.ofs_in_node - ofs_in_node; 109 | if (prealloc && dn.ofs_in_node != last_ofs_in_node + 1) { 110 | err = -ENOSPC; 111 | goto sync_out; 112 | } 113 | dn.ofs_in_node = end_offset; 114 | } 115 | 116 | ... 117 | return err; 118 | } 119 | ``` -------------------------------------------------------------------------------- /ImportantDataStructure/f2fs_rename.md: -------------------------------------------------------------------------------- 1 | # F2FS的rename流程 2 | 3 | ## rename流程介绍 4 | 5 | 1. sys_rename函数 6 | 2. do_renameat2函数 7 | 3. vfs_rename函数 8 | 4. f2fs_rename函数 9 | 10 | 11 | 12 | ## sys_rename函数 13 | 14 | sys_rename函数是一个系统调用,是rename函数进入内核层的第一个函数: 15 | 16 | ```c 17 | SYSCALL_DEFINE2(rename, const char __user *, oldname, const char __user *, newname) 18 | { 19 | // AT_FDCWD表示以相对路径的方法找oldname和newname这个文件,flags=0 20 | return do_renameat2(AT_FDCWD, oldname, AT_FDCWD, newname, 0); 21 | } 22 | ``` 23 | 24 | ## do_renameat2函数 25 | 26 | do_renameat2函数比较长,考虑多个输入flag的作用,这里只考虑sys_rename函数rename一个文件的情形,即flag=0,并以此精简函数。 27 | 28 | ```c 29 | static int do_renameat2(int olddfd, const char __user *oldname, int newdfd, 30 | const char __user *newname, unsigned int flags) 31 | { 32 | struct dentry *old_dentry, *new_dentry; 33 | struct dentry *trap; 34 | struct path old_path, new_path; 35 | struct qstr old_last, new_last; 36 | int old_type, new_type; 37 | struct inode *delegated_inode = NULL; 38 | struct filename *from; 39 | struct filename *to; 40 | unsigned int lookup_flags = 0, target_flags = LOOKUP_RENAME_TARGET; 41 | bool should_retry = false; 42 | int error; 43 | 44 | retry: 45 | // 接下来两个函数最重要的作用是根据oldname和newname找到父目录的dentry结构 46 | // 这两个dentry结构保存在old_path和new_path中(注意是父目录的dentry) 47 | from = filename_parentat(olddfd, getname(oldname), lookup_flags, 48 | &old_path, &old_last, &old_type); 49 | 50 | to = filename_parentat(newdfd, getname(newname), lookup_flags, 51 | &new_path, &new_last, &new_type); 52 | 53 | retry_deleg: 54 | // 这个函数会触发一个全局的rename的互斥锁,然后锁两个父目录inode结构 55 | trap = lock_rename(new_path.dentry, old_path.dentry); 56 | // 根据old path的父目录找到需要被rename的文件的dentry 57 | old_dentry = __lookup_hash(&old_last, old_path.dentry, lookup_flags); 58 | // 根据new path的父目录找到或创建新的dentry 59 | new_dentry = __lookup_hash(&new_last, new_path.dentry, lookup_flags | target_flags); 60 | // 调用vfs_rename函数进行重命名 61 | // 传入的是新旧两个目录的inode,以及需要重命名的两个dentry, flags = 0 62 | error = vfs_rename(old_path.dentry->d_inode, old_dentry, 63 | new_path.dentry->d_inode, new_dentry, 64 | &delegated_inode, flags); 65 | 66 | dput(new_dentry); 67 | 68 | dput(old_dentry); 69 | // 解锁全局rename互斥锁,释放两个inode锁 70 | unlock_rename(new_path.dentry, old_path.dentry); 71 | 72 | path_put(&new_path); 73 | putname(to); 74 | 75 | path_put(&old_path); 76 | putname(from); 77 | exit: 78 | return error; 79 | } 80 | ``` 81 | 82 | ## vfs_rename函数 83 | 84 | vfs_rename函数也会做简化,简化的情形是将文件A重命名到文件B (B可能已经存在,或者不存在),flags=0。 85 | 86 | ```c 87 | int vfs_rename(struct inode *old_dir, struct dentry *old_dentry, 88 | struct inode *new_dir, struct dentry *new_dentry, 89 | struct inode **delegated_inode, unsigned int flags) 90 | { 91 | int error; 92 | bool is_dir = d_is_dir(old_dentry); 93 | struct inode *source = old_dentry->d_inode; // 旧文件inode 94 | struct inode *target = new_dentry->d_inode; // 新文件inode 95 | bool new_is_dir = false; 96 | unsigned max_links = new_dir->i_sb->s_max_links; 97 | struct name_snapshot old_name; 98 | 99 | dget(new_dentry); // 对新文件的引用计数+1 100 | if (target) 101 | inode_lock(target); // 如果新文件已经存在,则上锁 102 | 103 | 104 | error = old_dir->i_op->rename(old_dir, old_dentry, 105 | new_dir, new_dentry, flags); 106 | 107 | 108 | out: 109 | if (target) 110 | inode_unlock(target); // 如果新文件已经存在,则解锁 111 | dput(new_dentry); // 对新文件的引用计数-1 112 | return error; 113 | } 114 | ``` 115 | 116 | ## f2fs_rename函数 117 | 118 | f2fs_rename函数也会做简化,简化的情形是将文件A重命名到文件B (B可能已经存在,或者不存在),flags=0。 119 | 120 | ```c 121 | static int f2fs_rename(struct inode *old_dir, struct dentry *old_dentry, 122 | struct inode *new_dir, struct dentry *new_dentry, 123 | unsigned int flags) 124 | { 125 | struct f2fs_sb_info *sbi = F2FS_I_SB(old_dir); 126 | struct inode *old_inode = d_inode(old_dentry); 127 | struct inode *new_inode = d_inode(new_dentry); 128 | struct inode *whiteout = NULL; 129 | struct page *old_dir_page; 130 | struct page *old_page, *new_page = NULL; 131 | struct f2fs_dir_entry *old_dir_entry = NULL; 132 | struct f2fs_dir_entry *old_entry; 133 | struct f2fs_dir_entry *new_entry; 134 | bool is_old_inline = f2fs_has_inline_dentry(old_dir); 135 | int err; 136 | 137 | // 输入显然是 138 | // 旧的父目录old_dir,旧的文件old_dentry 139 | // 新的父目录new_dir,新的文件new_dentry 140 | 141 | // 根据旧文件的名字找到对应的f2fs_dir_entry,old_page保存的是磁盘上的dir_entry数据 142 | old_entry = f2fs_find_entry(old_dir, &old_dentry->d_name, &old_page); 143 | 144 | if (new_inode) { // 如果新文件已经存在 145 | 146 | // 根据新文件的名字找到对应的f2fs_dir_entry,new_page保存的是磁盘上的数据 147 | new_entry = f2fs_find_entry(new_dir, &new_dentry->d_name, 148 | &new_page); 149 | 150 | // F2FS获取一个全局读信号量 151 | f2fs_lock_op(sbi); 152 | 153 | // 在管理orphan inode的全局结构中,将orphan inode的数目+1。 154 | err = f2fs_acquire_orphan_inode(sbi); 155 | 156 | // 这里进行新旧inode的link的变化: 157 | // 将new_dentry所属的inode指向old_inode 158 | // 因为rename的时候新inode是已经存在了,因此rename的操作就是将 159 | // 新路径原来的inode无效掉,然后替换为旧路径的inode 160 | f2fs_set_link(new_dir, new_entry, new_page, old_inode); 161 | 162 | new_inode->i_ctime = current_time(new_inode); 163 | 164 | 165 | down_write(&F2FS_I(new_inode)->i_sem); // 拿写信号量 166 | // 减少新inode一个引用计数,因为被rename了 167 | f2fs_i_links_write(new_inode, false); 168 | up_write(&F2FS_I(new_inode)->i_sem); // 释放写信号量 169 | 170 | // 如果引用计数下降到0,则添加到orphan inode中,在checkpoint管理 171 | if (!new_inode->i_nlink) 172 | f2fs_add_orphan_inode(new_inode); 173 | else 174 | f2fs_release_orphan_inode(sbi); // 否则管理结构将orphan inode的数目-1。 175 | } else { 176 | 177 | // 这个情况是新路径的Inode不存在 178 | 179 | // F2FS获取一个全局读信号量 180 | f2fs_lock_op(sbi); 181 | 182 | // 由于新inode是不存在的,因此直接将旧inode添加到新的f2fs_dir_entry中 183 | err = f2fs_add_link(new_dentry, old_inode); 184 | 185 | } 186 | 187 | 188 | down_write(&F2FS_I(old_inode)->i_sem); 189 | if (!old_dir_entry || whiteout) 190 | file_lost_pino(old_inode); // 这个操作要保留着用于数据恢复 191 | else 192 | F2FS_I(old_inode)->i_pino = new_dir->i_ino; 193 | up_write(&F2FS_I(old_inode)->i_sem); 194 | 195 | old_inode->i_ctime = current_time(old_inode); 196 | f2fs_mark_inode_dirty_sync(old_inode, false); 197 | 198 | // 新的数据已经加入到新的f2fs_dir_entry,因此旧entry就去去除掉 199 | f2fs_delete_entry(old_entry, old_page, old_dir, NULL); 200 | 201 | // F2FS释放全局读信号量 202 | f2fs_unlock_op(sbi); 203 | 204 | f2fs_update_time(sbi, REQ_TIME); 205 | return 0; 206 | } 207 | ``` 208 | 209 | -------------------------------------------------------------------------------- /ImportantDataStructure/f2fs_summary.md: -------------------------------------------------------------------------------- 1 | ## f2fs_summary 和 f2fs_summary_block 2 | 3 | ### 介绍 4 | 因为每一个segment需要管理512个Block的地址,而且很多场合需要通过block地址找到这个block是属于哪一个node,以及属于这个node的第几个block。 `f2fs_summary` 主要保存了block->node的映射信息: 5 | 6 | ```c 7 | struct f2fs_summary { 8 | __le32 nid; /* parent node id */ 9 | union { 10 | __u8 reserved[3]; 11 | struct { 12 | __u8 version; /* node version number */ 13 | __le16 ofs_in_node; /* block index in parent node */ 14 | } __packed; 15 | }; 16 | } __packed; 17 | ``` 18 | 一个segment对应的512个 `f2fs_summary` 是通过一个4KB的block保存,`f2fs_summary_block` 保存在元数据区域的**SSA**区域: 19 | 20 | ```c 21 | struct f2fs_summary_block { 22 | struct f2fs_summary entries[ENTRIES_IN_SUM]; 23 | struct f2fs_journal journal; 24 | struct summary_footer footer; 25 | } __packed; 26 | 27 | struct summary_footer { 28 | unsigned char entry_type; /* SUM_TYPE_XXX */ 29 | __le32 check_sum; /* summary checksum */ 30 | } __packed; 31 | ``` 32 | 33 | 其中 `summary_footer` 记录了这个 `f2fs_summary_block` 的一些属性,如校验信息,以及这个 `f2fs_summary_block` 对应的segment所管理的block是属于node还是data。 34 | 35 | ### 应用场景 36 | 37 | #### GC流程 38 | 1. **GC基本流程:** 选一个无效block最多的当选择出需要gc的victim segment,然后将这个victim segment的block迁移插入到其他segment中,这样就可以制造出一个全部block都可以用的segment。 39 | 2. **f2fs_summary在GC的作用:** 当选择出需要gc的victim segment之后,可以通过这个victim segment的segno,在SSA区域找到 `f2fs_summary_block`。对victim segment的每一个block进行迁移的时候,会根据block的地址在 `f2fs_summary_block` 找到 它所对应的`f2fs_summary` 然后根据它所记录的 `f2fs_summary->nid` 以及 `f2fs_summary->ofs_in_node` 找到对应的具体的block的数据,然后将这些数据设置为dirty,然后等待vfs的writeback机制完成页迁移。 -------------------------------------------------------------------------------- /ImportantDataStructure/get_dnode.md: -------------------------------------------------------------------------------- 1 | # F2FS物理地址寻址的实现 2 | VFS的读写都依赖于物理地址的寻址。经典的读流程,VFS会传入inode以及page index信息给文件系统,然后文件系统需要根据以上信心,找到物理地址,然后访问磁盘将其读取出来。F2FS的物理地址寻址,是通过`f2fs_get_dnode_of_data`函数实现。 3 | 4 | 在执行这个`f2fs_get_dnode_of_data`函数之前,需要通过`set_new_dnode`函数进行对数据结构`struct dnode_of_data`进行初始化: 5 | ```c 6 | struct dnode_of_data { 7 | struct inode *inode; /* VFS inode结构 */ 8 | struct page *inode_page; /* f2fs_inode对应的node page */ 9 | struct page *node_page; /* 用户需要访问的物理地址所在的node page,有可能跟inode_page一样*/ 10 | nid_t nid; /* 用户需要访问的物理地址所在的node的nid,与上面的node_page对应*/ 11 | unsigned int ofs_in_node; /* 用户需要访问的物理地址位于上面的node_page对应的addr数组第几个位置 */ 12 | bool inode_page_locked; /* inode page is locked or not */ 13 | bool node_changed; /* is node block changed */ 14 | char cur_level; /* 当前node_page的层次,按直接访问或者简介访问的深度区分 */ 15 | char max_level; /* level of current page located */ 16 | block_t data_blkaddr; /* 用户需要访问的物理地址 */ 17 | }; 18 | 19 | static inline void set_new_dnode(struct dnode_of_data *dn, struct inode *inode, 20 | struct page *ipage, struct page *npage, nid_t nid) 21 | { 22 | memset(dn, 0, sizeof(*dn)); 23 | dn->inode = inode; 24 | dn->inode_page = ipage; 25 | dn->node_page = npage; 26 | dn->nid = nid; 27 | } 28 | ``` 29 | 大部分情况下,仅需要传入inode进行初始化: 30 | ```c 31 | set_new_dnode(&dn, inode, NULL, NULL, 0); // 0表示不清楚nid 32 | ``` 33 | 然后根据需要访问的page index,执行`f2fs_get_dnode_of_data`函数寻找: 34 | ```c 35 | err = f2fs_get_dnode_of_data(&dn, page->index, type); // type类型影响了寻址的行为 36 | blockt blkaddr = dn.data_blkaddr; // 获得对应位置的物理地址信息 37 | ``` 38 | 接下来分析,函数是如何寻址,由于函数比较长和复杂,先分析一个比较重要的函数`get_node_path`函数的作用,它的用法是: 39 | #### 概念 40 | 在分析之前,我们要明确几个概念。f2fs有三种node的类型,`f2fs_inode`,`direct_node`,和`indirect node`。其中`f2fs_inode`和`direct_node`都是直接保存数据的地址指针,因此一般统称为direct node,若有下横线,例如`direct_node`,则表示数据结构`struct direct_node`,如果没有下横线,则表示直接保存数据的地址指针的node,即`f2fs_inode`和`direct_node`。另外`indirect node`保存的是间接寻址的node的nid,因此一般直接称为为indirect node。 41 | #### 函数用法 42 | ```c 43 | int level; 44 | int offset[4]; 45 | unsigned int noffset[4]; 46 | level = get_node_path(inode, page->index, offset, noffset); 47 | ``` 48 | 这里offset和noffset分别表示block offset和node offset,返回的level表示寻址的深度,一共有4个深度,使用0~3表示: 49 | level=0: 表示可以直接在`f2fs_inode`找到物理地址 50 | level=1: 表示可以在`f2fs_inode->i_nid[0~1]`对应的`direct_node`能够找到物理地址 51 | level=2: 表示可以在`f2fs_inode->i_nid[2~3]`对应的`indirect_node`下的nid对应的`direct_node`能够找到物理地址 52 | level=3: 表示只能在`f2fs_inode->i_nid[4]`对应`indirect_node`的nid对应的`indirect_node`的nid对应的`direct_node`才能找到地址 53 | 54 | 由于offset和noffset,表示的是物理地址寻址信息,分别表示block偏移和direct node偏移来表示,它们是长度为4的数组,代表不同level 0~3 的寻址信息。之后的函数可以通过offset和noffset将数据块计算出来。 55 | #### 寻址原理 56 | 给定page->index,计算出level之后,offset[level]表示该page在所对应的direct node里面的block的偏移,noffset[level]表示当前的node是属于这个文件的第几个node(包括f2fs_node, direct_node, indirect_node),下面用几个例子展示一下 (注意下面计算的是不使用xattr的f2fs版本,如果使用了xattr结果会不同,但是表示的含义是一样的): 57 | 58 | **例子1: 物理地址位于f2fs_inode** 59 | 例如我们要寻找page->index = 665的数据块所在的位置,显然655是位于`f2fs_inode`内,因此level=0,因此我们只需要看offset[0]以及noffset[0]的信息,如下图。offset[0] = 665表示这个数据块在当前direct node(注意: f2fs_inode也是direct node的一种)的位置;noffset[0]表示当前direct node是属于这个文件的第几个node,由于f2fs_inode是第一个node,所以noffset[0] = 0。 60 | ```c 61 | level = 0 // 可以直接在f2fs_inode找到物理地址 62 | offset[0] = 665 // 由于level=0,因此我们只需要看offset[level]=offset[0]的信息,这里offset[0] = 665表示地址位于f2fs_inode->i_addr[665] 63 | noffset[0] = 0 // 对于level=0的情况,即看noffset[0],因为level=0表示数据在唯一一个的f2fs_inode中,因此这里表示inode。 64 | ``` 65 | 66 | **例子2: 物理地址位于direct_node** 67 | 例如我们要寻找page->index = 2113的数据块所在的位置,它位于第二个direct_node,所以level=1。我们只需要看offset[1]以及noffset[1]的信息,如下图。offset[1] = 172表示这个数据块在当前direct node的位置,即`direct_node->addr[172]`;noffset[1]表示当前direct node是属于这个文件的第几个node,由于它位于第二个direct_node,前面还有一个f2fs_inode以及一个direct node,所以这是第三个node,因此noffset[1] = 2。 68 | ```c 69 | level = 1 // 表示可以在f2fs_inode->i_nid[0~1]对应的direct_node能够找到物理地址 70 | offset[1] = 172 // 表示物理地址位于对应的node page的i_addr的第172个位置中,即direct_node->addr[172] 71 | noffset[1] = 2 // 数据保存在总共第三个node中 (1个f2fs_inode,2个direct_node) 72 | ``` 73 | 74 | **例子3: 物理地址位于indirect_node** 75 | 例如我们要寻找page->index = 4000的数据块所在的位置,它位于第1个indirect_node的第2个direct_node中,所以level=2。我们只需要看offset[2]以及noffset[2]的信息,如下图。offset[2] = 23表示这个数据块在当前direct node的位置;noffset[2]表示当前direct node是属于这个文件的第几个direct node,即这是第6个node。(1 * f2fs_inode + 2 * direct_node + 1 * indirect_node + 2 * direct node)。 76 | 77 | ```c 78 | offset[2] = 23 79 | noffset[2] = 5 80 | ``` 81 | 82 | **例子4: 物理地址位于indirect_node再indiret_node中 (double indirect node)** 83 | 例如我们要寻找page->index = 2075624的数据块所在的位置,它位于第一个double indirect_node的第一个indirect_node的第一个direct_node中,所以level=3。同理我们只需要看offset[3]以及noffset[3]的信息,如下,可以自己计算一下: 84 | ```c 85 | offset[3] = 17 86 | noffset[3] = 2043 87 | ``` 88 | 89 | 从上面可以知道`get_node_path`函数以后,执行可以根据offset和noffset直接知道page->index对应的物理地址,位于第几个node page的第几个offset对应的物理地址中。下面分析`f2fs_get_dnode_of_data`的原理: 90 | 91 | ```c 92 | int f2fs_get_dnode_of_data(struct dnode_of_data *dn, pgoff_t index, int mode) 93 | { 94 | struct f2fs_sb_info *sbi = F2FS_I_SB(dn->inode); 95 | struct page *npage[4]; 96 | struct page *parent = NULL; 97 | int offset[4]; 98 | unsigned int noffset[4]; 99 | nid_t nids[4]; 100 | int level, i = 0; 101 | int err = 0; 102 | 103 | // 通过计算得到offset, noffset,从而知道位于第几个node page的第几个offset对应的物理地址中 104 | level = get_node_path(dn->inode, index, offset, noffset); 105 | if (level < 0) 106 | return level; 107 | 108 | nids[0] = dn->inode->i_ino; 109 | npage[0] = dn->inode_page; 110 | 111 | if (!npage[0]) { 112 | npage[0] = f2fs_get_node_page(sbi, nids[0]); // 获取inode对应的f2fs_inode的node page 113 | } 114 | 115 | parent = npage[0]; 116 | if (level != 0) 117 | nids[1] = get_nid(parent, offset[0], true); // 获取f2fs_inode->i_nid 118 | 119 | dn->inode_page = npage[0]; 120 | dn->inode_page_locked = true; 121 | 122 | for (i = 1; i <= level; i++) { 123 | bool done = false; 124 | 125 | if (!nids[i] && mode == ALLOC_NODE) { 126 | // 创建模式,常用,写入文件时,需要node page再写入数据,因此对于较大文件,在这里创建node page 127 | if (!f2fs_alloc_nid(sbi, &(nids[i]))) { // 分配nid 128 | err = -ENOSPC; 129 | goto release_pages; 130 | } 131 | 132 | dn->nid = nids[i]; 133 | npage[i] = f2fs_new_node_page(dn, noffset[i]); // 分配node page 134 | // 如果i == 1,表示f2fs_inode->nid[0~1],即direct node,直接赋值到f2fs_inode->i_nid中 135 | // 如果i != 1,表示parent是indirect node类型的,要赋值到indirect_node->nid中 136 | set_nid(parent, offset[i - 1], nids[i], i == 1); 137 | f2fs_alloc_nid_done(sbi, nids[i]); 138 | done = true; 139 | } else if (mode == LOOKUP_NODE_RA && i == level && level > 1) { 140 | // 预读模式,少用,将node page全部预读出来 141 | npage[i] = f2fs_get_node_page_ra(parent, offset[i - 1]); 142 | done = true; 143 | } 144 | if (i == 1) { 145 | dn->inode_page_locked = false; 146 | unlock_page(parent); 147 | } else { 148 | f2fs_put_page(parent, 1); 149 | } 150 | 151 | if (!done) { 152 | npage[i] = f2fs_get_node_page(sbi, nids[i]); // 根据nid获取node page 153 | } 154 | if (i < level) { 155 | parent = npage[i]; // 注意这里parent被递归地赋值,目的是处理direct node和indrect node的赋值问题 156 | nids[i + 1] = get_nid(parent, offset[i], false); // 计算下一个nid 157 | } 158 | } 159 | // 全部完成后,将结果赋值到dn,然后退出函数 160 | dn->nid = nids[level]; 161 | dn->ofs_in_node = offset[level]; 162 | dn->node_page = npage[level]; 163 | dn->data_blkaddr = datablock_addr(dn->inode, dn->node_page, dn->ofs_in_node); // 这个就是根据page index所得到的物理地址 164 | return 0; 165 | } 166 | ``` 167 | -------------------------------------------------------------------------------- /ImportantDataStructure/node_footer.md: -------------------------------------------------------------------------------- 1 | ## Node Footer的作用 2 | 3 | `footer`是F2FS中,记录node的属性的一个数据,它源码如下 4 | 5 | ```c 6 | struct f2fs_node { 7 | union { 8 | struct f2fs_inode i; 9 | struct direct_node dn; 10 | struct indirect_node in; 11 | }; 12 | struct node_footer footer; 13 | } __packed; 14 | 15 | struct node_footer { 16 | __le32 nid; /* node id */ 17 | __le32 ino; /* inode nunmber */ 18 | __le32 flag; /* include cold/fsync/dentry marks and offset */ 19 | __le64 cp_ver; /* checkpoint version */ 20 | __le32 next_blkaddr; /* next node page block address */ 21 | } __packed; 22 | ``` 23 | 24 | F2FS有三种类型的node,分别是`f2fs_inode`、`direct_node`、`indirect_node`,每一种类型的node都有对应的footer。 25 | 26 | 27 | 28 | ### footer->nid和footer->ino 29 | 30 | 每一个node都有一个独特的`nid`,它被记录在`footer`中,如果是`direct_node`或者`indirect_node`,它们都有一个对应的`f2fs_inode`,因此为了记录从属关系,还需要`footer`记录它所属于的`f2fs_inode`的`nid`,即`ino`。因此,如果`footer->nid == footer->ino`,那么这个node就是inode,反正这个`node`是`direct_node`或者`indirect_node`。 31 | 32 | 33 | 34 | ### footer->flag 35 | 36 | `footer->flag`的作用是标记当前的node的属性。目前F2FS给node定义了三种属性: 37 | 38 | ```c 39 | enum { 40 | COLD_BIT_SHIFT = 0, 41 | FSYNC_BIT_SHIFT, 42 | DENT_BIT_SHIFT, 43 | OFFSET_BIT_SHIFT 44 | }; 45 | 46 | #define OFFSET_BIT_MASK (0x07) /* (0x01 << OFFSET_BIT_SHIFT) - 1 */ 47 | ``` 48 | 49 | 其中`footer->flag`的 50 | 51 | 第0位表示这个node是否是cold node。 52 | 53 | 第1位表示这个node是否执行了完整的fsync。F2FS为了`fsync`的效率做了一些改进,F2FS不会在`fsync`刷写所有脏的node page进去磁盘,只会刷写一些根据data直接相关的node page进入磁盘,例如`f2fs_inode`和`direct_node`。因此这个标志位是用来记录这个node是否执行了完整的fsync,以便系统在crash中恢复。 54 | 55 | 第3位表示这个node是是用来保存文件数据,还是目录数据的,也是用于数据恢复 56 | 57 | 58 | 59 | ### footer->cp_ver和footer->next_blkaddr 60 | 61 | `footer->cp_ver`分别用来记录当前的checkpoint的version,恢复的时候比较version版本确定如何进行数据恢复。 62 | 63 | `footer->next_blkaddr`则是用来记录这个node对应下一个node page的地址,也是用来恢复数据 64 | 65 | -------------------------------------------------------------------------------- /ImportantDataStructure/segment.md: -------------------------------------------------------------------------------- 1 | ## seg_entry 和 sit_info 2 | 3 | ### seg_entry结构 4 | #### 介绍 5 | 因为每一个segment需要管理512个Block的地址,因此需要通过某种方式去标记一个segment下的block,哪些是已经使用的,哪些block是处于无效状态等待回收。在F2FS中,通过结构体 `seg_entry` 去管理一个segment下的所有block的使用信息: 6 | 7 | ```c 8 | struct seg_entry { 9 | unsigned int type:6; /* 这个segment的类型 */ 10 | unsigned int valid_blocks:10; /* 已经使用的块的数目 */ 11 | unsigned int ckpt_valid_blocks:10; /* 上一次执行CP时,使用的块的数目 */ 12 | unsigned int padding:6; /* padding */ 13 | unsigned char *cur_valid_map; /* 通过bitmap(512位)表示这个segment哪些被使用,哪些没使用 */ 14 | #ifdef CONFIG_F2FS_CHECK_FS 15 | unsigned char *cur_valid_map_mir; /* mirror of current valid bitmap */ 16 | #endif 17 | /* 18 | * # of valid blocks and the validity bitmap stored in the the last 19 | * checkpoint pack. This information is used by the SSR mode. 20 | */ 21 | unsigned char *ckpt_valid_map; /* 上次CP时的bitmap状态 */ 22 | unsigned char *discard_map; /* 标记哪些block需要discard的bitmap */ 23 | unsigned long long mtime; /* 修改时间 */ 24 | }; 25 | ``` 26 | 27 | `seg_entry` 由于跟磁盘空间大小有关,因此初始化时以动态分配的方式,保存在元数据区域的**SIT**区域当中,代码的具体实现为`sbi->sit_info->sentries` 中。 28 | 29 | #### 应用场景 30 | **写流程:** 当文件的修改某一个block的数据时,需要经过的流程是: 31 | 1) 分配一个新的block; 32 | 2) 将数据写入到新分配的block中; 33 | 3) 将旧block置为无效,等待回收; 34 | 4) 将新block写入到磁盘中。 35 | 36 | 这一个流程需要更新的segment的管理信息,因为新block和旧block可能来自不同的segment,因此需要更新segment的统计信息,具体流程是: 根据新block的地址,找到对应segment number和seg_entry,然后在 `seg_entry `的根据新block在segment的bitmap对应位置设为1,然后给 `seg_entry->valid_blocks` 加一,表示这个segment新增加了一个被使用block;对于旧block,一样是根据block地址找到segment number和seg_entry,然后执行相反操作对bitmap设为0,然后 `seg_entry->valid_blocks` 减一。 37 | 38 | ### curseg_info结构 39 | #### 介绍 40 | `curseg_info` 在F2FS中表示的是当前使用的segment的信息。一般情况下,F2FS同时运行着6个 `curseg_info` ,分别表示**(NODE,DATA) X (HOT,WARM,COLD)**这些不同类型的segment。 它的基本结构和关联数据结构是: 41 | ```c 42 | struct curseg_info { 43 | struct mutex curseg_mutex; /* lock for consistency */ 44 | struct f2fs_summary_block *sum_blk; /* cached summary block */ 45 | struct rw_semaphore journal_rwsem; /* protect journal area */ 46 | struct f2fs_journal *journal; /* cached journal info */ 47 | unsigned char alloc_type; /* current allocation type */ 48 | unsigned int segno; /* current segment number */ 49 | unsigned short next_blkoff; /* next block offset to write */ 50 | unsigned int zone; /* current zone number */ 51 | unsigned int next_segno; /* preallocated segment */ 52 | }; 53 | ``` 54 | 它的主要成员的含义是: 55 | **f2fs_summary_block:** `curseg_info` 表示一个segment,因此通过 `f2fs_summary_block` 管理这个segment下的所有block。 `f2fs_summary_block` 包含512个 `f2fs_summary`,每个summary代表一个这个segment里面的一个block,它的结构是: 56 | ```c 57 | struct f2fs_summary_block { 58 | struct f2fs_summary entries[ENTRIES_IN_SUM]; /* ENTRIES_IN_SUM = 512 表示被管理的512个块 */ 59 | struct f2fs_journal journal; 60 | struct summary_footer footer; /* 指示这个segment的类型 */ 61 | } __packed; 62 | 63 | struct f2fs_summary { 64 | __le32 nid; /* 属主node id */ 65 | union { 66 | __u8 reserved[3]; 67 | struct { 68 | __u8 version; /* node version number */ 69 | __le16 ofs_in_node; /* 属主node里面的第几个block */ 70 | } __packed; 71 | }; 72 | } __packed; 73 | 74 | ``` 75 | 可以看到每一个 `f2fs_summary` 用来描述这个segment里面的block是属于哪一个node,而且是这个node里面的第几个block。 76 | 77 | **f2fs_journal:** `curseg_info` 管理着512个block,需要一种机制去记录每一个它所管理的block是否已经被分配出去。 因此 `f2fs_journal` 的作用就是记录每一个block是否是有效。它的结构如下: 78 | ```c 79 | struct f2fs_journal { 80 | union { 81 | __le16 n_nats; 82 | __le16 n_sits; /* 这个journal里面包含多少个sit_journal对象 */ 83 | }; 84 | /* spare area is used by NAT or SIT journals or extra info */ 85 | union { 86 | struct nat_journal nat_j; 87 | struct sit_journal sit_j; 88 | struct f2fs_extra_info info; 89 | }; 90 | } __packed; 91 | 92 | struct sit_journal { 93 | struct sit_journal_entry entries[SIT_JOURNAL_ENTRIES]; 94 | __u8 reserved[SIT_JOURNAL_RESERVED]; 95 | } __packed; 96 | 97 | struct sit_journal_entry { 98 | __le32 segno; 99 | struct f2fs_sit_entry se; 100 | } __packed; 101 | 102 | struct f2fs_sit_entry { 103 | __le16 vblocks; /* reference above */ 104 | __u8 valid_map[SIT_VBLOCK_MAP_SIZE]; /* SIT_VBLOCK_MAP_SIZE = 64,64 * 8 = 512 可以表示每一个块的valid状态 */ 105 | __le64 mtime; /* segment age for cleaning */ 106 | } __packed; 107 | ``` 108 | `f2fs_journal` 可以记录NAT和SIT的journal,这一节只关注SIT的作用。 通过 `f2fs_sit_entry` 可以发现,`f2fs_journal` 保存的是有效block的数目 `vblocks` 以及 它的bitmap `valid_map`。 109 | 110 | #### curseg_info的作用 111 | `curseg_info` 的作用主要是当一个Node或者Data需要分配一个新的block的时候,就会根据这个block的类型,在 `curseg_info` 取出一个segment,然后在这个segment分配出一个新的block,然后将新的block的映射信息,写入 `curseg_info` 的 `f2fs_summary_block` 和 `f2fs_journal` 中。这样设计的原因是,将大部分更新元数据的操作都放在 `curseg_info` 完成,避免了频繁读写磁盘。 112 | -------------------------------------------------------------------------------- /Outline.md: -------------------------------------------------------------------------------- 1 | # F2FS笔记 2 | 3 | ### 实验环境的搭建 4 | [实验环境的搭建](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Experiment/实验环境搭建.md) 5 | 6 | ### 一、文件系统布局以及结构 7 | 1. [总体结构](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/%E6%80%BB%E4%BD%93%E7%BB%93%E6%9E%84.md) 8 | 2. [Superblock结构](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/Superblock%E7%BB%93%E6%9E%84.md) 9 | 3. [Checkpoint结构](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/Checkpoint%E7%BB%93%E6%9E%84.md) 10 | 4. [Segment Infomation Table结构(SIT)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/Segment%20Infomation%20Table%E7%BB%93%E6%9E%84.md) 11 | 5. [Node Address Table结构(NAT)(未完成)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/Node%20Address%20Table%E7%BB%93%E6%9E%84.md) 12 | 6. [Segment Summary Area结构(SSA)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/Segment%20Summary%20Area%E7%BB%93%E6%9E%84.md) 13 | 14 | ### 二、文件数据的存储以及读写 15 | 1. [F2FS文件数据组织方式](https://github.com/RiweiPan/F2FS-NOTES/blob/master/Reading-and-Writing/file_data_structure.md) 16 | 2. [一般文件读流程](https://github.com/RiweiPan/F2FS-NOTES/blob/master/Reading-and-Writing/%E8%AF%BB%E6%B5%81%E7%A8%8B.md) 17 | 3. [一般文件写流程](https://github.com/RiweiPan/F2FS-NOTES/blob/master/Reading-and-Writing/%E5%86%99%E6%B5%81%E7%A8%8B.md) 18 | 19 | ### 三、文件与目录的创建以及删除 20 | 1. [一般文件的创建(未完成)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/File-Creation-and-Deletion/%E6%96%87%E4%BB%B6%E5%88%9B%E5%BB%BA.md) 21 | 2. [一般目录的创建(未完成)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/File-Creation-and-Deletion/%E7%9B%AE%E5%BD%95%E5%88%9B%E5%BB%BA.md) 22 | 3. [一般文件的删除(未完成)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/File-Creation-and-Deletion/%E7%9B%AE%E5%BD%95%E5%88%9B%E5%BB%BA.md) 23 | 4. [一般目录的删除(未完成)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/File-Creation-and-Deletion/%E7%9B%AE%E5%BD%95%E5%88%A0%E9%99%A4.md) 24 | 25 | ### 四、垃圾回收流程 26 | 1. [垃圾回收流程分析](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-GC/GC%E6%B5%81%E7%A8%8B%E4%BB%8B%E7%BB%8D.md) 27 | 2. [如何选择victim segment](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-GC/%E9%80%89%E6%8B%A9victim%20segment.md) 28 | 29 | ### 五、数据恢复流程 30 | 1. [数据恢复的原理以及方式](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Data-Recovery/%E6%95%B0%E6%8D%AE%E6%81%A2%E5%A4%8D%E7%9A%84%E5%8E%9F%E7%90%86%E4%BB%A5%E5%8F%8A%E6%96%B9%E5%BC%8F.md) 31 | 2. [后滚恢复和Checkpoint的作用与实现](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Data-Recovery/Checkpoint%E6%B5%81%E7%A8%8B.md) 32 | 3. [前滚恢复和Recovery的作用与实现(未完成)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Data-Recovery/Recovery%E7%9A%84%E6%B5%81%E7%A8%8B.md) 33 | 34 | ### 六、重要数据结构或者函数的分析 35 | 1. [f2fs_summary和f2fs_summary_block的介绍和应用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/f2fs_summary.md) 36 | 2. [seg_entry和sit_info的作用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/segment.md) 37 | 3. [f2fs_journal的作用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/f2fs_journal.md) 38 | 4. [f2fs_fill_super的分析](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/f2fs_fill_super.md) 39 | 5. [f2fs_map_block的作用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/f2fs_map_blocks.md) 40 | 6. [CURSEG的作用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/curseg.md) 41 | 7. [物理地址寻址的实现](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/get_dnode.md) 42 | 8. [node_footer的作用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/node_footer.md) 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # F2FS笔记 2 | 3 | ### 实验环境的搭建 4 | [实验环境的搭建](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Experiment/实验环境搭建.md) 5 | 6 | ### 一、文件系统布局以及元数据结构 7 | 1. [总体结构](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/%E6%80%BB%E4%BD%93%E7%BB%93%E6%9E%84.md) 8 | 2. [Superblock区域](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/Superblock%E7%BB%93%E6%9E%84.md) 9 | 3. [Checkpoint区域](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/Checkpoint%E7%BB%93%E6%9E%84.md) 10 | 4. [Segment Infomation Table区域(SIT)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/Segment%20Infomation%20Table%E7%BB%93%E6%9E%84.md) 11 | 5. [Node Address Table区域(NAT)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/Node%20Address%20Table%E7%BB%93%E6%9E%84.md) 12 | 6. [Segment Summary Area区域(SSA)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/Segment%20Summary%20Area%E7%BB%93%E6%9E%84.md) 13 | 14 | ### 二、文件数据的存储以及读写 15 | 1. [F2FS文件数据组织方式](https://github.com/RiweiPan/F2FS-NOTES/blob/master/Reading-and-Writing/file_data_structure.md) 16 | 2. [一般文件读流程](https://github.com/RiweiPan/F2FS-NOTES/blob/master/Reading-and-Writing/%E8%AF%BB%E6%B5%81%E7%A8%8B.md) 17 | 3. [一般文件写流程](https://github.com/RiweiPan/F2FS-NOTES/blob/master/Reading-and-Writing/%E5%86%99%E6%B5%81%E7%A8%8B.md) 18 | 19 | ### 三、文件与目录的创建以及删除 20 | 1. [一般文件的创建](https://github.com/RiweiPan/F2FS-NOTES/blob/master/File-Creation-and-Deletion/%E6%96%87%E4%BB%B6%E5%88%9B%E5%BB%BA.md) 21 | 2. [一般文件的删除(未完成)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/File-Creation-and-Deletion/%E7%9B%AE%E5%BD%95%E5%88%9B%E5%BB%BA.md) 22 | 23 | ### 四、垃圾回收流程 24 | 1. [垃圾回收流程分析](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-GC/GC%E6%B5%81%E7%A8%8B%E4%BB%8B%E7%BB%8D.md) 25 | 2. [如何选择victim segment](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-GC/%E9%80%89%E6%8B%A9victim%20segment.md) 26 | 27 | ### 五、数据恢复流程 28 | 1. [数据恢复的原理以及方式](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Data-Recovery/%E6%95%B0%E6%8D%AE%E6%81%A2%E5%A4%8D%E7%9A%84%E5%8E%9F%E7%90%86%E4%BB%A5%E5%8F%8A%E6%96%B9%E5%BC%8F.md) 29 | 2. [后滚恢复和Checkpoint的作用与实现](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Data-Recovery/Checkpoint%E6%B5%81%E7%A8%8B.md) 30 | 3. [前滚恢复和Recovery的作用与实现(未完成)](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Data-Recovery/Recovery%E7%9A%84%E6%B5%81%E7%A8%8B.md) 31 | 32 | ### 六、重要数据结构或者函数的分析 33 | 1. [f2fs_summary和f2fs_summary_block的介绍和应用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/f2fs_summary.md) 34 | 2. [seg_entry和sit_info的作用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/segment.md) 35 | 3. [f2fs_journal的作用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/f2fs_journal.md) 36 | 4. [f2fs_fill_super的分析](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/f2fs_fill_super.md) 37 | 5. [f2fs_map_block的作用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/f2fs_map_blocks.md) 38 | 6. [CURSEG的作用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/curseg.md) 39 | 7. [物理地址寻址的实现](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/get_dnode.md) 40 | 8. [node_footer的作用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/node_footer.md) 41 | 9. [f2fs_rename的作用](https://github.com/RiweiPan/F2FS-NOTES/blob/master/ImportantDataStructure/f2fs_rename.md) 42 | -------------------------------------------------------------------------------- /Reading-and-Writing/file_data_structure.md: -------------------------------------------------------------------------------- 1 | # 文件数据的保存以及物理地址的映射 2 | 文件数据的组织方式一般时被设计为inode-data模式,即 每一个文件都具有一个inode,这个inode记录data的组织关系,这个关系称为**文件结构**。例如用户需要访问A文件的第1000个字节,系统就会先根据A文件的路径找到的A的inode,然后从inode找到第1000个字节所在的物理地址,然后从磁盘读取出来。那么F2FS的文件结构是怎么样的呢? 3 | 4 | ![](../img/F2FS-Layout/inode-blockmap.png) 5 | 6 | 如上图,F2FS中的一个inode,包含两个主要部分: metadata部分,和数据块寻址部分。我们重点观察数据块寻址部分,分析inode时如何将数据块索引出来。在图中,数据块寻址部分包含direct pointers,single-indirect,double-indirect,以及triple-indirect。它们的含义分别是: 7 | 8 | **direct pointer:** inode内直接指向数据块(图右上角Data)的地址数组,即**inode->data模式**。 9 | 10 | **single-indirect pointer:** inode记录了两个single-indirect pointer(图右上角Direct node),每一个single-indirect pointer存储了多个数据块的地址,即**inode->direct_node->data模式**。 11 | 12 | **double-indirect:** inode记录了两个double-indirect pointer(图右上角indirect node),每一个double-indirect pointer记录了许多single-indirect pointer,每一个single-indirect pointer指向了数据块,即**inode->indirect_node->direct_node->data模式**。 13 | 14 | **triple-indirect:** inode记录了一个triple-indirect pointer(图右上角indirect node),每一个triple-indirect pointer记录了许多double-indirect pointer,每一个double-indirect pointer记录了许多single-indirect pointer,最后每一个single-indirect pointer指向了数据块。即**inode->indirect_node->indirect_node->direct_node->data模式**。 15 | 16 | 因此,我们可以发现,F2FS的inode结构采取indirect_node,首先在inode内部寻找物理地址,如果找不到再去direct_node找,层层深入。 17 | 18 | ## f2fs_node的结构以及作用 19 | 根据上面的分析,我们可以发现一个对于一个较大的文件,它可能包含inode以外的node,去保存一些间接寻址的信息。single-indirect pointer记录的是数据块的地址,而double-indirect pointer记录的是single-indirect pointer的地址,triple-indirect pointer记录的double-indirect pointer地址。在F2FS中, 20 | 21 | inode对应的是`f2fs_inode`结构,包含了多个direct pointer指向数据块物理地址; 22 | 23 | single-indirect pointer对应的是`direct_node`结构,包含了多个direct pointer指向物理地址; 24 | 25 | double-indirect pointer对应的是`indirect_node`结构,包含了多个指向`direct_node`的地址; 26 | 27 | triple-indirect pointer对应的也是`indirect_node`结构,包含了多个指向`indirect_node`的地址 28 | 29 | 接下来我们逐个分析F2FS每一个node的具体数据结构。 30 | 31 | 32 | 33 | ### 基本node结构 34 | 35 | 为了方便F2FS的对node的区分和管理,`f2fs_inode`和`direct_node`以及`indirect_node`都使用了同一个数据结构`f2fs_node`进行描述,并通过union的方式,将`f2fs_node`初始化成不同的node形式,它的结构如下: 36 | 37 | ```c 38 | struct f2fs_node { 39 | union { 40 | struct f2fs_inode i; 41 | struct direct_node dn; 42 | struct indirect_node in; 43 | }; 44 | struct node_footer footer; // footer用于记录node的类型 45 | } __packed; 46 | 47 | struct node_footer { 48 | __le32 nid; /* node id */ 49 | __le32 ino; /* inode nunmber */ 50 | __le32 flag; /* include cold/fsync/dentry marks and offset */ 51 | __le64 cp_ver; /* checkpoint version */ 52 | __le32 next_blkaddr; /* next node page block address */ 53 | } __packed; 54 | ``` 55 | 其中起到区分是哪一种node的关键数据结构是`node_footer`。如果`node_footer`的`nid`和`ino`相等,则表示这是一个`f2fs_inode`结构,如果不相等,则表示这是一个`direct_node`或者`indirect_node`。 56 | 57 | 58 | 59 | ### f2fs_inode结构 60 | 61 | 我们先看`f2fs_inode`的结构,省略其他元数据的信息,重点关注文件如何索引的,结构如下: 62 | 63 | ```c 64 | struct f2fs_inode { 65 | ... 66 | __le32 i_addr[DEF_ADDRS_PER_INODE]; // DEF_ADDRS_PER_INODE=923 67 | __le32 i_nid[DEF_NIDS_PER_INODE]; // DEF_NIDS_PER_INODE=5 68 | ... 69 | } __packed; 70 | ``` 71 | 72 | `i_addr`数组就是前面提及的direct pointer,数组的下标是文件的逻辑位置,数组的值就是flash设备的物理地址。例如文件的第一个页就对应`i_addr[0]`,第二个页就对应`i_addr[1]`,而`i_addr[0]`和`i_addr[1]`所记录的物理地址,就是文件第一个页(page)和第二个页的数据的物理地址,系统可以将两个物理地址提交到flash设备,将数据读取出来。 73 | 74 | 我们可以发现`i_addr`的数组长度只有923,即一个`f2fs_inode`只能直接索引到923个页/块的地址(约3.6MB),对于大于3.6MB的文件,就需要使用**间接寻址**。`f2fs_inode`的`i_nid`数组就是为了间接寻址而设计,`i_nid`数组是一个长度为5的数组,可以记录5个node的地址。其中 75 | 76 | `i_nid[0]`和`i_nid[1]`记录的是`direct_node`的地址,即对应前述的single-indirect pointer。 77 | 78 | `i_nid[2]`和`i_nid[3]`记录的是`indirect_node`的地址,这两个`indirect_node`记录的是`direct_node`的地址,即对应前述的double-indirect pointer。 79 | 80 | `i_nid[4]`记录的是`indirect_node`的地址,但是这个`indirect_node`记录的是`indirect_node`的地址,即前述的triple-indirect pointer。 81 | 82 | 83 | 84 | ### direct_node和indirect_node结构 85 | 86 | `direct_inode`以及`indirect_inode`的结构如下所示,`direct_node`记录的是数据块的地址,`indirect_inode`记录的是node的id,系统可以通过nid找到对应的node的地址。 87 | 88 | ```c 89 | struct direct_node { 90 | __le32 addr[ADDRS_PER_BLOCK]; // ADDRS_PER_BLOCK=1018 91 | } __packed; 92 | 93 | struct indirect_node { 94 | __le32 nid[NIDS_PER_BLOCK]; // NIDS_PER_BLOCK=1018 95 | } __packed; 96 | ``` 97 | 98 | 99 | 100 | ### Wandering Tree问题 101 | 102 | 在第一章的第一节提到,F2FS的设计是为了解决wandering tree的问题,那么现在的设计是如何解决这个问题的呢。假设一个文件发生更改,修改了`direct_node`里面的某一个block的数据,根据LFS的异地更新特性,我们需要给更改后的数据一个新的block。传统的LFS需要将这个新的block的地址一层层网上传递,直到inode结构。而F2FS的设计是只需要将`direct_node`对应位置的`addr`的值更新为新block的地址,从而没必要往上传递,因此解决了wandering tree的问题。 103 | 104 | 105 | 106 | ## 普通文件数据的保存 107 | 108 | 从上节描述可以知道,一个文件由一个`f2fs_inode`和多个`direct_inode`或者`indirect_inode`所组成。当系统创建一个文件的时候,它会首先创建一个`f2fs_inode`写入到flash设备,然后用户往该文件写入第一个page的时候,会将数据写入到main area的一个block中,然后将该block的物理地址赋值到`f2fs_inode->i_addr[0]`中,这样就完成了Node-Data的管理关系。随着对同一文件写入的数据的增多,会逐渐使用到其他类型的node去保存文件的数据。 109 | 110 | 经过上面的分析,我们可以计算F2FS单个文件的最大尺寸: 111 | 1. `f2fs_inode` 直接保存了923个block的数据的物理地址 112 | 2. `f2fs_inode->i_nid[0~1]` 保存了两个 `direct_node` 的地址,这里可以保存 2 x 1018个block的数据 113 | 3. `f2fs_inode->i_nid[2~3]` 保存了两个`indirect_node` 的地址,这两个其中2个`indirect_node`保存的是 `direct_node` 的nid,因此可以保存 2 x 1018 x 1018个block的数据; 114 | 4. `f2fs_inode->i_nid[4]` 保存了一个`indirect_node` 的地址,这个`indirect_node`保存的是 `indirect_node` 的nid,因此可以保存 1018 x 1018 x 1018个页的数据 115 | 116 | 可以得到如下计算公式: 117 | **4KB x (923 + 2 x 1018 + 2 x 1018 x 1018 + 1 x 1018 x 1018 x 1018) = 3.93TB** 118 | 因此F2FS单个文件最多了保存3.93TB数据。 119 | 120 | 121 | 122 | ## 内联文件数据的保存 123 | 124 | 从上节可以知道,文件的实际数据是保存在`f2fs_inode->i_addr`对应的物理块当中,因此即使一个很小的文件,如1个字节的小文件,也需要一个node和data block才能实现正常的保存和读写,也就是需要8KB的磁盘空间去保存一个尺寸为1字节的小文件。而且`f2fs_inode->i_addr[923]`里面除了`f2fs_inode->i_addr[0]`保存了一个物理地址,其余的922个i_addr都被闲置,造成了空间的浪费。 125 | 126 | 因此F2FS为了减少空间的使用量,使用内联(inline)文件减少这些空间的浪费。它的核心思想是当文件足够小的时候,使用`f2fs_inode->i_addr`数组直接保存数据本身,而不单独写入一个block中,再进行寻址。因此,如上面的例子,只有1个字节大小的文件,只需要一个`f2fs_inode`结构,即4KB,就可以同时将node信息和data信息同时保存,减少了一半的空间使用量。 127 | 128 | 根据上述定义,可以计算得到多大的文件可以使用内联的方式进行保存,`f2fs_inode`有尺寸为923的用于保存数据物理地址的数组i_addr,它的数据类型是__le32,即4个字节。保留一个数组成员另做它用,因此内联文件最大尺寸为: 922 * 4 = 3688字节。 129 | 130 | 131 | 132 | ## 文件读写与物理地址的映射的例子 133 | 134 | Linux的文件是通过page进行组织起来的,默认page的size是4KB,使用index作为编号。 135 | 136 | 137 | 138 | **一个小文件访问例子** 139 | 140 | 例如一个size=10KB的文件,需要3个page去保存数据,这3个page的编号是0,1,2。当用户访问这个文件的第2~6kb的数据的时候,系统就会计算出数据保存在page index = 0和1的page中,然后根据文件的路径找到对应的`f2fs_inode`结构,page index = 0和1即对应`f2fs_inode`的`i_addr[0]`和`i_addr[1]`。系统进而从这两个`i_addr`读取物理地址,提交到flash设备将数据读取出来。 141 | 142 | 143 | 144 | **一个大文件访问例子** 145 | 146 | 假设用户需要读取文件第4000个页(page index = 3999)的数据, 147 | 第一步: 那么首先系统会根据文件路径找到对应的f2fs_inode结构 148 | 第二步: 由于4000 >(923 + 1018 + 1018),`f2fs_inode->i_addr`和`f2fs_inode->nid[0]和nid[1]`都无法满足需求,因此系统根据`f2fs_inode->nid[2]`找到对应的 `indirect_node`的地址 149 | 第三步: `indirect_node`保存的是`direct_node`的nid数组,由于 4000 - 923 - 1018 - 1018 = 1041,而一个`direct_node`只能保存1018个block,因此可以知道数据位于`indirect_node->nid[1]`对应的`direct_node`中 150 | 第四步: 计算剩下的的偏移(4000-923-1018-1018-1018=23)找到数据的物理地址位于该`direct_node`的`direct_node->addr[23]`中。 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /Reading-and-Writing/写流程.md: -------------------------------------------------------------------------------- 1 | # F2FS的写流程 2 | 3 | ## 写流程介绍 4 | F2FS的写流程主要包含了以下几个子流程: 5 | 1. 调用vfs_write函数 6 | 2. 调用f2fs_file_write_iter函数: 初始化f2fs_node的信息 7 | 3. 调用f2fs_write_begin函数: 创建page cache,并填充数据 8 | 4. 写入到page cache: 等待系统触发writeback回写到磁盘 9 | 5. 调用f2fs_write_end函数: 将page设置为最新状态 10 | 6. 调用f2fs_write_data_pages函数: 系统writeback或者fsync触发的时候执行这个函数写入到磁盘 11 | 12 | 第一步的vfs_write函数是VFS层面的流程,下面仅针对涉及F2FS的写流程,且经过简化的主要流程进行分析。 13 | 14 | ## f2fs_file_write_iter函数 15 | 这个函数的主要作用是在数据写入文件之前进行预处理,核心流程就是将该文件对应`f2fs_inode`或者`direct_node`对应写入位置的`i_addr`或者`addr`的值进行初始化。例如用户需要在第4个page的位置写入数据,那么`f2fs_file_write_iter`函数会首先找到该文件对应的`f2fs_inode`,然后找到第4个page对应的数据块地址记录,即`f2fs_inode->i_addr[3]`。如果该位置的值是`NULL_ADDR`则表示当前是**添加写(Append Write)**,因此将值初始化为`NEW_ADDR`;如果是该位置的值是一个具体的block号,那么表示为**覆盖写(Overwrite)**,不需要做处理。 16 | ```c 17 | static ssize_t f2fs_file_write_iter(struct kiocb *iocb, struct iov_iter *from) 18 | { 19 | struct file *file = iocb->ki_filp; 20 | struct inode *inode = file_inode(file); 21 | ssize_t ret; 22 | 23 | ... 24 | err = f2fs_preallocate_blocks(iocb, from); // 进行预处理 25 | ... 26 | ret = __generic_file_write_iter(iocb, from); // 预处理完成后继续执行下一步写流程 27 | ... 28 | 29 | return ret; 30 | } 31 | ``` 32 | 下面继续分析`f2fs_preallocate_blocks`: 33 | ```c 34 | int f2fs_preallocate_blocks(struct kiocb *iocb, struct iov_iter *from) 35 | { 36 | struct inode *inode = file_inode(iocb->ki_filp); // 获取inode 37 | struct f2fs_map_blocks map; 38 | 39 | map.m_lblk = F2FS_BLK_ALIGN(iocb->ki_pos); // 根据文件指针偏移计算需要从第几个block开始写入 40 | map.m_len = F2FS_BYTES_TO_BLK(iocb->ki_pos + iov_iter_count(from)); // 计算要写入block的个数 41 | 42 | // 初始化一些信息 43 | map.m_next_pgofs = NULL; 44 | map.m_next_extent = NULL; 45 | map.m_seg_type = NO_CHECK_TYPE; 46 | 47 | flag = F2FS_GET_BLOCK_PRE_AIO; 48 | 49 | map_blocks: 50 | err = f2fs_map_blocks(inode, &map, 1, flag); // 进行初始化 51 | return err; 52 | } 53 | ``` 54 | `f2fs_map_blocks`函数的作用非常广泛,主要作用是通过逻辑地址(文件偏移指针)找到对应的物理地址(block号)。因此在读写流程中都有作用。在写流程中,该函数的主要作用是初始化地址信息: 55 | ```c 56 | int f2fs_map_blocks(struct inode *inode, struct f2fs_map_blocks *map, 57 | int create, int flag) 58 | { 59 | unsigned int maxblocks = map->m_len; 60 | 61 | struct f2fs_sb_info *sbi = F2FS_I_SB(inode); 62 | int mode = create ? ALLOC_NODE : LOOKUP_NODE; 63 | 64 | map->m_len = 0; 65 | map->m_flags = 0; 66 | 67 | pgofs = (pgoff_t)map->m_lblk; // 获得文件访问偏移量 68 | end = pgofs + maxblocks; // 获得需要读取的block的长度 69 | 70 | next_dnode: 71 | 72 | set_new_dnode(&dn, inode, NULL, NULL, 0); // 初始化dnode,dnode的作用是根据逻辑地址找到物理地址 73 | 74 | // 根据inode找到对应的f2fs_inode或者direct_node结构,然后通过pgofs(文件页偏移)获得物理地址,记录在dn中 75 | err = f2fs_get_dnode_of_data(&dn, pgofs, mode); 76 | 77 | start_pgofs = pgofs; 78 | prealloc = 0; 79 | last_ofs_in_node = ofs_in_node = dn.ofs_in_node; 80 | end_offset = ADDRS_PER_PAGE(dn.node_page, inode); 81 | 82 | next_block: 83 | // 根据dn获得物理地址,ofs_in_node表示这个物理地址位于当前node的第几个数据块 84 | // 如 f2fs_inode->i_addr[3],那么dn.ofs_in_node=3 85 | blkaddr = datablock_addr(dn.inode, dn.node_page, dn.ofs_in_node); 86 | ... 87 | if (!is_valid_blkaddr(blkaddr)) { // is_valid_blkaddr函数用于判断是否存在旧数据 88 | // 如果不存在旧数据 89 | if (create) { 90 | if (flag == F2FS_GET_BLOCK_PRE_AIO) { 91 | if (blkaddr == NULL_ADDR) { 92 | prealloc++; // 记录有多少个添加写的block 93 | last_ofs_in_node = dn.ofs_in_node; 94 | } 95 | } 96 | map->m_flags |= F2FS_MAP_NEW; // F2FS_MAP_NEW表示正在处理一个从未使用的数据 97 | blkaddr = dn.data_blkaddr; // 记录当前的物理地址 98 | } 99 | } 100 | ... 101 | // 记录处理了多少个block 102 | dn.ofs_in_node++; 103 | pgofs++; 104 | ... 105 | // 这里表示已经处理到最后一个block了 106 | if (flag == F2FS_GET_BLOCK_PRE_AIO && 107 | (pgofs == end || dn.ofs_in_node == end_offset)) { 108 | 109 | dn.ofs_in_node = ofs_in_node; // 回到第一个block 110 | err = f2fs_reserve_new_blocks(&dn, prealloc); // 通过这个函数将其地址设置为NEW_ADDR 111 | map->m_len += dn.ofs_in_node - ofs_in_node; 112 | dn.ofs_in_node = end_offset; 113 | } 114 | ... 115 | if (pgofs >= end) 116 | goto sync_out; // 表示已经全部处理完,可以退出这个函数了 117 | else if (dn.ofs_in_node < end_offset) 118 | goto next_block; // 每执行上面的流程就处理一个block,如果没有处理所有用户写入的block,那么回去继续处理 119 | ... 120 | sync_out: 121 | ... 122 | out: 123 | return err; 124 | } 125 | ``` 126 | 然后分析`f2fs_reserve_new_blocks`: 127 | ```c 128 | int f2fs_reserve_new_blocks(struct dnode_of_data *dn, blkcnt_t count) 129 | { 130 | struct f2fs_sb_info *sbi = F2FS_I_SB(dn->inode); 131 | int err; 132 | 133 | ... 134 | for (; count > 0; dn->ofs_in_node++) { 135 | block_t blkaddr = datablock_addr(dn->inode, 136 | dn->node_page, dn->ofs_in_node); 137 | if (blkaddr == NULL_ADDR) { // 首先判断是不是NULL_ADDR,如果是则初始化为NEW_ADDR 138 | dn->data_blkaddr = NEW_ADDR; 139 | __set_data_blkaddr(dn); 140 | count--; 141 | } 142 | } 143 | ... 144 | 145 | return 0; 146 | } 147 | ``` 148 | ## f2fs_write_begin和f2fs_write_end函数 149 | VFS中`write_begin`和`write_end`函数分别是数据写入page cache前以及写入后的处理。写入page cache后,系统会维护一段时间,直到满足一定条件后(如fsync和writeback会写),VFS会调用writepages函数,将这些缓存在内存中的page一次性写入到磁盘中。`write_begin`和`write_end`函数的调用可以参考VFS的`generic_perform_write`函数, 150 | ```c 151 | ssize_t generic_perform_write(struct file *file, 152 | struct iov_iter *i, loff_t pos) 153 | { 154 | struct address_space *mapping = file->f_mapping; 155 | const struct address_space_operations *a_ops = mapping->a_ops; 156 | long status = 0; 157 | ssize_t written = 0; 158 | unsigned int flags = 0; 159 | 160 | do { 161 | struct page *page; 162 | unsigned long offset; 163 | unsigned long bytes; 164 | size_t copied; 165 | void *fsdata; 166 | 167 | offset = (pos & (PAGE_SIZE - 1)); // 计算文件偏移,按page计算 168 | bytes = min_t(unsigned long, PAGE_SIZE - offset, iov_iter_count(i)); // 计算需要写多少个字节 169 | again: 170 | status = a_ops->write_begin(file, mapping, pos, bytes, flags, &page, &fsdata); // 调用write_begin,对page进行初始化 171 | 172 | copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes); // 将处理后的数据拷贝到page当中 173 | flush_dcache_page(page); // 将包含用户数据的page加入到page cache中,等待系统触发writeback的时候回写 174 | 175 | status = a_ops->write_end(file, mapping, pos, bytes, copied, page, fsdata); // 调用write_end函数进行后续处理 176 | 177 | copied = status; 178 | 179 | iov_iter_advance(i, copied); 180 | 181 | pos += copied; 182 | written += copied; 183 | 184 | balance_dirty_pages_ratelimited(mapping); 185 | } while (iov_iter_count(i)); // 直到处理完所有的数据 186 | 187 | return written ? written : status; 188 | } 189 | ``` 190 | 然后分析VFS的`write_begin`和`write_end`对应的功能,`write_begin`在F2FS中对应的是`f2fs_write_begin`,它的作用是将根据用户需要写入的数据类型,对page进行初始化,如下所示: 191 | ```c 192 | static int f2fs_write_begin(struct file *file, struct address_space *mapping, 193 | loff_t pos, unsigned len, unsigned flags, 194 | struct page **pagep, void **fsdata) 195 | { 196 | struct inode *inode = mapping->host; 197 | struct f2fs_sb_info *sbi = F2FS_I_SB(inode); 198 | struct page *page = NULL; 199 | pgoff_t index = ((unsigned long long) pos) >> PAGE_SHIFT; 200 | bool need_balance = false, drop_atomic = false; 201 | block_t blkaddr = NULL_ADDR; 202 | int err = 0; 203 | 204 | repeat: 205 | page = f2fs_pagecache_get_page(mapping, index, 206 | FGP_LOCK | FGP_WRITE | FGP_CREAT, GFP_NOFS); // 第一步创建或者获取page cache 207 | 208 | *pagep = page; 209 | 210 | err = prepare_write_begin(sbi, page, pos, len, 211 | &blkaddr, &need_balance); // 第二步根据页偏移信息获取到对应的物理地址blkaddr 212 | 213 | // 第三步,根据写类型对新创建的page进行初始化处理 214 | if (blkaddr == NEW_ADDR) { //如果是添加写,则将该page直接使用0填充 215 | zero_user_segment(page, 0, PAGE_SIZE); 216 | SetPageUptodate(page); 217 | } else { //如果是覆盖写,则将该page直接使用0填充 218 | err = f2fs_submit_page_read(inode, page, blkaddr); // 从磁盘中将旧数据读取出来 219 | 220 | lock_page(page); 221 | if (unlikely(page->mapping != mapping)) { 222 | f2fs_put_page(page, 1); 223 | goto repeat; 224 | } 225 | if (unlikely(!PageUptodate(page))) { 226 | err = -EIO; 227 | goto fail; 228 | } 229 | } 230 | return 0; 231 | } 232 | ``` 233 | 通过`flush_dcache_page`函数将用户数据写入到page cache之后,进行`write_end`处理,在F2FS中它对应的是`f2fs_write_end`函数,它的作用是,如下所述: 234 | ```c 235 | static int f2fs_write_end(struct file *file, 236 | struct address_space *mapping, 237 | loff_t pos, unsigned len, unsigned copied, 238 | struct page *page, void *fsdata) 239 | { 240 | struct inode *inode = page->mapping->host; 241 | 242 | if (!PageUptodate(page)) { // 判断是否已经将page cache在写入是否到达了最新的状态 243 | if (unlikely(copied != len)) 244 | copied = 0; 245 | else 246 | SetPageUptodate(page); // 如果不是就处理后设置为最新 247 | } 248 | if (!copied) 249 | goto unlock_out; 250 | 251 | set_page_dirty(page); // 将page设置为dirty,就会加入到inode->mapping的radix tree中,等待系统回写 252 | 253 | if (pos + copied > i_size_read(inode)) 254 | f2fs_i_size_write(inode, pos + copied); // 更新文件尺寸 255 | unlock_out: 256 | f2fs_put_page(page, 1); 257 | f2fs_update_time(F2FS_I_SB(inode), REQ_TIME); // 更新文件修改日期 258 | return copied; 259 | } 260 | ``` 261 | 262 | ## f2fs_write_data_pages函数 263 | 如上一节所述,系统会将用户写入的数据先写入到page cache,然后等待时机回写到磁盘中。page cache的回写是通过` f2fs_write_data_pages`函数进行。系统会将page cache中dirty的pages加入到一个list当中,然后传入到` f2fs_write_data_pages进行处理。针对F2FS文件系统,它包含如下步骤: 264 | 1. f2fs_write_data_pages&__f2fs_write_data_pages函数: 做一些不那么重要的预处理 265 | 2. f2fs_write_cache_pages函数: 从inode->mapping的radix tree中取出page 266 | 3. __write_data_page函数: 判断文件类型(内联文件,目录文件,普通文件)进行不同的写入 267 | 4. f2fs_do_write_data_page: 根据F2FS的状态选择进行就地回写(在原物理地址更新)还是异地回写(在其他物理地址更新) 268 | 5. f2fs_outplace_write_data: 执行回写,更新f2fs_inode的状态 269 | 6. do_write_page: 从CURSEG分配物理地址,然后写入到磁盘 270 | 下面各自进行分析。 271 | 272 | ### f2fs_write_data_pages&__f2fs_write_data_pages函数 273 | 这两个函数只是包含了一些不太重要的预处理 274 | ```c 275 | static int f2fs_write_data_pages(struct address_space *mapping, 276 | struct writeback_control *wbc) 277 | { 278 | struct inode *inode = mapping->host; 279 | 280 | return __f2fs_write_data_pages(mapping, wbc, 281 | F2FS_I(inode)->cp_task == current ? 282 | FS_CP_DATA_IO : FS_DATA_IO); // 这个函数可以知道当前是普通的写入,还是Checkpoint数据的写入 283 | } 284 | 285 | static int __f2fs_write_data_pages(struct address_space *mapping, 286 | struct writeback_control *wbc, 287 | enum iostat_type io_type) 288 | { 289 | struct inode *inode = mapping->host; 290 | struct f2fs_sb_info *sbi = F2FS_I_SB(inode); 291 | struct blk_plug plug; 292 | int ret; 293 | 294 | 295 | blk_start_plug(&plug); 296 | 297 | ret = f2fs_write_cache_pages(mapping, wbc, io_type); // 取出需要回写的page,然后写入 298 | 299 | blk_finish_plug(&plug); 300 | 301 | f2fs_remove_dirty_inode(inode); // 写入后将inode从dirty标志清除,即不需要再回写 302 | return ret; 303 | skip_write: 304 | wbc->pages_skipped += get_dirty_pages(inode); 305 | trace_f2fs_writepages(mapping->host, wbc, DATA); 306 | return 0; 307 | } 308 | ``` 309 | 310 | ### f2fs_write_cache_pages函数 311 | 这个函数的主要作用是从inode对应的mapping(radix tree的root)中,取出所有需要回写的page,然后通过一个循环,逐个写入到磁盘。 312 | ```c 313 | static int f2fs_write_cache_pages(struct address_space *mapping, 314 | struct writeback_control *wbc, 315 | enum iostat_type io_type) 316 | { 317 | struct pagevec pvec; 318 | 319 | pagevec_init(&pvec); // 这是一个用于装载page的数组,数组大小是15个page 320 | 321 | if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages) 322 | tag = PAGECACHE_TAG_TOWRITE; // tag是mapping给每一个pae的标志,用于标志这些page的属性 323 | else 324 | tag = PAGECACHE_TAG_DIRTY; 325 | 326 | retry: 327 | if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages) 328 | tag_pages_for_writeback(mapping, index, end); // SYNC模式下,将所有的tag=PAGECACHE_TAG_DIRTY的page重新标志为PAGECACHE_TAG_TOWRITE,作用是SYNC模式下必须全部回写到磁盘 329 | done_index = index; 330 | 331 | while (!done && (index <= end)) { 332 | int i; 333 | 334 | // 从mapping中取出tag类型的15个page,装载到pvec中 335 | nr_pages = pagevec_lookup_range_tag(&pvec, mapping, &index, end, tag); 336 | 337 | // 循环将pvec中的page取出,回写到磁盘 338 | for (i = 0; i < nr_pages; i++) { 339 | struct page *page = pvec.pages[i]; 340 | bool submitted = false; 341 | 342 | ret = __write_data_page(page, &submitted, wbc, io_type); // 写入磁盘的核心函数 343 | 344 | if (--wbc->nr_to_write <= 0 && 345 | wbc->sync_mode == WB_SYNC_NONE) { 346 | done = 1; // 如果本次writeback的所有page写完就退出 347 | break; 348 | } 349 | } 350 | pagevec_release(&pvec); // 释放掉pvec 351 | cond_resched(); 352 | } 353 | 354 | if (wbc->range_cyclic || (range_whole && wbc->nr_to_write > 0)) 355 | mapping->writeback_index = done_index; 356 | 357 | if (last_idx != ULONG_MAX) 358 | // page通过一些函数后,会放入到bio中,然后提交到磁盘。 359 | // f2fs的机制是不会马上提交bio,需要等到bio包含了一定数目的page之后才会提交 360 | // 因此这个函数作用是,即使数目不够,但是仍要强制提交bio,需要与磁盘同步 361 | f2fs_submit_merged_write_cond(F2FS_M_SB(mapping), mapping->host, 362 | 0, last_idx, DATA); 363 | 364 | return ret; 365 | } 366 | ``` 367 | 368 | ### __write_data_page函数 369 | 这个函数的作用是判断文件类型(目录文件,内联文件,普通文件)进行不同的写入。F2FS针对普通文件,有两种保存方式,分别是内联方式(inline)和普通方式。内联方式在[数据的保存以及逻辑地址和物理地址的映射](https://github.com/RiweiPan/F2FS-NOTES/blob/master/Reading-and-Writing/file_data_structure.md) 这一节已做介绍。这里主要介绍普通文件的写流程,内联文件以后再更新。 370 | ```c 371 | static int __write_data_page(struct page *page, bool *submitted, 372 | struct writeback_control *wbc, 373 | enum iostat_type io_type) 374 | { 375 | struct inode *inode = page->mapping->host; 376 | struct f2fs_sb_info *sbi = F2FS_I_SB(inode); 377 | loff_t i_size = i_size_read(inode); 378 | const pgoff_t end_index = ((unsigned long long) i_size) >> PAGE_SHIFT; 379 | // 这个数据结构在整个写流程非常重要,记录了写入的信息 380 | // 关键变量是 fio->old_blkaddr 以及 fio->new_blkaddr记录旧地址和新地址 381 | struct f2fs_io_info fio = { 382 | .sbi = sbi, 383 | .ino = inode->i_ino, 384 | .type = DATA, 385 | .op = REQ_OP_WRITE, 386 | .op_flags = wbc_to_write_flags(wbc), 387 | .old_blkaddr = NULL_ADDR, 388 | .page = page, // 即将写入的page 389 | .encrypted_page = NULL, 390 | .submitted = false, 391 | .need_lock = LOCK_RETRY, 392 | .io_type = io_type, 393 | .io_wbc = wbc, 394 | }; 395 | 396 | if (page->index < end_index) 397 | goto write; 398 | 399 | write: 400 | if (S_ISDIR(inode->i_mode)) { // 如果是目录文件,直接写入不需要修改 401 | err = f2fs_do_write_data_page(&fio); 402 | goto done; 403 | } 404 | 405 | err = -EAGAIN; 406 | if (f2fs_has_inline_data(inode)) { // 内联文件使用内联的写入方式 407 | err = f2fs_write_inline_data(inode, page); 408 | if (!err) 409 | goto out; 410 | } 411 | 412 | if (err == -EAGAIN) { // 普通文件则使用普通的方式 413 | err = f2fs_do_write_data_page(&fio); 414 | } 415 | 416 | done: 417 | if (err && err != -ENOENT) 418 | goto redirty_out; 419 | 420 | out: 421 | inode_dec_dirty_pages(inode); // 每写入一个page,就清除了inode一个dirty pages,因此数目减去1 422 | if (err) 423 | ClearPageUptodate(page); 424 | 425 | unlock_page(page); 426 | 427 | if (submitted) 428 | *submitted = fio.submitted; 429 | 430 | return 0; 431 | 432 | redirty_out: 433 | redirty_page_for_writepage(wbc, page); 434 | if (!err || wbc->for_reclaim) 435 | return AOP_WRITEPAGE_ACTIVATE; 436 | unlock_page(page); 437 | return err; 438 | } 439 | ``` 440 | ### f2fs_do_write_data_page函数 441 | 这个函数的作用是根据系统的状态选择就地更新数据(inplace update)还是异地更新数据(outplace update)。一般情况下,系统只会在磁盘空间比较满的时候选择就地更新策略,避免触发过多的gc影响性能。因此,这里主要介绍异地更新的写流程: 442 | ```c 443 | int f2fs_do_write_data_page(struct f2fs_io_info *fio) // 前面提到fio是写流程最重要的数据结构 444 | { 445 | struct page *page = fio->page; 446 | struct inode *inode = page->mapping->host; 447 | struct dnode_of_data dn; 448 | struct extent_info ei = {0,0,0}; 449 | bool ipu_force = false; 450 | int err = 0; 451 | 452 | set_new_dnode(&dn, inode, NULL, NULL, 0); // 初始化dnode 453 | err = f2fs_get_dnode_of_data(&dn, page->index, LOOKUP_NODE); // 根据文件偏移page->index获取物理地址 454 | 455 | fio->old_blkaddr = dn.data_blkaddr; // 将旧的物理地址赋值给fio->old_blkaddr 456 | 457 | if (fio->old_blkaddr == NULL_ADDR) { // 前面提及到f2fs_file_write_iter已经将物理地址设置为NEW_ADDR或者具体的block号,因此这里表示在写入磁盘之前,用户又将这部分数据删除了,所以没必要写入了 458 | ClearPageUptodate(page); 459 | goto out_writepage; 460 | } 461 | got_it: 462 | if (ipu_force || (is_valid_blkaddr(fio->old_blkaddr) && 463 | need_inplace_update(fio))) { // 判断是否需要就地更新 464 | err = encrypt_one_page(fio); 465 | if (err) 466 | goto out_writepage; 467 | 468 | set_page_writeback(page); 469 | ClearPageError(page); 470 | f2fs_put_dnode(&dn); 471 | if (fio->need_lock == LOCK_REQ) 472 | f2fs_unlock_op(fio->sbi); 473 | err = f2fs_inplace_write_data(fio); // 使用就地更新的方式写入 474 | trace_f2fs_do_write_data_page(fio->page, IPU); 475 | set_inode_flag(inode, FI_UPDATE_WRITE); 476 | return err; 477 | } 478 | 479 | err = encrypt_one_page(fio); // 如果开启系统加密,会将这个fio->page先加密 480 | 481 | set_page_writeback(page); 482 | ClearPageError(page); 483 | 484 | f2fs_outplace_write_data(&dn, fio); // 执行异地更新函数 485 | 486 | set_inode_flag(inode, FI_APPEND_WRITE); 487 | if (page->index == 0) 488 | set_inode_flag(inode, FI_FIRST_BLOCK_WRITTEN); 489 | out_writepage: 490 | f2fs_put_dnode(&dn); 491 | out: 492 | if (fio->need_lock == LOCK_REQ) 493 | f2fs_unlock_op(fio->sbi); 494 | return err; 495 | } 496 | ``` 497 | 498 | ### f2fs_outplace_write_data函数 499 | 这个函数主要用作异地更新,所谓异地更新即不在原先的物理地址更新数据,因此包含了如下四个步骤: 500 | 1. 分配一个新的物理地址 501 | 2. 将数据写入新的物理地址 502 | 3. 将旧的物理地址无效掉,然后等GC回收 503 | 4. 更新逻辑地址和物理地址的映射关系 504 | 505 | 本函数即完成以上四个步骤: 506 | ```c 507 | void f2fs_outplace_write_data(struct dnode_of_data *dn, 508 | struct f2fs_io_info *fio) 509 | { 510 | struct f2fs_sb_info *sbi = fio->sbi; 511 | struct f2fs_summary sum; 512 | struct node_info ni; 513 | 514 | f2fs_get_node_info(sbi, dn->nid, &ni); 515 | set_summary(&sum, dn->nid, dn->ofs_in_node, ni.version); 516 | 517 | do_write_page(&sum, fio); // 这里完成第1,2,3步骤 518 | f2fs_update_data_blkaddr(dn, fio->new_blkaddr); // 这里完成第四个步骤,重新建立映射 519 | } 520 | ``` 521 | 上面多次提及到`struct dnode_of_data dn`的作用是根据文件inode,找到`f2fs_inode`或者`direct_node`,然后再通过文件偏移得到物理地址,因此`f2fs_update_data_blkaddr`也是通过`dnode_of_data`将新的物理地址更新到`f2fs_inode`或者`direct_node`对应的位置中。 522 | ```c 523 | void f2fs_update_data_blkaddr(struct dnode_of_data *dn, block_t blkaddr) 524 | { 525 | dn->data_blkaddr = blkaddr; // 获得新的物理地址 526 | f2fs_set_data_blkaddr(dn); // 更新地址到f2fs_inode或者direct_node 527 | f2fs_update_extent_cache(dn); // 更新cache 528 | } 529 | 530 | void f2fs_set_data_blkaddr(struct dnode_of_data *dn) 531 | { 532 | f2fs_wait_on_page_writeback(dn->node_page, NODE, true); // 因为要更新node,所以要保证当前的node是最新状态 533 | __set_data_blkaddr(dn); 534 | if (set_page_dirty(dn->node_page)) // 设置dirty,因为更新后的地址要回写到磁盘记录 535 | dn->node_changed = true; 536 | } 537 | 538 | static void __set_data_blkaddr(struct dnode_of_data *dn) 539 | { 540 | struct f2fs_node *rn = F2FS_NODE(dn->node_page); // 根据node page转换到对应的f2fs_node 541 | __le32 *addr_array; 542 | int base = 0; 543 | 544 | addr_array = blkaddr_in_node(rn); // 这个用于获得f2fs_inode->i_addr地址或者direct_node->addr地址 545 | addr_array[base + dn->ofs_in_node] = cpu_to_le32(dn->data_blkaddr); // 根据偏移赋值更新 546 | } 547 | 548 | static inline __le32 *blkaddr_in_node(struct f2fs_node *node) 549 | { 550 | // RAW_IS_INODE判断当前node是属于f2fs_inode还是f2fs_node,然后返回物理地址数组指针 551 | return RAW_IS_INODE(node) ? node->i.i_addr : node->dn.addr; 552 | } 553 | ``` 554 | 555 | ### do_write_page函数 556 | 上一节提及到异地更新的1,2,3步骤都是在这里完成,分别是`f2fs_allocate_data_block`函数完成新物理地址的分配,以及旧物理地址的回收; `f2fs_submit_page_write`函数完成最后一步,将数据提交到磁盘。下面进行分析: 557 | 558 | ```c 559 | static void do_write_page(struct f2fs_summary *sum, struct f2fs_io_info *fio) 560 | { 561 | int type = __get_segment_type(fio); // 获取数据类型,这个类型指HOT/WARM/COLD X NODE/DATA的六种类型 562 | 563 | f2fs_allocate_data_block(fio->sbi, fio->page, fio->old_blkaddr, 564 | &fio->new_blkaddr, sum, type, fio, true); // 完成异地更新的1,2步 565 | 566 | f2fs_submit_page_write(fio); //完成异地更新的第3步 567 | 568 | } 569 | ``` 570 | `f2fs_allocate_data_block`函数首先会根据type获得CURSEG(定义可以参考[Active Segment](https://github.com/RiweiPan/F2FS-NOTES/blob/master/F2FS-Layout/Checkpoint%E7%BB%93%E6%9E%84.md))。然后在CURSEG分配一个新的物理块,然后将旧的物理块无效掉。 571 | ```c 572 | void f2fs_allocate_data_block(struct f2fs_sb_info *sbi, struct page *page, 573 | block_t old_blkaddr, block_t *new_blkaddr, 574 | struct f2fs_summary *sum, int type, 575 | struct f2fs_io_info *fio, bool add_list) 576 | { 577 | struct sit_info *sit_i = SIT_I(sbi); 578 | struct curseg_info *curseg = CURSEG_I(sbi, type); 579 | 580 | *new_blkaddr = NEXT_FREE_BLKADDR(sbi, curseg); // 获取新的物理地址 581 | 582 | __add_sum_entry(sbi, type, sum); // 将当前summary更新到CURSEG中 583 | 584 | __refresh_next_blkoff(sbi, curseg); // 更新下一次可以用的物理地址 585 | 586 | // 下面更新主要是更新SIT区域的segment信息 587 | 588 | // 根据new_blkaddr找到对应的sit_entry,然后更新状态为valid(值为1),表示被用户使用,不可被其他人所使用 589 | update_sit_entry(sbi, *new_blkaddr, 1); 590 | 591 | // 根据old_blkaddr找到对应的sit_entry,然后更新状态为invalid(值为-1),表示被覆盖了,等待GC回收后重新投入使用 592 | if (GET_SEGNO(sbi, old_blkaddr) != NULL_SEGNO) 593 | update_sit_entry(sbi, old_blkaddr, -1); 594 | 595 | // 如果当前segment没有空间进行下一次分配了,就分配一个新的segment给CURSEG 596 | if (!__has_curseg_space(sbi, type)) 597 | sit_i->s_ops->allocate_segment(sbi, type, false); 598 | 599 | // 将segment设置为脏,等CP写回磁盘 600 | locate_dirty_segment(sbi, GET_SEGNO(sbi, old_blkaddr)); 601 | locate_dirty_segment(sbi, GET_SEGNO(sbi, *new_blkaddr)); 602 | 603 | } 604 | ``` 605 | `f2fs_submit_page_write`完成最后的提交到磁盘的任务,具体步骤是先创建一个bio,然后将page加入到bio中,如果bio满了就提交到磁盘。 606 | ```c 607 | void f2fs_submit_page_write(struct f2fs_io_info *fio) 608 | { 609 | struct f2fs_sb_info *sbi = fio->sbi; 610 | enum page_type btype = PAGE_TYPE_OF_BIO(fio->type); 611 | struct f2fs_bio_info *io = sbi->write_io[btype] + fio->temp; // 这个是F2FS用于临时存放bio的变量 612 | struct page *bio_page; 613 | 614 | down_write(&io->io_rwsem); 615 | next: 616 | // 第一步根据是否有加密,将bio_page设置为对应的page 617 | if (fio->encrypted_page) 618 | bio_page = fio->encrypted_page; 619 | else 620 | bio_page = fio->page; 621 | 622 | fio->submitted = true; 623 | 624 | alloc_new: 625 | // 如果bio是null,就创建一个新的bio 626 | if (io->bio == NULL) { 627 | io->bio = __bio_alloc(sbi, fio->new_blkaddr, fio->io_wbc, 628 | BIO_MAX_PAGES, false, 629 | fio->type, fio->temp); // BIO_MAX_PAGES一般等于256 630 | io->fio = *fio; 631 | } 632 | 633 | // 将page加入到bio中,如果 < PAGE_SIZE 表示bio已经满了,因此就先将这个bio提交,然后重新分配一个新的bio 634 | if (bio_add_page(io->bio, bio_page, PAGE_SIZE, 0) < PAGE_SIZE) { 635 | __submit_merged_bio(io); // 提交bio,最终会执行submit_bio函数 636 | goto alloc_new; 637 | } 638 | out: 639 | up_write(&io->io_rwsem); 640 | } 641 | ``` 642 | 需要注意的是,在这个函数,当bio还没有填满page的时候是不会被提交到磁盘的,这是因为F2FS通过增大bio的size提高了写性能。因此,在用户fsync或者系统writeback的时候,为了保证这些page都可以刷写到磁盘,会如`f2fs_write_cache_pages`函数所介绍一样,通过`f2fs_submit_merged_write_cond`函数或者其他函数强行提交这个page未满的bio。 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | -------------------------------------------------------------------------------- /Reading-and-Writing/读流程.md: -------------------------------------------------------------------------------- 1 | # F2FS的读流程 2 | 3 | ## 读流程介绍 4 | F2FS的读流程包含了以下几个子流程: 5 | 1. vfs_read函数 6 | 2. generic_file_read_iter函数: 根据访问类型执行不同的处理 7 | 3. generic_file_buffered_read: 根据用户传入的文件偏移,读取尺寸等信息,计算起始位置和页数,然后遍历每一个page,通过预读或者单个读取的方式从磁盘中读取出来 8 | 4. f2fs_read_data_page&f2fs_read_data_pages函数: 从磁盘读取1个page或者多个page 9 | 5. f2fs_mpage_readpages函数: f2fs读取数据的主流程 10 | 11 | 第一步的vfs_read函数是VFS层面的流程,下面仅针对涉及F2FS的读流程,且经过简化的主要流程进行分析。 12 | 13 | ## generic_file_read_iter函数 14 | 这个函数的作用是处理普通方式访问以及direct方式访问的读行为,这里仅针对普通方式的读访问进行分析: 15 | ```c 16 | ssize_t generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter) 17 | { 18 | size_t count = iov_iter_count(iter); // 获取需要读取的字节数 19 | ssize_t retval = 0; 20 | 21 | if (!count) 22 | goto out; 23 | 24 | if (iocb->ki_flags & IOCB_DIRECT) { // 处理direct方式的访问,这里不做介绍 25 | ... 26 | } 27 | 28 | retval = generic_file_buffered_read(iocb, iter, retval); // 进行普通的读访问 29 | out: 30 | return retval; 31 | } 32 | ``` 33 | 34 | ## generic_file_buffered_read 35 | 在介绍这两个之前,需要先介绍一种VFS提高读取速度的机制: 预读(readahead)机制。它的核心原理是,当用户访问page 1,系统就会将page 1后续的page 2,page 3,page 4一起读取到page cache(减少与磁盘这种速度慢设备的交互次数,提高读性能)。之后用户再连续读取page 2,page 3,page 4时,由于已经读取到内存中,因此可以快速地返回给用户。 36 | 37 | `generic_file_buffered_read`函数的主要作用是循环地从磁盘或者内存读取用户需要的page,同时也会在某些情况调用`page_cache_sync_readahead`函数进行预读,由于函数比较复杂,且很多goto语句,简化后的步骤如下: 38 | 39 | **情况1: 预读(readahead)机制成功预读到用户需要接下来访问的page** 40 | 41 | 1. ind_get_page: 系统无法在cache中找到用户需要的page 42 | 2. page_cache_sync_readahead: 系统执行该函数进行预读,一次性读取多个page 43 | 3. find_get_page: 再重新在cache获取一次page,获取成功后跳转到page ok区域 44 | 4. page_ok: 复制page的数据去用户传入的buffer中,然后判读是否为最后一个page,如果是则退出读流程 45 | 46 | **情况2: 预读(readahead)机制错误预读到用户需要接下来访问的page** 47 | 48 | 1. find_get_page: 系统无法在cache中找到用户需要的page 49 | 2. page_cache_sync_readahead: 系统执行该函数进行预读,一次性读取多个page 50 | 3. find_get_page: 再重新在cache获取一次page,获取失败,跳转到no_cached_page区域 51 | 4. no_cached_page: 创建一个page cache结构,加入到LRU后,跳转到readpage区域 52 | 5. readpage: 执行`mapping->a_ops->readpage`函数从磁盘读取数据,成功后跳转到page ok区域 53 | 6. page_ok: 复制page的数据去用户传入的buffer中,然后判读是否为最后一个page,如果是则退出读流程。 54 | 55 | 56 | ```c 57 | static ssize_t generic_file_buffered_read(struct kiocb *iocb, 58 | struct iov_iter *iter, ssize_t written) 59 | { 60 | 61 | index = *ppos >> PAGE_SHIFT; // 文件指针偏移*ppos除以page的大小就是页偏移index 62 | prev_index = ra->prev_pos >> PAGE_SHIFT; 63 | prev_offset = ra->prev_pos & (PAGE_SIZE-1); 64 | last_index = (*ppos + iter->count + PAGE_SIZE-1) >> PAGE_SHIFT; 65 | offset = *ppos & ~PAGE_MASK; 66 | 67 | for (;;) { 68 | find_page: 69 | page = find_get_page(mapping, index); // 根据页偏移index从cache获取page 70 | if (!page) { // 获取失败进行一次预读 71 | page_cache_sync_readahead(mapping, ra, filp, 72 | index, last_index - index); 73 | page = find_get_page(mapping, index); // 预读后再从cache获取page 74 | if (unlikely(page == NULL)) // 如果仍然失败则跳转到no_cached_page,成功则直接去page ok区域 75 | goto no_cached_page; 76 | } 77 | page_ok: 78 | // page数据读取成功后都进入这个区域,用于将数据复制到用户传入的buffer中 79 | isize = i_size_read(inode); 80 | end_index = (isize - 1) >> PAGE_SHIFT; 81 | 82 | nr = PAGE_SIZE; 83 | if (index == end_index) { // 如果到了最后一个index就退出循环 84 | nr = ((isize - 1) & ~PAGE_MASK) + 1; 85 | if (nr <= offset) { 86 | put_page(page); 87 | goto out; 88 | } 89 | } 90 | nr = nr - offset; 91 | ret = copy_page_to_iter(page, offset, nr, iter); // 复制用户数据到buffer中 92 | offset += ret; 93 | index += offset >> PAGE_SHIFT; 94 | offset &= ~PAGE_MASK; 95 | prev_offset = offset; 96 | 97 | put_page(page); 98 | written += ret; 99 | if (!iov_iter_count(iter)) // 如果将所有数据读取完毕后退出循环 100 | goto out; 101 | if (ret < nr) { 102 | error = -EFAULT; 103 | goto out; 104 | } 105 | continue; 106 | readpage: 107 | ClearPageError(page); 108 | error = mapping->a_ops->readpage(filp, page); // 去磁盘进行读取 109 | goto page_ok; 110 | no_cached_page: 111 | page = page_cache_alloc(mapping); // 创建page cache 112 | error = add_to_page_cache_lru(page, mapping, index, 113 | mapping_gfp_constraint(mapping, GFP_KERNEL)); // 加入lru 114 | goto readpage; 115 | } 116 | out: 117 | ra->prev_pos = prev_index; 118 | ra->prev_pos <<= PAGE_SHIFT; 119 | ra->prev_pos |= prev_offset; 120 | 121 | *ppos = ((loff_t)index << PAGE_SHIFT) + offset; 122 | file_accessed(filp); 123 | return written ? written : error; 124 | } 125 | ``` 126 | 预读函数`page_cache_sync_readahead`的分析由于篇幅有限无法全部展示,因此这里仅分析它的核心调用函数`__do_page_cache_readahead`: 127 | ```c 128 | unsigned int __do_page_cache_readahead(struct address_space *mapping, 129 | struct file *filp, pgoff_t offset, unsigned long nr_to_read, 130 | unsigned long lookahead_size) 131 | { 132 | end_index = ((isize - 1) >> PAGE_SHIFT); // 得到文件的最后一个页的页偏移index 133 | 134 | for (page_idx = 0; page_idx < nr_to_read; page_idx++) { // nr_to_read是需要预读的page的数目 135 | pgoff_t page_offset = offset + page_idx; // offset表示从第几个page开始预读 136 | 137 | if (page_offset > end_index) // 预读超过了文件大小就退出 138 | break; 139 | 140 | page = __page_cache_alloc(gfp_mask); // 创建page cache 141 | page->index = page_offset; // 设置page index 142 | list_add(&page->lru, &page_pool); // 将所有预读的page加入到一个list中 143 | nr_pages++; 144 | } 145 | 146 | if (nr_pages) 147 | read_pages(mapping, filp, &page_pool, nr_pages, gfp_mask); // 执行预读 148 | BUG_ON(!list_empty(&page_pool)); 149 | out: 150 | return nr_pages; 151 | } 152 | 153 | static int read_pages(struct address_space *mapping, struct file *filp, 154 | struct list_head *pages, unsigned int nr_pages, gfp_t gfp) 155 | { 156 | struct blk_plug plug; 157 | unsigned page_idx; 158 | int ret; 159 | 160 | blk_start_plug(&plug); 161 | 162 | if (mapping->a_ops->readpages) { 163 | ret = mapping->a_ops->readpages(filp, mapping, pages, nr_pages); // 执行readpages函数进行预读 164 | put_pages_list(pages); 165 | goto out; 166 | } 167 | ret = 0; 168 | 169 | out: 170 | blk_finish_plug(&plug); 171 | 172 | return ret; 173 | } 174 | ``` 175 | 176 | ## f2fs_read_data_page&f2fs_read_data_pages函数 177 | 从上一节可以知道,当预读机制会调用`mapping->a_ops->readpages`函数一次性读取多个page。而当预读失败时,也会调用`mapping->a_ops->readpage`读取单个page。这两个函数在f2fs中对应的就是`f2fs_read_page`和`f2fs_read_pages`,如下所示: 178 | ```c 179 | static int f2fs_read_data_page(struct file *file, struct page *page) 180 | { 181 | struct inode *inode = page->mapping->host; 182 | int ret = -EAGAIN; 183 | 184 | trace_f2fs_readpage(page, DATA); 185 | 186 | if (f2fs_has_inline_data(inode)) // inline文件使用特定的读取方法,这里暂不分析 187 | ret = f2fs_read_inline_data(inode, page); 188 | ret = f2fs_mpage_readpages(page->mapping, NULL, page, 1); // 读取1个page 189 | return ret; 190 | } 191 | 192 | static int f2fs_read_data_pages(struct file *file, 193 | struct address_space *mapping, 194 | struct list_head *pages, unsigned nr_pages) 195 | { 196 | struct inode *inode = mapping->host; 197 | struct page *page = list_last_entry(pages, struct page, lru); 198 | 199 | trace_f2fs_readpages(inode, page, nr_pages); 200 | 201 | if (f2fs_has_inline_data(inode)) // inline文件是size小于1个page的文件,因此不需要进行预读,直接return 0 202 | return 0; 203 | 204 | return f2fs_mpage_readpages(mapping, pages, NULL, nr_pages); // 读取nr_pages个page 205 | } 206 | ``` 207 | 208 | ## f2fs_mpage_readpages函数 209 | 无论是`f2fs_read_page`函数还是`f2fs_read_pages`函数,都是调用`f2fs_mpage_readpages`函数进行读取,区别仅在于传入参数。`f2fs_mpage_readpages`的定义为: 210 | ```c 211 | static int f2fs_mpage_readpages(struct address_space *mapping, 212 | struct list_head *pages, struct page *page, unsigned nr_pages); 213 | ``` 214 | 第二个参数表示一个链表头,这个链表保存了多个page,因此需要写入多个page的时候,就要传入一个List。 215 | 第三个参数表示单个page,在写入单个page的时候,通过这个函数写入。 216 | 第四个参数表示需要写入page的数目。 217 | 218 | 因此 219 | 在写入多个page的时候,需要设定第二个参数,和第四个参数,然后设定第三个参数为NULL。 220 | 在写入单个page的时候,需要设定第三个参数,和第四个参数,然后设定第二个参数为NULL。 221 | 222 | 然后分析这个函数的执行流程: 223 | 1. 遍历传入的page,得到每一个page的index以及inode 224 | 2. 将page的inode以及index传入 `f2fs_map_blocks` 函数获取到该page的物理地址 225 | 3. 将物理地址通过 `submit_bio` 读取该page在磁盘中的数据 226 | 227 | ```c 228 | static int f2fs_mpage_readpages(struct address_space *mapping, 229 | struct list_head *pages, struct page *page, 230 | unsigned nr_pages) 231 | { 232 | // 主流程第一步 初始化map结构,这个步骤非常重要,用于获取page在磁盘的物理地址 233 | struct f2fs_map_blocks map; 234 | map.m_pblk = 0; 235 | map.m_lblk = 0; 236 | map.m_len = 0; 237 | map.m_flags = 0; 238 | map.m_next_pgofs = NULL; 239 | 240 | // 主流程第二步 开始进行遍历,结束条件为 nr_pages 不为空 241 | for (page_idx = 0; nr_pages; page_idx++, nr_pages--) { 242 | 243 | // 循环第一步,如果是读取多个page,则pages不为空,从list里面读取每一次的page结构 244 | if (pages) { 245 | page = list_entry(pages->prev, struct page, lru); 246 | list_del(&page->lru); 247 | if (add_to_page_cache_lru(page, mapping, 248 | page->index, GFP_KERNEL)) 249 | goto next_page; 250 | } 251 | 252 | /** 253 | * map.m_lblk是上一个block_in_file 254 | * map.m_lblk + map.m_len是需要读取长度的最后一个blokaddr 255 | * 因此这里的意思是,如果是在这个 map.m_lblk < block_in_file < map.m_lblk + map.m_len 256 | * 这个范围里面,不需要map,直接将上次的blkaddr+1就是需要的地址 257 | * 258 | */ 259 | // 循环第二步,如果上一次找到了page,则跳到 got_it 通过bio获取page的具体数据 260 | if ((map.m_flags & F2FS_MAP_MAPPED) && block_in_file > map.m_lblk && 261 | block_in_file < (map.m_lblk + map.m_len)) 262 | goto got_it; 263 | 264 | // 循环第三步,使用page offset和length,通过f2fs_map_blocks获得物理地址 265 | map.m_flags = 0; 266 | if (block_in_file < last_block) { 267 | map.m_lblk = block_in_file; // 文件的第几个block 268 | map.m_len = last_block - block_in_file; // 读取的block的长度 269 | 270 | if (f2fs_map_blocks(inode, &map, 0, 271 | F2FS_GET_BLOCK_READ)) 272 | goto set_error_page; 273 | } 274 | 275 | got_it: 276 | // 循环第四步,通过map的结果执行不一样的处理方式 277 | if ((map.m_flags & F2FS_MAP_MAPPED)) { // 如果找到了地址,则计算block_nr得到磁盘的地址 278 | block_nr = map.m_pblk + block_in_file - map.m_lblk; 279 | SetPageMappedToDisk(page); 280 | 281 | if (!PageUptodate(page) && !cleancache_get_page(page)) { 282 | SetPageUptodate(page); 283 | goto confused; 284 | } 285 | } else { // 获取失败了,则跳过这个page 286 | zero_user_segment(page, 0, PAGE_SIZE); 287 | SetPageUptodate(page); 288 | unlock_page(page); 289 | goto next_page; 290 | } 291 | 292 | /** 293 | * 这部分开始用于将物理地址通过submit_bio提交到磁盘读取数据 294 | * 由于从磁盘读取数据是一个相对耗时的操作, 295 | * 因此显然每读取一个页就访问一次磁盘一次的方式是低效的且影响读性能的, 296 | * 所以F2FS会尽量一次性提交多个页到磁盘读取数据,以提高性能。 297 | * 298 | * 这部分开始就是具体实现: 299 | * 1. 创建一个bio(最大一次性提交256个页) 300 | * 2. 将需要读取的页添加到这个bio中, 301 | * ------如果bio未满则将page添加到bio中 302 | * ------如果bio满了立即访问磁盘读取 303 | * ------如果循环结束以后,bio还是未满,则通过本函数末尾的操作提交未满的bio。 304 | * 305 | */ 306 | 307 | // 循环第五步,判断bio装的page是否到了设定的最大数量,如果到了最大值则先发送到磁盘 308 | if (bio && (last_block_in_bio != block_nr - 1)) { 309 | submit_and_realloc: 310 | submit_bio(READ, bio); 311 | bio = NULL; 312 | } 313 | 314 | // 循环第六步,如果bio是空,则创建一个bio,然后指定的f2fs_read_end_io进行读取 315 | if (bio == NULL) { 316 | struct fscrypt_ctx *ctx = NULL; 317 | 318 | if (f2fs_encrypted_inode(inode) && 319 | S_ISREG(inode->i_mode)) { 320 | 321 | ctx = fscrypt_get_ctx(inode, GFP_NOFS); 322 | if (IS_ERR(ctx)) 323 | goto set_error_page; 324 | 325 | /* wait the page to be moved by cleaning */ 326 | f2fs_wait_on_encrypted_page_writeback( 327 | F2FS_I_SB(inode), block_nr); 328 | } 329 | 330 | bio = bio_alloc(GFP_KERNEL, 331 | min_t(int, nr_pages, BIO_MAX_PAGES)); // 创建bio 332 | if (!bio) { 333 | if (ctx) 334 | fscrypt_release_ctx(ctx); 335 | goto set_error_page; 336 | } 337 | bio->bi_bdev = bdev; 338 | bio->bi_iter.bi_sector = SECTOR_FROM_BLOCK(block_nr); // 设定bio的sector地址 339 | bio->bi_end_io = f2fs_read_end_io; 340 | bio->bi_private = ctx; 341 | } 342 | 343 | // 循环第七步,将page加入到bio中,等待第五步满了之后发送到磁盘 344 | if (bio_add_page(bio, page, blocksize, 0) < blocksize) 345 | goto submit_and_realloc; 346 | 347 | set_error_page: 348 | SetPageError(page); 349 | zero_user_segment(page, 0, PAGE_SIZE); 350 | unlock_page(page); 351 | goto next_page; 352 | confused: // 特殊情况进行submit bio 353 | if (bio) { 354 | submit_bio(READ, bio); 355 | bio = NULL; 356 | } 357 | unlock_page(page); 358 | next_page: 359 | if (pages) 360 | put_page(page); 361 | 362 | } 363 | 364 | 365 | BUG_ON(pages && !list_empty(pages)); 366 | 367 | // 如果还有bio没有处理,例如读取的页遍历完以后,还没有达到第五步要求的bio的最大保存页数,就会在这里提交bio到磁盘读取 368 | if (bio) 369 | submit_bio(READ, bio); 370 | return 0; 371 | } 372 | ``` -------------------------------------------------------------------------------- /img/F2FS-Layout/cp_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/img/F2FS-Layout/cp_layout.png -------------------------------------------------------------------------------- /img/F2FS-Layout/cp_layout2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/img/F2FS-Layout/cp_layout2.png -------------------------------------------------------------------------------- /img/F2FS-Layout/f2fs-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/img/F2FS-Layout/f2fs-layout.png -------------------------------------------------------------------------------- /img/F2FS-Layout/inode-blockmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/img/F2FS-Layout/inode-blockmap.png -------------------------------------------------------------------------------- /img/F2FS-Layout/nat_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/img/F2FS-Layout/nat_layout.png -------------------------------------------------------------------------------- /img/F2FS-Layout/node-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/img/F2FS-Layout/node-table.png -------------------------------------------------------------------------------- /img/F2FS-Layout/sb_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/img/F2FS-Layout/sb_layout.png -------------------------------------------------------------------------------- /img/F2FS-Layout/sb_layout2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/img/F2FS-Layout/sb_layout2.png -------------------------------------------------------------------------------- /img/F2FS-Layout/sit_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/img/F2FS-Layout/sit_layout.png -------------------------------------------------------------------------------- /img/F2FS-Layout/sit_layout2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/img/F2FS-Layout/sit_layout2.png -------------------------------------------------------------------------------- /img/F2FS-Layout/ssa_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/img/F2FS-Layout/ssa_layout.png -------------------------------------------------------------------------------- /img/F2FS-Layout/ssa_layout2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiweiPan/F2FS-NOTES/ddea1fb304cb1b5f31c8448bdb1a2c353155c145/img/F2FS-Layout/ssa_layout2.png --------------------------------------------------------------------------------