模块参数与导出符号:让驱动更灵活
写驱动的时候,我经常遇到一个尴尬的情况:同一个驱动,在不同硬件上要配置不同的参数。比如中断号、缓冲区大小、采样率这些。难道每次都要重新编译内核模块?
当然不用。Linux 内核提供了两个很实用的机制——模块参数和导出符号。说白了,一个让用户能在加载模块时传参,另一个让模块之间能共享函数和变量。
今天我们就来聊聊这两个东西。我会结合我实际踩过的坑,带你一次性搞明白。
模块参数:让驱动学会「听话」
先说说模块参数。它的作用很简单:在 insmod 或 modprobe 加载模块时,动态传入配置值。
比如你写了一个字符设备驱动,缓冲区大小想留给用户决定。那你就可以定义一个模块参数:
#include <linux/module.h>
#include <linux/moduleparam.h>
static int buffer_size = 1024;
module_param(buffer_size, int, 0644);
MODULE_PARM_DESC(buffer_size, "缓冲区大小,默认1024字节");
加载的时候这样用:
sudo insmod my_driver.ko buffer_size=4096
嗯,这里要注意:module_param 的第三个参数是文件权限。0644 表示在 /sys/module/ 下能看到这个参数,并且 root 可以改。如果你不想暴露给用户空间,传 0 就行。
参数类型与数组参数
模块参数支持的类型不少:int、long、charp(字符串指针)、bool 等等。我列个表给你看:
| 类型宏 | C 类型 | 说明 |
|---|---|---|
| module_param(name, int, perm) | int | 整型参数 |
| module_param(name, long, perm) | long | 长整型 |
| module_param(name, charp, perm) | char * | 字符串指针 |
| module_param(name, bool, perm) | bool | 布尔值 |
| module_param_array(name, type, num, perm) | 数组 | 数组参数,num 返回元素个数 |
数组参数稍微特殊一点。我记得有一次我需要传多个 GPIO 号,就用到了数组参数:
static int gpio_pins[4];
static int gpio_count;
module_param_array(gpio_pins, int, &gpio_count, 0644);
MODULE_PARM_DESC(gpio_pins, "GPIO引脚列表,最多4个");
加载时这样传:
sudo insmod my_driver.ko gpio_pins=17,22,27,5
内核会自动解析逗号分隔的值,gpio_count 会变成 4。
导出符号:让模块之间「互通有无」
模块参数是让用户和驱动交互。那驱动和驱动之间怎么交互?
答案就是 EXPORT_SYMBOL。它可以把一个函数或变量导出,让其他模块也能调用。
举个例子。你写了一个底层硬件操作模块,封装了 SPI 读写函数。另一个模块想用这些函数,就可以通过导出符号来实现。
// spi_core.c
#include <linux/module.h>
int spi_read_reg(unsigned int addr)
{
// 硬件操作...
return 0;
}
EXPORT_SYMBOL(spi_read_reg);
int spi_write_reg(unsigned int addr, int val)
{
// 硬件操作...
return 0;
}
EXPORT_SYMBOL(spi_write_reg);
另一个模块使用时,只需要声明一下:
// sensor_driver.c
extern int spi_read_reg(unsigned int addr);
extern int spi_write_reg(unsigned int addr, int val);
void sensor_init(void)
{
int val = spi_read_reg(0x10);
// ...
}
编译时,两个模块各自编译成 .ko。加载时先加载 spi_core.ko,再加载 sensor_driver.ko。
EXPORT_SYMBOL_GPL 与 EXPORT_SYMBOL 的区别
你可能还会看到 EXPORT_SYMBOL_GPL。它和 EXPORT_SYMBOL 的区别只有一个:许可证限制。
EXPORT_SYMBOL:任何模块都能用,不管它是什么许可证。EXPORT_SYMBOL_GPL:只有 GPL 许可证的模块才能用。
我个人习惯:能加 GPL 就加 GPL。这不仅是法律层面的考虑,也是一种技术态度——你用了我的代码,你的模块也得开源。
当然,如果你写的是公司内部驱动,不想被外部模块依赖,用 EXPORT_SYMBOL_GPL 也能起到一定的隔离作用。
多文件模块的编译
实际项目中,驱动代码不可能全写在一个文件里。比如 spi_core.c、i2c_core.c、gpio_utils.c 各司其职,最后编译成一个 .ko。
怎么做?很简单,在 Makefile 里列出所有源文件:
obj-m := my_driver.o
my_driver-objs := main.o spi_core.o gpio_utils.o
这里 my_driver-objs 指定了组成最终模块的目标文件。内核构建系统会自动编译每个 .c 文件,然后链接成 my_driver.ko。
完整的 Makefile 示例:
obj-m := my_driver.o
my_driver-objs := main.o spi_core.o gpio_utils.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
多文件模块的符号可见性
多文件编译时,有一个细节容易忽略:文件之间的符号可见性。
默认情况下,同一个模块内的 .c 文件之间,函数和全局变量是互相可见的。但如果你用 static 修饰了函数或变量,它就只在当前文件可见。
举个例子:
// spi_core.c
static int spi_lock; // 只在 spi_core.c 可见
int spi_read_reg(unsigned int addr) { ... } // 模块内可见
EXPORT_SYMBOL(spi_read_reg); // 所有模块可见
所以,模块内部共享的函数,不要加 static,也不要加 EXPORT_SYMBOL。只有你想让其他模块调用的函数,才需要导出。
本章知识体系
下面这张图总结了模块参数和导出符号的核心逻辑:
这张图把三个核心知识点串在了一起。左边是模块参数,右边是导出符号,下面是多文件编译。你写驱动时,这三者经常要配合使用。
好了,这一章的内容就到这里。模块参数和导出符号是驱动开发中非常实用的工具。掌握了它们,你的驱动就不再是「死代码」,而是可以灵活配置、模块化组合的「活代码」。
公众号:蓝海资料掘金营,微信deep3321