What are Atomic Operations in the Linux Kernel: A Beginner’s Guide to Critical Sections-Free Linux Device Drivers Training

Atomic Operations in the Linux Kernel: A Beginner’s Guide to Critical Sections
Part of the Free Linux Kernel Development Course on EmbeddedPathashala

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.

Linux Kernel Programming Atomic Operations Critical Section Kernel Synchronization Free Device Drivers Course
What You Will Learn
Why shared data needs protection What a critical section really is Why “i++” is not safe in the kernel How atomic_t and atomic64_t work today Modern refcount_t and atomic API changes Common mistakes and how to avoid them

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 insmod and rmmod)
  • 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.

Why open_count++ can go wrong on two CPUs
CPU 0
  1. Reads open_count = 5
  2. Adds 1 → 6 (in register)
  3. Writes 6 back to memory
CPU 1 (runs in between)
  1. Reads open_count = 5
  2. Adds 1 → 6 (in register)
  3. 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:

FactorWhat it ControlsCan You Rely on It?
Processor architecture (ISA)Whether the CPU has a single instruction that can increment memory directlyVaries across x86, ARM, RISC-V builds
Compiler and optimization levelWhether the compiler chooses to emit that single instructionNot guaranteed, and can change between builds
Important: Because both factors can vary between architectures, kernel builds, and even compiler versions, you should never assume a plain increment is atomic. Always use the kernel’s atomic API for shared counters.

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:

FunctionPurpose
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”
Tip: Modern kernels also provide 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.

Workflow of atomic_inc() across CPUs
CPU 0 calls atomic_inc()
→
Hardware locks the cache line / uses exclusive access
→
Value updated in one indivisible step
→
CPU 1’s atomic_inc() waits its turn automatically

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

MistakeWhy It’s a ProblemFix
Mixing plain int increments with atomic reads elsewhereOnly some accesses are protected — the race condition still existsUse the atomic type and atomic functions everywhere the variable is touched
Assuming atomic_t protects more than a single variableAtomicity applies only to that one variable, not to a group of related fieldsUse a spinlock or mutex to protect multi-field critical sections
Using atomic operations as a substitute for proper locking of complex data structuresAtomic ops guarantee atomicity, not ordering guarantees across multiple variablesCombine with memory barriers or use locks when several fields must stay consistent together

Best Practices for Using Atomic Operations

  • Prefer refcount_t over raw atomic_t for 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

Q1. Is atomic_t the same as declaring a variable as volatile?

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.

Q2. Do I need atomic operations on a single-core embedded system?

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.

Q3. What’s the difference between atomic_t and refcount_t?

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.

Q4. Can atomic operations replace spinlocks entirely?

No. Atomic operations only protect a single variable. If you need to update several related fields together consistently, you still need a lock.

Q5. Are atomic operations free in terms of performance?

They are cheaper than locks but not free — they still involve hardware-level synchronization and can cause cache-line contention under heavy concurrent access.

Q6. Where can I practice this for free?

This tutorial is part of EmbeddedPathashala’s free Linux kernel development course, where you can follow along with real kernel module examples.

Continue Your Free Linux Kernel Development Course

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

Leave a Reply

Your email address will not be published. Required fields are marked *