Skip to content

Feature request: worker threads in a Protection Domain #614

Description

@garlinski

I have a use case where a Protection Domain performs CPU-intensive data processing and needs to execute work in parallel on multiple CPU cores.

At the moment, the natural way to use multiple cores in Microkit is to split the application into multiple PDs. This works well when the components are logically separate, but is awkward when they are really execution contexts of the same application.

In my case, the processing application contains a scheduler and a fairly large object graph allocated on the heap. Processing objects have internal state and are connected through ordinary pointers. Splitting the workers into separate PDs would require moving most or all of the heap into explicitly shared memory and using a shared-memory-aware allocation scheme, even though there is no desired isolation boundary between the workers.

The seL4 model already naturally supports multiple TCBs sharing the same VSpace and CSpace, which seems like a much better fit for this use case.

I would therefore like to propose optional additional worker threads within a Microkit Protection Domain.

Possible SDF interface

For example:

<protection_domain name="audio_processor" cpu="0" priority="200" budget="1000" period="1000">
	<program_image path="audio_processor.elf" />

	<thread id="0" cpu="1" entry="worker_entry" stack_size="0x8000" />
	<thread id="1" cpu="2" entry="worker_entry" stack_size="0x8000" />
</protection_domain>

The exact syntax is of course only a suggestion.

Proposed semantics

The existing PD thread would remain the main thread and retain the current Microkit programming model:

  • it runs the normal program/runtime initialisation;
  • it executes init;
  • it owns the Microkit event loop;
  • only this thread executes notified, protected, and fault.

Additional threads would be worker threads rather than additional Microkit event loops.

Each worker would have:

  • its own TCB;
  • its own Scheduling Context;
  • its own stack;
  • its own CPU affinity;
  • the same VSpace as the main PD thread;
  • the same CSpace as the main PD thread.

This means normal pointers, heap allocations, vtables/trait objects, shared static data, and capabilities can be used directly by all threads without constructing a separate shared-memory object model.

Scheduling parameters such as priority, budget and period could initially be inherited from the parent PD, with only cpu being specified per thread. Per-thread overrides could potentially be added later if useful.

Thread initialisation and startup

Worker threads must not enter the normal program startup path independently.

In particular, global/runtime initialisation and the PD's init function must only execute once on the main thread.

Worker TCBs could therefore be created by the system initializer in a suspended state. Their initial PC would point either to their configured entry point or to a small Microkit thread trampoline.

They should only become runnable after the main PD has completed its initialisation.

There are at least two possible APIs for this:

  1. automatically resume all worker threads after init returns; or
  2. provide the PD with a small API such as:
microkit_thread_start(thread_id);

so that the application can explicitly start workers near the end of its initialisation.

I personally prefer explicit startup, as it also permits applications to initialise shared scheduler state before workers become runnable.

A worker entry point could look conceptually like:

void worker_entry(microkit_thread_id id);

and should probably be required not to return.

IPC buffers and initial scope

I do not think full IPC support is necessary for an initial implementation.

A useful first version could support worker threads intended primarily for computation and shared-memory synchronisation.

For example, workers could use Notifications to sleep while no work is available and to signal other components after producing output.

Therefore an initial implementation could potentially create worker TCBs without IPC buffers.

Full per-thread IPC could be added later by allocating a separate IPC buffer for each thread and providing appropriate TLS/runtime support.

This would keep the first implementation considerably simpler.

Synchronisation use case

A typical worker loop in my application is conceptually:

loop:
	take executable work from shared scheduler
	execute available work
	if no work remains:
		wait for a notification

seL4 Notifications are particularly suitable for this because a notification that arrives before the worker waits remains pending, avoiding the usual lost-wakeup race between checking the work queue and going to sleep.

The notification is only a wake-up hint; the shared scheduler/work queue remains the source of truth, so notification coalescing is not a problem.

Why not model the workers as separate PDs?

Separate PDs introduce an isolation boundary that is not desired for this use case.

To make several worker PDs operate on the same state, an application has to explicitly place essentially its entire mutable object graph/heap into shared memory.

For pointer-heavy or stateful processing applications this is significantly more intrusive than simply having several execution contexts within one address space.

Another possible workaround would be to create dummy PDs and later reconfigure their TCBs to use the CSpace and VSpace of a "master" PD. Apart from being a hack, this requires exposing capabilities that allow runtime CSpace/VSpace reconfiguration and no longer accurately reflects the static system structure described by the SDF. Recently, cap_cspace was removed from capability sharing for this reason.

Representing the threads explicitly in the system description avoids that problem; the authority graph remains static and all threads are explicitly part of the same protection domain.

Fault handling

It would also be useful for worker faults to identify the worker/thread ID.

For example, debug fault reporting could distinguish:

audio_processor (main)
audio_processor.thread[0]
audio_processor.thread[1]

even though all of them belong to the same PD.

Initial feature scope

A minimal version that would already cover my use case could therefore be:

  • multiple TCBs belonging to one PD;
  • shared CSpace and VSpace;
  • separate stacks and Scheduling Contexts;
  • per-thread CPU affinity;
  • configurable entry point;
  • workers initially suspended;
  • explicit or automatic start after PD initialisation;
  • no additional Microkit event loops;
  • no notified/protected dispatch on workers;
  • no worker IPC buffers initially;
  • Notification-based wakeup/signalling;
  • worker ID available to the entry point and fault reporting.

More complete thread runtime support, TLS, per-thread IPC buffers, and more sophisticated scheduling parameters could be added separately.

Would this fit the intended Microkit model for the "multithreaded applications" case, or is there another preferred way to represent this?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions