Skip to main content

ax_task/sched/system/task_system/park_exit/
exit.rs

1//! Exit under the owning scheduler transaction.
2
3use super::*;
4
5impl TaskSystem {
6    /// Validates all fallible current-thread exit prerequisites without
7    /// publishing the thread as exited.
8    pub(crate) fn prepare_current_exit(
9        &self,
10        cpu: Pin<&mut CpuLocal>,
11        current: &ThreadHandle,
12    ) -> Result<CurrentExitPermit, TaskError> {
13        self.prepare_current_exit_inner(cpu, current, true)
14    }
15
16    pub(in crate::sched::system::task_system) fn prepare_current_exit_inner(
17        &self,
18        mut cpu: Pin<&mut CpuLocal>,
19        current: &ThreadHandle,
20        require_runtime_context: bool,
21    ) -> Result<CurrentExitPermit, TaskError> {
22        self.ensure_owner_cpu_context(&cpu)?;
23        self.drain_owner_work(cpu.as_mut())?;
24        let current_id = current.id();
25        if cpu.remote().idle_thread() == Some(current_id) {
26            return Err(TaskError::InvalidConfiguration);
27        }
28        let current_core = Arc::clone(current.runtime_core_arc());
29        // Close before taking registry or thread-state locks. An activity that
30        // won before this edge may need either lock to finish, just as Linux
31        // takes p->pi_lock before rq/task-state validation rather than waiting
32        // for a reader while holding rq.
33        let scheduler_exit = current_core
34            .close_owned_scheduler_activity()
35            .ok_or(TaskError::ThreadBusy)?;
36        let state = self.state.lock();
37        state.ensure_cpu_online(&cpu)?;
38        let record = state.thread_record(current_id)?;
39        if !Arc::ptr_eq(&record.core, &current_core) {
40            return Err(TaskError::StaleThreadId);
41        }
42        let sched = record.sched.lock();
43        let placement = record.sched.placement();
44        let lifecycle = sched.lifecycle.state();
45        if lifecycle != ThreadState::Running {
46            return Err(TaskError::InvalidTransition {
47                from: lifecycle,
48                to: ThreadState::Exited,
49            });
50        }
51        if sched.pi.blocked_on.is_some() || !sched.pi.donors.is_empty() {
52            return Err(TaskError::InvalidPiState);
53        }
54        if placement.queued_cpu() != Some(cpu.owner()) || placement.on_cpu() != Some(cpu.owner()) {
55            return Err(TaskError::ThreadBusy);
56        }
57        if require_runtime_context && record.resources.context().is_none() {
58            return Err(TaskError::InvalidRuntimeHandle);
59        }
60        record.callbacks.validate_prepare_exit()?;
61        Ok(CurrentExitPermit {
62            scheduler_exit,
63            current_core,
64        })
65    }
66
67    /// Atomically prepares and commits current-thread exit.
68    ///
69    /// Runtime integrations that publish OS completion between those phases
70    /// use the crate-private prepared form instead.
71    pub fn exit_current(
72        &self,
73        mut cpu: Pin<&mut CpuLocal>,
74        current: ThreadHandle,
75    ) -> Result<ScheduleDecision, TaskError> {
76        // Pure scheduler users may model a transition without installing an
77        // architecture context. The runtime facade uses the stricter prepared
78        // form before publishing OS-visible completion.
79        let permit = self.prepare_current_exit_inner(cpu.as_mut(), &current, false)?;
80        // The architecture current entry no longer needs a lookup lease once
81        // the permit pins its core. Release it before publishing Exited so its
82        // eventual lease drop cannot manufacture pre-switch-tail reap work.
83        drop(current);
84        self.commit_current_exit_after_owner_drain(cpu, permit)
85    }
86
87    /// Commits a prepared current-thread exit and selects a replacement.
88    /// Commits a prepared exit while the runtime owns the IRQ-off scheduler baton.
89    ///
90    /// # Safety
91    ///
92    /// The scheduler frame must remain active until this function returns.
93    pub(crate) unsafe fn commit_prepared_current_exit(
94        &self,
95        cpu: Pin<&mut CpuLocal>,
96        permit: CurrentExitPermit,
97    ) -> ScheduleDecision {
98        let exiting = permit.thread();
99        if self.ensure_owner_cpu_context(&cpu).is_err()
100            || cpu.as_ref().get_ref().switch_handoff().is_some()
101        {
102            task_runtime::fatal_invariant(0x4558_0014, exiting.as_u64() as usize);
103        }
104        self.commit_current_exit_owner(cpu, permit, OwnerRqEntry::SchedulerFrame)
105            .unwrap_or_else(|_| {
106                task_runtime::fatal_invariant(0x4558_0015, exiting.as_u64() as usize)
107            })
108    }
109
110    /// Commits the non-returning half of current exit after owner work drained.
111    ///
112    /// The move-only permit has already closed new scheduler activity. A
113    /// message whose delivery reservation predates that close remains an
114    /// in-flight late delivery and pins registry resources until its owner
115    /// drains it as an exited no-op.
116    pub(in crate::sched::system::task_system) fn commit_current_exit_after_owner_drain(
117        &self,
118        cpu: Pin<&mut CpuLocal>,
119        permit: CurrentExitPermit,
120    ) -> Result<ScheduleDecision, TaskError> {
121        self.commit_current_exit_owner(cpu, permit, OwnerRqEntry::IrqSave)
122    }
123
124    pub(super) fn commit_current_exit_owner(
125        &self,
126        mut cpu: Pin<&mut CpuLocal>,
127        mut permit: CurrentExitPermit,
128        rq_entry: OwnerRqEntry,
129    ) -> Result<ScheduleDecision, TaskError> {
130        let exiting = permit.thread();
131        let exited_core = Arc::clone(permit.current_core());
132        {
133            let state = self.state.lock();
134            state.ensure_cpu_online(&cpu)?;
135            let record = state.thread_record(exiting)?;
136            if !Arc::ptr_eq(&record.core, &exited_core) {
137                return Err(TaskError::StaleThreadId);
138            }
139            if record.has_live_pi_edges() {
140                return Err(TaskError::InvalidPiState);
141            }
142            record.callbacks.validate_prepare_exit()?;
143        }
144
145        // SAFETY: the owner borrow pins the CpuLocal and its immutable remote
146        // endpoint while this exit transaction and switch tail are live.
147        let remote = unsafe { cpu.as_ref().get_ref().remote_for_owner() };
148        let initial_request = remote.claim_scheduler_request(SchedulerRequestScope::All);
149        // SAFETY: propagated from the selected entry contract.
150        let mut exited_sched = unsafe { rq_entry.lock_thread_sched(exited_core.sched()) };
151        // SAFETY: propagated from the selected entry contract.
152        let mut transaction = unsafe { rq_entry.begin(self, remote) };
153        let now_ns = transaction.clock().wall().as_nanos();
154        if transaction.current_thread() != Some(exiting)
155            || transaction
156                .current_core()
157                .is_none_or(|core| !Arc::ptr_eq(&core, &exited_core))
158        {
159            transaction.adopt_scheduler_request(initial_request);
160            transaction.commit_and_finish_scheduler_request();
161            return Err(TaskError::StaleThreadId);
162        }
163        transaction.adopt_scheduler_request(initial_request);
164        transaction.merge_scheduler_request(SchedulerRequestScope::All);
165        let dispatch_commit = self.settle_owner_current_dispatch_in_rq(&mut transaction);
166        // Exit necessarily selects a replacement, so accounting requests from
167        // the outgoing task are consumed by this decision.
168        transaction.merge_scheduler_request(SchedulerRequestScope::All);
169        let previous_endpoint = transaction.current_switch_endpoint().unwrap_or_else(|| {
170            task_runtime::fatal_invariant(0x4558_0007, exiting.as_u64() as usize)
171        });
172        let previous_urgency = transaction.current_scheduling_urgency().unwrap_or_else(|| {
173            task_runtime::fatal_invariant(0x4558_0007, exiting.as_u64() as usize)
174        });
175        let held_reservation = {
176            let placement = exited_core.sched().placement();
177            let sched = &mut *exited_sched;
178            if sched.lifecycle.state() != ThreadState::Running
179                || placement.queued_cpu() != Some(cpu.owner())
180                || placement.on_cpu() != Some(cpu.owner())
181            {
182                task_runtime::fatal_invariant(0x4558_1101, exiting.as_u64() as usize);
183            }
184            Self::detach_owner_deadline_bandwidth_in_rq(
185                &exited_core,
186                sched,
187                cpu.remote(),
188                &mut transaction,
189            );
190            if transaction.is_linked_current(exiting) {
191                transaction.deactivate_task(exiting);
192            } else {
193                transaction.deactivate_unlinked_current(exiting);
194            }
195            if sched.transition(&exited_core, ThreadState::Exited).is_err() {
196                task_runtime::fatal_invariant(0x4558_0001, exiting.as_u64() as usize);
197            }
198            // Exit removes rq ownership immediately. The outgoing execution
199            // claim remains in `on_cpu` until the per-CPU switch handoff tail
200            // releases it, exactly like Linux `do_task_dead()` followed by
201            // `finish_task_switch()`.
202            placement.block_current(cpu.owner());
203            permit.seal();
204            let held = sched.held_deadline_reservation();
205            sched.deadline.bandwidth.replace_detached_reservation(0);
206            sched.policy.discard_pending_update();
207            held
208        };
209        transaction.take_current();
210        let next = self.pick_owner_next_in_rq(cpu.as_mut(), &mut transaction, None);
211        let OwnerNext {
212            core: next_core,
213            policy: next_policy_ref,
214            urgency: next_urgency,
215        } = next;
216        let next_endpoint = transaction.current_switch_endpoint().unwrap_or_else(|| {
217            task_runtime::fatal_invariant(0x4558_0008, next_core.as_ref().id().as_u64() as usize)
218        });
219        let handoff = Self::prepare_switch_handoff(
220            Some(exiting),
221            Some(PreviousSwitchOwnership::retained(Arc::clone(&exited_core))),
222            next_core,
223            next_policy_ref,
224            PreviousSwitchDisposition::Exited,
225            None,
226        );
227        let deadline_rq_observation =
228            transaction.scheduler_deadline_rq_observation(cpu.as_ref().get_ref());
229        self.commit_owner_switch_selection(cpu.as_mut(), transaction, handoff, false);
230        drop(exited_sched);
231        self.finish_owner_dispatch_commit(dispatch_commit);
232
233        {
234            let mut state = self.state.lock();
235            let record = state.thread_record_mut(exiting).unwrap_or_else(|_| {
236                task_runtime::fatal_invariant(0x4558_0002, exiting.as_u64() as usize)
237            });
238            if record
239                .callbacks
240                .prepare_exit(record.extension.is_some())
241                .is_err()
242            {
243                task_runtime::fatal_invariant(0x4558_0003, exiting.as_u64() as usize);
244            }
245            state.queue_exited_thread(exiting);
246        }
247        self.root_domain.lock().release_deadline(held_reservation);
248        exited_core.notify_affinity_waiters();
249        drop(permit);
250        self.finish_owner_selection(
251            cpu.as_mut(),
252            Some(previous_endpoint.thread()),
253            next_endpoint.thread(),
254            Some(previous_urgency),
255            next_urgency,
256            OwnerSchedulerDeadline::Reevaluate(deadline_rq_observation),
257        );
258        let decision = Self::owner_switch_plan(
259            Some(previous_endpoint),
260            next_endpoint,
261            SwitchReason::Exited,
262            now_ns,
263        );
264        Ok(decision)
265    }
266}