Suppose an application creates 100 independent units of work. Must the kernel know about all 100?
Not necessarily.
A user-space runtime can maintain 100 execution contexts while asking the kernel to schedule only one underlying thread. Alternatively, every application thread can have a corresponding kernel-scheduled thread. A system can also place many user-space contexts over a smaller pool of kernel threads.
These designs differ in who manages the threads and which threads the kernel can schedule directly.
A user-level thread is managed in user space without being an independently visible scheduling entity to the kernel. A kernel-level thread is represented and scheduled directly by the kernel.
The distinction is about management and scheduling visibility. It is not the same as the distinction between user mode and kernel mode.
The operating system always schedules some kernel-visible execution context onto each logical CPU. An application runtime may add another scheduler above the kernel.
The two schedulers make different decisions.
The runtime scheduler may decide which user-level thread should use an underlying kernel thread. The kernel scheduler decides which kernel-level thread should use a CPU.
The kernel can schedule only objects it knows about. If 100 user-level threads are multiplexed over one kernel thread, the kernel sees one runnable entity, not 100. It cannot select the ninety-seventh user-level thread directly.
Likewise, the user-space runtime receives CPU time only when the kernel schedules one of its kernel-visible threads. A user-level thread can be ready according to the runtime while the entire application remains unable to execute because none of its kernel threads currently owns a CPU.
This layered view is the foundation for understanding every user-to-kernel thread mapping.
A user-level thread is an execution context created and managed by code in the application's address space. That managing code may be a library, language runtime, virtual machine, or custom scheduler.
The runtime needs enough state to suspend and resume each user-level thread. A conceptual record might contain:
This record is ordinary user-space data. The kernel does not have a separate task descriptor or TID for each user-level thread.
Imagine three user-level threads mapped onto one kernel thread:
| User-level thread | What the runtime keeps for it | Its state |
|---|---|---|
| ULT A | Saved context and stack | Ready |
| ULT B | Saved context and stack | Running |
| ULT C | Saved context and stack | Waiting |
All three ride on one kernel thread, TID 7100, which is the only thing the kernel schedules onto a CPU.
While ULT B runs, the CPU's registers contain B's live state. When the runtime switches to ULT A, it preserves B's required state in user memory, restores A's state, and transfers control to A.
From the kernel's perspective, TID 7100 continued executing throughout. The instruction pointer and stack pointer changed, but no new kernel-scheduled task took ownership of the CPU.
The trigger for a user-level switch depends on the runtime. A user-level thread may yield voluntarily, reach an operation the runtime knows cannot proceed, or be preempted by a runtime mechanism. Those policy details belong to the runtime rather than to the kernel's ordinary thread scheduler.
A kernel-level thread is an execution context the kernel represents as an independent schedulable object.
For an application thread, the kernel maintains:
If a process has three kernel-level threads, the kernel sees three scheduling candidates:
Each thread is scheduled on its own merits. TID 7201's wait does not hold back the other two.
The scheduler can place TID 7200 and TID 7202 on different logical CPUs at the same time. It can block TID 7201 without treating the other two as blocked.
Modern Linux POSIX threads use this kernel-visible model. Each ordinary pthread corresponds to a Linux task with its own TID, even though the tasks share process-wide resources.
The phrase kernel-level thread does not mean that the thread executes application code in kernel mode. Application instructions still run in user mode. When the thread makes a system call or handles an exception, that same kernel-visible thread enters the kernel and later returns to user mode.
The thread runs application instructions in user mode, makes a system call, has the kernel serve it in kernel mode, and returns to user mode to continue.
The word kernel-level describes who manages and schedules the thread, not the privilege level of every instruction it executes.
In a many-to-one model, many user-level threads are mapped onto one kernel-level thread.
Four user-level threads funnel into one kernel thread, so the kernel can place at most one of them on a CPU at a time no matter how many cores the machine has.
The runtime decides whether A, B, C, or D runs when KLT 1 receives CPU time. Switching between them can remain entirely in user space.
This model gives the runtime complete control over scheduling among its user-level threads. It can use application-specific information that the kernel does not understand. It also avoids creating a kernel task for every user-level context.
The mapping has two fundamental limitations.
First, it cannot execute more than one user-level thread at the same instant. The kernel sees only KLT 1, and one kernel thread can occupy at most one logical CPU at a time. A machine may have 32 available CPUs while this application uses only one.
Second, a blocking kernel operation can make the only KLT non-runnable:
B and C are ready to run and still cannot, because the runtime scheduler that would pick them needs the very kernel thread that is now blocked.
The kernel does not know that B and C could make progress. Their readiness exists only in user-space data managed by a runtime that is not executing.
A runtime can reduce this problem by arranging non-blocking operations, intercepting known blocking calls, or moving blocking work elsewhere. Those techniques change how often the limitation appears; they do not change the fact that one blocked kernel thread removes the model's only kernel scheduling opportunity.
In a one-to-one model, each user-visible application thread corresponds to one kernel-level thread.
One kernel thread per application thread means all four can be on CPUs at once, and one blocking call stalls only its own thread.
There is no second population of application threads being multiplexed over fewer kernel threads. The kernel can schedule each thread directly.
If Thread A enters a blocking system call, the kernel blocks KLT A. Threads B, C, and D remain independent candidates:
The model can use multiple CPUs naturally because the kernel sees multiple runnable threads. Operating-system tools can also report each thread's TID, CPU consumption, state, scheduling policy, and CPU affinity.
The tradeoff is that every application thread consumes a kernel-managed thread object and associated resources. Creating a very large population therefore creates an equally large kernel-visible population.
The kernel also schedules with operating-system information. It knows CPU usage, priorities, affinity, and runnable state, but it does not inherently understand which application request is most valuable or which user-level tasks belong to one higher-level operation.
One-to-one is the normal thread model on Linux for POSIX threads. It provides a direct relationship between what the application calls a thread and what tools such as ps -L show as a TID.
In a many-to-many model, a runtime multiplexes many user-level threads over a set of kernel-level threads, often called workers or carriers in this context.
Six user-level threads run on three kernel threads, and KLT 3 has no CPU at this instant. Two schedulers are making decisions here, and neither can see the other's queue.
Here, six user-level threads are available, but only three kernel-level threads exist. At most three of the user-level threads can execute simultaneously.
The runtime chooses which user-level thread runs on each available KLT. The kernel independently chooses which KLTs receive CPUs.
Many-to-many tries to combine two properties:
If KLT 1 blocks, KLT 2 and KLT 3 can still run other user-level threads. The runtime may also adjust its carrier population if its design permits.
The flexibility introduces coordination complexity. The runtime must track which user-level thread is mounted on which KLT, preserve thread-local semantics, integrate blocking operations, handle asynchronous events, and expose useful information to debuggers and profilers.
The model also does not create unlimited parallelism. Ten thousand ready user-level threads mapped onto eight runnable KLTs can use at most eight logical CPUs at an instant.
A two-level variation allows some user-level threads to be bound to particular kernel threads while others are multiplexed. The essential idea remains the same: user-level contexts and kernel scheduling contexts are separate populations with a controlled mapping between them.
The mapping determines what the kernel can see and what the runtime must manage:
| Property | Many-to-one | One-to-one | Many-to-many |
|---|---|---|---|
| User-level contexts | Many | One per KLT | Many |
| Kernel-level threads | One | Same count as application threads | A controlled set |
| Runtime schedules user contexts | Yes | No separate multiplexing layer | Yes |
| Parallel execution within the process | Limited to one CPU | Up to the runnable KLT count | Up to the runnable carrier count |
| One blocking KLT stops all application work | Yes | No | Usually no, while other carriers can run |
| Kernel observes each application thread | No | Yes | Kernel sees carriers, not every user-level thread |
| Kernel resources per application context | Low | One KLT per context | One KLT per carrier |
The table describes pure models. Real runtimes may add non-blocking I/O, special handling for blocking calls, dynamic workers, or pinned tasks. Those mechanisms refine behavior without changing the central question:
How many user-managed execution contexts map to how many kernel-scheduled contexts?
Loading simulation...
The word blocked can refer to two different states in a layered threading system.
A user-level thread may be runtime-blocked while waiting for another user-space event. The runtime records that state and runs a different user-level thread on the same KLT. The kernel may continue to see the KLT as running or runnable.
When ULT A waits for a runtime event, the runtime marks A waiting and runs ULT B instead. From the kernel's view, the carrier thread stayed runnable the whole time.
A kernel-level thread is kernel-blocked when the kernel determines that it cannot make progress until an event occurs. The KLT leaves the runnable population, so no user-level work can execute on that KLT until it wakes.
When ULT A performs a blocking kernel operation, the kernel marks the carrier thread waiting, and the runtime cannot use that kernel thread for ULT B.
This distinction explains an otherwise puzzling observation: a runtime may report thousands of ready tasks while the operating system reports only a handful of runnable threads.
The tasks are ready at the user level. They still require a runnable carrier and CPU time from the kernel before they can execute.
With user-level threading, scheduling decisions form a hierarchy.
Suppose a runtime has ten ready user-level threads mapped onto two KLTs:
The runtime can prioritize work using application knowledge. It might know that one task is on a request's critical path while another performs background maintenance.
The kernel has a wider system view. It balances the runtime's KLTs against threads belonging to databases, shells, other services, and the kernel itself.
Neither scheduler can fully replace the other:
Kernel settings such as CPU affinity and operating-system scheduling priority apply to the KLTs. In a many-to-many design, every ULT currently using one carrier inherits that carrier's opportunity to run unless the runtime remaps it.
This can make performance behavior less obvious. A low-priority carrier may temporarily host user-level work the application considers urgent, while the kernel sees only the carrier's priority.
Kernel tools report kernel-visible threads.
On Linux, inspect the threads of a process with:
The rows correspond to Linux tasks. For a one-to-one POSIX-threaded application, each application thread normally has a row.
For a many-to-many runtime, the command shows the carrier KLTs. It does not automatically show every user-level thread scheduled on those carriers. A process with 100,000 user-level tasks might have only eight rows because the kernel sees eight KLTs.
The same boundary affects accounting:
This is operationally important. “The process has eight threads” may mean it has eight kernel threads, while a runtime diagnostic reports thousands of user-level execution contexts. Both numbers can be correct because they describe different layers.
The phrase kernel thread is used for two related but different concepts.
First, a kernel-level application thread is a user execution context that the kernel schedules directly. It normally has a user-space stack, executes application code in user mode, and enters kernel mode for system services.
Second, an operating system can create kernel-only threads that execute kernel functions and do not run an ordinary user-space program. Linux uses such threads for work including deferred device processing, background writeback, and memory management.
| Kernel-level application thread | Kernel-only thread | |
|---|---|---|
| What it runs | User application code | A kernel function |
| How it uses the kernel | System calls into the kernel | Kernel work only |
| User context | Has one | No ordinary user program to return to |
Both are kernel-scheduled tasks. Both can become runnable, sleep while waiting, consume CPU time, and be selected by the scheduler. Their purpose and execution environment differ.
On a Linux host, commands such as:
may show kernel-only tasks with names such as kthreadd and kworker/.... The exact set depends on the kernel, hardware, and current workload. Containers often cannot see the host's kernel threads because the container has a restricted process view.
Kernel-only threads are not additional threads inside a user process. They belong to the kernel's own internal work and cannot directly execute application functions from a process's heap or stack.
A user-level thread can request operating-system services. Its system call executes through the kernel-level thread currently carrying it.
Suppose ULT C is running on KLT 2:
The kernel identifies KLT 2 as the caller. It does not gain a permanent schedulable object for ULT C merely because C made a system call.
Similarly, a kernel-level application thread is not permanently in the kernel. It spends ordinary application execution in user mode.
The terms answer different questions:
Keeping these axes separate prevents a large class of terminology mistakes.
User-level management gives a runtime control that a general-purpose kernel scheduler does not have.
The runtime can create execution contexts with stack sizes and metadata tailored to its needs. It can switch among them using user-space data and select work according to application semantics. A many-to-many runtime can support far more suspended user contexts than it has kernel threads.
These properties are valuable when an application has a large amount of concurrent work that frequently waits and when the runtime can integrate that waiting with its scheduler.
The advantages are not automatic. Small user stacks can still grow, runtime metadata still consumes memory, and millions of waiting contexts still have bookkeeping costs. If user-level work performs long CPU computations without giving the runtime a chance to reschedule, other work sharing that carrier may be delayed.
User-level scheduling therefore moves responsibility rather than removing it. The runtime must decide when work yields, how waiting is represented, how carriers are used, and how application tasks remain observable.
Only kernel-visible threads can be placed directly on CPUs by the operating system. They are the bridge between application execution and hardware scheduling.
Kernel-level threads provide:
Even a runtime built around user-level threads ultimately needs one or more kernel-scheduled contexts. User-space scheduling cannot bypass the kernel scheduler or assign itself a CPU.
The design question is therefore not whether kernel-level threads exist. It is how many the application exposes to the kernel and whether another population of user-level contexts is multiplexed over them.
A user-level thread is represented and scheduled by a user-space runtime. A kernel-level thread is represented and scheduled directly by the operating system.
Many-to-one mapping offers user-space control but limits parallelism and makes one blocked KLT affect all user-level work. One-to-one mapping gives every application thread independent kernel scheduling and blocking. Many-to-many mapping places many user contexts over several carrier KLTs, combining runtime scheduling with bounded kernel-visible parallelism.
The kernel sees and schedules only KLTs. Any additional user-level concurrency exists as runtime-managed state above that kernel boundary.
5 quizzes