関連記事
- 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
概要
QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。
本章では、MMCサブシステムの初期化処理について確認した。
はじめに
ユーザプロセスはファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。
調査対象や環境などはPart 1: 環境セットアップを参照。
MMCサブシステム
マルチメディアカード (MMC) は 1997年に発表されたメモリーカードの規格の一つである。
SDメモリカード (SDC) は、MMC と互換性のある規格で、デジタルカメラや携帯電話など現在でも幅広く利用されている。
Linux では、e.MMC や SDカード といったストレージメディアをMMCサブシステムで扱う。
- MMC block: 上位の汎用ブロックデバイスからのI/O要求を受け取り、データを管理する
- MMC core: 対象となるストレージメディアが SDC や MMC かといった規格を判別し、規格固有の制御(スピードモードの設定など)する
- MMC host:: ホストコントローラのレジスタを操作、機種依存の差をここで実装する
Linuxデバイスモデル
Linuxでは、システムの接続状態を管理にできるように共通フレームワーク (Linuxデバイスモデル) が提供されている。
Linuxデバイスモデルは、バス、デバイス、ドライバの3つで構成される。
また、これらに加えて、クラスと呼ばれる概念も存在する。 クラスでは、デバイスを上位レベルでとらえたもので、下位レベルを抽象化する。
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_initcall
の mmc_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共通の次のような初期化処理を実施する。
- mmcバス (
mmc
) の追加 - mmcクラス (
mmc_host
) の追加 - 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) 構造体で表現されている。
これらは オブジェクトの参照カウントやリンク参照による階層構造といった機能を持っている。
デバイスモデルの各オブジェクトは kobject を Wrap した形式で表現される。
bus_register
関数は、bus
サブシステムに対して、引数で渡した bus_type
構造体のオブジェクトを子オブジェクトとして追加する関数である。
詳細は省くが、mmc_register_bus
関数を実行したことで、既存の bus
オブジェクトに mmcの subsys_private
(kobjectを内包する)オブジェクトが追加される。
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
構造体のオブジェクトを子オブジェクトとして追加する関数である。
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共通の次のような初期化処理を実施する。
- mmcバス (
mmc_rpmb
) とrpmb
(Replay Protected Memory Block)の追加 - ブロックデバイス (
mmc
) の追加 - 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_names
の major
番目に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ホストの初期化
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
構造体のオブジェクトを子オブジェクトとして追加する関数である。
おわりに
本記事では、カーネル起動時に呼び出される mmc_init
関数と mmc_blk_init
関数について確認した。
これらの初期化によって、sysfs は次のようなエントリが追加される。(mmc_rpmbバスは省略)
変更履歴
- 2023/12/10: 記事公開
- 2023/12/23: mmcホストの初期化を追加
参考
- Linuxのmmcドライバ概要 #Linux - Qiita
- Linux Kernel MMC Storage driver Overview | PPT
- MMC 概要
- mmc子系统分析(一)_mmc命令响应 r1-CSDN博客
- mmc subsystem 概要
- Linux内核4.14版本——mmc core(2)——bus模块_intext:sys/bus/mmc/devices/-CSDN博客
- Linux MMC framework(1)_软件架构
- mmc subsystem概要
- MMC/SDCの使いかた
- MMC/SDC概要
- https://elinux.org/images/9/91/Clement-sd-mmc-high-speed-support-in-linux-kernel_0.pdf
- 2017年における MMC/SDCの high speedサポートの紹介記事
- Linux 2.6 Device Model
- The Linux Kernel Device Model — The Linux Kernel documentation
- Linux Device Model — The Linux Kernel documentation
- Linuxのドライバの初期化が呼ばれる流れ #Linux - Qiita
- ドライバのが呼ばれる順番
- JVNVU#97690270 (RPMB脆弱性)を簡単に確認してみた #Linux - Qiita
- RPMB脆弱性の簡易解説