启动配置

作者:

Masami Hiramatsu <mhiramat@kernel.org>

概述

启动配置扩展了当前的内核命令行,以支持在启动内核时以高效方式处理额外的键值对数据。这允许管理员传递一个结构化键的配置文件。

配置文件语法

启动配置语法是简单的结构化键值对。每个键由点连接的单词组成,键和值通过 = 连接。值字符串必须由下面描述的定界符终止。

每个键名单词必须仅包含字母、数字、连字符 (-) 或下划线 (_)。每个值仅包含可打印字符或空格,但不包括定界符,如分号 (;)、换行符 (\n)、逗号 (,)、井号 (#) 和闭括号 (})。

如果 = 后面只有空格直到出现上述定界符之一,则该键被赋予空值。

对于数组,数组值由逗号 (\n) 的折行以提高可读性。因此,数组的第一个条目必须与键在同一行。

KEY[.WORD[...]] = VALUE[, VALUE2[...]][;]

与内核命令行语法不同,逗号和 = 周围的空格(包括制表符)会被忽略。

如果你想在值中使用这些定界符,可以使用双引号 ("VALUE") 或单引号 ('VALUE') 来引用。注意,你不能转义这些引号。

可以存在没有值或具有空值的键。这些键用于检查该键是否存在(类似于布尔值)。

键值语法

启动配置语法允许用户使用括号合并部分相同的单词键。例如

foo.bar.baz = value1
foo.bar.qux.quux = value2

这些也可以写成

foo.bar {
   baz = value1
   qux.quux = value2
}

或者更简短,写成如下形式

foo.bar { baz = value1; qux.quux = value2 }

在两种风格中,相同的键名单词在引导时解析时会自动合并。因此你可以附加类似的树或键值对。

同名键的值

禁止两个或多个值或数组共享同一个键。例如,

foo = bar, baz
foo = qux  # !ERROR! we can not re-define same key

如果你想更新该值,必须显式使用覆盖运算符 :=。例如

foo = bar, baz
foo := qux

此时,qux 被赋值给 foo 键。这对于在不解析默认引导配置的情况下,通过添加(部分)自定义 bootconfigs 来覆盖默认值非常有用。

如果你想将该值作为数组成员追加到现有键,可以使用 += 运算符。例如

foo = bar, baz
foo += qux

在这种情况下,键 foo 拥有 barbazqux

此外,子键和值可以在父键下共存。例如,允许以下配置。

foo = value1
foo.bar = value2
foo := value3 # This will update foo's value.

注意,由于没有语法可以将原始值直接放在结构化键下,因此你必须在括号外定义它。例如

foo {
    bar = value1
    bar {
        baz = value2
        qux = value3
    }
}

此外,键下值节点的顺序是固定的。如果同时存在值和子键,值始终是键的第一个子节点。因此,如果用户先指定子键,例如

foo.bar = value1
foo = value2

在程序中(以及 /proc/bootconfig 中),它将如下所示

foo = value2
foo.bar = value1

注释

配置语法接受 shell 脚本风格的注释。以井号 (“#”) 开始直到换行符 (“\n”) 的注释将被忽略。

# comment line
foo = value # value is set to foo.
bar = 1, # 1st element
      2, # 2nd element
      3  # 3rd element

其解析如下

foo = value
bar = 1, 2, 3

注意,你不能在值和定界符(;)之间放置注释或换行符。这意味着以下配置存在语法错误

key = 1 # comment
      ,2

/proc/bootconfig

/proc/bootconfig 是启动配置的用户空间接口。与 /proc/cmdline 不同,此文件显示键值风格的列表。每个键值对以如下风格显示在每一行中

KEY[.WORDS...] = "[VALUE]"[,"VALUE2"...]

使用启动配置引导内核

使用 bootconfig 引导内核有两种选择:将 bootconfig 附加到 initrd 镜像,或者将其嵌入到内核本身中。

将启动配置附加到 Initrd

由于启动配置文件默认随 initrd 加载,它将被添加到 initrd (initramfs) 镜像文件的末尾,包含填充、大小、校验和以及 12 字节的魔法字,如下所示。

[initrd][bootconfig][padding][size(le32)][checksum(le32)][#BOOTCONFIG\n]

大小 (size) 和校验和 (checksum) 字段是无符号 32 位小端序值。

当启动配置被添加到 initrd 镜像时,总文件大小会按 4 字节对齐。为了填补空隙,将添加空字符 (\0)。因此,size 是启动配置文件长度 + 填充字节。

Linux 内核在内存中解码 initrd 镜像的最后一部分,以获取启动配置数据。由于这种“背负” (piggyback) 方式,只要引导加载程序传递正确的 initrd 文件大小,就不需要更改或更新引导加载程序和内核镜像本身。如果万一引导加载程序传递了更长的大小,内核将无法找到 bootconfig 数据。

为了执行此操作,Linux 内核在 tools/bootconfig 下提供了 bootconfig 命令,允许管理员向 initrd 镜像应用或从中删除配置文件。你可以使用以下命令构建它

# make -C tools/bootconfig

要将你的启动配置文件添加到 initrd 镜像,请如下运行 bootconfig(如果已存在旧数据,将自动删除)

# tools/bootconfig/bootconfig -a your-config /boot/initrd.img-X.Y.Z

要从镜像中删除配置,你可以如下使用 -d 选项

# tools/bootconfig/bootconfig -d /boot/initrd.img-X.Y.Z

然后在正常的内核命令行中添加 “bootconfig”,告诉内核在 initrd 文件末尾寻找 bootconfig。或者,在构建内核时选择 CONFIG_BOOT_CONFIG_FORCE Kconfig 选项。

将启动配置嵌入内核

如果你无法使用 initrd,也可以通过 Kconfig 选项将 bootconfig 文件嵌入内核。在这种情况下,你需要使用以下配置重新编译内核

CONFIG_BOOT_CONFIG_EMBED=y
CONFIG_BOOT_CONFIG_EMBED_FILE="/PATH/TO/BOOTCONFIG/FILE"

CONFIG_BOOT_CONFIG_EMBED_FILE 需要 bootconfig 文件相对于源码树或对象树的绝对路径或相对路径。内核会将其作为默认启动配置嵌入。

就像将 bootconfig 附加到 initrd 一样,你需要在内核命令行中使用 bootconfig 选项来启用嵌入式 bootconfig,或者通过启用 CONFIG_BOOT_CONFIG_FORCE Kconfig 选项来构建内核。

注意,即使设置了此选项,你仍然可以通过附加到 initrd 的另一个 bootconfig 来覆盖嵌入的 bootconfig。

通过启动配置传递内核参数

除了内核命令行外,启动配置还可以用于传递内核参数。所有在 kernel 键下的键值对都将直接传递给内核命令行。此外,init 下的键值对将通过命令行传递给 init 进程。这些参数按照以下顺序与用户给出的内核命令行字符串拼接,以便命令行参数可以覆盖 bootconfig 参数(这取决于子系统如何处理参数,但通常后出现的参数会覆盖先出现的参数。)

[bootconfig params][cmdline params] -- [bootconfig init params][cmdline init params]

这是一个用于内核/init 参数的 bootconfig 文件示例。

kernel {
  root = 01234567-89ab-cdef-0123-456789abcd
}
init {
 splash
}

这将被复制到内核命令行字符串中,如下所示

root="01234567-89ab-cdef-0123-456789abcd" -- splash

如果用户给出了其他一些命令行,例如,

ro bootconfig -- quiet

最终的内核命令行将如下所示

root="01234567-89ab-cdef-0123-456789abcd" ro bootconfig -- splash quiet

配置文件限制

目前最大配置大小为 32KB,总键名单词(非键值条目)必须在 1024 个节点以内。注意:这不是条目数而是节点数,一个条目必须消耗超过 2 个节点(一个键名单词和一个值)。所以理论上,它最多可包含 512 个键值对。如果键平均包含 3 个单词,则可以包含 256 个键值对。在大多数情况下,配置项的数量会在 100 个条目以下且小于 8KB,因此应该足够。如果节点数超过 1024,即使文件大小小于 32KB,解析器也会返回错误。(请注意,最大大小不包括填充的空字符。)无论如何,由于 bootconfig 命令在将启动配置附加到 initrd 镜像时会进行验证,用户可以在启动前察觉到。

Bootconfig API

用户可以查询或循环遍历键值对,也可以找到根(前缀)键节点并查找该节点下的键值对。

如果你有一个键字符串,可以使用 xbc_find_value() 直接查询其值。如果你想知道启动配置中存在哪些键,可以使用 xbc_for_each_key_value() 迭代键值对。注意,你需要使用 xbc_array_for_each_value() 来访问每个数组的值,例如:

vnode = NULL;
xbc_find_value("key.word", &vnode);
if (vnode && xbc_node_is_array(vnode))
   xbc_array_for_each_value(vnode, value) {
     printk("%s ", value);
   }

如果你想专注于具有某个前缀字符串的键,可以使用 xbc_find_node() 通过前缀字符串查找节点,并使用 xbc_node_for_each_key_value() 迭代该前缀节点下的键。

但最典型的用法是在前缀下获取指定名称的值或数组,如下所示

root = xbc_find_node("key.prefix");
value = xbc_node_find_value(root, "option", &vnode);
...
xbc_node_for_each_array_value(root, "array-option", value, anode) {
   ...
}

这会访问 “key.prefix.option” 的值和 “key.prefix.array-option” 的数组。

不需要锁定,因为在初始化之后,配置变为只读。如果需要修改,必须复制所有数据和键。

函数和结构

uint32_t xbc_calc_checksum(const void *data, uint32_t size)

计算 bootconfig 的校验和

参数

const void *data

Bootconfig 数据。

uint32_t size

bootconfig 数据的大小。

描述

计算 bootconfig 数据的校验和值。该校验和将与 BOOTCONFIG_MAGIC 和大小一起用于将 bootconfig 嵌入 initrd 镜像。

bool xbc_node_is_value(struct xbc_node *node)

测试节点是否为值节点

参数

struct xbc_node *node

一个 XBC 节点。

描述

测试 node 是否为值节点,如果是则返回 true,否则返回 false。

bool xbc_node_is_key(struct xbc_node *node)

测试节点是否为键节点

参数

struct xbc_node *node

一个 XBC 节点。

描述

测试 node 是否为键节点,如果是则返回 true,否则返回 false。

bool xbc_node_is_array(struct xbc_node *node)

测试节点是否为数组成员值节点

参数

struct xbc_node *node

一个 XBC 节点。

描述

测试 node 是否为数组成员值节点。

bool xbc_node_is_leaf(struct xbc_node *node)

测试节点是否为叶子键节点

参数

struct xbc_node *node

一个 XBC 节点。

描述

测试 node 是否为叶子键节点,即该节点是键节点且拥有值节点或没有子节点。如果是叶子节点则返回 true,否则返回 false。注意叶子节点除了值节点外还可以有子键节点。

const char *xbc_find_value(const char *key, struct xbc_node **vnode)

查找匹配键的值

参数

const char *key

搜索键

struct xbc_node **vnode

用于存放 XBC 值节点指针的容器。

描述

从整个 XBC 树中搜索其键匹配 key 的值,如果找到则返回该值。找到的值节点存储在 *vnode 中。注意,对于仅包含键(无值)的条目,这可能会返回 0 长度字符串并将 NULL 存储在 *vnode 中。

struct xbc_node *xbc_find_node(const char *key)

查找匹配键的节点

参数

const char *key

搜索键

描述

从整个 XBC 树中搜索其键匹配 key 的(键)节点,如果找到则返回该节点。如果未找到,则返回 NULL。

struct xbc_node *xbc_node_get_subkey(struct xbc_node *node)

如果存在,返回第一个子键节点

参数

struct xbc_node *node

父节点

描述

返回 node 的第一个子键节点。如果 node 没有子节点或只有值节点,则返回 NULL。

xbc_array_for_each_value

xbc_array_for_each_value (anode, value)

迭代数组上的值节点

参数

anode

一个 XBC 数组成员值节点

value

一个值

描述

迭代从 anode 开始的数组值节点和值。这预期与 xbc_find_value()xbc_node_find_value() 结合使用,以便用户可以处理每个数组条目节点。

xbc_node_for_each_child

xbc_node_for_each_child (parent, child)

迭代子节点

参数

parent

一个 XBC 节点。

child

迭代的 XBC 节点。

描述

迭代 parent 的子节点。每个子节点都存储到 child 中。child 可能是值节点和子键节点的混合。

xbc_node_for_each_subkey

xbc_node_for_each_subkey (parent, child)

迭代子键节点

参数

parent

一个 XBC 节点。

child

迭代的 XBC 节点。

描述

迭代 parent 的子键节点。每个子节点都存储到 child 中。此时 child 仅为子键节点。

xbc_node_for_each_array_value

xbc_node_for_each_array_value (node, key, anode, value)

迭代给定键的数组条目

参数

node

一个 XBC 节点。

key

node 下搜索的键字符串

anode

数组条目的已迭代 XBC 节点。

value

数组条目的已迭代值。

描述

迭代 node 下给定 key 的数组条目。每个数组条目节点存储在 anodevalue 中。如果 node 没有 key 节点,则不执行任何操作。注意,即使找到的键节点只有一个值(不是数组),这也会执行一次代码块。但是,如果找到的键节点没有值(仅键节点),这也不执行任何操作。因此,请勿将其用于测试键值对是否存在。

xbc_node_for_each_key_value

xbc_node_for_each_key_value (node, knode, value)

迭代节点下的键值对

参数

node

一个 XBC 节点。

knode

已迭代的键节点

value

已迭代的值字符串

描述

迭代 node 下的键值对。每个键节点和值字符串分别存储在 knodevalue 中。

xbc_for_each_key_value

xbc_for_each_key_value (knode, value)

迭代整个树的键值对

参数

knode

已迭代的键节点

value

已迭代的值字符串

描述

迭代整个 XBC 树中的键值对。每个键节点和值字符串分别存储在 knodevalue 中。

int xbc_node_compose_key(struct xbc_node *node, char *buf, size_t size)

合成 XBC 节点的完整键字符串

参数

struct xbc_node *node

一个 XBC 节点。

char *buf

用于存储键的缓冲区。

size_t size

buf 的大小。

描述

node 的完整长度键合成为 buf。返回存储在 buf 中的键的总长度。如果 node 为 NULL 则返回 -EINVAL,如果键深度超过最大深度则返回 -ERANGE。

int xbc_get_info(int *node_size, size_t *data_size)

获取已加载启动配置的信息

参数

int *node_size

存储节点数量的指针。

size_t *data_size

存储启动配置数据大小的指针。

描述

如果在 node_size 非空,则将已用节点数存入其中;如果在 data_size 非空,则将启动配置数据的大小存入其中。如果启动配置已初始化,则返回 0,否则返回 -ENODEV。

struct xbc_node *xbc_root_node(void)

获取扩展启动配置的根节点

参数

void

无参数

描述

返回扩展启动配置根节点的地址。如果未初始化扩展启动配置,则返回 NULL。

uint16_t xbc_node_index(struct xbc_node *node)

获取 XBC 节点的索引

参数

struct xbc_node *node

要获取索引的目标节点。

描述

返回 node 在 XBC 节点列表中的索引号。

struct xbc_node *xbc_node_get_parent(struct xbc_node *node)

获取父 XBC 节点

参数

struct xbc_node *node

一个 XBC 节点。

描述

返回 node 的父节点。如果该节点是树的顶级节点,则返回 NULL。

struct xbc_node *xbc_node_get_child(struct xbc_node *node)

获取子 XBC 节点

参数

struct xbc_node *node

一个 XBC 节点。

描述

返回 node 的第一个子节点。如果该节点没有子节点,则返回 NULL。

struct xbc_node *xbc_node_get_next(struct xbc_node *node)

获取下一个兄弟 XBC 节点

参数

struct xbc_node *node

一个 XBC 节点。

描述

返回 node 的下一个兄弟节点。如果该节点没有下一个兄弟节点,则返回 NULL。注意,即使返回 NULL,也不意味着 node 没有兄弟节点。(你还需要检查父节点的子节点是否为 node。)

const char *xbc_node_get_data(struct xbc_node *node)

获取 XBC 节点的数据

参数

struct xbc_node *node

一个 XBC 节点。

描述

返回 node 的数据(始终是以 null 结尾的字符串)。如果节点数据无效,则发出警告并返回 NULL。

struct xbc_node *xbc_node_find_subkey(struct xbc_node *parent, const char *key)

查找与给定键匹配的子键节点

参数

struct xbc_node *parent

一个 XBC 节点。

const char *key

键字符串。

描述

parent 下搜索匹配 key 的键节点。key 可以包含多个用 ‘.’ 连接的单词。如果 parent 为 NULL,则在整个树中搜索节点。如果没有匹配的节点,则返回 NULL。

const char *xbc_node_find_value(struct xbc_node *parent, const char *key, struct xbc_node **vnode)

查找与给定键匹配的值节点

参数

struct xbc_node *parent

一个 XBC 节点。

const char *key

键字符串。

struct xbc_node **vnode

用于存放找到的 XBC 节点的容器指针。

描述

parent 下搜索其(父)键节点匹配 key 的值节点,将其存储在 *vnode 中,并返回值字符串。key 可以包含多个用 ‘.’ 连接的单词。如果 parent 为 NULL,则在整个树中搜索节点。如果找到匹配的键,则返回值字符串;如果没有匹配的节点,则返回 NULL。注意,如果该键没有值,这将返回 0 长度字符串并将 NULL 存储在 *vnode 中。此外,如果值为数组,它将返回第一个条目的值。

int xbc_node_compose_key_after(struct xbc_node *root, struct xbc_node *node, char *buf, size_t size)

合成 XBC 节点的部分键字符串

参数

struct xbc_node *root

根 XBC 节点

struct xbc_node *node

目标 XBC 节点。

char *buf

用于存储键的缓冲区。

size_t size

buf 的大小。

描述

node 的部分键合成为 buf,该键从 root 之后开始(不包括 root)。如果 root 为 NULL,这将返回 node 的完整键名。返回存储在 buf 中的键的总长度。如果 node 为 NULL、root 不是 node 的祖先或 root 就是 node,则返回 -EINVAL;如果键深度超过最大深度,则返回 -ERANGE。这预期与 xbc_find_node() 结合使用,以列出给定键下的所有(子)键。

struct xbc_node *xbc_node_find_next_leaf(struct xbc_node *root, struct xbc_node *node)

查找给定节点下的下一个叶子节点

参数

struct xbc_node *root

一个 XBC 根节点

struct xbc_node *node

开始查找的 XBC 节点。

描述

root 节点(包括 root 节点本身)下搜索 node 的下一个叶子节点(即终点键节点)。返回下一个节点,如果未找到下一个叶子节点,则返回 NULL。

const char *xbc_node_find_next_key_value(struct xbc_node *root, struct xbc_node **leaf)

查找下一个键值对节点

参数

struct xbc_node *root

一个 XBC 根节点

struct xbc_node **leaf

开始搜索的 XBC 节点的容器指针。

描述

root 节点下搜索 *leaf 的下一个叶子节点(即终点键节点)。如果找到下一个叶子节点,则返回值并更新 *leaf,如果未找到下一个叶子节点,则返回 NULL。注意,如果该键没有值,这将返回 0 长度字符串,如果值为数组,则返回第一个条目的值。

int xbc_snprint_cmdline(char *buf, size_t size, struct xbc_node *root)

root 下的 bootconfig 键渲染为命令行字符串

参数

char *buf

目标缓冲区(当 size 为 0 用于查询长度时可能为 NULL)

size_t size

buf 的字节大小

struct xbc_node *root

其键=值对需要渲染的子树根

描述

遍历 root 下的所有键/值对,并将它们作为空格分隔的命令行字符串输出到 buf。包含空格的值用双引号括起来。返回如果 buf 足够大(匹配 snprintf 语义)时将写入的字节数,或失败时返回负 errno。

void _xbc_exit(bool early)

清除所有已解析的 bootconfig

参数

bool early

如果在伙伴系统初始化之前调用,则设置为 true。

描述

这会清除内存中已解析引导配置的所有数据结构。如果你需要用新的启动配置重用 xbc_init(),可以使用此函数。

int xbc_init(const char *data, size_t size, const char **emsg, int *epos)

解析给定的 XBC 文件并构建 XBC 内部树

参数

const char *data

启动配置文本的原始数据

size_t size

data 的大小

const char **emsg

用于存储错误消息的 const char * 类型的指针

int *epos

用于存储错误位置的 int 类型的指针

描述

这会解析 data 中的引导配置文本。size 必须小于 XBC_DATA_MAX。如果成功,则返回存储的节点数(>0),如果有任何错误,则返回 -errno。在出错的情况下,emsg 将更新为错误消息,epos 将更新为错误位置(即 buf 的字节偏移量)。如果错误不是解析器错误,epos 将为 -1。