If you are learning kernel or device driver programming, understanding atomic operations in the Linux kernel is one of the very first skills you must build. Whether you are following a free Linux kernel development course, a free Linux device drivers course, or a free embedded systems course, you will repeatedly run into shared variables that are touched by multiple CPUs at the same time. This tutorial explains, in plain language, why a simple counter increment can silently corrupt data on a multi-core system, and how atomic operations solve that problem in modern kernels.
Prerequisites
Before starting this tutorial on atomic operations in the Linux kernel, it helps to be comfortable with the following:
- Basic C programming, including pointers and function calls
- A general idea of what a kernel module is (loading with
insmodandrmmod) - A Linux machine or virtual machine with kernel headers installed, so you can build modules
- Familiarity with the terms “process” and “CPU core” at a conceptual level
You do not need any prior synchronization or locking experience — that is exactly what this article, and the free Linux kernel development course series it belongs to, will teach you from scratch.
Why a Simple Counter Increment Is Not Safe
Imagine a shared integer that several parts of your driver update, for example a count of how many times a device file has been opened. A newcomer to kernel programming will often write something that looks like this:
static int open_count;
static int mydev_open(struct inode *inode, struct file *filp)
{
open_count++; /* looks harmless, but is it? */
return 0;
}
On the surface, open_count++ looks like one operation. In reality, the CPU has to do three separate steps to complete it: read the current value from memory into a register, add one to that register, and write the updated register value back to memory. This sequence — read, modify, write — is exactly what we call a critical section: a piece of code that touches shared, writable data and must not be interrupted halfway through by another thread of execution doing the same thing.
- Reads open_count = 5
- Adds 1 → 6 (in register)
- Writes 6 back to memory
- Reads open_count = 5
- Adds 1 → 6 (in register)
- Writes 6 back to memory
Result: two opens happened, but open_count only shows 6 instead of 7 — one update was silently lost.
This kind of bug is called a race condition, and it is one of the very first traps every kernel and device driver beginner falls into. The fix is to either serialize access with a lock, or — for a simple counter like this — use the kernel’s built-in atomic operations, which is the focus of the rest of this tutorial.
What Makes an Operation Truly Atomic
An operation is atomic when it completes as a single, indivisible unit — no other CPU or interrupt can observe it “half done.” Whether a plain i++ is atomic on a given system depends on two things:
| Factor | What it Controls | Can You Rely on It? |
|---|---|---|
| Processor architecture (ISA) | Whether the CPU has a single instruction that can increment memory directly | Varies across x86, ARM, RISC-V builds |
| Compiler and optimization level | Whether the compiler chooses to emit that single instruction | Not guaranteed, and can change between builds |
Using Kernel Atomic Operations the Right Way
The Linux kernel provides a dedicated atomic integer type, atomic_t (and atomic64_t for 64-bit counters), together with a family of functions that guarantee true atomicity on every supported architecture, regardless of what the compiler decides to do. Here is the safe version of the earlier example:
#include <linux/atomic.h>
static atomic_t open_count = ATOMIC_INIT(0);
static int mydev_open(struct inode *inode, struct file *filp)
{
atomic_inc(&open_count);
pr_info("mydev: opened %d times\n", atomic_read(&open_count));
return 0;
}
static int mydev_release(struct inode *inode, struct file *filp)
{
atomic_dec(&open_count);
return 0;
}
A few commonly used atomic functions you will meet throughout a free Linux kernel development course:
| Function | Purpose |
|---|---|
atomic_read(v) | Read the current value |
atomic_set(v, i) | Set the value directly |
atomic_inc(v) / atomic_dec(v) | Increment or decrement by 1, atomically |
atomic_add(i, v) / atomic_sub(i, v) | Add or subtract an arbitrary value |
atomic_dec_and_test(v) | Decrement and check if the result is zero, in one atomic step — very common in reference counting |
atomic_cmpxchg(v, old, new) | Compare-and-swap: update only if the current value matches “old” |
refcount_t (in linux/refcount.h) specifically for object reference counting. It behaves like atomic_t but adds extra checks to catch use-after-free and reference-counting bugs, and is generally preferred over a raw atomic_t when you are counting references to a driver or object.
How an Atomic Increment Is Implemented Under the Hood
You don’t need assembly knowledge to use atomic operations, but understanding the workflow builds real confidence. The kernel’s atomic functions are architecture-specific under the hood: on x86, many map to instructions with the LOCK prefix, which asserts exclusive ownership of the memory bus (or cache line, on modern CPUs) for that instruction. On ARM, they are typically built from load-exclusive/store-exclusive instruction pairs with a retry loop.
The key takeaway is that the kernel abstracts away all of this architecture-specific behavior behind one portable API, so your driver code stays identical whether it eventually runs on x86_64, ARM64, or RISC-V.
Real-World Use Cases for Atomic Operations
- Device open/close counters, as shown above, to know how many processes currently hold a device file open
- Reference counting for kernel objects such as
struct kobject, where the object is freed only when the count reaches zero - Statistics counters in networking and block-layer drivers, such as packets sent or I/O requests completed
- Flags and state machines that must be checked and updated safely from interrupt context
Common Mistakes When Working with Atomic Variables
| Mistake | Why It’s a Problem | Fix |
|---|---|---|
Mixing plain int increments with atomic reads elsewhere | Only some accesses are protected — the race condition still exists | Use the atomic type and atomic functions everywhere the variable is touched |
| Assuming atomic_t protects more than a single variable | Atomicity applies only to that one variable, not to a group of related fields | Use a spinlock or mutex to protect multi-field critical sections |
| Using atomic operations as a substitute for proper locking of complex data structures | Atomic ops guarantee atomicity, not ordering guarantees across multiple variables | Combine with memory barriers or use locks when several fields must stay consistent together |
Best Practices for Using Atomic Operations
- Prefer
refcount_tover rawatomic_tfor object lifetime/reference counting - Keep the scope of what needs atomicity as small and well-defined as a single variable
- Document why a variable is atomic, so future maintainers don’t “simplify” it back to a plain int
- Use tools like the kernel’s lock/atomic checkers and static analyzers during development
Performance Considerations
Atomic operations are generally much cheaper than a full spinlock, since they avoid the overhead of a separate lock/unlock pair and don’t block other unrelated code. However, on systems with many CPU cores, frequent atomic updates to the same cache line can still cause cache-line bouncing, where ownership of that memory location repeatedly moves between CPU caches. For extremely hot counters accessed by many cores, consider per-CPU counters that are only periodically summed, rather than a single global atomic variable.
Security Considerations
Reference-counting bugs are a well-known source of use-after-free vulnerabilities in the kernel. An incorrectly balanced atomic_inc/atomic_dec pair around object lifetimes can let a structure be freed while another CPU still holds a pointer to it. This is precisely why modern kernels encourage refcount_t, which adds built-in checks to detect saturation and underflow of the counter, turning a silent security bug into a loud, debuggable warning.
Summary and Key Takeaways
- A plain
i++on shared kernel data is a critical section and is not safe by default - Atomicity depends on both the CPU architecture and the compiler, so never assume it
- The kernel’s atomic API (
atomic_t,atomic64_t,refcount_t) gives you portable, guaranteed-atomic operations - Atomic operations protect a single variable — larger critical sections still need locks, which we cover in the next lecture
Conclusion
Getting comfortable with atomic operations in the Linux kernel is a foundational step in any free Linux kernel development course, free Linux device drivers course, or free embedded systems course. Once you understand why a simple increment can silently corrupt shared state, and how the kernel’s atomic API removes that risk portably across architectures, you are ready to move to the next building block of kernel synchronization: locks. That is exactly where the next lecture in this series picks up.
Frequently Asked Questions
No. volatile only tells the compiler not to optimize away memory accesses; it says nothing about hardware-level atomicity across CPUs. atomic_t functions provide actual atomic, multi-CPU-safe updates.
Often yes, because interrupts can still preempt your code at any point, causing the same kind of race condition even without a second CPU core.
refcount_t is built on top of atomic operations but adds extra protection against reference-count underflow and overflow bugs, making it the preferred choice for object lifetime counting.
No. Atomic operations only protect a single variable. If you need to update several related fields together consistently, you still need a lock.
They are cheaper than locks but not free — they still involve hardware-level synchronization and can cause cache-line contention under heavy concurrent access.
This tutorial is part of EmbeddedPathashala’s free Linux kernel development course, where you can follow along with real kernel module examples.
Next up: how locks work in the Linux kernel, and when to reach for a spinlock versus a mutex.
Next Lecture Back to Course Index.

2 Comments