I'll start with a short background on the topic for context. I'll assume some familiarity with message passing, as that's generally what thread migration is compared against.
Background
Thread migration in the context of IPC means allowing threads to move between different processes, taking along some amount of data. For example, you could have a filesystem running as a userspace process, and when a thread wants to write to a file, it migrates into the filesystem, performs the write on its own (respecting other potentially concurrent threads) and migrates back to its original process. The filesystem process doesn't necessarily have to contain any threads at any given time.
Compared to a typical synchronous message passing implementation, thread migration can be more easily parallel on the service side, as multiple threads can enter a service process (filesystem, etc.) in parallel and start providing services to 'themselves'. A message passing implementation would likely have to queue all the incoming requests into some kind of mailbox, which the service process would have to fetch from and potentially distribute the messages across threads to achieve similar parallelism.
Thread migration can be seen as a procedure call into another address space, and processors are pretty good at doing procedure calls, thus thread migration can potentially benefit significantly from hardware acceleration, both on research hardware (https://dl.acm.org/doi/10.1145/3064176.3064197) and even conventional (server-grade) hardware (https://faculty.cs.gwu.edu/gparmer/publ ... 5janus.pdf). However, even with bog-standard modern hardware, thread migration is roughly as performant as message passing. The second link has a benchmark that shows unaccelerated thread migration to be roughly on par with seL4 message passing (Table 1, Composite).
I also think some soft benefits exist, such as easy userspace thread scheduling and a more 1:1 relationship between a thread and the resources it uses, which can make scheduling threads a bit easier, but these are a bit more difficult to demonstrate, so take them as opinion and with a grain of salt.
The main downside, as I see it, is that some of the flexibility of message passing is lost. For instance, message passing can potentially be used across a cluster of machines, and can allow both efficient synchronous and asynchronous communication on the same system. Thread migration is purely synchronous and realistically only applicable for communication within a single machine.
History/current state
Message passing in itself is not new, the first implementation I could find was from 1994, an experimental PA-RISC port of the Mach kernel retrofitted thread migration into an existing remote procedure call (RPC) mechanism that was previously implemented on top of message passing: https://www.usenix.org/legacy/publicati ... s/ford.pdf According to the paper, the thread migrating RPC was ~5x faster than the message passing implementation while only requiring a bit under half the source lines of code. Mach itself never really went anywhere, and the PA-RISC port in particular more or less completely disappeared, but you can still get the sources from https://www.emulab.net/downloads/OLD/pa ... ot3.tar.gz. GNU Hurd is famously based on Mach, but doesn't include the thread migration parts.
There seems to only be one active academic research project related to thread migration, which is the COMPOSITE kernel: https://faculty.cs.gwu.edu/gparmer/publ ... pert10.pdf COMPOSITE targets real-time systems, and as such is not quite usable as a general-purpose OS. In particular, it handles thread stacks in a kind of interesting way: Each process that wants to host threads migrating into it must provide them with stacks. When a thread migrates into a process, the first thing it does is check if there are any stacks available, and if there aren't it allocates one. This works pretty well when you have control over more-or-less the whole system, but means that worst-case, M*N stacks need to be allocated, where M is the number of processes and N is the number of threads.
Sun's Doors or the "process hopping" presented in viewtopic.php?t=52225 are not quite what I would understand to be thread migration, as (IIUC) they create and destroy threads on the fly. I want to stress that with thread migration, the same thread is always active and snakes its way through different processes, possibly switching or allocating stacks on the fly. Regardless, they're close enough to deserve a mention.
In short, as far as I can tell, there's currently no operating system/kernel that targets 'general usage' with thread migration.
Targeting 'general usage'/my kernel
My kernel can be found over at https://github.com/Kimplul/kmi. It currently only supports 64bit RISC-V, and it's probably closest to a hybrid kernel. I implemented memory handling within the kernel instead of outside it, mainly because I'm more interested in playing around with this cool IPC mechanism than I am about implementing a 'proper' microkernel, and having memory management in-kernel seemed a bit easier. I am semi-actively working on a unix-like userspace for the kernel (not very creative, I know), but it's not really far enough along that I'd be comfortable publishing it yet, sorry. I'm targeting 'general usage', by which I mean that the workload is unknown ahead of time and processes generally don't have full knowledge of what other processes might be running on the system and have to dynamically figure things out.
Right off the bat, each thread is allocated its own virtual address space, that is split into three regions: process, stack and kernel. The kernel region is reserved for supervisor/kernel use, fairly standard. The process region is closest to a typical userspace, all threads in a process share the same process region. The stack is region is accessible from userspace but stays static for each thread across different processes. So, when a thread migrates, it performs a system call into the kernel, where the kernel copies the target process' region mapping into the thread's process mapping and pushes an activation record onto the stack region. The stack pages accessed by the previous process are marked inaccessible for security reasons, but when the thread enters the new process, it can directly continue using the free space in the stack. This avoids the worst-case of N*M stacks, but of course means that threads can't share data on their respective stacks between each other by default. I anticipate that processes that need this behaviour (client processes like cat/grep/etc.) can be allocated stacks from the process region by libc, and service processes (filesystem server, drivers, etc.) can be written with this limitation in mind. See attachment one, where boxes are memory pages of the thread stack. Colored boxes are marked inaccessible from the current process after a thread has migrated into it. `k` is kernel data is pushed to the stack, `a`, `b` and `c` are process stack data. Having each thread migration event be an activation record on a stack lends itself naturally to a request-response mechanism, and I've implemented a couple different 'request' kinds for better control over the control flow. Probably the simplest request is just called `req()`, and does what you'd expect: It migrates the thread to the target process, taking four registers worth of data along with it. The kernel populates the receiver's argument registers with the four data registers as well as the process ID (`eid`) and thread ID (`tid`) of where the migration came from and who did it. This way, processes or threads can't masquerade as other threads/processes. Requests are of course recursive, so if the target process find itself needing some kind of service (logging text to a file, whatever), it can perform a `req()` itself. A request is responded to with a `resp()`, which migrates the thread back to the previous process. Four registers worth of data can be returned via `resp()`, and the kernel populates a `status` register as well as the process ID of the process that responded. See attachment 0. Let's consider a 'router' of sorts, which is a service process that just knows all other service processes currently running. A client process might not know which process ID maps to which service, and can perform a migration to the router to ask for the information (I'm assuming that the 'router' ID is always known, process 1 or whatever). This can be done by the above `req()` call, or alternatively, the client just performs the request it would've done to the service process to the router, and the router then 'forwards' the request to the appropriate process. This can be done with `fwd()`, which only differs from `req()` in that the kernel sets the process ID to the same value as when migrating to the router, so to the service process it appears as if the client performed a migration directly to it. I use the term 'effective ID', `eid`, to represent this, sort of "who this request should serve", which may or may not be the previous process. If the router process doesn't have any reason to process the service response further, it can also perform a `kick()` ('kick the can down the road'), which both performs a forwarding of the effective ID and re-uses the current activation record on the stack (kind of like a tail call in a programming language), meaning that when the service responds, the thread is directly migrated back to the client, saving the need to perform an extra migration. See attachment 2 (Hopefully the above is not too messy, I would've liked to use a separate graph for `kick()` and `fwd()` but there's an attachment limitation of 3)
I also implement signaling via thread migration. When a thread receives a signal (and it's currently in its 'root' process, i.e. where it was spawned) it is interrupted and performs a migration into the same process it's currently in. When the signal handler is finished, it 'responds' to the 'request' and the thread state is restored from the stack. Note that unlike other thread migration implementations, I perform all migrations to `_start`, so all processes are kind of 'service processes' and we can do fun stuff like the above.
I've benchmarked my kernel against seL4 and found it to be (eerily) similar in performance on real RISC-V hardware. Unfortunately not quite the same hardware, see details in my blog post below if you're curious. I have some performance optimizations and tricks that I use to avoid doing as much work as possible, and for example copying the process data region during a migration is generally a single load+store pair of overhead. I haven't gone into them in detail this post to at least attempt brevity.
There are of course a myriad of other small things that together build up to this whole thing, but I hope the above paints a good enough picture. I have a blog post about the kernel that more or less goes through the above, but maybe in slightly more detail (and with more pictures) if anyone is curious: https://metanimi.dy.fi/blog/kmi/
Discussion
I don't really have any pre-written discussion questions, but any and all feedback/questions/comments regarding the design I came up with and thread migration in general with welcome. I hope there was at least something interesting in this post and that it wasn't too difficult to follow, thanks for reading
