関連記事
- Part 1: 環境セットアップ
- Part 2: System call Interface
- Part 3: VFS
- Part 4: ext2 (1) write_iter
- Part 5: ext2 (2) write_begin
- Part 6: ext2 (3) get_block
- Part 7: ext2 (4) write_end
- Part 8: writeback (1) work Queue
- Part 9: writeback (2) wb_writeback
- Part 10: writeback (3) writepages
- Part 11: writeback (4) write_inode
- Part 12: block (1) submit_bio
- Part 13: block (2) blk_mq
- Part 14: I/O scheduler (1) mq-deadline
- Part 15: I/O scheduler (2) insert_request
- Part 16: I/O scheduler (3) dispatch_request
- Part 17: block (3) blk_mq_run_work_fn
- Part 18: block (4) block: blk_mq_do_dispatch_sched
- Part 19: MMC (1) initialization
- Part 20: PL181 (1) mmci_probe
- Part 21: MMC (2) mmc_start_host
- Part 22: MMC (3) mmc_rescan
- Part 23: MMC (4) mmc_attach_sd
概要
QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。
本章では、ext2_write_inode
関数を確認した。
はじめに
ユーザプロセスはファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。
調査対象や環境などはPart 1: 環境セットアップを参照。
本記事では、writebackカーネルスレッドがwrite_inode
を呼び出すところを確認する。
write_inodeの概要
kerner thread のライトバック処理によって、Dirtyのinodeに対してwritepages
関数とwrite_inode
関数を呼び出す。
基本的には、writepages
関数はファイルの実データの書き込み、write_inode
関数はファイルのメタデータの書き込みをする。
ext2ファイルシステムの場合では、ext2_writepages
関数とext2_write_inode
関数が定義されている。
ext2_writepages
関数の定義は次の通りとなっている。
// 1639: int ext2_write_inode(struct inode *inode, struct writeback_control *wbc) { return __ext2_write_inode(inode, wbc->sync_mode == WB_SYNC_ALL); }
ext2_write_inode
関数は、__ext2_write_inode
関数を呼び出す。
// 1635: static int __ext2_write_inode(struct inode *inode, int do_sync) { struct ext2_inode_info *ei = EXT2_I(inode); struct super_block *sb = inode->i_sb; ino_t ino = inode->i_ino; uid_t uid = i_uid_read(inode); gid_t gid = i_gid_read(inode); struct buffer_head * bh; struct ext2_inode * raw_inode = ext2_get_inode(sb, ino, &bh); int n; int err = 0; if (IS_ERR(raw_inode)) return -EIO; /* For fields not not tracking in the in-memory inode, * initialise them to zero for new inodes. */ if (ei->i_state & EXT2_STATE_NEW) memset(raw_inode, 0, EXT2_SB(sb)->s_inode_size); raw_inode->i_mode = cpu_to_le16(inode->i_mode); if (!(test_opt(sb, NO_UID32))) { raw_inode->i_uid_low = cpu_to_le16(low_16_bits(uid)); raw_inode->i_gid_low = cpu_to_le16(low_16_bits(gid)); /* * Fix up interoperability with old kernels. Otherwise, old inodes get * re-used with the upper 16 bits of the uid/gid intact */ if (!ei->i_dtime) { raw_inode->i_uid_high = cpu_to_le16(high_16_bits(uid)); raw_inode->i_gid_high = cpu_to_le16(high_16_bits(gid)); } else { raw_inode->i_uid_high = 0; raw_inode->i_gid_high = 0; } } else { raw_inode->i_uid_low = cpu_to_le16(fs_high2lowuid(uid)); raw_inode->i_gid_low = cpu_to_le16(fs_high2lowgid(gid)); raw_inode->i_uid_high = 0; raw_inode->i_gid_high = 0; } raw_inode->i_links_count = cpu_to_le16(inode->i_nlink); raw_inode->i_size = cpu_to_le32(inode->i_size); raw_inode->i_atime = cpu_to_le32(inode->i_atime.tv_sec); raw_inode->i_ctime = cpu_to_le32(inode->i_ctime.tv_sec); raw_inode->i_mtime = cpu_to_le32(inode->i_mtime.tv_sec); raw_inode->i_blocks = cpu_to_le32(inode->i_blocks); raw_inode->i_dtime = cpu_to_le32(ei->i_dtime); raw_inode->i_flags = cpu_to_le32(ei->i_flags); raw_inode->i_faddr = cpu_to_le32(ei->i_faddr); raw_inode->i_frag = ei->i_frag_no; raw_inode->i_fsize = ei->i_frag_size; raw_inode->i_file_acl = cpu_to_le32(ei->i_file_acl); if (!S_ISREG(inode->i_mode)) raw_inode->i_dir_acl = cpu_to_le32(ei->i_dir_acl); else { raw_inode->i_size_high = cpu_to_le32(inode->i_size >> 32); if (inode->i_size > 0x7fffffffULL) { if (!EXT2_HAS_RO_COMPAT_FEATURE(sb, EXT2_FEATURE_RO_COMPAT_LARGE_FILE) || EXT2_SB(sb)->s_es->s_rev_level == cpu_to_le32(EXT2_GOOD_OLD_REV)) { /* If this is the first large file * created, add a flag to the superblock. */ spin_lock(&EXT2_SB(sb)->s_lock); ext2_update_dynamic_rev(sb); EXT2_SET_RO_COMPAT_FEATURE(sb, EXT2_FEATURE_RO_COMPAT_LARGE_FILE); spin_unlock(&EXT2_SB(sb)->s_lock); ext2_sync_super(sb, EXT2_SB(sb)->s_es, 1); } } } raw_inode->i_generation = cpu_to_le32(inode->i_generation); if (S_ISCHR(inode->i_mode) || S_ISBLK(inode->i_mode)) { if (old_valid_dev(inode->i_rdev)) { raw_inode->i_block[0] = cpu_to_le32(old_encode_dev(inode->i_rdev)); raw_inode->i_block[1] = 0; } else { raw_inode->i_block[0] = 0; raw_inode->i_block[1] = cpu_to_le32(new_encode_dev(inode->i_rdev)); raw_inode->i_block[2] = 0; } } else for (n = 0; n < EXT2_N_BLOCKS; n++) raw_inode->i_block[n] = ei->i_data[n]; mark_buffer_dirty(bh); if (do_sync) { sync_dirty_buffer(bh); if (buffer_req(bh) && !buffer_uptodate(bh)) { printk ("IO error syncing ext2 inode [%s:%08lx]\n", sb->s_id, (unsigned long) ino); err = -EIO; } } ei->i_state &= ~EXT2_STATE_NEW; brelse (bh); return err; }
ext2ファイルシステムにおいてinodeは、struct ext2_inode
データ構造によって管理している。
struct ext2_inode
は、inode
型のメンバvfs_inode
があり、VFSレイヤから渡されたデータはここに格納されている。
__ext2_write_inode
関数は、書き込み対象のinodeをstruct ex2_inode
データ構造を介して書き込みを実施する。
そのため、struct ext2_inode
構造体はストレージ上におけるinodeの構造と一致している。
struct ext2_inode
構造体は次のメンバを持つデータ構造である。
型 | メンバ | 概要 |
---|---|---|
__le16 |
i_mode |
ファイルのモード |
__le16 |
i_uid |
ユーザID |
__le32 |
i_size; |
ファイルサイズ(バイト) |
__le32 |
i_atime |
最終ファイルアクセス時間 |
__le32 |
i_ctime |
最終ファイル作成時間 |
__le32 |
i_mtime |
最終ファイル修正時間 |
__le32 |
i_dtime |
ファイル削除時間 |
__le16 |
i_gid |
グループID |
__le16 |
i_links_count |
ハードリンク数 |
__le32 |
i_blocks |
データブロック数 |
__le32 |
i_flags |
ファイルのフラグ |
__le32 |
osd1 |
OS依存情報1 |
__le32 |
i_block[EXT2_N_BLOCKS] |
データブロックの番号 |
__le32 |
i_generation |
NFS用のファイルバージョン |
__le32 |
i_file_acl |
ファイル用ACL |
__le32 |
i_dir_acl |
ディレクトリ用ACI |
__le32 |
i_faddr |
フラグメントのアドレス |
__le32 |
osd2 |
OS依存情報2 |
ext2ファイルシステムでは、ext2_get_inode
関数でstruct ext2_inode
データ構造を取得する。
// 1328: static struct ext2_inode *ext2_get_inode(struct super_block *sb, ino_t ino, struct buffer_head **p) { struct buffer_head * bh; unsigned long block_group; unsigned long block; unsigned long offset; struct ext2_group_desc * gdp; *p = NULL; if ((ino != EXT2_ROOT_INO && ino < EXT2_FIRST_INO(sb)) || ino > le32_to_cpu(EXT2_SB(sb)->s_es->s_inodes_count)) goto Einval; block_group = (ino - 1) / EXT2_INODES_PER_GROUP(sb); gdp = ext2_get_group_desc(sb, block_group, NULL); if (!gdp) goto Egdp; /* * Figure out the offset within the block group inode table */ offset = ((ino - 1) % EXT2_INODES_PER_GROUP(sb)) * EXT2_INODE_SIZE(sb); block = le32_to_cpu(gdp->bg_inode_table) + (offset >> EXT2_BLOCK_SIZE_BITS(sb)); if (!(bh = sb_bread(sb, block))) goto Eio; *p = bh; offset &= (EXT2_BLOCK_SIZE(sb) - 1); return (struct ext2_inode *) (bh->b_data + offset); Einval: ext2_error(sb, "ext2_get_inode", "bad inode number: %lu", (unsigned long) ino); return ERR_PTR(-EINVAL); Eio: ext2_error(sb, "ext2_get_inode", "unable to read inode block - inode=%lu, block=%lu", (unsigned long) ino, block); Egdp: return ERR_PTR(-EIO); }
ブロックグループディスクリプタの探索
ext2_get_group_desc
関数では、渡されたinode番号ino
を基にそのinodeが格納されているグループディスクリプタ block_group
を探索する。
ext2ファイルシステムでは、super blockとblock group descriptorはBlock Group #0に保存され、他のBlock Groupには複製が保存される。
グループディスクリプタ struct ext2_group_desc
データ構造は次のメンバを持つ。
型 | メンバ | 概要 |
---|---|---|
__le32 |
bg_block_bitmap |
Data Block Bitmapのブロック番号 |
__le32 |
bg_inode_bitmap |
inode Bitmapのブロック番号 |
__le32 |
bg_inode_table |
最初のinode tableがあるブロックのブロック番号 |
__le16 |
bg_free_blocks_count |
ブロックグループ内の空きブロック数 |
__le16 |
bg_free_inodes_count |
ブロックグループ内の空きinode数 |
__le16 |
bg_used_dirs_count |
ブロックグループ内のディレクトリ数 |
__le16 |
bg_pad |
4バイト境界のアライン |
__le32 |
bg_reserved[3] |
予約領域 |
inode tableの探索
グループディスクリプタにあるinode tableの先頭ブロック番号bg_inode_table
とinode番号から、対象のブロックを検索する。
得られたブロック番号から、sb_bread
関数によってメモリ上にロードする。(sb_bread
関数については下記参照)
その結果、次のようなデータ構造となる。
bufferの書き出し
__ext2_write_inode
関数は、ext2_get_inode
関数によってストレージからロードしたext2_inode
データ構造体をinode
を基に更新し、ストレージに書き出しする。
その後、更新したinodeが含まれているbh
に対して、mark_buffer_dirty
関数(と必要に応じてsync_dirty_buffer
関数)を呼び出す。
mark_buffer_dirty
関数では、buffer_head
とpage
にDirtyフラグをセットとDirtyリストに追加する。
また、inodeのexpireを契機としている場合は、do_sync
には0
が設定されるため、sync_dirty_buffer
関数は呼ばれない。
sb_bread
関数では、def_blk_aops
操作群が設定される。
// 377: const struct address_space_operations def_blk_aops = { .set_page_dirty = __set_page_dirty_buffers, .readpage = blkdev_readpage, .readahead = blkdev_readahead, .writepage = blkdev_writepage, .write_begin = blkdev_write_begin, .write_end = blkdev_write_end, .writepages = blkdev_writepages, .direct_IO = blkdev_direct_IO, .migratepage = buffer_migrate_page_norefs, .is_dirty_writeback = buffer_check_dirty_writeback, };
writebackのシーケンスに関しては、ext2と同様なので省略する。
// 371: static int blkdev_writepages(struct address_space *mapping, struct writeback_control *wbc) { return generic_writepages(mapping, wbc); }
blkdev_writepages
関数は、generic_writepages
関数を呼び出す。
// 2335: int generic_writepages(struct address_space *mapping, struct writeback_control *wbc) { struct blk_plug plug; int ret; /* deal with chardevs and other special file */ if (!mapping->a_ops->writepage) return 0; blk_start_plug(&plug); ret = write_cache_pages(mapping, wbc, __writepage, mapping); blk_finish_plug(&plug); return ret; }
write_cache_pages
関数を呼ぶ流れについても、ext2_writepages
関数と同様であるので省略する。
おわりに
本記事では、ext2ファイルシステムのwrite_inode
操作 (ext2_write_inode
)を解説した。
今回の環境でext2_write_inode
関数を実行することで、次のwriteback kthreadの起床タイミング (同条件) で 次のようなbio
構造体が生成された。
変更履歴
- 2022/05/06: 記事公開
- 2022/06/13: 動作後のイメージ図の追加
- 2022/09/19: カーネルバージョンを5.15に変更