LeavaTailの日記

LeavaTailの日記

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

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ホストの初期化を追加

参考