LeavaTailの日記

LeavaTailの日記

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

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: 誤字修正

参考文献

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

関連記事

概要

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

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

はじめに

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

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

注意

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

Device Tree

Open Firmware Device Tree (DT) は、システムのハードウェア構成を記述するためのデータ構造である。 一般的に使われている PC (x86 および x86_64) では別の仕組み (ACPI) によって解決することが多いが、シングルボードコンピュータ (ARM) では DT によって情報を解決することが多い。 Linux では システムのハードウェア情報を DT として切り出すことによって kernel や ドライバ を共通化している。

DT では、各ノードにプロパティが定義できる木構造で表現される。 例えば、デュアルコアの cortex-a9 と I2Cデバイス (PCIe Switch)と MMCI が搭載しているシステムを考えてみる。

Device Tree 概念図

MMC に関連するノードに注目すると、ARM PrimeCell Multimedia Card Interface (PL180/1) ドライバ にプロパティregclocks を渡すことを表現している。 これらのプロパティに何が指定できるかについては、ドライバ毎によって異なる。 kernel に組み込まれているドライバについては、 Documentation/devicetree/bindings 以下にドキュメントが用意されている。

PL180/1 ドライバは Documentation/devicetree/bindings/mmc/arm,pl18x.yaml に記載されている。 例えば、reg については次のような記載がされている。

// 68:
  reg:
    description: the MMIO memory window must be exactly 4KB (0x1000) and the
      layout should provide the PrimeCell ID registers so that the device can
      be discovered. On ST Micro variants, a second register window may be
      defined if a delay block is present and used for tuning.

上記の場合では、PL180/1ドライバに対して次のデータを渡している。

  • 親ノード / からの相対アドレス 0x5000 から 0x1000 byte を MMIO memory window
  • Advanced Peripheral Bus(APB) に v2m_clk24mhz(24MHz)、MCI adapter Clock(MCLK) に smbclksmbclk(33Mhz ~ 100MHz)

QEMU Arm Versatile Express boards

起動しているLinuxマシンにロードされている DT は /proc/device-tree//sys/firmware/devicetree/base/ から確認することができる。
今回使用している vexpress-a9 (QEMU) で使用している DT は次のような木構造で表現できる。(橙色 は QEMU によって追加されたノードを表現している)

vexpress-a9(QEMU) の device tree structure

ここでは、MMCに関連するノードを抜粋して確認する。 QEMU Arm Versatile Express boards では vexpress-v2m.dtsi に MMC関連のノードが記載されている。

// 191:
                mmci@5000 {
                    compatible = "arm,pl180", "arm,primecell";
                    reg = <0x05000 0x1000>;
                    interrupts = <9>, <10>;
                    cd-gpios = <&v2m_mmc_gpios 0 0>;
                    wp-gpios = <&v2m_mmc_gpios 1 0>;
                    max-frequency = <12000000>;
                    vmmc-supply = <&v2m_fixed_3v3>;
                    clocks = <&v2m_clk24mhz>, <&smbclk>;
                    clock-names = "mclk", "apb_pclk";
                };

プロパティcompatible"arm,pl180", "arm,primecell"を指定しており、この値がデバイスドライバとの対応関係に用いられる。 QEMU Arm Versatile Express boards では、PL180/1 ドライバ で動作することになる。

MMCI の probe

PL180 は AMBA 規格に準拠した SoC のペリフェラルである。 AMBA 規格のバスに登録されているドライバは of_platform_default_populate_init()amba_deferred_retry() などによって probe 処理が呼ばれる。 この時、of_amba_device_create関数で amba_device構造体と amba_id構造体に最低限のデータを詰め込んでから渡される。 例えば、ノード名をnameに設定したり、regプロパティから先頭アドレスを設定をしたりする。

mmci の場合には次のようなデータを渡されることになる。

mmci_probe関数の引数の概要

mmci では、probe として mmci_probe関数が呼び出される。 この関数の定義は次のようになっている。

// 1990:
static int mmci_probe(struct amba_device *dev,
    const struct amba_id *id)
{
    struct mmci_platform_data *plat = dev->dev.platform_data;
    struct device_node *np = dev->dev.of_node;
    struct variant_data *variant = id->data;
    struct mmci_host *host;
    struct mmc_host *mmc;
    int ret;

    /* Must have platform data or Device Tree. */
    if (!plat && !np) {
        dev_err(&dev->dev, "No plat data or DT found\n");
        return -EINVAL;
    }

    if (!plat) {
        plat = devm_kzalloc(&dev->dev, sizeof(*plat), GFP_KERNEL);
        if (!plat)
            return -ENOMEM;
    }

    mmc = mmc_alloc_host(sizeof(struct mmci_host), &dev->dev);
    if (!mmc)
        return -ENOMEM;

    host = mmc_priv(mmc);
    host->mmc = mmc;
    host->mmc_ops = &mmci_ops;
    mmc->ops = &mmci_ops;

    ret = mmci_of_parse(np, mmc);
    if (ret)
        goto host_free;

    /*
    * Some variant (STM32) doesn't have opendrain bit, nevertheless
    * pins can be set accordingly using pinctrl
    */
    if (!variant->opendrain) {
        host->pinctrl = devm_pinctrl_get(&dev->dev);
        if (IS_ERR(host->pinctrl)) {
            dev_err(&dev->dev, "failed to get pinctrl");
            ret = PTR_ERR(host->pinctrl);
            goto host_free;
        }

        host->pins_opendrain = pinctrl_lookup_state(host->pinctrl,
                                MMCI_PINCTRL_STATE_OPENDRAIN);
        if (IS_ERR(host->pins_opendrain)) {
            dev_err(mmc_dev(mmc), "Can't select opendrain pins\n");
            ret = PTR_ERR(host->pins_opendrain);
            goto host_free;
        }
    }

    host->hw_designer = amba_manf(dev);
    host->hw_revision = amba_rev(dev);
    dev_dbg(mmc_dev(mmc), "designer ID = 0x%02x\n", host->hw_designer);
    dev_dbg(mmc_dev(mmc), "revision = 0x%01x\n", host->hw_revision);

    host->clk = devm_clk_get(&dev->dev, NULL);
    if (IS_ERR(host->clk)) {
        ret = PTR_ERR(host->clk);
        goto host_free;
    }

    ret = clk_prepare_enable(host->clk);
    if (ret)
        goto host_free;

    if (variant->qcom_fifo)
        host->get_rx_fifocnt = mmci_qcom_get_rx_fifocnt;
    else
        host->get_rx_fifocnt = mmci_get_rx_fifocnt;

    host->plat = plat;
    host->variant = variant;
    host->mclk = clk_get_rate(host->clk);
    /*
    * According to the spec, mclk is max 100 MHz,
    * so we try to adjust the clock down to this,
    * (if possible).
    */
    if (host->mclk > variant->f_max) {
        ret = clk_set_rate(host->clk, variant->f_max);
        if (ret < 0)
            goto clk_disable;
        host->mclk = clk_get_rate(host->clk);
        dev_dbg(mmc_dev(mmc), "eventual mclk rate: %u Hz\n",
            host->mclk);
    }

    host->phybase = dev->res.start;
    host->base = devm_ioremap_resource(&dev->dev, &dev->res);
    if (IS_ERR(host->base)) {
        ret = PTR_ERR(host->base);
        goto clk_disable;
    }

    if (variant->init)
        variant->init(host);

    /*
    * The ARM and ST versions of the block have slightly different
    * clock divider equations which means that the minimum divider
    * differs too.
    * on Qualcomm like controllers get the nearest minimum clock to 100Khz
    */
    if (variant->st_clkdiv)
        mmc->f_min = DIV_ROUND_UP(host->mclk, 257);
    else if (variant->stm32_clkdiv)
        mmc->f_min = DIV_ROUND_UP(host->mclk, 2046);
    else if (variant->explicit_mclk_control)
        mmc->f_min = clk_round_rate(host->clk, 100000);
    else
        mmc->f_min = DIV_ROUND_UP(host->mclk, 512);
    /*
    * If no maximum operating frequency is supplied, fall back to use
    * the module parameter, which has a (low) default value in case it
    * is not specified. Either value must not exceed the clock rate into
    * the block, of course.
    */
    if (mmc->f_max)
        mmc->f_max = variant->explicit_mclk_control ?
                min(variant->f_max, mmc->f_max) :
                min(host->mclk, mmc->f_max);
    else
        mmc->f_max = variant->explicit_mclk_control ?
                fmax : min(host->mclk, fmax);


    dev_dbg(mmc_dev(mmc), "clocking block at %u Hz\n", mmc->f_max);

    host->rst = devm_reset_control_get_optional_exclusive(&dev->dev, NULL);
    if (IS_ERR(host->rst)) {
        ret = PTR_ERR(host->rst);
        goto clk_disable;
    }
    ret = reset_control_deassert(host->rst);
    if (ret)
        dev_err(mmc_dev(mmc), "failed to de-assert reset\n");

    /* Get regulators and the supported OCR mask */
    ret = mmc_regulator_get_supply(mmc);
    if (ret)
        goto clk_disable;

    if (!mmc->ocr_avail)
        mmc->ocr_avail = plat->ocr_mask;
    else if (plat->ocr_mask)
        dev_warn(mmc_dev(mmc), "Platform OCR mask is ignored\n");

    /* We support these capabilities. */
    mmc->caps |= MMC_CAP_CMD23;

    /*
    * Enable busy detection.
    */
    if (variant->busy_detect) {
        mmci_ops.card_busy = mmci_card_busy;
        /*
        * Not all variants have a flag to enable busy detection
        * in the DPSM, but if they do, set it here.
        */
        if (variant->busy_dpsm_flag)
            mmci_write_datactrlreg(host,
                           host->variant->busy_dpsm_flag);
        mmc->caps |= MMC_CAP_WAIT_WHILE_BUSY;
    }

    /* Variants with mandatory busy timeout in HW needs R1B responses. */
    if (variant->busy_timeout)
        mmc->caps |= MMC_CAP_NEED_RSP_BUSY;

    /* Prepare a CMD12 - needed to clear the DPSM on some variants. */
    host->stop_abort.opcode = MMC_STOP_TRANSMISSION;
    host->stop_abort.arg = 0;
    host->stop_abort.flags = MMC_RSP_R1B | MMC_CMD_AC;

    /* We support these PM capabilities. */
    mmc->pm_caps |= MMC_PM_KEEP_POWER;

    /*
    * We can do SGIO
    */
    mmc->max_segs = NR_SG;

    /*
    * Since only a certain number of bits are valid in the data length
    * register, we must ensure that we don't exceed 2^num-1 bytes in a
    * single request.
    */
    mmc->max_req_size = (1 << variant->datalength_bits) - 1;

    /*
    * Set the maximum segment size.  Since we aren't doing DMA
    * (yet) we are only limited by the data length register.
    */
    mmc->max_seg_size = mmc->max_req_size;

    /*
    * Block size can be up to 2048 bytes, but must be a power of two.
    */
    mmc->max_blk_size = 1 << variant->datactrl_blocksz;

    /*
    * Limit the number of blocks transferred so that we don't overflow
    * the maximum request size.
    */
    mmc->max_blk_count = mmc->max_req_size >> variant->datactrl_blocksz;

    spin_lock_init(&host->lock);

    writel(0, host->base + MMCIMASK0);

    if (variant->mmcimask1)
        writel(0, host->base + MMCIMASK1);

    writel(0xfff, host->base + MMCICLEAR);

    /*
    * If:
    * - not using DT but using a descriptor table, or
    * - using a table of descriptors ALONGSIDE DT, or
    * look up these descriptors named "cd" and "wp" right here, fail
    * silently of these do not exist
    */
    if (!np) {
        ret = mmc_gpiod_request_cd(mmc, "cd", 0, false, 0);
        if (ret == -EPROBE_DEFER)
            goto clk_disable;

        ret = mmc_gpiod_request_ro(mmc, "wp", 0, 0);
        if (ret == -EPROBE_DEFER)
            goto clk_disable;
    }

    ret = devm_request_threaded_irq(&dev->dev, dev->irq[0], mmci_irq,
                    mmci_irq_thread, IRQF_SHARED,
                    DRIVER_NAME " (cmd)", host);
    if (ret)
        goto clk_disable;

    if (!dev->irq[1])
        host->singleirq = true;
    else {
        ret = devm_request_irq(&dev->dev, dev->irq[1], mmci_pio_irq,
                IRQF_SHARED, DRIVER_NAME " (pio)", host);
        if (ret)
            goto clk_disable;
    }

    writel(MCI_IRQENABLE | variant->start_err, host->base + MMCIMASK0);

    amba_set_drvdata(dev, mmc);

    dev_info(&dev->dev, "%s: PL%03x manf %x rev%u at 0x%08llx irq %d,%d (pio)\n",
         mmc_hostname(mmc), amba_part(dev), amba_manf(dev),
         amba_rev(dev), (unsigned long long)dev->res.start,
         dev->irq[0], dev->irq[1]);

    mmci_dma_setup(host);

    pm_runtime_set_autosuspend_delay(&dev->dev, 50);
    pm_runtime_use_autosuspend(&dev->dev);

    mmc_add_host(mmc);

    pm_runtime_put(&dev->dev);
    return 0;

 clk_disable:
    clk_disable_unprepare(host->clk);
 host_free:
    mmc_free_host(mmc);
    return ret;
}

mmci_probe関数は次のように様々な処理を行うため、それぞれ分割して確認していく。

  • mmc_host 初期化
  • DT 読み込み
  • Open Drain Bit 非サポートの対応
  • clock 設定
  • 仮想アドレスへマップ
  • variant specific 対応
  • 周波数設定
  • Regulators の取得
  • GPIO descriptor 設定
  • IRQ 設定
  • DMA Engine の設定
  • Runtime PM の autosuspend 設定
  • host 登録

mmc_host 初期化

// 2012:
    mmc = mmc_alloc_host(sizeof(struct mmci_host), &dev->dev);
    if (!mmc)
        return -ENOMEM;

    host = mmc_priv(mmc);
    host->mmc = mmc;
    host->mmc_ops = &mmci_ops;
    mmc->ops = &mmci_ops;

MMC host ドライバでは、mmc_host構造体に MMC host コントローラを管理するためのメタデータを詰め込む。 mmc_host構造体には、MMC host コントローラ毎に異なる固有のメタデータprivate に格納することができる。 PL181の場合では、pricatemmci_host構造体を固有のメタデータを詰め込む。

mmc_host構造体の初期化は mmc_alloc_host関数で実施する。

// 513:
struct mmc_host *mmc_alloc_host(int extra, struct device *dev)
{
    int index;
    struct mmc_host *host;
    int alias_id, min_idx, max_idx;

    host = kzalloc(sizeof(struct mmc_host) + extra, GFP_KERNEL);
    if (!host)
        return NULL;

    /* scanning will be enabled when we're ready */
    host->rescan_disable = 1;

    alias_id = of_alias_get_id(dev->of_node, "mmc");
    if (alias_id >= 0) {
        index = alias_id;
    } else {
        min_idx = mmc_first_nonreserved_index();
        max_idx = 0;

        index = ida_simple_get(&mmc_host_ida, min_idx, max_idx, GFP_KERNEL);
        if (index < 0) {
            kfree(host);
            return NULL;
        }
    }

    host->index = index;

    dev_set_name(&host->class_dev, "mmc%d", host->index);
    host->ws = wakeup_source_register(NULL, dev_name(&host->class_dev));

    host->parent = dev;
    host->class_dev.parent = dev;
    host->class_dev.class = &mmc_host_class;
    device_initialize(&host->class_dev);
    device_enable_async_suspend(&host->class_dev);

    if (mmc_gpio_alloc(host)) {
        put_device(&host->class_dev);
        return NULL;
    }

    spin_lock_init(&host->lock);
    init_waitqueue_head(&host->wq);
    INIT_DELAYED_WORK(&host->detect, mmc_rescan);
    INIT_DELAYED_WORK(&host->sdio_irq_work, sdio_irq_work);
    timer_setup(&host->retune_timer, mmc_retune_timer, 0);

    /*
    * By default, hosts do not support SGIO or large requests.
    * They have to set these according to their abilities.
    */
    host->max_segs = 1;
    host->max_seg_size = PAGE_SIZE;

    host->max_req_size = PAGE_SIZE;
    host->max_blk_size = 512;
    host->max_blk_count = PAGE_SIZE / 512;

    host->fixed_drv_type = -EINVAL;
    host->ios.power_delay_ms = 10;
    host->ios.power_mode = MMC_POWER_UNDEFINED;

    return host;
}

mmc_alloc_host関数には、固有のメタデータのサイズ extraと デバイスメタデータ dev を受け取り、作成した mmc_host型のデータを返す。 この時、mmc_host構造体に最低限度の初期設定を施しているが、ここではすべてを確認することはしない。

mmc_alloc_host関数の成果物

DT 読み込み

// 2021:
    ret = mmci_of_parse(np, mmc);
    if (ret)
        goto host_free;

先述の通り、一部のパラメータ(コントローラ固有) はドライバから DT に切り出している。 MMCホストコントローラは、DT に記載されたパラメータを mmci_of_parse関数によって取得する。

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

// 1955:
static int mmci_of_parse(struct device_node *np, struct mmc_host *mmc)
{
    struct mmci_host *host = mmc_priv(mmc);
    int ret = mmc_of_parse(mmc);

    if (ret)
        return ret;

    if (of_get_property(np, "st,sig-dir-dat0", NULL))
        host->pwr_reg_add |= MCI_ST_DATA0DIREN;
    if (of_get_property(np, "st,sig-dir-dat2", NULL))
        host->pwr_reg_add |= MCI_ST_DATA2DIREN;
    if (of_get_property(np, "st,sig-dir-dat31", NULL))
        host->pwr_reg_add |= MCI_ST_DATA31DIREN;
    if (of_get_property(np, "st,sig-dir-dat74", NULL))
        host->pwr_reg_add |= MCI_ST_DATA74DIREN;
    if (of_get_property(np, "st,sig-dir-cmd", NULL))
        host->pwr_reg_add |= MCI_ST_CMDDIREN;
    if (of_get_property(np, "st,sig-pin-fbclk", NULL))
        host->pwr_reg_add |= MCI_ST_FBCLKEN;
    if (of_get_property(np, "st,sig-dir", NULL))
        host->pwr_reg_add |= MCI_STM32_DIRPOL;
    if (of_get_property(np, "st,neg-edge", NULL))
        host->clk_reg_add |= MCI_STM32_CLK_NEGEDGE;
    if (of_get_property(np, "st,use-ckin", NULL))
        mmci_probe_level_translator(mmc);

    if (of_get_property(np, "mmc-cap-mmc-highspeed", NULL))
        mmc->caps |= MMC_CAP_MMC_HIGHSPEED;
    if (of_get_property(np, "mmc-cap-sd-highspeed", NULL))
        mmc->caps |= MMC_CAP_SD_HIGHSPEED;

    return 0;
}

mmci_of_parse関数では、MMC hostコントローラ共通のパラメータを読み込む mmc_of_parse関数と、MMC hostコントローラ固有のパラメータを読み込む mmci_of_parse関数を呼び出す。 mmci_of_parse関数内でパースされるプロパティはドキュメントにも記載されている。

elixir.bootlin.com

mmc_of_parse`関数内でパースされるプロパティもドキュメントに記載されている。

elixir.bootlin.com

今回の環境では mmci_of_parse関数でパースされるパラメータは次のとおりである。

プロパティ 概要
interrupts 9, 10 コマンド割り込み と ポーリング割り込み
cd-gpios v2m_mmc_gpios 0 0 Card Detection (CD) 用の GPIO
wp-gpios v2m_mmc_gpios 1 0 Write Protect (WP) 用の GPIO
max-frequency 12000000 バスの最大動作周波数
vmmc-supply v2m_fixed_3v3 電源供給
clocks v2m_clk24mhz, smbclk "apb_pclk" と "MCLK"
clock-names mclk, apb_pclk Clock名

Open Drain Bit 非サポートの対応

// 2029:
    if (!variant->opendrain) {
        host->pinctrl = devm_pinctrl_get(&dev->dev);
        if (IS_ERR(host->pinctrl)) {
            dev_err(&dev->dev, "failed to get pinctrl");
            ret = PTR_ERR(host->pinctrl);
            goto host_free;
        }

        host->pins_opendrain = pinctrl_lookup_state(host->pinctrl,
                                MMCI_PINCTRL_STATE_OPENDRAIN);
        if (IS_ERR(host->pins_opendrain)) {
            dev_err(mmc_dev(mmc), "Can't select opendrain pins\n");
            ret = PTR_ERR(host->pins_opendrain);
            goto host_free;
        }
    }

Open Drain bit がない機種向けに pinctrl を使用することで対応することになっている。 ただし、今回はここに該当しないため詳細は省く。

clock 設定

ここで、APB と MCLK の初期設定をする。

// 2051:
    host->clk = devm_clk_get(&dev->dev, NULL);
    if (IS_ERR(host->clk)) {
        ret = PTR_ERR(host->clk);
        goto host_free;
    }

    ret = clk_prepare_enable(host->clk);
    if (ret)
        goto host_free;

APB は devm_clk_get関数によってhost->clk に設定される。

// 2068:
    host->mclk = clk_get_rate(host->clk);
    /*
    * According to the spec, mclk is max 100 MHz,
    * so we try to adjust the clock down to this,
    * (if possible).
    */
    if (host->mclk > variant->f_max) {
        ret = clk_set_rate(host->clk, variant->f_max);
        if (ret < 0)
            goto clk_disable;
        host->mclk = clk_get_rate(host->clk);
        dev_dbg(mmc_dev(mmc), "eventual mclk rate: %u Hz\n",
            host->mclk);
    }

MCLK は host->clkと同じ周波数で設定される。

仮想アドレスへマップ

// 2083:
    host->phybase = dev->res.start;
    host->base = devm_ioremap_resource(&dev->dev, &dev->res);
    if (IS_ERR(host->base)) {
        ret = PTR_ERR(host->base);
        goto clk_disable;
    }

Linuxカーネルでは、MMUを有効化しているため、Linuxカーネルから物理アドレスにアクセスするためにはioremap関数などでマッピングする必要がある。 そこで、devm_ioremap_resource関数ではDevres(Managed Device Resource) による管理をしながら host->baseマッピングする。

ioremap関数で仮想アドレスにマッピングする

variant specific 対応

// 2090:
    if (variant->init)
        variant->init(host);

今回の環境では、mmaci_variant_opsが設定される。

// 2124:
    host->rst = devm_reset_control_get_optional_exclusive(&dev->dev, NULL);
    if (IS_ERR(host->rst)) {
        ret = PTR_ERR(host->rst);
        goto clk_disable;
    }
    ret = reset_control_deassert(host->rst);
    if (ret)
        dev_err(mmc_dev(mmc), "failed to de-assert reset\n");

STM32 sdmmc では、電源再投入時にリセット処理が必要になる。ただし、今回の環境では必要ないため省略する。

// 2149:
    if (variant->busy_detect) {
        mmci_ops.card_busy = mmci_card_busy;
        /*
        * Not all variants have a flag to enable busy detection
        * in the DPSM, but if they do, set it here.
        */
        if (variant->busy_dpsm_flag)
            mmci_write_datactrlreg(host,
                           host->variant->busy_dpsm_flag);
        mmc->caps |= MMC_CAP_WAIT_WHILE_BUSY;
    }

    /* Variants with mandatory busy timeout in HW needs R1B responses. */
    if (variant->busy_timeout)
        mmc->caps |= MMC_CAP_NEED_RSP_BUSY;

ux500, STM32 では、busy_detectフラグによって HWによるビジー検出をサポートしている。ただし、今回の環境では必要ないため省略する。

周波数設定

// 2093:
    /*
    * The ARM and ST versions of the block have slightly different
    * clock divider equations which means that the minimum divider
    * differs too.
    * on Qualcomm like controllers get the nearest minimum clock to 100Khz
    */
    if (variant->st_clkdiv)
        mmc->f_min = DIV_ROUND_UP(host->mclk, 257);
    else if (variant->stm32_clkdiv)
        mmc->f_min = DIV_ROUND_UP(host->mclk, 2046);
    else if (variant->explicit_mclk_control)
        mmc->f_min = clk_round_rate(host->clk, 100000);
    else
        mmc->f_min = DIV_ROUND_UP(host->mclk, 512);
    /*
    * If no maximum operating frequency is supplied, fall back to use
    * the module parameter, which has a (low) default value in case it
    * is not specified. Either value must not exceed the clock rate into
    * the block, of course.
    */
    if (mmc->f_max)
        mmc->f_max = variant->explicit_mclk_control ?
                min(variant->f_max, mmc->f_max) :
                min(host->mclk, mmc->f_max);
    else
        mmc->f_max = variant->explicit_mclk_control ?
                fmax : min(host->mclk, fmax);

ARM版とST版では最大/最小動作周波数が異なる。 そのため、variantに応じて値を調整する。

Regulators の取得

// 2133:
    /* Get regulators and the supported OCR mask */
    ret = mmc_regulator_get_supply(mmc);
    if (ret)
        goto clk_disable;

    if (!mmc->ocr_avail)
        mmc->ocr_avail = plat->ocr_mask;
    else if (plat->ocr_mask)
        dev_warn(mmc_dev(mmc), "Platform OCR mask is ignored\n");

Linuxカーネルでは、他の機器に電力を共有する電子機器 (レギュレータ) のAPI が多数用意されている。

SDカードは、3.3V (第二ロウの場合には1.8Vも必要) の電源が必要になる。 今回は、DT で vmmc-supplyv2m_fixed_3v3 ( 電圧固定のレギュレータ 3.3V) を指定しているため、mmc_regulator_get_supply関数で mmc->supply.vmmc に レギュレータが設定される。

MMC/SD/SDIO には operation conditions register (OCR) と呼ばれる32bit のレジスタを持つ。 このレジスタには MMC/SD/SDIOに供給可能な電圧のマスクが格納される。

GPIO descriptor 設定

// 2218:
    if (!np) {
        ret = mmc_gpiod_request_cd(mmc, "cd", 0, false, 0);
        if (ret == -EPROBE_DEFER)
            goto clk_disable;

        ret = mmc_gpiod_request_ro(mmc, "wp", 0, 0);
        if (ret == -EPROBE_DEFER)
            goto clk_disable;
    }

DT ではなく GPIO descriptor によって cdwp を設定することができる。 ただし、今回の環境では DT から設定するため省略する。

IRQ 設定

// 2204:
    writel(0, host->base + MMCIMASK0);

    if (variant->mmcimask1)
        writel(0, host->base + MMCIMASK1);

PL180/1 では、 MMCIMASK0MMCIMASK1 を割り込みマスクレジスタとなる。 割り込みハンドラの設定の前に各種割り込みの入力を禁止 (0x0000) しておく。

// 2228:
    ret = devm_request_threaded_irq(&dev->dev, dev->irq[0], mmci_irq,
                    mmci_irq_thread, IRQF_SHARED,
                    DRIVER_NAME " (cmd)", host);
    if (ret)
        goto clk_disable;

    if (!dev->irq[1])
        host->singleirq = true;
    else {
        ret = devm_request_irq(&dev->dev, dev->irq[1], mmci_pio_irq,
                IRQF_SHARED, DRIVER_NAME " (pio)", host);
        if (ret)
            goto clk_disable;
    }

    writel(MCI_IRQENABLE | variant->start_err, host->base + MMCIMASK0);

PL180/1 ドライバでは、最大で二つの割り込み信号を提供している。

  • コマンド(cmd): コマンド終了時のイベントに対応する
  • Polling I/O (pio): カードからの一括読み出しの一部として FIFO を空にする必要がある場合に発生する

devm_request_threaded_irq関数と devm_request_irq関数によって、指定したIRQに割り込みハンドラを登録する。

PL180/1 ドライバでは、前者の割り込み(cmd) の場合はmmci_irq関数を割り込みハンドラ、後者の割り込み(pio) の場合はmmci_pio_irq関数を割り込みハンドラとする。 また、cmdに対する割り込み IRQ_ENABLE | variant->start_err の一部 (0x002f) を有効化する。

コマンド終了時のイベントに対応するフラグ

DMA Engine の設定

// 2252:
    mmci_dma_setup(host);

今回の環境では DMA Engine を使用していないため省略する。

Runtime PM の autosuspend 設定

// 2254:
    pm_runtime_set_autosuspend_delay(&dev->dev, 50);
    pm_runtime_use_autosuspend(&dev->dev);

I/Oデバイス で autosuspend を有効にするためには、 pm_runtime_use_autosuspend関数を呼び出す必要がある。

git.kernel.org

また、すべてのリクエストにレイテンシ追加を防ぐために、autosuspend に50 ms の delay を設定する必要がある。

host 登録

// 2257:
    mmc_add_host(mmc);

これまでの処理でHOst コントローラの準備が完了したため、mmc_add_host関数によってドライバモデルに登録する。 mmc_add_host関数の定義は次のようになっている。

// 590:
int mmc_add_host(struct mmc_host *host)
{
    int err;

    WARN_ON((host->caps & MMC_CAP_SDIO_IRQ) &&
        !host->ops->enable_sdio_irq);

    err = device_add(&host->class_dev);
    if (err)
        return err;

    led_trigger_register_simple(dev_name(&host->class_dev), &host->led);

#ifdef CONFIG_DEBUG_FS
    mmc_add_host_debugfs(host);
#endif

    mmc_start_host(host);
    return 0;
}

mmc_add_host関数では、Host コントローラの最終セットアップとしていくつかの処理を実施する。

  • /sys/devices 下に MMCI 用のサブディレクトリを作成
  • /sys/kernel/debug/mmc0/ 以下に MMC用のサブディレクトリを作成
  • LED Triggers の登録
  • Card Detect フラグを有効化

この中でも重要な処理となる mmc_start_host関数について、次回以降で確認する。

その他の設定

これ以外にも、PL180/1 を利用するにあたって、MMCIのメタデータ host のパラメータを設定する。

// 2046:
    host->hw_designer = amba_manf(dev);
    host->hw_revision = amba_rev(dev);
    dev_dbg(mmc_dev(mmc), "designer ID = 0x%02x\n", host->hw_designer);
    dev_dbg(mmc_dev(mmc), "revision = 0x%01x\n", host->hw_revision);

PL180/1 ドライバ で定義されている periphid から designer ID と revision を設定する。

// 2173:
    /*
    * We can do SGIO
    */
    mmc->max_segs = NR_SG;

    /*
    * Since only a certain number of bits are valid in the data length
    * register, we must ensure that we don't exceed 2^num-1 bytes in a
    * single request.
    */
    mmc->max_req_size = (1 << variant->datalength_bits) - 1;

    /*
    * Set the maximum segment size.  Since we aren't doing DMA
    * (yet) we are only limited by the data length register.
    */
    mmc->max_seg_size = mmc->max_req_size;

    /*
    * Block size can be up to 2048 bytes, but must be a power of two.
    */
    mmc->max_blk_size = 1 << variant->datactrl_blocksz;

    /*
    * Limit the number of blocks transferred so that we don't overflow
    * the maximum request size.
    */
    mmc->max_blk_count = mmc->max_req_size >> variant->datactrl_blocksz;

1回のリクエストで 2num-1 を超えないようにしなければならないので max_req_size を指定しなければならない。 max_seg_sizeはデータ長レジスタによって制限されるので、仮で max_req_size を設定する。 max_blk_size は 2n かつ 2048 以下で設定する。 また、最大リクエストサイズがオーバーフローしないように max_blk_count で転送ブロック数を制限する。

おわりに

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

変更履歴

  • 2024/01/04: 記事公開
  • 2024/02/18: タイトル修正

参考

syzkaller によるファジングテストの実施とテストのカスタマイズ

概要

本稿では、この QEMU による syzkallerの x86_64カーネルのファジングを実施した。

syzkaller は Google が開発したカーネルのファジングツールである。 ファジングを適切に実施することで、規模が膨大なソフトウェア Linuxカーネルの不具合・バグを検出することができる。 しかし、設定方法を誤って実施してしまうと、不具合・バグを見過ごすことになり、リソースの無駄となってしまう。

そこで、公式ドキュメントやブログなどの情報を基に、syzkaller をインストールし、テストを実施した。本稿では、その時のインストール手順やカスタマイズ方法についてまとめる。

はじめに

近年、ソフトウェアの規模が飛躍的に増大しており、潜在バグが存在する可能性が高いため、ソフトウェアテストの実施が必要不可欠になりつつある。 ソフトウェアが期待するに動作することを確認するためにソフトウェアテストを実施することが多い。
しかし、ソフトウェアの規模が大きくなっている昨今では、すべてのケースを網羅したテストを作成・実施することは現実的ではない。

「ファジング」は近年注目されているソフトウェアテストの手法の一つで、検査対象のソフトウェアに対して問題を引き起こしそうなデータを入力し、その応答や挙動を監視する手法である。 ファジングではツールを用いて自動化することが多く、非常に多くのケースを実行することができる。

Linuxカーネルは、ソースコードの行数だけ見ても 2000万行を超える大きなソフトウェアであり、セキュリティの重要性も高くなっている。 syzkaller は Google が開発したカーネルのファジングツールの一つとなっている。

github.com

syzkaller 自体は Go言語で記述されたプログラムであり、Linux を含め様々な OS をサポートしている。

実験環境

本稿では、Ubuntu PC(x86-64)上に QEMU によるファジングするような syzkaller を実施する。

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

環境 概要
CPU AMD Ryzen 3 3300X
RAM DDR4-2666 16GB ×2
OS Ubuntu Desktop 22.04.03 LTS
Host kernel 6.2.0-37-generic
syzkaller 28b24332d95f2f7df44ec7e7a5e0025bcadc6277
Guest kernel v6.6.5

セットアップ

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

Linux Kernelのビルド

  1. カーネルのビルドに必要なパッケージをインストールする

     leava@ubuntu:/work/$ sudo apt update
     leava@ubuntu:/work/$ sudo apt install make gcc flex bison libncurses-dev libelf-dev libssl-dev
    
  2. Linuxソースコードを取得する

     leava@ubuntu:/work/$ export KERNEL=linux-6.6.5
     leava@ubuntu:/work/$ wget https://cdn.kernel.org/pub/linux/kernel/v${KERNEL:6:1}.x/${KERNEL}.tar.xz
     leava@ubuntu:/work/$ tar xf ${KERNEL}.tar.gz
     leava@ubuntu:/work/$ cd ${KERNEL}
    
  3. QEMU/KVM の実行用にデフォルト構成からコンフィグを生成する

     leava@ubuntu:/work/linux-6.6.5/$ make defconfig
     leava@ubuntu:/work/linux-6.6.5/$ make kvm_guest.config
    
  4. syzkaller に必要なコンフィグを追記する

     leava@ubuntu:/work/linux-6.6.5/$ echo "CONFIG_KCOV=y\nCONFIG_DEBUG_INFO_DWARF4=y\nCONFIG_KASAN=y\nCONFIG_KASAN_INLINE=y\nCONFIG_CONFIGFS_FS=y\nCONFIG_SECURITYFS=y" >> .config
    
  5. トラブル回避のために、ネットワークインターフェースの名前が予測可能機能を無効にする

     leava@ubuntu:/work/linux-6.6.5/$ echo "CONFIG_CMDLINE_BOOL=y\nCONFIG_CMDLINE=\"net.ifnames=0\"" >> .config
    
  6. Linuxカーネルをビルドする

     leava@ubuntu:/work/linux-6.6.5/$ make -j$(nproc)
    
  7. 成果物を確認する

     leava@ubuntu:/work/linux-6.6.5/$ ls vmlinux
     vmlinux
     leava@ubuntu:/work/linux-6.6.5/$ ls arch/arm64/boot/Image
     arch/arm64/boot/Image
    

rootfsの生成

  1. ベースイメージの生成に必要なパッケージをインストールする

     leava@ubuntu:/work/$sudo apt install debootstrap
    
  2. イメージの生成準備

     leava@ubuntu:/work/$ export IMAGE=image
     leava@ubuntu:/work/$ mkdir ${IMAGE}
     leava@ubuntu:/work/$ cd ${IMAGE}
    
  3. Debian Bullseye イメージから rootfs を生成する

     leava@ubuntu:/work/image$ wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.sh
     leava@ubuntu:/work/image$ chmod +x create-image.sh
     leava@ubuntu:/work/image$ ./create-image.sh
    
  4. 成果物を確認する

     leava@ubuntu:/work/image$ ls bullseye.img
     bullseye.img
    

仮想マシンで起動確認

  1. QEMU/KVM の実行に必要なパッケージをインストールする

     leava@ubuntu:/work/$ sudo apt install qemu-system-x86
    
  2. 生成したカーネルQEMU/KVM を起動する

     leava@ubuntu:/work/$ qemu-system-x86_64 \
                                  -m 2G \
                                  -smp 2 \
                                  -kernel linux-6.6.5/arch/x86/boot/bzImage \
                                  -append "console=ttyS0 root=/dev/sda earlyprintk=serial net.ifnames=0" \
                                  -drive file=image/bullseye.img,format=raw \
                                  -net user,host=10.0.2.10,hostfwd=tcp:127.0.0.1:10021-:22 \
                                  -net nic,model=e1000 \
                                  -enable-kvm \
                                  -nographic \
                                  -pidfile vm.pid \
                                  2>&1 | tee vm.log
    
  3. 別の端末から QEMUインスタンスssh接続できるかどうかを確認する

     leava@ubuntu:/work/$ ssh -i image/bullseye.id_rsa -p 10021 -o  "StrictHostKeyChecking no" root@localhost
    

syzkaller のビルド

  1. Goのソースコードを公式サイトからダウンロードする

     leava@ubuntu:/work/$ wget https://dl.google.com/go/go1.21.4.linux-amd64.tar.gz
     leava@ubuntu:/work/$ tar -xf go1.21.4.linux-amd64.tar.gz
    
  2. ホスト環境にGoの実行環境をセットアップする

     leava@ubuntu:/work/$ export PATH=$PATH:/usr/local/go/bin
     leava@ubuntu:/work/$ export PATH=$GOROOT/bin:$PATH
    
  3. Goの実行環境が正しくインストールされているか確認する

     leava@ubuntu:/work/$ go version
     leava@ubuntu:/work/$ go version go1.21.4 linux/amd64
    
  4. syzkaller のソースコードを取得する

     leava@ubuntu:/work/$ git clone https://github.com/google/syzkaller.git
     leava@ubuntu:/work/$ cd syzkaller
    
  5. syzkaller をビルドする

     leava@ubuntu:/work/syzkaller/$ make
    

syzkaller の設定ファイルの準備

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

// 1:
{
        "target": "linux/amd64",
        "http": "127.0.0.1:56741",
        "workdir": "workdir",
        "kernel_obj": "../linux-6.6.5",
        "image": "../image/bullseye.img",
        "sshkey": "../image/bullseye.id_rsa",
        "syzkaller": "../syzkaller",
        "procs": 8,
        "type": "qemu",
        "vm": {
                "count": 4,
                "kernel": "../linux-6.6.5/arch/x86/boot/bzImage",
                "cpu": 2,
                "mem": 2048
        }
}

設定できるパラメータは、公式ドキュメント syzkaller/docs/configuration.md から参照することができる。

変数 概要
target ファジングする対象のOS
http syzkallerのビューワとして出力する URL
workdir 実行に使用する一時ファイル置き場
kernel_obj カーネルソースツリー
image rootfsディスクイメージ
sshkey rootfsのSSH秘密鍵
syzkaller syzkallerのトップディレクト
procs VM内の並列プロセス数
type VMの種類
vm VMのパラメータ

syzkallerの実行

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

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

syzkaller の起動に成功すると、ブラウザ (http://127.0.0.1:56700/)からダッシュボードにアクセスすることができる。
下記は syzkaller 実行から1日経過したときの様子をスクリーンショットしたものである。

main ダッシュボード

syzkallerの トップ画面には "Status" "Crashes", "Logs"の3つのセクションに分かれている。

  • Status: ツールの実行環境や実行結果を確認することができる
  • Crashes: 実行中にシステムがクラッシュしたときにその詳細や再現方法を確認することができる
  • Log: ツール実行中のログを確認することができる

トップページにある "corpus" をクリック/タップすると実施されたコーパス (システムコールの入力データセット) の詳細を確認することができる。

corpus ダッシュボード

一方で、トップページにある "coverage" をクリック/タップするとコードカバレッジを確認することができる。

coverage ダッシュボード

また、"Crashes" セクションには、syzkaller 実行中に VM で発生したカーネルクラッシュが記録されている。 記録されたカーネルクラッシュごとにログやレポートを確認することができ、再現プログラムも自動生成されることもある。

Crashes ダッシュボード

カスタマイズ

Syscall descriptionの追加

syzkaller では、システムコール記述言語syzlang を使用して入力データ(Syscall description)を定義する。

github.com

ここで、システムコールにどのようなパラメータを与えるかの情報を付加する。 syzkaller はこの情報を基に、意味のあるインプットをプログラムに与えていく。

そこで、既存の構成ファイルを更新し、入力データを追加する手法を確認していく。 今回は、既存の ptraceに対して固定値 0xdeadbeafを入力するような入力データを追加する。

これを実現するために次の3つの作業を実施することになる。

  • syscall descriptionの作成
  • syscall descriptionを Goコードに変換する (.const中間ファイルを介して)
  • syskaller を再ビルドする

syscall description については、既存 sys/linux/sys.txtの内容を流用する。 また、期待通りに syzkaller が追加された情報を使ったどうかを判断するために、カーネル側に手を加えておく。

// 1:
diff --git a/kernel/ptrace.c b/kernel/ptrace.c
index 443057b..66505f7 100644
--- a/kernel/ptrace.c
+++ b/kernel/ptrace.c
@@ -1281,6 +1281,9 @@ SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
        struct task_struct *child;
        long ret;

+       if (pid == 0xdeadbeaf)
+               panic("Invalid process ID\n");
+
        if (request == PTRACE_TRACEME) {
                ret = ptrace_traceme();
                goto out;

上記のパッチを当てたカーネルをビルドしておくことで、syzkaller が "Invalid Process ID"パニックが観測できるかどうかを確認していく。 まずは、syscall description の作成から手掛けていく。

  1. 必要なパッケージをインストールする

     leava@ubuntu:/work/$ sudo apt update
     leava@ubuntu:/work/$ sudo apt install clang-format
    
  2. 最小限の syscall description を生成する。

     leava@ubuntu:/work/syzkaller$ echo "ptrace$panic(req int32, pid const[0xdeadbeaf])" > sys/linux/ptrace_panic.txt
    
  3. syscall description とカーネルソースから amd64向けの中間ファイル.constを生成する

     leava@ubuntu:/work/syzkaller$ make bin/syz-extract
     leava@ubuntu:/work/syzkaller$ bin/syz-extract -os linux -arch amd64 -sourcedir ../${KERNEL} -builddir ../${KERNEL} ptrace_panic.txt
    
  4. syzkaller から使えるように Go コードに変換する

     leava@ubuntu:/work/syzkaller$ make generate
    
  5. syzkaller を再ビルドする

     leava@ubuntu:/work/syzkaller$ make
    

これで、syzkaller を実行すると追加された ptrace$panicが実行されるようになる。

このままでも問題ないが、確認を容易にできるするために ptrace$panicのみシステムコールを有効にする。 enable_syscalls, disable_syscallsオプションを使用することで、実行するシステムコールのセットを有効/無効にすることができる。

leava@ubuntu:/work/syzkaller$ sed "1a \        \"enable_syscalls\": [\"ptrace$panic\"],"

このコンフィグファイルを入力として、syz-manager を実行する。
下記は syzkaller実行から20分経過したときの様子をスクリーンショットしたものである。

ptrace$panic によるsyzkallerトップ画面

syzkaller のトップ画面から kernel panic: Invalid proceed IDが確認できる。 また、syzkaller による再現プログラムも確認することができる。

Syzkaller hit 'kernel panic: Invalid process ID' bug.

audit: type=1400 audit(1702720428.078:6): avc:  denied  { execmem } for  pid=220 comm="syz-executor250" scontext=system_u:system_r:kernel_t:s0 tcontext=system_u:system_r:kernel_t:s0 tclass=process permissive=1
Kernel panic - not syncing: Invalid process ID
CPU: 1 PID: 221 Comm: syz-executor250 Not tainted 6.6.5 #7
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
Call Trace:
 <TASK>
 __dump_stack lib/dump_stack.c:88 [inline]
 dump_stack_lvl+0x50/0x70 lib/dump_stack.c:106
 panic+0x53e/0x5c0 kernel/panic.c:340
 __do_sys_ptrace kernel/ptrace.c:1285 [inline]
 __se_sys_ptrace kernel/ptrace.c:1278 [inline]
 __x64_sys_ptrace+0x24e/0x250 kernel/ptrace.c:1278
 do_syscall_x64 arch/x86/entry/common.c:50 [inline]
 do_syscall_64+0x3f/0x90 arch/x86/entry/common.c:80
 entry_SYSCALL_64_after_hwframe+0x6e/0xd8
RIP: 0033:0x7fa24ee5464d
Code: c3 e8 a7 1f 00 00 0f 1f 80 00 00 00 00 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 b8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007fffa2c5da18 EFLAGS: 00000246 ORIG_RAX: 0000000000000065
RAX: ffffffffffffffda RBX: 00007fffa2c5dc78 RCX: 00007fa24ee5464d
RDX: 0000000000000000 RSI: 00000000deadbeaf RDI: 0000000000000006
RBP: 0000000000000000 R08: 00007fffa2c5d480 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000001
R13: 431bde82d7b634db R14: 00007fa24eed14f0 R15: 0000000000000001
 </TASK>
Kernel Offset: 0x15200000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)


Syzkaller reproducer:
# {Threaded:false Repeat:true RepeatTimes:0 Procs:1 Slowdown:1 Sandbox: SandboxArg:0 Leak:false NetInjection:false NetDevices:false NetReset:false Cgroups:false BinfmtMisc:false CloseFDs:false KCSAN:false DevlinkPCI:false NicVF:false USB:false VhciInjection:false Wifi:false IEEE802154:false Sysctl:false Swap:false UseTmpDir:false HandleSegv:false Repro:false Trace:false LegacyOptions:{Collide:false Fault:false FaultCall:0 FaultNth:0}}
ptrace$panic(0x6, 0xdeadbeaf)


C reproducer:
// autogenerated by syzkaller (https://github.com/google/syzkaller)

#define _GNU_SOURCE

#include <dirent.h>
#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

static void sleep_ms(uint64_t ms)
{
  usleep(ms * 1000);
}

static uint64_t current_time_ms(void)
{
  struct timespec ts;
  if (clock_gettime(CLOCK_MONOTONIC, &ts))
    exit(1);
  return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
}

static bool write_file(const char* file, const char* what, ...)
{
  char buf[1024];
  va_list args;
  va_start(args, what);
  vsnprintf(buf, sizeof(buf), what, args);
  va_end(args);
  buf[sizeof(buf) - 1] = 0;
  int len = strlen(buf);
  int fd = open(file, O_WRONLY | O_CLOEXEC);
  if (fd == -1)
    return false;
  if (write(fd, buf, len) != len) {
    int err = errno;
    close(fd);
    errno = err;
    return false;
  }
  close(fd);
  return true;
}

static void kill_and_wait(int pid, int* status)
{
  kill(-pid, SIGKILL);
  kill(pid, SIGKILL);
  for (int i = 0; i < 100; i++) {
    if (waitpid(-1, status, WNOHANG | __WALL) == pid)
      return;
    usleep(1000);
  }
  DIR* dir = opendir("/sys/fs/fuse/connections");
  if (dir) {
    for (;;) {
      struct dirent* ent = readdir(dir);
      if (!ent)
        break;
      if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
        continue;
      char abort[300];
      snprintf(abort, sizeof(abort), "/sys/fs/fuse/connections/%s/abort",
               ent->d_name);
      int fd = open(abort, O_WRONLY);
      if (fd == -1) {
        continue;
      }
      if (write(fd, abort, 1) < 0) {
      }
      close(fd);
    }
    closedir(dir);
  } else {
  }
  while (waitpid(-1, status, __WALL) != pid) {
  }
}

static void setup_test()
{
  prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0);
  setpgrp();
  write_file("/proc/self/oom_score_adj", "1000");
}

static void execute_one(void);

#define WAIT_FLAGS __WALL

static void loop(void)
{
  int iter = 0;
  for (;; iter++) {
    int pid = fork();
    if (pid < 0)
      exit(1);
    if (pid == 0) {
      setup_test();
      execute_one();
      exit(0);
    }
    int status = 0;
    uint64_t start = current_time_ms();
    for (;;) {
      if (waitpid(-1, &status, WNOHANG | WAIT_FLAGS) == pid)
        break;
      sleep_ms(1);
      if (current_time_ms() - start < 5000)
        continue;
      kill_and_wait(pid, &status);
      break;
    }
  }
}

void execute_one(void)
{
  syscall(__NR_ptrace, /*req=*/6, /*pid=*/0xdeadbeaful, 0, 0);
}
int main(void)
{
  syscall(__NR_mmap, /*addr=*/0x1ffff000ul, /*len=*/0x1000ul, /*prot=*/0ul,
          /*flags=*/0x32ul, /*fd=*/-1, /*offset=*/0ul);
  syscall(__NR_mmap, /*addr=*/0x20000000ul, /*len=*/0x1000000ul, /*prot=*/7ul,
          /*flags=*/0x32ul, /*fd=*/-1, /*offset=*/0ul);
  syscall(__NR_mmap, /*addr=*/0x21000000ul, /*len=*/0x1000ul, /*prot=*/0ul,
          /*flags=*/0x32ul, /*fd=*/-1, /*offset=*/0ul);
  loop();
  return 0;
}

Pseudo Syscallの追加

syzkaller は(基本的に)システムコールに対するファジングをすることで、バグのレポートや網羅率の集計なども取得することできる。 ただし、システムコール単位ではファイルシステムのようなサブシステムには向かないケースもある。

syzkaller では、通常のシステムコールのほかに、疑似システムコール(Pseudo syscall)と呼ばれる独自のアクションを追加することができる。 例えば、疑似システムコールの一つsyz_image_mountは指定したファイルシステムをループバックマウントし、カレントディレクトリを変更する。

そこで、Pseudo Syscall syz_mycall を追加する手法を確認していく。ただし、意味もなく Pseudo Syscall を追加することは推奨されていない。

Use of pseudo-syscalls is generally discouraged because they ruin all advantages of the declarative descriptions (declarativeness, conciseness, fuzzer control over all aspects, possibility of global improvements to the logic, static checking, fewer bugs, etc), increase maintenance burden, are non-reusable and make C reproducers longer.

github.com

syzkallerからPseudo Syscallを実行できるようにするためには

  • executor に Pseudo Syscall を追加する
  • 該当の Pseudo Syscall が該当OSでサポートされていることを明記する
  • Syscall description の追加
  • テストの追加 [任意]

テストの追加については、今回の確認作業では省略する。

  1. Pseudo Syscallを追加

     leava@ubuntu:/work/syzkaller$ cat << EOF >> executor/common_linux.h
    
     #if SYZ_EXECUTOR
     static long syz_mycall(volatile long x, volatile long y)
     {
            return x + y;
     }
     #endif
     EOF
    
  2. Linux で syz_mycall をサポートしていることを明記する

     leava@ubuntu:/work/syzkaller$ sed -i '/syz_socket_connect_nvme_tcp/a\       \"syz_mycall\":                  isSyzmycallSupported,' pkg/host/syscalls_linux.go
     leava@ubuntu:/work/syzkaller$ cat << EOF >> pkg/host/syscalls_linux.go
    
    
     func isSyzmycallSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
            return true, ""
     }
     EOF
    
  3. Syscall description の追加

     leava@ubuntu:/work/syzkaller$ echo "syz_mycall(x int32[0:10], y int32[0:10])" > sys/linux/mysyscall.txt
    
  4. syzkaller を再ビルド (pseudo syscall 含む) する

     leava@ubuntu:/work/syzkaller$ make bin/syz-extract
     leava@ubuntu:/work/syzkaller$ bin/syz-extract -os linux -arch amd64 -sourcedir ../${KERNEL} -builddir ../${KERNEL} mysyscall.txt
     leava@ubuntu:/work/syzkaller$ make generate
     leava@ubuntu:/work/syzkaller$ make
    

おわりに

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

変更履歴

  • 2023/12/18: 記事公開

参考文献

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

関連記事

概要

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

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

はじめに

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

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

MMCサブシステム

マルチメディアカード (MMC) は 1997年に発表されたメモリーカードの規格の一つである。
SDメモリカード (SDC) は、MMC と互換性のある規格で、デジタルカメラや携帯電話など現在でも幅広く利用されている。

Linux では、e.MMC や SDカード といったストレージメディアをMMCサブシステムで扱う。

Linux MMCサブシステムアーキテクチャ

  • MMC block: 上位の汎用ブロックデバイスからのI/O要求を受け取り、データを管理する
  • MMC core: 対象となるストレージメディアが SDC や MMC かといった規格を判別し、規格固有の制御(スピードモードの設定など)する
  • MMC host:: ホストコントローラのレジスタを操作、機種依存の差をここで実装する

Linuxバイスモデル

Linuxでは、システムの接続状態を管理にできるように共通フレームワーク (Linuxバイスモデル) が提供されている。
Linuxバイスモデルは、バス、デバイス、ドライバの3つで構成される。

Linuxバイスモデルの概略

また、これらに加えて、クラスと呼ばれる概念も存在する。 クラスでは、デバイスを上位レベルでとらえたもので、下位レベルを抽象化する。

MMCコアの初期化

Linuxでは、カーネル起動時にあらかじめ登録されていたコールバック関数 initcall を呼ぶ機構がある。
initcallは、ブートプロセス中のいくつかの段階で多くの関数を呼び出すことができ、多くのアーキテクチャやドライバで使用される。

initcallのタイミング (レベル) とそのマクロ名の対応関係は次のようになっている。

Macro Level
pure_initcall 0
core_initcall 1
postcore_initcall 2
arch_initcall 3
subsys_initcall 4
fs_initcall 5
rootfs_initcall rootfs
device_initcall 6
late_initcall 7

MMCでは、 mmc_init関数や mmc_blk_init関数(device)によって最低限の初期化処理がされる。
まず、subsys_initcallmmc_init関数から確認する。

// 2295:
static int __init mmc_init(void)
{
    int ret;

    ret = mmc_register_bus();
    if (ret)
        return ret;

    ret = mmc_register_host_class();
    if (ret)
        goto unregister_bus;

    ret = sdio_register_bus();
    if (ret)
        goto unregister_host_class;

    return 0;

unregister_host_class:
    mmc_unregister_host_class();
unregister_bus:
    mmc_unregister_bus();
    return ret;
}

mmc_init関数では、MMC共通の次のような初期化処理を実施する。

  1. mmcバス (mmc) の追加
  2. mmcクラス (mmc_host) の追加
  3. mmcバス (sdio) の追加

mmcバスの追加

mmcバスは mmc_register_bus関数によって追加される。
mmc_register_bus関数と、その引数で必要となる mmc_bus_typeの定義は次の通りとなっている。

// 226:
static struct bus_type mmc_bus_type = {
    .name       = "mmc",
    .dev_groups = mmc_dev_groups,
    .match      = mmc_bus_match,
    .uevent     = mmc_bus_uevent,
    .probe      = mmc_bus_probe,
    .remove     = mmc_bus_remove,
    .shutdown   = mmc_bus_shutdown,
    .pm     = &mmc_bus_pm_ops,
};

int mmc_register_bus(void)
{
    return bus_register(&mmc_bus_type);
}

Linuxでは、デバイスモデルの基本的なデータ構造として kobject(kset) 構造体で表現されている。
これらは オブジェクトの参照カウントやリンク参照による階層構造といった機能を持っている。

ksetの簡易構造

バイスモデルの各オブジェクトは kobject を Wrap した形式で表現される。

bus_register関数は、busサブシステムに対して、引数で渡した bus_type構造体のオブジェクトを子オブジェクトとして追加する関数である。
詳細は省くが、mmc_register_bus関数を実行したことで、既存の busオブジェクトに mmcの subsys_private(kobjectを内包する)オブジェクトが追加される。

mmc_register_bus関数を実行した結果

mmc_hostクラスの追加

mmc_register_host_class関数と、その引数で必要となる mmc_host_classの定義は次の通りとなっている。

// 83:
static struct class mmc_host_class = {
    .name       = "mmc_host",
    .dev_release    = mmc_host_classdev_release,
    .pm     = MMC_HOST_CLASS_DEV_PM_OPS,
};

int mmc_register_host_class(void)
{
    return class_register(&mmc_host_class);
}

mmc_register_host_class関数は、classサブシステムに対して、引数で渡したclass構造体のオブジェクトを子オブジェクトとして追加する関数である。

mmc_register_host_class関数を実行した結果

sdioバスの追加

sdio_register_bus関数と、その引数で必要となる sdio_bus_typeの定義は次の通りとなっている。

// 246:
static struct bus_type sdio_bus_type = {
    .name       = "sdio",
    .dev_groups = sdio_dev_groups,
    .match      = sdio_bus_match,
    .uevent     = sdio_bus_uevent,
    .probe      = sdio_bus_probe,
    .remove     = sdio_bus_remove,
    .pm     = &sdio_bus_pm_ops,
};

int sdio_register_bus(void)
{
    return bus_register(&sdio_bus_type);
}

SDIOは、本記事の対象外であるため割愛する。

MMCブロックの初期化

次に、mmc_blk_init関数から確認する。

// 3019:
static int __init mmc_blk_init(void)
{
    int res;

    res  = bus_register(&mmc_rpmb_bus_type);
    if (res < 0) {
        pr_err("mmcblk: could not register RPMB bus type\n");
        return res;
    }
    res = alloc_chrdev_region(&mmc_rpmb_devt, 0, MAX_DEVICES, "rpmb");
    if (res < 0) {
        pr_err("mmcblk: failed to allocate rpmb chrdev region\n");
        goto out_bus_unreg;
    }

    if (perdev_minors != CONFIG_MMC_BLOCK_MINORS)
        pr_info("mmcblk: using %d minors per device\n", perdev_minors);

    max_devices = min(MAX_DEVICES, (1 << MINORBITS) / perdev_minors);

    res = register_blkdev(MMC_BLOCK_MAJOR, "mmc");
    if (res)
        goto out_chrdev_unreg;

    res = mmc_register_driver(&mmc_driver);
    if (res)
        goto out_blkdev_unreg;

    return 0;

out_blkdev_unreg:
    unregister_blkdev(MMC_BLOCK_MAJOR, "mmc");
out_chrdev_unreg:
    unregister_chrdev_region(mmc_rpmb_devt, MAX_DEVICES);
out_bus_unreg:
    bus_unregister(&mmc_rpmb_bus_type);
    return res;
}

mmc_init関数では、MMC共通の次のような初期化処理を実施する。

  1. mmcバス (mmc_rpmb) と rpmb(Replay Protected Memory Block)の追加
  2. ブロックデバイス (mmc) の追加
  3. mmcドライバ (mmcblk) の追加

mmc_rpmbバスの追加

Replay Protected Memory Block (RPMB) は、共通鍵を利用して、HostとDeviceの間のデータを検証するセキュリティに関係するプロトコルである。
MMC v4.4 specification によって、MMC が RPMB のサポートが仕様化され、近年の e·MMCデバイスでは デフォルトで RPMBパーティションとして認識されるだろう。

ただし、ファイルアクセスの本質とは離れるため RPMB の関連の確認は割愛する。

mmcブロックデバイスの追加

register_blkdevマクロによってブロックデバイス mmc を登録する。

// 286:
#define register_blkdev(major, name) \
   __register_blkdev(major, name, NULL)

register_blkdevマクロは __register_blkdev関数 のWarpperとなっている。
__register_blkdev関数の定義は次の通りとなっている。

// 231:
int __register_blkdev(unsigned int major, const char *name,
        void (*probe)(dev_t devt))
{
    struct blk_major_name **n, *p;
    int index, ret = 0;

    mutex_lock(&major_names_lock);

    /* temporary */
    if (major == 0) {
        for (index = ARRAY_SIZE(major_names)-1; index > 0; index--) {
            if (major_names[index] == NULL)
                break;
        }

        if (index == 0) {
            printk("%s: failed to get major for %s\n",
                   __func__, name);
            ret = -EBUSY;
            goto out;
        }
        major = index;
        ret = major;
    }

    if (major >= BLKDEV_MAJOR_MAX) {
        pr_err("%s: major requested (%u) is greater than the maximum (%u) for %s\n",
               __func__, major, BLKDEV_MAJOR_MAX-1, name);

        ret = -EINVAL;
        goto out;
    }

    p = kmalloc(sizeof(struct blk_major_name), GFP_KERNEL);
    if (p == NULL) {
        ret = -ENOMEM;
        goto out;
    }

    p->major = major;
    p->probe = probe;
    strlcpy(p->name, name, sizeof(p->name));
    p->next = NULL;
    index = major_to_index(major);

    spin_lock(&major_names_spinlock);
    for (n = &major_names[index]; *n; n = &(*n)->next) {
        if ((*n)->major == major)
            break;
    }
    if (!*n)
        *n = p;
    else
        ret = -EBUSY;
    spin_unlock(&major_names_spinlock);

    if (ret < 0) {
        printk("register_blkdev: cannot get major %u for %s\n",
               major, name);
        kfree(p);
    }
out:
    mutex_unlock(&major_names_lock);
    return ret;
}

__register_blkdev関数では、グローバル変数の配列 major_namesmajor番目にnameを登録することができる。

mmcblkドライバの追加

mmc_register_driver関数と、その引数で必要となる mmc_driverの定義は次の通りとなっている。

// 3009:
static struct mmc_driver mmc_driver = {
    .drv        = {
        .name   = "mmcblk",
        .pm = &mmc_blk_pm_ops,
    },
    .probe      = mmc_blk_probe,
    .remove     = mmc_blk_remove,
    .shutdown   = mmc_blk_shutdown,
};
// 251:
int mmc_register_driver(struct mmc_driver *drv)
{
    drv->drv.bus = &mmc_bus_type;
    return driver_register(&drv->drv);
}

mmc_register_driver関数は、driversサブシステムに対して、引数で渡した mmc_driver構造体のオブジェクトを子オブジェクトとして追加する関数である。

mmc_register_driver関数を実行した結果

MMCホストの初期化

Arm Versatile Express boards (vexpress-a9) では、MMCホストのドライバとして mmci-pl18x を使うことになる。

mmci-pl18x ドライバでは、Advanced Microcontroller Bus Architecture (AMBA) と呼ばれる規格に則った実装となっている。 amba_driver構造体で定義されたデータ構造を module_amba_driverマクロを呼び出すことで、ambaバスにドライバを登録/解除することができる。

// 2445:
static struct amba_driver mmci_driver = {
    .drv        = {
        .name   = DRIVER_NAME,
        .pm = &mmci_dev_pm_ops,
    },
    .probe      = mmci_probe,
    .remove     = mmci_remove,
    .id_table   = mmci_ids,
};

module_amba_driver(mmci_driver);
// 217:
#define module_amba_driver(__amba_drv) \
   module_driver(__amba_drv, amba_driver_register, amba_driver_unregister)

module_amba_driverマクロは、module_driverマクロのwrapperとなっている。
module_driverマクロでは、amba_driver_register関数とamba_driver_unregister関数を追加する。

// 258:
#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \
{ \
   return __register(&(__driver) , ##__VA_ARGS__); \
} \
module_init(__driver##_init); \
static void __exit __driver##_exit(void) \
{ \
   __unregister(&(__driver) , ##__VA_ARGS__); \
} \
module_exit(__driver##_exit);
// 341:
int amba_driver_register(struct amba_driver *drv)
{
    if (!drv->probe)
        return -EINVAL;

    drv->drv.bus = &amba_bustype;

    return driver_register(&drv->drv);
}
// 359:
void amba_driver_unregister(struct amba_driver *drv)
{
    driver_unregister(&drv->drv);
}

amba_driver_register関数は、driversサブシステムに対して、引数で渡した amba_driver構造体のオブジェクトを子オブジェクトとして追加する関数である。

amba_driver_register関数を実行した結果

おわりに

本記事では、カーネル起動時に呼び出される mmc_init関数と mmc_blk_init関数について確認した。
これらの初期化によって、sysfs は次のようなエントリが追加される。(mmc_rpmbバスは省略)

mmc関連ドライバの初期化処理によるsysfsディレクトリ構造

変更履歴

  • 2023/12/10: 記事公開
  • 2023/12/23: mmcホストの初期化を追加

参考