LeavaTailの日記

LeavaTailの日記

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

Raspberry Pi 4 で OverlayFS を併用した Read-Only な rootfs を構築する

背景

組込み機器では、セキュリティやストレージデバイスの摩耗を抑えるなどといった観点からルートファイルシステムを読み取り専用にすることがある。 Raspberry Pi OS では、raspi-configoverlayroot によってルートファイルシステムを読み取り専用に変更することができる。

overlayroot では、既存のルートファイルシステムに OverlayFS を導入しやすくするのツールの一つである。 このパッケージによって読み取り専用となったルートファイルシステムに対して、tmpfsのような一時ファイルシステムを上位のレイヤに追加することで、達成することができる。

overlayrootでの複数レイヤによる書き込みと読み込みの分離

Linux では、読み取り専用ファイルシステムがいくつかサポートしている。 こういったファイルシステムは、読み取り専用に設計されているため、圧縮や重複除去といった機能がサポートされている。 Android では、SquashFS(Android 9 以前)、EROFS をシステムパーティションとして利用している。

本稿では、Raspberry Pi 4 のルートファイルシステムを読み取り専用に変更し、そのうえで SquashFS や EROFS として作成することを目指す。

実行環境

Raspberry Pi 4 は microSDカード経由でRaspberry Pi OSを起動させる。

計測環境の概要

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

項目 Raspberry Pi 4
CPU Cortex-A72 (ARM v8) 1.5GHz
メモリ 4GB LPDDR4-3200
OS Raspberry Pi OS (Mar 15th 2024)
Linux kernel 6.6.261
micro SD card KTHN-MW016G

ファイルシステムを Read-Only化する

  1. raspi-config から Non-Interactiveモードで OverlayFS による Read-Only な rootfs を有効化する。(必要であれば追加パッケージのインストールされる)

     pi@raspberrypi:~$ sudo raspi-config nonint enable_overlayfs
    
  2. システムを再起動する

     pi@raspberrypi:~$ sudo systemctl reboot
    
  3. 再起動後にシステムのマウント状況を確認する

     pi@raspberrypi:~$ mount | grep -E 'mmc|root-ro|root-rw'
     /dev/mmcblk0p2 on /media/root-ro type ext4 (ro,relatime)
     tmpfs-root on /media/root-rw type tmpfs (rw,relatime)
     overlayroot on / type overlay (rw,relatime,lowerdir=/media/root-ro,upperdir=/media/root-rw/overlay,workdir=/media/root-rw/overlay-workdir/_,uuid=on)
     /dev/mmcblk0p1 on /boot/firmware type vfat (rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro)        
    

読み取り専用ファイルシステムを使用する

ルートファイルシステムSquashFSに変更する

SquashFS は 読み取り専用の圧縮ファイルシステムとなっており、ファイルやディレクトリ、それらメタデータを圧縮することができる。(gzip/xz/lzo/zstdなどがサポートされている)

SquashFSファイルシステムを作成するためには、mksquashfs プログラムを使用する必要がある。 mksquashfs では、圧縮アルゴリズムの選択や作成されるファイルシステムのメタ情報を調整することができる。

ここでは、デフォルトのパラメータ(gzipによる圧縮)でSquashFSファイルシステムを作成する。

  1. Raspberry Pi OS の SDメモリカードのイメージをバックアップする

     $ sudo dd if=/dev/sdd1 of=bootfs.img
     $ sudo dd if=/dev/sdd2 of=rootfs.img
    
  2. SquashFSファイルシステムを作成する

     $ sudo mksquashfs /mnt/ rootfs_gzip.sfs
    
  3. 作成したSquashFSファイルシステムSDメモリカードに書き込む

     $ sudo dd if=rootfs_gzip.sfs of=/dev/sdd2
    

また、overlayrootのインストールによって、カーネルコマンドラインにも変更が加わっている。
SquashFSでマウントするように次のような修正をする。

// 1:
--- cmdline.txt.orig    2024-05-05 17:51:26.000000000 +0900
+++ cmdline.txt 2024-05-05 17:51:50.000000000 +0900
@@ -1 +1 @@
-overlayroot=tmpfs console=serial0,115200 console=tty1 root=PARTUUID=9e2953b9-02 rootfstype=ext4 fsck.repair=yes rootwait quiet splash plymouth.ignore-serial-consoles cfg80211.ieee80211_regdom=JP
+overlayroot=tmpfs console=serial0,115200 console=tty1 root=PARTUUID=9e2953b9-02 rootfstype=squashfs fsck.repair=yes rootwait quiet splash plymouth.ignore-serial-consoles cfg80211.ieee80211_regdom=JP

このSDメモリカードRaspberry Pi 4 を起動させたとき、SquashFSでマウントされていることが確認できる。

pi@raspberrypi:~$ mount | grep 'mmcblk0p2'
/dev/mmcblk0p2 on /media/root-ro type squashfs (ro,relatime,errors=continue)

ルートファイルシステムをEROFSに変更する

EROFS も 読み取り専用の(圧縮)ファイルシステムとなっており、ファイルやディレクトリ、それらメタデータを圧縮することができる。(lz4/lzmaなどがサポートされている)

SquashFSファイルシステムを作成するためには、mkfs.erofs プログラムを使用する必要がある。 mkfs.erofs では、圧縮アルゴリズムの選択や作成されるファイルシステムのメタ情報を調整することができる。

ここでは、デフォルトのパラメータ(非圧縮)でSquashFSファイルシステムを作成する。

  1. EROFSファイルシステムを作成する

     $ sudo mkfs.erofs rootfs.erofs /mnt
    
  2. 作成したEROFSファイルシステムSDメモリカードに書き込む

     $ sudo dd if=rootfs.erofs of=/dev/sdd2
    

また、EROFSでマウントするようにカーネルコマンドラインを次のような修正をする。

// 1:
--- cmdline.txt.orig    2024-05-05 17:51:26.000000000 +0900
+++ cmdline.txt 2024-05-05 17:51:50.000000000 +0900
@@ -1 +1 @@
-overlayroot=tmpfs console=serial0,115200 console=tty1 root=PARTUUID=9e2953b9-02 rootfstype=ext4 fsck.repair=yes rootwait quiet splash plymouth.ignore-serial-consoles cfg80211.ieee80211_regdom=JP
+overlayroot=tmpfs console=serial0,115200 console=tty1 root=PARTUUID=9e2953b9-02 rootfstype=erofs fsck.repair=yes rootwait quiet splash plymouth.ignore-serial-consoles cfg80211.ieee80211_regdom=JP

このSDメモリカードRaspberry Pi 4 を起動させたとき、SquashFSでマウントされていることが確認できる。

pi@raspberrypi:~$ mount | grep mmcblk0p2
/dev/mmcblk0p2 on /media/root-ro type erofs (ro,relatime,user_xattr,acl,cache_strategy=readaround)

測定

起動時間と圧縮率の二つの観点でそれぞれの起動方法を評価していく。 起動時間は systemd-analyzeから kernel と userpaceの合計時間(s)、圧縮率はデフォルトのイメージサイズ (4580120.72 KB) との割合から計測した。

S No. 項目 mkfsのオプション 起動時間(s) 圧縮率
1 ext4 (R/W) - 16.553 -
2 ext4 (R/O) - 15.988 -
3 SquashFS (uncompressed) -noD -noI -noX -noF 18.789 -
4 SquashFS (gzip) none 19.000 38.45%
5 SquashFS (LZ4HC) -comp lz4 -Xhc 16.751 44.18%
6 SquashFS (xz) -comp xz 28.728 32.74%
7 SquashFS (lzo) -comp lzo 17.563 41.63%
8 SquashFS (zstd) -comp zstd 16.891 36.11%
9 EROFS (uncompressed) none 15.842 -
10 EROFS (LZ4HC) -zlz4hc,12 15.842 54.70%
11 EROFS (lzma) -zlzma 22.001 32.75%
12 EROFS (big pcluster) -zlz4hc -C65536 15.779 48.81%
13 EROFS (multiple) --well-compressed=docs/compress-hints.example -zlz4hc,12 17.419 45.01%
14 EROFS (well-compressed) -C1048576 -Eztailpacking -Eall-fragments -Ededupe -zlz4hc,12 18.820 43.39%

x軸を起動時間として、y軸を圧縮率としたときに次のようなグラフが得られた。

Raspberry Pi 4の起動時間とrootfsのイメージ圧縮率

このグラフでは、プロットが左にあればあるほど起動時間が短く、下にあればあるほどrootfsのイメージサイズが小さいことを表している。

変更履歴

  • 2024/05/12: 記事公開

参考文献


  1. 2024年4月17日現在の Raspberry Pi OS のカーネルでは、SquashFS や EROFS がビルドインされていないため、カーネルは手元でビルドしたものに更新している。

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

関連記事

概要

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

本章では、MMCサブシステムの初期化処理について確認した。

はじめに

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

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

注意

一部の仕様書は非公開となっているため、公開情報からの推測が含まれています。そのため、内容に誤りが含まれている恐れがります。

SD判定

ここまで、MMCサブシステムから デバイス検出処理 (mmc_rescan関数) が、非同期による遅延処理が実行される。 その処理内で、 mmc_attach_sd関数によって、デバイスがSDであるかの検出とSDの初期化に入る。

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

// 1804:
int mmc_attach_sd(struct mmc_host *host)
{
    int err;
    u32 ocr, rocr;

    WARN_ON(!host->claimed);

    err = mmc_send_app_op_cond(host, 0, &ocr);
    if (err)
        return err;

    mmc_attach_bus(host, &mmc_sd_ops);
    if (host->ocr_avail_sd)
        host->ocr_avail = host->ocr_avail_sd;

    /*
    * We need to get OCR a different way for SPI.
    */
    if (mmc_host_is_spi(host)) {
        mmc_go_idle(host);

        err = mmc_spi_read_ocr(host, 0, &ocr);
        if (err)
            goto err;
    }

    /*
    * Some SD cards claims an out of spec VDD voltage range. Let's treat
    * these bits as being in-valid and especially also bit7.
    */
    ocr &= ~0x7FFF;

    rocr = mmc_select_voltage(host, ocr);

    /*
    * Can we support the voltage(s) of the card(s)?
    */
    if (!rocr) {
        err = -EINVAL;
        goto err;
    }

    /*
    * Detect and init the card.
    */
    err = mmc_sd_init_card(host, rocr, NULL);
    if (err)
        goto err;

    mmc_release_host(host);
    err = mmc_add_card(host->card);
    if (err)
        goto remove_card;

    mmc_claim_host(host);
    return 0;

remove_card:
    mmc_remove_card(host->card);
    host->card = NULL;
    mmc_claim_host(host);
err:
    mmc_detach_bus(host);

    pr_err("%s: error %d whilst initialising SD card\n",
        mmc_hostname(host), err);

    return err;
}

初めに、 mmc_send_app_op_cond関数によって SDかどうかを判定する。 mmc_send_app_op_cond関数では、ACMD41(SD_SEND_OP_COND) のレスポンスで判断する。*1 SD仕様に準拠したメモリカードである場合には、このコマンドでレスポンスが返ってくる。

"inquiry CMD41"の場合には、レスポンスとしてOCR(Operation Conditions Register?)が取得できる。

Vddの選択

OCR は、メモリカードの動作電圧範囲が 100mV 単位で表現される。

SPIモードでは、OCR の取得方法が異なり、mmc_spi_read_ocr関数によって取得する。 (しかし、今回はSPIモードではないため割愛する)

ここでパッチ概要によると、一部のメモリカードでは、この OCRの特定ビットを無効な電圧範囲とすることがある。

patchwork.kernel.org

取得したOCRとホストコントローラの対応電圧を使い、mmc_select_voltage関数は 供給電圧を設定する。 mmc_select_voltage関数の定義は次のようになっている。

// 1109:
u32 mmc_select_voltage(struct mmc_host *host, u32 ocr)
{
    int bit;

    /*
    * Sanity check the voltages that the card claims to
    * support.
    */
    if (ocr & 0x7F) {
        dev_warn(mmc_dev(host),
        "card claims to support voltages below defined range\n");
        ocr &= ~0x7F;
    }

    ocr &= host->ocr_avail;
    if (!ocr) {
        dev_warn(mmc_dev(host), "no support for card's volts\n");
        return 0;
    }

    if (host->caps2 & MMC_CAP2_FULL_PWR_CYCLE) {
        bit = ffs(ocr) - 1;
        ocr &= 3 << bit;
        mmc_power_cycle(host, ocr);
    } else {
        bit = fls(ocr) - 1;
        ocr &= 3 << bit;
        if (bit != host->ios.vdd)
            dev_warn(mmc_dev(host), "exceeding card's volts\n");
    }

    return ocr;
}

mmc_select_voltage関数では、OCRで取得された値とホストコントローラがサポートしている電圧から、最大VDDを設定する。 例えば、inquiry CMD41でocr0xff8000 、ホストコントローラの対応範囲host->ocr_avail0x300000の場合には、3.3~3.4Vとなる。

Vddの設定

カード初期化

OCRの値がホストコントローラの対応電圧の範囲に入っている場合、mmc_sd_init_card関数によって、SDメモリカードの初期化の処理に入る。 mmc_sd_init_card関数の定義は次のようになっている。

// 1389:
static int mmc_sd_init_card(struct mmc_host *host, u32 ocr,
    struct mmc_card *oldcard)
{
    struct mmc_card *card;
    int err;
    u32 cid[4];
    u32 rocr = 0;
    bool v18_fixup_failed = false;

    WARN_ON(!host->claimed);
retry:
    err = mmc_sd_get_cid(host, ocr, cid, &rocr);
    if (err)
        return err;

    if (oldcard) {
        if (memcmp(cid, oldcard->raw_cid, sizeof(cid)) != 0) {
            pr_debug("%s: Perhaps the card was replaced\n",
                mmc_hostname(host));
            return -ENOENT;
        }

        card = oldcard;
    } else {
        /*
        * Allocate card structure.
        */
        card = mmc_alloc_card(host, &sd_type);
        if (IS_ERR(card))
            return PTR_ERR(card);

        card->ocr = ocr;
        card->type = MMC_TYPE_SD;
        memcpy(card->raw_cid, cid, sizeof(card->raw_cid));
    }

    /*
    * Call the optional HC's init_card function to handle quirks.
    */
    if (host->ops->init_card)
        host->ops->init_card(host, card);

    /*
    * For native busses:  get card RCA and quit open drain mode.
    */
    if (!mmc_host_is_spi(host)) {
        err = mmc_send_relative_addr(host, &card->rca);
        if (err)
            goto free_card;
    }

    if (!oldcard) {
        err = mmc_sd_get_csd(card);
        if (err)
            goto free_card;

        mmc_decode_cid(card);
    }

    /*
    * handling only for cards supporting DSR and hosts requesting
    * DSR configuration
    */
    if (card->csd.dsr_imp && host->dsr_req)
        mmc_set_dsr(host);

    /*
    * Select card, as all following commands rely on that.
    */
    if (!mmc_host_is_spi(host)) {
        err = mmc_select_card(card);
        if (err)
            goto free_card;
    }

    err = mmc_sd_setup_card(host, card, oldcard != NULL);
    if (err)
        goto free_card;

    /*
    * If the card has not been power cycled, it may still be using 1.8V
    * signaling. Detect that situation and try to initialize a UHS-I (1.8V)
    * transfer mode.
    */
    if (!v18_fixup_failed && !mmc_host_is_spi(host) && mmc_host_uhs(host) &&
        mmc_sd_card_using_v18(card) &&
        host->ios.signal_voltage != MMC_SIGNAL_VOLTAGE_180) {
        /*
        * Re-read switch information in case it has changed since
        * oldcard was initialized.
        */
        if (oldcard) {
            err = mmc_read_switch(card);
            if (err)
                goto free_card;
        }
        if (mmc_sd_card_using_v18(card)) {
            if (mmc_host_set_uhs_voltage(host) ||
                mmc_sd_init_uhs_card(card)) {
                v18_fixup_failed = true;
                mmc_power_cycle(host, ocr);
                if (!oldcard)
                    mmc_remove_card(card);
                goto retry;
            }
            goto done;
        }
    }

    /* Initialization sequence for UHS-I cards */
    if (rocr & SD_ROCR_S18A && mmc_host_uhs(host)) {
        err = mmc_sd_init_uhs_card(card);
        if (err)
            goto free_card;
    } else {
        /*
        * Attempt to change to high-speed (if supported)
        */
        err = mmc_sd_switch_hs(card);
        if (err > 0)
            mmc_set_timing(card->host, MMC_TIMING_SD_HS);
        else if (err)
            goto free_card;

        /*
        * Set bus speed.
        */
        mmc_set_clock(host, mmc_sd_get_max_clock(card));

        /*
        * Switch to wider bus (if supported).
        */
        if ((host->caps & MMC_CAP_4_BIT_DATA) &&
            (card->scr.bus_widths & SD_SCR_BUS_WIDTH_4)) {
            err = mmc_app_set_bus_width(card, MMC_BUS_WIDTH_4);
            if (err)
                goto free_card;

            mmc_set_bus_width(host, MMC_BUS_WIDTH_4);
        }
    }

    if (!oldcard) {
        /* Read/parse the extension registers. */
        err = sd_read_ext_regs(card);
        if (err)
            goto free_card;
    }

    /* Enable internal SD cache if supported. */
    if (card->ext_perf.feature_support & SD_EXT_PERF_CACHE) {
        err = sd_enable_cache(card);
        if (err)
            goto free_card;
    }

    if (host->cqe_ops && !host->cqe_enabled) {
        err = host->cqe_ops->cqe_enable(host, card);
        if (!err) {
            host->cqe_enabled = true;
            host->hsq_enabled = true;
            pr_info("%s: Host Software Queue enabled\n",
                mmc_hostname(host));
        }
    }

    if (host->caps2 & MMC_CAP2_AVOID_3_3V &&
        host->ios.signal_voltage == MMC_SIGNAL_VOLTAGE_330) {
        pr_err("%s: Host failed to negotiate down from 3.3V\n",
            mmc_hostname(host));
        err = -EINVAL;
        goto free_card;
    }
done:
    host->card = card;
    return 0;

free_card:
    if (!oldcard)
        mmc_remove_card(card);

    return err;
}

カード識別モード

SDメモリカードには、カードを識別する番号 Card IDentification(CID) を持つ。

mmc_sd_init_card関数では、初めに CID を取得する。 mmc_sd_init_card関数の定義は次のようになっている。

// 808:
int mmc_sd_get_cid(struct mmc_host *host, u32 ocr, u32 *cid, u32 *rocr)
{
    int err;
    u32 max_current;
    int retries = 10;
    u32 pocr = ocr;

try_again:
    if (!retries) {
        ocr &= ~SD_OCR_S18R;
        pr_warn("%s: Skipping voltage switch\n", mmc_hostname(host));
    }

    /*
    * Since we're changing the OCR value, we seem to
    * need to tell some cards to go back to the idle
    * state.  We wait 1ms to give cards time to
    * respond.
    */
    mmc_go_idle(host);

    /*
    * If SD_SEND_IF_COND indicates an SD 2.0
    * compliant card and we should set bit 30
    * of the ocr to indicate that we can handle
    * block-addressed SDHC cards.
    */
    err = mmc_send_if_cond(host, ocr);
    if (!err)
        ocr |= SD_OCR_CCS;

    /*
    * If the host supports one of UHS-I modes, request the card
    * to switch to 1.8V signaling level. If the card has failed
    * repeatedly to switch however, skip this.
    */
    if (retries && mmc_host_uhs(host))
        ocr |= SD_OCR_S18R;

    /*
    * If the host can supply more than 150mA at current voltage,
    * XPC should be set to 1.
    */
    max_current = sd_get_host_max_current(host);
    if (max_current > 150)
        ocr |= SD_OCR_XPC;

    err = mmc_send_app_op_cond(host, ocr, rocr);
    if (err)
        return err;

    /*
    * In case the S18A bit is set in the response, let's start the signal
    * voltage switch procedure. SPI mode doesn't support CMD11.
    * Note that, according to the spec, the S18A bit is not valid unless
    * the CCS bit is set as well. We deliberately deviate from the spec in
    * regards to this, which allows UHS-I to be supported for SDSC cards.
    */
    if (!mmc_host_is_spi(host) && rocr && (*rocr & 0x01000000)) {
        err = mmc_set_uhs_voltage(host, pocr);
        if (err == -EAGAIN) {
            retries--;
            goto try_again;
        } else if (err) {
            retries = 0;
            goto try_again;
        }
    }

    err = mmc_send_cid(host, cid);
    return err;
}

カード認識するにあたってメモリカードを idle状態に設定する必要がある。 mmc_go_idle関数によって、メモリカードを idle状態に設定することができる。

mmc_go_idle関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。CMD0(FO_IDLE) はカードをidle状態に設定するコマンドである。

[    1.173771][   T45] mmc0: starting CMD0 arg 00000000 flags 000000c0

SDメモリカードには、SDv1とSDv2の異なるバージョンが存在しており、それぞれで初期化のシーケンスが若干異なる。

SDv1かSDv2か判定するためには、SDv2で追加されたCMD8(SEND_IF_COND)のレスポンスによって判定する。

SDv2判定のためのCMD8は mmc_send_if_conf関数によってコマンドを発行する発行することができる。 mmc_send_if_cond関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.192937][   T45] mmc0: starting CMD8 arg 000001aa flags 000002f5

もし、レスポンスが返ってきた場合、SD High Capacity(SDHC) や SD eXtended Capacity(SDXC) を意味する Card Capacity Status(CCS) を設定する。(SDHC や SDXC は SDv2 で追加された仕様である)

一方で、UHS-I は 信号電圧を 1.8Vまで省電圧化されている。 もし、UHS-Iがサポートされている場合には、S18R (Switching to 1.8V Request) を設定する。

その後、sd_get_host_max_current関数にてホストが供給できる最大電流を取得する。 もし、150mAまで供給できる場合には XPC (SDXC Power Control?) のビットを設定する。

ここまでで設定したocrを引数として ACMD41(SD_SEND_OP_COND) を呼び出す。 ACMD41 は引数が設定されている場合には、inquiry ではなく first ACMD41 として扱われる。

first ACMD41の引数

mmc_send_app_op_cond関数(first ACMD41)が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.193312][   T45] mmc0: starting CMD55 arg 00000000 flags 000000f5
[    1.193603][   T45] mmc0: starting CMD41 arg 40200000 flags 000000e1

first AMCD41のレスポンス rocr の特定ビット S18A(Switching to 1.8V Accepted) は、1.8Vへの切り替えが可能であることを意味する。

CIDの取得には、mmc_send_cid関数を用いる。 SDモードでは、CMD2(ALL_SEND_CID)によってCIDを取得することができる。

mmc_send_cid関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.193849][   T45] mmc0: starting CMD2 arg 00000000 flags 00000007

ここで取得した CID から既存のカードの置き換え処理でない場合には、mmc_alloc_card関数によって mmc_card構造体の変数cardの確保と初期化する。((もし、ホストコントローラ特有の初期化処理が必要な場合には、init_cardを呼び出すことができる))

SDモードでは、RCA(Relative Card Address) の取得が必要となる。 これは、CMD3(SEND_RELATIVE_ADDR)によって取得できる。

mmc_send_relative_addr関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.194188][   T45] mmc0: starting CMD3 arg 00000000 flags 00000075

SDメモリカードでは記憶容量などといった情報を CSD (Card-Specific Data?) レジスタに保持している。 このレジスタの値の取得とcardに情報を代入する処理を mmc_sd_get_csd関数が担う。

データ転送モード

SDメモリカードは、CMD9(SEND_CSD)を受け取ると CSDレジスタの値を返す。 mmc_sd_get_csd関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.448682][   T48] mmc0: starting CMD9 arg 45670000 flags 00000007

一部のSDメモリカードには DSR(Driver Stage Register) が実装されており、CMD/DATA出力の立ち上がり/立ち下がりの時間が強さを調整できる。 デバイスツリーに dsrプロパティを設定している場合、mmc_set_dsr関数によってCMD4 (SET_DSR)が発行される。

ただし、今回はこれが設定されていないため、CMD4は発行されない。

git.kernel.org

SDメモリカードでは、データ転送の前にCMD7(SELECT/DESELECT_CARD)でカードの選択をする。 mmc_select_card関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.194743][   T45] mmc0: starting CMD7 arg 45670000 flags 00000015

そして、mmc_sd_setup_card関数によってカードの取得・設定していく。 mmc_sd_setup_card関数の定義は次のようになっている。

// 919:
int mmc_sd_setup_card(struct mmc_host *host, struct mmc_card *card,
    bool reinit)
{
    int err;

    if (!reinit) {
        /*
        * Fetch SCR from card.
        */
        err = mmc_app_send_scr(card);
        if (err)
            return err;

        err = mmc_decode_scr(card);
        if (err)
            return err;

        /*
        * Fetch and process SD Status register.
        */
        err = mmc_read_ssr(card);
        if (err)
            return err;

        /* Erase init depends on CSD and SSR */
        mmc_init_erase(card);

        /*
        * Fetch switch information from card.
        */
        err = mmc_read_switch(card);
        if (err)
            return err;
    }

    /*
    * For SPI, enable CRC as appropriate.
    * This CRC enable is located AFTER the reading of the
    * card registers because some SDHC cards are not able
    * to provide valid CRCs for non-512-byte blocks.
    */
    if (mmc_host_is_spi(host)) {
        err = mmc_spi_set_crc(host, use_spi_crc);
        if (err)
            return err;
    }

    /*
    * Check if read-only switch is active.
    */
    if (!reinit) {
        int ro = mmc_sd_get_ro(host);

        if (ro < 0) {
            pr_warn("%s: host does not support reading read-only switch, assuming write-enable\n",
                mmc_hostname(host));
        } else if (ro > 0) {
            mmc_card_set_readonly(card);
        }
    }

    return 0;
}

SDメモリカードには、SCR(Sd Configuration Register) で SDメモリカードの特定の情報を持つ。 これには、mmc_app_send_scr関数が ACMD51 を発行する必要がある。 この関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.194989][   T45] mmc0: starting CMD55 arg 45670000 flags 00000095
[    1.195346][   T45] mmc0: starting CMD51 arg 00000000 flags 000000b5

その後、SSR(Sd Status Register) を取得する。 これには、mmc_read_ssr関数が ACMD13 を発行する必要がある。 この関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.196964][   T45] mmc0: starting CMD55 arg 45670000 flags 00000095
[    1.197161][   T45] mmc0: starting CMD13 arg 00000000 flags 000001b5

mmc_init_erase関数では、SSRなどから erase_size(eraseの最小単位) や preferred_erase_size(Allocation Unit size) を card に設定する。

mmc_read_switch関数は、SDメモリカードが対応しているバススピードモードを確認する。 これには、mmc_read_switch関数が CMD6 を発行する必要がある。 この関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.197741][   T45] mmc0: starting CMD6 arg 00fffff0 flags 000000b5

その後、バススピードやバス幅の設定をする。 ここでは、UHS-Iの場合とそうではない場合で分かれており、対応関係は次のようになっている。

バスインターフェース バススピードモード バススピード
default 12.5MB/s
High speed 25MB/s
UHS-I SDR50/DDR50 50MB/s
UHS-I SDR104 104MB/s

また、SD spec 6.x から 性能改善機能としてキャッシュなどが追加されており、これらをサポートしているカードに対しては CMD48(READ_EXTR_SINGLE)とCMD49(WRITE_EXTR_SINGLE)を発行することができる。

MMCバスに追加

mmc_add_card関数によって初期化の処理が完了したMMCメモリカードLinuxバイスモデルに登録する。 mmc_add_card関数の定義は次のようになっている。

// 308:
int mmc_add_card(struct mmc_card *card)
{
    int ret;
    const char *type;
    const char *uhs_bus_speed_mode = "";
    static const char *const uhs_speeds[] = {
        [UHS_SDR12_BUS_SPEED] = "SDR12 ",
        [UHS_SDR25_BUS_SPEED] = "SDR25 ",
        [UHS_SDR50_BUS_SPEED] = "SDR50 ",
        [UHS_SDR104_BUS_SPEED] = "SDR104 ",
        [UHS_DDR50_BUS_SPEED] = "DDR50 ",
    };


    dev_set_name(&card->dev, "%s:%04x", mmc_hostname(card->host), card->rca);

    switch (card->type) {
    case MMC_TYPE_MMC:
        type = "MMC";
        break;
    case MMC_TYPE_SD:
        type = "SD";
        if (mmc_card_blockaddr(card)) {
            if (mmc_card_ext_capacity(card))
                type = "SDXC";
            else
                type = "SDHC";
        }
        break;
    case MMC_TYPE_SDIO:
        type = "SDIO";
        break;
    case MMC_TYPE_SD_COMBO:
        type = "SD-combo";
        if (mmc_card_blockaddr(card))
            type = "SDHC-combo";
        break;
    default:
        type = "?";
        break;
    }

    if (mmc_card_uhs(card) &&
        (card->sd_bus_speed < ARRAY_SIZE(uhs_speeds)))
        uhs_bus_speed_mode = uhs_speeds[card->sd_bus_speed];

    if (mmc_host_is_spi(card->host)) {
        pr_info("%s: new %s%s%s card on SPI\n",
            mmc_hostname(card->host),
            mmc_card_hs(card) ? "high speed " : "",
            mmc_card_ddr52(card) ? "DDR " : "",
            type);
    } else {
        pr_info("%s: new %s%s%s%s%s%s card at address %04x\n",
            mmc_hostname(card->host),
            mmc_card_uhs(card) ? "ultra high speed " :
            (mmc_card_hs(card) ? "high speed " : ""),
            mmc_card_hs400(card) ? "HS400 " :
            (mmc_card_hs200(card) ? "HS200 " : ""),
            mmc_card_hs400es(card) ? "Enhanced strobe " : "",
            mmc_card_ddr52(card) ? "DDR " : "",
            uhs_bus_speed_mode, type, card->rca);
    }

#ifdef CONFIG_DEBUG_FS
    mmc_add_card_debugfs(card);
#endif
    card->dev.of_node = mmc_of_find_child_device(card->host, 0);

    device_enable_async_suspend(&card->dev);

    ret = device_add(&card->dev);
    if (ret)
        return ret;

    mmc_card_set_present(card);

    return 0;
}

ここまでの処理によってカード種類などが判明しているため、カーネルメッセージに出力する。 例えば、今回の環境では次のようなデバッグメッセージが確認することができる。

[    1.197018][   T45] mmc0: new SD card at address 4567

その後、mmc_add_card_debugfs関数で debugfs にエントリを追加し、device_add関数で sysfs にエントリを追加する。 mmc_add_card_debugfs関数の定義は次のようになっている。

// 253:
void mmc_add_card_debugfs(struct mmc_card *card)
{
    struct mmc_host    *host = card->host;
    struct dentry  *root;

    if (!host->debugfs_root)
        return;

    root = debugfs_create_dir(mmc_card_id(card), host->debugfs_root);
    card->debugfs_root = root;

    debugfs_create_x32("state", S_IRUSR, root, &card->state);
}

mmc_add_card_debugfs関数によって メモリカードの状態を確認できるようなエントリが追加される。

// 1:
/* Card states */
#define MMC_STATE_PRESENT   (1<<0)      /* present in sysfs */
#define MMC_STATE_READONLY  (1<<1)      /* card is read-only */
#define MMC_STATE_BLOCKADDR (1<<2)      /* card uses block-addressing */
#define MMC_CARD_SDXC       (1<<3)      /* card is SDXC */
#define MMC_CARD_REMOVED    (1<<4)      /* card has been removed */
#define MMC_STATE_SUSPENDED (1<<5)      /* card is suspended */

例えば、今回の環境で起動直後に確認した場合には次のような結果が得られる。

# cat /sys/kernel/debug/mmc0/mmc0\:4567/state
0x00000001

device_add関数はLinuxバイスモデルに"Device"を登録するAPIである。

この関数によって、関連するコンポーネントとして bus の probe処理が呼ばれる。 今回は mmc_bus_probe関数 (と mmc_blk_probe関数) を実行する。

おわりに

本記事では、カーネル起動時に呼び出される mmc_attach_sd関数について確認した。
この関数では、SDメモリカードの初期化ロジックが組み込まれており、次のようなCMDが発行されている。

SDメモリカードの初期化フロー図

変更履歴

  • 2024/05/10: 記事公開

参考

*1:ACMDは、直前にCMD55である場合に、そのコマンドをACMDとして解釈される

Raspberry Pi 4 で USBフラッシュドライブ に Bcachefs を試してみる

背景

Bcachefs は LInuxカーネル 6.7 からサポートされた Copy-On-Write (CoW) のファイルシステムである。 Bcachefs は、従来のLinuxでサポートされていた "bcache" をベースとしており、堅牢性と信頼性に加えて、多くの機能をサポートしていることで注目を浴びている。

目的

手元の Raspberry Pi 4 Model B (Raspberry Pi 4) で Bcachefs のドキュメントに従って実行することで、挙動や機能の概要を把握する。

実行環境

Raspberry Pi 4 は microSDカード経由でRaspberry Pi OSを起動させる。

計測環境の概要

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

項目 Raspberry Pi 4
CPU Cortex-A72 (ARM v8) 1.5GHz
メモリ 4GB LPDDR4-3200
OS Raspberry Pi OS Lite (Mar 15th 2024)
Linux kernel v6.9-rc31
bcachefs-tools version v0.1-nogit
fio fio-3.33
micro SD card KTHN-MW016G
USB 3.0 (1) USM32GU
USB 3.0 (2) SP032GBUF3B02V1K
USB 2.0 (1) USM32GR
ケース 陽極酸化アルミニウム製ヒートシンクケース

シングルドライブでの実験

単一のUSBフラッシュドライブのみに対して bcachefsを使用してみる。

ディスクパーティション /dev/sda を bcachefs でフォーマットするには、 bcacahefs format コマンドを実行する。

pi@raspberrypi:~$ sudo bcachefs format /dev/sda
External UUID:                              87cb6bbc-c417-4d66-8053-a96e07bc1dc2
Internal UUID:                              19bbba4a-3a3d-4f72-974d-dcaedc54bbdb
Device index:                               0
Label:
Version:                                    unwritten_extents
Oldest version on disk:                     unwritten_extents
Created:                                    Wed Apr 17 05:53:46 2024
Sequence number:                            0
Superblock size:                            816
Clean:                                      0
Devices:                                    1
Sections:                                   members
Features:                                   new_siphash,new_extent_overwrite,btree_ptr_v2,extents_above_btree_updates,btree    _updates_journalled,new_varint,journal_no_flush,alloc_v2,extentssCompat features:

Options:
  block_size:                               512 B
  btree_node_size:                          256 KiB
  errors:                                   continue [ro] panic
  metadata_replicas:                        1
  data_replicas:                            1
  metadata_replicas_required:               1
  data_replicas_required:                   1
  encoded_extent_max:                       64.0 KiB
  metadata_checksum:                        none [crc32c] crc64 xxhash
  data_checksum:                            none [crc32c] crc64 xxhash
  compression:                              [none] lz4 gzip zstd
  background_compression:                   [none] lz4 gzip zstd
  str_hash:                                 crc32c crc64 [siphash]
  metadata_target:                          none
  foreground_target:                        none
  background_target:                        none
  promote_target:                           none
  erasure_code:                             0
  inodes_32bit:                             1
  shard_inode_numbers:                      1
  inodes_use_key_cache:                     1
  gc_reserve_percent:                       8
  gc_reserve_bytes:                         0 B
  root_reserve_percent:                     0
  wide_macs:                                0
  acl:                                      1
  usrquota:                                 0
  grpquota:                                 0
  prjquota:                                 0
  journal_flush_delay:                      1000
  journal_flush_disabled:                   0
  journal_reclaim_delay:                    100
  nocow:                                    0

members (size 64):
  Device:                                   0
    UUID:                                   6b8b7038-fa14-4c21-8d1e-00b0cac0adf6
    Size:                                   28.9 GiB
    Bucket size:                            256 KiB
    First bucket:                           0
    Buckets:                                118296
    Last mount:                             (never)
    State:                                  rw
    Label:                                  (none)
    Data allowed:                           journal,btree,user
    Has data:                               (none)
    Discard:                                0
    Freespace initialized:                  0
initializing new filesystem
going read-write
initializing freespace
mounted version=unwritten_extents opts=noinodes_use_key_cache

kernel が bcachefs をサポートしている場合、従来のファイルシステムと同様に mountコマンドにより指定したマウントポイントに bcachefs をマウントすることができる。

pi@raspberrypi:~$ sudo mount -t bcachefs /dev/sda /mnt/

ここで、Flexible I/O tester (FIO) による簡易な読み書きパフォーマンスのベンチマークを取ってみる。 I/O サイズを 1 MB で順次書き込みをするようなジョブファイル write1.fioを使って書き込み帯域幅を確認する。

// 1:
[global]
ioengine=libaio
size=4G
invalidate=1
direct=1
verify=0
randrepeat=0
unlink=0
sync=0
; 順次読み込みの場合は rw=read
rw=write
bs=1M
time_based=1

[job]
name=write_bandwidth_test
; ブロックデバイスでの測定の場合は file=/dev/sda
directory=/mnt
ramp_time=2
runtime=5m
numjobs=4
group_reporting=1
iodepth=1

上記の測定結果(bcachefs上のファイルにアクセスした場合)に加えて、ブロックデバイスに直接アクセスした場合の書き込み/読み込み帯域幅を図示すると次のようになった。

シングルドライブにおける bcachefs による書き込み帯域幅オーバーヘッド測定

シングルドライブにおける bcachefs による読み込み帯域幅オーバーヘッド測定

この結果だけ見ると、Bcachefs によるオーバーヘッドが大きく見えてしまうが、"データと"ファイル"へのアクセスを比較しているため、これを性能の優劣をつけることはできない。 本来であれば、測定結果の妥当性の確認もしておきたいが、それは本記事の目的から外れるため割愛する。

暗号化

Bcachefsでは、認証付暗号化方式 AEAD の暗号化 (ChaCha20/Poly1305) をサポートしている。 これにより、ファイルシステム全体に対して暗号化することができ、スーパーブロックを除くすべてのメタデータが暗号化される。

暗号化を使用して Bcachefs でフォーマットするには、bcachefs formatコマンドに --encryptedオプションを追加する。 このとき、passphrase の入力が求められる。

pi@raspberrypi:~$ sudo bcachefs format --encrypted /dev/sda
Enter passphrase:
Enter same passphrase again:
External UUID:                              0ac05aa8-9b50-48fd-9bfc-95a016a0e74e
Internal UUID:                              5da9ad34-0907-4eee-a2c4-e99378bb1719
Device index:                               0
Label:
Version:                                    unwritten_extents
Oldest version on disk:                     unwritten_extents
Created:                                    Mon Apr 15 00:08:25 2024
Sequence number:                            0
Superblock size:                            880
Clean:                                      0
Devices:                                    1
Sections:                                   members,crypt
Features:                                   new_siphash,new_extent_overwrite,btree_ptr_v2,extents_above_btree_updates,btree    _updates_journalled,new_varint,journal_no_flush,alloc_v2,extentssCompat features:

Options:
  block_size:                               512 B
  btree_node_size:                          256 KiB
  errors:                                   continue [ro] panic
  metadata_replicas:                        1
  data_replicas:                            1
  metadata_replicas_required:               1
  data_replicas_required:                   1
  encoded_extent_max:                       64.0 KiB
  metadata_checksum:                        none [crc32c] crc64 xxhash
  data_checksum:                            none [crc32c] crc64 xxhash
  compression:                              [none] lz4 gzip zstd
  background_compression:                   [none] lz4 gzip zstd
  str_hash:                                 crc32c crc64 [siphash]
  metadata_target:                          none
  foreground_target:                        none
  background_target:                        none
  promote_target:                           none
  erasure_code:                             0
  inodes_32bit:                             1
  shard_inode_numbers:                      1
  inodes_use_key_cache:                     1
  gc_reserve_percent:                       8
  gc_reserve_bytes:                         0 B
  root_reserve_percent:                     0
  wide_macs:                                0
  acl:                                      1
  usrquota:                                 0
  grpquota:                                 0
  prjquota:                                 0
  journal_flush_delay:                      1000
  journal_flush_disabled:                   0
  journal_reclaim_delay:                    100
  nocow:                                    0

members (size 64):
  Device:                                   0
    UUID:                                   466c5ef5-893c-4b7a-8781-0dcc2d2734ed
    Size:                                   28.9 GiB
    Bucket size:                            256 KiB
    First bucket:                           0
    Buckets:                                118272
    Last mount:                             (never)
    State:                                  rw
    Label:                                  (none)
    Data allowed:                           journal,btree,user
    Has data:                               (none)
    Discard:                                0
    Freespace initialized:                  0

暗号化された bcachefs ファイルシステムはロック状態となっているため、そのままではマウントすることはできない。

pi@raspberrypi:~$ sudo mount -t bcachefs /dev/sda /mnt/
mount: /mnt: mount(2) system call failed: Required key not available.
       dmesg(1) may have more information after failed mount system call.

pi@raspberrypi:~$ sudo dmesg | grep bcachefs
[  599.228656] bcachefs (cd0e560a-0916-4b26-9db8-5d4aa60500e4): error requesting encryption key: ENOKEY

暗号化された bcachefs ファイルシステムは、 bcachefs unlockコマンドによってロック解除することができる。 このとき、暗号化で使用した passphrase の入力が求められる。

pi@raspberrypi:~$ sudo bcachefs unlock /dev/sda                                                                                                                                             
Enter passphrase:

これにより、暗号化キーがカーネル内のキーリングに追加される。 ただし、ここからマウントなどする場合には、キーをセッションに手動でリンクする必要があるらしい。(または、unlockのときに-k sessionオプションを追加する)

Re: Mounting a encrypted disk: Fatal error: Required key not available - Martin Steigerwald

pi@raspberrypi:~$ sudo keyctl link @u @s

これによって、現在のセッションで暗号化された bcachefs が利用 (マウント) できるようになる。

pi@raspberrypi:~$ sudo mount -t bcachefs /dev/sda /mnt/

ここで、FIO による簡易な読み書きパフォーマンスのベンチマークを取ってみる。 I/O サイズを 1 MB で順次書き込みをするようなジョブファイル write1.fioを使って書き込み帯域幅を確認する。

暗号化有効による書き込み帯域幅オーバーヘッド測定

暗号化有効による読み込み帯域幅オーバーヘッド測定

今回の測定では、暗号化機能を有効にした場合の書き込み帯域幅のオーバーヘッドは微小であった。 一方で、USB3.0(1)と(2)の読み込み帯域幅は25%程度の低下が見られた。また、USB2.0(1)の低下が微小であった。オーバーヘッドが微小であるのは、ストレージデバイスへのアクセスで律速しているケースと考えられる、

また、この機能を有効化していない場合、ブロックデバイス経由でbcachefsにあるファイルの名前が確認できたが、

pi@raspberrypi:~$ sudo xxd -a /dev/sda | grep -E "job\.[0-3]\.0" 
002801d0: 086a 6f62 2e30 2e30 0600 0000 0000 0000  .job.0.0........
0210fb70: 0210 0000 0000 0000 086a 6f62 2e33 2e30  .........job.3.0
03f19970: 0310 0000 0000 0000 086a 6f62 2e32 2e30  .........job.2.0
05d0c190: 0410 0000 0000 0000 086a 6f62 2e31 2e30  .........job.1.0
0ed40440: 0000 0020 0000 0000 086a 6f62 2e30 2e30  ... .....job.0.0
0ed40640: 0210 0000 0000 0000 086a 6f62 2e33 2e30  .........job.3.0
0ed40840: 0410 0000 0000 0000 086a 6f62 2e31 2e30  .........job.1.0
0ed40870: 086a 6f62 2e32 2e30 0000 0000 0000 0000  .job.2.0........ 

この機能を有効にしている場合、ブロックデバイス経由でbcachefsにあるファイルの名前を確認することはできなかった。

pi@raspberrypi:~$ sudo xxd -a /dev/sda | grep -E "job\.[0-3]\.0" 
pi@raspberrypi:~$  

圧縮

Bcachefsでは、データをエクステント単位による圧縮 (gzip、lz4、zstd) をサポートしている。 圧縮レベルは、015 を指定することができる。

さらに、コマンド bcachefs setattrによって特定のファイル/ディレクトリに対しても有効となっている。 また、rebalanceスレッドによって別のアルゴリズムによるデータを圧縮/再圧縮することもできる。

lz4で圧縮、zstdでバックグラウンド圧縮するためには、bcachefs formatコマンドに --compression--background_compressionオプションを追加する。

pi@raspberrypi:~$ sudo bcachefs format --compression=lz4 --background_compression=zstd /dev/sda

Bcachefs を /mnt以下にマウントする。

pi@raspberrypi:~$ sudo mount -t bcachefs /dev/sda /mnt/

ここで、FIO による簡易な読み書きパフォーマンスのベンチマークを取ってみる。 I/O サイズを 1 MB で順次書き込みをするようなジョブファイル write1.fioを使って書き込み帯域幅を確認する。

圧縮有効による書き込み帯域幅オーバーヘッド測定

圧縮有効による読み込み帯域幅オーバーヘッド測定

また、この機能による効果を確認するために、巨大なテキストファイル群 (linux-6.9-rc4.tar) をコピーしてみる。

pi@raspberrypi:~$ ls -l /mnt
total 1454330
-rw-r--r-- 1 root root 1489233920 Apr 18 05:44 linux-6.9-rc4.tar
drwx------ 2 root root          0 Apr 18 05:42 lost+found    
pi@raspberrypi:~$ df /dev/sda
Filesystem     1K-blocks   Used Available Use% Mounted on
/dev/sda        27615550 662787  26538105   3% /mnt
pi@raspberrypi:~$ sync
pi@raspberrypi:~$ df /dev/sda
Filesystem     1K-blocks   Used Available Use% Mounted on
/dev/sda        27615550 397492  26799319   2% /mnt

対象のファイル群はファイルサイズ1.4GBであったが、bcachefsによる圧縮(lz4)の効果により 662KB の使用量まで抑えられている。 また、rebalanceスレッドによってzstdへと再圧縮されており、400KBまで減っていることが分かった。

マルチドライブでの実験

bcachefs はマルチデバイス2に対応しているファイルシステムである。

ここでは、Raspberry Pi OSがそれぞれのUSBフラッシュドライブを次のように認識している場合である。

バイスファイル USBフラッシュドライブ 書き込み帯域幅(暫定) 読み込み帯域幅(暫定)
/dev/sda USB3.0(1) 11.8MB/s 109MB/s
/dev/sdb USB3.0(2) 15.4MB/s 110MB/s
/dev/sdc USB2.0(1) 4.1MB/s 26.6MB/s

ストライピング

bcachefsでは、複数のドライブを指定したときはストライピング(RAID0)として扱う。

pi@raspberrypi:~$ sudo bcachefs format /dev/sda /dev/sdb
External UUID:                              3e548aa6-9b4e-4465-988c-86f6c40c6348
Internal UUID:                              b95602d7-ce7c-47e0-b7bf-26b1300a9b5e
Device index:                               1
Label:
Version:                                    unwritten_extents
Oldest version on disk:                     unwritten_extents
Created:                                    Thu Apr 18 20:25:12 2024
Sequence number:                            0
Superblock size:                            872
Clean:                                      0
Devices:                                    2
Sections:                                   members
Features:                                   new_siphash,new_extent_overwrite,btree_ptr_v2,extents_above_btree_updates,btree    _updates_journalled,new_varint,journal_no_flush,alloc_v2,extents_across_btree_nodes
Compat features:

Options:
  block_size:                               512 B
  btree_node_size:                          256 KiB
  errors:                                   continue [ro] panic
  metadata_replicas:                        1
  data_replicas:                            1
  metadata_replicas_required:               1
  data_replicas_required:                   1
  encoded_extent_max:                       64.0 KiB
  metadata_checksum:                        none [crc32c] crc64 xxhash
  data_checksum:                            none [crc32c] crc64 xxhash
  compression:                              [none] lz4 gzip zstd
  background_compression:                   [none] lz4 gzip zstd
  str_hash:                                 crc32c crc64 [siphash]
  metadata_target:                          none
  foreground_target:                        none
  background_target:                        none
  promote_target:                           none
  erasure_code:                             0
  inodes_32bit:                             1
  shard_inode_numbers:                      1
  inodes_use_key_cache:                     1
  gc_reserve_percent:                       8
  gc_reserve_bytes:                         0 B
  root_reserve_percent:                     0
  wide_macs:                                0
  acl:                                      1
  usrquota:                                 0
  grpquota:                                 0
  prjquota:                                 0
  journal_flush_delay:                      1000
  journal_flush_disabled:                   0
  journal_reclaim_delay:                    100
  nocow:                                    0

members (size 120):
  Device:                                   0
    UUID:                                   16aa5679-4832-4197-bb0c-ea8004dac946
    Size:                                   28.9 GiB
    Bucket size:                            256 KiB
    First bucket:                           0
    Buckets:                                118272
    Last mount:                             (never)
    State:                                  rw
    Label:                                  (none)
    Data allowed:                           journal,btree,user
    Has data:                               (none)
    Discard:                                0
    Freespace initialized:                  0
  Device:                                   1
    UUID:                                   175843da-3652-40a1-ab42-13aed32fdc7f
    Size:                                   28.9 GiB
    Bucket size:                            256 KiB
    First bucket:                           0
    Buckets:                                118296
    Last mount:                             (never)
    State:                                  rw
    Label:                                  (none)
    Data allowed:                           journal,btree,user
    Has data:                               (none)
    Discard:                                0
    Freespace initialized:                  0
initializing new filesystem
going read-write
initializing freespace

マルチドライブによる bcachefs のマウントには、 :によってブロックデバイス名を指定する必要がある。

pi@raspberrypi:~$ sudo mount -t bcachefs /dev/sda:/dev/sdb /mnt/

/mnt/dev/sda/dev/sdbの2つのブロックデバイスから構成されているので、合計領域もそれらの総和となっている。

pi@raspberrypi:~$ df -h /mnt/
Filesystem         Size  Used Avail Use% Mounted on
/dev/sda:/dev/sdb   53G  1.5M   52G   1% /mnt

ここで、FIO による簡易な読み書きパフォーマンスのベンチマークを取ってみる。 I/O サイズを 1 MB で順次書き込みをするようなジョブファイル write1.fioを使って書き込み帯域幅を確認する。

ストライピング機能を有効にした時の読み込み書き込み帯域幅

レプリケーション

2台のドライブによるレプリケーション(RAID1)するには、bcachefs formatコマンドに --replicas オプションを追加する。

pi@raspberrypi:~$ sudo bcachefs format /dev/sda /dev/sdb  --replicas=2
External UUID:                              de438111-fb04-402f-a8c9-a2a2033df85d
Internal UUID:                              07de9a56-0613-40a9-a6e3-5361ea4165f7
Device index:                               1
Label:
Version:                                    unwritten_extents
Oldest version on disk:                     unwritten_extents
Created:                                    Thu Apr 18 07:10:24 2024
Sequence number:                            0
Superblock size:                            872
Clean:                                      0
Devices:                                    2
Sections:                                   members
Features:                                   new_siphash,new_extent_overwrite,btree_ptr_v2,extents_above_btree_updates,btree    _updates_journalled,new_varint,journal_no_flush,alloc_v2,extentss
Compat features:

Options:
  block_size:                               512 B
  btree_node_size:                          256 KiB
  errors:                                   continue [ro] panic
  metadata_replicas:                        2
  data_replicas:                            2
  metadata_replicas_required:               1
  data_replicas_required:                   1
  encoded_extent_max:                       64.0 KiB
  metadata_checksum:                        none [crc32c] crc64 xxhash
  data_checksum:                            none [crc32c] crc64 xxhash
  compression:                              [none] lz4 gzip zstd
  background_compression:                   [none] lz4 gzip zstd
  str_hash:                                 crc32c crc64 [siphash]
  metadata_target:                          none
  foreground_target:                        none
  background_target:                        none
  promote_target:                           none
  erasure_code:                             0
  inodes_32bit:                             1
  shard_inode_numbers:                      1
  inodes_use_key_cache:                     1
  gc_reserve_percent:                       8
  gc_reserve_bytes:                         0 B
  root_reserve_percent:                     0
  wide_macs:                                0
  acl:                                      1
  usrquota:                                 0
  grpquota:                                 0
  prjquota:                                 0
  journal_flush_delay:                      1000
  journal_flush_disabled:                   0
  journal_reclaim_delay:                    100
  nocow:                                    0

members (size 120):
  Device:                                   0
    UUID:                                   28f83018-915d-4423-986c-32ad8f361fde
    Size:                                   28.9 GiB
    Bucket size:                            256 KiB
    First bucket:                           0
    Buckets:                                118272
    Last mount:                             (never)
    State:                                  rw
    Label:                                  (none)
    Data allowed:                           journal,btree,user
    Has data:                               (none)
    Discard:                                0
    Freespace initialized:                  0
  Device:                                   1
    UUID:                                   3b4b848a-89f2-48d2-96de-556b69f15cb4
    Size:                                   28.9 GiB
    Bucket size:                            256 KiB
    First bucket:                           0
    Buckets:                                118296
    Last mount:                             (never)
    State:                                  rw
    Label:                                  (none)
    Data allowed:                           journal,btree,user
    Has data:                               (none)
    Discard:                                0
    Freespace initialized:                  0
initializing new filesystem
going read-write
initializing freespace
mounted version=unwritten_extents opts=metadata_replicas=2,data_replicas=2,noinodes_use_key_cache

Bcachefs を /mnt以下にマウントする場合には複数のブロックデバイスを指定する。

pi@raspberrypi:~$ sudo mount /dev/sda:/dev/sdb /mnt/

ここで、FIO による簡易な読み書きパフォーマンスのベンチマークを取ってみる。 I/O サイズを 1 MB で順次書き込みをするようなジョブファイル write1.fioを使って書き込み帯域幅を確認する。

レプリケーション機能を有効にした時の読み込み書き込み帯域幅

キャッシュ

bcachefs ではデバイスにラベルを付与することができる。 このラベルによってグループ化された特定のデバイスに特定にアクションを優先させたりすることで、ストレージデバイスの特性を活かすことができる。

bcachefs では、読み書きのターゲットとして、フォアグラウンドでの書き込み先 --forground_target、バックグラウンドで書き戻す先 --background_target、読み込み時にキャッシュとして使う --promote_target と設定することができる。

例えば、アクセス速度が速いUSB3.0(1) /dev/sdaUSB3.0(2) /dev/sdbssdラベル、遅いUSB2.0(/dev/sdc)には hddラべルを付与することで、上記のターゲットのルールは次のように設定することができる。[^3]

pi@raspberrypi:~$ sudo bcachefs format \
                      --label=ssd.ssd1 /dev/sda \
                      --label=hdd.hdd1 /dev/sdb \
                      --label=hdd.hdd2 /dev/sdc \
                      --foreground_target=ssd \
                      --promote_target=ssd \
                      --background_target=hdd

Bcachefs を /mnt以下にマウントする場合には複数のブロックデバイスを指定する。

pi@raspberrypi:~$ sudo mount /dev/sda:/dev/sdb /mnt/

そこで、いくつかのパターンで FIO による簡易な読み書きパフォーマンスのベンチマークを取ってみる。 I/O サイズを 1 MB で順次書き込みをするようなジョブファイル write1.fioを使って書き込み帯域幅を確認する。

パターン foreground_target promote_target background_target
2 SSDs USB3.0(1) + USB3.0(2) USB3.0(1) + USB3.0(2) USB2.0(1)
1 SSD USB3.0(1) USB3.0(1) USB3.0(1) + USB2.0(1)
Slow 1 SSD USB2.0(1) USB2.0(1) USB3.0(1) + USB3.0(2)

キャッシュ機能を有効にした時の読み込み書き込み帯域幅

この測定でも、フォアグラウンドでの読み書きを高速なデバイスに割り当て、そうでないデバイスをバックグラウンドに割り当てたほうがパフォーマンスが良い傾向が見られた。

変更履歴

  • 2024/04/23: 記事公開

参考文献


  1. 2024年4月17日現在の Raspberry Pi OS Lite のカーネルv6.6では、bcachefs がサポートされていないため、カーネルは手元でビルドしたものに更新している。
  2. バイスは同じサイズである必要はない。

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

関連記事

概要

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

本章では、カードの識別処理の概要を確認した。

はじめに

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

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

注意

一部の仕様書は非公開となっているため、公開情報からの推測が含まれています。そのため、内容に誤りが含まれている恐れがります。

MMC の rescan

mmc_add_host関数でWorkQueueに追加された detect によって、カード検出処理 mmc_rescan関数を呼び出す。 mmc_rescan関数の定義は次のようになっている。

// 2188:
void mmc_rescan(struct work_struct *work)
{
    struct mmc_host *host =
        container_of(work, struct mmc_host, detect.work);
    int i;

    if (host->rescan_disable)
        return;

    /* If there is a non-removable card registered, only scan once */
    if (!mmc_card_is_removable(host) && host->rescan_entered)
        return;
    host->rescan_entered = 1;

    if (host->trigger_card_event && host->ops->card_event) {
        mmc_claim_host(host);
        host->ops->card_event(host);
        mmc_release_host(host);
        host->trigger_card_event = false;
    }

    /* Verify a registered card to be functional, else remove it. */
    if (host->bus_ops)
        host->bus_ops->detect(host);

    host->detect_change = 0;

    /* if there still is a card present, stop here */
    if (host->bus_ops != NULL)
        goto out;

    mmc_claim_host(host);
    if (mmc_card_is_removable(host) && host->ops->get_cd &&
            host->ops->get_cd(host) == 0) {
        mmc_power_off(host);
        mmc_release_host(host);
        goto out;
    }

    /* If an SD express card is present, then leave it as is. */
    if (mmc_card_sd_express(host)) {
        mmc_release_host(host);
        goto out;
    }

    for (i = 0; i < ARRAY_SIZE(freqs); i++) {
        unsigned int freq = freqs[i];
        if (freq > host->f_max) {
            if (i + 1 < ARRAY_SIZE(freqs))
                continue;
            freq = host->f_max;
        }
        if (!mmc_rescan_try_freq(host, max(freq, host->f_min)))
            break;
        if (freqs[i] <= host->f_min)
            break;
    }
    mmc_release_host(host);

 out:
    if (host->caps & MMC_CAP_NEEDS_POLL)
        mmc_schedule_delayed_work(&host->detect, HZ);
}

MMCバスや関連するコンポーネントが利用不可能の状態であるとき、 rescan_disable によってカード検出ロジックを無効にすることができる。 導入パッチによると、sus/res中のMMC/SDメモリカード抜去による対応となっている。

git.kernel.org

v5.15時点では、この変数は mmc_start_host関数によって初期化、mmc_stop_host関数によって設定される。

また、リムーバブルメディアでない(mmc_card_is_removable)場合、 カード検出ロジックを何度も実施する必要がないため、host->rescan_entered に一度実施したかどうかを設定する。 今回のSDメモリカードはリムーバルメディア(non-removable)であるため、以降の処理を呼び出すことになる。

ホストコントローラによっては、カード挿入/抜去時に追加のアクションが必要になる。
そのようなホストコントローラは、trigger_card_eventをセットしておくことで card_eventを呼ぶことができる。 MMCIではそのような制御が不要であるため、trigger_card_eventは設定されていない。

以降のmmc_rescan_try_freq関数の処理が正常に終了している場合、 host->bus_opsに SD/SDIO/MMCカード毎の初期化処理が登録される。
その場合には、抜去や再挿入といったカードの変更を検出するために、bus->bus_ops->detectを呼び出す。

この時に設定される detect_change は、カードの抜去を検知できたことを示す。

ここで、SD specification v7.0 から規格化された SD Express Memory Cards の条件分岐が入る。 SD Express Memory Cards では、後方互換性のために従来のシーケンスでの初期化をするが、ここで分岐することになる。

www.sdcard.org

その後、mmc_rescan_try_freq関数によって周波数の設定を試みる。 ただし、カードによっては初期周波数 400KHz が対応できないことがあるため、400KHz、300KHz、200KHz、100KHz の順にリトライする。

git.kernel.org

周波数の設定

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

// 2035:
static int mmc_rescan_try_freq(struct mmc_host *host, unsigned freq)
{
    host->f_init = freq;

    pr_debug("%s: %s: trying to init card at %u Hz\n",
        mmc_hostname(host), __func__, host->f_init);

    mmc_power_up(host, host->ocr_avail);

    /*
    * Some eMMCs (with VCCQ always on) may not be reset after power up, so
    * do a hardware reset if possible.
    */
    mmc_hw_reset_for_init(host);

    /*
    * sdio_reset sends CMD52 to reset card.  Since we do not know
    * if the card is being re-initialized, just send it.  CMD52
    * should be ignored by SD/eMMC cards.
    * Skip it if we already know that we do not support SDIO commands
    */
    if (!(host->caps2 & MMC_CAP2_NO_SDIO))
        sdio_reset(host);

    mmc_go_idle(host);

    if (!(host->caps2 & MMC_CAP2_NO_SD)) {
        if (mmc_send_if_cond_pcie(host, host->ocr_avail))
            goto out;
        if (mmc_card_sd_express(host))
            return 0;
    }

    /* Order's important: probe SDIO, then SD, then MMC */
    if (!(host->caps2 & MMC_CAP2_NO_SDIO))
        if (!mmc_attach_sdio(host))
            return 0;

    if (!(host->caps2 & MMC_CAP2_NO_SD))
        if (!mmc_attach_sd(host))
            return 0;

    if (!(host->caps2 & MMC_CAP2_NO_MMC))
        if (!mmc_attach_mmc(host))
            return 0;

out:
    mmc_power_off(host);
    return -EIO;
}

mmc_rescan_try_freq関数の引数で渡された freq を 初期周波数として host->f_initに設定する。

その後、mmc_power_up関数によって POEWER ON 状態に繊維される。 ただし、ここではmmc_start_host関数によって状態となっているため、処理はスキップする。

ここで、eMMC によっては 電源投入後にハードウェアリセットされないものもある。 そういったデバイスのために、mmc_hw_reset_for_init関数によってホストコントローラからハードウェアリセットさせる仕組み (host->ops->hw_reset) が提供されている。 例えば、Raspberry Pi などで使用されている bcm2835 では hw_reset に独自の処理が設定されていたりする。 しかし、今回の環境では該当しないため、mmc_hw_reset_for_init関数では何も処理をせず、すぐに return される。

SDIOの初期化

sdio_reset関数は、SDIOベースのI/Oカードを初期化するための関数である。

// 202:
int sdio_reset(struct mmc_host *host)
{
    int ret;
    u8 abort;

    /* SDIO Simplified Specification V2.0, 4.4 Reset for SDIO */

    ret = mmc_io_rw_direct_host(host, 0, 0, SDIO_CCCR_ABORT, 0, &abort);
    if (ret)
        abort = 0x08;
    else
        abort |= 0x08;

    return mmc_io_rw_direct_host(host, 1, 0, SDIO_CCCR_ABORT, abort, NULL);
}

初期化には power reset または CMD52 の二通りのやり方が存在する。

git.kernel.org

sdio_reset関数では、CMD52を発行することで、これを実現する。 ただし、この処理はCMD0より前に発行しなければならない。

実処理は mmc_io_rw_direct_host関数が担っている。 この関数の詳細は省くが、mmc_wait_for_cmd関数によって指定されたコマンドを発行するものである。

sd_reset関数の一つ目のmmc_io_rw_direct_host関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.205283][   T48] mmc0: starting CMD52 arg 00000c00 flags 00000195

これは、CMD52 によって レジスタ Card Common Control Registers(CCCR) の SDIO_CCCR_ABORTの値を読み込みをしている。
しかし今回は、SDメモリカードであるため ETIMEDOUT となり失敗する。

その後、二つ目のmmc_io_rw_direct_host関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.207221][   T48] mmc0: starting CMD52 arg 80000c08 flags 00000195

これは、CMD52 によって レジスタ Card Common Control Registers(CCCR) の SDIO_CCCR_ABORTの値に0x08を書き込みしている。
しかし今回は、SDメモリカードであるため ETIMEDOUT となり失敗する。

カードを初期状態に戻す

SD規格ファミリーのカードでは特定の初期化シーケンスが必要となる。 mmc_go_idle関数は、初期化シーケンスに移る前に、カードを初期状態する。

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

// 139:
int mmc_go_idle(struct mmc_host *host)
{
    int err;
    struct mmc_command cmd = {};

    /*
    * Non-SPI hosts need to prevent chipselect going active during
    * GO_IDLE; that would put chips into SPI mode.  Remind them of
    * that in case of hardware that won't pull up DAT3/nCS otherwise.
    *
    * SPI hosts ignore ios.chip_select; it's managed according to
    * rules that must accommodate non-MMC slaves which this layer
    * won't even know about.
    */
    if (!mmc_host_is_spi(host)) {
        mmc_set_chip_select(host, MMC_CS_HIGH);
        mmc_delay(1);
    }

    cmd.opcode = MMC_GO_IDLE_STATE;
    cmd.arg = 0;
    cmd.flags = MMC_RSP_SPI_R1 | MMC_RSP_NONE | MMC_CMD_BC;

    err = mmc_wait_for_cmd(host, &cmd, 0);

    mmc_delay(1);

    if (!mmc_host_is_spi(host)) {
        mmc_set_chip_select(host, MMC_CS_DONTCARE);
        mmc_delay(1);
    }

    host->use_spi_crc = 0;

    return err;
}

ここで、SPIモードではない場合には Chip Select(CS)がアクティブになることを防ぐ必要がある。

git.kernel.org

mmc_go_idle関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.212842][   T48] mmc0: starting CMD0 arg 00000000 flags 000000c0

これは、CMD0によってソフトウェアリセットをかけている。

カードの識別

ここから、SD規格ファミリーのデバイスを識別する。

バイスツリーのProperty(no-sdio,no-sd, no-mmc)を設定していない場合には、SD express/SDIO/SD/MMCの順番に確認していく。

SD express

// 2059:
    if (!(host->caps2 & MMC_CAP2_NO_SD)) {
        if (mmc_send_if_cond_pcie(host, host->ocr_avail))
            goto out;
        if (mmc_card_sd_express(host))
            return 0;
    }

mmc_send_if_pcie関数は、動作電圧を確認する__mmc_send_if_cond関数のラッパーとなっている。 ここでは、mmc_send_if_pcie関数の詳細は割愛するが、この関数を実行したとき、次のようなデバッグメッセージが確認することができる。

[    1.232039][   T48] mmc0: starting CMD8 arg 000001aa flags 000002f5

これは、CMD8によって ocr レジスタに 動作電圧 (PCIe) のサポート状況を確認する。

今回使用しているカードは SD express ではないため、mmc_card_sd_express関数で弾かれる。

SDIO

// 2069:
    if (!(host->caps2 & MMC_CAP2_NO_SDIO))
        if (!mmc_attach_sdio(host))
            return 0;

SDIOカードの識別はmmc_attach_sdio関数内のmmc_send_io_op_cond関数で実施される。

// 18:
int mmc_send_io_op_cond(struct mmc_host *host, u32 ocr, u32 *rocr)
{
    struct mmc_command cmd = {};
    int i, err = 0;

    cmd.opcode = SD_IO_SEND_OP_COND;
    cmd.arg = ocr;
    cmd.flags = MMC_RSP_SPI_R4 | MMC_RSP_R4 | MMC_CMD_BCR;

    for (i = 100; i; i--) {
        err = mmc_wait_for_cmd(host, &cmd, MMC_CMD_RETRIES);
        if (err)
            break;

        /* if we're just probing, do a single pass */
        if (ocr == 0)
            break;

        /* otherwise wait until reset completes */
        if (mmc_host_is_spi(host)) {
            /*
            * Both R1_SPI_IDLE and MMC_CARD_BUSY indicate
            * an initialized card under SPI, but some cards
            * (Marvell's) only behave when looking at this
            * one.
            */
            if (cmd.resp[1] & MMC_CARD_BUSY)
                break;
        } else {
            if (cmd.resp[0] & MMC_CARD_BUSY)
                break;
        }

        err = -ETIMEDOUT;

        mmc_delay(10);
    }

    if (rocr)
        *rocr = cmd.resp[mmc_host_is_spi(host) ? 1 : 0];

    return err;
}

mmc_send_io_ops_cond関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.232680][   T48] mmc0: starting CMD5 arg 00000000 flags 000002e1

SDIOの判定は、CMD5 を発行したときの結果を確認することで確認できる。 今回は、SDメモリカードであるため ETIMEDOUT となり失敗する。

SD

// 2073:
    if (!(host->caps2 & MMC_CAP2_NO_SD))
        if (!mmc_attach_sd(host))
            return 0;

SDの識別はmmc_attach_sd関数内のmmc_send_app_op_cond関数で実施される。

// 118:
int mmc_send_app_op_cond(struct mmc_host *host, u32 ocr, u32 *rocr)
{
    struct mmc_command cmd = {};
    int i, err = 0;

    cmd.opcode = SD_APP_OP_COND;
    if (mmc_host_is_spi(host))
        cmd.arg = ocr & (1 << 30); /* SPI only defines one bit */
    else
        cmd.arg = ocr;
    cmd.flags = MMC_RSP_SPI_R1 | MMC_RSP_R3 | MMC_CMD_BCR;

    for (i = 100; i; i--) {
        err = mmc_wait_for_app_cmd(host, NULL, &cmd);
        if (err)
            break;

        /* if we're just probing, do a single pass */
        if (ocr == 0)
            break;

        /* otherwise wait until reset completes */
        if (mmc_host_is_spi(host)) {
            if (!(cmd.resp[0] & R1_SPI_IDLE))
                break;
        } else {
            if (cmd.resp[0] & MMC_CARD_BUSY)
                break;
        }

        err = -ETIMEDOUT;

        mmc_delay(10);
    }

    if (!i)
        pr_err("%s: card never left busy state\n", mmc_hostname(host));

    if (rocr && !mmc_host_is_spi(host))
        *rocr = cmd.resp[0];

    return err;
}

mmc_send_app_ops_cond関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.233542][   T48] mmc0: starting CMD55 arg 00000000 flags 000000f5
[    1.233807][   T48] mmc0: starting CMD41 arg 00000000 flags 000000e1

SDIOの判定は、ACMD41 を発行したときの結果を確認することで確認できる。 ちなみに、CMD55は次のコマンドがアプリケーションコマンドであることを表している。

MMC

// 2077:
    if (!(host->caps2 & MMC_CAP2_NO_MMC))
        if (!mmc_attach_mmc(host))
            return 0;

MMCの識別はmmc_attach_mmc関数内のmmc_send_op_cond関数で実施される。

// 176:
int mmc_send_op_cond(struct mmc_host *host, u32 ocr, u32 *rocr)
{
    struct mmc_command cmd = {};
    int i, err = 0;

    cmd.opcode = MMC_SEND_OP_COND;
    cmd.arg = mmc_host_is_spi(host) ? 0 : ocr;
    cmd.flags = MMC_RSP_SPI_R1 | MMC_RSP_R3 | MMC_CMD_BCR;

    for (i = 100; i; i--) {
        err = mmc_wait_for_cmd(host, &cmd, 0);
        if (err)
            break;

        /* wait until reset completes */
        if (mmc_host_is_spi(host)) {
            if (!(cmd.resp[0] & R1_SPI_IDLE))
                break;
        } else {
            if (cmd.resp[0] & MMC_CARD_BUSY)
                break;
        }

        err = -ETIMEDOUT;

        mmc_delay(10);

        /*
        * According to eMMC specification v5.1 section 6.4.3, we
        * should issue CMD1 repeatedly in the idle state until
        * the eMMC is ready. Otherwise some eMMC devices seem to enter
        * the inactive mode after mmc_init_card() issued CMD0 when
        * the eMMC device is busy.
        */
        if (!ocr && !mmc_host_is_spi(host))
            cmd.arg = cmd.resp[0] | BIT(30);
    }

    if (rocr && !mmc_host_is_spi(host))
        *rocr = cmd.resp[0];

    return err;
}

mmc_send_op_cond関数が呼ばれたとき、次のようなデバッグメッセージが確認することができる。

[    1.372548][   T34] mmc0: starting CMD1 arg 00000000 flags 000000e1

MMCの判定は、CMD1 を発行したときの結果を確認することで確認できる。

おわりに

本記事では、カーネル起動時に呼び出される mmc_rescan_try_freq関数について確認した。
この関数では、カード検出ロジックが組み込まれており、カード検出には対応するCMDのレスポンスで判断できる。

mmc_rescan_try_freq関数のフロー図

変更履歴

  • 2024/03/10: 記事公開

参考

SDメモリカードで摩耗平滑化の恩恵を体感してみる

概要

本稿では、Raspberry Pi 4 上で KIOXIA社の16GB SDメモリカードに書き込みを続けたときの状態を確認した。 先頭から 約1GB の書き込みを 10,000回繰り返してみたが、メモリカードは故障の兆しが見られなかった。

背景

SDメモリカードは、フラッシュメモリ技術を利用してデータを保存する記憶装置である。 フラッシュメモリはデータを記録する最小単位 メモリセルが複数個から構成されており、絶縁体に囲まれたに浮遊ゲートに電子を出し入れすることで情報を記憶する。

フラッシュメモリの基本動作

近年では、大容量化を実現するため一つのメモリセルに複数のデータを記憶する仕組みがある。 一つのメモリセルに1bitデータを記憶する仕組み Single Level Cell(SLC)、2bitデータを記憶する仕組み Multi Level Cell(MLC)、3bitデータを記憶する仕組み Triple Level Cell(TLC) などが存在する。

1つのメモリセルに多くのデータを記憶することでデータ量あたりのコストを下げることができるが、性能や耐久性といった課題がある。 フラッシュメモリでは、データの記憶(や消去)を繰り替えすことで絶縁体が劣化し、電子を正常に取り扱えなくなる恐れがある。 SLCでは100,000回の書き込みに耐えられるといわれているが、MLCでは10,000回程度、TLCでは3,000回程度と言われている。

各メーカーは、大容量化・高耐久性・省コスト化など様々な工夫によって他のメーカーとの差別化を目指している。 その中でもウェアレベリングは、書き込みが一か所に集中しないように均等に分散するように制御する一般的な仕組みである。

ウェアレベリング技術のイメージ

ウェアレベリングをサポートしていない場合、同じデータを繰り返し更新していると、一か所に書き込みが集中してしまう。 ウェアレベリングによって、書き込み箇所を平滑化することによって長寿命化を目指す。

このウェアレベリング技術には、2種類 ダイナミックウェアレベリングスタティックウェアレベリングに大別できる。

ダイナミックウェアレベリングのイメージ

ダイナミックウェアレベリングは、 データが書かれていない領域のみに対して、摩耗していないブロックを書き込みをウェアレベリング対象先する。 例えば、書き換えが多発しているブロック#9に対して更新したとき、未使用で書き換えが少ない#16に記録することで書き込み箇所を平滑化している。

スタティックウェアレベリングのイメージ

一方で、スタティックウェアレベリングでは、ストレージが待機状態となっているときなどに実施される。 書き込みが発生しにくいと判断されたブロックを移動させることで、摩耗しているブロックへの摩耗を最小限にすることができる。 例えば、ブロック#13, #14, #15, #16は書き換えがほぼ発生しなかったデータであるとき、摩耗しかけているブロック#9, #10, #11, #12に移動させる。

目的

SDメモリカードに書き込みを続けた場合の挙動を確認する。

実行環境

Raspberry Pi 4 Model B (Raspberry Pi 4) は USBフラッシュドライブ経由でRaspberry Pi OSを起動させる。 測定用のSDカードは Raspberry Pi 4の microSD card slot に挿入する。

計測環境の概要

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

項目 Raspberry Pi 4
CPU Cortex-A72 (ARM v8) 1.5GHz
メモリ 4GB LPDDR4-3200
OS Raspberry Pi OS (Dec 5th 2023)
ケース 陽極酸化アルミニウム製ヒートシンクケース
ストレージ KTHN-MW016G

ここで、実験に使用するストレージ (SDメモリカード) は新品未開封のものを使用する。

また、各実験の前に、このストレージで発行できる discard 処理を発行してある。 *1

pi@raspberrypi:~$ sudo blkdiscard -z /dev/mmcblk0
pi@raspberrypi:~$ sudo blkdiscard /dev/mmcblk0

実験

本記事では、2つの条件下でSDメモリカードに負荷をかけ(Flexible I/O Tester (fio)による書き込み)、その時の挙動を確認する。

未使用領域が十分に確保できる状態での書き込み

書き込みは先頭からシーケンシャルで1MB単位に実施していき、60s を 1 Iterationとする。 このとき、Rasperry Pi OS による影響を抑えるために、ダイレクトI/O や none I/O scheduler を使用する。

ただし、SDメモリカードの仕様が公式から明記されていないため、価格やスペックなどから TLC と仮定し、iterationの回数を 10,000 とした。

// 1:
    #!/bin/bash

    CNT=${1:-10000}
        
    for i in `seq ${CNT}`; do
            sync; sync; sync;
            echo 3 > /proc/sys/vm/drop_caches
    
            fio --name=test \
                --readwrite=write \
                --ioengine=libaio --iodepth=4 \
                --bs=1M --size=16G \
                --filename=/dev/mmcblk0 \
                --runtime=60 --time_based \
                --invalidate=1 --direct=1 \
                --ioscheduler=none \
                --output /tmp/fio.log --output-format=terse
    done

fio の実行結果 (/tmp/fio.log) から 項目write_bandwidthをプロットした結果を下記に示す。

スループット遷移 (実験1)

fioによる書き込みを 10,000回したが、いずれもスループットは 18MB/s が得られた。 さらに、カーネルからエラーメッセージや警告が出力されていないことから、SDメモリカードは正常に動作していると判断した。

また、項目write_kbを合算した結果 (アプリケーションから見た総書き込み量)、11,205,597,184 KB (約10TB) であった。 仮に、新品のSDメモリカードが全領域で均一化できたとして、約700回に抑えることができる *2TLC の書き込み上限を3,000回としても、この結果は妥当であると考えられる。

つまり、ユーザプログラムからは局所的な書き込みをしているが、ウェアレベリングによって書き込み箇所を平滑化されているように見受けられた。

未使用領域が十分に確保できない状態での書き込み

SDカードコントローラによって未使用領域を扱われないよう、領域を 0xff で埋めた状態で改めて局所的な書き込みを続ける。

root@raspberrypi:/home/pi# tr '\0' '\377' < /dev/zero > /dev/mmcblk0

書き込みは局所化させるために、先頭から4MBを書き込む。これを 1 Iterationとする。 このとき、Rasperry Pi OS による影響を抑えるために、ダイレクトI/O や none I/O scheduler を使用する。

// 1:
    #!/bin/bash

    CNT=${1:-1000}
        
    for i in `seq ${CNT}`; do
            sync; sync; sync;
            echo 3 > /proc/sys/vm/drop_caches
            blkdiscard -z -l 4194304 /dev/mmcblk0

            fio --name=test \
                --readwrite=write \
                --ioengine=sync \
                --bs=4M --size=4M \
                --loops=100 \
                --filename=/dev/mmcblk0 \
                --invalidate=1 --direct=1 \
                --fsync_on_close=1 \
                --ioscheduler=none \
                --output /tmp/fio.log --output-format=terse
    done

fio の実行結果 (/tmp/fio.log) から 項目write_bandwidthをプロットした結果を下記に示す。

スループット遷移 (実験2)

fioによる書き込みを 計 100,000回したが、カーネルからエラーメッセージや警告が出力されなかった。

スループットは34MB/s、8MB/s、4MB/s 近傍という結果となった。 3つの近傍が出たのは fio による書き込みが数秒で終わるほど短時間であったため、正常な計測できていない可能性も考えられるが、それについては回数と傾向によって判断する。 スループットの傾向としては、おおよそ変化はないように見える。

これらのスループットの傾向やカーネルメッセージから、SDメモリカードは正常に動作していると判断した。

この実験では、前の実験とは異なり ダイナミックウェアレベリングが有効には動作できない状況下であったにも関わらず、平滑化されているように見える。 実験の合間に スタティックウェアレベリングのような何かが動作しているのではないかとも考えられる。

変更履歴

  • 2024/03/05: 記事公開
  • 2024/03/29: 実験2 を追加

参考文献

*1:今回のSDメモリカードでは secure eraseは未サポートであるため実行していない

*2:ここでは 愚直に14.4GBで除算しているため値は正確ではない

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

関連記事

概要

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

本章では、MMCサブシステムの初期化処理について確認した。

はじめに

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

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

注意

一部の仕様書は非公開となっているため、公開情報からの推測が含まれています。そのため、内容に誤りが含まれている恐れがります。

host の起動

MMCホストコントローラドライバ PL180/1 の probe処理の後半で ホストコントローラの初期化処理のために mmc_add_host関数が呼ばれる。 初期化処理の最後に ホストコントローラを起動させるために mmc_start_host関数を呼び出す。

// 2252:
void mmc_start_host(struct mmc_host *host)
{
    host->f_init = max(min(freqs[0], host->f_max), host->f_min);
    host->rescan_disable = 0;

    if (!(host->caps2 & MMC_CAP2_NO_PRESCAN_POWERUP)) {
        mmc_claim_host(host);
        mmc_power_up(host, host->ocr_avail);
        mmc_release_host(host);
    }

    mmc_gpiod_request_cd_irq(host);
    _mmc_detect_change(host, 0, false);
}

mmc_start_host関数では、ホストコントローラのパラメータを持つmmc_host構造体を用いて、電源シーケンスの制御やカード検出シーケンスを(遅延)実行する。

この時、特定のコントローラ (rtsx_usb_sdmmcなど) では、初期化時間が長くブート時間に影響を与えるものもある。 そういったコントローラには、 MMC_CAP2_NO_PRESCAN_POWERUP フラグを付けて、起動を遅延させることがある。

git.kernel.org

今回の PL180/1ドライバではそういった考慮が必要ないため、ifブロック文の mmc_power_up関数によって起動シーケンスに入る。

コントローラの占有・解放

起動シーケンスでは、mmcホストコントローラの資産を利用する(コマンド発行など) ために排他制御する必要がある。 mmc_claim_host関数は、mmcホストコントローラが占有されているかどうかを検出する。

// 130:
static inline void mmc_claim_host(struct mmc_host *host)
{
    __mmc_claim_host(host, NULL, NULL);
}

git.kernel.org

mmc_claim_host関数は互換性維持のために用意されたラッパ関数であり、実態は __mmc_claim_host関数となっている。

// 780:
int __mmc_claim_host(struct mmc_host *host, struct mmc_ctx *ctx,
             atomic_t *abort)
{
    struct task_struct *task = ctx ? NULL : current;
    DECLARE_WAITQUEUE(wait, current);
    unsigned long flags;
    int stop;
    bool pm = false;

    might_sleep();

    add_wait_queue(&host->wq, &wait);
    spin_lock_irqsave(&host->lock, flags);
    while (1) {
        set_current_state(TASK_UNINTERRUPTIBLE);
        stop = abort ? atomic_read(abort) : 0;
        if (stop || !host->claimed || mmc_ctx_matches(host, ctx, task))
            break;
        spin_unlock_irqrestore(&host->lock, flags);
        schedule();
        spin_lock_irqsave(&host->lock, flags);
    }
    set_current_state(TASK_RUNNING);
    if (!stop) {
        host->claimed = 1;
        mmc_ctx_set_claimer(host, ctx, task);
        host->claim_cnt += 1;
        if (host->claim_cnt == 1)
            pm = true;
    } else
        wake_up(&host->wq);
    spin_unlock_irqrestore(&host->lock, flags);
    remove_wait_queue(&host->wq, &wait);

    if (pm)
        pm_runtime_get_sync(mmc_dev(host));

    return stop;
}
EXPORT_SYMBOL(__mmc_claim_host);

__mmc_claim_host関数はメインコントローラが占有できるまで Wait Queue の仕組みを使って休止状態で待ち続ける関数となっている。

git.kernel.org

また、占有されたホストコントローラは、mmc_release_host関数によって解除し、他のプログラムに ホストコントローラの占有権を譲渡することができる。

// 828:
void mmc_release_host(struct mmc_host *host)
{
    unsigned long flags;

    WARN_ON(!host->claimed);

    spin_lock_irqsave(&host->lock, flags);
    if (--host->claim_cnt) {
        /* Release for nested claim */
        spin_unlock_irqrestore(&host->lock, flags);
    } else {
        host->claimed = 0;
        host->claimer->task = NULL;
        host->claimer = NULL;
        spin_unlock_irqrestore(&host->lock, flags);
        wake_up(&host->wq);
        pm_runtime_mark_last_busy(mmc_dev(host));
        if (host->caps & MMC_CAP_SYNC_RUNTIME_PM)
            pm_runtime_put_sync_suspend(mmc_dev(host));
        else
            pm_runtime_put_autosuspend(mmc_dev(host));
    }
}

コントローラの電源 On

ホストコントローラが占有でき次第、 mmc_power_up関数によって電源 On のシーケンスに移る。
この時、電源が安定する前にクロックを有効にすべきではないので、いくつかのステップに分けて実施する。

MMCによる電力供給のステップ

  1. クロックが動作していない状態でカードに電力を供給する。
  2. 電源が安定するまで待つ。
  3. バスドライバとクロックを有効にする

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

// 1320:
void mmc_power_up(struct mmc_host *host, u32 ocr)
{
    if (host->ios.power_mode == MMC_POWER_ON)
        return;

    mmc_pwrseq_pre_power_on(host);

    host->ios.vdd = fls(ocr) - 1;
    host->ios.power_mode = MMC_POWER_UP;
    /* Set initial state and call mmc_set_ios */
    mmc_set_initial_state(host);

    mmc_set_initial_signal_voltage(host);

    /*
    * This delay should be sufficient to allow the power supply
    * to reach the minimum voltage.
    */
    mmc_delay(host->ios.power_delay_ms);

    mmc_pwrseq_post_power_on(host);

    host->ios.clock = host->f_init;

    host->ios.power_mode = MMC_POWER_ON;
    mmc_set_ios(host);

    /*
    * This delay must be at least 74 clock sizes, or 1 ms, or the
    * time required to reach a stable voltage.
    */
    mmc_delay(host->ios.power_delay_ms);
}

mmc_pwrseq_pre_power_on関数やmmc_pwrseq_post_power_on関数は、特定のコントローラのために MMC の power sequence の一部にコールバック処理を挟む仕組みの一つである。

git.kernel.org

ホストコントローラによっては、電源投入時のシーケンス動作に特別な処理が必要なモジュール (Marvell SD8787など) がある。 PL180/1 ドライバでは、そういった処理が不要であるため、ここでは割愛する。

その後、mmc_set_initial_state関数とmmc_set_initial_signal_voltage関数によって、ホストコントローラの初期設定を実施する。

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

// 974:
void mmc_set_initial_state(struct mmc_host *host)
{
    if (host->cqe_on)
        host->cqe_ops->cqe_off(host);

    mmc_retune_disable(host);

    if (mmc_host_is_spi(host))
        host->ios.chip_select = MMC_CS_HIGH;
    else
        host->ios.chip_select = MMC_CS_DONTCARE;
    host->ios.bus_mode = MMC_BUSMODE_PUSHPULL;
    host->ios.bus_width = MMC_BUS_WIDTH_1;
    host->ios.timing = MMC_TIMING_LEGACY;
    host->ios.drv_type = 0;
    host->ios.enhanced_strobe = false;

    /*
    * Make sure we are in non-enhanced strobe mode before we
    * actually enable it in ext_csd.
    */
    if ((host->caps2 & MMC_CAP2_HS400_ES) &&
         host->ops->hs400_enhanced_strobe)
        host->ops->hs400_enhanced_strobe(host, &host->ios);

    mmc_set_ios(host);

    mmc_crypto_set_initial_state(host);
}

e-MMC は JEDECによって規格化されており、2024年現在では バージョン5.1までアップデートされている。 ここでは、HS400モードCommand Queue Engine (CQE) に着目する。

  • HS400モード: e-MMC 5.0 からサポートされた 400MB/secの転送速度に対応した仕様 (mmc-spi-slot)
  • CQE: e-MMC 5.1からサポートされた コマンドにキューに入れ適切な順番で実行するための機構 (mmc-hs400-enhanced-strobe)

その後、mmc_set_ios関数から MMCホストコントローラに依存したデータ速度、mmc フェーズ、電力モード、データ バス幅などを設定する。 mmc_set_ios関数の定義は次のようになっている。

// 884:
static inline void mmc_set_ios(struct mmc_host *host)
{
    struct mmc_ios *ios = &host->ios;

    pr_debug("%s: clock %uHz busmode %u powermode %u cs %u Vdd %u "
        "width %u timing %u\n",
         mmc_hostname(host), ios->clock, ios->bus_mode,
         ios->power_mode, ios->chip_select, ios->vdd,
         1 << ios->bus_width, ios->timing);

    host->ops->set_ios(host, ios);
}

今回の環境では、mmci_set_ios関数によって設定する。 本稿では、この関数まで追うことは割愛するが、設定後に mmc_set_initial_signal_voltage関数によって電源への供給を試みる。

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

// 1159:
void mmc_set_initial_signal_voltage(struct mmc_host *host)
{
    /* Try to set signal voltage to 3.3V but fall back to 1.8v or 1.2v */
    if (!mmc_set_signal_voltage(host, MMC_SIGNAL_VOLTAGE_330))
        dev_dbg(mmc_dev(host), "Initial signal voltage of 3.3v\n");
    else if (!mmc_set_signal_voltage(host, MMC_SIGNAL_VOLTAGE_180))
        dev_dbg(mmc_dev(host), "Initial signal voltage of 1.8v\n");
    else if (!mmc_set_signal_voltage(host, MMC_SIGNAL_VOLTAGE_120))
        dev_dbg(mmc_dev(host), "Initial signal voltage of 1.2v\n");
}

mmc_set_initial_signal_voltage関数では、3.3V, 1.8V, 1.2V を初期値として設定を試みる。 設定のためにmmc_set_signal_voltage関数を呼び出す。

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

// 1143:
int mmc_set_signal_voltage(struct mmc_host *host, int signal_voltage)
{
    int err = 0;
    int old_signal_voltage = host->ios.signal_voltage;

    host->ios.signal_voltage = signal_voltage;
    if (host->ops->start_signal_voltage_switch)
        err = host->ops->start_signal_voltage_switch(host, &host->ios);

    if (err)
        host->ios.signal_voltage = old_signal_voltage;

    return err;

}

mmc_set_signal_voltage関数では、ホストコントローラ特有の start_signal_voltage_switchを呼びだす。 PL180/1ドライバの場合には、mmci_sig_volt_switch関数を呼び出す。

ここではその詳細を割愛するが、start_signal_voltage_switch関数によって Vqmmc のレギュレータを有効化する。

Card Detect の GPIO 検出

mmc_start_host関数では、Card Detect用のGPIOのために mmc_gpiod_request_cd_irq関数を呼び出す。 mmc_gpiod_request_cd_irq関数の定義は次のようになっている。

// 88:
void mmc_gpiod_request_cd_irq(struct mmc_host *host)
{
    struct mmc_gpio *ctx = host->slot.handler_priv;
    int irq = -EINVAL;
    int ret;

    if (host->slot.cd_irq >= 0 || !ctx || !ctx->cd_gpio)
        return;

    /*
    * Do not use IRQ if the platform prefers to poll, e.g., because that
    * IRQ number is already used by another unit and cannot be shared.
    */
    if (!(host->caps & MMC_CAP_NEEDS_POLL))
        irq = gpiod_to_irq(ctx->cd_gpio);

    if (irq >= 0) {
        if (!ctx->cd_gpio_isr)
            ctx->cd_gpio_isr = mmc_gpio_cd_irqt;
        ret = devm_request_threaded_irq(host->parent, irq,
            NULL, ctx->cd_gpio_isr,
            IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
            ctx->cd_label, host);
        if (ret < 0)
            irq = ret;
    }

    host->slot.cd_irq = irq;

    if (irq < 0)
        host->caps |= MMC_CAP_NEEDS_POLL;
}

PL180/1 ドライバでは、 mmci_probe関数がすでに実施しているため処理は不要である。 そこで host->slot.cd.irq には、"デバイスまたはアドレスが存在しない" ENXIOを設定する。

mmc のスキャン

その後、_mmc_detect_change関数でカードの検出を試みる。
_mmc_detect_change関数の定義は次のようになっている。

// 1401:
void _mmc_detect_change(struct mmc_host *host, unsigned long delay, bool cd_irq)
{
    /*
    * Prevent system sleep for 5s to allow user space to consume the
    * corresponding uevent. This is especially useful, when CD irq is used
    * as a system wakeup, but doesn't hurt in other cases.
    */
    if (cd_irq && !(host->caps & MMC_CAP_NEEDS_POLL))
        __pm_wakeup_event(host->ws, 5000);

    host->detect_change = 1;
    mmc_schedule_delayed_work(&host->detect, delay);
}

_mmc_detect_change関数では、カードが取り外されているかどうかのフラグ detect_change と、mmc_schedule_delayed_work関数によって検出ロジックに移る。

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

// 64:
static int mmc_schedule_delayed_work(struct delayed_work *work,
                     unsigned long delay)
{
    /*
    * We use the system_freezable_wq, because of two reasons.
    * First, it allows several works (not the same work item) to be
    * executed simultaneously. Second, the queue becomes frozen when
    * userspace becomes frozen during system PM.
    */
    return queue_delayed_work(system_freezable_wq, work, delay);
}

mmc_schedule_delayed_work関数では、Work Queue でmmc_rescan関数を遅延実行させることで、カードの検出を開始する。

おわりに

本記事では、カーネル起動時に呼び出される mmc_start_host関数について確認した。

変更履歴

  • 2024/02/23: 記事公開

参考文献

Raspberry Pi 4 で microSD の読み込み/書き込み性能を比較する

概要

Raspberry Pi 4 上で 6種のメーカー (Kingston, Transcend, SUNEAST, Gigastone, KIOXIA, San Disk) のSDメモリカードの読み込み/書き込み性能を比較した。 その結果、下記のような傾向を確認することができた。

  • Raspberry Pi 4 (Raspberry Pi OS) では、カタログの最大書き込み・読み込み性能に達することは難しい。
  • Raspberry Pi 4 (Raspberry Pi OS) では、ファイルシステムを介することによる書き込み性能が大きく低下するが、読み込み性能はそこまでの低下は見られなかった
  • SDメモリカードによって特性が異なり、ランダムな書き込みによって性能が大きく低下するものとそうではないものがあった

背景

SDメモリカードは、家庭用ゲーム機やデジタルカメラなど外部ストレージとして広く使われている。 SDメモリカードは、SD Associationによって定めたSD標準規格を基に様々なメーカーから販売されている。 各メーカーは、大容量化・高耐久性・省コスト化など様々な工夫によって他のメーカーとの差別化を目指している。 そのため、同じ要領のSDメモリカードでも異なる特性を持つことがある。 そこで複数のmicroSDカードの読み込み・書き込み性能を測定することで、それらの傾向を確認する。

目的

  1. カタログの最大書き込み・読み込み性能値と実測値の差を確認する
  2. ファイルシステムを介することによるオーバーヘッドを計測する
  3. microSDカードの違いによってパフォーマンスが異なることを確認する

実行環境

Raspberry Pi 4 Model B (Raspberry Pi 4) は USBフラッシュドライブ経由でRaspberry Pi OSを起動させる。 測定用のSDカードは Raspberry Pi 4の microSD card slot に挿入する。

計測環境の概要

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

項目 Raspberry Pi 4
CPU Cortex-A72 (ARM v8) 1.5GHz
メモリ 4GB LPDDR4-3200
OS Raspberry Pi OS (Dec 5th 2023)
ケース 陽極酸化アルミニウム製ヒートシンクケース

今回の調査には、下記6種類の未使用のmicroSDカードを6種類を使用し、Raspberriy Pi 4上で計測する。 (カタログに記載されていないものは、空欄としている)

Canvas Select Plus microSD Transcend USD300S SUNEAST ULTIMATE PRO GIGASTONE Prime 600X EXCERIA HIGH ENDURANCE SanDisk MAX ENDURANCE
型番号 KF-C4032-7I TS32GUSD300S-A SE-MSDU1032C180 GJMX-32GU190R KEMU-A032G SDSQQVR-032G-JN3ID
スピードクラス C10 C10 C10 C10 C10 C10
UHSスピードクラス U1 U1 U1 U1 U1 U3
ビデオスピードクラス V10 V10 V10 V30
アプリケーションパフォーマンスクラス A1 A1 A1 A1
インタフェース UHS-I UHS-I UHS-I UHS-I UHS-1 UHS-1
容量 32GB 32GB 32GB 32GB 32GB 32GB
最大読み出し速度 100 MB/s 100 MB/s 98 MB/s 90 MB/s 100 MB/s 100 MB/s
最大書き込み速度 85 MB/s 90 MB/s 30MB/s 40 MB/s

RAWデバイスのパフォーマンスをベンチマークする

まずは、簡単な性能測定として、ファイルシステムと Page Cache を介さないような測定パターン "RAWデバイスに対してダイレクト I/O" でベンチマークする。ここで計測するパターンは下記の4種類とする。

  1. I/O サイズを 1 MB でシーケンシャルな書き込み
  2. I/O サイズを 1 MB でシーケンシャルな読み込み
  3. I/O サイズを 4 KB でランダムな書き込み
  4. I/O サイズを 4 KB でランダムな読み込み

これらがどのようなI/Oフローでデータアクセスするかを考える。

RAWデバイスへのアクセスによるI/Oフローの概略図

fio から I/Oサイズごとにread/writeが発行されると、VFS からブロックレイヤにリクエストを要求する。ブロックレイヤに渡された bio は ioスケジューラなどによってマージやスプリットされる。ioスケジューラからディスパッチされた ioリクエストはブロック単位に書き込み・読み込みをする。また、連続するブロックに対するアクセスはマルチブロック読み込み・書き込みコマンドとして発行することもできる。

I/O サイズを 1 MB でシーケンシャルな書き込み

sudo fio --name=write_bandwidth_test \
  --filename=/dev/mmcblk0 --filesize=16G \
  --time_based --ramp_time=2s --runtime=60 \
  --ioengine=sync --direct=1 --bs=1M --iodepth=1 numjobs=1 \
  --rw=write --fsync_on_close=1 --invalidate=1

このコマンドを10回 実行させた結果と標準偏差は次の通りとなった。

SDメモリカード 平均スループット(MB/s) スループット標準偏差 平均IOPS IOPS標準偏差
kingston 22.2 0.1 22 0
gigastone 18.6 0.6 18 0
trancend 18.1 0.1 17 0
suneast 26.3 3.2 25 3
kioxia 25.6 0.3 25 0
sandisk 33.6 0.2 33 0

これらのスループットをグラフに描画してみると、次の通りとなった。

RAWデバイスにおけるシーケンシャルな書き込み性能

一つ目に、SDメモリカードによって性能差が出ていることに注目する。 測定結果の最低値と最大値を比較すると 2倍近くの差が出ていることがわかる。 これは、SDメモリカード内の構成やファームウェアの違いによるものと考えられる。

二つ目に、書き込み性能がカタログの最大性能に届かないことにも注目する。 SDメモリカードによっては、最大書き込み速度の半分以下の測定結果となっている。 これは、一般的なPCと比べると非力な Raspberry Pi 4 の Linux OS 上で計測しているため、SDメモリカードが処理しきれるだけのデータを転送できていない可能性がありそうである。

三つ目に、subeastを除き、標準偏差が小さいことにも注目する。 SUNEAST ULTIMATE PRO の結果を除き、標準偏差は 1%程度 に収まっている。 これは、購入直後に測定しているため、SDメモリカードの状態が安定していることを示しているだろう。

I/O サイズを 1 MB でシーケンシャルな読み込み

sudo fio --name=write_bandwidth_test \
  --filename=/dev/mmcblk0 --filesize=16G \
  --time_based --ramp_time=2s --runtime=60 \
  --ioengine=sync --direct=1 --bs=1M --iodepth=1 numjobs=1 \
  --rw=read --fsync_on_close=1 --invalidate=1

このコマンドを10回 実行させた結果と標準偏差は次の通りとなった。

SDメモリカード 平均スループット(MB/s) スループット標準偏差 平均IOPS IOPS標準偏差
kingston 40.1 0.3 39 0
gigastone 39.9 0.2 39 0
trancend 41.3 0.2 41 0
suneast 41.2 0.3 40 0
kioxia 40.9 0.3 40 0
sandisk 40.5 0.3 40 0

これらのスループットをグラフに描画してみると、次の通りとなった。

RAWデバイスにおけるシーケンシャルな読み込み性能

ここでは、SDメモリカードによって性能差が出ていないことに注目する。 シーケンシャルな書き込みとは異なり、異なるSDメモリカード間の読み込み速度に大きな差が見られなかった。 これは、一般的なPCと比べると非力な Raspberry Pi 4 の Linux OS 上で計測しているため、読み込み性能が何かしらに律速している可能性がある。

I/O サイズを 4 KB でランダムな書き込み

sudo fio --name=write_bandwidth_test \
  --filename=/dev/mmcblk0 --filesize=16G \
  --time_based --ramp_time=2s --runtime=60 \
  --ioengine=sync --direct=1 --bs=4K --iodepth=1 numjobs=1 \
  --rw=randwrite --fsync_on_close=1 --invalidate=1

このコマンドを10回 実行させた結果と標準偏差は次の通りとなった。

SDメモリカード 平均スループット(MB/s) スループット標準偏差 平均IOPS IOPS標準偏差
kingston 3.7 0.0 947 3
gigastone 1.0 0.0 254 4
trancend 2.3 0.0 589 2
suneast 5.8 0.3 1489 65
kioxia 2.2 0.0 571 12
sandisk 2.5 0.0 631 2

これらのスループットをグラフに描画してみると、次の通りとなった。

RAWデバイスにおけるランダムな書き込み性能

ここでは、シーケンシャルな書き込みと傾向が異なることに注目する。 シーケンシャルな書き込みの場合、EXCERIA HIGH ENDURANCE や SanDisk MAX ENDURANCE は高めの傾向が出ていたが、ランダムな書き込みではこれらの性能地が低めの傾向となった。 これらSDメモリカードが "高耐久" を特徴としていることから、SDメモリカード側のファームウェアが小さいブロックで疎らな要求をメモリセルの高耐久性に向けて制御しているのではないかと考えられる。

I/O サイズを 4 KB でランダムな読み込み

sudo fio --name=write_bandwidth_test \
  --filename=/dev/mmcblk0 --filesize=16G \
  --time_based --ramp_time=2s --runtime=60 \
  --ioengine=sync --direct=1 --bs=4K --iodepth=1 numjobs=1 \
  --rw=randread --fsync_on_close=1 --invalidate=1

このコマンドを10回 実行させた結果と標準偏差は次の通りとなった。

SDメモリカード 平均スループット(MB/s) スループット標準偏差 平均IOPS IOPS標準偏差
Canvas Select Plus microSD 8.6 0.0 2212 3
GIGASTONE Prime 600X 7.0 0.0 1788 2
Transcend USD300S 12.0 0.0 3072 6
SUNEAST ULTIMATE PRO 9.7 0.0 2475 4
EXCERIA HIGH ENDURANCE 12.7 0.0 3240 5
SanDisk MAX ENDURANCE 6.3 0.0 1601 2

これらのスループットをグラフに描画してみると、次の通りとなった。

RAWデバイスにおけるランダムな読み込み性能

ここでも、シーケンシャルな読み込みと傾向が異なることに注目する。 シーケンシャルな読み込みの場合、全体的に性能差はほとんどなく 40MB/s まで達していたが、ランダムな読み込みの場合、SDメモリカードによって性能差が出ており、10 MB/s あたりまで性能が低下している。 SDメモリカードで使用される NAND型フラッシュメモリでは、HDDと比較してシークタイムのロスタイムは小さいため、シークによるものとは考えにくい。 細かいI/Oを大量に発行したことで Raspberry Pi 側 に負荷がかかったことにより、SDメモリカード側のコントローラの違いが顕著に表れたとも考えられる。

vfatファイルシステムのパフォーマンスをベンチマークする

次に、 Page Cache を介さないような測定パターン "vfatファイルシステムに対してダイレクト I/O" でベンチマークする。ここで計測するパターンは下記の4種類とする。

  1. I/O サイズを 1 MB でシーケンシャルな書き込み
  2. I/O サイズを 1 MB でシーケンシャルな読み込み
  3. I/O サイズを 4 KB でランダムな書き込み
  4. I/O サイズを 4 KB でランダムな読み込み

これらがどのようなI/Oフローでデータアクセスするかを考える。

vfatファイルシステムへのアクセスによるI/Oフローの概略図

fio から I/Oサイズごとにread/writeが発行されると、VFS は vfatファイルシステムにwritepages(実データ)とwrite_inode(メタデータ)を呼び出す。 vfatファイルシステムはブロックレイヤにリクエストを要求する。 ブロックレイヤに渡された bio は ioスケジューラなどによってマージやスプリットされる。 ioスケジューラからディスパッチされた ioリクエストはブロック単位に書き込み・読み込みをする。 連続するブロックに対するアクセスはマルチブロック読み込み・書き込みコマンドとして発行することもできる。

その後、一定期間経過後にメタデータの書き込みを開始する。 今回の環境ではFAT32 (クラスタサイズは16 KB) であり、想定される書き込み対象は次の通りである。

  • FAT (1つのクラスタに対して、エントリのサイズは4 B) × 2
  • ディレクトリエントリ (1つのファイルに対して、最低でも32 B)
  • FSINFO (512 B)

この計測では、ダイレクト I/O かつ メモリにも十分な空きがあるはずなので、inode の expireされるまで遅延されるだろう。 デフォルトの値が 30s であるので 一回の計測で 2回程度の書き込みとなる。

I/O サイズを 1 MB でシーケンシャルな書き込み

sudo fio --name=write_bandwidth_test \
  --directory=/$TEST_DIR --filesize=4095M \
  --time_based --ramp_time=2s --runtime=60 \
  --ioengine=sync --direct=1 --bs=1M --iodepth=1 numjobs=1 \
  --rw=write --fsync_on_close=1 --invalidate=1 

このコマンドを10回 実行させた結果と標準偏差は次の通りとなった。

SDメモリカード 平均スループット(MB/s) スループット標準偏差 平均IOPS IOPS標準偏差
kingston 18.0 0.3 17 0
gigastone 15.6 1.4 15 1
trancend 15.5 0.8 15 0
suneast 18.4 1.8 17 1
kioxia 19.9 0.1 19 0
sandisk 25.3 0.1 25 0

これらのスループットをRAWデバイスの結果と共にグラフに描画してみると、次の通りとなった。

vfatファイルシステムにおけるシーケンシャルな書き込み性能

ここでは、RAWデバイスから性能が低下していることに注目する。 RAWデバイスへの書き込みとは異なり、ファイルへの書き込みしているため実際のデータに加えて、メタデータの書き込みも発生している。 今回の環境では、1 GB のファイルは 65,536個のクラスタから構成される。 そのため、1 GB のファイルを書き込みする場合には、 256 KB + α の追加データを書き込みすることになる。 それに加えて、追加の計算量が発生することを加味すれば、これらオーバーヘッドが原因と考えることができるだろう。

I/O サイズを 1 MB でシーケンシャルな読み込み

sudo fio --name=write_bandwidth_test \
  --directory=/$TEST_DIR --filesize=4095M \
  --time_based --ramp_time=2s --runtime=60 \
  --ioengine=sync --direct=1 --bs=1M --iodepth=1 numjobs=1 \
  --rw=read --fsync_on_close=1 --invalidate=1

このコマンドを10回 実行させた結果と標準偏差は次の通りとなった。

SDメモリカード 平均スループット(MB/s) スループット標準偏差 平均IOPS IOPS標準偏差
kingston 40.3 0.5 39 0
gigastone 39.9 0.4 39 0
trancend 41.4 0.3 41 0
suneast 41.6 0.3 41 0
kioxia 40.5 0.1 40 0
sandisk 40.6 0.3 40 0

これらのスループットをRAWデバイスの結果と共にグラフに描画してみると、次の通りとなった。

vfatファイルシステムにおけるシーケンシャルな読み込み性能

ここでは、RAWデバイスの場合と同様の性能であることに注目する。 vfatファイルシステムのファイルを読み込む場合には、クラスタチェインを辿る必要がある。 しかし、この計測ではそういったオーバーヘッドは小さく、RAWデバイスの時と同様に何かしらで律速しているように見える。

I/O サイズを 4 KB でランダムな書き込み

sudo fio --name=write_bandwidth_test \
  --directory=/$TEST_DIR --filesize=4095M \
  --time_based --ramp_time=2s --runtime=60 \
  --ioengine=sync --direct=1 --bs=4K --iodepth=1 numjobs=1 \
  --rw=randwrite --fsync_on_close=1 --invalidate=1

このコマンドを10回 実行させた結果と標準偏差は次の通りとなった。

SDメモリカード 平均スループット(KB/s) スループット標準偏差 平均IOPS IOPS標準偏差
kingston 540.3 253.9 134 63
gigastone 43.0 18.5 10 4
trancend 1.3 0.0 0 0
suneast 772.9 338.7 192 84
kioxia 487.5 149.8 121 37
sandisk 717.5 113.5 179 28

これらのスループットをRAWデバイスの結果と共にグラフに描画してみると、次の通りとなった。

vfatファイルシステムにおけるランダムな書き込み性能

一つ目に、RAWデバイスから性能が大きく低下していることに注目する。 vfatファイルシステムではスパースファイルが未対応であるため、ランダムシークによって書き込まれていない領域 (ホール) に対して 0 を書き込む作業が発生する。 RAWデバイスに対する書き込みと比較して、書き込み総量が大きくなったことで、性能が低下しているのではないかと考えられる。

二つ目に、全体的に標準偏差が大きいことにも注目する。 上記の仮説と同様に、スパースファイルが未対応であるため、ランダムシークの傾向によっては ファイルサイズの2倍の書き込みが発生することもある。 そのため、ランダムシード (書き込みパターン) によって書き込み総量が変化することが原因ではないかと考えられる。

I/O サイズを 4 KB でランダムな読み込み

sudo fio --name=write_bandwidth_test \
  --directory=/$TEST_DIR --filesize=4095M \
  --time_based --ramp_time=2s --runtime=60 \
  --ioengine=sync --direct=1 --bs=4K --iodepth=1 numjobs=1 \
  --rw=randread --fsync_on_close=1 --invalidate=1

このコマンドを10回 実行させた結果と標準偏差は次の通りとなった。

SDメモリカード 平均スループット(KB/s) スループット
標準偏差
平均IOPS IOPS
標準偏差
kingston 7.7 0.0 1973 7
gigastone 5.4 0.0 1375 1
trancend 10.5 0.0 2702 5
suneast 10.4 0.1 2673 23
kioxia 10.4 0.1 2674 11
sandisk 8.6 0.0 2198 7

これらのスループットをRAWデバイスの結果と共にグラフに描画してみると、次の通りとなった。

vfatファイルシステムにおけるランダムな読み込み性能

ここでは、RAWデバイスの場合より性能が高くなるケースについて注目する。 通常であれば、RAWデバイスの計測した環境から vfatファイルシステムのレイヤーが追加されるため、オーバーヘッドの追加が想定される。 vfatファイルシステムの場合だと、ファイルの中身を読み込むためにはクラスタチェインを辿る必要がある。 しかし、SUNEAST ULTIMATE PRO と SanDisk MAX ENDURANCE については、その仮説通りとはいかなかった。 これは、ランダム読み込みで何かしらの情報をキャッシュすることで性能が向上したのではないかと考えられる。 しかし、vfatファイルシステムが持つキャッシュはクラスタチェインの連続性のみであり、read-ahead などの仕組みはダイレクトI/Oによってバイパスされているため、これらは無関係であると思われる。

変更履歴

  • 2024/02/03: 記事公開
  • 2024/03/01: 誤字修正

参考文献