Make the semantics of tty-related devices correct

This commit is contained in:
Chen Chengjun 2025-11-28 02:03:02 +00:00 committed by Tate, Hongliang Tian
parent 1b11a8453e
commit e048a76afc
8 changed files with 277 additions and 65 deletions

View File

@ -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)

View File

@ -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<ConsoleMode> {
None
}
fn set_keyboard_mode(&self, _mode: KeyboardMode) -> bool {
false
}
fn keyboard_mode(&self) -> Option<KeyboardMode> {
None
}
}

View File

@ -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};

View File

@ -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<Arc<dyn Device>> {
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<dyn Device>)
.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")
}),
}
}

View File

@ -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: <https://www.kernel.org/doc/html/latest/admin-guide/devices.html>
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<Tty<VtDriver>> {
// 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<Box<dyn FileIo>> {
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<Box<dyn FileIo>> {
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<dyn Terminal>,
}
impl SystemConsole {
/// Returns the singleton instance of the console device.
pub fn singleton() -> &'static Arc<SystemConsole> {
static INSTANCE: Once<Arc<SystemConsole>> = 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<dyn Terminal> {
&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<Box<dyn FileIo>> {
self.inner.open()
}
}

View File

@ -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;

View File

@ -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<dyn AnyConsoleDevice>,
}
impl TtyDriver for ConsoleDriver {
impl TtyDriver for VtDriver {
// Reference: <https://elixir.bootlin.com/linux/v6.17/source/include/uapi/linux/major.h#L18>.
const DEVICE_MAJOR_ID: u32 = 4;
fn open(tty: Arc<Tty<Self>>) -> Result<Box<dyn FileIo>> {
Ok(Box::new(ConsoleFile(tty)))
Ok(Box::new(TtyFile(tty)))
}
fn push_output(&self, chs: &[u8]) -> Result<usize> {
@ -50,14 +57,50 @@ impl TtyDriver for ConsoleDriver {
}
}
struct ConsoleFile(Arc<Tty<ConsoleDriver>>);
/// The driver for hypervisor console devices.
#[derive(Clone)]
pub struct HvcDriver {
console: Arc<dyn AnyConsoleDevice>,
}
impl TtyDriver for HvcDriver {
// Reference: <https://elixir.bootlin.com/linux/v6.17/source/Documentation/admin-guide/devices.txt#L2936>.
const DEVICE_MAJOR_ID: u32 = 229;
fn open(tty: Arc<Tty<Self>>) -> Result<Box<dyn FileIo>> {
Ok(Box::new(TtyFile(tty)))
}
fn push_output(&self, chs: &[u8]) -> Result<usize> {
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<D>(Arc<Tty<D>>);
#[inherit_methods(from = "self.0")]
impl Pollable for ConsoleFile {
impl<D: TtyDriver> Pollable for TtyFile<D> {
fn poll(&self, mask: IoEvents, poller: Option<&mut PollHandle>) -> IoEvents;
}
impl InodeIo for ConsoleFile {
impl<D: TtyDriver> InodeIo for TtyFile<D> {
fn read_at(
&self,
_offset: usize,
@ -78,7 +121,7 @@ impl InodeIo for ConsoleFile {
}
#[inherit_methods(from = "self.0")]
impl FileIo for ConsoleFile {
impl<D: TtyDriver> FileIo for TtyFile<D> {
fn ioctl(&self, cmd: IoctlCmd, arg: usize) -> Result<i32>;
fn check_seekable(&self) -> Result<()> {
@ -90,54 +133,71 @@ impl FileIo for ConsoleFile {
}
}
static N_TTY: Once<Box<[Arc<Tty<ConsoleDriver>>]>> = Once::new();
static TTY1: Once<Arc<Tty<VtDriver>>> = 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<Arc<Tty<HvcDriver>>> = 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<Tty<VtDriver>> {
TTY1.get().unwrap()
}
fn create_n_tty(index: u32, device: Arc<dyn AnyConsoleDevice>) -> Arc<Tty<ConsoleDriver>> {
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<Tty<HvcDriver>>> {
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<Infallible>| {
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<Tty<ConsoleDriver>> {
&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<Item = &'static Arc<Tty<ConsoleDriver>>> {
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<Infallible>| {
let mut chs = vec![0u8; reader.remain()];
reader.read(&mut VmWriter::from(chs.as_mut_slice()));
let _ = hvc0.push_input(chs.as_slice());
},
)));
}
}

View File

@ -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<dyn Terminal>)
(crate::device::tty::SystemConsole::singleton()
.terminal()
.clone() as Arc<dyn Terminal>)
.set_control(ctx.process.as_ref())
.unwrap();
}