Skip to main content

strat9_kernel/process/scheduler/
core_impl.rs

1use super::{runtime_ops::idle_task_main, *};
2
3/// Create a `SchedulerCpu` for the given CPU index (creates its idle task).
4pub(super) fn create_cpu_scheduler(cpu_idx: usize) -> SchedulerCpu {
5    crate::serial_println!(
6        "[trace][sched] create_cpu_scheduler cpu={} create idle begin",
7        cpu_idx
8    );
9    let idle_task = Task::new_kernel_task(idle_task_main, "idle", TaskPriority::Idle)
10        .expect("Failed to create idle task");
11    crate::serial_println!(
12        "[trace][sched] create_cpu_scheduler cpu={} create idle done id={}",
13        cpu_idx,
14        idle_task.id.as_u64()
15    );
16    idle_task.set_sched_policy(crate::process::sched::SchedPolicy::Idle);
17    let mut class_rqs = PerCpuClassRqSet::new();
18    class_rqs.enqueue(crate::process::sched::SchedClassId::Idle, idle_task.clone());
19    SchedulerCpu {
20        class_rqs,
21        current_task: None,
22        current_runtime: crate::process::sched::CurrentRuntime::new(),
23        idle_task,
24        task_to_requeue: None,
25        task_to_drop: None,
26        need_resched: false,
27        class_table: crate::process::sched::SchedClassTable::default(),
28    }
29}
30
31impl GlobalSchedState {
32    /// Create a new global scheduler state (no per-CPU runqueues : those live in LOCAL_SCHEDULERS).
33    pub fn new() -> Self {
34        crate::serial_println!("[trace][sched] GlobalSchedState::new enter");
35        GlobalSchedState {
36            all_tasks: BTreeMap::new(),
37            all_tasks_scan: Vec::new(),
38            task_cpu: BTreeMap::new(),
39            wake_deadlines: BTreeMap::new(),
40            wake_deadline_of: BTreeMap::new(),
41            zombies: BTreeMap::new(),
42            class_table: crate::process::sched::SchedClassTable::default(),
43        }
44    }
45
46    /// Performs the member add operation.
47    pub(crate) fn member_add(
48        map: &mut BTreeMap<Pid, alloc::vec::Vec<TaskId>>,
49        key: Pid,
50        task_id: TaskId,
51    ) {
52        let members = map.entry(key).or_default();
53        if !members.iter().any(|id| *id == task_id) {
54            members.push(task_id);
55        }
56    }
57
58    /// Performs the member remove operation.
59    pub(crate) fn member_remove(
60        map: &mut BTreeMap<Pid, alloc::vec::Vec<TaskId>>,
61        key: Pid,
62        task_id: TaskId,
63    ) {
64        let mut clear = false;
65        if let Some(members) = map.get_mut(&key) {
66            members.retain(|id| *id != task_id);
67            clear = members.is_empty();
68        }
69        if clear {
70            map.remove(&key);
71        }
72    }
73
74    /// Performs the register identity locked operation.
75    pub(crate) fn register_identity_locked(identity: &mut SchedIdentity, task: &Arc<Task>) {
76        let task_id = task.id;
77        let pid = task.pid;
78        let pgid = task.pgid.load(Ordering::Relaxed);
79        let sid = task.sid.load(Ordering::Relaxed);
80        crate::serial_println!(
81            "[trace][sched] register_identity enter tid={} pid={} pgid={} sid={}",
82            task_id.as_u64(),
83            pid,
84            pgid,
85            sid
86        );
87        identity.pid_to_pgid.insert(pid, pgid);
88        crate::serial_println!(
89            "[trace][sched] register_identity pid_to_pgid inserted pid={}",
90            pid
91        );
92        identity.pid_to_sid.insert(pid, sid);
93        crate::serial_println!(
94            "[trace][sched] register_identity pid_to_sid inserted pid={}",
95            pid
96        );
97        Self::member_add(&mut identity.pgid_members, pgid, task_id);
98        Self::member_add(&mut identity.sid_members, sid, task_id);
99        crate::serial_println!(
100            "[trace][sched] register_identity done tid={}",
101            task_id.as_u64()
102        );
103    }
104
105    /// Performs the unregister identity locked operation.
106    pub(crate) fn unregister_identity_locked(
107        identity: &mut SchedIdentity,
108        task_id: TaskId,
109        pid: Pid,
110        tid: Tid,
111    ) {
112        identity.pid_to_task.remove(&pid);
113        identity.tid_to_task.remove(&tid);
114        if let Some(pgid) = identity.pid_to_pgid.remove(&pid) {
115            Self::member_remove(&mut identity.pgid_members, pgid, task_id);
116        }
117        if let Some(sid) = identity.pid_to_sid.remove(&pid) {
118            Self::member_remove(&mut identity.sid_members, sid, task_id);
119        }
120    }
121
122    /// Add a task to the scheduler
123    pub fn add_task(&mut self, task: Arc<Task>) -> Option<usize> {
124        let cpu_index = self.select_cpu_for_task();
125        self.add_task_on_cpu(task, cpu_index)
126    }
127
128    /// Performs the add task with parent operation.
129    pub fn add_task_with_parent(&mut self, task: Arc<Task>, parent: TaskId) -> Option<usize> {
130        let child = task.id;
131        let cpu_index = self.select_cpu_for_task();
132        let ipi = self.add_task_on_cpu(task, cpu_index);
133        {
134            let mut identity = SCHED_IDENTITY.write();
135            identity.parent_of.insert(child, parent);
136            identity.children_of.entry(parent).or_default().push(child);
137        }
138        ipi
139    }
140
141    /// Performs the add task on cpu operation.
142    fn add_task_on_cpu(&mut self, task: Arc<Task>, cpu_index: usize) -> Option<usize> {
143        let task_id = task.id;
144        crate::serial_println!(
145            "[trace][sched] add_task_on_cpu enter tid={} cpu={}",
146            task_id.as_u64(),
147            cpu_index
148        );
149        task.set_state(TaskState::Ready);
150        crate::serial_println!(
151            "[trace][sched] add_task_on_cpu state ready tid={}",
152            task_id.as_u64()
153        );
154
155        crate::serial_println!(
156            "[trace][sched] add_task_on_cpu before clone tid={} all_tasks_len={}",
157            task_id.as_u64(),
158            self.all_tasks.len()
159        );
160        let task_clone = task.clone();
161        crate::serial_println!(
162            "[trace][sched] add_task_on_cpu before all_tasks.insert tid={}",
163            task_id.as_u64()
164        );
165        self.insert_all_task_locked(task_id, task_clone);
166        crate::serial_println!(
167            "[trace][sched] add_task_on_cpu all_tasks inserted tid={}",
168            task_id.as_u64()
169        );
170        self.task_cpu.insert(task_id, cpu_index);
171        task.home_cpu
172            .store(cpu_index, core::sync::atomic::Ordering::Relaxed);
173        crate::serial_println!(
174            "[trace][sched] add_task_on_cpu task_cpu inserted tid={}",
175            task_id.as_u64()
176        );
177        {
178            let mut identity = SCHED_IDENTITY.write();
179            identity.pid_to_task.insert(task.pid, task_id);
180            crate::serial_println!(
181                "[trace][sched] add_task_on_cpu pid map inserted tid={}",
182                task_id.as_u64()
183            );
184            identity.tid_to_task.insert(task.tid, task_id);
185            crate::serial_println!(
186                "[trace][sched] add_task_on_cpu tid map inserted tid={}",
187                task_id.as_u64()
188            );
189            Self::register_identity_locked(&mut identity, &task);
190            crate::serial_println!(
191                "[trace][sched] add_task_on_cpu identity registered tid={}",
192                task_id.as_u64()
193            );
194        }
195        {
196            let class = self.class_table.class_for_task(&task);
197            if let Some(ref mut local_cpu) = *LOCAL_SCHEDULERS[cpu_index].lock() {
198                local_cpu.class_rqs.enqueue(class, task);
199                local_cpu.need_resched = true;
200                crate::serial_println!(
201                    "[trace][sched] add_task_on_cpu enqueued tid={} cpu={}",
202                    task_id.as_u64(),
203                    cpu_index
204                );
205            }
206        }
207        sched_trace(format_args!(
208            "enqueue task={} cpu={}",
209            task_id.as_u64(),
210            cpu_index
211        ));
212        if cpu_index != current_cpu_index() {
213            Some(cpu_index)
214        } else {
215            None
216        }
217    }
218
219    pub(super) fn insert_all_task_locked(&mut self, task_id: TaskId, task: Arc<Task>) {
220        assert_eq!(
221            task.id,
222            task_id,
223            "scheduler corruption: insert_all_task_locked task.id={} != task_id={}",
224            task.id.as_u64(),
225            task_id.as_u64()
226        );
227        if self.all_tasks.contains_key(&task_id) {
228            unsafe {
229                core::arch::asm!("mov al, 'D'; out 0xe9, al", out("al") _);
230            }
231            crate::serial_force_println!(
232                "[RACE] insert_all_task_locked: duplicate tid={} all_tasks={} all_tasks_scan={}",
233                task_id.as_u64(),
234                self.all_tasks.len(),
235                self.all_tasks_scan.len()
236            );
237            panic!(
238                "scheduler corruption: duplicate insert_all_task_locked tid={}",
239                task_id.as_u64()
240            );
241        }
242        self.all_tasks.insert(task_id, task.clone());
243        self.all_tasks_scan.push(task);
244        // Race/corruption diagnostic: all_tasks and all_tasks_scan must stay in sync.
245        let bt_len = self.all_tasks.len();
246        let scan_len = self.all_tasks_scan.len();
247        if bt_len != scan_len {
248            unsafe {
249                core::arch::asm!("mov al, 'X'; out 0xe9, al", out("al") _);
250            }
251            crate::serial_force_println!(
252                "[RACE] insert_all_task_locked: all_tasks={} != all_tasks_scan={} tid={}",
253                bt_len,
254                scan_len,
255                task_id.as_u64()
256            );
257            panic!(
258                "scheduler corruption: insert_all_task_locked len mismatch all_tasks={} all_tasks_scan={} tid={}",
259                bt_len,
260                scan_len,
261                task_id.as_u64()
262            );
263        }
264    }
265
266    pub(super) fn remove_all_task_locked(&mut self, task_id: TaskId) -> Option<Arc<Task>> {
267        let removed = self.all_tasks.remove(&task_id);
268        if removed.is_some() {
269            if let Some(idx) = self
270                .all_tasks_scan
271                .iter()
272                .position(|task| task.id == task_id)
273            {
274                self.all_tasks_scan.swap_remove(idx);
275            } else {
276                unsafe { core::arch::asm!("mov al, 'Z'; out 0xe9, al", out("al") _) };
277                crate::serial_force_println!(
278                    "[RACE] remove_all_task_locked: tid={} in all_tasks but NOT in all_tasks_scan",
279                    task_id.as_u64()
280                );
281                panic!(
282                    "scheduler corruption: remove_all_task_locked missing scan entry tid={}",
283                    task_id.as_u64()
284                );
285            }
286        }
287        let bt_len = self.all_tasks.len();
288        let scan_len = self.all_tasks_scan.len();
289        if bt_len != scan_len {
290            unsafe {
291                core::arch::asm!("mov al, 'X'; out 0xe9, al", out("al") _);
292            }
293            crate::serial_force_println!(
294                "[RACE] remove_all_task_locked: all_tasks={} != all_tasks_scan={} tid={}",
295                bt_len,
296                scan_len,
297                task_id.as_u64()
298            );
299            panic!(
300                "scheduler corruption: remove_all_task_locked len mismatch all_tasks={} all_tasks_scan={} tid={}",
301                bt_len,
302                scan_len,
303                task_id.as_u64()
304            );
305        }
306        removed
307    }
308
309    /// Performs the clear task wake deadline locked operation.
310    pub fn clear_task_wake_deadline_locked(&mut self, id: TaskId) -> bool {
311        if let Some(task) = self.all_tasks.get(&id) {
312            task.wake_deadline_ns.store(0, Ordering::Relaxed);
313            true
314        } else {
315            false
316        }
317    }
318
319    /// Sets task wake deadline locked.
320    pub fn set_task_wake_deadline_locked(&mut self, id: TaskId, deadline: u64) -> bool {
321        if deadline == 0 {
322            return self.clear_task_wake_deadline_locked(id);
323        }
324        if let Some(task) = self.all_tasks.get(&id) {
325            task.wake_deadline_ns.store(deadline, Ordering::Relaxed);
326            true
327        } else {
328            false
329        }
330    }
331
332    /// Performs the wake task locked operation.
333    ///
334    /// Returns `(was_woken, ipi_cpu)`. The caller must send a resched IPI to
335    /// `ipi_cpu` after releasing the scheduler lock.
336    ///
337    /// NOTE: `blocked_tasks` lives in the separate `BLOCKED_TASKS` lock.
338    /// This method handles waking the task directly if it is already in `BLOCKED_TASKS`,
339    /// or setting `wake_pending` as a fallback if the task is still transitioning to Blocked.
340    pub fn wake_task_locked(&mut self, id: TaskId) -> (bool, Option<usize>) {
341        self.clear_task_wake_deadline_locked(id);
342
343        // Check if the task is already in BLOCKED_TASKS first to wake it directly.
344        let mut woken = false;
345        let mut ipi_cpu = None;
346        {
347            let mut blocked = super::BLOCKED_TASKS.lock();
348            if let Some(task) = blocked.remove(&id) {
349                task.set_state(TaskState::Ready);
350                let home = task.home_cpu.load(core::sync::atomic::Ordering::Relaxed);
351                let cpu_index = if home != usize::MAX { home } else { 0 };
352
353                let class = {
354                    use crate::process::sched::SchedClassId;
355                    match task.sched_policy() {
356                        crate::process::sched::SchedPolicy::RealTimeRR { .. }
357                        | crate::process::sched::SchedPolicy::RealTimeFifo { .. } => {
358                            SchedClassId::RealTime
359                        }
360                        crate::process::sched::SchedPolicy::Fair(_) => SchedClassId::Fair,
361                        crate::process::sched::SchedPolicy::Idle => SchedClassId::Idle,
362                    }
363                };
364
365                if let Some(ref mut local_cpu) = *super::LOCAL_SCHEDULERS[cpu_index].lock() {
366                    local_cpu.class_rqs.enqueue(class, task.clone());
367                    local_cpu.need_resched = true;
368                }
369
370                ipi_cpu = if cpu_index != current_cpu_index() {
371                    Some(cpu_index)
372                } else {
373                    None
374                };
375                woken = true;
376            }
377        }
378
379        if woken {
380            return (true, ipi_cpu);
381        }
382
383        // Fallback: task is not yet in BLOCKED_TASKS (still transitioning to
384        // Blocked). Set wake_pending so block_current_task will skip blocking.
385        if let Some(task) = self.all_tasks.get(&id) {
386            task.wake_pending
387                .store(true, core::sync::atomic::Ordering::Release);
388            (true, None)
389        } else {
390            (false, None)
391        }
392    }
393
394    /// Attempts to reap child locked.
395    /// If `target` is `Some(tid)`, only reaps that child; otherwise, reaps any child.
396    /// Returns `WaitChildResult` indicating the outcome.
397    /// Must be called with the scheduler lock held.
398    ///
399    pub fn try_reap_child_locked(
400        &mut self,
401        parent: TaskId,
402        target: Option<TaskId>,
403    ) -> WaitChildResult {
404        // First, check children under SCHED_IDENTITY lock.
405        let target_is_child = {
406            let identity = SCHED_IDENTITY.read();
407            let Some(children_view) = identity.children_of.get(&parent) else {
408                return WaitChildResult::NoChildren;
409            };
410
411            if children_view.is_empty() {
412                return WaitChildResult::NoChildren;
413            }
414
415            let target_is_child = if let Some(target_id) = target {
416                children_view.iter().any(|&id| id == target_id)
417            } else {
418                true
419            };
420            target_is_child
421        };
422
423        if !target_is_child {
424            return WaitChildResult::NoChildren;
425        }
426
427        // Find the zombie child : re-check children under SCHED_IDENTITY.
428        let zombie = {
429            let identity = SCHED_IDENTITY.read();
430            let children = match identity.children_of.get(&parent) {
431                Some(c) => c.clone(),
432                None => return WaitChildResult::NoChildren,
433            };
434            children
435                .iter()
436                .copied()
437                .find(|id| target.map_or(true, |t| t == *id) && self.zombies.contains_key(id))
438        };
439
440        if let Some(child) = zombie {
441            let (status, child_pid) = self.zombies.remove(&child).unwrap_or((0, 0));
442            // Remove from all_tasks now so that pick_next_task (if it races with
443            // reaping) will see was_registered=false and skip cleanup_task_resources.
444            let reaped_task = self.remove_all_task_locked(child);
445            if let Some(task) = reaped_task.as_ref() {
446                super::task_ops::cleanup_task_resources(task);
447            }
448            let child_tid = reaped_task.as_ref().map(|t| t.tid);
449            if child_pid != 0 {
450                if let Some(tid) = child_tid {
451                    let mut identity = SCHED_IDENTITY.write();
452                    Self::unregister_identity_locked(&mut identity, child, child_pid, tid);
453                }
454            }
455            {
456                let mut identity = SCHED_IDENTITY.write();
457                if let Some(children) = identity.children_of.get_mut(&parent) {
458                    children.retain(|&id| id != child);
459                    if children.is_empty() {
460                        identity.children_of.remove(&parent);
461                    }
462                }
463                identity.parent_of.remove(&child);
464            }
465            return WaitChildResult::Reaped {
466                child,
467                pid: child_pid,
468                status,
469            };
470        }
471
472        WaitChildResult::StillRunning
473    }
474
475    /// Select the least-loaded CPU for a newly created task.
476    ///
477    /// Uses **blocking** `LOCAL_SCHEDULERS[i].lock()` for each CPU while
478    /// `GLOBAL_SCHED_STATE` is already held by the caller.  This is safe because the
479    /// hot-path only ever does `try_lock_no_irqsave` on `GLOBAL_SCHED_STATE` (so it
480    /// cannot deadlock with us), but it may briefly stall behind a timer tick
481    /// that holds a LOCAL lock.  The stall is bounded by one tick period.
482    fn select_cpu_for_task(&self) -> usize {
483        // Early boot: before the first real task is running, keep all new tasks
484        // on the BSP. Spreading init/shell/status across CPUs at this point can
485        // strand boot-critical work on AP scheduler instances that have not yet
486        // entered their steady-state scheduling loop.
487        let n = active_cpu_count();
488        let all_idle = (0..n).all(|i| {
489            LOCAL_SCHEDULERS[i]
490                .lock()
491                .as_ref()
492                .map(|cpu| cpu.current_task.is_none())
493                .unwrap_or(true)
494        });
495        if all_idle {
496            crate::serial_println!("[trace][sched] select_cpu_for_task early-boot best=0");
497            return 0;
498        }
499        let mut best = 0usize;
500        let mut best_load = usize::MAX;
501        for idx in 0..n {
502            let load = {
503                let guard = LOCAL_SCHEDULERS[idx].lock();
504                if let Some(ref cpu) = *guard {
505                    let mut l = cpu.class_rqs.runnable_len();
506                    if let Some(current) = cpu.current_task.as_ref() {
507                        if self.class_table.class_for_task(current)
508                            != crate::process::sched::SchedClassId::Idle
509                        {
510                            l += 1;
511                        }
512                    }
513                    l
514                } else {
515                    0
516                }
517            };
518            if load < best_load {
519                best = idx;
520                best_load = load;
521            }
522        }
523        crate::serial_println!(
524            "[trace][sched] select_cpu_for_task best={} load={}",
525            best,
526            best_load
527        );
528        best
529    }
530
531    /// Performs the migrate ready tasks for new class table operation.
532    pub fn migrate_ready_tasks_for_new_class_table(&mut self) {
533        let mut ready: Vec<(TaskId, Arc<Task>, usize)> = Vec::new();
534        for (id, task) in self.all_tasks.iter() {
535            let state = task.get_state();
536            if state != TaskState::Ready {
537                continue;
538            }
539            let cpu = self.task_cpu.get(id).copied().unwrap_or(0);
540            ready.push((*id, task.clone(), cpu));
541        }
542
543        for (id, task, cpu_idx) in ready {
544            let mut guard = LOCAL_SCHEDULERS[cpu_idx].lock();
545            let Some(ref mut cpu) = *guard else {
546                continue;
547            };
548            if cpu.class_rqs.remove(id) {
549                let class = self.class_table.class_for_task(&task);
550                cpu.class_rqs.enqueue(class, task);
551                cpu.need_resched = true;
552            }
553        }
554    }
555}
556
557//  Per-CPU hot-path helpers
558//
559// These functions operate primarily on `SchedulerCpu` (acquired via
560// `LOCAL_SCHEDULERS[cpu_index]`).  Most never touch the global `SCHEDULER`
561// lock.  The one exception is `steal_task_local`, which does a **non-blocking**
562// `GLOBAL_SCHED_STATE.try_lock_no_irqsave()` to update `task_cpu` after a successful
563// steal.  This is an intentional lock-order inversion (LOCAL held, then GLOBAL
564// attempted) that is safe because the try-lock never blocks : if GLOBAL_SCHED_STATE is
565// contended, we simply skip stealing.
566//
567// Lock order for steal: own LOCAL held → try_lock GLOBAL → try_lock sibling
568// LOCALs.  Never blocking-wait, so no deadlock possible.
569
570/// Steal a task from the busiest sibling CPU using per-CPU LOCAL locks.
571///
572/// Called with `cpu` borrowed from `LOCAL_SCHEDULERS[cpu_index]` (our own
573/// LOCAL lock already held). Uses `try_lock_no_irqsave` on sibling entries :
574/// if a sibling or the global scheduler state is contended, we skip stealing
575/// rather than waiting.
576pub(super) fn steal_task_local(cpu: &mut SchedulerCpu, cpu_index: usize) -> Option<Arc<Task>> {
577    let now_tick = TICK_COUNT.load(Ordering::Relaxed);
578    if now_tick < LAST_STEAL_TICK[cpu_index].load(Ordering::Relaxed) + STEAL_COOLDOWN_TICKS {
579        return None;
580    }
581
582    // Best-effort only: if a cold path is holding the global scheduler, skip
583    // stealing instead of blocking the hot path.
584    let mut scheduler = GLOBAL_SCHED_STATE.try_lock_no_irqsave()?;
585    let sched = scheduler.as_mut()?;
586
587    let n = active_cpu_count();
588    let my_load = cpu.class_rqs.runnable_len();
589
590    let mut best_cpu = None;
591    let mut best_load = 0usize;
592
593    for i in 0..n {
594        if i == cpu_index {
595            continue;
596        }
597        // try_lock_no_irqsave: returns immediately if contended (no deadlock).
598        if let Some(guard) = LOCAL_SCHEDULERS[i].try_lock_no_irqsave() {
599            if let Some(ref sib) = *guard {
600                let load = sib.class_rqs.runnable_len();
601                if load > best_load {
602                    best_load = load;
603                    best_cpu = Some(i);
604                }
605            }
606        }
607    }
608
609    if best_load < my_load.saturating_add(STEAL_IMBALANCE_MIN) {
610        return None;
611    }
612    let steal_from = best_cpu?;
613
614    // Re-acquire the sibling lock to perform the steal.
615    if let Some(mut guard) = LOCAL_SCHEDULERS[steal_from].try_lock_no_irqsave() {
616        if let Some(ref mut sib) = *guard {
617            if sib.class_rqs.runnable_len() < 2 {
618                return None;
619            }
620            if let Some(task) = sib.class_rqs.steal_candidate(&sib.class_table) {
621                sched.task_cpu.insert(task.id, cpu_index);
622                task.home_cpu
623                    .store(cpu_index, core::sync::atomic::Ordering::Relaxed);
624                if cpu_is_valid(cpu_index) {
625                    CPU_STEAL_IN_COUNT[cpu_index].fetch_add(1, Ordering::Relaxed);
626                }
627                if cpu_is_valid(steal_from) {
628                    CPU_STEAL_OUT_COUNT[steal_from].fetch_add(1, Ordering::Relaxed);
629                }
630                LAST_STEAL_TICK[cpu_index].store(now_tick, Ordering::Relaxed);
631                return Some(task);
632            }
633        }
634    }
635    None
636}
637
638/// Pick the next task using only per-CPU LOCAL state.
639///
640/// Handles current task disposition (re-queue, drop-for-cleanup, or ignore if
641/// Blocked), then picks from the local class_rqs, falls back to work-stealing,
642/// and finally returns the idle task.
643///
644/// **Dead tasks**: if the current task is Dead, it goes into `task_to_drop`.
645/// Global map cleanup (`all_tasks`, `task_cpu`, etc.) must have been performed
646/// by the caller (e.g., `exit_current_task`) BEFORE reaching this point.
647pub(super) fn pick_next_task_local(cpu: &mut SchedulerCpu, cpu_index: usize) -> Arc<Task> {
648    // Step 1: dispose of the current task.
649    if let Some(task) = cpu.current_task.take() {
650        match task.get_state() {
651            TaskState::Running => {
652                task.set_state(TaskState::Ready);
653                if !Arc::ptr_eq(&task, &cpu.idle_task) {
654                    // Defer re-queue to finish_switch (not yet safe to enqueue :
655                    // another CPU could steal it before our context is saved).
656                    cpu.task_to_requeue = Some(task);
657                }
658            }
659            TaskState::Dead => {
660                // Global maps already cleaned by exit_current_task / kill_task.
661                // Defer the Arc drop so KernelStack::drop → buddy_alloc runs
662                // outside any lock.
663                cpu.task_to_drop = Some(task);
664            }
665            TaskState::Blocked | TaskState::Ready => {
666                // Blocked: moved to blocked_tasks by block_current_task : do nothing.
667                // Ready: shouldn't normally occur for current_task; safe to ignore.
668            }
669        }
670    }
671
672    // Step 2: pick from local class_rqs.
673    let next = if let Some(next) = cpu.class_rqs.pick_next(&cpu.class_table) {
674        next
675    } else if let Some(stolen) = steal_task_local(cpu, cpu_index) {
676        // Step 3: try work-stealing from a sibling CPU.
677        stolen
678    } else {
679        // Step 4: idle fallback.
680        cpu.idle_task.clone()
681    };
682
683    next.set_state(TaskState::Running);
684    cpu.current_task = Some(next.clone());
685    cpu.current_runtime = crate::process::sched::CurrentRuntime::new();
686    next
687}
688
689/// Prepare a LOCAL-only context switch.
690///
691/// Updates the TSS, SYSCALL RSP, and CR3 for the next task. Returns the
692/// raw pointer pair needed by `do_switch_context`.
693///
694/// Returns `None` if there is no task to switch to (same task or invalid context).
695pub(super) fn yield_cpu_local(cpu: &mut SchedulerCpu, cpu_index: usize) -> Option<SwitchTarget> {
696    let current = cpu.current_task.as_ref()?.clone();
697
698    let next = pick_next_task_local(cpu, cpu_index);
699
700    if Arc::ptr_eq(&current, &next) {
701        return None;
702    }
703    if cpu_is_valid(cpu_index) {
704        CPU_SWITCH_COUNT[cpu_index].fetch_add(1, Ordering::Relaxed);
705    }
706
707    if let Err(e) = validate_task_context(&next) {
708        let bad_rsp = unsafe { (*next.context.get()).saved_rsp };
709        let stk_base = next.kernel_stack.virt_base.as_u64();
710        let stk_top = stk_base + next.kernel_stack.size as u64;
711        crate::serial_println!(
712            "[sched-local] WARN: invalid ctx task='{}' id={} cpu={}: {} \
713             rsp={:#x} stack=[{:#x}..{:#x}] : restoring current",
714            next.name,
715            next.id.as_u64(),
716            cpu_index,
717            e,
718            bad_rsp,
719            stk_base,
720            stk_top,
721        );
722
723        // Restore invariants: undo what pick_next_task_local mutated.
724        let is_idle = Arc::ptr_eq(&next, &cpu.idle_task);
725        drop(cpu.task_to_drop.take());
726        if let Some(prev) = cpu.task_to_requeue.take() {
727            prev.set_state(TaskState::Running);
728            cpu.current_task = Some(prev);
729        } else {
730            current.set_state(TaskState::Running);
731            cpu.current_task = Some(current.clone());
732        }
733        if !is_idle {
734            next.set_state(TaskState::Ready);
735            let class = cpu.class_table.class_for_task(&next);
736            cpu.class_rqs.enqueue(class, next);
737        }
738        return None;
739    }
740
741    // Update TSS.rsp0 and SYSCALL kernel RSP for the new task.
742    let stack_top = next.kernel_stack.virt_base.as_u64() + next.kernel_stack.size as u64;
743    crate::arch::x86_64::tss::set_kernel_stack(x86_64::VirtAddr::new(stack_top));
744    crate::arch::x86_64::syscall::set_kernel_rsp(stack_top);
745
746    // Switch CR3 if the new task has a different address space.
747    // SAFETY: The new task's address space has a valid PML4 with the kernel half mapped.
748    unsafe {
749        next.process.address_space_arc().switch_to();
750    }
751
752    Some(SwitchTarget {
753        old_rsp_ptr: unsafe { &raw mut (*current.context.get()).saved_rsp },
754        new_rsp_ptr: unsafe { &raw const (*next.context.get()).saved_rsp },
755        old_fpu_ptr: current.fpu_state.get() as *mut u8,
756        new_fpu_ptr: next.fpu_state.get() as *const u8,
757        old_xcr0: current
758            .xcr0_mask
759            .load(core::sync::atomic::Ordering::Relaxed),
760        new_xcr0: next.xcr0_mask.load(core::sync::atomic::Ordering::Relaxed),
761    })
762}
763
764/// Post-switch cleanup using only LOCAL state: re-enqueue the previous task
765/// and optionally extract the task-to-drop for deferred deallocation.
766pub(super) fn drain_post_switch_local(
767    cpu: &mut SchedulerCpu,
768    take_drop: bool,
769) -> Option<Arc<Task>> {
770    let task_to_drop = if take_drop {
771        cpu.task_to_drop.take()
772    } else {
773        None
774    };
775    if let Some(task) = cpu.task_to_requeue.take() {
776        let class = cpu.class_table.class_for_task(&task);
777        cpu.class_rqs.enqueue(class, task);
778    }
779    task_to_drop
780}