関連記事
- Part 1: 環境セットアップ
- Part 2: System call Interface
- Part 3: VFS
- Part 4: ext2 (1) write_iter
- Part 5: ext2 (2) write_begin
- Part 6: ext2 (3) get_block
- Part 7: ext2 (4) write_end
- Part 8: writeback (1) work Queue
- Part 9: writeback (2) wb_writeback
- Part 10: writeback (3) writepages
- Part 11: writeback (4) write_inode
- Part 12: block (1) submit_bio
- Part 13: block (2) blk_mq
- Part 14: I/O scheduler (1) mq-deadline
- Part 15: I/O scheduler (2) insert_request
- Part 16: I/O scheduler (3) dispatch_request
- Part 17: block (3) blk_mq_run_work_fn
- Part 18: block (4) block: blk_mq_do_dispatch_sched
- Part 19: MMC (1) initialization
- Part 20: PL181 (1) mmci_probe
- Part 21: MMC (2) mmc_start_host
- Part 22: MMC (3) mmc_rescan
- Part 23: MMC (4) mmc_attach_sd
- Part 24: MMC (5) mmc_blk_probe
概要
QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。
本章では、MMCブロックシステムの初期化処理について確認した。
はじめに
ユーザプロセスはファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。
調査対象や環境などはPart 1: 環境セットアップを参照。
MMCブロックの Probe
カーネルがMMCデバイスを認識したとき、mmc_probe関数が呼ばれる。
このとき、ブロックデバイスとして扱う必要がある場合には、MMCブロックのprobe処理も必要になる。
MMCメモリカード追加によって呼び出されるdevice_add関数では、関連コンポーネントの probeを実施する。
MMCバスのProbe処理によって、MMCブロックのProbe mmc_blk_probe関数を実施する。
mmc_blk_probe関数の定義は次のようになっている。
// 2885: static int mmc_blk_probe(struct mmc_card *card) { struct mmc_blk_data *md; int ret = 0; /* * Check that the card supports the command class(es) we need. */ if (!(card->csd.cmdclass & CCC_BLOCK_READ)) return -ENODEV; mmc_fixup_device(card, mmc_blk_fixups); card->complete_wq = alloc_workqueue("mmc_complete", WQ_MEM_RECLAIM | WQ_HIGHPRI, 0); if (!card->complete_wq) { pr_err("Failed to create mmc completion workqueue"); return -ENOMEM; } md = mmc_blk_alloc(card); if (IS_ERR(md)) { ret = PTR_ERR(md); goto out_free; } ret = mmc_blk_alloc_parts(card, md); if (ret) goto out; /* Add two debugfs entries */ mmc_blk_add_debugfs(card, md); pm_runtime_set_autosuspend_delay(&card->dev, 3000); pm_runtime_use_autosuspend(&card->dev); /* * Don't enable runtime PM for SD-combo cards here. Leave that * decision to be taken during the SDIO init sequence instead. */ if (card->type != MMC_TYPE_SD_COMBO) { pm_runtime_set_active(&card->dev); pm_runtime_enable(&card->dev); } return 0; out: mmc_blk_remove_parts(card, md); mmc_blk_remove_req(md); out_free: destroy_workqueue(card->complete_wq); return ret; }
mmc_blk_probe関数では、MMCメモリカードをブロックデバイスとしてアクセスできるようにセットアップする関数である。
MMCメモリカードのプロパティや状態を管理している cardを引数として受け取り、この関数が正常に完了(return 0)すると、LinuxカーネルがMMCデバイスをブロックデバイスとして扱えるようになる。
この関数では、以下の処理を実施する。
それぞれの処理について、順番に確認していく。
特定のデバイスに対する調整
MMCメモリカードやSDメモリカードなどのメディアでは、メーカーごとにハードウェア固有の調整や設定が追加で必要となることもある。
mmc_fixup_device関数では、事前に登録された fixupリストを参照して、該当デバイスが追加の修正が必要かどうかを確認し、必要に応じて適用する。
// 148: static inline void mmc_fixup_device(struct mmc_card *card, const struct mmc_fixup *table) { const struct mmc_fixup *f; u64 rev = cid_rev_card(card); for (f = table; f->vendor_fixup; f++) { if ((f->manfid == CID_MANFID_ANY || f->manfid == card->cid.manfid) && (f->oemid == CID_OEMID_ANY || f->oemid == card->cid.oemid) && (f->name == CID_NAME_ANY || !strncmp(f->name, card->cid.prod_name, sizeof(card->cid.prod_name))) && (f->cis_vendor == card->cis.vendor || f->cis_vendor == (u16) SDIO_ANY_ID) && (f->cis_device == card->cis.device || f->cis_device == (u16) SDIO_ANY_ID) && (f->ext_csd_rev == EXT_CSD_REV_ANY || f->ext_csd_rev == card->ext_csd.rev) && rev >= f->rev_start && rev <= f->rev_end) { dev_dbg(&card->dev, "calling %ps\n", f->vendor_fixup); f->vendor_fixup(card, f->data); } } }
MMCメモリカード系の fixupリストは drivers/mmc/core/quirks.h で定義されている。
今回の環境は fixup の必要がないため、詳細は割愛する。
完了通知の Work Queue
MMCサブシステムでは、ホストコントローラがMMCコマンドなどが完了した後に、上位層に通知をするために Work Queue mmc_complete が用意されている。
この Work Queue mmc_complete は、上位層のページキャッシュを回収することがあるために、 WQ_MEM_RECLAIM | WQ_HIGHPRIフラグを付与しておく。
MMCブロックデバイスの初期化
Linuxカーネルでは、MMC デバイスに対応するブロックデバイス構造体(mmc_blk_data)が用意されており、mmc_blk_alloc関数で初期化と設定をする。
// 2455: static struct mmc_blk_data *mmc_blk_alloc(struct mmc_card *card) { sector_t size; if (!mmc_card_sd(card) && mmc_card_blockaddr(card)) { /* * The EXT_CSD sector count is in number or 512 byte * sectors. */ size = card->ext_csd.sectors; } else { /* * The CSD capacity field is in units of read_blkbits. * set_capacity takes units of 512 bytes. */ size = (typeof(sector_t))card->csd.capacity << (card->csd.read_blkbits - 9); } return mmc_blk_alloc_req(card, &card->dev, size, false, NULL, MMC_BLK_DATA_AREA_MAIN, 0); }
mmc_blk_alloc関数は、容量(セクタ数) を計算するだけとなっており、その結果を mmc_blk_alloc_req関数となっている。
SDメモリカードでは利用されないが、e.MMC は Extended Card Specific Data (EXT_CSD) レジスタに、カード固有のパラメータ(容量や書き込みモード、セキュリティ設定など)に関する情報が格納差されている。そのため、EXT_CSD レジスタから取得した値を size に設定する。
一方で、SDメモリカードでは Card Specific Data (CSD) レジスタに、カードの基本情報や機能情報が格納されている。
そのため、 CSDレジスタから取得した値をread_blkbitsから 512バイト単位で size に設定する。
こうして得られた size と他パラメータを使って、mmc_blk_alloc_req関数を呼び出す。
// 2336: static struct mmc_blk_data *mmc_blk_alloc_req(struct mmc_card *card, struct device *parent, sector_t size, bool default_ro, const char *subname, int area_type, unsigned int part_type) { struct mmc_blk_data *md; int devidx, ret; char cap_str[10]; devidx = ida_simple_get(&mmc_blk_ida, 0, max_devices, GFP_KERNEL); if (devidx < 0) { /* * We get -ENOSPC because there are no more any available * devidx. The reason may be that, either userspace haven't yet * unmounted the partitions, which postpones mmc_blk_release() * from being called, or the device has more partitions than * what we support. */ if (devidx == -ENOSPC) dev_err(mmc_dev(card->host), "no more device IDs available\n"); return ERR_PTR(devidx); } md = kzalloc(sizeof(struct mmc_blk_data), GFP_KERNEL); if (!md) { ret = -ENOMEM; goto out; } md->area_type = area_type; /* * Set the read-only status based on the supported commands * and the write protect switch. */ md->read_only = mmc_blk_readonly(card); md->disk = mmc_init_queue(&md->queue, card); if (IS_ERR(md->disk)) { ret = PTR_ERR(md->disk); goto err_kfree; } INIT_LIST_HEAD(&md->part); INIT_LIST_HEAD(&md->rpmbs); kref_init(&md->kref); md->queue.blkdata = md; md->part_type = part_type; md->disk->major = MMC_BLOCK_MAJOR; md->disk->minors = perdev_minors; md->disk->first_minor = devidx * perdev_minors; md->disk->fops = &mmc_bdops; md->disk->private_data = md; md->parent = parent; set_disk_ro(md->disk, md->read_only || default_ro); md->disk->flags = GENHD_FL_EXT_DEVT; if (area_type & (MMC_BLK_DATA_AREA_RPMB | MMC_BLK_DATA_AREA_BOOT)) md->disk->flags |= GENHD_FL_NO_PART_SCAN | GENHD_FL_SUPPRESS_PARTITION_INFO; /* * As discussed on lkml, GENHD_FL_REMOVABLE should: * * - be set for removable media with permanent block devices * - be unset for removable block devices with permanent media * * Since MMC block devices clearly fall under the second * case, we do not set GENHD_FL_REMOVABLE. Userspace * should use the block device creation/destruction hotplug * messages to tell when the card is present. */ snprintf(md->disk->disk_name, sizeof(md->disk->disk_name), "mmcblk%u%s", card->host->index, subname ? subname : ""); set_capacity(md->disk, size); if (mmc_host_cmd23(card->host)) { if ((mmc_card_mmc(card) && card->csd.mmca_vsn >= CSD_SPEC_VER_3) || (mmc_card_sd(card) && card->scr.cmds & SD_SCR_CMD23_SUPPORT)) md->flags |= MMC_BLK_CMD23; } if (mmc_card_mmc(card) && md->flags & MMC_BLK_CMD23 && ((card->ext_csd.rel_param & EXT_CSD_WR_REL_PARAM_EN) || card->ext_csd.rel_sectors)) { md->flags |= MMC_BLK_REL_WR; blk_queue_write_cache(md->queue.queue, true, true); } string_get_size((u64)size, 512, STRING_UNITS_2, cap_str, sizeof(cap_str)); pr_info("%s: %s %s %s %s\n", md->disk->disk_name, mmc_card_id(card), mmc_card_name(card), cap_str, md->read_only ? "(ro)" : ""); /* used in ->open, must be set before add_disk: */ if (area_type == MMC_BLK_DATA_AREA_MAIN) dev_set_drvdata(&card->dev, md); device_add_disk(md->parent, md->disk, mmc_disk_attr_groups); return md; err_kfree: kfree(md); out: ida_simple_remove(&mmc_blk_ida, devidx); return ERR_PTR(ret); }
mmc_blk_alloc_req関数では、MMCブロックデバイスに対するリクエストで必要となるデータ構造(mmc_blk_data)のリソースを割り当て、初期化するための関数となっている。
この関数では、MMCブロックデバイス構造体の様々なメンバーが初期化されるが、分類で分けて考えていく。
- IDA (ID Allocator) による ID取得
- kzalloc関数による構造体の割り当て
- mmc queueの初期化と紐づけ ((
mmc_init_queue関数は次回以降で確認する)) - デバイスの登録:
- その他のパラメータ設定
Linuxでは、ID Allocator (IDA) と呼ばれる仕組みによって識別子を割り当てができる。
MMCブロックデバイスの識別子に IDR (mmc_blk_ida) を使用する。
これによって、最初に識別されたMMCブロックデバイスは0、次に識別されたMMCブロックデバイスは1となる。
mmc_blk_data型のmdの領域(376バイト)は kzalloc関数で確保する。
このデータmdは、mmc_blk_alloc_req関数とそこで呼び出される mmc_init_queue関数によって初期化される。

MMCメモリカードやSDメモリカードでは、ブロック単位でデータを管理する。 この時、ブロックごとに書き込みする方式 (CMD17, CMD24) 以外にも、複数のブロックを書き込みする方式 (CMD18, 25) が定義されている。
SDメモリカードでは、UHS104 や SDXC 向けのコマンドが用意されており、CMD23はこれに該当する。 どのコマンドがサポートされているかどうかは、SCRのフィールドにで管理されている。

また、MMCメモリカードでは Reliable Write と呼ばれるデータの信頼性を保証するためにMMCが提供する書き込み操作の一種である。 Reliable Write は、CMD23の引数内で特定のビットを設定することで有効化することができる。
そこで、カードが Reliable Writeをサポートしているかどうか(EXT_CSD_WR_REL_PARAM) とReliable Writeの書き込み単位 (MMC_BLK_REL_WR) を確認する。
Reliable Writeがサポートされている場合には、blk_queue_write_cache 関数でリクエストキューに対して書き込みキャッシュ(wc)とFUAfuaを有効化する。

パラメータを初期化した後に、ブロックデバイスをシステムに登録するためにdevice_add_disk関数を呼び出す。
この関数は、ブロックデバイスをカーネルのデバイスモデルに追加し、ユーザ空間からそのディスクをアクセスできるようにすることを目的とする。 これ以外にも、次のような役割もあるがここでは割愛する。
- ディスクキューの初期化
- 関連するイベントの通知
- デバイスノードの作成
MMC物理パーティション
mmc_blk_alloc_parts 関数は、MMCデバイスのパーティションの初期化と管理する。
// 2654: static int mmc_blk_alloc_parts(struct mmc_card *card, struct mmc_blk_data *md) { int idx, ret; if (!mmc_card_mmc(card)) return 0; for (idx = 0; idx < card->nr_parts; idx++) { if (card->part[idx].area_type & MMC_BLK_DATA_AREA_RPMB) { /* * RPMB partitions does not provide block access, they * are only accessed using ioctl():s. Thus create * special RPMB block devices that do not have a * backing block queue for these. */ ret = mmc_blk_alloc_rpmb_part(card, md, card->part[idx].part_cfg, card->part[idx].size >> 9, card->part[idx].name); if (ret) return ret; } else if (card->part[idx].size) { ret = mmc_blk_alloc_part(card, md, card->part[idx].part_cfg, card->part[idx].size >> 9, card->part[idx].force_ro, card->part[idx].name, card->part[idx].area_type); if (ret) return ret; } } return 0; }
e·MMCでは、パーティションの構成が仕様として規定されている。

2つのブートパーティションと1つのReplay Protected Memory Block(RPMB)パーティション、1つのユーザエリアパーティションから構成される。
Linuxカーネルからは RPMBパーティションとユーザエリアパーティション を取り扱う。
mmc_blk_alloc_parts関数では、これがパーティション内にあるかどうかを確認し、必要に応じて初期化・設定する。
debugfsにエントリの追加
mmc_blk_add_debugfs 関数は、メモリカードのデバッグ情報を debugfs として提供する。
// 2825: static int mmc_blk_add_debugfs(struct mmc_card *card, struct mmc_blk_data *md) { struct dentry *root; if (!card->debugfs_root) return 0; root = card->debugfs_root; if (mmc_card_mmc(card) || mmc_card_sd(card)) { md->status_dentry = debugfs_create_file_unsafe("status", 0400, root, card, &mmc_dbg_card_status_fops); if (!md->status_dentry) return -EIO; } if (mmc_card_mmc(card)) { md->ext_csd_dentry = debugfs_create_file("ext_csd", S_IRUSR, root, card, &mmc_dbg_ext_csd_fops); if (!md->ext_csd_dentry) return -EIO; } return 0; }
mmc_blk_add_debugfs関数で登録するエントリは次の2つである。
デバイスの runtime PM を有効化
Linux では、Runtime Power Management (Runtime PM) のためのフレームワークが用意されており、必要に応じて消費電力を削減することに貢献する。
MMCブロックでも Runtime Power Management のサポートされており、probe時に設定する。
pm_runtime_use_autosuspend関数は、デバイスで自動サスペンド機能を有効にする。
自動サスペンドまでの待機時間は、pm_runtime_set_autosuspend_delay関数で3秒に設定されている。
ただし、これらは SDIO のような コンボカードでは適用されない。
runtime PM を有効にするためには、pm_runtime_set_active関数で"active"ステータスに設定したうえで、pm_runtime_enable関数で有効化する。
おわりに
本記事では、MMCブロックのprobeで呼び出される mmc_blk_probe関数について確認した。
この関数では、e·MMCやSDメモリカードなどブロックデバイスとしての初期化ロジックが組み込まれている。
変更履歴
- 2024/11/30: 記事公開
参考
- SD and MMC Device Partitions — The Linux Kernel documentation
- SD や MMC のパーティションについて
- Runtime Power Management Framework for I/O Devices — The Linux Kernel documentation
- LinuxにおけるRuntime PMの仕組みについて
- Device Power Management Basics — The Linux Kernel documentation
- デバイスの電源管理について
- 【FileSystem】車載外部ストレージ その11【SDカード⑦】 | シミュレーションの世界に引きこもる部屋
- SDメモリカードにおけるMultiple blockについて