Project 5: Linux Kernel Module – Character Device Driver
Implement a kernel module that exposes
/dev/mydevicewith 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:
- Build and load an out-of-tree kernel module safely.
- Implement a character device with
file_operations. - Use
copy_to_user/copy_from_usercorrectly. - Design a lock-safe ring buffer for concurrent readers/writers.
- Implement ioctl commands and validate user input.
- Debug kernel code with
dmesgand 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)
- Build module with kernel headers.
- Load module via
insmod. - Kernel calls init; you register the device.
- 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
- Why must headers match the running kernel?
- What happens if init fails halfway?
Check-your-understanding answers
- Symbols and ABI must align with the running kernel.
- You must unwind and free anything already allocated.
Real-world applications
- Device drivers and kernel instrumentation
Where you’ll apply it
- This project: Section 5.1 setup, Section 5.10 Phase 1.
- Also used in: P08-mini-operating-system-kernel for kernel boot modules concepts.
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
- Build a hello-world module that logs on init/exit.
Solutions to the homework/exercises
- Use
printkand checkdmesg.
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)
- Register char device number.
- Create
cdevand add to kernel. - User opens
/dev/mydevice. - VFS calls your
read/writemethods.
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
- What does VFS do when a user calls
read()? - Why do you need a major number?
Check-your-understanding answers
- It dispatches to your device’s
readmethod. - It identifies the driver handling the device node.
Real-world applications
- Serial ports, input devices, pseudo devices
Where you’ll apply it
- This project: Section 4.2 components, Section 5.10 Phase 2.
- Also used in: P01-syscall-tracer-strace-clone to understand syscall dispatch.
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
- Register a device and implement a read that returns “hello”.
Solutions to the homework/exercises
- Use
copy_to_userto 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)
- Validate size and pointer.
- Call
copy_from_user. - 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
- Why can’t the kernel just dereference user pointers?
- What error code should be returned on failure?
Check-your-understanding answers
- It can cause faults or security issues.
-EFAULT.
Real-world applications
- All kernel subsystems interacting with user space
Where you’ll apply it
- This project: Section 3.2 Functional Requirements, Section 5.10 Phase 2.
- Also used in: P07-interrupt-latency-profiler for safe data exports.
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
- Implement a read path that returns a user-provided length.
Solutions to the homework/exercises
- Clamp length and use
copy_to_userwith 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
- Register char device with dynamic major/minor.
- Implement
open,release,read,write,ioctl. - Maintain a ring buffer with configurable size.
- Support blocking reads with wait queues.
- Protect shared state with spinlocks or mutexes.
- Provide
MYDEV_GET_STATSioctl returning buffer stats.
3.3 Non-Functional Requirements
- Reliability: no kernel panic; safe unload.
- Performance: handle 1MB/s write throughput.
- Usability: clear
/devnode 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 --statsfor 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
0success2invalid 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
- Write: copy from user into ring buffer, advance write pointer.
- Read: if empty, block or return
-EAGAIN; else copy to user. - 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
- Module lifecycle and kbuild.
- VFS + file_operations.
- User/kernel memory access.
5.5 Questions to Guide Your Design
- Should reads block or return
-EAGAIN? - Which lock type is appropriate?
- 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
- Why is
copy_to_userrequired? - 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
- Write 4KB and read back identical bytes.
- Empty read blocks until writer arrives.
- 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 -wfor live logs. - Add
pr_debugstatements 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.