OS Lab 11 - Linux Kernel Modules
Objectives
Upon completion of this lab, you will be able to:
- Explain the kernel module lifecycle and how loadable modules extend kernel functionality without requiring a reboot or kernel recompilation.
- Understand the miscdevice framework for creating character devices that appear in
/dev/and respond to file operations. - Implement proper kernel-space buffer management with mutex-based synchronization to handle concurrent access safely.
- Use the ioctl() interface to pass configuration structures between userspace and kernel space, enabling device-specific operations beyond standard read/write.
- Apply safe data transfer techniques using copy_to_user() and copy_from_user() to respect memory protection boundaries.
- Implement byte transformations (case conversion, character filtering) that execute in kernel context during write operations.
- Debug kernel code using printk() and interpret kernel log messages to diagnose issues.
- Connect kernel programming concepts to previous labs: device files (Lab 2), system calls (Lab 3), IPC mechanisms (Lab 6), and kernel primitives (Labs 7-10).
Introduction
What You'll Build: The Superpipe Device
You'll implement a loadable kernel module called superpipe that creates a character device at /dev/superpipe. This device acts like a regular pipe with a twist: it can transform data as it passes through.
How it works:
- Userspace writes data to
/dev/superpipe(normalwrite()syscall) - Your kernel module receives the data, applies a transformation (uppercase, lowercase, filter vowels, etc.), and stores it in a kernel buffer
- Userspace reads data from
/dev/superpipe(normalread()syscall) - Your kernel module returns the transformed data
- Userspace configures the transformation mode using
ioctl()(passing configuration structures)
Example usage:
# Set transformation to uppercase via ioctl
./test_superpipe set_mode upper
# Write lowercase text
echo "hello world" > /dev/superpipe
# Read back - gets uppercase version
cat /dev/superpipe
# Output: HELLO WORLD
This demonstrates core kernel programming concepts:
- Device registration: Creating a device file that appears in
/dev/ - File operations: Implementing the kernel side of open, read, write, close
- Memory management: Allocating kernel buffers with
kmalloc(), usingkfree() - Data transfer: Safely moving data between user and kernel space
- Synchronization: Using mutexes to protect shared data from concurrent access
- ioctl interface: Passing configuration structures for device-specific operations
- Transform-on-write: Applying byte transformations as data enters the kernel
Prerequisites
System Requirements
Operating System: Linux-based system with kernel 5.x or 6.x
Check your kernel version:
uname -r
If you're running kernel 4.x or older, you may encounter compatibility issues with some kernel APIs used in this lab.
Kernel Headers: You must have kernel headers installed that match your running kernel. The headers provide the necessary definitions and build infrastructure for compiling kernel modules.
Build Tools: GCC compiler and make utility for building the module.
Privileges: Root access via sudo is required for loading/unloading modules and accessing device files.
Disk Space: At least 500MB free for kernel headers and build artifacts.
IMPORTANT: Work in a Virtual Machine
Kernel programming is different from userspace programming. Bugs in kernel code can cause kernel panics, system freezes, or data corruption. A simple null pointer dereference that would segfault in userspace can crash your entire system in kernel space.
Therefore, it is strongly recommended to work in a virtual machine (VirtualBox, VMware, QEMU, or similar). This way, if your module crashes the kernel, you can simply restart the VM without affecting your host system.
Required Packages
Install the necessary packages:
sudo apt update
sudo apt install -y \
build-essential \
linux-headers-$(uname -r) \
vim
Package descriptions:
build-essential: Meta-package that installs GCC, make, and other compilation toolslinux-headers-$(uname -r): Kernel header files matching your running kernel versionvim: Text editor (or use your preferred editor)
Verify kernel headers installation:
ls -l /lib/modules/$(uname -r)/build
This should show a symbolic link pointing to the kernel build directory (usually in /usr/src/). If this directory doesn't exist, your kernel headers are not properly installed.
Theoretical Background
Kernel Modules: Extending the Kernel Dynamically
What is a Kernel Module?
The Linux kernel is the core of the operating system—it manages hardware, enforces security, schedules processes, and provides the abstractions (files, processes, sockets) that userspace programs rely on. But the kernel faces a design challenge: it must support thousands of different hardware devices, filesystems, network protocols, and features. If every possible driver and feature were compiled directly into the kernel, the kernel image would be enormous, and systems would waste memory loading features they never use.
Linux solves this with loadable kernel modules—pieces of code that can be dynamically loaded into the running kernel and unloaded when no longer needed. Think of modules as plugins for the kernel.
Benefits of kernel modules:
- Flexibility: Load only the drivers and features you actually need
- Development speed: Test kernel code without rebooting
- Modularity: Separate concerns (network drivers, filesystem support, etc.) into independent modules
- Distribution: Ship proprietary drivers separately from the GPL-licensed kernel
- Memory efficiency: Don't waste RAM on unused features
Examples of kernel modules:
lsmod
This command lists currently loaded modules. You'll see entries like:
nvidia- NVIDIA graphics driverext4- ext4 filesystem supportiwlwifi- Intel wireless driverbluetooth- Bluetooth protocol support
Each of these started as a .ko file (kernel object) on disk and was loaded into the running kernel.
Module Lifecycle
A kernel module progresses through a well-defined lifecycle:
┌─────────────────────┐
│ Module Source │ module.c (your code)
│ (.c file) │
└──────────┬──────────┘
│ Compilation (make)
↓
┌─────────────────────┐
│ Module Binary │ module.ko (kernel object)
│ (.ko file) │ Lives on disk
└──────────┬──────────┘
│ insmod / modprobe
↓
┌─────────────────────┐
│ Init Function │ module_init() runs
│ Runs Once │ Allocates resources
│ │ Registers device
└──────────┬──────────┘
│ Success
↓
┌─────────────────────┐
│ Module Loaded │ Module is active
│ Functions Callable│ Handlers installed
│ │ Device available
└──────────┬──────────┘
│ rmmod
↓
┌─────────────────────┐
│ Exit Function │ module_exit() runs
│ Runs Once │ Frees resources
│ │ Unregisters device
└─────────────────────┘
Key commands:
- insmod: Insert module (basic loader, requires full path)
- rmmod: Remove module (must not be in use)
- lsmod: List loaded modules
- modprobe: Smart loader (resolves dependencies, searches standard paths)
- modinfo: Display module information
Example:
# Compile module
make
# Load module
sudo insmod superpipe.ko
# Verify loaded
lsmod | grep superpipe
# View module info
modinfo superpipe.ko
# Unload module
sudo rmmod superpipe
Kernel Space vs. User Space
Modern operating systems divide memory into two distinct regions with different privilege levels:
User Space:
- Where normal applications run
- Limited privileges
- Cannot access hardware directly
- Cannot access other processes' memory
- Errors cause process to crash (segmentation fault), not system crash
Kernel Space:
- Where kernel code runs (including your module!)
- Full privileges (can access all memory, all hardware)
- Can execute privileged CPU instructions
- Errors can crash the entire system (kernel panic)
┌─────────────────────────────────────────────┐
│ User Space │
│ │
│ [Process 1] [Process 2] [Process 3] ... │
│ │
│ Limited privileges │
│ Protected memory │
│ Crash = segfault (only that process dies) │
│ │
└─────────────────┬───────────────────────────┘
│ System Call Interface
│ (open, read, write, ioctl, ...)
┌─────────────────▼───────────────────────────┐
│ Kernel Space │
│ │
│ [Kernel Core] [Your Module] [Drivers] ... │
│ │
│ Full privileges │
│ Access to all memory │
│ Crash = kernel panic (entire system down) │
│ │
└─────────────────────────────────────────────┘
Critical differences for kernel programming:
| Aspect | User Space | Kernel Space |
|---|---|---|
| Memory allocation | malloc(), free()
|
kmalloc(), kfree()
|
| Printing | printf()
|
printk()
|
| Standard library | Full libc available | No libc! Only kernel functions |
| Floating point | Allowed | Forbidden (saves/restores FPU state manually) |
| Sleep/wait | Can block freely | Must use special functions |
| Error handling | Program crashes | System crashes |
Why these restrictions?
The kernel is responsible for managing all system resources and mediating between competing processes. It cannot rely on any userspace code or libraries because it IS the foundation that everything else relies on. Additionally, the kernel must be extremely efficient—it gets invoked millions of times per second.
Character Devices and the Miscdevice Framework
Device Files in /dev/
In Unix philosophy, "everything is a file." This includes hardware devices. The /dev/ directory contains special files that represent devices—when you read from or write to these files, you're actually communicating with hardware or kernel drivers.
Examine your system:
ls -l /dev/
You'll see entries like:
crw-rw-rw- 1 root root 1, 3 Dec 19 10:00 null crw-rw-rw- 1 root root 1, 5 Dec 19 10:00 zero crw-rw-rw- 1 root root 1, 8 Dec 19 10:00 random crw-rw-rw- 1 root root 1, 9 Dec 19 10:00 urandom brw-rw---- 1 root disk 8, 0 Dec 19 10:00 sda crw------- 1 root root 10, 58 Dec 19 10:00 hw_random
Decoding the first character:
c= character deviceb= block devicel= symbolic linkd= directory
Character Devices vs. Block Devices
Character devices (c):
- Data accessed as a stream of bytes
- No random access (typically)
- Examples: serial ports (
/dev/ttyS0), terminals (/dev/tty), random number generator (/dev/random) - Used for devices where data flows sequentially
Block devices (b):
- Data accessed in fixed-size blocks (typically 512 bytes, 1kB or 4kB)
- Random access supported
- Examples: solid-state drives (
/dev/nvme0n1), hard drives (/dev/sda), USB drives (/dev/sdb), loop devices - Used for storage devices with addressable blocks
Our superpipe device is a character device because it operates on byte streams without random access.
Major and Minor Numbers
Every device file has two numbers associated with it:
crw-rw-rw- 1 root root 1, 3 Dec 19 10:00 null
↑ ↑
Major Minor
Major number: Identifies the driver
- The kernel uses this to route operations to the correct driver
- Example: Major 1 = memory devices (
/dev/null,/dev/zero,/dev/random) - Example: Major 8 = SCSI disk devices (
/dev/sda,/dev/sdb)
Minor number: Identifies specific device instance within that driver
- Interpreted by the driver, not the kernel
- Example:
/dev/sda(8,0),/dev/sda1(8,1),/dev/sda2(8,2) - The SCSI driver uses minor number to distinguish between disks and partitions
The Miscdevice Framework
Creating a character device traditionally requires:
- Registering a major number with
register_chrdev() - Implementing a full character device structure
- Handling device creation in
/dev/(or relying on udev)
This is tedious for simple devices. The miscdevice framework simplifies this:
Benefits:
- Automatic major number: All miscdevices share major number 10
- Dynamic minor allocation: Kernel assigns minor numbers automatically
- Simplified registration: Single function call to register
- Automatic /dev/ creation: Device appears automatically when registered
Perfect for:
- Simple character devices
- Pseudo-devices (devices not tied to hardware)
- Control interfaces
The miscdevice structure:
#include <linux/miscdevice.h>
struct miscdevice {
int minor; /* Minor number (or MISC_DYNAMIC_MINOR) */
const char *name; /* Device name (creates /dev/<name>) */
const struct file_operations *fops; /* File operations */
/* ... other fields ... */
};
Using miscdevice:
static struct miscdevice my_device = {
.minor = MISC_DYNAMIC_MINOR, /* Kernel assigns minor number */
.name = "mydevice", /* Creates /dev/mydevice */
.fops = &my_fops, /* Your file operations */
};
/* In module init */
misc_register(&my_device); /* Register device */
/* In module exit */
misc_deregister(&my_device); /* Unregister device */
That's it! The device appears in /dev/mydevice and is ready to use.
File Operations: The Kernel Side of System Calls
The file_operations Structure
When userspace makes system calls like open(), read(), or write() on your device file, the kernel needs to know which functions in your module to call. This mapping is provided by the file_operations structure.
The structure (simplified):
#include <linux/fs.h>
struct file_operations {
struct module *owner; /* Pointer to module (prevents unload while in use) */
/* Open/close */
int (*open)(struct inode *, struct file *);
int (*release)(struct inode *, struct file *);
/* Read/write */
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
/* ioctl */
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
/* ... many more operations (lseek, poll, mmap, etc.) ... */
};
Example:
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
};
You implement only the operations your device supports. Operations you don't implement default to appropriate behavior (typically returning -EINVAL or -ENOSYS).
Understanding the Handlers
open handler:
Called when userspace opens your device file.
static int my_open(struct inode *inode, struct file *file)
{
/* Allocate per-file state, increment usage counters, etc. */
printk(KERN_INFO "Device opened\n");
return 0; /* Success */
}
Parameters:
inode: Represents the device file on disk (contains major/minor numbers)file: Represents an open file descriptor (contains file offset, flags, private_data)
release handler:
Called when userspace closes your device file.
static int my_release(struct inode *inode, struct file *file)
{
/* Free resources, decrement counters, etc. */
printk(KERN_INFO "Device closed\n");
return 0;
}
Note: Called "release" not "close" because multiple file descriptors can point to the same open file (via dup() or fork()). Release is called when the last reference is closed.
read handler:
Called when userspace reads from your device.
static ssize_t my_read(struct file *file, char __user *buffer,
size_t count, loff_t *offset)
{
/* Copy data from kernel space to userspace buffer */
/* Return number of bytes read, 0 for EOF, negative for error */
}
Parameters:
file: The open filebuffer: Userspace buffer (marked with__userannotation)count: Number of bytes requestedoffset: File offset (can be updated)
Return value:
- Positive: Number of bytes successfully read
- 0: End of file
- Negative: Error code
write handler:
Called when userspace writes to your device.
static ssize_t my_write(struct file *file, const char __user *buffer,
size_t count, loff_t *offset)
{
/* Copy data from userspace buffer to kernel space */
/* Return number of bytes written, negative for error */
}
unlocked_ioctl handler:
Called when userspace invokes ioctl() for device-specific operations.
static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
/* Handle device-specific commands */
/* 'arg' can be a value or a pointer to userspace memory */
}
We'll explore ioctl in detail in section 4.6.
Moving Data Between Kernel and User Space
Why We Can't Access User Memory Directly
This is one of the most critical concepts in kernel programming and a common source of bugs for beginners.
The problem:
In userspace, when you have a pointer, you can just dereference it:
char *str = "hello";
printf("%c", str[0]); /* Works fine in userspace */
In kernel space, you cannot do this with pointers to userspace memory:
/* WRONG - will crash the kernel! */
static ssize_t my_read(struct file *file, char __user *user_buffer, ...)
{
user_buffer[0] = 'A'; /* NEVER DO THIS! */
strcpy(user_buffer, kernel_data); /* NEVER DO THIS! */
}
Why not?
- Memory protection: User memory might not be mapped, might be swapped to disk, or might not exist at all (bad pointer)
- Page faults: Accessing user memory can cause page faults, which need special handling in kernel context
- Security: The kernel must validate that the user actually has permission to access that memory
- MMU (Memory Management Unit): The CPU's MMU enforces that kernel code cannot directly access user address space
What happens if you try?
In the best case, you get a kernel oops (non-fatal kernel error). In the worst case, you corrupt memory and cause a kernel panic (crash).
The Safe Way: copy_to_user and copy_from_user
The kernel provides safe functions for transferring data between kernel and user space:
copy_to_user: Copy from kernel to userspace
#include <linux/uaccess.h>
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
Parameters:
to: Destination in userspace (marked with__user)from: Source in kernel spacen: Number of bytes to copy
Return value:
- 0: Success (all bytes copied)
- Non-zero: Number of bytes that could NOT be copied (failure)
Example:
char kernel_buffer[100] = "Hello from kernel";
if (copy_to_user(user_buffer, kernel_buffer, strlen(kernel_buffer))) {
return -EFAULT; /* Bad address - tell userspace */
}
copy_from_user: Copy from userspace to kernel
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
Parameters:
to: Destination in kernel spacefrom: Source in userspace (marked with__user)n: Number of bytes to copy
Return value:
- 0: Success
- Non-zero: Number of bytes that could NOT be copied
Example:
char kernel_buffer[100];
if (copy_from_user(kernel_buffer, user_buffer, count)) {
return -EFAULT; /* Bad address */
}
What these functions do:
- Check that the user address is valid
- Check that the user has permission to access that memory
- Handle page faults if the memory is swapped out
- Actually perform the copy
- Return success/failure
The __user annotation:
The __user marker is a sparse (static analysis tool) annotation that helps catch bugs at compile time. It doesn't affect the generated code, but it helps ensure you're using the right functions.
Error Codes: Communicating Failures
When something goes wrong in your kernel module, you communicate this to userspace by returning negative error codes.
Common error codes:
| Error Code | Value | Meaning | When to use |
|---|---|---|---|
-EFAULT
|
-14 | Bad address | copy_to_user/copy_from_user failed |
-EINVAL
|
-22 | Invalid argument | Invalid ioctl command, bad parameter |
-ENOMEM
|
-12 | Out of memory | kmalloc() failed |
-ENOSPC
|
-28 | No space left | Device buffer full |
-ENODEV
|
-19 | No such device | Device not found/initialized |
-EBUSY
|
-16 | Device busy | Resource in use |
-EIO
|
-5 | I/O error | Hardware error |
How to use:
if (!buffer) {
return -ENOMEM; /* Allocation failed */
}
if (copy_from_user(buffer, user_ptr, len)) {
return -EFAULT; /* Bad pointer from userspace */
}
if (cmd != VALID_COMMAND) {
return -EINVAL; /* Invalid command */
}
Userspace sees these as errno values:
/* Userspace code */
if (write(fd, data, len) < 0) {
perror("write"); /* Prints "write: Bad address" for -EFAULT */
}
The Concurrency Problem
Your kernel module can be accessed by multiple processes simultaneously. Consider this scenario:
Time Process A Process B
---- --------------------- ---------------------
1 open("/dev/superpipe")
2 write("hello") open("/dev/superpipe")
3 [writing to buffer] write("world")
4 [writing to buffer] [writing to buffer] ← RACE!
5 [writing to buffer] ← DATA CORRUPTION!
Without synchronization, both processes could write to the buffer simultaneously, resulting in garbled data, corrupted state, or kernel crashes.
Types of concurrency:
- Multiple processes: Different processes calling your module
- Multiple threads: Threads in the same process
- Interrupts: Interrupt handlers can preempt your code
- Multiple CPUs: Code running simultaneously on different CPU cores
The shared state problem:
/* Global device state - shared by all processes */
static struct {
char buffer[4096];
size_t len;
} dev_state;
/* UNSAFE - race condition! */
static ssize_t unsafe_write(...) {
/* What if another process modifies dev_state.len right here? */
memcpy(&dev_state.buffer[dev_state.len], data, count);
dev_state.len += count; /* Race! Another write could have happened */
}
Mutexes in the Kernel
A mutex (mutual exclusion lock) ensures that only one thread can access a critical section at a time.
Declaring a mutex:
#include <linux/mutex.h>
static DEFINE_MUTEX(my_mutex); /* Declares and initializes */
/* Or for dynamic initialization: */
struct mutex my_mutex;
mutex_init(&my_mutex);
Using a mutex:
/* Acquire lock - will sleep if already held */
mutex_lock(&my_mutex);
/* Critical section - only one thread at a time */
/* Access shared data here */
/* Release lock */
mutex_unlock(&my_mutex);
Interruptible locking:
In syscall handlers (read, write, ioctl), use the interruptible version so users can interrupt with Ctrl+C:
/* Try to acquire lock, but allow signals to interrupt */
if (mutex_lock_interruptible(&my_mutex)) {
return -ERESTARTSYS; /* System call was interrupted */
}
/* Critical section */
mutex_unlock(&my_mutex);
Safe write example:
static DEFINE_MUTEX(device_mutex);
static ssize_t safe_write(struct file *file, const char __user *user_buffer,
size_t count, loff_t *offset)
{
/* Acquire lock */
if (mutex_lock_interruptible(&device_mutex))
return -ERESTARTSYS;
/* Critical section - safe from concurrent access */
if (dev_state.len + count > BUFFER_SIZE) {
mutex_unlock(&device_mutex);
return -ENOSPC;
}
if (copy_from_user(&dev_state.buffer[dev_state.len], user_buffer, count)) {
mutex_unlock(&device_mutex);
return -EFAULT;
}
dev_state.len += count;
/* Release lock */
mutex_unlock(&device_mutex);
return count;
}
Rules for mutex usage:
- Always release: Every lock must have a corresponding unlock (even on error paths)
- Short critical sections: Hold locks for the minimum time necessary
- No sleeping with spinlocks: Don't use mutexes in interrupt context (use spinlocks instead)
- Avoid deadlocks: Don't acquire multiple locks, or always acquire in the same order
The ioctl Interface: Device-Specific Commands
What is ioctl?
The standard file operations (read, write) are generic—they work the same way across all devices. But sometimes you need device-specific operations that don't fit the read/write model:
- Configure a serial port's baud rate
- Set video resolution on a graphics device
- Enable a network interface
- Set transformation mode on our superpipe device
This is what ioctl() (input/output control) is for: device-specific commands that go beyond simple data transfer.
Userspace signature:
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
Parameters:
fd: File descriptorrequest: Command code (defined by driver)...: Optional argument (pointer or value)
Example usage:
int fd = open("/dev/superpipe", O_RDWR);
/* Simple command with no data */
ioctl(fd, SUPERPIPE_IOC_RESET);
/* Command that passes a value */
int mode = TRANSFORM_UPPER;
ioctl(fd, SUPERPIPE_IOC_SET_MODE, mode);
/* Command that passes a structure */
struct superpipe_config cfg = { .mode = TRANSFORM_UPPER };
ioctl(fd, SUPERPIPE_IOC_SET_CONFIG, &cfg);
Defining ioctl Commands
ioctl commands are unsigned integers, but they're not arbitrary—they're constructed using macros that encode information about the command.
Command encoding:
Direction | Size of data | Magic | Sequence ____________|______________|_______|__________ | 2 bits | 14 bits | 8 bits | 8 bits | ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
The macros:
#include <linux/ioctl.h>
_IO(magic, number) /* No data transfer */
_IOR(magic, number, datatype) /* Read from device (copy_to_user) */
_IOW(magic, number, datatype) /* Write to device (copy_from_user) */
_IOWR(magic, number, datatype) /* Both read and write */
Parameters:
magic: 8-bit magic number (identifies your driver, typically a character)number: Command number (sequential numbering)datatype: Type of data being transferred (for size encoding)
Example:
#define SUPERPIPE_IOC_MAGIC 'k'
/* No data transfer */
#define SUPERPIPE_IOC_RESET _IO(SUPERPIPE_IOC_MAGIC, 0)
/* Write an integer to device */
#define SUPERPIPE_IOC_SET_MODE _IOW(SUPERPIPE_IOC_MAGIC, 1, int)
/* Read an integer from device */
#define SUPERPIPE_IOC_GET_MODE _IOR(SUPERPIPE_IOC_MAGIC, 2, int)
/* Read and write a structure */
#define SUPERPIPE_IOC_CONFIG _IOWR(SUPERPIPE_IOC_MAGIC, 3, struct superpipe_config)
Why use macros instead of simple numbers?
- Type safety: Encodes the size of the data being transferred
- Direction: Indicates whether data flows to/from device
- Collision avoidance: Magic number helps avoid conflicts with other drivers
- Self-documenting: The macro tells you what the command does
Passing Structures with ioctl
For complex configuration, passing structures is more flexible than passing simple values.
Define the structure (in UAPI header, shared between kernel and userspace):
/* superpipe_uapi.h */
struct superpipe_ioctl_config {
__u32 mode; /* Transformation mode */
__u32 flags; /* Additional flags (for future expansion) */
};
Note: Use __u32, __u64, etc. (double underscore types) in UAPI headers. These are guaranteed to have the same size in both 32-bit and 64-bit architectures.
Define the ioctl command:
#define SUPERPIPE_IOC_SET_CONFIG _IOW(SUPERPIPE_IOC_MAGIC, 1, struct superpipe_ioctl_config)
#define SUPERPIPE_IOC_GET_CONFIG _IOR(SUPERPIPE_IOC_MAGIC, 2, struct superpipe_ioctl_config)
Userspace usage:
#include "superpipe_uapi.h"
int fd = open("/dev/superpipe", O_RDWR);
struct superpipe_ioctl_config cfg = {
.mode = TRANSFORM_UPPER,
.flags = 0,
};
if (ioctl(fd, SUPERPIPE_IOC_SET_CONFIG, &cfg) < 0) {
perror("ioctl");
}
Kernel implementation:
static long superpipe_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct superpipe_ioctl_config cfg;
switch (cmd) {
case SUPERPIPE_IOC_SET_CONFIG:
/* Copy structure from userspace */
if (copy_from_user(&cfg, (void __user *)arg, sizeof(cfg)))
return -EFAULT;
/* Validate */
if (cfg.mode >= TRANSFORM_MAX)
return -EINVAL;
/* Apply configuration */
device_state.mode = cfg.mode;
return 0;
case SUPERPIPE_IOC_GET_CONFIG:
/* Prepare structure */
cfg.mode = device_state.mode;
cfg.flags = 0;
/* Copy to userspace */
if (copy_to_user((void __user *)arg, &cfg, sizeof(cfg)))
return -EFAULT;
return 0;
default:
return -EINVAL; /* Unknown command */
}
}
Key points:
- arg is a userspace pointer: Must use copy_from_user/copy_to_user
- Validate inputs: Check that values are in valid ranges
- Return 0 on success: Negative error code on failure
- Handle unknown commands: Return -EINVAL for commands you don't recognize
The UAPI Header: Sharing Definitions
What is UAPI?
UAPI stands for "User API"—definitions that are shared between kernel code and userspace code. In the Linux kernel source tree, these headers live in include/uapi/linux/.
Why separate UAPI headers?
- Single source of truth: Define ioctl commands, structures, constants once
- Consistency: Kernel and userspace always agree on command values and structure layouts
- Stability: UAPI is part of the kernel's stable ABI—once defined, it shouldn't change (backward compatibility)
- Clear boundary: Clearly separates internal kernel definitions from external interface
What goes in a UAPI header?
- ioctl command definitions
- Structure definitions for data passed between kernel and userspace
- Constant definitions (modes, flags, limits)
- Enum definitions used in the interface
What does NOT go in a UAPI header?
- Internal kernel structures
- Function declarations
- Kernel-only constants
Our UAPI header structure:
/* superpipe_uapi.h - Shared between kernel and userspace */
#ifndef _SUPERPIPE_UAPI_H
#define _SUPERPIPE_UAPI_H
#include <linux/ioctl.h>
#include <linux/types.h> /* For __u32, __u64, etc. */
/* Transform modes */
#define SUPERPIPE_MODE_NONE 0
#define SUPERPIPE_MODE_UPPER 1
#define SUPERPIPE_MODE_LOWER 2
#define SUPERPIPE_MODE_DROP_VOWELS 3
#define SUPERPIPE_MODE_DROP_SPACES 4
/* Configuration structure */
struct superpipe_ioctl_config {
__u32 mode; /* One of SUPERPIPE_MODE_* */
__u32 flags; /* Reserved for future use */
};
/* ioctl commands */
#define SUPERPIPE_IOC_MAGIC 'k'
#define SUPERPIPE_IOC_SET_CONFIG _IOW(SUPERPIPE_IOC_MAGIC, 1, struct superpipe_ioctl_config)
#define SUPERPIPE_IOC_GET_CONFIG _IOR(SUPERPIPE_IOC_MAGIC, 2, struct superpipe_ioctl_config)
#define SUPERPIPE_IOC_RESET _IO(SUPERPIPE_IOC_MAGIC, 3)
#endif /* _SUPERPIPE_UAPI_H */
Using in kernel code:
/* superpipe.c */
#include "superpipe_uapi.h"
static long superpipe_ioctl(...)
{
struct superpipe_ioctl_config cfg;
switch (cmd) {
case SUPERPIPE_IOC_SET_CONFIG:
/* Use the definitions from UAPI header */
if (copy_from_user(&cfg, ...))
return -EFAULT;
if (cfg.mode == SUPERPIPE_MODE_UPPER) {
/* ... */
}
break;
}
}
Using in userspace code:
/* test_superpipe.c */
#include "superpipe_uapi.h" /* Same header! */
int main()
{
struct superpipe_ioctl_config cfg = {
.mode = SUPERPIPE_MODE_UPPER,
.flags = 0,
};
ioctl(fd, SUPERPIPE_IOC_SET_CONFIG, &cfg);
}
Benefits:
- Change command codes in one place
- Add new modes without editing two files
- Compiler catches mismatches automatically
- Standard Linux kernel practice
Design: The Superpipe Device
Device Behavior
The superpipe device implements a buffered pipe with configurable byte transformations. Here's the complete behavior specification:
Basic operations:
- Write: Userspace writes data → Kernel applies transformation → Stores in buffer
- Read: Kernel copies data from buffer → Returns to userspace → Removes from buffer (consuming read)
- ioctl: Userspace sends configuration → Kernel updates transformation mode
Buffer properties:
- Size: 4096 bytes (4KB)
- Type: Circular/linear buffer (your choice)
- Behavior when full: write() returns -ENOSPC
- Behavior when empty: read() returns 0 (EOF)
Transformation timing:
Data is transformed during write, not during read.
Example flow:
Time Action Buffer State Mode
---- ------------------------------ ----------------- -----------
1 ioctl(SET_CONFIG, NONE) [] NONE
2 write("Hello") ["Hello"] NONE
3 read() → "Hello" [] NONE
4 ioctl(SET_CONFIG, UPPER) [] UPPER
5 write("world") ["WORLD"] UPPER
6 read() → "WORLD" [] UPPER
7 ioctl(SET_CONFIG, NONE) [] NONE
8 write("test") ["test"] NONE
9 ioctl(SET_CONFIG, UPPER) ["test"] UPPER (mode change doesn't affect existing data!)
10 write("NEW") ["testNEW"] UPPER
11 read() → "testNEW" [] UPPER
Key insight from example:
At time 9, we change mode to UPPER, but the existing data "test" stays lowercase. Only NEW data (written after the mode change) gets transformed. This is a consequence of transform-on-write: once data is in the buffer, its transformation is "baked in."
Implementation:
/* Pseudo-code for transform-on-write */
static ssize_t superpipe_write(...)
{
char *write_position;
size_t final_len;
/* Copy from userspace to buffer */
write_position = buffer + buffer_len;
copy_from_user(write_position, user_data, count);
/* Transform in-place immediately */
final_len = count; /* Start with bytes copied */
switch (mode) {
case TRANSFORM_UPPER:
transform_upper(write_position, count);
/* final_len stays count - no compaction */
break;
case TRANSFORM_DROP_VOWELS:
final_len = transform_drop_vowels(write_position, count);
/* final_len may be less than count - data compacted */
break;
}
/* Update buffer length by actual stored bytes */
buffer_len += final_len;
/* Return bytes consumed from user, not bytes stored */
return count; /* Standard UNIX semantics */
}
Note for filtering transforms: When dropping characters (vowels, spaces), the amount of data stored may be less than the amount written. The write() call returns the actual number of bytes stored after filtering.
Device State Structure
All device state is stored in a single global structure:
/* Transform modes - internal to kernel */
enum transform_mode {
TRANSFORM_NONE = 0,
TRANSFORM_UPPER,
TRANSFORM_LOWER,
TRANSFORM_DROP_VOWELS,
TRANSFORM_DROP_SPACES,
TRANSFORM_MAX /* Sentinel for validation */
};
/* Device state */
struct superpipe_device {
char *buffer; /* Kernel buffer (allocated with kmalloc) */
size_t buffer_len; /* Current amount of data in buffer */
struct mutex lock; /* Protects buffer and mode from concurrent access */
enum transform_mode mode; /* Current transformation mode */
};
static struct superpipe_device dev_state;
Design notes:
Global state: We use a single global structure because we have exactly one device instance (/dev/superpipe). All opens share the same buffer and transformation mode.
Alternative designs:
- Per-file state: Each open() could have independent buffer and mode (use file->private_data)
- Embedded miscdevice: Embed miscdevice in device structure, use container_of() to access
For this lab, global state is simpler and clearer for learning.
Initialization:
static int __init superpipe_init(void)
{
/* Allocate buffer */
dev_state.buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
if (!dev_state.buffer)
return -ENOMEM;
/* Initialize state */
dev_state.buffer_len = 0;
dev_state.mode = TRANSFORM_NONE;
mutex_init(&dev_state.lock);
/* Register device */
return misc_register(&superpipe_miscdev);
}
Cleanup:
static void __exit superpipe_exit(void)
{
misc_deregister(&superpipe_miscdev);
kfree(dev_state.buffer);
}
Reference Implementation: Superpipe with Uppercase Transform
In this section, we present a complete, working kernel module that implements the superpipe device with support for two transformation modes: NONE (pass-through) and UPPER (convert to uppercase). This reference implementation demonstrates all the concepts covered in the theoretical background.
Study this code carefully. The exercise at the end asks you to extend it to support additional transformations.
UAPI Header: superpipe_uapi.h
This header is shared between kernel code and userspace programs.
/* superpipe_uapi.h - User API definitions for superpipe device */
#ifndef _SUPERPIPE_UAPI_H
#define _SUPERPIPE_UAPI_H
#include <linux/ioctl.h>
#include <linux/types.h>
/*
* Transform modes
* These constants identify which transformation to apply
*/
#define SUPERPIPE_MODE_NONE 0 /* Pass through unchanged */
#define SUPERPIPE_MODE_UPPER 1 /* Convert to uppercase */
/*
* Configuration structure
* Passed between userspace and kernel via ioctl
*/
struct superpipe_ioctl_config {
__u32 mode; /* One of SUPERPIPE_MODE_* constants */
__u32 flags; /* Reserved for future use (set to 0) */
};
/*
* ioctl commands
* Magic number 'k' chosen arbitrarily (could be any character)
*/
#define SUPERPIPE_IOC_MAGIC 'k'
/* Set transformation configuration (write config to device) */
#define SUPERPIPE_IOC_SET_CONFIG _IOW(SUPERPIPE_IOC_MAGIC, 1, struct superpipe_ioctl_config)
/* Get current configuration (read config from device) */
#define SUPERPIPE_IOC_GET_CONFIG _IOR(SUPERPIPE_IOC_MAGIC, 2, struct superpipe_ioctl_config)
/* Reset device (clear buffer) */
#define SUPERPIPE_IOC_RESET _IO(SUPERPIPE_IOC_MAGIC, 3)
#endif /* _SUPERPIPE_UAPI_H */
Kernel Module: superpipe_ref.c
This is the complete kernel module implementation.
/* superpipe_ref.c - Reference implementation of superpipe device */
#include <linux/module.h> /* Core module functionality */
#include <linux/kernel.h> /* Kernel types and macros */
#include <linux/init.h> /* module_init, module_exit */
#include <linux/miscdevice.h> /* Miscdevice framework */
#include <linux/fs.h> /* File operations structure */
#include <linux/uaccess.h> /* copy_to_user, copy_from_user */
#include <linux/mutex.h> /* Mutex synchronization */
#include <linux/slab.h> /* kmalloc, kfree */
#include <linux/ctype.h> /* toupper, tolower */
#include "superpipe_uapi.h" /* Shared UAPI definitions */
#define DEVICE_NAME "superpipe"
#define BUFFER_SIZE 4096
/*
* Internal transform modes
* Maps to UAPI constants but kept internal to module
*/
enum transform_mode {
TRANSFORM_NONE = SUPERPIPE_MODE_NONE,
TRANSFORM_UPPER = SUPERPIPE_MODE_UPPER,
TRANSFORM_MAX /* Sentinel for validation */
};
/*
* Device state structure
* All state is stored here (single global instance)
*/
struct superpipe_device {
char *buffer; /* Dynamically allocated kernel buffer */
size_t buffer_len; /* Current amount of data in buffer */
struct mutex lock; /* Protects all fields from concurrent access */
enum transform_mode mode; /* Current transformation mode */
};
/* Global device state (single instance for one device) */
static struct superpipe_device dev_state;
/*
* transform_upper - Apply uppercase transformation in-place
* @data: Pointer to data to transform
* @len: Length of data in bytes
*
* Converts all ASCII lowercase letters (a-z) to uppercase (A-Z).
* Other characters pass through unchanged.
*/
static void transform_upper(char *data, size_t len)
{
size_t i;
for (i = 0; i < len; i++) {
if (islower(data[i])) {
data[i] = toupper(data[i]);
}
}
}
/*
* superpipe_open - Handle device open
* @inode: Inode representing the device file
* @file: File structure for this open
*
* Called when userspace opens /dev/superpipe.
* We don't need to do anything special, just log the event.
*
* Return: 0 on success
*/
static int superpipe_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "superpipe: Device opened by process %d (%s)\n",
current->pid, current->comm);
return 0;
}
/*
* superpipe_release - Handle device close
* @inode: Inode representing the device file
* @file: File structure being closed
*
* Called when userspace closes /dev/superpipe.
* Cleanup would go here if we allocated per-file resources.
*
* Return: 0 on success
*/
static int superpipe_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "superpipe: Device closed by process %d (%s)\n",
current->pid, current->comm);
return 0;
}
/*
* superpipe_read - Handle read operation
* @file: File structure
* @user_buffer: Userspace buffer to copy data into
* @count: Number of bytes requested
* @offset: File offset (unused for character devices)
*
* Copies data from kernel buffer to userspace.
* Data is removed from buffer after reading (consuming read).
* With transform-on-write, data is already transformed, so we just copy.
*
* Return: Number of bytes read, 0 for EOF, negative error code on failure
*/
static ssize_t superpipe_read(struct file *file, char __user *user_buffer,
size_t count, loff_t *offset)
{
size_t to_copy;
/* Acquire lock - interruptible so Ctrl+C works */
if (mutex_lock_interruptible(&dev_state.lock))
return -ERESTARTSYS; /* Interrupted by signal */
/* Check if there's data available */
if (dev_state.buffer_len == 0) {
mutex_unlock(&dev_state.lock);
return 0; /* EOF - no data available */
}
/* Determine how much to copy: minimum of requested and available */
to_copy = (count < dev_state.buffer_len) ? count : dev_state.buffer_len;
/* Copy data to userspace - data is already transformed */
if (copy_to_user(user_buffer, dev_state.buffer, to_copy)) {
mutex_unlock(&dev_state.lock);
return -EFAULT; /* Bad userspace address */
}
/*
* Remove copied data from buffer by shifting remaining data forward
* There are more efficient ways to do this (eg. ring buffers)
*/
dev_state.buffer_len -= to_copy;
if (dev_state.buffer_len > 0) {
memmove(dev_state.buffer, dev_state.buffer + to_copy,
dev_state.buffer_len);
}
mutex_unlock(&dev_state.lock);
printk(KERN_INFO "superpipe: Read %zu bytes\n", to_copy);
return to_copy;
}
/*
* superpipe_write - Handle write operation
* @file: File structure
* @user_buffer: Userspace buffer containing data to write
* @count: Number of bytes to write
* @offset: File offset (unused for character devices)
*
* Copies data from userspace to kernel buffer, applying transformation.
* This is where transform-on-write happens: data is transformed as it
* enters the buffer, not when it's read out.
*
* Return: Number of bytes written, negative error code on failure
*/
static ssize_t superpipe_write(struct file *file,
const char __user *user_buffer,
size_t count, loff_t *offset)
{
size_t space_available;
size_t to_write;
char *write_position;
/* Acquire lock */
if (mutex_lock_interruptible(&dev_state.lock))
return -ERESTARTSYS;
/* Check available space in buffer */
space_available = BUFFER_SIZE - dev_state.buffer_len;
if (space_available == 0) {
mutex_unlock(&dev_state.lock);
return -ENOSPC; /* Buffer full - no space left */
}
/* Determine how much we can actually write */
to_write = (count < space_available) ? count : space_available;
/* Calculate where to write in buffer */
write_position = dev_state.buffer + dev_state.buffer_len;
/* Copy from userspace to kernel buffer */
if (copy_from_user(write_position, user_buffer, to_write)) {
mutex_unlock(&dev_state.lock);
return -EFAULT; /* Bad userspace address */
}
/*
* TRANSFORM ON WRITE
* Apply transformation to the newly written data in-place
* Data in buffer is always in its final transformed state
*/
switch (dev_state.mode) {
case TRANSFORM_UPPER:
transform_upper(write_position, to_write);
break;
case TRANSFORM_NONE:
/* No transformation - data already copied as-is */
break;
default:
/* Should never happen if validation is correct */
printk(KERN_WARNING "superpipe: Invalid mode %d\n", dev_state.mode);
break;
}
/* Update buffer length - for this reference, same as to_write */
dev_state.buffer_len += to_write;
mutex_unlock(&dev_state.lock);
printk(KERN_INFO "superpipe: Wrote %zu bytes (mode=%d)\n",
to_write, dev_state.mode);
/* Return bytes consumed from user buffer */
return to_write;
}
/*
* superpipe_ioctl - Handle ioctl commands
* @file: File structure
* @cmd: ioctl command code
* @arg: ioctl argument (pointer or value, depends on command)
*
* Handles device-specific configuration commands.
* Commands are defined in superpipe_uapi.h
*
* Return: 0 on success, negative error code on failure
*/
static long superpipe_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
struct superpipe_ioctl_config cfg;
int ret = 0;
/* Acquire lock */
if (mutex_lock_interruptible(&dev_state.lock))
return -ERESTARTSYS;
switch (cmd) {
case SUPERPIPE_IOC_SET_CONFIG:
/* Copy configuration structure from userspace */
if (copy_from_user(&cfg, (void __user *)arg, sizeof(cfg))) {
ret = -EFAULT;
break;
}
/* Validate mode */
if (cfg.mode >= TRANSFORM_MAX) {
printk(KERN_WARNING "superpipe: Invalid mode %u\n", cfg.mode);
ret = -EINVAL;
break;
}
/* Apply configuration */
dev_state.mode = cfg.mode;
printk(KERN_INFO "superpipe: Mode set to %u\n", cfg.mode);
break;
case SUPERPIPE_IOC_GET_CONFIG:
/* Prepare configuration structure */
cfg.mode = dev_state.mode;
cfg.flags = 0;
/* Copy to userspace */
if (copy_to_user((void __user *)arg, &cfg, sizeof(cfg))) {
ret = -EFAULT;
break;
}
printk(KERN_INFO "superpipe: Returned config (mode=%u)\n", cfg.mode);
break;
case SUPERPIPE_IOC_RESET:
/* Clear buffer */
dev_state.buffer_len = 0;
printk(KERN_INFO "superpipe: Buffer reset\n");
break;
default:
/* Unknown command */
printk(KERN_WARNING "superpipe: Unknown ioctl command: 0x%x\n", cmd);
ret = -EINVAL;
break;
}
mutex_unlock(&dev_state.lock);
return ret;
}
/*
* File operations structure
* Maps system calls to our handler functions
*/
static const struct file_operations superpipe_fops = {
.owner = THIS_MODULE,
.open = superpipe_open,
.release = superpipe_release,
.read = superpipe_read,
.write = superpipe_write,
.unlocked_ioctl = superpipe_ioctl,
};
/*
* Miscdevice structure
* Defines our character device
*/
static struct miscdevice superpipe_miscdev = {
.minor = MISC_DYNAMIC_MINOR, /* Kernel assigns minor number automatically */
.name = DEVICE_NAME, /* Creates /dev/superpipe */
.fops = &superpipe_fops, /* Our file operations */
.mode = 0666, /* Device permissions (rw-rw-rw-) */
};
/*
* superpipe_init - Module initialization
*
* Called when module is loaded with insmod.
* Allocates resources and registers the device.
*
* Return: 0 on success, negative error code on failure
*/
static int __init superpipe_init(void)
{
int ret;
/* Allocate kernel buffer (4KB) */
dev_state.buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
if (!dev_state.buffer) {
printk(KERN_ERR "superpipe: Failed to allocate buffer\n");
return -ENOMEM;
}
/* Initialize device state */
dev_state.buffer_len = 0;
dev_state.mode = TRANSFORM_NONE; /* Start in pass-through mode */
mutex_init(&dev_state.lock);
/* Register miscdevice - creates /dev/superpipe */
ret = misc_register(&superpipe_miscdev);
if (ret) {
printk(KERN_ERR "superpipe: Failed to register device (error %d)\n", ret);
kfree(dev_state.buffer);
return ret;
}
printk(KERN_INFO "superpipe: Module loaded successfully\n");
printk(KERN_INFO "superpipe: Device created at /dev/%s\n", DEVICE_NAME);
printk(KERN_INFO "superpipe: Buffer size: %d bytes\n", BUFFER_SIZE);
return 0;
}
/*
* superpipe_exit - Module cleanup
*
* Called when module is unloaded with rmmod.
* Unregisters device and frees resources.
*/
static void __exit superpipe_exit(void)
{
/* Unregister device - removes /dev/superpipe */
misc_deregister(&superpipe_miscdev);
/* Free buffer */
kfree(dev_state.buffer);
printk(KERN_INFO "superpipe: Module unloaded\n");
}
/* Register initialization and cleanup functions */
module_init(superpipe_init);
module_exit(superpipe_exit);
/* Module metadata */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <your.email@example.com>");
MODULE_DESCRIPTION("Superpipe character device with configurable transformations");
MODULE_VERSION("1.0");
Understanding the Reference Implementation
Let's walk through the key components of this implementation.
Module Initialization
static int __init superpipe_init(void)
{
/* 1. Allocate buffer */
dev_state.buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
/* 2. Initialize state */
dev_state.buffer_len = 0;
dev_state.mode = TRANSFORM_NONE;
mutex_init(&dev_state.lock);
/* 3. Register device */
ret = misc_register(&superpipe_miscdev);
}
Step-by-step:
- Allocate buffer:
kmalloc()is the kernel equivalent ofmalloc().GFP_KERNELmeans "can sleep, normal priority allocation." - Initialize state: Set buffer empty, mode to pass-through, initialize mutex
- Register device: This creates
/dev/superpipeand makes it available
If anything fails: Clean up already-allocated resources before returning error code.
The Open Handler
static int superpipe_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "superpipe: Device opened by process %d (%s)\n",
current->pid, current->comm);
return 0;
}
What happens here:
currentis a kernel global pointing to the current process's task_structcurrent->pidis the process IDcurrent->commis the process name (from argv[0])- We don't need to allocate per-file resources, so just log and return success
The Write Handler: Transform-on-Write
static ssize_t superpipe_write(...)
{
/* 1. Acquire lock */
mutex_lock_interruptible(&dev_state.lock);
/* 2. Check space */
if (space_available == 0)
return -ENOSPC;
/* 3. Copy from user */
write_position = dev_state.buffer + dev_state.buffer_len;
copy_from_user(write_position, user_buffer, to_write);
/* 4. TRANSFORM IN-PLACE */
switch (dev_state.mode) {
case TRANSFORM_UPPER:
transform_upper(write_position, to_write);
break;
}
/* 5. Update length */
dev_state.buffer_len += to_write;
/* 6. Release lock */
mutex_unlock(&dev_state.lock);
return to_write;
}
Key points:
- Data is transformed immediately after being copied from userspace
- The transformation happens on
write_position(the buffer location), not a temporary copy - Once stored, data is in its final transformed state
The Read Handler: Simple Copy
static ssize_t superpipe_read(...)
{
/* 1. Check for data */
if (dev_state.buffer_len == 0)
return 0; /* EOF */
/* 2. Copy to user - data already transformed */
copy_to_user(user_buffer, dev_state.buffer, to_copy);
/* 3. Remove consumed data */
dev_state.buffer_len -= to_copy;
memmove(dev_state.buffer, dev_state.buffer + to_copy,
dev_state.buffer_len);
return to_copy;
}
Why it's simple:
- No transformation needed—data is already in final form
- Just copy to userspace and remove from buffer
memmove()shifts remaining data to the beginning
The ioctl Handler: Configuration with Structs
static long superpipe_ioctl(...)
{
struct superpipe_ioctl_config cfg;
switch (cmd) {
case SUPERPIPE_IOC_SET_CONFIG:
/* 1. Copy struct from userspace */
if (copy_from_user(&cfg, (void __user *)arg, sizeof(cfg)))
return -EFAULT;
/* 2. Validate */
if (cfg.mode >= TRANSFORM_MAX)
return -EINVAL;
/* 3. Apply */
dev_state.mode = cfg.mode;
break;
case SUPERPIPE_IOC_GET_CONFIG:
/* 1. Prepare struct */
cfg.mode = dev_state.mode;
cfg.flags = 0;
/* 2. Copy to userspace */
if (copy_to_user((void __user *)arg, &cfg, sizeof(cfg)))
return -EFAULT;
break;
}
return 0;
}
Pattern for struct-based ioctl:
- For SET (write to device):
copy_from_user→ validate → apply - For GET (read from device): prepare →
copy_to_user - Always validate inputs before applying
Module Cleanup
static void __exit superpipe_exit(void)
{
misc_deregister(&superpipe_miscdev); /* Remove device */
kfree(dev_state.buffer); /* Free buffer */
}
Cleanup must:
- Undo everything done in init
- Be safe to call even if init partially failed (though our init handles that)
- Not leak resources
Compiling and Testing the Reference Implementation
Step 1: Compile the Module
Create a Makefile:
obj-m += superpipe_ref.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Compile:
make
Expected output:
make -C /lib/modules/6.5.0-generic/build M=/home/user/lab11 modules make[1]: Entering directory '/usr/src/linux-headers-6.5.0-generic' CC [M] /home/user/lab11/superpipe_ref.o MODPOST /home/user/lab11/Module.symvers CC [M] /home/user/lab11/superpipe_ref.mod.o LD [M] /home/user/lab11/superpipe_ref.ko make[1]: Leaving directory '/usr/src/linux-headers-6.5.0-generic'
You now have superpipe_ref.ko (kernel object file).
Step 2: Load the Module
Load the module:
sudo insmod superpipe_ref.ko
Verify it loaded:
lsmod | grep superpipe
Expected output:
superpipe_ref 16384 0
The "0" means no one is currently using it.
Check kernel messages:
sudo dmesg | tail -5
Expected output:
[12345.678] superpipe: Module loaded successfully [12345.678] superpipe: Device created at /dev/superpipe [12345.678] superpipe: Buffer size: 4096 bytes
Verify device file:
ls -l /dev/superpipe
Expected output:
crw-rw-rw- 1 root root 10, 58 Dec 19 12:00 /dev/superpipe
The device is ready to use!
Step 3: Build the Test Program
Create test_superpipe.c:
/* test_superpipe.c - Userspace test program for superpipe */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include "superpipe_uapi.h"
void usage(const char *progname)
{
fprintf(stderr, "Usage: %s <command> [args]\n", progname);
fprintf(stderr, "Commands:\n");
fprintf(stderr, " set_mode <none|upper> - Set transformation mode\n");
fprintf(stderr, " get_mode - Get current mode\n");
fprintf(stderr, " write <text> - Write text to device\n");
fprintf(stderr, " read - Read from device\n");
fprintf(stderr, " reset - Reset device (clear buffer)\n");
exit(1);
}
int main(int argc, char *argv[])
{
int fd;
struct superpipe_ioctl_config cfg;
char buffer[256];
ssize_t n;
if (argc < 2) {
usage(argv[0]);
}
/* Open device */
fd = open("/dev/superpipe", O_RDWR);
if (fd < 0) {
perror("Failed to open /dev/superpipe");
fprintf(stderr, "Make sure the module is loaded\n");
return 1;
}
/* Process command */
if (strcmp(argv[1], "set_mode") == 0) {
if (argc < 3) {
fprintf(stderr, "Error: set_mode requires mode argument\n");
close(fd);
return 1;
}
if (strcmp(argv[2], "none") == 0) {
cfg.mode = SUPERPIPE_MODE_NONE;
} else if (strcmp(argv[2], "upper") == 0) {
cfg.mode = SUPERPIPE_MODE_UPPER;
} else {
fprintf(stderr, "Error: Unknown mode '%s'\n", argv[2]);
close(fd);
return 1;
}
cfg.flags = 0;
if (ioctl(fd, SUPERPIPE_IOC_SET_CONFIG, &cfg) < 0) {
perror("ioctl SET_CONFIG");
close(fd);
return 1;
}
printf("✓ Mode set to %s\n", argv[2]);
} else if (strcmp(argv[1], "get_mode") == 0) {
if (ioctl(fd, SUPERPIPE_IOC_GET_CONFIG, &cfg) < 0) {
perror("ioctl GET_CONFIG");
close(fd);
return 1;
}
printf("Current mode: ");
switch (cfg.mode) {
case SUPERPIPE_MODE_NONE:
printf("NONE\n");
break;
case SUPERPIPE_MODE_UPPER:
printf("UPPER\n");
break;
default:
printf("UNKNOWN (%u)\n", cfg.mode);
break;
}
} else if (strcmp(argv[1], "write") == 0) {
if (argc < 3) {
fprintf(stderr, "Error: write requires text argument\n");
close(fd);
return 1;
}
n = write(fd, argv[2], strlen(argv[2]));
if (n < 0) {
perror("write");
close(fd);
return 1;
}
printf("✓ Wrote %zd bytes\n", n);
} else if (strcmp(argv[1], "read") == 0) {
n = read(fd, buffer, sizeof(buffer) - 1);
if (n < 0) {
perror("read");
close(fd);
return 1;
}
if (n == 0) {
printf("(no data available)\n");
} else {
buffer[n] = '\0';
printf("%s\n", buffer);
}
} else if (strcmp(argv[1], "reset") == 0) {
if (ioctl(fd, SUPERPIPE_IOC_RESET) < 0) {
perror("ioctl RESET");
close(fd);
return 1;
}
printf("✓ Device reset\n");
} else {
fprintf(stderr, "Error: Unknown command '%s'\n", argv[1]);
close(fd);
usage(argv[0]);
}
close(fd);
return 0;
}
Compile:
gcc -o test_superpipe test_superpipe.c
Step 4: Test Basic Write and Read
Test pass-through mode:
./test_superpipe set_mode none
./test_superpipe write "Hello World 123"
./test_superpipe read
Expected output:
✓ Mode set to none ✓ Wrote 15 bytes Hello World 123
The data passes through unchanged.
Step 5: Test Uppercase Transformation
Enable uppercase mode and test:
./test_superpipe set_mode upper
./test_superpipe write "hello world 123"
./test_superpipe read
Expected output:
✓ Mode set to upper ✓ Wrote 15 bytes HELLO WORLD 123
The data was transformed to uppercase during the write!
Step 6: Test Mixed Mode Writes
Test that mode changes only affect new writes:
./test_superpipe set_mode none
./test_superpipe write "plain "
./test_superpipe set_mode upper
./test_superpipe write "caps "
./test_superpipe set_mode none
./test_superpipe write "normal"
./test_superpipe read
Expected output:
✓ Mode set to none ✓ Wrote 6 bytes ✓ Mode set to upper ✓ Wrote 5 bytes ✓ Mode set to none ✓ Wrote 6 bytes plain CAPS normal
Each write was transformed according to the mode at the time of writing!
Step 7: View Kernel Messages
Check what the kernel logged:
sudo dmesg | tail -20
Expected output:
[12345.678] superpipe: Module loaded successfully [12345.678] superpipe: Device created at /dev/superpipe [12346.123] superpipe: Device opened by process 1234 (test_superpipe) [12346.124] superpipe: Mode set to 0 [12346.125] superpipe: Device closed by process 1234 (test_superpipe) [12346.200] superpipe: Device opened by process 1235 (test_superpipe) [12346.201] superpipe: Wrote 15 bytes (mode=0) [12346.202] superpipe: Device closed by process 1235 (test_superpipe) [12346.300] superpipe: Device opened by process 1236 (test_superpipe) [12346.301] superpipe: Read 15 bytes [12346.302] superpipe: Device closed by process 1236 (test_superpipe) ...
You can see every open, close, read, write, and ioctl operation logged.
Step 8: Unload the Module
Unload:
sudo rmmod superpipe_ref
Verify device is gone:
ls -l /dev/superpipe
Expected:
ls: cannot access '/dev/superpipe': No such file or directory
Check kernel log:
sudo dmesg | tail -1
Expected:
[12350.456] superpipe: Module unloaded
Exercise: Full Superpipe Implementation
Objective
Extend the reference implementation to support five transformation modes:
- NONE - Pass through unchanged (already implemented)
- UPPER - Convert to uppercase (already implemented)
- LOWER - Convert to lowercase (NEW)
- DROP_VOWELS - Remove vowels (a, e, i, o, u, A, E, I, O, U) (NEW)
- DROP_SPACES - Remove space characters (0x20) (NEW)
Requirements
1. Update the UAPI Header
Add new mode constants to superpipe_uapi.h:
#define SUPERPIPE_MODE_NONE 0
#define SUPERPIPE_MODE_UPPER 1
#define SUPERPIPE_MODE_LOWER 2 /* NEW */
#define SUPERPIPE_MODE_DROP_VOWELS 3 /* NEW */
#define SUPERPIPE_MODE_DROP_SPACES 4 /* NEW */
2. Implement Transformation Functions
Add three new transformation functions to your kernel module:
/* Convert to lowercase */
static void transform_lower(char *data, size_t len);
/* Remove vowels (returns new length after filtering) */
static size_t transform_drop_vowels(char *data, size_t len);
/* Remove spaces (returns new length after filtering) */
static size_t transform_drop_spaces(char *data, size_t len);
3. Update the Write Handler
Modify superpipe_write() to handle all transformation modes:
/* Apply transformation - filtering may reduce length */
size_t final_len = to_write;
switch (dev_state.mode) {
case TRANSFORM_NONE:
/* No transformation */
break;
case TRANSFORM_UPPER:
transform_upper(write_position, to_write);
break;
case TRANSFORM_LOWER:
transform_lower(write_position, to_write);
break;
case TRANSFORM_DROP_VOWELS:
final_len = transform_drop_vowels(write_position, to_write);
break;
case TRANSFORM_DROP_SPACES:
final_len = transform_drop_spaces(write_position, to_write);
break;
}
/* Update buffer length by actual stored data */
dev_state.buffer_len += final_len;
return final_len; /* Return bytes actually stored */
4. Update the Test Program
Add support for new modes in test_superpipe.c:
} else if (strcmp(argv[2], "lower") == 0) {
cfg.mode = SUPERPIPE_MODE_LOWER;
} else if (strcmp(argv[2], "drop_vowels") == 0) {
cfg.mode = SUPERPIPE_MODE_DROP_VOWELS;
} else if (strcmp(argv[2], "drop_spaces") == 0) {
cfg.mode = SUPERPIPE_MODE_DROP_SPACES;
Transformation Specifications
TRANSFORM_LOWER:
Convert all ASCII uppercase letters (A-Z) to lowercase (a-z). Other characters unchanged.
Input: "Hello WORLD 123" Output: "hello world 123"
TRANSFORM_DROP_VOWELS:
Remove all vowels (both upper and lowercase): a, e, i, o, u, A, E, I, O, U
Input: "beautiful day" Output: "btfl dy" Input: "AEIOU" Output: "" (empty)
TRANSFORM_DROP_SPACES:
Remove all space characters (ASCII 0x20)
Input: "hello world test" Output: "helloworldtest" Input: " spaces " Output: "spaces"
Important for filtering transforms:
When characters are removed, the data must be compacted in the buffer. However, the write() system call still returns the number of bytes consumed from the user's buffer (standard UNIX behavior).
Example:
./test_superpipe set_mode drop_vowels
./test_superpipe write "hello" # Writes 5 bytes
# Output: ✓ Wrote 5 bytes # Returns 5 (consumed all user data)
./test_superpipe read
# Output: hll # But only 3 bytes stored in buffer
Implementation Guidance
Helper function for vowel detection:
static inline bool is_vowel(char c)
{
return (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||
c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U');
}
Pattern for filtering (compacting) transformations:
static size_t transform_drop_vowels(char *data, size_t len)
{
size_t i, j = 0;
/* Iterate through input, copy only non-vowels */
for (i = 0; i < len; i++) {
if (!is_vowel(data[i])) {
data[j++] = data[i]; /* Keep this character */
}
/* Vowels are skipped - not copied to output position */
}
/* j now contains the number of characters kept */
return j;
}
Why this works:
- We iterate with
ithrough all input characters - We write kept characters to position
j jonly advances when we keep a character- At the end,
jis the new length
Pattern for case conversion:
static void transform_lower(char *data, size_t len)
{
size_t i;
for (i = 0; i < len; i++) {
if (isupper(data[i])) {
data[i] = tolower(data[i]);
}
}
}
Testing Requirements
Create a comprehensive test script test.sh that demonstrates:
Test 1: NONE mode
./test_superpipe set_mode none
./test_superpipe write "Hello World 123"
./test_superpipe read
# Expected: Hello World 123
Test 2: UPPER mode
./test_superpipe set_mode upper
./test_superpipe write "hello world 123"
./test_superpipe read
# Expected: HELLO WORLD 123
Test 3: LOWER mode
./test_superpipe set_mode lower
./test_superpipe write "HELLO WORLD 123"
./test_superpipe read
# Expected: hello world 123
Test 4: DROP_VOWELS mode
./test_superpipe set_mode drop_vowels
./test_superpipe write "beautiful"
./test_superpipe read
# Expected: btfl
Test 5: DROP_SPACES mode
./test_superpipe set_mode drop_spaces
./test_superpipe write "hello world test"
./test_superpipe read
# Expected: helloworldtest
Test 6: Mixed modes (different modes for different writes)
./test_superpipe set_mode none
./test_superpipe write "plain "
./test_superpipe set_mode upper
./test_superpipe write "CAPS "
./test_superpipe set_mode lower
./test_superpipe write "lower"
./test_superpipe read
# Expected: plain CAPS lower
Test 7: Edge cases
# All vowels
./test_superpipe set_mode drop_vowels
./test_superpipe write "aeiou"
./test_superpipe read
# Expected: (empty)
# All spaces
./test_superpipe set_mode drop_spaces
./test_superpipe write " "
./test_superpipe read
# Expected: (empty)
Deliverables
Submit a PDF with your code and screenshots of:
- All transform modes tested and working
- Detailed kernel logs
Reference: Kernel Programming Quick Guide
Module Macros and Metadata
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
/* Declare init and exit functions */
module_init(my_init_function);
module_exit(my_exit_function);
/* Module metadata (required) */
MODULE_LICENSE("GPL"); /* Must be GPL-compatible */
MODULE_AUTHOR("Your Name <email>");
MODULE_DESCRIPTION("Brief description");
MODULE_VERSION("1.0");
/* Init function signature */
static int __init my_init_function(void)
{
/* Return 0 on success, negative error code on failure */
}
/* Exit function signature */
static void __exit my_exit_function(void)
{
/* No return value */
}
Memory Management
#include <linux/slab.h>
/* Allocate memory */
void *ptr = kmalloc(size, GFP_KERNEL);
if (!ptr)
return -ENOMEM;
/* Allocate and zero memory */
void *ptr = kzalloc(size, GFP_KERNEL);
/* Free memory */
kfree(ptr);
/* GFP flags:
* GFP_KERNEL - Normal allocation, can sleep
* GFP_ATOMIC - Cannot sleep (use in interrupt context)
* GFP_USER - Allocate for userspace
*/
Data Transfer Functions
#include <linux/uaccess.h>
/* Copy to userspace */
if (copy_to_user(user_ptr, kernel_ptr, size))
return -EFAULT;
/* Copy from userspace */
if (copy_from_user(kernel_ptr, user_ptr, size))
return -EFAULT;
/* Get a single value from userspace */
int val;
if (get_user(val, (int __user *)user_ptr))
return -EFAULT;
/* Put a single value to userspace */
if (put_user(val, (int __user *)user_ptr))
return -EFAULT;
Synchronization
#include <linux/mutex.h>
/* Declare mutex */
static DEFINE_MUTEX(my_mutex);
/* Or dynamic initialization */
struct mutex my_mutex;
mutex_init(&my_mutex);
/* Lock (sleeps if unavailable) */
mutex_lock(&my_mutex);
/* ... critical section ... */
mutex_unlock(&my_mutex);
/* Lock (interruptible by signals) */
if (mutex_lock_interruptible(&my_mutex))
return -ERESTARTSYS;
/* ... critical section ... */
mutex_unlock(&my_mutex);
/* Try lock (non-blocking) */
if (mutex_trylock(&my_mutex)) {
/* Got lock */
/* ... critical section ... */
mutex_unlock(&my_mutex);
} else {
/* Lock held by someone else */
}
Logging and Debugging
#include <linux/kernel.h>
/* Log levels */
printk(KERN_EMERG "Emergency\n"); /* System unusable */
printk(KERN_ALERT "Alert\n"); /* Action must be taken */
printk(KERN_CRIT "Critical\n"); /* Critical condition */
printk(KERN_ERR "Error\n"); /* Error condition */
printk(KERN_WARNING "Warning\n"); /* Warning */
printk(KERN_NOTICE "Notice\n"); /* Normal but significant */
printk(KERN_INFO "Info\n"); /* Informational */
printk(KERN_DEBUG "Debug\n"); /* Debug messages */
/* Formatted output (like printf) */
printk(KERN_INFO "Value: %d, String: %s\n", value, string);
/* Access current process info */
printk(KERN_INFO "PID: %d, Name: %s\n", current->pid, current->comm);
/* View logs */
/* dmesg */
/* tail -f /var/log/kern.log */
Common Error Codes
#include <linux/errno.h>
return 0; /* Success */
return -ENOMEM; /* Out of memory */
return -EFAULT; /* Bad address (copy_to/from_user failed) */
return -EINVAL; /* Invalid argument */
return -ENOSPC; /* No space left on device */
return -EBUSY; /* Device or resource busy */
return -EIO; /* I/O error */
return -ENODEV; /* No such device */
return -EPERM; /* Operation not permitted */
return -EACCES; /* Permission denied */
return -ERESTARTSYS; /* Interrupted system call */
Common Issues and Troubleshooting
Compilation Errors
Issue: "No rule to make target"
make: *** No rule to make target 'modules'. Stop.
Cause: Wrong KDIR path in Makefile
Fix: Verify kernel headers are installed and path is correct:
ls -l /lib/modules/$(uname -r)/build
# Should exist and link to kernel source
Issue: "missing MODULE_LICENSE"
WARNING: modpost: missing MODULE_LICENSE() in ...
Cause: Forgot to add MODULE_LICENSE macro
Fix: Add to your module:
MODULE_LICENSE("GPL");
Issue: "implicit declaration of function"
Cause: Missing #include directive
Fix: Add appropriate header. Common headers:
<linux/kernel.h>- Core kernel functions<linux/uaccess.h>- copy_to_user, copy_from_user<linux/slab.h>- kmalloc, kfree<linux/mutex.h>- Mutex functions
Module Loading Issues
Issue: "Invalid module format"
insmod: ERROR: could not insert module: Invalid module format
Cause: Module compiled for different kernel version
Fix: Clean and rebuild:
make clean
make
Ensure uname -r matches the kernel you're running.
Issue: "Operation not permitted"
Cause: Need root privileges
Fix: Use sudo:
sudo insmod module.ko
Issue: "Device or resource busy"
rmmod: ERROR: Module is in use
Cause: Device is still open or module is in use
Fix: Close all programs using the device:
lsof /dev/superpipe # Find processes using device
kill <PID> # Kill processes if necessary
sudo rmmod module
Runtime Issues
Issue: "Bad address" (-EFAULT) from read/write
Cause: Forgot copy_to_user/copy_from_user
Fix: Never directly access user pointers:
/* WRONG */
strcpy(user_buffer, kernel_data);
/* CORRECT */
if (copy_to_user(user_buffer, kernel_data, len))
return -EFAULT;
Issue: Kernel panic on module load
Cause: Accessing NULL pointer or uninitialized memory
Fix: Always check return values:
buffer = kmalloc(size, GFP_KERNEL);
if (!buffer) {
printk(KERN_ERR "Failed to allocate\n");
return -ENOMEM; /* Don't continue with NULL pointer! */
}
Issue: Data corruption with concurrent access
Cause: Missing mutex locks
Fix: Protect all shared data:
/* Acquire lock before accessing shared state */
mutex_lock_interruptible(&dev_state.lock);
/* Access shared data */
/* Release lock */
mutex_unlock(&dev_state.lock);
Issue: Module stuck, can't unload
Cause: Process stuck in kernel code (infinite loop or deadlock)
Fix:
- Kill the hung process
- If that doesn't work, reboot (this is why VMs are recommended!)
Debugging Strategies
Strategy 1: Liberally use printk
printk(KERN_DEBUG "Entering function, buffer_len=%zu\n", dev_state.buffer_len);
printk(KERN_DEBUG "After copy, to_write=%zu\n", to_write);
printk(KERN_DEBUG "Exiting function, returning %zd\n", ret);
Strategy 2: Check return values
ret = copy_from_user(...);
printk(KERN_DEBUG "copy_from_user returned %lu\n", ret);
if (ret) {
printk(KERN_ERR "Failed to copy %lu bytes\n", ret);
return -EFAULT;
}
Strategy 3: Dump buffer contents
/* Hexdump helper */
void hexdump(const char *data, size_t len)
{
size_t i;
printk(KERN_DEBUG "Hexdump (%zu bytes):\n", len);
for (i = 0; i < len; i++) {
printk(KERN_CONT "%02x ", (unsigned char)data[i]);
if ((i + 1) % 16 == 0)
printk(KERN_CONT "\n");
}
printk(KERN_CONT "\n");
}
Strategy 4: Monitor kernel logs in real-time
In one terminal:
sudo dmesg -w
In another terminal, run your tests. You'll see kernel messages appear immediately.
Strategy 5: Add assertions
#define ASSERT(cond) \
do { \
if (!(cond)) { \
printk(KERN_ERR "Assertion failed: %s\n", #cond); \
BUG(); \
} \
} while (0)
ASSERT(buffer_len <= BUFFER_SIZE);
ASSERT(mode < TRANSFORM_MAX);
Note: BUG() causes a kernel oops. Use only for debugging, remove before submission.
For Further Study
Advanced Topics
1. Multiple Device Instances
Extend superpipe to support multiple independent devices (/dev/superpipe0, /dev/superpipe1, etc.) with separate buffers and modes.
Hint: Use dynamic miscdevice allocation and embed miscdevice in device structure.
2. Per-File-Descriptor State
Allow each open() to have independent buffer and transformation mode using file->private_data.
3. Non-Blocking I/O and Poll
Implement O_NONBLOCK support and poll() operation so userspace can use select() or epoll().
4. Sysfs Interface
Expose device statistics via sysfs (/sys/class/misc/superpipe/):
- Total bytes read
- Total bytes written
- Current mode
- Buffer usage
5. Advanced Transformations
Implement more complex transformations:
- ROT13 cipher
- Base64 encoding/decoding
- Compression (simple run-length encoding)
6. Ring Buffer Implementation
Replace linear buffer with circular ring buffer for better performance (no memmove on read).
Relevant Documentation
Kernel Documentation:
# View kernel documentation
cd /usr/src/linux-headers-$(uname -r)
ls Documentation/
# Useful sections:
# - kernel-hacking/
# - driver-api/
# - core-api/
Online Resources:
- Linux Kernel Documentation: https://www.kernel.org/doc/html/latest/
- Linux Device Drivers (LDD3): Classic book, available online
- Kernel Newbies: https://kernelnewbies.org/
- KernelCI: https://kernelci.org/
- LKML Archives: https://lkml.org/
- Bootlin Linux Source View: https://elixir.bootlin.com/linux
Manual pages:
man 2 open # open() system call
man 2 read # read() system call
man 2 write # write() system call
man 2 ioctl # ioctl() system call
man 7 capabilities # Linux capabilities
Kernel source as reference:
# Find examples of miscdevice usage
cd /usr/src/linux-source-*/
grep -r "misc_register" drivers/
# Example drivers to study:
# - drivers/char/mem.c (/dev/null, /dev/zero, /dev/random)
# - drivers/char/misc.c (miscdevice framework itself)
Debugging:
addr2line: Convert addresses in oops messages to source linesobjdump: Disassemble kernel modulesgdbwith/proc/kcore: Debug live kernelSystemTap/eBPF: Dynamic kernel tracing
Remember: The Linux kernel is one of the largest open-source projects. Reading kernel code is the best way to learn kernel programming.