Linux Kernel Module Parameters, Stacking, and Library-like Features
Free Linux Kernel Programming Course — LKMs Part 2, Section 2
Intermediate
~22 min
Linux 6.x
Free
What You Will Learn
- How to pass parameters to a Linux kernel module at load time using
module_param - How to pass arrays of values to a module using
module_param_array - How to expose module parameters to sysfs so they can be changed at runtime
- What module stacking is, why it exists, and how to implement it using
EXPORT_SYMBOL - How to emulate library-like shared code between multiple kernel modules
- Why floating point arithmetic is not allowed in the kernel and what to use instead
- How to gather system information from within a kernel module
Passing Parameters to a Linux Kernel Module
In userspace, you pass arguments to a program on the command line. In the kernel, the equivalent mechanism for Linux kernel modules is module parameters. You can pass values to a module when you load it with insmod or modprobe, and you can even change some values at runtime through the /sys/module/ filesystem.
Module parameters are one of the most practical features you will use as a Linux kernel module developer. They let you make your module configurable without recompiling — you can change debug levels, buffer sizes, GPIO pin numbers, timeout values, and more.
The module_param Macro
The primary macro for declaring a module parameter is module_param(). It takes three arguments: the variable name, the type, and the permissions for the sysfs entry.
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Module Parameters Demo - Free Linux Kernel Course");
/* Declare parameters with default values */
static int debug_level = 0;
static char *device_name = "ep_device";
static int timeout_ms = 1000;
/* Register the parameters */
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Set debug verbosity level (0=off, 1=basic, 2=verbose)");
module_param(device_name, charp, 0444);
MODULE_PARM_DESC(device_name, "Name of the device to manage");
module_param(timeout_ms, int, 0644);
MODULE_PARM_DESC(timeout_ms, "Operation timeout in milliseconds (default: 1000)");
static int __init param_demo_init(void)
{
pr_info("param_demo: loaded\n");
pr_info(" debug_level = %d\n", debug_level);
pr_info(" device_name = %s\n", device_name);
pr_info(" timeout_ms = %d\n", timeout_ms);
return 0;
}
static void __exit param_demo_exit(void)
{
pr_info("param_demo: unloaded\n");
}
module_init(param_demo_init);
module_exit(param_demo_exit);
Loading a Module with Parameters
# Load the module with custom parameter values
sudo insmod param_demo.ko debug_level=2 device_name="my_sensor" timeout_ms=500
# Check the kernel log
sudo dmesg | tail -10
# Output:
# param_demo: loaded
# debug_level = 2
# device_name = my_sensor
# timeout_ms = 500
# View the parameters in sysfs (they appear here after loading)
ls /sys/module/param_demo/parameters/
# debug_level timeout_ms
# (device_name is not shown here because its permission is 0444 = read-only
# from user POV but 0444 means it IS readable — check carefully)
# Read the current value of a parameter
cat /sys/module/param_demo/parameters/debug_level
# 2
# Change a parameter at runtime (only if sysfs permission allows write)
echo 1 > /sys/module/param_demo/parameters/debug_level
cat /sys/module/param_demo/parameters/debug_level
# 1
| Permission Value | Meaning | Typical Use |
|---|---|---|
| 0 | No sysfs entry created | Parameter set only at load time, not visible after |
| 0444 | sysfs entry readable by all, not writable | Read-only info: device name, hardware IDs |
| 0644 | Root can read/write, others can read | Tunable at runtime: debug level, timeouts |
| 0600 | Root only — read and write | Security-sensitive parameters |
0666 (world-writable) for module parameters. This allows any unprivileged user to change kernel behavior. For tunable parameters, use 0644 at most — only root should be able to change kernel module settings.
Supported Parameter Types
| Type String | C Type | Notes |
|---|---|---|
bool |
bool |
Accepts 0/1, y/n, true/false |
int |
int |
Signed 32-bit integer |
uint |
unsigned int |
Unsigned 32-bit integer |
long |
long |
Signed long |
ulong |
unsigned long |
Unsigned long |
charp |
char * |
String pointer — kernel allocates memory for it |
short |
short |
Signed 16-bit |
ushort |
unsigned short |
Unsigned 16-bit |
Passing Arrays as Module Parameters
Sometimes you need to pass multiple values for a single parameter — like a list of GPIO pin numbers or IRQ numbers. The module_param_array macro handles this.
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
#define MAX_PINS 8
static int gpio_pins[MAX_PINS] = {4, 17, 27, 22, 0, 0, 0, 0};
static int num_pins = 0; /* will be filled by the kernel */
/* Declare array parameter */
module_param_array(gpio_pins, int, &num_pins, 0644);
MODULE_PARM_DESC(gpio_pins, "List of GPIO pin numbers (up to 8)");
static int __init array_param_init(void)
{
int i;
pr_info("array_param: %d pin(s) specified\n", num_pins);
for (i = 0; i < num_pins; i++)
pr_info(" gpio_pins[%d] = %d\n", i, gpio_pins[i]);
return 0;
}
static void __exit array_param_exit(void)
{
pr_info("array_param: unloaded\n");
}
module_init(array_param_init);
module_exit(array_param_exit);
# Pass multiple GPIO pins at load time (comma-separated)
sudo insmod array_param.ko gpio_pins=4,17,27,22
# Check output
sudo dmesg | tail -10
# array_param: 4 pin(s) specified
# gpio_pins[0] = 4
# gpio_pins[1] = 17
# gpio_pins[2] = 27
# gpio_pins[3] = 22
module_param_array is a pointer to an integer that the kernel fills with the actual number of values passed. Always use this count variable instead of assuming all array slots were filled — users may pass fewer values than the maximum.
Understanding Linux Kernel Module Stacking
Module stacking is the mechanism that allows one Linux kernel module to use functions and data exported by another kernel module. It is how the kernel achieves modularity at the driver level — think of it as building a dependency chain between modules.
The most common real-world example of module stacking is the USB subsystem: usbcore exports functions that all USB device drivers (like usb-storage or cdc_acm) depend on. Another example is the I2C subsystem — i2c-core exports functions used by every I2C device driver.
| Kernel Module Dependency Stack | ||
|
my_i2c_sensor.ko
(your device driver)
calls: i2c_transfer(), i2c_add_adapter()
|
depends on ↓ |
i2c-core.ko
(I2C subsystem)
exports: i2c_transfer(), i2c_add_adapter()
|
| i2c-core.ko must be loaded BEFORE my_i2c_sensor.ko | modprobe handles this automatically via modules.dep | ||
How to Export Symbols from a Kernel Module
To make a function or variable in your module available for other modules to use, you export it with EXPORT_SYMBOL() or EXPORT_SYMBOL_GPL(). The GPL version restricts use to GPL-licensed modules only.
/* === helper_module.c — the "library" module === */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Helper module exporting shared functions - Free Linux Kernel Course");
/* A shared function — available to other modules */
int ep_add_numbers(int a, int b)
{
pr_info("helper_module: ep_add_numbers(%d, %d) called\n", a, b);
return a + b;
}
EXPORT_SYMBOL_GPL(ep_add_numbers);
/* A shared variable */
int ep_shared_counter = 0;
EXPORT_SYMBOL_GPL(ep_shared_counter);
static int __init helper_init(void)
{
pr_info("helper_module: loaded and symbols exported\n");
return 0;
}
static void __exit helper_exit(void)
{
pr_info("helper_module: unloaded\n");
}
module_init(helper_init);
module_exit(helper_exit);
/* === consumer_module.c — the module that depends on helper_module === */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Consumer module using helper_module - Free Linux Kernel Course");
/* Declare the external symbols we will use */
extern int ep_add_numbers(int a, int b);
extern int ep_shared_counter;
static int __init consumer_init(void)
{
int result;
pr_info("consumer_module: loaded\n");
result = ep_add_numbers(10, 32);
pr_info("consumer_module: ep_add_numbers(10, 32) = %d\n", result);
ep_shared_counter++;
pr_info("consumer_module: ep_shared_counter = %d\n", ep_shared_counter);
return 0;
}
static void __exit consumer_exit(void)
{
pr_info("consumer_module: unloaded\n");
}
module_init(consumer_init);
module_exit(consumer_exit);
Loading Stacked Modules in the Right Order
# Step 1: Load the helper module first (it exports the symbols)
sudo insmod helper_module.ko
# Step 2: Load the consumer module (it uses the exported symbols)
sudo insmod consumer_module.ko
# Check the output
sudo dmesg | tail -10
# helper_module: loaded and symbols exported
# consumer_module: loaded
# helper_module: ep_add_numbers(10, 32) called
# consumer_module: ep_add_numbers(10, 32) = 42
# consumer_module: ep_shared_counter = 1
# To unload, reverse the order — consumer first, then helper
sudo rmmod consumer_module
sudo rmmod helper_module
# What happens if you try to rmmod helper_module while consumer is loaded?
sudo rmmod helper_module
# rmmod: ERROR: Module helper_module is in use by: consumer_module
EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL
| Macro | Who Can Use It | When to Use |
|---|---|---|
EXPORT_SYMBOL() |
Any module, including proprietary ones | For general utility symbols that should be universally accessible |
EXPORT_SYMBOL_GPL() |
Only GPL-licensed modules | For core kernel subsystem functions — most in-tree kernel exports use this |
EXPORT_SYMBOL_GPL(). If you are writing open-source Linux kernel code — which is the recommended practice — all your modules should have MODULE_LICENSE("GPL") and you can use all GPL-exported symbols freely.
Checking Module Dependencies
# After make install and depmod -a, check dependencies
modinfo consumer_module | grep depends
# depends: helper_module
# See the full dependency tree
modprobe --show-depends consumer_module
# insmod /lib/modules/6.8.0/extra/helper_module.ko
# insmod /lib/modules/6.8.0/extra/consumer_module.ko
# modprobe loads both automatically in the right order
sudo modprobe consumer_module
Emulating Library-like Features for Kernel Modules
In userspace, you share code between programs using shared libraries (.so files). In the kernel, there are no shared libraries in that sense. However, you can achieve a similar effect using the module stacking mechanism shown above — create a “library module” that exports commonly used functions, and have multiple other modules depend on it.
This pattern is widely used in the Linux kernel itself. The best examples are:
- libata — a library module for SATA/PATA drivers. All SATA host controller drivers use it.
- i2c-core — provides the core I2C framework used by all I2C device drivers.
- spi-core — provides the SPI bus framework.
- regmap — a generic register access library used by audio codecs, PMICs, and other register-based chips.
Building a Practical Library Module
Here is a more realistic example — a logging/tracing helper module that multiple driver modules in a product can share:
/* === ep_logger.c — shared logging helper module === */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h> /* for kmalloc */
#include <linux/string.h>
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Shared logging helper - EmbeddedPathashala Free Kernel Course");
/* Configurable log prefix */
static char log_prefix[32] = "[EP]";
module_param_string(log_prefix, log_prefix, sizeof(log_prefix), 0644);
MODULE_PARM_DESC(log_prefix, "Prefix string for all log messages");
/*
* ep_log_info - shared logging function
* Called by other modules via EXPORT_SYMBOL_GPL
*/
void ep_log_info(const char *module_name, const char *msg)
{
pr_info("%s [%s]: %s\n", log_prefix, module_name, msg);
}
EXPORT_SYMBOL_GPL(ep_log_info);
void ep_log_warn(const char *module_name, const char *msg)
{
pr_warn("%s [%s] WARN: %s\n", log_prefix, module_name, msg);
}
EXPORT_SYMBOL_GPL(ep_log_warn);
static int __init ep_logger_init(void)
{
pr_info("ep_logger: ready, prefix='%s'\n", log_prefix);
return 0;
}
static void __exit ep_logger_exit(void)
{
pr_info("ep_logger: unloaded\n");
}
module_init(ep_logger_init);
module_exit(ep_logger_exit);
Gathering System Information from Inside a Kernel Module
A common need — especially during debugging — is to print useful system information from within a kernel module. The kernel provides several ways to do this. Knowing these is part of writing practical Linux kernel modules.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/utsname.h> /* for utsname() */
#include <linux/cpumask.h> /* for num_online_cpus() */
#include <linux/mm.h> /* for si_meminfo() */
MODULE_LICENSE("GPL");
static int __init sysinfo_init(void)
{
struct sysinfo si;
/* Kernel version and hostname */
pr_info("sysinfo: kernel version = %s\n", utsname()->release);
pr_info("sysinfo: hostname = %s\n", utsname()->nodename);
pr_info("sysinfo: machine arch = %s\n", utsname()->machine);
/* CPU count */
pr_info("sysinfo: online CPUs = %d\n", num_online_cpus());
pr_info("sysinfo: possible CPUs = %d\n", num_possible_cpus());
/* Memory info */
si_meminfo(&si);
pr_info("sysinfo: total RAM = %lu pages (%lu MB)\n",
si.totalram,
(si.totalram * si.mem_unit) / (1024 * 1024));
pr_info("sysinfo: free RAM = %lu pages (%lu MB)\n",
si.freeram,
(si.freeram * si.mem_unit) / (1024 * 1024));
return 0;
}
static void __exit sysinfo_exit(void) { }
module_init(sysinfo_init);
module_exit(sysinfo_exit);
# Load and check output
sudo insmod sysinfo.ko
sudo dmesg | tail -10
# sysinfo: kernel version = 6.8.0-51-generic
# sysinfo: hostname = my-dev-board
# sysinfo: machine arch = aarch64
# sysinfo: online CPUs = 4
# sysinfo: possible CPUs = 4
# sysinfo: total RAM = 2031616 pages (7936 MB)
# sysinfo: free RAM = 1200000 pages (4687 MB)
Why Floating Point Is Not Allowed in Linux Kernel Code
This surprises many developers coming from userspace. You cannot use float or double in Linux kernel module code. If you try, the compiler will allow it but the behavior at runtime will be incorrect or the kernel will crash.
The Technical Reason
When a userspace process uses floating point operations, the CPU’s Floating Point Unit (FPU) registers are automatically saved and restored as part of context switches (the kernel saves the FPU state as part of the process’s register context). In the kernel itself, this automatic FPU save/restore is not in effect. If kernel code modifies the FPU registers and a context switch happens, the userspace process that gets scheduled next will find its FPU state corrupted — leading to wrong results or crashes in that process.
| Without FPU Guard in Kernel | Correct Approach |
|---|---|
|
|
What to Use Instead of Floating Point
Almost all cases where you think you need floating point in kernel code can be solved with integer arithmetic and fixed-point math:
/* === Userspace approach (NOT allowed in kernel) === */
float temperature = raw_adc_value * 0.0625; /* WRONG: no float in kernel */
/* === Kernel approach: scale up, do integer math, scale down === */
/*
* Example: ADC reads raw value, 1 LSB = 0.0625 degrees C
* Instead of multiplying by 0.0625, multiply by 10000 and
* divide by 160000 (= 10000 / 0.0625) to get millidegrees
*/
int raw = 512; /* example ADC reading */
/* Represent 0.0625 as 625/10000 */
int temp_millidegrees = (raw * 625) / 10;
/* = 512 * 625 / 10 = 32000 millidegrees = 32.000 degrees C */
pr_info("Temperature: %d.%03d degrees C\n",
temp_millidegrees / 1000,
temp_millidegrees % 1000);
/* Output: Temperature: 32.000 degrees C */
/* Another common pattern: percentage calculation without float */
/* percentage = (value * 100) / total */
unsigned long used = 3072; /* bytes used */
unsigned long total = 8192; /* total bytes */
unsigned int pct = (used * 100) / total;
pr_info("Usage: %u%%\n", pct); /* Usage: 37% */
kernel_fpu_begin() and fpu_end() for cases where float is absolutely necessary (like certain crypto or SIMD operations). However, this is an advanced technique that saves/restores FPU state manually and adds significant overhead. For sensor drivers, data conversion, or general logic, always use integer arithmetic.
Common Mistakes with Module Parameters and Module Stacking
- Using non-static variables for module parameters: Module parameter variables should always be declared
static. This limits their scope to the module’s translation unit and avoids symbol conflicts with the global kernel namespace. - Not providing MODULE_PARM_DESC: Always document your parameters with
MODULE_PARM_DESC(). This description shows up inmodinfooutput and is essential for users of your module to understand what each parameter does. - Loading consumer before helper: If you use
insmoddirectly, you must load dependencies manually in the right order. Usemodprobeinstead — it reads the dependency database and loads modules in the correct sequence automatically. - Forgetting
externdeclarations for stacked symbols: When a module uses an exported symbol from another module, declare it asexternin the consumer’s source. Without this, the compiler will warn about implicit function declarations. - Using float for sensor math: Always convert to scaled integer representation. This is a discipline that experienced kernel developers apply automatically.
Performance Considerations
- Module parameter reads in hot paths: Reading a module parameter variable in a very frequently called function (interrupt handler, packet processing loop) is fine — it is just a global variable read. However, avoid making the parameter itself a sysfs-backed value if the read path is ultra-latency-sensitive.
- Module stacking overhead: There is essentially no overhead to calling an exported function from another module. Once loaded, the symbol is resolved to a direct function pointer — there is no dynamic lookup at call time.
- Integer vs float: Integer arithmetic in the kernel is inherently faster than the FPU-save/restore dance required for floating point, making the “no float” rule a performance benefit as well as a correctness requirement.
Key Takeaways
module_param()andmodule_param_array()let you pass configuration values to kernel modules at load time, and optionally change them at runtime via/sys/module/.- Always use appropriate sysfs permissions for module parameters — never world-writable.
- Module stacking allows one kernel module to export functions for other modules to use, using
EXPORT_SYMBOL_GPL(). This is how kernel subsystems like I2C, SPI, and USB are organized. - Floating point arithmetic is not safe in kernel code. Use scaled integer math instead.
- Use
modproberather thaninsmodfor modules with dependencies — it handles load order automatically using the dependency database built bydepmod. - Gathering system info (kernel version, CPU count, memory) from within a module is straightforward using
utsname(),num_online_cpus(), andsi_meminfo().
Frequently Asked Questions
/sys/module/<module_name>/parameters/<param_name>. The change takes effect immediately for the next read of that variable by the module. Be careful with parameters that affect hardware state — changing them at runtime may require additional synchronization logic in your module.EXPORT_SYMBOL() makes a symbol available to any kernel module, including proprietary (non-GPL) ones. EXPORT_SYMBOL_GPL() restricts the symbol to GPL-licensed modules only. The kernel enforces this at load time — if a non-GPL module tries to use a GPL-only symbol, the load is refused with an error about license taint. Most core kernel subsystem exports use EXPORT_SYMBOL_GPL().kernel_fpu_begin() explicitly saves the current FPU state before using floating point or SIMD instructions, and kernel_fpu_end() restores it. This makes FPU usage safe by preventing corruption of the task’s FPU state. It is used in performance-critical kernel subsystems like AES-NI cryptography (which uses AES hardware via SSE registers) and video codec drivers. It is not available on all architectures and should only be used when there is a strong performance justification and no integer alternative.Authoritative References
- Linux Kernel Driver API: Basics (kernel.org)
- Linux Kernel Symbol Namespaces (kernel.org)
- Linux Kernel Core API Reference (kernel.org)
Free Linux Kernel Programming — Keep Learning
This is part of EmbeddedPathashala’s completely free Linux kernel development course. No fees, no login. Just practical kernel programming knowledge updated for Linux 6.x.
View Full Course Index Subscribe on YouTube