LeavaTailの日記

LeavaTailの日記

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

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

参考

Visual Studio Code で Linuxカーネルのコードリーディング

概要

Visual Studio Code (VSCode)では、短時間でエディタのセットアップすることができ、テキストエディタとして次の機能が使えることが判明した。

  • 関数定義を参照する
  • 関数呼び出し元を参照する
  • 入力補完機能
  • ホバーによるドキュメント表示
  • シンタックスチェック
  • シンボルの一斉置換

また、VSCodeではデバッグ機能が標準で提供されているので、VSCodeからGDBでアタッチしてデバッグすることもできた。
そこで本記事では、Linuxカーネルのコーディングに必要な設定手順について記載する。

はじめに

Visual Studio Code (VSCode)は、Microsoftが開発・提供しているオープンソースのエディタの一つである。
VSCodeでは、プログラマが必要とする機能 (補完機能やデバッグ機能など) がデフォルトで搭載されていることに加えて、拡張機能が豊富に提供されている。

そこで、VSCodeソースコードリーディングするために、「初期設定に手間がかかるかどうか」と「実際に使ってみてどうなのか」を確認していくことにする。

環境

今回、Windows 10のローカルマシンからVSCodeを利用して、Ubuntu 20.04のリモートマシンにアクセスして、リモートマシン上にあるソースコードやツールチェーンを利用して、コードリーディングをすることを想定する。

VSCode の Remote Development

ここでは、次のバージョンで動作を確認している。

Windows 10 Home

  • VSCode v1.63.2
  • Remote Development v0.21.0

Ubuntu 20.04.3

  • C/C++ Extension Pack v1.1.0

VSCode のインストール

Visual Studio Code のサイト VSCodeは、WIndowsに加えてMacOSLinuxにも対応している。

code.visualstudio.com

Remote Development のインストール

Remote Development は、コンテナ・リモートマシン・WSL内のファイルをアクセスできるVSCode拡張機能の一つ *1 である。

Remote Development のインストール

  1. アクティビティバーにある拡張機能アイコンを選択する
  2. 検索窓に「Remote Development」と入力する
  3. 検索候補にある「Remote Development」を選択する
  4. エディタグループにある「インストール」を選択する

インストールが完了すると、アクティビティバーに「リモート エクスプローラー」が追加される。

SSH 未設定の場合

  1. アクティビティバーにあるリモートエクスプローラーを選択する
  2. SSH Target」に変更する

現在、ローカルマシンにはリモートマシンにアクセスするための設定をしていないため、サイドバーには何も表示されていない。

リモートマシンに公開鍵を追加する

既にローカルマシンからリモートマシンにSSHでリモートアクセスできる場合、公開鍵認証が必要ない場合には省略可能。

ここでは、Windows PowerShell を利用して公開鍵を生成をする。*2

PowerShellSSH用の公開鍵を生成する

  1. ssh-keygen.exeを実行する
  2. 公開鍵の保存先を設定する
  3. 秘密鍵パスフレーズを設定する
  4. 確認用にパスフレーズを再度入力する

コマンドが成功すると指定されたパスに秘密鍵と公開鍵がローカルマシンに生成される。 (ここでは、秘密鍵id_rsa、公開鍵をid_rsa.pubとする)

公開鍵id_rsa.pubを、何らかの手段(SSH, USBメモリ経由, クリップボードなど) でリモートマシンにコピーし、リモートマシンの.ssh/authorized_keysに登録する。

その後、ローカルマシンのVSCodeに戻り、SSHの接続先を設定する。

SSH の接続先設定

  1. SSH TARGETS」にマウスオーバーすると表示される「Configure」を選択する

SSH の接続先を設定する

  1. SSH接続先を記述する
  2. サイドバーに記述したホスト名が出現するので、「Connect to Host in New Window」を選択する

リモートマシンにあるLinuxソースコードを開く

SSH接続に成功した場合、新規ウインドウでリモートマシン内のファイルにアクセスすることができる。

  1. ステータスバーに接続先のホスト名が出力されていることを確認する
  2. アクティビティバーの「エクスプローラー」を選択する
  3. サイドバーから「フォルダーを開く」を選択し、Linuxソースコードのトップディレクトリを選択する

リモートマシンのVSCode拡張機能をインストールする

Linuxカーネルソースコードの多くはCプログラムとなっているため、リモートマシンにはC/C++開発用の拡張機能をインストールしていく。

C/C++開発用の拡張機能をインストールする

  1. アクティビティバーから「拡張機能」を選択する
  2. 検索的に「C++」と入力する
  3. C/C++ extension Pack」(C/C++を開発するための拡張機能がバンドルされている) をインストールする

その後、再読み込みを促されるので、「再読み込み」を選択する。

インテリセンスを設定する

Linuxカーネルでは、かなり複雑な依存関係を持つため、手動でコンパイルフラグを作成することは難しい。
幸いにも、Linuxカーネルではgen_compile_commands.py と呼ばれるスクリプトが用意されている。

    ~/linux:$ ./scripts/clang-tools/gen_compile_commands.py  

ビルド済みのカーネルツリーに対して、上記のコマンドを実行することでcompile_commands.jsonファイルが生成される。

上記の情報を渡してあげるためには、VSCodeの設定を変更する必要がある。

C/C++構成の設定

  1. Ctrl + Shift + Pを選択し、「C/C++; 構成の編集 (JSON)」を選択する

デフォルトのc_cpp_properities.jsonが表示されるので、compileCommandsフィールドを追加する。

// 1:
{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "gnu17",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "linux-gcc-x64",
            "compileCommands": "${workspaceFolder}/compile_commands.json",
        }
    ],
    "version": 4
}

またクロスコンパイルを検討している場合には、compilerPathintelliSenseModeを設定する。

これによりVSCodeでは、プロジェクト内にあるcompile_commands.jsonを用いてインテリセンスを設定する。

VSCodeソースコードを確認する

関数定義を参照する

  • <Alt> + <F12>

関数名や変数名にカーソルを合わせ、<Alt> + <F12> を押下すると、その関数の定義元が表示される。

__ext4_handle_dirty_metadata関数の定義を表示する

関数呼び出し元を参照する

  • <Shift> + <F12>

関数名や変数名にカーソルを合わせ、<Shift> + <F12> を押下すると、その関数の呼び出し元が表示される。

__ext4_handle_dirty_metadata関数の呼び出し元を表示する

入力補完機能

  • <Ctrl> + <Space>

関数や変数を入力すると、自動でポップアップが表示される。また、<Ctrl> + <Space>を入力でも同様にポップアップが表示される。

inode構造体のメンバから入力候補をリストアップする

ホバーによるドキュメント表示

  • マウスオーバー

マウスアップして関数のドキュメント表示する

シンタックスチェック

Cプログラムの構文に合わないようなコードを記述する

シンボルの一斉置換

  • <Ctrl> + <F2>

シンボル (super_block) を一斉に置換する

カーネルのビルド

規模が大きい場合には、コードリーディングだけでプログラムの仕様を完全に理解することは難しい。
そこで、実際にプログラムを動かしてみることで、より詳細な挙動を確認することができる。

まずは、VSCodeからカーネルをビルドできるような設定をしていく。

VSCodeでは、"タスク"と呼ばれる単位で何かしらの処理を実行することができる。
まず初めに、カーネルのビルド/クリーンアップするタスクを追加していく。

タスクの設定

  1. 「ターミナル」を選択する
  2. 「タスクの構成」を選択する

テンプレートからtasks.jsonを生成する

  1. 「テンプレートからtasks.jsonを生成する」を選択する

これにより、新規エディタが立ち上がり、tasks.jsonのテンプレートが表示される。
次のようなmake -j$(nproc) bzImageを実行する Build bzImageタスクとmake cleanを実行するCleanタスクを追加する。

// 1:
{
        // See https://go.microsoft.com/fwlink/?LinkId=733558
        // for the documentation about the tasks.json format
        "version": "2.0.0",
        "type": "shell",
        "echoCommand": true,
        "tasks": [
                {
                        "label": "Build bzImage",
                        "command": "make",
                        "args": [
                                "-j$(nproc)",
                                "bzImage"
                        ],
                        "group": {
                                "kind": "build",
                                "isDefault": true
                        },
                }, 
                {
                        "label": "Clean",
                        "command": "make",
                        "args": [
                                "clean"
                        ],
                        "group": "none"
                },
        ]
}

これにより、ビルドタスクとしてデフォルトでmake -j$(nproc) bzImageが実行されるようになった。

ビルドタスクを実行するためには、<Ctrl> + Shift + B がショートカットとして割り当てられている。

カーネルをビルドする

QEMUで指定したカーネルをブートする

ビルドしたカーネルをお試しで動作確認する場合、QEMU*3でエミュレートすると効率が良い。
そこで、VSCodeからQEMUを利用して、カーネルをブートできるような設定をしておく。

カーネルのビルドと同様にQEMUでブートするようなタスクを作成する。

// 1:
{
        // See https://go.microsoft.com/fwlink/?LinkId=733558
        // for the documentation about the tasks.json format
        "version": "2.0.0",
        "type": "shell",
        "echoCommand": true,
        "tasks": [
                {
                        "label": "Build bzImage",
                        "command": "make",
                        "args": [
                                "-j$(nproc)",
                                "bzImage"
                        ],
                        "group": {
                                "kind": "build",
                                "isDefault": true
                        },
                }, 

                {
                        "label": "Clean",
                        "command": "make",
                        "args": [
                                "clean"
                        ],
                        "group": "none"
                },

                {
                        "label": "Boot kernel in QEMU",
                        "command": "qemu-system-x86_64",
                        "args": [
                                "-kernel",
                                "arch/x86_64/boot/bzImage",
                                "-drive",
                                "file=rootfs.ext4,if=ide,format=raw",
                                "-nographic",
                                "-append",
                                "'root=/dev/sda console=ttyS0 init=/bin/sh'",
                                "-s",
                                "-S"
                        ],
                        "group": "none"
                },
        ]
}

タスクの実行を選択する

  1. タブバーから「ターミナル」を選択する
  2. 「タスクの実行」を選択する

QEMUを実行するタスクを選択する

作成したタスクのラベル (Boot kernel in QEMU) を選択すると、ターミナルに実行結果が出力される。

QEMUでビルドしたカーネルをブートする

GDBでターゲットにアタッチする

上記の手順にて、ビルドしたカーネルを起動させているのでVSCodeからGDBにアタッチしてみる。

VSCodeでは、デバッグ機能が標準として提供されており、GDBを用いたテンプレートが用意されている。
今回は、そのテンプレートを利用して、QEMUで起動させたカーネルにアタッチする。

デバッグのための設定ファイルを作成する

  1. 「実行とデバッグ」を選択する
  2. 「launch.jsonファイルを作成します」を選択する
  3. C++ (GDB/LLDB)」を選択する

127.0.0.1:1234GDBで接続するためにlaunch.jsonを編集する。

// 1:
{
        // IntelliSense を使用して利用可能な属性を学べます。
        // 既存の属性の説明をホバーして表示します。
        // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
        "version": "0.2.0",
        "configurations": [
                {
                        "name": "kernel-debug",
                        "type": "cppdbg",
                        "request": "launch",
                        "miDebuggerServerAddress": "127.0.0.1:1234",
                        "program": "${workspaceFolder}/vmlinux",
                        "args": [],
                        "stopAtEntry": false,
                        "cwd": "${workspaceFolder}",
                        "environment": [],
                        "externalConsole": false,
                        "logging": {
                            "engineLogging": false
                        },
                        "MIMode": "gdb",
                }
        ]
}

上記のファイルを保存した結果、launch.jsonで作成したサイドバーが更新される。

ext4_fill_super関数にブレークポイントを設置する

試しに、ext4_fill_super関数にブレークポイントを設置して、GDBデバッグしてみる。

  1. 行番号の左に選択すると、ブレークポイントが設置される
  2. 「実行とデバッグ」に作成した kernel-debug が選択されているので、「デバッグの開始」を選択する

VSCode からGDBを実行する

おわりに

VSCodeは非常に便利なテキストエディタであり、豊富な機能が提供されている。
今回の場合では、1時間足らずで次のような機能が有効で確認することができた。

  • 関数定義を参照する
  • 関数呼び出し元を参照する
  • 入力補完機能
  • ホバーによるドキュメント表示
  • シンタックスチェック
  • シンボルの一斉置換

変更履歴

  • 2022/2/1: 記事公開

参考

*1:Remote - Containers, Remote -SSH, Remote - WSL の 3つから成る拡張機能となっている

*2:PuTTYやGit Bashなど生成する手段は何でもよい

*3:VirtualBoxなども含む

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

関連記事

概要

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

本章では、writebackワークキューの作成から、ワーカーの追加・取り出しに関係する次の関数を確認した。

  • wb_wakeup_delayed関数
  • wb_wakeup関数
  • wb_queue_work関数

はじめに

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

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

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

本記事では、writebackカーネルスレッドが起床するために用いる機構work Queueについて確認する。

writeback kthreadの概要

メインメモリから記憶装置に書き込む手法は大きく分けて「write back」と「write through」の二通り存在する。

itmanabi.com

PCのようなシステムでは、記憶装置の書き込み上限や速度の観点から「write back」方式を利用することが多い。
ここでは、write back方式におけるデータの書き込み方法を確認する。

wiki.bit-hive.com

write back方式では、ユーザプロセスからファイル書き込みをすると、該当するページキャッシュやinodeキャッシュに対して、Dirtyのフラグを立て処理を終了する。
その後、write back用のカーネルスレッドがDirtyになっているキャッシュの記憶装置への書き込みを実施する。

Linuxでは、主に次のようなタイミングでライトバック処理が実行される。

  • sync(1)fsync(2)が実行されたタイミング
  • ページ回収のタイミング
  • ファイル操作などによる遅延実行のタイミング

writeback用のワーカスレッドの作成

Linux v5.15では、ライトバックを実現するためにWork Queueと呼ばれる機構を用いている。

writebackワークキュー概要

Work Queueでは、指定した処理を指定した時間経過後に呼び出すことのできる仕組みとなっている。(詳細な説明は下記を参照)

www.coins.tsukuba.ac.jp

writeback用のWork Queueは、Linuxカーネルの起動時に下記の関数によって生成される。

// 234:
static int __init default_bdi_init(void)
{
    int err;

    bdi_wq = alloc_workqueue("writeback", WQ_MEM_RECLAIM | WQ_UNBOUND |
                 WQ_SYSFS, 0);
    if (!bdi_wq)
        return -ENOMEM;

    err = bdi_init(&noop_backing_dev_info);

    return err;
}
subsys_initcall(default_bdi_init);

ここで、write back用のWork Queue(bdi_wq)はグローバル変数である。

Work Queueの生成には、alloc_workqueue関数を利用する。

lwn.net

writeback用のWork Queueには下記のフラグを指定している。

  • WQ_MEM_RECLAIM: ページフレームの回収に利用されることがある
  • WQ_UNBOUND: Work Queueを一つのCPUに割り付けない
  • WQ_SYSFS: sysfs (devices/virtual/workqueue/writeback)を生成する

その後、NFSといったblock deviceの実態が存在しないファイルシステムのためにbdi_init関数によって、noop_backing_bdi_initを初期化する。
noop_backing_bdi_initは、backing_dev_info型の変数であり、SDカードなど含めた周辺機器に対する情報を保持する。

default_bdi_init関数で作成されたwriteback用のWork Queueは、下記の3つの関数にて使用される。

  1. wb_wakeup_delayed: dirty_writeback_centisecsで指定されたセンチ秒後に、bdi_writebackに紐づいている関数を実行する。
  2. wb_wakeup: bdi_writebackに紐づいている関数を実行する。
  3. wb_queue_work: Writeback用のキューのリストを末尾に追加して、bdi_writebackに紐づいている関数を実行する。

それぞれの関数は共通して、bdi_writeback型の変数を引数としている。
bdi_writeback型は、それぞれのブロックデバイスにおけるwritebackに関連するパラメータを保持した構造体となっている。 今回の環境では、対象デバイスがSDカードであるのでカーネルがSDカードを認識したタイミング(mmc_rescan)でSDカード用のbdi_writeback型のデータを生成する。

wb_init関数が、**指定時間経過後にwb_workfn関数を呼び出すようにbdi_writebackを初期化する。

// 287:
static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi,
           gfp_t gfp)
{
    int i, err;

    memset(wb, 0, sizeof(*wb));

    if (wb != &bdi->wb)
        bdi_get(bdi);
    wb->bdi = bdi;
    wb->last_old_flush = jiffies;
    INIT_LIST_HEAD(&wb->b_dirty);
    INIT_LIST_HEAD(&wb->b_io);
    INIT_LIST_HEAD(&wb->b_more_io);
    INIT_LIST_HEAD(&wb->b_dirty_time);
    spin_lock_init(&wb->list_lock);

    atomic_set(&wb->writeback_inodes, 0);
    wb->bw_time_stamp = jiffies;
    wb->balanced_dirty_ratelimit = INIT_BW;
    wb->dirty_ratelimit = INIT_BW;
    wb->write_bandwidth = INIT_BW;
    wb->avg_write_bandwidth = INIT_BW;

    spin_lock_init(&wb->work_lock);
    INIT_LIST_HEAD(&wb->work_list);
    INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
    INIT_DELAYED_WORK(&wb->bw_dwork, wb_update_bandwidth_workfn);
    wb->dirty_sleep = jiffies;

    err = fprop_local_init_percpu(&wb->completions, gfp);
    if (err)
        goto out_put_bdi;

    for (i = 0; i < NR_WB_STAT_ITEMS; i++) {
        err = percpu_counter_init(&wb->stat[i], 0, gfp);
        if (err)
            goto out_destroy_stat;
    }

    return 0;

out_destroy_stat:
    while (i--)
        percpu_counter_destroy(&wb->stat[i]);
    fprop_local_destroy_percpu(&wb->completions);
out_put_bdi:
    if (wb != &bdi->wb)
        bdi_put(bdi);
    return err;
}

writebackワークキューにキューを追加

writeback用のWork Queueは以下の3つの関数で使用される。

  • wb_wakeup_delayed
  • wb_wakeup
  • wb_queue_work

wb_wakeup_delayed関数

elixir.bootlin.com

  • wb_workfn: dirty_writeback_intervalが経過した場合に、呼び出す。
  • __mark_inode_dirty: 対象のbdi_writeback型のデータに対して、初めての書き込みの場合のみ呼び出す。

wb_wakeup関数

elixir.bootlin.com

  • inode_switch_wbs_work_fn関数
  • wb_start_writeback関数
    • wakeup_flusher_threads_bdi関数: 下記の関数から呼ばれる
      • laptop_mode_timer_fn関数: Laptop Modeによるタイマのコールバック関数。
    • wakeup_flusher_threads 下記の3つの関数から呼ばれる。
      • ksys_sync関数: syncシステムコールなどから呼ばれる。
      • dirty_writeback_centisecs_handler関数: /proc/sys/vm/dirty_writeback_centisecsを書き換えた場合かつ、経過時間が過ぎている場合に呼ばれる。
      • shrink_inactive_list: 今回は調査を省略。
  • wb_start_background_writeback関数
    • balance_dirty_pages関数: Dirtyの閾値に応じて、カーネルスレッドに (または、現在のプロセス) でwritebackを実施する。
  • wb_workfn関数: 後述するwb_queue_workで追加されたキューがある場合に呼び出す。
  • wakeup_dirtytime_writeback関数: dirty_expire_centisecsミリ秒経過毎に呼び出される。

wb_queue_work関数

elixir.bootlin.com

  • bdi_split_work_to_wbs関数: backing_dev_infoのノードをキューに分割する。
  • cgroup_writeback_by_id関数: 今回は調査を省略。

writebackワークキューからキューを取り出す

ext2ファイルシステムのwrite処理内の__mark_inode_dirtyでは、前述で紹介したwb_wakeup_delayed関数を呼び出す。

leavatail.hatenablog.com

// 263:
void wb_wakeup_delayed(struct bdi_writeback *wb)
{
    unsigned long timeout;

    timeout = msecs_to_jiffies(dirty_writeback_interval * 10);
    spin_lock_bh(&wb->work_lock);
    if (test_bit(WB_registered, &wb->state))
        queue_delayed_work(bdi_wq, &wb->dwork, timeout);
    spin_unlock_bh(&wb->work_lock);
}

おわりに

本記事では、Linux v5.15におけるwriteback用のWorkqueueを解説した。

変更履歴

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

参考

Ubuntu-base-20.04 を QEMU から NFSRootで起動する

概要

QEMUでエミュレートした armhfプロセッサ に Linuxを起動させ 、Ubuntu-base-20.04イメージをNFS mountする。

はじめに

Ubuntu baseは、最低限のUbuntuのrootfsである。 そのため、LXCやDockerのコンテナイメージで使われることがある。

一方でrootfsをNFS server上に置き、rootfsをNFS経由でmountする手法は、(主に組込み機器の)開発において有効である。*1

そこで、本記事では、QEMU上でarmhfプロセッサをエミュレートさせて、Ubuntu-base-20.04をrootfsとしてNFS mountする方法を記述する。

QEMUでarmhf用 Linuxカーネルを起動させる方法は、下記の記事を流用する。

leavatail.hatenablog.com

leavatail.hatenablog.com

実行環境

Host PCは下記のような環境で実験する。

項目 概要
Host PC (OS) Ubuntu 20.04.02
Architecture x86_64
QEMU qemu v5.1.0
nfs-kernlel-server 1:1.3.4-2.5ubuntu3.4
buildroot buildroot-2020.11

上記の環境で、QEMUを実行しゲストOSとしてLinux v5.10.1を起動させる。

動作環境の全体像

手順

  1. Host PCに下記パッケージをインストールする。

     $ sudo apt-get install nfs-kernel-server
    
  2. Host PCでNFSサーバの設定する

     $ echo "/srv/rootfs     127.0.0.1(rw,wdelay,insecure,no_root_squash,no_subtree_check,sec=sys,rw,insecure,no_root_squash,no_all_squash)" | sudo tee -a /etc/exports
     $ sudo exportfs -v
    
  3. Host PCにUbuntu-base-20.04 のイメージをダウンロードする

     $ wget -P /tmp http://cdimage.ubuntu.com/ubuntu-base/releases/20.04/release/ubuntu-base-20.04.3-base-armhf.tar.gz
     $ sudo tar xf /tmp/ubuntu-base-20.04.3-base-armhf.tar.gz -C /srv/rootfs/armhf
    
  4. [任意] Host PCに下記パッケージをインストールする。

     $ sudo apt-get install qemu-user-static
    
  5. [任意] Ubuntu-base-20.04 の rootfs に qemu-arm-staticをコピーする

     $ sudo update-binfmts --display | grep arm
     qemu-arm (enabled):
      interpreter = /usr/bin/qemu-arm-static
     qemu-armeb (enabled):
      interpreter = /usr/bin/qemu-armeb-static
    
     $ sudo cp /usr/bin/qemu-arm-static /srv/rootfs/armhf/usr/bin/    
    
  6. rootfs の initスクリプトを追加する

#!/bin/bash

PATH=/sbin:/bin:/usr/sbin:/usr/sbin

set -em
trap rescue ERR

function rescue {
        echo -e "\e[31m NG \e[m"
        exec /bin/sh
}

echo -n "[init] Connect console:"
if (exec 0</dev/console) 2>/dev/null; then
    exec 0</dev/console
    exec 1>/dev/console
    exec 2>/dev/console
fi
echo -e "\e[32m OK \e[m"

echo -n "[init] Mount filesystem:"
# mount -t devtmpfs udev /dev
mount -t proc /proc /proc
mount -t sysfs sysfs /sys
echo -e "\e[32m OK \e[m"

echo -n "[init] Mount filesystem additionaly:"
mount -t tmpfs -o size=32m tmpfs /tmp
if [ -d "/dev/pts" ]; then
        mount -t devpts /dev/pts /dev/pts
fi

if [ -d "/sys/kernel/debug" ]; then
        mount -t debugfs none /sys/kernel/debug
fi
echo -e "\e[32m OK \e[m"

exec setsid /sbin/agetty --long-hostname --autologin root -s ttyAMA0 115200,38400,9600 linux

起動確認

$ sudo qemu-system-arm -M vexpress-a9 \
        -smp 1 \
        -m 1024 \
        -kernel output/images/zImage \
        -dtb output/images/vexpress-v2p-ca9.dtb \
        -append "console=ttyAMA0,115200 rootwait ip=on root=/dev/nfs nfsroot=/srv/rootfs/armhf user_debug=31 rw" \
        -net nic,model=lan9118 \
        -net user \
        -nographic

... 

VFS: Mounted root (nfs filesystem) on device 0:14.
devtmpfs: mounted
Freeing unused kernel memory: 1024K
Run /sbin/init as init process
random: fast init done
[init] Connect console: OK 
[init] Mount filesystem: OK 
[init] Mount filesystem additionaly: OK 

Ubuntu 20.04.3 LTS 10.0.2.15 ttyAMA0

10.0.2.15 login: root (automatic login)

Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.10.1 armv7l)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.
Last login: Sun Oct 10 11:57:52 JST 2021 on ttyAMA0
root@10:~# 

補足

ここでは、手順や起動確認で解説できなかった内容について補足する。

NFSの設定について

QEMUのネットワークは複数の設定ができる。

Documentation/Networking - QEMU

今回は、User Networking を使用しているため、ゲストからホストはNATで扱われる。
そのため、NFSの設定でローカルループバックアドレスに任意のポートから受け付けるようにしている。*2

rootfsの更新について

x86_64アーキテクチャから、armhfのrootfsを直接chrootすることはできない。
そこで、qemu-user-staticを介することでrootfsを操作できるようにしている。

このようにしておくことで、Ubuntu-base 20.04に、systemdがインストールするなどが容易にできる。
その場合は、名前解決できるように適切に設定しておく必要はある。

  # echo  nameserver 8.8.8.8 >> /etc/resolv.conf

おわりに

本記事では、QEMU上でarmhfプロセッサをエミュレートさせて、Ubuntu-base-20.04をrootfsとしてNFS mountする方法を記述した。

Ubuntu baseイメージは最小のrootfsであるため、systemdを追加でインストールしたりなどカスタマイズ性が高い。

変更履歴

  • 2021/10/10: 記事公開

参考

QEMUの設定関連

Ubuntu-base関連

initramfs関連

rootfs関連

*1:今回はQEMUで起動させているので、恩恵は小さいが

*2:ポートフォワーディングやブリッジ接続をすれば、ここも@変えることはできる

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

関連記事

概要

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

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

はじめに

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

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

処理シーケンス図としては、下記の赤枠部分が該当する。

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

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

write_endの概略

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

このwrite_begin操作やwrite_end操作はページキャッシュに対する操作となっており、address_space_opearationsのメンバの一つとして定義される。
write_end操作は、write_iter操作(で呼び出されるgeneric_perform_write関数)から呼び出される。

// 3781:
        status = a_ops->write_end(file, mapping, pos, bytes, copied,
                        page, fsdata);

write_end操作開始時の全体イメージ

変数名
file オープンしたファイル
mapping ファイルが持っているページキャッシュのXArray(radix-tree)
pos 書き込み先の位置
bytes 書き込むバイト数
copied ページキャッシュにコピーされたバイト数
page 取得したページを格納する
fsdata ext2ファイルシステムでは使用しない

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

// 897:
static int ext2_write_end(struct file *file, struct address_space *mapping,
            loff_t pos, unsigned len, unsigned copied,
            struct page *page, void *fsdata)
{
    int ret;

    ret = generic_write_end(file, mapping, pos, len, copied, page, fsdata);
    if (ret < len)
        ext2_write_failed(mapping, pos + len);
    return ret;
}

ext2ファイルシステムでは、汎用APIgeneric_write_end関数を呼び出す。

// 2168:
int generic_write_end(struct file *file, struct address_space *mapping,
            loff_t pos, unsigned len, unsigned copied,
            struct page *page, void *fsdata)
{
    struct inode *inode = mapping->host;
    loff_t old_size = inode->i_size;
    bool i_size_changed = false;

    copied = block_write_end(file, mapping, pos, len, copied, page, fsdata);

    /*
    * No need to use i_size_read() here, the i_size cannot change under us
    * because we hold i_rwsem.
    *
    * But it's important to update i_size while still holding page lock:
    * page writeout could otherwise come in and zero beyond i_size.
    */
    if (pos + copied > inode->i_size) {
        i_size_write(inode, pos + copied);
        i_size_changed = true;
    }

    unlock_page(page);
    put_page(page);

    if (old_size < pos)
        pagecache_isize_extended(inode, old_size, pos);
    /*
    * Don't mark the inode dirty under page lock. First, it unnecessarily
    * makes the holding time of page lock longer. Second, it forces lock
    * ordering of page lock and transaction start for journaling
    * filesystems.
    */
    if (i_size_changed)
        mark_inode_dirty(inode);
    return copied;
}

generic_write_end関数の処理は下記の通りとなっている。

  • ページキャッシュのフラグを更新する
  • バッファキャッシュのフラグを更新する
  • ファイルサイズを更新する

まず初めに、メインとなるblock_write_end関数から確認していく。

キャッシュのフラグの更新

block_write_end関数は下記のような定義となっている。

// 2132:
int block_write_end(struct file *file, struct address_space *mapping,
            loff_t pos, unsigned len, unsigned copied,
            struct page *page, void *fsdata)
{
    struct inode *inode = mapping->host;
    unsigned start;

    start = pos & (PAGE_SIZE - 1);

    if (unlikely(copied < len)) {
        /*
        * The buffers that were written will now be uptodate, so we
        * don't have to worry about a readpage reading them and
        * overwriting a partial write. However if we have encountered
        * a short write and only partially written into a buffer, it
        * will not be marked uptodate, so a readpage might come in and
        * destroy our partial write.
        *
        * Do the simplest thing, and just treat any short write to a
        * non uptodate page as a zero-length write, and force the
        * caller to redo the whole thing.
        */
        if (!PageUptodate(page))
            copied = 0;

        page_zero_new_buffers(page, start+copied, start+len);
    }
    flush_dcache_page(page);

    /* This could be a short (even 0-length) commit */
    __block_commit_write(inode, page, start, start+copied);

    return copied;
}

別スレッドでなどバッファに更新があった場合は、2141行で新規バッファキャッシュの確保など実施する。

今回のケースでは、flush_dcache_page関数でdcacheをフラッシュしたのちに、__block_commit_write関数を呼び出す。

flush_dcache_page関数については下記の記事にて解説済み。

leavatail.hatenablog.com

__block_commit_write関数には、書き込み先のinodeと、書き込み対象のページキャッシュpage, 書き込み対象が記載されているオフセット (ページキャッシュの先頭から)start, 書き込み対象が記載されている末尾 start+copiedを渡す。

__block_commit_write関数の定義は下記の定義となっている。

// 2064:
static int __block_commit_write(struct inode *inode, struct page *page,
        unsigned from, unsigned to)
{
    unsigned block_start, block_end;
    int partial = 0;
    unsigned blocksize;
    struct buffer_head *bh, *head;

    bh = head = page_buffers(page);
    blocksize = bh->b_size;

    block_start = 0;
    do {
        block_end = block_start + blocksize;
        if (block_end <= from || block_start >= to) {
            if (!buffer_uptodate(bh))
                partial = 1;
        } else {
            set_buffer_uptodate(bh);
            mark_buffer_dirty(bh);
        }
        if (buffer_new(bh))
            clear_buffer_new(bh);

        block_start = block_end;
        bh = bh->b_this_page;
    } while (bh != head);

    /*
    * If this is a partial write which happened to make all buffers
    * uptodate then we can optimize away a bogus readpage() for
    * the next read(). Here we 'discover' whether the page went
    * uptodate as a result of this (potentially partial) write.
    */
    if (!partial)
        SetPageUptodate(page);
    return 0;
}

__block_commit_write関数では、各バッファキャッシュのフラグを更新していく。
また、ページキャッシュstruct page構造体からバッファキャッシュstruct buffer_headの取得には、page_buffersマクロを使用する。

// 141:
/* If we *know* page->private refers to buffer_heads */
#define page_buffers(page)                  \
   ({                            \
       BUG_ON(!PagePrivate(page));           \
       ((struct buffer_head *)page_private(page)); \
   })

PagePrivateマクロにより、struct page構造体にprivateが設定されているかどうか確認する。
struct buffer_head構造体はprivateに関連付けれらているため、これがFalseとなる場合はバグであるのでカーネルパニックさせる。

// 260:
#define page_private(page)      ((page)->private)

struct page構造体に紐づけられている各struct buffer_head構造体に対して、フラグの更新をする。
更新されたデータにBH_DirtyBH_Uptodateのフラグを付与する。

バッファキャッシュのフラグを更新する

BH_Dirtyのフラグを更新する際には、mark_buffer_dirty関数で実施する。 mark_buffer_dirty関数の定義は下記の通りとなっている。

// 1082:
void mark_buffer_dirty(struct buffer_head *bh)
{
    WARN_ON_ONCE(!buffer_uptodate(bh));

    trace_block_dirty_buffer(bh);

    /*
    * Very *carefully* optimize the it-is-already-dirty case.
    *
    * Don't let the final "is it dirty" escape to before we
    * perhaps modified the buffer.
    */
    if (buffer_dirty(bh)) {
        smp_mb();
        if (buffer_dirty(bh))
            return;
    }

    if (!test_set_buffer_dirty(bh)) {
        struct page *page = bh->b_page;
        struct address_space *mapping = NULL;

        lock_page_memcg(page);
        if (!TestSetPageDirty(page)) {
            mapping = page_mapping(page);
            if (mapping)
                __set_page_dirty(page, mapping, 0);
        }
        unlock_page_memcg(page);
        if (mapping)
            __mark_inode_dirty(mapping->host, I_DIRTY_PAGES);
    }
}
  • buffer_dirtyマクロ: struct buffer_headb_stateBH_Dirtyが設定されていた場合にTrueを返す。
  • test_set_buffer_dirrtyマクロ: struct buffer_headb_stateBH_Dirtyをセットする。
    • マクロ実施前にBH_Dirtyが設定されていない場合はFalseを返す。
    • マクロ実施後にBH_Dirtyが設定されていない場合はTrueを返す。

mark_buffer_dirty関数では、初めに別のスレッドで同一のstruct buffer_headがdirtyになっていた場合は、何もせずに処理を終了させる。

cgroup系の処理(lock_page_memcgunlock_page_memcg)については、省略する。

test_set_buffer_dirtyTestSetPageDirtyによって、バッファキャッシュとページキャッシュにDirtyフラグを付与する。

バッファキャッシュとページキャッシュにDirtyフラグを付与する

__mark_inode_dirty関数は、writeback用のリストに対象のinodeを追加する。
ただし、下記の記事にてリストに追加されているので、ここでは何も実施しない。

leavatail.hatenablog.com

下記のコードで、was_dirtyI_DIRTY_SYNC (0x1)が代入される。

// 2426:
    if ((inode->i_state & flags) != flags) {
        const int was_dirty = inode->i_state & I_DIRTY;

その後の処理は、!was_dirtyで判定されるため、このタイミングでは処理をせず戻る。

// 2460:
        /*
        * If the inode was already on b_dirty/b_io/b_more_io, don't
        * reposition it (that would break b_dirty time-ordering).
        */
        if (!was_dirty) {
        ...

ファイルサイズの更新

ファイルのもともとのサイズinode->i_sizecopiedだけ増えるので、ファイルサイズの更新処理が入る。

// 2178:
    /*
    * No need to use i_size_read() here, the i_size cannot change under us
    * because we hold i_rwsem.
    *
    * But it's important to update i_size while still holding page lock:
    * page writeout could otherwise come in and zero beyond i_size.
    */
    if (pos + copied > inode->i_size) {
        i_size_write(inode, pos + copied);
        i_size_changed = true;
    }

i_size_write関数は、割り込みなどを考慮してinode->i_sizeを更新する。
i_size_read関数と内容が似ているため、ここでは省略する。

おわりに

本記事では、ext2ファイルシステムwrite_end操作(ext2_write_end)を解説した。
write_end操作は、write_iter操作で書き込んだキャッシュにDirtyフラグを立てて、ファイルのサイズの更新するための操作である。

変更履歴

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

参考

SDカードの読み込み/書き込み性能を fio の測定結果から比較する

概要

SDカードには規格が定義されており、それぞれの製品には準拠した規格が記載されていることが多い。
本記事では、4種類のSDカードと2種類のSDカードリーダを用いて、それぞれ読み込み/書き込み性能を測定した。

その結果、下記のような傾向が見られた。

  • ファイルシステムを介することによる書き込み性能が大きく低下する
  • SDカードによって性能が最も発揮できるブロックサイズが異なる
  • Raspberry Pi 4の場合だと、並列処理によりパフォーマンスは低下する
  • UHS-II 非対応のカードリーダの場合、UHS-II対応のSDカードの性能を発揮できない

はじめに

SDカードには規格が定義されており、それぞれの製品には準拠した規格が記載されていることが多い。
ユースケースに応じて、適切なSDカードを使用することでその製品を最大限に生かすことができる。 しかし、これら規格は数が多く複雑なものが多い。

そこで複数のSDカードを計測することで、実測値と規格の関係性を調査する。

調査には、未使用のSDカードを4種類を使用し、Raspberriy Pi 4上で計測する。 このとき、UHS-II の影響も調査するために2種類のカードリーダ(UHS-II 未対応、UHS-II 対応) を用意し、 UHS-II対応カードリーダとの組み合わせも考慮する。

目的

  1. ファイルシステムを介することによるオーバーヘッドを計測する
  2. SDカードの違いによってパフォーマンスが異なることを確認する
  3. ブロックサイズの違いによってパフォーマンスが異なることを確認する
  4. 並列処理させることでパフォーマンスが異なることを確認する
  5. UHS-II 非対応のカードリーダがボトルネックになることを確認する

実行環境

Raspberry Pi 4 Model B (Raspberry Pi 4) は microSDカード経由でRaspberry Pi OSを起動させる。 また、Raspbery Pi 4のUSB3.0ポートにSDカードリーダを接続し、そこから計測対象SDカードを挿入する。

計測環境の概要

ここで使用するRaspberry Pi 4のスペックについて、必要な情報だけ抜粋したものを下記に示す。

項目 Raspberry Pi 4
CPU Cortex-A72 (ARM v8) 1.5GHz
メモリ 4GB LPDDR4-3200
OS Raspberry Pi OS (May 7th 2021)
OS格納先ストレージ microSDHC 16GB Class10 UHS-1
ケース 陽極酸化アルミニウム製ヒートシンクケース

今回は、SDカードリーダによる性能の差を図るために下記の二つを用意した。

  • BSCR27U3BK: UHS-IIに対応していないSDカードリーダ
  • MRW-S1: UHS-IIに対応したSDカードリーダ

上記の環境に対して、下記4種類のSDカードを計測する。(カタログに記載されていないものは、空欄としている)

ELITE SDXC UHS-I SanDisk Extreme Pro(並行輸入品) SF-E64 SF-M64T
SDスピードクラス CLASS10 CLASS10 CLASS10
UHSスピードクラス U3 U3 U3 U3
ビデオスピードクラス V30 V30 V30 V60
インタフェース UHS-I UHS-I UHS-II UHS-II
容量 64GB 64GB 64GB 64GB
最大読み出し速度 90 MB/s 170 MB/s 270 MB/s 277 MB/s
最大書き込み速度 45MB/s 90 MB/s 70 MB/s 150 MB/s

計測方法

計測には、Flexible I/O tester-3.12 (fio) を用いて、読み込みと書き込みの性能を計測する。
fioでは、パラメータを指定することで様々な計測を実施することができる。
本実験では、bsnumjobsfilename の値を変更するような計測を実施する。

  • bs: ユーザプログラムから読み込み・書き込みをするの一回当たりのサイズ。4KB・1MB・256MBに変更して確認する。
  • numjobs: 計測プログラムを並列して複数個のプロセス/スレッドで実行する。1・4・8に変更して確認する。
  • filename: 計測プログラムの読み込み・書き込み先のファイルを指定する。「ファイルシステム以下のファイル」と「デバイスファイル」の二つを確認する。

そこで、下記のようなパラメータファイルを用意し、これらの値を書き換えながら計測することにした。

[global]
size=1G
directory=/mnt
runtime=300
invalidate=1
group_reporting=1
numjobs=1 # FIXME

[write]
name=write
description="WRITE TEST"
unlink=0
rw=randwrite # randread
ioengine=sync
exec_prerun="./prerun.sh"
#exec_postrun="./postrun.sh"
#filename=/dev/sda1
time_based
bs=4K # FIXME
// 1:
#!/bin/sh

echo 3 > /proc/sys/vm/drop_caches

また、SDカードの状態による測定値のブレを最小限に抑えるために、測定前にSDカードをSD Card Formatter 5.0.1の上書きフォーマットを実施しておく。

実験結果

ファイルシステムによるオーバーヘッド

ファイルシステムを介することによる書き込み性能の比較

  • ファイルシステムへの書き込みは、デバイスファイルに直接書き込む場合と比較して、オーバーヘッドが出ている。
    • さらに、並列処理されている場合のオーバーヘッドは顕著に出ている。

ファイルシステムを介することによる読み込み性能の比較

SDカードの違い

SDカードの違いによる書き込み性能の比較

  • 一部例外はあったが、カタログに載っている最大書き込み速度と傾向は同じになっている。
    • SF-M64Tは、ブロックサイズが小さい場合に性能が出ていない傾向にあった。

SDカードの違いによる読み込み性能の比較

  • 一部例外はあったが、カタログに載っている最大読み込み速度と傾向は同じになっている。
    • SF-E64はブロックサイズが小さい場合に性能が出ていない傾向にあった。

ブロックサイズの違い

ブロックサイズの違いによる書き込み性能の比較

  • ブロックサイズが大きくすれば、書き込み性能は向上した (~256MB)

ブロックサイズの違いによる読み込み性能の比較

  • ブロックサイズが大きくすれば、読み込み性能は向上した (~256MB)

UHS-II 非対応によるオーバーヘッド

UHS-II非対応カードリーダによる書き込み性能の比較

UHS-II非対応カードリーダによる読み込み性能の比較

  • 読み込み性能・書き込み性能の双方、UHS-II非対応のカードリーダでは UHS-Iの転送速度104MB/sより下回っている

並列処理によるオーバーヘッド

並列処理による書き込み性能の比較

並列処理による読み込み性能の比較

  • 基本的に並列処理数を増やすことで性能が落ちている。

結論

  • ファイルシステムを介することによる書き込み性能が大きく低下する傾向がある
  • SDカードの違いによってパフォーマンスは異なる
    • また、SDカードによって性能が最も発揮できるブロックサイズが異なる
  • Raspberry Pi 4の場合だと、並列処理によりパフォーマンスは低下する
  • UHS-II 非対応のカードリーダの場合、UHS-II対応のSDカードの性能を発揮できない

おわりに

本記事では、さまざまな環境で読み込み書き込み性能を測定した。
SDカード・ブロックサイズ・並列処理といった要因がパフォーマンスに大きく影響を与えていた。

変更履歴

  • 2021/9/11: 記事公開
  • 2023/3/5: 記事タイトル「様々なSDカードの読み込み/書き込み性能の実測値を計測する」から変更

参考

補足

実験で得られたデータを本章に残す。

SD card Card Reader Block size jobs io File performance
ELITE SDXC UHS-I MRW-S1 4KB 1 randwrite Default 1409KB/s
ELITE SDXC UHS-I MRW-S1 4KB 4 randwrite Default 483KB/s
ELITE SDXC UHS-I MRW-S1 4KB 8 randwrite Default 567B/s
ELITE SDXC UHS-I MRW-S1 1MB 1 randwrite Default 23.1MB/s
ELITE SDXC UHS-I MRW-S1 1MB 4 randwrite Default 9877KB/s
ELITE SDXC UHS-I MRW-S1 1MB 8 randwrite Default 148KB/s
ELITE SDXC UHS-I MRW-S1 256MB 1 randwrite Default 32.0MB/s
ELITE SDXC UHS-I MRW-S1 256MB 4 randwrite Default 19.4MB/s
ELITE SDXC UHS-I MRW-S1 256MB 8 randwrite Default 12.8MB/s
ELITE SDXC UHS-I BSCR27U3BK 256MB 1 randwrite Default 31.2MB/s
ELITE SDXC UHS-I BSCR27U3BK 256MB 4 randwrite Default 20.2MB/s
ELITE SDXC UHS-I BSCR27U3BK 256MB 8 randwrite Default 13.4MB/s
ELITE SDXC UHS-I MRW-S1 4KB 1 randwrite /dev/sda1 1556KB/s
ELITE SDXC UHS-I MRW-S1 4KB 4 randwrite /dev/sda1 1523KB/s
ELITE SDXC UHS-I MRW-S1 4KB 8 randwrite /dev/sda1 1573MB/s
ELITE SDXC UHS-I MRW-S1 1MB 1 randwrite /dev/sda1 29.1MB/s
ELITE SDXC UHS-I MRW-S1 1MB 4 randwrite /dev/sda1 26.9MB/s
ELITE SDXC UHS-I MRW-S1 1MB 8 randwrite /dev/sda1 25.7MB/s
ELITE SDXC UHS-I MRW-S1 256MB 1 randwrite /dev/sda1 32.0MB/s
ELITE SDXC UHS-I MRW-S1 256MB 4 randwrite /dev/sda1 27.0MB/s
ELITE SDXC UHS-I MRW-S1 256MB 8 randwrite /dev/sda1 24.9MB/s
ELITE SDXC UHS-I MRW-S1 4KB 1 randread Default 4685KB/s
ELITE SDXC UHS-I MRW-S1 4KB 4 randread Default 4906KB/s
ELITE SDXC UHS-I MRW-S1 4KB 8 randread Default 4862KB/s
ELITE SDXC UHS-I MRW-S1 1MB 1 randread Default 71.4MB/s
ELITE SDXC UHS-I MRW-S1 1MB 4 randread Default 59.3MB/s
ELITE SDXC UHS-I MRW-S1 1MB 8 randread Default 61.0MB/s
ELITE SDXC UHS-I MRW-S1 256MB 1 randread Default 79.7MB/s
ELITE SDXC UHS-I MRW-S1 256MB 4 randread Default 66.6MB/s
ELITE SDXC UHS-I MRW-S1 256MB 8 randread Default 65.5MB/s
ELITE SDXC UHS-I BSCR27U3BK 256MB 1 randread Default 60.2MB/s
ELITE SDXC UHS-I BSCR27U3BK 256MB 4 randread Default 58.0MB/s
ELITE SDXC UHS-I BSCR27U3BK 256MB 8 randread Default 58.2MB/s
ELITE SDXC UHS-I MRW-S1 4KB 1 randread /dev/sda1 4644KB/s
ELITE SDXC UHS-I MRW-S1 4KB 4 randread /dev/sda1 4668KB/s
ELITE SDXC UHS-I MRW-S1 4KB 8 randread /dev/sda1 4850MB/s
ELITE SDXC UHS-I MRW-S1 1MB 1 randread /dev/sda1 68.8MB/s
ELITE SDXC UHS-I MRW-S1 1MB 4 randread /dev/sda1 54.8MB/s
ELITE SDXC UHS-I MRW-S1 1MB 8 randread /dev/sda1 54.1MB/s
ELITE SDXC UHS-I MRW-S1 256MB 1 randread /dev/sda1 83.1MB/s
ELITE SDXC UHS-I MRW-S1 256MB 4 randread /dev/sda1 65.0MB/s
ELITE SDXC UHS-I MRW-S1 256MB 8 randread /dev/sda1 62.8MB/s
SanDisk Extreme Pro MRW-S1 4KB 1 randwrite Default 1950KB/s
SanDisk Extreme Pro MRW-S1 4KB 4 randwrite Default 462KB/s
SanDisk Extreme Pro MRW-S1 4KB 8 randwrite Default 369B/s
SanDisk Extreme Pro MRW-S1 1MB 1 randwrite Default 49.7MB/s
SanDisk Extreme Pro MRW-S1 1MB 4 randwrite Default 7411KB/s
SanDisk Extreme Pro MRW-S1 1MB 8 randwrite Default 85.4KB/s
SanDisk Extreme Pro MRW-S1 256MB 1 randwrite Default 72.9MB/s
SanDisk Extreme Pro MRW-S1 256MB 4 randwrite Default 17.5KB/s
SanDisk Extreme Pro MRW-S1 256MB 8 randwrite Default 9782KB/s
SanDisk Extreme Pro MRW-S1 4KB 1 randwrite /dev/sda1 1907KB/s
SanDisk Extreme Pro MRW-S1 4KB 4 randwrite /dev/sda1 1466KB/s
SanDisk Extreme Pro MRW-S1 4KB 8 randwrite /dev/sda1 2005KB/s
SanDisk Extreme Pro MRW-S1 1MB 1 randwrite /dev/sda1 85.1MB/s
SanDisk Extreme Pro MRW-S1 1MB 4 randwrite /dev/sda1 66.0MB/s
SanDisk Extreme Pro MRW-S1 1MB 8 randwrite /dev/sda1 62.1MB/s
SanDisk Extreme Pro MRW-S1 256MB 1 randwrite /dev/sda1 73.6MB/s
SanDisk Extreme Pro MRW-S1 256MB 4 randwrite /dev/sda1 35.6MB/s
SanDisk Extreme Pro MRW-S1 256MB 8 randwrite /dev/sda1 31.6MB/s
SanDisk Extreme Pro MRW-S1 4KB 1 randread Default 5513KB/s
SanDisk Extreme Pro MRW-S1 4KB 4 randread Default 5753KB/s
SanDisk Extreme Pro MRW-S1 4KB 8 randread Default 5720KB/s
SanDisk Extreme Pro MRW-S1 1MB 1 randread Default 72MB/s
SanDisk Extreme Pro MRW-S1 1MB 4 randread Default 48.1MB/s
SanDisk Extreme Pro MRW-S1 1MB 4 randread Default 54.6MB/s
SanDisk Extreme Pro MRW-S1 256MB 1 randread Default 83.0MB/s
SanDisk Extreme Pro MRW-S1 256MB 4 randread Default 66.1MB/s
SanDisk Extreme Pro MRW-S1 256MB 8 randread Default 57.3MB/s
SanDisk Extreme Pro MRW-S1 4KB 1 randread /dev/sda1 5431KB/s
SanDisk Extreme Pro MRW-S1 4KB 4 randread /dev/sda1 5724KB/s
SanDisk Extreme Pro MRW-S1 4KB 8 randread /dev/sda1 5722KB/s
SanDisk Extreme Pro MRW-S1 1MB 1 randread /dev/sda1 64.3MB/s
SanDisk Extreme Pro MRW-S1 1MB 4 randread /dev/sda1 55.0MB/s
SanDisk Extreme Pro MRW-S1 1MB 8 randread /dev/sda1 55.9MB/s
SanDisk Extreme Pro MRW-S1 256MB 1 randread /dev/sda1 80.0MB/s
SanDisk Extreme Pro MRW-S1 256MB 4 randread /dev/sda1 63.2MB/s
SanDisk Extreme Pro MRW-S1 256MB 8 randread /dev/sda1 64.7MB/s
SF-E64 MRW-S1 4KB 1 randwrite Default 1456KB/s
SF-E64 MRW-S1 4KB 4 randwrite Default 1976KB/s
SF-E64 MRW-S1 4KB 8 randwrite Default 5947B/s
SF-E64 MRW-S1 1MB 1 randwrite Default 41.3MB/s
SF-E64 MRW-S1 1MB 4 randwrite Default 14.2MB/s
SF-E64 MRW-S1 1MB 8 randwrite Default 647KB/s
SF-E64 MRW-S1 256MB 1 randwrite Default 60.3MB/s
SF-E64 MRW-S1 256MB 4 randwrite Default 25.3MB/s
SF-E64 MRW-S1 256MB 8 randwrite Default 15.8MB/s
SF-E64 MRW-S1 4KB 1 randread Default 14.5MB/s
SF-E64 MRW-S1 4KB 4 randread Default 8856KB/s
SF-E64 MRW-S1 4KB 8 randread Default 8758KB/s
SF-E64 MRW-S1 1MB 1 randread Default 29.1MB/s
SF-E64 MRW-S1 1MB 4 randread Default 24.9MB/s
SF-E64 MRW-S1 1MB 8 randread Default 23.9MB/s
SF-E64 MRW-S1 256MB 1 randread Default 164MB/s
SF-E64 MRW-S1 256MB 4 randread Default 132MB/s
SF-E64 MRW-S1 256MB 8 randread Default 130MB/s
SF-M64T MRW-S1 4KB 1 randwrite Default 920KB/s
SF-M64T MRW-S1 4KB 4 randwrite Default 67B/s
SF-M64T MRW-S1 4KB 8 randwrite Default 44B/s
SF-M64T MRW-S1 1MB 1 randwrite Default 10.8MB/s
SF-M64T MRW-S1 1MB 4 randwrite Default 19.1KB/s
SF-M64T MRW-S1 1MB 8 randwrite Default 11.0KB/s
SF-M64T MRW-S1 256MB 1 randwrite Default 129MB/s
SF-M64T MRW-S1 256MB 4 randwrite Default 3281KB/s
SF-M64T MRW-S1 256MB 8 randwrite Default 2738 KB/s
SF-M64T BSCR27U3BK 256MB 1 randwrite Default 69.6MB/s
SF-M64T BSCR27U3BK 256MB 4 randwrite Default 2786KB/s
SF-M64T BSCR27U3BK 256MB 8 randwrite Default 2867KB/s
SF-M64T MRW-S1 4KB 1 randread Default 7760KB/s
SF-M64T MRW-S1 4KB 4 randread Default 8418KB/s
SF-M64T MRW-S1 4KB 4 randread Default 8394KB/s
SF-M64T MRW-S1 1MB 1 randread Default 128MB/s
SF-M64T MRW-S1 1MB 4 randread Default 131MB/s
SF-M64T MRW-S1 1MB 8 randread Default 128MB/s
SF-M64T MRW-S1 256MB 1 randread Default 177MB/s
SF-M64T MRW-S1 256MB 4 randread Default 140MB/s
SF-M64T MRW-S1 256MB 8 randread Default 141MB/s
SF-M64T BSCR27U3BK 256MB 1 randread Default 74.9MB/s
SF-M64T BSCR27U3BK 256MB 4 randread Default 69.3MB/s
SF-M64T BSCR27U3BK 256MB 8 randread Default 72.0MB/s