From 43ae2d67e7f715b3be0a59ef7585a98d0f074155 Mon Sep 17 00:00:00 2001 From: Jonesxq <239089032+Jonesxq@users.noreply.github.com> Date: Thu, 30 Jul 2026 03:36:51 +0800 Subject: [PATCH 1/3] Add SBI console input on RISC-V --- .github/workflows/ci.yml | 1 - src/arch/riscv64/kernel/serial.rs | 87 +++++++++++++++++++++++++++---- src/console/mod.rs | 5 ++ src/fd/stdio/console.rs | 43 ++++++++++++--- 4 files changed, 118 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2f1233865..559dc19067 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -258,7 +258,6 @@ jobs: - run: cargo xtask ci rs --arch ${{ matrix.arch }} --profile ${{ matrix.profile }} ${{ matrix.rs_flags }} --package hello_world --no-default-features qemu ${{ matrix.qemu_flags }} --microvm if: matrix.arch == 'x86_64' - run: cargo xtask ci rs --arch ${{ matrix.arch }} --profile ${{ matrix.profile }} ${{ matrix.rs_flags }} --package stdin qemu ${{ matrix.qemu_flags }} - if: matrix.arch != 'riscv64' - run: cargo xtask ci rs --arch ${{ matrix.arch }} --profile ${{ matrix.profile }} ${{ matrix.rs_flags }} --package stdin --features hermit/virtio-console qemu ${{ matrix.qemu_flags }} --devices virtio-console-pci if: matrix.arch != 'riscv64' - run: cargo xtask ci rs --arch ${{ matrix.arch }} --profile ${{ matrix.profile }} ${{ matrix.rs_flags }} --package stdin --features hermit/virtio-console --no-default-features qemu ${{ matrix.qemu_flags }} --devices virtio-console-mmio --microvm diff --git a/src/arch/riscv64/kernel/serial.rs b/src/arch/riscv64/kernel/serial.rs index 30f1f3dc97..26cf3502ce 100644 --- a/src/arch/riscv64/kernel/serial.rs +++ b/src/arch/riscv64/kernel/serial.rs @@ -1,12 +1,64 @@ +use core::hint; + use embedded_io::{ErrorType, Read, ReadReady, Write}; +use sbi_rt::Physical; use crate::errno::Errno; -pub(crate) struct SerialDevice; +const SBI_CONSOLE_BUFFER_SIZE: usize = 256; + +#[repr(C, align(4096))] +pub(crate) struct SerialDevice { + sbi_buffer: [u8; SBI_CONSOLE_BUFFER_SIZE], + buffered_byte: Option, +} impl SerialDevice { pub fn new() -> Self { - Self {} + Self { + sbi_buffer: [0; SBI_CONSOLE_BUFFER_SIZE], + buffered_byte: None, + } + } + + fn read_from_console(&mut self, buf: &mut [u8]) -> Result { + let len = buf.len().min(self.sbi_buffer.len()); + if len == 0 { + return Ok(0); + } + + // Kernel data is identity-mapped on RISC-V. Using a page-aligned bounce buffer + // avoids walking the page table before it exists or while it is already locked. + let physical = Physical::<&mut [u8]>::new(len, self.sbi_buffer.as_mut_ptr().addr(), 0); + let read = sbi_rt::console_read(physical) + .into_result() + .map_err(|_| Errno::Io)?; + + if read > len { + return Err(Errno::Io); + } + + buf[..read].copy_from_slice(&self.sbi_buffer[..read]); + Ok(read) + } + + fn write_to_console(&mut self, buf: &[u8]) -> Result { + let len = buf.len().min(self.sbi_buffer.len()); + if len == 0 { + return Ok(0); + } + + self.sbi_buffer[..len].copy_from_slice(&buf[..len]); + let physical = Physical::<&[u8]>::new(len, self.sbi_buffer.as_ptr().addr(), 0); + let written = sbi_rt::console_write(physical) + .into_result() + .map_err(|_| Errno::Io)?; + + if written > len { + return Err(Errno::Io); + } + + Ok(written) } } @@ -16,24 +68,41 @@ impl ErrorType for SerialDevice { impl Read for SerialDevice { fn read(&mut self, buf: &mut [u8]) -> Result { - let _ = buf; - Ok(0) + if buf.is_empty() { + return Ok(0); + } + + if let Some(byte) = self.buffered_byte.take() { + buf[0] = byte; + return Ok(1); + } + + self.read_from_console(buf) } } impl ReadReady for SerialDevice { fn read_ready(&mut self) -> Result { - Ok(false) + if self.buffered_byte.is_none() { + let mut byte = 0; + if self.read_from_console(core::slice::from_mut(&mut byte))? == 1 { + self.buffered_byte = Some(byte); + } + } + + Ok(self.buffered_byte.is_some()) } } impl Write for SerialDevice { fn write(&mut self, buf: &[u8]) -> Result { - for byte in buf { - sbi_rt::console_write_byte(*byte); + loop { + let written = self.write_to_console(buf)?; + if written > 0 || buf.is_empty() { + return Ok(written); + } + hint::spin_loop(); } - - Ok(buf.len()) } fn flush(&mut self) -> Result<(), Self::Error> { diff --git a/src/console/mod.rs b/src/console/mod.rs index 4b974d8d50..3d229fa33d 100644 --- a/src/console/mod.rs +++ b/src/console/mod.rs @@ -15,6 +15,7 @@ use crate::executor::WakerRegistration; const SERIAL_BUFFER_SIZE: usize = 256; +#[cfg_attr(target_arch = "riscv64", allow(clippy::large_enum_variant))] pub(crate) enum IoDevice { #[cfg(feature = "uhyve")] Uhyve(uhyve::UhyveSerial), @@ -89,6 +90,10 @@ impl Console { } } + pub fn requires_input_polling(&self) -> bool { + cfg!(target_arch = "riscv64") && matches!(&self.device, IoDevice::Uart(_)) + } + #[cfg(feature = "virtio-console")] pub fn replace_device(&mut self, device: IoDevice) { self.device = device; diff --git a/src/fd/stdio/console.rs b/src/fd/stdio/console.rs index 93f688f305..36cb56a261 100644 --- a/src/fd/stdio/console.rs +++ b/src/fd/stdio/console.rs @@ -11,22 +11,49 @@ pub struct ConsoleStdin; impl ObjectInterface for ConsoleStdin { async fn poll(&self, event: PollEvent) -> io::Result { - let available = if CONSOLE.lock().read_ready()? { - PollEvent::POLLIN | PollEvent::POLLRDNORM | PollEvent::POLLRDBAND - } else { - PollEvent::empty() - }; - - Ok(event & available) + future::poll_fn(|cx| { + let readable = PollEvent::POLLIN | PollEvent::POLLRDNORM | PollEvent::POLLRDBAND; + let (available, requires_polling) = { + let mut console = CONSOLE.lock(); + (console.read_ready()?, console.requires_input_polling()) + }; + let ready = event + & if available { + readable + } else { + PollEvent::empty() + }; + + if !ready.is_empty() || !event.intersects(readable) { + Poll::Ready(Ok(ready)) + } else { + if requires_polling { + cx.waker().wake_by_ref(); + } else { + CONSOLE_WAKER.lock().register(cx.waker()); + if CONSOLE.lock().read_ready()? { + return Poll::Ready(Ok(event & readable)); + } + } + Poll::Pending + } + }) + .await } async fn read(&self, buf: &mut [u8]) -> io::Result { future::poll_fn(|cx| { - let read_bytes = CONSOLE.lock().read(buf)?; + let (read_bytes, requires_polling) = { + let mut console = CONSOLE.lock(); + (console.read(buf)?, console.requires_input_polling()) + }; if read_bytes > 0 { CONSOLE.lock().write_all(&buf[..read_bytes])?; CONSOLE.lock().flush()?; Poll::Ready(Ok(read_bytes)) + } else if requires_polling { + cx.waker().wake_by_ref(); + Poll::Pending } else { CONSOLE_WAKER.lock().register(cx.waker()); Poll::Pending From 5ff894113da25f2ff9ae2ce7fc5f013e84b5055b Mon Sep 17 00:00:00 2001 From: Jonesxq <239089032+Jonesxq@users.noreply.github.com> Date: Thu, 30 Jul 2026 04:42:32 +0800 Subject: [PATCH 2/3] Avoid competing RISC-V stdin consumers --- xtask/src/ci/qemu.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/xtask/src/ci/qemu.rs b/xtask/src/ci/qemu.rs index e14ec22c07..e25c149311 100644 --- a/xtask/src/ci/qemu.rs +++ b/xtask/src/ci/qemu.rs @@ -135,7 +135,7 @@ impl Qemu { let qemu = cmd!(sh, "{program} {arg...}") .args(&["-display", "none"]) - .args(self.serial_args()) + .args(self.serial_args(image_name, arch)) .args(self.image_args(image, arch)?) .args(self.machine_args(arch)) .args(self.cpu_args(arch)) @@ -382,13 +382,16 @@ impl Qemu { 1024 } - fn serial_args(&self) -> &[&str] { + fn serial_args(&self, image_name: &str, arch: Arch) -> &[&str] { if self .devices .iter() .any(|device| matches!(device, Device::VirtioConsoleMmio | Device::VirtioConsolePci)) { &[] + } else if arch == Arch::Riscv64 && image_name == "stdin" { + // OpenSBI semihosting and the serial device would otherwise both read stdin. + &["-serial", "none"] } else { &["-serial", "stdio"] } From 8f19f39eeb3829c9b716822eae638fa9c3985e1a Mon Sep 17 00:00:00 2001 From: Jonesxq <239089032+Jonesxq@users.noreply.github.com> Date: Thu, 30 Jul 2026 04:55:18 +0800 Subject: [PATCH 3/3] Run RISC-V stdin without semihosting --- .github/workflows/ci.yml | 3 +++ xtask/src/ci/qemu.rs | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 559dc19067..547ddebc1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -258,6 +258,9 @@ jobs: - run: cargo xtask ci rs --arch ${{ matrix.arch }} --profile ${{ matrix.profile }} ${{ matrix.rs_flags }} --package hello_world --no-default-features qemu ${{ matrix.qemu_flags }} --microvm if: matrix.arch == 'x86_64' - run: cargo xtask ci rs --arch ${{ matrix.arch }} --profile ${{ matrix.profile }} ${{ matrix.rs_flags }} --package stdin qemu ${{ matrix.qemu_flags }} + if: matrix.arch != 'riscv64' + - run: cargo xtask ci rs --arch ${{ matrix.arch }} --profile ${{ matrix.profile }} --package stdin qemu ${{ matrix.qemu_flags }} --no-semihosting + if: matrix.arch == 'riscv64' - run: cargo xtask ci rs --arch ${{ matrix.arch }} --profile ${{ matrix.profile }} ${{ matrix.rs_flags }} --package stdin --features hermit/virtio-console qemu ${{ matrix.qemu_flags }} --devices virtio-console-pci if: matrix.arch != 'riscv64' - run: cargo xtask ci rs --arch ${{ matrix.arch }} --profile ${{ matrix.profile }} ${{ matrix.rs_flags }} --package stdin --features hermit/virtio-console --no-default-features qemu ${{ matrix.qemu_flags }} --devices virtio-console-mmio --microvm diff --git a/xtask/src/ci/qemu.rs b/xtask/src/ci/qemu.rs index e25c149311..2859c9a979 100644 --- a/xtask/src/ci/qemu.rs +++ b/xtask/src/ci/qemu.rs @@ -41,6 +41,10 @@ pub struct Qemu { #[arg(long)] uefi: bool, + /// Disable semihosting. + #[arg(long)] + no_semihosting: bool, + /// Devices to enable. #[arg(long)] devices: Vec, @@ -135,7 +139,7 @@ impl Qemu { let qemu = cmd!(sh, "{program} {arg...}") .args(&["-display", "none"]) - .args(self.serial_args(image_name, arch)) + .args(self.serial_args()) .args(self.image_args(image, arch)?) .args(self.machine_args(arch)) .args(self.cpu_args(arch)) @@ -345,7 +349,9 @@ impl Qemu { cpu_args.push("max,lpa2=off".to_owned()); } - cpu_args.push("-semihosting".to_owned()); + if !self.no_semihosting { + cpu_args.push("-semihosting".to_owned()); + } cpu_args } Arch::Riscv64 => { @@ -358,7 +364,9 @@ impl Qemu { } } - cpu_args.push("-semihosting".to_owned()); + if !self.no_semihosting { + cpu_args.push("-semihosting".to_owned()); + } cpu_args } } @@ -382,16 +390,13 @@ impl Qemu { 1024 } - fn serial_args(&self, image_name: &str, arch: Arch) -> &[&str] { + fn serial_args(&self) -> &[&str] { if self .devices .iter() .any(|device| matches!(device, Device::VirtioConsoleMmio | Device::VirtioConsolePci)) { &[] - } else if arch == Arch::Riscv64 && image_name == "stdin" { - // OpenSBI semihosting and the serial device would otherwise both read stdin. - &["-serial", "none"] } else { &["-serial", "stdio"] }