How do character device or character special files work?

They are actually just that - interfaces. Encoded by a "major" and "minor" number they provide a hook to the kernel.

They come in two flavors (well, three, but named pipes are out of the scope of this explanation for now): Character Devices and Block Devices.

Block Devices tend to be storage devices, capable of buffering output and storing data for later retrieval.

Character Devices are things like audio or graphics cards, or input devices like keyboard and mouse.

In each case, when the kernel loads the correct driver (either at boot time, or via programs like udev) it scans the various buses to see if any devices handled by that driver are actually present on the system. If so, it sets up a device that 'listens' on the appropriate major/minor number.

(For instance, the Digital Signal Processor of the first audio card found by your system gets the major/minor number pair of 14/3; the second gets 14,35, etc.)

It's up to udev to create an entry in /dev named dsp as a character device marked major 14 minor 3.

(In significantly older or minimum-footprint versions of Linux, /dev/ may not be dynamically loaded but just contain all possible device files statically.)

Then, when a userspace program tries to access a file that's marked as a 'character special file' with the appropriate major/minor number (for instance, your audio player trying to send digital audio to /dev/dsp ), the kernel knows that this data needs to be transmitted via the driver that major/minor number is attached to; presumably said driver knows what to do with it in turn.


Every file, device or otherwise, supports 6 basic operations within the VFS:

  1. Open
  2. Close
  3. Read
  4. Write
  5. Seek
  6. Tell

Additionally, device files support I/O Control, which allows other miscellaneous operations not covered by the first 6.

In the case of a character special, seek and tell are not implemented since they support a streaming interface. That is, reading or writing directly such as is done with redirection in the shell:

echo 'foo' > /dev/some/char
sed ... < /dev/some/char

Minimal runnable file_operations example

Once you see a minimal example, it all becomes obvious.

The key ideas are:

  • file_operations contains the callbacks for each file related syscall
  • mknod <path> c <major> <minor> creates a character device which uses those file_operations
  • for character devices that dynamically allocate device numbers (the norm to avoid conflicts), find the number with cat /proc/devices

character_device.ko kernel module:

#include <asm/uaccess.h> /* copy_from_user, copy_to_user */
#include <linux/errno.h> /* EFAULT */
#include <linux/fs.h> /* register_chrdev, unregister_chrdev */
#include <linux/jiffies.h>
#include <linux/module.h>
#include <linux/printk.h> /* printk */
#include <uapi/linux/stat.h> /* S_IRUSR */

#define NAME "lkmc_character_device"

MODULE_LICENSE("GPL");

static int major;

static ssize_t read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
    size_t ret;
    char kbuf[] = {'a', 'b', 'c', 'd'};

    ret = 0;
    if (*off == 0) {
        if (copy_to_user(buf, kbuf, sizeof(kbuf))) {
            ret = -EFAULT;
        } else {
            ret = sizeof(kbuf);
            *off = 1;
        }
    }
    return ret;
}

static const struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = read,
};

static int myinit(void)
{
    major = register_chrdev(0, NAME, &fops);
    return 0;
}

static void myexit(void)
{
    unregister_chrdev(major, NAME);
}

module_init(myinit)
module_exit(myexit)

Userland test program:

insmod /character_device.ko
dev="lkmc_character_device"
major="$(grep "$dev" /proc/devices | cut -d ' ' -f 1)"
mknod "/dev/$dev" c "$major" 0
cat /dev/lkmc_character_device
# => abcd
rm /dev/lkmc_character_device
rmmod character_device

GitHub QEMU + Buildroot upstream with boilerplate to actually run it:

  • https://github.com/cirosantilli/linux-kernel-module-cheat/blob/6788a577c394a2fc512d8f3df0806d84dc09f355/kernel_module/character_device.c
  • https://github.com/cirosantilli/linux-kernel-module-cheat/blob/master/rootfs_overlay/character_device.sh

More complex examples:

  • read, write, lseek with a fixed size internal buffer and on debugfs instead of a character device: https://github.com/cirosantilli/linux-kernel-module-cheat/blob/6788a577c394a2fc512d8f3df0806d84dc09f355/kernel_module/fops.c
  • poll: https://github.com/cirosantilli/linux-kernel-module-cheat/blob/6788a577c394a2fc512d8f3df0806d84dc09f355/kernel_module/poll.c
  • ioctl: https://github.com/cirosantilli/linux-kernel-module-cheat/blob/6788a577c394a2fc512d8f3df0806d84dc09f355/kernel_module/poll.c
  • anon_inode_getfd associates a file_operations to a file descriptor without any filesystem file: https://stackoverflow.com/questions/4508998/what-is-anonymous-inode/44388030#44388030

Tags:

Drivers

Files