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}