Skip to content

第 3 章 设备树的基础骨架

想象一下,你刚把 Linux 内核移植到了一块新的板子上。编译很顺利,烧录也没报错,但串口里就是死一样的寂静——没有任何启动日志。你检查了波特率,确认了 TX/RX 引脚,甚至重新焊了一遍电源线。问题到底在哪?

后来你发现,是因为内核根本不知道这块板子上有一块内存可用,也不知道该用哪个串口输出信息。内核就像一个蒙着眼睛被扔进陌生房间的巨人,它有力气,有脑子,但没有「地图」。

本章的任务,就是画这张地图。这张地图就是设备树,但在这张地图上画山川河流之前,我们得先确立它的坐标系和边界。换句话说,我们需要先搞清楚:无论多简单的板子,哪些节点是绝对不能少的? 这不仅是规范的问题,更是为了让内核在启动的第一微秒就能找到「北」。


3.1 必不可少的三块基石

设备树的规范其实很霸道,它说你可以有很多花哨的设备,但有两个节点只要你想让内核跑起来,就必须有。一个是 /cpus 告诉内核「谁在干活」,一个是 /memory 告诉内核「活儿记在哪」。当然,所有这一切都必须挂在一个唯一的根节点 / 下面。

听起来很简单?别急,这两个节点加上根节点构成了整个系统的物理拓扑基础。如果这两个节点描述错了或者干脆忘了写,内核就会在解压后的第一时间 panic——连报错信息都打印不出来的那种。

我们先来看看这棵树的「根」。


3.2 根节点:一切从这里开始

设备树必须有一个且只能有一个根节点,所有其他的设备——无论是 CPU、内存还是外设——都是它的子孙节点。它的路径就是简单的 /。但这棵树的根部不仅仅是个挂载点,它还得定义一些全局的「系统规则」。

根节点下最重要的属性之一是规定子节点的地址格式。在设备树里,地址和长度是用一个个 32 位单元格(cell)组成的数组表示的。但问题是:在这个特定的系统里,表示一个地址需要几个 cell? 在 32 位系统里通常 1 个 cell (<u32>) 就能表示一个地址,但在 64 位系统里你可能需要 2 个 cell (<u64>) 来装下一个物理地址。

根节点通过 #address-cells#size-cells 这两个属性告诉子节点:「在这个板子上,地址用 X 个 cell 表示,长度用 Y 个 cell 表示。」这里 # 号开头的属性并不是什么特殊语法,它只是属性名的一部分,这是一种约定俗成的命名方式用来表示这是一个「数量」值而不是一个数据值。

除了这两个硬性规定,根节点通常还包含一些描述系统身份的信息,就像产品的铭牌。#address-cells#size-cells 是必填的,它们规定了子节点 reg 属性中地址和长度各占用几个 cell。model 是这块板子的具体型号,推荐格式 manufacturer,model-numbercompatible 是兼容的平台列表用于 OS 选取特定代码,推荐格式 manufacturer,modelserial-number 是设备的序列号(可选)。chassis-type 是系统的物理形态,比如 desktoplaptopserverembedded 等(推荐选填)。

这里有个细节值得玩味:compatible 属性看起来是个字符串列表,但内核在读它的时候是从前往后匹配的。最精确的型号写在最前面(比如 fsl,mpc8572ds),比较通用的家族兼容性写在后面。如果内核没有这块板子的专门驱动,至少可以尝试跑通这个家族的通用代码——这是一种优雅的降级策略


3.3 /aliases 节点:给设备起个外号

随着设备树越来越复杂,某个设备的路径可能会变得非常长,比如 /simple-bus@fe000000/serial@llc500。每次写代码都要引用这一长串路径不仅容易敲错,而且简直是在考验耐心。这时候,/aliases 节点就派上用场了。

它位于根节点下,专门用来定义「别名」。你可以把它理解为系统级的「快捷方式」或「软链接」。每个属性定义一个别名,属性名就是别名(比如 serial0),属性值是目标设备的全路径字符串。当你写代码或配置内核参数时只需要写 serial0,客户端程序(比如内核)会自动去 /aliases 里查找把它翻译成真实的路径。

虽然起外号很自由,但设备树规范对别名的字符有严格限制:只能用小写字母、数字和连字符(-),长度在 1 到 31 个字符之间。有了这个定义,内核的命令行参数里就可以直接写 console=serial0,而不需要写那串长得要命的路径了。

c
aliases {
    serial0 = "/simple-bus@fe000000/serial@llc500";
    ethernet0 = "/simple-bus@fe000000/ethernet@31c000";
};

3.4 /memory 节点:内存从哪来?

这是最关键的一个节点。没有它,内核连初始化都做不到。 所有的设备树必须至少包含一个 /memory 节点,用来描述系统的物理内存布局。

节点名字通常叫 memory,或者带上地址叫 memory@<address>。它最核心的属性是 reg,这个属性里存放了一组「地址-长度」对,告诉内核「从 A 地址开始,有 B 大小的内存可用」。device_type 必须是 "memory",这是内核识别内存节点的标志。reg 是物理内存的地址和大小数组。initial-mapped-area 是初始映射区域(三元组:有效地址、物理地址、大小),可选。hotpluggable 是空值属性,提示 OS 这块内存可能会被热插拔移除,可选。

千万别把 device_type 写错。虽然现在的 Linux 内核对于某些节点可能不再严格依赖 device_type,但对于内存节点,很多旧版本的内核或 bootloader 依然是通过查找 device_type = "memory" 来定位内存的。写错或漏写,内核可能会报 Memory: not available 然后 Kernel panic - not syncing: No usable memory found

假设你有一个 64 位的 Power 系统,物理内存布局有点怪:第 1 段从 0x0 开始 2GB 大小,中间空了一段(可能是给 PCIe 预留的 IO 空间),第 2 段从 0x1_0000_0000(4GB 处)开始 4GB 大小。假设根节点定义了 #address-cells = <2>#size-cells = <2>(意味着地址和长度各用 2 个 32 位 cell 表示,即 64 位)。

你可以用一个 memory 节点在 reg 里列两段,或者用两个节点分开写。两种写法都是合法的,内核在处理时会把所有 device_type = "memory" 的节点找出来,把它们的 reg 拼成一张完整的内存地图。

如果你的系统是通过 UEFI 启动的,情况就有点不一样了。UEFI 自己维护了一份内存地图(通过 GetMemoryMap() 服务)。在 UEFI 启动环境下,OS 必须忽略设备树里的 /memory 节点,完全听 UEFI 的——这是为了避免固件告诉你的地址和设备树里写的不一致导致冲突。


3.5 /reserved-memory 节点:禁区划分

内存不是所有的都能随便给内核拿去分给进程用。有些内存区域是给特定硬件留的,比如显卡的显存、多媒体处理器的编解码缓冲区、某些安全世界用的 TEE 内存。如果内核把这些内存分配给普通进程用了,显卡画面花掉是小事,导致系统崩溃甚至数据泄露是大事。

/reserved-memory 节点就是用来在设备树里画出这些「禁区」的。它是一个容器节点,子节点每一个代表一块保留内存。这里有个重要的技术细节:ranges 必须为空,这表示保留内存的地址空间直接继承自根节点,不需要进行地址转换。

子节点可以通过两种方式指定内存块:静态指定reg 属性直接写死物理地址,或者动态申请只指定大小和对齐要求让启动时的分配器去挑一块空闲内存给你。比如 CMA 池可以只指定大小和对齐要求,让分配器自己挑。

有几个控制行为的属性非常有讲究。no-map 告诉 OS:绝对不要为这块区域建立虚拟映射,也不要进行任何推测性访问。这通常用于安全设备或 DSP 的内存,防止 CPU 意外触碰导致硬件故障。reusable 允许 OS 在设备驱动不需要这块内存的时候,临时把它用作可回收的普通内存(比如存缓存数据),一旦驱动需要 OS 必须把数据踢走并把内存归还。Linux 的 CMA(Contiguous Memory Allocator)就是基于这个机制。

光在 /reserved-memory 里定义还不够,你得告诉对应的设备「这块内存是给你的」。这通过在设备节点里添加 memory-region 属性来实现,值是一个指向保留内存子节点的 phandle。如果是多块内存,还可以加个 memory-region-names 来起名字,方便驱动里按名字查找。

no-mapreusable 是互斥的!你不能在同一个节点里既说它是可回收的,又说它是不能映射的,这逻辑上完全矛盾。


3.6 /chosen 节点:运行时的参数表

/chosen 节点不代表任何硬件设备,它更像是一个公告板,用来存放系统固件在启动时选定的参数。它最核心的使命是告诉内核:「控制台在哪」以及「启动参数是什么」。

bootargs 是传递给内核的命令行参数,比如 root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200stdout-path 指定作为标准输出设备的路径,可以是全路径也可以是别名。如果中间有 :,则 : 后面的部分通常指代具体的配置参数(如波特率)。stdin-path 指定标准输入设备路径,如果不填默认使用 stdout-path

有些老设备树里可能会用 linux,stdout-path。虽然现在标准已经废弃它改用 stdout-path,但为了兼容老内核或老 bootloader,你的代码最好能识别这两种写法。

c
chosen {
    bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
    stdout-path = "serial0:115200n8";
};

3.7 /cpus 节点:CPU 的容器

所有的设备树必须包含一个 /cpus 节点。它本身不描述某个具体的 CPU,而是作为一个容器用来容纳描述各个 CPU 的子节点。在这个容器上,我们主要定义两个规则:#address-cells 规定子节点 reg 属性中地址占用几个 cell,#size-cells 必须为 0——因为 CPU 根本不需要「大小」这个概念。


3.8 /cpus/cpu* 节点:描述核心

每个 CPU 子节点(通常叫 cpu@0, cpu@1...)代表一个足够独立的硬件执行单元。如果一个硬件线程可以独立运行操作系统,或者它是中断控制器里独立的一个 target,那它通常就需要一个独立的节点。如果是两个硬件线程共享一个 MMU(比如某些超线程技术),通常只算一个 CPU 节点。

device_type 必须是 "cpu"reg 是 CPU 的 ID(逻辑编号),对应中断控制器的编号。clock-frequencytimebase-frequency 是时钟频率,可选。status"okay", "disabled", "fail",用于 SMP 控制。

在 ARM 或 PowerPC 的多核系统中,主核(Boot CPU)是一直运行的,但从核通常在复位状态或者在一个死循环里等着。要让从核启动,需要一种特定的机制。enable-method 属性就是用来告诉内核:「怎么把那个处于 disabled 状态的 CPU 唤醒」。如果值是 "spin-table",意味着内核需要往一个特定的物理地址(由 cpu-release-addr 指定)写入跳转指令,把 CPU 从死循环里「踢」出来。如果是厂商特定的值(如 "fsl,MPC8572DS"),则对应厂商的特定启动流程。

CPU 节点里还可以极其详细地描述 L1 Cache 和 TLB 的结构。比如 cache-unified 表示指令和数据缓存是统一的,d-cache-size / i-cache-size 是数据或指令缓存大小,tlb-split 表示 TLB 是否分离。这些属性对于编写极度底层的汇编代码或进行性能调优至关重要,但对于一般的驱动开发者来说了解即可。

如果所有的 CPU 核心都有一样的 L1 Cache 大小或者一样的时钟频率,难道要每个节点都抄一遍?不需要。你可以把这些公共属性写在父节点 /cpus 里,内核在查找属性时如果当前 CPU 节点没有就会自动去父节点找。


3.9 多级与共享缓存节点 (l?-cache)

现在的处理器缓存层级越来越复杂。CPU0 有 L2,CPU1 也有 L2,它们可能共享一个 L3。如果每个 CPU 节点里都塞一堆属性描述 L2、L3,那不仅臃肿,而且无法表达「共享」的关系。

为此,设备树允许把缓存本身描述成一个独立的节点(compatible = "cache"),然后通过指针(phandle)连接起来。compatible 必须包含 "cache"cache-level 是缓存级别(2 代表 L2,3 代表 L3)。cache-unified 表示是否统一缓存,可选。

CPU 节点或上一级缓存节点,通过 next-level-cache 属性指向下一级。这种结构非常清晰地展示了硬件拓扑:内核可以很容易地看出哪些缓存是私有的,哪些是共享的,从而在调度任务时做出更智能的决策(比如尽量把通信频繁的两个线程放在共享 L2 的两个核心上)。


本章回响

这一章我们搭建了设备树的骨架。还记得开头那个「蒙着眼睛的巨人」吗?现在我们给了他一张地图的边框:根节点界定了坐标系,/memory 告诉了他思维的空间,/cpus 告诉了他有多少个脑袋在思考,而 /chosen 则像是耳边的一句低语,告诉他控制台在哪、该怎么启动。

这些节点看似枯燥,全是表格和数组,但它们是 Linux 内核启动的前置条件。缺了任何一个,或者填错了一个 cell 的数量,那个巨人就会立刻倒下,甚至连句遗言都留不下。下一章,我们将开始给这张骨架填充血肉——也就是总线、串口、网卡这些真正的「设备」。届时你会发现,今天建立的关于「寻址」和「引用」的直觉,会以一种意想不到的方式频繁出现。

Built with VitePress