原始模型
-
图灵机
-
冯诺依曼架构
- 输入输出:和设备之间交互数据
- CPU:包括处理单元和控制单元
- 存储器:内存
经典计算模式
- CPU:解析指令
- 内存:存储指令和数据
冯诺依曼有什么局限?
- CPU和内存的交互引起的内存墙问题(CPU速度越来越快,内存的带宽成为发展瓶颈)
- 数据和指令不区分,会出现数据等指令或指令等数据的状态(哈弗结构将指令和数据在缓存中分开)
- 串行顺序处理,缺乏数据并行能力
为什么选择ARM
- 智能手机的模式指令集
- ARM正在走向服务器
华为鲲鹏处理器
ARM发展
- ARMv4及之前
- ARM 32位指令集
- ARMv4T、ARMv5TE
- 加入Thumb 16位指令集
- ARMv6
- 多核、SIMD、TrustZone
- Thumb-2 32位指令集
- ARMv7
- 增强SIMD扩展——NEON
- 全面支持平台操作系统,如Linux
- 可预测实时高性能
- ARMv8
- 扩大物理寻址
4GB以外的物理地址 - 64位虚拟地址
- 自动时间信号
低功耗、高性能的自旋锁 - 硬件加速加密
- 新的异常模型
这里用到了Processor cluster,用cluster是因为现在芯片要考虑到功耗,比如手机里面很多时候并不是要所有核都处于一个最高的频率,所以早期的时候会有大核小核来处理不同的任务。
大核处理器更复杂,主频更高;小核处理频率低,功能相对简单
故一些低功耗的操作可以在小核上去做,可以节能,对手机来说续航是非常关键的;一些和用户体验非常相关的,比如游戏,可以在大核上运行
现在ARMv8上有大中小三种核,这样可以根据需要在不同cluster上执行。
–
对于处理器就会相关的时钟(Timers)定制器,这对调度是非常关键的
–
下面address decoding有一些地址的转化
里面的secure就比如TrustZone,是安全分区,把一些安全的敏感的操作放到TrustZone中运行。其中他的内存有安全的分区(Secure RAM),外设也有安全的分区(Secure peripheral),中断也有安全的划分(GIC:通用中断控制器)
Non-secure是正常运行的操作,比如说Android、ios运行的非安全分区的,里面也有相应的内存(RAM)、外设、系统控制器、网络等,通用对应有中断控制器,对中断进行管理
- 扩大物理寻址
CPU与指令集架构
ISA:指令集架构,是CPU和软件之间的桥梁
ISA包含指令集、特权级、寄存器、执行模式、安全扩展、性能加速扩展等方面
指令集
指令集是ISA的重要组成部分,通常包含一系列不同功能的指令
这里用到的硬件体系结构:AARCH64体系结构-ARM64位架构
AARCH64:ARM ARCH 64
指令集属于精简指令集计算机(RISC)
AArch64中每条指令的长度固定位4字节,指令类型包括:
- 数据搬移指令(如mov)
- 寄存器计算指令(add、sub)
- 内存读写指令(ldr、str)
- 跳转指令(无条件跳转b)
- 过程调用指令(调用指令bl、返回指令ret)
- 特权指令(读取系统寄存器指令msr、写入系统寄存器指令mrs)
ARMv8支持的执行模式
- Aarch64:支持A64指令集(实际支持32和64混跑)
- Aarch32:支持A32、T32、T16指令集
会出现情况:OS运行在64位的,但是应用运行在32位,甚至 里面还有16位的指令集
右边是华为的麒麟970开发板
Aarch64实现
- PC指向当前执行的指令
- 指令长度相同(RISC,32bit)
- PC会被跳转指令修改:B,BL,BX,BLX
寄存器ARM
与x86不同的是,ARM是一个RISC的指令集,他的通用寄存器是非常多的
- 31个64位通用寄存器
- X0-X30(其中X29用作帧指针FP寄存器,一般用于保存函数调用过程中栈顶的地址;X30用作链接指针LP寄存器,因为CPU在执行函数调用指令bl时会自动把返回地址保存在其中)
- 1个PC寄存器
- 4个栈寄存器(切换时保存SP)(SP:stack pointer)
- SP_EL0, SP_EL1, SP_EL2, SP_EL3
- 与X86不同的是,X86保存在栈上面,这里用了4个栈寄存器,同时是在不同的模式里面
- 实际是4个特征集:EL0是用户态的,EL1是操作系统的,EL2是虚拟机的,EL3是vmware等的
- 这样用4个特征集来保存SP,省去了把SP保存在栈上的操作
- 这样在做上下文切换的时候就要考虑如何保存和恢复SP
- 3个异常链接寄存器(保存异常的返回地址)
- ELR_EL1, ELR_EL2, ELR_EL3(没有EL0,因为这里EL0会跳到EL1中)
- 3个程序状态寄存器(切换时保存Program STATE)
- SPSR_EL1, SPSR_EL2, SPSR_EL3(这里也是3个特征集!)
对应到x86-64
- 16个通用寄存器
- 1个%rip寄存器–PC
- 1个%rsp寄存器–跨模式转换时需要压栈
返回地址压栈
EFLAGS–程序状态寄存器
X86架构中,切换特权级时rsp是如何保存,以及如何恢复的?
把相关的rsp保存到相关的栈中,返回时像函数调用一样将其恢复出来
这样相对来说就多了两次内存操作
在EL1特权级下有两个页表基地址寄存器(Translation Table Base Register,TTBR),即TTBR0_EL1和TTBR1_EL1,他们负责翻译虚拟地址空间中不同的地址段,负责的地址范围由另一个控制寄存器TCR_EL1(翻译控制寄存器,Translation Control Register)决定
另一种常见配置见下面“内存空间”所示
指令集
- RISC
- 固定长度指令格式
- 更多的通用寄存器
- Load/store结构(只有load和store访问内存,其他都不行)
- 简化寻址方式
- RISC vs CISC
ARM闻名是以低功耗著称的
RISC和CISC指令集的优劣
“技术出现时是有明显界限的,在发展后实际上两个是融合的”
CISC是先转化为内部的RISC指令集,再进行微处理结构的设计
所以在微处理结构设计角度来说,两者原理是相似的
RISC也在扩展自己的指令集
特权级
特权级也是ISA的重要组成部分
AArch64中的特权级被称为异常级别(Exception Level,EL),共有4种特权级
ARM(Exception Level)
- 非安全分区有4分层级
- EL0:用户态(应用的特权级),最低的特权级,应用程序通常运行在该特权级
- EL1:内核态(操作系统的),操作系统通常运行在该特权级
- EL2:hypervisor(虚拟机监控器,VMM),在虚拟化场景下使用
- EL3:monitor,和安全特性TrustZone有关,负责普通世界(normal world)和安全世界(secure world)之间的切换
TrustZone是从ARMv6体系结构开始引入的安全特性
该特性从逻辑上将整个系统分为安全世界和普通世界,计算资源可以被划分到这两个世界中
- 安全世界可以不受限制的访问所有计算资源
- 普通世界不能访问被划分到安全世界的计算资源
- 安全分区也有2个EL
- EL0
- EL1
- 最新的一些硬件实现也开始在TrustZone中增加EL2特权级
CPU在提供4种特权级的同时也提供了特权级之间切换的方式
- 一般从EL0切换到EL1的可能场景有三种:
- (同步的CPU特权级切换,由CPU中正在执行的指令导致的)应用程序需要调用操作系统提供的系统调用,此时应用程序会通过执行svc(特权调用,supervisor call)指令将CPU特权级从EL0切换到EL1
- (同步的CPU特权级切换)应用程序执行了一条指令而该指令触发了异常,该异常导致CPU特权级从EL0切换到EL1(如访存指令触发缺页异常)
- (异步的CPU特权级切换,不是由CPU中运行应用程序中的指令导致的)应用程序在执行过程中,CPU收到一个来自外设的中断,该中断也会导致CPU特权级从EL0切换到EL1
- 发生特权转换时,CPU保存当前执行状态,以便OS内核在处理异常时使用并在处理结束后能恢复应用程序的执行
- 触发异常的指令地址(当前程序计数器PC),保存在ELR_EL1(异常链接寄存器,Exception Link Register)
- 异常原因(异常时由于执行svc指令还是由于访问缺页导致的),保存在ESR_EL1(异常症状寄存器,Exception Syndrome Register)中
- CPU将栈指针(SP)从SP_EL0(应用程序使用的栈)切换到SP_EL1(OS可以通过设置这个寄存器配置处理过程中使用的栈)
- 其他信息如将CPU的相关状态保存在SPSR_EL1(已保存的程序状态寄存器,Saved Program Status Register),将引发缺页异常的地址保存在FAR_EL1(错误地址寄存器,Fault Address Register)
- 处理完毕后调用eret指令进行返回
- ELR_EL1 -> PC恢复PC状态
- SPSR_EL1 -> PSTATE 恢复处理器状态
- 降至EL0,硬件自动使用SP_EL0作为栈指针
- 恢复执行
X84-64
- Non-root(跑虚拟机,包括启动操作系统和应用)
- Ring 0:Guest app
- Ring 3:Guest os
- Root(虚拟机监控器)
- Ring 0:App
- Ring 3:Hypervisor
系统状态寄存器
X86-64
用户态和操作系统看到的EFLAG是不一样的
ARM
- 抽象进程状态信息(PSTATE)
- 条件标记(Condition flags)
- 执行状态(Execution Statement controls)
- 异常掩码(Exception mask bits)
- 访问,时钟控制(Access control bits)
系统控制寄存器(control register)
x86-64
- CR0:整体控制
- CR1:在上图没有用到
- CR2:发生Page-Fault后知道从哪个寄存器中获取page-fault的地址
- CR3:页表的根目
- CR4:一系列的状态位
ARM
- 对系统的顶层控制
大小端、使用MMU、检查Tag、内存系统- 大端:传统网络处理用的比较多,但现在在往小端归一
- 是否开MMU等可以在状态位上得到体现
内存系统相关寄存器
-
TCR:Translation Control Register (页表翻译控制的寄存器,来控制如何对页表进行管理)
-
TTBR:Translation Table Base Register(对应X86-64的CR3)
Aarch64的TTBR0支持(0~248-1)的地址映射,TTBR1支持(248 ~264 )的地址映射,这样的硬件设计与x86-64中的CR3相比较,能够如何协助到操作系统的设计?
- X86的CR3,如果只有一个页表的话,kernel和用户态实际上是在不同的地址空间的,这时如果出现user到kernel的转换的话,就会有一些切换页表的过程
- 如果对不同的特权级用不同的列表,那么就不用再做页表的切换
地址翻译
内存空间
上图是对应内存空间的划分
注意看kernel space 和user space的地址空间
Kernel space和user space的中间有个空洞,为了防止kernel的地址空间和user的地址空间出现非法映射。用户的space在比较低的位置,kernel的space在比较高的位置,这样就把两部分分开来了
这里看到的映射是TTBR1_EL1对应上面的映射,TTBR0_EL0对应下面的映射
ARM输入/输出
-
MMIO与PIO
- MMIO(Memory-mapped IO)
- 将设备映射到连续的物理内存中,使用相同的指令去操作
- 如,Raspi3映射到0x3F200000
- 行为与内存不完全一样,读写会有副作用
- PIO(Port IO)
- IO设备具有独立的地址空间
- 使用特殊的指令(如x86中的in/out指令)
- MMIO(Memory-mapped IO)
-
MMIO(memory map IO)
- 复用ldr和str指令
- 映射到物理内存的特殊地址段
-
如下代码对外设进行操作
unsigned int early_uart_recv(void) // 对uart进行接收操作(收某个字符) { while(1) // 等他的状态位是ready了 { if (early_uart_lsr() & 0x01) // 进这里是不ready break; } return early_get32(AUX_MU_IO_REG) & 0XFF; // 再将里面的内容(比如是AUX_MU_IO_REG)读出 } void early_uart_send(unsigned int c) { while(1){ //等可以接受数据的时候 if (early_uart_lsr() & 0x20) break; } early_put32(AUX_MU_IO_REG,c); //再接受数据 }
- UART (Universal Asynchronous Receiver/Transmitter,通用异步收发器) 是一种用于实现串行通信的硬件接口。UART 允许设备通过串行数据链路发送和接收数据,常用于微控制器与外围设备之间的通信
- 里面early_get32对应下来的话就是用MMIO实现的,用的是load指令ldr
BEGIN_FUNC(early_get32) ldr w0, [x0] ret END_FUNC(early_get32)
- early_put32也是MMIO,用的str
BEGIN_FUNC(early_put32) str w1,[x0] ret END_FUNC(early_put32)
操作系统启动过程
以ChCore为例
设备上电后…
- OS如何启动
- 不同ARM设备启动OS的方式不同
- Raspi 3:CPU进行初始化后从SD卡上加载
- Hikey970:通过BL(Bootloader)链和UEFI加载
- OS启动时进行硬件初始化工作,并开启页表
- 进入内核
- 不同ARM设备启动OS的方式不同
- ChCore Bootliader
- Bootloader和kernel放在同一个ELF文件中
- Bootloader位于.init段,并通过链接器设置入口
- Kernel位于.text段
可以知道入口是这个_startset_property( TARFER kernel.img APPEND_STRING PROPERTY LINK_FLAGS "-T ${CMAKE_CURRENT_BINARY_DIR}/${link_script} -e _start" )
- 主CPU启动,其他次CPU等待
BEGIN_FUNC(_start) mrs x8, mpidr_el1 mov x9, #0xc1000000 bic x8, x8, x9 cbz x8, primary
- Bootloader和kernel放在同一个ELF文件中
层级切换
因为OS是运行在EL1的,所以要从其他特权级跳转到EL1特权级
- 其他EL(>=1)到EL1
Lno_gic_sr: // Set EL1 to 64 bit.设置EL1是64位的 mov x9,HCR_EL2_RW msr hcr_el2, x9 // Set the return address and exception level.设置返回地址和Exception level adr x9, .Ltarget msr elr_el2, x9 mov x9, #(SPSR_ELX_DAIF | SPSR_ELX_EL1H) msr spsr_el2, x9 isb //类似于内存屏障的操作 eret //返回 .Ltarget: ret // 要注意在ARM 中写汇编/底层代码时要注意他是一个弱内存序的体系结构 // 要保证在预return之前要保证前面这些指令都已经commit以后才可以return
准备函数栈和异常向量
- 准备C函数栈
- 设置SP寄存器
- 设置异常向量
- 为了debug
初始化UART
通过UART协议进行输出
- 根据UART协议进行内存空间初始化
- 映射到IO的内存空间
- 映射到IO的内存空间
- AUX那些是对应UART里面的一些寄存器,这里是对他进行一些设置
- 设置好以后进行一下delay,使上面的操作能够生效(因为硬件与软件不同,写了这个指令之后不一定马上生效)
初始化页表并开启MMU
- 将Kernel代码映射到低地址段(和物理地址相同)和高地址段两份
这里实际上做了两件事:将Kernel的代码同时映射到了低的地址段和高的地址段
这是为了方便启动的时候可以在两个地方同时访问,但是结束后会将低的地址段去掉
进入Kernel
- 跳转到Kernel的main函数
- main函数中是对一些功能与核的初始化
初始化UART、Exception vector、内存、锁、调度器、线程,再把其他的核启动起来,把pmu(相关的性能控制单元)设置起来
- main函数中是对一些功能与核的初始化
- 真正进入ChCore
- 开启OS的各种服务
X86-64的启动过程
(与arm流程差不多)
- 为了向前兼容,内核启动中存在兼容性工作
- 通过段寄存器,进行模式转换(兼容需要)
- 实模式(16-bit)
- 保护模式(32-bit)
- IA32E(64bit)
- 由于实模式的内存限制(64KB),再将内核代码按段拷贝到内存中
- 进入内核
硬件模拟
现在模拟器实现,有需要再加载硬件
使用模拟器进行开发
用软件模拟硬件行为
- ARM模拟器
- 仅用软件完全实现硬件行为
- 如同泡在真正的ARM硬件上
模拟CPU
无限循环,根据opcode进行相应操作
模拟寄存器
模拟内存
模拟设备
- 硬盘:host OS的文件
- VGA显示:host OS的显示窗口
- 键盘:host OS的键盘API
- 时钟芯片:host OS的时钟
- …
物理内存与CPU缓存
- 为什么要引入CPU缓存
相比于CPU处理的速度,内存访问速度非常慢:一条算术运算指令可能只需要一个or几个时钟周期,而访存指令可能需要上百个时钟周期
为了降低CPU访存的开销,现在的CPU中引入缓存,用于存放一部分物理内存中的数据
缓存结构
CPU缓存由若干个缓存行(cache line)组成的
- 每个缓存行包括:
- 一个有效位(valid bit):表示其是否有效
- 一个标记地址(tag address):标识其对应的物理地址
- 一些其他的状态信息
通常CPU以缓存行(常见是64字节)为单位把物理内存中的数据读取到CPU缓存中/写回物理内存
缓存地址在逻辑上可以分为:Tag、Index(set)、Offset三段
- 组(Set)与路(Way)是CPU缓存的经典概念
- 组:物理地址中的Set段能表示的最大数目(如Set段8位,则对应CPU缓存的组数为28 =256)
- 路:同一组(Set段相等)下支持的最大的Tag数->同一组下的缓存行数目