LeavaTailの日記

LeavaTailの日記

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

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

関連記事

概要

QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。

本章では、submit_bio関数を確認した。

はじめに

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

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

調査対象 (シーケンス図)

本記事では、writebackカーネルスレッドがsubmit_bio関数を呼び出すところから、blk_mq_submit_bio関数を呼ぶところまでを確認する。

BIOの概要

Linuxでは、ブロックデバイスにIOのやり取りをするために BIO と呼ばれるデータ構造を用いる。
ファイルシステムからファイルへの書き込みをすることで、writepages関数とwrite_inode関数が呼ばれ、次のようなbioが生成される。

(実データの書き込みとinodeの書き込みはそれぞれ別のwriteback kthreadで生成される)

これまでに生成させたBIO

また、bio構造体には、どのような操作するかどうかをbi_opfで管理している。
Linux v5.10では次のような操作群が定義されている。

Operation Value Description
REQ_OP_READ 0 read sectors from the device
REQ_OP_WRITE 1 write sectors to the device
REQ_OP_FLUSH 2 flush the volatile write cache
REQ_OP_DISCARD 3 discard sectors
REQ_OP_SECURE_ERASE 5 securely erase sectors
REQ_OP_WRITE_SAME 7 write the same sector many times
REQ_OP_WRITE_ZEROES 9 write the zero filled sector many times
REQ_OP_ZONE_OPEN 10 Open a zone
REQ_OP_ZONE_CLOSE 11 Close a zone
REQ_OP_ZONE_FINISH 12 Transition a zone to full
REQ_OP_ZONE_APPEND 13 write data at the current zone write pointer
REQ_OP_ZONE_RESET 15 reset a zone write pointer
REQ_OP_ZONE_RESET_ALL 17 reset all the zone present on the device
REQ_OP_DRV_IN 34 Driver private requests
REQ_OP_DRV_OUT 35 Driver private requests

今回のケースでは、ファイルへの追記書き込みのみとなっているので、bi_opfには1 (REQ_OP_WRITE)が設定されている。

submit_bio関数

submit_bio関数は、bi_opfのフラグよりI/Oスケジューラにリクエストとして追加する。
submit_bio関数は下記の通りとなっている。

// 1057:
blk_qc_t submit_bio(struct bio *bio)
{
    if (blkcg_punt_bio_submit(bio))
        return BLK_QC_T_NONE;

    /*
    * If it's a regular read/write or a barrier with data attached,
    * go through the normal accounting stuff before submission.
    */
    if (bio_has_data(bio)) {
        unsigned int count;

        if (unlikely(bio_op(bio) == REQ_OP_WRITE_SAME))
            count = queue_logical_block_size(
                    bio->bi_bdev->bd_disk->queue) >> 9;
        else
            count = bio_sectors(bio);

        if (op_is_write(bio_op(bio))) {
            count_vm_events(PGPGOUT, count);
        } else {
            task_io_account_read(bio->bi_iter.bi_size);
            count_vm_events(PGPGIN, count);
        }
    }

    /*
    * If we're reading data that is part of the userspace workingset, count
    * submission time as memory stall.  When the device is congested, or
    * the submitting cgroup IO-throttled, submission can be a significant
    * part of overall IO time.
    */
    if (unlikely(bio_op(bio) == REQ_OP_READ &&
        bio_flagged(bio, BIO_WORKINGSET))) {
        unsigned long pflags;
        blk_qc_t ret;

        psi_memstall_enter(&pflags);
        ret = submit_bio_noacct(bio);
        psi_memstall_leave(&pflags);

        return ret;
    }

    return submit_bio_noacct(bio);
}

submit_bio関数では、blkcg_punt_bio_submit関数でcgroup処理に加えてsubmit_bio_noacct関数を呼び出す。

本環境では、CONFIG_BLK_CGROUP=nなので、blkcg_punt_bio_submit関数はfalseを返すインライン関数となっている。

  • REQ_OP_WRITE: vm_event_states.event[PGPGOUT]countだけ増加させる。
  • REQ_OP_WRITE_SAME: countを調整したうえで、REQ_OP_WRITEと同様に実施する。
  • REQ_OP_READ: プロセス毎のIO統計情報/proc/<pid>/ioread_bytesの増加、vm_event_states.event[PGPGIN]countだけ増加させる。

bio_has_data関数では、submit予定のbioにデータがあることを確認する。

// 61:
static inline bool bio_has_data(struct bio *bio)
{
    if (bio &&
        bio->bi_iter.bi_size &&
        bio_op(bio) != REQ_OP_DISCARD &&
        bio_op(bio) != REQ_OP_SECURE_ERASE &&
        bio_op(bio) != REQ_OP_WRITE_ZEROES)
        return true;

    return false;
}

基本的には、bioのサイズでデータの有無を確認することができるが、DISCARDSECURE_ERASEといった操作はデータを必要としないのでfalseを返す。

直後に、REQ_OP_WRITE_SAMEとの比較する処理があるが、Linuxでは0埋めにしか使われておらずLinux 5.18で削除されている。

listman.redhat.com

そのため、今回はelse文で実行されるbio_sectorsを確認する。

// 49:
#define bio_sectors(bio)    bvec_iter_sectors((bio)->bi_iter)

bvec_iter_sectorsbio構造体のbi_size (ブロックのサイズ)から 512で割った値(セクタ数)を返す。

// 46:
#define bvec_iter_sectors(iter) ((iter).bi_size >> 9)

また、op_is_write関数でwrite操作であるかどうかを判定し、

// 444:
static inline bool op_is_write(unsigned int op)
{
    return (op & 1);
}

writeの場合はPGPGOUT、readの場合はPGPGINの値を更新する。

writeの操作の場合では、submit_bio_noacc関数を呼び出す。

submit_bio_noacct関数

submit_bio_noacct関数は、__submit_bio_noacct関数のラッパ関数となっている。

submit_bio_noacct関数の定義は次のようになっている。

// 1025:
blk_qc_t submit_bio_noacct(struct bio *bio)
{
    /*
    * We only want one ->submit_bio to be active at a time, else stack
    * usage with stacked devices could be a problem.  Use current->bio_list
    * to collect a list of requests submited by a ->submit_bio method while
    * it is active, and then process them after it returned.
    */
    if (current->bio_list) {
        bio_list_add(&current->bio_list[0], bio);
        return BLK_QC_T_NONE;
    }

    if (!bio->bi_bdev->bd_disk->fops->submit_bio)
        return __submit_bio_noacct_mq(bio);
    return __submit_bio_noacct(bio);
}
  • MD(Multi-Device)/DM(Device Mapper)のようなstacked block deviceの場合は、bio_list_add関数によって現在のプロセスが持つbio_listbioを追加する。
  • block deviceがblk-mq対応の場合、__submit_bio_noacct_mq関数を呼び出す。
  • 上記に該当しない場合、__submit_bio_noacct関数を呼び出す。

今回の環境はシンプルな構成(MDやDMなどを使用しない) であり、blk-mq対応ドライバ(下記のblock_device_operationsを参照)となっている。

// 823:
static const struct block_device_operations mmc_bdops = {
    .open           = mmc_blk_open,
    .release        = mmc_blk_release,
    .getgeo         = mmc_blk_getgeo,
    .owner          = THIS_MODULE,
    .ioctl          = mmc_blk_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl       = mmc_blk_compat_ioctl,
#endif
    .alternative_gpt_sector = mmc_blk_alternative_gpt_sector,
};

__submit_bio_noacct_mq関数

__submit_bio_noacct_mq関数は、ブロックデバイスに対する要求 (request) の作成・依頼する bio_mq_submit_bio関数のラッパ関数となっている。

ここで、それぞれのデータ構造の関係性を図示してみる。

request_queueとbioの関係性

bio構造体とrequest_queue構造体は、1パーティションを管理するgendisk構造体によって紐づいている。

さらに、stacked block deviceの対応として、この処理の間はcurrnet->bio_listに処理中のbioを入れておく。

__submit_bio_noacct_mq関数の定義は次の通りとなっている。

// 1001:
static blk_qc_t __submit_bio_noacct_mq(struct bio *bio)
{
    struct bio_list bio_list[2] = { };
    blk_qc_t ret;

    current->bio_list = bio_list;

    do {
        ret = __submit_bio(bio);
    } while ((bio = bio_list_pop(&bio_list[0])));

    current->bio_list = NULL;
    return ret;
}

__submit_bio_noacct_mq関数は、bio_listにある各bioに対して__submit_bio関数を実行する。

__submit_bio関数

__submit_bio関数は、発行するbioを準備したうえでdiskに対応するsubmit_bio操作を実行する。

// 915:
static blk_qc_t __submit_bio(struct bio *bio)
{
    struct gendisk *disk = bio->bi_bdev->bd_disk;
    blk_qc_t ret = BLK_QC_T_NONE;

    if (unlikely(bio_queue_enter(bio) != 0))
        return BLK_QC_T_NONE;

    if (!submit_bio_checks(bio) || !blk_crypto_bio_prep(&bio))
        goto queue_exit;
    if (disk->fops->submit_bio) {
        ret = disk->fops->submit_bio(bio);
        goto queue_exit;
    }
    return blk_mq_submit_bio(bio);

queue_exit:
    blk_queue_exit(disk->queue);
    return ret;
}

__submit_bio関数は、リクエストをブロックデバイスに投げる前に、次のような処理を実行する。

  • リクエストキューのリファレンスカウントを増やす
  • ブロックデバイスが対応している操作かどうか確認する
  • ブロックレイヤでインライン暗号化を使用する

まず初めに、リファレンスカウントを増やすbio_queue_enter関数について確認する。

リファレンスカウントを増やす

bio_queue_enter関数の定義は次のようになっている。

// 471:
static inline int bio_queue_enter(struct bio *bio)
{
    struct gendisk *disk = bio->bi_bdev->bd_disk;
    struct request_queue *q = disk->queue;

    while (!blk_try_enter_queue(q, false)) {
        if (bio->bi_opf & REQ_NOWAIT) {
            if (test_bit(GD_DEAD, &disk->state))
                goto dead;
            bio_wouldblock_error(bio);
            return -EBUSY;
        }

        /*
        * read pair of barrier in blk_freeze_queue_start(), we need to
        * order reading __PERCPU_REF_DEAD flag of .q_usage_counter and
        * reading .mq_freeze_depth or queue dying flag, otherwise the
        * following wait may never return if the two reads are
        * reordered.
        */
        smp_rmb();
        wait_event(q->mq_freeze_wq,
               (!q->mq_freeze_depth &&
                blk_pm_resume_queue(false, q)) ||
               test_bit(GD_DEAD, &disk->state));
        if (test_bit(GD_DEAD, &disk->state))
            goto dead;
    }

    return 0;
dead:
    bio_io_error(bio);
    return -ENODEV;
}

bio_queue_enter関数では、blk_try_enter_queue関数でキューが利用可能であるまでwaitする。
ただし、REQ_NOWAITの場合や複数回ループしてしまった場合には-ENODEVを返す。

blk_try_entry_queue関数の定義は以下の通りとなっている。

// 415:
static bool blk_try_enter_queue(struct request_queue *q, bool pm)
{
    rcu_read_lock();
    if (!percpu_ref_tryget_live(&q->q_usage_counter))
        goto fail;

    /*
    * The code that increments the pm_only counter must ensure that the
    * counter is globally visible before the queue is unfrozen.
    */
    if (blk_queue_pm_only(q) &&
        (!pm || queue_rpm_status(q) == RPM_SUSPENDED))
        goto fail_put;

    rcu_read_unlock();
    return true;

fail_put:
    percpu_ref_put(&q->q_usage_counter);
fail:
    rcu_read_unlock();
    return false;
}
// 284:
static inline bool percpu_ref_tryget_live(struct percpu_ref *ref)
{
    unsigned long __percpu *percpu_count;
    bool ret = false;

    rcu_read_lock();

    if (__ref_is_percpu(ref, &percpu_count)) {
        this_cpu_inc(*percpu_count);
        ret = true;
    } else if (!(ref->percpu_count_ptr & __PERCPU_REF_DEAD)) {
        ret = atomic_long_inc_not_zero(&ref->data->count);
    }

    rcu_read_unlock();

    return ret;
}

詳細は省くが、blk_try_entry_queue関数は、そのキューが使用可能かどうかを確認し、使用可能であればq_usage_counterをインクリメントする。
q_usage_counter0(使用不可能)である場合、whileループを抜ける。

ブロックデバイスが対応しているかどうか確認する

この時、submit_bio_checks関数は、発行するbioが妥当性を確認する。(例えば、「ストレージデバイスが対応していない操作を実行する」など)

submit_bio_checks関数の定義は次の通りとなっている。

// 797:
static noinline_for_stack bool submit_bio_checks(struct bio *bio)
{
    struct block_device *bdev = bio->bi_bdev;
    struct request_queue *q = bdev->bd_disk->queue;
    blk_status_t status = BLK_STS_IOERR;
    struct blk_plug *plug;

    might_sleep();

    plug = blk_mq_plug(q, bio);
    if (plug && plug->nowait)
        bio->bi_opf |= REQ_NOWAIT;

    /*
    * For a REQ_NOWAIT based request, return -EOPNOTSUPP
    * if queue does not support NOWAIT.
    */
    if ((bio->bi_opf & REQ_NOWAIT) && !blk_queue_nowait(q))
        goto not_supported;

    if (should_fail_bio(bio))
        goto end_io;
    if (unlikely(bio_check_ro(bio)))
        goto end_io;
    if (!bio_flagged(bio, BIO_REMAPPED)) {
        if (unlikely(bio_check_eod(bio)))
            goto end_io;
        if (bdev->bd_partno && unlikely(blk_partition_remap(bio)))
            goto end_io;
    }

    /*
    * Filter flush bio's early so that bio based drivers without flush
    * support don't have to worry about them.
    */
    if (op_is_flush(bio->bi_opf) &&
        !test_bit(QUEUE_FLAG_WC, &q->queue_flags)) {
        bio->bi_opf &= ~(REQ_PREFLUSH | REQ_FUA);
        if (!bio_sectors(bio)) {
            status = BLK_STS_OK;
            goto end_io;
        }
    }

    if (!test_bit(QUEUE_FLAG_POLL, &q->queue_flags))
        bio_clear_hipri(bio);

    switch (bio_op(bio)) {
    case REQ_OP_DISCARD:
        if (!blk_queue_discard(q))
            goto not_supported;
        break;
    case REQ_OP_SECURE_ERASE:
        if (!blk_queue_secure_erase(q))
            goto not_supported;
        break;
    case REQ_OP_WRITE_SAME:
        if (!q->limits.max_write_same_sectors)
            goto not_supported;
        break;
    case REQ_OP_ZONE_APPEND:
        status = blk_check_zone_append(q, bio);
        if (status != BLK_STS_OK)
            goto end_io;
        break;
    case REQ_OP_ZONE_RESET:
    case REQ_OP_ZONE_OPEN:
    case REQ_OP_ZONE_CLOSE:
    case REQ_OP_ZONE_FINISH:
        if (!blk_queue_is_zoned(q))
            goto not_supported;
        break;
    case REQ_OP_ZONE_RESET_ALL:
        if (!blk_queue_is_zoned(q) || !blk_queue_zone_resetall(q))
            goto not_supported;
        break;
    case REQ_OP_WRITE_ZEROES:
        if (!q->limits.max_write_zeroes_sectors)
            goto not_supported;
        break;
    default:
        break;
    }

    /*
    * Various block parts want %current->io_context, so allocate it up
    * front rather than dealing with lots of pain to allocate it only
    * where needed. This may fail and the block layer knows how to live
    * with it.
    */
    if (unlikely(!current->io_context))
        create_task_io_context(current, GFP_ATOMIC, q->node);

    if (blk_throtl_bio(bio)) {
        blkcg_bio_issue_init(bio);
        return false;
    }

    blk_cgroup_bio_start(bio);
    blkcg_bio_issue_init(bio);

    if (!bio_flagged(bio, BIO_TRACE_COMPLETION)) {
        trace_block_bio_queue(bio);
        /* Now that enqueuing has been traced, we need to trace
        * completion as well.
        */
        bio_set_flag(bio, BIO_TRACE_COMPLETION);
    }
    return true;

not_supported:
    status = BLK_STS_NOTSUPP;
end_io:
    bio->bi_status = status;
    bio_endio(bio);
    return false;
}

submit_bio_checks関数では以下のような状態を確認する。

ブロックレイヤのインライン暗号化

ブロックレイヤのインライン暗号化は、Linux v5.8から導入されたカーネルの機能の一つとなっている。

docs.kernel.org

// 176:
config BLK_INLINE_ENCRYPTION
    bool "Enable inline encryption support in block layer"
    help
      Build the blk-crypto subsystem. Enabling this lets the
      block layer handle encryption, so users can take
      advantage of inline encryption hardware if present.

CONFIG_BLK_INLINE_ENCRYPTION=yの場合には、インライン暗号化の準備するために__blk_crypto_bio_prep関数を呼び出す。

// 122:
bool __blk_crypto_bio_prep(struct bio **bio_ptr);
static inline bool blk_crypto_bio_prep(struct bio **bio_ptr)
{
    if (bio_has_crypt_ctx(*bio_ptr))
        return __blk_crypto_bio_prep(bio_ptr);
    return true;
}

今回の場合では、CONFIG_BLK_INLINE_ENCRYPTION=nであるため、__blk_crypto_bio_prep関数は常にfalseを返す。

おわりに

本記事では、ブロックレイヤのsubmit_bio関数を確認した。

変更履歴

  • 2022/09/25: 記事公開

参考

KUnitフレームワークによるLinuxカーネルのテスト実施方法

概要

本記事では、kunit_toolを使用して、既存のKUnitテストをQEMU(armアーキテクチャ)で実行する手順について確認する。

はじめに

KUnitは、Linuxカーネル用のテストフレームワークである。
Linuxカーネル v5.5から導入されたスクリプトtools/testing/kunit.pyを実行することで、テスト実行・結果解析することができる。
このスクリプトを利用することで、User Mode Linux (UML) やQEMUでテストを実行することができる。

KUnitでは、内部ライブラリ (include/linux/list.hなど) といったユーザ空間からテストすることができる。 また、アサーションの作成やsetup/clean-upなどのテストを簡単に書くためのライブラリを提供している。

実験環境

本記事で使用した開発用PC (Host PC)の構成は次の通りとなっている。

環境 概要
CPU AMD Ryzen 3 3300X
RAM DDR4-2666 16GB ×2
Host OS Ubuntu Desktop 22.04
Target kernel v5.15.37
QEMU 6.2.0 (Debian 1:6.2-+dfsg-2ubuntu6)

準備

LinuxのソースツリーがDirtyの場合、次のエラーが発生する。あらかじめmake mrproperを実施しておくこと。

leava@kbuild:~/workspace/linux$ ./tools/testing/kunit/kunit.py run --arch=arm --cross_compile=arm-linux-gnueabihf- --jobs=12 --qemu_config=./tools/testing/kunit/qemu_configs/arm.py 
[18:35:11] Configuring KUnit Kernel ...
Generating .config ...
Populating config with:
$ make ARCH=arm olddefconfig CROSS_COMPILE=arm-linux-gnueabihf- O=.kunit
ERROR:root:make[1]: Entering directory '/home/leava/workspace/linux/.kunit'
***
*** The source tree is not clean, please run 'make ARCH=arm mrproper'
*** in /home/leava/workspace/linux
***
make[1]: *** [/home/leava/workspace/linux/Makefile:570: outputmakefile] Error 1
make[1]: Leaving directory '/home/leava/workspace/linux/.kunit'
make: *** [Makefile:219: __sub-make] Error 2

また、KUnitを実行するためには、次のカーネルコンフィグが必要となる。 

CONFIG_KUNIT=y
CONFIG_MSDOS_FS=y
CONFIG_FAT_KUNIT_TEST=y

KUnitでは、Linuxカーネルのコンフィグファイル.configとは別のファイル.kunit/.kunitconfigでもカーネルコンフィグを管理することができる。

コンフィグファイルの生成

kunit_toolのconfigを指定することでKUnitに必要な生成される。

leava@kbuild:~/workspace/linux$ ./tools/testing/kunit/kunit.py config
[22:41:14] Configuring KUnit Kernel ...
Generating .config ...
Populating config with:
$ make ARCH=um olddefconfig O=.kunit
[22:41:16] Elapsed time: 2.409s

leava@kbuild:~/workspace/linux$ ls -lA .kunit/
total 48
-rw-r--r-- 1 leava root 22976 May  8 22:41 .config
-rw-r--r-- 1 leava root    68 May  8 22:41 .config.old
-rw-r--r-- 1 leava root    39 May  8 22:41 .gitignore
-rw-r--r-- 1 leava root    68 May  8 22:41 .kunitconfig
-rw-r--r-- 1 leava root    73 May  8 22:41 Makefile
drwxr-xr-x 4 leava root  4096 May  8 22:41 include
drwxr-xr-x 4 leava root  4096 May  8 22:41 scripts
lrwxrwxrwx 1 leava root     2 May  8 22:41 source -> ..

leava@kbuild:~/workspace/linux$ cat .kunit/.kunitconfig 
CONFIG_KUNIT=y
CONFIG_KUNIT_EXAMPLE_TEST=y
CONFIG_KUNIT_ALL_TESTS=y

カーネルのビルド

kunit_toolのbuildを指定することでKUnitで実行するカーネルをビルドする。 このとき、--arch--cross_compileオプションを指定することでクロスコンパイルすることができる。

leava@kbuild:~/workspace/linux$ ./tools/testing/kunit/kunit.py build --arch=arm --cross_compile=arm-linux
[23:09:22] Building KUnit Kernel ...
Populating config with:
$ make ARCH=arm olddefconfig CROSS_COMPILE=arm-linux-gnueabihf- O=.kunit
Building with:
$ make ARCH=arm --jobs=12 CROSS_COMPILE=arm-linux-gnueabihf- O=.kunit
[23:09:57] Elapsed time: 35.098s

leava@kbuild:~/workspace/linux$ ls -la .kunit/
total 19400
drwxr-xr-x 19 leava root    4096 May  8 23:09 .
drwxrwxr-x 26 leava leava    4096 May  8 22:41 ..
-rw-r--r--  1 leava root   37237 May  8 23:09 .config
-rw-r--r--  1 leava root   22976 May  8 22:41 .config.old
-rw-r--r--  1 leava root      39 May  8 22:41 .gitignore
-rw-r--r--  1 leava root      68 May  8 22:41 .kunitconfig
-rw-r--r--  1 leava root     633 May  8 23:09 .missing-syscalls.d
-rw-r--r--  1 leava root  448911 May  8 23:09 .tmp_System.map
-rwxr-xr-x  1 leava root 4125012 May  8 23:09 .tmp_vmlinux.kallsyms1
-rw-r--r--  1 leava root  959465 May  8 23:09 .tmp_vmlinux.kallsyms1.S
-rw-r--r--  1 leava root  165852 May  8 23:09 .tmp_vmlinux.kallsyms1.o
-rwxr-xr-x  1 leava root 4256384 May  8 23:09 .tmp_vmlinux.kallsyms2
-rw-r--r--  1 leava root  959465 May  8 23:09 .tmp_vmlinux.kallsyms2.S
-rw-r--r--  1 leava root  165852 May  8 23:09 .tmp_vmlinux.kallsyms2.o
-rw-r--r--  1 leava root       2 May  8 23:09 .version
-rw-r--r--  1 leava root    1172 May  8 23:09 .vmlinux.cmd
-rw-r--r--  1 leava root      73 May  8 23:09 Makefile
-rw-r--r--  1 leava root  448911 May  8 23:09 System.map
drwxr-xr-x  5 leava root    4096 May  8 23:09 arch
drwxr-xr-x  3 leava root    4096 May  8 23:09 block
drwxr-xr-x  2 leava root    4096 May  8 23:09 certs
drwxr-xr-x  2 leava root    4096 May  8 23:09 crypto
drwxr-xr-x 44 leava root    4096 May  8 23:09 drivers
drwxr-xr-x 11 leava root    4096 May  8 23:09 fs
drwxr-xr-x  4 leava root    4096 May  8 22:41 include
drwxr-xr-x  2 leava root    4096 May  8 23:09 init
drwxr-xr-x  2 leava root    4096 May  8 23:09 ipc
drwxr-xr-x 12 leava root    4096 May  8 23:09 kernel
drwxr-xr-x  5 leava root   12288 May  8 23:09 lib
drwxr-xr-x  2 leava root    4096 May  8 23:09 mm
-rw-r--r--  1 leava root    1378 May  8 23:09 modules.builtin
-rw-r--r--  1 leava root    8994 May  8 23:09 modules.builtin.modinfo
drwxr-xr-x  6 leava root    4096 May  8 23:09 scripts
drwxr-xr-x  2 leava root    4096 May  8 23:09 security
drwxr-xr-x  2 leava root    4096 May  8 23:09 sound
lrwxrwxrwx  1 leava root       2 May  8 23:09 source -> ..
drwxr-xr-x  2 leava root    4096 May  8 23:09 usr
drwxr-xr-x  3 leava root    4096 May  8 23:09 virt
-rwxr-xr-x  1 leava root 4256384 May  8 23:09 vmlinux
-rw-r--r--  1 leava root 4678424 May  8 23:09 vmlinux.o
-rw-r--r--  1 leava root       0 May  8 23:09 vmlinux.symvers    

テスト実行

kunit_toolのrunを指定することでKUnitで実行するカーネルをビルドする。 このとき、--arch--cross_compileオプションを指定することで起動するカーネルアーキテクチャを指定することができる。

jobsオプションを指定することで、makeコマンド実行を並列処理させることができる。

また、デフォルトはUMLでの起動となっているが、QEMUで起動する場合には--qemu_configオプションを指定する。 パラメータとして指定するファイルには、QEMUで実行する場合に必要となるパラメータ (qemuのパラメータや追加のカーネルコンフィグなど)を指定することができる。

leava@kbuild:~/workspace/linux$ ./tools/testing/kunit/kunit.py run \        
                                --arch=arm \
                                --cross_compile=arm-linux-gnueabihf- \
                                --jobs=12 \
                                --timeout=300 \
                                --qemu_config=./tools/testing/kunit/qemu_configs/arm.py

上記のコマンドを実行すると、次のような結果が得られる。

テスト実施時の様子

おわりに

本稿では、既存のKUnitのテストをQEMU (armアーキテクチャ) で実行する手順を確認した。

また、KUnitは公式ドキュメントが充実しており、ドキュメントを一読するだけでも簡単なテストを作成することができると思われる。 そのため、興味のある人はカーネルドキュメントを参考に一度KUnitを利用してみることをお勧めする。

変更履歴

  • 2022/5/9: 記事公開

参考文献

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に変更

kcovによるカーネルのソースコードカバレッジの分析

概要

kcovは、カーネルソースコードカバレッジを測定するツールである。
本記事では、kcovに関係する次の二つの内容が含まれている。

はじめに

ソフトウェアのコードカバレッジを計測することは、プログラムの品質を確認するための要素の一つになる。 Linuxカーネルのような規模が大きいプログラムとなると、簡単に定量的な結果を残せるコードカバレッジは有用であると考えられる。

kcovは、Linux v4.6から導入されたファジングのカーネルコードカバレッジ機能である。 同様のツールとしてgcovもあるが、カーネルソースコードカバレッジを計測する場合には「コードブロックが膨大である」「他プロセス(スレッド)が常に動作している」という点で、kcovが優位性がある。

そこで、本記事ではkcovのドキュメントに沿ってソースコードカバレッジを計測してみる。 また、内部でkcovを利用するファジングツールsyzkallerの実行結果も確認してみる。

実験環境

本記事で使用した開発用PC (Host PC)の構成は次の通りとなっている。

環境 概要
CPU AMD Ryzen 3 3300X
RAM DDR4-2666 16GB ×2
OS Ubuntu Desktop 20.04.04
kernel 5.13.0-40-generic
QEMU 4.2.1 (Debian 1:4.2-3ubuntu6.21)

使用方法

kcovのインターフェースがdebugfsを経由したIOCTLによるものとなっている。

次のカーネルコンフィグにより、debugfs直下にkcovがインターフェースとして生成される。

CONFIG_KCOV=y

kcovは、次のIOCTLの操作を提供している。

API group value len 概要
KCOV_INIT_TRACE 'c' 1 unsigned long kcovの初期セットアップ
KCOV_ENABLE 'c' 100 現在のプロセスから発行されたsyscallに対するカバレッジの取得を開始する
KCOV_DISABLE 'c' 101 カバレッジの所得を終了する
KCOV_REMOTE_ENABLE 'c' 102 カーネルコードの任意の部分に対するカバレッジを取得を開始する

kcovによるコードカバレッジの確認

Host OS(x86_64)上に、QEMUによるGuest OS(armhf)にkcovを実施する環境を構築する。

kcovの実施に利用した環境

このとき、Guest OSは次のコマンドにより生成した。

qemu-system-arm 
    -M vexpress-a9 \
    -smp 1 \
    -m 1024 \
    -kernel ${KIMAGE_DIR}/zImage \
    -dtb ${KIMAGE_DIR}/dts/vexpress-v2p-ca9.dtb \
    -drive file=${SD_IMAGE},if=sd,format=raw \
    -append "rootwait root=/dev/nfs console=ttyAMA0 ip=on rw" \
    -net nic,model=lan9118 -net user \
    -nographic

また、NFSroot先は Ubuntu Base 20.04.4 LTS (armhf) を基に、Host PC上に格納してあり、binutilsパッケージを追加している。

測定プログラムは公式ドキュメントにあるものをそのまま利用する。

www.kernel.org

測定プログラムcoverage.cはread(-1, NULL, 0);(46行目)を実行したときのカバレッジを測定する。

// 1:
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/types.h>

#define KCOV_INIT_TRACE                     _IOR('c', 1, unsigned long)
#define KCOV_ENABLE                 _IO('c', 100)
#define KCOV_DISABLE                        _IO('c', 101)
#define COVER_SIZE                  (64<<10)

#define KCOV_TRACE_PC  0
#define KCOV_TRACE_CMP 1

int main(int argc, char **argv)
{
    int fd;
    unsigned long *cover, n, i;

    /* A single fd descriptor allows coverage collection on a single
     * thread.
     */
    fd = open("/sys/kernel/debug/kcov", O_RDWR);
    if (fd == -1)
            perror("open"), exit(1);
    /* Setup trace mode and trace size. */
    if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE))
            perror("ioctl"), exit(1);
    /* Mmap buffer shared between kernel- and user-space. */
    cover = (unsigned long*)mmap(NULL, COVER_SIZE * sizeof(unsigned long),
                                 PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if ((void*)cover == MAP_FAILED)
            perror("mmap"), exit(1);
    /* Enable coverage collection on the current thread. */
    if (ioctl(fd, KCOV_ENABLE, KCOV_TRACE_PC))
            perror("ioctl"), exit(1);
    /* Reset coverage from the tail of the ioctl() call. */
    __atomic_store_n(&cover[0], 0, __ATOMIC_RELAXED);
    /* That's the target syscal call. */
    read(-1, NULL, 0);
    /* Read number of PCs collected. */
    n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED);
    for (i = 0; i < n; i++)
            printf("0x%lx\n", cover[i + 1]);
    /* Disable coverage collection for the current thread. After this call
     * coverage can be enabled for a different thread.
     */
    if (ioctl(fd, KCOV_DISABLE, 0))
            perror("ioctl"), exit(1);
    /* Free resources. */
    if (munmap(cover, COVER_SIZE * sizeof(unsigned long)))
            perror("munmap"), exit(1);
    if (close(fd))
            perror("close"), exit(1);
    return 0;
}

その後、測定プログラムをクロスコンパイルしたバイナリcoverageと、Guest OSのカーネルバイナリvmlinuxをGuest OSのrootfsにコピーする。

測定プログラムcoverageではアドレスで出力されるため、addr2lineにパイプすることで解読できるような形で出力させる。

# ./coverage | addr2line -e vmlinux
/home/leava/linux/fs/read_write.c:644
/home/leava/linux/./include/linux/file.h:75
/home/leava/linux/fs/file.c:915
/home/leava/linux/fs/file.c:901
/home/leava/linux/./include/linux/fdtable.h:85
/home/leava/linux/fs/file.c:912
/home/leava/linux/fs/file.c:936
/home/leava/linux/fs/read_write.c:640

出力結果のファイル名と行数は、測定対象のシステムコールを実行したときに実行された行番号となる。

ただし、新しめのLInuxカーネルでは、デフォルトでGCCの最適化オプション(-O2)が指定されているため、コードカバレッジの状態と実際のソースコードが異なることがある。

syzkallerによるコードカバレッジの確認

kcovはあくまでカーネルの一機能であり、ユーザが使うツールとしては利便性の面ではあまり優れていない。 そのため、kcov単体で使うのではなく、kcovを内部で利用しているツールを使うことが好ましい。

syzkallerはGoogleが開発したカーネルのファジングツールの一つとなっている。

github.com

syzkaller自体はGo言語で記述されたプログラムである。 内部でkcovでなどカーネルの機能を利用しているため、ファジングした結果のコードカバレッジも取得することができる。

Host OS(x86_64)上で、QEMUによるGuest OS(arm64)にksyzkallerを実施する。

syzkaller実行環境

公式ドキュメントにあるSetup: Linux host, QEMU vm, arm64 kernelに沿って実施する。

項目 概要 補足
rootfs buildroot 2022.02.1 qemu_aarch64_virt_defconfig をベースとする
kernel Linux v5.15 defconfigをベースとする
toolchain AArch64 FNU/Linux 2021.07 cross compiler
syzkaller dc9e52595336dbe32f9a20f5da9f09cb8172cd21

Go実行環境のインストール

公式サイトの手順通りにHost OSにGo実行環境をインストールする。

go.dev

leava@ubuntu:/work/$ wget https://go.dev/dl/go1.18.1.linux-amd64.tar.gz
leava@ubuntu:/work/$ tar -C /usr/local -xzf go1.18.1.linux-amd64.tar.gz

Goの実行に必要なディレクトリにPATHを通す

leava@ubuntu:/work/$ export PATH=$PATH:/usr/local/go/bin

Goの実行環境が正しくインストールされているか確認する

leava@ubuntu:/work/$ go version
leava@ubuntu:/work/$ go version go1.18.1 linux/amd64

buildrootによるrootfsの生成

buildrootのソースコードを取得する

leava@ubuntu:/work/$ wget https://buildroot.uclibc.org/downloads/buildroot-2022.02.1.tar.gz
leava@ubuntu:/work/$ tar xf buildroot-2022.02.1.tar.gz
leava@ubuntu:/work/$ cd buildroot-2022.02.1

デフォルトのコンフィグを生成する

leava@ubuntu:/work/buildroot-2022.02.1/$ make qemu_aarch64_virt_defconfig

syzkallerに必要なコンフィグを修正する

Target options
    Target Architecture - Aarch64 (little endian)
Toolchain
    Toolchain type (External toolchain)
System Configuration
    [*] Enable root login with password
    (password) Root password
    [*] Run a getty (login prompt) after boot
        (ttyAMA0) TTY port
Target packages
    [*]   Show packages that are also provided by busybox
    Networking applications
        [*] dhcpcd
        [*] iproute2
        [*] openssh
Filesystem images
    [*] ext2/3/4 root filesystem
        ext2/3/4 variant (ext3)
    (60M) exact size
    [*] tar the root filesystem

buildrootでビルドする

leava@ubuntu:/work/buildroot-2022.02.1/$ make

成果物を確認する

leava@ubuntu:/work/buildroot-2022.02.1/$ ls -l output/images
total 50668
-rw-r--r-- 1 blue root 10883584 May  3 23:41 Image
-rw-r--r-- 1 blue root 62914560 May  4 09:12 rootfs.ext2
lrwxrwxrwx 1 blue root       11 May  3 23:41 rootfs.ext3 -> rootfs.ext2
-rw-r--r-- 1 blue root 16640000 May  3 23:41 rootfs.tar
-rwxr-xr-x 1 blue root      486 May  4 09:12 start-qemu.sh

Linux Kernelのビルド

Linuxソースコードを取得する

leava@ubuntu:/work/$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.15.tar.gz
leava@ubuntu:/work/$ tar xf linux-5.15.tar.gz
leava@ubuntu:/work/$ cd linux-5.15

デフォルトのコンフィグを生成する

leava@ubuntu:/work/linux-5.15/$ ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make defconfig

syzkallerに必要なコンフィグを修正する

CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_REDUCED=n
CONFIG_KCOV=y
CONFIG_KCOV_INSTRUMENT_ALL=y
CONFIG_KASAN=y
CONFIG_CMDLINE="console=ttyAMA0"
CONFIG_FAULT_INJECTION=y

Linuxをビルドする

leava@ubuntu:/work/linux-5.15/$ ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-make -j$(nproc)

成果物を確認する

leava@ubuntu:/work/linux-5.15/$ ls -la arch/arm64/boot/Image
-rw-r--r-- 1 blue root 82354688 May  4 15:38 arch/arm64/boot/Image

rootfsのカスタマイズ

QEMUで作成したカーネルを起動させる (root:passwordでログインが可能)

leava@ubuntu:/work/$ qemu-system-aarch64 \
  -machine virt \
  -cpu cortex-a57 \
  -nographic -smp 1 \
  -hda /path/to/rootfs.ext3 \
  -kernel /path/to/arch/arm64/boot/Image \
  -append "console=ttyAMA0 root=/dev/vda oops=panic panic_on_warn=1 panic=-1 ftrace_dump_on_oops=orig_cpu debug earlyprintk=serial slub_debug=UZ" \
  -m 2048 \
  -net user,hostfwd=tcp::10023-:22 -net nic    

initスクリプトに次の処理を追加する (/etc/init.d/S50sshd)

ifconfig eth0 up
dhcpcd
mount -t debugfs none /sys/kernel/debug
chmod 777 /sys/kernel/debug/kcov

sshdに次の設定を追加する (/etc/ssh/sshd_config)

PermitRootLogin yes
PubkeyAuthentication yes
PasswordAuthentication yes    

sshdの設定更新のために、Guest OSを再起動する。

Guest OSのために、Host OS上で公開鍵ペアを作成し、Guest OSに送る。

leava@ubuntu:/work/$ ssh-keygen
leava@ubuntu:/work/$ ssh-copy-id -i id_rsa.pub root@localhost -p 10023

Host OSからGuest OSに対して、sshできることを確認できたらGuest OSの電源を落とす。

syzkallerのビルド

ツールチェインの取得

leava@ubuntu:/work/$ wget https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.xz
leava@ubuntu:/work/$ tar xf gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.xz

syzkallerのソースコードを取得する

leava@ubuntu:/work/$ git clone https://github.com/google/syzkaller.git
leava@ubuntu:/work/syzkaller/$ git clone https://github.com/google/syzkaller.git

syzkallerをビルドする

leava@ubuntu:/work/syzkaller/$ CC=/work/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-gnu-g++
leava@ubuntu:/work/syzkaller/$ make TARGETARCH=arm64

syzkallerを実行する

syzkallerの設定ファイル (my.cfg) を用意する。

// 1:
{
        "name": "QEMU-aarch64",
        "target": "linux/arm64",
        "http": ":56700",
        "workdir": "/work/syzkaller/workdir",
        "kernel_obj": "/work/linux-5.15",
        "image": "/work/buildroot-2022.02.1/output/images/rootfs.ext3",
        "sshkey": "~/.ssh/id_rsa",
        "syzkaller": "/work/syzkaller",
        "procs": 1,
        "type": "qemu",
        "disable_syscalls": ["keyctl", "add_key", "request_key"],
        "suppressions": ["some known bug"],
        "vm": {
                "count": 1,
                "qemu": "qemu-system-aarch64",
                "cmdline": "console=ttyAMA0 root=/dev/vda",
                "kernel": "/work/linux-5.15/arch/arm64/boot/Image",
                "cpu": 2,
                "mem": 2048
        }
}

用意したコンフィグファイルを入力として、syz-managerを実行することで、ファジングテストが開始する。

leava@ubuntu:/work/syzkaller/$ sudo bin/syz-manager -config=my.cfg

テストが開始してからしばらく待った後に、ブラウザから http://127.0.0.1:56700/ にアクセスすると、次のようなWebページが表示される。

syzkallerのトップ画面

coverageにある数字をクリックすると次のようなWebページが表示される。

syzkallerによるcoverageの確認

github.com

おわりに

本記事では、kcovによるソースコードカバレッジの取得方法について確認した。
また、ファジングツールの一つsyzkallerで、ファジングテストの結果におけるカバレッジの取得方法についても確認した。

変更履歴

  • 2022/5/5: 記事公開

参考文献

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

関連記事

概要

QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。

本章では、ext2_writepages関数を確認した。

はじめに

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

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

調査対象 (シーケンス図)

本記事では、writebackカーネルスレッドがwritepagesを呼び出すところから、submit_bio関数を呼ぶところまでを確認する。

writepagesの概要

kerner thread のライトバック処理によって、Dirtyのinodeに対してwritepages関数とwrite_inode関数を呼び出す。

基本的には、writepages関数はファイルの実データの書き込み、write_inode関数はファイルのメタデータの書き込みをする。
ext2ファイルシステムの場合では、ext2_writepages関数とext2_write_inode関数が定義されている。

writepages関数の開始時のイメージ

ext2_writepages関数の定義は次の通りとなっている。

// 950:
static int
ext2_writepages(struct address_space *mapping, struct writeback_control *wbc)
{
    return mpage_writepages(mapping, wbc, ext2_get_block);
}

ext2_writepage関数は、mpage_writepage関数を呼び出す。(関数の引数にext2固有の関数ポインタを渡す)

mpage_writepages関数

// 693:
int
mpage_writepages(struct address_space *mapping,
        struct writeback_control *wbc, get_block_t get_block)
{
    struct blk_plug plug;
    int ret;

    blk_start_plug(&plug);

    if (!get_block)
        ret = generic_writepages(mapping, wbc);
    else {
        struct mpage_data mpd = {
            .bio = NULL,
            .last_block_in_bio = 0,
            .get_block = get_block,
            .use_writepage = 1,
        };

        ret = write_cache_pages(mapping, wbc, __mpage_writepage, &mpd);
        if (mpd.bio) {
            int op_flags = (wbc->sync_mode == WB_SYNC_ALL ?
                  REQ_SYNC : 0);
            mpage_bio_submit(REQ_OP_WRITE, op_flags, mpd.bio);
        }
    }
    blk_finish_plug(&plug);
    return ret;
}

ここで、writepages関数では、IOリクエストを管理するためのblk_plug構造体を取り扱う。
blk_start_plug関数で初期化 (または保留されているものを戻す) 、blk_finidh_plug関数で完了を表す。

blk_plug構造体のイメージ

ext2ファイルシステムでは、独自のget_block関数を渡すため、write_cache_pages関数とmpage_bio_submit関数が主な処理となっている。
write_cache_pages関数は、ext2_writepages関数から渡された関数に加えて、単体ページ書き込み関数__mpage_writepage関数とページ書き込み用メタデータmpage_data構造体を渡す。

ここで、今回の場合におけるwriteback_control構造体の値を再掲する。

変数名
nr_pages ノード内にあるDirtyページ数
sync_mode WB_SYNC_NONE
for_kupdate 1
for_background 0
range_cyclic 1

write_cache_pages関数の定義は次の通りとなっている。

write_cache_pages関数

// 2177:
int write_cache_pages(struct address_space *mapping,
              struct writeback_control *wbc, writepage_t writepage,
              void *data)
{
    int ret = 0;
    int done = 0;
    int error;
    struct pagevec pvec;
    int nr_pages;
    pgoff_t index;
    pgoff_t end;        /* Inclusive */
    pgoff_t done_index;
    int range_whole = 0;
    xa_mark_t tag;

    pagevec_init(&pvec);
    if (wbc->range_cyclic) {
        index = mapping->writeback_index; /* prev offset */
        end = -1;
    } else {
        index = wbc->range_start >> PAGE_SHIFT;
        end = wbc->range_end >> PAGE_SHIFT;
        if (wbc->range_start == 0 && wbc->range_end == LLONG_MAX)
            range_whole = 1;
    }
    if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages) {
        tag_pages_for_writeback(mapping, index, end);
        tag = PAGECACHE_TAG_TOWRITE;
    } else {
        tag = PAGECACHE_TAG_DIRTY;
    }
    done_index = index;
    while (!done && (index <= end)) {
        int i;

        nr_pages = pagevec_lookup_range_tag(&pvec, mapping, &index, end,
                tag);
        if (nr_pages == 0)
            break;

        for (i = 0; i < nr_pages; i++) {
            struct page *page = pvec.pages[i];

            done_index = page->index;

            lock_page(page);

            /*
            * Page truncated or invalidated. We can freely skip it
            * then, even for data integrity operations: the page
            * has disappeared concurrently, so there could be no
            * real expectation of this data integrity operation
            * even if there is now a new, dirty page at the same
            * pagecache address.
            */
            if (unlikely(page->mapping != mapping)) {
continue_unlock:
                unlock_page(page);
                continue;
            }

            if (!PageDirty(page)) {
                /* someone wrote it for us */
                goto continue_unlock;
            }

            if (PageWriteback(page)) {
                if (wbc->sync_mode != WB_SYNC_NONE)
                    wait_on_page_writeback(page);
                else
                    goto continue_unlock;
            }

            BUG_ON(PageWriteback(page));
            if (!clear_page_dirty_for_io(page))
                goto continue_unlock;

            trace_wbc_writepage(wbc, inode_to_bdi(mapping->host));
            error = (*writepage)(page, wbc, data);
            if (unlikely(error)) {
                /*
                * Handle errors according to the type of
                * writeback. There's no need to continue for
                * background writeback. Just push done_index
                * past this page so media errors won't choke
                * writeout for the entire file. For integrity
                * writeback, we must process the entire dirty
                * set regardless of errors because the fs may
                * still have state to clear for each page. In
                * that case we continue processing and return
                * the first error.
                */
                if (error == AOP_WRITEPAGE_ACTIVATE) {
                    unlock_page(page);
                    error = 0;
                } else if (wbc->sync_mode != WB_SYNC_ALL) {
                    ret = error;
                    done_index = page->index + 1;
                    done = 1;
                    break;
                }
                if (!ret)
                    ret = error;
            }

            /*
            * We stop writing back only if we are not doing
            * integrity sync. In case of integrity sync we have to
            * keep going until we have written all the pages
            * we tagged for writeback prior to entering this loop.
            */
            if (--wbc->nr_to_write <= 0 &&
                wbc->sync_mode == WB_SYNC_NONE) {
                done = 1;
                break;
            }
        }
        pagevec_release(&pvec);
        cond_resched();
    }

    /*
    * If we hit the last page and there is more work to be done: wrap
    * back the index back to the start of the file for the next
    * time we are called.
    */
    if (wbc->range_cyclic && !done)
        done_index = 0;
    if (wbc->range_cyclic || (range_whole && wbc->nr_to_write > 0))
        mapping->writeback_index = done_index;

    return ret;
}

write_cache_pages関数では、それぞれのページキャッシュに対して、次のような処理を実施していく。

  1. ページキャッシュをロックを取得する
  2. 引数のwritepage (__mpage_writepage関数)を呼び出す
  3. ページキャッシュのロックを解放する

そのために、pagevec_lookup_range_tag関数からPAGECACHE_TAG_DIRTY(tag)を検索していき、pvec(pagevec構造体)で管理する。
検索して得られたページがDirtyフラグが立っていない、Writebackフラグが立っている場合には、continu_unlockラベル (ロック解放して次のページを移る) にジャンプする。

__mpage_writepage関数

__mpage_writepage関数の定義は次の通りとなっている。

// 478:
static int __mpage_writepage(struct page *page, struct writeback_control *wbc,
              void *data)
{
    struct mpage_data *mpd = data;
    struct bio *bio = mpd->bio;
    struct address_space *mapping = page->mapping;
    struct inode *inode = page->mapping->host;
    const unsigned blkbits = inode->i_blkbits;
    unsigned long end_index;
    const unsigned blocks_per_page = PAGE_SIZE >> blkbits;
    sector_t last_block;
    sector_t block_in_file;
    sector_t blocks[MAX_BUF_PER_PAGE];
    unsigned page_block;
    unsigned first_unmapped = blocks_per_page;
    struct block_device *bdev = NULL;
    int boundary = 0;
    sector_t boundary_block = 0;
    struct block_device *boundary_bdev = NULL;
    int length;
    struct buffer_head map_bh;
    loff_t i_size = i_size_read(inode);
    int ret = 0;
    int op_flags = wbc_to_write_flags(wbc);

    if (page_has_buffers(page)) {
        struct buffer_head *head = page_buffers(page);
        struct buffer_head *bh = head;

        /* If they're all mapped and dirty, do it */
        page_block = 0;
        do {
            BUG_ON(buffer_locked(bh));
            if (!buffer_mapped(bh)) {
                /*
                * unmapped dirty buffers are created by
                * __set_page_dirty_buffers -> mmapped data
                */
                if (buffer_dirty(bh))
                    goto confused;
                if (first_unmapped == blocks_per_page)
                    first_unmapped = page_block;
                continue;
            }

            if (first_unmapped != blocks_per_page)
                goto confused;    /* hole -> non-hole */

            if (!buffer_dirty(bh) || !buffer_uptodate(bh))
                goto confused;
            if (page_block) {
                if (bh->b_blocknr != blocks[page_block-1] + 1)
                    goto confused;
            }
            blocks[page_block++] = bh->b_blocknr;
            boundary = buffer_boundary(bh);
            if (boundary) {
                boundary_block = bh->b_blocknr;
                boundary_bdev = bh->b_bdev;
            }
            bdev = bh->b_bdev;
        } while ((bh = bh->b_this_page) != head);

        if (first_unmapped)
            goto page_is_mapped;

        /*
        * Page has buffers, but they are all unmapped. The page was
        * created by pagein or read over a hole which was handled by
        * block_read_full_page().  If this address_space is also
        * using mpage_readahead then this can rarely happen.
        */
        goto confused;
    }

    /*
    * The page has no buffers: map it to disk
    */
    BUG_ON(!PageUptodate(page));
    block_in_file = (sector_t)page->index << (PAGE_SHIFT - blkbits);
    last_block = (i_size - 1) >> blkbits;
    map_bh.b_page = page;
    for (page_block = 0; page_block < blocks_per_page; ) {

        map_bh.b_state = 0;
        map_bh.b_size = 1 << blkbits;
        if (mpd->get_block(inode, block_in_file, &map_bh, 1))
            goto confused;
        if (buffer_new(&map_bh))
            clean_bdev_bh_alias(&map_bh);
        if (buffer_boundary(&map_bh)) {
            boundary_block = map_bh.b_blocknr;
            boundary_bdev = map_bh.b_bdev;
        }
        if (page_block) {
            if (map_bh.b_blocknr != blocks[page_block-1] + 1)
                goto confused;
        }
        blocks[page_block++] = map_bh.b_blocknr;
        boundary = buffer_boundary(&map_bh);
        bdev = map_bh.b_bdev;
        if (block_in_file == last_block)
            break;
        block_in_file++;
    }
    BUG_ON(page_block == 0);

    first_unmapped = page_block;

page_is_mapped:
    end_index = i_size >> PAGE_SHIFT;
    if (page->index >= end_index) {
        /*
        * The page straddles i_size.  It must be zeroed out on each
        * and every writepage invocation because it may be mmapped.
        * "A file is mapped in multiples of the page size.  For a file
        * that is not a multiple of the page size, the remaining memory
        * is zeroed when mapped, and writes to that region are not
        * written out to the file."
        */
        unsigned offset = i_size & (PAGE_SIZE - 1);

        if (page->index > end_index || !offset)
            goto confused;
        zero_user_segment(page, offset, PAGE_SIZE);
    }

    /*
    * This page will go to BIO.  Do we need to send this BIO off first?
    */
    if (bio && mpd->last_block_in_bio != blocks[0] - 1)
        bio = mpage_bio_submit(REQ_OP_WRITE, op_flags, bio);

alloc_new:
    if (bio == NULL) {
        if (first_unmapped == blocks_per_page) {
            if (!bdev_write_page(bdev, blocks[0] << (blkbits - 9),
                                page, wbc))
                goto out;
        }
        bio = mpage_alloc(bdev, blocks[0] << (blkbits - 9),
                BIO_MAX_VECS, GFP_NOFS|__GFP_HIGH);
        if (bio == NULL)
            goto confused;

        wbc_init_bio(wbc, bio);
        bio->bi_write_hint = inode->i_write_hint;
    }

    /*
    * Must try to add the page before marking the buffer clean or
    * the confused fail path above (OOM) will be very confused when
    * it finds all bh marked clean (i.e. it will not write anything)
    */
    wbc_account_cgroup_owner(wbc, page, PAGE_SIZE);
    length = first_unmapped << blkbits;
    if (bio_add_page(bio, page, length, 0) < length) {
        bio = mpage_bio_submit(REQ_OP_WRITE, op_flags, bio);
        goto alloc_new;
    }

    clean_buffers(page, first_unmapped);

    BUG_ON(PageWriteback(page));
    set_page_writeback(page);
    unlock_page(page);
    if (boundary || (first_unmapped != blocks_per_page)) {
        bio = mpage_bio_submit(REQ_OP_WRITE, op_flags, bio);
        if (boundary_block) {
            write_boundary_block(boundary_bdev,
                    boundary_block, 1 << blkbits);
        }
    } else {
        mpd->last_block_in_bio = blocks[blocks_per_page - 1];
    }
    goto out;

confused:
    if (bio)
        bio = mpage_bio_submit(REQ_OP_WRITE, op_flags, bio);

    if (mpd->use_writepage) {
        ret = mapping->a_ops->writepage(page, wbc);
    } else {
        ret = -EAGAIN;
        goto out;
    }
    /*
    * The caller has a ref on the inode, so *mapping is stable
    */
    mapping_set_error(mapping, ret);
out:
    mpd->bio = bio;
    return ret;
}

__mpage_writepage関数は、該当ページが連続しているかどうかを考慮しつつ、bioを準備する関数となっている。

連続している場合 (かつ、ページキャッシュ内のデータがすべて書き込み対象である) には、それに対応するbioを生成する。

書き込み連続している場合の__mpage_writepage関数

一方で、連続していない場合には、連続しているブロックのみのbioを発行し、残りのブロックのためのbioを生成する。

書き込み非連続している場合の__mpage_writepage関数

これらを踏まえて、ソースコード上の動作も確認する。

__mpage_writepage

__mpage_writepage関数は、それぞれの状態に応じてラベルにジャンプしたり条件分岐することで多数の状態に対応している。

page_is_mapped

ページキャッシュが記憶装置のブロックとマッピングされている場合の処理となる。

page_is_mappedラベルの概要図

  • ページキャッシュに載っている書き込み対象でない範囲のゼロ埋めを実施する。
  • 非連続となっているブロックのbioを発行する

ページキャッシュをゼロ埋めをする処理はzero_user_segment関数が担う。

// 222:
static inline void zero_user_segment(struct page *page,
    unsigned start, unsigned end)
{
    zero_user_segments(page, start, end, 0, 0);
}

zero_user_segment関数は、zero_user_segments関数のラッパ関数となっている。

// 201:
static inline void zero_user_segments(struct page *page,
        unsigned start1, unsigned end1,
        unsigned start2, unsigned end2)
{
    void *kaddr = kmap_atomic(page);
    unsigned int i;

    BUG_ON(end1 > page_size(page) || end2 > page_size(page));

    if (end1 > start1)
        memset(kaddr + start1, 0, end1 - start1);

    if (end2 > start2)
        memset(kaddr + start2, 0, end2 - start2);

    kunmap_atomic(kaddr);
    for (i = 0; i < compound_nr(page); i++)
        flush_dcache_page(page + i);
}

kunmap_ztomic関数やflush_dcache_page関数は、以前の記事 (write_iter) と同様である。

leavatail.hatenablog.com

alloc_new

mmcドライバではできないので詳細は割愛するが、bdev_write_page関数によってページキャッシュによる書き込みができるドライバであれば、実施して終了する。

// 357:
int bdev_write_page(struct block_device *bdev, sector_t sector,
            struct page *page, struct writeback_control *wbc)
{
    int result;
    const struct block_device_operations *ops = bdev->bd_disk->fops;

    if (!ops->rw_page || bdev_get_integrity(bdev))
        return -EOPNOTSUPP;
    result = blk_queue_enter(bdev->bd_disk->queue, 0);
    if (result)
        return result;

    set_page_writeback(page);
    result = ops->rw_page(bdev, sector + get_start_sect(bdev), page,
                  REQ_OP_WRITE);
    if (result) {
        end_page_writeback(page);
    } else {
        clean_page_buffers(page);
        unlock_page(page);
    }
    blk_queue_exit(bdev->bd_disk->queue);
    return result;
}

ちなみに、Linux v5.10現在で対応しているのは次の3種類のみとなっている。

  • zram
  • brd
  • nvdimm

そうではない場合には、mpage_alloc関数によってbioを生成し、書き込み対象をbiovecに追加する処理となる。

// 71:
static struct bio *
mpage_alloc(struct block_device *bdev,
        sector_t first_sector, int nr_vecs,
        gfp_t gfp_flags)
{
    struct bio *bio;

    /* Restrict the given (page cache) mask for slab allocations */
    gfp_flags &= GFP_KERNEL;
    bio = bio_alloc(gfp_flags, nr_vecs);

    if (bio == NULL && (current->flags & PF_MEMALLOC)) {
        while (!bio && (nr_vecs /= 2))
            bio = bio_alloc(gfp_flags, nr_vecs);
    }

    if (bio) {
        bio_set_dev(bio, bdev);
        bio->bi_iter.bi_sector = first_sector;
    }
    return bio;
}

mpage_alloc関数は、bioの生成するためにbio_alloc関数を呼び出す。

// 442:
static inline struct bio *bio_alloc(gfp_t gfp_mask, unsigned int nr_iovecs)
{
    return bio_alloc_bioset(gfp_mask, nr_iovecs, &fs_bio_set);
}

bioの生成には、bio_alloc_bioset関数と呼ばれるAPIを実行する。(詳細は割愛)

www.kernel.org

その後、新規に生成したbioに対して、IO対象となるページキャッシュを追加する。

bioがマージ可能かどうか

bioをマージできる条件として次のようなものがある。

  • ページキャッシュが同じ場合
  • 書き込み先が連続している場合
// 1004:
int bio_add_page(struct bio *bio, struct page *page,
         unsigned int len, unsigned int offset)
{
    bool same_page = false;

    if (!__bio_try_merge_page(bio, page, len, offset, &same_page)) {
        if (bio_full(bio, len))
            return 0;
        __bio_add_page(bio, page, len, offset);
    }
    return len;
}

これらの場合には、__bio_try_merge_page関数でbioのマージ (bv_lenbi_sizeの更新) を試みる。

confused

生成できているだけのbioを発行する。

mpage_bio_submit関数

mpage_bio_submit関数

// 61:
static struct bio *mpage_bio_submit(int op, int op_flags, struct bio *bio)
{
    bio->bi_end_io = mpage_end_io;
    bio_set_op_attrs(bio, op, op_flags);
    guard_bio_eod(bio);
    submit_bio(bio);
    return NULL;
}

bi_end_ioはI/Oが完了時に呼び出すハンドラとなっている。(詳細は別記事) bio_set_op_attrs関数は、bio構造体のbi_opfにフラグを設定する

// 438:
static inline void bio_set_op_attrs(struct bio *bio, unsigned op,
        unsigned op_flags)
{
    bio->bi_opf = op | op_flags;
}

今回のケースでは、op_flagswbc_to_write_flags関数によって更新される。

//97:
static inline int wbc_to_write_flags(struct writeback_control *wbc)
{
    int flags = 0;

    if (wbc->punt_to_cgroup)
        flags = REQ_CGROUP_PUNT;

    if (wbc->sync_mode == WB_SYNC_ALL)
        flags |= REQ_SYNC;
    else if (wbc->for_kupdate || wbc->for_background)
        flags |= REQ_BACKGROUND;

    return flags;
}
変数名 wb_check_start_all wb_check_old_data_flush wb_check_background_flush
sync_mode WB_SYNC_NONE WB_SYNC_NONE WB_SYNC_NONE
for_kupdate 0 1 0
for_background 0 0 1

wb_check_old_data_flush関数やwb_check_background_flush関数の場合には、REQ_BACKGROUNDが付与される。

// 600:
void guard_bio_eod(struct bio *bio)
{
    sector_t maxsector = bdev_nr_sectors(bio->bi_bdev);

    if (!maxsector)
        return;

    /*
    * If the *whole* IO is past the end of the device,
    * let it through, and the IO layer will turn it into
    * an EIO.
    */
    if (unlikely(bio->bi_iter.bi_sector >= maxsector))
        return;

    maxsector -= bio->bi_iter.bi_sector;
    if (likely((bio->bi_iter.bi_size >> 9) <= maxsector))
        return;

    bio_truncate(bio, maxsector << 9);
}

また、bioの書き込み範囲がブロックデバイスを超えている場合、guard_bio_eod関数により切り詰める。

こうして得られたbioをsubmit_bio関数を実行して、IOを発行する。

おわりに

本記事では、ext2ファイルシステムwritepages操作 (ext2_writepages)を解説した。
今回の環境でext2_writepages関数を実行することで、次のようなbio構造体が生成された。

ext2_writepages関数の結果

変更履歴

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

参考

xfstests-bld で ファイルシステムのテストを実行する

概要

xfstests-bldは、xfstestsによるテスト実行をサポートするツールである。
本記事では、qemu/kvm上の仮想マシン (amd64)に対して、次の5つの手順について確認した。

はじめに

xfstestsはファイルシステム回帰テストのテストスイートであり、Linuxでメジャーなファイルシステム(xfs, ext2, ext4, cifs, btrfs, f2fs, reiserfs, gfs, jfs, udf, nfs, and tmpfs)でも使用することができる。
多くのLinuxファイルシステムを開発しているメンテナは、本流にマージする前にxfstestsのテストを実施している。
一方で、xfstestsのフルテストを実施するためには、テスト実施環境として求められる要件も少なくない。 つまり、必要なカーネルコンフィグやパッケージが多数あるので、一からxfstestsのテスト環境を構築することは難しいとされている。

xfstests-bldでは、そのような課題を解決するためのツールであり、xfstestsのビルドやテスト環境構築に必要なファイルが用意されている。
また、そこで構築されたテスト環境を、xfstests-bldから次のような環境で実行することができる。

そこで、本記事ではxfstests-bldを使ってQemu/kvm上の仮想マシン(amd64)に対して、xfstestsのテストを実行するまでの手順を確認する。

xfstests-bld (Qemu/kvm) の全体像

実験環境

本記事で使用した開発用PC (Host PC)の構成は次の通りとなっている。

環境 概要
CPU AMD Ryzen 3 3300X
RAM DDR4-2666 16GB ×2
OS Ubuntu Desktop 20.04.04
kernel 5.13.0-30-generic
QEMU 4.2.1 (Debian 1:4.2-3ubuntu6.19)

準備

KVMをインストールする前に、開発環境がKVMを使用できるかどうかを確認しておく。

  1. CPUが仮想化機能をサポートしているかどうか確認する。 (0ではない場合は使用できる模様)

     leava@ubuntu:~/work$ egrep -c '(vmx|svm)' /proc/cpuinfo
     8
    
  2. システムがKVMアクセラレーションを使用できるかどうか確認する。(kvm-okコマンドは、cpu-checkerパッケージに含まれる)

     leava@ubuntu:~/work$ sudo kvm-ok
     INFO: /dev/kvm exists
     KVM acceleration can be used
    

今回の実験環境では、KVMが使用できることが判明したので、次章以降で必要なパッケージをインストールする。

使用できない場合には、BIOSの設定やカーネルコンフィグの見直しが必要となる。

Qemu/kvmのインストール

  1. KVMに関連するパッケージをインストールする

     leava@ubuntu:~/work$ sudo apt update
     leava@ubuntu:~/work$ sudo apt install qemu-kvm
     leava@ubuntu:~/work$ sudo apt install libvirt-daemon-system libvirt-clients bridge-utils
     leava@ubuntu:~/work$ sudo apt install virt-manager
    
  2. 一般ユーザでも仮想マシンを実行できるように、libvirtkvmグループに追加する

     leava@ubuntu:~/work$ sudo adduser `whoami` libvirt
     leava@ubuntu:~/work$ sudo adduser `whoami` kvm  
    
  3. 正しくインストールされているかどうか確認する

     leava@ubuntu:~/work$ virsh list --all
      Id   Name   State
     --------------------
    
  4. libvertdの状態も確認しておく (activeになっていない場合には、sudo systemctl enable --now libvirtdを実行する)

     leava@ubuntu:~/work$ sudo systemctl status libvirtd
     ● libvirtd.service - Virtualization daemon
          Loaded: loaded (/lib/systemd/system/libvirtd.service; enabled; vendor preset: enabled)
          Active: active (running) since Wed 2022-02-23 13:36:41 JST; 10min ago
     TriggeredBy: ● libvirtd.socket
                  ● libvirtd-ro.socket
                  ● libvirtd-admin.socket
            Docs: man:libvirtd(8)
                  https://libvirt.org
        Main PID: 1128 (libvirtd)
           Tasks: 19 (limit: 32768)
          Memory: 30.0M
          CGroup: /system.slice/libvirtd.service
                  ├─1128 /usr/sbin/libvirtd
                  ├─1403 /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/lib/libvirt/libvirt_leaseshelper
                  └─1404 /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/lib/libvirt/libvirt_leaseshelper
    

使用するファイルのダウンロード

xfstests-bldのソースコードとrootfsのダウンロードをする。
xfstests-bldでは、テスト環境用のrootfsを作成する方法が提供されている。building-rootfs.mdを参照。

注意

xfstests-bldのバージョンによって手順は異なるため、ドキュメントを読むこと。

今回はメンテナが用意してあるrootfs (amd64/arm64/i386) からamd64を使用する。

  1. xfstests-bldのソースコードをダウンロードする

     leava@ubuntu:~/work$ git clone git://git.kernel.org/pub/scm/fs/ext2/xfstests-bld.git fstests
    
  2. ワーキングディレクトリを変更する

     leava@ubuntu:~/work$ cd fstests/kvm-xfstests
    
  3. xfstestsのメンテナが用意したrootfsをダウンロードする

     leava@ubuntu:~/work/fstests/kvm-xfstests$ wget -O test-appliance/root_fs.img https://www.kernel.org/pub/linux/kernel/people/tytso/kvm-xfstests/root_fs.amd64.tar.xz
    
  4. ワーキングディレクトリを変更する

     leava@ubuntu:~/work/fstests/kvm-xfstests$ cd ../../
    

仮想マシン用のLinuxカーネルの準備

Qemu/kvmで動作させるカーネルを用意する。
xfstests-bldでは、xfstestsのテスト環境用カーネルコンフィグを生成するスクリプトが提供されている。

今回は、該当スクリプトで生成したコンフィグでカーネルバージョン5.15をビルドする。

  1. カーネルソースコードをダウンロードする

     leava@ubuntu:~/work$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
    
  2. ワーキングディレクトリを変更する

     leava@ubuntu:~/work$ cd linux
    
  3. v5.15をチェックアウトする

     leava@ubuntu:~/work/linux$ git checkout refs/tags/v5.15
    
  4. テスト実行に必要なコンフィグファイルを基に.configを作成する

     leava@ubuntu:~/work/linux$ ../fstests/kvm-xfstests.sh install-kconfig
    
  5. カーネルをビルドする

     leava@ubuntu:~/work/linux$ make -j$(nproc) bzImage
    
  6. ワーキングディレクトリを変更する

     leava@ubuntu:~/work/linux$ cd ../
    

xfstests-bldのセットアップ

xfstests-bldでQemu/kvmを利用する場合には、config.kvmファイルによって設定することができる。
最低限度設定しなければならないパラメータは、ローカルタイムゾーンカーネルのパスとなっている。

  1. ワーキングディレクトリを変更する

     leava@ubuntu:~/work$ cd fstests/kvm-xfstests
    
  2. configファイルw

     leava@ubuntu:~/work/fstests/kvm-xfstests$ echo "TZ=Asia/Tokyo" >> config.kvm 
     leava@ubuntu:~/work/fstests/kvm-xfstests$ echo "KERNEL=$HOME/work/linux/arch/x86/boot/bzImage" >> config.kvm
    
  3. ワーキングディレクトリを変更する

     leava@ubuntu:~/work/fstests/kvm-xfstests$ cd ../../
    

xfstests-bldのインストール

xfstests-bldのためのスクリプトを生成し、パスが通っているディレクトリに移動させる。

  1. ワーキングディレクトリを変更する

     leava@ubuntu:~/work$ cd fstests
    
  2. Qemu/kvm用のxfstests-bldをビルドする

     leava@ubuntu:~/work/fstests$ make kvm-xfstests.sh
    
  3. Qemu/kvm用のxfstests-bldをインストールする

     leava@ubuntu:~/work/fstests$ cp kvm-xfstests.sh ~/bin/kvm-xfstests
    
  4. ワーキングディレクトリを変更する

     leava@ubuntu:~/work/fstests$ cd ../
    

xfstests-bldの実行

xfstestesを実行するためには、kvm-xfstestsスクリプトを実行する。
kvm-xfstestsスクリプトには様々なパラメータを設定することができる。

ここで、kvm-xfstestsの使用方法について次の記載する。

    Usage: kvm-xfstests [<OPTIONS>] smoke|full
    Usage: kvm-xfstests [<OPTIONS>] <test> ...
    Usage: kvm-xfstests [<OPTIONS>] -g <group> ...
    Usage: kvm-xfstests [<OPTIONS>] shell|maint
    Usage: kvm-xfstests [<OPTIONS>] syz <repro>

    Common options are:
            -a              - Disable auto-exclude; run all tests
            -c config       - Specify a file system configuration
            -C count        - Run the specified tests multiple times
            -I image        - Use this test appliance image
            -m mountopts    - Append mount options to fs config
            -n nr_cpus      - Specify the number of cpu's
            -numa num       - Ask KVM to create <num> NUMA nodes
            -N              - Enable networking (requires root)
            -o opts         - Extra kernel command line options
            -O opts         - Extra options for test runner
            -r ram          - Specify memory to be used in megabytes
            -x group        - Exclude group of tests from running
            -X test         - Exclude test from running
            --kernel file   - Boot the specified kernel
            --initrd initrd - Boot with the specified initrd
            --no-log        - Don't save the log file for this run
            --no-action     - Print the command to start the VM

ただし、上記のオプションがすべてではない。(kvm-xfstests/util/parse_cliを参照)

起動したVMを終了させるためには、poweroffコマンド、またはCtrl-a xを押下する必要がある。
また、テスト結果は、logsディレクトリ以下に保存される。

今回は、次の5つの手順を確認する。

スモークテストの実行

kvm-xfstests smokeでスモークテスト (-g quick)を実行することができる。

     leava@ubuntu:~/work/$ kvm-xfstests smoke

     CMDLINE: "smoke"
     KERNEL: kernel  5.15.0-xfstests #456 SMP Wed Feb 23 19:12:12 JST 2022 x86_64
     FSTESTVER: blktests     3be7849 (Tue, 19 Oct 2021 12:22:17 -0700)
     FSTESTVER: e2fsprogs    v1.46.4-27-g6b7a6e4a (Thu, 4 Nov 2021 20:29:19 -0400)
     FSTESTVER: fio          fio-3.29 (Sat, 18 Dec 2021 07:09:32 -0700)
     FSTESTVER: fsverity     v1.4-4-gddc6bc9 (Wed, 22 Sep 2021 11:55:11 -0700)
     FSTESTVER: ima-evm-utils        v1.3.2 (Wed, 28 Oct 2020 13:18:08 -0400)
     FSTESTVER: nvme-cli     v1.16 (Thu, 11 Nov 2021 13:09:06 -0800)
     FSTESTVER: quota                v4.05-43-gd2256ac (Fri, 17 Sep 2021 14:04:16 +0200)
     FSTESTVER: util-linux   v2.37.2 (Mon, 16 Aug 2021 15:23:50 +0200)
     FSTESTVER: xfsprogs     v5.13.0 (Fri, 20 Aug 2021 12:03:57 -0400)
     FSTESTVER: xfstests     linux-v3.8-3438-gf0a05db9 (Sun, 9 Jan 2022 18:58:11 -0500)
     FSTESTVER: xfstests-bld 8b681c94 (Sat, 8 Jan 2022 22:20:42 -0500)
     FSTESTVER: zz_build-distro      bullseye
     FSTESTCFG: "4k"
     FSTESTSET: "-g quick"
     FSTESTEXC: ""
     FSTESTOPT: "aex"
     MNTOPTS: ""
     CPUS: "2"
     MEM: "1958.76"

テストが終了すると、自動で仮想マシンが終了する。

シェルの起動

kvm-xfstests shell仮想マシンにログインすることができる。

    leava@ubuntu:~/work/$ kvm-xfstests shell

    Debian GNU/Linux 11 kvm-xfstests ttyS0

    kvm-xfstests login: root (automatic login)

    Linux kvm-xfstests 5.15.0-xfstests #456 SMP Wed Feb 23 19:12:12 JST 2022 x86_64

    The programs included with the Debian GNU/Linux system are free software;
    the exact distribution terms for each program are described in the
    individual files in /usr/share/doc/*/copyright.

    Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
    permitted by applicable law.
    Last login: Thu Feb 24 23:56:29 JST 2022 on ttyS1
    COLUMNS=238
    LINES=58
    root@kvm-xfstests:~# 

仮想マシンを終了すると、この環境で更新された内容は破棄されるので注意が必要である。
そのため、rootfsを更新したい場合には、kvm-xfstestsmaintを実行する。

シェルからテストを実行するためには、/root/runtest.shを実行する。

    root@kvm-xfstests:~# /root/runtests.sh 
    CMDLINE: ""
    KERNEL: kernel  5.15.0-xfstests #456 SMP Wed Feb 23 19:12:12 JST 2022 x86_64
    FSTESTVER: blktests     3be7849 (Tue, 19 Oct 2021 12:22:17 -0700)
    FSTESTVER: e2fsprogs    v1.46.4-27-g6b7a6e4a (Thu, 4 Nov 2021 20:29:19 -0400)
    FSTESTVER: fio          fio-3.29 (Sat, 18 Dec 2021 07:09:32 -0700)
    FSTESTVER: fsverity     v1.4-4-gddc6bc9 (Wed, 22 Sep 2021 11:55:11 -0700)
    FSTESTVER: ima-evm-utils        v1.3.2 (Wed, 28 Oct 2020 13:18:08 -0400)
    FSTESTVER: nvme-cli     v1.16 (Thu, 11 Nov 2021 13:09:06 -0800)
    FSTESTVER: quota                v4.05-43-gd2256ac (Fri, 17 Sep 2021 14:04:16 +0200)
    FSTESTVER: util-linux   v2.37.2 (Mon, 16 Aug 2021 15:23:50 +0200)
    FSTESTVER: xfsprogs     v5.13.0 (Fri, 20 Aug 2021 12:03:57 -0400)
    FSTESTVER: xfstests     linux-v3.8-3438-gf0a05db9 (Sun, 9 Jan 2022 18:58:11 -0500)
    FSTESTVER: xfstests-bld 8b681c94 (Sat, 8 Jan 2022 22:20:42 -0500)
    FSTESTVER: zz_build-distro      bullseye
    FSTESTCFG: "4k"
    FSTESTSET: "generic/001"
    FSTESTEXC: ""
    FSTESTOPT: "aex"
    MNTOPTS: ""
    CPUS: "2"
    MEM: "1958.76"
                   total        used        free      shared  buff/cache   available
    Mem:            1958          82        1775           0          99        1843
    Swap:              0           0           0

このとき、パラメータは/root/test-envに設定されている。
それらの環境変数を設定することで、xfstestsのテスト実施環境を変更することができる。

この環境は、次のポート(TCP)がデバッグ目的で使用可能となっている。

例えば、開発用PCからgdbでアクセスすることができる。

    leava@ubuntu:~/work$ gdb linux/vmlinux
    (gdb) target remote :7499
    Remote debugging using :7499
    0xffffffff81dcbce0 in default_idle () at ./arch/x86/include/asm/irqflags.h:51
    51              asm volatile("sti; hlt": : :"memory");

実施するテストの変更

xfstests-bldでは、xfstestsと同様に特定のテストのみを実施することができる。
例えば、generic/001のテストを実施したい場合には、次のように実行すればよい。

    leava@ubuntu:~/work$ kvm-xfstests generic/001
    CMDLINE: "generic/001"
    KERNEL: kernel  5.15.0-xfstests #456 SMP Wed Feb 23 19:12:12 JST 2022 x86_64
    FSTESTVER: blktests     3be7849 (Tue, 19 Oct 2021 12:22:17 -0700)
    FSTESTVER: e2fsprogs    v1.46.4-27-g6b7a6e4a (Thu, 4 Nov 2021 20:29:19 -0400)
    FSTESTVER: fio          fio-3.29 (Sat, 18 Dec 2021 07:09:32 -0700)
    FSTESTVER: fsverity     v1.4-4-gddc6bc9 (Wed, 22 Sep 2021 11:55:11 -0700)
    FSTESTVER: ima-evm-utils        v1.3.2 (Wed, 28 Oct 2020 13:18:08 -0400)
    FSTESTVER: nvme-cli     v1.16 (Thu, 11 Nov 2021 13:09:06 -0800)
    FSTESTVER: quota                v4.05-43-gd2256ac (Fri, 17 Sep 2021 14:04:16 +0200)
    FSTESTVER: util-linux   v2.37.2 (Mon, 16 Aug 2021 15:23:50 +0200)
    FSTESTVER: xfsprogs     v5.13.0 (Fri, 20 Aug 2021 12:03:57 -0400)
    FSTESTVER: xfstests     linux-v3.8-3438-gf0a05db9 (Sun, 9 Jan 2022 18:58:11 -0500)
    FSTESTVER: xfstests-bld 8b681c94 (Sat, 8 Jan 2022 22:20:42 -0500)
    FSTESTVER: zz_build-distro      bullseye
    FSTESTCFG: "all"
    FSTESTSET: "generic/001"
    FSTESTEXC: ""
    FSTESTOPT: "aex"
    MNTOPTS: ""
    CPUS: "2"
    MEM: "1958.76"
                   total        used        free      shared  buff/cache   available
    Mem:            1958          80        1782           0          95        1845
    Swap:              0           0           0

また、xfstestsと同様にグループによるテストの指定も可能となっている。 例えば、rwグループに属しているテストを実施したい場合には、次のように実行すればよい。

    leava@ubuntu:~/work$ kvm-xfstests -g rw
    CMDLINE: "-g rw"
    KERNEL: kernel  5.15.0-xfstests #456 SMP Wed Feb 23 19:12:12 JST 2022 x86_64
    FSTESTVER: blktests     3be7849 (Tue, 19 Oct 2021 12:22:17 -0700)
    FSTESTVER: e2fsprogs    v1.46.4-27-g6b7a6e4a (Thu, 4 Nov 2021 20:29:19 -0400)
    FSTESTVER: fio          fio-3.29 (Sat, 18 Dec 2021 07:09:32 -0700)
    FSTESTVER: fsverity     v1.4-4-gddc6bc9 (Wed, 22 Sep 2021 11:55:11 -0700)
    FSTESTVER: ima-evm-utils        v1.3.2 (Wed, 28 Oct 2020 13:18:08 -0400)
    FSTESTVER: nvme-cli     v1.16 (Thu, 11 Nov 2021 13:09:06 -0800)
    FSTESTVER: quota                v4.05-43-gd2256ac (Fri, 17 Sep 2021 14:04:16 +0200)
    FSTESTVER: util-linux   v2.37.2 (Mon, 16 Aug 2021 15:23:50 +0200)
    FSTESTVER: xfsprogs     v5.13.0 (Fri, 20 Aug 2021 12:03:57 -0400)
    FSTESTVER: xfstests     linux-v3.8-3438-gf0a05db9 (Sun, 9 Jan 2022 18:58:11 -0500)
    FSTESTVER: xfstests-bld 8b681c94 (Sat, 8 Jan 2022 22:20:42 -0500)
    FSTESTVER: zz_build-distro      bullseye
    FSTESTCFG: "all"
    FSTESTSET: "-g rw"
    FSTESTEXC: ""
    FSTESTOPT: "aex"
    MNTOPTS: ""
    CPUS: "2"
    MEM: "1958.76"
                   total        used        free      shared  buff/cache   available
    Mem:            1958          80        1782           0          95        1845
    Swap:              0           0           0

テスト用ファイルシステムの変更

xfstests-bldでは、PRIMARY_FSTYPEによってテスト対象のファイルシステムを指定する。
このパラメータは、kvm-xfstests/config.commonファイルを編集するか、--primary_fstypeオプションを指定する必要がある。

例えば、恒久的にxfsファイルシステムでテストしたい場合には、次のようにkvm-xfstests/config.commonファイルを修正すればよい。

diff --git a/kvm-xfstests/config.common b/kvm-xfstests/config.common
index 12cae0a..1de021f 100644
--- a/kvm-xfstests/config.common
+++ b/kvm-xfstests/config.common
@@ -4,4 +4,4 @@
 # Variables set here may be overridden in ~/.config/xfstests-common
 #
 
-PRIMARY_FSTYPE="ext4"
+PRIMARY_FSTYPE="xfs"

一方で、一時的にxfsファイルシステムでテストを実行したい場合には、次のように実行すればよい。

    leava@ubuntu:~/work$ kvm-xfstests --primary_fstype xfs generic/001
    CMDLINE: "--primary_fstype xfs smoke"
    KERNEL: kernel  5.15.0-xfstests #456 SMP Wed Feb 23 19:12:12 JST 2022 x86_64
    FSTESTVER: blktests     3be7849 (Tue, 19 Oct 2021 12:22:17 -0700)
    FSTESTVER: e2fsprogs    v1.46.4-27-g6b7a6e4a (Thu, 4 Nov 2021 20:29:19 -0400)
    FSTESTVER: fio          fio-3.29 (Sat, 18 Dec 2021 07:09:32 -0700)
    FSTESTVER: fsverity     v1.4-4-gddc6bc9 (Wed, 22 Sep 2021 11:55:11 -0700)
    FSTESTVER: ima-evm-utils        v1.3.2 (Wed, 28 Oct 2020 13:18:08 -0400)
    FSTESTVER: nvme-cli     v1.16 (Thu, 11 Nov 2021 13:09:06 -0800)
    FSTESTVER: quota                v4.05-43-gd2256ac (Fri, 17 Sep 2021 14:04:16 +0200)
    FSTESTVER: util-linux   v2.37.2 (Mon, 16 Aug 2021 15:23:50 +0200)
    FSTESTVER: xfsprogs     v5.13.0 (Fri, 20 Aug 2021 12:03:57 -0400)
    FSTESTVER: xfstests     linux-v3.8-3438-gf0a05db9 (Sun, 9 Jan 2022 18:58:11 -0500)
    FSTESTVER: xfstests-bld 8b681c94 (Sat, 8 Jan 2022 22:20:42 -0500)
    FSTESTVER: zz_build-distro      bullseye
    FSTESTCFG: "4k"
    FSTESTSET: "-g quick"
    FSTESTEXC: ""
    FSTESTOPT: "aex"
    MNTOPTS: ""
    CPUS: "2"
    MEM: "1958.76"
                   total        used        free      shared  buff/cache   available
    Mem:            1958          81        1780           0          96        1845
    Swap:     

テスト用ファイルシステムのパラメータ変更

xfstests-bldでは、cfgによってテスト用ファイルシステムのパラメータを変更することができる。
ここで、ファイルシステムのパラメータとは「mkfsの引数」や「マウントオプション」である。

デフォルトで用意されているcfgファイルは、fstests/kvm-xfstests/test-appliance/files/root/fs/${fsname}/cfgに保存されている。
kvm-xfstestsでは、これらのファイルを-cオプションで指定することができる。

例えば、nojournalgeneric/001のテストを実行したい場合には、次のように実行すればよい。

    leava@ubuntu:~/work$ kvm-xfstests -c nojournal generic/001
    CMDLINE:   -c 4k generic/001
    CPUS:      2
    MEM:       1958.76

    ext4/4k: 1 tests, 4 seconds
      generic/001  Pass     4s
    Totals: 1 tests, 0 skipped, 0 failures, 0 errors, 4s

    FSTESTVER: blktests 3be7849 (Tue, 19 Oct 2021 12:22:17 -0700)
    FSTESTVER: e2fsprogs v1.46.4-27-g6b7a6e4a (Thu, 4 Nov 2021 20:29:19 -0400)
    FSTESTVER: fio  fio-3.29 (Sat, 18 Dec 2021 07:09:32 -0700)
    FSTESTVER: fsverity v1.4-4-gddc6bc9 (Wed, 22 Sep 2021 11:55:11 -0700)
    FSTESTVER: ima-evm-utils v1.3.2 (Wed, 28 Oct 2020 13:18:08 -0400)
    FSTESTVER: nvme-cli v1.16 (Thu, 11 Nov 2021 13:09:06 -0800)
    FSTESTVER: quota  v4.05-43-gd2256ac (Fri, 17 Sep 2021 14:04:16 +0200)
    FSTESTVER: util-linux v2.37.2 (Mon, 16 Aug 2021 15:23:50 +0200)
    FSTESTVER: xfsprogs v5.13.0 (Fri, 20 Aug 2021 12:03:57 -0400)
    CMDLINE: "-c nojournal generic/001"
    KERNEL: kernel  5.15.0-xfstests #456 SMP Wed Feb 23 19:12:12 JST 2022 x86_64
    FSTESTVER: blktests     3be7849 (Tue, 19 Oct 2021 12:22:17 -0700)
    FSTESTVER: e2fsprogs    v1.46.4-27-g6b7a6e4a (Thu, 4 Nov 2021 20:29:19 -0400)
    FSTESTVER: fio          fio-3.29 (Sat, 18 Dec 2021 07:09:32 -0700)
    FSTESTVER: fsverity     v1.4-4-gddc6bc9 (Wed, 22 Sep 2021 11:55:11 -0700)
    FSTESTVER: ima-evm-utils        v1.3.2 (Wed, 28 Oct 2020 13:18:08 -0400)
    FSTESTVER: nvme-cli     v1.16 (Thu, 11 Nov 2021 13:09:06 -0800)
    FSTESTVER: quota                v4.05-43-gd2256ac (Fri, 17 Sep 2021 14:04:16 +0200)
    FSTESTVER: util-linux   v2.37.2 (Mon, 16 Aug 2021 15:23:50 +0200)
    FSTESTVER: xfsprogs     v5.13.0 (Fri, 20 Aug 2021 12:03:57 -0400)
    FSTESTVER: xfstests     linux-v3.8-3438-gf0a05db9 (Sun, 9 Jan 2022 18:58:11 -0500)
    FSTESTVER: xfstests-bld 8b681c94 (Sat, 8 Jan 2022 22:20:42 -0500)
    FSTESTVER: zz_build-distro      bullseye
    FSTESTCFG: "nojournal"
    FSTESTSET: "generic/001"
    FSTESTEXC: ""
    FSTESTOPT: "aex"
    MNTOPTS: ""
    CPUS: "2"
    MEM: "1958.76"
                   total        used        free      shared  buff/cache   available
    Mem:            1958          81        1781           0          95        1844
    Swap:              0           0           0        

また、cfgファイルを指定しなかった場合は、fstests/kvm-xfstests/test-appliance/files/root/fs/${fsname}/cfg/all.listが参照される。

おわりに

本記事では、xfstests-bldによってqemu/kvm上の仮想マシン(amd64)に対して、xfstestsのテストを実行する手順について確認した。
xfstests単体でテストを実行する場合に比べて、容易にテスト環境が作成できるため、これからxfstestsを実行してみたいと思っているユーザはぜひ確認してほしい。

また、本記事でまとめた内容はxfstests-bldのほんの一部の機能である。
その他の使い方 (例えば、rootfsのカスタマイズ方法) については、ドキュメントやスクリプトの中身を確認してほしい。

変更履歴

  • 2022/2/27: 記事公開

参考文献

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

関連記事

概要

QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。

本章では、wb_workfn関数からwrite_inodewritepagesを呼びところまで確認した。

はじめに

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

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

調査対象 (シーケンス図)

本記事では、writebackカーネルスレッドからwritepageswrite_inodeを呼び出すまでを確認する。

writebackするための条件確認

再掲: bdi_writebackの状態

wb_wakeup_delayed関数から呼び出すwb_workfn関数の定義は下記の通りとなっている。

// 2219:
void wb_workfn(struct work_struct *work)
{
    struct bdi_writeback *wb = container_of(to_delayed_work(work),
                        struct bdi_writeback, dwork);
    long pages_written;

    set_worker_desc("flush-%s", bdi_dev_name(wb->bdi));
    current->flags |= PF_SWAPWRITE;

    if (likely(!current_is_workqueue_rescuer() ||
           !test_bit(WB_registered, &wb->state))) {
        /*
        * The normal path.  Keep writing back @wb until its
        * work_list is empty.  Note that this path is also taken
        * if @wb is shutting down even when we're running off the
        * rescuer as work_list needs to be drained.
        */
        do {
            pages_written = wb_do_writeback(wb);
            trace_writeback_pages_written(pages_written);
        } while (!list_empty(&wb->work_list));
    } else {
        /*
        * bdi_wq can't get enough workers and we're running off
        * the emergency worker.  Don't hog it.  Hopefully, 1024 is
        * enough for efficient IO.
        */
        pages_written = writeback_inodes_wb(wb, 1024,
                            WB_REASON_FORKER_THREAD);
        trace_writeback_pages_written(pages_written);
    }

    if (!list_empty(&wb->work_list))
        wb_wakeup(wb);
    else if (wb_has_dirty_io(wb) && dirty_writeback_interval)
        wb_wakeup_delayed(wb);

    current->flags &= ~PF_SWAPWRITE;
}

wb_workfn関数は、wb_init関数のINIT_DELAYED_WORKマクロによって追加されたworkである。 container_ofマクロを使うことで、元の構造体bdi_writebackを取得することができる。

また、現在のプロセスにスワップへのアクセスを許可するためにPF_SWAPWRITEを立てる。

lkml.iu.edu

current_is_workqueue_rescuer関数は、導入時のコミットメッセージから「Work Queue rescuerを実行しているかどうかを確認する」ものだろう。

lore.kernel.org

そのため、ここでは通常時の 2228行のif文がTrueの場合のみを考慮する。 wb_do_writeback関数は次のようになっている。

// 2188:
static long wb_do_writeback(struct bdi_writeback *wb)
{
    struct wb_writeback_work *work;
    long wrote = 0;

    set_bit(WB_writeback_running, &wb->state);
    while ((work = get_next_work_item(wb)) != NULL) {
        trace_writeback_exec(wb, work);
        wrote += wb_writeback(wb, work);
        finish_writeback_work(wb, work);
    }

    /*
    * Check for a flush-everything request
    */
    wrote += wb_check_start_all(wb);

    /*
    * Check for periodic writeback, kupdated() style
    */
    wrote += wb_check_old_data_flush(wb);
    wrote += wb_check_background_flush(wb);
    clear_bit(WB_writeback_running, &wb->state);

    return wrote;
}

wb_do_writeback関数は、次のような処理を担う。

  • wb_writeback_workを順番に実行していく
  • 特定の条件を満たしているページキャッシュをwritebackする

ここで、「特定の条件」について軽く確認しておくと、次の通りとなっている。

  • wb_check_start_all: writebackが途中となっている状態かどうかを確認する
  • wb_check_old_data_flush: Dirtyになってからdirty_writeback_centisecs時間経過しているかどうかを確認する。
  • wb_check_background_flush: 現在キャッシュされているDirtyなページがしきい値を超えているかどうかを確認する。

これらの条件が満たされている場合、次のようなwb_writeback_workを引数にwb_writebackを実行する。

変数名 wb_check_start_all wb_check_old_data_flush wb_check_background_flush
nr_pages wb_split_bdi_pages(wb, nr_pages) nr_pages LONG_MAX
sync_mode WB_SYNC_NONE WB_SYNC_NONE WB_SYNC_NONE
for_kupdate 0 1 0
for_background 0 0 1
range_cyclic 1 1 1
reason wb->start_all_reason WB_REASON_PERIODIC WB_REASON_BACKGROUND

すべてのダーティデータを書き込み

// 2161:
static long wb_check_start_all(struct bdi_writeback *wb)
{
    long nr_pages;

    if (!test_bit(WB_start_all, &wb->state))
        return 0;

    nr_pages = get_nr_dirty_pages();
    if (nr_pages) {
        struct wb_writeback_work work = {
            .nr_pages   = wb_split_bdi_pages(wb, nr_pages),
            .sync_mode  = WB_SYNC_NONE,
            .range_cyclic   = 1,
            .reason     = wb->start_all_reason,
        };

        nr_pages = wb_writeback(wb, &work);
    }

    clear_bit(WB_start_all, &wb->state);
    return nr_pages;
}

wb_check_start_all関数は、次の場合にwb_writeback関数で指定されたページをwritebackする関数となっている。

  • WB_start_all フラグが立っている場合 (他プロセスによるwritebackがまだ終わっていない場合)
  • Dirtyとなっているページが存在する場合

bdi_writeback構造体にWB_start_allフラグがセットされているのは、wb_start_writeback関数のみとなっている。

// 1222:
static void wb_start_writeback(struct bdi_writeback *wb, enum wb_reason reason)
{
    if (!wb_has_dirty_io(wb))
        return;

    /*
    * All callers of this function want to start writeback of all
    * dirty pages. Places like vmscan can call this at a very
    * high frequency, causing pointless allocations of tons of
    * work items and keeping the flusher threads busy retrieving
    * that work. Ensure that we only allow one of them pending and
    * inflight at the time.
    */
    if (test_bit(WB_start_all, &wb->state) ||
        test_and_set_bit(WB_start_all, &wb->state))
        return;

    wb->start_all_reason = reason;
    wb_wakeup(wb);
}

この関数は、wb_io_lists_populated関数が実行されてからwb_io_lists_depopulated関数が実行されるまでの間でのみ、WB_start_allフラグをセットする。
そして、WB_start_allフラグがセットできた場合には、wb_wakeup関数によってmod_delayed_work関数を実行する。
mod_delayed_work関数については、前章のWork QueueのAPIとなっている。

leavatail.hatenablog.com

expireしたデータをフラッシュする

// 2127:
static long wb_check_old_data_flush(struct bdi_writeback *wb)
{
    unsigned long expired;
    long nr_pages;

    /*
    * When set to zero, disable periodic writeback
    */
    if (!dirty_writeback_interval)
        return 0;

    expired = wb->last_old_flush +
            msecs_to_jiffies(dirty_writeback_interval * 10);
    if (time_before(jiffies, expired))
        return 0;

    wb->last_old_flush = jiffies;
    nr_pages = get_nr_dirty_pages();

    if (nr_pages) {
        struct wb_writeback_work work = {
            .nr_pages   = nr_pages,
            .sync_mode  = WB_SYNC_NONE,
            .for_kupdate    = 1,
            .range_cyclic   = 1,
            .reason     = WB_REASON_PERIODIC,
        };

        return wb_writeback(wb, &work);
    }

    return 0;
}

wb_check_old_data_flush関数は、次の場合にwb_writeback関数で指定されたページをwritebackする関数となっている。

  • dirty_writeback_interval が設定されている場合
  • 前回のwb_check_old_data_flush関数が実行されてからdirty_writeback_interval × 10 ミリ秒経過している場合
  • ノード内にあるNR_FILE_DIRTYのページが存在する場合

dirty_writeback_intervalは、カーネルがwritebackを定期的に実行する間隔となっている。
この値は、{procfs}/sys/vm/dirty_writeback_centisecs で設定することができる。 (本実験環境では、デフォルト値として500が設定されていた)

この時、get_nr_dirty_pages関数によって、Dirty (となっている) のページの数を取得する。

// 1216:
static unsigned long get_nr_dirty_pages(void)
{
    return global_node_page_state(NR_FILE_DIRTY) +
        get_nr_dirty_inodes();
}

get_nr_dirty_pages関数では、ノード内にあるNR_FILE_DIRTYのページとDirtyとなっている大まかなinodeの数を返す。

Backgroundによる書き込みの閾値を超えている

別の記事で取り上げることにする。

writebackを開始する

これまでにwritebackする条件を満たしていた場合、writebackする単位をworkとしてwb_writeback関数を実行する。
また、workには次の値が渡される。 (再掲)

変数名 wb_check_start_all wb_check_old_data_flush wb_check_background_flush
nr_pages ノード内にあるDirtyページ (最大) ノード内にあるDirtyページ数 LONG_MAX
sync_mode WB_SYNC_NONE WB_SYNC_NONE WB_SYNC_NONE
for_kupdate 0 1 0
for_background 0 0 1
range_cyclic 1 1 1
reason WB_REASON_LAPTOP_TIMER
WB_REASON_SYNC
WB_REASON_PERIODIC
WB_REASON_VMSCAN
WB_REASON_PERIODIC WB_REASON_BACKGROUND

wb_writeback関数の定義は次の通りとなっている。

// 2002:
static long wb_writeback(struct bdi_writeback *wb,
             struct wb_writeback_work *work)
{
    long nr_pages = work->nr_pages;
    unsigned long dirtied_before = jiffies;
    struct inode *inode;
    long progress;
    struct blk_plug plug;

    blk_start_plug(&plug);
    spin_lock(&wb->list_lock);
    for (;;) {
        /*
        * Stop writeback when nr_pages has been consumed
        */
        if (work->nr_pages <= 0)
            break;

        /*
        * Background writeout and kupdate-style writeback may
        * run forever. Stop them if there is other work to do
        * so that e.g. sync can proceed. They'll be restarted
        * after the other works are all done.
        */
        if ((work->for_background || work->for_kupdate) &&
            !list_empty(&wb->work_list))
            break;

        /*
        * For background writeout, stop when we are below the
        * background dirty threshold
        */
        if (work->for_background && !wb_over_bg_thresh(wb))
            break;

        /*
        * Kupdate and background works are special and we want to
        * include all inodes that need writing. Livelock avoidance is
        * handled by these works yielding to any other work so we are
        * safe.
        */
        if (work->for_kupdate) {
            dirtied_before = jiffies -
                msecs_to_jiffies(dirty_expire_interval * 10);
        } else if (work->for_background)
            dirtied_before = jiffies;

        trace_writeback_start(wb, work);
        if (list_empty(&wb->b_io))
            queue_io(wb, work, dirtied_before);
        if (work->sb)
            progress = writeback_sb_inodes(work->sb, wb, work);
        else
            progress = __writeback_inodes_wb(wb, work);
        trace_writeback_written(wb, work);

        /*
        * Did we write something? Try for more
        *
        * Dirty inodes are moved to b_io for writeback in batches.
        * The completion of the current batch does not necessarily
        * mean the overall work is done. So we keep looping as long
        * as made some progress on cleaning pages or inodes.
        */
        if (progress)
            continue;
        /*
        * No more inodes for IO, bail
        */
        if (list_empty(&wb->b_more_io))
            break;
        /*
        * Nothing written. Wait for some inode to
        * become available for writeback. Otherwise
        * we'll just busyloop.
        */
        trace_writeback_wait(wb, work);
        inode = wb_inode(wb->b_more_io.prev);
        spin_lock(&inode->i_lock);
        spin_unlock(&wb->list_lock);
        /* This function drops i_lock... */
        inode_sleep_on_writeback(inode);
        spin_lock(&wb->list_lock);
    }
    spin_unlock(&wb->list_lock);
    blk_finish_plug(&plug);

    return nr_pages - work->nr_pages;
}

wb_writeback関数では、workの内容次第で処理が異なる。

wb_check_start_all関数が追加したworkの場合

bdi_writeback構造体に繋がっているすべてのinodeをexpiredとし、writebackを開始する。

  • 最初の条件分岐 (2017行目)では、他の要因によってDirtyページがすべて書き出されていない限り条件を満たさない。
  • 2つ目の条件分岐 (2026行目)では、for_backgroundfor_kupdateフラグが立っていないので条件を満たさない。
  • 3つ目の条件分岐 (2034行目)では、for_backgroundフラグが立っていないので条件を満たさない。
  • 4つ目の条件分岐 (2043, 2046行目)では、for_backgroundfor_kupdateフラグが立っていないので条件を満たさない。
  • 5つ目の条件分岐 (2050行目)では、wb->b_ioが空であった場合、queue_io関数を実行する。
  • 6つ目の条件分岐 (2052行目)では、work->sbNULLであるため、__writeback_inode_wb関数を実行する。

wb_b_ioリストが空queue_io関数の定義は次の通りとなっている。

// 1445:
static void queue_io(struct bdi_writeback *wb, struct wb_writeback_work *work,
             unsigned long dirtied_before)
{
    int moved;
    unsigned long time_expire_jif = dirtied_before;

    assert_spin_locked(&wb->list_lock);
    list_splice_init(&wb->b_more_io, &wb->b_io);
    moved = move_expired_inodes(&wb->b_dirty, &wb->b_io, dirtied_before);
    if (!work->for_sync)
        time_expire_jif = jiffies - dirtytime_expire_interval * HZ;
    moved += move_expired_inodes(&wb->b_dirty_time, &wb->b_io,
                     time_expire_jif);
    if (moved)
        wb_io_lists_populated(wb);
    trace_writeback_queue_io(wb, work, dirtied_before, moved);
}

引数にあるdirtied_beforeは、expireとなるtick数を表している。(つまりこの値より小さい場合には、expairdなinodeとなる)
次のような手順でbdi_writeback構造体のメンバb_ioにinodeを追加する。

  1. b_more_ioに連結しているノードをb_ioに繋げる
  2. b_dirtyに連結しているexpiredなノードをb_ioに繋げる
  3. b_dirty_timeに連結しているexpiredなノードをb_ioに繋げる

ここで、次のような状態のbdi_writeback構造体に対してqueue_io関数を実行したときを考えてみる。

queue_io関数の実行前のリスト例

実際には、各ノードの末尾は先頭とつながっているが簡略化の都合上、図からは排除している。
また、b_dirtyb_dirty_timeはメンバ内のdirtied_whenに依存してノードの追加をするが、簡略化の都合上、対象のノードにはexpiredと赤字を付けている。

まず初めに、b_more_iob_ioを引数としてlist_splice_init関数を実行する。
list_splice_init関数は下記の通りとなっている。

// 478:
static inline void list_splice_init(struct list_head *list,
                    struct list_head *head)
{
    if (!list_empty(list)) {
        __list_splice(list, head, head->next);
        INIT_LIST_HEAD(list);
    }
}

__list_splice関数は、headnextlistを挿入する関数となっている。
その結果、b_more_ioに連結していたinode (E) と inode (D) が b_io の先頭に挿入される。

list_splice_init関数を実行した結果

その後、b_dirtyb_dirty_timeそれぞれに対して、move_expired_inodes関数を実行する。
この時、第3引数のdirtied_beforeには、expireとなるtick数を渡している。

move_expired_inodes関数の定義は次の通りとなっている。

// 1338:
static int move_expired_inodes(struct list_head *delaying_queue,
                   struct list_head *dispatch_queue,
                   unsigned long dirtied_before)
{
    LIST_HEAD(tmp);
    struct list_head *pos, *node;
    struct super_block *sb = NULL;
    struct inode *inode;
    int do_sb_sort = 0;
    int moved = 0;

    while (!list_empty(delaying_queue)) {
        inode = wb_inode(delaying_queue->prev);
        if (inode_dirtied_after(inode, dirtied_before))
            break;
        list_move(&inode->i_io_list, &tmp);
        moved++;
        spin_lock(&inode->i_lock);
        inode->i_state |= I_SYNC_QUEUED;
        spin_unlock(&inode->i_lock);
        if (sb_is_blkdev_sb(inode->i_sb))
            continue;
        if (sb && sb != inode->i_sb)
            do_sb_sort = 1;
        sb = inode->i_sb;
    }

    /* just one sb in list, splice to dispatch_queue and we're done */
    if (!do_sb_sort) {
        list_splice(&tmp, dispatch_queue);
        goto out;
    }

    /* Move inodes from one superblock together */
    while (!list_empty(&tmp)) {
        sb = wb_inode(tmp.prev)->i_sb;
        list_for_each_prev_safe(pos, node, &tmp) {
            inode = wb_inode(pos);
            if (inode->i_sb == sb)
                list_move(&inode->i_io_list, dispatch_queue);
        }
    }
out:
    return moved;
}

move_expired_inodes関数では、inode_dirtied_after関数で対象のinodeがexpireしているかどうか確認し、変数tmpを介してdispatch_queue(今回の場合はb_io)に繋げていく。

move_expired_inodes関数を実行した結果

b_dirtyに対してmore_expired_inodes関数を実行した結果は、inode(a)がb_ioの先頭に挿入される。
最終的に、queue_io関数実行後には次のような状態となっている。

queue_io関数を実行した結果

この時、b_ioに連結しているノードがwritebackの対象となる。

wb_check_old_data_flush関数が追加したworkの場合

bdi_writeback構造体に繋がっているinodeで、Dirtyになってからdirty_expire_interval × 10 ミリ秒経過しているinodeのみ、writebackを開始する。

  • 最初の条件分岐 (2017行目)では、他の要因によってDirtyページがすべて書き出されていない限り条件を満たさない。
  • 2つ目の条件分岐 (2026行目)では、for_kupdateフラグが立っているので、wb->work_listが空の場合、処理を中断する。
  • 3つ目の条件分岐 (2034行目)では、for_backgroundフラグが立っていないので条件を満たさない。
  • 4つ目の条件分岐 (2043, 2046行目)では、for_kupdateフラグが立っているので、dirtied_beforeを更新する。
  • 5つ目の条件分岐 (2050行目)では、wb->b_ioが空であった場合、queue_io関数を実行する。
  • 6つ目の条件分岐 (2052行目)では、work->sbNULLであるため、__writeback_inode_wb関数を実行する。

wb_check_background_flush関数が追加したworkの場合

  • 最初の条件分岐 (2017行目)では、LONG_MAXなので条件を満たさない。
  • 2つ目の条件分岐 (2026行目)では、for_backgroundフラグが立っているので、wb->work_listが空の場合、処理を中断する。
  • 3つ目の条件分岐 (2034行目)では、for_backgroundフラグが立っているので、wb_over_bg_thresh関数の結果次第では処理を中断する。
  • 4つ目の条件分岐 (2043, 2046目)では、for_backgroundフラグが立っているので、dirtied_beforeを更新する。
  • 5つ目の条件分岐 (2050行目)では、wb->b_ioが空であった場合、queue_io関数を実行する。
  • 6つ目の条件分岐 (2052行目)では、work->sbが未設定であるため、__writeback_inode_wb関数を実行する。

inodeのwrite-backを開始する

上記3つのパターンからwb_writeback関数を直接呼び出した場合には、__writeback_inodes_wb関数が呼び出される。
__writeback_inodes_wb関数の定義は次の通りとなっている。

// 1931:
static long __writeback_inodes_wb(struct bdi_writeback *wb,
                  struct wb_writeback_work *work)
{
    unsigned long start_time = jiffies;
    long wrote = 0;

    while (!list_empty(&wb->b_io)) {
        struct inode *inode = wb_inode(wb->b_io.prev);
        struct super_block *sb = inode->i_sb;

        if (!trylock_super(sb)) {
            /*
            * trylock_super() may fail consistently due to
            * s_umount being grabbed by someone else. Don't use
            * requeue_io() to avoid busy retrying the inode/sb.
            */
            redirty_tail(inode, wb);
            continue;
        }
        wrote += writeback_sb_inodes(sb, wb, work);
        up_read(&sb->s_umount);

        /* refer to the same tests at the end of writeback_sb_inodes */
        if (wrote) {
            if (time_is_before_jiffies(start_time + HZ / 10UL))
                break;
            if (work->nr_pages <= 0)
                break;
        }
    }
    /* Leave any unwritten inodes on b_io */
    return wrote;
}

__writeback_inodes_wb関数は、superblock構造体のs_umount(アンマウント用のセマフォ)を獲得したうえで、writeback_sb_inodes関数を呼び出す。
writeback_sb_inodes関数は次の通りとなっている。

// 1789:
static long writeback_sb_inodes(struct super_block *sb,
                struct bdi_writeback *wb,
                struct wb_writeback_work *work)
{
    struct writeback_control wbc = {
        .sync_mode      = work->sync_mode,
        .tagged_writepages  = work->tagged_writepages,
        .for_kupdate        = work->for_kupdate,
        .for_background     = work->for_background,
        .for_sync       = work->for_sync,
        .range_cyclic       = work->range_cyclic,
        .range_start        = 0,
        .range_end      = LLONG_MAX,
    };
    unsigned long start_time = jiffies;
    long write_chunk;
    long wrote = 0;  /* count both pages and inodes */

    while (!list_empty(&wb->b_io)) {
        struct inode *inode = wb_inode(wb->b_io.prev);
        struct bdi_writeback *tmp_wb;

        if (inode->i_sb != sb) {
            if (work->sb) {
                /*
                * We only want to write back data for this
                * superblock, move all inodes not belonging
                * to it back onto the dirty list.
                */
                redirty_tail(inode, wb);
                continue;
            }

            /*
            * The inode belongs to a different superblock.
            * Bounce back to the caller to unpin this and
            * pin the next superblock.
            */
            break;
        }

        /*
        * Don't bother with new inodes or inodes being freed, first
        * kind does not need periodic writeout yet, and for the latter
        * kind writeout is handled by the freer.
        */
        spin_lock(&inode->i_lock);
        if (inode->i_state & (I_NEW | I_FREEING | I_WILL_FREE)) {
            redirty_tail_locked(inode, wb);
            spin_unlock(&inode->i_lock);
            continue;
        }
        if ((inode->i_state & I_SYNC) && wbc.sync_mode != WB_SYNC_ALL) {
            /*
            * If this inode is locked for writeback and we are not
            * doing writeback-for-data-integrity, move it to
            * b_more_io so that writeback can proceed with the
            * other inodes on s_io.
            *
            * We'll have another go at writing back this inode
            * when we completed a full scan of b_io.
            */
            spin_unlock(&inode->i_lock);
            requeue_io(inode, wb);
            trace_writeback_sb_inodes_requeue(inode);
            continue;
        }
        spin_unlock(&wb->list_lock);

        /*
        * We already requeued the inode if it had I_SYNC set and we
        * are doing WB_SYNC_NONE writeback. So this catches only the
        * WB_SYNC_ALL case.
        */
        if (inode->i_state & I_SYNC) {
            /* Wait for I_SYNC. This function drops i_lock... */
            inode_sleep_on_writeback(inode);
            /* Inode may be gone, start again */
            spin_lock(&wb->list_lock);
            continue;
        }
        inode->i_state |= I_SYNC;
        wbc_attach_and_unlock_inode(&wbc, inode);

        write_chunk = writeback_chunk_size(wb, work);
        wbc.nr_to_write = write_chunk;
        wbc.pages_skipped = 0;

        /*
        * We use I_SYNC to pin the inode in memory. While it is set
        * evict_inode() will wait so the inode cannot be freed.
        */
        __writeback_single_inode(inode, &wbc);

        wbc_detach_inode(&wbc);
        work->nr_pages -= write_chunk - wbc.nr_to_write;
        wrote += write_chunk - wbc.nr_to_write;

        if (need_resched()) {
            /*
            * We're trying to balance between building up a nice
            * long list of IOs to improve our merge rate, and
            * getting those IOs out quickly for anyone throttling
            * in balance_dirty_pages().  cond_resched() doesn't
            * unplug, so get our IOs out the door before we
            * give up the CPU.
            */
            blk_flush_plug(current);
            cond_resched();
        }

        /*
        * Requeue @inode if still dirty.  Be careful as @inode may
        * have been switched to another wb in the meantime.
        */
        tmp_wb = inode_to_wb_and_lock_list(inode);
        spin_lock(&inode->i_lock);
        if (!(inode->i_state & I_DIRTY_ALL))
            wrote++;
        requeue_inode(inode, tmp_wb, &wbc);
        inode_sync_complete(inode);
        spin_unlock(&inode->i_lock);

        if (unlikely(tmp_wb != wb)) {
            spin_unlock(&tmp_wb->list_lock);
            spin_lock(&wb->list_lock);
        }

        /*
        * bail out to wb_writeback() often enough to check
        * background threshold and other termination conditions.
        */
        if (wrote) {
            if (time_is_before_jiffies(start_time + HZ / 10UL))
                break;
            if (work->nr_pages <= 0)
                break;
        }
    }
    return wrote;
}

writeback_sb_inodes関数では、b_ioにつながっているinode構造体を末尾から取り出していき、処理を続けていく。
ただし、次のような状態の場合にも対応しなければならない。

  • inodeを保持しているsuper blockと、bdi_writebackで管理しているsuper blockが異なる場合
  • 新しいinodeや解放されるinodeである場合
  • inodeSYNCフラグが立っているが、bdi_writebackではWB_SYNC_ALLフラグが立っていない場合

一つ目のケースでは、redirty_tail関数によってbdi_writebackで管理しているsuper block構造体で再度Dirtyフラグをセットする。
redirty_tail関数の定義は下記の通りとなっている。

// 1332:
static void redirty_tail(struct inode *inode, struct bdi_writeback *wb)
{
    spin_lock(&inode->i_lock);
    redirty_tail_locked(inode, wb);
    spin_unlock(&inode->i_lock);
}

redirty_tail関数では、inodeのデータを書き換えるのでi_lockでロックする必要がある。
実際にDirtyフラグをセットする処理はredirty_tail_locked関数となっている。

redirty_tail_locked関数は次の通りとなっている。

// 1327:
static void redirty_tail_locked(struct inode *inode, struct bdi_writeback *wb)
{
    assert_spin_locked(&inode->i_lock);

    if (!list_empty(&wb->b_dirty)) {
        struct inode *tail;

        tail = wb_inode(wb->b_dirty.next);
        if (time_before(inode->dirtied_when, tail->dirtied_when))
            inode->dirtied_when = jiffies;
    }
    inode_io_list_move_locked(inode, wb, &wb->b_dirty);
    inode->i_state &= ~I_SYNC_QUEUED;
}

redirty_tail_locaked関数は、inodeb_dirtyリストに追加する。
この時、inodeb_dirtyリストの先頭inodeのDirtyになった時刻より古い場合には、Dirtyになった時刻を更新する。

redirty_tail_locked関数でdirtied_whenが更新される場合

二つ目のケースでも同様に、redirty_tail_lock関数によってbdi_writebackで管理しているsuper block構造体で再度Dirtyフラグをセットする。

三つ目のケースでは同様に、inode_sleep_on_writeback関数によってbdi_writebackで管理しているsuper block構造体で再度Dirtyフラグをセットする。
inode_sleep_on_writeback関数の定義は下記の通りとなっている。

// 1511:
static void inode_sleep_on_writeback(struct inode *inode)
    __releases(inode->i_lock)
{
    DEFINE_WAIT(wait);
    wait_queue_head_t *wqh = bit_waitqueue(&inode->i_state, __I_SYNC);
    int sleep;

    prepare_to_wait(wqh, &wait, TASK_UNINTERRUPTIBLE);
    sleep = inode->i_state & I_SYNC;
    spin_unlock(&inode->i_lock);
    if (sleep)
        schedule();
    finish_wait(wqh, &wait);
}

inode_sleep_on_writeback関数の詳細は省略するが、I_SYNCがクリアされるまでスリープする関数となっている。

以降はinodeをwritebackのための下準備を実施する。

  • inodeI_SYNCフラグをセットする
  • wbc (writebackに関する情報) にwriteback予定のinodeを関連付ける
    • wbc_attach_and_unlock_inode関数の説明は省く
  • 書き込みサイズを設定する。
    • writeback_chunk_size関数の説明は省く

そして、__writeback_single_inode関数により、Dirtyとなっているページとinodeのwritebackを実施する。
__writeback_single_inode関数の定義は次の通りとなっている。

// 1605:
static int
__writeback_single_inode(struct inode *inode, struct writeback_control *wbc)
{
    struct address_space *mapping = inode->i_mapping;
    long nr_to_write = wbc->nr_to_write;
    unsigned dirty;
    int ret;

    WARN_ON(!(inode->i_state & I_SYNC));

    trace_writeback_single_inode_start(inode, wbc, nr_to_write);

    ret = do_writepages(mapping, wbc);

    /*
    * Make sure to wait on the data before writing out the metadata.
    * This is important for filesystems that modify metadata on data
    * I/O completion. We don't do it for sync(2) writeback because it has a
    * separate, external IO completion path and ->sync_fs for guaranteeing
    * inode metadata is written back correctly.
    */
    if (wbc->sync_mode == WB_SYNC_ALL && !wbc->for_sync) {
        int err = filemap_fdatawait(mapping);
        if (ret == 0)
            ret = err;
    }

    /*
    * If the inode has dirty timestamps and we need to write them, call
    * mark_inode_dirty_sync() to notify the filesystem about it and to
    * change I_DIRTY_TIME into I_DIRTY_SYNC.
    */
    if ((inode->i_state & I_DIRTY_TIME) &&
        (wbc->sync_mode == WB_SYNC_ALL ||
         time_after(jiffies, inode->dirtied_time_when +
            dirtytime_expire_interval * HZ))) {
        trace_writeback_lazytime(inode);
        mark_inode_dirty_sync(inode);
    }

    /*
    * Get and clear the dirty flags from i_state.  This needs to be done
    * after calling writepages because some filesystems may redirty the
    * inode during writepages due to delalloc.  It also needs to be done
    * after handling timestamp expiration, as that may dirty the inode too.
    */
    spin_lock(&inode->i_lock);
    dirty = inode->i_state & I_DIRTY;
    inode->i_state &= ~dirty;

    /*
    * Paired with smp_mb() in __mark_inode_dirty().  This allows
    * __mark_inode_dirty() to test i_state without grabbing i_lock -
    * either they see the I_DIRTY bits cleared or we see the dirtied
    * inode.
    *
    * I_DIRTY_PAGES is always cleared together above even if @mapping
    * still has dirty pages.  The flag is reinstated after smp_mb() if
    * necessary.  This guarantees that either __mark_inode_dirty()
    * sees clear I_DIRTY_PAGES or we see PAGECACHE_TAG_DIRTY.
    */
    smp_mb();

    if (mapping_tagged(mapping, PAGECACHE_TAG_DIRTY))
        inode->i_state |= I_DIRTY_PAGES;

    spin_unlock(&inode->i_lock);

    /* Don't write the inode if only I_DIRTY_PAGES was set */
    if (dirty & ~I_DIRTY_PAGES) {
        int err = write_inode(inode, wbc);
        if (ret == 0)
            ret = err;
    }
    trace_writeback_single_inode(inode, wbc, nr_to_write);
    return ret;
}

__writeback_single_inode関数の大きな流れは次の通りとなっている。

  1. do_writepages関数で、inode関連づいているページでDirtyとなっているものをwritebackする。
  2. 必要に応じてDirtyページの書き込みが完了するまで待つ。
  3. 必要に応じてinodeをDirtyにする。
  4. write_inode関数で、inodeをwritebackする。

この中でwritebackの主の処理となっているdo_writepages関数とwrite_inode関数を確認していく。
do_writepages関数の定義は次の通りとなっている。

// 2353:
int do_writepages(struct address_space *mapping, struct writeback_control *wbc)
{
    int ret;
    struct bdi_writeback *wb;

    if (wbc->nr_to_write <= 0)
        return 0;
    wb = inode_to_wb_wbc(mapping->host, wbc);
    wb_bandwidth_estimate_start(wb);
    while (1) {
        if (mapping->a_ops->writepages)
            ret = mapping->a_ops->writepages(mapping, wbc);
        else
            ret = generic_writepages(mapping, wbc);
        if ((ret != -ENOMEM) || (wbc->sync_mode != WB_SYNC_ALL))
            break;
        cond_resched();
        congestion_wait(BLK_RW_ASYNC, HZ/50);
    }
    /*
    * Usually few pages are written by now from those we've just submitted
    * but if there's constant writeback being submitted, this makes sure
    * writeback bandwidth is updated once in a while.
    */
    if (time_is_before_jiffies(READ_ONCE(wb->bw_time_stamp) +
                   BANDWIDTH_INTERVAL))
        wb_update_bandwidth(wb);
    return ret;
}

mappingwritebpagesの関数が定義されている場合にはそれを呼び出し、そうでなければ汎用のgeneric_writepages関数を呼び出す。
今回の場合では、ext2_writepages関数が設定されている。

write_inode関数の定義は次の通りとなっている。

// 1463:
static int write_inode(struct inode *inode, struct writeback_control *wbc)
{
    int ret;

    if (inode->i_sb->s_op->write_inode && !is_bad_inode(inode)) {
        trace_writeback_write_inode_start(inode, wbc);
        ret = inode->i_sb->s_op->write_inode(inode, wbc);
        trace_writeback_write_inode(inode, wbc);
        return ret;
    }
    return 0;
}

write_inode関数では、do_writepages関数と同様にinodeに設定されているwrite_inodeを呼び出す。 今回の場合では、ext2_write_inode関数が設定されている。

Dirtyとなっているページキャッシュとinodeの書き込みを実施後はwriteback_sb_inodes関数に戻り、後処理をする。
後処理で実施する内容は次の通りとなっている。

  1. wbcからinodeの関連を削除する
  2. リストからinodeを削除する
  3. inodeのフラグを更新する

おわりに

本記事では、Linux v5.15におけるwriteback kthread (wb_workfn関数)を確認した。

変更履歴

  • 2022/02/23: 記事公開
  • 2022/09/19: カーネルバージョンを5.15に変更

参考