JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Character, Block, and Network Devices

The kernel does not have one shape for every gadget. It has three. This guide shows why a keyboard, a disk, and an Ethernet card each plug into a different socket in the kernel, and how a character driver wires open() and read() straight to your code.

Three sockets, not one

Guide 1 gave you the driver model: a bus offers a device, the kernel matches it to a driver, and the driver's probe function runs. But "the driver runs" leaves a question hanging — runs as what? When a program later calls read() on your hardware, where does that call actually land? The kernel's answer is not one universal door. It sorts every device into one of three families, and each family plugs into a different socket in the kernel with a different shape. A keyboard, a hard disk, and a network card do not look alike to user space at all, and this guide is about why.

The three families are the character device, the block device, and the network device. The dividing line is the access pattern the hardware naturally wants. A character device is a stream: bytes flow past one after another, in order, and once a byte is read it is gone — think a keyboard, a serial port, an audio line. A block device is random-access storage: it is a numbered array of fixed-size blocks (commonly 512 bytes or 4 KiB each) that you can seek into and re-read at will — a disk, an SSD, a USB stick. A network device is neither: it does not hold data, it moves packets, and it does not even fit the file metaphor the way the other two do.

How user space names a device: the device node

Before a program can talk to your driver, it needs a name to open. That name is a device node — a special file under /dev, like /dev/sda or /dev/ttyS0. It looks like a file in a directory listing, but it holds no data of its own; it is a labelled doorway. The label is two small numbers stored in the node, the major and minor numbers. The major number says which driver owns this node, and the minor number says which device among the ones that driver handles. So /dev/sda and /dev/sdb can share a major (both are the same disk driver) and differ only in minor (first disk, second disk).

Now the path lights up. When a program runs open("/dev/ttyS0", O_RDWR), the kernel resolves the path, finds the node, reads its major number, and looks that major up in a table to find your driver. From that instant, this open file descriptor is bound to your code: the next read() or write() on it will not go to a filesystem, it will go straight to functions you wrote. The device node is the bridge between the ordinary file API that every program already speaks and the specific driver sitting behind the hardware.

The character driver, wired in: file_operations

Of the three, the character device is the one to learn first, because its wiring is the most direct and it is where almost everyone writes their first driver. The connection between "a program called read()" and "my function runs" is a single struct full of function pointers: the file_operations struct. You declare a struct file_operations, fill in the slots you care about with the addresses of your own functions, and register it under your major number. Each slot corresponds to one system call: the .read field gets called for read(), .write for write(), .open for open(), .release for close(), and so on.

// a character driver's wiring, stripped to the essentials

static ssize_t my_read(struct file *f, char __user *ubuf,
                       size_t n, loff_t *off)
{
    char msg[] = "hi";
    size_t len = sizeof(msg) - 1;
    if (*off >= len)
        return 0;                 // 0 == end of file, no more bytes
    if (n > len - *off)
        n = len - *off;
    if (copy_to_user(ubuf, msg + *off, n))  // checked crossing!
        return -EFAULT;           // bad user pointer -> clean error
    *off += n;
    return n;                     // number of bytes delivered
}

static const struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .read  = my_read,             // read() lands here
    .open  = my_open,
    .release = my_release,
};
The whole idea of a character driver in one struct: fill in the function pointers, and the kernel calls your code when user space makes the matching syscall.

Read my_read() slowly, because every line earns its place and several echo earlier guides. The char __user *ubuf parameter is the user's buffer, and the __user tag is a deliberate reminder that this pointer is not safe to dereference in the kernel — exactly the boundary the system-call guide drilled into you. So instead of *ubuf = ..., the driver calls copy_to_user(), which checks the address and faults cleanly into -EFAULT if it is bad. The off pointer is the file offset, letting a stream keep its place across calls, and returning 0 is how a character device signals end of file. None of this is decoration: skip the copy_to_user() check and you have written a kernel security hole, not a shortcut.

Block devices: why a disk is not just a slow character device

A beginner's natural guess is that a disk is a character device with more bytes — just read them in order. That guess is wrong in an instructive way, and seeing why is the whole point of the block family. Storage hardware has two stubborn facts: it is addressed in fixed-size blocks, not single bytes, and a block you read once you will very likely read again soon. So the kernel does not wire your disk driver straight to read() the way a character driver is wired. It slides two heavyweight layers in between, and that is what makes a block device a different animal.

The first layer is the page cache. When you read a file, the kernel keeps the blocks it fetched in RAM, so the next read of the same data never touches the disk at all — it is served from memory. When you write, your bytes land in the cache first and are flushed to the platter later, in the background. This is why your disk light does not blink on every write() and why re-reading a file is suddenly instant. A character device gets none of this caching, because caching a keyboard stream would be nonsense — you cannot re-read a keystroke that already flowed past.

The second layer is the block I/O layer and its scheduler. Real reads and writes that do reach the disk get bundled into block requests, then handed to an I/O scheduler that reorders and merges them before the hardware sees them. The reason is mechanical sympathy: on a spinning disk, serving requests in address order instead of arrival order avoids flinging the head back and forth; even on an SSD, merging many small requests into a few large ones is a big win. So a block driver's job is not "copy these bytes now" but "complete this queue of requests efficiently." Your driver implements the bottom of that stack, and the cache and scheduler sit above it doing work a character driver never has to think about.

Network devices: the family that broke the file mold

The network device is the honest exception, and it is worth being clear about how far it differs rather than pretending it fits. There is no /dev/eth0 to open, no file_operations with a .read for your card. Instead the kernel hands a network driver a completely different structure (struct net_device) whose key slot is not "how do I read a byte" but "here is a packet — transmit it." The hardware does not deliver a tidy stream of bytes you asked for; it delivers packets, asynchronously, whenever they happen to arrive from the wire, addressed to whichever program is listening.

That asynchrony forces an inversion you should hold onto. A character or block driver is mostly reactive: it sits still until a syscall calls one of its function pointers, then runs. A network driver must also handle data that nobody asked for at the moment it arrives — the wire does not wait for your read(). So incoming packets are pushed up the network stack toward whatever socket eventually wants them, rather than pulled down by a read. This is why network devices reach user space through the socket API you met in the networking rung, not through open() on a path: the file model assumes you pull bytes when you choose, and packets simply do not work that way.

Step back and the map is clean. Every device the kernel knows is sorted into character, block, or network, and the family decides everything downstream: a character driver wires its file_operations struct straight to read() and write() for a byte stream; a block driver lives under a cache and an I/O scheduler that turn random storage into fast files; a network driver moves packets up and down a stack and is reached through sockets, not a path. They share the driver model and the major/minor naming scheme, but the shape of the work could hardly be more different. Next, guide 3 goes inside the moment a device actually demands attention — the interrupt — and the careful split between handling it fast and handling it fully.