Linux内核启动时处理启动参数

一、处理启动参数的相关宏定义

  下述宏定义以及函数声明位于内核源码kernel-5.10/include/linux/init.h

1. __setup_param

#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 }
  • str: 是一个字符串,表示内核启动参数。
  • unique_id: 是一个唯一的标识符,用于生成内部变量名。
  • fn: 是一个函数指针,指向处理该启动参数的函数。
  • early: 是一个布尔值,表示该参数是否在早期处理。

这个宏定义了两个静态变量:

  • __setup_str_##unique_id: 存储启动参数字符串,类型为 const char[],并使用 __aligned(1) 确保对齐。
  • __setup_##unique_id: 是一个 struct obs_kernel_param 类型的结构体,存储在 .init.setup 段中,并使用 __attribute__((aligned((sizeof(long))))) 确保对齐。该结构体包含启动参数字符串指针、处理函数指针和早期处理标志。
struct obs_kernel_param {
	const char *str;
	int (*setup_func)(char *);
	int early;
};

2. __setup

#define __setup(str, fn)						\
	__setup_param(str, fn, fn, 0)
  • str: 是一个启动参数字符串。
  • fn: 是处理该启动参数的函数。

  这个宏调用 __setup_param 宏,并将 early 参数设置为 0,表示该参数不是早期处理的。

3. early_param

#define early_param(str, fn)						\
	__setup_param(str, fn, fn, 1)
  • str: 是一个启动参数字符串。
  • fn: 是处理该启动参数的函数。

  这个宏调用 __setup_param 宏,并将 early 参数设置为 1,表示该参数是早期处理的。

4. early_param_on_off

#define early_param_on_off(str_on, str_off, var, config)		\
									\
	int var = IS_ENABLED(config);					\
									\
	static int __init parse_##var##_on(char *arg)			\
	{								\
		var = 1;						\
		return 0;						\
	}								\
	__setup_param(str_on, parse_##var##_on, parse_##var##_on, 1);	\
									\
	static int __init parse_##var##_off(char *arg)			\
	{								\
		var = 0;						\
		return 0;						\
	}								\
	__setup_param(str_off, parse_##var##_off, parse_##var##_off, 1)
  • str_on: 是一个表示启用的启动参数字符串。
  • str_off: 是一个表示禁用的启动参数字符串。
  • var: 是一个变量,用于存储是否启用的标志。
  • config: 是一个配置选项,用于初始化 var 的值。

  这个宏定义了两个处理函数 parse_##var##_onparse_##var##_off,分别用于处理启用和禁用的启动参数,并将它们注册为早期处理的参数。

5. parse_early_paramparse_early_options 函数

//函数实现位于kernel-5.10/init/main.c
void __init parse_early_param(void);
void __init parse_early_options(char *cmdline);
  • parse_early_param: 是一个函数,用于解析早期启动参数。
  • parse_early_options: 是一个函数,用于解析启动命令行中的早期选项。
/**
 * 解析早期启动选项
 * 
 * @param cmdline 命令行参数字符串
 * 
 * 此函数在系统初始化阶段解析传递给内核的命令行参数称为"early options"
 * 它使用parse_args函数来处理这些参数并将它们标记为早期选项
 * 这些选项通常用于配置内核的早期启动过程
 * 
 * 注意:这个函数假定调用者已经准备好了合适的命令行参数字符串
 * 并且这个函数通常在内核初始化过程中被调用
 */
void __init parse_early_options(char *cmdline)
{
    // 调用parse_args函数解析早期选项参数
    // "early options"是参数的描述用于调试和日志
    // cmdline是指向命令行参数字符串的指针
    // 后续参数为NULL和0表示默认行为和设置
    // do_early_param是处理每个参数的回调函数
    parse_args("early options", cmdline, NULL, 0, 0, 0, NULL, do_early_param);
}
/**
 * 解析早期启动参数。
 * 
 * 该函数用于解析内核完全初始化之前传递给内核的启动参数。
 * 它通常在内核启动过程的早期阶段调用,以处理诸如 'root=' 和 'init=' 等参数。
 * 
 * 此函数不接受任何参数,因为它直接使用全局变量 boot_command_line。
 * 没有返回值,解析结果会反映在全局状态中。
 */
void __init parse_early_param(void)
{
    // 标志位,确保只解析一次
    static int done __initdata;
    // 临时缓冲区,用于存储启动命令行
    static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;

    // 检查是否已经完成解析,避免重复执行
    if (done)
        return;

    // 将启动命令行复制到临时缓冲区以便解析
    strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
    // 解析早期启动参数
    parse_early_options(tmp_cmdline);
    // 设置完成标志,防止再次解析
    done = 1;
}

6. 总结

  这些宏和函数主要用于在 Linux 内核初始化过程中解析和处理启动参数。通过 __setup_param 宏,可以方便地注册启动参数及其处理函数,并将这些参数分类为早期处理或非早期处理。early_param_on_off 宏则提供了一种便捷的方式来处理启用和禁用的启动参数。parse_early_paramparse_early_options 函数则在内核初始化过程中调用,解析这些参数。

二、使用示例

  假设我们有一个内核模块(或内核代码的一部分),需要在启动时解析一个名为 my_option 的启动参数,并根据该参数设置一个全局变量 my_value。我们还希望有一个早期处理的参数 my_early_option

1. 定义全局变量

首先,我们定义一个全局变量 my_value,用于存储 my_option 参数的值:

static int my_value = 0;  // 默认值为 0

2. 编写参数处理函数

接下来,我们编写两个函数,分别用于处理 my_optionmy_early_option

// 处理 my_option 参数的函数
static int __init parse_my_option(char *arg)
{
    int value;
    if (kstrtoint(arg, 10, &value) == 0) {  // 将字符串转换为整数
        my_value = value;
        pr_info("my_option set to %d\n", my_value);
        return 0;  // 返回 0 表示成功处理
    }
    pr_err("Invalid value for my_option: %s\n", arg);
    return -EINVAL;  // 返回错误码表示处理失败
}

// 处理 my_early_option 参数的函数
static int __init parse_my_early_option(char *arg)
{
    pr_info("my_early_option received: %s\n", arg);
    return 0;  // 返回 0 表示成功处理
}

3. 使用 __setupearly_param 注册启动参数

现在,我们使用 __setupearly_param 宏来注册这两个启动参数:

__setup("my_option=", parse_my_option);  // 注册 my_option 参数
early_param("my_early_option", parse_my_early_option);  // 注册 my_early_option 参数

4. 完整的代码示例

将上述代码整合到一起,完整的代码示例如下:

#include 
#include 
#include 
#include 
#include 

static int my_value = 0;  // 默认值为 0

// 处理 my_option 参数的函数
static int __init parse_my_option(char *arg)
{
    int value;
    if (kstrtoint(arg, 10, &value) == 0) {  // 将字符串转换为整数
        my_value = value;
        pr_info("my_option set to %d\n", my_value);
        return 0;  // 返回 0 表示成功处理
    }
    pr_err("Invalid value for my_option: %s\n", arg);
    return -EINVAL;  // 返回错误码表示处理失败
}

// 处理 my_early_option 参数的函数
static int __init parse_my_early_option(char *arg)
{
    pr_info("my_early_option received: %s\n", arg);
    return 0;  // 返回 0 表示成功处理
}

// 注册启动参数
__setup("my_option=", parse_my_option);  // 注册 my_option 参数
early_param("my_early_option", parse_my_early_option);  // 注册 my_early_option 参数

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example of using __setup and early_param macros");

5. 启动参数的使用

可在uboot下命令行中添加这些参数,例如:

my_option=42 my_early_option=early_string
  • my_option=42:内核启动后,my_value 的值会被设置为 42,并输出一条日志信息。
  • my_early_option=early_string:内核启动时,会立即处理 my_early_option,并输出一条日志信息。

6. 结果分析

  • 如果 my_option=42 被正确解析,内核日志中会输出:
    my_option set to 42
    
  • 如果 my_early_option=early_string 被正确解析,内核日志中会输出:
    my_early_option received: early_string
    

  通过上述示例,我们展示了如何使用 __setupearly_param 宏来注册启动参数,并在内核启动时解析它们。__setup 用于注册普通的启动参数,而 early_param 用于注册需要在早期处理的启动参数。这些宏极大地简化了启动参数的注册和处理流程,是内核模块开发中非常有用的工具。

三、看看内核源码中处理的一些启动参数

Linux内核启动时处理启动参数_第1张图片

你可能感兴趣的:(Linux驱动,初窥uboot与Linux内核,linux,驱动开发,arm开发,嵌入式,系统架构)