Manage poweroff and restart handlers

This commit is contained in:
Ruihan Li 2025-11-28 12:42:52 +08:00 committed by Tate, Hongliang Tian
parent 15446386af
commit 35d70fca71
26 changed files with 289 additions and 208 deletions

View File

@ -1,4 +1,9 @@
// SPDX-License-Identifier: MPL-2.0
pub mod cpu;
mod power;
pub mod signal;
pub fn init() {
power::init();
}

View File

@ -0,0 +1,50 @@
// SPDX-License-Identifier: MPL-2.0
use ostd::{
arch::boot::DEVICE_TREE,
io::IoMem,
mm::VmIoOnce,
power::{inject_poweroff_handler, ExitCode},
};
use spin::Once;
static POWEROFF_REG_AND_VAL: Once<(IoMem, u8)> = Once::new();
fn try_poweroff(_code: ExitCode) {
// If possible, keep this method panic-free because it may be called by the panic handler.
if let Some((reg, val)) = POWEROFF_REG_AND_VAL.get() {
let _ = reg.write_once(0, val);
}
}
pub(super) fn init() {
let Some((poweroff_addr, poweroff_value)) = lookup_poweroff_paddr_value() else {
return;
};
let Ok(poweroff_reg) = IoMem::acquire(poweroff_addr..poweroff_addr + size_of::<u8>()) else {
log::warn!("The poweroff register from syscon-poweroff is not available");
return;
};
POWEROFF_REG_AND_VAL.call_once(move || (poweroff_reg, poweroff_value));
inject_poweroff_handler(try_poweroff);
}
fn lookup_poweroff_paddr_value() -> Option<(usize, u8)> {
let device_tree = DEVICE_TREE.get().unwrap();
let ged = device_tree.find_node("/ged")?;
if !ged.compatible()?.all().any(|c| c == "syscon") {
return None;
}
let ged_reg_base_address = ged.reg()?.next()?.starting_address as usize;
let poweroff = device_tree.find_node("/poweroff")?;
if !poweroff.compatible()?.all().any(|c| c == "syscon-poweroff") {
return None;
}
let poweroff_offset = poweroff.property("offset")?.as_usize()?;
let poweroff_value = poweroff.property("value")?.as_usize()? as u8;
Some((ged_reg_base_address + poweroff_offset, poweroff_value))
}

View File

@ -2,3 +2,5 @@
pub mod cpu;
pub mod signal;
pub fn init() {}

View File

@ -2,3 +2,5 @@
pub mod cpu;
pub mod signal;
pub fn init() {}

View File

@ -4,11 +4,7 @@
use aster_cmdline::KCMDLINE;
use component::InitStage;
use ostd::{
arch::qemu::{exit_qemu, QemuExitCode},
cpu::CpuId,
util::id_set::Id,
};
use ostd::{cpu::CpuId, util::id_set::Id};
use spin::once::Once;
use crate::{
@ -39,6 +35,7 @@ pub(super) fn main() {
}
fn init() {
crate::arch::init();
crate::thread::init();
crate::util::random::init();
crate::driver::init();
@ -108,13 +105,15 @@ fn bsp_idle_loop() {
ostd::task::halt_cpu();
}
// TODO: exit via qemu isa debug device should not be the only way.
let exit_code = if init_process.status().exit_code() == 0 {
QemuExitCode::Success
// According to the Linux implementation, we should panic once the init process exits.
// Currently, we choose to power off the machine for more flexibility in testing with QEMU.
let raw_exit_code = init_process.status().exit_code();
let exit_code = if raw_exit_code == 0 {
ostd::power::ExitCode::Success
} else {
QemuExitCode::Failed
ostd::power::ExitCode::Failure
};
exit_qemu(exit_code);
ostd::power::poweroff(exit_code);
}
fn ap_idle_loop() {

View File

@ -1,5 +1,7 @@
// SPDX-License-Identifier: MPL-2.0
use ostd::power::{poweroff, restart, ExitCode};
use super::SyscallReturn;
use crate::{prelude::*, process::credentials::capabilities::CapSet};
@ -55,14 +57,9 @@ pub fn sys_reboot(
let cmd = RebootCmd::try_from(op)?;
// TODO: Perform any necessary cleanup before powering off or restarting.
match cmd {
RebootCmd::Restart => {
// TODO: Implement restart functionality.
return_errno_with_message!(Errno::ENOSYS, "restart is not implemented yet");
}
RebootCmd::Halt | RebootCmd::PowerOff => {
// TODO: Perform any necessary cleanup before powering off.
ostd::arch::cpu::poweroff::poweroff();
}
RebootCmd::Restart => restart(ExitCode::Success),
RebootCmd::Halt | RebootCmd::PowerOff => poweroff(ExitCode::Success),
}
}

View File

@ -38,14 +38,14 @@ fn main() {
let test_task = move || {
use alloc::string::ToString;
use ostd::arch::qemu::{exit_qemu, QemuExitCode};
use ostd::power::{poweroff, ExitCode};
match run_ktests(
get_ktest_test_whitelist().map(|s| s.iter().map(|s| s.to_string())),
get_ktest_crate_whitelist(),
) {
KtestResult::Ok => exit_qemu(QemuExitCode::Success),
KtestResult::Failed => exit_qemu(QemuExitCode::Failed),
KtestResult::Ok => poweroff(ExitCode::Success),
KtestResult::Failed => poweroff(ExitCode::Failure),
};
};

View File

@ -12,11 +12,11 @@ use alloc::sync::Arc;
use alloc::vec;
use ostd::arch::cpu::context::UserContext;
use ostd::arch::qemu::{exit_qemu, QemuExitCode};
use ostd::mm::{
CachePolicy, FallibleVmRead, FrameAllocOptions, PageFlags, PageProperty, Vaddr, VmIo, VmSpace,
VmWriter, PAGE_SIZE,
};
use ostd::power::{poweroff, ExitCode};
use ostd::prelude::*;
use ostd::task::{disable_preempt, Task, TaskOptions};
use ostd::user::{ReturnReason, UserMode};
@ -130,7 +130,7 @@ fn handle_syscall(user_context: &mut UserContext, vm_space: &VmSpace) {
// Manipulate the user-space CPU registers safely.
user_context.set_rax(buf_len);
}
SYS_EXIT => exit_qemu(QemuExitCode::Success),
SYS_EXIT => poweroff(ExitCode::Success),
_ => unimplemented!(),
}
}

View File

@ -4,4 +4,3 @@
pub mod context;
pub mod local;
pub mod poweroff;

View File

@ -1,11 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//! Provides the implementation of the poweroff functionality.
use crate::arch::qemu::{exit_qemu, QemuExitCode};
/// Powers off the system.
pub fn poweroff() -> ! {
// TODO: Implement the poweroff behavior on a real machine.
exit_qemu(QemuExitCode::Success);
}

View File

@ -11,7 +11,6 @@ mod io;
pub(crate) mod iommu;
pub(crate) mod irq;
pub(crate) mod mm;
pub mod qemu;
pub(crate) mod serial;
pub(crate) mod task;
mod timer;

View File

@ -1,47 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//! Providing the ability to exit QEMU and return a value as debug result.
use crate::arch::{boot::DEVICE_TREE, mm::paddr_to_daddr};
/// The exit code of QEMU.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QemuExitCode {
/// The code that indicates a successful exit.
Success,
/// The code that indicates a failed exit.
Failed,
}
/// Exits QEMU with the given exit code.
// FIXME: Support the transfer of the exit code to QEMU and multiple platforms.
pub fn exit_qemu(_exit_code: QemuExitCode) {
let (poweroff_addr, poweroff_value) = lookup_poweroff_paddr_value().unwrap();
let poweroff_daddr = paddr_to_daddr(poweroff_addr) as *mut u8;
// SAFETY: It is safe because the poweroff address is acquired from the device tree,
// and be mapped in DMW2.
unsafe {
core::ptr::write_volatile(poweroff_daddr, poweroff_value);
}
}
// FIXME: We should reserve the address region in `io_mem_allocator`.
fn lookup_poweroff_paddr_value() -> Option<(usize, u8)> {
let device_tree = DEVICE_TREE.get().unwrap();
let ged = device_tree.find_node("/ged")?;
if !ged.compatible()?.all().any(|c| c == "syscon") {
return None;
}
let ged_reg_base_address = ged.reg()?.next()?.starting_address as usize;
let poweroff = device_tree.find_node("/poweroff").unwrap();
if !poweroff.compatible()?.all().any(|c| c == "syscon-poweroff") {
return None;
}
let poweroff_offset = poweroff.property("offset")?.as_usize()?;
let poweroff_value = poweroff.property("value")?.as_usize()? as u8;
Some((ged_reg_base_address + poweroff_offset, poweroff_value))
}

View File

@ -5,4 +5,3 @@
pub mod context;
pub mod extension;
pub mod local;
pub mod poweroff;

View File

@ -1,11 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//! Provides the implementation of the poweroff functionality.
use crate::arch::qemu::{exit_qemu, QemuExitCode};
/// Powers off the system.
pub fn poweroff() -> ! {
// TODO: Implement the poweroff behavior on a real machine.
exit_qemu(QemuExitCode::Success);
}

View File

@ -11,7 +11,7 @@ mod io;
pub(crate) mod iommu;
pub mod irq;
pub(crate) mod mm;
pub mod qemu;
mod power;
pub(crate) mod serial;
pub(crate) mod task;
mod timer;
@ -61,6 +61,8 @@ pub(crate) unsafe fn late_init_on_bsp() {
// 1. All the system device memory have been removed from the builder.
// 2. RISC-V platforms do not have port I/O.
unsafe { crate::io::init(io_mem_builder) };
power::init();
}
/// Initializes application-processor-specific state.

View File

@ -0,0 +1,24 @@
// SPDX-License-Identifier: MPL-2.0
//! Power management.
use crate::power::{inject_poweroff_handler, inject_restart_handler, ExitCode};
fn try_poweroff(code: ExitCode) {
let _ = match code {
ExitCode::Success => sbi_rt::system_reset(sbi_rt::Shutdown, sbi_rt::NoReason),
ExitCode::Failure => sbi_rt::system_reset(sbi_rt::Shutdown, sbi_rt::SystemFailure),
};
}
fn try_restart(code: ExitCode) {
let _ = match code {
ExitCode::Success => sbi_rt::system_reset(sbi_rt::ColdReboot, sbi_rt::NoReason),
ExitCode::Failure => sbi_rt::system_reset(sbi_rt::ColdReboot, sbi_rt::SystemFailure),
};
}
pub(super) fn init() {
inject_poweroff_handler(try_poweroff);
inject_restart_handler(try_restart);
}

View File

@ -1,26 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//! Providing the ability to exit QEMU and return a value as debug result.
/// The exit code of QEMU.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QemuExitCode {
/// The code that indicates a successful exit.
Success,
/// The code that indicates a failed exit.
Failed,
}
/// Exit QEMU with the given exit code.
pub fn exit_qemu(exit_code: QemuExitCode) {
log::debug!("exit qemu with exit code {exit_code:?}");
let error_code = match exit_code {
QemuExitCode::Success => sbi_rt::system_reset(sbi_rt::Shutdown, sbi_rt::NoReason),
QemuExitCode::Failed => sbi_rt::system_reset(sbi_rt::Shutdown, sbi_rt::SystemFailure),
};
log::error!(
"SBI system_reset cannot shut down the underlying machine, error code: {:#?}",
error_code
);
}

View File

@ -6,4 +6,3 @@ pub mod context;
pub mod cpuid;
pub mod extension;
pub mod local;
pub mod poweroff;

View File

@ -1,17 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//! Provides the implementation of the poweroff functionality.
use crate::arch::{
cpu::cpuid::query_is_running_in_qemu,
qemu::{exit_qemu, QemuExitCode},
};
/// Powers off the system.
pub fn poweroff() -> ! {
if query_is_running_in_qemu() {
exit_qemu(QemuExitCode::Success);
}
todo!("Implement ACPI shutdown");
}

View File

@ -10,7 +10,7 @@ pub(crate) mod iommu;
pub mod irq;
pub mod kernel;
pub(crate) mod mm;
pub mod qemu;
mod power;
pub(crate) mod serial;
pub(crate) mod task;
mod timer;
@ -74,8 +74,6 @@ pub(crate) unsafe fn late_init_on_bsp() {
kernel::tsc::init_tsc_freq();
timer::init_on_bsp();
kernel::acpi::init();
// SAFETY: We're on the BSP and we're ready to boot all APs.
unsafe { crate::boot::smp::boot_all_aps() };
@ -92,6 +90,9 @@ pub(crate) unsafe fn late_init_on_bsp() {
// 2. All the port I/O regions belonging to the system device are defined using the macros.
// 3. `MAX_IO_PORT` defined in `crate::arch::io` is the maximum value specified by x86-64.
unsafe { crate::io::init(io_mem_builder) };
kernel::acpi::init();
power::init();
}
/// Initializes application-processor-specific state.

View File

@ -0,0 +1,62 @@
// SPDX-License-Identifier: MPL-2.0
//! Power management.
mod qemu_isa_debug {
//! The isa-debug-exit device in QEMU.
//!
//! Reference: <https://elixir.bootlin.com/qemu/v10.1.2/source/hw/misc/debugexit.c>
use spin::Once;
use crate::{
arch::device::io_port::WriteOnlyAccess,
io::IoPort,
power::{inject_poweroff_handler, ExitCode},
};
// For `qemu-system-x86_64`, the exit code will be `(code << 1) | 1`. So it is not possible to
// let QEMU invoke `exit(0)`. We also need to check if the exit code is returned by the kernel,
// so we cannot use `0` as `EXIT_SUCCESS` because it may conflict with QEMU's return value `1`,
// which indicates that QEMU itself fails.
const EXIT_SUCCESS: u32 = 0x10;
const EXIT_FAILURE: u32 = 0x20;
static DEBUG_EXIT_PORT: Once<IoPort<u32, WriteOnlyAccess>> = Once::new();
fn try_exit_qemu(code: ExitCode) {
let value = match code {
ExitCode::Success => EXIT_SUCCESS,
ExitCode::Failure => EXIT_FAILURE,
};
// If possible, keep this method panic-free because it may be called by the panic handler.
if let Some(port) = DEBUG_EXIT_PORT.get() {
port.write(value);
}
}
pub(super) fn init() {
const DEBUG_EXIT_PORT_NUM: u16 = 0xF4;
let debug_exit_port = IoPort::acquire(DEBUG_EXIT_PORT_NUM).unwrap();
DEBUG_EXIT_PORT.call_once(|| debug_exit_port);
inject_poweroff_handler(try_exit_qemu);
}
}
pub(super) fn init() {
use super::cpu::cpuid;
if !cpuid::query_is_running_in_qemu() {
return;
}
// FIXME: We assume that the kernel is running in QEMU with the following QEMU command line
// arguments that specify the isa-debug-exit device:
// `-device isa-debug-exit,iobase=0xf4,iosize=0x04`.
log::info!("QEMU hypervisor detected, assuming that the isa-debug-exit device exists");
qemu_isa_debug::init();
}

View File

@ -1,38 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//! Provides the ability to exit QEMU and return a value as debug result.
/// The exit code of x86 QEMU isa debug device.
///
/// In `qemu-system-x86_64` the exit code will be `(code << 1) | 1`. So you
/// could never let QEMU invoke `exit(0)`. We also need to check if the exit
/// code is returned by the kernel, so we couldn't use 0 as exit_success
/// because this may conflict with QEMU return value 1, which indicates that
/// QEMU itself fails.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
/// The code that indicates a successful exit.
Success = 0x10,
/// The code that indicates a failed exit.
Failed = 0x20,
}
/// Exits QEMU with the given exit code.
///
/// This function assumes that the kernel is run in QEMU with the following
/// QEMU command line arguments that specifies the ISA debug exit device:
/// `-device isa-debug-exit,iobase=0xf4,iosize=0x04`.
pub fn exit_qemu(exit_code: QemuExitCode) {
#[cfg(feature = "coverage")]
crate::coverage::on_qemu_exit();
use x86_64::instructions::port::Port;
let mut port = Port::new(0xf4);
// SAFETY: The write to the ISA debug exit port is safe and `0xf4` should
// be the port number.
unsafe {
port.write(exit_code as u32);
}
}

View File

@ -8,18 +8,21 @@
use alloc::vec::Vec;
use core::mem::ManuallyDrop;
use spin::Once;
use crate::sync::SpinLock;
/// A hook to be invoked on QEMU exit for dumping the code coverage data.
pub(crate) fn on_qemu_exit() {
let mut coverage = ManuallyDrop::new(Vec::new());
static COV_LOCK: SpinLock<()> = SpinLock::new(());
let _guard = COV_LOCK.disable_irq().lock();
// SAFETY: The above lock ensures that this function is not called
// concurrently by multiple threads.
unsafe {
minicov::capture_coverage(&mut *coverage).unwrap();
}
/// A hook that is invoked when the system exits to dump the code coverage data.
pub(crate) fn on_system_exit() {
static COVERAGE_DATA: Once<Vec<u8>> = Once::new();
let coverage = COVERAGE_DATA.call_once(|| {
let mut coverage = Vec::new();
// SAFETY: `call_once` guarantees that this function will not be called concurrently by
// multiple threads.
unsafe { minicov::capture_coverage(&mut coverage).unwrap() };
coverage
});
crate::early_println!("#### Coverage: {:p} {}", coverage.as_ptr(), coverage.len());
}

View File

@ -38,6 +38,7 @@ pub mod irq;
pub mod logger;
pub mod mm;
pub mod panic;
pub mod power;
pub mod prelude;
pub mod smp;
pub mod sync;

View File

@ -2,12 +2,7 @@
//! Panic support.
use core::sync::atomic::Ordering;
use crate::{
arch::qemu::{exit_qemu, QemuExitCode},
early_println,
};
use crate::early_println;
extern crate cfg_if;
extern crate gimli;
@ -38,18 +33,14 @@ pub fn __ostd_panic_handler(info: &core::panic::PanicInfo) -> ! {
abort();
}
/// Aborts the QEMU
/// Aborts the system.
///
/// This function will first attempt to power off the system. If that fails, it will halt all CPUs.
pub fn abort() -> ! {
use crate::{arch::irq::disable_local_and_halt, cpu::CpuSet, smp::inter_processor_call};
exit_qemu(QemuExitCode::Failed);
// TODO: `inter_processor_call` may panic again (e.g., if IPIs have not been initialized or if
// there is an out-of-memory error). We should find a way to make it panic-free.
if !crate::IN_BOOTSTRAP_CONTEXT.load(Ordering::Relaxed) {
inter_processor_call(&CpuSet::new_full(), || disable_local_and_halt());
}
disable_local_and_halt();
// TODO: The main purpose of powering off here is to allow QEMU to exit. Otherwise, the CI may
// freeze after panicking. However, this is unnecessary and may prevent debugging on a real
// machine (i.e., the message will disappear afterward).
crate::power::poweroff(crate::power::ExitCode::Failure);
}
#[cfg(not(target_arch = "loongarch64"))]

96
ostd/src/power.rs Normal file
View File

@ -0,0 +1,96 @@
// SPDX-License-Identifier: MPL-2.0
//! Power management.
use core::sync::atomic::Ordering;
use spin::Once;
use crate::{arch::irq::disable_local_and_halt, cpu::CpuSet, smp::inter_processor_call};
/// An exit code that denotes the reason for restarting or powering off.
///
/// Whether or not the code is used depends on the hardware. In a virtualization environment, it
/// can be passed to the hypervisor (e.g., as QEMU's exit code). In a bare-metal environment, it
/// can be passed to the firmware. In either case, the code may be silently ignored if reporting
/// the code is not supported.
pub enum ExitCode {
/// The code that indicates a successful exit.
Success,
/// The code that indicates a failed exit.
Failure,
}
static RESTART_HANDLER: Once<fn(ExitCode)> = Once::new();
/// Injects a handler that can restart the system.
///
/// The function may be called only once; subsequent calls take no effect.
///
/// Note that, depending on the specific architecture, OSTD may already have a built-in handler. If
/// so, calling this function outside of OSTD will never take effect. Currently, it happens in
/// - x86_64: Never;
/// - riscv64: Always;
/// - loongarch64: Never.
pub fn inject_restart_handler(handler: fn(ExitCode)) {
RESTART_HANDLER.call_once(|| handler);
}
/// Restarts the system.
///
/// This function will not return. If a restart handler is missing or not working, it will halt all
/// CPUs on the machine.
pub fn restart(code: ExitCode) -> ! {
if let Some(handler) = RESTART_HANDLER.get() {
(handler)(code);
log::error!("Failed to restart the system because the restart handler fails");
} else {
log::error!("Failed to restart the system because a restart handler is missing");
}
machine_halt();
}
static POWEROFF_HANDLER: Once<fn(ExitCode)> = Once::new();
/// Injects a handler that can power off the system.
///
/// The function may be called only once; subsequent calls take no effect.
///
/// Note that, depending on the specific architecture, OSTD may already have a built-in handler. If
/// so, calling this function outside of OSTD will never take effect. Currently, it happens in
/// - x86_64: If a QEMU hypervisor is detected;
/// - riscv64: Always;
/// - loongarch64: Never.
pub fn inject_poweroff_handler(handler: fn(ExitCode)) {
POWEROFF_HANDLER.call_once(|| handler);
}
/// Powers off the system.
///
/// This function will not return. If a poweroff handler is missing or not working, it will halt
/// all CPUs on the machine.
pub fn poweroff(code: ExitCode) -> ! {
#[cfg(feature = "coverage")]
crate::coverage::on_system_exit();
if let Some(handler) = POWEROFF_HANDLER.get() {
(handler)(code);
log::error!("Failed to power off the system because the poweroff handler fails");
} else {
log::error!("Failed to power off the system because a poweroff handler is missing");
}
machine_halt();
}
fn machine_halt() -> ! {
log::error!("Halting the machine...");
// TODO: `inter_processor_call` may panic again (e.g., if IPIs have not been initialized or if
// there is an out-of-memory error). We should find a way to make it panic-free.
if !crate::IN_BOOTSTRAP_CONTEXT.load(Ordering::Relaxed) {
inter_processor_call(&CpuSet::new_full(), || disable_local_and_halt());
}
disable_local_and_halt();
}