符号命名空间

以下文档描述了如何使用符号命名空间来组织通过 EXPORT_SYMBOL() 宏系列导出的内核内部符号的导出接口。

简介

引入符号命名空间是为了组织内核内部 API 的导出接口。它允许子系统维护者将其导出的符号划分到单独的命名空间中。这对于文档目的(例如 SUBSYSTEM_DEBUG 命名空间)以及限制一组符号在内核其他部分的可用性非常有用。目前,使用导出到命名空间中的符号的模块,需要导入该命名空间。否则,内核将根据其配置,拒绝加载该模块或警告缺少导入。

此外,还可以将符号放入模块命名空间中,严格限制哪些模块可以使用这些符号。

如何定义符号命名空间

符号可以使用不同的方法导出到命名空间中。所有这些方法都改变了 EXPORT_SYMBOL 及其相关宏生成 ksymtab 条目的方式。

使用 EXPORT_SYMBOL 宏

除了允许将内核符号导出到内核符号表的 EXPORT_SYMBOL() 和 EXPORT_SYMBOL_GPL() 宏之外,还有用于将符号导出到特定命名空间的变体:EXPORT_SYMBOL_NS() 和 EXPORT_SYMBOL_NS_GPL()。它们接受一个额外的参数:作为字符串常量的命名空间。请注意,此字符串不能包含空格。例如,要将符号 usb_stor_suspend 导出到命名空间 USB_STORAGE,请使用

EXPORT_SYMBOL_NS(usb_stor_suspend, "USB_STORAGE");

相应的 ksymtab 条目结构 kernel_symbol 将相应地设置成员 namespace。没有导出到命名空间的符号将指向 NULL。如果未定义,则没有默认命名空间。modpost 和 kernel/module/main.c 分别在构建时或模块加载时使用命名空间。

使用 DEFAULT_SYMBOL_NAMESPACE 定义

为一个子系统的所有符号定义命名空间可能非常冗长且难以维护。因此,提供了一个默认定义(DEFAULT_SYMBOL_NAMESPACE),如果设置,它将成为所有未指定命名空间的 EXPORT_SYMBOL() 和 EXPORT_SYMBOL_GPL() 宏扩展的默认值。

有多种方法可以指定此定义,具体取决于子系统和维护者的偏好选择哪种方法。第一种选择是在子系统的 Makefile 中定义默认命名空间。例如,要将 usb-common 中定义的所有符号导出到 USB_COMMON 命名空间,请在 drivers/usb/common/Makefile 中添加如下一行

ccflags-y += -DDEFAULT_SYMBOL_NAMESPACE='"USB_COMMON"'

这将影响所有 EXPORT_SYMBOL() 和 EXPORT_SYMBOL_GPL() 语句。当此定义存在时,使用 EXPORT_SYMBOL_NS() 导出的符号仍将被导出到作为命名空间参数传递的命名空间中,因为此参数优先于默认符号命名空间。

定义默认命名空间的第二种选择是直接在编译单元中作为预处理语句。上述示例将变为

#define DEFAULT_SYMBOL_NAMESPACE "USB_COMMON"

在相应的编译单元中,位于 <linux/export.h> 的 #include 之前。通常它放在第一个 #include 语句之前。

使用 EXPORT_SYMBOL_GPL_FOR_MODULES() 宏

使用此宏导出的符号被放入模块命名空间。此命名空间无法导入。

此宏接受一个逗号分隔的模块名称列表,只允许这些模块访问此符号。支持简单的尾部通配符。

例如

EXPORT_SYMBOL_GPL_FOR_MODULES(preempt_notifier_inc, "kvm,kvm-*")

将此符号的使用限制为名称与给定模式匹配的模块。

如何使用命名空间中导出的符号

为了使用导出到命名空间中的符号,内核模块需要显式导入这些命名空间。否则内核可能会拒绝加载该模块。模块代码需要使用 MODULE_IMPORT_NS 宏来导入其使用的符号所在的命名空间。例如,使用上述 usb_stor_suspend 符号的模块,需要使用如下语句导入 USB_STORAGE 命名空间

MODULE_IMPORT_NS("USB_STORAGE");

这将在模块中为每个导入的命名空间创建一个 modinfo 标签。这带来的副作用是,可以使用 modinfo 工具检查模块导入的命名空间。

$ modinfo drivers/usb/storage/ums-karma.ko
[...]
import_ns:      USB_STORAGE
[...]

建议将 MODULE_IMPORT_NS() 语句添加到靠近其他模块元数据定义(如 MODULE_AUTHOR() 或 MODULE_LICENSE())的位置。

加载使用命名空间符号的模块

在模块加载时(例如 insmod),内核将检查模块引用的每个符号的可用性,以及其可能导出到的命名空间是否已被模块导入。内核的默认行为是拒绝加载未指定足够导入的模块。将记录一个错误,并且加载将因 EINVAL 失败。为了允许加载不满足此前提条件的模块,提供了一个配置选项:设置 MODULE_ALLOW_MISSING_NAMESPACE_IMPORTS=y 将允许加载,但会发出警告。

自动创建 MODULE_IMPORT_NS 语句

在构建时可以很容易地检测到缺少的命名空间导入。实际上,如果模块使用来自某个命名空间的符号而未导入该命名空间,modpost 将发出警告。MODULE_IMPORT_NS() 语句通常会添加在特定位置(与其他模块元数据一起)。为了方便模块作者(和子系统维护者),提供了一个脚本和 make 目标来修复缺少的导入。可以通过以下方式修复缺少的导入:

$ make nsdeps

模块作者的典型场景是

- write code that depends on a symbol from a not imported namespace
- ``make``
- notice the warning of modpost telling about a missing import
- run ``make nsdeps`` to add the import to the correct code location

对于引入命名空间的子系统维护者,步骤非常相似。同样,make nsdeps 最终将为树内模块添加缺少的命名空间导入

- move or add symbols to a namespace (e.g. with EXPORT_SYMBOL_NS())
- ``make`` (preferably with an allmodconfig to cover all in-kernel
  modules)
- notice the warning of modpost telling about a missing import
- run ``make nsdeps`` to add the import to the correct code location

你也可以为外部模块构建运行 nsdeps。典型用法是

$ make -C <path_to_kernel_src> M=$PWD nsdeps

注意:它会很“乐意”地为模块命名空间生成导入语句;但这将不起作用,并导致构建和运行时失败。