From e048a76afc3d87357ccefa236795c6a79fc6e45a Mon Sep 17 00:00:00 2001 From: Chen Chengjun Date: Fri, 28 Nov 2025 02:03:02 +0000 Subject: [PATCH] Make the semantics of tty-related devices correct --- Makefile | 8 + kernel/comps/framebuffer/src/dummy_console.rs | 39 +++++ kernel/comps/framebuffer/src/lib.rs | 2 + kernel/src/device/mod.rs | 36 +++-- kernel/src/device/tty/device.rs | 101 +++++++++++- kernel/src/device/tty/mod.rs | 6 +- kernel/src/device/tty/n_tty.rs | 146 ++++++++++++------ kernel/src/process/process/mod.rs | 4 +- 8 files changed, 277 insertions(+), 65 deletions(-) create mode 100644 kernel/comps/framebuffer/src/dummy_console.rs diff --git a/Makefile b/Makefile index f4bafa873..9bf87f7a2 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,13 @@ FEATURES ?= NO_DEFAULT_FEATURES ?= 0 COVERAGE ?= 0 ENABLE_BASIC_TEST ?= false +# Specify the primary system console (supported: hvc0, tty0). +# - hvc0: The virtio-console terminal. +# - tty0: The active virtual terminal (VT). +# Asterinas will automatically fall back to tty0 if hvc0 is not available. +# Note that currently the virtual terminal (tty0) can only work with +# linux-efi-handover64 and linux-efi-pe64 boot protocol. +CONSOLE ?= hvc0 # End of global build options. # GDB debugging and profiling options. @@ -57,6 +64,7 @@ CARGO_OSDK := ~/.cargo/bin/cargo-osdk CARGO_OSDK_COMMON_ARGS := --target-arch=$(OSDK_TARGET_ARCH) # The build arguments also apply to the `cargo osdk run` command. CARGO_OSDK_BUILD_ARGS := --kcmd-args="ostd.log_level=$(LOG_LEVEL)" +CARGO_OSDK_BUILD_ARGS += --kcmd-args="console=$(CONSOLE)" CARGO_OSDK_TEST_ARGS := ifeq ($(AUTO_TEST), syscall) diff --git a/kernel/comps/framebuffer/src/dummy_console.rs b/kernel/comps/framebuffer/src/dummy_console.rs new file mode 100644 index 000000000..90704b17b --- /dev/null +++ b/kernel/comps/framebuffer/src/dummy_console.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MPL-2.0 + +use aster_console::{ + font::BitmapFont, + mode::{ConsoleMode, KeyboardMode}, + AnyConsoleDevice, ConsoleCallback, ConsoleSetFontError, +}; + +/// A dummy console device. +/// +/// This is used when no framebuffer is available. All operations are no-ops. +#[derive(Debug)] +pub struct DummyFramebufferConsole; + +impl AnyConsoleDevice for DummyFramebufferConsole { + fn send(&self, _buf: &[u8]) {} + + fn register_callback(&self, _callback: &'static ConsoleCallback) {} + + fn set_font(&self, _font: BitmapFont) -> Result<(), ConsoleSetFontError> { + Err(ConsoleSetFontError::InappropriateDevice) + } + + fn set_mode(&self, _mode: ConsoleMode) -> bool { + false + } + + fn mode(&self) -> Option { + None + } + + fn set_keyboard_mode(&self, _mode: KeyboardMode) -> bool { + false + } + + fn keyboard_mode(&self) -> Option { + None + } +} diff --git a/kernel/comps/framebuffer/src/lib.rs b/kernel/comps/framebuffer/src/lib.rs index abb553748..f99ee3113 100644 --- a/kernel/comps/framebuffer/src/lib.rs +++ b/kernel/comps/framebuffer/src/lib.rs @@ -9,11 +9,13 @@ extern crate alloc; mod ansi_escape; mod console; mod console_input; +mod dummy_console; mod framebuffer; mod pixel; use component::{init_component, ComponentInitError}; pub use console::{FramebufferConsole, CONSOLE_NAME, FRAMEBUFFER_CONSOLE}; +pub use dummy_console::DummyFramebufferConsole; pub use framebuffer::{ColorMapEntry, FrameBuffer, FRAMEBUFFER, MAX_CMAP_SIZE}; pub use pixel::{Pixel, PixelFormat, RenderedPixel}; diff --git a/kernel/src/device/mod.rs b/kernel/src/device/mod.rs index 3fc4a6331..850536ff2 100644 --- a/kernel/src/device/mod.rs +++ b/kernel/src/device/mod.rs @@ -10,8 +10,6 @@ mod pty; mod shm; pub mod tty; -use alloc::format; - use device_id::DeviceId; pub use mem::{getrandom, geturandom}; pub use pty::{new_pty_pair, PtyMaster, PtySlave}; @@ -43,16 +41,22 @@ pub fn init_in_first_process(ctx: &Context) -> Result<()> { let dev_path = fs_resolver.lookup(&FsPath::try_from("/dev")?)?; dev_path.mount(RamFs::new(), PerMountFlags::default(), ctx)?; - tty::init(); + tty::init_in_first_process(); + + let tty0 = Arc::new(tty::Tty0Device); + add_node(tty0, "tty0", &fs_resolver)?; + + let tty1 = tty::tty1_device().clone(); + add_node(tty1, "tty1", &fs_resolver)?; let tty = Arc::new(tty::TtyDevice); add_node(tty, "tty", &fs_resolver)?; - let console = tty::system_console().clone(); + let console = tty::SystemConsole::singleton().clone(); add_node(console, "console", &fs_resolver)?; - for (index, tty) in tty::iter_n_tty().enumerate() { - add_node(tty.clone(), &format!("tty{}", index), &fs_resolver)?; + if let Some(hvc0) = tty::hvc0_device() { + add_node(hvc0.clone(), "hvc0", &fs_resolver)?; } pty::init_in_first_process(&fs_resolver, ctx)?; @@ -75,18 +79,18 @@ pub fn get_device(devid: DeviceId) -> Result> { let minor = devid.minor().get(); match (major, minor) { - (4, minor) => { - let Some(tty) = tty::iter_n_tty().nth(minor as usize) else { - return_errno_with_message!(Errno::EINVAL, "the TTY minor ID is invalid"); - }; - Ok(tty.clone()) - } + (4, 0) => Ok(Arc::new(tty::Tty0Device)), + (4, 1) => Ok(tty::tty1_device().clone()), (5, 0) => Ok(Arc::new(tty::TtyDevice)), + (5, 1) => Ok(tty::SystemConsole::singleton().clone()), + (229, 0) => tty::hvc0_device() + .cloned() + .map(|device| device as _) + .ok_or_else(|| Error::with_message(Errno::ENODEV, "the hvc0 device is not available")), _ => char::lookup(devid) .map(|device| Arc::new(char::CharFile::new(device)) as Arc) - .ok_or(Error::with_message( - Errno::EINVAL, - "the device ID is invalid or unsupported", - )), + .ok_or_else(|| { + Error::with_message(Errno::EINVAL, "the device ID is invalid or unsupported") + }), } } diff --git a/kernel/src/device/tty/device.rs b/kernel/src/device/tty/device.rs index 7164c43d3..a31f07af8 100644 --- a/kernel/src/device/tty/device.rs +++ b/kernel/src/device/tty/device.rs @@ -1,20 +1,69 @@ // SPDX-License-Identifier: MPL-2.0 +//! TTY devices. +//! +//! This module implements TTY devices such as `/dev/tty0`, `/dev/tty`, and `/dev/console`. +//! +//! Reference: + use device_id::{DeviceId, MajorId, MinorId}; +use spin::Once; use crate::{ + device::tty::{hvc0_device, n_tty::VtDriver, tty1_device, Tty}, fs::{ device::{Device, DeviceType}, inode_handle::FileIo, }, + kcmdline::KCmdlineArg, prelude::*, + process::{JobControl, Terminal}, }; +/// Corresponds to `/dev/tty0` in the file system. This device represents the active virtual +/// terminal. +pub struct Tty0Device; + +impl Tty0Device { + fn active_vt(&self) -> &Arc> { + // Currently there is only one virtual terminal `tty1`. + tty1_device() + } +} + +impl Device for Tty0Device { + fn type_(&self) -> DeviceType { + DeviceType::Char + } + + fn id(&self) -> DeviceId { + DeviceId::new(MajorId::new(4), MinorId::new(0)) + } + + fn open(&self) -> Result> { + self.active_vt().open() + } +} + +impl Terminal for Tty0Device { + fn job_control(&self) -> &JobControl { + self.active_vt().job_control() + } +} + /// Corresponds to `/dev/tty` in the file system. This device represents the controlling terminal -/// of the session of current process. +/// of the session of the current process. pub struct TtyDevice; impl Device for TtyDevice { + fn type_(&self) -> DeviceType { + DeviceType::Char + } + + fn id(&self) -> DeviceId { + DeviceId::new(MajorId::new(5), MinorId::new(0)) + } + fn open(&self) -> Result> { let Some(terminal) = current!().terminal() else { return_errno_with_message!( @@ -25,12 +74,60 @@ impl Device for TtyDevice { terminal.open() } +} +/// Corresponds to `/dev/console` in the file system. This device represents a console to which +/// system messages will be sent. +pub struct SystemConsole { + inner: Arc, +} + +impl SystemConsole { + /// Returns the singleton instance of the console device. + pub fn singleton() -> &'static Arc { + static INSTANCE: Once> = Once::new(); + + INSTANCE.call_once(|| { + // TODO: Support specifying multiple TTY devices, e.g., "console=hvc0 console=tty0". + let console_name = KCmdlineArg::singleton() + .get_console_names() + .first() + .map(String::as_str) + .unwrap_or("tty0"); + + let device = match console_name { + "tty0" => Some(Arc::new(Tty0Device) as _), + "hvc0" => hvc0_device().cloned().map(|device| device as _), + _ => None, + }; + let inner = device.unwrap_or_else(|| { + warn!( + "'{}' console not found, falling back to 'tty0'", + console_name + ); + Arc::new(Tty0Device) as _ + }); + + Arc::new(Self { inner }) + }) + } + + /// Returns the terminal associated with the console device. + pub fn terminal(&self) -> &Arc { + &self.inner + } +} + +impl Device for SystemConsole { fn type_(&self) -> DeviceType { DeviceType::Char } fn id(&self) -> DeviceId { - DeviceId::new(MajorId::new(5), MinorId::new(0)) + DeviceId::new(MajorId::new(5), MinorId::new(1)) + } + + fn open(&self) -> Result> { + self.inner.open() } } diff --git a/kernel/src/device/tty/mod.rs b/kernel/src/device/tty/mod.rs index 89b7090be..3ae58580e 100644 --- a/kernel/src/device/tty/mod.rs +++ b/kernel/src/device/tty/mod.rs @@ -32,11 +32,11 @@ mod line_discipline; mod n_tty; mod termio; -pub use device::TtyDevice; +pub use device::{SystemConsole, Tty0Device, TtyDevice}; pub use driver::TtyDriver; pub(super) use flags::TtyFlags; -pub(super) use n_tty::init; -pub use n_tty::{iter_n_tty, system_console}; +pub(super) use n_tty::init_in_first_process; +pub use n_tty::{hvc0_device, tty1_device}; const IO_CAPACITY: usize = 4096; diff --git a/kernel/src/device/tty/n_tty.rs b/kernel/src/device/tty/n_tty.rs index f2467820f..c41aa6aa7 100644 --- a/kernel/src/device/tty/n_tty.rs +++ b/kernel/src/device/tty/n_tty.rs @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 +use alloc::{boxed::Box, sync::Arc}; + use aster_console::AnyConsoleDevice; +use aster_framebuffer::DummyFramebufferConsole; use inherit_methods_macro::inherit_methods; use ostd::mm::{Infallible, VmReader, VmWriter}; use spin::Once; @@ -16,16 +19,20 @@ use crate::{ process::signal::{PollHandle, Pollable}, }; -pub struct ConsoleDriver { +/// The driver for VT (virtual terminal) devices. +// +// TODO: This driver needs to support more features for future VT management. +#[derive(Clone)] +pub struct VtDriver { console: Arc, } -impl TtyDriver for ConsoleDriver { +impl TtyDriver for VtDriver { // Reference: . const DEVICE_MAJOR_ID: u32 = 4; fn open(tty: Arc>) -> Result> { - Ok(Box::new(ConsoleFile(tty))) + Ok(Box::new(TtyFile(tty))) } fn push_output(&self, chs: &[u8]) -> Result { @@ -50,14 +57,50 @@ impl TtyDriver for ConsoleDriver { } } -struct ConsoleFile(Arc>); +/// The driver for hypervisor console devices. +#[derive(Clone)] +pub struct HvcDriver { + console: Arc, +} + +impl TtyDriver for HvcDriver { + // Reference: . + const DEVICE_MAJOR_ID: u32 = 229; + + fn open(tty: Arc>) -> Result> { + Ok(Box::new(TtyFile(tty))) + } + + fn push_output(&self, chs: &[u8]) -> Result { + self.console.send(chs); + Ok(chs.len()) + } + + fn drain_output(&self) {} + + fn echo_callback(&self) -> impl FnMut(&[u8]) + '_ { + |chs| self.console.send(chs) + } + + fn can_push(&self) -> bool { + true + } + + fn notify_input(&self) {} + + fn console(&self) -> Option<&dyn AnyConsoleDevice> { + Some(&*self.console) + } +} + +struct TtyFile(Arc>); #[inherit_methods(from = "self.0")] -impl Pollable for ConsoleFile { +impl Pollable for TtyFile { fn poll(&self, mask: IoEvents, poller: Option<&mut PollHandle>) -> IoEvents; } -impl InodeIo for ConsoleFile { +impl InodeIo for TtyFile { fn read_at( &self, _offset: usize, @@ -78,7 +121,7 @@ impl InodeIo for ConsoleFile { } #[inherit_methods(from = "self.0")] -impl FileIo for ConsoleFile { +impl FileIo for TtyFile { fn ioctl(&self, cmd: IoctlCmd, arg: usize) -> Result; fn check_seekable(&self) -> Result<()> { @@ -90,54 +133,71 @@ impl FileIo for ConsoleFile { } } -static N_TTY: Once>]>> = Once::new(); +static TTY1: Once>> = Once::new(); -pub(in crate::device) fn init() { - let devices = { - let mut devices = aster_console::all_devices(); - // Sort by priorities to ensure that the TTY for the virtio-console device comes first. Is - // there a better way than hardcoding this? - devices.sort_by_key(|(name, _)| match name.as_str() { - aster_virtio::device::console::DEVICE_NAME => 0, - aster_framebuffer::CONSOLE_NAME => 1, - _ => 2, - }); - devices - }; +static HVC0: Once>> = Once::new(); - let ttys = devices - .into_iter() - .enumerate() - .map(|(index, (_, device))| create_n_tty(index as _, device)) - .collect(); - N_TTY.call_once(|| ttys); +/// Returns the `tty1` device. +/// +/// # Panics +/// +/// This function will panic if the `tty1` device has not been initialized. +pub fn tty1_device() -> &'static Arc> { + TTY1.get().unwrap() } -fn create_n_tty(index: u32, device: Arc) -> Arc> { - let driver = ConsoleDriver { - console: device.clone(), +/// Returns the `hvc0` device. +/// +/// Returns `None` if the device is not found nor initialized. +pub fn hvc0_device() -> Option<&'static Arc>> { + HVC0.get() +} + +pub(in crate::device) fn init_in_first_process() { + let devices = aster_console::all_devices(); + + // Initialize the `tty1` device. + + let fb_console = devices + .iter() + .find(|(name, _)| name.as_str() == aster_framebuffer::CONSOLE_NAME) + .map(|(_, device)| device.clone()) + .unwrap_or_else(|| Arc::new(DummyFramebufferConsole)); + + let driver = VtDriver { + console: fb_console.clone(), }; + let tty1 = Tty::new(1, driver); + TTY1.call_once(|| tty1.clone()); - let tty = Tty::new(index, driver); - let tty_cloned = tty.clone(); - - device.register_callback(Box::leak(Box::new( + fb_console.register_callback(Box::leak(Box::new( move |mut reader: VmReader| { let mut chs = vec![0u8; reader.remain()]; reader.read(&mut VmWriter::from(chs.as_mut_slice())); - let _ = tty.push_input(chs.as_slice()); + let _ = tty1.push_input(chs.as_slice()); }, ))); - tty_cloned -} + // Initialize the `hvc0` device if the virtio console is available. -/// Returns the system console, i.e., `/dev/console`. -pub fn system_console() -> &'static Arc> { - &N_TTY.get().unwrap()[0] -} + let virtio_console = devices + .iter() + .find(|(name, _)| name.as_str() == aster_virtio::device::console::DEVICE_NAME) + .map(|(_, device)| device.clone()); -/// Iterates all TTY devices, i.e., `/dev/tty1`, `/dev/tty2`, e.t.c. -pub fn iter_n_tty() -> impl Iterator>> { - N_TTY.get().unwrap().iter() + if let Some(virtio_console) = virtio_console { + let driver = HvcDriver { + console: virtio_console.clone(), + }; + let hvc0 = Tty::new(0, driver); + HVC0.call_once(|| hvc0.clone()); + + virtio_console.register_callback(Box::leak(Box::new( + move |mut reader: VmReader| { + let mut chs = vec![0u8; reader.remain()]; + reader.read(&mut VmWriter::from(chs.as_mut_slice())); + let _ = hvc0.push_input(chs.as_slice()); + }, + ))); + } } diff --git a/kernel/src/process/process/mod.rs b/kernel/src/process/process/mod.rs index 924c2f7ea..6dfd77f6e 100644 --- a/kernel/src/process/process/mod.rs +++ b/kernel/src/process/process/mod.rs @@ -70,7 +70,9 @@ pub(super) fn init_on_each_cpu() { pub(super) fn init_in_first_process(ctx: &Context) { // FIXME: This should be done by the userspace init process. - (crate::device::tty::system_console().clone() as Arc) + (crate::device::tty::SystemConsole::singleton() + .terminal() + .clone() as Arc) .set_control(ctx.process.as_ref()) .unwrap(); }