関連記事
- 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
概要
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
フラグを付けて、起動を遅延させることがある。
今回の 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); }
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 の仕組みを使って休止状態で待ち続ける関数となっている。
また、占有されたホストコントローラは、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_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 の一部にコールバック処理を挟む仕組みの一つである。
ホストコントローラによっては、電源投入時のシーケンス動作に特別な処理が必要なモジュール (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: 記事公開