Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 66

Character Device File Operations

Recommended resources

Hello! Welcome back to the Embedded Interview Preparation Bootcamp.

In our last lesson, we successfully built and registered the skeleton of a character device driver. We have a /dev node that can be opened and closed, but it can't transfer any data yet. Now, we'll breathe life into it by implementing the very core of its functionality.

This lesson focuses on making your driver do useful work. You'll learn how to move data safely between your driver in kernel space and an application in user space, which is one of the most critical and frequently tested concepts in Linux driver development.

Your learning outcome for this lesson is to: Implement read, write, and ioctl handlers, using copy_from_user/copy_to_user for safe data transfer.


1. The Great Wall: User Space vs. Kernel Space

Before we write a single line of code to transfer data, we must understand why we can't simply dereference a pointer that a user application passes to us. The Linux kernel enforces a strict memory separation between user space (where applications run) and kernel space (where the OS and drivers run).

  • Protection: This prevents a buggy or malicious application from crashing the entire system or accessing another process's data.
  • Virtual Memory: Each user process gets its own virtual address space. An address like 0x400500 in one process points to different physical RAM than the same address in another process. The kernel has its own separate memory mapping.

Therefore, a pointer from user space is not directly valid in the kernel's context. Attempting to access it can lead to a kernel panic.

Linux Virtual Address Space Layout (32-bit)
This diagram shows the default 32-bit Linux virtual memory layout. The kernel occupies a reserved, protected portion of the address space (typically the top 1GB), inaccessible to user applications. Each user process operates within its own 3GB virtual space.

To understand this boundary and how the kernel manages the transition, the following video provides an excellent deep-dive into system calls and the machinery that protects the kernel.

Syscalls, Kernel vs. User Mode and Linux Kernel Source Code - bin 0x09

The video 'Syscalls, Kernel vs. User Mode' from LiveOverflow explains the fundamental concepts of privilege levels and the user/kernel boundary. It brilliantly illustrates why direct memory access is forbidden and introduces the copy_from_user function by tracing it through the kernel source.

Watch from the beginning to 07:46. Focus on these key points: The distinction between user mode (ring 3) and kernel mode (ring 0). How system calls (syscall) are the controlled entry point into the kernel. The concept of virtual vs. physical addresses and the MMU's role. The introduction of copy_from_user as the necessary tool to bridge the address space gap.


2. The Golden Gate: copy_from_user and copy_to_user

The kernel provides a specific, secure API for moving data across the user-kernel boundary. These functions are mandatory for any data transfer in a driver.

Character device drivers — The Linux Kernel documentation

The official Linux Kernel documentation gives the definitive explanation of these functions.

Please read the section titled 'Access to the address space of the process'. This short section introduces the four key functions and explains their purpose and return values. Pay close attention to the prototypes for copy_to_user and copy_from_user.

The two most important functions for our read and write handlers are:

  • unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
    • Copies n bytes from user space (from) to kernel space (to).
    • Used in your write handler.
  • unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
    • Copies n bytes from kernel space (from) to user space (to).
    • Used in your read handler.

Key Properties:

  • __user Marker: The __user annotation is a signal to the kernel (and static analysis tools like sparse) that the pointer originates from user space and must not be trusted.
  • Return Value: Both functions return the number of bytes that failed to copy. A return value of 0 indicates success. A non-zero return value means an error occurred (e.g., the user passed a bad pointer), and you should typically return -EFAULT from your handler.


3. Implementing read and write

Now let's add these handlers to the file_operations struct we created in the last lesson. We'll create a simple driver that has an internal 1KB buffer.

First, let's look at the standard implementations for these handlers.

Character device drivers — The Linux Kernel documentation

This documentation provides clear, commented code snippets for both read and write handlers.

Read the section 'Read and write'. Analyze the provided my_read and my_write example functions. Note how they: Use min() to determine how many bytes to transfer, considering the requested size and the remaining data/space in the buffer. Handle the offset pointer to support sequential reads and writes. Use copy_to_user and copy_from_user and check their return values. Update the *offset after a successful transfer. Return the number of bytes actually transferred.

Let's integrate this into our driver. We'll modify the code from the previous lesson to include a data buffer and the read/write implementations.




#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h> // Required for copy_to/from_user
#include <linux/slab.h>   // Required for kmalloc/kfree




#define DRIVER_NAME "my_cdev"
#define CLASS_NAME  "my_class"
#define BUFFER_SIZE 1024

// Forward declarations
static int my_open(struct inode *inode, struct file *file);
static int my_release(struct inode *inode, struct file *file);
static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off);
static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off);

// Data for our driver
static dev_t my_device_nr;
static struct class* my_class = NULL;
static struct cdev my_cdev;

// Device-specific data
struct my_device_data {
    char *buffer;
    size_t data_len;
};

// File operations structure
static const struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
};

// Implementation of read
static ssize_t my_read(struct file *filp, char __user *user_buf, size_t len, loff_t *off) {
    struct my_device_data *dev_data = filp->private_data;
    ssize_t bytes_to_read;

    // How many bytes can we actually read?
    bytes_to_read = min(len, dev_data->data_len - *off);

    if (bytes_to_read <= 0) {
        pr_info(DRIVER_NAME ": End of file reached.\n");
        return 0; // EOF
    }

    // Safely copy data from our kernel buffer to the user buffer
    if (copy_to_user(user_buf, dev_data->buffer + *off, bytes_to_read) != 0) {
        pr_err(DRIVER_NAME ": copy_to_user failed.\n");
        return -EFAULT;
    }

    // Advance the file offset
    *off += bytes_to_read;

    pr_info(DRIVER_NAME ": Read %zd bytes.\n", bytes_to_read);
    return bytes_to_read;
}

// Implementation of write
static ssize_t my_write(struct file *filp, const char __user *user_buf, size_t len, loff_t *off) {
    struct my_device_data *dev_data = filp->private_data;
    ssize_t bytes_to_write;

    // How many bytes can we actually write?
    bytes_to_write = min(len, BUFFER_SIZE - *off);

    if (bytes_to_write <= 0) {
        pr_warn(DRIVER_NAME ": No space left on device.\n");
        return -ENOSPC; // No space left
    }
    
    // Safely copy data from the user buffer to our kernel buffer
    if (copy_from_user(dev_data->buffer + *off, user_buf, bytes_to_write) != 0) {
        pr_err(DRIVER_NAME ": copy_from_user failed.\n");
        return -EFAULT;
    }
    
    // Update data length and file offset
    *off += bytes_to_write;
    dev_data->data_len = max(dev_data->data_len, (size_t)*off);

    pr_info(DRIVER_NAME ": Wrote %zd bytes.\n", bytes_to_write);
    return bytes_to_write;
}

// In open, we allocate our buffer
static int my_open(struct inode *inode, struct file *file) {
    struct my_device_data *dev_data;

    dev_data = kmalloc(sizeof(struct my_device_data), GFP_KERNEL);
    if (!dev_data) {
        return -ENOMEM;
    }

    dev_data->buffer = kzalloc(BUFFER_SIZE, GFP_KERNEL);
    if (!dev_data->buffer) {
        kfree(dev_data);
        return -ENOMEM;
    }
    dev_data->data_len = 0;

    file->private_data = dev_data; // Store for other calls
    pr_info(DRIVER_NAME ": Device opened.\n");
    return 0;
}

// In release, we free it
static int my_release(struct inode *inode, struct file *file) {
    struct my_device_data *dev_data = file->private_data;

    if (dev_data) {
        kfree(dev_data->buffer);
        kfree(dev_data);
    }
    pr_info(DRIVER_NAME ": Device released.\n");
    return 0;
}

// ... init and exit functions remain similar to the previous lesson,
// but now should also create the class and device for udev.


4. Implementing ioctl for Out-of-Band Control

read and write are great for data streams, but what if you need to perform a specific action that doesn't fit the stream model? For example:

  • Setting a device configuration parameter (e.g., baud rate).
  • Querying device status.
  • Resetting the device.

This is where ioctl (Input/Output Control) comes in. It's a generic, catch-all system call for device-specific commands. The ioctl handler in your driver is unlocked_ioctl.

The following video provides a complete, practical demonstration of adding ioctl support to a driver.

Let's code a Linux Driver - 13: IOCtl in a Linux Kernel Module

The video 'Let's code a Linux Driver - 13: IOCtl' is a step-by-step tutorial that perfectly aligns with our goal. It shows how to define commands, implement the handler with copy_from_user/copy_to_user, and test it from a user-space app.

Watch from 05:30 to 18:58. This is the core of the implementation. Follow along as the presenter: Defines custom ioctl commands (_IOW, _IOR) in a shared header file. Implements the my_ioctl function with a switch statement to handle different commands. Uses copy_from_user and copy_to_user to pass data for the commands. Registers the handler in the file_operations struct. Writes and runs a user-space test application that calls ioctl().

The process for implementing ioctl can be broken into three main steps:

Step 1: Define Your Commands
Commands should be defined in a header file shared between the kernel driver and the user-space application. This ensures they use the same unique numbers.

// In a shared header file, e.g., "my_cdev_ioctl.h"



#include <linux/ioctl.h>

// Command to get the current data size



#define MY_CDEV_GET_LEN _IOR('k', 1, int*)

// Command to zero out the buffer



#define MY_CDEV_ZERO _IO('k', 2)

// 'k' is the "magic number". 1 and 2 are command numbers.

Step 2: Implement the unlocked_ioctl Handler
Add the function to your driver and wire it up in file_operations.

// In your driver.c file



#include "my_cdev_ioctl.h"

static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
    struct my_device_data *dev_data = file->private_data;
    int len_to_copy;

    switch (cmd) {
        case MY_CDEV_GET_LEN:
            len_to_copy = dev_data->data_len;
            if (copy_to_user((int*)arg, &len_to_copy, sizeof(int))) {
                return -EFAULT;
            }
            pr_info(DRIVER_NAME ": ioctl: GET_LEN gave %d\n", len_to_copy);
            break;

        case MY_CDEV_ZERO:
            memset(dev_data->buffer, 0, BUFFER_SIZE);
            dev_data->data_len = 0;
            pr_info(DRIVER_NAME ": ioctl: ZEROed buffer\n");
            break;

        default:
            return -ENOTTY; // Command not supported
    }
    return 0; // Success
}

// In your file_operations struct
static const struct file_operations my_fops = {
    // ... other handlers
    .unlocked_ioctl = my_ioctl,
};

Step 3: Call ioctl from User Space
Your test application can now use these commands.

// In your user-space test_app.c



#include <sys/ioctl.h>
#include "my_cdev_ioctl.h"

// ... after opening the device file to get 'fd' ...

int len;
ioctl(fd, MY_CDEV_GET_LEN, &len);
printf("Driver reports data length is: %d\n", len);

printf("Zeroing driver buffer...\n");
ioctl(fd, MY_CDEV_ZERO, NULL);


Conclusion

You have now successfully implemented the three most important file_operations handlers, transforming your driver from a passive skeleton into a functional component that can bidirectionally transfer data and respond to control commands.

Key Takeaways:

  • User/Kernel Boundary: This is a hard security and stability boundary. Never trust a pointer from user space.
  • Safe Data Transfer: copy_from_user() (for write) and copy_to_user() (for read) are the mandatory, non-negotiable functions for moving data. Always check their return value.
  • read/write Handlers: These are designed for stream-like data. A robust implementation must correctly manage the number of bytes transferred and update the file offset.
  • unlocked_ioctl Handler: This is the versatile tool for all other device-specific control operations. Its implementation typically involves a switch statement on the command number.

Preview of the Next Lesson:

So far, we've created a driver for a "virtual" device that exists only in memory. In a real system, a driver must be associated with physical hardware. In the next lesson, we will explore this by learning to explain the platform driver model, including device tree probing, compatible string matching, and resource retrieval. This is how modern Linux kernels discover and bind drivers to hardware automatically.