[Linux]学习笔记系列 -- [kernel[params
title: params
categories:
- linux
- kernel
tags: - linux
- kernel
abbrlink: 8fce0ef3
date: 2025-10-03 09:01:49
文章目录
- kernel/params.c 内核模块参数(Kernel Module Parameters) 实现模块加载时参数传递
- 历史与背景
- 这项技术是为了解决什么特定问题而诞生的?
- 它的发展经历了哪些重要的里程碑或版本迭代?
- 目前该技术的社区活跃度和主流应用情况如何?
- 核心原理与设计
- 它的核心工作原理是什么?
- 它的主要优势体现在哪些方面?
- 它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 使用场景
- 在哪些具体的业务或技术场景下,它是首选解决方案?
- 是否有不推荐使用该技术的场景?为什么?
- 对比分析
- 请将其 与 其他相似技术 进行详细对比。
- include/linux/moduleparam.h
- `core_param`: 定义一个历史性的核心内核参数
- `core_param` 宏的分解:
- `__module_param_call`: 创建`kernel_param`结构体的核心宏
- `__module_param_call` 宏的分解:
- kernel/params.c
- next_arg 分隔参数
- parse_one 解析参数
- parse_args 解析输入参数命令行
- Linux 内核模块的 Sysfs 接口初始化
- 核心数据结构与回调函数
- 初始化入口函数

https://github.com/wdfk-prog/linux-study
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中的代码提供了实现这种读写操作的底层函数。 - 自定义类型的支持:框架被设计为可扩展的。开发者可以通过提供自定义的
set和get回调函数,来支持任意复杂的数据类型作为模块参数。
目前该技术的社区活跃度和主流应用情况如何?
模块参数是Linux内核驱动和子系统开发中一项基础性、极其稳定且被普遍使用的功能。它不是一个可选的库,而是编写可配置内核模块的标准范式。几乎所有的内核驱动程序都使用模块参数来暴露调试选项、硬件配置、功能开关等。任何向内核提交代码的开发者都必须熟悉这一机制。
核心原理与设计
它的核心工作原理是什么?
params.c 的核心原理是基于编译时元数据生成和加载时参数解析。
-
编译时:元数据生成
- 当开发者在模块代码中使用
module_param()宏时,C预处理器会将其展开。 - 这个展开的宏定义了一个
struct kernel_param类型的静态变量。这个结构体包含了参数的所有元信息:参数名(字符串)、指向模块中实际存储参数值的变量的指针、参数类型(通过一组标准的回调函数表示)、以及在sysfs中的文件权限。 - 最关键的一步是,编译器会将这个
struct kernel_param实例放入一个特殊的ELF段(section)中。
- 当开发者在模块代码中使用
-
加载时:解析与赋值
- 当用户使用
insmod或modprobe加载模块时(例如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),然后通过元数据中存储的指针,将转换后的值写入模块的全局变量中。
- 当用户使用
-
运行时:Sysfs交互
- 模块成功加载后,模块子系统会为每个参数在
/sys/module/.../parameters/目录下创建一个文件。 - 当用户空间程序
read或write这个文件时,VFS层会最终调用到与该参数类型关联的get或set函数,从而实现对模块内部变量的运行时读写。
- 模块成功加载后,模块子系统会为每个参数在
它的主要优势体现在哪些方面?
- 灵活性与可配置性:无需重新编译即可改变模块行为。
- 简单易用:为内核开发者提供了极其简单的宏接口,隐藏了所有复杂性。
- 类型安全:框架负责处理字符串到具体类型的转换,减少了驱动中的模板代码和出错可能。
- 运行时交互:与sysfs的无缝集成为参数提供了标准的运行时查看和修改接口。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 静态定义:模块的参数必须在编译时定义好,不能在运行时动态添加或删除参数。
- 配置而非命令:该机制主要用于设置“值”或“状态”,不适合用于触发复杂的操作或命令。对于后者,
ioctl等机制更合适。 - 不适合大数据量:不适合用于在用户空间和内核之间传输大量或高频率的数据。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
模块参数是内核模块暴露简单配置选项的标准和首选方案。
- 开启调试功能:一个网络驱动可以通过一个布尔型参数
debug来控制是否打印详细的调试日志。insmod e1000.ko debug=1。 - 指定硬件资源:在硬件资源无法被内核自动探测的旧系统中,驱动可能需要通过参数来手动指定中断号(IRQ)或I/O端口地址。
- 设置工作模式:一个无线网卡驱动可能有一个参数
wifi_mode,允许用户在加载时指定其工作在AP(接入点)模式还是Station(客户端)模式。 - 覆盖默认值:模块可以有一个内部的默认配置,但允许用户通过参数在加载时覆盖它,例如一个缓冲区大小的默认值。
是否有不推荐使用该技术的场景?为什么?
- 高频数据交换:例如,一个应用程序需要频繁地向一个驱动发送数据包。这种场景应该使用
write()系统调用、netlink套接字或ioctl。 - 导出大量状态信息:如果需要向用户空间导出大量、复杂的、结构化的状态信息或统计数据,
procfs或debugfs是更合适的工具。 - 设备特定命令:向一个打开的设备文件发送一个特定的命令(例如,让硬盘休眠),应该使用
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 宏的分解:
#define core_param(name, var, type, perm) \/* 1. 类型检查 */param_check_##type(name, &(var)); \/* 2. 调用底层宏 */__module_param_call("", name, ¶m_ops_##type, &var, perm, -1, 0)
-
param_check_##type(name, &(var));:- 这是一个编译时的类型安全检查。
##是C预处理器的“记号粘贴”(Token Pasting)操作符。它会将param_check_和宏参数type的值(例如int)粘贴在一起,形成一个新的函数名,如param_check_int。- 内核为每种基本类型都定义了这样的检查函数。
param_check_int会确保你传递给它的var变量确实是一个int类型的指针。如果类型不匹配,编译器会在这里产生一个警告或错误,从而在早期就捕捉到潜在的bug。
-
__module_param_call("", name, ¶m_ops_##type, &var, perm, -1, 0):- 这是对底层核心宏的调用,传递了特定的参数来表现出
core_param的行为。 "": 第一个参数prefix被设置为空字符串。这就是core_param定义的参数没有前缀的核心原因。name: 参数的名称。¶m_ops_##type: 传递一个指向操作函数集的指针。同样使用了记号粘贴,对于int类型,它会变成¶m_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 宏的分解:
#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 } }
-
static const char __param_str_##name[] = prefix #name;:- 这一行创建了一个字符串常量,包含了参数的完整名称。
#name是C预处理器的“字符串化”(Stringification)操作符,它会将宏参数name的内容变成一个字符串字面量。例如,如果name是my_value,#name就是"my_value"。prefix #name是C语言的一个特性,两个相邻的字符串字面量会被编译器自动合并成一个。- 示例:
- 对于
core_param("root", ...),prefix是"",name是root,结果是"" "root",合并为"root"。 - 对于模块
my_drv中的module_param(my_var, ...),prefix会是"my_drv.",name是my_var,结果是"my_drv." "my_var",合并为"my_drv.my_var"。
- 对于
-
static struct kernel_param ... __param_##name:- 这是在静态地定义一个
struct kernel_param类型的变量。变量名是通过记号粘贴生成的,例如__param_root。 __moduleparam_const在非模块化编译时通常就是const,表示这个结构体是只读的。
- 这是在静态地定义一个
-
编译器属性:
__used: 告诉编译器,即使你在当前文件中看不到任何对这个变量的引用,也绝对不能把它优化掉。这是必需的,因为引用它的代码(parse_args)在另一个文件中,并且是通过链接器定义的地址符号来找到它的。__section("__param"): 这是整个机制的魔法核心。它指示编译器将这个struct kernel_param变量放入一个特殊的、名为__param的ELF段中。__aligned(...): 确保结构体按其自然边界对齐,以获得最佳性能。
-
结构体初始化:
__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 分隔参数
/** 解析字符串以获取参数值对。* 您可以在空格周围使用 “,但不能转义 ”。* 参数名称中的连字符和下划线等效。*/
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;} elseargs += i;/* Chew up trailing spaces. */return skip_spaces(args);
}
EXPORT_SYMBOL(next_arg);
parse_one 解析参数
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(¶ms[i]))err = params[i].ops->set(val, ¶ms[i]);elseerr = -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 解析输入参数命令行
/* 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, ¶m, &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文件系统集成的核心部分。它的根本原理是利用内核的kobject和kset对象模型, 在系统启动的早期阶段, 创建一个顶层的/sys/module/目录。同时, 它定义了一整套的回调函数和类型信息, 用来规定未来任何内核模块加载时, 其在sysfs中的表现形式和行为——即如何创建模块自己的子目录(如/sys/module/nfsd/)以及目录下的属性文件(如refcnt, version), 以及这些文件在被读写时应该执行什么操作。
这个机制是实现内核模块与用户空间交互、监控和管理的关键。它允许用户空间的工具(如lsmod, modinfo)或系统管理员通过简单的文件操作来查看模块的状态、引用计数, 甚至修改模块的参数。
核心数据结构与回调函数
这些是定义模块sysfs行为的构建块。
/** 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目录的函数。
/** 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);
