1mod bpf_skel;
11
12mod arenalib;
13pub use arenalib::ArenaLib;
14mod scx_libarena;
15pub use scx_libarena::ScxLibArena;
16
17use std::fs::File;
18use std::io::BufRead;
19use std::io::BufReader;
20use std::io::ErrorKind;
21
22#[cfg(feature = "build-support")]
23pub mod build_support;
24use std::os::fd::AsFd;
25use std::os::fd::AsRawFd;
26use std::os::fd::BorrowedFd;
27use std::os::fd::FromRawFd;
28use std::os::fd::OwnedFd;
29use std::time::Duration;
30use std::time::Instant;
31
32use anyhow::Context;
33use anyhow::Result;
34use anyhow::bail;
35use libbpf_rs::AsRawLibbpf as _;
36use libbpf_rs::MapCore as _;
37use libbpf_rs::libbpf_sys;
38
39#[cfg(target_arch = "s390x")]
42pub const CACHELINE_SIZE: usize = 256;
43#[cfg(target_arch = "powerpc64")]
44pub const CACHELINE_SIZE: usize = 128;
45#[cfg(not(any(target_arch = "s390x", target_arch = "powerpc64")))]
46pub const CACHELINE_SIZE: usize = 64;
47
48const MEMBARRIER_CMD_GLOBAL: libc::c_long = 1;
49const URCU_DOORBELL: &str = "scx_urcu_doorbell";
50const URCU_MIN_INTERVAL: Duration = Duration::from_millis(1);
51const URCU_FALLBACK_INTERVAL: Duration = Duration::from_millis(100);
53const URCU_MEMBARRIER_RETRIES: u32 = 4;
55
56#[derive(Debug)]
59pub(crate) struct Daemon {
60 stop: OwnedFd,
61 thread: Option<std::thread::JoinHandle<()>>,
62}
63
64impl Drop for Daemon {
65 fn drop(&mut self) {
66 let one: u64 = 1;
67 let ret = unsafe {
68 libc::write(
69 self.stop.as_raw_fd(),
70 &one as *const u64 as *const libc::c_void,
71 std::mem::size_of::<u64>(),
72 )
73 };
74 if ret != std::mem::size_of::<u64>() as isize {
76 return;
77 }
78 if let Some(thread) = self.thread.take() {
79 let _ = thread.join();
80 }
81 }
82}
83
84fn stop_eventfd() -> Result<OwnedFd> {
86 let fd = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC) };
87 if fd < 0 {
88 bail!(
89 "creating daemon stop eventfd failed: {}",
90 std::io::Error::last_os_error()
91 );
92 }
93 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
94}
95
96fn prog_fd_clone(prog: &libbpf_rs::Program<'_>) -> Result<Option<OwnedFd>> {
100 let raw_fd = unsafe { libbpf_sys::bpf_program__fd(prog.as_libbpf_object().as_ptr()) };
101 if raw_fd < 0 {
102 return Ok(None);
103 }
104 let fd = unsafe { BorrowedFd::borrow_raw(raw_fd) }
105 .try_clone_to_owned()
106 .with_context(|| format!("cloning the fd of BPF prog {:?}", prog.name()))?;
107 Ok(Some(fd))
108}
109
110fn urcu_run_prog(fd: &OwnedFd) -> Result<u32> {
124 let mut opts: libbpf_sys::bpf_test_run_opts = unsafe { std::mem::zeroed() };
125
126 opts.sz = std::mem::size_of::<libbpf_sys::bpf_test_run_opts>() as _;
127 let ret = unsafe { libbpf_sys::bpf_prog_test_run_opts(fd.as_raw_fd(), &mut opts) };
128 if ret != 0 {
129 bail!("urcu driver program run failed: {}", ret);
130 }
131 Ok(opts.retval)
132}
133
134struct UrcuGracePeriod {
141 membarrier: bool,
142 outer: libbpf_rs::MapHandle,
143 inner: libbpf_rs::MapHandle,
144 warned: bool,
145}
146
147fn membarrier_global() -> std::io::Result<()> {
148 let ret = unsafe { libc::syscall(libc::SYS_membarrier, MEMBARRIER_CMD_GLOBAL, 0, 0) };
149 if ret != 0 {
150 return Err(std::io::Error::last_os_error());
151 }
152 Ok(())
153}
154
155fn hotplug_seq() -> Result<u64> {
157 let seq = std::fs::read_to_string("/sys/kernel/sched_ext/hotplug_seq")
158 .context("reading /sys/kernel/sched_ext/hotplug_seq")?;
159 seq.trim()
160 .parse()
161 .with_context(|| format!("parsing hotplug_seq {:?}", seq))
162}
163
164fn online_cpus() -> Result<libc::c_long> {
165 let ret = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN) };
166 if ret < 0 {
167 bail!(
168 "reading the online CPU count failed: {}",
169 std::io::Error::last_os_error()
170 );
171 }
172 Ok(ret)
173}
174
175impl UrcuGracePeriod {
176 fn new() -> Result<Self> {
177 let membarrier = match membarrier_global() {
178 Ok(()) => true,
179 Err(e) if e.raw_os_error() == Some(libc::EINVAL) => false,
180 Err(e) => bail!("membarrier(GLOBAL) failed: {}", e),
181 };
182
183 let opts = libbpf_sys::bpf_map_create_opts {
184 sz: std::mem::size_of::<libbpf_sys::bpf_map_create_opts>() as _,
185 ..Default::default()
186 };
187 let inner = libbpf_rs::MapHandle::create(
188 libbpf_rs::MapType::Array,
189 Some("scx_urcu_inner"),
190 4,
191 4,
192 1,
193 &opts,
194 )
195 .context("creating urcu inner map")?;
196 let opts = libbpf_sys::bpf_map_create_opts {
197 inner_map_fd: inner.as_fd().as_raw_fd() as u32,
198 ..opts
199 };
200 let outer = libbpf_rs::MapHandle::create(
201 libbpf_rs::MapType::ArrayOfMaps,
202 Some("scx_urcu_outer"),
203 4,
204 4,
205 1,
206 &opts,
207 )
208 .context("creating urcu outer map")?;
209 let gp = Self {
210 membarrier,
211 outer,
212 inner,
213 warned: false,
214 };
215 gp.map_update()
216 .context("probing urcu map-update grace period")?;
217 Ok(gp)
218 }
219
220 fn map_update(&self) -> Result<()> {
221 self.outer
227 .update(
228 &0u32.to_ne_bytes(),
229 &self.inner.as_fd().as_raw_fd().to_ne_bytes(),
230 libbpf_rs::MapFlags::ANY,
231 )
232 .context("waiting for urcu map-update grace period")
233 }
234
235 fn synchronize(&mut self) -> Result<bool> {
237 if self.membarrier {
238 for _ in 0..URCU_MEMBARRIER_RETRIES {
262 if online_cpus()? <= 1 {
263 break;
264 }
265 let seq = hotplug_seq()?;
266
267 membarrier_global().context("membarrier(GLOBAL) failed")?;
268 if online_cpus()? <= 1 {
269 break;
270 }
271 if hotplug_seq()? == seq {
272 return Ok(false);
273 }
274 }
275 }
276
277 if !self.warned {
278 self.warned = true;
279 eprintln!(
280 "scx-urcu: {}, waiting for grace periods through expedited map-in-map updates at least {:?} apart",
281 if self.membarrier {
282 "a single CPU is online"
283 } else {
284 "membarrier is unavailable with nohz_full"
285 },
286 URCU_FALLBACK_INTERVAL
287 );
288 }
289 self.map_update()?;
290 Ok(true)
291 }
292}
293
294fn urcu_daemon(
295 stop: OwnedFd,
296 doorbell: libbpf_rs::MapHandle,
297 pairs: Vec<(OwnedFd, OwnedFd)>,
298 mut gp: UrcuGracePeriod,
299) -> Result<()> {
300 let mut builder = libbpf_rs::RingBufferBuilder::new();
301 builder
302 .add(&doorbell, |_| 0)
303 .context("adding urcu doorbell to ring buffer")?;
304 let rb = builder
305 .build()
306 .context("building urcu doorbell ring buffer")?;
307
308 let mut last: Option<Instant> = None;
309 let mut interval = URCU_MIN_INTERVAL;
310 loop {
311 let mut fds = [
312 libc::pollfd {
313 fd: doorbell.as_fd().as_raw_fd(),
314 events: libc::POLLIN,
315 revents: 0,
316 },
317 libc::pollfd {
318 fd: stop.as_raw_fd(),
319 events: libc::POLLIN,
320 revents: 0,
321 },
322 ];
323
324 let ret = unsafe { libc::poll(fds.as_mut_ptr(), 2, -1) };
325 if ret < 0 {
326 let err = std::io::Error::last_os_error();
327 if err.raw_os_error() == Some(libc::EINTR) {
328 continue;
329 }
330 bail!("urcu doorbell poll failed: {}", err);
331 }
332
333 let stopping = fds[1].revents != 0;
335
336 loop {
338 let pace = if stopping { Duration::ZERO } else { interval };
339 if let Some(last) = last {
340 let elapsed = last.elapsed();
341 if elapsed < pace {
342 std::thread::sleep(pace - elapsed);
343 }
344 }
345
346 rb.consume().context("consuming urcu doorbell")?;
353
354 let mut reclaims = Vec::new();
355 for (pending, reclaim) in &pairs {
356 if urcu_run_prog(pending)? != 0 {
357 reclaims.push(reclaim);
358 }
359 }
360 if reclaims.is_empty() {
361 break;
362 }
363 last = Some(Instant::now());
364
365 interval = if gp.synchronize()? {
366 URCU_FALLBACK_INTERVAL
367 } else {
368 URCU_MIN_INTERVAL
369 };
370
371 for reclaim in reclaims {
372 while urcu_run_prog(reclaim)? != 0 {}
373 }
374 }
375
376 if stopping {
377 return Ok(());
378 }
379 }
380}
381
382pub(crate) fn urcu_spawn(obj: &libbpf_rs::Object) -> Result<Option<Daemon>> {
386 let Some(doorbell) = obj.maps().find(|m| m.name() == URCU_DOORBELL) else {
387 return Ok(None);
388 };
389 let doorbell =
390 libbpf_rs::MapHandle::try_from(&doorbell).context("cloning urcu doorbell handle")?;
391
392 let mut pairs = Vec::new();
393 for prog in obj.progs() {
394 let Some(name) = prog.name().to_str() else {
395 continue;
396 };
397 let Some(base) = name.strip_suffix("_pending") else {
398 continue;
399 };
400 if !name.starts_with("scx_urcu_") {
401 continue;
402 }
403
404 let reclaim_name = format!("{}_reclaim", base);
405 let reclaim = obj
406 .progs()
407 .find(|p| p.name() == reclaim_name.as_str())
408 .with_context(|| format!("urcu driver program {} not found", reclaim_name))?;
409
410 let Some(pending_fd) = prog_fd_clone(&prog)? else {
411 bail!("urcu driver program {} is not loaded", name);
412 };
413 let Some(reclaim_fd) = prog_fd_clone(&reclaim)? else {
414 bail!("urcu driver program {} is not loaded", reclaim_name);
415 };
416 pairs.push((pending_fd, reclaim_fd));
417 }
418 if pairs.is_empty() {
419 return Ok(None);
420 }
421
422 let gp = UrcuGracePeriod::new().context("setting up urcu grace periods")?;
423 let stop = stop_eventfd()?;
424 let daemon_stop = stop.try_clone().context("cloning urcu stop eventfd")?;
425 let thread = std::thread::Builder::new()
426 .name("scx-urcu".into())
427 .spawn(move || {
428 if let Err(e) = urcu_daemon(daemon_stop, doorbell, pairs, gp) {
429 let _ = std::io::Write::write_fmt(
430 &mut std::io::stderr(),
431 format_args!("FATAL: scx-urcu daemon failed: {:#}\n", e),
432 );
433 std::process::exit(1);
434 }
435 })
436 .context("spawning urcu daemon thread")?;
437
438 Ok(Some(Daemon {
439 stop,
440 thread: Some(thread),
441 }))
442}
443
444const BPF_STDOUT: u32 = 1;
445const BPF_STDERR: u32 = 2;
446const BPF_STREAMS: [(u32, &str); 2] = [(BPF_STDOUT, "stdout"), (BPF_STDERR, "stderr")];
447const STREAM_POLL_INTERVAL: Duration = Duration::from_secs(1);
448
449fn stream_read(fd: &OwnedFd, stream_id: u32, buf: &mut [u8]) -> i32 {
450 unsafe {
451 libbpf_sys::bpf_prog_stream_read(
452 fd.as_raw_fd(),
453 stream_id,
454 buf.as_mut_ptr() as *mut _,
455 buf.len() as u32,
456 std::ptr::null_mut(),
457 )
458 }
459}
460
461fn stream_is_fatal(lines: &[String]) -> bool {
464 lines.iter().any(|l| l.starts_with("ERROR: "))
465}
466
467fn stream_extract_lines(carry: &mut String, new_data: bool) -> Vec<String> {
473 let mut lines: Vec<String> = Vec::new();
474
475 if let Some(pos) = carry.rfind('\n') {
476 lines = carry[..pos].split('\n').map(str::to_string).collect();
477 carry.drain(..=pos);
478 }
479 if !new_data && !carry.is_empty() {
480 lines.push(std::mem::take(carry));
481 }
482
483 lines
484}
485
486fn stream_forward(name: &str, stream_id: u32, label: &str, lines: &[String]) {
492 let kept: Vec<&str> = lines
493 .iter()
494 .map(String::as_str)
495 .filter(|l| !l.starts_with("IGN: "))
496 .collect();
497 if !kept.is_empty() {
498 let msg = kept.join("\n");
499 let out = format!("BPF {} of prog {}:\n{}\n", label, name, msg);
500 if stream_id == BPF_STDOUT {
501 print!("{}", out);
502 let _ = std::io::Write::flush(&mut std::io::stdout());
503 } else {
504 eprint!("{}", out);
505 }
506 }
507 if stream_id == BPF_STDERR && stream_is_fatal(lines) {
508 eprintln!("FATAL: aborting on BPF error report");
509 std::process::exit(1);
510 }
511}
512
513struct StreamFd {
516 name: String,
517 stream_id: u32,
518 label: &'static str,
519 reader: BufReader<File>,
520 pending: Vec<u8>,
521 eof: bool,
522}
523
524impl StreamFd {
525 fn read_lines(&mut self, last: bool) {
529 let mut lines = Vec::new();
530
531 loop {
532 match self.reader.read_until(b'\n', &mut self.pending) {
533 Ok(0) => self.eof = true,
534 Ok(_) if self.pending.ends_with(b"\n") => {
535 self.pending.pop();
536 lines.push(String::from_utf8_lossy(&self.pending).into_owned());
537 self.pending.clear();
538 continue;
539 }
540 Ok(_) => self.eof = true,
542 Err(e) if e.kind() == ErrorKind::WouldBlock => {}
543 Err(e) => {
544 eprintln!("reading BPF {} of prog {}: {}", self.label, self.name, e);
545 self.eof = true;
546 }
547 }
548 break;
549 }
550 if (self.eof || last) && !self.pending.is_empty() {
551 lines.push(String::from_utf8_lossy(&self.pending).into_owned());
552 self.pending.clear();
553 }
554 if !lines.is_empty() {
555 stream_forward(&self.name, self.stream_id, self.label, &lines);
556 }
557 }
558}
559
560fn stream_watch_fds(mut streams: Vec<StreamFd>, stop: OwnedFd) {
563 while !streams.is_empty() {
564 let mut fds: Vec<libc::pollfd> = std::iter::once(stop.as_raw_fd())
565 .chain(streams.iter().map(|s| s.reader.get_ref().as_raw_fd()))
566 .map(|fd| libc::pollfd {
567 fd,
568 events: libc::POLLIN,
569 revents: 0,
570 })
571 .collect();
572 let ret = unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as libc::nfds_t, -1) };
573 if ret < 0 {
574 let err = std::io::Error::last_os_error();
575 if err.kind() == ErrorKind::Interrupted {
576 continue;
577 }
578 eprintln!("BPF stream watcher stopping, poll failed: {}", err);
579 break;
580 }
581
582 let stopping = fds[0].revents != 0;
583 for (i, stream) in streams.iter_mut().enumerate() {
584 if stopping || fds[i + 1].revents != 0 {
585 stream.read_lines(stopping);
586 }
587 }
588 streams.retain(|s| !s.eof);
589 if stopping {
590 break;
591 }
592 }
593}
594
595fn stream_watch_periodic(progs: Vec<(String, OwnedFd)>, stop: OwnedFd) {
598 let mut buf = vec![0u8; 65536];
599 let mut carries: Vec<[String; 2]> = (0..progs.len())
600 .map(|_| [String::new(), String::new()])
601 .collect();
602
603 loop {
604 let mut fds = [libc::pollfd {
605 fd: stop.as_raw_fd(),
606 events: libc::POLLIN,
607 revents: 0,
608 }];
609 let ret =
610 unsafe { libc::poll(fds.as_mut_ptr(), 1, STREAM_POLL_INTERVAL.as_millis() as i32) };
611 let stopping = if ret < 0 {
613 std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR)
614 } else {
615 ret > 0
616 };
617
618 for (pidx, (name, fd)) in progs.iter().enumerate() {
619 for (sidx, (stream_id, label)) in BPF_STREAMS.iter().enumerate() {
620 let carry = &mut carries[pidx][sidx];
621 let mut new_data = false;
622
623 loop {
625 let n = stream_read(fd, *stream_id, &mut buf);
626 if n <= 0 {
627 break;
628 }
629 carry.push_str(&String::from_utf8_lossy(&buf[..n as usize]));
630 new_data = true;
631 if (n as usize) < buf.len() {
632 break;
633 }
634 }
635
636 let lines = stream_extract_lines(carry, new_data && !stopping);
638 if !lines.is_empty() {
639 stream_forward(name, *stream_id, label, &lines);
640 }
641 }
642 }
643
644 if stopping {
645 break;
646 }
647 }
648}
649
650pub(crate) fn stream_watcher_spawn(obj: &libbpf_rs::Object) -> Result<Daemon> {
657 let with_fds = *scx_utils::compat::PROG_STREAM_OPEN_SUPPORTED;
658 let mut progs = Vec::new();
659 let mut streams = Vec::new();
660
661 for prog in obj.progs() {
662 let Some(name) = prog.name().to_str() else {
663 continue;
664 };
665 let Some(fd) = prog_fd_clone(&prog)? else {
667 continue;
668 };
669 if !with_fds {
670 progs.push((name.to_string(), fd));
671 continue;
672 }
673 for (stream_id, label) in BPF_STREAMS {
674 let sfd = scx_utils::compat::prog_stream_open(fd.as_fd(), stream_id, true)
675 .with_context(|| format!("opening BPF {} of prog {}", label, name))?;
676 streams.push(StreamFd {
677 name: name.to_string(),
678 stream_id,
679 label,
680 reader: BufReader::new(File::from(sfd)),
681 pending: Vec::new(),
682 eof: false,
683 });
684 }
685 }
686
687 let stop = stop_eventfd()?;
688 let daemon_stop = stop
689 .try_clone()
690 .context("cloning stream watcher stop eventfd")?;
691 let thread = std::thread::Builder::new()
692 .name("scx-bpf-stream".into())
693 .spawn(move || {
694 if with_fds {
695 stream_watch_fds(streams, daemon_stop);
696 } else {
697 stream_watch_periodic(progs, daemon_stop);
698 }
699 })
700 .context("spawning BPF stream watcher")?;
701
702 Ok(Daemon {
703 stop,
704 thread: Some(thread),
705 })
706}
707
708#[cfg(test)]
709mod tests {
710 use super::stream_extract_lines;
711 use super::stream_is_fatal;
712
713 #[test]
714 fn test_stream_extract_lines() {
715 let mut carry = String::new();
716
717 assert!(stream_extract_lines(&mut carry, false).is_empty());
719
720 carry.push_str("ERROR: whole line\npartial");
722 assert_eq!(
723 stream_extract_lines(&mut carry, true),
724 ["ERROR: whole line"]
725 );
726 assert_eq!(carry, "partial");
727
728 assert!(stream_extract_lines(&mut carry, true).is_empty());
730 assert_eq!(carry, "partial");
731
732 carry.push_str(" continued\nnext");
734 assert_eq!(
735 stream_extract_lines(&mut carry, true),
736 ["partial continued"]
737 );
738 assert_eq!(carry, "next");
739
740 assert_eq!(stream_extract_lines(&mut carry, false), ["next"]);
742 assert!(carry.is_empty());
743 assert!(stream_extract_lines(&mut carry, false).is_empty());
744
745 carry.push_str("a\n\nb\ntail");
747 assert_eq!(stream_extract_lines(&mut carry, true), ["a", "", "b"]);
748 assert_eq!(carry, "tail");
749 carry.clear();
750
751 carry.push_str("caf");
753 assert!(stream_extract_lines(&mut carry, true).is_empty());
754 carry.push_str("\u{e9}\n");
755 assert_eq!(stream_extract_lines(&mut carry, true), ["caf\u{e9}"]);
756 assert!(carry.is_empty());
757 }
758
759 #[test]
760 fn test_stream_is_fatal() {
761 let fatal = ["noise".to_string(), "ERROR: Arena READ access".to_string()];
762 assert!(stream_is_fatal(&fatal));
763
764 let glued = ["no task dataERROR: Arena READ access".to_string()];
766 assert!(!stream_is_fatal(&glued));
767 assert!(!stream_is_fatal(&[]));
768 }
769}