COMP 3150Operating Systems

01 Introduction

Lesson 4.2 examined the user-thread model and identified two fundamental limitations. First, a blocking system call in any one user thread stalls the entire process, because the kernel suspends the process and the library cannot run. Second, user threads of one process cannot run on multiple CPU cores at once, because the kernel sees only one schedulable entity.

Both limitations have the same root cause: the kernel does not know the threads exist. The natural fix is to change that — to make the kernel aware of each thread and let it schedule them independently. This is the kernel-thread model, and it is what modern general-purpose operating systems — Linux, Windows, macOS, and the BSDs — actually implement.

Kernel threads solve both fundamental problems. If one thread blocks, the kernel simply schedules another. If the machine has multiple cores, the kernel can dispatch different threads to different cores and get true parallelism. The cost is that thread operations now require kernel involvement: creating and switching threads is more expensive than in the user-thread model, because the kernel must allocate resources and take a trap. The trade-off is worth it for the vast majority of workloads.

This lesson defines what a kernel thread is, examines how the kernel represents and schedules threads, presents the four classical threading models that combine user and kernel support, analyses the costs, and closes with the concrete Linux implementation. It also distinguishes kernel-managed user threads from Linux's own internal kernel threads — a related but distinct concept.

Prerequisites

This lesson assumes you have read Lesson 4.2 and understand the blocking problem, the user-thread switch mechanism, and the cooperative scheduling model. The classification of threading models also depends on the concept of "user thread" developed in Lesson 4.2 and the concept of "kernel thread" developed here.

02 Learning objectives

By the end of this lesson you will be able to

  • Define kernel-level threads.
  • Describe kernel thread scheduling.
  • Compare threading models.
  • Analyse system-call costs.
  • Evaluate implementation strategies.

This lesson contributes to U4.3 and to course learning outcomes CLO2 and CLO8.

03 Required reading

  • Operating System Concepts — Chapter 4, Section 4.3, Multithreading Models, and Section 4.5, Multicore Programming. The classical taxonomy of the four threading models and the multicore considerations.
  • OSTEP — Chapter 26, the sections on kernel-level threading and the threading APIs. OSTEP's framing of the trade-offs is concise.
  • Love, R. — Linux Kernel Development, 3rd ed., Chapter 3 (Process Management). The section on task_struct and thread creation via clone.
  • Drepper, U., & Molnar, I. — The Native POSIX Thread Library for Linux. Red Hat, 2003. The design document for NPTL, the implementation Linux uses today.
  • Linux man pages: man 2 clone, man 7 pthreads, man 3 pthread_create, man 5 proc (for /proc/<pid>/task/), man 1 ps (the -L and -T options).

Reading strategy

Read Silberschatz Section 4.3 first for the formal taxonomy of the four threading models — this is the classical presentation, and the SVG figure in this lesson is derived from it. Then read Love's section on thread creation to see how Linux actually implements the one-to-one model, and skim the Drepper–Molnar paper for the design rationale behind NPTL.

04 Core content

4.1 What a kernel thread is

A kernel-level thread — often just called a "kernel thread" in this context — is a thread that the kernel creates, schedules, and manages directly. The kernel is aware of the thread as an independent schedulable entity, not as part of some larger process that it schedules as a unit. Each kernel thread has:

  • Its own kernel-side task structure. On Linux this is a task_struct — the same structure that represents a process. A thread is, from the kernel's point of view, a task that happens to share some resources with other tasks.
  • Its own kernel stack. When the thread makes a system call or handles an interrupt, it uses its own kernel-mode stack, separate from every other thread's. On Linux the kernel stack is typically 8 or 16 KB, allocated when the thread is created.
  • Its own register state. The program counter, general-purpose registers, and status flags are saved and restored by the kernel on each context switch, just as they are for a process.
  • Its own scheduling record. Priority, virtual runtime, and the other scheduling fields are per-thread. The kernel's scheduler dispatches threads, not processes.

What a kernel thread does not have, in general, is a private address space. Threads of the same process share one address space; the kernel's memory-management structure (mm_struct on Linux) is shared by all threads of that process. This is what makes a kernel thread cheaper than a kernel process: the kernel does not need to duplicate the page table or the file descriptor table.

4.2 The kernel's view: tasks and thread groups

On Linux, the kernel represents every schedulable entity with the same data structure, struct task_struct, introduced in Lesson 3.3. A process and a thread are both instances of task_struct; the difference is in which fields they share.

Specifically, threads of the same process share:

  • The address space — the mm_struct is the same.
  • The file descriptor table — the files_struct is the same.
  • The signal handlers — the sighand_struct is the same.
  • The credentials — the cred structure is shared.
  • The thread group ID (TGID), which is what user space calls the "process ID" of the group.

And each thread has its own:

  • Kernel stack — a separate region of kernel memory.
  • User-space stack — a separate region of the shared address space.
  • Program counter and register set.
  • Thread ID (TID) — distinct from the PID of the group.
  • Scheduling parameters — priority, CFS virtual runtime, time slice.

This design is sometimes described as "Linux has no concept of a thread" — because both processes and threads are tasks. In fact Linux has a very precise concept of a thread: it is a task that shares a set of resources with other tasks in the same thread group. The distinction between processes and threads is not about a separate data structure, but about which resources are shared.

4.3 How kernel threads solve the blocking problem

Return to the scenario from Lesson 4.2. Thread 1 calls read on a slow device. The kernel's read handler determines that the data is not immediately available and marks the calling task as Blocked, moving it off the run queue.

With kernel threads, this is where the story differs. The kernel has moved one task to the Blocked state. The other threads of the same process are separate tasks, each with its own state in the process table. The scheduler looks at the ready queue and finds those other threads — runnable, waiting for a CPU. It dispatches one of them.

Thread 2 now runs. It has no idea that Thread 1 is blocked in the kernel; it just sees the shared memory as it was, and continues executing. When the disk eventually responds, the kernel wakes Thread 1 (moving it from Blocked to Ready) and it will be scheduled again in due course.

From the application's point of view, the process has simply kept running — one thread waiting on I/O, the others continuing their work. This is what "concurrency" was supposed to mean all along, and it is what user threads could not deliver.

The key insight

The blocking problem is solved by making the kernel aware of each thread as an independently schedulable entity. The kernel does not need to know what the threads are doing together; it just needs to know that when one blocks, the others are still runnable. Every thread has its own state field in its own task_struct, so the kernel can move one to Blocked without affecting the others.

4.4 How kernel threads solve the multicore problem

The multicore problem is solved by the same change in the kernel's model. Because each thread is an independent schedulable entity, the kernel can dispatch different threads of the same process onto different CPU cores simultaneously. Thread 1 runs on core 0 while Thread 2 runs on core 1 while Thread 3 runs on core 2 — all of them executing code from the same address space at the same time.

This is what makes multithreaded programs scale. A program that was limited to one core as a single-threaded process becomes capable of using all available cores as a multithreaded one. The performance gain can be large — a four-thread program on a four-core machine can complete in roughly a quarter of the time if the workload is parallelisable.

The kernel's job in supporting this is straightforward: its scheduler must be able to run more than one task at once, and its per-CPU run queues must be able to hold tasks from the same thread group. Modern Linux does exactly this with per-CPU run queues and a global load balancer. Lesson 4.4 examines the details of multicore scheduling.

4.5 The four threading models

Once we have both user-level threads and kernel-level threads, the question becomes how to combine them. The classical literature identifies four models, distinguished by the relationship between user threads (as the application sees them) and kernel threads (as the kernel schedules them).

The four multithreading models Four panels. Many-to-one: many user threads mapped to one kernel thread. One-to-one: each user thread has its own kernel thread. Many-to-many: many user threads mapped to a smaller or equal number of kernel threads. Two-level: user threads may be multiplexed onto kernel threads, with some bound one-to-one. THE FOUR MULTITHREADING MODELS Many-to-one Many user threads → one kernel thread U1 U2 U3 U4 K1 One kernel-visible task No parallelism Blocking stalls the process Fastest user-level switch Example: early green threads One-to-one Each user thread → its own kernel thread U1 U2 U3 U4 K1 K2 K3 K4 Kernel schedules each thread True parallelism possible Blocking is per-thread Kernel-level switch cost Example: Linux NPTL, Windows Many-to-many Many user threads → fewer or equal kernel threads U1 U2 U3 U4 K1 K2 Library multiplexes user threads Parallelism possible Blocking is per kernel thread Complex implementation Example: Solaris LWP, hybrid Go runtime Two-level Many-to-many, plus some user threads bound one-to-one U1 U2 U3 U4 K1 K2 K3 Flexible: some bound, some pooled Parallelism + efficiency Historical; largely superseded user thread kernel thread Arrows show which user threads are scheduled by which kernel threads.
Figure 4.3 — The four multithreading models. Each panel shows how user threads (top) map to kernel threads (bottom). The trade-offs are: the more kernel threads, the more parallelism and blocking tolerance, but the higher the cost of thread management.
Many-to-one
Many user threads multiplexed onto a single kernel thread. This is the user-thread model of Lesson 4.2, viewed from the perspective of a system that also supports kernel threads. Fastest switching, but no parallelism and the blocking problem remains.
One-to-one
Each user thread has its own kernel thread. The kernel schedules each thread independently. True parallelism, blocking is per-thread. More expensive per thread. This is the model Linux, Windows, and macOS use.
Many-to-many
Many user threads multiplexed onto a smaller or equal number of kernel threads. Combines cheap user-level switching with partial parallelism and blocking tolerance. Complex to implement correctly. Solaris implemented this; most modern systems have moved away from it.
Two-level
A variant of many-to-many that also allows a user thread to be bound to a specific kernel thread. Historical; largely superseded by one-to-one in practice.

4.6 How many kernel threads?

A subtle but important design question: if the kernel is going to schedule threads, how many kernel threads should exist for a given number of user-level tasks? The answer differs by model and by system.

  • Many-to-one. Exactly one kernel thread per process, regardless of how many user threads the application creates. The application can create a million logical tasks but only one of them runs at a time.
  • One-to-one. One kernel thread per user thread, created on demand. The kernel schedules each thread independently. There is no multiplexing; the number of kernel threads equals the number of user threads.
  • Many-to-many. The library chooses a number of kernel threads — usually equal to the number of CPU cores, or a small multiple of that — and multiplexes its user threads onto them. This keeps the number of kernel-scheduled entities bounded while still exploiting all cores.

On Linux, the one-to-one model means that an application that creates 10,000 pthreads creates 10,000 kernel tasks. This is feasible because each kernel task is fairly small, but it does consume memory (kernel stack plus task_struct) and the scheduler's run queue becomes large. Language runtimes that want very large concurrency (Go, Erlang) use the many-to-many model internally, precisely so that the number of kernel- scheduled entities stays bounded even when the number of logical tasks is huge.

4.7 Costs of kernel threads

Kernel threads solve the two problems of the user-thread model, but they are not free. Three costs matter.

Creation cost

Creating a kernel thread requires a system call (on Linux, clone with the appropriate flags), allocation of a kernel stack (typically 8–16 KB), initialisation of a task_struct, and scheduling bookkeeping. On a typical system this costs tens of microseconds — roughly an order of magnitude more than creating a user thread, which requires only a library call.

Switching cost

A kernel-thread context switch requires a trap into the kernel, a scheduler decision, a save of the outgoing thread's state on its kernel stack, a restore of the incoming thread's state, and a return-from-trap. This is the process-switch cost from Lesson 3.5, with the simplification that the address space does not change. A kernel-thread switch is typically 5–50× more expensive than a user-thread switch — microseconds rather than tens of nanoseconds.

Memory cost

Each kernel thread needs a kernel stack — a separate region of kernel memory used while the thread executes in kernel mode. On Linux the default is 8 KB on 32-bit and 16 KB on 64-bit (though configurations vary). In addition, each thread's user-space stack consumes virtual address space, typically 8 MB by default. A process with thousands of threads can consume tens of gigabytes of address space, most of it unused.

4.8 Linux's implementation

Linux uses the one-to-one model, implemented through two mechanisms that work together: the clone system call and the Native POSIX Thread Library (NPTL).

clone. Linux creates both processes and threads with a single system call, clone. The difference is which resources the new task shares with its creator. The clone call takes a set of flags that control sharing:

/* A Linux thread is created with clone and a set of sharing flags */
clone(thread_fn, stack_top,
      CLONE_VM      |   /* share address space */
      CLONE_FS      |   /* share filesystem info (cwd, root) */
      CLONE_FILES   |   /* share file descriptor table */
      CLONE_SIGHAND |   /* share signal handlers */
      CLONE_THREAD  |   /* same thread group (same PID to user space) */
      CLONE_SYSVSEM,    /* share System V semaphore undo */
      arg);

The same clone call with different flags — omitting the sharing flags — creates a traditional process. This is why we said that Linux has no separate "thread" concept: it has one task creation primitive, parameterised by what is shared.

NPTL. The Native POSIX Thread Library is the userspace side of the same model. It was introduced in glibc 2.3 (2002) and integrated with the 2.6 kernel (2003). Before NPTL, Linux used LinuxThreads, which had significant problems — separate PIDs for threads, expensive signal-based synchronization, and incorrect POSIX semantics in several corner cases. NPTL solved these by using clone directly and by taking advantage of the kernel's futex mechanism for fast user-space synchronization (covered in Unit 6).

When you call pthread_create on a modern Linux system, here is what happens: NPTL calls clone with the sharing flags, the kernel creates a new task that shares the address space, file descriptors, and signal handlers of the caller, and a new pthread handle is returned to the caller. The new kernel task appears in /proc/<pid>/task/<tid> and is scheduled independently by the kernel's CFS scheduler.

4.9 Linux kernel threads (a distinct concept)

There is a naming collision worth clarifying. In Linux, the term "kernel thread" is also used for a different concept: a task that runs entirely in kernel mode and has no user-space component at all. These are the threads that handle background work inside the kernel — reclaiming memory, flushing dirty pages, running deferred work, migrating tasks between cores.

Examples include:

  • kswapd — the kernel's page reclaim daemon. It wakes when memory is low and scans pages for reclaim, running in the background so that user processes are not stalled by memory pressure.
  • kworker — the worker pool for deferred kernel work. File-system writeback, network cleanup, and many other tasks are executed by kernel workers.
  • migration/N — a per-CPU kernel thread that migrates tasks between CPUs when the load balancer decides to rebalance.
  • kthreadd — the parent of all kernel threads. It is PID 2 on a Linux system.

These tasks are visible in ps -ef with names in square brackets and PPID 2, and they can be scheduled by the kernel like any other task. But they are not the same as a user-created thread: they have no user-space stack, no user address space (they borrow the previous task's on a switch, which is why they appear with mm == NULL and use the concept of an "active_mm").

For the rest of this course, when we say "kernel thread" without qualification, we mean the first concept: a user-space thread that the kernel schedules independently. We will use "kernel daemon" or "kernel-internal thread" for the second.

4.10 When kernel threads are the right choice

Kernel threads are the default on modern general-purpose systems, and for most workloads they are the correct choice. But they are not universally better than user threads, and there are workloads where the user-thread model would be preferable in principle.

  • Kernel threads are right when blocking is possible. If any thread in the program might block — on I/O, on a lock, on a pipe, on a network — kernel threads give the other threads a way to keep running. This describes almost every real workload.
  • Kernel threads are right when parallelism is desired. Any program that wants to use more than one CPU core needs kernel threads (or processes). User threads cannot deliver parallelism on a single-process basis.
  • User threads can be better when neither applies. A program that is CPU-bound, runs entirely on one core, and never blocks can use user threads to get cheap cooperative multitasking. This is rare in practice, but the pattern still appears in embedded systems and in specialised runtimes.
  • Hybrid models can win when both apply but neither dominates. A runtime that knows its own workload (Go, Erlang) can use a many-to-many model to get cheap user-level switching when the workload is cooperative and kernel-level parallelism when it is not.

The history of the last twenty years is a shift toward kernel-thread models for the OS itself, and toward user-level task models inside language runtimes — with the runtime managing the complex interaction between the two. In both cases, the underlying primitive is the kernel thread; the difference is who schedules it.

05 Worked example

To make the kernel-thread model concrete, look at the same blocking scenario from Lesson 4.2 and see how kernel threads change the outcome, then measure the difference in cost.

Blocking behaviour: user threads vs. kernel threads

The program is the same in both versions: two workers, one of which will block on read partway through its loop. The question is whether the other worker continues.

Version 1 — user threads (the version from Lesson 4.2)

Using the minimal user-thread library from the previous lesson, worker A blocks on read. As we saw, the whole process stalls and worker B cannot run. The output is:

A: 0
B: 0
A: 1
A: about to read from stdin (will block)
# ... program hangs until input is provided ...
A: got input
B: 1
B: 2
... continues

Worker B's remaining iterations do not execute until A's read returns. From the perspective of the user-thread library, this is unavoidable: the library is inside the blocked process and cannot run.

Version 2 — kernel threads (POSIX threads)

The same program, rewritten with pthread_create instead of the user-thread library:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void *worker_a(void *arg) {
    (void)arg;
    for (int i = 0; i < 5; i++) {
        printf("A: %d\n", i);
        if (i == 1) {
            printf("A: about to read from stdin (will block)\n");
            char buf[16];
            read(0, buf, sizeof(buf) - 1);
            printf("A: got input\n");
        }
    }
    return NULL;
}

void *worker_b(void *arg) {
    (void)arg;
    for (int i = 0; i < 5; i++) {
        printf("B: %d\n", i);
        /* Busy work to keep the thread active */
        for (volatile long j = 0; j < 10000000L; j++) ;
    }
    return NULL;
}

int main(void) {
    pthread_t ta, tb;
    pthread_create(&ta, NULL, worker_a, NULL);
    pthread_create(&tb, NULL, worker_b, NULL);
    pthread_join(ta, NULL);
    pthread_join(tb, NULL);
    return 0;
}

Build and run:

gcc -Wall -Wextra -g -O2 -pthread -o kthreads kthreads.c
./kthreads

The output is now interleaved but continuous:

A: 0
B: 0
A: 1
A: about to read from stdin (will block)
B: 1
B: 2
B: 3
B: 4
# ... and after you press Enter ...
A: got input
A: 2
A: 3
A: 4

Worker B runs to completion while A is blocked on input. The kernel has scheduled B in place of the blocked A; when A's read returns, A is rescheduled and finishes its own work. This is the entire point of the kernel-thread model, made concrete in a program you can run.

Confirming the mechanism

While the program is blocked in the read, open another terminal and run:

ps -L -C kthreads -o pid,tid,stat,comm

You will see two kernel tasks — two rows — each with its own tid. One is in state S (blocked on the read); the other is in state R (running or runnable). The kernel knows about both and schedules them independently.

Measuring the cost difference

Now compare the cost of switching between the two threads against the user-thread measurement from Lesson 4.2.

Modify both workers so that instead of printing they call sched_yield a million times each, and time the run. The result is a rough upper bound on the kernel-thread switch cost:

Kernel-thread switch (approximate, on a modern x86-64 VM):
    Total wall-clock for 2 million yields:  0.68 s
    Cost per yield (two switches):          ~340 ns
    Cost per switch:                        ~170 ns

User-thread switch (measured in Lesson 4.2):
    Cost per switch:                        ~70 ns

Ratio (kernel/user):                      ~2.4×

The kernel-thread switch is roughly two to three times more expensive than the user-thread switch on the same hardware. The exact ratio depends on the machine and on the kernel version, but the order of magnitude is stable: microseconds vs. tens of nanoseconds. Notice that the difference is not enormous in this measurement — this is the result of decades of optimisation in Linux's context switch, and of fast hardware features such as PCID that avoid TLB flushes.

In exchange for the extra ~100 ns per switch, the kernel gives you the ability to run threads on multiple cores, and the assurance that a blocking call in one thread will not stall the others. The trade is heavily in the kernel's favour for almost every real workload.

Observing the kernel-side task

Finally, confirm that the kernel really is treating each thread as a separate task. While the program is running, look at the process's /proc directory:

PID=$(pgrep kthreads)
ls /proc/$PID/task/
# 1234  1235
# Each of these is a kernel task ID; there are two, one per thread.

cat /proc/$PID/status | grep -E 'Name|Tgid|Pid|Threads'
# Name:    kthreads
# Tgid:    1234
# Pid:     1234
# Threads: 2

cat /proc/$PID/task/1235/status | grep -E 'Name|Tgid|Pid'
# Name:    kthreads
# Tgid:    1234
# Pid:     1235
# ...
# Both tasks share the same Tgid (the thread group ID, which is what
# user space calls the process ID) but have distinct Pids (the kernel's
# per-task ID, which user space calls the thread ID).

This is the concrete representation of the one-to-one model on Linux. Two threads, two kernel tasks, one process — and the kernel schedules each task independently.

06 Interactive activity

Activity · explore kernel threads on your VM

Work through the following in your course VM. The goal is to see kernel threads as the kernel sees them, and to observe the difference between threads and processes at the kernel level.

  1. Compare the two models. Write two versions of a small program: one that uses POSIX threads (pthread_create) and one that uses fork. In both versions, have each child or thread print its own identifier and then sleep for thirty seconds. Run each and, while it is running, inspect it with ps -L and ps -eLf. Which model creates kernel tasks that share a PID? Which creates kernel tasks with distinct PIDs?
  2. Watch the thread count change. Write a program that creates N threads, where N is given on the command line, and each thread sleeps for a fixed time. While it is running, check the number of threads the kernel sees:
    PID=$(pgrep myprogram)
    cat /proc/$PID/status | grep Threads
    ls /proc/$PID/task/ | wc -l
    Do the two numbers match? Try with N = 1, 10, 100.
  3. Inspect a live thread's state. While a multithreaded program is running, list the state of each thread:
    ps -L -p $PID -o pid,tid,stat,pcpu,comm
    You should see rows with different tid values and different states (some S, some R). This is what independent scheduling looks like at the kernel level.
  4. Observe kernel-internal threads. Run:
    ps -ef | awk '$3 == 2'
    These are the kernel-internal threads described in section 4.9 — the kernel daemons with PPID 2 (kthreadd). Look for [kswapd0], [kworker/N], and others. Note the square brackets around their names — that is the conventional way to indicate that these are kernel-internal tasks, not user processes.
  5. Count kernel threads on your VM.
    ps -ef | awk '$3 == 2' | wc -l
    How many kernel-internal threads does your VM have? Which ones do you recognise from the lesson?
  6. Demonstrate the blocking solution. Build the two-thread program from the worked example, where thread A blocks on read while thread B continues. Confirm that B runs to completion before A's read returns. Then rebuild the same program using the user-thread library from Lesson 4.2 and confirm that B does not run — this is the concrete difference between the two models.
  7. Measure the switch cost difference. Time two programs that each perform one million thread switches (or yields): one using POSIX threads with sched_yield, and one using the user-thread library from Lesson 4.2 with uthread_yield. Record both timings and compute the ratio.

Reflect: which of these observations best illustrated that the kernel schedules threads independently? Which illustrated the cost of the kernel-mediated switch? What would you lose if the kernel treated all threads of a process as a single schedulable entity, as the user-thread model requires?

07 Knowledge check

Check your understanding

Question 1

What is the fundamental difference between a user thread and a kernel thread?

Show answer
A user thread is created, scheduled, and managed entirely by a library in user space; the kernel does not know it exists. A kernel thread is created, scheduled, and managed by the kernel itself; the kernel has a task_struct, a kernel stack, and scheduling metadata for each one. The kernel can block one without affecting the others, and it can schedule different threads onto different CPU cores independently. The cost is that thread operations require a trap into the kernel.

Question 2

Describe the one-to-one threading model and explain why it is the dominant model on modern systems.

Show answer
In the one-to-one model, each user thread is backed by its own kernel thread. The kernel schedules each thread independently, so the two problems of the user-thread model are both solved: a blocking call only blocks the calling thread (the kernel moves on to the others), and threads can run in parallel on multiple CPU cores. The cost is higher per thread — creating a kernel thread requires a system call and kernel resources — but on modern hardware this is a small price to pay. Linux (via NPTL), Windows, and macOS all use this model. It is the simplest model that solves both fundamental problems, which is why it dominates.

Question 3

What is the many-to-many threading model, and why has it largely been superseded?

Show answer
In the many-to-many model, many user threads are multiplexed onto a smaller or equal number of kernel threads. The library schedules the user threads; the kernel schedules the kernel threads. This gives cheap user-level switching when possible and true parallelism when needed, and it keeps the number of kernel-scheduled entities bounded even when the application creates many logical tasks. It was implemented in Solaris and other research systems. It has been largely superseded because (a) the implementation is complex — coordinating two levels of scheduling correctly is difficult — and (b) the one-to-one model on modern hardware has become cheap enough that the extra complexity is not worth it. The many-to-many idea survives inside modern language runtimes (Go, Erlang) where the runtime controls the whole program and can implement the coordination correctly.

Question 4

On Linux, what does the clone system call do, and why is it the same call used to create both processes and threads?

Show answer
clone creates a new task. The caller passes a set of flags that control which resources the new task shares with the caller. If the sharing flags (CLONE_VM, CLONE_FILES, CLONE_SIGHAND, CLONE_THREAD, and so on) are set, the new task shares the caller's address space, file table, and signal handlers — which is what makes it a thread. If they are cleared, the new task has its own copies of these resources — which is what makes it a process. The same primitive creates both because on Linux there is no fundamental distinction between the two at the kernel level; the distinction is entirely about which resources are shared. This is why Linux is sometimes described as "having no concept of a thread" — but it is more accurate to say that it has a single concept, the task, parameterised by sharing.

Question 5

Linux kernel threads such as kswapd and kworker are called "kernel threads" but are a different concept from user-space threads managed by the kernel. Explain the difference.

Show answer
A user-space thread managed by the kernel is a thread created by a user-space program; the kernel is aware of it and schedules it, but it runs user code and has a user-space stack, a user-space identity, and a share of the process's address space. A kernel-internal thread (also called a kernel daemon) is a task created by the kernel itself, runs entirely in kernel mode, has no user-space component, and typically has no address space of its own (it borrows the previous task's mm while running, using the concept of an active_mm). Kernel-internal threads execute the kernel's background work — memory reclaim, writeback, migration, deferred work. They appear in ps -ef with square brackets around their names and PPID 2. The two are both tasks in the Linux sense, but they serve different purposes and have different resource footprints.

08 Unit discussion

Discussion prompt

Many-to-many threading was once seen as the future of threading on general-purpose systems. It combined the cheap user-level switching of the user-thread model with the parallelism and blocking tolerance of the kernel-thread model, in a design that seemed to get the best of both worlds. It is now rare outside language runtimes.

Why did the industry move to the simpler one-to-one model, even though many-to-many is theoretically more efficient? Consider the trade-off between implementation complexity and the performance it buys, the rate at which hardware has improved, and the role of operating-system politics and standards. Argue for whichever explanation you find most persuasive, and identify one workload where many-to-many would clearly be better.

Post your response to the Unit 4 discussion forum and reply to at least two classmates. Supports CLO2 and CLO7.

09 Practice exercise

This ungraded exercise consolidates the lesson and prepares you for Lab 3 and the thread portion of Assignment 1. Aim to spend 60–90 minutes on it.

  1. Draw the four models. Without consulting Figure 4.3, draw the four threading models — many-to-one, one-to-one, many-to-many, two-level — showing user threads above, kernel threads below, and the mapping between them. Then check against the figure. Which model did you find hardest to draw, and why?
  2. Classify real systems. For each of the following systems, state which threading model it uses and justify your answer: Linux, Windows, macOS, early Java (green threads), modern Go, Solaris. You may consult the course materials, the man pages, or online references.
  3. Write the blocking demonstration. Build both versions of the blocking program from the worked example — one using POSIX threads, one using the user-thread library from Lesson 4.2. Confirm that the kernel-thread version allows the other thread to run while the first is blocked. Capture the output of both.
  4. Measure the switch cost difference precisely. Write two benchmark programs. The first uses POSIX threads and sched_yield; the second uses the user-thread library from Lesson 4.2. Each program should perform ten million context switches. Time both, compute the cost per switch, and record the ratio. Run each three times and report the median, not just the first result.
  5. Count kernel-internal threads. On your VM, use ps -ef to list every kernel-internal thread (PPID 2). How many are there? What is each one doing? (You may consult the kernel documentation or search online for each name.) Are any of them obviously specific to your VM's hardware or configuration?
  6. Read the clone man page. Read man 2 clone carefully. List the sharing flags and explain in one sentence what each one means. Which combination of flags would you pass to create a traditional process? Which to create a POSIX thread?
  7. Reflect. In your own words, explain why the kernel-thread model is the right default for a general-purpose operating system, and describe one workload where you would still prefer user threads.

10 Summary

  • A kernel-level thread is a thread the kernel knows about. The kernel has a task structure, a kernel stack, and scheduling metadata for each one, and it schedules each thread independently.
  • Kernel threads solve both fundamental limitations of the user-thread model: a blocking call only blocks the calling thread (the kernel moves on to the others), and threads can run in parallel on multiple CPU cores.
  • The four threading models describe how user threads map to kernel threads: many-to-one (user threads only), one-to-one (each user thread has its own kernel thread), many-to-many (multiplexed onto a bounded number of kernel threads), and two-level (many-to-many plus some bound threads).
  • Kernel threads cost more than user threads: creating one requires a system call and kernel resources; switching between them requires a trap into the kernel. The cost is typically a few microseconds, versus tens of nanoseconds for a user-thread switch. But the cost is well worth it for the capabilities it buys.
  • Linux uses the one-to-one model via two mechanisms: clone (the system call that creates tasks, parameterised by sharing flags) and NPTL (the userspace library that implements POSIX threads on top of clone).
  • Linux also has a distinct concept of kernel-internal threads — tasks that run entirely in kernel mode with no user-space component, such as kswapd, kworker, and migration/N. These are kernel daemons, not user threads, and appear in ps -ef with names in square brackets and PPID 2.
  • Modern language runtimes such as Go and Erlang implement many-to-many models internally — but the underlying primitive they build on is the OS kernel thread.

Looking ahead

Lesson 4.4 examines the multicore implications of threading in detail — cache coherence, false sharing, memory models, and the scaling limits that eventually bound what parallel threads can achieve. The kernel-thread model is a prerequisite for everything that lesson discusses: without it, threads could not run in parallel at all.

11 References

  • Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 4, Sections 4.3 and 4.5.
  • Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C. Operating Systems: Three Easy Pieces, Chapter 26. Available online at ostep.org.
  • Love, R. Linux Kernel Development, 3rd ed., Chapter 3 (Process Management). Addison-Wesley, 2010.
  • Drepper, U., & Molnar, I. The Native POSIX Thread Library for Linux. Red Hat, 2003.
  • Anderson, T. E., Bershad, B. N., Lazowska, E. D., & Levy, H. M. Scheduler Activations: Effective Kernel Support for the User-Level Management of Parallelism. ACM TOCS, 1992.
  • Linux man-pages: man 2 clone, man 7 pthreads, man 3 pthread_create, man 5 proc, man 1 ps.
👤