The kernel's responsibilities, CPU privilege levels (rings on x64, exception levels on Arm64), why applications run in user mode, how and when the CPU switches to kernel mode, and what that means for .NET applications, drivers, containers and security.
The kernel is the core of the operating system. It schedules threads on CPUs, manages memory, talks to devices through drivers, and implements file systems, networking and security. The CPU has privilege levels: in kernel mode (ring 0 on x64, EL1 on Arm64) code can access all memory and hardware; in user mode (ring 3, EL0) it can't touch hardware or other processes' memory directly. Every .NET app runs in user mode. When it needs the kernel (to read a file, send on a socket, allocate memory), it makes a system call, the CPU switches to kernel mode, the kernel does the work and returns. Interrupts and faults also switch modes. The split protects the system: a bug in your app crashes your process, not the machine.
| Responsibility | Examples |
|---|---|
| Process and thread management | Create processes, schedule threads on cores, context switches |
| Memory management | Virtual memory, page tables, page faults, swapping |
| Device I/O | Drivers for disks, network cards, GPUs |
| File systems | NTFS, ext4, XFS; permissions, caching |
| Networking | TCP/IP stack, sockets, firewall hooks |
| Security | Users, permissions, isolation between processes |
| Inter-process communication | Pipes, shared memory, signals, events |
Windows (NT kernel) and Linux are mostly monolithic kernels with loadable drivers (Windows splits some work into user-mode services). Microkernels move more into user mode for isolation, at some performance cost.
| User mode | Kernel mode | |
|---|---|---|
| CPU privilege | Ring 3 (x64), EL0 (Arm64) | Ring 0, EL1 |
| Memory access | Only its own process's mapped pages | All memory |
| Hardware access | None directly | Full |
| Who runs here | Applications, the .NET runtime, most services | Kernel, drivers |
| Effect of a bug | Process crashes | System crash (blue screen, kernel panic) or security hole |
syscall on x64, svc on Arm64); the CPU jumps to a kernel entry point in kernel mode.The kernel then returns to user mode at the right instruction. Each round trip costs from about a hundred nanoseconds to a few microseconds, more with security mitigations.
NullReferenceException is a user-mode fault turned into a managed exception; it can't take down the host.strace (Linux) and Process Monitor (Windows) show the system calls your app makes.A logging library wrote every log line directly to a file with an unbuffered FileStream and Flush() after each line. Under load the service spent about 20% of CPU in kernel mode. strace -c showed hundreds of thousands of write and fsync calls per minute. Switching to buffered, asynchronous logging (writing batches) cut kernel time to a few percent.
Checks foundational OS knowledge that underpins performance and security discussions.
Green flags: clear description of kernel duties, privilege levels and mode switches via system calls, interrupts and faults, connects it to .NET exceptions, I/O performance, containers and least privilege.
Red flags: vague "the kernel is the OS"; can't explain why user code can't access hardware; thinks containers have their own kernel.
+---------------------------------------------+
| user mode (ring 3 / EL0) |
| MyApi (dotnet) | nginx | sqlservr | bash |
+--------------------- syscall / svc ---------+
| kernel mode (ring 0 / EL1) |
| scheduler | memory manager | file systems |
| TCP/IP stack | drivers | security |
+---------------------------------------------+
| hardware: CPU, RAM, disks, NICs |
+---------------------------------------------+
x64 defines rings 0–3 but mainstream OSes use only 0 and 3. Hypervisors run at a further privileged level (often described as "ring −1", or EL2 on Arm64).
C#: obj.Name where obj is null
CPU: load from address 0x0000000000000008 -> page fault (no mapping)
OS: delivers an access violation (Windows) / SIGSEGV (Linux) to the process
CLR: recognises the faulting instruction in managed code -> throws NullReferenceException
App: catch block or unhandled exception handler runs
The same mechanism in native code without a runtime just kills the process.
| Tool | What it shows |
|---|---|
top / htop (Linux) |
us (user) versus sy (system) CPU percentages |
pidstat -u -p <pid> 1 |
Per-process %usr and %system |
| Task Manager → Performance → "Show kernel times" (Windows) | Kernel time shaded in the CPU graph |
Process.UserProcessorTime, PrivilegedProcessorTime (.NET) |
Split for the current process |
strace -c -f -p <pid> |
Count and time of each system call |
High system time usually means lots of small I/O, page faults, context switches or contention in kernel locks.
| Container | Virtual machine | |
|---|---|---|
| Kernel | Shared with the host | Its own |
| Isolation | Namespaces, cgroups, seccomp, capabilities | Hardware virtualisation |
| Start-up | Milliseconds to seconds | Seconds to minutes |
| Risk of kernel exploits | Affects all containers on the node | Contained to the VM |
Sandboxed runtimes (for example Kata Containers or gVisor) add a VM or user-space kernel boundary when multi-tenant isolation matters.
USER app in the official .NET images since .NET 8), drop Linux capabilities, use read-only root file systems.strace -c -f dotnet MyApp.dll for a simple console app that reads a file, and look at the system call summary.FileStream (bufferSize: 0 or 1), then with the default buffer; compare kernel time with pidstat or Process.PrivilegedProcessorTime.id and check whether you're root; then switch to the app user in your Dockerfile.namespace UserKernel;
using System.Diagnostics;
public static class Program
{
public static void Main()
{
string path = Path.Combine(Path.GetTempPath(), "modes.bin");
Measure("Unbuffered, 1 byte per write", () =>
{
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 0);
for (var i = 0; i < 200_000; i++)
{
stream.WriteByte(1); // each call becomes a system call
}
});
Measure("Buffered (default 4 KB buffer)", () =>
{
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write);
for (var i = 0; i < 200_000; i++)
{
stream.WriteByte(1); // batched into far fewer system calls
}
});
}
private static void Measure(string label, Action action)
{
using var process = Process.GetCurrentProcess();
var user = process.UserProcessorTime;
var kernel = process.PrivilegedProcessorTime;
var sw = Stopwatch.StartNew();
action();
process.Refresh();
Console.WriteLine($"{label}: wall {sw.ElapsedMilliseconds} ms, " +
$"user {(process.UserProcessorTime - user).TotalMilliseconds:F0} ms, " +
$"kernel {(process.PrivilegedProcessorTime - kernel).TotalMilliseconds:F0} ms");
}
}
# Linux: count system calls made by the app
strace -c -f dotnet UserKernel.dll
Mistake: thinking .NET code runs "in the kernel" or has special privileges. The runtime and your code are ordinary user-mode processes; they reach the kernel only through system calls. Instead: explain the boundary and that every file, socket and memory operation crosses it via the OS.
Mistake: ignoring kernel time when profiling.
A managed profiler may show little user CPU while the process spends much of its time in system calls or page faults.
Instead: check user versus system CPU (top, pidstat, Task Manager kernel times) and use strace or Process Monitor when system time is high.
Mistake: assuming containers are as isolated as VMs. Containers share the host kernel, so a kernel vulnerability can affect every container on the node. Instead: patch nodes, run as non-root with minimal capabilities, and use VM-level isolation for untrusted multi-tenant workloads.
Mistake: running services as root or Administrator "to avoid permission problems". A compromised process can then ask the kernel to do anything, which turns a bug into a full system compromise. Instead: run with least privilege and grant only the specific permissions needed.
Mistake: making many tiny I/O calls. Each crosses into kernel mode and back, so per-byte or per-line writes waste CPU. Instead: buffer and batch I/O, and use async APIs that let the kernel do the waiting.
Seniors use the user/kernel boundary as both a performance and a security lens.
| Lens | What to watch | What to do |
|---|---|---|
| Performance | High system (sy) CPU, many syscalls in strace -c |
Buffer and batch I/O, async I/O, fewer context switches |
| Stability | Process crashes versus host crashes | Keep code in user mode; avoid third-party kernel drivers on servers |
| Security | Running as root/Administrator, unpatched kernels | Non-root containers, dropped capabilities, read-only root FS, prompt node patching (ACSC Essential Eight: restrict admin privileges, patch OS) |
| Isolation | Multi-tenant or untrusted code on shared nodes | VM-level isolation (separate node pools, Kata/gVisor-style sandboxes) |
| Observability | Need kernel-level insight | eBPF tools on Linux, ETW on Windows, rather than custom kernel code |
They also know containers are a kernel feature, which explains both their efficiency (no guest OS) and their isolation limits (one kernel bug affects every container on the node).
A system call is how a user-mode program asks the kernel to do something privileged: read a file, send data, allocate memory, create a thread. The program puts arguments in registers and executes a special instruction (syscall on x64); the CPU switches to kernel mode, the kernel validates the request, does the work and returns. In .NET, File.ReadAllBytesAsync goes through FileStream/RandomAccess and a SafeFileHandle to ReadFile on Windows or pread on Linux; Socket.SendAsync uses I/O completion ports on Windows and epoll on Linux. Each call costs hundreds of nanoseconds to microseconds, more with security mitigations, so thousands of tiny calls add up. Buffering, batching and async I/O keep the count down.
A process is a running program with its own virtual address space, handles (open files, sockets) and security context; processes are isolated from each other. A thread is a path of execution inside a process with its own stack, registers and thread-local storage, while sharing the process's memory with other threads. Sharing makes communication cheap but requires synchronisation. In .NET, a System.Diagnostics.Process represents an OS process, a Thread is one-to-one with an OS thread, the thread pool keeps a reusable set of threads, and a Task is a unit of work scheduled onto them. An await on I/O usually uses no thread at all while it waits.
Last updated 23 Oct 2026.