scxtui/kernel.rs
1// SPDX-License-Identifier: GPL-2.0
2
3//! The kernel's own view of `sched_ext`, read straight from sysfs.
4//!
5//! `scx.service` only knows whether its unit is active; the kernel knows
6//! what is actually attached. Reading `/sys/kernel/sched_ext` lets the UI
7//! show ground truth and flag disagreements — a scheduler launched by hand
8//! next to the service, a scheduler that got kicked out by the watchdog
9//! while the unit is still active, and so on. Everything here is plain
10//! world-readable sysfs, so no privileges are needed.
11
12use std::fs;
13use std::path::Path;
14
15use anyhow::{Context, Result};
16
17const SYSFS_DIR: &str = "/sys/kernel/sched_ext";
18
19#[derive(Debug, Clone, PartialEq)]
20pub struct KernelState {
21 /// Contents of `state`: "enabled", "disabled", or a transitional value
22 /// ("enabling"/"disabling") if we catch a switch mid-flight.
23 pub state: String,
24 /// BPF ops name from `root/ops` when a scheduler is attached — the name
25 /// the scheduler registered with, typically without the `scx_` prefix
26 /// (e.g. "lavd" for `scx_lavd`).
27 pub ops: Option<String>,
28}
29
30impl KernelState {
31 pub fn enabled(&self) -> bool {
32 self.state == "enabled"
33 }
34
35 /// Whether `sched_name` (full name, e.g. `scx_lavd`) plausibly matches
36 /// the ops the kernel reports. Ops names are chosen by each scheduler
37 /// and some embed extra detail, with or without the `scx_` prefix —
38 /// bpfland registers e.g. `bpfland_1.1.2_x86_64_unknown_linux_gnu`,
39 /// flow registers `scx_flow_3.1.1_x86_64_unknown_linux_gnu` — so
40 /// beyond an exact match (with or without the prefix) an ops name
41 /// that *starts with* either form of the scheduler name followed by
42 /// `_` also counts. Used for a soft warning only, so a rare false
43 /// negative is acceptable.
44 pub fn matches(&self, sched_name: &str) -> bool {
45 let stripped = sched_name.strip_prefix("scx_").unwrap_or(sched_name);
46 self.ops.as_deref().is_some_and(|ops| {
47 ops == sched_name
48 || ops == stripped
49 || ops
50 .strip_prefix(sched_name)
51 .is_some_and(|rest| rest.starts_with('_'))
52 || ops
53 .strip_prefix(stripped)
54 .is_some_and(|rest| rest.starts_with('_'))
55 })
56 }
57}
58
59/// Reads the kernel's `sched_ext` state. `Ok(None)` means the sysfs
60/// directory is absent, i.e. the running kernel was built without
61/// `sched_ext`; `Err` means it is there but `state` could not be read,
62/// which is a different fact and must not be reported as the former.
63/// `root/ops` is absent while nothing is attached, so NotFound on it is
64/// not an error; any other failure on it is.
65pub fn read() -> Result<Option<KernelState>> {
66 let dir = Path::new(SYSFS_DIR);
67 if !dir.is_dir() {
68 return Ok(None);
69 }
70 let state = fs::read_to_string(dir.join("state"))
71 .with_context(|| format!("cannot read {}/state", dir.display()))?
72 .trim()
73 .to_owned();
74 // `root/ops` exists only while a scheduler is attached, so NotFound
75 // is the normal "nothing attached" answer; any other failure is a
76 // broken observation and must not pass for one.
77 let ops = match fs::read_to_string(dir.join("root/ops")) {
78 Ok(s) => Some(s.trim().to_owned()).filter(|s| !s.is_empty()),
79 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
80 Err(err) => {
81 return Err(err).with_context(|| format!("cannot read {}/root/ops", dir.display()));
82 }
83 };
84 Ok(Some(KernelState { state, ops }))
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 fn enabled_with(ops: Option<&str>) -> KernelState {
92 KernelState {
93 state: "enabled".to_owned(),
94 ops: ops.map(str::to_owned),
95 }
96 }
97
98 #[test]
99 fn matches_stripped_ops_name() {
100 assert!(enabled_with(Some("lavd")).matches("scx_lavd"));
101 }
102
103 #[test]
104 fn matches_full_ops_name() {
105 assert!(enabled_with(Some("scx_lavd")).matches("scx_lavd"));
106 }
107
108 #[test]
109 fn matches_versioned_ops_suffix() {
110 // bpfland registers e.g. "bpfland_1.1.2_x86_64_unknown_linux_gnu".
111 assert!(
112 enabled_with(Some("bpfland_1.1.2_x86_64_unknown_linux_gnu")).matches("scx_bpfland")
113 );
114 }
115
116 #[test]
117 fn matches_versioned_ops_suffix_with_prefix() {
118 // flow keeps the scx_ prefix in its versioned ops name.
119 assert!(enabled_with(Some("scx_flow_3.1.1_x86_64_unknown_linux_gnu")).matches("scx_flow"));
120 }
121
122 #[test]
123 fn rejects_full_name_prefix_without_separator() {
124 assert!(!enabled_with(Some("scx_flowmatic_1.0")).matches("scx_flow"));
125 }
126
127 #[test]
128 fn rejects_a_different_scheduler() {
129 assert!(!enabled_with(Some("flow_1.0")).matches("scx_flash"));
130 }
131
132 #[test]
133 fn rejects_prefix_without_separator() {
134 assert!(!enabled_with(Some("bpflandish")).matches("scx_bpfland"));
135 }
136
137 #[test]
138 fn no_ops_never_matches() {
139 assert!(!enabled_with(None).matches("scx_lavd"));
140 }
141}