Kernel Architecture
Understanding the separation between kernel space and user space is fundamental to driver development.
CPU Privilege Levels
Modern CPUs implement protection rings:
flowchart TB
subgraph Ring3["Ring 3 (User Mode)"]
Apps["Applications, Libraries"]
end
subgraph Ring0["Ring 0 (Kernel Mode)"]
Kernel["Drivers, Kernel Core, Scheduler"]
end
Ring3 --> Ring0
- Ring 0 (Kernel mode): Full hardware access, all instructions available
- Ring 3 (User mode): Restricted access, some instructions prohibited
Address Space Separation
Each process has virtual address space divided between user and kernel:
64-bit Linux (typical):
0xFFFFFFFFFFFFFFFF ┌──────────────────┐
│ │
│ Kernel Space │ (shared by all processes)
│ ~128 TB │
│ │
0xFFFF800000000000 ├──────────────────┤
│ Non-canonical │ (hole)
0x00007FFFFFFFFFFF ├──────────────────┤
│ │
│ User Space │ (per-process)
│ ~128 TB │
│ │
0x0000000000000000 └──────────────────┘
Key Points
- User processes cannot access kernel memory directly
- Kernel can access user memory (carefully, with checks)
- Memory addresses are virtual, not physical
- Page tables control access permissions
System Call Interface
User space communicates with kernel via system calls:
flowchart LR
subgraph UserSpace["User Space"]
direction LR
subgraph App["Application"]
direction LR
Open["open()"]
Read["read()"]
Write["write()"]
Ioctl["ioctl()"]
end
end
subgraph KernelSpace["Kernel Space"]
Handler["System Call Handler"]
VFS["VFS Layer"]
Driver["Driver"]
end
App -->|syscall| KernelSpace
Handler <--> VFS
VFS <--> Driver
System Call Mechanism
- User code calls library function (e.g.,
read()) - Library triggers syscall instruction
- CPU switches to kernel mode
- Kernel executes handler
- Result returned, CPU switches back to user mode
Kernel vs User Space Constraints
| Aspect | User Space | Kernel Space |
|---|---|---|
| Memory access | Own process only | All physical memory |
| Stack size | 8 MB typical | 8-16 KB only! |
| Libraries | glibc, pthreads, etc. | Kernel API only |
| Floating point | Available | Avoid (expensive to save/restore) |
| Sleep/block | Freely | Only in process context |
| Errors | Return -1, set errno | Return negative errno |
| Crashes | Process dies | System panic (usually) |
Execution Contexts
Kernel code runs in different contexts:
Process Context
- Executing on behalf of a user process (syscall, ioctl)
- Has
currentpointer to process task_struct - Can sleep/block
- Can access user memory
/* In process context */
pr_info("Running in process: %s (pid %d)\n",
current->comm, current->pid);
Interrupt Context (Atomic Context)
- Handling hardware interrupt
- No
currentprocess - Cannot sleep/block
- Must be fast
/* In interrupt context - cannot sleep! */
static irqreturn_t my_irq_handler(int irq, void *dev)
{
/* Quick work only - defer lengthy processing */
return IRQ_HANDLED;
}
Softirq/Tasklet Context
- Deferred interrupt processing
- Still atomic - cannot sleep
- Can be preempted by hardware interrupts
The current Macro
In process context, current points to the current task:
#include <linux/sched.h>
static int my_func(void)
{
/* Access current process info */
pr_info("Process: %s\n", current->comm);
pr_info("PID: %d\n", current->pid);
pr_info("UID: %u\n", current_uid().val);
return 0;
}
current is undefined in interrupt context. Always check execution context before using it.
Checking Execution Context
#include <linux/preempt.h>
void check_context(void)
{
if (in_interrupt())
pr_info("In interrupt context\n");
else if (in_atomic())
pr_info("In atomic context (preemption disabled)\n");
else
pr_info("In process context - can sleep\n");
}
User Space Access
Kernel must never directly dereference user pointers:
/* WRONG - never do this! */
int __user *uptr = ...;
int value = *uptr; /* Can crash, security hole */
/* CORRECT - use accessor functions */
int value;
if (get_user(value, uptr))
return -EFAULT;
Functions for user space access:
| Function | Purpose |
|---|---|
get_user(val, ptr) |
Read single value |
put_user(val, ptr) |
Write single value |
copy_from_user(to, from, n) |
Copy buffer from user |
copy_to_user(to, from, n) |
Copy buffer to user |
access_ok(ptr, size) |
Check pointer validity |
Module vs Built-in
Kernel code can be:
- Module (
.ko): Loaded/unloaded at runtime - Built-in: Compiled into kernel image
Modules allow:
- Dynamic loading without reboot
- Memory savings (unload when not needed)
- Easier development/testing
Summary
- Kernel runs in privileged mode with full hardware access
- User space is isolated and protected
- System calls bridge the user/kernel boundary
- Execution context determines what operations are safe
- Always use proper functions for user space access
Next
Learn about the module lifecycle - how modules are loaded, initialized, and unloaded.