Project 10: Everything Is a File (Device Files)

Build character devices that behave like real /dev nodes and expose kernel-like behavior.

Quick Reference

Attribute Value
Difficulty Advanced
Time Estimate 12-18 hours
Main Programming Language C (kernel module or FUSE)
Alternative Programming Languages Rust (kernel module), FUSE in Go
Coolness Level High
Business Potential Medium (kernel/dev tooling)
Prerequisites file I/O, permissions, basic kernel module knowledge
Key Topics device nodes, char devices, read/write semantics

1. Learning Objectives

By completing this project, you will:

  1. Register a character device with a major/minor number.
  2. Implement read/write semantics for device files.
  3. Expose state through /dev nodes with permissions.
  4. Understand how user-space sees devices as files.

2. All Theory Needed (Per-Concept Breakdown)

Device Nodes and Character Device Semantics

Fundamentals

Unix treats devices as files. A device node in /dev is a special file that points to a driver via major and minor numbers. Character devices provide a stream of bytes, while block devices support random access in blocks. When a process reads or writes a device node, the kernel calls the driver’s read/write handlers. This abstraction allows the same read/write API to work for hardware (disks, terminals) and software pseudo-devices (/dev/null, /dev/urandom).

Deep Dive into the concept

The major/minor numbers encode which driver and which instance handles the device. The major number maps to a driver; the minor distinguishes multiple devices handled by the same driver. When you register a character device, you provide a file_operations structure with function pointers for open, read, write, and release. The kernel routes system calls to your callbacks. For FUSE, you implement read/write in user space, but the conceptual model is identical.

Read semantics are subtle: a read should return bytes and the number of bytes read. Returning 0 signals EOF. If you never return 0, programs that read in a loop (like cat) will block or spin. Write semantics should accept data, update device state, and return the number of bytes written. You must handle offsets: for some devices, the offset is ignored (stream devices), while for others it tracks position.

Permissions matter. The device node has mode bits and ownership like any file, and udev rules can set them automatically. This is how systems control access to devices (e.g., only root can write to /dev/mem). Your project should explicitly create nodes with appropriate permissions and demonstrate access control.

This project can be implemented as a kernel module (more authentic) or as a FUSE filesystem (safer). In kernel space, you must copy data to/from user space using copy_to_user and copy_from_user. Failing to do so can crash the kernel. In user space, you simply respond to read/write callbacks. Either way, the interface is identical to user programs: a device file is just a path that supports open/read/write.

How this fit on projects

This concept connects Section 3.2 and Section 3.7 and is used later in Project 15 (kernel modules) and Project 16 (network stack instrumentation).

Definitions & key terms

  • Character device: device that provides a byte stream.
  • Major/minor: identifiers for device driver and instance.
  • udev: userspace manager for /dev nodes.
  • file_operations: kernel callbacks for file I/O.

Mental model diagram (ASCII)

read(/dev/mydev) -> VFS -> driver read() -> device state

How it works (step-by-step)

  1. Register device and obtain major/minor.
  2. Create /dev node with mknod.
  3. User opens /dev node.
  4. Kernel calls driver read/write.
  5. Driver returns bytes and updates state.

Minimal concrete example

static ssize_t my_read(struct file *f, char __user *buf, size_t len, loff_t *off) {
    return simple_read_from_buffer(buf, len, off, data, data_len);
}

Common misconceptions

  • “Device nodes are regular files”: they are special inodes with major/minor numbers.
  • “Read always returns full length”: it may return fewer bytes.

Check-your-understanding questions

  1. What is the difference between a char and block device?
  2. Why must read return 0 to signal EOF?
  3. What does major number represent?

Check-your-understanding answers

  1. Char devices are streams; block devices are block-addressable.
  2. Many programs read until EOF; without 0, they never stop.
  3. It selects the driver that handles the device.

Real-world applications

  • /dev/null, /dev/random, /dev/tty.

Where you’ll apply it

  • This project: Section 3.2, Section 3.7, Section 5.10 Phase 2.
  • Also used in: Project 15.

References

  • “Linux Kernel Development” Ch. 6-7
  • TLPI Ch. 13

Key insights

Device files are the same API as files, but their meaning is implemented by the kernel.

Summary

By implementing devices, you learn how the OS exposes hardware as simple file I/O.

Homework/Exercises to practice the concept

  1. Add a counter device that increments on each read.
  2. Add a log device that stores the last N writes.
  3. Add a random device that uses a fixed seed for determinism.

Solutions to the homework/exercises

  1. Store a counter and format it as text.
  2. Implement a ring buffer in kernel or user space.
  3. Use a PRNG with a fixed seed and return bytes.

3. Project Specification

3.1 What You Will Build

Three device nodes: /dev/myrand, /dev/mycount, and /dev/mylog. Each provides specific read/write behavior and logs activity.

3.2 Functional Requirements

  1. Register a character device and create nodes.
  2. Implement myrand to return deterministic random bytes.
  3. Implement mycount to increment on each read.
  4. Implement mylog to accept writes and log them.

3.3 Non-Functional Requirements

  • Performance: reads return within 10 ms.
  • Reliability: no kernel crashes (if module) or FUSE deadlocks.
  • Usability: clear README and demo commands.

3.4 Example Usage / Output

$ head -c 8 /dev/myrand | hexdump -C
00000000  4f 12 a9 7c 2b 90 33 01

3.5 Data Formats / Schemas / Protocols

  • mylog stores last N lines as newline-delimited bytes.

3.6 Edge Cases

  • Read with len=0.
  • Multiple concurrent readers.
  • Writes larger than buffer.

3.7 Real World Outcome

3.7.1 How to Run (Copy/Paste)

sudo insmod mydev.ko   # or start FUSE
sudo mknod /dev/myrand c 240 0
sudo mknod /dev/mycount c 240 1
sudo mknod /dev/mylog c 240 2

3.7.2 Golden Path Demo (Deterministic)

  • myrand uses a fixed seed (seed=42) so output is repeatable.

3.7.3 If CLI: exact terminal transcript

$ head -c 4 /dev/myrand | hexdump -C
00000000  5f 0a 2b 91
$ cat /dev/mycount
1
$ cat /dev/mycount
2
$ echo "hello" > /dev/mylog
$ dmesg | tail -n 1
mylog: hello

Failure demo (deterministic):

$ cat /dev/myrand
cat: /dev/myrand: Permission denied

Exit codes:

  • 0 success
  • 2 invalid args
  • 3 permission error

4. Solution Architecture

4.1 High-Level Design

/dev node -> VFS -> device driver -> device state

4.2 Key Components

| Component | Responsibility | Key Decisions | |———–|—————-|—————| | Driver init | register device | static major=240 | | Read handler | produce bytes | deterministic PRNG | | Write handler | accept log lines | ring buffer |

4.3 Data Structures (No Full Code)

struct mylog {
    char buf[4096];
    size_t head;
};

4.4 Algorithm Overview

Key Algorithm: ring buffer write

  1. Copy input bytes.
  2. Write to buffer at head.
  3. Wrap on overflow.

Complexity Analysis:

  • Time: O(n) per write
  • Space: O(1)

5. Implementation Guide

5.1 Development Environment Setup

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

5.2 Project Structure

project-root/
|-- mydev.c
|-- Makefile
`-- README.md

5.3 The Core Question You’re Answering

“How does the OS expose devices as simple files with read/write semantics?”

5.4 Concepts You Must Understand First

  1. char device registration.
  2. copy_to_user/copy_from_user.
  3. device node permissions.

5.5 Questions to Guide Your Design

  1. How will you assign major/minor numbers?
  2. What does read mean for each device?
  3. How will you prevent concurrent access bugs?

5.6 Thinking Exercise

Design a /dev/uptime device. What does it return and how is it computed?

5.7 The Interview Questions They’ll Ask

  1. What is the difference between a char and block device?
  2. Why does Unix treat devices as files?

5.8 Hints in Layers

Hint 1: Implement a device that always returns a fixed string.

Hint 2: Add stateful counter and log buffer.

Hint 3: Add permission and concurrency handling.

5.9 Books That Will Help

| Topic | Book | Chapter | |——-|——|———| | Devices | Linux Kernel Development | 6-7 | | Device files | TLPI | 13 |

5.10 Implementation Phases

Phase 1: Device registration (3-4 hours)

Goals: /dev nodes appear and open works.

Phase 2: Read/write semantics (4-6 hours)

Goals: myrand, mycount, mylog behaviors.

Phase 3: Permissions + concurrency (3-4 hours)

Goals: correct access control and safe locking.

5.11 Key Implementation Decisions

| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | Kernel vs FUSE | module vs user space | module (if safe) | real driver semantics | | Random source | /dev/urandom vs PRNG | PRNG with seed | deterministic tests |


6. Testing Strategy

6.1 Test Categories

| Category | Purpose | Examples | |———-|———|———-| | Unit | ring buffer | wrap-around logic | | Integration | /dev reads | cat/head tests | | Security | permissions | non-root access |

6.2 Critical Test Cases

  1. Two readers on mycount increments sequentially.
  2. Large write to mylog wraps correctly.
  3. myrand output matches expected seed.

6.3 Test Data

Seed=42 => first 4 bytes: 5f 0a 2b 91

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |——–|———|———-| | read never returns 0 | cat hangs | return EOF when appropriate | | missing copy_to_user | kernel oops | always use copy helpers | | race conditions | inconsistent output | add mutexes |

7.2 Debugging Strategies

  • Use dmesg logs for kernel module debugging.
  • Add verbose logs for read/write paths.

7.3 Performance Traps

  • Excessive logging inside read/write.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add /dev/myuptime device.

8.2 Intermediate Extensions

  • Add ioctl support for configuration.

8.3 Advanced Extensions

  • Implement a block device backed by a file.

9. Real-World Connections

9.1 Industry Applications

  • Custom kernel devices, monitoring tools.
  • Linux kernel drivers in drivers/char.

9.3 Interview Relevance

  • Device file and driver model questions.

10. Resources

10.1 Essential Reading

  • Linux Kernel Development Ch. 6-7

10.2 Video Resources

  • Kernel module basics lectures

10.3 Tools & Documentation

  • man mknod, kernel module docs

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain major/minor numbers.
  • I can explain char vs block devices.

11.2 Implementation

  • Device nodes behave as specified.

11.3 Growth

  • I can debug a simple device driver.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • One working device node.

Full Completion:

  • All three devices with correct semantics.

Excellence (Going Above & Beyond):

  • ioctl support and block device emulation.