Linux Kernel Module Parameters – Advanced Patterns

Linux Kernel Module Parameters – sysfs, Arrays & Real Driver Patterns | Free Linux Device Drivers Course

› Module Parameters – Advanced Patterns

Kernel Module Parameters – Advanced Patterns
Array parameters, sysfs runtime control, debug level pattern, and real device driver techniques – Free Linux Device Drivers Course
Level
Intermediate
Kernel
Linux 6.x
Topic
Driver Params
Course
100% Free
Topics Covered:
module_param_array() debug_level pattern sysfs parameter files modprobe.d config parameter validation real driver patterns free embedded systems course

What You Will Learn

  • How to pass multiple values using module_param_array()
  • The complete debug_level pattern used in production Linux drivers
  • How sysfs parameter files are structured and how to use them safely
  • How to make module parameters persistent across reboots with modprobe.d
  • How to validate parameters in module_init() and return clean errors
  • A full working multi-parameter driver skeleton you can use as a template
  • Real-world use cases from Ethernet drivers, storage drivers, and GPIO drivers

In the previous lecture we covered the basics of module_param() — how to declare a parameter, what types are supported, and how modinfo reads descriptions. This lecture goes deeper. We look at the patterns you will actually use in real device drivers: multiple values via arrays, guarding production code with debug levels, making settings persistent, and writing clean validation logic in your init function.

By the end of this lecture you will have a solid template you can drop into any free Linux device drivers project and adapt to your hardware. This is part of EmbeddedPathashala’s completely free embedded systems course, covering both the theory and the practical side of Linux kernel development.

Passing Multiple Values with module_param_array()

A single module_param() handles one value. But what if your driver supports multiple hardware instances and each one needs its own IRQ number or I/O port? This is where module_param_array() comes in. It lets the user pass a comma-separated list of values at load time.

The macro signature is:

module_param_array(name, type, nump, perm);

The difference from module_param() is the extra nump argument — a pointer to a variable where the kernel stores how many values the user actually passed. If the user passes fewer values than the array size, only those positions are filled.

// File: multiport.c
// Example: multiple I/O ports for a multi-instance driver
// Free Linux Device Drivers Course – EmbeddedPathashala

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("module_param_array() demo for Linux 6.x");

#define MAX_PORTS  4

static unsigned int io_ports[MAX_PORTS];
static int num_ports;   /* kernel fills this in */

module_param_array(io_ports, uint, &num_ports, 0444);
MODULE_PARM_DESC(io_ports,
    "I/O base addresses for up to 4 device instances (comma-separated)");

static int __init multiport_init(void)
{
    int i;

    if (num_ports == 0) {
        pr_err("multiport: no I/O ports specified\n");
        return -EINVAL;
    }

    pr_info("multiport: %d port(s) registered\n", num_ports);
    for (i = 0; i < num_ports; i++)
        pr_info("multiport: port[%d] = 0x%x\n", i, io_ports[i]);

    return 0;
}

static void __exit multiport_exit(void)
{
    pr_info("multiport: unloaded\n");
}

module_init(multiport_init);
module_exit(multiport_exit);

Loading this module with three ports looks like this:

sudo insmod multiport.ko io_ports=0x3F8,0x2F8,0x3E8

The kernel parses the comma-separated hex values, fills the array, and sets num_ports = 3. Your init function then loops over exactly the ports the user specified.

module_param_array() — How Multiple Values Are Filled
insmod argument io_ports[0] io_ports[1] io_ports[2] io_ports[3] num_ports
io_ports=0x3F8,0x2F8,0x3E8 0x3F8 0x2F8 0x3E8 0 (untouched) 3
io_ports=0x300 0x300 0 0 0 1
(not specified) 0 0 0 0 0

The Debug Level Pattern – Used in Production Linux Drivers

This is one of the most widely used patterns in Linux kernel drivers. The idea is simple: you have a single integer parameter called something like debug or debug_level. Every debug message in the driver is guarded by a check against this integer. Higher values give more verbose output.

Debug Level Pattern – Verbosity Layers
debug_level = 0 Silent — production mode
debug_level = 1 Module load/unload info only
debug_level = 2 Data path events & interrupts
debug_level = 3 Full verbose trace — development only

Here is a clean implementation of this pattern in a driver skeleton:

// debug_level pattern – production-quality implementation
// Free Linux Kernel Programming Course – EmbeddedPathashala

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

MODULE_LICENSE("GPL");

static int debug_level;
module_param(debug_level, int, 0660);
MODULE_PARM_DESC(debug_level,
    "Debug verbosity 0-3: 0=off, 1=info, 2=data path, 3=trace (default: 0)");

/* A convenience macro to avoid repeating the check everywhere */
#define MYDRV_DBG(level, fmt, ...)                          \
    do {                                                    \
        if (debug_level >= (level))                         \
            pr_info("mydrv: " fmt, ##__VA_ARGS__);         \
    } while (0)

static int __init mydrv_init(void)
{
    if (debug_level < 0 || debug_level > 3) {
        pr_err("mydrv: debug_level must be 0-3, got %d\n", debug_level);
        return -EINVAL;
    }

    MYDRV_DBG(1, "driver loaded, debug_level=%d\n", debug_level);
    MYDRV_DBG(2, "data path monitoring active\n");
    MYDRV_DBG(3, "full trace mode – expect high log volume\n");

    return 0;
}

static void __exit mydrv_exit(void)
{
    MYDRV_DBG(1, "driver unloaded\n");
}

module_init(mydrv_init);
module_exit(mydrv_exit);

The MYDRV_DBG macro keeps the code clean — you do not scatter if (debug_level >= X) checks everywhere. The macro is a zero-overhead wrapper when the compiler can see that debug_level is 0 and the condition is always false.

Tip: Because debug_level is registered with perm=0660, you can raise it on a running system without rebooting: echo 3 | sudo tee /sys/module/mydrv/parameters/debug_level. This is extremely useful when chasing intermittent bugs in a live system.

Making Module Parameters Persistent with modprobe.d

When you use insmod, parameters are one-time. When the system reboots or the module is reloaded by modprobe, it goes back to defaults. For production systems where you always want a specific debug level or device name, use the /etc/modprobe.d/ directory.

Create a file named after your module:

# Create /etc/modprobe.d/mydrv.conf
sudo nano /etc/modprobe.d/mydrv.conf

Add the options line:

# /etc/modprobe.d/mydrv.conf
# Set debug_level to 1 on every load, and device_name to eth_primary
options mydrv debug_level=1 device_name=eth_primary

Now every time modprobe mydrv runs (including at boot), it automatically applies these parameters. This is how real embedded Linux products are configured — no source changes, no recompilation, just a config file.

Module Parameter Flow – Boot Time vs Manual Load
System Boot
systemd / initramfs reads /etc/modprobe.d/*.conf
modprobe mydrv
(with params)
Manual Load
sudo modprobe mydrv (reads same conf file)
Params applied
automatically

Robust Parameter Validation in module_init()

The kernel enforces type checking but not value range checking. If your driver expects debug_level to be between 0 and 3 and a user passes 999, the kernel happily accepts it. Your init function must catch this.

The right approach is to check every parameter at the very start of module_init() before doing anything else. If a parameter is invalid, print a clear error message and return -EINVAL:

static int __init validated_init(void)
{
    /* Validate debug_level range */
    if (debug_level < 0 || debug_level > 3) {
        pr_err("mydrv: invalid debug_level=%d (valid: 0-3)\n", debug_level);
        return -EINVAL;
    }

    /* Validate string parameter is not empty */
    if (!device_name || device_name[0] == '\0') {
        pr_err("mydrv: device_name cannot be empty\n");
        return -EINVAL;
    }

    /* Validate array count if using module_param_array */
    if (num_ports == 0) {
        pr_warn("mydrv: no ports specified, using defaults\n");
        /* ... set up defaults ... */
    } else if (num_ports > MAX_PORTS) {
        pr_err("mydrv: too many ports: %d (max %d)\n",
               num_ports, MAX_PORTS);
        return -EINVAL;
    }

    pr_info("mydrv: parameters validated OK\n");
    return 0;
}

Returning -EINVAL from module_init() causes insmod to report an error and the module is not loaded. The user sees:

insmod: ERROR: could not insert module mydrv.ko: Invalid argument

And dmesg shows your specific error message, which tells them exactly what went wrong.

Complete Driver Parameter Template for Linux 6.x

Here is a production-ready skeleton that combines everything covered in both parts of this tutorial. Use this as your starting point for any real device driver that needs runtime configuration.

// File: driver_template.c
// Complete module parameter template for Linux 6.x
// EmbeddedPathashala – Free Linux Device Drivers Course
//
// Covers:
//   - Integer parameter with range validation
//   - String parameter
//   - Boolean flag
//   - Array parameter for multi-instance support
//   - sysfs exposure with correct permissions
//   - modprobe.d compatible
//   - checkpatch.pl compliant

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("YourName <you@email.com>");
MODULE_DESCRIPTION("Device driver template with module parameters");
MODULE_VERSION("1.0");

/* ------ Parameters ------ */

#define MAX_INSTANCES  4

static int debug_level;
module_param(debug_level, int, 0660);
MODULE_PARM_DESC(debug_level, "Debug level 0-3 (default: 0)");

static char *drv_label = "mydrv0";
module_param(drv_label, charp, 0444);
MODULE_PARM_DESC(drv_label, "Driver label string (default: mydrv0)");

static bool loopback_mode;
module_param(loopback_mode, bool, 0660);
MODULE_PARM_DESC(loopback_mode, "Enable loopback test mode (default: N)");

static unsigned int irq_list[MAX_INSTANCES];
static int irq_count;
module_param_array(irq_list, uint, &irq_count, 0444);
MODULE_PARM_DESC(irq_list, "IRQ numbers for each instance (comma-separated)");

/* ------ Debug helper ------ */
#define DRV_DBG(lvl, fmt, ...)                              \
    do {                                                    \
        if (debug_level >= (lvl))                           \
            pr_info("mydrv: " fmt, ##__VA_ARGS__);         \
    } while (0)

/* ------ Validation ------ */
static int validate_params(void)
{
    if (debug_level < 0 || debug_level > 3) {
        pr_err("mydrv: debug_level must be 0-3\n");
        return -EINVAL;
    }
    if (!drv_label || strlen(drv_label) == 0) {
        pr_err("mydrv: drv_label cannot be empty\n");
        return -EINVAL;
    }
    if (irq_count > MAX_INSTANCES) {
        pr_err("mydrv: too many IRQs (max %d)\n", MAX_INSTANCES);
        return -EINVAL;
    }
    return 0;
}

/* ------ Init ------ */
static int __init drv_init(void)
{
    int ret, i;

    ret = validate_params();
    if (ret)
        return ret;

    DRV_DBG(1, "loaded label=%s debug=%d loopback=%d irqs=%d\n",
            drv_label, debug_level, loopback_mode, irq_count);

    if (loopback_mode)
        DRV_DBG(1, "loopback mode active – TX loops back to RX\n");

    for (i = 0; i < irq_count; i++)
        DRV_DBG(2, "instance[%d]: IRQ=%u\n", i, irq_list[i]);

    return 0;
}

/* ------ Exit ------ */
static void __exit drv_exit(void)
{
    DRV_DBG(1, "unloaded\n");
}

module_init(drv_init);
module_exit(drv_exit);

Real-World Use Cases – Where Module Parameters Are Used in Linux Drivers

Module parameters are not just a learning exercise. The mainline Linux kernel uses them extensively in real drivers:

Ethernet Drivers (e.g., e1000, ixgbe)

Intel’s Gigabit and 10-Gigabit Ethernet drivers accept parameters like the number of receive/transmit descriptors, interrupt throttle rate, and auto-negotiation settings. These allow system administrators to tune network performance for high-throughput servers without recompiling the driver.

USB Host Controller Drivers

USB drivers use boolean module parameters to enable or disable quirk workarounds for specific broken hardware. For example, a parameter like ignore_oc=1 can disable overcurrent protection on boards where the detection circuit is unreliable.

GPIO and I2C Drivers on Embedded Systems

On embedded systems that do not use Device Tree for hardware description, GPIO and I2C drivers often use module parameters to specify which GPIO pin number or which I2C bus address to use. This avoids hardcoding board-specific values into the driver itself.

Serial Port (UART) Drivers

Legacy ISA serial port drivers use module_param_array() to accept a list of I/O port addresses and IRQ numbers for each UART instance. This is exactly the multi-instance pattern demonstrated in this tutorial.

Comparison: module_param() vs module_param_array() vs Sysfs Custom Attribute

Feature module_param() module_param_array() Custom sysfs attribute
Values per parameter One Multiple (up to array size) Anything you write
Set at load time Yes Yes No (after load only)
Runtime modification Yes (if perm != 0) Yes (if perm != 0) Yes (full control)
Code complexity Very low Low High (manual store/show)
Type validation Automatic (basic) Automatic (basic) Manual (you implement it)
modprobe.d support Yes Yes No
Best for Simple scalar config Multi-instance config Complex runtime state

Key Takeaways from Both Lectures

  • Use module_param() for single-value runtime configuration — it is the simplest and most readable approach.
  • Use module_param_array() when your driver supports multiple hardware instances that each need their own configuration value.
  • The debug_level integer pattern with a helper macro is the cleanest way to handle conditional verbose logging in a kernel driver.
  • Always validate parameter values inside module_init() and return -EINVAL for out-of-range inputs.
  • Use /etc/modprobe.d/ to make parameters persistent across reboots — the right way to configure production systems.
  • Keep string parameters read-only (0444) at the sysfs level unless you have carefully implemented safe memory handling for writes.
  • Module parameters are widely used in real Linux drivers and mastering them is a core skill for anyone taking a free Linux device drivers course.

Frequently Asked Questions

Q1: Can I add new module parameters to a driver that is already loaded?

No. Module parameters are part of the module’s compiled structure. You cannot add or remove them without recompiling and reloading the module. However, you can change the values of existing parameters at runtime through their sysfs files if they were registered with write permission.

Q2: What happens if the user passes more values than the array size?

The kernel rejects the extra values and returns an error at insmod time. The module is not loaded. This is why you define a maximum array size constant (like MAX_INSTANCES) and mention it in MODULE_PARM_DESC() so users know the limit upfront.

Q3: Is it safe to read a module parameter from an interrupt context?

Reading a simple integer or boolean parameter from an interrupt handler is safe because reads of aligned native-width integers are atomic on most architectures. However, if another CPU is simultaneously writing the parameter through sysfs, you could read a stale value. For interrupt-safe runtime configuration, consider using READ_ONCE() and WRITE_ONCE() macros around accesses to parameter variables that might change at runtime.

Q4: How do I set a default value for an array parameter?

Initialise the array in the declaration. For example: static unsigned int irq_list[MAX_INSTANCES] = {5, 10, 0, 0};. If the user does not pass irq_list at load time, num_ports will be 0 and your init function should detect that and use the defaults manually. The kernel does not auto-apply initialised values to the count — you manage that logic yourself.

Q5: Can I use module parameters in a built-in kernel driver (not a loadable module)?

Yes. For built-in drivers the same macros work. Instead of being passed at insmod time, the values are read from the kernel command line at boot time in the format drivername.paramname=value. This is how many built-in kernel subsystems are configured in embedded Linux products.

Q6: What is the difference between module parameters and Device Tree properties?

Device Tree properties are static hardware descriptions baked into the DTB at build time. Module parameters are runtime configuration values passed by a human or a script when loading the driver. For modern ARM-based embedded systems, hardware description (IRQ numbers, I2C addresses, GPIO pins) should come from Device Tree. Module parameters are better suited for tunable behaviour like debug levels, timeout values, or feature flags.

Keep Learning – Completely Free Embedded Systems Course

This is part of EmbeddedPathashala’s free Linux kernel programming and device drivers course. No fees, no registration — just practical, hands-on content for embedded systems engineers.

Free Linux Kernel Course Index Free Embedded Systems Course

Leave a Reply

Your email address will not be published. Required fields are marked *