shadow_shim/preempt.rs
1//! Native preemption for managed code.
2//!
3//! This module is used to regain control from managed code that would otherwise
4//! run indefinitely (or for a long time) without otherwise returning control to
5//! Shadow. When control is regained in this way, simulated time is moved
6//! forward some amount, and the current thread potentially rescheduled.
7//!
8//! `process_init` should be called once per process, before any other methods are called.
9//!
10//! `enable` should be called to enable preemption for the current thread, and
11//! `disable` to disable preemption for the current thread.
12use linux_api::signal::{SigActionFlags, siginfo_t, sigset_t};
13use log::{debug, trace};
14use shadow_shim_helper_rs::option::FfiOption;
15use shadow_shim_helper_rs::shadow_syscalls::ShadowSyscallNum;
16use shadow_shim_helper_rs::shim_event::ShimEventSyscall;
17use shadow_shim_helper_rs::syscall_types::SyscallArgs;
18
19use crate::ExecutionContext;
20
21// The signal we use for preemption.
22const PREEMPTION_SIGNAL: linux_api::signal::Signal = linux_api::signal::Signal::SIGVTALRM;
23
24extern "C" fn handle_timer_signal(signo: i32, _info: *mut siginfo_t, _ctx: *mut core::ffi::c_void) {
25 let prev = ExecutionContext::Shadow.enter();
26 trace!("Got preemption timer signal.");
27
28 assert_eq!(signo, i32::from(PREEMPTION_SIGNAL));
29
30 if prev.ctx() == ExecutionContext::Shadow {
31 // There's a small chance of us getting here when the timer signal fires
32 // just as we're switching contexts. It's simpler to just ignore it here than
33 // to completely prevent this possibility.
34 trace!("Got timer signal in shadow context. Ignoring.");
35 return;
36 }
37
38 let FfiOption::Some(config) = &crate::global_manager_shmem::get().native_preemption_config
39 else {
40 // Not configured.
41 panic!("Preemption signal handler somehow invoked when it wasn't configured.");
42 };
43
44 // Preemption should be rare. Probably worth at least a debug-level message.
45 debug!(
46 "Native preemption incrementing simulated CPU latency by {:?} after waiting {:?}",
47 config.sim_duration, config.native_duration
48 );
49
50 {
51 // Move simulated time forward.
52 let host = crate::global_host_shmem::get();
53 let mut host_lock = host.protected().lock();
54 host_lock.unapplied_cpu_latency += config.sim_duration;
55 }
56 // Transfer control to shadow, which will handle the time update and potentially
57 // reschedule this thread.
58 //
59 // We *could* try to apply the cpu-latency here and avoid yielding to shadow
60 // if we haven't yet reached the maximum runahead time, as we do in
61 // `shim_sys_handle_syscall_locally`, but in practice `config.sim_duration`
62 // should be large enough that shadow will always choose to reschedule this
63 // thread anyway, so we wouldn't actually get any performance benefit in
64 // exchange for the additional complexity.
65 let syscall_event = ShimEventSyscall {
66 syscall_args: SyscallArgs {
67 number: i64::from(u32::from(ShadowSyscallNum::shadow_yield)),
68 args: [0.into(); 6],
69 },
70 };
71 unsafe { crate::syscall::emulated_syscall_event(None, &syscall_event) };
72}
73
74/// Initialize state for the current native process. This does not yet actually
75/// enable preemption, which is done by calling `enable`.
76pub fn process_init() {
77 debug_assert_eq!(ExecutionContext::current(), ExecutionContext::Shadow);
78 let FfiOption::Some(_config) = &crate::global_manager_shmem::get().native_preemption_config
79 else {
80 // Not configured.
81 return;
82 };
83
84 let handler = linux_api::signal::SignalHandler::Action(handle_timer_signal);
85 let flags = SigActionFlags::SA_SIGINFO | SigActionFlags::SA_ONSTACK;
86 let mask = sigset_t::EMPTY;
87 let action = linux_api::signal::sigaction::new_with_default_restorer(handler, flags, mask);
88 unsafe { linux_api::signal::rt_sigaction(PREEMPTION_SIGNAL, &action, None).unwrap() };
89}
90
91/// Disable preemption for the current thread.
92pub fn disable() {
93 debug_assert_eq!(ExecutionContext::current(), ExecutionContext::Shadow);
94 let Some(manager_shmem) = &crate::global_manager_shmem::try_get() else {
95 // Not initialized yet. e.g. we get here the first time we enter the
96 // Shadow execution context, before completing initialization.
97 // In any case, there should be nothing to disable.
98 return;
99 };
100 let FfiOption::Some(_config) = &manager_shmem.native_preemption_config else {
101 // Not configured.
102 return;
103 };
104
105 log::trace!("Disabling preemption");
106
107 // Disable the itimer, effectively discarding any CPU-time we've spent.
108 //
109 // Functionality-wise this isn't *strictly* required for purposes of
110 // supporting cpu-only-busy-loop-escape, since we also block the signal
111 // below. However we currently want to minimize the effects of this feature
112 // on the simulation, and hence don't want to "accumulate" progress towards
113 // the timer firing and then cause regular preemptions even in the absence
114 // of long cpu-only operations.
115 //
116 // Allowing such accumulation is also undesirable since we currently use a
117 // process-wide itimer, with the `ITIMER_VIRTUAL` clock that measures
118 // process-wide CPU time. Hence time spent running in one thread without
119 // the timer firing would bring *all* threads in that process closer to
120 // firing. That issue *could* be addressed by using `timer_create` timers,
121 // which support a thread-cpu-time clock `CLOCK_THREAD_CPUTIME_ID`.
122 let zero = linux_api::time::kernel_old_timeval {
123 tv_sec: 0,
124 tv_usec: 0,
125 };
126 linux_api::time::setitimer(
127 linux_api::time::ITimerId::ITIMER_VIRTUAL,
128 &linux_api::time::kernel_old_itimerval {
129 it_interval: zero,
130 it_value: zero,
131 },
132 None,
133 )
134 .unwrap();
135
136 // Block the timer's signal for this thread.
137 // We're using a process-wide signal, so need to do this to ensure *this*
138 // thread doesn't get awoken if shadow ends up suspending this thread and
139 // running another, and that thread re-enables the timer and has it fire.
140 //
141 // We *could* consider using timers created via `timer_create`, which
142 // supports being configured to fire thread-targeted signals, and thus
143 // wouldn't require us to unblock and re-block the signal when enabling and
144 // disabling. However, we'd probably then want to *destroy* the timer when
145 // disabling, and re-create when enabling, to avoid bumping into the system
146 // limit on the number of such timers (and any potential undocumented
147 // scalability issues with having a large number of such timers). This is
148 // likely to be at least as expensive as blocking and unblocking the signal.
149 linux_api::signal::rt_sigprocmask(
150 linux_api::signal::SigProcMaskAction::SIG_BLOCK,
151 &PREEMPTION_SIGNAL.into(),
152 None,
153 )
154 .unwrap();
155}
156
157/// Enable preemption for the current thread.
158///
159/// # Safety
160///
161/// Preemption must not currently be enabled for any other threads in the current process.
162pub unsafe fn enable() {
163 debug_assert_eq!(ExecutionContext::current(), ExecutionContext::Shadow);
164 let FfiOption::Some(config) = &crate::global_manager_shmem::get().native_preemption_config
165 else {
166 return;
167 };
168 log::trace!(
169 "Enabling preemption with native duration {:?}",
170 config.native_duration
171 );
172 linux_api::time::setitimer(
173 linux_api::time::ITimerId::ITIMER_VIRTUAL,
174 &linux_api::time::kernel_old_itimerval {
175 // We *usually* don't need the timer to repeat, since normally it'll
176 // fire when we're in the managed application context, and the
177 // signal handler will cause the timer to be re-armed after
178 // finishing. However there are some edge cases where the timer can
179 // fire while in the shim context, in which case the signal handler
180 // just returns, and the timer won't be re-armed. We *could*
181 // explicitly re-arm the timer there, but probably more robust to
182 // just have an interval here.
183 it_interval: config.native_duration,
184 it_value: config.native_duration,
185 },
186 None,
187 )
188 .unwrap();
189 // Allow this thread to receive the preemption signal, which we would have
190 // blocked in the last call to `disable`.
191 linux_api::signal::rt_sigprocmask(
192 linux_api::signal::SigProcMaskAction::SIG_UNBLOCK,
193 &PREEMPTION_SIGNAL.into(),
194 None,
195 )
196 .unwrap();
197}
198
199mod export {
200 use super::*;
201
202 #[unsafe(no_mangle)]
203 pub extern "C-unwind" fn preempt_process_init() {
204 process_init();
205 }
206}