探索Linux Kernel:早期I/O内存映射的奥秘
大家都知道,计算机的硬件设备在与系统进行交互时,需要通过 I/O 接口来实现数据的传输和控制。而内存映射则是将硬件设备的物理地址映射到系统的内存空间,以便操作系统能够直接访问这些设备。早期 I/O 内存映射在这个过程中扮演着重要的角色,它为内核提供了一种高效的方式来管理和访问硬件设备。
想象一下,当我们启动计算机时,系统需要快速地识别和配置各种硬件设备。早期 I/O 内存映射就像是一座桥梁,连接着硬件设备和系统内存。通过它,内核能够快速地将硬件设备的物理地址映射到内存空间,从而实现对设备的访问和控制。这不仅提高了系统的运行效率,还为后续的各种操作奠定了基础。那么,早期 I/O 内存映射究竟是如何实现的呢?它又有哪些特点和优势呢?接下来,就让我们一起深入探讨这个话题,揭开早期 I/O 内存映射的神秘面纱。
一、早期I/O内存映射概述
在计算机的世界里,Linux 系统就像一个庞大而精密的工厂,各个组件协同运作,而内存管理则是这个工厂至关重要的 “物流调度中心”。它掌控着内存资源的分配、回收与调度,确保系统的每一个环节都能顺畅运行。当我们聚焦于 Linux Kernel 内存管理的一个关键环节 —— 早期 I/O 内存映射(early ioremap),就如同揭开了这个工厂中一个神秘而高效的 “小车间” 的面纱。它在系统启动的初期,为硬件设备与内核之间搭建起一座快速通信的桥梁,使得设备能够迅速被识别、配置并投入使用,为整个系统的稳定运行奠定基石。
任何系统都免不了要有输入/输出,所以对 I/O 设备的访问是 CPU 的一个重要功能。一般来说,对 I/O 设备的访问有两种不同的形式:
-
通过端口映射(Port-mapped I/O,PMIO);
-
通过内存映射(Memory-Mapped I/O,MMIO);
在采用内存映射的方式时, I/O 设备的存储单元,如控制寄存器、数据寄存器、状态寄存器等等,是作为内存的一部分出现在系统中的。CPU 可以像访问内存单元一样访问外部设备的存储单元,所以不需要专门用于外设的 I/O 指令。而采用端口映射的系统则不同,外部设备的存储单元所在的地址空间与内存分属于两个不同的体系。访问内存的指令不能用来访问外部设备的存储单元,所以在 x86 架构的 CPU 中设立了专门的 IN 和 OUT 指令。
两种映射方式在地址空间上的比较:
二、基础铺垫:理解内存管理与I/O操作
2.1内存管理的基石:虚拟地址与物理地址
在计算机的内存管理体系里,虚拟地址和物理地址宛如一对 “双胞胎”,看似相似却有着截然不同的分工。物理地址,顾名思义,是实实在在的内存芯片级别的寻址地址,它对应着内存单元的真实 “物理位置”,就好比城市中每一栋房子都有一个固定的、独一无二的门牌号,是硬件层面识别内存单元的标识。而虚拟地址,则像是给程序开发者和操作系统准备的一份 “地图”,程序在运行时操作的都是虚拟地址,它为每个进程提供了一个独立、连续且看起来非常规整的地址空间,让进程仿佛 “独占” 了一大片内存,而无需关心物理内存的实际分配情况。
以一个常见的生活场景来比喻,物理地址就像是图书馆书架上每一本书实际摆放的位置编号,是真实的、固定不变的存储位置;而虚拟地址则像是图书馆的检索系统给每一本书分配的索书号,读者通过索书号(虚拟地址)向图书馆管理员(操作系统)查询,管理员再依据索书号找到对应的实际书架位置(物理地址)。
在 Linux 系统中,这种从虚拟地址到物理地址的转换,通常由内存管理单元(MMU)来负责完成。当进程访问某个虚拟地址时,MMU 会依据预先设置好的页表,快速查找到对应的物理地址,就如同图书馆管理员依据索书号在馆藏目录(页表)中找到对应的书架位置一样,实现高效、准确的地址转换,确保进程顺利读写内存数据。
2.2I/O 操作的关键:外设寄存器访问
在计算机系统里,CPU 与外设进行交互沟通的关键桥梁便是外设寄存器。几乎每一种外设,无论是显卡、硬盘控制器,还是键盘、鼠标等,都是通过读写设备上的寄存器来实现控制与数据传输,这些寄存器通常涵盖控制寄存器、状态寄存器和数据寄存器三大类。它们如同外设的 “大脑”,掌控着外设的工作模式、状态反馈以及数据的流入流出。
根据 CPU 体系结构的不同,CPU 对这些外设寄存器所对应的 I/O 端口的编址方式存在两种:
一种是 I/O 映射方式(I/O-mapped),典型的如 X86 处理器,专门为外设开辟了一个独立的地址空间,即 “I/O 地址空间” 或者 “I/O 端口空间”,CPU 需要借助专门的 I/O 指令(如 X86 的 IN 和 OUT 指令)来访问这一特殊空间中的地址单元,就好像你要进入一个特殊的 “贵宾室”,必须使用专门的 “贵宾卡”(专用指令)才能通行;
另一种是内存映射方式(Memory-mapped),像 ARM、PowerPC 等 RISC 指令系统的 CPU 通常只采用单一的物理地址空间,外设 I/O 端口被巧妙地融入内存空间,成为其一部分,此时 CPU 可以如同访问普通内存单元那样访问外设 I/O 端口,无需额外的特殊指令,这种方式让访问外设变得更加便捷统一。
在 Linux 内核中,针对这两种编址方式,提供了相应的函数来实现高效访问。对于采用内存映射方式的外设寄存器,当系统运行时,虽然外设的 I/O 内存资源的物理地址已知(由硬件设计决定),但 CPU 并没有为其预定义虚拟地址范围,这就需要借助 ioremap () 函数来大展身手,它能够将 I/O 内存资源的物理地址精准映射到核心虚地址空间(通常是 3GB-4GB)之中,为后续的访问搭建好通道;而与之对应的 iounmap 函数,则用于在不需要映射时,干净利落地取消 ioremap () 所做的映射操作,释放资源。这些函数就像是精准的导航仪,确保内核能够在复杂的内存与 I/O 资源迷宫中,准确找到并操控外设,实现系统的稳定运行与高效交互。
三、主角登场:early ioremap详解
3.1诞生背景:内核启动初期的困境
在内核启动的最初阶段,就如同一个刚刚奠基的建筑工地,各项基础设施尚不完善。此时,内存管理系统还处于 “未成熟” 状态,大部分物理内存都还没有建立起完整的页表映射,就像建筑工地的材料仓库还没有整理好,货物杂乱无章,难以快速找到所需物品。这就导致了一些常规的内存操作函数,如 ioremap、kmalloc 等,如同没有导航的船只在茫茫大海中迷失方向,无法正常发挥作用。
然而,系统中的一些硬件设备却像是着急开工的工人,它们迫切需要与内核进行通信,以完成初始化、配置等关键任务,不能等到内存管理系统完全成熟。在这种 “万事俱备,只欠东风” 的紧迫情况下,early ioremap 应运而生,它就像是建筑工地临时搭建的简易运输通道,虽然不够完美,但能够在关键时刻快速搬运少量 “材料”,为早期的硬件设备与内核之间搭起一座通信的桥梁,确保系统启动过程能够顺利推进。
3.2fixmap:early ioremap 的基石
⑴概念剖析:固定虚拟地址区域的奥秘
fixmap,从字面意思理解,就是固定映射的区域。它在内核的虚拟地址空间中,占据着一段特殊的、编译时就已确定的领地。就好比城市规划中,有一片特定的区域在城市建设之初就被划定用途,不会轻易更改。在 ARM 架构下(以常见的 ARM 体系为例),fixmap 的虚拟地址范围通常是固定的,如在某些配置下可能是 0xffc00000 - 0xfff00000 这一段。这些地址在编译阶段就被 “锁定”,等待在系统启动过程中,根据实际需求将相应的物理地址与之建立映射关系,就像是给这片固定区域的每一个 “门牌号” 预先分配好,后续根据实际入住的 “住户”(物理地址)进行精准匹配。
⑵分段功能:各司其职的内存区域
fixmap 并不是一个单一功能的区域,它像是一个多功能的园区,被精细地划分为多个小段,每个小段都肩负着独特的使命。
FDT 段:用于设备树(Device Tree)信息的获取。设备树就像是整个硬件系统的 “族谱”,详细描述了硬件设备的层级关系、资源配置等信息。内核需要读取设备树来了解系统中有哪些硬件设备、它们的连接方式以及所需的资源等,而 FDT 段就是内核访问设备树物理地址的 “入口”,确保内核能精准找到并解读这份 “族谱”。
console 段:主要服务于早期调试工作。在系统启动初期,当出现问题时,开发人员需要通过串口等控制台输出调试信息,就像医生通过听诊器了解病人身体状况一样。console 段为串口相关的 IO 寄存器提供了映射空间,使得内核可以顺利地向控制台输出 log 信息,帮助开发人员快速定位问题,让系统启动过程中的 “疑难杂症” 无处遁形。
BITMAP 段:这片区域是为 early ioremap 量身定制的。它为早期 I/O 内存映射提供了临时的 “栖息之所”,就像为紧急物资准备的临时仓库,当硬件设备急需与内核通信,而常规内存映射机制尚未就绪时,BITMAP 段能迅速响应,为设备寄存器分配虚拟地址,满足设备的紧急需求。
⑶初始化流程:构建映射框架
early_fixmap_init 函数是 fixmap 初始化的 “总指挥”。在系统启动进入内核初始化的早期阶段,这个函数就开始大展身手。它主要完成两项关键任务:一是构建起基本的页表框架,为后续的地址映射奠定基础。这就好比搭建房屋的主体结构,先把大梁、柱子立起来;二是将 fixmap 区域与物理地址之间的映射关系初步建立起来,虽然此时还没有具体到每一个设备寄存器的精确映射,但已经铺好了 “轨道”,后续具体的映射操作只需沿着这条 “轨道” 进行即可,确保了整个映射过程的有序性与高效性。
3.3early ioremap 的初始化与使用
⑴初始化步骤:开启早期 I/O 内存映射之门
early ioremap 的初始化由 early_ioremap_init 函数牵头,它就像一场音乐会的指挥,协调着各个环节。在这个函数内部,会调用 early_ioremap_setup 函数,二者紧密配合,完成一系列精细的操作。首先,它们会对相关的数组、变量进行初始化,比如 prev_map 数组用于记录前期的映射信息,避免重复映射,就像出行前检查车辆的行程记录,防止走冤枉路;slot_virt 数组则保存着固定映射区域的虚拟地址,这是后续映射操作的关键索引,如同地图上的坐标点。通过这些初始化操作,为 early ioremap 的正式运行搭建好了 “舞台”,确保在系统启动初期,硬件设备能够及时、准确地与内核进行沟通。
I/O 内存映射将设备的寄存器和内存映射到主存地址空间。内核提供了 ioremap 函数执行此类操作,换句话说,ioremap 将 I/O 物理内存区域映射到内核虚拟内存空间以使内核可以访问它们。
但是,ioremap 函数需要 vmalloc 功能支持;在内核启动早期,vmalloc 功能尚未完成初始化,此时无法使用 ioremap 函数。为了能够在内核启动早期就可以通过 I/O 内存映射来访问 I/O设备,内核提供了 early_ioremap 函数来实现该功能。另外,在使用 early_ioremap函数之前,需要对其使用的内存区域进行初始化。
①early_ioremap_init
在 setup_arch 函数中,通过调用 early_ioremap_init 函数,来进行早期 ioremap 的初始化。
// file: arch/x86/kernel/setup.c
/*
* setup_arch - architecture-specific boot-time initializations
*
* Note: On x86_64, fixmaps are ready for use even before this is called.
*/
void __init setup_arch(char **cmdline_p)
{
...
early_ioremap_init();
...
}
early_ioremap_init 函数定义如下:
// file: arch/x86/mm/ioremap.c
void __init early_ioremap_init(void)
{
pmd_t *pmd;
int i;
if (early_ioremap_debug)
printk(KERN_INFO "early_ioremap_init()\n");
for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
pmd = early_ioremap_pmd(fix_to_virt(FIX_BTMAP_BEGIN));
memset(bm_pte, 0, sizeof(bm_pte));
pmd_populate_kernel(&init_mm, pmd, bm_pte);
/*
* The boot-ioremap range spans multiple pmds, for which
* we are not prepared:
*/
#define __FIXADDR_TOP (-PAGE_SIZE)
BUILD_BUG_ON((__fix_to_virt(FIX_BTMAP_BEGIN) >> PMD_SHIFT)
!= (__fix_to_virt(FIX_BTMAP_END) >> PMD_SHIFT));
#undef __FIXADDR_TOP
if (pmd != early_ioremap_pmd(fix_to_virt(FIX_BTMAP_END))) {
WARN_ON(1);
printk(KERN_WARNING "pmd %p != %p\n",
pmd, early_ioremap_pmd(fix_to_virt(FIX_BTMAP_END)));
printk(KERN_WARNING "fix_to_virt(FIX_BTMAP_BEGIN): %08lx\n",
fix_to_virt(FIX_BTMAP_BEGIN));
printk(KERN_WARNING "fix_to_virt(FIX_BTMAP_END): %08lx\n",
fix_to_virt(FIX_BTMAP_END));
printk(KERN_WARNING "FIX_BTMAP_END: %d\n", FIX_BTMAP_END);
printk(KERN_WARNING "FIX_BTMAP_BEGIN: %d\n",
FIX_BTMAP_BEGIN);
}
}
函数内部,首先声明了 pmd_t 类型的指针变量 pmd 以及 int 类型的变量 i。
pmd_t *pmd;
int i;
pmd_t 表示中级页目录项,是一种结构体类型,其定义如下:
// file: arch/x86/include/asm/pgtable_types.h
typedef struct { pmdval_t pmd; } pmd_t;
其结构体成员 pmd 为 pmdval_t 类型,pmdval_t 是 unsigned long 的别名。
// file: arch/x86/include/asm/pgtable_64_types.h
typedef unsigned long pmdval_t;
接下来,检查变量 early_ioremap_debug
,决定是否要打印调试信息。
if (early_ioremap_debug)
printk(KERN_INFO "early_ioremap_init()\n");
early_ioremap_debug是一个静态变量,由于未进行显式初始化,其默认值为 0。
// file: arch/x86/mm/ioremap.c
static int __initdata early_ioremap_debug;
通过命令行参数 early_ioremap_debug,可以修改上述变量的值。
// file: arch/x86/mm/ioremap.c
static int __init early_ioremap_debug_setup(char *str)
{
early_ioremap_debug = 1;
return 0;
}
early_param("early_ioremap_debug", early_ioremap_debug_setup);
可以看到,当设置了 early_ioremap_debug 参数后,参数处理函数 early_ioremap_debug_setup 会将变量的值设置为 1。
我们在前文提到过,在进行早期 ioremap 时,内核的内存管理子系统还没有准备好,没办法通过 vmalloc 为 I/O 映射分配虚拟内存。所以,在系统启动阶段,内核在固定映射区(Fixmap)内,为早期的 ioremap 分配了一段内存空间(从 FIX_BTMAP_BEGIN 到 FIX_BTMAP_END),即 Fixmap 中的临时映射区。
接下来,对临时映射区进行初始化。
for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
临时映射区分为 FIX_BTMAPS_SLOTS(扩展为 4) 个槽,每个槽 NR_FIX_BTMAPS (扩展为 64) 个元素,所以临时映射区总共可以容纳 TOTAL_FIX_BTMAPS (256) 个元素。每个元素对应着 Fixmap 区域中的一个页(4K),所以总大小为 256个页,即 1MB。
// file: arch/x86/include/asm/fixmap.h
/*
* 256 temporary boot-time mappings, used by early_ioremap(),
* before ioremap() is functional.
*
* If necessary we round it up to the next 256 pages boundary so
* that we can have a single pgd entry and a single pte table:
*/
#define NR_FIX_BTMAPS 64
#define FIX_BTMAPS_SLOTS 4
#define TOTAL_FIX_BTMAPS (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
内核使用 slot_virt数组来表示临时映射区的空间,该数组有 FIX_BTMAPS_SLOTS(扩展为 4) 个元素,对应着临时映射区的 4 个槽。
static unsigned long slot_virt[FIX_BTMAPS_SLOTS] __initdata;
FIX_BTMAP_BEGIN 和 FIX_BTMAP_END 分别是临时映射区的起始索引和结束索引,__fix_to_virt用于将 Fixmap 中的索引转换为对应的虚拟地址。
FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i 获取到每个槽的起始索引,然后通过宏 __fix_to_virt将索引值转换成虚拟地址。for 循环执行完成后,slot_virt里保存的是每个槽区的起始地址。此时,临时映射区示意图如下:
接下来,通过 early_ioremap_pmd 函数计算临时映射区起始地址对应的中层页目录项地址。
pmd = early_ioremap_pmd(fix_to_virt(FIX_BTMAP_BEGIN));
上文说过,FIX_BTMAP_BEGIN是临时映射区起始索引;fix_to_virt 函数把索引值转换为虚拟地址,所以,fix_to_virt(FIX_BTMAP_BEGIN)就得到了临时映射区的起始虚拟地址。
early_ioremap_pmd 函数会计算指定虚拟地址所对应的中层页目录项地址,具体实现见 early_ioremap_pmd 节。再接着,通过 memset 函数将将变量 bm_pte 初始化为 0。
memset(bm_pte, 0, sizeof(bm_pte));
变量bm_pte 定义如下:
static pte_t bm_pte[PAGE_SIZE/sizeof(pte_t)] __page_aligned_bss;
可以看到,bm_pte是 pte_t 类型的数组,PAGE_SIZE/sizeof(pte_t) 表示每页能容纳的 pte_t类型数据的数量。我们知道,每个页表大小为 4KB,也就是一个 PAGE_SIZE 大小,而pte_t 代表的是页表项,所以 bm_pte 实际是一张页表。将 bm_pte 初始化为 0,意味着该页表中的每个页表项均为 0,都是无效页表项。
我们再来分析下宏 __page_aligned_bss 的作用,该宏定义如下:
// file: include/linux/linkage.h
#define __page_aligned_bss __section(.bss..page_aligned) __aligned(PAGE_SIZE)
__page_aligned_bss 内部又引用了宏 __section 和 __aligned ,他们分别定义如下:
// file: include/linux/compiler.h
# define __section(S) __attribute__ ((__section__(#S)))
// file: include/linux/compiler-gcc.h
#define __aligned(x) __attribute__((aligned(x)))
综上所述,宏 __page_aligned_bss 指示编译器将数组 bm_pte放入 .bss..page_aligned 节中,并对齐到页大小。再下来,执行
pmd_populate_kernel(&init_mm, pmd, bm_pte);
现在我们已经得到了中层页目录项地址 pmd,页表基地址 bm_pte ,通过pmd_populate_kernel 函数,将 bm_pte的物理地址及页属性组合成表项数据,并写入 中层页目录项 pmd 中。
此行代码执行后,分页结构及变量pmd与bm_pte关系如下:
由于在(早期)页表初始化时,已经为 Fixmap 区域建立了各级页表,此步执行完后,各级页表结构如下图所示:
再来看下面一段代码:
/*
* The boot-ioremap range spans multiple pmds, for which
* we are not prepared:
*/
#define __FIXADDR_TOP (-PAGE_SIZE)
BUILD_BUG_ON((__fix_to_virt(FIX_BTMAP_BEGIN) >> PMD_SHIFT)
!= (__fix_to_virt(FIX_BTMAP_END) >> PMD_SHIFT));
#undef __FIXADDR_TOP
其中,宏 __FIXADDR_TOP 在 32 位系统才会用到,可以忽略。
接下来检查临时映射区是否在同一个中层页目录项里。检查方法就是将临时映射区的起始虚拟地址和结束地址,全部右移 PMD_SHIFT(扩展为 21)位,然后比较移位后的值是否相等。如果相等,则属于同一个中层页目录项,条件为假;否则,跨越了多个中层页目录项,条件为真。
宏 BUILD_BUG_ON 会在编译时检查给定条件是否为真,如果条件为真,会在打印错误信息后将进程挂起。最后一段代码如下:
if (pmd != early_ioremap_pmd(fix_to_virt(FIX_BTMAP_END))) {
WARN_ON(1);
printk(KERN_WARNING "pmd %p != %p\n",
pmd, early_ioremap_pmd(fix_to_virt(FIX_BTMAP_END)));
...
}
变量pmd 是通过 pmd = early_ioremap_pmd(fix_to_virt(FIX_BTMAP_BEGIN));得到的,是临时映射区起始地址所对应的中层页目录项地址;early_ioremap_pmd(fix_to_virt(FIX_BTMAP_END)得到的是临时映射区结束地址所对应的中层页目录项的地址。如果两者不相等,说明临时映射区跨越了多个中层页目录项,就会打印警告信息。
至此,完成了 ioreamp 的早期初始化工作。
②early_ioremap_pmd
early_ioremap_pmd 函数接收虚拟地址作为参数,返回虚拟地址对应的中层页目录项地址,其定义如下:
// file: arch/x86/mm/ioremap.c
static inline pmd_t * __init early_ioremap_pmd(unsigned long addr)
{
/* Don't assume we're using swapper_pg_dir at this point */
pgd_t *base = __va(read_cr3());
pgd_t *pgd = &base[pgd_index(addr)];
pud_t *pud = pud_offset(pgd, addr);
pmd_t *pmd = pmd_offset(pud, addr);
return pmd;
}
第一行代码,获取到全局页目录的虚拟地址。
pgd_t *base = __va(read_cr3());
我们知道,控制寄存器 CR3 中保存着顶级页表,即全局页目录(Page Global Directory,PGD)的物理地址。顾名思义,read_cr3()函数读取控制寄存器 CR3 的值。read_cr3 函数内部调用了 native_read_cr3 函数来实现读取功能。
// file: arch/x86/include/asm/special_insns.h
static inline unsigned long read_cr3(void)
{
return native_read_cr3();
}
native_read_cr3 使用内联汇编来读取 CR3 的值。
// file: arch/x86/include/asm/special_insns.h
static inline unsigned long native_read_cr3(void)
{
unsigned long val;
asm volatile("mov %%cr3,%0\n\t" : "=r" (val), "=m" (__force_order));
return val;
}
内联汇编代码就是一个简单的 mov 指令,但是我们看到在输出操作数中,除了需要的寄存器值外,还多了一个内存操作数 __force_order。
// file: arch/x86/include/asm/special_insns.h
/*
* Volatile isn't enough to prevent the compiler from reordering the
* read/write functions for the control registers and messing everything up.
* A memory clobber would solve the problem, but would prevent reordering of
* all loads stores around it, which can hurt performance. Solution is to
* use a variable and mimic reads and writes to it to enforce serialization
*/
static unsigned long __force_order;
之所以要增加一个变量,是为了阻止编译器的重排序。具体可参考变量注释。通过 read_cr3() 函数获取到全局页目录的物理地址后,通过宏 __va 将物理地址转换成虚拟地址,该虚拟地址就是全局页目录的基地址。接下来执行:
pgd_t *pgd = &base[pgd_index(addr)];
pgd_index(addr) 获取到虚拟地址对应的全局页目录项索引;base[pgd_index(addr)]获取到全局页目录项的值;而 &base[pgd_index(addr)] 获取到全局页目录项的虚拟地址,并赋值给指针变量 pgd。
再接着,执行
pud_t *pud = pud_offset(pgd, addr);
pmd_t *pmd = pmd_offset(pud, addr);
return pmd;
pud_offset 和 pmd_offset 函数分别获取到虚拟地址 addr对应的上层页目录项的虚拟地址和中层页目录项的虚拟地址。最后,把中层页目录项地址 pmd 返回。
⑵实际应用:点亮 LED 灯案例解析
以 tiny4412 开发板为例,在点亮 LED 灯的驱动开发中,early ioremap 发挥着不可或缺的作用。通常,LED 灯连接到开发板的特定 GPIO 引脚上,而这些 GPIO 寄存器的物理地址是已知的,但在系统启动早期,常规的内存映射方式无法使用。此时,借助 early ioremap,驱动程序可以轻松地将 GPIO 寄存器的物理地址映射到内核虚拟地址空间。就像在陌生的城市里,通过特殊的导航(early ioremap)找到了隐藏在小巷子里的宝藏(GPIO 寄存器)。具体代码实现如下:
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <asm/io.h>
#define GPIO_BASE_PHYS 0x11000000 // 假设这是GPIO寄存器的物理基地址
#define GPIO_CON_OFFSET 0x02E0
#define GPIO_DAT_OFFSET 0x02E4
static void __iomem *gpio_base_virt;
static int __init led_init(void)
{
// 使用early ioremap进行映射
gpio_base_virt = early_ioremap(GPIO_BASE_PHYS, 0x1000);
if (!gpio_base_virt) {
printk(KERN_ALERT "Failed to map GPIO address\n");
return -ENOMEM;
}
// 设置GPIO为输出模式
writel(0x1111, gpio_base_virt + GPIO_CON_OFFSET);
// 点亮LED(假设低电平点亮)
writel(0x0, gpio_base_virt + GPIO_DAT_OFFSET);
return 0;
}
static void __exit led_exit(void)
{
// 关闭LED
writel(0xF, gpio_base_virt + GPIO_DAT_OFFSET);
// 取消映射
early_iounmap(gpio_base_virt);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
在上述代码中,首先通过 early_ioremap 将 GPIO 寄存器的物理地址范围映射到虚拟地址空间,得到 gpio_base_virt 指针,这就如同拿到了开启 LED 灯控制的 “钥匙”。接着,利用 writel 函数向控制寄存器写入配置值,设置 GPIO 为输出模式,然后再向数据寄存器写入相应的值来点亮 LED 灯。当模块卸载时,使用 early_iounmap 及时释放映射的虚拟地址空间,就像用完工具后将其归位,避免资源浪费,确保系统的内存管理始终井然有序。通过这个简单而典型的案例,我们可以清晰地看到 early ioremap 在实际硬件驱动开发中的关键作用,它为内核早期与硬件设备的交互提供了强有力的支持,使得系统能够顺利地完成初始化任务,逐步走向稳定运行的状态。
四、早期ioremap接口函数
在早期 ioremap 初始化完成后,我们就可以使用它了。内核提供两个函数用于 I/O 物理地址到虚拟地址的映射/取消映射:
-
early_ioremap
-
early_iounmap
4.1 early_ioremap
early_ioremap 函数定义如下:
// file: arch/x86/mm/ioremap.c
/* Remap an IO device */
void __init __iomem *
early_ioremap(resource_size_t phys_addr, unsigned long size)
{
return __early_ioremap(phys_addr, size, PAGE_KERNEL_IO);
}
该函数内部调用了 __early_ioremap 函数,__early_ioremap 函数接收三个参数:
-
phys_addr-- 待映射的 I/O 物理地址
-
size -- 待映射内存区域的大小
-
prot -- 页属性
__early_ioremap 函数定义如下:
// file: arch/x86/mm/ioremap.c
static void __init __iomem *
__early_ioremap(resource_size_t phys_addr, unsigned long size, pgprot_t prot)
{
unsigned long offset;
resource_size_t last_addr;
unsigned int nrpages;
enum fixed_addresses idx0, idx;
int i, slot;
WARN_ON(system_state != SYSTEM_BOOTING);
slot = -1;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
if (!prev_map[i]) {
slot = i;
break;
}
}
if (slot < 0) {
printk(KERN_INFO "early_iomap(%08llx, %08lx) not found slot\n",
(u64)phys_addr, size);
WARN_ON(1);
return NULL;
}
if (early_ioremap_debug) {
printk(KERN_INFO "early_ioremap(%08llx, %08lx) [%d] => ",
(u64)phys_addr, size, slot);
dump_stack();
}
/* Don't allow wraparound or zero size */
last_addr = phys_addr + size - 1;
if (!size || last_addr < phys_addr) {
WARN_ON(1);
return NULL;
}
prev_size[slot] = size;
/*
* Mappings have to be page-aligned
*/
offset = phys_addr & ~PAGE_MASK;
phys_addr &= PAGE_MASK;
size = PAGE_ALIGN(last_addr + 1) - phys_addr;
/*
* Mappings have to fit in the FIX_BTMAP area.
*/
nrpages = size >> PAGE_SHIFT;
if (nrpages > NR_FIX_BTMAPS) {
WARN_ON(1);
return NULL;
}
/*
* Ok, go for it..
*/
idx0 = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
idx = idx0;
while (nrpages > 0) {
early_set_fixmap(idx, phys_addr, prot);
phys_addr += PAGE_SIZE;
--idx;
--nrpages;
}
if (early_ioremap_debug)
printk(KERN_CONT "%08lx + %08lx\n", offset, slot_virt[slot]);
prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]);
return prev_map[slot];
}
因为 __early_ioremap 本身是为内核启动时服务的,启动完成后,应该调用 ioremap 函数完成同样的功能。所以,函数一开始,检查内核当前是否处于启动状态,如果不是,则打印警告信息。
WARN_ON(system_state != SYSTEM_BOOTING);
system_state是枚举变量,而 SYSTEM_BOOTING 是枚举类型的一个成员。
// file: include/linux/kernel.h
/* Values used for system_state */
extern enum system_states {
SYSTEM_BOOTING,
SYSTEM_RUNNING,
SYSTEM_HALT,
SYSTEM_POWER_OFF,
SYSTEM_RESTART,
} system_state;
枚举变量 system_state 的初始值为 0,即 SYSTEM_BOOTING;该变量在 kernel_init 函数中被修改为 SYSTEM_RUNNING:
// file: init/main.c
static int __ref kernel_init(void *unused)
{
...
system_state = SYSTEM_RUNNING;
...
}
接下来,将槽号 slot 设置为默认值 -1,表示无槽可用;然后遍历prev_map数组,搜索数组中的第一个空闲槽。当找到空闲槽时,把槽号保存到变量 slot中:
slot = -1;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
if (!prev_map[i]) {
slot = i;
break;
}
}
其中,prev_map 是一个静态数组,数组元素类型为 void * ,该数组内保存的是物理地址映射的虚拟地址;类似的,prev_size 也是一个静态数组,其元素类型为 unsigned long,该数组内保存的是映射区域的原始大小。我们在早期 ioremap 的初始化时,还看到过另外一个类似的数组 slot_virt,此数组内保存的是每个槽的起始虚拟地址。
// file: arch/x86/mm/ioremap.c
static void __iomem *prev_map[FIX_BTMAPS_SLOTS] __initdata;
static unsigned long prev_size[FIX_BTMAPS_SLOTS] __initdata;
完成搜索后,如果变量 slot 小于 0,说明没找到空闲槽,打印错误信息并返回 NULL。
if (slot < 0) {
printk(KERN_INFO "early_iomap(%08llx, %08lx) not found slot\n",
(u64)phys_addr, size);
WARN_ON(1);
return NULL;
}
如果变量 early_ioremap_debug 为真,那么需要打印调试信息。变量early_ioremap_debug 的信息我们在上文中已经介绍过来,此处不再赘述。
if (early_ioremap_debug) {
printk(KERN_INFO "early_ioremap(%08llx, %08lx) [%d] => ",
(u64)phys_addr, size, slot);
dump_stack();
}
计算出待映射区域的最大物理地址,保存到变量 last_addr
中,然后进行参数检查。
/* Don't allow wraparound or zero size */
last_addr = phys_addr + size - 1;
if (!size || last_addr < phys_addr) {
WARN_ON(1);
return NULL;
}
如果 size 为 0 或者 I/O 物理区域的结束地址小于起始地址(说明地址发生了回绕),这两种都是异常情况,打印警告信息,并返回 NULL。把搜索到的空闲槽位和区域大小,在数组 prev_size 中建立映射关系。
prev_size[slot] = size;
接下来,我们看到如下代码:
/*
* Mappings have to be page-aligned
*/
offset = phys_addr & ~PAGE_MASK;
phys_addr &= PAGE_MASK;
size = PAGE_ALIGN(last_addr + 1) - phys_addr;
宏 PAGE_MASK
是页掩码,该宏定义如下:
// file: arch/x86/include/asm/page_types.h
/* PAGE_SHIFT determines the page size */
#define PAGE_MASK (~(PAGE_SIZE-1))
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_SHIFT 12
我们知道一页的大小是 4096 字节或者二进制1000000000000;PAGE_SIZE - 1将会是 111111111111;~(PAGE_SIZE-1)将得到 000000000000,即 PAGE_MASK;~PAGE_MASK让我们再次得到 111111111111。
所以,变量 offset 中保存的是物理地址 phys_addr 中低 12 位的值,即页内偏移。在下一行,清除了物理地址 phys_addr 的低 12 位,保留的是页基地址。然后我们调整区域大小,让它的上下边界都对齐到页。调整完成后,该区域大小是页的整数倍。
接下来,需要计算新区域占用的页数。如果页数大于每个槽允许的最大页数 NR_FIX_BTMAPS(扩展为 64),说明新区域的大小超出了槽的容量,打印错误信息,并返回 NULL。
/*
* Mappings have to fit in the FIX_BTMAP area.
*/
nrpages = size >> PAGE_SHIFT;
if (nrpages > NR_FIX_BTMAPS) {
WARN_ON(1);
return NULL;
}
槽位已经确定了,接下来,我们计算出该槽位的起始索引,并保存到变量 idx
中。
idx0 = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
idx = idx0;
现在新区域的页面数量以及在 Fixmap 中的起始索引都已经确定了,我们就可以通过循环,把新区域中的每个页基地址与槽中的索引逐一建立映射关系。每次迭代,我们调用early_set_fixmap
函数,将给定的物理地址映射到索引值,然后让物理地址增加页面大小(4096 字节),并更新索引和页面数:
while (nrpages > 0) {
early_set_fixmap(idx, phys_addr, prot);
phys_addr += PAGE_SIZE;
--idx;
--nrpages;
}
至此,我们已经建立好物理地址和虚拟地址的映射关系。early_set_fixmap 函数的实现细节,见 early_set_fixmap 小节, 我们先继续往下看。
在通过循环,将物理内存区域映射到固定映射区后,会执行以下代码:
prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]);
return prev_map[slot];
slot_virt 数组中,保存着每个槽的起始虚拟地址;变量 slot 中保存的是我们实际映射到的槽位;所以 slot_virt[slot] 就是物理页基地址映射到的虚拟地址,offset + slot_virt[slot] 就是指定物理地址映射到的虚拟地址。
最后,将映射后的虚拟地址保存到 prev_map[slot] 中,并返回。
early_ioremap 工作示意图:
⑴early_set_fixmap
early_set_fixmap 函数实现如下:
// file: arch/x86/mm/ioremap.c
static inline void __init early_set_fixmap(enum fixed_addresses idx,
phys_addr_t phys, pgprot_t prot)
{
if (after_paging_init)
__set_fixmap(idx, phys, prot);
else
__early_set_fixmap(idx, phys, prot);
}
如果 after_paging_init为真,那么调用 __set_fixmap 函数来完成映射;否则,调用__early_set_fixmap 函数来完成早期的映射。
after_paging_init是一个静态变量,由于未显式初始化,所以其初始值为 0。
// file: arch/x86/mm/ioremap.c
static __initdata int after_paging_init;
通过调用 early_ioremap_reset 函数,可以将其值修改为 1。
// file: arch/x86/mm/ioremap.c
void __init early_ioremap_reset(void)
{
after_paging_init = 1;
}
early_ioremap_reset 函数只有在 32 位系统中才会调用。所以,在 64 位系统下,最终会调用 __early_set_fixmap 函数完成映射。
⑵__early_set_fixmap
__early_set_fixmap 函数的主要功能是填充页表项,建立页表项和物理页的映射关系,其定义如下:
// file: arch/x86/mm/ioremap.c
static void __init __early_set_fixmap(enum fixed_addresses idx,
phys_addr_t phys, pgprot_t flags)
{
unsigned long addr = __fix_to_virt(idx);
pte_t *pte;
if (idx >= __end_of_fixed_addresses) {
BUG();
return;
}
pte = early_ioremap_pte(addr);
if (pgprot_val(flags))
set_pte(pte, pfn_pte(phys >> PAGE_SHIFT, flags));
else
pte_clear(&init_mm, addr, pte);
__flush_tlb_one(addr);
}
__early_set_fixmap函数接收 3 个参数:
-
idx -- 固定映射区的索引值
-
phys -- 待映射的物理地址
-
flags -- 页属性
首先调用 __fix_to_virt 将索引值转换为虚拟地址。然后检查索引值 idx 是否越界。__end_of_fixed_addresses是固定映射区的边界索引,如果 idx 大于等于该边界值,说明索引越界,报错并返回。接下来,调用 early_ioremap_pte 函数,获取虚拟地址的页表项地址。
pte = early_ioremap_pte(addr);
我们来看下 early_ioremap_pte 函数的具体实现:
// file: arch/x86/mm/ioremap.c
static inline pte_t * __init early_ioremap_pte(unsigned long addr)
{
return &bm_pte[pte_index(addr)];
}
还记得么,我们在早期 ioremap 初始化时将临时映射区的所有页表项保存在数组 bm_pte中。由于 bm_pte就是临时映射区的页表,所以先是通过 pte_index 计算出虚拟地址所对应的页表项索引,然后通过 bm_pte[pte_index(addr)]获取到页表项,最后返回该页表项的地址。
下一步,我们使用宏pgprot_val 获取到页属性,然后检查页属性是否为 0。如果页属性不为 0,说明页属性有效,调用 set_pte 函数设置页表项;否则,调用 pte_clear 函数解除页表项 pte 与页的映射关系。
if (pgprot_val(flags))
set_pte(pte, pfn_pte(phys >> PAGE_SHIFT, flags));
else
pte_clear(&init_mm, addr, pte);
在 early_ioremap 函数中,我们将PAGE_KERNEL_IO作为页属性传递给__early_ioremap。PAGE_KERNEL_IO扩展为:
// file: arch/x86/include/asm/pgtable_types.h
#define PAGE_KERNEL_IO __pgprot(__PAGE_KERNEL_IO)
#define __PAGE_KERNEL_IO (__PAGE_KERNEL | _PAGE_IOMAP)
#define __PAGE_KERNEL (__PAGE_KERNEL_EXEC | _PAGE_NX)
宏 _PAGE_IOMAP
定义如下:
// file: arch/x86/include/asm/pgtable_types.h
#define _PAGE_IOMAP (_AT(pteval_t, 1) << _PAGE_BIT_IOMAP)
#define _PAGE_BIT_IOMAP 10 /* flag used to indicate IO mapping */
注意这里的 _PAGE_IOMAP 位,从定义上看,这是表项标志位的第 10 位。但是,处理器并不支持该标志位,也就是说,这是内核自己用的,与处理器无关。由于 flags 有效,所以我们会调用set_pte函数来设置页表项。此步执行后,就建立了页表项和物理地址的映射关系:
由于我们手动更改了分页结构,处理器并不知晓,所以我们需要手动刷新 TLB。在 __early_set_fixmap 函数的最后,就调用__flush_tlb_one 函数来刷新 TLB,使 TLB 中的给定地址无效:
__flush_tlb_one(addr);
__flush_tlb_one 函数定义如下:
// file: arch/x86/include/asm/tlbflush.h
static inline void __flush_tlb_one(unsigned long addr)
{
__flush_tlb_single(addr);
}
__flush_tlb_one 函数内部调用了宏 __flush_tlb_single:
// file: arch/x86/include/asm/tlbflush.h
#define __flush_tlb_single(addr) __native_flush_tlb_single(addr)
宏 __flush_tlb_single 扩展为 __native_flush_tlb_single:
// file: arch/x86/include/asm/tlbflush.h
static inline void __native_flush_tlb_single(unsigned long addr)
{
asm volatile("invlpg (%0)" ::"r" (addr) : "memory");
}
__native_flush_tlb_single 函数调用了内联汇编,使用了汇编指令 invlpg使 TLB 中指定的地址失效。
4.2early_iounmap
函数 early_iounmap 取消I/O内存区域的映射,函数定义如下:
// file: arch/x86/mm/ioremap.c
void __init early_iounmap(void __iomem *addr, unsigned long size)
{
unsigned long virt_addr;
unsigned long offset;
unsigned int nrpages;
enum fixed_addresses idx;
int i, slot;
slot = -1;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
if (prev_map[i] == addr) {
slot = i;
break;
}
}
if (slot < 0) {
printk(KERN_INFO "early_iounmap(%p, %08lx) not found slot\n",
addr, size);
WARN_ON(1);
return;
}
if (prev_size[slot] != size) {
printk(KERN_INFO "early_iounmap(%p, %08lx) [%d] size not consistent %08lx\n",
addr, size, slot, prev_size[slot]);
WARN_ON(1);
return;
}
if (early_ioremap_debug) {
printk(KERN_INFO "early_iounmap(%p, %08lx) [%d]\n", addr,
size, slot);
dump_stack();
}
virt_addr = (unsigned long)addr;
if (virt_addr < fix_to_virt(FIX_BTMAP_BEGIN)) {
WARN_ON(1);
return;
}
offset = virt_addr & ~PAGE_MASK;
nrpages = PAGE_ALIGN(offset + size) >> PAGE_SHIFT;
idx = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
while (nrpages > 0) {
early_clear_fixmap(idx);
--idx;
--nrpages;
}
prev_map[slot] = NULL;
}
该函数接收两个参数:
-
addr -- 取消映射的虚拟地址
-
size -- 区域大小。
首先,将槽号 slot 初始化为 -1,表示未找到地址对应的槽位;然后遍历prev_map数组,查看哪个槽的数据与给定地址相等。还记得么,我们在 __early_ioremap 函数最后,把映射到的虚拟地址保存到了 prev_map[slot] 中。当找到对应的槽时,会把它保存到变量 slot 中:
slot = -1;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
if (prev_map[i] == addr) {
slot = i;
break;
}
}
如果 slot
小于 0,说明没找到对应的槽,打印错误信息并返回。
if (slot < 0) {
printk(KERN_INFO "early_iounmap(%p, %08lx) not found slot\n",
addr, size);
WARN_ON(1);
return;
}
如果已映射区域的大小与要释放的大小不一致,打印错误信息并返回。
if (prev_size[slot] != size) {
printk(KERN_INFO "early_iounmap(%p, %08lx) [%d] size not consistent %08lx\n",
addr, size, slot, prev_size[slot]);
WARN_ON(1);
return;
}
如果设置了调试参数,那么会打印调式信息。
if (early_ioremap_debug) {
printk(KERN_INFO "early_iounmap(%p, %08lx) [%d]\n", addr,
size, slot);
dump_stack();
}
如果传入的虚拟地址小于临时映射区的最小地址,说明地址有误,打印警告信息并返回。
virt_addr = (unsigned long)addr;
if (virt_addr < fix_to_virt(FIX_BTMAP_BEGIN)) {
WARN_ON(1);
return;
}
计算页内偏移及内存区域对应的页数,可参考 __early_ioremap
函数中的计算过程。
offset = virt_addr & ~PAGE_MASK;
nrpages = PAGE_ALIGN(offset + size) >> PAGE_SHIFT;
计算出待释放区域的起始地址对应的固定映射区索引值。然后通过循序,调用 early_clear_fixmap
函数,取消索引和地址的映射关系,并更新索引值和页数。
idx = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
while (nrpages > 0) {
early_clear_fixmap(idx);
--idx;
--nrpages;
}
最后,通过将数组 prev_map
中对应的槽位设置为 NULL,来释放该槽位。
prev_map[slot] = NULL;
early_clear_fixmap
取消地址映射的功能,主要是在 early_clear_fixmap 函数中执行的,我们来看下具体实现:
// file: arch/x86/mm/ioremap.c
static inline void __init early_clear_fixmap(enum fixed_addresses idx)
{
if (after_paging_init)
clear_fixmap(idx);
else
__early_set_fixmap(idx, 0, __pgprot(0));
}
我们在 early_set_fixmap 函数中介绍过变量 after_paging_init ,该变量在 32 位系统中才会为设置为 1。所以,此处我们会执行到 __early_set_fixmap 函数。
我们在上文已经介绍过 __early_set_fixmap 函数,该函数会把 idx 对应的虚拟地址映射到给定的物理地址,并设置页属性。在当前情况下,会把 idx 对应的虚拟地址映射到物理地址 0,且页属性也为 0。由于处理器是根据存在位(位 0 )来判断表项是否有效,当页属性为 0 时,该表项是无效的,也就意味着取消了映射关系。
五、对比探究:与常规ioremap的异同
5.1相同之处:殊途同归的映射目的
常规的 ioremap 函数与 early ioremap,尽管在使用时机和一些特性上存在差异,但它们的核心目标是一致的,那就是实现物理地址到虚拟地址的精准映射,为内核访问 I/O 内存资源架起桥梁。无论是在系统运行的稳定期,还是在内核启动的关键早期,当内核需要与外设进行交互,读取或写入外设寄存器数据时,都需要借助这种映射机制,将已知的外设物理地址转换为内核能够直接访问的虚拟地址,就如同将现实世界中的物理地点在地图上标记出对应的虚拟坐标一样,使得内核能够按照统一的虚拟地址规则,顺畅地与各种外设进行通信,完成诸如设备初始化、数据传输等关键任务,保障系统整体的正常运转。
5.2差异之美:适用场景与特性的分化
使用阶段:常规 ioremap 在系统内存管理系统基本初始化完成后,也就是内核启动进入相对稳定的运行阶段发挥作用。此时,内存的页表体系已经搭建完善,各类内存管理函数都能正常运作,它可以灵活地根据需求随时为外设 I/O 内存资源建立映射,就像一个成熟的物流配送中心,可以随时接收新的货物订单并安排精准配送。而 early ioremap 则专注于内核启动的早期,当内存管理还处于 “襁褓” 之中,大部分内存尚未建立完善页表映射时,它挺身而出,满足那些迫不及待需要与内核通信的硬件设备的需求,为系统启动过程中的关键设备初始化提供支持,就像是在物流中心刚筹备时,为急需的重要物资开辟的一条临时绿色通道。
灵活性:常规 ioremap 的灵活性更高,它可以在系统运行的任意时刻,根据设备驱动的需求,对不同的物理地址范围进行映射,并且映射的大小、位置等参数可以较为自由地设定,只要符合内存管理的基本规则即可,如同一位经验丰富的画家,可以在画布的任意位置、以各种尺寸绘制精美的图案。而 early ioremap 由于依托 fixmap 区域,其映射的范围相对固定,只能在预先划定的 fixmap 区域内,按照固定的粒度(通常与页大小相关)进行映射分配,就像是在一个已经规划好格子的模板上填色,虽然受限,但在早期却能快速、稳定地解决关键问题。
内存开销:常规 ioremap 在建立映射时,需要遵循完整的内存管理流程,包括页表的动态分配、更新等操作,这在一定程度上会带来一定的内存开销和时间成本,尤其是频繁进行小范围、频繁变动的映射操作时,可能会对系统性能产生一些影响,就好比频繁地重新规划仓库布局来存放不同的小批量货物。而 early ioremap 利用 fixmap 的预定义特性,在早期避免了复杂的动态内存管理操作,直接使用固定的映射区域,减少了额外的内存分配与管理开销,能够以较低的成本快速满足硬件设备的紧急需求,就像在紧急情况下直接使用预先准备好的应急物资仓库,快速响应,高效解决问题。
5.3early ioremap 对系统启动的价值
early ioremap 在 Linux 系统启动过程中扮演着不可或缺的角色,其重要性犹如大厦基石,支撑着整个系统的初期构建。在内核启动的最初时刻,当硬件设备从 “沉睡” 中苏醒,急切渴望与内核进行交互时,early ioremap 就如同一位敏捷的使者,迅速搭建起沟通的桥梁。它使得内核能够在内存管理体系尚未完全成熟的情况下,精准地访问硬件设备的寄存器,获取关键的硬件信息,如设备的型号、配置参数等,这些信息如同医生手中的病历,为后续的设备初始化与配置提供了至关重要的依据。
以计算机启动时的硬盘控制器为例,内核需要在早期读取硬盘控制器的寄存器,确定硬盘的型号、容量、传输模式等参数,以便正确加载相应的驱动程序并进行初始化设置。若没有 early ioremap,内核在启动初期将因无法访问这些寄存器而陷入困境,导致硬盘无法正常识别,进而使整个系统启动失败。正是 early ioremap 的存在,确保了内核能够有条不紊地对硬件设备进行逐一初始化,为系统进入稳定运行状态奠定坚实基础,让计算机从按下开机键的那一刻起,就沿着正确的轨道逐步走向功能完备的 “数字世界”,为用户提供高效、稳定的服务。
六、结语:总结与展望
回顾 early ioremap 重点,鼓励读者深入探索 Linux 内核,畅想未来优化方向。
至此,我们对 Linux Kernel 内存管理中的早期 I/O 内存映射(early ioremap)有了较为深入的了解。从它诞生的背景,到 fixmap 基石的支撑,再到初始化与实际应用的详细过程,以及与常规 ioremap 的异同对比,我们看到了 early ioremap 在内核启动初期为系统稳定运行所付出的努力。它就像是一位幕后英雄,默默解决内核启动时内存管理不完善带来的难题,确保硬件设备能及时与内核沟通,为整个 Linux 系统的顺利起航保驾护航。
然而,技术的发展永无止境。随着硬件性能的不断提升,如更快的存储设备、更强大的处理器集成显卡等新型硬件的涌现,以及软件应用场景日益复杂多样化,对 Linux 内核内存管理尤其是早期 I/O 内存映射机制提出了更高的要求。或许在未来,我们会看到 early ioremap 在映射效率上进一步优化,能够以更低的开销、更快的速度完成关键设备的初始化映射;又或者在与新硬件架构适配方面更加智能,自动识别并适应不同硬件平台的独特需求,无需过多人工干预。希望各位读者能以此为契机,深入探索 Linux 内核的神秘世界,说不定未来的某一天,你就是推动这些技术变革的关键一员,让 Linux 系统在各个领域绽放更加耀眼的光芒。