计算机系统启动过程

计算机系统启动过程

阅读笔记:

《计算机体系结构基础(第三版)》-- 胡伟武

第7章:计算机系统启动过程分析

系统启动的整个过程中, 计算机系统在软件的控制下由无序到有序, 所有的组成部分都由程序管理, 按照程序的执行发挥各自的功能, 最终将系统的控制权安全交到操作系统手中, 完成整个启动过程。

无论采用何种指令系统的处理器, 复位后的第一条指令都会从一个预先定义的特定地址取回

处理器的启动过程, 实际上就是一个特定程序的执行过程。 这个程序我们称之为固件, 又称为 BIOS(Basic Input Output System, 基本输入输出系统)

对于 LoongArch, 处理器复位后的第一条指令将固定从地址 0x1C000000 的位置获取。 这个地址需要对应一个能够给处理器核提供指令的设备, 这个设备以前是各种 ROM, 现在通常是闪存(Flash)

第一条执行的指令是由硬件层面决定的,这个地址需要结合硬件相关的手册。

1、处理器核初始化

所谓初始化, 实际上是将计算机内部的各种寄存器状态从不确定设置为确定, 将一些模块状态从无序强制为有序的过程。 简单来说, 就是通过 load / store 指令或其他方法将指定寄存器或结构设置为特定数值。

在 MIPS 和 LoongArch 结构中, 都只将 0 号寄存器的值强制规定为 0, 而其他的通用寄存器值是没有要求的。 在处理器复位后开始运行的时候, 这些寄存器的值可能是任意值。如果需要用到寄存器内容, 就需要先对其进行赋值, 将这个寄存器的内容设置为软件期望的值。

在现代处理器支持的猜测执行、 预取等微结构特性中, 可能会利用某些通用寄存器的值或者高速缓存的内容进行猜测。 如果整个处理器的状态并没有完全可控, 或许会猜测出一些极不合理的值, 导致处理器微结构上执行出错而引发死机。 这样就需要对一些必要的单元结构进行初始化, 防止这种情况发生。

在处理器开始执行之后, 一方面需要先对相关的寄存器内容进行初始化, 设置为一个正常地址值, 另一方面则需要对地址空间进行处理, 防止出现一般情况下不可访问的地址空洞。 这样即使发生了这种猜测访问, 也可以得到响应, 避免系统出错或死机。

1.1、 处理器复位

处理器的第一条指令实际上是由复位信号控制的, 但受限于各种其他因素, 复位信号并没有对处理器内部的所有部分进行控制, 例如 TLB、 Cache 等复杂结构, 而是只保证从取指部件到 BIOS 取指令的通路畅通。

在 LoongArch 架构下, 处理器复位后工作在直接地址翻译模式下。 该模式下的地址为虚实地址直接对应的关系, 也就是不经 TLB 映射, 也不经窗口映射。 默认情况下, 无论是取指访问还是数据访问, 都是 Uncache 模式, 也即不经缓存。 这样即使硬件不对 TLB、 Cache 两个结构进行初始化, 处理器也能正常启动并通过软件在后续的执行中对这些结构进行初始化。

但现在大部分处理器在硬件上自动处理, 从而减轻软件负担, 缩短系统启动时间。硬件初始化的时机是在系统复位解除之后、 取指访问开始之前, 以此来缩短 BIOS 的启动时间。

LoongArch 处理器复位后的第一条指令将固定从地址 0x1C000000 的位置获取, 这个过程是由处理器的执行指针寄存器PC被硬件复位为 0x1C000000 而决定的。

需要指出的是,处理器复位后先是通过频率为几十兆赫兹 (MHz) 以下的低速设备取指令, 例如 SPI 或 LPC 等接口。 一拍只能取出 1 比特 (SPI) 或 4 比特 (LPC), 而一条指令一般需要 32 比特。

整个处理器由系统复位到操作系统启动的简要流程如图7.1所示。 其中第一列为处理器核初始化过程, 第二列为芯片核外部分初始化过程, 第三列为设备初始化过程, 第四列为内核加载过程, 第五列为多核芯片中的从核 (Slave Core) 独有的启动过程

由内之外的过程:核内初始化 -> 核心周围初始化 -> 设备初始化 -> 加载内核 -> 唤醒其他核心

在这里插入图片描述

1.2、 调试接口初始化

在启动过程中优先初始化的是什么呢? 首先是用于调试的接口部分。 比如开机时听到的蜂鸣器响声, 或者在一些主板上看到的数码管显示, 都是最基本的调试用接口。 对于龙芯3 号处理器来说, 最先初始化的结构是芯片内集成的串口控制器。

对串口的初始化操作实际上是处理器对串口执行一连串约定好的 IO 操作。在 X86 结构下, IO 地址空间与内存地址空间相互独立, IO 操作与访存操作是通过不同的指令实现的。 MIPS 和LoongArch 等结构并不显式区分 IO 和内存地址, 而是采用全局编址, 使用地址空间将 IO 和内存隐式分离, 并通过地址空间或 TLB 映射对访问方式进行定序及缓存等的控制。

内存空间对应的是存储器, 存储器不会发生存储内容的自行更新。 也就是说, 如果处理器核向存储单元 A 中写入了 0x5a5a 的数值, 除非有其他的主控设备 (例如其他的处理器核或是其他的设备 DMA) 对它也进行写入操作, 否则这个 0x5a5a 的数值是不会发生变化的。

IO 空间一般对应的是控制寄存器或状态寄存器, 是受 IO 设备的工作状态影响的。 此时,写入的数据与读出的数据可能会不一致, 而多次读出的数据也可能不一致, 其读出数据是受具体设备状态影响的。

串口的线路状态寄存器里的各个数据位都与当时的设备状态相关。

IO 寄存器的行为与具体的设备紧密相关, 每种 IO 设备都有各自不同的寄存器说明, 需要按照其规定的访问方式进行读写, 而不像内存可以进行随意的读写操作。

处理器上运行的指令使用虚拟地址, 虚拟地址通过地址映射规则与物理地址相关联。基本的虚拟地址属性首先区分为经缓存 (Cache) 与不经缓存 (Uncache)两种。 对于内存操作, 现代高性能通用处理器都采用 Cache 方式进行访问, 以提升访存性能。Cache 在离处理器更近的位置上利用访存局部性原理进行缓存, 以加速重复访存或者其他规则访存 (通过预取等手段)。 对于存储器来说, 在 Cache 中进行缓存是没有问题的, 因为存储器所存储的内容不会自行修改 (但可能会被其他核或设备所修改, 这个问题可以通过缓存一致性协议解决)。 但是对于 IO 设备来说, 因为其寄存器状态是随着工作状态的变化而变化的, 如果缓存在 Cache 中, 那么处理器核将无法得到状态的更新, 所以一般情况下不能对 IO 地址空间进行 Cache 访问, 需要使用 Uncache 访问。 使用 Uncache 访问对 IO 进行操作还有另一个作用,就是可以严格控制读写的访问顺序, 不会因为预取类的操作导致寄存器状态的丢失。

串口初始化程序仅仅是对串口的通信速率及一些控制方法进行设置, 以使其很方便地通过一个串口交互主机进行字符的读写交互。

1.3、 TLB初始化

TLB 作为一个地址映射的管理模块, 主要负责操作系统里用户进程地址空间的管理, 用以支持多用户多任务并发。然而在处理器启动的过程中, 处理器核处于特权态, 整个 BIOS 都工作在统一的地址空间里, 并不需要对用户地址空间进行过多干预。此时 TLB 的作用更多是地址转换, 以映射更大的地址空间供程序使用。

LoongArch 结构采用了分段和分页两种不同的地址映射机制。 分段机制将大段的地址空间与物理地址进行映射, 具体的映射方法在 BIOS 下使用窗口机制进行配置, 主要供系统软件使用。 而分页机制通过 TLB 起作用, 主要由操作系统管理, 供用户程序使用

BIOS 一般映射两段, 其中 0x90000000_00000000 开始的地址空间被映射为经缓存的地址, 0x80000000_00000000 开始的地址空间被映射为不经缓存的地址。 根据地址空间的转换规则, 这两段转换为物理地址时直接抹除地址的高位, 分别对应整个物理地址空间, 仅仅在是否经过Cache 缓存上有所区别。

这个地址也并不是绝对的,要看相关手册。

由于分段机制是通过不同的虚拟地址来映射全部的物理地址空间, 并不适合用作用户程序的空间隔离和保护, 也不适合需要更灵活地址空间映射方法的场合。 这些场景下就需要利用TLB 机制。

TLB 的初始化主要是将全部表项初始化为无效项。

**初始化为无效项就是将 TLB 的每项逐一清空, 以免程序中使用的地址被未初始化的 TLB 表项所错误映射。**在没有硬件复位 TLB 逻辑的处理器里, 启动时 TLB 里可能会包含一些残留的或者随机的内容, 这部分内容可能会导致 TLB 映射空间的错误命中。 因此在未实现硬件复位TLB 的处理器中, 需要对整个 TLB 进行初始化操作。

越来越多的处理器已经实现了在芯片复位时由硬件进行 TLB 表项的初始化, 这样在 BIOS 代码中可以不用再使用类似的软件初始化流程。

1.4、 Cache初始化

Cache 的引入能够减小处理器执行和访存延迟之间的性能差异, 即缓解存储墙的问题。 引入 Cache 结构, 能够大大提高处理器的整体运行效率。

在系统复位之后, Cache 同样也处于一个未经初始化的状态, 也就是说 Cache 里面可能包含残留的或随机的数据, 如果不经初始化, 对于 Cache 空间的访问也可能会导致错误的命中。

Cache 的组织结构主要包含标签 (Tag) 和数据 (Data) 两个部分, Tag 用于保存 Cache 块状态、 Cache 块地址等信息, Data 则保存数据内容。 大多数情况下对 Cache 的初始化就是对 Tag 的初始化, 只要将其中的 Cache 块状态设置为无效, 其他部分的随机数据就不会产生影响。

处理器在复位完成之后就处于最高特权态中, 完成各项初始化。

在完成所有 Cache 层次的初始化之后, 就可以跳转到 Cache 空间开始执行。 此后程序的运行效率将会有数十倍的提升。

在跳转到 Cache 空间执行后, 程序运行效率大大提升, 随之而来的是处理器内各种复杂猜测机制的使用。

得益于摩尔定律的持续生效, 片上 Cache 的容量越来越大, 由此却带来了初始化时间越来越长的问题。 但同时, 在拥有越来越多可用片上资源的情况下, TLB、 Cache 等结构的初始化也更多地开始使用硬件自动完成, 软件需要在这些初始化上耗费的时间也越来越少。

TLB初始化要在Cache初始化之前进行。

完成 Cache 空间的初始化并跳转至 Cache 空间运行也标志着处理器的核心部分, 或者说体系结构相关的初始化部分已经基本完成。 接下来将对计算机系统所使用的内存总线和 IO 总线等外围部分进行初始化。

2、 总线接口初始化

在使用同一款处理器的不同系统上, TLB、 Cache 这些体系结构紧密相关的芯片组成部分的初始化方法是基本一致的, 不需要进行特别的改动。 与此不同的是, 内存和 IO 设备的具体组成在计算机系统中则可以比较灵活地搭配, 不同系统间的差异可能会比较大。

这些不同的配置情况要求在计算机系统启动时能够进行有针对性的初始化。

2.1、 内存初始化

冯·诺依曼结构下, 计算机运行时的程序和数据都被保存在内存之中。 相对复位时用于取指的 ROM 或是 Flash 器件来说, 内存的读写性能大幅提高。

越来越多的处理器已经集成内存控制器。 因为内存的使用和设置与外接内存芯片的种类、配置相关, 所以在计算机系统启动的过程中需要先获取内存的配置信息, 再根据该信息对内存控制器进行配置。

获取这些信息是程序通过 I2C 总线对外部内存条的 SPD 芯片进行读操作来完成的。 SPD 芯片也相当于一个 Flash 芯片, 专门用于存储内存条的配置信息。

对内存的初始化实际上就是根据内存配置信息对内存控制器进行初始化。 与 Cache 初始化类似的是, 内存初始化并不涉及其存储的初始数据。 与 Cache 又有所不同的地方在于, Cache 有专门的硬件控制位来表示 Cache 块是否有效, 而内存却并不需要这样的硬件控制位。 内存的使用完全是由软件控制的, 软件知道其访问的每一个地址是否存在有效的数据。 而 Cache 是一个硬件加速部件, 大多数情况下软件并不需要真正知道而且也不希望知道其存在, Cache 的硬件控制位正是为了掩盖内存访问延迟, 保证访存操作的正确性。 因此内存初始化仅仅需要对内存控制器进行初始化, 并通过控制器对内存的状态进行初始化。 在初始化完成以后, 如果是休眠唤醒, 程序可以使用内存中已有的数据来恢复系统状态; 如果是普通开机, 则程序可以完全不关心内存数据而随意使用。

内存控制器的初始化包括两个相对独立的部分, 一是根据内存的行地址、 列地址等对内存地址映射进行配置, 二是根据协议对内存信号调整的支持方式对内存读写信号相关的寄存器进行训练, 以保证传输时的数据完整性。

在内存初始化完成后, 可能还需要根据内存的大小对系统可用的物理地址空间进行相应的调整和设置。

2.2、 IO总线初始化

受外围桥片搭配及可插拔设备变化的影响, 系统每次启动时需要对 IO 总线进行有针对性的初始化操作。

完成内存与 IO 总线初始化后, BIOS 中基本硬件初始化的功能目标已经达到。 但是为了加载操作系统, 还必须对系统中的一些设备进行配置和驱动。 操作系统所需要的存储空间比较大, 通常无法保存在 Flash 这样的存储设备中, 一般保存在硬盘中并在使用时加载, 或者也可以通过网口、 U 盘等设备进行加载。 为此就需要使用更复杂的软件协议来驱动系统中的各种设备, 以达到加载操作系统的最终目标。

在此之前的程序运行基本没有使用内存进行存数取数操作, 程序也是存放在 Flash 空间之中的, 只不过是经过了 Cache 的缓存加速。 在此之后的程序使用的复杂数据结构和算法才会对内存进行大量的读写操作。 为了进一步提高程序的运行效率, 先将程序复制到内存中, 再跳转到内存空间开始执行。

此后, 还需要对处理器的运行频率进行测量, 对 BIOS 中的计时函数进行校准, 以便在需要等待的位置进行精确的时间同步。 在经过对各种软件结构必要的初始化之后, BIOS 将开始一个比较通用的设备枚举和驱动加载的过程。

3、 设备的探测及驱动加载

PCI 总线于 20 世纪 90 年代初提出, 发展到现在已经逐渐被 PCIE 等高速接口所替代, 但其软件配置结构却基本没有发生变化, 包括 HyperTransport、 PCIE 等新一代高速总线都兼容 PCI协议的软件框架。

在 PCI 软件框架下, 系统可以灵活地支持设备的自动识别和驱动的自动加载。

在 PCI 协议下, IO 的系统空间分为三个部分: 配置空间、 IO 空间和 Memory 空间。 配置空间存储设备的基本信息, 主要用于设备的探测和发现; IO 空间比较小, 用于少量的设备寄存器访问; Memory 空间可映射的区域较大, 可以方便地映射设备所需要的大块物理地址空间。

配置空间的地址偏移由总线号、 设备号、 功能号和寄存器号的组合得到, 通过对这个组合的全部枚举, 可以很方便地检测到系统中存在的所有设备。

每一个提供 PCI 设备的厂商都应该拥有唯一的厂商识别号, 以在设备枚举时正确地找到其对应的驱动程序。设备识别号对于每一个设备提供商的设备来说应该是唯一的。 这两个识别号的组合就可以在系统中唯一地指明正确的驱动程序。

所谓驱动程序就是一组函数, 包含用于初始化设备、 关闭设备或是使用设备的各种相关操作。

基址寄存器 (Base Address Register, 简称 BAR)。其最低位表示该 BAR 是 IO 空间还是 Memory 空间。BAR 中间有一部分只读位为 0, 正是这些 0 的个数表示该 BAR 所映射空间的大小, 也就是说BAR 所映射的空间为 2 的幂次方大小。 BAR 的高位是可写位, 用来存储软件设置的基地址。

在这里插入图片描述

对 PCI 设备的探测和驱动加载是一个递归调用过程, 大致算法如下:

  1. 将初始总线号、 初始设备号、 初始功能号设为 0。

  2. 使用当前的总线号、 设备号、 功能号组成一个配置空间地址, 这个地址的构成如图 3 所示, 使用该地址, 访问其 0 号寄存器, 检查其设备号。

  3. 如果读出全 1 或全 0, 表示无设备。

  4. 如果该设备为有效设备, 检查每个 BAR 所需的空间大小, 并收集相关信息。

  5. 检测其是否为一个多功能设备, 如果是则将功能号加 1 再重复扫描, 执行第 2 步。

  6. 如果该设备为桥设备, 则给该桥配置一个新的总线号, 再使用该总线号, 从设备号 0、功能号 0 开始递归调用, 执行第 2 步。

  7. 如果设备号非 31, 则设备号加 1, 继续执行第 2 步; 如果设备号为 31, 且总线号为 0,表示扫描结束, 如果总线号非 0, 则退回上一层递归调用。

通过这个递归调用, 就可以得到整个 PCI 总线上的所有设备及其所需要的所有空间信息。有了这些信息, 就可以使用排序的方法对所有的空间从大到小进行分配。 最后, 利用分配的基地址和设备的 ID 信息, 加载相应的驱动就能够正常使用该设备。

4、 多核启动过程

实现不同处理器核之间相互同步与通信的一种手段是核间中断与通信信箱机制

实际上, 信箱寄存器完全可以通过在内存中分配一块地址空间实现, 这样 CPU 访问延迟更短。 而专门使用寄存器实现的信箱寄存器更多是为了在内存还没有初始化前就让不同的核间能够有效通信。

4.1、 初始化时的多核协同

在 BIOS 启动过程中, 为了简化处理流程, 实际上并没有用到中断寄存器, 对于各种外设也没有使用中断机制, 都是依靠处理器的轮询来实现与其他设备的协同工作。

为了简化多核计算机系统的启动过程, 我们将多核处理器中的一个核定为主核, 其他核定为从核主核除了对本处理器核进行初始化之外, 还要负责对各种总线及外设进行初始化; 而从核只需要对本处理器核的私有部件进行初始化, 之后在启动过程中就可以保持空闲状态, 直至进入操作系统再由主核进行调度。

从核需要初始化的工作包括哪些部分呢? 首先是从核私有的部分。 所谓私有, 就是其他处理器核无法直接操纵的部件, 例如核内的私有寄存器、 TLB、 私有 Cache 等, 这些器件只能由每个核自己进行初始化而无法由其他核代为进行。 其次还有为了加速整个启动过程,由主核分配给从核做的工作, 例如当共享 Cache 的初始化操作非常耗时的时候, 可以将整个共享 Cache 分为多个部分, 由不同的核负责某一块共享 Cache 的初始化, 通过并行处理的方式进行加速。

主核的启动过程与前三节介绍的内容基本是一致的。 但在一些重要的节点上则需要与从核进行同步与通信, 或者说通知从核系统已经到达了某种状态。 为了实现这种通知机制,可以将信箱寄存器中不同的位定义为不同的含义, 一旦主核完成了某些初始化阶段, 就可以给信箱寄存器写入相应的值。 例如将信箱寄存器的第 0 位定义为 “ 串口初始化完成” 标志, 第 1 位定义为 “ 共享 Cache 初始化完成” 标志, 第 2 位定义为 “ 内存初始化完成”标志。

在主核完成串口的初始化后, 可以向自己的信箱寄存器写入 0x1。 从核在第一次使用串口之前需要查询主核的信箱寄存器, 如果第 0 位为 0, 则等待并轮询, 如果非 0, 则表示串口已经初始化完成, 可以使用。

在主核完成了共享 Cache 的初始化后, 向自己的信箱寄存器写入 0x3。 而从核在初始化自己的私有 Cache 之后, 还不能直接跳转到 Cache 空间执行, 必须等待信号, 以确信主核已将全部的共享 Cache 初始化完成, 然后再开始 Cache 执行才是安全的。

在主核完成了内存初始化后, 其他核才能使用内存进行数据的读写操作。 那么从核在第一次用到内存之前就必须等待表示内存初始化完成的 0x7 标志。

4.2、 操作系统启动时的多核唤醒

当从核完成了自身的初始化之后, 如果没有其他工作需要进行, 就会跳转到一段等待唤醒的程序。 在这个等待程序里, 从核定时查询自己的信箱寄存器。 如果为 0, 则表示没有得到唤醒标志。 如果非 0, 则表示主核开始唤醒从核, 此时从核还需要从其他几个信箱寄存器里得到唤醒程序的目标地址, 以及执行时的参数。 然后从核将跳转到目标地址开始执行。

在操作系统中, 主核在各种数据结构准备好的情况下就可以开始依次唤醒每一个从核。 唤醒的过程也是串行的, 主核唤醒从核之后也会进入一个等待过程, 直到从核执行完毕再通知主核, 再唤醒一个新的从核, 如此往复, 直至系统中所有的处理器核都被唤醒并交由操作系统管理。

4.2、 核间同步和通信

操作系统启动之前, 利用信箱寄存器进行了大量的多核同步与通信操作, 但在操作系统启动之后, 除了休眠唤醒一类的操作, 却基本不会用到信箱寄存器。 Linux 内核中, 只需要使用核间中断就可以完成各种核间的同步与通信操作。

核间中断也是利用一组 IO 寄存器实现的。 通过将目标核的核间中断寄存器置 1 来产生一个中断信号, 使目标核进入中断处理。 **中断处理的具体内容则是通过内存进行交互的。 内核中为每个核维护一个队列 (内存中的一个数据结构), 当一个核想要中断其他核时, 它将需要处理的内容加入目标核的这个队列, 然后再向目标核发出核间中断 (设置其核间中断寄存器)。**当目标核被中断之后, 开始处理其核间通信队列, 如果其间还收到了更多的核间中断请求, 也会一并处理。

为什么 Linux 内核中的核间中断处理不通过信箱寄存器进行呢? 首先信箱寄存器只有一组, 也就是说如果通过信箱寄存器发送通信消息, 在这个消息没被处理之前, 是不能有其他核再向其发出新的核间中断的。 这样无疑会导致核间中断发送方的阻塞。 另外, 核间中断寄存器实际上是 IO 寄存器, 前面我们提到, 对于 IO 寄存器的访问是通过不经缓存这种严格访问序的方式进行的, 相比于 Cache 访问方式, 不经缓存读写效率极其低下, 本身延迟开销很大, 还可能会导致流水线的停顿。 因此在实际的内核中, 只有类似休眠唤醒这种特定的同步操作才会利用信箱寄存器进行, 其他的同步通信操作则是利用内存传递信息, 并利用核间中断寄存器产生中断的方式共同完成的。

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值