Linux单进程用户空间能否实现原生代码抢占式多任务?
Great question! Let's break this down step by step, covering feasibility on Linux, fixes for your SIGALRM/context approach, and alternative implementations.
1. Can this be done on Linux?
Yes, but with critical caveats. POSIX explicitly marks getcontext()/swapcontext() as async-signal-unsafe—calling them in a signal handler can lead to data races, corrupted state, or undefined behavior if the interrupted code was already running another async-signal-unsafe function.
That said, Linux has de facto behaviors that make this approach work in practice (many user-space thread libraries rely on similar mechanics). However, your code will be Linux-specific; you can't rely on POSIX compliance here.
2. Fixing your SIGALRM/context-based approach
The core issue with your current signal handler is calling async-signal-unsafe functions (getcontext()/makecontext()) inside the handler. Here's how to adjust the scheme for better safety:
Key fixes:
- Pre-initialize the scheduler context: Set up
signal_contextonce during program startup, not in the signal handler:ucontext_t signal_context; char signal_stack[STACKSIZE]; void init_scheduler() { getcontext(&signal_context); signal_context.uc_stack.ss_sp = signal_stack; signal_context.uc_stack.ss_size = STACKSIZE; signal_context.uc_stack.ss_flags = 0; sigemptyset(&signal_context.uc_sigmask); // Unblock SIGALRM so the scheduler can receive future preemption signals sigprocmask(SIG_UNBLOCK, &signal_context.uc_sigmask, NULL); makecontext(&signal_context, scheduler_main, 1); } - Use the signal handler's built-in context: The third argument to your handler (
void *old_context) is a pointer to theucontext_tof the interrupted thread. Instead of callinggetcontext(), directly save this pointer to track the current thread's state:ucontext_t *cur_context; void timer_interrupt(int j, siginfo_t *si, void *old_context) { // Save the interrupted thread's context cur_context = (ucontext_t *)old_context; // Switch control to the user-space scheduler swapcontext(cur_context, &signal_context); } - Enforce signal-safe operations: Ensure your
scheduler_mainfunction only uses async-signal-safe tools when modifying shared state (like thread queues). Use atomic operations or avoid blocking calls—mutexes are often async-signal-unsafe, so usepthread_mutex_trylock()if necessary.
3. Alternative implementations
For a more robust, maintainable solution, consider these approaches:
a. User-level threads via clone()
Linux's clone() system call lets you create lightweight processes (LWPs) that share the same address space, file descriptors, and signal handlers (using flags like CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND). This is how most user-space thread libraries (e.g., musl's pthread implementation) work:
- Each "thread" is an LWP managed by your user-space scheduler.
- Use
SIGALRMorSIGVTALRMto trigger preemption, just like your original scheme. - The scheduler can switch between threads using
swapcontext()for full context preservation.
b. Signal-based preemption with setjmp()/longjmp()
While setjmp()/longjmp() don't save floating-point or extended registers (unlike context functions), they're async-signal-safe (with caveats). They work for simpler workloads:
- Use
setjmp()to capture a thread's state at startup. - In the signal handler, save the current
jmp_buf(via the signal's context) andlongjmp()to the scheduler'sjmp_buf. - Note: You'll need manual handling of non-general-purpose registers if your code uses them.
c. Pre-built user-space scheduler libraries
If you don't want to build everything from scratch, look into libraries that implement user-space preemption:
- Frameworks like
libtaskor custom coroutine libraries (most are cooperative by default, but you can add signal-based preemption to make them preemptive).
内容的提问来源于stack exchange,提问作者Praxeolitic

