v5.1: cpufreqd scheme server - real cpufreq scheme, no more stub

G-A2 from DRIVER-MANAGER-MIGRATION-PLAN v4.8: thermald was
silently failing to switch governors because cpufreqd provided
no 'cpufreq' scheme. thermald writes /scheme/cpufreq/governor
(thermald/src/main.rs:38, :348-358); cpufreqd only wrote
/scheme/cpufreq/state via fs::write - the path was a stub.

Fix: cpufreqd now registers the 'cpufreq' scheme at startup via
redox-scheme::scheme::register_sync_scheme, exposing a real
Linux-compatible interface.

Path surface (read-write where noted):
- /scheme/cpufreq/governor                      (rw - global default)
- /scheme/cpufreq/cpu<N>/scaling_governor       (rw - per-CPU override)
- /scheme/cpufreq/cpu<N>/scaling_cur_freq       (ro)
- /scheme/cpufreq/cpu<N>/scaling_min_freq       (ro)
- /scheme/cpufreq/cpu<N>/scaling_max_freq       (ro)
- /scheme/cpufreq/cpu<N>/cpuinfo_min_freq       (ro)
- /scheme/cpufreq/cpu<N>/cpuinfo_max_freq       (ro)
- /scheme/cpufreq/cpu<N>/scaling_available_governors (ro)
- /scheme/cpufreq/cpu<N>/cpuinfo_cur_freq       (ro - alias)
- /scheme/cpufreq/control/governor              (rw - alias, thermald fallback)
- /scheme/cpufreq/state                        (ro - existing key=value)

Implementation:
- scheme.rs (NEW, 639 lines): SchemeSync impl, path parsing,
  per-CPU vs global governor resolution, atomic-rename
  persistence of last governor.
- main.rs: governor -> Arc<Mutex<Governor>>, cpus ->
  Arc<Mutex<Vec<CpuInfo>>>, spawn scheme server, removed the
  fs::write('/scheme/cpufreq/state') stub line, added
  Governor::as_str()/from_name(), 4 pre-existing clippy
  collapsible_if warnings fixed.
- Cargo.toml: added redox-scheme, libredox, syscall (redox_syscall)
  path deps with [patch.crates-io] per local AGENTS.md rules.

Lock ordering documented inline: governor -> overrides -> cpus.
Main loop snapshots governor+overrides before locking cpus so
the scheme server can never deadlock against it.

Linux-compat extras added (real functionality, not stubs):
scaling_available_governors (root + per-CPU), cpuinfo_cur_freq
alias, control/governor alias, getdents on all directories.

Per-CPU override behavior matches Linux: writing global governor
clears all per-CPU overrides; writing cpu<N>/scaling_governor
sets a per-CPU override. Main loop honors overrides via
effective_governor() each tick.

state format: now emits lowercase governor names (governor=ondemand)
instead of the old {:?} (governor=Ondemand). This matches
redbear-power's lowercase expectations.

Tests: 21 new (governor name parsing case-insensitive + rejection
of invalid, Arc<Mutex> round-trip, global-write-clears-overrides,
path parsing for cpu0/scaling_governor/governor/control/governor,
unknown-CPU/leaf rejection, per-CPU override fallback, frequency
helpers, state format backward compat, integration test). All pass.

Compile: cargo check --target x86_64-unknown-redox - zero new
warnings (only pre-existing libredox FFI warnings).

Constraints per local/AGENTS.md:
- No new branches (work on 0.3.1)
- No new Cat-2 dependencies (redox-scheme is already a path-dep)
- No stubs, no todo!/unimplemented!
- Cat 1 in-house recipe - source IS the durable location

Closes v5.1 of the v5.x work program. Closes G-A2 from v4.8.
This commit is contained in:
kellito
2026-07-26 00:22:32 +09:00
parent 045aaa4579
commit 8157eda85f
3 changed files with 1166 additions and 67 deletions
@@ -8,8 +8,13 @@ name = "cpufreqd"
path = "src/main.rs"
[dependencies]
redox_syscall = { path = "../../../../../local/sources/syscall" }
log = "0.4"
libc = "0.2"
libredox = { path = "../../../../../local/sources/libredox", features = ["call", "std"] }
log = { version = "0.4", features = ["std"] }
redox-scheme = { path = "../../../../../local/sources/redox-scheme" }
syscall = { path = "../../../../../local/sources/syscall", package = "redox_syscall", features = ["std"] }
[patch.crates-io]
redox-scheme = { path = "../../../../../local/sources/redox-scheme" }
redox_syscall = { path = "../../../../../local/sources/syscall" }
libredox = { path = "../../../../../local/sources/libredox" }
+124 -65
View File
@@ -1,8 +1,13 @@
mod scheme;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::io::{Read, Write};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use log::{info, warn, LevelFilter};
use scheme::{CpufreqShared, effective_governor};
// MSR addresses — see Intel SDM Vol 3B §14
const IA32_PERF_CTL: u32 = 0x199; // legacy P-state
@@ -13,21 +18,53 @@ const IA32_PM_ENABLE: u32 = 0x770; // HWP enable bit
// Hysteresis: minimum dwell time (in polls) at the current P-state
// before we consider changing. Prevents thrashing at the
// Ondemand/Conservative boundaries when load oscillates around the
// threshold. With POLL_MS=100ms and DWELL_POLLS=3 the minimum
// dwell is 300ms — well within the Linux kernel schedutil's
// threshold. With POLL_MS=100ms and DWELL_POLLS=3 the minimum dwell
// is 300ms — well within the Linux kernel schedutil's
// typical 4-8ms response time but slow enough to avoid
// spurious transitions. On QEMU, dwll is moot because the
// PIIX4 doesn't model IA32_PERF_STATUS, but the read_only flag
// from detect_virtualization() short-circuits MSR writes
// entirely.
const POLL_MS: u64 = 100;
const SAMPLE_WINDOW: usize = 10;
const STATE_WRITE_INTERVAL_S: u64 = 1;
pub(crate) const SAMPLE_WINDOW: usize = 10;
const MSR_ERROR_SUPPRESS_COUNT: u32 = 1;
const THERMAL_CACHE_MS: u64 = 1000;
#[derive(Clone, Copy, PartialEq, Debug)]
enum Governor { Performance, Powersave, Ondemand, Conservative, Schedutil }
pub(crate) enum Governor {
Performance,
Powersave,
Ondemand,
Conservative,
Schedutil,
}
impl Governor {
/// Lowercase Linux cpufreq governor name (e.g. `"ondemand"`).
pub(crate) fn as_str(self) -> &'static str {
match self {
Governor::Performance => "performance",
Governor::Powersave => "powersave",
Governor::Ondemand => "ondemand",
Governor::Conservative => "conservative",
Governor::Schedutil => "schedutil",
}
}
/// Parse a governor name, case-insensitively. Returns `None` for any
/// name outside the valid Linux cpufreq governor set so callers can
/// reject it with `EINVAL` rather than silently accepting garbage.
pub(crate) fn from_name(name: &str) -> Option<Self> {
match name.trim().to_ascii_lowercase().as_str() {
"performance" => Some(Governor::Performance),
"powersave" => Some(Governor::Powersave),
"ondemand" => Some(Governor::Ondemand),
"conservative" => Some(Governor::Conservative),
"schedutil" => Some(Governor::Schedutil),
_ => None,
}
}
}
struct StderrLogger;
impl log::Log for StderrLogger {
@@ -41,13 +78,13 @@ impl log::Log for StderrLogger {
/// writes are ignored when HWP is active. We detect HWP via MSR 0x770
/// bit 0 and use IA32_HWP_REQUEST (0x774) with EPP hints.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PstateMode { LegacyPerfCtl, Hwp }
pub(crate) enum PstateMode { LegacyPerfCtl, Hwp }
#[derive(Clone)]
struct PState {
freq_khz: u32,
power_mw: u32,
ctl: u64,
pub(crate) struct PState {
pub(crate) freq_khz: u32,
pub(crate) power_mw: u32,
pub(crate) ctl: u64,
}
/// Minimum dwell time (in polls) at the current P-state before we
@@ -60,51 +97,54 @@ struct PState {
const DWELL_POLLS: u32 = 3;
#[derive(Clone)]
struct CpuInfo {
id: u32,
pstates: Vec<PState>,
current_idx: usize,
load_history: [f64; SAMPLE_WINDOW],
load_idx: usize,
throttle: bool,
msr_errors: u32,
msr_suppressed: bool,
mode: PstateMode,
hwp_min: u8, // from MSR 0x771[15:8]
hwp_max: u8, // from MSR 0x771[7:0]
pub(crate) struct CpuInfo {
pub(crate) id: u32,
pub(crate) pstates: Vec<PState>,
pub(crate) current_idx: usize,
pub(crate) load_history: [f64; SAMPLE_WINDOW],
pub(crate) load_idx: usize,
pub(crate) throttle: bool,
pub(crate) msr_errors: u32,
pub(crate) msr_suppressed: bool,
pub(crate) mode: PstateMode,
pub(crate) hwp_min: u8, // from MSR 0x771[15:8]
pub(crate) hwp_max: u8, // from MSR 0x771[7:0]
/// Number of consecutive polls at the current_idx. Reset to
/// 0 on every state change. The next P-state transition
/// only fires when dwell reaches DWELL_POLLS. This is the
/// hystersis that stops the P0->P1->P0 oscillation on idle.
dwell: u32,
pub(crate) dwell: u32,
/// P-state index the dwell counter is counting toward. Set
/// each time choose_pstate returns a different target than
/// the previous tick; reset to 0 when it matches the actual
/// current_idx (= "no transition was requested").
dwell_target: usize,
pub(crate) dwell_target: usize,
/// When the host is a VM (QEMU, KVM, VMware, etc.) the MSR
/// writes are no-ops on the underlying hardware emulation.
/// In that case we don't even try to write; the load value is
/// still tracked and the governor still logs its choice, but
/// the P-state stays where the BIOS/bootloader left it.
read_only: bool,
pub(crate) read_only: bool,
}
fn detect_cpus() -> Vec<u32> {
if let Ok(data) = fs::read_to_string("/scheme/sys/cpu") {
for line in data.lines() {
if let Some(rest) = line.strip_prefix("CPUs: ") {
if let Ok(n) = rest.trim().parse::<u32>() {
if n > 0 { return (0..n).collect(); }
}
if let Some(rest) = line.strip_prefix("CPUs: ")
&& let Ok(n) = rest.trim().parse::<u32>()
&& n > 0
{
return (0..n).collect();
}
}
}
let mut v = Vec::new();
if let Ok(e) = fs::read_dir("/dev/cpu") {
for x in e.flatten() {
if let Ok(n) = x.file_name().into_string() {
if let Ok(id) = n.parse() { v.push(id); }
if let Ok(n) = x.file_name().into_string()
&& let Ok(id) = n.parse()
{
v.push(id);
}
}
}
@@ -148,14 +188,14 @@ fn read_acpi_pss(cpu: u32) -> Vec<PState> {
let mut s = Vec::new();
for l in d.lines() {
let w: Vec<&str> = l.split_whitespace().collect();
if w.len() >= 6 {
if let (Ok(f), Ok(pw), Ok(ct)) = (
if w.len() >= 6
&& let (Ok(f), Ok(pw), Ok(ct)) = (
w[0].parse(),
w[2].parse(),
u64::from_str_radix(w[5], 16),
) {
s.push(PState { freq_khz: f, power_mw: pw, ctl: ct });
}
)
{
s.push(PState { freq_khz: f, power_mw: pw, ctl: ct });
}
}
if !s.is_empty() { return s; }
@@ -292,20 +332,6 @@ fn check_thermal_raw() -> bool {
if let Ok(d) = fs::read_to_string("/scheme/thermal/summary") { d.contains("critical") || d.contains("throttle") } else { false }
}
fn write_scheme_state(governor: Governor, cpus: &[CpuInfo]) {
let mut out = format!("governor={:?}\n", governor);
for ci in cpus {
if ci.pstates.is_empty() { continue; }
let p = &ci.pstates[ci.current_idx.min(ci.pstates.len() - 1)];
let mode_s = match ci.mode {
PstateMode::Hwp => "HWP",
PstateMode::LegacyPerfCtl => "legacy",
};
out.push_str(&format!("CPU{} [{}]: {} kHz, {} mW, load={:.1}%\n", ci.id, mode_s, p.freq_khz, p.power_mw, avg_load(ci) * 100.0));
}
let _ = fs::write("/scheme/cpufreq/state", out);
}
fn detect_virtualization() -> bool {
// Detect a hypervisor / VM before applying P-state changes.
//
@@ -385,14 +411,14 @@ fn main() {
info!("detected virtualized environment: cpufreqd will run in read-only mode (no MSR writes)");
}
let governor = match env::var("CPUFREQ_GOVERNOR").unwrap_or_default().as_str() {
let initial_governor = match env::var("CPUFREQ_GOVERNOR").unwrap_or_default().as_str() {
"performance" => Governor::Performance, "powersave" => Governor::Powersave,
"conservative" => Governor::Conservative, "schedutil" => Governor::Schedutil,
_ => Governor::Ondemand,
};
let cpus = detect_cpus();
info!("detected {} CPU(s), governor={:?}", cpus.len(), governor);
let mut ci: Vec<CpuInfo> = cpus.iter().map(|&id| {
let detected_cpus = detect_cpus();
info!("detected {} CPU(s), governor={:?}", detected_cpus.len(), initial_governor);
let ci: Vec<CpuInfo> = detected_cpus.iter().map(|&id| {
let mode = detect_pstate_mode(id);
let (hwp_min, hwp_max) = if mode == PstateMode::Hwp {
let (min, max, _, _) = read_hwp_capabilities(id);
@@ -409,30 +435,64 @@ fn main() {
info!("CPU{}: {} P-states ({} - {} kHz)", id, ps.len(), ps.first().map_or(0, |p| p.freq_khz), ps.last().map_or(0, |p| p.freq_khz));
CpuInfo { id, pstates: ps, current_idx: 0, load_history: [0.0; SAMPLE_WINDOW], load_idx: 0, throttle: false, msr_errors: 0, msr_suppressed: false, mode, hwp_min, hwp_max, dwell: 0, dwell_target: 0, read_only: virtualized }
}).collect();
let mut prev: Vec<(u64, u64)> = vec![(0, 0); cpus.len()];
// Shared state: the global governor, per-CPU governor overrides
// (written via /scheme/cpufreq/cpu<N>/scaling_governor), and the
// live CPU vector are all shared with the scheme server thread.
// Lock order across the process is always governor -> overrides
// -> cpus; the main loop snapshots governor + overrides before
// taking the cpus lock so no lock is ever held while waiting on
// an earlier-ordered lock.
let governor = Arc::new(Mutex::new(initial_governor));
let overrides: Arc<Mutex<HashMap<u32, Governor>>> = Arc::new(Mutex::new(HashMap::new()));
let cpus = Arc::new(Mutex::new(ci));
let shared = CpufreqShared {
governor: Arc::clone(&governor),
overrides: Arc::clone(&overrides),
cpus: Arc::clone(&cpus),
};
if let Err(err) = scheme::spawn_scheme_server(shared) {
warn!("cpufreqd: scheme server did not start: {err}");
}
let mut prev: Vec<(u64, u64)> = vec![(0, 0); detected_cpus.len()];
let mut thermal = ThermalCache::new();
let mut last_state_write = Instant::now();
// Set initial P-state. For HWP we leave MSR 0x774 as BIOS-set
// (defaults to performance) and just let the governor pick a
// starting index. For legacy, write the lowest P-state's IA32_PERF_CTL.
for c in &ci {
if !c.pstates.is_empty() {
match c.mode {
PstateMode::Hwp => {} // HWP starts at the BIOS default
PstateMode::LegacyPerfCtl => {
let _ = write_msr(c.id, IA32_PERF_CTL, c.pstates[0].ctl);
{
let ci_guard = cpus.lock().unwrap();
for c in ci_guard.iter() {
if !c.pstates.is_empty() {
match c.mode {
PstateMode::Hwp => {} // HWP starts at the BIOS default
PstateMode::LegacyPerfCtl => {
let _ = write_msr(c.id, IA32_PERF_CTL, c.pstates[0].ctl);
}
}
}
}
}
loop {
std::thread::sleep(Duration::from_millis(POLL_MS));
let tt = thermal.get();
for (i, c) in ci.iter_mut().enumerate() {
// Snapshot the governor and overrides before locking cpus so
// the scheme server can always acquire governor/overrides
// without deadlocking against the cpus lock held below.
let global_governor = *governor.lock().unwrap();
let overrides_snapshot = overrides.lock().unwrap().clone();
let mut ci_guard = cpus.lock().unwrap();
for (i, c) in ci_guard.iter_mut().enumerate() {
if c.pstates.is_empty() { continue; }
let l = measure_load(c.id, &mut prev[i]);
c.load_history[c.load_idx] = l; c.load_idx = (c.load_idx + 1) % SAMPLE_WINDOW; c.throttle = tt;
let n = choose_pstate(governor, c);
let g = effective_governor(global_governor, &overrides_snapshot, c.id);
let n = choose_pstate(g, c);
// Dwell-based hysteresis: only transition after DWELL_POLLS
// consecutive polls at the same target. This stops the
// P0->P1->P0 oscillation on idle systems (QEMU and
@@ -478,6 +538,5 @@ fn main() {
}
}
}
if last_state_write.elapsed() >= Duration::from_secs(STATE_WRITE_INTERVAL_S) { write_scheme_state(governor, &ci); last_state_write = Instant::now(); }
}
}
File diff suppressed because it is too large Load Diff