Skip to content

Process Management

kazah-png edited this page Jul 26, 2026 · 8 revisions

Process Management

A preemptive, weighted round-robin scheduler over a fixed table of 512 process slots, with copy-on-write fork, in-place execve, POSIX-subset signals, real threads sharing an address space, and job control. On a multi-core machine each CPU runs its own scheduler over the same table — see SMP.

See also: Architecture, Memory Management, Syscalls, Userspace, SMP, Shell

Process states

State Value Meaning
PROC_PARKED 0 Not runnable — a retired kernel thread; the scheduler skips it
PROC_RUN 1 Runnable or running
PROC_ZOMBIE 2 Exited, awaiting reap
PROC_BLOCKED 3 Blocked in kwait(), sleep(), read(), accept(), futex_wait()
PROC_STOPPED 4 Job-control stopped by SIGTSTP/SIGSTOP, parked until SIGCONT

Scheduler

  • Every task carries a sched_weight (1–64) and a sched_quantum countdown.
  • The PIT fires at 1000 Hz. Each tick decrements the current task's quantum; at zero the scheduler picks the next runnable task and stamps it a fresh quantum equal to its weight.
  • A high-weight task therefore runs several consecutive ticks and receives a proportionally larger share. The compositor gets SCHED_WEIGHT_GUI (4) so the desktop stays responsive while background jobs at weight 1 run.
  • nice <weight> <file> spawns a job at a chosen weight; renice <pid> <weight> changes a running one. Both are kernel-shell commands.
  • sched_cpu pins a task to a specific core. This is the entire mutual-exclusion argument for AP scheduling: no other core will even consider a task pinned elsewhere.

The idle desktop does not busy-poll. When nothing is happening — no key queued, no mouse button, pointer unmoved, no drag/resize/menu active — the compositor sleep(5)s instead of spinning, handing its slice to background work. Any activity skips the sleep, so responsiveness is unchanged.

Kernel threads

Created with create_process(name, entry, arg) or create_kernel_thread_on_cpu(name, entry, cpu). They run in ring 0 on the kernel PML4. The compositor is one of these.

mtdemo is the scheduler's self-test: it spins up two demo threads at different weights, heartbeats for a few seconds, then retires them. Measured over a 400 ms sample, a weight-3 thread counted 99.1M against a weight-1 thread's 34.3M — a ratio of 2.89 against a nominal 3.0.

User processes

Ring 3, each with its own PML4. A process is created by spawn_user_path() (loads an ELF as a scheduler-managed process) or by execve().

Path Behaviour
exec <file> Foreground. Spawns the child, blocks the shell in kwait(), prints the exit code. The desktop keeps recompositing at ~16 fps while the child runs preemptively, so full-screen TUIs render live in the GUI window
spawn <file> Background. Returns immediately; the scheduler time-slices the child while the shell stays interactive. ps lists it with the shell as PPID
bare name The kernel shell auto-execs /name.elf if it is not a builtin, with argv forwarded

execve()

Replaces the caller's image in place: same pid, same open fds, same cwd, new address space. The ELF is loaded into a fresh page directory by elf_load_image, then the old one is freed (refcount-aware, so COW-shared frames survive). argv and envp are threaded onto the new stack in SysV layout — crt0 reads argc from [rsp] and argv from rsp+8, and sets environ from envp, so a child inherits its parent's environment.

fork()

SYS_FORK clones the calling process copy-on-write. The mechanics are in Memory Management; the visible contract is the POSIX one — the child gets 0, the parent gets the child's pid, and both continue round-robin.

Exit and reaping

exit() marks the task PROC_ZOMBIE and wakes anything waiting on it. The address space and kernel stacks are freed later by reap_zombies() from a safe context — a process cannot free the stack it is currently running on. A parent collects the exit status with waitpid(); orphans are reaped by the background task.

close_proc_fds() runs at reap, closing anything the process left open, so a crashed or careless program cannot leak VFS handles.

Threads

clone(fn, stack_top, arg, CLONE_VM) creates a real thread: a new task that shares the caller's address space instead of getting fork's copy. Without CLONE_VM the call fails — use fork() for a separate address space.

Three pieces of shared state make the thread model coherent:

Shared Mechanism
Address space CLONE_VM — no page-table copy
Heap and mmap VMAs tgid names the thread-group leader; every sbrk/mmap/VMA lookup defers to it via tg_leader(), so malloc from any thread is seen by all
File descriptors One fd table per thread group

Synchronisation is a futex:

futex_wait(&addr, val);   // sleep while *addr == val — returns immediately if it already differs
futex_wake(&addr, n);     // wake up to n waiters, returns how many

The compare-and-sleep is what makes a lock race-free. user/threads.c is the worked example.

A thread must finish with exit(); it never "returns" anywhere. Thread groups stay pinned to one core, because they share page tables.

Blocking

Primitive Blocks until
kwait(pid) / waitpid A child exits (pid <= 0 waits for any)
kwait_all() All children have exited — the shell's wait
sleep(ms) / nanosleep wake_tick arrives; the scheduler's per-tick wait-queue pass wakes the sleeper
read() Data is available on a pipe, socket or the keyboard
accept() A client connects
poll() An fd becomes ready, or the timeout elapses
futex_wait() futex_wake, or the value changes

All of them park the task with the classic cli-check-then-sti; hlt pattern, which makes check-then-sleep atomic against the wakeup — otherwise a wakeup arriving between the two would be lost.

Every process has its own kernel stack for syscalls. This is what makes blocking real: without it a second process entering the kernel would trample the first one's parked frame, and waitpid had to poll instead of sleeping.

Signals

A POSIX subset in kernel/signal.c. Signals are posted as a bit in sig_pending and delivered at return-to-ring-3 by signal_dispatch.

Disposition Effect
SIG_DFL Terminates the process with exit status 128 + signo
SIG_IGN Dropped silently
User handler The interrupted frame is saved, a trampoline is pushed onto the user stack, and the handler is entered with the signal number in RDI; it returns through SYS_SIGRETURN, which restores the context

sigprocmask(how, set, oldset) reads and changes the blocked-signal mask — the primitive behind sigsetjmp/siglongjmp in libc.

CPU exceptions become catchable signals. A ring-3 page fault, divide error or illegal instruction is turned into SIGSEGV/SIGFPE/SIGILL and delivered to the faulting process. With a handler installed the program recovers; without one it dies with status 128 + signo. A crashing user program no longer panics the kernel — user/fault.c and user/segv.c exercise this deliberately.

alarm(seconds) schedules SIGALRM for later; it returns whatever was left on any previous alarm.

Job control

setfg(pid) names the terminal foreground process, so keyboard-generated signals target the running job rather than the shell:

  • Ctrl-CSIGINT to the foreground process; a blocking read() returns -EINTR
  • Ctrl-ZSIGTSTP; the process enters PROC_STOPPED

The shell points setfg at a job while it runs in the foreground and back at itself afterwards (setfg(0) clears it). jobs lists background and stopped jobs, fg resumes one in the foreground, bg resumes one in the background, and waitpid reports a stop through WIFSTOPPED(status) / WSTOPSIG(status) when passed WUNTRACED.

Concurrency safety

Two different mechanisms, for two different problems:

  • preempt_disable/preempt_enable stops a context switch on the local core. The count itself is per-CPU. This is the right tool for single-core re-entrancy — a shell command touching the VFS on the compositor thread being preempted by a process doing the same.
  • Spinlocks are what actually work across cores. The physical allocator, kernel heap, slab caches, VFS node pool, mount table, keyboard input path, network stack and EXT2 driver each sit behind one, taken with interrupts disabled.

get_current_process() resolves per-CPU. It used to answer with the BSP's task for anything running on an application processor, which was the root cause of two separate multi-core hangs.

Inspecting processes

Where What
ps Process table — pid, ppid, state, CPU time, command (kernel builtin and ring-3 ps.elf)
top Live auto-refreshing monitor (ring 3, full-screen TUI)
/proc/<pid>/status Name, Pid, PPid, State
/proc/<pid>/cmdline Full command line
/proc/<pid>/maps Mapped memory regions
jobs The shell's background and stopped jobs

See also

External resources

Clone this wiki locally