[TOC]

kernel/params.c 内核模块参数(Kernel Module Parameters) 实现模块加载时参数传递

历史与背景

这项技术是为了解决什么特定问题而诞生的?

kernel/params.c 中的代码实现了一个核心的内核功能:模块参数(Module Parameters)。这项技术的诞生是为了解决内核模块灵活性和可配置性的问题。

在早期,内核模块的行为通常是硬编码的。如果需要调整一个参数(例如,一个驱动的调试打印级别,或者一个硬件设备的特定配置选项),唯一的办法就是修改源代码,然后重新编译整个模块。这极大地降低了软件的灵活性和可重用性。

模块参数机制的出现,就是为了提供一个标准化的接口,允许用户在加载模块时从外部向模块传递配置值,而无需重新编译。这类似于给用户空间的应用程序传递命令行参数。该机制后来也扩展到了内核自身,允许在系统启动时通过内核引导命令行传递参数。

它的发展经历了哪些重要的里程碑或版本迭代?

模块参数机制是随着Linux内核模块化系统一起演进的。

  • 基本实现:最初的实现提供了一组宏(如 MODULE_PARM),允许开发者将模块内的变量“导出”为参数,支持整数、字符串等基本类型。
  • 标准化宏的引入:后来引入了 module_param(name, type, perm)module_param_named(name, value, type, perm)module_param_array(name, type, num, perm) 等一系列更加强大和易用的宏。这些宏简化了参数的定义,并增加了对数组类型和权限控制的支持。
  • 与Sysfs的集成:这是一个关键的里程碑。当模块被加载后,它的参数会自动在sysfs文件系统中以文件的形式出现,路径通常为 /sys/module/<module_name>/parameters/<param_name>。这不仅允许用户查看参数的当前值,还允许在运行时动态地修改那些被赋予了写权限的参数,极大地增强了系统的动态可配置性。params.c 中的代码提供了实现这种读写操作的底层函数。
  • 自定义类型的支持:框架被设计为可扩展的。开发者可以通过提供自定义的 setget 回调函数,来支持任意复杂的数据类型作为模块参数。

目前该技术的社区活跃度和主流应用情况如何?

模块参数是Linux内核驱动和子系统开发中一项基础性、极其稳定且被普遍使用的功能。它不是一个可选的库,而是编写可配置内核模块的标准范式。几乎所有的内核驱动程序都使用模块参数来暴露调试选项、硬件配置、功能开关等。任何向内核提交代码的开发者都必须熟悉这一机制。

核心原理与设计

它的核心工作原理是什么?

params.c 的核心原理是基于编译时元数据生成加载时参数解析

  1. 编译时:元数据生成

    • 当开发者在模块代码中使用 module_param() 宏时,C预处理器会将其展开。
    • 这个展开的宏定义了一个 struct kernel_param 类型的静态变量。这个结构体包含了参数的所有元信息:参数名(字符串)、指向模块中实际存储参数值的变量的指针、参数类型(通过一组标准的回调函数表示)、以及在sysfs中的文件权限。
    • 最关键的一步是,编译器会将这个 struct kernel_param 实例放入一个特殊的ELF段(section)中。
  2. 加载时:解析与赋值

    • 当用户使用 insmodmodprobe 加载模块时(例如 insmod mydriver.ko debug_level=1),内核的模块加载器 (kernel/module.c) 会解析模块的ELF文件。
    • 加载器会找到那个存放 kernel_param 结构的特殊段,并遍历其中的所有参数元数据。
    • 同时,加载器会解析用户在命令行上传递的 key=value 形式的参数。
    • 对于每个用户传入的参数,加载器会在模块的参数元数据列表中按名字查找匹配项。
    • 如果找到匹配,加载器会调用与该参数类型关联的 set 函数(这些函数由 params.c 提供,如 param_set_int, param_set_bool)。set 函数负责将用户提供的字符串值(如 “1”)转换成对应的C类型(如 int 1),然后通过元数据中存储的指针,将转换后的值写入模块的全局变量中。
  3. 运行时:Sysfs交互

    • 模块成功加载后,模块子系统会为每个参数在 /sys/module/.../parameters/ 目录下创建一个文件。
    • 当用户空间程序 readwrite 这个文件时,VFS层会最终调用到与该参数类型关联的 getset 函数,从而实现对模块内部变量的运行时读写。

它的主要优势体现在哪些方面?

  • 灵活性与可配置性:无需重新编译即可改变模块行为。
  • 简单易用:为内核开发者提供了极其简单的宏接口,隐藏了所有复杂性。
  • 类型安全:框架负责处理字符串到具体类型的转换,减少了驱动中的模板代码和出错可能。
  • 运行时交互:与sysfs的无缝集成为参数提供了标准的运行时查看和修改接口。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 静态定义:模块的参数必须在编译时定义好,不能在运行时动态添加或删除参数。
  • 配置而非命令:该机制主要用于设置“值”或“状态”,不适合用于触发复杂的操作或命令。对于后者,ioctl 等机制更合适。
  • 不适合大数据量:不适合用于在用户空间和内核之间传输大量或高频率的数据。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

模块参数是内核模块暴露简单配置选项的标准和首选方案。

  • 开启调试功能:一个网络驱动可以通过一个布尔型参数 debug 来控制是否打印详细的调试日志。insmod e1000.ko debug=1
  • 指定硬件资源:在硬件资源无法被内核自动探测的旧系统中,驱动可能需要通过参数来手动指定中断号(IRQ)或I/O端口地址。
  • 设置工作模式:一个无线网卡驱动可能有一个参数 wifi_mode,允许用户在加载时指定其工作在 AP(接入点)模式还是 Station(客户端)模式。
  • 覆盖默认值:模块可以有一个内部的默认配置,但允许用户通过参数在加载时覆盖它,例如一个缓冲区大小的默认值。

是否有不推荐使用该技术的场景?为什么?

  • 高频数据交换:例如,一个应用程序需要频繁地向一个驱动发送数据包。这种场景应该使用 write() 系统调用、netlink 套接字或 ioctl
  • 导出大量状态信息:如果需要向用户空间导出大量、复杂的、结构化的状态信息或统计数据,procfsdebugfs 是更合适的工具。
  • 设备特定命令:向一个打开的设备文件发送一个特定的命令(例如,让硬盘休眠),应该使用 ioctl 机制。模块参数是模块全局的,而 ioctl 是与一个具体的文件描述符关联的。

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 内核模块参数 (Module Parameters) Sysfs (直接创建属性) Procfs ioctl
抽象层次 。一个宏就完成了所有工作。 。需要手动编写show/store函数并注册属性。 。非常灵活,但需要编写完整的file_operations 。需要定义命令号和实现ioctl回调。
主要用途 模块加载时的配置,以及简单的运行时状态读/写。 表示设备模型中的属性(状态和配置)。 导出文本格式的进程和系统信息、统计数据。 向打开的设备文件发送命令和交换数据。
作用域 模块全局 设备或驱动全局 通常是全局每个进程 每个文件描述符
数据格式 强类型(int, bool, string, array等)。 文本格式,由show/store函数解释。 自由的文本格式。 二进制,任意结构。
使用场景 insmod mydrv.ko option=val cat /sys/devices/.../attr cat /proc/meminfo ioctl(fd, MY_COMMAND, &data)
总结 专为模块配置设计的简化版sysfs接口。 内核对象属性的标准表示方法。 用于信息导出和调试的传统接口。 面向设备的命令通道。

include/linux/moduleparam.h

core_param: 定义一个历史性的核心内核参数

core_param 宏的作用是定义一个核心内核参数。这些参数不隶属于任何特定的可加载模块,而是属于内核的核心部分。它们可以直接在内核启动的命令行中被设置(例如,由U-Boot传递),并且不像模块参数那样有模块名前缀。

正如注释所说,它主要用于兼容历史悠久的 __setup() 机制,为核心代码提供一个类型安全、有sysfs接口的现代化参数定义方式。

core_param 宏的分解:

1
2
3
4
5
#define core_param(name, var, type, perm)				\
/* 1. 类型检查 */
param_check_##type(name, &(var)); \
/* 2. 调用底层宏 */
__module_param_call("", name, &param_ops_##type, &var, perm, -1, 0)
  1. param_check_##type(name, &(var));:

    • 这是一个编译时的类型安全检查
    • ## 是C预处理器的“记号粘贴”(Token Pasting)操作符。它会将 param_check_ 和宏参数 type 的值(例如 int)粘贴在一起,形成一个新的函数名,如 param_check_int
    • 内核为每种基本类型都定义了这样的检查函数。param_check_int 会确保你传递给它的 var 变量确实是一个 int 类型的指针。如果类型不匹配,编译器会在这里产生一个警告或错误,从而在早期就捕捉到潜在的bug。
  2. __module_param_call("", name, &param_ops_##type, &var, perm, -1, 0):

    • 这是对底层核心宏的调用,传递了特定的参数来表现出 core_param 的行为。
    • "": 第一个参数 prefix 被设置为空字符串。这就是 core_param 定义的参数没有前缀的核心原因
    • name: 参数的名称。
    • &param_ops_##type: 传递一个指向操作函数集的指针。同样使用了记号粘贴,对于 int 类型,它会变成 &param_ops_int。这个结构体包含了将字符串转换为整数(set函数)和将整数转换为字符串(get函数)的回调函数指针。
    • &var: 你的C变量的地址,内核会将解析后的参数值存放在这里。
    • perm: sysfs中对应文件的权限。
    • -1: level 参数被设置为-1。这是一个特殊值,意味着这个参数不与任何特定的initcall级别绑定,可以在启动过程的任何时候被处理。这对于需要非常早期生效的核心参数(如 console)是必需的。
    • 0: flags 参数为0,没有特殊标志。

__module_param_call: 创建kernel_param结构体的核心宏

这个宏是所有参数定义宏(module_param, core_param 等)的最终目的地。它的唯一工作就是在编译时静态地定义并初始化一个 struct kernel_param 实例,并使用特殊的编译器属性将其放入名为 __param 的内存段中。

__module_param_call 宏的分解:

1
2
3
4
5
6
7
8
9
10
11
#define __module_param_call(prefix, name, ops, arg, perm, level, flags)	\
/* 1. 创建参数全名字符串 */
static const char __param_str_##name[] = prefix #name; \
/* 2. 定义并初始化 kernel_param 结构体 */
static struct kernel_param __moduleparam_const __param_##name \
/* 3. 添加编译器属性 */
__used __section("__param") \
__aligned(__alignof__(struct kernel_param)) \
/* 4. 初始化结构体成员 */
= { __param_str_##name, THIS_MODULE, ops, \
VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } }
  1. static const char __param_str_##name[] = prefix #name;:

    • 这一行创建了一个字符串常量,包含了参数的完整名称
    • #name 是C预处理器的“字符串化”(Stringification)操作符,它会将宏参数 name 的内容变成一个字符串字面量。例如,如果 namemy_value#name 就是 "my_value"
    • prefix #name 是C语言的一个特性,两个相邻的字符串字面量会被编译器自动合并成一个。
    • 示例:
      • 对于 core_param("root", ...)prefix""nameroot,结果是 "" "root",合并为 "root"
      • 对于模块 my_drv 中的 module_param(my_var, ...)prefix 会是 "my_drv."namemy_var,结果是 "my_drv." "my_var",合并为 "my_drv.my_var"
  2. static struct kernel_param ... __param_##name:

    • 这是在静态地定义一个 struct kernel_param 类型的变量。变量名是通过记号粘贴生成的,例如 __param_root
    • __moduleparam_const 在非模块化编译时通常就是 const,表示这个结构体是只读的。
  3. 编译器属性:

    • __used: 告诉编译器,即使你在当前文件中看不到任何对这个变量的引用,也绝对不能把它优化掉。这是必需的,因为引用它的代码(parse_args)在另一个文件中,并且是通过链接器定义的地址符号来找到它的。
    • __section("__param"): 这是整个机制的魔法核心。它指示编译器将这个 struct kernel_param 变量放入一个特殊的、名为 __param 的ELF段中。
    • __aligned(...): 确保结构体按其自然边界对齐,以获得最佳性能。
  4. 结构体初始化:

    • __param_str_##name: 指向我们第一步创建的全名字符串。
    • THIS_MODULE: 这是一个宏,对于内置代码(如core_param的情况),它的值是NULL;对于可加载模块,它是一个指向该模块struct module实例的指针。
    • ops: 指向类型特定的操作函数集。
    • VERIFY_OCTAL_PERMISSIONS(perm): 一个用于在编译时验证权限值是否为有效八进制数的宏,增加了代码的健壮性。
    • level: 要处理此参数的initcall级别。
    • flags: 任何额外的标志。
    • { arg }: 指向最终要被修改的C变量的指针。花括号用于C99风格的联合体成员初始化。

kernel/params.c

next_arg 分隔参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*
* 解析字符串以获取参数值对。
* 您可以在空格周围使用 “,但不能转义 ”。
* 参数名称中的连字符和下划线等效。
*/
char *next_arg(char *args, char **param, char **val)
{
unsigned int i, equals = 0;
int in_quote = 0, quoted = 0;

if (*args == '"') {
args++;
in_quote = 1;
quoted = 1;
}

for (i = 0; args[i]; i++) {
if (isspace(args[i]) && !in_quote)
break;
if (equals == 0) {
if (args[i] == '=')
equals = i;
}
if (args[i] == '"')
in_quote = !in_quote;
}

*param = args;
if (!equals)
*val = NULL;
else {
args[equals] = '\0';
*val = args + equals + 1;

/*不要在 value 中包含引号. */
if (**val == '"') {
(*val)++;
if (args[i-1] == '"')
args[i-1] = '\0';
}
}
if (quoted && i > 0 && args[i-1] == '"')
args[i-1] = '\0';

if (args[i]) {
args[i] = '\0';
args += i + 1;
} else
args += i;

/* Chew up trailing spaces. */
return skip_spaces(args);
}
EXPORT_SYMBOL(next_arg);

parse_one 解析参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static int parse_one(char *param,
char *val,
const char *doing,
const struct kernel_param *params,
unsigned num_params,
s16 min_level,
s16 max_level,
void *arg, parse_unknown_fn handle_unknown)
{
unsigned int i;
int err;

/* Find parameter */
for (i = 0; i < num_params; i++) {
if (parameq(param, params[i].name)) {
if (params[i].level < min_level
|| params[i].level > max_level)
return 0;
/* No one handled NULL, so do it here. */
if (!val &&
!(params[i].ops->flags & KERNEL_PARAM_OPS_FL_NOARG))
return -EINVAL;
pr_debug("handling %s with %p\n", param,
params[i].ops->set);
kernel_param_lock(params[i].mod);
if (param_check_unsafe(&params[i]))
err = params[i].ops->set(val, &params[i]);
else
err = -EPERM;
kernel_param_unlock(params[i].mod);
return err;
}
}

if (handle_unknown) {
pr_debug("doing %s: %s='%s'\n", doing, param, val);
return handle_unknown(param, val, doing, arg);
}

pr_debug("Unknown argument '%s'\n", param);
return -ENOENT;
}

parse_args 解析输入参数命令行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

/* Args looks like "foo=bar,bar2 baz=fuz wiz". */
char *parse_args(const char *doing,
char *args,
const struct kernel_param *params,
unsigned num,
s16 min_level,
s16 max_level,
void *arg, parse_unknown_fn unknown)
{
char *param, *val, *err = NULL;

/* Chew leading spaces */
args = skip_spaces(args);

if (*args)
pr_debug("doing %s, parsing ARGS: '%s'\n", doing, args);

while (*args) {
int ret;
int irq_was_disabled;

args = next_arg(args, &param, &val);
/* Stop at -- */
if (!val && strcmp(param, "--") == 0)
return err ?: args;
irq_was_disabled = irqs_disabled();
ret = parse_one(param, val, doing, params, num,
min_level, max_level, arg, unknown);
if (irq_was_disabled && !irqs_disabled())
pr_warn("%s: option '%s' enabled irq's!\n",
doing, param);

switch (ret) {
case 0:
continue;
case -ENOENT:
pr_err("%s: Unknown parameter `%s'\n", doing, param);
break;
case -ENOSPC:
pr_err("%s: `%s' too large for parameter `%s'\n",
doing, val ?: "", param);
break;
default:
pr_err("%s: `%s' invalid for parameter `%s'\n",
doing, val ?: "", param);
break;
}

err = ERR_PTR(ret);
}

return err;
}

Linux 内核模块的 Sysfs 接口初始化

此代码片段是Linux内核模块子系统与sysfs文件系统集成的核心部分。它的根本原理是利用内核的kobjectkset对象模型, 在系统启动的早期阶段, 创建一个顶层的/sys/module/目录。同时, 它定义了一整套的回调函数和类型信息, 用来规定未来任何内核模块加载时, 其在sysfs中的表现形式和行为——即如何创建模块自己的子目录(如/sys/module/nfsd/)以及目录下的属性文件(如refcnt, version), 以及这些文件在被读写时应该执行什么操作。

这个机制是实现内核模块与用户空间交互、监控和管理的关键。它允许用户空间的工具(如lsmod, modinfo)或系统管理员通过简单的文件操作来查看模块的状态、引用计数, 甚至修改模块的参数。


核心数据结构与回调函数

这些是定义模块sysfs行为的构建块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/*
* module_attr_show: 当从sysfs读取一个模块属性文件时被调用的函数(例如 cat /sys/module/nfsd/refcnt).
* 这是一个 "分发器" 函数.
*/
static ssize_t module_attr_show(struct kobject *kobj,
struct attribute *attr,
char *buf)
{
const struct module_attribute *attribute;
struct module_kobject *mk;
int ret;

/* 将通用的 attribute 和 kobject 指针转换为模块特定的类型. */
attribute = to_module_attr(attr);
mk = to_module_kobject(kobj);

/* 检查具体的属性是否定义了 show 方法, 如果没有则返回IO错误. */
if (!attribute->show)
return -EIO;

/* 调用该属性自己的show方法, 由它来填充buf并返回结果. */
ret = attribute->show(attribute, mk, buf);

return ret;
}

/*
* module_attr_store: 当向一个模块属性文件写入时被调用的函数(例如 echo 1 > /sys/module/nfsd/parameters/some_param).
* 这同样是一个 "分发器" 函数.
*/
static ssize_t module_attr_store(struct kobject *kobj,
struct attribute *attr,
const char *buf, size_t len)
{
const struct module_attribute *attribute;
struct module_kobject *mk;
int ret;

/* 将通用的 attribute 和 kobject 指针转换为模块特定的类型. */
attribute = to_module_attr(attr);
mk = to_module_kobject(kobj);

/* 检查具体的属性是否定义了 store 方法, 如果没有则返回IO错误. */
if (!attribute->store)
return -EIO;

/* 调用该属性自己的store方法, 由它来解析buf并执行操作. */
ret = attribute->store(attribute, mk, buf, len);

return ret;
}

/*
* module_sysfs_ops: 将 show 和 store 分发器函数打包成一个 sysfs_ops 结构体.
* 这个结构体将被关联到所有模块的kobject上.
*/
static const struct sysfs_ops module_sysfs_ops = {
.show = module_attr_show,
.store = module_attr_store,
};

/*
* uevent_filter: 一个过滤器函数, 用于决定是否为一个kobject生成uevent.
*/
static int uevent_filter(const struct kobject *kobj)
{
const struct kobj_type *ktype = get_ktype(kobj);

/* 只有当kobject的类型是模块类型(module_ktype)时,才返回1(表示允许生成uevent). */
if (ktype == &module_ktype)
return 1;
return 0;
}

/*
* module_uevent_ops: 将 uevent 过滤器打包.
* 当模块被加载或卸载时, 这个过滤器会确保向用户空间(如udev)发送一个事件.
*/
static const struct kset_uevent_ops module_uevent_ops = {
.filter = uevent_filter,
};

/* module_kset: 一个全局指针, 它将指向代表 /sys/module/ 目录的kset对象. */
struct kset *module_kset;

/*
* module_kobj_release: 当一个模块kobject的引用计数降为0时, 内核调用的最终释放函数.
*/
static void module_kobj_release(struct kobject *kobj)
{
struct module_kobject *mk = to_module_kobject(kobj);

/* 如果有其他代码正在等待这个kobject被释放(例如在模块卸载过程中), 就唤醒它. */
if (mk->kobj_completion)
complete(mk->kobj_completion);
}

/*
* module_ktype: 定义了所有"模块kobject"的共同行为和属性.
* 它像一个 "类" 的定义.
*/
const struct kobj_type module_ktype = {
.release = module_kobj_release, /* 指定释放函数. */
.sysfs_ops = &module_sysfs_ops, /* 指定sysfs文件操作. */
};

初始化入口函数

这是将所有部件组合在一起并实际创建/sys/module目录的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
* param_sysfs_init: 创建 "module" kset.
*
* 这必须在initramfs被解压、request_module()变得可能之前完成,
* 否则模块加载将在 mod_sysfs_init 中失败.
*/
static int __init param_sysfs_init(void)
{
/*
* 调用 kset_create_and_add() 来创建一个名为 "module" 的 kset, 并将其添加到sysfs中.
* 这个单一的调用就创建了 /sys/module/ 目录.
* 它还将 module_uevent_ops 与这个kset关联起来.
*/
module_kset = kset_create_and_add("module", &module_uevent_ops, NULL);
if (!module_kset) {
/* 如果创建失败, 打印警告并返回错误. 这通常是致命的. */
printk(KERN_WARNING "%s (%d): error creating kset\n",
__FILE__, __LINE__);
return -ENOMEM;
}

return 0;
}
/*
* subsys_initcall() 是一个宏, 它将 param_sysfs_init 函数注册为一个在内核启动早期阶段
* (在核心驱动和文件系统初始化之后, 但在大部分设备驱动初始化之前)就要被调用的函数.
* 这确保了模块的sysfs基础设施在任何模块需要被加载之前就已经准备就绪.
*/
subsys_initcall(param_sysfs_init);