LeavaTailの日記

LeavaTailの日記

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

Linuxカーネルのファイルアクセスの処理を追いかける (5) write_begin

はじめに

ユーザプロセスはファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。

調査対象や環境などはPart 1: 環境セットアップを参照。

f:id:LeavaTail:20201231223348p:plain
調査対象

本記事では、ext2ファイルシステムwrite_iter操作から呼び出されるwrite_begin操作を解説する。

変更履歴

  • 2021/4/24: 記事公開

ステータス

変数 補足
sb->s_writers.rw_sem SB_FREEZE_WRITE - 1 指定したCPU個別
inode->i_rwsem 1

ファイルシステム: write_begin

前回解説したwrite_iter操作(generic_file_write_iter)は、write_begin操作とwrite_end操作を呼び出すことになっている。

このwrite_begin操作やwrite_end操作はページキャッシュに対する操作となっており、address_space_opearationsのメンバの一つとして定義される。

ここで、address_spaceについて再掲する。

f:id:LeavaTail:20210417001359p:plain
inodeとaddress_spaceの対応関係

address_spaceは、inodeとページキャッシュ(ファイルのデータ)を紐づけるデータ型となっている。
ファイルに紐づいているページキャッシュは、xarray型のi_pagesで管理される。 (バージョン4.20より前はradix treeで管理される)

詳細については、下記の記事に詳しく記載されているのでそちらを参照。

qiita.com

ext2ファイルシステムのaddress_space_operationsは、ext2_aopsext2_nobh_aopsext2_dax_aopsが定義されている。
これらは、マウントオプションnobhdaxが指定された場合に、使用するappress_space_operationsを変更することになっている。

今回は処理が簡単なext2_aopsを用いて考えていく。
ext2_aopsの定義は下記の通り。

// 963:
const struct address_space_operations ext2_aops = {
    .readpage       = ext2_readpage,
    .readahead      = ext2_readahead,
    .writepage      = ext2_writepage,
    .write_begin        = ext2_write_begin,
    .write_end      = ext2_write_end,
    .bmap           = ext2_bmap,
    .direct_IO      = ext2_direct_IO,
    .writepages     = ext2_writepages,
    .migratepage        = buffer_migrate_page,
    .is_partially_uptodate  = block_is_partially_uptodate,
    .error_remove_page  = generic_error_remove_page,
};

まずは、write_begin操作のext2_write_begin関数から確認する。

write_begin操作は、write_iter操作(で呼び出されるgeneric_perform_write関数)から呼び出される。

// 3329:
        status = a_ops->write_begin(file, mapping, pos, bytes, flags,
                        &page, &fsdata);
変数名
file オープンしたファイル
mapping ファイルが持っているページキャッシュのXArray(radix-tree)
pos 書き込み先の位置
bytes 書き込むバイト数
flags 0
page 取得したページを格納する
fsdata ext2ファイルシステムでは使用しない

ext2ファイルシステムの場合には、write_begin操作でext2_write_begin関数を実行する。

// 882:
static int
ext2_write_begin(struct file *file, struct address_space *mapping,
        loff_t pos, unsigned len, unsigned flags,
        struct page **pagep, void **fsdata)
{
    int ret;

    ret = block_write_begin(mapping, pos, len, flags, pagep,
                ext2_get_block);
    if (ret < 0)
        ext2_write_failed(mapping, pos + len);
    return ret;
}

この関数では、カーネル内で定義されている汎用の関数block_write_begin関数に、ext2ファイルシステムにおけるブロック取得のext2_get_block関数(次回解説)を渡している。

// 2103:
/*
 * block_write_begin takes care of the basic task of block allocation and
 * bringing partial write blocks uptodate first.
 *
 * The filesystem needs to handle block truncation upon failure.
 */
int block_write_begin(struct address_space *mapping, loff_t pos, unsigned len,
        unsigned flags, struct page **pagep, get_block_t *get_block)
{
    pgoff_t index = pos >> PAGE_SHIFT;
    struct page *page;
    int status;

    page = grab_cache_page_write_begin(mapping, index, flags);
    if (!page)
        return -ENOMEM;

    status = __block_write_begin(page, pos, len, get_block);
    if (unlikely(status)) {
        unlock_page(page);
        put_page(page);
        page = NULL;
    }

    *pagep = page;
    return status;
}

block_write_begin関数では、下記を実施する。

  • ページキャッシュを取得する
  • バッファキャッシュ用のデータ構造を作成する

まずは、ページキャッシュを取得するgrab_cache_page_write_begin関数を確認する。

ページキャッシュを取得する

// 3266:
/*
 * Find or create a page at the given pagecache position. Return the locked
 * page. This function is specifically for buffered writes.
 */
struct page *grab_cache_page_write_begin(struct address_space *mapping,
                    pgoff_t index, unsigned flags)
{
    struct page *page;
    int fgp_flags = FGP_LOCK|FGP_WRITE|FGP_CREAT;

    if (flags & AOP_FLAG_NOFS)
        fgp_flags |= FGP_NOFS;

    page = pagecache_get_page(mapping, index, fgp_flags,
            mapping_gfp_mask(mapping));
    if (page)
        wait_for_stable_page(page);

    return page;
}
EXPORT_SYMBOL(grab_cache_page_write_begin);

grab_cache_page_write_begin関数では、下記を実施する。

  • ページキャッシュを取得する
  • ページキャッシュの割り当て中にアクセスされたページをフラッシュする

ただし、後者('wait_for_stable_page`関数)については、本質でない(かつ未調査である)ため説明を省略する。(該当パッチ)

まずは、ページキャッシュを取得するpagecache_get_page関数を確認する。 このとき、引数のfgp_flagsには次のような値が設定されている。

フラグ名 説明
FGP_LOCK 得られたページをロックする
FGP_WRITE ページに書き込みをする
FGP_CREAT ページが見つからない場合はページを作る

また、引数のgfp_maskには次のような値が設定されている。(実機で0x100ccaであることを確認済み)

パラメータ名 説明
___GFP_HIGHMEM ZONE_HIGHMEMメモリゾーンに属するページフレームを取得する
___GFP_MOVABLE ページ圧縮中にこのページは移動可能であることを示す
___GFP_IO ページフレームを開放するためにI/O転送処置を行ってもよい
___GFP_FS ファイルシステム関連の操作を行っても良い
___GFP_DIRECT_RECLAIM メモリ確保を読んだタスクのコンテキストでメモリ回収する
___GFP_KSWAPD_RECLAIM kswapdカーネルスレッドを起動してメモリ回収できる
___GFP_WRITE ページは書き込み可能である

pagecache_get_page関数の定義は下記のとおりである。
ただし、今回のケースだと呼び出されない行は独自にコメントアウトしている。

// 1787:
struct page *pagecache_get_page(struct address_space *mapping, pgoff_t index,
        int fgp_flags, gfp_t gfp_mask)
{
    struct page *page;

repeat:
    page = find_get_entry(mapping, index);
    if (xa_is_value(page))
        page = NULL;
    if (!page)
        goto no_page;

    if (fgp_flags & FGP_LOCK) {
        // if (fgp_flags & FGP_NOWAIT) {
        // if (!trylock_page(page)) {
        //     put_page(page);
        //     return NULL;
        // }
        // } else {
            lock_page(page);
        // }

        /* Has the page been truncated? */
        if (unlikely(page->mapping != mapping)) {
            unlock_page(page);
            put_page(page);
            goto repeat;
        }
        VM_BUG_ON_PAGE(!thp_contains(page, index), page);
    }

    // if (fgp_flags & FGP_ACCESSED)
    // mark_page_accessed(page);
    else if (fgp_flags & FGP_WRITE) {
        /* Clear idle flag for buffer write */
        if (page_is_idle(page))
            clear_page_idle(page);
    }
    if (!(fgp_flags & FGP_HEAD))
        page = find_subpage(page, index);

no_page:
    if (!page && (fgp_flags & FGP_CREAT)) {
        int err;
        if ((fgp_flags & FGP_WRITE) && mapping_can_writeback(mapping))
            gfp_mask |= __GFP_WRITE;
        // if (fgp_flags & FGP_NOFS)
        //     gfp_mask &= ~__GFP_FS;

        page = __page_cache_alloc(gfp_mask);
        if (!page)
            return NULL;

        if (WARN_ON_ONCE(!(fgp_flags & (FGP_LOCK | FGP_FOR_MMAP))))
            fgp_flags |= FGP_LOCK;

        /* Init accessed so avoid atomic mark_page_accessed later */
        // if (fgp_flags & FGP_ACCESSED)
        //     __SetPageReferenced(page);

        err = add_to_page_cache_lru(page, mapping, index, gfp_mask);
        if (unlikely(err)) {
            put_page(page);
            page = NULL;
            if (err == -EEXIST)
                goto repeat;
        }

        /*
        * add_to_page_cache_lru locks the page, and for mmap we expect
        * an unlocked page.
        */
        // if (page && (fgp_flags & FGP_FOR_MMAP))
        // unlock_page(page);
    }

    return page;
}

まずは、find_get_entry関数の定義を確認する。

// 1691:
struct page *find_get_entry(struct address_space *mapping, pgoff_t index)
{
    XA_STATE(xas, &mapping->i_pages, index);
    struct page *page;

    rcu_read_lock();
repeat:
    xas_reset(&xas);
    page = xas_load(&xas);
    if (xas_retry(&xas, page))
        goto repeat;
    /*
    * A shadow entry of a recently evicted page, or a swap entry from
    * shmem/tmpfs.  Return it without attempting to raise page count.
    */
    if (!page || xa_is_value(page))
        goto out;

    if (!page_cache_get_speculative(page))
        goto repeat;

    /*
    * Has the page moved or been split?
    * This is part of the lockless pagecache protocol. See
    * include/linux/pagemap.h for details.
    */
    if (unlikely(page != xas_reload(&xas))) {
        put_page(page);
        goto repeat;
    }
out:
    rcu_read_unlock();

    return page;
}

find_get_entry関数はmappingからページキャッシュを検索する。

ページキャッシュは、Read-copy update (RCU) による排他制御を実現している。
ここで、RCUとはロックを取得しないという特徴があり、ページキャッシュのような書き込みより読み込みのほうが多いデータ構造に適用される。

RCUの仕組みについて下記の図を用いて解説する。

f:id:LeavaTail:20210306141415p:plain
Read-copy-update (RCU) の概略

4コアのシステムで共通の資源(pageが示すAとB)を扱う場合

  • readの場合、CPU1とCPU2はAをreadできる
  • writeの場合、CPU3はBをreadして、CPU4はBの複製B'にwriteする
    • 新しくBを参照する場合、B'に対してread/writeする
    • Bを参照しなくなったら、BへのポイントをB'に更新する

より詳しく知りたい人は下記の記事で解説されているので、そちらを参照。

tkokamo.hateblo.jp

find_get_entry関数の先頭と末尾にあるrcu_read_lock関数とrcu_read_unlock関数は、RCUにおけるreadの開始と終わりを表す。
rcu_read_lock関数とrcu_read_unlock関数は、ロックを取るわけではなく、プリエンプトを無効にするだけとなっている。

RCUを宣言したら、XArrayからページキャッシュの取得を試みる。

  1. XA_STATEマクロでXArrayデータ構造の宣言をする
  2. xas_reset関数でXArrayの状態を初期化する
  3. xas_load関数でXA_STATEで宣言したxasを辿りデータを取得することができる。
  4. internal entryを返すことがある(multi-index entryの場合)ため、必要に応じてxas_retry関数で再試行する
  5. xa_is_value関数で取得したデータが値エントリであることを確認する
  6. page_cache_get_speculative関数でページキャッシュのリファレンスカウントを更新する
  7. 取得したページキャッシュが移動されていないかxas_reload関数で確認する

これらを踏まえて、pagecache_get_page関数に戻る。
pagecache_get_page関数は、find_get_entry関数で取得したページキャッシュが有効かどうかで処理が変わる。

ページキャッシュが存在する場合

f:id:LeavaTail:20210417001603p:plain
ページキャッシュが有効であるときの状態

その場合、pagecache_get_page関数は1797行目のgoto文は実行されず、処理を続ける。

// 1792:
repeat:
    page = find_get_entry(mapping, index);
    if (xa_is_value(page))
        page = NULL;
    if (!page)
        goto no_page;

    if (fgp_flags & FGP_LOCK) {
        // if (fgp_flags & FGP_NOWAIT) {
        // if (!trylock_page(page)) {
        //     put_page(page);
        //     return NULL;
        // }
        // } else {
            lock_page(page);
        // }

        /* Has the page been truncated? */
        if (unlikely(page->mapping != mapping)) {
            unlock_page(page);
            put_page(page);
            goto repeat;
        }
        VM_BUG_ON_PAGE(!thp_contains(page, index), page);
    }

    // if (fgp_flags & FGP_ACCESSED)
    // mark_page_accessed(page);
    else if (fgp_flags & FGP_WRITE) {
        /* Clear idle flag for buffer write */
        if (page_is_idle(page))
            clear_page_idle(page);
    }
    if (!(fgp_flags & FGP_HEAD))
        page = find_subpage(page, index);

ページキャッシュが有効である場合の挙動は以下の通りとなる。

  1. 既にPG_lockedフラグが設定されている場合、ページキャッシュが獲得するまでwaitする。(lock_page関数)
  2. これまでの間にページキャッシュが切り捨てられてしまった場合には、フラグの解除とリファレンスカウントを下げて、再度ページキャッシュの取得を試みる。
  3. ページがidle状態になっていれば、PAGE_EXT_IDLEフラグを落とす。
  4. 取得したページキャッシュを返す*1

状況によって異なるが、参考としてページキャッシュ取得時のフラグを下記に示す。

フラグ名 説明
PG_uptodate ページ読み込みが完了している
PG_lru LRUリスト内にある
PG_private privateメンバを使用している
ページキャッシュが存在していない場合

f:id:LeavaTail:20210405234055p:plain
ページキャッシュが存在しない場合

下記のリンクに解説あり。

kernhack.hatenablog.com

次に、ページキャッシュが有効である場合(xa_is_valuetrue)について考える。
pagecache_get_page関数はno_pageラベルにジャンプする。

// 1828:
no_page:
    if (!page && (fgp_flags & FGP_CREAT)) {
        int err;
        if ((fgp_flags & FGP_WRITE) && mapping_can_writeback(mapping))
            gfp_mask |= __GFP_WRITE;
        // if (fgp_flags & FGP_NOFS)
        //     gfp_mask &= ~__GFP_FS;

        page = __page_cache_alloc(gfp_mask);
        if (!page)
            return NULL;

        // if (WARN_ON_ONCE(!(fgp_flags & (FGP_LOCK | FGP_FOR_MMAP))))
        //     fgp_flags |= FGP_LOCK;

        /* Init accessed so avoid atomic mark_page_accessed later */
        // if (fgp_flags & FGP_ACCESSED)
        //     __SetPageReferenced(page);

        err = add_to_page_cache_lru(page, mapping, index, gfp_mask);
        if (unlikely(err)) {
            put_page(page);
            page = NULL;
            if (err == -EEXIST)
                goto repeat;
        }

        /*
        * add_to_page_cache_lru locks the page, and for mmap we expect
        * an unlocked page.
        */
        // if (page && (fgp_flags & FGP_FOR_MMAP))
        // unlock_page(page);
    }

    return page;
}

もし、有効なページキャッシュが存在していなかった場合、__page_cache_alloc関数でページを確保する。
__page_cache_alloc関数の定義は下記の通りである。

// 284:
#ifdef CONFIG_NUMA
extern struct page *__page_cache_alloc(gfp_t gfp);
#else
static inline struct page *__page_cache_alloc(gfp_t gfp)
{
    return alloc_pages(gfp, 0);
}
#endif

今回の環境はCONFIG_NUMA=nであるので、__page_cache_alloc関数はalloc_pagesを呼び出す。

// 555:
static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
    return alloc_pages_node(numa_node_id(), gfp_mask, order);
}

numa_node_id()はマクロであり、現在使用しているCPU(今回の場合は0)となる。
alloc_pages関数は、alloc_pages_node関数を呼び出す。

// 519:
static inline struct page *
__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
    VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES);
    VM_WARN_ON((gfp_mask & __GFP_THISNODE) && !node_online(nid));

    return __alloc_pages(gfp_mask, order, nid);
}

__alloc_pages_node関数はパラメータのチェックをして、__alloc_pages関数を呼び出す。

// 508:
static inline struct page *
__alloc_pages(gfp_t gfp_mask, unsigned int order, int preferred_nid)
{
    return __alloc_pages_nodemask(gfp_mask, order, preferred_nid, NULL);
}

__alloc_pages関数は、__alloc_pages_nodemask関数を呼び出す。
ただし下記の記事に解説が載っているので、解説は省略する。

kernhack.hatenablog.com

kernhack.hatenablog.com

kernhack.hatenablog.com

grab_cache_page_write_begin関数の結果、指定されたゾーン (今回はZONE_NORMAL) からページを取得できる。

ページ取得した直後のフラグを下記に示す。

フラグ名 説明

f:id:LeavaTail:20210417001746p:plain
__page_cache_alloc関数でページキャッシュを取得した直後の状態

// 1836:
        page = __page_cache_alloc(gfp_mask);
        if (!page)
            return NULL;

        // if (WARN_ON_ONCE(!(fgp_flags & (FGP_LOCK | FGP_FOR_MMAP))))
        // fgp_flags |= FGP_LOCK;

        /* Init accessed so avoid atomic mark_page_accessed later */
        // if (fgp_flags & FGP_ACCESSED)
        // __SetPageReferenced(page);

        err = add_to_page_cache_lru(page, mapping, index, gfp_mask);
        if (unlikely(err)) {
            put_page(page);
            page = NULL;
            if (err == -EEXIST)
                goto repeat;
        }

        /*
        * add_to_page_cache_lru locks the page, and for mmap we expect
        * an unlocked page.
        */
        // if (page && (fgp_flags & FGP_FOR_MMAP))
        // unlock_page(page);
    }

    return page;
}

ページキャッシュを取得できたら、add_to_page_cache_lru関数を実行する。
最近参照したデータほど再度参照する可能性が高いということから、LRUリストでも管理している。

f:id:LeavaTail:20210417002049p:plain
ページキャッシュとLRUリスト

add_to_page_cache_lru関数はLRUリストに追加する関数であり、定義は下記のようになっている。

// 932:
int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
                pgoff_t offset, gfp_t gfp_mask)
{
    void *shadow = NULL;
    int ret;

    __SetPageLocked(page);
    ret = __add_to_page_cache_locked(page, mapping, offset,
                     gfp_mask, &shadow);
    if (unlikely(ret))
        __ClearPageLocked(page);
    else {
        /*
        * The page might have been evicted from cache only
        * recently, in which case it should be activated like
        * any other repeatedly accessed page.
        * The exception is pages getting rewritten; evicting other
        * data from the working set, only to cache data that will
        * get overwritten with something else, is a waste of memory.
        */
        WARN_ON_ONCE(PageActive(page));
        if (!(gfp_mask & __GFP_WRITE) && shadow)
            workingset_refault(page, shadow);
        lru_cache_add(page);
    }
    return ret;
}

938行目の__SetPageLockedマクロは引数のpageに対して、PG_lockedフラグを設定する。

フラグ名 説明
PG_locked ページはロック状態

その後、939行目の__add_to_page_cache_locked関数でpageとaddress_space構造体を関連付ける。(概要は省略する)

f:id:LeavaTail:20210417002128p:plain
address_space構造体とpageの関連付け後

// 462:
void lru_cache_add(struct page *page)
{
    struct pagevec *pvec;

    VM_BUG_ON_PAGE(PageActive(page) && PageUnevictable(page), page);
    VM_BUG_ON_PAGE(PageLRU(page), page);

    get_page(page);
    local_lock(&lru_pvecs.lock);
    pvec = this_cpu_ptr(&lru_pvecs.lru_add);
    if (!pagevec_add(pvec, page) || PageCompound(page))
        __pagevec_lru_add(pvec);
    local_unlock(&lru_pvecs.lock);
}

lru_cache_add関数では、獲得したページをlruリストにすぐには登録しない。
pagevec_add関数で一旦CPU変数へ登録して、後ほどまとめてlryリストに登録する。

pagevec_add関数が0を返すとき、溜まったページキャッシュを__pagevec_lru_add関数でlruリストに登録する。

f:id:LeavaTail:20210417002321p:plain
LRUにページキャッシュを登録する

バッファキャッシュ用のデータ構造を作成する

Linuxではページキャッシュとは別にバッファキャッシュというデータ構造が存在している。

バッファキャッシュはストレージのデータをメモリにキャッシュするためのものである。
ファイルシステムの観点でいえば、ファイルの実データやinode, ディレクトリなどが対象となっている。 さらに、バッファキャッシュはページキャッシュと紐づけることになっている。

write_beginでは、__block_write_begin関数でバッファキャッシュを準備する。

// 2103:
/*
 * block_write_begin takes care of the basic task of block allocation and
 * bringing partial write blocks uptodate first.
 *
 * The filesystem needs to handle block truncation upon failure.
 */
int block_write_begin(struct address_space *mapping, loff_t pos, unsigned len,
        unsigned flags, struct page **pagep, get_block_t *get_block)
{
    pgoff_t index = pos >> PAGE_SHIFT;
    struct page *page;
    int status;

    page = grab_cache_page_write_begin(mapping, index, flags);
    if (!page)
        return -ENOMEM;

    status = __block_write_begin(page, pos, len, get_block);
    if (unlikely(status)) {
        unlock_page(page);
        put_page(page);
        page = NULL;
    }

    *pagep = page;
    return status;
}

grab_cache_page_write_begin関数によってページキャッシュが取得できたら、__block_write_begin関数でバッファキャッシュ用のデータ構造を作成する。
__block_write_begin関数の定義は下記のとおりである。

// 2058:
int __block_write_begin(struct page *page, loff_t pos, unsigned len,
        get_block_t *get_block)
{
    return __block_write_begin_int(page, pos, len, get_block, NULL);
}
EXPORT_SYMBOL(__block_write_begin);

__block_write_begin関数は、仮引数と__block_write_begin_int関数に渡すだけとなっている。
__block_write_begin_int関数の定義は下記の通りである。

// 1973:
int __block_write_begin_int(struct page *page, loff_t pos, unsigned len,
        get_block_t *get_block, struct iomap *iomap)
{
    unsigned from = pos & (PAGE_SIZE - 1);
    unsigned to = from + len;
    struct inode *inode = page->mapping->host;
    unsigned block_start, block_end;
    sector_t block;
    int err = 0;
    unsigned blocksize, bbits;
    struct buffer_head *bh, *head, *wait[2], **wait_bh=wait;

    BUG_ON(!PageLocked(page));
    BUG_ON(from > PAGE_SIZE);
    BUG_ON(to > PAGE_SIZE);
    BUG_ON(from > to);

    head = create_page_buffers(page, inode, 0);
    blocksize = head->b_size;
    bbits = block_size_bits(blocksize);

    block = (sector_t)page->index << (PAGE_SHIFT - bbits);

    for(bh = head, block_start = 0; bh != head || !block_start;
        block++, block_start=block_end, bh = bh->b_this_page) {
        block_end = block_start + blocksize;
        if (block_end <= from || block_start >= to) {
            if (PageUptodate(page)) {
                if (!buffer_uptodate(bh))
                    set_buffer_uptodate(bh);
            }
            continue;
        }
        if (buffer_new(bh))
            clear_buffer_new(bh);
        if (!buffer_mapped(bh)) {
            WARN_ON(bh->b_size != blocksize);
            if (get_block) {
                err = get_block(inode, block, bh, 1);
                if (err)
                    break;
            } else {
                iomap_to_bh(inode, block, bh, iomap);
            }

            if (buffer_new(bh)) {
                clean_bdev_bh_alias(bh);
                if (PageUptodate(page)) {
                    clear_buffer_new(bh);
                    set_buffer_uptodate(bh);
                    mark_buffer_dirty(bh);
                    continue;
                }
                if (block_end > to || block_start < from)
                    zero_user_segments(page,
                        to, block_end,
                        block_start, from);
                continue;
            }
        }
        if (PageUptodate(page)) {
            if (!buffer_uptodate(bh))
                set_buffer_uptodate(bh);
            continue; 
        }
        if (!buffer_uptodate(bh) && !buffer_delay(bh) &&
            !buffer_unwritten(bh) &&
             (block_start < from || block_end > to)) {
            ll_rw_block(REQ_OP_READ, 0, 1, &bh);
            *wait_bh++=bh;
        }
    }
    /*
    * If we issued read requests - let them complete.
    */
    while(wait_bh > wait) {
        wait_on_buffer(*--wait_bh);
        if (!buffer_uptodate(*wait_bh))
            err = -EIO;
    }
    if (unlikely(err))
        page_zero_new_buffers(page, from, to);
    return err;
}

__block_write_begin_int関数の最初に引数のチェックがある。
下記のどれかを満たしていない場合、BUG_ONマクロでカーネルパニックさせる。

  • ページにロックがかかっていること
  • 開始オフセットがページサイズより小さいこと
  • 終了オフセットがページサイズより小さいこと
  • 終了オフセットが開始オフセットより大きいこと
バッファキャッシュを準備する

引数をチェックし問題がなければ、create_page_buffers関数でbuffer_headを生成する。

// 1673:
static struct buffer_head *create_page_buffers(struct page *page, struct inode *inode, unsigned int b_state)
{
    BUG_ON(!PageLocked(page));

    if (!page_has_buffers(page))
        create_empty_buffers(page, 1 << READ_ONCE(inode->i_blkbits),
                     b_state);
    return page_buffers(page);
}

すでにバッファキャッシュが確保済みである場合は、page_has_buffersマクロがTrueとなりバッファキャッシュを再度確保せずに終了する。
そうでなければ、create_empty_buffers関数でバッファキャッシュの作成を試みる。

// 1556:
void create_empty_buffers(struct page *page,
            unsigned long blocksize, unsigned long b_state)
{
    struct buffer_head *bh, *head, *tail;

    head = alloc_page_buffers(page, blocksize, true);
    bh = head;
    do {
        bh->b_state |= b_state;
        tail = bh;
        bh = bh->b_this_page;
    } while (bh);
    tail->b_this_page = head;

    spin_lock(&page->mapping->private_lock);
    if (PageUptodate(page) || PageDirty(page)) {
        bh = head;
        do {
            if (PageDirty(page))
                set_buffer_dirty(bh);
            if (PageUptodate(page))
                set_buffer_uptodate(bh);
            bh = bh->b_this_page;
        } while (bh != head);
    }
    attach_page_private(page, head);
    spin_unlock(&page->mapping->private_lock);
}
EXPORT_SYMBOL(create_empty_buffers);

create_empty_buffers関数の処理は大きく分けて下記のとおりである。

  • RAM上にバッファキャッシュを確保する
  • バッファキャッシュ間で循環ループを作成する
  • バッファキャッシュとファイルシステムのデータを同期させる
バッファキャッシュを確保する

バッファキャッシュの確保は、alloc_page_buffers関数で実施する。
ただし、今回は説明を簡略化するためにcgroupの話は省着する。

ブロックサイズが1024, ページサイズが4096の場合、4つのbuffer_headを生成する。

alloc_page_buffers関数によって得られるデータ構造のイメージは下記のとおりである。

f:id:LeavaTail:20210419233548p:plain
バッファキャッシュの確保

alloc_page_buffers関数の定義は下記のとおりである。

// 839:
struct buffer_head *alloc_page_buffers(struct page *page, unsigned long size,
        bool retry)
{
    struct buffer_head *bh, *head;
    gfp_t gfp = GFP_NOFS | __GFP_ACCOUNT;
    long offset;
    struct mem_cgroup *memcg, *old_memcg;

    if (retry)
        gfp |= __GFP_NOFAIL;

    memcg = get_mem_cgroup_from_page(page);
    old_memcg = set_active_memcg(memcg);

    head = NULL;
    offset = PAGE_SIZE;
    while ((offset -= size) >= 0) {
        bh = alloc_buffer_head(gfp);
        if (!bh)
            goto no_grow;

        bh->b_this_page = head;
        bh->b_blocknr = -1;
        head = bh;

        bh->b_size = size;

        /* Link the buffer to its page */
        set_bh_page(bh, page, offset);
    }
out:
    set_active_memcg(old_memcg);
    mem_cgroup_put(memcg);
    return head;
/*
 * In case anything failed, we just free everything we got.
 */
no_grow:
    if (head) {
        do {
            bh = head;
            head = head->b_this_page;
            free_buffer_head(bh);
        } while (head);
    }

    goto out;
}
EXPORT_SYMBOL_GPL(alloc_page_buffers);

alloc_page_buffers関数では、855行目のwhile文がメインとなる。

  1. alloc_buffer_head関数でbuffer_headを取得する
  2. 確保したbhをひとつ前のheadに繋げる
  3. set_bh_page関数で、ページキャッシュとバッファを紐づける

alloc_buffer_head関数は、kmem_cache_zalloc関数をデータを確保する。

// 3334:
struct buffer_head *alloc_buffer_head(gfp_t gfp_flags)
{
    struct buffer_head *ret = kmem_cache_zalloc(bh_cachep, gfp_flags);
    if (ret) {
        INIT_LIST_HEAD(&ret->b_assoc_buffers);
        spin_lock_init(&ret->b_uptodate_lock);
        preempt_disable();
        __this_cpu_inc(bh_accounting.nr);
        recalc_bh_state();
        preempt_enable();
    }
    return ret;
}
EXPORT_SYMBOL(alloc_buffer_head);

また、set_bh_page関数は下記の通りとなっている。

// 1443:
void set_bh_page(struct buffer_head *bh,
        struct page *page, unsigned long offset)
{
    bh->b_page = page;
    BUG_ON(offset >= PAGE_SIZE);
    if (PageHighMem(page))
        /*
        * This catches illegal uses and preserves the offset:
        */
        bh->b_data = (char *)(0 + offset);
    else
        bh->b_data = page_address(page) + offset;
}
EXPORT_SYMBOL(set_bh_page);
循環ループを作成する

alloc_page_buffers関数でbuffer_headのリストを生成した後、各データのb_state変数を設定する。

// 1562:
    bh = head;
    do {
        bh->b_state |= b_state;
        tail = bh;
        bh = bh->b_this_page;
    } while (bh);
    tail->b_this_page = head;

その後、末尾のbuffer_headと先頭のbuffer_headをつなげる。

f:id:LeavaTail:20210419234026p:plain
循環リストの更新

バッファキャッシュとファイルシステムを紐付ける

create_page_buffers関数によってバッファキャッシュを作成した後、取得したバッファキャッシュに書き込み対象ファイルのデータを読み込む。

// 1990:
    head = create_page_buffers(page, inode, 0);
    blocksize = head->b_size;
    bbits = block_size_bits(blocksize);

    block = (sector_t)page->index << (PAGE_SHIFT - bbits);

    for(bh = head, block_start = 0; bh != head || !block_start;
        block++, block_start=block_end, bh = bh->b_this_page) {
        block_end = block_start + blocksize;
        if (block_end <= from || block_start >= to) {
            if (PageUptodate(page)) {
                if (!buffer_uptodate(bh))
                    set_buffer_uptodate(bh);
            }
            continue;
        }
        if (buffer_new(bh))
            clear_buffer_new(bh);
        if (!buffer_mapped(bh)) {
            WARN_ON(bh->b_size != blocksize);
            if (get_block) {
                err = get_block(inode, block, bh, 1);
                if (err)
                    break;
            } else {
                iomap_to_bh(inode, block, bh, iomap);
            }

            if (buffer_new(bh)) {
                clean_bdev_bh_alias(bh);
                if (PageUptodate(page)) {
                    clear_buffer_new(bh);
                    set_buffer_uptodate(bh);
                    mark_buffer_dirty(bh);
                    continue;
                }
                if (block_end > to || block_start < from)
                    zero_user_segments(page,
                        to, block_end,
                        block_start, from);
                continue;
            }
        }
        if (PageUptodate(page)) {
            if (!buffer_uptodate(bh))
                set_buffer_uptodate(bh);
            continue; 
        }
        if (!buffer_uptodate(bh) && !buffer_delay(bh) &&
            !buffer_unwritten(bh) &&
             (block_start < from || block_end > to)) {
            ll_rw_block(REQ_OP_READ, 0, 1, &bh);
            *wait_bh++=bh;
        }
    }
    /*
    * If we issued read requests - let them complete.
    */
    while(wait_bh > wait) {
        wait_on_buffer(*--wait_bh);
        if (!buffer_uptodate(*wait_bh))
            err = -EIO;
    }
    if (unlikely(err))
        page_zero_new_buffers(page, from, to);
    return err;
}

1990行の実行直後ではページディスクリプタは下記のようなフラグが立っている。

フラグ名 説明
PG_locked ページはロック状態
PG_private ページディスクリプタのprivateメンバが使用中であること

create_page_buffers関数の以降の処理は下記の通りとなる。

  1. ページキャッシュにPG_uptodate*2が付与されていた場合、バッファキャッシュにもBH_Uptodateを付与し、次のバッファキャッシュを確認する
  2. バッファキャッシュにBH_New*3が付与されていた場合、フラグをクリアする
  3. バッファキャッシュにBH_mapped*4が付与されていない場合、引数のget_blockハンドラを実行してバッファキャッシュにマッピングする
  4. get_blockハンドラでバッファキャッシュを新規にマッピングした場合、ブロックデバイスのバッファをフラッシュする (参考)
  5. get_blockハンドラでバッファキャッシュを新規にマッピングした場合かつページディスクリプタPG_Uptodateである場合、BH_Newフラグを落としBH_Uptodatemark_buffer_dirty関数を実行して次のバッファキャッシュを確認する。
  6. get_blockハンドラでバッファキャッシュを新規にマッピングした場合で、ページキャッシュのゼロ化が必要な場合、ゼロ化し次のバッファキャッシュを確認する
  7. ページディスクリプタPG_Uptodateであるにもかかわらず、バッファキャッシュがBH_Uptodateでない場合、BH_Uptodateフラグを立てて次のバッファキャッシュを確認する。
  8. バッファキャッシュにBH_UptodateBH_Delay*5かつBH_Unwritten*6の何れも立っていない場合、読み込みのためのIOを発行する。

2045行目を超えると、バッファキャッシュには下記のような値が設定されている。 (今回のケースの限る)

フラグ名 説明
BH_Mapped get_blockハンドラで新しく作成された
BH_New バッファキャッシュはディスクにマッピングされている

各バッファキャッシュの更新を終えた後、読み込みのためのIOを発行していた場合は、2049行目のwait_on_bufferインライン関数でIOの完了を待つ。

write_begin処理内で失敗した場合

これまでの処理で何かしらも問題が発生した場合、block_write_begin関数の返り値に負の値が返る。

// 882:
static int
ext2_write_begin(struct file *file, struct address_space *mapping,
        loff_t pos, unsigned len, unsigned flags,
        struct page **pagep, void **fsdata)
{
    int ret;

    ret = block_write_begin(mapping, pos, len, flags, pagep,
                ext2_get_block);
    if (ret < 0)
        ext2_write_failed(mapping, pos + len);
    return ret;
}

その場合、ext2_write_failed関数を呼び、ページキャッシュの解放とaddress_spaceのロールバックをする。詳細は割愛。

// 59:
static void ext2_write_failed(struct address_space *mapping, loff_t to)
{
    struct inode *inode = mapping->host;

    if (to > inode->i_size) {
        truncate_pagecache(inode, inode->i_size);
        ext2_truncate_blocks(inode, inode->i_size);
    }
}

まとめ

本記事では、ext2ファイルシステムwrite_begin操作(ext2_write_begin)を解説した。
write_begin操作は、write_iter操作でページキャッシュに書き込むための準備をするための操作である。

f:id:LeavaTail:20210417094129p:plain
buffer

参考

*1:find_subpage関数については調査中

*2:バッファのデータが有効であることを示すフラグ

*3:作成後、まだ一度もアクセスされていないことを表すフラグ

*4:バッファキャッシュをディスクにマッピングしていることを示すフラグ

*5:まだ対応付けしていないことを表すフラグ

*6:確保済みだが書き込みされていないことを表すフラグ