theseus-os / Theseus

Theseus is a modern OS written from scratch in Rust that explores 𝐢𝐧𝐭𝐫𝐚𝐥𝐢𝐧𝐠𝐮𝐚𝐥 𝐝𝐞𝐬𝐢𝐠𝐧: closing the semantic gap between compiler and hardware by maximally leveraging the power of language safety and affine types. Theseus aims to shift OS responsibilities like resource management into the compiler.
https://www.theseus-os.com/
MIT License
2.92k stars 172 forks source link

Add `update` method to CLS variables #1034

Closed tsoutsman closed 1 year ago

tsoutsman commented 1 year ago

Not sure if update is the "correct" name. The closest analogue I could find in std is Cell::update, but that has a very different API.

This function is necessary for some upcoming work on the scheduler. Essentially, there's a global list of schedulers, and then also a CPU-local which contains a reference to the current CPUs scheduler.

static SCHEDULERS: Mutex<Vec<(CpuId, &'static ConcurrentScheduler)>> = Mutex::new(Vec::new());
#[cls::cpu_local]
static SCHEDULER: Option<&'static ConcurrentScheduler> = None;

type ConcurrentScheduler = Mutex<Box<dyn Scheduler>>;

So SCHEDULER would contain a reference that is also an entry in SCHEDULERS.

Now, when updating the policy we need to remove the scheduler from SCHEDULERS and update SCHEDULER in a special way to prevent race conditions:

let mut locked = SCHEDULERS.lock();
let guard = hold_preemption();
let old_scheduler = SCHEDULER.replace(new_scheduler);

// Do some stuff with `locked` and `old_scheduler`.

drop(guard);

But we should really be abstracting away from preemption:

let mut locked = SCHEDULERS.lock();
SCHEDULER.update(|old_scheduler| {
  // Do some stuff with `locked` and `old_scheduler`.

  *old_scheduler = new_scheduler;
});

Both snippets of code are functionally equivalent, but the latter one is at a higher abstraction level with fewer footguns.

kevinaboos commented 1 year ago

The syntax is also similar to fetch_update from the world of atomics, though I'm not sure if that's actually superior to update. Another option could be update_with, since functions that take closures to operate on some value often use the word "with" in Rust.

Regarding the two options, I think having an update method of some kind is a good choice. In the specific case of the scheduler, we might need to wrap SCHEDULERS (ideally we'd use a name that's more than 1 character different than SCHEDULER...) in an interrupt-safe mutex since schedulers generally are accessed within interrupt contexts.

Regarding whether to disable interrupts or preemption, in the general case preemption should suffice, since you typically just expect to access something local to the CPU in a way that would still work if the update function logic had to persist across jumps to interrupt handlers. If a particular case requires more "atomicity", i.e., no interrupts can happen, then I'd say that's on the developer of that particular case to disable interrupts separately.

I'm curious to see how SCHEDULERS gets used, since it's pretty tough to coordinate things across multiple CPUs simultaneously.

tsoutsman commented 1 year ago

I think I'll leave it as update because fetch_update also has a different API, and we aren't really fetching the value. Also AFAICT container types usually don't have "with" functions.