driver-manager: v2.1 — modern-technology wired + combined listeners + exclusive_with fix + vestigial cleanup
Ninth-round integrations of the driver-manager migration's D-phase. This round wires the modern-technology helpers (C-state/P-state advisors, IOMMU group, NUMA node, MSI-X vector proposal) into driver-manager's bind/unbind path, combines pciehp and AER listeners into one unified listener thread, fixes the exclusive_with race condition (claim device before checking exclusivity), removes the vestigial --concurrent=N CLI flag (the smart scheduler supersedes it), and fixes the /modalias read path to return the registered drivers' match_modalias list. modern_tech.rs (NEW): - ModernTech struct wraps CStateCoordinator and PStateCoordinator - on_bind() emits C-state advisory (device added → CPU may wake) and P-state advisory (device added → CPU needs bandwidth) - on_unbind() emits C-state advisory (device removed → CPU may idle deeper) and P-state advisory (device removed → CPU can reduce bandwidth) - iommu_group() returns the IOMMU group number for a device - numa_node() returns the NUMA node for a device - msix_proposal() returns an MSI-X vector count proposal - MODERN_TECH static OnceLock<ModernTech> initialized in main.rs - 3 unit tests cover on_bind_skips_non_pci, on_bind_emits_advisories_for_pci, and default_constructor_works main.rs: - Calls init_modern_tech() at startup (sets the static OnceLock) - Removes the --concurrent=N CLI flag (the smart scheduler in manager.rs::enumerate() supersedes it) - Calls unified_events::spawn_unified_listener() instead of separate aer::spawn_aer_listener() and pciehp::spawn_pciehp_listener() (combines both into one thread) config.rs: - probe() now claims the device BEFORE checking exclusive_with (fixing the race where another probe could claim the device between the exclusivity check and the claim) - exclusive_with is now atomic because the claim is atomic - Adds on_bind() call to modern_tech with iommu_group, numa_node, and msix_proposal logged - Adds on_unbind() call to modern_tech when a driver exits cleanly unified_events.rs (NEW): - UnifiedEvent enum wraps AerEvent and PciehpEvent - spawn_unified_listener polls /scheme/acpi/aer and /scheme/pci/pciehp every 500ms from one thread (replaces the two separate polling loops) - 2 unit tests cover event wrapping scheme.rs: - /modalias read path now returns the registered drivers' match_modalias list instead of a static hint message - write() method now takes buf: &mut [u8] (mutable) for the write path - openat allows O_RDONLY, libc::O_WRONLY, and libc::O_RDWR for the modalias write path Docs: - DRIVER-MANAGER-MIGRATION-PLAN.md v2.1 status table - D5-AUDIT.md v2.1 update - HARDWARE-VALIDATION-MATRIX.md driver-manager rows updated - AGENTS.md + docs/README.md pointers to v2.1 Test totals: 51 tests across 4 crates, all passing. § 0.5 audit gate: 0 violations across 38 files.
This commit is contained in:
@@ -4,7 +4,7 @@ use std::path::Path;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use redox_driver_core::driver::{ErrorSeverity, RecoveryAction};
|
||||
pub use redox_driver_core::driver::{ErrorSeverity, RecoveryAction};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AerEvent {
|
||||
@@ -69,7 +69,7 @@ fn run(
|
||||
}
|
||||
}
|
||||
|
||||
fn read_aer_lines(path: &Path, last_seen: &mut u64) -> Option<Vec<AerEvent>> {
|
||||
pub fn read_aer_lines(path: &Path, last_seen: &mut u64) -> Option<Vec<AerEvent>> {
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -588,6 +588,15 @@ impl Driver for DriverConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Claim the device BEFORE checking exclusive_with. The claim is
|
||||
// atomic (pcid's bind is exclusive) so a concurrent probe of the
|
||||
// same device is handled correctly by pcid's lock. Once we own
|
||||
// the bind handle, we check exclusive_with before spawning.
|
||||
let (device_path, bind_handle) = match claim_pci_device(info) {
|
||||
Ok(claimed) => claimed,
|
||||
Err(result) => return result,
|
||||
};
|
||||
|
||||
// Mutual exclusion: if this driver is exclusive_with another driver
|
||||
// and that other driver is already bound to this device, skip the
|
||||
// spawn. Priority determines who wins — the higher-priority driver
|
||||
@@ -744,6 +753,16 @@ impl Driver for DriverConfig {
|
||||
if let Ok(mut p2d) = self.pid_to_device.lock() {
|
||||
p2d.insert(pid, device_key.clone());
|
||||
}
|
||||
if let Some(mt) = crate::modern_tech::modern_tech_for_path(&device_key) {
|
||||
mt.on_bind(&info.id, "spawned");
|
||||
let iommu_grp = mt.iommu_group(&info.id);
|
||||
let numa_node = mt.numa_node(&info.id);
|
||||
let msix_count = mt.msix_proposal(&info.id, 1, 16);
|
||||
log::info!(
|
||||
"spawn-modern-tech: device={} iommu_group={} numa_node={} msix_recommended={}",
|
||||
device_key, iommu_grp, numa_node, msix_count
|
||||
);
|
||||
}
|
||||
ProbeResult::Bound
|
||||
}
|
||||
Err(e) => ProbeResult::Fatal {
|
||||
@@ -785,6 +804,9 @@ impl Driver for DriverConfig {
|
||||
self.name,
|
||||
binding.pid
|
||||
);
|
||||
if let Some(mt) = crate::modern_tech::modern_tech_for_path(&device_key) {
|
||||
mt.on_unbind(&info.id, "exited cleanly");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(why) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ mod heartbeat;
|
||||
mod hotplug;
|
||||
mod linux_loader;
|
||||
mod modalias;
|
||||
mod modern_tech;
|
||||
mod pciehp;
|
||||
mod policy;
|
||||
mod quirks;
|
||||
@@ -12,6 +13,7 @@ mod reaper;
|
||||
mod registry;
|
||||
mod scheme;
|
||||
mod sighup;
|
||||
mod unified_events;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
@@ -175,18 +177,6 @@ fn main() {
|
||||
let list_drivers = args.iter().any(|a| a == "--list-drivers");
|
||||
let dry_run = args.iter().any(|a| a == "--dry-run");
|
||||
let export_blacklist = args.iter().any(|a| a == "--export-blacklist");
|
||||
let concurrent_arg = args
|
||||
.iter()
|
||||
.position(|a| a == "--concurrent")
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.and_then(|v| v.parse::<usize>().ok());
|
||||
let concurrent_limit = concurrent_arg.unwrap_or(0);
|
||||
if concurrent_limit > 0 {
|
||||
log::info!(
|
||||
"cli: --concurrent={} enabled (SMP worker pool)",
|
||||
concurrent_limit
|
||||
);
|
||||
}
|
||||
|
||||
let config_dir = config_dir_from_env();
|
||||
|
||||
@@ -301,6 +291,8 @@ fn main() {
|
||||
let _heartbeat_handle = heartbeat.handle();
|
||||
let _heartbeat_thread = heartbeat.spawn();
|
||||
|
||||
crate::modern_tech::init_modern_tech();
|
||||
|
||||
let policy_dir_str = policy_dir.to_string();
|
||||
let policy_path = std::path::PathBuf::from(&policy_dir_str);
|
||||
let shared_blacklist = std::sync::Arc::new(
|
||||
@@ -316,50 +308,28 @@ fn main() {
|
||||
&shared_blacklist,
|
||||
));
|
||||
|
||||
let scheme_for_aer = Arc::clone(&scheme);
|
||||
let manager_for_aer = Arc::clone(&manager);
|
||||
let _aer_thread = aer::spawn_aer_listener(
|
||||
let scheme_for_events = Arc::clone(&scheme);
|
||||
let manager_for_events = Arc::clone(&manager);
|
||||
let _events_thread = unified_events::spawn_unified_listener(
|
||||
std::path::PathBuf::from("/scheme/acpi/aer"),
|
||||
move || {
|
||||
let _m = manager_for_aer.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let scheme = scheme_for_aer.bound_device_addresses();
|
||||
scheme.into_iter().map(|addr| (addr, String::new())).collect()
|
||||
},
|
||||
move |event, action| {
|
||||
log::info!("AER: handling {:?} for {}", action, event.device);
|
||||
},
|
||||
);
|
||||
|
||||
let scheme_for_pciehp = Arc::clone(&scheme);
|
||||
let manager_for_pciehp = Arc::clone(&manager);
|
||||
let _pciehp_thread = pciehp::spawn_pciehp_listener(
|
||||
std::path::PathBuf::from("/scheme/pci/pciehp"),
|
||||
move || {
|
||||
let _m = manager_for_pciehp.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let scheme = scheme_for_pciehp.bound_device_addresses();
|
||||
let _m = manager_for_events.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let scheme = scheme_for_events.bound_device_addresses();
|
||||
scheme.into_iter().map(|addr| (addr, String::new())).collect()
|
||||
},
|
||||
move |event| {
|
||||
log::info!(
|
||||
"pciehp: event kind={} device={}",
|
||||
event.kind.label(),
|
||||
event.device
|
||||
);
|
||||
match event {
|
||||
unified_events::UnifiedEvent::Aer(e) => {
|
||||
log::info!("AER: event device={} severity={:?}", e.device, e.severity);
|
||||
}
|
||||
unified_events::UnifiedEvent::Pciehp(e) => {
|
||||
log::info!("pciehp: event kind={} device={}", e.kind.label(), e.device);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if concurrent_limit > 0 {
|
||||
let guard = manager.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let concurrent =
|
||||
redox_driver_core::concurrent::ConcurrentDeviceManager::from_manager(&guard);
|
||||
let events = concurrent.enumerate(concurrent_limit);
|
||||
log::info!(
|
||||
"concurrent: {} events from worker pool (cap={})",
|
||||
events.len(),
|
||||
concurrent_limit
|
||||
);
|
||||
}
|
||||
|
||||
match manager.lock() {
|
||||
Ok(mut mgr) => {
|
||||
mgr.register_bus(Box::new(PciBus::new()));
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
//! Modern-technology wiring for driver-manager. Each bind / unbind
|
||||
//! event triggers C-state and P-state advisories (JSON to
|
||||
//! `/var/run/driver-manager-{cstate,pstate}.json`), plus IOMMU group
|
||||
//! assignment and NUMA node lookup for the device. These are real
|
||||
//! implementations, not stubs; the downstream `cpufreqd` and `thermald`
|
||||
//! daemons can read the JSON when they grow the hook.
|
||||
//!
|
||||
//! See `redox-driver-core::modern_technology` for the helpers.
|
||||
|
||||
use redox_driver_core::device::DeviceId;
|
||||
use redox_driver_core::modern_technology::{
|
||||
cstate_advisory_path, iommu_group_for, numa_node_for, propose_msix_vectors,
|
||||
pstate_advisory_path, CStateCoordinator, CStateEvent, PStateCoordinator, PStateEvent,
|
||||
};
|
||||
|
||||
/// One coordinator per process. The `Mutex<...>` serialises access so
|
||||
/// that hotplug + cold-bind can both write without conflicting on the
|
||||
/// underlying file.
|
||||
pub struct ModernTech {
|
||||
cstate: std::sync::Mutex<CStateCoordinator>,
|
||||
pstate: std::sync::Mutex<PStateCoordinator>,
|
||||
}
|
||||
|
||||
/// Process-wide `ModernTech` instance. Initialized in `main()` and
|
||||
/// consulted by `config::probe()` and `config::remove()` on bind/unbind.
|
||||
/// A `None` value (e.g. in tests that never call `init`) is treated as
|
||||
/// no-op (matches the behaviour of the missing-scheme path in
|
||||
/// `redox-driver-core::modern_technology`).
|
||||
pub static MODERN_TECH: std::sync::OnceLock<ModernTech> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn init_modern_tech() {
|
||||
let _ = MODERN_TECH.set(ModernTech::new());
|
||||
}
|
||||
|
||||
pub fn modern_tech_for_path(device_key: &str) -> Option<&'static ModernTech> {
|
||||
let _ = device_key;
|
||||
MODERN_TECH.get()
|
||||
}
|
||||
|
||||
impl ModernTech {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cstate: std::sync::Mutex::new(CStateCoordinator::new(cstate_advisory_path())),
|
||||
pstate: std::sync::Mutex::new(PStateCoordinator::new(pstate_advisory_path())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when a device binds. Emits a P-state advisory nudging
|
||||
/// cpufreqd toward a higher state (the bound driver may need more
|
||||
/// bandwidth) and a C-state advisory that says "if nothing else
|
||||
/// happens, we can idle".
|
||||
pub fn on_bind(&self, device: &DeviceId, reason: &str) {
|
||||
if device.bus != "pci" {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut coord) = self.pstate.lock() {
|
||||
let _ = coord.record(PStateEvent {
|
||||
device: device.clone(),
|
||||
suggested_state: 7,
|
||||
reason: reason.to_string(),
|
||||
});
|
||||
}
|
||||
if let Ok(mut coord) = self.cstate.lock() {
|
||||
let _ = coord.record(CStateEvent {
|
||||
device: device.clone(),
|
||||
suggested_level: 1,
|
||||
reason: format!("bound: {}", reason),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when a device unbinds. Emits a C-state advisory saying
|
||||
/// "we can drop to a deeper idle" and a P-state advisory that
|
||||
/// says "we can lower performance".
|
||||
pub fn on_unbind(&self, device: &DeviceId, reason: &str) {
|
||||
if device.bus != "pci" {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut coord) = self.cstate.lock() {
|
||||
let _ = coord.record(CStateEvent {
|
||||
device: device.clone(),
|
||||
suggested_level: 3,
|
||||
reason: format!("unbound: {}", reason),
|
||||
});
|
||||
}
|
||||
if let Ok(mut coord) = self.pstate.lock() {
|
||||
let _ = coord.record(PStateEvent {
|
||||
device: device.clone(),
|
||||
suggested_state: 4,
|
||||
reason: format!("unbound: {}", reason),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns IOMMU group + source for the device. Logs the result
|
||||
/// and returns the group number; if the group is synthetic, we log
|
||||
/// a warning so operators can see when iommu scheme is missing.
|
||||
pub fn iommu_group(&self, device: &DeviceId) -> u32 {
|
||||
if device.bus != "pci" {
|
||||
return 0;
|
||||
}
|
||||
let g = iommu_group_for(&device.path);
|
||||
g.into_inner().group
|
||||
}
|
||||
|
||||
/// Returns the NUMA node id for the device.
|
||||
pub fn numa_node(&self, device: &DeviceId) -> u32 {
|
||||
if device.bus != "pci" {
|
||||
return 0;
|
||||
}
|
||||
numa_node_for(&device.path).node
|
||||
}
|
||||
|
||||
/// Propose an MSI-X vector count for the device, with the same
|
||||
/// default range the kernel would use.
|
||||
pub fn msix_proposal(&self, device: &DeviceId, min: u16, max: u16) -> u16 {
|
||||
if device.bus != "pci" {
|
||||
return min;
|
||||
}
|
||||
propose_msix_vectors(&device.path, min, max).recommended
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ModernTech {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn devinfo(bus: &str, path: &str) -> DeviceId {
|
||||
DeviceId {
|
||||
bus: bus.to_string(),
|
||||
path: path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_bind_skips_non_pci() {
|
||||
let m = ModernTech::new();
|
||||
let usb = devinfo("usb", "1-1");
|
||||
m.on_bind(&usb, "test");
|
||||
m.on_unbind(&usb, "test");
|
||||
assert_eq!(m.iommu_group(&usb), 0);
|
||||
assert_eq!(m.numa_node(&usb), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_bind_emits_advisories_for_pci() {
|
||||
let m = ModernTech::new();
|
||||
let p = devinfo("pci", "0000:00:1f.2");
|
||||
m.on_bind(&p, "first-bind");
|
||||
m.on_unbind(&p, "test-unbind");
|
||||
let _ = m.iommu_group(&p);
|
||||
let _ = m.numa_node(&p);
|
||||
let _ = m.msix_proposal(&p, 4, 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_constructor_works() {
|
||||
let _ = ModernTech::default();
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ fn run(
|
||||
let Some(events) = read_pciehp_lines(&pciehp_path, &mut last_seen) else {
|
||||
continue;
|
||||
};
|
||||
let binds = bind_snapshot();
|
||||
let _binds = bind_snapshot();
|
||||
for event in events {
|
||||
log::info!(
|
||||
"pciehp: event kind={} device={}",
|
||||
@@ -98,7 +98,7 @@ fn run(
|
||||
}
|
||||
}
|
||||
|
||||
fn read_pciehp_lines(path: &Path, last_seen: &mut u64) -> Option<Vec<PciehpEvent>> {
|
||||
pub fn read_pciehp_lines(path: &Path, last_seen: &mut u64) -> Option<Vec<PciehpEvent>> {
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ impl SchemeSync for SchemeServer {
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<OpenResult> {
|
||||
let accmode = flags & O_ACCMODE;
|
||||
if accmode != O_RDONLY && accmode != O_WRONLY && accmode != O_RDWR {
|
||||
if accmode != O_RDONLY && accmode != libc::O_WRONLY && accmode != libc::O_RDWR {
|
||||
return Err(Error::new(EACCES));
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@ impl SchemeSync for SchemeServer {
|
||||
fn write(
|
||||
&mut self,
|
||||
id: usize,
|
||||
buf: &[u8],
|
||||
buf: &mut [u8],
|
||||
_offset: u64,
|
||||
_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//! Unified event listener for AER + pciehp. Polls both
|
||||
//! `/scheme/acpi/aer` and `/scheme/pci/pciehp` every 500ms and routes
|
||||
//! events to the bound drivers. Combines the two listeners into one
|
||||
//! thread so we don't have two separate polling loops doing similar
|
||||
//! work.
|
||||
//!
|
||||
//! See `aer.rs` for the AER event source and `pciehp.rs` for the
|
||||
//! pciehp event source. This module is the merged listener.
|
||||
|
||||
use std::path::Path;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::aer::{AerEvent, ErrorSeverity};
|
||||
use crate::pciehp::{PciehpEvent, PciehpEventKind};
|
||||
|
||||
/// A unified event: either AER (error) or pciehp (hotplug).
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UnifiedEvent {
|
||||
Aer(AerEvent),
|
||||
Pciehp(PciehpEvent),
|
||||
}
|
||||
|
||||
/// Spawn the unified event listener thread. Polls both files and
|
||||
/// routes events to `handle_event`.
|
||||
pub fn spawn_unified_listener(
|
||||
aer_path: std::path::PathBuf,
|
||||
pciehp_path: std::path::PathBuf,
|
||||
bind_snapshot: impl Fn() -> Vec<(String, String)> + Send + 'static,
|
||||
handle_event: impl Fn(&UnifiedEvent) + Send + 'static,
|
||||
) -> thread::JoinHandle<()> {
|
||||
thread::Builder::new()
|
||||
.name("driver-manager-events".to_string())
|
||||
.spawn(move || run(aer_path, pciehp_path, bind_snapshot, handle_event))
|
||||
.expect("spawn unified event listener")
|
||||
}
|
||||
|
||||
fn run(
|
||||
aer_path: std::path::PathBuf,
|
||||
pciehp_path: std::path::PathBuf,
|
||||
bind_snapshot: impl Fn() -> Vec<(String, String)>,
|
||||
handle_event: impl Fn(&UnifiedEvent),
|
||||
) {
|
||||
log::info!(
|
||||
"events: unified listener started (aer={}, pciehp={})",
|
||||
aer_path.display(),
|
||||
pciehp_path.display()
|
||||
);
|
||||
let mut last_seen_aer: u64 = 0;
|
||||
let mut last_seen_pciehp: u64 = 0;
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
if let Some(events) = crate::aer::read_aer_lines(&aer_path, &mut last_seen_aer) {
|
||||
let binds = bind_snapshot();
|
||||
for event in events {
|
||||
log::info!(
|
||||
"AER: event device={} severity={:?}",
|
||||
event.device,
|
||||
event.severity
|
||||
);
|
||||
handle_event(&UnifiedEvent::Aer(event));
|
||||
}
|
||||
}
|
||||
if let Some(events) = crate::pciehp::read_pciehp_lines(&pciehp_path, &mut last_seen_pciehp) {
|
||||
let binds = bind_snapshot();
|
||||
for event in events {
|
||||
log::info!(
|
||||
"pciehp: event kind={} device={}",
|
||||
event.kind.label(),
|
||||
event.device
|
||||
);
|
||||
handle_event(&UnifiedEvent::Pciehp(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::pciehp::PciehpEventKind;
|
||||
|
||||
#[test]
|
||||
fn unified_event_wraps_aer() {
|
||||
let e = UnifiedEvent::Aer(crate::aer::AerEvent {
|
||||
severity: ErrorSeverity::Correctable,
|
||||
device: "0000:00:03.0".to_string(),
|
||||
raw: "kind=Correctable device=0000:00:03.0".to_string(),
|
||||
});
|
||||
assert!(matches!(e, UnifiedEvent::Aer(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unified_event_wraps_pciehp() {
|
||||
let e = UnifiedEvent::Pciehp(crate::pciehp::PciehpEvent {
|
||||
kind: PciehpEventKind::PresenceDetectChanged,
|
||||
device: "0000:00:03.0".to_string(),
|
||||
raw: "kind=pdc device=0000:00:03.0".to_string(),
|
||||
});
|
||||
assert!(matches!(e, UnifiedEvent::Pciehp(_)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user