A user level kernel for non-preemptive multiprocessing
A user level kernel for non-preemptive multiprocessing on various 64 bit o/s's.[1]
Code can be found at https://github.com/mahtin/kernel64/ and should be cloned locally in order to run. No precompiled binary is provided.
The project home is the https://kernel64.net website.
[1] Also some 32 bit o/s's - armv6l/armv7l (older Raspberry Pi's) and Intel i386.
Kernel64 is a portable cooperative multitasking kernel inspired by many other schedulers found online. The implementation is written primarily in C and uses:
setjmp()longjmp()to provide lightweight processes without requiring operating system threads. Kernel64 currently supports:
The scheduler is entirely cooperative. Processes execute until they voluntarily yield, sleep, exit, or are destroyed.
Each process is represented by a struct proc64.
A process contains:
pid)jmp_buf)Processes are linked together in a doubly-linked process list.
The currently executing process is tracked by:
struct proc64 *curproc;
and the complete process list is tracked by:
struct proc64 *proclist;
Kernel64 represents process state as a single character value.
| State | Character | Description |
|---|---|---|
P_GO |
G |
Process created but never executed |
P_RUNNING |
R |
Runnable process |
P_WAITING |
W |
Waiting for wakeup event |
P_SLEEPING |
S |
Sleeping for a timed delay |
P_DESTROYED |
D |
Process destroyed because of error |
P_ZOMBIE |
Z |
Process exited and awaiting cleanup |
The scheduler considers the following states runnable:
P_GO
P_RUNNING
A process in any other state will not be selected for execution.
Processes are created with:
k64spawn(name, entry, arg, stack_size, v);
During creation Kernel64:
setjmp().P_GO.A newly created process does not begin execution immediately. Execution starts when the scheduler first selects the process.
Each process owns its own private stack.
Stack size can be specified by the caller via the k64spawn() call.
Stacks are allocated using virtual memory rather than standard heap allocation.
Linux and macOS use:
mmap();
mprotect();
The lowest page of each stack allocation is marked:
PROT_NONE
to act as a stack guard page.
Windows uses:
VirtualAlloc();
VirtualProtect();
The lowest page of the allocation is marked:
PAGE_NOACCESS
to provide the same stack protection.
All process stacks contain an inaccessible guard page at the bottom of the stack. Normal stack growth proceeds downward toward the guard page. If a process overflows its stack, the CPU immediately generates a fault upon entering the guard page. This provides deterministic stack overflow detection.
After allocation, every usable byte of stack memory is initialized with:
STACK_GUARD_BYTE
currently (which is really a random choice):
0xAA
The function:
k64stack_used()
measures stack usage by scanning upward through the stack until the first modified byte is located. This permits:
without platform-specific APIs.
Timer implementation is work in progress.
Kernel64 always creates an idle process during initialization. The idle process becomes PID 1. Initialization is performed within:
k64init();
which must be called before anything else and internally creates:
[idle]
The idle process serves several purposes:
The idle process runs at the lowest priority value used by the scheduler (255).
Kernel64 performs scheduling through:
setjmp();
longjmp();
A context switch occurs in two stages. This code does not fully work on Windows. (Hence the work in progress.)
When a running process yields:
setjmp(curproc->context);
stores:
inside the process control block. Each processor, operating system, architecture will decide how information is stored and is external to this code.
When the scheduler selects a process:
longjmp(next->context, 1);
restores the saved execution state and resumes execution exactly where that process last yielded. No operating system thread context switching occurs. All scheduling remains inside user space.
A newly created process (via k64spawn()) begins in state:
P_GO
The first time the scheduler selects such a process:
k64process_bootstrap();
is invoked.
The bootstrap routine:
k64exit() when the process-entry routine returns.The bootstrap function never returns.
Kernel64 contains architecture-specific assembly routines named:
k64_switch_to_stack();
Implementations exist for:
The purpose of the routine is:
Only this small assembly component is architecture-specific. All scheduling logic remains portable C code.
Scheduling is performed by:
k64schedule();
Kernel64 performs a circular scan of the process list.
During the scan it:
Priority values are interpreted as:
Lower value = Higher priority
For example:
Priority 10 > Priority 50
Priority 50 > Priority 100
where ">" means "runs before".
The selected process is activated using:
k64switch();
A process voluntarily relinquishes execution by calling:
k64yield();
This keep the process in a runnable state:
P_RUNNING
and then invokes the scheduler.
Because Kernel64 is cooperative:
A process should call k64yield() often. If no other process is ready to run, then the kernel returns to the call process quite quickly.
Timed delays are implemented with:
k64delay(milliseconds);
This places the process into:
P_SLEEPING
and stores a wakeup value. The scheduler compares the wakeup time on each scheduling pass. When the wakeup is reached, the process is makred as runnable again.
Processes may block waiting for an event. Waiting is performed with:
k64sleep(event);
The process enters state:
P_WAITING
Another process may resume it by calling:
k64wakeup(process);
which returns the process to:
P_RUNNING
and makes it eligible for scheduling again.
A process terminates itself by calling:
k64exit();
or by returning from its entry function. There are no exit values.
Exit processing:
P_ZOMBIE.Zombie processes remain in the process list with their resources until the idle process reaps their resources. Presently only the stack is the resource reaped.
Kernel64 can force the removal of a process by calling:
k64destroy(process);
A destroyed process enters state:
P_DESTROYED
Destroyed processes are eventually cleaned up by the idle process. This mechanism is used primarily when stack corruption or internal errors are detected.
The k64destroy(process) can be called by another process.
As present there's no mechaanism to record reasons for a process to be destroyed.
Prior to scheduling a process, Kernel64 performs:
k64stack_sanity_check();
and
k64stack_overflowed();
checks.
If corruption is detected:
P_DESTROYED
This prevents a corrupted process from continuing execution.
Kernel64 provides:
k64print_ps();
which generates a textual snapshot of all processes send to stdout.
Displayed information includes:
This facility serves as the primary scheduler debugging and monitoring tool.
Kernel64 attempts to preserve the simplicity of classic cooperative process systems while remaining portable across modern architectures. Key goals include:
The resulting design provides a lightweight process environment suitable for experimentation, embedded systems, network services, and educational operating system development.
Read the full README page for install, license, and other inforation.
Copyright (C) 2025-2026 Martin J Levy - W6LHI/G8LHI - @mahtin - https://github.com/mahtin