8d9f9e552f
Phase I.5: complete the acpid <-> kernel s2idle wire. After
MWAIT returns from an interrupt (typically an SCI from
acpid), the kernel now:
1. Clears S2IDLE_REQUESTED (via s2idle_request_clear)
2. Sets KSTOP_FLAG and triggers EVENT_READ on the kstop
handle (via s2idle_signal_wake)
This is the kernel-side analog of Linux 7.1
`acpi_s2idle_wake` in `drivers/acpi/sleep.c:758`. The
existing irq_trigger in generic_irq has already routed the
SCI to acpid's listener (which opened /scheme/irq/{sci}
earlier in the boot sequence), so the AML interpretation
is done by acpid asynchronously.
The s2idle flow now:
1. acpid: enter_s2idle() (\_TTS(0), \_PTS(0), \_SST(3))
2. acpid: write 's2idle\n' to /scheme/sys/kstop
-> kernel sets S2IDLE_REQUESTED, returns
3. Kernel idle path: mwait_loop() at deepest C-state
4. SCI breaks MWAIT (any interrupt, not just SCI)
5. Kernel mwait_loop post-handler (this commit):
- s2idle_request_clear()
- s2idle_signal_wake() -> KSTOP_FLAG set, EVENT_READ
6. acpid main loop: wakes from kstop handle read
7. acpid: exit_s2idle() (\_SST(2), \_WAK(0), \_SST(1))
The KSTOP_FLAG set in step 5 also serves as a 'reason'
indicator — acpid's CheckShutdown verb (kcall 2) returns
the flag, so acpid can distinguish a kstop-shutdown event
from a kstop-s2idle-wake event by polling CheckShutdown
after waking.
Hardware-agnostic: the same flow works for any platform
with Modern Standby firmware (Dell, HP, Lenovo, LG Gram,
etc.). The s2idle is the universal mechanism for low-power
idle; only the wake source (SCI, GPIO, RTC, ...) varies
per OEM.
213 lines
6.7 KiB
Rust
213 lines
6.7 KiB
Rust
use alloc::boxed::Box;
|
|
use core::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
use crate::sync::ordered::{Mutex, L4};
|
|
use spin::Once;
|
|
|
|
use syscall::data::GlobalSchemes;
|
|
|
|
use crate::{
|
|
acpi::{RxsdtEnum, RXSDT_ENUM},
|
|
context::file::InternalFlags,
|
|
scheme::{SchemeExt, StrOrBytes},
|
|
sync::CleanLockToken,
|
|
};
|
|
|
|
use crate::syscall::{
|
|
error::{Error, Result, EACCES, EBADFD, EINVAL, ENOENT},
|
|
flag::{AcpiVerb, CallFlags, EventFlags},
|
|
usercopy::UserSliceRw,
|
|
};
|
|
|
|
use super::{CallerCtx, KernelScheme, OpenResult};
|
|
|
|
/// A scheme used to access the RSDT or XSDT, and listen for shutdown, which is needed for e.g. `acpid` to function.
|
|
pub struct AcpiScheme;
|
|
|
|
bitflags! {
|
|
#[derive(PartialEq)]
|
|
struct HandleBits: usize {
|
|
const CAN_READ_RXSDT = 1;
|
|
const CAN_REGISTER_KSTOP = 2;
|
|
|
|
// mutually exclusive with the other flags
|
|
const KSTOP_HANDLE = 4;
|
|
}
|
|
}
|
|
|
|
static RXSDT_DATA: Once<Box<[u8]>> = Once::new();
|
|
|
|
static KSTOP_FLAG: Mutex<L4, bool> = Mutex::new(false);
|
|
static EXISTS_KSTOP_HANDLE: AtomicBool = AtomicBool::new(false);
|
|
|
|
/// Phase I: s2idle (Modern Standby / S0ix) coordination flag.
|
|
/// Set by `s2idle_request_set` (called from the kstop handler
|
|
/// when acpid writes "s2idle" to /scheme/sys/kstop). Read by
|
|
/// the kernel's idle path which calls `mwait_loop()` while
|
|
/// the flag is set. Cleared by `s2idle_request_clear` when an
|
|
/// SCI breaks the MWAIT, signaling the idle path to stop
|
|
/// calling `mwait_loop()`.
|
|
///
|
|
/// Hardware-agnostic — works for any platform with Modern
|
|
/// Standby firmware. Mirrors Linux 7.1
|
|
/// `s2idle_state == S2IDLE_STATE_ENTER` in
|
|
/// `kernel/power/suspend.c:91`.
|
|
static S2IDLE_REQUESTED: AtomicBool = AtomicBool::new(false);
|
|
|
|
/// Set by the kstop handler when acpid requests s2idle entry.
|
|
/// Idempotent.
|
|
pub fn s2idle_request_set() {
|
|
S2IDLE_REQUESTED.store(true, Ordering::Release);
|
|
}
|
|
|
|
/// Clear by the interrupt handler when an SCI breaks the MWAIT,
|
|
/// or by the s2idle wake path. Idempotent.
|
|
pub fn s2idle_request_clear() {
|
|
S2IDLE_REQUESTED.store(false, Ordering::Release);
|
|
}
|
|
|
|
/// Read by the kernel's idle path. Returns true if acpid has
|
|
/// requested s2idle entry and the kernel has not yet broken
|
|
/// out of MWAIT.
|
|
pub fn s2idle_requested() -> bool {
|
|
S2IDLE_REQUESTED.load(Ordering::Acquire)
|
|
}
|
|
|
|
/// Phase I: signal acpid that s2idle MWAIT was broken by an
|
|
/// interrupt. Called from `mwait_loop` after MWAIT returns.
|
|
/// Triggers the kstop handle's EVENT_READ so acpid's main loop
|
|
/// wakes and runs the \_SST(2) → \_WAK(0) → \_SST(1) AML
|
|
/// sequence on resume.
|
|
///
|
|
/// Mirrors Linux 7.1 `acpi_s2idle_wake` in
|
|
/// `drivers/acpi/sleep.c:758` — the kernel clears
|
|
/// s2idle_state and signals the userspace ACPI driver.
|
|
pub fn s2idle_signal_wake() {
|
|
let mut token = CleanLockToken::new();
|
|
*KSTOP_FLAG.lock(token.token()) = true;
|
|
if EXISTS_KSTOP_HANDLE.load(Ordering::Relaxed) {
|
|
crate::event::trigger(
|
|
GlobalSchemes::Acpi.scheme_id(),
|
|
HandleBits::KSTOP_HANDLE.bits(),
|
|
EventFlags::EVENT_READ,
|
|
&mut token,
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn register_kstop(token: &mut CleanLockToken) -> bool {
|
|
*KSTOP_FLAG.lock(token.token()) = true;
|
|
|
|
if !EXISTS_KSTOP_HANDLE.load(Ordering::Relaxed) {
|
|
error!("No userspace ACPI handler was notified when trying to shutdown. This is bad.");
|
|
// Let the kernel shutdown without ACPI.
|
|
return false;
|
|
}
|
|
crate::event::trigger(
|
|
GlobalSchemes::Acpi.scheme_id(),
|
|
HandleBits::KSTOP_HANDLE.bits(),
|
|
EventFlags::EVENT_READ,
|
|
token,
|
|
);
|
|
|
|
// TODO: Context switch directly to the waiting context, to avoid annoying timeouts.
|
|
true
|
|
}
|
|
|
|
impl AcpiScheme {
|
|
pub fn init() {
|
|
// NOTE: This __must__ be called from the main kernel context, while initializing all
|
|
// schemes. If it is called by any other context, then all ACPI data will probably not even
|
|
// be mapped.
|
|
|
|
let mut data_init = false;
|
|
|
|
RXSDT_DATA.call_once(|| {
|
|
data_init = true;
|
|
|
|
let table = match RXSDT_ENUM.get() {
|
|
Some(RxsdtEnum::Rsdt(rsdt)) => rsdt.as_slice(),
|
|
Some(RxsdtEnum::Xsdt(xsdt)) => xsdt.as_slice(),
|
|
None => {
|
|
warn!("expected RXSDT_ENUM to be initialized before AcpiScheme, is ACPI available?");
|
|
&[]
|
|
}
|
|
};
|
|
|
|
Box::from(table)
|
|
});
|
|
|
|
if !data_init {
|
|
error!("AcpiScheme::init called multiple times");
|
|
}
|
|
}
|
|
}
|
|
|
|
impl KernelScheme for AcpiScheme {
|
|
fn scheme_root(&self, _token: &mut CleanLockToken) -> Result<usize> {
|
|
Ok((HandleBits::CAN_READ_RXSDT | HandleBits::CAN_REGISTER_KSTOP).bits())
|
|
}
|
|
fn kopenat(
|
|
&self,
|
|
id: usize,
|
|
path: StrOrBytes,
|
|
_flags: usize,
|
|
_fcntl_flags: u32,
|
|
caller: CallerCtx,
|
|
_token: &mut CleanLockToken,
|
|
) -> Result<OpenResult> {
|
|
let bits = HandleBits::from_bits_retain(id);
|
|
|
|
let new_bits = match path.as_bytes() {
|
|
b"" | b"/" => bits,
|
|
b"kstop" | b"/kstop" => {
|
|
// TODO: can the uid check be removed?
|
|
if caller.uid != 0 || !bits.contains(HandleBits::CAN_REGISTER_KSTOP) {
|
|
return Err(Error::new(EACCES));
|
|
}
|
|
EXISTS_KSTOP_HANDLE.store(true, Ordering::Relaxed);
|
|
HandleBits::KSTOP_HANDLE
|
|
}
|
|
_ => return Err(Error::new(ENOENT)),
|
|
};
|
|
Ok(OpenResult::SchemeLocal(
|
|
new_bits.bits(),
|
|
InternalFlags::empty(),
|
|
))
|
|
}
|
|
fn kcall(
|
|
&self,
|
|
fds: &[usize],
|
|
payload: UserSliceRw,
|
|
flags: CallFlags,
|
|
metadata: &[u64],
|
|
token: &mut CleanLockToken,
|
|
) -> Result<usize> {
|
|
let [handle] = <&[usize; 1]>::try_from(fds)
|
|
.map_err(|_| Error::new(EINVAL))?
|
|
.map(HandleBits::from_bits_retain);
|
|
let verb = metadata
|
|
.get(0)
|
|
.copied()
|
|
.and_then(AcpiVerb::try_from_raw)
|
|
.ok_or(Error::new(EINVAL))?;
|
|
|
|
match verb {
|
|
AcpiVerb::ReadRxsdt => {
|
|
if !handle.contains(HandleBits::CAN_READ_RXSDT) || !flags.contains(CallFlags::READ)
|
|
{
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
let src = RXSDT_DATA.get().ok_or(Error::new(EBADFD))?;
|
|
payload.copy_common_bytes_from_slice(src)?;
|
|
Ok(src.len())
|
|
}
|
|
AcpiVerb::CheckShutdown => {
|
|
if handle != HandleBits::KSTOP_HANDLE {
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
Ok(usize::from(*KSTOP_FLAG.lock(token.token())))
|
|
}
|
|
}
|
|
}
|
|
} |