上一篇<<linux设备模型:设备及设备节点创建过程分析>>中分析了设备的初始化及注册过程,包括设备与驱动绑定,设备与电源管理之间的联系、中断域的储存及物理设备之间的关系等等。
本篇分析固件系列(以efi为例),固件可以分为(系统)引导阶段(efi stub启动模式)、固件内存映射(物理地址映射到虚拟地址等等)、固件注册到平台设备、平台设备运行等等。固件设备具有更广泛的意义,当然复杂度也更高一些。
固件系列更偏向于开发板,通常由内核模块(服务)与应用程序完成一些硬件级上电/下电及设备形式控制(如更新固件)等等。参考Documentation中一些文章内容:
为了支持ACPI开放式硬件配置(例如开发板),我们需要一种方法来增强由固件映像提供的ACPI配置。一个常见的例子是连接开发板上I2C / SPI总线上的传感器。
尽管这可以通过创建内核平台驱动程序或使用更新的ACPI表重新编译固件映像来实现,但这两种方法都不实际:前者增加了特定于主板的内核代码,而后者需要访问固件工具,而这些工具通常不公开可用。
因为ACPI在AML代码中支持外部引用,所以增强固件ACPI配置的一种更实用的方法是动态加载用户定义的SSDT表,其中包含特定于单板的信息。然后,生成的AML代码可以由内核使用其中一种方法加载在下面。
从initrd加载ACPI ssdt
此选项允许从 initrd 加载用户定义的 SSDT,当系统不支持 EFI 或没有足够的 EFI 存储时,此选项非常有用。
它的工作方式与基于 initrd 的 ACPI 表覆盖/升级类似:SSDT AML 代码必须放置在“内核/固件/ACPI”路径下的第一个未压缩的 initrd 中。可以使用多个文件,这将转换为加载多个表。仅允许 SSDT 和 OEM 表。
下面是一个示例:
# Add the raw ACPI tables to an uncompressed cpio archive.
# They must be put into a /kernel/firmware/acpi directory inside the
# cpio archive.
# The uncompressed cpio archive must be the first.
# Other, typically compressed cpio archives, must be
# concatenated on top of the uncompressed one.
mkdir -p kernel/firmware/acpi
cp ssdt.aml kernel/firmware/acpi
# Create the uncompressed cpio archive and concatenate the original initrd
# on top:
find kernel | cpio -H newc --create > /boot/instrumented_initrd
cat /boot/initrd >>/boot/instrumented_initrd
从EFI变量加载ACPI ssdt
当平台上支持 EFI 时,这是首选方法,因为它允许以独立于操作系统的持久方式存储用户定义的 SSDT。此外,还正在努力实现对加载用户定义的 SSDT 的 EFI 支持,使用此方法将使在 EFI 加载机制到来时更容易转换为 EFI 加载机制。要启用它,CONFIG_EFI_CUSTOM_SSDT_OVERLAYS shoyld 被选为 y。
为了从 EFI 变量加载 SSDT,可以使用内核命令行参数(名称限制为 16 个字符)。选项的参数是要使用的变量名称。如果有多个变量具有相同的名称,但具有不同的供应商 GUID,则将加载所有这些变量。“efivar_ssdt=…”
为了将 AML 代码存储在 EFI 变量中,可以使用 efivarfs 文件系统。默认情况下,它在所有最近的发行版中启用并挂载在 /sys/firmware/efi/efivars 中。
在 /sys/firmware/efi/efivars 中创建新文件将自动创建一个新的 EFI 变量。更新 /sys/firmware/efi/efivars 中的文件将更新 EFI 变量。请注意,文件名需要特殊格式化为“Name-GUID”,文件中的前 4 个字节(小端格式)表示 EFI 变量的属性(请参阅 include/linux/efi.h 中的EFI_VARIABLE_MASK)。写入文件也必须通过一个写入操作完成。
例如,您可以使用以下 bash 脚本使用给定文件中的内容创建/更新 EFI 变量:
#!/bin/sh -e
while [ -n "$1" ]; do
case "$1" in
"-f") filename="$2"; shift;;
"-g") guid="$2"; shift;;
*) name="$1";;
esac
shift
done
usage()
{
echo "Syntax: ${0##*/} -f filename [ -g guid ] name"
exit 1
}
[ -n "$name" -a -f "$filename" ] || usage
EFIVARFS="/sys/firmware/efi/efivars"
[ -d "$EFIVARFS" ] || exit 2
if stat -tf $EFIVARFS | grep -q -v de5e81e4; then
mount -t efivarfs none $EFIVARFS
fi
# try to pick up an existing GUID
[ -n "$guid" ] || guid=$(find "$EFIVARFS" -name "$name-*" | head -n1 | cut -f2- -d-)
# use a randomly generated GUID
[ -n "$guid" ] || guid="$(cat /proc/sys/kernel/random/uuid)"
# efivarfs expects all of the data in one write
tmp=$(mktemp)
/bin/echo -ne "\007\000\000\000" | cat - $filename > $tmp
dd if=$tmp of="$EFIVARFS/$name-$guid" bs=$(stat -c %s $tmp)
rm $tmp
从配置加载ACPI SSDTs
此选项允许通过 configfs 接口从用户空间加载用户定义的 SSDT。必须选择CONFIG_ACPI_CONFIGFS选项,并且必须挂载配置。在下面的例子中,我们假设configfs已经挂载在/sys/kernel/config中。
可以通过在 /sys/kernel/config/acpi/table 中创建新目录并在 aml 属性中写入 SSDT AML 代码来加载新表:
cd /sys/kernel/config/acpi/table
mkdir my_ssdt
cat ~/ssdt.aml > my_ssdt/aml
本篇文章主要分析从固件设备创建到(efi)平台设备的运行过程等等,(系统)引导阶段之前的文章有过记录,这里不再分析。
固件设备与块设备、字符设备类似,首先创建根kobj(firmware_kobj),各类固件设备将以它为基础。
efi平台设备运行过程如下:
efi系统初始化
映射efi系统表物理地址,地址有效的情况下输出EFI版本等信息,进入EFI虚拟模式后,许多配置表条目被重新映射到虚拟地址,并将这些条目更正为它们各自的物理地址,目前只处理一些固件实现所必需的smbios,检查配置表条目是否有效,解析/赋值固件表信息,分析EFI属性表,设置内存属性特征(运行时数据区域映射为不可执行),可以使用运行时服务,在运行时打印额外的调试信息等等。
efi加载执行模式
kexec_enter_virtual_mode
efi子系统初始化
分配efi订单(order)工作队列 efi_rts_wq,添加平台级设备(“rtc-efi”)及其资源,分配efi kobj,指定固件kobj为父级,注册操作EFI变量的工具和库(内核部分) efivar,执行efivar内核(服务),用于动态加载用户定义的ssdt表,添加平台级设备efivars,efi_kobj创建属性组,efi运行时映射初始化,并创建挂载点等等。
目录
1. 函数分析
1.1 firmware_init
分配固件根对象firmware_kobj,各类固件设备将以它为基础
int __init firmware_init(void)
{
firmware_kobj = kobject_create_and_add("firmware", NULL); // // 分配并初始化kobj对象firmware_kobj
// 固件根对象
if (!firmware_kobj)
return -ENOMEM;
return 0;
}
1.2 efi_init
efi系统初始化
映射efi系统表物理地址,地址有效的情况下输出EFI版本等信息
进入EFI虚拟模式后,许多配置表条目被重新映射到虚拟地址
然而,kexec内核需要它们的物理地址,因此我们通过setup_data传递它们,
并将这些条目更正为它们各自的物理地址,目前只处理一些固件实现所必需的smbios
检查配置表条目是否有效,解析/赋值固件表信息
分析EFI属性表,设置内存属性特征(运行时数据区域映射为不可执行)
可以使用运行时服务,在运行时打印额外的调试信息
void __init efi_init(void)
{
...
efi_systab_phys = boot_params.efi_info.efi_systab |
((__u64)boot_params.efi_info.efi_systab_hi << 32); // efi系统表物理地址
if (efi_systab_init(efi_systab_phys)) // 映射efi系统表物理地址,地址有效的情况下输出EFI版本等信息
return;
/*