-
Notifications
You must be signed in to change notification settings - Fork 257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deadlock in switch_to
Due to Premature Task Placement in Run Queue (SMP)
#197
Comments
Additional Notes on Linux Task MigrationThe task migration process in Linux is intricate and involves several critical functions to ensure safe and atomic migration between CPUs. The migration relies on Here’s a brief outline of the core steps:
/*
* This is how migration works:
*
* 1) we invoke migration_cpu_stop() on the target CPU using
* stop_one_cpu().
* 2) stopper starts to run (implicitly forcing the migrated thread
* off the CPU)
* 3) it checks whether the migrated task is still in the wrong runqueue.
* 4) if it's in the wrong runqueue then the migration thread removes
* it and puts it into the right queue.
* 5) stopper completes and stop_one_cpu() returns and the migration
* is done.
*/
/*
* move_queued_task - move a queued task to new rq.
*
* Returns (locked) new rq. Old rq's lock is released.
*/
static struct rq *move_queued_task(struct rq *rq, struct rq_flags *rf,
struct task_struct *p, int new_cpu)
{
lockdep_assert_held(&rq->lock);
deactivate_task(rq, p, DEQUEUE_NOCLOCK);
set_task_cpu(p, new_cpu);
rq_unlock(rq, rf);
rq = cpu_rq(new_cpu);
rq_lock(rq, rf);
BUG_ON(task_cpu(p) != new_cpu);
activate_task(rq, p, 0);
check_preempt_curr(rq, p, 0);
return rq;
} |
I don't think we need a complex mechanism like Linux’s stop task to handle task migration between run queues, nor do we need a separate work flow similar to Since we already store a weak pointer to the This approach is similiar to @guoweikang‘s idea, but it’s less radical. @guoweikang suggests that all insert To implement this approach, we first need to upgrade the weak pointer to also, we may need a
pub fn migrate_current(&mut self) {
let curr = &self.current_task;
trace!("task migrate: {}", curr.id_name());
assert!(curr.is_running());
curr.set_state(TaskState::Migrating);
self.inner.resched();
}
#[cfg(feature = "smp")]
pub(crate) unsafe fn finish_prev_task_switch(&self) {
let prev_task = (*self.prev_task.get())
// Takes the `Arc` reference to the previous task, setting it to None in `self`.
.take()
.expect("Invalid prev_task pointer");
// Clears the `on_cpu` field of previous task running on this CPU.
prev_task.set_on_cpu(false);
// Migrate the previous task to the run queue of correct CPU if necessary.
// If `prev_task` doesn't need to be migrated, its `AxTaskRef` will be dropped automatically.
if prev_task.is_migrating() {
select_run_queue::<kernel_guard::NoOp>(&prev_task).migrate_task(prev_task);
}
// No need to manually set the `prev_task` to `None` since `take()` already did it.
}
pub fn migrate_task(&mut self, task: AxTaskRef) {
let task_id_name = task.id_name();
let cpu_id = self.inner.cpu_id;
debug!("task migrate: {} to run_queue {}", task_id_name, cpu_id);
assert!(self
.inner
.put_task_with_state(task, TaskState::Migrating, false))
} |
Description
In
run_queue.rs
, theswitch_to
function in ArceOS currently implements a spin-loop to wait until theon_cpu
flag for the next_task is set tofalse
.This design is intended to ensure that the scheduling of next_task occurs atomically on the current CPU, preventing simultaneous access to the same task on different CPUs.
However, this design leads to a deadlock scenario under specific conditions in an SMP setup.
If:
task 0
totask 1
task 1
totask 0
Core 0 will be stuck waiting for
task 1
to fully yield Core 1, and Core 1 will be waiting fortask 0
to yield Core 0, resulting in a deadlock.Root Cause
The deadlock is caused by prematurely placing a task in the target core’s run queue (rq), allowing it to be immediately accessed and potentially popped by
switch_to
on that core.This access in
switch_to
leads to the aforementioned blocking spin-loop, as both cores await each other.Previous Solution
The polling should be placed at
unblock_task
, which would only insert the target task into the target CPU’s run queue once theon_cpu
flag has been safely unset on the previous CPU.This will ensure the task is only placed into the run queue when it’s no longer active on another core.
Complication with migrate_current
The reason why I place the polling in
switch_to
is that we need to supportmigrate_current
, as I claimed in #183 (comment)Moving the polling to
unblock_task
introduces complexity for themigrate_current
method.Since
migrate_current
is intended to place the current task in another CPU’s run queue, it would require waiting on the current task'son_cpu
flag to befalse
—which is inherently impossible for the current task.The text was updated successfully, but these errors were encountered: