From cf9c093d014cf47d18277cc0a640aac83adff884 Mon Sep 17 00:00:00 2001 From: Wang Yue Date: Tue, 16 Jun 2026 23:26:31 +0800 Subject: [PATCH] Support virtio-mmio devices from kernel command line --- Cargo.lock | 1 + .../linux-compatibility/kernel-parameters.md | 22 +++ kernel/comps/cmdline/src/lib.rs | 2 +- kernel/comps/cmdline/src/parse.rs | 9 +- kernel/comps/cmdline/src/types.rs | 139 +++++++++++++++++- kernel/comps/virtio/Cargo.toml | 1 + .../virtio/src/transport/mmio/bus/arch/x86.rs | 62 +++++++- .../virtio/src/transport/mmio/bus/mod.rs | 2 +- 8 files changed, 222 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 731263d3f..039f63abb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -408,6 +408,7 @@ version = "0.1.0" dependencies = [ "aster-bigtcp", "aster-block", + "aster-cmdline", "aster-console", "aster-fuse", "aster-input", diff --git a/book/src/kernel/linux-compatibility/kernel-parameters.md b/book/src/kernel/linux-compatibility/kernel-parameters.md index f2b6c7b43..9aaf11e4c 100644 --- a/book/src/kernel/linux-compatibility/kernel-parameters.md +++ b/book/src/kernel/linux-compatibility/kernel-parameters.md @@ -35,6 +35,28 @@ console=ttyS0 console=ttyS0 console=hvc0 ``` +### `virtio_mmio.device` + +Register a VirtIO-MMIO device from the kernel command line. +This parameter may be specified multiple times. + +Format: +```text +virtio_mmio.device=@:[:] +``` + +Notes: +- `size` and `base` may be decimal or hexadecimal with a `0x` prefix. +- `size` may use `K`, `M`, `G`, or `T` suffixes. +- `irq` must be nonzero. +- The optional `id` field is accepted for Linux compatibility but ignored. + +Examples: +```text +virtio_mmio.device=0x200@0x5950f000:10 +virtio_mmio.device=1K@0x1001e000:74 +``` + ## Asterinas-specific ### `ostd.log_level` diff --git a/kernel/comps/cmdline/src/lib.rs b/kernel/comps/cmdline/src/lib.rs index 73aa04ac9..d2199b6d4 100644 --- a/kernel/comps/cmdline/src/lib.rs +++ b/kernel/comps/cmdline/src/lib.rs @@ -127,7 +127,7 @@ macro_rules! define_kv_param_early { /// /// The stored value type `S::Value` must implement /// [`crate::parse::ParseRepeatableParamValue`]. This crate provides a default -/// implementation for `Vec` where `T: FromStr`. +/// implementation for `Vec` where `T: ParseParamValue`. /// /// # Examples /// diff --git a/kernel/comps/cmdline/src/parse.rs b/kernel/comps/cmdline/src/parse.rs index f41ab6c28..9e06103e3 100644 --- a/kernel/comps/cmdline/src/parse.rs +++ b/kernel/comps/cmdline/src/parse.rs @@ -86,13 +86,10 @@ impl ParseParamValue for T { } } -/// A `Vec` where `T: FromStr` can be a repeatable parameter. -impl ParseRepeatableParamValue for Vec { +/// A `Vec` where `T: ParseParamValue` can be a repeatable parameter. +impl ParseRepeatableParamValue for Vec { fn parse_all(values: &[&str]) -> Result { - values - .iter() - .map(|v| v.parse().map_err(|_| ParamError::InvalidValue)) - .collect() + values.iter().map(|value| T::parse_param(value)).collect() } } diff --git a/kernel/comps/cmdline/src/types.rs b/kernel/comps/cmdline/src/types.rs index ea3a4cc4c..3003b26ca 100644 --- a/kernel/comps/cmdline/src/types.rs +++ b/kernel/comps/cmdline/src/types.rs @@ -6,7 +6,7 @@ //! command lines so users of this framework don't need to rewrite them. use alloc::vec::Vec; -use core::num::NonZeroU32; +use core::num::{NonZeroU32, NonZeroUsize}; use crate::parse::{ParamError, ParseParamValue}; @@ -208,6 +208,113 @@ fn parse_u32(s: &str) -> Result { s.parse::().map_err(|_| ParamError::InvalidValue) } +/// Linux-style MMIO device descriptor. +/// +/// This type parses values in the form `@:[:]`. +/// `size` supports binary suffixes (`K`, `M`, `G`, and `T`), while `base`, +/// `irq`, and `id` may be decimal or hexadecimal with a `0x` prefix. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MmioDevice { + base: usize, + size: NonZeroUsize, + irq: NonZeroU32, + id: Option, +} + +impl MmioDevice { + /// Returns the base address of the MMIO region. + pub fn base(&self) -> usize { + self.base + } + + /// Returns the size of the MMIO region. + pub fn size(&self) -> NonZeroUsize { + self.size + } + + /// Returns the interrupt line described by the command-line value. + pub fn irq(&self) -> NonZeroU32 { + self.irq + } + + /// Returns the optional device ID. + pub fn id(&self) -> Option { + self.id + } +} + +impl ParseParamValue for MmioDevice { + fn parse_param(value: &str) -> Result { + parse_mmio_device(value).ok_or(ParamError::InvalidValue) + } +} + +fn parse_mmio_device(value: &str) -> Option { + let (size, rest) = value.split_once('@')?; + let mut rest_segments = rest.split(':'); + + let base = parse_usize_with_hex_prefix(rest_segments.next()?)?; + let irq = NonZeroU32::new(parse_u32_with_hex_prefix(rest_segments.next()?)?)?; + let id = match rest_segments.next() { + Some(device_id) => Some(parse_u32_with_hex_prefix(device_id)?), + None => None, + }; + if rest_segments.next().is_some() { + return None; + } + + let size = NonZeroUsize::new(parse_size(size)?)?; + + Some(MmioDevice { + base, + size, + irq, + id, + }) +} + +fn parse_size(value: &str) -> Option { + let (number, shift) = match value.as_bytes().last()? { + b'k' | b'K' => (&value[..value.len() - 1], 10), + b'm' | b'M' => (&value[..value.len() - 1], 20), + b'g' | b'G' => (&value[..value.len() - 1], 30), + b't' | b'T' => (&value[..value.len() - 1], 40), + _ => (value, 0), + }; + + parse_usize_with_hex_prefix(number)?.checked_mul(1usize.checked_shl(shift)?) +} + +fn parse_usize_with_hex_prefix(value: &str) -> Option { + if value.is_empty() { + return None; + } + + if let Some(value) = value + .strip_prefix("0x") + .or_else(|| value.strip_prefix("0X")) + { + usize::from_str_radix(value, 16).ok() + } else { + value.parse().ok() + } +} + +fn parse_u32_with_hex_prefix(value: &str) -> Option { + if value.is_empty() { + return None; + } + + if let Some(value) = value + .strip_prefix("0x") + .or_else(|| value.strip_prefix("0X")) + { + u32::from_str_radix(value, 16).ok() + } else { + value.parse().ok() + } +} + /// Linux-style metric-suffixed u64 value. /// /// Supports binary multiples (KiB-style): @@ -259,6 +366,36 @@ mod test { use super::*; + #[ktest] + fn mmio_device_parse_ok() { + let dev = MmioDevice::parse_param("0x200@0x5950f000:10").unwrap(); + assert_eq!(dev.base(), 0x5950_f000); + assert_eq!(dev.size().get(), 0x200); + assert_eq!(dev.irq().get(), 10); + assert_eq!(dev.id(), None); + + let dev = MmioDevice::parse_param("1K@0x1001e000:74:2").unwrap(); + assert_eq!(dev.base(), 0x1001_e000); + assert_eq!(dev.size().get(), 1024); + assert_eq!(dev.irq().get(), 74); + assert_eq!(dev.id(), Some(2)); + } + + #[ktest] + fn mmio_device_parse_err() { + for value in [ + "", + "0x200@0x1000", + "0x200@0x1000:0", + "0@0x1000:1", + "1K@0x1000:1:2:3", + "1Z@0x1000:1", + "virtio_mmio.device=0x200@0x1000:1", + ] { + assert!(MmioDevice::parse_param(value).is_err()); + } + } + #[ktest] fn metric_u64_parse_ok() { assert_eq!(MetricU64::parse_param("0").unwrap(), MetricU64(0)); diff --git a/kernel/comps/virtio/Cargo.toml b/kernel/comps/virtio/Cargo.toml index e34befeb1..d61b4fc04 100644 --- a/kernel/comps/virtio/Cargo.toml +++ b/kernel/comps/virtio/Cargo.toml @@ -8,6 +8,7 @@ edition.workspace = true [dependencies] aster-bigtcp.workspace = true aster-block.workspace = true +aster-cmdline.workspace = true aster-console.workspace = true aster-fuse.workspace = true aster-input.workspace = true diff --git a/kernel/comps/virtio/src/transport/mmio/bus/arch/x86.rs b/kernel/comps/virtio/src/transport/mmio/bus/arch/x86.rs index 8660ea0c3..76c74bf1a 100644 --- a/kernel/comps/virtio/src/transport/mmio/bus/arch/x86.rs +++ b/kernel/comps/virtio/src/transport/mmio/bus/arch/x86.rs @@ -1,17 +1,65 @@ // SPDX-License-Identifier: MPL-2.0 +use alloc::vec::Vec; + +use aster_cmdline::types::MmioDevice; pub(super) use ostd::arch::irq::MappedIrqLine; -use ostd::{arch::irq::IRQ_CHIP, debug}; +use ostd::{arch::irq::IRQ_CHIP, debug, info, warn}; +use spin::Once; use crate::transport::mmio::bus::MmioRegisterError; pub(super) fn probe_for_device() { - // TODO: The correct method for detecting VirtIO-MMIO devices on x86_64 systems is to parse the - // kernel command line if ACPI tables are absent [1], or the ACPI SSDT if ACPI tables are - // present [2]. Neither of them is supported for now. This function's approach of blindly - // scanning the MMIO region is only a workaround. - // [1]: https://github.com/torvalds/linux/blob/0ff41df1cb268fc69e703a08a57ee14ae967d0ca/drivers/virtio/virtio_mmio.c#L733 - // [2]: https://github.com/torvalds/linux/blob/0ff41df1cb268fc69e703a08a57ee14ae967d0ca/drivers/virtio/virtio_mmio.c#L840 + probe_from_kernel_cmdline(); + probe_from_microvm_constants(); +} + +static VIRTIO_MMIO_CMDLINE_DEVICES: Once> = Once::new(); +aster_cmdline::define_repeatable_kv_param!("virtio_mmio.device", VIRTIO_MMIO_CMDLINE_DEVICES); + +/// Probes Linux-compatible `virtio_mmio.device=@:[:]` parameters. +/// +/// This format follows Linux's `virtio_mmio.device` kernel parameter. +fn probe_from_kernel_cmdline() { + let Some(devices) = VIRTIO_MMIO_CMDLINE_DEVICES.get() else { + return; + }; + + let irq_chip = IRQ_CHIP.get().unwrap(); + + for device in devices { + info!( + "Probe MMIO command-line device: base={:#x}, size={:#x}, irq={}", + device.base(), + device.size().get(), + device.irq().get() + ); + + let Some(mmio_end) = device.base().checked_add(device.size().get()) else { + warn!( + "Ignore MMIO command-line device at {:#x} because its range overflows", + device.base() + ); + continue; + }; + + if let Err(err) = super::try_register_mmio_device(device.base()..mmio_end, |irq_line| { + irq_chip.map_gsi_pin_to(irq_line, device.irq().get()) + }) { + warn!( + "Ignore MMIO command-line device at {:#x} due to an error ({:?})", + device.base(), + err, + ); + } + } +} + +fn probe_from_microvm_constants() { + // TODO: If ACPI tables are present, the correct method for detecting VirtIO-MMIO + // devices is to parse the ACPI SSDT [1]. It is not supported yet, so we fall + // back to blindly scanning QEMU MicroVM's fixed MMIO window as a workaround. + // [1]: https://github.com/torvalds/linux/blob/0ff41df1cb268fc69e703a08a57ee14ae967d0ca/drivers/virtio/virtio_mmio.c#L840 // Constants from QEMU MicroVM. We should remove them as they're QEMU's implementation details. // diff --git a/kernel/comps/virtio/src/transport/mmio/bus/mod.rs b/kernel/comps/virtio/src/transport/mmio/bus/mod.rs index affed2683..18064083f 100644 --- a/kernel/comps/virtio/src/transport/mmio/bus/mod.rs +++ b/kernel/comps/virtio/src/transport/mmio/bus/mod.rs @@ -97,7 +97,7 @@ where Ok(()) } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] enum MmioRegisterError { /// MMIO region not available. MmioUnavailable,