Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Linux Kernel Architecture

Introduction

The Linux kernel architecture is a carefully layered system that balances performance with maintainability. While the kernel is monolithic in the sense that all core components share a single address space, its internal design follows clear separation of concerns with well-defined interfaces between subsystems.

This chapter examines the kernel’s architecture in detail: the relationships between subsystems, data flow paths, and the design decisions that make Linux both fast and flexible.

High-Level Architecture Diagram

The following diagram shows the major components of the Linux kernel and their relationships:

graph TB
    subgraph "User Space"
        APP[Applications]
        GLIBC[glibc / musl]
        LIBC[System Call Interface]
    end

    subgraph "Kernel Space"
        subgraph "System Call Interface"
            SYSCALL[syscall dispatch]
        end

        subgraph "Core Subsystems"
            VFS[Virtual File System]
            MM[Memory Manager]
            SCHED[Process Scheduler]
            IPC[IPC Subsystem]
            NET[Network Stack]
            SECURITY[Security Framework]
        end

        subgraph "Support Subsystems"
            CRYPTO[Crypto API]
            IRQ[IRQ Management]
            SYNC[Synchronization]
            WORK[Workqueues]
        end

        subgraph "Hardware Abstraction"
            DEV[Device Model / Driver Framework]
            CHAR[Char Devices]
            BLOCK[Block Devices]
            NETDEV[Network Devices]
        end

        subgraph "Architecture Layer"
            ARCH[x86/ARM64/RISC-V/...]
        end
    end

    subgraph "Hardware"
        CPU[CPU / Memory]
        DISK[Storage]
        NIC[Network]
        OTHER[Other Devices]
    end

    APP --> GLIBC --> LIBC --> SYSCALL
    SYSCALL --> VFS
    SYSCALL --> MM
    SYSCALL --> SCHED
    SYSCALL --> IPC
    SYSCALL --> NET
    SYSCALL --> SECURITY

    VFS --> BLOCK
    VFS --> MM
    MM --> SCHED
    NET --> NETDEV
    DEV --> CHAR
    DEV --> BLOCK
    DEV --> NETDEV

    IRQ --> SCHED
    IRQ --> NET
    IRQ --> BLOCK
    WORK --> DEV

    BLOCK --> DISK
    NETDEV --> NIC
    CHAR --> OTHER
    ARCH --> CPU

Subsystem Relationship Map

The kernel subsystems are deeply interconnected. Here is a detailed view of the dependencies:

graph LR
    subgraph "Data Flow Dependencies"
        VFS -->|page cache| MM
        MM -->|page faults| VFS
        MM -->|OOM killer| SCHED
        SCHED -->|context switch| ARCH
        NET -->|socket buffers| MM
        NET -->|NAPI poll| IRQ
        BLOCK -->|I/O scheduler| SCHED
        BLOCK -->|buffer heads| MM
        BLOCK -->|bio requests| DEV
        DEV -->|DMA mapping| MM
        SECURITY -->|access checks| VFS
        SECURITY -->|capability checks| SYSCALL
        IPC -->|shared memory| MM
    end

The System Call Interface

The system call interface is the primary gateway between user space and kernel space. It’s implemented in architecture-specific code but follows a common pattern:

x86-64 System Call Path

sequenceDiagram
    participant App as User Application
    participant Libc as glibc
    participant Entry as syscall_entry (asm)
    participant Dispatch as sys_call_table
    participant Handler as sys_xxx()
    participant Return as syscall_return

    App->>Libc: write(fd, buf, count)
    Libc->>Entry: syscall instruction
    Entry->>Entry: Save registers
    Entry->>Entry: Switch to kernel stack
    Entry->>Dispatch: Lookup sys_call_table[nr]
    Dispatch->>Handler: Call sys_write()
    Handler->>Handler: Process request
    Handler-->>Entry: Return value in RAX
    Entry->>Return: Restore registers
    Return->>App: Return to user space

The entry point is defined in assembly:

/* arch/x86/entry/entry_64.S (simplified) */
entry_SYSCALL_64:
    swapgs
    mov    [gs:cpu_tss_rw.x86_tss.sp2], rsp  /* save user RSP */
    mov    rsp, [gs:cpu_tss_rw.x86_tss.sp0]  /* load kernel stack */
    /* save registers to pt_regs on stack */
    push   r11
    push   rcx
    push   rbp
    push   rbx
    /* ... */
    mov    rdi, rsp           /* pt_regs as first arg */
    call   do_syscall_64      /* C handler */
    /* restore and return */
    jmp    swapgs_restore_regs_and_return_to_usermode

The C-level dispatch:

/* arch/x86/kernel/syscall_64.c */
__visible void do_syscall_64(struct pt_regs *regs)
{
    unsigned long nr = regs->orig_ax;

    if (nr < NR_syscalls) {
        regs->ax = sys_call_table[nr](
            regs->di, regs->si, regs->dx,
            regs->r10, regs->r8, regs->r9
        );
    }
}

Process Scheduler Architecture

The scheduler is one of the most critical kernel subsystems. Linux implements a modular scheduling framework:

graph TB
    subgraph "Scheduler Framework"
        CORE[Scheduler Core]
        subgraph "Scheduling Classes"
            STOP[stop_sched_class]
            DL[dl_sched_class]
            RT[rt_sched_class]
            FAIR[fair_sched_class]
            IDLE[idle_sched_class]
        end
    end

    STOP -->|highest priority| DL
    DL -->|deadline scheduling| RT
    RT -->|real-time FIFO/RR| FAIR
    FAIR -->|CFS / EEVDF| IDLE
    IDLE -->|lowest priority| CORE

    subgraph "Per-CPU Run Queues"
        RQ0[Run Queue CPU 0]
        RQ1[Run Queue CPU 1]
        RQN[Run Queue CPU N]
    end

    CORE --> RQ0
    CORE --> RQ1
    CORE --> RQN

Each scheduling class has a defined interface:

/* include/linux/sched.h */
struct sched_class {
    void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags);
    void (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags);
    void (*yield_task)(struct rq *rq);
    void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
    struct task_struct *(*pick_next_task)(struct rq *rq);
    void (*put_prev_task)(struct rq *rq, struct task_struct *p);
    void (*set_curr_task)(struct rq *rq);
    void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
    void (*switched_to)(struct rq *rq, struct task_struct *p);
    void (*prio_changed)(struct rq *rq, struct task_struct *p, int oldprio);
    /* ... */
};

CFS / EEVDF Scheduling

The Completely Fair Scheduler (CFS) uses a red-black tree keyed by virtual runtime (vruntime). The task with the smallest vruntime is always picked next, ensuring fair CPU time distribution.

Starting with kernel 6.6, the EEVDF (Earliest Eligible Virtual Deadline First) scheduler replaces CFS. EEVDF assigns each task a virtual deadline based on its request and lag, and picks the eligible task with the earliest deadline:

/* kernel/sched/fair.c — EEVDF pick logic (simplified) */
static struct sched_entity *pick_eevdf(struct cfs_rq *cfs_rq)
{
    struct sched_entity *best = NULL;
    struct rb_node *node = cfs_rq->tasks_timeline.rb_leftmost;

    /* Walk the tree to find the earliest eligible virtual deadline */
    for_each_eligible_entity(se, cfs_rq) {
        if (!best || entity_before(se, best))
            best = se;
    }
    return best;
}

Memory Management Architecture

The memory management subsystem is layered from low-level page allocation to high-level virtual memory abstractions:

graph TB
    subgraph "User Space"
        MMAP[mmap/munmap]
        BRK[brk/sbrk]
        MALLOC[malloc/free]
    end

    subgraph "Kernel Memory Management"
        subgraph "Virtual Memory"
            VMA[Virtual Memory Areas]
            PAGETABLE[Page Tables]
            FAULT[Page Fault Handler]
        end

        subgraph "Page Allocator"
            BUDDY[Buddy Allocator]
            ZONES[Memory Zones]
            PCPU[Per-CPU Page Cache]
        end

        subgraph "Object Allocator"
            SLUB[SLUB Allocator]
            KMEM[Kmem Caches]
        end

        subgraph "Page Cache"
            PGCACHE[Page Cache]
            WRITEBACK[Writeback]
        end

        subgraph "Reclaim"
            KSWAPD[kswapd]
            LRU[LRU Lists]
            OOM[OOM Killer]
        end
    end

    subgraph "Hardware"
        MMU[MMU / TLB]
        RAM[Physical RAM]
    end

    MMAP --> VMA
    BRK --> VMA
    MALLOC --> MMAP
    MALLOC --> BRK

    VMA --> PAGETABLE
    FAULT --> VMA
    FAULT --> PAGETABLE
    FAULT --> PGCACHE
    FAULT --> BUDDY

    BUDDY --> ZONES
    ZONES --> PCPU
    SLUB --> BUDDY
    KMEM --> SLUB

    PGCACHE --> WRITEBACK
    KSWAPD --> LRU
    LRU --> BUDDY
    OOM --> SCHED

    PAGETABLE --> MMU
    BUDDY --> RAM

Key Memory Data Structures

/* Each process has a mm_struct describing its address space */
struct mm_struct {
    struct maple_tree mm_mt;        /* VMAs stored in maple tree */
    struct rw_semaphore mmap_lock;
    unsigned long task_size;        /* size of user address space */
    pgd_t *pgd;                     /* page global directory */
    atomic_t mm_users;              /* number of processes sharing this mm */
    atomic_t mm_count;              /* reference count */
    int map_count;                  /* number of VMAs */
    unsigned long total_vm;         /* total pages mapped */
    unsigned long locked_vm;        /* pages locked in memory */
    unsigned long data_vm;          /* VM_WRITE & ~VM_SHARED */
    unsigned long stack_vm;         /* VM_GROWSUP/DOWN */
    unsigned long start_code, end_code;
    unsigned long start_data, end_data;
    unsigned long start_brk, brk;
    unsigned long start_stack;
    /* ... */
};

/* Virtual Memory Area — describes a contiguous region of virtual memory */
struct vm_area_struct {
    unsigned long vm_start;         /* start address */
    unsigned long vm_end;           /* end address */
    pgprot_t vm_page_prot;          /* access permissions */
    unsigned long vm_flags;         /* VM_READ|VM_WRITE|VM_EXEC|... */
    struct rb_node vm_rb;           /* node in mm's maple tree/rbtree */
    struct file *vm_file;           /* file mapped (NULL for anonymous) */
    void *vm_private_data;          /* driver-specific data */
    const struct vm_operations_struct *vm_ops;
    /* ... */
};

Virtual File System (VFS) Architecture

VFS provides a uniform interface for all filesystems:

graph TB
    subgraph "System Call Layer"
        SYS_OPEN[open]
        SYS_READ[read]
        SYS_WRITE[write]
        SYS_CLOSE[close]
    end

    subgraph "VFS Layer"
        INODE[inode -- file metadata]
        DENTRY[dentry -- directory entry cache]
        FILE[struct file -- open file instance]
        SB[super_block -- filesystem instance]
    end

    subgraph "Filesystem Implementations"
        EXT4[ext4]
        XFS[XFS]
        BTRFS[Btrfs]
        TMPFS[tmpfs]
        PROCFS[procfs]
    end

    subgraph "Block Layer"
        BIO[bio -- block I/O request]
        BDI[backing_dev_info]
        IOSCHED[I/O Scheduler]
    end

    SYS_OPEN --> DENTRY
    SYS_READ --> FILE
    SYS_WRITE --> FILE
    FILE --> INODE
    INODE --> DENTRY
    DENTRY --> SB
    SB --> EXT4
    SB --> XFS
    SB --> BTRFS
    SB --> TMPFS
    EXT4 --> BIO
    XFS --> BIO
    BIO --> IOSCHED
    IOSCHED --> BDI

Key VFS Objects

/* struct inode — represents a filesystem object (file, directory, etc.) */
struct inode {
    umode_t                 i_mode;     /* file type and permissions */
    unsigned short          i_opflags;
    kuid_t                  i_uid;      /* owner UID */
    kgid_t                  i_gid;      /* owner GID */
    unsigned int            i_flags;
    const struct inode_operations   *i_op;
    struct super_block      *i_sb;
    struct address_space    *i_mapping; /* page cache mapping */
    unsigned long           i_ino;      /* inode number */
    loff_t                  i_size;     /* file size in bytes */
    struct timespec64       __i_atime;
    struct timespec64       __i_mtime;
    struct timespec64       __i_ctime;
    const struct file_operations    *i_fop;
    /* ... */
};

/* struct file — represents an open file */
struct file {
    struct path             f_path;     /* contains vfsmount and dentry */
    struct inode            *f_inode;
    const struct file_operations    *f_op;
    atomic_long_t           f_count;
    unsigned int            f_flags;    /* O_RDONLY, O_NONBLOCK, etc. */
    fmode_t                 f_mode;
    loff_t                  f_pos;      /* current file position */
    struct address_space    *f_mapping;
    void                    *private_data;
    /* ... */
};

Network Subsystem Architecture

The networking stack follows a layered design similar to the OSI model:

graph TB
    subgraph "User Space"
        SOCK_APP[Socket Application]
    end

    subgraph "Socket Layer"
        SOCKET[struct socket]
        SOCK[struct sock]
        SK_BUFF[sk_buff management]
    end

    subgraph "Transport Layer"
        TCP[TCP]
        UDP[UDP]
        RAW[Raw Sockets]
    end

    subgraph "Network Layer"
        IP[IPv4 / IPv6]
        ROUTE[Routing]
        NETFILTER[Netfilter / iptables]
    end

    subgraph "Link Layer"
        DEV_CORE[Network Device Core]
        QDISC[Queueing Disciplines]
        BRIDGE[Bridging]
    end

    subgraph "Driver Layer"
        NAPI[NAPI Polling]
        NETDEV2[net_device ops]
    end

    SOCK_APP --> SOCKET
    SOCKET --> SOCK
    SOCK --> TCP
    SOCK --> UDP
    SOCK --> RAW
    TCP --> IP
    UDP --> IP
    IP --> ROUTE
    IP --> NETFILTER
    NETFILTER --> DEV_CORE
    DEV_CORE --> QDISC
    QDISC --> NAPI
    NAPI --> NETDEV2

sk_buff — The Socket Buffer

The sk_buff is the fundamental data unit in the networking stack:

/* include/linux/skbuff.h (simplified) */
struct sk_buff {
    struct sk_buff      *next, *prev;
    struct sock         *sk;           /* owning socket */
    unsigned int        len;           /* data length */
    unsigned int        data_len;      /* non-linear data length */
    __u16               mac_len;       /* MAC header length */
    __u16               hdr_len;       /* skb headroom used */
    __u16               queue_mapping;
    __u8                cloned:1;
    __u8                ip_summed:2;

    /* Transport layer header */
    __u16               transport_header;
    /* Network layer header */
    __u16               network_header;
    /* Link layer header */
    __u16               mac_header;

    /* Data pointers */
    unsigned char       *head;         /* buffer head */
    unsigned char       *data;         /* data start */
    unsigned char       *tail;         /* data end */
    unsigned char       *end;          /* buffer end */

    /* Timestamp, dev, protocol, etc. */
    ktime_t             tstamp;
    struct net_device   *dev;
    __be16              protocol;
    /* ... */
};

Device Model Architecture

The Linux device model provides a unified view of all devices through sysfs:

graph TB
    subgraph "Device Model Core"
        KOBJ[kobject -- reference-counted object]
        KSET[kset -- collection of kobjects]
        KTYPE[ktype -- object type operations]
    end

    subgraph "Bus / Device / Driver Model"
        BUS[struct bus_type]
        DEVICE[struct device]
        DRIVER[struct device_driver]
    end

    subgraph "Bus Types"
        PCI[PCI bus]
        USB[USB bus]
        PLATFORM[Platform bus]
        I2C[I2C bus]
        SPI[SPI bus]
    end

    subgraph "sysfs Representation"
        SYSFS_BUS["/sys/bus/"]
        SYSFS_DEV["/sys/devices/"]
        SYSFS_CLASS["/sys/class/"]
    end

    KOBJ --> KSET
    KOBJ --> KTYPE
    BUS --> DEVICE
    BUS --> DRIVER
    DEVICE --> KOBJ
    DRIVER --> KOBJ
    PCI --> BUS
    USB --> BUS
    PLATFORM --> BUS
    BUS --> SYSFS_BUS
    DEVICE --> SYSFS_DEV
    DEVICE --> SYSFS_CLASS

Device-Driver Binding

/* drivers/base/bus.c — simplified binding logic */
static int driver_match_device(struct device_driver *drv,
                               struct device *dev)
{
    return drv->bus->match ? drv->bus->match(dev, drv) : 1;
}

/* drivers/pci/pci-driver.c — PCI match function */
static const struct pci_device_id *pci_match_device(
    const struct pci_device_id *ids, struct pci_dev *dev)
{
    /* Match vendor, device, subvendor, subdevice, class */
    while (ids->vendor || ids->subvendor || ids->class_mask) {
        if (pci_match_one_device(ids, dev))
            return ids;
        ids++;
    }
    return NULL;
}

Interrupt Handling Architecture

Linux uses a two-phase interrupt handling model to minimize the time spent with interrupts disabled:

graph TB
    subgraph "Hardware Interrupt"
        HW_IRQ[Hardware IRQ Line]
    end

    subgraph "Top Half (Hard IRQ)"
        DESC[irq_desc]
        HANDLER[irq handler -- quick acknowledgment]
        ACK[Disable/acknowledge IRQ]
    end

    subgraph "Bottom Half Mechanisms"
        SOFTIRQ[softirq -- ksoftirqd]
        TASKLET[tasklet -- deprecated in 6.x]
        WORKQ[workqueue -- most common]
        THREADED[Threaded IRQs]
    end

    subgraph "Action"
        ACTION1[irqaction 1]
        ACTION2[irqaction 2]
        ACTIONN[irqaction N]
    end

    HW_IRQ --> DESC
    DESC --> HANDLER
    HANDLER --> ACK
    HANDLER --> SOFTIRQ
    HANDLER --> TASKLET
    HANDLER --> WORKQ
    HANDLER --> THREADED
    DESC --> ACTION1
    DESC --> ACTION2
    DESC --> ACTIONN
# View interrupt information
$ cat /proc/interrupts | head -5
           CPU0       CPU1       CPU2       CPU3
  1:          9          0          0          0   IO-APIC   1-edge      i8042
  8:          1          0          0          0   IO-APIC   8-edge      rtc0
  9:          0          0          23         0   IO-APIC   9-fasteoi   acpi
 16:         56        234          0          0   IO-APIC  16-fasteoi   ehci_hcd
 23:          0          0       1234          0   IO-APIC  23-fasteoi   nvidia

# View per-CPU softirq statistics
$ cat /proc/softirqs
                    CPU0       CPU1       CPU2       CPU3
          HI:          0          0          0          0
       TIMER:    1234567    1234566    1234567    1234566
      NET_TX:       1234       1234       1233       1234
      NET_RX:      56789      56788      56789      56788
       BLOCK:      12345      12344      12345      12344
    IRQ_POLL:          0          0          0          0
     TASKLET:       1234       1234       1233       1234
       SCHED:     234567     234566     234567     234566
     HRTIMER:          0          0          0          0
         RCU:     345678     345677     345678     345677

Synchronization Primitives

The kernel provides various synchronization mechanisms for different use cases:

PrimitiveUse CaseContext
SpinlockShort critical sections, IRQ-safeAtomic (no sleep)
MutexLonger critical sections, can sleepProcess context
RCURead-mostly data, zero read overheadAny context
SemaphoreCounting synchronizationProcess context
rwlockMany readers, few writersAtomic
atomic_tSimple countersAny context
seqlockReader-writer, readers never blockAny context
CompletionWait for eventProcess context
/* Example: spinlock usage */
DEFINE_SPINLOCK(my_lock);
unsigned long flags;

spin_lock_irqsave(&my_lock, flags);  /* disable interrupts */
/* critical section — must not sleep */
spin_unlock_irqrestore(&my_lock, flags);

/* Example: mutex usage */
DEFINE_MUTEX(my_mutex);

mutex_lock(&my_mutex);
/* critical section — can sleep */
mutex_unlock(&my_mutex);

/* Example: RCU usage */
rcu_read_lock();
/* read-side critical section — no blocking */
list_for_each_entry_rcu(ptr, &my_list, list) {
    /* read data */
}
rcu_read_unlock();

Kernel Configuration Architecture

The configuration system uses a hierarchy of Kconfig files:

graph TB
    KCONFIG_ROOT[Kconfig -- root]
    INIT_K[init/Kconfig]
    MM_K[mm/Kconfig]
    NET_K[net/Kconfig]
    FS_K[fs/Kconfig]
    DRV_K[drivers/Kconfig]
    ARCH_K[arch/x86/Kconfig]

    KCONFIG_ROOT --> INIT_K
    KCONFIG_ROOT --> MM_K
    KCONFIG_ROOT --> NET_K
    KCONFIG_ROOT --> FS_K
    KCONFIG_ROOT --> DRV_K
    KCONFIG_ROOT --> ARCH_K

    DRV_K --> DRV_NET_K[drivers/net/Kconfig]
    DRV_K --> DRV_USB_K[drivers/usb/Kconfig]
    DRV_NET_K --> DRV_IGB_K[drivers/net/ethernet/intel/Kconfig]

See Build System and Configuration for full details.

Cross-Subsystem Data Flow Examples

File Read Path

A read() system call traverses multiple subsystems:

sequenceDiagram
    participant App as Application
    participant VFS as VFS
    participant FS as Filesystem (ext4)
    participant PC as Page Cache
    participant MM as Memory Manager
    participant Block as Block Layer
    participant Dev as Device Driver

    App->>VFS: read(fd, buf, count)
    VFS->>VFS: fdget() → struct file
    VFS->>FS: file->f_op->read()
    FS->>PC: find_lock_page() in page cache
    alt Page in cache
        PC-->>FS: page found
    else Page not in cache
        PC->>MM: alloc_page()
        MM->>Block: submit_bio()
        Block->>Dev: queue request
        Dev-->>Block: I/O complete (IRQ)
        Block-->>PC: page populated
    end
    FS-->>VFS: data available
    VFS->>VFS: copy_to_user(buf, page_data)
    VFS-->>App: bytes read

Network Packet Receive Path

sequenceDiagram
    participant NIC as Network Card
    participant IRQ as IRQ Handler
    participant NAPI as NAPI SoftIRQ
    participant IP as IP Layer
    participant TCP as TCP Layer
    participant Socket as Socket Buffer
    participant App as Application

    NIC->>IRQ: Hardware interrupt
    IRQ->>IRQ: Acknowledge IRQ
    IRQ->>NAPI: napi_schedule()
    NAPI->>NAPI: napi_poll() -- receive packets
    NAPI->>IP: netif_receive_skb()
    IP->>IP: Route lookup
    IP->>IP: Netfilter hooks
    IP->>TCP: tcp_v4_rcv()
    TCP->>TCP: Sequence/ordering
    TCP->>Socket: sk_data_ready()
    Socket->>App: recv() returns data

Further Reading