Linux内核6.6 内存管理 (16)early_init_dt_scan_memory详解
https://elixir.bootlin.com/linux/v6.11/source/drivers/of/fdt.c#L964
1.扫描内存/非内存节点
详解这一段函数的内容,这一段代码主要分为两部分
首先是打印了设备树所有的顶层子节点,但是只会停留在memory,这里打印了memory节点的值。
问题1:为什么160字节?
这里表示用2个cell表示地址,2个cell表示大小,每个cell固定是4个字节。4*4=16
下面又打印了memory range, 一共10个,16*10就是160个字节
bootloader中填充了这些内存范围的内容
2.内存分块
内存分块后解析出了具体的内容
地址不连续(剩余部分可能为硬件准备)/内存大小具有差异
可以使用 cat /proc/iomem 看到硬件的地址空间布局
举个小例子:
[ 0.000000] OF: fdt: LX Parsed memory range: Base=0x80000000, Size=0x5f20000 (99745792 bytes)
和
80000000-805fffff : reserved
80600000-85cfffff : System RAM
85d00000-85efffff : reserved
85f40000-85f7ffff : reserved
这里又把内存块区分成了,reserved和system ram, 加粗部分为拆开分布。最后一小部分是下块内存的预留部分
3.CPU是如何访问外设的
核心逻辑:硬件设计师把 UFS 控制器的寄存器分配到物理地址 01d84000-01d86fff
(MMIO 物理地址)→ 内核用 ioremap
把这个物理地址映射成内核虚拟地址(比如 0xffff000012340000
)→ 内核用 ioread/iowrite
读写这个虚拟地址→ MMU 把虚拟地址翻译成物理地址→ 地址译码器把物理地址转发到 UFS 控制器→ 控制器执行对应的硬件动作(返回状态 / 传输数据)。
1) 将物理地址映射为虚拟地址
https://elixir.bootlin.com/linux/v6.11/source/drivers/ufs/host/ufshcd-pltfrm.c#L471
访问寄存器:映射完成后,就可以通过指针base
来访问外设的寄存器。比如要读取某个寄存器的值,可以使用readl(base + offset)
函数(假设是 32 位读取,offset
是寄存器相对于基地址base
的偏移量);要写入值,可以使用writel(value, base + offset)
。 以是否调用函数ufs_qcom_device_reset为例
liuxin@liuxin-gv:/local/mnt/workspace/92_talos_upstream/linux-sheepdog/linux-next$ git diff drivers/ufs/host/ufs-qcom.c diff --git a/drivers/ufs/host/ufs-qcom.c b/drivers/ufs/host/ufs-qcom.c index a5a0646bb80a..4705ffc72e17 100644 --- a/drivers/ufs/host/ufs-qcom.c +++ b/drivers/ufs/host/ufs-qcom.c @@ -1549,12 +1549,27 @@ static int ufs_qcom_device_reset(struct ufs_hba *hba) * The UFS device shall detect reset pulses of 1us, sleep for 10us to * be on the safe side. */ + dev_info(hba->dev, "ufs_qcom_device_reset entry\n");//确定函数被调用 + int reg_val; + void __iomem *vir_addr; + + vir_addr = ioremap(0x359F004,4); //这时候通过ioremap转换实际需要的物理地址已经需要获取的大小,转换成内核可以访问的地址 + + reg_val = readl(vir_addr); + dev_info(hba->dev, "0x%x\n", reg_val); + ufs_qcom_device_reset_ctrl(hba, true); usleep_range(10, 15); + reg_val = readl(vir_addr); + dev_info(hba->dev, "0x%x\n", reg_val); + ufs_qcom_device_reset_ctrl(hba, false); usleep_range(10, 15); + reg_val = readl(vir_addr); + dev_info(hba->dev, "0x%x\n", reg_val); + return 0; } 最后输出:验证成功 [ 5.121632] ufshcd-qcom 1d84000.ufshc: 0x0 [ 5.144866] ufshcd-qcom 1d84000.ufshc: 0x0 [ 5.163031] ufshcd-qcom 1d84000.ufshc: 0x1
要理解「内存映射(Memory-Mapped I/O,简称 MMIO)」,核心是先打破一个误区:CPU 不能直接访问外设硬件,只能访问 “地址”——MMIO 的本质就是把「外设的寄存器 / 缓冲区」“伪装” 成一段普通的物理内存地址,让 CPU 像读写内存一样,完成对外设的控制和数据交互。
2) 问题1
CPU为什么不能直接访问外设?
CPU核心功能就是执行命令和读写地址,只有一套内存地址总线。CPU只能通过这套流程,无法直接识别 “这是内存” 还是 “这是 UFS 控制器”。
而外设(如 UFS 控制器)的硬件逻辑是由「寄存器」控制的(比如 “数据寄存器” 存要传输的数据,“状态寄存器” 表示设备是否就绪)。这些寄存器本质是 “硬件电路的开关”,需要 CPU 来 “拨动”—— 但 CPU 只能通过地址访问,所以硬件设计师想出了一个办法:把外设的寄存器,分配一段专属的「物理地址空间」(比如 01d84000-01d86fff),这段地址不对应实际的内存芯片,而是对应外设的寄存器电路。当 CPU 读写这段地址时,硬件会自动把 “地址信号” 转发到外设,而不是内存 —— 这就是 MMIO 的核心思路。
3)问题2
MMIO 的硬件原理:地址怎么 “转发” 到外设?
MMIO 能工作,靠的是硬件层面的「地址译码器」和「总线仲裁」,这是理解 MMIO 的关键:
- 地址分配:硬件设计时,会给每个外设分配一段不重叠的「物理地址范围」(比如 UFS 控制器 01d84000-01d86fff,串口 00880000-00883fff),这些地址范围会避开普通内存的地址(比如内存从 80000000 开始),避免冲突。
- 地址译码:CPU 发送一个物理地址(比如 01d84004)时,地址总线会把这个地址传给「地址译码器」。译码器会判断:如果地址属于「内存地址范围」(如 80000000 以上)→ 通知内存芯片,CPU 要读写内存;如果地址属于「MMIO 地址范围」(如 01d84004)→ 通知对应的外设(UFS 控制器),CPU 要读写它的寄存器。
- 数据交互:当外设收到 “地址命中” 的信号后,会根据 CPU 的 “读写信号”(读:CPU 要获取数据;写:CPU 要发送指令),操作自己的寄存器:读操作:外设把寄存器里的数据(比如 UFS 设备的状态)放到数据总线,CPU 从总线读取;写操作:CPU 把数据(比如 “开始传输” 的指令)放到数据总线,外设从总线读取并写入寄存器,触发硬件动作。
举个具体例子:CPU 要读 UFS 控制器的 “状态寄存器”(假设偏移 0x04
,物理地址 01d84004
):
- CPU 执行指令:
read 0x01d84004
; - 地址总线发送
01d84004
到译码器; - 译码器识别到这是 UFS 控制器的地址,通知 UFS 控制器 “准备数据”;
- UFS 控制器把 “状态寄存器” 的值(比如
0x01
表示就绪)放到数据总线; - CPU 从数据总线读取
0x01
,知道 UFS 设备已就绪。
4)问题3
内核中为什么要 “二次映射”?(物理地址 → 虚拟地址)
MMIO 的物理地址(如 01d84000)属于「未被 MMU 管理的地址」,内核要访问它,必须先通过 ioremap 函数做两件事:
1.告诉 MMU:“把物理地址 01d84000-01d86fff 映射到内核的虚拟地址空间(比如 0xffff000012340000)”;
2.禁用该虚拟地址的 “缓存”:因为外设寄存器的值是实时变化的(比如状态寄存器会随硬件状态更新),如果开启缓存,CPU 可能读的是缓存里的旧值,而不是实际硬件的状态 ——ioremap 会自动标记这段虚拟地址为 “非缓存”。
ioremap
是内核通过配置 MMU 实现 “物理地址→内核虚拟地址” 映射的关键函数