Project 5: Linux Kernel Module – Character Device Driver

Implement a kernel module that exposes /dev/mydevice with a ring buffer, blocking reads, and ioctl stats.

Quick Reference

Attribute Value
Difficulty Level 4: Expert
Time Estimate 3-4 weeks
Main Programming Language C (kernel C only)
Alternative Programming Languages None
Coolness Level Level 4: Hardcore
Business Potential Level 3: Systems / driver work
Prerequisites C, basic kernel concepts, build tools, Linux headers
Key Topics kernel modules, char devices, uaccess, synchronization

1. Learning Objectives

By completing this project, you will:

  1. Build and load an out-of-tree kernel module safely.
  2. Implement a character device with file_operations.
  3. Use copy_to_user/copy_from_user correctly.
  4. Design a lock-safe ring buffer for concurrent readers/writers.
  5. Implement ioctl commands and validate user input.
  6. Debug kernel code with dmesg and dynamic debug.

2. All Theory Needed (Per-Concept Breakdown)

2.1 Kernel Module Lifecycle and kbuild

Fundamentals

Kernel modules are loadable pieces of kernel code. They must register themselves, allocate resources, and clean up on unload. Out-of-tree modules use kbuild to compile against the running kernel headers.

Deep Dive into the concept

A module defines module_init and module_exit. On load, the kernel resolves symbols and executes the init function. On unload, the exit function runs. You must handle error paths because a partially-initialized module can leak resources. kbuild uses Makefile rules to compile with the correct kernel configuration and version. A mismatch between headers and the running kernel can cause load failures.

How this fits on projects

This is required for Section 3.7 and Section 5.1/5.10 Phase 1.

Definitions & key terms

  • module -> dynamically loadable kernel code
  • kbuild -> kernel build system
  • insmod/rmmod -> load/unload module

Mental model diagram (ASCII)

insmod -> init() -> register device -> ready
rmmod -> exit() -> unregister device -> cleanup

How it works (step-by-step)

  1. Build module with kernel headers.
  2. Load module via insmod.
  3. Kernel calls init; you register the device.
  4. Unload via rmmod; kernel calls exit.

Minimal concrete example

module_init(my_init);
module_exit(my_exit);

Common misconceptions

  • Misconception: unloading is optional. Correction: cleanup is mandatory or the kernel leaks resources.

Check-your-understanding questions

  1. Why must headers match the running kernel?
  2. What happens if init fails halfway?

Check-your-understanding answers

  1. Symbols and ABI must align with the running kernel.
  2. You must unwind and free anything already allocated.

Real-world applications

  • Device drivers and kernel instrumentation

Where you’ll apply it

References

  • Kernel docs: kbuild modules

Key insights

Kernel modules live inside the kernel–mistakes are system-wide.

Summary

Module lifecycle discipline is the difference between safe experiments and kernel crashes.

Homework/Exercises to practice the concept

  1. Build a hello-world module that logs on init/exit.

Solutions to the homework/exercises

  1. Use printk and check dmesg.

2.2 VFS, file_operations, and Char Devices

Fundamentals

Character devices expose byte streams. The VFS calls your file_operations methods (open, read, write, release). You register a device number and create a node in /dev.

Deep Dive into the concept

VFS routes system calls to file operations via function pointers in struct file_operations. Your driver must implement correct semantics for open/read/write. You may block reads when the buffer is empty and wake readers when data arrives. The device node uses a major/minor number; you can allocate one dynamically (alloc_chrdev_region). User space interacts via open(2), read(2), and write(2).

How this fits on projects

This is the API contract in Section 3.1 and Section 3.2.

Definitions & key terms

  • VFS -> virtual filesystem layer
  • file_operations -> dispatch table for device methods
  • major/minor -> identifiers for device nodes

Mental model diagram (ASCII)

read() -> VFS -> file_operations.read -> driver ring buffer

How it works (step-by-step)

  1. Register char device number.
  2. Create cdev and add to kernel.
  3. User opens /dev/mydevice.
  4. VFS calls your read/write methods.

Minimal concrete example

static const struct file_operations fops = {
    .read = my_read,
    .write = my_write,
};

Common misconceptions

  • Misconception: device node appears automatically. Correction: you must create it (udev or mknod).

Check-your-understanding questions

  1. What does VFS do when a user calls read()?
  2. Why do you need a major number?

Check-your-understanding answers

  1. It dispatches to your device’s read method.
  2. It identifies the driver handling the device node.

Real-world applications

  • Serial ports, input devices, pseudo devices

Where you’ll apply it

References

  • “Linux Kernel Development” (Love), VFS chapters

Key insights

A char device is a file in user space but code in kernel space.

Summary

file_operations is the kernel-facing API you must implement correctly.

Homework/Exercises to practice the concept

  1. Register a device and implement a read that returns “hello”.

Solutions to the homework/exercises

  1. Use copy_to_user to copy a static buffer.

2.3 User/Kernel Memory Access (uaccess)

Fundamentals

User pointers are untrusted. Kernel code must use copy_to_user and copy_from_user to safely move data across the boundary.

Deep Dive into the concept

The kernel cannot trust user pointers; they may be invalid or point to unmapped memory. copy_to_user checks access permissions and returns the number of bytes not copied. You must handle partial copies and return -EFAULT on failure. You should never dereference a user pointer directly in kernel code. This rule is critical for security and stability.

How this fits on projects

This is required for your read/write/ioctl paths in Section 3.2 and Section 5.10.

Definitions & key terms

  • uaccess -> user access helpers
  • copy_to_user/copy_from_user -> safe copy functions
  • -EFAULT -> error for bad memory access

Mental model diagram (ASCII)

user buffer --(copy_from_user)--> kernel buffer

How it works (step-by-step)

  1. Validate size and pointer.
  2. Call copy_from_user.
  3. If copy fails, return -EFAULT.

Minimal concrete example

if (copy_from_user(kbuf, ubuf, len)) return -EFAULT;

Common misconceptions

  • Misconception: user pointers are always valid. Correction: they can point to unmapped or kernel memory.

Check-your-understanding questions

  1. Why can’t the kernel just dereference user pointers?
  2. What error code should be returned on failure?

Check-your-understanding answers

  1. It can cause faults or security issues.
  2. -EFAULT.

Real-world applications

  • All kernel subsystems interacting with user space

Where you’ll apply it

References

  • Kernel uaccess docs

Key insights

Safe copying is the heart of kernel/user boundary safety.

Summary

Never trust user pointers–always use uaccess helpers.

Homework/Exercises to practice the concept

  1. Implement a read path that returns a user-provided length.

Solutions to the homework/exercises

  1. Clamp length and use copy_to_user with error handling.

3. Project Specification

3.1 What You Will Build

A kernel module that registers /dev/mydevice. The device stores data in a ring buffer, supports blocking reads, non-blocking reads (O_NONBLOCK), and ioctl commands for stats and reset.

3.2 Functional Requirements

  1. Register char device with dynamic major/minor.
  2. Implement open, release, read, write, ioctl.
  3. Maintain a ring buffer with configurable size.
  4. Support blocking reads with wait queues.
  5. Protect shared state with spinlocks or mutexes.
  6. Provide MYDEV_GET_STATS ioctl returning buffer stats.

3.3 Non-Functional Requirements

  • Reliability: no kernel panic; safe unload.
  • Performance: handle 1MB/s write throughput.
  • Usability: clear /dev node creation instructions.

3.4 Example Usage / Output

$ sudo insmod mydevice.ko
$ echo "hello" > /dev/mydevice
$ cat /dev/mydevice
hello
$ ./myctl --stats
buffer_size=4096 used=5 read_pos=0 write_pos=5

3.5 Data Formats / Schemas / Protocols

  • ioctl payload struct:
    typedef struct {
      uint32_t size;
      uint32_t used;
      uint32_t read_pos;
      uint32_t write_pos;
    } mydev_stats_t;
    

3.6 Edge Cases

  • Empty buffer reads (block or return -EAGAIN).
  • Concurrent writers.
  • Unload while fd is open.

3.7 Real World Outcome

3.7.1 How to Run (Copy/Paste)

make
sudo insmod mydevice.ko
sudo mknod /dev/mydevice c <major> 0

3.7.2 Golden Path Demo (Deterministic)

  • Use fixed input strings and myctl --stats for deterministic output.

3.7.3 CLI Transcript (Success + Failure)

$ echo "hello" > /dev/mydevice
$ cat /dev/mydevice
hello

$ ./myctl --bad-ioctl
error: invalid ioctl
exit code: 2

3.7.4 Exit Codes

  • 0 success
  • 2 invalid ioctl or user error

4. Solution Architecture

4.1 High-Level Design

VFS -> file_operations -> ring buffer -> wait queues

4.2 Key Components

| Component | Responsibility | Key Decisions | |———–|—————-|—————| | cdev | device registration | dynamic major | | ring buffer | data storage | power-of-two size | | wait queue | blocking reads | wake on write | | ioctl | stats/reset | struct-based payload |

4.3 Data Structures (No Full Code)

struct ringbuf {
    char *buf;
    size_t size;
    size_t read_pos;
    size_t write_pos;
    spinlock_t lock;
};

4.4 Algorithm Overview

  1. Write: copy from user into ring buffer, advance write pointer.
  2. Read: if empty, block or return -EAGAIN; else copy to user.
  3. Ioctl: read stats or reset state.

5. Implementation Guide

5.1 Development Environment Setup

sudo apt install linux-headers-$(uname -r)

5.2 Project Structure

mydevice/
|-- mydevice.c
|-- Makefile
`-- user/
    |-- myctl.c

5.3 The Core Question You’re Answering

“How does the kernel expose a device to user space safely?”

5.4 Concepts You Must Understand First

  1. Module lifecycle and kbuild.
  2. VFS + file_operations.
  3. User/kernel memory access.

5.5 Questions to Guide Your Design

  1. Should reads block or return -EAGAIN?
  2. Which lock type is appropriate?
  3. How will you handle open file references on unload?

5.6 Thinking Exercise

Two writers race to write 4 bytes each. Draw the ring buffer state with and without locks.

5.7 The Interview Questions They’ll Ask

  1. Why is copy_to_user required?
  2. What’s the difference between spinlock and mutex?

5.8 Hints in Layers

  • Hint 1: implement read/write with a single global buffer.
  • Hint 2: add wait queues for blocking reads.
  • Hint 3: add ioctl for stats.

5.9 Books That Will Help

| Topic | Book | Chapter | |——|——|———| | Kernel modules | Linux Kernel Development | Module chapters | | Drivers | LKD | Driver basics |

5.10 Implementation Phases

Phase 1: minimal char device. Phase 2: ring buffer + blocking. Phase 3: ioctl + stats.


6. Testing Strategy

6.1 Test Categories

| Category | Purpose | Examples | |———|———|———-| | Unit | ring buffer | wrap-around behavior | | Integration | device IO | write then read | | Edge | concurrent writers | stress with threads |

6.2 Critical Test Cases

  1. Write 4KB and read back identical bytes.
  2. Empty read blocks until writer arrives.
  3. Invalid ioctl returns -EINVAL.

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |———|———|———-| | Wrong size in copy_to_user | kernel oops | validate length | | Missing locking | corrupted buffer | use spinlock |

7.2 Debugging Strategies

  • Use dmesg -w for live logs.
  • Add pr_debug statements with dynamic debug.

8. Extensions & Challenges

  • Add poll/select support.
  • Add multiple device instances.

9. Real-World Connections

  • USB serial devices, pseudo terminals, sensors

10. Resources

  • Kernel docs: Documentation/core-api/uaccess.rst

11. Self-Assessment Checklist

  • I can explain file_operations dispatch.
  • I can reason about kernel/user memory safety.

12. Submission / Completion Criteria

Minimum: device loads, read/write works. Full: ring buffer + ioctl stats. Excellence: poll/select + multi-device.


13. Determinism Notes

  • Use fixed input strings and deterministic buffer size.