Skip to main content

scx_utils/
user_exit_info.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2//
3// This software may be used and distributed according to the terms of the
4// GNU General Public License version 2.
5use crate::bindings;
6use crate::compat;
7use anyhow::bail;
8use anyhow::Result;
9use std::ffi::CStr;
10use std::os::raw::c_char;
11use std::sync::Mutex;
12
13pub struct UeiDumpPtr {
14    pub ptr: *const c_char,
15}
16unsafe impl Send for UeiDumpPtr {}
17
18pub static UEI_DUMP_PTR_MUTEX: Mutex<UeiDumpPtr> = Mutex::new(UeiDumpPtr {
19    ptr: std::ptr::null(),
20});
21
22lazy_static::lazy_static! {
23    pub static ref SCX_ECODE_RSN_HOTPLUG: u64 =
24    compat::read_enum("scx_exit_code", "SCX_ECODE_RSN_HOTPLUG").unwrap_or(0);
25}
26
27lazy_static::lazy_static! {
28    pub static ref SCX_ECODE_ACT_RESTART: u64 =
29    compat::read_enum("scx_exit_code", "SCX_ECODE_ACT_RESTART").unwrap_or(0);
30}
31
32pub enum ScxExitKind {
33    None = bindings::scx_exit_kind_SCX_EXIT_NONE as isize,
34    Done = bindings::scx_exit_kind_SCX_EXIT_DONE as isize,
35    Unreg = bindings::scx_exit_kind_SCX_EXIT_UNREG as isize,
36    UnregBPF = bindings::scx_exit_kind_SCX_EXIT_UNREG_BPF as isize,
37    UnregKern = bindings::scx_exit_kind_SCX_EXIT_UNREG_KERN as isize,
38    SysRq = bindings::scx_exit_kind_SCX_EXIT_SYSRQ as isize,
39    Error = bindings::scx_exit_kind_SCX_EXIT_ERROR as isize,
40    ErrorBPF = bindings::scx_exit_kind_SCX_EXIT_ERROR_BPF as isize,
41    ErrorStall = bindings::scx_exit_kind_SCX_EXIT_ERROR_STALL as isize,
42}
43
44pub enum ScxConsts {
45    ExitDumpDflLen = bindings::scx_consts_SCX_EXIT_DUMP_DFL_LEN as isize,
46}
47
48/// Takes a reference to C struct user_exit_info and reads it into
49/// UserExitInfo. See UserExitInfo.
50#[macro_export]
51macro_rules! uei_read {
52    ($skel: expr, $uei:ident) => {{
53        scx_utils::paste! {
54            let bpf_uei = $skel.maps.data_data.as_ref().unwrap().$uei;
55            let bpf_dump = scx_utils::UEI_DUMP_PTR_MUTEX.lock().unwrap().ptr;
56            let exit_code_ptr = match scx_utils::compat::struct_has_field("scx_exit_info", "exit_code") {
57                Ok(true) => &bpf_uei.exit_code as *const _,
58                _ => std::ptr::null(),
59            };
60            let exit_cpu_ptr = match scx_utils::compat::struct_has_field("scx_exit_info", "exit_cpu") {
61                Ok(true) => &bpf_uei.exit_cpu as *const _,
62                _ => std::ptr::null(),
63            };
64
65            scx_utils::UserExitInfo::new(
66                &bpf_uei.kind as *const _,
67                exit_code_ptr,
68                exit_cpu_ptr,
69                bpf_uei.reason.as_ptr() as *const _,
70                bpf_uei.msg.as_ptr() as *const _,
71                bpf_dump,
72            )
73        }
74    }};
75}
76
77/// Resize debug dump area according to ops.exit_dump_len. If this macro is
78/// not called, debug dump area is not allocated and debug dump won't be
79/// printed out.
80#[macro_export]
81macro_rules! uei_set_size {
82    ($skel: expr, $ops: ident, $uei:ident) => {{
83        scx_utils::paste! {
84            let len = match $skel.struct_ops.$ops().exit_dump_len {
85                0 => scx_utils::ScxConsts::ExitDumpDflLen as u32,
86                v => v,
87            };
88            $skel.maps.rodata_data.as_mut().unwrap().[<$uei _dump_len>] = len;
89            $skel.maps.[<data_ $uei _dump>].set_value_size(len).unwrap();
90
91            let mut ptr = scx_utils::UEI_DUMP_PTR_MUTEX.lock().unwrap();
92            *ptr = scx_utils::UeiDumpPtr { ptr:
93                       $skel
94                       .maps
95                       .[<data_ $uei _dump>]
96                       .initial_value()
97                       .unwrap()
98                       .as_ptr() as *const _,
99            };
100        }
101    }};
102}
103
104/// Takes a reference to C struct user_exit_info and test whether the BPF
105/// scheduler has exited. See UserExitInfo.
106#[macro_export]
107macro_rules! uei_exited {
108    ($skel: expr, $uei:ident) => {{
109        let bpf_uei = $skel.maps.data_data.as_ref().unwrap().uei;
110        (unsafe { std::ptr::read_volatile(&bpf_uei.kind as *const _) } != 0)
111    }};
112}
113
114/// Takes a reference to C struct user_exit_info, reads, invokes
115/// UserExitInfo::report() on and then returns Ok(uei). See UserExitInfo.
116#[macro_export]
117macro_rules! uei_report {
118    ($skel: expr, $uei:ident) => {{
119        let uei = scx_utils::uei_read!($skel, $uei);
120        uei.report().and_then(|_| Ok(uei))
121    }};
122}
123
124/// Rust counterpart of C struct user_exit_info.
125#[derive(Debug)]
126pub struct UserExitInfo {
127    /// The C enum scx_exit_kind value. Test against ScxExitKind. None-zero
128    /// value indicates that the BPF scheduler has exited.
129    kind: i32,
130    exit_code: i64,
131    /// CPU that triggered the exit, or -1 if unknown.
132    exit_cpu: i32,
133    reason: Option<String>,
134    msg: Option<String>,
135    dump: Option<String>,
136}
137
138impl Default for UserExitInfo {
139    fn default() -> Self {
140        Self {
141            kind: 0,
142            exit_code: 0,
143            exit_cpu: -1,
144            reason: None,
145            msg: None,
146            dump: None,
147        }
148    }
149}
150
151impl UserExitInfo {
152    /// Create UserExitInfo from C struct user_exit_info. Each scheduler
153    /// implementation creates its own Rust binding for the C struct
154    /// user_exit_info, so we can't take the type directly. Instead, this
155    /// method takes each member field. Use the macro uei_read!() on the C
156    /// type which then calls this method with the individual fields.
157    pub fn new(
158        kind_ptr: *const i32,
159        exit_code_ptr: *const i64,
160        exit_cpu_ptr: *const i32,
161        reason_ptr: *const c_char,
162        msg_ptr: *const c_char,
163        dump_ptr: *const c_char,
164    ) -> Self {
165        let kind = unsafe { std::ptr::read_volatile(kind_ptr) };
166        let exit_code = if exit_code_ptr.is_null() {
167            0
168        } else {
169            unsafe { std::ptr::read_volatile(exit_code_ptr) }
170        };
171        /*
172         * Start from -1 and only read the field when the kernel reports it
173         * so that missing information stays distinguishable from CPU 0.
174         */
175        let exit_cpu = if exit_cpu_ptr.is_null() {
176            -1
177        } else {
178            unsafe { std::ptr::read_volatile(exit_cpu_ptr) }
179        };
180
181        let (reason, msg) = (
182            Some(
183                unsafe { CStr::from_ptr(reason_ptr) }
184                    .to_str()
185                    .expect("Failed to convert reason to string")
186                    .to_string(),
187            )
188            .filter(|s| !s.is_empty()),
189            Some(
190                unsafe { CStr::from_ptr(msg_ptr) }
191                    .to_str()
192                    .expect("Failed to convert msg to string")
193                    .to_string(),
194            )
195            .filter(|s| !s.is_empty()),
196        );
197
198        let dump = if dump_ptr.is_null() {
199            None
200        } else {
201            Some(
202                unsafe { CStr::from_ptr(dump_ptr) }
203                    .to_str()
204                    .expect("Failed to convert msg to string")
205                    .to_string(),
206            )
207            .filter(|s| !s.is_empty())
208        };
209
210        Self {
211            kind,
212            exit_code,
213            exit_cpu,
214            reason,
215            msg,
216            dump,
217        }
218    }
219
220    /// Print out the exit message to stderr if the exit was normal. After
221    /// an error exit, it throws an error containing the exit message
222    /// instead. If debug dump exists, it's always printed to stderr.
223    pub fn report(&self) -> Result<()> {
224        if self.kind == 0 {
225            return Ok(());
226        }
227
228        if let Some(dump) = &self.dump {
229            eprintln!("\nDEBUG DUMP");
230            eprintln!(
231                "================================================================================\n"
232            );
233            eprintln!("{dump}");
234            eprintln!(
235                "================================================================================\n"
236            );
237        }
238
239        let cpu = match self.exit_cpu {
240            v if v >= 0 => format!(" on CPU {v}"),
241            _ => "".into(),
242        };
243        let why = match (&self.reason, &self.msg) {
244            (Some(reason), None) => format!("EXIT: {reason}{cpu}"),
245            (Some(reason), Some(msg)) => format!("EXIT: {reason} ({msg}){cpu}"),
246            _ => format!("<UNKNOWN>{cpu}"),
247        };
248
249        if self.kind <= ScxExitKind::UnregKern as i32 {
250            eprintln!("{why}");
251            Ok(())
252        } else {
253            bail!("{why}")
254        }
255    }
256
257    /// Return the exit code that the scheduler gracefully exited with. This
258    /// only applies when the BPF scheduler exits with scx_bpf_exit(), i.e. kind
259    /// ScxExitKind::UnregBPF.
260    pub fn exit_code(&self) -> Option<i64> {
261        (self.kind == ScxExitKind::UnregBPF as i32 || self.kind == ScxExitKind::UnregKern as i32)
262            .then_some(self.exit_code)
263    }
264
265    /// CPU on which the exit condition was triggered. None if the kernel
266    /// did not report it.
267    pub fn exit_cpu(&self) -> Option<i32> {
268        (self.exit_cpu >= 0).then_some(self.exit_cpu)
269    }
270
271    /// Test whether the BPF scheduler requested restart.
272    pub fn should_restart(&self) -> bool {
273        match self.exit_code() {
274            Some(ecode) => (ecode & *SCX_ECODE_ACT_RESTART as i64) != 0,
275            _ => false,
276        }
277    }
278}