LeavaTailの日記

LeavaTailの日記

Linuxエンジニアを目指した技術者の備忘録

Linuxカーネルのファイルアクセスの処理を追いかける (11) ext2: write_inode

関連記事

概要

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_Iマクロとinode構造体

__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には複製が保存される。

ext2ファイルシステムとブロックグループディスクリプタ

グループディスクリプタ 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番号から、対象のブロックを検索する。

ext2ファイルシステムとiノードテーブル

得られたブロック番号から、sb_bread関数によってメモリ上にロードする。(sb_bread関数については下記参照)

qiita.com

その結果、次のようなデータ構造となる。

sb_bread関数を実施した後のバッファ

bufferの書き出し

__ext2_write_inode関数は、ext2_get_inode関数によってストレージからロードしたext2_inodeデータ構造体をinodeを基に更新し、ストレージに書き出しする。
その後、更新したinodeが含まれているbhに対して、mark_buffer_dirty関数(と必要に応じてsync_dirty_buffer関数)を呼び出す。

mark_buffer_dirty関数では、buffer_headpageにDirtyフラグをセットとDirtyリストに追加する。

mark_buffer_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構造体が生成された。

ext2_write_inode関数の結果

変更履歴

  • 2022/05/06: 記事公開
  • 2022/06/13: 動作後のイメージ図の追加
  • 2022/09/19: カーネルバージョンを5.15に変更