driver-know-hows

device driver related stuff

View on GitHub

Chapter 1: Kernel Modules & Driver Fundamentals

Table of Contents

  1. Introduction to Kernel Space
  2. Kernel Architecture Overview
  3. Your First Kernel Module
  4. Module Parameters
  5. Module Dependencies
  6. Kernel Symbols and Exports
  7. Best Practices

Introduction to Kernel Space

User Space vs Kernel Space

Linux systems operate with two distinct privilege levels:

User Space (Ring 3 - Unprivileged)

Kernel Space (Ring 0 - Privileged)

┌─────────────────────────────────────────┐
│         User Space (Ring 3)             │
│  ┌──────────┐  ┌──────────┐  ┌───────┐  │
│  │  App 1   │  │  App 2   │  │ App N │  │
│  └──────────┘  └──────────┘  └───────┘  │
├─────────────────────────────────────────┤
│            System Call Interface        │
├─────────────────────────────────────────┤
│         Kernel Space (Ring 0)           │
│  ┌──────────────────────────────────┐   │
│  │     Virtual File System (VFS)    │   │
│  ├──────────────────────────────────┤   │
│  │  Process   │ Memory  │ Network   │   │
│  │  Scheduler │ Manager │   Stack   │   │
│  ├──────────────────────────────────┤   │
│  │      Device Drivers Layer        │   │
│  └──────────────────────────────────┘   │
├─────────────────────────────────────────┤
│              Hardware                   │
└─────────────────────────────────────────┘

Why Device Drivers?

Device drivers are kernel modules that:


Kernel Architecture Overview

Monolithic Kernel Design

Linux uses a monolithic kernel with loadable modules:

Major Kernel Subsystems

  1. Process Management: Scheduling, context switching, fork/exec
  2. Memory Management: Virtual memory, paging, allocation
  3. Virtual File System: Abstraction over different filesystems
  4. Network Stack: TCP/IP, routing, sockets
  5. Device Drivers: Character, block, network devices
  6. Inter-Process Communication: Signals, pipes, shared memory

The Device Model

Modern Linux uses a sophisticated device model:

Bus (PCI, USB, Platform)
    ↓
Device (Physical device representation)
    ↓
Driver (Software that controls device)
    ↓
Class (Logical grouping: input, block, net)

Your First Kernel Module

Theory: Module Lifecycle

Every kernel module has:

  1. Initialization Function: Called when module loads (module_init)
  2. Cleanup Function: Called when module unloads (module_exit)
  3. Module Metadata: Author, license, description, version

Code Example: hello.c

/*
 * hello.c - The simplest kernel module
 *
 * Demonstrates:
 * - Module initialization and cleanup
 * - Kernel logging with printk
 * - Module metadata
 */

#include <linux/init.h>      /* Macros for __init and __exit */
#include <linux/module.h>    /* Core header for loading modules */
#include <linux/kernel.h>    /* KERN_* log level macros */

/*
 * module_init() - Entry point when module is loaded
 *
 * This function is called when the module is inserted into the kernel
 * using insmod or modprobe. Return 0 on success, negative error code
 * on failure.
 *
 * __init macro tells the kernel this function is only needed during
 * initialization and can be freed after module loads (saves memory).
 */
static int __init hello_init(void)
{
    /*
     * printk() - Kernel's printf equivalent
     * 
     * KERN_INFO is the log level (similar to syslog levels)
     * Log levels: EMERG, ALERT, CRIT, ERR, WARNING, NOTICE, INFO, DEBUG
     * 
     * Output goes to kernel log buffer, viewable with dmesg or journalctl
     */
    printk(KERN_INFO "Hello: Module loaded successfully\n");
    printk(KERN_INFO "Hello: Running in kernel space!\n");
    
    /*
     * Return 0 indicates successful initialization
     * Returning non-zero prevents module from loading
     */
    return 0;
}

/*
 * module_exit() - Cleanup function called when module is removed
 *
 * This function is called when the module is removed using rmmod.
 * Must clean up all resources allocated during init or runtime.
 *
 * __exit macro indicates this function is only needed during cleanup
 * and can be omitted if module is built into kernel (not as module).
 */
static void __exit hello_exit(void)
{
    printk(KERN_INFO "Hello: Module unloaded, goodbye!\n");
    
    /*
     * void return - cleanup functions don't fail
     * If you can't clean up properly, the module becomes "tainted"
     * and cannot be unloaded (requires reboot)
     */
}

/*
 * Register init and exit functions
 * These macros create the necessary metadata for the module loader
 */
module_init(hello_init);
module_exit(hello_exit);

/*
 * Module Metadata
 * 
 * These macros provide information about the module:
 * - Shown in modinfo output
 * - Used for dependency resolution
 * - Required for kernel licensing compliance
 */
MODULE_LICENSE("GPL");              /* Must specify license (GPL for kernel APIs) */
MODULE_AUTHOR("Your Name");         /* Author information */
MODULE_DESCRIPTION("A simple hello world kernel module"); /* Brief description */
MODULE_VERSION("1.0");              /* Version string */

Makefile for Building the Module

# Makefile for building kernel modules
#
# Usage:
#   make          - Build the module
#   make clean    - Remove build artifacts
#   make install  - Install module (requires root)

# Object file to build (hello.ko will be created from hello.o)
obj-m += hello.o

# Kernel build directory (where kernel was built)
# Uses currently running kernel by default
KERNEL_DIR := /lib/modules/$(shell uname -r)/build

# Current directory
PWD := $(shell pwd)

# Default target: build the module
all:
	# -C: Change to kernel build directory
	# M=$(PWD): Module source is in current directory
	# modules: Build modules (not the entire kernel)
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules

# Clean build artifacts
clean:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

# Install module to system (requires root)
install:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules_install
	depmod -a

# Load the module
load:
	sudo insmod hello.ko

# Unload the module
unload:
	sudo rmmod hello

# Show kernel log
log:
	dmesg | tail -20

.PHONY: all clean install load unload log

Building and Testing

# Build the module
make

# Load the module into kernel
sudo insmod hello.ko

# Check if module is loaded
lsmod | grep hello

# View kernel messages
dmesg | tail

# Get module information
modinfo hello.ko

# Unload the module
sudo rmmod hello

# View unload message
dmesg | tail

Expected Output:

[ 1234.567890] Hello: Module loaded successfully
[ 1234.567891] Hello: Running in kernel space!
[ 1250.123456] Hello: Module unloaded, goodbye!

Module Parameters

Theory: Passing Parameters to Modules

Module parameters allow configuration at load time:

Code Example: param_demo.c

/*
 * param_demo.c - Demonstrating module parameters
 *
 * Shows how to:
 * - Define parameters with different types
 * - Set permissions for sysfs access
 * - Validate parameter values
 */

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>   /* For module_param macros */
#include <linux/stat.h>          /* For permission flags */

/*
 * Module Parameters
 * 
 * Syntax: module_param(name, type, permissions)
 * 
 * Types: int, uint, long, ulong, short, ushort, byte, bool, charp (string)
 * 
 * Permissions (for /sys/module/MODULE_NAME/parameters/):
 *   0        - No sysfs entry
 *   S_IRUGO  - Read by all (0444)
 *   S_IWUSR  - Write by owner (0200)
 *   S_IRUGO|S_IWUSR - Read all, write owner (0644)
 */

/* Integer parameter with default value */
static int debug_level = 0;
module_param(debug_level, int, S_IRUGO | S_IWUSR);
MODULE_PARM_DESC(debug_level, "Debug level (0=off, 1=info, 2=verbose)");

/* String parameter */
static char *device_name = "default_device";
module_param(device_name, charp, S_IRUGO);
MODULE_PARM_DESC(device_name, "Name of the device to control");

/* Boolean parameter */
static bool enable_feature = false;
module_param(enable_feature, bool, S_IRUGO | S_IWUSR);
MODULE_PARM_DESC(enable_feature, "Enable experimental feature");

/* Array parameter */
static int irq_list[4] = { 0 };
static int irq_count = 0;  /* Number of array elements provided */
module_param_array(irq_list, int, &irq_count, S_IRUGO);
MODULE_PARM_DESC(irq_list, "List of IRQ numbers to use");

/*
 * Custom parameter validation
 * 
 * For complex validation, use a setter function
 */
static int max_connections = 10;

static int set_max_conn(const char *val, const struct kernel_param *kp)
{
    int n, ret;
    
    /* Convert string to integer */
    ret = kstrtoint(val, 10, &n);
    if (ret)
        return ret;
    
    /* Validate range */
    if (n < 1 || n > 100) {
        printk(KERN_ERR "param_demo: max_connections must be 1-100\n");
        return -EINVAL;
    }
    
    /* Set the value */
    max_connections = n;
    printk(KERN_INFO "param_demo: max_connections set to %d\n", n);
    
    return 0;
}

static int get_max_conn(char *buffer, const struct kernel_param *kp)
{
    /* Return current value as string */
    return sprintf(buffer, "%d", max_connections);
}

/* Define custom parameter operations */
static const struct kernel_param_ops max_conn_ops = {
    .set = set_max_conn,
    .get = get_max_conn,
};

/* Register parameter with custom ops */
module_param_cb(max_connections, &max_conn_ops, &max_connections, S_IRUGO | S_IWUSR);
MODULE_PARM_DESC(max_connections, "Maximum number of connections (1-100)");

static int __init param_demo_init(void)
{
    int i;
    
    printk(KERN_INFO "param_demo: Module loaded with parameters:\n");
    printk(KERN_INFO "  debug_level = %d\n", debug_level);
    printk(KERN_INFO "  device_name = %s\n", device_name);
    printk(KERN_INFO "  enable_feature = %s\n", enable_feature ? "true" : "false");
    printk(KERN_INFO "  max_connections = %d\n", max_connections);
    
    /* Print array parameters */
    if (irq_count > 0) {
        printk(KERN_INFO "  irq_list = [");
        for (i = 0; i < irq_count; i++) {
            printk(KERN_CONT "%d%s", irq_list[i], 
                   (i < irq_count - 1) ? ", " : "");
        }
        printk(KERN_CONT "]\n");
    }
    
    return 0;
}

static void __exit param_demo_exit(void)
{
    printk(KERN_INFO "param_demo: Module unloaded\n");
}

module_init(param_demo_init);
module_exit(param_demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tutorial Author");
MODULE_DESCRIPTION("Demonstration of module parameters");
MODULE_VERSION("1.0");

Testing Parameters

# Load with default parameters
sudo insmod param_demo.ko

# Load with custom parameters
sudo insmod param_demo.ko debug_level=2 device_name="eth0" enable_feature=true

# Load with array parameter
sudo insmod param_demo.ko irq_list=10,11,12,13

# Change parameter at runtime
echo 5 > /sys/module/param_demo/parameters/debug_level

# Read current value
cat /sys/module/param_demo/parameters/debug_level

# View parameter info
modinfo param_demo.ko

# Unload
sudo rmmod param_demo

Module Dependencies

Theory: Symbol Resolution

Modules can use symbols (functions/variables) from:

  1. Kernel Core: Always available (e.g., printk, kmalloc)
  2. Other Modules: Must be loaded first

The kernel maintains a symbol table for exported symbols.

Code Example: Exporting Symbols

library.c - Provides shared functions:

/*
 * library.c - Module that exports symbols for other modules
 */

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

/*
 * Shared function that can be used by other modules
 * 
 * Must be exported using EXPORT_SYMBOL or EXPORT_SYMBOL_GPL
 */
int library_add(int a, int b)
{
    printk(KERN_INFO "library: add(%d, %d) = %d\n", a, b, a + b);
    return a + b;
}
EXPORT_SYMBOL(library_add);  /* Export for any module */

int library_multiply(int a, int b)
{
    printk(KERN_INFO "library: multiply(%d, %d) = %d\n", a, b, a * b);
    return a * b;
}
EXPORT_SYMBOL_GPL(library_multiply);  /* Export only for GPL modules */

/*
 * Private function - not exported
 * Only usable within this module
 */
static int library_private_func(void)
{
    printk(KERN_INFO "library: This is private\n");
    return 0;
}

static int __init library_init(void)
{
    printk(KERN_INFO "library: Module loaded\n");
    library_private_func();
    return 0;
}

static void __exit library_exit(void)
{
    printk(KERN_INFO "library: Module unloaded\n");
}

module_init(library_init);
module_exit(library_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tutorial Author");
MODULE_DESCRIPTION("Library module that exports symbols");
MODULE_VERSION("1.0");

client.c - Uses exported symbols:

/*
 * client.c - Module that uses symbols from library module
 */

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

/*
 * Declare external functions from library module
 * 
 * These must match the prototypes in library.c
 */
extern int library_add(int a, int b);
extern int library_multiply(int a, int b);

static int __init client_init(void)
{
    int result;
    
    printk(KERN_INFO "client: Module loaded\n");
    
    /*
     * Use functions from library module
     * 
     * Note: library module must be loaded first,
     * or this module will fail to load with "Unknown symbol" error
     */
    result = library_add(5, 3);
    printk(KERN_INFO "client: Result of add = %d\n", result);
    
    result = library_multiply(5, 3);
    printk(KERN_INFO "client: Result of multiply = %d\n", result);
    
    return 0;
}

static void __exit client_exit(void)
{
    printk(KERN_INFO "client: Module unloaded\n");
}

module_init(client_init);
module_exit(client_exit);

MODULE_LICENSE("GPL");  /* Must be GPL to use GPL-only symbols */
MODULE_AUTHOR("Tutorial Author");
MODULE_DESCRIPTION("Client module that uses library symbols");
MODULE_VERSION("1.0");

Testing Module Dependencies

# Load library first
sudo insmod library.ko

# Now load client (depends on library)
sudo insmod client.ko

# Check dependencies
lsmod | grep -E "library|client"

# Try to unload library (will fail because client depends on it)
sudo rmmod library  # Error: Module library is in use by: client

# Unload in correct order
sudo rmmod client
sudo rmmod library

# Alternative: Use modprobe for automatic dependency resolution
# (requires modules to be installed in /lib/modules/)

Kernel Symbols and Exports

Symbol Visibility

/*
 * Understanding symbol visibility in kernel modules
 */

/* 1. Static symbols - Local to this file only */
static int local_variable = 0;
static void local_function(void) {
    /* Only visible in this file */
}

/* 2. Global symbols - Visible within module only (not exported) */
int module_global = 0;
void module_function(void) {
    /* Visible across files in same module, but not to other modules */
}

/* 3. Exported symbols - Available to other modules */
int shared_variable = 0;
EXPORT_SYMBOL(shared_variable);

void shared_function(void) {
    /* Can be used by any module */
}
EXPORT_SYMBOL(shared_function);

/* 4. GPL-only exported symbols - Only for GPL modules */
void gpl_only_function(void) {
    /* Only GPL-licensed modules can use this */
}
EXPORT_SYMBOL_GPL(gpl_only_function);

Viewing Exported Symbols

# View all kernel symbols
cat /proc/kallsyms | grep module_name

# View symbols exported by a module
nm -D module_name.ko

# Find where a symbol is defined
grep -r "EXPORT_SYMBOL.*symbol_name" /usr/src/linux/

Best Practices

1. Error Handling

static int __init example_init(void)
{
    int ret;
    void *buffer;
    
    /* Always check return values */
    buffer = kmalloc(1024, GFP_KERNEL);
    if (!buffer) {
        printk(KERN_ERR "example: Failed to allocate memory\n");
        return -ENOMEM;  /* Return appropriate error code */
    }
    
    ret = some_initialization_function();
    if (ret < 0) {
        printk(KERN_ERR "example: Initialization failed: %d\n", ret);
        goto err_free_buffer;  /* Use goto for cleanup */
    }
    
    return 0;  /* Success */

err_free_buffer:
    kfree(buffer);
    return ret;
}

2. Logging Best Practices

/*
 * Use appropriate log levels:
 * 
 * KERN_EMERG   - System is unusable
 * KERN_ALERT   - Action must be taken immediately
 * KERN_CRIT    - Critical conditions
 * KERN_ERR     - Error conditions
 * KERN_WARNING - Warning conditions
 * KERN_NOTICE  - Normal but significant condition
 * KERN_INFO    - Informational
 * KERN_DEBUG   - Debug-level messages
 */

/* Use pr_* macros (preferred over printk) */
pr_err("Failed to initialize device\n");
pr_warn("Device timeout, retrying...\n");
pr_info("Device registered successfully\n");
pr_debug("Register value: 0x%08x\n", reg_val);

/* For device-specific messages, use dev_* macros */
dev_err(&pdev->dev, "Failed to map registers\n");
dev_info(&pdev->dev, "Device probe successful\n");

3. Memory Management

/* Always free what you allocate */
static int __init safe_init(void)
{
    char *buf1, *buf2, *buf3;
    
    buf1 = kmalloc(100, GFP_KERNEL);
    if (!buf1)
        return -ENOMEM;
    
    buf2 = kmalloc(200, GFP_KERNEL);
    if (!buf2) {
        kfree(buf1);
        return -ENOMEM;
    }
    
    buf3 = kmalloc(300, GFP_KERNEL);
    if (!buf3) {
        kfree(buf2);
        kfree(buf1);
        return -ENOMEM;
    }
    
    /* Use the buffers... */
    
    /* Cleanup in reverse order */
    kfree(buf3);
    kfree(buf2);
    kfree(buf1);
    
    return 0;
}

4. Coding Style

Follow the Linux kernel coding style:

# Check your code with kernel's style checker
./scripts/checkpatch.pl --no-tree --file your_module.c

# Key points:
# - Use tabs (not spaces) for indentation
# - 80 character line limit (flexible for readability)
# - Opening brace on same line for functions
# - Descriptive variable names (no Hungarian notation)
# - Comments explain "why", not "what"

5. Version Compatibility

#include <linux/version.h>

/* Check kernel version at compile time */
#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 0, 0)
    #error "This module requires kernel 5.0 or later"
#endif

/* Conditional compilation for API changes */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
    /* Use new API */
    new_function();
#else
    /* Use old API */
    old_function();
#endif

Summary

In this chapter, you learned:

Kernel vs User Space: Different privilege levels and restrictions
Module Basics: Init/exit functions, printk, metadata
Module Parameters: Configurable module behavior
Symbol Export: Sharing functions between modules
Best Practices: Error handling, logging, memory management

Key Takeaways

  1. Kernel modules extend kernel functionality without requiring recompilation
  2. Always initialize and cleanup properly - leaks persist until reboot
  3. Use appropriate log levels for different message types
  4. Check all return values and handle errors gracefully
  5. Follow kernel coding style for consistency and maintainability

Next Steps

Proceed to 02-char-drivers.md to learn about character device drivers and how to create device files that userspace programs can interact with.


Quick Reference

Essential Headers

#include <linux/init.h>       /* __init, __exit */
#include <linux/module.h>     /* Core module support */
#include <linux/kernel.h>     /* printk, KERN_* */
#include <linux/moduleparam.h> /* module_param */

Common Module Macros

module_init(func)            /* Register init function */
module_exit(func)            /* Register exit function */
MODULE_LICENSE("GPL")        /* Required: GPL, GPL v2, Proprietary */
MODULE_AUTHOR("name")        /* Author information */
MODULE_DESCRIPTION("text")   /* Brief description */
MODULE_VERSION("1.0")        /* Version string */

Useful Commands

insmod module.ko             # Load module
rmmod module                 # Unload module
lsmod                        # List loaded modules
modinfo module.ko            # Show module information
dmesg                        # View kernel log
modprobe module              # Load with dependency resolution