硬件结构
硬件结构
操作系统建立在计算机之上,依托于计算机的众多硬件运行,其中最主要的两个硬件就是 CPU 和内存,此外还有显卡,网卡,硬盘,键盘,鼠标等等
冯诺依曼模型将计算机基本结构定义为运算器、控制器、存储器、输入设备、输出设备 5 个部分
CPU
CPU 包括寄存器、控制单元和逻辑运算单元等
为了知道新任务从哪加载,又从哪开始运行,操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器
CPU 寄存器是 CPU 内部一个容量小,但是速度极快的内存
程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置
CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务
32 位 VS 64 位
硬件的 64 位和 32 位指的是 CPU 的位宽,软件的 64 位和 32 位指的是指令的位宽
32 位和 64 位 CPU 的区别
- 一次计算数据的字节数不同(最主要)
前者一次能计算 4 byte(32bit)的数据,而后者可以计算 8 byte(64 bit) - 计算范围不同
64 位 CPU 可以一次性算出加和两个 64 位数字的结果,而 32 位需要把这 2 个 64 位的数字分成 2 个低位 32 位数字和 2 个高位 32 位数字来计算
在计算的数字不超过 32 位的情况下,32 位和 64 位 CPU 之间没什么区别 - 寻址范围不同
32 位 CPU 最大寻址范围仅为4GB,而 64 位 CPU 理论最大的寻址空间为2^64bit
64 位/32 位软件表示其 CPU 指令是 64 位还是 32 位的
32 位指令可以在 64 位机器上执行,但 64 位指令不能在 32 位机器上执行,因为 32 位的寄存器存不下 64 位的指令
寄存器
CPU 中的寄存器主要作用是存储计算时的数据,相比于内存,寄存器就位于 CPU 中,因此速度更快
寄存器一般要求在半个 CPU 时钟周期内完成读写
常见寄存器种类:
- 通用寄存器,用来存放需要进行运算的数据
- 程序计数器,用来存储 CPU 要执行下一条指令所在的内存地址
- 指令寄存器,用来存放当前正在执行的指令本身
CPU Cache
CPU Cache 使用 SRAM(Static Random-Access Memory,静态随机存储器) 芯片
CPU Cache 通常可以分为 L1、L2、L3 三层高速缓存,也称为一级缓存、二级缓存、三级缓存
- L1 Cache 通常分成指令缓存和数据缓存,每个 CPU 核心都有一块属于自己的 L1 Cache,访问速度通常只需要
2~4个时钟周期 - L2 Cache 同样为每个 CPU 核心独立拥有,访问速度在
10~20个时钟周期 - L3 Cache 通常多个 CPU 核心共用,访问速度在
20~60个时钟周期
CPU Cache 由多个 Cache Line 组成
Cache Line 是 CPU 从内存读取数据的基本单位,由标志(Tag) + 数据块(Data Block)组成
直接映射 Cache
使用取模运算把内存块的地址始终映射在同一个 CPU Cache Line 中,通过 Tag 区分模相同的内存块,此外 Cache 中存在有效位(Valid Bit),当有效位为 0 时,CPU 跳过 Cache 直接从内存中加载数据
如果内存中的数据已经在 CPU Cache 中了,那 CPU 访问一个内存地址的时候,会经历这 4 个步骤:
- 根据内存地址中索引信息,计算在 CPU Cache 中的索引,也就是找出对应的 CPU Cache Line 的地址
- 找到对应 CPU Cache Line 后,判断 CPU Cache Line 中的有效位,如果是无效的,CPU 就会直接访问内存,并重新加载数据
- 对比内存地址中和 CPU Cache Line 中的组标记,确认 CPU Cache Line 中的数据是我们要访问的内存数据,如果不是的话,CPU 就会直接访问内存,并重新加载数据
- 根据内存地址中偏移量信息,从 CPU Cache Line 的数据块中,读取对应的字
缓存一致性
写直达:把数据同时写入内存和 Cache 中
写回:当发生写操作时,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Block 被替换时才需要写到内存中
由于 L1 Cache 和 L2 Cache 通常不共有,采用写回策略时多核 CPU 存在缓存一致性问题
解决方法:
- 写传播:某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache
- 事务串行化:某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的
CPU 通过 MESI 协议实现了缓存一致性
MESI 是 4 个状态单词的开头字母缩写
- Modified,已修改
- Exclusive,独占
- Shared,共享
- Invalidated,已失效
伪共享
多个线程同时读写同一个 Cache Line 中的不同变量而导致 CPU Cache 失效
解决方法:空间换时间,使竞争激烈的同一个 Cache Line 中的不同变量处于不同的 Cache Line 中
指令
指令的内容是一串二进制数字的机器码,每条指令都有对应的机器码,CPU 通过解析机器码来知道指令的内容
一条指令通常分为 4 个阶段,称为 4 级流水线,又称为指令周期(Instrution Cycle),CPU 的工作就是一个周期接着一个周期,周而复始
Fetch(取得指令)- CPU 通过程序计数器读取对应内存地址的指令
Decode(指令译码)- CPU 对指令进行解码
Execution(执行指令)- CPU 执行指令
Store(数据回写)- CPU 将计算结果存回寄存器或者将寄存器的值存入内存
指令类型
指令执行速度
任务调度
Linux 内核中进程和线程都用 task_struct 结构体表示,因此 Linux 内核中的调度器针对 task_struct 进行调度
Linux 系统中,根据任务的优先级以及响应要求,主要分为两种,其中优先级的数值越小,优先级越高:
- 实时任务,对系统的响应时间要求很高,也就是要尽可能快的执行实时任务,优先级在 0~99 范围内
- 普通任务,响应时间没有很高的要求,优先级在 100~139 范围内
每个 CPU 都有自己的运行队列(Run Queue, rq),用于描述在此 CPU 上所运行的所有进程,包含三个运行队列:Deadline 运行队列 dl_rq、实时任务运行队列 rt_rq 和 CFS 运行队列 cfs_rq
| 调度类 | 调度器 | 调度策略 | 运行队列 |
|---|---|---|---|
| Deadline | Deadline调度器 | SCHED_DEADLINE | dl_rq |
| Realtime | RT 调度器 | SCHED_FIFO SCHED_RR |
rt_rq |
| Fair | CFS 调度器 | SCHED_NORMAL SCHED_BATCH |
cfs_rq |
Deadline 和 Realtime 应用于实时任务
- SCHED_DEADLINE:按照 deadline 进行调度的,deadline距离当前时间点最近的任务会被优先调度
- SCHED_FIFO:相同优先级的任务先来先服务,优先级更高的任务可以抢占低优先级的任务
- SCHED_RR:相同优先级的任务按照时间片轮流运行,高优先级的任务依然可以抢占低优先级的任务
Fair 应用于普通任务
- SCHED_NORMAL:普通任务使用的调度策略
- SCHED_BATCH:后台任务的调度策略,不和终端进行交互,因此在不影响其他需要交互的任务的情况下可适当降低它的优先级
Linux 内核按照Deadline > Realtime > Fair 优先级顺序从运行队列中选择任务执行
因此,实时任务总是会比普通任务优先被执行
完全公平调度
Linux 中 CFS 调度器采用完全公平调度算法,通过每个任务的虚拟运行时间 vruntime 进行调度
`vruntime = min_vruntime + (exec_time * weight / priority)
min_vruntime 是一个常数,表示当前进程在调度队列中的最小虚拟运行时间
exec_time 表示进程在 CPU 上执行的时间
weight 是进程的权重,priority 是进程的优先级
cfs_rq 用红黑树存储,按 vruntime 大小排列
优先级调整
可以通过调整任务的 nice 值调整其优先级
nice 值范围为 -20~19,priority = default_priority+nice
总线
总线用于 CPU 和内存以及其他设备之间的通信
总线分为 3 种:
- 地址总线,用于指定 CPU 将要操作的内存地址
- 数据总线,用于读写内存的数据
- 控制总线,用于发送和接收信号,比如中断、设备复位等信号,CPU 收到信号后自然进行响应,这时也需要控制总线
CPU 要读写内存数据时:
- 首先通过地址总线指定内存地址
- 然后通过控制总线控制是读或写命令
- 最后通过数据总线传输数据
存储
内存
内存用于存储运行的程序和数据
数据段:存放程序运行时数据
正文段:存放指令
内存速度大概在 200~300 个时钟周期之间
硬盘
设备控制
设备控制器
CPU 通过设备控制器与设备通信
设备控制器中有芯片和寄存器,CPU 通过读写设备控制器中的寄存器控制设备
设备控制器有三类寄存器:
- 数据寄存器:记录 CPU 传入的数据
- 命令寄存器:记录 CPU 命令
- 状态寄存器:通知 CPU 当前是否已在执行任务
输入输出设备可分为两大类 :
- 块设备:数据存储在固定大小的块中,每个块有自己的地址,e.g. 硬盘、USB
- 块设备存在一个可读写的数据缓冲区用于减少对设备的频繁操作
- 字符设备:以字符为单位发送或接收一个字符流,不可寻址的,也没有寻道操作,e.g. 鼠标
IO 控制方式
CPU 通过两种方式与设备通信:
- 端口 IO:每个控制寄存器被分配一个 IO 端口,可以通过特殊的汇编指令操作这些寄存器
- 内存映射 IO:将所有控制寄存器映射到内存空间中,像读写内存一样读写数据缓冲区
设备通过中断与 CPU 通信,硬件通常有一个中断控制器,当设备完成任务后触发中断到中断控制器,CPU 中断当前进程转而处理中断
设备可通过DMA(Direct Memory Access) 功能自行把 IO 数据放入内存
具体流程:
CPU 仅在开始时向 DMA 发送指令,DMA 在数据拷贝完成后触发中断通知 CPU
驱动程序
设备控制器属于硬件,而设备驱动程序属于操作系统
不同的设备控制器功能不同,但设备驱动程序提供统一接口给操作系统,使不同设备以相同方式接入操作系统
设备驱动程序中包含该设备的中断处理函数,CPU 响应设备中断后调用该函数
通用块层
Linux 通过统一的通用块层来管理不同的块设备
通用块层是处于文件系统和磁盘驱动中间的一个块设备抽象层,主要有两个功能:
- 向上为文件系统和应用程序,提供访问块设备的标准接口,向下把各种不同的磁盘设备抽象为统一的块设备,并在内核层面,提供一个框架来管理这些设备的驱动程序
- 给文件系统和应用程序发来的 I/O 请求排队,接着会对队列进行重新排序、请求合并等 IO 调度,以提高磁盘读写的效率
Linux 存在五种 IO 调度算法:
- 没有调度算法
- 虚拟机IO
- 先入先出调度算法:先进入 IO 调度队列的 IO 请求先发生
- 完全公平调度算法:为每个进程维护一个 IO 调度队列,按照时间片均匀分布每个进程的 IO 请求
- 大部分系统的默认算法
- 优先级调度:优先级高的 IO 请求先发生
- 适用于运行大量进程的系统
- 最终期限调度算法:分别为读写请求创建 IO 队列,同时确保达到最终期限的请求被优先处理
- 适用于 IO 压力较大的场景
IO 软件分层
Linux 存储系统的 I/O 由上到下可以分为三个层次:
- 文件系统层:包括虚拟文件系统和其他文件系统的具体实现,向上为应用程序统一提供了标准的文件访问接口,向下会通过通用块层来存储和管理磁盘数据。
- 通用块层:包括块设备的 IO 队列和 IO 调度器,对文件系统的 I/O 请求进行排队,再通过 IO 调度器,选择一个 IO 发给下一层的设备层。
- 设备层:包括硬件设备、设备控制器和驱动程序,负责最终物理设备的 I/O 操作
存储系统的 I/O 是整个系统最慢的一个环节,所以 Linux 提供了不少缓存机制来提高 I/O 的效率:
- 为了提高文件访问的效率,会使用页缓存、索引节点缓存、目录项缓存等多种缓存机制,目的是为了减少对块设备的直接调用
- 为了提高块设备的访问效率,会使用缓冲区来缓存块设备的数据
完整流程(键盘输入到屏幕显示)
- 用户输入了键盘字符,键盘控制器就会产生扫描码数据,并将其缓冲在键盘控制器的寄存器中
- 键盘控制器通过总线给 CPU 发送中断请求
- CPU 收到中断请求后,操作系统保存被中断进程的 CPU 上下文,然后调用键盘的中断处理程序(在键盘驱动程序初始化时注册)
- 键盘中断处理程序从键盘控制器的寄存器的缓冲区读取扫描码,再根据扫描码找到用户在键盘输入的字符,如果输入的字符是显示字符,那就会把扫描码翻译成对应显示字符的 ASCII 码
- 得到了显示字符的 ASCII 码后,把 ASCII 码放到读缓冲区队列
- 显示设备的驱动程序定时从读缓冲区队列读取数据放到写缓冲区队列,然后把写缓冲区队列的数据写入到显示设备控制器的寄存器中的数据缓冲区,最后将这些数据显示在屏幕里
- 显示出结果后,恢复被中断进程的上下文








