Skip to main content

Command Palette

Search for a command to run...

Understanding Linux Kernel Threads and Spinlocks

Updated
13 min readView as Markdown
Understanding Linux Kernel Threads and Spinlocks

Introduction

Concurrent execution is an important concept in operating systems and kernel development. Modern processors can execute multiple tasks concurrently, often across multiple CPU cores. When multiple execution contexts access the same shared resource, synchronization becomes necessary to prevent inconsistent or unexpected results.

Linux provides several synchronization mechanisms for kernel development. One of the commonly used mechanisms is the spinlock.

This article explains the concept of combining Linux kernel threads with a spinlock-protected shared counter. The focus is on understanding the design, synchronization mechanism, execution flow, and expected behavior rather than implementation details.


What Is a Linux Kernel Thread?

A Linux kernel thread is a thread that executes entirely within kernel space.

Kernel threads are useful for performing background work that is managed by the kernel rather than directly by a user-space application.

A kernel thread:

  • Runs in kernel space.
  • Is scheduled by the Linux scheduler.
  • Can execute concurrently with other kernel threads.
  • Can access kernel data structures and resources.
  • Can be started and stopped by kernel code.
  • Can execute independently of a normal user-space process.

Kernel threads are commonly used for background kernel activities such as:

  • Deferred processing.
  • Device-related operations.
  • Background maintenance.
  • Resource management.
  • Kernel subsystems that require independent execution contexts.

Why Synchronization Is Necessary

Consider a shared counter accessed by two kernel threads.

Both threads perform repeated increments of the same variable.

At first glance, incrementing a counter appears to be a simple operation. However, an increment is conceptually composed of multiple steps:

  1. Read the current value.
  2. Add one to the value.
  3. Write the new value back.

When two threads perform these operations concurrently, their actions can overlap.

For example:

Thread 1 Thread 2
Reads the counter
Reads the same counter
Adds one
Adds one
Writes the result
Writes the result

Both threads may calculate their new value from the same original value.

As a result, one increment can effectively overwrite another increment.

This situation is known as a race condition.


What Is a Race Condition?

A race condition occurs when the final result of an operation depends on the timing or ordering of concurrent execution.

Race conditions are particularly dangerous in kernel programming because kernel code often works with shared resources used by multiple execution contexts.

A race condition can result in:

  • Incorrect counter values.
  • Corrupted data.
  • Inconsistent state.
  • Difficult-to-reproduce bugs.
  • Unexpected system behavior.

The problem is not necessarily that either thread is incorrect individually. The problem is that both threads are accessing shared state without sufficient synchronization.


The Role of a Spinlock

A spinlock provides mutual exclusion.

Its purpose is to ensure that only one execution context can enter a protected critical section at a time.

The basic concept is:

  1. A thread attempts to acquire the lock.
  2. If the lock is available, the thread acquires it.
  3. The thread accesses the protected resource.
  4. The thread releases the lock.
  5. Another waiting thread can acquire the lock.

This prevents two threads from modifying the same protected data simultaneously.


Critical Section

A critical section is a portion of code that accesses shared data and therefore must be protected from concurrent modification.

In the counter example, the critical section contains the counter update.

The conceptual execution becomes:

Step Thread 1 Thread 2
1 Acquires lock Waits
2 Updates counter Waits
3 Releases lock Waits
4 Continues execution Acquires lock
5 Continues Updates counter
6 Continues Releases lock

The important property is that the shared counter update is serialized.


How the Two Threads Work

The example uses two kernel threads.

Each thread performs a fixed number of counter updates.

The threads share:

  • A common counter.
  • A common spinlock.

Each thread also maintains its own individual count.

This provides two different types of information:

  • The shared counter shows the total number of protected operations.
  • The individual counters show how many operations each thread performed.

This makes the example useful for understanding both synchronization and concurrent execution.


Shared Data and Private Data

The design can be divided into shared and thread-specific data.

Data Purpose Shared?
Counter Stores the total number of increments Yes
Spinlock Protects shared data Yes
Thread 1 count Tracks Thread 1 operations No, logically thread-specific
Thread 2 count Tracks Thread 2 operations No, logically thread-specific
Thread identifiers Identify the executing worker No

The shared counter requires synchronization because both threads modify it.


Why the Spinlock Protects the Counter

Without synchronization, both threads can access the counter at the same time.

With a spinlock, access becomes mutually exclusive.

The conceptual relationship is:

Two kernel threads → One shared counter → One spinlock → Controlled access

The lock does not prevent the threads from existing simultaneously.

Instead, it controls when they are allowed to access the protected resource.

This distinction is important.


Spinlocks and Waiting

A spinlock is different from a traditional sleeping synchronization mechanism.

When a thread cannot immediately acquire a spinlock, it waits by repeatedly checking the lock rather than putting itself to sleep.

This behavior is called spinning.

Spinning can be useful when:

  • The protected critical section is very short.
  • The expected waiting time is small.
  • Sleeping is not appropriate in the current execution context.

However, spinning also consumes CPU resources while waiting.

Therefore, spinlocks should generally protect short critical sections.


Why the Critical Section Should Be Small

A spinlock should not be held for unnecessarily long periods.

Suppose Thread 1 acquires a spinlock and performs a lengthy operation while holding it.

Thread 2 cannot enter the protected section during that period and may continuously spin while waiting.

This wastes CPU time.

A good synchronization design therefore aims to:

  • Acquire the lock as late as practical.
  • Perform only necessary protected operations.
  • Release the lock as soon as possible.
  • Avoid lengthy operations while holding the lock.

In the counter example, the protected operation is intentionally small, making it suitable for demonstrating a spinlock.


Initial Counter Update

The combined example also performs a protected counter update during module initialization.

This demonstrates that spinlocks are not limited to kernel-thread synchronization.

A spinlock can protect shared kernel data whenever multiple execution contexts could potentially access that data.

The initial update establishes a starting value before the worker threads perform their operations.


Final Counter Calculation

The example performs one initial increment.

Each of the two worker threads then performs one hundred thousand increments.

Therefore, the expected total is:

Source Increments
Module initialization 1
Thread 1 100,000
Thread 2 100,000
Total 200,001

The final counter therefore demonstrates that the protected updates were successfully preserved.


Individual Thread Counters

The example also maintains an individual count for each worker.

The expected values are:

Thread Expected Operations
Thread 1 100,000
Thread 2 100,000

These counters provide additional visibility into the work performed by each kernel thread.

They also help verify that both threads completed their expected workloads.


Kernel Thread Lifecycle

A kernel thread has a lifecycle similar to other kernel-managed execution contexts.

The general lifecycle is:

  1. Module initialization begins.
  2. The shared state is initialized.
  3. The first worker thread is created.
  4. The second worker thread is created.
  5. Both threads execute concurrently.
  6. Each thread performs its assigned work.
  7. Each thread finishes.
  8. The module can later be unloaded.
  9. Cleanup ensures the thread resources are properly handled.

This lifecycle demonstrates an important principle of kernel programming:

Resources created by a kernel module must be managed carefully and cleaned up appropriately.


Stopping Kernel Threads

The worker design also supports a stop request.

A kernel thread should not simply be assumed to terminate immediately when the module is being removed.

Instead, the thread can periodically check whether it has been asked to stop.

This creates a cooperative termination mechanism.

The general concept is:

  • The module requests that the worker stop.
  • The worker notices the stop request.
  • The worker exits its execution loop.
  • The worker returns.
  • Cleanup continues.

This is safer than assuming that a running kernel thread can simply be terminated from outside.


Error Handling

Kernel programming requires careful error handling because resource creation can fail.

For example, the first thread may be created successfully while the second thread fails to start.

In that situation, the module should not simply abandon the first thread.

The appropriate design is:

  1. Detect the second thread creation failure.
  2. Stop the already-created first thread.
  3. Clean up the partially initialized state.
  4. Return the appropriate error.

This is an example of failure-path cleanup.

Good kernel code should consider both:

  • The successful execution path.
  • The partial-failure path.

Why Error Handling Matters More in Kernel Code

Errors in kernel code can have consequences beyond a single application.

Poor cleanup can result in:

  • Resource leaks.
  • Threads continuing to run unexpectedly.
  • Invalid references.
  • Kernel instability.
  • Difficult-to-debug failures.

Therefore, kernel modules should carefully manage every resource they create.


Spinlock Versus No Synchronization

The difference can be summarized conceptually:

Without synchronization With spinlock
Multiple threads can update shared data simultaneously Access is serialized
Race conditions are possible Race conditions are prevented for the protected section
Final result may be incorrect Final result is deterministic under the demonstrated workload
Shared data is vulnerable to concurrent modification Shared data is protected
Debugging can be difficult Access rules are explicit

The spinlock does not make the threads execute sequentially overall.

It only serializes access to the protected critical section.


Spinlock Versus Mutex

Spinlocks and mutexes are both synchronization mechanisms, but they have different characteristics.

Feature Spinlock Mutex
Waiting behavior Spins Can sleep
CPU usage while waiting Higher Lower
Suitable for very short critical sections Yes Yes
Can sleep while holding it No Generally no sleeping-dependent usage within the critical section
Common use Short kernel critical sections Longer operations where sleeping is acceptable
Main advantage Very low waiting overhead for short waits Avoids wasting CPU while waiting

The choice depends on the execution context and the expected duration of the critical section.

A spinlock is particularly useful when the protected operation is short and sleeping is not appropriate.


Important Kernel Programming Considerations

When using spinlocks in Linux kernel code, several principles are important.

Keep Critical Sections Short

Long critical sections increase contention and can waste CPU resources.

Do Not Sleep While Holding a Spinlock

A spinlock is designed for atomic, non-sleeping sections of kernel code.

Protect the Correct Data

The lock must protect every access that requires synchronization.

Protecting only some accesses can still leave race conditions.

Release the Lock

Every successful lock acquisition must have a corresponding release.

Handle Failure Paths

If initialization partially succeeds, previously allocated or started resources must be cleaned up.


What This Example Demonstrates

The small kernel module combines several important operating-system concepts:

  • Kernel module lifecycle.
  • Kernel threads.
  • Shared memory.
  • Race conditions.
  • Mutual exclusion.
  • Spinlocks.
  • Critical sections.
  • Thread synchronization.
  • Thread termination.
  • Error handling.
  • Resource cleanup.

Because the example uses a simple counter, the synchronization behavior is easy to understand without introducing unnecessary kernel subsystems.


Conceptual Execution Flow

The overall flow can be summarized as:

Module initialization

The module initializes its shared state and performs an initial protected update.

Thread creation

Two worker threads are created.

Concurrent execution

Both threads begin performing their assigned operations.

Synchronization

Before modifying the shared counter, each thread must obtain the spinlock.

Critical section

The counter and corresponding thread-specific statistics are updated.

Lock release

The thread releases the spinlock so another thread can access the protected data.

Thread completion

Both workers eventually complete their workloads.

Module cleanup

The module ensures that the worker threads have stopped and then reports the final statistics.


Key Learning Points

The most important lessons from this example are:

  • Concurrent execution creates the possibility of race conditions.
  • Shared kernel data must be synchronized appropriately.
  • A spinlock provides mutual exclusion for a critical section.
  • Spinlocks are especially useful for short, non-sleeping critical sections.
  • Kernel threads can execute concurrently and access shared kernel state.
  • Thread termination should be handled cooperatively.
  • Kernel modules must clean up resources during failure and removal.
  • Synchronization protects data; it does not eliminate concurrency.
  • A good critical section should be as small as practical.
  • Correct synchronization makes concurrent behavior predictable.

Conclusion

Linux kernel threads provide a mechanism for executing kernel-level work concurrently. While concurrency improves flexibility and allows multiple activities to progress independently, it also introduces synchronization challenges.

A shared counter is a simple but effective example of this problem. Two kernel threads can access the same variable simultaneously, creating the possibility of a race condition. A spinlock solves this problem by ensuring that only one thread at a time can enter the critical section responsible for modifying the shared data.

The combined design demonstrates more than just how a spinlock works. It also illustrates kernel-thread lifecycle management, error handling, protected initialization and cleanup, and the importance of keeping synchronization regions small.

The broader lesson is fundamental to kernel development:

Concurrency requires synchronization whenever multiple execution contexts access shared mutable state.

Understanding this principle provides a strong foundation for studying more advanced Linux kernel synchronization mechanisms and concurrent operating-system design.

GitHub Repository: 👉 spin_lock_1 Explore the complete source code, build files, and module implementation on GitHub.