Skip to main content What Separates Applications, the Kernel, and Drivers? | IoT Worker

What Separates Applications, the Kernel, and Drivers?

When application code calls read(), write(), or ioctl(), it can look like the program is directly operating a device. Reading a UART, writing to a network interface, controlling GPIO, or accessing a sensor may all appear to be simple function calls.

But that path is not the application touching hardware directly.

On systems with an operating system, applications, the kernel, and drivers are separated by several boundaries: permission boundaries, address-space boundaries, system-call boundaries, device abstraction boundaries, and blocking semantics. Many application-driver debugging problems come from mixing these boundaries together.

The safest first model is this: the application runs in user space and requests kernel services through system calls; the kernel uses permissions, file descriptors, and the device model to find the right driver; the driver accesses registers, interrupts, DMA, and buffers according to hardware rules.

Application code
-> C library or runtime wrapper
-> system call
-> kernel VFS/device model
-> driver
-> hardware registers / interrupts / DMA

Each layer can change what the call actually means.

Why Applications Cannot Access Hardware Directly

If ordinary applications could freely access hardware registers, the system would be hard to protect.

A bad address could disable interrupts, corrupt page tables, misconfigure DMA, overwrite another device’s registers, or hang the whole system. In a multiprocess system, multiple applications could also fight over the same hardware with no central arbitration.

General-purpose operating systems usually split CPU privilege into at least two modes:

  • user space: where ordinary applications run with limited privileges
  • kernel space: where the kernel runs and can access hardware and core resources

User-space programs cannot directly execute privileged instructions or freely access kernel addresses and device registers. They must request kernel services through controlled entry points.

Those controlled entry points are system calls.

A System Call Is Not a Normal Function Call

From application code, read(fd, buf, len) looks like a normal function. But entering the kernel involves a privilege transition.

A simplified path is:

application calls read
-> C library prepares arguments
-> system-call instruction is executed
-> CPU enters kernel mode
-> kernel checks arguments, permissions, and file descriptor
-> kernel dispatches to the correct path

A system call differs from a normal function call:

  • it crosses user/kernel privilege boundary
  • arguments come from untrusted user space
  • the kernel must check addresses, lengths, and permissions
  • the call may block, be interrupted by a signal, or return an error code
  • the return value is the kernel result, not always proof that hardware has completed everything

So a system call is a controlled gate between application and kernel, not just a library function.

Why a File Descriptor Can Represent a Device

In Linux, many devices can be opened as file descriptors. UARTs, GPIO, I2C devices, input devices, block devices, and network sockets may all be exposed through file-like interfaces.

That does not mean all devices are ordinary files. It means the kernel provides a unified handle model to applications.

Behind an fd, the kernel keeps objects such as:

  • which file or device the handle refers to
  • current offset or state
  • open mode and permissions
  • blocking or nonblocking flags
  • a table of operation functions

When the application calls read(fd, ...), the kernel uses the fd to find the object and dispatches to a filesystem, socket, character device, or another implementation.

That explains why the same read() behaves differently for a normal file, UART, socket, or sensor device. Similar interface does not mean identical semantics.

What a Driver Solves Inside the Kernel

A driver is not a library for applications to call. A driver usually runs inside the kernel and translates operating-system device abstractions into hardware operations.

It handles things such as:

  • hardware initialization
  • register configuration
  • interrupt management
  • DMA and buffer management
  • implementing open/read/write/ioctl/mmap
  • power management
  • integration with kernel subsystems
  • exposing a suitable interface to user space

One side of a driver faces kernel frameworks. The other side faces the hardware manual.

When the application says “read 100 bytes,” the driver must decide whether device data is available; if not, whether to block, return EAGAIN, or wait for an interrupt. If data is available, it must decide how to copy it from a hardware FIFO, DMA buffer, or kernel queue to user space.

The hard part of driver work is not only “write registers.” It is connecting asynchronous hardware events, kernel synchronization rules, and user-space interface semantics.

Why User Buffers Cannot Be Trusted Directly

When an application passes buf to the kernel, it passes a user-space virtual address.

The kernel cannot assume that address is valid. Reasons include:

  • pointer may be null or invalid
  • length may exceed the accessible region
  • pages may not be mapped yet
  • user space may lack permission
  • another thread may modify the buffer concurrently
  • accessing the user page may fault

The kernel usually needs special mechanisms to copy data between user space and kernel space, such as copy_from_user and copy_to_user.

This also explains why a user-space pointer should not be casually stored by a driver for later use. After the system call returns, the user process may free, reuse, swap, or modify that memory. If a driver needs data asynchronously, it usually needs to copy it, pin pages, or establish a clear DMA mapping.

User addresses, kernel addresses, physical addresses, and DMA addresses are not the same layer.

Blocking and Nonblocking Change Call Semantics

The same device read can behave very differently depending on blocking mode.

In blocking mode, if the device has no data, read() may put the current thread to sleep. When an interrupt, queue, or another event indicates that data has arrived, the kernel wakes it up.

application read
-> driver finds no data
-> current thread sleeps
-> hardware interrupt arrives
-> driver queues data and wakes waiters
-> read returns

In nonblocking mode, if no data is available, read() usually returns an error such as EAGAIN, and the application decides whether to retry later, enter an event loop, or do other work.

That is why the same driver interface can behave differently under different open flags. The issue may not be that the driver “did not return data,” but that the application chose a different waiting semantic.

Mechanisms such as poll, select, and epoll build on this idea: the application does not blindly block in one read(), but first asks which file descriptors are currently readable, writable, or in error.

Why ioctl Easily Becomes a Junk Drawer

Many devices have control operations that ordinary read/write cannot express: setting baud rate, reading status, triggering capture, configuring modes, or sending private commands.

ioctl is often used for those control paths.

The problem is that it is very flexible and easily becomes uncontrolled. If every unclassified feature is put into ioctl, the interface becomes a junk drawer:

  • unclear command numbers
  • incompatible argument structures
  • 32/64-bit process compatibility issues
  • scattered permission checks
  • unclear async and blocking semantics
  • difficult version evolution

When designing a user-driver interface, separate:

  • data streams that fit read/write
  • state attributes that fit sysfs, netlink, or similar mechanisms
  • control commands that truly fit ioctl
  • functionality that should use an existing kernel subsystem rather than a private interface

An interface is not only something that works today. It becomes a long-term contract between application and driver.

Why mmap Reduces Copying and Adds Boundary Complexity

Some devices or drivers map a kernel buffer, device memory, or DMA buffer into user space so the application can access it directly.

This can reduce copying and fits large or frequent data paths such as video frames, capture buffers, or shared ring buffers.

But mmap also makes boundaries more complex:

  • user space and kernel may access the same buffer
  • buffer lifetime must be clear
  • cache coherency must be handled
  • user space must not access beyond the mapped range
  • ordering between device DMA and CPU access must be defined
  • mappings must be cleaned up after close or process crash

So mmap is not simply “a better read.” It makes the data path faster while exposing synchronization, lifetime, and cache issues.

What Boundary to Check During Application-Driver Bugs

When an application cannot read data, a driver hangs, ioctl returns errors, or DMA data is wrong, do not immediately conclude “driver bug” or “application bug.”

Separate the boundaries.

First, are user-space parameters correct? Do fd, address, length, flags, command number, and structure version match?

Second, are permissions and device nodes correct? Can the application open the device, and does the node refer to the expected driver?

Third, do call semantics match? Blocking, nonblocking, timeout, signal interruption, and event notification must match expectations.

Fourth, does the driver handle hardware events correctly? Does the interrupt fire? Does the hardware FIFO or DMA ring contain data? Are status bits cleared?

Fifth, is user/kernel copying correct? Is the user pointer valid, is length bounded, and is the structure layout compatible?

Sixth, are cache and DMA coherent? Does the device see the same data version as the CPU?

These questions turn “the app and driver do not work” into a path that can be checked.

What to Remember in Practice

Applications, the kernel, and drivers are not one ordinary function-call chain.

They are separated by:

  • user/kernel privilege boundary
  • process address space and kernel address space boundary
  • system-call entry
  • file descriptor and device model
  • blocking, nonblocking, and event notification semantics
  • user buffers, kernel buffers, physical memory, and DMA address boundaries

The application sees interfaces such as read/write/ioctl/mmap. The kernel sees permissions, objects, state, and scheduling. The driver sees registers, interrupts, DMA, FIFOs, and hardware timing.

Separating these boundaries prevents debugging from collapsing permission errors, address errors, blocking behavior, hardware events, and cache coherency into one vague “the device does not respond.”