Linux在x86与ARM架构上的设备树需求差异:技术原理与历史演进
引言:
在Linux内核开发和嵌入式系统领域,设备树(Device Tree)是一个经常被讨论的话题。有趣的是,Linux在不同硬件架构上对设备树的依赖程度存在显著差异:在ARM架构上,设备树已成为标准配置方法,而在x86架构上,系统却能够在没有设备树的情况下正常工作。这种差异不仅反映了两种架构的技术特点,也体现了计算机体系结构发展的历史轨迹和设计哲学。
本文将深入探讨为什么Linux在ARM平台上需要设备树,而在x86平台上却不需要,分析背后的技术原理、历史演变以及未来发展趋势。通过这种分析,我们可以更好地理解不同计算机架构的设计理念,以及Linux内核如何适应多样化的硬件平台。
一句话总结,:x86有一个叫acpi的高级设备树,同时pcie总线还有自枚举功能,多数x86外设都挂在pcie总线上
设备树的基本概念:
设备树的定义与结构
设备树是一种描述硬件的数据结构,它以树状结构表示计算机系统中的硬件设备及其属性。在Linux中,设备树通常以文本形式(DTS,Device Tree Source)编写,经过设备树编译器(Device Tree Compiler,DTC)编译后生成二进制形式(DTB,Device Tree Blob),然后加载到内核中,为内核提供必要的硬件信息。
设备树的基本结构包括节点(node)和属性(property)。节点代表系统中的设备或总线,可以包含子节点,形成树状结构;属性则是节点的特性描述,如地址范围、中断号等。一个简化的设备树示例如下:
/dts-v1/;
/ {
compatible = "vendor,example-board";
model = "Vendor Example Board";
#address-cells = <1>;
#size-cells = <1>;
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512MB RAM */
};
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart@10000000 {
compatible = "vendor,example-uart";
reg = <0x10000000 0x1000>;
interrupts = <0 42 4>;
clocks = <&clk_uart>;
status = "okay";
};
i2c@10001000 {
compatible = "vendor,example-i2c";
reg = <0x10001000 0x1000>;
interrupts = <0 43 4>;
clock-frequency = <100000>;
status = "okay";
eeprom@50 {
compatible = "at24,24c256";
reg = <0x50>;
pagesize = <64>;
};
};
};
clocks {
clk_uart: uart_clk {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <24000000>;
};
};
};
设备树的作用
设备树的主要作用是解耦硬件描述和内核代码。在没有设备树的情况下,硬件信息通常硬编码在内核代码中,这导致内核与特定硬件平台紧密耦合,难以移植和维护。设备树的引入,使得内核可以动态地获取硬件信息,从而支持更广泛的硬件平台,并简化了内核的维护工作。
具体来说,设备树具有以下几个方面的作用:
-
硬件描述:设备树描述了系统中各个设备的类型、地址、中断号、时钟频率等信息,使得内核能够正确地识别和配置这些设备。
-
驱动匹配:设备树中的设备节点包含了设备的兼容性信息(compatible),内核可以根据这些信息找到对应的驱动程序,实现驱动的自动加载和绑定。
-
平台配置:设备树可以配置系统的启动参数、内存布局、中断路由等,使得内核能够根据不同的硬件平台进行定制。
-
分离硬件描述与内核代码:设备树将硬件描述从内核代码中分离出来,使得添加新板卡支持不再需要修改内核源码。只需为新板卡创建相应的设备树文件,编译成DTB格式,并在启动时加载即可。
x86架构的设备发现机制:
要理解为什么x86不需要设备树,我们需要先了解x86架构的设备发现机制。x86架构有着长达数十年的发展历史,在这个过程中形成了一套完整的硬件自描述机制。
BIOS/UEFI与硬件抽象层
x86平台上的BIOS(Basic Input/Output System)或其现代替代品UEFI(Unified Extensible Firmware Interface)提供了硬件抽象层,它们在操作系统启动前初始化硬件,并提供标准接口供操作系统访问硬件资源。
BIOS通过中断向量表(Interrupt Vector Table,IVT)提供了一系列硬件访问接口,操作系统可以通过这些接口来访问硬件。例如,INT 10h用于访问视频服务,INT 13h用于访问磁盘服务等。
UEFI则提供了更现代化的接口,它使用可扩展固件接口(Extensible Firmware Interface,EFI)来描述硬件信息,操作系统可以通过EFI接口来获取硬件配置。UEFI还提供了运行时服务(Runtime Services)和启动时服务(Boot Services),为操作系统提供更强大的硬件访问能力。
ACPI(高级配置与电源接口)
ACPI(Advanced Configuration and Power Interface)是x86平台上最重要的硬件描述机制之一。它提供了一套标准化的接口,用于描述系统硬件配置、电源管理功能以及设备枚举。
ACPI使用AML(ACPI Machine Language)描述硬件特性和行为,这些描述存储在ACPI表中,由固件提供给操作系统。主要的ACPI表包括:
- RSDT(Root System Description Table):根系统描述表,包含指向其他ACPI表的指针。
- DSDT(Differentiated System Description Table):差分系统描述表,包含大部分设备的描述信息。
- SSDT(Secondary System Description Table):次级系统描述表,包含补充的设备描述信息。
- FADT(Fixed ACPI Description Table):固定ACPI描述表,包含系统硬件的固定配置信息。
Linux内核解析这些表,获取设备信息,并据此加载相应的驱动程序。ACPI还提供了一系列方法(如_STA、_CRS、_PRS等),用于查询设备状态、资源需求等信息。
PCI总线的自发现机制
PCI(Peripheral Component Interconnect)总线是x86架构中广泛使用的外设总线标准。PCI设计了完善的设备枚举和自发现机制。每个PCI设备都有标准化的配置空间,包含厂商ID、设备ID、类别码等信息。
操作系统可以通过扫描PCI总线,读取每个设备的配置空间,从而发现并识别系统中的PCI设备。以下是PCI设备枚举的简化代码示例:
void pci_scan_bus(void)
{
for (int bus = 0; bus < 256; bus++) {
for (int dev = 0; dev < 32; dev++) {
for (int func = 0; func < 8; func++) {
u16 vendor_id = pci_read_config_word(bus, dev, func, PCI_VENDOR_ID);
if (vendor_id != 0xFFFF) {
/* 找到一个PCI设备 */
u16 device_id = pci_read_config_word(bus, dev, func, PCI_DEVICE_ID);
u8 class_code = pci_read_config_byte(bus, dev, func, PCI_CLASS_CODE);
/* 根据设备信息加载相应驱动 */
}
}
}
}
}
在x86平台上,PCI配置空间的访问是通过标准的I/O端口或内存映射方式实现的:
u32 pci_read_config_dword(u8 bus, u8 dev, u8 func, u8 offset)
{
u32 address = (1 << 31) | (bus << 16) | (dev << 11) | (func << 8) | offset;
outl(address, 0xCF8);
return inl(0xCFC);
}
USB的自描述能力
类似于PCI,USB(Universal Serial Bus)也具有强大的自描述能力。USB设备通过描述符(Descriptor)提供其功能和特性信息。当USB设备连接到系统时,主机控制器会读取设备的描述符,获取设备类型、厂商、产品等信息,然后操作系统根据这些信息加载适当的驱动程序。
主要的USB描述符包括:
- 设备描述符(Device Descriptor):包含设备的基本信息,如厂商ID、产品ID、设备类别等。
- 配置描述符(Configuration Descriptor):描述设备的配置信息,如电源需求、接口数量等。
- 接口描述符(Interface Descriptor):描述设备提供的功能接口,如HID(Human Interface Device)、存储、音频等。
- 端点描述符(Endpoint Descriptor):描述数据传输的端点,如传输类型、方向、最大包大小等。
SMBios/DMI
SMBios(System Management BIOS)或DMI(Desktop Management Interface)提供了关于系统硬件组件的详细信息,如主板、处理器、内存等。Linux通过解析SMBios表获取这些信息,用于系统识别和硬件管理。
这些自发现机制共同构成了x86平台的硬件描述框架,使得操作系统能够自动识别和配置硬件,而无需额外的设备树。
ARM架构的设备发现挑战:
相比于x86平台,ARM架构面临着截然不同的设备发现挑战:
ARM架构的多样性和碎片化
ARM不是单一的硬件平台,而是一种CPU架构,被广泛应用于各种嵌入式系统、移动设备和服务器。不同厂商可以基于ARM架构设计各种SoC(System on Chip),集成不同的外设和功能模块。这种多样性导致没有统一的硬件标准,每个ARM平台可能有完全不同的外设配置。
ARM架构的碎片化主要体现在以下几个方面:
-
SoC厂商众多:不同的SoC厂商(如高通、联发科、三星、华为等)设计了各种各样的ARM处理器,这些处理器在架构、外设接口、中断控制器等方面存在差异。
-
外设接口多样:ARM平台的外设接口种类繁多,如UART、SPI、I2C、USB、Ethernet等。不同的平台可能使用不同的外设接口,或者使用相同的外设接口但配置不同。
-
中断控制器各异:ARM平台的中断控制器种类繁多,如GIC(Generic Interrupt Controller)、VIC(Vectored Interrupt Controller)等。不同的平台可能使用不同的中断控制器,或者使用相同的中断控制器但配置不同。
缺乏标准化的自发现机制
与x86不同,ARM平台传统上缺乏像ACPI或PCI那样的标准化自发现机制。早期的ARM系统通常是为特定应用定制的,硬件配置相对固定,因此没有强烈的需求开发通用的设备发现标准。
在ARM平台上,访问设备寄存器通常需要知道其物理地址,这些信息在没有设备树的情况下,需要硬编码在内核中:
#define UART_BASE_ADDR 0x10000000
#define UART_REG_DATA 0x00
#define UART_REG_STATUS 0x04
void uart_send_char(char c)
{
volatile unsigned int *uart_data = (unsigned int *)(UART_BASE_ADDR + UART_REG_DATA);
volatile unsigned int *uart_status = (unsigned int *)(UART_BASE_ADDR + UART_REG_STATUS);
/* 等待发送缓冲区为空 */
while (*uart_status & (1 << 5))
;
/* 发送字符 */
*uart_data = c;
}
板级支持包(BSP)的问题
在设备树出现之前,Linux对ARM平台的支持主要依赖于板级支持包(Board Support Package,BSP)。每种ARM板卡都需要在内核中添加专门的代码,描述其硬件配置。这种方法导致内核代码膨胀,且每添加一种新板卡都需要修改内核源码,不利于维护和扩展。
以下是设备树出现前ARM平台上硬件描述的简化示例:
/* 针对特定板卡的硬件描述代码 */
static struct resource example_board_uart_resources[] = {
{
.start = 0x10000000,
.end = 0x10000FFF,
.flags = IORESOURCE_MEM,
},
{
.start = 42,
.end = 42,
.flags = IORESOURCE_IRQ,
},
};
static struct platform_device example_board_uart = {
.name = "vendor-uart",
.id = 0,
.resource = example_board_uart_resources,
.num_resources = ARRAY_SIZE(example_board_uart_resources),
};
static void __init example_board_init(void)
{
platform_device_register(&example_board_uart);
/* 注册其他设备... */
}
MACHINE_START(EXAMPLE_BOARD, "Example Board")
.init_machine = example_board_init,
/* 其他初始化函数... */
MACHINE_END
这种方法的缺点显而易见:每添加一种新的板卡,就需要在内核中添加类似上面的代码,导致内核代码膨胀,且维护困难。Linux内核中曾经有数百个这样的板级支持文件,使得内核代码变得臃肿不堪。
设备树解决方案的引入:
为了解决ARM平台上的设备发现问题,Linux社区引入了设备树机制。
设备树的历史起源
设备树最初源于OpenFirmware,这是一种由Sun Microsystems开发的固件标准,后来被IEEE 1275标准采纳。PowerPC架构的计算机(如早期的苹果Mac电脑)使用OpenFirmware作为启动固件,它使用设备树来描述硬件配置。
随着PowerPC架构在Linux中的发展,设备树机制被引入到Linux内核中。最终,在Linux 3.x内核版本中,设备树成为ARM架构的标准配置方法。这一变化被称为"ARM平台的大规模重构"(ARM Platform Consolidation),它显著减少了内核代码量,并提高了ARM平台的可维护性。
设备树在ARM Linux中的实现
在ARM Linux中,设备树的实现涉及多个层面:
-
设备树源文件(DTS):设备树源文件是人类可读的文本文件,使用特定语法描述硬件配置。
-
设备树编译器(DTC):设备树编译器将DTS文件编译成二进制格式的DTB文件,供内核加载使用。编译命令示例:
dtc -I dts -O dtb -o example-board.dtb example-board.dts
-
设备树在引导过程中的加载:在ARM系统启动时,bootloader(如U-Boot)负责加载内核镜像和DTB文件到内存,并将DTB的内存地址传递给内核。内核启动后,解析DTB获取硬件信息,并据此初始化设备驱动。
-
内核中的设备树解析:Linux内核包含设备树解析代码,负责读取DTB并构建内部设备模型。以下是设备树解析的简化流程:
void __init setup_arch(char **cmdline_p)
{
/* ... */
/* 获取设备树地址 */
extern char __dtb_start[];
extern char __dtb_end[];
void *dtb = __dtb_start;
size_t dtb_size = __dtb_end - __dtb_start;
/* 解析设备树 */
early_init_dt_scan(dtb);
/* ... */
}
static void __init early_init_dt_scan(void *dtb)
{
/* ... */
/* 遍历设备树节点 */
of_scan_flat_dt(early_init_dt_scan_root, NULL);
/* ... */
}
static void early_init_dt_scan_root(const struct of_flat_dt_entry *node, void *data)
{
/* 处理根节点 */
if (of_flat_dt_is_compatible(node, "vendor,example-board")) {
/* 初始化板级硬件 */
}
/* 扫描子节点 */
of_scan_flat_dt(early_init_dt_scan_memory, NULL);
of_scan_flat_dt(early_init_dt_scan_devices, NULL);
}
- 设备驱动的匹配:内核根据设备树中的兼容性信息(compatible属性)将设备与驱动程序匹配。驱动程序通过of_match_table声明其支持的设备类型:
static const struct of_device_id example_uart_dt_ids[] = {
{ .compatible = "vendor,example-uart" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, example_uart_dt_ids);
static struct platform_driver example_uart_driver = {
.probe = example_uart_probe,
.remove = example_uart_remove,
.driver = {
.name = "example-uart",
.of_match_table = example_uart_dt_ids,
},
};
设备树解决的问题
设备树为ARM平台带来了以下几个方面的改进:
-
减少内核代码量:设备树将硬件描述从内核代码中分离出来,显著减少了内核中针对特定板卡的代码量。
-
提高可维护性:添加新板卡支持不再需要修改内核源码,只需创建相应的设备树文件即可,简化了维护工作。
-
统一硬件描述方法:设备树为ARM平台提供了统一的硬件描述方法,使得不同厂商的ARM平台可以使用相同的机制描述硬件。
-
支持动态配置:设备树支持运行时修改和覆盖,使得同一内核镜像可以支持多种硬件配置,提高了灵活性。
x86与ARM架构差异的深层次分析:
历史发展路径差异
x86和ARM的历史发展路径有着根本性的差异,这直接影响了它们的设备发现机制:
-
x86的标准化历程:x86架构源于个人计算机领域,经历了数十年的发展和标准化过程。从最初的IBM PC到现代的个人电脑和服务器,x86平台形成了一套完整的硬件标准,包括BIOS/UEFI、ACPI、PCI等。这些标准保证了不同厂商的x86设备可以互操作,也为操作系统提供了一致的硬件抽象层。
-
ARM的嵌入式起源:ARM架构起源于嵌入式系统领域,最初设计用于特定应用,如早期的Acorn计算机。在嵌入式领域,系统往往是为特定应用定制的,硬件配置相对固定,因此没有强烈的需求开发通用的设备发现标准。随着ARM架构进入移动设备和服务器领域,硬件配置变得更加复杂和多样化,但缺乏像x86那样的统一标准。
设计哲学差异
x86和ARM在设计哲学上也存在显著差异:
-
x86的向后兼容性:x86架构高度重视向后兼容性,新的x86处理器必须能够运行为旧处理器设计的软件。这种兼容性要求导致x86架构保留了许多历史特性,如实模式、BIOS中断等。这些特性构成了x86平台的硬件自描述机制的基础。
-
ARM的简洁设计:ARM架构强调简洁和效率,设计理念是"做得更少,但做得更好"(Do less, but do it better)。ARM处理器通常只包含必要的功能,减少了不必要的复杂性。这种设计理念使得ARM处理器功耗低、效率高,但也导致缺乏像x86那样丰富的硬件自描述机制。
硬件集成度的差异
x86和ARM在硬件集成度上也存在显著差异:
-
x86的分立架构:传统的x86系统采用分立架构,处理器、芯片组、外设等组件由不同厂商生产,通过标准接口(如PCI)连接。这种架构需要标准化的设备发现机制,以确保不同厂商的组件可以互操作。
-
ARM的SoC架构:ARM系统通常采用SoC架构,将处理器、内存控制器、外设等集成在单一芯片上。SoC由单一厂商设计和生产,因此不需要像x86那样的标准化设备发现机制。不同厂