module_param_named 内核启动时模块参数实现原理

基于上节内核启动参数实现原理内容, 其中对early_param的实现流程做了分析, 已基本清晰. 但有不少的参数是在内核模块中声明的, 具体赋值流程也值得一探究竟.

nomodeset

装过Linux系统的同学可能多少有看到过nomodeset这个参数, 解决一些显卡点不亮Linux的问题.

那么这个nomodeset是怎么生效的呢?

在内核代码中查找一下:

[mxd@5 linux-4.19-loongson]$ grep nomodeset -rnIw drivers/video/
drivers/video/console/vgacon.c:116:	pr_warning("You have booted with nomodeset. This means your GPU drivers are DISABLED\n");
drivers/video/console/vgacon.c:118:	pr_warning("Unless you actually understand what nomodeset does, you should reboot without enabling it\n");
drivers/video/console/vgacon.c:124:__setup("nomodeset", text_mode);
drivers/video/fbdev/aty/radeon_base.c:263:static bool nomodeset = 0;
drivers/video/fbdev/aty/radeon_base.c:1472:	if (nomodeset)
drivers/video/fbdev/aty/radeon_base.c:2624:		} else if (!strncmp(this_opt, "nomodeset", 9)) {
drivers/video/fbdev/aty/radeon_base.c:2625:			nomodeset = 1;
drivers/video/fbdev/aty/radeon_base.c:2671:module_param(nomodeset, bool, 0);
drivers/video/fbdev/aty/radeon_base.c:2672:MODULE_PARM_DESC(nomodeset, "bool: disable actual setting of video mode");

drivers/video/console/vgacon.c中有相关声明:

static int __init text_mode(char *str)
{
    vgacon_text_mode_force = true;

    pr_warning("You have booted with nomodeset. This means your GPU drivers are DISABLED\n");
    pr_warning("Any video related functionality will be severely degraded, and you may not even be able to suspend the system properly\n");
    pr_warning("Unless you actually understand what nomodeset does, you should reboot without enabling it\n");

    return 1;
}

/* force text mode - used by kernel modesetting */
__setup("nomodeset", text_mode);

而关于__setup的定义, 在上节中已经看到过了:

#ifndef MODULE
#define __setup_param(str, unique_id, fn, early)            \
    static const char __setup_str_##unique_id[] __initconst     \
        __aligned(1) = str;                     \
    static struct obs_kernel_param __setup_##unique_id      \
        __used __section(.init.setup)               \
        __attribute__((aligned((sizeof(long)))))        \
        = { __setup_str_##unique_id, fn, early }
#else /* MODULE */
#define __setup_param(str, unique_id, fn)   /* nothing */
#endif

#define __setup(str, fn)                        \
    __setup_param(str, fn, fn, 0)

上节中的early_param是只在内核中用, 这个可就不好判断了呀.

看一下编译内容:

[mxd@5 linux-4.19-loongson]$ grep vgacon.o -rn drivers/video/console/Makefile
9:obj-$(CONFIG_VGA_CONSOLE)         += vgacon.o
[mxd@5 linux-4.19-loongson]$ cat .config | grep CONFIG_VGA_CONSOLE
CONFIG_VGA_CONSOLE=y

所以, 这个参数还是给内核用的, 但与early_param不同, 他的并不满足early_param的条件:

  1. 如果成员是early类型, 且成员中的变量名与传入的参数名一致
  2. 如果参数名是console, 且成员中的变量名是earlycon时.

还记得有一个obsolete_checksetup吗? 它是在参数未知的情况下注册的:

static int __init unknown_bootoption(char *param, char *val,
                     const char *unused, void *arg)
{
    repair_env_string(param, val, unused, NULL);

    /* Handle obsolete-style parameters */
    if (obsolete_checksetup(param))
        return 0;
    ......
    ......
}

asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;
    ......
    ......
    after_dashes = parse_args("Booting kernel",
              static_command_line, __start___param,
              __stop___param - __start___param,
              -1, -1, NULL, &unknown_bootoption);
    ......
    ......
}

static bool __init obsolete_checksetup(char *line)
{
    const struct obs_kernel_param *p;
    bool had_early_param = false;

    p = __setup_start;
    do {
        int n = strlen(p->str);
        if (parameqn(line, p->str, n)) {
            if (p->early) {
                /* Already done in parse_early_param?
                 * (Needs exact match on param part).
                 * Keep iterating, as we can have early
                 * params and __setups of same names 8( */
                if (line[n] == '\0' || line[n] == '=')
                    had_early_param = true;
            } else if (!p->setup_func) {
                pr_warn("Parameter %s is obsolete, ignored\n",
                    p->str);
                return true;
            } else if (p->setup_func(line + n))
                return true;
        }
        p++;
    } while (p < __setup_end);

    return had_early_param;
}

所以nomodeset是在start_kernel时初始化并调用的. 仍然属于内核参数.

radeon.modeset

nomodeset类似, radeon.modeset也是在搭配显卡时安装Linux黑屏的解决方案之一.

radeon的驱动中查找一下看看:

[mxd@5 linux-4.19-loongson]$ grep modeset -rnwI drivers/gpu/drm/radeon/ | grep -v include
drivers/gpu/drm/radeon/trinity_dpm.c:1983:	pi->voltage_drop_in_dce = false; /* need to restructure dpm/modeset interaction */
drivers/gpu/drm/radeon/radeon_kms.c:144:		dev_err(&dev->pdev->dev, "Fatal error during modeset init\n");
drivers/gpu/drm/radeon/radeon_kms.c:150:	/* Call ACPI methods: require modeset init
drivers/gpu/drm/radeon/radeon_connectors.c:195:			/* mode_clock is clock in kHz for mode to be modeset on this connector */
drivers/gpu/drm/radeon/radeon_drv.c:204:MODULE_PARM_DESC(modeset, "Disable/Enable modesetting");
drivers/gpu/drm/radeon/radeon_drv.c:205:module_param_named(modeset, radeon_modeset, int, 0400);
drivers/gpu/drm/radeon/radeon_pm.c:281:				/* This can fail if a modeset is in progress */
drivers/gpu/drm/radeon/radeon_pm.c:1565:				 * a modeset to call this.
drivers/gpu/drm/radeon/radeon_display.c:175:	/* XXX match this to the depth of the crtc fmt block, move to modeset? */

可见在drivers/gpu/drm/radeon/radeon_drv.c中通过module_param_namedMODULE_PARM_DESC声明和描述了这个参数.

看看代码:

#define __MODULE_INFO(tag, name, info)                    \
static const char __UNIQUE_ID(name)[]                     \
  __used __attribute__((section(".modinfo"), unused, aligned(1)))     \
  = __stringify(tag) "=" info

#define __MODULE_PARM_TYPE(name, _type)                   \
  __MODULE_INFO(parmtype, name##type, #name ":" _type)

/* One for each parameter, describing how to use it.  Some files do
   multiple of these per line, so can't just use MODULE_INFO. */
#define MODULE_PARM_DESC(_parm, desc) \
    __MODULE_INFO(parm, _parm, #_parm ":" desc)

#define module_param_cb(name, ops, arg, perm)                     \
    __module_param_call(MODULE_PARAM_PREFIX, name, ops, arg, perm, -1, 0)

#define __module_param_call(prefix, name, ops, arg, perm, level, flags) \
    /* Default value instead of permissions? */         \
    static const char __param_str_##name[] = prefix #name;      \
    static struct kernel_param __moduleparam_const __param_##name   \
    __used                              \
    __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \
    = { __param_str_##name, THIS_MODULE, ops,           \
        VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } }

#define module_param_named(name, value, type, perm)            \
    param_check_##type(name, &(value));                \
    module_param_cb(name, &param_ops_##type, &value, perm);        \
    __MODULE_PARM_TYPE(name, #type)

所以radeon.modeset的声明实体就是:

param_check_int(modeset, &(radeon_modeset));
static const char __param_str_modeset[] = radeon.modeset;
static struct kernel_param __moduleparam_const __param_modeset
        __used __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) = {
            __param_str_modeset,
            radeon,
            &param_ops_int,
            VERIFY_OCTAL_PERMISSIONS(0400),
            -1,
            0,
            {&(radeon_modeset)}
        };
static const char __UNIQUE_ID(modesettype)[]
    __used __attribute__((section(".modinfo"), unused, aligned(1))) = "parmtype=modeset:int";

类似的, 也是将相关的参数存入指定的section中, 在加载modules时进行初始化及调用. 原理同early_param类似, 所以不再细说.

AI

学习的时候, 没必要把代码全都看得这么细, 除非对算法有研究. 在AI时代, 借助AI分析, 并掌握一个大项目的实现策略的优先级是更高的. 看看AI的说法:

MODULE_PARM_DESC有什么用?

MODULE_PARM_DESC 是 Linux 内核模块编程中的一个宏,用于描述模块参数的意义和用途。当你在内核模块中定义了一个可以由用户空间程序修改的参数时,使用 MODULE_PARM_DESC 宏可以帮助记录这个参数的描述性信息。

当你在模块中定义一个参数时,通常会使用 module_param 或 module_param_array 宏来声明这个参数,并指定它的类型和权限。接着,你可以在同一地方使用 MODULE_PARM_DESC 宏来添加对该参数的描述。

例如:

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

int myparam = 10;
module_param(myparam, int, 0644);
MODULE_PARM_DESC(myparam, "This is a description of the parameter myparam.");

在这个例子中,myparam 参数被声明为整型,权限为 0644(读写权限对所有用户开放),并且有一个描述,告诉用户这个参数的含义。

当你使用 modinfo 命令查询模块信息时,MODULE_PARM_DESC 提供的描述会被显示出来,帮助用户理解参数的作用。这对于调试和配置模块时非常有用,因为用户可以更容易地理解每个参数的功能。

例如,如果你有一个名为 example.ko 的模块,你可以使用以下命令来查看参数信息:

$ modinfo example.ko
filename:       /lib/modules/$(uname -r)/kernel/drivers/example.ko
license:        GPL
description:    Example kernel module
author:         Your Name
srcversion:     1234567890abcdef...
depends:
retpoline:      Y
name:           example
vermagic:       4.19.0-xyz SMP mod_unload
parm:           myparam:This is a description of the parameter myparam. (int)

在 parm: 行中,你会看到 myparam 参数的描述。这样,当用户使用 modprobe 加载模块或使用 insmod 命令手动加载模块并设置参数时,他们可以根据描述来正确地设置参数值。

module_param_named和模块初始化代码的调用顺序是怎样的

在Linux内核模块中,module_param_named宏的使用和模块初始化函数的调用顺序是非常关键的,因为它们决定了模块参数如何被内核识别和处理。以下是典型的调用顺序和工作流程:

模块参数声明与注册:
在模块的源文件中,你需要使用module_param_named(或其他module_param_*宏)来声明模块参数。这些宏会在编译时生成相应的代码,注册参数,并将其关联到一个特定的回调函数,该函数用于处理模块加载或卸载时的参数设置。
这些宏的执行是在编译阶段完成的,它们生成的代码会被链接到模块的.init.text和.data段中。
模块初始化:
模块初始化函数(如init_module或自定义命名的初始化函数)会在模块加载到内核时由内核自动调用。在这之前,内核已经解析了模块信息段(.modinfo),包括所有由module_param_*宏注册的参数信息。
初始化函数可以访问通过module_param_*宏注册的所有参数。这意味着你可以在这个函数中使用这些参数来初始化模块的状态或者配置模块的行为。
参数解析与应用:
当模块加载时,内核会读取传递给模块的命令行参数,并尝试匹配这些参数与模块参数。如果找到了匹配,内核会调用相应的回调函数来设置参数值。
这一过程发生在模块初始化函数被调用之前。因此,当你的模块初始化代码执行时,所有通过命令行传递的参数值已经被设置好了。
模块清理:
类似地,当模块被卸载时,模块的清理函数(如cleanup_module)会被调用。此时,module_param_*宏所注册的参数仍然可用,直到模块完全从内核中移除。
总结来说,module_param_named宏的调用应该在模块初始化函数的定义之前,这样在模块加载时,内核就能正确地解析和设置参数。而模块初始化函数则会在参数设置完成后被调用,此时就可以使用这些参数来配置模块了。

使用

借助modinfo命令:

[root@mxd ~]# modinfo /lib/modules/6.8.6-2/kernel/drivers/gpu/drm/radeon/radeon.ko.zst
......
name:           radeon
vermagic:       6.8.6-2 SMP preempt mod_unload modversions LOONGARCH 64BIT
parm:           no_wb:Disable AGP writeback for scratch registers (int)
parm:           modeset:Disable/Enable modesetting (int)
parm:           dynclks:Disable/Enable dynamic clocks (int)
parm:           r4xx_atom:Enable ATOMBIOS modesetting for R4xx (int)
parm:           vramlimit:Restrict VRAM for testing, in megabytes (int)
......

所以在内核启动时, 可以通过radeon.modeset=0的方式禁用radeon驱动的modesetting.

龙芯的GPU也有相似的功能:

root@loongson-pc:/home/loongson# modinfo /lib/modules/4.19.0-19-loongson-3/kernel/drivers/gpu/drm/gsgpu/gpu/gsgpu.ko
filename:       /lib/modules/4.19.0-19-loongson-3/kernel/drivers/gpu/drm/gsgpu/gpu/gsgpu.ko
license:        GPL and additional rights
description:    GS GPU Driver
author:         Loongson graphics driver team
firmware:       loongson/lg100_cp.bin
alias:          pci:v00000014d00007A25sv*sd*bc*sc*i*
depends:        gpu-sched
intree:         Y
name:           gsgpu
vermagic:       4.19.0-19-loongson-3 SMP mod_unload modversions LOONGARCH 64BIT
parm:           LG100_support:LG100 support (1 = enabled (default), 0 = disabled (int)
parm:           vramlimit:Restrict VRAM for testing, in megabytes (int)
parm:           vis_vramlimit:Restrict visible VRAM for testing, in megabytes (int)
parm:           gartsize:Size of GART to setup in megabytes (32, 64, etc., -1=auto) (uint)
parm:           gttsize:Size of the GTT domain in megabytes (-1 = auto) (int)
parm:           moverate:Maximum buffer migration rate in MB/s. (32, 64, etc., -1=auto, 0=1=disabled) (int)
parm:           benchmark:Run benchmark (int)

所以当龙芯的板卡出问题时, 可以通过gsgpu.LG100_support=0来禁用gsgpu功能.

参考文献

  1. 内核源码
  2. 通义千问
  • 16
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值