funwithlinux blog

Which C Version Does the Linux Kernel Use? C90, C99, or C11 – Explained

The Linux kernel, the heart of the world’s most widely used operating system, is written primarily in C. As a cornerstone of modern computing, its design choices—including the version of C it uses—have far-reaching implications for developers, hardware compatibility, and system stability. If you’ve ever wondered whether the Linux kernel relies on C90 (C89), C99, C11, or a mix, you’re not alone. This blog dives deep into the kernel’s relationship with C standards, exploring its historical evolution, adopted features, and reasons for avoiding certain modern C constructs. By the end, you’ll understand why the kernel’s approach to C standards is a masterclass in pragmatism.

2026-01

Table of Contents#

  1. A Quick Recap: C Standards (C90, C99, C11)
  2. The Linux Kernel and C Standards: A Historical Perspective
  3. C99 Features in the Linux Kernel: What’s Used (and What’s Not)
  4. Why C11 Isn’t (Yet) the Kernel’s Standard
  5. The Kernel’s Build System and Compiler Flags
  6. Conclusion
  7. References

A Quick Recap: C Standards (C90, C99, C11)#

Before diving into the kernel, let’s briefly recap the key C standards relevant to our discussion.

C90 (C89): The ANSI Standard#

  • Released: 1989 (ANSI), 1990 (ISO).
  • Key Traits: The first formal C standard, establishing core syntax and features like for loops, switch statements, and basic data types (int, char, etc.).
  • Limitations: No support for mixed declarations and code, inline functions, or fixed-size integer types (e.g., uint32_t). All variables had to be declared at the start of a block, and library support was minimal.

C99: Expanding the Language#

  • Released: 1999 (ISO).
  • Key Additions:
    • Mixed declarations and code (declare variables mid-block).
    • Inline functions (standardized inline keyword).
    • Fixed-size integer types via <stdint.h> (e.g., uint8_t, int64_t).
    • Flexible array members (e.g., struct { int len; char data[]; }).
    • Variable-length arrays (VLAs, e.g., int arr[n];).
    • Complex numbers and improved math functions.
  • Impact: Made C more expressive and portable, especially for systems programming.

C11: Modernizing with New Features#

  • Released: 2011 (ISO).
  • Key Additions:
    • Atomic operations (_Atomic keyword) for thread safety.
    • Thread-local storage (_Thread_local).
    • Generic selections (_Generic for type-safe macros).
    • Unicode support via <uchar.h>.
    • Bounds-checking interfaces (optional, via <stdckdint.h>).
  • Target: Modern applications with multi-threading and safety concerns.

The Linux Kernel and C Standards: A Historical Perspective#

The Linux kernel’s relationship with C standards is rooted in pragmatism: prioritize stability, portability, and compatibility over strict adherence to the latest specs. Let’s trace its evolution.

Early Days: Sticking to C90 (GNU89)#

When Linus Torvalds first released the Linux kernel in 1991, C90 (then the newest standard) was the obvious choice. For decades, the kernel remained tightly coupled to C90, using the GNU dialect (gnu89) via GCC flags like -std=gnu89. This dialect included GCC-specific extensions (e.g., __attribute__, inline assembly) critical for low-level hardware interaction, while adhering to C90’s core rules.

Why C90? Compatibility was key. The kernel needed to run on diverse architectures and work with older compilers. C90 was universally supported, and its simplicity reduced bugs in a codebase where stability is paramount.

The Shift Towards C99: Why and When?#

By the 2010s, C99 features had become too useful to ignore. Developers wanted cleaner code (mixed declarations), portable fixed-size types (stdint.h), and safer alternatives to macros (inline functions). In 2014, discussions began about relaxing the kernel’s C90 restriction. By 2018, the kernel officially adopted C99 features, driven by:

  • Improved Compiler Support: Modern GCC and Clang versions fully supported C99.
  • Developer Productivity: C99 features reduced boilerplate and improved readability.
  • Portability: stdint.h types eliminated architecture-specific integer size guesswork.

Today: GNU11 as the Default, But Not Full C11#

In 2018, Linus Torvalds announced the kernel would switch to -std=gnu11—the GNU dialect of C11. This was not a leap to C11, but rather a formalization of the kernel’s de facto use of C99 features. The gnu11 flag enables C11 syntax and GNU extensions, but the kernel primarily uses C99 features. C11-specific additions (e.g., _Atomic, _Thread_local) remain largely unused.

C99 Features in the Linux Kernel: What’s Used (and What’s Not)#

The kernel selectively adopts C99 features that add value without risk. Here’s a breakdown:

Adopted C99 Features#

1. Mixed Declarations and Code#

C99 allows declaring variables mid-block, not just at the top. This makes code cleaner and reduces scope errors:

void process_packet(struct packet *p) {
    int header_len = p->header_size; // Declaration at block start (C90)
    parse_header(p, header_len);
 
    int data_len = p->total_size - header_len; // Mixed declaration (C99)
    process_data(p->data, data_len); // Variable used immediately after declaration
}

2. Inline Functions#

C99 standardized the inline keyword, replacing error-prone macros for small, performance-critical functions. For example, the kernel uses inline for helper functions in <linux/bitops.h>:

static inline void set_bit(int nr, volatile unsigned long *addr) {
    // Atomic bit-set operation (safer than a macro)
    asm volatile("bts %1,%0" : "+m" (*addr) : "Ir" (nr));
}

3. Fixed-Size Integer Types (<stdint.h>)#

C99’s <stdint.h> defines types like uint32_t (32-bit unsigned) and int64_t (64-bit signed), critical for hardware interactions where exact bit widths matter. The kernel uses these extensively:

struct timer {
    uint64_t expires; // 64-bit timestamp (works across 32/64-bit architectures)
    uint32_t flags;   // 32-bit flags field
};

4. Flexible Array Members#

C99 introduced flexible array members (FAMs), allowing structs to end with a variable-length array. The kernel uses FAMs for dynamic buffers, avoiding pointer overhead:

struct msg_buffer {
    size_t len;    // Length of data
    char data[];   // Flexible array member (C99)
};
 
// Allocate buffer with 1024 bytes of data
struct msg_buffer *buf = kmalloc(sizeof(*buf) + 1024, GFP_KERNEL);
buf->len = 1024;

C99 Features Still Avoided#

1. Variable-Length Arrays (VLAs)#

VLAs (e.g., int arr[n];) allocate memory on the stack, which is dangerous in the kernel: stack sizes are limited, and overflow can crash the system. Instead, the kernel uses dynamic allocation (kmalloc) for variable-length data.

2. Complex Numbers#

C99 added support for complex numbers (_Complex), but the kernel has no use for complex math. This feature is irrelevant for low-level systems programming.

3. <tgmath.h> and Advanced Math Functions#

C99’s math extensions (e.g., type-generic math in <tgmath.h>) rely on user-space libraries. The kernel avoids libc, so these are unused.

Why C11 Isn’t (Yet) the Kernel’s Standard#

Despite adopting gnu11, the kernel rarely uses C11-specific features. Here’s why:

Unnecessary Features for Kernel Development#

C11 focuses on modern application needs (e.g., multi-threading, safety checks) that don’t apply to the kernel:

  • _Atomic: The kernel uses custom atomic primitives (e.g., atomic_t) optimized for hardware, not C11’s generic _Atomic.
  • _Thread_local: Kernel threads are managed via task_struct, not C11’s thread-local storage.
  • Bounds Checking: C11’s optional bounds-checking interfaces (e.g., strcpy_s) are redundant; the kernel uses its own safe string functions (strlcpy).

Compatibility and Compiler Support#

The kernel supports older compilers (e.g., GCC 5.1+ as of 2023). Many C11 features require newer compilers, which could exclude embedded systems or legacy hardware.

GCC Extensions vs. C11 Features#

GCC extensions often provide C11-like functionality with more control. For example:

  • C11’s _Generic (type-generic macros) vs. GCC’s __builtin_types_compatible_p.
  • C11’s alignas vs. GCC’s __attribute__((aligned)).

The kernel prefers GCC extensions for their flexibility and long-standing reliability.

The Kernel’s Build System and Compiler Flags#

The kernel’s Makefile explicitly controls C standards via compiler flags, ensuring consistency across builds.

-std=gnu11: The Current Standard#

Since 2018, the kernel uses -std=gnu11, which:

  • Enables C11 syntax (allowing C99 features, as C11 is backward-compatible with C99).
  • Includes GNU extensions (e.g., __attribute__, typeof).

This flag is set in the top-level Makefile:

# From linux/Makefile (simplified)
KBUILD_CFLAGS += -std=gnu11

Compiler Requirements and Backwards Compatibility#

The kernel supports GCC 5.1+ and Clang 10+. This ensures compatibility with older systems while allowing modern features. The gnu11 flag balances standard compliance with the kernel’s reliance on GCC extensions.

Conclusion#

The Linux kernel’s approach to C standards is pragmatic: it adopts features that improve code quality, portability, and maintainability while avoiding unnecessary or risky additions. Today, it primarily uses C99 features under the gnu11 dialect, leveraging GCC extensions for low-level control. C11 remains largely unused, as its features don’t address kernel-specific needs, and compatibility with older compilers is prioritized.

For developers contributing to the kernel, understanding this balance is key: write clean, portable code using adopted C99 features, avoid risky constructs like VLAs, and rely on GCC extensions for hardware-specific logic.

References#